diff --git a/.gitignore b/.gitignore index faeda6aac4..fe46bdbac3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,12 @@ -fastlane/Appfile -fastlane/Fastfile fastlane/README.md fastlane/report.xml -fastlane/actions/hockey_sparkle.rb -fastlane/sparkle/dsa_priv.pem -fastlane/sparkle/dsa_pub.pem -fastlane/sparkle/sign_update -Telegram-Mac/Config.swift +xcuserdata +xcuserstate +.DS_Store +core-xprojects/Mozjpeg/build +core-xprojects/OpenSSLEncryption/build +core-xprojects/libwebp/build +core-xprojects/libopus/build +core-xprojects/ffmpeg/build +core-xprojects/webrtc/build +core-xprojects/webrtc/build-debug \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000..45cdbc04b4 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,64 @@ +before_script: + - bash ./buildbox/sync-toolbox.sh + +stages: + - deploy + +variables: + LANG: "en_US.UTF-8" + LC_ALL: "en_US.UTF-8" + GIT_SUBMODULE_STRATEGY: recursive + GIT_STRATEGY: fetch + + +beta: + tags: + - beta + stage: deploy + only: + - beta + except: + - tags + script: + - bash ./buildbox/internal.sh beta + environment: + name: beta + +alpha: + tags: + - alpha + stage: deploy + only: + - alpha + except: + - tags + script: + - bash ./buildbox/internal.sh alpha + environment: + name: alpha + +appstore: + tags: + - appstore + stage: deploy + only: + - appstore + except: + - tags + script: + - bash ./buildbox/internal.sh appstore + environment: + name: appstore + +release: + tags: + - release + stage: deploy + only: + - release + except: + - tags + script: + - bash ./buildbox/internal.sh release + environment: + name: release diff --git a/.gitmodules b/.gitmodules index affda3b991..916cfbac8f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,18 +1,18 @@ -[submodule "submodules/Postbox"] - path = submodules/Postbox - url = https://github.com/peter-iakovlev/Postbox.git -[submodule "submodules/TelegramCore"] - path = submodules/TelegramCore - url = https://github.com/peter-iakovlev/TelegramCore.git -[submodule "Signals"] - path = Signals - url = https://github.com/peter-iakovlev/Signals.git -[submodule "submodules/Signals"] - path = submodules/Signals - url = https://github.com/peter-iakovlev/Signals.git -[submodule "submodules/MtProtoKit"] - path = submodules/MtProtoKit - url = https://github.com/peter-iakovlev/MtProtoKit.git [submodule "submodules/libtgvoip"] path = submodules/libtgvoip - url = https://github.com/grishka/libtgvoip +url=git@github.com:telegramdesktop/libtgvoip +[submodule "submodules/Sparkle"] + path = submodules/Sparkle +url=https://github.com/overtake/Sparkle +[submodule "submodules/telegram-ios"] + path = submodules/telegram-ios +url=git@github.com:overtake/Telegram-iOS.git +[submodule "submodules/rlottie"] + path = submodules/rlottie +url=git@github.com:overtake/rlottie.git +[submodule "submodules/tgcalls"] + path = submodules/tgcalls + url = git@github.com:john-preston/tgcalls.git +[submodule "submodules/tg_owt"] + path = submodules/tg_owt + url = git@github.com:desktop-app/tg_owt.git diff --git a/HockeySDK.framework/Headers b/HockeySDK.framework/Headers deleted file mode 120000 index a177d2a6b9..0000000000 --- a/HockeySDK.framework/Headers +++ /dev/null @@ -1 +0,0 @@ -Versions/Current/Headers \ No newline at end of file diff --git a/HockeySDK.framework/HockeySDK b/HockeySDK.framework/HockeySDK deleted file mode 120000 index 8b45692e61..0000000000 --- a/HockeySDK.framework/HockeySDK +++ /dev/null @@ -1 +0,0 @@ -Versions/Current/HockeySDK \ No newline at end of file diff --git a/HockeySDK.framework/Modules b/HockeySDK.framework/Modules deleted file mode 120000 index 5736f3186e..0000000000 --- a/HockeySDK.framework/Modules +++ /dev/null @@ -1 +0,0 @@ -Versions/Current/Modules \ No newline at end of file diff --git a/HockeySDK.framework/PrivateHeaders b/HockeySDK.framework/PrivateHeaders deleted file mode 120000 index d8e5645269..0000000000 --- a/HockeySDK.framework/PrivateHeaders +++ /dev/null @@ -1 +0,0 @@ -Versions/Current/PrivateHeaders \ No newline at end of file diff --git a/HockeySDK.framework/Resources b/HockeySDK.framework/Resources deleted file mode 120000 index 953ee36f3b..0000000000 --- a/HockeySDK.framework/Resources +++ /dev/null @@ -1 +0,0 @@ -Versions/Current/Resources \ No newline at end of file diff --git a/HockeySDK.framework/Versions/A/Headers/BITCrashDetails.h b/HockeySDK.framework/Versions/A/Headers/BITCrashDetails.h deleted file mode 100644 index a6ceecd83d..0000000000 --- a/HockeySDK.framework/Versions/A/Headers/BITCrashDetails.h +++ /dev/null @@ -1,73 +0,0 @@ -#import - -/** - * Provides details about the crash that occured in the previous app session - */ -@interface BITCrashDetails : NSObject - -/** - * UUID for the crash report - */ -@property (nonatomic, readonly, copy) NSString *incidentIdentifier; - -/** - * UUID for the app installation on the device - */ -@property (nonatomic, readonly, copy) NSString *reporterKey; - -/** - * Signal that caused the crash - */ -@property (nonatomic, readonly, copy) NSString *signal; - -/** - * Exception name that triggered the crash, nil if the crash was not caused by an exception - */ -@property (nonatomic, readonly, copy) NSString *exceptionName; - -/** - * Exception reason, nil if the crash was not caused by an exception - */ -@property (nonatomic, readonly, copy) NSString *exceptionReason; - -/** - * Date and time the app started, nil if unknown - */ -@property (nonatomic, readonly, copy) NSDate *appStartTime; - -/** - * Date and time the crash occured, nil if unknown - */ -@property (nonatomic, readonly, copy) NSDate *crashTime; - -/** - * Operation System version string the app was running on when it crashed. - */ -@property (nonatomic, readonly, copy) NSString *osVersion; - -/** - * Operation System build string the app was running on when it crashed - * - * This may be unavailable. - */ -@property (nonatomic, readonly, copy) NSString *osBuild; - -/** - * CFBundleShortVersionString value of the app that crashed - * - * Can be `nil` if the crash was captured with an older version of the SDK - * or if the app doesn't set the value. - */ -@property (nonatomic, readonly, copy) NSString *appVersion; - -/** - * CFBundleVersion value of the app that crashed - */ -@property (nonatomic, readonly, copy) NSString *appBuild; - -/** - * Identifier of the app process that crashed - */ -@property (nonatomic, readonly, assign) NSUInteger appProcessIdentifier; - -@end diff --git a/HockeySDK.framework/Versions/A/Headers/BITCrashExceptionApplication.h b/HockeySDK.framework/Versions/A/Headers/BITCrashExceptionApplication.h deleted file mode 100644 index 6c55e9a0dc..0000000000 --- a/HockeySDK.framework/Versions/A/Headers/BITCrashExceptionApplication.h +++ /dev/null @@ -1,127 +0,0 @@ -#import - -/** - * `NSApplication` subclass to catch additional exceptions - * - * On OS X runtime not all uncaught exceptions do end in an custom `NSUncaughtExceptionHandler`. - * In addition "sometimes" exceptions don't even cause the app to crash, depending on where and - * when they happen. - * - * Here are the known scenarios: - * - * 1. Custom `NSUncaughtExceptionHandler` don't start working until after `NSApplication` has finished - * calling all of its delegate methods! - * - * Example: - * - (void)applicationDidFinishLaunching:(NSNotification *)note { - * ... - * [NSException raise:@"ExceptionAtStartup" format:@"This will not be recognized!"]; - * ... - * } - * - * - * 2. The default `NSUncaughtExceptionHandler` in `NSApplication` only logs exceptions to the console and - * ends their processing. Resulting in exceptions that occur in the `NSApplication` "scope" not - * occurring in a registered custom `NSUncaughtExceptionHandler`. - * - * Example: - * - (void)applicationDidFinishLaunching:(NSNotification *)note { - * ... - * [self performSelector:@selector(delayedException) withObject:nil afterDelay:5]; - * ... - * } - * - * - (void)delayedException { - * NSArray *array = [NSArray array]; - * [array objectAtIndex:23]; - * } - * - * 3. Any exceptions occurring in IBAction or other GUI does not even reach the NSApplication default - * UncaughtExceptionHandler. - * - * Example: - * - (IBAction)doExceptionCrash:(id)sender { - * NSArray *array = [NSArray array]; - * [array objectAtIndex:23]; - * } - * - * - * Solution A: - * - * Implement `NSExceptionHandler` and set the `ExceptionHandlingMask` to `NSLogAndHandleEveryExceptionMask` - * - * Benefits: - * - * 1. Solves all of the above scenarios - * - * 2. Clean solution using a standard Cocoa System specifically meant for this purpose. - * - * 3. Safe. Doesn't use private API. - * - * Problems: - * - * 1. To catch all exceptions the `NSExceptionHandlers` mask has to include `NSLogOtherExceptionMask` and - * `NSHandleOtherExceptionMask`. But this will result in @catch blocks to be called after the exception - * handler processed the exception and likely lets the app crash and create a crash report. - * This makes the @catch block basically not working at all. - * - * 2. If anywhere in the app a custom `NSUncaughtExceptionHandler` will be registered, e.g. in a closed source - * library the develop has to use, the complete mechanism will stop working - * - * 3. Not clear if this solves all scenarios there can be. - * - * 4. Requires to adjust PLCrashReporter not to register its `NSUncaughtExceptionHandler` which is not a good idea, - * since it would require the `NSExceptionHandler` would catch *all* exceptions and that would cause - * PLCrashReporter to stop all running threads every time an exception occurs even if will be handled right - * away, e.g. by a system framework. - * - * - * Solution B: - * - * Overwrite and extend specific methods of `NSApplication`. Can be implemented via subclassing NSApplication or - * by using a category. - * - * Benefits: - * - * 1. Solves scenarios 2 (by overwriting `reportException:`) and 3 (by overwriting `sendEvent:`) - * - * 2. Subclassing approach isn't enforcing the mechanism onto apps and let developers opt-in. - * (Category approach would enforce it and rather be a problem of this soltuion.) - * - * 3. Safe. Doesn't use private API. - * - * Problems: - * - * 1. Does not automatically solve scenario 1. Developer would have to put all that code into @try @catch blocks - * - * 2. Not a clean implementation, rather feels like a workaround. - * - * 3. Not clear if this solves all scenarios there can be. - * - * - * Chosen Solution: B via subclassing - * - * Reasons: - * - * 1. The Problems 1. and 2. of Solution A are too drastic and aren't acceptable for every developer using this SDK - * Especially Problem 1 is a big No Go for lots of developers. - * - * 2. Solution B can be used optionally, can be adopted easily into developers own `NSApplication` subclasses and - * by implementing it in a subclass instead of a category isn't enforced even though it requires additional - * steps for setup. - * - * 3. The not covered Scenario 1. can be achieved by the developer by enclosing most of the code within - * NSApplication startup delegates in @try @catch blocks or moving as much code as possible out of these - * methods and deferring their execution, e.g. using background threads. Not ideal though. - * - * - * References: - * https://developer.apple.com/library/mac/documentation/cocoa/Conceptual/Exceptions/Tasks/ControllingAppResponse.html#//apple_ref/doc/uid/20000473-BBCHGJIJ - * http://stackoverflow.com/a/4199717/474794 - * http://stackoverflow.com/a/3419073/474794 - * http://macdevcenter.com/pub/a/mac/2007/07/31/understanding-exceptions-and-handlers-in-cocoa.html - * - */ -@interface BITCrashExceptionApplication : NSApplication - -@end diff --git a/HockeySDK.framework/Versions/A/Headers/BITCrashManager.h b/HockeySDK.framework/Versions/A/Headers/BITCrashManager.h deleted file mode 100644 index 88cc0f004e..0000000000 --- a/HockeySDK.framework/Versions/A/Headers/BITCrashManager.h +++ /dev/null @@ -1,279 +0,0 @@ -#import - -#import "BITHockeyBaseManager.h" - -// flags if the crashreporter is activated at all -// set this as bool in user defaults e.g. in the settings, if you want to let the user be able to deactivate it -#define kHockeySDKCrashReportActivated @"HockeySDKCrashReportActivated" - -// flags if the crashreporter should automatically send crashes without asking the user again -// set this as bool in user defaults e.g. in the settings, if you want to let the user be able to set this on or off -// or set it on runtime using the `autoSubmitCrashReport property` -#define kHockeySDKAutomaticallySendCrashReports @"HockeySDKAutomaticallySendCrashReports" - -@protocol BITCrashManagerDelegate; - -@class BITCrashDetails; -@class BITCrashMetaData; -@class BITCrashReportUI; - - -/** - * Custom block that handles the alert that prompts the user whether he wants to send crash reports - * - * @param crashReportText The textual representation of the crash report - * @param applicationLog The application log that will be attached to the crash report - */ -typedef void(^BITCustomCrashReportUIHandler)(NSString *crashReportText, NSString *applicationLog); - - -/** - * Prototype of a callback function used to execute additional user code. Called upon completion of crash - * handling, after the crash report has been written to disk. - * - * @param context The API client's supplied context value. - * - * @see `BITCrashManagerCallbacks` - * @see `[BITCrashManager setCrashCallbacks:]` - */ -typedef void (*BITCrashManagerPostCrashSignalCallback)(void *context); - -/** - * This structure contains callbacks supported by `BITCrashManager` to allow the host application to perform - * additional tasks prior to program termination after a crash has occured. - * - * @see `BITCrashManagerPostCrashSignalCallback` - * @see `[BITCrashManager setCrashCallbacks:]` - */ -typedef struct BITCrashManagerCallbacks { - /** An arbitrary user-supplied context value. This value may be NULL. */ - void *context; - - /** - * The callback used to report caught signal information. - */ - BITCrashManagerPostCrashSignalCallback handleSignal; -} BITCrashManagerCallbacks; - -/** - * Crash Manager alert user input - */ -typedef NS_ENUM(NSUInteger, BITCrashManagerUserInput) { - /** - * User chose not to send the crash report - */ - BITCrashManagerUserInputDontSend = 0, - /** - * User wants the crash report to be sent - */ - BITCrashManagerUserInputSend = 1, - /** - * User chose to always send crash reports - */ - BITCrashManagerUserInputAlwaysSend = 2 - -}; - - -/** - * The crash reporting module. - * - * This is the HockeySDK module for handling crash reports, including when distributed via the App Store. - * As a foundation it is using the open source, reliable and async-safe crash reporting framework - * [PLCrashReporter](https://www.plcrashreporter.org). - * - * This module works as a wrapper around the underlying crash reporting framework and provides functionality to - * detect new crashes, queues them if networking is not available, present a user interface to approve sending - * the reports to the HockeyApp servers and more. - * - * It also provides options to add additional meta information to each crash report, like `userName`, `userEmail`, - * additional textual log information via `BITCrashanagerDelegate` protocol and a way to detect startup - * crashes so you can adjust your startup process to get these crash reports too and delay your app initialization. - * - * Crashes are send the next time the app starts. If `autoSubmitCrashReport` is enabled, crashes will be send - * without any user interaction, otherwise an alert will appear allowing the users to decide whether they want - * to send the report or not. This module is not sending the reports right when the crash happens - * deliberately, because if is not safe to implement such a mechanism while being async-safe (any Objective-C code - * is _NOT_ async-safe!) and not causing more danger like a deadlock of the device, than helping. We found that users - * do start the app again because most don't know what happened, and you will get by far most of the reports. - * - * Sending the reports on startup is done asynchronously (non-blocking) if the crash happened outside of the - * time defined in `maxTimeIntervalOfCrashForReturnMainApplicationDelay`. - * - * More background information on this topic can be found in the following blog post by Landon Fuller, the - * developer of [PLCrashReporter](https://www.plcrashreporter.org), about writing reliable and - * safe crash reporting: [Reliable Crash Reporting](http://goo.gl/WvTBR) - * - * @warning If you start the app with the Xcode debugger attached, detecting crashes will _NOT_ be enabled! - */ -@interface BITCrashManager : BITHockeyBaseManager - - -///----------------------------------------------------------------------------- -/// @name Configuration -///----------------------------------------------------------------------------- - -/** - * Defines if the build in crash report UI should ask for name and email - * - * Default: _YES_ - */ -@property (nonatomic, assign) BOOL askUserDetails; - - -/** - * Trap fatal signals via a Mach exception server. This is now used by default! - * - * Default: _YES_ - * - * @deprecated Mach Exception Handler is now enabled by default! - */ -@property (nonatomic, assign, getter=isMachExceptionHandlerEnabled) BOOL enableMachExceptionHandler __attribute__((deprecated("Mach Exceptions are now enabled by default. If you want to disable them, please use the new property disableMachExceptionHandler"))); - - -/** - * Disable trap fatal signals via a Mach exception server. - * - * By default the SDK is catching fatal signals via a Mach exception server. - * This option allows you to use in-process BSD Signals for catching crashes instead. - * - * Default: _NO_ - * - * @warning The Mach exception handler executes in-process, and will interfere with debuggers when - * they attempt to suspend all active threads (which will include the Mach exception handler). - * Mach-based handling should _NOT_ be used when a debugger is attached. The SDK will not - * enable catching exceptions if the app is started with the debugger running. If you attach - * the debugger during runtime, this may cause issues if it is not disabled! - */ -@property (nonatomic, assign, getter=isMachExceptionHandlerDisabled) BOOL disableMachExceptionHandler; - - -/** - * Submit crash reports without asking the user - * - * _YES_: The crash report will be submitted without asking the user - * _NO_: The user will be asked if the crash report can be submitted (default) - * - * Default: _NO_ - */ -@property (nonatomic, assign, getter=isAutoSubmitCrashReport) BOOL autoSubmitCrashReport; - -/** - * Set the callbacks that will be executed prior to program termination after a crash has occurred - * - * PLCrashReporter provides support for executing an application specified function in the context - * of the crash reporter's signal handler, after the crash report has been written to disk. - * - * Writing code intended for execution inside of a signal handler is exceptionally difficult, and is _NOT_ recommended! - * - * _Program Flow and Signal Handlers_ - * - * When the signal handler is called the normal flow of the program is interrupted, and your program is an unknown state. Locks may be held, the heap may be corrupt (or in the process of being updated), and your signal handler may invoke a function that was being executed at the time of the signal. This may result in deadlocks, data corruption, and program termination. - * - * _Async-Safe Functions_ - * - * A subset of functions are defined to be async-safe by the OS, and are safely callable from within a signal handler. If you do implement a custom post-crash handler, it must be async-safe. A table of POSIX-defined async-safe functions and additional information is available from the [CERT programming guide - SIG30-C](https://www.securecoding.cert.org/confluence/display/seccode/SIG30-C.+Call+only+asynchronous-safe+functions+within+signal+handlers). - * - * Most notably, the Objective-C runtime itself is not async-safe, and Objective-C may not be used within a signal handler. - * - * Documentation taken from PLCrashReporter: https://www.plcrashreporter.org/documentation/api/v1.2-rc2/async_safety.html - * - * @see BITCrashManagerPostCrashSignalCallback - * @see BITCrashManagerCallbacks - * - * @param callbacks A pointer to an initialized PLCrashReporterCallback structure, see https://www.plcrashreporter.org/documentation/api/v1.2-rc2/struct_p_l_crash_reporter_callbacks.html - */ -- (void)setCrashCallbacks: (BITCrashManagerCallbacks *) callbacks; - - -///----------------------------------------------------------------------------- -/// @name Crash Meta Information -///----------------------------------------------------------------------------- - -/** - * Indicates if the app crash in the previous session - * - * Use this on startup, to check if the app starts the first time after it crashed - * previously. You can use this also to disable specific events, like asking - * the user to rate your app. - * - * @warning This property only has a correct value, once `[BITHockeyManager startManager]` was - * invoked! - */ -@property (nonatomic, readonly) BOOL didCrashInLastSession; - -/** - Provides an interface to pass user input from a custom alert to a crash report - - @param userInput Defines the users action wether to send, always send, or not to send the crash report. - @param userProvidedMetaData The content of this optional BITCrashMetaData instance will be attached to the crash report and allows to ask the user for e.g. additional comments or info. - - @return Returns YES if the input is a valid option and successfully triggered further processing of the crash report - - @see BITCrashManagerUserInput - @see BITCrashMetaData - */ -- (BOOL)handleUserInput:(BITCrashManagerUserInput)userInput withUserProvidedMetaData:(BITCrashMetaData *)userProvidedMetaData; - -/** - Lets you set a custom block which handles showing a custom UI and asking the user - whether he wants to send the crash report. - - This replaces the default alert the SDK would show! - - You can use this to present any kind of user interface which asks the user for additional information, - e.g. what they did in the app before the app crashed. - - In addition to this you should always ask your users if they agree to send crash reports, send them - always or not and return the result when calling `handleUserInput:withUserProvidedCrashDescription`. - - @param crashReportUIHandler A block that is responsible for loading, presenting and and dismissing your custom user interface which prompts the user if he wants to send crash reports. The block is also responsible for triggering further processing of the crash reports. - - @warning Block needs to call the `[BITCrashManager handleUserInput:withUserProvidedMetaData:]` method! - - @warning This needs to be set before calling `[BITHockeyManager startManager]`! - */ -- (void)setCrashReportUIHandler:(BITCustomCrashReportUIHandler)crashReportUIHandler; - -/** - * Provides details about the crash that occured in the last app session - */ -@property (nonatomic, readonly) BITCrashDetails *lastSessionCrashDetails; - -/** - * Provides the time between startup and crash in seconds - * - * Use this in together with `didCrashInLastSession` to detect if the app crashed very - * early after startup. This can be used to delay app initialization until the crash - * report has been sent to the server or if you want to do any other actions like - * cleaning up some cache data etc. - * - * The `BITCrashManagerDelegate` protocol provides some delegates to inform if sending - * a crash report was finished successfully, ended in error or was cancelled by the user. - * - * *Default*: _-1_ - * @see didCrashInLastSession - * @see BITCrashManagerDelegate - */ -@property (nonatomic, readonly) NSTimeInterval timeintervalCrashInLastSessionOccured; - - -///----------------------------------------------------------------------------- -/// @name Helper -///----------------------------------------------------------------------------- - -/** - * Lets the app crash for easy testing of the SDK - * - * The best way to use this is to trigger the crash with a button action. - * - * Make sure not to let the app crash in `applicationDidFinishLaunching` or any other - * startup method! Since otherwise the app would crash before the SDK could process it. - * - * Note that our SDK provides support for handling crashes that happen early on startup. - * Check the documentation for more information on how to use this. - */ -- (void)generateTestCrash; - - -@end diff --git a/HockeySDK.framework/Versions/A/Headers/BITCrashManagerDelegate.h b/HockeySDK.framework/Versions/A/Headers/BITCrashManagerDelegate.h deleted file mode 100644 index 9c16d751a8..0000000000 --- a/HockeySDK.framework/Versions/A/Headers/BITCrashManagerDelegate.h +++ /dev/null @@ -1,103 +0,0 @@ -#import - -@class BITHockeyAttachment; - -/** - * The `BITCrashManagerDelegate` formal protocol defines methods further configuring - * the behaviour of `BITCrashManager`. - */ -@protocol BITCrashManagerDelegate - -@optional - -/** - * Not used any longer! - * - * In previous SDK versions this invoked once the user interface asking for crash details and if the data should be send is dismissed - * - * @param crashManager The `BITCrashManager` instance invoking the method - * @deprecated The default crash report UI is not shown modal any longer, so this delegate is not being used any more! - */ -- (void) showMainApplicationWindowForCrashManager:(BITCrashManager *)crashManager __attribute__((deprecated("The default crash report UI is not shown modal any longer, so this delegate is now called right away. We recommend to remove the implementation of this method."))); - -///----------------------------------------------------------------------------- -/// @name Additional meta data -///----------------------------------------------------------------------------- - -/** Return any log string based data the crash report being processed should contain - * - * @param crashManager The `BITCrashManager` instance invoking this delegate - */ --(NSString *)applicationLogForCrashManager:(BITCrashManager *)crashManager; - -/** Return a BITHockeyAttachment object providing an NSData object the crash report - being processed should contain - - Please limit your attachments to reasonable files to avoid high traffic costs for your users. - - Example implementation: - - - (BITHockeyAttachment *)attachmentForCrashManager:(BITCrashManager *)crashManager { - NSData *data = [NSData dataWithContentsOfURL:@"mydatafile"]; - - BITHockeyAttachment *attachment = [[BITHockeyAttachment alloc] initWithFilename:@"myfile.data" - hockeyAttachmentData:data - contentType:@"'application/octet-stream"]; - return attachment; - } - - @param crashManager The `BITCrashManager` instance invoking this delegate - @see applicationLogForCrashManager: - */ --(BITHockeyAttachment *)attachmentForCrashManager:(BITCrashManager *)crashManager; - -///----------------------------------------------------------------------------- -/// @name Alert -///----------------------------------------------------------------------------- - -/** - * Invoked before the user is asked to send a crash report, so you can do additional actions. - * - * E.g. to make sure not to ask the user for an app rating :) - * - * @param crashManager The `BITCrashManager` instance invoking this delegate - */ --(void)crashManagerWillShowSubmitCrashReportAlert:(BITCrashManager *)crashManager; - - -/** - * Invoked after the user did choose _NOT_ to send a crash in the alert - * - * @param crashManager The `BITCrashManager` instance invoking this delegate - */ --(void)crashManagerWillCancelSendingCrashReport:(BITCrashManager *)crashManager; - - -///----------------------------------------------------------------------------- -/// @name Networking -///----------------------------------------------------------------------------- - -/** - * Invoked right before sending crash reports will start - * - * @param crashManager The `BITCrashManager` instance invoking this delegate - */ -- (void)crashManagerWillSendCrashReport:(BITCrashManager *)crashManager; - -/** - * Invoked after sending crash reports failed - * - * @param crashManager The `BITCrashManager` instance invoking this delegate - * @param error The error returned from the NSURLSessionDataTask call or `kBITCrashErrorDomain` - * with reason of type `BITCrashErrorReason`. - */ -- (void)crashManager:(BITCrashManager *)crashManager didFailWithError:(NSError *)error; - -/** - * Invoked after sending crash reports succeeded - * - * @param crashManager The `BITCrashManager` instance invoking this delegate - */ -- (void)crashManagerDidFinishSendingCrashReport:(BITCrashManager *)crashManager; - -@end diff --git a/HockeySDK.framework/Versions/A/Headers/BITCrashMetaData.h b/HockeySDK.framework/Versions/A/Headers/BITCrashMetaData.h deleted file mode 100644 index 3ea226335a..0000000000 --- a/HockeySDK.framework/Versions/A/Headers/BITCrashMetaData.h +++ /dev/null @@ -1,29 +0,0 @@ -#import - - -/** - * This class provides properties that can be attached to a crash report via a custom alert view flow - */ -@interface BITCrashMetaData : NSObject - -/** - * User provided description that should be attached to the crash report as plain text - */ -@property (nonatomic, copy) NSString *userDescription; - -/** - * User name that should be attached to the crash report - */ -@property (nonatomic, copy) NSString *userName; - -/** - * User email that should be attached to the crash report - */ -@property (nonatomic, copy) NSString *userEmail; - -/** - * User ID that should be attached to the crash report - */ -@property (nonatomic, copy) NSString *userID; - -@end diff --git a/HockeySDK.framework/Versions/A/Headers/BITFeedbackManager.h b/HockeySDK.framework/Versions/A/Headers/BITFeedbackManager.h deleted file mode 100644 index c59f4547f8..0000000000 --- a/HockeySDK.framework/Versions/A/Headers/BITFeedbackManager.h +++ /dev/null @@ -1,140 +0,0 @@ -#import - -#import "BITHockeyBaseManager.h" - - -// Notification message which tells that loading messages finished -#define BITHockeyFeedbackMessagesLoadingStarted @"BITHockeyFeedbackMessagesLoadingStarted" - -// Notification message which tells that loading messages finished -#define BITHockeyFeedbackMessagesLoadingFinished @"BITHockeyFeedbackMessagesLoadingFinished" - - -/** - * Defines behavior of the user data field - */ -typedef NS_ENUM(NSInteger, BITFeedbackUserDataElement) { - /** - * don't ask for this user data element at all - */ - BITFeedbackUserDataElementDontShow = 0, - /** - * the user may provide it, but does not have to - */ - BITFeedbackUserDataElementOptional = 1, - /** - * the user has to provide this to continue - */ - BITFeedbackUserDataElementRequired = 2 -}; - - -@class BITFeedbackMessage; -@class BITFeedbackWindowController; - - -/** - The feedback module. - - This is the HockeySDK module for letting your users to communicate directly with you via - the app and an integrated user interface. It provides to have a single threaded - discussion with a user running your app. - - The user interface provides a window than can be presented using - `[BITFeedbackManager showFeedbackWindow]`. - This window integrates all features to load new messages, write new messages, view message - and ask the user for additional (optional) data like name and email. - - If the user provides the email address, all responses from the server will also be send - to the user via email and the user is also able to respond directly via email too. - - It is also integrates actions to invoke the user interface to compose a new messages, - reload the list content from the server and changing the users name or email if these - are allowed to be set. - - If new messages are written while the device is offline, the SDK automatically retries to - send them once the app starts again or gets active again, or if the notification - `BITHockeyNetworkDidBecomeReachableNotification` is fired. - - New message are automatically loaded on startup, when the app becomes active again - or when the notification `BITHockeyNetworkDidBecomeReachableNotification` is fired and - the last server communication task was more than 5 minutes ago. This - only happens if the user ever did initiate a conversation by writing the first - feedback message. - */ - -@interface BITFeedbackManager : BITHockeyBaseManager - -///----------------------------------------------------------------------------- -/// @name General settings -///----------------------------------------------------------------------------- - - -/** - Define if a name has to be provided by the user when providing feedback - - - `BITFeedbackUserDataElementDontShow`: Don't ask for this user data element at all - - `BITFeedbackUserDataElementOptional`: The user may provide it, but does not have to - - `BITFeedbackUserDataElementRequired`: The user has to provide this to continue - - The default value is `BITFeedbackUserDataElementOptional`. - - @warning If you provide a non nil value for the `BITFeedbackManager` class via - `[BITHockeyManagerDelegate userNameForHockeyManager:componentManager:]` then this - property will automatically be set to `BITFeedbackUserDataElementDontShow` - - @see requireUserEmail - @see `[BITHockeyManagerDelegate userNameForHockeyManager:componentManager:]` - */ -@property (nonatomic, readwrite) BITFeedbackUserDataElement requireUserName; - - -/** - Define if an email address has to be provided by the user when providing feedback - - If the user provides the email address, all responses from the server will also be send - to the user via email and the user is also able to respond directly via email too. - - - `BITFeedbackUserDataElementDontShow`: Don't ask for this user data element at all - - `BITFeedbackUserDataElementOptional`: The user may provide it, but does not have to - - `BITFeedbackUserDataElementRequired`: The user has to provide this to continue - - The default value is `BITFeedbackUserDataElementOptional`. - - @warning If you provide a non nil value for the `BITFeedbackManager` class via - `[BITHockeyManagerDelegate userEmailForHockeyManager:componentManager:]` then this - property will automatically be set to `BITFeedbackUserDataElementDontShow` - - @see requireUserName - @see `[BITHockeyManagerDelegate userEmailForHockeyManager:componentManager:]` - */ -@property (nonatomic, readwrite) BITFeedbackUserDataElement requireUserEmail; - - -/** - Indicates if an Notification Center alert should be shown when new messages arrived - - The alert is only shown, if the newest message is not originated from the current user. - This requires the users email address to be present! The optional userid property - cannot be used, because users could also answer via email and then this information - is not available. - - Default is `YES` - @see requireUserEmail - @see `[BITHockeyManagerDelegate userEmailForHockeyManager:componentManager:]` - */ -@property (nonatomic, readwrite) BOOL showAlertOnIncomingMessages; - - -///----------------------------------------------------------------------------- -/// @name User Interface -///----------------------------------------------------------------------------- - - -/** - Present the modal feedback list user interface. - */ -- (void)showFeedbackWindow; - - -@end diff --git a/HockeySDK.framework/Versions/A/Headers/BITFeedbackWindowController.h b/HockeySDK.framework/Versions/A/Headers/BITFeedbackWindowController.h deleted file mode 100644 index 94ffa5f275..0000000000 --- a/HockeySDK.framework/Versions/A/Headers/BITFeedbackWindowController.h +++ /dev/null @@ -1,9 +0,0 @@ -#import - -@class BITFeedbackManager; - -@interface BITFeedbackWindowController : NSWindowController - -- (id)initWithManager:(BITFeedbackManager *)feedbackManager; - -@end diff --git a/HockeySDK.framework/Versions/A/Headers/BITHockeyAttachment.h b/HockeySDK.framework/Versions/A/Headers/BITHockeyAttachment.h deleted file mode 100644 index 65fee8a5b6..0000000000 --- a/HockeySDK.framework/Versions/A/Headers/BITHockeyAttachment.h +++ /dev/null @@ -1,38 +0,0 @@ -#import - -/** - Provides support to add binary attachments to crash reports - - This is used by `[BITCrashManagerDelegate attachmentForCrashManager:]` - */ -@interface BITHockeyAttachment : NSObject - -/** - The filename the attachment should get - */ -@property (nonatomic, readonly, copy) NSString *filename; - -/** - The attachment data as NSData object - */ -@property (nonatomic, readonly, strong) NSData *hockeyAttachmentData; - -/** - The content type of your data as MIME type - */ -@property (nonatomic, readonly, copy) NSString *contentType; - -/** - Create an BITHockeyAttachment instance with a given filename and NSData object - - @param filename The filename the attachment should get. If nil will get a automatically generated filename - @param hockeyAttachmentData The attachment data as NSData. The instance will be ignore if this is set to nil! - @param contentType The content type of your data as MIME type. If nil will be set to "application/octet-stream" - - @return An instsance of BITHockeyAttachment - */ -- (instancetype)initWithFilename:(NSString *)filename - hockeyAttachmentData:(NSData *)hockeyAttachmentData - contentType:(NSString *)contentType; - -@end diff --git a/HockeySDK.framework/Versions/A/Headers/BITHockeyBaseManager.h b/HockeySDK.framework/Versions/A/Headers/BITHockeyBaseManager.h deleted file mode 100644 index 53d2a46c83..0000000000 --- a/HockeySDK.framework/Versions/A/Headers/BITHockeyBaseManager.h +++ /dev/null @@ -1,24 +0,0 @@ -#import - -/** - The internal superclass for all component managers - - */ - -@interface BITHockeyBaseManager : NSObject - -///----------------------------------------------------------------------------- -/// @name Modules -///----------------------------------------------------------------------------- - - -/** - Defines the server URL to send data to or request data from - - By default this is set to the HockeyApp servers and there rarely should be a - need to modify that. - */ -@property (nonatomic, copy) NSString *serverURL; - - -@end diff --git a/HockeySDK.framework/Versions/A/Headers/BITHockeyManager.h b/HockeySDK.framework/Versions/A/Headers/BITHockeyManager.h deleted file mode 100644 index 3783399bd2..0000000000 --- a/HockeySDK.framework/Versions/A/Headers/BITHockeyManager.h +++ /dev/null @@ -1,361 +0,0 @@ -@class BITCrashManager; -@class BITFeedbackManager; -@class BITMetricsManager; -@protocol BITHockeyManagerDelegate; - -#import "HockeySDK.h" - -/** - The HockeySDK manager. Responsible for setup and management of all components - - This is the principal SDK class. It represents the entry point for the HockeySDK. The main promises of the class are initializing the SDK - modules, providing access to global properties and to all modules. Initialization is divided into several distinct phases: - - 1. Setup the [HockeyApp](http://hockeyapp.net/) app identifier and the optional delegate: This is the least required information on setting up the SDK and using it. It does some simple validation of the app identifier. - 2. Provides access to the SDK module `BITCrashManager`. This way all modules can be further configured to personal needs, if the defaults don't fit the requirements. - 3. Configure each module. - 4. Start up all modules. - - The SDK is optimized to defer everything possible to a later time while making sure e.g. crashes on startup can also be caught and each module executes other code with a delay some seconds. This ensures that applicationDidFinishLaunching will process as fast as possible and the SDK will not block the startup sequence resulting in a possible kill by the watchdog process. - - All modules do **NOT** show any user interface if the module is not activated or not integrated. - `BITCrashManager`: Shows an alert on startup asking the user if he/she agrees on sending the crash report, if `[BITCrashManager autoSubmitCrashReport]` is enabled (default) - - Example: - - [[BITHockeyManager sharedHockeyManager] - configureWithIdentifier:@""]; - [[BITHockeyManager sharedHockeyManager] startManager]; - - @warning The SDK is **NOT** thread safe and has to be set up on the main thread! - - @warning You should **NOT** change any module configuration after calling `startManager`! - - */ -@interface BITHockeyManager : NSObject - -#pragma mark - Public Methods - -///----------------------------------------------------------------------------- -/// @name Initialization -///----------------------------------------------------------------------------- - -/** - * Returns the shared manager object - * - * @return A singleton BITHockeyManager instance ready use - */ -+ (BITHockeyManager *)sharedHockeyManager; - -/** - * Initializes the manager with a particular app identifier, company name and delegate - * - * Initialize the manager with a HockeyApp app identifier. - * - * @see BITCrashManagerDelegate - * @see startManager - * @see configureWithIdentifier:delegate: - * @param appIdentifier The app identifier that should be used. - */ -- (void)configureWithIdentifier:(NSString *)appIdentifier; - -/** - * Initializes the manager with a particular app identifier, company name and delegate - * - * Initialize the manager with a HockeyApp app identifier and assign the class that - * implements the optional protocol `BITCrashManagerDelegate`. - * - * @see BITCrashManagerDelegate - * @see startManager - * @see configureWithIdentifier: - * @param appIdentifier The app identifier that should be used. - * @param delegate `nil` or the class implementing the optional protocols - */ -- (void)configureWithIdentifier:(NSString *)appIdentifier delegate:(id ) delegate; - -/** - * Initializes the manager with a particular app identifier, company name and delegate - * - * Initialize the manager with a HockeyApp app identifier and assign the class that - * implements the required protocol `BITCrashManagerDelegate`. - * - * @see BITCrashManagerDelegate - * @see startManager - * @see configureWithIdentifier: - * @see configureWithIdentifier:delegate: - * @param appIdentifier The app identifier that should be used. - * @param companyName `nil` or the company name, this is not used anywhere any longer. - * @param delegate `nil` or the class implementing the required protocols - */ -- (void)configureWithIdentifier:(NSString *)appIdentifier companyName:(NSString *)companyName delegate:(id ) delegate __attribute__((deprecated("Use configureWithIdentifier:delegate: instead"))); - -/** - * Starts the manager and runs all modules - * - * Call this after configuring the manager and setting up all modules. - * - * @see configureWithIdentifier: - * @see configureWithIdentifier:delegate: - */ -- (void)startManager; - - -#pragma mark - Public Properties - -///----------------------------------------------------------------------------- -/// @name General -///----------------------------------------------------------------------------- - - -/** - * Set the delegate - * - * Defines the class that implements the optional protocol `BITHockeyManagerDelegate`. - * - * @see BITHockeyManagerDelegate - * @see BITCrashManagerDelegate - */ -@property (nonatomic, weak) id delegate; - - -///----------------------------------------------------------------------------- -/// @name Modules -///----------------------------------------------------------------------------- - - -/** - * Defines the server URL to send data to or request data from - * - * By default this is set to the HockeyApp servers and there rarely should be a - * need to modify that. - * Please be aware that the URL for `BITMetricsManager` needs to be set separately - * as this class uses a different endpoint! - */ -@property (nonatomic, copy) NSString *serverURL; - -/** - * Reference to the initialized BITCrashManager module - * - * Returns the BITCrashManager instance initialized by BITHockeyManager - * - * @see configureWithIdentifier: - * @see configureWithIdentifier:delegate: - * @see startManager - * @see disableCrashManager - */ -@property (nonatomic, strong, readonly) BITCrashManager *crashManager; - - -/** - * Flag the determines whether the Crash Manager should be disabled - * - * If this flag is enabled, then crash reporting is disabled and no crashes will - * be send. - * - * Please note that the Crash Manager will be initialized anyway! - * - * *Default*: _NO_ - * @see crashManager - */ -@property (nonatomic, getter = isCrashManagerDisabled) BOOL disableCrashManager; - - -/** - Reference to the initialized BITFeedbackManager module - - Returns the BITFeedbackManager instance initialized by BITHockeyManager - - @see configureWithIdentifier:delegate: - @see startManager - @see disableFeedbackManager - */ -@property (nonatomic, strong, readonly) BITFeedbackManager *feedbackManager; - - -/** - Flag the determines whether the Feedback Manager should be disabled - - If this flag is enabled, then letting the user give feedback and - get responses will be turned off! - - Please note that the Feedback Manager will be initialized anyway! - - *Default*: _NO_ - @see feedbackManager - */ -@property (nonatomic, getter = isFeedbackManagerDisabled) BOOL disableFeedbackManager; - - -/** - Reference to the initialized BITMetricsManager module - - Returns the BITMetricsManager instance initialized by BITHockeyManager - */ -@property (nonatomic, strong, readonly) BITMetricsManager *metricsManager; - -/** - Flag the determines whether the BITMetricsManager should be disabled - - If this flag is enabled, then sending metrics data such as sessions and users - will be turned off! - - Please note that the BITMetricsManager instance will be initialized anyway! - - *Default*: _NO_ - @see metricsManager - */ -@property (nonatomic, getter = isMetricsManagerDisabled) BOOL disableMetricsManager; - - -///----------------------------------------------------------------------------- -/// @name Configuration -///----------------------------------------------------------------------------- - - -/** Set the userid that should used in the SDK components - - Right now this is used by the `BITCrashManager` to attach to a crash report. - `BITFeedbackManager` uses it too for assigning the user to a discussion thread. - - The value can be set at any time and will be stored in the keychain on the current - device only! To delete the value from the keychain set the value to `nil`. - - This property is optional and can be used as an alternative to the delegate. If you - want to define specific data for each component, use the delegate instead which does - overwrite the values set by this property. - - @warning When returning a non nil value, crash reports are not anonymous any more - and the crash alerts will not show the word "anonymous"! - - @warning This property needs to be set before calling `startManager` to be considered - for being added to crash reports as meta data. - - @see [BITHockeyManagerDelegate userIDForHockeyManager:componentManager:] - @see setUserName: - @see setUserEmail: - - @param userID NSString value for the userID - */ -- (void)setUserID:(NSString *)userID; - - -/** Set the user name that should used in the SDK components - - Right now this is used by the `BITCrashManager` to attach to a crash report. - `BITFeedbackManager` uses it too for assigning the user to a discussion thread. - - The value can be set at any time and will be stored in the keychain on the current - device only! To delete the value from the keychain set the value to `nil`. - - This property is optional and can be used as an alternative to the delegate. If you - want to define specific data for each component, use the delegate instead which does - overwrite the values set by this property. - - @warning When returning a non nil value, crash reports are not anonymous any more - and the crash alerts will not show the word "anonymous"! - - @warning This property needs to be set before calling `startManager` to be considered - for being added to crash reports as meta data. - - @see [BITHockeyManagerDelegate userNameForHockeyManager:componentManager:] - @see setUserID: - @see setUserEmail: - - @param userName NSString value for the userName - */ -- (void)setUserName:(NSString *)userName; - - -/** Set the users email address that should used in the SDK components - - Right now this is used by the `BITCrashManager` to attach to a crash report. - `BITFeedbackManager` uses it too for assigning the user to a discussion thread. - - The value can be set at any time and will be stored in the keychain on the current - device only! To delete the value from the keychain set the value to `nil`. - - This property is optional and can be used as an alternative to the delegate. If you - want to define specific data for each component, use the delegate instead which does - overwrite the values set by this property. - - @warning When returning a non nil value, crash reports are not anonymous any more - and the crash alerts will not show the word "anonymous"! - - @warning This property needs to be set before calling `startManager` to be considered - for being added to crash reports as meta data. - - @see [BITHockeyManagerDelegate userEmailForHockeyManager:componentManager:] - @see setUserID: - @see setUserName: - - @param userEmail NSString value for the userEmail - */ -- (void)setUserEmail:(NSString *)userEmail; - - -///----------------------------------------------------------------------------- -/// @name Debug Logging -///----------------------------------------------------------------------------- - -/** - This property is used indicate the amount of verboseness and severity for which - you want to see log messages in the console. - */ -@property (nonatomic, assign) BITLogLevel logLevel; - -/** - Flag that determines whether additional logging output should be generated - by the manager and all modules. - - This is ignored if the app is running in the App Store and reverts to the - default value in that case. - - @warning This property needs to be set before calling `startManager` - - *Default*: _NO_ - */ -@property (nonatomic, assign, getter=isDebugLogEnabled) BOOL debugLogEnabled DEPRECATED_MSG_ATTRIBUTE("Use logLevel instead!"); - -/** - Set a custom block that handles all the log messages that are emitted from the SDK. - - You can use this to reroute the messages that would normally be logged by `NSLog();` - to your own custom logging framework. - - An example of how to do this with NSLogger: - - ``` - [[BITHockeyManager sharedHockeyManager] setLogHandler:^(BITLogMessageProvider messageProvider, BITLogLevel logLevel, const char *file, const char *function, uint line) { - LogMessageRawF(file, (int)line, function, @"HockeySDK", (int)logLevel-1, messageProvider()); - }]; - ``` - - or with CocoaLumberjack: - - ``` - [[BITHockeyManager sharedHockeyManager] setLogHandler:^(BITLogMessageProvider messageProvider, BITLogLevel logLevel, const char *file, const char *function, uint line) { - [DDLog log:YES message:messageProvider() level:ddLogLevel flag:(DDLogFlag)(1 << (logLevel-1)) context:<#CocoaLumberjackContext#> file:file function:function line:line tag:nil]; - }]; - ``` - - @param logHandler The block of type BITLogHandler that will process all logged messages. - */ -- (void)setLogHandler:(BITLogHandler)logHandler; - - -///----------------------------------------------------------------------------- -/// @name Integration test -///----------------------------------------------------------------------------- - -/** - Pings the server with the HockeyApp app identifiers used for initialization - - Call this method once for debugging purposes to test if your SDK setup code - reaches the server successfully. - - Once invoked, check the apps page on HockeyApp for a verification. - */ -- (void)testIdentifier; - - -@end diff --git a/HockeySDK.framework/Versions/A/Headers/BITHockeyManagerDelegate.h b/HockeySDK.framework/Versions/A/Headers/BITHockeyManagerDelegate.h deleted file mode 100644 index 784cba2a12..0000000000 --- a/HockeySDK.framework/Versions/A/Headers/BITHockeyManagerDelegate.h +++ /dev/null @@ -1,91 +0,0 @@ -#import -#import "BITCrashManagerDelegate.h" - -@class BITHockeyManager; -@class BITHockeyBaseManager; - -/** - The `BITHockeyManagerDelegate` formal protocol defines methods further configuring - the behaviour of `BITHockeyManager`, as well as the delegate of the modules it manages. - */ - -@protocol BITHockeyManagerDelegate - -@optional - - -///----------------------------------------------------------------------------- -/// @name Additional meta data -///----------------------------------------------------------------------------- - - -/** Return the userid that should used in the SDK components - - Right now this is used by the `BITCrashMananger` to attach to a crash report and `BITFeedbackManager`. - - You can find out the component requesting the user name like this: - - (NSString *)userNameForHockeyManager:(BITHockeyManager *)hockeyManager componentManager:(BITCrashManager *)componentManager { - if (componentManager == crashManager) { - return UserNameForFeedback; - } else { - return nil; - } - } - - - - @param hockeyManager The `BITHockeyManager` HockeyManager instance invoking this delegate - @param componentManager The `BITCrashManager` component instance invoking this delegate - @see [BITHockeyManager setUserID:] - @see userNameForHockeyManager:componentManager: - @see userEmailForHockeyManager:componentManager: - */ -- (NSString *)userIDForHockeyManager:(BITHockeyManager *)hockeyManager componentManager:(BITHockeyBaseManager *)componentManager; - - -/** Return the user name that should used in the SDK components - - Right now this is used by the `BITCrashMananger` to attach to a crash report and `BITFeedbackManager`. - - You can find out the component requesting the user name like this: - - (NSString *)userNameForHockeyManager:(BITHockeyManager *)hockeyManager componentManager:(BITCrashManager *)componentManager { - if (componentManager == crashManager) { - return UserNameForFeedback; - } else { - return nil; - } - } - - - @param hockeyManager The `BITHockeyManager` HockeyManager instance invoking this delegate - @param componentManager The `BITCrashManager` component instance invoking this delegate - @see [BITHockeyManager setUserName:] - @see userIDForHockeyManager:componentManager: - @see userEmailForHockeyManager:componentManager: - */ -- (NSString *)userNameForHockeyManager:(BITHockeyManager *)hockeyManager componentManager:(BITHockeyBaseManager *)componentManager; - - -/** Return the users email address that should used in the SDK components - - Right now this is used by the `BITCrashMananger` to attach to a crash report and `BITFeedbackManager`. - - You can find out the component requesting the user name like this: - - (NSString *)userNameForHockeyManager:(BITHockeyManager *)hockeyManager componentManager:(BITCrashManager *)componentManager { - if (componentManager == hockeyManager.crashManager) { - return UserNameForCrashReports; - } else { - return nil; - } - } - - - @param hockeyManager The `BITHockeyManager` HockeyManager instance invoking this delegate - @param componentManager The `BITCrashManager` component instance invoking this delegate - @see [BITHockeyManager setUserEmail:] - @see userIDForHockeyManager:componentManager: - @see userNameForHockeyManager:componentManager: - */ -- (NSString *)userEmailForHockeyManager:(BITHockeyManager *)hockeyManager componentManager:(BITHockeyBaseManager *)componentManager; - -@end diff --git a/HockeySDK.framework/Versions/A/Headers/BITMetricsManager.h b/HockeySDK.framework/Versions/A/Headers/BITMetricsManager.h deleted file mode 100644 index a3ef7a3f66..0000000000 --- a/HockeySDK.framework/Versions/A/Headers/BITMetricsManager.h +++ /dev/null @@ -1,54 +0,0 @@ -#import -#import "BITHockeyBaseManager.h" -#import "HockeySDKNullability.h" - -NS_ASSUME_NONNULL_BEGIN - -/** - The metrics module. - - This is the HockeySDK module that handles users, sessions and events tracking. - - Unless disabled, this module automatically tracks users and session of your app to give you - better insights about how your app is being used. - Users are tracked in a completely anonymous way without collecting any personally identifiable - information. - - Before starting to track events, ask yourself the questions that you want to get answers to. - For instance, you might be interested in business, performance/quality or user experience aspects. - Name your events in a meaningful way and keep in mind that you will use these names - when searching for events in the HockeyApp web portal. - - It is your reponsibility to not collect personal information as part of the events tracking or get - prior consent from your users as necessary. - */ -@interface BITMetricsManager : BITHockeyBaseManager - -/** - * A property indicating whether the BITMetricsManager instance is disabled. - */ -@property (nonatomic, assign) BOOL disabled; - -/** - * This method allows to track an event that happened in your app. - * Remember to choose meaningful event names to have the best experience when diagnosing your app - * in the HockeyApp web portal. - * - * @param eventName The event's name as a string. - */ -- (void)trackEventWithName:(nonnull NSString *)eventName; - -/** - * This method allows to track an event that happened in your app. - * Remember to choose meaningful event names to have the best experience when diagnosing your app - * in the web portal. - * - * @param eventName the name of the event, which should be tracked. - * @param properties key value pairs with additional info about the event. - * @param measurements key value pairs, which contain custom metrics. - */ -- (void)trackEventWithName:(nonnull NSString *)eventName properties:(nullable NSDictionary *)properties measurements:(nullable NSDictionary *)measurements; - -@end - -NS_ASSUME_NONNULL_END diff --git a/HockeySDK.framework/Versions/A/Headers/BITSystemProfile.h b/HockeySDK.framework/Versions/A/Headers/BITSystemProfile.h deleted file mode 100644 index ad8a76d261..0000000000 --- a/HockeySDK.framework/Versions/A/Headers/BITSystemProfile.h +++ /dev/null @@ -1,102 +0,0 @@ -#import - -/** - * Helper class for accessing system information and measuring usage time - */ -@interface BITSystemProfile : NSObject - -///----------------------------------------------------------------------------- -/// @name Initialization -///----------------------------------------------------------------------------- - -/** - * Returns a shared BITSystemProfile object - * - * @return A singleton BITSystemProfile instance ready use - */ -+ (BITSystemProfile *)sharedSystemProfile; - - -///----------------------------------------------------------------------------- -/// @name Generic -///----------------------------------------------------------------------------- - -/** - * Return the current devices identifier - * - * @return NSString with the device identifier - */ -+ (NSString *)deviceIdentifier; - -/** - * Return the current device model - * - * @return NSString with the repesentation of the device model - */ -+ (NSString *)deviceModel; - -/** - * Return the system version of the current device - * - * @return NSString with the system version - */ -+ (NSString *)systemVersionString; - -/** - * Return an array with system data for a specific bundle - * - * @param bundle The app or framework bundle to get the system data from - * - * @return NSMutableArrray with system data - */ -- (NSMutableArray *)systemDataForBundle:(NSBundle *)bundle; - -/** - * Return an array with system data - * - * @return NSMutableArray with system data - */ -- (NSMutableArray *)systemData; - -/** - * Return an array with system usage data for a specific bundle - * - * @param bundle The app or framework bundle to get the usage data from - * - * @return NSMutableArray with system and bundle usage data - */ -- (NSMutableArray *)systemUsageDataForBundle:(NSBundle *)bundle; - -/** - * Return an array with system usage data that can be used with Sparkle - * - * Call this method in the Sparkle delegate `feedParametersForUpdater:sendingSystemProfile:` - * to attach system and app data to each Sparkle request - * - * @return NSMutableArray with system and app usage data - */ -- (NSMutableArray *)systemUsageData; - - -///----------------------------------------------------------------------------- -/// @name Usage time -///----------------------------------------------------------------------------- - -/** - * Start recording usage time for a specific app or framework bundle - * - * @param bundle The app or framework bundle to measure the usage time for - */ -- (void)startUsageForBundle:(NSBundle *)bundle; - -/** - * Start recording usage time for the current app - */ -- (void)startUsage; - -/** - * stop recording usage time - */ -- (void)stopUsage; - -@end diff --git a/HockeySDK.framework/Versions/A/Headers/HockeySDK.h b/HockeySDK.framework/Versions/A/Headers/HockeySDK.h deleted file mode 100644 index 8d960c9b3e..0000000000 --- a/HockeySDK.framework/Versions/A/Headers/HockeySDK.h +++ /dev/null @@ -1,96 +0,0 @@ -#import - -#import "HockeySDKEnums.h" - -#import "BITHockeyManager.h" -#import "BITHockeyManagerDelegate.h" - -#import "BITHockeyAttachment.h" - -#import "BITCrashManager.h" -#import "BITCrashManagerDelegate.h" -#import "BITCrashDetails.h" -#import "BITCrashMetaData.h" -#import "BITCrashExceptionApplication.h" - -#import "BITSystemProfile.h" - -#import "BITFeedbackManager.h" -#import "BITFeedbackWindowController.h" - -#import "BITMetricsManager.h" - -// Notification message which HockeyManager is listening to, to retry requesting updated from the server -#define BITHockeyNetworkDidBecomeReachableNotification @"BITHockeyNetworkDidBecomeReachable" - -extern NSString *const kBITDefaultUserID; -extern NSString *const kBITDefaultUserName; -extern NSString *const kBITDefaultUserEmail; - -/** - * HockeySDK Crash Reporter error domain - */ -typedef NS_ENUM (NSInteger, BITCrashErrorReason) { - /** - * Unknown error - */ - BITCrashErrorUnknown, - /** - * API Server rejected app version - */ - BITCrashAPIAppVersionRejected, - /** - * API Server returned empty response - */ - BITCrashAPIReceivedEmptyResponse, - /** - * Connection error with status code - */ - BITCrashAPIErrorWithStatusCode -}; -extern NSString *const kBITCrashErrorDomain; - - -/** - * HockeySDK Feedback error domain - */ -typedef NS_ENUM(NSInteger, BITFeedbackErrorReason) { - /** - * Unknown error - */ - BITFeedbackErrorUnknown, - /** - * API Server returned invalid status - */ - BITFeedbackAPIServerReturnedInvalidStatus, - /** - * API Server returned invalid data - */ - BITFeedbackAPIServerReturnedInvalidData, - /** - * API Server returned empty response - */ - BITFeedbackAPIServerReturnedEmptyResponse, - /** - * Authorization secret missing - */ - BITFeedbackAPIClientAuthorizationMissingSecret, - /** - * No internet connection - */ - BITFeedbackAPIClientCannotCreateConnection -}; -extern NSString *const kBITFeedbackErrorDomain; - - -/** - * HockeySDK global error domain - */ -typedef NS_ENUM(NSInteger, BITHockeyErrorReason) { - /** - * Unknown error - */ - BITHockeyErrorUnknown -}; -extern NSString *const __attribute__((unused)) kBITHockeyErrorDomain; -// HockeySDK diff --git a/HockeySDK.framework/Versions/A/Headers/HockeySDKEnums.h b/HockeySDK.framework/Versions/A/Headers/HockeySDKEnums.h deleted file mode 100644 index 5c1ec95fb2..0000000000 --- a/HockeySDK.framework/Versions/A/Headers/HockeySDKEnums.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef HockeySDKEnums_h -#define HockeySDKEnums_h - -/** - * HockeySDK Log Levels - */ -typedef NS_ENUM(NSUInteger, BITLogLevel) { - /** - * Logging is disabled - */ - BITLogLevelNone = 0, - /** - * Only errors will be logged - */ - BITLogLevelError = 1, - /** - * Errors and warnings will be logged - */ - BITLogLevelWarning = 2, - /** - * Debug information will be logged - */ - BITLogLevelDebug = 3, - /** - * Logging will be very chatty - */ - BITLogLevelVerbose = 4 -}; - -typedef NSString *(^BITLogMessageProvider)(void); -typedef void (^BITLogHandler)(BITLogMessageProvider messageProvider, BITLogLevel logLevel, const char *file, const char *function, uint line); - -#endif /* HockeySDKEnums_h */ diff --git a/HockeySDK.framework/Versions/A/Headers/HockeySDKNullability.h b/HockeySDK.framework/Versions/A/Headers/HockeySDKNullability.h deleted file mode 100644 index fcd3c160d4..0000000000 --- a/HockeySDK.framework/Versions/A/Headers/HockeySDKNullability.h +++ /dev/null @@ -1,33 +0,0 @@ -// -// HockeySDKNullability.h -// HockeySDK -// -// Created by Andreas Linde on 12/06/15. -// -// - -#ifndef HockeySDK_HockeyNullability_h -#define HockeySDK_HockeyNullability_h - -// Define nullability fallback for backwards compatibility -#if !__has_feature(nullability) -#define NS_ASSUME_NONNULL_BEGIN -#define NS_ASSUME_NONNULL_END -#define nullable -#define nonnull -#define null_unspecified -#define null_resettable -#define __nullable -#define __nonnull -#define __null_unspecified -#endif - -// Fallback for convenience syntax which might not be available in older SDKs -#ifndef NS_ASSUME_NONNULL_BEGIN -#define NS_ASSUME_NONNULL_BEGIN _Pragma("clang assume_nonnull begin") -#endif -#ifndef NS_ASSUME_NONNULL_END -#define NS_ASSUME_NONNULL_END _Pragma("clang assume_nonnull end") -#endif - -#endif diff --git a/HockeySDK.framework/Versions/A/HockeySDK b/HockeySDK.framework/Versions/A/HockeySDK deleted file mode 100755 index 6c28237d90..0000000000 Binary files a/HockeySDK.framework/Versions/A/HockeySDK and /dev/null differ diff --git a/HockeySDK.framework/Versions/A/Modules/module.modulemap b/HockeySDK.framework/Versions/A/Modules/module.modulemap deleted file mode 100644 index e6cbc2c826..0000000000 --- a/HockeySDK.framework/Versions/A/Modules/module.modulemap +++ /dev/null @@ -1,6 +0,0 @@ -framework module HockeySDK { - umbrella header "HockeySDK.h" - - export * - module * { export * } -} diff --git a/HockeySDK.framework/Versions/A/PrivateHeaders/BITHockeyLoggerPrivate.h b/HockeySDK.framework/Versions/A/PrivateHeaders/BITHockeyLoggerPrivate.h deleted file mode 100644 index 279cd11384..0000000000 --- a/HockeySDK.framework/Versions/A/PrivateHeaders/BITHockeyLoggerPrivate.h +++ /dev/null @@ -1,3 +0,0 @@ -#import "BITHockeyLogger.h" - -FOUNDATION_EXPORT BITLogHandler const defaultLogHandler; diff --git a/HockeySDK.framework/Versions/A/Resources/BITCrashReportUI.nib b/HockeySDK.framework/Versions/A/Resources/BITCrashReportUI.nib deleted file mode 100644 index 7accb5cd34..0000000000 Binary files a/HockeySDK.framework/Versions/A/Resources/BITCrashReportUI.nib and /dev/null differ diff --git a/HockeySDK.framework/Versions/A/Resources/BITFeedbackWindowController.nib b/HockeySDK.framework/Versions/A/Resources/BITFeedbackWindowController.nib deleted file mode 100644 index 2de9706a08..0000000000 Binary files a/HockeySDK.framework/Versions/A/Resources/BITFeedbackWindowController.nib and /dev/null differ diff --git a/HockeySDK.framework/Versions/A/Resources/Info.plist b/HockeySDK.framework/Versions/A/Resources/Info.plist deleted file mode 100644 index e534e790e3..0000000000 --- a/HockeySDK.framework/Versions/A/Resources/Info.plist +++ /dev/null @@ -1,46 +0,0 @@ - - - - - BuildMachineOSBuild - 16G29 - CFBundleDevelopmentRegion - English - CFBundleExecutable - HockeySDK - CFBundleIdentifier - net.hockeyapp.sdk.mac - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - HockeySDK - CFBundlePackageType - FMWK - CFBundleShortVersionString - 5.0.0 - CFBundleSignature - ???? - CFBundleSupportedPlatforms - - MacOSX - - CFBundleVersion - 63 - DTCompiler - com.apple.compilers.llvm.clang.1_0 - DTPlatformBuild - 8E3004b - DTPlatformVersion - GM - DTSDKBuild - 16E185 - DTSDKName - macosx10.12 - DTXcode - 0833 - DTXcodeBuild - 8E3004b - NSHumanReadableCopyright - Copyright © Microsoft Corporation. All rights reserved. - - diff --git a/HockeySDK.framework/Versions/A/Resources/de.lproj/HockeySDK.strings b/HockeySDK.framework/Versions/A/Resources/de.lproj/HockeySDK.strings deleted file mode 100644 index df82980e71..0000000000 Binary files a/HockeySDK.framework/Versions/A/Resources/de.lproj/HockeySDK.strings and /dev/null differ diff --git a/HockeySDK.framework/Versions/A/Resources/en.lproj/HockeySDK.strings b/HockeySDK.framework/Versions/A/Resources/en.lproj/HockeySDK.strings deleted file mode 100644 index c9b3750a63..0000000000 Binary files a/HockeySDK.framework/Versions/A/Resources/en.lproj/HockeySDK.strings and /dev/null differ diff --git a/HockeySDK.framework/Versions/A/Resources/fi.lproj/HockeySDK.strings b/HockeySDK.framework/Versions/A/Resources/fi.lproj/HockeySDK.strings deleted file mode 100644 index 2f2f7fb235..0000000000 Binary files a/HockeySDK.framework/Versions/A/Resources/fi.lproj/HockeySDK.strings and /dev/null differ diff --git a/HockeySDK.framework/Versions/A/Resources/fr.lproj/HockeySDK.strings b/HockeySDK.framework/Versions/A/Resources/fr.lproj/HockeySDK.strings deleted file mode 100644 index f7c68e1a6e..0000000000 Binary files a/HockeySDK.framework/Versions/A/Resources/fr.lproj/HockeySDK.strings and /dev/null differ diff --git a/HockeySDK.framework/Versions/A/Resources/it.lproj/HockeySDK.strings b/HockeySDK.framework/Versions/A/Resources/it.lproj/HockeySDK.strings deleted file mode 100644 index 5634156233..0000000000 Binary files a/HockeySDK.framework/Versions/A/Resources/it.lproj/HockeySDK.strings and /dev/null differ diff --git a/HockeySDK.framework/Versions/A/Resources/ja.lproj/HockeySDK.strings b/HockeySDK.framework/Versions/A/Resources/ja.lproj/HockeySDK.strings deleted file mode 100644 index 9c8e96bab0..0000000000 Binary files a/HockeySDK.framework/Versions/A/Resources/ja.lproj/HockeySDK.strings and /dev/null differ diff --git a/HockeySDK.framework/Versions/A/Resources/nb.lproj/HockeySDK.strings b/HockeySDK.framework/Versions/A/Resources/nb.lproj/HockeySDK.strings deleted file mode 100644 index 2cb8932b0e..0000000000 Binary files a/HockeySDK.framework/Versions/A/Resources/nb.lproj/HockeySDK.strings and /dev/null differ diff --git a/HockeySDK.framework/Versions/A/Resources/sv.lproj/HockeySDK.strings b/HockeySDK.framework/Versions/A/Resources/sv.lproj/HockeySDK.strings deleted file mode 100644 index 5807f82cb6..0000000000 Binary files a/HockeySDK.framework/Versions/A/Resources/sv.lproj/HockeySDK.strings and /dev/null differ diff --git a/HockeySDK.framework/Versions/A/_CodeSignature/CodeResources b/HockeySDK.framework/Versions/A/_CodeSignature/CodeResources deleted file mode 100644 index 2756b37d63..0000000000 --- a/HockeySDK.framework/Versions/A/_CodeSignature/CodeResources +++ /dev/null @@ -1,536 +0,0 @@ - - - - - files - - Resources/BITCrashReportUI.nib - - 3PMlLH/T9ihjR9hTJmlfhoYlTTo= - - Resources/BITFeedbackWindowController.nib - - U7138Kz9mVv0MYl0vqFDlXTQ50g= - - Resources/Info.plist - - h0M1SxUeldVGyV1UYD0S4nFfEAY= - - Resources/de.lproj/HockeySDK.strings - - hash - - y7EHyQ0RgIoSEhvNFom4v5X8UDg= - - optional - - - Resources/en.lproj/HockeySDK.strings - - hash - - AKYswjSpEoQ27x2n9DDQpTU0cWI= - - optional - - - Resources/fi.lproj/HockeySDK.strings - - hash - - 7DWjpj+VdXhYZkryUxbU5Prazf4= - - optional - - - Resources/fr.lproj/HockeySDK.strings - - hash - - suxOXWFc6JEfYQzFi/zoGOYNYg8= - - optional - - - Resources/it.lproj/HockeySDK.strings - - hash - - NJw3EFVb4Oh/T5bLbt3ZHFZczd0= - - optional - - - Resources/ja.lproj/HockeySDK.strings - - hash - - TECOwkRhKVts/HxhErq91PeiDRU= - - optional - - - Resources/nb.lproj/HockeySDK.strings - - hash - - Dt7iPI2Bv05u24cdQxX9iLrL5qw= - - optional - - - Resources/sv.lproj/HockeySDK.strings - - hash - - BZjpUHORIoU7zIfpLpttsqkVvVg= - - optional - - - - files2 - - Headers/BITCrashDetails.h - - hash - - QJpS1iLzgJyvZ7PlJGQ92NTqraw= - - hash2 - - rCKw9nYmrVue7lxcIZogn8CRlXfKkSRnnCJXXrLd7Uo= - - - Headers/BITCrashExceptionApplication.h - - hash - - CnAjiJPcrwY4FnZT4gcbbUF/cis= - - hash2 - - HBtmsUcjqK86q6l2B81huD53VBcyZA5VGcRqeMvncWQ= - - - Headers/BITCrashManager.h - - hash - - ns2xwXIeh5E0NUYv1LRzY19UwFM= - - hash2 - - qvp6ae+k7RNYX9TWdExICzGKCM03Iy2/KAcYrMnnmtQ= - - - Headers/BITCrashManagerDelegate.h - - hash - - fdD+yyIt4Q9R92+R3VZIov5cWt8= - - hash2 - - AK1gODWZBPb7wpwt7obZ+1AhAbK+QGh40vp/JOamc3c= - - - Headers/BITCrashMetaData.h - - hash - - z3EPOJuS2Q9CJWLHdvERSwjJdQw= - - hash2 - - 8Ib92FRU2+8LHGqgUiTUmNoDPbS0arq6qa54q6dC9/g= - - - Headers/BITFeedbackManager.h - - hash - - 8SboRqHv+QKX4VJAHSnNGqi8an4= - - hash2 - - VPeYBf8PB9IJWGTXvyabGdoZoylZxWX6/L3fRjxyfCw= - - - Headers/BITFeedbackWindowController.h - - hash - - YPSJ/IOJ+8xdDe9E7vxKunmoBbw= - - hash2 - - yirwMfEo2EiL3sZRpMU9p39wErq4bwyFMe0QU5lcstY= - - - Headers/BITHockeyAttachment.h - - hash - - JSmTX4EAKu9TWU5Pd17ZAOLSjQ4= - - hash2 - - htj8SuvYHGyMPTPZjRG9as4KGmXDA6S0EZ29OqKeQP8= - - - Headers/BITHockeyBaseManager.h - - hash - - SMWk+44pA/RtGH5FYoL+mVyQ8NE= - - hash2 - - HcqaPSgqBUHG/+36MBKHfBenEcRr/hzVBqOz+iIzb6A= - - - Headers/BITHockeyManager.h - - hash - - NxzbmgJaQ8HOEFEBXT2bBQYTJRY= - - hash2 - - RNl/bqMwbM3tUAxNWpw1iNXwJgzo9UitWDMWlhBc9OI= - - - Headers/BITHockeyManagerDelegate.h - - hash - - Zj+MlMn4bYTRzL8I4JARHhr1ec0= - - hash2 - - q1/tCrlRvXxfYisuuVxiMEAuxRT1VLMy5irnysDzyhQ= - - - Headers/BITMetricsManager.h - - hash - - qWhXB7JYsqsqvTC851q5UFEUTzI= - - hash2 - - gQhOEtypxjLjjrUytWttK/lpEl/BswFjnRx5OHTBLt8= - - - Headers/BITSystemProfile.h - - hash - - bJtVVlH1ZlPxJ58JIRPwfkQbusI= - - hash2 - - b24vir8WuGAEZPouNvgqNzjVp6Fszdm2PDAl4jCxmIw= - - - Headers/HockeySDK.h - - hash - - ZkiUTj9hi/AcNU2/g6olcr6o0is= - - hash2 - - Rp5ynNJGtmgHOXI2yG5JaKEToBNx5/MjvuVAwgT1+2k= - - - Headers/HockeySDKEnums.h - - hash - - 7ELC6eqCDz4yjv1UCiciRNIY6RQ= - - hash2 - - jqvsQwd0m+ipBHmrgGkruOiV0qjHXJdSywNcsLJG2Bc= - - - Headers/HockeySDKNullability.h - - hash - - +iyPYq4RIO6vV67WYzO4PyQkNS0= - - hash2 - - 8+bfuR2FtE3P7o+D1jEB4W5xVehtqZW2bV9Z1lEh8GA= - - - Modules/module.modulemap - - hash - - XqNxHBjpNA+vmtw20+/D9PQwPx8= - - hash2 - - 1MjRn+z/i5P2zT/JGA+OlkSGE/Hr63Rrfp4loNHpZTc= - - - PrivateHeaders/BITHockeyLoggerPrivate.h - - hash - - 41vaag/XwWLo0GpB7j3xsdbdG6k= - - hash2 - - e42bIPN/Hp+jC6G7GbZHYPK9GwzixGOaEhfLixGwSvk= - - - Resources/BITCrashReportUI.nib - - hash - - 3PMlLH/T9ihjR9hTJmlfhoYlTTo= - - hash2 - - BnfITmSkCJY49DaG1Z5xoaxFx1y5RvFlJ9kZzzQwJZM= - - - Resources/BITFeedbackWindowController.nib - - hash - - U7138Kz9mVv0MYl0vqFDlXTQ50g= - - hash2 - - 9b32gwvSUkWnc/wgCrhdawCpBEE1huzAkXqFEkUNrTE= - - - Resources/Info.plist - - hash - - h0M1SxUeldVGyV1UYD0S4nFfEAY= - - hash2 - - 0x2hvQ5GSR082qIngDqCeYZgbHd/IYWV6tLp3kzrWjw= - - - Resources/de.lproj/HockeySDK.strings - - hash - - y7EHyQ0RgIoSEhvNFom4v5X8UDg= - - hash2 - - gjVOvn490tuAXiQ+tw8gY2U3mn2BA8kMKgYcTF9weGY= - - optional - - - Resources/en.lproj/HockeySDK.strings - - hash - - AKYswjSpEoQ27x2n9DDQpTU0cWI= - - hash2 - - eFrvSgciUp4mmTmTFnIuDjo/OdB8pSODl/+ad3cRw0Y= - - optional - - - Resources/fi.lproj/HockeySDK.strings - - hash - - 7DWjpj+VdXhYZkryUxbU5Prazf4= - - hash2 - - l/qYWKlOCj+YOqmf+sj1dLwrjTy17qBM36gMr8Ft588= - - optional - - - Resources/fr.lproj/HockeySDK.strings - - hash - - suxOXWFc6JEfYQzFi/zoGOYNYg8= - - hash2 - - DT0NTIIiqwo2wTTK3kU7oG5FCmdL88E3bC3xplj4EnU= - - optional - - - Resources/it.lproj/HockeySDK.strings - - hash - - NJw3EFVb4Oh/T5bLbt3ZHFZczd0= - - hash2 - - g7CCohKaKeOX5rhtdPNLh3mPCjhcjYoemkGssoSNSeg= - - optional - - - Resources/ja.lproj/HockeySDK.strings - - hash - - TECOwkRhKVts/HxhErq91PeiDRU= - - hash2 - - 5c3+sfUws6TpX5200QAXQYYxv19Q1HeYbp05PzAzZms= - - optional - - - Resources/nb.lproj/HockeySDK.strings - - hash - - Dt7iPI2Bv05u24cdQxX9iLrL5qw= - - hash2 - - 0dmd8IZMJQ5Aoxc+4yZnHqWnVyRupJH65qvT553+7eU= - - optional - - - Resources/sv.lproj/HockeySDK.strings - - hash - - BZjpUHORIoU7zIfpLpttsqkVvVg= - - hash2 - - 3/MmYvPGxm5H65DlVNwmcEpl3hH356AxT/0cRK0QWu8= - - optional - - - - rules - - ^Resources/ - - ^Resources/.*\.lproj/ - - optional - - weight - 1000 - - ^Resources/.*\.lproj/locversion.plist$ - - omit - - weight - 1100 - - ^Resources/Base\.lproj/ - - weight - 1010 - - ^version.plist$ - - - rules2 - - .*\.dSYM($|/) - - weight - 11 - - ^(.*/)?\.DS_Store$ - - omit - - weight - 2000 - - ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ - - nested - - weight - 10 - - ^.* - - ^Info\.plist$ - - omit - - weight - 20 - - ^PkgInfo$ - - omit - - weight - 20 - - ^Resources/ - - weight - 20 - - ^Resources/.*\.lproj/ - - optional - - weight - 1000 - - ^Resources/.*\.lproj/locversion.plist$ - - omit - - weight - 1100 - - ^Resources/Base\.lproj/ - - weight - 1010 - - ^[^/]+$ - - nested - - weight - 10 - - ^embedded\.provisionprofile$ - - weight - 20 - - ^version\.plist$ - - weight - 20 - - - - diff --git a/HockeySDK.framework/Versions/Current b/HockeySDK.framework/Versions/Current deleted file mode 120000 index 8c7e5a667f..0000000000 --- a/HockeySDK.framework/Versions/Current +++ /dev/null @@ -1 +0,0 @@ -A \ No newline at end of file diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000000..85ebbcb5e2 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,22 @@ +# How to Build Telegram for macOS + +1. Clone this repository with submodules: + + ```sh + git clone https://github.com/overtake/TelegramSwift.git --recurse-submodules + ``` +2. ```brew install cmake ninja openssl@1.1 zlib autoconf libtool automake yasm pkg-config``` +3. Open `Telegram-Mac.xcworkspace` in **Xcode 10.3**. Avoid Xcode 10.11+ because it causes additional errors when building the libraries with optimizations turned on. +4. Select build target to **Github** and **Run** build. + + + +# If you want to develop a fork + +1. Do first and second step above. +2. Change bundle Identifier and team-id. Easiest way is to search all mentions `ru.keepcoder.Telegram` and change it to your own. Team-id you can find on apple developer portal. +3. Obtain your [API ID](https://core.telegram.org/api/obtaining_api_id). **Note:** The built-in `apiId` is highly limited for api usage. **Do not use it** in any circumstances except verify binaries. +4. Open `Telegram-Mac/Config.swift` and repalce `apiId` and `apiHash` from previous step. **Note:** Do not forget to change `teamId` either. +5. Replace or remove `SFEED_URL` and `APPCENTER_SECRET` in `*.xcconfig` files. (First uses for in-app updates and second for collecting crashes on [appcenter](https://appcenter.ms)) +6. Write new better code. +7. If you still have a questions feel free to open new issue [here](https://github.com/overtake/TelegramSwift/issues/new). diff --git a/README.md b/README.md index c5f3743ffb..55ecac749d 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,75 @@ -[Telegram](https://telegram.org) is a messaging app with a focus on speed and security. It’s superfast, simple and free. -This repo contains the official source code for [Telegram App for MacOS](https://macos.telegram.org). +
+ +

Telegram for macOS

+
-## Creating your Telegram Application +![Telegram macOS screenshot](images/tg.png) -We welcome all developers to use our API and source code to create applications on our platform. -There are several things we require from **all developers** for the moment. +[**Telegram**](https://telegram.org) is a messaging app with a focus on speed and security. It’s superfast, simple, and free! This repo contains the official source code for [Telegram for macOS](https://macos.telegram.org/). -1. [**Obtain your own api_id**](https://core.telegram.org/api/obtaining_api_id) for your application. -2. Please **do not** use the name Telegram for your app — or make sure your users understand that it is unofficial. -3. Kindly **do not** use our standard logo (white paper plane in a blue circle) as your app's logo. -3. Please study our [**security guidelines**](https://core.telegram.org/mtproto/security_guidelines) and take good care of your users' data and privacy. -4. Please remember to publish **your** code too in order to comply with the licences. +## Get it -## Usage +[![Download on the Mac App Store](images/mas_badge.png)](https://itunes.apple.com/us/app/telegram/id747648890?mt=12) + + +### Using Homebrew -1. Clone repo with submodules ``` -git clone https://github.com/overtake/TelegramSwift.git --recursive +brew install --cask telegram ``` -2. Open Telegram-Mac.xcworkspace -3. Create Config.swift file with + +### Using `mas-cli` + ``` -let API_ID:Int32 = 'api_id' -let API_HASH:String = "api_hash" -let TEST_SERVER:Bool = false -let languagesCategory = "macos" +mas install 747648890 ``` -4. build and enjoy +### Manual download + +If you would like, you can [download the non-MAS version](https://telegram.org/dl/macos). + +You can also [download the beta version](https://telegram.org/dl/macos/beta) if you want to try the latest features and you are prepared for bugs and crashes. If you are running the beta, join the [beta testing chat on Telegram](https://t.me/macswift) to report bugs. + +## Contributors + +### Contributors on GitHub +See [this repository’s contributors graph](https://github.com/overtake/TelegramSwift/graphs/contributors). + +### Bugs and Suggestions +You can report bug or suggestions feature for Telegram for macOS on [Telegram’s Bugs & Suggestions platform](https://bugs.telegram.org). Read [the platform tip](https://bugs.telegram.org/c/746) before creating first card. + +### Translations +You can help translate Telegram for macOS on [Telegram’s translations platform](https://translations.telegram.org). Pick your language, then look for the macOS translation set. + + + + +## Permissions +Telegram strives to protect your privacy. This app asks for as few permissions as possible: + +* **Microphone**: You can send voice messages and make audio calls with Telegram. +* **Camera**: You can set your profile picture using your Mac’s iSight camera. +* **Location**: You can send your location to friends. +* **Outgoing network connections**: Telegram needs to connect to the internet to send your messages to your friends. +* **Incoming network connections**: Telegram needs to accept incoming connections for peer-to-peer voice calls. +* **User-selected files**: You can save files or images to your Mac. +* **Downloads folder**: Telegram can automatically download files or images you receive. + +## Shortcuts +With [Shortcuts](https://github.com/overtake/TelegramSwift/wiki) you can learn how easy is navigate using your devices. + +## License +Telegram for macOS is licensed under the GNU Public License, version 2.0. See [LICENSE](LICENSE) for more information. + +## Forking +You can fork this application and make something awesome! Make sure that your fork follows these five requirements: + +1. **Do** [get your own API ID](https://core.telegram.org/api/obtaining_api_id). +2. **Don’t** call your fork **Telegram** — or at least make sure your users understand that yours is unofficial. +3. **Don’t** use our standard logo (white paper plane in a blue circle) for your fork. +3. **Do** read and follow our [security guidelines](https://core.telegram.org/mtproto/security_guidelines) to make sure you take good care of your users’ data and protect their privacy. +4. **Do** publish your code. The [GPL license](LICENSE) requires it! +## How to Build +Instructions for building Telegram for macOS are in [INSTALL.md](INSTALL.md). diff --git a/Sparkle.framework/Headers b/Sparkle.framework/Headers deleted file mode 120000 index a177d2a6b9..0000000000 --- a/Sparkle.framework/Headers +++ /dev/null @@ -1 +0,0 @@ -Versions/Current/Headers \ No newline at end of file diff --git a/Sparkle.framework/Modules b/Sparkle.framework/Modules deleted file mode 120000 index 5736f3186e..0000000000 --- a/Sparkle.framework/Modules +++ /dev/null @@ -1 +0,0 @@ -Versions/Current/Modules \ No newline at end of file diff --git a/Sparkle.framework/PrivateHeaders b/Sparkle.framework/PrivateHeaders deleted file mode 120000 index d8e5645269..0000000000 --- a/Sparkle.framework/PrivateHeaders +++ /dev/null @@ -1 +0,0 @@ -Versions/Current/PrivateHeaders \ No newline at end of file diff --git a/Sparkle.framework/Resources b/Sparkle.framework/Resources deleted file mode 120000 index 953ee36f3b..0000000000 --- a/Sparkle.framework/Resources +++ /dev/null @@ -1 +0,0 @@ -Versions/Current/Resources \ No newline at end of file diff --git a/Sparkle.framework/Sparkle b/Sparkle.framework/Sparkle deleted file mode 120000 index b2c52731ea..0000000000 --- a/Sparkle.framework/Sparkle +++ /dev/null @@ -1 +0,0 @@ -Versions/Current/Sparkle \ No newline at end of file diff --git a/Sparkle.framework/Versions/A/Headers/SUAppcast.h b/Sparkle.framework/Versions/A/Headers/SUAppcast.h deleted file mode 100644 index d7363d0a70..0000000000 --- a/Sparkle.framework/Versions/A/Headers/SUAppcast.h +++ /dev/null @@ -1,40 +0,0 @@ -// -// SUAppcast.h -// Sparkle -// -// Created by Andy Matuschak on 3/12/06. -// Copyright 2006 Andy Matuschak. All rights reserved. -// - -#ifndef SUAPPCAST_H -#define SUAPPCAST_H - -#if __has_feature(modules) -@import Foundation; -#else -#import -#endif -#import "SUExport.h" - -NS_ASSUME_NONNULL_BEGIN - -@class SUAppcastItem; -SU_EXPORT @interface SUAppcast : NSObject - -@property (copy, nullable) NSString *userAgentString; - -#if __has_feature(objc_generics) -@property (copy, nullable) NSDictionary *httpHeaders; -#else -@property (copy, nullable) NSDictionary *httpHeaders; -#endif - -- (void)fetchAppcastFromURL:(NSURL *)url inBackground:(BOOL)bg completionBlock:(void (^)(NSError *_Nullable))err; -- (SUAppcast *)copyWithoutDeltaUpdates; - -@property (readonly, copy, nullable) NSArray *items; -@end - -NS_ASSUME_NONNULL_END - -#endif diff --git a/Sparkle.framework/Versions/A/Headers/SUAppcastItem.h b/Sparkle.framework/Versions/A/Headers/SUAppcastItem.h deleted file mode 100644 index 5c861ddd8e..0000000000 --- a/Sparkle.framework/Versions/A/Headers/SUAppcastItem.h +++ /dev/null @@ -1,51 +0,0 @@ -// -// SUAppcastItem.h -// Sparkle -// -// Created by Andy Matuschak on 3/12/06. -// Copyright 2006 Andy Matuschak. All rights reserved. -// - -#ifndef SUAPPCASTITEM_H -#define SUAPPCASTITEM_H - -#if __has_feature(modules) -@import Foundation; -#else -#import -#endif -#import "SUExport.h" - -SU_EXPORT @interface SUAppcastItem : NSObject -@property (copy, readonly) NSString *title; -@property (copy, readonly) NSString *dateString; -@property (copy, readonly) NSString *itemDescription; -@property (strong, readonly) NSURL *releaseNotesURL; -@property (copy, readonly) NSString *DSASignature; -@property (copy, readonly) NSString *minimumSystemVersion; -@property (copy, readonly) NSString *maximumSystemVersion; -@property (strong, readonly) NSURL *fileURL; -@property (nonatomic, readonly) uint64_t contentLength; -@property (copy, readonly) NSString *versionString; -@property (copy, readonly) NSString *osString; -@property (copy, readonly) NSString *displayVersionString; -@property (copy, readonly) NSDictionary *deltaUpdates; -@property (strong, readonly) NSURL *infoURL; - -// Initializes with data from a dictionary provided by the RSS class. -- (instancetype)initWithDictionary:(NSDictionary *)dict; -- (instancetype)initWithDictionary:(NSDictionary *)dict failureReason:(NSString **)error; - -@property (getter=isDeltaUpdate, readonly) BOOL deltaUpdate; -@property (getter=isCriticalUpdate, readonly) BOOL criticalUpdate; -@property (getter=isMacOsUpdate, readonly) BOOL macOsUpdate; -@property (getter=isInformationOnlyUpdate, readonly) BOOL informationOnlyUpdate; - -// Returns the dictionary provided in initWithDictionary; this might be useful later for extensions. -@property (readonly, copy) NSDictionary *propertiesDictionary; - -- (NSURL *)infoURL; - -@end - -#endif diff --git a/Sparkle.framework/Versions/A/Headers/SUErrors.h b/Sparkle.framework/Versions/A/Headers/SUErrors.h deleted file mode 100644 index 8557d7fbb4..0000000000 --- a/Sparkle.framework/Versions/A/Headers/SUErrors.h +++ /dev/null @@ -1,53 +0,0 @@ -// -// SUErrors.h -// Sparkle -// -// Created by C.W. Betts on 10/13/14. -// Copyright (c) 2014 Sparkle Project. All rights reserved. -// - -#ifndef SUERRORS_H -#define SUERRORS_H - -#if __has_feature(modules) -@import Foundation; -#else -#import -#endif -#import "SUExport.h" - -/** - * Error domain used by Sparkle - */ -SU_EXPORT extern NSString *const SUSparkleErrorDomain; - -typedef NS_ENUM(OSStatus, SUError) { - // Appcast phase errors. - SUAppcastParseError = 1000, - SUNoUpdateError = 1001, - SUAppcastError = 1002, - SURunningFromDiskImageError = 1003, - - // Download phase errors. - SUTemporaryDirectoryError = 2000, - SUDownloadError = 2001, - - // Extraction phase errors. - SUUnarchivingError = 3000, - SUSignatureError = 3001, - - // Installation phase errors. - SUFileCopyFailure = 4000, - SUAuthenticationFailure = 4001, - SUMissingUpdateError = 4002, - SUMissingInstallerToolError = 4003, - SURelaunchError = 4004, - SUInstallationError = 4005, - SUDowngradeError = 4006, - SUInstallationCancelledError = 4007, - - // System phase errors - SUSystemPowerOffError = 5000 -}; - -#endif diff --git a/Sparkle.framework/Versions/A/Headers/SUExport.h b/Sparkle.framework/Versions/A/Headers/SUExport.h deleted file mode 100644 index 3e3f8a1646..0000000000 --- a/Sparkle.framework/Versions/A/Headers/SUExport.h +++ /dev/null @@ -1,18 +0,0 @@ -// -// SUExport.h -// Sparkle -// -// Created by Jake Petroules on 2014-08-23. -// Copyright (c) 2014 Sparkle Project. All rights reserved. -// - -#ifndef SUEXPORT_H -#define SUEXPORT_H - -#ifdef BUILDING_SPARKLE -#define SU_EXPORT __attribute__((visibility("default"))) -#else -#define SU_EXPORT -#endif - -#endif diff --git a/Sparkle.framework/Versions/A/Headers/SUStandardVersionComparator.h b/Sparkle.framework/Versions/A/Headers/SUStandardVersionComparator.h deleted file mode 100644 index ed11921a51..0000000000 --- a/Sparkle.framework/Versions/A/Headers/SUStandardVersionComparator.h +++ /dev/null @@ -1,52 +0,0 @@ -// -// SUStandardVersionComparator.h -// Sparkle -// -// Created by Andy Matuschak on 12/21/07. -// Copyright 2007 Andy Matuschak. All rights reserved. -// - -#ifndef SUSTANDARDVERSIONCOMPARATOR_H -#define SUSTANDARDVERSIONCOMPARATOR_H - -#if __has_feature(modules) -@import Foundation; -#else -#import -#endif -#import "SUExport.h" -#import "SUVersionComparisonProtocol.h" - -NS_ASSUME_NONNULL_BEGIN - -/*! - Sparkle's default version comparator. - - This comparator is adapted from MacPAD, by Kevin Ballard. - It's "dumb" in that it does essentially string comparison, - in components split by character type. -*/ -SU_EXPORT @interface SUStandardVersionComparator : NSObject - -/*! - Initializes a new instance of the standard version comparator. - */ -- (instancetype)init; - -/*! - Returns a singleton instance of the comparator. - - It is usually preferred to alloc/init new a comparator instead. -*/ -+ (SUStandardVersionComparator *)defaultComparator; - -/*! - Compares version strings through textual analysis. - - See the implementation for more details. -*/ -- (NSComparisonResult)compareVersion:(NSString *)versionA toVersion:(NSString *)versionB; -@end - -NS_ASSUME_NONNULL_END -#endif diff --git a/Sparkle.framework/Versions/A/Headers/SUUpdater.h b/Sparkle.framework/Versions/A/Headers/SUUpdater.h deleted file mode 100644 index a47447559d..0000000000 --- a/Sparkle.framework/Versions/A/Headers/SUUpdater.h +++ /dev/null @@ -1,235 +0,0 @@ -// -// SUUpdater.h -// Sparkle -// -// Created by Andy Matuschak on 1/4/06. -// Copyright 2006 Andy Matuschak. All rights reserved. -// - -#ifndef SUUPDATER_H -#define SUUPDATER_H - -#if __has_feature(modules) -@import Cocoa; -#else -#import -#endif -#import "SUExport.h" -#import "SUVersionComparisonProtocol.h" -#import "SUVersionDisplayProtocol.h" - -@class SUAppcastItem, SUAppcast; - -@protocol SUUpdaterDelegate; - -/*! - The main API in Sparkle for controlling the update mechanism. - - This class is used to configure the update paramters as well as manually - and automatically schedule and control checks for updates. - */ -SU_EXPORT @interface SUUpdater : NSObject - -@property (unsafe_unretained) IBOutlet id delegate; - -/*! - The shared updater for the main bundle. - - This is equivalent to passing [NSBundle mainBundle] to SUUpdater::updaterForBundle: - */ -+ (SUUpdater *)sharedUpdater; - -/*! - The shared updater for a specified bundle. - - If an updater has already been initialized for the provided bundle, that shared instance will be returned. - */ -+ (SUUpdater *)updaterForBundle:(NSBundle *)bundle; - -/*! - Designated initializer for SUUpdater. - - If an updater has already been initialized for the provided bundle, that shared instance will be returned. - */ -- (instancetype)initForBundle:(NSBundle *)bundle; - -/*! - Explicitly checks for updates and displays a progress dialog while doing so. - - This method is meant for a main menu item. - Connect any menu item to this action in Interface Builder, - and Sparkle will check for updates and report back its findings verbosely - when it is invoked. - - This will find updates that the user has opted into skipping. - */ -- (IBAction)checkForUpdates:(id)sender; - -/*! - The menu item validation used for the -checkForUpdates: action - */ -- (BOOL)validateMenuItem:(NSMenuItem *)menuItem; - -/*! - Checks for updates, but does not display any UI unless an update is found. - - This is meant for programmatically initating a check for updates. That is, - it will display no UI unless it actually finds an update, in which case it - proceeds as usual. - - If automatic downloading of updates it turned on and allowed, however, - this will invoke that behavior, and if an update is found, it will be downloaded - in the background silently and will be prepped for installation. - - This will not find updates that the user has opted into skipping. - */ -- (void)checkForUpdatesInBackground; - -/*! - A property indicating whether or not to check for updates automatically. - - Setting this property will persist in the host bundle's user defaults. - The update schedule cycle will be reset in a short delay after the property's new value is set. - This is to allow reverting this property without kicking off a schedule change immediately - */ -@property BOOL automaticallyChecksForUpdates; - -/*! - A property indicating whether or not updates can be automatically downloaded in the background. - - Note that automatic downloading of updates can be disallowed by the developer - or by the user's system if silent updates cannot be done (eg: if they require authentication). - In this case, -automaticallyDownloadsUpdates will return NO regardless of how this property is set. - - Setting this property will persist in the host bundle's user defaults. - */ -@property BOOL automaticallyDownloadsUpdates; - -/*! - A property indicating the current automatic update check interval. - - Setting this property will persist in the host bundle's user defaults. - The update schedule cycle will be reset in a short delay after the property's new value is set. - This is to allow reverting this property without kicking off a schedule change immediately - */ -@property NSTimeInterval updateCheckInterval; - -/*! - Begins a "probing" check for updates which will not actually offer to - update to that version. - - However, the delegate methods - SUUpdaterDelegate::updater:didFindValidUpdate: and - SUUpdaterDelegate::updaterDidNotFindUpdate: will be called, - so you can use that information in your UI. - - Updates that have been skipped by the user will not be found. - */ -- (void)checkForUpdateInformation; - -/*! - The URL of the appcast used to download update information. - - Setting this property will persist in the host bundle's user defaults. - If you don't want persistence, you may want to consider instead implementing - SUUpdaterDelegate::feedURLStringForUpdater: or SUUpdaterDelegate::feedParametersForUpdater:sendingSystemProfile: - - This property must be called on the main thread. - */ -@property (copy) NSURL *feedURL; - -/*! - The host bundle that is being updated. - */ -@property (readonly, strong) NSBundle *hostBundle; - -/*! - The bundle this class (SUUpdater) is loaded into. - */ -@property (strong, readonly) NSBundle *sparkleBundle; - -/*! - The user agent used when checking for updates. - - The default implementation can be overrided. - */ -@property (nonatomic, copy) NSString *userAgentString; - -/*! - The HTTP headers used when checking for updates. - - The keys of this dictionary are HTTP header fields (NSString) and values are corresponding values (NSString) - */ -#if __has_feature(objc_generics) -@property (copy) NSDictionary *httpHeaders; -#else -@property (copy) NSDictionary *httpHeaders; -#endif - -/*! - A property indicating whether or not the user's system profile information is sent when checking for updates. - - Setting this property will persist in the host bundle's user defaults. - */ -@property BOOL sendsSystemProfile; - -/*! - A property indicating the decryption password used for extracting updates shipped as Apple Disk Images (dmg) - */ -@property (nonatomic, copy) NSString *decryptionPassword; - -/*! - This function ignores normal update schedule, ignores user preferences, - and interrupts users with an unwanted immediate app update. - - WARNING: this function should not be used in regular apps. This function - is a user-unfriendly hack only for very special cases, like unstable - rapidly-changing beta builds that would not run correctly if they were - even one day out of date. - - Instead of this function you should set `SUAutomaticallyUpdate` to `YES`, - which will gracefully install updates when the app quits. - - For UI-less/daemon apps that aren't usually quit, instead of this function, - you can use the delegate method - SUUpdaterDelegate::updater:willInstallUpdateOnQuit:immediateInstallationInvocation: - to immediately start installation when an update was found. - - A progress dialog is shown but the user will never be prompted to read the - release notes. - - This function will cause update to be downloaded twice if automatic updates are - enabled. - - You may want to respond to the userDidCancelDownload delegate method in case - the user clicks the "Cancel" button while the update is downloading. - */ -- (void)installUpdatesIfAvailable; - -/*! - Returns the date of last update check. - - \returns \c nil if no check has been performed. - */ -@property (readonly, copy) NSDate *lastUpdateCheckDate; - -/*! - Appropriately schedules or cancels the update checking timer according to - the preferences for time interval and automatic checks. - - This call does not change the date of the next check, - but only the internal NSTimer. - */ -- (void)resetUpdateCycle; - -/*! - A property indicating whether or not an update is in progress. - - Note this property is not indicative of whether or not user initiated updates can be performed. - Use SUUpdater::validateMenuItem: for that instead. - */ -@property (readonly) BOOL updateInProgress; - -@end - -#endif diff --git a/Sparkle.framework/Versions/A/Headers/SUUpdaterDelegate.h b/Sparkle.framework/Versions/A/Headers/SUUpdaterDelegate.h deleted file mode 100644 index dbc1402401..0000000000 --- a/Sparkle.framework/Versions/A/Headers/SUUpdaterDelegate.h +++ /dev/null @@ -1,281 +0,0 @@ -// -// SUUpdaterDelegate.h -// Sparkle -// -// Created by Mayur Pawashe on 12/25/16. -// Copyright © 2016 Sparkle Project. All rights reserved. -// - -#if __has_feature(modules) -@import Foundation; -#else -#import -#endif - -#import "SUExport.h" - -@protocol SUVersionComparison, SUVersionDisplay; -@class SUUpdater, SUAppcast, SUAppcastItem; - -NS_ASSUME_NONNULL_BEGIN - -// ----------------------------------------------------------------------------- -// SUUpdater Notifications for events that might be interesting to more than just the delegate -// The updater will be the notification object -// ----------------------------------------------------------------------------- -SU_EXPORT extern NSString *const SUUpdaterDidFinishLoadingAppCastNotification; -SU_EXPORT extern NSString *const SUUpdaterDidFindValidUpdateNotification; -SU_EXPORT extern NSString *const SUUpdaterDidNotFindUpdateNotification; -SU_EXPORT extern NSString *const SUUpdaterWillRestartNotification; -#define SUUpdaterWillRelaunchApplicationNotification SUUpdaterWillRestartNotification; -#define SUUpdaterWillInstallUpdateNotification SUUpdaterWillRestartNotification; - -// Key for the SUAppcastItem object in the SUUpdaterDidFindValidUpdateNotification userInfo -SU_EXPORT extern NSString *const SUUpdaterAppcastItemNotificationKey; -// Key for the SUAppcast object in the SUUpdaterDidFinishLoadingAppCastNotification userInfo -SU_EXPORT extern NSString *const SUUpdaterAppcastNotificationKey; - -// ----------------------------------------------------------------------------- -// SUUpdater Delegate: -// ----------------------------------------------------------------------------- - -/*! - Provides methods to control the behavior of an SUUpdater object. - */ -@protocol SUUpdaterDelegate -@optional - -/*! - Returns whether to allow Sparkle to pop up. - - For example, this may be used to prevent Sparkle from interrupting a setup assistant. - - \param updater The SUUpdater instance. - */ -- (BOOL)updaterMayCheckForUpdates:(SUUpdater *)updater; - -/*! - Returns additional parameters to append to the appcast URL's query string. - - This is potentially based on whether or not Sparkle will also be sending along the system profile. - - \param updater The SUUpdater instance. - \param sendingProfile Whether the system profile will also be sent. - - \return An array of dictionaries with keys: "key", "value", "displayKey", "displayValue", the latter two being specifically for display to the user. - */ -#if __has_feature(objc_generics) -- (NSArray *> *)feedParametersForUpdater:(SUUpdater *)updater sendingSystemProfile:(BOOL)sendingProfile; -#else -- (NSArray *)feedParametersForUpdater:(SUUpdater *)updater sendingSystemProfile:(BOOL)sendingProfile; -#endif - -/*! - Returns a custom appcast URL. - - Override this to dynamically specify the entire URL. - - An alternative may be to use SUUpdaterDelegate::feedParametersForUpdater:sendingSystemProfile: - and let the server handle what kind of feed to provide. - - \param updater The SUUpdater instance. - */ -- (nullable NSString *)feedURLStringForUpdater:(SUUpdater *)updater; - -/*! - Returns whether Sparkle should prompt the user about automatic update checks. - - Use this to override the default behavior. - - \param updater The SUUpdater instance. - */ -- (BOOL)updaterShouldPromptForPermissionToCheckForUpdates:(SUUpdater *)updater; - -/*! - Called after Sparkle has downloaded the appcast from the remote server. - - Implement this if you want to do some special handling with the appcast once it finishes loading. - - \param updater The SUUpdater instance. - \param appcast The appcast that was downloaded from the remote server. - */ -- (void)updater:(SUUpdater *)updater didFinishLoadingAppcast:(SUAppcast *)appcast; - -/*! - Returns the item in the appcast corresponding to the update that should be installed. - - If you're using special logic or extensions in your appcast, - implement this to use your own logic for finding a valid update, if any, - in the given appcast. - - \param appcast The appcast that was downloaded from the remote server. - \param updater The SUUpdater instance. - */ -- (nullable SUAppcastItem *)bestValidUpdateInAppcast:(SUAppcast *)appcast forUpdater:(SUUpdater *)updater; - -/*! - Called when a valid update is found by the update driver. - - \param updater The SUUpdater instance. - \param item The appcast item corresponding to the update that is proposed to be installed. - */ -- (void)updater:(SUUpdater *)updater didFindValidUpdate:(SUAppcastItem *)item; - -/*! - Called when a valid update is not found. - - \param updater The SUUpdater instance. - */ -- (void)updaterDidNotFindUpdate:(SUUpdater *)updater; - -/*! - Called immediately before downloading the specified update. - - \param updater The SUUpdater instance. - \param item The appcast item corresponding to the update that is proposed to be downloaded. - \param request The mutable URL request that will be used to download the update. - */ -- (void)updater:(SUUpdater *)updater willDownloadUpdate:(SUAppcastItem *)item withRequest:(NSMutableURLRequest *)request; - -/*! - Called after the specified update failed to download. - - \param updater The SUUpdater instance. - \param item The appcast item corresponding to the update that failed to download. - \param error The error generated by the failed download. - */ -- (void)updater:(SUUpdater *)updater failedToDownloadUpdate:(SUAppcastItem *)item error:(NSError *)error; - -/*! - Called when the user clicks the cancel button while and update is being downloaded. - - \param updater The SUUpdater instance. - */ -- (void)userDidCancelDownload:(SUUpdater *)updater; - -/*! - Called immediately before installing the specified update. - - \param updater The SUUpdater instance. - \param item The appcast item corresponding to the update that is proposed to be installed. - */ -- (void)updater:(SUUpdater *)updater willInstallUpdate:(SUAppcastItem *)item; - -/*! - Returns whether the relaunch should be delayed in order to perform other tasks. - - This is not called if the user didn't relaunch on the previous update, - in that case it will immediately restart. - - \param updater The SUUpdater instance. - \param item The appcast item corresponding to the update that is proposed to be installed. - \param invocation The invocation that must be completed with `[invocation invoke]` before continuing with the relaunch. - - \return \c YES to delay the relaunch until \p invocation is invoked. - */ -- (BOOL)updater:(SUUpdater *)updater shouldPostponeRelaunchForUpdate:(SUAppcastItem *)item untilInvoking:(NSInvocation *)invocation; - -/*! - Returns whether the application should be relaunched at all. - - Some apps \b cannot be relaunched under certain circumstances. - This method can be used to explicitly prevent a relaunch. - - \param updater The SUUpdater instance. - */ -- (BOOL)updaterShouldRelaunchApplication:(SUUpdater *)updater; - -/*! - Called immediately before relaunching. - - \param updater The SUUpdater instance. - */ -- (void)updaterWillRelaunchApplication:(SUUpdater *)updater; - -/*! - Called immediately after relaunching. SUUpdater delegate must be set before applicationDidFinishLaunching: to catch this event. - - \param updater The SUUpdater instance. - */ -- (void)updaterDidRelaunchApplication:(SUUpdater *)updater; - -/*! - Returns an object that compares version numbers to determine their arithmetic relation to each other. - - This method allows you to provide a custom version comparator. - If you don't implement this method or return \c nil, - the standard version comparator will be used. - - \sa SUStandardVersionComparator - - \param updater The SUUpdater instance. - */ -- (nullable id)versionComparatorForUpdater:(SUUpdater *)updater; - -/*! - Returns an object that formats version numbers for display to the user. - - If you don't implement this method or return \c nil, - the standard version formatter will be used. - - \sa SUUpdateAlert - - \param updater The SUUpdater instance. - */ -- (nullable id)versionDisplayerForUpdater:(SUUpdater *)updater; - -/*! - Returns the path which is used to relaunch the client after the update is installed. - - The default is the path of the host bundle. - - \param updater The SUUpdater instance. - */ -- (nullable NSString *)pathToRelaunchForUpdater:(SUUpdater *)updater; - -/*! - Called before an updater shows a modal alert window, - to give the host the opportunity to hide attached windows that may get in the way. - - \param updater The SUUpdater instance. - */ -- (void)updaterWillShowModalAlert:(SUUpdater *)updater; - -/*! - Called after an updater shows a modal alert window, - to give the host the opportunity to hide attached windows that may get in the way. - - \param updater The SUUpdater instance. - */ -- (void)updaterDidShowModalAlert:(SUUpdater *)updater; - -/*! - Called when an update is scheduled to be silently installed on quit. - This is after an update has been automatically downloaded in the background. - (i.e. SUUpdater::automaticallyDownloadsUpdates is YES) - - \param updater The SUUpdater instance. - \param item The appcast item corresponding to the update that is proposed to be installed. - \param invocation Can be used to trigger an immediate silent install and relaunch. - */ -- (void)updater:(SUUpdater *)updater willInstallUpdateOnQuit:(SUAppcastItem *)item immediateInstallationInvocation:(NSInvocation *)invocation; - -/*! - Calls after an update that was scheduled to be silently installed on quit has been canceled. - - \param updater The SUUpdater instance. - \param item The appcast item corresponding to the update that was proposed to be installed. - */ -- (void)updater:(SUUpdater *)updater didCancelInstallUpdateOnQuit:(SUAppcastItem *)item; - -/*! - Called after an update is aborted due to an error. - - \param updater The SUUpdater instance. - \param error The error that caused the abort - */ -- (void)updater:(SUUpdater *)updater didAbortWithError:(NSError *)error; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Sparkle.framework/Versions/A/Headers/SUVersionComparisonProtocol.h b/Sparkle.framework/Versions/A/Headers/SUVersionComparisonProtocol.h deleted file mode 100644 index c654fc4d0f..0000000000 --- a/Sparkle.framework/Versions/A/Headers/SUVersionComparisonProtocol.h +++ /dev/null @@ -1,37 +0,0 @@ -// -// SUVersionComparisonProtocol.h -// Sparkle -// -// Created by Andy Matuschak on 12/21/07. -// Copyright 2007 Andy Matuschak. All rights reserved. -// - -#ifndef SUVERSIONCOMPARISONPROTOCOL_H -#define SUVERSIONCOMPARISONPROTOCOL_H - -#if __has_feature(modules) -@import Foundation; -#else -#import -#endif -#import "SUExport.h" - -NS_ASSUME_NONNULL_BEGIN - -/*! - Provides version comparison facilities for Sparkle. -*/ -@protocol SUVersionComparison - -/*! - An abstract method to compare two version strings. - - Should return NSOrderedAscending if b > a, NSOrderedDescending if b < a, - and NSOrderedSame if they are equivalent. -*/ -- (NSComparisonResult)compareVersion:(NSString *)versionA toVersion:(NSString *)versionB; // *** MAY BE CALLED ON NON-MAIN THREAD! - -@end - -NS_ASSUME_NONNULL_END -#endif diff --git a/Sparkle.framework/Versions/A/Headers/SUVersionDisplayProtocol.h b/Sparkle.framework/Versions/A/Headers/SUVersionDisplayProtocol.h deleted file mode 100644 index 980efb3fe7..0000000000 --- a/Sparkle.framework/Versions/A/Headers/SUVersionDisplayProtocol.h +++ /dev/null @@ -1,29 +0,0 @@ -// -// SUVersionDisplayProtocol.h -// EyeTV -// -// Created by Uli Kusterer on 08.12.09. -// Copyright 2009 Elgato Systems GmbH. All rights reserved. -// - -#if __has_feature(modules) -@import Foundation; -#else -#import -#endif -#import "SUExport.h" - -/*! - Applies special display formatting to version numbers. -*/ -@protocol SUVersionDisplay - -/*! - Formats two version strings. - - Both versions are provided so that important distinguishing information - can be displayed while also leaving out unnecessary/confusing parts. -*/ -- (void)formatVersion:(NSString *_Nonnull*_Nonnull)inOutVersionA andVersion:(NSString *_Nonnull*_Nonnull)inOutVersionB; - -@end diff --git a/Sparkle.framework/Versions/A/Headers/Sparkle.h b/Sparkle.framework/Versions/A/Headers/Sparkle.h deleted file mode 100644 index d3f6ff2b80..0000000000 --- a/Sparkle.framework/Versions/A/Headers/Sparkle.h +++ /dev/null @@ -1,24 +0,0 @@ -// -// Sparkle.h -// Sparkle -// -// Created by Andy Matuschak on 3/16/06. (Modified by CDHW on 23/12/07) -// Copyright 2006 Andy Matuschak. All rights reserved. -// - -#ifndef SPARKLE_H -#define SPARKLE_H - -// This list should include the shared headers. It doesn't matter if some of them aren't shared (unless -// there are name-space collisions) so we can list all of them to start with: - -#import "SUAppcast.h" -#import "SUAppcastItem.h" -#import "SUStandardVersionComparator.h" -#import "SUUpdater.h" -#import "SUUpdaterDelegate.h" -#import "SUVersionComparisonProtocol.h" -#import "SUVersionDisplayProtocol.h" -#import "SUErrors.h" - -#endif diff --git a/Sparkle.framework/Versions/A/Modules/module.modulemap b/Sparkle.framework/Versions/A/Modules/module.modulemap deleted file mode 100644 index af3fe6d050..0000000000 --- a/Sparkle.framework/Versions/A/Modules/module.modulemap +++ /dev/null @@ -1,6 +0,0 @@ -framework module Sparkle { - umbrella header "Sparkle.h" - - export * - module * { export * } -} diff --git a/Sparkle.framework/Versions/A/PrivateHeaders/SUUnarchiver.h b/Sparkle.framework/Versions/A/PrivateHeaders/SUUnarchiver.h deleted file mode 100644 index a52bf5a2dd..0000000000 --- a/Sparkle.framework/Versions/A/PrivateHeaders/SUUnarchiver.h +++ /dev/null @@ -1,21 +0,0 @@ -// -// SUUnarchiver.h -// Sparkle -// -// Created by Andy Matuschak on 3/16/06. -// Copyright 2006 Andy Matuschak. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -@protocol SUUnarchiverProtocol; - -@interface SUUnarchiver : NSObject - -+ (nullable id )unarchiverForPath:(NSString *)path updatingHostBundlePath:(nullable NSString *)hostPath decryptionPassword:(nullable NSString *)decryptionPassword; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Info.plist b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Info.plist deleted file mode 100644 index 74e911743c..0000000000 --- a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Info.plist +++ /dev/null @@ -1,54 +0,0 @@ - - - - - BuildMachineOSBuild - 17A330h - CFBundleDevelopmentRegion - English - CFBundleExecutable - Autoupdate - CFBundleIconFile - AppIcon.icns - CFBundleIdentifier - org.sparkle-project.Sparkle.Autoupdate - CFBundleInfoDictionaryVersion - 6.0 - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.18.1 21-g558cfd21 - CFBundleSignature - ???? - CFBundleSupportedPlatforms - - MacOSX - - CFBundleVersion - 1.18.1 - DTCompiler - com.apple.compilers.llvm.clang.1_0 - DTPlatformBuild - 9M136h - DTPlatformVersion - GM - DTSDKBuild - 17A263z - DTSDKName - macosx10.13 - DTXcode - 0900 - DTXcodeBuild - 9M136h - LSBackgroundOnly - 1 - LSMinimumSystemVersion - 10.7 - LSUIElement - 1 - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - - diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/MacOS/Autoupdate b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/MacOS/Autoupdate deleted file mode 100755 index 1afce79acf..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/MacOS/Autoupdate and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/MacOS/fileop b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/MacOS/fileop deleted file mode 100755 index cd0ae0b6aa..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/MacOS/fileop and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/PkgInfo b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/PkgInfo deleted file mode 100644 index bd04210fb4..0000000000 --- a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/PkgInfo +++ /dev/null @@ -1 +0,0 @@ -APPL???? \ No newline at end of file diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/AppIcon.icns b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/AppIcon.icns deleted file mode 100644 index 7f2a571c80..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/AppIcon.icns and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/SUStatus.nib b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/SUStatus.nib deleted file mode 100644 index 5fa3ecf6c0..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/SUStatus.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ar.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ar.lproj/Sparkle.strings deleted file mode 100644 index 4cd92c0dd7..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ar.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ca.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ca.lproj/Sparkle.strings deleted file mode 100644 index cc238f685a..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ca.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/cs.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/cs.lproj/Sparkle.strings deleted file mode 100644 index c93688a316..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/cs.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/da.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/da.lproj/Sparkle.strings deleted file mode 100644 index 10e3c5a5d8..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/da.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/de.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/de.lproj/Sparkle.strings deleted file mode 100644 index 9b0968f68a..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/de.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/el.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/el.lproj/Sparkle.strings deleted file mode 100644 index deed9efb22..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/el.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/en.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/en.lproj/Sparkle.strings deleted file mode 100644 index 842d2551f5..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/en.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/es.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/es.lproj/Sparkle.strings deleted file mode 100644 index 3027ecdcb6..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/es.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/fi.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/fi.lproj/Sparkle.strings deleted file mode 100644 index 32d3107f92..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/fi.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/fr.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/fr.lproj/Sparkle.strings deleted file mode 100644 index e4294a58ec..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/fr.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/he.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/he.lproj/Sparkle.strings deleted file mode 100644 index 99124ccc88..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/he.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/is.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/is.lproj/Sparkle.strings deleted file mode 100644 index 74ae72802a..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/is.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/it.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/it.lproj/Sparkle.strings deleted file mode 100644 index 68b6d366bc..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/it.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ja.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ja.lproj/Sparkle.strings deleted file mode 100644 index 5d2315cf55..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ja.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ko.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ko.lproj/Sparkle.strings deleted file mode 100644 index 92c18eeb2a..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ko.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/nb.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/nb.lproj/Sparkle.strings deleted file mode 100644 index ec2561b8ad..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/nb.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/nl.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/nl.lproj/Sparkle.strings deleted file mode 100644 index 58be0e82bb..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/nl.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/pl.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/pl.lproj/Sparkle.strings deleted file mode 100644 index 2b9c461520..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/pl.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/pt_BR.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/pt_BR.lproj/Sparkle.strings deleted file mode 100644 index c94db0986b..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/pt_BR.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/pt_PT.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/pt_PT.lproj/Sparkle.strings deleted file mode 100644 index 00df86ff13..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/pt_PT.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ro.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ro.lproj/Sparkle.strings deleted file mode 100644 index 318baa960d..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ro.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ru.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ru.lproj/Sparkle.strings deleted file mode 100644 index c33086d89f..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/ru.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/sk.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/sk.lproj/Sparkle.strings deleted file mode 100644 index a7d2ebce67..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/sk.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/sl.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/sl.lproj/Sparkle.strings deleted file mode 100644 index 1be2a80798..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/sl.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/sv.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/sv.lproj/Sparkle.strings deleted file mode 100644 index 738c9008b4..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/sv.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/th.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/th.lproj/Sparkle.strings deleted file mode 100644 index eca2570247..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/th.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/tr.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/tr.lproj/Sparkle.strings deleted file mode 100644 index 4def140e5a..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/tr.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/uk.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/uk.lproj/Sparkle.strings deleted file mode 100644 index f7eb257b7e..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/uk.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/zh_CN.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/zh_CN.lproj/Sparkle.strings deleted file mode 100644 index 214331cd13..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/zh_CN.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/zh_TW.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/zh_TW.lproj/Sparkle.strings deleted file mode 100644 index 533e208624..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/Autoupdate.app/Contents/Resources/zh_TW.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/Info.plist b/Sparkle.framework/Versions/A/Resources/Info.plist deleted file mode 100644 index f70151a2c8..0000000000 --- a/Sparkle.framework/Versions/A/Resources/Info.plist +++ /dev/null @@ -1,44 +0,0 @@ - - - - - BuildMachineOSBuild - 17A330h - CFBundleDevelopmentRegion - en - CFBundleExecutable - Sparkle - CFBundleIdentifier - org.sparkle-project.Sparkle - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - Sparkle - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.18.1 21-g558cfd21 - CFBundleSignature - ???? - CFBundleSupportedPlatforms - - MacOSX - - CFBundleVersion - 1.18.1 - DTCompiler - com.apple.compilers.llvm.clang.1_0 - DTPlatformBuild - 9M136h - DTPlatformVersion - GM - DTSDKBuild - 17A263z - DTSDKName - macosx10.13 - DTXcode - 0900 - DTXcodeBuild - 9M136h - - diff --git a/Sparkle.framework/Versions/A/Resources/SUModelTranslation.plist b/Sparkle.framework/Versions/A/Resources/SUModelTranslation.plist deleted file mode 100644 index 1f75b248c5..0000000000 --- a/Sparkle.framework/Versions/A/Resources/SUModelTranslation.plist +++ /dev/null @@ -1,314 +0,0 @@ - - - - - ADP2,1 - Developer Transition Kit - iMac1,1 - iMac G3 (Rev A-D) - iMac4,1 - iMac (Core Duo) - iMac4,2 - iMac for Education (17 inch, Core Duo) - iMac5,1 - iMac (Core 2 Duo, 17 or 20 inch, SuperDrive) - iMac5,2 - iMac (Core 2 Duo, 17 inch, Combo Drive) - iMac6,1 - iMac (Core 2 Duo, 24 inch, SuperDrive) - iMac7,1 - iMac Intel Core 2 Duo (aluminum enclosure) - iMac8,1 - iMac (Core 2 Duo, 20 or 24 inch, Early 2008 ) - iMac9,1 - iMac (Core 2 Duo, 20 or 24 inch, Early or Mid 2009 ) - iMac10,1 - iMac (Core 2 Duo, 21.5 or 27 inch, Late 2009 ) - iMac11,1 - iMac (Core i5 or i7, 27 inch Late 2009) - iMac11,2 - 21.5" iMac (mid 2010) - iMac11,3 - iMac (Core i5 or i7, 27 inch Mid 2010) - iMac12,1 - iMac (Core i3 or i5 or i7, 21.5 inch Mid 2010 or Late 2011) - iMac12,2 - iMac (Core i5 or i7, 27 inch Mid 2011) - iMac13,1 - iMac (Core i3 or i5 or i7, 21.5 inch Late 2012 or Early 2013) - iMac13,2 - iMac (Core i5 or i7, 27 inch Late 2012) - iMac14,1 - iMac (Core i5, 21.5 inch Late 2013) - iMac14,2 - iMac (Core i5 or i7, 27 inch Late 2013) - iMac14,3 - iMac (Core i5 or i7, 21.5 inch Late 2013) - iMac14,4 - iMac (Core i5, 21.5 inch Mid 2014) - iMac15,1 - iMac (Retina 5K Core i5 or i7, 27 inch Late 2014 or Mid 2015) - iMac16,1 - iMac (Core i5, 21,5 inch Late 2015) - iMac16,2 - iMac (Retina 4K Core i5 or i7, 21.5 inch Late 2015) - iMac17,1 - iMac (Retina 5K Core i5 or i7, 27 inch Late 2015) - MacBook1,1 - MacBook (Core Duo) - MacBook2,1 - MacBook (Core 2 Duo) - MacBook4,1 - MacBook (Core 2 Duo Feb 2008) - MacBook5,1 - MacBook (Core 2 Duo, Late 2008, Unibody) - MacBook5,2 - MacBook (Core 2 Duo, Early 2009, White) - MacBook6,1 - MacBook (Core 2 Duo, Late 2009, Unibody) - MacBook7,1 - MacBook (Core 2 Duo, Mid 2010, White) - MacBook8,1 - MacBook (Core M, 12 inch, Early 2015) - MacBookAir1,1 - MacBook Air (Core 2 Duo, 13 inch, Early 2008) - MacBookAir2,1 - MacBook Air (Core 2 Duo, 13 inch, Mid 2009) - MacBookAir3,1 - MacBook Air (Core 2 Duo, 11 inch, Late 2010) - MacBookAir3,2 - MacBook Air (Core 2 Duo, 13 inch, Late 2010) - MacBookAir4,1 - MacBook Air (Core i5 or i7, 11 inch, Mid 2011) - MacBookAir4,2 - MacBook Air (Core i5 or i7, 13 inch, Mid 2011) - MacBookAir5,1 - MacBook Air (Core i5 or i7, 11 inch, Mid 2012) - MacBookAir5,2 - MacBook Air (Core i5 or i7, 13 inch, Mid 2012) - MacBookAir6,1 - MacBook Air (Core i5 or i7, 11 inch, Mid 2013 or Early 2014) - MacBookAir6,2 - MacBook Air (Core i5 or i7, 13 inch, Mid 2013 or Early 2014) - MacBookAir7,1 - MacBook Air (Core i5 or i7, 11 inch, Early 2015) - MacBookAir7,2 - MacBook Air (Core i5 or i7, 13 inch, Early 2015) - MacBookPro1,1 - MacBook Pro Core Duo (15-inch) - MacBookPro1,2 - MacBook Pro Core Duo (17-inch) - MacBookPro2,1 - MacBook Pro Core 2 Duo (17-inch) - MacBookPro2,2 - MacBook Pro Core 2 Duo (15-inch) - MacBookPro3,1 - MacBook Pro Core 2 Duo (15-inch LED, Core 2 Duo) - MacBookPro3,2 - MacBook Pro Core 2 Duo (17-inch HD, Core 2 Duo) - MacBookPro4,1 - MacBook Pro (Core 2 Duo Feb 2008) - MacBookPro5,1 - MacBook Pro Intel Core 2 Duo (aluminum unibody) - MacBookPro5,2 - MacBook Pro Intel Core 2 Duo (aluminum unibody) - MacBookPro5,3 - MacBook Pro Intel Core 2 Duo (aluminum unibody) - MacBookPro5,4 - MacBook Pro Intel Core 2 Duo (aluminum unibody) - MacBookPro5,5 - MacBook Pro Intel Core 2 Duo (aluminum unibody) - MacBookPro6,1 - MacBook Pro Intel Core i5, Intel Core i7 (mid 2010) - MacBookPro6,2 - MacBook Pro Intel Core i5, Intel Core i7 (mid 2010) - MacBookPro7,1 - MacBook Pro Intel Core 2 Duo (mid 2010) - MacBookPro8,1 - MacBook Pro Intel Core i5, Intel Core i7, 13" (early 2011) - MacBookPro8,2 - MacBook Pro Intel Core i7, 15" (early 2011) - MacBookPro8,3 - MacBook Pro Intel Core i7, 17" (early 2011) - MacBookPro9,1 - MacBook Pro (15-inch, Mid 2012) - MacBookPro9,2 - MacBook Pro (13-inch, Mid 2012) - MacBookPro10,1 - MacBook Pro (Retina, Mid 2012) - MacBookPro10,2 - MacBook Pro (Retina, 13-inch, Late 2012) - MacBookPro11,1 - MacBook Pro (Retina, 13-inch, Late 2013) - MacBookPro11,2 - MacBook Pro (Retina, 15-inch, Late 2013) - MacBookPro11,3 - MacBook Pro (Retina, 15-inch, Late 2013) - MacbookPro11,4 - MacBook Pro (Retina, 15-inch, Mid 2015) - MacbookPro11,5 - MacBook Pro (Retina, 15-inch, Mid 2015) - MacbookPro12,1  - MacBook Pro (Retina, 13-inch, Early 2015) - Macmini1,1 - Mac Mini (Core Solo/Duo) - Macmini2,1 - Mac mini Intel Core - Macmini3,1 - Mac mini Intel Core - Macmini4,1 - Mac mini Intel Core (Mid 2010) - Macmini5,1 - Mac mini (Core i5, Mid 2011) - Macmini5,2 - Mac mini (Core i5 or Core i7, Mid 2011) - Macmini5,3 - Mac mini (Core i7, Server, Mid 2011) - Macmini6,1 - Mac mini (Core i5, Late 2012) - Macmini6,2 - Mac mini (Core i7, Normal or Server, Late 2012) - Macmini7,1 - Mac mini (Core i5 or Core i7, Late 2014) - MacPro1,1,Quad - Mac Pro - MacPro1,1 - Mac Pro (four-core) - MacPro2,1 - Mac Pro (eight-core) - MacPro3,1 - Mac Pro (January 2008 4- or 8- core "Harpertown") - MacPro4,1 - Mac Pro (March 2009) - MacPro5,1 - Mac Pro (2010 or 2012) - MacPro6,1 - Mac Pro (Late 2013) - PowerBook1,1 - PowerBook G3 - PowerBook2,1 - iBook G3 - PowerBook2,2 - iBook G3 (FireWire) - PowerBook2,3 - iBook G3 - PowerBook2,4 - iBook G3 - PowerBook3,1 - PowerBook G3 (FireWire) - PowerBook3,2 - PowerBook G4 - PowerBook3,3 - PowerBook G4 (Gigabit Ethernet) - PowerBook3,4 - PowerBook G4 (DVI) - PowerBook3,5 - PowerBook G4 (1GHz / 867MHz) - PowerBook4,1 - iBook G3 (Dual USB, Late 2001) - PowerBook4,2 - iBook G3 (16MB VRAM) - PowerBook4,3 - iBook G3 Opaque 16MB VRAM, 32MB VRAM, Early 2003) - PowerBook5,1 - PowerBook G4 (17 inch) - PowerBook5,2 - PowerBook G4 (15 inch FW 800) - PowerBook5,3 - PowerBook G4 (17-inch 1.33GHz) - PowerBook5,4 - PowerBook G4 (15 inch 1.5/1.33GHz) - PowerBook5,5 - PowerBook G4 (17-inch 1.5GHz) - PowerBook5,6 - PowerBook G4 (15 inch 1.67GHz/1.5GHz) - PowerBook5,7 - PowerBook G4 (17-inch 1.67GHz) - PowerBook5,8 - PowerBook G4 (Double layer SD, 15 inch) - PowerBook5,9 - PowerBook G4 (Double layer SD, 17 inch) - PowerBook6,1 - PowerBook G4 (12 inch) - PowerBook6,2 - PowerBook G4 (12 inch, DVI) - PowerBook6,3 - iBook G4 - PowerBook6,4 - PowerBook G4 (12 inch 1.33GHz) - PowerBook6,5 - iBook G4 (Early-Late 2004) - PowerBook6,7 - iBook G4 (Mid 2005) - PowerBook6,8 - PowerBook G4 (12 inch 1.5GHz) - PowerMac1,1 - Power Macintosh G3 (Blue & White) - PowerMac1,2 - Power Macintosh G4 (PCI Graphics) - PowerMac2,1 - iMac G3 (Slot-loading CD-ROM) - PowerMac2,2 - iMac G3 (Summer 2000) - PowerMac3,1 - Power Macintosh G4 (AGP Graphics) - PowerMac3,2 - Power Macintosh G4 (AGP Graphics) - PowerMac3,3 - Power Macintosh G4 (Gigabit Ethernet) - PowerMac3,4 - Power Macintosh G4 (Digital Audio) - PowerMac3,5 - Power Macintosh G4 (Quick Silver) - PowerMac3,6 - Power Macintosh G4 (Mirrored Drive Door) - PowerMac4,1 - iMac G3 (Early/Summer 2001) - PowerMac4,2 - iMac G4 (Flat Panel) - PowerMac4,4 - eMac - PowerMac4,5 - iMac G4 (17-inch Flat Panel) - PowerMac5,1 - Power Macintosh G4 Cube - PowerMac5,2 - Power Mac G4 Cube - PowerMac6,1 - iMac G4 (USB 2.0) - PowerMac6,3 - iMac G4 (20-inch Flat Panel) - PowerMac6,4 - eMac (USB 2.0, 2005) - PowerMac7,2 - Power Macintosh G5 - PowerMac7,3 - Power Macintosh G5 - PowerMac8,1 - iMac G5 - PowerMac8,2 - iMac G5 (Ambient Light Sensor) - PowerMac9,1 - Power Macintosh G5 (Late 2005) - PowerMac10,1 - Mac Mini G4 - PowerMac10,2 - Mac Mini (Late 2005) - PowerMac11,2 - Power Macintosh G5 (Late 2005) - PowerMac12,1 - iMac G5 (iSight) - RackMac1,1 - Xserve G4 - RackMac1,2 - Xserve G4 (slot-loading, cluster node) - RackMac3,1 - Xserve G5 - Xserve1,1 - Xserve (Intel Xeon) - Xserve2,1 - Xserve (January 2008 quad-core) - Xserve3,1 - Xserve (early 2009) - - diff --git a/Sparkle.framework/Versions/A/Resources/SUStatus.nib b/Sparkle.framework/Versions/A/Resources/SUStatus.nib deleted file mode 100644 index 5fa3ecf6c0..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/SUStatus.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/ar.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/ar.lproj/SUAutomaticUpdateAlert.nib deleted file mode 100644 index ee833b0c8c..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/ar.lproj/SUAutomaticUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/ar.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/ar.lproj/SUUpdateAlert.nib deleted file mode 100644 index a3bf0ac030..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/ar.lproj/SUUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/ar.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/ar.lproj/SUUpdatePermissionPrompt.nib deleted file mode 100644 index 6889325724..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/ar.lproj/SUUpdatePermissionPrompt.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/ar.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/ar.lproj/Sparkle.strings deleted file mode 100644 index 4cd92c0dd7..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/ar.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/ca.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/ca.lproj/Sparkle.strings deleted file mode 100644 index cc238f685a..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/ca.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/cs.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/cs.lproj/SUAutomaticUpdateAlert.nib deleted file mode 100644 index 114105bf35..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/cs.lproj/SUAutomaticUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/cs.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/cs.lproj/SUUpdateAlert.nib deleted file mode 100644 index f4323fb491..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/cs.lproj/SUUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/cs.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/cs.lproj/SUUpdatePermissionPrompt.nib deleted file mode 100644 index d5f66622f8..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/cs.lproj/SUUpdatePermissionPrompt.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/cs.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/cs.lproj/Sparkle.strings deleted file mode 100644 index c93688a316..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/cs.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/da.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/da.lproj/SUAutomaticUpdateAlert.nib deleted file mode 100644 index 25e40b2ffd..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/da.lproj/SUAutomaticUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/da.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/da.lproj/SUUpdateAlert.nib deleted file mode 100644 index 043d4b8ee3..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/da.lproj/SUUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/da.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/da.lproj/SUUpdatePermissionPrompt.nib deleted file mode 100644 index c9d44e9780..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/da.lproj/SUUpdatePermissionPrompt.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/da.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/da.lproj/Sparkle.strings deleted file mode 100644 index 10e3c5a5d8..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/da.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/de.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/de.lproj/SUAutomaticUpdateAlert.nib deleted file mode 100644 index 0447897bdd..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/de.lproj/SUAutomaticUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/de.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/de.lproj/SUUpdateAlert.nib deleted file mode 100644 index cc343d41cd..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/de.lproj/SUUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/de.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/de.lproj/SUUpdatePermissionPrompt.nib deleted file mode 100644 index 5d584dcb6b..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/de.lproj/SUUpdatePermissionPrompt.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/de.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/de.lproj/Sparkle.strings deleted file mode 100644 index 9b0968f68a..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/de.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/el.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/el.lproj/SUAutomaticUpdateAlert.nib deleted file mode 100644 index c4a64b4a69..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/el.lproj/SUAutomaticUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/el.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/el.lproj/SUUpdateAlert.nib deleted file mode 100644 index 46cbdc34dd..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/el.lproj/SUUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/el.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/el.lproj/SUUpdatePermissionPrompt.nib deleted file mode 100644 index c24a4aa946..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/el.lproj/SUUpdatePermissionPrompt.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/el.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/el.lproj/Sparkle.strings deleted file mode 100644 index deed9efb22..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/el.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/en.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/en.lproj/SUAutomaticUpdateAlert.nib deleted file mode 100644 index 4236118ee3..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/en.lproj/SUAutomaticUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdateAlert.nib deleted file mode 100644 index 12b3e8e1f8..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdatePermissionPrompt.nib deleted file mode 100644 index 1dd0f78492..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/en.lproj/SUUpdatePermissionPrompt.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/en.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/en.lproj/Sparkle.strings deleted file mode 100644 index 842d2551f5..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/en.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/es.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/es.lproj/SUAutomaticUpdateAlert.nib deleted file mode 100644 index 130d71a265..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/es.lproj/SUAutomaticUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/es.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/es.lproj/SUUpdateAlert.nib deleted file mode 100644 index e24c3ca5b6..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/es.lproj/SUUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/es.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/es.lproj/SUUpdatePermissionPrompt.nib deleted file mode 100644 index caeb75c579..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/es.lproj/SUUpdatePermissionPrompt.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/es.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/es.lproj/Sparkle.strings deleted file mode 100644 index 3027ecdcb6..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/es.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/fi.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/fi.lproj/Sparkle.strings deleted file mode 100644 index 32d3107f92..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/fi.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/fr.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/fr.lproj/SUAutomaticUpdateAlert.nib deleted file mode 100644 index aded902855..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/fr.lproj/SUAutomaticUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/fr.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/fr.lproj/SUUpdateAlert.nib deleted file mode 100644 index 247aab8464..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/fr.lproj/SUUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/fr.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/fr.lproj/SUUpdatePermissionPrompt.nib deleted file mode 100644 index a2468beff9..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/fr.lproj/SUUpdatePermissionPrompt.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/fr.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/fr.lproj/Sparkle.strings deleted file mode 100644 index e4294a58ec..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/fr.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/fr_CA.lproj b/Sparkle.framework/Versions/A/Resources/fr_CA.lproj deleted file mode 120000 index f9834a395e..0000000000 --- a/Sparkle.framework/Versions/A/Resources/fr_CA.lproj +++ /dev/null @@ -1 +0,0 @@ -fr.lproj \ No newline at end of file diff --git a/Sparkle.framework/Versions/A/Resources/he.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/he.lproj/Sparkle.strings deleted file mode 100644 index 99124ccc88..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/he.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/is.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/is.lproj/SUAutomaticUpdateAlert.nib deleted file mode 100644 index b10697c0c1..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/is.lproj/SUAutomaticUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/is.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/is.lproj/SUUpdateAlert.nib deleted file mode 100644 index 049132f2e4..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/is.lproj/SUUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/is.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/is.lproj/SUUpdatePermissionPrompt.nib deleted file mode 100644 index 7533345bdc..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/is.lproj/SUUpdatePermissionPrompt.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/is.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/is.lproj/Sparkle.strings deleted file mode 100644 index 74ae72802a..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/is.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/it.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/it.lproj/SUAutomaticUpdateAlert.nib deleted file mode 100644 index 75d8251794..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/it.lproj/SUAutomaticUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/it.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/it.lproj/SUUpdateAlert.nib deleted file mode 100644 index 14b4e51a74..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/it.lproj/SUUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/it.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/it.lproj/SUUpdatePermissionPrompt.nib deleted file mode 100644 index 3bd60d3d0d..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/it.lproj/SUUpdatePermissionPrompt.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/it.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/it.lproj/Sparkle.strings deleted file mode 100644 index 68b6d366bc..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/it.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/ja.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/ja.lproj/SUAutomaticUpdateAlert.nib deleted file mode 100644 index c36e920f86..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/ja.lproj/SUAutomaticUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/ja.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/ja.lproj/SUUpdateAlert.nib deleted file mode 100644 index ebbd509c06..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/ja.lproj/SUUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/ja.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/ja.lproj/SUUpdatePermissionPrompt.nib deleted file mode 100644 index a7b0bddbc5..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/ja.lproj/SUUpdatePermissionPrompt.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/ja.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/ja.lproj/Sparkle.strings deleted file mode 100644 index 5d2315cf55..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/ja.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/ko.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/ko.lproj/SUAutomaticUpdateAlert.nib deleted file mode 100644 index 86f6040197..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/ko.lproj/SUAutomaticUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/ko.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/ko.lproj/SUUpdateAlert.nib deleted file mode 100644 index 9fb502c4d9..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/ko.lproj/SUUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/ko.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/ko.lproj/SUUpdatePermissionPrompt.nib deleted file mode 100644 index 149e81d516..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/ko.lproj/SUUpdatePermissionPrompt.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/ko.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/ko.lproj/Sparkle.strings deleted file mode 100644 index 92c18eeb2a..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/ko.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/nb.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/nb.lproj/SUAutomaticUpdateAlert.nib deleted file mode 100644 index 4808185707..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/nb.lproj/SUAutomaticUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/nb.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/nb.lproj/SUUpdateAlert.nib deleted file mode 100644 index 211dc8fd10..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/nb.lproj/SUUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/nb.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/nb.lproj/SUUpdatePermissionPrompt.nib deleted file mode 100644 index 86e42a6940..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/nb.lproj/SUUpdatePermissionPrompt.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/nb.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/nb.lproj/Sparkle.strings deleted file mode 100644 index ec2561b8ad..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/nb.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/nl.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/nl.lproj/SUAutomaticUpdateAlert.nib deleted file mode 100644 index e4ec625c70..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/nl.lproj/SUAutomaticUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/nl.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/nl.lproj/SUUpdateAlert.nib deleted file mode 100644 index 0ba6762f0c..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/nl.lproj/SUUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/nl.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/nl.lproj/SUUpdatePermissionPrompt.nib deleted file mode 100644 index 4f9b38ba97..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/nl.lproj/SUUpdatePermissionPrompt.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/nl.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/nl.lproj/Sparkle.strings deleted file mode 100644 index 58be0e82bb..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/nl.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/pl.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/pl.lproj/SUAutomaticUpdateAlert.nib deleted file mode 100644 index e48d71c36b..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/pl.lproj/SUAutomaticUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/pl.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/pl.lproj/SUUpdateAlert.nib deleted file mode 100644 index fe98402073..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/pl.lproj/SUUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/pl.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/pl.lproj/SUUpdatePermissionPrompt.nib deleted file mode 100644 index 0e8f33d47e..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/pl.lproj/SUUpdatePermissionPrompt.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/pl.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/pl.lproj/Sparkle.strings deleted file mode 100644 index 2b9c461520..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/pl.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/pt.lproj b/Sparkle.framework/Versions/A/Resources/pt.lproj deleted file mode 120000 index 3c1c9f6dce..0000000000 --- a/Sparkle.framework/Versions/A/Resources/pt.lproj +++ /dev/null @@ -1 +0,0 @@ -pt_BR.lproj \ No newline at end of file diff --git a/Sparkle.framework/Versions/A/Resources/pt_BR.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/pt_BR.lproj/SUAutomaticUpdateAlert.nib deleted file mode 100644 index d185ad8f23..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/pt_BR.lproj/SUAutomaticUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/pt_BR.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/pt_BR.lproj/SUUpdateAlert.nib deleted file mode 100644 index 93a0e0e3a3..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/pt_BR.lproj/SUUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/pt_BR.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/pt_BR.lproj/SUUpdatePermissionPrompt.nib deleted file mode 100644 index 10f4cdeea4..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/pt_BR.lproj/SUUpdatePermissionPrompt.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/pt_BR.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/pt_BR.lproj/Sparkle.strings deleted file mode 100644 index c94db0986b..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/pt_BR.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/pt_PT.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/pt_PT.lproj/SUAutomaticUpdateAlert.nib deleted file mode 100644 index 2f95438888..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/pt_PT.lproj/SUAutomaticUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/pt_PT.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/pt_PT.lproj/SUUpdateAlert.nib deleted file mode 100644 index 90a40d4a3b..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/pt_PT.lproj/SUUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/pt_PT.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/pt_PT.lproj/SUUpdatePermissionPrompt.nib deleted file mode 100644 index 4e84e87de4..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/pt_PT.lproj/SUUpdatePermissionPrompt.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/pt_PT.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/pt_PT.lproj/Sparkle.strings deleted file mode 100644 index 00df86ff13..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/pt_PT.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/ro.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/ro.lproj/SUAutomaticUpdateAlert.nib deleted file mode 100644 index 8e791fa3f7..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/ro.lproj/SUAutomaticUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/ro.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/ro.lproj/SUUpdateAlert.nib deleted file mode 100644 index 244b6787ff..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/ro.lproj/SUUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/ro.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/ro.lproj/SUUpdatePermissionPrompt.nib deleted file mode 100644 index e86d1d05d2..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/ro.lproj/SUUpdatePermissionPrompt.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/ro.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/ro.lproj/Sparkle.strings deleted file mode 100644 index 318baa960d..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/ro.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/ru.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/ru.lproj/SUAutomaticUpdateAlert.nib deleted file mode 100644 index c7f72e387c..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/ru.lproj/SUAutomaticUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/ru.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/ru.lproj/SUUpdateAlert.nib deleted file mode 100644 index 5a5626abe2..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/ru.lproj/SUUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/ru.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/ru.lproj/SUUpdatePermissionPrompt.nib deleted file mode 100644 index c09c353967..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/ru.lproj/SUUpdatePermissionPrompt.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/ru.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/ru.lproj/Sparkle.strings deleted file mode 100644 index c33086d89f..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/ru.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/sk.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/sk.lproj/SUAutomaticUpdateAlert.nib deleted file mode 100644 index 0573ebc6dc..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/sk.lproj/SUAutomaticUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/sk.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/sk.lproj/SUUpdateAlert.nib deleted file mode 100644 index 91e8c17cae..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/sk.lproj/SUUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/sk.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/sk.lproj/SUUpdatePermissionPrompt.nib deleted file mode 100644 index 5b732b8db4..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/sk.lproj/SUUpdatePermissionPrompt.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/sk.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/sk.lproj/Sparkle.strings deleted file mode 100644 index a7d2ebce67..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/sk.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/sl.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/sl.lproj/SUAutomaticUpdateAlert.nib deleted file mode 100644 index 4fa1df5e0c..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/sl.lproj/SUAutomaticUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/sl.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/sl.lproj/SUUpdateAlert.nib deleted file mode 100644 index 382d671e59..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/sl.lproj/SUUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/sl.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/sl.lproj/SUUpdatePermissionPrompt.nib deleted file mode 100644 index 5333ab24ff..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/sl.lproj/SUUpdatePermissionPrompt.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/sl.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/sl.lproj/Sparkle.strings deleted file mode 100644 index 1be2a80798..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/sl.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/sv.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/sv.lproj/SUAutomaticUpdateAlert.nib deleted file mode 100644 index 4604b86648..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/sv.lproj/SUAutomaticUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/sv.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/sv.lproj/SUUpdateAlert.nib deleted file mode 100644 index a26c7fda4d..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/sv.lproj/SUUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/sv.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/sv.lproj/SUUpdatePermissionPrompt.nib deleted file mode 100644 index ddc182489e..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/sv.lproj/SUUpdatePermissionPrompt.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/sv.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/sv.lproj/Sparkle.strings deleted file mode 100644 index 738c9008b4..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/sv.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/th.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/th.lproj/SUAutomaticUpdateAlert.nib deleted file mode 100644 index e45b9a15a2..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/th.lproj/SUAutomaticUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/th.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/th.lproj/SUUpdateAlert.nib deleted file mode 100644 index 5a66b95cca..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/th.lproj/SUUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/th.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/th.lproj/SUUpdatePermissionPrompt.nib deleted file mode 100644 index 9172044316..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/th.lproj/SUUpdatePermissionPrompt.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/th.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/th.lproj/Sparkle.strings deleted file mode 100644 index eca2570247..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/th.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/tr.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/tr.lproj/SUAutomaticUpdateAlert.nib deleted file mode 100644 index ec8ff840f1..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/tr.lproj/SUAutomaticUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/tr.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/tr.lproj/SUUpdateAlert.nib deleted file mode 100644 index 26f8576817..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/tr.lproj/SUUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/tr.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/tr.lproj/SUUpdatePermissionPrompt.nib deleted file mode 100644 index 5c7359ad2e..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/tr.lproj/SUUpdatePermissionPrompt.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/tr.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/tr.lproj/Sparkle.strings deleted file mode 100644 index 4def140e5a..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/tr.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/uk.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/uk.lproj/SUAutomaticUpdateAlert.nib deleted file mode 100644 index d8920a5103..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/uk.lproj/SUAutomaticUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/uk.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/uk.lproj/SUUpdateAlert.nib deleted file mode 100644 index 5c6c5213cc..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/uk.lproj/SUUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/uk.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/uk.lproj/SUUpdatePermissionPrompt.nib deleted file mode 100644 index 72912e9b42..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/uk.lproj/SUUpdatePermissionPrompt.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/uk.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/uk.lproj/Sparkle.strings deleted file mode 100644 index f7eb257b7e..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/uk.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/zh_CN.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/zh_CN.lproj/SUAutomaticUpdateAlert.nib deleted file mode 100644 index 23759231db..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/zh_CN.lproj/SUAutomaticUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/zh_CN.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/zh_CN.lproj/SUUpdateAlert.nib deleted file mode 100644 index f6dcd869f4..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/zh_CN.lproj/SUUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/zh_CN.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/zh_CN.lproj/SUUpdatePermissionPrompt.nib deleted file mode 100644 index 1107ca76ef..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/zh_CN.lproj/SUUpdatePermissionPrompt.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/zh_CN.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/zh_CN.lproj/Sparkle.strings deleted file mode 100644 index 214331cd13..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/zh_CN.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/zh_TW.lproj/SUAutomaticUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/zh_TW.lproj/SUAutomaticUpdateAlert.nib deleted file mode 100644 index 9abca3c2e5..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/zh_TW.lproj/SUAutomaticUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/zh_TW.lproj/SUUpdateAlert.nib b/Sparkle.framework/Versions/A/Resources/zh_TW.lproj/SUUpdateAlert.nib deleted file mode 100644 index 9065be8439..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/zh_TW.lproj/SUUpdateAlert.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/zh_TW.lproj/SUUpdatePermissionPrompt.nib b/Sparkle.framework/Versions/A/Resources/zh_TW.lproj/SUUpdatePermissionPrompt.nib deleted file mode 100644 index 919230b50e..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/zh_TW.lproj/SUUpdatePermissionPrompt.nib and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Resources/zh_TW.lproj/Sparkle.strings b/Sparkle.framework/Versions/A/Resources/zh_TW.lproj/Sparkle.strings deleted file mode 100644 index 533e208624..0000000000 Binary files a/Sparkle.framework/Versions/A/Resources/zh_TW.lproj/Sparkle.strings and /dev/null differ diff --git a/Sparkle.framework/Versions/A/Sparkle b/Sparkle.framework/Versions/A/Sparkle deleted file mode 100755 index ce286a23fb..0000000000 Binary files a/Sparkle.framework/Versions/A/Sparkle and /dev/null differ diff --git a/Sparkle.framework/Versions/Current b/Sparkle.framework/Versions/Current deleted file mode 120000 index 8c7e5a667f..0000000000 --- a/Sparkle.framework/Versions/Current +++ /dev/null @@ -1 +0,0 @@ -A \ No newline at end of file diff --git a/TGUIKit/TGUIKit.xcodeproj/project.pbxproj b/TGUIKit/TGUIKit.xcodeproj/project.pbxproj deleted file mode 100644 index 9088c89dec..0000000000 --- a/TGUIKit/TGUIKit.xcodeproj/project.pbxproj +++ /dev/null @@ -1,993 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - C20232B61D845B0B007C9ADE /* TextNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20232B51D845B0B007C9ADE /* TextNode.swift */; }; - C20232B81D85525C007C9ADE /* ViewUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20232B71D85525C007C9ADE /* ViewUtils.swift */; }; - C21178021F17E78E00AC706D /* TimableProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C21178011F17E78E00AC706D /* TimableProgressView.swift */; }; - C21656C61EE0A7050041A6BA /* SegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C21656C51EE0A7050041A6BA /* SegmentedControl.swift */; }; - C2167E5D1DC2534300F98E03 /* SelectingControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2167E5C1DC2534300F98E03 /* SelectingControl.swift */; }; - C2167E611DC356FD00F98E03 /* LinearProgressControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2167E601DC356FD00F98E03 /* LinearProgressControl.swift */; }; - C219E1D51D87F4A00042F0C8 /* SwiftSignalKitMac.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C219E1D41D87F4A00042F0C8 /* SwiftSignalKitMac.framework */; }; - C219E1DD1D8978960042F0C8 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C219E1DC1D8978960042F0C8 /* QuartzCore.framework */; }; - C219E1E01D8A71820042F0C8 /* SImageLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C219E1DF1D8A71820042F0C8 /* SImageLayer.swift */; }; - C219E1E21D8A93050042F0C8 /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C219E1E11D8A93050042F0C8 /* TextView.swift */; }; - C219E1EB1D8AD1470042F0C8 /* NavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C219E1EA1D8AD1470042F0C8 /* NavigationViewController.swift */; }; - C219E1ED1D8ADEE00042F0C8 /* NavigationBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C219E1EC1D8ADEE00042F0C8 /* NavigationBarView.swift */; }; - C219E1EF1D8AE3310042F0C8 /* AnimationStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C219E1EE1D8AE3310042F0C8 /* AnimationStyle.swift */; }; - C219E1F11D8AFD140042F0C8 /* AnimationBlockDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C219E1F01D8AFD140042F0C8 /* AnimationBlockDelegate.swift */; }; - C219E1F31D8B02FA0042F0C8 /* CAAnimationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C219E1F21D8B02FA0042F0C8 /* CAAnimationUtils.swift */; }; - C219E1F81D8C14930042F0C8 /* BarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C219E1F71D8C14930042F0C8 /* BarView.swift */; }; - C219E1FA1D8C316C0042F0C8 /* TitledBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C219E1F91D8C316C0042F0C8 /* TitledBarView.swift */; }; - C21AAE381DB233F7007638C5 /* UIUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C21AAE371DB233F7007638C5 /* UIUtils.swift */; }; - C21AAE3A1DB2398B007638C5 /* DisplayLinkDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C21AAE391DB2398B007638C5 /* DisplayLinkDispatcher.swift */; }; - C21AAE3C1DB239ED007638C5 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C21AAE3B1DB239ED007638C5 /* Foundation.framework */; }; - C224A72B1EB7581500F43F3F /* MajorNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C224A72A1EB7581500F43F3F /* MajorNavigationController.swift */; }; - C226741B1DBCD6E8000BA9ED /* GridNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C226741A1DBCD6E8000BA9ED /* GridNode.swift */; }; - C226741D1DBCDBA8000BA9ED /* ContainableController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C226741C1DBCDBA8000BA9ED /* ContainableController.swift */; }; - C2271DA61DAD16B4001792B6 /* BadgeNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271DA51DAD16B4001792B6 /* BadgeNode.swift */; }; - C2271DC81DAEAAF9001792B6 /* SwitchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271DC71DAEAAF9001792B6 /* SwitchView.swift */; }; - C2271DD41DAF766A001792B6 /* WeakReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271DD31DAF766A001792B6 /* WeakReference.swift */; }; - C2271DED1DAFE4D0001792B6 /* TransformImageArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271DEC1DAFE4D0001792B6 /* TransformImageArguments.swift */; }; - C2271F331DB4CEB30045E719 /* HorizontalTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271F321DB4CEB30045E719 /* HorizontalTableView.swift */; }; - C2271F361DB4CF270045E719 /* HorizontalRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271F351DB4CF270045E719 /* HorizontalRowView.swift */; }; - C2271F431DB4EE2F0045E719 /* TableStickItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271F421DB4EE2F0045E719 /* TableStickItem.swift */; }; - C2271F451DB4EE3C0045E719 /* TableStickView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271F441DB4EE3C0045E719 /* TableStickView.swift */; }; - C22E062E1D7F3CC000A11C88 /* TGColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C22E062D1D7F3CC000A11C88 /* TGColor.swift */; }; - C22E06351D804C8200A11C88 /* TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C22E06341D804C8200A11C88 /* TableView.swift */; }; - C22E06371D804D3B00A11C88 /* TableRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C22E06361D804D3B00A11C88 /* TableRowItem.swift */; }; - C22E06391D804DCD00A11C88 /* ScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C22E06381D804DCD00A11C88 /* ScrollView.swift */; }; - C22E063B1D8067A000A11C88 /* TableRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C22E063A1D8067A000A11C88 /* TableRowView.swift */; }; - C22E06451D80967E00A11C88 /* CoreText.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C22E06441D80967E00A11C88 /* CoreText.framework */; }; - C2303E6F1D9950E000098E12 /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2303E6E1D9950E000098E12 /* Button.swift */; }; - C2303E711D9956C400098E12 /* Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2303E701D9956C400098E12 /* Style.swift */; }; - C2303E761D996E6300098E12 /* ImageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2303E751D996E6300098E12 /* ImageButton.swift */; }; - C2303E7A1D9987FE00098E12 /* Popover.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2303E791D9987FE00098E12 /* Popover.swift */; }; - C2303E7C1D99880B00098E12 /* Modal.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2303E7B1D99880B00098E12 /* Modal.swift */; }; - C2303E7F1D99BEAE00098E12 /* OverlayControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2303E7E1D99BEAE00098E12 /* OverlayControl.swift */; }; - C2303E821D9A692B00098E12 /* TabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2303E811D9A692B00098E12 /* TabBarController.swift */; }; - C2303E841D9A69BD00098E12 /* TabBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2303E831D9A69BD00098E12 /* TabBarView.swift */; }; - C2303E861D9A6C2A00098E12 /* TabItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2303E851D9A6C2A00098E12 /* TabItem.swift */; }; - C2303E8C1D9A8B3000098E12 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2303E8B1D9A8B3000098E12 /* SearchView.swift */; }; - C230B9181DD3A6350057F596 /* ProgressModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = C230B9171DD3A6350057F596 /* ProgressModal.swift */; }; - C232EA151E165AEF00C4D38C /* ImageBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C232EA141E165AEF00C4D38C /* ImageBarView.swift */; }; - C234CA8E1D97DF26003023F7 /* DrawingContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C234CA8B1D97DF26003023F7 /* DrawingContext.swift */; }; - C234CA8F1D97DF26003023F7 /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = C234CA8C1D97DF26003023F7 /* Image.swift */; }; - C234CA971D981F00003023F7 /* Control.swift in Sources */ = {isa = PBXBuildFile; fileRef = C234CA961D981F00003023F7 /* Control.swift */; }; - C2449A151F0E490C00DF5650 /* ProgressIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2449A141F0E490C00DF5650 /* ProgressIndicator.swift */; }; - C24A37161F324A36004ADE5C /* ShadowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C24A37151F324A36004ADE5C /* ShadowView.swift */; }; - C24A371A1F338406004ADE5C /* SectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C24A37191F338406004ADE5C /* SectionViewController.swift */; }; - C253A9521D9147A400CDC850 /* Layer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C253A9511D9147A400CDC850 /* Layer.swift */; }; - C253A9651D91B24100CDC850 /* TextViewLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C253A9641D91B24100CDC850 /* TextViewLabel.swift */; }; - C253A9761D9430A900CDC850 /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C253A9751D9430A900CDC850 /* ImageView.swift */; }; - C25FC7F71D86DC370041E303 /* TGClipView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C25FC7F61D86DC370041E303 /* TGClipView.swift */; }; - C25FC7F91D86E53A0041E303 /* CoreVideo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C25FC7F81D86E53A0041E303 /* CoreVideo.framework */; }; - C26505901E02EBEE001954DC /* MagnifyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C265058F1E02EBEE001954DC /* MagnifyView.swift */; }; - C28BAB0D1DF8550E0027CE3A /* WindowSaver.swift in Sources */ = {isa = PBXBuildFile; fileRef = C28BAB0C1DF8550E0027CE3A /* WindowSaver.swift */; }; - C28CFC7D1F387A7F00C55596 /* TokenizedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C28CFC7C1F387A7F00C55596 /* TokenizedView.swift */; }; - C291ED991DB35BC5008C6B2A /* Window.swift in Sources */ = {isa = PBXBuildFile; fileRef = C291ED981DB35BC5008C6B2A /* Window.swift */; }; - C291ED9B1DB361ED008C6B2A /* KeyboardUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C291ED9A1DB361ED008C6B2A /* KeyboardUtils.swift */; }; - C296AF841D8D9D7D001DBB59 /* RadialProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C296AF831D8D9D7D001DBB59 /* RadialProgressView.swift */; }; - C29B5F3C1DC66DAC00D13E65 /* DraggingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C29B5F3B1DC66DAC00D13E65 /* DraggingView.swift */; }; - C29B5F471DC8CA2B00D13E65 /* NavigationModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C29B5F461DC8CA2B00D13E65 /* NavigationModalView.swift */; }; - C29B5F491DC8CA3C00D13E65 /* NavigationModalAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = C29B5F481DC8CA3C00D13E65 /* NavigationModalAction.swift */; }; - C2A71CEB1DDB382F00C69F73 /* TableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A71CEA1DDB382F00C69F73 /* TableViewController.swift */; }; - C2B1A1181DA1673300ACB1DD /* TableAnimationInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2B1A1171DA1673200ACB1DD /* TableAnimationInterface.swift */; }; - C2B1A11E1DA26BA400ACB1DD /* TransactionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2B1A11D1DA26BA400ACB1DD /* TransactionHandler.swift */; }; - C2B1A1251DA2F3DC00ACB1DD /* ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2B1A1241DA2F3DC00ACB1DD /* ContextMenu.swift */; }; - C2B1A1291DA3DDED00ACB1DD /* Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2B1A1281DA3DDED00ACB1DD /* Node.swift */; }; - C2B1A12C1DA50CC600ACB1DD /* TitleButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2B1A12B1DA50CC600ACB1DD /* TitleButton.swift */; }; - C2B1A12E1DA5148B00ACB1DD /* TextButtonBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2B1A12D1DA5148B00ACB1DD /* TextButtonBarView.swift */; }; - C2B1A1301DA53E7D00ACB1DD /* BackNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2B1A12F1DA53E7D00ACB1DD /* BackNavigationBar.swift */; }; - C2B1A1321DA57E8300ACB1DD /* EditableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2B1A1311DA57E8300ACB1DD /* EditableViewController.swift */; }; - C2B9BE861EFC373D00D6B96F /* PresentationTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2B9BE841EFC373D00D6B96F /* PresentationTheme.swift */; }; - C2B9BE871EFC373D00D6B96F /* PresentationResourceCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2B9BE851EFC373D00D6B96F /* PresentationResourceCache.swift */; }; - C2CBCABB1D80AD1F00142EC0 /* TGFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2CBCABA1D80AD1F00142EC0 /* TGFont.swift */; }; - C2CBCABD1D814D4B00142EC0 /* System.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2CBCABC1D814D4B00142EC0 /* System.swift */; }; - C2CBCAC31D81595900142EC0 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2CBCAC21D81595900142EC0 /* Extensions.swift */; }; - C2E0DEAC1D7EF51C00EF1C8D /* TGUIKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C2E0DEAA1D7EF51C00EF1C8D /* TGUIKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; - C2E0DEB71D7EF58200EF1C8D /* TGSplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E0DEB61D7EF58200EF1C8D /* TGSplitView.swift */; }; - C2E0DEB91D7EF7F300EF1C8D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E0DEB81D7EF7F300EF1C8D /* ViewController.swift */; }; - C2E0DEBB1D7F05AB00EF1C8D /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E0DEBA1D7F05AB00EF1C8D /* View.swift */; }; - C2E8694F1F44481400BDD0A2 /* GridItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E8694E1F44481400BDD0A2 /* GridItem.swift */; }; - C2E869511F4449A700BDD0A2 /* GridItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E869501F4449A700BDD0A2 /* GridItemNode.swift */; }; - C2FD33E71E6952A8008D13D4 /* RadialProgressContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2FD33E61E6952A8008D13D4 /* RadialProgressContainerView.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXFileReference section */ - C20232B51D845B0B007C9ADE /* TextNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextNode.swift; sourceTree = ""; }; - C20232B71D85525C007C9ADE /* ViewUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewUtils.swift; sourceTree = ""; }; - C21178011F17E78E00AC706D /* TimableProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimableProgressView.swift; sourceTree = ""; }; - C21656C51EE0A7050041A6BA /* SegmentedControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SegmentedControl.swift; sourceTree = ""; }; - C2167E5C1DC2534300F98E03 /* SelectingControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectingControl.swift; sourceTree = ""; }; - C2167E601DC356FD00F98E03 /* LinearProgressControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinearProgressControl.swift; sourceTree = ""; }; - C219E1D41D87F4A00042F0C8 /* SwiftSignalKitMac.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftSignalKitMac.framework; path = "../../../Library/Developer/Xcode/DerivedData/Telegram-Mac-hikohhgxyaqnbcboyjbphnuqswbk/Build/Products/Debug/SwiftSignalKitMac.framework"; sourceTree = ""; }; - C219E1DC1D8978960042F0C8 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; - C219E1DF1D8A71820042F0C8 /* SImageLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SImageLayer.swift; sourceTree = ""; }; - C219E1E11D8A93050042F0C8 /* TextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextView.swift; sourceTree = ""; }; - C219E1EA1D8AD1470042F0C8 /* NavigationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationViewController.swift; sourceTree = ""; }; - C219E1EC1D8ADEE00042F0C8 /* NavigationBarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationBarView.swift; sourceTree = ""; }; - C219E1EE1D8AE3310042F0C8 /* AnimationStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimationStyle.swift; sourceTree = ""; }; - C219E1F01D8AFD140042F0C8 /* AnimationBlockDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimationBlockDelegate.swift; sourceTree = ""; }; - C219E1F21D8B02FA0042F0C8 /* CAAnimationUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CAAnimationUtils.swift; sourceTree = ""; }; - C219E1F71D8C14930042F0C8 /* BarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BarView.swift; sourceTree = ""; }; - C219E1F91D8C316C0042F0C8 /* TitledBarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TitledBarView.swift; sourceTree = ""; }; - C21AAE371DB233F7007638C5 /* UIUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIUtils.swift; sourceTree = ""; }; - C21AAE391DB2398B007638C5 /* DisplayLinkDispatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayLinkDispatcher.swift; sourceTree = ""; }; - C21AAE3B1DB239ED007638C5 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; - C224A72A1EB7581500F43F3F /* MajorNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MajorNavigationController.swift; sourceTree = ""; }; - C226741A1DBCD6E8000BA9ED /* GridNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridNode.swift; sourceTree = ""; }; - C226741C1DBCDBA8000BA9ED /* ContainableController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContainableController.swift; sourceTree = ""; }; - C2271DA51DAD16B4001792B6 /* BadgeNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BadgeNode.swift; sourceTree = ""; }; - C2271DC71DAEAAF9001792B6 /* SwitchView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchView.swift; sourceTree = ""; }; - C2271DD31DAF766A001792B6 /* WeakReference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WeakReference.swift; sourceTree = ""; }; - C2271DEC1DAFE4D0001792B6 /* TransformImageArguments.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransformImageArguments.swift; sourceTree = ""; }; - C2271F321DB4CEB30045E719 /* HorizontalTableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalTableView.swift; sourceTree = ""; }; - C2271F351DB4CF270045E719 /* HorizontalRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalRowView.swift; sourceTree = ""; }; - C2271F421DB4EE2F0045E719 /* TableStickItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableStickItem.swift; sourceTree = ""; }; - C2271F441DB4EE3C0045E719 /* TableStickView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableStickView.swift; sourceTree = ""; }; - C22E062D1D7F3CC000A11C88 /* TGColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TGColor.swift; sourceTree = ""; }; - C22E06341D804C8200A11C88 /* TableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableView.swift; sourceTree = ""; }; - C22E06361D804D3B00A11C88 /* TableRowItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableRowItem.swift; sourceTree = ""; }; - C22E06381D804DCD00A11C88 /* ScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollView.swift; sourceTree = ""; }; - C22E063A1D8067A000A11C88 /* TableRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableRowView.swift; sourceTree = ""; }; - C22E06441D80967E00A11C88 /* CoreText.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreText.framework; path = System/Library/Frameworks/CoreText.framework; sourceTree = SDKROOT; }; - C2303E6E1D9950E000098E12 /* Button.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = ""; }; - C2303E701D9956C400098E12 /* Style.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Style.swift; sourceTree = ""; }; - C2303E751D996E6300098E12 /* ImageButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageButton.swift; sourceTree = ""; }; - C2303E791D9987FE00098E12 /* Popover.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Popover.swift; sourceTree = ""; }; - C2303E7B1D99880B00098E12 /* Modal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Modal.swift; sourceTree = ""; }; - C2303E7E1D99BEAE00098E12 /* OverlayControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverlayControl.swift; sourceTree = ""; }; - C2303E811D9A692B00098E12 /* TabBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabBarController.swift; sourceTree = ""; }; - C2303E831D9A69BD00098E12 /* TabBarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabBarView.swift; sourceTree = ""; }; - C2303E851D9A6C2A00098E12 /* TabItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabItem.swift; sourceTree = ""; }; - C2303E8B1D9A8B3000098E12 /* SearchView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; - C230B9171DD3A6350057F596 /* ProgressModal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressModal.swift; sourceTree = ""; }; - C232EA141E165AEF00C4D38C /* ImageBarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageBarView.swift; sourceTree = ""; }; - C234CA881D97DEB6003023F7 /* TGLibrary.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TGLibrary.framework; path = ../TGLibrary/build/Debug/TGLibrary.framework; sourceTree = ""; }; - C234CA8B1D97DF26003023F7 /* DrawingContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DrawingContext.swift; sourceTree = ""; }; - C234CA8C1D97DF26003023F7 /* Image.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = ""; }; - C234CA921D97E151003023F7 /* PostboxMac.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PostboxMac.framework; path = "../../../Library/Developer/Xcode/DerivedData/Telegram-Mac-hikohhgxyaqnbcboyjbphnuqswbk/Build/Products/Debug/PostboxMac.framework"; sourceTree = ""; }; - C234CA941D97E163003023F7 /* TelegramCoreMac.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TelegramCoreMac.framework; path = "../../../Library/Developer/Xcode/DerivedData/Telegram-Mac-hikohhgxyaqnbcboyjbphnuqswbk/Build/Products/Debug/TelegramCoreMac.framework"; sourceTree = ""; }; - C234CA961D981F00003023F7 /* Control.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Control.swift; sourceTree = ""; }; - C2449A141F0E490C00DF5650 /* ProgressIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressIndicator.swift; sourceTree = ""; }; - C24A37151F324A36004ADE5C /* ShadowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowView.swift; sourceTree = ""; }; - C24A37191F338406004ADE5C /* SectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SectionViewController.swift; sourceTree = ""; }; - C253A9511D9147A400CDC850 /* Layer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Layer.swift; sourceTree = ""; }; - C253A9641D91B24100CDC850 /* TextViewLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextViewLabel.swift; sourceTree = ""; }; - C253A9751D9430A900CDC850 /* ImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = ""; }; - C25FC7F61D86DC370041E303 /* TGClipView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TGClipView.swift; sourceTree = ""; }; - C25FC7F81D86E53A0041E303 /* CoreVideo.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreVideo.framework; path = System/Library/Frameworks/CoreVideo.framework; sourceTree = SDKROOT; }; - C265058F1E02EBEE001954DC /* MagnifyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MagnifyView.swift; sourceTree = ""; }; - C27624881D95AD7300FE5B2B /* ObjcExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ObjcExtension.framework; path = ../ObjcExtension/build/Debug/ObjcExtension.framework; sourceTree = ""; }; - C28BAB0C1DF8550E0027CE3A /* WindowSaver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowSaver.swift; sourceTree = ""; }; - C28CFC7C1F387A7F00C55596 /* TokenizedView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenizedView.swift; sourceTree = ""; }; - C291ED981DB35BC5008C6B2A /* Window.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Window.swift; sourceTree = ""; }; - C291ED9A1DB361ED008C6B2A /* KeyboardUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardUtils.swift; sourceTree = ""; }; - C296AF831D8D9D7D001DBB59 /* RadialProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadialProgressView.swift; sourceTree = ""; }; - C29B5F3B1DC66DAC00D13E65 /* DraggingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DraggingView.swift; sourceTree = ""; }; - C29B5F461DC8CA2B00D13E65 /* NavigationModalView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationModalView.swift; sourceTree = ""; }; - C29B5F481DC8CA3C00D13E65 /* NavigationModalAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationModalAction.swift; sourceTree = ""; }; - C2A71CEA1DDB382F00C69F73 /* TableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableViewController.swift; sourceTree = ""; }; - C2B1A1171DA1673200ACB1DD /* TableAnimationInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TableAnimationInterface.swift; sourceTree = ""; }; - C2B1A11D1DA26BA400ACB1DD /* TransactionHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionHandler.swift; sourceTree = ""; }; - C2B1A1241DA2F3DC00ACB1DD /* ContextMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextMenu.swift; sourceTree = ""; }; - C2B1A1281DA3DDED00ACB1DD /* Node.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Node.swift; sourceTree = ""; }; - C2B1A12B1DA50CC600ACB1DD /* TitleButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TitleButton.swift; sourceTree = ""; }; - C2B1A12D1DA5148B00ACB1DD /* TextButtonBarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextButtonBarView.swift; sourceTree = ""; }; - C2B1A12F1DA53E7D00ACB1DD /* BackNavigationBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackNavigationBar.swift; sourceTree = ""; }; - C2B1A1311DA57E8300ACB1DD /* EditableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditableViewController.swift; sourceTree = ""; }; - C2B9BE841EFC373D00D6B96F /* PresentationTheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationTheme.swift; sourceTree = ""; }; - C2B9BE851EFC373D00D6B96F /* PresentationResourceCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationResourceCache.swift; sourceTree = ""; }; - C2CBCABA1D80AD1F00142EC0 /* TGFont.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TGFont.swift; sourceTree = ""; }; - C2CBCABC1D814D4B00142EC0 /* System.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = System.swift; sourceTree = ""; }; - C2CBCAC21D81595900142EC0 /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; - C2E0DEA71D7EF51C00EF1C8D /* TGUIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TGUIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - C2E0DEAA1D7EF51C00EF1C8D /* TGUIKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TGUIKit.h; sourceTree = ""; }; - C2E0DEAB1D7EF51C00EF1C8D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - C2E0DEB61D7EF58200EF1C8D /* TGSplitView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TGSplitView.swift; sourceTree = ""; }; - C2E0DEB81D7EF7F300EF1C8D /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - C2E0DEBA1D7F05AB00EF1C8D /* View.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; - C2E8694E1F44481400BDD0A2 /* GridItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridItem.swift; sourceTree = ""; }; - C2E869501F4449A700BDD0A2 /* GridItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridItemNode.swift; sourceTree = ""; }; - C2FD33E61E6952A8008D13D4 /* RadialProgressContainerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadialProgressContainerView.swift; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - C2E0DEA31D7EF51C00EF1C8D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - C21AAE3C1DB239ED007638C5 /* Foundation.framework in Frameworks */, - C219E1DD1D8978960042F0C8 /* QuartzCore.framework in Frameworks */, - C219E1D51D87F4A00042F0C8 /* SwiftSignalKitMac.framework in Frameworks */, - C25FC7F91D86E53A0041E303 /* CoreVideo.framework in Frameworks */, - C22E06451D80967E00A11C88 /* CoreText.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - C219E1DE1D8A71680042F0C8 /* layers */ = { - isa = PBXGroup; - children = ( - C219E1DF1D8A71820042F0C8 /* SImageLayer.swift */, - C253A9511D9147A400CDC850 /* Layer.swift */, - ); - name = layers; - sourceTree = ""; - }; - C219E1E91D8AD1330042F0C8 /* navigation */ = { - isa = PBXGroup; - children = ( - C24A37181F3383DF004ADE5C /* SectionController */, - C219E1F41D8BEA1F0042F0C8 /* bar */, - C219E1EA1D8AD1470042F0C8 /* NavigationViewController.swift */, - C224A72A1EB7581500F43F3F /* MajorNavigationController.swift */, - C2E0DEB81D7EF7F300EF1C8D /* ViewController.swift */, - C2B1A1311DA57E8300ACB1DD /* EditableViewController.swift */, - C2A71CEA1DDB382F00C69F73 /* TableViewController.swift */, - ); - name = navigation; - sourceTree = ""; - }; - C219E1F41D8BEA1F0042F0C8 /* bar */ = { - isa = PBXGroup; - children = ( - C219E1EC1D8ADEE00042F0C8 /* NavigationBarView.swift */, - C219E1F71D8C14930042F0C8 /* BarView.swift */, - C219E1F91D8C316C0042F0C8 /* TitledBarView.swift */, - C2B1A12D1DA5148B00ACB1DD /* TextButtonBarView.swift */, - C2B1A12F1DA53E7D00ACB1DD /* BackNavigationBar.swift */, - C232EA141E165AEF00C4D38C /* ImageBarView.swift */, - ); - name = bar; - sourceTree = ""; - }; - C22674191DBCD6CA000BA9ED /* gridnode */ = { - isa = PBXGroup; - children = ( - C226741A1DBCD6E8000BA9ED /* GridNode.swift */, - C2E8694E1F44481400BDD0A2 /* GridItem.swift */, - C2E869501F4449A700BDD0A2 /* GridItemNode.swift */, - ); - name = gridnode; - sourceTree = ""; - }; - C2271DA71DAD1728001792B6 /* nodes */ = { - isa = PBXGroup; - children = ( - C2271DA51DAD16B4001792B6 /* BadgeNode.swift */, - ); - name = nodes; - sourceTree = ""; - }; - C2271F341DB4CF040045E719 /* horizontal */ = { - isa = PBXGroup; - children = ( - C2271F321DB4CEB30045E719 /* HorizontalTableView.swift */, - C2271F351DB4CF270045E719 /* HorizontalRowView.swift */, - ); - name = horizontal; - sourceTree = ""; - }; - C2271F411DB4EE170045E719 /* stick */ = { - isa = PBXGroup; - children = ( - C2271F421DB4EE2F0045E719 /* TableStickItem.swift */, - C2271F441DB4EE3C0045E719 /* TableStickView.swift */, - ); - name = stick; - sourceTree = ""; - }; - C22E062A1D7F3C2700A11C88 /* utils */ = { - isa = PBXGroup; - children = ( - C2271DEC1DAFE4D0001792B6 /* TransformImageArguments.swift */, - C2CBCAC11D81593B00142EC0 /* extensions */, - C22E062D1D7F3CC000A11C88 /* TGColor.swift */, - C2CBCABA1D80AD1F00142EC0 /* TGFont.swift */, - C2CBCABC1D814D4B00142EC0 /* System.swift */, - C219E1EE1D8AE3310042F0C8 /* AnimationStyle.swift */, - C219E1F01D8AFD140042F0C8 /* AnimationBlockDelegate.swift */, - C219E1F21D8B02FA0042F0C8 /* CAAnimationUtils.swift */, - C2303E701D9956C400098E12 /* Style.swift */, - C2B1A11D1DA26BA400ACB1DD /* TransactionHandler.swift */, - C2271DD31DAF766A001792B6 /* WeakReference.swift */, - C21AAE371DB233F7007638C5 /* UIUtils.swift */, - C21AAE391DB2398B007638C5 /* DisplayLinkDispatcher.swift */, - C291ED9A1DB361ED008C6B2A /* KeyboardUtils.swift */, - C226741C1DBCDBA8000BA9ED /* ContainableController.swift */, - C28BAB0C1DF8550E0027CE3A /* WindowSaver.swift */, - ); - name = utils; - sourceTree = ""; - }; - C22E06331D804C5800A11C88 /* table */ = { - isa = PBXGroup; - children = ( - C22674191DBCD6CA000BA9ED /* gridnode */, - C2271F411DB4EE170045E719 /* stick */, - C2271F341DB4CF040045E719 /* horizontal */, - C22E06341D804C8200A11C88 /* TableView.swift */, - C22E06361D804D3B00A11C88 /* TableRowItem.swift */, - C22E063A1D8067A000A11C88 /* TableRowView.swift */, - C2B1A1171DA1673200ACB1DD /* TableAnimationInterface.swift */, - ); - name = table; - sourceTree = ""; - }; - C22E06431D80967E00A11C88 /* Frameworks */ = { - isa = PBXGroup; - children = ( - C21AAE3B1DB239ED007638C5 /* Foundation.framework */, - C234CA941D97E163003023F7 /* TelegramCoreMac.framework */, - C234CA921D97E151003023F7 /* PostboxMac.framework */, - C234CA881D97DEB6003023F7 /* TGLibrary.framework */, - C27624881D95AD7300FE5B2B /* ObjcExtension.framework */, - C219E1DC1D8978960042F0C8 /* QuartzCore.framework */, - C219E1D41D87F4A00042F0C8 /* SwiftSignalKitMac.framework */, - C25FC7F81D86E53A0041E303 /* CoreVideo.framework */, - C22E06441D80967E00A11C88 /* CoreText.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - C2303E7D1D99881500098E12 /* overlay */ = { - isa = PBXGroup; - children = ( - C2B1A1241DA2F3DC00ACB1DD /* ContextMenu.swift */, - C2303E791D9987FE00098E12 /* Popover.swift */, - C2303E7B1D99880B00098E12 /* Modal.swift */, - C29B5F461DC8CA2B00D13E65 /* NavigationModalView.swift */, - C29B5F481DC8CA3C00D13E65 /* NavigationModalAction.swift */, - C230B9171DD3A6350057F596 /* ProgressModal.swift */, - ); - name = overlay; - sourceTree = ""; - }; - C2303E801D9A691300098E12 /* tabbar */ = { - isa = PBXGroup; - children = ( - C2303E811D9A692B00098E12 /* TabBarController.swift */, - C2303E831D9A69BD00098E12 /* TabBarView.swift */, - C2303E851D9A6C2A00098E12 /* TabItem.swift */, - ); - name = tabbar; - sourceTree = ""; - }; - C2449A0F1F0E475200DF5650 /* thrid-party */ = { - isa = PBXGroup; - children = ( - ); - name = "thrid-party"; - sourceTree = ""; - }; - C24A37181F3383DF004ADE5C /* SectionController */ = { - isa = PBXGroup; - children = ( - C24A37191F338406004ADE5C /* SectionViewController.swift */, - ); - name = SectionController; - sourceTree = ""; - }; - C2B9BE831EFC373200D6B96F /* presentation */ = { - isa = PBXGroup; - children = ( - C2B9BE841EFC373D00D6B96F /* PresentationTheme.swift */, - C2B9BE851EFC373D00D6B96F /* PresentationResourceCache.swift */, - ); - name = presentation; - sourceTree = ""; - }; - C2CBCAC11D81593B00142EC0 /* extensions */ = { - isa = PBXGroup; - children = ( - C234CA8B1D97DF26003023F7 /* DrawingContext.swift */, - C234CA8C1D97DF26003023F7 /* Image.swift */, - C2CBCAC21D81595900142EC0 /* Extensions.swift */, - ); - name = extensions; - sourceTree = ""; - }; - C2E0DE9D1D7EF51C00EF1C8D = { - isa = PBXGroup; - children = ( - C2E0DEA91D7EF51C00EF1C8D /* TGUIKit */, - C2E0DEA81D7EF51C00EF1C8D /* Products */, - C22E06431D80967E00A11C88 /* Frameworks */, - ); - sourceTree = ""; - }; - C2E0DEA81D7EF51C00EF1C8D /* Products */ = { - isa = PBXGroup; - children = ( - C2E0DEA71D7EF51C00EF1C8D /* TGUIKit.framework */, - ); - name = Products; - sourceTree = ""; - }; - C2E0DEA91D7EF51C00EF1C8D /* TGUIKit */ = { - isa = PBXGroup; - children = ( - C2449A0F1F0E475200DF5650 /* thrid-party */, - C2B9BE831EFC373200D6B96F /* presentation */, - C2271DA71DAD1728001792B6 /* nodes */, - C2303E801D9A691300098E12 /* tabbar */, - C2303E7D1D99881500098E12 /* overlay */, - C219E1E91D8AD1330042F0C8 /* navigation */, - C22E06331D804C5800A11C88 /* table */, - C22E062A1D7F3C2700A11C88 /* utils */, - C2E0DEBC1D7F071300EF1C8D /* view */, - C2E0DEB41D7EF53900EF1C8D /* split-view */, - C2E0DEAA1D7EF51C00EF1C8D /* TGUIKit.h */, - C2E0DEAB1D7EF51C00EF1C8D /* Info.plist */, - ); - path = TGUIKit; - sourceTree = ""; - }; - C2E0DEB41D7EF53900EF1C8D /* split-view */ = { - isa = PBXGroup; - children = ( - C2E0DEB61D7EF58200EF1C8D /* TGSplitView.swift */, - ); - name = "split-view"; - sourceTree = ""; - }; - C2E0DEBC1D7F071300EF1C8D /* view */ = { - isa = PBXGroup; - children = ( - C219E1DE1D8A71680042F0C8 /* layers */, - C296AF831D8D9D7D001DBB59 /* RadialProgressView.swift */, - C219E1E11D8A93050042F0C8 /* TextView.swift */, - C2E0DEBA1D7F05AB00EF1C8D /* View.swift */, - C22E06381D804DCD00A11C88 /* ScrollView.swift */, - C20232B51D845B0B007C9ADE /* TextNode.swift */, - C20232B71D85525C007C9ADE /* ViewUtils.swift */, - C25FC7F61D86DC370041E303 /* TGClipView.swift */, - C253A9641D91B24100CDC850 /* TextViewLabel.swift */, - C253A9751D9430A900CDC850 /* ImageView.swift */, - C234CA961D981F00003023F7 /* Control.swift */, - C2303E6E1D9950E000098E12 /* Button.swift */, - C2303E751D996E6300098E12 /* ImageButton.swift */, - C2303E7E1D99BEAE00098E12 /* OverlayControl.swift */, - C2303E8B1D9A8B3000098E12 /* SearchView.swift */, - C2B1A1281DA3DDED00ACB1DD /* Node.swift */, - C2B1A12B1DA50CC600ACB1DD /* TitleButton.swift */, - C2271DC71DAEAAF9001792B6 /* SwitchView.swift */, - C291ED981DB35BC5008C6B2A /* Window.swift */, - C2167E601DC356FD00F98E03 /* LinearProgressControl.swift */, - C2167E5C1DC2534300F98E03 /* SelectingControl.swift */, - C29B5F3B1DC66DAC00D13E65 /* DraggingView.swift */, - C265058F1E02EBEE001954DC /* MagnifyView.swift */, - C2FD33E61E6952A8008D13D4 /* RadialProgressContainerView.swift */, - C21656C51EE0A7050041A6BA /* SegmentedControl.swift */, - C2449A141F0E490C00DF5650 /* ProgressIndicator.swift */, - C21178011F17E78E00AC706D /* TimableProgressView.swift */, - C24A37151F324A36004ADE5C /* ShadowView.swift */, - C28CFC7C1F387A7F00C55596 /* TokenizedView.swift */, - ); - name = view; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXHeadersBuildPhase section */ - C2E0DEA41D7EF51C00EF1C8D /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - C2E0DEAC1D7EF51C00EF1C8D /* TGUIKit.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXHeadersBuildPhase section */ - -/* Begin PBXNativeTarget section */ - C2E0DEA61D7EF51C00EF1C8D /* TGUIKit */ = { - isa = PBXNativeTarget; - buildConfigurationList = C2E0DEAF1D7EF51C00EF1C8D /* Build configuration list for PBXNativeTarget "TGUIKit" */; - buildPhases = ( - C2E0DEA21D7EF51C00EF1C8D /* Sources */, - C2E0DEA31D7EF51C00EF1C8D /* Frameworks */, - C2E0DEA41D7EF51C00EF1C8D /* Headers */, - C2E0DEA51D7EF51C00EF1C8D /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = TGUIKit; - productName = TGUIKit; - productReference = C2E0DEA71D7EF51C00EF1C8D /* TGUIKit.framework */; - productType = "com.apple.product-type.framework"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - C2E0DE9E1D7EF51C00EF1C8D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 0900; - ORGANIZATIONNAME = Telegram; - TargetAttributes = { - C2E0DEA61D7EF51C00EF1C8D = { - CreatedOnToolsVersion = 8.0; - DevelopmentTeam = 6N38VWS5BX; - LastSwiftMigration = 0830; - ProvisioningStyle = Automatic; - }; - }; - }; - buildConfigurationList = C2E0DEA11D7EF51C00EF1C8D /* Build configuration list for PBXProject "TGUIKit" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; - hasScannedForEncodings = 0; - knownRegions = ( - en, - ); - mainGroup = C2E0DE9D1D7EF51C00EF1C8D; - productRefGroup = C2E0DEA81D7EF51C00EF1C8D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - C2E0DEA61D7EF51C00EF1C8D /* TGUIKit */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - C2E0DEA51D7EF51C00EF1C8D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - C2E0DEA21D7EF51C00EF1C8D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - C219E1F81D8C14930042F0C8 /* BarView.swift in Sources */, - C2303E861D9A6C2A00098E12 /* TabItem.swift in Sources */, - C219E1EF1D8AE3310042F0C8 /* AnimationStyle.swift in Sources */, - C2271DA61DAD16B4001792B6 /* BadgeNode.swift in Sources */, - C234CA8F1D97DF26003023F7 /* Image.swift in Sources */, - C2B1A1181DA1673300ACB1DD /* TableAnimationInterface.swift in Sources */, - C2271F331DB4CEB30045E719 /* HorizontalTableView.swift in Sources */, - C2E0DEBB1D7F05AB00EF1C8D /* View.swift in Sources */, - C2271DD41DAF766A001792B6 /* WeakReference.swift in Sources */, - C22E06351D804C8200A11C88 /* TableView.swift in Sources */, - C232EA151E165AEF00C4D38C /* ImageBarView.swift in Sources */, - C2167E611DC356FD00F98E03 /* LinearProgressControl.swift in Sources */, - C22E062E1D7F3CC000A11C88 /* TGColor.swift in Sources */, - C2271DED1DAFE4D0001792B6 /* TransformImageArguments.swift in Sources */, - C2E0DEB71D7EF58200EF1C8D /* TGSplitView.swift in Sources */, - C2B1A1291DA3DDED00ACB1DD /* Node.swift in Sources */, - C2B9BE861EFC373D00D6B96F /* PresentationTheme.swift in Sources */, - C234CA8E1D97DF26003023F7 /* DrawingContext.swift in Sources */, - C219E1E01D8A71820042F0C8 /* SImageLayer.swift in Sources */, - C28CFC7D1F387A7F00C55596 /* TokenizedView.swift in Sources */, - C2B1A11E1DA26BA400ACB1DD /* TransactionHandler.swift in Sources */, - C2303E8C1D9A8B3000098E12 /* SearchView.swift in Sources */, - C234CA971D981F00003023F7 /* Control.swift in Sources */, - C2303E6F1D9950E000098E12 /* Button.swift in Sources */, - C21656C61EE0A7050041A6BA /* SegmentedControl.swift in Sources */, - C21AAE381DB233F7007638C5 /* UIUtils.swift in Sources */, - C20232B61D845B0B007C9ADE /* TextNode.swift in Sources */, - C2FD33E71E6952A8008D13D4 /* RadialProgressContainerView.swift in Sources */, - C219E1F31D8B02FA0042F0C8 /* CAAnimationUtils.swift in Sources */, - C291ED991DB35BC5008C6B2A /* Window.swift in Sources */, - C296AF841D8D9D7D001DBB59 /* RadialProgressView.swift in Sources */, - C24A371A1F338406004ADE5C /* SectionViewController.swift in Sources */, - C219E1EB1D8AD1470042F0C8 /* NavigationViewController.swift in Sources */, - C253A9761D9430A900CDC850 /* ImageView.swift in Sources */, - C24A37161F324A36004ADE5C /* ShadowView.swift in Sources */, - C2B1A1321DA57E8300ACB1DD /* EditableViewController.swift in Sources */, - C2167E5D1DC2534300F98E03 /* SelectingControl.swift in Sources */, - C230B9181DD3A6350057F596 /* ProgressModal.swift in Sources */, - C21178021F17E78E00AC706D /* TimableProgressView.swift in Sources */, - C2E0DEB91D7EF7F300EF1C8D /* ViewController.swift in Sources */, - C2303E841D9A69BD00098E12 /* TabBarView.swift in Sources */, - C22E063B1D8067A000A11C88 /* TableRowView.swift in Sources */, - C2303E7A1D9987FE00098E12 /* Popover.swift in Sources */, - C29B5F3C1DC66DAC00D13E65 /* DraggingView.swift in Sources */, - C226741D1DBCDBA8000BA9ED /* ContainableController.swift in Sources */, - C253A9651D91B24100CDC850 /* TextViewLabel.swift in Sources */, - C219E1FA1D8C316C0042F0C8 /* TitledBarView.swift in Sources */, - C28BAB0D1DF8550E0027CE3A /* WindowSaver.swift in Sources */, - C2303E7C1D99880B00098E12 /* Modal.swift in Sources */, - C219E1E21D8A93050042F0C8 /* TextView.swift in Sources */, - C2449A151F0E490C00DF5650 /* ProgressIndicator.swift in Sources */, - C21AAE3A1DB2398B007638C5 /* DisplayLinkDispatcher.swift in Sources */, - C2271F431DB4EE2F0045E719 /* TableStickItem.swift in Sources */, - C2E8694F1F44481400BDD0A2 /* GridItem.swift in Sources */, - C2B9BE871EFC373D00D6B96F /* PresentationResourceCache.swift in Sources */, - C2271F451DB4EE3C0045E719 /* TableStickView.swift in Sources */, - C2E869511F4449A700BDD0A2 /* GridItemNode.swift in Sources */, - C25FC7F71D86DC370041E303 /* TGClipView.swift in Sources */, - C2CBCAC31D81595900142EC0 /* Extensions.swift in Sources */, - C219E1ED1D8ADEE00042F0C8 /* NavigationBarView.swift in Sources */, - C2303E821D9A692B00098E12 /* TabBarController.swift in Sources */, - C2CBCABB1D80AD1F00142EC0 /* TGFont.swift in Sources */, - C2B1A12E1DA5148B00ACB1DD /* TextButtonBarView.swift in Sources */, - C20232B81D85525C007C9ADE /* ViewUtils.swift in Sources */, - C2271F361DB4CF270045E719 /* HorizontalRowView.swift in Sources */, - C22E06391D804DCD00A11C88 /* ScrollView.swift in Sources */, - C219E1F11D8AFD140042F0C8 /* AnimationBlockDelegate.swift in Sources */, - C253A9521D9147A400CDC850 /* Layer.swift in Sources */, - C22E06371D804D3B00A11C88 /* TableRowItem.swift in Sources */, - C2303E711D9956C400098E12 /* Style.swift in Sources */, - C2271DC81DAEAAF9001792B6 /* SwitchView.swift in Sources */, - C2B1A1251DA2F3DC00ACB1DD /* ContextMenu.swift in Sources */, - C224A72B1EB7581500F43F3F /* MajorNavigationController.swift in Sources */, - C2B1A1301DA53E7D00ACB1DD /* BackNavigationBar.swift in Sources */, - C2303E761D996E6300098E12 /* ImageButton.swift in Sources */, - C226741B1DBCD6E8000BA9ED /* GridNode.swift in Sources */, - C2CBCABD1D814D4B00142EC0 /* System.swift in Sources */, - C2303E7F1D99BEAE00098E12 /* OverlayControl.swift in Sources */, - C29B5F471DC8CA2B00D13E65 /* NavigationModalView.swift in Sources */, - C26505901E02EBEE001954DC /* MagnifyView.swift in Sources */, - C2B1A12C1DA50CC600ACB1DD /* TitleButton.swift in Sources */, - C291ED9B1DB361ED008C6B2A /* KeyboardUtils.swift in Sources */, - C29B5F491DC8CA3C00D13E65 /* NavigationModalAction.swift in Sources */, - C2A71CEB1DDB382F00C69F73 /* TableViewController.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin XCBuildConfiguration section */ - C22069D21E8EB4D700E82730 /* Release Hockeyapp */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - 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_SUSPICIOUS_MOVES = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - 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; - MACOSX_DEPLOYMENT_TARGET = 10.10; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = "Release Hockeyapp"; - }; - C22069D31E8EB4D700E82730 /* Release Hockeyapp */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - COMBINE_HIDPI_IMAGES = YES; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 6N38VWS5BX; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - FRAMEWORK_VERSION = A; - INFOPLIST_FILE = TGUIKit/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; - MACH_O_TYPE = mh_dylib; - MACOSX_DEPLOYMENT_TARGET = 10.11; - PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TGUIKit; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_OBJC_BRIDGING_HEADER = ""; - SWIFT_VERSION = 4.0; - }; - name = "Release Hockeyapp"; - }; - C232EA391E1BCFDE00C4D38C /* Release AppStore */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - 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_SUSPICIOUS_MOVES = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - 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; - MACOSX_DEPLOYMENT_TARGET = 10.10; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = "Release AppStore"; - }; - C232EA3A1E1BCFDE00C4D38C /* Release AppStore */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - COMBINE_HIDPI_IMAGES = YES; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 6N38VWS5BX; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - FRAMEWORK_VERSION = A; - INFOPLIST_FILE = TGUIKit/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; - MACH_O_TYPE = mh_dylib; - MACOSX_DEPLOYMENT_TARGET = 10.11; - PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TGUIKit; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_OBJC_BRIDGING_HEADER = ""; - SWIFT_VERSION = 4.0; - }; - name = "Release AppStore"; - }; - C2E0DEAD1D7EF51C00EF1C8D /* Debug Hockeyapp */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - 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_SUSPICIOUS_MOVES = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = dwarf; - 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_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; - MACOSX_DEPLOYMENT_TARGET = 10.10; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = "Debug Hockeyapp"; - }; - C2E0DEAE1D7EF51C00EF1C8D /* Debug AppStore */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - 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_SUSPICIOUS_MOVES = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = 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; - MACOSX_DEPLOYMENT_TARGET = 10.10; - MTL_ENABLE_DEBUG_INFO = NO; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = "Debug AppStore"; - }; - C2E0DEB01D7EF51C00EF1C8D /* Debug Hockeyapp */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - COMBINE_HIDPI_IMAGES = YES; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 6N38VWS5BX; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - FRAMEWORK_VERSION = A; - INFOPLIST_FILE = TGUIKit/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; - MACH_O_TYPE = mh_dylib; - MACOSX_DEPLOYMENT_TARGET = 10.11; - PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TGUIKit; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_OBJC_BRIDGING_HEADER = ""; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.0; - }; - name = "Debug Hockeyapp"; - }; - C2E0DEB11D7EF51C00EF1C8D /* Debug AppStore */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - COMBINE_HIDPI_IMAGES = YES; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 6N38VWS5BX; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - FRAMEWORK_VERSION = A; - INFOPLIST_FILE = TGUIKit/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; - MACH_O_TYPE = mh_dylib; - MACOSX_DEPLOYMENT_TARGET = 10.11; - PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TGUIKit; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SWIFT_OBJC_BRIDGING_HEADER = ""; - SWIFT_VERSION = 4.0; - }; - name = "Debug AppStore"; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - C2E0DEA11D7EF51C00EF1C8D /* Build configuration list for PBXProject "TGUIKit" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - C2E0DEAD1D7EF51C00EF1C8D /* Debug Hockeyapp */, - C2E0DEAE1D7EF51C00EF1C8D /* Debug AppStore */, - C22069D21E8EB4D700E82730 /* Release Hockeyapp */, - C232EA391E1BCFDE00C4D38C /* Release AppStore */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = "Debug AppStore"; - }; - C2E0DEAF1D7EF51C00EF1C8D /* Build configuration list for PBXNativeTarget "TGUIKit" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - C2E0DEB01D7EF51C00EF1C8D /* Debug Hockeyapp */, - C2E0DEB11D7EF51C00EF1C8D /* Debug AppStore */, - C22069D31E8EB4D700E82730 /* Release Hockeyapp */, - C232EA3A1E1BCFDE00C4D38C /* Release AppStore */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = "Debug AppStore"; - }; -/* End XCConfigurationList section */ - }; - rootObject = C2E0DE9E1D7EF51C00EF1C8D /* Project object */; -} diff --git a/TGUIKit/TGUIKit.xcodeproj/xcuserdata/mikhailfilimonov.xcuserdatad/xcschemes/xcschememanagement.plist b/TGUIKit/TGUIKit.xcodeproj/xcuserdata/mikhailfilimonov.xcuserdatad/xcschemes/xcschememanagement.plist deleted file mode 100644 index 2755b71a46..0000000000 --- a/TGUIKit/TGUIKit.xcodeproj/xcuserdata/mikhailfilimonov.xcuserdatad/xcschemes/xcschememanagement.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - SchemeUserState - - TGUIKit.xcscheme - - isShown - - orderHint - 0 - - - SuppressBuildableAutocreation - - C2E0DEA61D7EF51C00EF1C8D - - primary - - - - - diff --git a/TGUIKit/TGUIKit/AnimationStyle.swift b/TGUIKit/TGUIKit/AnimationStyle.swift deleted file mode 100644 index e776e8528d..0000000000 --- a/TGUIKit/TGUIKit/AnimationStyle.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// AnimationStyle.swift -// TGUIKit -// -// Created by keepcoder on 15/09/16. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - -public struct AnimationStyle { - - public let duration:CFTimeInterval - public let function:String - -} diff --git a/TGUIKit/TGUIKit/BackNavigationBar.swift b/TGUIKit/TGUIKit/BackNavigationBar.swift deleted file mode 100644 index fb154ac20b..0000000000 --- a/TGUIKit/TGUIKit/BackNavigationBar.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// BackNavigationBar.swift -// TGUIKit -// -// Created by keepcoder on 05/10/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - -open class BackNavigationBar: TextButtonBarView { - - - public init(_ controller:ViewController) { - let backSettings = controller.backSettings() - super.init(controller: controller, text: backSettings.0, style: navigationButtonStyle) - - if let image = backSettings.1 { - button.set(image: image, for: .Normal) - } - button.disableActions() - button.set(handler: { [weak self] _ in - self?.controller?.executeReturn() - }, for: .Up) - } - - public func requestUpdate() { - let backSettings = controller?.backSettings() ?? ("",nil) - button.set(text: backSettings.0, for: .Normal) - if let image = backSettings.1 { - button.set(image: image, for: .Normal) - } else { - button.removeImage(for: .Normal) - } - style = navigationButtonStyle - button.style = navigationButtonStyle - needsLayout = true - } - - open override func layout() { - super.layout() - if controller?.backSettings().1 != nil { - button.centerY(x: 10) - } else { - button.center() - } - } - - deinit { - var bp:Int = 0 - bp += 1 - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - required public init(frame frameRect: NSRect) { - fatalError("init(frame:) has not been implemented") - } - -} diff --git a/TGUIKit/TGUIKit/BarView.swift b/TGUIKit/TGUIKit/BarView.swift deleted file mode 100644 index 8f86ddfe50..0000000000 --- a/TGUIKit/TGUIKit/BarView.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// BarView.swift -// TGUIKit -// -// Created by keepcoder on 16/09/16. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - -open class BarView: OverlayControl { - - - public var clickHandler:()->Void = {} - - public var minWidth:CGFloat = 80 - public private(set) weak var controller: ViewController? - - - override open func draw(_ layer: CALayer, in ctx: CGContext) { - super.draw(layer, in: ctx) - } - - - open override func setFrameSize(_ newSize: NSSize) { - super.setFrameSize(newSize) - self.setNeedsDisplay() - } - - public init(_ width:CGFloat = 80, controller: ViewController) { - self.minWidth = width - self.controller = controller - super.init() - animates = false - frame = NSMakeRect(0, 0, minWidth, 50) - overlayInitEvent() - } - - override open func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - set(background: presentation.colors.background, for: .Normal) - backgroundColor = presentation.colors.background - } - - - func overlayInitEvent() -> Void { - set(handler: { [weak self] control in - self?.clickHandler() - }, for: .Click) - updateLocalizationAndTheme() - } - - required public init(frame frameRect: NSRect) { - super.init(frame:frameRect) - overlayInitEvent() - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - -} diff --git a/TGUIKit/TGUIKit/CAAnimationUtils.swift b/TGUIKit/TGUIKit/CAAnimationUtils.swift deleted file mode 100644 index 5db353b86d..0000000000 --- a/TGUIKit/TGUIKit/CAAnimationUtils.swift +++ /dev/null @@ -1,385 +0,0 @@ -// -// CAAnimationUtils.swift -// TGUIKit -// -// Created by keepcoder on 15/09/16. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - -@objc public class CALayerAnimationDelegate: NSObject, CAAnimationDelegate { - var completion: ((Bool) -> Void)? - - public init(completion: ((Bool) -> Void)?) { - self.completion = completion - - super.init() - } - - @objc public func animationDidStop(_ anim: CAAnimation, finished flag: Bool) { - if let completion = self.completion { - completion(flag) - } - } -} - -private let completionKey = "CAAnimationUtils_completion" - -public let kCAMediaTimingFunctionSpring = "CAAnimationUtilsSpringCurve" - -public extension CAAnimation { - public var completion: ((Bool) -> Void)? { - get { - if let delegate = self.delegate as? CALayerAnimationDelegate { - return delegate.completion - } else { - return nil - } - } set(value) { - if let delegate = self.delegate as? CALayerAnimationDelegate { - delegate.completion = value - } else { - self.delegate = CALayerAnimationDelegate(completion: value) - } - } - } -} - -public func makeSpringAnimation(_ path:String) -> CABasicAnimation { - if #available(OSX 10.11, *) { - let springAnimation:CASpringAnimation = CASpringAnimation(keyPath: path) - springAnimation.mass = 3.0; - springAnimation.stiffness = 1000.0; - springAnimation.damping = 500.0; - springAnimation.initialVelocity = 0.0; - springAnimation.duration = 0.5;//springAnimation.settlingDuration; - springAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) - return springAnimation; - } else { - let anim:CABasicAnimation = CABasicAnimation(keyPath: path) - anim.duration = 0.2 - anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) - - return anim - } - -} - -public func makeSpringBounceAnimation(_ path:String, _ initialVelocity:CGFloat, _ damping: CGFloat = 88.0) -> CABasicAnimation { - if #available(OSX 10.11, *) { - let springAnimation:CASpringAnimation = CASpringAnimation(keyPath: path) - springAnimation.mass = 5.0 - springAnimation.stiffness = 900.0 - springAnimation.damping = damping - springAnimation.initialVelocity = initialVelocity - springAnimation.duration = springAnimation.settlingDuration - springAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) - return springAnimation; - } else { - let anim:CABasicAnimation = CABasicAnimation(keyPath: path) - anim.duration = 0.2 - anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) - - return anim - } - -} - - -public extension CALayer { - public func animate(from: AnyObject, to: AnyObject, keyPath: String, timingFunction: String, duration: Double, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { - if timingFunction == kCAMediaTimingFunctionSpring { - let animation = makeSpringAnimation(keyPath) - animation.fromValue = from - animation.toValue = to - animation.isRemovedOnCompletion = removeOnCompletion - animation.fillMode = kCAFillModeForwards - if let completion = completion { - animation.delegate = CALayerAnimationDelegate(completion: completion) - } - - let k = Float(1.0) - var speed: Float = 1.0 - if k != 0 && k != 1 { - speed = Float(1.0) / k - } - - animation.speed = speed * Float(animation.duration / duration) - animation.isAdditive = additive - - self.add(animation, forKey: keyPath) - } else { - let k = Float(1.0) - var speed: Float = 1.0 - if k != 0 && k != 1 { - speed = Float(1.0) / k - } - - let animation = CABasicAnimation(keyPath: keyPath) - animation.fromValue = from - animation.toValue = to - animation.duration = duration - animation.timingFunction = CAMediaTimingFunction(name: timingFunction) - animation.isRemovedOnCompletion = removeOnCompletion - animation.fillMode = kCAFillModeForwards - animation.speed = speed - animation.isAdditive = additive - if let completion = completion { - animation.delegate = CALayerAnimationDelegate(completion: completion) - } - - self.add(animation, forKey: keyPath) - } - } - - public func animateAdditive(from: NSValue, to: NSValue, keyPath: String, key: String, timingFunction: String, duration: Double, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { - let k = Float(1.0) - var speed: Float = 1.0 - if k != 0 && k != 1 { - speed = Float(1.0) / k - } - - let animation = CABasicAnimation(keyPath: keyPath) - animation.fromValue = from - animation.toValue = to - animation.duration = duration - animation.timingFunction = CAMediaTimingFunction(name: timingFunction) - animation.isRemovedOnCompletion = removeOnCompletion - animation.fillMode = kCAFillModeForwards - animation.speed = speed - animation.isAdditive = true - if let completion = completion { - animation.delegate = CALayerAnimationDelegate(completion: completion) - } - - self.add(animation, forKey: key) - } - - public func animateScaleSpring(from: CGFloat, to: CGFloat, duration: Double, initialVelocity: CGFloat = 0.0, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { - let animation = makeSpringBounceAnimation("transform", initialVelocity) - - var fr = CATransform3DIdentity - fr = CATransform3DTranslate(fr, floorToScreenPixels(frame.width / 2), floorToScreenPixels(frame.height / 2), 0) - fr = CATransform3DScale(fr, from, from, 1) - fr = CATransform3DTranslate(fr, -floorToScreenPixels(frame.width / 2), -floorToScreenPixels(frame.height / 2), 0) - - animation.fromValue = NSValue(caTransform3D: fr) - animation.toValue = to - animation.isRemovedOnCompletion = removeOnCompletion - animation.fillMode = kCAFillModeForwards - if let completion = completion { - animation.delegate = CALayerAnimationDelegate(completion: completion) - } - - let speed: Float = 1.0 - - - animation.speed = speed * Float(animation.duration / duration) - animation.isAdditive = additive - - var tr = CATransform3DIdentity - tr = CATransform3DTranslate(tr, floorToScreenPixels(frame.width / 2), floorToScreenPixels(frame.height / 2), 0) - tr = CATransform3DScale(tr, to, to, 1) - tr = CATransform3DTranslate(tr, -floorToScreenPixels(frame.width / 2), -floorToScreenPixels(frame.height / 2), 0) - animation.toValue = NSValue(caTransform3D: tr) - - - self.add(animation, forKey: "transform") - } - - public func animateScaleCenter(from: CGFloat, to: CGFloat, duration: Double, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { - let animation = CABasicAnimation(keyPath: "transform") - animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) - - var fr = CATransform3DIdentity - fr = CATransform3DTranslate(fr, floorToScreenPixels(frame.width / 2), floorToScreenPixels(frame.height / 2), 0) - fr = CATransform3DScale(fr, from, from, 1) - fr = CATransform3DTranslate(fr, -floorToScreenPixels(frame.width / 2), -floorToScreenPixels(frame.height / 2), 0) - - animation.fromValue = NSValue(caTransform3D: fr) - animation.toValue = to - animation.isRemovedOnCompletion = removeOnCompletion - animation.fillMode = kCAFillModeForwards - if let completion = completion { - animation.delegate = CALayerAnimationDelegate(completion: completion) - } - - - - animation.duration = duration - animation.isAdditive = additive - - var tr = CATransform3DIdentity - tr = CATransform3DTranslate(tr, floorToScreenPixels(frame.width / 2), floorToScreenPixels(frame.height / 2), 0) - tr = CATransform3DScale(tr, to, to, 1) - tr = CATransform3DTranslate(tr, -floorToScreenPixels(frame.width / 2), -floorToScreenPixels(frame.height / 2), 0) - animation.toValue = NSValue(caTransform3D: tr) - - - self.add(animation, forKey: "transform") - } - - public func animateRotateCenter(from: CGFloat, to: CGFloat, duration: Double, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { - - - let animation = makeSpringAnimation("transform") - - var fr = CATransform3DIdentity - fr = CATransform3DTranslate(fr, floorToScreenPixels(frame.width / 2), floorToScreenPixels(frame.height / 2), 0) - fr = CATransform3DRotate(fr, from * CGFloat.pi / 180, 0, 0, 1.0) - fr = CATransform3DTranslate(fr, -floorToScreenPixels(frame.width / 2), -floorToScreenPixels(frame.height / 2), 0) - - animation.fromValue = NSValue(caTransform3D: fr) - - animation.isRemovedOnCompletion = removeOnCompletion - animation.fillMode = kCAFillModeForwards - if let completion = completion { - animation.delegate = CALayerAnimationDelegate(completion: completion) - } - - let speed: Float = 1.0 - - - animation.speed = speed * Float(animation.duration / duration) - animation.isAdditive = additive - - var tr = CATransform3DIdentity - tr = CATransform3DTranslate(tr, floorToScreenPixels(frame.width / 2), floorToScreenPixels(frame.height / 2), 0) - tr = CATransform3DRotate(fr, to * CGFloat.pi / 180, 0, 0, 1.0) - tr = CATransform3DTranslate(tr, -floorToScreenPixels(frame.width / 2), -floorToScreenPixels(frame.height / 2), 0) - animation.toValue = NSValue(caTransform3D: tr) - - - self.add(animation, forKey: "transform") - -// let animation = CABasicAnimation(keyPath: "transform") -// animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) -// var fr = CATransform3DIdentity -// fr = CATransform3DTranslate(fr, floorToScreenPixels(frame.width / 2), floorToScreenPixels(frame.height / 2), 0) -// fr = CATransform3DRotate(fr, from * CGFloat.pi / 180, 0, 0, 1.0) -// fr = CATransform3DTranslate(fr, -floorToScreenPixels(frame.width / 2), -floorToScreenPixels(frame.height / 2), 0) -// -// animation.fromValue = NSValue(caTransform3D: fr) -// animation.isRemovedOnCompletion = removeOnCompletion -// // animation.fillMode = kCAFillModeForwards -// if let completion = completion { -// animation.delegate = CALayerAnimationDelegate(completion: completion) -// } -// -// let speed: Float = 1.0 -// -// -// animation.speed = speed * Float(animation.duration / duration) -// animation.isAdditive = additive -// -// var tr = CATransform3DIdentity -// // tr = CATransform3DTranslate(tr, floorToScreenPixels(frame.width / 2), floorToScreenPixels(frame.height / 2), 0) -// tr = CATransform3DRotate(fr, to * CGFloat.pi / 180, 0, 0, 1.0) -// //tr = CATransform3DTranslate(tr, -floorToScreenPixels(frame.width / 2), -floorToScreenPixels(frame.height / 2), 0) -// animation.toValue = NSValue(caTransform3D: tr) -// -// -// self.add(animation, forKey: "transform") - } - - - public func animateAlpha(from: CGFloat, to: CGFloat, duration: Double, timingFunction: String = kCAMediaTimingFunctionEaseOut, removeOnCompletion: Bool = true, completion: ((Bool) -> ())? = nil) { - self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "opacity", timingFunction: timingFunction, duration: duration, removeOnCompletion: removeOnCompletion, completion: completion) - } - - public func animateSpring(from: AnyObject, to: AnyObject, keyPath: String, duration: Double, initialVelocity: CGFloat = 0.0, damping: CGFloat = 88.0, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { - let animation: CABasicAnimation - if #available(iOS 9.0, *) { - animation = makeSpringBounceAnimation(keyPath, initialVelocity, damping) - } else { - animation = makeSpringAnimation(keyPath) - } - animation.fromValue = from - animation.toValue = to - animation.isRemovedOnCompletion = removeOnCompletion - animation.fillMode = kCAFillModeForwards - if let completion = completion { - animation.delegate = CALayerAnimationDelegate(completion: completion) - } - - let k = Float(1) - var speed: Float = 1.0 - if k != 0 && k != 1 { - speed = Float(1.0) / k - } - - animation.speed = speed * Float(animation.duration / duration) - animation.isAdditive = additive - - self.add(animation, forKey: keyPath) - } - - public func animateScale(from: CGFloat, to: CGFloat, duration: Double, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) { - self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "transform.scale", timingFunction: timingFunction, duration: duration, removeOnCompletion: removeOnCompletion, completion: completion) - } - - func animatePosition(from: NSPoint, to: NSPoint, duration: Double = 0.2, timingFunction: String = kCAMediaTimingFunctionEaseOut, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { - if from == to { - if let completion = completion { - completion(true) - } - return - } - self.animate(from: NSValue(point: from), to: NSValue(point: to), keyPath: "position", timingFunction: timingFunction, duration: duration, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion) - } - - func animateBounds(from: NSRect, to: NSRect, duration: Double, timingFunction: String, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { - if from == to { - if let completion = completion { - completion(true) - } - return - } - self.animate(from: NSValue(rect: from), to: NSValue(rect: to), keyPath: "bounds", timingFunction: timingFunction, duration: duration, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion) - } - - public func animateBoundsOriginYAdditive(from: CGFloat, to: CGFloat, duration: Double) { - self.animateAdditive(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.origin.y", key: "boundsOriginYAdditive", timingFunction: kCAMediaTimingFunctionEaseOut, duration: duration, removeOnCompletion: true) - } - - public func animateFrame(from: CGRect, to: CGRect, duration: Double, timingFunction: String, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) { - if from == to { - if let completion = completion { - completion(true) - } - return - } - self.animatePosition(from: CGPoint(x: from.midX, y: from.midY), to: CGPoint(x: to.midX, y: to.midY), duration: duration, timingFunction: timingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: nil) - self.animateBounds(from: CGRect(origin: self.bounds.origin, size: from.size), to: CGRect(origin: self.bounds.origin, size: to.size), duration: duration, timingFunction: timingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion) - } - - public func shake(_ duration:CFTimeInterval, from:NSPoint, to:NSPoint) { - let animation = CABasicAnimation(keyPath: "position") - animation.duration = duration; - animation.repeatCount = 4 - animation.autoreverses = true - animation.isRemovedOnCompletion = true - - animation.fromValue = NSValue(point: from) - animation.toValue = NSValue(point: to) - - self.add(animation, forKey: "position") - } - - /* - + (CAAnimation *)shakeWithDuration:(float)duration fromValue:(CGPoint)fromValue toValue:(CGPoint)toValue { - CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"]; - animation.duration = duration; - animation.repeatCount = 4; - animation.autoreverses = YES; - animation.removedOnCompletion = YES; - NSValue *fromValueValue = [NSValue value:&fromValue withObjCType:@encode(CGPoint)]; - NSValue *toValueValue = [NSValue value:&toValue withObjCType:@encode(CGPoint)]; - - animation.fromValue = fromValueValue; - animation.toValue = toValueValue; - return animation; - } - */ -} diff --git a/TGUIKit/TGUIKit/ContainableController.swift b/TGUIKit/TGUIKit/ContainableController.swift deleted file mode 100644 index 71f9bf0162..0000000000 --- a/TGUIKit/TGUIKit/ContainableController.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// ContainableController.swift -// TGUIKit -// -// Created by keepcoder on 23/10/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - -public enum ContainedViewLayoutTransitionCurve { - case easeInOut - case spring -} - -public extension ContainedViewLayoutTransitionCurve { - var timingFunction: String { - switch self { - case .easeInOut: - return kCAMediaTimingFunctionEaseInEaseOut - case .spring: - return kCAMediaTimingFunctionSpring - } - } -} - -public enum ContainedViewLayoutTransition { - case immediate - case animated(duration: Double, curve: ContainedViewLayoutTransitionCurve) -} - -public extension ContainedViewLayoutTransition { - func updateFrame(view: View, frame: CGRect, completion: ((Bool) -> Void)? = nil) { - switch self { - case .immediate: - view.frame = frame - if let completion = completion { - completion(true) - } - case let .animated(duration, curve): - let previousFrame = view.frame - view.frame = frame - view.layer?.animateFrame(from: previousFrame, to: frame, duration: duration, timingFunction: curve.timingFunction, completion: { result in - if let completion = completion { - completion(result) - } - }) - } - } - - - func updateAlpha(view: View, alpha: CGFloat, completion: ((Bool) -> Void)? = nil) { - switch self { - case .immediate: - view.alphaValue = alpha - if let completion = completion { - completion(true) - } - case let .animated(duration, curve): - let previousAlpha = view.alphaValue - view.alphaValue = alpha - view.layer?.animateAlpha(from: previousAlpha, to: alpha, duration: duration, timingFunction: curve.timingFunction, completion: { result in - if let completion = completion { - completion(result) - } - }) - } - } -} - -public protocol ContainableController: class { - var view: View! { get } - - func containerLayoutUpdated(transition: ContainedViewLayoutTransition) -} diff --git a/TGUIKit/TGUIKit/ContextMenu.swift b/TGUIKit/TGUIKit/ContextMenu.swift deleted file mode 100644 index a8fbf7b1c0..0000000000 --- a/TGUIKit/TGUIKit/ContextMenu.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// ContextMenu.swift -// TGUIKit -// -// Created by keepcoder on 03/10/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - - -public class ContextSeparatorItem : ContextMenuItem { - public init() { - super.init("", handler: {}, image: nil) - } - - public override var isSeparatorItem: Bool { - return true - } - - required public init(coder decoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -public class ContextMenuItem : NSMenuItem { - - fileprivate var handler:()->Void = {() in} - - public init(_ title:String, handler:@escaping()->Void, image:NSImage? = nil) { - self.handler = handler - - super.init(title: title, action: nil, keyEquivalent: "") - - self.title = title - self.action = #selector(click) - self.target = self - self.isEnabled = true - self.image = image - } - - required public init(coder decoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc func click() -> Void { - handler() - } - -} - -public final class ContextMenu : NSMenu, NSMenuDelegate { - - var onShow:(ContextMenu)->Void = {(ContextMenu) in} - var onClose:()->Void = {() in} - - weak var view:NSView? - - public static func show(items:[ContextMenuItem], view:NSView, event:NSEvent, onShow:@escaping(ContextMenu)->Void, onClose:@escaping()->Void) -> Void { - - let menu = ContextMenu.init() - menu.onShow = onShow - menu.onClose = onClose - menu.view = view - - for item in items { - menu.addItem(item) - } - - menu.delegate = menu - NSMenu.popUpContextMenu(menu, with: event, for: view) - } - - - public override func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { - return true - } - - public func menuWillOpen(_ menu: NSMenu) { - onShow(self) - } - - public func menuDidClose(_ menu: NSMenu) { - onClose() - } - -} diff --git a/TGUIKit/TGUIKit/Control.swift b/TGUIKit/TGUIKit/Control.swift deleted file mode 100644 index 1bbba35f26..0000000000 --- a/TGUIKit/TGUIKit/Control.swift +++ /dev/null @@ -1,394 +0,0 @@ -// -// Control.swift -// TGUIKit -// -// Created by keepcoder on 25/09/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import SwiftSignalKitMac - -public enum ControlState { - case Normal - case Hover - case Highlight - case Other -} - -public enum ControlEvent { - case Down - case Up - case Click - case SingleClick - case RightClick - case MouseDragging - case LongMouseDown - case LongMouseUp - case LongOver -} - -open class Control: View { - - open var isEnabled:Bool = true { - didSet { - if isEnabled != oldValue { - apply(state: controlState) - } - } - } - open var hideAnimated:Bool = false - - private let longHandleDisposable = MetaDisposable() - private let longOverHandleDisposable = MetaDisposable() - public var isSelected:Bool { - didSet { - if isSelected != oldValue { - apply(state: isSelected ? .Highlight : self.controlState) - } - } - } - - open var animationStyle:AnimationStyle = AnimationStyle(duration:0.3, function:kCAMediaTimingFunctionSpring) - - var trackingArea:NSTrackingArea? - - public var interactionStateForRestore:Bool? = nil - - public var userInteractionEnabled:Bool = true - - private var handlers:[(ControlEvent,(Control) -> Void)] = [] - private var stateHandlers:[(ControlState,(Control) -> Void)] = [] - - private var backgroundState:[ControlState:NSColor] = [:] - private var mouseMovedInside: Bool = true - private var longInvoked: Bool = false - open override var backgroundColor: NSColor { - get{ - return self.style.backgroundColor - } - set { - if self.style.backgroundColor != newValue { - self.style.backgroundColor = newValue - self.setNeedsDisplayLayer() - } - } - } - - public var style:ControlStyle = ControlStyle() { - didSet { - if style != oldValue { - apply(style:style) - } - } - } - - open override func viewDidMoveToWindow() { - super.viewDidMoveToWindow() - apply(state: self.controlState) - } - - public private(set) var controlState:ControlState = .Normal { - didSet { - if oldValue != controlState { - apply(state: isSelected ? .Highlight : controlState) - - for (state,handler) in stateHandlers { - if state == controlState { - handler(self) - } - } - - } - } - } - - public func apply(state:ControlState) -> Void { - let state:ControlState = self.isSelected ? .Highlight : state - if let color = backgroundState[state] { - self.layer?.backgroundColor = color.cgColor - } else { - self.layer?.backgroundColor = self.backgroundColor.cgColor - } - if animates { - self.layer?.animateBackground() - } - } - - private var mouseIsDown:Bool = false - - open override func updateTrackingAreas() { - super.updateTrackingAreas(); - - if let trackingArea = trackingArea { - self.removeTrackingArea(trackingArea) - } - - trackingArea = nil - - if let _ = window { - let options:NSTrackingArea.Options = [NSTrackingArea.Options.cursorUpdate, NSTrackingArea.Options.mouseEnteredAndExited, NSTrackingArea.Options.mouseMoved, NSTrackingArea.Options.activeInKeyWindow, NSTrackingArea.Options.inVisibleRect] - self.trackingArea = NSTrackingArea(rect: self.bounds, options: options, owner: self, userInfo: nil) - - self.addTrackingArea(self.trackingArea!) - } - - } - - open override func viewDidMoveToSuperview() { - super.viewDidMoveToSuperview() - updateTrackingAreas() - } - - deinit { - if let trackingArea = self.trackingArea { - self.removeTrackingArea(trackingArea) - } - longHandleDisposable.dispose() - longOverHandleDisposable.dispose() - } - - public var controlIsHidden: Bool { - return super.isHidden || layer!.opacity < Float(1.0) - } - - open override var isHidden: Bool { - get { - return super.isHidden - } - set { - if newValue != super.isHidden { - if hideAnimated { - if !newValue { - super.isHidden = newValue - } - self.layer?.opacity = newValue ? 0.0 : 1.0 - self.layer?.animateAlpha(from: newValue ? 1.0 : 0.0, to: newValue ? 0.0 : 1.0, duration: 0.2, completion:{[weak self](completed) in - self?.updateHiddenState(newValue) - }) - } else { - updateHiddenState(newValue) - } - } - } - } - - public func forceHide() -> Void { - super.isHidden = true - self.layer?.removeAllAnimations() - } - - private func updateHiddenState(_ value:Bool) -> Void { - super.isHidden = value - } - - - public var canHighlight: Bool = true - - public func set(handler:@escaping (Control) -> Void, for event:ControlEvent) -> Void { - handlers.append((event,handler)) - } - - public func set(handler:@escaping (Control) -> Void, for event:ControlState) -> Void { - stateHandlers.append((event,handler)) - } - - public func set(background:NSColor, for state:ControlState) -> Void { - backgroundState[state] = background - apply(state: self.controlState) - self.setNeedsDisplayLayer() - } - - public func removeLastHandler() -> Void { - if !handlers.isEmpty { - handlers.removeLast() - } - } - - public func removeLastStateHandler() -> Void { - if !stateHandlers.isEmpty { - stateHandlers.removeLast() - } - } - - public func removeAllStateHandler() -> Void { - stateHandlers.removeAll() - } - - public func removeAllHandlers() ->Void { - handlers.removeAll() - } - - override open func mouseDown(with event: NSEvent) { - mouseIsDown = true - longInvoked = false - longOverHandleDisposable.set(nil) - - if event.modifierFlags.contains(.control) { - super.mouseDown(with: event) - return - } - - if userInteractionEnabled { - send(event: .Down) - updateState() - - let disposable = (Signal.single(Void()) |> delay(0.3, queue: Queue.mainQueue())).start(next: { [weak self] in - if let inside = self?.mouseInside(), inside { - self?.longInvoked = true - self?.send(event: .LongMouseDown) - } - }) - - longHandleDisposable.set(disposable) - - } else { - super.mouseDown(with: event) - } - } - - override open func mouseUp(with event: NSEvent) { - - longHandleDisposable.set(nil) - longOverHandleDisposable.set(nil) - mouseIsDown = false - - if userInteractionEnabled && !event.modifierFlags.contains(.control) { - if isEnabled { - send(event: .Up) - - if mouseInside() && !longInvoked { - if event.clickCount == 1 { - send(event: .SingleClick) - } - send(event: .Click) - } - } - - updateState() - - } else { - super.mouseUp(with: event) - } - } - - func send(event:ControlEvent) -> Void { - for (e,handler) in handlers { - if e == event { - handler(self) - } - } - } - - override open func mouseMoved(with event: NSEvent) { - if userInteractionEnabled { - updateState() - } else { - super.mouseMoved(with: event) - } - } - - open override func rightMouseDown(with event: NSEvent) { - if userInteractionEnabled { - updateState() - super.rightMouseDown(with: event) - } else { - super.rightMouseDown(with: event) - } - } - - public func updateState() -> Void { - if mouseInside() { - if mouseIsDown && canHighlight { - self.controlState = .Highlight - } else if mouseMovedInside { - self.controlState = .Hover - } else { - self.controlState = .Normal - } - } else { - self.controlState = .Normal - } - } - - override open func mouseEntered(with event: NSEvent) { - if userInteractionEnabled { - - let disposable = (Signal.single(Void()) |> delay(0.3, queue: Queue.mainQueue())).start(next: { [weak self] in - if let strongSelf = self, strongSelf.mouseInside(), strongSelf.controlState == .Hover { - strongSelf.send(event: .LongOver) - } - }) - longOverHandleDisposable.set(disposable) - - updateState() - } else { - super.mouseEntered(with: event) - } - } - - override open func mouseExited(with event: NSEvent) { - if userInteractionEnabled { - - updateState() - } else { - super.mouseExited(with: event) - } - } - - - - override open func mouseDragged(with event: NSEvent) { - if userInteractionEnabled { - send(event: .MouseDragging) - updateState() - } else { - super.mouseDragged(with: event) - } - } - - func apply(style:ControlStyle) -> Void { - set(background: style.backgroundColor, for: .Normal) - self.backgroundColor = style.backgroundColor - self.setNeedsDisplayLayer() - } - - - - required public init(frame frameRect: NSRect) { - self.isSelected = false - super.init(frame: frameRect) - animates = true - guard #available(OSX 10.12, *) else { - layer?.opacity = 0.99 - return - } - //self.wantsLayer = true - //self.layer?.isOpaque = true - } - - public override init() { - self.isSelected = false - super.init(frame: NSZeroRect) - animates = true - - guard #available(OSX 10.12, *) else { - layer?.opacity = 0.99 - return - } - //self.wantsLayer = true - //self.layer?.isOpaque = true - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - open override func becomeFirstResponder() -> Bool { - if let window = kitWindow { - return window.makeFirstResponder(self) - } - return false - } - -} diff --git a/TGUIKit/TGUIKit/DraggingView.swift b/TGUIKit/TGUIKit/DraggingView.swift deleted file mode 100644 index b06ebc5e83..0000000000 --- a/TGUIKit/TGUIKit/DraggingView.swift +++ /dev/null @@ -1,164 +0,0 @@ -// -// DragController.swift -// Telegram-Mac -// -// Created by keepcoder on 30/10/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - -public class DragItem { - let title:String - let desc:String - let handler:()->Void - - let attr:NSAttributedString - - public init(title:String, desc:String,handler:@escaping()->Void) { - self.title = title - self.desc = desc - self.handler = handler - - let attr:NSMutableAttributedString = NSMutableAttributedString() - _ = attr.append(string: title, color: presentation.colors.grayText, font: .normal(.huge)) - _ = attr.append(string: "\n") - - _ = attr.append(string: desc, color: presentation.colors.text, font: .medium(.custom(16))) - - self.attr = attr.copy() as! NSAttributedString - } -} - -class DragView : OverlayControl { - var item:DragItem - - var textView:TextView = TextView() - - init(item:DragItem) { - self.item = item - super.init(frame: NSZeroRect) - - addSubview(textView) - textView.backgroundColor = presentation.colors.background - self.layer?.cornerRadius = .cornerRadius - self.layer?.borderWidth = 2.0 - self.layer?.backgroundColor = presentation.colors.background.cgColor - self.layer?.borderColor = presentation.colors.border.cgColor - self.backgroundColor = presentation.colors.background - self.set(handler: { control in - control.layer?.borderColor = presentation.colors.blueUI.cgColor - control.layer?.animateBorder() - }, for: .Hover) - - self.set(handler: { control in - control.layer?.borderColor = presentation.colors.border.cgColor - control.layer?.animateBorder() - }, for: .Normal) - - - } - - - - override func layout() { - super.layout() - let layout:TextViewLayout = TextViewLayout(item.attr, maximumNumberOfLines: 2, truncationType: .middle, alignment:.center) - layout.measure(width: frame.width - 20) - - textView.update(layout) - textView.center() - } - - override func setFrameSize(_ newSize: NSSize) { - super.setFrameSize(newSize) - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - required public init(frame frameRect: NSRect) { - fatalError("init(frame:) has not been implemented") - } -} - -public class DraggingView: SplitView { - - var container:View = View() - - public weak var controller:ViewController? - - required public init(frame frameRect: NSRect) { - super.init(frame: frameRect) - - container.backgroundColor = .clear - self.registerForDraggedTypes([.string, .tiff, .kUrl, .kFilenames]) - } - - override public func performDragOperation(_ sender: NSDraggingInfo) -> Bool { - - for itemView in container.subviews as! [DragView] { - if itemView.mouseInside() { - itemView.item.handler() - return true - } - } - - return false - } - - func layoutItems(with items:[DragItem]) { - container.removeAllSubviews() - - let itemSize = NSMakeSize(frame.width - 10, ceil((frame.height - 10 - (5 * (CGFloat(items.count) - 1))) / CGFloat(items.count))) - - var y:CGFloat = 5 - for item in items { - let view:DragView = DragView(item:item) - view.frame = NSMakeRect(5, y, itemSize.width, itemSize.height) - container.addSubview(view) - y += itemSize.height + 5 - - } - - - } - - override public func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { - - if let items = controller?.draggingItems(for: sender.draggingPasteboard()), items.count > 0, !sender.draggingSourceOperationMask().isEmpty { - - container.frame = bounds - - if container.superview == nil { - layoutItems(with: items) - addSubview(container) - } - container.layer?.removeAllAnimations() - container.layer?.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) - } - - - - return sender.draggingSourceOperationMask() - } - - override public func draggingExited(_ sender: NSDraggingInfo?) { - container.layer?.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion:false, completion:{[weak self] (completed) in - if completed { - self?.container.removeFromSuperview() - } - }) - } - - public override func draggingEnded(_ sender: NSDraggingInfo?) { - draggingExited(sender) - } - - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - -} diff --git a/TGUIKit/TGUIKit/DrawingContext.swift b/TGUIKit/TGUIKit/DrawingContext.swift deleted file mode 100644 index 200b0546a1..0000000000 --- a/TGUIKit/TGUIKit/DrawingContext.swift +++ /dev/null @@ -1,299 +0,0 @@ -// -// DrawingContext.swift -// TGLibrary -// -// Created by keepcoder on 18/09/16. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - - -public func generateImage(_ size: CGSize, contextGenerator: (CGSize, CGContext) -> Void, opaque: Bool = false) -> CGImage? { - let scale:CGFloat = 2.0 - let scaledSize = CGSize(width: size.width * scale, height: size.height * scale) - let bytesPerRow = (4 * Int(scaledSize.width) + 15) & (~15) - let length = bytesPerRow * Int(scaledSize.height) - let bytes = malloc(length)!.assumingMemoryBound(to: Int8.self) - - guard let provider = CGDataProvider(dataInfo: bytes, data: bytes, size: length, releaseData: { bytes, _, _ in - free(bytes) - }) - else { - return nil - } - - let bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | (opaque ? CGImageAlphaInfo.noneSkipFirst.rawValue : CGImageAlphaInfo.premultipliedFirst.rawValue)) - - guard let context = CGContext(data: bytes, width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo.rawValue) - else { - return nil - } - - context.scaleBy(x: scale, y: scale) - - contextGenerator(size, context) - - guard let image = CGImage(width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: false, intent: .defaultIntent) - else { - return nil - } - - return image -} - -public func generateImage(_ size: CGSize, opaque: Bool = false, scale: CGFloat? = nil, rotatedContext: (CGSize, CGContext) -> Void) -> CGImage? { - let selectedScale = scale ?? System.backingScale - let scaledSize = CGSize(width: size.width * selectedScale, height: size.height * selectedScale) - let bytesPerRow = (4 * Int(scaledSize.width) + 15) & (~15) - let length = bytesPerRow * Int(scaledSize.height) - let bytes = malloc(length)!.assumingMemoryBound(to: Int8.self) - - guard let provider = CGDataProvider(dataInfo: bytes, data: bytes, size: length, releaseData: { bytes, _, _ in - free(bytes) - }) - else { - return nil - } - - let bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | (opaque ? CGImageAlphaInfo.noneSkipFirst.rawValue : CGImageAlphaInfo.premultipliedFirst.rawValue)) - - guard let context = CGContext(data: bytes, width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo.rawValue) else { - return nil - } - - context.scaleBy(x: selectedScale, y: selectedScale) - 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) - - rotatedContext(size, context) - - guard let image = CGImage(width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: false, intent: .defaultIntent) - else { - return nil - } - - return image -} - -let deviceColorSpace = CGColorSpaceCreateDeviceRGB() - -public enum DrawingContextBltMode { - case Alpha -} - -public class DrawingContext { - public let size: CGSize - public let scale: CGFloat - public let scaledSize: CGSize - public let bytesPerRow: Int - private let bitmapInfo: CGBitmapInfo - public let length: Int - public let bytes: UnsafeMutableRawPointer - let provider: CGDataProvider? - - private var _context: CGContext? - - public func withContext(_ f: (CGContext) -> ()) { - if self._context == nil { - if let c = CGContext(data: bytes, width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: self.bitmapInfo.rawValue) { - c.scaleBy(x: scale, y: scale) - self._context = c - } - } - - if let _context = self._context { - f(_context) - } - } - - public func withFlippedContext(horizontal: Bool = false, vertical: Bool = false, _ f: (CGContext) -> ()) { - if self._context == nil { - if let c = CGContext(data: bytes, width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: self.bitmapInfo.rawValue) { - c.scaleBy(x: scale, y: scale) - self._context = c - } - } - - if let _context = self._context { - _context.translateBy(x: self.size.width / 2.0, y: self.size.height / 2.0) - _context.scaleBy(x: horizontal ? -1.0 : 1.0, y: vertical ? -1.0 : 1.0) - _context.translateBy(x: -self.size.width / 2.0, y: -self.size.height / 2.0) - - f(_context) - - _context.translateBy(x: self.size.width / 2.0, y: self.size.height / 2.0) - _context.scaleBy(x: horizontal ? -1.0 : 1.0, y: vertical ? -1.0 : 1.0) - _context.translateBy(x: -self.size.width / 2.0, y: -self.size.height / 2.0) - } - } - - public init(size: CGSize, scale: CGFloat, clear: Bool = false) { - self.size = size - self.scale = scale - self.scaledSize = CGSize(width: size.width * scale, height: size.height * scale) - - self.bytesPerRow = (4 * Int(scaledSize.width) + 15) & (~15) - self.length = bytesPerRow * Int(scaledSize.height) - - self.bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue) - - self.bytes = malloc(length)! - if clear { - memset(self.bytes, 0, self.length) - } - self.provider = CGDataProvider(dataInfo: bytes, data: bytes, size: length, releaseData: { bytes, _, _ in - free(bytes) - }) - } - - public func generateImage() -> CGImage? { - if let image = CGImage(width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo, provider: provider!, decode: nil, shouldInterpolate: false, intent: .defaultIntent) { - return image - } else { - return nil - } - } - - public func colorAt(_ point: CGPoint) -> NSColor { - let x = Int(point.x * self.scale) - let y = Int(point.y * self.scale) - if x >= 0 && x < Int(self.scaledSize.width) && y >= 0 && y < Int(self.scaledSize.height) { - let srcLine = self.bytes.advanced(by: y * self.bytesPerRow).assumingMemoryBound(to: UInt32.self) - let pixel = srcLine + x - let colorValue = pixel.pointee - return NSColor(UInt32(colorValue)) - } else { - return NSColor.clear - } - } - - public func blt(_ other: DrawingContext, at: CGPoint, mode: DrawingContextBltMode = .Alpha) { - if abs(other.scale - self.scale) < CGFloat.ulpOfOne { - let srcX = 0 - var srcY = 0 - let dstX = Int(at.x * self.scale) - var dstY = Int(at.y * self.scale) - - let width = min(Int(self.size.width * self.scale) - dstX, Int(other.size.width * self.scale)) - let height = min(Int(self.size.height * self.scale) - dstY, Int(other.size.height * self.scale)) - - let maxDstX = dstX + width - let maxDstY = dstY + height - - switch mode { - case .Alpha: - while dstY < maxDstY { - let srcLine = other.bytes.advanced(by: max(0, srcY) * other.bytesPerRow).assumingMemoryBound(to: UInt32.self) - let dstLine = self.bytes.advanced(by: max(0, dstY) * self.bytesPerRow).assumingMemoryBound(to: UInt32.self) - - var dx = dstX - var sx = srcX - while dx < maxDstX { - let srcPixel = srcLine + sx - let dstPixel = dstLine + dx - - let baseColor = dstPixel.pointee - let baseAlpha = (baseColor >> 24) & 0xff - let baseR = (baseColor >> 16) & 0xff - let baseG = (baseColor >> 8) & 0xff - let baseB = baseColor & 0xff - - let alpha = min(baseAlpha, srcPixel.pointee >> 24) - - let r = (baseR * alpha) / 255 - let g = (baseG * alpha) / 255 - let b = (baseB * alpha) / 255 - - dstPixel.pointee = (alpha << 24) | (r << 16) | (g << 8) | b - - dx += 1 - sx += 1 - } - - dstY += 1 - srcY += 1 - } - } - } - } -} - -public enum ParsingError: Error { - case Generic -} - -public func readCGFloat(_ index: inout UnsafePointer, end: UnsafePointer, separator: UInt8) throws -> CGFloat { - let begin = index - var seenPoint = false - while index <= end { - let c = index.pointee - index = index.successor() - - if c == 46 { // . - if seenPoint { - throw ParsingError.Generic - } else { - seenPoint = true - } - } else if c == separator { - break - } else if c < 48 || c > 57 { - throw ParsingError.Generic - } - } - - if index == begin { - throw ParsingError.Generic - } - - if let value = NSString(bytes: UnsafeRawPointer(begin), length: index - begin, encoding: String.Encoding.utf8.rawValue)?.floatValue { - return CGFloat(value) - } else { - throw ParsingError.Generic - } -} - -public func drawSvgPath(_ context: CGContext, path: StaticString) throws { - var index: UnsafePointer = path.utf8Start - let end = path.utf8Start.advanced(by: path.utf8CodeUnitCount) - while index < end { - let c = index.pointee - index = index.successor() - - if c == 77 { // M - let x = try readCGFloat(&index, end: end, separator: 44) - let y = try readCGFloat(&index, end: end, separator: 32) - - //print("Move to \(x), \(y)") - context.move(to: CGPoint(x: x, y: y)) - } else if c == 76 { // L - let x = try readCGFloat(&index, end: end, separator: 44) - let y = try readCGFloat(&index, end: end, separator: 32) - - //print("Line to \(x), \(y)") - context.addLine(to: CGPoint(x: x, y: y)) - } else if c == 67 { // C - let x1 = try readCGFloat(&index, end: end, separator: 44) - let y1 = try readCGFloat(&index, end: end, separator: 32) - let x2 = try readCGFloat(&index, end: end, separator: 44) - let y2 = try readCGFloat(&index, end: end, separator: 32) - let x = try readCGFloat(&index, end: end, separator: 44) - let y = try readCGFloat(&index, end: end, separator: 32) - context.addCurve(to: CGPoint(x: x1, y: y1), control1: CGPoint(x: x2, y: y2), control2: CGPoint(x: x, y: y)) - - //print("Line to \(x), \(y)") - - } else if c == 90 { // Z - if index != end && index.pointee != 32 { - throw ParsingError.Generic - } - - //CGContextClosePath(context) - context.fillPath() - //CGContextBeginPath(context) - //print("Close") - } - } -} diff --git a/TGUIKit/TGUIKit/Extensions.swift b/TGUIKit/TGUIKit/Extensions.swift deleted file mode 100644 index 81a5022ac0..0000000000 --- a/TGUIKit/TGUIKit/Extensions.swift +++ /dev/null @@ -1,1041 +0,0 @@ -// -// Extensions.swift -// TGUIKit -// -// Created by keepcoder on 08/09/16. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Foundation - - -public extension NSAttributedString { - - func CTSize(_ width:CGFloat, framesetter:CTFramesetter?) -> (CTFramesetter,NSSize) { - - var fs = framesetter - - if fs == nil { - fs = CTFramesetterCreateWithAttributedString(self); - } - - var textSize:CGSize = CTFramesetterSuggestFrameSizeWithConstraints(fs!, CFRangeMake(0,self.length), nil, NSMakeSize(width, CGFloat.greatestFiniteMagnitude), nil); - - textSize.width = ceil(textSize.width) - textSize.height = ceil(textSize.height) - - return (fs!,textSize); - - } - - public var range:NSRange { - return NSMakeRange(0, self.length) - } - - public func trimRange(_ range:NSRange) -> NSRange { - let loc:Int = min(range.location,self.length) - let length:Int = min(range.length, self.length - loc) - return NSMakeRange(loc, length) - } - - public static func initialize(string:String?, color:NSColor? = nil, font:NSFont? = nil, coreText:Bool = true) -> NSAttributedString { - let attr:NSMutableAttributedString = NSMutableAttributedString() - _ = attr.append(string: string, color: color, font: font, coreText: true) - - return attr.copy() as! NSAttributedString - } - - -} - -public extension String { - - - public static func prettySized(with size:Int) -> String { - var converted:Double = Double(size) - var factor:Int = 0 - - let tokens:[String] = ["Bytes", "KB", "MB", "GB", "TB"] - - while converted > 1024.0 { - converted /= 1024.0 - factor += 1 - } - - if factor == 0 { - converted = 1.0 - } - factor = Swift.max(1,factor) - - if ceil(converted) - converted != 0.0 { - return String(format: "%.2f %@", converted, tokens[factor]) - } else { - return String(format: "%.0f %@", converted, tokens[factor]) - } - - } - -} - -public extension NSAttributedStringKey { - public static var preformattedCode: NSAttributedStringKey { - return NSAttributedStringKey(rawValue: "TGPreformattedCodeAttributeName") - } - public static var preformattedPre: NSAttributedStringKey { - return NSAttributedStringKey(rawValue: "TGPreformattedPreAttributeName") - } - public static var selectedColor: NSAttributedStringKey { - return NSAttributedStringKey(rawValue: "KSelectedColorAttributeName") - } -} - -public extension NSPasteboard.PasteboardType { - public static var kUrl:NSPasteboard.PasteboardType { - return NSPasteboard.PasteboardType(kUTTypeURL as String) - } - public static var kFilenames:NSPasteboard.PasteboardType { - return NSPasteboard.PasteboardType("NSFilenamesPboardType") - } - public static var kFileUrl: NSPasteboard.PasteboardType { - return NSPasteboard.PasteboardType(kUTTypeFileURL as String) - } -} - -public struct ParsingType: OptionSet { - public var rawValue: UInt32 - - public init(rawValue: UInt32) { - self.rawValue = rawValue - } - - public init() { - self.rawValue = 0 - } - - public init(_ flags: ParsingType) { - var rawValue: UInt32 = 0 - - if flags.contains(ParsingType.Links) { - rawValue |= ParsingType.Links.rawValue - } - - if flags.contains(ParsingType.Mentions) { - rawValue |= ParsingType.Mentions.rawValue - } - - if flags.contains(ParsingType.Commands) { - rawValue |= ParsingType.Commands.rawValue - } - - if flags.contains(ParsingType.Hashtags) { - rawValue |= ParsingType.Hashtags.rawValue - } - - self.rawValue = rawValue - } - - public static let Links = ParsingType(rawValue: 1) - public static let Mentions = ParsingType(rawValue: 2) - public static let Commands = ParsingType(rawValue: 4) - public static let Hashtags = ParsingType(rawValue: 8) -} - -public extension NSMutableAttributedString { - - public func append(string:String?, color:NSColor? = nil, font:NSFont? = nil, coreText:Bool = true) -> NSRange { - - if(string == nil) { - return NSMakeRange(0, 0) - } - - let slength:Int = self.length - - - var range:NSRange - - self.append(NSAttributedString(string: string!)) - let nlength:Int = self.length - slength - range = NSMakeRange(self.length - nlength, nlength) - - if let c = color { - self.addAttribute(NSAttributedStringKey.foregroundColor, value: c, range:range ) - } - - if let f = font { - if coreText { - self.setCTFont(font: f, range: range) - } - self.setFont(font: f, range: range) - } - - - return range - - } - - public func add(link:Any, for range:NSRange, color: NSColor = presentation.colors.link) { - self.addAttribute(NSAttributedStringKey.link, value: link, range: range) - self.addAttribute(NSAttributedStringKey.foregroundColor, value: color, range: range) - } - - public func setCTFont(font:NSFont, range:NSRange) -> Void { - self.addAttribute(NSAttributedStringKey(kCTFontAttributeName as String), value: CTFontCreateWithFontDescriptor(font.fontDescriptor, 0, nil), range: range) - } - - public func setSelected(color:NSColor,range:NSRange) -> Void { - self.addAttribute(.selectedColor, value: color, range: range) - } - - - public func setFont(font:NSFont, range:NSRange) -> Void { - self.addAttribute(NSAttributedStringKey.font, value: font, range: range) - } - -} - - -public extension CALayer { - - public func disableActions() -> Void { - - self.actions = ["onOrderIn":NSNull(),"sublayers":NSNull(),"bounds":NSNull(),"frame":NSNull(),"position":NSNull(),"contents":NSNull(),"backgroundColor":NSNull(),"border":NSNull(), "shadowOffset": NSNull()] - - } - - public func animateBackground() ->Void { - let animation = CABasicAnimation(keyPath: "backgroundColor") - animation.duration = 0.2 - self.add(animation, forKey: "backgroundColor") - } - - - - public func animateBorder() ->Void { - let animation = CABasicAnimation(keyPath: "borderWidth") - animation.duration = 0.2 - self.add(animation, forKey: "borderWidth") - } - - public func animateContents() ->Void { - let animation = CABasicAnimation(keyPath: "contents") - animation.duration = 0.2 - self.add(animation, forKey: "contents") - } - -} - -public extension String { - - public var nsstring:NSString { - return self as NSString - } - - public var length:Int { - return self.nsstring.length - } -} - - -public extension NSView { - - public var snapshot: NSImage { - guard let bitmapRep = bitmapImageRepForCachingDisplay(in: bounds) else { return NSImage() } - cacheDisplay(in: bounds, to: bitmapRep) - let image = NSImage() - image.addRepresentation(bitmapRep) - bitmapRep.size = bounds.size - return NSImage(data: dataWithPDF(inside: bounds))! - } - - public func _mouseInside() -> Bool { - if let window = self.window { - var location:NSPoint = window.mouseLocationOutsideOfEventStream - location = self.convert(location, from: nil) - - if let view = window.contentView!.hitTest(window.mouseLocationOutsideOfEventStream) { - if let view = view as? View { - if view.isEventLess { - return NSPointInRect(location, self.bounds) - } - } - if view == self { - return NSPointInRect(location, self.bounds) - } else { - var s = view.superview - while let sv = s { - if sv == self { - return NSPointInRect(location, self.bounds) - } - s = sv.superview - } - } - } - - } - return false - } - - public var backingScaleFactor: CGFloat { - if let window = window { - return window.backingScaleFactor - } else { - return System.backingScale - } - } - - public func removeAllSubviews() -> Void { - while (self.subviews.count > 0) { - self.subviews[0].removeFromSuperview(); - } - } - - public func isInnerView(_ view:NSView?) -> Bool { - var inner = false - for i in 0 ..< subviews.count { - inner = subviews[i] == view - if !inner && !subviews[i].subviews.isEmpty { - inner = subviews[i].isInnerView(view) - } - if inner { - break - } - } - return inner - } - - public func setFrameSize(_ width:CGFloat, _ height:CGFloat) { - self.setFrameSize(NSMakeSize(width, height)) - } - - public func setFrameOrigin(_ x:CGFloat, _ y:CGFloat) { - self.setFrameOrigin(NSMakePoint(x, y)) - } - - public var background:NSColor { - get { - if let view = self as? View { - return view.backgroundColor - } - if let backgroundColor = layer?.backgroundColor { - return NSColor(cgColor: backgroundColor) ?? .white - } - return .white - } - set { - if let view = self as? View { - view.backgroundColor = newValue - } else { - self.layer?.backgroundColor = newValue.cgColor - } - } - } - - public func centerX(_ superView:NSView? = nil, y:CGFloat? = nil) -> Void { - - var x:CGFloat = 0 - - if let sv = superView { - x = CGFloat(roundf(Float((sv.frame.width - frame.width)/2.0))) - } else if let sv = self.superview { - x = CGFloat(roundf(Float((sv.frame.width - frame.width)/2.0))) - } - - self.setFrameOrigin(NSMakePoint(x, y == nil ? NSMinY(self.frame) : y!)) - } - - public func focus(_ size:NSSize) -> NSRect { - var x:CGFloat = 0 - var y:CGFloat = 0 - - x = CGFloat(roundf(Float((frame.width - size.width)/2.0))) - y = CGFloat(roundf(Float((frame.height - size.height)/2.0))) - - - return NSMakeRect(x, y, size.width, size.height) - } - - public func focus(_ size:NSSize, inset:NSEdgeInsets) -> NSRect { - let x:CGFloat = CGFloat(roundf(Float((frame.width - size.width + (inset.left + inset.right))/2.0))) - let y:CGFloat = CGFloat(roundf(Float((frame.height - size.height + (inset.top + inset.bottom))/2.0))) - return NSMakeRect(x, y, size.width, size.height) - } - - public func centerY(_ superView:NSView? = nil, x:CGFloat? = nil) -> Void { - - var y:CGFloat = 0 - - if let sv = superView { - y = CGFloat(roundf(Float((sv.frame.height - frame.height)/2.0))) - } else if let sv = self.superview { - y = CGFloat(roundf(Float((sv.frame.height - frame.height)/2.0))) - } - - self.setFrameOrigin(NSMakePoint(x ?? frame.minX, y)) - } - - - public func center(_ superView:NSView? = nil) -> Void { - - var x:CGFloat = 0 - var y:CGFloat = 0 - - if let sv = superView { - x = CGFloat(roundf(Float((sv.frame.width - frame.width)/2.0))) - y = CGFloat(roundf(Float((sv.frame.height - frame.height)/2.0))) - } else if let sv = self.superview { - x = CGFloat(roundf(Float((sv.frame.width - frame.width)/2.0))) - y = CGFloat(roundf(Float((sv.frame.height - frame.height)/2.0))) - } - - self.setFrameOrigin(NSMakePoint(x, y)) - - } - - - public func _change(pos position: NSPoint, animated: Bool, _ save:Bool = true, removeOnCompletion: Bool = true, duration:Double = 0.2, timingFunction: String = kCAMediaTimingFunctionEaseOut, completion:((Bool)->Void)? = nil) -> Void { - if animated { - - var presentX = NSMinX(self.frame) - var presentY = NSMinY(self.frame) - let presentation:CALayer? = self.layer?.presentation() - if let presentation = presentation, self.layer?.animation(forKey:"position") != nil { - presentY = NSMinY(presentation.frame) - presentX = NSMinX(presentation.frame) - } - - self.layer?.animatePosition(from: NSMakePoint(presentX, presentY), to: position, duration: duration, timingFunction: timingFunction, removeOnCompletion: removeOnCompletion, completion: completion) - } else { - self.layer?.removeAnimation(forKey: "position") - } - if save { - self.setFrameOrigin(position) - if let completion = completion, !animated { - completion(true) - } - } - - } - - public func shake() { - let a:CGFloat = 3 - if let layer = layer { - self.layer?.shake(0.04, from:NSMakePoint(-a + layer.position.x,layer.position.y), to:NSMakePoint(a + layer.position.x, layer.position.y)) - } - NSSound.beep() - } - - public func _change(size: NSSize, animated: Bool, _ save:Bool = true, removeOnCompletion: Bool = true, duration:Double = 0.2, timingFunction: String = kCAMediaTimingFunctionEaseOut, completion:((Bool)->Void)? = nil) { - if animated { - var presentBounds:NSRect = self.layer?.bounds ?? self.bounds - let presentation = self.layer?.presentation() - if let presentation = presentation, self.layer?.animation(forKey:"bounds") != nil { - presentBounds.size.width = NSWidth(presentation.bounds) - presentBounds.size.height = NSHeight(presentation.bounds) - } - - self.layer?.animateBounds(from: presentBounds, to: NSMakeRect(0, 0, size.width, size.height), duration: duration, timingFunction: timingFunction, removeOnCompletion: removeOnCompletion, completion: completion) - - } else { - self.layer?.removeAnimation(forKey: "bounds") - } - if save { - self.frame = NSMakeRect(NSMinX(self.frame), NSMinY(self.frame), size.width, size.height) - } - } - - public func _changeBounds(from: NSRect, to: NSRect, animated: Bool, _ save:Bool = true, removeOnCompletion: Bool = true, duration:Double = 0.2, timingFunction: String = kCAMediaTimingFunctionEaseOut, completion:((Bool)->Void)? = nil) { - - if save { - self.bounds = to - } - - if animated { - self.layer?.animateBounds(from: from, to: to, duration: duration, timingFunction: timingFunction, removeOnCompletion: removeOnCompletion, completion: completion) - - } else { - self.layer?.removeAnimation(forKey: "bounds") - } - - if !animated { - completion?(true) - } - } - - public func _change(opacity to: CGFloat, animated: Bool = true, _ save:Bool = true, removeOnCompletion: Bool = true, duration:Double = 0.2, timingFunction: String = kCAMediaTimingFunctionEaseOut, completion:((Bool)->Void)? = nil) { - if animated { - if let layer = self.layer { - var opacity:CGFloat = CGFloat(layer.opacity) - if let presentation = self.layer?.presentation(), self.layer?.animation(forKey:"opacity") != nil { - opacity = CGFloat(presentation.opacity) - } - - layer.animateAlpha(from: opacity, to: to, duration:duration, timingFunction: timingFunction, removeOnCompletion: removeOnCompletion, completion: completion) - } - - - } else { - layer?.removeAnimation(forKey: "opacity") - } - if save { - self.layer?.opacity = Float(to) - if let completion = completion, !animated { - completion(true) - } - } - } - - public func disableHierarchyInteraction() -> Void { - for sub in self.subviews { - if let sub = sub as? Control { - sub.interactionStateForRestore = sub.userInteractionEnabled - sub.userInteractionEnabled = false - } - sub.disableHierarchyInteraction() - } - } - - public func restoreHierarchyInteraction() -> Void { - for sub in self.subviews { - if let sub = sub as? Control, let resporeState = sub.interactionStateForRestore { - sub.userInteractionEnabled = resporeState - sub.interactionStateForRestore = nil - } - sub.restoreHierarchyInteraction() - } - } - -} - - - - -public extension NSTableView.AnimationOptions { - public static var none: NSTableView.AnimationOptions { get { - return NSTableView.AnimationOptions(rawValue: 0) - } - } - -} - -public extension CGSize { - public func fitted(_ size: CGSize) -> CGSize { - var fittedSize = self - if fittedSize.width > size.width { - fittedSize = CGSize(width: size.width, height: floor((fittedSize.height * size.width / max(fittedSize.width, 1.0)))) - } - if fittedSize.height > size.height { - fittedSize = CGSize(width: floor((fittedSize.width * size.height / max(fittedSize.height, 1.0))), height: size.height) - } - return fittedSize - } - - func fit(_ maxSize: CGSize) -> CGSize { - var size = self - if self.width < 1.0 { - return CGSize() - } - if self.height < 1.0 { - return CGSize() - } - - 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; - } - - public func fittedToArea(_ area: CGFloat) -> CGSize { - if self.height < 1.0 || self.width < 1.0 { - return CGSize() - } - let aspect = self.width / self.height - let height = sqrt(area / aspect) - let width = aspect * height - return CGSize(width: floor(width), height: floor(height)) - } - - public func aspectFilled(_ size: CGSize) -> CGSize { - let scale = max(size.width / max(1.0, self.width), size.height / max(1.0, self.height)) - return CGSize(width: floor(self.width * scale), height: floor(self.height * scale)) - } - - public func aspectFitted(_ size: CGSize) -> CGSize { - let scale = min(size.width / max(1.0, self.width), size.height / max(1.0, self.height)) - return CGSize(width: floor(self.width * scale), height: floor(self.height * scale)) - } - - public func multipliedByScreenScale() -> CGSize { - let scale:CGFloat = 2.0 - return CGSize(width: self.width * scale, height: self.height * scale) - } - - public func dividedByScreenScale() -> CGSize { - let scale:CGFloat = 2.0 - return CGSize(width: self.width / scale, height: self.height / scale) - } -} - -public extension NSImage { - - func precomposed(_ color:NSColor? = nil, flipVertical:Bool = false, flipHorizontal:Bool = false) -> CGImage { - - let drawContext:DrawingContext = DrawingContext(size: self.size, scale: 2.0, clear: true) - - let image:NSImage = self - - let make:(CGContext) -> Void = { ctx in - let rect = NSMakeRect(0, 0, drawContext.size.width, drawContext.size.height) - //ctx.interpolationQuality = .high - - - var imageRect:CGRect = NSMakeRect(0, 0, image.size.width, image.size.height) - - let cimage = image.cgImage(forProposedRect: &imageRect, context: nil, hints: nil) - //CGImageSourceCreateImageAtIndex(CGImageSourceCreateWithData(image.tiffRepresentation! as CFData, nil)!, 0, nil) - - if let color = color { - ctx.clip(to: rect, mask: cimage!) - ctx.setFillColor(color.cgColor) - ctx.fill(rect) - } else { - ctx.draw(cimage!, in: imageRect) - } - - } - - drawContext.withFlippedContext(horizontal: flipHorizontal, vertical: flipVertical, make) - - - - - return drawContext.generateImage()! - -// var image:NSImage = self.copy() as! NSImage -// if let color = color { -// image.lockFocus() -// color.set() -// var imageRect = NSMakeRect(0, 0, image.size.width * 2.0, image.size.height * 2.0) -// NSRectFillUsingOperation(imageRect, NSCompositeSourceAtop) -// image.unlockFocus() -// } - - // return roundImage(image.tiffRepresentation!, self.size, cornerRadius: 0, reversed:reversed)! - } - -} - -public extension CGRect { - public var topLeft: CGPoint { - return self.origin - } - - public var topRight: CGPoint { - return CGPoint(x: self.maxX, y: self.minY) - } - - public var bottomLeft: CGPoint { - return CGPoint(x: self.minX, y: self.maxY) - } - - public var bottomRight: CGPoint { - return CGPoint(x: self.maxX, y: self.maxY) - } - - public var center: CGPoint { - return CGPoint(x: self.midX, y: self.midY) - } -} - -public extension CGPoint { - public func offsetBy(dx: CGFloat, dy: CGFloat) -> CGPoint { - return CGPoint(x: self.x + dx, y: self.y + dy) - } -} - -public extension CGImage { - - var backingSize:NSSize { - return NSMakeSize(CGFloat(width) / 2.0, CGFloat(height) / 2.0) - } - - var size:NSSize { - return NSMakeSize(CGFloat(width), CGFloat(height)) - } - - var scale:CGFloat { - return 2.0 - } - -} - -extension Array { - static func fromCFArray(records : CFArray?) -> Array? { - var result: [Element]? - if let records = records { - for i in 0.. Bool { - return NSLocationInRange(index, self) - } -} - -public extension NSBezierPath { - public var cgPath:CGPath? { - if self.elementCount == 0 { - return nil - } - - let path = CGMutablePath() - var didClosePath = false - - for i in 0 ..< self.elementCount { - var points = [NSPoint](repeating: NSZeroPoint, count: 3) - - switch self.element(at: i, associatedPoints: &points) { - case .moveToBezierPathElement: - path.move(to: points[0]) - case .lineToBezierPathElement: - path.addLine(to: points[0]) - didClosePath = false - case .curveToBezierPathElement: - path.addCurve(to: points[0], control1: points[1], control2: points[2]) - didClosePath = false - case .closePathBezierPathElement: - path.closeSubpath() - didClosePath = true; - } - } - - if !didClosePath { - path.closeSubpath() - } - - return path - } -} - - -public extension NSEdgeInsets { - - public init(left:CGFloat = 0, right:CGFloat = 0, top:CGFloat = 0, bottom:CGFloat = 0) { - self.left = left - self.right = right - self.top = top - self.bottom = bottom - } -} - -public extension NSColor { - public convenience init(_ rgbValue:UInt32, _ alpha:CGFloat = 1.0) { - self.init(deviceRed: ((CGFloat)((rgbValue & 0xFF0000) >> 16))/255.0, green: ((CGFloat)((rgbValue & 0xFF00) >> 8))/255.0, blue: ((CGFloat)(rgbValue & 0xFF))/255.0, alpha: alpha) - } -} - -public extension Int { - - func prettyFormatter(_ n: Int, iteration: Int) -> String { - let keys = ["K", "M", "B", "T"] - let d = Double((n / 100)) / 10.0 - let isRound:Bool = (Int(d) * 10) % 10 == 0 - if d < 1000 { - if d == 1 { - return "\(Int(d))\(keys[iteration])" - } else { - return "\((d > 99.9 || isRound || (!isRound && d > 9.99)) ? d * 10 / 10 : d)\(keys[iteration])" - } - } - else { - return self.prettyFormatter(Int(d), iteration: iteration + 1) - } - } - - public var prettyNumber:String { - if self < 1000 { - return "\(self)" - } - return self.prettyFormatter(self, iteration: 0) - } - public var separatedNumber: String { - if self < 1000 { - return "\(self)" - } - let string = "\(self)" - - let length: Int = string.length - var result:String = "" - var index:Int = 0 - while index < length { - let modulo = length % 3 - if index == 0 && modulo != 0 { - result = string.nsstring.substring(with: NSMakeRange(index, modulo)) - index += modulo - } else { - let count:Int = 3 - let value = string.nsstring.substring(with: NSMakeRange(index, count)) - if index == 0 { - result = value - } else { - result += " " + value - } - index += count - } - } - return result - } -} - - -public extension ProgressIndicator { - public func set(color:NSColor) { - let color = color.usingColorSpace(NSColorSpace.sRGB) - - let colorPoly = CIFilter(name: "CIColorPolynomial") - if let colorPoly = colorPoly, let color = color { - colorPoly.setDefaults() - let redVector = CIVector(x: color.redComponent, y: 0, z: 0, w: 0) - let greenVector = CIVector(x: color.greenComponent, y: 0, z: 0, w: 0) - let blueVector = CIVector(x: color.blueComponent, y: 0, z: 0, w: 0) - - colorPoly.setValue(redVector, forKey: "inputRedCoefficients") - colorPoly.setValue(greenVector, forKey: "inputGreenCoefficients") - colorPoly.setValue(blueVector, forKey: "inputBlueCoefficients") - self.contentFilters = [colorPoly] - } - } -} - -public extension String { - public func prefix(_ by:Int) -> String { - if let index = index(startIndex, offsetBy: by, limitedBy: endIndex) { - return String(self[.. String { - if let index = index(startIndex, offsetBy: by, limitedBy: endIndex) { - return String(self[index.. String { - let h = elapsed / 3600 - let m = (elapsed / 60) % 60 - let s = elapsed % 60 - - if h > 0 { - return String.init(format: "%d:%02d:%02d", h, m, s) - } else { - return String.init(format: "%02d:%02d", m, s) - } - } - - -} - -public extension NSTextField { - public func setSelectionRange(_ range: NSRange) { - textView?.setSelectedRange(range) - } - - public var selectedRange: NSRange { - if let textView = textView { - return textView.selectedRange - } - return NSMakeRange(0, 0) - } - - public func setCursorToEnd() { - self.setSelectionRange(NSRange(location: self.stringValue.length, length: 0)) - } - - public func setCursorToStart() { - self.setSelectionRange(NSRange(location: 0, length: 0)) - } - - public var textView:NSTextView? { - return (self.window?.fieldEditor(true, for: self) as? NSTextView) - } -} - - -public extension String { - public var emojiSkinToneModifiers: [String] { - return [ "🏻", "🏼", "🏽", "🏾", "🏿" ] - } - - public var emojiVisibleLength: Int { - var count = 0 - enumerateSubstrings(in: startIndex..(uncheckedBounds: (self.index(after: self.startIndex), self.endIndex)) - return String(self[range]) - } - - public var canHaveSkinToneModifier: Bool { - if self.characters.isEmpty { - return false - } - - let modified = self.emojiUnmodified + self.emojiSkinToneModifiers[0] - return modified.emojiVisibleLength == 1 - } - - public var glyphCount: Int { - - let richText = NSAttributedString(string: self) - let line = CTLineCreateWithAttributedString(richText) - return CTLineGetGlyphCount(line) - } - - public var isSingleEmoji: Bool { - return glyphCount == 1 && containsEmoji - } - - public var containsEmoji: Bool { - - return !unicodeScalars.filter { $0.isEmoji }.isEmpty - } - - public var containsOnlyEmoji: Bool { - - return unicodeScalars.first(where: { !$0.isEmoji && !$0.isZeroWidthJoiner }) == nil - } - - - public var emojiString: String { - - return emojiScalars.map { String($0) }.reduce("", +) - } - - var emojis: [String] { - - var scalars: [[UnicodeScalar]] = [] - var currentScalarSet: [UnicodeScalar] = [] - var previousScalar: UnicodeScalar? - - for scalar in emojiScalars { - - if let prev = previousScalar, !prev.isZeroWidthJoiner && !scalar.isZeroWidthJoiner { - - scalars.append(currentScalarSet) - currentScalarSet = [] - } - currentScalarSet.append(scalar) - - previousScalar = scalar - } - - scalars.append(currentScalarSet) - - return scalars.map { $0.map{ String($0) } .reduce("", +) } - } - - fileprivate var emojiScalars: [UnicodeScalar] { - - var chars: [UnicodeScalar] = [] - var previous: UnicodeScalar? - for cur in unicodeScalars { - - if let previous = previous, previous.isZeroWidthJoiner && cur.isEmoji { - chars.append(previous) - chars.append(cur) - - } else if cur.isEmoji { - chars.append(cur) - } - - previous = cur - } - - return chars - } - -} - -extension UnicodeScalar { - - var isEmoji: Bool { - - switch value { - case 0x3030, 0x00AE, 0x00A9, - 0x1D000 ... 0x1F77F, - 0x2100 ... 0x27BF, - 0xFE00 ... 0xFE0F, - 0x1F900 ... 0x1F9FF: - return true - - default: return false - } - } - - var isZeroWidthJoiner: Bool { - return value == 8205 - } -} - diff --git a/TGUIKit/TGUIKit/HorizontalRowView.swift b/TGUIKit/TGUIKit/HorizontalRowView.swift deleted file mode 100644 index 530a6545f3..0000000000 --- a/TGUIKit/TGUIKit/HorizontalRowView.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// HorizontalRowView.swift -// TGUIKit -// -// Created by keepcoder on 17/10/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - -open class HorizontalRowView: TableRowView { - - private var container:View = View() - - required public init(frame frameRect: NSRect) { - super.init(frame: frameRect) - self.layer?.delegate = nil - container.layer?.delegate = self - super.addSubview(container) - - container.frame = NSMakeRect(0, 0, frame.height, frame.width) - container.frameCenterRotation = 90 - } - - - - open override func set(item: TableRowItem, animated: Bool) { - super.set(item: item, animated: animated) - container.backgroundColor = backdorColor - } - - open override func draw(_ dirtyRect: NSRect) { - - } - - open override func draw(_ layer: CALayer, in ctx: CGContext) { - super.draw(layer, in: ctx) -// ctx.translateBy(x: frame.width / 2.0, y: frame.height / 2.0) -// ctx.scaleBy(x: -1.0, y: 1.0) -// ctx.rotate(by: 90 * CGFloat(M_PI) / 180) -// ctx.translateBy(x: -frame.width / 2.0, y: -frame.height / 2.0) - } - - open override func addSubview(_ view: NSView) { - container.addSubview(view) - } - - deinit { - container.removeAllSubviews() - } - - open override func setFrameSize(_ newSize: NSSize) { - super.setFrameSize(newSize) - container.setFrameSize(newSize) - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - -} diff --git a/TGUIKit/TGUIKit/HorizontalTableView.swift b/TGUIKit/TGUIKit/HorizontalTableView.swift deleted file mode 100644 index e2e9a51241..0000000000 --- a/TGUIKit/TGUIKit/HorizontalTableView.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// HorizontalTableView.swift -// TGUIKit -// -// Created by keepcoder on 17/10/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - -public class HorizontalTableView: TableView { - - public required init(frame frameRect: NSRect, isFlipped: Bool = true, bottomInset:CGFloat = 0, drawBorder: Bool = false) { - super.init(frame: frameRect, isFlipped: isFlipped, bottomInset: bottomInset, drawBorder: drawBorder) - // [[self.scrollView verticalScroller] setControlSize:NSSmallControlSize]; - //self.verticalScroller?.controlSize = NSControlSize.small - self.rotate(byDegrees: 270) - - self.clipView.border = [] - self.tableView.border = [] - } - - - open override var hasVerticalScroller: Bool { - get { - return false - } - set { - super.hasVerticalScroller = newValue - } - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func rowView(item:TableRowItem) -> TableRowView { - let identifier:String = NSStringFromClass(item.viewClass()) - var view = self.tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: identifier), owner: self.tableView) - if(view == nil) { - let vz = item.viewClass() as! TableRowView.Type - - view = vz.init(frame:NSMakeRect(0, 0, item.height, frame.height)) - - view?.identifier = NSUserInterfaceItemIdentifier(rawValue: identifier) - - } - - return view as! TableRowView; - } - -} diff --git a/TGUIKit/TGUIKit/Image.swift b/TGUIKit/TGUIKit/Image.swift deleted file mode 100644 index 8f45c78a62..0000000000 --- a/TGUIKit/TGUIKit/Image.swift +++ /dev/null @@ -1,70 +0,0 @@ -import Foundation -import ImageIO -import Accelerate - - - - -public func roundImage(_ data:Data, _ s:NSSize, cornerRadius:CGFloat = -1, reversed:Bool = false, scale:CGFloat = 1.0) -> CGImage? { - let image:CGImageSource? = CGImageSourceCreateWithData(data as CFData, nil) - - let size = NSMakeSize(s.width * scale, s.height * scale) - - let context:CGContext? = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: 8, bytesPerRow: Int(4*size.width), space: NSColorSpace.genericRGB.cgColorSpace!, bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue) - - - if let ctx = context { - if let img = image { - let cimage = CGImageSourceCreateImageAtIndex(img, 0, nil) - if let c = cimage { - - if cornerRadius == -1 { - var startAngle: Float = Float(2 * Double.pi) - var endAngle: Float = 0.0 - let radius:Float = Float(size.width/2.0) - let center = NSMakePoint(size.width/2.0, size.height/2.0) - - startAngle = startAngle - Float(Double.pi / 2) - endAngle = endAngle - Float(Double.pi / 2) - ctx.addArc(center: center, radius: CGFloat(radius), startAngle: CGFloat(startAngle), endAngle: CGFloat(endAngle), clockwise: false) - } else if cornerRadius > 0 { - - let minx:CGFloat = 0, midx = size.width/2.0, maxx = size.width - let miny:CGFloat = 0, midy = size.height/2.0, maxy = size.height - - - ctx.move(to: NSMakePoint(minx, midy)) - ctx.addArc(tangent1End: NSMakePoint(minx, miny), tangent2End: NSMakePoint(midx, miny), radius: cornerRadius) - ctx.addArc(tangent1End: NSMakePoint(maxx, miny), tangent2End: NSMakePoint(maxx, midy), radius: cornerRadius) - ctx.addArc(tangent1End: NSMakePoint(maxx, maxy), tangent2End: NSMakePoint(midx, maxy), radius: cornerRadius) - ctx.addArc(tangent1End: NSMakePoint(minx, maxy), tangent2End: NSMakePoint(minx, midy), radius: cornerRadius) - - } - - if cornerRadius > 0 || cornerRadius == -1 { - ctx.closePath() - ctx.clip() - } - - if reversed { - ctx.translateBy(x: size.width/2.0, y: size.height/2.0) - ctx.scaleBy(x: 1.0, y: -1.0) - ctx.translateBy(x: -(size.width/2.0), y: -(size.height/2.0)) - - } - ctx.draw(c, in: NSMakeRect(0, 0, size.width, size.height)) - - - return ctx.makeImage() - - } - - } - } - - return nil -} - - - - diff --git a/TGUIKit/TGUIKit/ImageButton.swift b/TGUIKit/TGUIKit/ImageButton.swift deleted file mode 100644 index 1117c555f6..0000000000 --- a/TGUIKit/TGUIKit/ImageButton.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// ImageButton.swift -// TGUIKit -// -// Created by keepcoder on 26/09/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - -open class ImageButton: Button { - - var imageView:ImageView = ImageView() - - private var images:[ControlState:CGImage] = [:] - private var backgroundImage:[ControlState:CGImage] = [:] - - - public func removeImage(for state:ControlState) { - images.removeValue(forKey: state) - apply(state: self.controlState) - } - - public func set(image:CGImage, for state:ControlState) -> Void { - - images[state] = image - - apply(state: self.controlState) - } - - open override func viewDidChangeBackingProperties() { - super.viewDidChangeBackingProperties() - } - - override func prepare() { - super.prepare() - imageView.animates = true - self.addSubview(imageView) - } - - override public func apply(state: ControlState) { - let state:ControlState = self.isSelected ? .Highlight : state - super.apply(state: state) - - if let image = images[state] { - imageView.image = image - } else if state == .Highlight && autohighlight, let image = images[.Normal] { - imageView.image = style.highlight(image: image) - } else if state == .Hover && highlightHovered, let image = images[.Normal] { - imageView.image = style.highlight(image: image) - } else { - imageView.image = images[.Normal] - } - updateLayout() - } - - public func disableActions() { - animates = false - self.layer?.disableActions() - imageView.animates = false - } - - - override public func sizeToFit(_ addition: NSSize = NSZeroSize, _ maxSize:NSSize = NSZeroSize, thatFit:Bool = false) { - super.sizeToFit(addition) - - if let image = images[.Normal] { - var size = image.backingSize - - if maxSize.width > 0 || maxSize.height > 0 { - size = maxSize - } - - size.width += addition.width - size.height += addition.height - self.setFrameSize(size) - } - } - - public override func updateLayout() { - if let image = images[controlState] { - imageView.setFrameSize(image.backingSize) - } - imageView.center() - } - -} diff --git a/TGUIKit/TGUIKit/ImageView.swift b/TGUIKit/TGUIKit/ImageView.swift deleted file mode 100644 index 797bdeda93..0000000000 --- a/TGUIKit/TGUIKit/ImageView.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// ImageView.swift -// TGUIKit -// -// Created by keepcoder on 22/09/16. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - -public class ImageView: NSView { - - public var animates:Bool = false - - public var image:CGImage? { - didSet { - self.layer?.contents = image - if animates { - animate() - } - } - } - - public func sizeToFit() { - if let image = self.image { - setFrameSize(image.backingSize) - } - } - - override public init(frame frameRect: NSRect) { - super.init(frame: frameRect) - self.wantsLayer = true - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public func animate() -> Void { - let animation = CABasicAnimation(keyPath: "contents") - animation.duration = 0.2 - self.layer?.add(animation, forKey: "contents") - } - - override public func viewDidChangeBackingProperties() { - if let window = self.window { - self.layer?.contentsScale = window.backingScaleFactor/2.0; - } - } - - public func change(pos position: NSPoint, animated: Bool, _ save:Bool = true, removeOnCompletion: Bool = true, duration:Double = 0.2, timingFunction: String = kCAMediaTimingFunctionEaseOut, completion:((Bool)->Void)? = nil) -> Void { - super._change(pos: position, animated: animated, save, removeOnCompletion: removeOnCompletion, duration: duration, timingFunction: timingFunction, completion: completion) - } - - public func change(size: NSSize, animated: Bool, _ save:Bool = true, removeOnCompletion: Bool = true, duration:Double = 0.2, timingFunction: String = kCAMediaTimingFunctionEaseOut, completion:((Bool)->Void)? = nil) { - super._change(size: size, animated: animated, save, removeOnCompletion: removeOnCompletion, duration: duration, timingFunction: timingFunction, completion: completion) - } - public func change(opacity to: CGFloat, animated: Bool = true, _ save:Bool = true, removeOnCompletion: Bool = true, duration:Double = 0.2, timingFunction: String = kCAMediaTimingFunctionEaseOut, completion:((Bool)->Void)? = nil) { - super._change(opacity: to, animated: animated, save, removeOnCompletion: removeOnCompletion, duration: duration, timingFunction: timingFunction, completion: completion) - } - -} diff --git a/TGUIKit/TGUIKit/LinearProgressControl.swift b/TGUIKit/TGUIKit/LinearProgressControl.swift deleted file mode 100644 index 4c98d1a19b..0000000000 --- a/TGUIKit/TGUIKit/LinearProgressControl.swift +++ /dev/null @@ -1,118 +0,0 @@ -// -// LinearProgressControl.swift -// TGUIKit -// -// Created by keepcoder on 28/10/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - -public class LinearProgressControl: Control { - - private var progressView:View! - private var containerView:Control! - private var progress:CGFloat = 0 - public var progressHeight:CGFloat - public var onUserChanged:((Float)->Void)? - - public override func mouseDragged(with event: NSEvent) { - super.mouseDragged(with: event) - if let onUserChanged = onUserChanged { - let location = convert(event.locationInWindow, from: nil) - let progress = Float(location.x / frame.width) - onUserChanged(progress) - } - } - - public var interactiveValue:Float { - if let window = window { - let location = convert(window.mouseLocationOutsideOfEventStream, from: nil) - return Float(location.x / frame.width) - } - return 0 - } - - open override func updateTrackingAreas() { - super.updateTrackingAreas(); - - - if let trackingArea = trackingArea { - self.removeTrackingArea(trackingArea) - } - - trackingArea = nil - - if let _ = window { - let options:NSTrackingArea.Options = [NSTrackingArea.Options.cursorUpdate, NSTrackingArea.Options.mouseEnteredAndExited, NSTrackingArea.Options.mouseMoved, NSTrackingArea.Options.enabledDuringMouseDrag, NSTrackingArea.Options.activeInKeyWindow,NSTrackingArea.Options.inVisibleRect] - self.trackingArea = NSTrackingArea(rect: self.bounds, options: options, owner: self, userInfo: nil) - - self.addTrackingArea(self.trackingArea!) - } - } - - deinit { - if let trackingArea = self.trackingArea { - self.removeTrackingArea(trackingArea) - } - } - - public override var style: ControlStyle { - set { - self.progressView.layer?.backgroundColor = newValue.foregroundColor.cgColor - containerView.style = newValue - } - get { - return super.style - } - } - - public func set(progress:CGFloat, animated:Bool = false) { - let progress:CGFloat = progress.isNaN ? 1 : progress - self.progress = progress - let size = NSMakeSize(floorToScreenPixels(frame.width * progress), progressHeight) - progressView.change(size: size, animated: animated) - progressView.setFrameOrigin(NSMakePoint(0, frame.height - progressHeight)) - } - - - - - public init(progressHeight:CGFloat = 4) { - self.progressHeight = progressHeight - super.init() - - initialize() - } - - public override func layout() { - super.layout() - progressView.setFrameSize(progressView.frame.width, progressHeight) - containerView.setFrameOrigin(0, frame.height - containerView.frame.height) - } - - - private func initialize() { - - containerView = Control(frame:NSMakeRect(0, 0, 0, progressHeight)) - containerView.wantsLayer = true - containerView.layer?.backgroundColor = style.foregroundColor.cgColor - addSubview(containerView) - - - progressView = View(frame:NSMakeRect(0, 0, 0, progressHeight)) - progressView.backgroundColor = style.foregroundColor - addSubview(progressView) - } - - required public init(frame frameRect: NSRect) { - self.progressHeight = frameRect.height - super.init(frame:frameRect) - initialize() - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - -} diff --git a/TGUIKit/TGUIKit/MagnifyView.swift b/TGUIKit/TGUIKit/MagnifyView.swift deleted file mode 100644 index 76fa254320..0000000000 --- a/TGUIKit/TGUIKit/MagnifyView.swift +++ /dev/null @@ -1,227 +0,0 @@ -// -// MagnifyView.swift -// TGUIKit -// -// Created by keepcoder on 15/12/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import SwiftSignalKitMac -public class MagnifyView : NSView { - - public private(set) var magnify:CGFloat = 1.0 { - didSet { - magnifyUpdater.set(magnify) - } - } - public var maxMagnify:CGFloat = 8.0 - public var minMagnify:CGFloat = 1.0 - public let smartUpdater:Promise = Promise() - public let magnifyUpdater:ValuePromise = ValuePromise(ignoreRepeated: true) - private var mov_start:NSPoint = NSZeroPoint - private var mov_content_start:NSPoint = NSZeroPoint - - - public let contentView:NSView - let containerView:NSView = NSView() - public var contentSize:NSSize = NSZeroSize { - didSet { - contentView.frame = focus(magnifiedSize) - } - } - private var magnifiedSize:NSSize { - return NSMakeSize(floorToScreenPixels(contentSize.width * magnify), floorToScreenPixels(contentSize.height * magnify)) - } - public init(_ contentView:NSView, contentSize:NSSize) { - self.contentView = contentView - contentView.setFrameSize(contentSize) - self.contentSize = contentSize - contentView.wantsLayer = true - super.init(frame: NSZeroRect) - wantsLayer = true - containerView.wantsLayer = true - addSubview(containerView) - containerView.addSubview(contentView) - contentView.background = NSColor.clear - smartUpdater.set(.single(contentSize)) - } - - public func resetMagnify() { - magnify = 1.0 - contentView.setFrameSize(magnifiedSize) - contentView.center() - } - - public func zoomIn() { - add(magnify: 0.5, for: NSMakePoint(containerView.frame.width/2, containerView.frame.height/2), animated: true) - } - - public func zoomOut() { - add(magnify: -0.5, for: NSMakePoint(containerView.frame.width/2, containerView.frame.height/2), animated: true) - } - - public override func layout() { - super.layout() - containerView.setFrameSize(frame.size) - contentView.center() - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override public func magnify(with event: NSEvent) { - super.magnify(with: event) - - add(magnify: event.magnification, for: containerView.convert(event.locationInWindow, from: nil)) - - if event.phase == .ended { - smartUpdater.set(.single(magnifiedSize) |> delay(0.3, queue: Queue.mainQueue())) - } else if event.phase == .began { - smartUpdater.set(smartUpdater.get()) - } - } - - override public func smartMagnify(with event: NSEvent) { - super.smartMagnify(with: event) - addSmart(for: containerView.convert(event.locationInWindow, from: nil)) - smartUpdater.set(.single(magnifiedSize) |> delay(0.2, queue: Queue.mainQueue())) - } - - func addSmart(for location:NSPoint) { - var minFactor:CGFloat = min(max(frame.size.width / magnifiedSize.width,frame.size.height / magnifiedSize.height),2.0) - if magnify > 1.0 { - minFactor = 1 - magnify - } - add(magnify: minFactor, for: location, animated: true) - } - - public func add(magnify:CGFloat, for location:NSPoint, animated:Bool = false) { - self.magnify += magnify - self.magnify = min(max(minMagnify,self.magnify),maxMagnify) - let point = magnifyOrigin( for: location, from:contentView.frame, factor: magnify) - - //contentView.change(pos: point, animated: animated) - // contentView.change(size: magnifiedSize, animated: animated) - let content = animated ? contentView.animator() : contentView - content.frame = NSMakeRect(point.x, point.y, magnifiedSize.width, magnifiedSize.height) - } - - func magnifyOrigin(for location:NSPoint, from past:NSRect, factor:CGFloat) -> NSPoint { - - var point:NSPoint = past.origin - let focused = focus(magnifiedSize).origin - if NSPointInRect(location, contentView.frame) { - if magnifiedSize.width < frame.width || magnifiedSize.height < frame.height { - point = focused - } else { - point.x -= (magnifiedSize.width - past.width) * ((location.x - past.minX) / past.width) - point.y -= (magnifiedSize.height - past.height) * ((location.y - past.minY) / past.height) - - point = adjust(with: point) - - } - } else { - point = focused - } - return point - } - - override public func mouseDown(with theEvent: NSEvent) { - self.mov_start = convert(theEvent.locationInWindow, from: nil) - self.mov_content_start = contentView.frame.origin - } - - override public func mouseUp(with theEvent: NSEvent) { - self.mov_start = NSZeroPoint - self.mov_content_start = NSZeroPoint - super.mouseUp(with: theEvent) - } - - override public func mouseDragged(with theEvent: NSEvent) { - super.mouseDragged(with: theEvent) - if (mov_start.x == 0 || mov_start.y == 0) || (frame.width > magnifiedSize.width && frame.height > magnifiedSize.height) { - return - } - var current = convert(theEvent.locationInWindow, from: nil) - current = NSMakePoint(current.x - mov_start.x, current.y - mov_start.y) - - let adjust = self.adjust(with: NSMakePoint(mov_content_start.x + current.x, mov_content_start.y + current.y)) - - var point = contentView.frame.origin - if magnifiedSize.width > frame.width { - point.x = adjust.x - } - if magnifiedSize.height > frame.height { - point.y = adjust.y - } - - contentView.setFrameOrigin(point) - - - } - - private func adjust(with point:NSPoint) -> NSPoint { - var point = point - point.x = floorToScreenPixels(max(min(0, point.x), point.x + (frame.width - (point.x + magnifiedSize.width)))) - point.y = floorToScreenPixels(max(min(0, point.y), point.y + (frame.height - (point.y + magnifiedSize.height)))) - return point - } - - override public func scrollWheel(with event: NSEvent) { - - if magnify == minMagnify { - super.scrollWheel(with: event) - return - } - - if event.type == .smartMagnify || event.type == .magnify || (event.scrollingDeltaY == 0 && event.scrollingDeltaX == 0) { - return - } - - - let content_f = contentView.frame.origin - if (content_f.x == 0 && event.scrollingDeltaX > 0) || (content_f.x == (frame.width - magnifiedSize.width) && event.scrollingDeltaX < 0) { - // super.scrollWheel(with: event) - return - } - - if (content_f.y == 0 && event.scrollingDeltaY < 0) || (content_f.y == (frame.height - magnifiedSize.height) && event.scrollingDeltaY > 0) { - // super.scrollWheel(with: event) - return - } - - var point = content_f - let adjust = self.adjust(with: NSMakePoint(content_f.x + event.scrollingDeltaX, content_f.y + -event.scrollingDeltaY)) - if event.scrollingDeltaX != 0 && magnifiedSize.width > frame.width { - point.x = adjust.x - } - if event.scrollingDeltaY != 0 && magnifiedSize.height > frame.height { - point.y = adjust.y - } - if point.equalTo(content_f) { - // super.scrollWheel(with: event) - return - } - - contentView.setFrameOrigin(point) - } - - deinit { - var bp:Int = 0 - bp += 1 - } - - - public var mouseInContent:Bool { - if let window = window { - let point = window.mouseLocationOutsideOfEventStream - return NSPointInRect(convert(point, from: nil), contentView.frame) - } - return false - } - - -} - diff --git a/TGUIKit/TGUIKit/MajorNavigationController.swift b/TGUIKit/TGUIKit/MajorNavigationController.swift deleted file mode 100644 index 98611853a1..0000000000 --- a/TGUIKit/TGUIKit/MajorNavigationController.swift +++ /dev/null @@ -1,318 +0,0 @@ -// -// SingleChatNavigationController.swift -// Telegram-Mac -// -// Created by keepcoder on 13/10/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import SwiftSignalKitMac - - - -public protocol MajorControllerListener : class { - func navigationWillShowMajorController(_ controller:ViewController); -} - -open class MajorNavigationController: NavigationViewController, SplitViewDelegate { - - public var alwaysAnimate: Bool = false - private var majorClass:AnyClass - private var defaultEmpty:ViewController - private var listeners:[WeakReference] = [] - - private let container:GenericViewController = GenericViewController() - - override var containerView:View { - get { - return container.genericView - } - set { - super.containerView = newValue - } - } - - - - open override func loadView() { - super.loadView() - - genericView.setProportion(proportion: SplitProportion(min:380, max: .greatestFiniteMagnitude), state: .single) - - controller._frameRect = bounds - controller.viewWillAppear(false) - controller.navigationController = self - - containerView.addSubview(navigationBar) - containerView.frame = bounds - navigationBar.frame = NSMakeRect(0, 0, containerView.frame.width, controller.bar.height) - controller.view.frame = NSMakeRect(0, controller.bar.height , containerView.frame.width, containerView.frame.height - controller.bar.height) - - navigationBar.switchViews(left: controller.leftBarView, center: controller.centerBarView, right: controller.rightBarView, controller: controller, style: .none, animationStyle: controller.animationStyle) - - containerView.addSubview(controller.view) - Queue.mainQueue().justDispatch { - self.controller.viewDidAppear(false) - } - - } - - public func closeSidebar() { - genericView.removeProportion(state: .dual) - genericView.setProportion(proportion: SplitProportion(min:380, max: .greatestFiniteMagnitude), state: .single) - genericView.layout() - } - - public init(_ majorClass:AnyClass, _ empty:ViewController) { - self.majorClass = majorClass - self.defaultEmpty = empty - container.bar = .init(height: 0) - assert(majorClass is ViewController.Type) - - super.init(empty) - } - - open override func currentControllerDidChange() { - if let view = view as? DraggingView { - view.controller = controller - } - for listener in listeners { - listener.value?.navigationWillChangeController() - } - } - - open override func viewDidLoad() { - //super.viewDidLoad() - - genericView.delegate = self - genericView.update() - - } - - public func splitViewDidNeedSwapToLayout(state: SplitViewState) { - genericView.removeAllControllers(); - - switch state { - case .dual: - genericView.addController(controller: container, proportion: SplitProportion(min: 800, max: .greatestFiniteMagnitude)) - if let sidebar = sidebar { - genericView.addController(controller: sidebar, proportion: SplitProportion(min:350, max: 350)) - } - case .single: - genericView.addController(controller: container, proportion: SplitProportion(min: 800, max: .greatestFiniteMagnitude)) - default: - break - } - } - - public func splitViewDidNeedMinimisize(controller: ViewController) { - - } - - public func splitViewDidNeedFullsize(controller: ViewController) { - - } - - public func splitViewIsCanMinimisize() -> Bool { - return false; - } - - public func splitViewDrawBorder() -> Bool { - return true - } - - open override func viewClass() ->AnyClass { - return DraggingView.self - } - - public var genericView:SplitView { - return view as! SplitView - } - - override open func push(_ controller: ViewController, _ animated: Bool, style:ViewControllerStyle? = nil) { - - assertOnMainThread() - - controller.navigationController = self - controller.loadViewIfNeeded(self.container.bounds) - - genericView.update() - - - pushDisposable.set((controller.ready.get() |> deliverOnMainQueue |> take(1)).start(next: {[weak self] _ in - if let strongSelf = self { - strongSelf.lock = true - let isMajorController = controller.className == NSStringFromClass(strongSelf.majorClass) - let removeAnimateFlag = strongSelf.stackCount == 2 && isMajorController && !strongSelf.alwaysAnimate - - if isMajorController { - for controller in strongSelf.stack { - controller.didRemovedFromStack() - } - strongSelf.stack.removeAll() - - strongSelf.stack.append(strongSelf.empty) - } - - if let index = strongSelf.stack.index(of: controller) { - strongSelf.stack.remove(at: index) - } - - strongSelf.stack.append(controller) - - let anim = animated && (!isMajorController || strongSelf.controller != strongSelf.defaultEmpty) && !removeAnimateFlag - - let newStyle:ViewControllerStyle - if let style = style { - newStyle = style - } else { - newStyle = anim ? .push : .none - } - - - strongSelf.show(controller, newStyle) - - - } - })) - } - - open override func back(animated:Bool = true) -> Void { - if stackCount > 1 && !isLocked, let last = stack.last, last.invokeNavigationBack() { - let ncontroller = stack[stackCount - 2] - let removeAnimateFlag = (ncontroller == defaultEmpty || !animated) && !alwaysAnimate - last.didRemovedFromStack() - stack.removeLast() - - show(ncontroller, removeAnimateFlag ? .none : .pop) - } - } - - open override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - self.window?.set(handler: { [weak self] in - if let strongSelf = self { - return strongSelf.escapeKeyAction() - } - return .rejected - }, with: self, for: .Escape, priority:.medium) - - self.window?.set(handler: { [weak self] in - if let strongSelf = self { - return strongSelf.returnKeyAction() - } - return .rejected - }, with: self, for: .Return, priority:.medium) - - - self.window?.set(handler: { [weak self] in - if let strongSelf = self { - return strongSelf.backKeyAction() - } - return .rejected - }, with: self, for: .LeftArrow, priority:.medium) - - self.window?.set(handler: { [weak self] in - if let strongSelf = self { - return strongSelf.nextKeyAction() - } - return .rejected - }, with: self, for: .RightArrow, priority:.medium) - - self.window?.add(swipe: { [weak self] direction -> KeyHandlerResult in - if let strongSelf = self, let window = strongSelf.window, !hasPopover(window) && !hasModals() && !strongSelf.isLocked { - switch direction { - case .left: - return strongSelf.backKeyAction() - case .right: - return strongSelf.nextKeyAction() - case .none: - var nextResult = strongSelf.nextKeyAction() - if nextResult != .rejected { - nextResult = strongSelf.backKeyAction() - } - return nextResult - } - } - - return .invokeNext - }, with: self) - - - } - - - - open override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - self.window?.removeAllHandlers(for: self) - } - - open override func backKeyAction() -> KeyHandlerResult { - let status:KeyHandlerResult = stackCount > 1 ? .invoked : .rejected - - let cInvoke = self.controller.backKeyAction() - - if cInvoke == .invokeNext { - return .invokeNext - } else if cInvoke == .invoked { - return .invoked - } - self.back() - return status - } - - open override func nextKeyAction() -> KeyHandlerResult { - return self.controller.nextKeyAction() - } - - - open override func escapeKeyAction() -> KeyHandlerResult { - let status:KeyHandlerResult = stackCount > 1 ? .invoked : .rejected - - let cInvoke = self.controller.escapeKeyAction() - - if cInvoke == .invokeNext { - return .invokeNext - } else if cInvoke == .invoked { - return .invoked - } - self.back() - return status - } - - open override func returnKeyAction() -> KeyHandlerResult { - let status:KeyHandlerResult = .rejected - - let cInvoke = self.controller.returnKeyAction() - - if cInvoke == .invokeNext { - return .invokeNext - } else if cInvoke == .invoked { - return .invoked - } - return status - } - - public func add(listener:WeakReference) -> Void { - let index = listeners.index(where: { (weakView) -> Bool in - return listener.value == weakView.value - }) - if index == nil { - listeners.append(listener) - } - } - - public func remove(listener:WeakReference) -> Void { - - let index = listeners.index(where: { (weakView) -> Bool in - return listener.value == weakView.value - }) - - if let index = index { - listeners.remove(at: index) - } - } - -} diff --git a/TGUIKit/TGUIKit/Modal.swift b/TGUIKit/TGUIKit/Modal.swift deleted file mode 100644 index d6be79c6ca..0000000000 --- a/TGUIKit/TGUIKit/Modal.swift +++ /dev/null @@ -1,430 +0,0 @@ -// -// Modal.swift -// TGUIKit -// -// Created by keepcoder on 26/09/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import SwiftSignalKitMac - - -private class ModalBackground : Control { - fileprivate override func scrollWheel(with event: NSEvent) { - - } -} - -private var activeModals:[WeakReference] = [] - -public class ModalInteractions { - let accept:(()->Void)? - let cancel:(()->Void)? - let acceptTitle:String - let cancelTitle:String? - let drawBorder:Bool - let height:CGFloat - var enables:((Bool)->Void)? = nil - - - var doneUpdatable:(((TitleButton)->Void)->Void)? = nil - var cancelUpdatable:(((TitleButton)->Void)->Void)? = nil - - public init(acceptTitle:String, accept:(()->Void)? = nil, cancelTitle:String? = nil, cancel:(()->Void)? = nil, drawBorder:Bool = false, height:CGFloat = 50) { - self.drawBorder = drawBorder - self.accept = accept - self.cancel = cancel - self.acceptTitle = acceptTitle - self.cancelTitle = cancelTitle - self.height = height - } - - public func updateEnables(_ enable:Bool) -> Void { - if let enables = enables { - enables(enable) - } - } - - public func updateDone(_ f:@escaping (TitleButton) -> Void) -> Void { - doneUpdatable?(f) - } - public func updateCancel(_ f:@escaping(TitleButton) -> Void) -> Void { - cancelUpdatable?(f) - } - -} - -private class ModalInteractionsContainer : View { - let acceptView:TitleButton - let cancelView:TitleButton? - let interactions:ModalInteractions - let borderView:View? - - override func mouseUp(with event: NSEvent) { - - } - override func mouseDown(with event: NSEvent) { - - } - - init(interactions:ModalInteractions, modal:Modal) { - self.interactions = interactions - acceptView = TitleButton() - acceptView.style = ControlStyle(font:.medium(.text), foregroundColor: presentation.colors.blueUI, backgroundColor: presentation.colors.background) - acceptView.set(text: interactions.acceptTitle, for: .Normal) - acceptView.disableActions() - acceptView.sizeToFit() - if let cancelTitle = interactions.cancelTitle { - cancelView = TitleButton() - cancelView?.style = ControlStyle(font:.medium(.text), foregroundColor: presentation.colors.blueUI, backgroundColor: presentation.colors.background) - cancelView?.set(text: cancelTitle, for: .Normal) - cancelView?.sizeToFit() - - } else { - cancelView = nil - } - - if interactions.drawBorder { - borderView = View() - borderView?.backgroundColor = presentation.colors.border - } else { - borderView = nil - } - - - - super.init() - self.backgroundColor = presentation.colors.background - if let cancel = interactions.cancel { - cancelView?.set(handler: { _ in - cancel() - }, for: .Click) - } else { - cancelView?.set(handler: { [weak modal] _ in - modal?.close() - }, for: .Click) - } - - if let accept = interactions.accept { - acceptView.set(handler: { _ in - accept() - }, for: .Click) - } else { - acceptView.set(handler: { [weak modal] _ in - modal?.close() - }, for: .Click) - - } - - addSubview(acceptView) - if let cancelView = cancelView { - addSubview(cancelView) - } - if let borderView = borderView { - addSubview(borderView) - } - - interactions.enables = { [weak self] enable in - self?.acceptView.isEnabled = enable - self?.acceptView.apply(state: .Normal) - } - - interactions.doneUpdatable = { [weak self] f in - if let strongSelf = self { - f(strongSelf.acceptView) - } - self?.updateDone() - } - interactions.cancelUpdatable = { [weak self] f in - if let strongSelf = self, let cancelView = strongSelf.cancelView { - f(cancelView) - } - self?.updateCancel() - } - - - } - - public func updateDone() { - acceptView.sizeToFit() - needsLayout = true - } - - public func updateCancel() { - cancelView?.sizeToFit() - needsLayout = true - } - public func updateThrid(_ text:String) { - acceptView.set(text: text, for: .Normal) - acceptView.sizeToFit() - - needsLayout = true - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - required public init(frame frameRect: NSRect) { - fatalError("init(frame:) has not been implemented") - } - - fileprivate override func layout() { - super.layout() - - acceptView.centerY(x:frame.width - acceptView.frame.width - 30) - if let cancelView = cancelView { - cancelView.centerY(x:acceptView.frame.minX - cancelView.frame.width - 30) - } - borderView?.frame = NSMakeRect(0, 0, frame.width, .borderSize) - } - - - -} - -private class ModalContainerView: View { - - deinit { - var bp:Int = 0 - bp += 1 - } - - - - fileprivate override func mouseDown(with event: NSEvent) { - - } - - fileprivate override func mouseUp(with event: NSEvent) { - - } -} - -public class Modal: NSObject { - - private var background:ModalBackground - fileprivate var controller:ModalViewController? - private var container:ModalContainerView! - let window:Window - private let disposable:MetaDisposable = MetaDisposable() - private var interactionsView:ModalInteractionsContainer? - public let interactions:ModalInteractions? - fileprivate let animated: Bool - private let isOverlay: Bool - public init(controller:ModalViewController, for window:Window, animated: Bool = true, isOverlay: Bool) { - - self.controller = controller - self.window = window - self.animated = animated - self.isOverlay = isOverlay - background = ModalBackground() - background.backgroundColor = controller.background - background.layer?.disableActions() - self.interactions = controller.modalInteractions - super.init() - - if let interactions = interactions { - interactionsView = ModalInteractionsContainer(interactions: interactions, modal:self) - interactionsView?.frame = NSMakeRect(0, controller.bounds.height, controller.bounds.width, interactions.height) - } - - if controller.isFullScreen { - controller._frameRect = window.contentView!.bounds - } - - container = ModalContainerView(frame: containerRect) - container.layer?.cornerRadius = .cornerRadius - container.layer?.shouldRasterize = true - container.layer?.rasterizationScale = CGFloat(System.backingScale) - container.backgroundColor = controller.containerBackground - - container.addSubview(controller.view) - - if let interactionsView = interactionsView { - container.addSubview(interactionsView) - } - - background.addSubview(container) - - background.userInteractionEnabled = controller.handleEvents - - if controller.handleEvents { - window.set(responder: { [weak controller] () -> NSResponder? in - return controller?.firstResponder() - }, with: self, priority: .modal) - - if controller.handleAllEvents { - window.set(handler: { () -> KeyHandlerResult in - return .invokeNext - }, with: self, for: .All, priority: .modal) - } - - - window.set(escape: {[weak self] () -> KeyHandlerResult in - if self?.controller?.escapeKeyAction() == .rejected { - self?.close() - } - return .invoked - }, with: self, priority: .modal) - - window.set(handler: { [weak self] () -> KeyHandlerResult in - if let controller = self?.controller { - return controller.returnKeyAction() - } - return .invokeNext - }, with: self, for: .Return, priority: .modal) - } - - - - background.set(handler: { [weak self] _ in - if let closable = self?.controller?.closable, closable { - self?.close() - } - }, for: .Click) - - if controller.dynamicSize { - background.customHandler.size = { [weak self] (size) in - self?.controller?.measure(size: size) - } - } - activeModals.append(WeakReference(value: self)) - } - - - public func resize(with size:NSSize, animated:Bool = true) { - - let focus:NSRect - if let interactions = controller?.modalInteractions { - focus = background.focus(NSMakeSize(size.width, size.height + interactions.height)) - interactionsView?.change(pos: NSMakePoint(0, size.height), animated: animated) - } else { - focus = background.focus(size) - } - container.change(size: focus.size, animated: animated) - container.change(pos: focus.origin, animated: animated) - - controller?.view._change(size: size, animated: animated) - } - - private var containerRect:NSRect { - if let controller = controller { - var containerRect = controller.bounds - if let interactions = controller.modalInteractions { - containerRect.size.height += interactions.height - } - return containerRect - } - return NSZeroRect - } - - public func close(_ callAcceptInteraction:Bool = false) ->Void { - window.removeAllHandlers(for: self) - controller?.viewWillDisappear(true) - - if callAcceptInteraction, let interactionsView = interactionsView { - interactionsView.interactions.accept?() - } - - background.layer?.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: {[weak self] (complete) in - if let stongSelf = self { - stongSelf.background.removeFromSuperview() - stongSelf.controller?.viewDidDisappear(true) - stongSelf.controller?.modal = nil - stongSelf.controller = nil - } - }) - - } - - deinit { - disposable.dispose() - for i in stride(from: activeModals.count - 1, to: -1, by: -1) { - if activeModals[i].value == self { - activeModals.remove(at: i) - break - } - } - - } - - func show() -> Void { - // if let view - if let controller = controller { - disposable.set((controller.ready.get() |> take(1)).start(next: { [weak self, weak controller] ready in - if let strongSelf = self, let view = (strongSelf.isOverlay ? strongSelf.window.contentView?.superview : strongSelf.window.contentView), let controller = controller { - strongSelf.controller?.viewWillAppear(true) - strongSelf.background.frame = view.bounds - strongSelf.container.center() - strongSelf.background.background = controller.isFullScreen ? controller.containerBackground : controller.background - if strongSelf.animated { - if !controller.isFullScreen { - strongSelf.container.layer?.animateScaleSpring(from: 0.1, to: 1.0, duration: 0.3) - } else { - strongSelf.container.layer?.animateAlpha(from: 0.1, to: 1.0, duration: 0.3) - } - } - - strongSelf.background.autoresizingMask = [.width,.height] - strongSelf.background.customHandler.layout = { [weak strongSelf] view in - strongSelf?.container.center() - } - - if controller.isFullScreen { - strongSelf.background.customHandler.size = { [weak strongSelf] size in - strongSelf?.container.setFrameSize(size) - } - } - - view.addSubview(strongSelf.background) - if let value = strongSelf.controller?.becomeFirstResponder(), value { - strongSelf.window.makeFirstResponder(strongSelf.controller?.firstResponder()) - } - - if strongSelf.animated { - strongSelf.background.layer?.animateAlpha(from: 0, to: 1, duration: 0.2, completion:{[weak strongSelf] (completed) in - strongSelf?.controller?.viewDidAppear(true) - }) - } else { - strongSelf.controller?.viewDidAppear(false) - } - } - })) - } - - } - -} - -public func hasModals() -> Bool { - - for i in stride(from: activeModals.count - 1, to: -1, by: -1) { - if activeModals[i].value == nil { - activeModals.remove(at: i) - } - } - - return !activeModals.isEmpty -} - -public func closeAllModals() { - for modal in activeModals { - modal.value?.close() - } -} - -public func showModal(with controller:ModalViewController, for window:Window, isOverlay: Bool = false) -> Void { - assert(controller.modal == nil) - for weakModal in activeModals { - if weakModal.value?.controller?.className == controller.className { - weakModal.value?.close() - } - } - - controller.modal = Modal(controller: controller, for: window, isOverlay: isOverlay) - controller.modal?.show() -} - - diff --git a/TGUIKit/TGUIKit/NavigationBarView.swift b/TGUIKit/TGUIKit/NavigationBarView.swift deleted file mode 100644 index ff57ec0683..0000000000 --- a/TGUIKit/TGUIKit/NavigationBarView.swift +++ /dev/null @@ -1,227 +0,0 @@ -// -// NavigationBarView.swift -// TGUIKit -// -// Created by keepcoder on 15/09/16. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - -public struct NavigationBarStyle { - public let height:CGFloat - public let enableBorder:Bool - public init(height:CGFloat, enableBorder:Bool = true) { - self.height = height - self.enableBorder = enableBorder - } -} - -class NavigationBarView: View { - - private var bottomBorder:View = View() - - private var leftView:View = View() - private var centerView:View = View() - private var rightView:View = View() - - override init() { - super.init() - self.autoresizingMask = [.width] - updateLocalizationAndTheme() - } - - required init(frame frameRect: NSRect) { - super.init(frame: frameRect) - self.autoresizingMask = [.width] - updateLocalizationAndTheme() - } - - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - bottomBorder.backgroundColor = presentation.colors.border - backgroundColor = presentation.colors.background - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public override func draw(_ layer: CALayer, in ctx: CGContext) { - - super.draw(layer, in: ctx) -// ctx.setFillColor(NSColor.white.cgColor) -// ctx.fill(self.bounds) -// -// ctx.setFillColor(theme.colors.border.cgColor) -// ctx.fill(NSMakeRect(0, NSHeight(self.frame) - .borderSize, NSWidth(self.frame), .borderSize)) - } - - override func layout() { - super.layout() - self.bottomBorder.setNeedsDisplay() - } - - override func setFrameSize(_ newSize: NSSize) { - super.setFrameSize(newSize) - self.bottomBorder.frame = NSMakeRect(0, newSize.height - .borderSize, newSize.width, .borderSize) - self.layout(left: leftView, center: centerView, right: rightView) - } - - - func layout(left: View, center: View, right: View) -> Void { - if frame.height > 0 { - left.frame = NSMakeRect(0, 0, NSWidth(left.frame), frame.height - .borderSize); - center.frame = NSMakeRect(left.frame.maxX, 0, frame.width - (left.frame.width + right.frame.width), frame.height - .borderSize); - right.frame = NSMakeRect(center.frame.maxX, 0, NSWidth(right.frame), frame.height - .borderSize); - } - } - - // ! PUSH ! - // left from center - // right cross fade - // center from right - - // ! POP ! - // old left -> new center - // old center -> right - // old right -> fade - - - - public func switchViews(left:BarView, center:BarView, right:BarView, controller:ViewController, style:ViewControllerStyle, animationStyle:AnimationStyle) { - - layout(left: left, center: center, right: right) - self.bottomBorder.isHidden = !controller.bar.enableBorder - if style != .none { - - CATransaction.begin() - - self.addSubview(left) - self.addSubview(center) - self.addSubview(right) - self.addSubview(bottomBorder) - - left.setNeedsDisplay() - center.setNeedsDisplay() - right.setNeedsDisplay() - - let pLeft = self.leftView - let pCenter = self.centerView - let pRight = self.rightView - - self.leftView = left - self.centerView = center - self.rightView = right - - var pLeft_from:CGFloat = 0,pRight_from:CGFloat = 0, pCenter_from:CGFloat = 0, pLeft_to:CGFloat = 0, pRight_to:CGFloat = 0, pCenter_to:CGFloat = 0 - var nLeft_from:CGFloat = 0, nRight_from:CGFloat = 0, nCenter_from:CGFloat = 0, nLeft_to:CGFloat = 0, nRight_to:CGFloat = 0, nCenter_to:CGFloat = 0 - - switch style { - case .push: - - //left - pLeft_from = 0 - pLeft_to = 0 - nLeft_from = round(NSWidth(self.frame) - NSWidth(left.frame))/2.0 - nLeft_to = 0 - - //center - pCenter_from = NSMinX(pCenter.frame) - pCenter_to = 0 - nCenter_from = NSMinX(right.frame) - nCenter_to = NSMaxX(left.frame) - - //right - pRight_from = NSMinX(right.frame) - pRight_to = NSMinX(right.frame) - nRight_from = NSMinX(right.frame) - nRight_to = NSMinX(right.frame) - - break - case .pop: - - //left - pLeft_from = 0 - pLeft_to = 0 - nLeft_from = 0 - nLeft_to = 0 - - //center - pCenter_from = NSMinX(center.frame) - pCenter_to = NSMinX(right.frame) - nCenter_from = 0 - nCenter_to = NSMaxX(left.frame) - - //right - pRight_from = NSMinX(right.frame) - pRight_to = NSMinX(right.frame) - nRight_from = NSMinX(right.frame) - nRight_to = NSMinX(right.frame) - - - break - case .none: - break - } - - - // old - pLeft.layer?.animate(from: 1.0 as NSNumber, to: 0.0 as NSNumber, keyPath: "opacity", timingFunction: animationStyle.function, duration: animationStyle.duration, removeOnCompletion: false, completion:{ [weak pLeft] (completed) in - if completed { - pLeft?.removeFromSuperview() - } - }) - pLeft.layer?.animate(from: pLeft_from as NSNumber, to: pLeft_to as NSNumber, keyPath: "position.x", timingFunction: kCAMediaTimingFunctionSpring, duration: animationStyle.duration) - - pCenter.layer?.animate(from: 1.0 as NSNumber, to: 0.0 as NSNumber, keyPath: "opacity", timingFunction: kCAMediaTimingFunctionSpring, duration: animationStyle.duration, removeOnCompletion: false, completion:{ [weak pCenter] (completed) in - if completed { - pCenter?.removeFromSuperview() - } - }) - pCenter.layer?.animate(from: pCenter_from as NSNumber, to: pCenter_to as NSNumber, keyPath: "position.x", timingFunction: animationStyle.function, duration: animationStyle.duration) - - pRight.layer?.animate(from: 1.0 as NSNumber, to: 0.0 as NSNumber, keyPath: "opacity", timingFunction: kCAMediaTimingFunctionSpring, duration: animationStyle.duration, removeOnCompletion: false, completion:{ [weak pRight] (completed) in - if completed { - pRight?.removeFromSuperview() - } - }) - pRight.layer?.animate(from: pRight_from as NSNumber, to: pRight_to as NSNumber, keyPath: "position.x", timingFunction: animationStyle.function, duration: animationStyle.duration) - - // new - if !left.isHidden { - left.layer?.animate(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "opacity", timingFunction: kCAMediaTimingFunctionSpring, duration: animationStyle.duration) - } - left.layer?.animate(from: nLeft_from as NSNumber, to: nLeft_to as NSNumber, keyPath: "position.x", timingFunction: animationStyle.function, duration: animationStyle.duration) - if !center.isHidden { - center.layer?.animate(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "opacity", timingFunction: kCAMediaTimingFunctionSpring, duration: animationStyle.duration) - } - center.layer?.animate(from: nCenter_from as NSNumber, to: nCenter_to as NSNumber, keyPath: "position.x", timingFunction: animationStyle.function, duration: animationStyle.duration) - - if !right.isHidden { - right.layer?.animate(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "opacity", timingFunction: kCAMediaTimingFunctionSpring, duration: animationStyle.duration) - } - right.layer?.animate(from: nRight_from as NSNumber, to: nRight_to as NSNumber, keyPath: "position.x", timingFunction: animationStyle.function, duration: animationStyle.duration) - - - - CATransaction.commit() - - } else { - self.removeAllSubviews() - self.addSubview(left) - self.addSubview(center) - self.addSubview(right) - - self.leftView = left - self.centerView = center - self.rightView = right - - self.addSubview(bottomBorder) - } - - - - } - -} diff --git a/TGUIKit/TGUIKit/NavigationViewController.swift b/TGUIKit/TGUIKit/NavigationViewController.swift deleted file mode 100644 index dbcf2aea41..0000000000 --- a/TGUIKit/TGUIKit/NavigationViewController.swift +++ /dev/null @@ -1,594 +0,0 @@ -// -// NavigationViewController.swift -// TGUIKit -// -// Created by keepcoder on 15/09/16. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import SwiftSignalKitMac -public enum ViewControllerStyle { - case push; - case pop; - case none; -} - -open class NavigationHeaderView : View { - public private(set) weak var header:NavigationHeader? - public let ready:Promise = Promise() - public init(_ header:NavigationHeader) { - self.header = header - super.init() - self.autoresizingMask = [.width] - } - - required public init(frame frameRect: NSRect) { - fatalError("init(frame:) has not been implemented") - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - -} - -open class NavigationHeader { - fileprivate var callHeader:NavigationHeader? - let height:CGFloat - let initializer:(NavigationHeader)->NavigationHeaderView - weak var navigation:NavigationViewController? - fileprivate var _view:NavigationHeaderView? - fileprivate let disposable:MetaDisposable = MetaDisposable() - fileprivate var isShown:Bool = false - public var needShown:Bool = false - public init(_ height:CGFloat, initializer:@escaping(NavigationHeader)->NavigationHeaderView) { - self.height = height - self.initializer = initializer - } - - public var view:NavigationHeaderView { - if _view == nil { - _view = initializer(self) - } - return _view! - } - - deinit { - disposable.dispose() - } - - open func show(_ animated:Bool) { - assert(navigation != nil) - needShown = true - if isShown { - return - } - isShown = true - if let navigation = navigation { - let view = self.view - let height = self.height - view.frame = NSMakeRect(0, 0, navigation.containerView.frame.width, height) - - disposable.set((view.ready.get() |> filter {$0} |> take(1)).start(next: { [weak navigation, weak self, weak view] (ready) in - if let navigation = navigation, let view = view { - let contentInset = navigation.controller.bar.height + height - navigation.containerView.addSubview(view, positioned: .above, relativeTo: navigation.controller.view) - - var inset:CGFloat = navigation.controller.bar.height - - if let callHeader = self?.callHeader, callHeader.needShown { - inset += callHeader.height - } - - view.change(pos: NSMakePoint(0, inset), animated: animated, completion: { [weak navigation] completed in - if let navigation = navigation, completed { - navigation.controller.view.frame = NSMakeRect(0, contentInset, navigation.controller.frame.width, navigation.frame.height - contentInset) - navigation.controller.view.needsLayout = true - } - }) - - } - })) - } - - } - - open func hide(_ animated:Bool) { - assert(navigation != nil) - if !isShown { - return - } - needShown = false - isShown = false - - if let navigation = navigation { - if animated { - view.change(pos: NSMakePoint(0, 0), animated: animated, removeOnCompletion: false, completion: { [weak self] completed in - self?._view?.removeFromSuperview() - self?._view = nil - }) - } else { - view.removeFromSuperview() - _view = nil - } - - var inset:CGFloat = navigation.controller.bar.height - - if let callHeader = callHeader, callHeader.needShown { - inset += callHeader.height - } - navigation.controller.view.setFrameSize(NSMakeSize(navigation.controller.frame.width, navigation.frame.height - inset)) - navigation.controller.view.setFrameOrigin(NSMakePoint(0, inset)) - } - - } -} - -public class CallNavigationHeader : NavigationHeader { - fileprivate weak var simpleHeader:NavigationHeader? - public override func show(_ animated:Bool) { - assert(navigation != nil) - needShown = true - if isShown { - return - } - isShown = true - if let navigation = navigation { - let view = self.view - let height = self.height - view.frame = NSMakeRect(0, 0, navigation.containerView.frame.width, height) - - disposable.set((view.ready.get() |> take(1)).start(next: { [weak navigation, weak view] (ready) in - if let navigation = navigation, let view = view { - let contentInset = navigation.controller.bar.height + height - navigation.containerView.addSubview(view, positioned: .above, relativeTo: navigation.controller.view) - - navigation.navigationBar.change(pos: NSMakePoint(0, height), animated: animated) - - self.simpleHeader?.view.change(pos: NSMakePoint(0, height + navigation.controller.bar.height), animated: animated) - - view.change(pos: NSMakePoint(0, 0), animated: animated, completion: { [weak navigation] completed in - if let navigation = navigation, completed { - navigation.controller.view.frame = NSMakeRect(0, contentInset, navigation.controller.frame.width, navigation.frame.height - contentInset) - navigation.controller.view.needsLayout = true - } - }) - - } - })) - } - - } - - public override func hide(_ animated:Bool) { - assert(navigation != nil) - if !isShown { - return - } - needShown = false - isShown = false - - if let navigation = navigation { - if animated { - view.change(pos: NSMakePoint(0, -height), animated: animated, removeOnCompletion: false, completion: { [weak self] completed in - self?._view?.removeFromSuperview() - self?._view = nil - }) - } else { - view.removeFromSuperview() - _view = nil - } - - if let header = simpleHeader, header.needShown { - header.view.change(pos: NSMakePoint(0, navigation.controller.bar.height), animated: animated) - } - - navigation.navigationBar.change(pos: NSZeroPoint, animated: animated) - navigation.controller.view.frame = NSMakeRect(0, navigation.controller.bar.height, navigation.controller.frame.width, navigation.frame.height - navigation.controller.bar.height) - navigation.controller.view.needsLayout = true - } - - } -} - -open class NavigationViewController: ViewController, CALayerDelegate,CAAnimationDelegate { - - public private(set) var modalAction:NavigationModalAction? - - var stack:[ViewController] = [ViewController]() - var lock:Bool = false - - public var empty:ViewController { - didSet { - empty.navigationController = self - - let prev = self.stack.last - self.stack.remove(at: 0) - self.stack.insert(empty, at: 0) - - - var controllerInset:CGFloat = 0 - - if let header = header, header.needShown { - controllerInset += header.height - } - if let header = callHeader, header.needShown { - controllerInset += header.height - } - - empty.loadViewIfNeeded(NSMakeRect(0, controllerInset, self.bounds.width, self.bounds.height - controllerInset)) - - if prev == oldValue { - controller = empty - oldValue.removeFromSuperview() - containerView.addSubview(empty.view) - - if let header = header, header.needShown { - header.view.removeFromSuperview() - containerView.addSubview(header.view) - } - if let header = callHeader, header.needShown { - header.view.removeFromSuperview() - containerView.addSubview(header.view) - } - - } - - empty.view.frame = NSMakeRect(0, controllerInset, self.bounds.width, self.bounds.height - controllerInset - bar.height) - - - } - } - - open var isLocked:Bool { - return lock - } - - public private(set) var controller:ViewController { - didSet { - currentControllerDidChange() - } - } - - func _setController(_ controller:ViewController) { - self.controller = controller - } - - var navigationBar:NavigationBarView = NavigationBarView() - - var pushDisposable:MetaDisposable = MetaDisposable() - var popDisposable:MetaDisposable = MetaDisposable() - - private(set) public var header:NavigationHeader? - private(set) public var callHeader:CallNavigationHeader? - - var containerView:View = View() - - - public func set(header:NavigationHeader?) { - self.header?.hide(false) - header?.navigation = self - header?.callHeader = callHeader - callHeader?.simpleHeader = header - self.header = header - } - - public func set(callHeader:CallNavigationHeader?) { - self.callHeader?.hide(false) - callHeader?.navigation = self - header?.callHeader = callHeader - callHeader?.simpleHeader = header - self.callHeader = callHeader - } - - open override func loadView() { - super.loadView(); - viewDidLoad() - } - - open override func viewDidLoad() { - super.viewDidLoad() - - containerView.frame = bounds - self.view.autoresizesSubviews = true - containerView.autoresizingMask = [.width, .height] - self.view.addSubview(containerView, positioned: .below, relativeTo: self.view.subviews.first) - controller._frameRect = bounds - controller.viewWillAppear(false) - controller.navigationController = self - - containerView.addSubview(navigationBar) - - navigationBar.frame = NSMakeRect(0, 0, NSWidth(containerView.frame), controller.bar.height) - controller.view.frame = NSMakeRect(0, controller.bar.height , NSWidth(containerView.frame), NSHeight(containerView.frame) - controller.bar.height) - - navigationBar.switchViews(left: controller.leftBarView, center: controller.centerBarView, right: controller.rightBarView, controller: controller, style: .none, animationStyle: controller.animationStyle) - - containerView.addSubview(controller.view) - Queue.mainQueue().justDispatch { - self.controller.viewDidAppear(false) - } - - } - - - - - open override var canBecomeResponder: Bool { - return false - } - open func currentControllerDidChange() { - - } - - public override var backgroundColor: NSColor { - set { - self.view.background = newValue - containerView.backgroundColor = newValue - navigationBar.backgroundColor = newValue - } - get { - return self.view.background - } - } - - public init(_ empty:ViewController) { - self.empty = empty - self.controller = empty - self.stack.append(controller) - - super.init() - bar = .init(height: 0) - } - - public var stackCount:Int { - return stack.count - } - - deinit { - self.popDisposable.dispose() - self.pushDisposable.dispose() - } - - public func stackInsert(_ controller:ViewController, at: Int) -> Void { - stack.insert(controller, at: at) - } - - open func push(_ controller:ViewController, _ animated:Bool = true, style: ViewControllerStyle? = nil) -> Void { - -// if isLocked { -// return -// } - - - controller.navigationController = self - controller.loadViewIfNeeded(self.bounds) - self.pushDisposable.set((controller.ready.get() |> take(1)).start(next: {[weak self] _ in - if let strongSelf = self { - strongSelf.lock = true - controller.navigationController = strongSelf - - if let index = strongSelf.stack.index(of: controller) { - strongSelf.stack.remove(at: index) - } - - strongSelf.stack.append(controller) - - let newStyle:ViewControllerStyle - if let style = style { - newStyle = style - } else { - newStyle = animated && strongSelf.stack.count > 1 ? .push : .none - } - - strongSelf.show(controller, newStyle) - } - })) - } - - - func show(_ controller:ViewController,_ style:ViewControllerStyle) -> Void { - - let previous:ViewController = self.controller; - self.controller = controller - controller.navigationController = self - - - if(previous === controller) { - previous.viewWillDisappear(false) - previous.viewDidDisappear(false) - - controller.viewWillAppear(false) - controller.viewDidAppear(false) - _ = controller.becomeFirstResponder() - - return; - } - - var contentInset = controller.bar.height - - var barInset:CGFloat = 0 - if let header = callHeader, header.needShown { - header.view.frame = NSMakeRect(0, 0, containerView.frame.width, header.height) - contentInset += header.height - barInset += header.height - } - - self.navigationBar.frame = NSMakeRect(0, barInset, NSWidth(containerView.frame), controller.bar.height) - - - if let header = header, header.needShown { - header.view.frame = NSMakeRect(0, contentInset, containerView.frame.width, header.height) - containerView.addSubview(header.view, positioned: .below, relativeTo: self.navigationBar) - contentInset += header.height - } - - controller.view.removeFromSuperview() - controller.view.frame = NSMakeRect(0, contentInset , NSWidth(containerView.frame), NSHeight(containerView.frame) - contentInset) - if #available(OSX 10.12, *) { - - } else { - controller.view.needsLayout = true - } - - - let reloadHeaders = { [weak self] in - if let header = self?.header, header.needShown { - header.view.removeFromSuperview() - self?.containerView.addSubview(header.view, positioned: .above, relativeTo: controller.view) - } - - if let header = self?.callHeader, header.needShown { - header.view.removeFromSuperview() - self?.containerView.addSubview(header.view, positioned: .below, relativeTo: self?.navigationBar) - } - } - - var pfrom:CGFloat = 0, pto:CGFloat = 0, nto:CGFloat = 0, nfrom:CGFloat = 0; - - switch style { - case .push: - nfrom = NSWidth(containerView.frame) - nto = 0 - pfrom = 0 - pto = -100//round(NSWidth(self.frame)/3.0) - containerView.addSubview(controller.view, positioned: .above, relativeTo: previous.view) - case .pop: - nfrom = -round(NSWidth(containerView.frame)/3.0) - nto = 0 - pfrom = 0 - pto = NSWidth(containerView.frame) - previous.view.setFrameOrigin(NSMakePoint(pto, previous.frame.minY)) - containerView.addSubview(controller.view, positioned: .below, relativeTo: previous.view) - case .none: - previous.viewWillDisappear(false); - previous.view.removeFromSuperview() - containerView.addSubview(controller.view) - controller.viewWillAppear(false); - previous.viewDidDisappear(false); - controller.viewDidAppear(false); - _ = controller.becomeFirstResponder(); - - self.navigationBar.switchViews(left: controller.leftBarView, center: controller.centerBarView, right: controller.rightBarView, controller: controller, style: style, animationStyle: controller.animationStyle) - lock = false - - navigationBar.removeFromSuperview() - containerView.addSubview(navigationBar) - - reloadHeaders() - - return // without animations - } - - - - if previous.removeAfterDisapper, let index = stack.index(of: previous) { - self.stack.remove(at: index) - } - - navigationBar.removeFromSuperview() - containerView.addSubview(navigationBar) - - reloadHeaders() - - previous.viewWillDisappear(true); - controller.viewWillAppear(true); - - - CATransaction.begin() - - - self.navigationBar.switchViews(left: controller.leftBarView, center: controller.centerBarView, right: controller.rightBarView, controller: controller, style: style, animationStyle: controller.animationStyle) - - previous.view.layer?.animate(from: pfrom as NSNumber, to: pto as NSNumber, keyPath: "position.x", timingFunction: kCAMediaTimingFunctionSpring, duration: previous.animationStyle.duration, removeOnCompletion: true, additive: false, completion: { [weak self] completed in - if completed { - previous.view.removeFromSuperview() - previous.viewDidDisappear(true); - } - - self?.lock = false - }); - - - controller.view.layer?.animate(from: nfrom as NSNumber, to: nto as NSNumber, keyPath: "position.x", timingFunction: kCAMediaTimingFunctionSpring, duration: controller.animationStyle.duration, removeOnCompletion: true, additive: false, completion: { completed in - if completed { - controller.viewDidAppear(true); - _ = controller.becomeFirstResponder() - } - - }); - - - CATransaction.commit() - - } - - open override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - navigationBar.updateLocalizationAndTheme() - } - - open func back(animated:Bool = true) -> Void { - if stackCount > 1 && !isLocked, let last = stack.last, last.invokeNavigationBack() { - let controller = stack[stackCount - 2] - last.didRemovedFromStack() - stack.removeLast() - show(controller, animated ? .pop : .none) - } - } - - public func to( index:Int? = nil) -> Void { - if stackCount > 1, let index = index { - if index < 0 { - gotoEmpty(false) - } else { - let controller = stack[index] - stack.removeSubrange(min(max(1, index + 1), stackCount) ..< stackCount) - show(controller, .none) - } - } - } - - public func gotoEmpty(_ animated:Bool = true) -> Void { - if controller != empty { - stack.removeSubrange(1 ..< stackCount - 1) - show(empty, animated ? .pop : .none) - } - } - - public func close(animated:Bool = true) ->Void { - if stackCount > 1 && !isLocked { - let controller = stack[0] - stack.last?.didRemovedFromStack() - stack.removeLast() - show(controller, animated ? .pop : .none) - } - } - - public func set(modalAction:NavigationModalAction, _ showView:Bool = true) { - self.modalAction?.view?.removeFromSuperview() - self.modalAction = modalAction - modalAction.navigation = self - if showView { - let actionView = NavigationModalView(action: modalAction, viewController: self) - modalAction.view = actionView - actionView.frame = bounds - view.addSubview(actionView) - } - } - - public func removeModalAction() { - self.modalAction?.view?.removeFromSuperview() - self.modalAction = nil - } - - public func enumerateControllers(_ f:(ViewController, Int)->Bool) { - for i in stride(from: stack.count - 1, to: -1, by: -1) { - if f(stack[i], i) { - break - } - } - } - -} diff --git a/TGUIKit/TGUIKit/Popover.swift b/TGUIKit/TGUIKit/Popover.swift deleted file mode 100644 index 554fc97fc4..0000000000 --- a/TGUIKit/TGUIKit/Popover.swift +++ /dev/null @@ -1,360 +0,0 @@ -// -// Popover.swift -// TGUIKit -// -// Created by keepcoder on 26/09/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import SwiftSignalKitMac - -class PopoverBackground: Control { - fileprivate weak var popover:Popover? -} - -open class Popover: NSObject { - - private weak var window:Window? - - private var disposable:MetaDisposable = MetaDisposable() - - public var animates:Bool = true - - public var controller:ViewController? - - private weak var control:Control? - - public var isShown:Bool = false - - public var overlay:OverlayControl! - private var background:PopoverBackground = PopoverBackground(frame: NSZeroRect) - - public var animationStyle:AnimationStyle = AnimationStyle(duration:0.2, function:kCAMediaTimingFunctionSpring) - - var readyDisposable:MetaDisposable = MetaDisposable() - - required public init(controller:ViewController) { - self.controller = controller - self.background.layer?.shadowOpacity = 0.4 - self.background.layer?.rasterizationScale = CGFloat(System.backingScale) - self.background.layer?.shouldRasterize = true - self.background.layer?.isOpaque = false - self.background.layer?.shadowOffset = NSMakeSize(0, 0) - self.background.layer?.cornerRadius = 4 - super.init() - - background.popover = self - } - - - - open func show(for control:Control, edge:NSRectEdge? = nil, inset:NSPoint = NSZeroPoint, contentRect:NSRect = NSMakeRect(7, 7, 0, 0), delayBeforeShown: Double = 0.2) -> Void { - - if let controller = controller, let parentView = control.window?.contentView { - - controller.loadViewIfNeeded() - controller.viewWillAppear(animates) - - self.window = control.kitWindow - - var signal = controller.ready.get() |> filter {$0} |> take(1) - if control.controlState == .Hover && delayBeforeShown > 0.0 { - signal = signal |> delay(delayBeforeShown, queue: Queue.mainQueue()) - } - self.readyDisposable.set(signal.start(next: {[weak self, weak controller, weak parentView] (ready) in - - if let parentView = parentView { - for subview in parentView.subviews { - if let view = subview as? PopoverBackground { - view.popover?.hide(false) - } - } - } - - if let strongSelf = self, let controller = controller, let parentView = parentView, (strongSelf.inside() || (control.controlState == .Hover || control.controlState == .Highlight) || !control.userInteractionEnabled), control.window != nil, control.visibleRect != NSZeroRect { - - control.isSelected = true - - strongSelf.window?.set(escape: { [weak strongSelf] () -> KeyHandlerResult in - strongSelf?.hide() - return .invoked - }, with: strongSelf, priority: .modal) - - strongSelf.window?.set(handler: { () -> KeyHandlerResult in - return .invokeNext - }, with: strongSelf, for: .All) - - strongSelf.window?.set(mouseHandler: { [weak self] (_) -> KeyHandlerResult in - if let strongSelf = self, !strongSelf.inside() { - strongSelf.hide() - } - return .invokeNext - }, with: strongSelf, for: .leftMouseUp, priority: .high) - - - strongSelf.window?.set(mouseHandler: { [weak self] (_) -> KeyHandlerResult in - if let strongSelf = self, !strongSelf.inside() && !control.mouseInside() { - strongSelf.hide() - } - return .invokeNext - }, with: strongSelf, for: .scrollWheel, priority: .high) - - strongSelf.control = control - strongSelf.background.flip = false - var point:NSPoint = control.convert(NSMakePoint(0, 0), to: parentView) - - if let edge = edge { - - switch edge { - case .maxX: - point.x -= controller.frame.width - case .maxY: - // point.x += floorToScreenPixels((control.superview!.frame.width - controller.frame.width) / 2.0) - point.y -= controller.frame.height - strongSelf.background.flip = true - case .minX: - point.x -= (controller.frame.width - control.frame.width) - point.y -= controller.frame.height - strongSelf.background.flip = true - default: - fatalError("Not Implemented") - } - - - } - - - - if inset.x != 0 { - point.x += (inset.x) - - } - if inset.y != 0 { - point.y += inset.y - } - - - controller.viewDidAppear(strongSelf.animates) - - var rect = controller.bounds - if !NSIsEmptyRect(contentRect) { - rect = contentRect - } - - point.x = min(max(5, point.x), (parentView.frame.width - rect.width - 12) - 5) - point.y = min(max(5, point.y), (parentView.frame.height - rect.height - 12) - 5) - - parentView.layer?.isOpaque = true - - //.borderSize * 2 - strongSelf.background.frame = NSMakeRect(point.x, point.y, rect.width + 14, rect.height + 14) - strongSelf.background.backgroundColor = .clear - strongSelf.background.layer?.cornerRadius = .cornerRadius - - strongSelf.overlay = OverlayControl(frame: NSMakeRect(contentRect.minX, contentRect.minY, controller.frame.width , controller.frame.height )) - strongSelf.overlay.backgroundColor = presentation.colors.background - strongSelf.overlay.layer?.cornerRadius = .cornerRadius - strongSelf.overlay.layer?.opacity = 0.99 - - - let bg = View(frame: NSMakeRect(strongSelf.overlay.frame.minX + 2, strongSelf.overlay.frame.minY + 2, strongSelf.overlay.frame.width - 4, strongSelf.overlay.frame.height - 4)) - bg.layer?.cornerRadius = .cornerRadius - bg.backgroundColor = presentation.colors.background - - strongSelf.background.addSubview(bg) - - strongSelf.background.addSubview(strongSelf.overlay) - - - controller.view.layer?.cornerRadius = .cornerRadius - controller.view.setFrameOrigin(NSMakePoint(0, 0)) - - - strongSelf.overlay.addSubview(controller.view) - - parentView.addSubview(strongSelf.background) - - //strongSelf.overlay.center() - - _ = controller.becomeFirstResponder() - - strongSelf.isShown = true - - if let _ = strongSelf.overlay { - if strongSelf.animates { - - var once:Bool = false - - for sub in strongSelf.background.subviews { - sub.layer?.animate(from: (-strongSelf.background.frame.height) as NSNumber, to: (sub.frame.minY) as NSNumber, keyPath: "position.y", timingFunction: strongSelf.animationStyle.function, duration: strongSelf.animationStyle.duration, removeOnCompletion: true, additive: false, completion:{ [weak controller] (comple) in - if let strongSelf = self, !once { - once = true - controller?.viewDidAppear(strongSelf.animates) - } - - }) - - // sub.layer?.animate(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "opacity", timingFunction: strongSelf.animationStyle.function, duration: strongSelf.animationStyle.duration) - } - - - - } - - let nHandler:(Control) -> Void = { [weak strongSelf] control in - if let strongSelf = strongSelf { - let s = Signal.single(Void()) |> delay(0.2, queue: Queue.mainQueue()) |> then(Signal.single(Void()) |> delay(0.1, queue: Queue.mainQueue()) |> restart) - - strongSelf.disposable.set(s.start(next: { [weak strongSelf] () in - if let strongSelf = strongSelf { - if !strongSelf.inside() && !control.mouseInside() { - strongSelf.hide() - } - } - - })) - } - - - } - - var first: Bool = true - - control.kitWindow?.set(mouseHandler: { [weak strongSelf, weak control] _ -> KeyHandlerResult in - if let strongSelf = strongSelf, first, let control = control { - if !strongSelf.inside() && !control.mouseInside() { - first = false - nHandler(control) - } - } - return .invokeNext - }, with: strongSelf, for: .mouseMoved, priority: .high) - - let hHandler:(Control) -> Void = { [weak strongSelf] _ in - - strongSelf?.disposable.set(nil) - - } - - strongSelf.background.set(handler: nHandler, for: .Normal) - strongSelf.background.set(handler: hHandler, for: .Hover) - - - control.set(handler: nHandler, for: .Normal) - control.set(handler: hHandler, for: .Hover) - - - } - } else if let strongSelf = self { - controller?.viewWillDisappear(false) - controller?.viewDidDisappear(false) - controller?.popover = nil - strongSelf.controller = nil - strongSelf.window?.removeAllHandlers(for: strongSelf) - strongSelf.window?.remove(object: strongSelf, for: .All) - } - - })) - - } - - } - - - public func addSubview(_ subview: View) -> Void { - self.background.addSubview(subview) - } - - func inside() -> Bool { - - // return true - - if let window = control?.window { - let g:NSPoint = NSEvent.mouseLocation - let w:NSPoint = window.contentView!.convert(window.convertFromScreen(NSMakeRect(g.x, g.y, 1, 1)).origin, from: nil) - //if w.x > background.frame.minX && background - return NSPointInRect(w, background.frame) - } - return false - } - - - deinit { - self.disposable.dispose() - self.readyDisposable.dispose() - window?.remove(object: self, for: .All) - background.removeFromSuperview() - } - - public func hide(_ removeHandlers:Bool = true) -> Void { - if !isShown { - return - } - isShown = false - control?.isSelected = false - window?.removeAllHandlers(for: self) - window?.remove(object: self, for: .All) - - overlay?.removeLastStateHandler() - overlay?.removeLastStateHandler() - - if removeHandlers { - control?.removeLastStateHandler() - control?.removeLastStateHandler() - } - - self.disposable.dispose() - self.readyDisposable.dispose() - controller?.viewWillDisappear(true) - if animates { - var once:Bool = false - background.change(opacity: 0, animated: animates) - for sub in background.subviews { - - sub._change(opacity: 0, animated: true, duration: animationStyle.duration, timingFunction: animationStyle.function, completion: { [weak self] complete in - if let strongSelf = self, !once { - once = true - strongSelf.controller?.viewDidDisappear(true) - strongSelf.controller?.popover = nil - strongSelf.controller = nil - strongSelf.background.removeFromSuperview() - } - }) - - } - } else { - controller?.viewDidDisappear(false) - controller?.popover = nil - controller = nil - background.removeFromSuperview() - } - } - -} - -public func hasPopover(_ window:Window) -> Bool { - if !window.sheets.isEmpty { - return true - } - for subview in window.contentView!.subviews { - if let subview = subview as? PopoverBackground, let popover = subview.popover { - return popover.isShown - } - } - return false -} - -public func showPopover(for control:Control, with controller:ViewController, edge:NSRectEdge? = nil, inset:NSPoint = NSZeroPoint, delayBeforeShown: Double = 0.2) -> Void { - if let window = control.window as? Window, !hasPopover(window) { - if controller.popover == nil { - controller.popover = (controller.popoverClass as! Popover.Type).init(controller: controller) - } - - if let popover = controller.popover { - popover.show(for: control, edge: edge, inset: inset, delayBeforeShown: delayBeforeShown) - } - } -} - - diff --git a/TGUIKit/TGUIKit/PresentationTheme.swift b/TGUIKit/TGUIKit/PresentationTheme.swift deleted file mode 100644 index 9098ffa3ba..0000000000 --- a/TGUIKit/TGUIKit/PresentationTheme.swift +++ /dev/null @@ -1,154 +0,0 @@ -// -// PresentationTheme.swift -// Telegram -// -// Created by keepcoder on 22/06/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - -import Cocoa -import SwiftSignalKitMac - -// - - - - - -public struct SearchTheme { - public let backgroundColor: NSColor - public let searchImage:CGImage - public let clearImage:CGImage - public let placeholder:String - public let textColor: NSColor - public let placeholderColor: NSColor - public init(_ backgroundColor: NSColor, _ searchImage:CGImage, _ clearImage:CGImage, _ placeholder:String, _ textColor: NSColor, _ placeholderColor: NSColor) { - self.backgroundColor = backgroundColor - self.searchImage = searchImage - self.clearImage = clearImage - self.placeholder = placeholder - self.textColor = textColor - self.placeholderColor = placeholderColor - } -} - -public struct ColorPallete { - public let background: NSColor - public let text: NSColor - public let grayText:NSColor - public let link:NSColor - public let blueUI:NSColor - public let redUI:NSColor - public let greenUI:NSColor - public let blackTransparent:NSColor - public let grayTransparent:NSColor - public let grayUI:NSColor - public let darkGrayText:NSColor - public let blueText:NSColor - public let blueSelect:NSColor - public let selectText:NSColor - public let blueFill:NSColor - public let border:NSColor - public let grayBackground:NSColor - public let grayForeground:NSColor - public let grayIcon:NSColor - public let blueIcon:NSColor - public let badgeMuted:NSColor - public let badge:NSColor - public let indicatorColor: NSColor - public let selectMessage: NSColor - public init(background:NSColor, text: NSColor, grayText: NSColor, link: NSColor, blueUI:NSColor, redUI:NSColor, greenUI:NSColor, blackTransparent:NSColor, grayTransparent:NSColor, grayUI:NSColor, darkGrayText:NSColor, blueText:NSColor, blueSelect:NSColor, selectText:NSColor, blueFill:NSColor, border:NSColor, grayBackground:NSColor, grayForeground:NSColor, grayIcon:NSColor, blueIcon:NSColor, badgeMuted:NSColor, badge:NSColor, indicatorColor: NSColor, selectMessage: NSColor) { - self.background = background - self.text = text - self.grayText = grayText - self.link = link - self.blueUI = blueUI - self.redUI = redUI - self.greenUI = greenUI - self.blackTransparent = blackTransparent - self.grayTransparent = grayTransparent - self.grayUI = grayUI - self.darkGrayText = darkGrayText - self.blueText = blueText - self.blueSelect = blueSelect - self.selectText = selectText - self.blueFill = blueFill - self.border = border - self.grayBackground = grayBackground - self.grayForeground = grayForeground - self.grayIcon = grayIcon - self.blueIcon = blueIcon - self.badgeMuted = badgeMuted - self.badge = badge - self.indicatorColor = indicatorColor - self.selectMessage = selectMessage - } -} - - - -open class PresentationTheme : Equatable { - - public let colors:ColorPallete - public let search: SearchTheme - - public let resourceCache = PresentationsResourceCache() - - public init(colors: ColorPallete, search: SearchTheme) { - self.colors = colors - self.search = search - } - - static var current: PresentationTheme { - return presentation - } - - - public static func ==(lhs: PresentationTheme, rhs: PresentationTheme) -> Bool { - return lhs === rhs - } - -// public func image(_ key: Int32, _ generate: (PresentationTheme) -> CGImage?) -> CGImage? { -// return self.resourceCache.image(key, self, generate) -// } -// -// public func object(_ key: Int32, _ generate: (PresentationTheme) -> AnyObject?) -> AnyObject? { -// return self.resourceCache.object(key, self, generate) -// } -} - - -public var navigationButtonStyle:ControlStyle { - return ControlStyle(font: .normal(.title), foregroundColor: presentation.colors.link, backgroundColor: presentation.colors.background, highlightColor: presentation.colors.blueUI) -} -public var switchViewAppearance: SwitchViewAppearance { - return SwitchViewAppearance(backgroundColor: presentation.colors.background, stateOnColor: presentation.colors.blueUI, stateOffColor: presentation.colors.grayBackground, disabledColor: presentation.colors.grayTransparent, borderColor: presentation.colors.border) -} -//0xE3EDF4 -public let whitePallete = ColorPallete(background: .white, text: NSColor(0x000000), grayText: NSColor(0x999999), link: NSColor(0x2481cc), blueUI: NSColor(0x2481cc), redUI: NSColor(0xff3b30), greenUI:NSColor(0x63DA6E), blackTransparent: NSColor(0x000000, 0.6), grayTransparent: NSColor(0xf4f4f4, 0.4), grayUI: NSColor(0xFaFaFa), darkGrayText:NSColor(0x333333), blueText:NSColor(0x2481CC), blueSelect:NSColor(0x4c91c7), selectText:NSColor(0xeaeaea), blueFill:NSColor(0x4ba3e2), border:NSColor(0xeaeaea), grayBackground:NSColor(0xf4f4f4), grayForeground:NSColor(0xe4e4e4), grayIcon:NSColor(0x9e9e9e), blueIcon:NSColor(0x0f8fe4), badgeMuted:NSColor(0xd7d7d7), badge:NSColor(0x4ba3e2), indicatorColor: NSColor(0x464a57), selectMessage: NSColor(0xE3EDF4)) - -//04afc8 -//282b35 -public let darkPallete = ColorPallete(background: NSColor(0x292b36), text: NSColor(0xe9e9e9), grayText: NSColor(0x8699a3), link: NSColor(0x04afc8), blueUI: NSColor(0x04afc8), redUI: NSColor(0xec6657), greenUI:NSColor(0x49ad51), blackTransparent: NSColor(0x000000, 0.6), grayTransparent: NSColor(0x2f313d, 0.5), grayUI: NSColor(0x292b36), darkGrayText:NSColor(0x8699a3), blueText:NSColor(0x04afc8), blueSelect:NSColor(0x20889a), selectText: NSColor(0x8699a3), blueFill: NSColor(0x04afc8), border: NSColor(0x464a57), grayBackground:NSColor(0x464a57), grayForeground:NSColor(0x3d414d), grayIcon: NSColor(0x8699a3), blueIcon: NSColor(0x04afc8), badgeMuted:NSColor(0x8699a3), badge:NSColor(0x04afc8), indicatorColor: .white, selectMessage: NSColor(0x3d414d)) - -/* - public let darkPallete = ColorPallete(background: NSColor(0x282e33), text: NSColor(0xe9e9e9), grayText: NSColor(0x999999), link: NSColor(0x20eeda), blueUI: NSColor(0x20eeda), redUI: NSColor(0xec6657), greenUI:NSColor(0x63DA6E), blackTransparent: NSColor(0x000000, 0.6), grayTransparent: NSColor(0xf4f4f4, 0.4), grayUI: NSColor(0xFaFaFa), darkGrayText:NSColor(0x333333), blueText:NSColor(0x009687), blueSelect:NSColor(0x009687), selectText:NSColor(0xeaeaea), blueFill: NSColor(0x20eeda), border: NSColor(0x3d444b), grayBackground:NSColor(0x3d444b), grayForeground:NSColor(0xe4e4e4), grayIcon:NSColor(0x757676), blueIcon: NSColor(0x20eeda), badgeMuted:NSColor(0xd7d7d7), badge:NSColor(0x4ba3e2), indicatorColor: .white) - */ - - -private var _theme:Atomic = Atomic(value: whiteTheme) - -public let whiteTheme = PresentationTheme(colors: whitePallete, search: SearchTheme(.grayBackground, #imageLiteral(resourceName: "Icon_SearchField").precomposed(), #imageLiteral(resourceName: "Icon_SearchClear").precomposed(), localizedString("SearchField.Search"), .text, .grayText)) - - - -public var presentation:PresentationTheme { - return _theme.modify {$0} -} - -public func updateTheme(_ theme:PresentationTheme) { - assertOnMainThread() - _ = _theme.swap(theme) -} - - diff --git a/TGUIKit/TGUIKit/ProgressIndicator.swift b/TGUIKit/TGUIKit/ProgressIndicator.swift deleted file mode 100644 index 93f43a6aa2..0000000000 --- a/TGUIKit/TGUIKit/ProgressIndicator.swift +++ /dev/null @@ -1,356 +0,0 @@ -// -// ProgressIndicator.swift -// TGUIKit -// -// Created by keepcoder on 06/07/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - -import Cocoa - -/*fileprivate let kITSpinAnimationKey: String = "spinAnimation" -fileprivate let kITProgressPropertyKey: String = "progress" - - -extension NSBezierPath { - func it_rotatedBezierPath(_ angle: Float) -> NSBezierPath { - return it_rotatedBezierPath(angle, aboutPoint: NSMakePoint(NSMidX(bounds), NSMidY(bounds))) - } - - func it_rotatedBezierPath(_ angle: Float, aboutPoint point: NSPoint) -> NSBezierPath { - if angle == 0.0 { - return self - } else { - let copy: NSBezierPath = self - let xfm: NSAffineTransform = it_rotationTransform(withAngle: angle, aboutPoint: point) - copy.transform(using: xfm as AffineTransform) - return copy - } - } - - func it_rotationTransform(withAngle angle: Float, aboutPoint: NSPoint) -> NSAffineTransform { - let xfm = NSAffineTransform() - xfm.translateX(by: aboutPoint.x, yBy: aboutPoint.y) - xfm.rotate(byRadians: CGFloat(angle)) - xfm.translateX(by: -aboutPoint.x, yBy: -aboutPoint.y) - return xfm - } -} - -public class ProgressIndicator: View { - public var isIndeterminate: Bool = false { - didSet { - if (!isIndeterminate) { - self.animates = false; - } - } - } - public var progress:CGFloat = 0 { - didSet { - if (isIndeterminate) { - reloadIndicatorContent() - } - } - } - public override var animates: Bool { - didSet { - reloadIndicatorContent() - reloadAnimation() - reloadVisibility() - } - } - public var hideWhenStopped:Bool { - didSet { - reloadVisibility() - } - } - public var lengthOfLine:CGFloat { - didSet { - reloadIndicatorContent() - } - } - public var widthOfLine:CGFloat { - didSet { - reloadIndicatorContent() - } - } - public var numberOfLines:Int32 { - didSet { - reloadAnimation() - reloadIndicatorContent() - } - } - public var innerMargin:CGFloat { - didSet { - reloadIndicatorContent() - } - } - public var animationDuration:CGFloat { - didSet { - reloadAnimation() - } - } - public var steppedAnimation:Bool { - didSet { - reloadAnimation() - } - } - public var color:NSColor { - didSet { - reloadIndicatorContent() - } - } - - private let progressIndicatorLayer: CALayer = CALayer() - - public required init(frame frameRect: NSRect) { - self.color = presentation.colors.indicatorColor - self.innerMargin = 4; - self.widthOfLine = 3; - self.lengthOfLine = 6; - self.numberOfLines = 8; - self.animationDuration = 0.6; - self.isIndeterminate = true; - self.steppedAnimation = true; - self.hideWhenStopped = true; - - - super.init(frame: frameRect) - self.animates = true; - self.wantsLayer = true - self.backgroundColor = .clear - self.progressIndicatorLayer.frame = bounds - self.layer!.addSublayer(progressIndicatorLayer) - self.flip = false - reloadIndicatorContent() - reloadAnimation() - } - - - public override func viewDidMoveToWindow() { - super.viewDidMoveToWindow() - if let _ = window { - animates = true - } else { - animates = false - } - } - convenience override init() { - self.init(frame: NSMakeRect(0, 0, 20, 20)) - } - - public override func setFrameSize(_ newSize: NSSize) { - super.setFrameSize(newSize) - self.progressIndicatorLayer.frame = bounds - } - - func reloadIndicatorContent() { - progressIndicatorLayer.contents = progressImage - } - - func reloadAnimation() { - progressIndicatorLayer.removeAnimation(forKey: kITSpinAnimationKey) - if animates { - progressIndicatorLayer.add(keyFrameAnimationForCurrentPreferences(), forKey: kITSpinAnimationKey) - } - } - - - var progressImage: NSImage { - let progressImage = NSImage(size: bounds.size) - progressImage.lockFocus() - do { - NSGraphicsContext.saveGraphicsState() - do { - color.set() - let r: NSRect = bounds - let numberOfLines:Float = Float(self.numberOfLines) - let line = NSBezierPath(roundedRect: NSMakeRect((r.width / 2) - (widthOfLine / 2), (r.height / 2) - innerMargin - lengthOfLine, widthOfLine, lengthOfLine), xRadius: widthOfLine / 2, yRadius: widthOfLine / 2) - - let lineDrawingBlock: (_ line: Int32) -> Void = { (_ lineNumber: Int32) -> Void in - var lineInstance: NSBezierPath = line.copy() as! NSBezierPath - lineInstance = lineInstance.it_rotatedBezierPath(((2 * Float.pi) / numberOfLines * Float(lineNumber)) + Float.pi, aboutPoint: NSMakePoint(r.width / 2, r.height / 2)) - - if self.isIndeterminate { - self.color.withAlphaComponent(CGFloat(1.0 - (1.0 / Float(self.numberOfLines) * Float(lineNumber)))).set() - } - lineInstance.fill() - } - - if !isIndeterminate { - var i = self.numberOfLines - - while i > Int32(round(numberOfLines - (numberOfLines * Float(progress)))) { - lineDrawingBlock(i) - i -= 1 - } - } - else { - for i in 0 ..< self.numberOfLines { - lineDrawingBlock(i) - } - } - } - NSGraphicsContext.restoreGraphicsState() - } - progressImage.unlockFocus() - return progressImage - } - - func keyFrameAnimationForCurrentPreferences() -> CAKeyframeAnimation { - var keyFrameValues:[NSNumber] = [] - var keyTimeValues:[NSNumber] = [] - if steppedAnimation { - do { - keyFrameValues.append(NSNumber(value: 0.0)) - for i in 0 ..< numberOfLines { - let i:Float = Float(i) - keyFrameValues.append(NSNumber(value: -Float.pi * (2.0 / Float(numberOfLines) * i))) - keyFrameValues.append(NSNumber(value: -Float.pi * (2.0 / Float(numberOfLines) * i))) - } - keyFrameValues.append(NSNumber(value: -Float.pi * 2.0)) - } - do { - keyTimeValues.append(NSNumber(value: 0.0)) - for i in 0 ..< (numberOfLines - 1) { - let i:Float = Float(i) - keyTimeValues.append(NSNumber(value: 1.0 / Float(numberOfLines) * i)) - keyTimeValues.append(NSNumber(value: 1.0 / Float(numberOfLines) * (i + 1))) - } - keyTimeValues.append(NSNumber(value: 1.0 / Float(numberOfLines) * (Float(numberOfLines) - 1))) - } - } - else { - do { - keyFrameValues.append(NSNumber(value: -Float.pi * 0.0)) - keyFrameValues.append(NSNumber(value: -Float.pi * 0.5)) - keyFrameValues.append(NSNumber(value: -Float.pi * 1.0)) - keyFrameValues.append(NSNumber(value: -Float.pi * 1.5)) - keyFrameValues.append(NSNumber(value: -Float.pi * 2.0)) - } - } - let animation = CAKeyframeAnimation(keyPath: "transform") - animation.repeatCount = .greatestFiniteMagnitude - animation.values = keyFrameValues - animation.keyTimes = keyTimeValues - animation.valueFunction = CAValueFunction(name: kCAValueFunctionRotateZ) - animation.duration = CFTimeInterval(animationDuration) - animation.beginTime = 1 - return animation - } - - func reloadVisibility() { -// if hideWhenStopped && !animates && isIndeterminate { -// isHidden = true -// } -// else { -// isHidden = false -// } - } - - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - -}*/ - - -private class ProgressLayer : CALayer { - - fileprivate func update(_ hasAnimation: Bool) { - if hasAnimation { - var fromValue: Float = 0 - - if let layer = presentation(), let from = layer.value(forKeyPath: "transform.rotation.z") as? Float { - fromValue = from - } - let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z") - basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) - basicAnimation.duration = 0.8 - basicAnimation.fromValue = fromValue - basicAnimation.toValue = Double.pi * 2.0 - basicAnimation.repeatCount = Float.infinity - basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) - add(basicAnimation, forKey: "progressRotation") - } else { - removeAllAnimations() - } - } - - - override func draw(in ctx: CGContext) { - - ctx.setStrokeColor(PresentationTheme.current.colors.indicatorColor.cgColor) - - let startAngle = 2.0 * (CGFloat.pi) * 0.8 - CGFloat.pi / 2 - let endAngle = -(CGFloat.pi / 2) - - let lineWidth: CGFloat = 2.0 - let diameter = floorToScreenPixels(frame.height) - - let pathDiameter = diameter - lineWidth - lineWidth * 2 - ctx.addArc(center: NSMakePoint(diameter / 2.0, floorToScreenPixels(diameter / 2.0)), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: true) - - ctx.setLineWidth(lineWidth); - ctx.setLineCap(.round); - ctx.strokePath() - } -} - -public class ProgressIndicator : View { - private let indicator: ProgressLayer = ProgressLayer() - public required init(frame frameRect: NSRect) { - super.init(frame: frameRect) - indicator.frame = bounds - layer?.addSublayer(indicator) - indicator.isOpaque = false - indicator.contentsScale = System.backingScale - } - - public override func setFrameSize(_ newSize: NSSize) { - super.setFrameSize(newSize) - indicator.frame = bounds - indicator.setNeedsDisplay() - } - - public override init() { - super.init(frame: NSMakeRect(0, 0, 20, 20)) - indicator.frame = bounds - layer?.addSublayer(indicator) - indicator.isOpaque = false - indicator.contentsScale = System.backingScale - } - - public override func viewDidMoveToSuperview() { - updateWantsAnimation() - } - - public override func viewDidMoveToWindow() { - updateWantsAnimation() - } - - public override func viewDidHide() { - updateWantsAnimation() - } - - public override func viewDidUnhide() { - updateWantsAnimation() - } - - private func updateWantsAnimation() { - indicator.update(!isHidden && superview != nil && window != nil) - indicator.setNeedsDisplay() - } - - - override public func draw(_ layer: CALayer, in ctx: CGContext) { - //super.draw(layer, in: ctx) - - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - diff --git a/TGUIKit/TGUIKit/ProgressModal.swift b/TGUIKit/TGUIKit/ProgressModal.swift deleted file mode 100644 index 9bf4930e5e..0000000000 --- a/TGUIKit/TGUIKit/ProgressModal.swift +++ /dev/null @@ -1,112 +0,0 @@ -// -// ProgressModal.swift -// TGUIKit -// -// Created by keepcoder on 09/11/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import SwiftSignalKitMac -class ProgressModalController: ModalViewController { - - private var progressView:RadialProgressView? - private var timer:SwiftSignalKitMac.Timer? - private var progress:Float = 0.2 - override var background: NSColor { - return .clear - } - - override var containerBackground: NSColor { - return .clear - } - - override func loadView() { - super.loadView() - - progressView = RadialProgressView(theme: RadialProgressTheme(backgroundColor: .clear, foregroundColor: .white, icon: nil)) - progressView?.state = .ImpossibleFetching(progress: progress, force: false) - view.background = NSColor(0x000000,0.8) - view.addSubview(progressView!) - progressView?.center() - - viewDidLoad() - } - - override func viewDidResized(_ size: NSSize) { - super.viewDidResized(size) - } - - override func viewDidLoad() { - super.viewDidLoad() - readyOnce() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - - self.timer = SwiftSignalKitMac.Timer(timeout: 0.05, repeat: true, completion: { [weak self] in - if let strongSelf = self { - strongSelf.progress += 0.05 - strongSelf.progressView?.state = .ImpossibleFetching(progress: strongSelf.progress, force: false) - if strongSelf.progress >= 0.8 { - strongSelf.timer?.invalidate() - } - } - }, queue: Queue.mainQueue()) - self.timer?.start() - - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - timer?.invalidate() - timer = nil - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - } - - override init() { - super.init(frame:NSMakeRect(0,0,100,100)) - self.bar = .init(height: 0) - } - - -} - -public func showModalProgress(signal:Signal, for window:Window) -> Signal { - return Signal { subscriber in - - let signal = signal |> deliverOnMainQueue - - let modal = ProgressModalController() - let beforeModal:Signal = .single(Void()) |> delay(0.25, queue: Queue.mainQueue()) - - let beforeDisposable:DisposableSet = DisposableSet() - - beforeDisposable.add(beforeModal.start(completed: { - showModal(with: modal, for: window) - })) - - - - beforeDisposable.add(signal.start(next: { next in - subscriber.putNext(next) - }, error: { error in - subscriber.putError(error) - //beforeDisposable.dispose() - modal.close() - }, completed: { - subscriber.putCompletion() - beforeDisposable.dispose() - modal.close() - })) - - return beforeDisposable - } - - -} diff --git a/TGUIKit/TGUIKit/RadialProgressView.swift b/TGUIKit/TGUIKit/RadialProgressView.swift deleted file mode 100644 index edfed2b1f4..0000000000 --- a/TGUIKit/TGUIKit/RadialProgressView.swift +++ /dev/null @@ -1,428 +0,0 @@ -// -// RadialProgressLayer.swift -// TGUIKit -// -// Created by keepcoder on 17/09/16. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import SwiftSignalKitMac - -private let progressInteractiveThumb:CGImage = { - - let context = DrawingContext(size: NSMakeSize(40, 40), scale: 1.0, clear: true) - - context.withContext { (ctx) in - - ctx.round(context.size, context.size.height/2.0) - ctx.setFillColor(NSColor.blueFill.cgColor) - - let image = #imageLiteral(resourceName: "Icon_MessageFile").precomposed() - - ctx.fill(NSMakeRect(0, 0, context.size.width, context.size.height)) - ctx.draw(image, in: NSMakeRect(floorToScreenPixels((context.size.width - image.backingSize.width) / 2.0), floorToScreenPixels((context.size.height - image.backingSize.height) / 2.0), image.backingSize.width, image.backingSize.height)) - - } - - return context.generateImage()! - -}() - -public struct FetchControls { - public let fetch: () -> Void - public init(fetch:@escaping()->Void) { - self.fetch = fetch - } -} - - -private class RadialProgressParameters: NSObject { - let theme: RadialProgressTheme - let diameter: CGFloat - let twist: Bool - let state: RadialProgressState - - init(theme: RadialProgressTheme, diameter: CGFloat, state: RadialProgressState, twist: Bool = true) { - self.theme = theme - self.diameter = diameter - self.state = state - self.twist = twist - super.init() - } -} - -public struct RadialProgressTheme : Equatable { - public let backgroundColor: NSColor - public let foregroundColor: NSColor - public let icon: CGImage? - public let iconInset:NSEdgeInsets - public let diameter:CGFloat? - public let lineWidth: CGFloat - public init(backgroundColor:NSColor, foregroundColor:NSColor, icon:CGImage? = nil, iconInset:NSEdgeInsets = NSEdgeInsets(), diameter: CGFloat? = nil, lineWidth: CGFloat = 2) { - self.iconInset = iconInset - self.backgroundColor = backgroundColor - self.foregroundColor = foregroundColor - self.icon = icon - self.diameter = diameter - self.lineWidth = lineWidth - } -} - -public func ==(lhs:RadialProgressTheme, rhs:RadialProgressTheme) -> Bool { - return lhs.backgroundColor == rhs.backgroundColor && lhs.foregroundColor == rhs.foregroundColor && ((lhs.icon == nil) == (rhs.icon == nil)) -} - -public enum RadialProgressState: Equatable { - case None - case Remote - case Fetching(progress: Float, force: Bool) - case ImpossibleFetching(progress: Float, force: Bool) - case Play - case Icon(image:CGImage, mode:CGBlendMode) -} - -public func ==(lhs:RadialProgressState, rhs:RadialProgressState) -> Bool { - switch lhs { - case .None: - if case .None = rhs { - return true - } else { - return false - } - case .Remote: - if case .Remote = rhs { - return true - } else { - return false - } - case .Play: - if case .Play = rhs { - return true - } else { - return false - } - case let .Fetching(lhsProgress): - if case let .Fetching(rhsProgress) = rhs, lhsProgress == rhsProgress { - return true - } else { - return false - } - case let .ImpossibleFetching(lhsProgress): - if case let .ImpossibleFetching(rhsProgress) = rhs, lhsProgress == rhsProgress { - return true - } else { - return false - } - case .Icon: - if case .Icon = rhs { - return true - } else { - return false - } - } -} - - -private class RadialProgressOverlayLayer: Layer { - let theme: RadialProgressTheme - let twist: Bool - private var timer: SwiftSignalKitMac.Timer? - private var _progress: Float = 0 - private var progress: Float = 0 - var parameters:RadialProgressParameters { - return RadialProgressParameters(theme: self.theme, diameter: theme.diameter ?? frame.width, state: self.state, twist: twist) - } - - - var state: RadialProgressState = .None { - didSet { - switch state { - case .None, .Play, .Remote, .Icon: - self.progress = 0 - self._progress = 0 - case let .Fetching(progress, f), let .ImpossibleFetching(progress, f): - self.progress = progress - if f { - _progress = progress - } - } - let fps: Float = 60 - let difference = progress - _progress - let tick: Float = Float(difference / (fps * 0.2)) - if difference > 0 { - timer = SwiftSignalKitMac.Timer(timeout: TimeInterval(1 / fps), repeat: true, completion: { [weak self] in - if let strongSelf = self { - strongSelf._progress += tick - strongSelf.setNeedsDisplay() - if strongSelf._progress == strongSelf.progress || strongSelf._progress < 0 || strongSelf._progress > strongSelf.progress { - strongSelf.stopAnimation() - } - } - }, queue: Queue.mainQueue()) - timer?.start() - } else { - stopAnimation() - _progress = progress - } - - self.setNeedsDisplay() - } - } - - func stopAnimation() { - timer?.invalidate() - timer = nil - self.setNeedsDisplay() - } - - init(theme: RadialProgressTheme, twist: Bool) { - self.theme = theme - self.twist = twist - super.init() - - self.isOpaque = false - } - - fileprivate override func draw(in ctx: CGContext) { - ctx.setStrokeColor(parameters.theme.foregroundColor.cgColor) - - let startAngle = 2.0 * (CGFloat.pi) * CGFloat(_progress) - CGFloat.pi / 2 - let endAngle = -(CGFloat.pi / 2) - - let pathDiameter = !twist ? parameters.diameter - parameters.theme.lineWidth : parameters.diameter - parameters.theme.lineWidth - parameters.theme.lineWidth * parameters.theme.lineWidth - ctx.addArc(center: NSMakePoint(parameters.diameter / 2.0, floorToScreenPixels(parameters.diameter / 2.0)), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: true) - - ctx.setLineWidth(parameters.theme.lineWidth); - ctx.setLineCap(.round); - ctx.strokePath() - } - - override func layerMoved(to superlayer: CALayer?) { - - super.layerMoved(to: superlayer) - - if let _ = superlayer, parameters.twist { - let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z") - basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) - basicAnimation.duration = 2.0 - basicAnimation.fromValue = NSNumber(value: Float(0.0)) - basicAnimation.toValue = NSNumber(value: Float.pi * 2.0) - basicAnimation.repeatCount = Float.infinity - basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) - self.add(basicAnimation, forKey: "progressRotation") - } else { - self.removeAllAnimations() - } - } - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - - -public class RadialProgressView: Control { - - - public var fetchControls:FetchControls? { - didSet { - self.removeAllHandlers() - if let fetchControls = fetchControls { - set(handler: { _ in - fetchControls.fetch() - }, for: .Click) - } - } - } - - public var theme:RadialProgressTheme { - didSet { - self.setNeedsDisplay() - } - } - private let overlay: RadialProgressOverlayLayer - private var parameters:RadialProgressParameters { - return RadialProgressParameters(theme: self.theme, diameter: NSWidth(self.frame), state: self.state) - } - - - - - public var state: RadialProgressState = .None { - didSet { - self.overlay.state = self.state - if case .Fetching = state { - if self.overlay.superlayer == nil { - self.layer?.addSublayer(self.overlay) - } - } else if case .ImpossibleFetching = state { - if self.overlay.superlayer == nil { - self.layer?.addSublayer(self.overlay) - } - } else { - if self.overlay.superlayer != nil { - self.overlay.removeFromSuperlayer() - } - } - switch oldValue { - case .Fetching: - switch self.state { - case .Fetching: - break - default: - self.setNeedsDisplay() - } - case .ImpossibleFetching: - switch self.state { - case .ImpossibleFetching: - break - default: - self.setNeedsDisplay() - } - case .Remote: - switch self.state { - case .Remote: - break - default: - self.setNeedsDisplay() - } - case .None: - switch self.state { - case .None: - break - default: - self.setNeedsDisplay() - } - case .Play: - switch self.state { - case .Play: - break - default: - self.setNeedsDisplay() - } - case .Icon: - switch self.state { - case .Icon: - break - default: - self.setNeedsDisplay() - } - } - - } - } - - public override func viewDidMoveToSuperview() { - if self.superview == nil { - self.state = .None - } - } - - - - required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - required public init(frame frameRect: NSRect) { - fatalError("init(frame:) has not been implemented") - } - - override public var frame: CGRect { - get { - return super.frame - } set (value) { - let redraw = value.size != self.frame.size - super.frame = value - - if redraw { - self.overlay.frame = CGRect(origin: CGPoint(), size: value.size) - self.setNeedsDisplay() - self.overlay.setNeedsDisplay() - } - } - } - - public init(theme: RadialProgressTheme = RadialProgressTheme(backgroundColor: .blackTransparent, foregroundColor: .white, icon: nil), twist: Bool = true) { - self.theme = theme - self.overlay = RadialProgressOverlayLayer(theme: theme, twist: twist) - super.init() - self.overlay.contentsScale = backingScaleFactor - self.frame = NSMakeRect(0, 0, 40, 40) - - } - - - public override func draw(_ layer: CALayer, in context: CGContext) { - context.setFillColor(parameters.theme.backgroundColor.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: parameters.diameter, height: parameters.diameter))) - - switch parameters.state { - case .None: - break - case .Fetching: - context.setStrokeColor(parameters.theme.foregroundColor.cgColor) - context.setLineWidth(2.0) - context.setLineCap(.round) - - let crossSize: CGFloat = 14.0 - context.move(to: CGPoint(x: parameters.diameter / 2.0 - crossSize / 2.0, y: parameters.diameter / 2.0 - crossSize / 2.0)) - context.addLine(to: CGPoint(x: parameters.diameter / 2.0 + crossSize / 2.0, y: parameters.diameter / 2.0 + crossSize / 2.0)) - context.strokePath() - context.move(to: CGPoint(x: parameters.diameter / 2.0 + crossSize / 2.0, y: parameters.diameter / 2.0 - crossSize / 2.0)) - context.addLine(to: CGPoint(x: parameters.diameter / 2.0 - crossSize / 2.0, y: parameters.diameter / 2.0 + crossSize / 2.0)) - context.strokePath() - case .Remote: - context.setStrokeColor(parameters.theme.foregroundColor.cgColor) - context.setLineWidth(2.0) - context.setLineCap(.round) - context.setLineJoin(.round) - - let arrowHeadSize: CGFloat = 15.0 - let arrowLength: CGFloat = 18.0 - let arrowHeadOffset: CGFloat = 1.0 - - context.move(to: CGPoint(x: parameters.diameter / 2.0, y: parameters.diameter / 2.0 - arrowLength / 2.0 + arrowHeadOffset)) - context.addLine(to: CGPoint(x: parameters.diameter / 2.0, y: parameters.diameter / 2.0 + arrowLength / 2.0 - 1.0 + arrowHeadOffset)) - context.strokePath() - - context.move(to: CGPoint(x: parameters.diameter / 2.0 - arrowHeadSize / 2.0, y: parameters.diameter / 2.0 + arrowLength / 2.0 - arrowHeadSize / 2.0 + arrowHeadOffset)) - context.addLine(to: CGPoint(x: parameters.diameter / 2.0, y: parameters.diameter / 2.0 + arrowLength / 2.0 + arrowHeadOffset)) - context.addLine(to: CGPoint(x: parameters.diameter / 2.0 + arrowHeadSize / 2.0, y: parameters.diameter / 2.0 + arrowLength / 2.0 - arrowHeadSize / 2.0 + arrowHeadOffset)) - context.strokePath() - case .Play: - if let icon = parameters.theme.icon { - var f = focus(icon.backingSize) - f.origin.x += parameters.theme.iconInset.left - f.origin.x -= parameters.theme.iconInset.right - f.origin.y += parameters.theme.iconInset.top - f.origin.y -= parameters.theme.iconInset.bottom - context.draw(icon, in: f) - } - case .ImpossibleFetching: - break - case let .Icon(image: icon, mode:blendMode): - var f = focus(icon.backingSize) - f.origin.x += parameters.theme.iconInset.left - f.origin.x -= parameters.theme.iconInset.right - f.origin.y += parameters.theme.iconInset.top - f.origin.y -= parameters.theme.iconInset.bottom - context.setBlendMode(blendMode) - context.draw(icon, in: f) - } - - } - - public override func copy() -> Any { - let view = View() - view.frame = self.frame - view.layer?.contents = progressInteractiveThumb - return view - - } - - public override func apply(state: ControlState) { - - } - -} diff --git a/TGUIKit/TGUIKit/ScrollView.swift b/TGUIKit/TGUIKit/ScrollView.swift deleted file mode 100644 index a800353bd8..0000000000 --- a/TGUIKit/TGUIKit/ScrollView.swift +++ /dev/null @@ -1,261 +0,0 @@ -// -// ScrollView.swift -// TGUIKit -// -// Created by keepcoder on 07/09/16. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import Foundation -public enum ScrollDirection { - case top; - case bottom; - case none; -} - - - -public struct ScrollPosition : Equatable { - public private(set) var rect:NSRect - public private(set) var visibleRows: NSRange - public private(set) var direction:ScrollDirection - public init(_ rect:NSRect = NSZeroRect, _ direction:ScrollDirection = .none, _ visibleRows: NSRange = NSMakeRange(NSNotFound, 0)) { - self.rect = rect - self.visibleRows = visibleRows - self.direction = direction - } -} - -public func ==(lhs:ScrollPosition, rhs:ScrollPosition) -> Bool { - return NSEqualRects(lhs.rect, rhs.rect) && lhs.direction == rhs.direction && NSEqualRanges(lhs.visibleRows, rhs.visibleRows) -} - -open class ScrollView: NSScrollView, CALayerDelegate{ - private var currentpos:ScrollPosition = ScrollPosition() - public var deltaCorner:Int64 = 60 - - public func scrollPosition(_ visibleRange: NSRange = NSMakeRange(NSNotFound, 0)) -> (current: ScrollPosition, previous: ScrollPosition) { - - let rect = NSMakeRect(contentView.bounds.minX, contentView.bounds.maxY,contentView.documentRect.width, contentView.documentRect.height) - - var d:ScrollDirection = .none - - - if abs(currentpos.rect.minY - rect.minY) < 5 { - return (currentpos, currentpos) - } - - if(currentpos.rect.minY > rect.minY) { - d = .top - } else if(currentpos.rect.minY < rect.minY) { - d = .bottom - } - - let n = ScrollPosition(rect, d, visibleRange) - let previous = currentpos - currentpos = n - return (n, previous) - } - - func updateScroll(_ visibleRange: NSRange = NSMakeRange(NSNotFound, 0)) -> Void { - self.currentpos = ScrollPosition(NSMakeRect(contentView.bounds.minX, contentView.bounds.maxY,contentView.documentRect.width, contentView.documentRect.height), .none, visibleRange) - } - - public var documentOffset:NSPoint { - return NSMakePoint(NSMinX(self.contentView.bounds), NSMinY(self.contentView.bounds)) - } - - public var documentSize:NSSize { - return self.contentView.documentRect.size; - } - - - open override func draw(_ dirtyRect: NSRect) { - - } - - open func draw(_ layer: CALayer, in ctx: CGContext) { - ctx.setFillColor(presentation.colors.background.cgColor) - ctx.fill(bounds) - } - - public var clipView:TGClipView { - return self.contentView as! TGClipView - } - - - override public init(frame frameRect: NSRect) { - super.init(frame: frameRect) - - self.wantsLayer = true; - - self.layer?.delegate = self -// self.canDrawSubviewsIntoLayer = true -// self.layer?.drawsAsynchronously = System.drawAsync - - // self.contentView.wantsLayer = true - // self.contentView.layerContentsRedrawPolicy = .onSetNeedsDisplay - // self.contentView.layer?.drawsAsynchronously = System.drawAsync - - // self.layerContentsRedrawPolicy = .onSetNeedsDisplay; - // self.layer?.isOpaque = false - - let clipView = TGClipView(frame:self.contentView.frame) - self.contentView = clipView; - - - - self.horizontalScroller?.scrollerStyle = .overlay - self.verticalScroller?.scrollerStyle = .overlay - - // verticalScrollElasticity = .automatic - //allowsMagnification = true - //self.hasVerticalScroller = false - - // self.scrollerStyle = .overlay - - } - - open override var scrollerStyle: NSScroller.Style { - set { - super.scrollerStyle = .overlay - } - get { - return .overlay - } - } - - -// -// open override var hasVerticalScroller: Bool { -// get { -// return true -// } -// set { -// super.hasVerticalScroller = newValue -// } -// } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - assertOnMainThread() - } - - -// override open func scrollWheel(with event: NSEvent) { -// NSLog("\(event)") -// var scrollPoint = self.contentView.bounds.origin -// // var isInverted = CBool(UserDefaults.standard.object(forKey: "com.apple.swipescrolldirection")!) -//// if !isInverted { -//// scrollPoint.x += (event.scrollingDeltaY() + event.scrollingDeltaX()) -//// } -//// else { -// scrollPoint.y -= (event.scrollingDeltaY + event.scrollingDeltaY) -// // } -// self.clipView.scroll(to: scrollPoint) -// } - - let dynamic:CGFloat = 100.0 - - open override func scrollWheel(with event: NSEvent) { - - if deltaCorner > 0 { - var origin = clipView.bounds.origin - - deltaCorner = max(Int64(floorToScreenPixels(frame.height / 6.0)),40) - - - - let deltaScrollY = min(max(Int64(event.scrollingDeltaY),-deltaCorner),deltaCorner) - - - // NSLog("\(event.deltaY)") - - if let cgEvent = event.cgEvent?.copy() { - - - - // cgEvent.setDoubleValueField(.scrollWheelEventDeltaAxis1, value: Double(min(max(-4,event.deltaY),4))) - - - //if delta == deltaCorner || delta == -deltaCorner || delta == 0 { - cgEvent.setIntegerValueField(.scrollWheelEventScrollCount, value: min(1,cgEvent.getIntegerValueField(.scrollWheelEventScrollCount))) - // } - cgEvent.setIntegerValueField(.scrollWheelEventPointDeltaAxis1, value: deltaScrollY) - // if event.scrollingDeltaY > 0 { - // - // } else { - // cgEvent.setIntegerValueField(.scrollWheelEventPointDeltaAxis1, value: ) - // } - - // NSLog("\(cgEvent.getIntegerValueField(.scrollWheelEventScrollCount)) == \(delta)") - - // cgEvent.setIntegerValueField(.scrollWheelEventPointDeltaAxis1, value: 10) - // cgEvent.setIntegerValueField(.scrollWheelEventScrollCount, value: Int64(delta)) - - let newEvent = NSEvent(cgEvent: cgEvent)! - - super.scrollWheel(with: newEvent) - - - } else { - //NSLog("\(cgEvent.getIntegerValueField(.scrollWheelEventScrollCount))") - - super.scrollWheel(with: event) - } - - - if origin == clipView.bounds.origin, abs(deltaScrollY) >= deltaCorner - { - - if let documentView = documentView, !(self is HorizontalTableView) { - - if frame.minY < origin.y - frame.height - 50 { - if origin.y > documentView.frame.maxY + dynamic { - clipView.scroll(to: NSMakePoint(origin.x, documentView.frame.minY)) - } - - if origin.y < documentView.frame.height { - if documentView.isFlipped { - if origin.y < documentView.frame.height - (frame.height + frame.minY) { - origin.y -= CGFloat(deltaScrollY) - clipView.scroll(to: origin) - reflectScrolledClipView(clipView) - } - } else { - if origin.y + frame.height < documentView.frame.height { - origin.y += CGFloat(deltaScrollY) - clipView.scroll(to: origin) - reflectScrolledClipView(clipView) - } - - } - } - } else if origin.y < -dynamic { - clipView.scroll(to: NSMakePoint(origin.x, 0)) - } - } - } - } else { - super.scrollWheel(with: event) - } - } - - - public func change(pos position: NSPoint, animated: Bool, _ save:Bool = true, removeOnCompletion: Bool = true, duration:Double = 0.2, timingFunction: String = kCAMediaTimingFunctionEaseOut, completion:((Bool)->Void)? = nil) -> Void { - super._change(pos: position, animated: animated, save, removeOnCompletion: removeOnCompletion, duration: duration, timingFunction: timingFunction, completion: completion) - } - - public func change(size: NSSize, animated: Bool, _ save:Bool = true, removeOnCompletion: Bool = true, duration:Double = 0.2, timingFunction: String = kCAMediaTimingFunctionEaseOut, completion:((Bool)->Void)? = nil) { - super._change(size: size, animated: animated, save, removeOnCompletion: removeOnCompletion, duration: duration, timingFunction: timingFunction, completion: completion) - } - public func change(opacity to: CGFloat, animated: Bool = true, _ save:Bool = true, removeOnCompletion: Bool = true, duration:Double = 0.2, timingFunction: String = kCAMediaTimingFunctionEaseOut, completion:((Bool)->Void)? = nil) { - super._change(opacity: to, animated: animated, save, removeOnCompletion: removeOnCompletion, duration: duration, timingFunction: timingFunction, completion: completion) - - } - -} diff --git a/TGUIKit/TGUIKit/SearchView.swift b/TGUIKit/TGUIKit/SearchView.swift deleted file mode 100644 index 4ef249035e..0000000000 --- a/TGUIKit/TGUIKit/SearchView.swift +++ /dev/null @@ -1,435 +0,0 @@ -// -// SearchView.swift -// TGUIKit -// -// Created by keepcoder on 27/09/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - - - - -class SearchTextField: NSTextView { - - - - override func resignFirstResponder() -> Bool { - self.delegate?.textDidEndEditing?(Notification(name: NSControl.textDidChangeNotification)) - return super.resignFirstResponder() - } - - override func becomeFirstResponder() -> Bool { - self.delegate?.textDidBeginEditing?(Notification(name: NSControl.textDidChangeNotification)) - return super.becomeFirstResponder() - } - -} - -public enum SearchFieldState { - case None; - case Focus; -} - -public struct SearchState : Equatable { - public let state:SearchFieldState - public let request:String - public init(state:SearchFieldState, request:String?) { - self.state = state - self.request = request ?? "" - } -} - -public func ==(lhs:SearchState, rhs:SearchState) -> Bool { - return lhs.state == rhs.state && lhs.request == rhs.request -} - -public final class SearchInteractions { - public let stateModified:(SearchState) -> Void - public let textModified:(SearchState) -> Void - - public init(_ state:@escaping(SearchState)->Void, _ text:@escaping(SearchState)->Void) { - stateModified = state - textModified = text - } -} - -open class SearchView: OverlayControl, NSTextViewDelegate { - - public private(set) var state:SearchFieldState = .None - - private(set) public var input:NSTextView = SearchTextField() - - private var lock:Bool = false - - private let clear:ImageButton = ImageButton() - private let search:ImageView = ImageView() - private let progressIndicator:ProgressIndicator = ProgressIndicator(frame: NSMakeRect(0, 0, 18, 18)) - private let placeholder:TextViewLabel = TextViewLabel() - - private let animateContainer:View = View() - - public let inset:CGFloat = 6 - public let leftInset:CGFloat = 10.0 - - public var searchInteractions:SearchInteractions? - - - - - public var isLoading:Bool = false { - didSet { - if oldValue != isLoading { - self.updateLoading() - needsLayout = true - } - } - } - - override open func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - - input.textColor = presentation.search.textColor - placeholder.attributedString = NSAttributedString.initialize(string: presentation.search.placeholder, color: presentation.search.placeholderColor, font: .normal(.text)) - placeholder.backgroundColor = presentation.search.backgroundColor - self.backgroundColor = presentation.search.backgroundColor - placeholder.sizeToFit() - search.frame = NSMakeRect(0, 0, presentation.search.searchImage.backingSize.width, presentation.search.searchImage.backingSize.height) - search.image = presentation.search.searchImage - animateContainer.setFrameSize(NSMakeSize(placeholder.frame.width + placeholderTextInset, max(placeholder.frame.height, search.frame.height))) - - clear.set(image: presentation.search.clearImage, for: .Normal) - clear.sizeToFit() - - placeholder.centerY(x: placeholderTextInset) - search.centerY() - input.insertionPointColor = presentation.search.textColor - - needsLayout = true - - } - - open var startTextInset: CGFloat { - return search.frame.width + inset - } - - open var placeholderTextInset: CGFloat { - return startTextInset - } - - required public init(frame frameRect: NSRect) { - super.init(frame: frameRect) - self.backgroundColor = .grayBackground - self.layer?.cornerRadius = .cornerRadius - - progressIndicator.isHidden = true -// progressIndicator.numberOfLines = 8 -// progressIndicator.innerMargin = 3; -// progressIndicator.widthOfLine = 3; -// progressIndicator.lengthOfLine = 6; - // input.isBordered = false - // input.isBezeled = false - input.focusRingType = .none - input.frame = self.bounds - input.autoresizingMask = [.width, .height] - input.backgroundColor = NSColor.clear - input.delegate = self - input.isRichText = false - - input.textContainer?.widthTracksTextView = true - input.textContainer?.heightTracksTextView = false - - // input.maxSize = NSMakeSize(100, .greatestFiniteMagnitude) - input.isHorizontallyResizable = false - input.isVerticallyResizable = false - - - //input.placeholderAttributedString = NSAttributedString.initialize(string: localizedString("SearchField.Search"), color: .grayText, font: .normal(.text), coreText: false) - - input.font = .normal(.text) - input.textColor = .text - input.isHidden = true - - - animateContainer.backgroundColor = .clear - - placeholder.sizeToFit() - animateContainer.addSubview(placeholder) - - animateContainer.addSubview(search) - - self.animateContainer.setFrameSize(NSMakeSize(NSWidth(placeholder.frame) + NSWidth(search.frame) + inset, max(NSHeight(placeholder.frame), NSHeight(search.frame)))) - - placeholder.centerY(nil, x: NSWidth(search.frame) + inset) - search.centerY() - - addSubview(animateContainer) - addSubview(input) - - - clear.backgroundColor = .clear - - - clear.set(handler: { [weak self] _ in - self?.cancelSearch() - }, for: .Click) - - addSubview(clear) - - clear.isHidden = true - - animateContainer.center() - - self.set(handler: {[weak self] (event) in - if let strongSelf = self { - if strongSelf.isEmpty { - strongSelf.change(state:strongSelf.state == .None ? .Focus : .None,true) - } - } - }, for: .Click) - - updateLocalizationAndTheme() - } - - open func cancelSearch() { - change(state: .None, true) - } - - open func textView(_ textView: NSTextView, shouldChangeTextIn affectedCharRange: NSRange, replacementString: String?) -> Bool { - if let replacementString = replacementString { - return !replacementString.contains("\n") && !replacementString.contains("\r") - } - return false - } - - - - open func textDidChange(_ notification: Notification) { - - if let searchInteractions = searchInteractions { - searchInteractions.textModified(SearchState(state: state, request: input.string.trimmingCharacters(in: CharacterSet(charactersIn: "\n\r")))) - } - let pHidden = !input.string.isEmpty - if placeholder.isHidden != pHidden { - placeholder.isHidden = pHidden - } - let iHidden = !(state == .Focus && !input.string.isEmpty) - if input.isHidden != iHidden { - // input.isHidden = iHidden - window?.makeFirstResponder(input) - } - - } - - open func textDidEndEditing(_ notification: Notification) { - didResignResponder() - } - - open func textDidBeginEditing(_ notification: Notification) { - didBecomeResponder() - } - - open var isEmpty: Bool { - return query.isEmpty - } - - open func didResignResponder() { - if isEmpty { - change(state: .None, true) - } - self.kitWindow?.removeAllHandlers(for: self) - self.kitWindow?.removeObserver(for: self) - } - - open func didBecomeResponder() { - change(state: .Focus, true) - - self.kitWindow?.set(escape: {[weak self] () -> KeyHandlerResult in - if let strongSelf = self { - return strongSelf.changeResponder() ? .invoked : .rejected - } - return .rejected - - }, with: self, priority: .high) - - self.kitWindow?.set(responder: {[weak self] () -> NSResponder? in - return self?.input - }, with: self, priority: .high) - } - - - open override func draw(_ layer: CALayer, in ctx: CGContext) { - super.draw(layer, in: ctx) - } - - - - open func change(state:SearchFieldState, _ animated:Bool) -> Void { - - if state != self.state && !lock { - self.state = state - - if let searchInteractions = searchInteractions { - let text = input.string.trimmingCharacters(in: CharacterSet(charactersIn: "\n\r")) - searchInteractions.stateModified(SearchState(state: state, request: state == .None ? nil : text)) - } - - lock = true - - if state == .Focus { - - window?.makeFirstResponder(input) - - let inputInset = placeholderTextInset + 5 - - let fromX:CGFloat = animateContainer.frame.minX - animateContainer.centerY(x: leftInset) - - - self.input.frame = NSMakeRect(inputInset, NSMinY(self.animateContainer.frame), NSWidth(self.frame) - inputInset - inset - clear.frame.width, NSHeight(placeholder.frame)) - - if animated { - animateContainer.layer?.animate(from: fromX as NSNumber, to: leftInset as NSNumber, keyPath: "position.x", timingFunction: animationStyle.function, duration: animationStyle.duration, removeOnCompletion: true, additive: false, completion: {[weak self] (complete) in - self?.input.isHidden = false - self?.window?.makeFirstResponder(self?.input) - self?.lock = false - }) - } else { - self.input.isHidden = false - self.window?.makeFirstResponder(self.input) - self.lock = false - } - - - clear.isHidden = false - clear.layer?.opacity = 1.0 - if animated { - clear.layer?.animate(from: 0.0 as NSNumber, to: 1.0 as NSNumber, keyPath: "opacity", timingFunction: animationStyle.function, duration: animationStyle.duration) - } - } - - if state == .None { - - self.kitWindow?.removeAllHandlers(for: self) - self.kitWindow?.removeObserver(for: self) - - self.input.isHidden = true - self.input.string = "" - self.window?.makeFirstResponder(nil) - self.placeholder.isHidden = false - - animateContainer.center() - if animated { - animateContainer.layer?.animate(from: leftInset as NSNumber, to: NSMinX(animateContainer.frame) as NSNumber, keyPath: "position.x", timingFunction: animationStyle.function, duration: animationStyle.duration, removeOnCompletion: true) - - clear.layer?.animate(from: 1.0 as NSNumber, to: 0.0 as NSNumber, keyPath: "opacity", timingFunction: animationStyle.function, duration: animationStyle.duration, removeOnCompletion:true, additive:false, completion: {[weak self] (complete) in - self?.clear.isHidden = true - self?.lock = false - }) - } else { - clear.isHidden = true - lock = false - } - - clear.layer?.opacity = 0.0 - } - self.needsLayout = true - } - - } - - open override func viewDidMoveToSuperview() { - guard let _ = superview else { - return - } - self.kitWindow?.removeAllHandlers(for: self) - self.kitWindow?.removeObserver(for: self) - } - - func updateLoading() { - if isLoading { - if progressIndicator.superview == nil { - addSubview(progressIndicator) - } - progressIndicator.isHidden = false - clear.isHidden = true - rightAccessory.isHidden = true - progressIndicator.animates = true - } else { - progressIndicator.animates = false - progressIndicator.removeFromSuperview() - progressIndicator.isHidden = true - clear.isHidden = self.state == .None || !clearVisibility - rightAccessory.isHidden = self.state == .None - } - if window?.firstResponder == input { - window?.makeFirstResponder(input) - } - } - private var clearVisibility: Bool = true - - public func updateClearVisibility(_ visible: Bool) { - clearVisibility = visible - clear.isHidden = !visible || isLoading - } - - open var rightAccessory: NSView { - return clear - } - - - open override func layout() { - super.layout() - switch state { - case .None: - animateContainer.center() - case .Focus: - animateContainer.centerY(x: leftInset) - } - clear.centerY(x: frame.width - inset - clear.frame.width) - progressIndicator.centerY(x: frame.width - inset - progressIndicator.frame.width) - - self.input.setFrameOrigin(placeholderTextInset + 5, animateContainer.frame.minY) - - } - - public func changeResponder(_ animated:Bool = true) -> Bool { - if state == .Focus { - cancelSearch() - } else { - change(state: .Focus, animated) - } - return true - } - - deinit { - self.kitWindow?.removeAllHandlers(for: self) - self.kitWindow?.removeObserver(for: self) - } - - public var query:String { - return self.input.string - } - - public override func change(size: NSSize, animated: Bool = true, _ save: Bool = true, removeOnCompletion: Bool = false, duration: Double = 0.2, timingFunction: String = kCAMediaTimingFunctionEaseOut, completion: ((Bool) -> Void)? = nil) { - super.change(size: size, animated: animated, save, duration: duration, timingFunction: timingFunction) - clear.change(pos: NSMakePoint(frame.width - inset - clear.frame.width, clear.frame.minY), animated: animated) - } - - - public func setString(_ string:String) { - self.input.string = string - textDidChange(Notification(name: NSText.didChangeNotification)) - needsLayout = true - } - - public func cancel(_ animated:Bool) -> Void { - change(state: .None, animated) - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - -} diff --git a/TGUIKit/TGUIKit/SectionViewController.swift b/TGUIKit/TGUIKit/SectionViewController.swift deleted file mode 100644 index 868dc8e95f..0000000000 --- a/TGUIKit/TGUIKit/SectionViewController.swift +++ /dev/null @@ -1,245 +0,0 @@ -// -// SectionViewController.swift -// TGUIKit -// -// Created by keepcoder on 03/08/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - -import Cocoa -import SwiftSignalKitMac - -private final class SectionControllerArguments { - let select:(Int)->Void - init(select:@escaping(Int)->Void) { - self.select = select - } -} - -public class SectionControllerView : View { - private var header: View = View() - private let selector:View = View() - private let container: View = View() - private weak var current: ViewController? - - fileprivate var selectorIndex:Int = 0 { - didSet { - if selectorIndex != oldValue { - var index:Int = 0 - for hContainer in header.subviews { - for t in hContainer.subviews { - if let t = t as? TextView { - t.update(TextViewLayout(.initialize(string: t.layout?.attributedString.string, color: selectorIndex == index ? presentation.colors.blueUI : presentation.colors.grayText, font: .medium(.title)), maximumNumberOfLines: 1, truncationType: .middle)) - } - - } - index += 1 - } - } - } - } - public required init(frame frameRect: NSRect) { - super.init(frame: frameRect) - addSubview(header) - addSubview(container) - addSubview(selector) - updateLocalizationAndTheme() - needsLayout = true - } - - fileprivate func layout(sections: [SectionControllerItem], selected: Int, arguments: SectionControllerArguments) { - header.removeAllSubviews() - self.selectorIndex = selected - for i in 0 ..< sections.count { - let section = sections[i] - let headerContainer = Control(frame: NSMakeRect(0, 0, 0, 50)) - let title: TextView = TextView() - title.isSelectable = false - title.userInteractionEnabled = false - title.update(TextViewLayout(.initialize(string: section.title, color: i == selected ? presentation.colors.blueUI : presentation.colors.grayText, font: .medium(.title)), maximumNumberOfLines: 1, truncationType: .middle)) - headerContainer.addSubview(title) - header.addSubview(headerContainer) - headerContainer.border = [.Bottom] - headerContainer.set(handler: { _ in - arguments.select(i) - }, for: .Click) - } - needsLayout = true - } - - fileprivate func select(controller: ViewController, index: Int, animated: Bool) { - let previousIndex = self.selectorIndex - let previous = self.current - self.current = controller - selectorIndex = index - - controller.view.frame = container.bounds - previous?.viewWillDisappear(animated) - - container.addSubview(controller.view) - - if animated { - CATransaction.begin() - let container = header.subviews[index] - selector.change(pos: NSMakePoint(container.frame.minX, selector.frame.minY), animated: animated, timingFunction: kCAMediaTimingFunctionSpring) - - - let pto: NSPoint - let nfrom: NSPoint - - - if previousIndex < index { - pto = NSMakePoint(-container.frame.width, 0) - nfrom = NSMakePoint(container.frame.width, 0) - } else { - pto = NSMakePoint(container.frame.width, 0) - nfrom = NSMakePoint(-container.frame.width, 0) - } - - previous?.view._change(pos: pto, animated: animated, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak previous] complete in - if complete { - previous?.view.removeFromSuperview() - previous?.viewDidDisappear(animated) - } - }) - controller.view.layer?.animatePosition(from: nfrom, to: NSZeroPoint, timingFunction: kCAMediaTimingFunctionSpring) - CATransaction.commit() - } else { - container.removeAllSubviews() - previous?.viewDidDisappear(animated) - container.addSubview(controller.view) - controller.viewDidAppear(animated) - } - needsLayout = true - } - - public override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - self.backgroundColor = presentation.colors.background - selector.backgroundColor = presentation.colors.blueUI - container.backgroundColor = presentation.colors.background - var index:Int = 0 - for hContainer in header.subviews { - hContainer.background = presentation.colors.background - for t in hContainer.subviews { - if let t = t as? TextView { - t.update(TextViewLayout(.initialize(string: t.layout?.attributedString.string, color: selectorIndex == index ? presentation.colors.blueUI : presentation.colors.grayText, font: .medium(.title)), maximumNumberOfLines: 1, truncationType: .middle)) - } - - t.background = presentation.colors.background - } - index += 1 - } - } - - public override func layout() { - super.layout() - header.setFrameSize(NSMakeSize(frame.width, 50)) - let width = floorToScreenPixels(frame.width / CGFloat(max(header.subviews.count, 3))) - - selector.frame = NSMakeRect(CGFloat(selectorIndex) * width, 50 - .borderSize, width, .borderSize) - container.frame = NSMakeRect(0, header.frame.maxY, frame.width, frame.height - header.frame.height) - container.subviews.first?.frame = container.bounds - - var x:CGFloat = 0 - for i in 0 ..< header.subviews.count { - let hContainer = header.subviews[i] - let width = i == header.subviews.count - 1 ? frame.width - x : width - hContainer.frame = NSMakeRect(x, 0, width, hContainer.frame.height) - if let textView = hContainer.subviews.first as? TextView { - textView.layout?.measure(width: width - 10) - textView.update(textView.layout) - textView.center() - } - x += width - } - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -public class SectionControllerItem { - let title: String - let controller: ViewController - public init(title: String, controller: ViewController) { - self.title = title - self.controller = controller - } -} - - -public class SectionViewController: GenericViewController { - - private var sections:[SectionControllerItem] = [] - public var selectedSection:SectionControllerItem - public private(set) var selectedIndex: Int = -1 - private let disposable = MetaDisposable() - - public var selectionUpdateHandler:((Int)->Void)? - - public func addSection(_ section: SectionControllerItem) { - sections.append(section) - } - - deinit { - disposable.dispose() - } - - fileprivate func select(_ index:Int, _ animated: Bool) { - if selectedIndex != index || !animated { - selectedSection = sections[index] - sections[index].controller._frameRect = NSMakeRect(0, 0, frame.width, frame.height - 50) - sections[index].controller.loadViewIfNeeded() - let controller = sections[index].controller - selectedIndex = index - selectionUpdateHandler?(index) - sections[index].controller.viewWillAppear(animated) - disposable.set((sections[index].controller.ready.get() |> filter {$0} |> take(1)).start(next: { [weak self, weak controller] ready in - if let strongSelf = self, let controller = controller { - strongSelf.genericView.select(controller: controller, index: index, animated: animated) - } - })) - } - } - override public func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - selectedSection.controller._frameRect = NSMakeRect(0, 0, frame.width, frame.height - 50) - selectedSection.controller.viewWillAppear(animated) - self.ready.set(sections[selectedIndex].controller.ready.get()) - } - override public func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - selectedSection.controller.viewWillDisappear(animated) - } - - override public func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - selectedSection.controller.viewDidAppear(animated) - } - override public func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - selectedSection.controller.viewDidDisappear(animated) - } - - public override func viewDidLoad() { - super.viewDidLoad() - - let arguments = SectionControllerArguments { [weak self] index in - self?.select(index, true) - } - genericView.layout(sections: sections, selected: selectedIndex, arguments: arguments) - select(selectedIndex, false) - } - - public init(sections: [SectionControllerItem], selected: Int = 0) { - assert(!sections.isEmpty) - self.sections = sections - self.selectedSection = sections[selected] - self.selectedIndex = selected - super.init() - bar = .init(height: 0) - } - -} diff --git a/TGUIKit/TGUIKit/SegmentedControl.swift b/TGUIKit/TGUIKit/SegmentedControl.swift deleted file mode 100644 index d3002c5012..0000000000 --- a/TGUIKit/TGUIKit/SegmentedControl.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// SegmentedControl.swift -// TGUIKit -// -// Created by keepcoder on 01.06.17. -// Copyright © 2017 Telegram. All rights reserved. -// - -import Cocoa - -public class SegmentedItem { - let title:String - init(title:String) { - self.title = title - } -} - -public class SegmentedControl: View { - - - - override public func draw(_ dirtyRect: NSRect) { - super.draw(dirtyRect) - - // Drawing code here. - } - - public func addItem() { - - } - -} diff --git a/TGUIKit/TGUIKit/SelectingControl.swift b/TGUIKit/TGUIKit/SelectingControl.swift deleted file mode 100644 index c7408f6a20..0000000000 --- a/TGUIKit/TGUIKit/SelectingControl.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// SelectingControl.swift -// TGUIKit -// -// Created by keepcoder on 27/10/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - -public class SelectingControl: Control { - - - private var imageView:ImageView = ImageView() - - private var unselectedImage:CGImage - private var selectedImage:CGImage - - public init(unselectedImage:CGImage, selectedImage:CGImage) { - self.unselectedImage = unselectedImage - self.selectedImage = selectedImage - imageView.image = unselectedImage - super.init(frame:NSMakeRect(0, 0, max(unselectedImage.backingSize.width ,selectedImage.backingSize.width ), max(unselectedImage.backingSize.height,selectedImage.backingSize.height ))) - userInteractionEnabled = false - - addSubview(imageView) - - } - - public override func layout() { - super.layout() - imageView.setFrameSize(unselectedImage.backingSize) - imageView.center() - } - - public func set(selected:Bool, animated:Bool = false) { - if selected != isSelected { - self.isSelected = selected - imageView.image = selected ? selectedImage : unselectedImage - if animated { - self.layer?.animateScaleSpring(from: 0.1, to: 1.0, duration: 0.4) - } - } - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - required public init(frame frameRect: NSRect) { - fatalError("init(frame:) has not been implemented") - } - - override public func draw(_ dirtyRect: NSRect) { - super.draw(dirtyRect) - - // Drawing code here. - } - -} diff --git a/TGUIKit/TGUIKit/ShadowView.swift b/TGUIKit/TGUIKit/ShadowView.swift deleted file mode 100644 index 3f7b3f7c8e..0000000000 --- a/TGUIKit/TGUIKit/ShadowView.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// ShadowView.swift -// TGUIKit -// -// Created by keepcoder on 02/08/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - -import Cocoa - -public class ShadowView: View { - - override public func draw(_ layer: CALayer, in ctx: CGContext) { - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: NSArray(array: [backgroundColor.withAlphaComponent(0).cgColor, backgroundColor.cgColor]), locations: nil)! - - ctx.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: layer.bounds.height), options: CGGradientDrawingOptions()) - } - -} diff --git a/TGUIKit/TGUIKit/System.swift b/TGUIKit/TGUIKit/System.swift deleted file mode 100644 index 7e45fd79de..0000000000 --- a/TGUIKit/TGUIKit/System.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// System.swift -// TGUIKit -// -// Created by keepcoder on 08/09/16. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - -public struct System { - - public static var isRetina:Bool { - get { - return NSScreen.main?.backingScaleFactor == 2.0 - } - } - - public static var backingScale:CGFloat { - return CGFloat(NSScreen.main?.backingScaleFactor ?? 2.0) - } - - public static var drawAsync:Bool { - return false - } - -} - -public var uiLocalizationFunc:((String)->String)? - -public func localizedString(_ key:String) -> String { - if let uiLocalizationFunc = uiLocalizationFunc { - return uiLocalizationFunc(key) - } else { - return NSLocalizedString(key, comment: "") - } -} - -//public func localizedString(_ key:String, countable:Int = 0, apply:Bool = true) -> String { -// let suffix:String -// if countable == 1 { -// suffix = ".singular" -// } else if countable > 1 { -// suffix = ".pluar" -// } else { -// suffix = ".zero" -// } -// if apply { -// return String(format: localizedString(key + suffix), countable) -// } else { -// return localizedString(key + suffix) -// } -//} - -public func reverseIndexList(_ list:[(Int,T,Int?)], _ previousCount:Int, _ updateCount:Int) -> [(Int,T,Int?)] { - var reversed:[(Int,T,Int?)] = [] - - for (int1,obj,int2) in list.reversed() { - if let s = int2 { - reversed.append((updateCount - int1 - 1,obj, previousCount - s - 1)) - } else { - reversed.append((updateCount - int1 - 1,obj, nil)) - } - } - return reversed -} - -public func reverseIndexList(_ list:[(Int,T)], _ previousCount:Int, _ updateCount:Int) -> [(Int,T)] { - var reversed:[(Int,T)] = [] - - for (int1,obj) in list.reversed() { - reversed.append((updateCount - int1 - 1,obj)) - } - return reversed -} - -public func reverseIndexList(_ list:[(Int,T,Int)], _ previousCount:Int, _ updateCount:Int) -> [(Int,T,Int)] { - var reversed:[(Int,T,Int)] = [] - - for (int1,obj,int2) in list.reversed() { - reversed.append((updateCount - int1 - 1,obj, previousCount - int2 - 1)) - } - return reversed -} - -public func reverseIndexList(_ list:[Int], _ count:Int) -> [Int] { - var reversed:[(Int)] = [] - for int1 in list.reversed() { - reversed.append(count - int1 - 1) - } - return reversed -} diff --git a/TGUIKit/TGUIKit/TGColor.swift b/TGUIKit/TGUIKit/TGColor.swift deleted file mode 100644 index 6ee56afd31..0000000000 --- a/TGUIKit/TGUIKit/TGColor.swift +++ /dev/null @@ -1,201 +0,0 @@ -// -// Color.swift -// TGUIKit -// -// Created by keepcoder on 06/09/16. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Foundation - -public extension NSColor { - - public static func colorFromRGB(rgbValue:UInt32) ->NSColor { - return NSColor.init(deviceRed: ((CGFloat)((rgbValue & 0xFF0000) >> 16))/255.0, green: ((CGFloat)((rgbValue & 0xFF00) >> 8))/255.0, blue: ((CGFloat)(rgbValue & 0xFF))/255.0, alpha: 1.0) - } - - public static func colorFromRGB(rgbValue:UInt32, alpha:CGFloat) ->NSColor { - return NSColor.init(deviceRed: ((CGFloat)((rgbValue & 0xFF0000) >> 16))/255.0, green: ((CGFloat)((rgbValue & 0xFF00) >> 8))/255.0, blue: ((CGFloat)(rgbValue & 0xFF))/255.0, alpha:alpha) - } - - - public static var link:NSColor { - return .colorFromRGB(rgbValue: 0x2481cc) - } - - public static var blueUI:NSColor { - return .colorFromRGB(rgbValue: 0x2481cc) - } - - public static var redUI:NSColor { - return colorFromRGB(rgbValue: 0xff3b30) - } - - public static var greenUI:NSColor { - return colorFromRGB(rgbValue: 0x63DA6E) - } - - public static var blackTransparent:NSColor { - return colorFromRGB(rgbValue: 0x000000, alpha: 0.6) - } - - public static var grayTransparent:NSColor { - return colorFromRGB(rgbValue: 0xf4f4f4, alpha: 0.4) - } - - public static var grayUI:NSColor { - return colorFromRGB(rgbValue: 0xFaFaFa, alpha: 1.0) - } - - public static var darkGrayText:NSColor { - return NSColor(0x333333) - } - - public static var text:NSColor { - return NSColor.black - } - - - public static var blueText:NSColor { - get { - return colorFromRGB(rgbValue: 0x4ba3e2) - } - } - - public static var blueSelect:NSColor { - get { - return colorFromRGB(rgbValue: 0x4c91c7) - } - } - - - public static var selectText:NSColor { - get { - return colorFromRGB(rgbValue: 0xeaeaea, alpha:1.0) - } - } - - public static var random:NSColor { - get { - return colorFromRGB(rgbValue: arc4random_uniform(16000000)) - } - } - - public static var blueFill:NSColor { - get { - return colorFromRGB(rgbValue: 0x4ba3e2) - } - } - - - public static var border:NSColor { - get { - return colorFromRGB(rgbValue: 0xeaeaea) - } - } - - - - public static var grayBackground:NSColor { - get { - return colorFromRGB(rgbValue: 0xf4f4f4) - } - } - - public static var grayForeground:NSColor { - get { - return colorFromRGB(rgbValue: 0xe4e4e4) - } - } - - - - public static var grayIcon:NSColor { - get { - return colorFromRGB(rgbValue: 0x9e9e9e) - } - } - - - public static var blueIcon:NSColor { - get { - return colorFromRGB(rgbValue: 0x0f8fe4) - } - } - - public static var badgeMuted:NSColor { - get { - return colorFromRGB(rgbValue: 0xd7d7d7) - } - } - - public static var badge:NSColor { - get { - return .blueFill - } - } - - public static var grayText:NSColor { - get { - return colorFromRGB(rgbValue: 0x999999) - } - } -} - -public extension NSColor { - 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) - } - - convenience init(rgb: UInt32, alpha: CGFloat) { - self.init(red: CGFloat((rgb >> 16) & 0xff) / 255.0, green: CGFloat((rgb >> 8) & 0xff) / 255.0, blue: CGFloat(rgb & 0xff) / 255.0, alpha: alpha) - } - - convenience init(argb: UInt32) { - self.init(red: CGFloat((argb >> 16) & 0xff) / 255.0, green: CGFloat((argb >> 8) & 0xff) / 255.0, blue: CGFloat(argb & 0xff) / 255.0, alpha: CGFloat((argb >> 24) & 0xff) / 255.0) - } - - var argb: UInt32 { - - let color = self.usingColorSpaceName(NSColorSpaceName.calibratedRGB)! - var red: CGFloat = 0.0 - var green: CGFloat = 0.0 - var blue: CGFloat = 0.0 - var alpha: CGFloat = 0.0 - color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) - - return (UInt32(alpha * 255.0) << 24) | (UInt32(red * 255.0) << 16) | (UInt32(green * 255.0) << 8) | (UInt32(blue * 255.0)) - } - - var rgb: UInt32 { - - let color = self.usingColorSpaceName(NSColorSpaceName.calibratedRGB) - if let color = color { - let red: CGFloat = color.redComponent - let green: CGFloat = color.greenComponent - let blue: CGFloat = color.blueComponent - - return (UInt32(red * 255.0) << 16) | (UInt32(green * 255.0) << 8) | (UInt32(blue * 255.0)) - } - return 0x000000 - } -} - -public extension CGFloat { - - - public static var cornerRadius:CGFloat { - return 5 - } - - public static var borderSize:CGFloat { - get { - return 1 - } - } - - - -} - - diff --git a/TGUIKit/TGUIKit/TGFont.swift b/TGUIKit/TGUIKit/TGFont.swift deleted file mode 100644 index e904734ccc..0000000000 --- a/TGUIKit/TGUIKit/TGFont.swift +++ /dev/null @@ -1,140 +0,0 @@ -// -// TGFont.swift -// TGUIKit -// -// Created by keepcoder on 07/09/16. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - - -public func systemFont(_ size:CGFloat) ->NSFont { - - if #available(OSX 10.11, *) { - return NSFont.systemFont(ofSize: size, weight: NSFont.Weight.regular) - } else { - return NSFont.init(name: "HelveticaNeue", size: size)! - } -} - -public func systemMediumFont(_ size:CGFloat) ->NSFont { - - if #available(OSX 10.11, *) { - return NSFont.systemFont(ofSize: size, weight: NSFont.Weight.semibold) - } else { - return NSFont.init(name: "HelveticaNeue-Medium", size: size)! - } - -} - -public func systemBoldFont(_ size:CGFloat) ->NSFont { - - if #available(OSX 10.11, *) { - return NSFont.systemFont(ofSize: size, weight: NSFont.Weight.bold) - } else { - return NSFont.init(name: "HelveticaNeue-Bold", size: size)! - } -} - -public extension NSFont { - public static func normal(_ size:FontSize) ->NSFont { - - if #available(OSX 10.11, *) { - return NSFont.systemFont(ofSize: convert(from:size), weight: NSFont.Weight.regular) - } else { - return NSFont(name: "HelveticaNeue", size: convert(from:size))! - } - } - - public static func italic(_ size: FontSize) -> NSFont { - return NSFontManager.shared.convert(.normal(size), toHaveTrait: .italicFontMask) - } - - public static func avatar(_ size: FontSize) -> NSFont { - - if let font = NSFont(name: ".SFCompactRounded-Semibold", size: convert(from:size)) { - return font - } else { - return .medium(size) - } - } - - public static func medium(_ size:FontSize) ->NSFont { - - if #available(OSX 10.11, *) { - return NSFont.systemFont(ofSize: convert(from:size), weight: NSFont.Weight.medium) - } else { - return NSFont(name: "HelveticaNeue-Medium", size: convert(from:size))! - } - - } - - public static func bold(_ size:FontSize) ->NSFont { - - if #available(OSX 10.11, *) { - return NSFont.systemFont(ofSize: convert(from:size), weight: NSFont.Weight.bold) - } else { - return NSFont(name: "HelveticaNeue-Bold", size: convert(from:size))! - } - } - - public static func code(_ size:FontSize) ->NSFont { - return NSFont(name: "Menlo-Regular", size: convert(from:size)) ?? NSFont.systemFont(ofSize: 17.0) - } -} - -public enum FontSize { - case small - case short - case text - case title - case header - case huge - case custom(CGFloat) -} - -fileprivate func convert(from s:FontSize) -> CGFloat { - switch s { - case .small: - return 11.0 - case .short: - return 12.0 - case .text: - return 13.0 - case .title: - return 14.0 - case .header: - return 15.0 - case .huge: - return 18.0 - case let .custom(size): - return size - } -} - - - -public struct TGFont { - - public static var shortSize:CGFloat { - return 12 - } - - public static var textSize:CGFloat { - return 13 - } - - public static var headerSize:CGFloat { - return 15 - } - - public static var titleSize:CGFloat { - return 14 - } - - public static var hugeSize:CGFloat { - return 18 - } - -} diff --git a/TGUIKit/TGUIKit/TGSplitView.swift b/TGUIKit/TGUIKit/TGSplitView.swift deleted file mode 100644 index e55816706f..0000000000 --- a/TGUIKit/TGUIKit/TGSplitView.swift +++ /dev/null @@ -1,386 +0,0 @@ -// -// SplitView.swift -// TGUIKit -// -// Created by keepcoder on 06/09/16. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Foundation -import SwiftSignalKitMac -fileprivate class SplitMinimisizeView : Control { - - private var startPoint:NSPoint = NSZeroPoint - weak var splitView:SplitView? - override init() { - super.init() - userInteractionEnabled = false - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - required public init(frame frameRect: NSRect) { - fatalError("init(frame:) has not been implemented") - } - - fileprivate override func mouseMoved(with event: NSEvent) { - super.mouseMoved(with: event) - checkCursor() - } - - func checkCursor() { - if let splitView = splitView { - if let minimisize = splitView.delegate?.splitViewIsCanMinimisize(), minimisize { - if mouseInside() { - if splitView.state == .minimisize { - NSCursor.resizeRight.set() - } else { - NSCursor.resizeLeft.set() - } - } else { - NSCursor.arrow.set() - } - } - } - } - - fileprivate override func mouseEntered(with event: NSEvent) { - super.mouseEntered(with: event) - checkCursor() - } - - - - fileprivate override func mouseExited(with event: NSEvent) { - super.mouseExited(with: event) - checkCursor() - } - - fileprivate override func mouseDragged(with event: NSEvent) { - super.mouseDragged(with: event) - - if let splitView = splitView { - if let minimisize = splitView.delegate?.splitViewIsCanMinimisize(), minimisize { - if splitView.state == .minimisize { - NSCursor.resizeRight.set() - } else { - NSCursor.resizeLeft.set() - } - - let current = splitView.convert(event.locationInWindow, from: nil) - - - if startPoint.x - current.x >= 100, splitView.state != .minimisize { - splitView.needMinimisize() - startPoint = current - } else if current.x - startPoint.x >= 100, splitView.state == .minimisize { - splitView.needFullsize() - startPoint = current - } - } - - } - } - - fileprivate override func mouseUp(with event: NSEvent) { - startPoint = NSZeroPoint - } - - fileprivate override func mouseDown(with event: NSEvent) { - if let splitView = splitView { - startPoint = splitView.convert(event.locationInWindow, from: nil) - } - } - - override func draw(_ layer: CALayer, in ctx: CGContext) { - super.draw(layer, in: ctx) - - if let splitView = splitView { - if let drawBorder = splitView.delegate?.splitViewDrawBorder(), drawBorder { - ctx.setFillColor(presentation.colors.border.cgColor) - ctx.fill(NSMakeRect(floorToScreenPixels(frame.width / 2), 0, .borderSize, frame.height)) - } - } - } -} - -public struct SplitProportion { - var min:CGFloat = 0; - var max:CGFloat = 0; - - public init(min:CGFloat, max:CGFloat) { - self.min = min; - self.max = max; - } -} - -public enum SplitViewState : Int { - case none = -1; - case single = 0; - case dual = 1; - case triple = 2; - case minimisize = 3 -} - - -public protocol SplitViewDelegate : class { - func splitViewDidNeedSwapToLayout(state:SplitViewState) -> Void - func splitViewDidNeedMinimisize(controller:ViewController) -> Void - func splitViewDidNeedFullsize(controller:ViewController) -> Void - func splitViewIsCanMinimisize() -> Bool - func splitViewDrawBorder() -> Bool -} - - -public class SplitView : View { - - private let minimisizeOverlay:SplitMinimisizeView = SplitMinimisizeView() - private let container:View - private var forceNotice:Bool = false - public var state: SplitViewState = .none { - didSet { - let notify:Bool = state != oldValue; - assert(notify); - if(notify) { - self.delegate?.splitViewDidNeedSwapToLayout(state: state); - } - } - } - - - public var canChangeState:Bool = true; - public weak var delegate:SplitViewDelegate? - - - private var _proportions:[Int:SplitProportion] = [Int:SplitProportion]() - private var _startSize:[Int:NSSize] = [Int:NSSize]() - private var _controllers:[ViewController] = [ViewController]() - private var _issingle:Bool? - private var _layoutProportions:[SplitViewState:SplitProportion] = [SplitViewState:SplitProportion]() - - private var _splitIdx:Int? - - - public override func draw(_ dirtyRect: NSRect) { - super.draw(dirtyRect) - } - - required public init(frame frameRect: NSRect) { - container = View(frame: NSMakeRect(0,0,frameRect.width, frameRect.height)) - super.init(frame: frameRect); - self.autoresizingMask = [.width, .height] - self.autoresizesSubviews = true - container.autoresizesSubviews = false - container.autoresizingMask = [.width, .height] - addSubview(container) - minimisizeOverlay.splitView = self - - } - - public override var backgroundColor: NSColor { - didSet { - container.backgroundColor = backgroundColor - } - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - - public func addController(controller:ViewController, proportion:SplitProportion) ->Void { - controller._frameRect = NSMakeRect(0, 0, proportion.min, frame.height) - container.addSubview(controller.view); - controller.viewWillAppear(false) - _controllers.append(controller); - _startSize.updateValue(controller.view.frame.size, forKey: controller.internalId); - _proportions.updateValue(proportion, forKey: controller.internalId) - controller.viewDidAppear(false) - } - - func removeController(controller:ViewController) -> Void { - - controller.viewWillDisappear(false) - let idx = _controllers.index(of: controller)!; - container.subviews[idx].removeFromSuperview(); - _controllers.remove(at: idx); - _startSize.removeValue(forKey: controller.internalId); - _proportions.removeValue(forKey: controller.internalId); - - controller.viewDidDisappear(false) - } - - public func removeAllControllers() -> Void { - - var copy:[ViewController] = [] - - - for controller in _controllers { - copy.append(controller) - } - - for controller in copy { - controller.viewWillDisappear(false) - } - - container.removeAllSubviews(); - _controllers.removeAll(); - _startSize.removeAll(); - _proportions.removeAll(); - - for controller in copy { - controller.viewDidDisappear(false) - } - } - - public func setProportion(proportion:SplitProportion, state:SplitViewState) -> Void { - _layoutProportions[state] = proportion; - } - - public func removeProportion(state:SplitViewState) -> Void { - _layoutProportions.removeValue(forKey: state); - if(_controllers.count > state.rawValue) { - _controllers.remove(at: state.rawValue) - } - } - - public func updateStartSize(size:NSSize, controller:ViewController) -> Void { - _startSize[controller.internalId] = size; - - _proportions[controller.internalId] = SplitProportion(min:size.width, max:size.height); - - update(); - - } - - public func update(_ forceNotice:Bool = false) -> Void { - Queue.mainQueue().justDispatch { - self.forceNotice = forceNotice - self.needsLayout = true - } - } - - public override func layout() { - super.layout() - - //assert(!_controllers.isEmpty) - - let single:SplitProportion! = _layoutProportions[.single] - let dual:SplitProportion! = _layoutProportions[.dual] - let triple:SplitProportion! = _layoutProportions[.triple] - - - - if acceptLayout(prop: single) && canChangeState && state != .minimisize { - if frame.width < single.max { - if self.state != .single { - self.state = .single; - } - } else if acceptLayout(prop: dual) { - if acceptLayout(prop: triple) { - if frame.width >= dual.min && frame.width <= dual.max { - if state != .dual { - state = .dual; - } - } else if state != .triple { - self.state = .triple; - } - } else { - if state != .dual && frame.width >= dual.min { - self.state = .dual; - } - } - - } - } - - if forceNotice { - forceNotice = false - self.delegate?.splitViewDidNeedSwapToLayout(state: state) - } - - var x:CGFloat = 0; - - for (index, obj) in _controllers.enumerated() { - - let proportion:SplitProportion = _proportions[obj.internalId]!; - let startSize:NSSize = _startSize[obj.internalId]!; - var size:NSSize = NSMakeSize(x, frame.height); - var min:CGFloat = startSize.width; - - - - min = proportion.min; - - if(proportion.max == CGFloat.greatestFiniteMagnitude && index != _controllers.count-1) { - - var m2:CGFloat = 0; - - for i:Int in index + 1 ..< _controllers.count - index { - - let split:ViewController = _controllers[i]; - - let proportion:SplitProportion = _proportions[split.internalId]!; - - m2+=proportion.min; - } - - min = frame.width - x - m2; - - } - - - if(index == _controllers.count - 1) { - min = frame.width - x; - } - - size = NSMakeSize(x + min > frame.width ? (frame.width - x) : min, frame.height); - - let rect:NSRect = NSMakeRect(x, 0, size.width, size.height); - - if(!NSEqualRects(rect, obj.view.frame)) { - obj.view.frame = rect; - } - - x+=size.width; - - } - - //assert(state != .none) - if state != .none { - if state == .dual || state == .minimisize { - if let first = container.subviews.first { - if minimisizeOverlay.superview == nil { - addSubview(minimisizeOverlay) - } - minimisizeOverlay.frame = NSMakeRect(first.frame.maxX - 5, 0, 10, frame.height) - } - - } else { - minimisizeOverlay.removeFromSuperview() - } - } - - } - - - - public func needFullsize() { - self.state = .none - self.needsLayout = true - } - - public func needMinimisize() { - self.state = .minimisize - self.needsLayout = true - - } - - - func acceptLayout(prop:SplitProportion!) -> Bool { - return prop != nil ? (prop!.min > 0 && prop!.max > 0) : false; - } - -} diff --git a/TGUIKit/TGUIKit/TabBarController.swift b/TGUIKit/TGUIKit/TabBarController.swift deleted file mode 100644 index eb140a5986..0000000000 --- a/TGUIKit/TGUIKit/TabBarController.swift +++ /dev/null @@ -1,121 +0,0 @@ -// -// TabBarController.swift -// TGUIKit -// -// Created by keepcoder on 27/09/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - -private class TabBarViewController : View { - let tabView:TabBarView - - - required init(frame frameRect: NSRect) { - tabView = TabBarView(frame: NSMakeRect(0, frameRect.height - 50, frameRect.width, 50)) - super.init(frame: frameRect) - addSubview(tabView) - } - - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - self.background = presentation.colors.background - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func setFrameSize(_ newSize: NSSize) { - super.setFrameSize(newSize) - } - - override func layout() { - super.layout() - tabView.frame = NSMakeRect(0, frame.height - 50, frame.width, 50) - } -} - -public class TabBarController: ViewController, TabViewDelegate { - - - public var didChangedIndex:(Int)->Void = {_ in} - - public weak var current:ViewController? { - didSet { - current?.navigationController = self.navigationController - } - } - - private var genericView:TabBarViewController { - return view as! TabBarViewController - } - - public override func viewClass() -> AnyClass { - return TabBarViewController.self - } - - public override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - genericView.tabView.enumerateItems({ item in - if item.controller.isLoaded() { - item.controller.updateLocalizationAndTheme() - } - return false - }) - } - - public override func loadView() { - super.loadView() - genericView.tabView.delegate = self - genericView.autoresizingMask = [] - } - - public func didChange(selected item: TabItem, index: Int) { - - if current != item.controller { - if let current = current { - current.window?.makeFirstResponder(nil) - current.viewWillDisappear(false) - current.view.removeFromSuperview() - current.viewDidDisappear(false) - } - item.controller._frameRect = NSMakeRect(0, 0, bounds.width, bounds.height - genericView.tabView.frame.height) - item.controller.view.frame = item.controller._frameRect - item.controller.viewWillAppear(false) - view.addSubview(item.controller.view) - item.controller.viewDidAppear(false) - current = item.controller - didChangedIndex(index) - } - } - - public override func scrollup() { - current?.scrollup() - } - - public func hideTabView(_ hide:Bool) { - genericView.tabView.isHidden = hide - current?.view.frame = hide ? bounds : NSMakeRect(0, 0, bounds.width, bounds.height - genericView.tabView.frame.height) - - } - - public func select(index:Int) -> Void { - genericView.tabView.setSelectedIndex(index, respondToDelegate: true) - } - - public func add(tab:TabItem) -> Void { - genericView.tabView.addTab(tab) - } - public func tab(at index:Int) -> TabItem { - return genericView.tabView.tab(at: index) - } - public func replace(tab: TabItem, at index:Int) -> Void { - genericView.tabView.replaceTab(tab, at: index) - } - public var isEmpty:Bool { - return genericView.tabView.isEmpty - } - -} diff --git a/TGUIKit/TGUIKit/TabBarView.swift b/TGUIKit/TGUIKit/TabBarView.swift deleted file mode 100644 index 0988e92b86..0000000000 --- a/TGUIKit/TGUIKit/TabBarView.swift +++ /dev/null @@ -1,194 +0,0 @@ -// -// TabBarView.swift -// TGUIKit -// -// Created by keepcoder on 27/09/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - - -public protocol TabViewDelegate : class { - func didChange(selected item:TabItem, index:Int) - func scrollup() -} - -public class TabBarView: View { - - private var tabs:[TabItem] = [] - public private(set) var selectedIndex:Int = 0 - - public weak var delegate:TabViewDelegate? - - - required public init(frame frameRect: NSRect) { - super.init(frame: frameRect) - autoresizesSubviews = false - autoresizingMask = [] - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public override func draw(_ layer: CALayer, in ctx: CGContext) { - super.draw(layer, in: ctx) - - ctx.setFillColor(presentation.colors.border.cgColor) - ctx.fill(self.bounds) - } - - - - func addTab(_ tab: TabItem) { - self.tabs.append(tab) - self.redraw() - } - func replaceTab(_ tab: TabItem, at index:Int) { - self.tabs.remove(at: index) - self.tabs.insert(tab, at: index) - self.redraw() - } - - func insertTab(_ tab: TabItem, at index: Int) { - self.tabs.insert(tab, at: index) - self.redraw() - } - - func removeTab(_ tab: TabItem) { - self.tabs.remove(at: self.tabs.index(of: tab)!) - self.redraw() - } - - func enumerateItems(_ f:(TabItem)->Bool) { - for item in tabs { - if f(item) { - break - } - } - } - - func removeTab(at index: Int) { - self.tabs.remove(at: index) - self.redraw() - } - public var isEmpty:Bool { - return tabs.isEmpty - } - public func tab(at index:Int) -> TabItem { - return self.tabs[index] - } - - func redraw() { - let width = NSWidth(self.bounds) - let height = NSHeight(self.bounds) - .borderSize - let defWidth = width / CGFloat(self.tabs.count) - self.removeAllSubviews() - var xOffset:CGFloat = 0 - - - for i in 0 ..< tabs.count { - let tab = tabs[i] - let itemWidth = defWidth - let view = Control(frame: NSMakeRect(xOffset, .borderSize, itemWidth, height)) - view.backgroundColor = presentation.colors.background - let container = View(frame: view.bounds) - view.set(handler: { [weak tab] control in - tab?.longHoverHandler?(control) - }, for: .LongOver) - - view.set(handler: { [weak self] control in - if let strongSelf = self { - if strongSelf.selectedIndex == i { - strongSelf.delegate?.scrollup() - } else { - strongSelf.setSelectedIndex(i, respondToDelegate:true) - } - } - }, for: .Click) - view.autoresizingMask = [.minXMargin, .maxXMargin, .width] - view.autoresizesSubviews = true - let imageView = ImageView(frame: NSMakeRect(0, 0, tab.image.backingSize.width, tab.image.backingSize.height)) - imageView.image = tab.image - container.addSubview(imageView) - container.backgroundColor = presentation.colors.background - container.setFrameSize(NSMakeSize(NSWidth(imageView.frame), NSHeight(container.frame))) - view.addSubview(container) - - if let subView = tab.subNode?.view { - view.addSubview(subView) - } - - imageView.center() - container.center() - - self.addSubview(view) - xOffset += itemWidth - } - - self.setSelectedIndex(self.selectedIndex, respondToDelegate: false) - setFrameSize(frame.size) - } - - public override func updateLocalizationAndTheme() { - for subview in subviews { - subview.background = presentation.colors.background - for container in subview.subviews { - container.background = presentation.colors.background - } - } - self.backgroundColor = presentation.colors.background - needsDisplay = true - super.updateLocalizationAndTheme() - } - - - override public func setFrameSize(_ newSize: NSSize) { - super.setFrameSize(newSize) - // if previous != newSize.width { - let width = NSWidth(self.bounds) - let height = NSHeight(self.bounds) - .borderSize - let defWidth = floorToScreenPixels(width / CGFloat( max(1, self.tabs.count) )) - var xOffset:CGFloat = 0 - - var idx:Int = 0 - - for subview in subviews { - let w = idx == subviews.count - 1 ? defWidth - .borderSize : defWidth - let child = subview.subviews[0] - subview.frame = NSMakeRect(xOffset, .borderSize, w, height) - child.center() - xOffset += w - - idx += 1 - } - // } - } - - - public func setSelectedIndex(_ selectedIndex: Int, respondToDelegate: Bool) { - if selectedIndex > self.tabs.count || self.tabs.count == 0 { - return - } - let deselectItem = self.tabs[self.selectedIndex] - let deselectView = self.subviews[self.selectedIndex] - - var image:ImageView = deselectView.subviews[0].subviews[0] as! ImageView - image.image = deselectItem.image - self.selectedIndex = selectedIndex - let selectItem = self.tabs[self.selectedIndex] - let selectView = self.subviews[self.selectedIndex] - - image = selectView.subviews[0].subviews[0] as! ImageView - image.image = selectItem.selectedImage - if respondToDelegate { - self.delegate?.didChange(selected: selectItem, index: selectedIndex) - } - - } - - - - -} diff --git a/TGUIKit/TGUIKit/TabItem.swift b/TGUIKit/TGUIKit/TabItem.swift deleted file mode 100644 index 088f7072ca..0000000000 --- a/TGUIKit/TGUIKit/TabItem.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// TabItem.swift -// TGUIKit -// -// Created by keepcoder on 27/09/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - - - -open class TabItem: NSObject { - let image: CGImage - let selectedImage: CGImage - let controller:ViewController - let subNode:Node? - let longHoverHandler:((Control)->Void)? - public init(image: CGImage, selectedImage: CGImage, controller:ViewController, subNode:Node? = nil, longHoverHandler:((Control)->Void)? = nil) { - self.image = image - self.longHoverHandler = longHoverHandler - self.selectedImage = selectedImage - self.controller = controller - self.subNode = subNode - super.init() - } - - public func withUpdatedImages(_ image: CGImage, _ selectedImage: CGImage) -> TabItem { - return TabItem(image: image, selectedImage: selectedImage, controller: self.controller, subNode: self.subNode, longHoverHandler: self.longHoverHandler) - } -} diff --git a/TGUIKit/TGUIKit/TableAnimationInterface.swift b/TGUIKit/TGUIKit/TableAnimationInterface.swift deleted file mode 100644 index 21c9f40b9d..0000000000 --- a/TGUIKit/TGUIKit/TableAnimationInterface.swift +++ /dev/null @@ -1,106 +0,0 @@ - -// -// TableAnimationInterface.swift -// TGUIKit -// -// Created by keepcoder on 02/10/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - -open class TableAnimationInterface: NSObject { - - public let scrollBelow:Bool - public let saveIfAbove:Bool - public init(_ scrollBelow:Bool = true, _ saveIfAbove:Bool = true) { - self.scrollBelow = scrollBelow - self.saveIfAbove = saveIfAbove - } - - public func animate(table:TableView, added:[TableRowItem], removed:[TableRowItem]) -> Void { - - var height:CGFloat = 0 - - for item in added { - height += item.height - } - - for item in removed { - height -= item.height - } - - if added.isEmpty && removed.isEmpty { - return - } - - let contentView = table.contentView - let bounds = contentView.bounds - - let scrollBelow = self.scrollBelow || (bounds.minY - height) < 0 - - - - if bounds.minY > height, scrollBelow { -// height = bounds.minY -// -// let presentation = contentView.layer?.presentation() -// if let presentation = presentation, contentView.layer?.animation(forKey:"bounds") != nil { -// height += presentation.bounds.minY -// } -// - - table.scroll(to: .down(true)) - - - - } else if height - bounds.height < table.frame.height, scrollBelow { - - if scrollBelow { - contentView.bounds = NSMakeRect(0, 0, contentView.bounds.width, contentView.bounds.height) - } - - let range:NSRange = table.visibleRows(height) - - for item in added { - if item.index < range.location || item.index > range.location + range.length { - return - } - } - - CATransaction.begin() - for idx in range.location ..< range.length { - - if let view = table.viewNecessary(at: idx), let layer = view.layer { - - var inset = (layer.frame.minY - height); - if let presentLayer = layer.presentation(), presentLayer.animation(forKey: "position") != nil { - inset = presentLayer.position.y - } - layer.animatePosition(from: NSMakePoint(0, inset), to: NSMakePoint(0, layer.position.y), duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseOut) - - for item in added { - if item.index == idx { - layer.animateAlpha(from: 0, to: 1, duration: 0.2) - } - } - - } - - } - - CATransaction.commit() - - } else if !scrollBelow { - contentView.bounds = NSMakeRect(0, bounds.minY + height, contentView.bounds.width, contentView.bounds.height) - table.reflectScrolledClipView(contentView) - } - - } - - public func scroll(table:TableView, from:NSRect, to:NSRect) -> Void { - table.contentView.layer?.animateBounds(from: from, to: to, duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseOut) - } - - -} diff --git a/TGUIKit/TGUIKit/TableRowItem.swift b/TGUIKit/TGUIKit/TableRowItem.swift deleted file mode 100644 index f43eb995dc..0000000000 --- a/TGUIKit/TGUIKit/TableRowItem.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// TableRowItem.swift -// TGUIKit -// -// Created by keepcoder on 07/09/16. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import SwiftSignalKitMac - -open class TableRowItem: NSObject { - public weak var table:TableView? { - didSet { - tableViewDidUpdated() - } - } - public let initialSize:NSSize - - open func tableViewDidUpdated() { - - } - - open var animatable:Bool { - return true - } - - open var instantlyResize:Bool { - return false - } - - open private(set) var height:CGFloat = 60; - - - public var size:NSSize { - return NSMakeSize(width, height) - } - - public var oldWidth:CGFloat = 0 - - public var width:CGFloat { - if let table = table { - return table.frame.width - } else { - return initialSize.width - } - } - - open var stableId:AnyHashable { - return 0 - } - - public var index:Int { - get { - if let table = table, let index = table.index(of:self) { - return index - } else { - return -1 - } - } - - } - - public init(_ initialSize:NSSize) { - self.initialSize = initialSize - } - - open func prepare(_ selected:Bool) { - - } - - open var isVisible: Bool { - if let table = table { - let visible = table.visibleRows() - return visible.indexIn(index) - } - return false - } - - - open func menuItems() -> Signal<[ContextMenuItem], Void> { - return .single([]) - } - - public func redraw()->Void { - table?.reloadData(row: index) - } - - public var isSelected:Bool { - if let table = table { - return table.isSelected(self) - } else { - return false - } - } - - open var identifier:String { - return NSStringFromClass(viewClass()) - } - - open func viewClass() ->AnyClass { - return TableRowView.self; - } - - open func makeSize(_ width:CGFloat = CGFloat.greatestFiniteMagnitude, oldWidth:CGFloat = 0) -> Bool { - self.oldWidth = width - return true; - } -} diff --git a/TGUIKit/TGUIKit/TableRowView.swift b/TGUIKit/TGUIKit/TableRowView.swift deleted file mode 100644 index b8a17b814b..0000000000 --- a/TGUIKit/TGUIKit/TableRowView.swift +++ /dev/null @@ -1,283 +0,0 @@ -// -// TableRowView.swift -// TGUIKit -// -// Created by keepcoder on 07/09/16. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import SwiftSignalKitMac - - -open class TableRowView: NSTableRowView, CALayerDelegate { - - open private(set) weak var item:TableRowItem? - private let menuDisposable = MetaDisposable() - // var selected:Bool? - - open var border:BorderType? - public var animates:Bool = true - - public private(set) var contextMenu:ContextMenu? - - - required public override init(frame frameRect: NSRect) { - super.init(frame: frameRect) - - // self.layer = (self.layerClass() as! CALayer.Type).init() - self.wantsLayer = true - - self.layerContentsRedrawPolicy = .onSetNeedsDisplay - self.layer?.delegate = self - self.layer?.drawsAsynchronously = System.drawAsync - autoresizesSubviews = false - pressureConfiguration = NSPressureConfiguration(pressureBehavior: .primaryDeepClick) - } - - - open func updateColors() { - - } - - open func layerClass() ->AnyClass { - return CALayer.self; - } - - open var backdorColor: NSColor { - return presentation.colors.background - } - - open var isSelect: Bool { - return item?.isSelected ?? false - } - - open override func draw(_ dirtyRect: NSRect) { - - } - - open func draw(_ layer: CALayer, in ctx: CGContext) { - ctx.setFillColor(backdorColor.cgColor) - ctx.fill(layer.bounds) - - if let border = border { - - ctx.setFillColor(presentation.colors.border.cgColor) - - if border.contains(.Top) { - ctx.fill(NSMakeRect(0, frame.height - .borderSize, frame.width, .borderSize)) - } - if border.contains(.Bottom) { - ctx.fill(NSMakeRect(0, 0, frame.width, .borderSize)) - } - if border.contains(.Left) { - ctx.fill(NSMakeRect(0, 0, .borderSize, frame.height)) - } - if border.contains(.Right) { - ctx.fill(NSMakeRect(frame.width - .borderSize, 0, .borderSize, frame.height)) - } - - } - - } - - open var interactionContentView:NSView { - return self - } - - open var firstResponder:NSResponder? { - return self - } - - open override func mouseDown(with event: NSEvent) { - if event.modifierFlags.contains(.control) && event.clickCount == 1 { - showContextMenu(event) - } else { - if event.clickCount == 2 { - doubleClick(in: convert(event.locationInWindow, from: nil)) - return - } - super.mouseDown(with: event) - } - } - - private var lastPressureEventStage = 0 - - open override func pressureChange(with event: NSEvent) { - super.pressureChange(with: event) - if event.stage == 2 && lastPressureEventStage < 2 { - forceClick(in: convert(event.locationInWindow, from: nil)) - } - lastPressureEventStage = event.stage - } - - open override func rightMouseDown(with event: NSEvent) { - super.rightMouseDown(with: event) - showContextMenu(event) - } - - open func doubleClick(in location:NSPoint) -> Void { - - } - - open func forceClick(in location: NSPoint) { - - } - - open func showContextMenu(_ event:NSEvent) -> Void { - - menuDisposable.set(nil) - contextMenu = nil - - if let item = item { - menuDisposable.set((item.menuItems() |> deliverOnMainQueue |> take(1)).start(next: { [weak self] items in - if let strongSelf = self { - let menu = ContextMenu() - menu.onShow = { [weak strongSelf] menu in - strongSelf?.contextMenu = menu - strongSelf?.onShowContextMenu() - } - menu.delegate = menu - menu.onClose = { [weak strongSelf] in - strongSelf?.contextMenu = nil - strongSelf?.onCloseContextMenu() - } - for item in items { - menu.addItem(item) - } - - menu.delegate = menu - NSMenu.popUpContextMenu(menu, with: event, for: strongSelf) - } - - })) - } - - - } - - open override func menu(for event: NSEvent) -> NSMenu? { - return NSMenu() - } - - - - - open func onShowContextMenu() ->Void { - self.layer?.setNeedsDisplay() - } - - open func onCloseContextMenu() ->Void { - self.layer?.setNeedsDisplay() - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - open func updateMouse() { - - } - - public var isInsertionAnimated:Bool { - if let layer = layer?.presentation(), layer.animation(forKey: "position") != nil { - return true - } - return false - } - - public var rect:NSRect { - if let layer = layer?.presentation(), layer.animation(forKey: "position") != nil { - let rect = NSMakeRect(layer.position.x, layer.position.y, frame.width, frame.height) - return rect - } - return frame - } - - open override func setFrameSize(_ newSize: NSSize) { - super.setFrameSize(newSize) - guard #available(OSX 10.12, *) else { - needsLayout = true - return - } - } - - open override func setFrameOrigin(_ newOrigin: NSPoint) { - super.setFrameOrigin(newOrigin) - guard #available(OSX 10.12, *) else { - needsLayout = true - return - } - } - - open override func viewDidMoveToSuperview() { - if superview != nil { - guard #available(OSX 10.12, *) else { - needsLayout = true - return - } - } - } - - open override func layout() { - super.layout() - } - - public func notifySubviewsToLayout(_ subview:NSView) -> Void { - for sub in subview.subviews { - sub.needsLayout = true - } - } - - open override var needsLayout: Bool { - set { - super.needsLayout = newValue - if newValue { - guard #available(OSX 10.12, *) else { - layout() - notifySubviewsToLayout(self) - return - } - } - } - get { - return super.needsLayout - } - } - - deinit { - menuDisposable.dispose() - } - - - open override func copy() -> Any { - let view:View = View(frame:bounds) - view.backgroundColor = self.backdorColor - return view - } - - open func set(item:TableRowItem, animated:Bool = false) -> Void { - self.item = item; - updateColors() - } - - open func focusAnimation() { - - } - - public func change(pos position: NSPoint, animated: Bool, _ save:Bool = true, removeOnCompletion: Bool = true, duration:Double = 0.2, timingFunction: String = kCAMediaTimingFunctionEaseOut, completion:((Bool)->Void)? = nil) -> Void { - super._change(pos: position, animated: animated, save, removeOnCompletion: removeOnCompletion, duration: duration, timingFunction: timingFunction, completion: completion) - } - - public func change(size: NSSize, animated: Bool, _ save:Bool = true, removeOnCompletion: Bool = true, duration:Double = 0.2, timingFunction: String = kCAMediaTimingFunctionEaseOut, completion:((Bool)->Void)? = nil) { - super._change(size: size, animated: animated, save, removeOnCompletion: removeOnCompletion, duration: duration, timingFunction: timingFunction, completion: completion) - } - public func change(opacity to: CGFloat, animated: Bool = true, _ save:Bool = true, removeOnCompletion: Bool = true, duration:Double = 0.2, timingFunction: String = kCAMediaTimingFunctionEaseOut, completion:((Bool)->Void)? = nil) { - super._change(opacity: to, animated: animated, save, removeOnCompletion: removeOnCompletion, duration: duration, timingFunction: timingFunction, completion: completion) - } - - open func mouseInside() -> Bool { - return super._mouseInside() - } - -} diff --git a/TGUIKit/TGUIKit/TableView.swift b/TGUIKit/TGUIKit/TableView.swift deleted file mode 100644 index 4a5b6d2493..0000000000 --- a/TGUIKit/TGUIKit/TableView.swift +++ /dev/null @@ -1,1747 +0,0 @@ -// -// TableView.swift -// TGUIKit -// -// Created by keepcoder on 07/09/16. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import SwiftSignalKitMac - - -public enum TableSeparator { - case bottom; - case top; - case right; - case left; - case none; -} - -public class UpdateTransition { - public let inserted:[(Int,T)] - public let updated:[(Int,T)] - public let deleted:[Int] - public let animateVisibleOnly: Bool - public init(deleted:[Int], inserted:[(Int,T)], updated:[(Int,T)], animateVisibleOnly: Bool = true) { - self.inserted = inserted - self.updated = updated - self.deleted = deleted - self.animateVisibleOnly = animateVisibleOnly - } - - public var isEmpty:Bool { - return inserted.isEmpty && updated.isEmpty && deleted.isEmpty - } - - public var description: String { - return "inserted: \(inserted.count), updated:\(updated.count), deleted:\(deleted.count)" - } -} - - -public class TableUpdateTransition : UpdateTransition { - public let state:TableScrollState - public let animated:Bool - public let grouping:Bool - - public init(deleted:[Int], inserted:[(Int,TableRowItem)], updated:[(Int,TableRowItem)], animated:Bool = false, state:TableScrollState = .none(nil), grouping:Bool = true, animateVisibleOnly: Bool = true) { - self.animated = animated - self.state = state - self.grouping = grouping - super.init(deleted: deleted, inserted: inserted, updated: updated, animateVisibleOnly: animateVisibleOnly) - } - -} - -public final class TableEntriesTransition : TableUpdateTransition { - public let entries:T - public init(deleted:[Int], inserted:[(Int,TableRowItem)], updated:[(Int,TableRowItem)], entries:T, animated:Bool = false, state:TableScrollState = .none(nil), grouping:Bool = true) { - self.entries = entries - super.init(deleted: deleted, inserted: inserted, updated: updated, animated:animated, state: state, grouping:grouping) - } -} - -public protocol TableViewDelegate : class { - - func selectionDidChange(row:Int, item:TableRowItem, byClick:Bool, isNew:Bool) -> Void; - func selectionWillChange(row:Int, item:TableRowItem) -> Bool; - func isSelectable(row:Int, item:TableRowItem) -> Bool; - -} - -public enum TableSavingSide { - case lower - case upper -} - -public enum TableScrollState :Equatable { - case top(id: AnyHashable, animated: Bool, focus: Bool, inset: CGFloat); // stableId, animated, focus, inset - case bottom(id: AnyHashable, animated: Bool, focus: Bool, inset: CGFloat); // stableId, animated, focus, inset - case center(id: AnyHashable, animated: Bool, focus: Bool, inset: CGFloat); // stableId, animated, focus, inset - case saveVisible(TableSavingSide) - case none(TableAnimationInterface?); - case down(Bool); - case up(Bool); -} - -public extension TableScrollState { - public func swap(to stableId:AnyHashable) -> TableScrollState { - switch self { - case let .top(_, animated, focus, inset): - return .top(id: stableId, animated: animated, focus: focus, inset: inset) - case let .bottom(_, animated, focus, inset): - return .bottom(id: stableId, animated: animated, focus: focus, inset: inset) - case let .center(_, animated, focus, inset): - return .center(id: stableId, animated: animated, focus: focus, inset: inset) - default: - return self - } - } - - public var animated: Bool { - switch self { - case let .top(_, animated, _, _): - return animated - case let .bottom(_, animated, _, _): - return animated - case let .center(_, animated, _, _): - return animated - case .down(let animated): - return animated - case .up(let animated): - return animated - default: - return false - } - } -} - -public func ==(lhs:TableScrollState, rhs:TableScrollState) -> Bool { - switch lhs { - case let .top(stableId, animated, focus, inset): - if case .top(stableId, animated, focus, inset) = rhs { - return true - } else { - return false - } - case let .bottom(stableId, animated, focus, inset): - if case .bottom(stableId, animated, focus, inset) = rhs { - return true - } else { - return false - } - case let .center(stableId, animated, focus, inset): - if case .center(stableId, animated, focus, inset) = rhs { - return true - } else { - return false - } - case let .down(lhsAnimated): - switch rhs { - case let .down(rhsAnimated): - return lhsAnimated == rhsAnimated - default: - return false - } - case let .up(lhsAnimated): - switch rhs { - case let .up(rhsAnimated): - return lhsAnimated == rhsAnimated - default: - return false - } - case .none: - switch rhs { - case .none: - return true - default: - return false - } - case let .saveVisible(lhsType): - switch rhs { - case let .saveVisible(rhsType): - return lhsType == rhsType - default: - return false - } - } -} - -protocol SelectDelegate : class { - func selectRow(index:Int) -> Void; -} - -class TGFlipableTableView : NSTableView, CALayerDelegate { - - var bottomInset:CGFloat = 0 - - public var flip:Bool = true - - public weak var sdelegate:SelectDelegate? - weak var table:TableView? - var border:BorderType? - - override init(frame frameRect: NSRect) { - super.init(frame: frameRect) - self.autoresizesSubviews = false - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override var isFlipped: Bool { - return flip - } - - override func draw(_ dirtyRect: NSRect) { - - } - -// override public func setNeedsDisplay(_ invalidRect: NSRect) { -// -// } - - override func addSubview(_ view: NSView) { - super.addSubview(view) - } - - func draw(_ layer: CALayer, in ctx: CGContext) { - ctx.setFillColor(presentation.colors.background.cgColor) - ctx.fill(self.bounds) - - if let border = border { - - ctx.setFillColor(presentation.colors.border.cgColor) - - if border.contains(.Top) { - ctx.fill(NSMakeRect(0, NSHeight(self.frame) - .borderSize, NSWidth(self.frame), .borderSize)) - } - if border.contains(.Bottom) { - ctx.fill(NSMakeRect(0, 0, NSWidth(self.frame), .borderSize)) - } - if border.contains(.Left) { - ctx.fill(NSMakeRect(0, 0, .borderSize, NSHeight(self.frame))) - } - if border.contains(.Right) { - ctx.fill(NSMakeRect(NSWidth(self.frame) - .borderSize, 0, .borderSize, NSHeight(self.frame))) - } - - } - } - - - override func mouseDown(with event: NSEvent) { - let point = self.convert(event.locationInWindow, from: nil) - let range = self.rows(in: NSMakeRect(point.x, point.y, 1, 1)); - sdelegate?.selectRow(index: range.location) - } - - - override func setFrameSize(_ newSize: NSSize) { - let oldWidth: CGFloat = frame.width - super.setFrameSize(newSize) - - if oldWidth != frame.width { - if let table = table { - table.layoutIfNeeded(with: table.visibleRows(), oldWidth: oldWidth) - } - } - - } - - - - var liveWidth:CGFloat = 0 - - override func viewWillStartLiveResize() { - liveWidth = frame.width - } - - - override func viewDidEndLiveResize() { - if liveWidth != frame.width { - liveWidth = 0 - table?.layoutItems() - } - } - - - override func mouseUp(with event: NSEvent) { - - } - -} - -public protocol InteractionContentViewProtocol : class { - func contentInteractionView(for stableId: AnyHashable) -> NSView? -} - -public class TableScrollListener : NSObject { - fileprivate let uniqueId:UInt32 = arc4random() - fileprivate let handler:(ScrollPosition)->Void - fileprivate let dispatchWhenVisibleRangeUpdated: Bool - fileprivate var first: Bool = true - public init(dispatchWhenVisibleRangeUpdated: Bool = true, _ handler:@escaping(ScrollPosition)->Void) { - self.dispatchWhenVisibleRangeUpdated = dispatchWhenVisibleRangeUpdated - self.handler = handler - } - -} - -open class TableView: ScrollView, NSTableViewDelegate,NSTableViewDataSource,SelectDelegate,InteractionContentViewProtocol { - - public var separator:TableSeparator = .none - - var list:[TableRowItem] = [TableRowItem](); - var tableView:TGFlipableTableView - weak public var delegate:TableViewDelegate? - private var trackingArea:NSTrackingArea? - private var listhash:[AnyHashable:TableRowItem] = [AnyHashable:TableRowItem](); - - private let mergePromise:Promise = Promise() - private let mergeDisposable:MetaDisposable = MetaDisposable() - - public let selectedhash:Atomic = Atomic(value: nil); - - private var updating:Bool = false - - private var previousScroll:ScrollPosition? - public var needUpdateVisibleAfterScroll:Bool = false - private var scrollHandler:(_ scrollPosition:ScrollPosition) ->Void = {_ in} - - - private var scrollListeners:[TableScrollListener] = [] - - - public var emptyItem:TableRowItem? { - didSet { - if let _ = emptyView { - updateEmpties() - } - } - } - private var emptyView:TableRowView? - - public func addScroll(listener:TableScrollListener) { - scrollListeners.append(listener) - } - - - public var bottomInset:CGFloat = 0 { - didSet { - tableView.bottomInset = bottomInset - } - } - - - - public func removeScroll(listener:TableScrollListener) { - var index:Int = 0 - var found:Bool = false - for enumerate in scrollListeners { - if enumerate.uniqueId == listener.uniqueId { - found = true - break - } - index += 1 - } - - if found { - scrollListeners.remove(at: index) - } - - } - - public var count:Int { - get { - return self.list.count - } - } - - open override func setNeedsDisplay(_ invalidRect: NSRect) { - - } - - open override var isFlipped: Bool { - return true - } - - convenience override init(frame frameRect: NSRect) { - self.init(frame:frameRect, isFlipped:true, drawBorder: false) - } - - public var border:BorderType? { - didSet { - self.clipView.border = border - self.tableView.border = border - } - } - - open override var backgroundColor: NSColor { - didSet { - documentView?.background = backgroundColor - contentView.background = backgroundColor - self.clipView.backgroundColor = backgroundColor - self.clipView.needsDisplay = true - documentView?.needsDisplay = true - } - } - - public func setIsFlipped(_ flipped: Bool) { - self.tableView.flip = flipped - } - - public required init(frame frameRect: NSRect, isFlipped:Bool = true, bottomInset:CGFloat = 0, drawBorder: Bool = false) { - - let table = TGFlipableTableView(frame:frameRect) - table.flip = isFlipped - - self.tableView = table - self.tableView.wantsLayer = true - - self.tableView.layerContentsRedrawPolicy = .onSetNeedsDisplay - - - super.init(frame: frameRect); - - table.table = self - - self.bottomInset = bottomInset - table.bottomInset = bottomInset - - if drawBorder { - self.clipView.border = BorderType([.Right]) - self.tableView.border = BorderType([.Right]) - } - - self.hasVerticalScroller = true; - - self.documentView = self.tableView; - self.autoresizesSubviews = true; - self.autoresizingMask = [.width, .height] - - self.tableView.delegate = self; - self.tableView.dataSource = self; - self.tableView.sdelegate = self - self.tableView.allowsColumnReordering = true - self.tableView.headerView = nil; - self.tableView.intercellSpacing = NSMakeSize(0, 0) - - let tableColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: "column")) - tableColumn.width = frame.width - self.tableView.addTableColumn(tableColumn) - - - mergeDisposable.set(mergePromise.get().start(next: { [weak self] (transition) in - self?.merge(with: transition) - })) - - } - - - open override func layout() { - super.layout() - emptyView?.frame = bounds - if needsLayouItemsOnNextTransition { - layoutItems() - } - } - - open override func draw(_ layer: CALayer, in ctx: CGContext) { - super.draw(layer, in: ctx) - - } - - func layoutIfNeeded(with range:NSRange, oldWidth:CGFloat) { - for i in range.min ..< range.max { - let item = self.item(at: i) - let before = item.height - let updated = item.makeSize(tableView.frame.width, oldWidth: oldWidth) - let after = item.height - if (before != after && updated) || item.instantlyResize { - reloadData(row: i, animated: false) - noteHeightOfRow(i, false) - } - } - } - - open override func viewDidMoveToSuperview() { - if superview != nil { - let clipView = self.contentView - - NotificationCenter.default.addObserver(forName: NSView.boundsDidChangeNotification, object: clipView, queue: nil, using: { [weak self] notification in - if let strongSelf = self { - - let reqCount = strongSelf.count / 6 - - strongSelf.updateStickAfterScroll(false) - - let scroll = strongSelf.scrollPosition(strongSelf.visibleRows()) - - if (!strongSelf.updating && !strongSelf.clipView.isAnimateScrolling) { - - let range = scroll.current.visibleRows - - if(scroll.current.direction != strongSelf.previousScroll?.direction && scroll.current.rect != strongSelf.previousScroll?.rect) { - - switch(scroll.current.direction) { - case .top: - if(range.location <= reqCount) { - strongSelf.scrollHandler(scroll.current) - strongSelf.previousScroll = scroll.current - - } - case .bottom: - if(strongSelf.count - (range.location + range.length) <= reqCount) { - strongSelf.scrollHandler(scroll.current) - strongSelf.previousScroll = scroll.current - - } - case .none: - strongSelf.scrollHandler(scroll.current) - strongSelf.previousScroll = scroll.current - - } - } - - } - for listener in strongSelf.scrollListeners { - if !listener.dispatchWhenVisibleRangeUpdated || listener.first || !NSEqualRanges(scroll.current.visibleRows, scroll.previous.visibleRows) { - listener.handler(scroll.current) - listener.first = false - } - } - } - - }) - } else { - NotificationCenter.default.removeObserver(self) - } - } - - - private var stickClass:AnyClass? - private var stickView:TableStickView? - private var stickItem:TableStickItem? { - didSet { - if stickItem != oldValue { - if let stickHandler = stickHandler { - stickHandler(stickItem) - } - } - } - } - private var stickHandler:((TableStickItem?)->Void)? - private var firstTime: Bool = false - public func set(stickClass:AnyClass?, visible: Bool = true, handler:@escaping(TableStickItem?)->Void) { - self.stickClass = stickClass - self.stickHandler = handler - self.firstTime = true - if let stickClass = stickClass as? TableStickItem.Type { - if stickView == nil { - let stickItem:TableStickItem = stickClass.init(frame.size) - - self.stickItem = stickItem - if visible { - let vz = stickItem.viewClass() as! TableStickView.Type - stickView = vz.init(frame:NSMakeRect(0, 0, NSWidth(self.frame), stickItem.height)) - stickView!.header = true - stickView!.set(item: stickItem, animated: false) - tableView.addSubview(stickView!) - } - } - - updateStickAfterScroll(false) - - } else { - stickView?.removeFromSuperview() - stickView = nil - stickItem = nil - } - - } - - func optionalItem(at:Int) -> TableRowItem? { - return at < count && at >= 0 ? self.item(at: at) : nil - } - - private var needsLayouItemsOnNextTransition:Bool = false - - public func layouItemsOnNextTransition() { - needsLayouItemsOnNextTransition = true - } - - public func layoutItems() { - - let visibleItems = self.visibleItems() - - beginTableUpdates() - enumerateItems { item in - _ = item.makeSize(frame.width, oldWidth: item.width) - reloadData(row: item.index, animated: false) - NSAnimationContext.current.duration = 0.0 - tableView.noteHeightOfRows(withIndexesChanged: IndexSet(integer: item.index)) - return true - } - endTableUpdates() - - saveScrollState(visibleItems) - - needsLayouItemsOnNextTransition = false - } - - private func saveScrollState(_ visibleItems: [(TableRowItem,CGFloat,CGFloat)]) -> Void { - if !visibleItems.isEmpty, clipView.bounds.minY > 0 { - var nrect:NSRect = NSZeroRect - - let strideTo:StrideTo = stride(from: visibleItems.count - 1, to: -1, by: -1) - - for i in strideTo { - let visible = visibleItems[i] - if let item = self.item(stableId: visible.0.stableId) { - - nrect = rectOf(item: item) - - if let view = viewNecessary(at: i) { - if view.isInsertionAnimated { - break - } - } - - let y:CGFloat - if !tableView.isFlipped { - y = nrect.minY - (frame.height - visible.1) + nrect.height - } else { - y = nrect.minY - visible.1 - } - - //clipView.scroll(to: NSMakePoint(0, y + frame.minY), animated: false) - self.contentView.bounds = NSMakeRect(0, y, 0, clipView.bounds.height) - reflectScrolledClipView(clipView) - break - } - } - } - } - - private let stickTimeoutDisposable = MetaDisposable() - - public func updateStickAfterScroll(_ animated: Bool) -> Void { - let range = self.visibleRows() - - if let stickClass = stickClass { - if documentSize.height > frame.height { - var index:Int = range.location + range.length - 1 - - let flipped = tableView.isFlipped - - let scrollInset = self.documentOffset.y + (flipped ? 0 : frame.height) - var item:TableRowItem? = optionalItem(at: index) - - while let s = item, !s.isKind(of: stickClass) { - index += 1 - item = self.optionalItem(at: index) - } - - if item == nil { - index = range.location + range.length - while item == nil && index < count { - if let s = self.optionalItem(at: index), s.isKind(of: stickClass) { - item = s - } - index += 1 - } - } - - if let item = item as? TableStickItem { - var currentStick:TableStickItem? - - for index in stride(from: item.index - 1, to: -1, by: -1) { - let item = self.optionalItem(at: index) - if let item = item, item.isKind(of: stickClass) { - currentStick = item as? TableStickItem - break - } - } - - if stickView?.item != item { - stickView?.set(item: item, animated: tableView.subviews.last == stickView) - } - - if let item = stickItem { - (viewNecessary(at: item.index) as? TableStickView)?.updateIsVisible(!firstTime, animated: false) - - - } - - stickItem = currentStick ?? item - - (viewNecessary(at: item.index) as? TableStickView)?.updateIsVisible(false, animated: false) - - if let stickView = stickView { - if tableView.subviews.last != stickView { - stickView.removeFromSuperview() - tableView.addSubview(stickView) - } - } - - stickView?.setFrameSize(tableView.frame.width, item.height) - let itemRect:NSRect = tableView.rect(ofRow: item.index) - - if let item = stickItem, item.isKind(of: stickClass), let stickView = stickView { - let rect:NSRect = tableView.rect(ofRow: item.index) - let dif:CGFloat - if currentStick != nil { - dif = min(scrollInset - rect.maxY, item.height) - } else { - dif = item.height - } - let yTopOffset:CGFloat = min(max(scrollInset - dif, 0), documentSize.height - item.height) - if stickView.frame.minY != yTopOffset { - stickView.isHidden = firstTime - if !animated || stickView.layer?.opacity != 0 { - stickView.change(opacity: firstTime ? 0 : 1, animated: !firstTime) - firstTime = false - } - } - stickView.change(pos: NSMakePoint(0, yTopOffset), animated: animated) - stickView.header = fabs(dif) <= item.height - stickTimeoutDisposable.set((Signal.single(Void()) |> delay(2.0, queue: Queue.mainQueue())).start(next: { [weak stickView, weak item] in - if let item = item, abs(itemRect.minY - yTopOffset) > item.height, let stickView = stickView { - stickView.change(opacity: 0.0, completion: { [weak stickView] completed in - if completed { - stickView?.isHidden = true - } - }) - } - - })) - - } - - } else if let stickView = stickView { - stickView.setFrameOrigin(0, max(0,scrollInset)) - stickView.header = true - } - - } - - } - } - - - public func resetScrollNotifies() ->Void { - self.previousScroll = nil - updateScroll() - } - - public func notifyScrollHandlers() -> Void { - let scroll = scrollPosition(visibleRows()).current - for listener in scrollListeners { - listener.handler(scroll) - } - } - - public var topVisibleRow:Int? { - let visible = visibleItems() - if !isFlipped { - return visible.first?.0.index - } else { - return visible.last?.0.index - } - } - - public var bottomVisibleRow:Int? { - let visible = visibleItems() - if isFlipped { - return visible.first?.0.index - } else { - return visible.last?.0.index - } - } - - open override func setFrameOrigin(_ newOrigin: NSPoint) { - super.setFrameOrigin(newOrigin); - self.updateTrackingAreas(); - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public func selectedItem() -> TableRowItem? { - - let hash = selectedhash.modify({$0}) - if let hash = hash { - return self.item(stableId:hash) - } - return nil - } - - public func isSelected(_ item:TableRowItem) ->Bool { - return selectedhash.modify({$0}) == item.stableId - } - - public func item(stableId:AnyHashable) -> TableRowItem? { - return self.listhash[stableId]; - } - - public func index(of:TableRowItem) -> Int? { - - if let it = self.listhash[of.stableId] { - return self.list.index(of: it); - } - - return nil - } - - public func index(hash:AnyHashable) -> Int? { - - if let it = self.listhash[hash] { - return self.list.index(of: it); - } - - return nil - } - - public func insert(item:TableRowItem, at:Int = 0, redraw:Bool = true, animation:NSTableView.AnimationOptions = .none) -> Bool { - - assert(self.item(stableId:item.stableId) == nil, "inserting existing row inTable: \(self.item(stableId:item.stableId)!.className), new: \(item.className)") - self.listhash[item.stableId] = item; - self.list.insert(item, at: min(at, list.count)); - item.table = self; - - let animation = animation != .none ? item.animatable ? animation : .none : .none - NSAnimationContext.current.duration = animation != .none ? 0.2 : 0.0 - - if(redraw) { - self.tableView.insertRows(at: IndexSet(integer: at), withAnimation: animation) - } - - return true; - - } - - public func addItem(item:TableRowItem, redraw:Bool = true, animation:NSTableView.AnimationOptions = .none) -> Bool { - return self.insert(item: item, at: self.count, redraw: redraw, animation:animation) - } - - public func insert(items:[TableRowItem], at:Int = 0, redraw:Bool = true, animation:NSTableView.AnimationOptions = .none) -> Void { - - - var current:Int = 0; - for item in items { - - if(self.insert(item: item, at: at + current, redraw: false)) { - current += 1; - } - - } - - if(current != 0 && redraw) { - self.tableView.insertRows(at: IndexSet(integersIn: at ..< current + at), withAnimation: animation) - } - - } - - public var firstItem:TableRowItem? { - return self.list.first - } - - public var lastItem:TableRowItem? { - return self.list.last - } - - public func noteHeightOfRow(_ row:Int, _ animated:Bool = true) { - if !animated { - NSAnimationContext.current.duration = 0 - } - tableView.noteHeightOfRows(withIndexesChanged: IndexSet(integer: row)) - } - - - - public func reloadData(row:Int, animated:Bool = false) -> Void { - if let view = self.viewNecessary(at: row) { - let item = self.item(at: row) - if view.isKind(of: item.viewClass()) { - if let viewItem = view.item { - if viewItem.height != item.height { - NSAnimationContext.current.duration = animated ? 0.2 : 0.0 - tableView.noteHeightOfRows(withIndexesChanged: IndexSet(integer: row)) - } - } else { - NSAnimationContext.current.duration = animated ? 0.2 : 0.0 - tableView.noteHeightOfRows(withIndexesChanged: IndexSet(integer: row)) - } - - view.set(item: item, animated: animated) - view.needsDisplay = true - } else { - self.tableView.removeRows(at: IndexSet(integer: row), withAnimation: !animated ? .none : .effectFade) - self.tableView.insertRows(at: IndexSet(integer: row), withAnimation: !animated ? .none : .effectFade) - } - } else { - NSAnimationContext.current.duration = 0.0 - tableView.noteHeightOfRows(withIndexesChanged: IndexSet(integer: row)) - } - //self.moveItem(from: row, to: row) - } - - public func moveItem(from:Int, to:Int, changeItem:TableRowItem? = nil, redraw:Bool = true, animation:NSTableView.AnimationOptions = .none) -> Void { - - - var item:TableRowItem = self.item(at:from); - let animation: NSTableView.AnimationOptions = animation != .none ? item.animatable ? animation : .none : .none - NSAnimationContext.current.duration = animation != .none ? NSAnimationContext.current.duration : 0.0 - - if let change = changeItem { - assert(change.stableId == item.stableId) - change.table = self - self.listhash.removeValue(forKey: item.stableId) - self.listhash[change.stableId] = change - item = change - } - - self.list.remove(at: from); - - self.list.insert(item, at: to); - - - if(redraw) { - - if from == to { - self.reloadData(row: to) - } else { - self.tableView.removeRows(at: IndexSet(integer:from), withAnimation: from == to ? .none : animation) - self.tableView.insertRows(at: IndexSet(integer:to), withAnimation: from == to ? .none : animation) - } - - } - - } - - public func beginUpdates() -> Void { - updating = true - updateScroll(visibleRows()) - self.previousScroll = nil - CATransaction.begin() - } - - public func endUpdates() -> Void { - updating = false - updateScroll(visibleRows()) - self.previousScroll = nil - CATransaction.commit() - } - - public func rectOf(item:TableRowItem) -> NSRect { - if let index = self.index(of: item) { - return self.tableView.rect(ofRow: index) - } else { - return NSZeroRect - } - } - - public func rectOf(index:Int) -> NSRect { - return self.tableView.rect(ofRow: index) - } - - public func remove(at:Int, redraw:Bool = true, animation:NSTableView.AnimationOptions = .none) -> Void { - if at < count { - let item = self.item(at: at) - let animation: NSTableView.AnimationOptions = animation != .none ? item.animatable ? animation : .none : .none - NSAnimationContext.current.duration = animation == .none ? 0.0 : 0.2 - - self.list.remove(at: at); - self.listhash.removeValue(forKey: item.stableId) - - if(redraw) { - self.tableView.removeRows(at: IndexSet(integer:at), withAnimation: animation != .none ? .effectFade : .none) - } - } - } - - public func remove(range:Range, redraw:Bool = true, animation:NSTableView.AnimationOptions = .none) -> Void { - - for i in range.lowerBound ..< range.upperBound { - remove(at: i, redraw: false) - } - - if(redraw) { - self.tableView.removeRows(at: IndexSet(integersIn:range), withAnimation: animation != .none ? .effectFade : .none) - } - } - - - - public func removeAll(redraw:Bool = true, animation:NSTableView.AnimationOptions = .none) -> Void { - let count:Int = self.count; - self.list.removeAll() - self.listhash.removeAll() - - if(redraw) { - - self.tableView.removeRows(at: IndexSet(integersIn: 0 ..< count), withAnimation: animation != .none ? .effectFade : .none) - } - } - - public func selectNext(_ scroll:Bool = true, _ animated:Bool = false) -> Void { - - if let hash = selectedhash.modify({$0}) { - let selectedItem = self.item(stableId: hash) - if let selectedItem = selectedItem { - var selectedIndex = self.index(of: selectedItem)! - selectedIndex += 1 - - if selectedIndex == count { - selectedIndex = 0 - } - if let delegate = delegate { - let sIndex = selectedIndex - for i in sIndex ..< list.count { - if delegate.selectionWillChange(row: i, item: item(at: i)) { - selectedIndex = i - break - } - } - } - - - _ = select(item: item(at: selectedIndex)) - } - - - } else { - if let delegate = delegate { - for item in list { - if delegate.selectionWillChange(row: item.index, item: item) { - _ = self.select(item: item) - break - } - } - } - - } - if let hash = selectedhash.modify({$0}), scroll { - self.scroll(to: .top(id: hash, animated: animated, focus: false, inset: 0), inset: NSEdgeInsets(), true) - } - } - - public func selectPrev(_ scroll:Bool = true, _ animated:Bool = false) -> Void { - - if let hash = selectedhash.modify({$0}) { - let selectedItem = self.item(stableId: hash) - if let selectedItem = selectedItem { - var selectedIndex = self.index(of: selectedItem)! - selectedIndex -= 1 - - if selectedIndex == -1 { - selectedIndex = count - 1 - } - - if let delegate = delegate { - let sIndex = selectedIndex - for i in stride(from: sIndex, to: -1, by: -1) { - if delegate.selectionWillChange(row: i, item: item(at: i)) { - selectedIndex = i - break - } - } - } - - - _ = select(item: item(at: selectedIndex)) - } - - - } else { - if let delegate = delegate { - for i in stride(from: list.count - 1, to: -1, by: -1) { - if delegate.selectionWillChange(row: i, item: item(at: i)) { - _ = self.select(item: item(at: i)) - break - } - } - } - - } - - if let hash = selectedhash.modify({$0}), scroll { - self.scroll(to: .bottom(id: hash, animated: animated, focus: false, inset: 0), inset: NSEdgeInsets(), true) - } - } - - public var isEmpty:Bool { - return self.list.isEmpty || (!tableView.isFlipped && list.count == 1) - } - - public func reloadData() -> Void { - self.tableView.reloadData() - } - - public func item(at:Int) -> TableRowItem { - return self.list[at] - } - - public func visibleRows(_ insetHeight:CGFloat = 0) -> NSRange { - return self.tableView.rows(in: NSMakeRect(self.tableView.visibleRect.minX, self.tableView.visibleRect.minY, self.tableView.visibleRect.width, self.tableView.visibleRect.height + insetHeight)) - } - - public var listHeight:CGFloat { - var height:CGFloat = 0 - for item in list { - height += item.height - } - return height - } - - public func row(at point:NSPoint) -> Int { - return tableView.row(at: NSMakePoint(point.x, point.y - bottomInset)) - } - - public func viewNecessary(at row:Int) -> TableRowView? { - if row < 0 || row > count - 1 { - return nil - } - return self.tableView.rowView(atRow: row, makeIfNecessary: false) as? TableRowView - } - - - public func select(item:TableRowItem, notify:Bool = true, byClick:Bool = false) -> Bool { - - if let delegate = delegate, delegate.isSelectable(row: item.index, item: item) { - if(self.item(stableId:item.stableId) != nil) { - if delegate.selectionWillChange(row: item.index, item: item) { - let new = item.stableId != selectedhash.modify({$0}) - self.cancelSelection(); - let _ = selectedhash.swap(item.stableId) - item.prepare(true) - self.reloadData(row:item.index) - if notify { - delegate.selectionDidChange(row: item.index, item: item, byClick:byClick, isNew:new) - } - return true; - } - } - } - return false; - - } - - public func changeSelection(stableId:AnyHashable?) { - if let stableId = stableId { - if let item = self.item(stableId: stableId) { - _ = self.select(item:item, notify:false) - } else { - cancelSelection() - _ = self.selectedhash.swap(stableId) - } - } else { - cancelSelection() - } - } - - public func cancelSelection() -> Void { - if let hash = selectedhash.modify({$0}) { - if let item = self.item(stableId: hash) { - item.prepare(false) - let _ = selectedhash.swap(nil) - self.reloadData(row:item.index) - } else { - let _ = selectedhash.swap(nil) - } - } - - } - - - func rowView(item:TableRowItem) -> TableRowView { - let identifier:String = item.identifier - - var view = self.tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: identifier), owner: self.tableView) - if(view == nil) { - let vz = item.viewClass() as! TableRowView.Type - - view = vz.init(frame:NSMakeRect(0, 0, NSWidth(self.frame), item.height)) - - view?.identifier = NSUserInterfaceItemIdentifier(rawValue: identifier) - - } - return view as! TableRowView; - } - - public func numberOfRows(in tableView: NSTableView) -> Int { - return self.count; - } - - public func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { - return max(self.item(at: row).height, 1) - } - - public func tableView(_ tableView: NSTableView, isGroupRow row: Int) -> Bool { - return false; - } - - public func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { - - return nil - } - - - public func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? { - let item:TableRowItem = self.item(at: row); - - let view:TableRowView = self.rowView(item: item); - - - view.set(item: item, animated: false) - - return view - } - - - func visibleItems() -> [(TableRowItem,CGFloat,CGFloat)] { // item, top offset, bottom offset - - var list:[(TableRowItem,CGFloat,CGFloat)] = [] - - let visible = visibleRows() - - for i in visible.location ..< visible.location + visible.length { - let item = self.item(at: i) - let rect = rectOf(index: i) - if rect.height == item.height { - if !tableView.isFlipped { - let top = frame.height - (rect.minY - documentOffset.y) - rect.height - let bottom = (rect.minY - documentOffset.y) - list.append((item,top,bottom)) - } else { - let top = rect.minY - documentOffset.y - let bottom = frame.height - (rect.minY - documentOffset.y) - rect.height - list.append((item,top,bottom)) - //fatalError("not supported") - } - } - - // list.append(item,) - } - - - return list; - - } - - func itemRects() -> [(TableRowItem, NSRect, Int)] { - var ilist:[(TableRowItem,NSRect,Int)] = [(TableRowItem,NSRect,Int)]() - - for i in 0 ..< self.list.count { - ilist.append((item(at: i),self.rectOf(index: i), i)) - - } - - return ilist; - - } - - public func beginTableUpdates() { - self.tableView.beginUpdates() - } - - public func endTableUpdates() { - self.tableView.endUpdates() - } - - public func stopMerge() { - mergeDisposable.set(nil) - mergePromise.set(.single(TableUpdateTransition(deleted: [], inserted: [], updated: []))) - } - - public func startMerge() { - mergeDisposable.set((mergePromise.get() |> deliverOnMainQueue).start(next: { [weak self] transition in - self?.merge(with: transition) - })) - } - - public func merge(with transition:Signal) { - mergePromise.set(transition) - } - - - private var first:Bool = true - - public func merge(with transition:TableUpdateTransition) -> Void { - - assertOnMainThread() - assert(!updating) - - let oldEmpty = self.isEmpty - - self.beginUpdates() - - let visibleItems = self.visibleItems() - let visibleRange = self.visibleRows() - if transition.grouping && !transition.isEmpty { - self.tableView.beginUpdates() - } - - var inserted:[TableRowItem] = [] - var removed:[TableRowItem] = [] - - for rdx in transition.deleted.reversed() { - let effect:NSTableView.AnimationOptions - if case let .none(interface) = transition.state, interface != nil { - effect = (visibleRange.indexIn(rdx) || !transition.animateVisibleOnly) ? .effectFade : .none - } else { - effect = transition.animated && (visibleRange.indexIn(rdx) || !transition.animateVisibleOnly) ? .effectFade : .none - } - if rdx < visibleRange.location { - removed.append(item(at: rdx)) - } - self.remove(at: rdx, redraw: true, animation:effect) - } - - NSAnimationContext.current.duration = transition.animated ? 0.2 : 0.0 - - - for (idx,item) in transition.inserted { - let effect:NSTableView.AnimationOptions = transition.animated ? .effectFade : .none - _ = self.insert(item: item, at:idx, redraw: true, animation: effect) - if item.animatable { - inserted.append(item) - } - } - - - for (index,item) in transition.updated { - let animated:Bool - if case .none = transition.state { - animated = true - } else { - animated = false - } - replace(item:item, at:index, animated: animated) - } - - if transition.grouping && !transition.isEmpty { - self.tableView.endUpdates() - } - let state: TableScrollState - - if case .none = transition.state { - let isSomeOfItemVisible = !inserted.filter({$0.isVisible}).isEmpty || !removed.filter({$0.isVisible}).isEmpty - if isSomeOfItemVisible { - state = transition.state - } else { - state = .saveVisible(.upper) - } - } else { - state = transition.state - } - - //reflectScrolledClipView(clipView) - switch state { - case let .none(animation): - // print("scroll do nothing") - animation?.animate(table:self, added: inserted, removed:removed) - - case .bottom, .top, .center: - self.scroll(to: transition.state) - case .up(_), .down(_): - self.scroll(to: transition.state) - case let .saveVisible(side): - -// if transition.isEmpty { -// break -// } - - var nrect:NSRect = NSZeroRect - - let strideTo:StrideTo - - if !tableView.isFlipped { - switch side { - case .lower: - strideTo = stride(from: visibleItems.count - 1, to: -1, by: -1) - case .upper: - strideTo = stride(from: visibleItems.count - 1, to: -1, by: -1) //stride(from: 0, to: visibleItems.count, by: 1) - } - } else { - switch side { - case .upper: - strideTo = stride(from: visibleItems.count - 1, to: -1, by: -1) - case .lower: - strideTo = stride(from: 0, to: visibleItems.count, by: 1) - } - } - - - for i in strideTo { - let visible = visibleItems[i] - if let item = self.item(stableId: visible.0.stableId) { - - nrect = rectOf(item: item) - - if let view = viewNecessary(at: i) { - if view.isInsertionAnimated { - break - } - } - - let y:CGFloat - - switch side { - case .lower: - if !tableView.isFlipped { - y = nrect.minY - (frame.height - visible.1) + nrect.height - } else { - y = nrect.minY - visible.1 - } - break - case .upper: - if !tableView.isFlipped { - y = nrect.minY - (frame.height - visible.1) + nrect.height - } else { - y = nrect.minY - visible.1 - } - break - } - self.contentView.bounds = NSMakeRect(0, y, 0, clipView.bounds.height) - reflectScrolledClipView(clipView) - break - } - } - - break - } - - - self.endUpdates() - - - - if oldEmpty != isEmpty || first { - updateEmpties() - } - - first = false - performScrollEvent() - } - - func updateEmpties() { - if let emptyItem = emptyItem { - if isEmpty { - if let empt = emptyView, !empt.isKind(of: emptyItem.viewClass()) || empt.item != emptyItem { - emptyView?.removeFromSuperview() - emptyView = nil - } - if emptyView == nil { - let vz = emptyItem.viewClass() as! TableRowView.Type - emptyView = vz.init(frame:bounds) - emptyView?.identifier = identifier - } - emptyView?.frame = bounds - if emptyView?.superview == nil { - addSubview(emptyView!) - } - emptyView?.set(item: emptyItem) - emptyView?.needsLayout = true - } else { - emptyView?.removeFromSuperview() - emptyView = nil - } - } - - } - - - public func replace(item:TableRowItem, at index:Int, animated:Bool) { - list[index] = item - listhash[item.stableId] = item - item.table = self - reloadData(row: index, animated: animated) - } - - public func contentInteractionView(for stableId: AnyHashable) -> NSView? { - if let item = self.item(stableId: stableId) { - let view = viewNecessary(at:item.index) - if let view = view, !NSIsEmptyRect(view.visibleRect) { - return view.interactionContentView - } - - } - - return nil - } - - - func selectRow(index: Int) { - if self.count > index { - _ = self.select(item: self.item(at: index), byClick:true) - } - } - - public override func change(size: NSSize, animated: Bool, _ save:Bool = true, removeOnCompletion: Bool = true, duration:Double = 0.2, timingFunction: String = kCAMediaTimingFunctionEaseOut, completion:((Bool)->Void)? = nil) { - - - if animated { - - if !tableView.isFlipped { - - CATransaction.begin() - var presentBounds:NSRect = self.layer?.bounds ?? self.bounds - let presentation = self.layer?.presentation() - if let presentation = presentation, self.layer?.animation(forKey:"bounds") != nil { - presentBounds = presentation.bounds - } - - self.layer?.animateBounds(from: presentBounds, to: NSMakeRect(0, self.bounds.minY, size.width, size.height), duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseOut) - let y = (size.height - presentBounds.height) - - presentBounds = contentView.layer?.bounds ?? contentView.bounds - if let presentation = contentView.layer?.presentation(), contentView.layer?.animation(forKey:"bounds") != nil { - presentBounds = presentation.bounds - } - - if y > 0 { - presentBounds.origin.y -= y - presentBounds.size.height += y - } else { - presentBounds.origin.y += y - presentBounds.size.height -= y - } - - contentView.layer?.animateBounds(from: presentBounds, to: NSMakeRect(0, contentView.bounds.minY, size.width, size.height), duration: duration, timingFunction: timingFunction, removeOnCompletion: removeOnCompletion, completion: completion) - CATransaction.commit() - } else { - super.change(size: size, animated: animated, removeOnCompletion: removeOnCompletion, duration: duration, timingFunction: timingFunction, completion: completion) - return - } - } - self.setFrameSize(size) - updateStickAfterScroll(animated) - } - - - - public func scroll(to state:TableScrollState, inset:NSEdgeInsets = NSEdgeInsets(), _ toVisible:Bool = false) { - // if let index = self.index(of: item) { - - var rowRect:NSRect = bounds - - var item:TableRowItem? - var animate:Bool = false - var focus: Bool = false - var relativeInset: CGFloat = 0 - switch state { - case let .center(stableId, _animate, _focus, _inset): - item = self.item(stableId: stableId) - animate = _animate - relativeInset = _inset - focus = _focus - case let .bottom(stableId, _animate, _focus, _inset): - item = self.item(stableId: stableId) - animate = _animate - relativeInset = _inset - focus = _focus - case let .top(stableId, _animate, _focus, _inset): - item = self.item(stableId: stableId) - animate = _animate - relativeInset = _inset - focus = _focus - case let .down(_animate): - animate = _animate - if !tableView.isFlipped { - rowRect.origin = NSZeroPoint - } else { - rowRect.origin = NSMakePoint(0, max(0,documentSize.height - frame.height)) - } - case let .up(_animate): - animate = _animate - if !tableView.isFlipped { - rowRect.origin = NSMakePoint(0, max(documentSize.height,frame.height)) - } else { - rowRect.origin = NSZeroPoint - } - default: - fatalError("for scroll to item, you can use only .top, center, .bottom enumeration") - } - - let bottomInset = self.bottomInset != 0 ? (self.bottomInset) : 0 - let height:CGFloat = self is HorizontalTableView ? frame.width : frame.height - - if let item = item { - rowRect = self.rectOf(item: item) - - switch state { - case .bottom: - if tableView.isFlipped { - rowRect.origin.y -= (height - rowRect.height) - bottomInset - } - case .top: - // break - if !tableView.isFlipped { - rowRect.origin.y -= (height - rowRect.height) - bottomInset - } - case .center: - if !tableView.isFlipped { - rowRect.origin.y -= floorToScreenPixels((height - rowRect.height) / 2.0) - bottomInset - } else { - - if rowRect.maxY > height/2.0 { - rowRect.origin.y -= floorToScreenPixels((height - rowRect.height) / 2.0) - bottomInset - } else { - rowRect.origin.y = 0 - } - - - // fatalError("not implemented") - } - - default: - fatalError("not implemented") - } - - if toVisible { - let view = self.viewNecessary(at: item.index) - if let view = view, view.visibleRect.height == item.height { - if focus { - view.focusAnimation() - } - return - } - } - } - rowRect.origin.y = round(min(max(rowRect.minY + relativeInset,0), documentSize.height - height) + inset.top) - if clipView.bounds.minY != rowRect.minY { - - var applied = false - let scrollListener = TableScrollListener({ [weak self, weak item] position in - if let item = item, !applied, let view = self?.viewNecessary(at: item.index), view.visibleRect.height > 10 { - applied = true - if focus { - view.focusAnimation() - } - } - }) - - addScroll(listener: scrollListener) - - let bounds = NSMakeRect(0, rowRect.minY, clipView.bounds.width, clipView.bounds.height) - - - let getEdgeInset:()->CGFloat = { - if bounds.minY > self.clipView.bounds.minY { - return height - } else { - return -height - } - } - - if abs(bounds.minY - clipView.bounds.minY) < height { - clipView.scroll(to: bounds.origin, animated: animate, completion: { [weak self] _ in - self?.removeScroll(listener: scrollListener) - }) - } else { - let edgeRect:NSRect = NSMakeRect(clipView.bounds.minX, bounds.minY - getEdgeInset() - frame.minY, clipView.bounds.width, clipView.bounds.height) - clipView._changeBounds(from: edgeRect, to: bounds, animated: animate, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] completed in - self?.removeScroll(listener: scrollListener) - }) - } - } else { - if let item = item, focus { - viewNecessary(at: item.index)?.focusAnimation() - } - } - - } - - open override func setFrameSize(_ newSize: NSSize) { - let visible = visibleItems() - let oldWidth = frame.width - super.setFrameSize(newSize) - //updateStickAfterScroll(false) - if oldWidth != newSize.width { - saveScrollState(visible) - } - } - - public func setScrollHandler(_ handler: @escaping (_ scrollPosition:ScrollPosition) ->Void) -> Void { - - scrollHandler = handler - - } - - override open func scrollWheel(with event: NSEvent) { - super.scrollWheel(with: event) - if needUpdateVisibleAfterScroll { - let range = visibleRows() - for i in range.location ..< range.location + range.length { - if let view = viewNecessary(at: i) { - view.updateMouse() - } - } - } - - } - - public func enumerateItems(with callback:(TableRowItem)->Bool) { - for item in list { - if !callback(item) { - break - } - } - } - - public func enumerateVisibleItems(reversed: Bool = false, with callback:(TableRowItem)->Bool) { - let visible = visibleRows() - - if reversed { - for i in stride(from: visible.location + visible.length - 1, to: visible.location - 1, by: -1) { - if !callback(list[i]) { - break - } - } - } else { - for i in visible.location ..< visible.location + visible.length { - if !callback(list[i]) { - break - } - } - } - - } - - public func enumerateViews(with callback:(TableRowView)->Bool) { - for index in 0 ..< list.count { - if let view = viewNecessary(at: index) { - if !callback(view) { - break - } - } - } - } - - public func enumerateVisibleViews(with callback:(TableRowView)->Void) { - let visibleRows = self.visibleRows() - for index in visibleRows.location ..< visibleRows.location + visibleRows.length { - if let view = viewNecessary(at: index) { - callback(view) - } - } - } - - public func performScrollEvent() -> Void { - self.updateScroll(visibleRows()) - NotificationCenter.default.post(name: NSView.boundsDidChangeNotification, object: self.contentView) - } - - deinit { - mergeDisposable.dispose() - stickTimeoutDisposable.dispose() - } - - - -} diff --git a/TGUIKit/TGUIKit/TextButtonBarView.swift b/TGUIKit/TGUIKit/TextButtonBarView.swift deleted file mode 100644 index fc57ac4cc3..0000000000 --- a/TGUIKit/TGUIKit/TextButtonBarView.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// TextButtonBarView.swift -// TGUIKit -// -// Created by keepcoder on 05/10/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - -public enum TextBarAligment { - case Left - case Right - case Center -} - -open class TextButtonBarView: BarView { - - private(set) public var button:TitleButton - - public var alignment:TextBarAligment = .Center - - public init(controller: ViewController, text:String, style:ControlStyle = navigationButtonStyle, alignment:TextBarAligment = .Center) { - - - button = TitleButton(frame:NSZeroRect) - button.style = style - button.set(text: text, for: .Normal) - button.disableActions() - - super.init(controller: controller) - - self.alignment = alignment - - - self.addSubview(button) - - } - - override open func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - - button.set(background: presentation.colors.background, for: .Normal) - } - - open override func layout() { - switch alignment { - case .Center: - button.sizeToFit(NSZeroSize,NSMakeSize(frame.width, frame.height - .borderSize)) - case .Left: - button.sizeToFit(NSZeroSize,NSMakeSize(frame.width, frame.height - .borderSize)) - case .Right: - button.sizeToFit(NSZeroSize,NSMakeSize(frame.width - 20, frame.height - .borderSize), thatFit: true) - let f = focus(button.frame.size) - button.setFrameOrigin(NSMakePoint(frame.width - button.frame.width - 16, f.minY)) - } - super.layout() - } - - - - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - required public init(frame frameRect: NSRect) { - fatalError("init(frame:) has not been implemented") - } - -} diff --git a/TGUIKit/TGUIKit/TextView.swift b/TGUIKit/TGUIKit/TextView.swift deleted file mode 100644 index b1445a8732..0000000000 --- a/TGUIKit/TGUIKit/TextView.swift +++ /dev/null @@ -1,1116 +0,0 @@ -// -// TextView.swift -// TGUIKit -// -// Created by keepcoder on 15/09/16. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import SwiftSignalKitMac - - - -private enum CornerType { - case topLeft - case topRight - case bottomLeft - case bottomRight -} - -private func drawFullCorner(context: CGContext, color: NSColor, at point: CGPoint, type: CornerType, radius: CGFloat) { - context.setFillColor(color.cgColor) - switch type { - case .topLeft: - context.clear(CGRect(origin: point, size: CGSize(width: radius, height: radius))) - context.fillEllipse(in: CGRect(origin: point, size: CGSize(width: radius * 2.0, height: radius * 2.0))) - case .topRight: - context.clear(CGRect(origin: CGPoint(x: point.x - radius, y: point.y), size: CGSize(width: radius, height: radius))) - context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0))) - case .bottomLeft: - context.clear(CGRect(origin: CGPoint(x: point.x, y: point.y - radius), size: CGSize(width: radius, height: radius))) - context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0))) - case .bottomRight: - context.clear(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius))) - context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0))) - } -} - -private func drawConnectingCorner(context: CGContext, color: NSColor, at point: CGPoint, type: CornerType, radius: CGFloat) { - context.setFillColor(color.cgColor) - switch type { - case .topLeft: - context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y), size: CGSize(width: radius, height: radius))) - context.setFillColor(NSColor.clear.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0))) - case .topRight: - context.fill(CGRect(origin: CGPoint(x: point.x, y: point.y), size: CGSize(width: radius, height: radius))) - context.setFillColor(NSColor.clear.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0))) - case .bottomLeft: - context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius))) - context.setFillColor(NSColor.clear.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0))) - case .bottomRight: - context.fill(CGRect(origin: CGPoint(x: point.x, y: point.y - radius), size: CGSize(width: radius, height: radius))) - context.setFillColor(NSColor.clear.cgColor) - context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0))) - } -} - -private func generateRectsImage(color: NSColor, rects: [CGRect], inset: CGFloat, outerRadius: CGFloat, innerRadius: CGFloat) -> (CGPoint, CGImage?) { - if rects.isEmpty { - return (CGPoint(), nil) - } - - var topLeft = rects[0].origin - var bottomRight = CGPoint(x: rects[0].maxX, y: rects[0].maxY) - for i in 1 ..< rects.count { - topLeft.x = min(topLeft.x, rects[i].origin.x) - topLeft.y = min(topLeft.y, rects[i].origin.y) - bottomRight.x = max(bottomRight.x, rects[i].maxX) - bottomRight.y = max(bottomRight.y, rects[i].maxY) - } - - topLeft.x -= inset - topLeft.y -= inset - bottomRight.x += inset - bottomRight.y += inset - - return (topLeft, generateImage(CGSize(width: bottomRight.x - topLeft.x, height: bottomRight.y - topLeft.y), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - context.setFillColor(color.cgColor) - - context.setBlendMode(.copy) - - for i in 0 ..< rects.count { - let rect = rects[i].insetBy(dx: -inset, dy: -inset) - context.fill(rect.offsetBy(dx: -topLeft.x, dy: -topLeft.y)) - } - - for i in 0 ..< rects.count { - let rect = rects[i].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y) - - var previous: CGRect? - if i != 0 { - previous = rects[i - 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y) - } - - var next: CGRect? - if i != rects.count - 1 { - next = rects[i + 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y) - } - - if let previous = previous { - if previous.contains(rect.topLeft) { - if abs(rect.topLeft.x - previous.minX) >= innerRadius { - var radius = innerRadius - if let next = next { - radius = min(radius, floor((next.minY - previous.maxY) / 2.0)) - } - drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topLeft.x, y: previous.maxY), type: .topLeft, radius: radius) - } - } else { - drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: outerRadius) - } - if previous.contains(rect.topRight.offsetBy(dx: -1.0, dy: 0.0)) { - if abs(rect.topRight.x - previous.maxX) >= innerRadius { - var radius = innerRadius - if let next = next { - radius = min(radius, floor((next.minY - previous.maxY) / 2.0)) - } - drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topRight.x, y: previous.maxY), type: .topRight, radius: radius) - } - } else { - drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: outerRadius) - } - } else { - drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: outerRadius) - drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: outerRadius) - } - - if let next = next { - if next.contains(rect.bottomLeft) { - if abs(rect.bottomRight.x - next.maxX) >= innerRadius { - var radius = innerRadius - if let previous = previous { - radius = min(radius, floor((next.minY - previous.maxY) / 2.0)) - } - drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.bottomLeft.x, y: next.minY), type: .bottomLeft, radius: radius) - } - } else { - drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: outerRadius) - } - if next.contains(rect.bottomRight.offsetBy(dx: -1.0, dy: 0.0)) { - if abs(rect.bottomRight.x - next.maxX) >= innerRadius { - var radius = innerRadius - if let previous = previous { - radius = min(radius, floor((next.minY - previous.maxY) / 2.0)) - } - drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.bottomRight.x, y: next.minY), type: .bottomRight, radius: radius) - } - } else { - drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: outerRadius) - } - } else { - drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: outerRadius) - drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: outerRadius) - } - } - })) - -} - - - -public final class TextViewInteractions { - public var processURL:(Any!)->Void // link, isPresent - public var copy:(()->Bool)? - public var menuItems:(()->Signal<[ContextMenuItem], Void>)? - public init(processURL:@escaping (Any!)->Void = {_ in}, copy:(()-> Bool)? = nil, menuItems:(()->Signal<[ContextMenuItem], Void>)? = nil) { - self.processURL = processURL - self.copy = copy - self.menuItems = menuItems - } -} - -public final class TextViewLine { - public let line: CTLine - public let frame: NSRect - - init(line: CTLine, frame: CGRect) { - self.line = line - self.frame = frame - } - -} - - -public enum TextViewCutoutPosition { - case TopLeft - case TopRight -} - -public struct TextViewCutout: Equatable { - public let position: TextViewCutoutPosition - public let size: NSSize - public init(position:TextViewCutoutPosition, size:NSSize) { - self.position = position - self.size = size - } -} - -public func ==(lhs: TextViewCutout, rhs: TextViewCutout) -> Bool { - return lhs.position == rhs.position && lhs.size == rhs.size -} - -private let defaultFont:NSFont = .normal(.text) - -public final class TextViewLayout : Equatable { - - - public fileprivate(set) var attributedString:NSAttributedString - public fileprivate(set) var constrainedWidth:CGFloat = 0 - public var interactions:TextViewInteractions = TextViewInteractions() - public var selectedRange:TextSelectedRange = TextSelectedRange() - public var penFlush:CGFloat - public var insets:NSSize = NSZeroSize - public fileprivate(set) var lines:[TextViewLine] = [] - public fileprivate(set) var isPerfectSized:Bool = true - public let maximumNumberOfLines:Int32 - public let truncationType:CTLineTruncationType - public var cutout:TextViewCutout? - - fileprivate var monospacedImage:(CGPoint, CGImage?) = (CGPoint(), nil) - - public fileprivate(set) var lineSpacing:CGFloat? - - public private(set) var layoutSize:NSSize = NSZeroSize - public private(set) var perfectSize:NSSize = NSZeroSize - public init(_ attributedString:NSAttributedString, constrainedWidth:CGFloat = 0, maximumNumberOfLines:Int32 = INT32_MAX, truncationType: CTLineTruncationType = .end, cutout:TextViewCutout? = nil, alignment:NSTextAlignment = .left, lineSpacing:CGFloat? = nil) { - self.truncationType = truncationType - self.maximumNumberOfLines = maximumNumberOfLines - self.cutout = cutout - self.attributedString = attributedString - self.constrainedWidth = constrainedWidth - - switch alignment { - case .center: - penFlush = 0.5 - case .right: - penFlush = 1.0 - default: - penFlush = 0.0 - } - self.lineSpacing = lineSpacing - } - - func calculateLayout() -> Void { - - isPerfectSized = true - - let font: CTFont - if attributedString.length != 0 { - if let stringFont = attributedString.attribute(NSAttributedStringKey(kCTFontAttributeName as String), at: 0, effectiveRange: nil) { - font = stringFont as! CTFont - } else { - font = defaultFont - } - } else { - font = defaultFont - } - - self.lines.removeAll() - - let fontAscent = CTFontGetAscent(font) - let fontDescent = CTFontGetDescent(font) - - let fontLineHeight = floor(fontAscent + fontDescent) - - var monospacedRects:[NSRect] = [] - - var fontLineSpacing:CGFloat = floor(fontLineHeight * 0.12) - - - var maybeTypesetter: CTTypesetter? - maybeTypesetter = CTTypesetterCreateWithAttributedString(attributedString as CFAttributedString) - - let typesetter = maybeTypesetter! - - var lastLineCharacterIndex: CFIndex = 0 - var layoutSize = NSSize() - - var cutoutEnabled = false - var cutoutMinY: CGFloat = 0.0 - var cutoutMaxY: CGFloat = 0.0 - var cutoutWidth: CGFloat = 0.0 - var cutoutOffset: CGFloat = 0.0 - if let cutout = cutout { - cutoutMinY = -fontLineSpacing - cutoutMaxY = cutout.size.height + fontLineSpacing - cutoutWidth = cutout.size.width - if case .TopLeft = cutout.position { - cutoutOffset = cutoutWidth - } - cutoutEnabled = true - } - - var first = true - var breakInset: CGFloat = 0 - var isWasPreformatted: Bool = false - while true { - var lineConstrainedWidth = constrainedWidth - var lineOriginY: CGFloat = 0 - - var lineCutoutOffset: CGFloat = 0.0 - var lineAdditionalWidth: CGFloat = 0.0 - - var isPreformattedLine: CGFloat? = nil - - fontLineSpacing = floor(fontLineHeight * 0.12) - - - - if attributedString.length > 0, let space = (attributedString.attribute(.preformattedPre, at: min(lastLineCharacterIndex, attributedString.length - 1), effectiveRange: nil) as? NSNumber) { - breakInset = CGFloat(space.floatValue * 2) - lineCutoutOffset += CGFloat(space.floatValue) - lineAdditionalWidth += breakInset - - lineOriginY += CGFloat(space.floatValue/2) - - if !isWasPreformatted && !first { - lineOriginY += CGFloat(space.floatValue) - fontLineSpacing = CGFloat(space.floatValue) - fontLineSpacing - } else { - if isWasPreformatted || first { - fontLineSpacing = -CGFloat(space.floatValue/2) - lineOriginY -= (CGFloat(space.floatValue + space.floatValue/2)) - } - } - - isPreformattedLine = CGFloat(space.floatValue) - isWasPreformatted = true - } else { - - if isWasPreformatted && !first { - lineOriginY -= (2 - fontLineSpacing) - } - - isWasPreformatted = false - } - - lineOriginY += floor(layoutSize.height + fontLineHeight - fontLineSpacing * 2.0) - - if !first { - lineOriginY += fontLineSpacing - } - - if cutoutEnabled { - if lineOriginY < cutoutMaxY && lineOriginY + fontLineHeight > cutoutMinY { - lineConstrainedWidth = max(1.0, lineConstrainedWidth - cutoutWidth) - lineCutoutOffset = cutoutOffset - lineAdditionalWidth = cutoutWidth - } - } - - - - let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, lastLineCharacterIndex, Double(lineConstrainedWidth - breakInset)) - - - var lineHeight = fontLineHeight - - let lineString = attributedString.attributedSubstring(from: NSMakeRange(lastLineCharacterIndex, lineCharacterCount)) - if !lineString.string.emojiString.isEmpty { - lineHeight += floor(fontDescent) - if first { - lineOriginY += floor(fontDescent) - } - } - - if maximumNumberOfLines != 0 && lines.count == (Int(maximumNumberOfLines) - 1) && lineCharacterCount > 0 { - if first { - first = false - } else { - layoutSize.height += fontLineSpacing - } - - let coreTextLine: CTLine - - let originalLine = CTTypesetterCreateLineWithOffset(typesetter, CFRange(location: lastLineCharacterIndex, length: attributedString.length - lastLineCharacterIndex), 0.0) - - if CTLineGetTypographicBounds(originalLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(originalLine) < Double(constrainedWidth) { - coreTextLine = originalLine - } else { - var truncationTokenAttributes: [NSAttributedStringKey : Any] = [:] - truncationTokenAttributes[NSAttributedStringKey(kCTFontAttributeName as String)] = font - truncationTokenAttributes[NSAttributedStringKey(kCTForegroundColorFromContextAttributeName as String)] = true as NSNumber - let tokenString = "\u{2026}" - let truncatedTokenString = NSAttributedString(string: tokenString, attributes: truncationTokenAttributes) - let truncationToken = CTLineCreateWithAttributedString(truncatedTokenString) - - coreTextLine = CTLineCreateTruncatedLine(originalLine, Double(constrainedWidth), truncationType, truncationToken) ?? truncationToken - isPerfectSized = false - } - - - let lineWidth = ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine))) - let lineFrame = CGRect(x: lineCutoutOffset, y: lineOriginY, width: lineWidth, height: lineHeight) - layoutSize.height += lineHeight + fontLineSpacing - layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth) - - lines.append(TextViewLine(line: coreTextLine, frame: lineFrame)) - - break - } else { - if lineCharacterCount > 0 { - - - if first { - first = false - } else { - layoutSize.height += fontLineSpacing - } - - let coreTextLine = CTTypesetterCreateLineWithOffset(typesetter, CFRangeMake(lastLineCharacterIndex, lineCharacterCount), 100.0) - - - let lineWidth = ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine))) - let lineFrame = CGRect(x: lineCutoutOffset, y: lineOriginY, width: lineWidth, height: lineHeight) - layoutSize.height += lineHeight - layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth) - - if let space = lineString.attribute(.preformattedPre, at: 0, effectiveRange: nil) as? NSNumber { - - layoutSize.width = self.constrainedWidth - let preformattedSpace = CGFloat(space.floatValue) * 2 - - monospacedRects.append(NSMakeRect(0, lineFrame.minY - lineFrame.height, layoutSize.width, lineFrame.height + preformattedSpace)) - } - - lines.append(TextViewLine(line: coreTextLine, frame: lineFrame)) - lastLineCharacterIndex += lineCharacterCount - } else { - if !lines.isEmpty { - layoutSize.height += fontLineSpacing - } - break - } - } - - - if let isPreformattedLine = isPreformattedLine { - layoutSize.height += isPreformattedLine * 2 - if lastLineCharacterIndex == attributedString.length { - layoutSize.height += isPreformattedLine/2 - } - // fontLineSpacing = isPreformattedLine - } - - } - - let sortedIndices = (0 ..< monospacedRects.count).sorted(by: { monospacedRects[$0].width > monospacedRects[$1].width }) - for i in 0 ..< sortedIndices.count { - let index = sortedIndices[i] - for j in -1 ... 1 { - if j != 0 && index + j >= 0 && index + j < sortedIndices.count { - if abs(monospacedRects[index + j].width - monospacedRects[index].width) < 40.0 { - monospacedRects[index + j].size.width = max(monospacedRects[index + j].width, monospacedRects[index].width) - } - } - } - } - - self.monospacedImage = generateRectsImage(color: presentation.colors.grayBackground, rects: monospacedRects, inset: 0, outerRadius: .cornerRadius, innerRadius: .cornerRadius) - - //self.monospacedStrokeImage = generateRectsImage(color: presentation.colors.border, rects: monospacedRects, inset: 0, outerRadius: .cornerRadius, innerRadius: .cornerRadius) - - - self.layoutSize = layoutSize - } - - public func measure(width: CGFloat = 0) -> Void { - - if width != 0 { - constrainedWidth = width - } - calculateLayout() - - } - - public func clearSelect() { - self.selectedRange.range = NSMakeRange(NSNotFound, 0) - } - - public func selectedRange(startPoint:NSPoint, currentPoint:NSPoint) -> NSRange { - - var selectedRange:NSRange = NSMakeRange(NSNotFound, 0) - - if (currentPoint.x != -1 && currentPoint.y != -1 && !lines.isEmpty) { - - - let startSelectLineIndex = findIndex(location: startPoint) - let currentSelectLineIndex = findIndex(location: currentPoint) - let dif = abs(startSelectLineIndex - currentSelectLineIndex) - let isReversed = currentSelectLineIndex < startSelectLineIndex - var i = startSelectLineIndex - while isReversed ? i >= currentSelectLineIndex : i <= currentSelectLineIndex { - let line = lines[i].line - let lineRange = CTLineGetStringRange(line) - var startIndex: CFIndex = CTLineGetStringIndexForPosition(line, startPoint) - var endIndex: CFIndex = CTLineGetStringIndexForPosition(line, currentPoint) - if dif > 0 { - if i != currentSelectLineIndex { - endIndex = (lineRange.length + lineRange.location) - } - if i != startSelectLineIndex { - startIndex = lineRange.location - } - if isReversed { - if i == startSelectLineIndex { - endIndex = startIndex - startIndex = lineRange.location - } - if i == currentSelectLineIndex { - startIndex = endIndex - endIndex = (lineRange.length + lineRange.location) - } - } - } - if startIndex > endIndex { - startIndex = endIndex + startIndex - endIndex = startIndex - endIndex - startIndex = startIndex - endIndex - } - if abs(Int(startIndex) - Int(endIndex)) > 0 && (selectedRange.location == NSNotFound || selectedRange.location > startIndex) { - selectedRange.location = startIndex - } - selectedRange.length += (endIndex - startIndex) - i += isReversed ? -1 : 1 - } - } - return selectedRange - } - - - public func findIndex(location:NSPoint) -> Int { - - if location.y == .greatestFiniteMagnitude { - return lines.count - 1 - } else if location.y == 0 { - return 0 - } - - for idx in 0 ..< lines.count { - if isCurrentLine(pos: location, index: idx) { - return idx - } - } - - return location.y <= layoutSize.height ? 0 : (lines.count - 1) - - } - - public func inSelectedRange(_ location:NSPoint) -> Bool { - let index = findCharacterIndex(at: location) - return selectedRange.range.location < index && selectedRange.range.location + selectedRange.range.length > index - } - - public func isCurrentLine(pos:NSPoint, index:Int) -> Bool { - - let line = lines[index] - var rect = line.frame - - var ascent:CGFloat = 0 - var descent:CGFloat = 0 - var leading:CGFloat = 0 - - CTLineGetTypographicBounds(line.line, &ascent, &descent, &leading) - - rect.origin.y = rect.minY - rect.height + ceil(descent - leading) - rect.size.height += ceil(descent - leading) - - return (pos.y > rect.minY) && pos.y < rect.maxY - - } - - public func link(at point:NSPoint) -> (Any, NSRect)? { - - let index = findIndex(location: point) - - guard index != -1 else { - return nil - } - - let line = lines[index] - var ascent:CGFloat = 0 - var descent:CGFloat = 0 - var leading:CGFloat = 0 - - let width:CGFloat = CGFloat(CTLineGetTypographicBounds(line.line, &ascent, &descent, &leading)); - - if width > point.x { - var pos = CTLineGetStringIndexForPosition(line.line, point); - pos = min(max(0,pos),attributedString.length - 1) - var range:NSRange = NSMakeRange(NSNotFound, 0) - let attrs = attributedString.attributes(at: pos, effectiveRange: &range) - - let link:Any? = attrs[NSAttributedStringKey.link] - - if let link = link { - let startOffset = CTLineGetOffsetForStringIndex(line.line, range.location, nil); - let endOffset = CTLineGetOffsetForStringIndex(line.line, range.location + range.length, nil); - return (link, NSMakeRect(startOffset, line.frame.minY, endOffset - startOffset, ceil(ascent + ceil(descent) + leading))) - } - } - return nil - } - - func findCharacterIndex(at point:NSPoint) -> Int { - let index = findIndex(location: point) - - guard index != -1 else { - return -1 - } - - let line = lines[index] - let width:CGFloat = CGFloat(CTLineGetTypographicBounds(line.line, nil, nil, nil)); - if width > point.x { - let charIndex = Int(CTLineGetStringIndexForPosition(line.line, point)) - return charIndex == attributedString.length ? charIndex - 1 : charIndex - } - return -1 - } - - public func selectAll(at point:NSPoint) -> Void { - - let startIndex = findCharacterIndex(at: point) - if startIndex == -1 { - return - } - - var blockRange: NSRange = NSMakeRange(NSNotFound, 0) - if let _ = attributedString.attribute(.preformattedPre, at: startIndex, effectiveRange: &blockRange) { - self.selectedRange = TextSelectedRange(range: blockRange, color: .selectText, def: true) - } else { - self.selectedRange = TextSelectedRange(range: NSMakeRange(0,attributedString.length), color: .selectText, def: true) - } - - } - - public func selectWord(at point:NSPoint) -> Void { - let startIndex = findCharacterIndex(at: point) - if startIndex == -1 { - return - } - var prev = startIndex - var next = startIndex - var range = NSMakeRange(startIndex, 1) - let char:NSString = attributedString.string.nsstring.substring(with: range) as NSString - var effectiveRange:NSRange = NSMakeRange(NSNotFound, 0) - let check = attributedString.attribute(NSAttributedStringKey.link, at: range.location, effectiveRange: &effectiveRange) - if check != nil && effectiveRange.location != NSNotFound { - self.selectedRange = TextSelectedRange(range: effectiveRange, color: presentation.colors.selectText, def: true) - return - } - if char == "" { - self.selectedRange = TextSelectedRange() - return - } - let valid:Bool = char.trimmingCharacters(in: NSCharacterSet.alphanumerics) == "" || char == "_" - let string:NSString = attributedString.string.nsstring - while valid { - let prevChar = string.substring(with: NSMakeRange(prev, 1)) - let nextChar = string.substring(with: NSMakeRange(next, 1)) - var prevValid:Bool = prevChar.trimmingCharacters(in: NSCharacterSet.alphanumerics) == "" || prevChar == "_" - var nextValid:Bool = nextChar.trimmingCharacters(in: NSCharacterSet.alphanumerics) == "" || nextChar == "_" - if (prevValid && prev > 0) { - prev -= 1 - } - if(nextValid && next < string.length - 1) { - next += 1 - } - range.location = prevValid ? prev : prev + 1; - range.length = next - range.location; - if prev == 0 { - prevValid = false - } - if(next == string.length - 1) { - nextValid = false - range.length += 1 - } - if !prevValid && !nextValid { - break - } - if prev == 0 && !nextValid { - break - } - } - - self.selectedRange = TextSelectedRange(range: range, color: presentation.colors.selectText, def: true) - } - - -} - -public func ==(lhs:TextViewLayout, rhs:TextViewLayout) -> Bool { - return lhs.constrainedWidth == rhs.constrainedWidth && lhs.attributedString.isEqual(to: rhs.attributedString) && lhs.selectedRange == rhs.selectedRange && lhs.maximumNumberOfLines == rhs.maximumNumberOfLines && lhs.cutout == rhs.cutout && lhs.truncationType == rhs.truncationType && lhs.constrainedWidth == rhs.constrainedWidth -} - -public struct TextSelectedRange: Equatable { - public var range:NSRange = NSMakeRange(NSNotFound, 0) - public var color:NSColor = presentation.colors.selectText - public var def:Bool = true - - public var hasSelectText:Bool { - return range.location != NSNotFound - } -} - -public func ==(lhs:TextSelectedRange, rhs:TextSelectedRange) -> Bool { - return lhs.def == rhs.def && lhs.range.location == rhs.range.location && lhs.range.length == rhs.range.length -} - - -public class TextView: Control { - - private let menuDisposable = MetaDisposable() - - private(set) public var layout:TextViewLayout? - - private var beginSelect:NSPoint = NSZeroPoint - private var endSelect:NSPoint = NSZeroPoint - - public var canBeResponder:Bool = true - - public var isSelectable:Bool = true { - didSet { - if oldValue != isSelectable { - self.setNeedsDisplayLayer() - } - } - } - - - public override init() { - super.init(); - self.style = ControlStyle(backgroundColor:.white) -// wantsLayer = false -// self.layer?.delegate = nil - } - - public override var isFlipped: Bool { - return true - } - - public required init(frame frameRect: NSRect) { - super.init(frame:frameRect) - self.style = ControlStyle(backgroundColor:.white) -// wantsLayer = false -// self.layer?.delegate = nil - // self.layer?.drawsAsynchronously = System.drawAsync - } - - - public override func draw(_ layer: CALayer, in ctx: CGContext) { - //backgroundColor = .random - super.draw(layer, in: ctx) - - if let layout = layout { - - - ctx.setAllowsAntialiasing(true) - - ctx.setAllowsFontSmoothing(backingScaleFactor == 1.0) - ctx.setShouldSmoothFonts(backingScaleFactor == 1.0) - - - if let image = layout.monospacedImage.1 { - ctx.draw(image, in: NSMakeRect(layout.monospacedImage.0.x, layout.monospacedImage.0.y, image.backingSize.width, image.backingSize.height)) - } - - - - if layout.selectedRange.range.location != NSNotFound && isSelectable { - - var lessRange = layout.selectedRange.range - - var lines:[TextViewLine] = layout.lines - - let beginIndex:Int = 0 - let endIndex:Int = layout.lines.count - 1 - - - let isReversed = endIndex < beginIndex - - var i:Int = beginIndex - - while isReversed ? i >= endIndex : i <= endIndex { - - - let line = lines[i].line - var rect:NSRect = lines[i].frame - let lineRange = CTLineGetStringRange(line) - - var beginLineIndex:CFIndex = 0 - var endLineIndex:CFIndex = 0 - - if (lineRange.location + lineRange.length >= lessRange.location) && lessRange.length > 0 { - beginLineIndex = lessRange.location - let max = lineRange.length + lineRange.location - let maxSelect = max - beginLineIndex - - let selectLength = min(maxSelect,lessRange.length) - - lessRange.length-=selectLength - lessRange.location+=selectLength - - endLineIndex = beginLineIndex + selectLength - - var ascent:CGFloat = 0 - var descent:CGFloat = 0 - var leading:CGFloat = 0 - - var width:CGFloat = CGFloat(CTLineGetTypographicBounds(line, &ascent, &descent, &leading)); - - let startOffset = CTLineGetOffsetForStringIndex(line, beginLineIndex, nil); - let endOffset = CTLineGetOffsetForStringIndex(line, endLineIndex, nil); - - width = endOffset - startOffset; - - let blockValue:CGFloat = CGFloat((layout.attributedString.attribute(.preformattedPre, at: beginLineIndex, effectiveRange: nil) as? NSNumber)?.floatValue ?? 0) - - - - rect.size.width = width - blockValue / 2 - - rect.origin.x = startOffset + blockValue - rect.origin.y = rect.minY - rect.height + blockValue / 2 - rect.size.height += ceil(descent - leading) - let color:NSColor = presentation.colors.selectText - - ctx.setFillColor(color.cgColor) - ctx.fill(rect) - } - - i += isReversed ? -1 : 1 - - } - - } - - let textMatrix = ctx.textMatrix - let textPosition = ctx.textPosition - let startPosition = focus(layout.layoutSize).origin - - - - ctx.textMatrix = CGAffineTransform(scaleX: 1.0, y: -1.0) - - for i in 0 ..< layout.lines.count { - let line = layout.lines[i] - - let penOffset = CGFloat( CTLineGetPenOffsetForFlush(line.line, layout.penFlush, Double(frame.width))) + line.frame.minX - - ctx.textPosition = CGPoint(x: penOffset, y: startPosition.y + line.frame.minY) - CTLineDraw(line.line, ctx) - - } - - ctx.textMatrix = textMatrix - ctx.textPosition = CGPoint(x: textPosition.x, y: textPosition.y) - - } - - } - - - public override func rightMouseDown(with event: NSEvent) { - if let layout = layout, userInteractionEnabled { - if !layout.selectedRange.hasSelectText || !layout.inSelectedRange(convert(event.locationInWindow, from: nil)) { - layout.selectWord(at : self.convert(event.locationInWindow, from: nil)) - } - self.setNeedsDisplayLayer() - if layout.selectedRange.hasSelectText { - if let menuItems = layout.interactions.menuItems?() { - menuDisposable.set((menuItems |> deliverOnMainQueue).start(next:{ [weak self] items in - if let strongSelf = self { - let menu = NSMenu() - for item in items { - menu.addItem(item) - } - NSMenu.popUpContextMenu(menu, with: event, for: strongSelf) - } - })) - } else { - let menu = NSMenu() - let copy = NSMenuItem(title: localizedString("Text.Copy"), action: #selector(copy(_:)), keyEquivalent: "") - menu.addItem(copy) - NSMenu.popUpContextMenu(menu, with: event, for: self) - } - } else { - super.rightMouseDown(with: event) - } - } else { - super.rightMouseDown(with: event) - } - } - - /* - var view: NSTextView? = (self.window?.fieldEditor(true, forObject: self) as? NSTextView) - view?.isEditable = false - view?.isSelectable = true - view?.string = layout.attributedString.string - view?.selectedRange = NSRange(location: 0, length: view?.string?.length) - NSMenu.popUpContextMenu(view?.menu(for: event), with: event, for: view) - */ - - public override func menu(for event: NSEvent) -> NSMenu? { - - return nil - } - - deinit { - menuDisposable.dispose() - } - - public func isEqual(to layout:TextViewLayout) -> Bool { - return self.layout == layout - } - - public func update(_ layout:TextViewLayout?, origin:NSPoint? = nil) -> Void { - self.layout = layout - - - if let layout = layout { - self.set(selectedRange: layout.selectedRange.range, display: false) - let point:NSPoint - if let origin = origin { - point = origin - } else { - point = frame.origin - } - self.frame = NSMakeRect(point.x, point.y, layout.layoutSize.width + layout.insets.width, layout.layoutSize.height + layout.insets.height) - } else { - self.set(selectedRange: NSMakeRange(NSNotFound, 0), display: false) - } - self.setNeedsDisplayLayer() - } - - public func set(layout:TextViewLayout?) { - self.layout = layout - self.setNeedsDisplayLayer() - } - - func set(selectedRange range:NSRange, display:Bool = true) -> Void { - - - layout?.selectedRange = TextSelectedRange(range:range, color:.selectText, def:true) - - beginSelect = NSMakePoint(-1, -1) - endSelect = NSMakePoint(-1, -1) - - - if display { - self.setNeedsDisplayLayer() - } - - } - - public override func mouseDown(with event: NSEvent) { - if isSelectable { - self.window?.makeFirstResponder(nil) - } - super.mouseDown(with: event) - _mouseDown(with: event) - } - - func _mouseDown(with event: NSEvent) -> Void { - - if !isSelectable || !userInteractionEnabled { - return - } - - _ = self.becomeFirstResponder() - - set(selectedRange: NSMakeRange(NSNotFound, 0), display: false) - self.beginSelect = self.convert(event.locationInWindow, from: nil) - - self.setNeedsDisplayLayer() - - } - - public override func mouseDragged(with event: NSEvent) { - super.mouseDragged(with: event) - checkCursor(event) - _mouseDragged(with: event) - } - - func _mouseDragged(with event: NSEvent) -> Void { - if !isSelectable || !userInteractionEnabled { - return - } - - endSelect = self.convert(event.locationInWindow, from: nil) - if let layout = layout { - layout.selectedRange.range = layout.selectedRange(startPoint: beginSelect, currentPoint: endSelect) - } - self.setNeedsDisplayLayer() - } - - - public override func mouseEntered(with event: NSEvent) { - super.mouseEntered(with: event) - checkCursor(event) - } - - public override func mouseExited(with event: NSEvent) { - super.mouseExited(with: event) - checkCursor(event) - } - - public override func mouseMoved(with event: NSEvent) { - super.mouseMoved(with: event) - checkCursor(event) - } - - - - public override func mouseUp(with event: NSEvent) { - super.mouseUp(with: event) - - if let layout = layout, userInteractionEnabled { - let point = self.convert(event.locationInWindow, from: nil) - if event.clickCount == 3 { - layout.selectAll(at: point) - } else if event.clickCount == 2 || (event.type == .rightMouseUp && !layout.selectedRange.hasSelectText) { - layout.selectWord(at : point) - } else if !layout.selectedRange.hasSelectText || !isSelectable && event.clickCount == 1 { - if let (link,_) = layout.link(at: point) { - layout.interactions.processURL(link) - } - } - setNeedsDisplay() - } - } - - - - func checkCursor(_ event:NSEvent) -> Void { - let location = self.convert(event.locationInWindow, from: nil) - - if self.mouse(location , in: self.visibleRect) && mouseInside() && userInteractionEnabled { - - if let layout = layout, let (_, _) = layout.link(at: location) { - NSCursor.pointingHand.set() - } else if isSelectable { - NSCursor.iBeam.set() - } else { - NSCursor.arrow.set() - } - } else { - NSCursor.arrow.set() - } - } - - - - - public override func becomeFirstResponder() -> Bool { - if canBeResponder { - if let window = self.window { - return window.makeFirstResponder(self) - } - } - - - return false - } - - - public override func resignFirstResponder() -> Bool { - _resignFirstResponder() - return super.resignFirstResponder() - } - - func _resignFirstResponder() -> Void { - self.set(selectedRange: NSMakeRange(NSNotFound, 0)) - } - - public override func responds(to aSelector: Selector!) -> Bool { - - if NSStringFromSelector(aSelector) == "copy:" { - return self.layout?.selectedRange.range.location != NSNotFound - } - - return super.responds(to: aSelector) - } - - @objc public func copy(_ sender:Any) -> Void { - if let layout = layout, layout.selectedRange.range.location != NSNotFound { - if let copy = layout.interactions.copy { - if !copy() { - let pb = NSPasteboard.general - pb.declareTypes([.string], owner: self) - pb.setString(layout.attributedString.string.nsstring.substring(with: layout.selectedRange.range), forType: .string) - } - } else { - let pb = NSPasteboard.general - pb.declareTypes([.string], owner: self) - pb.setString(layout.attributedString.string.nsstring.substring(with: layout.selectedRange.range), forType: .string) - } - } - } - - @objc func paste(_ sender:Any) { - - } - - - - required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - -} diff --git a/TGUIKit/TGUIKit/TitleButton.swift b/TGUIKit/TGUIKit/TitleButton.swift deleted file mode 100644 index 9242139a47..0000000000 --- a/TGUIKit/TGUIKit/TitleButton.swift +++ /dev/null @@ -1,242 +0,0 @@ -// -// TitleButton.swift -// TGUIKit -// -// Created by keepcoder on 05/10/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - -public enum TitleButtonImageDirection { - case left - case right -} - -class TextLayerExt: CATextLayer { - - override func draw(in ctx: CGContext) { - ctx.setAllowsAntialiasing(true) - ctx.setShouldAntialias(true) - ctx.setShouldSmoothFonts(false) - ctx.setAllowsFontSmoothing(false) - super.draw(in: ctx) - } - -} - - -public class TitleButton: ImageButton { - - private var text:TextLayerExt = TextLayerExt() - - private var stateText:[ControlState:String] = [:] - private var stateColor:[ControlState:NSColor] = [:] - private var stateFont:[ControlState:NSFont] = [:] - - - public var direction: TitleButtonImageDirection = .left { - didSet { - if direction != oldValue { - updateLayout() - } - } - } - - public override init() { - super.init() - } - - public func set(text:String, for state:ControlState) -> Void { - stateText[state] = text - apply(state: self.controlState) - sizeToFit(NSZeroSize, self.frame.size) - } - - public func set(color:NSColor, for state:ControlState) -> Void { - stateColor[state] = color - apply(state: self.controlState) - } - - public func set(font:NSFont, for state:ControlState) -> Void { - stateFont[state] = font - apply(state: self.controlState) - } - - override public func apply(state: ControlState) { - let state:ControlState = self.isSelected ? .Highlight : state - super.apply(state: state) - - if let stateText = stateText[state] { - text.string = stateText - } else { - text.string = stateText[.Normal] - } - - if isEnabled { - if let stateColor = stateColor[state] { - text.foregroundColor = stateColor.cgColor - } else if let stateColor = stateColor[.Normal] { - text.foregroundColor = stateColor.cgColor - } else { - text.foregroundColor = style.foregroundColor.cgColor - } - } else { - text.foregroundColor = presentation.colors.grayText.cgColor - } - - - if let stateFont = stateFont[state] { - text.font = stateFont.fontName as CFTypeRef - text.fontSize = stateFont.pointSize - } else if let stateFont = stateFont[.Normal] { - text.font = stateFont.fontName as CFTypeRef - text.fontSize = stateFont.pointSize - } else { - text.font = style.font.fontName as CFTypeRef - text.fontSize = style.font.pointSize - } - - } - - public override func sizeToFit(_ addition: NSSize = NSZeroSize, _ maxSize:NSSize = NSZeroSize, thatFit:Bool = false) { - super.sizeToFit(addition) - - - let size:NSSize = self.size(with: self.text.string as! String?, font:NSFont(name: self.text.font as! String, size: text.fontSize)) - - var msize:NSSize = size - - if maxSize.width < size.width { - if let image = imageView.image { - msize.width += (image.backingSize.width + 12) // max size - } - } - - let maxWidth:CGFloat = !thatFit ? ( maxSize.width > 0 ? maxSize.width : msize.width ) : min(maxSize.width, size.width) - - - - var textSize:CGFloat = maxWidth - - if let image = imageView.image { - - textSize = min(maxWidth,size.width) - - let iwidth:CGFloat = (image.backingSize.width + 12) - - if textSize == maxWidth { - textSize -= iwidth - } else { - textSize = (maxWidth - size.width) >= iwidth ? size.width : maxWidth - iwidth - } - } - - - - self.text.frame = NSMakeRect(0, 0, textSize, size.height) - self.frame = CGRect(x: self.frame.origin.x, y: self.frame.origin.y, width: maxWidth, height: max(size.height,maxSize.height)) - - } - - public override func updateLayout() { - super.updateLayout() - - let textFocus:NSRect = focus(self.text.frame.size) - if let _ = imageView.image { - let imageFocus:NSRect = focus(self.imageView.frame.size) - switch direction { - case .left: - self.imageView.frame = NSMakeRect(round((self.frame.width - textFocus.width - imageFocus.width)/2.0 - 6.0), imageFocus.minY, imageFocus.width, imageFocus.height) - self.text.frame = NSMakeRect(imageView.frame.maxX + 6.0, textFocus.minY, textFocus.width, textFocus.height) - case .right: - self.imageView.frame = NSMakeRect(round(frame.width - imageFocus.width - 6.0), imageFocus.minY, imageFocus.width, imageFocus.height) - self.text.frame = NSMakeRect(0, textFocus.minY, textFocus.width, textFocus.height) - } - - } else { - self.text.frame = textFocus - } - - } - - - func size(with string: String?, font: NSFont?) -> NSSize { - if font == nil || string == nil { - return NSZeroSize - } - let attributedString:NSAttributedString = NSAttributedString.initialize(string: string, font: font, coreText: true) - var size:NSSize = attributedString.CTSize(CGFloat.greatestFiniteMagnitude, framesetter: nil).1 - size.width = ceil(size.width) + 10 - size.height = ceil(size.height) - return size - } - - - public override var style: ControlStyle { - set { - super.style = newValue -// -// self.set(color: style.foregroundColor, for: .Normal) -// self.set(color: style.highlightColor, for: .Highlight) -// self.set(font: style.font, for: .Normal) -// self.backgroundColor = style.backgroundColor - } - get { - return super.style - } - } - - override func prepare() { - super.prepare() - text.truncationMode = "middle"; - text.alignmentMode = "center"; - self.layer?.addSublayer(text) - - text.actions = ["bounds":NSNull(),"position":NSNull()] - } - - public override func draw(_ dirtyRect: NSRect) { - super.draw(dirtyRect) - } - - public required init(frame frameRect: NSRect) { - super.init(frame: frameRect) - - } - - public override var backgroundColor: NSColor { - set { - super.backgroundColor = newValue - self.text.backgroundColor = newValue.cgColor - } - get { - return super.backgroundColor - } - } - - public override func viewDidChangeBackingProperties() { - super.viewDidChangeBackingProperties() - if let screen = NSScreen.main { - self.text.contentsScale = screen.backingScaleFactor - } - - } - - public override func disableActions() { - super.disableActions() - - self.text.disableActions() - self.layer?.disableActions() - } - - public override func setFrameSize(_ newSize: NSSize) { - super.setFrameSize(newSize) - } - - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - -} diff --git a/TGUIKit/TGUIKit/TitledBarView.swift b/TGUIKit/TGUIKit/TitledBarView.swift deleted file mode 100644 index f90691d2e4..0000000000 --- a/TGUIKit/TGUIKit/TitledBarView.swift +++ /dev/null @@ -1,175 +0,0 @@ -// -// TitledBarView.swift -// TGUIKit -// -// Created by keepcoder on 16/09/16. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - -private class TitledContainerView : View { - - private var statusNode:TextNode = TextNode() - private var titleNode:TextNode = TextNode() - var titleImage:CGImage? { - didSet { - self.setNeedsDisplay() - } - } - - var inset:CGFloat = 50 - - var text:NSAttributedString? { - didSet { - if text != oldValue { - self.setNeedsDisplay() - } - } - } - - var status:NSAttributedString? { - didSet { - if status != oldValue { - self.setNeedsDisplay() - } - } - } - - var hiddenStatus:Bool = false { - didSet { - self.setNeedsDisplay() - } - } - - var textInset:CGFloat? = nil { - didSet { - self.setNeedsDisplay() - } - } - - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - backgroundColor = presentation.colors.background - } - - fileprivate override func draw(_ layer: CALayer, in ctx: CGContext) { - ctx.setFillColor(presentation.colors.background.cgColor) - ctx.fill(bounds) - if let text = text { - let (textLayout, textApply) = TextNode.layoutText(maybeNode: titleNode, text, nil, 1, .end, NSMakeSize(NSWidth(layer.bounds) - inset, NSHeight(layer.bounds)), nil,false, .left) - var tY = NSMinY(focus(textLayout.size)) - - if let status = status { - - let (statusLayout, statusApply) = TextNode.layoutText(maybeNode: statusNode, status, nil, 1, .end, NSMakeSize(NSWidth(layer.bounds) - inset, NSHeight(layer.bounds)), nil,false, .left) - - let t = textLayout.size.height + statusLayout.size.height + 2.0 - tY = (NSHeight(self.frame) - t) / 2.0 - - let sY = tY + textLayout.size.height + 2.0 - if !hiddenStatus { - statusApply.draw(NSMakeRect(textInset == nil ? floorToScreenPixels((layer.bounds.width - statusLayout.size.width)/2.0) : textInset!, sY, statusLayout.size.width, statusLayout.size.height), in: ctx, backingScaleFactor: backingScaleFactor) - } - } - - var textRect = NSMakeRect(textInset == nil ? floorToScreenPixels((layer.bounds.width - textLayout.size.width)/2.0) : textInset!, tY, textLayout.size.width, textLayout.size.height) - - if let titleImage = titleImage { - ctx.draw(titleImage, in: NSMakeRect(textInset == nil ? textRect.minX - titleImage.backingSize.width : textInset!, tY + 4, titleImage.backingSize.width, titleImage.backingSize.height)) - textRect.origin.x += floorToScreenPixels(titleImage.backingSize.width) + 4 - } - - textApply.draw(textRect, in: ctx, backingScaleFactor: backingScaleFactor) - } - } -} - -open class TitledBarView: BarView { - - public var titleImage:CGImage? { - didSet { - _containerView.titleImage = titleImage - } - } - - open override var backgroundColor: NSColor { - didSet { - containerView.backgroundColor = backgroundColor - } - } - - public var text:NSAttributedString? { - didSet { - if text != oldValue { - _containerView.inset = inset - _containerView.text = text - } - } - } - - public var status:NSAttributedString? { - didSet { - if status != oldValue { - _containerView.inset = inset - _containerView.status = status - } - } - } - - private let _containerView:TitledContainerView = TitledContainerView() - public var containerView:View { - return _containerView - } - - public var hiddenStatus:Bool = false { - didSet { - _containerView.hiddenStatus = hiddenStatus - } - } - - open var inset:CGFloat { - return 50 - } - - public var textInset:CGFloat? { - didSet { - _containerView.textInset = textInset - } - } - - open override func setFrameSize(_ newSize: NSSize) { - super.setFrameSize(newSize) - containerView.setFrameSize(newSize) - containerView.setNeedsDisplay() - } - public init(controller: ViewController, _ text:NSAttributedString? = nil, _ status:NSAttributedString? = nil, textInset:CGFloat? = nil) { - self.text = text - self.status = status - self.textInset = textInset - super.init(controller: controller) - addSubview(containerView) - _containerView.text = text - _containerView.status = status - _containerView.textInset = textInset - } - - open override func draw(_ dirtyRect: NSRect) { - - } - - deinit { - var bp:Int = 0 - bp += 1 - } - - - required public init(frame frameRect: NSRect) { - fatalError("init(frame:) has not been implemented") - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - -} diff --git a/TGUIKit/TGUIKit/TokenizedView.swift b/TGUIKit/TGUIKit/TokenizedView.swift deleted file mode 100644 index 600596dfb0..0000000000 --- a/TGUIKit/TGUIKit/TokenizedView.swift +++ /dev/null @@ -1,399 +0,0 @@ -// -// TokenizedView.swift -// TGUIKit -// -// Created by keepcoder on 07/08/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - -import Cocoa -import SwiftSignalKitMac - - -public struct SearchToken : Equatable { - public let name:String - public let uniqueId:Int64 - public init(name:String, uniqueId: Int64) { - self.name = name - self.uniqueId = uniqueId - } -} - -public func ==(lhs:SearchToken, rhs: SearchToken) -> Bool { - return lhs.name == rhs.name && lhs.uniqueId == rhs.uniqueId -} - -private class TokenView : Control { - fileprivate let token:SearchToken - private let dismiss: ImageButton = ImageButton() - private let nameView: TextView = TextView() - fileprivate var immediatlyPaste: Bool = true - override var isSelected: Bool { - didSet { - updateLocalizationAndTheme() - } - } - - init(_ token: SearchToken, maxSize: NSSize, onDismiss:@escaping()->Void, onSelect: @escaping()->Void) { - self.token = token - super.init() - self.layer?.cornerRadius = .cornerRadius - let layout = TextViewLayout(.initialize(string: token.name, color: .white, font: .normal(.title)), maximumNumberOfLines: 1) - layout.measure(width: maxSize.width - 30) - self.nameView.update(layout) - - nameView.userInteractionEnabled = false - nameView.isSelectable = false - - setFrameSize(NSMakeSize(layout.layoutSize.width + 30, maxSize.height)) - dismiss.autohighlight = false - updateLocalizationAndTheme() - needsLayout = true - addSubview(nameView) - addSubview(dismiss) - dismiss.set(handler: { _ in - onDismiss() - }, for: .Click) - set(handler: { _ in - onSelect() - }, for: .Click) - } - - fileprivate var isPerfectSized: Bool { - return nameView.layout?.isPerfectSized ?? false - } - - override func change(size: NSSize, animated: Bool = true, _ save: Bool = true, removeOnCompletion: Bool = false, duration: Double = 0.2, timingFunction: String = kCAMediaTimingFunctionEaseOut, completion: ((Bool) -> Void)? = nil) { - nameView.layout?.measure(width: size.width - 30) - - let size = NSMakeSize(min(((nameView.layout?.layoutSize.width ?? 0) + 30), size.width), size.height) - - super.change(size: size, animated: animated, save, duration: duration, timingFunction: timingFunction) - - let point = focus(dismiss.frame.size) - dismiss.change(pos: NSMakePoint(frame.width - 5 - dismiss.frame.width, point.minY), animated: animated) - - nameView.update(nameView.layout) - } - - override func layout() { - super.layout() - nameView.centerY(x: 5) - nameView.setFrameOrigin(5, nameView.frame.minY - 1) - dismiss.centerY(x: frame.width - 5 - dismiss.frame.width) - } - - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - dismiss.set(image: #imageLiteral(resourceName: "Icon_SearchClear").precomposed(NSColor.white.withAlphaComponent(0.7)), for: .Normal) - dismiss.set(image: #imageLiteral(resourceName: "Icon_SearchClear").precomposed(NSColor.white), for: .Highlight) - dismiss.sizeToFit() - nameView.backgroundColor = isSelected ? presentation.colors.blueSelect : presentation.colors.blueFill - self.background = isSelected ? presentation.colors.blueSelect : presentation.colors.blueFill - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - required public init(frame frameRect: NSRect) { - fatalError("init(frame:) has not been implemented") - } -} - -public protocol TokenizedProtocol { - func tokenizedViewDidChangedHeight(_ view: TokenizedView, height: CGFloat, animated: Bool) -} - -public class TokenizedView: ScrollView, AppearanceViewProtocol, NSTextViewDelegate { - private var tokens:[SearchToken] = [] - private let container: View = View() - private let input:SearchTextField = SearchTextField() - private(set) public var state: SearchFieldState = .None { - didSet { - stateValue.set(state) - } - } - public let stateValue: ValuePromise = ValuePromise(.None, ignoreRepeated: true) - private var selectedIndex: Int? = nil { - didSet { - for view in container.subviews { - if let view = view as? TokenView { - view.isSelected = selectedIndex != nil && view.token == tokens[selectedIndex!] - } - } - } - } - - - private let _tokensUpdater:Promise<[SearchToken]> = Promise([]) - public var tokensUpdater:Signal<[SearchToken], Void> { - return _tokensUpdater.get() - } - - private let _textUpdater:ValuePromise = ValuePromise("", ignoreRepeated: true) - public var textUpdater:Signal { - return _textUpdater.get() - } - - public var delegate: TokenizedProtocol? = nil - private let placeholder: TextView = TextView() - - public func addToken(token: SearchToken, animated: Bool) -> Void { - tokens.append(token) - - let view = TokenView(token, maxSize: NSMakeSize(100, 22), onDismiss: { [weak self] in - self?.removeToken(uniqueId: token.uniqueId, animated: true) - }, onSelect: { [weak self] in - self?.selectedIndex = self?.tokens.index(of: token) - }) - - container.addSubview(view) - layoutContainer(animated: animated) - _tokensUpdater.set(.single(tokens)) - input.string = "" - textDidChange(Notification(name: NSText.didChangeNotification)) - (contentView as? TGClipView)?.scroll(to: NSMakePoint(0, container.frame.height - frame.height), animated: animated) - } - - public func removeToken(uniqueId: Int64, animated: Bool) { - var index:Int? = nil - for i in 0 ..< tokens.count { - if tokens[i].uniqueId == uniqueId { - index = i - break - } - } - if let index = index { - tokens.remove(at: index) - for view in container.subviews { - if let view = view as? TokenView { - if view.token.uniqueId == uniqueId { - view.change(opacity: 0, animated: animated, completion: { [weak view] completed in - if completed { - view?.removeFromSuperview() - } - }) - - } - } - } - layoutContainer(animated: animated) - } - _tokensUpdater.set(.single(tokens)) - } - - private func layoutContainer(animated: Bool) { - CATransaction.begin() - - let mainw = frame.width - let between = NSMakePoint(5, 4) - var point: NSPoint = between - var extraLine: Bool = false - let count = container.subviews.count - for i in 0 ..< count { - let subview = container.subviews[i] - let next = container.subviews[min(i + 1, count - 1)] - if let token = subview as? TokenView, token.layer?.opacity != 0 { - token.change(pos: point, animated: token.immediatlyPaste ? false : animated) - if animated, token.immediatlyPaste { - token.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) - } - //token.change(size: NSMakeSize(100, token.frame.height), animated: animated) - token.immediatlyPaste = false - - point.x += subview.frame.width + between.x - - let dif = mainw - (point.x + (i == count - 1 ? mainw/3 : next.frame.width) + between.x) - if dif < between.x { - // if !token.isPerfectSized { - // token.change(size: NSMakeSize(frame.width - startPointX - between.x, token.frame.height), animated: animated) - // } - point.x = between.x - point.y += token.frame.height + between.y - } - - } - if subview == container.subviews.last { - if mainw - point.x > mainw/3 { - extraLine = true - } - } - } - - input.frame = NSMakeRect(point.x, point.y + 3, mainw - point.x - between.x, 16) - placeholder.change(pos: NSMakePoint(point.x + 6, point.y + 3), animated: animated) - placeholder.change(opacity: tokens.isEmpty ? 1.0 : 0.0, animated: animated) - let contentHeight = max(point.y + between.y + (extraLine ? 22 : 0), 30) - container.change(size: NSMakeSize(container.frame.width, contentHeight), animated: animated) - - let height = min(contentHeight, 108) - if height != frame.height { - - _change(size: NSMakeSize(mainw, height), animated: animated) - delegate?.tokenizedViewDidChangedHeight(self, height: height, animated: animated) - } - CATransaction.commit() - } - - public func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { - let range = input.selectedRange() - if range.location == 0 { - if commandSelector == #selector(moveLeft(_:)) { - if let index = selectedIndex { - selectedIndex = max(index - 1, 0) - } else { - selectedIndex = tokens.count - 1 - } - return true - } else if commandSelector == #selector(moveRight(_:)) { - if let index = selectedIndex { - if index + 1 == tokens.count { - selectedIndex = nil - input.setSelectedRange(NSMakeRange(input.string.length, 0)) - } else { - selectedIndex = index + 1 - } - return true - } - } - - if commandSelector == #selector(deleteBackward(_:)) { - if let selectedIndex = selectedIndex { - removeToken(uniqueId: tokens[selectedIndex].uniqueId, animated: true) - if selectedIndex != tokens.count { - self.selectedIndex = min(selectedIndex, tokens.count - 1) - } else { - self.selectedIndex = nil - input.setSelectedRange(NSMakeRange(input.string.length, 0)) - } - - return true - } else { - if !tokens.isEmpty { - self.selectedIndex = tokens.count - 1 - return true - } - } - - } - } - - - return false - } - - open func textDidChange(_ notification: Notification) { - - let pHidden = !input.string.isEmpty - if placeholder.isHidden != pHidden { - placeholder.isHidden = pHidden - } - _textUpdater.set(input.string) - selectedIndex = nil - } - - - - public func textDidEndEditing(_ notification: Notification) { - didResignResponder() - } - - public func textDidBeginEditing(_ notification: Notification) { - didBecomeResponder() - } - - public override var needsLayout: Bool { - set { - super.needsLayout = false - } - get { - return super.needsLayout - } - } - - public override func layout() { - super.layout() - //layoutContainer(animated: false) - } - - - private let localizationFunc: (String)->String - private let placeholderKey: String - required public init(frame frameRect: NSRect, localizationFunc: @escaping(String)->String, placeholderKey:String) { - self.localizationFunc = localizationFunc - self.placeholderKey = placeholderKey - super.init(frame: frameRect) - - hasVerticalScroller = true - container.frame = bounds - container.autoresizingMask = [.width] - contentView.documentView = container - - input.focusRingType = .none - input.backgroundColor = NSColor.clear - input.delegate = self - input.isRichText = false - - input.textContainer?.widthTracksTextView = true - input.textContainer?.heightTracksTextView = false - - input.isHorizontallyResizable = false - input.isVerticallyResizable = false - - - placeholder.set(handler: { [weak self] _ in - self?.window?.makeFirstResponder(self?.responder) - }, for: .Click) - - input.font = .normal(.text) - container.addSubview(input) - container.addSubview(placeholder) - container.layer?.cornerRadius = .cornerRadius - wantsLayer = true - self.layer?.cornerRadius = .cornerRadius - self.layer?.backgroundColor = presentation.colors.grayBackground.cgColor - updateLocalizationAndTheme() - } - - - - open func didResignResponder() { - state = .None - } - - open func didBecomeResponder() { - state = .Focus - - } - - override public func becomeFirstResponder() -> Bool { - window?.makeFirstResponder(input) - return true - } - - public var responder: NSResponder? { - return input - } - - public func updateLocalizationAndTheme() { - background = presentation.colors.background - contentView.background = presentation.colors.background - self.container.backgroundColor = presentation.colors.grayBackground - input.textColor = presentation.colors.text - input.insertionPointColor = presentation.search.textColor - let placeholderLayout = TextViewLayout(.initialize(string: localizedString(placeholderKey), color: presentation.colors.grayText, font: .normal(.title)), maximumNumberOfLines: 1) - placeholderLayout.measure(width: .greatestFiniteMagnitude) - placeholder.update(placeholderLayout) - placeholder.backgroundColor = presentation.colors.grayBackground - placeholder.isSelectable = false - layoutContainer(animated: false) - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - - -} diff --git a/TGUIKit/TGUIKit/View.swift b/TGUIKit/TGUIKit/View.swift deleted file mode 100644 index 9fc28c94a6..0000000000 --- a/TGUIKit/TGUIKit/View.swift +++ /dev/null @@ -1,347 +0,0 @@ -// -// View.swift -// TGUIKit -// -// Created by keepcoder on 06/09/16. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Foundation -import SwiftSignalKitMac -public let kUIKitAnimationBackground = "UIKitAnimationBackground" - -public protocol AppearanceViewProtocol { - func updateLocalizationAndTheme() -} - -class ViewLayer : CALayer { - override init(layer: Any) { - super.init(layer: layer) - } - - override open class func needsDisplay(forKey:String) -> Bool { - if forKey == kUIKitAnimationBackground { - return true - } - return false - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -public struct BorderType: OptionSet { - public var rawValue: UInt32 - - public init(rawValue: UInt32) { - self.rawValue = rawValue - } - - public init() { - self.rawValue = 0 - } - - public init(_ flags: BorderType) { - var rawValue: UInt32 = 0 - - if flags.contains(BorderType.Top) { - rawValue |= BorderType.Top.rawValue - } - - if flags.contains(BorderType.Bottom) { - rawValue |= BorderType.Bottom.rawValue - } - - if flags.contains(BorderType.Left) { - rawValue |= BorderType.Left.rawValue - } - - if flags.contains(BorderType.Right) { - rawValue |= BorderType.Right.rawValue - } - - self.rawValue = rawValue - } - - public static let Top = BorderType(rawValue: 1) - public static let Bottom = BorderType(rawValue: 2) - public static let Left = BorderType(rawValue: 4) - public static let Right = BorderType(rawValue: 8) -} - -public protocol ViewDisplayDelegate : class { - func draw(_ layer: CALayer, in ctx: CGContext); -} - -public class CustomViewHandlers { - public var size:((NSSize) ->Void)? - public var origin:((NSPoint) ->Void)? - public var layout:((View) ->Void)? - - deinit { - var bp:Int = 0 - bp += 1 - } -} - - - -open class View : NSView, CALayerDelegate, AppearanceViewProtocol { - - public var animates:Bool = false - - public var isEventLess: Bool = false - - public weak var displayDelegate:ViewDisplayDelegate? - - public let customHandler:CustomViewHandlers = CustomViewHandlers() - - open var backgroundColor:NSColor = presentation.colors.background { - didSet { - if oldValue != backgroundColor { - setNeedsDisplay() - } - } - } - - - public var flip:Bool = true - - public var border:BorderType? - - - open override func layout() { - super.layout() - if let layout = customHandler.layout { - layout(self) - } - } - - open func draw(_ layer: CALayer, in ctx: CGContext) { - - - if let displayDelegate = displayDelegate { - displayDelegate.draw(layer, in: ctx) - } else { - - // ctx.setShadow(offset: NSMakeSize(5.0, 5.0), blur: 0.0, color: .shadow.cgColor) - - ctx.setFillColor(self.backgroundColor.cgColor) - ctx.fill(bounds) - - if let border = border { - ctx.setFillColor(presentation.colors.border.cgColor) - - if border.contains(.Top) { - ctx.fill(NSMakeRect(0, !self.isFlipped ? NSHeight(self.frame) - .borderSize : 0, NSWidth(self.frame), .borderSize)) - } - if border.contains(.Bottom) { - ctx.fill(NSMakeRect(0, self.isFlipped ? NSHeight(self.frame) - .borderSize : 0, NSWidth(self.frame), .borderSize)) - } - if border.contains(.Left) { - ctx.fill(NSMakeRect(0, 0, .borderSize, NSHeight(self.frame))) - } - if border.contains(.Right) { - ctx.fill(NSMakeRect(NSWidth(self.frame) - .borderSize, 0, .borderSize, NSHeight(self.frame))) - } - - } - } - } - - public func setNeedsDisplay() -> Void { - self.layer?.setNeedsDisplay() - assertOnMainThread() - } - - - - open override var isFlipped: Bool { - return flip - } - - public init() { - super.init(frame: NSZeroRect) - assertOnMainThread() - self.wantsLayer = true - acceptsTouchEvents = true - self.layerContentsRedrawPolicy = .onSetNeedsDisplay - - // self.layer?.delegate = self - // self.layer?.isOpaque = false - // self.autoresizesSubviews = false - // self.layerContentsRedrawPolicy = .onSetNeedsDisplay - // self.layer?.drawsAsynchronously = System.drawAsync - } - - override required public init(frame frameRect: NSRect) { - super.init(frame: frameRect) - assertOnMainThread() - acceptsTouchEvents = true - self.wantsLayer = true - - - - // self.layer?.delegate = self - // self.layer?.isOpaque = false - self.layerContentsRedrawPolicy = .onSetNeedsDisplay - // self.layer?.drawsAsynchronously = System.drawAsync - } - - open override var translatesAutoresizingMaskIntoConstraints: Bool { - get { - return true - } - set { - - } - } - - open func mouseInside() -> Bool { - return super._mouseInside() - } - - public func change(pos position: NSPoint, animated: Bool, _ save:Bool = true, removeOnCompletion: Bool = true, duration:Double = 0.2, timingFunction: String = kCAMediaTimingFunctionEaseOut, completion:((Bool)->Void)? = nil) -> Void { - super._change(pos: position, animated: animated, save, removeOnCompletion: removeOnCompletion, duration: duration, timingFunction: timingFunction, completion: completion) - } - - public func change(size: NSSize, animated: Bool, _ save:Bool = true, removeOnCompletion: Bool = true, duration:Double = 0.2, timingFunction: String = kCAMediaTimingFunctionEaseOut, completion:((Bool)->Void)? = nil) { - super._change(size: size, animated: animated, save, removeOnCompletion: removeOnCompletion, duration: duration, timingFunction: timingFunction, completion: completion) - } - public func change(opacity to: CGFloat, animated: Bool = true, _ save:Bool = true, removeOnCompletion: Bool = true, duration:Double = 0.2, timingFunction: String = kCAMediaTimingFunctionEaseOut, completion:((Bool)->Void)? = nil) { - super._change(opacity: to, animated: animated, save, removeOnCompletion: removeOnCompletion, duration: duration, timingFunction: timingFunction, completion: completion) - - } - - open override func swipe(with event: NSEvent) { - super.swipe(with: event) - } - - open override func beginGesture(with event: NSEvent) { - super.beginGesture(with: event) - } - - open func updateLocalizationAndTheme() { - for subview in subviews { - if let subview = subview as? AppearanceViewProtocol { - subview.updateLocalizationAndTheme() - } - } - } - - open override func viewDidMoveToSuperview() { - if superview != nil { - guard #available(OSX 10.12, *) else { - // self.needsLayout = true - return - } - } - } - - open override func setFrameSize(_ newSize: NSSize) { - super.setFrameSize(newSize) - - if let size = customHandler.size { - size(newSize) - } - guard #available(OSX 10.12, *) else { - self.needsLayout = true - return - } - } - - public func notifySubviewsToLayout(_ subview:NSView) -> Void { - for sub in subview.subviews { - sub.needsLayout = true - } - } - - open override var needsLayout: Bool { - set { - super.needsLayout = newValue - if newValue { - - guard #available(OSX 10.12, *) else { - layout() - notifySubviewsToLayout(self) - return - } - - } - } - get { - return super.needsLayout - } - } - - - @objc func layoutInRunLoop() { - layout() - } - - open override func setFrameOrigin(_ newOrigin: NSPoint) { - super.setFrameOrigin(newOrigin) - if let origin = customHandler.origin { - origin(newOrigin) - } - guard #available(OSX 10.12, *) else { - self.needsLayout = true - return - } - } - - deinit { - assertOnMainThread() - } - - - open var responder:NSResponder? { - return self - } - - open func setNeedsDisplayLayer() -> Void { - self.layer?.setNeedsDisplay() - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - open override func mouseDown(with event: NSEvent) { - // self.window?.makeFirstResponder(nil) - super.mouseDown(with: event) - } - - open override func draw(_ dirtyRect: NSRect) { - - } - - public var hasVisibleModal:Bool { - if let contentView = self.window?.contentView { - for subview in contentView.subviews { - if subview is PopoverBackground { - return true - } - } - } - - - return false - } - - open override func copy() -> Any { - let copy:View = View(frame:bounds) - copy.layer?.contents = self.layer?.contents - return copy - } - - - - open var kitWindow: Window? { - return super.window as? Window - } - - - -} diff --git a/TGUIKit/TGUIKit/ViewController.swift b/TGUIKit/TGUIKit/ViewController.swift deleted file mode 100644 index 1df8b79f9c..0000000000 --- a/TGUIKit/TGUIKit/ViewController.swift +++ /dev/null @@ -1,612 +0,0 @@ -// -// ViewController.swift -// TGUIKit -// -// Created by keepcoder on 06/09/16. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Foundation -import SwiftSignalKitMac - -class ControllerToasterView : View { - - private weak var toaster:ControllerToaster? - private let textView:TextView = TextView() - required init(frame frameRect: NSRect) { - super.init(frame: frameRect) - addSubview(textView) - textView.isSelectable = false - self.autoresizingMask = [.width] - self.border = [.Bottom] - updateLocalizationAndTheme() - } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - self.backgroundColor = presentation.colors.background - self.textView.backgroundColor = presentation.colors.background - } - - - func update(with toaster:ControllerToaster) { - self.toaster = toaster - } - - override func layout() { - super.layout() - if let toaster = toaster { - toaster.text.measure(width: frame.width - 40) - textView.update(toaster.text) - textView.center() - } - self.setNeedsDisplayLayer() - } - - required public init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -public class ControllerToaster { - let text:TextViewLayout - var view:ControllerToasterView? - let disposable:MetaDisposable = MetaDisposable() - private let height:CGFloat - public init(text:NSAttributedString, height:CGFloat = 30.0) { - self.text = TextViewLayout(text, maximumNumberOfLines: 1, truncationType: .middle) - self.height = height - } - - public init(text:String, height:CGFloat = 30.0) { - self.text = TextViewLayout(NSAttributedString.initialize(string: text, color: presentation.colors.text, font: .medium(.text)), maximumNumberOfLines: 1, truncationType: .middle) - self.height = height - } - - func show(for controller:ViewController, timeout:Double, animated:Bool) { - assert(view == nil) - view = ControllerToasterView(frame: NSMakeRect(0, 0, controller.frame.width, height)) - view?.update(with: self) - controller.addSubview(view!) - - if animated { - view?.layer?.animatePosition(from: NSMakePoint(0, -height), to: NSZeroPoint, duration: 0.2) - } - - let signal:Signal = .single(Void()) |> delay(timeout, queue: Queue.mainQueue()) - disposable.set(signal.start(next:{ [weak self] in - self?.hide(true) - })) - } - - func hide(_ animated:Bool) { - if animated { - view?.layer?.animatePosition(from: NSZeroPoint, to: NSMakePoint(0, -height), duration: 0.2, removeOnCompletion:false, completion:{ [weak self] (completed) in - self?.view?.removeFromSuperview() - self?.view = nil - }) - } else { - view?.removeFromSuperview() - view = nil - disposable.dispose() - } - } - - deinit { - let view = self.view - view?.layer?.animatePosition(from: NSZeroPoint, to: NSMakePoint(0, -height), duration: 0.2, removeOnCompletion:false, completion:{ (completed) in - view?.removeFromSuperview() - }) - disposable.dispose() - } - -} - -open class ViewController : NSObject { - fileprivate var _view:NSView?; - public var _frameRect:NSRect - - private var toaster:ControllerToaster? - - public var atomicSize:Atomic = Atomic(value:NSZeroSize) - - weak open var navigationController:NavigationViewController? { - didSet { - if navigationController != oldValue { - updateNavigation(navigationController) - } - } - } - - public var noticeResizeWhenLoaded: Bool = true - - public var animationStyle:AnimationStyle = AnimationStyle(duration:0.4, function:kCAMediaTimingFunctionSpring) - public var bar:NavigationBarStyle = NavigationBarStyle(height:50) - - public var leftBarView:BarView! - public var centerBarView:TitledBarView! - public var rightBarView:BarView! - - public var popover:Popover? - open var modal:Modal? - - - private let _ready = Promise() - open var ready: Promise { - return self._ready - } - public var didSetReady:Bool = false - - public let isKeyWindow:Promise = Promise(false) - - public var view:NSView { - get { - if(_view == nil) { - loadView(); - } - - return _view!; - } - - } - - public var backgroundColor: NSColor { - set { - self.view.background = newValue - } - get { - return self.view.background - } - } - - open var enableBack:Bool { - return false - } - - open func executeReturn() -> Void { - self.navigationController?.back() - } - - open func updateNavigation(_ navigation:NavigationViewController?) { - - } - - open func navigationWillChangeController() { - - } - - open var sidebar:ViewController? { - return nil - } - - open var sidebarWidth:CGFloat { - return 350 - } - - public private(set) var internalId:Int = 0; - - public override init() { - _frameRect = NSZeroRect - self.internalId = Int(arc4random()); - super.init() - } - - public init(frame frameRect:NSRect) { - _frameRect = frameRect; - self.internalId = Int(arc4random()); - } - - open func readyOnce() -> Void { - if !didSetReady { - didSetReady = true - ready.set(.single(true)) - } - } - - open func updateLocalizationAndTheme() { - (view as? AppearanceViewProtocol)?.updateLocalizationAndTheme() - self.navigationController?.updateLocalizationAndTheme() - } - - open func loadView() -> Void { - if(_view == nil) { - - leftBarView = getLeftBarViewOnce() - centerBarView = getCenterBarViewOnce() - rightBarView = getRightBarViewOnce() - - let vz = viewClass() as! NSView.Type - _view = vz.init(frame: _frameRect); - _view?.autoresizingMask = [.width,.height] - - NotificationCenter.default.addObserver(self, selector: #selector(viewFrameChanged(_:)), name: NSView.frameDidChangeNotification, object: _view!) - - _ = atomicSize.swap(_view!.frame.size) - } - } - - open func requestUpdateBackBar() { - if isLoaded(), let leftBarView = leftBarView as? BackNavigationBar { - leftBarView.requestUpdate() - } - self.leftBarView.style = navigationButtonStyle - } - - open func requestUpdateCenterBar() { - setCenterTitle(defaultBarTitle) - } - - open func dismiss() { - if navigationController?.controller == self { - navigationController?.back() - } - } - - open func requestUpdateRightBar() { - (self.rightBarView as? TextButtonBarView)?.button.style = navigationButtonStyle - self.rightBarView.style = navigationButtonStyle - } - - - @objc func viewFrameChanged(_ notification:Notification) { - viewDidResized(frame.size) - } - - open func viewDidResized(_ size:NSSize) { - _ = atomicSize.swap(size) - } - - open func invokeNavigationBack() -> Bool { - return true - } - - open func getLeftBarViewOnce() -> BarView { - return enableBack ? BackNavigationBar(self) : BarView(controller: self) - } - - open var defaultBarTitle:String { - return localizedString(self.className) - } - - open func getCenterBarViewOnce() -> TitledBarView { - return TitledBarView(controller: self, .initialize(string: defaultBarTitle, color: presentation.colors.text, font: .medium(.title))) - } - - public func setCenterTitle(_ text:String) { - self.centerBarView.text = .initialize(string: text, color: presentation.colors.text, font: .medium(.title)) - } - - open func getRightBarViewOnce() -> BarView { - return BarView(controller: self) - } - - open func viewClass() ->AnyClass { - return View.self - } - - open func draggingItems(for pasteboard:NSPasteboard) -> [DragItem] { - return [] - } - - public func loadViewIfNeeded(_ frame:NSRect = NSZeroRect) -> Void { - - guard _view != nil else { - if !NSIsEmptyRect(frame) { - _frameRect = frame - } - self.loadView() - - return - } - } - - open func viewDidLoad() -> Void { - if noticeResizeWhenLoaded { - viewDidResized(view.frame.size) - } - } - - open func viewWillAppear(_ animated:Bool) -> Void { - - } - - deinit { - self.window?.removeObserver(for: self) - window?.removeAllHandlers(for: self) - NotificationCenter.default.removeObserver(self) - assertOnMainThread() - } - - open func viewWillDisappear(_ animated:Bool) -> Void { - //assert(self.window != nil) - if canBecomeResponder { - self.window?.removeObserver(for: self) - } - NotificationCenter.default.removeObserver(self, name: NSWindow.didBecomeKeyNotification, object: window) - NotificationCenter.default.removeObserver(self, name: NSWindow.didResignKeyNotification, object: window) - isKeyWindow.set(.single(false)) - } - - public func isLoaded() -> Bool { - return _view != nil - } - - open func viewDidAppear(_ animated:Bool) -> Void { - //assert(self.window != nil) - if canBecomeResponder { - self.window?.set(responder: {[weak self] () -> NSResponder? in - return self?.firstResponder() - }, with: self, priority: responderPriority) - if let become = becomeFirstResponder(), become == true { - self.window?.applyResponderIfNeeded() - } - } - - NotificationCenter.default.addObserver(self, selector: #selector(windowDidBecomeKey), name: NSWindow.didBecomeKeyNotification, object: window) - NotificationCenter.default.addObserver(self, selector: #selector(windowDidResignKey), name: NSWindow.didResignKeyNotification, object: window) - if let window = window { - isKeyWindow.set(.single(window.isKeyWindow)) - } - } - - @objc open func windowDidBecomeKey() { - isKeyWindow.set(.single(true)) - } - - @objc open func windowDidResignKey() { - isKeyWindow.set(.single(false)) - } - - open var canBecomeResponder: Bool { - return true - } - - open var removeAfterDisapper:Bool { - return false - } - - open func escapeKeyAction() -> KeyHandlerResult { - return .rejected - } - - open func backKeyAction() -> KeyHandlerResult { - return .rejected - } - - open func nextKeyAction() -> KeyHandlerResult { - return .invokeNext - } - - open func returnKeyAction() -> KeyHandlerResult { - return .rejected - } - - open func didRemovedFromStack() -> Void { - - } - - open func viewDidDisappear(_ animated:Bool) -> Void { - - } - - open func scrollup() { - - } - - open func becomeFirstResponder() -> Bool? { - - return false - } - - open var window:Window? { - return _view?.window as? Window - } - - open func firstResponder() -> NSResponder? { - return nil - } - - open var responderPriority:HandlerPriority { - return .low - } - - - - public var frame:NSRect { - get { - return isLoaded() ? self.view.frame : _frameRect - } - set { - self.view.frame = newValue - } - } - public var bounds:NSRect { - return isLoaded() ? self.view.bounds : NSMakeRect(0, 0, _frameRect.width, _frameRect.height - bar.height) - } - - public func addSubview(_ subview:NSView) -> Void { - self.view.addSubview(subview) - } - - public func removeFromSuperview() ->Void { - self.view.removeFromSuperview() - } - - - open func backSettings() -> (String,CGImage?) { - return (localizedString("Navigation.back"),#imageLiteral(resourceName: "Icon_NavigationBack").precomposed(presentation.colors.blueIcon)) - } - - open var popoverClass:AnyClass { - return Popover.self - } - - open func show(for control:Control, edge:NSRectEdge? = nil, inset:NSPoint = NSZeroPoint) -> Void { - if popover == nil { - self.popover = (self.popoverClass as! Popover.Type).init(controller: self) - } - - if let popover = popover { - popover.show(for: control, edge: edge, inset: inset) - } - } - - open func closePopover() -> Void { - self.popover?.hide() - } - - open func invokeNavigation(action:NavigationModalAction) { - _ = (self.ready.get() |> take(1) |> deliverOnMainQueue).start(next: { (ready) in - action.close() - }) - } - - public func show(toaster:ControllerToaster, for delay:Double = 3.0, animated:Bool = true) { - assert(isLoaded()) - if let toaster = self.toaster { - toaster.hide(true) - } - - self.toaster = toaster - toaster.show(for: self, timeout: delay, animated: animated) - - } -} - - -open class GenericViewController : ViewController where T:NSView { - public var genericView:T { - return super.view as! T - } - - open override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - genericView.background = presentation.colors.background - } - - override open func loadView() -> Void { - if(_view == nil) { - - leftBarView = getLeftBarViewOnce() - centerBarView = getCenterBarViewOnce() - rightBarView = getRightBarViewOnce() - - _view = initializer() - _view?.autoresizingMask = [.width,.height] - - NotificationCenter.default.addObserver(self, selector: #selector(viewFrameChanged(_:)), name: NSView.frameDidChangeNotification, object: _view!) - - _ = atomicSize.swap(_view!.frame.size) - } - viewDidLoad() - } - - - - open func initializer() -> T { - let vz = T.self as NSView.Type - //controller.bar.height - return vz.init(frame: NSMakeRect(_frameRect.minX, _frameRect.minY, _frameRect.width, _frameRect.height - bar.height)) as! T; - } - -} - - -open class ModalViewController : ViewController { - - open var closable:Bool { - return true - } - - - - open var background:NSColor { - return NSColor(0x000000, 0.27) - } - - - open var isFullScreen:Bool { - return false - } - - open var containerBackground: NSColor { - return presentation.colors.background - } - - open var dynamicSize:Bool { - return false - } - - - - open func measure(size:NSSize) { - - } - - open var modalInteractions:ModalInteractions? { - return nil - } - - open override var responderPriority: HandlerPriority { - return .modal - } - - open override func firstResponder() -> NSResponder? { - return self.view - } - - open func close() { - modal?.close() - } - - open var handleEvents:Bool { - return true - } - - open var handleAllEvents: Bool { - return true - } - - override open func loadView() -> Void { - if(_view == nil) { - - _view = initializer() - _view?.autoresizingMask = [.width,.height] - - NotificationCenter.default.addObserver(self, selector: #selector(viewFrameChanged(_:)), name: NSView.frameDidChangeNotification, object: _view!) - - _ = atomicSize.swap(_view!.frame.size) - } - viewDidLoad() - } - - open func initializer() -> NSView { - let vz = viewClass() as! NSView.Type - return vz.init(frame: NSMakeRect(_frameRect.minX, _frameRect.minY, _frameRect.width, _frameRect.height - bar.height)); - } - - -} - -open class TableModalViewController : ModalViewController { - override open var dynamicSize: Bool { - return true - } - - override open func measure(size: NSSize) { - self.modal?.resize(with:NSMakeSize(genericView.frame.width, min(size.height - 70, genericView.listHeight)), animated: false) - } - - override open func viewClass() -> AnyClass { - return TableView.self - } - - public var genericView:TableView { - return self.view as! TableView - } - - public func updateSize(_ animated: Bool) { - if let contentSize = self.modal?.window.contentView?.frame.size { - self.modal?.resize(with:NSMakeSize(genericView.frame.width, min(contentSize.height - 70, genericView.listHeight)), animated: animated) - } - } -} diff --git a/TGUIKit/TGUIKit/Window.swift b/TGUIKit/TGUIKit/Window.swift deleted file mode 100644 index 26a75274fe..0000000000 --- a/TGUIKit/TGUIKit/Window.swift +++ /dev/null @@ -1,481 +0,0 @@ -// -// Window.swift -// TGUIKit -// -// Created by keepcoder on 16/10/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - -public enum HandlerPriority: Int, Comparable { - case low = 0 - case medium = 1 - case high = 2 - case modal = 3 - -} - -public func <(lhs: HandlerPriority, rhs: HandlerPriority) -> Bool { - return lhs.rawValue < rhs.rawValue -} - -class KeyHandler : Comparable { - let handler:()->KeyHandlerResult - let object:WeakReference - let priority:HandlerPriority - let modifierFlags:NSEvent.ModifierFlags? - - init(_ handler:@escaping()->KeyHandlerResult, _ object:NSObject?, _ priority:HandlerPriority, _ flags:NSEvent.ModifierFlags?) { - self.handler = handler - self.object = WeakReference(value: object) - self.priority = priority - self.modifierFlags = flags - } -} -func ==(lhs: KeyHandler, rhs: KeyHandler) -> Bool { - return lhs.priority == rhs.priority -} -func <(lhs: KeyHandler, rhs: KeyHandler) -> Bool { - return lhs.priority < rhs.priority -} - -public enum SwipeDirection { - case left - case right - case none -} - -class SwipeHandler : Comparable { - let handler:(SwipeDirection)->KeyHandlerResult - let object:WeakReference - let priority:HandlerPriority - - init(_ handler:@escaping(SwipeDirection)->KeyHandlerResult, _ object:NSObject?, _ priority:HandlerPriority) { - self.handler = handler - self.object = WeakReference(value: object) - self.priority = priority - } -} -func ==(lhs: SwipeHandler, rhs: SwipeHandler) -> Bool { - return lhs.priority == rhs.priority -} -func <(lhs: SwipeHandler, rhs: SwipeHandler) -> Bool { - return lhs.priority < rhs.priority -} - - -class ResponderObserver : Comparable { - let handler:()->NSResponder? - let object:WeakReference - let priority:HandlerPriority - - init(_ handler:@escaping()->NSResponder?, _ object:NSObject?, _ priority:HandlerPriority) { - self.handler = handler - self.object = WeakReference(value: object) - self.priority = priority - } -} -func ==(lhs: ResponderObserver, rhs: ResponderObserver) -> Bool { - return lhs.priority == rhs.priority -} -func <(lhs: ResponderObserver, rhs: ResponderObserver) -> Bool { - return lhs.priority < rhs.priority -} - -class MouseObserver : Comparable { - let handler:(NSEvent)->KeyHandlerResult - let object:WeakReference - let priority:HandlerPriority - let type:NSEvent.EventType? - - init(_ handler:@escaping(NSEvent)->KeyHandlerResult, _ object:NSObject?, _ priority:HandlerPriority, _ type:NSEvent.EventType) { - self.handler = handler - self.object = WeakReference(value: object) - self.priority = priority - self.type = type - } -} -func ==(lhs: MouseObserver, rhs: MouseObserver) -> Bool { - return lhs.priority == rhs.priority -} -func <(lhs: MouseObserver, rhs: MouseObserver) -> Bool { - return lhs.priority < rhs.priority -} - -public enum KeyHandlerResult { - case invoked // invoke and return - case rejected // can invoke next priprity event - case invokeNext // invoke and send global event -} - -public class Window: NSWindow { - public var name: String = "TGUIKit.Window" - private var keyHandlers:[KeyboardKey:[KeyHandler]] = [:] - private var swipeHandlers:[SwipeHandler] = [] - private var responsders:[ResponderObserver] = [] - private var mouseHandlers:[NSEvent.EventType:[MouseObserver]] = [:] - private var swipePoints:[NSPoint] = [] - private var saver:WindowSaver? - public var initFromSaver:Bool = false - public var copyhandler:(()->Void)? = nil - public var closeInterceptor:(()->Void)? = nil - public func set(responder:@escaping() -> NSResponder?, with object:NSObject?, priority:HandlerPriority) { - responsders.append(ResponderObserver(responder, object, priority)) - } - - public func removeObserver(for object:NSObject) { - var copy:[ResponderObserver] = [] - for observer in responsders { - copy.append(observer) - } - for i in stride(from: copy.count - 1, to: -1, by: -1) { - if copy[i].object.value == object || copy[i].object.value == nil { - responsders.remove(at: i) - } - } - } - - - public func set(handler:@escaping() -> KeyHandlerResult, with object:NSObject, for key:KeyboardKey, priority:HandlerPriority = .low, modifierFlags:NSEvent.ModifierFlags? = nil) -> Void { - var handlers:[KeyHandler]? = keyHandlers[key] - if handlers == nil { - handlers = [] - keyHandlers[key] = handlers - } - keyHandlers[key]?.append(KeyHandler(handler, object, priority, modifierFlags)) - - } - - public func add(swipe handler:@escaping(SwipeDirection) -> KeyHandlerResult, with object:NSObject, priority:HandlerPriority = .low) -> Void { - swipeHandlers.append(SwipeHandler(handler, object, priority)) - } - - - public func removeAllHandlers(for object:NSObject) { - - var newKeyHandlers:[KeyboardKey:[KeyHandler]] = [:] - for (key, handlers) in keyHandlers { - newKeyHandlers[key] = handlers - } - - for (key, handlers) in keyHandlers { - var copy:[KeyHandler] = [] - for handle in handlers { - copy.append(handle) - } - for i in stride(from: copy.count - 1, to: -1, by: -1) { - if copy[i].object.value == object { - newKeyHandlers[key]?.remove(at: i) - } - } - } - self.keyHandlers = newKeyHandlers - - var newMouseHandlers:[NSEvent.EventType:[MouseObserver]] = [:] - for (key, handlers) in mouseHandlers { - newMouseHandlers[key] = handlers - } - - for (key, handlers) in mouseHandlers { - var copy:[MouseObserver] = [] - for handle in handlers { - copy.append(handle) - } - for i in stride(from: copy.count - 1, to: -1, by: -1) { - if copy[i].object.value == object { - newMouseHandlers[key]?.remove(at: i) - } - } - } - self.mouseHandlers = newMouseHandlers - - - var copyGesture:[SwipeHandler] = [] - for gesture in swipeHandlers { - copyGesture.append(gesture) - } - for i in stride(from: swipeHandlers.count - 1, to: -1 , by: -1) { - if copyGesture[i].object.value == object { - copyGesture.remove(at: i) - } - } - self.swipeHandlers = copyGesture - } - - public func remove(object:NSObject, for key:KeyboardKey) { - let handlers = keyHandlers[key] - if let handlers = handlers { - var copy:[KeyHandler] = [] - for handle in handlers { - copy.append(handle) - } - for i in stride(from: copy.count - 1, to: -1, by: -1) { - if copy[i].object.value == object || copy[i].object.value == nil { - keyHandlers[key]?.remove(at: i) - } - } - } - } - - private func cleanUndefinedHandlers() { - var newKeyHandlers:[KeyboardKey:[KeyHandler]] = [:] - for (key, handlers) in keyHandlers { - newKeyHandlers[key] = handlers - } - - for (key, handlers) in keyHandlers { - var copy:[KeyHandler] = [] - for handle in handlers { - copy.append(handle) - } - for i in stride(from: copy.count - 1, to: -1, by: -1) { - if copy[i].object.value == nil { - newKeyHandlers[key]?.remove(at: i) - } - } - } - self.keyHandlers = newKeyHandlers - - var newMouseHandlers:[NSEvent.EventType:[MouseObserver]] = [:] - for (key, handlers) in mouseHandlers { - newMouseHandlers[key] = handlers - } - - for (key, handlers) in mouseHandlers { - var copy:[MouseObserver] = [] - for handle in handlers { - copy.append(handle) - } - for i in stride(from: copy.count - 1, to: -1, by: -1) { - if copy[i].object.value == nil { - newMouseHandlers[key]?.remove(at: i) - } - } - } - self.mouseHandlers = newMouseHandlers - - var copyGesture:[SwipeHandler] = [] - for gesture in swipeHandlers { - copyGesture.append(gesture) - } - for i in stride(from: swipeHandlers.count - 1, to: -1 , by: -1) { - if copyGesture[i].object.value == nil { - copyGesture.remove(at: i) - } - } - self.swipeHandlers = copyGesture - - } - - public func set(mouseHandler:@escaping(NSEvent) -> KeyHandlerResult, with object:NSObject, for type:NSEvent.EventType, priority:HandlerPriority = .low) -> Void { - var handlers:[MouseObserver]? = mouseHandlers[type] - if handlers == nil { - handlers = [] - mouseHandlers[type] = handlers - } - mouseHandlers[type]?.append(MouseObserver(mouseHandler, object, priority, type)) - } - - public func remove(object:NSObject, for type:NSEvent.EventType) { - let handlers = mouseHandlers[type] - if let handlers = handlers { - var copy:[MouseObserver] = [] - for handle in handlers { - copy.append(handle) - } - for i in stride(from: copy.count - 1, to: -1, by: -1) { - if copy[i].object.value == object || copy[i].object.value == nil { - mouseHandlers[type]?.remove(at: i) - } - } - } - } - - - public func applyResponderIfNeeded() ->Void { - let sorted = responsders.sorted(by: >) - - for observer in sorted { - if let responder = observer.handler() { - if self.firstResponder != responder { - let _ = self.resignFirstResponder() - if responder.responds(to: NSSelectorFromString("window")) { - let window:NSWindow? = responder.value(forKey: "window") as? NSWindow - if window != self { - continue - } - } - self.makeFirstResponder(responder) - if let responder = responder as? NSTextField { - responder.setCursorToEnd() - } - } - break - } - } - } - - - - public override func close() { - if let closeInterceptor = closeInterceptor { - closeInterceptor() - return - } - if isReleasedWhenClosed { - super.close() - } else { - super.close() - } - } - - @objc public func pasteToFirstResponder(_ sender: Any) { - - applyResponderIfNeeded() - - if let firstResponder = firstResponder, firstResponder.responds(to: NSSelectorFromString("paste:")) { - firstResponder.performSelector(onMainThread: NSSelectorFromString("paste:"), with: sender, waitUntilDone: false) - } - } - - @objc public func copyFromFirstResponder(_ sender: Any) { - if let copyhandler = copyhandler { - copyhandler() - } else { - if let firstResponder = firstResponder, firstResponder.responds(to: NSSelectorFromString("copy:")) { - firstResponder.performSelector(onMainThread: NSSelectorFromString("copy:"), with: sender, waitUntilDone: false) - } - } - - } - - public override func sendEvent(_ event: NSEvent) { - - if sheets.isEmpty { - if event.type == .keyDown { - - - if KeyboardKey(rawValue:event.keyCode) != KeyboardKey.Escape { - applyResponderIfNeeded() - } - - cleanUndefinedHandlers() - - if let globalHandler = keyHandlers[.All]?.sorted(by: >).first, let keyCode = KeyboardKey(rawValue:event.keyCode) { - if let handle = keyHandlers[keyCode]?.sorted(by: >).first { - if globalHandler.object.value != handle.object.value { - if (handle.modifierFlags == nil || event.modifierFlags.contains(handle.modifierFlags!)) { - switch globalHandler.handler() { - case .invoked: - return - case .rejected: - break - case .invokeNext: - super.sendEvent(event) - return - } - } else { - // super.sendEvent(event) - // return - } - } - } - } - - if let keyCode = KeyboardKey(rawValue:event.keyCode), let handlers = keyHandlers[keyCode]?.sorted(by: >) { - loop: for handle in handlers { - - if (handle.modifierFlags == nil || event.modifierFlags.contains(handle.modifierFlags!)) { - - switch handle.handler() { - case .invoked: - return - case .rejected: - continue - case .invokeNext: - break loop - } - - } - } - } - - } else if let handlers = mouseHandlers[event.type] { - let sorted = handlers.sorted(by: >) - loop: for handle in sorted { - switch handle.handler(event) { - case .invoked: - return - case .rejected: - continue - case .invokeNext: - break loop - } - } - } - - super.sendEvent(event) - } else { - //super.sendEvent(event) - } - - - } - -// public func set(copy handler:@escaping()->Void) -> (()-> Void,NSEventModifierFlags?)? { -// return self.set(handler: handler, for: .C, priority:.low, modifierFlags: [.command]) -// } -// -// public func set(paste handler:@escaping()->Void) -> (()-> Void,NSEventModifierFlags?)? { -// return self.set(handler: handler, for: .V, modifierFlags: [.command]) -// } - - public func set(escape handler:@escaping() -> KeyHandlerResult, with object:NSObject, priority:HandlerPriority = .low, modifierFlags:NSEvent.ModifierFlags? = nil) -> Void { - set(handler: handler, with: object, for: .Escape, priority:priority, modifierFlags:modifierFlags) - } - - - - public override var canBecomeKey: Bool { - return true - } - - public func initSaver() { - self.initFromSaver = true - self.saver = .find(for: self) - if let saver = saver { - self.setFrame(saver.rect, display: true) - } - } - - @objc func windowDidNeedSaveState(_ notification: Notification) { - saver?.rect = frame - saver?.save() - } - - deinit { - NotificationCenter.default.removeObserver(self) - } - - public func windowImageShot() -> CGImage? { - return CGWindowListCreateImage(CGRect.null, [.optionIncludingWindow], CGWindowID(windowNumber), [.boundsIgnoreFraming]) - } - - - public override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing bufferingType: NSWindow.BackingStoreType, defer flag: Bool) { - super.init(contentRect: contentRect, styleMask: style, backing: bufferingType, defer: flag) - - self.acceptsMouseMovedEvents = true - self.contentView?.wantsLayer = true - - - self.contentView?.acceptsTouchEvents = true - NotificationCenter.default.addObserver(self, selector: #selector(windowDidNeedSaveState(_:)), name: NSWindow.didMoveNotification, object: self) - NotificationCenter.default.addObserver(self, selector: #selector(windowDidNeedSaveState(_:)), name: NSWindow.didResizeNotification, object: self) - - - - // self.contentView?.canDrawSubviewsIntoLayer = true - } -} diff --git a/Telegram-Mac.xcworkspace/contents.xcworkspacedata b/Telegram-Mac.xcworkspace/contents.xcworkspacedata index b725446cc7..b1fbe6bba0 100644 --- a/Telegram-Mac.xcworkspace/contents.xcworkspacedata +++ b/Telegram-Mac.xcworkspace/contents.xcworkspacedata @@ -2,22 +2,118 @@ + location = "group:core-xprojects/rnnoise/rnnoise.xcodeproj"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + location = "group:core-xprojects/LegacyReachability/LegacyReachability.xcodeproj"> + location = "group:core-xprojects/NetworkLogging/NetworkLogging.xcodeproj"> + location = "group:core-xprojects/CryptoUtils/CryptoUtils.xcodeproj"> + location = "group:core-xprojects/StringTransliteration/StringTransliteration.xcodeproj"> + location = "group:core-xprojects/MurMurHash32/MurMurHash32.xcodeproj"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Telegram-Mac.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Telegram-Mac.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/Telegram-Mac.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Telegram-Mac.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/Telegram-Mac.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000000..0c67376eba --- /dev/null +++ b/Telegram-Mac.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/Telegram-Mac.xcworkspace/xcuserdata/mikhailfilimonov.xcuserdatad/IDEFindNavigatorScopes.plist b/Telegram-Mac.xcworkspace/xcuserdata/mikhailfilimonov.xcuserdatad/IDEFindNavigatorScopes.plist deleted file mode 100644 index 5dd5da85fd..0000000000 --- a/Telegram-Mac.xcworkspace/xcuserdata/mikhailfilimonov.xcuserdatad/IDEFindNavigatorScopes.plist +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/Telegram-Mac.xcworkspace/xcuserdata/mikhailfilimonov.xcuserdatad/UserInterfaceState (MacBook-Pro-Mikhail's conflicted copy 2016-09-07).xcuserstate b/Telegram-Mac.xcworkspace/xcuserdata/mikhailfilimonov.xcuserdatad/UserInterfaceState (MacBook-Pro-Mikhail's conflicted copy 2016-09-07).xcuserstate deleted file mode 100644 index b100371056..0000000000 Binary files a/Telegram-Mac.xcworkspace/xcuserdata/mikhailfilimonov.xcuserdatad/UserInterfaceState (MacBook-Pro-Mikhail's conflicted copy 2016-09-07).xcuserstate and /dev/null differ diff --git a/Telegram-Mac.xcworkspace/xcuserdata/mikhailfilimonov.xcuserdatad/UserInterfaceState (MacBook-Pro-Mikhail's conflicted copy 2017-03-15).xcuserstate b/Telegram-Mac.xcworkspace/xcuserdata/mikhailfilimonov.xcuserdatad/UserInterfaceState (MacBook-Pro-Mikhail's conflicted copy 2017-03-15).xcuserstate deleted file mode 100644 index 865c30d5b5..0000000000 Binary files a/Telegram-Mac.xcworkspace/xcuserdata/mikhailfilimonov.xcuserdatad/UserInterfaceState (MacBook-Pro-Mikhail's conflicted copy 2017-03-15).xcuserstate and /dev/null differ diff --git a/Telegram-Mac.xcworkspace/xcuserdata/mikhailfilimonov.xcuserdatad/UserInterfaceState (MacBook-Pro-Mikhail's conflicted copy 2017-04-14).xcuserstate b/Telegram-Mac.xcworkspace/xcuserdata/mikhailfilimonov.xcuserdatad/UserInterfaceState (MacBook-Pro-Mikhail's conflicted copy 2017-04-14).xcuserstate deleted file mode 100644 index 86c5f55c19..0000000000 Binary files a/Telegram-Mac.xcworkspace/xcuserdata/mikhailfilimonov.xcuserdatad/UserInterfaceState (MacBook-Pro-Mikhail's conflicted copy 2017-04-14).xcuserstate and /dev/null differ diff --git a/Telegram-Mac.xcworkspace/xcuserdata/mikhailfilimonov.xcuserdatad/UserInterfaceState.xcuserstate b/Telegram-Mac.xcworkspace/xcuserdata/mikhailfilimonov.xcuserdatad/UserInterfaceState.xcuserstate deleted file mode 100644 index 51ea1f892f..0000000000 Binary files a/Telegram-Mac.xcworkspace/xcuserdata/mikhailfilimonov.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ diff --git a/Telegram-Mac.xcworkspace/xcuserdata/mikhailfilimonov.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Telegram-Mac.xcworkspace/xcuserdata/mikhailfilimonov.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist deleted file mode 100644 index f3ddaa1b2d..0000000000 --- a/Telegram-Mac.xcworkspace/xcuserdata/mikhailfilimonov.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ /dev/null @@ -1,247 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Telegram-Mac.xcworkspace/xcuserdata/mikhailfilimonov.xcuserdatad/xcdebugger/Expressions.xcexplist b/Telegram-Mac.xcworkspace/xcuserdata/mikhailfilimonov.xcuserdatad/xcdebugger/Expressions.xcexplist deleted file mode 100644 index ae4f22a9a5..0000000000 --- a/Telegram-Mac.xcworkspace/xcuserdata/mikhailfilimonov.xcuserdatad/xcdebugger/Expressions.xcexplist +++ /dev/null @@ -1,1472 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Telegram-Mac.xcworkspace/xcuserdata/mikhailfilimonov.xcuserdatad/xcschemes/xcschememanagement.plist b/Telegram-Mac.xcworkspace/xcuserdata/mikhailfilimonov.xcuserdatad/xcschemes/xcschememanagement.plist deleted file mode 100644 index ee3458dd76..0000000000 --- a/Telegram-Mac.xcworkspace/xcuserdata/mikhailfilimonov.xcuserdatad/xcschemes/xcschememanagement.plist +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/Telegram-Mac.xcworkspace/xcuserdata/peter.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Telegram-Mac.xcworkspace/xcuserdata/peter.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist deleted file mode 100644 index b029793357..0000000000 --- a/Telegram-Mac.xcworkspace/xcuserdata/peter.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - diff --git a/Telegram-Mac/AboutModalController.swift b/Telegram-Mac/AboutModalController.swift index b009cac752..21ac17bef3 100644 --- a/Telegram-Mac/AboutModalController.swift +++ b/Telegram-Mac/AboutModalController.swift @@ -9,6 +9,22 @@ import Cocoa import TGUIKit +var APP_VERSION_STRING: String { + var vText = "\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] ?? "1") (\(Bundle.main.infoDictionary?["CFBundleVersion"] ?? "0"))" + + + #if STABLE + vText += " Stable" + #elseif APP_STORE + vText += " AppStore" + #elseif ALPHA + vText += " Alpha" + #else + vText += " Beta" + #endif + return vText +} + fileprivate class AboutModalView : Control { fileprivate let copyright:TextView = TextView() fileprivate let descView:TextView = TextView() @@ -19,35 +35,42 @@ fileprivate class AboutModalView : Control { formatter.dateFormat = "yyyy" - let copyrightLayout = TextViewLayout(NSAttributedString.initialize(string: "Copyright © 2016 - \(formatter.string(from: Date(timeIntervalSinceReferenceDate: Date.timeIntervalSinceReferenceDate))) TELEGRAM MESSENGER", color: theme.colors.grayText, font: .normal(.text)), alignment: .center) + let copyrightLayout = TextViewLayout(.initialize(string: "Copyright © 2016 - \(formatter.string(from: Date(timeIntervalSinceReferenceDate: Date.timeIntervalSinceReferenceDate))) TELEGRAM MESSENGER", color: theme.colors.grayText, font: .normal(.text)), alignment: .center) copyrightLayout.measure(width:frameRect.width - 40) - var vText = "\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] ?? "1").\(Bundle.main.infoDictionary?["CFBundleVersion"] ?? "0")" - - - #if STABLE - vText += " Stable" - #elseif APP_STORE - vText += " AppStore" - #else - vText += " Beta" - #endif + let vText = APP_VERSION_STRING let attr = NSMutableAttributedString() _ = attr.append(string: appName, color: theme.colors.text, font: .medium(.header)) _ = attr.append(string: "\n\(vText)", color: theme.colors.grayText, font: .medium(.text)) + _ = attr.append(string: " (", color: theme.colors.grayText, font: .medium(.text)) + + let range = attr.append(string: L10n.x3vGGIWUTitle.lowercased(), color: theme.colors.accent, font: .medium(.text)) + attr.addAttribute(.link, value: "copy", range: range) + _ = attr.append(string: ")", color: theme.colors.grayText, font: .medium(.text)) + _ = attr.append(string: "\n\n") - _ = attr.append(string: tr(.aboutDescription), color: theme.colors.text, font: .normal(.text)) + + + _ = attr.append(string: L10n.aboutDescription, color: theme.colors.text, font: .normal(.text)) let descLayout = TextViewLayout(attr, alignment: .center) descLayout.measure(width:frameRect.width - 40) - + descLayout.interactions.copy = { + copyToClipboard(APP_VERSION_STRING) + return true + } + + descLayout.interactions.processURL = { _ in + var bp:Int = 0 + bp += 1 + } copyright.update(copyrightLayout) descView.update(descLayout) @@ -57,6 +80,8 @@ fileprivate class AboutModalView : Control { addSubview(descView) + + descView.isSelectable = false copyright.isSelectable = false } @@ -84,15 +109,23 @@ class AboutModalController: ModalViewController { bar = .init(height: 0) } + override func close(animationType: ModalAnimationCloseBehaviour = .common) { + super.close(animationType: animationType) + } + override func viewDidLoad() { super.viewDidLoad() - genericView.descView.layout?.interactions = TextViewInteractions(processURL: {[weak self] (url) in + genericView.descView.layout?.interactions.processURL = { [weak self] url in if let url = url as? inAppLink { execute(inapp: url) + } else if let url = url as? String, url == "copy" { + copyToClipboard(APP_VERSION_STRING) + self?.show(toaster: ControllerToaster(text: L10n.shareLinkCopied)) + return } self?.close() - }) + } readyOnce() } diff --git a/Telegram-Mac/AccentColorRowItem.swift b/Telegram-Mac/AccentColorRowItem.swift new file mode 100644 index 0000000000..944db4d05a --- /dev/null +++ b/Telegram-Mac/AccentColorRowItem.swift @@ -0,0 +1,325 @@ +// +// AccentColorRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 02/01/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import TGUIKit +import SwiftSignalKit + + +private func generateAccentColor(_ color: PaletteAccentColor, bubbled: Bool) -> CGImage { + return generateImage(CGSize(width: 42.0, height: 42.0), scale: System.backingScale, rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + context.setFillColor(color.accent.cgColor) + context.fillEllipse(in: bounds) + + if let messages = color.messages, bubbled { + let imageSize = CGSize(width: 16, height: 16) + let image = generateImage(imageSize, contextGenerator: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.round(size, size.height / 2) + let colors = messages.reversed() + if colors.count > 1 { + let gradientColors = colors.reversed().map { $0.cgColor } as CFArray + let delta: CGFloat = 1.0 / (CGFloat(colors.count) - 1.0) + var locations: [CGFloat] = [] + for i in 0 ..< colors.count { + locations.append(delta * CGFloat(i)) + } + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + ctx.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: rect.height), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]) + } else if let color = messages.first { + ctx.setFillColor(color.cgColor) + ctx.fill(rect) + } + })! + + context.draw(image, in: bounds.focus(imageSize)) + } + })! +} + +private func generateCustomSwatchImage() -> CGImage { + return generateImage(CGSize(width: 42.0, height: 42.0), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + let dotSize = CGSize(width: 10.0, height: 10.0) + + context.setFillColor(NSColor(rgb: 0xd33213).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 14.0, y: 16.0), size: dotSize)) + + context.setFillColor(NSColor(rgb: 0xf08200).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 14.0, y: 0.0), size: dotSize)) + + context.setFillColor(NSColor(rgb: 0xedb400).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 28.0, y: 8.0), size: dotSize)) + + context.setFillColor(NSColor(rgb: 0x70bb23).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 28.0, y: 24.0), size: dotSize)) + + context.setFillColor(NSColor(rgb: 0x5396fa).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 14.0, y: 32.0), size: dotSize)) + + context.setFillColor(NSColor(rgb: 0x9472ee).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 24.0), size: dotSize)) + + context.setFillColor(NSColor(rgb: 0xeb6ca4).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 8.0), size: dotSize)) + })! +} + +private func generateSelectedRing(backgroundColor: NSColor) -> CGImage { + return generateImage(CGSize(width: 32, height: 32), rotatedContext: { size, context in + context.clear(NSMakeRect(0, 0, size.width, size.height)) + context.setStrokeColor(backgroundColor.cgColor) + context.setLineWidth(2.0) + context.strokeEllipse(in: NSMakeRect(1.0, 1.0, size.width - 2.0, size.height - 2.0)) + })! +} + + +class AccentColorRowItem: GeneralRowItem { + let selectAccentColor:(AppearanceAccentColor?)->Void + let menuItems: (AppearanceAccentColor)->[ContextMenuItem] + let list: [AppearanceAccentColor] + let isNative: Bool + let theme: TelegramPresentationTheme + let context: AccountContext + init(_ initialSize: NSSize, stableId: AnyHashable, context: AccountContext, list: [AppearanceAccentColor], isNative: Bool, theme: TelegramPresentationTheme, viewType: GeneralViewType = .legacy, selectAccentColor: @escaping(AppearanceAccentColor?)->Void, menuItems: @escaping(AppearanceAccentColor)->[ContextMenuItem]) { + self.selectAccentColor = selectAccentColor + self.list = list + self.theme = theme + self.isNative = isNative + self.menuItems = menuItems + self.context = context + super.init(initialSize, height: 36 + viewType.innerInset.top + viewType.innerInset.bottom, stableId: stableId, viewType: viewType) + } + + + override func viewClass() -> AnyClass { + return AccentColorRowView.self + } +} + + +private final class AccentScrollView : ScrollView { + override func scrollWheel(with event: NSEvent) { + + var scrollPoint = contentView.bounds.origin + let isInverted: Bool = System.isScrollInverted + if event.scrollingDeltaY != 0 { + if isInverted { + scrollPoint.x += -event.scrollingDeltaY + } else { + scrollPoint.x -= event.scrollingDeltaY + } + } + if event.scrollingDeltaX != 0 { + if !isInverted { + scrollPoint.x += -event.scrollingDeltaX + } else { + scrollPoint.x -= event.scrollingDeltaX + } + } + if documentView!.frame.width > frame.width { + scrollPoint.x = min(max(0, scrollPoint.x), documentView!.frame.width - frame.width) + clipView.scroll(to: scrollPoint) + } else { + superview?.scrollWheel(with: event) + } + } +} + +final class AccentColorRowView : TableRowView { + private let containerView = GeneralRowContainerView(frame: NSZeroRect) + private let scrollView: AccentScrollView = AccentScrollView() + private let borderView:View = View() + private let documentView: View = View() + + private let disposable = MetaDisposable() + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + containerView.addSubview(scrollView) + scrollView.documentView = documentView + scrollView.backgroundColor = .clear + scrollView.background = .clear + containerView.addSubview(borderView) + documentView.backgroundColor = .clear + addSubview(containerView) + } + + override var backdorColor: NSColor { + guard let item = item as? AccentColorRowItem else { + return theme.colors.background + } + return item.theme.colors.background + } + + override func updateColors() { + guard let item = item as? AccentColorRowItem else { + return + } + self.containerView.backgroundColor = backdorColor + borderView.backgroundColor = item.theme.colors.border + self.backgroundColor = item.viewType.rowBackground + } + + override func layout() { + super.layout() + + guard let item = item as? AccentColorRowItem else { + return + } + + let innerInset = item.viewType.innerInset + + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) + self.containerView.setCorners(item.viewType.corners) + self.borderView.frame = NSMakeRect(innerInset.left, self.containerView.frame.height - .borderSize, self.containerView.frame.width - innerInset.left - innerInset.right, .borderSize) + + scrollView.frame = NSMakeRect(0, innerInset.top, item.blockWidth, containerView.frame.height - innerInset.top - innerInset.bottom) + } + + override func menu(for event: NSEvent) -> NSMenu? { + guard let item = item as? AccentColorRowItem else { + return nil + } + + let documentPoint = documentView.convert(event.locationInWindow, from: nil) + + for (_, subview) in documentView.subviews.enumerated() { + if NSPointInRect(documentPoint, subview.frame), let accent = (subview as? Button)?.contextObject as? AppearanceAccentColor { + let items = item.menuItems(accent) + let menu = ContextMenu() + for item in items { + menu.addItem(item) + } + return menu + } + } + + return nil + } + + private let selectedImageView = ImageView() + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + documentView.removeAllSubviews() + + guard let item = item as? AccentColorRowItem else { + return + } + + self.layout() + + selectedImageView.image = generateSelectedRing(backgroundColor: theme.colors.background) + selectedImageView.setFrameSize(NSMakeSize(32, 32)) + selectedImageView.removeFromSuperview() + var colorList: [AppearanceAccentColor] = item.list + + borderView.isHidden = !item.viewType.hasBorder + + let insetWidth: CGFloat = 20 + + var x: CGFloat = insetWidth + + + if item.isNative { + let custom = ImageButton(frame: NSMakeRect(x, 0, 36, 36)) + custom.autohighlight = false + custom.animates = false + custom.set(image: generateCustomSwatchImage(), for: .Normal) + custom.setImageContentGravity(.resize) + custom.set(handler: { _ in + item.selectAccentColor(nil) + }, for: .Click) + documentView.addSubview(custom) + + x += custom.frame.width + insetWidth + } + + if !colorList.contains(where: { $0.accent.accent == theme.colors.accent && $0.cloudTheme?.id == theme.cloudTheme?.id }) { + let button = ImageButton(frame: NSMakeRect(x, 0, 36, 36)) + button.autohighlight = false + button.animates = false + button.layer?.cornerRadius = button.frame.height / 2 + button.set(background: theme.colors.accent, for: .Normal) + button.addSubview(selectedImageView) + selectedImageView.center() + x += button.frame.width + insetWidth + documentView.addSubview(button) + } + + let disposableSet = DisposableSet() + + for i in 0 ..< colorList.count { + + var accent = colorList[i] + + if let cloudTheme = accent.cloudTheme, let settings = cloudTheme.settings { + let signal = themeAppearanceThumbAndData(context: item.context, bubbled: false, source: .local(settings.palette, cloudTheme)) |> deliverOnMainQueue + disposableSet.add(signal.start(next: { result in + switch result.1 { + case let .cloud(_, cachedData): + accent = accent.withUpdatedCachedTheme(cachedData) + default: + break + } + })) + } + + let button = ImageButton(frame: NSMakeRect(x, 0, 36, 36)) + button.autohighlight = false + button.animates = false + // button.layer?.cornerRadius = button.frame.height / 2 + let icon = generateAccentColor(accent.accent, bubbled: theme.bubbled) + button.contextObject = colorList[i] + button.setImageContentGravity(.resize) + button.set(image: icon, for: .Normal) + button.set(image: icon, for: .Hover) + button.set(image: icon, for: .Highlight) + button.set(handler: { _ in + item.selectAccentColor(accent) + }, for: .Click) + if accent.accent.accent == theme.colors.accent { + if accent.cloudTheme?.id == theme.cloudTheme?.id { + button.addSubview(selectedImageView) + selectedImageView.center() + } + } + documentView.addSubview(button) + x += button.frame.width + insetWidth + + if i == colorList.count - 1 { + x -= insetWidth + } + } + + + disposable.set(disposableSet) + + + + + documentView.setFrameSize(NSMakeSize(x + insetWidth, frame.height)) + } + + deinit { + disposable.dispose() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/AccountContext.swift b/Telegram-Mac/AccountContext.swift new file mode 100644 index 0000000000..b51adb0d81 --- /dev/null +++ b/Telegram-Mac/AccountContext.swift @@ -0,0 +1,793 @@ +// +// AccountContext.swift +// Telegram +// +// Created by Mikhail Filimonov on 25/02/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Foundation +import SwiftSignalKit +import Postbox +import TelegramCore + +import TGUIKit + + +protocol ChatLocationContextHolder: class { +} + + + +enum ChatLocation: Equatable { + case peer(PeerId) + case replyThread(ChatReplyThreadMessage) +} + + + +struct TemporaryPasswordContainer { + let date: TimeInterval + let password: String + + var isActive: Bool { + return date + 15 * 60 > Date().timeIntervalSince1970 + } +} + +enum ApplyThemeUpdate { + case local(ColorPalette) + case cloud(TelegramTheme) +} + +final class AccountContextBindings { + #if !SHARE + let rootNavigation: () -> MajorNavigationController + let mainController: () -> MainViewController + let showControllerToaster: (ControllerToaster, Bool) -> Void + let globalSearch:(String)->Void + let switchSplitLayout:(SplitViewState)->Void + let entertainment:()->EntertainmentViewController + let needFullsize:()->Void + let displayUpgradeProgress:(CGFloat)->Void + let callSession: ()->PCallSession? + let groupCall: ()->GroupCallContext? + let getContext:()->AccountContext? + init(rootNavigation: @escaping() -> MajorNavigationController = { fatalError() }, mainController: @escaping() -> MainViewController = { fatalError() }, showControllerToaster: @escaping(ControllerToaster, Bool) -> Void = { _, _ in fatalError() }, globalSearch: @escaping(String) -> Void = { _ in fatalError() }, entertainment: @escaping()->EntertainmentViewController = { fatalError() }, switchSplitLayout: @escaping(SplitViewState)->Void = { _ in fatalError() }, needFullsize: @escaping() -> Void = { fatalError() }, displayUpgradeProgress: @escaping(CGFloat)->Void = { _ in fatalError() }, callSession: @escaping()->PCallSession? = { return nil }, groupCall: @escaping()->GroupCallContext? = { return nil }, getContext:@escaping()->AccountContext? = { return nil }) { + self.rootNavigation = rootNavigation + self.mainController = mainController + self.showControllerToaster = showControllerToaster + self.globalSearch = globalSearch + self.entertainment = entertainment + self.switchSplitLayout = switchSplitLayout + self.needFullsize = needFullsize + self.displayUpgradeProgress = displayUpgradeProgress + self.callSession = callSession + self.groupCall = groupCall + self.getContext = getContext + } + #endif +} + +private var lastTimeFreeSpaceNotified: TimeInterval? + + + +final class AccountContext { + let sharedContext: SharedAccountContext + let account: Account + let window: Window + + #if !SHARE + let fetchManager: FetchManager + let diceCache: DiceCache + let cachedGroupCallContexts: AccountGroupCallContextCacheImpl + #endif + private(set) var timeDifference:TimeInterval = 0 + #if !SHARE + let peerChannelMemberCategoriesContextsManager: PeerChannelMemberCategoriesContextsManager + let chatUndoManager = ChatUndoManager() + let blockedPeersContext: BlockedPeersContext + let cacheCleaner: AccountClearCache + let activeSessionsContext: ActiveSessionsContext + let webSessions: WebSessionsContext + private var chatInterfaceTempState:[PeerId : ChatInterfaceTempState] = [:] + private let _chatThemes: Promise<[(String, TelegramPresentationTheme)]> = Promise([]) + var chatThemes: Signal<[(String, TelegramPresentationTheme)], NoError> { + return _chatThemes.get() |> deliverOnMainQueue + } + #endif + + let cancelGlobalSearch:ValuePromise = ValuePromise(ignoreRepeated: false) + + + + var isCurrent: Bool = false { + didSet { + if !self.isCurrent { + //self.callManager = nil + } + } + } + + + let globalPeerHandler:Promise = Promise() + + func updateGlobalPeer() { + globalPeerHandler.set(globalPeerHandler.get() |> take(1)) + } + + let hasPassportSettings: Promise = Promise(false) + + private var _recentlyPeerUsed:[PeerId] = [] + private let _recentlyUserPeerIds = ValuePromise<[PeerId]>([]) + var recentlyUserPeerIds:Signal<[PeerId], NoError> { + return _recentlyUserPeerIds.get() + } + + private(set) var recentlyPeerUsed:[PeerId] { + set { + _recentlyPeerUsed = newValue + _recentlyUserPeerIds.set(newValue) + } + get { + return _recentlyPeerUsed + } + } + + var peerId: PeerId { + return account.peerId + } + + private let updateDifferenceDisposable = MetaDisposable() + private let temporaryPwdDisposable = MetaDisposable() + private let actualizeCloudTheme = MetaDisposable() + private let applyThemeDisposable = MetaDisposable() + private let cloudThemeObserver = MetaDisposable() + private let freeSpaceDisposable = MetaDisposable() + private let prefDisposable = DisposableSet() + private let _limitConfiguration: Atomic = Atomic(value: LimitsConfiguration.defaultValue) + + var limitConfiguration: LimitsConfiguration { + return _limitConfiguration.with { $0 } + } + + private let _appConfiguration: Atomic = Atomic(value: AppConfiguration.defaultValue) + + var appConfiguration: AppConfiguration { + return _appConfiguration.with { $0 } + } + + + private let isKeyWindowValue: ValuePromise = ValuePromise(ignoreRepeated: true) + + var isKeyWindow: Signal { + return isKeyWindowValue.get() |> deliverOnMainQueue + } + + private let _autoplayMedia: Atomic = Atomic(value: AutoplayMediaPreferences.defaultSettings) + + var autoplayMedia: AutoplayMediaPreferences { + return _autoplayMedia.with { $0 } + } + + + var isInGlobalSearch: Bool = false + + + private let _contentSettings: Atomic = Atomic(value: ContentSettings.default) + + var contentSettings: ContentSettings { + return _contentSettings.with { $0 } + } + + // public let tonContext: StoredTonContext! + + public var closeFolderFirst: Bool = false + + private let preloadGifsDisposable = MetaDisposable() + let engine: TelegramEngine + + + init(sharedContext: SharedAccountContext, window: Window, account: Account) { + self.sharedContext = sharedContext + self.account = account + self.window = window + self.engine = TelegramEngine(account: account) + #if !SHARE + self.peerChannelMemberCategoriesContextsManager = PeerChannelMemberCategoriesContextsManager(self.engine, account: account) + self.diceCache = DiceCache(postbox: account.postbox, engine: self.engine) + self.fetchManager = FetchManager(postbox: account.postbox) + self.blockedPeersContext = BlockedPeersContext(account: account) + self.cacheCleaner = AccountClearCache(account: account) + self.cachedGroupCallContexts = AccountGroupCallContextCacheImpl() + self.activeSessionsContext = engine.privacy.activeSessions() + self.webSessions = engine.privacy.webSessions() + #endif + + + let engine = self.engine + + repliesPeerId = account.testingEnvironment ? test_repliesPeerId : prod_repliesPeerId + + let limitConfiguration = _limitConfiguration + prefDisposable.add(account.postbox.preferencesView(keys: [PreferencesKeys.limitsConfiguration]).start(next: { view in + _ = limitConfiguration.swap(view.values[PreferencesKeys.limitsConfiguration] as? LimitsConfiguration ?? LimitsConfiguration.defaultValue) + })) + let preloadGifsDisposable = self.preloadGifsDisposable + let appConfiguration = _appConfiguration + prefDisposable.add(account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]).start(next: { view in + let configuration = view.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? AppConfiguration.defaultValue + _ = appConfiguration.swap(configuration) + + + })) + + + + #if !SHARE + let signal:Signal = Signal { subscriber in + + let signal: Signal = account.postbox.transaction { + return $0.getPreferencesEntry(key: PreferencesKeys.appConfiguration) as? AppConfiguration ?? AppConfiguration.defaultValue + } |> mapToSignal { configuration in + let value = GIFKeyboardConfiguration.with(appConfiguration: configuration) + var signals = value.emojis.map { + engine.stickers.searchGifs(query: $0) + } + signals.insert(engine.stickers.searchGifs(query: ""), at: 0) + return combineLatest(signals) |> ignoreValues + } + + let disposable = signal.start(completed: { + subscriber.putCompletion() + }) + + return ActionDisposable { + disposable.dispose() + } + } + + let updated = (signal |> then(.complete() |> suspendAwareDelay(20.0 * 60.0, queue: .concurrentDefaultQueue()))) |> restart + preloadGifsDisposable.set(updated.start()) + + + let chatThemes: Signal<[(String, TelegramPresentationTheme)], NoError> = combineLatest(appearanceSignal, engine.themes.getChatThemes(accountManager: sharedContext.accountManager) ) |> mapToSignal { appearance, themes in + var signals:[Signal<(String, TelegramPresentationTheme), NoError>] = [] + + for theme in themes { + let emoji = theme.emoji + let effective = appearance.presentation.dark ? theme.darkTheme : theme.theme + if let settings = effective.settings { + let newTheme = appearance.presentation.withUpdatedColors(settings.palette) + if let wallpaper = settings.wallpaper?.uiWallpaper { + signals.append(moveWallpaperToCache(postbox: account.postbox, wallpaper: wallpaper) |> map { wallpaper in + return (emoji, newTheme.withUpdatedWallpaper(.init(wallpaper: wallpaper, associated: nil))) + }) + } else { + signals.append(.single((emoji, newTheme))) + } + } + } + + let first = Signal<[(String, TelegramPresentationTheme)], NoError>.single([]) + return first |> then(combineLatest(signals)) |> map { values in + var dict: [(String, TelegramPresentationTheme)] = [] + for value in values { + dict.append((value.0, value.1)) + } + return dict + } + } + self._chatThemes.set((chatThemes |> then(.complete() |> suspendAwareDelay(20.0 * 60.0, queue: .concurrentDefaultQueue()))) |> restart) + + #endif + + let autoplayMedia = _autoplayMedia + prefDisposable.add(account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.autoplayMedia]).start(next: { view in + _ = autoplayMedia.swap(view.values[ApplicationSpecificPreferencesKeys.autoplayMedia] as? AutoplayMediaPreferences ?? AutoplayMediaPreferences.defaultSettings) + })) + + let contentSettings = _contentSettings + prefDisposable.add(getContentSettings(postbox: account.postbox).start(next: { settings in + _ = contentSettings.swap(settings) + })) + + + globalPeerHandler.set(.single(nil)) + + if account.network.globalTime > 0 { + timeDifference = floor(account.network.globalTime - Date().timeIntervalSince1970) + } + + updateDifferenceDisposable.set((Signal.single(Void()) + |> delay(5, queue: Queue.mainQueue()) |> restart).start(next: { [weak self, weak account] in + if let account = account, account.network.globalTime > 0 { + self?.timeDifference = floor(account.network.globalTime - Date().timeIntervalSince1970) + } + })) + + + let cloudSignal = themeUnmodifiedSettings(accountManager: sharedContext.accountManager) |> distinctUntilChanged(isEqual: { lhs, rhs -> Bool in + return lhs.cloudTheme == rhs.cloudTheme + }) + |> map { value in + return (value.cloudTheme, value.palette) + } + |> deliverOnMainQueue + + cloudThemeObserver.set(cloudSignal.start(next: { [weak self] (cloud, palette) in + let update: ApplyThemeUpdate + if let cloud = cloud { + update = .cloud(cloud) + } else { + update = .local(palette) + } + self?.updateTheme(update) + })) + + + NotificationCenter.default.addObserver(self, selector: #selector(updateKeyWindow), name: NSWindow.didBecomeKeyNotification, object: window) + NotificationCenter.default.addObserver(self, selector: #selector(updateKeyWindow), name: NSWindow.didResignKeyNotification, object: window) + + + + #if !SHARE + var freeSpaceSignal:Signal = Signal { subscriber in + + subscriber.putNext(freeSystemGigabytes()) + subscriber.putCompletion() + + return ActionDisposable { + + } + } |> runOn(.concurrentDefaultQueue()) + + + + freeSpaceSignal = (freeSpaceSignal |> then(.complete() |> suspendAwareDelay(60.0 * 30, queue: Queue.concurrentDefaultQueue()))) |> restart + + + let isLocked = (NSApp.delegate as? AppDelegate)?.passlock ?? .single(false) + + + freeSpaceDisposable.set(combineLatest(queue: .mainQueue(), freeSpaceSignal, isKeyWindow, isLocked).start(next: { [weak self] space, isKeyWindow, locked in + + + let limit: UInt64 = 5 + + guard let `self` = self, isKeyWindow, !locked, let space = space, space < limit else { + return + } + if lastTimeFreeSpaceNotified == nil || (lastTimeFreeSpaceNotified! + 60.0 * 60.0 * 3 < Date().timeIntervalSince1970) { + lastTimeFreeSpaceNotified = Date().timeIntervalSince1970 + showOutOfMemoryWarning(window, freeSpace: space, context: self) + } + + })) + + account.callSessionManager.updateVersions(versions: OngoingCallContext.versions(includeExperimental: true, includeReference: false).map { version, supportsVideo -> CallSessionManagerImplementationVersion in + CallSessionManagerImplementationVersion(version: version, supportsVideo: supportsVideo) + }) + + + #endif + } + + @objc private func updateKeyWindow() { + self.isKeyWindowValue.set(window.isKeyWindow) + } + + private func updateTheme(_ update: ApplyThemeUpdate) { + switch update { + case let .cloud(cloudTheme): + _ = applyTheme(accountManager: self.sharedContext.accountManager, account: self.account, theme: cloudTheme).start() + let signal = actualizedTheme(account: self.account, accountManager: self.sharedContext.accountManager, theme: cloudTheme) |> deliverOnMainQueue + self.actualizeCloudTheme.set(signal.start(next: { [weak self] cloudTheme in + if let `self` = self { + self.applyThemeDisposable.set(downloadAndApplyCloudTheme(context: self, theme: cloudTheme, install: theme.cloudTheme?.id != cloudTheme.id).start()) + } + })) + case let .local(palette): + actualizeCloudTheme.set(applyTheme(accountManager: self.sharedContext.accountManager, account: self.account, theme: nil).start()) + applyThemeDisposable.set(updateThemeInteractivetly(accountManager: self.sharedContext.accountManager, f: { + return $0.withUpdatedPalette(palette).withUpdatedCloudTheme(nil) + }).start()) + } + } + + var timestamp: Int32 { + var time:TimeInterval = TimeInterval(Date().timeIntervalSince1970) + time -= self.timeDifference + return Int32(time) + } + + + private var _temporartPassword: String? + var temporaryPassword: String? { + return _temporartPassword + } + + func resetTemporaryPwd() { + _temporartPassword = nil + temporaryPwdDisposable.set(nil) + } + #if !SHARE + func setChatInterfaceTempState(_ state: ChatInterfaceTempState, for peerId: PeerId) { + self.chatInterfaceTempState[peerId] = state + } + func getChatInterfaceTempState(_ peerId: PeerId?) -> ChatInterfaceTempState? { + if let peerId = peerId { + return self.chatInterfaceTempState[peerId] + } else { + return nil + } + } + #endif + + func setTemporaryPwd(_ password: String) -> Void { + _temporartPassword = password + let signal = Signal.single(Void()) |> delay(30 * 60, queue: Queue.mainQueue()) + temporaryPwdDisposable.set(signal.start(next: { [weak self] in + self?._temporartPassword = nil + })) + } + + deinit { + cleanup() + } + + + func cleanup() { + updateDifferenceDisposable.dispose() + temporaryPwdDisposable.dispose() + prefDisposable.dispose() + actualizeCloudTheme.dispose() + applyThemeDisposable.dispose() + cloudThemeObserver.dispose() + preloadGifsDisposable.dispose() + freeSpaceDisposable.dispose() + NotificationCenter.default.removeObserver(self) + #if !SHARE + // self.walletPasscodeTimeoutContext.clear() + self.diceCache.cleanup() + #endif + } + + + func checkFirstRecentlyForDuplicate(peerId:PeerId) { + if let index = recentlyPeerUsed.firstIndex(of: peerId), index == 0 { + // recentlyPeerUsed.remove(at: index) + } + } + + func addRecentlyUsedPeer(peerId:PeerId) { + if let index = recentlyPeerUsed.firstIndex(of: peerId) { + recentlyPeerUsed.remove(at: index) + } + recentlyPeerUsed.insert(peerId, at: 0) + } + + + func chatLocationInput(for location: ChatLocation, contextHolder: Atomic) -> ChatLocationInput { + switch location { + case let .peer(peerId): + return .peer(peerId) + case let .replyThread(data): + let context = chatLocationContext(holder: contextHolder, account: self.account, data: data) + return .external(data.messageId.peerId, makeMessageThreadId(data.messageId), context.state) + } + } + + func chatLocationOutgoingReadState(for location: ChatLocation, contextHolder: Atomic) -> Signal { + switch location { + case .peer: + return .single(nil) + case let .replyThread(data): + let context = chatLocationContext(holder: contextHolder, account: self.account, data: data) + return context.maxReadOutgoingMessageId + } + } + + public func chatLocationUnreadCount(for location: ChatLocation, contextHolder: Atomic) -> Signal { + switch location { + case let .peer(peerId): + let unreadCountsKey: PostboxViewKey = .unreadCounts(items: [.peer(peerId), .total(nil)]) + return self.account.postbox.combinedView(keys: [unreadCountsKey]) + |> map { views in + var unreadCount: Int32 = 0 + + if let view = views.views[unreadCountsKey] as? UnreadMessageCountsView { + if let count = view.count(for: .peer(peerId)) { + unreadCount = count + } + } + + return Int(unreadCount) + } + case let .replyThread(data): + let context = chatLocationContext(holder: contextHolder, account: self.account, data: data) + return context.unreadCount + } + } + + + + func applyMaxReadIndex(for location: ChatLocation, contextHolder: Atomic, messageIndex: MessageIndex) { + switch location { + case .peer: + let _ = self.engine.messages.applyMaxReadIndexInteractively(index: messageIndex).start() + case let .replyThread(data): + let context = chatLocationContext(holder: contextHolder, account: self.account, data: data) + context.applyMaxReadIndex(messageIndex: messageIndex) + } + } + + + + + + #if !SHARE + func composeCreateGroup(selectedPeers:Set = Set()) { + createGroup(with: self, selectedPeers: selectedPeers) + } + func composeCreateChannel() { + createChannel(with: self) + } + func composeCreateSecretChat() { + let account = self.account + let window = self.window + let engine = self.engine + let confirmationImpl:([PeerId])->Signal = { peerIds in + if let first = peerIds.first, peerIds.count == 1 { + return account.postbox.loadedPeerWithId(first) |> deliverOnMainQueue |> mapToSignal { peer in + return confirmSignal(for: window, information: L10n.composeConfirmStartSecretChat(peer.displayTitle)) + } + } + return confirmSignal(for: window, information: L10n.peerInfoConfirmAddMembers1Countable(peerIds.count)) + } + let select = selectModalPeers(window: window, context: self, title: L10n.composeSelectSecretChat, limit: 1, confirmation: confirmationImpl) + + let create = select |> map { $0.first! } |> mapToSignal { peerId in + return engine.peers.createSecretChat(peerId: peerId) |> `catch` {_ in .complete()} + } |> deliverOnMainQueue |> mapToSignal{ peerId -> Signal in + return showModalProgress(signal: .single(peerId), for: mainWindow) + } + + _ = create.start(next: { [weak self] peerId in + guard let `self` = self else {return} + self.sharedContext.bindings.rootNavigation().push(ChatController(context: self, chatLocation: .peer(peerId))) + }) + } + #endif +} + + +func downloadAndApplyCloudTheme(context: AccountContext, theme cloudTheme: TelegramTheme, install: Bool = false) -> Signal { + if let cloudSettings = cloudTheme.settings { + return Signal { subscriber in + #if !SHARE + let wallpaperDisposable = DisposableSet() + let palette = cloudSettings.palette + var wallpaper: Signal? = nil + let associated = theme.wallpaper.associated?.wallpaper + if let w = cloudSettings.wallpaper, theme.wallpaper.wallpaper == associated || install { + wallpaper = .single(w) + } else if install, let wrapper = palette.wallpaper.wallpaper.cloudWallpaper { + wallpaper = .single(wrapper) + } + + if let wallpaper = wallpaper { + wallpaperDisposable.add(wallpaper.start(next: { cloud in + if let cloud = cloud { + let wp = Wallpaper(cloud) + wallpaperDisposable.add(moveWallpaperToCache(postbox: context.account.postbox, wallpaper: wp).start(next: { wallpaper in + _ = updateThemeInteractivetly(accountManager: context.sharedContext.accountManager, f: { settings in + var settings = settings.withUpdatedPalette(palette).withUpdatedCloudTheme(cloudTheme) + var updateDefault:DefaultTheme = palette.isDark ? settings.defaultDark : settings.defaultDay + updateDefault = updateDefault.updateCloud { _ in + return DefaultCloudTheme(cloud: cloudTheme, palette: palette, wallpaper: AssociatedWallpaper(cloud: cloud, wallpaper: wp)) + } + settings = palette.isDark ? settings.withUpdatedDefaultDark(updateDefault) : settings.withUpdatedDefaultDay(updateDefault) + settings = settings.withUpdatedDefaultIsDark(palette.isDark) + return settings.updateWallpaper { value in + return value.withUpdatedWallpaper(wallpaper) + .withUpdatedAssociated(AssociatedWallpaper(cloud: cloud, wallpaper: wallpaper)) + }.saveDefaultWallpaper().withSavedAssociatedTheme().saveDefaultAccent(color: cloudSettings.accent) + }).start() + + subscriber.putCompletion() + })) + } else { + _ = updateThemeInteractivetly(accountManager: context.sharedContext.accountManager, f: { settings in + var settings = settings + var updateDefault:DefaultTheme = palette.isDark ? settings.defaultDark : settings.defaultDay + updateDefault = updateDefault.updateCloud { _ in + return DefaultCloudTheme(cloud: cloudTheme, palette: palette, wallpaper: AssociatedWallpaper(cloud: cloud, wallpaper: .none)) + } + settings = palette.isDark ? settings.withUpdatedDefaultDark(updateDefault) : settings.withUpdatedDefaultDay(updateDefault) + settings = settings.withUpdatedDefaultIsDark(palette.isDark) + + return settings.withUpdatedPalette(palette).withUpdatedCloudTheme(cloudTheme).updateWallpaper({ value in + return value.withUpdatedWallpaper(.none) + .withUpdatedAssociated(AssociatedWallpaper(cloud: cloud, wallpaper: .none)) + }).saveDefaultWallpaper().withSavedAssociatedTheme().saveDefaultAccent(color: cloudSettings.accent) + }).start() + subscriber.putCompletion() + } + }, error: { _ in + subscriber.putCompletion() + })) + } else { + _ = updateThemeInteractivetly(accountManager: context.sharedContext.accountManager, f: { settings in + var settings = settings.withUpdatedPalette(palette).withUpdatedCloudTheme(cloudTheme) + var updateDefault:DefaultTheme = palette.isDark ? settings.defaultDark : settings.defaultDay + updateDefault = updateDefault.updateCloud { current in + let associated = current?.wallpaper ?? AssociatedWallpaper(cloud: nil, wallpaper: palette.wallpaper.wallpaper) + return DefaultCloudTheme(cloud: cloudTheme, palette: palette, wallpaper: associated) + } + settings = palette.isDark ? settings.withUpdatedDefaultDark(updateDefault) : settings.withUpdatedDefaultDay(updateDefault) + return settings.withSavedAssociatedTheme().saveDefaultAccent(color: cloudSettings.accent) + }).start() + subscriber.putCompletion() + } + #endif + return ActionDisposable { + #if !SHARE + wallpaperDisposable.dispose() + #endif + } + } + |> runOn(.mainQueue()) + |> deliverOnMainQueue + } else if let file = cloudTheme.file { + return Signal { subscriber in + let fetchDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: MediaResourceReference.standalone(resource: file.resource)).start() + let wallpaperDisposable = DisposableSet() + + let resourceData = context.account.postbox.mediaBox.resourceData(file.resource) |> filter { $0.complete } |> take(1) + + let dataDisposable = resourceData.start(next: { data in + + if let palette = importPalette(data.path) { + var wallpaper: Signal? = nil + var newSettings: WallpaperSettings = WallpaperSettings() + #if !SHARE + switch palette.wallpaper { + case .none: + if theme.wallpaper.wallpaper == theme.wallpaper.associated?.wallpaper || install { + wallpaper = .single(nil) + } + case .builtin: + if theme.wallpaper.wallpaper == theme.wallpaper.associated?.wallpaper || install { + wallpaper = .single(.builtin(WallpaperSettings())) + } + case let .color(color): + if theme.wallpaper.wallpaper == theme.wallpaper.associated?.wallpaper || install { + wallpaper = .single(.color(color.argb)) + } + case let .url(string): + let link = inApp(for: string as NSString, context: context) + switch link { + case let .wallpaper(values): + switch values.preview { + case let .slug(slug, settings): + if theme.wallpaper.wallpaper == theme.wallpaper.associated?.wallpaper || install { + if let associated = theme.wallpaper.associated, let cloud = associated.cloud { + switch cloud { + case let .file(values): + if values.slug == values.slug && values.settings == settings { + wallpaper = .single(cloud) + } else { + wallpaper = getWallpaper(network: context.account.network, slug: slug) |> map(Optional.init) + } + default: + wallpaper = getWallpaper(network: context.account.network, slug: slug) |> map(Optional.init) + } + } else { + wallpaper = getWallpaper(network: context.account.network, slug: slug) |> map(Optional.init) + } + } + newSettings = settings + default: + break + } + default: + break + } + } + + #endif + + if let wallpaper = wallpaper { + #if !SHARE + wallpaperDisposable.add(wallpaper.start(next: { cloud in + if let cloud = cloud { + let wp = Wallpaper(cloud).withUpdatedSettings(newSettings) + wallpaperDisposable.add(moveWallpaperToCache(postbox: context.account.postbox, wallpaper: wp).start(next: { wallpaper in + _ = updateThemeInteractivetly(accountManager: context.sharedContext.accountManager, f: { settings in + var settings = settings.withUpdatedPalette(palette).withUpdatedCloudTheme(cloudTheme) + var updateDefault:DefaultTheme = palette.isDark ? settings.defaultDark : settings.defaultDay + updateDefault = updateDefault.updateCloud { _ in + return DefaultCloudTheme(cloud: cloudTheme, palette: palette, wallpaper: AssociatedWallpaper(cloud: cloud, wallpaper: wp)) + } + settings = palette.isDark ? settings.withUpdatedDefaultDark(updateDefault) : settings.withUpdatedDefaultDay(updateDefault) + settings = settings.withUpdatedDefaultIsDark(palette.isDark) + return settings.updateWallpaper { value in + return value.withUpdatedWallpaper(wallpaper) + .withUpdatedAssociated(AssociatedWallpaper(cloud: cloud, wallpaper: wallpaper)) + }.saveDefaultWallpaper() + }).start() + + subscriber.putCompletion() + })) + } else { + _ = updateThemeInteractivetly(accountManager: context.sharedContext.accountManager, f: { settings in + var settings = settings + var updateDefault:DefaultTheme = palette.isDark ? settings.defaultDark : settings.defaultDay + updateDefault = updateDefault.updateCloud { _ in + return DefaultCloudTheme(cloud: cloudTheme, palette: palette, wallpaper: AssociatedWallpaper(cloud: cloud, wallpaper: .none)) + } + settings = palette.isDark ? settings.withUpdatedDefaultDark(updateDefault) : settings.withUpdatedDefaultDay(updateDefault) + settings = settings.withUpdatedDefaultIsDark(palette.isDark) + + return settings.withUpdatedPalette(palette).withUpdatedCloudTheme(cloudTheme).updateWallpaper({ value in + return value.withUpdatedWallpaper(.none) + .withUpdatedAssociated(AssociatedWallpaper(cloud: cloud, wallpaper: .none)) + }).saveDefaultWallpaper() + }).start() + subscriber.putCompletion() + } + }, error: { _ in + subscriber.putCompletion() + })) + #endif + } else { + _ = updateThemeInteractivetly(accountManager: context.sharedContext.accountManager, f: { settings in + var settings = settings.withUpdatedPalette(palette).withUpdatedCloudTheme(cloudTheme) + var updateDefault:DefaultTheme = palette.isDark ? settings.defaultDark : settings.defaultDay + updateDefault = updateDefault.updateCloud { current in + let associated = current?.wallpaper ?? AssociatedWallpaper(cloud: nil, wallpaper: palette.wallpaper.wallpaper) + return DefaultCloudTheme(cloud: cloudTheme, palette: palette, wallpaper: associated) + } + settings = palette.isDark ? settings.withUpdatedDefaultDark(updateDefault) : settings.withUpdatedDefaultDay(updateDefault) + return settings + }).start() + subscriber.putCompletion() + } + } + }) + + return ActionDisposable { + fetchDisposable.dispose() + dataDisposable.dispose() + wallpaperDisposable.dispose() + } + } + |> runOn(.mainQueue()) + |> deliverOnMainQueue + } else { + return .complete() + } +} + + + +private func chatLocationContext(holder: Atomic, account: Account, data: ChatReplyThreadMessage) -> ReplyThreadHistoryContext { + let holder = holder.modify { current in + if let current = current as? ChatLocationContextHolderImpl { + return current + } else { + return ChatLocationContextHolderImpl(account: account, data: data) + } + } as! ChatLocationContextHolderImpl + return holder.context +} + +private final class ChatLocationContextHolderImpl: ChatLocationContextHolder { + let context: ReplyThreadHistoryContext + + init(account: Account, data: ChatReplyThreadMessage) { + self.context = ReplyThreadHistoryContext(account: account, peerId: data.messageId.peerId, data: data) + } +} diff --git a/Telegram-Mac/AccountInfoItem.swift b/Telegram-Mac/AccountInfoItem.swift index 304e9d5f6b..a0e546b712 100644 --- a/Telegram-Mac/AccountInfoItem.swift +++ b/Telegram-Mac/AccountInfoItem.swift @@ -8,66 +8,63 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac +import Postbox +import TelegramCore -enum AccountInfoItemState { - case normal - case edit -} +import SwiftSignalKit -class AccountInfoItem: TableRowItem { - - let saveCallback:()->Void - var firstName:String - var lastName:String - - let account: Account - let peer: TelegramUser - let connectionStatus: ConnectionStatus - var state:AccountInfoItemState - let statusLayout:(TextNodeLayout, TextNode) - let nameLayout:(TextNodeLayout, TextNode) - let editCallback:()->Void - let imageCallback:()->Void - private let _stableId:AnyHashable - override var stableId: AnyHashable { - return _stableId - } + + +class AccountInfoItem: GeneralRowItem { - override var height:CGFloat { - return 100 - } + fileprivate let textLayout: TextViewLayout + fileprivate let activeTextlayout: TextViewLayout + fileprivate let context: AccountContext + fileprivate let peer: TelegramUser + private(set) var photos: [TelegramPeerPhoto] = [] + + private let peerPhotosDisposable = MetaDisposable() - init(_ initialSize:NSSize, stableId:AnyHashable, account: Account, peer: TelegramUser, state:AccountInfoItemState, connectionStatus: ConnectionStatus, saveCallback:@escaping()->Void, editCallback:@escaping()->Void, imageCallback:@escaping()->Void) { - self.saveCallback = saveCallback - self.editCallback = editCallback - self.imageCallback = imageCallback - self.account = account - self._stableId = stableId + init(_ initialSize:NSSize, stableId:AnyHashable, context: AccountContext, peer: TelegramUser, action: @escaping()->Void) { + self.context = context self.peer = peer - self.state = state - self.connectionStatus = connectionStatus - self.firstName = peer.firstName ?? "" - self.lastName = peer.lastName ?? "" - let statusAttributed:NSAttributedString - switch connectionStatus { - case .connecting(let toProxy): - statusAttributed = .initialize(string: toProxy ? tr(.connectingStatusConnectingToProxy) : tr(.connectingStatusConnecting), color: theme.colors.grayText, font: .normal(.text)) - case .online: - statusAttributed = .initialize(string: tr(.connectingStatusOnline), color: theme.colors.blueUI, font: .normal(.text)) - case .updating: - statusAttributed = .initialize(string: tr(.connectingStatusUpdating), color: theme.colors.grayText, font: .normal(.text)) - case .waitingForNetwork: - statusAttributed = .initialize(string: tr(.connectingStatusWaitingNetwork), color: theme.colors.grayText, font: .normal(.text)) + let attr = NSMutableAttributedString() + + _ = attr.append(string: peer.displayTitle, color: theme.colors.text, font: .medium(.title)) + if let phone = peer.phone { + _ = attr.append(string: "\n") + _ = attr.append(string: formatPhoneNumber(phone), color: theme.colors.grayText, font: .normal(.text)) + } + if let username = peer.username, !username.isEmpty { + _ = attr.append(string: "\n") + _ = attr.append(string: "@\(username)", color: theme.colors.grayText, font: .normal(.text)) } - statusLayout = TextNode.layoutText(maybeNode: nil, statusAttributed, nil, 1, .end, NSMakeSize(initialSize.width - 140, 20), nil, false, .left) - nameLayout = TextNode.layoutText(maybeNode: nil, .initialize(string: peer.displayTitle, color: theme.colors.text, font: .normal(.title)), nil, 1, .end, NSMakeSize(initialSize.width - 140, 20), nil, false, .left) + textLayout = TextViewLayout(attr, maximumNumberOfLines: 4) - super.init(initialSize) + let active = attr.mutableCopy() as! NSMutableAttributedString + active.addAttribute(.foregroundColor, value: theme.colors.underSelectedColor, range: active.range) + activeTextlayout = TextViewLayout(active, maximumNumberOfLines: 4) + super.init(initialSize, height: 90, stableId: stableId, action: action) + + self.photos = syncPeerPhotos(peerId: peer.id) + let signal = peerPhotos(context: context, peerId: peer.id, force: true) |> deliverOnMainQueue + peerPhotosDisposable.set(signal.start(next: { [weak self] photos in + self?.photos = photos + self?.redraw() + })) + + } + + deinit { + peerPhotosDisposable.dispose() + } + override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) + textLayout.measure(width: width - 100) + activeTextlayout.measure(width: width - 100) + return success } override func viewClass() -> AnyClass { @@ -76,209 +73,182 @@ class AccountInfoItem: TableRowItem { } -class AccountInfoView : TableRowView, TGModernGrowingDelegate { +class AccountInfoView : TableRowView { - let avatarView:AvatarControl - let firstNameTextView:TGModernGrowingTextView = TGModernGrowingTextView(frame: NSZeroRect) - let lastNameTextView:TGModernGrowingTextView = TGModernGrowingTextView(frame: NSZeroRect) - private let editButton: ImageButton = ImageButton() - private let updoadPhotoCap:ImageButton = ImageButton() + private let avatarView:AvatarControl + private let textView: TextView = TextView() + private let actionView: ImageView = ImageView() + + private var photoVideoView: MediaPlayerView? + private var photoVideoPlayer: MediaPlayer? + + required init(frame frameRect: NSRect) { - avatarView = AvatarControl(font: .avatar(.custom(22))) + avatarView = AvatarControl(font: .avatar(22.0)) avatarView.setFrameSize(NSMakeSize(60, 60)) super.init(frame: frameRect) - + layerContentsRedrawPolicy = .onSetNeedsDisplay avatarView.animated = true - addSubview(avatarView) - addSubview(firstNameTextView) - addSubview(lastNameTextView) - addSubview(editButton) + textView.userInteractionEnabled = false + textView.isSelectable = false - updoadPhotoCap.backgroundColor = NSColor.black.withAlphaComponent(0.4) - updoadPhotoCap.setFrameSize(avatarView.frame.size) - updoadPhotoCap.layer?.cornerRadius = updoadPhotoCap.frame.width / 2 - updoadPhotoCap.set(image: ControlStyle(highlightColor: .white).highlight(image: theme.icons.chatAttachCamera), for: .Normal) - updoadPhotoCap.set(image: ControlStyle(highlightColor: theme.colors.blueIcon).highlight(image: theme.icons.chatAttachCamera), for: .Highlight) - - avatarView.addSubview(updoadPhotoCap) + addSubview(avatarView) + addSubview(actionView) + addSubview(textView) avatarView.set(handler: { [weak self] _ in if let item = self?.item as? AccountInfoItem, let _ = item.peer.largeProfileImage { - showPhotosGallery(account: item.account, peerId: item.peer.id, firstStableId: item.stableId, item.table, nil) + showPhotosGallery(context: item.context, peerId: item.peer.id, firstStableId: item.stableId, item.table, nil) } }, for: .Click) - firstNameTextView.textColor = theme.colors.text - lastNameTextView.textColor = theme.colors.text - - firstNameTextView.delegate = self - firstNameTextView.textFont = .normal(.title) - firstNameTextView.min_height = 17 - firstNameTextView.isSingleLine = true - firstNameTextView.max_height = 17 - - lastNameTextView.delegate = self - lastNameTextView.textFont = .normal(.title) - lastNameTextView.min_height = 17 - lastNameTextView.max_height = 17 - lastNameTextView.isSingleLine = true - editButton.set(handler: { [weak self] _ in - if let item = self?.item as? AccountInfoItem { - item.editCallback() - } - }, for: .Click) - - updoadPhotoCap.set(handler: { [weak self] _ in - if let item = self?.item as? AccountInfoItem { - item.imageCallback() - } - }, for: .Click) } - override var backdorColor: NSColor { - return theme.colors.background - } - - override var firstResponder: NSResponder? { - if window?.firstResponder == lastNameTextView.inputView || window?.firstResponder == firstNameTextView.inputView { - return window?.firstResponder + override func mouseUp(with event: NSEvent) { + if let item = item as? AccountInfoItem, mouseInside() { + item.action() } - return self.firstNameTextView - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") } - func textViewHeightChanged(_ height: CGFloat, animated: Bool) { - - } - - func maxCharactersLimit() -> Int32 { - return 30 + override var backdorColor: NSColor { + return isSelect ? theme.colors.accentSelect : theme.colors.background } - - func textViewSize() -> NSSize { - return NSMakeSize(frame.width - (avatarView.frame.maxX + 10), 17) + + @objc func updatePlayerIfNeeded() { + let accept = window != nil && window!.isKeyWindow && !NSIsEmptyRect(visibleRect) + if accept { + photoVideoPlayer?.play() + } else { + photoVideoPlayer?.pause() + } } - func textViewEnterPressed(_ event:NSEvent) -> Bool { - if FastSettings.checkSendingAbility(for: event) { - if let item = item as? AccountInfoItem { - item.firstName = self.firstNameTextView.string() - item.lastName = self.lastNameTextView.string() - item.saveCallback() - } - return true - } - return false + override func addAccesoryOnCopiedView(innerId: AnyHashable, view: NSView) { + photoVideoPlayer?.seek(timestamp: 0) } - func textViewIsTypingEnabled() -> Bool { - return true + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + updateListeners() + updatePlayerIfNeeded() } - func textViewNeedClose(_ textView: Any) { - + override func viewDidMoveToSuperview() { + super.viewDidMoveToSuperview() + updateListeners() + updatePlayerIfNeeded() } - func textViewTextDidChange(_ string: String) { - if let item = item as? AccountInfoItem { - item.firstName = self.firstNameTextView.string() - item.lastName = self.lastNameTextView.string() + func updateListeners() { + if let window = window { + NotificationCenter.default.removeObserver(self) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSWindow.didBecomeKeyNotification, object: window) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSWindow.didResignKeyNotification, object: window) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSView.boundsDidChangeNotification, object: item?.table?.clipView) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSView.boundsDidChangeNotification, object: self) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSView.frameDidChangeNotification, object: item?.table?.view) + } else { + removeNotificationListeners() } } - func textViewTextDidChangeSelectedRange(_ range: NSRange) { - + func removeNotificationListeners() { + NotificationCenter.default.removeObserver(self) } - func textViewDidPaste(_ pasteboard: NSPasteboard) -> Bool { - return false + deinit { + removeNotificationListeners() } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var videoRepresentation: TelegramMediaImage.VideoRepresentation? override func set(item: TableRowItem, animated: Bool) { super.set(item: item) if let item = item as? AccountInfoItem { - editButton.set(image: theme.icons.settingsEditInfo, for: .Normal) - editButton.sizeToFit() - firstNameTextView.textColor = theme.colors.text - lastNameTextView.textColor = theme.colors.text - avatarView.setPeer(account: item.account, peer: item.peer) - firstNameTextView.setString(item.peer.firstName ?? "") - lastNameTextView.setString(item.peer.lastName ?? "") - - if item.state != .normal { - updoadPhotoCap.isHidden = false - } - if item.state == .normal { - editButton.isHidden = false - } - - - updoadPhotoCap.change(opacity: item.state == .normal ? 0 : 1, animated: animated, completion: { [weak self, weak item] completed in - if completed, let item = item { - self?.updoadPhotoCap.isHidden = item.state == .normal - } - }) - - editButton.change(opacity: item.state != .normal ? 0 : 1, animated: animated, completion: { [weak self, weak item] completed in - if completed, let item = item { - self?.editButton.isHidden = item.state != .normal + actionView.image = item.isSelected ? nil : theme.icons.generalNext + actionView.sizeToFit() + avatarView.setPeer(account: item.context.account, peer: item.peer) + textView.update(isSelect ? item.activeTextlayout : item.textLayout) + if !item.photos.isEmpty { + if let first = item.photos.first, let video = first.image.videoRepresentations.last { + let equal = videoRepresentation?.resource.id.isEqual(to: video.resource.id) ?? false + if !equal { + + self.photoVideoView?.removeFromSuperview() + self.photoVideoView = nil + + self.photoVideoView = MediaPlayerView() + self.photoVideoView!.layer?.cornerRadius = self.avatarView.frame.height / 2 + self.addSubview(self.photoVideoView!) + self.photoVideoView!.isEventLess = true + self.photoVideoView!.frame = self.avatarView.frame + + let file = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: first.image.representations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: video.resource.size, attributes: []) + + let mediaPlayer = MediaPlayer(postbox: item.context.account.postbox, reference: MediaResourceReference.standalone(resource: file.resource), streamable: true, video: true, preferSoftwareDecoding: false, enableSound: false, fetchAutomatically: true) + + mediaPlayer.actionAtEnd = .loop(nil) + + self.photoVideoPlayer = mediaPlayer + + mediaPlayer.play() + + if let seekTo = video.startTimestamp { + mediaPlayer.seek(timestamp: seekTo) + } + + mediaPlayer.attachPlayerView(self.photoVideoView!) + self.videoRepresentation = video + updatePlayerIfNeeded() + } + } else { + self.photoVideoPlayer = nil + self.photoVideoView?.removeFromSuperview() + self.photoVideoView = nil } - }) - - firstNameTextView.isHidden = item.state == .normal - lastNameTextView.isHidden = item.state == .normal + } else { + self.photoVideoPlayer = nil + self.photoVideoView?.removeFromSuperview() + self.photoVideoView = nil + } needsDisplay = true + needsLayout = true } } + override func updateColors() { + super.updateColors() + textView.backgroundColor = backdorColor + } + override func draw(_ layer: CALayer, in ctx: CGContext) { super.draw(layer, in: ctx) ctx.setFillColor(theme.colors.border.cgColor) ctx.fill(NSMakeRect(frame.width - .borderSize, 0, .borderSize, frame.height)) - - if let item = item as? AccountInfoItem { - if item.state == .normal { - var tY = NSMinY(focus(item.nameLayout.0.size)) - - let t = item.nameLayout.0.size.height + item.statusLayout.0.size.height + 4.0 - tY = (NSHeight(self.frame) - t) / 2.0 - - let sY = tY + item.statusLayout.0.size.height + 4.0 - item.statusLayout.1.draw(NSMakeRect(100, floorToScreenPixels(sY), item.statusLayout.0.size.width, item.statusLayout.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor) - - item.nameLayout.1.draw(NSMakeRect(100, floorToScreenPixels(tY), item.nameLayout.0.size.width, item.nameLayout.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor) - } else { - ctx.fill(NSMakeRect((avatarView.frame.maxX + 12), 46, frame.width - (avatarView.frame.maxX + 20), .borderSize)) - ctx.fill(NSMakeRect((avatarView.frame.maxX + 12), 74, frame.width - (avatarView.frame.maxX + 20), .borderSize)) - } - - } } override func layout() { super.layout() avatarView.centerY(x:16) - firstNameTextView.setFrameSize(frame.width - (avatarView.frame.maxX + 16), 17) - lastNameTextView.setFrameSize(frame.width - (avatarView.frame.maxX + 16), 17) - - firstNameTextView.setFrameOrigin((avatarView.frame.maxX + 10), 27) - lastNameTextView.setFrameOrigin((avatarView.frame.maxX + 10), 55) - editButton.centerY(x: frame.width - editButton.frame.width - 19) + textView.centerY(x: avatarView.frame.maxX + 25) + actionView.centerY(x: frame.width - actionView.frame.width - 10) + photoVideoView?.frame = avatarView.frame } - override var interactionContentView: NSView { + override func interactionContentView(for innerId: AnyHashable, animateIn: Bool ) -> NSView { return avatarView } @@ -287,3 +257,4 @@ class AccountInfoView : TableRowView, TGModernGrowingDelegate { } } + diff --git a/Telegram-Mac/AccountUtils.swift b/Telegram-Mac/AccountUtils.swift new file mode 100644 index 0000000000..b3f2c49cbb --- /dev/null +++ b/Telegram-Mac/AccountUtils.swift @@ -0,0 +1,53 @@ +// +// AccountUtils.swift +// Telegram +// +// Created by Mikhail Filimonov on 02.12.2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import SwiftSignalKit +import Postbox +import TelegramCore + + +let maximumNumberOfAccounts = 3 + + +func activeAccountsAndPeers(context: AccountContext, includePrimary: Bool = false) -> Signal<((Account, Peer)?, [(Account, Peer, Int32)]), NoError> { + let sharedContext = context.sharedContext + return context.sharedContext.activeAccounts + |> mapToSignal { primary, activeAccounts, _ -> Signal<((Account, Peer)?, [(Account, Peer, Int32)]), NoError> in + var accounts: [Signal<(Account, Peer, Int32)?, NoError>] = [] + func accountWithPeer(_ account: Account) -> Signal<(Account, Peer, Int32)?, NoError> { + return combineLatest(account.postbox.peerView(id: account.peerId), renderedTotalUnreadCount(accountManager: sharedContext.accountManager, postbox: account.postbox)) + |> map { view, totalUnreadCount -> (Peer?, Int32) in + return (view.peers[view.peerId], totalUnreadCount.0) + } + |> distinctUntilChanged { lhs, rhs in + return arePeersEqual(lhs.0, rhs.0) && lhs.1 == rhs.1 + } + |> map { peer, totalUnreadCount -> (Account, Peer, Int32)? in + if let peer = peer { + return (account, peer, totalUnreadCount) + } else { + return nil + } + } + } + for (_, account, _) in activeAccounts { + accounts.append(accountWithPeer(account)) + } + + return combineLatest(accounts) + |> map { accounts -> ((Account, Peer)?, [(Account, Peer, Int32)]) in + var primaryRecord: (Account, Peer)? + if let first = accounts.filter({ $0?.0.id == primary?.id }).first, let (account, peer, _) = first { + primaryRecord = (account, peer) + } + let accountRecords: [(Account, Peer, Int32)] = (includePrimary ? accounts : accounts.filter({ $0?.0.id != primary?.id })).compactMap({ $0 }) + return (primaryRecord, accountRecords) + } + } +} diff --git a/Telegram-Mac/AccountViewController.swift b/Telegram-Mac/AccountViewController.swift index 6b93c61115..8107cb8582 100644 --- a/Telegram-Mac/AccountViewController.swift +++ b/Telegram-Mac/AccountViewController.swift @@ -8,17 +8,82 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore +import Postbox +import SwiftSignalKit + + +private final class AccountSearchBarView: TitledBarView { + fileprivate let searchView = SearchView(frame: NSMakeRect(0, 0, 100, 30)) + init(controller: ViewController) { + super.init(controller: controller) + addSubview(searchView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + searchView.updateLocalizationAndTheme(theme: theme) + } + + override func layout() { + super.layout() + searchView.setFrameSize(NSMakeSize(frame.width, 30)) + searchView.center() + } + +} + + +fileprivate final class AccountInfoArguments { + let context: AccountContext + let presentController:(ViewController, Bool) -> Void + let openFaq:()->Void + let ask:()->Void + let openUpdateApp:() -> Void + init(context: AccountContext, presentController:@escaping(ViewController, Bool)->Void, openFaq: @escaping()->Void, ask:@escaping()->Void, openUpdateApp: @escaping() -> Void) { + self.context = context + self.presentController = presentController + self.openFaq = openFaq + self.ask = ask + self.openUpdateApp = openUpdateApp + } +} class AccountViewController: NavigationViewController { private var layoutController:LayoutAccountController - init(_ account:Account, accountManager: AccountManager) { - self.layoutController = LayoutAccountController(account, accountManager: accountManager) - super.init(layoutController) - layoutController.navigationController = self + private let disposable = MetaDisposable() + init(_ context: AccountContext) { + self.layoutController = LayoutAccountController(context) + super.init(layoutController, context.window) + self.ready.set(layoutController.ready.get()) + disposable.set(context.hasPassportSettings.get().start(next: { [weak self] value in + self?.layoutController.passportPromise.set(.single(value)) + })) + self.applyAppearOnLoad = false + } + + override func viewDidLoad() { + super.viewDidLoad() + (self.view as? View)?.border = [.Right] + } + + deinit { + disposable.dispose() + } + + override func viewDidResized(_ size: NSSize) { + super.viewDidResized(size) + layoutController._frameRect = bounds + layoutController.frame = NSMakeRect(0, layoutController.bar.height, bounds.width, bounds.height - layoutController.bar.height) } override func viewWillAppear(_ animated: Bool) { @@ -26,6 +91,10 @@ class AccountViewController: NavigationViewController { layoutController.viewWillAppear(animated) } + override func scrollup(force: Bool = false) { + layoutController.scrollup() + } + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) layoutController.viewDidAppear(animated) @@ -40,104 +109,126 @@ class AccountViewController: NavigationViewController { } } +private enum AccountInfoEntryId : Hashable { + case index(Int) + case account(AccountWithInfo) + + var hashValue: Int { + return 0 + } +} - -enum AccountInfoEntry : Comparable, Identifiable { - case info(index:Int, AccountInfoItemState, TelegramUser, ConnectionStatus) - case updatePhoto(index: Int) +private enum AccountInfoEntry : TableItemListNodeEntry { + case info(index:Int, TelegramUser) + case accountRecord(index: Int, info: AccountWithInfo) + case addAccount(index: Int) + case proxy(index: Int, status: String?) case general(index: Int) case stickers(index: Int) - case notifications(index: Int) - case username(index: Int, username: String) - case bio(index: Int, about: String) + case notifications(index: Int, status: UNUserNotifications.AuthorizationStatus) case language(index: Int, current: String) case appearance(index: Int) - case phone(index: Int, phone: String) - case privacy(index: Int) + case privacy(index: Int, AccountPrivacySettings?, WebSessionsContextState) case dataAndStorage(index: Int) - case accounts(index: Int) + case activeSessions(index: Int, activeSessions: Int) + case passport(index: Int, peer: Peer) + case wallet(index: Int) + case update(index: Int, state: Any) + case filters(index: Int) + case readArticles(index: Int) case about(index: Int) case faq(index: Int) case ask(index: Int) - case logout(index: Int) case whiteSpace(index:Int, height:CGFloat) - var stableId: Int { + var stableId: AccountInfoEntryId { switch self { case .info: - return 0 - case .updatePhoto: - return 1 - case .username: - return 2 - case .bio: - return 3 - case .phone: - return 4 + return .index(0) + case let .accountRecord(_, info): + return .account(info) + case .addAccount: + return .index(1) case .general: - return 5 + return .index(2) + case .proxy: + return .index(3) case .notifications: - return 6 + return .index(4) case .dataAndStorage: - return 7 + return .index(5) + case .activeSessions: + return .index(6) case .privacy: - return 8 + return .index(7) case .language: - return 9 + return .index(8) case .stickers: - return 10 + return .index(9) + case .filters: + return .index(10) + case .update: + return .index(11) case .appearance: - return 11 - case .accounts: - return 12 + return .index(12) + case .passport: + return .index(13) + case .wallet: + return .index(14) + case .readArticles: + return .index(15) case .about: - return 13 + return .index(16) case .faq: - return 14 + return .index(17) case .ask: - return 15 - case .logout: - return 16 + return .index(18) case let .whiteSpace(index, _): - return 1000 + index + return .index(1000 + index) } } var index:Int { switch self { - case let .info(index, _, _, _): + case let .info(index, _): return index - case let .updatePhoto(index): + case let .accountRecord(index, _): return index - case let .general(index): + case let .addAccount(index): return index - case let .stickers(index): + case let .general(index): return index - case let .notifications(index): + case let .proxy(index, _): return index - case let .username(index, _): + case let .stickers(index): return index - case let .bio(index, _): + case let .notifications(index, _): return index case let .language(index, _): return index case let .appearance(index): return index - case let .phone(index, _): - return index - case let .privacy(index): + case let .privacy(index, _, _): return index case let .dataAndStorage(index): return index - case let .accounts(index): + case let .activeSessions(index, _): return index case let .about(index): return index + case let .passport(index, _): + return index + case let .filters(index): + return index + case let .wallet(index): + return index + case let .readArticles(index): + return index case let .faq(index): return index case let .ask(index): return index - case let .logout(index): + case let .update(index, _): return index case let .whiteSpace(index, _): return index @@ -146,14 +237,20 @@ enum AccountInfoEntry : Comparable, Identifiable { static func ==(lhs:AccountInfoEntry, rhs:AccountInfoEntry) -> Bool { switch lhs { - case let .info(lhsIndex, lhsState, lhsPeer, lhsConnectionState): - if case let .info(rhsIndex, rhsState, rhsPeer, rhsConnectionState) = rhs { - return lhsIndex == rhsIndex && lhsState == rhsState && lhsIndex == rhsIndex && lhsConnectionState == rhsConnectionState && lhsPeer.isEqual(rhsPeer) + case let .info(lhsIndex, lhsPeer): + if case let .info(rhsIndex, rhsPeer) = rhs { + return lhsIndex == rhsIndex && lhsPeer.isEqual(rhsPeer) + } else { + return false + } + case let .accountRecord(lhsIndex, lhsInfo): + if case let .accountRecord(rhsIndex, rhsInfo) = rhs { + return lhsIndex == rhsIndex && lhsInfo == rhsInfo } else { return false } - case let .updatePhoto(lhsIndex): - if case let .updatePhoto(rhsIndex) = rhs { + case let .addAccount(lhsIndex): + if case let .addAccount(rhsIndex) = rhs { return lhsIndex == rhsIndex } else { return false @@ -170,21 +267,15 @@ enum AccountInfoEntry : Comparable, Identifiable { } else { return false } - case let .notifications(lhsIndex): - if case let .notifications(rhsIndex) = rhs { - return lhsIndex == rhsIndex + case let .proxy(lhsIndex, lhsStatus): + if case let .proxy(rhsIndex, rhsStatus) = rhs { + return lhsIndex == rhsIndex && lhsStatus == rhsStatus } else { return false } - case let .username(lhsIndex, lhsUsername): - if case let .username(rhsIndex, rhsUsername) = rhs { - return lhsIndex == rhsIndex && lhsUsername == rhsUsername - } else { - return false - } - case let .bio(lhsIndex, lhsAbout): - if case let .bio(rhsIndex, rhsAbout) = rhs { - return lhsIndex == rhsIndex && lhsAbout == rhsAbout + case let .notifications(index, status): + if case .notifications(index, status) = rhs { + return true } else { return false } @@ -200,15 +291,9 @@ enum AccountInfoEntry : Comparable, Identifiable { } else { return false } - case let .phone(lhsIndex, lhsPhone): - if case let .phone(rhsIndex, rhsPhone) = rhs { - return lhsIndex == rhsIndex && lhsPhone == rhsPhone - } else { - return false - } - case let .privacy(lhsIndex): - if case let .privacy(rhsIndex) = rhs { - return lhsIndex == rhsIndex + case let .privacy(index, privacy, webSessions): + if case .privacy(index, privacy, webSessions) = rhs { + return true } else { return false } @@ -218,9 +303,9 @@ enum AccountInfoEntry : Comparable, Identifiable { } else { return false } - case let .accounts(lhsIndex): - if case let .accounts(rhsIndex) = rhs { - return lhsIndex == rhsIndex + case let .activeSessions(index, activeSessions): + if case .activeSessions(index, activeSessions) = rhs { + return true } else { return false } @@ -230,6 +315,24 @@ enum AccountInfoEntry : Comparable, Identifiable { } else { return false } + case let .passport(lhsIndex, lhsPeer): + if case let .passport(rhsIndex, rhsPeer) = rhs { + return lhsIndex == rhsIndex && lhsPeer.isEqual(rhsPeer) + } else { + return false + } + case let .wallet(index): + if case .wallet(index) = rhs { + return true + } else { + return false + } + case let .readArticles(lhsIndex): + if case let .readArticles(rhsIndex) = rhs { + return lhsIndex == rhsIndex + } else { + return false + } case let .faq(lhsIndex): if case let .faq(rhsIndex) = rhs { return lhsIndex == rhsIndex @@ -242,8 +345,21 @@ enum AccountInfoEntry : Comparable, Identifiable { } else { return false } - case let .logout(lhsIndex): - if case let .logout(rhsIndex) = rhs { + case let .filters(lhsIndex): + if case let .filters(rhsIndex) = rhs { + return lhsIndex == rhsIndex + } else { + return false + } + case let .update(lhsIndex, lhsState): + if case let .update(rhsIndex, rhsState) = rhs { + #if !APP_STORE + let lhsState = lhsState as? AppUpdateState + let rhsState = rhsState as? AppUpdateState + if lhsState != rhsState { + return false + } + #endif return lhsIndex == rhsIndex } else { return false @@ -261,152 +377,538 @@ enum AccountInfoEntry : Comparable, Identifiable { return lhs.index < rhs.index } -} - + func item(_ arguments: AccountInfoArguments, initialSize: NSSize) -> TableRowItem { + switch self { + case let .info(_, peer): + return AccountInfoItem(initialSize, stableId: stableId, context: arguments.context, peer: peer, action: { + let first: Atomic = Atomic(value: true) + EditAccountInfoController(context: arguments.context, f: { controller in + arguments.presentController(controller, first.swap(false)) + }) + }) + case let .accountRecord(_, info): + return ShortPeerRowItem(initialSize, peer: info.peer, account: info.account, height: 42, photoSize: NSMakeSize(28, 28), titleStyle: ControlStyle(font: .normal(.title), foregroundColor: theme.colors.text, highlightColor: theme.colors.underSelectedColor), borderType: [.Right], inset: NSEdgeInsets(left:16), action: { + arguments.context.sharedContext.switchToAccount(id: info.account.id, action: nil) + }, contextMenuItems: { + return .single([ContextMenuItem(L10n.accountSettingsDeleteAccount, handler: { + confirm(for: arguments.context.window, information: L10n.accountConfirmLogoutText, successHandler: { _ in + _ = logoutFromAccount(id: info.account.id, accountManager: arguments.context.sharedContext.accountManager, alreadyLoggedOutRemotely: false).start() + }) + })]) + }, alwaysHighlight: true, badgeNode: GlobalBadgeNode(info.account, sharedContext: arguments.context.sharedContext, getColor: { _ in theme.colors.accent }), compactText: true) + case .addAccount: + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.accountSettingsAddAccount, nameStyle: ControlStyle(font: .normal(.title), foregroundColor: theme.colors.accentIcon), type: .none, action: { + let testingEnvironment = NSApp.currentEvent?.modifierFlags.contains(.command) == true + arguments.context.sharedContext.beginNewAuth(testingEnvironment: testingEnvironment) + + }, thumb: GeneralThumbAdditional(thumb: theme.icons.peerInfoAddMember, textInset: 35, thumbInset: 0), border:[BorderType.Right], inset:NSEdgeInsets(left:15)) + case .general: + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.accountSettingsGeneral, icon: theme.icons.settingsGeneral, activeIcon: theme.icons.settingsGeneralActive, type: .next, action: { + arguments.presentController(GeneralSettingsViewController(arguments.context), true) + }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) + case .proxy(_, let status): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.accountSettingsProxy, icon: theme.icons.settingsProxy, activeIcon: theme.icons.settingsProxyActive, type: .nextContext(status ?? ""), action: { + let controller = proxyListController(accountManager: arguments.context.sharedContext.accountManager, network: arguments.context.account.network, share: { servers in + var message: String = "" + for server in servers { + message += server.link + "\n\n" + } + message = message.trimmed + + showModal(with: ShareModalController(ShareLinkObject(arguments.context, link: message)), for: mainWindow) + }, pushController: { controller in + arguments.presentController(controller, false) + }) + arguments.presentController(controller, true) + }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) + case .stickers: + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.accountSettingsStickers, icon: theme.icons.settingsStickers, activeIcon: theme.icons.settingsStickersActive, type: .next, action: { + arguments.presentController(InstalledStickerPacksController(arguments.context), true) + }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) + case .notifications(_, let status): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.accountSettingsNotifications, icon: theme.icons.settingsNotifications, activeIcon: theme.icons.settingsNotificationsActive, type: status == .denied ? .image(#imageLiteral(resourceName: "Icon_MessageSentFailed").precomposed()) : .next, action: { + arguments.presentController(NotificationPreferencesController(arguments.context), true) + }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) + case let .language(_, current): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.accountSettingsLanguage, icon: theme.icons.settingsLanguage, activeIcon: theme.icons.settingsLanguageActive, type: .nextContext(current), action: { + arguments.presentController(LanguageViewController(arguments.context), true) + }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) + case .appearance: + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.accountSettingsTheme, icon: theme.icons.settingsAppearance, activeIcon: theme.icons.settingsAppearanceActive, type: .next, action: { + arguments.presentController(AppAppearanceViewController(context: arguments.context), true) + }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) + case let .privacy(_, privacySettings, webSessions): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.accountSettingsPrivacyAndSecurity, icon: theme.icons.settingsSecurity, activeIcon: theme.icons.settingsSecurityActive, type: .next, action: { + arguments.presentController(PrivacyAndSecurityViewController(arguments.context, initialSettings: privacySettings), true) + }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) + case .dataAndStorage: + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.accountSettingsDataAndStorage, icon: theme.icons.settingsStorage, activeIcon: theme.icons.settingsStorageActive, type: .next, action: { + arguments.presentController(DataAndStorageViewController(arguments.context), true) + }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) + case let .activeSessions(_, count): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.privacySettingsActiveSessions, icon: theme.icons.settingsSessions, activeIcon: theme.icons.settingsSessionsActive, type: .nextContext("\(count)"), action: { + arguments.presentController(RecentSessionsController(arguments.context), true) + }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) + case .about: + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.accountSettingsAbout, icon: theme.icons.settingsFaq, activeIcon: theme.icons.settingsFaqActive, type: .next, action: { + showModal(with: AboutModalController(), for: mainWindow) + }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) + case let .passport(_, peer): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.accountSettingsPassport, icon: theme.icons.settingsPassport, activeIcon: theme.icons.settingsPassportActive, type: .next, action: { + arguments.presentController(PassportController(arguments.context, peer, request: nil, nil), true) + }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) + case .wallet: + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.accountSettingsWallet, icon: theme.icons.settingsWallet, activeIcon: theme.icons.settingsWalletActive, type: .next, action: { + let context = arguments.context + }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) + case .faq: + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.accountSettingsFAQ, icon: theme.icons.settingsFaq, activeIcon: theme.icons.settingsFaqActive, type: .next, action: { + + arguments.openFaq() + + }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) + case .readArticles: + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.accountSettingsReadArticles, icon: theme.icons.settingsFaq, activeIcon: theme.icons.settingsFaqActive, type: .next, action: { + + + }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) + case .ask: + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.accountSettingsAskQuestion, icon: theme.icons.settingsAskQuestion, activeIcon: theme.icons.settingsAskQuestionActive, type: .next, action: { + confirm(for: mainWindow, information: L10n.accountConfirmAskQuestion, thridTitle: L10n.accountConfirmGoToFaq, successHandler: { result in + switch result { + case .basic: + + _ = showModalProgress(signal: arguments.context.engine.peers.supportPeerId(), for: mainWindow).start(next: { peerId in + if let peerId = peerId { + arguments.presentController(ChatController(context: arguments.context, chatLocation: .peer(peerId)), true) + } + }) + case .thrid: + arguments.openFaq() + } + }) + + }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) + case .filters: + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.accountSettingsFilters, icon: theme.icons.settingsFilters, activeIcon: theme.icons.settingsFiltersActive, type: .next, action: { + arguments.presentController(ChatListFiltersListController(context: arguments.context), true) + }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) + case .update(_, let state): + + var text: String = "" + #if !APP_STORE + if let state = state as? AppUpdateState { + switch state.loadingState { + case let .loading(_, current, total): + text = "\(Int(Float(current) / Float(total) * 100))%" + case let .readyToInstall(item), let .unarchiving(item): + text = "\(item.displayVersionString!).\(item.versionString!)" + case .uptodate: + text = "" //L10n.accountViewControllerDescUpdated + case .failed: + text = L10n.accountViewControllerDescFailed + default: + text = "" + } + } + #endif + + + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.accountViewControllerUpdate, icon: theme.icons.settingsUpdate, activeIcon: theme.icons.settingsUpdateActive, type: .nextContext(text), action: { + arguments.openUpdateApp() + }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) + case let .whiteSpace(_, height): + return GeneralRowItem(initialSize, height: height, stableId: stableId, border:[BorderType.Right]) + } + } + +} -class LayoutAccountController : EditableViewController, TableViewDelegate { +private func accountInfoEntries(peerView:PeerView, context: AccountContext, accounts: [AccountWithInfo], language: TelegramLocalization, privacySettings: AccountPrivacySettings?, webSessions: WebSessionsContextState, proxySettings: (ProxySettings, ConnectionStatus), passportVisible: Bool, appUpdateState: Any?, hasWallet: Bool, hasFilters: Bool, sessionsCount: Int, unAuthStatus: UNUserNotifications.AuthorizationStatus) -> [AccountInfoEntry] { + var entries:[AccountInfoEntry] = [] - func selectionDidChange(row: Int, item: TableRowItem, byClick: Bool, isNew: Bool) { + var index:Int = 0 + if let peer = peerViewMainPeer(peerView) as? TelegramUser { + entries.append(.info(index: index, peer)) + index += 1 } - func selectionWillChange(row: Int, item: TableRowItem) -> Bool { - return true + entries.append(.whiteSpace(index: index, height: 20)) + index += 1 + + for account in accounts { + if account.account.id != context.account.id { + entries.append(.accountRecord(index: index, info: account)) + index += 1 + } + } + if accounts.count < 3 { + entries.append(.addAccount(index: index)) + index += 1 + } + entries.append(.whiteSpace(index: index, height: 20)) + index += 1 + + if !proxySettings.0.servers.isEmpty { + let status: String + switch proxySettings.1 { + case .online: + status = proxySettings.0.enabled ? L10n.accountSettingsProxyConnected : L10n.accountSettingsProxyDisabled + default: + status = proxySettings.0.enabled ? L10n.accountSettingsProxyConnecting : L10n.accountSettingsProxyDisabled + } + entries.append(.proxy(index: index, status: status)) + index += 1 + + entries.append(.whiteSpace(index: index, height: 20)) + index += 1 } - func isSelectable(row: Int, item: TableRowItem) -> Bool { - return row > 0 + entries.append(.general(index: index)) + index += 1 + entries.append(.notifications(index: index, status: unAuthStatus)) + index += 1 + entries.append(.privacy(index: index, privacySettings, webSessions)) + index += 1 + entries.append(.dataAndStorage(index: index)) + index += 1 + entries.append(.activeSessions(index: index, activeSessions: sessionsCount)) + index += 1 + entries.append(.appearance(index: index)) + index += 1 + entries.append(.language(index: index, current: language.localizedName)) + index += 1 + entries.append(.stickers(index: index)) + index += 1 + + if hasFilters { + entries.append(.filters(index: index)) + index += 1 } + + if let state = appUpdateState { + entries.append(.update(index: index, state: state)) + index += 1 + } + + + entries.append(.whiteSpace(index: index, height: 20)) + index += 1 + + if let peer = peerViewMainPeer(peerView) as? TelegramUser, passportVisible { + entries.append(.passport(index: index, peer: peer)) + index += 1 + } + + entries.append(.whiteSpace(index: index, height: 20)) + index += 1 + entries.append(.faq(index: index)) + index += 1 + entries.append(.ask(index: index)) + index += 1 + + entries.append(.whiteSpace(index: index, height: 20)) + index += 1 + + return entries +} - private let accountManager:AccountManager - private let peer = Promise() - private let statePromise:ValuePromise = ValuePromise(ignoreRepeated: true) - private let connectionPromise = Promise(.online) - private let entries:Atomic<[AppearanceWrapperEntry]?> = Atomic(value: nil) +private func prepareEntries(left: [AppearanceWrapperEntry], right: [AppearanceWrapperEntry], arguments: AccountInfoArguments, initialSize: NSSize) -> TableUpdateTransition { + let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in + return entry.entry.item(arguments, initialSize: initialSize) + } + return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true) +} + + +class LayoutAccountController : TableViewController { private let disposable = MetaDisposable() - private let updatePhotoDisposable = MetaDisposable() + private var searchController: InputDataController? + private let searchState: ValuePromise = ValuePromise(ignoreRepeated: true) var navigation:NavigationViewController? { - return super.navigationController?.navigationController + return context.sharedContext.bindings.rootNavigation() } - override func viewDidLoad() { - super.viewDidLoad() - genericView.border = [.Right] - genericView.delegate = self - self.rightBarView.border = [.Right] - readyOnce() + override func viewDidResized(_ size: NSSize) { + super.viewDidResized(size) + self.searchController?.view.frame = bounds + } + + override func getCenterBarViewOnce() -> TitledBarView { + let searchBar = AccountSearchBarView(controller: self) + searchBar.searchView.searchInteractions = SearchInteractions({ [weak self] state, animated in + guard let `self` = self else {return} + self.searchState.set(state) + switch state.state { + case .Focus: + self.showSearchController(animated: animated) + case .None: + self.hideSearchController(animated: animated) + } + + }, { [weak self] state in + self?.searchState.set(state) + }) + + return searchBar + } + + private func showSearchController(animated: Bool) { + if searchController == nil { + let rect = genericView.bounds + let searchController = SearchSettingsController(context: context, searchQuery: self.searchState.get(), archivedStickerPacks: .single(nil), privacySettings: self.settings.get() |> map { $0.0 }) + searchController.bar = .init(height: 0) + searchController._frameRect = rect + searchController.tableView.border = [.Right] + self.searchController = searchController + searchController.navigationController = self.navigationController + searchController.viewWillAppear(true) + if animated { + searchController.view.layer?.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion:{ [weak self] complete in + if complete { + self?.searchController?.viewDidAppear(animated) + } + }) + } else { + searchController.viewDidAppear(animated) + } + + self.addSubview(searchController.view) + } } - private var editButton:ImageButton? = nil - private var doneButton:TitleButton? = nil + + + private func hideSearchController(animated: Bool) { + if let searchController = self.searchController { + searchController.viewWillDisappear(animated) + searchController.view.layer?.opacity = animated ? 1.0 : 0.0 + searchController.viewDidDisappear(true) + self.searchController = nil + let view = searchController.view + + searchController.view._change(opacity: 0, animated: animated, duration: 0.25, timingFunction: CAMediaTimingFunctionName.spring, completion: { [weak view] completed in + view?.removeFromSuperview() + }) + } + } + + override func escapeKeyAction() -> KeyHandlerResult { + guard context.sharedContext.layout != .minimisize else { + return .invoked + } + let searchView = (self.centerBarView as? AccountSearchBarView)?.searchView + if let searchView = searchView { + if searchView.state == .None { + return searchView.changeResponder() ? .invoked : .rejected + } else if searchView.state == .Focus && searchView.query.length > 0 { + searchView.change(state: .None, true) + return .invoked + } + } + return .rejected + } + + override func getRightBarViewOnce() -> BarView { + let button = TextButtonBarView(controller: self, text: L10n.navigationEdit, style: navigationButtonStyle, alignment:.Right) + let context = self.context + button.set(handler: { [weak self] _ in + guard let `self` = self else {return} + let first: Atomic = Atomic(value: true) + EditAccountInfoController(context: context, f: { [weak self] controller in + self?.arguments?.presentController(controller, first.swap(false)) + }) + }, for: .Click) + return button + } override func requestUpdateRightBar() { super.requestUpdateRightBar() - editButton?.style = navigationButtonStyle - editButton?.set(image: theme.icons.chatActions, for: .Normal) - editButton?.set(image: theme.icons.chatActionsActive, for: .Highlight) - - editButton?.setFrameSize(68, 50) - editButton?.center() - doneButton?.set(color: theme.colors.blueUI, for: .Normal) - doneButton?.style = navigationButtonStyle + (rightBarView as? TextButtonBarView)?.set(text: L10n.navigationEdit, for: .Normal) + (rightBarView as? TextButtonBarView)?.set(color: theme.colors.accent, for: .Normal) + (rightBarView as? TextButtonBarView)?.needsLayout = true } - override func getRightBarViewOnce() -> BarView { - let back = BarView(70, controller: self) - back.border = [.Right] - let editButton = ImageButton() - editButton.disableActions() - back.addSubview(editButton) + override func selectionWillChange(row: Int, item: TableRowItem, byClick: Bool) -> Bool { + return item is GeneralInteractedRowItem || item is AccountInfoItem || item is ShortPeerRowItem + } + + override func isSelectable(row: Int, item: TableRowItem) -> Bool { + return true + } + + private let settings: Promise<(AccountPrivacySettings?, WebSessionsContextState, (ProxySettings, ConnectionStatus), Bool)> = Promise() + private let syncLocalizations = MetaDisposable() + fileprivate let passportPromise: Promise = Promise(false) + fileprivate let hasWallet: Promise = Promise(false) + fileprivate let hasFilters: Promise = Promise(false) + + private weak var arguments: AccountInfoArguments? + override func viewDidLoad() { + super.viewDidLoad() + genericView.border = [.Right] + genericView.delegate = self + // self.rightBarView.border = [.Right] + let context = self.context + genericView.getBackgroundColor = { + return .clear + } + + let privacySettings = context.engine.privacy.requestAccountPrivacySettings() |> map(Optional.init) + + settings.set(combineLatest(Signal.single(nil) |> then(privacySettings), context.webSessions.state, proxySettings(accountManager: context.sharedContext.accountManager) |> mapToSignal { settings in + return context.account.network.connectionStatus |> map {(settings, $0)} + }, passportPromise.get())) - self.editButton = editButton - let doneButton = TitleButton() - doneButton.disableActions() - doneButton.set(font: .medium(.text), for: .Normal) - doneButton.set(text: tr(.navigationDone), for: .Normal) - doneButton.sizeToFit() - back.addSubview(doneButton) - doneButton.center() - self.doneButton = doneButton + syncLocalizations.set(context.engine.localization.synchronizedLocalizationListState().start()) + self.hasWallet.set(context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) + |> map { view -> Bool in + let appConfiguration = view.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? .defaultValue + let configuration = WalletConfiguration.with(appConfiguration: appConfiguration) + if #available(OSX 10.12, *) { + return configuration.config != nil + } else { + return false + } + }) - doneButton.isHidden = true + self.hasFilters.set(context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) + |> map { view -> Bool in + let configuration = ChatListFilteringConfiguration(appConfiguration: view.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? .defaultValue) + return configuration.isEnabled + }) + let previous:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) - editButton.set(handler: { [weak self] control in - - if !hasPopover(mainWindow), let strongSelf = self { - var items: [SPopoverItem] = [] - items.append(SPopoverItem(tr(.accountSettingsAbout), { - showModal(with: AboutModalController(), for: mainWindow) - }, theme.icons.settingsAbout)) - items.append(SPopoverItem(tr(.accountSettingsLogout), { [weak strongSelf] in - confirm(for: mainWindow, with: tr(.accountConfirmLogout), and: tr(.accountConfirmLogoutText), successHandler: {_ in - if let strongSelf = strongSelf { - let _ = logoutFromAccount(id: strongSelf.account.id, accountManager: strongSelf.accountManager).start() - } - }) - - }, theme.icons.settingsLogout, theme.colors.redUI)) - showPopover(for: control, with: SPopoverViewController(items: items), edge: .maxY, inset: NSMakePoint(-60, -50)) + + let arguments = AccountInfoArguments(context: context, presentController: { [weak self] controller, main in + guard let navigation = self?.navigation as? MajorNavigationController else {return} + guard let singleLayout = self?.context.sharedContext.layout else {return} + var main = main + if let controller = navigation.controller as? InputDataController, controller.identifier == "wallet-create" { + main = false } - + if main { + navigation.removeExceptMajor() + } + navigation.push(controller, !main || singleLayout == .single) + }, openFaq: { + openFaq(context: context) + }, ask: { + + }, openUpdateApp: { [weak self] in + guard let navigation = self?.navigation as? MajorNavigationController else {return} + #if !APP_STORE + navigation.push(AppUpdateViewController(), false) + #endif + }) + + self.arguments = arguments + + let atomicSize = self.atomicSize + - }, for: .Hover) + let appUpdateState: Signal + #if APP_STORE + appUpdateState = .single(nil) + #else + appUpdateState = appUpdateStateSignal |> map(Optional.init) + #endif - doneButton.set(handler: { [weak self] _ in - self?.changeState() - }, for: .Click) - requestUpdateRightBar() - return back - } + let sessionsCount = context.activeSessionsContext.state |> map { + $0.sessions.count + } + + let apply = combineLatest(queue: prepareQueue, context.account.viewTracker.peerView(context.account.peerId), context.sharedContext.activeAccountsWithInfo, appearanceSignal, settings.get(), appUpdateState, hasWallet.get(), hasFilters.get(), sessionsCount, UNUserNotifications.recurrentAuthorizationStatus(context)) |> map { peerView, accounts, appearance, settings, appUpdateState, hasWallet, hasFilters, sessionsCount, unAuthStatus -> TableUpdateTransition in + let entries = accountInfoEntries(peerView: peerView, context: context, accounts: accounts.accounts, language: appearance.language, privacySettings: settings.0, webSessions: settings.1, proxySettings: settings.2, passportVisible: settings.3, appUpdateState: appUpdateState, hasWallet: hasWallet, hasFilters: hasFilters, sessionsCount: sessionsCount, unAuthStatus: unAuthStatus).map {AppearanceWrapperEntry(entry: $0, appearance: appearance)} + var size = atomicSize.modify {$0} + size.width = max(size.width, 280) + return prepareEntries(left: previous.swap(entries), right: entries, arguments: arguments, initialSize: size) + } |> deliverOnMainQueue + + disposable.set(apply.start(next: { [weak self] transition in + self?.genericView.merge(with: transition) + self?.readyOnce() + })) + } + + + override func navigationWillChangeController() { if let navigation = navigation as? ExMajorNavigationController { - if navigation.controller is StorageUsageController { - if let item = genericView.item(stableId: AnyHashable(AccountInfoEntry.dataAndStorage(index: 0).stableId)) { - _ = genericView.select(item: item) - } - } else if navigation.controller is NotificationSettingsViewController { - if let item = genericView.item(stableId: AnyHashable(AccountInfoEntry.notifications(index: 0).stableId)) { + if navigation.controller is DataAndStorageViewController { + if let item = genericView.item(stableId: AnyHashable(AccountInfoEntryId.index(5))) { _ = genericView.select(item: item) } } else if navigation.controller is PrivacyAndSecurityViewController { - if let item = genericView.item(stableId: AnyHashable(AccountInfoEntry.privacy(index: 0).stableId)) { + if let item = genericView.item(stableId: AnyHashable(AccountInfoEntryId.index(7))) { _ = genericView.select(item: item) } } else if navigation.controller is LanguageViewController { - if let item = genericView.item(stableId: AnyHashable(AccountInfoEntry.language(index: 0, current: "").stableId)) { + if let item = genericView.item(stableId: AnyHashable(AccountInfoEntryId.index(8))) { _ = genericView.select(item: item) } } else if navigation.controller is InstalledStickerPacksController { - if let item = genericView.item(stableId: AnyHashable(AccountInfoEntry.stickers(index: 0).stableId)) { + if let item = genericView.item(stableId: AnyHashable(AccountInfoEntryId.index(9))) { _ = genericView.select(item: item) } + } else if navigation.controller is GeneralSettingsViewController { - if let item = genericView.item(stableId: AnyHashable(AccountInfoEntry.general(index: 0).stableId)) { + if let item = genericView.item(stableId: AnyHashable(AccountInfoEntryId.index(2))) { _ = genericView.select(item: item) } - } else if navigation.controller is UsernameSettingsViewController { - if let item = genericView.item(stableId: AnyHashable(AccountInfoEntry.username(index: 0, username: "").stableId)) { + } else if navigation.controller is RecentSessionsController { + if let item = genericView.item(stableId: AnyHashable(AccountInfoEntryId.index(6))) { _ = genericView.select(item: item) } - } else if navigation.controller is BioViewController { - if let item = genericView.item(stableId: AnyHashable(AccountInfoEntry.bio(index: 0, about: "").stableId)) { + } else if navigation.controller is PassportController { + if let item = genericView.item(stableId: AccountInfoEntryId.index(Int(13))) { _ = genericView.select(item: item) } - } else if PhoneNumberIntroController.assciatedControllerTypes.contains(where: {navigation.controller.isKind(of: $0)}) { - - if let item = genericView.item(stableId: AnyHashable(AccountInfoEntry.phone(index: 0, phone: "").stableId)) { - _ = genericView.select(item: item) + } else if let controller = navigation.controller as? InputDataController { + switch true { + case controller.identifier == "proxy": + if let item = genericView.item(stableId: AnyHashable(AccountInfoEntryId.index(3))) { + _ = genericView.select(item: item) + } + case controller.identifier == "account": + if let item = genericView.item(stableId: AnyHashable(AccountInfoEntryId.index(0))) { + _ = genericView.select(item: item) + } + case controller.identifier == "passport": + if let item = genericView.item(stableId: AnyHashable(AccountInfoEntryId.index(13))) { + _ = genericView.select(item: item) + } + case controller.identifier == "app_update": + if let item = genericView.item(stableId: AnyHashable(AccountInfoEntryId.index(11))) { + _ = genericView.select(item: item) + } + case controller.identifier == "filters": + if let item = genericView.item(stableId: AnyHashable(AccountInfoEntryId.index(10))) { + _ = genericView.select(item: item) + } + case controller.identifier == "notification-settings": + if let item = genericView.item(stableId: AnyHashable(AccountInfoEntryId.index(4))) { + _ = genericView.select(item: item) + } + case controller.identifier == "app_appearance": + if let item = genericView.item(stableId: AnyHashable(AccountInfoEntryId.index(12))) { + _ = genericView.select(item: item) + } + default: + genericView.cancelSelection() } + } else { genericView.cancelSelection() } @@ -416,313 +918,82 @@ class LayoutAccountController : EditableViewController, TableViewDele override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) (navigation as? MajorNavigationController)?.add(listener: WeakReference(value: self)) - updateLocalizationAndTheme() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - let apply = combineLatest(account.viewTracker.peerView( account.peerId), connectionPromise.get(), statePromise.get(), appearanceSignal) |> deliverOn(Queue.mainQueue()) |> map { [weak self] peerView, connection, state, appearance -> TableUpdateTransition in - - if let strongSelf = self { - let entries = strongSelf.entries(for: state, account: strongSelf.account, connection: connection, peerView: peerView, language: appearance.language).map {AppearanceWrapperEntry(entry: $0, appearance: appearance)} - let previous = strongSelf.entries.swap(entries) - return strongSelf.prepareEntries(left: previous, right: entries, account: strongSelf.account, accountManager: strongSelf.accountManager, animated: true, atomicSize: strongSelf.atomicSize.modify({$0})) - } - return TableUpdateTransition(deleted: [], inserted: [], updated: []) - - } - - disposable.set(apply.start(next: { [weak self] transition in - - self?.genericView.merge(with: transition) - self?.navigationWillChangeController() - })) - peer.set(account.viewTracker.peerView(account.peerId) |> map { peerView -> TelegramUser? in - return peerView.peers[peerView.peerId] as? TelegramUser + passportPromise.set(context.engine.auth.twoStepAuthData() |> map { value in + return value.hasSecretValues + } |> `catch` { error -> Signal in + return .single(false) }) - connectionPromise.set(account.network.connectionStatus) - statePromise.set(.Normal) + updateLocalizationAndTheme(theme: theme) + + + context.window.set(handler: { [weak self] _ in + if let strongSelf = self { + return strongSelf.escapeKeyAction() + } + return .invokeNext + }, with: self, for: .Escape, priority:.low) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - disposable.set(nil) + window?.removeAllHandlers(for: self) } - - override func update(with state: ViewControllerState) { - if state == .Normal { - saveNamesIfNeeded(false) - } - super.update(with: state) - statePromise.set(state) + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + let context = self.context - editButton?.isHidden = state == .Edit - doneButton?.isHidden = state == .Normal - switch state { - case .Normal: - window?.removeObserver(for: self) - case .Edit: - window?.set(responder: { [weak self] () -> NSResponder? in - if let view = self?.genericView.viewNecessary(at: 0) as? AccountInfoView { - return view.firstResponder - } - return nil - }, with: self, priority: .high) - default: - break - } + let privacySettings = context.engine.privacy.requestAccountPrivacySettings() |> map(Optional.init) + + settings.set(combineLatest(Signal.single(nil) |> then(privacySettings), context.webSessions.state, proxySettings(accountManager: context.sharedContext.accountManager) |> mapToSignal { settings in + return context.account.network.connectionStatus |> map {(settings, $0)} + }, passportPromise.get())) + + + syncLocalizations.set(context.engine.localization.synchronizedLocalizationListState().start()) + } override func getLeftBarViewOnce() -> BarView { - return BarView(controller: self) + return BarView(10, controller: self) } - init(_ account:Account, accountManager:AccountManager) { - self.accountManager = accountManager - super.init(account) + override init(_ context: AccountContext) { + super.init(context) } - func entries(for state:ViewControllerState, account:Account, connection:ConnectionStatus, peerView:PeerView, language: Language) -> [AccountInfoEntry] { - var entries:[AccountInfoEntry] = [] - - var index:Int = 0 - - let user = peerViewMainPeer(peerView) as? TelegramUser - - if let peer = peerViewMainPeer(peerView) as? TelegramUser { - entries.append(.info(index: index, state == .Edit ? .edit : .normal, peer, connection)) - index += 1 - } - - - - entries.append(.username(index: index, username: peerViewMainPeer(peerView)?.addressName ?? "")) - index += 1 - - let cachedData = peerView.cachedData as? CachedUserData - entries.append(.bio(index: index, about: cachedData?.about ?? "")) - index += 1 - - entries.append(.phone(index: index, phone: user?.phone ?? "")) - index += 1 - - entries.append(.whiteSpace(index: index, height: 30)) - index += 1 - - entries.append(.general(index: index)) - index += 1 - entries.append(.notifications(index: index)) - index += 1 - entries.append(.dataAndStorage(index: index)) - index += 1 - entries.append(.privacy(index: index)) - index += 1 - entries.append(.language(index: index, current: tr(.accountSettingsCurrentLanguage))) - index += 1 - - entries.append(.stickers(index: index)) - index += 1 - - - -// entries.append(.accounts(index: index)) -// index += 1 - entries.append(.whiteSpace(index: index, height: 30)) - index += 1 - // entries.append(.about(index: index)) - // index += 1 - entries.append(.faq(index: index)) - index += 1 - entries.append(.ask(index: index)) - - if state == .Edit { - index += 1 - entries.append(.whiteSpace(index: index, height: 20)) - index += 1 - entries.append(.logout(index: index)) - } - - return entries + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + navigationController?.updateLocalizationAndTheme(theme: theme) } - func saveNamesIfNeeded(_ cState:Bool = true) -> Void { - if let item = genericView.item(at: 0) as? AccountInfoItem { - if item.firstName != (item.peer.firstName ?? "") || item.lastName != (item.peer.lastName ?? "") { - _ = showModalProgress(signal: updateAccountPeerName(account: account, firstName: item.firstName, lastName: item.lastName), for: mainWindow).start() - } - if cState { - changeState() - } - } + override func firstResponder() -> NSResponder? { + return nil } + - fileprivate func prepareEntries(left:[AppearanceWrapperEntry]?, right:[AppearanceWrapperEntry], account:Account, accountManager: AccountManager, animated:Bool, atomicSize:NSSize) -> TableUpdateTransition { - let atomicSize = NSMakeSize(max(280, atomicSize.width), atomicSize.height) - let (deleted,inserted,updated) = proccessEntriesWithoutReverse(left, right: right, { entry -> TableRowItem in - switch entry.entry { - case let .info(_, state, peer, connection): - return AccountInfoItem(atomicSize, stableId: entry.stableId, account: account, peer: peer, state: state, connectionStatus: connection, saveCallback: { [weak self] in - self?.saveNamesIfNeeded() - }, editCallback: { [weak self] in - self?.changeState() - }, imageCallback: { [weak self] in - if let strongSelf = self { - pickImage(for: mainWindow, completion:{ [weak strongSelf] image in - if let image = image { - strongSelf?.startUpdatePhoto(image, account: account) - } - }) - } - - }) - case .updatePhoto: - return GeneralInteractedRowItem(atomicSize, stableId: entry.stableId, name: tr(.accountSettingsSetProfilePhoto), nameStyle: blueActionButton, type: .none, action: {}, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) - case .general: - return GeneralInteractedRowItem(atomicSize, stableId: entry.stableId, name: tr(.accountSettingsGeneral), icon: theme.icons.settingsGeneral, type: .none, action: {[weak self] in - if !(self?.navigation?.controller is GeneralSettingsViewController) { - self?.navigation?.push(GeneralSettingsViewController(account)) - } - }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) - case .stickers: - return GeneralInteractedRowItem(atomicSize, stableId: entry.stableId, name: tr(.accountSettingsStickers), icon: theme.icons.settingsStickers, type: .none, action: { [weak self] in - if !(self?.navigation?.controller is InstalledStickerPacksController) { - self?.navigation?.push(InstalledStickerPacksController(account)) - } - }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) - case .notifications: - return GeneralInteractedRowItem(atomicSize, stableId: entry.stableId, name: tr(.accountSettingsNotifications), icon: theme.icons.settingsNotifications, type: .none, action: { [weak self] in - if !(self?.navigation?.controller is NotificationSettingsViewController) { - self?.navigation?.push(NotificationSettingsViewController(account)) - } - }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) - case let .username(_, username): - return GeneralInteractedRowItem(atomicSize, stableId: entry.stableId, name: username.isEmpty ? tr(.accountSettingsSetUsername) : username, icon: theme.icons.settingsUsername, nameStyle: ControlStyle(font: .normal(.title), foregroundColor: username.isEmpty ? theme.colors.blueUI : theme.colors.text), type: .none, action: { [weak self] in - if !(self?.navigation?.controller is UsernameSettingsViewController) { - self?.navigation?.push(UsernameSettingsViewController(account)) - } - }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) - case .bio(_, let about): - - return GeneralInteractedRowItem(atomicSize, stableId: entry.stableId, name: about.isEmpty ? tr(.accountSettingsSetBio) : about, icon: theme.icons.settingsBio, nameStyle: ControlStyle(font: .normal(.title), foregroundColor: about.isEmpty ? theme.colors.blueUI : theme.colors.text), type: .none, action: { [weak self] in - if !(self?.navigation?.controller is BioViewController) { - self?.navigation?.push(BioViewController(account)) - } - }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) - case let .phone(_, phone): - return GeneralInteractedRowItem(atomicSize, stableId: entry.stableId, name: formatPhoneNumber(phone), icon: theme.icons.settingsPhoneNumber, type: .none, action: { [weak self] in - if !(self?.navigation?.controller is PhoneNumberIntroController) { - self?.navigation?.push(PhoneNumberIntroController(account)) - } - }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) - case let .language(_, current): - return GeneralInteractedRowItem(atomicSize, stableId: entry.stableId, name: tr(.accountSettingsLanguage), icon: theme.icons.settingsLanguage, type: .context(stateback: { - return current - }), action: { [weak self] in - if !(self?.navigation?.controller is LanguageViewController) { - self?.navigation?.push(LanguageViewController(account)) - } - }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) - case .appearance: - return GeneralInteractedRowItem(atomicSize, stableId: entry.stableId, name: tr(.accountSettingsAppearance), type: .none, action: { [weak self] in - if !(self?.navigation?.controller is AppearanceViewController) { - self?.navigation?.push(AppearanceViewController(account)) - } - }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) - case .privacy: - return GeneralInteractedRowItem(atomicSize, stableId: entry.stableId, name: tr(.accountSettingsPrivacyAndSecurity), icon: theme.icons.settingsSecurity, type: .none, action: { [weak self] in - if !(self?.navigation?.controller is PrivacyAndSecurityViewController) { - self?.navigation?.push(PrivacyAndSecurityViewController(account, initialSettings: .single(nil) |> then(requestAccountPrivacySettings(account: account) |> map { Optional($0) }))) - } - }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) - case .dataAndStorage: - return GeneralInteractedRowItem(atomicSize, stableId: entry.stableId, name: tr(.accountSettingsStorage), icon: theme.icons.settingsStorage, type: .none, action: { [weak self] in - if !(self?.navigation?.controller is StorageUsageController) { - self?.navigation?.push(StorageUsageController(account)) - } - }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) - case .accounts: - return GeneralInteractedRowItem(atomicSize, stableId: entry.stableId, name: "tr(.accountSettingsAccounts)", type: .none, action: { [weak self] in - self?.navigation?.push(AccountsListViewController(account, accountManager: accountManager)) - }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) - case .about: - return GeneralInteractedRowItem(atomicSize, stableId: entry.stableId, name: tr(.accountSettingsAbout), icon: theme.icons.settingsFaq, type: .none, action: { [weak self] in - if let window = self?.window { - showModal(with: AboutModalController(), for: window) - } - }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) - case .faq: - return GeneralInteractedRowItem(atomicSize, stableId: entry.stableId, name: tr(.accountSettingsFAQ), icon: theme.icons.settingsFaq, type: .none, action: { - - let language = appCurrentLanguage.languageCode[appCurrentLanguage.languageCode.index(appCurrentLanguage.languageCode.endIndex, offsetBy: -2) ..< appCurrentLanguage.languageCode.endIndex] - - _ = showModalProgress(signal: webpagePreview(account: account, url: "https://telegram.org/faq/" + language) |> deliverOnMainQueue, for: mainWindow).start(next: { webpage in - if let webpage = webpage { - showInstantPage(InstantPageViewController(account, webPage: webpage, message: nil)) - } else { - execute(inapp: .external(link: "https://telegram.org/faq/" + language, true)) - } - }) - - }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) - case .ask: - return GeneralInteractedRowItem(atomicSize, stableId: entry.stableId, name: tr(.accountSettingsAskQuestion), icon: theme.icons.settingsAskQuestion, type: .none, action: { [weak self] in - - confirm(for: mainWindow, with: appName, and: tr(.accountConfirmAskQuestion), thridTitle: tr(.accountConfirmGoToFaq), successHandler: { [weak self] result in - switch result { - case .basic: - _ = showModalProgress(signal: supportPeerId(account: account), for: mainWindow).start(next: { [weak self] peerId in - if let peerId = peerId { - self?.navigation?.push(ChatController(account: account, peerId: peerId)) - } - }) - case .thrid: - execute(inapp: .external(link: "https://telegram.org/faq", false)) - } - }) - - }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) - case .logout: - return GeneralInteractedRowItem(atomicSize, stableId: entry.stableId, name: tr(.accountSettingsLogout), nameStyle: redActionButton, type: .none, action: { [weak self] in - - confirm(for: mainWindow, with: tr(.accountConfirmLogout), and: tr(.accountConfirmLogoutText), successHandler: { [weak self] _ in - if let strongSelf = self { - let _ = logoutFromAccount(id: strongSelf.account.id, accountManager: strongSelf.accountManager).start() - } - }) - - - - }, border:[BorderType.Right], inset:NSEdgeInsets(left:16)) - case let .whiteSpace(_, height): - return GeneralRowItem(atomicSize, height: height, stableId: entry.stableId, border:[BorderType.Right]) - } - }) + override func scrollup(force: Bool = false) { - return TableUpdateTransition(deleted: deleted, inserted: inserted, updated: updated, animated: animated) - } - - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - navigationController?.updateLocalizationAndTheme() - } - - func startUpdatePhoto(_ image: NSImage, account:Account) { + if searchController != nil { + let searchView = (self.centerBarView as? AccountSearchBarView)?.searchView + searchView?.cancel(true) + return + } + + if let currentEvent = NSApp.currentEvent, currentEvent.clickCount == 5 { + context.sharedContext.bindings.rootNavigation().push(DeveloperViewController(context: context)) + } - updatePhotoDisposable.set((putToTemp(image: image) |> mapToSignal { path -> Signal in - return updatePeerPhoto(account: account, peerId: account.peerId, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: arc4random64())) - |> mapError {_ in} |> map {_ in} - }).start()) + genericView.scroll(to: .up(true)) } deinit { + syncLocalizations.dispose() disposable.dispose() - updatePhotoDisposable.dispose() } } diff --git a/Telegram-Mac/AccountsListViewController.swift b/Telegram-Mac/AccountsListViewController.swift deleted file mode 100644 index e632e78a31..0000000000 --- a/Telegram-Mac/AccountsListViewController.swift +++ /dev/null @@ -1,161 +0,0 @@ -// -// AccountsListViewController.swift -// Telegram -// -// Created by keepcoder on 20/02/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - -import Cocoa -import TGUIKit -import SwiftSignalKitMac -import PostboxMac -import TelegramCoreMac - -enum AccountRecordEntryStableId : Hashable { - case record(AccountRecordId) - case newAccount - - static func ==(lhs:AccountRecordEntryStableId, rhs:AccountRecordEntryStableId) -> Bool { - switch lhs { - case let .record(id): - if case .record(id) = rhs { - return true - } else { - return false - } - case .newAccount: - if case .newAccount = rhs { - return true - } else { - return false - } - } - } - - var hashValue: Int { - switch self { - case let .record(id): - return id.hashValue - case .newAccount: - return 1000 - } - } -} - -enum AccountRecordEntry : Identifiable, Comparable { - case record(AccountRecord, Bool, Int) - case newAccount - - var stableId: AccountRecordEntryStableId { - switch self { - case let .record(record, _, _): - return .record(record.id) - case .newAccount: - return .newAccount - } - } - - var index:Int { - switch self { - case let .record(_, _, idx): - return idx - case .newAccount: - return 10000 - } - } -} - -func ==(lhs:AccountRecordEntry, rhs: AccountRecordEntry) -> Bool { - switch lhs { - case let .record(id, isCurrent, idx): - if case .record(id, isCurrent, idx) = rhs { - return true - } else { - return false - } - case .newAccount: - if case .newAccount = rhs { - return true - } else { - return false - } - } -} - -func <(lhs:AccountRecordEntry, rhs: AccountRecordEntry) -> Bool { - return lhs.index < rhs.index -} - - -class AccountsListViewController : GenericViewController { - private let account:Account - private let accountManager:AccountManager - private let statePromise:ValuePromise = ValuePromise(.Normal, ignoreRepeated: true) - init(_ account:Account, accountManager:AccountManager) { - self.account = account - self.accountManager = accountManager - super.init() - } - - override func viewDidLoad() { - super.viewDidLoad() - let entries:Atomic<[AccountRecordEntry]> = Atomic(value: []) - let initialSize = self.atomicSize - self.genericView.merge(with: accountManager.accountRecords() |> mapToSignal { [weak self] records -> Signal in - if let strongSelf = self { - strongSelf.readyOnce() - let converted = strongSelf.entries(from: records) - return strongSelf.prepareEntries(left: entries.swap(converted), right: converted, initialSize: initialSize.modify {$0}) - } else { - return .complete() - } - } |> deliverOnMainQueue) - - - } - - private func entries(from: AccountRecordsView) -> [AccountRecordEntry] { - var entries:[AccountRecordEntry] = [] - entries.append(.newAccount) - - var i:Int = 0 - for record in from.records { - entries.append(.record(record, record == from.currentRecord, i)) - i += 1 - } - return entries - } - - func prepareEntries(left: [AccountRecordEntry], right:[AccountRecordEntry], initialSize:NSSize) -> Signal { - return Signal { subscriber in - - let (removed, inserted, updated) = proccessEntries(left, right: right, { (entry) -> TableRowItem in - - switch entry { - case .newAccount: - return GeneralInteractedRowItem(initialSize, stableId: entry.stableId, name: tr(.accountsControllerNewAccount), nameStyle: blueActionButton, type: .none, action: { [weak self] in - let _ = self?.accountManager.modify({ modifier -> Void in - let id = modifier.createRecord([]) - modifier.setCurrentId(id) - }).start() - }) - case let .record(record, isCurrent, _): - return GeneralInteractedRowItem(initialSize, stableId: entry.stableId, name: "\(record.id.hashValue)", nameStyle: ControlStyle(font: .normal(.title), foregroundColor: isCurrent ? .grayText : .text), type: .none, action: { [weak self] in - let _ = self?.accountManager.modify({ modifier -> Void in - modifier.setCurrentId(record.id) - }).start() - }) - } - - }) - - subscriber.putNext(TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated)) - subscriber.putCompletion() - - return EmptyDisposable - } - } - - -} diff --git a/Telegram-Mac/AddContactModalController.swift b/Telegram-Mac/AddContactModalController.swift index 580d61206e..d0e4c863f3 100644 --- a/Telegram-Mac/AddContactModalController.swift +++ b/Telegram-Mac/AddContactModalController.swift @@ -8,154 +8,174 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac +import TelegramCore -private class AddContactControllerView : View, NSTextFieldDelegate { - private let headerView:TextView = TextView() - fileprivate let firstName:NSTextField = NSTextField() - fileprivate let lastName:NSTextField = NSTextField() - fileprivate let phoneNumber:NSTextField = NSTextField() - required init(frame frameRect: NSRect) { - super.init(frame: frameRect) - let layout = TextViewLayout(.initialize(string: tr(.contactsAddContact), color: theme.colors.text, font: .normal(.title)), maximumNumberOfLines: 1) - layout.measure(width: frameRect.width) - headerView.update(layout) - addSubview(headerView) - addSubview(firstName) - addSubview(lastName) - addSubview(phoneNumber) - - firstName.nextResponder = lastName - firstName.nextKeyView = lastName - - lastName.nextResponder = phoneNumber - lastName.nextKeyView = phoneNumber - - //phoneNumber.nextResponder = firstName - //phoneNumber.nextKeyView = firstName - - firstName.delegate = self - lastName.delegate = self - phoneNumber.delegate = self +import SwiftSignalKit +import Postbox - - firstName.isBordered = false - firstName.isBezeled = false - firstName.focusRingType = .none - - lastName.isBordered = false - lastName.isBezeled = false - lastName.focusRingType = .none - - phoneNumber.isBordered = false - phoneNumber.isBezeled = false - phoneNumber.focusRingType = .none - - - firstName.placeholderAttributedString = NSAttributedString.initialize(string: tr(.contactsFirstNamePlaceholder), color: theme.colors.grayText, font: .normal(.custom(13.5))) - lastName.placeholderAttributedString = NSAttributedString.initialize(string: tr(.contactsLastNamePlaceholder), color: theme.colors.grayText, font: .normal(.custom(13.5))) - phoneNumber.placeholderAttributedString = NSAttributedString.initialize(string: tr(.contactsPhoneNumberPlaceholder), color: theme.colors.grayText, font: .normal(.custom(13.5))) - - firstName.setFrameSize(NSMakeSize(frameRect.width - 40, 20)) - lastName.setFrameSize(NSMakeSize(frameRect.width - 40, 20)) - phoneNumber.setFrameSize(NSMakeSize(frameRect.width - 40, 20)) - - updateLocalizationAndTheme() - } - - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - backgroundColor = theme.colors.background - headerView.backgroundColor = theme.colors.background - firstName.backgroundColor = theme.colors.background - lastName.backgroundColor = theme.colors.background - phoneNumber.backgroundColor = theme.colors.background - } +private struct AddContactState : Equatable { + let firstName: String + let lastName: String + let phoneNumber: String + let errors: [InputDataIdentifier : InputDataValueError] - override func controlTextDidChange(_ obj: Notification) { - firstName.stringValue = firstName.stringValue.nsstring.substring(with: NSMakeRange(0, min(firstName.stringValue.length, 20))) - lastName.stringValue = lastName.stringValue.nsstring.substring(with: NSMakeRange(0, min(lastName.stringValue.length, 20))) - phoneNumber.stringValue = formatPhoneNumber(phoneNumber.stringValue) + init(firstName: String, lastName: String, phoneNumber: String, errors: [InputDataIdentifier : InputDataValueError]) { + self.firstName = firstName + self.lastName = lastName + self.phoneNumber = phoneNumber + self.errors = errors } - - override func layout() { - super.layout() - headerView.centerX(y: floorToScreenPixels((50 - headerView.frame.height)/2)) - firstName.centerX(y: 50 + 35) - lastName.centerX(y: firstName.frame.maxY + 30) - phoneNumber.centerX(y: lastName.frame.maxY + 30) + func withUpdatedError(_ error: InputDataValueError?, for key: InputDataIdentifier) -> AddContactState { + var errors = self.errors + if let error = error { + errors[key] = error + } else { + errors.removeValue(forKey: key) + } + return AddContactState(firstName: self.firstName, lastName: self.lastName, phoneNumber: self.phoneNumber, errors: errors) } - override func draw(_ layer: CALayer, in ctx: CGContext) { - super.draw(layer, in: ctx) - ctx.setFillColor(theme.colors.border.cgColor) - ctx.fill(NSMakeRect(0, 50, frame.width, .borderSize)) - ctx.fill(NSMakeRect(firstName.frame.minX, firstName.frame.maxY + 2, firstName.frame.width, .borderSize)) - ctx.fill(NSMakeRect(lastName.frame.minX, lastName.frame.maxY + 2, lastName.frame.width, .borderSize)) - ctx.fill(NSMakeRect(phoneNumber.frame.minX, phoneNumber.frame.maxY + 2, phoneNumber.frame.width, .borderSize)) + func withUpdatedFirstName(_ firstName: String) -> AddContactState { + return AddContactState(firstName: firstName, lastName: self.lastName, phoneNumber: self.phoneNumber, errors: self.errors) } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + func withUpdatedLastName(_ lastName: String) -> AddContactState { + return AddContactState(firstName: self.firstName, lastName: lastName, phoneNumber: self.phoneNumber, errors: self.errors) + } + func withUpdatedPhoneNumber(_ phoneNumber: String) -> AddContactState { + return AddContactState(firstName: self.firstName, lastName: self.lastName, phoneNumber: phoneNumber, errors: self.errors) } } -class AddContactModalController: ModalViewController { +private let _id_input_first_name = InputDataIdentifier("_id_input_first_name") +private let _id_input_last_name = InputDataIdentifier("_id_input_last_name") +private let _id_input_phone_number = InputDataIdentifier("_id_input_phone_number") - private let account:Account - override func viewDidLoad() { - super.viewDidLoad() - readyOnce() - } +private func addContactEntries(state: AddContactState) -> [InputDataEntry] { + var entries: [InputDataEntry] = [] - override func viewClass() -> AnyClass { - return AddContactControllerView.self - } + var sectionId: Int32 = 0 + var index: Int32 = 0 + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: .string(state.firstName), error: state.errors[_id_input_first_name], identifier: _id_input_first_name, mode: .plain, data: InputDataRowData(viewType: .firstItem), placeholder: nil, inputPlaceholder: L10n.contactsFirstNamePlaceholder, filter: { $0 }, limit: 255)) + index += 1 + + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: .string(state.lastName), error: state.errors[_id_input_last_name], identifier: _id_input_last_name, mode: .plain, data: InputDataRowData(viewType: .innerItem), placeholder: nil, inputPlaceholder: L10n.contactsLastNamePlaceholder, filter: { $0 }, limit: 255)) + index += 1 - override func firstResponder() -> NSResponder? { - if genericView.window?.firstResponder == genericView.firstName.textView || genericView.window?.firstResponder == genericView.lastName.textView || genericView.window?.firstResponder == genericView.phoneNumber.textView { - return genericView.window?.firstResponder - } - return genericView.firstName - } + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: .string(state.phoneNumber), error: state.errors[_id_input_phone_number], identifier: _id_input_phone_number, mode: .plain, data: InputDataRowData(viewType: .lastItem), placeholder: nil, inputPlaceholder: L10n.contactsPhoneNumberPlaceholder, filter: { text in + return text.trimmingCharacters(in: CharacterSet(charactersIn: "0987654321+ ").inverted) + }, limit: 30)) + index += 1 + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func AddContactModalController(_ context: AccountContext) -> InputDataModalController { - private var genericView:AddContactControllerView { - return self.view as! AddContactControllerView + + let initialState = AddContactState(firstName: "", lastName: "", phoneNumber: "", errors: [:]) + + let statePromise = ValuePromise(initialState, ignoreRepeated: false) + let stateValue = Atomic(value: initialState) + let updateState: ((AddContactState) -> AddContactState) -> Void = { f in + statePromise.set(stateValue.modify (f)) } - init(account: Account) { - self.account = account - super.init(frame: NSMakeRect(0, 0, 300, 240)) - bar = .init(height: 0) + let dataSignal = statePromise.get() |> map { state in + return addContactEntries(state: state) } - func importAndCloseIfPossible() { - if genericView.firstName.stringValue.length == 0 { - genericView.firstName.shake() - } else if genericView.phoneNumber.stringValue.length == 0 { - genericView.phoneNumber.shake() - } else { - close() - _ = (showModalProgress(signal: importContact(account: account, firstName: genericView.firstName.stringValue , lastName: genericView.lastName.stringValue, phoneNumber: genericView.phoneNumber.stringValue), for: mainWindow) |> deliverOnMainQueue).start(next: { [weak self] peerId in - if let peerId = peerId, let account = self?.account { - account.context.mainNavigation?.push(ChatController(account: account, peerId: peerId)) - } else { - alert(for: mainWindow, header: tr(.contactsNotRegistredTitle), info: tr(.contactsNotRegistredDescription)) + var close: (() -> Void)? + + var shouldMakeNextResponderAfterTransition: InputDataIdentifier? = nil + + let controller = InputDataController(dataSignal: dataSignal |> map { InputDataSignalValue(entries: $0) }, title: L10n.contactsAddContact, validateData: { data in + + return .fail(.doSomething { f in + let state = stateValue.with {$0} + + var fields: [InputDataIdentifier : InputDataValidationFailAction] = [:] + + if state.firstName.isEmpty { + fields[_id_input_first_name] = .shake + shouldMakeNextResponderAfterTransition = _id_input_first_name + } + + if state.phoneNumber.isEmpty { + fields[_id_input_phone_number] = .shake + if shouldMakeNextResponderAfterTransition == nil { + shouldMakeNextResponderAfterTransition = _id_input_phone_number + } + updateState { + $0.withUpdatedError(InputDataValueError(description: L10n.contactsPhoneNumberInvalid, target: .data), for: _id_input_phone_number) } - }) + } + + if !fields.isEmpty { + f(.fail(.fields(fields))) + } else { + _ = (showModalProgress(signal: context.engine.contacts.importContact(firstName: state.firstName, lastName: state.lastName, phoneNumber: state.phoneNumber), for: mainWindow) |> deliverOnMainQueue).start(next: { peerId in + if let peerId = peerId { + context.sharedContext.bindings.rootNavigation().push(ChatController(context: context, chatLocation: .peer(peerId))) + close?() + } else { + updateState { + $0.withUpdatedError(InputDataValueError(description: L10n.contactsPhoneNumberNotRegistred, target: .data), for: _id_input_phone_number) + } + } + }) + } + + updateState { + $0 + } + }) + + + }, updateDatas: { data in + updateState { state in + return state + .withUpdatedFirstName(data[_id_input_first_name]?.stringValue ?? "") + .withUpdatedLastName(data[_id_input_last_name]?.stringValue ?? "") + .withUpdatedPhoneNumber(formatPhoneNumber(data[_id_input_phone_number]?.stringValue ?? "")) + .withUpdatedError(nil, for: _id_input_first_name) + .withUpdatedError(nil, for: _id_input_last_name) + .withUpdatedError(nil, for: _id_input_phone_number) } - } + + return .none + }, afterDisappear: { + + }, afterTransaction: { controller in + if let identifier = shouldMakeNextResponderAfterTransition { + controller.makeFirstResponderIfPossible(for: identifier) + } + shouldMakeNextResponderAfterTransition = nil + }) + + + let modalInteractions = ModalInteractions(acceptTitle: L10n.modalOK, accept: { [weak controller] in + controller?.validateInputValues() + }, drawBorder: true, singleButton: true) + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions, size: NSMakeSize(300, 300)) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) - override var modalInteractions: ModalInteractions? { - return ModalInteractions(acceptTitle: tr(.contactsAddContact), accept: { [weak self] in - self?.importAndCloseIfPossible() - }, cancelTitle: tr(.modalCancel), drawBorder: false) + close = { [weak modalController] in + modalController?.close() } + return modalController } diff --git a/Telegram-Mac/AddContactTableItem.swift b/Telegram-Mac/AddContactTableItem.swift index e3bb2e3494..40fd872599 100644 --- a/Telegram-Mac/AddContactTableItem.swift +++ b/Telegram-Mac/AddContactTableItem.swift @@ -17,14 +17,16 @@ class AddContactTableItem: TableRowItem { fileprivate let addContact:()->Void init(_ initialSize: NSSize, stableId: AnyHashable, addContact: @escaping()->Void) { _stableId = stableId - self.text = TextViewLayout(.initialize(string: tr(.contactsAddContact), color: theme.colors.blueUI, font: .normal(.title)), maximumNumberOfLines: 1) + + self.text = TextViewLayout(.initialize(string: tr(L10n.contactsAddContact), color: theme.colors.accent, font: .normal(.title)), maximumNumberOfLines: 1) self.addContact = addContact super.init(initialSize) } override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) text.measure(width: width - 80) - return super.makeSize(width, oldWidth: oldWidth) + return success } override func viewClass() -> AnyClass { @@ -90,7 +92,7 @@ class AddContactTableRowView : TableRowView { override func layout() { super.layout() - imageView.centerY(x: floorToScreenPixels((56 - imageView.frame.width)/2)) + imageView.centerY(x: floorToScreenPixels(backingScaleFactor, (56 - imageView.frame.width)/2)) textView.layout?.measure(width: frame.width - 66) textView.update(textView.layout) textView.centerY(x: 56) diff --git a/Telegram-Mac/AdditionalSettings.swift b/Telegram-Mac/AdditionalSettings.swift new file mode 100644 index 0000000000..41d17a805d --- /dev/null +++ b/Telegram-Mac/AdditionalSettings.swift @@ -0,0 +1,71 @@ +// +// AdditionalSettings.swift +// Telegram +// +// Created by keepcoder on 13/11/2017. +// Copyright © 2017 Telegram. All rights reserved. +// + + +import Cocoa +import Postbox +import SwiftSignalKit +import TelegramCore + + +public struct AdditionalSettings: PreferencesEntry, Equatable { + public let useTouchId: Bool + + public static var defaultSettings: AdditionalSettings { + return AdditionalSettings(useTouchId: false) + } + + init(useTouchId: Bool) { + self.useTouchId = useTouchId + } + + public init(decoder: PostboxDecoder) { + self.useTouchId = decoder.decodeBoolForKey("ti", orElse: false) + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeBool(self.useTouchId, forKey: "ti") + } + + public func isEqual(to: PreferencesEntry) -> Bool { + if let to = to as? AdditionalSettings { + return self == to + } else { + return false + } + } + + public static func ==(lhs: AdditionalSettings, rhs: AdditionalSettings) -> Bool { + return lhs.useTouchId == rhs.useTouchId + } + + func withUpdatedTouchId(_ useTouchId: Bool) -> AdditionalSettings { + return AdditionalSettings(useTouchId: useTouchId) + } +} + +func updateAdditionalSettingsInteractively(accountManager: AccountManager, _ f: @escaping (AdditionalSettings) -> AdditionalSettings) -> Signal { + return accountManager.transaction { transaction -> Void in + transaction.updateSharedData(ApplicationSharedPreferencesKeys.additionalSettings, { entry in + let currentSettings: AdditionalSettings + if let entry = entry as? AdditionalSettings { + currentSettings = entry + } else { + currentSettings = AdditionalSettings.defaultSettings + } + return f(currentSettings) + }) + } +} + +func additionalSettings(accountManager: AccountManager) -> Signal { + return accountManager.sharedData(keys: [ApplicationSharedPreferencesKeys.additionalSettings]) |> map { view in + return (view.entries[ApplicationSharedPreferencesKeys.additionalSettings] as? AdditionalSettings) ?? AdditionalSettings.defaultSettings + } +} + diff --git a/Telegram-Mac/Alpha.xcconfig b/Telegram-Mac/Alpha.xcconfig new file mode 100644 index 0000000000..54a91e80d5 --- /dev/null +++ b/Telegram-Mac/Alpha.xcconfig @@ -0,0 +1,18 @@ +// +// Alpha.xcconfig +// Telegram +// +// Created by Mikhail Filimonov on 13/06/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 + + +DSA_PEM_FILE = dsa_pub.pem + +SIMPLE_SLASH=/ + +SFEED_URL = https:${SIMPLE_SLASH}/api.appcenter.ms/v0.1/public/sparkle/apps/f012091f-35d9-47bb-b3db-9cbd3b0232d3 +APPCENTER_SECRET = f012091f-35d9-47bb-b3db-9cbd3b0232d3 diff --git a/Telegram-Mac/AnimatedBadgeView.swift b/Telegram-Mac/AnimatedBadgeView.swift new file mode 100644 index 0000000000..352aa475c2 --- /dev/null +++ b/Telegram-Mac/AnimatedBadgeView.swift @@ -0,0 +1,50 @@ +// +// AnimatedBadgeView.swift +// Telegram +// +// Created by Mikhail Filimonov on 30.12.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit + + +final class AnimatedBadgeView : View { + + private let textView: DynamicCounterTextView = DynamicCounterTextView() + + override init() { + super.init(frame: .zero) + addSubview(textView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + func update(dynamicValue: DynamicCounterTextView.Value, backgroundColor: NSColor, animated: Bool, frame: NSRect) { + + textView.update(dynamicValue, animated: animated) + + let textFrame = frame.focus(dynamicValue.size) + + + self.change(size: frame.size, animated: animated) + self.change(pos: frame.origin, animated: animated) + textView.change(size: textFrame.size, animated: animated) + textView.change(pos: textFrame.origin, animated: animated) + + self.backgroundColor = backgroundColor + if animated { + layer?.animateBackground() + } + + layer?.cornerRadius = frame.height / 2 + } +} diff --git a/Telegram-Mac/AnimatedStickerUtils.swift b/Telegram-Mac/AnimatedStickerUtils.swift new file mode 100644 index 0000000000..c84ccb222d --- /dev/null +++ b/Telegram-Mac/AnimatedStickerUtils.swift @@ -0,0 +1,56 @@ +// +// AnimatedStickerUtils.swift +// Telegram +// +// Created by Mikhail Filimonov on 27/05/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import Foundation +import SwiftSignalKit +import AVFoundation +import TGUIKit + +private func verifyLottieItems(_ items: [Any]?, shapes: Bool = true) -> Bool { + if let items = items { + for case let item as [AnyHashable: Any] in items { + if let type = item["ty"] as? String { + if type == "rp" || type == "sr" || type == "mm" || type == "gs" { + return false + } + } + + if shapes, let subitems = item["it"] as? [Any] { + if !verifyLottieItems(subitems, shapes: false) { + return false + } + } + } + } + return true; +} + +private func verifyLottieLayers(_ layers: [AnyHashable: Any]?) -> Bool { + return true +} + +func validateStickerComposition(json: [AnyHashable: Any]) -> Bool { + guard let tgs = json["tgs"] as? Int, tgs == 1 else { + return false + } + + return true +} + +private let writeQueue = DispatchQueue(label: "assetWriterQueue") + + +private func fillPixelBufferFromImage(_ image: CGImage, pixelBuffer: CVPixelBuffer) { + CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) + let pixelData = CVPixelBufferGetBaseAddress(pixelBuffer) + let rgbColorSpace = CGColorSpaceCreateDeviceRGB() + let context = CGContext(data: pixelData, width: Int(image.size.width), height: Int(image.size.height), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer), space: rgbColorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue) + context?.draw(image, in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)) + CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) +} diff --git a/Telegram-Mac/AnimtedStickerHeaderItem.swift b/Telegram-Mac/AnimtedStickerHeaderItem.swift new file mode 100644 index 0000000000..e3a57de3e1 --- /dev/null +++ b/Telegram-Mac/AnimtedStickerHeaderItem.swift @@ -0,0 +1,85 @@ +// +// DiscussionHeaderItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 23/05/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import TGUIKit + + +class AnimtedStickerHeaderItem: GeneralRowItem { + fileprivate let context: AccountContext + fileprivate let textLayout: TextViewLayout + fileprivate let sticker: LocalAnimatedSticker + init(_ initialSize: NSSize, stableId: AnyHashable, context: AccountContext, sticker: LocalAnimatedSticker, text: NSAttributedString) { + self.context = context + self.sticker = sticker + self.textLayout = TextViewLayout(text, alignment: .center, alwaysStaticItems: true) + super.init(initialSize, stableId: stableId, inset: NSEdgeInsets(left: 30.0, right: 30.0, top: 0, bottom: 10)) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { + textLayout.measure(width: width - inset.left - inset.right) + return super.makeSize(width, oldWidth: oldWidth) + } + + override func viewClass() -> AnyClass { + return AnimtedStickerHeaderView.self + } + + override var height: CGFloat { + return inset.top + inset.bottom + 160 + inset.top + textLayout.layoutSize.height + } +} + + +private final class AnimtedStickerHeaderView : TableRowView { + private let imageView: MediaAnimatedStickerView = MediaAnimatedStickerView(frame: .zero) + private let textView: TextView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(imageView) + addSubview(textView) + + textView.isSelectable = false + textView.userInteractionEnabled = false + } + + override var backdorColor: NSColor { + return theme.colors.listBackground + } + + override func updateColors() { + super.updateColors() + textView.backgroundColor = backdorColor + } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + + guard let item = item as? AnimtedStickerHeaderItem else { return } + + imageView.update(with: item.sticker.file, size: NSMakeSize(160, 160), context: item.context, parent: nil, table: item.table, parameters: item.sticker.parameters, animated: animated, positionFlags: nil, approximateSynchronousValue: false) + +// self.imageView.image = item.icon +// self.imageView.sizeToFit() + + self.textView.update(item.textLayout) + + needsLayout = true + } + + override func layout() { + super.layout() + guard let item = item as? AnimtedStickerHeaderItem else { return } + + self.imageView.centerX(y: item.inset.top) + self.textView.centerX(y: self.imageView.frame.maxY + item.inset.bottom) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/AppAppearanceViewController.swift b/Telegram-Mac/AppAppearanceViewController.swift new file mode 100644 index 0000000000..c335076c13 --- /dev/null +++ b/Telegram-Mac/AppAppearanceViewController.swift @@ -0,0 +1,507 @@ +// +// AppAppearanceViewController.swift +// Telegram +// +// Created by Mikhail Filimonov on 14/09/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import SwiftSignalKit +import Postbox +import TGUIKit + + + +struct AppearanceAccentColor : Equatable { + let accent: PaletteAccentColor + let cloudTheme: TelegramTheme? + let cachedTheme: InstallCloudThemeCachedData? + init(accent: PaletteAccentColor, cloudTheme: TelegramTheme?, cachedTheme: InstallCloudThemeCachedData? = nil) { + self.accent = accent + self.cloudTheme = cloudTheme + self.cachedTheme = cachedTheme + } + func withUpdatedCachedTheme(_ cachedTheme: InstallCloudThemeCachedData?) -> AppearanceAccentColor { + return .init(accent: self.accent, cloudTheme: self.cloudTheme, cachedTheme: cachedTheme) + } +} + +enum ThemeSettingsEntryTag: ItemListItemTag { + case fontSize + case theme + case autoNight + case chatMode + case accentColor + func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? ThemeSettingsEntryTag, self == other { + return true + } else { + return false + } + } + + var stableId: InputDataEntryId { + switch self { + case .fontSize: + return .custom(_id_theme_text_size) + case .theme: + return .custom(_id_theme_list) + case .autoNight: + return .general(_id_theme_auto_night) + case .chatMode: + return .general(_id_theme_chat_mode) + case .accentColor: + return .custom(_id_theme_accent_list) + } + } +} + + +struct InstallCloudThemeCachedData : Equatable { + let palette: ColorPalette + let wallpaper: Wallpaper + let cloudWallpaper: TelegramWallpaper? +} +enum InstallThemeSource : Equatable { + case local(ColorPalette) + case cloud(TelegramTheme, InstallCloudThemeCachedData?) +} + +private final class AppAppearanceViewArguments { + let context: AccountContext + let togglePalette:(InstallThemeSource)->Void + let toggleBubbles:(Bool)->Void + let toggleFontSize:(CGFloat)->Void + let selectAccentColor:(AppearanceAccentColor?)->Void + let selectChatBackground:()->Void + let openAutoNightSettings:()->Void + let removeTheme:(TelegramTheme)->Void + let editTheme:(TelegramTheme)->Void + let shareTheme:(TelegramTheme)->Void + let shareLocal:(ColorPalette)->Void + init(context: AccountContext, togglePalette: @escaping(InstallThemeSource)->Void, toggleBubbles: @escaping(Bool)->Void, toggleFontSize: @escaping(CGFloat)->Void, selectAccentColor: @escaping(AppearanceAccentColor?)->Void, selectChatBackground:@escaping()->Void, openAutoNightSettings:@escaping()->Void, removeTheme:@escaping(TelegramTheme)->Void, editTheme: @escaping(TelegramTheme)->Void, shareTheme:@escaping(TelegramTheme)->Void, shareLocal:@escaping(ColorPalette)->Void) { + self.context = context + self.togglePalette = togglePalette + self.toggleBubbles = toggleBubbles + self.toggleFontSize = toggleFontSize + self.selectAccentColor = selectAccentColor + self.selectChatBackground = selectChatBackground + self.openAutoNightSettings = openAutoNightSettings + self.removeTheme = removeTheme + self.editTheme = editTheme + self.shareTheme = shareTheme + self.shareLocal = shareLocal + } +} + + +private let _id_theme_preview = InputDataIdentifier("_id_theme_preview") +private let _id_theme_list = InputDataIdentifier("_id_theme_list") +private let _id_theme_accent_list = InputDataIdentifier("_id_theme_accent_list") +private let _id_theme_chat_mode = InputDataIdentifier("_id_theme_chat_mode") +private let _id_theme_wallpaper1 = InputDataIdentifier("_id_theme_wallpaper") +private let _id_theme_wallpaper2 = InputDataIdentifier("_id_theme_wallpaper") +private let _id_theme_text_size = InputDataIdentifier("_id_theme_text_size") +private let _id_theme_auto_night = InputDataIdentifier("_id_theme_auto_night") + +private func appAppearanceEntries(appearance: Appearance, settings: ThemePaletteSettings, cloudThemes: [TelegramTheme], autoNightSettings: AutoNightThemePreferences, arguments: AppAppearanceViewArguments) -> [InputDataEntry] { + + var entries:[InputDataEntry] = [] + var sectionId: Int32 = 0 + var index:Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.appearanceSettingsColorThemeHeader), data: .init(viewType: .textTopItem))) + index += 1 + + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_theme_preview, equatable: InputDataEquatable(appearance), comparable: nil, item: { initialSize, stableId in + return ThemePreviewRowItem(initialSize, stableId: stableId, context: arguments.context, theme: appearance.presentation, viewType: .firstItem) + })) + + var accentList = appearance.presentation.cloudTheme == nil || appearance.presentation.cloudTheme?.settings != nil ? appearance.presentation.colors.accentList.map { AppearanceAccentColor(accent: $0, cloudTheme: nil) } : [] + + var cloudThemes = cloudThemes + if let cloud = appearance.presentation.cloudTheme { + if !cloudThemes.contains(where: {$0.id == cloud.id}) { + cloudThemes.append(cloud) + } + } + if appearance.presentation.cloudTheme == nil || appearance.presentation.cloudTheme?.settings != nil { + let copy = cloudThemes + var cloudAccents:[AppearanceAccentColor] = [] + for cloudTheme in copy { + if let settings = cloudTheme.settings, settings.palette.parent == appearance.presentation.colors.parent { + cloudAccents.append(AppearanceAccentColor(accent: settings.accent, cloudTheme: cloudTheme)) + } + } + accentList.insert(contentsOf: cloudAccents, at: 0) + } + + cloudThemes.removeAll(where:{ $0.settings != nil }) + + struct ListEquatable : Equatable { + let theme: TelegramPresentationTheme + let cloudThemes:[TelegramTheme] + } + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_theme_list, equatable: InputDataEquatable(ListEquatable(theme: appearance.presentation, cloudThemes: cloudThemes)), comparable: nil, item: { initialSize, stableId in + + let selected: ThemeSource + if let cloud = appearance.presentation.cloudTheme { + if let _ = cloud.settings { + selected = .local(appearance.presentation.colors, cloud) + } else { + selected = .cloud(cloud) + } + } else { + selected = .local(appearance.presentation.colors, nil) + } + + let dayClassicCloud = settings.associated.first(where: { $0.local == dayClassicPalette.parent })?.cloud?.cloud + let dayCloud = settings.associated.first(where: { $0.local == whitePalette.parent })?.cloud?.cloud + let nightAccentCloud = settings.associated.first(where: { $0.local == nightAccentPalette.parent })?.cloud?.cloud + + var locals: [LocalPaletteWithReference] = [LocalPaletteWithReference(palette: dayClassicPalette, cloud: dayClassicCloud), + LocalPaletteWithReference(palette: whitePalette, cloud: dayCloud), + LocalPaletteWithReference(palette: nightAccentPalette, cloud: nightAccentCloud), + LocalPaletteWithReference(palette: systemPalette, cloud: nil)] + + for (i, local) in locals.enumerated() { + if let accent = settings.accents.first(where: { $0.name == local.palette.parent }), accent.color.accent != local.palette.basicAccent { + locals[i] = local.withAccentColor(accent.color) + } + } + + return ThemeListRowItem(initialSize, stableId: stableId, context: arguments.context, theme: appearance.presentation, selected: selected, local: locals, cloudThemes: cloudThemes, viewType: accentList.isEmpty ? .lastItem : .innerItem, togglePalette: arguments.togglePalette, menuItems: { source in + var items:[ContextMenuItem] = [] + var cloud: TelegramTheme? + + switch source { + case let .cloud(c): + cloud = c + case let .local(_, c): + cloud = c + } + + if let cloud = cloud { + if cloud.isCreator { + items.append(ContextMenuItem(L10n.appearanceThemeEdit, handler: { + arguments.editTheme(cloud) + })) + } + items.append(ContextMenuItem(L10n.appearanceThemeShare, handler: { + arguments.shareTheme(cloud) + })) + items.append(ContextMenuItem(L10n.appearanceThemeRemove, handler: { + arguments.removeTheme(cloud) + })) + } + + return items + }) + })) + + + if !accentList.isEmpty { + + struct ALEquatable : Equatable { + let accentList: [AppearanceAccentColor] + let theme: TelegramPresentationTheme + } + + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_theme_accent_list, equatable: InputDataEquatable(ALEquatable(accentList: accentList, theme: appearance.presentation)), comparable: nil, item: { initialSize, stableId in + return AccentColorRowItem(initialSize, stableId: stableId, context: arguments.context, list: accentList, isNative: true, theme: appearance.presentation, viewType: .lastItem, selectAccentColor: arguments.selectAccentColor, menuItems: { accent in + var items:[ContextMenuItem] = [] + if let cloud = accent.cloudTheme { + items.append(ContextMenuItem(L10n.appearanceThemeShare, handler: { + arguments.shareTheme(cloud) + })) + items.append(ContextMenuItem(L10n.appearanceThemeRemove, handler: { + arguments.removeTheme(cloud) + })) + } + return items + }) + })) + index += 1 + } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_theme_chat_mode, data: InputDataGeneralData(name: L10n.appearanceSettingsBubblesMode, color: appearance.presentation.colors.text, type: .switchable(appearance.presentation.bubbled), viewType: appearance.presentation.bubbled ? .firstItem : .singleItem, action: { + arguments.toggleBubbles(!appearance.presentation.bubbled) + }))) + index += 1 + + if appearance.presentation.bubbled { + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_theme_wallpaper1, data: InputDataGeneralData(name: L10n.generalSettingsChatBackground, color: appearance.presentation.colors.text, type: .next, viewType: .lastItem, action: arguments.selectChatBackground))) + index += 1 + } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.appearanceSettingsTextSizeHeader), data: .init(viewType: .textTopItem))) + index += 1 + + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_theme_text_size, equatable: InputDataEquatable(appearance), comparable: nil, item: { initialSize, stableId in + let sizes:[Int32] = [11, 12, 13, 14, 15, 16, 17, 18] + return SelectSizeRowItem(initialSize, stableId: stableId, current: Int32(appearance.presentation.fontSize), sizes: sizes, hasMarkers: true, viewType: .singleItem, selectAction: { index in + arguments.toggleFontSize(CGFloat(sizes[index])) + }) + })) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.appearanceSettingsAutoNightHeader), data: .init(viewType: .textTopItem))) + index += 1 + + let autoNightText: String + if autoNightSettings.systemBased { + autoNightText = L10n.autoNightSettingsSystemBased + } else if let _ = autoNightSettings.schedule { + autoNightText = L10n.autoNightSettingsScheduled + } else { + autoNightText = L10n.autoNightSettingsDisabled + } + + sectionId += 1 + + + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_theme_auto_night, data: InputDataGeneralData(name: L10n.appearanceSettingsAutoNight, color: appearance.presentation.colors.text, type: .nextContext(autoNightText), viewType: .singleItem, action: arguments.openAutoNightSettings))) + index += 1 + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func AppAppearanceViewController(context: AccountContext, focusOnItemTag: ThemeSettingsEntryTag? = nil) -> InputDataController { + + let applyCloudThemeDisposable = MetaDisposable() + let updateDisposable = MetaDisposable() + + + let applyTheme:(InstallThemeSource)->Void = { source in + switch source { + case let .local(palette): + updateDisposable.set(updateThemeInteractivetly(accountManager: context.sharedContext.accountManager, f: { settings in + var settings = settings + settings = settings.withUpdatedPalette(palette).withUpdatedCloudTheme(nil) + + let defaultTheme = DefaultTheme(local: palette.parent, cloud: nil) + if palette.isDark { + settings = settings.withUpdatedDefaultDark(defaultTheme) + } else { + settings = settings.withUpdatedDefaultDay(defaultTheme) + } + return settings.installDefaultWallpaper().installDefaultAccent().withUpdatedDefaultIsDark(palette.isDark).withSavedAssociatedTheme() + }).start()) + case let .cloud(cloud, cached): + if let cached = cached { + updateDisposable.set(updateThemeInteractivetly(accountManager: context.sharedContext.accountManager, f: { settings in + var settings = settings + settings = settings.withUpdatedPalette(cached.palette) + settings = settings.withUpdatedCloudTheme(cloud) + settings = settings.updateWallpaper { _ in + return ThemeWallpaper(wallpaper: cached.wallpaper, associated: AssociatedWallpaper(cloud: cached.cloudWallpaper, wallpaper: cached.wallpaper)) + } + let defaultTheme = DefaultTheme(local: settings.palette.parent, cloud: DefaultCloudTheme(cloud: cloud, palette: cached.palette, wallpaper: AssociatedWallpaper(cloud: cached.cloudWallpaper, wallpaper: cached.wallpaper))) + if cached.palette.isDark { + settings = settings.withUpdatedDefaultDark(defaultTheme) + } else { + settings = settings.withUpdatedDefaultDay(defaultTheme) + } + return settings + .saveDefaultWallpaper() + .withUpdatedDefaultIsDark(cached.palette.isDark) + .withSavedAssociatedTheme() + }).start()) + + applyCloudThemeDisposable.set(downloadAndApplyCloudTheme(context: context, theme: cloud, install: true).start()) + } else if cloud.file != nil || cloud.settings != nil { + applyCloudThemeDisposable.set(showModalProgress(signal: downloadAndApplyCloudTheme(context: context, theme: cloud, install: true), for: context.window).start()) + } else { + showEditThemeModalController(context: context, theme: cloud) + } + } + } + + + let arguments = AppAppearanceViewArguments(context: context, togglePalette: { source in + + let nightSettings = autoNightSettings(accountManager: context.sharedContext.accountManager) |> take(1) |> deliverOnMainQueue + + _ = nightSettings.start(next: { settings in + if settings.systemBased || settings.schedule != nil { + confirm(for: context.window, header: L10n.darkModeConfirmNightModeHeader, information: L10n.darkModeConfirmNightModeText, okTitle: L10n.darkModeConfirmNightModeOK, successHandler: { _ in + let disableNightMode = context.sharedContext.accountManager.transaction { transaction -> Void in + transaction.updateSharedData(ApplicationSharedPreferencesKeys.autoNight, { entry in + let settings: AutoNightThemePreferences = entry as? AutoNightThemePreferences ?? AutoNightThemePreferences.defaultSettings + return settings.withUpdatedSystemBased(false).withUpdatedSchedule(nil) + }) + } |> deliverOnMainQueue + _ = disableNightMode.start(next: { + applyTheme(source) + }) + }) + } else { + applyTheme(source) + } + }) + + + }, toggleBubbles: { value in + updateDisposable.set(updateThemeInteractivetly(accountManager: context.sharedContext.accountManager, f: { settings in + return settings.withUpdatedBubbled(value) + }).start()) + }, toggleFontSize: { value in + updateDisposable.set(updateThemeInteractivetly(accountManager: context.sharedContext.accountManager, f: { settings in + return settings.withUpdatedFontSize(value) + }).start()) + }, selectAccentColor: { value in + let updateColor:(AppearanceAccentColor)->Void = { color in + if let cloudTheme = color.cloudTheme { + applyTheme(.cloud(cloudTheme, color.cachedTheme)) + } else { + updateDisposable.set(updateThemeInteractivetly(accountManager: context.sharedContext.accountManager, f: { settings in + let clearPalette = settings.palette.withoutAccentColor() + var settings = settings + if color.accent.accent == settings.palette.basicAccent { + settings = settings.withUpdatedPalette(clearPalette) + } else { + settings = settings.withUpdatedPalette(clearPalette.withAccentColor(color.accent)) + } + + let defaultTheme = DefaultTheme(local: settings.palette.parent, cloud: nil) + if settings.palette.isDark { + settings = settings.withUpdatedDefaultDark(defaultTheme) + } else { + settings = settings.withUpdatedDefaultDay(defaultTheme) + } + + return settings.withUpdatedCloudTheme(nil).saveDefaultAccent(color: color.accent).installDefaultWallpaper().withSavedAssociatedTheme() + }).start()) + } + } + if let color = value { + updateColor(color) + } else { + showModal(with: CustomAccentColorModalController(context: context, updateColor: { accent in + updateColor(AppearanceAccentColor(accent: accent, cloudTheme: nil)) + }), for: context.window) + } + }, selectChatBackground: { + showModal(with: ChatWallpaperModalController(context), for: context.window) + }, openAutoNightSettings: { + context.sharedContext.bindings.rootNavigation().push(AutoNightSettingsController(context: context)) + }, removeTheme: { cloudTheme in + confirm(for: context.window, header: L10n.appearanceConfirmRemoveTitle, information: L10n.appearanceConfirmRemoveText, okTitle: L10n.appearanceConfirmRemoveOK, successHandler: { _ in + var signals:[Signal] = [] + if theme.cloudTheme?.id == cloudTheme.id { + signals.append(updateThemeInteractivetly(accountManager: context.sharedContext.accountManager, f: { settings in + var settings = settings.withUpdatedCloudTheme(nil) + .withUpdatedToDefault(dark: settings.defaultIsDark, onlyLocal: true) + let defaultTheme = DefaultTheme(local: settings.palette.parent, cloud: nil) + if settings.defaultIsDark { + settings = settings.withUpdatedDefaultDark(defaultTheme) + } else { + settings = settings.withUpdatedDefaultDay(defaultTheme) + } + return settings.withSavedAssociatedTheme() + + })) + } + signals.append(deleteThemeInteractively(account: context.account, accountManager: context.sharedContext.accountManager, theme: cloudTheme)) + updateDisposable.set(combineLatest(signals).start()) + }) + }, editTheme: { value in + showEditThemeModalController(context: context, theme: value) + }, shareTheme: { value in + showModal(with: ShareModalController(ShareLinkObject(context, link: "https://t.me/addtheme/\(value.slug)")), for: context.window) + }, shareLocal: { palette in + + }) + + let cloudThemes = telegramThemes(postbox: context.account.postbox, network: context.account.network, accountManager: context.sharedContext.accountManager) + let nightSettings = autoNightSettings(accountManager: context.sharedContext.accountManager) + + let signal:Signal = combineLatest(queue: prepareQueue, appearanceSignal, themeUnmodifiedSettings(accountManager: context.sharedContext.accountManager), cloudThemes, nightSettings) |> map { appearance, themeSettings, cloudThemes, autoNightSettings in + return appAppearanceEntries(appearance: appearance, settings: themeSettings, cloudThemes: cloudThemes.reversed(), autoNightSettings: autoNightSettings, arguments: arguments) + } + |> map { entries in + return InputDataSignalValue(entries: entries, animated: false) + } |> deliverOnMainQueue + + + let controller = InputDataController(dataSignal: signal, title: L10n.telegramAppearanceViewController, removeAfterDisappear:false, identifier: "app_appearance", customRightButton: { controller in + + let view = ImageBarView(controller: controller, theme.icons.chatActions) + + view.button.set(handler: { control in + var items:[SPopoverItem] = [] + if theme.colors.parent != .system { + items.append(SPopoverItem(L10n.appearanceNewTheme, { + showModal(with: NewThemeController(context: context, palette: theme.colors.withUpdatedWallpaper(theme.wallpaper.paletteWallpaper)), for: context.window) + })) + items.append(SPopoverItem(L10n.appearanceExportTheme, { + exportPalette(palette: theme.colors.withUpdatedName(theme.cloudTheme?.title ?? theme.colors.name).withUpdatedWallpaper(theme.wallpaper.paletteWallpaper)) + })) + if let cloudTheme = theme.cloudTheme { + items.append(SPopoverItem(L10n.appearanceThemeShare, { + showModal(with: ShareModalController(ShareLinkObject(context, link: "https://t.me/addtheme/\(cloudTheme.slug)")), for: context.window) + })) + } + + if theme.cloudTheme != nil || theme.colors.accent != theme.colors.basicAccent { + items.append(SPopoverItem(L10n.appearanceReset, { + _ = updateThemeInteractivetly(accountManager: context.sharedContext.accountManager, f: { settings in + var settings = settings + if settings.defaultIsDark { + settings = settings.withUpdatedDefaultDark(DefaultTheme(local: TelegramBuiltinTheme.nightAccent, cloud: nil)).saveDefaultAccent(color: PaletteAccentColor(nightAccentPalette.accent)) + } else { + settings = settings.withUpdatedDefaultDay(DefaultTheme(local: TelegramBuiltinTheme.dayClassic, cloud: nil)).saveDefaultAccent(color: PaletteAccentColor(dayClassicPalette.accent)) + } + + return settings.installDefaultAccent().withUpdatedCloudTheme(nil).updateWallpaper({ _ -> ThemeWallpaper in + return ThemeWallpaper(wallpaper: settings.palette.wallpaper.wallpaper, associated: nil) + }).installDefaultWallpaper() + }).start() + })) + } + + showPopover(for: control, with: SPopoverViewController(items: items), edge: .minX, inset: NSMakePoint(0,-50)) + } + }, for: .Click) + view.button.set(image: theme.icons.chatActions, for: .Normal) + view.button.set(image: theme.icons.chatActionsActive, for: .Highlight) + return view + + }) + + controller.updateRightBarView = { view in + if let view = view as? ImageBarView { + view.button.set(image: theme.icons.chatActions, for: .Normal) + view.button.set(image: theme.icons.chatActionsActive, for: .Highlight) + } + } + + controller.didLoaded = { controller, _ in + if let focusOnItemTag = focusOnItemTag { + controller.genericView.tableView.scroll(to: .center(id: focusOnItemTag.stableId, innerId: nil, animated: true, focus: .init(focus: true), inset: 0), inset: NSEdgeInsets()) + } + } + + return controller +} diff --git a/Telegram-Mac/AppDelegate.swift b/Telegram-Mac/AppDelegate.swift index dbb1b88312..c35d013385 100644 --- a/Telegram-Mac/AppDelegate.swift +++ b/Telegram-Mac/AppDelegate.swift @@ -1,29 +1,103 @@ import Cocoa +import FFMpegBinding +import SwiftSignalKit +import Postbox +import TelegramCore -import SwiftSignalKitMac -import PostboxMac -import TelegramCoreMac import TGUIKit import Quartz -import MtProtoKitMac +import MtProtoKit +import CoreServices +import LocalAuthentication +//import WalletCore +import OpenSSLEncryption +import CoreSpotlight #if !APP_STORE - import HockeySDK - import Sparkle +import AppCenter +import AppCenterCrashes #endif +let enableBetaFeatures = true + +private(set) var appDelegate: AppDelegate? + +#if !SHARE +extension Account { + var diceCache: DiceCache? { + return appDelegate?.contextValue?.context.diceCache + } +} +#endif + + + +private struct AutologinToken : Equatable { + + + private let token: String + private let domains:[String] + + fileprivate init(token: String, domains: [String]) { + self.token = token + self.domains = domains + } + + static func with(appConfiguration: AppConfiguration) -> AutologinToken? { + if let data = appConfiguration.data, let value = data["autologin_token"] as? String { + let dict:[String] = data["autologin_domains"] as? [String] ?? [] + return AutologinToken(token: value, domains: dict) + } else { + return nil + } + } + + func applyTo(_ link: String, isTestServer: Bool) -> String? { + let url = URL(string: link) + if let url = url, let host = url.host, domains.contains(host) { + var queryItems = [URLQueryItem(name: "autologin_token", value: self.token)] + if isTestServer { + queryItems.append(URLQueryItem(name: "_test", value: "1")) + } + var urlComps = URLComponents(string: link)! + urlComps.queryItems = (urlComps.queryItems ?? []) + queryItems + return urlComps.url?.absoluteString + } + return nil + } +} + + +private final class SharedApplicationContext { + let sharedContext: SharedAccountContext + let notificationManager: SharedNotificationManager + let sharedWakeupManager: SharedWakeupManager + init(sharedContext: SharedAccountContext, notificationManager: SharedNotificationManager, sharedWakeupManager: SharedWakeupManager) { + self.sharedContext = sharedContext + self.notificationManager = notificationManager + self.sharedWakeupManager = sharedWakeupManager + } +} + @NSApplicationMain -class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDelegate { +class AppDelegate: NSResponder, NSApplicationDelegate, NSUserNotificationCenterDelegate, NSWindowDelegate { - #if !APP_STORE - @IBOutlet weak var updater: SUUpdater! - #endif + @IBOutlet weak var window: Window! { didSet { - window.initSaver() + window.delegate = self + window.isOpaque = true + let notInitial = window.initSaver() + + if !notInitial { + let size = NSMakeSize(700, 550) + if let screen = NSScreen.main { + window.setFrame(NSMakeRect((screen.frame.width - size.width) / 2, (screen.frame.height - size.height) / 2, size.width, size.height), display: true) + } + } } } @@ -32,209 +106,1013 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele NSAppleEventManager.shared().setEventHandler(self, andSelector: #selector(handleURLEvent(_: with:)), forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL)) } - /* - { - set { - - } - get { - // let path = "\(Bundle.main.bundlePath)/Contents/Frameworks/Sparkle.framework" - // return SUUpdater.init(for: Bundle.init(identifier: "")) - } - } - */ - + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + let presentAccountStatus = Promise(false) fileprivate let nofityDisposable:MetaDisposable = MetaDisposable() var containerUrl:String! - private let accountManagerPromise = Promise() - private var contextValue: ApplicationContext? - private let context = Promise() - private let contextDisposable = MetaDisposable() + private let sharedContextPromise = Promise() + private var sharedContextOnce: Signal { + return sharedContextPromise.get() |> take(1) |> deliverOnMainQueue + } + private var sharedApplicationContextValue: SharedApplicationContext? + + + var passlock: Signal { + return sharedContextPromise.get() |> mapToSignal { + return $0.notificationManager.passlocked + } + } + + fileprivate var contextValue: AuthorizedApplicationContext? + private let context = Promise() + + private var authContextValue: UnauthorizedApplicationContext? + private let authContext = Promise() + + private var activeValue: ValuePromise = ValuePromise(true, ignoreRepeated: true) + + var isActive: Signal { + return self.activeValue.get() + } + private let encryptionValue:Promise = Promise() + private let handleEventContextDisposable = MetaDisposable() + private let proxyDisposable = MetaDisposable() private var activity:Any? + private var executeUrlAfterLogin: String? = nil + private var timer: SwiftSignalKit.Timer? + + private(set) var appEncryption: AppEncryptionParameters! + func applicationWillFinishLaunching(_ notification: Notification) { + + } + + var baseAppBundleId: String { + return Bundle.main.bundleIdentifier! + } + func applicationDidFinishLaunching(_ aNotification: Notification) { + appDelegate = self + ApiEnvironment.migrate() + + initializeSelectManager() + startLottieCacheCleaner() - let appGroupName = "6N38VWS5BX.ru.keepcoder.Telegram" - guard let containerUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName) else { + if #available(OSX 10.12.2, *) { + NSApplication.shared.isAutomaticCustomizeTouchBarMenuItemEnabled = true + } + + + guard let containerUrl = ApiEnvironment.containerURL else { return } + + + self.containerUrl = containerUrl.path + + TempBox.initializeShared(basePath: self.containerUrl, processType: "app", launchSpecificId: arc4random64()) + + + let v = View() + v.flip = false + window.contentView = v + window.contentView?.autoresizingMask = [.width, .height] + window.contentView?.autoresizesSubviews = true + + let crashed = isCrashedLastTime(containerUrl.path) + deinitCrashHandler(containerUrl.path) + + if crashed { + let alert: NSAlert = NSAlert() + alert.addButton(withTitle: L10n.crashOnLaunchOK) + alert.addButton(withTitle: L10n.crashOnLaunchCancel) + alert.messageText = L10n.crashOnLaunchMessage + alert.informativeText = L10n.crashOnLaunchInformation + alert.alertStyle = .critical + if alert.runModal() == NSApplication.ModalResponse.alertFirstButtonReturn { + try? FileManager.default.removeItem(atPath: self.containerUrl) + } + } + + saveIntermediateDate() + uiLocalizationFunc = { key in return _NSLocalizedString(key) } DateUtils.setDateLocalizationFunc ({ key -> String in - return _NSLocalizedString(key) + return _NSLocalizedString(key!) }) + setInputLocalizationFunc { (key) -> String in + return _NSLocalizedString(key) + } - // applyMainMenuLocalization(window) - - mw = window - self.containerUrl = containerUrl.path + var paths: [String?] = [] + paths.append(Bundle.main.path(forResource: "opening", ofType:"m4a")) + paths.append(Bundle.main.path(forResource: "voip_busy", ofType:"caf")) + paths.append(Bundle.main.path(forResource: "voip_ringback", ofType:"caf")) + paths.append(Bundle.main.path(forResource: "voip_connecting", ofType:"mp3")) + paths.append(Bundle.main.path(forResource: "voip_fail", ofType:"caf")) + paths.append(Bundle.main.path(forResource: "voip_end", ofType:"caf")) + paths.append(Bundle.main.path(forResource: "sent", ofType:"caf")) + - #if !APP_STORE - self.updater.automaticallyChecksForUpdates = true - // self.updater.automaticallyDownloadsUpdates = false - self.updater.checkForUpdatesInBackground() - #endif + for path in paths { + if let path = path { + let player = try? AVAudioPlayer(contentsOf: URL(fileURLWithPath: path)) + player?.prepareToPlay() + } + } + FFMpegGlobals.initializeGlobals() - Timer.scheduledTimer(timeInterval: 60, target: self, selector: #selector(checkUpdates), userInfo: nil, repeats: true) + // applyMainMenuLocalization(window) + mw = window - for argument in CommandLine.arguments { - switch argument { - case "DEBUG_SESSION": - isDebug = true - default: - break + #if !APP_STORE + if let secret = Bundle.main.infoDictionary?["APPCENTER_SECRET"] as? String { + AppCenter.start(withAppSecret: secret, services: [Crashes.self]) } - } + #endif - if !isDebug { - #if BETA - - let hockeyAppId:String = "6ed2ac3049e1407387c2f1ffcb74e81f" - BITHockeyManager.shared().configure(withIdentifier: hockeyAppId) - BITHockeyManager.shared().crashManager.isAutoSubmitCrashReport = true - BITHockeyManager.shared().start() - - #endif -// -// #if STABLE  -// let hockeyAppId:String = "d77af558b21e0878953100680b5ac66a" -// BITHockeyManager.shared().configure(withIdentifier: hockeyAppId) -// BITHockeyManager.shared().crashManager.isAutoSubmitCrashReport = false -// #endif - - } + Timer.scheduledTimer(timeInterval: 10, target: self, selector: #selector(saveIntermediateDate), userInfo: nil, repeats: true) telegramUIDeclareEncodables() MTLogSetEnabled(UserDefaults.standard.bool(forKey: "enablelogs")) - let logger = Logger(basePath: containerUrl.path + "/logs") - logger.logToConsole = UserDefaults.standard.bool(forKey: "enablelogs") + let logger = Logger(rootPath: containerUrl.path, basePath: containerUrl.path + "/logs") + logger.logToConsole = false logger.logToFile = UserDefaults.standard.bool(forKey: "enablelogs") - #if APP_STORE || STABLE + #if DEBUG + MTLogSetEnabled(true) logger.logToConsole = false - MTLogSetEnabled(false) + logger.logToFile = true #endif - Logger.setSharedLogger(logger) + initializeMimeStore() - #if !APP_STORE - if let feedUrl = Bundle.main.infoDictionary?["SUFeedURL"] as? String, let url = URL(string: feedUrl) { - updater.feedURL = url - } - #endif +// #if APP_STORE || STABLE +// logger.logToConsole = false +// MTLogSetEnabled(false) +// #endif + Logger.setSharedLogger(logger) - + let bundleId = Bundle.main.bundleIdentifier if let bundleId = bundleId { LSSetDefaultHandlerForURLScheme("tg" as CFString, bundleId as CFString) } - - self.accountManagerPromise.set(accountManager(basePath: containerUrl.path + "/accounts-metadata")) + launchInterface() + + } + + + private func launchInterface() { + initializeAccountManagement() + + let rootPath = containerUrl! + let window = self.window! + _ = System.scaleFactor.swap(window.backingScaleFactor) + window.minSize = NSMakeSize(380, 500) + + let appEncryption = AppEncryptionParameters(path: rootPath) - let _ = (accountManagerPromise.get() - |> mapToSignal { manager in - return managedCleanupAccounts(networkArguments: NetworkInitializationArguments(apiId: API_ID, languagesCategory: languagesCategory), accountManager: manager, appGroupPath: containerUrl.path, auxiliaryMethods: telegramAccountAuxiliaryMethods) + let accountManager = AccountManager(basePath: containerUrl + "/accounts-metadata", isTemporary: false, isReadOnly: false) + + if let deviceSpecificEncryptionParameters = appEncryption.decrypt() { + let parameters = ValueBoxEncryptionParameters(forceEncryptionIfNoSet: true, key: ValueBoxEncryptionParameters.Key(data: deviceSpecificEncryptionParameters.key)!, salt: ValueBoxEncryptionParameters.Salt(data: deviceSpecificEncryptionParameters.salt)!) + self.launchApp(accountManager: accountManager, encryptionParameters: parameters, appEncryption: appEncryption) + } else { + + + let themeSemaphore = DispatchSemaphore(value: 0) + var themeSettings: ThemePaletteSettings = ThemePaletteSettings.defaultTheme + _ = (themeSettingsView(accountManager: accountManager) |> take(1)).start(next: { settings in + themeSettings = settings + themeSemaphore.signal() + }) + themeSemaphore.wait() + + var localization: LocalizationSettings? = nil + let localizationSemaphore = DispatchSemaphore(value: 0) + _ = (accountManager.transaction { transaction in + localization = transaction.getSharedData(SharedDataKeys.localizationSettings) as? LocalizationSettings + localizationSemaphore.signal() }).start() + localizationSemaphore.wait() + + if let localization = localization { + applyUILocalization(localization) + } + + updateTheme(with: themeSettings, for: window) + + self.window.makeKeyAndOrderFront(self) + NSApp.activate(ignoringOtherApps: true) + + showModal(with: ColdStartPasslockController(checkNextValue: { passcode in + appEncryption.applyPasscode(passcode) + if let params = appEncryption.decrypt() { + let parameters = ValueBoxEncryptionParameters(forceEncryptionIfNoSet: true, key: ValueBoxEncryptionParameters.Key(data: params.key)!, salt: ValueBoxEncryptionParameters.Salt(data: params.salt)!) + self.launchApp(accountManager: accountManager, encryptionParameters: parameters, appEncryption: appEncryption) + return true + } else { + return false + } + }, logoutImpl: { + return Signal { subscriber in + try? FileManager.default.removeItem(atPath: rootPath) + subscriber.putCompletion() + DispatchQueue.main.async { + let appEncryption = AppEncryptionParameters(path: rootPath) + let accountManager = AccountManager(basePath: self.containerUrl + "/accounts-metadata", isTemporary: false, isReadOnly: false) + if let params = appEncryption.decrypt() { + let parameters = ValueBoxEncryptionParameters(forceEncryptionIfNoSet: true, key: ValueBoxEncryptionParameters.Key(data: params.key)!, salt: ValueBoxEncryptionParameters.Salt(data: params.salt)!) + self.launchApp(accountManager: accountManager, encryptionParameters: parameters, appEncryption: appEncryption) + } + } + return EmptyDisposable + } |> runOn(prepareQueue) + }), for: window) + } + } + + private func launchApp(accountManager: AccountManager, encryptionParameters: ValueBoxEncryptionParameters, appEncryption: AppEncryptionParameters) { + self.appEncryption = appEncryption + let rootPath = containerUrl! + let window = self.window! + _ = System.scaleFactor.swap(window.backingScaleFactor) - self.context.set(self.accountManagerPromise.get() |> deliverOnMainQueue |> mapToSignal { accountManager -> Signal in - return applicationContext(window: self.window, shouldOnlineKeeper: self.presentAccountStatus.get(), accountManager: accountManager, appGroupPath: containerUrl.path, testingEnvironment: TEST_SERVER) - }) + window.minSize = NSMakeSize(380, 500) + + let networkDisposable = MetaDisposable() + + + let displayUpgrade:(Float?) -> Void = { progress in + if let progress = progress { + let view = HackUtils.findElements(byClass: "Telegram.OpmizeDatabaseView", in: self.window.contentView!).first as? OpmizeDatabaseView ?? OpmizeDatabaseView(frame: self.window.bounds) + view.setProgress(progress) + self.window.contentView?.addSubview(view, positioned: .below, relativeTo: self.window.contentView?.subviews.first) + self.window.makeKeyAndOrderFront(self) + } else { + (HackUtils.findElements(byClass: "Telegram.OpmizeDatabaseView", in: self.window.contentView!).first as? NSView)?.removeFromSuperview() + } + } + + + let _ = (upgradedAccounts(accountManager: accountManager, rootPath: rootPath, encryptionParameters: encryptionParameters) |> deliverOnMainQueue).start(next: { value in + if value > 0 { + displayUpgrade(value) + } else { + displayUpgrade(nil) + } + }, completed: { + + let passcodeSemaphore = DispatchSemaphore(value: 0) + + _ = accountManager.transaction { modifier -> Void in + let passcode = modifier.getAccessChallengeData() + + switch passcode { + case let .numericalPassword(value), let .plaintextPassword(value): + if !value.isEmpty { + appEncryption.change(value) + modifier.setAccessChallengeData(.plaintextPassword(value: "")) + } + default: + break + } + passcodeSemaphore.signal() + }.start() + passcodeSemaphore.wait() - self.contextDisposable.set(self.context.get().start(next: { context in - assert(Queue.mainQueue().isCurrent()) - self.window.makeKeyAndOrderFront(self) - self.contextValue = context - self.window.contentView?.removeAllSubviews() + + + let themeSemaphore = DispatchSemaphore(value: 0) + var themeSettings: ThemePaletteSettings = ThemePaletteSettings.defaultTheme + _ = (themeSettingsView(accountManager: accountManager) |> take(1)).start(next: { settings in + themeSettings = settings + themeSemaphore.signal() + }) + themeSemaphore.wait() + + + var localization: LocalizationSettings? = nil + let localizationSemaphore = DispatchSemaphore(value: 0) + _ = (accountManager.transaction { transaction in + localization = transaction.getSharedData(SharedDataKeys.localizationSettings) as? LocalizationSettings + localizationSemaphore.signal() + }).start() + localizationSemaphore.wait() + + if let localization = localization { + applyUILocalization(localization) + } + + updateTheme(with: themeSettings, for: window) + + + let basicTheme = Atomic(value: themeSettings) + let viewDidChangedAppearance: ValuePromise = ValuePromise(true) + let backingProperties:ValuePromise = ValuePromise(System.backingScale, ignoreRepeated: true) + + + var previousBackingScale = System.backingScale + _ = combineLatest(queue: .mainQueue(), themeSettingsView(accountManager: accountManager), backingProperties.get()).start(next: { settings, backingScale in + let previous = basicTheme.swap(settings) + if previous?.palette != settings.palette || previous?.bubbled != settings.bubbled || previous?.wallpaper != settings.wallpaper || previous?.fontSize != settings.fontSize || previousBackingScale != backingScale { + updateTheme(with: settings, for: window, animated: window.isKeyWindow && ((previous?.fontSize == settings.fontSize && previous?.palette != settings.palette) || previous?.bubbled != settings.bubbled || previous?.cloudTheme?.id != settings.cloudTheme?.id || previous?.palette.isDark != settings.palette.isDark)) + self.contextValue?.applyNewTheme() + } + previousBackingScale = backingScale + }) + + NotificationCenter.default.addObserver(forName: NSWindow.didChangeBackingPropertiesNotification, object: window, queue: nil, using: { notification in + backingProperties.set(System.backingScale) + }) + + let autoNightSignal = viewDidChangedAppearance.get() |> mapToSignal { _ in + return combineLatest(autoNightSettings(accountManager: accountManager), Signal.single(Void()) |> then( Signal.single(Void()) |> delay(60, queue: Queue.mainQueue()) |> restart)) + } |> deliverOnMainQueue + + + _ = autoNightSignal.start(next: { preference, _ in + + var isEnabled: Bool + var isDark: Bool = false + + if let schedule = preference.schedule { + + isEnabled = true + + let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + var now: time_t = time_t(nowTimestamp) + var timeinfoNow: tm = tm() + localtime_r(&now, &timeinfoNow) + let t = timeinfoNow.tm_hour * 60 * 60 + timeinfoNow.tm_min * 60 + timeinfoNow.tm_sec + + switch schedule { + case let .sunrise(coordinate): + if coordinate.latitude == 0 || coordinate.longitude == 0 { + isEnabled = theme.colors.isDark + } else { + if let sunrise = EDSunriseSet(date: Date(), timezone: NSTimeZone.local, latitude: coordinate.latitude, longitude: coordinate.longitude) { + let from = Int32(sunrise.sunset.timeIntervalSince1970 - sunrise.sunset.startOfDay.timeIntervalSince1970) + let to = Int32(sunrise.sunrise.timeIntervalSince1970 - sunrise.sunrise.startOfDay.timeIntervalSince1970) + isDark = to > from && t >= from && t <= to || to < from && (t >= from || t <= to) + } else { + isDark = false + } + } + case let .timeSensitive(from, to): + let from = from * 60 * 60 + let to = to * 60 * 60 + isDark = to > from && t >= from && t < to || to < from && (t >= from || t < to) + } + + } else if preference.systemBased { + isEnabled = true + if #available(OSX 10.14, *) { + switch systemAppearance.name { + case NSAppearance.Name.aqua: + isDark = false + case NSAppearance.Name.darkAqua: + isDark = true + default: + isDark = false + } + } else { + isDark = false + } + } else { + isEnabled = false + } + + _ = updateThemeInteractivetly(accountManager: accountManager, f: { settings -> ThemePaletteSettings in + var settings = settings + if isEnabled { + settings = settings.withUpdatedToDefault(dark: isDark) + if isDark { + if let theme = preference.theme.cloud { + settings = settings.withUpdatedCloudTheme(theme.cloud).withUpdatedPalette(theme.palette).updateWallpaper { current in + return ThemeWallpaper(wallpaper: theme.wallpaper.wallpaper, associated: theme.wallpaper) + } + } else { + settings = settings.withUpdatedPalette(preference.theme.local.palette).withUpdatedCloudTheme(nil).installDefaultWallpaper().installDefaultAccent() + } + } + } + return settings + }).start() + }) + + + let basicLocalization = Atomic(value: localization) + _ = (accountManager.sharedData(keys: [SharedDataKeys.localizationSettings]) |> deliverOnMainQueue).start(next: { view in + if let settings = view.entries[SharedDataKeys.localizationSettings] as? LocalizationSettings { + if basicLocalization.swap(settings) != settings { + applyUILocalization(settings) + } + } + }) + + + let voipVersions = OngoingCallContext.versions(includeExperimental: true, includeReference: false).map { version, supportsVideo -> CallSessionManagerImplementationVersion in + CallSessionManagerImplementationVersion(version: version, supportsVideo: supportsVideo) + } + + let networkArguments = NetworkInitializationArguments(apiId: ApiEnvironment.apiId, apiHash: ApiEnvironment.apiHash, languagesCategory: ApiEnvironment.language, appVersion: ApiEnvironment.version, voipMaxLayer: OngoingCallContext.maxLayer, voipVersions: voipVersions, appData: .single(ApiEnvironment.appData), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider()) + + let sharedContext = SharedAccountContext(accountManager: accountManager, networkArguments: networkArguments, rootPath: rootPath, encryptionParameters: encryptionParameters, appEncryption: appEncryption, displayUpgradeProgress: displayUpgrade) + + self.hangKeybind(sharedContext) + + + let rawAccounts = sharedContext.activeAccounts + |> map { _, accounts, _ -> [Account] in + return accounts.map({ $0.1 }) + } + let _ = (sharedAccountInfos(accountManager: sharedContext.accountManager, accounts: rawAccounts) + |> deliverOn(Queue())).start(next: { infos in + storeAccountsData(rootPath: rootPath, accounts: infos) + }) + + + let notificationsBindings = SharedNotificationBindings(navigateToChat: { account, peerId in + + if let contextValue = self.contextValue, contextValue.context.account.id == account.id { + let navigation = contextValue.context.sharedContext.bindings.rootNavigation() + + if let controller = navigation.controller as? ChatController { + if controller.chatInteraction.peerId == peerId { + controller.scrollup() + } else { + navigation.push(ChatAdditionController(context: contextValue.context, chatLocation: .peer(peerId))) + } + } else { + navigation.push(ChatController(context: contextValue.context, chatLocation: .peer(peerId))) + } + + } else { + sharedContext.switchToAccount(id: account.id, action: .chat(peerId, necessary: true)) + } + NSApp.activate(ignoringOtherApps: true) + window.deminiaturize(nil) + }, navigateToThread: { account, threadId, fromId in + if let contextValue = self.contextValue, contextValue.context.account.id == account.id { + + let pushController: (ChatLocation, ChatMode, MessageId, Atomic, Bool) -> Void = { chatLocation, mode, messageId, contextHolder, addition in + let navigation = contextValue.context.sharedContext.bindings.rootNavigation() + let controller: ChatController + if addition { + controller = ChatAdditionController(context: contextValue.context, chatLocation: chatLocation, mode: mode, messageId: messageId, initialAction: nil, chatLocationContextHolder: contextHolder) + } else { + controller = ChatController(context: contextValue.context, chatLocation: chatLocation, mode: mode, messageId: messageId, initialAction: nil, chatLocationContextHolder: contextHolder) + } + navigation.push(controller) + } + + let navigation = contextValue.context.sharedContext.bindings.rootNavigation() + + let currentInChat = navigation.controller is ChatController + let controller = navigation.controller as? ChatController + + if controller?.chatInteraction.mode.threadId == threadId { + controller?.scrollup() + } else { + + let signal:Signal = fetchAndPreloadReplyThreadInfo(context: contextValue.context, subject: .channelPost(threadId)) + + _ = showModalProgress(signal: signal |> take(1), for: contextValue.context.window).start(next: { result in + let chatLocation: ChatLocation = .replyThread(result.message) + + let updatedMode: ReplyThreadMode + if result.isChannelPost { + updatedMode = .comments(origin: fromId) + } else { + updatedMode = .replies(origin: fromId) + } + pushController(chatLocation, .replyThread(data: result.message, mode: updatedMode), fromId, result.contextHolder, currentInChat) + + }, error: { error in + + }) + } + + } else { + sharedContext.switchToAccount(id: account.id, action: .thread(threadId, fromId, necessary: true)) + } + NSApp.activate(ignoringOtherApps: true) + window.deminiaturize(nil) + }, updateCurrectController: { + if let contextValue = self.contextValue { + contextValue.context.sharedContext.bindings.rootNavigation().controller.updateController() + } + }, applyMaxReadIndexInteractively: { index in + if let context = self.contextValue?.context { + _ = context.engine.messages.applyMaxReadIndexInteractively(index: index).start() + } + + }) + + let sharedNotificationManager = SharedNotificationManager(activeAccounts: sharedContext.activeAccounts |> map { ($0.0, $0.1.map { ($0.0, $0.1) }) }, appEncryption: appEncryption, accountManager: accountManager, window: window, bindings: notificationsBindings) + let sharedWakeupManager = SharedWakeupManager(sharedContext: sharedContext, inForeground: self.presentAccountStatus.get()) + let sharedApplicationContext = SharedApplicationContext(sharedContext: sharedContext, notificationManager: sharedNotificationManager, sharedWakeupManager: sharedWakeupManager) + + self.sharedApplicationContextValue = sharedApplicationContext - context?.showRoot(for: self.window) + self.sharedContextPromise.set(accountManager.transaction { transaction -> (SharedApplicationContext, LoggingSettings) in + return (sharedApplicationContext, transaction.getSharedData(SharedDataKeys.loggingSettings) as? LoggingSettings ?? LoggingSettings.defaultSettings) + } + |> mapToSignal { sharedApplicationContext, loggingSettings -> Signal in + #if BETA || ALPHA + Logger.shared.logToFile = true + #else + Logger.shared.logToFile = loggingSettings.logToFile + #endif + Logger.shared.logToConsole = false//loggingSettings.logToConsole + Logger.shared.redactSensitiveData = true//loggingSettings.redactSensitiveData + return .single(sharedApplicationContext) + }) + + self.context.set(self.sharedContextPromise.get() + |> deliverOnMainQueue + |> mapToSignal { sharedApplicationContext -> Signal in + return sharedApplicationContext.sharedContext.activeAccounts + |> map { primary, _, _ -> Account? in + return primary + } + |> distinctUntilChanged(isEqual: { lhs, rhs in + if lhs !== rhs { + return false + } + return true + }) + |> map { account in + if let account = account { + var settings: LaunchSettings? + if let action = sharedContext.getLaunchActionOnce(for: account.id) { + settings = LaunchSettings(applyText: nil, previousText: nil, navigation: action, openAtLaunch: true) + } else { + let semaphore = DispatchSemaphore(value: 0) + _ = account.postbox.transaction { transaction in + settings = transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.launchSettings) as? LaunchSettings + semaphore.signal() + }.start() + semaphore.wait() + } + // let tonContext = StoredTonContext(basePath: account.basePath, postbox: account.postbox, network: account.network, keychain: tonKeychain) + + let context = AccountContext(sharedContext: sharedApplicationContext.sharedContext, window: window, account: account) + return AuthorizedApplicationContext(window: window, context: context, launchSettings: settings ?? LaunchSettings.defaultSettings, callSession: sharedContext.getCrossAccountCallSession(), groupCallContext: sharedContext.getCrossAccountGroupCall()) + + } else { + return nil + } + } + }) + + + self.authContext.set(self.sharedContextPromise.get() + |> deliverOnMainQueue + |> mapToSignal { sharedApplicationContext -> Signal in + return sharedApplicationContext.sharedContext.activeAccounts + |> map { primary, accounts, auth -> (Account?, UnauthorizedAccount, [Account])? in + if let auth = auth { + return (primary, auth, Array(accounts.map({ $0.1 }))) + } else { + return nil + } + } + |> distinctUntilChanged(isEqual: { lhs, rhs in + if lhs?.1 !== rhs?.1 { + return false + } + return true + }) + |> mapToSignal { authAndAccounts -> Signal<(UnauthorizedAccount, ((String, AccountRecordId, Bool)?, [(String, AccountRecordId, Bool)]))?, NoError> in + if let (primary, auth, accounts) = authAndAccounts { + let phoneNumbers = combineLatest(accounts.map { account -> Signal<(AccountRecordId, String, Bool)?, NoError> in + return account.postbox.transaction { transaction -> (AccountRecordId, String, Bool)? in + if let phone = (transaction.getPeer(account.peerId) as? TelegramUser)?.phone { + return (account.id, phone, account.testingEnvironment) + } else { + return nil + } + } + }) + return phoneNumbers + |> map { phoneNumbers -> (UnauthorizedAccount, ((String, AccountRecordId, Bool)?, [(String, AccountRecordId, Bool)]))? in + var primaryNumber: (String, AccountRecordId, Bool)? + if let primary = primary { + for idAndNumber in phoneNumbers { + if let (id, number, testingEnvironment) = idAndNumber, id == primary.id { + primaryNumber = (number, id, testingEnvironment) + break + } + } + } + return (auth, (primaryNumber, phoneNumbers.compactMap({ $0.flatMap({ ($0.1, $0.0, $0.2) }) }))) + } + } else { + return .single(nil) + } + } + |> mapToSignal { accountAndOtherAccountPhoneNumbers -> Signal<(UnauthorizedAccount, ((String, AccountRecordId, Bool)?, [(String, AccountRecordId, Bool)]))?, NoError> in + if let (account, otherAccountPhoneNumbers) = accountAndOtherAccountPhoneNumbers { + return .single((account, otherAccountPhoneNumbers)) + } else { + return .single(nil) + } + } + |> deliverOnMainQueue + |> mapToSignal { accountAndSettings -> Signal in + if let accountAndSettings = accountAndSettings { + return .single(UnauthorizedApplicationContext(window: window, sharedContext: sharedApplicationContext.sharedContext, account: accountAndSettings.0, otherAccountPhoneNumbers: accountAndSettings.1)) + } else { + return .single(nil) + } + } + }) + + + + + _ = (self.context.get() |> mapToSignal { context -> Signal in + if let context = context { + return context.ready |> map { [weak context] _ in + return context + } + } else { + return .single(nil) + } + + } |> deliverOnMainQueue).start(next: { context in + assert(Queue.mainQueue().isCurrent()) + + if let contextValue = self.contextValue { + contextValue.context.isCurrent = false + contextValue.rootView.removeFromSuperview() + } else if context == nil { + globalAudio?.stop() + } + + (HackUtils.findElements(byClass: "Telegram.OpmizeDatabaseView", in: self.window.contentView!).first as? NSView)?.removeFromSuperview() + + closeModal(ColdStartPasslockController.self) + closeAllPopovers(for: window) + + self.contextValue = context + + if let context = context { + context.context.isCurrent = true + context.applyNewTheme() + self.window.contentView?.addSubview(context.rootView, positioned: .below, relativeTo: self.window.contentView?.subviews.first) + + context.runLaunchAction() + if let executeUrlAfterLogin = self.executeUrlAfterLogin { + self.executeUrlAfterLogin = nil + execute(inapp: inApp(for: executeUrlAfterLogin.nsstring, context: context.context)) + } + #if !APP_STORE + networkDisposable.set((context.context.account.postbox.preferencesView(keys: [PreferencesKeys.networkSettings]) |> delay(5.0, queue: Queue.mainQueue()) |> deliverOnMainQueue).start(next: { settings in + let settings = settings.values[PreferencesKeys.networkSettings] as? NetworkSettings + + let applicationUpdateUrlPrefix: String? + if let prefix = settings?.applicationUpdateUrlPrefix { + if prefix.range(of: "://") == nil { + applicationUpdateUrlPrefix = "https://" + prefix + } else { + applicationUpdateUrlPrefix = prefix + } + } else { + applicationUpdateUrlPrefix = nil + } + setAppUpdaterBaseDomain(applicationUpdateUrlPrefix) + #if STABLE + updater_resetWithUpdaterSource(.internal(context: context.context)) + #else + updater_resetWithUpdaterSource(.external(context: context.context)) + #endif + + })) + #endif + + if let url = AppDelegate.eventProcessed { + self.processURL(url) + } + if let action = AppDelegate.spotlightAction { + self.processSpotlightAction(action) + } + + if !self.window.isKeyWindow { + self.window.makeKeyAndOrderFront(self) + } + self.window.deminiaturize(self) + NSApp.activate(ignoringOtherApps: true) + + + } + }) + + + var presentAuthAnimated: Bool = false + + let authContextReadyDisposable = MetaDisposable() + + _ = (self.authContext.get() + |> deliverOnMainQueue).start(next: { context in + + (HackUtils.findElements(byClass: "Telegram.OpmizeDatabaseView", in: self.window.contentView!).first as? NSView)?.removeFromSuperview() + + if let authContextValue = self.authContextValue { + authContextValue.account.shouldBeServiceTaskMaster.set(.single(.never)) + authContextValue.modal.close() + } + self.authContextValue = context + if let context = context { + let isReady: Signal = .single(true) + authContextReadyDisposable.set((isReady + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(next: { _ in + + window.makeKeyAndOrderFront(nil) + showModal(with: context.modal, for: window, animated: presentAuthAnimated) + + #if !APP_STORE + networkDisposable.set((context.account.postbox.preferencesView(keys: [PreferencesKeys.networkSettings]) |> delay(5.0, queue: Queue.mainQueue()) |> deliverOnMainQueue).start(next: { settings in + let settings = settings.values[PreferencesKeys.networkSettings] as? NetworkSettings + + let applicationUpdateUrlPrefix: String? + if let prefix = settings?.applicationUpdateUrlPrefix { + if prefix.range(of: "://") == nil { + applicationUpdateUrlPrefix = "https://" + prefix + } else { + applicationUpdateUrlPrefix = prefix + } + } else { + applicationUpdateUrlPrefix = nil + } + setAppUpdaterBaseDomain(applicationUpdateUrlPrefix) + #if STABLE + if let context = self.contextValue?.context { + updater_resetWithUpdaterSource(.internal(context: context)) + } else { + updater_resetWithUpdaterSource(.external(context: nil)) + } + #else + updater_resetWithUpdaterSource(.external(context: self.contextValue?.context)) + #endif + + })) + #endif + + + })) + } else { + presentAuthAnimated = true + authContextReadyDisposable.set(nil) + } + }) - })) + + + + + // + + + self.saveIntermediateDate() + + + if #available(OSX 10.14, *) { + DistributedNotificationCenter.default().addObserver(forName: Notification.Name("AppleInterfaceThemeChangedNotification"), object: nil, queue: nil, using: { _ in + delay(0.1, closure: { + forceUpdateStatusBarIconByDockTile(sharedContext: sharedContext) + viewDidChangedAppearance.set(true) + }) + }) + + (window.contentView as? View)?.viewDidChangedEffectiveAppearance = { + viewDidChangedAppearance.set(true) + } + } + + NotificationCenter.default.addObserver(self, selector: #selector(self.windiwDidChangeBackingProperties), name: NSWindow.didChangeBackingPropertiesNotification, object: window) + + self.window.contentView?.wantsLayer = true + + sharedWakeupManager.onSleepValueUpdated = { value in + self.updatePeerPresence() + } + sharedNotificationManager.didUpdateLocked = { value in + self.updatePeerPresence() + } + + self.timer = SwiftSignalKit.Timer(timeout: 5, repeat: true, completion: { + self.updatePeerPresence() + }, queue: .mainQueue()) + self.timer?.start() + }) - self.window.contentView?.wantsLayer = true + } + func navigateProfile(_ peerId: PeerId, account: Account) { + if let context = self.contextValue?.context, context.peerId == account.peerId { + context.sharedContext.bindings.rootNavigation().push(PeerInfoController(context: context, peerId: peerId)) + context.window.makeKeyAndOrderFront(nil) + context.window.orderFrontRegardless() + } else { + sharedApplicationContextValue?.sharedContext.switchToAccount(id: account.id, action: .profile(peerId, necessary: true)) + } + } + func navigateChat(_ peerId: PeerId, account: Account) { + if let context = self.contextValue?.context, context.peerId == account.peerId { + context.sharedContext.bindings.rootNavigation().push(ChatAdditionController.init(context: context, chatLocation: .peer(peerId))) + context.window.makeKeyAndOrderFront(nil) + context.window.orderFrontRegardless() + } else { + sharedApplicationContextValue?.sharedContext.switchToAccount(id: account.id, action: .chat(peerId, necessary: true)) + } + } + + + private func updatePeerPresence() { + if let sharedApplicationContextValue = sharedApplicationContextValue { + let isOnline = NSApp.isActive && NSApp.isRunning && !NSApp.isHidden && !sharedApplicationContextValue.sharedWakeupManager.isSleeping && !sharedApplicationContextValue.notificationManager._lockedValue.screenLock && !sharedApplicationContextValue.notificationManager._lockedValue.passcodeLock && SystemIdleTime() < 30 + + + + #if DEBUG + NSLog("accountIsOnline: \(isOnline)") + #endif + presentAccountStatus.set(.single(isOnline) |> then(.single(isOnline) |> delay(50, queue: Queue.concurrentBackgroundQueue())) |> restart) + } + } + + @objc public func windiwDidChangeBackingProperties() { + _ = System.scaleFactor.swap(window.backingScaleFactor) + } + + func playSound(_ sound: String) { + if let context = self.contextValue?.context { + SoundEffectPlay.play(postbox: context.account.postbox, name: sound, type: "m4a", volume: 0.7) + } + } + + + @IBAction func checkForUpdates(_ sender: Any) { + #if !APP_STORE + showModal(with: InputDataModalController(AppUpdateViewController()), for: window) + #if STABLE + if let context = self.contextValue?.context { + updater_resetWithUpdaterSource(.internal(context: context)) + } else { + updater_resetWithUpdaterSource(.external(context: nil)) + } + #else + updater_resetWithUpdaterSource(.external(context: self.contextValue?.context)) + #endif + #endif + } + + override func awakeFromNib() { + #if APP_STORE + if let menu = NSApp.mainMenu?.item(at: 0)?.submenu, let sparkleItem = menu.item(withTag: 1000) { + menu.removeItem(sparkleItem) + } + #endif } + @objc func checkUpdates() { #if !APP_STORE - updater.checkForUpdatesInBackground() + showModal(with: InputDataModalController(AppUpdateViewController()), for: window) #endif } - private static var eventProcessed: Bool = false + + @objc func saveIntermediateDate() { + crashIntermediateDate(containerUrl) + } + + private static var eventProcessed: String? = nil + private static var spotlightAction: SpotlightIdentifier? = nil + @objc func handleURLEvent(_ event:NSAppleEventDescriptor, with replyEvent:NSAppleEventDescriptor) { - AppDelegate.eventProcessed = false let url = event.paramDescriptor(forKeyword: keyDirectObject)?.stringValue - self.handleEventContextDisposable.set((self.context.get()).start(next: { context in - if !AppDelegate.eventProcessed { - NSApp.activate(ignoringOtherApps: true) - self.window.deminiaturize(self) + processURL(url) + } + + private func processURL(_ url: String?) { + AppDelegate.eventProcessed = url + + if let url = AppDelegate.eventProcessed { + NSApp.activate(ignoringOtherApps: true) + self.window.deminiaturize(self) + if let context = self.contextValue?.context { + AppDelegate.eventProcessed = nil - if let url = url, let context = context { - switch context { - case let .authorized(context): - AppDelegate.eventProcessed = true - let link = inApp(for: url as NSString, account: context.account, openInfo: { (peerId, isChat, postId, action) in - context.rightController.push(ChatController(account: context.account, peerId: peerId, messageId:postId, initialAction:action), true) - }, applyProxy: { proxy in - applyExternalProxy(proxy, postbox: context.account.postbox, network: context.account.network) - }) - - execute(inapp: link) - case .unauthorized(let context): - let settings = proxySettings(from: url) - if settings.1 { - AppDelegate.eventProcessed = true - if let proxy = settings.0 { - applyExternalProxy(proxy, postbox: context.account.postbox, network: context.account.network) - } else { - _ = applyProxySettings(postbox: context.account.postbox, network: context.account.network, settings: nil).start() + let link = inApp(for: url as NSString, context: context, openInfo: { (peerId, isChat, postId, action) in + context.sharedContext.bindings.rootNavigation().push(ChatController(context: context, chatLocation: .peer(peerId), messageId:postId, initialAction:action), true) + }, applyProxy: { proxy in + applyExternalProxy(proxy, accountManager: context.sharedContext.accountManager) + }) + execute(inapp: link) + } else if let authContext = self.authContextValue { + let settings = proxySettings(from: url) + if settings.1 { + AppDelegate.eventProcessed = nil + if let proxy = settings.0 { + applyExternalProxy(proxy, accountManager: authContext.sharedContext.accountManager) + } else { + _ = updateProxySettingsInteractively(accountManager: authContext.sharedContext.accountManager, { current -> ProxySettings in + return current.withUpdatedActiveServer(nil) + }).start() + } + } + + if url.range(of: legacyPassportUsername) != nil || url.range(of: "tg://passport") != nil { + alert(for: mainWindow, info: L10n.secureIdLoginText) + self.executeUrlAfterLogin = url + } + } + } + } + + private func hangKeybind(_ sharedContext: SharedAccountContext) { + let signal = combineLatest(queue: .mainQueue(), voiceCallSettings(sharedContext.accountManager), sharedContext.groupCallContext) + + _ = signal.start(next: { settings, activeCall in + if let pushToTalk = settings.pushToTalk, let _ = activeCall { + self.window.isPushToTalkEquaivalent = { event in + if !pushToTalk.modifierFlags.isEmpty, pushToTalk.keyCodes.contains(event.keyCode) { + for modifier in pushToTalk.modifierFlags { + if modifier.flag == event.modifierFlags.rawValue { + return true } } - default: - break } + return false } + } else { + self.window.isPushToTalkEquaivalent = nil } - })) + + }) + + + } + func window(_ window: NSWindow, willPositionSheet sheet: NSWindow, using rect: NSRect) -> NSRect { + var rect = rect + rect.origin.y -= 22 + return rect; + } + func applicationDidBecomeActive(_ notification: Notification) { - presentAccountStatus.set(.single(true) |> then(.single(true) |> delay(50, queue: Queue.concurrentBackgroundQueue())) |> restart) + updatePeerPresence() } - func applicationDidResignActive(_ notification: Notification) { - presentAccountStatus.set(.single(false)) + func tryApplyAutologinToken(_ url: String) -> String? { + if let config = contextValue?.context.appConfiguration { + if let value = AutologinToken.with(appConfiguration: config) { + return value.applyTo(url, isTestServer: contextValue?.context.account.testingEnvironment ?? false) + } + } + return nil } func applicationDidHide(_ notification: Notification) { - presentAccountStatus.set(.single(false)) + updatePeerPresence() } func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { @@ -242,7 +1120,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele } func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { - window.makeKeyAndOrderFront(sender) + if !self.window.isVisible { + self.window.makeKeyAndOrderFront(self) + self.window.orderFrontRegardless() + } + if viewer != nil { + viewer?.windowDidResignKey() + } return true } @@ -278,61 +1162,149 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele } } - - - override func awakeFromNib() { - #if APP_STORE - if let menu = NSApp.mainMenu?.item(at: 0)?.submenu, let sparkleItem = menu.item(withTag: 1000) { - menu.removeItem(sparkleItem) + func applicationWillUnhide(_ notification: Notification) { + window.makeKeyAndOrderFront(nil) + } + + func applicationWillBecomeActive(_ notification: Notification) { + if contextValue != nil { + if !self.window.isVisible { + self.window.makeKeyAndOrderFront(self) + self.window.orderFrontRegardless() } - #endif + if viewer != nil { + viewer?.windowDidResignKey() + } + self.activeValue.set(true) + + } } - @IBAction func checkForUpdates(_ sender: Any) { + + + + func applicationDidResignActive(_ notification: Notification) { + updatePeerPresence() + if viewer != nil { + viewer?.window.orderOut(nil) + } + self.activeValue.set(false) + } + + func applicationWillTerminate(_ notification: Notification) { + deinitCrashHandler(containerUrl) + #if !APP_STORE - updater.checkForUpdates(sender) + updateAppIfNeeded() #endif } + func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { + + if let context = self.contextValue?.context { + let navigation = context.sharedContext.bindings.rootNavigation() + (navigation.controller as? ChatController)?.chatInteraction.saveState(sync: true) + } + + return .terminateNow + } + + + func windowDidDeminiaturize(_ notification: Notification) { + window.orderOut(nil) + window.makeKeyAndOrderFront(nil) + } + + func windowDidMiniaturize(_ notification: Notification) { + window.resignMain() + } + + + var hasAuthorized: Bool { + return contextValue?.context != nil + } @IBAction func unhide(_ sender: Any) { window.makeKeyAndOrderFront(sender) } - // LocalizationWrapper.setLanguageCode("ru") @IBAction func aboutAction(_ sender: Any) { - window.makeKeyAndOrderFront(sender) showModal(with: AboutModalController(), for: window) + window.makeKeyAndOrderFront(sender) } @IBAction func preferencesAction(_ sender: Any) { + + if let context = contextValue?.context { + context.sharedContext.bindings.mainController().showPreferences() + } window.makeKeyAndOrderFront(sender) - if let context = self.contextValue { - switch context { - case let .authorized(appContext): - appContext.leftController.showPreferences() - if !(appContext.rightController.controller is GeneralSettingsViewController) { - appContext.rightController.push(GeneralSettingsViewController(appContext.account), false) - } - default: - break - } + + } + @IBAction func globalSearch(_ sender: Any) { + if let context = contextValue?.context { + context.sharedContext.bindings.mainController().focusSearch(animated: true) } } @IBAction func closeWindow(_ sender: Any) { NSApp.keyWindow?.close() } - @IBAction func showQuickSwitcher(_ sender: Any) { - window.makeKeyAndOrderFront(sender) - if let context = contextValue { - switch context { - case .authorized(let authorized): - if !authorized.isLocked { - showModal(with: QuickSwitcherModalController(account: authorized.account), for: mainWindow) + func application(_ application: NSApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([NSUserActivityRestoring]) -> Void) -> Bool { + if userActivity.activityType == CSSearchableItemActionType { + if let uniqueIdentifier = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String { + if let identifier = parseSpotlightIdentifier(uniqueIdentifier) { + self.processSpotlightAction(identifier) + } + } + } + + return true + } + + private func processSpotlightAction(_ identifier: SpotlightIdentifier) { + if let context = contextValue?.context { + AppDelegate.spotlightAction = nil + if context.account.id == identifier.recordId { + switch identifier.source { + case let .peerId(peerId): + context.sharedContext.bindings.rootNavigation().push(ChatController(context: context, chatLocation: .peer(peerId))) } - default: - break + } else { + switch identifier.source { + case let .peerId(peerId): + context.sharedContext.switchToAccount(id: identifier.recordId, action: .chat(peerId, necessary: true)) + } + } + } else { + AppDelegate.spotlightAction = identifier + } + + } + + func getLogFilesContentWithMaxSize() -> String { + + let semaphore = DispatchSemaphore(value: 0) + var result: String = "" + _ = Logger.shared.collectShortLog().start(next: { logs in + for log in logs.suffix(500) { + result += log.1 + "\n" } + semaphore.signal() + }) + semaphore.wait() + + return result + } + + @IBAction func showQuickSwitcher(_ sender: Any) { + + if let context = contextValue?.context, authContextValue == nil { + _ = sharedContextOnce.start(next: { applicationContext in + if !applicationContext.notificationManager.isLocked { + showModal(with: QuickSwitcherModalController(context), for: self.window) + } + }) } + window.makeKeyAndOrderFront(sender) } } diff --git a/Telegram-Mac/AppUpdateViewController.swift b/Telegram-Mac/AppUpdateViewController.swift new file mode 100644 index 0000000000..863e3af0c4 --- /dev/null +++ b/Telegram-Mac/AppUpdateViewController.swift @@ -0,0 +1,634 @@ +// +// AppUpdateViewController.swift +// Telegram +// +// Created by Mikhail Filimonov on 01/02/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +#if !APP_STORE + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit +import Sparkle + + + +final class TelegramUpdater : NSObject, SUUpdaterPrivate { + var delegate: SUUpdaterDelegate! + + var userAgentString: String! + + var domain: String! = nil + var host: String! = nil + + var httpHeaders: [AnyHashable : Any]! + + var decryptionPassword: String! + + var sparkleBundle: Bundle! + + override init() { + self.sparkleBundle = Bundle(for: SUUpdateDriver.self) + } +} + +extension SUAppcastItem { + var updateText: String { + var updateText = (itemDescription.html2Attributed?.string ?? itemDescription).replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression, range: nil) + while let range = updateText.range(of: " ") { + updateText = updateText.replacingOccurrences(of: " ", with: " ", options: [], range: range) + } + updateText = updateText.replacingOccurrences(of: "•", with: "\n•", options: [], range: nil) + + if updateText.first == "\n" { + updateText.removeFirst() + } + updateText = updateText.replacingOccurrences(of: "\t", with: " ", options: [], range: nil) + return updateText + } + + var versionTitle: String { + return "Version \(self.displayVersionString!) (\(self.versionString!))" + } +} + + + +enum AppUpdateLoadingState : Equatable { + case initializing + case loading(item:SUAppcastItem, current: Int, total: Int) + case hasUpdate(SUAppcastItem) + case readyToInstall(SUAppcastItem) + case uptodate + case unarchiving(SUAppcastItem) + case installing + case failed(NSError) +} + +private let initialState = AppUpdateState(items: [], loadingState: .initializing) +private let statePromise: ValuePromise = ValuePromise(initialState, ignoreRepeated: true) +private let stateValue = Atomic(value: initialState) + +var appUpdateStateSignal: Signal { + return statePromise.get() +} + +private let updateState:((AppUpdateState)->AppUpdateState) -> Void = { f in + statePromise.set(stateValue.modify(f)) +} +private let updater = TelegramUpdater() +private var driver:SUBasicUpdateDriver? +private let host = SUHost(bundle: Bundle.main) + +func updateApplication(sharedContext: SharedAccountContext) { + + + let state = stateValue.with {$0.loadingState} + switch state { + case let .readyToInstall(item): + var text: String = "Telegram was updated to \(item.versionTitle.lowercased())" + text += "\n\n" + + text += item.updateText + + + _ = (sharedContext.activeAccountsWithInfo |> take(1) |> mapToSignal { _, accounts -> Signal in + return combineLatest(accounts.map { addAppUpdateText($0.account.postbox, applyText: text) }) |> ignoreValues + } |> deliverOnMainQueue).start(completed: { + driver?.install(withToolAndRelaunch: true) + + }) + + case .installing: + break + default: + resetUpdater() + } +} + + +struct AppUpdateState : Equatable { + let items: [SUAppcastItem] + let loadingState: AppUpdateLoadingState + + fileprivate init(items: [SUAppcastItem], loadingState: AppUpdateLoadingState) { + self.items = items + self.loadingState = loadingState + + } + func withUpdatedItems(_ items: [SUAppcastItem]) -> AppUpdateState { + return AppUpdateState(items: items, loadingState: self.loadingState) + } + func withUpdatedLoadingState(_ loadingState: AppUpdateLoadingState) -> AppUpdateState { + return AppUpdateState(items: self.items, loadingState: loadingState) + } +} + +extension String{ + var html2Attributed: NSAttributedString? { + do { + guard let data = data(using: String.Encoding.utf8) else { + return nil + } + return try NSAttributedString(data: data, + options: [.documentType: NSAttributedString.DocumentType.html, + .characterEncoding: String.Encoding.utf8.rawValue], + documentAttributes: nil) + } catch { + print("error: ", error) + return nil + } + } +} + +private let _id_update_app: InputDataIdentifier = InputDataIdentifier("_id_update_app") +private let _id_initializing: InputDataIdentifier = InputDataIdentifier("_id_initializing") +private let _id_downloading: InputDataIdentifier = InputDataIdentifier("_id_downloading") +private let _id_download_update: InputDataIdentifier = InputDataIdentifier("_id_download_update") +private let _id_install_update: InputDataIdentifier = InputDataIdentifier("_id_install_update") +private let _id_check_for_updates: InputDataIdentifier = InputDataIdentifier("_id_check_for_updates") +private let _id_unarchiving: InputDataIdentifier = InputDataIdentifier("_id_unarchiving") + +private func appUpdateEntries(state: AppUpdateState) -> [InputDataEntry] { + var entries: [InputDataEntry] = [] + + var sectionId: Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + var currentItem: SUAppcastItem? + + switch state.loadingState { + case let .failed(error): + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_check_for_updates, data: InputDataGeneralData(name: L10n.appUpdateCheckForUpdates, color: theme.colors.accent, icon: nil, type: .none, viewType: .singleItem, action: nil))) + index += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(error.localizedDescription), data: InputDataGeneralTextData(color: theme.colors.redUI, detectBold: false, viewType: .textBottomItem))) + index += 1 + + case let .hasUpdate(item): + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_download_update, data: InputDataGeneralData(name: L10n.appUpdateDownloadUpdate, color: theme.colors.accent, icon: nil, type: .none, viewType: .singleItem, action: nil))) + index += 1 + + currentItem = item + case .initializing: + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_initializing, data: InputDataGeneralData(name: L10n.appUpdateRetrievingInfo, color: theme.colors.grayText, icon: nil, type: .none, viewType: .singleItem, action: nil))) + index += 1 + case let .loading(item, current, total): + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_downloading, data: InputDataGeneralData(name: "\(L10n.appUpdateDownloading) \(String.prettySized(with: current) + " / " + String.prettySized(with: total))", color: theme.colors.grayText, icon: nil, type: .none, viewType: .singleItem, action: nil))) + index += 1 + + currentItem = item + case .uptodate: + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_check_for_updates, data: InputDataGeneralData(name: L10n.appUpdateCheckForUpdates, color: theme.colors.accent, icon: nil, type: .none, viewType: .singleItem, action: nil))) + index += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.appUpdateUptodate), data: InputDataGeneralTextData(detectBold: false, viewType: .textBottomItem))) + index += 1 + case let .unarchiving(item): + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_unarchiving, data: InputDataGeneralData(name: L10n.appUpdateUnarchiving, color: theme.colors.grayText, icon: nil, type: .none, viewType: .singleItem, action: nil))) + index += 1 + + currentItem = item + case let .readyToInstall(item): + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_install_update, data: InputDataGeneralData(name: L10n.updateUpdateTelegram, color: theme.colors.accent, icon: nil, type: .none, viewType: .singleItem, action: nil))) + index += 1 + + currentItem = item + case .installing: + break + } + + + if let item = currentItem { + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.appUpdateNewestAvailable), data: InputDataGeneralTextData(detectBold: false, viewType: .textTopItem))) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + let text = "**" + item.versionTitle + "**" + "\n" + item.updateText + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier(item.fileURL.path), equatable: nil, comparable: nil, item: { initialSize, stableId in + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, textColor: theme.colors.listGrayText, fontSize: 13, isTextSelectable: true, viewType: .textTopItem) + })) + index += 1 + } + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + return entries +} + + + +func AppUpdateViewController() -> InputDataController { + + let signal: Signal = statePromise.get() |> deliverOnResourceQueue |> map { value in + return appUpdateEntries(state: value) + } |> map { InputDataSignalValue(entries: $0) } + + + return InputDataController(dataSignal: signal, title: L10n.appUpdateTitle, validateData: { data in + + if let _ = data[_id_download_update] { + driver?.downloadUpdate() + } + if let _ = data[_id_check_for_updates] { + resetUpdater() + } + if let _ = data[_id_install_update] { + driver?.install(withToolAndRelaunch: true) + } + + return .none + }, afterDisappear: { + + }, hasDone: false, identifier: "app_update") + + +} + +private let updates_channel_xml = "macos_stable_updates_xml" + + + +private final class InternalUpdaterDownloader : SPUDownloaderSession { + private let context: AccountContext + private let updateItem: SUAppcastItem + private let disposable = MetaDisposable() + init(context: AccountContext, updateItem: SUAppcastItem, delegate: SPUDownloaderDelegate) { + self.context = context + self.updateItem = updateItem + super.init(delegate: delegate) + } + + deinit { + disposable.dispose() + } + + override func suggestedFilename() -> String! { + return "Telegram.app.zip" + } + + + + override func moveItem(atPath fromPath: String!, toPath: String!, error: Error) -> Bool { + try? FileManager.default.removeItem(atPath: toPath) + do { + try FileManager.default.copyItem(atPath: fromPath, toPath: toPath) + return true + } catch { + return false + } + } + + + override func startDownload(with request: SPUURLRequest!) { + if let internalUrl = self.updateItem.internalUrl { + + let url = inApp(for: internalUrl as NSString, context: self.context, peerId: nil, openInfo: { _, _, _, _ in }, hashtag: nil, command: nil, applyProxy: nil, confirm: false) + switch url { + case let .followResolvedName(_, username, messageId, context, _, _): + if let messageId = messageId { + let signal = downloadAppUpdate(account: context.account, source: username, messageId: messageId) |> deliverOnMainQueue + disposable.set(signal.start(next: { [weak self] result in + guard let `self` = self else { + return + } + switch result { + case let .started(total): + self.delegate.downloaderDidReceiveExpectedContentLength(Int64(total)) + case let .progress(current, _): + self.delegate.downloaderDidReceiveData(ofLength: UInt64(current)) + case let .finished(path): + self.urlSession(URLSession(), downloadTask: URLSessionDownloadTask(), didFinishDownloadingTo: URL(fileURLWithPath: path)) + } + }, error: { [weak self] error in + self?.delegate.downloaderDidFailWithError(NSError(domain: "Failed to download archive. Please try again.", code: 0, userInfo: nil)) + })) + } else { + self.delegate.downloaderDidFailWithError(NSError(domain: "Wrong internal link. Please try again.", code: 0, userInfo: nil)) + } + + default: + self.delegate.downloaderDidFailWithError(NSError(domain: "Wrong internal link. Please try again.", code: 0, userInfo: nil)) + } + + + } else { + self.delegate.downloaderDidFailWithError(NSError(domain: "No internal link for this version. Please try again.", code: 0, userInfo: nil)) + } + + } + + + override func cancel() { + disposable.set(nil) + } + +} + +private final class InternalUpdateDriver : ExternalUpdateDriver { + + + private let disposabe = MetaDisposable() + private let context: AccountContext + + init(updater:TelegramUpdater, context: AccountContext) { + self.context = context + super.init(updater: updater) + } + + deinit { + disposabe.dispose() + } + + override func checkForUpdates(at URL: URL!, host aHost: SUHost!, domain: String) { + self.host = aHost + + updateState { + return $0.withUpdatedLoadingState(.initializing) + } + + let signal = requestUpdatesXml(account: self.context.account, source: updates_channel_xml) |> deliverOnMainQueue |> timeout(20.0, queue: .mainQueue(), alternate: .fail(.xmlLoad)) + + disposabe.set(signal.start(next: { [weak self] data in + let appcast = SUAppcast() + appcast.parseAppcastItems(fromXMLData: data, error: nil) + self?.appcastDidFinishLoading(appcast) + }, error: { [weak self] error in + self?.abortUpdateWithError(NSError(domain: "Failed to download updating info. Please try again.", code: 0, userInfo: nil)) + })) + } + + override func downloadUpdate() { + let downloader = InternalUpdaterDownloader(context: self.context, updateItem: self.updateItem, delegate: self) + self.download = downloader + let fileName = "Telegram \(self.updateItem.versionString ?? "")" + + downloader.startPersistentDownload(with: SPUURLRequest(), bundleIdentifier: host.bundle.bundleIdentifier!, desiredFilename: fileName) + } + + override func downloaderDidReceiveData(ofLength length: UInt64) { + updateState { state in + switch state.loadingState { + case let .loading(item, _, total): + return state.withUpdatedLoadingState(.loading(item: item, current: Int(length), total: total)) + default: + return state + } + } + } + + override func downloaderDidReceiveExpectedContentLength(_ expectedContentLength: Int64) { + updateState { state in + return state.withUpdatedLoadingState(.loading(item: self.updateItem, current: 0, total: Int(expectedContentLength))) + } + } + +} + +private class ExternalUpdateDriver : SUBasicUpdateDriver { + + override func extractUpdate() { + super.extractUpdate() + updateState { + return $0.withUpdatedLoadingState(.unarchiving(self.updateItem)) + } + } + + + + override func install(withToolAndRelaunch relaunch: Bool, displayingUserInterface showUI: Bool) { + updateState { + return $0.withUpdatedLoadingState(.installing) + } + resourcesQueue.async { + super.install(withToolAndRelaunch: relaunch, displayingUserInterface: showUI) + } + } + + override func appcastDidFinishLoading(_ ac: SUAppcast!) { + updateState { + return $0.withUpdatedItems(ac.items?.compactMap({$0 as? SUAppcastItem}) ?? []) + } + super.appcastDidFinishLoading(ac) + } + + override func didNotFindUpdate() { + updateState { + return $0.withUpdatedLoadingState(.uptodate) + } + } + + override func checkForUpdates(at url: URL!, host aHost: SUHost!, domain: String) { + updateState { + return $0.withUpdatedLoadingState(.initializing) + } + super.checkForUpdates(at: url, host: aHost, domain: domain) + + } + + override func downloadUpdate() { + updateState { + return $0.withUpdatedLoadingState(.loading(item: self.updateItem, current: 0, total: Int(self.updateItem.contentLength))) + } + super.downloadUpdate() + } + + override func downloaderDidFinish(withTemporaryDownloadData downloadData: SPUDownloadData!) { + super.downloaderDidFinish(withTemporaryDownloadData: downloadData) + } + + override func unarchiverDidFinish(_ ua: Any!) { + updateState { + return $0.withUpdatedLoadingState(.readyToInstall(self.updateItem)) + } + } + + override func unarchiver(_ ua: Any!, extractedProgress progress: Double) { + + } + + override func downloaderDidReceiveData(ofLength length: UInt64) { + updateState { state in + switch state.loadingState { + case let .loading(item, current, total): + return state.withUpdatedLoadingState(.loading(item: item, current: current + Int(length), total: total)) + default: + return state + } + } + } + + override func downloaderDidReceiveExpectedContentLength(_ expectedContentLength: Int64) { + updateState { state in + return state.withUpdatedLoadingState(.loading(item: self.updateItem, current: 0, total: Int(expectedContentLength))) + } + } + + override func downloaderDidFailWithError(_ error: Error!) { + super.downloaderDidFailWithError(error) + updateState { state in + return state.withUpdatedLoadingState(.failed(error as NSError? ?? NSError(domain: L10n.unknownError, code: 0, userInfo: nil))) + } + } + + override func abortUpdateWithError(_ error: Error!) { + super.abortUpdateWithError(error) + updateState { state in + return state.withUpdatedLoadingState(.failed(error as NSError? ?? NSError(domain: L10n.unknownError, code: 0, userInfo: nil))) + } + trySwitchUpdaterBetweenSources() + } + + override func installer(for host: SUHost!, failedWithError error: Error!) { + super.installer(for: host, failedWithError: error) + updateState { state in + return state.withUpdatedLoadingState(.failed(error as NSError? ?? NSError(domain: L10n.unknownError, code: 0, userInfo: nil))) + } + trySwitchUpdaterBetweenSources() + } +} + + + + +private let disposable = MetaDisposable() + +func setAppUpdaterBaseDomain(_ domain: String?) { + updater.domain = domain + if let domain = domain { + updater.host = URL(string: domain)?.host + } else { + updater.host = nil + } +} + + +func updateAppIfNeeded() { + let state = stateValue.with {$0.loadingState} + + switch state { + case .readyToInstall: + driver?.install(withToolAndRelaunch: false, displayingUserInterface: true) + default: + break + } +} + + +enum UpdaterSource : Equatable { + static func == (lhs: UpdaterSource, rhs: UpdaterSource) -> Bool { + switch lhs { + case let .external(lhsContext): + if case let .external(rhsContext) = rhs { + if let lhsContext = lhsContext, let rhsContext = rhsContext { + return lhsContext.account.peerId == rhsContext.account.peerId + } else if (lhsContext != nil) != (rhsContext != nil) { + return false + } + return true + } else { + return false + } + case let .internal(lhsContext): + if case let .internal(rhsContext) = rhs { + return lhsContext.account.peerId == rhsContext.account.peerId + } else { + return false + } + } + } + + case external(context: AccountContext?) + case `internal`(context: AccountContext) +} + + +private func resetUpdater() { + + #if !GITHUB + let update:()->Void = { + let url = updater.domain ?? Bundle.main.infoDictionary!["SUFeedURL"] as! String + let state = stateValue.with { $0.loadingState } + switch state { + case .readyToInstall, .installing, .unarchiving, .loading: + break + default: + driver?.checkForUpdates(at: URL(string: url)!, host: host, domain: updater.host) + } + } + + + let signal: Signal = Signal { subscriber in + update() + subscriber.putCompletion() + return EmptyDisposable + } |> delay(20 * 60, queue: .mainQueue()) |> restart + disposable.set(signal.start()) + + update() + #endif + + +} + +private var updaterSource: UpdaterSource? = nil + +func updater_resetWithUpdaterSource(_ source: UpdaterSource, force: Bool = true) { + let state = stateValue.with { $0 } + switch state.loadingState { + case .readyToInstall: + return + default: + break + } + if updaterSource != source { + updaterSource = source + switch source { + case .external: + driver = ExternalUpdateDriver(updater: updater) + case let .internal(context): + driver = InternalUpdateDriver(updater: updater, context: context) + } + } + if force { + updateState { + $0.withUpdatedLoadingState(.initializing) + } + resetUpdater() + } +} + + +private func trySwitchUpdaterBetweenSources() { + if let source = updaterSource { + switch source { + case let .external(context): + #if STABLE || DEBUG + if let context = context { + updater_resetWithUpdaterSource(.internal(context: context), force: true) + } + #endif + case let .internal(context): + updater_resetWithUpdaterSource(.external(context: context), force: false) + } + } +} + +#endif + diff --git a/Telegram-Mac/Appearance.swift b/Telegram-Mac/Appearance.swift index ae08b61450..f8f258436e 100644 --- a/Telegram-Mac/Appearance.swift +++ b/Telegram-Mac/Appearance.swift @@ -8,8 +8,709 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac +import TelegramCore + +import SwiftSignalKit +import Postbox + + + +func generateFilledCircleImage(diameter: CGFloat, color: NSColor?, strokeColor: NSColor? = nil, strokeWidth: CGFloat? = nil, backgroundColor: NSColor? = nil) -> CGImage { + return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + if let backgroundColor = backgroundColor { + context.setFillColor(backgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + } + + if let strokeColor = strokeColor, let strokeWidth = strokeWidth { + context.setFillColor(strokeColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + + if let color = color { + context.setFillColor(color.cgColor) + } else { + context.setFillColor(NSColor.clear.cgColor) + context.setBlendMode(.copy) + } + context.fillEllipse(in: CGRect(origin: CGPoint(x: strokeWidth, y: strokeWidth), size: CGSize(width: size.width - strokeWidth * 2.0, height: size.height - strokeWidth * 2.0))) + } else { + if let color = color { + context.setFillColor(color.cgColor) + } else { + context.setFillColor(NSColor.clear.cgColor) + context.setBlendMode(.copy) + } + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + } + })! +} + + +func generateTextIcon(_ text: NSAttributedString) -> CGImage { + + let textNode = TextNode.layoutText(text, nil, 1, .end, NSMakeSize(.greatestFiniteMagnitude, 20), nil, false, .center) + var size = textNode.0.size + size.width = max(size.width, 24) + size.width = max(size.height, 23) + return generateImage(textNode.0.size, rotatedContext: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + + textNode.1.draw(rect.focus(textNode.0.size), in: ctx, backingScaleFactor: System.backingScale, backgroundColor: .clear) + })! +} + +private func generateGradientBubble(_ colors: [NSColor]) -> CGImage { + var colors = colors + if !System.supportsTransparentFontDrawing { + let blended = colors.reduce(colors.first!, { + $0.blended(withFraction: 0.5, of: $1)! + }) + for (i, _) in colors.enumerated() { + colors[i] = blended + } + } + + return generateImage(CGSize(width: 32, height: 32), opaque: true, scale: 1.0, rotatedContext: { size, context in + + if colors.count > 1 { + if colors.count == 2 { + let colors = colors.map { $0.cgColor } as NSArray + + let delta: CGFloat = 1.0 / (CGFloat(colors.count) - 1.0) + + var locations: [CGFloat] = [] + for i in 0 ..< colors.count { + locations.append(delta * CGFloat(i)) + } + + let colorSpace = deviceColorSpace + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + } else { + let preview = AnimatedGradientBackgroundView.generatePreview(size: NSMakeSize(32, 32), colors: colors) + context.draw(preview, in: size.bounds) + } + + } else if let color = colors.first { + context.setFillColor(color.cgColor) + context.fill(size.bounds) + } + })! +} + +private func generateProfileIcon(_ image: CGImage, backgroundColor: NSColor) -> CGImage { + return generateImage(image.backingSize, contextGenerator: { size, ctx in + let rect = CGRect(origin: CGPoint(), size: size) + ctx.clear(rect) + + + + ctx.setFillColor(backgroundColor.cgColor) + ctx.fillEllipse(in: NSMakeRect(2, 2, rect.width - 4, rect.height - 4)) + + ctx.clip(to: CGRect(origin: CGPoint(), size: size), mask: image) + + ctx.clear(rect) + + + // ctx.clip(to: rect) +// +// ctx.setFillColor(NSColor.red.cgColor) +// ctx.fillEllipse(in: NSMakeRect(2, 2, rect.width - 4, rect.height - 4)) + + + })! +} + +private func generateChatTabFiltersIcon(_ image: CGImage) -> CGImage { + return generateImage(image.backingSize, contextGenerator: { size, ctx in + let rect = CGRect(origin: CGPoint(), size: size) + ctx.clear(rect) + + ctx.draw(image, in: CGRect(origin: CGPoint(), size: size)) + + ctx.setBlendMode(.clear) + + var x: CGFloat = 14 + ctx.fillEllipse(in: NSMakeRect(x, 17, 3, 3)) + x += (3 + 2) + ctx.fillEllipse(in: NSMakeRect(x, 17, 3, 3)) + x += (3 + 2) + ctx.fillEllipse(in: NSMakeRect(x, 17, 3, 3)) + + })! +} + +private func generateChatAction(_ image: CGImage, background: NSColor) -> CGImage { + return generateImage(NSMakeSize(36, 36), contextGenerator: { size, ctx in + let rect = CGRect(origin: CGPoint(), size: size) + ctx.clear(rect) + + ctx.setFillColor(background.cgColor) + ctx.fillEllipse(in: rect) + ctx.draw(image, in: rect.focus(image.backingSize)) + + })! +} + +private func generatePollIcon(_ image: NSImage, backgound: NSColor, doubleSize: Bool = false) -> CGImage { + return generateImage(NSMakeSize(doubleSize ? 36 : 18, doubleSize ? 36 : 18), contextGenerator: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + + ctx.setBlendMode(.copy) + ctx.round(size, size.height / 2) + ctx.setFillColor(backgound.cgColor) + ctx.fill(rect) + + ctx.setBlendMode(.normal) + let image = image.cgImage(forProposedRect: nil, context: nil, hints: nil)! + if backgound == NSColor(0xffffff) { + ctx.clip(to: rect, mask: image) + ctx.clear(rect) + } else { + ctx.draw(image, in: rect.focus(NSMakeSize(image.size.width / System.backingScale * (doubleSize ? 2 : 1), image.size.height / System.backingScale * (doubleSize ? 2 : 1)))) + } + }, scale: System.backingScale)! +} + +private func generateSecretThumbSmall(_ image: CGImage) -> CGImage { + return generateImage(NSMakeSize(floor(image.size.width * 0.7), floor(image.size.height * 0.7)), contextGenerator: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.clip(to: rect, mask: image) + ctx.setBlendMode(.difference) + ctx.setFillColor(.white) + ctx.fill(rect) + ctx.draw(image, in: rect) + }, scale: 1.0)! +} + +private func generateSecretThumb(_ image: CGImage) -> CGImage { + return generateImage(image.size, contextGenerator: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.clip(to: rect, mask: image) + ctx.setBlendMode(.difference) + ctx.setFillColor(.white) + ctx.fill(rect) + ctx.draw(image, in: rect) + }, scale: 1.0)! +} + +private func generateLoginQrEmptyCap() -> CGImage { + return generateImage(NSMakeSize(60, 60), contextGenerator: { size, ctx in + ctx.clear(CGRect(origin: CGPoint(), size: size)) + })! +} + +private func generateSendIcon(_ image: NSImage, _ color: NSColor) -> CGImage { + let image = image.precomposed(color) + if color.lightness > 0.7 { + return image + } else { + return generateImage(image.backingSize, contextGenerator: { size, ctx in + let rect = CGRect(origin: CGPoint(), size: size) + ctx.clear(rect) + + ctx.setFillColor(.white) + ctx.fillEllipse(in: rect.focus(NSMakeSize(rect.width - 8, rect.height - 8))) + + ctx.draw(image, in: CGRect(origin: CGPoint(), size: size)) + })! + } +} + +private func generateUnslectedCap(_ color: NSColor) -> CGImage { + return generateImage(NSMakeSize(22, 22), contextGenerator: { size, ctx in + let rect = CGRect(origin: CGPoint(), size: size) + ctx.clear(rect) + + ctx.setStrokeColor(color.withAlphaComponent(0.7).cgColor) + ctx.setLineWidth(1.0) + ctx.strokeEllipse(in: NSMakeRect(1, 1, size.width - 2, size.height - 2)) + })! +} + +private func generatePollAddOption(_ color: NSColor) -> CGImage { + let image = NSImage(named: "Icon_PollAddOption")!.precomposed(color) + return generateImage(image.backingSize, contextGenerator: { size, ctx in + ctx.clear(CGRect(origin: CGPoint(), size: size)) + + ctx.setFillColor(.white) + ctx.fillEllipse(in: NSMakeRect(0, 0, size.width, size.height)) + + ctx.draw(image, in: CGRect(origin: CGPoint(), size: size)) + })! +} + +func generateThemePreview(for palette: ColorPalette, wallpaper: Wallpaper, backgroundMode: TableBackgroundMode) -> CGImage { + return generateImage(NSMakeSize(320, 320), rotatedContext: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + + //background + ctx.setFillColor(palette.chatBackground.cgColor) + ctx.fill(rect) + + #if !SHARE + switch wallpaper { + case .builtin, .file, .color, .gradient: + ctx.saveGState() + ctx.translateBy(x: size.width / 2.0, y: size.height / 2.0) + ctx.scaleBy(x: 1.0, y: -1.0) + ctx.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + drawBg(backgroundMode, palette: palette, bubbled: true, rect: rect, in: ctx) + ctx.restoreGState() + default: + break + } + #endif + + + //top and bottom + ctx.setFillColor(palette.background.cgColor) + ctx.fill(NSMakeRect(0, 0, rect.width, 50)) + ctx.setFillColor(palette.background.cgColor) + ctx.fill(NSMakeRect(0, rect.height - 50, rect.width, 50)) + + + + //top border + ctx.setFillColor(palette.border.cgColor) + ctx.fill(NSMakeRect(0, 50, rect.width, .borderSize)) + + //bottom border + ctx.setFillColor(palette.border.cgColor) + ctx.fill(NSMakeRect(0, rect.height - 50, rect.width, .borderSize)) + + + //fill avatar + ctx.setFillColor(palette.grayForeground.cgColor) + ctx.fillEllipse(in: NSMakeRect(20, (50 - 36) / 2, 36, 36)) + + //fill chat actions + let chatAction = NSImage(named: "Icon_ChatActions")!.precomposed(palette.accentIcon) + ctx.draw(chatAction, in: NSMakeRect(rect.width - 20 - chatAction.backingSize.width, (50 - chatAction.backingSize.height) / 2, chatAction.backingSize.width, chatAction.backingSize.height)) + + //fill attach icon + let inputAttach = NSImage(named: "Icon_ChatAttach")!.precomposed(palette.grayIcon, flipVertical: true) + ctx.draw(inputAttach, in: NSMakeRect(20, rect.height - 50 + ((50 - inputAttach.backingSize.height) / 2), inputAttach.backingSize.width, inputAttach.backingSize.height)) + + //fill micro icon + let micro = NSImage(named: "Icon_RecordVoice")!.precomposed(palette.grayIcon, flipVertical: true) + ctx.draw(micro, in: NSMakeRect(rect.width - 20 - inputAttach.backingSize.width, (rect.height - 50 + (50 - micro.backingSize.height) / 2), micro.backingSize.width, micro.backingSize.height)) + + let chatServiceItemColor: NSColor + + + switch wallpaper { + case .builtin, .file, .color, .gradient: + switch backgroundMode { + case let .background(image, _, colors, _): + if let colors = colors, let first = colors.first { + let blended = colors.reduce(first, { color, with in + return color.blended(withFraction: 0.5, of: with)! + }) + chatServiceItemColor = getAverageColor(blended) + } else { + chatServiceItemColor = getAverageColor(image) + } + case let .color(color): + if color != palette.background { + chatServiceItemColor = getAverageColor(color) + } else { + chatServiceItemColor = color + } + case let .gradient(colors, _): + let blended = colors.reduce(colors.first!, { color, with in + return color.blended(withFraction: 0.5, of: with)! + }) + chatServiceItemColor = getAverageColor(blended) + case let .tiled(image): + chatServiceItemColor = getAverageColor(image) + case .plain: + chatServiceItemColor = palette.chatBackground + } + default: + chatServiceItemColor = getAverageColor(palette.chatBackground) + } + + + + //fill date + ctx.setFillColor(chatServiceItemColor.cgColor) + let path = NSBezierPath(roundedRect: NSMakeRect(rect.width / 2 - 30, rect.height - 50 - 10 - 60 - 5 - 20 - 5, 60, 20), xRadius: 10, yRadius: 10) + ctx.addPath(path.cgPath) + ctx.closePath() + ctx.fillPath() + + + //fill outgoing bubble + CATransaction.begin() + if true { + + let image = generateImage(NSMakeSize(150, 30), rotatedContext: { size, ctx in + ctx.clear(NSMakeRect(0, 0, size.width, size.height)) + let data = messageBubbleImageModern(incoming: false, fillColor: palette.blendedOutgoingColors, strokeColor: palette.bubbleBorder_outgoing, neighbors: .none) + + let layer = CALayer() + layer.frame = NSMakeRect(0, 0, 150, 30) + layer.contentsScale = 2.0 + let imageSize = data.0.backingSize + let insets = data.1 + let halfPixelFudge: CGFloat = 0.49 + let otherPixelFudge: CGFloat = 0.02 + var contentsCenter: CGRect = NSMakeRect(0.0, 0.0, 1.0, 1.0); + if (insets.left > 0 || insets.right > 0) { + contentsCenter.origin.x = ((insets.left + halfPixelFudge) / imageSize.width); + contentsCenter.size.width = (imageSize.width - (insets.left + insets.right + 1.0) + otherPixelFudge) / imageSize.width; + } + if (insets.top > 0 || insets.bottom > 0) { + contentsCenter.origin.y = ((insets.top + halfPixelFudge) / imageSize.height); + contentsCenter.size.height = (imageSize.height - (insets.top + insets.bottom + 1.0) + otherPixelFudge) / imageSize.height; + } + layer.contentsGravity = .resize; + layer.contentsCenter = contentsCenter; + layer.contents = data.0 + + layer.render(in: ctx) + })! + + var bubble = image + bubble = generateImage(NSMakeSize(150, 30), contextGenerator: { size, ctx in + let colors = palette.bubbleBackground_outgoing.map { $0.withAlphaComponent(1.0) } + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.clip(to: rect, mask: image) + + if colors.count > 1 { + let gradientColors = colors.map { $0.cgColor } as CFArray + let delta: CGFloat = 1.0 / (CGFloat(colors.count) - 1.0) + + var locations: [CGFloat] = [] + for i in 0 ..< colors.count { + locations.append(delta * CGFloat(i)) + } + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + ctx.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: rect.height), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]) + } else if colors.count == 1 { + ctx.setFillColor(colors[0].cgColor) + ctx.fill(rect) + } + + })! + ctx.draw(bubble, in: NSMakeRect(160, 230, 150, 30)) + + } + + //fill incoming bubble + if true { + let image = generateImage(NSMakeSize(150, 30), rotatedContext: { size, ctx in + ctx.clear(NSMakeRect(0, 0, size.width, size.height)) + let data = messageBubbleImageModern(incoming: true, fillColor: palette.bubbleBackground_incoming, strokeColor: palette.bubbleBorder_incoming, neighbors: .none) + + let layer = CALayer() + layer.frame = NSMakeRect(0, 0, 150, 30) + layer.contentsScale = 2.0 + let imageSize = data.0.backingSize + let insets = data.1 + let halfPixelFudge: CGFloat = 0.49 + let otherPixelFudge: CGFloat = 0.02 + var contentsCenter: CGRect = NSMakeRect(0.0, 0.0, 1.0, 1.0); + if (insets.left > 0 || insets.right > 0) { + contentsCenter.origin.x = ((insets.left + halfPixelFudge) / imageSize.width); + contentsCenter.size.width = (imageSize.width - (insets.left + insets.right + 1.0) + otherPixelFudge) / imageSize.width; + } + if (insets.top > 0 || insets.bottom > 0) { + contentsCenter.origin.y = ((insets.top + halfPixelFudge) / imageSize.height); + contentsCenter.size.height = (imageSize.height - (insets.top + insets.bottom + 1.0) + otherPixelFudge) / imageSize.height; + } + layer.contentsGravity = .resize; + layer.contentsCenter = contentsCenter; + layer.contents = data.0 + + layer.render(in: ctx) + })! + + ctx.draw(image, in: NSMakeRect(10, 200, 150, 30)) + + } + CATransaction.commit() + + })! +} + +func generateDialogVerify(background: NSColor, foreground: NSColor, reversed: Bool = false) -> CGImage { + if reversed { + return generateImage(NSMakeSize(24, 24), contextGenerator: { size, ctx in + ctx.clear(CGRect(origin: CGPoint(), size: size)) + + let image = NSImage(named: "Icon_VerifyDialog")!.precomposed(foreground) + + ctx.setFillColor(background.cgColor) + ctx.fillEllipse(in: NSMakeRect(8, 8, size.width - 16, size.height - 16)) + + ctx.draw(image, in: CGRect(origin: CGPoint(), size: size)) + })! + } else { + return generateImage(NSMakeSize(24, 24), rotatedContext: { size, ctx in + ctx.clear(CGRect(origin: CGPoint(), size: size)) + + let image = NSImage(named: "Icon_VerifyDialog")!.precomposed(foreground) + + ctx.setFillColor(background.cgColor) + ctx.fillEllipse(in: NSMakeRect(8, 8, size.width - 16, size.height - 16)) + + ctx.draw(image, in: CGRect(origin: CGPoint(), size: size)) + })! + } + +} + +private func generatePollDeleteOption(_ color: NSColor) -> CGImage { + let image = NSImage(named: "Icon_PollDeleteOption")!.precomposed(color) + return generateImage(image.backingSize, contextGenerator: { size, ctx in + ctx.clear(CGRect(origin: CGPoint(), size: size)) + + ctx.setFillColor(.white) + ctx.fillEllipse(in: NSMakeRect(0, 0, size.width, size.height)) + + ctx.draw(image, in: CGRect(origin: CGPoint(), size: size)) + })! +} + +private func generateStickerPackSelection(_ color: NSColor) -> CGImage { + return generateImage(NSMakeSize(35, 35), contextGenerator: { size, ctx in + ctx.interpolationQuality = .low + ctx.clear(CGRect(origin: CGPoint(), size: size)) + ctx.round(size, .cornerRadius) + ctx.setFillColor(color.cgColor) + ctx.fill(NSMakeRect(0, 0, size.width, size.height)) + })! +} + +private func generateHitActiveIcon(activeColor: NSColor, backgroundColor: NSColor) -> CGImage { + return generateImage(NSMakeSize(12, 12), contextGenerator: { size, ctx in + ctx.interpolationQuality = .high + ctx.clear(CGRect(origin: CGPoint(), size: size)) + ctx.round(size, size.width / 2) + ctx.setFillColor(backgroundColor.cgColor) + ctx.fillEllipse(in: NSMakeRect(0, 0, size.width, size.height)) + + ctx.setFillColor(activeColor.cgColor) + ctx.fillEllipse(in: NSMakeRect(2, 2, 8, 8)) + })! +} + +func generateScamIcon(foregroundColor: NSColor, backgroundColor: NSColor, text: String = L10n.markScam, isReversed: Bool = false) -> CGImage { + let textNode = TextNode.layoutText(.initialize(string: text, color: foregroundColor, font: .medium(9)), nil, 1, .end, NSMakeSize(.greatestFiniteMagnitude, 20), nil, false, .center) + + let draw: (CGSize, CGContext) -> Void = { size, ctx in + ctx.interpolationQuality = .high + ctx.clear(CGRect(origin: CGPoint(), size: size)) + + let borderPath = NSBezierPath(roundedRect: NSMakeRect(1, 1, size.width - 2, size.height - 2), xRadius: 2, yRadius: 2) + + ctx.setStrokeColor(foregroundColor.cgColor) + ctx.addPath(borderPath.cgPath) + ctx.closePath() + ctx.strokePath() + + let textRect = NSMakeRect((size.width - textNode.0.size.width) / 2, (size.height - textNode.0.size.height) / 2 + 1, textNode.0.size.width, textNode.0.size.height) + textNode.1.draw(textRect, in: ctx, backingScaleFactor: System.backingScale, backgroundColor: backgroundColor) + } + if !isReversed { + return generateImage(NSMakeSize(textNode.0.size.width + 8, 16), contextGenerator: draw)! + } else { + return generateImage(NSMakeSize(textNode.0.size.width + 8, 16), rotatedContext: draw)! + } +} + +func generateScamIconReversed(foregroundColor: NSColor, backgroundColor: NSColor) -> CGImage { + return generateScamIcon(foregroundColor: foregroundColor, backgroundColor: backgroundColor, isReversed: true) +} + +func generateFakeIcon(foregroundColor: NSColor, backgroundColor: NSColor, isReversed: Bool = false) -> CGImage { + return generateScamIcon(foregroundColor: foregroundColor, backgroundColor: backgroundColor, text: L10n.markFake, isReversed: isReversed) +} +func generateFakeIconReversed(foregroundColor: NSColor, backgroundColor: NSColor) -> CGImage { + return generateScamIcon(foregroundColor: foregroundColor, backgroundColor: backgroundColor, text: L10n.markFake, isReversed: true) +} + +private func generateVideoMessageChatCap(backgroundColor: NSColor) -> CGImage { + return generateImage(NSMakeSize(200, 200), contextGenerator: { size, ctx in + ctx.clear(CGRect(origin: CGPoint(), size: size)) + + ctx.setFillColor(backgroundColor.cgColor) + ctx.fill(NSMakeRect(0, 0, size.width, size.height)) + + ctx.setFillColor(.clear) + ctx.setBlendMode(.clear) + + let radius = size.width / 2 + + let center = NSMakePoint(100, 100) + + ctx.addArc(center: center, radius: radius - 0.54, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: false) + ctx.drawPath(using: .fill) + ctx.setBlendMode(.normal) +// CGContextAddArc(context, center.x, center.y, radius - 0.54, 0, 2 * M_PI, 0); +// CGContextDrawPath(context, kCGPathFill); +// CGContextSetBlendMode(context, kCGBlendModeNormal); + + + })! +} + +private func generateEditMessageMediaIcon(_ icon: CGImage, background: NSColor) -> CGImage { + return generateImage(NSMakeSize(icon.backingSize.width + 1, icon.backingSize.height + 1), contextGenerator: { size, ctx in + ctx.clear(CGRect(origin: CGPoint(), size: size)) + ctx.round(size, size.width / 2) + + ctx.setFillColor(background.cgColor) + ctx.fillEllipse(in: NSMakeRect(0, 0, size.width, size.height)) + + let imageRect = NSMakeRect(floorToScreenPixels(System.backingScale, (size.width - icon.backingSize.width) / 2), floorToScreenPixels(System.backingScale, (size.height - icon.backingSize.height) / 2), icon.backingSize.width, icon.backingSize.height) + ctx.draw(icon, in: imageRect) + + })! +} + +private func generateUnreadFeaturedStickers(_ icon: CGImage, _ color: NSColor) -> CGImage { + return generateImage(NSMakeSize(icon.systemSize.width, icon.systemSize.height), contextGenerator: { size, ctx in + ctx.clear(CGRect(origin: CGPoint(), size: size)) + + let imageRect = NSMakeRect(floorToScreenPixels(System.backingScale, (size.width - icon.backingSize.width) / 2), floorToScreenPixels(System.backingScale, (size.height - icon.backingSize.height) / 2), icon.backingSize.width, icon.backingSize.height) + ctx.draw(icon, in: imageRect) + + ctx.setFillColor(color.cgColor) + ctx.fillEllipse(in: NSMakeRect(size.width - 11, size.height - 12, 6, 6)) + + }, scale: System.backingScale)! +} + + +private func generatePlayerListAlbumPlaceholder(_ icon: CGImage?, background: NSColor, radius: CGFloat) -> CGImage { + return generateImage(NSMakeSize(40, 40), contextGenerator: { size, ctx in + ctx.clear(CGRect(origin: CGPoint(), size: size)) + ctx.round(size, radius) + + ctx.setFillColor(background.cgColor) + ctx.fill(NSMakeRect(0, 0, size.width, size.height)) + + if let icon = icon { + let imageRect = NSMakeRect(floorToScreenPixels(System.backingScale, (size.width - icon.backingSize.width) / 2), floorToScreenPixels(System.backingScale, (size.height - icon.backingSize.height) / 2), icon.backingSize.width, icon.backingSize.height) + ctx.draw(icon, in: imageRect) + } + + })! +} + +private func generateLocationPinIcon(_ background: NSColor) -> CGImage { + return generateImage(NSMakeSize(40, 40), contextGenerator: { size, ctx in + ctx.clear(CGRect(origin: CGPoint(), size: size)) + ctx.round(size, size.width / 2) + + ctx.setFillColor(background.cgColor) + ctx.fillEllipse(in: NSMakeRect(0, 0, size.width, size.height)) + + let icon = #imageLiteral(resourceName: "Icon_LocationPin").precomposed(.white) + let imageRect = NSMakeRect(floorToScreenPixels(System.backingScale, (size.width - icon.backingSize.width) / 2), floorToScreenPixels(System.backingScale, (size.height - icon.backingSize.height) / 2), icon.backingSize.width, icon.backingSize.height) + ctx.draw(icon, in: imageRect) + + })! +} + +private func generateChatTabSelected(_ color: NSColor, _ icon: CGImage) -> CGImage { + let main = #imageLiteral(resourceName: "Icon_TabChatList_Highlighted").precomposed(color) + return generateImage(main.backingSize, contextGenerator: { size, ctx in + ctx.clear(CGRect(origin: CGPoint(), size: size)) + ctx.draw(main, in: NSMakeRect(0, 0, size.width, size.height)) + + + let imageRect = NSMakeRect(floorToScreenPixels(System.backingScale, (size.width - icon.backingSize.width) / 2) - 2, floorToScreenPixels(System.backingScale, (size.height - icon.backingSize.height) / 2) + 2, icon.backingSize.width, icon.backingSize.height) + ctx.draw(icon, in: imageRect) + + })! +} + + +private func generateTriangle(_ size: NSSize, color: NSColor) -> CGImage { + return generateImage(size, contextGenerator: { size, ctx in + let rect = CGRect(origin: CGPoint(), size: size) + ctx.clear(rect) + + ctx.beginPath() + ctx.move(to: CGPoint(x: rect.minX, y: rect.maxY)) + ctx.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY)) + ctx.addLine(to: CGPoint(x: (rect.midX), y: rect.minY)) + ctx.closePath() + + ctx.setFillColor(color.cgColor) + ctx.fillPath() + })! +} + + + +private func generateLocationMapPinIcon(_ background: NSColor) -> CGImage { + return generateImage(NSMakeSize(40, 46), contextGenerator: { size, ctx in + ctx.clear(CGRect(origin: CGPoint(), size: size)) + + ctx.setFillColor(background.cgColor) + ctx.fillEllipse(in: NSMakeRect(0, 6, size.width, size.height - 6)) + + let icon = #imageLiteral(resourceName: "Icon_LocationPin").precomposed(.white) + let imageRect = NSMakeRect(floorToScreenPixels(System.backingScale, (size.width - icon.backingSize.width) / 2), floorToScreenPixels(System.backingScale, (size.height - icon.backingSize.height) / 2) + 3, icon.backingSize.width, icon.backingSize.height) + ctx.draw(icon, in: imageRect) + + let triangle = generateTriangle(NSMakeSize(12, 10), color: background) + let triangleRect = NSMakeRect(floorToScreenPixels(System.backingScale, (size.width - triangle.backingSize.width) / 2), 0, triangle.backingSize.width, triangle.backingSize.height) + + ctx.draw(triangle, in: triangleRect) + + })! +} + +private func generateLockerBody(_ color: NSColor, backgroundColor: NSColor) -> CGImage { + return generateImage(NSMakeSize(12.5, 12.5), contextGenerator: { size, ctx in + ctx.clear(CGRect(origin: CGPoint(), size: size)) + + ctx.setFillColor(backgroundColor.cgColor) + ctx.fillEllipse(in: NSMakeRect(0, 0, size.width, size.height)) + + ctx.setFillColor(color.cgColor) + ctx.setStrokeColor(color.cgColor) + ctx.setLineWidth(1.0) + ctx.strokeEllipse(in: CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: CGSize(width: size.width - 2.0, height: size.height - 2.0))) + ctx.fillEllipse(in: NSMakeRect(floorToScreenPixels(System.backingScale, (size.width - 2)/2), floorToScreenPixels(System.backingScale, (size.height - 2)/2), 2, 2)) + + })! +} +private func generateLockerHead(_ color: NSColor, backgroundColor: NSColor) -> CGImage { + return generateImage(NSMakeSize(10, 20), contextGenerator: { size, ctx in + ctx.clear(CGRect(origin: CGPoint(), size: size)) + ctx.round(size, size.width / 2) + + ctx.setFillColor(color.cgColor) + ctx.fill(NSMakeRect(0, 0, size.width, size.height)) + + ctx.setFillColor(backgroundColor.cgColor) + ctx.fillEllipse(in: CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: CGSize(width: size.width - 2, height: size.width - 2))) + ctx.fillEllipse(in: CGRect(origin: CGPoint(x: 1.0, y: size.height - size.width + 1), size: CGSize(width: size.width - 2, height: size.width - 2))) + ctx.fill(NSMakeRect(1.0, 0, size.width - 1, 14)) + + ctx.clear(NSMakeRect(0, 0, size.width, 3)) + + + + })! +} private func generateChatMention(backgroundColor: NSColor, border: NSColor, foregroundColor: NSColor) -> CGImage { return generateImage(NSMakeSize(38, 38), contextGenerator: { size, ctx in @@ -18,28 +719,199 @@ private func generateChatMention(backgroundColor: NSColor, border: NSColor, fore ctx.fillEllipse(in: CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: CGSize(width: size.width - 2.0, height: size.height - 2.0))) ctx.setLineWidth(1.0) ctx.setStrokeColor(border.withAlphaComponent(0.7).cgColor) - ctx.strokeEllipse(in: CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: CGSize(width: size.width - 2.0, height: size.height - 2.0))) + // ctx.strokeEllipse(in: CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: CGSize(width: size.width - 2.0, height: size.height - 2.0))) let icon = #imageLiteral(resourceName: "Icon_ChatMention").precomposed(foregroundColor) - let imageRect = NSMakeRect(floorToScreenPixels((size.width - icon.backingSize.width) / 2), floorToScreenPixels((size.height - icon.backingSize.height) / 2), icon.backingSize.width, icon.backingSize.height) + let imageRect = NSMakeRect(floorToScreenPixels(System.backingScale, (size.width - icon.backingSize.width) / 2), floorToScreenPixels(System.backingScale, (size.height - icon.backingSize.height) / 2), icon.backingSize.width, icon.backingSize.height) ctx.draw(icon, in: imageRect) })! } +private func generateChatFailed(backgroundColor: NSColor, border: NSColor, foregroundColor: NSColor) -> CGImage { + return generateImage(NSMakeSize(38, 38), contextGenerator: { size, ctx in + ctx.clear(CGRect(origin: CGPoint(), size: size)) + ctx.setFillColor(backgroundColor.cgColor) + ctx.fillEllipse(in: CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: CGSize(width: size.width - 2.0, height: size.height - 2.0))) + ctx.setLineWidth(1.0) + ctx.setStrokeColor(border.withAlphaComponent(0.7).cgColor) + //ctx.strokeEllipse(in: CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: CGSize(width: size.width - 2.0, height: size.height - 2.0))) + + let icon = NSImage(named: "Icon_DialogSendingError")!.precomposed(foregroundColor) + let imageRect = NSMakeRect(floorToScreenPixels(System.backingScale, (size.width - icon.backingSize.width) / 2), floorToScreenPixels(System.backingScale, (size.height - icon.backingSize.height) / 2), icon.backingSize.width, icon.backingSize.height) + + ctx.draw(icon, in: imageRect) + })! +} + + +private func generateSettingsIcon(_ icon: CGImage) -> CGImage { + return generateImage(icon.backingSize, contextGenerator: { size, ctx in + ctx.clear(CGRect(origin: CGPoint(), size: size)) + ctx.setFillColor(.white) + ctx.fill(CGRect(origin: CGPoint(x: 2, y: 2), size: NSMakeSize(size.width - 4, size.height - 4))) + ctx.draw(icon, in: CGRect(origin: CGPoint(), size: size)) + })! +} + + +private func generateSettingsActiveIcon(_ icon: CGImage, background: NSColor) -> CGImage { + return generateImage(icon.backingSize, contextGenerator: { size, ctx in + ctx.clear(CGRect(origin: CGPoint(), size: size)) + ctx.setFillColor(background.cgColor) + ctx.fill(CGRect(origin: CGPoint(x: 2, y: 2), size: NSMakeSize(size.width - 4, size.height - 4))) + ctx.draw(icon, in: CGRect(origin: CGPoint(), size: size)) + })! +} + +private func generateStickersEmptySearch(color: NSColor) -> CGImage { + return generateImage(NSMakeSize(100, 100), contextGenerator: { size, ctx in + let rect = CGRect(origin: CGPoint(), size: size) + ctx.clear(rect) + + let icon = #imageLiteral(resourceName: "Icon_EmptySearchResults").precomposed(color) + let imageSize = icon.backingSize.fitted(size) + ctx.draw(icon, in: rect.focus(imageSize)) + }, scale: 1.0)! +} + +private func generateAlertCheckBoxSelected(backgroundColor: NSColor) -> CGImage { + return generateImage(NSMakeSize(14, 14), contextGenerator: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.setFillColor(backgroundColor.cgColor) + ctx.round(size, 2) + ctx.fill(rect) + + let icon = #imageLiteral(resourceName: "Icon_AlertCheckBoxMark").precomposed() + ctx.draw(icon, in: NSMakeRect((rect.width - icon.backingSize.width) / 2, (rect.height - icon.backingSize.height) / 2, icon.backingSize.width, icon.backingSize.height)) + + })! +} +private func generateAlertCheckBoxUnselected(border: NSColor) -> CGImage { + return generateImage(NSMakeSize(14, 14), contextGenerator: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.setStrokeColor(border.cgColor) + ctx.setLineWidth(3.0) + ctx.round(size, 2) + ctx.stroke(rect) + })! +} + + +private func generateTransparentBackground() -> CGImage { + return generateImage(NSMakeSize(20, 20), contextGenerator: { size, ctx in + ctx.clear(CGRect(origin: CGPoint(), size: size)) + ctx.setFillColor(NSColor(0xcbcbcb).cgColor) + ctx.fill(NSMakeRect(0, 0, 10, 10)) + ctx.setFillColor(NSColor(0xfdfdfd).cgColor) + ctx.fill(NSMakeRect(10, 0, 10, 10)) + + ctx.setFillColor(NSColor(0xfdfdfd).cgColor) + ctx.fill(NSMakeRect(0, 10, 10, 10)) + ctx.setFillColor(NSColor(0xcbcbcb).cgColor) + ctx.fill(NSMakeRect(10, 10, 10, 10)) + + })! +} + +private func generateLottieTransparentBackground() -> CGImage { + return generateImage(NSMakeSize(10, 10), contextGenerator: { size, ctx in + ctx.clear(CGRect(origin: CGPoint(), size: size)) + ctx.setFillColor(.black) + ctx.fill(NSMakeRect(0, 0, 5, 5)) + ctx.setFillColor(NSColor.lightGray.cgColor) + ctx.fill(NSMakeRect(5, 0, 5, 5)) + + ctx.setFillColor(NSColor.lightGray.cgColor) + ctx.fill(NSMakeRect(0, 5, 5, 5)) + ctx.setFillColor(.black) + ctx.fill(NSMakeRect(5, 5, 5, 5)) + + })! +} + +private func generateIVAudioPlay(color: NSColor) -> CGImage { + return generateImage(NSMakeSize(40, 40), contextGenerator: { size, ctx in + ctx.clear(CGRect(origin: CGPoint(), size: size)) + ctx.setStrokeColor(color.cgColor) + ctx.setLineWidth(3) + ctx.strokeEllipse(in: NSMakeRect(2, 2, size.width - 4, size.height - 4)) + let icon = #imageLiteral(resourceName: "Icon_ChatMusicPlay").precomposed(color) + + ctx.draw(icon, in: NSMakeRect(floorToScreenPixels(System.backingScale, (size.width - icon.backingSize.width) / 2), floorToScreenPixels(System.backingScale, (size.height - icon.backingSize.height) / 2), icon.backingSize.width, icon.backingSize.height)) + + })! +} + +private func generateIVAudioPause(color: NSColor) -> CGImage { + return generateImage(NSMakeSize(40, 40), contextGenerator: { size, ctx in + ctx.clear(CGRect(origin: CGPoint(), size: size)) + ctx.setStrokeColor(color.cgColor) + ctx.setLineWidth(3) + ctx.strokeEllipse(in: NSMakeRect(2, 2, size.width - 4, size.height - 4)) + let icon = #imageLiteral(resourceName: "Icon_ChatMusicPause").precomposed(color) + ctx.draw(icon, in: NSMakeRect(floorToScreenPixels(System.backingScale, (size.width - icon.backingSize.width) / 2), floorToScreenPixels(System.backingScale, (size.height - icon.backingSize.height) / 2), icon.backingSize.width, icon.backingSize.height)) + })! +} + private func generateBadgeMention(backgroundColor: NSColor, foregroundColor: NSColor) -> CGImage { - return generateImage(NSMakeSize(20, 20), contextGenerator: { size, ctx in + return generateImage(NSMakeSize(20, 20), rotatedContext: { size, ctx in ctx.clear(NSMakeRect(0, 0, size.width, size.height)) ctx.round(size, size.width/2) ctx.setFillColor(backgroundColor.cgColor) ctx.fill(NSMakeRect(0, 0, size.width, size.height)) let icon = #imageLiteral(resourceName: "Icon_ChatListMention").precomposed(foregroundColor, flipVertical: true) - let imageRect = NSMakeRect(floorToScreenPixels((size.width - icon.backingSize.width) / 2), floorToScreenPixels((size.height - icon.backingSize.height) / 2), icon.backingSize.width, icon.backingSize.height) + let imageRect = NSMakeRect(floorToScreenPixels(System.backingScale, (size.width - icon.backingSize.width) / 2), floorToScreenPixels(System.backingScale, (size.height - icon.backingSize.height) / 2), icon.backingSize.width, icon.backingSize.height) ctx.draw(icon, in: imageRect) })! } +func generateChatGroupToggleSelected(foregroundColor: NSColor, backgroundColor: NSColor) -> CGImage { + let icon = #imageLiteral(resourceName: "Icon_Check").precomposed(foregroundColor) + return generateImage(NSMakeSize(icon.backingSize.width + 2, icon.backingSize.height + 2), contextGenerator: { size, ctx in + ctx.clear(NSMakeRect(0, 0, size.width, size.height)) + ctx.round(size, size.width/2) + ctx.setFillColor(backgroundColor.cgColor) + ctx.fill(NSMakeRect(0, 0, size.width, size.height)) + let imageRect = NSMakeRect((size.width - icon.backingSize.width) / 2, (size.height - icon.backingSize.height) / 2, icon.backingSize.width, icon.backingSize.height) + ctx.draw(icon, in: imageRect) + }, scale: 2)! +} + +private func generateChatGroupToggleSelectionForeground(foregroundColor: NSColor, backgroundColor: NSColor) -> CGImage { + let icon = #imageLiteral(resourceName: "Icon_SelectionUncheck").precomposed(foregroundColor) + return generateImage(NSMakeSize(icon.backingSize.width + 4, icon.backingSize.height + 4), contextGenerator: { size, ctx in + ctx.clear(NSMakeRect(0, 0, size.width, size.height)) + ctx.round(size, size.width/2) + ctx.setFillColor(backgroundColor.cgColor) + ctx.fill(NSMakeRect(0, 0, size.width, size.height)) + let imageRect = NSMakeRect((size.width - icon.backingSize.width) / 2, (size.height - icon.backingSize.height) / 2, icon.backingSize.width, icon.backingSize.height) + ctx.draw(icon, in: imageRect) + }, scale: 2)! +} + +func generateChatGroupToggleUnselected(foregroundColor: NSColor, backgroundColor: NSColor) -> CGImage { + let icon = #imageLiteral(resourceName: "Icon_SelectionUncheck").precomposed(foregroundColor) + return generateImage(NSMakeSize(icon.backingSize.width, icon.backingSize.height), contextGenerator: { size, ctx in + ctx.clear(NSMakeRect(0, 0, size.width, size.height)) + ctx.round(size, size.width/2) + ctx.setFillColor(backgroundColor.cgColor) + ctx.fill(NSMakeRect(0, 0, size.width, size.height)) + let imageRect = NSMakeRect((size.width - icon.backingSize.width) / 2, (size.height - icon.backingSize.height) / 2, icon.backingSize.width, icon.backingSize.height) + ctx.draw(icon, in: imageRect) + }, scale: 2)! +} +func generateAvatarPlaceholder(foregroundColor: NSColor, size: NSSize) -> CGImage { + return generateImage(size, contextGenerator: { size, ctx in + ctx.clear(NSMakeRect(0, 0, size.width, size.height)) + ctx.round(size, size.width/2) + ctx.setFillColor(foregroundColor.cgColor) + ctx.fill(NSMakeRect(0, 0, size.width, size.height)) + }, scale: 1.0)! +} private func deleteItemIcon(_ color: NSColor) -> CGImage { return generateImage(NSMakeSize(24,24), contextGenerator: { (size, ctx) in @@ -78,29 +950,26 @@ private func generateSendingFrame(_ color: NSColor) -> CGImage { ctx.strokeEllipse(in: NSMakeRect(1.0, 1.0,size.width - 2,size.height - 2)) })! } -private func generateSendingHour(_ color: NSColor) -> CGImage { - return generateImage(NSMakeSize(12, 12), contextGenerator: { size, ctx in - ctx.clear(NSMakeRect(0, 0, size.width, size.height)) - ctx.setFillColor(color.cgColor) - ctx.fill(NSMakeRect(5,5,4,1.5)) - })! -} -private func generateSendingMin(_ color: NSColor) -> CGImage { - return generateImage(NSMakeSize(12, 12), contextGenerator: { size, ctx in - ctx.clear(NSMakeRect(0, 0, size.width, size.height)) - ctx.setFillColor(color.cgColor) - ctx.fill(NSMakeRect(5, 5, 4, 1)) +private func generateClockMinImage(_ color: NSColor) -> CGImage { + return generateImage(CGSize(width: 10, height: 10), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(color.cgColor) + let strokeWidth: CGFloat = 1 + context.fill(CGRect(x: (10 - strokeWidth) / 2.0, y: (10 - strokeWidth) / 2.0, width: 10 / 2.0 - strokeWidth, height: strokeWidth)) })! } -private func generateChatScrolldownImage(backgroundColor: NSColor, borderColor: NSColor, arrowColor: NSColor) -> CGImage { + +private func generateChatScrolldownImage(backgroundColor: NSColor, borderColor: NSColor, arrowColor: NSColor) -> CGImage { return generateImage(CGSize(width: 38.0, height: 38.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(backgroundColor.cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: CGSize(width: size.width - 2.0, height: size.height - 2.0))) context.setLineWidth(1.0) - context.setStrokeColor(borderColor.withAlphaComponent(0.7).cgColor) - context.strokeEllipse(in: CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: CGSize(width: size.width - 2.0, height: size.height - 2.0))) + if borderColor != .clear { + context.setStrokeColor(borderColor.withAlphaComponent(0.7).cgColor) + context.strokeEllipse(in: CGRect(origin: CGPoint(x: 1.0, y: 1.0), size: CGSize(width: size.width - 2.0, height: size.height - 2.0))) + } context.setStrokeColor(arrowColor.cgColor) context.setLineWidth(1.0) @@ -113,6 +982,55 @@ private func generateChatScrolldownImage(backgroundColor: NSColor, borderColor: })! } +private func generateConfirmDeleteMessagesAccessory(backgroundColor: NSColor) -> CGImage { + return generateImage(NSMakeSize(50, 50), contextGenerator: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.round(size, size.height / 2) + ctx.setFillColor(backgroundColor.cgColor) + ctx.fill(rect) + let icon = #imageLiteral(resourceName: "Icon_ConfirmDeleteMessagesAccessory").precomposed() + let point = NSMakePoint((rect.width - icon.backingSize.width) / 2, (rect.height - icon.backingSize.height) / 2) + ctx.draw(icon, in: NSMakeRect(point.x, point.y, icon.backingSize.width, icon.backingSize.height)) + })! +} + +private func generateConfirmPinAccessory(backgroundColor: NSColor) -> CGImage { + return generateImage(NSMakeSize(50, 50), contextGenerator: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.round(size, size.height / 2) + ctx.setFillColor(backgroundColor.cgColor) + ctx.fill(rect) + let icon = #imageLiteral(resourceName: "Icon_ConfirmPinAccessory").precomposed() + let point = NSMakePoint((rect.width - icon.backingSize.width) / 2, (rect.height - icon.backingSize.height) / 2) + ctx.draw(icon, in: NSMakeRect(point.x, point.y, icon.backingSize.width, icon.backingSize.height)) + })! +} + +private func generateConfirmDeleteChatAccessory(backgroundColor: NSColor, foregroundColor: NSColor) -> CGImage { + return generateImage(NSMakeSize(34, 34), contextGenerator: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.round(size, size.height / 2) + + ctx.setFillColor(backgroundColor.cgColor) + ctx.fill(rect) + + ctx.setFillColor(foregroundColor.cgColor) + ctx.fillEllipse(in: NSMakeRect(2, 2, size.width - 4, size.height - 4)) + let icon = #imageLiteral(resourceName: "Icon_ConfirmDeleteChatAccessory").precomposed() + let point = NSMakePoint((rect.width - icon.backingSize.width) / 2, (rect.height - icon.backingSize.height) / 2) + ctx.draw(icon, in: NSMakeRect(point.x, point.y, icon.backingSize.width, icon.backingSize.height)) + })! +} + + + +/* + + */ + private func generateRecentActionsTriangle(_ color: NSColor) -> CGImage { return generateImage(NSMakeSize(10, 8), contextGenerator: { (size, ctx) in let bounds = NSMakeRect(0, 0, size.width, size.height) @@ -130,7 +1048,7 @@ private func generateRecentActionsTriangle(_ color: NSColor) -> CGImage { } var blueActionButton:ControlStyle { - return ControlStyle(font: NSFont.normal(.title), foregroundColor: theme.colors.blueUI) + return ControlStyle(font: NSFont.normal(.title), foregroundColor: theme.colors.accent) } var redActionButton:ControlStyle { return ControlStyle(font: .normal(.title), foregroundColor: theme.colors.redUI) @@ -138,19 +1056,25 @@ var redActionButton:ControlStyle { -class ActivitiesTheme { +struct ActivitiesTheme : Equatable { let text:[CGImage] let uploading:[CGImage] let recording:[CGImage] + let choosingSticker:[CGImage] let textColor:NSColor let backgroundColor:NSColor - init(text:[CGImage], uploading:[CGImage], recording:[CGImage], textColor:NSColor, backgroundColor:NSColor) { + init(text:[CGImage], uploading:[CGImage], recording:[CGImage], choosingSticker: [CGImage], textColor:NSColor, backgroundColor:NSColor) { self.text = text self.uploading = uploading self.recording = recording + self.choosingSticker = choosingSticker self.textColor = textColor self.backgroundColor = backgroundColor } + + static func ==(lhs: ActivitiesTheme, rhs: ActivitiesTheme) -> Bool { + return lhs.textColor.argb == rhs.textColor.argb && lhs.backgroundColor.argb == rhs.backgroundColor.argb + } } @@ -177,264 +1101,6 @@ final class TelegramTabBarTheme { } - - -struct TelegramIconsTheme { - let dialogMuteImage: CGImage - let dialogMuteImageSelected: CGImage - let outgoingMessageImage: CGImage - let readMessageImage: CGImage - let outgoingMessageImageSelected: CGImage - let readMessageImageSelected: CGImage - let sendingImage: CGImage - let sendingImageSelected: CGImage - let secretImage:CGImage - let secretImageSelected: CGImage - let pinnedImage: CGImage - let pinnedImageSelected: CGImage - let verifiedImage: CGImage - let verifiedImageSelected: CGImage - let errorImage: CGImage - let errorImageSelected: CGImage - - let chatSearch: CGImage - let chatCall: CGImage - let chatActions: CGImage - - let chatOutgoingFailedCall: CGImage - let chatIncomingFailedCall: CGImage - let chatOutgoingCall: CGImage - let chatIncomingCall: CGImage - let chatFallbackCall: CGImage - - let chatToggleSelected: CGImage - let chatToggleUnselected: CGImage - let chatShare: CGImage - let chatMusicPlay: CGImage - let chatMusicPause: CGImage - - let composeNewChat:CGImage - let composeNewChatActive: CGImage - let composeNewGroup:CGImage - let composeNewSecretChat: CGImage - let composeNewChannel: CGImage - - let contactsNewContact: CGImage - - let chatReadMark1: CGImage - let chatReadMark2: CGImage - let sentFailed: CGImage - let chatChannelViews:CGImage - - let chatNavigationBack: CGImage - - let peerInfoAddMember: CGImage - - - let chatSearchUp: CGImage - let chatSearchUpDisabled: CGImage - let chatSearchDown: CGImage - let chatSearchDownDisabled: CGImage - let chatSearchCalendar: CGImage - - let dismissAccessory: CGImage - - let chatScrollUp: CGImage - let chatScrollUpActive: CGImage - - - let audioPlayerPlay: CGImage - let audioPlayerPause: CGImage - let audioPlayerNext: CGImage - let audioPlayerPrev: CGImage - let auduiPlayerDismiss: CGImage - let audioPlayerRepeat: CGImage - let audioPlayerRepeatActive: CGImage - - let audioPlayerLockedPlay: CGImage - let audioPlayerLockedNext: CGImage - let audioPlayerLockedPrev: CGImage - - - - let chatSendMessage: CGImage - let chatRecordVoice: CGImage - let chatEntertainment: CGImage - let chatInlineDismiss: CGImage - let chatActiveReplyMarkup: CGImage - let chatDisabledReplyMarkup: CGImage - let chatSecretTimer: CGImage - - let chatForwardMessagesActive: CGImage - let chatForwardMessagesInactive: CGImage - let chatDeleteMessagesActive: CGImage - let chatDeleteMessagesInactive: CGImage - - let generalNext: CGImage - let generalSelect: CGImage - - - let chatVoiceRecording: CGImage - let chatVideoRecording: CGImage - let chatRecord: CGImage - - let deleteItem: CGImage - let deleteItemDisabled: CGImage - - let chatAttach: CGImage - let chatAttachFile: CGImage - let chatAttachPhoto: CGImage - let chatAttachCamera: CGImage - let chatAttachLocation: CGImage - - let mediaEmptyShared: CGImage - let mediaEmptyFiles: CGImage - let mediaEmptyMusic: CGImage - let mediaEmptyLinks: CGImage - - let mediaDropdown: CGImage - - let stickersAddFeatured: CGImage - let stickersAddedFeatured: CGImage - let stickersRemove: CGImage - - let peerMediaDownloadFileStart: CGImage - let peerMediaDownloadFilePause: CGImage - - let stickersShare: CGImage - - let emojiRecentTab: CGImage - let emojiSmileTab: CGImage - let emojiNatureTab: CGImage - let emojiFoodTab: CGImage - let emojiSportTab: CGImage - let emojiCarTab: CGImage - let emojiObjectsTab: CGImage - let emojiSymbolsTab: CGImage - let emojiFlagsTab: CGImage - - let emojiRecentTabActive: CGImage - let emojiSmileTabActive: CGImage - let emojiNatureTabActive: CGImage - let emojiFoodTabActive: CGImage - let emojiSportTabActive: CGImage - let emojiCarTabActive: CGImage - let emojiObjectsTabActive: CGImage - let emojiSymbolsTabActive: CGImage - let emojiFlagsTabActive: CGImage - - let stickerBackground: CGImage - let stickerBackgroundActive: CGImage - let stickersTabRecent: CGImage - let stickersTabGIF: CGImage - - let chatSendingFrame: CGImage - let chatSendingHour: CGImage - let chatSendingMin: CGImage - let chatActionUrl: CGImage - - let callInlineDecline: CGImage - let callInlineMuted: CGImage - let callInlineUnmuted: CGImage - let eventLogTriangle: CGImage - let channelIntro: CGImage - let chatFileThumb: CGImage - let chatSecretThumb: CGImage - let chatMapPin: CGImage - let chatSecretTitle: CGImage - let emptySearch: CGImage - let calendarBack: CGImage - let calendarNext: CGImage - let calendarBackDisabled: CGImage - let calendarNextDisabled: CGImage - let newChatCamera: CGImage - let peerInfoVerify: CGImage - let peerInfoCall: CGImage - let callOutgoing: CGImage - let recentDismiss: CGImage - let recentDismissActive: CGImage - let webgameShare: CGImage - - let chatSearchCancel: CGImage - let chatSearchFrom: CGImage - - let callWindowDecline: CGImage - let callWindowAccept: CGImage - let callWindowMute: CGImage - let callWindowUnmute: CGImage - let callWindowClose: CGImage - let callWindowDeviceSettings: CGImage - let callWindowCancel: CGImage - - let chatActionEdit: CGImage - let chatActionInfo: CGImage - let chatActionMute: CGImage - let chatActionUnmute: CGImage - let chatActionClearHistory: CGImage - - let dismissPinned: CGImage - let chatActionsActive: CGImage - let chatEntertainmentSticker: CGImage - let chatEmpty: CGImage - let stickerPackClose: CGImage - let stickerPackDelete: CGImage - - let modalShare: CGImage - let modalClose: CGImage - - let ivChannelJoined: CGImage - let chatListMention: CGImage - let chatListMentionActive: CGImage - - let chatMention: CGImage - let chatMentionActive: CGImage - - let sliderControl: CGImage - let sliderControlActive: CGImage - - let stickersTabFave: CGImage - let chatInstantView: CGImage - - let instantViewShare: CGImage - let instantViewActions: CGImage - let instantViewActionsActive: CGImage - let instantViewSafari: CGImage - let instantViewBack: CGImage - let instantViewCheck: CGImage - - let groupStickerNotFound: CGImage - - let settingsAskQuestion: CGImage - let settingsBio: CGImage - let settingsEditInfo: CGImage - let settingsFaq: CGImage - let settingsGeneral: CGImage - let settingsLanguage: CGImage - let settingsNotifications: CGImage - let settingsPhoneNumber: CGImage - let settingsSecurity: CGImage - let settingsStickers: CGImage - let settingsStorage: CGImage - let settingsUsername: CGImage - - let generalCheck: CGImage - let settingsAbout: CGImage - let settingsLogout: CGImage - - let fastSettingsLock: CGImage - let fastSettingsDark: CGImage - let fastSettingsSunny: CGImage - let fastSettingsMute: CGImage - let fastSettingsUnmute: CGImage - - let chatRecordVideo: CGImage - - let inputChannelMute: CGImage - let inputChannelUnmute: CGImage - - let changePhoneNumberIntro: CGImage -} - final class TelegramChatListTheme { let selectedBackgroundColor: NSColor let singleLayoutSelectedBackgroundColor: NSColor @@ -463,6 +1129,9 @@ final class TelegramChatListTheme { init(selectedBackgroundColor: NSColor, singleLayoutSelectedBackgroundColor: NSColor, activeDraggingBackgroundColor: NSColor, pinnedBackgroundColor: NSColor, contextMenuBackgroundColor: NSColor, textColor: NSColor, grayTextColor: NSColor, secretChatTextColor: NSColor, peerTextColor: NSColor, activityColor: NSColor, activitySelectedColor: NSColor, activityContextMenuColor: NSColor, activityPinnedColor: NSColor, badgeTextColor: NSColor, badgeBackgroundColor: NSColor, badgeSelectedTextColor: NSColor, badgeSelectedBackgroundColor: NSColor, badgeMutedTextColor: NSColor, badgeMutedBackgroundColor: NSColor) { + + + self.selectedBackgroundColor = selectedBackgroundColor self.singleLayoutSelectedBackgroundColor = singleLayoutSelectedBackgroundColor self.activeDraggingBackgroundColor = activeDraggingBackgroundColor @@ -488,36 +1157,743 @@ final class TelegramChatListTheme { } } -extension ColorPallete { - init(_ settings: ThemePalleteSettings) { - self.init(background: settings.background, text: settings.text, grayText: settings.grayText, link: settings.link, blueUI: settings.blueUI, redUI: settings.redUI, greenUI: settings.greenUI, blackTransparent: settings.blackTransparent, grayTransparent: settings.grayTransparent, grayUI: settings.grayUI, darkGrayText: settings.darkGrayText, blueText: settings.blueText, blueSelect: settings.blueSelect, selectText: settings.selectText, blueFill: settings.blueFill, border: settings.border, grayBackground: settings.grayBackground, grayForeground: settings.grayForeground, grayIcon: settings.grayIcon, blueIcon: settings.blueIcon, badgeMuted: settings.badgeMuted, badge: settings.badge, indicatorColor: settings.indicatorColor, selectMessage: settings.selectMessage) + + +extension WallpaperSettings { + func withUpdatedBlur(_ blur: Bool) -> WallpaperSettings { + return WallpaperSettings(blur: blur, motion: self.motion, colors: self.colors, intensity: self.intensity) + } + func withUpdatedColor(_ color: UInt32?) -> WallpaperSettings { + return WallpaperSettings(blur: self.blur, motion: self.motion, colors: color != nil ? [color!] : [], intensity: self.intensity) + } + + func isSemanticallyEqual(to other: WallpaperSettings) -> Bool { + return self.colors == other.colors && self.intensity == other.intensity } } -extension TelegramPresentationTheme { - var appearance: NSAppearance? { - return dark ? NSAppearance(named: NSAppearance.Name.vibrantDark) : NSAppearance(named: NSAppearance.Name.vibrantLight) +enum Wallpaper : Equatable, PostboxCoding { + case builtin + case color(UInt32) + case gradient(Int64?, [UInt32], Int32?) + case image([TelegramMediaImageRepresentation], settings: WallpaperSettings) + case file(slug: String, file: TelegramMediaFile, settings: WallpaperSettings, isPattern: Bool) + case none + case custom(TelegramMediaImageRepresentation, blurred: Bool) + + init(_ wallpaper: TelegramWallpaper) { + switch wallpaper { + case .builtin: + self = .builtin + case let .color(color): + self = .color(color) + case let .image(image, settings): + self = .image(image, settings: settings) + case let .file(values): + self = .file(slug: values.slug, file: values.file, settings: values.settings, isPattern: values.isPattern) + case let .gradient(gradient): + self = .gradient(gradient.id, gradient.colors, gradient.settings.rotation) + } + } + + static func ==(lhs: Wallpaper, rhs: Wallpaper) -> Bool { + switch lhs { + case .builtin: + if case .builtin = rhs { + return true + } else { + return false + } + case let .color(value): + if case .color(value) = rhs { + return true + } else { + return false + } + case let .gradient(id, colors, rotation): + if case .gradient(id, colors, rotation) = rhs { + return true + } else { + return false + } + case let .image(reps, settings): + if case .image(reps, settings: settings) = rhs { + return true + } else { + return false + } + case let .file(slug, lhsFile, settings, isPattern): + if case .file(slug, let rhsFile, settings, isPattern) = rhs, lhsFile.isSemanticallyEqual(to: rhsFile) { + return true + } else { + return false + } + case let .custom(rep, blurred): + if case .custom(rep, blurred) = rhs { + return true + } else { + return false + } + case .none: + if case .none = rhs { + return true + } else { + return false + } + } + } + + var wallpaperUrl: String? { + switch self { + case .builtin: + return "builtin" + case let .file(slug, _, settings, isPattern): + var options: [String] = [] + if settings.blur { + options.append("mode=blur") + } + if isPattern { + if let pattern = settings.colors.first { + var color = NSColor(argb: pattern).withAlphaComponent(1.0).hexString.lowercased() + color = String(color[color.index(after: color.startIndex) ..< color.endIndex]) + options.append("bg_color=\(color)") + } + if let intensity = settings.intensity { + options.append("intensity=\(intensity)") + } + } + var optionsString = "" + if !options.isEmpty { + optionsString = "?\(options.joined(separator: "&"))" + } + return "https://t.me/bg/\(slug)\(optionsString)" + default: + return nil + } + } + + public init(decoder: PostboxDecoder) { + switch decoder.decodeInt32ForKey("v", orElse: 0) { + case 0: + self = .builtin + case 1: + self = .color(UInt32(bitPattern: decoder.decodeInt32ForKey("c", orElse: 0))) + case 2: + let settings = decoder.decodeObjectForKey("settings", decoder: { WallpaperSettings(decoder: $0) }) as? WallpaperSettings ?? WallpaperSettings() + self = .image(decoder.decodeObjectArrayWithDecoderForKey("i"), settings: settings) + case 3: + let settings = decoder.decodeObjectForKey("settings", decoder: { WallpaperSettings(decoder: $0) }) as? WallpaperSettings ?? WallpaperSettings() + self = .file(slug: decoder.decodeStringForKey("slug", orElse: ""), file: decoder.decodeObjectForKey("file", decoder: { TelegramMediaFile(decoder: $0) }) as! TelegramMediaFile, settings: settings, isPattern: decoder.decodeInt32ForKey("p", orElse: 0) == 1) + case 4: + self = .custom(decoder.decodeObjectForKey("rep", decoder: { TelegramMediaImageRepresentation(decoder: $0) }) as! TelegramMediaImageRepresentation, blurred: decoder.decodeInt32ForKey("b", orElse: 0) == 1) + case 5: + self = .none + case 6: + var colors = decoder.decodeInt32ArrayForKey("c").map { UInt32(bitPattern: $0) } + + if colors.isEmpty { + colors = [UInt32(bitPattern: decoder.decodeInt32ForKey("ct", orElse: 0)), UInt32(bitPattern: decoder.decodeInt32ForKey("cb", orElse: 0))] + } + self = .gradient(decoder.decodeOptionalInt64ForKey("id"), colors, decoder.decodeOptionalInt32ForKey("cr")) + + default: + assertionFailure() + self = .color(0xffffff) + } + } + + + public func encode(_ encoder: PostboxEncoder) { + switch self { + case .builtin: + encoder.encodeInt32(0, forKey: "v") + case let .color(color): + encoder.encodeInt32(1, forKey: "v") + encoder.encodeInt32(Int32(bitPattern: color), forKey: "c") + case let .image(representations, settings): + encoder.encodeInt32(2, forKey: "v") + encoder.encodeObjectArray(representations, forKey: "i") + encoder.encodeObject(settings, forKey: "settings") + case let .file(slug, file, settings, isPattern): + encoder.encodeInt32(3, forKey: "v") + encoder.encodeString(slug, forKey: "slug") + encoder.encodeObject(file, forKey: "file") + encoder.encodeObject(settings, forKey: "settings") + encoder.encodeInt32(isPattern ? 1 : 0, forKey: "p") + case let .custom(resource, blurred): + encoder.encodeInt32(4, forKey: "v") + encoder.encodeObject(resource, forKey: "rep") + encoder.encodeInt32(blurred ? 1 : 0, forKey: "b") + case .none: + encoder.encodeInt32(5, forKey: "v") + case let .gradient(id, colors, rotation): + encoder.encodeInt32(6, forKey: "v") + encoder.encodeInt32Array(colors.map { Int32(bitPattern: $0) }, forKey: "c") + if let rotation = rotation { + encoder.encodeInt32(rotation, forKey: "cr") + } else { + encoder.encodeNil(forKey: "cr") + } + if let id = id { + encoder.encodeInt64(id, forKey: "id") + } else { + encoder.encodeNil(forKey: "id") + } + } + } + + func withUpdatedBlurrred(_ blurred: Bool) -> Wallpaper { + switch self { + case .builtin: + return self + case .color: + return self + case .gradient: + return self + case let .image(representations, settings): + return .image(representations, settings: WallpaperSettings(blur: blurred, motion: settings.motion, colors: settings.colors, intensity: settings.intensity, rotation: settings.rotation)) + case let .file(values): + return .file(slug: values.slug, file: values.file, settings: WallpaperSettings(blur: blurred, motion: settings.motion, colors: settings.colors, intensity: settings.intensity, rotation: settings.rotation), isPattern: values.isPattern) + case let .custom(path, _): + return .custom(path, blurred: blurred) + case .none: + return self + } + } + + func withUpdatedSettings(_ settings: WallpaperSettings) -> Wallpaper { + switch self { + case .builtin: + return self + case .color: + return self + case .gradient: + return self + case let .image(representations, _): + return .image(representations, settings: settings) + case let .file(values): + return .file(slug: values.slug, file: values.file, settings: settings, isPattern: values.isPattern) + case .custom: + return self + case .none: + return self + } + } + + var isBlurred: Bool { + switch self { + case .builtin: + return false + case .color: + return false + case .gradient: + return false + case let .image(_, settings): + return settings.blur + case let .file(values): + return values.settings.blur + case let .custom(_, blurred): + return blurred + case .none: + return false + } + } + + var settings: WallpaperSettings { + switch self { + case let .image(_, settings): + return settings + case let .file(values): + return values.settings + case let .color(t): + return WallpaperSettings(colors: [t]) + case let .gradient(_, colors, r): + return WallpaperSettings(colors: colors, rotation: r) + default: + return WallpaperSettings() + } + } + + func isSemanticallyEqual(to other: Wallpaper) -> Bool { + switch self { + case .none: + return other == self + case .builtin: + return other == self + case .color: + return other == self + case .gradient: + return other == self + case let .custom(resource, _): + if case .custom(resource, _) = other { + return true + } else { + return false + } + case let .image(representations, _): + if case .image(representations, _) = other { + return true + } else { + return false + } + case let .file(values): + if case .file(slug: values.slug, _, _, _) = other { + return true + } else { + return false + } + } } } +func getAverageColor(_ image: NSImage) -> NSColor { + let context = DrawingContext(size: CGSize(width: 1.0, height: 1.0), scale: 1.0, clear: false) + context.withFlippedContext({ [weak image] context in + if let cgImage = image { + context.draw(cgImage.cgImage(forProposedRect: nil, context: nil, hints: nil)!, in: CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0)) + } + }) + var color = context.colorAt(CGPoint()) + + var hue: CGFloat = 0.0 + var saturation: CGFloat = 0.0 + var brightness: CGFloat = 0.0 + var alpha: CGFloat = 0.0 +// color = color.usingColorSpaceName(NSColorSpaceName.deviceRGB)! + color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) + saturation = min(1.0, saturation + 0.1 + 0.1 * (1.0 - saturation)) + brightness = max(0.0, brightness * 0.65) + alpha = 0.5 + color = NSColor(hue: hue, saturation: saturation, brightness: brightness, alpha: alpha) + + return color +} + +func getAverageColor(_ color: NSColor) -> NSColor { + var hue: CGFloat = 0.0 + var saturation: CGFloat = 0.0 + var brightness: CGFloat = 0.0 + var alpha: CGFloat = 0.0 + let color = color.usingColorSpaceName(NSColorSpaceName.deviceRGB)! + color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) + saturation = min(1.0, saturation + 0.1 + 0.1 * (1.0 - saturation)) + brightness = max(0.0, brightness * 0.65) + alpha = 0.5 + + return NSColor(hue: hue, saturation: saturation, brightness: brightness, alpha: alpha) +} + +func generateBackgroundMode(_ wallpaper: Wallpaper, palette: ColorPalette, maxSize: NSSize = NSMakeSize(1040, 1580)) -> TableBackgroundMode { + #if !SHARE + var backgroundMode: TableBackgroundMode + switch wallpaper { + case .builtin: + backgroundMode = TelegramPresentationTheme.defaultBackground + case let.color(color): + backgroundMode = .color(color: NSColor(color)) + case let .gradient(_, colors, rotation): + backgroundMode = .gradient(colors: colors.map({ NSColor(argb: $0).withAlphaComponent(1.0) }), rotation: rotation) + case let .image(representation, settings): + if let resource = largestImageRepresentation(representation)?.resource, let image = NSImage(contentsOf: URL(fileURLWithPath: wallpaperPath(resource, settings: settings))) { + backgroundMode = .background(image: image, intensity: settings.intensity, colors: settings.colors.map { NSColor(argb: $0) }, rotation: settings.rotation) + } else { + backgroundMode = TelegramPresentationTheme.defaultBackground + } + + case let .file(_, file, settings, _): + if let image = NSImage(contentsOf: URL(fileURLWithPath: wallpaperPath(file.resource, settings: settings))) { + + let intense = CGFloat(abs(settings.intensity ?? 0)) / 100 + + var image = image + if presentation.colors.isDark, settings.colors.count > 1 { + image = generateImage(image.size, contextGenerator: { size, ctx in + ctx.clear(size.bounds) + ctx.setFillColor(NSColor.black.cgColor) + ctx.fill(size.bounds) + ctx.clip(to: size.bounds, mask: image._cgImage!) + + ctx.clear(size.bounds) + ctx.setFillColor(NSColor.black.withAlphaComponent(1 - intense).cgColor) + ctx.fill(size.bounds) + })!._NSImage + } + + backgroundMode = .background(image: image, intensity: settings.intensity, colors: settings.colors.map { NSColor(argb: $0) }, rotation: settings.rotation) + } else { + backgroundMode = TelegramPresentationTheme.defaultBackground + } + case .none: + backgroundMode = .color(color: palette.chatBackground) + case let .custom(representation, blurred): + if let image = NSImage(contentsOf: URL(fileURLWithPath: wallpaperPath(representation.resource, settings: WallpaperSettings(blur: blurred)))) { + backgroundMode = .background(image: image, intensity: nil, colors: nil, rotation: nil) + } else { + backgroundMode = TelegramPresentationTheme.defaultBackground + } + } + return backgroundMode + #else + return .plain + #endif +} +#if !SHARE +private func builtinBackgound() -> NSImage { + var data = try! Data(contentsOf: Bundle.main.url(forResource: "builtin-wallpaper-svg", withExtension: nil)!) + data = TGGUnzipData(data, 8 * 1024 * 1024)! + return drawSvgImageNano(data, NSMakeSize(400, 800))! +} +#endif class TelegramPresentationTheme : PresentationTheme { let chatList:TelegramChatListTheme + #if !SHARE + let chat: TelegramChatColors + #endif + let cloudTheme: TelegramTheme? let tabBar:TelegramTabBarTheme let icons: TelegramIconsTheme - let dark: Bool + let bubbled: Bool + let wallpaper: ThemeWallpaper + + #if !SHARE + static let defaultBackground: TableBackgroundMode = .background(image: builtinBackgound(), intensity: nil, colors: [0xdbddbb, 0x6ba587, 0xd5d88d, 0x88b884].map { .init(argb: $0) }, rotation: nil) + #endif + + + private var _emptyChatNavigationPrev: CGImage? + private var _emptyChatNavigationNext: CGImage? + var emptyChatNavigationPrev: CGImage { + if let icon = _emptyChatNavigationPrev { + return icon + } else { + let new = NSImage(named: "Icon_GeneralNext")!.precomposed(self.chatServiceItemTextColor, flipHorizontal: true) + _emptyChatNavigationPrev = new + return new + } + } + var emptyChatNavigationNext: CGImage { + if let icon = _emptyChatNavigationNext { + return icon + } else { + let new = NSImage(named: "Icon_GeneralNext")!.precomposed(self.chatServiceItemTextColor) + _emptyChatNavigationNext = new + return new + } + } + + private var _chatReadMarkServiceOverlayBubble1: CGImage? + private var _chatReadMarkServiceOverlayBubble2: CGImage? + var chatReadMarkServiceOverlayBubble1: CGImage { + if let icon = _chatReadMarkServiceOverlayBubble1 { + return icon + } else { + let new = NSImage(named: "Icon_MessageCheckMark1")!.precomposed(self.chatServiceItemTextColor) + _chatReadMarkServiceOverlayBubble1 = new + return new + } + } + var chatReadMarkServiceOverlayBubble2: CGImage { + if let icon = _chatReadMarkServiceOverlayBubble2 { + return icon + } else { + let new = NSImage(named: "Icon_MessageCheckmark2")!.precomposed(self.chatServiceItemTextColor) + _chatReadMarkServiceOverlayBubble2 = new + return new + } + } + + private var _chatSendingOverlayServiceFrame: CGImage? + private var _chatSendingOverlayServiceHour: CGImage? + private var _chatSendingOverlayServiceMin: CGImage? + var chatSendingOverlayServiceFrame: CGImage { + if let icon = _chatSendingOverlayServiceFrame { + return icon + } else { + let new = generateSendingFrame(self.chatServiceItemTextColor) + _chatSendingOverlayServiceFrame = new + return new + } + } + var chatSendingOverlayServiceHour: CGImage { + if let icon = _chatSendingOverlayServiceHour { + return icon + } else { + let new = generateClockMinImage(self.chatServiceItemTextColor) + _chatSendingOverlayServiceHour = new + return new + } + } + var chatSendingOverlayServiceMin: CGImage { + if let icon = _chatSendingOverlayServiceMin { + return icon + } else { + let new = generateClockMinImage(self.chatServiceItemTextColor) + _chatSendingOverlayServiceMin = new + return new + } + } + + private var _chatChannelViewsOverlayServiceBubble: CGImage? + var chatChannelViewsOverlayServiceBubble: CGImage { + if let icon = _chatChannelViewsOverlayServiceBubble { + return icon + } else { + let new = #imageLiteral(resourceName: "Icon_ChannelViews").precomposed(self.chatServiceItemTextColor, flipVertical: true) + _chatChannelViewsOverlayServiceBubble = new + return new + } + } + + private var _chat_pinned_message_overlay_service_bubble: CGImage? + var chat_pinned_message_overlay_service_bubble: CGImage { + if let icon = _chat_pinned_message_overlay_service_bubble { + return icon + } else { + let new = NSImage(named: "Icon_ChatPinnedMessage")!.precomposed(self.chatServiceItemTextColor, flipVertical: true) + _chat_pinned_message_overlay_service_bubble = new + return new + } + } + + + private var _chat_reply_count_overlay_service_bubble: CGImage? + var chat_reply_count_overlay_service_bubble: CGImage { + if let icon = _chat_reply_count_overlay_service_bubble { + return icon + } else { + let new = NSImage(named: "Icon_ChatRepliesCount")!.precomposed(self.chatServiceItemTextColor, flipVertical: true) + _chat_reply_count_overlay_service_bubble = new + return new + } + } + + private var _chat_like_inside_bubble_service: CGImage? + var chat_like_inside_bubble_service: CGImage { + if let icon = _chat_like_inside_bubble_service { + return icon + } else { + + let new = NSImage(named: "Icon_Like_MessageInside")!.precomposed(self.chatServiceItemTextColor, flipVertical: true) + _chat_like_inside_bubble_service = new + return new + } + } + private var _chat_like_inside_empty_bubble_service: CGImage? + var chat_like_inside_empty_bubble_service: CGImage { + if let icon = _chat_like_inside_empty_bubble_service { + return icon + } else { + let new = NSImage(named: "Icon_Like_MessageInsideEmpty")!.precomposed(self.chatServiceItemTextColor, flipVertical: true) + _chat_like_inside_empty_bubble_service = new + return new + } + } + + private var _chat_comments_overlay: CGImage? + var chat_comments_overlay: CGImage { + if let icon = _chat_comments_overlay { + return icon + } else { + let new = NSImage(named: "Icon_ChannelComments_Overlay")!.precomposed(self.chatServiceItemTextColor) + _chat_comments_overlay = new + return new + } + } + private var _chat_toggle_selected: CGImage? + var chat_toggle_selected: CGImage { + if let icon = _chat_toggle_selected { + return icon + } else { + let new = generateChatGroupToggleSelected(foregroundColor: colors.accentIcon, backgroundColor: colors.underSelectedColor) + _chat_toggle_selected = new + return new + } + } + private var _chat_toggle_unselected: CGImage? + var chat_toggle_unselected: CGImage { + if let icon = _chat_toggle_unselected { + return icon + } else { + let new = generateChatGroupToggleUnselected(foregroundColor: chatBackground == chatServiceItemColor ? colors.grayIcon.withAlphaComponent(0.6) : chatServiceItemColor, backgroundColor: NSColor.black.withAlphaComponent(0.05)) + _chat_toggle_unselected = new + return new + } + } + + private var _empty_chat_showtips: CGImage? + var empty_chat_showtips: CGImage { + if let icon = _empty_chat_showtips { + return icon + } else { + _empty_chat_showtips = NSImage(named: "Icon_Empty_ShowTips")!.precomposed(chatServiceItemTextColor) + return _empty_chat_showtips! + } + } + private var _empty_chat_hidetips: CGImage? + var empty_chat_hidetips: CGImage { + if let icon = _empty_chat_hidetips { + return icon + } else { + _empty_chat_hidetips = NSImage(named: "Icon_Empty_CloseTips")!.precomposed(chatServiceItemTextColor) + return _empty_chat_hidetips! + } + } + + + + private var _chatServiceItemColor: NSColor? + var chatServiceItemColor: NSColor { + if let value = _chatServiceItemColor { + return value + } else { + let chatServiceItemColor: NSColor + if bubbled { + switch backgroundMode { + case let .background(image, _, colors, _): + if let colors = colors, let first = colors.first { + let blended = colors.reduce(first, { color, with in + return color.blended(withFraction: 0.5, of: with)! + }) + chatServiceItemColor = getAverageColor(blended) + } else { + chatServiceItemColor = getAverageColor(image) + } + case let .color(color): + if color != colors.background { + chatServiceItemColor = getAverageColor(color) + } else { + chatServiceItemColor = color + } + case let .gradient(colors, _): + let blended = colors.reduce(colors.first!, { color, with in + return color.blended(withFraction: 0.5, of: with)! + }) + chatServiceItemColor = getAverageColor(blended) + + case let .tiled(image): + chatServiceItemColor = getAverageColor(image) + case .plain: + chatServiceItemColor = colors.chatBackground + } + } else { + chatServiceItemColor = colors.chatBackground + } + + self._chatServiceItemColor = chatServiceItemColor + return chatServiceItemColor + } + } + private var _chatServiceItemTextColor: NSColor? + var chatServiceItemTextColor: NSColor { + if let value = _chatServiceItemTextColor { + return value + } else { + let chatServiceItemTextColor: NSColor + if bubbled { + switch backgroundMode { + case .background: + chatServiceItemTextColor = .white + case let .color(color): + if color != colors.background { + chatServiceItemTextColor = chatServiceItemColor.brightnessAdjustedColor + } else { + chatServiceItemTextColor = colors.grayText + } + case .gradient: + chatServiceItemTextColor = chatServiceItemColor.brightnessAdjustedColor + case .tiled: + chatServiceItemTextColor = chatServiceItemColor.brightnessAdjustedColor + case .plain: + chatServiceItemTextColor = colors.grayText + } + } else { + chatServiceItemTextColor = colors.grayText + } + + self._chatServiceItemTextColor = chatServiceItemTextColor + return chatServiceItemTextColor + } + } + let fontSize: CGFloat - init(colors: ColorPallete, search: SearchTheme, chatList: TelegramChatListTheme, tabBar: TelegramTabBarTheme, icons: TelegramIconsTheme, dark: Bool, fontSize: CGFloat) { + + var controllerBackgroundMode: TableBackgroundMode { + if self.bubbled { + return self.backgroundMode + } else { + return .color(color: colors.chatBackground) + } + } + + var chatBackground: NSColor { + return self.colors.chatBackground + } + + var backgroundSize: NSSize = NSMakeSize(1040, 1580) + + private var _backgroundMode: TableBackgroundMode? + var backgroundMode: TableBackgroundMode { + if let value = _backgroundMode { + return value + } else { + let backgroundMode: TableBackgroundMode = generateBackgroundMode(wallpaper.wallpaper, palette: colors, maxSize: backgroundSize) + + self._backgroundMode = backgroundMode + return backgroundMode + } + } + init(colors: ColorPalette, cloudTheme: TelegramTheme?, search: SearchTheme, chatList: TelegramChatListTheme, tabBar: TelegramTabBarTheme, icons: TelegramIconsTheme, bubbled: Bool, fontSize: CGFloat, wallpaper: ThemeWallpaper) { self.chatList = chatList + #if !SHARE + self.chat = TelegramChatColors(colors, bubbled) + #endif self.tabBar = tabBar self.icons = icons - self.dark = dark + self.wallpaper = wallpaper + self.bubbled = bubbled self.fontSize = fontSize + self.cloudTheme = cloudTheme + super.init(colors: colors, search: search) } + var dark: Bool { + return colors.isDark + } + #if !SHARE + var insantPageThemeType: InstantPageThemeType { + if colors.isDark { + return .dark + } else { + return .light + } + } + #endif + + + deinit { + + } + + func withUpdatedColors(_ colors: ColorPalette) -> TelegramPresentationTheme { + return TelegramPresentationTheme(colors: colors, cloudTheme: self.cloudTheme, search: self.search, chatList: self.chatList, tabBar: self.tabBar, icons: generateIcons(from: colors, bubbled: self.bubbled), bubbled: self.bubbled, fontSize: self.fontSize, wallpaper: self.wallpaper) + } + func withUpdatedChatMode(_ bubbled: Bool) -> TelegramPresentationTheme { + return TelegramPresentationTheme(colors: colors, cloudTheme: self.cloudTheme, search: self.search, chatList: self.chatList, tabBar: self.tabBar, icons: generateIcons(from: colors, bubbled: bubbled), bubbled: bubbled, fontSize: self.fontSize, wallpaper: self.wallpaper) + } + + func withUpdatedBackgroundSize(_ size: NSSize) -> TelegramPresentationTheme { + self.backgroundSize = size + return self + } + + func withUpdatedWallpaper(_ wallpaper: ThemeWallpaper) -> TelegramPresentationTheme { + return TelegramPresentationTheme(colors: self.colors, cloudTheme: self.cloudTheme, search: self.search, chatList: self.chatList, tabBar: self.tabBar, icons: generateIcons(from: colors, bubbled: self.bubbled), bubbled: self.bubbled, fontSize: self.fontSize, wallpaper: wallpaper) + } + func activity(key:Int32, foregroundColor: NSColor, backgroundColor: NSColor) -> ActivitiesTheme { - return activityResources.object(key, { () -> AnyObject in - return ActivitiesTheme(text: textActivityAnimation(foregroundColor), uploading: uploadFileActivityAnimation(foregroundColor, backgroundColor), recording: recordVoiceActivityAnimation(foregroundColor), textColor: foregroundColor, backgroundColor: backgroundColor) + return activityResources.object(key, { () -> Any in + return ActivitiesTheme(text: textActivityAnimation(foregroundColor), uploading: uploadFileActivityAnimation(foregroundColor, backgroundColor), recording: recordVoiceActivityAnimation(foregroundColor), choosingSticker: choosingStickerActivityAnimation(foregroundColor), textColor: foregroundColor, backgroundColor: backgroundColor) }) as! ActivitiesTheme } @@ -527,248 +1903,720 @@ class TelegramPresentationTheme : PresentationTheme { let _themeSignal:ValuePromise = ValuePromise(ignoreRepeated: true) -var themeSignal:Signal { +var themeSignal:Signal { return _themeSignal.get() |> distinctUntilChanged |> deliverOnMainQueue } +extension ColorPalette { + var transparentBackground: NSColor { + return NSColor(patternImage: NSImage(cgImage: theme.icons.transparentBackground, size: theme.icons.transparentBackground.backingSize)) + } + var lottieTransparentBackground: NSColor { + return NSColor(patternImage: NSImage(cgImage: theme.icons.lottieTransparentBackground, size: theme.icons.lottieTransparentBackground.backingSize)) + } +} + +private func generateIcons(from palette: ColorPalette, bubbled: Bool) -> TelegramIconsTheme { + return TelegramIconsTheme(dialogMuteImage: { #imageLiteral(resourceName: "Icon_DialogMute").precomposed(palette.grayIcon) }, + dialogMuteImageSelected: { #imageLiteral(resourceName: "Icon_DialogMute").precomposed(palette.underSelectedColor) }, + outgoingMessageImage: { #imageLiteral(resourceName: "Icon_MessageCheckMark1").precomposed(palette.accentIcon, flipVertical:true) }, + readMessageImage: { #imageLiteral(resourceName: "Icon_MessageCheckmark2").precomposed(palette.accentIcon, flipVertical:true) }, + outgoingMessageImageSelected: { #imageLiteral(resourceName: "Icon_MessageCheckMark1").precomposed(palette.underSelectedColor, flipVertical:true) }, + readMessageImageSelected: { #imageLiteral(resourceName: "Icon_MessageCheckmark2").precomposed(palette.underSelectedColor, flipVertical:true) }, + sendingImage: { #imageLiteral(resourceName: "Icon_ChatStateSending").precomposed(palette.grayIcon, flipVertical:true) }, + sendingImageSelected: { #imageLiteral(resourceName: "Icon_ChatStateSending").precomposed(palette.underSelectedColor, flipVertical:true) }, + secretImage: { #imageLiteral(resourceName: "Icon_SecretChatLock").precomposed(palette.accent, flipVertical:true) }, + secretImageSelected:{ #imageLiteral(resourceName: "Icon_SecretChatLock").precomposed(palette.underSelectedColor, flipVertical:true) }, + pinnedImage: { #imageLiteral(resourceName: "Icon_ChatListPinned").precomposed(palette.grayIcon, flipVertical:true) }, + pinnedImageSelected: { #imageLiteral(resourceName: "Icon_ChatListPinned").precomposed(palette.underSelectedColor, flipVertical:true) }, + verifiedImage: { #imageLiteral(resourceName: "Icon_VerifyPeer").precomposed(flipVertical: true) }, + verifiedImageSelected: { #imageLiteral(resourceName: "Icon_VerifyPeerActive").precomposed(flipVertical: true) }, + errorImage: { #imageLiteral(resourceName: "Icon_MessageSentFailed").precomposed(flipVertical: true) }, + errorImageSelected: { #imageLiteral(resourceName: "Icon_DialogSendingError").precomposed(flipVertical: true) }, + chatSearch: { generateChatAction(#imageLiteral(resourceName: "Icon_SearchChatMessages").precomposed(palette.accentIcon), background: palette.background) }, + chatSearchActive: { generateChatAction( #imageLiteral(resourceName: "Icon_SearchChatMessages").precomposed(palette.accentIcon), background: palette.grayIcon.withAlphaComponent(0.1)) }, + chatCall: { generateChatAction(#imageLiteral(resourceName: "Icon_callNavigationHeader").precomposed(palette.accentIcon), background: palette.background) }, + chatCallActive: { generateChatAction( #imageLiteral(resourceName: "Icon_callNavigationHeader").precomposed(palette.accentIcon), background: palette.grayIcon.withAlphaComponent(0.1)) }, + chatActions: { generateChatAction(#imageLiteral(resourceName: "Icon_ChatActionsActive").precomposed(palette.accentIcon), background: palette.background) }, + chatFailedCall_incoming: { #imageLiteral(resourceName: "Icon_MessageCallIncoming").precomposed(palette.redUI) }, + chatFailedCall_outgoing: { #imageLiteral(resourceName: "Icon_MessageCallOutgoing").precomposed(palette.redUI) }, + chatCall_incoming: { #imageLiteral(resourceName: "Icon_MessageCallIncoming").precomposed(palette.greenUI) }, + chatCall_outgoing: { #imageLiteral(resourceName: "Icon_MessageCallOutgoing").precomposed(palette.greenUI) }, + chatFailedCallBubble_incoming: { #imageLiteral(resourceName: "Icon_MessageCallIncoming").precomposed(palette.redBubble_incoming) }, + chatFailedCallBubble_outgoing: { #imageLiteral(resourceName: "Icon_MessageCallOutgoing").precomposed(palette.redBubble_outgoing) }, + chatCallBubble_incoming: { #imageLiteral(resourceName: "Icon_MessageCallIncoming").precomposed(palette.greenBubble_incoming) }, + chatCallBubble_outgoing: { #imageLiteral(resourceName: "Icon_MessageCallOutgoing").precomposed(palette.greenBubble_outgoing) }, + chatFallbackCall: { #imageLiteral(resourceName: "Icon_MessageCall").precomposed(palette.accentIcon) }, + chatFallbackCallBubble_incoming: { #imageLiteral(resourceName: "Icon_callNavigationHeader").precomposed(palette.textBubble_incoming) }, + chatFallbackCallBubble_outgoing: { #imageLiteral(resourceName: "Icon_callNavigationHeader").precomposed(palette.textBubble_outgoing) }, + chatFallbackVideoCall: { #imageLiteral(resourceName: "Icon_VideoCall").precomposed(palette.accentIcon) }, + chatFallbackVideoCallBubble_incoming: { #imageLiteral(resourceName: "Icon_VideoCall").precomposed(palette.textBubble_incoming) }, + chatFallbackVideoCallBubble_outgoing: { #imageLiteral(resourceName: "Icon_VideoCall").precomposed(palette.textBubble_outgoing) }, + chatToggleSelected: { generateChatGroupToggleSelected(foregroundColor: palette.accentIcon, backgroundColor: palette.underSelectedColor) }, + chatToggleUnselected: { generateChatGroupToggleUnselected(foregroundColor: palette.grayIcon.withAlphaComponent(0.6), backgroundColor: NSColor.black.withAlphaComponent(0.01)) }, + chatMusicPlay: { #imageLiteral(resourceName: "Icon_ChatMusicPlay").precomposed(palette.fileActivityForeground) }, + chatMusicPlayBubble_incoming: { #imageLiteral(resourceName: "Icon_ChatMusicPlay").precomposed(palette.fileActivityForegroundBubble_incoming) }, + chatMusicPlayBubble_outgoing: { #imageLiteral(resourceName: "Icon_ChatMusicPlay").precomposed(palette.fileActivityForegroundBubble_outgoing) }, + chatMusicPause: { #imageLiteral(resourceName: "Icon_ChatMusicPause").precomposed(palette.fileActivityForeground) }, + chatMusicPauseBubble_incoming: { #imageLiteral(resourceName: "Icon_ChatMusicPause").precomposed(palette.fileActivityForegroundBubble_incoming) }, + chatMusicPauseBubble_outgoing: { #imageLiteral(resourceName: "Icon_ChatMusicPause").precomposed(palette.fileActivityForegroundBubble_outgoing) }, + chatGradientBubble_incoming: { generateGradientBubble([palette.bubbleBackground_incoming]) }, + chatGradientBubble_outgoing: { generateGradientBubble(palette.bubbleBackground_outgoing) }, + chatBubble_none_incoming_withInset: { messageBubbleImageModern(incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .none) }, + chatBubble_none_outgoing_withInset: { messageBubbleImageModern(incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .none) }, + chatBubbleBorder_none_incoming_withInset: { messageBubbleImageModern(incoming: true, fillColor: .clear, strokeColor: palette.bubbleBorder_incoming, neighbors: .none) }, + chatBubbleBorder_none_outgoing_withInset: { messageBubbleImageModern(incoming: false, fillColor: .clear, strokeColor: palette.bubbleBorder_outgoing, neighbors: .none) }, + chatBubble_both_incoming_withInset: { messageBubbleImageModern(incoming: true, fillColor: .black, strokeColor: .clear, neighbors: .both) }, + chatBubble_both_outgoing_withInset: { messageBubbleImageModern(incoming: false, fillColor: .black, strokeColor: .clear, neighbors: .both) }, + chatBubbleBorder_both_incoming_withInset: { messageBubbleImageModern(incoming: true, fillColor: .clear, strokeColor: palette.bubbleBorder_incoming, neighbors: .both) }, + chatBubbleBorder_both_outgoing_withInset: { messageBubbleImageModern(incoming: false, fillColor: .clear, strokeColor: palette.bubbleBorder_outgoing, neighbors: .both) }, + composeNewChat: { #imageLiteral(resourceName: "Icon_NewMessage").precomposed(palette.accentIcon) }, + composeNewChatActive: { #imageLiteral(resourceName: "Icon_NewMessage").precomposed(palette.underSelectedColor) }, + composeNewGroup: { #imageLiteral(resourceName: "Icon_NewGroup").precomposed(palette.accentIcon) }, + composeNewSecretChat: { #imageLiteral(resourceName: "Icon_NewSecretChat").precomposed(palette.accentIcon) }, + composeNewChannel: { #imageLiteral(resourceName: "Icon_NewChannel").precomposed(palette.accentIcon) }, + contactsNewContact: { #imageLiteral(resourceName: "Icon_NewContact").precomposed(palette.accentIcon) }, + chatReadMarkInBubble1_incoming: { #imageLiteral(resourceName: "Icon_MessageCheckMark1").precomposed(palette.accentIconBubble_incoming) }, + chatReadMarkInBubble2_incoming: { #imageLiteral(resourceName: "Icon_MessageCheckmark2").precomposed(palette.accentIconBubble_incoming) }, + chatReadMarkInBubble1_outgoing: { #imageLiteral(resourceName: "Icon_MessageCheckMark1").precomposed(palette.accentIconBubble_outgoing) }, + chatReadMarkInBubble2_outgoing: { #imageLiteral(resourceName: "Icon_MessageCheckmark2").precomposed(palette.accentIconBubble_outgoing) }, + chatReadMarkOutBubble1: { #imageLiteral(resourceName: "Icon_MessageCheckMark1").precomposed(palette.accentIcon) }, + chatReadMarkOutBubble2: { #imageLiteral(resourceName: "Icon_MessageCheckmark2").precomposed(palette.accentIcon) }, + chatReadMarkOverlayBubble1: { #imageLiteral(resourceName: "Icon_MessageCheckMark1").precomposed(.white) }, + chatReadMarkOverlayBubble2: { #imageLiteral(resourceName: "Icon_MessageCheckmark2").precomposed(.white) }, + sentFailed: { generateImage(NSMakeSize(13, 13), contextGenerator: { size, ctx in + ctx.clear(NSMakeRect(0, 0, size.width, size.height)) + ctx.draw(#imageLiteral(resourceName: "Icon_MessageSentFailed").precomposed(), in: NSMakeRect(0, 0, size.width, size.height)) + })! }, + chatChannelViewsInBubble_incoming: { #imageLiteral(resourceName: "Icon_ChannelViews").precomposed(palette.grayIconBubble_incoming, flipVertical: true) }, + chatChannelViewsInBubble_outgoing: { #imageLiteral(resourceName: "Icon_ChannelViews").precomposed(palette.grayIconBubble_outgoing, flipVertical: true) }, + chatChannelViewsOutBubble: { #imageLiteral(resourceName: "Icon_ChannelViews").precomposed(palette.grayIcon, flipVertical: true) }, + chatChannelViewsOverlayBubble: { #imageLiteral(resourceName: "Icon_ChannelViews").precomposed(.white, flipVertical: true) }, + chatNavigationBack: { #imageLiteral(resourceName: "Icon_ChatNavigationBack").precomposed(palette.accentIcon) }, + peerInfoAddMember: { #imageLiteral(resourceName: "Icon_NewContact").precomposed(palette.accentIcon, flipVertical: true) }, + chatSearchUp: { #imageLiteral(resourceName: "Icon_SearchArrow").precomposed(palette.accentIcon) }, + chatSearchUpDisabled: { #imageLiteral(resourceName: "Icon_SearchArrow").precomposed(palette.grayIcon) }, + chatSearchDown: { #imageLiteral(resourceName: "Icon_SearchArrow").precomposed(palette.accentIcon, flipVertical:true) }, + chatSearchDownDisabled: { #imageLiteral(resourceName: "Icon_SearchArrow").precomposed(palette.grayIcon, flipVertical:true) }, + chatSearchCalendar: { #imageLiteral(resourceName: "Icon_Calendar").precomposed(palette.accentIcon) }, + dismissAccessory: { #imageLiteral(resourceName: "Icon_ChatSearchCancel").precomposed(palette.grayIcon) }, + chatScrollUp: { generateChatScrolldownImage(backgroundColor: palette.background, borderColor: palette.chatBackground == palette.background && palette.isDark ? palette.grayIcon : .clear, arrowColor: palette.grayIcon) }, + chatScrollUpActive: { generateChatScrolldownImage(backgroundColor: palette.background, borderColor: palette.chatBackground == palette.background && palette.isDark ? palette.accentIcon : .clear, arrowColor: palette.accentIcon) }, + chatSendMessage: { #imageLiteral(resourceName: "Icon_SendMessage").precomposed(palette.accentIcon) }, + chatSaveEditedMessage: { generateSendIcon(NSImage(named: "Icon_SaveEditedMessage")!, palette.accentIcon) }, + chatRecordVoice: { #imageLiteral(resourceName: "Icon_RecordVoice").precomposed(palette.grayIcon) }, + chatEntertainment: { #imageLiteral(resourceName: "Icon_Entertainments").precomposed(palette.grayIcon) }, + chatInlineDismiss: { #imageLiteral(resourceName: "Icon_InlineResultCancel").precomposed(palette.grayIcon) }, + chatActiveReplyMarkup: { #imageLiteral(resourceName: "Icon_ReplyMarkupButton").precomposed(palette.accentIcon) }, + chatDisabledReplyMarkup: { #imageLiteral(resourceName: "Icon_ReplyMarkupButton").precomposed(palette.grayIcon) }, + chatSecretTimer: { #imageLiteral(resourceName: "Icon_SecretTimer").precomposed(palette.grayIcon) }, + chatForwardMessagesActive: { #imageLiteral(resourceName: "Icon_MessageActionPanelForward").precomposed(palette.accentIcon) }, + chatForwardMessagesInactive: { #imageLiteral(resourceName: "Icon_MessageActionPanelForward").precomposed(palette.grayIcon) }, + chatDeleteMessagesActive: { #imageLiteral(resourceName: "Icon_MessageActionPanelDelete").precomposed(palette.redUI) }, + chatDeleteMessagesInactive: { #imageLiteral(resourceName: "Icon_MessageActionPanelDelete").precomposed(palette.grayIcon) }, + generalNext: { #imageLiteral(resourceName: "Icon_GeneralNext").precomposed(palette.grayIcon.withAlphaComponent(0.5)) }, + generalNextActive: { #imageLiteral(resourceName: "Icon_GeneralNext").precomposed(.white) }, + generalSelect: { #imageLiteral(resourceName: "Icon_UsernameAvailability").precomposed(palette.accentIcon) }, + chatVoiceRecording: { #imageLiteral(resourceName: "Icon_RecordingVoice").precomposed(palette.accentIcon) }, + chatVideoRecording: { #imageLiteral(resourceName: "Icon_RecordVideoMessage").precomposed(palette.accentIcon) }, + chatRecord: { #imageLiteral(resourceName: "Icon_RecordVoice").precomposed(palette.grayIcon) }, + deleteItem: { deleteItemIcon(palette.redUI) }, + deleteItemDisabled: { deleteItemIcon(palette.grayText) }, + chatAttach: { #imageLiteral(resourceName: "Icon_ChatAttach").precomposed(palette.grayIcon) }, + chatAttachFile: { #imageLiteral(resourceName: "Icon_AttachFile").precomposed(palette.accentIcon) }, + chatAttachPhoto: { #imageLiteral(resourceName: "Icon_AttachPhoto").precomposed(palette.accentIcon) }, + chatAttachCamera: { #imageLiteral(resourceName: "Icon_AttachCamera").precomposed(palette.accentIcon) }, + chatAttachLocation: { #imageLiteral(resourceName: "Icon_AttachLocation").precomposed(palette.accentIcon) }, + chatAttachPoll: { #imageLiteral(resourceName: "Icon_AttachPoll").precomposed(palette.accentIcon) }, + mediaEmptyShared: { #imageLiteral(resourceName: "Icon_EmptySharedMedia").precomposed(palette.grayIcon) }, + mediaEmptyFiles: { #imageLiteral(resourceName: "Icon_EmptySharedFiles").precomposed() }, + mediaEmptyMusic: { #imageLiteral(resourceName: "Icon_EmptySharedMusic").precomposed(palette.grayIcon) }, + mediaEmptyLinks: { #imageLiteral(resourceName: "Icon_EmptySharedLinks").precomposed(palette.grayIcon) }, + stickersAddFeatured: { #imageLiteral(resourceName: "Icon_GroupInfoAddMember").precomposed(palette.accentIcon) }, + stickersAddedFeatured: { #imageLiteral(resourceName: "Icon_UsernameAvailability").precomposed(palette.grayIcon) }, + stickersRemove: { #imageLiteral(resourceName: "Icon_InlineResultCancel").precomposed(palette.grayIcon) }, + peerMediaDownloadFileStart: { #imageLiteral(resourceName: "Icon_MediaDownload").precomposed(palette.accentIcon) }, + peerMediaDownloadFilePause: { downloadFilePauseIcon(palette.accentIcon) }, + stickersShare: { #imageLiteral(resourceName: "Icon_ShareStickerPack").precomposed(palette.accentIcon) }, + emojiRecentTab: { #imageLiteral(resourceName: "Icon_EmojiTabRecent").precomposed(palette.grayIcon, flipVertical:true, flipHorizontal:true) }, + emojiSmileTab: { #imageLiteral(resourceName: "Icon_EmojiTabSmiles").precomposed(palette.grayIcon, flipVertical:true, flipHorizontal:true) }, + emojiNatureTab: { #imageLiteral(resourceName: "Icon_EmojiTabNature").precomposed(palette.grayIcon, flipVertical:true, flipHorizontal:true) }, + emojiFoodTab: { #imageLiteral(resourceName: "Icon_EmojiTabFood").precomposed(palette.grayIcon, flipVertical:true, flipHorizontal:true) }, + emojiSportTab: { #imageLiteral(resourceName: "Icon_EmojiTabSports").precomposed(palette.grayIcon, flipVertical:true, flipHorizontal:true) }, + emojiCarTab: { #imageLiteral(resourceName: "Icon_EmojiTabCar").precomposed(palette.grayIcon, flipVertical:true, flipHorizontal:true) }, + emojiObjectsTab: { #imageLiteral(resourceName: "Icon_EmojiTabObjects").precomposed(palette.grayIcon, flipVertical:true, flipHorizontal:true) }, + emojiSymbolsTab: { #imageLiteral(resourceName: "Icon_EmojiTabSymbols").precomposed(palette.grayIcon, flipVertical:true, flipHorizontal:true) }, + emojiFlagsTab: { #imageLiteral(resourceName: "Icon_EmojiTabFlag").precomposed(palette.grayIcon, flipVertical:true, flipHorizontal:true) }, + emojiRecentTabActive: { #imageLiteral(resourceName: "Icon_EmojiTabRecent").precomposed(palette.accentIcon, flipVertical:true, flipHorizontal:true) }, + emojiSmileTabActive: { #imageLiteral(resourceName: "Icon_EmojiTabSmiles").precomposed(palette.accentIcon, flipVertical:true, flipHorizontal:true) }, + emojiNatureTabActive: { #imageLiteral(resourceName: "Icon_EmojiTabNature").precomposed(palette.accentIcon, flipVertical:true, flipHorizontal:true) }, + emojiFoodTabActive: { #imageLiteral(resourceName: "Icon_EmojiTabFood").precomposed(palette.accentIcon, flipVertical:true, flipHorizontal:true) }, + emojiSportTabActive: { #imageLiteral(resourceName: "Icon_EmojiTabSports").precomposed(palette.accentIcon, flipVertical:true, flipHorizontal:true) }, + emojiCarTabActive: { #imageLiteral(resourceName: "Icon_EmojiTabCar").precomposed(palette.accentIcon, flipVertical:true, flipHorizontal:true) }, + emojiObjectsTabActive: { #imageLiteral(resourceName: "Icon_EmojiTabObjects").precomposed(palette.accentIcon, flipVertical:true, flipHorizontal:true) }, + emojiSymbolsTabActive: { #imageLiteral(resourceName: "Icon_EmojiTabSymbols").precomposed(palette.accentIcon, flipVertical:true, flipHorizontal:true) }, + emojiFlagsTabActive: { #imageLiteral(resourceName: "Icon_EmojiTabFlag").precomposed(palette.accentIcon, flipVertical:true, flipHorizontal:true) }, + stickerBackground: { generateStickerBackground(NSMakeSize(83, 83), palette.background) }, + stickerBackgroundActive: { generateStickerBackground(NSMakeSize(83, 83), palette.grayBackground) }, + stickersTabRecent: { #imageLiteral(resourceName: "Icon_EmojiTabRecent").precomposed(palette.grayIcon) }, + stickersTabGIF: { #imageLiteral(resourceName: "Icon_GifToggle").precomposed(palette.grayIcon) }, + chatSendingInFrame_incoming: { generateSendingFrame(palette.grayIconBubble_incoming) }, + chatSendingInHour_incoming: { generateClockMinImage(palette.grayIconBubble_incoming) }, + chatSendingInMin_incoming: { generateClockMinImage(palette.grayIconBubble_incoming) }, + chatSendingInFrame_outgoing: { generateSendingFrame(palette.grayIconBubble_outgoing) }, + chatSendingInHour_outgoing: { generateClockMinImage(palette.grayIconBubble_outgoing) }, + chatSendingInMin_outgoing: { generateClockMinImage(palette.grayIconBubble_outgoing) }, + chatSendingOutFrame: { generateSendingFrame(palette.grayIcon) }, + chatSendingOutHour: { generateClockMinImage(palette.grayIcon) }, + chatSendingOutMin: { generateClockMinImage(palette.grayIcon) }, + chatSendingOverlayFrame: { generateSendingFrame(.white) }, + chatSendingOverlayHour: { generateClockMinImage(.white) }, + chatSendingOverlayMin: { generateClockMinImage(.white) }, + chatActionUrl: { #imageLiteral(resourceName: "Icon_InlineBotUrl").precomposed(palette.text) }, + callInlineDecline: { #imageLiteral(resourceName: "Icon_CallDecline_Inline").precomposed(.white) }, + callInlineMuted: { #imageLiteral(resourceName: "Icon_CallMute_Inline").precomposed(.white) }, + callInlineUnmuted: { #imageLiteral(resourceName: "Icon_CallUnmuted_Inline").precomposed(.white) }, + eventLogTriangle: { generateRecentActionsTriangle(palette.text) }, + channelIntro: { #imageLiteral(resourceName: "Icon_ChannelIntro").precomposed() }, + chatFileThumb: { #imageLiteral(resourceName: "Icon_MessageFile").precomposed(flipVertical:true) }, + chatFileThumbBubble_incoming: { #imageLiteral(resourceName: "Icon_MessageFile").precomposed(palette.fileActivityForegroundBubble_incoming, flipVertical:true) }, + chatFileThumbBubble_outgoing: { #imageLiteral(resourceName: "Icon_MessageFile").precomposed(palette.fileActivityForegroundBubble_outgoing, flipVertical:true) }, + chatSecretThumb: { generateSecretThumb(#imageLiteral(resourceName: "Icon_SecretAutoremoveMedia").precomposed(.black, flipVertical:true)) }, + chatSecretThumbSmall: { generateSecretThumbSmall(#imageLiteral(resourceName: "Icon_SecretAutoremoveMedia").precomposed(.black, flipVertical:true)) }, + chatMapPin: { #imageLiteral(resourceName: "Icon_MapPinned").precomposed() }, + chatSecretTitle: { #imageLiteral(resourceName: "Icon_SecretChatLock").precomposed(palette.text, flipVertical:true) }, + emptySearch: { #imageLiteral(resourceName: "Icon_EmptySearchResults").precomposed(palette.grayIcon) }, + calendarBack: { #imageLiteral(resourceName: "Icon_NavigationBack").precomposed(palette.accentIcon) }, + calendarNext: { #imageLiteral(resourceName: "Icon_NavigationBack").precomposed(palette.accentIcon, flipHorizontal: true) }, + calendarBackDisabled: { #imageLiteral(resourceName: "Icon_NavigationBack").precomposed(palette.grayIcon) }, + calendarNextDisabled: { #imageLiteral(resourceName: "Icon_NavigationBack").precomposed(palette.grayIcon, flipHorizontal: true) }, + newChatCamera: { #imageLiteral(resourceName: "Icon_AttachCamera").precomposed(palette.grayIcon) }, + peerInfoVerify: { #imageLiteral(resourceName: "Icon_VerifyPeer").precomposed(flipVertical: true) }, + peerInfoVerifyProfile: { #imageLiteral(resourceName: "Icon_VerifyPeer").precomposed() }, + peerInfoCall: { #imageLiteral(resourceName: "Icon_ProfileCall").precomposed(palette.accent) }, + callOutgoing: { #imageLiteral(resourceName: "Icon_CallOutgoing").precomposed(palette.grayIcon, flipVertical: true) }, + recentDismiss: { NSImage(named: "Icon_Search_RemoveRecent")!.precomposed(palette.grayIcon) }, + recentDismissActive: { NSImage(named: "Icon_Search_RemoveRecent")!.precomposed(palette.underSelectedColor) }, + webgameShare: { #imageLiteral(resourceName: "Icon_ShareStickerPack").precomposed(palette.accentIcon) }, + chatSearchCancel: { #imageLiteral(resourceName: "Icon_ChatSearchCancel").precomposed(palette.accentIcon) }, + chatSearchFrom: { #imageLiteral(resourceName: "Icon_ChatSearchFrom").precomposed(palette.accentIcon) }, + callWindowDecline: { #imageLiteral(resourceName: "Icon_CallDecline_Window").precomposed(.white) }, + callWindowDeclineSmall: { NSImage(named: "Icon_callDeclineSmall_Window")!.precomposed(.white) }, + callWindowAccept: { #imageLiteral(resourceName: "Icon_CallAccept_Window").precomposed(.white) }, + callWindowVideo: { #imageLiteral(resourceName: "Icon_CallVideo_Window").precomposed(.white) }, + callWindowVideoActive: { #imageLiteral(resourceName: "Icon_CallVideo_Window").precomposed(.grayIcon) }, + callWindowMute: { #imageLiteral(resourceName: "Icon_CallMuted_Window").precomposed(.white) }, + callWindowMuteActive: { #imageLiteral(resourceName: "Icon_CallMuted_Window").precomposed(.grayIcon) }, + callWindowClose: { #imageLiteral(resourceName: "Icon_CallWindowClose").precomposed(.white) }, + callWindowDeviceSettings: { #imageLiteral(resourceName: "Icon_CallDeviceSettings").precomposed(.white) }, + callSettings: { #imageLiteral(resourceName: "Icon_CallDeviceSettings").precomposed(palette.accentIcon) }, + callWindowCancel: { #imageLiteral(resourceName: "Icon_CallCancelIcon").precomposed(.white) }, + chatActionEdit: { #imageLiteral(resourceName: "Icon_ChatActionEdit").precomposed(palette.accentIcon) }, + chatActionInfo: { #imageLiteral(resourceName: "Icon_ChatActionInfo").precomposed(palette.accentIcon) }, + chatActionMute: { #imageLiteral(resourceName: "Icon_ChatActionMute").precomposed(palette.accentIcon) }, + chatActionUnmute: { #imageLiteral(resourceName: "Icon_ChatActionUnmute").precomposed(palette.accentIcon) }, + chatActionClearHistory: { #imageLiteral(resourceName: "Icon_ClearChat").precomposed(palette.accentIcon) }, + chatActionDeleteChat: { #imageLiteral(resourceName: "Icon_MessageActionPanelDelete").precomposed(palette.accentIcon) }, + dismissPinned: { #imageLiteral(resourceName: "Icon_ChatSearchCancel").precomposed(palette.accentIcon) }, + chatActionsActive: { generateChatAction(#imageLiteral(resourceName: "Icon_ChatActionsActive").precomposed(palette.accentIcon), background: palette.grayIcon.withAlphaComponent(0.1)) }, + chatEntertainmentSticker: { #imageLiteral(resourceName: "Icon_ChatEntertainmentSticker").precomposed(palette.grayIcon) }, + chatEmpty: { #imageLiteral(resourceName: "Icon_EmptyChat").precomposed(palette.grayForeground) }, + stickerPackClose: { #imageLiteral(resourceName: "Icon_ChatSearchCancel").precomposed(palette.accentIcon) }, + stickerPackDelete: { #imageLiteral(resourceName: "Icon_MessageActionPanelDelete").precomposed(palette.accentIcon) }, + modalShare: { #imageLiteral(resourceName: "Icon_ShareStickerPack").precomposed(palette.accentIcon) }, + modalClose: { #imageLiteral(resourceName: "Icon_ChatSearchCancel").precomposed(palette.accentIcon) }, + ivChannelJoined: { #imageLiteral(resourceName: "Icon_MessageCheckMark1").precomposed(.white) }, + chatListMention: { generateBadgeMention(backgroundColor: palette.badge, foregroundColor: palette.background) }, + chatListMentionActive: { generateBadgeMention(backgroundColor: .white, foregroundColor: palette.accentSelect) }, + chatListMentionArchived: { generateBadgeMention(backgroundColor: palette.badgeMuted, foregroundColor: palette.background) }, + chatListMentionArchivedActive: { generateBadgeMention(backgroundColor: palette.underSelectedColor, foregroundColor: palette.accentSelect) }, + chatMention: { generateChatMention(backgroundColor: palette.background, border: palette.grayIcon, foregroundColor: palette.grayIcon) }, + chatMentionActive: { generateChatMention(backgroundColor: palette.background, border: palette.accentIcon, foregroundColor: palette.accentIcon) }, + sliderControl: { #imageLiteral(resourceName: "Icon_SliderNormal").precomposed() }, + sliderControlActive: { #imageLiteral(resourceName: "Icon_SliderNormal").precomposed() }, + stickersTabFave: { #imageLiteral(resourceName: "Icon_FaveStickers").precomposed(palette.grayIcon) }, + chatInstantView: { #imageLiteral(resourceName: "Icon_ChatIV").precomposed(palette.webPreviewActivity) }, + chatInstantViewBubble_incoming: { #imageLiteral(resourceName: "Icon_ChatIV").precomposed(palette.webPreviewActivityBubble_incoming) }, + chatInstantViewBubble_outgoing: { #imageLiteral(resourceName: "Icon_ChatIV").precomposed(palette.webPreviewActivityBubble_outgoing) }, + instantViewShare: { #imageLiteral(resourceName: "Icon_ShareStickerPack").precomposed(palette.accentIcon) }, + instantViewActions: { #imageLiteral(resourceName: "Icon_ChatActions").precomposed(palette.accentIcon) }, + instantViewActionsActive: { #imageLiteral(resourceName: "Icon_ChatActionsActive").precomposed(palette.accentIcon) }, + instantViewSafari: { #imageLiteral(resourceName: "Icon_InstantViewSafari").precomposed(palette.accentIcon) }, + instantViewBack: { #imageLiteral(resourceName: "Icon_InstantViewBack").precomposed(palette.accentIcon) }, + instantViewCheck: { #imageLiteral(resourceName: "Icon_InstantViewCheck").precomposed(palette.accentIcon) }, + groupStickerNotFound: { #imageLiteral(resourceName: "Icon_GroupStickerNotFound").precomposed(palette.grayIcon) }, + settingsAskQuestion: { generateSettingsIcon(#imageLiteral(resourceName: "Icon_SettingsAskQuestion").precomposed(flipVertical: true)) }, + settingsFaq: { generateSettingsIcon(#imageLiteral(resourceName: "Icon_SettingsFaq").precomposed(flipVertical: true)) }, + settingsGeneral: { generateSettingsIcon(#imageLiteral(resourceName: "Icon_SettingsGeneral").precomposed(flipVertical: true)) }, + settingsLanguage: { generateSettingsIcon(#imageLiteral(resourceName: "Icon_SettingsLanguage").precomposed(flipVertical: true)) }, + settingsNotifications: { generateSettingsIcon(#imageLiteral(resourceName: "Icon_SettingsNotifications").precomposed(flipVertical: true)) }, + settingsSecurity: { generateSettingsIcon(#imageLiteral(resourceName: "Icon_SettingsSecurity").precomposed(flipVertical: true)) }, + settingsStickers: { generateSettingsIcon(#imageLiteral(resourceName: "Icon_SettingsStickers").precomposed(flipVertical: true)) }, + settingsStorage: { generateSettingsIcon(#imageLiteral(resourceName: "Icon_SettingsStorage").precomposed(flipVertical: true)) }, + settingsSessions: { generateSettingsIcon(#imageLiteral(resourceName: "Icon_PrivacySettings_ActiveSessions").precomposed(flipVertical: true)) }, + settingsProxy: { generateSettingsIcon(#imageLiteral(resourceName: "Icon_SettingsProxy").precomposed(flipVertical: true)) }, + settingsAppearance: { generateSettingsIcon(#imageLiteral(resourceName: "Icon_AppearanceSettings").precomposed(flipVertical: true)) }, + settingsPassport: { generateSettingsIcon(#imageLiteral(resourceName: "Icon_SettingsSecurity").precomposed(flipVertical: true)) }, + settingsWallet: { generateSettingsIcon(NSImage(named: "Icon_SettingsWallet")!.precomposed(NSColor(0x59a7d8), flipVertical: true)) }, + settingsUpdate: { generateSettingsIcon(NSImage(named: "Icon_SettingsUpdate")!.precomposed(flipVertical: true)) }, + settingsFilters: { generateSettingsIcon(NSImage(named: "Icon_SettingsFilters")!.precomposed(flipVertical: true)) }, + settingsAskQuestionActive: { generateSettingsActiveIcon(#imageLiteral(resourceName: "Icon_SettingsAskQuestion").precomposed(palette.underSelectedColor, flipVertical: true), background: palette.accentSelect) }, + settingsFaqActive: { generateSettingsActiveIcon(#imageLiteral(resourceName: "Icon_SettingsFaq").precomposed(palette.underSelectedColor, flipVertical: true), background: palette.accentSelect) }, + settingsGeneralActive: { generateSettingsActiveIcon(#imageLiteral(resourceName: "Icon_SettingsGeneral").precomposed(palette.underSelectedColor, flipVertical: true), background: palette.accentSelect) }, + settingsLanguageActive: { generateSettingsActiveIcon(#imageLiteral(resourceName: "Icon_SettingsLanguage").precomposed(palette.underSelectedColor, flipVertical: true), background: palette.accentSelect) }, + settingsNotificationsActive: { generateSettingsActiveIcon(#imageLiteral(resourceName: "Icon_SettingsNotifications").precomposed(palette.underSelectedColor, flipVertical: true), background: palette.accentSelect) }, + settingsSecurityActive: { generateSettingsActiveIcon(#imageLiteral(resourceName: "Icon_SettingsSecurity").precomposed(palette.underSelectedColor, flipVertical: true), background: palette.accentSelect) }, + settingsStickersActive: { generateSettingsActiveIcon(#imageLiteral(resourceName: "Icon_SettingsStickers").precomposed(palette.underSelectedColor, flipVertical: true), background: palette.accentSelect) }, + settingsStorageActive: { generateSettingsActiveIcon(#imageLiteral(resourceName: "Icon_SettingsStorage").precomposed(palette.underSelectedColor, flipVertical: true), background: palette.accentSelect) }, + settingsSessionsActive: { generateSettingsActiveIcon(#imageLiteral(resourceName: "Icon_PrivacySettings_ActiveSessions").precomposed(palette.underSelectedColor, flipVertical: true), background: palette.accentSelect) }, + settingsProxyActive: { generateSettingsActiveIcon(#imageLiteral(resourceName: "Icon_SettingsProxy").precomposed(palette.underSelectedColor, flipVertical: true), background: palette.accentSelect) }, + settingsAppearanceActive: { generateSettingsActiveIcon(#imageLiteral(resourceName: "Icon_AppearanceSettings").precomposed(palette.underSelectedColor, flipVertical: true), background: palette.accentSelect) }, + settingsPassportActive: { generateSettingsActiveIcon(#imageLiteral(resourceName: "Icon_SettingsSecurity").precomposed(palette.underSelectedColor, flipVertical: true), background: palette.accentSelect) }, + settingsWalletActive: { generateSettingsActiveIcon(NSImage(named: "Icon_SettingsWallet")!.precomposed(palette.underSelectedColor, flipVertical: true), background: palette.accentSelect) }, + settingsUpdateActive: { generateSettingsActiveIcon(NSImage(named: "Icon_SettingsUpdate")!.precomposed(palette.underSelectedColor, flipVertical: true), background: palette.accentSelect) }, + settingsFiltersActive: { generateSettingsActiveIcon(NSImage(named: "Icon_SettingsFilters")!.precomposed(palette.underSelectedColor, flipVertical: true), background: palette.accentSelect) }, + settingsProfile: { generateSettingsIcon(NSImage(named: "Icon_SettingsProfile")!.precomposed(flipVertical: true)) }, + generalCheck: { #imageLiteral(resourceName: "Icon_Check").precomposed(palette.accentIcon) }, + settingsAbout: { #imageLiteral(resourceName: "Icon_SettingsAbout").precomposed(palette.accentIcon) }, + settingsLogout: { #imageLiteral(resourceName: "Icon_SettingsLogout").precomposed(palette.redUI) }, + fastSettingsLock: { #imageLiteral(resourceName: "Icon_FastSettingsLock").precomposed(palette.accentIcon) }, + fastSettingsDark: { #imageLiteral(resourceName: "Icon_FastSettingsDark").precomposed(palette.accentIcon) }, + fastSettingsSunny: { #imageLiteral(resourceName: "Icon_FastSettingsSunny").precomposed(palette.accentIcon) }, + fastSettingsMute: { #imageLiteral(resourceName: "Icon_ChatActionMute").precomposed(palette.accentIcon) }, + fastSettingsUnmute: { #imageLiteral(resourceName: "Icon_ChatActionUnmute").precomposed(palette.accentIcon) }, + chatRecordVideo: { #imageLiteral(resourceName: "Icon_RecordVideoMessage").precomposed(palette.grayIcon) }, + inputChannelMute: { #imageLiteral(resourceName: "Icon_InputChannelMute").precomposed(palette.grayIcon) }, + inputChannelUnmute: { #imageLiteral(resourceName: "Icon_InputChannelUnmute").precomposed(palette.grayIcon) }, + changePhoneNumberIntro: { #imageLiteral(resourceName: "Icon_ChangeNumberIntro").precomposed() }, + peerSavedMessages: { #imageLiteral(resourceName: "Icon_SavedMessages").precomposed() }, + previewSenderCollage: { #imageLiteral(resourceName: "Icon_PreviewCollage").precomposed(palette.grayIcon) }, + previewSenderPhoto: { NSImage(named: "Icon_PreviewSenderPhoto")!.precomposed(palette.grayIcon) }, + previewSenderFile: { NSImage(named: "Icon_PreviewSenderFile")!.precomposed(palette.grayIcon) }, + previewSenderCrop: { NSImage(named: "Icon_PreviewSenderCrop")!.precomposed(.white) }, + previewSenderDelete: { NSImage(named: "Icon_PreviewSenderDelete")!.precomposed(.white) }, + previewSenderDeleteFile: { NSImage(named: "Icon_PreviewSenderDelete")!.precomposed(palette.accentIcon) }, + previewSenderArchive: { NSImage(named: "Icon_PreviewSenderArchive")!.precomposed(palette.grayIcon) }, + chatGroupToggleSelected: { generateChatGroupToggleSelected(foregroundColor: palette.accentIcon, backgroundColor: palette.underSelectedColor) }, + chatGroupToggleUnselected: { generateChatGroupToggleUnselected(foregroundColor: palette.grayIcon.withAlphaComponent(0.6), backgroundColor: NSColor.black.withAlphaComponent(0.01)) }, + successModalProgress: { #imageLiteral(resourceName: "Icon_ProgressWindowCheck").precomposed(palette.grayIcon) }, + accentColorSelect: { #imageLiteral(resourceName: "Icon_UsernameAvailability").precomposed(.white) }, + transparentBackground: { generateTransparentBackground() }, + lottieTransparentBackground: { generateLottieTransparentBackground() }, + passcodeTouchId: { #imageLiteral(resourceName: "Icon_TouchId").precomposed(palette.underSelectedColor) }, + passcodeLogin: { #imageLiteral(resourceName: "Icon_PasscodeLogin").precomposed(palette.underSelectedColor) }, + confirmDeleteMessagesAccessory: { generateConfirmDeleteMessagesAccessory(backgroundColor: palette.redUI) }, + alertCheckBoxSelected: { generateAlertCheckBoxSelected(backgroundColor: palette.accentIcon) }, + alertCheckBoxUnselected: { generateAlertCheckBoxUnselected(border: palette.grayIcon) }, + confirmPinAccessory: { generateConfirmPinAccessory(backgroundColor: palette.accentIcon) }, + confirmDeleteChatAccessory: { generateConfirmDeleteChatAccessory(backgroundColor: palette.background, foregroundColor: palette.redUI) }, + stickersEmptySearch: { generateStickersEmptySearch(color: palette.grayIcon) }, + twoStepVerificationCreateIntro: { #imageLiteral(resourceName: "Icon_TwoStepVerification_Create").precomposed() }, + secureIdAuth: { #imageLiteral(resourceName: "Icon_SecureIdAuth").precomposed() }, + ivAudioPlay: { generateIVAudioPlay(color: palette.text) }, + ivAudioPause: { generateIVAudioPause(color: palette.text) }, + proxyEnable: { #imageLiteral(resourceName: "Icon_ProxyEnable").precomposed(palette.accent) }, + proxyEnabled: { #imageLiteral(resourceName: "Icon_ProxyEnabled").precomposed(palette.accent) }, + proxyState: { #imageLiteral(resourceName: "Icon_ProxyState").precomposed(palette.accent) }, + proxyDeleteListItem: { #imageLiteral(resourceName: "Icon_MessageActionPanelDelete").precomposed(palette.accentIcon) }, + proxyInfoListItem: { NSImage(named: "Icon_DetailedInfo")!.precomposed(palette.accentIcon) }, + proxyConnectedListItem: { #imageLiteral(resourceName: "Icon_UsernameAvailability").precomposed(palette.accentIcon) }, + proxyAddProxy: { #imageLiteral(resourceName: "Icon_GroupInfoAddMember").precomposed(palette.accentIcon, flipVertical: true) }, + proxyNextWaitingListItem: { #imageLiteral(resourceName: "Icon_UsernameAvailability").precomposed(palette.grayIcon) }, + passportForgotPassword: { #imageLiteral(resourceName: "Icon_SecureIdForgotPassword").precomposed(palette.grayIcon) }, + confirmAppAccessoryIcon: { #imageLiteral(resourceName: "Icon_ConfirmAppAccessory").precomposed() }, + passportPassport: { #imageLiteral(resourceName: "Icon_PassportPassport").precomposed(palette.accentIcon, flipVertical: true) }, + passportIdCardReverse: { #imageLiteral(resourceName: "Icon_PassportIdCardReverse").precomposed(palette.accentIcon, flipVertical: true) }, + passportIdCard: { #imageLiteral(resourceName: "Icon_PassportIdCard").precomposed(palette.accentIcon, flipVertical: true) }, + passportSelfie: { #imageLiteral(resourceName: "Icon_PassportSelfie").precomposed(palette.accentIcon, flipVertical: true) }, + passportDriverLicense: { #imageLiteral(resourceName: "Icon_PassportDriverLicense").precomposed(palette.accentIcon, flipVertical: true) }, + chatOverlayVoiceRecording: { #imageLiteral(resourceName: "Icon_RecordingVoice").precomposed(.white) }, + chatOverlayVideoRecording: { #imageLiteral(resourceName: "Icon_RecordVideoMessage").precomposed(.white) }, + chatOverlaySendRecording: { #imageLiteral(resourceName: "Icon_ChatOverlayRecordingSend").precomposed(.white) }, + chatOverlayLockArrowRecording: { #imageLiteral(resourceName: "Icon_DropdownArrow").precomposed(palette.accentIcon, flipVertical: true) }, + chatOverlayLockerBodyRecording: { generateLockerBody(palette.accentIcon, backgroundColor: palette.background) }, + chatOverlayLockerHeadRecording: { generateLockerHead(palette.accentIcon, backgroundColor: palette.background) }, + locationPin: { generateLocationPinIcon(palette.accentIcon) }, + locationMapPin: { generateLocationMapPinIcon(palette.accentIcon) }, + locationMapLocate: { #imageLiteral(resourceName: "Icon_MapLocate").precomposed(palette.grayIcon) }, + locationMapLocated: { #imageLiteral(resourceName: "Icon_MapLocate").precomposed(palette.accentIcon) }, + passportSettings: { #imageLiteral(resourceName: "Icon_PassportSettings").precomposed(palette.grayIcon) }, + passportInfo: { #imageLiteral(resourceName: "Icon_SettingsBio").precomposed(palette.accentIcon) }, + editMessageMedia: { generateEditMessageMediaIcon(#imageLiteral(resourceName: "Icon_ReplaceMessageMedia").precomposed(palette.accentIcon), background: palette.background) }, + playerMusicPlaceholder: { generatePlayerListAlbumPlaceholder(#imageLiteral(resourceName: "Icon_MusicPlayerSmallAlbumArtPlaceholder").precomposed(palette.listGrayText), background: palette.listBackground, radius: .cornerRadius) }, + chatMusicPlaceholder: { generatePlayerListAlbumPlaceholder(#imageLiteral(resourceName: "Icon_MusicPlayerSmallAlbumArtPlaceholder").precomposed(palette.fileActivityForeground), background: palette.fileActivityBackground, radius: 20) }, + chatMusicPlaceholderCap: { generatePlayerListAlbumPlaceholder(nil, background: palette.fileActivityBackground, radius: 20) }, + searchArticle: { #imageLiteral(resourceName: "Icon_SearchArticles").precomposed(.white) }, + searchSaved: { #imageLiteral(resourceName: "Icon_SearchSaved").precomposed(.white) }, + archivedChats: { #imageLiteral(resourceName: "Icon_ArchiveAvatar").precomposed(.white) }, + hintPeerActive: { generateHitActiveIcon(activeColor: palette.accent, backgroundColor: palette.background) }, + hintPeerActiveSelected: { generateHitActiveIcon(activeColor: palette.underSelectedColor, backgroundColor: palette.accentSelect) }, + chatSwiping_delete: { #imageLiteral(resourceName: "Icon_ChatSwipingDelete").precomposed(.white) }, + chatSwiping_mute: { #imageLiteral(resourceName: "Icon_ChatSwipingMute").precomposed(.white) }, + chatSwiping_unmute: { #imageLiteral(resourceName: "Icon_ChatSwipingUnmute").precomposed(.white) }, + chatSwiping_read: { #imageLiteral(resourceName: "Icon_ChatSwipingRead").precomposed(.white) }, + chatSwiping_unread: { #imageLiteral(resourceName: "Icon_ChatSwipingUnread").precomposed(.white) }, + chatSwiping_pin: { #imageLiteral(resourceName: "Icon_ChatSwipingPin").precomposed(.white) }, + chatSwiping_unpin: { #imageLiteral(resourceName: "Icon_ChatSwipingUnpin").precomposed(.white) }, + chatSwiping_archive: { #imageLiteral(resourceName: "Icon_ChatListSwiping_Archive").precomposed(.white) }, + chatSwiping_unarchive: { #imageLiteral(resourceName: "Icon_ChatListSwiping_Unarchive").precomposed(.white) }, + galleryPrev: { #imageLiteral(resourceName: "Icon_GalleryPrev").precomposed(.white) }, + galleryNext: { #imageLiteral(resourceName: "Icon_GalleryNext").precomposed(.white) }, + galleryMore: { #imageLiteral(resourceName: "Icon_GalleryMore").precomposed(.white) }, + galleryShare: { #imageLiteral(resourceName: "Icon_GalleryShare").precomposed(.white) }, + galleryFastSave: { NSImage(named: "Icon_Gallery_FastSave")!.precomposed(.white) }, + galleryRotate: {NSImage(named: "Icon_GalleryRotate")!.precomposed(.white) }, + galleryZoomIn: {NSImage(named: "Icon_GalleryZoomIn")!.precomposed(.white) }, + galleryZoomOut: { NSImage(named: "Icon_GalleryZoomOut")!.precomposed(.white) }, + editMessageCurrentPhoto: { NSImage(named: "Icon_EditMessageCurrentPhoto")!.precomposed(palette.accentIcon) }, + videoPlayerPlay: { NSImage(named: "Icon_VideoPlayer_Play")!.precomposed(.white) }, + videoPlayerPause: { NSImage(named: "Icon_VideoPlayer_Pause")!.precomposed(.white) }, + videoPlayerEnterFullScreen: { NSImage(named: "Icon_VideoPlayer_EnterFullScreen")!.precomposed(.white) }, + videoPlayerExitFullScreen: { NSImage(named: "Icon_VideoPlayer_ExitFullScreen")!.precomposed(.white) }, + videoPlayerPIPIn: { NSImage(named: "Icon_VideoPlayer_PIPIN")!.precomposed(.white) }, + videoPlayerPIPOut: { NSImage(named: "Icon_VideoPlayer_PIPOUT")!.precomposed(.white) }, + videoPlayerRewind15Forward: { NSImage(named: "Icon_VideoPlayer_Rewind15Forward")!.precomposed(.white) }, + videoPlayerRewind15Backward: { NSImage(named: "Icon_VideoPlayer_Rewind15Backward")!.precomposed(.white) }, + videoPlayerVolume: { NSImage(named: "Icon_VideoPlayer_Volume")!.precomposed(.white) }, + videoPlayerVolumeOff: { NSImage(named: "Icon_VideoPlayer_VolumeOff")!.precomposed(.white) }, + videoPlayerClose: { NSImage(named: "Icon_VideoPlayer_Close")!.precomposed(.white) }, + videoPlayerSliderInteractor: { NSImage(named: "Icon_Slider")!.precomposed() }, + streamingVideoDownload: { NSImage(named: "Icon_StreamingDownload")!.precomposed(.white) }, + videoCompactFetching: { NSImage(named: "Icon_VideoCompactFetching")!.precomposed(.white) }, + compactStreamingFetchingCancel: { NSImage(named: "Icon_CompactStreamingFetchingCancel")!.precomposed(.white) }, + customLocalizationDelete: { NSImage(named: "Icon_MessageActionPanelDelete")!.precomposed(palette.accentIcon) }, + pollAddOption: { generatePollAddOption(palette.accentIcon) }, + pollDeleteOption: { generatePollDeleteOption(palette.redUI) }, + resort: { NSImage(named: "Icon_Resort")!.precomposed(palette.grayIcon.withAlphaComponent(0.6)) }, + chatPollVoteUnselected: { #imageLiteral(resourceName: "Icon_SelectionUncheck").precomposed(palette.grayText.withAlphaComponent(0.3)) }, + chatPollVoteUnselectedBubble_incoming: { #imageLiteral(resourceName: "Icon_SelectionUncheck").precomposed(palette.grayTextBubble_incoming.withAlphaComponent(0.3)) }, + chatPollVoteUnselectedBubble_outgoing: { #imageLiteral(resourceName: "Icon_SelectionUncheck").precomposed(palette.grayTextBubble_outgoing.withAlphaComponent(0.3)) }, + peerInfoAdmins: { NSImage(named: "Icon_ChatAdmins")!.precomposed(flipVertical: true) }, + peerInfoPermissions: { NSImage(named: "Icon_ChatPermissions")!.precomposed(flipVertical: true) }, + peerInfoBanned: { NSImage(named: "Icon_ChatBanned")!.precomposed(flipVertical: true) }, + peerInfoMembers: { NSImage(named: "Icon_ChatMembers")!.precomposed(flipVertical: true) }, + chatUndoAction: { NSImage(named: "Icon_ChatUndoAction")!.precomposed(NSColor(0x29ACFF)) }, + appUpdate: { NSImage(named: "Icon_AppUpdate")!.precomposed() }, + inlineVideoSoundOff: { NSImage(named: "Icon_InlineVideoSoundOff")!.precomposed() }, + inlineVideoSoundOn: { NSImage(named: "Icon_InlineVideoSoundOn")!.precomposed() }, + logoutOptionAddAccount: { generateSettingsIcon(NSImage(named: "Icon_LogoutOption_AddAccount")!.precomposed(flipVertical: true)) }, + logoutOptionSetPasscode: { generateSettingsIcon(NSImage(named: "Icon_LogoutOption_SetPasscode")!.precomposed(flipVertical: true)) }, + logoutOptionClearCache: { generateSettingsIcon(NSImage(named: "Icon_LogoutOption_ClearCache")!.precomposed(flipVertical: true)) }, + logoutOptionChangePhoneNumber: { generateSettingsIcon(NSImage(named: "Icon_LogoutOption_ChangePhoneNumber")!.precomposed(flipVertical: true)) }, + logoutOptionContactSupport: { generateSettingsIcon(NSImage(named: "Icon_LogoutOption_ContactSupport")!.precomposed(flipVertical: true)) }, + disableEmojiPrediction: { NSImage(named: "Icon_CallWindowClose")!.precomposed(palette.grayIcon) }, + scam: { generateScamIcon(foregroundColor: palette.redUI, backgroundColor: .clear) }, + scamActive: { generateScamIcon(foregroundColor: palette.underSelectedColor, backgroundColor: .clear) }, + chatScam: { generateScamIconReversed(foregroundColor: palette.redUI, backgroundColor: .clear) }, + fake: { generateFakeIcon(foregroundColor: palette.redUI, backgroundColor: .clear) }, + fakeActive: { generateFakeIcon(foregroundColor: palette.underSelectedColor, backgroundColor: .clear) }, + chatFake: { generateFakeIconReversed(foregroundColor: palette.redUI, backgroundColor: .clear) }, + chatUnarchive: { NSImage(named: "Icon_ChatUnarchive")!.precomposed(palette.accentIcon) }, + chatArchive: { NSImage(named: "Icon_ChatArchive")!.precomposed(palette.accentIcon) }, + privacySettings_blocked: { generateSettingsIcon(NSImage(named: "Icon_PrivacySettings_Blocked")!.precomposed(flipVertical: true)) }, + privacySettings_activeSessions: { generateSettingsIcon(NSImage(named: "Icon_PrivacySettings_ActiveSessions")!.precomposed(flipVertical: true)) }, + privacySettings_passcode: { generateSettingsIcon(NSImage(named: "Icon_SettingsSecurity")!.precomposed(palette.greenUI, flipVertical: true)) }, + privacySettings_twoStep: { generateSettingsIcon(NSImage(named: "Icon_PrivacySettings_TwoStep")!.precomposed(flipVertical: true)) }, + deletedAccount: { NSImage(named: "Icon_DeletedAccount")!.precomposed() }, + stickerPackSelection: { generateStickerPackSelection(.clear) }, + stickerPackSelectionActive: { generateStickerPackSelection(palette.grayForeground) }, + entertainment_Emoji: { NSImage(named: "Icon_Entertainment_Emoji")!.precomposed(palette.grayIcon) }, + entertainment_Stickers: { NSImage(named: "Icon_Entertainment_Stickers")!.precomposed(palette.grayIcon) }, + entertainment_Gifs: { NSImage(named: "Icon_Entertainment_Gifs")!.precomposed(palette.grayIcon) }, + entertainment_Search: { NSImage(named: "Icon_Entertainment_Search")!.precomposed(palette.grayIcon) }, + entertainment_Settings: { NSImage(named: "Icon_Entertainment_Settings")!.precomposed(palette.grayIcon) }, + entertainment_SearchCancel: { NSImage(named: "Icon_Entertainment_SearchCancel")!.precomposed(palette.grayIcon) }, + scheduledAvatar: { NSImage(named: "Icon_AvatarScheduled")!.precomposed(.white) }, + scheduledInputAction: { NSImage(named: "Icon_ChatActionScheduled")!.precomposed(palette.accentIcon) }, + verifyDialog: { generateDialogVerify(background: .white, foreground: palette.basicAccent) }, + verifyDialogActive: { generateDialogVerify(background: palette.accentIcon, foreground: palette.underSelectedColor) }, + chatInputScheduled: { NSImage(named: "Icon_ChatInputScheduled")!.precomposed(palette.grayIcon) }, + appearanceAddPlatformTheme: { + let image = NSImage(named: "Icon_AppearanceAddTheme")!.precomposed(palette.accentIcon) + return generateImage(image.backingSize, contextGenerator: { size, ctx in + ctx.clear(NSMakeRect(0, 0, size.width, size.height)) + ctx.draw(image, in: NSMakeRect(0, 0, size.width, size.height)) + }, scale: System.backingScale)! }, + wallet_close: { #imageLiteral(resourceName: "Icon_ChatSearchCancel").precomposed(palette.accentIcon) }, + wallet_qr: { NSImage(named: "Icon_WalletQR")!.precomposed(palette.accentIcon) }, + wallet_receive: { NSImage(named: "Icon_WalletReceive")!.precomposed(palette.underSelectedColor) }, + wallet_send: { NSImage(named: "Icon_WalletSend")!.precomposed(palette.underSelectedColor) }, + wallet_settings: { NSImage(named: "Icon_WalletSettings")!.precomposed(palette.accentIcon) }, + wallet_update: { NSImage(named: "Icon_WalletUpdate")!.precomposed(palette.grayIcon) }, + wallet_passcode_visible: { NSImage(named: "Icon_WalletPasscodeVisible")!.precomposed(palette.grayIcon) }, + wallet_passcode_hidden: { NSImage(named: "Icon_WalletPasscodeHidden")!.precomposed(palette.grayIcon) }, + wallpaper_color_close: { NSImage(named: "Icon_GradientClose")!.precomposed(palette.grayIcon) }, + wallpaper_color_add: { NSImage(named: "Icon_GradientAdd")!.precomposed(palette.grayIcon) }, + wallpaper_color_swap: { NSImage(named: "Icon_GradientSwap")!.precomposed(palette.grayIcon) }, + wallpaper_color_rotate: { NSImage(named: "Icon_GradientRotate")!.precomposed(.white) }, + wallpaper_color_play: { NSImage(named: "Icon_ChatMusicPlay")!.precomposed(.white) }, + login_cap: { NSImage(named: "Icon_LoginCap")!.precomposed(palette.accentIcon) }, + login_qr_cap: { NSImage(named: "Icon_loginQRCap")!.precomposed(palette.accentIcon) }, + login_qr_empty_cap: { generateLoginQrEmptyCap() }, + chat_failed_scroller: { generateChatFailed(backgroundColor: palette.background, border: palette.redUI, foregroundColor: palette.redUI) }, + chat_failed_scroller_active: { generateChatFailed(backgroundColor: palette.background, border: palette.accentIcon, foregroundColor: palette.accentIcon) }, + poll_quiz_unselected: { generateUnslectedCap(palette.grayText) }, + poll_selected: { generatePollIcon(NSImage(named: "Icon_PollSelected")!, backgound: palette.webPreviewActivity) }, + poll_selection: { generatePollIcon(NSImage(named: "Icon_PollSelected")!, backgound: palette.webPreviewActivity, doubleSize: System.backingScale == 1) }, + poll_selected_correct: { generatePollIcon(NSImage(named: "Icon_PollSelected")!, backgound: palette.greenUI) }, + poll_selected_incorrect: { generatePollIcon(NSImage(named: "Icon_PollSelectedIncorrect")!, backgound: palette.redUI) }, + poll_selected_incoming: { generatePollIcon(NSImage(named: "Icon_PollSelected")!, backgound: palette.webPreviewActivityBubble_incoming) }, + poll_selection_incoming: { generatePollIcon(NSImage(named: "Icon_PollSelected")!, backgound: palette.webPreviewActivityBubble_incoming, doubleSize: System.backingScale == 1) }, + poll_selected_correct_incoming: { generatePollIcon(NSImage(named: "Icon_PollSelected")!, backgound: palette.greenBubble_incoming) }, + poll_selected_incorrect_incoming: { generatePollIcon(NSImage(named: "Icon_PollSelectedIncorrect")!, backgound: palette.redBubble_incoming) }, + poll_selected_outgoing: { generatePollIcon(NSImage(named: "Icon_PollSelected")!, backgound: palette.webPreviewActivityBubble_outgoing) }, + poll_selection_outgoing: { generatePollIcon(NSImage(named: "Icon_PollSelected")!, backgound: palette.webPreviewActivityBubble_outgoing, doubleSize: System.backingScale == 1) }, + poll_selected_correct_outgoing: { generatePollIcon(NSImage(named: "Icon_PollSelected")!, backgound: palette.greenBubble_outgoing) }, + poll_selected_incorrect_outgoing: { generatePollIcon(NSImage(named: "Icon_PollSelectedIncorrect")!, backgound: palette.redBubble_outgoing) }, + chat_filter_edit: { NSImage(named: "Icon_FilterEdit")!.precomposed(palette.accentIcon) }, + chat_filter_add: { NSImage(named: "Icon_FilterAdd")!.precomposed(palette.accentIcon) }, + chat_filter_bots: { NSImage(named: "Icon_FilterBots")!.precomposed(palette.accentIcon) }, + chat_filter_channels: { NSImage(named: "Icon_FilterChannels")!.precomposed(palette.accentIcon) }, + chat_filter_custom: { NSImage(named: "Icon_FilterCustom")!.precomposed(palette.accentIcon) }, + chat_filter_groups: { NSImage(named: "Icon_FilterGroups")!.precomposed(palette.accentIcon) }, + chat_filter_muted: { NSImage(named: "Icon_FilterMuted")!.precomposed(palette.accentIcon) }, + chat_filter_private_chats: { NSImage(named: "Icon_FilterPrivateChats")!.precomposed(palette.accentIcon) }, + chat_filter_read: { NSImage(named: "Icon_FilterRead")!.precomposed(palette.accentIcon) }, + chat_filter_secret_chats: { NSImage(named: "Icon_FilterSecretChats")!.precomposed(palette.accentIcon) }, + chat_filter_unmuted: { NSImage(named: "Icon_FilterUnmuted")!.precomposed(palette.accentIcon) }, + chat_filter_unread: { NSImage(named: "Icon_FilterUnread")!.precomposed(palette.accentIcon) }, + chat_filter_large_groups: { NSImage(named: "Icon_FilterLargeGroups")!.precomposed(palette.accentIcon) }, + chat_filter_non_contacts: { NSImage(named: "Icon_FilterNonContacts")!.precomposed(palette.accentIcon) }, + chat_filter_archive: { NSImage(named: "Icon_FilterArchive")!.precomposed(palette.accentIcon) }, + chat_filter_bots_avatar: { NSImage(named: "Icon_FilterBots")!.precomposed(.white) }, + chat_filter_channels_avatar: { NSImage(named: "Icon_FilterChannels")!.precomposed(.white) }, + chat_filter_custom_avatar: { NSImage(named: "Icon_FilterCustom")!.precomposed(.white) }, + chat_filter_groups_avatar: { NSImage(named: "Icon_FilterGroups")!.precomposed(.white) }, + chat_filter_muted_avatar: { NSImage(named: "Icon_FilterMuted")!.precomposed(.white) }, + chat_filter_private_chats_avatar: { NSImage(named: "Icon_FilterPrivateChats")!.precomposed(.white) }, + chat_filter_read_avatar: { NSImage(named: "Icon_FilterRead")!.precomposed(.white) }, + chat_filter_secret_chats_avatar: { NSImage(named: "Icon_FilterSecretChats")!.precomposed(.white) }, + chat_filter_unmuted_avatar: { NSImage(named: "Icon_FilterUnmuted")!.precomposed(.white) }, + chat_filter_unread_avatar: { NSImage(named: "Icon_FilterUnread")!.precomposed(.white) }, + chat_filter_large_groups_avatar: { NSImage(named: "Icon_FilterLargeGroups")!.precomposed(.white) }, + chat_filter_non_contacts_avatar: { NSImage(named: "Icon_FilterNonContacts")!.precomposed(.white) }, + chat_filter_archive_avatar: { NSImage(named: "Icon_FilterArchive")!.precomposed(.white) }, + group_invite_via_link: { NSImage(named: "Icon_InviteViaLink")!.precomposed(palette.accentIcon) }, + tab_contacts: { NSImage(named: "Icon_TabContacts")!.precomposed(palette.grayIcon) }, + tab_contacts_active: { NSImage(named: "Icon_TabContacts")!.precomposed(palette.accentIcon) }, + tab_calls: { NSImage(named: "Icon_TabRecentCalls")!.precomposed(palette.grayIcon) }, + tab_calls_active: { NSImage(named: "Icon_TabRecentCalls")!.precomposed(palette.accentIcon) }, + tab_chats: { NSImage(named: "Icon_TabChatList")!.precomposed(palette.grayIcon) }, + tab_chats_active: { NSImage(named: "Icon_TabChatList")!.precomposed(palette.accentIcon) }, + tab_chats_active_filters: { generateChatTabFiltersIcon(NSImage(named: "Icon_TabChatList")!.precomposed(palette.accentIcon)) }, + tab_settings: { NSImage(named: "Icon_TabSettings")!.precomposed(palette.grayIcon) }, + tab_settings_active: { NSImage(named: "Icon_TabSettings")!.precomposed(palette.accentIcon) }, + profile_add_member: { generateProfileIcon(NSImage(named: "Icon_Profile_AddMember")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, + profile_call: { generateProfileIcon(NSImage(named: "Icon_Profile_Call")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, + profile_video_call: { generateProfileIcon(NSImage(named: "Icon_Profile_VideoCall")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, + profile_leave: { generateProfileIcon(NSImage(named: "Icon_Profile_Leave")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, + profile_message: { generateProfileIcon(NSImage(named: "Icon_Profile_Message")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, + profile_more: { generateProfileIcon(NSImage(named: "Icon_Profile_More")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, + profile_mute: { generateProfileIcon(NSImage(named: "Icon_Profile_Mute")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, + profile_unmute: { generateProfileIcon(NSImage(named: "Icon_Profile_Unmute")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, + profile_search: { generateProfileIcon(NSImage(named: "Icon_Profile_Search")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, + profile_secret_chat: { generateProfileIcon(NSImage(named: "Icon_Profile_SecretChat")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, + profile_edit_photo: { NSImage(named: "Icon_Profile_EditPhoto")!.precomposed(.white)}, + profile_block: { generateProfileIcon(NSImage(named: "Icon_Profile_Block")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, + profile_report: { generateProfileIcon(NSImage(named: "Icon_Profile_Report")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, + profile_share: { generateProfileIcon(NSImage(named: "Icon_Profile_Share")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, + profile_stats: { generateProfileIcon(NSImage(named: "Icon_Profile_Stats")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, + profile_unblock: { generateProfileIcon(NSImage(named: "Icon_Profile_Unblock")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, + chat_quiz_explanation: { NSImage(named: "Icon_QuizExplanation")!.precomposed(palette.accentIcon) }, + chat_quiz_explanation_bubble_incoming: { NSImage(named: "Icon_QuizExplanation")!.precomposed(palette.accentIconBubble_incoming) }, + chat_quiz_explanation_bubble_outgoing: { NSImage(named: "Icon_QuizExplanation")!.precomposed(palette.accentIconBubble_outgoing) }, + stickers_add_featured: { NSImage(named: "Icon_AddFeaturedStickers")!.precomposed(palette.grayIcon) }, + stickers_add_featured_unread: { generateUnreadFeaturedStickers(NSImage(named: "Icon_AddFeaturedStickers")!.precomposed(palette.grayIcon), palette.redUI) }, + channel_info_promo: { NSImage(named: "Icon_ChannelPromoInfo")!.precomposed(palette.grayIcon) }, + channel_info_promo_bubble_incoming: { NSImage(named: "Icon_ChannelPromoInfo")!.precomposed(palette.grayTextBubble_incoming) }, + channel_info_promo_bubble_outgoing: { NSImage(named: "Icon_ChannelPromoInfo")!.precomposed(palette.grayTextBubble_outgoing) }, + chat_share_message: { NSImage(named: "Icon_ChannelShare")!.precomposed(palette.accent) }, + chat_goto_message: { NSImage(named: "Icon_ChatGoMessage")!.precomposed(palette.accentIcon) }, + chat_swipe_reply: { NSImage(named: "Icon_ChannelShare")!.precomposed(palette.accentIcon, flipHorizontal: true) }, + chat_like_message: { NSImage(named: "Icon_Like_MessageButton")!.precomposed(palette.accentIcon) }, + chat_like_message_unlike: { NSImage(named: "Icon_Like_MessageButtonUnlike")!.precomposed(palette.accentIcon) }, + chat_like_inside: { NSImage(named: "Icon_Like_MessageInside")!.precomposed(palette.redUI, flipVertical: true) }, + chat_like_inside_bubble_incoming: { NSImage(named: "Icon_Like_MessageInside")!.precomposed(palette.redBubble_incoming, flipVertical: true) }, + chat_like_inside_bubble_outgoing: { NSImage(named: "Icon_Like_MessageInside")!.precomposed(palette.redBubble_outgoing, flipVertical: true) }, + chat_like_inside_bubble_overlay: { NSImage(named: "Icon_Like_MessageInside")!.precomposed(.white, flipVertical: true) }, + chat_like_inside_empty: { NSImage(named: "Icon_Like_MessageInsideEmpty")!.precomposed(palette.grayIcon, flipVertical: true) }, + chat_like_inside_empty_bubble_incoming: { NSImage(named: "Icon_Like_MessageInsideEmpty")!.precomposed(palette.grayIconBubble_incoming, flipVertical: true) }, + chat_like_inside_empty_bubble_outgoing: { NSImage(named: "Icon_Like_MessageInsideEmpty")!.precomposed(palette.grayIconBubble_outgoing, flipVertical: true) }, + chat_like_inside_empty_bubble_overlay: { NSImage(named: "Icon_Like_MessageInsideEmpty")!.precomposed(.white, flipVertical: true) }, + gif_trending: { NSImage(named: "Icon_GifTrending")!.precomposed(palette.grayIcon) }, + chat_list_thumb_play: { NSImage(named: "Icon_ChatListThumbPlay")!.precomposed() }, + call_tooltip_battery_low: { NSImage(named: "Icon_Call_BatteryLow")!.precomposed(.white) }, + call_tooltip_camera_off: { NSImage(named: "Icon_Call_CameraOff")!.precomposed(.white) }, + call_tooltip_micro_off: { NSImage(named: "Icon_Call_MicroOff")!.precomposed(.white) }, + call_screen_sharing: { NSImage(named: "Icon_CallScreenSharing")!.precomposed(.white) }, + call_screen_sharing_active: { NSImage(named: "Icon_CallScreenSharing")!.precomposed(.grayIcon) }, + call_screen_settings: { NSImage(named: "Icon_CallScreenSettings")!.precomposed(.white) }, + search_filter: { NSImage(named: "Icon_SearchFilter")!.precomposed(palette.grayIcon) }, + search_filter_media: { NSImage(named: "Icon_SearchFilter_Media")!.precomposed(palette.grayIcon) }, + search_filter_files: { NSImage(named: "Icon_SearchFilter_Files")!.precomposed(palette.grayIcon) }, + search_filter_links: { NSImage(named: "Icon_SearchFilter_Links")!.precomposed(palette.grayIcon) }, + search_filter_music: { NSImage(named: "Icon_SearchFilter_Music")!.precomposed(palette.grayIcon) }, + search_filter_add_peer: { NSImage(named: "Icon_SearchFilter_AddPeer")!.precomposed(palette.grayIcon) }, + search_filter_add_peer_active: { NSImage(named: "Icon_SearchFilter_AddPeer")!.precomposed(palette.underSelectedColor) }, + chat_reply_count_bubble_incoming: { NSImage(named: "Icon_ChatRepliesCount")!.precomposed(palette.grayIconBubble_incoming, flipVertical: true) }, + chat_reply_count_bubble_outgoing: { NSImage(named: "Icon_ChatRepliesCount")!.precomposed(palette.grayIconBubble_outgoing, flipVertical: true) }, + chat_reply_count: { NSImage(named: "Icon_ChatRepliesCount")!.precomposed(palette.grayIcon, flipVertical: true) }, + chat_reply_count_overlay: { NSImage(named: "Icon_ChatRepliesCount")!.precomposed(.white, flipVertical: true) }, + channel_comments_bubble: { NSImage(named: "Icon_ChannelComments_Bubble")!.precomposed(palette.accentIcon + , flipVertical: true) }, + channel_comments_bubble_next: { NSImage(named: "Icon_ChannelComments_Next")!.precomposed(palette.accentIcon, flipVertical: true) }, + channel_comments_list: { NSImage(named: "Icon_ChannelComments")!.precomposed(palette.accent, flipVertical: true) }, + channel_comments_overlay: { NSImage(named: "Icon_ChannelComments_Bubble")!.precomposed(palette.accent) }, + chat_replies_avatar: { NSImage(named: "Icon_RepliesChat")!.precomposed() }, + group_selection_foreground: { generateChatGroupToggleSelectionForeground(foregroundColor: palette.grayText.withAlphaComponent(0.4), backgroundColor: NSColor.black.withAlphaComponent(0.1)) }, + group_selection_foreground_bubble_incoming: { generateChatGroupToggleSelectionForeground(foregroundColor: palette.grayTextBubble_incoming.withAlphaComponent(0.4), backgroundColor: NSColor.black.withAlphaComponent(0.1)) }, + group_selection_foreground_bubble_outgoing: { generateChatGroupToggleSelectionForeground(foregroundColor: palette.grayTextBubble_outgoing.withAlphaComponent(0.4), backgroundColor: NSColor.black.withAlphaComponent(0.1)) }, + chat_pinned_list: { NSImage(named: "Icon_ChatPinnedList")!.precomposed(palette.accentIcon) }, + chat_pinned_message: { NSImage(named: "Icon_ChatPinnedMessage")!.precomposed(palette.accentIcon, flipVertical: true) }, + chat_pinned_message_bubble_incoming: { NSImage(named: "Icon_ChatPinnedMessage")!.precomposed(palette.grayIconBubble_incoming, flipVertical: true) }, + chat_pinned_message_bubble_outgoing: { NSImage(named: "Icon_ChatPinnedMessage")!.precomposed(palette.grayIconBubble_outgoing, flipVertical: true) }, + chat_pinned_message_overlay_bubble: { NSImage(named: "Icon_ChatPinnedMessage")!.precomposed(.white, flipVertical: true) }, + chat_voicechat_can_unmute: { NSImage(named: "Icon_GroupCall_Small_Muted")!.precomposed(.white) }, + chat_voicechat_cant_unmute: { NSImage(named: "Icon_GroupCall_Small_Muted")!.precomposed(palette.redUI) }, + chat_voicechat_unmuted: { NSImage(named: "Icon_GroupCall_Small_Unmuted")!.precomposed(.white) }, + profile_voice_chat: { generateProfileIcon(NSImage(named: "Icon_Profile_VoiceChat")!.precomposed(palette.accentIcon), backgroundColor: palette.accent) }, + chat_voice_chat: { generateChatAction(NSImage(named: "Icon_VoiceChat_Title")!.precomposed(palette.accentIcon), background: palette.background) }, + chat_voice_chat_active: { generateChatAction(NSImage(named: "Icon_VoiceChat_Title")!.precomposed(palette.accentIcon), background: palette.grayIcon.withAlphaComponent(0.1)) }, + editor_draw: { NSImage(named: "Icon_Editor_Paint")!.precomposed(.white) }, + editor_delete: { NSImage(named: "Icon_Editor_Delete")!.precomposed(.white) }, + editor_crop: { NSImage(named: "Icon_Editor_Crop")!.precomposed(.white) }, + fast_copy_link: { NSImage(named: "Icon_FastCopyLink")!.precomposed(palette.accent) }, + profile_channel_sign: {NSImage(named: "Icon_Profile_ChannelSign")!.precomposed(flipVertical: true)}, + profile_channel_type: {NSImage(named: "Icon_Profile_ChannelType")!.precomposed(flipVertical: true)}, + profile_group_type: {NSImage(named: "Icon_Profile_GroupType")!.precomposed(flipVertical: true)}, + profile_group_destruct: {NSImage(named: "Icon_Profile_Destruct")!.precomposed(flipVertical: true)}, + profile_group_discussion: {NSImage(named: "Icon_Profile_Discussion")!.precomposed(flipVertical: true)}, + profile_removed: {NSImage(named: "Icon_Profile_Removed")!.precomposed(flipVertical: true)}, + profile_links: {NSImage(named: "Icon_Profile_Links")!.precomposed(flipVertical: true)}, + destruct_clear_history: { NSImage(named: "Icon_ClearChat")!.precomposed(palette.redUI, flipVertical: true) }, + chat_gigagroup_info: { NSImage(named: "Icon_GigagroupInfo")!.precomposed(palette.accent) }, + playlist_next: { NSImage(named: "Icon_PlayList_Next")!.precomposed(palette.text) }, + playlist_prev: { NSImage(named: "Icon_PlayList_Next")!.precomposed(palette.text, flipHorizontal: true) }, + playlist_next_locked: { NSImage(named: "Icon_PlayList_Next")!.precomposed(palette.text.withAlphaComponent(0.6)) }, + playlist_prev_locked: { NSImage(named: "Icon_PlayList_Next")!.precomposed(palette.text.withAlphaComponent(0.6), flipHorizontal: true) }, + playlist_random: { NSImage(named: "Icon_PlayList_Random")!.precomposed(palette.text) }, + playlist_order_normal: { NSImage(named: "Icon_PlayList_Order")!.precomposed(palette.text) }, + playlist_order_reversed: { NSImage(named: "Icon_PlayList_Order")!.precomposed(palette.accent) }, + playlist_order_random: { NSImage(named: "Icon_PlayList_Random")!.precomposed(palette.accent) }, + playlist_repeat_none: { NSImage(named: "Icon_PlayList_Repeat")!.precomposed(palette.text) }, + playlist_repeat_circle: { NSImage(named: "Icon_PlayList_Repeat")!.precomposed(palette.accent) }, + playlist_repeat_one: { NSImage(named: "Icon_PlayList_RepeatOne")!.precomposed(palette.accent) }, + audioplayer_next: { NSImage(named: "Icon_InlinePlayerNext")!.precomposed(palette.accent) }, + audioplayer_prev: { NSImage(named: "Icon_InlinePlayerNext")!.precomposed(palette.accent, flipHorizontal: true) }, + audioplayer_dismiss: { NSImage(named: "Icon_ChatSearchCancel")!.precomposed(palette.accentIcon) }, + audioplayer_repeat_none: { NSImage(named: "Icon_InlinePlayer_Repeat")!.precomposed(palette.grayIcon) }, + audioplayer_repeat_circle: { NSImage(named: "Icon_InlinePlayer_Repeat")!.precomposed(palette.accent) }, + audioplayer_repeat_one: { NSImage(named: "Icon_InlinePlayer_RepeatOne")!.precomposed(palette.accentIcon) }, + audioplayer_locked_next: { NSImage(named: "Icon_InlinePlayerNext")!.precomposed(palette.grayIcon) }, + audioplayer_locked_prev: { NSImage(named: "Icon_InlinePlayerNext")!.precomposed(palette.grayIcon, flipHorizontal: true) }, + audioplayer_volume: { NSImage(named: "Icon_InlinePlayer_VolumeOn")!.precomposed(palette.accent) }, + audioplayer_volume_off: { NSImage(named: "Icon_InlinePlayer_VolumeOff")!.precomposed(palette.grayIcon) }, + audioplayer_speed_x1: { NSImage(named: "Icon_InlinePlayer_x2")!.precomposed(palette.grayIcon) }, + audioplayer_speed_x2: { NSImage(named: "Icon_InlinePlayer_x2")!.precomposed(palette.accentIcon) }, + chat_info_voice_chat: { NSImage(named: "Icon_VoiceChat_Title")!.precomposed(palette.accentIcon) }, + chat_info_create_group: { NSImage(named: "Icon_NewGroup")!.precomposed(palette.accentIcon) }, + chat_info_change_colors: { NSImage(named: "Icon_ChangeColors")!.precomposed(palette.accentIcon) }, + empty_chat_system: { NSImage(named: "Icon_EmptyChat_System")!.precomposed(palette.text) }, + empty_chat_dark: { NSImage(named: "Icon_EmptyChat_Dark")!.precomposed(palette.text) }, + empty_chat_light: { NSImage(named: "Icon_EmptyChat_Light")!.precomposed(palette.text) }, + empty_chat_system_active: { NSImage(named: "Icon_EmptyChat_System")!.precomposed(palette.accent) }, + empty_chat_dark_active: { NSImage(named: "Icon_EmptyChat_Dark")!.precomposed(palette.accent) }, + empty_chat_light_active: { NSImage(named: "Icon_EmptyChat_Light")!.precomposed(palette.accent) }, + empty_chat_storage_clear: { NSImage(named: "Icon_EmptyChat_Storage_Clear")!.precomposed(palette.text) }, + empty_chat_storage_low: { NSImage(named: "Icon_EmptyChat_Storage_Medium")!.precomposed(palette.text) }, + empty_chat_storage_medium: { NSImage(named: "Icon_EmptyChat_Storage_High")!.precomposed(palette.text) }, + empty_chat_storage_high: { NSImage(named: "Icon_EmptyChat_Storage_NoLimit")!.precomposed(palette.text) }, + empty_chat_storage_low_active: { NSImage(named: "Icon_EmptyChat_Storage_Medium")!.precomposed(palette.accent) }, + empty_chat_storage_medium_active: { NSImage(named: "Icon_EmptyChat_Storage_High")!.precomposed(palette.accent) }, + empty_chat_storage_high_active: { NSImage(named: "Icon_EmptyChat_Storage_NoLimit")!.precomposed(palette.accent) }, + empty_chat_stickers_none: { NSImage(named: "Icon_EmptyChat_Stickers_None")!.precomposed(palette.text) }, + empty_chat_stickers_mysets: { NSImage(named: "Icon_EmptyChat_Stickers_MySets")!.precomposed(palette.text) }, + empty_chat_stickers_allsets: { NSImage(named: "Icon_EmptyChat_Stickers_AllSets")!.precomposed(palette.text) }, + empty_chat_stickers_none_active: { NSImage(named: "Icon_EmptyChat_Stickers_None")!.precomposed(palette.accent) }, + empty_chat_stickers_mysets_active: { NSImage(named: "Icon_EmptyChat_Stickers_MySets")!.precomposed(palette.accent) }, + empty_chat_stickers_allsets_active: { NSImage(named: "Icon_EmptyChat_Stickers_AllSets")!.precomposed(palette.accent) }, + chat_action_dismiss: { NSImage(named: "Icon_ChatAction_Close")!.precomposed(palette.accent) }, + chat_action_edit_message: { NSImage(named: "Icon_ChatAction_EditMessage")!.precomposed(palette.accent) }, + chat_action_forward_message: { NSImage(named: "Icon_ChatAction_ForwardMessage")!.precomposed(palette.accent) }, + chat_action_reply_message: { NSImage(named: "Icon_ChatAction_ReplyMessage")!.precomposed(palette.accent) }, + chat_action_url_preview: { NSImage(named: "Icon_ChatAction_UrlPreview")!.precomposed(palette.accent) }, + chat_action_menu_update_chat: { NSImage(named: "Icon_ChatAction_Menu_UpdateChat")!.precomposed(palette.accent) }, + chat_action_menu_selected: { NSImage(named: "Icon_UsernameAvailability")!.precomposed(palette.accent) }, + widget_peers_favorite: { NSImage(named: "Icon_Widget_Peers_Favorite")!.precomposed(palette.text) }, + widget_peers_recent: { NSImage(named: "Icon_Widget_Peers_Recent")!.precomposed(palette.text) }, + widget_peers_both: { NSImage(named: "Icon_Widget_Peers_Both")!.precomposed(palette.text) }, + widget_peers_favorite_active: { NSImage(named: "Icon_Widget_Peers_Favorite")!.precomposed(palette.accent) }, + widget_peers_recent_active: { NSImage(named: "Icon_Widget_Peers_Recent")!.precomposed(palette.accent) }, + widget_peers_both_active: { NSImage(named: "Icon_Widget_Peers_Both")!.precomposed(palette.accent) } + ) -private func generateIcons(from pallete: ColorPallete) -> TelegramIconsTheme { - return TelegramIconsTheme(dialogMuteImage: #imageLiteral(resourceName: "Icon_DialogMute").precomposed(pallete.grayIcon), - dialogMuteImageSelected: #imageLiteral(resourceName: "Icon_DialogMute").precomposed(.white), - outgoingMessageImage: #imageLiteral(resourceName: "Icon_MessageCheckMark1").precomposed(pallete.blueIcon, flipVertical:true), - readMessageImage: #imageLiteral(resourceName: "Icon_MessageCheckmark2").precomposed(pallete.blueIcon, flipVertical:true), - outgoingMessageImageSelected: #imageLiteral(resourceName: "Icon_MessageCheckMark1").precomposed(.white, flipVertical:true), - readMessageImageSelected: #imageLiteral(resourceName: "Icon_MessageCheckmark2").precomposed(.white, flipVertical:true), - sendingImage: #imageLiteral(resourceName: "Icon_ChatStateSending").precomposed(pallete.grayIcon, flipVertical:true), - sendingImageSelected: #imageLiteral(resourceName: "Icon_ChatStateSending").precomposed(.white, flipVertical:true), - secretImage:#imageLiteral(resourceName: "Icon_SecretChatLock").precomposed(pallete.blueIcon, flipVertical:true), - secretImageSelected: #imageLiteral(resourceName: "Icon_SecretChatLock").precomposed(.white, flipVertical:true), - pinnedImage: #imageLiteral(resourceName: "Icon_ChatListPinned").precomposed(pallete.grayIcon, flipVertical:true), - pinnedImageSelected: #imageLiteral(resourceName: "Icon_ChatListPinned").precomposed(.white, flipVertical:true), - verifiedImage: #imageLiteral(resourceName: "Icon_VerifyPeer").precomposed(flipVertical: true), - verifiedImageSelected: #imageLiteral(resourceName: "Icon_VerifyPeerActive").precomposed(flipVertical: true), - errorImage: #imageLiteral(resourceName: "Icon_DialogSendingError").precomposed(flipVertical: true), - errorImageSelected: #imageLiteral(resourceName: "Icon_MessageSentFailed").precomposed(flipVertical: true), - chatSearch: #imageLiteral(resourceName: "Icon_SearchChatMessages").precomposed(pallete.blueIcon), - chatCall: #imageLiteral(resourceName: "Icon_callNavigationHeader").precomposed(pallete.blueIcon), - chatActions: #imageLiteral(resourceName: "Icon_ChatActions").precomposed(pallete.blueIcon), - chatOutgoingFailedCall: #imageLiteral(resourceName: "Icon_MessageCallOutgoing").precomposed(pallete.redUI), - chatIncomingFailedCall: #imageLiteral(resourceName: "Icon_MessageCallIncoming").precomposed(pallete.redUI), - chatOutgoingCall: #imageLiteral(resourceName: "Icon_MessageCallOutgoing").precomposed(pallete.greenUI), - chatIncomingCall: #imageLiteral(resourceName: "Icon_MessageCallIncoming").precomposed(pallete.greenUI), - chatFallbackCall: #imageLiteral(resourceName: "Icon_MessageCall").precomposed(pallete.blueUI), - chatToggleSelected: #imageLiteral(resourceName: "Icon_Check").precomposed(pallete.blueIcon), - chatToggleUnselected: #imageLiteral(resourceName: "Icon_SelectionUncheck").precomposed(), - chatShare: #imageLiteral(resourceName: "Icon_ChannelShare").precomposed(pallete.blueIcon), - chatMusicPlay: #imageLiteral(resourceName: "Icon_ChatMusicPlay").precomposed(), - chatMusicPause: #imageLiteral(resourceName: "Icon_ChatMusicPause").precomposed(), - composeNewChat:#imageLiteral(resourceName: "Icon_NewMessage").precomposed(pallete.blueIcon), - composeNewChatActive:#imageLiteral(resourceName: "Icon_NewMessage").precomposed(.white), - composeNewGroup:#imageLiteral(resourceName: "Icon_NewGroup").precomposed(pallete.blueIcon), - composeNewSecretChat: #imageLiteral(resourceName: "Icon_NewSecretChat").precomposed(pallete.blueIcon), - composeNewChannel: #imageLiteral(resourceName: "Icon_NewChannel").precomposed(pallete.blueIcon), - contactsNewContact: #imageLiteral(resourceName: "Icon_NewContact").precomposed(pallete.blueIcon), - chatReadMark1: #imageLiteral(resourceName: "Icon_MessageCheckMark1").precomposed(pallete.blueIcon), - chatReadMark2: #imageLiteral(resourceName: "Icon_MessageCheckmark2").precomposed(pallete.blueIcon), - sentFailed: #imageLiteral(resourceName: "Icon_MessageSentFailed").precomposed(), - chatChannelViews: #imageLiteral(resourceName: "Icon_ChannelViews").precomposed(pallete.grayIcon, flipVertical: true), - chatNavigationBack: #imageLiteral(resourceName: "Icon_ChatNavigationBack").precomposed(pallete.blueIcon), - peerInfoAddMember: #imageLiteral(resourceName: "Icon_GroupInfoAddMember").precomposed(pallete.blueIcon, flipVertical: true), - chatSearchUp: #imageLiteral(resourceName: "Icon_SearchArrow").precomposed(pallete.blueIcon), - chatSearchUpDisabled: #imageLiteral(resourceName: "Icon_SearchArrow").precomposed(pallete.grayIcon), - chatSearchDown: #imageLiteral(resourceName: "Icon_SearchArrow").precomposed(pallete.blueIcon, flipVertical:true), - chatSearchDownDisabled: #imageLiteral(resourceName: "Icon_SearchArrow").precomposed(pallete.grayIcon, flipVertical:true), - chatSearchCalendar: #imageLiteral(resourceName: "Icon_Calendar").precomposed(pallete.blueIcon), - dismissAccessory: #imageLiteral(resourceName: "Icon_ChatSearchCancel").precomposed(pallete.grayIcon), - chatScrollUp: generateChatScrolldownImage(backgroundColor: pallete.background, borderColor: pallete.grayIcon, arrowColor: pallete.grayIcon), - chatScrollUpActive: generateChatScrolldownImage(backgroundColor: pallete.background, borderColor: pallete.blueIcon, arrowColor: pallete.blueIcon), - audioPlayerPlay: #imageLiteral(resourceName: "Icon_InlinePlayerPlay").precomposed(pallete.blueIcon), - audioPlayerPause: #imageLiteral(resourceName: "Icon_InlinePlayerPause").precomposed(pallete.blueIcon), - audioPlayerNext: #imageLiteral(resourceName: "Icon_InlinePlayerNext").precomposed(pallete.blueIcon), - audioPlayerPrev: #imageLiteral(resourceName: "Icon_InlinePlayerPrevious").precomposed(pallete.blueIcon), - auduiPlayerDismiss: #imageLiteral(resourceName: "Icon_ChatSearchCancel").precomposed(pallete.blueIcon), - audioPlayerRepeat: #imageLiteral(resourceName: "Icon_RepeatAudio").precomposed(pallete.grayIcon), - audioPlayerRepeatActive: #imageLiteral(resourceName: "Icon_RepeatAudio").precomposed(pallete.blueIcon), - audioPlayerLockedPlay: #imageLiteral(resourceName: "Icon_InlinePlayerPlay").precomposed(pallete.grayIcon), - audioPlayerLockedNext: #imageLiteral(resourceName: "Icon_InlinePlayerNext").precomposed(pallete.grayIcon), - audioPlayerLockedPrev: #imageLiteral(resourceName: "Icon_InlinePlayerPrevious").precomposed(pallete.grayIcon), - chatSendMessage: #imageLiteral(resourceName: "Icon_SendMessage").precomposed(pallete.blueIcon), - chatRecordVoice: #imageLiteral(resourceName: "Icon_RecordVoice").precomposed(pallete.grayIcon), - chatEntertainment: #imageLiteral(resourceName: "Icon_Entertainments").precomposed(pallete.grayIcon), - chatInlineDismiss: #imageLiteral(resourceName: "Icon_InlineResultCancel").precomposed(pallete.grayIcon), - chatActiveReplyMarkup: #imageLiteral(resourceName: "Icon_ReplyMarkupButton").precomposed(pallete.blueIcon), - chatDisabledReplyMarkup: #imageLiteral(resourceName: "Icon_ReplyMarkupButton").precomposed(pallete.grayIcon), - chatSecretTimer: #imageLiteral(resourceName: "Icon_SecretTimer").precomposed(pallete.grayIcon), - chatForwardMessagesActive: #imageLiteral(resourceName: "Icon_MessageActionPanelForward").precomposed(pallete.blueIcon), - chatForwardMessagesInactive: #imageLiteral(resourceName: "Icon_MessageActionPanelForward").precomposed(pallete.grayIcon), - chatDeleteMessagesActive: #imageLiteral(resourceName: "Icon_MessageActionPanelDelete").precomposed(pallete.redUI), - chatDeleteMessagesInactive: #imageLiteral(resourceName: "Icon_MessageActionPanelDelete").precomposed(pallete.grayIcon), - generalNext: #imageLiteral(resourceName: "Icon_GeneralNext").precomposed(pallete.grayIcon), - generalSelect: #imageLiteral(resourceName: "Icon_UsernameAvailability").precomposed(pallete.blueIcon), - chatVoiceRecording: #imageLiteral(resourceName: "Icon_RecordingVoice").precomposed(pallete.blueIcon), - chatVideoRecording: #imageLiteral(resourceName: "Icon_RecordVideoMessage").precomposed(pallete.blueIcon), - chatRecord: #imageLiteral(resourceName: "Icon_RecordVoice").precomposed(pallete.grayIcon), - deleteItem: deleteItemIcon(pallete.redUI), - deleteItemDisabled: deleteItemIcon(pallete.grayTransparent), - chatAttach: #imageLiteral(resourceName: "Icon_ChatAttach").precomposed(pallete.grayIcon), - chatAttachFile: #imageLiteral(resourceName: "Icon_AttachFile").precomposed(pallete.blueIcon), - chatAttachPhoto: #imageLiteral(resourceName: "Icon_AttachPhoto").precomposed(pallete.blueIcon), - chatAttachCamera: #imageLiteral(resourceName: "Icon_AttachCamera").precomposed(pallete.blueIcon), - chatAttachLocation: #imageLiteral(resourceName: "Icon_AttachLocation").precomposed(pallete.blueIcon), - mediaEmptyShared: #imageLiteral(resourceName: "Icon_EmptySharedMedia").precomposed(pallete.grayIcon), - mediaEmptyFiles: #imageLiteral(resourceName: "Icon_EmptySharedFiles").precomposed(), - mediaEmptyMusic: #imageLiteral(resourceName: "Icon_EmptySharedMusic").precomposed(pallete.grayIcon), - mediaEmptyLinks: #imageLiteral(resourceName: "Icon_EmptySharedLinks").precomposed(pallete.grayIcon), - mediaDropdown: #imageLiteral(resourceName: "Icon_DropdownArrow").precomposed(pallete.blueIcon), - stickersAddFeatured: #imageLiteral(resourceName: "Icon_GroupInfoAddMember").precomposed(pallete.blueIcon), - stickersAddedFeatured: #imageLiteral(resourceName: "Icon_UsernameAvailability").precomposed(pallete.grayIcon), - stickersRemove: #imageLiteral(resourceName: "Icon_InlineResultCancel").precomposed(pallete.grayIcon), - peerMediaDownloadFileStart: #imageLiteral(resourceName: "Icon_MediaDownload").precomposed(pallete.blueIcon), - peerMediaDownloadFilePause: downloadFilePauseIcon(pallete.blueIcon), - stickersShare: #imageLiteral(resourceName: "Icon_ShareStickerPack").precomposed(pallete.blueIcon), - emojiRecentTab: #imageLiteral(resourceName: "Icon_EmojiTabRecent").precomposed(pallete.grayIcon, flipVertical:true, flipHorizontal:true), - emojiSmileTab: #imageLiteral(resourceName: "Icon_EmojiTabSmiles").precomposed(pallete.grayIcon, flipVertical:true, flipHorizontal:true), - emojiNatureTab: #imageLiteral(resourceName: "Icon_EmojiTabNature").precomposed(pallete.grayIcon, flipVertical:true, flipHorizontal:true), - emojiFoodTab: #imageLiteral(resourceName: "Icon_EmojiTabFood").precomposed(pallete.grayIcon, flipVertical:true, flipHorizontal:true), - emojiSportTab: #imageLiteral(resourceName: "Icon_EmojiTabSports").precomposed(pallete.grayIcon, flipVertical:true, flipHorizontal:true), - emojiCarTab: #imageLiteral(resourceName: "Icon_EmojiTabCar").precomposed(pallete.grayIcon, flipVertical:true, flipHorizontal:true), - emojiObjectsTab: #imageLiteral(resourceName: "Icon_EmojiTabObjects").precomposed(pallete.grayIcon, flipVertical:true, flipHorizontal:true), - emojiSymbolsTab: #imageLiteral(resourceName: "Icon_EmojiTabSymbols").precomposed(pallete.grayIcon, flipVertical:true, flipHorizontal:true), - emojiFlagsTab: #imageLiteral(resourceName: "Icon_EmojiTabFlag").precomposed(pallete.grayIcon, flipVertical:true, flipHorizontal:true), - emojiRecentTabActive: #imageLiteral(resourceName: "Icon_EmojiTabRecent").precomposed(pallete.blueIcon, flipVertical:true, flipHorizontal:true), - emojiSmileTabActive: #imageLiteral(resourceName: "Icon_EmojiTabSmiles").precomposed(pallete.blueIcon, flipVertical:true, flipHorizontal:true), - emojiNatureTabActive: #imageLiteral(resourceName: "Icon_EmojiTabNature").precomposed(pallete.blueIcon, flipVertical:true, flipHorizontal:true), - emojiFoodTabActive: #imageLiteral(resourceName: "Icon_EmojiTabFood").precomposed(pallete.blueIcon, flipVertical:true, flipHorizontal:true), - emojiSportTabActive: #imageLiteral(resourceName: "Icon_EmojiTabSports").precomposed(pallete.blueIcon, flipVertical:true, flipHorizontal:true), - emojiCarTabActive: #imageLiteral(resourceName: "Icon_EmojiTabCar").precomposed(pallete.blueIcon, flipVertical:true, flipHorizontal:true), - emojiObjectsTabActive: #imageLiteral(resourceName: "Icon_EmojiTabObjects").precomposed(pallete.blueIcon, flipVertical:true, flipHorizontal:true), - emojiSymbolsTabActive: #imageLiteral(resourceName: "Icon_EmojiTabSymbols").precomposed(pallete.blueIcon, flipVertical:true, flipHorizontal:true), - emojiFlagsTabActive: #imageLiteral(resourceName: "Icon_EmojiTabFlag").precomposed(pallete.blueIcon, flipVertical:true, flipHorizontal:true), - stickerBackground: generateStickerBackground(NSMakeSize(83, 83), pallete.background), - stickerBackgroundActive: generateStickerBackground(NSMakeSize(83, 83), pallete.grayBackground), - stickersTabRecent: #imageLiteral(resourceName: "Icon_EmojiTabRecent").precomposed(pallete.grayIcon), - stickersTabGIF: #imageLiteral(resourceName: "Icon_GifToggle").precomposed(pallete.grayIcon), - chatSendingFrame: generateSendingFrame(pallete.grayIcon), - chatSendingHour: generateSendingHour(pallete.grayIcon), - chatSendingMin: generateSendingMin(pallete.grayIcon), - chatActionUrl: #imageLiteral(resourceName: "Icon_InlineBotUrl").precomposed(pallete.text), - callInlineDecline: #imageLiteral(resourceName: "Icon_CallDecline_Inline").precomposed(.white), - callInlineMuted: #imageLiteral(resourceName: "Icon_CallMute_Inline").precomposed(.white), - callInlineUnmuted: #imageLiteral(resourceName: "Icon_CallUnmuted_Inline").precomposed(.white), - eventLogTriangle: generateRecentActionsTriangle(pallete.text), - channelIntro: #imageLiteral(resourceName: "Icon_ChannelIntro").precomposed(), - chatFileThumb: #imageLiteral(resourceName: "Icon_MessageFile").precomposed(flipVertical:true), - chatSecretThumb: #imageLiteral(resourceName: "Icon_SecretAutoremoveMedia").precomposed(.black, flipVertical:true), - chatMapPin: #imageLiteral(resourceName: "Icon_MapPinned").precomposed(), - chatSecretTitle: #imageLiteral(resourceName: "Icon_SecretChatLock").precomposed(pallete.text, flipVertical:true), - emptySearch: #imageLiteral(resourceName: "Icon_EmptySearchResults").precomposed(pallete.grayIcon), - calendarBack: #imageLiteral(resourceName: "Icon_NavigationBack").precomposed(pallete.blueIcon), - calendarNext: #imageLiteral(resourceName: "Icon_NavigationBack").precomposed(pallete.blueIcon, flipHorizontal: true), - calendarBackDisabled: #imageLiteral(resourceName: "Icon_NavigationBack").precomposed(pallete.grayIcon), - calendarNextDisabled: #imageLiteral(resourceName: "Icon_NavigationBack").precomposed(pallete.grayIcon, flipHorizontal: true), - newChatCamera: #imageLiteral(resourceName: "Icon_AttachCamera").precomposed(pallete.grayIcon), - peerInfoVerify: #imageLiteral(resourceName: "Icon_VerifyPeer").precomposed(flipVertical: true), - peerInfoCall: #imageLiteral(resourceName: "Icon_ProfileCall").precomposed(pallete.blueUI), - callOutgoing: #imageLiteral(resourceName: "Icon_CallOutgoing").precomposed(pallete.grayIcon, flipVertical: true), - recentDismiss: #imageLiteral(resourceName: "Icon_SearchClear").precomposed(pallete.grayIcon), - recentDismissActive: #imageLiteral(resourceName: "Icon_SearchClear").precomposed(.white), - webgameShare: #imageLiteral(resourceName: "Icon_ShareExternal").precomposed(pallete.blueIcon), - chatSearchCancel: #imageLiteral(resourceName: "Icon_ChatSearchCancel").precomposed(pallete.blueIcon), - chatSearchFrom: #imageLiteral(resourceName: "Icon_ChatSearchFrom").precomposed(pallete.blueIcon), - callWindowDecline: #imageLiteral(resourceName: "Icon_CallDecline_Window").precomposed(), - callWindowAccept: #imageLiteral(resourceName: "Icon_CallAccept_Window").precomposed(), - callWindowMute: #imageLiteral(resourceName: "Icon_CallMic_Window").precomposed(), - callWindowUnmute: #imageLiteral(resourceName: "Icon_CallMute_Inline").precomposed(), - callWindowClose: #imageLiteral(resourceName: "Icon_CallWindowClose").precomposed(.white), - callWindowDeviceSettings: #imageLiteral(resourceName: "Icon_CallDeviceSettings").precomposed(.white), - callWindowCancel: #imageLiteral(resourceName: "Icon_CallCancelIcon").precomposed(.white), - chatActionEdit: #imageLiteral(resourceName: "Icon_ChatActionEdit").precomposed(pallete.blueIcon), - chatActionInfo: #imageLiteral(resourceName: "Icon_ChatActionInfo").precomposed(pallete.blueIcon), - chatActionMute: #imageLiteral(resourceName: "Icon_ChatActionMute").precomposed(pallete.blueIcon), - chatActionUnmute: #imageLiteral(resourceName: "Icon_ChatActionUnmute").precomposed(pallete.blueIcon), - chatActionClearHistory: #imageLiteral(resourceName: "Icon_MessageActionPanelDelete").precomposed(pallete.blueIcon), - dismissPinned: #imageLiteral(resourceName: "Icon_ChatSearchCancel").precomposed(pallete.blueIcon), - chatActionsActive: #imageLiteral(resourceName: "Icon_ChatActionsActive").precomposed(pallete.blueIcon), - chatEntertainmentSticker: #imageLiteral(resourceName: "Icon_ChatEntertainmentSticker").precomposed(pallete.grayIcon), - chatEmpty: #imageLiteral(resourceName: "Icon_EmptyChat").precomposed(pallete.grayForeground), - stickerPackClose: #imageLiteral(resourceName: "Icon_ChatSearchCancel").precomposed(pallete.blueIcon), - stickerPackDelete: #imageLiteral(resourceName: "Icon_MessageActionPanelDelete").precomposed(pallete.blueIcon), - modalShare: #imageLiteral(resourceName: "Icon_ShareStickerPack").precomposed(pallete.blueIcon), - modalClose: #imageLiteral(resourceName: "Icon_ChatSearchCancel").precomposed(pallete.blueIcon), - ivChannelJoined: #imageLiteral(resourceName: "Icon_MessageCheckMark1").precomposed(.white), - chatListMention: generateBadgeMention(backgroundColor: pallete.blueUI, foregroundColor: pallete.background), - chatListMentionActive: generateBadgeMention(backgroundColor: pallete.background, foregroundColor: pallete.blueUI), - chatMention: generateChatMention(backgroundColor: pallete.background, border: pallete.grayIcon, foregroundColor: pallete.grayIcon), - chatMentionActive: generateChatMention(backgroundColor: pallete.background, border: pallete.blueIcon, foregroundColor: pallete.blueIcon), - sliderControl: #imageLiteral(resourceName: "Icon_SliderNormal").precomposed(), - sliderControlActive: #imageLiteral(resourceName: "Icon_SliderNormal").precomposed(), - stickersTabFave: #imageLiteral(resourceName: "Icon_FaveStickers").precomposed(pallete.grayIcon), - chatInstantView: #imageLiteral(resourceName: "Icon_ChatIV").precomposed(pallete.blueIcon), - instantViewShare: #imageLiteral(resourceName: "Icon_ShareStickerPack").precomposed(pallete.blueIcon), - instantViewActions: #imageLiteral(resourceName: "Icon_ChatActions").precomposed(pallete.blueIcon), - instantViewActionsActive: #imageLiteral(resourceName: "Icon_ChatActionsActive").precomposed(pallete.blueIcon), - instantViewSafari: #imageLiteral(resourceName: "Icon_InstantViewSafari").precomposed(pallete.blueIcon), - instantViewBack: #imageLiteral(resourceName: "Icon_InstantViewBack").precomposed(pallete.blueIcon), - instantViewCheck: #imageLiteral(resourceName: "Icon_InstantViewCheck").precomposed(pallete.blueIcon), - groupStickerNotFound: #imageLiteral(resourceName: "Icon_GroupStickerNotFound").precomposed(pallete.grayIcon), - settingsAskQuestion: #imageLiteral(resourceName: "Icon_SettingsAskQuestion").precomposed(pallete.blueIcon, flipVertical: true), - settingsBio: #imageLiteral(resourceName: "Icon_SettingsBio").precomposed(pallete.blueIcon, flipVertical: true), - settingsEditInfo: #imageLiteral(resourceName: "Icon_SettingsEditInfo").precomposed(pallete.blueIcon), - settingsFaq: #imageLiteral(resourceName: "Icon_SettingsFaq").precomposed(pallete.blueIcon, flipVertical: true), - settingsGeneral: #imageLiteral(resourceName: "Icon_SettingsGeneral").precomposed(pallete.blueIcon, flipVertical: true), - settingsLanguage: #imageLiteral(resourceName: "Icon_SettingsLanguage").precomposed(pallete.blueIcon, flipVertical: true), - settingsNotifications: #imageLiteral(resourceName: "Icon_SettingsNotifications").precomposed(pallete.blueIcon, flipVertical: true), - settingsPhoneNumber: #imageLiteral(resourceName: "Icon_SettingsPhoneNumber").precomposed(pallete.blueIcon, flipVertical: true), - settingsSecurity: #imageLiteral(resourceName: "Icon_SettingsSecurity").precomposed(pallete.blueIcon, flipVertical: true), - settingsStickers: #imageLiteral(resourceName: "Icon_SettingsStickers").precomposed(pallete.blueIcon, flipVertical: true), - settingsStorage: #imageLiteral(resourceName: "Icon_SettingsStorage").precomposed(pallete.blueIcon, flipVertical: true), - settingsUsername: #imageLiteral(resourceName: "Icon_SettingsUsername").precomposed(pallete.blueIcon, flipVertical: true), - generalCheck: #imageLiteral(resourceName: "Icon_Check").precomposed(pallete.blueIcon), - settingsAbout: #imageLiteral(resourceName: "Icon_SettingsAbout").precomposed(pallete.blueIcon), - settingsLogout: #imageLiteral(resourceName: "Icon_SettingsLogout").precomposed(pallete.redUI), - fastSettingsLock: #imageLiteral(resourceName: "Icon_FastSettingsLock").precomposed(pallete.blueIcon), - fastSettingsDark: #imageLiteral(resourceName: "Icon_FastSettingsDark").precomposed(pallete.blueIcon), - fastSettingsSunny: #imageLiteral(resourceName: "Icon_FastSettingsSunny").precomposed(pallete.blueIcon), - fastSettingsMute: #imageLiteral(resourceName: "Icon_ChatActionMute").precomposed(pallete.blueIcon), - fastSettingsUnmute: #imageLiteral(resourceName: "Icon_ChatActionUnmute").precomposed(pallete.blueIcon), - chatRecordVideo: #imageLiteral(resourceName: "Icon_RecordVideoMessage").precomposed(pallete.grayIcon), - inputChannelMute: #imageLiteral(resourceName: "Icon_InputChannelMute").precomposed(pallete.grayIcon), - inputChannelUnmute: #imageLiteral(resourceName: "Icon_InputChannelUnmute").precomposed(pallete.grayIcon), - changePhoneNumberIntro: #imageLiteral(resourceName: "Icon_ChangeNumberIntro").precomposed()) -} - - -private func generateTheme(pallete: ColorPallete, dark: Bool, fontSize: CGFloat) -> TelegramPresentationTheme { - - let chatList = TelegramChatListTheme(selectedBackgroundColor: pallete.blueSelect, - singleLayoutSelectedBackgroundColor: pallete.grayBackground, - activeDraggingBackgroundColor: pallete.border, - pinnedBackgroundColor: pallete.background, - contextMenuBackgroundColor: pallete.background, - textColor: pallete.text, - grayTextColor: pallete.grayText, - secretChatTextColor: pallete.blueUI, - peerTextColor: pallete.text, - activityColor: pallete.blueUI, - activitySelectedColor: .white, - activityContextMenuColor: pallete.blueUI, - activityPinnedColor: pallete.blueUI, - badgeTextColor: pallete.background, - badgeBackgroundColor: pallete.badge, - badgeSelectedTextColor: pallete.blueSelect, - badgeSelectedBackgroundColor: .white, +} +func generateTheme(palette: ColorPalette, cloudTheme: TelegramTheme?, bubbled: Bool, fontSize: CGFloat, wallpaper: ThemeWallpaper) -> TelegramPresentationTheme { + + let chatList = TelegramChatListTheme(selectedBackgroundColor: palette.accentSelect, + singleLayoutSelectedBackgroundColor: palette.grayBackground, + activeDraggingBackgroundColor: palette.border, + pinnedBackgroundColor: palette.background, + contextMenuBackgroundColor: palette.background, + textColor: palette.text, + grayTextColor: palette.grayText, + secretChatTextColor: palette.accent, + peerTextColor: palette.text, + activityColor: palette.accent, + activitySelectedColor: palette.underSelectedColor, + activityContextMenuColor: palette.accent, + activityPinnedColor: palette.accent, + badgeTextColor: palette.background, + badgeBackgroundColor: palette.badge, + badgeSelectedTextColor: palette.accentSelect, + badgeSelectedBackgroundColor: palette.underSelectedColor, badgeMutedTextColor: .white, - badgeMutedBackgroundColor: pallete.badgeMuted) + badgeMutedBackgroundColor: palette.badgeMuted) - let tabBar = TelegramTabBarTheme(color: pallete.grayIcon, selectedColor: pallete.blueIcon, badgeTextColor: .white, badgeColor: pallete.redUI) - return TelegramPresentationTheme(colors: pallete, search: SearchTheme(pallete.grayBackground, #imageLiteral(resourceName: "Icon_SearchField").precomposed(pallete.grayIcon), #imageLiteral(resourceName: "Icon_SearchClear").precomposed(pallete.grayIcon), tr(.searchFieldSearch), pallete.text, pallete.grayText), chatList: chatList, tabBar: tabBar, icons: generateIcons(from: pallete), dark: dark, fontSize: fontSize) + let tabBar = TelegramTabBarTheme(color: palette.grayIcon, selectedColor: palette.accentIcon, badgeTextColor: .white, badgeColor: palette.redUI) + return TelegramPresentationTheme(colors: palette, cloudTheme: cloudTheme, search: SearchTheme(palette.grayBackground, #imageLiteral(resourceName: "Icon_SearchField").precomposed(palette.grayIcon), #imageLiteral(resourceName: "Icon_SearchClear").precomposed(palette.grayIcon), { L10n.searchFieldSearch }, palette.text, palette.grayText), chatList: chatList, tabBar: tabBar, icons: generateIcons(from: palette, bubbled: bubbled), bubbled: bubbled, fontSize: fontSize, wallpaper: wallpaper) } -func updateTheme(with settings: ThemePalleteSettings, for window: Window? = nil, animated: Bool = false) { - telegramUpdateTheme(generateTheme(pallete: settings.dark ? darkPallete : whitePallete, dark: settings.dark, fontSize: settings.fontSize), window: window, animated: animated) +func updateTheme(with settings: ThemePaletteSettings, for window: Window? = nil, animated: Bool = false) { + let palette: ColorPalette + switch settings.palette.name { + case whitePalette.name: + if settings.palette.accent == whitePalette.accent { + palette = whitePalette + } else { + palette = settings.palette + } + case darkPalette.name: + if settings.palette.accent == darkPalette.accent { + palette = darkPalette + } else { + palette = settings.palette + } + case dayClassicPalette.name: + if settings.palette.accent == dayClassicPalette.accent { + palette = dayClassicPalette + } else { + palette = settings.palette + } + case nightAccentPalette.name: + if settings.palette.accent == nightAccentPalette.accent { + palette = nightAccentPalette + } else { + palette = settings.palette + } + case systemPalette.name: + palette = systemPalette + default: + palette = settings.palette + } + telegramUpdateTheme(generateTheme(palette: palette, cloudTheme: settings.cloudTheme, bubbled: settings.bubbled, fontSize: settings.fontSize, wallpaper: settings.wallpaper), window: window, animated: animated) } private let appearanceDisposable = MetaDisposable() @@ -779,46 +2627,39 @@ private func telegramUpdateTheme(_ theme: TelegramPresentationTheme, window: Win if let window = window { if animated, let contentView = window.contentView { - - var indexes:[Int] = [] - for i in 0 ..< contentView.subviews.count { - if contentView.subviews[i] is ImageView { - indexes.insert(i, at: 0) - } - } - - for index in indexes { - contentView.subviews[index].removeFromSuperview() - } - + let image = window.windowImageShot() let imageView = ImageView() imageView.image = image - imageView.frame = contentView.superview!.bounds + imageView.frame = window.bounds contentView.addSubview(imageView) - appearanceDisposable.set((Signal.single(Void()) |> delay(0.3, queue: Queue.mainQueue())).start(completed: { [weak imageView] in - if let strongImageView = imageView { - strongImageView.change(opacity: 0, animated: true, removeOnCompletion: false, duration: 0.2, completion: { [weak strongImageView] completed in - strongImageView?.removeFromSuperview() + let signal = Signal.single(Void()) |> delay(0.25, queue: Queue.mainQueue()) |> afterDisposed { [weak imageView] in + if let imageView = imageView { + imageView.change(opacity: 0, animated: true, removeOnCompletion: false, duration: 0.2, completion: { [weak imageView] completed in + imageView?.removeFromSuperview() }) } + } - })) + appearanceDisposable.set(signal.start()) } window.contentView?.background = theme.colors.background window.contentView?.subviews.first?.background = theme.colors.background window.appearance = theme.appearance + +// NSAppearance.current = theme.appearance + // window.titl window.backgroundColor = theme.colors.grayBackground - window.titlebarAppearsTransparent = theme.dark + window.titlebarAppearsTransparent = true//theme.dark + } _themeSignal.set(theme) } func setDefaultTheme(for window: Window? = nil) { - telegramUpdateTheme(generateTheme(pallete: whitePallete, dark: false, fontSize: 13.0), window: window, animated: false) + telegramUpdateTheme(generateTheme(palette: dayClassicPalette, cloudTheme: nil, bubbled: false, fontSize: 13.0, wallpaper: ThemeWallpaper()), window: window, animated: false) } - diff --git a/Telegram-Mac/AppearanceThumbs.swift b/Telegram-Mac/AppearanceThumbs.swift new file mode 100644 index 0000000000..3ed1d6a9aa --- /dev/null +++ b/Telegram-Mac/AppearanceThumbs.swift @@ -0,0 +1,702 @@ +// +// AppearanceThumbs.swift +// Telegram +// +// Created by Mikhail Filimonov on 14/09/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import TGUIKit +import SwiftSignalKit +import Postbox + +func drawBg(_ backgroundMode: TableBackgroundMode, palette: ColorPalette, bubbled: Bool, rect: NSRect, in ctx: CGContext) { + switch backgroundMode { + case let .background(image, intensity, colors, rotation): + let imageSize = image.size.aspectFilled(rect.size) + ctx.saveGState() +// ctx.translateBy(x: 1, y: -1) + + if let colors = colors, !colors.isEmpty { + + if palette.isDark { + ctx.setBlendMode(.normal) + ctx.clear(rect.focus(imageSize)) + ctx.setFillColor(NSColor.black.cgColor) + ctx.fill(rect.focus(imageSize)) + ctx.clip(to: rect.focus(imageSize), mask: image._cgImage!) + } + + if colors.count > 2 { + let preview = AnimatedGradientBackgroundView.generatePreview(size: NSMakeSize(200, 100).fitted(NSMakeSize(32, 32)), colors: colors) + + ctx.saveGState() + ctx.translateBy(x: rect.width / 2.0, y: rect.height / 2.0) + ctx.scaleBy(x: 1, y: -1.0) + ctx.translateBy(x: -rect.width / 2.0, y: -rect.height / 2.0) + + ctx.draw(preview, in: rect.focus(NSMakeSize(500, 500))) + ctx.restoreGState() + + } else if colors.count > 1 { + let gradientColors = colors.map { $0.cgColor } as CFArray + let delta: CGFloat = 1.0 / (CGFloat(colors.count) - 1.0) + var locations: [CGFloat] = [] + for i in 0 ..< colors.count { + locations.append(delta * CGFloat(i)) + } + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + ctx.saveGState() + ctx.translateBy(x: rect.width / 2.0, y: rect.height / 2.0) + ctx.rotate(by: CGFloat(rotation ?? 0) * CGFloat.pi / -180.0) + ctx.translateBy(x: -rect.width / 2.0, y: -rect.height / 2.0) + ctx.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: rect.height), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]) + ctx.restoreGState() + } else if let color = colors.first { + ctx.setFillColor(color.cgColor) + ctx.fill(rect) + } + } + + if let colors = colors, !colors.isEmpty { + if let image = image._cgImage, !palette.isDark { + ctx.setBlendMode(.softLight) + ctx.setAlpha(CGFloat(abs(intensity ?? 50)) / 100.0) + ctx.draw(image, in: rect.focus(imageSize)) + } + } + ctx.restoreGState() + case let .color(color): + ctx.setFillColor(color.cgColor) + ctx.fill(rect) + case let .gradient(colors, rotation): + if bubbled { + if colors.count > 2 { + let preview = AnimatedGradientBackgroundView.generatePreview(size: rect.size.fitted(NSMakeSize(32, 32)), colors: colors) + ctx.saveGState() + ctx.translateBy(x: rect.width / 2.0, y: rect.height / 2.0) + ctx.scaleBy(x: 1, y: -1.0) + ctx.translateBy(x: -rect.width / 2.0, y: -rect.height / 2.0) + + ctx.draw(preview, in: rect.size.bounds) + ctx.restoreGState() + } else { + let colors = colors.reversed() + let gradientColors = colors.map { $0.cgColor } as CFArray + let delta: CGFloat = 1.0 / (CGFloat(colors.count) - 1.0) + var locations: [CGFloat] = [] + for i in 0 ..< colors.count { + locations.append(delta * CGFloat(i)) + } + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + ctx.saveGState() + ctx.translateBy(x: rect.width / 2.0, y: rect.height / 2.0) + ctx.rotate(by: CGFloat(rotation ?? 0) * CGFloat.pi / -180.0) + ctx.translateBy(x: -rect.width / 2.0, y: -rect.height / 2.0) + ctx.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: rect.height), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]) + ctx.restoreGState() + } + } + default: + break + } +} + +private func cloudThemeData(context: AccountContext, theme: TelegramTheme, file: TelegramMediaFile) -> Signal<(ColorPalette, Wallpaper, TelegramWallpaper?), NoError> { + return Signal { subscriber in + + let fetchDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: MediaResourceReference.theme(theme: ThemeReference.slug(theme.slug), resource: file.resource)).start() + let wallpaperDisposable = DisposableSet() + + let resourceData = context.account.postbox.mediaBox.resourceData(file.resource) |> filter { $0.complete } |> take(1) + + let dataDisposable = resourceData.start(next: { data in + if let palette = importPalette(data.path) { + var wallpaper: Signal = .single(nil) + var newSettings: WallpaperSettings = WallpaperSettings() + switch palette.wallpaper { + case .none: + wallpaper = .single(nil) + case .builtin: + wallpaper = .single(.builtin(newSettings)) + case let .color(color): + wallpaper = .single(.color(color.argb)) + case let .url(string): + let link = inApp(for: string as NSString, context: context) + switch link { + case let .wallpaper(values): + switch values.preview { + case let .slug(slug, settings): + wallpaper = getWallpaper(network: context.account.network, slug: slug) |> map(Optional.init) + newSettings = settings + default: + break + } + default: + break + } + } + + wallpaperDisposable.add(wallpaper.start(next: { cloud in + if let cloud = cloud { + let wp = Wallpaper(cloud).withUpdatedSettings(newSettings) + wallpaperDisposable.add(moveWallpaperToCache(postbox: context.account.postbox, wallpaper: wp).start(next: { wallpaper in + subscriber.putNext((palette, wallpaper, cloud)) + subscriber.putCompletion() + })) + } else { + subscriber.putNext((palette, .none, nil)) + subscriber.putCompletion() + } + }, error: { _ in + subscriber.putCompletion() + })) + } + + }) + + return ActionDisposable { + fetchDisposable.dispose() + dataDisposable.dispose() + wallpaperDisposable.dispose() + } + } +} + +private func localThemeData(context: AccountContext, theme: TelegramTheme, palette: ColorPalette) -> Signal<(ColorPalette, Wallpaper, TelegramWallpaper?), NoError> { + return Signal { subscriber in + + var wallpaper: Signal = .single(nil) + var newSettings = WallpaperSettings() + if let wp = theme.settings?.wallpaper { + wallpaper = .single(wp) + } else { + switch palette.wallpaper { + case .none: + wallpaper = .single(nil) + case .builtin: + wallpaper = .single(.builtin(newSettings)) + case let .color(color): + wallpaper = .single(.color(color.argb)) + case let .url(string): + let link = inApp(for: string as NSString, context: context) + switch link { + case let .wallpaper(values): + switch values.preview { + case let .slug(slug, settings): + wallpaper = getWallpaper(network: context.account.network, slug: slug) |> map(Optional.init) + newSettings = settings + default: + break + } + default: + break + } + } + } + + let wallpaperDisposable = DisposableSet() + + wallpaperDisposable.add(wallpaper.start(next: { cloud in + if let cloud = cloud { + let wp = Wallpaper(cloud).withUpdatedSettings(newSettings) + wallpaperDisposable.add(moveWallpaperToCache(postbox: context.account.postbox, wallpaper: wp).start(next: { wallpaper in + subscriber.putNext((palette, wallpaper, cloud)) + subscriber.putCompletion() + })) + } else { + subscriber.putNext((palette, .none, nil)) + subscriber.putCompletion() + } + }, error: { _ in + subscriber.putCompletion() + })) + + return ActionDisposable { + wallpaperDisposable.dispose() + } + } +} + + +private func cloudThemeCrossplatformData(context: AccountContext, settings: TelegramThemeSettings) -> Signal<(ColorPalette, Wallpaper, TelegramWallpaper?), NoError> { + + let palette = settings.palette + let wallpaper: Wallpaper = settings.wallpaper?.uiWallpaper ?? .none + let cloud = settings.wallpaper + return moveWallpaperToCache(postbox: context.account.postbox, wallpaper: wallpaper) |> map { wallpaper in + return (palette, wallpaper, cloud) + } +} + + +private func generateThumb(palette: ColorPalette, bubbled: Bool, wallpaper: Wallpaper) -> Signal { + return Signal { subscriber in + let image = generateImage(NSMakeSize(80, 55), rotatedContext: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.round(size, 10) + + + let backgroundMode: TableBackgroundMode + if bubbled { + switch wallpaper { + case .builtin: + backgroundMode = TelegramPresentationTheme.defaultBackground + case let.color(color): + backgroundMode = .color(color: NSColor(argb: color).withAlphaComponent(1.0)) + case let .gradient(_, colors, rotation): + backgroundMode = .gradient(colors: colors.map { NSColor(argb: $0).withAlphaComponent(1.0) }, rotation: rotation) + case let .image(representation, settings): + if let resource = largestImageRepresentation(representation)?.resource, let image = NSImage(contentsOf: URL(fileURLWithPath: wallpaperPath(resource, settings: settings))) { + backgroundMode = .background(image: image, intensity: settings.intensity, colors: settings.colors.map { NSColor(argb: $0) }, rotation: settings.rotation) + } else { + backgroundMode = TelegramPresentationTheme.defaultBackground + } + + case let .file(_, file, settings, isPattern): + if let image = NSImage(contentsOf: URL(fileURLWithPath: wallpaperPath(file.resource, settings: settings))) { + backgroundMode = .background(image: image, intensity: settings.intensity, colors: settings.colors.map { NSColor(argb: $0) }, rotation: settings.rotation) + } else { + backgroundMode = TelegramPresentationTheme.defaultBackground + } + case .none: + backgroundMode = .color(color: palette.chatBackground) + case let .custom(representation, blurred): + if let image = NSImage(contentsOf: URL(fileURLWithPath: wallpaperPath(representation.resource, settings: WallpaperSettings(blur: blurred)))) { + backgroundMode = .background(image: image, intensity: nil, colors: nil, rotation: nil) + } else { + backgroundMode = TelegramPresentationTheme.defaultBackground + } + } + } else { + backgroundMode = .color(color: palette.chatBackground) + } + + func applyBubbles() { + let bubbleImage = NSImage(named: "Icon_ThemeBubble") + if let incoming = bubbleImage?.precomposed(palette.bubbleBackground_incoming, flipVertical: true) { + ctx.draw(incoming, in: NSMakeRect(7, 9, 48, 16)) + } + if let outgoing = bubbleImage?.precomposed(palette.bubbleBackground_outgoing, flipVertical: true, flipHorizontal: true) { + ctx.draw(outgoing, in: NSMakeRect(size.width - 57, size.height - 24, 48, 16)) + } + } + + func applyPlain() { + ctx.setFillColor(palette.accent.cgColor) + ctx.fillEllipse(in: NSMakeRect(10, 7, 17, 17)) + + if true { + let name1 = generateImage(NSMakeSize(20, 4), rotatedContext: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.round(size, 2) + ctx.setFillColor(palette.accent.cgColor) + ctx.fill(rect) + })! + ctx.draw(name1, in: NSMakeRect(10 + 17 + 3, 7 + 2, name1.backingSize.width, name1.backingSize.height)) + + let text1 = generateImage(NSMakeSize(40, 4), rotatedContext: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.round(size, 2) + ctx.setFillColor(palette.grayText.withAlphaComponent(0.5).cgColor) + ctx.fill(rect) + })! + ctx.draw(text1, in: NSMakeRect(10 + 17 + 3, 7 + 2 + 4 + 4, text1.backingSize.width, text1.backingSize.height)) + } + + if true { + ctx.setFillColor(palette.accent.cgColor) + ctx.fillEllipse(in: NSMakeRect(10, 7 + 17 + 7, 17, 17)) + + let name1 = generateImage(NSMakeSize(20, 4), rotatedContext: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.round(size, 2) + ctx.setFillColor(palette.accent.cgColor) + ctx.fill(rect) + })! + ctx.draw(name1, in: NSMakeRect(10 + 17 + 3, 7 + 17 + 7 + 2, name1.backingSize.width, name1.backingSize.height)) + + let text1 = generateImage(NSMakeSize(40, 4), rotatedContext: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.round(size, 2) + ctx.setFillColor(palette.grayText.withAlphaComponent(0.5).cgColor) + ctx.fill(rect) + })! + ctx.draw(text1, in: NSMakeRect(10 + 17 + 3, 7 + 17 + 7 + 2 + 4 + 4, text1.backingSize.width, text1.backingSize.height)) + } + + + } + drawBg(backgroundMode, palette: palette, bubbled: bubbled, rect: rect, in: ctx) + if bubbled { + applyBubbles() + } else { + applyPlain() + } + + })! + + subscriber.putNext(image) + subscriber.putCompletion() + + return EmptyDisposable + } |> runOn(Queue.concurrentDefaultQueue()) +} + + +private func generateWidgetThumb(palette: ColorPalette, bubbled: Bool, wallpaper: Wallpaper) -> Signal { + return Signal { subscriber in + let image = generateImage(NSMakeSize(132, 86), rotatedContext: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.round(size, 17) + + + let backgroundMode: TableBackgroundMode + if bubbled { + switch wallpaper { + case .builtin: + backgroundMode = TelegramPresentationTheme.defaultBackground + case let.color(color): + backgroundMode = .color(color: NSColor(argb: color).withAlphaComponent(1.0)) + case let .gradient(_, colors, rotation): + backgroundMode = .gradient(colors: colors.map { NSColor(argb: $0).withAlphaComponent(1.0) }, rotation: rotation) + case let .image(representation, settings): + if let resource = largestImageRepresentation(representation)?.resource, let image = NSImage(contentsOf: URL(fileURLWithPath: wallpaperPath(resource, settings: settings))) { + backgroundMode = .background(image: image, intensity: settings.intensity, colors: settings.colors.map { NSColor(argb: $0) }, rotation: settings.rotation) + } else { + backgroundMode = TelegramPresentationTheme.defaultBackground + } + + case let .file(_, file, settings, isPattern): + if let image = NSImage(contentsOf: URL(fileURLWithPath: wallpaperPath(file.resource, settings: settings))) { + backgroundMode = .background(image: image, intensity: settings.intensity, colors: settings.colors.map { NSColor(argb: $0) }, rotation: settings.rotation) + } else { + backgroundMode = TelegramPresentationTheme.defaultBackground + } + case .none: + backgroundMode = .color(color: palette.chatBackground) + case let .custom(representation, blurred): + if let image = NSImage(contentsOf: URL(fileURLWithPath: wallpaperPath(representation.resource, settings: WallpaperSettings(blur: blurred)))) { + backgroundMode = .background(image: image, intensity: nil, colors: nil, rotation: nil) + } else { + backgroundMode = TelegramPresentationTheme.defaultBackground + } + } + } else { + backgroundMode = .color(color: palette.chatBackground) + } + + func applyBubbles() { + + ctx.draw(NSImage(named: "Icon_ThemePreview_Dino")!.precomposed(flipVertical: true), in: NSMakeRect(10, 24, 24, 24)) + ctx.draw(NSImage(named: "Icon_ThemePreview_Duck")!.precomposed(flipVertical: true), in: NSMakeRect(10, 54, 24, 24)) + + let bubble1 = generateImage(NSMakeSize(80, 38), rotatedContext: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.round(size, 12) + ctx.setFillColor(palette.bubbleBackground_incoming.cgColor) + ctx.fill(rect) + })! + + ctx.draw(bubble1, in: NSMakeRect(40, 10, 80, 38)) + + let bubble1Text1 = generateImage(NSMakeSize(62, 6), rotatedContext: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.round(size, size.height / 2) + ctx.setFillColor(palette.grayText.withAlphaComponent(0.5).cgColor) + ctx.fill(rect) + })! + ctx.draw(bubble1Text1, in: NSMakeRect(49, 19, 62, 6)) + + let bubble1Text2 = generateImage(NSMakeSize(38, 6), rotatedContext: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.round(size, size.height / 2) + ctx.setFillColor(palette.grayText.withAlphaComponent(0.5).cgColor) + ctx.fill(rect) + })! + ctx.draw(bubble1Text2, in: NSMakeRect(49, 33, 38, 6)) + + + let bubble2 = generateImage(NSMakeSize(54, 24), rotatedContext: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.round(size, 12) + ctx.setFillColor(palette.bubbleBackground_incoming.cgColor) + ctx.fill(rect) + })! + + ctx.draw(bubble2, in: NSMakeRect(40, 54, 54, 24)) + + let bubble2Text1 = generateImage(NSMakeSize(36, 6), rotatedContext: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.round(size, size.height / 2) + ctx.setFillColor(palette.grayText.withAlphaComponent(0.5).cgColor) + ctx.fill(rect) + })! + ctx.draw(bubble2Text1, in: NSMakeRect(49, 63, 36, 6)) + } + + func applyPlain() { + ctx.draw(NSImage(named: "Icon_ThemePreview_Dino")!.precomposed(flipVertical: true), in: NSMakeRect(10, 10, 24, 24)) + if true { + let name1 = generateImage(NSMakeSize(32, 6), rotatedContext: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.round(size, size.height / 2) + ctx.setFillColor(palette.peerAvatarGreenTop.cgColor) + ctx.fill(rect) + })! + ctx.draw(name1, in: NSMakeRect(42, 13, name1.backingSize.width, name1.backingSize.height)) + + let text1 = generateImage(NSMakeSize(80, 6), rotatedContext: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.round(size, size.height / 2) + ctx.setFillColor(palette.grayText.withAlphaComponent(0.5).cgColor) + ctx.fill(rect) + })! + ctx.draw(text1, in: NSMakeRect(42, 25, text1.backingSize.width, text1.backingSize.height)) + + let text2 = generateImage(NSMakeSize(48, 6), rotatedContext: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.round(size, size.height / 2) + ctx.setFillColor(palette.grayText.withAlphaComponent(0.5).cgColor) + ctx.fill(rect) + })! + ctx.draw(text2, in: NSMakeRect(42, 37, text2.backingSize.width, text2.backingSize.height)) + + } + + if true { + ctx.draw(NSImage(named: "Icon_ThemePreview_Duck")!.precomposed(flipVertical: true), in: NSMakeRect(10, 54, 24, 24)) + + let name1 = generateImage(NSMakeSize(32, 6), rotatedContext: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.round(size, size.height / 2) + ctx.setFillColor(palette.peerAvatarVioletTop.cgColor) + ctx.fill(rect) + })! + ctx.draw(name1, in: NSMakeRect(42, 57, name1.backingSize.width, name1.backingSize.height)) + + let text1 = generateImage(NSMakeSize(80, 6), rotatedContext: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.round(size, size.height / 2) + ctx.setFillColor(palette.grayText.withAlphaComponent(0.5).cgColor) + ctx.fill(rect) + })! + ctx.draw(text1, in: NSMakeRect(42, 69, text1.backingSize.width, text1.backingSize.height)) + } + } + drawBg(backgroundMode, palette: palette, bubbled: bubbled, rect: rect, in: ctx) + if bubbled { + applyBubbles() + } else { + applyPlain() + } + })! + + subscriber.putNext(image) + subscriber.putCompletion() + + return EmptyDisposable + } |> runOn(Queue.concurrentDefaultQueue()) +} + +func generateChatThemeThumb(palette: ColorPalette, bubbled: Bool, backgroundMode: TableBackgroundMode) -> Signal { + return Signal { subscriber in + let image = generateImage(NSMakeSize(70, 80), rotatedContext: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.round(size, 10) + + func applyBubbles() { + let bubbleImage = NSImage(named: "Icon_ThemeBubble") + if let incoming = bubbleImage?.precomposed(palette.bubbleBackground_incoming, flipVertical: true) { + ctx.draw(incoming, in: NSMakeRect(7, 9, 38, 16)) + } + if let outgoing = bubbleImage?.precomposed(palette.bubbleBackground_outgoing, flipVertical: true, flipHorizontal: true) { + ctx.draw(outgoing, in: NSMakeRect(size.width - 47, 9 + 16 + 5, 38, 16)) + } + } + + func applyPlain() { + ctx.setFillColor(palette.accent.cgColor) + ctx.fillEllipse(in: NSMakeRect(10, 7, 17, 17)) + + if true { + let name1 = generateImage(NSMakeSize(20, 4), rotatedContext: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.round(size, 2) + ctx.setFillColor(palette.accent.cgColor) + ctx.fill(rect) + })! + ctx.draw(name1, in: NSMakeRect(10 + 17 + 3, 7 + 2, name1.backingSize.width, name1.backingSize.height)) + + let text1 = generateImage(NSMakeSize(40, 4), rotatedContext: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.round(size, 2) + ctx.setFillColor(palette.grayText.withAlphaComponent(0.5).cgColor) + ctx.fill(rect) + })! + ctx.draw(text1, in: NSMakeRect(10 + 17 + 3, 7 + 2 + 4 + 4, text1.backingSize.width, text1.backingSize.height)) + } + + if true { + ctx.setFillColor(palette.accent.cgColor) + ctx.fillEllipse(in: NSMakeRect(10, 7 + 17 + 7, 17, 17)) + + let name1 = generateImage(NSMakeSize(20, 4), rotatedContext: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.round(size, 2) + ctx.setFillColor(palette.accent.cgColor) + ctx.fill(rect) + })! + ctx.draw(name1, in: NSMakeRect(10 + 17 + 3, 7 + 17 + 7 + 2, name1.backingSize.width, name1.backingSize.height)) + + let text1 = generateImage(NSMakeSize(40, 4), rotatedContext: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + ctx.round(size, 2) + ctx.setFillColor(palette.grayText.withAlphaComponent(0.5).cgColor) + ctx.fill(rect) + })! + ctx.draw(text1, in: NSMakeRect(10 + 17 + 3, 7 + 17 + 7 + 2 + 4 + 4, text1.backingSize.width, text1.backingSize.height)) + } + + + } + drawBg(backgroundMode, palette: palette, bubbled: bubbled, rect: rect, in: ctx) + if bubbled { + applyBubbles() + } else { + applyPlain() + } + + })! + + subscriber.putNext(image) + subscriber.putCompletion() + + return EmptyDisposable + } |> runOn(Queue.concurrentDefaultQueue()) +} + + + + +func themeAppearanceThumbAndData(context: AccountContext, bubbled: Bool, source: ThemeSource, thumbSource: AppearanceThumbSource = .general) -> Signal<(TransformImageResult, InstallThemeSource), NoError> { + + var thumbGenerator = generateThumb + switch thumbSource { + case .widget: + thumbGenerator = generateWidgetThumb + case .general: + thumbGenerator = generateThumb + } + + switch source { + case let .cloud(cloud): + if let file = cloud.file { + return cloudThemeData(context: context, theme: cloud, file: file) |> mapToSignal { data in + return thumbGenerator(data.0, bubbled, data.1) |> map { image in + return (TransformImageResult(image, true), .cloud(cloud, InstallCloudThemeCachedData(palette: data.0, wallpaper: data.1, cloudWallpaper: data.2))) + } + } + } else if let palette = cloud.settings?.palette { + + let settings = themeSettingsView(accountManager: context.sharedContext.accountManager) |> take(1) + + return settings |> map { settings -> (Wallpaper, ColorPalette) in + let settings = settings + .withUpdatedPalette(palette) + .withUpdatedCloudTheme(cloud) + .installDefaultAccent() + .installDefaultWallpaper() + return (settings.wallpaper.wallpaper, settings.palette) + } |> mapToSignal { wallpaper, palette in + return thumbGenerator(palette, bubbled, wallpaper) |> map { image in + return (TransformImageResult(image, true), .cloud(cloud, InstallCloudThemeCachedData(palette: palette, wallpaper: wallpaper, cloudWallpaper: cloud.settings?.wallpaper))) + } + } + } else { + return .single((TransformImageResult(theme.icons.appearanceAddPlatformTheme, true), .cloud(cloud, nil))) + } + case let .local(palette, cloud): + let settings = themeSettingsView(accountManager: context.sharedContext.accountManager) |> take(1) + + return settings |> map { settings -> (Wallpaper, ColorPalette) in + let settings = settings + .withUpdatedPalette(palette) + .withUpdatedCloudTheme(cloud) + .installDefaultAccent() + .installDefaultWallpaper() + return (settings.wallpaper.wallpaper, settings.palette) + } |> mapToSignal { wallpaper, palette in + if let cloud = cloud { + return thumbGenerator(palette, bubbled, wallpaper) |> map { image in + return (TransformImageResult(image, true), .cloud(cloud, InstallCloudThemeCachedData(palette: palette, wallpaper: wallpaper, cloudWallpaper: cloud.settings?.wallpaper))) + } + } else { + return thumbGenerator(palette, bubbled, wallpaper) |> map { image in + return (TransformImageResult(image, true), .local(palette)) + } + } + } + } +} + + + + + +func themeInstallSource(context: AccountContext, source: ThemeSource) -> Signal { + + switch source { + case let .cloud(cloud): + if let file = cloud.file { + return cloudThemeData(context: context, theme: cloud, file: file) |> map { data in + return .cloud(cloud, InstallCloudThemeCachedData(palette: data.0, wallpaper: data.1, cloudWallpaper: data.2)) + } + } else { + return .single(.cloud(cloud, nil)) + } + case let .local(palette, cloud): + let settings = themeSettingsView(accountManager: context.sharedContext.accountManager) |> take(1) + + return settings |> map { settings -> (Wallpaper, ColorPalette) in + let settings = settings + .withUpdatedPalette(palette) + .withUpdatedCloudTheme(cloud) + .installDefaultAccent() + .installDefaultWallpaper() + return (settings.wallpaper.wallpaper, settings.palette) + } |> map { wallpaper, palette in + if let cloud = cloud { + return .cloud(cloud, InstallCloudThemeCachedData(palette: palette, wallpaper: wallpaper, cloudWallpaper: cloud.settings?.wallpaper)) + } else { + return .local(palette) + } + } + } +} diff --git a/Telegram-Mac/AppearanceViewController.swift b/Telegram-Mac/AppearanceViewController.swift deleted file mode 100644 index 22c8784886..0000000000 --- a/Telegram-Mac/AppearanceViewController.swift +++ /dev/null @@ -1,172 +0,0 @@ -// -// AppearanceViewController.swift -// Telegram -// -// Created by keepcoder on 07/07/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - -import Cocoa -import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac - -private final class AppearanceViewArguments { - let account:Account - let toggleDarkMode:(Bool)->Void - let toggleFontSize:(Int32)->Void - init(account:Account, toggleDarkMode: @escaping(Bool)->Void, toggleFontSize: @escaping(Int32)->Void) { - self.account = account - self.toggleDarkMode = toggleDarkMode - self.toggleFontSize = toggleFontSize - } -} - -private enum AppearanceViewEntry : TableItemListNodeEntry { - case darkMode(Int32, Bool) - case section(Int32) - case font(Int32, Int32) - case description(Int32, Int32, String) - - var stableId: Int32 { - switch self { - case .darkMode: - return 0 - case .section(let section): - return section + 1000 - case .font: - return 1 - case let .description(section, index, _): - return (section * 1000) + (index + 1) * 1000 - } - } - - var index:Int32 { - switch self { - case .darkMode(let section, _): - return (section * 1000) + 0 - case .section(let section): - return (section + 1) * 1000 - section - case .font(let section, _): - return (section * 1000) + 1 - case let .description(section, index, _): - return (section * 1000) + index + 2 - } - } - - func item(_ arguments: AppearanceViewArguments, initialSize: NSSize) -> TableRowItem { - switch self { - case .darkMode(_, let enabled): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.generalSettingsDarkMode), type: .switchable(stateback: { () -> Bool in - return enabled - }), action: { - arguments.toggleDarkMode(!enabled) - }) - case .description(_, _, let text): - return GeneralTextRowItem(initialSize, stableId: stableId, text: text) - case .section: - return GeneralRowItem(initialSize, height: 20, stableId: stableId) - case .font(_, let size): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.generalSettingsLargeFonts), type: .switchable(stateback: { () -> Bool in - return size == 15 - }), action: { - arguments.toggleFontSize(size == 13 ? 15 : 13) - }) - } - } -} -private func ==(lhs: AppearanceViewEntry, rhs: AppearanceViewEntry) -> Bool { - switch lhs { - case let .darkMode(section, enabled): - if case .darkMode(section, enabled) = rhs { - return true - } else { - return false - } - case .section(let section): - if case .section(section) = rhs { - return true - } else { - return false - } - case let .font(section, size): - if case .font(section, size) = rhs { - return true - } else { - return false - } - case let .description(section, index, description): - if case .description(section, index, description) = rhs { - return true - } else { - return false - } - } -} -private func <(lhs: AppearanceViewEntry, rhs: AppearanceViewEntry) -> Bool { - return lhs.index < rhs.index -} - -private func AppearanceViewEntries(dark:Bool, settings: BaseApplicationSettings?) -> [AppearanceViewEntry] { - var entries:[AppearanceViewEntry] = [] - - var sectionId:Int32 = 1 - var descIndex:Int32 = 1 - entries.append(.section(sectionId)) - sectionId += 1 - - entries.append(.darkMode(sectionId, dark)) - sectionId += 1 - - entries.append(.description(sectionId, descIndex, tr(.generalSettingsDarkModeDescription))) - descIndex += 1 - - entries.append(.section(sectionId)) - sectionId += 1 - - - entries.append(.font(sectionId, settings?.fontSize ?? 13)) - sectionId += 1 - - entries.append(.description(sectionId, descIndex, tr(.generalSettingsFontDescription))) - descIndex += 1 -// - return entries -} - -fileprivate func prepareTransition(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], initialSize:NSSize, arguments:AppearanceViewArguments) -> TableUpdateTransition { - - let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in - return entry.entry.item(arguments, initialSize: initialSize) - } - - return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true) -} - -class AppearanceViewController: TableViewController { - - override func viewDidLoad() { - super.viewDidLoad() - let account = self.account - let arguments = AppearanceViewArguments(account: account, toggleDarkMode: { enable in - _ = updateThemeSettings(postbox: account.postbox, pallete: enable ? darkPallete : whitePallete, dark: enable).start() - }, toggleFontSize: { size in - _ = updateBaseAppSettingsInteractively(postbox: account.postbox, { settings -> BaseApplicationSettings in - return settings.withUpdatedFontSize(size) - }).start() - }) - - let initialSize = self.atomicSize - - - let previous: Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) - genericView.merge(with: combineLatest(account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.baseAppSettings]) |> deliverOnMainQueue, appearanceSignal |> deliverOnMainQueue) |> map { pref, appearance in - let entries = AppearanceViewEntries(dark: appearance.presentation.dark, settings: pref.values[ApplicationSpecificPreferencesKeys.baseAppSettings] as? BaseApplicationSettings).map {AppearanceWrapperEntry(entry: $0, appearance: appearance)} - return prepareTransition(left: previous.swap(entries), right: entries, initialSize: initialSize.modify{$0}, arguments: arguments) - } |> deliverOnMainQueue) - readyOnce() - - } - -} diff --git a/Telegram-Mac/ApplicationContext.swift b/Telegram-Mac/ApplicationContext.swift index 7722d2bd35..ade85d95d6 100644 --- a/Telegram-Mac/ApplicationContext.swift +++ b/Telegram-Mac/ApplicationContext.swift @@ -1,307 +1,83 @@ import Foundation import TGUIKit -import SwiftSignalKitMac -import PostboxMac -import TelegramCoreMac -import MtProtoKitMac -import IOKit -func applicationContext(window: Window, shouldOnlineKeeper:Signal, accountManager: AccountManager, appGroupPath: String, testingEnvironment: Bool) -> Signal { - - return migrationData(accountManager: accountManager, appGroupPath: appGroupPath, testingEnvironment: testingEnvironment) - |> deliverOnMainQueue - |> map { migration -> Signal in - - switch migration { - case let .auth(result, ignorepasslock): - if let result = result { - switch result { - case let .unauthorized(account): - return account.postbox.preferencesView(keys: [PreferencesKeys.localizationSettings]) |> take(1) |> deliverOnMainQueue |> map { preferences in - return ApplicationContext.unauthorized(UnauthorizedApplicationContext(window: window, account: account, localization: preferences.values[PreferencesKeys.localizationSettings] as? LocalizationSettings)) - } - case let .authorized(account): - let paslock:Signal = !ignorepasslock ? account.postbox.modify { modifier -> PostboxAccessChallengeData in - return modifier.getAccessChallengeData() - } |> deliverOnMainQueue : .single(.none) - - return paslock |> mapToSignal { access -> Signal in - let promise:Promise = Promise() - let auth: Signal = combineLatest(promise.get(), account.postbox.preferencesView(keys: [PreferencesKeys.localizationSettings, ApplicationSpecificPreferencesKeys.themeSettings]) |> take(1)) |> deliverOnMainQueue |> map { _, preferences in - return .authorized(AuthorizedApplicationContext(window: window, shouldOnlineKeeper: shouldOnlineKeeper, account: account, accountManager: accountManager, localization: preferences.values[PreferencesKeys.localizationSettings] as? LocalizationSettings, themeSettings: preferences.values[ApplicationSpecificPreferencesKeys.themeSettings] as? ThemePalleteSettings)) - } - switch access { - case .none: - promise.set(.single(Void())) - return auth - default: - return account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.themeSettings, PreferencesKeys.localizationSettings]) |> take(1) |> deliverOnMainQueue |> map { value in - return ApplicationContext.postboxAccess(PasscodeAccessContext(window, promise: promise, account: account, accountManager: accountManager, localization: value.values[PreferencesKeys.localizationSettings] as? LocalizationSettings, themeSettings: value.values[ApplicationSpecificPreferencesKeys.themeSettings] as? ThemePalleteSettings)) - } |> then(auth) - } - } - case .upgrading: - return .single(nil) - } - } else { - return .single(nil) - } - case let .migrationIntro(promise, data): - return .single(.legacyIntro(LegacyIntroContext(window, promise: promise, defaultLegacyData: data))) - } - } |> switchToLatest -} +import SwiftSignalKit +import Postbox +import TelegramCore -enum MigrationData { - case migrationIntro(Promise, AuthorizationLegacyData) - case auth(AccountResult?, ignorepasslock: Bool) -} - - -func migrationData(accountManager: AccountManager, appGroupPath:String, testingEnvironment: Bool) -> Signal { - - return accountManager.modify { modifier -> Signal in - - if modifier.getCurrentId() == nil { - - let auth = legacyAuthData(passcode: emptyPasscodeData()) - let promise:Promise = Promise() - - switch auth { - case .data: - break - case .passcodeRequired: - break - case .none: - return currentAccount(networkArguments: NetworkInitializationArguments(apiId: API_ID, languagesCategory: languagesCategory), supplementary: false, manager: accountManager, appGroupPath: appGroupPath, testingEnvironment: testingEnvironment, auxiliaryMethods: telegramAccountAuxiliaryMethods) |> map { account in return .auth(account, ignorepasslock: false) } - } - - return .single(.migrationIntro(promise, auth)) |> then ( promise.get() |> take(1) |> mapToSignal { result in - return accountManager.modify { modifier -> Signal in - - switch result { - case let .data(migration): - let accountId = modifier.createRecord([]) - - let provider = ImportAccountProvider(mtProtoKeychain: { - return .single(migration.groups) - }, accountState: { - return .single(AuthorizedAccountState(masterDatacenterId: migration.masterDatacenterId, peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: migration.userId), state: nil)) - }, peers: { - return .single([]) - }) - //if !isDebug { - clearLegacyData() - // } - return accountWithId(networkArguments: NetworkInitializationArguments(apiId: API_ID, languagesCategory: languagesCategory), id: accountId, supplementary: false, appGroupPath: appGroupPath, testingEnvironment: testingEnvironment, auxiliaryMethods: telegramAccountAuxiliaryMethods, shouldKeepAutoConnection: false) |> mapToSignal { accountResult in - switch accountResult { - case .unauthorized(let left): - return importAccount(account: left, provider: provider) |> mapToSignal { - return accountManager.modify { modifier -> Void in - modifier.setCurrentId(accountId) - } |> mapToSignal { - return currentAccount(networkArguments: NetworkInitializationArguments(apiId: API_ID, languagesCategory: languagesCategory), supplementary: false, manager: accountManager, appGroupPath: appGroupPath, testingEnvironment: testingEnvironment, auxiliaryMethods: telegramAccountAuxiliaryMethods) |> map { accountResult -> Signal in - - if let accountResult = accountResult { - switch accountResult { - case .authorized(let account): - for (resource, data) in migration.resources { - account.postbox.mediaBox.storeResourceData(resource.id, data: data) - } - return account.postbox.modify { modifier -> MigrationData in - - updatePeers(modifier: modifier, peers: migration.peers, update: { (_, updated) -> Peer? in - return updated - }) - - for (peerId, state) in migration.secretState { - modifier.setPeerChatState(peerId, state: terminateLegacySecretChat(modifier: modifier, peerId: peerId, state: state)) - } - - if let passcode = migration.passcode { - modifier.setAccessChallengeData(.plaintextPassword(value: passcode, timeout: 60 * 60, attempts: nil)) - } - _ = modifier.addMessages(migration.secretMessages, location: .Random) - - for message in migration.secretMessages { - if let attribute = message.attributes.first as? AutoremoveTimeoutMessageAttribute { - switch message.id { - case let .Id(id): - let begin:Int32 = attribute.countdownBeginTime ?? Int32(Date().timeIntervalSince1970) - modifier.addTimestampBasedMessageAttribute(tag: 0, timestamp: begin + attribute.timeout, messageId: id) - default: - break - } - } - } - - return .auth(accountResult, ignorepasslock: true) - } - default: - break - } - } - - return .single(.auth(accountResult, ignorepasslock: false)) - } |> switchToLatest - } - } - default: - break - } - return currentAccount(networkArguments: NetworkInitializationArguments(apiId: 2834, languagesCategory: languagesCategory), supplementary: false, manager: accountManager, appGroupPath: appGroupPath, testingEnvironment: testingEnvironment, auxiliaryMethods: telegramAccountAuxiliaryMethods) |> map { account in return .auth(account, ignorepasslock: false) } - } - case .none: - clearLegacyData() - default: - assertionFailure() - } - - return currentAccount(networkArguments: NetworkInitializationArguments(apiId: API_ID, languagesCategory: languagesCategory), supplementary: false, manager: accountManager, appGroupPath: appGroupPath, testingEnvironment: testingEnvironment, auxiliaryMethods: telegramAccountAuxiliaryMethods) |> map { account in return .auth(account, ignorepasslock: false) } - } |> switchToLatest - - }) - - } - return currentAccount(networkArguments: NetworkInitializationArguments(apiId: API_ID, languagesCategory: languagesCategory), supplementary: false, manager: accountManager, appGroupPath: appGroupPath, testingEnvironment: testingEnvironment, auxiliaryMethods: telegramAccountAuxiliaryMethods) |> map { account in return .auth(account, ignorepasslock: false) } +import IOKit - } |> switchToLatest +private final class AuthModalController : ModalController { + override var background: NSColor { + return theme.colors.background + } + override var dynamicSize: Bool { + return true + } + override var closable: Bool { + return false + } + override func measure(size: NSSize) { + self.modal?.resize(with: NSMakeSize(size.width, size.height), animated: false) + } } -enum ApplicationContext { - case unauthorized(UnauthorizedApplicationContext) - case authorized(AuthorizedApplicationContext) - case legacyIntro(LegacyIntroContext) - case postboxAccess(PasscodeAccessContext) + + +final class UnauthorizedApplicationContext { + let account: UnauthorizedAccount + let rootController: MajorNavigationController + let window:Window + let modal: ModalController + let sharedContext: SharedAccountContext - func showRoot(for window:Window) { - if let content = window.contentView { - switch self { - case let .postboxAccess(context): - showModal(with: context.rootController, for: window) - default: - content.addSubview(rootView) - rootView.frame = content.bounds - viewDidAppear() - } - } - } + private let updatesDisposable: DisposableSet = DisposableSet() var rootView: NSView { - switch self { - case let .unauthorized(context): - return context.rootController.view - case let .authorized(context): - return context.splitView - case let .legacyIntro(context): - return context.rootController.view - case let .postboxAccess(context): - return context.rootController.view - } + return rootController.view } - func viewDidAppear() { - switch self { - case let .unauthorized(context): - context.rootController.viewDidAppear(false) - default: - break - } - } -} + init(window:Window, sharedContext: SharedAccountContext, account: UnauthorizedAccount, otherAccountPhoneNumbers: ((String, AccountRecordId, Bool)?, [(String, AccountRecordId, Bool)])) { -final class PasscodeAccessContext { - let rootController:PasscodeLockController - private let logoutDisposable = MetaDisposable() - init(_ window:Window, promise:Promise, account:Account, accountManager:AccountManager, localization: LocalizationSettings?, themeSettings: ThemePalleteSettings?) { - - dropLocalization() - if let localization = localization { - applyUILocalization(localization) - } - if let theme = themeSettings { - updateTheme(with: theme, for: window) - } else { - setDefaultTheme(for: window) - } + window.maxSize = NSMakeSize(.greatestFiniteMagnitude, .greatestFiniteMagnitude) + window.minSize = NSMakeSize(380, 500) - rootController = PasscodeLockController(account, .login, logoutImpl: { - _ = (confirmSignal(for: window, header: appName, information: tr(.accountConfirmLogoutText)) |> filter {$0} |> mapToSignal {_ in return logoutFromAccount(id: account.id, accountManager: accountManager)}).start() - }) - rootController._frameRect = NSMakeRect(0, 0, window.frame.width, window.frame.height) - window.maxSize = NSMakeSize(.greatestFiniteMagnitude, .greatestFiniteMagnitude) - window.minSize = NSMakeSize(380, 440) + updatesDisposable.add(managedAppConfigurationUpdates(accountManager: sharedContext.accountManager, network: account.network).start()) - promise.set(rootController.doneValue |> filter {$0} |> map {_ in}) - + if !window.initFromSaver { + window.setFrame(NSMakeRect(0, 0, 800, 650), display: true) + window.center() + } + + if window.frame.height < window.minSize.height { + window.setFrame(NSMakeRect(window.frame.minX, window.frame.minY, window.minSize.width, window.minSize.height), display: true) + } - } - - deinit { - logoutDisposable.dispose() - } -} - -final class LegacyIntroContext { - let rootController:LegacyIntroController - init(_ window:Window, promise:Promise, defaultLegacyData: AuthorizationLegacyData) { - rootController = LegacyIntroController(promise: promise, defaultLegacyData: defaultLegacyData) - let authSize = NSMakeSize(650, 600) - window.maxSize = authSize - window.minSize = authSize - window.setFrame(NSMakeRect(0, 0, authSize.width, authSize.height), display: true) - window.center() - rootController._frameRect = NSMakeRect(0, 0, authSize.width, authSize.height) - } -} - -final class UnauthorizedApplicationContext { - let account: UnauthorizedAccount - let localizationDisposable:MetaDisposable = MetaDisposable() - let rootController: AuthController - let window:Window - init(window:Window, account: UnauthorizedAccount, localization: LocalizationSettings?) { self.account = account self.window = window - self.rootController = AuthController(account) - let authSize = NSMakeSize(650, 600) - - setDefaultTheme(for: window) + self.sharedContext = sharedContext + self.rootController = MajorNavigationController(AuthController.self, AuthController(account, sharedContext: sharedContext, otherAccountPhoneNumbers: otherAccountPhoneNumbers), window) + rootController._frameRect = NSMakeRect(0, 0, window.frame.width, window.frame.height) + + self.modal = AuthModalController(rootController) + rootController.alwaysAnimate = true + account.shouldBeServiceTaskMaster.set(.single(.now)) - window.maxSize = authSize - window.minSize = authSize - window.setFrame(NSMakeRect(0, 0, authSize.width, authSize.height), display: true) - window.center() - window.initFromSaver = false - rootController._frameRect = NSMakeRect(0, 0, authSize.width, authSize.height) - + NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(receiveWakeNote(_:)), name: NSWorkspace.screensDidWakeNotification, object: nil) - - - - dropLocalization() - if let localization = localization { - applyUILocalization(localization) - } - - localizationDisposable.set(account.postbox.preferencesView(keys: [PreferencesKeys.localizationSettings]).start(next: { view in - if let settings = view.values[PreferencesKeys.localizationSettings] as? LocalizationSettings { - applyUILocalization(settings) - } - })) - } deinit { account.shouldBeServiceTaskMaster.set(.single(.never)) + updatesDisposable.dispose() NSWorkspace.shared.notificationCenter.removeObserver(self) } @@ -311,829 +87,861 @@ final class UnauthorizedApplicationContext { } -private struct LockNotificationsData : Equatable { - let screenLock:Bool - let passcodeLock:Bool + +enum ApplicationContextLaunchAction { + case navigate(ViewController) + case preferences +} + + +let leftSidebarWidth: CGFloat = 72 + +private final class ApplicationContainerView: View { + fileprivate let splitView: SplitView - init() { - self.screenLock = false - self.passcodeLock = false - } + fileprivate private(set) var leftSideView: NSView? - init(screenLock: Bool, passcodeLock: Bool) { - self.screenLock = screenLock - self.passcodeLock = passcodeLock + required init(frame frameRect: NSRect) { + splitView = SplitView(frame: NSMakeRect(0, 0, frameRect.width, frameRect.height)) + super.init(frame: frameRect) + addSubview(splitView) + autoresizingMask = [.width, .height] } - func withUpdatedScreenLock(_ lock: Bool) -> LockNotificationsData { - return LockNotificationsData(screenLock: lock, passcodeLock: passcodeLock) + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") } - func withUpdatedPasscodeLock(_ lock: Bool) -> LockNotificationsData { - return LockNotificationsData(screenLock: screenLock, passcodeLock: lock) + + func updateLeftSideView(_ view: NSView?, animated: Bool) { + if let view = view { + addSubview(view) + } else { + self.leftSideView?.removeFromSuperview() + } + + self.leftSideView = view + needsLayout = true } - static func ==(lhs:LockNotificationsData, rhs: LockNotificationsData) -> Bool { - return lhs.screenLock == rhs.screenLock && lhs.passcodeLock == rhs.screenLock + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + splitView.backgroundColor = theme.colors.background } - var isLocked: Bool { - return screenLock || passcodeLock + override func layout() { + super.layout() + + if let leftSideView = leftSideView { + leftSideView.frame = NSMakeRect(0, 0, leftSidebarWidth, frame.height) + splitView.frame = NSMakeRect(leftSideView.frame.maxX, 0, frame.width - leftSideView.frame.maxX, frame.height) + } else { + splitView.frame = bounds + } + } } -final class AuthorizedApplicationContext: NSObject, SplitViewDelegate, NSUserNotificationCenterDelegate { - - private let nofityDisposable:MetaDisposable = MetaDisposable() +final class AuthorizedApplicationContext: NSObject, SplitViewDelegate { - private var mediaKeyTap:SPMediaKeyTap? + + var rootView: View { + return view + } - let applicationContext: TelegramApplicationContext - let account: Account - let accountManager: AccountManager - let window:Window - let splitView:SplitView - let leftController:MainViewController - let rightController:MajorNavigationController + let context: AccountContext + private let window:Window + private let view:ApplicationContainerView + private let leftController:MainViewController + private let rightController:MajorNavigationController private let emptyController:EmptyChatViewController + private var entertainment: EntertainmentViewController? + + private var leftSidebarController: LeftSidebarController? + private let loggedOutDisposable = MetaDisposable() - private let passlockDisposable = MetaDisposable() - private let logoutDisposable = MetaDisposable() private let ringingStatesDisposable = MetaDisposable() - private let lockedScreenPromise:Promise = Promise(LockNotificationsData()) - private var _lockedValue:LockNotificationsData = LockNotificationsData() - private var resignTimestamp:Int32? = nil - private let _passlock = Promise() private let settingsDisposable = MetaDisposable() - private let localizationDisposable = MetaDisposable() private let suggestedLocalizationDisposable = MetaDisposable() - private let appearanceDisposable = MetaDisposable() - private func updateLocked(_ f:(LockNotificationsData) -> LockNotificationsData) { - _lockedValue = f(_lockedValue) - lockedScreenPromise.set(.single(_lockedValue)) + private let alertsDisposable = MetaDisposable() + private let audioDisposable = MetaDisposable() + private let termDisposable = MetaDisposable() + private let someActionsDisposable = DisposableSet() + private let clearReadNotifiesDisposable = MetaDisposable() + private let chatUndoManagerDisposable = MetaDisposable() + private let appUpdateDisposable = MetaDisposable() + private let updatesDisposable = MetaDisposable() + private let updateFoldersDisposable = MetaDisposable() + private let _ready:Promise = Promise() + var ready: Signal { + return _ready.get() |> filter { $0 } |> take (1) + } + + func applyNewTheme() { + rightController.backgroundColor = theme.colors.background + rightController.backgroundMode = theme.controllerBackgroundMode + view.updateLocalizationAndTheme(theme: theme) } - private let query:NSMetadataQuery + private var launchAction: ApplicationContextLaunchAction? - init(window: Window, shouldOnlineKeeper:Signal, account: Account, accountManager: AccountManager, localization:LocalizationSettings?, themeSettings: ThemePalleteSettings?) { - emptyController = EmptyChatViewController(account) + init(window: Window, context: AccountContext, launchSettings: LaunchSettings, callSession: PCallSession?, groupCallContext: GroupCallContext?) { + + self.context = context + emptyController = EmptyChatViewController(context) - self.account = account self.window = window - self.accountManager = accountManager - window.maxSize = NSMakeSize(.greatestFiniteMagnitude, .greatestFiniteMagnitude) - window.minSize = NSMakeSize(380, 440) - if let themeSettings = themeSettings { - updateTheme(with: themeSettings, for: window) - } else { - setDefaultTheme(for: window) - } - if !window.initFromSaver { window.setFrame(NSMakeRect(0, 0, 800, 650), display: true) window.center() } + context.account.importableContacts.set(.single([:])) - setupAccount(account, fetchCachedResourceRepresentation: fetchCachedResourceRepresentation, transformOutgoingMessageMedia: transformOutgoingMessageMedia) + self.view = ApplicationContainerView(frame: window.contentView!.bounds) - account.stateManager.reset() - account.shouldBeServiceTaskMaster.set(.single(.now)) - account.shouldKeepOnlinePresence.set(.single(true)) - account.shouldKeepOnlinePresence.set(shouldOnlineKeeper) + + self.view.splitView.setProportion(proportion: SplitProportion(min:380, max:300+350), state: .single); + self.view.splitView.setProportion(proportion: SplitProportion(min:300+350, max:300+350+600), state: .dual) - self.splitView = SplitView(frame:mainWindow.contentView!.bounds) - - splitView.setProportion(proportion: SplitProportion(min:380, max:300+350), state: .single); - splitView.setProportion(proportion: SplitProportion(min:300+350, max:300+350+600), state: .dual) + rightController = ExMajorNavigationController(context, ChatController.self, emptyController); + rightController.set(header: NavigationHeader(44, initializer: { header, contextObject, view -> (NavigationHeaderView, CGFloat) in + let newView = view ?? InlineAudioPlayerView(header) + newView.update(with: contextObject) + return (newView, 44) + })) - rightController = ExMajorNavigationController(account, ChatController.self, emptyController); - rightController.set(header: NavigationHeader(44, initializer: { (header) -> NavigationHeaderView in - let view = InlineAudioPlayerView(header) - return view + rightController.set(callHeader: CallNavigationHeader(35, initializer: { header, contextObject, view -> (NavigationHeaderView, CGFloat) in + let newView: NavigationHeaderView + if contextObject is GroupCallContext { + if let view = view, view.className == GroupCallNavigationHeaderView.className() { + newView = view + } else { + newView = GroupCallNavigationHeaderView(header) + } + } else if contextObject is PCallSession { + if let view = view, view.className == CallNavigationHeaderView.className() { + newView = view + } else { + newView = CallNavigationHeaderView(header) + } + } else { + fatalError("not supported") + } + newView.update(with: contextObject) + return (newView, 35 + 18) })) - rightController.set(callHeader: CallNavigationHeader(35, initializer: { header -> NavigationHeaderView in - let view = CallNavigationHeaderView(header) - return view - })) - + window.rootViewController = rightController - applicationContext = TelegramApplicationContext(rightController, EntertainmentViewController(size: NSMakeSize(350, window.frame.height), account: account), network: account.network) - account.applicationContext = applicationContext + leftController = MainViewController(context); + + leftController.navigationController = rightController - leftController = MainViewController(account, accountManager: accountManager); - leftController.navigationController = rightController + super.init() - query = NSMetadataQuery() + - + updatesDisposable.set(managedAppConfigurationUpdates(accountManager: context.sharedContext.accountManager, network: context.account.network).start()) - super.init() - startNotifyListener(with: account) - NSUserNotificationCenter.default.delegate = self - - - #if BETA || STABLE - - settingsDisposable.set((account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.baseAppSettings]) |> deliverOnMainQueue).start(next: { [weak self] settings in - - let baseSettings: BaseApplicationSettings - if let settings = settings.values[ApplicationSpecificPreferencesKeys.baseAppSettings] as? BaseApplicationSettings { - baseSettings = settings - } else { - baseSettings = BaseApplicationSettings.defaultSettings - } - - if baseSettings.handleInAppKeys { - self?.applicationContext.initMediaKeyTap() - } else { - self?.applicationContext.deinitMediaKeyTap() - } + context.sharedContext.bindings = AccountContextBindings(rootNavigation: { [weak self] () -> MajorNavigationController in + guard let `self` = self else { + return MajorNavigationController(ViewController.self, ViewController(), window) + } + return self.rightController + }, mainController: { [weak self] () -> MainViewController in + guard let `self` = self else { + fatalError("Cannot use bindings. Application context is not exists") + } + return self.leftController + }, showControllerToaster: { [weak self] toaster, animated in + guard let `self` = self else { + fatalError("Cannot use bindings. Application context is not exists") + } + self.rightController.controller.show(toaster: toaster, animated: animated) + }, globalSearch: { [weak self] search in + guard let `self` = self else { + fatalError("Cannot use bindings. Application context is not exists") + } + self.leftController.tabController.select(index: self.leftController.chatIndex) + self.leftController.chatList.globalSearch(search) + }, entertainment: { [weak self] () -> EntertainmentViewController in + guard let `self` = self else { + return EntertainmentViewController.init(size: NSZeroSize, context: context) + } + if self.entertainment == nil { + self.entertainment = EntertainmentViewController(size: NSMakeSize(350, 350), context: self.context) + } + return self.entertainment! + }, switchSplitLayout: { [weak self] state in + guard let `self` = self else { + fatalError("Cannot use bindings. Application context is not exists") + } + self.view.splitView.state = state + }, needFullsize: { [weak self] in + self?.view.splitView.needFullsize() + }, displayUpgradeProgress: { progress in - })) - - #endif + }, callSession: { [weak self] in + return self?.rightController.callHeader?.contextObject as? PCallSession + }, groupCall: { [weak self] in + return self?.rightController.callHeader?.contextObject as? GroupCallContext + }, getContext: { [weak self] in + return self?.context + }) + + + termDisposable.set((context.account.stateManager.termsOfServiceUpdate |> deliverOnMainQueue).start(next: { terms in + if let terms = terms { + showModal(with: TermsModalController(context, terms: terms), for: mainWindow) + } else { + closeModal(TermsModalController.self) + } + })) + - var forceNotice:Bool = false + // var forceNotice:Bool = false if FastSettings.isMinimisize { - self.splitView.state = .minimisize - forceNotice = true + self.view.splitView.mustMinimisize = true + // forceNotice = true + } else { + self.view.splitView.mustMinimisize = false } - splitView.delegate = self; - splitView.update(forceNotice) + self.view.splitView.delegate = self; + self.view.splitView.update(false) - let accountId = account.id - self.loggedOutDisposable.set(account.loggedOut.start(next: { value in + let accountId = context.account.id + self.loggedOutDisposable.set(context.account.loggedOut.start(next: { value in if value { - let _ = logoutFromAccount(id: accountId, accountManager: accountManager).start() + let _ = logoutFromAccount(id: accountId, accountManager: context.sharedContext.accountManager, alreadyLoggedOutRemotely: false).start() } })) - let passlock = Signal.single(Void()) |> delay(60, queue: Queue.concurrentDefaultQueue()) |> restart |> mapToSignal { () -> Signal in - return account.postbox.modify { modifier -> Int32? in - return modifier.getAccessChallengeData().timeout - } - } |> map { [weak self] timeout -> Bool in - if let timeout = timeout { - if let resignTimestamp = self?.resignTimestamp { - let current = Int32(Date().timeIntervalSince1970) - if current - resignTimestamp > timeout { - return true - } + + alertsDisposable.set((context.account.stateManager.displayAlerts |> deliverOnMainQueue).start(next: { alerts in + for text in alerts { + + let alert:NSAlert = NSAlert() + alert.window.appearance = theme.appearance + alert.alertStyle = .informational + alert.messageText = appName + alert.informativeText = text.text + + if text.isDropAuth { + alert.addButton(withTitle: L10n.editAccountLogout) + alert.addButton(withTitle: L10n.modalCancel) + } - return Int64(timeout) < SystemIdleTime() - } else { - return false - } - } - |> filter { [weak self] _ in - if let strongSelf = self { - return !strongSelf._lockedValue.passcodeLock + + alert.beginSheetModal(for: window, completionHandler: { result in + if result.rawValue == 1000 && text.isDropAuth { + let _ = logoutFromAccount(id: context.account.id, accountManager: context.sharedContext.accountManager, alreadyLoggedOutRemotely: false).start() + } + }) } - return false - } - |> deliverOnMainQueue - - - let showPasslock = passlock + })) + - _passlock.set(showPasslock) + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.rightController.push(ChatController(context: context, chatLocation: .peer(context.peerId))) + return .invoked + }, with: self, for: .Zero, priority: .low, modifierFlags: [.command]) - passlockDisposable.set((_passlock.get() |> deliverOnMainQueue |> mapToSignal { [weak self] show -> Signal in - if show { - let controller = PasscodeLockController(account, .login, logoutImpl: { [weak self] in - self?.logout() - }) - showModal(with: controller, for: window) - return .single(show) |> then( controller.doneValue |> map {_ in return false} |> take(1) ) - } - return .never() - } |> deliverOnMainQueue).start(next: { [weak self] lock in - - window.contentView?.subviews.first?.isHidden = lock - - self?.updateLocked { previous -> LockNotificationsData in - return previous.withUpdatedPasscodeLock(lock) - } - })) + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(0, false) + return .invoked + }, with: self, for: .One, priority: .low, modifierFlags: [.command]) - ringingStatesDisposable.set((account.callSessionManager.ringingStates() |> deliverOn(callQueue)).start(next: { states in - pullCurrentSession( { session in - if let state = states.first { - if session == nil { - showPhoneCallWindow(PCallSession(account: account, peerId: state.peerId, id: state.id)) - } else { - account.callSessionManager.drop(internalId: state.id, reason: .busy) - } - } - } ) - })) + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(1, false) + return .invoked + }, with: self, for: .Two, priority: .low, modifierFlags: [.command]) + + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(2, false) + return .invoked + }, with: self, for: .Three, priority: .low, modifierFlags: [.command]) + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(3, false) + return .invoked + }, with: self, for: .Four, priority: .low, modifierFlags: [.command]) - // NotificationCenter.default.addObserver(self, selector: #selector(windiwDidChangeBackingProperties), name: NSNotification.Name.NSWindowDidChangeBackingProperties, object: window) - + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(4, false) + return .invoked + }, with: self, for: .Five, priority: .low, modifierFlags: [.command]) - NotificationCenter.default.addObserver(self, selector: #selector(windowDidBecomeKey), name: NSWindow.didBecomeKeyNotification, object: window) - NotificationCenter.default.addObserver(self, selector: #selector(windowDidResignKey), name: NSWindow.didResignKeyNotification, object: window) - NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(receiveWakeNote(_:)), name: NSWorkspace.screensDidWakeNotification, object: nil) - - DistributedNotificationCenter.default().addObserver(self, selector: #selector(screenIsLocked), name: NSNotification.Name(rawValue: "com.apple.screenIsLocked"), object: nil) - DistributedNotificationCenter.default().addObserver(self, selector: #selector(screenIsUnlocked), name: NSNotification.Name(rawValue: "com.apple.screenIsUnlocked"), object: nil) + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(5, false) + return .invoked + }, with: self, for: .Six, priority: .low, modifierFlags: [.command]) -// -// NotificationCenter.default.addObserver(self, selector: #selector(queryUpdated(_:)), name: NSNotification.Name.NSMetadataQueryDidStartGathering, object: query) -// -// NotificationCenter.default.addObserver(self, selector: #selector(queryUpdated(_:)), name: NSNotification.Name.NSMetadataQueryDidUpdate, object: query) -// -// NotificationCenter.default.addObserver(self, selector: #selector(queryUpdated(_:)), name: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: query) -// -// query.predicate = NSPredicate(format: "kMDItemIsScreenCapture = 1") -// _ = query.start() - - window.set(handler: { [weak self] () -> KeyHandlerResult in - - if let strongSelf = self { - if !strongSelf._lockedValue.passcodeLock { - self?._passlock.set(account.postbox.modify { modifier -> Bool in - switch modifier.getAccessChallengeData() { - case .none: - return false - default: - return true - } - }) - } - } - + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(6, false) return .invoked - }, with: self, for: .L, priority: .low, modifierFlags: [.command]) + }, with: self, for: .Seven, priority: .low, modifierFlags: [.command]) - window.set(handler: { [weak self] () -> KeyHandlerResult in - if let strongSelf = self { - strongSelf.applicationContext.mainNavigation?.push(ChatController(account: strongSelf.account, peerId: strongSelf.account.peerId)) - } + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(7, false) return .invoked - }, with: self, for: .Zero, priority: .low, modifierFlags: [.command]) + }, with: self, for: .Eight, priority: .low, modifierFlags: [.command]) + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(8, false) + return .invoked + }, with: self, for: .Nine, priority: .low, modifierFlags: [.command]) - if let localization = localization { - applyUILocalization(localization) - } - suggestedLocalizationDisposable.set(( account.postbox.preferencesView(keys: [PreferencesKeys.suggestedLocalization]) |> mapToSignal { preferences -> Signal in - - let preferences = preferences.values[PreferencesKeys.suggestedLocalization] as? SuggestedLocalizationEntry - if preferences == nil || !preferences!.isSeen, preferences?.languageCode != appCurrentLanguage.languageCode, preferences?.languageCode != "en" { - return suggestedLocalizationInfo(network: account.network, languageCode: Locale.current.languageCode ?? "en", extractKeys: ["Suggest.Localization.Header", "Suggest.Localization.Other"]) |> take(1) - } - return .complete() - } |> deliverOnMainQueue).start(next: { suggestionInfo in - showModal(with: SuggestionLocalizationViewController(account, suggestionInfo: suggestionInfo), for: window) - })) - + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(0, true) + return .invoked + }, with: self, for: .One, priority: .low, modifierFlags: [.command, .option]) + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(1, true) + return .invoked + }, with: self, for: .Two, priority: .low, modifierFlags: [.command, .option]) - localizationDisposable.set(account.postbox.preferencesView(keys: [PreferencesKeys.localizationSettings]).start(next: { view in - if let settings = view.values[PreferencesKeys.localizationSettings] as? LocalizationSettings { - applyUILocalization(settings) - } - })) + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(2, true) + return .invoked + }, with: self, for: .Three, priority: .low, modifierFlags: [.command, .option]) - rightController.backgroundColor = theme.colors.background - splitView.backgroundColor = theme.colors.background - let basic = Atomic(value: themeSettings) - appearanceDisposable.set((account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.themeSettings]) |> deliverOnMainQueue).start(next: { [weak self] view in - if let settings = view.values[ApplicationSpecificPreferencesKeys.themeSettings] as? ThemePalleteSettings { - if basic.swap(nil)?.dark != settings.dark { - updateTheme(with: settings, for: window, animated: true) - self?.rightController.backgroundColor = theme.colors.background - self?.splitView.backgroundColor = theme.colors.background - } - } - })) + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(3, true) + return .invoked + }, with: self, for: .Four, priority: .low, modifierFlags: [.command, .option]) + + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(4, true) + return .invoked + }, with: self, for: .Five, priority: .low, modifierFlags: [.command, .option]) + + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(5, true) + return .invoked + }, with: self, for: .Six, priority: .low, modifierFlags: [.command, .option]) + + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(6, true) + return .invoked + }, with: self, for: .Seven, priority: .low, modifierFlags: [.command, .option]) + + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(7, true) + return .invoked + }, with: self, for: .Eight, priority: .low, modifierFlags: [.command, .option]) + + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(8, true) + return .invoked + }, with: self, for: .Nine, priority: .low, modifierFlags: [.command, .option]) + + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(9, true) + return .invoked + }, with: self, for: .Minus, priority: .low, modifierFlags: [.command, .option]) - } - - var isLocked: Bool { - return _lockedValue.isLocked - } - - @objc private func queryUpdated(_ notification: NSNotification) { - if query.resultCount != 0 { - var bp:Int = 0 - bp += 1 - } - } - - - func logout() { - self.logoutDisposable.set((confirmSignal(for: window, header: appName, information: tr(.accountConfirmLogoutText)) |> filter {$0} |> mapToSignal { [weak self] _ -> Signal in - if let strongSelf = self { - return logoutFromAccount(id: strongSelf.account.id, accountManager: strongSelf.accountManager) - } - return .complete() - }).start()) - } - - - @objc open func windowDidBecomeKey() { - self.resignTimestamp = nil - } - - - @objc open func windowDidResignKey() { - self.resignTimestamp = Int32(Date().timeIntervalSince1970) - } - - func splitViewDidNeedSwapToLayout(state: SplitViewState) { - splitView.removeAllControllers(); - let w:CGFloat = 300; - FastSettings.isMinimisize = false - switch state { - case .single: - rightController.empty = leftController + + + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(0, true) + return .invoked + }, with: self, for: .One, priority: .low, modifierFlags: [.control]) + + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(1, true) + return .invoked + }, with: self, for: .Two, priority: .low, modifierFlags: [.control]) + + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(2, true) + return .invoked + }, with: self, for: .Three, priority: .low, modifierFlags: [.control]) + + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(3, true) + return .invoked + }, with: self, for: .Four, priority: .low, modifierFlags: [.control]) + + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(4, true) + return .invoked + }, with: self, for: .Five, priority: .low, modifierFlags: [.control]) + + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(5, true) + return .invoked + }, with: self, for: .Six, priority: .low, modifierFlags: [.control]) + + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(6, true) + return .invoked + }, with: self, for: .Seven, priority: .low, modifierFlags: [.control]) + + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(7, true) + return .invoked + }, with: self, for: .Eight, priority: .low, modifierFlags: [.control]) + + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(8, true) + return .invoked + }, with: self, for: .Nine, priority: .low, modifierFlags: [.control]) + + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.openChat(9, true) + return .invoked + }, with: self, for: .Minus, priority: .low, modifierFlags: [.control]) + + + + +// window.set(handler: { [weak self] _ -> KeyHandlerResult in +// self?.leftController.focusSearch(animated: true) +// return .invoked +// }, with: self, for: .F, priority: .supreme, modifierFlags: [.command, .shift]) + + window.set(handler: { _ -> KeyHandlerResult in + context.sharedContext.bindings.rootNavigation().push(ShortcutListController(context: context)) + return .invoked + }, with: self, for: .Slash, priority: .low, modifierFlags: [.command]) + + + + #if DEBUG + window.set(handler: { _ -> KeyHandlerResult in - if rightController.modalAction != nil { - if rightController.controller is ChatController { - rightController.push(ForwardChatListController(account), false) + + showModal(with: CoreMediaVideoIOTest(context: context), for: window) + +// filePanel(with: ["webp"], allowMultiple: false, for: window, completion: { values in +// if let first = values?.first { +// showModal(with: AnimatedWebpController(context: context, path: first), for: window) +// } +// }) + +// showModalText(for: context.window, text: "qkwjeh fkqwejfh qkwef hqwkef hqwkef hqwkef hqwkef hqwekf qwhflkj") + +// +// _ = presentDesktopCapturerWindow(select: { _ in +// +// }, devices: context.sharedContext.devicesContext) + +// filePanel(with: ["mov", "mp4"], allowMultiple: false, for: window, completion: { values in +// if let first = values?.first { +// let asset = AVURLAsset(url: URL(fileURLWithPath: first)) +// let track = asset.tracks(withMediaType: .video).first +// if let track = track { +// showModal(with: VideoAvatarModalController(context: context, asset: asset, track: track), for: window) +// } +// } +// }) + // showModal(with: VideoAvatarModalController(context: context), for: window) + + // context.sharedContext.bindings.rootNavigation().push(ShortcutListController(context: context)) + return .invoked + }, with: self, for: .T, priority: .supreme, modifierFlags: .command) + #endif + + + appUpdateDisposable.set((context.account.stateManager.appUpdateInfo |> deliverOnMainQueue).start(next: { info in + + })) + + + suggestedLocalizationDisposable.set(( context.account.postbox.preferencesView(keys: [PreferencesKeys.suggestedLocalization]) |> mapToSignal { preferences -> Signal in + + let preferences = preferences.values[PreferencesKeys.suggestedLocalization] as? SuggestedLocalizationEntry + if preferences == nil || !preferences!.isSeen, preferences?.languageCode != appCurrentLanguage.languageCode, preferences?.languageCode != "en" { + let current = Locale.preferredLanguages[0] + let split = current.split(separator: "-") + let lan: String = !split.isEmpty ? String(split[0]) : "en" + if lan != "en" { + return context.engine.localization.suggestedLocalizationInfo(languageCode: lan, extractKeys: ["Suggest.Localization.Header", "Suggest.Localization.Other"]) |> take(1) } } - splitView.addController(controller: rightController, proportion: SplitProportion(min:380, max:CGFloat.greatestFiniteMagnitude)) - case .dual: - rightController.empty = emptyController - if rightController.controller is ForwardChatListController { - rightController.back(animated:false) + return .complete() + } |> deliverOnMainQueue).start(next: { suggestionInfo in + if suggestionInfo.availableLocalizations.count >= 2 { + showModal(with: SuggestionLocalizationViewController(context, suggestionInfo: suggestionInfo), for: window) } - splitView.addController(controller: leftController, proportion: SplitProportion(min:w, max:w)) - splitView.addController(controller: rightController, proportion: SplitProportion(min:380, max:CGFloat.greatestFiniteMagnitude)) - case .minimisize: - FastSettings.isMinimisize = true - splitView.addController(controller: leftController, proportion: SplitProportion(min:70, max:70)) - splitView.addController(controller: rightController, proportion: SplitProportion(min:380, max:CGFloat.greatestFiniteMagnitude)) - default: - break; - } + })) + + someActionsDisposable.add(context.engine.peers.managedUpdatedRecentPeers().start()) - account.context.layoutHandler.set(state) - splitView.layout() - } - - @objc func screenIsLocked() { - - if !_lockedValue.passcodeLock { - _passlock.set(account.postbox.modify { modifier -> Bool in - switch modifier.getAccessChallengeData() { - case .none: - return false - default: - return true - } - }) - } + + clearReadNotifiesDisposable.set(context.account.stateManager.appliedIncomingReadMessages.start(next: { msgIds in + UNUserNotifications.current?.clearNotifies(by: msgIds) + })) - updateLocked { (previous) -> LockNotificationsData in - return previous.withUpdatedScreenLock(true) - } - } - - @objc func screenIsUnlocked() { - updateLocked { (previous) -> LockNotificationsData in - return previous.withUpdatedScreenLock(false) - } - } - - func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true - } - - func splitViewDidNeedMinimisize(controller: ViewController) { + - } - - func splitViewDidNeedFullsize(controller: ViewController) { + someActionsDisposable.add(applyUpdateTextIfNeeded(context.account.postbox).start()) - } - - func splitViewIsCanMinimisize() -> Bool { - return self.leftController.isCanMinimisize(); - } - - func splitViewDrawBorder() -> Bool { - return false - } - - deinit { - self.account.shouldKeepOnlinePresence.set(.single(false)) - self.account.shouldBeServiceTaskMaster.set(.single(.never)) - nofityDisposable.dispose() - NSWorkspace.shared.notificationCenter.removeObserver(self) - DistributedNotificationCenter.default().removeObserver(self) - NotificationCenter.default.removeObserver(self, name: NSWindow.didBecomeKeyNotification, object: window) - NotificationCenter.default.removeObserver(self, name: NSWindow.didResignKeyNotification, object: window) - self.loggedOutDisposable.dispose() - passlockDisposable.dispose() - logoutDisposable.dispose() - window.removeAllHandlers(for: self) - settingsDisposable.dispose() - ringingStatesDisposable.dispose() - localizationDisposable.dispose() - suggestedLocalizationDisposable.dispose() - appearanceDisposable.dispose() - //query.stop() - } - - - func startNotifyListener(with account: Account) { + - let lockedSreenSignal = lockedScreenPromise.get() + let foldersSemaphore = DispatchSemaphore(value: 0) + var folders: ChatListFolders = ChatListFolders(list: [], sidebar: false) + + _ = (chatListFilterPreferences(engine: context.engine) |> take(1)).start(next: { value in + folders = value + foldersSemaphore.signal() + }) + foldersSemaphore.wait() - self.nofityDisposable.set((account.stateManager.notificationMessages |> mapToSignal { messages -> Signal<([Message], InAppNotificationSettings), Void> in - return account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.inAppNotificationSettings]) |> mapToSignal { (settings) -> Signal<([Message], InAppNotificationSettings), Void> in - - let inAppSettings: InAppNotificationSettings - if let settings = settings.values[ApplicationSpecificPreferencesKeys.inAppNotificationSettings] as? InAppNotificationSettings { - inAppSettings = settings - } else { - inAppSettings = InAppNotificationSettings.defaultSettings - } - if inAppSettings.enabled && inAppSettings.muteUntil < Int32(Date().timeIntervalSince1970) { - return .single((messages, inAppSettings)) + self.updateLeftSidebar(with: folders, layout: context.sharedContext.layout, animated: false) + + + self.view.splitView.layout() + + + + + if let navigation = launchSettings.navigation { + switch navigation { + case .settings: + self.launchAction = .preferences + _ready.set(leftController.settings.ready.get()) + leftController.tabController.select(index: leftController.settingsIndex) + case let .profile(peerId, necessary): + let peerSemaphore = DispatchSemaphore(value: 0) + var peer: Peer? + _ = context.account.postbox.transaction { transaction in + peer = transaction.getPeer(peerId) + peerSemaphore.signal() + }.start() + peerSemaphore.wait() + + _ready.set(leftController.chatList.ready.get()) + self.leftController.tabController.select(index: self.leftController.chatIndex) + + if (necessary || context.sharedContext.layout != .single) { + if let _ = peer { + let controller = PeerInfoController(context: context, peerId: peerId) + controller.navigationController = self.rightController + controller.loadViewIfNeeded(self.rightController.bounds) + + self.launchAction = .navigate(controller) + + self._ready.set(combineLatest(self.leftController.chatList.ready.get(), controller.ready.get()) |> map { $0 && $1 }) + self.leftController.tabController.select(index: self.leftController.chatIndex) + } else { + // self._ready.set(self.leftController.chatList.ready.get()) + self.leftController.tabController.select(index: self.leftController.chatIndex) + self._ready.set(.single(true)) + } } else { - return .complete() + // self._ready.set(.single(true)) + _ready.set(leftController.chatList.ready.get()) + self.leftController.tabController.select(index: self.leftController.chatIndex) } + case let .chat(peerId, necessary): + let peerSemaphore = DispatchSemaphore(value: 0) + var peer: Peer? + _ = context.account.postbox.transaction { transaction in + peer = transaction.getPeer(peerId) + peerSemaphore.signal() + }.start() + peerSemaphore.wait() - } - } - |> mapToSignal { messages, inAppSettings -> Signal<([Message],[MessageId:NSImage], InAppNotificationSettings), Void> in + _ready.set(leftController.chatList.ready.get()) + self.leftController.tabController.select(index: self.leftController.chatIndex) - var photos:[Signal<(MessageId, CGImage?),Void>] = [] - for message in messages { - var peer = message.author - if let mainPeer = messageMainPeer(message) { - if mainPeer is TelegramChannel || mainPeer is TelegramGroup { - peer = mainPeer - } - } + if (necessary || context.sharedContext.layout != .single) { if let peer = peer { - if let image = peerAvatarImage(account: account, peer: peer) { - photos.append(image |> map { image in return (message.id,image)}) - } + let controller = ChatController(context: context, chatLocation: .peer(peer.id)) + controller.navigationController = self.rightController + controller.loadViewIfNeeded(self.rightController.bounds) + + self.launchAction = .navigate(controller) + + self._ready.set(combineLatest(self.leftController.chatList.ready.get(), controller.ready.get()) |> map { $0 && $1 }) + self.leftController.tabController.select(index: self.leftController.chatIndex) + } else { + // self._ready.set(self.leftController.chatList.ready.get()) + self.leftController.tabController.select(index: self.leftController.chatIndex) + self._ready.set(.single(true)) } + } else { + // self._ready.set(.single(true)) + _ready.set(leftController.chatList.ready.get()) + self.leftController.tabController.select(index: self.leftController.chatIndex) } + case let .thread(threadId, fromId, _): + self.leftController.tabController.select(index: self.leftController.chatIndex) + self._ready.set(self.leftController.chatList.ready.get()) + + let signal:Signal = fetchAndPreloadReplyThreadInfo(context: context, subject: .channelPost(threadId)) - return combineLatest(photos) |> map { resources in - var images:[MessageId:NSImage] = [:] - for (messageId,image) in resources { - if let image = image { - images[messageId] = NSImage(cgImage: image, size: NSMakeSize(50,50)) - } + _ = showModalProgress(signal: signal |> take(1), for: context.window).start(next: { [weak context] result in + guard let context = context else { + return } - return (messages,images, inAppSettings) - } - } |> mapToSignal { messages, images, inAppSettings -> Signal<([Message],[MessageId:NSImage], InAppNotificationSettings, Bool), Void> in - return lockedSreenSignal |> take(1) - |> map { data in return (messages, images, inAppSettings, data.isLocked)} - } - |> mapToSignal { values in - return _callSession() |> map { s in - return (values.0, values.1, values.2, values.3, s != nil) - } - } |> deliverOnMainQueue).start(next: { messages, images, inAppSettings, screenIsLocked, inCall in - for message in messages { - if message.author?.id != account.peerId { - var title:String = message.author?.displayTitle ?? "" - var hasReplyButton:Bool = true - if let peer = message.peers[message.id.peerId], peer is TelegramChannel || peer is TelegramGroup { - title = peer.displayTitle - hasReplyButton = peer.canSendMessage - } - var text = chatListText(account: account, for: message).string.nsstring - var subText:String? - if text.contains("\n") { - let parts = text.components(separatedBy: "\n") - text = parts[1] as NSString - subText = parts[0] - } - - if !inAppSettings.displayPreviews || message.peers[message.id.peerId] is TelegramSecretChat || screenIsLocked { - text = tr(.notificationLockedPreview).nsstring - subText = nil - } - - let notification = NSUserNotification() - notification.title = title - notification.informativeText = text as String - notification.subtitle = subText - notification.contentImage = images[message.id] - notification.hasReplyButton = hasReplyButton - - if localizedString(inAppSettings.tone) != tr(.notificationSettingsToneNone) { - notification.soundName = inAppSettings.tone - } else { - notification.soundName = nil - } - - if message.muted || inCall { - notification.soundName = nil - } - - let encoded:WriteBuffer = WriteBuffer() - message.id.encodeToBuffer(encoded) - notification.userInfo = ["encodedMessageId":encoded.makeData(), "peerId.namespace":message.id.peerId.namespace, "peerId.id":message.id.peerId.id] - NSUserNotificationCenter.default.deliver(notification) + let chatLocation: ChatLocation = .replyThread(result.message) + + let updatedMode: ReplyThreadMode + if result.isChannelPost { + updatedMode = .comments(origin: fromId) + } else { + updatedMode = .replies(origin: fromId) } - } - })) - } - - - - @objc func receiveWakeNote(_ notificaiton:Notification) { - account.shouldBeServiceTaskMaster.set(.single(.never) |> then(.single(.now))) - } - - - - func userNotificationCenter(_ center: NSUserNotificationCenter, didActivate notification: NSUserNotification) { - - - if let encodedMessageId = notification.userInfo?["encodedMessageId"] as? Data { - let messageId = MessageId(ReadBuffer(memoryBufferNoCopy: MemoryBuffer(data: encodedMessageId))) - rightController.push(ChatController(account: account, peerId: messageId.peerId), false) - - if notification.activationType == .replied, let text = notification.response?.string { - var replyToMessageId:MessageId? - if messageId.peerId.namespace != Namespaces.Peer.CloudUser { - replyToMessageId = messageId - } - _ = enqueueMessages(account: account, peerId: messageId.peerId, messages: [EnqueueMessage.message(text: text, attributes: [], media: nil, replyToMessageId: replyToMessageId)]).start() - } else { - self.window.deminiaturize(self) - NSApp.activate(ignoringOtherApps: true) + + let controller = ChatController.init(context: context, chatLocation: chatLocation, mode: .replyThread(data: result.message, mode: updatedMode), messageId: fromId, initialAction: nil, chatLocationContextHolder: result.contextHolder) + + context.sharedContext.bindings.rootNavigation().push(controller) + + }, error: { error in + + }) + } + } else { + // self._ready.set(.single(true)) + _ready.set(leftController.chatList.ready.get()) + leftController.tabController.select(index: leftController.chatIndex) + // _ready.set(leftController.ready.get()) } - } - - -} - - - - - - - - - -private class LegacyPasscodeHeaderView : View { - - private let logo:ImageView = ImageView() - private let header:TextView = TextView() - private let desc1:TextView = TextView() - - private let desc2:TextView = TextView() - - required init(frame frameRect: NSRect) { - super.init(frame: frameRect) - - let logoImage = #imageLiteral(resourceName: "Icon_LegacyIntro").precomposed() - self.logo.image = logoImage - self.logo.sizeToFit() - let headerLayout = TextViewLayout(NSAttributedString.initialize(string: appName, color: NSColor.text, font: NSFont.normal(.custom(28))), maximumNumberOfLines: 1) - headerLayout.measure(width: CGFloat.greatestFiniteMagnitude) - header.update(headerLayout) + if let session = callSession { + context.sharedContext.showCall(with: session) + } - let descLayout1 = TextViewLayout(NSAttributedString.initialize(string: tr(.legacyIntroDescription1), color: .grayText, font: NSFont.normal(FontSize.text)), alignment: .center) - descLayout1.measure(width: frameRect.width - 200) - desc1.update(descLayout1) + if let groupCallContext = groupCallContext { + context.sharedContext.showGroupCall(with: groupCallContext) + } - let descLayout2 = TextViewLayout(NSAttributedString.initialize(string: tr(.legacyIntroDescription2), color: .grayText, font: NSFont.normal(FontSize.text)), alignment: .center) - descLayout2.measure(width: frameRect.width - 200) - desc2.update(descLayout2) + self.updateFoldersDisposable.set(combineLatest(queue: .mainQueue(), chatListFilterPreferences(engine: context.engine), context.sharedContext.layoutHandler.get()).start(next: { [weak self] value, layout in + self?.updateLeftSidebar(with: value, layout: layout, animated: true) + })) - addSubview(logo) - addSubview(header) - addSubview(desc1) - addSubview(desc2) - logo.centerX() - header.centerX(y: logo.frame.maxY + 10) - desc1.centerX(y: header.frame.maxY + 10) - desc2.centerX(y: desc1.frame.maxY + 10) + if let controller = globalAudio, let header = self.rightController.header { + self.rightController.header?.show(false, contextObject: InlineAudioPlayerView.ContextObject(controller: controller, context: context, tableView: nil, supportTableView: nil)) + } - self.setFrameSize(frame.width, desc2.frame.maxY) + // _ready.set(.single(true)) } - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - + private var folders: ChatListFolders? + private var previousLayout: SplitViewState? + private let foldersReadyDisposable = MetaDisposable() + private func updateLeftSidebar(with folders: ChatListFolders, layout: SplitViewState, animated: Bool) -> Void { + + let currentSidebar = !folders.list.isEmpty && (folders.sidebar || layout == .minimisize) + let previousSidebar = self.folders == nil ? nil : !self.folders!.list.isEmpty && (self.folders!.sidebar || self.previousLayout == SplitViewState.minimisize) -class LegacyIntroView : View, NSTextFieldDelegate { - fileprivate let input:NSSecureTextField - fileprivate let logoutTextView:TextView = TextView() - fileprivate let doneButton:TitleButton = TitleButton() - fileprivate let legacyIntro: LegacyPasscodeHeaderView - fileprivate var layoutWithPasscode: Bool = false { - didSet { - self.input.isHidden = !layoutWithPasscode - self.logoutTextView.isHidden = !layoutWithPasscode - self.needsLayout = true - self.needsDisplay = true + let readySignal: Signal + + if currentSidebar != previousSidebar { + if !currentSidebar { + leftSidebarController?.removeFromSuperview() + leftSidebarController = nil + readySignal = .single(true) + } else { + let controller = LeftSidebarController(context, filterData: leftController.chatList.filterSignal, updateFilter: leftController.chatList.updateFilter) + controller._frameRect = NSMakeRect(0, 0, leftSidebarWidth, window.frame.height) + controller.loadViewIfNeeded() + self.leftSidebarController = controller + readySignal = controller.ready.get() |> take(1) + } + let enlarge: CGFloat + + if currentSidebar && previousSidebar != nil { + enlarge = leftSidebarWidth + } else { + if previousSidebar == true { + enlarge = -leftSidebarWidth + } else { + enlarge = 0 + } + } + + foldersReadyDisposable.set(readySignal.start(next: { [weak self] _ in + guard let `self` = self else { + return + } + self.view.updateLeftSideView(self.leftSidebarController?.genericView, animated: animated) + if !self.window.isFullScreen { + self.window.setFrame(NSMakeRect(max(0, self.window.frame.minX - enlarge), self.window.frame.minY, self.window.frame.width + enlarge, self.window.frame.height), display: true, animate: false) + } + self.updateMinMaxWindowSize(animated: animated) + })) + + } + self.folders = folders + self.previousLayout = layout } - required init(frame frameRect: NSRect) { - input = NSSecureTextField(frame: NSZeroRect) - legacyIntro = LegacyPasscodeHeaderView(frame: NSMakeRect(0,0, frameRect.width, 300)) - super.init(frame: frameRect) - - - doneButton.set(font: .medium(.header), for: .Normal) - doneButton.set(text: tr(.legacyIntroNext), for: .Normal) - - doneButton.set(color: .blueUI, for: .Normal) - - doneButton.sizeToFit() - addSubview(doneButton) - - addSubview(input) - addSubview(logoutTextView) - - input.isBordered = false - input.isBezeled = false - input.focusRingType = .none - input.alignment = .center - input.delegate = self - - let attr = NSMutableAttributedString()//Passcode.EnterPasscodePlaceholder - _ = attr.append(string: tr(.passcodeEnterPasscodePlaceholder), color: .grayText, font: NSFont.normal(FontSize.text)) - attr.setAlignment(.center, range: attr.range) - input.placeholderAttributedString = attr - input.font = NSFont.normal(FontSize.text) - input.textColor = .text - input.sizeToFit() - - let logoutAttr = NSMutableAttributedString() - _ = logoutAttr.append(string: tr(.passcodeLogoutDescription), color: .grayText, font: .normal(.text)) - _ = logoutAttr.append(string: " ") - let range = logoutAttr.append(string: tr(.passcodeLogoutLinkText), color: .link, font: .normal(.text)) - logoutAttr.add(link: inAppLink.logout({}), for: range) - logoutTextView.set(layout: TextViewLayout(logoutAttr)) - + + + private func updateMinMaxWindowSize(animated: Bool) { + window.maxSize = NSMakeSize(.greatestFiniteMagnitude, .greatestFiniteMagnitude) + var width: CGFloat = 380 + if leftSidebarController != nil { + width += leftSidebarWidth + } + if context.sharedContext.layout == .minimisize { + width += 70 + } + window.minSize = NSMakeSize(width, 500) - addSubview(legacyIntro) + if window.frame.width < window.minSize.width { + window.setFrame(NSMakeRect(max(0, window.frame.minX - (window.minSize.width - window.frame.width)), window.frame.minY, window.minSize.width, window.frame.height), display: true, animate: false) + } } - override func draw(_ layer: CALayer, in ctx: CGContext) { - super.draw(layer, in: ctx) - if !input.isHidden { - ctx.setFillColor(theme.colors.border.cgColor) - ctx.fill(NSMakeRect(input.frame.minX, input.frame.maxY + 10, input.frame.width, .borderSize)) + + + func runLaunchAction() { + if let launchAction = launchAction { + switch launchAction { + case let .navigate(controller): + leftController.tabController.select(index: leftController.chatIndex) + context.sharedContext.bindings.rootNavigation().push(controller, context.sharedContext.layout == .single) + case .preferences: + leftController.tabController.select(index: leftController.settingsIndex) + } + self.launchAction = nil + } else { + leftController.tabController.select(index: leftController.chatIndex) + } + Queue.mainQueue().justDispatch { [weak self] in + self?.leftController.prepareControllers() } } - override func layout() { - super.layout() - - legacyIntro.centerX(y: 80) - - logoutTextView.layout?.measure(width: frame.width - 40) - logoutTextView.update(logoutTextView.layout) - - input.setFrameSize(200, input.frame.height) - input.centerX(y: legacyIntro.frame.maxY + 30) - logoutTextView.centerX(y:frame.height - logoutTextView.frame.height - 20) - - doneButton.centerX(y : (input.isHidden ? legacyIntro.frame.maxY : input.frame.maxY) + 30) - - setNeedsDisplayLayer() - + private func openChat(_ index: Int, _ force: Bool = false) { + leftController.openChat(index, force: force) } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + func splitResizeCursor(at point: NSPoint) -> NSCursor? { + if FastSettings.isMinimisize { + return NSCursor.resizeRight + } else { + if window.frame.width - point.x <= 380 { + return NSCursor.resizeLeft + } + return NSCursor.resizeLeftRight + } } -} - -class LegacyIntroController: GenericViewController { - private let disposable:MetaDisposable = MetaDisposable() - private let promise:Promise - private let defaultData:AuthorizationLegacyData - init(promise:Promise, defaultLegacyData:AuthorizationLegacyData) { - self.promise = promise - self.defaultData = defaultLegacyData - super.init() - } - - override func viewDidLoad() { - super.viewDidLoad() - readyOnce() - - genericView.doneButton.set(handler: { [weak self] _ in - self?.checkCodeAndAuth() - }, for: .Click) - - genericView.input.target = self - genericView.input.action = #selector(checkCodeAndAuth) - - switch defaultData { - case .data, .none: - genericView.layoutWithPasscode = false - case .passcodeRequired: - genericView.layoutWithPasscode = true + func splitViewShouldResize(at point: NSPoint) { + if !FastSettings.isMinimisize { + let max_w = window.frame.width - 380 + let result = round(min(max(point.x, 300), max_w)) + FastSettings.updateLeftColumnWidth(result) + self.view.splitView.updateStartSize(size: NSMakeSize(result, result), controller: leftController) } - mainWindow.set(responder: { [weak self] () -> NSResponder? in - return self?.firstResponder() - }, with: self, priority: .low) } - private func logout() { - promise.set(.single(.none)) - } + - @objc private func checkCodeAndAuth() { - - switch defaultData { - case .data: - promise.set(.single(defaultData)) - break - case .passcodeRequired: - if let md5Hash = ObjcUtils.md5(genericView.input.stringValue).data(using: .utf8) { - var part1: Data = md5Hash.subdata(in : 0 ..< 16) - var part2: Data = md5Hash.subdata(in : 16 ..< 32) - - var zero:UInt8 = 0 - for _ in 0 ..< 16 { - part1.append(&zero, count: 1) - part2.append(&zero, count: 1) - } - - part1.append(part2) - - let legacy = legacyAuthData(passcode: part1, textPasscode: genericView.input.stringValue) - - switch legacy { - case .data: - promise.set(.single(legacy)) - case .passcodeRequired: - genericView.input.shake() - case .none: - promise.set(.single(.none)) + func splitViewDidNeedSwapToLayout(state: SplitViewState) { + let previousState = self.view.splitView.state + self.view.splitView.removeAllControllers() + let w:CGFloat = FastSettings.leftColumnWidth + FastSettings.isMinimisize = false + self.view.splitView.mustMinimisize = false + switch state { + case .single: + rightController.empty = leftController + + if rightController.modalAction != nil { + if rightController.controller is ChatController { + rightController.push(ForwardChatListController(context), false) } - + } + if rightController.stackCount == 1, previousState != .none { + leftController.viewWillAppear(false) + } + self.view.splitView.addController(controller: rightController, proportion: SplitProportion(min:380, max:CGFloat.greatestFiniteMagnitude)) + if rightController.stackCount == 1, previousState != .none { + leftController.viewDidAppear(false) } - - + case .dual: + rightController.empty = emptyController + if rightController.controller is ForwardChatListController { + rightController.back(animated:false) + } + self.view.splitView.addController(controller: leftController, proportion: SplitProportion(min:w, max:w)) + self.view.splitView.addController(controller: rightController, proportion: SplitProportion(min:380, max:CGFloat.greatestFiniteMagnitude)) + case .minimisize: + self.view.splitView.mustMinimisize = true + FastSettings.isMinimisize = true + self.view.splitView.addController(controller: leftController, proportion: SplitProportion(min:70, max:70)) + self.view.splitView.addController(controller: rightController, proportion: SplitProportion(min:380, max:CGFloat.greatestFiniteMagnitude)) default: - break + break; } + + context.sharedContext.layoutHandler.set(state) + updateMinMaxWindowSize(animated: false) + self.view.splitView.layout() + } - override func firstResponder() -> NSResponder? { - if !(window?.firstResponder is NSText) { - return genericView.input - } - let editor = self.window?.fieldEditor(true, for: genericView.input) - if window?.firstResponder != editor { - return genericView.input - } - return editor + + + func splitViewDidNeedMinimisize(controller: ViewController) { } - deinit { - disposable.dispose() - mainWindow.removeObserver(for: self) + func splitViewDidNeedFullsize(controller: ViewController) { + + } + + func splitViewIsCanMinimisize() -> Bool { + return self.leftController.isCanMinimisize(); + } + + func splitViewDrawBorder() -> Bool { + return false } - override func viewClass() -> AnyClass { - return LegacyIntroView.self + deinit { + self.loggedOutDisposable.dispose() + window.removeAllHandlers(for: self) + settingsDisposable.dispose() + ringingStatesDisposable.dispose() + suggestedLocalizationDisposable.dispose() + audioDisposable.dispose() + alertsDisposable.dispose() + termDisposable.dispose() + viewer?.close() + someActionsDisposable.dispose() + clearReadNotifiesDisposable.dispose() + chatUndoManagerDisposable.dispose() + appUpdateDisposable.dispose() + updatesDisposable.dispose() + updateFoldersDisposable.dispose() + foldersReadyDisposable.dispose() + context.cleanup() + NotificationCenter.default.removeObserver(self) } } + + diff --git a/Telegram-Mac/ApplicationSpecificPreferencesKeys.swift b/Telegram-Mac/ApplicationSpecificPreferencesKeys.swift index 05091f0cac..fe6b110b6e 100644 --- a/Telegram-Mac/ApplicationSpecificPreferencesKeys.swift +++ b/Telegram-Mac/ApplicationSpecificPreferencesKeys.swift @@ -8,26 +8,72 @@ import Cocoa -import TelegramCoreMac +import TelegramCore + private enum ApplicationSpecificPreferencesKeyValues: Int32 { case inAppNotificationSettings case baseAppSettings - case automaticMediaDownloadSettings case generatedMediaStoreSettings - case voiceCallSettings - case themeSettings - case recentEmoji = 14 case instantViewAppearance = 11 + case additionalSettings = 15 + case themeSettings = 22 + case readArticles = 25 + case autoNight = 26 + case stickerSettings = 29 + case launchSettings = 30 + case automaticMediaDownloadSettings = 31 + case autoplayMedia = 32 + case voiceCallSettings = 34 + case downloadedPaths = 35 + case walletPasscodeTimeout = 37 + case passcodeSettings = 38 + case appConfiguration = 39 + case chatListSettings = 47 + case recentEmoji = 48 + case voipDerivedState = 49 } struct ApplicationSpecificPreferencesKeys { - static let inAppNotificationSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.inAppNotificationSettings.rawValue) - static let baseAppSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.baseAppSettings.rawValue) static let automaticMediaDownloadSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.automaticMediaDownloadSettings.rawValue) static let generatedMediaStoreSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.generatedMediaStoreSettings.rawValue) - static let voiceCallSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.voiceCallSettings.rawValue) - static let themeSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.themeSettings.rawValue) static let recentEmoji = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.recentEmoji.rawValue) static let instantViewAppearance = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.instantViewAppearance.rawValue) + static let readArticles = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.readArticles.rawValue) + static let stickerSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.stickerSettings.rawValue) + static let launchSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.launchSettings.rawValue) + static let autoplayMedia = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.autoplayMedia.rawValue) + static let downloadedPaths = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.downloadedPaths.rawValue) + static let walletPasscodeTimeout = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.walletPasscodeTimeout.rawValue) + static let chatListSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.chatListSettings.rawValue) + static let voipDerivedState = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.voipDerivedState.rawValue) +} + +struct ApplicationSharedPreferencesKeys { + static let baseAppSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.baseAppSettings.rawValue) + static let inAppNotificationSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.inAppNotificationSettings.rawValue) + static let themeSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.themeSettings.rawValue) + static let autoNight = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.autoNight.rawValue) + static let additionalSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.additionalSettings.rawValue) + static let voiceCallSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.voiceCallSettings.rawValue) + static let passcodeSettings = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.passcodeSettings.rawValue) + static let appConfiguration = applicationSpecificPreferencesKey(ApplicationSpecificPreferencesKeyValues.appConfiguration.rawValue) +} + + +private enum ApplicationSpecificItemCacheCollectionIdValues: Int8 { + case instantPageStoredState = 0 + case cachedInstantPages = 1 +} + +public struct ApplicationSpecificItemCacheCollectionId { + public static let instantPageStoredState = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.instantPageStoredState.rawValue) + public static let cachedInstantPages = applicationSpecificItemCacheCollectionId(ApplicationSpecificItemCacheCollectionIdValues.cachedInstantPages.rawValue) +} +private enum ApplicationSpecificOrderedItemListCollectionIdValues: Int32 { + case settingsSearchRecentItems = 0 +} + +public struct ApplicationSpecificOrderedItemListCollectionId { + public static let settingsSearchRecentItems = applicationSpecificOrderedItemListCollectionId(ApplicationSpecificOrderedItemListCollectionIdValues.settingsSearchRecentItems.rawValue) } diff --git a/Telegram-Mac/ArchivedStickerPacksController.swift b/Telegram-Mac/ArchivedStickerPacksController.swift index d5682efe36..56ae0e6b06 100644 --- a/Telegram-Mac/ArchivedStickerPacksController.swift +++ b/Telegram-Mac/ArchivedStickerPacksController.swift @@ -8,18 +8,19 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac -import PostboxMac -import TelegramCoreMac +import SwiftSignalKit +import Postbox +import TelegramCore + private final class ArchivedStickerPacksControllerArguments { - let account: Account + let context: AccountContext let openStickerPack: (StickerPackCollectionInfo) -> Void let removePack: (StickerPackCollectionInfo) -> Void - init(account: Account, openStickerPack: @escaping (StickerPackCollectionInfo) -> Void, removePack: @escaping (StickerPackCollectionInfo) -> Void) { - self.account = account + init(context: AccountContext, openStickerPack: @escaping (StickerPackCollectionInfo) -> Void, removePack: @escaping (StickerPackCollectionInfo) -> Void) { + self.context = context self.openStickerPack = openStickerPack self.removePack = removePack } @@ -30,99 +31,46 @@ private final class ArchivedStickerPacksControllerArguments { private enum ArchivedStickerPacksEntryId: Hashable { case index(Int32) case pack(ItemCollectionId) - + case loading var hashValue: Int { switch self { case let .index(index): return index.hashValue case let .pack(id): return id.hashValue - } - } - - static func ==(lhs: ArchivedStickerPacksEntryId, rhs: ArchivedStickerPacksEntryId) -> Bool { - switch lhs { - case let .index(index): - if case .index(index) = rhs { - return true - } else { - return false - } - case let .pack(id): - if case .pack(id) = rhs { - return true - } else { - return false - } + case .loading: + return -100 } } } private enum ArchivedStickerPacksEntry: TableItemListNodeEntry { case section(sectionId:Int32) - case info(sectionId:Int32, String) - case pack(sectionId:Int32, Int32, StickerPackCollectionInfo, StickerPackItem?, Int32, Bool, ItemListStickerPackItemEditing) - + case info(sectionId:Int32, String, GeneralViewType) + case pack(sectionId:Int32, Int32, StickerPackCollectionInfo, StickerPackItem?, Int32, Bool, ItemListStickerPackItemEditing, GeneralViewType) + case loading(Bool) var stableId: ArchivedStickerPacksEntryId { switch self { case .info: return .index(0) - case let .pack(_, _, info, _, _, _, _): + case .loading: + return .loading + case let .pack(_, _, info, _, _, _, _, _): return .pack(info.id) case let .section(sectionId): return .index((sectionId + 1) * 1000 - sectionId) } } - static func ==(lhs: ArchivedStickerPacksEntry, rhs: ArchivedStickerPacksEntry) -> Bool { - switch lhs { - case let .info(sectionId, text): - if case .info(sectionId, text) = rhs { - return true - } else { - return false - } - case let .pack(lhsSectionId, lhsIndex, lhsInfo, lhsTopItem, lhsCount, lhsEnabled, lhsEditing): - if case let .pack(rhsSectionId, rhsIndex, rhsInfo, rhsTopItem, rhsCount, rhsEnabled, rhsEditing) = rhs { - if lhsIndex != rhsIndex { - return false - } - if lhsSectionId != rhsSectionId { - return false - } - if lhsInfo != rhsInfo { - return false - } - if lhsTopItem != rhsTopItem { - return false - } - if lhsCount != rhsCount { - return false - } - if lhsEnabled != rhsEnabled { - return false - } - if lhsEditing != rhsEditing { - return false - } - return true - } else { - return false - } - case let .section(sectionId): - if case .section(sectionId) = rhs { - return true - } else { - return false - } - } - } + var stableIndex:Int32 { switch self { case .info: return 0 + case .loading: + return -1 case .pack: fatalError("") case let .section(sectionId): @@ -132,10 +80,11 @@ private enum ArchivedStickerPacksEntry: TableItemListNodeEntry { var index:Int32 { switch self { - - case let .info(sectionId, _): + case .loading: + return 0 + case let .info(sectionId, _, _): return (sectionId * 1000) + stableIndex - case let .pack( sectionId, index, _, _, _, _, _): + case let .pack( sectionId, index, _, _, _, _, _, _): return (sectionId * 1000) + 100 + index case let .section(sectionId): return (sectionId + 1) * 1000 - sectionId @@ -148,10 +97,10 @@ private enum ArchivedStickerPacksEntry: TableItemListNodeEntry { func item(_ arguments: ArchivedStickerPacksControllerArguments, initialSize: NSSize) -> TableRowItem { switch self { - case let .info(_, text): - return GeneralTextRowItem(initialSize, stableId: stableId, text: text) - case let .pack(_, _, info, topItem, count, enabled, editing): - return StickerSetTableRowItem(initialSize, account: arguments.account, stableId: stableId, info: info, topItem: topItem, itemCount: count, unread: false, editing: editing, enabled: enabled, control: .remove, action: { + case let .info(_, text, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, viewType: viewType) + case let .pack(_, _, info, topItem, count, enabled, editing, viewType): + return StickerSetTableRowItem(initialSize, context: arguments.context, stableId: stableId, info: info, topItem: topItem, itemCount: count, unread: false, editing: editing, enabled: enabled, control: .remove, viewType: viewType, action: { arguments.openStickerPack(info) }, addPack: { @@ -159,7 +108,9 @@ private enum ArchivedStickerPacksEntry: TableItemListNodeEntry { arguments.removePack(info) }) case .section: - return GeneralRowItem(initialSize, height: 20, stableId: stableId) + return GeneralRowItem(initialSize, height: 30, stableId: stableId, viewType: .separator) + case .loading(let loading): + return SearchEmptyRowItem(initialSize, stableId: stableId, isLoading: loading, text: L10n.archivedStickersEmpty) } } } @@ -201,28 +152,40 @@ private struct ArchivedStickerPacksControllerState: Equatable { private func archivedStickerPacksControllerEntries(state: ArchivedStickerPacksControllerState, packs: [ArchivedStickerPackItem]?, installedView: CombinedView) -> [ArchivedStickerPacksEntry] { var entries: [ArchivedStickerPacksEntry] = [] - var sectionId:Int32 = 1 - entries.append(.section(sectionId: sectionId)) - sectionId += 1 + if let packs = packs { - entries.append(.info(sectionId: sectionId, tr(.archivedStickersDescription))) - entries.append(.section(sectionId: sectionId)) - sectionId += 1 - var installedIds = Set() - if let view = installedView.views[.itemCollectionIds(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])] as? ItemCollectionIdsView, let ids = view.idsByNamespace[Namespaces.ItemCollection.CloudStickerPacks] { - installedIds = ids - } - - var index: Int32 = 0 - for item in packs { - if !installedIds.contains(item.info.id) { - entries.append(.pack(sectionId: sectionId, index, item.info, item.topItems.first, item.info.count, !state.removingPackIds.contains(item.info.id), ItemListStickerPackItemEditing(editable: true, editing: state.editing))) + if packs.isEmpty { + entries.append(.loading(false)) + } else { + var sectionId:Int32 = 1 + + entries.append(.section(sectionId: sectionId)) + sectionId += 1 + + entries.append(.info(sectionId: sectionId, L10n.archivedStickersDescription, .textTopItem)) + + var installedIds = Set() + if let view = installedView.views[.itemCollectionIds(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])] as? ItemCollectionIdsView, let ids = view.idsByNamespace[Namespaces.ItemCollection.CloudStickerPacks] { + installedIds = ids + } + + let packs = packs.filter { item in + return !installedIds.contains(item.info.id) + } + + var index: Int32 = 0 + for item in packs { + entries.append(.pack(sectionId: sectionId, index, item.info, item.topItems.first, item.info.count, !state.removingPackIds.contains(item.info.id), ItemListStickerPackItemEditing(editable: true, editing: state.editing), bestGeneralViewType(packs, for: item))) index += 1 } + entries.append(.section(sectionId: sectionId)) + sectionId += 1 } + } else { + entries.append(.loading(true)) } return entries @@ -238,35 +201,57 @@ private func prepareTransition(left:[AppearanceWrapperEntry Void + init(_ context: AccountContext, archived: [ArchivedStickerPackItem]?, updatedPacks: @escaping ([ArchivedStickerPackItem]?) -> Void) { + self.archived = archived + self.updatedPacks = updatedPacks + super.init(context) + } + + deinit { + disposable.dispose() + } override func viewDidLoad() { super.viewDidLoad() - let account = self.account + let context = self.context let statePromise = ValuePromise(ArchivedStickerPacksControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: ArchivedStickerPacksControllerState()) let updateState: ((ArchivedStickerPacksControllerState) -> ArchivedStickerPacksControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } - let actionsDisposable = DisposableSet() + let updatedPacks = self.updatedPacks + let actionsDisposable = DisposableSet() + let resolveDisposable = MetaDisposable() actionsDisposable.add(resolveDisposable) let removePackDisposables = DisposableDict() actionsDisposable.add(removePackDisposables) + + let stickerPacks = Promise<[ArchivedStickerPackItem]?>() - stickerPacks.set(.single(nil) |> then(archivedStickerPacks(account: account) |> map { Optional($0) })) + stickerPacks.set(.single(archived) |> then(context.engine.stickers.archivedStickerPacks() |> map { Optional($0) })) let installedStickerPacks = Promise() - installedStickerPacks.set(account.postbox.combinedView(keys: [.itemCollectionIds(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])])) + installedStickerPacks.set(context.account.postbox.combinedView(keys: [.itemCollectionIds(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])])) + + + actionsDisposable.add(stickerPacks.get().start(next: { packs in + updatedPacks(packs) + })) - let arguments = ArchivedStickerPacksControllerArguments(account: account, openStickerPack: { info in - showModal(with: StickersPackPreviewModalController(account, peerId: nil, reference: .name(info.shortName)), for: mainWindow) + + let arguments = ArchivedStickerPacksControllerArguments(context: context, openStickerPack: { info in + showModal(with: StickerPackPreviewModalController(context, peerId: nil, reference: .name(info.shortName)), for: mainWindow) }, removePack: { info in - confirm(for: mainWindow, with: appName, and: tr(.chatConfirmActionUndonable), successHandler: { _ in + confirm(for: context.window, information: tr(L10n.chatConfirmActionUndonable), successHandler: { _ in var remove = false updateState { state in var removingPackIds = state.removingPackIds @@ -295,7 +280,8 @@ class ArchivedStickerPacksController: TableViewController { return .complete() } - removePackDisposables.set((removeArchivedStickerPack(account: account, info: info) |> then(applyPacks) |> deliverOnMainQueue).start(completed: { + + removePackDisposables.set((context.engine.stickers.removeArchivedStickerPack(info: info) |> then(applyPacks) |> deliverOnMainQueue).start(completed: { updateState { state in var removingPackIds = state.removingPackIds removingPackIds.remove(info.id) @@ -309,15 +295,21 @@ class ArchivedStickerPacksController: TableViewController { let previousEntries:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) let initialSize = atomicSize - genericView.merge(with: combineLatest(statePromise.get(), stickerPacks.get(), installedStickerPacks.get(), appearanceSignal) |> deliverOnMainQueue + + let signal = combineLatest(queue: prepareQueue, statePromise.get(), stickerPacks.get(), installedStickerPacks.get(), appearanceSignal) |> map { state, packs, installedView, appearance -> TableUpdateTransition in let entries = archivedStickerPacksControllerEntries(state: state, packs: packs, installedView: installedView).map {AppearanceWrapperEntry(entry: $0, appearance: appearance)} return prepareTransition(left: previousEntries.swap(entries), right: entries, initialSize: initialSize.modify({$0}), arguments: arguments) - } |> afterDisposed { + } |> afterDisposed { actionsDisposable.dispose() - }) + } |> deliverOnMainQueue + + disposable.set(signal.start(next: { [weak self] transition in + self?.genericView.merge(with: transition) + self?.readyOnce() + })) + - readyOnce() } } diff --git a/Telegram-Mac/ArchiverContext.swift b/Telegram-Mac/ArchiverContext.swift new file mode 100644 index 0000000000..5379524a1f --- /dev/null +++ b/Telegram-Mac/ArchiverContext.swift @@ -0,0 +1,199 @@ +// +// ArchiverContext.swift +// Telegram +// +// Created by Mikhail Filimonov on 23/10/2018. +// Copyright © 2018 Telegram. All rights reserved. +// +import Zip +import SwiftSignalKit + +enum ArchiveStatus : Equatable { + case none + case waiting + case done(URL) + case fail(ZipError) + case progress(Double) +} +enum ArchiveSource : Hashable { + static func == (lhs: ArchiveSource, rhs: ArchiveSource) -> Bool { + switch lhs { + case let .resource(lhsResource): + if case let .resource(rhsResource) = rhs { + return lhsResource.isEqual(to: rhsResource) + } else { + return false + } + } + } + + var contents:[URL] { + switch self { + case let .resource(resource): + if resource.path.contains("tg_temp_archive_") { + let files = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: resource.path), includingPropertiesForKeys: nil, options: FileManager.DirectoryEnumerationOptions.skipsHiddenFiles) + return files ?? [URL(fileURLWithPath: resource.path)] + } + return [URL(fileURLWithPath: resource.path)] + } + } + + + var hashValue: Int { + switch self { + case let .resource(resource): + return resource.id.hashValue + } + } + + var destinationURL: URL { + return URL(fileURLWithPath: NSTemporaryDirectory() + "tarchive-\(self.uniqueId).zip") + } + + + var uniqueId: Int64 { + switch self { + case .resource(let resource): + return resource.randomId + } + } + + case resource(LocalFileArchiveMediaResource) +} + +private final class Archiver { + private let status: ValuePromise = ValuePromise(.waiting, ignoreRepeated: true) + var statusSignal:Signal { + return status.get() + } + let destination: URL + private let source: ArchiveSource + private let queue: Queue + init(source : ArchiveSource, queue: Queue) { + self.queue = queue + self.source = source + self.destination = source.destinationURL + } + + func start(cancelToken:@escaping()->Bool) { + let destination = self.destination + let source = self.source + queue.async { [weak status] in + guard let status = status else {return} + let contents = source.contents + if !contents.isEmpty { + do { + try Zip.zipFiles(paths: contents, zipFilePath: destination, password: nil, compression: ZipCompression.BestCompression, progress: { progress in + status.set(.progress(progress)) + }, cancel: cancelToken) + status.set(.done(destination)) + } catch { + if let error = error as? ZipError { + status.set(.fail(error)) + } + } + } + } + + } + +} +// добавить отмену архивирования если разлонигиваемся +private final class ArchiveStatusContext { + var status: ArchiveStatus = .none + let subscribers = Bag<(ArchiveStatus) -> Void>() +} +class ArchiverContext { + var statuses:[ArchiveSource : ArchiveStatus] = [:] + + private let queue = Queue(name: "ArchiverContext") + private var contexts: [ArchiveSource: Archiver] = [:] + private let archiveQueue: Queue = Queue.concurrentDefaultQueue() + private var statusContexts: [ArchiveSource: ArchiveStatusContext] = [:] + private var statusesDisposable:[ArchiveSource : Disposable] = [:] + private var cancelledTokens:[ArchiveSource : Any] = [:] + init() { + } + + deinit { + self.queue.sync { + self.contexts.removeAll() + for status in statusesDisposable { + status.value.dispose() + } + } + } + + func remove(_ source: ArchiveSource) { + queue.async { + self.contexts.removeValue(forKey: source) + self.statusesDisposable[source]?.dispose() + self.statuses.removeValue(forKey: source) + self.cancelledTokens[source] = true + } + } + + func archive(_ source: ArchiveSource, startIfNeeded: Bool = false) -> Signal { + let queue = self.queue + return Signal { [weak self] subscriber in + guard let `self` = self else { return EmptyDisposable } + if self.statusContexts[source] == nil { + self.statusContexts[source] = ArchiveStatusContext() + } + + let statusContext = self.statusContexts[source]! + + let index = statusContext.subscribers.add({ status in + subscriber.putNext(status) + }) + + if let _ = self.contexts[source] { + if let statusContext = self.statusContexts[source] { + for subscriber in statusContext.subscribers.copyItems() { + subscriber(statusContext.status) + } + } + } else { + if startIfNeeded { + let archiver = Archiver(source: source, queue: self.archiveQueue) + self.contexts[source] = archiver + self.statusesDisposable[source] = (archiver.statusSignal |> deliverOn(queue)).start(next: { status in + statusContext.status = status + for subscriber in statusContext.subscribers.copyItems() { + subscriber(statusContext.status) + } + }, completed: { + subscriber.putCompletion() + }) + + archiver.start(cancelToken: { + var cancelled: Bool = false + queue.sync { + cancelled = self.cancelledTokens[source] != nil + self.cancelledTokens.removeValue(forKey: source) + } + return cancelled + }) + } else { + for subscriber in statusContext.subscribers.copyItems() { + subscriber(statusContext.status) + } + } + + } + + + return ActionDisposable { + self.queue.async { + if let current = self.statusContexts[source] { + current.subscribers.remove(index) + } + } + } + } |> runOn(queue) + } + +} + + + diff --git a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Contents.json b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Contents.json index ae1eb5f152..11f98d214a 100644 --- a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -3,61 +3,61 @@ { "size" : "16x16", "idiom" : "mac", - "filename" : "icon2_16.png", + "filename" : "Logo_16.png", "scale" : "1x" }, { "size" : "16x16", "idiom" : "mac", - "filename" : "icon2_16@2x.png", + "filename" : "Logo_32.png", "scale" : "2x" }, { "size" : "32x32", "idiom" : "mac", - "filename" : "icon2_32.png", + "filename" : "Logo_32-1.png", "scale" : "1x" }, { "size" : "32x32", "idiom" : "mac", - "filename" : "icon2_32@2x.png", + "filename" : "Logo_64.png", "scale" : "2x" }, { "size" : "128x128", "idiom" : "mac", - "filename" : "icon2_128.png", + "filename" : "Logo_128.png", "scale" : "1x" }, { "size" : "128x128", "idiom" : "mac", - "filename" : "icon2_128@2x.png", + "filename" : "Logo_256.png", "scale" : "2x" }, { "size" : "256x256", "idiom" : "mac", - "filename" : "icon2_256.png", + "filename" : "Logo_256-1.png", "scale" : "1x" }, { "size" : "256x256", "idiom" : "mac", - "filename" : "icon2_256@2x.png", + "filename" : "Logo_512.png", "scale" : "2x" }, { "size" : "512x512", "idiom" : "mac", - "filename" : "icon2_512.png", + "filename" : "Logo_512-1.png", "scale" : "1x" }, { "size" : "512x512", "idiom" : "mac", - "filename" : "icon2_512x512@2x.png", + "filename" : "Logo_1024.png", "scale" : "2x" } ], diff --git a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_1024.png b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_1024.png new file mode 100644 index 0000000000..6b3e91670b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_1024.png differ diff --git a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_128.png b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_128.png new file mode 100644 index 0000000000..17740b8de3 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_128.png differ diff --git a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_16.png b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_16.png new file mode 100644 index 0000000000..db6ff024de Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_16.png differ diff --git a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_256-1.png b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_256-1.png new file mode 100644 index 0000000000..a351b5b0ce Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_256-1.png differ diff --git a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_256.png b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_256.png new file mode 100644 index 0000000000..a351b5b0ce Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_256.png differ diff --git a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_32-1.png b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_32-1.png new file mode 100644 index 0000000000..d6b5bcf0d9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_32-1.png differ diff --git a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_32.png b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_32.png new file mode 100644 index 0000000000..d6b5bcf0d9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_32.png differ diff --git a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_512-1.png b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_512-1.png new file mode 100644 index 0000000000..1de65d50f9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_512-1.png differ diff --git a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_512.png b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_512.png new file mode 100644 index 0000000000..1de65d50f9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_512.png differ diff --git a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_64.png b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_64.png new file mode 100644 index 0000000000..f9b6024e7e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/Logo_64.png differ diff --git a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_16.png b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_16.png deleted file mode 100644 index c1684ec3d3..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_16.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_16@2x.png b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_16@2x.png deleted file mode 100644 index 5039300522..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_16@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_256.png b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_256.png deleted file mode 100644 index 9ca9baf790..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_256.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_256@2x.png b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_256@2x.png deleted file mode 100644 index 11e91102bb..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_256@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_32.png b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_32.png deleted file mode 100644 index 5039300522..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_32.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_32@2x.png b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_32@2x.png deleted file mode 100644 index cd940b583d..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_32@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_512.png b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_512.png deleted file mode 100644 index b072e4f33c..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_512.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_512x512@2x.png b/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_512x512@2x.png deleted file mode 100644 index 38ac220d20..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_512x512@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Contents.json b/Telegram-Mac/Assets.xcassets/Contents.json index da4a164c91..73c00596a7 100644 --- a/Telegram-Mac/Assets.xcassets/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Telegram-Mac/Assets.xcassets/DiscussDarkBluePreview.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/DiscussDarkBluePreview.imageset/Contents.json new file mode 100644 index 0000000000..3655d21fb4 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/DiscussDarkBluePreview.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "DiscussDarkBluePreview.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "DiscussDarkBluePreview@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/DiscussDarkBluePreview.imageset/DiscussDarkBluePreview.png b/Telegram-Mac/Assets.xcassets/DiscussDarkBluePreview.imageset/DiscussDarkBluePreview.png new file mode 100644 index 0000000000..b81f5d544d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/DiscussDarkBluePreview.imageset/DiscussDarkBluePreview.png differ diff --git a/Telegram-Mac/Assets.xcassets/DiscussDarkBluePreview.imageset/DiscussDarkBluePreview@2x.png b/Telegram-Mac/Assets.xcassets/DiscussDarkBluePreview.imageset/DiscussDarkBluePreview@2x.png new file mode 100644 index 0000000000..f7c4f60b16 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/DiscussDarkBluePreview.imageset/DiscussDarkBluePreview@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/DiscussDarkPreview.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/DiscussDarkPreview.imageset/Contents.json new file mode 100644 index 0000000000..6709d95eb3 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/DiscussDarkPreview.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "DiscussDarkPreview.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "DiscussDarkPreview@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/DiscussDarkPreview.imageset/DiscussDarkPreview.png b/Telegram-Mac/Assets.xcassets/DiscussDarkPreview.imageset/DiscussDarkPreview.png new file mode 100644 index 0000000000..7238c06b53 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/DiscussDarkPreview.imageset/DiscussDarkPreview.png differ diff --git a/Telegram-Mac/Assets.xcassets/DiscussDarkPreview.imageset/DiscussDarkPreview@2x.png b/Telegram-Mac/Assets.xcassets/DiscussDarkPreview.imageset/DiscussDarkPreview@2x.png new file mode 100644 index 0000000000..581c987955 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/DiscussDarkPreview.imageset/DiscussDarkPreview@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/DiscussDayPreview.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/DiscussDayPreview.imageset/Contents.json new file mode 100644 index 0000000000..031e41e0b0 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/DiscussDayPreview.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "DiscussDayPreview.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "DiscussDayPreview@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/DiscussDayPreview.imageset/DiscussDayPreview.png b/Telegram-Mac/Assets.xcassets/DiscussDayPreview.imageset/DiscussDayPreview.png new file mode 100644 index 0000000000..82279aa518 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/DiscussDayPreview.imageset/DiscussDayPreview.png differ diff --git a/Telegram-Mac/Assets.xcassets/DiscussDayPreview.imageset/DiscussDayPreview@2x.png b/Telegram-Mac/Assets.xcassets/DiscussDayPreview.imageset/DiscussDayPreview@2x.png new file mode 100644 index 0000000000..f945f76e5a Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/DiscussDayPreview.imageset/DiscussDayPreview@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_AddFeaturedStickers.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_AddFeaturedStickers.imageset/Contents.json new file mode 100644 index 0000000000..e8ff6f4522 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_AddFeaturedStickers.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_addstickers.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_addstickers@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_AddFeaturedStickers.imageset/ic_addstickers.png b/Telegram-Mac/Assets.xcassets/Icon_AddFeaturedStickers.imageset/ic_addstickers.png new file mode 100644 index 0000000000..0633484d16 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_AddFeaturedStickers.imageset/ic_addstickers.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_AddFeaturedStickers.imageset/ic_addstickers@2x.png b/Telegram-Mac/Assets.xcassets/Icon_AddFeaturedStickers.imageset/ic_addstickers@2x.png new file mode 100644 index 0000000000..37fe08e93e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_AddFeaturedStickers.imageset/ic_addstickers@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_AlertCheckBoxMark.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_AlertCheckBoxMark.imageset/Contents.json new file mode 100644 index 0000000000..e025fa4149 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_AlertCheckBoxMark.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "check@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "check@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_AlertCheckBoxMark.imageset/check@1x.png b/Telegram-Mac/Assets.xcassets/Icon_AlertCheckBoxMark.imageset/check@1x.png new file mode 100644 index 0000000000..c226940609 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_AlertCheckBoxMark.imageset/check@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_AlertCheckBoxMark.imageset/check@2x.png b/Telegram-Mac/Assets.xcassets/Icon_AlertCheckBoxMark.imageset/check@2x.png new file mode 100644 index 0000000000..3a8fd97eb0 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_AlertCheckBoxMark.imageset/check@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_AppUpdate.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_AppUpdate.imageset/Contents.json new file mode 100644 index 0000000000..42db5e5372 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_AppUpdate.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Update.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Update@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_AppUpdate.imageset/Update.png b/Telegram-Mac/Assets.xcassets/Icon_AppUpdate.imageset/Update.png new file mode 100644 index 0000000000..79bbd5cb48 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_AppUpdate.imageset/Update.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_AppUpdate.imageset/Update@2x.png b/Telegram-Mac/Assets.xcassets/Icon_AppUpdate.imageset/Update@2x.png new file mode 100644 index 0000000000..b63f16afef Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_AppUpdate.imageset/Update@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_AppearanceAddTheme.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_AppearanceAddTheme.imageset/Contents.json new file mode 100644 index 0000000000..303203107d --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_AppearanceAddTheme.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ThemeMac.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ThemeMac@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_AppearanceAddTheme.imageset/ThemeMac.png b/Telegram-Mac/Assets.xcassets/Icon_AppearanceAddTheme.imageset/ThemeMac.png new file mode 100644 index 0000000000..e7a2283314 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_AppearanceAddTheme.imageset/ThemeMac.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_AppearanceAddTheme.imageset/ThemeMac@2x.png b/Telegram-Mac/Assets.xcassets/Icon_AppearanceAddTheme.imageset/ThemeMac@2x.png new file mode 100644 index 0000000000..707dbebece Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_AppearanceAddTheme.imageset/ThemeMac@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_AppearanceSettings.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_AppearanceSettings.imageset/Contents.json new file mode 100644 index 0000000000..114988ec9d --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_AppearanceSettings.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_theme@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_theme@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_AppearanceSettings.imageset/ic_theme@1x.png b/Telegram-Mac/Assets.xcassets/Icon_AppearanceSettings.imageset/ic_theme@1x.png new file mode 100644 index 0000000000..ae901ccbbf Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_AppearanceSettings.imageset/ic_theme@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_AppearanceSettings.imageset/ic_theme@2x.png b/Telegram-Mac/Assets.xcassets/Icon_AppearanceSettings.imageset/ic_theme@2x.png new file mode 100644 index 0000000000..4f6ed792a0 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_AppearanceSettings.imageset/ic_theme@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ArchiveAvatar.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ArchiveAvatar.imageset/Contents.json new file mode 100644 index 0000000000..36797f720f --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ArchiveAvatar.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "archiveavatar.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "archiveavatar@2x (1).png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ArchiveAvatar.imageset/archiveavatar.png b/Telegram-Mac/Assets.xcassets/Icon_ArchiveAvatar.imageset/archiveavatar.png new file mode 100644 index 0000000000..c7144e5714 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ArchiveAvatar.imageset/archiveavatar.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ArchiveAvatar.imageset/archiveavatar@2x (1).png b/Telegram-Mac/Assets.xcassets/Icon_ArchiveAvatar.imageset/archiveavatar@2x (1).png new file mode 100644 index 0000000000..aef04b1cbe Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ArchiveAvatar.imageset/archiveavatar@2x (1).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_AttachPoll.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_AttachPoll.imageset/Contents.json new file mode 100644 index 0000000000..bc0e0d9bd6 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_AttachPoll.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_poll.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_poll@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_AttachPoll.imageset/ic_poll.png b/Telegram-Mac/Assets.xcassets/Icon_AttachPoll.imageset/ic_poll.png new file mode 100644 index 0000000000..2205b5d55e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_AttachPoll.imageset/ic_poll.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_AttachPoll.imageset/ic_poll@2x.png b/Telegram-Mac/Assets.xcassets/Icon_AttachPoll.imageset/ic_poll@2x.png new file mode 100644 index 0000000000..0547bff5c9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_AttachPoll.imageset/ic_poll@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_AvatarScheduled.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_AvatarScheduled.imageset/Contents.json new file mode 100644 index 0000000000..3b16380d65 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_AvatarScheduled.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "avatar_scheduled.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "avatar_scheduled@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_AvatarScheduled.imageset/avatar_scheduled.png b/Telegram-Mac/Assets.xcassets/Icon_AvatarScheduled.imageset/avatar_scheduled.png new file mode 100644 index 0000000000..07450f288b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_AvatarScheduled.imageset/avatar_scheduled.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_AvatarScheduled.imageset/avatar_scheduled@2x.png b/Telegram-Mac/Assets.xcassets/Icon_AvatarScheduled.imageset/avatar_scheduled@2x.png new file mode 100644 index 0000000000..21c9d2cf36 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_AvatarScheduled.imageset/avatar_scheduled@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Calendar.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Calendar.imageset/Contents.json index 10268ace8e..6ff3ad9b7a 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_Calendar.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_Calendar.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_datesearch@1x.png", + "filename" : "ic_calendar.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ic_datesearch@2x.png", + "filename" : "ic_calendar@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_Calendar.imageset/ic_calendar.png b/Telegram-Mac/Assets.xcassets/Icon_Calendar.imageset/ic_calendar.png new file mode 100644 index 0000000000..293285142a Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Calendar.imageset/ic_calendar.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Calendar.imageset/ic_calendar@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Calendar.imageset/ic_calendar@2x.png new file mode 100644 index 0000000000..9a034eb340 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Calendar.imageset/ic_calendar@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Calendar.imageset/ic_datesearch@1x.png b/Telegram-Mac/Assets.xcassets/Icon_Calendar.imageset/ic_datesearch@1x.png deleted file mode 100644 index d9a74aabb6..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_Calendar.imageset/ic_datesearch@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Calendar.imageset/ic_datesearch@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Calendar.imageset/ic_datesearch@2x.png deleted file mode 100644 index 6dee5b3c28..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_Calendar.imageset/ic_datesearch@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallAccept_Window.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_CallAccept_Window.imageset/Contents.json index cdd61856dd..a6e89c220d 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_CallAccept_Window.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_CallAccept_Window.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "accept.png", + "filename" : "ic_calls_accept.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "accept@2x.png", + "filename" : "ic_calls_accept@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallAccept_Window.imageset/accept.png b/Telegram-Mac/Assets.xcassets/Icon_CallAccept_Window.imageset/accept.png deleted file mode 100644 index be0b762ff3..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_CallAccept_Window.imageset/accept.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallAccept_Window.imageset/accept@2x.png b/Telegram-Mac/Assets.xcassets/Icon_CallAccept_Window.imageset/accept@2x.png deleted file mode 100644 index 0e4dbcd910..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_CallAccept_Window.imageset/accept@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallAccept_Window.imageset/ic_calls_accept.png b/Telegram-Mac/Assets.xcassets/Icon_CallAccept_Window.imageset/ic_calls_accept.png new file mode 100644 index 0000000000..91d8553877 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_CallAccept_Window.imageset/ic_calls_accept.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallAccept_Window.imageset/ic_calls_accept@2x.png b/Telegram-Mac/Assets.xcassets/Icon_CallAccept_Window.imageset/ic_calls_accept@2x.png new file mode 100644 index 0000000000..23c5fa8966 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_CallAccept_Window.imageset/ic_calls_accept@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallCancelIcon.imageset/CallCancelIcon@2x.png b/Telegram-Mac/Assets.xcassets/Icon_CallCancelIcon.imageset/CallCancelIcon@2x.png deleted file mode 100644 index 1114479167..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_CallCancelIcon.imageset/CallCancelIcon@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallCancelIcon.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_CallCancelIcon.imageset/Contents.json index 3942fb5ae7..b7fe7a1958 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_CallCancelIcon.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_CallCancelIcon.imageset/Contents.json @@ -2,11 +2,12 @@ "images" : [ { "idiom" : "universal", + "filename" : "ic_call_cancel.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "CallCancelIcon@2x.png", + "filename" : "ic_call_cancel@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallCancelIcon.imageset/ic_call_cancel.png b/Telegram-Mac/Assets.xcassets/Icon_CallCancelIcon.imageset/ic_call_cancel.png new file mode 100644 index 0000000000..a0c12d8297 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_CallCancelIcon.imageset/ic_call_cancel.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallCancelIcon.imageset/ic_call_cancel@2x.png b/Telegram-Mac/Assets.xcassets/Icon_CallCancelIcon.imageset/ic_call_cancel@2x.png new file mode 100644 index 0000000000..791be838bd Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_CallCancelIcon.imageset/ic_call_cancel@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallDecline_Window.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_CallDecline_Window.imageset/Contents.json index 51b62bc611..6fae8b0d1e 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_CallDecline_Window.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_CallDecline_Window.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "decline.png", + "filename" : "ic_calls_decline.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "decline@2x.png", + "filename" : "ic_calls_decline@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallDecline_Window.imageset/decline.png b/Telegram-Mac/Assets.xcassets/Icon_CallDecline_Window.imageset/decline.png deleted file mode 100644 index 327c255f00..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_CallDecline_Window.imageset/decline.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallDecline_Window.imageset/decline@2x.png b/Telegram-Mac/Assets.xcassets/Icon_CallDecline_Window.imageset/decline@2x.png deleted file mode 100644 index 0b38588eaa..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_CallDecline_Window.imageset/decline@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallDecline_Window.imageset/ic_calls_decline.png b/Telegram-Mac/Assets.xcassets/Icon_CallDecline_Window.imageset/ic_calls_decline.png new file mode 100644 index 0000000000..6ffa826056 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_CallDecline_Window.imageset/ic_calls_decline.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallDecline_Window.imageset/ic_calls_decline@2x.png b/Telegram-Mac/Assets.xcassets/Icon_CallDecline_Window.imageset/ic_calls_decline@2x.png new file mode 100644 index 0000000000..a2e6e898b4 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_CallDecline_Window.imageset/ic_calls_decline@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallDeviceSettings.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_CallDeviceSettings.imageset/Contents.json index 7de62563be..5dbe5589ec 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_CallDeviceSettings.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_CallDeviceSettings.imageset/Contents.json @@ -1,13 +1,13 @@ { "images" : [ { + "filename" : "Settings-1.png", "idiom" : "universal", - "filename" : "settings (1).png", "scale" : "1x" }, { + "filename" : "Settings@2x.png", "idiom" : "universal", - "filename" : "settings.png", "scale" : "2x" }, { @@ -16,7 +16,7 @@ } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallDeviceSettings.imageset/Settings-1.png b/Telegram-Mac/Assets.xcassets/Icon_CallDeviceSettings.imageset/Settings-1.png new file mode 100644 index 0000000000..8865053171 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_CallDeviceSettings.imageset/Settings-1.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallDeviceSettings.imageset/Settings@2x.png b/Telegram-Mac/Assets.xcassets/Icon_CallDeviceSettings.imageset/Settings@2x.png new file mode 100644 index 0000000000..41788331b7 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_CallDeviceSettings.imageset/Settings@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallDeviceSettings.imageset/settings (1).png b/Telegram-Mac/Assets.xcassets/Icon_CallDeviceSettings.imageset/settings (1).png deleted file mode 100644 index 94b29d432c..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_CallDeviceSettings.imageset/settings (1).png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallDeviceSettings.imageset/settings.png b/Telegram-Mac/Assets.xcassets/Icon_CallDeviceSettings.imageset/settings.png deleted file mode 100644 index e266392616..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_CallDeviceSettings.imageset/settings.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallMuted_Window.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_CallMuted_Window.imageset/Contents.json index 0e7d976110..c0e6c328b0 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_CallMuted_Window.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_CallMuted_Window.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_mic2.png", + "filename" : "ic_calls_mute.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ic_mic2@2x.png", + "filename" : "ic_calls_mute@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallMuted_Window.imageset/ic_calls_mute.png b/Telegram-Mac/Assets.xcassets/Icon_CallMuted_Window.imageset/ic_calls_mute.png new file mode 100644 index 0000000000..f45c6ecea5 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_CallMuted_Window.imageset/ic_calls_mute.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallMuted_Window.imageset/ic_calls_mute@2x.png b/Telegram-Mac/Assets.xcassets/Icon_CallMuted_Window.imageset/ic_calls_mute@2x.png new file mode 100644 index 0000000000..0ef80350e1 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_CallMuted_Window.imageset/ic_calls_mute@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallMuted_Window.imageset/ic_mic2.png b/Telegram-Mac/Assets.xcassets/Icon_CallMuted_Window.imageset/ic_mic2.png deleted file mode 100644 index 97d9fa79da..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_CallMuted_Window.imageset/ic_mic2.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallMuted_Window.imageset/ic_mic2@2x.png b/Telegram-Mac/Assets.xcassets/Icon_CallMuted_Window.imageset/ic_mic2@2x.png deleted file mode 100644 index c592415474..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_CallMuted_Window.imageset/ic_mic2@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallScreenSettings.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_CallScreenSettings.imageset/Contents.json new file mode 100644 index 0000000000..28fda07672 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_CallScreenSettings.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "callsettings.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "callsettings@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallScreenSettings.imageset/callsettings.png b/Telegram-Mac/Assets.xcassets/Icon_CallScreenSettings.imageset/callsettings.png new file mode 100644 index 0000000000..d1df93e541 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_CallScreenSettings.imageset/callsettings.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallScreenSettings.imageset/callsettings@2x.png b/Telegram-Mac/Assets.xcassets/Icon_CallScreenSettings.imageset/callsettings@2x.png new file mode 100644 index 0000000000..6027c9d8e2 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_CallScreenSettings.imageset/callsettings@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallScreenSharing.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_CallScreenSharing.imageset/Contents.json new file mode 100644 index 0000000000..0d570901ed --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_CallScreenSharing.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "sharescreen.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "sharescreen@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallScreenSharing.imageset/sharescreen.png b/Telegram-Mac/Assets.xcassets/Icon_CallScreenSharing.imageset/sharescreen.png new file mode 100644 index 0000000000..ec5cad26b9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_CallScreenSharing.imageset/sharescreen.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallScreenSharing.imageset/sharescreen@2x.png b/Telegram-Mac/Assets.xcassets/Icon_CallScreenSharing.imageset/sharescreen@2x.png new file mode 100644 index 0000000000..b87448b8e0 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_CallScreenSharing.imageset/sharescreen@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallVideo_Window.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_CallVideo_Window.imageset/Contents.json new file mode 100644 index 0000000000..8a6691ef85 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_CallVideo_Window.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_calls_video.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_calls_video@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallVideo_Window.imageset/ic_calls_video.png b/Telegram-Mac/Assets.xcassets/Icon_CallVideo_Window.imageset/ic_calls_video.png new file mode 100644 index 0000000000..9d0a5696f1 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_CallVideo_Window.imageset/ic_calls_video.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CallVideo_Window.imageset/ic_calls_video@2x.png b/Telegram-Mac/Assets.xcassets/Icon_CallVideo_Window.imageset/ic_calls_video@2x.png new file mode 100644 index 0000000000..7924bf8205 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_CallVideo_Window.imageset/ic_calls_video@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Call_BatteryLow.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Call_BatteryLow.imageset/Contents.json new file mode 100644 index 0000000000..2854c5c6cd --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Call_BatteryLow.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_call_batteryislow.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_call_batteryislow@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Call_BatteryLow.imageset/ic_call_batteryislow.png b/Telegram-Mac/Assets.xcassets/Icon_Call_BatteryLow.imageset/ic_call_batteryislow.png new file mode 100644 index 0000000000..ef220e2a7f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Call_BatteryLow.imageset/ic_call_batteryislow.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Call_BatteryLow.imageset/ic_call_batteryislow@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Call_BatteryLow.imageset/ic_call_batteryislow@2x.png new file mode 100644 index 0000000000..da1982eaa3 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Call_BatteryLow.imageset/ic_call_batteryislow@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Call_CameraOff.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Call_CameraOff.imageset/Contents.json new file mode 100644 index 0000000000..89799f2c20 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Call_CameraOff.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_call_cameraoff.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_call_cameraoff@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Call_CameraOff.imageset/ic_call_cameraoff.png b/Telegram-Mac/Assets.xcassets/Icon_Call_CameraOff.imageset/ic_call_cameraoff.png new file mode 100644 index 0000000000..9a6ea66d13 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Call_CameraOff.imageset/ic_call_cameraoff.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Call_CameraOff.imageset/ic_call_cameraoff@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Call_CameraOff.imageset/ic_call_cameraoff@2x.png new file mode 100644 index 0000000000..6162645ea1 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Call_CameraOff.imageset/ic_call_cameraoff@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Call_MicroOff.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Call_MicroOff.imageset/Contents.json new file mode 100644 index 0000000000..78afc87766 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Call_MicroOff.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_call_microphoneoff.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_call_microphoneoff@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Call_MicroOff.imageset/ic_call_microphoneoff.png b/Telegram-Mac/Assets.xcassets/Icon_Call_MicroOff.imageset/ic_call_microphoneoff.png new file mode 100644 index 0000000000..b22951ae89 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Call_MicroOff.imageset/ic_call_microphoneoff.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Call_MicroOff.imageset/ic_call_microphoneoff@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Call_MicroOff.imageset/ic_call_microphoneoff@2x.png new file mode 100644 index 0000000000..48abd6a53d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Call_MicroOff.imageset/ic_call_microphoneoff@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChangeColors.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChangeColors.imageset/Contents.json new file mode 100644 index 0000000000..1377e1fe49 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChangeColors.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "colors_24.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "colors_24@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChangeColors.imageset/colors_24.png b/Telegram-Mac/Assets.xcassets/Icon_ChangeColors.imageset/colors_24.png new file mode 100644 index 0000000000..5de520434e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChangeColors.imageset/colors_24.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChangeColors.imageset/colors_24@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChangeColors.imageset/colors_24@2x.png new file mode 100644 index 0000000000..436b4c097c Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChangeColors.imageset/colors_24@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChannelComments.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChannelComments.imageset/Contents.json new file mode 100644 index 0000000000..c270e366c1 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChannelComments.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_msg_comments.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_msg_comments@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChannelComments.imageset/ic_msg_comments.png b/Telegram-Mac/Assets.xcassets/Icon_ChannelComments.imageset/ic_msg_comments.png new file mode 100644 index 0000000000..0f4dee6c51 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChannelComments.imageset/ic_msg_comments.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChannelComments.imageset/ic_msg_comments@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChannelComments.imageset/ic_msg_comments@2x.png new file mode 100644 index 0000000000..956bba5056 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChannelComments.imageset/ic_msg_comments@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChannelComments_Bubble.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChannelComments_Bubble.imageset/Contents.json new file mode 100644 index 0000000000..9118603f99 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChannelComments_Bubble.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "bubble.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "bubble@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChannelComments_Bubble.imageset/bubble.png b/Telegram-Mac/Assets.xcassets/Icon_ChannelComments_Bubble.imageset/bubble.png new file mode 100644 index 0000000000..1c55ad7340 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChannelComments_Bubble.imageset/bubble.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChannelComments_Bubble.imageset/bubble@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChannelComments_Bubble.imageset/bubble@2x.png new file mode 100644 index 0000000000..7c70e892f7 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChannelComments_Bubble.imageset/bubble@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChannelComments_Next.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChannelComments_Next.imageset/Contents.json new file mode 100644 index 0000000000..c8fb82dc93 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChannelComments_Next.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "open.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "open@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChannelComments_Next.imageset/open.png b/Telegram-Mac/Assets.xcassets/Icon_ChannelComments_Next.imageset/open.png new file mode 100644 index 0000000000..828fb9fb2a Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChannelComments_Next.imageset/open.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChannelComments_Next.imageset/open@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChannelComments_Next.imageset/open@2x.png new file mode 100644 index 0000000000..0319764f9b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChannelComments_Next.imageset/open@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChannelComments_Overlay.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChannelComments_Overlay.imageset/Contents.json new file mode 100644 index 0000000000..15317d8d2e --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChannelComments_Overlay.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "commentsstickers.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "commentsstickers@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChannelComments_Overlay.imageset/commentsstickers.png b/Telegram-Mac/Assets.xcassets/Icon_ChannelComments_Overlay.imageset/commentsstickers.png new file mode 100644 index 0000000000..9c980ebdc3 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChannelComments_Overlay.imageset/commentsstickers.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChannelComments_Overlay.imageset/commentsstickers@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChannelComments_Overlay.imageset/commentsstickers@2x.png new file mode 100644 index 0000000000..39ff5e4d26 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChannelComments_Overlay.imageset/commentsstickers@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChannelPromoInfo.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChannelPromoInfo.imageset/Contents.json new file mode 100644 index 0000000000..d6f0cec6aa --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChannelPromoInfo.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_help.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_help@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChannelPromoInfo.imageset/ic_help.png b/Telegram-Mac/Assets.xcassets/Icon_ChannelPromoInfo.imageset/ic_help.png new file mode 100644 index 0000000000..c952c47ebe Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChannelPromoInfo.imageset/ic_help.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChannelPromoInfo.imageset/ic_help@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChannelPromoInfo.imageset/ic_help@2x.png new file mode 100644 index 0000000000..206743b52b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChannelPromoInfo.imageset/ic_help@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChannelShare.imageset/ChannelShare.png b/Telegram-Mac/Assets.xcassets/Icon_ChannelShare.imageset/ChannelShare.png deleted file mode 100644 index 92d6b30246..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_ChannelShare.imageset/ChannelShare.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChannelShare.imageset/ChannelShare@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChannelShare.imageset/ChannelShare@2x.png deleted file mode 100644 index 7c08269920..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_ChannelShare.imageset/ChannelShare@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChannelShare.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChannelShare.imageset/Contents.json index 8a6b520186..445cb8e64f 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_ChannelShare.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_ChannelShare.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "ChannelShare.png", + "filename" : "ic_forward.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ChannelShare@2x.png", + "filename" : "ic_forward@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChannelShare.imageset/ic_forward.png b/Telegram-Mac/Assets.xcassets/Icon_ChannelShare.imageset/ic_forward.png new file mode 100644 index 0000000000..b78eaccdd5 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChannelShare.imageset/ic_forward.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChannelShare.imageset/ic_forward@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChannelShare.imageset/ic_forward@2x.png new file mode 100644 index 0000000000..de19303201 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChannelShare.imageset/ic_forward@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatActionScheduled.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatActionScheduled.imageset/Contents.json new file mode 100644 index 0000000000..f605e52e0b --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatActionScheduled.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "scheduledmenu.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "scheduledmenu@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatActionScheduled.imageset/scheduledmenu.png b/Telegram-Mac/Assets.xcassets/Icon_ChatActionScheduled.imageset/scheduledmenu.png new file mode 100644 index 0000000000..80c1f0ca03 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatActionScheduled.imageset/scheduledmenu.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatActionScheduled.imageset/scheduledmenu@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatActionScheduled.imageset/scheduledmenu@2x.png new file mode 100644 index 0000000000..9812d7aed1 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatActionScheduled.imageset/scheduledmenu@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatAction_Close.imageset/Close.png b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_Close.imageset/Close.png new file mode 100644 index 0000000000..a1c76c3bdb Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_Close.imageset/Close.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatAction_Close.imageset/Close@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_Close.imageset/Close@2x.png new file mode 100644 index 0000000000..78fbb12b36 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_Close.imageset/Close@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatAction_Close.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_Close.imageset/Contents.json new file mode 100644 index 0000000000..2d14e41bb8 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_Close.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "Close.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Close@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatAction_EditMessage.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_EditMessage.imageset/Contents.json new file mode 100644 index 0000000000..0e1b563e61 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_EditMessage.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_edit.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_edit@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatAction_EditMessage.imageset/ic_edit.png b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_EditMessage.imageset/ic_edit.png new file mode 100644 index 0000000000..d2b8c392d5 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_EditMessage.imageset/ic_edit.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatAction_EditMessage.imageset/ic_edit@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_EditMessage.imageset/ic_edit@2x.png new file mode 100644 index 0000000000..201821f73a Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_EditMessage.imageset/ic_edit@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatAction_ForwardMessage.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_ForwardMessage.imageset/Contents.json new file mode 100644 index 0000000000..f001c5ef03 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_ForwardMessage.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_forward.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_forward@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatAction_ForwardMessage.imageset/ic_forward.png b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_ForwardMessage.imageset/ic_forward.png new file mode 100644 index 0000000000..c3565d221b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_ForwardMessage.imageset/ic_forward.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatAction_ForwardMessage.imageset/ic_forward@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_ForwardMessage.imageset/ic_forward@2x.png new file mode 100644 index 0000000000..8890192eaf Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_ForwardMessage.imageset/ic_forward@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatAction_Menu_UpdateChat.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_Menu_UpdateChat.imageset/Contents.json new file mode 100644 index 0000000000..fa66216f8b --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_Menu_UpdateChat.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_menu_replace.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_menu_replace@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatAction_Menu_UpdateChat.imageset/ic_menu_replace.png b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_Menu_UpdateChat.imageset/ic_menu_replace.png new file mode 100644 index 0000000000..d5b08d443e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_Menu_UpdateChat.imageset/ic_menu_replace.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatAction_Menu_UpdateChat.imageset/ic_menu_replace@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_Menu_UpdateChat.imageset/ic_menu_replace@2x.png new file mode 100644 index 0000000000..518dce2950 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_Menu_UpdateChat.imageset/ic_menu_replace@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatAction_ReplyMessage.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_ReplyMessage.imageset/Contents.json new file mode 100644 index 0000000000..9553d6b60d --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_ReplyMessage.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_reply.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_reply@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatAction_ReplyMessage.imageset/ic_reply.png b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_ReplyMessage.imageset/ic_reply.png new file mode 100644 index 0000000000..29bb7e5411 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_ReplyMessage.imageset/ic_reply.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatAction_ReplyMessage.imageset/ic_reply@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_ReplyMessage.imageset/ic_reply@2x.png new file mode 100644 index 0000000000..6a989a6d9d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_ReplyMessage.imageset/ic_reply@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatAction_UrlPreview.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_UrlPreview.imageset/Contents.json new file mode 100644 index 0000000000..0dd332766a --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_UrlPreview.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_link.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_link@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatAction_UrlPreview.imageset/ic_link.png b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_UrlPreview.imageset/ic_link.png new file mode 100644 index 0000000000..9b5c91e1b7 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_UrlPreview.imageset/ic_link.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatAction_UrlPreview.imageset/ic_link@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_UrlPreview.imageset/ic_link@2x.png new file mode 100644 index 0000000000..5109e3483f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatAction_UrlPreview.imageset/ic_link@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatActions.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatActions.imageset/Contents.json index a7d9b14e4b..7706bf794c 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_ChatActions.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatActions.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_more@1x.png", + "filename" : "ic_more.png", "scale" : "1x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatActions.imageset/ic_more.png b/Telegram-Mac/Assets.xcassets/Icon_ChatActions.imageset/ic_more.png new file mode 100644 index 0000000000..b2d9e23aef Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatActions.imageset/ic_more.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatActions.imageset/ic_more@1x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatActions.imageset/ic_more@1x.png deleted file mode 100644 index 903f7ae375..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_ChatActions.imageset/ic_more@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatActions.imageset/ic_more@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatActions.imageset/ic_more@2x.png index 574f7a52a2..677a27f31a 100644 Binary files a/Telegram-Mac/Assets.xcassets/Icon_ChatActions.imageset/ic_more@2x.png and b/Telegram-Mac/Assets.xcassets/Icon_ChatActions.imageset/ic_more@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatActionsActive.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatActionsActive.imageset/Contents.json index 60396c2489..7706bf794c 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_ChatActionsActive.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatActionsActive.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_moreact@1x.png", + "filename" : "ic_more.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ic_moreact@2x.png", + "filename" : "ic_more@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatActionsActive.imageset/ic_more.png b/Telegram-Mac/Assets.xcassets/Icon_ChatActionsActive.imageset/ic_more.png new file mode 100644 index 0000000000..b2d9e23aef Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatActionsActive.imageset/ic_more.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatActionsActive.imageset/ic_more@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatActionsActive.imageset/ic_more@2x.png new file mode 100644 index 0000000000..677a27f31a Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatActionsActive.imageset/ic_more@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatActionsActive.imageset/ic_moreact@1x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatActionsActive.imageset/ic_moreact@1x.png deleted file mode 100644 index c1ac27724d..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_ChatActionsActive.imageset/ic_moreact@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatActionsActive.imageset/ic_moreact@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatActionsActive.imageset/ic_moreact@2x.png deleted file mode 100644 index 009e5c3d1a..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_ChatActionsActive.imageset/ic_moreact@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatAdmins.imageset/Admin.png b/Telegram-Mac/Assets.xcassets/Icon_ChatAdmins.imageset/Admin.png new file mode 100644 index 0000000000..0ec218bae6 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatAdmins.imageset/Admin.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatAdmins.imageset/Admin@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatAdmins.imageset/Admin@2x.png new file mode 100644 index 0000000000..cd0540ea10 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatAdmins.imageset/Admin@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatAdmins.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatAdmins.imageset/Contents.json new file mode 100644 index 0000000000..26f821ec4c --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatAdmins.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Admin.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Admin@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatArchive.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatArchive.imageset/Contents.json new file mode 100644 index 0000000000..d1e20f9543 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatArchive.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_archive (1).png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_archive@2x (1).png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatArchive.imageset/ic_archive (1).png b/Telegram-Mac/Assets.xcassets/Icon_ChatArchive.imageset/ic_archive (1).png new file mode 100644 index 0000000000..f06aac3009 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatArchive.imageset/ic_archive (1).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatArchive.imageset/ic_archive@2x (1).png b/Telegram-Mac/Assets.xcassets/Icon_ChatArchive.imageset/ic_archive@2x (1).png new file mode 100644 index 0000000000..eea38c184d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatArchive.imageset/ic_archive@2x (1).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatBanned.imageset/Banned.png b/Telegram-Mac/Assets.xcassets/Icon_ChatBanned.imageset/Banned.png new file mode 100644 index 0000000000..4fa8b77973 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatBanned.imageset/Banned.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatBanned.imageset/Banned@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatBanned.imageset/Banned@2x.png new file mode 100644 index 0000000000..eb85609dbc Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatBanned.imageset/Banned@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatBanned.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatBanned.imageset/Contents.json new file mode 100644 index 0000000000..e0f5be5473 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatBanned.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Banned.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Banned@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatGoMessage.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatGoMessage.imageset/Contents.json new file mode 100644 index 0000000000..364e260913 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatGoMessage.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_goto@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_goto@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatGoMessage.imageset/ic_goto@1x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatGoMessage.imageset/ic_goto@1x.png new file mode 100644 index 0000000000..0a0ec38112 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatGoMessage.imageset/ic_goto@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatGoMessage.imageset/ic_goto@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatGoMessage.imageset/ic_goto@2x.png new file mode 100644 index 0000000000..fa34235a0c Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatGoMessage.imageset/ic_goto@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatInputScheduled.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatInputScheduled.imageset/Contents.json new file mode 100644 index 0000000000..ae2de6c2f7 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatInputScheduled.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "scheduled.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "scheduled@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatInputScheduled.imageset/scheduled.png b/Telegram-Mac/Assets.xcassets/Icon_ChatInputScheduled.imageset/scheduled.png new file mode 100644 index 0000000000..2b00ab95ae Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatInputScheduled.imageset/scheduled.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatInputScheduled.imageset/scheduled@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatInputScheduled.imageset/scheduled@2x.png new file mode 100644 index 0000000000..38039db642 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatInputScheduled.imageset/scheduled@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatInvoice.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatInvoice.imageset/Contents.json new file mode 100644 index 0000000000..8e5237b9d3 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatInvoice.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "card.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "card@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatInvoice.imageset/card.png b/Telegram-Mac/Assets.xcassets/Icon_ChatInvoice.imageset/card.png new file mode 100644 index 0000000000..c2d9dafd4e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatInvoice.imageset/card.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatInvoice.imageset/card@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatInvoice.imageset/card@2x.png new file mode 100644 index 0000000000..ebfc57f3ad Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatInvoice.imageset/card@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatListScrollUnread.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatListScrollUnread.imageset/Contents.json new file mode 100644 index 0000000000..7faef628d1 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatListScrollUnread.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_down@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_down@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatListScrollUnread.imageset/ic_down@1x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatListScrollUnread.imageset/ic_down@1x.png new file mode 100644 index 0000000000..53e3817944 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatListScrollUnread.imageset/ic_down@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatListScrollUnread.imageset/ic_down@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatListScrollUnread.imageset/ic_down@2x.png new file mode 100644 index 0000000000..930be3537c Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatListScrollUnread.imageset/ic_down@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatListSwiping_Archive.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatListSwiping_Archive.imageset/Contents.json new file mode 100644 index 0000000000..64d209f0c8 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatListSwiping_Archive.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "archive.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "archive@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatListSwiping_Archive.imageset/archive.png b/Telegram-Mac/Assets.xcassets/Icon_ChatListSwiping_Archive.imageset/archive.png new file mode 100644 index 0000000000..0a346e0813 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatListSwiping_Archive.imageset/archive.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatListSwiping_Archive.imageset/archive@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatListSwiping_Archive.imageset/archive@2x.png new file mode 100644 index 0000000000..2a0f3b54de Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatListSwiping_Archive.imageset/archive@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatListSwiping_Unarchive.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatListSwiping_Unarchive.imageset/Contents.json new file mode 100644 index 0000000000..d6001b144d --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatListSwiping_Unarchive.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "unarchive.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "unarchive@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatListSwiping_Unarchive.imageset/unarchive.png b/Telegram-Mac/Assets.xcassets/Icon_ChatListSwiping_Unarchive.imageset/unarchive.png new file mode 100644 index 0000000000..adb165674c Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatListSwiping_Unarchive.imageset/unarchive.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatListSwiping_Unarchive.imageset/unarchive@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatListSwiping_Unarchive.imageset/unarchive@2x.png new file mode 100644 index 0000000000..826f2b9d0c Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatListSwiping_Unarchive.imageset/unarchive@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatListThumbPlay.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatListThumbPlay.imageset/Contents.json new file mode 100644 index 0000000000..34120fe1c8 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatListThumbPlay.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "playchats.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "playchats@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatListThumbPlay.imageset/playchats.png b/Telegram-Mac/Assets.xcassets/Icon_ChatListThumbPlay.imageset/playchats.png new file mode 100644 index 0000000000..dd6663c62f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatListThumbPlay.imageset/playchats.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatListThumbPlay.imageset/playchats@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatListThumbPlay.imageset/playchats@2x.png new file mode 100644 index 0000000000..673a46128d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatListThumbPlay.imageset/playchats@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatMembers.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatMembers.imageset/Contents.json new file mode 100644 index 0000000000..6435c03f53 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatMembers.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Members.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Members@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatMembers.imageset/Members.png b/Telegram-Mac/Assets.xcassets/Icon_ChatMembers.imageset/Members.png new file mode 100644 index 0000000000..3d8a7f7242 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatMembers.imageset/Members.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatMembers.imageset/Members@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatMembers.imageset/Members@2x.png new file mode 100644 index 0000000000..8188aed27c Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatMembers.imageset/Members@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatNavigationBack.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatNavigationBack.imageset/Contents.json index af3c120c93..156658228a 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_ChatNavigationBack.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatNavigationBack.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "arrow@1x.png", + "filename" : "ic_back.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "arrow@2x.png", + "filename" : "ic_back@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatNavigationBack.imageset/arrow@1x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatNavigationBack.imageset/arrow@1x.png deleted file mode 100644 index cffa5fc4ff..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_ChatNavigationBack.imageset/arrow@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatNavigationBack.imageset/arrow@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatNavigationBack.imageset/arrow@2x.png deleted file mode 100644 index 88a59d94f9..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_ChatNavigationBack.imageset/arrow@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatNavigationBack.imageset/ic_back.png b/Telegram-Mac/Assets.xcassets/Icon_ChatNavigationBack.imageset/ic_back.png new file mode 100644 index 0000000000..76229a2cf5 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatNavigationBack.imageset/ic_back.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatNavigationBack.imageset/ic_back@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatNavigationBack.imageset/ic_back@2x.png new file mode 100644 index 0000000000..e4ea9b097e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatNavigationBack.imageset/ic_back@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatOverlayRecordingSend.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatOverlayRecordingSend.imageset/Contents.json new file mode 100644 index 0000000000..a4d7985468 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatOverlayRecordingSend.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "send@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "send@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatOverlayRecordingSend.imageset/send@1x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatOverlayRecordingSend.imageset/send@1x.png new file mode 100644 index 0000000000..80d0538db0 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatOverlayRecordingSend.imageset/send@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatOverlayRecordingSend.imageset/send@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatOverlayRecordingSend.imageset/send@2x.png new file mode 100644 index 0000000000..1941f769b9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatOverlayRecordingSend.imageset/send@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatPermissions.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatPermissions.imageset/Contents.json new file mode 100644 index 0000000000..4f7b8946f2 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatPermissions.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Permissions.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Permissions@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatPermissions.imageset/Permissions.png b/Telegram-Mac/Assets.xcassets/Icon_ChatPermissions.imageset/Permissions.png new file mode 100644 index 0000000000..10340027a3 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatPermissions.imageset/Permissions.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatPermissions.imageset/Permissions@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatPermissions.imageset/Permissions@2x.png new file mode 100644 index 0000000000..27b5b7218e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatPermissions.imageset/Permissions@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatPinnedList.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatPinnedList.imageset/Contents.json new file mode 100644 index 0000000000..759f3266b2 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatPinnedList.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_menu_pinnedlist.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_menu_pinnedlist@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatPinnedList.imageset/ic_menu_pinnedlist.png b/Telegram-Mac/Assets.xcassets/Icon_ChatPinnedList.imageset/ic_menu_pinnedlist.png new file mode 100644 index 0000000000..e98b8ead27 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatPinnedList.imageset/ic_menu_pinnedlist.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatPinnedList.imageset/ic_menu_pinnedlist@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatPinnedList.imageset/ic_menu_pinnedlist@2x.png new file mode 100644 index 0000000000..3920e02704 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatPinnedList.imageset/ic_menu_pinnedlist@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatPinnedMessage.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatPinnedMessage.imageset/Contents.json new file mode 100644 index 0000000000..700dd655b8 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatPinnedMessage.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "messagepin.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "messagepin@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatPinnedMessage.imageset/messagepin.png b/Telegram-Mac/Assets.xcassets/Icon_ChatPinnedMessage.imageset/messagepin.png new file mode 100644 index 0000000000..bf38f0d11f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatPinnedMessage.imageset/messagepin.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatPinnedMessage.imageset/messagepin@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatPinnedMessage.imageset/messagepin@2x.png new file mode 100644 index 0000000000..f165f57330 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatPinnedMessage.imageset/messagepin@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatRepliesCount.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatRepliesCount.imageset/Contents.json new file mode 100644 index 0000000000..4e7fa421e4 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatRepliesCount.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "Replies.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "replies@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatRepliesCount.imageset/Replies.png b/Telegram-Mac/Assets.xcassets/Icon_ChatRepliesCount.imageset/Replies.png new file mode 100644 index 0000000000..14332e66a0 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatRepliesCount.imageset/Replies.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatRepliesCount.imageset/replies@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatRepliesCount.imageset/replies@2x.png new file mode 100644 index 0000000000..e06f78f848 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatRepliesCount.imageset/replies@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSearchCancel.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatSearchCancel.imageset/Contents.json index 5b0c487f74..81735d6821 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_ChatSearchCancel.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatSearchCancel.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_cancel@1x.png", + "filename" : "ic_close.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ic_cancel@2x.png", + "filename" : "ic_close@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSearchCancel.imageset/ic_cancel@1x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatSearchCancel.imageset/ic_cancel@1x.png deleted file mode 100644 index ac06a420ba..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_ChatSearchCancel.imageset/ic_cancel@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSearchCancel.imageset/ic_cancel@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatSearchCancel.imageset/ic_cancel@2x.png deleted file mode 100644 index 176df09770..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_ChatSearchCancel.imageset/ic_cancel@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSearchCancel.imageset/ic_close.png b/Telegram-Mac/Assets.xcassets/Icon_ChatSearchCancel.imageset/ic_close.png new file mode 100644 index 0000000000..92e2be2e24 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatSearchCancel.imageset/ic_close.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSearchCancel.imageset/ic_close@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatSearchCancel.imageset/ic_close@2x.png new file mode 100644 index 0000000000..678a34747c Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatSearchCancel.imageset/ic_close@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSearchFrom.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatSearchFrom.imageset/Contents.json index 5d0377a724..44507263bc 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_ChatSearchFrom.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatSearchFrom.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_usersearch@1x.png", + "filename" : "ic_member.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ic_usersearch@2x.png", + "filename" : "ic_member@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSearchFrom.imageset/ic_member.png b/Telegram-Mac/Assets.xcassets/Icon_ChatSearchFrom.imageset/ic_member.png new file mode 100644 index 0000000000..08cfee9258 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatSearchFrom.imageset/ic_member.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSearchFrom.imageset/ic_member@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatSearchFrom.imageset/ic_member@2x.png new file mode 100644 index 0000000000..b6b3c4e05b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatSearchFrom.imageset/ic_member@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSearchFrom.imageset/ic_usersearch@1x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatSearchFrom.imageset/ic_usersearch@1x.png deleted file mode 100644 index 95f57b764c..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_ChatSearchFrom.imageset/ic_usersearch@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSearchFrom.imageset/ic_usersearch@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatSearchFrom.imageset/ic_usersearch@2x.png deleted file mode 100644 index 1863adb747..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_ChatSearchFrom.imageset/ic_usersearch@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingDelete.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingDelete.imageset/Contents.json new file mode 100644 index 0000000000..9a309d8ebd --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingDelete.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "delete.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "delete@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingDelete.imageset/delete.png b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingDelete.imageset/delete.png new file mode 100644 index 0000000000..ce8aea7d4d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingDelete.imageset/delete.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingDelete.imageset/delete@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingDelete.imageset/delete@2x.png new file mode 100644 index 0000000000..405430c95c Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingDelete.imageset/delete@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingMute.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingMute.imageset/Contents.json new file mode 100644 index 0000000000..46ba9a2cda --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingMute.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "mute.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "mute@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingMute.imageset/mute.png b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingMute.imageset/mute.png new file mode 100644 index 0000000000..90640686ca Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingMute.imageset/mute.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingMute.imageset/mute@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingMute.imageset/mute@2x.png new file mode 100644 index 0000000000..33ef79ce26 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingMute.imageset/mute@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingPin.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingPin.imageset/Contents.json new file mode 100644 index 0000000000..35ee767046 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingPin.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "pin.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "pin@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingPin.imageset/pin.png b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingPin.imageset/pin.png new file mode 100644 index 0000000000..3c670e5cb3 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingPin.imageset/pin.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingPin.imageset/pin@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingPin.imageset/pin@2x.png new file mode 100644 index 0000000000..e50ee21a31 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingPin.imageset/pin@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingRead.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingRead.imageset/Contents.json new file mode 100644 index 0000000000..44c45f808f --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingRead.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "read.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "read@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingRead.imageset/read.png b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingRead.imageset/read.png new file mode 100644 index 0000000000..5977e6b37e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingRead.imageset/read.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingRead.imageset/read@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingRead.imageset/read@2x.png new file mode 100644 index 0000000000..b2a53720db Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingRead.imageset/read@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingUnmute.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingUnmute.imageset/Contents.json new file mode 100644 index 0000000000..20ffbe6a3d --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingUnmute.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "unmute.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "unmute@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingUnmute.imageset/unmute.png b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingUnmute.imageset/unmute.png new file mode 100644 index 0000000000..acd9d2c1cc Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingUnmute.imageset/unmute.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingUnmute.imageset/unmute@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingUnmute.imageset/unmute@2x.png new file mode 100644 index 0000000000..ab8c7f901e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingUnmute.imageset/unmute@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingUnpin.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingUnpin.imageset/Contents.json new file mode 100644 index 0000000000..152ddde7fd --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingUnpin.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "unpin.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "unpin@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingUnpin.imageset/unpin.png b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingUnpin.imageset/unpin.png new file mode 100644 index 0000000000..b05f16cd21 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingUnpin.imageset/unpin.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingUnpin.imageset/unpin@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingUnpin.imageset/unpin@2x.png new file mode 100644 index 0000000000..417be73f3e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingUnpin.imageset/unpin@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingUnread.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingUnread.imageset/Contents.json new file mode 100644 index 0000000000..21ec686112 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingUnread.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "unread.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "unread@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingUnread.imageset/unread.png b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingUnread.imageset/unread.png new file mode 100644 index 0000000000..b14767bfc7 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingUnread.imageset/unread.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingUnread.imageset/unread@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingUnread.imageset/unread@2x.png new file mode 100644 index 0000000000..b9106d7c57 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatSwipingUnread.imageset/unread@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatTouchBarAddLink.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatTouchBarAddLink.imageset/Contents.json new file mode 100644 index 0000000000..299e64531d --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatTouchBarAddLink.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "link@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatTouchBarAddLink.imageset/link@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatTouchBarAddLink.imageset/link@2x.png new file mode 100644 index 0000000000..83b231ddd1 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatTouchBarAddLink.imageset/link@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatUnarchive.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatUnarchive.imageset/Contents.json new file mode 100644 index 0000000000..32d2d2658d --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatUnarchive.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_unarchive.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_unarchive@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatUnarchive.imageset/ic_unarchive.png b/Telegram-Mac/Assets.xcassets/Icon_ChatUnarchive.imageset/ic_unarchive.png new file mode 100644 index 0000000000..5332b05b91 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatUnarchive.imageset/ic_unarchive.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatUnarchive.imageset/ic_unarchive@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatUnarchive.imageset/ic_unarchive@2x.png new file mode 100644 index 0000000000..e90cfb8cf0 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatUnarchive.imageset/ic_unarchive@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatUndoAction.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatUndoAction.imageset/Contents.json new file mode 100644 index 0000000000..44904a161c --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatUndoAction.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_undo.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_undo@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatUndoAction.imageset/ic_undo.png b/Telegram-Mac/Assets.xcassets/Icon_ChatUndoAction.imageset/ic_undo.png new file mode 100644 index 0000000000..714aef12be Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatUndoAction.imageset/ic_undo.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatUndoAction.imageset/ic_undo@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatUndoAction.imageset/ic_undo@2x.png new file mode 100644 index 0000000000..a5f0d4a930 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatUndoAction.imageset/ic_undo@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatVoiceChat.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ChatVoiceChat.imageset/Contents.json new file mode 100644 index 0000000000..cc98e0d84a --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ChatVoiceChat.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "profilevoice.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "profilevoice@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatVoiceChat.imageset/profilevoice.png b/Telegram-Mac/Assets.xcassets/Icon_ChatVoiceChat.imageset/profilevoice.png new file mode 100644 index 0000000000..61f5c2323e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatVoiceChat.imageset/profilevoice.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ChatVoiceChat.imageset/profilevoice@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ChatVoiceChat.imageset/profilevoice@2x.png new file mode 100644 index 0000000000..fbff94b873 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ChatVoiceChat.imageset/profilevoice@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ClearChat.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ClearChat.imageset/Contents.json new file mode 100644 index 0000000000..ee71834bbd --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ClearChat.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_clear@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_clear@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ClearChat.imageset/ic_clear@1x.png b/Telegram-Mac/Assets.xcassets/Icon_ClearChat.imageset/ic_clear@1x.png new file mode 100644 index 0000000000..ededbd4bf7 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ClearChat.imageset/ic_clear@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ClearChat.imageset/ic_clear@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ClearChat.imageset/ic_clear@2x.png new file mode 100644 index 0000000000..3fdeb57429 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ClearChat.imageset/ic_clear@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CompactStreamingFetchingCancel.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_CompactStreamingFetchingCancel.imageset/Contents.json new file mode 100644 index 0000000000..f469ef8a29 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_CompactStreamingFetchingCancel.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "canceldownload.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "canceldownload@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_CompactStreamingFetchingCancel.imageset/canceldownload.png b/Telegram-Mac/Assets.xcassets/Icon_CompactStreamingFetchingCancel.imageset/canceldownload.png new file mode 100644 index 0000000000..9bb2cf890d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_CompactStreamingFetchingCancel.imageset/canceldownload.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CompactStreamingFetchingCancel.imageset/canceldownload@2x.png b/Telegram-Mac/Assets.xcassets/Icon_CompactStreamingFetchingCancel.imageset/canceldownload@2x.png new file mode 100644 index 0000000000..71d8de08f6 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_CompactStreamingFetchingCancel.imageset/canceldownload@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ConfirmAppAccessory.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ConfirmAppAccessory.imageset/Contents.json new file mode 100644 index 0000000000..4f69318514 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ConfirmAppAccessory.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "icon2_128.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "icon2_128@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_128.png b/Telegram-Mac/Assets.xcassets/Icon_ConfirmAppAccessory.imageset/icon2_128.png similarity index 100% rename from Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_128.png rename to Telegram-Mac/Assets.xcassets/Icon_ConfirmAppAccessory.imageset/icon2_128.png diff --git a/Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_128@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ConfirmAppAccessory.imageset/icon2_128@2x.png similarity index 100% rename from Telegram-Mac/Assets.xcassets/AppIcon.appiconset/icon2_128@2x.png rename to Telegram-Mac/Assets.xcassets/Icon_ConfirmAppAccessory.imageset/icon2_128@2x.png diff --git a/Telegram-Mac/Assets.xcassets/Icon_ConfirmDeleteChatAccessory.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ConfirmDeleteChatAccessory.imageset/Contents.json new file mode 100644 index 0000000000..d7c8b8ab9b --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ConfirmDeleteChatAccessory.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "deletechat@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "deletechat@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ConfirmDeleteChatAccessory.imageset/deletechat@1x.png b/Telegram-Mac/Assets.xcassets/Icon_ConfirmDeleteChatAccessory.imageset/deletechat@1x.png new file mode 100644 index 0000000000..3cdbbb3e6e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ConfirmDeleteChatAccessory.imageset/deletechat@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ConfirmDeleteChatAccessory.imageset/deletechat@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ConfirmDeleteChatAccessory.imageset/deletechat@2x.png new file mode 100644 index 0000000000..7271feb618 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ConfirmDeleteChatAccessory.imageset/deletechat@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ConfirmDeleteMessagesAccessory.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ConfirmDeleteMessagesAccessory.imageset/Contents.json new file mode 100644 index 0000000000..9a30ab525e --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ConfirmDeleteMessagesAccessory.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "deletemessage@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "deletemessage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ConfirmDeleteMessagesAccessory.imageset/deletemessage@1x.png b/Telegram-Mac/Assets.xcassets/Icon_ConfirmDeleteMessagesAccessory.imageset/deletemessage@1x.png new file mode 100644 index 0000000000..89c978292f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ConfirmDeleteMessagesAccessory.imageset/deletemessage@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ConfirmDeleteMessagesAccessory.imageset/deletemessage@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ConfirmDeleteMessagesAccessory.imageset/deletemessage@2x.png new file mode 100644 index 0000000000..66ed6bf760 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ConfirmDeleteMessagesAccessory.imageset/deletemessage@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ConfirmPinAccessory.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ConfirmPinAccessory.imageset/Contents.json new file mode 100644 index 0000000000..e857164ef9 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ConfirmPinAccessory.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "pin@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "pin@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ConfirmPinAccessory.imageset/pin@1x.png b/Telegram-Mac/Assets.xcassets/Icon_ConfirmPinAccessory.imageset/pin@1x.png new file mode 100644 index 0000000000..f431cab3b2 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ConfirmPinAccessory.imageset/pin@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ConfirmPinAccessory.imageset/pin@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ConfirmPinAccessory.imageset/pin@2x.png new file mode 100644 index 0000000000..1e2d9dc171 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ConfirmPinAccessory.imageset/pin@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CopyLink.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_CopyLink.imageset/Contents.json new file mode 100644 index 0000000000..2ea3ceb4ad --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_CopyLink.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_copylink.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_copylink@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_CopyLink.imageset/ic_copylink.png b/Telegram-Mac/Assets.xcassets/Icon_CopyLink.imageset/ic_copylink.png new file mode 100644 index 0000000000..edaddd9e92 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_CopyLink.imageset/ic_copylink.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_CopyLink.imageset/ic_copylink@2x.png b/Telegram-Mac/Assets.xcassets/Icon_CopyLink.imageset/ic_copylink@2x.png new file mode 100644 index 0000000000..eda2ea8240 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_CopyLink.imageset/ic_copylink@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_DFRRepeat.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_DFRRepeat.imageset/Contents.json new file mode 100644 index 0000000000..c3d710f4d5 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_DFRRepeat.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "DFRRepeat2.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_DFRRepeat.imageset/DFRRepeat2.png b/Telegram-Mac/Assets.xcassets/Icon_DFRRepeat.imageset/DFRRepeat2.png new file mode 100644 index 0000000000..8046c5456b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_DFRRepeat.imageset/DFRRepeat2.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_DFRShuffle.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_DFRShuffle.imageset/Contents.json new file mode 100644 index 0000000000..015498a1f8 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_DFRShuffle.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "DFRShuffle@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_DFRShuffle.imageset/DFRShuffle@2x.png b/Telegram-Mac/Assets.xcassets/Icon_DFRShuffle.imageset/DFRShuffle@2x.png new file mode 100644 index 0000000000..acee28ca6c Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_DFRShuffle.imageset/DFRShuffle@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_DeletedAccount.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_DeletedAccount.imageset/Contents.json new file mode 100644 index 0000000000..ad76c2fcd9 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_DeletedAccount.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Ghost.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Ghost@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_DeletedAccount.imageset/Ghost.png b/Telegram-Mac/Assets.xcassets/Icon_DeletedAccount.imageset/Ghost.png new file mode 100644 index 0000000000..3df848b36f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_DeletedAccount.imageset/Ghost.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_DeletedAccount.imageset/Ghost@2x.png b/Telegram-Mac/Assets.xcassets/Icon_DeletedAccount.imageset/Ghost@2x.png new file mode 100644 index 0000000000..4a03717943 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_DeletedAccount.imageset/Ghost@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_DetailedInfo.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_DetailedInfo.imageset/Contents.json index 38924cc735..d000341f1d 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_DetailedInfo.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_DetailedInfo.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "glyph-info.png", + "filename" : "ic_info.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "glyph-info@2x.png", + "filename" : "ic_info@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_DetailedInfo.imageset/glyph-info.png b/Telegram-Mac/Assets.xcassets/Icon_DetailedInfo.imageset/glyph-info.png deleted file mode 100644 index 190cf3f3fa..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_DetailedInfo.imageset/glyph-info.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_DetailedInfo.imageset/glyph-info@2x.png b/Telegram-Mac/Assets.xcassets/Icon_DetailedInfo.imageset/glyph-info@2x.png deleted file mode 100644 index f615bea5c9..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_DetailedInfo.imageset/glyph-info@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_DetailedInfo.imageset/ic_info.png b/Telegram-Mac/Assets.xcassets/Icon_DetailedInfo.imageset/ic_info.png new file mode 100644 index 0000000000..afbc479ddc Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_DetailedInfo.imageset/ic_info.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_DetailedInfo.imageset/ic_info@2x.png b/Telegram-Mac/Assets.xcassets/Icon_DetailedInfo.imageset/ic_info@2x.png new file mode 100644 index 0000000000..580d1ee2c1 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_DetailedInfo.imageset/ic_info@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EditImageArrow.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_EditImageArrow.imageset/Contents.json new file mode 100644 index 0000000000..d92f988fba --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_EditImageArrow.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_arrow.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_arrow@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_EditImageArrow.imageset/ic_arrow.png b/Telegram-Mac/Assets.xcassets/Icon_EditImageArrow.imageset/ic_arrow.png new file mode 100644 index 0000000000..7fd91f76f8 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EditImageArrow.imageset/ic_arrow.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EditImageArrow.imageset/ic_arrow@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EditImageArrow.imageset/ic_arrow@2x.png new file mode 100644 index 0000000000..d9d093da24 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EditImageArrow.imageset/ic_arrow@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EditImageDraw.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_EditImageDraw.imageset/Contents.json new file mode 100644 index 0000000000..f6a7c7fd65 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_EditImageDraw.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Draw.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Draw@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_EditImageDraw.imageset/Draw.png b/Telegram-Mac/Assets.xcassets/Icon_EditImageDraw.imageset/Draw.png new file mode 100644 index 0000000000..717f9178e2 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EditImageDraw.imageset/Draw.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EditImageDraw.imageset/Draw@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EditImageDraw.imageset/Draw@2x.png new file mode 100644 index 0000000000..9ac46e1dee Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EditImageDraw.imageset/Draw@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EditImageEraser.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_EditImageEraser.imageset/Contents.json new file mode 100644 index 0000000000..3a4d4cee71 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_EditImageEraser.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Eraser.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Eraser@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_EditImageEraser.imageset/Eraser.png b/Telegram-Mac/Assets.xcassets/Icon_EditImageEraser.imageset/Eraser.png new file mode 100644 index 0000000000..81ab276be6 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EditImageEraser.imageset/Eraser.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EditImageEraser.imageset/Eraser@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EditImageEraser.imageset/Eraser@2x.png new file mode 100644 index 0000000000..b2e320d6c8 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EditImageEraser.imageset/Eraser@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EditImageFlip.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_EditImageFlip.imageset/Contents.json new file mode 100644 index 0000000000..ed5bcc95f4 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_EditImageFlip.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "flip.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "flip@2x (1).png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_EditImageFlip.imageset/flip.png b/Telegram-Mac/Assets.xcassets/Icon_EditImageFlip.imageset/flip.png new file mode 100644 index 0000000000..12a4d219d3 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EditImageFlip.imageset/flip.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EditImageFlip.imageset/flip@2x (1).png b/Telegram-Mac/Assets.xcassets/Icon_EditImageFlip.imageset/flip@2x (1).png new file mode 100644 index 0000000000..14ece941c0 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EditImageFlip.imageset/flip@2x (1).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EditImageRotate.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_EditImageRotate.imageset/Contents.json new file mode 100644 index 0000000000..dff015368f --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_EditImageRotate.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "rotate.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "rotate@2x (1).png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_EditImageRotate.imageset/rotate.png b/Telegram-Mac/Assets.xcassets/Icon_EditImageRotate.imageset/rotate.png new file mode 100644 index 0000000000..af4642119b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EditImageRotate.imageset/rotate.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EditImageRotate.imageset/rotate@2x (1).png b/Telegram-Mac/Assets.xcassets/Icon_EditImageRotate.imageset/rotate@2x (1).png new file mode 100644 index 0000000000..3e818cf844 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EditImageRotate.imageset/rotate@2x (1).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EditImageSizes.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_EditImageSizes.imageset/Contents.json new file mode 100644 index 0000000000..c48da651b4 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_EditImageSizes.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "sizes.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "sizes@2x (1).png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_EditImageSizes.imageset/sizes.png b/Telegram-Mac/Assets.xcassets/Icon_EditImageSizes.imageset/sizes.png new file mode 100644 index 0000000000..196f1e4560 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EditImageSizes.imageset/sizes.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EditImageSizes.imageset/sizes@2x (1).png b/Telegram-Mac/Assets.xcassets/Icon_EditImageSizes.imageset/sizes@2x (1).png new file mode 100644 index 0000000000..12174b3621 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EditImageSizes.imageset/sizes@2x (1).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EditImageUndo.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_EditImageUndo.imageset/Contents.json new file mode 100644 index 0000000000..290bfb05a6 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_EditImageUndo.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Undo.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Undo@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_EditImageUndo.imageset/Undo.png b/Telegram-Mac/Assets.xcassets/Icon_EditImageUndo.imageset/Undo.png new file mode 100644 index 0000000000..32bcb92e9d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EditImageUndo.imageset/Undo.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EditImageUndo.imageset/Undo@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EditImageUndo.imageset/Undo@2x.png new file mode 100644 index 0000000000..ce7c5dd58a Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EditImageUndo.imageset/Undo@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EditMessageCurrentPhoto.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_EditMessageCurrentPhoto.imageset/Contents.json new file mode 100644 index 0000000000..30f8894de6 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_EditMessageCurrentPhoto.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "editcurrent.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "editcurrent@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_EditMessageCurrentPhoto.imageset/editcurrent.png b/Telegram-Mac/Assets.xcassets/Icon_EditMessageCurrentPhoto.imageset/editcurrent.png new file mode 100644 index 0000000000..5fe5629f8b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EditMessageCurrentPhoto.imageset/editcurrent.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EditMessageCurrentPhoto.imageset/editcurrent@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EditMessageCurrentPhoto.imageset/editcurrent@2x.png new file mode 100644 index 0000000000..a01143785c Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EditMessageCurrentPhoto.imageset/editcurrent@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Editor_Crop.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Editor_Crop.imageset/Contents.json new file mode 100644 index 0000000000..5645c75c5f --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Editor_Crop.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_editor_crop.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_editor_crop@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Editor_Crop.imageset/ic_editor_crop.png b/Telegram-Mac/Assets.xcassets/Icon_Editor_Crop.imageset/ic_editor_crop.png new file mode 100644 index 0000000000..e9155258c1 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Editor_Crop.imageset/ic_editor_crop.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Editor_Crop.imageset/ic_editor_crop@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Editor_Crop.imageset/ic_editor_crop@2x.png new file mode 100644 index 0000000000..e350ed2f04 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Editor_Crop.imageset/ic_editor_crop@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Editor_Delete.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Editor_Delete.imageset/Contents.json new file mode 100644 index 0000000000..07044cb003 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Editor_Delete.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_editor_delete.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_editor_delete@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Editor_Delete.imageset/ic_editor_delete.png b/Telegram-Mac/Assets.xcassets/Icon_Editor_Delete.imageset/ic_editor_delete.png new file mode 100644 index 0000000000..93efe762b0 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Editor_Delete.imageset/ic_editor_delete.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Editor_Delete.imageset/ic_editor_delete@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Editor_Delete.imageset/ic_editor_delete@2x.png new file mode 100644 index 0000000000..073f88cee8 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Editor_Delete.imageset/ic_editor_delete@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Editor_Paint.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Editor_Paint.imageset/Contents.json new file mode 100644 index 0000000000..036d88f8b1 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Editor_Paint.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_editor_paint.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_editor_paint@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Editor_Paint.imageset/ic_editor_paint.png b/Telegram-Mac/Assets.xcassets/Icon_Editor_Paint.imageset/ic_editor_paint.png new file mode 100644 index 0000000000..6c3fbcbbf6 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Editor_Paint.imageset/ic_editor_paint.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Editor_Paint.imageset/ic_editor_paint@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Editor_Paint.imageset/ic_editor_paint@2x.png new file mode 100644 index 0000000000..1dd00d22f9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Editor_Paint.imageset/ic_editor_paint@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabCar.imageset/City.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabCar.imageset/City.png new file mode 100644 index 0000000000..adbfd738f5 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabCar.imageset/City.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabCar.imageset/City@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabCar.imageset/City@2x.png new file mode 100644 index 0000000000..ca37631ca2 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabCar.imageset/City@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabCar.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabCar.imageset/Contents.json index 975ca03420..2a525eecf5 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabCar.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabCar.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_city@1x.png", + "filename" : "City.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ic_city@2x.png", + "filename" : "City@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabCar.imageset/ic_city@1x.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabCar.imageset/ic_city@1x.png deleted file mode 100644 index 6527124ce5..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabCar.imageset/ic_city@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabCar.imageset/ic_city@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabCar.imageset/ic_city@2x.png deleted file mode 100644 index 24c63434ff..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabCar.imageset/ic_city@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFlag.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFlag.imageset/Contents.json index cd14e1a9ff..24e67803a4 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFlag.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFlag.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_flags@1x.png", + "filename" : "Flag.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ic_flags@2x.png", + "filename" : "Flag@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFlag.imageset/Flag.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFlag.imageset/Flag.png new file mode 100644 index 0000000000..18c7972159 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFlag.imageset/Flag.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFlag.imageset/Flag@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFlag.imageset/Flag@2x.png new file mode 100644 index 0000000000..37ded39b53 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFlag.imageset/Flag@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFlag.imageset/ic_flags@1x.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFlag.imageset/ic_flags@1x.png deleted file mode 100644 index 4efda39374..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFlag.imageset/ic_flags@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFlag.imageset/ic_flags@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFlag.imageset/ic_flags@2x.png deleted file mode 100644 index 423c9d5a25..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFlag.imageset/ic_flags@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFood.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFood.imageset/Contents.json index 3299627275..2ee6157f04 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFood.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFood.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_eats@1x.png", + "filename" : "Food.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ic_eats@2x.png", + "filename" : "Food@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFood.imageset/Food.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFood.imageset/Food.png new file mode 100644 index 0000000000..e0885c4c2d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFood.imageset/Food.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFood.imageset/Food@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFood.imageset/Food@2x.png new file mode 100644 index 0000000000..563736e7ac Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFood.imageset/Food@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFood.imageset/ic_eats@1x.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFood.imageset/ic_eats@1x.png deleted file mode 100644 index f4db82edc1..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFood.imageset/ic_eats@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFood.imageset/ic_eats@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFood.imageset/ic_eats@2x.png deleted file mode 100644 index 7e81d6d9fe..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabFood.imageset/ic_eats@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabNature.imageset/Animals.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabNature.imageset/Animals.png new file mode 100644 index 0000000000..a475d95716 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabNature.imageset/Animals.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabNature.imageset/Animals@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabNature.imageset/Animals@2x.png new file mode 100644 index 0000000000..01f9fc6937 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabNature.imageset/Animals@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabNature.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabNature.imageset/Contents.json index 91b4081e8c..c35cc2ff72 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabNature.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabNature.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_animals@1x.png", + "filename" : "Animals.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ic_animals@2x.png", + "filename" : "Animals@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabNature.imageset/ic_animals@1x.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabNature.imageset/ic_animals@1x.png deleted file mode 100644 index d5c2d3f42e..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabNature.imageset/ic_animals@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabNature.imageset/ic_animals@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabNature.imageset/ic_animals@2x.png deleted file mode 100644 index f36f290ee9..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabNature.imageset/ic_animals@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabObjects.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabObjects.imageset/Contents.json index d0228be721..db51039cd3 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabObjects.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabObjects.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_lamp@1x.png", + "filename" : "Lamp.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ic_lamp@2x.png", + "filename" : "Lamp@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabObjects.imageset/Lamp.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabObjects.imageset/Lamp.png new file mode 100644 index 0000000000..48cc591464 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabObjects.imageset/Lamp.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabObjects.imageset/Lamp@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabObjects.imageset/Lamp@2x.png new file mode 100644 index 0000000000..c3511ee789 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabObjects.imageset/Lamp@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabObjects.imageset/ic_lamp@1x.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabObjects.imageset/ic_lamp@1x.png deleted file mode 100644 index a9dafa9315..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabObjects.imageset/ic_lamp@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabObjects.imageset/ic_lamp@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabObjects.imageset/ic_lamp@2x.png deleted file mode 100644 index 913421c8b4..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabObjects.imageset/ic_lamp@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSmiles.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSmiles.imageset/Contents.json index 40149a86d2..5631d55a33 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSmiles.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSmiles.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_smiles@1x.png", + "filename" : "Smile.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ic_smiles@2x.png", + "filename" : "Smile@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSmiles.imageset/Smile.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSmiles.imageset/Smile.png new file mode 100644 index 0000000000..a6d7657cdf Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSmiles.imageset/Smile.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSmiles.imageset/Smile@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSmiles.imageset/Smile@2x.png new file mode 100644 index 0000000000..836dea9c97 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSmiles.imageset/Smile@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSmiles.imageset/ic_smiles@1x.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSmiles.imageset/ic_smiles@1x.png deleted file mode 100644 index e6a56ee588..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSmiles.imageset/ic_smiles@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSmiles.imageset/ic_smiles@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSmiles.imageset/ic_smiles@2x.png deleted file mode 100644 index d15a76de1f..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSmiles.imageset/ic_smiles@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSports.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSports.imageset/Contents.json index ecd562bf81..a250d31a03 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSports.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSports.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_sports@1x.png", + "filename" : "Sport.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ic_sports@2x.png", + "filename" : "Sport@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSports.imageset/Sport.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSports.imageset/Sport.png new file mode 100644 index 0000000000..a7db3fab0e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSports.imageset/Sport.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSports.imageset/Sport@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSports.imageset/Sport@2x.png new file mode 100644 index 0000000000..8ab463a78a Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSports.imageset/Sport@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSports.imageset/ic_sports@1x.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSports.imageset/ic_sports@1x.png deleted file mode 100644 index c281d729b0..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSports.imageset/ic_sports@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSports.imageset/ic_sports@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSports.imageset/ic_sports@2x.png deleted file mode 100644 index 0d70bd6a40..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSports.imageset/ic_sports@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSymbols.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSymbols.imageset/Contents.json index bc2009dd9d..92d8c9ff14 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSymbols.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSymbols.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_symb@1x.png", + "filename" : "Symbols.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ic_symb@2x.png", + "filename" : "Symbols@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSymbols.imageset/Symbols.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSymbols.imageset/Symbols.png new file mode 100644 index 0000000000..916269be08 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSymbols.imageset/Symbols.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSymbols.imageset/Symbols@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSymbols.imageset/Symbols@2x.png new file mode 100644 index 0000000000..2355a28c1f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSymbols.imageset/Symbols@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSymbols.imageset/ic_symb@1x.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSymbols.imageset/ic_symb@1x.png deleted file mode 100644 index cd0008aa38..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSymbols.imageset/ic_symb@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSymbols.imageset/ic_symb@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSymbols.imageset/ic_symb@2x.png deleted file mode 100644 index d8550d284c..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_EmojiTabSymbols.imageset/ic_symb@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Dark.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Dark.imageset/Contents.json new file mode 100644 index 0000000000..bd5f4f2e65 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Dark.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_darktheme.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_darktheme@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Dark.imageset/ic_darktheme.png b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Dark.imageset/ic_darktheme.png new file mode 100644 index 0000000000..4755e045d6 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Dark.imageset/ic_darktheme.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Dark.imageset/ic_darktheme@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Dark.imageset/ic_darktheme@2x.png new file mode 100644 index 0000000000..9a05e3074a Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Dark.imageset/ic_darktheme@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Light.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Light.imageset/Contents.json new file mode 100644 index 0000000000..84f0a50723 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Light.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_lighttheme.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_lighttheme@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Light.imageset/ic_lighttheme.png b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Light.imageset/ic_lighttheme.png new file mode 100644 index 0000000000..e4c6efca78 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Light.imageset/ic_lighttheme.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Light.imageset/ic_lighttheme@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Light.imageset/ic_lighttheme@2x.png new file mode 100644 index 0000000000..271d8c6fb5 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Light.imageset/ic_lighttheme@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Stickers_AllSets.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Stickers_AllSets.imageset/Contents.json new file mode 100644 index 0000000000..8676d4165e --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Stickers_AllSets.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_allstickers.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_allstickers@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Stickers_AllSets.imageset/ic_allstickers.png b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Stickers_AllSets.imageset/ic_allstickers.png new file mode 100644 index 0000000000..fc4630b41d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Stickers_AllSets.imageset/ic_allstickers.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Stickers_AllSets.imageset/ic_allstickers@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Stickers_AllSets.imageset/ic_allstickers@2x.png new file mode 100644 index 0000000000..b7af09ab2f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Stickers_AllSets.imageset/ic_allstickers@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Stickers_MySets.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Stickers_MySets.imageset/Contents.json new file mode 100644 index 0000000000..e57bd36850 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Stickers_MySets.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_mystickers.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_mystickers@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Stickers_MySets.imageset/ic_mystickers.png b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Stickers_MySets.imageset/ic_mystickers.png new file mode 100644 index 0000000000..44c753ddd0 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Stickers_MySets.imageset/ic_mystickers.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Stickers_MySets.imageset/ic_mystickers@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Stickers_MySets.imageset/ic_mystickers@2x.png new file mode 100644 index 0000000000..0e8b78ac93 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Stickers_MySets.imageset/ic_mystickers@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Stickers_None.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Stickers_None.imageset/Contents.json new file mode 100644 index 0000000000..96ee8f3f8d --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Stickers_None.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_nonestickers.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_nonestickers@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Stickers_None.imageset/ic_nonestickers.png b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Stickers_None.imageset/ic_nonestickers.png new file mode 100644 index 0000000000..365dd218d4 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Stickers_None.imageset/ic_nonestickers.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Stickers_None.imageset/ic_nonestickers@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Stickers_None.imageset/ic_nonestickers@2x.png new file mode 100644 index 0000000000..27473f6b56 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Stickers_None.imageset/ic_nonestickers@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_Clear.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_Clear.imageset/Contents.json new file mode 100644 index 0000000000..7d0a5c7404 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_Clear.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_clear.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_clear@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_Clear.imageset/ic_clear.png b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_Clear.imageset/ic_clear.png new file mode 100644 index 0000000000..629fb0c081 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_Clear.imageset/ic_clear.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_Clear.imageset/ic_clear@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_Clear.imageset/ic_clear@2x.png new file mode 100644 index 0000000000..d82b4bd4a7 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_Clear.imageset/ic_clear@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_High.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_High.imageset/Contents.json new file mode 100644 index 0000000000..dddcde9a79 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_High.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_high.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_high@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_High.imageset/ic_high.png b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_High.imageset/ic_high.png new file mode 100644 index 0000000000..ddf74897bc Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_High.imageset/ic_high.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_High.imageset/ic_high@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_High.imageset/ic_high@2x.png new file mode 100644 index 0000000000..f3acb94edb Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_High.imageset/ic_high@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_Low.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_Low.imageset/Contents.json new file mode 100644 index 0000000000..60d7862398 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_Low.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_low.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_low@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_Low.imageset/ic_low.png b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_Low.imageset/ic_low.png new file mode 100644 index 0000000000..67446acd24 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_Low.imageset/ic_low.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_Low.imageset/ic_low@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_Low.imageset/ic_low@2x.png new file mode 100644 index 0000000000..68f8ea7470 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_Low.imageset/ic_low@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_Medium.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_Medium.imageset/Contents.json new file mode 100644 index 0000000000..31ebf182bf --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_Medium.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_medium.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_medium@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_Medium.imageset/ic_medium.png b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_Medium.imageset/ic_medium.png new file mode 100644 index 0000000000..f0a26d7cb3 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_Medium.imageset/ic_medium.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_Medium.imageset/ic_medium@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_Medium.imageset/ic_medium@2x.png new file mode 100644 index 0000000000..1e2cb29b4f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_Medium.imageset/ic_medium@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_NoLimit.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_NoLimit.imageset/Contents.json new file mode 100644 index 0000000000..72409a1ad5 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_NoLimit.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_unlimited.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_unlimited@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_NoLimit.imageset/ic_unlimited.png b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_NoLimit.imageset/ic_unlimited.png new file mode 100644 index 0000000000..c8c1a75838 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_NoLimit.imageset/ic_unlimited.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_NoLimit.imageset/ic_unlimited@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_NoLimit.imageset/ic_unlimited@2x.png new file mode 100644 index 0000000000..d95f9f6b71 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_Storage_NoLimit.imageset/ic_unlimited@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_System.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_System.imageset/Contents.json new file mode 100644 index 0000000000..ee8c984683 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_System.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_autotheme.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_autotheme@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_System.imageset/ic_autotheme.png b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_System.imageset/ic_autotheme.png new file mode 100644 index 0000000000..b190f01336 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_System.imageset/ic_autotheme.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_System.imageset/ic_autotheme@2x.png b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_System.imageset/ic_autotheme@2x.png new file mode 100644 index 0000000000..1d1a14a94c Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_EmptyChat_System.imageset/ic_autotheme@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Empty_CloseTips.imageset/Close.png b/Telegram-Mac/Assets.xcassets/Icon_Empty_CloseTips.imageset/Close.png new file mode 100644 index 0000000000..a1c76c3bdb Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Empty_CloseTips.imageset/Close.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Empty_CloseTips.imageset/Close@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Empty_CloseTips.imageset/Close@2x.png new file mode 100644 index 0000000000..78fbb12b36 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Empty_CloseTips.imageset/Close@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Empty_CloseTips.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Empty_CloseTips.imageset/Contents.json new file mode 100644 index 0000000000..2d14e41bb8 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Empty_CloseTips.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "Close.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Close@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Empty_ShowTips.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Empty_ShowTips.imageset/Contents.json new file mode 100644 index 0000000000..7321b83b0b --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Empty_ShowTips.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "Info.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Info@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Empty_ShowTips.imageset/Info.png b/Telegram-Mac/Assets.xcassets/Icon_Empty_ShowTips.imageset/Info.png new file mode 100644 index 0000000000..fa9a97030b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Empty_ShowTips.imageset/Info.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Empty_ShowTips.imageset/Info@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Empty_ShowTips.imageset/Info@2x.png new file mode 100644 index 0000000000..9f978fa8ed Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Empty_ShowTips.imageset/Info@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Emoji.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Emoji.imageset/Contents.json new file mode 100644 index 0000000000..d2764552ba --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Emoji.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Smiles.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Smiles@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Emoji.imageset/Smiles.png b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Emoji.imageset/Smiles.png new file mode 100644 index 0000000000..d0ae54aa4d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Emoji.imageset/Smiles.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Emoji.imageset/Smiles@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Emoji.imageset/Smiles@2x.png new file mode 100644 index 0000000000..a1316b0dc9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Emoji.imageset/Smiles@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Gifs.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Gifs.imageset/Contents.json new file mode 100644 index 0000000000..6890c0589c --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Gifs.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Gifs.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Gifs@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Gifs.imageset/Gifs.png b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Gifs.imageset/Gifs.png new file mode 100644 index 0000000000..45aee7d33f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Gifs.imageset/Gifs.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Gifs.imageset/Gifs@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Gifs.imageset/Gifs@2x.png new file mode 100644 index 0000000000..90d0299c3c Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Gifs.imageset/Gifs@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Search.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Search.imageset/Contents.json new file mode 100644 index 0000000000..915688990a --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Search.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Search.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Search@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Search.imageset/Search.png b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Search.imageset/Search.png new file mode 100644 index 0000000000..84a2d1f850 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Search.imageset/Search.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Search.imageset/Search@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Search.imageset/Search@2x.png new file mode 100644 index 0000000000..e2692c7cbc Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Search.imageset/Search@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Entertainment_SearchCancel.imageset/Close.png b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_SearchCancel.imageset/Close.png new file mode 100644 index 0000000000..cb17d282f8 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_SearchCancel.imageset/Close.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Entertainment_SearchCancel.imageset/Close@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_SearchCancel.imageset/Close@2x.png new file mode 100644 index 0000000000..1c476c3e80 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_SearchCancel.imageset/Close@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Entertainment_SearchCancel.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_SearchCancel.imageset/Contents.json new file mode 100644 index 0000000000..ae2baaff29 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_SearchCancel.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Close.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Close@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Settings.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Settings.imageset/Contents.json new file mode 100644 index 0000000000..7ceea51f3d --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Settings.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Settings.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Settings@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Settings.imageset/Settings.png b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Settings.imageset/Settings.png new file mode 100644 index 0000000000..f7388f856d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Settings.imageset/Settings.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Settings.imageset/Settings@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Settings.imageset/Settings@2x.png new file mode 100644 index 0000000000..a74862d424 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Settings.imageset/Settings@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Stickers.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Stickers.imageset/Contents.json new file mode 100644 index 0000000000..9d8bfee079 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Stickers.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Stickers.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Stickers@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Stickers.imageset/Stickers.png b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Stickers.imageset/Stickers.png new file mode 100644 index 0000000000..4aa94d4f4b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Stickers.imageset/Stickers.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Stickers.imageset/Stickers@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Stickers.imageset/Stickers@2x.png new file mode 100644 index 0000000000..ac5e94a9f2 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Entertainment_Stickers.imageset/Stickers@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ExportedInvitation_Expired.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ExportedInvitation_Expired.imageset/Contents.json new file mode 100644 index 0000000000..18eb01ff0e --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ExportedInvitation_Expired.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_linkexpired.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_linkexpired@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_ExportedInvitation_Expired.imageset/ic_linkexpired.png b/Telegram-Mac/Assets.xcassets/Icon_ExportedInvitation_Expired.imageset/ic_linkexpired.png new file mode 100644 index 0000000000..24e23593a6 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ExportedInvitation_Expired.imageset/ic_linkexpired.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ExportedInvitation_Expired.imageset/ic_linkexpired@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ExportedInvitation_Expired.imageset/ic_linkexpired@2x.png new file mode 100644 index 0000000000..59846f1de4 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ExportedInvitation_Expired.imageset/ic_linkexpired@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ExportedInvitation_Fire.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ExportedInvitation_Fire.imageset/Contents.json new file mode 100644 index 0000000000..61656cfadf --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ExportedInvitation_Fire.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_linkfire.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_linkfire@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_ExportedInvitation_Fire.imageset/ic_linkfire.png b/Telegram-Mac/Assets.xcassets/Icon_ExportedInvitation_Fire.imageset/ic_linkfire.png new file mode 100644 index 0000000000..13292f9014 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ExportedInvitation_Fire.imageset/ic_linkfire.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ExportedInvitation_Fire.imageset/ic_linkfire@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ExportedInvitation_Fire.imageset/ic_linkfire@2x.png new file mode 100644 index 0000000000..bb6e0b1c93 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ExportedInvitation_Fire.imageset/ic_linkfire@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ExportedInvitation_Link.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ExportedInvitation_Link.imageset/Contents.json new file mode 100644 index 0000000000..76c51dcfc8 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ExportedInvitation_Link.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_invitelinks.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_invitelinks@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_ExportedInvitation_Link.imageset/ic_invitelinks.png b/Telegram-Mac/Assets.xcassets/Icon_ExportedInvitation_Link.imageset/ic_invitelinks.png new file mode 100644 index 0000000000..8c9f3ed7e0 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ExportedInvitation_Link.imageset/ic_invitelinks.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ExportedInvitation_Link.imageset/ic_invitelinks@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ExportedInvitation_Link.imageset/ic_invitelinks@2x.png new file mode 100644 index 0000000000..11f23c8484 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ExportedInvitation_Link.imageset/ic_invitelinks@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FastCopyLink.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_FastCopyLink.imageset/Contents.json new file mode 100644 index 0000000000..2ea3ceb4ad --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_FastCopyLink.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_copylink.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_copylink@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_FastCopyLink.imageset/ic_copylink.png b/Telegram-Mac/Assets.xcassets/Icon_FastCopyLink.imageset/ic_copylink.png new file mode 100644 index 0000000000..edaddd9e92 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FastCopyLink.imageset/ic_copylink.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FastCopyLink.imageset/ic_copylink@2x.png b/Telegram-Mac/Assets.xcassets/Icon_FastCopyLink.imageset/ic_copylink@2x.png new file mode 100644 index 0000000000..eda2ea8240 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FastCopyLink.imageset/ic_copylink@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterAdd.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_FilterAdd.imageset/Contents.json new file mode 100644 index 0000000000..32fa579a18 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_FilterAdd.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_add.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_add@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterAdd.imageset/ic_add.png b/Telegram-Mac/Assets.xcassets/Icon_FilterAdd.imageset/ic_add.png new file mode 100644 index 0000000000..d1421de182 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterAdd.imageset/ic_add.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterAdd.imageset/ic_add@2x.png b/Telegram-Mac/Assets.xcassets/Icon_FilterAdd.imageset/ic_add@2x.png new file mode 100644 index 0000000000..fd6fdb33fb Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterAdd.imageset/ic_add@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterArchive.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_FilterArchive.imageset/Contents.json new file mode 100644 index 0000000000..d1e20f9543 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_FilterArchive.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_archive (1).png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_archive@2x (1).png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterArchive.imageset/ic_archive (1).png b/Telegram-Mac/Assets.xcassets/Icon_FilterArchive.imageset/ic_archive (1).png new file mode 100644 index 0000000000..cc0e471f43 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterArchive.imageset/ic_archive (1).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterArchive.imageset/ic_archive@2x (1).png b/Telegram-Mac/Assets.xcassets/Icon_FilterArchive.imageset/ic_archive@2x (1).png new file mode 100644 index 0000000000..f20ff8de1f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterArchive.imageset/ic_archive@2x (1).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterBots.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_FilterBots.imageset/Contents.json new file mode 100644 index 0000000000..501805ac2a --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_FilterBots.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_bot.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_bot@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterBots.imageset/ic_bot.png b/Telegram-Mac/Assets.xcassets/Icon_FilterBots.imageset/ic_bot.png new file mode 100644 index 0000000000..1531619419 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterBots.imageset/ic_bot.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterBots.imageset/ic_bot@2x.png b/Telegram-Mac/Assets.xcassets/Icon_FilterBots.imageset/ic_bot@2x.png new file mode 100644 index 0000000000..1958600b96 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterBots.imageset/ic_bot@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterChannels.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_FilterChannels.imageset/Contents.json new file mode 100644 index 0000000000..2595db3b7d --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_FilterChannels.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_channel.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_channel@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterChannels.imageset/ic_channel.png b/Telegram-Mac/Assets.xcassets/Icon_FilterChannels.imageset/ic_channel.png new file mode 100644 index 0000000000..36e6cf8395 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterChannels.imageset/ic_channel.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterChannels.imageset/ic_channel@2x.png b/Telegram-Mac/Assets.xcassets/Icon_FilterChannels.imageset/ic_channel@2x.png new file mode 100644 index 0000000000..b757cf2444 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterChannels.imageset/ic_channel@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterCustom.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_FilterCustom.imageset/Contents.json new file mode 100644 index 0000000000..19c51d3606 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_FilterCustom.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_filter.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_filter@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterCustom.imageset/ic_filter.png b/Telegram-Mac/Assets.xcassets/Icon_FilterCustom.imageset/ic_filter.png new file mode 100644 index 0000000000..49ac6e01f3 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterCustom.imageset/ic_filter.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterCustom.imageset/ic_filter@2x.png b/Telegram-Mac/Assets.xcassets/Icon_FilterCustom.imageset/ic_filter@2x.png new file mode 100644 index 0000000000..2b2b26a2a3 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterCustom.imageset/ic_filter@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterEdit.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_FilterEdit.imageset/Contents.json new file mode 100644 index 0000000000..15fc6bb37a --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_FilterEdit.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_customlist.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_customlist@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterEdit.imageset/ic_customlist.png b/Telegram-Mac/Assets.xcassets/Icon_FilterEdit.imageset/ic_customlist.png new file mode 100644 index 0000000000..635068a6ab Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterEdit.imageset/ic_customlist.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterEdit.imageset/ic_customlist@2x.png b/Telegram-Mac/Assets.xcassets/Icon_FilterEdit.imageset/ic_customlist@2x.png new file mode 100644 index 0000000000..430b2793e6 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterEdit.imageset/ic_customlist@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterGroups.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_FilterGroups.imageset/Contents.json new file mode 100644 index 0000000000..152058a20c --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_FilterGroups.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_group.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_group@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterGroups.imageset/ic_group.png b/Telegram-Mac/Assets.xcassets/Icon_FilterGroups.imageset/ic_group.png new file mode 100644 index 0000000000..60dec9ec12 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterGroups.imageset/ic_group.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterGroups.imageset/ic_group@2x.png b/Telegram-Mac/Assets.xcassets/Icon_FilterGroups.imageset/ic_group@2x.png new file mode 100644 index 0000000000..230c153ae1 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterGroups.imageset/ic_group@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterLargeGroups.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_FilterLargeGroups.imageset/Contents.json new file mode 100644 index 0000000000..44464d1640 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_FilterLargeGroups.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_largegroup.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_largegroup@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterLargeGroups.imageset/ic_largegroup.png b/Telegram-Mac/Assets.xcassets/Icon_FilterLargeGroups.imageset/ic_largegroup.png new file mode 100644 index 0000000000..7198c5b1cb Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterLargeGroups.imageset/ic_largegroup.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterLargeGroups.imageset/ic_largegroup@2x.png b/Telegram-Mac/Assets.xcassets/Icon_FilterLargeGroups.imageset/ic_largegroup@2x.png new file mode 100644 index 0000000000..e5236173ae Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterLargeGroups.imageset/ic_largegroup@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterMuted.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_FilterMuted.imageset/Contents.json new file mode 100644 index 0000000000..70063b6f29 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_FilterMuted.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_muted.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_muted@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterMuted.imageset/ic_muted.png b/Telegram-Mac/Assets.xcassets/Icon_FilterMuted.imageset/ic_muted.png new file mode 100644 index 0000000000..072e34519f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterMuted.imageset/ic_muted.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterMuted.imageset/ic_muted@2x.png b/Telegram-Mac/Assets.xcassets/Icon_FilterMuted.imageset/ic_muted@2x.png new file mode 100644 index 0000000000..71da5d66a2 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterMuted.imageset/ic_muted@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterNonContacts.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_FilterNonContacts.imageset/Contents.json new file mode 100644 index 0000000000..ab3139cc68 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_FilterNonContacts.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_noncontact.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_noncontact@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterNonContacts.imageset/ic_noncontact.png b/Telegram-Mac/Assets.xcassets/Icon_FilterNonContacts.imageset/ic_noncontact.png new file mode 100644 index 0000000000..e908c2bd6a Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterNonContacts.imageset/ic_noncontact.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterNonContacts.imageset/ic_noncontact@2x.png b/Telegram-Mac/Assets.xcassets/Icon_FilterNonContacts.imageset/ic_noncontact@2x.png new file mode 100644 index 0000000000..4249fe835b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterNonContacts.imageset/ic_noncontact@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterPrivateChats.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_FilterPrivateChats.imageset/Contents.json new file mode 100644 index 0000000000..7cf411fdb7 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_FilterPrivateChats.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_user.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_user@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterPrivateChats.imageset/ic_user.png b/Telegram-Mac/Assets.xcassets/Icon_FilterPrivateChats.imageset/ic_user.png new file mode 100644 index 0000000000..b319abe040 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterPrivateChats.imageset/ic_user.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterPrivateChats.imageset/ic_user@2x.png b/Telegram-Mac/Assets.xcassets/Icon_FilterPrivateChats.imageset/ic_user@2x.png new file mode 100644 index 0000000000..bb0c490105 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterPrivateChats.imageset/ic_user@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterRead.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_FilterRead.imageset/Contents.json new file mode 100644 index 0000000000..39a49c5b0b --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_FilterRead.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_read.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_read@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterRead.imageset/ic_read.png b/Telegram-Mac/Assets.xcassets/Icon_FilterRead.imageset/ic_read.png new file mode 100644 index 0000000000..76f59b0f6b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterRead.imageset/ic_read.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterRead.imageset/ic_read@2x.png b/Telegram-Mac/Assets.xcassets/Icon_FilterRead.imageset/ic_read@2x.png new file mode 100644 index 0000000000..edb935ca51 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterRead.imageset/ic_read@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterSecretChats.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_FilterSecretChats.imageset/Contents.json new file mode 100644 index 0000000000..1f0d15ef0d --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_FilterSecretChats.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_secretchat.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_secretchat@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterSecretChats.imageset/ic_secretchat.png b/Telegram-Mac/Assets.xcassets/Icon_FilterSecretChats.imageset/ic_secretchat.png new file mode 100644 index 0000000000..9900b01051 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterSecretChats.imageset/ic_secretchat.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterSecretChats.imageset/ic_secretchat@2x.png b/Telegram-Mac/Assets.xcassets/Icon_FilterSecretChats.imageset/ic_secretchat@2x.png new file mode 100644 index 0000000000..65ea4cc9a2 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterSecretChats.imageset/ic_secretchat@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterUnmuted.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_FilterUnmuted.imageset/Contents.json new file mode 100644 index 0000000000..3d4d31a176 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_FilterUnmuted.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_unmuted.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_unmuted@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterUnmuted.imageset/ic_unmuted.png b/Telegram-Mac/Assets.xcassets/Icon_FilterUnmuted.imageset/ic_unmuted.png new file mode 100644 index 0000000000..a534eca9b1 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterUnmuted.imageset/ic_unmuted.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterUnmuted.imageset/ic_unmuted@2x.png b/Telegram-Mac/Assets.xcassets/Icon_FilterUnmuted.imageset/ic_unmuted@2x.png new file mode 100644 index 0000000000..5117cbce30 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterUnmuted.imageset/ic_unmuted@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterUnread.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_FilterUnread.imageset/Contents.json new file mode 100644 index 0000000000..0fc3beb1c2 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_FilterUnread.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_unread.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_unread@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterUnread.imageset/ic_unread.png b/Telegram-Mac/Assets.xcassets/Icon_FilterUnread.imageset/ic_unread.png new file mode 100644 index 0000000000..1ff9519368 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterUnread.imageset/ic_unread.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_FilterUnread.imageset/ic_unread@2x.png b/Telegram-Mac/Assets.xcassets/Icon_FilterUnread.imageset/ic_unread@2x.png new file mode 100644 index 0000000000..4aa1df6eb3 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_FilterUnread.imageset/ic_unread@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GalleryMore.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GalleryMore.imageset/Contents.json index 309f9f74a0..2a95784536 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_GalleryMore.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_GalleryMore.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "Photo_More.png", + "filename" : "morephoto@1x.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "Photo_More@2x.png", + "filename" : "morephoto@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_GalleryMore.imageset/Photo_More.png b/Telegram-Mac/Assets.xcassets/Icon_GalleryMore.imageset/Photo_More.png deleted file mode 100644 index b1434293ad..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_GalleryMore.imageset/Photo_More.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GalleryMore.imageset/Photo_More@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GalleryMore.imageset/Photo_More@2x.png deleted file mode 100644 index 4bc2e1c34e..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_GalleryMore.imageset/Photo_More@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GalleryMore.imageset/morephoto@1x.png b/Telegram-Mac/Assets.xcassets/Icon_GalleryMore.imageset/morephoto@1x.png new file mode 100644 index 0000000000..4d2e970ff2 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GalleryMore.imageset/morephoto@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GalleryMore.imageset/morephoto@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GalleryMore.imageset/morephoto@2x.png new file mode 100644 index 0000000000..ddb825f374 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GalleryMore.imageset/morephoto@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GalleryNext.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GalleryNext.imageset/Contents.json new file mode 100644 index 0000000000..4e824eaacf --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GalleryNext.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "rightphoto@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "rightphoto@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_GalleryNext.imageset/rightphoto@1x.png b/Telegram-Mac/Assets.xcassets/Icon_GalleryNext.imageset/rightphoto@1x.png new file mode 100644 index 0000000000..088bdee045 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GalleryNext.imageset/rightphoto@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GalleryNext.imageset/rightphoto@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GalleryNext.imageset/rightphoto@2x.png new file mode 100644 index 0000000000..344a66f97a Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GalleryNext.imageset/rightphoto@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GalleryPrev.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GalleryPrev.imageset/Contents.json new file mode 100644 index 0000000000..bf3bc00980 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GalleryPrev.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "leftphoto@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "leftphoto@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_GalleryPrev.imageset/leftphoto@1x.png b/Telegram-Mac/Assets.xcassets/Icon_GalleryPrev.imageset/leftphoto@1x.png new file mode 100644 index 0000000000..5d2db4a7a8 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GalleryPrev.imageset/leftphoto@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GalleryPrev.imageset/leftphoto@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GalleryPrev.imageset/leftphoto@2x.png new file mode 100644 index 0000000000..10ce9fc7c8 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GalleryPrev.imageset/leftphoto@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GalleryRotate.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GalleryRotate.imageset/Contents.json new file mode 100644 index 0000000000..3ffe9ef165 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GalleryRotate.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "turn 2.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "turn 2@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_GalleryRotate.imageset/turn 2.png b/Telegram-Mac/Assets.xcassets/Icon_GalleryRotate.imageset/turn 2.png new file mode 100644 index 0000000000..1546902f33 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GalleryRotate.imageset/turn 2.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GalleryRotate.imageset/turn 2@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GalleryRotate.imageset/turn 2@2x.png new file mode 100644 index 0000000000..c420fdb448 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GalleryRotate.imageset/turn 2@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GalleryShare.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GalleryShare.imageset/Contents.json new file mode 100644 index 0000000000..8fe5fa020c --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GalleryShare.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "sharephoto@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "sharephoto@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_GalleryShare.imageset/sharephoto@1x.png b/Telegram-Mac/Assets.xcassets/Icon_GalleryShare.imageset/sharephoto@1x.png new file mode 100644 index 0000000000..d009cd5175 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GalleryShare.imageset/sharephoto@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GalleryShare.imageset/sharephoto@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GalleryShare.imageset/sharephoto@2x.png new file mode 100644 index 0000000000..a4570cdaf2 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GalleryShare.imageset/sharephoto@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GalleryZoomIn.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GalleryZoomIn.imageset/Contents.json new file mode 100644 index 0000000000..501e522c58 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GalleryZoomIn.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "zoomin 2.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "zoomin 2@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_GalleryZoomIn.imageset/zoomin 2.png b/Telegram-Mac/Assets.xcassets/Icon_GalleryZoomIn.imageset/zoomin 2.png new file mode 100644 index 0000000000..789c273281 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GalleryZoomIn.imageset/zoomin 2.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GalleryZoomIn.imageset/zoomin 2@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GalleryZoomIn.imageset/zoomin 2@2x.png new file mode 100644 index 0000000000..0cb904e2c3 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GalleryZoomIn.imageset/zoomin 2@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GalleryZoomOut.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GalleryZoomOut.imageset/Contents.json new file mode 100644 index 0000000000..c2b8d4441e --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GalleryZoomOut.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "zooout 2.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "zooout 2@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_GalleryZoomOut.imageset/zooout 2.png b/Telegram-Mac/Assets.xcassets/Icon_GalleryZoomOut.imageset/zooout 2.png new file mode 100644 index 0000000000..d80fb883f9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GalleryZoomOut.imageset/zooout 2.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GalleryZoomOut.imageset/zooout 2@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GalleryZoomOut.imageset/zooout 2@2x.png new file mode 100644 index 0000000000..2c229c8224 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GalleryZoomOut.imageset/zooout 2@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Gallery_FastSave.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Gallery_FastSave.imageset/Contents.json new file mode 100644 index 0000000000..440eb19baa --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Gallery_FastSave.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Download (1).png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Download@2x (1).png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Gallery_FastSave.imageset/Download (1).png b/Telegram-Mac/Assets.xcassets/Icon_Gallery_FastSave.imageset/Download (1).png new file mode 100644 index 0000000000..959de0b716 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Gallery_FastSave.imageset/Download (1).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Gallery_FastSave.imageset/Download@2x (1).png b/Telegram-Mac/Assets.xcassets/Icon_Gallery_FastSave.imageset/Download@2x (1).png new file mode 100644 index 0000000000..acd6a9e690 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Gallery_FastSave.imageset/Download@2x (1).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GifTrending.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GifTrending.imageset/Contents.json new file mode 100644 index 0000000000..ee2034cd9b --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GifTrending.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Trending (2).png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "trending@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_GifTrending.imageset/Trending (2).png b/Telegram-Mac/Assets.xcassets/Icon_GifTrending.imageset/Trending (2).png new file mode 100644 index 0000000000..4b4b0c9b55 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GifTrending.imageset/Trending (2).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GifTrending.imageset/trending@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GifTrending.imageset/trending@2x.png new file mode 100644 index 0000000000..0a77737c0e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GifTrending.imageset/trending@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GigagroupInfo.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GigagroupInfo.imageset/Contents.json new file mode 100644 index 0000000000..3a13783cb6 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GigagroupInfo.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_question.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_question@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_GigagroupInfo.imageset/ic_question.png b/Telegram-Mac/Assets.xcassets/Icon_GigagroupInfo.imageset/ic_question.png new file mode 100644 index 0000000000..6c3f1754d1 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GigagroupInfo.imageset/ic_question.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GigagroupInfo.imageset/ic_question@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GigagroupInfo.imageset/ic_question@2x.png new file mode 100644 index 0000000000..79c3e54c7b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GigagroupInfo.imageset/ic_question@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GotoBubbleMessage.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GotoBubbleMessage.imageset/Contents.json new file mode 100644 index 0000000000..7bb7e08542 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GotoBubbleMessage.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_gomsg.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_gomsg@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_GotoBubbleMessage.imageset/ic_gomsg.png b/Telegram-Mac/Assets.xcassets/Icon_GotoBubbleMessage.imageset/ic_gomsg.png new file mode 100644 index 0000000000..10ac426fa4 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GotoBubbleMessage.imageset/ic_gomsg.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GotoBubbleMessage.imageset/ic_gomsg@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GotoBubbleMessage.imageset/ic_gomsg@2x.png new file mode 100644 index 0000000000..f38e7c80be Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GotoBubbleMessage.imageset/ic_gomsg@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GradientAdd.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GradientAdd.imageset/Contents.json new file mode 100644 index 0000000000..62cc567b3d --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GradientAdd.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_input_add@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_input_add@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_GradientAdd.imageset/ic_input_add@1x.png b/Telegram-Mac/Assets.xcassets/Icon_GradientAdd.imageset/ic_input_add@1x.png new file mode 100644 index 0000000000..7495574b51 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GradientAdd.imageset/ic_input_add@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GradientAdd.imageset/ic_input_add@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GradientAdd.imageset/ic_input_add@2x.png new file mode 100644 index 0000000000..a1332b155e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GradientAdd.imageset/ic_input_add@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GradientClose.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GradientClose.imageset/Contents.json new file mode 100644 index 0000000000..ba6311a888 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GradientClose.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_input_close@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_input_close@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_GradientClose.imageset/ic_input_close@1x.png b/Telegram-Mac/Assets.xcassets/Icon_GradientClose.imageset/ic_input_close@1x.png new file mode 100644 index 0000000000..74e40b4d82 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GradientClose.imageset/ic_input_close@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GradientClose.imageset/ic_input_close@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GradientClose.imageset/ic_input_close@2x.png new file mode 100644 index 0000000000..dbdaa2b6d3 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GradientClose.imageset/ic_input_close@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GradientRotate.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GradientRotate.imageset/Contents.json new file mode 100644 index 0000000000..6a0e0eed8b --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GradientRotate.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_gradchange@1x (1).png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_gradchange@2x (1).png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_GradientRotate.imageset/ic_gradchange@1x (1).png b/Telegram-Mac/Assets.xcassets/Icon_GradientRotate.imageset/ic_gradchange@1x (1).png new file mode 100644 index 0000000000..9bd7c040a2 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GradientRotate.imageset/ic_gradchange@1x (1).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GradientRotate.imageset/ic_gradchange@2x (1).png b/Telegram-Mac/Assets.xcassets/Icon_GradientRotate.imageset/ic_gradchange@2x (1).png new file mode 100644 index 0000000000..e128dfc108 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GradientRotate.imageset/ic_gradchange@2x (1).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GradientSwap.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GradientSwap.imageset/Contents.json new file mode 100644 index 0000000000..213a2af044 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GradientSwap.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_input_change@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_input_change@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_GradientSwap.imageset/ic_input_change@1x.png b/Telegram-Mac/Assets.xcassets/Icon_GradientSwap.imageset/ic_input_change@1x.png new file mode 100644 index 0000000000..5c3889281c Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GradientSwap.imageset/ic_input_change@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GradientSwap.imageset/ic_input_change@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GradientSwap.imageset/ic_input_change@2x.png new file mode 100644 index 0000000000..b1ce00e994 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GradientSwap.imageset/ic_input_change@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Decline.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Decline.imageset/Contents.json new file mode 100644 index 0000000000..1a9f832f98 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Decline.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_fullscreen_leave.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_fullscreen_leave@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Decline.imageset/ic_fullscreen_leave.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Decline.imageset/ic_fullscreen_leave.png new file mode 100644 index 0000000000..1e463f28a5 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Decline.imageset/ic_fullscreen_leave.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Decline.imageset/ic_fullscreen_leave@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Decline.imageset/ic_fullscreen_leave@2x.png new file mode 100644 index 0000000000..4e8a5a7960 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Decline.imageset/ic_fullscreen_leave@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Invite.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Invite.imageset/Contents.json new file mode 100644 index 0000000000..cd5c3b472e --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Invite.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_invite.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_invite@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Invite.imageset/ic_invite.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Invite.imageset/ic_invite.png new file mode 100644 index 0000000000..37035fdf8a Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Invite.imageset/ic_invite.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Invite.imageset/ic_invite@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Invite.imageset/ic_invite@2x.png new file mode 100644 index 0000000000..6e4eaf407d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Invite.imageset/ic_invite@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Invited.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Invited.imageset/Contents.json new file mode 100644 index 0000000000..1e510ef113 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Invited.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_invited.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_invited@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Invited.imageset/ic_invited.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Invited.imageset/ic_invited.png new file mode 100644 index 0000000000..078cef0719 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Invited.imageset/ic_invited.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Invited.imageset/ic_invited@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Invited.imageset/ic_invited@2x.png new file mode 100644 index 0000000000..7683a60a32 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Invited.imageset/ic_invited@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_RaiseHand_Small.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_RaiseHand_Small.imageset/Contents.json new file mode 100644 index 0000000000..007c343375 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_RaiseHand_Small.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_hand.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_hand@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_RaiseHand_Small.imageset/ic_hand.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_RaiseHand_Small.imageset/ic_hand.png new file mode 100644 index 0000000000..e9b3d34532 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_RaiseHand_Small.imageset/ic_hand.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_RaiseHand_Small.imageset/ic_hand@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_RaiseHand_Small.imageset/ic_hand@2x.png new file mode 100644 index 0000000000..f2d732feba Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_RaiseHand_Small.imageset/ic_hand@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Record_Avatar1.imageset/Avatar1.pdf b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Record_Avatar1.imageset/Avatar1.pdf new file mode 100644 index 0000000000..084021fe6e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Record_Avatar1.imageset/Avatar1.pdf differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Record_Avatar1.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Record_Avatar1.imageset/Contents.json new file mode 100644 index 0000000000..a828cff54e --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Record_Avatar1.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Avatar1.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Record_Avatar2.imageset/Avatar2.pdf b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Record_Avatar2.imageset/Avatar2.pdf new file mode 100644 index 0000000000..5d54ed11aa Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Record_Avatar2.imageset/Avatar2.pdf differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Record_Avatar2.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Record_Avatar2.imageset/Contents.json new file mode 100644 index 0000000000..6d5fcd42d9 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Record_Avatar2.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Avatar2.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Record_Avatar3.imageset/Avatar3.pdf b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Record_Avatar3.imageset/Avatar3.pdf new file mode 100644 index 0000000000..7d78f0370f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Record_Avatar3.imageset/Avatar3.pdf differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Record_Avatar3.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Record_Avatar3.imageset/Contents.json new file mode 100644 index 0000000000..e1d56913f8 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Record_Avatar3.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Avatar3.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Record_Avatar4.imageset/Avatar4.pdf b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Record_Avatar4.imageset/Avatar4.pdf new file mode 100644 index 0000000000..65c9306447 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Record_Avatar4.imageset/Avatar4.pdf differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Record_Avatar4.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Record_Avatar4.imageset/Contents.json new file mode 100644 index 0000000000..abd967377c --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Record_Avatar4.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Avatar4.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Settings.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Settings.imageset/Contents.json new file mode 100644 index 0000000000..74044b186e --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Settings.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_settings.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_settings@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Settings.imageset/ic_settings.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Settings.imageset/ic_settings.png new file mode 100644 index 0000000000..f81529350b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Settings.imageset/ic_settings.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Settings.imageset/ic_settings@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Settings.imageset/ic_settings@2x.png new file mode 100644 index 0000000000..51da18ff05 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Settings.imageset/ic_settings@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Small_Muted.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Small_Muted.imageset/Contents.json new file mode 100644 index 0000000000..45c997ceb2 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Small_Muted.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_muted.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_muted@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Small_Muted.imageset/ic_muted.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Small_Muted.imageset/ic_muted.png new file mode 100644 index 0000000000..fef28ca84f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Small_Muted.imageset/ic_muted.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Small_Muted.imageset/ic_muted@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Small_Muted.imageset/ic_muted@2x.png new file mode 100644 index 0000000000..f28d657913 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Small_Muted.imageset/ic_muted@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Small_Unmuted.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Small_Unmuted.imageset/Contents.json new file mode 100644 index 0000000000..be6291dea2 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Small_Unmuted.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_unmuted.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_unmuted@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Small_Unmuted.imageset/ic_unmuted.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Small_Unmuted.imageset/ic_unmuted.png new file mode 100644 index 0000000000..e86078bbaa Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Small_Unmuted.imageset/ic_unmuted.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Small_Unmuted.imageset/ic_unmuted@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Small_Unmuted.imageset/ic_unmuted@2x.png new file mode 100644 index 0000000000..f49929a686 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Small_Unmuted.imageset/ic_unmuted@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Muted.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Muted.imageset/Contents.json new file mode 100644 index 0000000000..b2d21b699a --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Muted.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_voicevolumeoff.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_voicevolumeoff@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Muted.imageset/ic_voicevolumeoff.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Muted.imageset/ic_voicevolumeoff.png new file mode 100644 index 0000000000..d2831e2e3e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Muted.imageset/ic_voicevolumeoff.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Muted.imageset/ic_voicevolumeoff@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Muted.imageset/ic_voicevolumeoff@2x.png new file mode 100644 index 0000000000..d408a4486d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Muted.imageset/ic_voicevolumeoff@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Screencast.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Screencast.imageset/Contents.json new file mode 100644 index 0000000000..87c122fb3f --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Screencast.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_voicesharing.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_voicesharing@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Screencast.imageset/ic_voicesharing.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Screencast.imageset/ic_voicesharing.png new file mode 100644 index 0000000000..e54bbd6416 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Screencast.imageset/ic_voicesharing.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Screencast.imageset/ic_voicesharing@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Screencast.imageset/ic_voicesharing@2x.png new file mode 100644 index 0000000000..ad9494bb97 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Screencast.imageset/ic_voicesharing@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Unmuted.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Unmuted.imageset/Contents.json new file mode 100644 index 0000000000..d26fe22b1a --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Unmuted.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_voicevolumeon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_voicevolumeon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Unmuted.imageset/ic_voicevolumeon.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Unmuted.imageset/ic_voicevolumeon.png new file mode 100644 index 0000000000..cd8a133c6f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Unmuted.imageset/ic_voicevolumeon.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Unmuted.imageset/ic_voicevolumeon@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Unmuted.imageset/ic_voicevolumeon@2x.png new file mode 100644 index 0000000000..8155f2f95b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Unmuted.imageset/ic_voicevolumeon@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Video.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Video.imageset/Contents.json new file mode 100644 index 0000000000..4c923ae6fa --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Video.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_voicecamera.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_voicecamera@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Video.imageset/ic_voicecamera.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Video.imageset/ic_voicecamera.png new file mode 100644 index 0000000000..3a9ea12c10 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Video.imageset/ic_voicecamera.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Video.imageset/ic_voicecamera@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Video.imageset/ic_voicecamera@2x.png new file mode 100644 index 0000000000..95824e06a2 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Status_Video.imageset/ic_voicecamera@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoBox_Muted.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoBox_Muted.imageset/Contents.json new file mode 100644 index 0000000000..f568805f74 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoBox_Muted.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_voicemicrooff.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_voicemicrooff@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoBox_Muted.imageset/ic_voicemicrooff.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoBox_Muted.imageset/ic_voicemicrooff.png new file mode 100644 index 0000000000..ab96a8af5a Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoBox_Muted.imageset/ic_voicemicrooff.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoBox_Muted.imageset/ic_voicemicrooff@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoBox_Muted.imageset/ic_voicemicrooff@2x.png new file mode 100644 index 0000000000..68a839cdd7 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoBox_Muted.imageset/ic_voicemicrooff@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoBox_Unmuted.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoBox_Unmuted.imageset/Contents.json new file mode 100644 index 0000000000..80a71b915d --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoBox_Unmuted.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_voicemicroon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_voicemicroon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoBox_Unmuted.imageset/ic_voicemicroon.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoBox_Unmuted.imageset/ic_voicemicroon.png new file mode 100644 index 0000000000..8fd7abd602 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoBox_Unmuted.imageset/ic_voicemicroon.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoBox_Unmuted.imageset/ic_voicemicroon@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoBox_Unmuted.imageset/ic_voicemicroon@2x.png new file mode 100644 index 0000000000..e833f08fd4 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoBox_Unmuted.imageset/ic_voicemicroon@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoOff.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoOff.imageset/Contents.json new file mode 100644 index 0000000000..023bfdc558 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoOff.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_fullscreen_cameraoff.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_fullscreen_cameraoff@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoOff.imageset/ic_fullscreen_cameraoff.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoOff.imageset/ic_fullscreen_cameraoff.png new file mode 100644 index 0000000000..c5a0c157ab Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoOff.imageset/ic_fullscreen_cameraoff.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoOff.imageset/ic_fullscreen_cameraoff@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoOff.imageset/ic_fullscreen_cameraoff@2x.png new file mode 100644 index 0000000000..689b5e28c4 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoOff.imageset/ic_fullscreen_cameraoff@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoOn.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoOn.imageset/Contents.json new file mode 100644 index 0000000000..855335c0d1 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoOn.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_fullscreen_camera.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_fullscreen_camera@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoOn.imageset/ic_fullscreen_camera.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoOn.imageset/ic_fullscreen_camera.png new file mode 100644 index 0000000000..eab73045c1 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoOn.imageset/ic_fullscreen_camera.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoOn.imageset/ic_fullscreen_camera@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoOn.imageset/ic_fullscreen_camera@2x.png new file mode 100644 index 0000000000..b5ce015eac Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_VideoOn.imageset/ic_fullscreen_camera@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Video_ZoomIn.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Video_ZoomIn.imageset/Contents.json new file mode 100644 index 0000000000..24793b06da --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Video_ZoomIn.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_fullscreenon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_fullscreenon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Video_ZoomIn.imageset/ic_fullscreenon.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Video_ZoomIn.imageset/ic_fullscreenon.png new file mode 100644 index 0000000000..fbd9f549c5 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Video_ZoomIn.imageset/ic_fullscreenon.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Video_ZoomIn.imageset/ic_fullscreenon@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Video_ZoomIn.imageset/ic_fullscreenon@2x.png new file mode 100644 index 0000000000..21daf5c7ee Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Video_ZoomIn.imageset/ic_fullscreenon@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Video_ZoomOut.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Video_ZoomOut.imageset/Contents.json new file mode 100644 index 0000000000..1e1c5329d3 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Video_ZoomOut.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_fullscreenoff.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_fullscreenoff@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Video_ZoomOut.imageset/ic_fullscreenoff.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Video_ZoomOut.imageset/ic_fullscreenoff.png new file mode 100644 index 0000000000..9eda64a75d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Video_ZoomOut.imageset/ic_fullscreenoff.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Video_ZoomOut.imageset/ic_fullscreenoff@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Video_ZoomOut.imageset/ic_fullscreenoff@2x.png new file mode 100644 index 0000000000..d7ad4b9c1d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupCall_Video_ZoomOut.imageset/ic_fullscreenoff@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupInfoAddMember.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_GroupInfoAddMember.imageset/Contents.json index e78c0cc6b8..32fa579a18 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_GroupInfoAddMember.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_GroupInfoAddMember.imageset/Contents.json @@ -2,11 +2,12 @@ "images" : [ { "idiom" : "universal", + "filename" : "ic_add.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "GroupInfoIconAddMember@2x.png", + "filename" : "ic_add@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupInfoAddMember.imageset/GroupInfoIconAddMember@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GroupInfoAddMember.imageset/GroupInfoIconAddMember@2x.png deleted file mode 100644 index 8a96306a36..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_GroupInfoAddMember.imageset/GroupInfoIconAddMember@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupInfoAddMember.imageset/ic_add.png b/Telegram-Mac/Assets.xcassets/Icon_GroupInfoAddMember.imageset/ic_add.png new file mode 100644 index 0000000000..7495574b51 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupInfoAddMember.imageset/ic_add.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_GroupInfoAddMember.imageset/ic_add@2x.png b/Telegram-Mac/Assets.xcassets/Icon_GroupInfoAddMember.imageset/ic_add@2x.png new file mode 100644 index 0000000000..a1332b155e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_GroupInfoAddMember.imageset/ic_add@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayerNext.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayerNext.imageset/Contents.json index de7b6135fb..52e66ec940 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayerNext.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayerNext.imageset/Contents.json @@ -1,13 +1,13 @@ { "images" : [ { + "filename" : "Next (1).png", "idiom" : "universal", - "filename" : "ic_next@1x.png", "scale" : "1x" }, { + "filename" : "Next@2x (1).png", "idiom" : "universal", - "filename" : "ic_next@2x.png", "scale" : "2x" }, { @@ -16,7 +16,7 @@ } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayerNext.imageset/Next (1).png b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayerNext.imageset/Next (1).png new file mode 100644 index 0000000000..e7669c434c Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayerNext.imageset/Next (1).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayerNext.imageset/Next@2x (1).png b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayerNext.imageset/Next@2x (1).png new file mode 100644 index 0000000000..6d3ed8afb1 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayerNext.imageset/Next@2x (1).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayerNext.imageset/ic_next@1x.png b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayerNext.imageset/ic_next@1x.png deleted file mode 100644 index 68c3bf65c5..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayerNext.imageset/ic_next@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayerNext.imageset/ic_next@2x.png b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayerNext.imageset/ic_next@2x.png deleted file mode 100644 index 4a6a06091f..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayerNext.imageset/ic_next@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayerPrevious.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayerPrevious.imageset/Contents.json deleted file mode 100644 index d4a7e69e50..0000000000 --- a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayerPrevious.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "ic_prew@1x.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "ic_prew@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayerPrevious.imageset/ic_prew@1x.png b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayerPrevious.imageset/ic_prew@1x.png deleted file mode 100644 index e5f50262df..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayerPrevious.imageset/ic_prew@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayerPrevious.imageset/ic_prew@2x.png b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayerPrevious.imageset/ic_prew@2x.png deleted file mode 100644 index 129ce336b2..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayerPrevious.imageset/ic_prew@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_Repeat.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_Repeat.imageset/Contents.json new file mode 100644 index 0000000000..13408828b9 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_Repeat.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "Repeat.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Repeat@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_Repeat.imageset/Repeat.png b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_Repeat.imageset/Repeat.png new file mode 100644 index 0000000000..1e6fa60380 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_Repeat.imageset/Repeat.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_Repeat.imageset/Repeat@2x.png b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_Repeat.imageset/Repeat@2x.png new file mode 100644 index 0000000000..33324a84fd Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_Repeat.imageset/Repeat@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_RepeatOne.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_RepeatOne.imageset/Contents.json new file mode 100644 index 0000000000..c1e6457158 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_RepeatOne.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Repeat_1@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Repeat_1@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_RepeatOne.imageset/Repeat_1@2x.png b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_RepeatOne.imageset/Repeat_1@2x.png new file mode 100644 index 0000000000..b526c108b4 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_RepeatOne.imageset/Repeat_1@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_RepeatOne.imageset/Repeat_1@3x.png b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_RepeatOne.imageset/Repeat_1@3x.png new file mode 100644 index 0000000000..f568166af9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_RepeatOne.imageset/Repeat_1@3x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_VolumeOff.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_VolumeOff.imageset/Contents.json new file mode 100644 index 0000000000..3424b930b5 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_VolumeOff.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_vol2.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_vol2@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_VolumeOff.imageset/ic_vol2.png b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_VolumeOff.imageset/ic_vol2.png new file mode 100644 index 0000000000..f19d221cd9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_VolumeOff.imageset/ic_vol2.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_VolumeOff.imageset/ic_vol2@2x.png b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_VolumeOff.imageset/ic_vol2@2x.png new file mode 100644 index 0000000000..b5d19767be Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_VolumeOff.imageset/ic_vol2@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_VolumeOn.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_VolumeOn.imageset/Contents.json new file mode 100644 index 0000000000..6886a66dcf --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_VolumeOn.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_vol1.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_vol1@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_VolumeOn.imageset/ic_vol1.png b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_VolumeOn.imageset/ic_vol1.png new file mode 100644 index 0000000000..a46509a9dc Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_VolumeOn.imageset/ic_vol1.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_VolumeOn.imageset/ic_vol1@2x.png b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_VolumeOn.imageset/ic_vol1@2x.png new file mode 100644 index 0000000000..17acf8ae58 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_VolumeOn.imageset/ic_vol1@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_x2.imageset/2x@1x.png b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_x2.imageset/2x@1x.png new file mode 100644 index 0000000000..fb90a2795d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_x2.imageset/2x@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_x2.imageset/2x@2x.png b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_x2.imageset/2x@2x.png new file mode 100644 index 0000000000..9ab9ad6ec3 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_x2.imageset/2x@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_x2.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_x2.imageset/Contents.json new file mode 100644 index 0000000000..8a2976f984 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_InlinePlayer_x2.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "2x@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "2x@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlineVideoSoundOff.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_InlineVideoSoundOff.imageset/Contents.json new file mode 100644 index 0000000000..0033ed83b6 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_InlineVideoSoundOff.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "soundOFF.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "soundOFF@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlineVideoSoundOff.imageset/soundOFF.png b/Telegram-Mac/Assets.xcassets/Icon_InlineVideoSoundOff.imageset/soundOFF.png new file mode 100644 index 0000000000..ce48a67a48 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_InlineVideoSoundOff.imageset/soundOFF.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlineVideoSoundOff.imageset/soundOFF@2x.png b/Telegram-Mac/Assets.xcassets/Icon_InlineVideoSoundOff.imageset/soundOFF@2x.png new file mode 100644 index 0000000000..e54c946fb1 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_InlineVideoSoundOff.imageset/soundOFF@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlineVideoSoundOn.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_InlineVideoSoundOn.imageset/Contents.json new file mode 100644 index 0000000000..4b5136039d --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_InlineVideoSoundOn.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "soundON.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "soundON@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlineVideoSoundOn.imageset/soundON.png b/Telegram-Mac/Assets.xcassets/Icon_InlineVideoSoundOn.imageset/soundON.png new file mode 100644 index 0000000000..a311164a0e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_InlineVideoSoundOn.imageset/soundON.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_InlineVideoSoundOn.imageset/soundON@2x.png b/Telegram-Mac/Assets.xcassets/Icon_InlineVideoSoundOn.imageset/soundON@2x.png new file mode 100644 index 0000000000..837cc8ed96 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_InlineVideoSoundOn.imageset/soundON@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_InviteViaLink.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_InviteViaLink.imageset/Contents.json new file mode 100644 index 0000000000..74dd81960b --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_InviteViaLink.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_invitelink.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_invitelink@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_InviteViaLink.imageset/ic_invitelink.png b/Telegram-Mac/Assets.xcassets/Icon_InviteViaLink.imageset/ic_invitelink.png new file mode 100644 index 0000000000..6654a4a387 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_InviteViaLink.imageset/ic_invitelink.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_InviteViaLink.imageset/ic_invitelink@2x.png b/Telegram-Mac/Assets.xcassets/Icon_InviteViaLink.imageset/ic_invitelink@2x.png new file mode 100644 index 0000000000..4b164c9912 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_InviteViaLink.imageset/ic_invitelink@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Like_MessageButton.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Like_MessageButton.imageset/Contents.json new file mode 100644 index 0000000000..ecdf5c0b9e --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Like_MessageButton.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_like (2).png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_like@2x (2).png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Like_MessageButton.imageset/ic_like (2).png b/Telegram-Mac/Assets.xcassets/Icon_Like_MessageButton.imageset/ic_like (2).png new file mode 100644 index 0000000000..1f98666b88 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Like_MessageButton.imageset/ic_like (2).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Like_MessageButton.imageset/ic_like@2x (2).png b/Telegram-Mac/Assets.xcassets/Icon_Like_MessageButton.imageset/ic_like@2x (2).png new file mode 100644 index 0000000000..18ff31d388 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Like_MessageButton.imageset/ic_like@2x (2).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Like_MessageButtonUnlike.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Like_MessageButtonUnlike.imageset/Contents.json new file mode 100644 index 0000000000..2a3585b546 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Like_MessageButtonUnlike.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_unlike (2).png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_unlike@2x (2).png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Like_MessageButtonUnlike.imageset/ic_unlike (2).png b/Telegram-Mac/Assets.xcassets/Icon_Like_MessageButtonUnlike.imageset/ic_unlike (2).png new file mode 100644 index 0000000000..3fa8cea694 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Like_MessageButtonUnlike.imageset/ic_unlike (2).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Like_MessageButtonUnlike.imageset/ic_unlike@2x (2).png b/Telegram-Mac/Assets.xcassets/Icon_Like_MessageButtonUnlike.imageset/ic_unlike@2x (2).png new file mode 100644 index 0000000000..aebbe9c1bb Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Like_MessageButtonUnlike.imageset/ic_unlike@2x (2).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Like_MessageInside.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Like_MessageInside.imageset/Contents.json new file mode 100644 index 0000000000..c0ef6c6486 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Like_MessageInside.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_likedmsg.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_likedmsg@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Like_MessageInside.imageset/ic_likedmsg.png b/Telegram-Mac/Assets.xcassets/Icon_Like_MessageInside.imageset/ic_likedmsg.png new file mode 100644 index 0000000000..0b4ded84ff Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Like_MessageInside.imageset/ic_likedmsg.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Like_MessageInside.imageset/ic_likedmsg@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Like_MessageInside.imageset/ic_likedmsg@2x.png new file mode 100644 index 0000000000..d7e96159c9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Like_MessageInside.imageset/ic_likedmsg@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Like_MessageInsideEmpty.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Like_MessageInsideEmpty.imageset/Contents.json new file mode 100644 index 0000000000..26a6beadac --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Like_MessageInsideEmpty.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_likemsg.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_likemsg@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Like_MessageInsideEmpty.imageset/ic_likemsg.png b/Telegram-Mac/Assets.xcassets/Icon_Like_MessageInsideEmpty.imageset/ic_likemsg.png new file mode 100644 index 0000000000..6290a48e99 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Like_MessageInsideEmpty.imageset/ic_likemsg.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Like_MessageInsideEmpty.imageset/ic_likemsg@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Like_MessageInsideEmpty.imageset/ic_likemsg@2x.png new file mode 100644 index 0000000000..f4b63e6df3 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Like_MessageInsideEmpty.imageset/ic_likemsg@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_LocationPin.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_LocationPin.imageset/Contents.json new file mode 100644 index 0000000000..00631d365d --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_LocationPin.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LiveLocationTitlePin@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_LocationPin.imageset/LiveLocationTitlePin@2x.png b/Telegram-Mac/Assets.xcassets/Icon_LocationPin.imageset/LiveLocationTitlePin@2x.png new file mode 100644 index 0000000000..602e0beea6 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_LocationPin.imageset/LiveLocationTitlePin@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_LoginCap.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_LoginCap.imageset/Contents.json new file mode 100644 index 0000000000..5126757a29 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_LoginCap.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "logo.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "logo@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_LoginCap.imageset/logo.png b/Telegram-Mac/Assets.xcassets/Icon_LoginCap.imageset/logo.png new file mode 100644 index 0000000000..3eae4d36f1 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_LoginCap.imageset/logo.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_LoginCap.imageset/logo@2x.png b/Telegram-Mac/Assets.xcassets/Icon_LoginCap.imageset/logo@2x.png new file mode 100644 index 0000000000..89b9bc2122 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_LoginCap.imageset/logo@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_AddAccount.imageset/AddAccount.png b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_AddAccount.imageset/AddAccount.png new file mode 100644 index 0000000000..b35decd854 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_AddAccount.imageset/AddAccount.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_AddAccount.imageset/AddAccount@2x.png b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_AddAccount.imageset/AddAccount@2x.png new file mode 100644 index 0000000000..33887c5f68 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_AddAccount.imageset/AddAccount@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_AddAccount.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_AddAccount.imageset/Contents.json new file mode 100644 index 0000000000..ccebe0b502 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_AddAccount.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "AddAccount.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "AddAccount@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_ChangePhoneNumber.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_ChangePhoneNumber.imageset/Contents.json new file mode 100644 index 0000000000..912e780340 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_ChangePhoneNumber.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Sim.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Sim@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_ChangePhoneNumber.imageset/Sim.png b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_ChangePhoneNumber.imageset/Sim.png new file mode 100644 index 0000000000..cc82eaf379 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_ChangePhoneNumber.imageset/Sim.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_ChangePhoneNumber.imageset/Sim@2x.png b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_ChangePhoneNumber.imageset/Sim@2x.png new file mode 100644 index 0000000000..f98a79deca Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_ChangePhoneNumber.imageset/Sim@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_ClearCache.imageset/Cache.png b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_ClearCache.imageset/Cache.png new file mode 100644 index 0000000000..3b095d1545 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_ClearCache.imageset/Cache.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_ClearCache.imageset/Cache@2x.png b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_ClearCache.imageset/Cache@2x.png new file mode 100644 index 0000000000..6531eff77b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_ClearCache.imageset/Cache@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_ClearCache.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_ClearCache.imageset/Contents.json new file mode 100644 index 0000000000..4e325ac462 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_ClearCache.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Cache.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Cache@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_ContactSupport.imageset/Ask.png b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_ContactSupport.imageset/Ask.png new file mode 100644 index 0000000000..721ca0d46f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_ContactSupport.imageset/Ask.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_ContactSupport.imageset/Ask@2x.png b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_ContactSupport.imageset/Ask@2x.png new file mode 100644 index 0000000000..032ff10e30 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_ContactSupport.imageset/Ask@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_ContactSupport.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_ContactSupport.imageset/Contents.json new file mode 100644 index 0000000000..2bb9f0b1b2 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_ContactSupport.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Ask.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Ask@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_SetPasscode.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_SetPasscode.imageset/Contents.json new file mode 100644 index 0000000000..64ec213de6 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_SetPasscode.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Passcode.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Passcode@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_SetPasscode.imageset/Passcode.png b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_SetPasscode.imageset/Passcode.png new file mode 100644 index 0000000000..3c33bdc783 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_SetPasscode.imageset/Passcode.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_SetPasscode.imageset/Passcode@2x.png b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_SetPasscode.imageset/Passcode@2x.png new file mode 100644 index 0000000000..51fd8eb1cf Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_LogoutOption_SetPasscode.imageset/Passcode@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_MapLocate.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_MapLocate.imageset/Contents.json new file mode 100644 index 0000000000..4dcc439f34 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_MapLocate.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "MapLocationIcon_Active@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_MapLocate.imageset/MapLocationIcon_Active@2x.png b/Telegram-Mac/Assets.xcassets/Icon_MapLocate.imageset/MapLocationIcon_Active@2x.png new file mode 100644 index 0000000000..8411f17bc5 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_MapLocate.imageset/MapLocationIcon_Active@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelDelete.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelDelete.imageset/Contents.json index 4876759544..1402929627 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelDelete.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelDelete.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_delete@1x.png", + "filename" : "ic_delete.png", "scale" : "1x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelDelete.imageset/ic_delete.png b/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelDelete.imageset/ic_delete.png new file mode 100644 index 0000000000..93efe762b0 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelDelete.imageset/ic_delete.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelDelete.imageset/ic_delete@1x.png b/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelDelete.imageset/ic_delete@1x.png deleted file mode 100644 index 51dd74507f..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelDelete.imageset/ic_delete@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelDelete.imageset/ic_delete@2x.png b/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelDelete.imageset/ic_delete@2x.png index c732c164b7..073f88cee8 100644 Binary files a/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelDelete.imageset/ic_delete@2x.png and b/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelDelete.imageset/ic_delete@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelForward.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelForward.imageset/Contents.json index 5dce8d6a62..445cb8e64f 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelForward.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelForward.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_forward@1x.png", + "filename" : "ic_forward.png", "scale" : "1x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelForward.imageset/ic_forward.png b/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelForward.imageset/ic_forward.png new file mode 100644 index 0000000000..a9109b438d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelForward.imageset/ic_forward.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelForward.imageset/ic_forward@1x.png b/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelForward.imageset/ic_forward@1x.png deleted file mode 100644 index e70c63caf1..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelForward.imageset/ic_forward@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelForward.imageset/ic_forward@2x.png b/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelForward.imageset/ic_forward@2x.png index d709a3a32a..1552f66ab2 100644 Binary files a/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelForward.imageset/ic_forward@2x.png and b/Telegram-Mac/Assets.xcassets/Icon_MessageActionPanelForward.imageset/ic_forward@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_MusicPlayerSmallAlbumArtPlaceholder.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_MusicPlayerSmallAlbumArtPlaceholder.imageset/Contents.json new file mode 100644 index 0000000000..4b4c1425df --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_MusicPlayerSmallAlbumArtPlaceholder.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_music@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_music@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_MusicPlayerSmallAlbumArtPlaceholder.imageset/ic_music@1x.png b/Telegram-Mac/Assets.xcassets/Icon_MusicPlayerSmallAlbumArtPlaceholder.imageset/ic_music@1x.png new file mode 100644 index 0000000000..1c5a2ecf5e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_MusicPlayerSmallAlbumArtPlaceholder.imageset/ic_music@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_MusicPlayerSmallAlbumArtPlaceholder.imageset/ic_music@2x.png b/Telegram-Mac/Assets.xcassets/Icon_MusicPlayerSmallAlbumArtPlaceholder.imageset/ic_music@2x.png new file mode 100644 index 0000000000..f0c0f565d1 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_MusicPlayerSmallAlbumArtPlaceholder.imageset/ic_music@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_NavigationBack.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_NavigationBack.imageset/Contents.json index c64efbbc5a..156658228a 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_NavigationBack.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_NavigationBack.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "back.png", + "filename" : "ic_back.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "back@2x.png", + "filename" : "ic_back@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_NavigationBack.imageset/back.png b/Telegram-Mac/Assets.xcassets/Icon_NavigationBack.imageset/back.png deleted file mode 100644 index 3303d0a889..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_NavigationBack.imageset/back.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_NavigationBack.imageset/back@2x.png b/Telegram-Mac/Assets.xcassets/Icon_NavigationBack.imageset/back@2x.png deleted file mode 100644 index 1fa5e5b04e..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_NavigationBack.imageset/back@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_NavigationBack.imageset/ic_back.png b/Telegram-Mac/Assets.xcassets/Icon_NavigationBack.imageset/ic_back.png new file mode 100644 index 0000000000..76229a2cf5 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_NavigationBack.imageset/ic_back.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_NavigationBack.imageset/ic_back@2x.png b/Telegram-Mac/Assets.xcassets/Icon_NavigationBack.imageset/ic_back@2x.png new file mode 100644 index 0000000000..e4ea9b097e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_NavigationBack.imageset/ic_back@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_NewChannel.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_NewChannel.imageset/Contents.json index 5b61edff85..2595db3b7d 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_NewChannel.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_NewChannel.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_newchannel@1x.png", + "filename" : "ic_channel.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ic_newchannel@2x.png", + "filename" : "ic_channel@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_NewChannel.imageset/ic_channel.png b/Telegram-Mac/Assets.xcassets/Icon_NewChannel.imageset/ic_channel.png new file mode 100644 index 0000000000..36e6cf8395 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_NewChannel.imageset/ic_channel.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_NewChannel.imageset/ic_channel@2x.png b/Telegram-Mac/Assets.xcassets/Icon_NewChannel.imageset/ic_channel@2x.png new file mode 100644 index 0000000000..b757cf2444 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_NewChannel.imageset/ic_channel@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_NewChannel.imageset/ic_newchannel@1x.png b/Telegram-Mac/Assets.xcassets/Icon_NewChannel.imageset/ic_newchannel@1x.png deleted file mode 100644 index 483979d56e..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_NewChannel.imageset/ic_newchannel@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_NewChannel.imageset/ic_newchannel@2x.png b/Telegram-Mac/Assets.xcassets/Icon_NewChannel.imageset/ic_newchannel@2x.png deleted file mode 100644 index 35b895906b..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_NewChannel.imageset/ic_newchannel@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_NewContact.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_NewContact.imageset/Contents.json index b794d43728..32fa579a18 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_NewContact.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_NewContact.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "NewContact.png", + "filename" : "ic_add.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "NewContact@2x.png", + "filename" : "ic_add@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_NewContact.imageset/NewContact.png b/Telegram-Mac/Assets.xcassets/Icon_NewContact.imageset/NewContact.png deleted file mode 100644 index 425638c39f..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_NewContact.imageset/NewContact.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_NewContact.imageset/NewContact@2x.png b/Telegram-Mac/Assets.xcassets/Icon_NewContact.imageset/NewContact@2x.png deleted file mode 100644 index 82dce03ff5..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_NewContact.imageset/NewContact@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_NewContact.imageset/ic_add.png b/Telegram-Mac/Assets.xcassets/Icon_NewContact.imageset/ic_add.png new file mode 100644 index 0000000000..615c7c6a7b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_NewContact.imageset/ic_add.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_NewContact.imageset/ic_add@2x.png b/Telegram-Mac/Assets.xcassets/Icon_NewContact.imageset/ic_add@2x.png new file mode 100644 index 0000000000..b5144bdca4 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_NewContact.imageset/ic_add@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_NewGroup.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_NewGroup.imageset/Contents.json index 640c2621a8..152058a20c 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_NewGroup.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_NewGroup.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_newgroup@1x.png", + "filename" : "ic_group.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ic_newgroup@2x.png", + "filename" : "ic_group@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_NewGroup.imageset/ic_group.png b/Telegram-Mac/Assets.xcassets/Icon_NewGroup.imageset/ic_group.png new file mode 100644 index 0000000000..60dec9ec12 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_NewGroup.imageset/ic_group.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_NewGroup.imageset/ic_group@2x.png b/Telegram-Mac/Assets.xcassets/Icon_NewGroup.imageset/ic_group@2x.png new file mode 100644 index 0000000000..230c153ae1 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_NewGroup.imageset/ic_group@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_NewGroup.imageset/ic_newgroup@1x.png b/Telegram-Mac/Assets.xcassets/Icon_NewGroup.imageset/ic_newgroup@1x.png deleted file mode 100644 index da4d227e9e..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_NewGroup.imageset/ic_newgroup@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_NewGroup.imageset/ic_newgroup@2x.png b/Telegram-Mac/Assets.xcassets/Icon_NewGroup.imageset/ic_newgroup@2x.png deleted file mode 100644 index f0efca75d9..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_NewGroup.imageset/ic_newgroup@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_NewMessage.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_NewMessage.imageset/Contents.json index 8377bed453..2d0df3765e 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_NewMessage.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_NewMessage.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_newmsg@1x.png", + "filename" : "ic_create.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ic_newmsg@2x.png", + "filename" : "ic_create@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_NewMessage.imageset/ic_create.png b/Telegram-Mac/Assets.xcassets/Icon_NewMessage.imageset/ic_create.png new file mode 100644 index 0000000000..bb1b300a35 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_NewMessage.imageset/ic_create.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_NewMessage.imageset/ic_create@2x.png b/Telegram-Mac/Assets.xcassets/Icon_NewMessage.imageset/ic_create@2x.png new file mode 100644 index 0000000000..ac08a7c3ec Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_NewMessage.imageset/ic_create@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_NewMessage.imageset/ic_newmsg@1x.png b/Telegram-Mac/Assets.xcassets/Icon_NewMessage.imageset/ic_newmsg@1x.png deleted file mode 100644 index eaee9ab31d..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_NewMessage.imageset/ic_newmsg@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_NewMessage.imageset/ic_newmsg@2x.png b/Telegram-Mac/Assets.xcassets/Icon_NewMessage.imageset/ic_newmsg@2x.png deleted file mode 100644 index 2c6b7b7c0d..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_NewMessage.imageset/ic_newmsg@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_NewSecretChat.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_NewSecretChat.imageset/Contents.json index e5c08584b0..1f0d15ef0d 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_NewSecretChat.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_NewSecretChat.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_newsecret@1x.png", + "filename" : "ic_secretchat.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ic_newsecret@2x.png", + "filename" : "ic_secretchat@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_NewSecretChat.imageset/ic_newsecret@1x.png b/Telegram-Mac/Assets.xcassets/Icon_NewSecretChat.imageset/ic_newsecret@1x.png deleted file mode 100644 index a7426057ef..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_NewSecretChat.imageset/ic_newsecret@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_NewSecretChat.imageset/ic_newsecret@2x.png b/Telegram-Mac/Assets.xcassets/Icon_NewSecretChat.imageset/ic_newsecret@2x.png deleted file mode 100644 index f2cdeb0950..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_NewSecretChat.imageset/ic_newsecret@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_NewSecretChat.imageset/ic_secretchat.png b/Telegram-Mac/Assets.xcassets/Icon_NewSecretChat.imageset/ic_secretchat.png new file mode 100644 index 0000000000..9900b01051 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_NewSecretChat.imageset/ic_secretchat.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_NewSecretChat.imageset/ic_secretchat@2x.png b/Telegram-Mac/Assets.xcassets/Icon_NewSecretChat.imageset/ic_secretchat@2x.png new file mode 100644 index 0000000000..65ea4cc9a2 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_NewSecretChat.imageset/ic_secretchat@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PasscodeLogin.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_PasscodeLogin.imageset/Contents.json new file mode 100644 index 0000000000..46d9275d88 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_PasscodeLogin.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "login@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "login@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_PasscodeLogin.imageset/login@1x.png b/Telegram-Mac/Assets.xcassets/Icon_PasscodeLogin.imageset/login@1x.png new file mode 100644 index 0000000000..8bdfe9c6bc Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PasscodeLogin.imageset/login@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PasscodeLogin.imageset/login@2x.png b/Telegram-Mac/Assets.xcassets/Icon_PasscodeLogin.imageset/login@2x.png new file mode 100644 index 0000000000..0b55a7e221 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PasscodeLogin.imageset/login@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PassportDriverLicense.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_PassportDriverLicense.imageset/Contents.json new file mode 100644 index 0000000000..db466b143d --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_PassportDriverLicense.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "driver@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "driver@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_PassportDriverLicense.imageset/driver@1x.png b/Telegram-Mac/Assets.xcassets/Icon_PassportDriverLicense.imageset/driver@1x.png new file mode 100644 index 0000000000..3c97b3b032 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PassportDriverLicense.imageset/driver@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PassportDriverLicense.imageset/driver@2x.png b/Telegram-Mac/Assets.xcassets/Icon_PassportDriverLicense.imageset/driver@2x.png new file mode 100644 index 0000000000..b5434d65e6 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PassportDriverLicense.imageset/driver@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PassportIdCard.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_PassportIdCard.imageset/Contents.json new file mode 100644 index 0000000000..15ce916766 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_PassportIdCard.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "idcard@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "idcard@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_PassportIdCard.imageset/idcard@1x.png b/Telegram-Mac/Assets.xcassets/Icon_PassportIdCard.imageset/idcard@1x.png new file mode 100644 index 0000000000..36434c8976 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PassportIdCard.imageset/idcard@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PassportIdCard.imageset/idcard@2x.png b/Telegram-Mac/Assets.xcassets/Icon_PassportIdCard.imageset/idcard@2x.png new file mode 100644 index 0000000000..d202cc522f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PassportIdCard.imageset/idcard@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PassportIdCardReverse.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_PassportIdCardReverse.imageset/Contents.json new file mode 100644 index 0000000000..14d45b47df --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_PassportIdCardReverse.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "reverse@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "reverse@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_PassportIdCardReverse.imageset/reverse@1x.png b/Telegram-Mac/Assets.xcassets/Icon_PassportIdCardReverse.imageset/reverse@1x.png new file mode 100644 index 0000000000..f39c051c86 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PassportIdCardReverse.imageset/reverse@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PassportIdCardReverse.imageset/reverse@2x.png b/Telegram-Mac/Assets.xcassets/Icon_PassportIdCardReverse.imageset/reverse@2x.png new file mode 100644 index 0000000000..820b0c012d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PassportIdCardReverse.imageset/reverse@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PassportPassport.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_PassportPassport.imageset/Contents.json new file mode 100644 index 0000000000..3ed0db215e --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_PassportPassport.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "passport@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "passport@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_PassportPassport.imageset/passport@1x.png b/Telegram-Mac/Assets.xcassets/Icon_PassportPassport.imageset/passport@1x.png new file mode 100644 index 0000000000..67fe4f1668 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PassportPassport.imageset/passport@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PassportPassport.imageset/passport@2x.png b/Telegram-Mac/Assets.xcassets/Icon_PassportPassport.imageset/passport@2x.png new file mode 100644 index 0000000000..26c388f98c Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PassportPassport.imageset/passport@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PassportSelfie.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_PassportSelfie.imageset/Contents.json new file mode 100644 index 0000000000..465c13492d --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_PassportSelfie.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "selfie@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "selfie@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_PassportSelfie.imageset/selfie@1x.png b/Telegram-Mac/Assets.xcassets/Icon_PassportSelfie.imageset/selfie@1x.png new file mode 100644 index 0000000000..beda565071 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PassportSelfie.imageset/selfie@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PassportSelfie.imageset/selfie@2x.png b/Telegram-Mac/Assets.xcassets/Icon_PassportSelfie.imageset/selfie@2x.png new file mode 100644 index 0000000000..1ad4c54717 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PassportSelfie.imageset/selfie@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PassportSettings.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_PassportSettings.imageset/Contents.json new file mode 100644 index 0000000000..6f4c65581b --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_PassportSettings.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "pass@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "pass@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_PassportSettings.imageset/pass@1x.png b/Telegram-Mac/Assets.xcassets/Icon_PassportSettings.imageset/pass@1x.png new file mode 100644 index 0000000000..55e1f22dc7 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PassportSettings.imageset/pass@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PassportSettings.imageset/pass@2x.png b/Telegram-Mac/Assets.xcassets/Icon_PassportSettings.imageset/pass@2x.png new file mode 100644 index 0000000000..bde6f69774 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PassportSettings.imageset/pass@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PlayList_Next.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_PlayList_Next.imageset/Contents.json new file mode 100644 index 0000000000..d16b8ffe92 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_PlayList_Next.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "Next.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Next@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_PlayList_Next.imageset/Next.png b/Telegram-Mac/Assets.xcassets/Icon_PlayList_Next.imageset/Next.png new file mode 100644 index 0000000000..07d492e0ad Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PlayList_Next.imageset/Next.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PlayList_Next.imageset/Next@2x.png b/Telegram-Mac/Assets.xcassets/Icon_PlayList_Next.imageset/Next@2x.png new file mode 100644 index 0000000000..d395a89f79 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PlayList_Next.imageset/Next@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PlayList_Order.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_PlayList_Order.imageset/Contents.json new file mode 100644 index 0000000000..1ea3e97365 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_PlayList_Order.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "Order.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Order@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_PlayList_Order.imageset/Order.png b/Telegram-Mac/Assets.xcassets/Icon_PlayList_Order.imageset/Order.png new file mode 100644 index 0000000000..afc6f5bb35 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PlayList_Order.imageset/Order.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PlayList_Order.imageset/Order@2x.png b/Telegram-Mac/Assets.xcassets/Icon_PlayList_Order.imageset/Order@2x.png new file mode 100644 index 0000000000..d7d170c83c Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PlayList_Order.imageset/Order@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PlayList_Random.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_PlayList_Random.imageset/Contents.json new file mode 100644 index 0000000000..21abf01d4f --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_PlayList_Random.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "Repeat_1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Repeat_1@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_PlayList_Random.imageset/Repeat_1.png b/Telegram-Mac/Assets.xcassets/Icon_PlayList_Random.imageset/Repeat_1.png new file mode 100644 index 0000000000..024b2af01e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PlayList_Random.imageset/Repeat_1.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PlayList_Random.imageset/Repeat_1@2x.png b/Telegram-Mac/Assets.xcassets/Icon_PlayList_Random.imageset/Repeat_1@2x.png new file mode 100644 index 0000000000..4fa525f5d5 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PlayList_Random.imageset/Repeat_1@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PlayList_Repeat.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_PlayList_Repeat.imageset/Contents.json new file mode 100644 index 0000000000..13408828b9 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_PlayList_Repeat.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "Repeat.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Repeat@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_PlayList_Repeat.imageset/Repeat.png b/Telegram-Mac/Assets.xcassets/Icon_PlayList_Repeat.imageset/Repeat.png new file mode 100644 index 0000000000..58795d5f2d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PlayList_Repeat.imageset/Repeat.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PlayList_Repeat.imageset/Repeat@2x.png b/Telegram-Mac/Assets.xcassets/Icon_PlayList_Repeat.imageset/Repeat@2x.png new file mode 100644 index 0000000000..5db721b093 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PlayList_Repeat.imageset/Repeat@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PlayList_RepeatOne.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_PlayList_RepeatOne.imageset/Contents.json new file mode 100644 index 0000000000..419c4fdd91 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_PlayList_RepeatOne.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "Random.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Random@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_PlayList_RepeatOne.imageset/Random.png b/Telegram-Mac/Assets.xcassets/Icon_PlayList_RepeatOne.imageset/Random.png new file mode 100644 index 0000000000..6729df41db Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PlayList_RepeatOne.imageset/Random.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PlayList_RepeatOne.imageset/Random@2x.png b/Telegram-Mac/Assets.xcassets/Icon_PlayList_RepeatOne.imageset/Random@2x.png new file mode 100644 index 0000000000..6d4c242c8b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PlayList_RepeatOne.imageset/Random@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Playbar.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Playbar.imageset/Contents.json new file mode 100644 index 0000000000..1cf668737e --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Playbar.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "playbar.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Playbar.imageset/playbar.png b/Telegram-Mac/Assets.xcassets/Icon_Playbar.imageset/playbar.png new file mode 100644 index 0000000000..ad43bcdbcf Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Playbar.imageset/playbar.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PollAddOption.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_PollAddOption.imageset/Contents.json new file mode 100644 index 0000000000..941640e592 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_PollAddOption.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_addoption.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_addoption@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_PollAddOption.imageset/ic_addoption.png b/Telegram-Mac/Assets.xcassets/Icon_PollAddOption.imageset/ic_addoption.png new file mode 100644 index 0000000000..bdb1040905 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PollAddOption.imageset/ic_addoption.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PollAddOption.imageset/ic_addoption@2x.png b/Telegram-Mac/Assets.xcassets/Icon_PollAddOption.imageset/ic_addoption@2x.png new file mode 100644 index 0000000000..9855ca0179 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PollAddOption.imageset/ic_addoption@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PollDeleteOption.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_PollDeleteOption.imageset/Contents.json new file mode 100644 index 0000000000..b25139f4c0 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_PollDeleteOption.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_deleteoption.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_deleteoption@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_PollDeleteOption.imageset/ic_deleteoption.png b/Telegram-Mac/Assets.xcassets/Icon_PollDeleteOption.imageset/ic_deleteoption.png new file mode 100644 index 0000000000..0dd32ce6be Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PollDeleteOption.imageset/ic_deleteoption.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PollDeleteOption.imageset/ic_deleteoption@2x.png b/Telegram-Mac/Assets.xcassets/Icon_PollDeleteOption.imageset/ic_deleteoption@2x.png new file mode 100644 index 0000000000..515a9459c6 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PollDeleteOption.imageset/ic_deleteoption@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PollSelected.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_PollSelected.imageset/Contents.json new file mode 100644 index 0000000000..117af6d461 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_PollSelected.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "check.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "check@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_PollSelected.imageset/check.png b/Telegram-Mac/Assets.xcassets/Icon_PollSelected.imageset/check.png new file mode 100644 index 0000000000..125566e384 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PollSelected.imageset/check.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PollSelected.imageset/check@2x.png b/Telegram-Mac/Assets.xcassets/Icon_PollSelected.imageset/check@2x.png new file mode 100644 index 0000000000..db471df9e2 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PollSelected.imageset/check@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PollSelectedIncorrect.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_PollSelectedIncorrect.imageset/Contents.json new file mode 100644 index 0000000000..a01572a69b --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_PollSelectedIncorrect.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "failpoll.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "failpoll@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_PollSelectedIncorrect.imageset/failpoll.png b/Telegram-Mac/Assets.xcassets/Icon_PollSelectedIncorrect.imageset/failpoll.png new file mode 100644 index 0000000000..5d55ecbd2b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PollSelectedIncorrect.imageset/failpoll.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PollSelectedIncorrect.imageset/failpoll@2x.png b/Telegram-Mac/Assets.xcassets/Icon_PollSelectedIncorrect.imageset/failpoll@2x.png new file mode 100644 index 0000000000..331e4ded0f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PollSelectedIncorrect.imageset/failpoll@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PreviewCollage.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_PreviewCollage.imageset/Contents.json new file mode 100644 index 0000000000..e34d4481a7 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_PreviewCollage.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_album.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_album@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_PreviewCollage.imageset/ic_album.png b/Telegram-Mac/Assets.xcassets/Icon_PreviewCollage.imageset/ic_album.png new file mode 100644 index 0000000000..5b46055f1f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PreviewCollage.imageset/ic_album.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PreviewCollage.imageset/ic_album@2x.png b/Telegram-Mac/Assets.xcassets/Icon_PreviewCollage.imageset/ic_album@2x.png new file mode 100644 index 0000000000..669f4242ec Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PreviewCollage.imageset/ic_album@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderArchive.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderArchive.imageset/Contents.json new file mode 100644 index 0000000000..844e041b78 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderArchive.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_archive.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_archive@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderArchive.imageset/ic_archive.png b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderArchive.imageset/ic_archive.png new file mode 100644 index 0000000000..e25005b660 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderArchive.imageset/ic_archive.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderArchive.imageset/ic_archive@2x.png b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderArchive.imageset/ic_archive@2x.png new file mode 100644 index 0000000000..386e9aba89 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderArchive.imageset/ic_archive@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderCrop.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderCrop.imageset/Contents.json new file mode 100644 index 0000000000..c37512f6ac --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderCrop.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "crop.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "crop@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderCrop.imageset/crop.png b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderCrop.imageset/crop.png new file mode 100644 index 0000000000..da8063f63e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderCrop.imageset/crop.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderCrop.imageset/crop@2x.png b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderCrop.imageset/crop@2x.png new file mode 100644 index 0000000000..baefabd91f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderCrop.imageset/crop@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderDelete.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderDelete.imageset/Contents.json new file mode 100644 index 0000000000..0c6af7ddf6 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderDelete.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "deletemedia.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "deletemedia@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderDelete.imageset/deletemedia.png b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderDelete.imageset/deletemedia.png new file mode 100644 index 0000000000..b2f844b644 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderDelete.imageset/deletemedia.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderDelete.imageset/deletemedia@2x.png b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderDelete.imageset/deletemedia@2x.png new file mode 100644 index 0000000000..ddd4992559 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderDelete.imageset/deletemedia@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderDraw.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderDraw.imageset/Contents.json new file mode 100644 index 0000000000..af3ef90682 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderDraw.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_editor_paint.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_editor_paint@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderDraw.imageset/ic_editor_paint.png b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderDraw.imageset/ic_editor_paint.png new file mode 100644 index 0000000000..6c3fbcbbf6 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderDraw.imageset/ic_editor_paint.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderDraw.imageset/ic_editor_paint@2x.png b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderDraw.imageset/ic_editor_paint@2x.png new file mode 100644 index 0000000000..1dd00d22f9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderDraw.imageset/ic_editor_paint@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderFile.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderFile.imageset/Contents.json new file mode 100644 index 0000000000..301f7d5d72 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderFile.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_file.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_file@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderFile.imageset/ic_file.png b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderFile.imageset/ic_file.png new file mode 100644 index 0000000000..e4e497d680 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderFile.imageset/ic_file.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderFile.imageset/ic_file@2x.png b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderFile.imageset/ic_file@2x.png new file mode 100644 index 0000000000..917cae037e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderFile.imageset/ic_file@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderPhoto.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderPhoto.imageset/Contents.json new file mode 100644 index 0000000000..f27dea6285 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderPhoto.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_photo.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_photo@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderPhoto.imageset/ic_photo.png b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderPhoto.imageset/ic_photo.png new file mode 100644 index 0000000000..93e2114285 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderPhoto.imageset/ic_photo.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderPhoto.imageset/ic_photo@2x.png b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderPhoto.imageset/ic_photo@2x.png new file mode 100644 index 0000000000..71efd46d56 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PreviewSenderPhoto.imageset/ic_photo@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PrivacySettings_ActiveSessions.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_PrivacySettings_ActiveSessions.imageset/Contents.json new file mode 100644 index 0000000000..fe3d2cc35e --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_PrivacySettings_ActiveSessions.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "sessions.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "sessions@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_PrivacySettings_ActiveSessions.imageset/sessions.png b/Telegram-Mac/Assets.xcassets/Icon_PrivacySettings_ActiveSessions.imageset/sessions.png new file mode 100644 index 0000000000..69673bc8df Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PrivacySettings_ActiveSessions.imageset/sessions.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PrivacySettings_ActiveSessions.imageset/sessions@2x.png b/Telegram-Mac/Assets.xcassets/Icon_PrivacySettings_ActiveSessions.imageset/sessions@2x.png new file mode 100644 index 0000000000..c6794f9150 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PrivacySettings_ActiveSessions.imageset/sessions@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PrivacySettings_Blocked.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_PrivacySettings_Blocked.imageset/Contents.json new file mode 100644 index 0000000000..08cd625062 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_PrivacySettings_Blocked.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "blocked.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "blocked@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_PrivacySettings_Blocked.imageset/blocked.png b/Telegram-Mac/Assets.xcassets/Icon_PrivacySettings_Blocked.imageset/blocked.png new file mode 100644 index 0000000000..0bdc5e4d67 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PrivacySettings_Blocked.imageset/blocked.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PrivacySettings_Blocked.imageset/blocked@2x.png b/Telegram-Mac/Assets.xcassets/Icon_PrivacySettings_Blocked.imageset/blocked@2x.png new file mode 100644 index 0000000000..788001eb52 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PrivacySettings_Blocked.imageset/blocked@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PrivacySettings_TwoStep.imageset/2step.png b/Telegram-Mac/Assets.xcassets/Icon_PrivacySettings_TwoStep.imageset/2step.png new file mode 100644 index 0000000000..1bbd735c96 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PrivacySettings_TwoStep.imageset/2step.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PrivacySettings_TwoStep.imageset/2step@2x.png b/Telegram-Mac/Assets.xcassets/Icon_PrivacySettings_TwoStep.imageset/2step@2x.png new file mode 100644 index 0000000000..3f1c1ca800 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_PrivacySettings_TwoStep.imageset/2step@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_PrivacySettings_TwoStep.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_PrivacySettings_TwoStep.imageset/Contents.json new file mode 100644 index 0000000000..f116ef99fa --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_PrivacySettings_TwoStep.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "2step.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "2step@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ProfileCall.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ProfileCall.imageset/Contents.json index 72d402c1ee..56c95501ec 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_ProfileCall.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_ProfileCall.imageset/Contents.json @@ -2,11 +2,12 @@ "images" : [ { "idiom" : "universal", + "filename" : "ic_call.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "TabIconCalls@2x.png", + "filename" : "ic_call@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_ProfileCall.imageset/TabIconCalls@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ProfileCall.imageset/TabIconCalls@2x.png deleted file mode 100644 index fc1df7e1b3..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_ProfileCall.imageset/TabIconCalls@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ProfileCall.imageset/ic_call.png b/Telegram-Mac/Assets.xcassets/Icon_ProfileCall.imageset/ic_call.png new file mode 100644 index 0000000000..6a48bc086e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ProfileCall.imageset/ic_call.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ProfileCall.imageset/ic_call@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ProfileCall.imageset/ic_call@2x.png new file mode 100644 index 0000000000..e2edd1f39b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ProfileCall.imageset/ic_call@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_AddMember.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Profile_AddMember.imageset/Contents.json new file mode 100644 index 0000000000..7656773d6c --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Profile_AddMember.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_pf_addmember.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_pf_addmember@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_AddMember.imageset/ic_pf_addmember.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_AddMember.imageset/ic_pf_addmember.png new file mode 100644 index 0000000000..ac395cc67b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_AddMember.imageset/ic_pf_addmember.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_AddMember.imageset/ic_pf_addmember@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_AddMember.imageset/ic_pf_addmember@2x.png new file mode 100644 index 0000000000..35b9ed6b35 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_AddMember.imageset/ic_pf_addmember@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Block.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Profile_Block.imageset/Contents.json new file mode 100644 index 0000000000..393677dfcc --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Profile_Block.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_pf_block.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_pf_block@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Block.imageset/ic_pf_block.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Block.imageset/ic_pf_block.png new file mode 100644 index 0000000000..44fa58f195 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Block.imageset/ic_pf_block.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Block.imageset/ic_pf_block@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Block.imageset/ic_pf_block@2x.png new file mode 100644 index 0000000000..a8123d33e9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Block.imageset/ic_pf_block@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Call.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Profile_Call.imageset/Contents.json new file mode 100644 index 0000000000..32778f68ca --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Profile_Call.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_pf_call.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_pf_call@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Call.imageset/ic_pf_call.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Call.imageset/ic_pf_call.png new file mode 100644 index 0000000000..288e27f37e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Call.imageset/ic_pf_call.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Call.imageset/ic_pf_call@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Call.imageset/ic_pf_call@2x.png new file mode 100644 index 0000000000..643bb61e9a Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Call.imageset/ic_pf_call@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_ChannelSign.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Profile_ChannelSign.imageset/Contents.json new file mode 100644 index 0000000000..b8373a7cf4 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Profile_ChannelSign.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_sign (3).png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_sign@2x (3).png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_ChannelSign.imageset/ic_sign (3).png b/Telegram-Mac/Assets.xcassets/Icon_Profile_ChannelSign.imageset/ic_sign (3).png new file mode 100644 index 0000000000..5416dea883 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_ChannelSign.imageset/ic_sign (3).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_ChannelSign.imageset/ic_sign@2x (3).png b/Telegram-Mac/Assets.xcassets/Icon_Profile_ChannelSign.imageset/ic_sign@2x (3).png new file mode 100644 index 0000000000..de2eb4788b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_ChannelSign.imageset/ic_sign@2x (3).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_ChannelType.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Profile_ChannelType.imageset/Contents.json new file mode 100644 index 0000000000..2fccf0acbc --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Profile_ChannelType.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_channeltype.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_channeltype@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_ChannelType.imageset/ic_channeltype.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_ChannelType.imageset/ic_channeltype.png new file mode 100644 index 0000000000..b274fef304 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_ChannelType.imageset/ic_channeltype.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_ChannelType.imageset/ic_channeltype@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_ChannelType.imageset/ic_channeltype@2x.png new file mode 100644 index 0000000000..e302baea0f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_ChannelType.imageset/ic_channeltype@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Destruct.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Profile_Destruct.imageset/Contents.json new file mode 100644 index 0000000000..f6aa1180c9 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Profile_Destruct.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_autodelete.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_autodelete@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Destruct.imageset/ic_autodelete.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Destruct.imageset/ic_autodelete.png new file mode 100644 index 0000000000..7a532a8ae4 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Destruct.imageset/ic_autodelete.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Destruct.imageset/ic_autodelete@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Destruct.imageset/ic_autodelete@2x.png new file mode 100644 index 0000000000..4b4a5c8231 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Destruct.imageset/ic_autodelete@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Discussion.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Profile_Discussion.imageset/Contents.json new file mode 100644 index 0000000000..aede42b13d --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Profile_Discussion.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_discussion.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_discussion@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Discussion.imageset/ic_discussion.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Discussion.imageset/ic_discussion.png new file mode 100644 index 0000000000..7e6b88c6a4 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Discussion.imageset/ic_discussion.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Discussion.imageset/ic_discussion@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Discussion.imageset/ic_discussion@2x.png new file mode 100644 index 0000000000..0c46a59ef4 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Discussion.imageset/ic_discussion@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_EditPhoto.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Profile_EditPhoto.imageset/Contents.json new file mode 100644 index 0000000000..c07376e531 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Profile_EditPhoto.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_photo (2).png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_photo@2x (2).png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_EditPhoto.imageset/ic_photo (2).png b/Telegram-Mac/Assets.xcassets/Icon_Profile_EditPhoto.imageset/ic_photo (2).png new file mode 100644 index 0000000000..aedcef315e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_EditPhoto.imageset/ic_photo (2).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_EditPhoto.imageset/ic_photo@2x (2).png b/Telegram-Mac/Assets.xcassets/Icon_Profile_EditPhoto.imageset/ic_photo@2x (2).png new file mode 100644 index 0000000000..4a9f4262a3 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_EditPhoto.imageset/ic_photo@2x (2).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_GroupType.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Profile_GroupType.imageset/Contents.json new file mode 100644 index 0000000000..32a77b24dc --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Profile_GroupType.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_grouptype.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_grouptype@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_GroupType.imageset/ic_grouptype.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_GroupType.imageset/ic_grouptype.png new file mode 100644 index 0000000000..c639ae3ab9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_GroupType.imageset/ic_grouptype.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_GroupType.imageset/ic_grouptype@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_GroupType.imageset/ic_grouptype@2x.png new file mode 100644 index 0000000000..58f273b4a2 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_GroupType.imageset/ic_grouptype@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Leave.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Profile_Leave.imageset/Contents.json new file mode 100644 index 0000000000..3fbe735ef7 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Profile_Leave.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_pf_leave.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_pf_leave@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Leave.imageset/ic_pf_leave.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Leave.imageset/ic_pf_leave.png new file mode 100644 index 0000000000..06d896e379 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Leave.imageset/ic_pf_leave.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Leave.imageset/ic_pf_leave@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Leave.imageset/ic_pf_leave@2x.png new file mode 100644 index 0000000000..89d4eb174e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Leave.imageset/ic_pf_leave@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Links.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Profile_Links.imageset/Contents.json new file mode 100644 index 0000000000..8e99878c83 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Profile_Links.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_links.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_links@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Links.imageset/ic_links.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Links.imageset/ic_links.png new file mode 100644 index 0000000000..9f68d645e5 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Links.imageset/ic_links.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Links.imageset/ic_links@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Links.imageset/ic_links@2x.png new file mode 100644 index 0000000000..43b9cfd63a Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Links.imageset/ic_links@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Message.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Profile_Message.imageset/Contents.json new file mode 100644 index 0000000000..5bb843342d --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Profile_Message.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_pf_message.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_pf_message@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Message.imageset/ic_pf_message.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Message.imageset/ic_pf_message.png new file mode 100644 index 0000000000..65bb206a20 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Message.imageset/ic_pf_message.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Message.imageset/ic_pf_message@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Message.imageset/ic_pf_message@2x.png new file mode 100644 index 0000000000..601423df6a Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Message.imageset/ic_pf_message@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_More.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Profile_More.imageset/Contents.json new file mode 100644 index 0000000000..43f7409fd5 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Profile_More.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_pf_more.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_pf_more@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_More.imageset/ic_pf_more.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_More.imageset/ic_pf_more.png new file mode 100644 index 0000000000..906321c358 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_More.imageset/ic_pf_more.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_More.imageset/ic_pf_more@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_More.imageset/ic_pf_more@2x.png new file mode 100644 index 0000000000..9feb95c619 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_More.imageset/ic_pf_more@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Mute.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Profile_Mute.imageset/Contents.json new file mode 100644 index 0000000000..ad37f4c6cd --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Profile_Mute.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_pf_mute.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_pf_mute@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Mute.imageset/ic_pf_mute.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Mute.imageset/ic_pf_mute.png new file mode 100644 index 0000000000..9e9b474a3b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Mute.imageset/ic_pf_mute.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Mute.imageset/ic_pf_mute@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Mute.imageset/ic_pf_mute@2x.png new file mode 100644 index 0000000000..627a76da88 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Mute.imageset/ic_pf_mute@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Removed.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Profile_Removed.imageset/Contents.json new file mode 100644 index 0000000000..edb9c303b0 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Profile_Removed.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_removed.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_removed@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Removed.imageset/ic_removed.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Removed.imageset/ic_removed.png new file mode 100644 index 0000000000..bb01b05606 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Removed.imageset/ic_removed.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Removed.imageset/ic_removed@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Removed.imageset/ic_removed@2x.png new file mode 100644 index 0000000000..83736b2101 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Removed.imageset/ic_removed@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Report.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Profile_Report.imageset/Contents.json new file mode 100644 index 0000000000..b0e98ac7bc --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Profile_Report.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_pf_report.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_pf_report@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Report.imageset/ic_pf_report.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Report.imageset/ic_pf_report.png new file mode 100644 index 0000000000..02f47c362c Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Report.imageset/ic_pf_report.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Report.imageset/ic_pf_report@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Report.imageset/ic_pf_report@2x.png new file mode 100644 index 0000000000..d3154cb751 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Report.imageset/ic_pf_report@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Search.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Profile_Search.imageset/Contents.json new file mode 100644 index 0000000000..1b56ce796c --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Profile_Search.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_pf_search.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_pf_search@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Search.imageset/ic_pf_search.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Search.imageset/ic_pf_search.png new file mode 100644 index 0000000000..11f37f2f8d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Search.imageset/ic_pf_search.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Search.imageset/ic_pf_search@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Search.imageset/ic_pf_search@2x.png new file mode 100644 index 0000000000..febc0d56f7 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Search.imageset/ic_pf_search@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_SecretChat.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Profile_SecretChat.imageset/Contents.json new file mode 100644 index 0000000000..67ca02a72e --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Profile_SecretChat.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_pf_secretchat.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_pf_secretchat@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_SecretChat.imageset/ic_pf_secretchat.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_SecretChat.imageset/ic_pf_secretchat.png new file mode 100644 index 0000000000..a7012e9455 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_SecretChat.imageset/ic_pf_secretchat.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_SecretChat.imageset/ic_pf_secretchat@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_SecretChat.imageset/ic_pf_secretchat@2x.png new file mode 100644 index 0000000000..cdfbdb1266 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_SecretChat.imageset/ic_pf_secretchat@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Share.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Profile_Share.imageset/Contents.json new file mode 100644 index 0000000000..f66029d80d --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Profile_Share.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_pf_share.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_pf_share@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Share.imageset/ic_pf_share.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Share.imageset/ic_pf_share.png new file mode 100644 index 0000000000..cb8ad6959f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Share.imageset/ic_pf_share.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Share.imageset/ic_pf_share@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Share.imageset/ic_pf_share@2x.png new file mode 100644 index 0000000000..9d97f36be0 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Share.imageset/ic_pf_share@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Stats.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Profile_Stats.imageset/Contents.json new file mode 100644 index 0000000000..1bfd54ee26 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Profile_Stats.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_pf_stats.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_pf_stats@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Stats.imageset/ic_pf_stats.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Stats.imageset/ic_pf_stats.png new file mode 100644 index 0000000000..737bc88bb9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Stats.imageset/ic_pf_stats.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Stats.imageset/ic_pf_stats@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Stats.imageset/ic_pf_stats@2x.png new file mode 100644 index 0000000000..616e52900e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Stats.imageset/ic_pf_stats@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Unblock.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Profile_Unblock.imageset/Contents.json new file mode 100644 index 0000000000..4bccb75444 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Profile_Unblock.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_pf_unblock.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_pf_unblock@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Unblock.imageset/ic_pf_unblock.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Unblock.imageset/ic_pf_unblock.png new file mode 100644 index 0000000000..9bbce4baad Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Unblock.imageset/ic_pf_unblock.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Unblock.imageset/ic_pf_unblock@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Unblock.imageset/ic_pf_unblock@2x.png new file mode 100644 index 0000000000..44bc66c6b3 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Unblock.imageset/ic_pf_unblock@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Unmute.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Profile_Unmute.imageset/Contents.json new file mode 100644 index 0000000000..20c312c072 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Profile_Unmute.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_pf_unmute.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_pf_unmute@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Unmute.imageset/ic_pf_unmute.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Unmute.imageset/ic_pf_unmute.png new file mode 100644 index 0000000000..220646b091 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Unmute.imageset/ic_pf_unmute.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_Unmute.imageset/ic_pf_unmute@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_Unmute.imageset/ic_pf_unmute@2x.png new file mode 100644 index 0000000000..bebc82ea11 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_Unmute.imageset/ic_pf_unmute@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_VideoCall.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Profile_VideoCall.imageset/Contents.json new file mode 100644 index 0000000000..7175088d07 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Profile_VideoCall.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Video.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Video@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_VideoCall.imageset/Video.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_VideoCall.imageset/Video.png new file mode 100644 index 0000000000..d83716104d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_VideoCall.imageset/Video.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_VideoCall.imageset/Video@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Profile_VideoCall.imageset/Video@2x.png new file mode 100644 index 0000000000..486a3c2476 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_VideoCall.imageset/Video@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_VoiceChat.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Profile_VoiceChat.imageset/Contents.json new file mode 100644 index 0000000000..06044c021d --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Profile_VoiceChat.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_pf_voicechat (1).png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_pf_voicechat@2x (1).png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_VoiceChat.imageset/ic_pf_voicechat (1).png b/Telegram-Mac/Assets.xcassets/Icon_Profile_VoiceChat.imageset/ic_pf_voicechat (1).png new file mode 100644 index 0000000000..a7efb3fc13 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_VoiceChat.imageset/ic_pf_voicechat (1).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Profile_VoiceChat.imageset/ic_pf_voicechat@2x (1).png b/Telegram-Mac/Assets.xcassets/Icon_Profile_VoiceChat.imageset/ic_pf_voicechat@2x (1).png new file mode 100644 index 0000000000..f046d9f742 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Profile_VoiceChat.imageset/ic_pf_voicechat@2x (1).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ProgressWindowCheck.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ProgressWindowCheck.imageset/Contents.json new file mode 100644 index 0000000000..e4794c892d --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ProgressWindowCheck.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ProgressWindowCheck@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ProgressWindowCheck.imageset/ProgressWindowCheck@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ProgressWindowCheck.imageset/ProgressWindowCheck@2x.png new file mode 100644 index 0000000000..0549db4699 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ProgressWindowCheck.imageset/ProgressWindowCheck@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ProxyEnable.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ProxyEnable.imageset/Contents.json new file mode 100644 index 0000000000..d569f3f746 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ProxyEnable.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_proxy2.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_proxy2@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ProxyEnable.imageset/ic_proxy2.png b/Telegram-Mac/Assets.xcassets/Icon_ProxyEnable.imageset/ic_proxy2.png new file mode 100644 index 0000000000..45f9e1bece Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ProxyEnable.imageset/ic_proxy2.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ProxyEnable.imageset/ic_proxy2@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ProxyEnable.imageset/ic_proxy2@2x.png new file mode 100644 index 0000000000..163c4e2017 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ProxyEnable.imageset/ic_proxy2@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ProxyEnabled.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ProxyEnabled.imageset/Contents.json new file mode 100644 index 0000000000..de89fa0e72 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ProxyEnabled.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_proxy1.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_proxy1@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ProxyEnabled.imageset/ic_proxy1.png b/Telegram-Mac/Assets.xcassets/Icon_ProxyEnabled.imageset/ic_proxy1.png new file mode 100644 index 0000000000..a8fa802e1b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ProxyEnabled.imageset/ic_proxy1.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ProxyEnabled.imageset/ic_proxy1@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ProxyEnabled.imageset/ic_proxy1@2x.png new file mode 100644 index 0000000000..0f1239ed7c Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ProxyEnabled.imageset/ic_proxy1@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ProxyState.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ProxyState.imageset/Contents.json new file mode 100644 index 0000000000..2fc23be868 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ProxyState.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_proxy3.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_proxy3@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ProxyState.imageset/ic_proxy3.png b/Telegram-Mac/Assets.xcassets/Icon_ProxyState.imageset/ic_proxy3.png new file mode 100644 index 0000000000..f47b2d98e9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ProxyState.imageset/ic_proxy3.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ProxyState.imageset/ic_proxy3@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ProxyState.imageset/ic_proxy3@2x.png new file mode 100644 index 0000000000..1a2c12a3ab Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ProxyState.imageset/ic_proxy3@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_QuizExplanation.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_QuizExplanation.imageset/Contents.json new file mode 100644 index 0000000000..c60505c37b --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_QuizExplanation.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_lamp.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_lamp@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_QuizExplanation.imageset/ic_lamp.png b/Telegram-Mac/Assets.xcassets/Icon_QuizExplanation.imageset/ic_lamp.png new file mode 100644 index 0000000000..63270cb835 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_QuizExplanation.imageset/ic_lamp.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_QuizExplanation.imageset/ic_lamp@2x.png b/Telegram-Mac/Assets.xcassets/Icon_QuizExplanation.imageset/ic_lamp@2x.png new file mode 100644 index 0000000000..37c61eaa93 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_QuizExplanation.imageset/ic_lamp@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_RepeatAudio.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_RepeatAudio.imageset/Contents.json deleted file mode 100644 index f1b32b8d9c..0000000000 --- a/Telegram-Mac/Assets.xcassets/Icon_RepeatAudio.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "ic_repeat@1x.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "ic_repeat@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_RepeatAudio.imageset/ic_repeat@1x.png b/Telegram-Mac/Assets.xcassets/Icon_RepeatAudio.imageset/ic_repeat@1x.png deleted file mode 100644 index a584e6326f..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_RepeatAudio.imageset/ic_repeat@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_RepeatAudio.imageset/ic_repeat@2x.png b/Telegram-Mac/Assets.xcassets/Icon_RepeatAudio.imageset/ic_repeat@2x.png deleted file mode 100644 index af58e4c0ac..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_RepeatAudio.imageset/ic_repeat@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ReplaceMessageMedia.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ReplaceMessageMedia.imageset/Contents.json new file mode 100644 index 0000000000..3f20c1879d --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ReplaceMessageMedia.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "newmedia@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "newmedia@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ReplaceMessageMedia.imageset/newmedia@1x.png b/Telegram-Mac/Assets.xcassets/Icon_ReplaceMessageMedia.imageset/newmedia@1x.png new file mode 100644 index 0000000000..08030152de Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ReplaceMessageMedia.imageset/newmedia@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ReplaceMessageMedia.imageset/newmedia@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ReplaceMessageMedia.imageset/newmedia@2x.png new file mode 100644 index 0000000000..cad63a848d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ReplaceMessageMedia.imageset/newmedia@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_RepliesChat.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_RepliesChat.imageset/Contents.json new file mode 100644 index 0000000000..ddc64baf13 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_RepliesChat.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Icon_RepliesChat.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Icon_RepliesChat@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_RepliesChat.imageset/Icon_RepliesChat.png b/Telegram-Mac/Assets.xcassets/Icon_RepliesChat.imageset/Icon_RepliesChat.png new file mode 100644 index 0000000000..dd5e9d9a82 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_RepliesChat.imageset/Icon_RepliesChat.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_RepliesChat.imageset/Icon_RepliesChat@2x.png b/Telegram-Mac/Assets.xcassets/Icon_RepliesChat.imageset/Icon_RepliesChat@2x.png new file mode 100644 index 0000000000..5b5a7e7439 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_RepliesChat.imageset/Icon_RepliesChat@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Replies_ListMode.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Replies_ListMode.imageset/Contents.json new file mode 100644 index 0000000000..6ae8d59227 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Replies_ListMode.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_msg_replies.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_msg_replies@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Replies_ListMode.imageset/ic_msg_replies.png b/Telegram-Mac/Assets.xcassets/Icon_Replies_ListMode.imageset/ic_msg_replies.png new file mode 100644 index 0000000000..2c9bfdf04b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Replies_ListMode.imageset/ic_msg_replies.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Replies_ListMode.imageset/ic_msg_replies@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Replies_ListMode.imageset/ic_msg_replies@2x.png new file mode 100644 index 0000000000..c7ce684d7b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Replies_ListMode.imageset/ic_msg_replies@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Resort.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Resort.imageset/Contents.json new file mode 100644 index 0000000000..461e29e54e --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Resort.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_burger (1).png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_burger@2x (1).png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Resort.imageset/ic_burger (1).png b/Telegram-Mac/Assets.xcassets/Icon_Resort.imageset/ic_burger (1).png new file mode 100644 index 0000000000..f048be27b8 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Resort.imageset/ic_burger (1).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Resort.imageset/ic_burger@2x (1).png b/Telegram-Mac/Assets.xcassets/Icon_Resort.imageset/ic_burger@2x (1).png new file mode 100644 index 0000000000..80a00979a8 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Resort.imageset/ic_burger@2x (1).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SaveEditedMessage.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_SaveEditedMessage.imageset/Contents.json new file mode 100644 index 0000000000..73d7b507ad --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_SaveEditedMessage.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_savemessage@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_savemessage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_SaveEditedMessage.imageset/ic_savemessage@1x.png b/Telegram-Mac/Assets.xcassets/Icon_SaveEditedMessage.imageset/ic_savemessage@1x.png new file mode 100644 index 0000000000..4d2aea58cb Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SaveEditedMessage.imageset/ic_savemessage@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SaveEditedMessage.imageset/ic_savemessage@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SaveEditedMessage.imageset/ic_savemessage@2x.png new file mode 100644 index 0000000000..81b9747ae5 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SaveEditedMessage.imageset/ic_savemessage@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SavedMessages.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_SavedMessages.imageset/Contents.json new file mode 100644 index 0000000000..a13dd3987a --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_SavedMessages.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_saved@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_SavedMessages.imageset/ic_saved@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SavedMessages.imageset/ic_saved@2x.png new file mode 100644 index 0000000000..de460c24ee Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SavedMessages.imageset/ic_saved@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchArrow.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_SearchArrow.imageset/Contents.json index 64d50fcbc5..f3b87a4e05 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_SearchArrow.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_SearchArrow.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_up@1x.png", + "filename" : "ic_up.png", "scale" : "1x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchArrow.imageset/ic_up.png b/Telegram-Mac/Assets.xcassets/Icon_SearchArrow.imageset/ic_up.png new file mode 100644 index 0000000000..98cf7f692d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SearchArrow.imageset/ic_up.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchArrow.imageset/ic_up@1x.png b/Telegram-Mac/Assets.xcassets/Icon_SearchArrow.imageset/ic_up@1x.png deleted file mode 100644 index 773d6dfce5..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_SearchArrow.imageset/ic_up@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchArrow.imageset/ic_up@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SearchArrow.imageset/ic_up@2x.png index d96b2f1dfe..d08fb4fd7d 100644 Binary files a/Telegram-Mac/Assets.xcassets/Icon_SearchArrow.imageset/ic_up@2x.png and b/Telegram-Mac/Assets.xcassets/Icon_SearchArrow.imageset/ic_up@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchArticles.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_SearchArticles.imageset/Contents.json new file mode 100644 index 0000000000..7ba8c86a28 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_SearchArticles.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "articles@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "articles@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchArticles.imageset/articles@1x.png b/Telegram-Mac/Assets.xcassets/Icon_SearchArticles.imageset/articles@1x.png new file mode 100644 index 0000000000..df25970532 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SearchArticles.imageset/articles@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchArticles.imageset/articles@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SearchArticles.imageset/articles@2x.png new file mode 100644 index 0000000000..0d407780b4 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SearchArticles.imageset/articles@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchChatMessages.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_SearchChatMessages.imageset/Contents.json index a38faa7a64..11f30f94df 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_SearchChatMessages.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_SearchChatMessages.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_search@1x.png", + "filename" : "ic_search.png", "scale" : "1x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchChatMessages.imageset/ic_search.png b/Telegram-Mac/Assets.xcassets/Icon_SearchChatMessages.imageset/ic_search.png new file mode 100644 index 0000000000..b0cbd8d4cf Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SearchChatMessages.imageset/ic_search.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchChatMessages.imageset/ic_search@1x.png b/Telegram-Mac/Assets.xcassets/Icon_SearchChatMessages.imageset/ic_search@1x.png deleted file mode 100644 index 094757dd61..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_SearchChatMessages.imageset/ic_search@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchChatMessages.imageset/ic_search@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SearchChatMessages.imageset/ic_search@2x.png index b6a9957ba6..53f6ff1c7f 100644 Binary files a/Telegram-Mac/Assets.xcassets/Icon_SearchChatMessages.imageset/ic_search@2x.png and b/Telegram-Mac/Assets.xcassets/Icon_SearchChatMessages.imageset/ic_search@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchFilter.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter.imageset/Contents.json new file mode 100644 index 0000000000..6121d858b4 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_search_all.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_search_all@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchFilter.imageset/ic_search_all.png b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter.imageset/ic_search_all.png new file mode 100644 index 0000000000..1560340773 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter.imageset/ic_search_all.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchFilter.imageset/ic_search_all@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter.imageset/ic_search_all@2x.png new file mode 100644 index 0000000000..30863fb386 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter.imageset/ic_search_all@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_AddPeer.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_AddPeer.imageset/Contents.json new file mode 100644 index 0000000000..959a485f22 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_AddPeer.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_addresult.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_addresult@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_AddPeer.imageset/ic_addresult.png b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_AddPeer.imageset/ic_addresult.png new file mode 100644 index 0000000000..44b7f9552c Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_AddPeer.imageset/ic_addresult.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_AddPeer.imageset/ic_addresult@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_AddPeer.imageset/ic_addresult@2x.png new file mode 100644 index 0000000000..9d83ff9267 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_AddPeer.imageset/ic_addresult@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Files.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Files.imageset/Contents.json new file mode 100644 index 0000000000..486f07428b --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Files.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_search_docs.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_search_docs@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Files.imageset/ic_search_docs.png b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Files.imageset/ic_search_docs.png new file mode 100644 index 0000000000..0f7e53d743 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Files.imageset/ic_search_docs.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Files.imageset/ic_search_docs@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Files.imageset/ic_search_docs@2x.png new file mode 100644 index 0000000000..5bb391494e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Files.imageset/ic_search_docs@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Links.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Links.imageset/Contents.json new file mode 100644 index 0000000000..23a187c53e --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Links.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_search_links.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_search_links@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Links.imageset/ic_search_links.png b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Links.imageset/ic_search_links.png new file mode 100644 index 0000000000..76c679beaa Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Links.imageset/ic_search_links.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Links.imageset/ic_search_links@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Links.imageset/ic_search_links@2x.png new file mode 100644 index 0000000000..c102133749 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Links.imageset/ic_search_links@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Media.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Media.imageset/Contents.json new file mode 100644 index 0000000000..4eeabd8ebe --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Media.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_search_media.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_search_media@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Media.imageset/ic_search_media.png b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Media.imageset/ic_search_media.png new file mode 100644 index 0000000000..ceda33b4ba Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Media.imageset/ic_search_media.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Media.imageset/ic_search_media@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Media.imageset/ic_search_media@2x.png new file mode 100644 index 0000000000..660c4ca774 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Media.imageset/ic_search_media@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Music.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Music.imageset/Contents.json new file mode 100644 index 0000000000..2026bee7ca --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Music.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_search_music.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_search_music@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Music.imageset/ic_search_music.png b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Music.imageset/ic_search_music.png new file mode 100644 index 0000000000..bbfebc6ad7 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Music.imageset/ic_search_music.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Music.imageset/ic_search_music@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Music.imageset/ic_search_music@2x.png new file mode 100644 index 0000000000..7725cccccd Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SearchFilter_Music.imageset/ic_search_music@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchSaved.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_SearchSaved.imageset/Contents.json new file mode 100644 index 0000000000..3cbfca8778 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_SearchSaved.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "saved@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "saved@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchSaved.imageset/saved@1x.png b/Telegram-Mac/Assets.xcassets/Icon_SearchSaved.imageset/saved@1x.png new file mode 100644 index 0000000000..339f96aa7a Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SearchSaved.imageset/saved@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SearchSaved.imageset/saved@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SearchSaved.imageset/saved@2x.png new file mode 100644 index 0000000000..231b3dd946 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SearchSaved.imageset/saved@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Search_RemoveRecent.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Search_RemoveRecent.imageset/Contents.json new file mode 100644 index 0000000000..7d0a5c7404 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Search_RemoveRecent.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_clear.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_clear@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Search_RemoveRecent.imageset/ic_clear.png b/Telegram-Mac/Assets.xcassets/Icon_Search_RemoveRecent.imageset/ic_clear.png new file mode 100644 index 0000000000..5a4e860cb4 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Search_RemoveRecent.imageset/ic_clear.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Search_RemoveRecent.imageset/ic_clear@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Search_RemoveRecent.imageset/ic_clear@2x.png new file mode 100644 index 0000000000..c1aa9e31ea Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Search_RemoveRecent.imageset/ic_clear@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SecureIdAuth.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_SecureIdAuth.imageset/Contents.json new file mode 100644 index 0000000000..9c270bc1e7 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_SecureIdAuth.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_security@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_security@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_SecureIdAuth.imageset/ic_security@1x.png b/Telegram-Mac/Assets.xcassets/Icon_SecureIdAuth.imageset/ic_security@1x.png new file mode 100644 index 0000000000..93c52feaa8 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SecureIdAuth.imageset/ic_security@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SecureIdAuth.imageset/ic_security@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SecureIdAuth.imageset/ic_security@2x.png new file mode 100644 index 0000000000..892abd456c Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SecureIdAuth.imageset/ic_security@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SecureIdForgotPassword.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_SecureIdForgotPassword.imageset/Contents.json new file mode 100644 index 0000000000..3575172b26 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_SecureIdForgotPassword.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_fr@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_fr@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_SecureIdForgotPassword.imageset/ic_fr@1x.png b/Telegram-Mac/Assets.xcassets/Icon_SecureIdForgotPassword.imageset/ic_fr@1x.png new file mode 100644 index 0000000000..d9f1533b1a Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SecureIdForgotPassword.imageset/ic_fr@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SecureIdForgotPassword.imageset/ic_fr@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SecureIdForgotPassword.imageset/ic_fr@2x.png new file mode 100644 index 0000000000..a4e7c6f034 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SecureIdForgotPassword.imageset/ic_fr@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SendMessage.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_SendMessage.imageset/Contents.json index 876af2c1cf..3088d79699 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_SendMessage.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_SendMessage.imageset/Contents.json @@ -7,7 +7,7 @@ }, { "idiom" : "universal", - "filename" : "ic_send@2x.png", + "filename" : "ic_send@2x (1).png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_SendMessage.imageset/ic_send@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SendMessage.imageset/ic_send@2x (1).png similarity index 100% rename from Telegram-Mac/Assets.xcassets/Icon_SendMessage.imageset/ic_send@2x.png rename to Telegram-Mac/Assets.xcassets/Icon_SendMessage.imageset/ic_send@2x (1).png diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsAskQuestion.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_SettingsAskQuestion.imageset/Contents.json index ed3069f4af..aec0962898 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_SettingsAskQuestion.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_SettingsAskQuestion.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_askaq@1x.png", + "filename" : "ic_ask@1x.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ic_askaq@2x.png", + "filename" : "ic_ask@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsAskQuestion.imageset/ic_ask@1x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsAskQuestion.imageset/ic_ask@1x.png new file mode 100644 index 0000000000..4d70ab3b09 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SettingsAskQuestion.imageset/ic_ask@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsAskQuestion.imageset/ic_ask@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsAskQuestion.imageset/ic_ask@2x.png new file mode 100644 index 0000000000..ee959d17e5 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SettingsAskQuestion.imageset/ic_ask@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsAskQuestion.imageset/ic_askaq@1x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsAskQuestion.imageset/ic_askaq@1x.png deleted file mode 100644 index 4065953569..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_SettingsAskQuestion.imageset/ic_askaq@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsAskQuestion.imageset/ic_askaq@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsAskQuestion.imageset/ic_askaq@2x.png deleted file mode 100644 index 8e30e4a039..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_SettingsAskQuestion.imageset/ic_askaq@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsFaq.imageset/ic_faq@1x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsFaq.imageset/ic_faq@1x.png index 24486ac758..5ce9a73978 100644 Binary files a/Telegram-Mac/Assets.xcassets/Icon_SettingsFaq.imageset/ic_faq@1x.png and b/Telegram-Mac/Assets.xcassets/Icon_SettingsFaq.imageset/ic_faq@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsFaq.imageset/ic_faq@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsFaq.imageset/ic_faq@2x.png index 857fd457d7..899bf0ebcb 100644 Binary files a/Telegram-Mac/Assets.xcassets/Icon_SettingsFaq.imageset/ic_faq@2x.png and b/Telegram-Mac/Assets.xcassets/Icon_SettingsFaq.imageset/ic_faq@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsFilters.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_SettingsFilters.imageset/Contents.json new file mode 100644 index 0000000000..29b5593836 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_SettingsFilters.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_filters.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_filters@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsFilters.imageset/ic_filters.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsFilters.imageset/ic_filters.png new file mode 100644 index 0000000000..2e099b795b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SettingsFilters.imageset/ic_filters.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsFilters.imageset/ic_filters@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsFilters.imageset/ic_filters@2x.png new file mode 100644 index 0000000000..47a228e9d7 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SettingsFilters.imageset/ic_filters@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsGeneral.imageset/ic_general@1x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsGeneral.imageset/ic_general@1x.png index 51c9a9cd05..017043a430 100644 Binary files a/Telegram-Mac/Assets.xcassets/Icon_SettingsGeneral.imageset/ic_general@1x.png and b/Telegram-Mac/Assets.xcassets/Icon_SettingsGeneral.imageset/ic_general@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsGeneral.imageset/ic_general@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsGeneral.imageset/ic_general@2x.png index 4824e49e8c..a583fd4daf 100644 Binary files a/Telegram-Mac/Assets.xcassets/Icon_SettingsGeneral.imageset/ic_general@2x.png and b/Telegram-Mac/Assets.xcassets/Icon_SettingsGeneral.imageset/ic_general@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsLanguage.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_SettingsLanguage.imageset/Contents.json index 2b59027df0..dd141fc913 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_SettingsLanguage.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_SettingsLanguage.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_language@1x.png", + "filename" : "ic_lang@1x.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ic_language@2x.png", + "filename" : "ic_lang@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsLanguage.imageset/ic_lang@1x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsLanguage.imageset/ic_lang@1x.png new file mode 100644 index 0000000000..362134a53f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SettingsLanguage.imageset/ic_lang@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsLanguage.imageset/ic_lang@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsLanguage.imageset/ic_lang@2x.png new file mode 100644 index 0000000000..44bfb20085 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SettingsLanguage.imageset/ic_lang@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsLanguage.imageset/ic_language@1x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsLanguage.imageset/ic_language@1x.png deleted file mode 100644 index e9d0c218d2..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_SettingsLanguage.imageset/ic_language@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsLanguage.imageset/ic_language@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsLanguage.imageset/ic_language@2x.png deleted file mode 100644 index 510479d003..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_SettingsLanguage.imageset/ic_language@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsNotifications.imageset/ic_notifications@1x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsNotifications.imageset/ic_notifications@1x.png index bb3333c808..ad7c762007 100644 Binary files a/Telegram-Mac/Assets.xcassets/Icon_SettingsNotifications.imageset/ic_notifications@1x.png and b/Telegram-Mac/Assets.xcassets/Icon_SettingsNotifications.imageset/ic_notifications@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsNotifications.imageset/ic_notifications@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsNotifications.imageset/ic_notifications@2x.png index 41884afbec..9f92da3940 100644 Binary files a/Telegram-Mac/Assets.xcassets/Icon_SettingsNotifications.imageset/ic_notifications@2x.png and b/Telegram-Mac/Assets.xcassets/Icon_SettingsNotifications.imageset/ic_notifications@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsPassport.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_SettingsPassport.imageset/Contents.json new file mode 100644 index 0000000000..01c2f0b13c --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_SettingsPassport.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_passport@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_passport@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsPassport.imageset/ic_passport@1x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsPassport.imageset/ic_passport@1x.png new file mode 100644 index 0000000000..dbe59e9de2 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SettingsPassport.imageset/ic_passport@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsPassport.imageset/ic_passport@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsPassport.imageset/ic_passport@2x.png new file mode 100644 index 0000000000..42a78722e7 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SettingsPassport.imageset/ic_passport@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsProfile.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_SettingsProfile.imageset/Contents.json new file mode 100644 index 0000000000..049d903b6f --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_SettingsProfile.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_prof.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_prof@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsProfile.imageset/ic_prof.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsProfile.imageset/ic_prof.png new file mode 100644 index 0000000000..5201dbd9fb Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SettingsProfile.imageset/ic_prof.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsProfile.imageset/ic_prof@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsProfile.imageset/ic_prof@2x.png new file mode 100644 index 0000000000..07fae246ce Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SettingsProfile.imageset/ic_prof@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsProxy.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_SettingsProxy.imageset/Contents.json new file mode 100644 index 0000000000..06d896bc14 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_SettingsProxy.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_proxy@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_proxy@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsProxy.imageset/ic_proxy@1x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsProxy.imageset/ic_proxy@1x.png new file mode 100644 index 0000000000..66f04f2027 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SettingsProxy.imageset/ic_proxy@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsProxy.imageset/ic_proxy@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsProxy.imageset/ic_proxy@2x.png new file mode 100644 index 0000000000..ef30339064 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SettingsProxy.imageset/ic_proxy@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsSecurity.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_SettingsSecurity.imageset/Contents.json index 9c270bc1e7..fc32dbebed 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_SettingsSecurity.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_SettingsSecurity.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_security@1x.png", + "filename" : "ic_privacy@1x.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ic_security@2x.png", + "filename" : "ic_privacy@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsSecurity.imageset/ic_privacy@1x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsSecurity.imageset/ic_privacy@1x.png new file mode 100644 index 0000000000..95b72d77ed Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SettingsSecurity.imageset/ic_privacy@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsSecurity.imageset/ic_privacy@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsSecurity.imageset/ic_privacy@2x.png new file mode 100644 index 0000000000..37f2e44df7 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SettingsSecurity.imageset/ic_privacy@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsSecurity.imageset/ic_security@1x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsSecurity.imageset/ic_security@1x.png deleted file mode 100644 index da61970c43..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_SettingsSecurity.imageset/ic_security@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsSecurity.imageset/ic_security@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsSecurity.imageset/ic_security@2x.png deleted file mode 100644 index e5b8f534a4..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_SettingsSecurity.imageset/ic_security@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsStickers.imageset/ic_stickers@1x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsStickers.imageset/ic_stickers@1x.png index 946da83760..7ac2eeec66 100644 Binary files a/Telegram-Mac/Assets.xcassets/Icon_SettingsStickers.imageset/ic_stickers@1x.png and b/Telegram-Mac/Assets.xcassets/Icon_SettingsStickers.imageset/ic_stickers@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsStickers.imageset/ic_stickers@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsStickers.imageset/ic_stickers@2x.png index 655b51c80a..cd444da4e5 100644 Binary files a/Telegram-Mac/Assets.xcassets/Icon_SettingsStickers.imageset/ic_stickers@2x.png and b/Telegram-Mac/Assets.xcassets/Icon_SettingsStickers.imageset/ic_stickers@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsStorage.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_SettingsStorage.imageset/Contents.json index 646741b1b1..af11311f7d 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_SettingsStorage.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_SettingsStorage.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_storage@1x.png", + "filename" : "ic_data@1x.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ic_storage@2x.png", + "filename" : "ic_data@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsStorage.imageset/ic_data@1x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsStorage.imageset/ic_data@1x.png new file mode 100644 index 0000000000..3b44a3f146 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SettingsStorage.imageset/ic_data@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsStorage.imageset/ic_data@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsStorage.imageset/ic_data@2x.png new file mode 100644 index 0000000000..75882f04b8 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SettingsStorage.imageset/ic_data@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsStorage.imageset/ic_storage@1x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsStorage.imageset/ic_storage@1x.png deleted file mode 100644 index 261e0e1e84..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_SettingsStorage.imageset/ic_storage@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsStorage.imageset/ic_storage@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsStorage.imageset/ic_storage@2x.png deleted file mode 100644 index 78aeba3a22..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_SettingsStorage.imageset/ic_storage@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsUpdate.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_SettingsUpdate.imageset/Contents.json new file mode 100644 index 0000000000..e658e397c0 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_SettingsUpdate.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "update (1).png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "update@2x (1).png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsUpdate.imageset/update (1).png b/Telegram-Mac/Assets.xcassets/Icon_SettingsUpdate.imageset/update (1).png new file mode 100644 index 0000000000..c4abb15cd2 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SettingsUpdate.imageset/update (1).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsUpdate.imageset/update@2x (1).png b/Telegram-Mac/Assets.xcassets/Icon_SettingsUpdate.imageset/update@2x (1).png new file mode 100644 index 0000000000..2f5efe3e13 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SettingsUpdate.imageset/update@2x (1).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsWallet.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_SettingsWallet.imageset/Contents.json new file mode 100644 index 0000000000..26bf697314 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_SettingsWallet.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_wallet.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_wallet@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsWallet.imageset/ic_wallet.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsWallet.imageset/ic_wallet.png new file mode 100644 index 0000000000..f2d4d738e9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SettingsWallet.imageset/ic_wallet.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_SettingsWallet.imageset/ic_wallet@2x.png b/Telegram-Mac/Assets.xcassets/Icon_SettingsWallet.imageset/ic_wallet@2x.png new file mode 100644 index 0000000000..641bc754ad Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_SettingsWallet.imageset/ic_wallet@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ShareExternal.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ShareExternal.imageset/Contents.json index 2040e083a9..80acc0806f 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_ShareExternal.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_ShareExternal.imageset/Contents.json @@ -2,11 +2,12 @@ "images" : [ { "idiom" : "universal", + "filename" : "ic_share.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ShareExternalIcon@2x.png", + "filename" : "ic_share@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_ShareExternal.imageset/ShareExternalIcon@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ShareExternal.imageset/ShareExternalIcon@2x.png deleted file mode 100644 index db4f34d225..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_ShareExternal.imageset/ShareExternalIcon@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ShareExternal.imageset/ic_share.png b/Telegram-Mac/Assets.xcassets/Icon_ShareExternal.imageset/ic_share.png new file mode 100644 index 0000000000..b6740770a9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ShareExternal.imageset/ic_share.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ShareExternal.imageset/ic_share@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ShareExternal.imageset/ic_share@2x.png new file mode 100644 index 0000000000..a678914461 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ShareExternal.imageset/ic_share@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ShareStickerPack.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ShareStickerPack.imageset/Contents.json index 899e0ebee9..80acc0806f 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_ShareStickerPack.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_ShareStickerPack.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_share@1x.png", + "filename" : "ic_share.png", "scale" : "1x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_ShareStickerPack.imageset/ic_share.png b/Telegram-Mac/Assets.xcassets/Icon_ShareStickerPack.imageset/ic_share.png new file mode 100644 index 0000000000..b6740770a9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ShareStickerPack.imageset/ic_share.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ShareStickerPack.imageset/ic_share@1x.png b/Telegram-Mac/Assets.xcassets/Icon_ShareStickerPack.imageset/ic_share@1x.png deleted file mode 100644 index dcbe6491e5..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_ShareStickerPack.imageset/ic_share@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ShareStickerPack.imageset/ic_share@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ShareStickerPack.imageset/ic_share@2x.png index 47e14f1567..a678914461 100644 Binary files a/Telegram-Mac/Assets.xcassets/Icon_ShareStickerPack.imageset/ic_share@2x.png and b/Telegram-Mac/Assets.xcassets/Icon_ShareStickerPack.imageset/ic_share@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_AllChats.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_AllChats.imageset/Contents.json new file mode 100644 index 0000000000..58d6ea74ef --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_AllChats.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_allchats.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_allchats@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_AllChats.imageset/ic_allchats.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_AllChats.imageset/ic_allchats.png new file mode 100644 index 0000000000..d5731c2a46 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_AllChats.imageset/ic_allchats.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_AllChats.imageset/ic_allchats@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_AllChats.imageset/ic_allchats@2x.png new file mode 100644 index 0000000000..4f0ccf241c Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_AllChats.imageset/ic_allchats@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Animal.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Animal.imageset/Contents.json new file mode 100644 index 0000000000..e1b68ea24c --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Animal.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_animal.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_animal@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Animal.imageset/ic_animal.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Animal.imageset/ic_animal.png new file mode 100644 index 0000000000..f8e3a75132 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Animal.imageset/ic_animal.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Animal.imageset/ic_animal@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Animal.imageset/ic_animal@2x.png new file mode 100644 index 0000000000..b17d52149b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Animal.imageset/ic_animal@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Book.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Book.imageset/Contents.json new file mode 100644 index 0000000000..274eccd216 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Book.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_book.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_book@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Book.imageset/ic_book.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Book.imageset/ic_book.png new file mode 100644 index 0000000000..497ae2314f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Book.imageset/ic_book.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Book.imageset/ic_book@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Book.imageset/ic_book@2x.png new file mode 100644 index 0000000000..6c1a276be7 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Book.imageset/ic_book@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Bot.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Bot.imageset/Contents.json new file mode 100644 index 0000000000..501805ac2a --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Bot.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_bot.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_bot@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Bot.imageset/ic_bot.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Bot.imageset/ic_bot.png new file mode 100644 index 0000000000..f96a4d9eb0 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Bot.imageset/ic_bot.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Bot.imageset/ic_bot@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Bot.imageset/ic_bot@2x.png new file mode 100644 index 0000000000..e66764359e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Bot.imageset/ic_bot@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Channel.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Channel.imageset/Contents.json new file mode 100644 index 0000000000..2595db3b7d --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Channel.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_channel.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_channel@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Channel.imageset/ic_channel.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Channel.imageset/ic_channel.png new file mode 100644 index 0000000000..98dfbe8442 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Channel.imageset/ic_channel.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Channel.imageset/ic_channel@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Channel.imageset/ic_channel@2x.png new file mode 100644 index 0000000000..2363b1e987 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Channel.imageset/ic_channel@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Coin.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Coin.imageset/Contents.json new file mode 100644 index 0000000000..c96a6bdf24 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Coin.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_coin.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_coin@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Coin.imageset/ic_coin.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Coin.imageset/ic_coin.png new file mode 100644 index 0000000000..48d4c4155d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Coin.imageset/ic_coin.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Coin.imageset/ic_coin@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Coin.imageset/ic_coin@2x.png new file mode 100644 index 0000000000..d56ff466b9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Coin.imageset/ic_coin@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Flash.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Flash.imageset/Contents.json new file mode 100644 index 0000000000..f9a5641ffe --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Flash.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_flash.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_flash@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Flash.imageset/ic_flash.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Flash.imageset/ic_flash.png new file mode 100644 index 0000000000..19cc5358c2 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Flash.imageset/ic_flash.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Flash.imageset/ic_flash@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Flash.imageset/ic_flash@2x.png new file mode 100644 index 0000000000..5c2989c4d8 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Flash.imageset/ic_flash@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Folder.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Folder.imageset/Contents.json new file mode 100644 index 0000000000..14ccd7f4fb --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Folder.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_folder.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_folder@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Folder.imageset/ic_folder.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Folder.imageset/ic_folder.png new file mode 100644 index 0000000000..bf6ee9f11b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Folder.imageset/ic_folder.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Folder.imageset/ic_folder@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Folder.imageset/ic_folder@2x.png new file mode 100644 index 0000000000..70c602b8a3 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Folder.imageset/ic_folder@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Game.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Game.imageset/Contents.json new file mode 100644 index 0000000000..9556655b27 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Game.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_gamepad.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_gamepad@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Game.imageset/ic_gamepad.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Game.imageset/ic_gamepad.png new file mode 100644 index 0000000000..cf8ddbdfd2 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Game.imageset/ic_gamepad.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Game.imageset/ic_gamepad@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Game.imageset/ic_gamepad@2x.png new file mode 100644 index 0000000000..904b10ffb6 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Game.imageset/ic_gamepad@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Group.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Group.imageset/Contents.json new file mode 100644 index 0000000000..152058a20c --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Group.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_group.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_group@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Group.imageset/ic_group.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Group.imageset/ic_group.png new file mode 100644 index 0000000000..62afae7802 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Group.imageset/ic_group.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Group.imageset/ic_group@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Group.imageset/ic_group@2x.png new file mode 100644 index 0000000000..bdafd3df4a Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Group.imageset/ic_group@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Home.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Home.imageset/Contents.json new file mode 100644 index 0000000000..6a813c7971 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Home.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_home.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_home@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Home.imageset/ic_home.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Home.imageset/ic_home.png new file mode 100644 index 0000000000..d60e3c8ff0 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Home.imageset/ic_home.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Home.imageset/ic_home@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Home.imageset/ic_home@2x.png new file mode 100644 index 0000000000..a70943b721 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Home.imageset/ic_home@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Lamp.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Lamp.imageset/Contents.json new file mode 100644 index 0000000000..c60505c37b --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Lamp.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_lamp.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_lamp@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Lamp.imageset/ic_lamp.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Lamp.imageset/ic_lamp.png new file mode 100644 index 0000000000..d8662163a7 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Lamp.imageset/ic_lamp.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Lamp.imageset/ic_lamp@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Lamp.imageset/ic_lamp@2x.png new file mode 100644 index 0000000000..985bf56e57 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Lamp.imageset/ic_lamp@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Like.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Like.imageset/Contents.json new file mode 100644 index 0000000000..2e8a5cea30 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Like.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_like.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_like@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Like.imageset/ic_like.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Like.imageset/ic_like.png new file mode 100644 index 0000000000..b87dd5168a Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Like.imageset/ic_like.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Like.imageset/ic_like@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Like.imageset/ic_like@2x.png new file mode 100644 index 0000000000..7ad91954f3 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Like.imageset/ic_like@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Lock.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Lock.imageset/Contents.json new file mode 100644 index 0000000000..d09de3c8f2 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Lock.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_lock.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_lock@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Lock.imageset/ic_lock.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Lock.imageset/ic_lock.png new file mode 100644 index 0000000000..a15099cda7 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Lock.imageset/ic_lock.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Lock.imageset/ic_lock@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Lock.imageset/ic_lock@2x.png new file mode 100644 index 0000000000..ef4c554b32 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Lock.imageset/ic_lock@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Love.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Love.imageset/Contents.json new file mode 100644 index 0000000000..c860da1d72 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Love.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_love.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_love@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Love.imageset/ic_love.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Love.imageset/ic_love.png new file mode 100644 index 0000000000..5457d04fb2 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Love.imageset/ic_love.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Love.imageset/ic_love@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Love.imageset/ic_love@2x.png new file mode 100644 index 0000000000..73250b3c43 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Love.imageset/ic_love@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Mask.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Mask.imageset/Contents.json new file mode 100644 index 0000000000..c9c9e7dc18 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Mask.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_mask.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_mask@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Mask.imageset/ic_mask.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Mask.imageset/ic_mask.png new file mode 100644 index 0000000000..838276322a Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Mask.imageset/ic_mask.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Mask.imageset/ic_mask@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Mask.imageset/ic_mask@2x.png new file mode 100644 index 0000000000..77f7e73b60 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Mask.imageset/ic_mask@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Math.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Math.imageset/Contents.json new file mode 100644 index 0000000000..b52fb82ad9 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Math.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_math.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_math@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Math.imageset/ic_math.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Math.imageset/ic_math.png new file mode 100644 index 0000000000..93920e7b88 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Math.imageset/ic_math.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Math.imageset/ic_math@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Math.imageset/ic_math@2x.png new file mode 100644 index 0000000000..9b1e7a095a Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Math.imageset/ic_math@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Music.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Music.imageset/Contents.json new file mode 100644 index 0000000000..53454b7c78 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Music.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_nusic.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_nusic@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Music.imageset/ic_nusic.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Music.imageset/ic_nusic.png new file mode 100644 index 0000000000..e6c4ebb26f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Music.imageset/ic_nusic.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Music.imageset/ic_nusic@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Music.imageset/ic_nusic@2x.png new file mode 100644 index 0000000000..f5053ea01b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Music.imageset/ic_nusic@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Muted.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Muted.imageset/Contents.json new file mode 100644 index 0000000000..70063b6f29 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Muted.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_muted.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_muted@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Muted.imageset/ic_muted.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Muted.imageset/ic_muted.png new file mode 100644 index 0000000000..fbd6c02ac5 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Muted.imageset/ic_muted.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Muted.imageset/ic_muted@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Muted.imageset/ic_muted@2x.png new file mode 100644 index 0000000000..7e63de96bc Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Muted.imageset/ic_muted@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Paint.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Paint.imageset/Contents.json new file mode 100644 index 0000000000..b3f33de944 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Paint.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_paint.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_paint@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Paint.imageset/ic_paint.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Paint.imageset/ic_paint.png new file mode 100644 index 0000000000..e4e5e846d9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Paint.imageset/ic_paint.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Paint.imageset/ic_paint@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Paint.imageset/ic_paint@2x.png new file mode 100644 index 0000000000..eeaf621fbe Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Paint.imageset/ic_paint@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Personal.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Personal.imageset/Contents.json new file mode 100644 index 0000000000..b773bac788 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Personal.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_personal.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_personal@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Personal.imageset/ic_personal.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Personal.imageset/ic_personal.png new file mode 100644 index 0000000000..8b48dcfba1 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Personal.imageset/ic_personal.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Personal.imageset/ic_personal@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Personal.imageset/ic_personal@2x.png new file mode 100644 index 0000000000..cbb88ce31a Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Personal.imageset/ic_personal@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Plane.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Plane.imageset/Contents.json new file mode 100644 index 0000000000..68e233fd83 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Plane.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_plane.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_plane@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Plane.imageset/ic_plane.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Plane.imageset/ic_plane.png new file mode 100644 index 0000000000..20e6ef5b80 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Plane.imageset/ic_plane.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Plane.imageset/ic_plane@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Plane.imageset/ic_plane@2x.png new file mode 100644 index 0000000000..6d3fd0d6fd Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Plane.imageset/ic_plane@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Read.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Read.imageset/Contents.json new file mode 100644 index 0000000000..39a49c5b0b --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Read.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_read.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_read@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Read.imageset/ic_read.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Read.imageset/ic_read.png new file mode 100644 index 0000000000..e6117bbd1a Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Read.imageset/ic_read.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Read.imageset/ic_read@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Read.imageset/ic_read@2x.png new file mode 100644 index 0000000000..caa5bec8a6 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Read.imageset/ic_read@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Sport.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Sport.imageset/Contents.json new file mode 100644 index 0000000000..60f2dd0cf5 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Sport.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_sport.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_sport@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Sport.imageset/ic_sport.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Sport.imageset/ic_sport.png new file mode 100644 index 0000000000..c9aade47af Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Sport.imageset/ic_sport.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Sport.imageset/ic_sport@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Sport.imageset/ic_sport@2x.png new file mode 100644 index 0000000000..e11a2ca6d1 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Sport.imageset/ic_sport@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Star.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Star.imageset/Contents.json new file mode 100644 index 0000000000..ed649ccf18 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Star.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_star.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_star@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Star.imageset/ic_star.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Star.imageset/ic_star.png new file mode 100644 index 0000000000..5484dceaa0 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Star.imageset/ic_star.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Star.imageset/ic_star@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Star.imageset/ic_star@2x.png new file mode 100644 index 0000000000..993885e6ae Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Star.imageset/ic_star@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Student.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Student.imageset/Contents.json new file mode 100644 index 0000000000..03d8bea9d4 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Student.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_student.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_student@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Student.imageset/ic_student.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Student.imageset/ic_student.png new file mode 100644 index 0000000000..aa1515bb54 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Student.imageset/ic_student.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Student.imageset/ic_student@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Student.imageset/ic_student@2x.png new file mode 100644 index 0000000000..94231d8a64 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Student.imageset/ic_student@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Telegram.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Telegram.imageset/Contents.json new file mode 100644 index 0000000000..a62f7b3bcb --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Telegram.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_telegram.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_telegram@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Telegram.imageset/ic_telegram.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Telegram.imageset/ic_telegram.png new file mode 100644 index 0000000000..5be8517c6c Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Telegram.imageset/ic_telegram.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Telegram.imageset/ic_telegram@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Telegram.imageset/ic_telegram@2x.png new file mode 100644 index 0000000000..12b4fb500b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Telegram.imageset/ic_telegram@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Unmuted.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Unmuted.imageset/Contents.json new file mode 100644 index 0000000000..3d4d31a176 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Unmuted.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_unmuted.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_unmuted@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Unmuted.imageset/ic_unmuted.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Unmuted.imageset/ic_unmuted.png new file mode 100644 index 0000000000..926bc050c6 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Unmuted.imageset/ic_unmuted.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Unmuted.imageset/ic_unmuted@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Unmuted.imageset/ic_unmuted@2x.png new file mode 100644 index 0000000000..ec8a9928fc Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Unmuted.imageset/ic_unmuted@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Unread.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Unread.imageset/Contents.json new file mode 100644 index 0000000000..0fc3beb1c2 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Unread.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_unread.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_unread@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Unread.imageset/ic_unread.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Unread.imageset/ic_unread.png new file mode 100644 index 0000000000..74330f46ef Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Unread.imageset/ic_unread.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Unread.imageset/ic_unread@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Unread.imageset/ic_unread@2x.png new file mode 100644 index 0000000000..09e4b8b279 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Unread.imageset/ic_unread@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Virus.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Virus.imageset/Contents.json new file mode 100644 index 0000000000..2aed4f0c13 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Virus.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_flow.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_flow@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Virus.imageset/ic_flow.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Virus.imageset/ic_flow.png new file mode 100644 index 0000000000..6adea7e5ef Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Virus.imageset/ic_flow.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Virus.imageset/ic_flow@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Virus.imageset/ic_flow@2x.png new file mode 100644 index 0000000000..b4a4cc5ce9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Virus.imageset/ic_flow@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Wine.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Wine.imageset/Contents.json new file mode 100644 index 0000000000..5e7e083c62 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Wine.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_wine.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_wine@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Wine.imageset/ic_wine.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Wine.imageset/ic_wine.png new file mode 100644 index 0000000000..c03638271b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Wine.imageset/ic_wine.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Wine.imageset/ic_wine@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Wine.imageset/ic_wine@2x.png new file mode 100644 index 0000000000..00e62417e9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Wine.imageset/ic_wine@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Work.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Work.imageset/Contents.json new file mode 100644 index 0000000000..591bbb841e --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Work.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_work.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_work@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Work.imageset/ic_work.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Work.imageset/ic_work.png new file mode 100644 index 0000000000..5026f0dd7d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Work.imageset/ic_work.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Work.imageset/ic_work@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Work.imageset/ic_work@2x.png new file mode 100644 index 0000000000..2b75a42837 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Sidebar_Work.imageset/ic_work@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Slider.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Slider.imageset/Contents.json new file mode 100644 index 0000000000..bb5459de83 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Slider.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Oval (1).png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Oval@2x (1).png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_Slider.imageset/Oval (1).png b/Telegram-Mac/Assets.xcassets/Icon_Slider.imageset/Oval (1).png new file mode 100644 index 0000000000..43406df4e1 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Slider.imageset/Oval (1).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Slider.imageset/Oval@2x (1).png b/Telegram-Mac/Assets.xcassets/Icon_Slider.imageset/Oval@2x (1).png new file mode 100644 index 0000000000..263b4d1b90 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Slider.imageset/Oval@2x (1).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_StreamingDownload.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_StreamingDownload.imageset/Contents.json new file mode 100644 index 0000000000..f5fbbe94c3 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_StreamingDownload.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Download.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Download@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_StreamingDownload.imageset/Download.png b/Telegram-Mac/Assets.xcassets/Icon_StreamingDownload.imageset/Download.png new file mode 100644 index 0000000000..ccb3725969 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_StreamingDownload.imageset/Download.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_StreamingDownload.imageset/Download@2x.png b/Telegram-Mac/Assets.xcassets/Icon_StreamingDownload.imageset/Download@2x.png new file mode 100644 index 0000000000..b090cb549f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_StreamingDownload.imageset/Download@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabChatList.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TabChatList.imageset/Contents.json index a48b454e28..8f7c7eb224 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_TabChatList.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_TabChatList.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_chats_1@1x.png", + "filename" : "ic_chats.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ic_chats_1@2x.png", + "filename" : "ic_chats@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabChatList.imageset/ic_chats.png b/Telegram-Mac/Assets.xcassets/Icon_TabChatList.imageset/ic_chats.png new file mode 100644 index 0000000000..9370cf5135 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TabChatList.imageset/ic_chats.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabChatList.imageset/ic_chats@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TabChatList.imageset/ic_chats@2x.png new file mode 100644 index 0000000000..2715b079b0 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TabChatList.imageset/ic_chats@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabChatList.imageset/ic_chats_1@1x.png b/Telegram-Mac/Assets.xcassets/Icon_TabChatList.imageset/ic_chats_1@1x.png deleted file mode 100644 index 0ca85775b4..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_TabChatList.imageset/ic_chats_1@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabChatList.imageset/ic_chats_1@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TabChatList.imageset/ic_chats_1@2x.png deleted file mode 100644 index e3829970c1..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_TabChatList.imageset/ic_chats_1@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabChatList_Highlighted.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TabChatList_Highlighted.imageset/Contents.json index ff26a57346..8f7c7eb224 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_TabChatList_Highlighted.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_TabChatList_Highlighted.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_chats_2@1x.png", + "filename" : "ic_chats.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ic_chats_2@2x.png", + "filename" : "ic_chats@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabChatList_Highlighted.imageset/ic_chats.png b/Telegram-Mac/Assets.xcassets/Icon_TabChatList_Highlighted.imageset/ic_chats.png new file mode 100644 index 0000000000..9370cf5135 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TabChatList_Highlighted.imageset/ic_chats.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabChatList_Highlighted.imageset/ic_chats@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TabChatList_Highlighted.imageset/ic_chats@2x.png new file mode 100644 index 0000000000..2715b079b0 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TabChatList_Highlighted.imageset/ic_chats@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabChatList_Highlighted.imageset/ic_chats_2@1x.png b/Telegram-Mac/Assets.xcassets/Icon_TabChatList_Highlighted.imageset/ic_chats_2@1x.png deleted file mode 100644 index 99cf338aaf..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_TabChatList_Highlighted.imageset/ic_chats_2@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabChatList_Highlighted.imageset/ic_chats_2@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TabChatList_Highlighted.imageset/ic_chats_2@2x.png deleted file mode 100644 index 6a177b96a2..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_TabChatList_Highlighted.imageset/ic_chats_2@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabContacts.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TabContacts.imageset/Contents.json index 2d057742d9..6d3745d5ec 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_TabContacts.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_TabContacts.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_contacts_1@1x.png", + "filename" : "ic_contacts.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ic_contacts_1@2x.png", + "filename" : "ic_contacts@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabContacts.imageset/ic_contacts.png b/Telegram-Mac/Assets.xcassets/Icon_TabContacts.imageset/ic_contacts.png new file mode 100644 index 0000000000..37b6eb9c95 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TabContacts.imageset/ic_contacts.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabContacts.imageset/ic_contacts@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TabContacts.imageset/ic_contacts@2x.png new file mode 100644 index 0000000000..65e44c6a3e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TabContacts.imageset/ic_contacts@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabContacts.imageset/ic_contacts_1@1x.png b/Telegram-Mac/Assets.xcassets/Icon_TabContacts.imageset/ic_contacts_1@1x.png deleted file mode 100644 index 99e010ace8..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_TabContacts.imageset/ic_contacts_1@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabContacts.imageset/ic_contacts_1@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TabContacts.imageset/ic_contacts_1@2x.png deleted file mode 100644 index 0fd0b01ce0..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_TabContacts.imageset/ic_contacts_1@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabContacts_Highlighted.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TabContacts_Highlighted.imageset/Contents.json deleted file mode 100644 index 25e4e58776..0000000000 --- a/Telegram-Mac/Assets.xcassets/Icon_TabContacts_Highlighted.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "ic_contacts_2@1x.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "ic_contacts_2@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabContacts_Highlighted.imageset/ic_contacts_2@1x.png b/Telegram-Mac/Assets.xcassets/Icon_TabContacts_Highlighted.imageset/ic_contacts_2@1x.png deleted file mode 100644 index eda01248f0..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_TabContacts_Highlighted.imageset/ic_contacts_2@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabContacts_Highlighted.imageset/ic_contacts_2@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TabContacts_Highlighted.imageset/ic_contacts_2@2x.png deleted file mode 100644 index dc03a4ef21..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_TabContacts_Highlighted.imageset/ic_contacts_2@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabRecentCalls.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TabRecentCalls.imageset/Contents.json index b24728a360..45a0013e5d 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_TabRecentCalls.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_TabRecentCalls.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_calls_1@1x.png", + "filename" : "ic_calls.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ic_calls_1@2x.png", + "filename" : "ic_calls@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabRecentCalls.imageset/ic_calls.png b/Telegram-Mac/Assets.xcassets/Icon_TabRecentCalls.imageset/ic_calls.png new file mode 100644 index 0000000000..beaa6d49a7 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TabRecentCalls.imageset/ic_calls.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabRecentCalls.imageset/ic_calls@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TabRecentCalls.imageset/ic_calls@2x.png new file mode 100644 index 0000000000..4baf298c68 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TabRecentCalls.imageset/ic_calls@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabRecentCalls.imageset/ic_calls_1@1x.png b/Telegram-Mac/Assets.xcassets/Icon_TabRecentCalls.imageset/ic_calls_1@1x.png deleted file mode 100644 index 83781c5aab..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_TabRecentCalls.imageset/ic_calls_1@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabRecentCalls.imageset/ic_calls_1@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TabRecentCalls.imageset/ic_calls_1@2x.png deleted file mode 100644 index 95810c6a0b..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_TabRecentCalls.imageset/ic_calls_1@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabRecentCallsHighlighted.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TabRecentCallsHighlighted.imageset/Contents.json deleted file mode 100644 index cda2d9a025..0000000000 --- a/Telegram-Mac/Assets.xcassets/Icon_TabRecentCallsHighlighted.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "ic_calls_2@1x.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "ic_calls_2@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabRecentCallsHighlighted.imageset/ic_calls_2@1x.png b/Telegram-Mac/Assets.xcassets/Icon_TabRecentCallsHighlighted.imageset/ic_calls_2@1x.png deleted file mode 100644 index 48c80fef53..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_TabRecentCallsHighlighted.imageset/ic_calls_2@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabRecentCallsHighlighted.imageset/ic_calls_2@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TabRecentCallsHighlighted.imageset/ic_calls_2@2x.png deleted file mode 100644 index 819b882a6a..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_TabRecentCallsHighlighted.imageset/ic_calls_2@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabSettings.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TabSettings.imageset/Contents.json index 5f66cf2b16..25de093c56 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_TabSettings.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_TabSettings.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_settings_1@1x.png", + "filename" : "ic_settings.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "ic_settings_1@2x.png", + "filename" : "ic_settings@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabSettings.imageset/ic_settings.png b/Telegram-Mac/Assets.xcassets/Icon_TabSettings.imageset/ic_settings.png new file mode 100644 index 0000000000..2dffab7701 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TabSettings.imageset/ic_settings.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabSettings.imageset/ic_settings@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TabSettings.imageset/ic_settings@2x.png new file mode 100644 index 0000000000..70f1196940 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TabSettings.imageset/ic_settings@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabSettings.imageset/ic_settings_1@1x.png b/Telegram-Mac/Assets.xcassets/Icon_TabSettings.imageset/ic_settings_1@1x.png deleted file mode 100644 index cd0e99956f..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_TabSettings.imageset/ic_settings_1@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabSettings.imageset/ic_settings_1@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TabSettings.imageset/ic_settings_1@2x.png deleted file mode 100644 index 1acafe38c4..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_TabSettings.imageset/ic_settings_1@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabSettings_Highlighted.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TabSettings_Highlighted.imageset/Contents.json deleted file mode 100644 index 3f0eb96621..0000000000 --- a/Telegram-Mac/Assets.xcassets/Icon_TabSettings_Highlighted.imageset/Contents.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "ic_settings_2@1x.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "ic_settings_2@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabSettings_Highlighted.imageset/ic_settings_2@1x.png b/Telegram-Mac/Assets.xcassets/Icon_TabSettings_Highlighted.imageset/ic_settings_2@1x.png deleted file mode 100644 index 5a36e0cf80..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_TabSettings_Highlighted.imageset/ic_settings_2@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TabSettings_Highlighted.imageset/ic_settings_2@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TabSettings_Highlighted.imageset/ic_settings_2@2x.png deleted file mode 100644 index 180d7240ae..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_TabSettings_Highlighted.imageset/ic_settings_2@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ThemeBubble.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ThemeBubble.imageset/Contents.json new file mode 100644 index 0000000000..17302c16da --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ThemeBubble.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "themebubble@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_ThemeBubble.imageset/themebubble@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ThemeBubble.imageset/themebubble@2x.png new file mode 100644 index 0000000000..17c90b1ce7 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ThemeBubble.imageset/themebubble@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ThemePreview_Dino.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ThemePreview_Dino.imageset/Contents.json new file mode 100644 index 0000000000..8927f67c68 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ThemePreview_Dino.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "preview_dino.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "preview_dino@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_ThemePreview_Dino.imageset/preview_dino.png b/Telegram-Mac/Assets.xcassets/Icon_ThemePreview_Dino.imageset/preview_dino.png new file mode 100644 index 0000000000..c44b5fa8c7 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ThemePreview_Dino.imageset/preview_dino.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ThemePreview_Dino.imageset/preview_dino@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ThemePreview_Dino.imageset/preview_dino@2x.png new file mode 100644 index 0000000000..528420247f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ThemePreview_Dino.imageset/preview_dino@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ThemePreview_Duck.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_ThemePreview_Duck.imageset/Contents.json new file mode 100644 index 0000000000..bbf2a17da4 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_ThemePreview_Duck.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "preview_duck.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "preview_duck@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_ThemePreview_Duck.imageset/preview_duck.png b/Telegram-Mac/Assets.xcassets/Icon_ThemePreview_Duck.imageset/preview_duck.png new file mode 100644 index 0000000000..c09266ae75 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ThemePreview_Duck.imageset/preview_duck.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_ThemePreview_Duck.imageset/preview_duck@2x.png b/Telegram-Mac/Assets.xcassets/Icon_ThemePreview_Duck.imageset/preview_duck@2x.png new file mode 100644 index 0000000000..089b28e47e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_ThemePreview_Duck.imageset/preview_duck@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBarBackgroundIcon.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBarBackgroundIcon.imageset/Contents.json new file mode 100644 index 0000000000..1f6801c0a0 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBarBackgroundIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "logo@1x-1.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "logo@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBarBackgroundIcon.imageset/logo@1x-1.png b/Telegram-Mac/Assets.xcassets/Icon_TouchBarBackgroundIcon.imageset/logo@1x-1.png new file mode 100644 index 0000000000..36dd72d033 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBarBackgroundIcon.imageset/logo@1x-1.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBarBackgroundIcon.imageset/logo@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TouchBarBackgroundIcon.imageset/logo@2x.png new file mode 100644 index 0000000000..d9ed1f0f87 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBarBackgroundIcon.imageset/logo@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_AttachFile.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_AttachFile.imageset/Contents.json new file mode 100644 index 0000000000..6d782f5fb0 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_AttachFile.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "document@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_AttachFile.imageset/document@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_AttachFile.imageset/document@2x.png new file mode 100644 index 0000000000..7df2920e28 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_AttachFile.imageset/document@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_AttachLocation.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_AttachLocation.imageset/Contents.json new file mode 100644 index 0000000000..8017d3fb31 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_AttachLocation.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "location@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_AttachLocation.imageset/location@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_AttachLocation.imageset/location@2x.png new file mode 100644 index 0000000000..2302a706ef Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_AttachLocation.imageset/location@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_AttachPhotoOrVideo.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_AttachPhotoOrVideo.imageset/Contents.json new file mode 100644 index 0000000000..a23e5d8efb --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_AttachPhotoOrVideo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "photo@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_AttachPhotoOrVideo.imageset/photo@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_AttachPhotoOrVideo.imageset/photo@2x.png new file mode 100644 index 0000000000..9862baf685 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_AttachPhotoOrVideo.imageset/photo@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_AttachPicture.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_AttachPicture.imageset/Contents.json new file mode 100644 index 0000000000..cc7769e2a4 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_AttachPicture.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "camera@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_AttachPicture.imageset/camera@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_AttachPicture.imageset/camera@2x.png new file mode 100644 index 0000000000..27847be5c4 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_AttachPicture.imageset/camera@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Call.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Call.imageset/Contents.json new file mode 100644 index 0000000000..6e91e28d90 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Call.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "call@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Call.imageset/call@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Call.imageset/call@2x.png new file mode 100644 index 0000000000..616978640d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Call.imageset/call@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ChatAttach.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ChatAttach.imageset/Contents.json new file mode 100644 index 0000000000..5d34c268a2 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ChatAttach.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "attach@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ChatAttach.imageset/attach@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ChatAttach.imageset/attach@2x.png new file mode 100644 index 0000000000..13f1d3018f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ChatAttach.imageset/attach@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ChatMore.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ChatMore.imageset/Contents.json new file mode 100644 index 0000000000..5fead88de4 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ChatMore.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "more@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ChatMore.imageset/more@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ChatMore.imageset/more@2x.png new file mode 100644 index 0000000000..b1c599688a Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ChatMore.imageset/more@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Compose.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Compose.imageset/Contents.json new file mode 100644 index 0000000000..6f54ddaa6b --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Compose.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "newchat@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Compose.imageset/newchat@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Compose.imageset/newchat@2x.png new file mode 100644 index 0000000000..3baa4dd040 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Compose.imageset/newchat@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ComposeChannel.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ComposeChannel.imageset/Contents.json new file mode 100644 index 0000000000..f83025925d --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ComposeChannel.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "channel@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ComposeChannel.imageset/channel@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ComposeChannel.imageset/channel@2x.png new file mode 100644 index 0000000000..8de1a717db Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ComposeChannel.imageset/channel@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ComposeGroup.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ComposeGroup.imageset/Contents.json new file mode 100644 index 0000000000..bf197f337e --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ComposeGroup.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "group@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ComposeGroup.imageset/group@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ComposeGroup.imageset/group@2x.png new file mode 100644 index 0000000000..33e19fb325 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ComposeGroup.imageset/group@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ComposeSecretChat.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ComposeSecretChat.imageset/Contents.json new file mode 100644 index 0000000000..e4a80cf363 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ComposeSecretChat.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "secretchat@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ComposeSecretChat.imageset/secretchat@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ComposeSecretChat.imageset/secretchat@2x.png new file mode 100644 index 0000000000..f4d08ab4af Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ComposeSecretChat.imageset/secretchat@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Emoji.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Emoji.imageset/Contents.json new file mode 100644 index 0000000000..8bb2eb39fc --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Emoji.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "emoji@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Emoji.imageset/emoji@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Emoji.imageset/emoji@2x.png new file mode 100644 index 0000000000..c1d85e3715 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Emoji.imageset/emoji@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Fav.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Fav.imageset/Contents.json new file mode 100644 index 0000000000..00fe980fda --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Fav.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "fav@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Fav.imageset/fav@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Fav.imageset/fav@2x.png new file mode 100644 index 0000000000..7ab60da256 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Fav.imageset/fav@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_BigText.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_BigText.imageset/Contents.json new file mode 100644 index 0000000000..2aa77a3a95 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_BigText.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "bigtext@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_BigText.imageset/bigtext@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_BigText.imageset/bigtext@2x.png new file mode 100644 index 0000000000..2f416d11cd Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_BigText.imageset/bigtext@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_Font_Georgia.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_Font_Georgia.imageset/Contents.json new file mode 100644 index 0000000000..b571989405 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_Font_Georgia.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "georgia@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_Font_Georgia.imageset/georgia@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_Font_Georgia.imageset/georgia@2x.png new file mode 100644 index 0000000000..74c4b83e83 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_Font_Georgia.imageset/georgia@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_Font_SF.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_Font_SF.imageset/Contents.json new file mode 100644 index 0000000000..69d486a116 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_Font_SF.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "sanfrancisco@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_Font_SF.imageset/sanfrancisco@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_Font_SF.imageset/sanfrancisco@2x.png new file mode 100644 index 0000000000..b841972cbc Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_Font_SF.imageset/sanfrancisco@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_Safari.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_Safari.imageset/Contents.json new file mode 100644 index 0000000000..c4ac0f64d7 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_Safari.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "safari@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_Safari.imageset/safari@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_Safari.imageset/safari@2x.png new file mode 100644 index 0000000000..9d1080f487 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_Safari.imageset/safari@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_SmallText.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_SmallText.imageset/Contents.json new file mode 100644 index 0000000000..4ec4c03f65 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_SmallText.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "smalltext@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_SmallText.imageset/smalltext@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_SmallText.imageset/smalltext@2x.png new file mode 100644 index 0000000000..4832b37da4 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_IV_SmallText.imageset/smalltext@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Info.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Info.imageset/Contents.json new file mode 100644 index 0000000000..fd18960fd6 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Info.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "info@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Info.imageset/info@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Info.imageset/info@2x.png new file mode 100644 index 0000000000..804618d66b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Info.imageset/info@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_MessagesDelete.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_MessagesDelete.imageset/Contents.json new file mode 100644 index 0000000000..08984b7ad7 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_MessagesDelete.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "delete@2x (1).png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_MessagesDelete.imageset/delete@2x (1).png b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_MessagesDelete.imageset/delete@2x (1).png new file mode 100644 index 0000000000..7fe4b6bf5c Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_MessagesDelete.imageset/delete@2x (1).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_MessagesForward.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_MessagesForward.imageset/Contents.json new file mode 100644 index 0000000000..4e1174120e --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_MessagesForward.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "share@2x (1).png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_MessagesForward.imageset/share@2x (1).png b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_MessagesForward.imageset/share@2x (1).png new file mode 100644 index 0000000000..e5569b356f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_MessagesForward.imageset/share@2x (1).png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Plus.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Plus.imageset/Contents.json new file mode 100644 index 0000000000..b763e76190 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Plus.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "plus@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Plus.imageset/plus@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Plus.imageset/plus@2x.png new file mode 100644 index 0000000000..83507f7480 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Plus.imageset/plus@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Search.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Search.imageset/Contents.json new file mode 100644 index 0000000000..c2602b4da8 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Search.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "search@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Search.imageset/search@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Search.imageset/search@2x.png new file mode 100644 index 0000000000..bb586c73d1 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Search.imageset/search@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Share.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Share.imageset/Contents.json new file mode 100644 index 0000000000..a0281e9bc1 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Share.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "share@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Share.imageset/share@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Share.imageset/share@2x.png new file mode 100644 index 0000000000..d690a77dc6 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Share.imageset/share@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Stickers.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Stickers.imageset/Contents.json new file mode 100644 index 0000000000..9b91529fa0 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Stickers.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "stickers@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Stickers.imageset/stickers@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Stickers.imageset/stickers@2x.png new file mode 100644 index 0000000000..31eeab4298 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_Stickers.imageset/stickers@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ZoomIn.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ZoomIn.imageset/Contents.json new file mode 100644 index 0000000000..b026edcfbf --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ZoomIn.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "zoomin@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ZoomIn.imageset/zoomin@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ZoomIn.imageset/zoomin@2x.png new file mode 100644 index 0000000000..b16b2ea1fe Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ZoomIn.imageset/zoomin@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ZoomOut.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ZoomOut.imageset/Contents.json new file mode 100644 index 0000000000..29bd126ef8 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ZoomOut.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "zoomout@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ZoomOut.imageset/zoomout@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ZoomOut.imageset/zoomout@2x.png new file mode 100644 index 0000000000..90358dfbfd Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchBar_ZoomOut.imageset/zoomout@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchId.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TouchId.imageset/Contents.json new file mode 100644 index 0000000000..1666d66321 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TouchId.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "touchid@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TouchId.imageset/touchid@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TouchId.imageset/touchid@2x.png new file mode 100644 index 0000000000..d4bab798f1 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TouchId.imageset/touchid@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TwoStepVerification_Create.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_TwoStepVerification_Create.imageset/Contents.json new file mode 100644 index 0000000000..e6c7cd150d --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_TwoStepVerification_Create.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "macpass@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "macpass@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_TwoStepVerification_Create.imageset/macpass@1x.png b/Telegram-Mac/Assets.xcassets/Icon_TwoStepVerification_Create.imageset/macpass@1x.png new file mode 100644 index 0000000000..762a91bcff Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TwoStepVerification_Create.imageset/macpass@1x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_TwoStepVerification_Create.imageset/macpass@2x.png b/Telegram-Mac/Assets.xcassets/Icon_TwoStepVerification_Create.imageset/macpass@2x.png new file mode 100644 index 0000000000..3fd63c0f81 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_TwoStepVerification_Create.imageset/macpass@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_UsernameAvailability.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_UsernameAvailability.imageset/Contents.json index 9b574c2dc4..f83b64c06d 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_UsernameAvailability.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_UsernameAvailability.imageset/Contents.json @@ -2,12 +2,12 @@ "images" : [ { "idiom" : "universal", - "filename" : "UsernameCheck.png", + "filename" : "ic_check.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "UsernameCheck@2x.png", + "filename" : "ic_check@2x.png", "scale" : "2x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_UsernameAvailability.imageset/UsernameCheck.png b/Telegram-Mac/Assets.xcassets/Icon_UsernameAvailability.imageset/UsernameCheck.png deleted file mode 100644 index 1f39ab8984..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_UsernameAvailability.imageset/UsernameCheck.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_UsernameAvailability.imageset/UsernameCheck@2x.png b/Telegram-Mac/Assets.xcassets/Icon_UsernameAvailability.imageset/UsernameCheck@2x.png deleted file mode 100644 index c1e1507701..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_UsernameAvailability.imageset/UsernameCheck@2x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_UsernameAvailability.imageset/ic_check.png b/Telegram-Mac/Assets.xcassets/Icon_UsernameAvailability.imageset/ic_check.png new file mode 100644 index 0000000000..733a6cf1c3 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_UsernameAvailability.imageset/ic_check.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_UsernameAvailability.imageset/ic_check@2x.png b/Telegram-Mac/Assets.xcassets/Icon_UsernameAvailability.imageset/ic_check@2x.png new file mode 100644 index 0000000000..785e20637f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_UsernameAvailability.imageset/ic_check@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VerifyDialog.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_VerifyDialog.imageset/Contents.json new file mode 100644 index 0000000000..f6cf5d6160 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_VerifyDialog.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Ic_verifychats.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Ic_verifychats@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_VerifyDialog.imageset/Ic_verifychats.png b/Telegram-Mac/Assets.xcassets/Icon_VerifyDialog.imageset/Ic_verifychats.png new file mode 100644 index 0000000000..8330710975 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VerifyDialog.imageset/Ic_verifychats.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VerifyDialog.imageset/Ic_verifychats@2x.png b/Telegram-Mac/Assets.xcassets/Icon_VerifyDialog.imageset/Ic_verifychats@2x.png new file mode 100644 index 0000000000..1f5b2a180b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VerifyDialog.imageset/Ic_verifychats@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoCall.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_VideoCall.imageset/Contents.json new file mode 100644 index 0000000000..22fa43d713 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_VideoCall.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_videocallchat.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_videocallchat@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoCall.imageset/ic_videocallchat.png b/Telegram-Mac/Assets.xcassets/Icon_VideoCall.imageset/ic_videocallchat.png new file mode 100644 index 0000000000..1b52c5e1b6 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VideoCall.imageset/ic_videocallchat.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoCall.imageset/ic_videocallchat@2x.png b/Telegram-Mac/Assets.xcassets/Icon_VideoCall.imageset/ic_videocallchat@2x.png new file mode 100644 index 0000000000..6dd87076ff Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VideoCall.imageset/ic_videocallchat@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoCompactFetching.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_VideoCompactFetching.imageset/Contents.json new file mode 100644 index 0000000000..1f570edf8a --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_VideoCompactFetching.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_download1.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_download1@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoCompactFetching.imageset/ic_download1.png b/Telegram-Mac/Assets.xcassets/Icon_VideoCompactFetching.imageset/ic_download1.png new file mode 100644 index 0000000000..56b3ee49d8 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VideoCompactFetching.imageset/ic_download1.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoCompactFetching.imageset/ic_download1@2x.png b/Telegram-Mac/Assets.xcassets/Icon_VideoCompactFetching.imageset/ic_download1@2x.png new file mode 100644 index 0000000000..44517e91cd Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VideoCompactFetching.imageset/ic_download1@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Close.imageset/Close.png b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Close.imageset/Close.png new file mode 100644 index 0000000000..1bec670b81 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Close.imageset/Close.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Close.imageset/Close@2x.png b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Close.imageset/Close@2x.png new file mode 100644 index 0000000000..8aa195cc7b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Close.imageset/Close@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Close.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Close.imageset/Contents.json new file mode 100644 index 0000000000..ae2baaff29 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Close.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Close.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Close@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_EnterFullScreen.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_EnterFullScreen.imageset/Contents.json new file mode 100644 index 0000000000..0d1b3290d2 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_EnterFullScreen.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "fullscreen.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "fullscreen@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_EnterFullScreen.imageset/fullscreen.png b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_EnterFullScreen.imageset/fullscreen.png new file mode 100644 index 0000000000..3c705ab8c8 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_EnterFullScreen.imageset/fullscreen.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_EnterFullScreen.imageset/fullscreen@2x.png b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_EnterFullScreen.imageset/fullscreen@2x.png new file mode 100644 index 0000000000..19705233d0 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_EnterFullScreen.imageset/fullscreen@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_ExitFullScreen.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_ExitFullScreen.imageset/Contents.json new file mode 100644 index 0000000000..cc0682599e --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_ExitFullScreen.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "smallscreen.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "smallscreen@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_ExitFullScreen.imageset/smallscreen.png b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_ExitFullScreen.imageset/smallscreen.png new file mode 100644 index 0000000000..5bee6ed1b9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_ExitFullScreen.imageset/smallscreen.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_ExitFullScreen.imageset/smallscreen@2x.png b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_ExitFullScreen.imageset/smallscreen@2x.png new file mode 100644 index 0000000000..dbb0cb2abc Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_ExitFullScreen.imageset/smallscreen@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_PIPIN.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_PIPIN.imageset/Contents.json new file mode 100644 index 0000000000..483588c5ab --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_PIPIN.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "pip.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "pip@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_PIPIN.imageset/pip.png b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_PIPIN.imageset/pip.png new file mode 100644 index 0000000000..22acb86b9e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_PIPIN.imageset/pip.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_PIPIN.imageset/pip@2x.png b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_PIPIN.imageset/pip@2x.png new file mode 100644 index 0000000000..7d6d0eb107 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_PIPIN.imageset/pip@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_PIPOUT.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_PIPOUT.imageset/Contents.json new file mode 100644 index 0000000000..f9f5d5b974 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_PIPOUT.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "pipout.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "pipout@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_PIPOUT.imageset/pipout.png b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_PIPOUT.imageset/pipout.png new file mode 100644 index 0000000000..83dc2835e7 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_PIPOUT.imageset/pipout.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_PIPOUT.imageset/pipout@2x.png b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_PIPOUT.imageset/pipout@2x.png new file mode 100644 index 0000000000..132684849b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_PIPOUT.imageset/pipout@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Pause.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Pause.imageset/Contents.json new file mode 100644 index 0000000000..7a67263af2 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Pause.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "pause.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "pause@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Pause.imageset/pause.png b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Pause.imageset/pause.png new file mode 100644 index 0000000000..477e6133d1 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Pause.imageset/pause.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Pause.imageset/pause@2x.png b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Pause.imageset/pause@2x.png new file mode 100644 index 0000000000..1681f6f30e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Pause.imageset/pause@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Play.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Play.imageset/Contents.json new file mode 100644 index 0000000000..2f2216ebeb --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Play.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "play.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "play@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Play.imageset/play.png b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Play.imageset/play.png new file mode 100644 index 0000000000..c532bb8905 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Play.imageset/play.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Play.imageset/play@2x.png b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Play.imageset/play@2x.png new file mode 100644 index 0000000000..140bc362fd Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Play.imageset/play@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Rewind15Backward.imageset/15left.png b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Rewind15Backward.imageset/15left.png new file mode 100644 index 0000000000..b722d5a78a Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Rewind15Backward.imageset/15left.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Rewind15Backward.imageset/15left@2x.png b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Rewind15Backward.imageset/15left@2x.png new file mode 100644 index 0000000000..bc96d9408f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Rewind15Backward.imageset/15left@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Rewind15Backward.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Rewind15Backward.imageset/Contents.json new file mode 100644 index 0000000000..d62aa2ad2b --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Rewind15Backward.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "15left.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "15left@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Rewind15Forward.imageset/15right.png b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Rewind15Forward.imageset/15right.png new file mode 100644 index 0000000000..069560364f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Rewind15Forward.imageset/15right.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Rewind15Forward.imageset/15right@2x.png b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Rewind15Forward.imageset/15right@2x.png new file mode 100644 index 0000000000..f59827cee0 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Rewind15Forward.imageset/15right@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Rewind15Forward.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Rewind15Forward.imageset/Contents.json new file mode 100644 index 0000000000..bf6658572c --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Rewind15Forward.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "15right.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "15right@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Volume.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Volume.imageset/Contents.json new file mode 100644 index 0000000000..a40329a525 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Volume.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "volume.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "volume@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Volume.imageset/volume.png b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Volume.imageset/volume.png new file mode 100644 index 0000000000..8f91cadcbe Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Volume.imageset/volume.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Volume.imageset/volume@2x.png b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Volume.imageset/volume@2x.png new file mode 100644 index 0000000000..256c8f6da2 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_Volume.imageset/volume@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_VolumeOff.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_VolumeOff.imageset/Contents.json new file mode 100644 index 0000000000..470f703aa0 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_VolumeOff.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "VolumeOff.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "VolumeOff@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_VolumeOff.imageset/VolumeOff.png b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_VolumeOff.imageset/VolumeOff.png new file mode 100644 index 0000000000..4564d4ad9c Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_VolumeOff.imageset/VolumeOff.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_VolumeOff.imageset/VolumeOff@2x.png b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_VolumeOff.imageset/VolumeOff@2x.png new file mode 100644 index 0000000000..9a9d17d8f2 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VideoPlayer_VolumeOff.imageset/VolumeOff@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_HidePeers.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_HidePeers.imageset/Contents.json new file mode 100644 index 0000000000..249ae2a40c --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_HidePeers.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_leftpanel.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_leftpanel@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_HidePeers.imageset/ic_leftpanel.png b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_HidePeers.imageset/ic_leftpanel.png new file mode 100644 index 0000000000..b014308f2d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_HidePeers.imageset/ic_leftpanel.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_HidePeers.imageset/ic_leftpanel@2x.png b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_HidePeers.imageset/ic_leftpanel@2x.png new file mode 100644 index 0000000000..53e212c1e9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_HidePeers.imageset/ic_leftpanel@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_InviteListener.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_InviteListener.imageset/Contents.json new file mode 100644 index 0000000000..b6c7e9463d --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_InviteListener.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_listener.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_listener@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_InviteListener.imageset/ic_listener.png b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_InviteListener.imageset/ic_listener.png new file mode 100644 index 0000000000..93f1b94a87 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_InviteListener.imageset/ic_listener.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_InviteListener.imageset/ic_listener@2x.png b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_InviteListener.imageset/ic_listener@2x.png new file mode 100644 index 0000000000..9e31884b94 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_InviteListener.imageset/ic_listener@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_InviteSpeaker.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_InviteSpeaker.imageset/Contents.json new file mode 100644 index 0000000000..c91497a6b0 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_InviteSpeaker.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_speaker.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_speaker@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_InviteSpeaker.imageset/ic_speaker.png b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_InviteSpeaker.imageset/ic_speaker.png new file mode 100644 index 0000000000..c2f05a6824 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_InviteSpeaker.imageset/ic_speaker.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_InviteSpeaker.imageset/ic_speaker@2x.png b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_InviteSpeaker.imageset/ic_speaker@2x.png new file mode 100644 index 0000000000..e2b278e352 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_InviteSpeaker.imageset/ic_speaker@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_PausedVideo.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_PausedVideo.imageset/Contents.json new file mode 100644 index 0000000000..af1fb88291 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_PausedVideo.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "wait.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "wait@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_PausedVideo.imageset/wait.png b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_PausedVideo.imageset/wait.png new file mode 100644 index 0000000000..fd96fecae0 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_PausedVideo.imageset/wait.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_PausedVideo.imageset/wait@2x.png b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_PausedVideo.imageset/wait@2x.png new file mode 100644 index 0000000000..13fc9794ea Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_PausedVideo.imageset/wait@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_PinVideo.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_PinVideo.imageset/Contents.json new file mode 100644 index 0000000000..2591e89df8 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_PinVideo.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_pin.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_pin@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_PinVideo.imageset/ic_pin.png b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_PinVideo.imageset/ic_pin.png new file mode 100644 index 0000000000..987371c498 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_PinVideo.imageset/ic_pin.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_PinVideo.imageset/ic_pin@2x.png b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_PinVideo.imageset/ic_pin@2x.png new file mode 100644 index 0000000000..8bf74f8c5e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_PinVideo.imageset/ic_pin@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_PinWindow.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_PinWindow.imageset/Contents.json new file mode 100644 index 0000000000..87472ede08 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_PinWindow.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_pinwindow.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_pinwindow@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_PinWindow.imageset/ic_pinwindow.png b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_PinWindow.imageset/ic_pinwindow.png new file mode 100644 index 0000000000..7623258d95 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_PinWindow.imageset/ic_pinwindow.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_PinWindow.imageset/ic_pinwindow@2x.png b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_PinWindow.imageset/ic_pinwindow@2x.png new file mode 100644 index 0000000000..0480486f9e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_PinWindow.imageset/ic_pinwindow@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_Title.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_Title.imageset/Contents.json new file mode 100644 index 0000000000..d02c46ad1f --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_Title.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_voicechat.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_voicechat@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_Title.imageset/ic_voicechat.png b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_Title.imageset/ic_voicechat.png new file mode 100644 index 0000000000..bb47defada Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_Title.imageset/ic_voicechat.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_Title.imageset/ic_voicechat@2x.png b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_Title.imageset/ic_voicechat@2x.png new file mode 100644 index 0000000000..fb466f7741 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_Title.imageset/ic_voicechat@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_Tooltip_Close.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_Tooltip_Close.imageset/Contents.json new file mode 100644 index 0000000000..307aa33981 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_Tooltip_Close.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "close.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "close@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_Tooltip_Close.imageset/close.png b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_Tooltip_Close.imageset/close.png new file mode 100644 index 0000000000..35aea79581 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_Tooltip_Close.imageset/close.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_Tooltip_Close.imageset/close@2x.png b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_Tooltip_Close.imageset/close@2x.png new file mode 100644 index 0000000000..6e71f44fb3 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_Tooltip_Close.imageset/close@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_UnpinVideo.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_UnpinVideo.imageset/Contents.json new file mode 100644 index 0000000000..38f0e86e5c --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_UnpinVideo.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_unpin.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_unpin@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_UnpinVideo.imageset/ic_unpin.png b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_UnpinVideo.imageset/ic_unpin.png new file mode 100644 index 0000000000..468a916dad Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_UnpinVideo.imageset/ic_unpin.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_UnpinVideo.imageset/ic_unpin@2x.png b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_UnpinVideo.imageset/ic_unpin@2x.png new file mode 100644 index 0000000000..027e9221f3 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_UnpinVideo.imageset/ic_unpin@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_VideoLimit.imageset/CameraOff.png b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_VideoLimit.imageset/CameraOff.png new file mode 100644 index 0000000000..c9cb5547e4 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_VideoLimit.imageset/CameraOff.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_VideoLimit.imageset/CameraOff@2x.png b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_VideoLimit.imageset/CameraOff@2x.png new file mode 100644 index 0000000000..1a5fdaef6d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_VideoLimit.imageset/CameraOff@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_VideoLimit.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_VideoLimit.imageset/Contents.json new file mode 100644 index 0000000000..955e1c7536 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_VoiceChat_VideoLimit.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "CameraOff.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "CameraOff@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_VolumeMenu_Off.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_VolumeMenu_Off.imageset/Contents.json new file mode 100644 index 0000000000..7bbed7e376 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_VolumeMenu_Off.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_menu_volumeoff.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_menu_volumeoff@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_VolumeMenu_Off.imageset/ic_menu_volumeoff.png b/Telegram-Mac/Assets.xcassets/Icon_VolumeMenu_Off.imageset/ic_menu_volumeoff.png new file mode 100644 index 0000000000..5d48cc7de8 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VolumeMenu_Off.imageset/ic_menu_volumeoff.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VolumeMenu_Off.imageset/ic_menu_volumeoff@2x.png b/Telegram-Mac/Assets.xcassets/Icon_VolumeMenu_Off.imageset/ic_menu_volumeoff@2x.png new file mode 100644 index 0000000000..96f1766d79 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VolumeMenu_Off.imageset/ic_menu_volumeoff@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VolumeMenu_On.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_VolumeMenu_On.imageset/Contents.json new file mode 100644 index 0000000000..c633129832 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_VolumeMenu_On.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_menu_volumeon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_menu_volumeon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_VolumeMenu_On.imageset/ic_menu_volumeon.png b/Telegram-Mac/Assets.xcassets/Icon_VolumeMenu_On.imageset/ic_menu_volumeon.png new file mode 100644 index 0000000000..03e35ba3a2 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VolumeMenu_On.imageset/ic_menu_volumeon.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_VolumeMenu_On.imageset/ic_menu_volumeon@2x.png b/Telegram-Mac/Assets.xcassets/Icon_VolumeMenu_On.imageset/ic_menu_volumeon@2x.png new file mode 100644 index 0000000000..b8b8545d98 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_VolumeMenu_On.imageset/ic_menu_volumeon@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_WalletClose.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_WalletClose.imageset/Contents.json new file mode 100644 index 0000000000..81735d6821 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_WalletClose.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_close.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_close@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_WalletClose.imageset/ic_close.png b/Telegram-Mac/Assets.xcassets/Icon_WalletClose.imageset/ic_close.png new file mode 100644 index 0000000000..95a6980281 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_WalletClose.imageset/ic_close.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_WalletClose.imageset/ic_close@2x.png b/Telegram-Mac/Assets.xcassets/Icon_WalletClose.imageset/ic_close@2x.png new file mode 100644 index 0000000000..7871880011 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_WalletClose.imageset/ic_close@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_WalletPasscodeHidden.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_WalletPasscodeHidden.imageset/Contents.json new file mode 100644 index 0000000000..8b08b1e251 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_WalletPasscodeHidden.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Hide.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Hide@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_WalletPasscodeHidden.imageset/Hide.png b/Telegram-Mac/Assets.xcassets/Icon_WalletPasscodeHidden.imageset/Hide.png new file mode 100644 index 0000000000..94b57217d7 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_WalletPasscodeHidden.imageset/Hide.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_WalletPasscodeHidden.imageset/Hide@2x.png b/Telegram-Mac/Assets.xcassets/Icon_WalletPasscodeHidden.imageset/Hide@2x.png new file mode 100644 index 0000000000..5a345e2cee Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_WalletPasscodeHidden.imageset/Hide@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_WalletPasscodeVisible.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_WalletPasscodeVisible.imageset/Contents.json new file mode 100644 index 0000000000..2ffbdd46fc --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_WalletPasscodeVisible.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "View.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "View@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_WalletPasscodeVisible.imageset/View.png b/Telegram-Mac/Assets.xcassets/Icon_WalletPasscodeVisible.imageset/View.png new file mode 100644 index 0000000000..5110808954 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_WalletPasscodeVisible.imageset/View.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_WalletPasscodeVisible.imageset/View@2x.png b/Telegram-Mac/Assets.xcassets/Icon_WalletPasscodeVisible.imageset/View@2x.png new file mode 100644 index 0000000000..4f270a67d3 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_WalletPasscodeVisible.imageset/View@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_WalletQR.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_WalletQR.imageset/Contents.json new file mode 100644 index 0000000000..b9dcf0b68c --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_WalletQR.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_qr.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_qr@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_WalletQR.imageset/ic_qr.png b/Telegram-Mac/Assets.xcassets/Icon_WalletQR.imageset/ic_qr.png new file mode 100644 index 0000000000..bfd8f23121 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_WalletQR.imageset/ic_qr.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_WalletQR.imageset/ic_qr@2x.png b/Telegram-Mac/Assets.xcassets/Icon_WalletQR.imageset/ic_qr@2x.png new file mode 100644 index 0000000000..3ff359ff23 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_WalletQR.imageset/ic_qr@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_WalletReceive.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_WalletReceive.imageset/Contents.json new file mode 100644 index 0000000000..d69346dd07 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_WalletReceive.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_receive.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_receive@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_WalletReceive.imageset/ic_receive.png b/Telegram-Mac/Assets.xcassets/Icon_WalletReceive.imageset/ic_receive.png new file mode 100644 index 0000000000..a5d24bc973 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_WalletReceive.imageset/ic_receive.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_WalletReceive.imageset/ic_receive@2x.png b/Telegram-Mac/Assets.xcassets/Icon_WalletReceive.imageset/ic_receive@2x.png new file mode 100644 index 0000000000..7143ca6949 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_WalletReceive.imageset/ic_receive@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_WalletSend.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_WalletSend.imageset/Contents.json new file mode 100644 index 0000000000..eea57b6522 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_WalletSend.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_send.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_send@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_WalletSend.imageset/ic_send.png b/Telegram-Mac/Assets.xcassets/Icon_WalletSend.imageset/ic_send.png new file mode 100644 index 0000000000..e0f3889182 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_WalletSend.imageset/ic_send.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_WalletSend.imageset/ic_send@2x.png b/Telegram-Mac/Assets.xcassets/Icon_WalletSend.imageset/ic_send@2x.png new file mode 100644 index 0000000000..8b690e5fc4 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_WalletSend.imageset/ic_send@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_WalletSettings.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_WalletSettings.imageset/Contents.json new file mode 100644 index 0000000000..25de093c56 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_WalletSettings.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_settings.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_settings@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_WalletSettings.imageset/ic_settings.png b/Telegram-Mac/Assets.xcassets/Icon_WalletSettings.imageset/ic_settings.png new file mode 100644 index 0000000000..b507291bb9 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_WalletSettings.imageset/ic_settings.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_WalletSettings.imageset/ic_settings@2x.png b/Telegram-Mac/Assets.xcassets/Icon_WalletSettings.imageset/ic_settings@2x.png new file mode 100644 index 0000000000..a4a18cfb04 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_WalletSettings.imageset/ic_settings@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_WalletUpdate.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_WalletUpdate.imageset/Contents.json new file mode 100644 index 0000000000..89ac135498 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_WalletUpdate.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_update.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_update@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_WalletUpdate.imageset/ic_update.png b/Telegram-Mac/Assets.xcassets/Icon_WalletUpdate.imageset/ic_update.png new file mode 100644 index 0000000000..697097cdf3 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_WalletUpdate.imageset/ic_update.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_WalletUpdate.imageset/ic_update@2x.png b/Telegram-Mac/Assets.xcassets/Icon_WalletUpdate.imageset/ic_update@2x.png new file mode 100644 index 0000000000..1d8a7b927a Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_WalletUpdate.imageset/ic_update@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Widget_Peers_Both.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Widget_Peers_Both.imageset/Contents.json new file mode 100644 index 0000000000..fd73732577 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Widget_Peers_Both.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_both.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_both@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Widget_Peers_Both.imageset/ic_both.png b/Telegram-Mac/Assets.xcassets/Icon_Widget_Peers_Both.imageset/ic_both.png new file mode 100644 index 0000000000..98d88ed129 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Widget_Peers_Both.imageset/ic_both.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Widget_Peers_Both.imageset/ic_both@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Widget_Peers_Both.imageset/ic_both@2x.png new file mode 100644 index 0000000000..3c9b80abdf Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Widget_Peers_Both.imageset/ic_both@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Widget_Peers_Favorite.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Widget_Peers_Favorite.imageset/Contents.json new file mode 100644 index 0000000000..ffc5a2f0b0 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Widget_Peers_Favorite.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_popular.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_popular@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Widget_Peers_Favorite.imageset/ic_popular.png b/Telegram-Mac/Assets.xcassets/Icon_Widget_Peers_Favorite.imageset/ic_popular.png new file mode 100644 index 0000000000..4cc9e3c284 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Widget_Peers_Favorite.imageset/ic_popular.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Widget_Peers_Favorite.imageset/ic_popular@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Widget_Peers_Favorite.imageset/ic_popular@2x.png new file mode 100644 index 0000000000..d78c3fa679 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Widget_Peers_Favorite.imageset/ic_popular@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Widget_Peers_Recent.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_Widget_Peers_Recent.imageset/Contents.json new file mode 100644 index 0000000000..dac25e42f7 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_Widget_Peers_Recent.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "ic_recent.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_recent@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/Icon_Widget_Peers_Recent.imageset/ic_recent.png b/Telegram-Mac/Assets.xcassets/Icon_Widget_Peers_Recent.imageset/ic_recent.png new file mode 100644 index 0000000000..9f57329e11 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Widget_Peers_Recent.imageset/ic_recent.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_Widget_Peers_Recent.imageset/ic_recent@2x.png b/Telegram-Mac/Assets.xcassets/Icon_Widget_Peers_Recent.imageset/ic_recent@2x.png new file mode 100644 index 0000000000..dc2da1c115 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_Widget_Peers_Recent.imageset/ic_recent@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_callDeclineSmall_Window.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_callDeclineSmall_Window.imageset/Contents.json new file mode 100644 index 0000000000..66c1c080b0 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_callDeclineSmall_Window.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ic_calls_declinesmall.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ic_calls_declinesmall@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_callDeclineSmall_Window.imageset/ic_calls_declinesmall.png b/Telegram-Mac/Assets.xcassets/Icon_callDeclineSmall_Window.imageset/ic_calls_declinesmall.png new file mode 100644 index 0000000000..2c02175fc3 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_callDeclineSmall_Window.imageset/ic_calls_declinesmall.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_callDeclineSmall_Window.imageset/ic_calls_declinesmall@2x.png b/Telegram-Mac/Assets.xcassets/Icon_callDeclineSmall_Window.imageset/ic_calls_declinesmall@2x.png new file mode 100644 index 0000000000..9c24371303 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_callDeclineSmall_Window.imageset/ic_calls_declinesmall@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_callNavigationHeader.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_callNavigationHeader.imageset/Contents.json index 903813afc2..56c95501ec 100644 --- a/Telegram-Mac/Assets.xcassets/Icon_callNavigationHeader.imageset/Contents.json +++ b/Telegram-Mac/Assets.xcassets/Icon_callNavigationHeader.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "ic_call@1x.png", + "filename" : "ic_call.png", "scale" : "1x" }, { diff --git a/Telegram-Mac/Assets.xcassets/Icon_callNavigationHeader.imageset/ic_call.png b/Telegram-Mac/Assets.xcassets/Icon_callNavigationHeader.imageset/ic_call.png new file mode 100644 index 0000000000..6a48bc086e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_callNavigationHeader.imageset/ic_call.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_callNavigationHeader.imageset/ic_call@1x.png b/Telegram-Mac/Assets.xcassets/Icon_callNavigationHeader.imageset/ic_call@1x.png deleted file mode 100644 index d0a0fdf7e8..0000000000 Binary files a/Telegram-Mac/Assets.xcassets/Icon_callNavigationHeader.imageset/ic_call@1x.png and /dev/null differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_callNavigationHeader.imageset/ic_call@2x.png b/Telegram-Mac/Assets.xcassets/Icon_callNavigationHeader.imageset/ic_call@2x.png index e77828ee93..e2edd1f39b 100644 Binary files a/Telegram-Mac/Assets.xcassets/Icon_callNavigationHeader.imageset/ic_call@2x.png and b/Telegram-Mac/Assets.xcassets/Icon_callNavigationHeader.imageset/ic_call@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_loginQRCap.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/Icon_loginQRCap.imageset/Contents.json new file mode 100644 index 0000000000..5dbb141b6b --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/Icon_loginQRCap.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "qrlogo.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "qrlogo@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/Icon_loginQRCap.imageset/qrlogo.png b/Telegram-Mac/Assets.xcassets/Icon_loginQRCap.imageset/qrlogo.png new file mode 100644 index 0000000000..22aba5ef7b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_loginQRCap.imageset/qrlogo.png differ diff --git a/Telegram-Mac/Assets.xcassets/Icon_loginQRCap.imageset/qrlogo@2x.png b/Telegram-Mac/Assets.xcassets/Icon_loginQRCap.imageset/qrlogo@2x.png new file mode 100644 index 0000000000..333368427e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/Icon_loginQRCap.imageset/qrlogo@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/StatusIcon.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/StatusIcon.imageset/Contents.json new file mode 100644 index 0000000000..42d2f32bfe --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/StatusIcon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "StatusIcon.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "StatusIcon@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/StatusIcon.imageset/StatusIcon.png b/Telegram-Mac/Assets.xcassets/StatusIcon.imageset/StatusIcon.png new file mode 100644 index 0000000000..b668fdfe25 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/StatusIcon.imageset/StatusIcon.png differ diff --git a/Telegram-Mac/Assets.xcassets/StatusIcon.imageset/StatusIcon@2x.png b/Telegram-Mac/Assets.xcassets/StatusIcon.imageset/StatusIcon@2x.png new file mode 100644 index 0000000000..7eccc34cf7 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/StatusIcon.imageset/StatusIcon@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/arrow_left.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/arrow_left.imageset/Contents.json new file mode 100644 index 0000000000..85628e19e2 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/arrow_left.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "arrow_right.pdf", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/arrow_left.imageset/arrow_right.pdf b/Telegram-Mac/Assets.xcassets/arrow_left.imageset/arrow_right.pdf new file mode 100644 index 0000000000..baf05c2435 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/arrow_left.imageset/arrow_right.pdf differ diff --git a/Telegram-Mac/Assets.xcassets/arrow_right.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/arrow_right.imageset/Contents.json new file mode 100644 index 0000000000..f78ba9e92b --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/arrow_right.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "arrow_left.pdf", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/arrow_right.imageset/arrow_left.pdf b/Telegram-Mac/Assets.xcassets/arrow_right.imageset/arrow_left.pdf new file mode 100644 index 0000000000..7b20434672 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/arrow_right.imageset/arrow_left.pdf differ diff --git a/Telegram-Mac/Assets.xcassets/icons8-circled-play-48.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/icons8-circled-play-48.imageset/Contents.json new file mode 100644 index 0000000000..02187c6534 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/icons8-circled-play-48.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "icons8-circled-play-48.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "icons8-circled-play-96.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/icons8-circled-play-48.imageset/icons8-circled-play-48.png b/Telegram-Mac/Assets.xcassets/icons8-circled-play-48.imageset/icons8-circled-play-48.png new file mode 100644 index 0000000000..8ca7c9365c Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/icons8-circled-play-48.imageset/icons8-circled-play-48.png differ diff --git a/Telegram-Mac/Assets.xcassets/icons8-circled-play-48.imageset/icons8-circled-play-96.png b/Telegram-Mac/Assets.xcassets/icons8-circled-play-48.imageset/icons8-circled-play-96.png new file mode 100644 index 0000000000..cea69779f6 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/icons8-circled-play-48.imageset/icons8-circled-play-96.png differ diff --git a/Telegram-Mac/Assets.xcassets/selection_frame_dark.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/selection_frame_dark.imageset/Contents.json new file mode 100644 index 0000000000..8689d392bf --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/selection_frame_dark.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "selection_frame_dark.pdf", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/selection_frame_dark.imageset/selection_frame_dark.pdf b/Telegram-Mac/Assets.xcassets/selection_frame_dark.imageset/selection_frame_dark.pdf new file mode 100644 index 0000000000..ae27128091 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/selection_frame_dark.imageset/selection_frame_dark.pdf differ diff --git a/Telegram-Mac/Assets.xcassets/selection_frame_light.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/selection_frame_light.imageset/Contents.json new file mode 100644 index 0000000000..3c2cd47618 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/selection_frame_light.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "selection_frame_light.pdf", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/selection_frame_light.imageset/selection_frame_light.pdf b/Telegram-Mac/Assets.xcassets/selection_frame_light.imageset/selection_frame_light.pdf new file mode 100644 index 0000000000..c426c5d695 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/selection_frame_light.imageset/selection_frame_light.pdf differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_amex.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/stp_card_amex.imageset/Contents.json new file mode 100644 index 0000000000..de70949060 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/stp_card_amex.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "stp_card_amex.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_card_amex@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/stp_card_amex.imageset/stp_card_amex.png b/Telegram-Mac/Assets.xcassets/stp_card_amex.imageset/stp_card_amex.png new file mode 100644 index 0000000000..c25fb10e65 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_amex.imageset/stp_card_amex.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_amex.imageset/stp_card_amex@2x.png b/Telegram-Mac/Assets.xcassets/stp_card_amex.imageset/stp_card_amex@2x.png new file mode 100644 index 0000000000..3e11213014 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_amex.imageset/stp_card_amex@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_amex_template.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/stp_card_amex_template.imageset/Contents.json new file mode 100644 index 0000000000..859db99a72 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/stp_card_amex_template.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "stp_card_amex_template.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_card_amex_template@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/stp_card_amex_template.imageset/stp_card_amex_template.png b/Telegram-Mac/Assets.xcassets/stp_card_amex_template.imageset/stp_card_amex_template.png new file mode 100644 index 0000000000..dd7d8529c8 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_amex_template.imageset/stp_card_amex_template.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_amex_template.imageset/stp_card_amex_template@2x.png b/Telegram-Mac/Assets.xcassets/stp_card_amex_template.imageset/stp_card_amex_template@2x.png new file mode 100644 index 0000000000..64eb765a5b Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_amex_template.imageset/stp_card_amex_template@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_applepay.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/stp_card_applepay.imageset/Contents.json new file mode 100644 index 0000000000..3e070ea7dc --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/stp_card_applepay.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "stp_card_applepay.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_card_applepay@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/stp_card_applepay.imageset/stp_card_applepay.png b/Telegram-Mac/Assets.xcassets/stp_card_applepay.imageset/stp_card_applepay.png new file mode 100644 index 0000000000..9dea38b70e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_applepay.imageset/stp_card_applepay.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_applepay.imageset/stp_card_applepay@2x.png b/Telegram-Mac/Assets.xcassets/stp_card_applepay.imageset/stp_card_applepay@2x.png new file mode 100644 index 0000000000..4ff86964c7 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_applepay.imageset/stp_card_applepay@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_cvc.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/stp_card_cvc.imageset/Contents.json new file mode 100644 index 0000000000..5ddd3c14ed --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/stp_card_cvc.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "stp_card_cvc.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_card_cvc@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/stp_card_cvc.imageset/stp_card_cvc.png b/Telegram-Mac/Assets.xcassets/stp_card_cvc.imageset/stp_card_cvc.png new file mode 100644 index 0000000000..a8cc72e863 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_cvc.imageset/stp_card_cvc.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_cvc.imageset/stp_card_cvc@2x.png b/Telegram-Mac/Assets.xcassets/stp_card_cvc.imageset/stp_card_cvc@2x.png new file mode 100644 index 0000000000..e74678b466 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_cvc.imageset/stp_card_cvc@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_cvc_amex.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/stp_card_cvc_amex.imageset/Contents.json new file mode 100644 index 0000000000..57f29576db --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/stp_card_cvc_amex.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "stp_card_cvc_amex.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_card_cvc_amex@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/stp_card_cvc_amex.imageset/stp_card_cvc_amex.png b/Telegram-Mac/Assets.xcassets/stp_card_cvc_amex.imageset/stp_card_cvc_amex.png new file mode 100644 index 0000000000..dc9636fad8 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_cvc_amex.imageset/stp_card_cvc_amex.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_cvc_amex.imageset/stp_card_cvc_amex@2x.png b/Telegram-Mac/Assets.xcassets/stp_card_cvc_amex.imageset/stp_card_cvc_amex@2x.png new file mode 100644 index 0000000000..f89464e97e Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_cvc_amex.imageset/stp_card_cvc_amex@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_diners.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/stp_card_diners.imageset/Contents.json new file mode 100644 index 0000000000..ecdd0be821 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/stp_card_diners.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "stp_card_diners.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_card_diners@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/stp_card_diners.imageset/stp_card_diners.png b/Telegram-Mac/Assets.xcassets/stp_card_diners.imageset/stp_card_diners.png new file mode 100644 index 0000000000..4741bb1b67 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_diners.imageset/stp_card_diners.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_diners.imageset/stp_card_diners@2x.png b/Telegram-Mac/Assets.xcassets/stp_card_diners.imageset/stp_card_diners@2x.png new file mode 100644 index 0000000000..d5594d180d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_diners.imageset/stp_card_diners@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_diners_template.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/stp_card_diners_template.imageset/Contents.json new file mode 100644 index 0000000000..3bcc45802e --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/stp_card_diners_template.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "stp_card_diners_template.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_card_diners_template@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/stp_card_diners_template.imageset/stp_card_diners_template.png b/Telegram-Mac/Assets.xcassets/stp_card_diners_template.imageset/stp_card_diners_template.png new file mode 100644 index 0000000000..4f91d33350 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_diners_template.imageset/stp_card_diners_template.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_diners_template.imageset/stp_card_diners_template@2x.png b/Telegram-Mac/Assets.xcassets/stp_card_diners_template.imageset/stp_card_diners_template@2x.png new file mode 100644 index 0000000000..9dd1c08d7f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_diners_template.imageset/stp_card_diners_template@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_discover.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/stp_card_discover.imageset/Contents.json new file mode 100644 index 0000000000..8a474df843 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/stp_card_discover.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "stp_card_discover.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_card_discover@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/stp_card_discover.imageset/stp_card_discover.png b/Telegram-Mac/Assets.xcassets/stp_card_discover.imageset/stp_card_discover.png new file mode 100644 index 0000000000..6d6a9eff84 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_discover.imageset/stp_card_discover.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_discover.imageset/stp_card_discover@2x.png b/Telegram-Mac/Assets.xcassets/stp_card_discover.imageset/stp_card_discover@2x.png new file mode 100644 index 0000000000..a2fb57b054 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_discover.imageset/stp_card_discover@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_discover_template.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/stp_card_discover_template.imageset/Contents.json new file mode 100644 index 0000000000..2a6b327c48 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/stp_card_discover_template.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "stp_card_discover_template.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_card_discover_template@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/stp_card_discover_template.imageset/stp_card_discover_template.png b/Telegram-Mac/Assets.xcassets/stp_card_discover_template.imageset/stp_card_discover_template.png new file mode 100644 index 0000000000..c69cad0ab3 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_discover_template.imageset/stp_card_discover_template.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_discover_template.imageset/stp_card_discover_template@2x.png b/Telegram-Mac/Assets.xcassets/stp_card_discover_template.imageset/stp_card_discover_template@2x.png new file mode 100644 index 0000000000..2861f32022 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_discover_template.imageset/stp_card_discover_template@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_error.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/stp_card_error.imageset/Contents.json new file mode 100644 index 0000000000..db2c796456 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/stp_card_error.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "stp_card_error.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_card_error@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/stp_card_error.imageset/stp_card_error.png b/Telegram-Mac/Assets.xcassets/stp_card_error.imageset/stp_card_error.png new file mode 100644 index 0000000000..225b0a4bf6 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_error.imageset/stp_card_error.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_error.imageset/stp_card_error@2x.png b/Telegram-Mac/Assets.xcassets/stp_card_error.imageset/stp_card_error@2x.png new file mode 100644 index 0000000000..a002c08eaa Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_error.imageset/stp_card_error@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_error_amex.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/stp_card_error_amex.imageset/Contents.json new file mode 100644 index 0000000000..fa2f011312 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/stp_card_error_amex.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "stp_card_error_amex.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_card_error_amex@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/stp_card_error_amex.imageset/stp_card_error_amex.png b/Telegram-Mac/Assets.xcassets/stp_card_error_amex.imageset/stp_card_error_amex.png new file mode 100644 index 0000000000..27ca7503fd Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_error_amex.imageset/stp_card_error_amex.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_error_amex.imageset/stp_card_error_amex@2x.png b/Telegram-Mac/Assets.xcassets/stp_card_error_amex.imageset/stp_card_error_amex@2x.png new file mode 100644 index 0000000000..509362d5e0 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_error_amex.imageset/stp_card_error_amex@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_form_back.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/stp_card_form_back.imageset/Contents.json new file mode 100644 index 0000000000..275a99d876 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/stp_card_form_back.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "stp_card_form_back.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_card_form_back@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/stp_card_form_back.imageset/stp_card_form_back.png b/Telegram-Mac/Assets.xcassets/stp_card_form_back.imageset/stp_card_form_back.png new file mode 100644 index 0000000000..15e57f9405 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_form_back.imageset/stp_card_form_back.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_form_back.imageset/stp_card_form_back@2x.png b/Telegram-Mac/Assets.xcassets/stp_card_form_back.imageset/stp_card_form_back@2x.png new file mode 100644 index 0000000000..cf9adcaaad Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_form_back.imageset/stp_card_form_back@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_form_front.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/stp_card_form_front.imageset/Contents.json new file mode 100644 index 0000000000..b03978a3dd --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/stp_card_form_front.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "stp_card_form_front.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_card_form_front@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/stp_card_form_front.imageset/stp_card_form_front.png b/Telegram-Mac/Assets.xcassets/stp_card_form_front.imageset/stp_card_form_front.png new file mode 100644 index 0000000000..98ac727c00 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_form_front.imageset/stp_card_form_front.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_form_front.imageset/stp_card_form_front@2x.png b/Telegram-Mac/Assets.xcassets/stp_card_form_front.imageset/stp_card_form_front@2x.png new file mode 100644 index 0000000000..69af8e63c1 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_form_front.imageset/stp_card_form_front@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_jcb.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/stp_card_jcb.imageset/Contents.json new file mode 100644 index 0000000000..737533022b --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/stp_card_jcb.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "stp_card_jcb.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_card_jcb@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/stp_card_jcb.imageset/stp_card_jcb.png b/Telegram-Mac/Assets.xcassets/stp_card_jcb.imageset/stp_card_jcb.png new file mode 100644 index 0000000000..da91814bbb Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_jcb.imageset/stp_card_jcb.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_jcb.imageset/stp_card_jcb@2x.png b/Telegram-Mac/Assets.xcassets/stp_card_jcb.imageset/stp_card_jcb@2x.png new file mode 100644 index 0000000000..39ca106197 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_jcb.imageset/stp_card_jcb@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_jcb_template.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/stp_card_jcb_template.imageset/Contents.json new file mode 100644 index 0000000000..f997808526 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/stp_card_jcb_template.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "stp_card_jcb_template.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_card_jcb_template@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/stp_card_jcb_template.imageset/stp_card_jcb_template.png b/Telegram-Mac/Assets.xcassets/stp_card_jcb_template.imageset/stp_card_jcb_template.png new file mode 100644 index 0000000000..af73541ecb Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_jcb_template.imageset/stp_card_jcb_template.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_jcb_template.imageset/stp_card_jcb_template@2x.png b/Telegram-Mac/Assets.xcassets/stp_card_jcb_template.imageset/stp_card_jcb_template@2x.png new file mode 100644 index 0000000000..5504c9733f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_jcb_template.imageset/stp_card_jcb_template@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_mastercard.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/stp_card_mastercard.imageset/Contents.json new file mode 100644 index 0000000000..cefaa3d7ca --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/stp_card_mastercard.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "stp_card_mastercard.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_card_mastercard@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/stp_card_mastercard.imageset/stp_card_mastercard.png b/Telegram-Mac/Assets.xcassets/stp_card_mastercard.imageset/stp_card_mastercard.png new file mode 100644 index 0000000000..f7cefef59c Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_mastercard.imageset/stp_card_mastercard.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_mastercard.imageset/stp_card_mastercard@2x.png b/Telegram-Mac/Assets.xcassets/stp_card_mastercard.imageset/stp_card_mastercard@2x.png new file mode 100644 index 0000000000..d754e986fd Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_mastercard.imageset/stp_card_mastercard@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_mastercard_template.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/stp_card_mastercard_template.imageset/Contents.json new file mode 100644 index 0000000000..34300bd543 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/stp_card_mastercard_template.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "stp_card_mastercard_template.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_card_mastercard_template@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/stp_card_mastercard_template.imageset/stp_card_mastercard_template.png b/Telegram-Mac/Assets.xcassets/stp_card_mastercard_template.imageset/stp_card_mastercard_template.png new file mode 100644 index 0000000000..003d7c6281 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_mastercard_template.imageset/stp_card_mastercard_template.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_mastercard_template.imageset/stp_card_mastercard_template@2x.png b/Telegram-Mac/Assets.xcassets/stp_card_mastercard_template.imageset/stp_card_mastercard_template@2x.png new file mode 100644 index 0000000000..ac4b140af4 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_mastercard_template.imageset/stp_card_mastercard_template@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_unknown.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/stp_card_unknown.imageset/Contents.json new file mode 100644 index 0000000000..bfcee72304 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/stp_card_unknown.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "stp_card_unknown.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_card_unknown@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/stp_card_unknown.imageset/stp_card_unknown.png b/Telegram-Mac/Assets.xcassets/stp_card_unknown.imageset/stp_card_unknown.png new file mode 100644 index 0000000000..c2100ceb6c Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_unknown.imageset/stp_card_unknown.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_unknown.imageset/stp_card_unknown@2x.png b/Telegram-Mac/Assets.xcassets/stp_card_unknown.imageset/stp_card_unknown@2x.png new file mode 100644 index 0000000000..563eb18ef0 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_unknown.imageset/stp_card_unknown@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_visa.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/stp_card_visa.imageset/Contents.json new file mode 100644 index 0000000000..b25071d138 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/stp_card_visa.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "stp_card_visa.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_card_visa@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/stp_card_visa.imageset/stp_card_visa.png b/Telegram-Mac/Assets.xcassets/stp_card_visa.imageset/stp_card_visa.png new file mode 100644 index 0000000000..39728b7756 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_visa.imageset/stp_card_visa.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_visa.imageset/stp_card_visa@2x.png b/Telegram-Mac/Assets.xcassets/stp_card_visa.imageset/stp_card_visa@2x.png new file mode 100644 index 0000000000..05997f02ef Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_visa.imageset/stp_card_visa@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_visa_template.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/stp_card_visa_template.imageset/Contents.json new file mode 100644 index 0000000000..52498e600a --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/stp_card_visa_template.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "stp_card_visa_template.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "stp_card_visa_template@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Telegram-Mac/Assets.xcassets/stp_card_visa_template.imageset/stp_card_visa_template.png b/Telegram-Mac/Assets.xcassets/stp_card_visa_template.imageset/stp_card_visa_template.png new file mode 100644 index 0000000000..24f3081591 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_visa_template.imageset/stp_card_visa_template.png differ diff --git a/Telegram-Mac/Assets.xcassets/stp_card_visa_template.imageset/stp_card_visa_template@2x.png b/Telegram-Mac/Assets.xcassets/stp_card_visa_template.imageset/stp_card_visa_template@2x.png new file mode 100644 index 0000000000..12ef2ce182 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/stp_card_visa_template.imageset/stp_card_visa_template@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/tabsselect_left_gray.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/tabsselect_left_gray.imageset/Contents.json new file mode 100644 index 0000000000..fbf8b7ae00 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/tabsselect_left_gray.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "tabsselect_left_gray.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "tabsselect_left_gray@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/tabsselect_left_gray.imageset/tabsselect_left_gray.png b/Telegram-Mac/Assets.xcassets/tabsselect_left_gray.imageset/tabsselect_left_gray.png new file mode 100644 index 0000000000..ac78900b7f Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/tabsselect_left_gray.imageset/tabsselect_left_gray.png differ diff --git a/Telegram-Mac/Assets.xcassets/tabsselect_left_gray.imageset/tabsselect_left_gray@2x.png b/Telegram-Mac/Assets.xcassets/tabsselect_left_gray.imageset/tabsselect_left_gray@2x.png new file mode 100644 index 0000000000..7393f3176d Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/tabsselect_left_gray.imageset/tabsselect_left_gray@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/tabsselect_left_systemcol.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/tabsselect_left_systemcol.imageset/Contents.json new file mode 100644 index 0000000000..37138cc61f --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/tabsselect_left_systemcol.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "tabsselect_left_systemcol.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "tabsselect_left_systemcol@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/tabsselect_left_systemcol.imageset/tabsselect_left_systemcol.png b/Telegram-Mac/Assets.xcassets/tabsselect_left_systemcol.imageset/tabsselect_left_systemcol.png new file mode 100644 index 0000000000..0e3014a9e5 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/tabsselect_left_systemcol.imageset/tabsselect_left_systemcol.png differ diff --git a/Telegram-Mac/Assets.xcassets/tabsselect_left_systemcol.imageset/tabsselect_left_systemcol@2x.png b/Telegram-Mac/Assets.xcassets/tabsselect_left_systemcol.imageset/tabsselect_left_systemcol@2x.png new file mode 100644 index 0000000000..7d5fded832 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/tabsselect_left_systemcol.imageset/tabsselect_left_systemcol@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/tabsselect_top_gray.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/tabsselect_top_gray.imageset/Contents.json new file mode 100644 index 0000000000..4035b537c7 --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/tabsselect_top_gray.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "tabsselect_top_gray.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "tabsselect_top_gray@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/tabsselect_top_gray.imageset/tabsselect_top_gray.png b/Telegram-Mac/Assets.xcassets/tabsselect_top_gray.imageset/tabsselect_top_gray.png new file mode 100644 index 0000000000..37e77339ac Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/tabsselect_top_gray.imageset/tabsselect_top_gray.png differ diff --git a/Telegram-Mac/Assets.xcassets/tabsselect_top_gray.imageset/tabsselect_top_gray@2x.png b/Telegram-Mac/Assets.xcassets/tabsselect_top_gray.imageset/tabsselect_top_gray@2x.png new file mode 100644 index 0000000000..717879cc61 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/tabsselect_top_gray.imageset/tabsselect_top_gray@2x.png differ diff --git a/Telegram-Mac/Assets.xcassets/tabsselect_top_systemcol.imageset/Contents.json b/Telegram-Mac/Assets.xcassets/tabsselect_top_systemcol.imageset/Contents.json new file mode 100644 index 0000000000..fc677b297b --- /dev/null +++ b/Telegram-Mac/Assets.xcassets/tabsselect_top_systemcol.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "tabsselect_top_systemcol.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "tabsselect_top_systemcol@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Telegram-Mac/Assets.xcassets/tabsselect_top_systemcol.imageset/tabsselect_top_systemcol.png b/Telegram-Mac/Assets.xcassets/tabsselect_top_systemcol.imageset/tabsselect_top_systemcol.png new file mode 100644 index 0000000000..5403c2fd16 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/tabsselect_top_systemcol.imageset/tabsselect_top_systemcol.png differ diff --git a/Telegram-Mac/Assets.xcassets/tabsselect_top_systemcol.imageset/tabsselect_top_systemcol@2x.png b/Telegram-Mac/Assets.xcassets/tabsselect_top_systemcol.imageset/tabsselect_top_systemcol@2x.png new file mode 100644 index 0000000000..7e32ccad39 Binary files /dev/null and b/Telegram-Mac/Assets.xcassets/tabsselect_top_systemcol.imageset/tabsselect_top_systemcol@2x.png differ diff --git a/Telegram-Mac/AudioCommandCenter.swift b/Telegram-Mac/AudioCommandCenter.swift new file mode 100644 index 0000000000..4e7f9aca94 --- /dev/null +++ b/Telegram-Mac/AudioCommandCenter.swift @@ -0,0 +1,196 @@ +// +// AudioCommandCenter.swift +// Telegram +// +// Created by Mikhail Filimonov on 18.06.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import MediaPlayer +import SwiftSignalKit +import TelegramCore + +import Postbox +import TGUIKit + + +@available(macOS 10.12.2, *) +final class AudioCommandCenter : NSObject, APDelegate { + + + let commandor: MPRemoteCommandCenter = MPRemoteCommandCenter.shared() + let center: MPNowPlayingInfoCenter = MPNowPlayingInfoCenter.default() + private let disposable = MetaDisposable() + private weak var controller: APController? + private var cache:[MessageId : NSImage] = [:] + + init(_ controller: APController) { + self.controller = controller + super.init() + controller.add(listener: self) + + + commandor.pauseCommand.addTarget(handler: { [weak controller] event in + _ = controller?.pause() + return .success + }) + commandor.playCommand.addTarget(handler: { [weak controller] event in + _ = controller?.play() + return .success + }) + commandor.stopCommand.addTarget(handler: { [weak controller] event in + controller?.stop() + return .success + }) + commandor.togglePlayPauseCommand.addTarget(handler: { [weak controller] event in + controller?.playOrPause() + return .success + }) + commandor.nextTrackCommand.addTarget(handler: { [weak controller] event in + controller?.next() + return .success + }) + commandor.previousTrackCommand.addTarget(handler: { [weak controller] event in + controller?.prev() + return .success + }) + + commandor.changePlaybackPositionCommand.addTarget(handler: { [weak controller] event in + if let event = event as? MPChangePlaybackPositionCommandEvent { + if let duration = controller?.currentSong?.duration { + controller?.set(trackProgress: Float(event.positionTime / TimeInterval(duration))) + } + } + return .success + }) + } + + + deinit { + commandor.pauseCommand.removeTarget(self) + commandor.playCommand.removeTarget(self) + commandor.stopCommand.removeTarget(self) + commandor.togglePlayPauseCommand.removeTarget(self) + commandor.nextTrackCommand.removeTarget(self) + commandor.previousTrackCommand.removeTarget(self) + commandor.changePlaybackPositionCommand.removeTarget(self) + + center.nowPlayingInfo = [:] + center.playbackState = .stopped + } + + private var song: APSongItem? + private var status: APController.State.Status? + private func update(_ controller: APController) { + commandor.nextTrackCommand.isEnabled = controller.nextEnabled + commandor.previousTrackCommand.isEnabled = controller.prevEnabled + + + if let song = controller.currentSong, self.song?.entry != song.entry { + + self.song = song + + var info:[String : Any] = [:] + + info[MPMediaItemPropertyTitle] = song.performerName + info[MPMediaItemPropertyArtist] = song.songName + info[MPMediaItemPropertyPlaybackDuration] = song.duration + if #available(macOS 10.13.2, *) { + + switch song.entry { + case let .song(message): + let file = message.media.first as! TelegramMediaFile + let resource: TelegramMediaResource? + if file.previewRepresentations.isEmpty { + if !file.mimeType.contains("ogg") { + resource = ExternalMusicAlbumArtResource(title: file.musicText.0, performer: file.musicText.1, isThumbnail: true) + } else { + resource = nil + } + } else { + resource = file.previewRepresentations.first!.resource + } + + if let resource = resource { + let iconSize = NSMakeSize(50, 50) + + let arguments = TransformImageArguments(corners: .init(), imageSize: iconSize, boundingSize: iconSize, intrinsicInsets: NSEdgeInsets()) + + let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: message.id.toInt64()), representations: [TelegramMediaImageRepresentation(dimensions: PixelDimensions(iconSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + + + let signal = chatMessagePhotoThumbnail(account: controller.context.account, imageReference: .message(message: MessageReference(message), media: image)) |> deliverOnMainQueue + + if let image = cache[message.id] { + info[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: iconSize, requestHandler: { size in + return image + }) + } else { + self.disposable.set(signal.start(next: { [weak self] data in + let image = data.execute(arguments, data.data)?.generateImage() + if let image = image { + let image = NSImage(cgImage: image, size: iconSize) + self?.center.nowPlayingInfo?[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: iconSize, requestHandler: { size in + return image + }) + self?.cache[message.id] = image + } + })) + } + + } else { + disposable.set(nil) + } + + default: + disposable.set(nil) + } + + } + center.nowPlayingInfo = info + } + + + if self.status != controller.state.status { + self.status = controller.state.status + switch controller.state.status { + case .paused: + center.playbackState = .paused + case .playing: + center.nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = controller.currentTime + center.playbackState = .playing + case .stopped: + center.playbackState = .stopped + case .waiting: + center.playbackState = .unknown + } + } + + } + + func songDidChanged(song: APSongItem, for controller: APController, animated: Bool) { + update(controller) + } + + func songDidChangedState(song: APSongItem, for controller: APController, animated: Bool) { + update(controller) + } + + func songDidStartPlaying(song: APSongItem, for controller: APController, animated: Bool) { + update(controller) + } + + func songDidStopPlaying(song: APSongItem, for controller: APController, animated: Bool) { + update(controller) + } + + func playerDidChangedTimebase(song: APSongItem, for controller: APController, animated: Bool) { + update(controller) + } + + func audioDidCompleteQueue(for controller: APController, animated: Bool) { + update(controller) + } + +} diff --git a/Telegram-Mac/AudioPlayer.swift b/Telegram-Mac/AudioPlayer.swift deleted file mode 100644 index 5a7d0670f2..0000000000 --- a/Telegram-Mac/AudioPlayer.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// AudioPlayer.swift -// TelegramMac -// -// Created by keepcoder on 22/11/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import SwiftSignalKitMac -import TelegramCoreMac - -protocol AudioPlayerDelegate : class { - func audioPlayerDidFinishPlaying(_ audioPlayer:AudioPlayer) - func audioPlayerDidStartPlaying(_ audioPlayer:AudioPlayer) - func audioPlayerDidChangedTimebase(_ audioPlayer:AudioPlayer) - - func audioPlayerDidPaused(_ audioPlayer:AudioPlayer) -} - -private var audioQueue:Queue = Queue(name: "AudioQueue") - -class AudioPlayer: NSObject { - - - - let path:String - public weak var delegate:AudioPlayerDelegate? - - static func player(for path:String) -> AudioPlayer { - - if OpusObjcBridge.canPlayFile(path) { - return OpusAudioPlayer(path) - } - - return NativeAudioPlayer(path) - } - - init(_ path:String) { - self.path = path - } - - func play() { - - } - func pause() { - - } - func stop() { - - } - func reset() { - - } - func cleanup() { - - } - - deinit { - cleanup() - } - - func playFrom(position:TimeInterval) { - - } - - func set(position:TimeInterval) { - - } - - var duration:TimeInterval { - return 0.0 - } - - var currentTime:TimeInterval { - return 0.0 - } - - var queue:Queue { - return audioQueue - } - - var timebase:CMTimebase? { - return nil - } - -} diff --git a/Telegram-Mac/AudioPlayerController.swift b/Telegram-Mac/AudioPlayerController.swift index 7ffd29ca3a..258e455fdd 100644 --- a/Telegram-Mac/AudioPlayerController.swift +++ b/Telegram-Mac/AudioPlayerController.swift @@ -7,69 +7,51 @@ // import Cocoa -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit import TGUIKit +import AVKit + + + class APSingleWrapper { let resource:TelegramMediaResource let name:String? + let mimeType: String let performer:String? let id:AnyHashable - init(resource:TelegramMediaResource, name:String?, performer:String?, id: AnyHashable) { + let duration: Int32? + init(resource:TelegramMediaResource, mimeType: String = "mp3", name:String?, performer:String?, duration: Int32?, id: AnyHashable) { self.resource = resource self.name = name + self.mimeType = mimeType self.performer = performer self.id = id + self.duration = duration } } -fileprivate(set) var globalAudio:APController? +let globalAudioPromise: Promise = Promise(nil) + +fileprivate(set) var globalAudio:APController? { + didSet { + globalAudioPromise.set(.single(globalAudio)) + } +} enum APState : Equatable { case waiting - case playing(current:TimeInterval,duration:TimeInterval, progress:TimeInterval, animated:Bool) // current, duration - case paused(current:TimeInterval,duration:TimeInterval, progress:TimeInterval, animated:Bool) + case playing(current:TimeInterval,duration:TimeInterval, progress:TimeInterval) // current, duration + case paused(current:TimeInterval,duration:TimeInterval, progress:TimeInterval) case stoped - case fetching(Float, Bool) -} -func ==(lhs:APState, rhs:APState) -> Bool { - switch lhs { - case .waiting: - if case .waiting = rhs { - return true - } else { - return false - } - case let .paused(lhsVars): - if case let .paused(rhsVars) = rhs, lhsVars.current == rhsVars.current && lhsVars.duration == rhsVars.duration { - return true - } else { - return false - } - case .stoped: - if case .stoped = rhs { - return true - } else { - return false - } - case let .playing(lhsVars): - if case let .playing(rhsVars) = rhs, lhsVars.current == rhsVars.current && lhsVars.duration == rhsVars.duration { - return true - } else { - return false - } - case let .fetching(lhsCurrent, _): - if case let .fetching(rhsCurrent, _) = rhs, lhsCurrent == rhsCurrent { - return true - } else { - return false - } - } + case fetching(Float) } + struct APResource { let complete:Bool let progress:Float @@ -78,13 +60,58 @@ struct APResource { class APItem : Equatable { - fileprivate(set) var state:APState = .waiting { - didSet { - _state.set(.single(state)) + private(set) var status: MediaPlayerStatus = MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: 0, dimensions: CGSize(), timestamp: 0, baseRate: 1.0, volume: 1.0, seekId: 0, status: .paused) + + func setStatus(_ status: MediaPlayerStatus, rate: Double) { + + let status = status.withUpdatedDuration(max(status.duration, self.status.duration)) + + var progress:TimeInterval = (status.timestamp / status.duration) + if progress.isNaN { + progress = 0 + } + + if !progress.isFinite { + progress = 1.0 + } + + switch status.status { + case .playing: + self.state = .playing(current: status.timestamp, duration: status.duration, progress: progress) + case .paused: + self.state = .paused(current: status.timestamp, duration: status.duration, progress: progress) + default: + self.state = .paused(current: status.timestamp, duration: status.duration, progress: progress) } + self.status = status } - fileprivate let _state:Promise = Promise() + func setProgress(_ progress: TimeInterval) { + switch status.status { + case .playing: + self.state = .playing(current: status.timestamp, duration: status.duration, progress: progress) + case .paused: + self.state = .paused(current: status.timestamp, duration: status.duration, progress: progress) + default: + break + } + } + + private var _state: APState = .waiting + fileprivate(set) var state:APState { + get { + return _state + } + set { + _state = newValue + _stateValue.set(.single(newValue)) + } + } + + private let _stateValue:Promise = Promise() + var stateValue: Signal { + return _stateValue.get() + } let entry:APEntry let account:Account @@ -101,21 +128,7 @@ func ==(lhs:APItem, rhs:APItem) -> Bool { return lhs.stableId == rhs.stableId } -class APHoleItem : APItem { - private let hole:MessageHistoryHole - override init(_ entry:APEntry, _ account:Account) { - if case let .hole(hole) = entry { - self.hole = hole - } else { - fatalError() - } - super.init(entry, account) - } - - override var stableId: ChatHistoryEntryId { - return hole.chatStableId - } -} + class APSongItem : APItem { let songName:String @@ -123,7 +136,7 @@ class APSongItem : APItem { let resource:TelegramMediaResource let ext:String private let fetchDisposable:MetaDisposable = MetaDisposable() - + override init(_ entry:APEntry, _ account:Account) { if case let .song(message) = entry { let file = (message.media.first as! TelegramMediaFile) @@ -139,25 +152,25 @@ class APSongItem : APItem { } if file.isVoice || file.isInstantVideo { if let forward = message.forwardInfo { - performerName = forward.author.displayTitle + songName = forward.authorTitle } else if let peer = message.author { if peer.id == account.peerId { - performerName = localizedString("You"); + songName = localizedString("You"); } else { - performerName = peer.displayTitle + songName = peer.displayTitle } } else { - performerName = "" + songName = "" } if file.isVoice { - songName = tr(.audioControllerVoiceMessage) + performerName = L10n.audioControllerVoiceMessage } else { - songName = tr(.audioControllerVideoMessage) + performerName = L10n.audioControllerVideoMessage } } else { var t:String? var p:String? - + for attribute in file.attributes { if case let .Audio(_, _, title, performer, _) = attribute { t = title @@ -168,15 +181,15 @@ class APSongItem : APItem { if let t = t { songName = t } else { - songName = tr(.audioUntitledSong) + songName = p != nil ? L10n.audioUntitledSong : "" } if let p = p { performerName = p } else { - performerName = tr(.audioUnknownArtist) + performerName = file.fileName ?? L10n.audioUnknownArtist } } - + } else if case let .single(wrapper) = entry { resource = wrapper.resource @@ -190,32 +203,116 @@ class APSongItem : APItem { } else { performerName = "" } - self.ext = "m4a" + if let _ = wrapper.mimeType.range(of: "m4a") { + self.ext = "m4a" + } else if let _ = wrapper.mimeType.range(of: "mp4") { + self.ext = "mp4" + } else { + self.ext = "mp3" + } } else { fatalError("🤔") } super.init(entry, account) } - + override var stableId: ChatHistoryEntryId { return entry.stableId } - private func fetch() { - fetchDisposable.set(account.postbox.mediaBox.fetchedResource(resource, tag: TelegramMediaResourceFetchTag(statsCategory: .audio)).start()) + var isPaused: Bool { + switch self.state { + case .paused: + return true + default: + return false + } + } + var isFetching: Bool { + switch self.state { + case .fetching: + return true + default: + return false + } + } + + var reference: MediaResourceReference { + switch entry { + case let .song(message): + return FileMediaReference.message(message: MessageReference(message), media: message.media.first as! TelegramMediaFile).resourceReference(resource) + default: + return MediaResourceReference.standalone(resource: resource) + } } + var coverImageMediaReference: ImageMediaReference? { + if let resource = coverResource { + let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [TelegramMediaImageRepresentation(dimensions: PixelDimensions(PeerMediaIconSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + + switch self.entry { + case let .song(message): + return ImageMediaReference.message(message: MessageReference(message), media: image) + default: + return ImageMediaReference.standalone(media: image) + } + } + return nil + } + + var coverResource: TelegramMediaResource? { + switch entry { + case let .song(message): + if let file = message.media.first as? TelegramMediaFile { + if file.previewRepresentations.isEmpty { + if ext == "mp3" { + return ExternalMusicAlbumArtResource(title: file.musicText.0, performer: file.musicText.1, isThumbnail: true) + } else { + return nil + } + } else { + return file.previewRepresentations.first!.resource + } + } else { + if ext == "mp3" { + return ExternalMusicAlbumArtResource(title: songName, performer: performerName, isThumbnail: true) + } else { + return nil + } + } + default: + if ext == "mp3" { + return ExternalMusicAlbumArtResource(title: songName, performer: performerName, isThumbnail: true) + } else { + return nil + } + } + } + + var duration: Int32? { + switch entry { + case let .song(message): + return (message.media.first as? TelegramMediaFile)?.duration + case let .single(wrapper): + return wrapper.duration + } + } + + private func fetch() { + fetchDisposable.set(fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: reference).start()) + } + private func cancelFetching() { fetchDisposable.set(nil) } - + deinit { fetchDisposable.dispose() } - - fileprivate func pullResource()->Signal { + + fileprivate func pullResource()->Signal { fetch() - return account.postbox.mediaBox.resourceStatus(resource) |> deliverOnMainQueue |> mapToSignal { [weak self] status -> Signal in + return account.postbox.mediaBox.resourceStatus(resource) |> deliverOnMainQueue |> mapToSignal { [weak self] status -> Signal in if let strongSelf = self { let ext = strongSelf.ext switch status { @@ -233,10 +330,10 @@ class APSongItem : APItem { return .complete() } } |> deliverOnMainQueue - + } - - + + } struct APTransition { @@ -245,26 +342,21 @@ struct APTransition { let updated:[(Int,APItem)] } -fileprivate func prepareItems(from:[APEntry]?, to:[APEntry], account:Account) -> Signal { +fileprivate func prepareItems(from:[APEntry]?, to:[APEntry], account:Account) -> Signal { return Signal {(subscriber) in - let (removed, inserted, updated) = proccessEntries(from, right: to, { (entry) -> APItem in - switch entry { case .song: return APSongItem(entry,account) - case .hole: - return APHoleItem(entry,account) case .single: return APSongItem(entry,account) } - + }) - subscriber.putNext(APTransition(inserted: inserted, removed: removed, updated:updated)) subscriber.putCompletion() return EmptyDisposable - + } |> runOn(prepareQueue) } @@ -275,7 +367,6 @@ enum APHistoryLocation : Equatable { enum APEntry : Comparable, Identifiable { case song(Message) - case hole(MessageHistoryHole) case single(APSingleWrapper) var stableId: ChatHistoryEntryId { switch self { @@ -285,24 +376,38 @@ enum APEntry : Comparable, Identifiable { if let stableId = wrapper.id.base as? ChatHistoryEntryId { return stableId } - return .maybeId(wrapper.id) - case let .hole(hole): - return hole.chatStableId + return .maybeId(wrapper.id) + } } - + func isEqual(to wrapper:APSingleWrapper) -> Bool { return stableId == .maybeId(wrapper.id) } - + func isEqual(to message:Message) -> Bool { return stableId == message.chatStableId } + func isEqual(to messageId: MessageId) -> Bool { + switch self { + case let .song(message): + return message.id == messageId + case let .single(wrapper): + if let stableId = wrapper.id.base as? ChatHistoryEntryId { + switch stableId { + case let .message(message): + return message.id == messageId + default: + break + } + } + } + return false + } + var index: MessageIndex { switch self { - case let .hole(hole): - return hole.maxIndex case let .song(message): return MessageIndex(message) case .single(_): @@ -321,12 +426,6 @@ func ==(lhs:APEntry, rhs:APEntry) -> Bool { } case .single(_): return false - case let .hole(lhsHole): - if case let .hole(rhsHole) = rhs, lhsHole == rhsHole { - return true - } else { - return false - } } } @@ -357,44 +456,121 @@ func ==(lhs:APHistoryLocation, rhs:APHistoryLocation) -> Bool { } protocol APDelegate : class { - func songDidChanged(song:APSongItem, for controller:APController) - func songDidChangedState(song:APSongItem, for controller:APController) - func songDidStartPlaying(song:APSongItem, for controller:APController) - func songDidStopPlaying(song:APSongItem, for controller:APController) - func playerDidChangedTimebase(song:APSongItem, for controller:APController) - func audioDidCompleteQueue(for controller:APController) + func songDidChanged(song:APSongItem, for controller:APController, animated: Bool) + func songDidChangedState(song:APSongItem, for controller:APController, animated: Bool) + func songDidStartPlaying(song:APSongItem, for controller:APController, animated: Bool) + func songDidStopPlaying(song:APSongItem, for controller:APController, animated: Bool) + func playerDidChangedTimebase(song:APSongItem, for controller:APController, animated: Bool) + func audioDidCompleteQueue(for controller:APController, animated: Bool) } -class APController : NSObject, AudioPlayerDelegate { + + +class APController : NSResponder { + + struct State : Equatable { + enum Status : Equatable { + case playing + case paused + case waiting + case stopped + } + enum RepeatState { + case none + case circle + case one + } + enum OrderState { + case normal + case reversed + case random + } + fileprivate(set) var status: Status + fileprivate(set) var repeatState: RepeatState + fileprivate(set) var orderState: OrderState + + fileprivate(set) var volume: Float + fileprivate(set) var baseRate: Double + + static var `default`:State { + return State(status: .waiting, repeatState: .none, orderState: .normal, volume: 1, baseRate: 1.0) + } + + } + + private var mediaPlayer: MediaPlayer? + + private let statusDisposable = MetaDisposable() + private let readyDisposable = MetaDisposable() + + + private let statePromise = ValuePromise(State.default, ignoreRepeated: true) + var stateValue: Signal { + return statePromise.get() + } + private(set) var state: State = State.default { + didSet { + statePromise.set(self.state) + if oldValue != state { + if oldValue.baseRate != state.baseRate { + mediaPlayer?.setBaseRate(state.baseRate) + } + if oldValue.volume != state.volume { + mediaPlayer?.setVolume(state.volume) + } + notifyGlobalStateChanged(animated: true) + } + } + } + public let ready:Promise = Promise() - let account:Account + let context: AccountContext + var account: Account { + return context.account + } + + private var _timebase: CMTimebase? + fileprivate let history:Promise = Promise() fileprivate let entries:Atomic = Atomic(value:nil) fileprivate let items:Atomic<[APItem]> = Atomic(value:[]) fileprivate let disposable:MetaDisposable = MetaDisposable() - + fileprivate let itemDisposable:MetaDisposable = MetaDisposable() fileprivate let songStateDisposable:MetaDisposable = MetaDisposable() - + fileprivate let timebaseDisposable:MetaDisposable = MetaDisposable() private var listeners:[WeakReference] = [] - - fileprivate var player:AudioPlayer? + + // fileprivate var player:AudioPlayer? fileprivate var current:Int = -1 - fileprivate(set) var needRepeat:Bool = false + fileprivate var played:[Int] = [] + + private let bufferingStatusValuePromise = Promise<(IndexSet, Int)?>() - fileprivate var timer:SwiftSignalKitMac.Timer? + private(set) var bufferingStatus: Signal<(IndexSet, Int)?, NoError> { + set { + self.bufferingStatusValuePromise.set(newValue) + } + get { + return bufferingStatusValuePromise.get() + } + } + + + fileprivate var timer:SwiftSignalKit.Timer? + + fileprivate var prevNextDisposable = DisposableSet() + private var _song:APSongItem? fileprivate var song:APSongItem? { set { self.stop() _song = newValue if let song = newValue { - songStateDisposable.set((song._state.get() |> distinctUntilChanged).start(next: {[weak self] (state) in - if let strongSelf = self { - strongSelf.notifyStateChanged(item: song) - } + songStateDisposable.set((song.stateValue |> distinctUntilChanged).start(next: { [weak self] _ in + self?.notifyStateChanged(item: song, animated: true) })) } else { songStateDisposable.set(nil) @@ -404,77 +580,148 @@ class APController : NSObject, AudioPlayerDelegate { return _song } } - + var timebase:CMTimebase? { - return self.player?.timebase + return _timebase//self.player?.timebase } - - private func notifyStateChanged(item:APSongItem) { + + func notifyGlobalStateChanged(animated: Bool) { + if let song = song { + notifyStateChanged(item: song, animated: animated) + } + } + + var isPlaying: Bool { + if let currentSong = currentSong { + switch currentSong.state { + case .playing: + return true + default: + return false + } + } + return false + } + + var isDownloading: Bool { + if let currentSong = currentSong { + switch currentSong.state { + case .fetching: + return true + default: + return false + } + } + return false + } + + private func notifyStateChanged(item:APSongItem, animated: Bool) { for listener in listeners { if let value = listener.value as? APDelegate { - value.songDidChangedState(song: item, for: self) + value.songDidChangedState(song: item, for: self, animated: animated) } } } - - private func notifySongChanged(item:APSongItem) { - for listener in listeners { + + private func notifySongChanged(item:APSongItem, animated: Bool) { + for listener in self.listeners { if let value = listener.value as? APDelegate { - value.songDidChanged(song: item, for: self) + value.songDidChanged(song: item, for: self, animated: animated) } } } - - private func notifySongDidStartPlaying(item:APSongItem) { - for listener in listeners { + + private func notifySongDidStartPlaying(item:APSongItem,animated: Bool) { + for listener in self.listeners { if let value = listener.value as? APDelegate { - value.songDidStartPlaying(song: item, for: self) + value.songDidStartPlaying(song: item, for: self, animated: animated) } } } - private func notifySongDidChangedTimebase(item:APSongItem) { - for listener in listeners { + private func notifySongDidChangedTimebase(item:APSongItem, animated: Bool) { + for listener in self.listeners { if let value = listener.value as? APDelegate { - value.playerDidChangedTimebase(song: item, for: self) + value.playerDidChangedTimebase(song: item, for: self, animated: animated) } } } - - - - private func notifySongDidStopPlaying(item:APSongItem) { - for listener in listeners { + + + + private func notifySongDidStopPlaying(item:APSongItem, animated: Bool) { + for listener in self.listeners { if let value = listener.value as? APDelegate { - value.songDidStopPlaying(song: item, for: self) + value.songDidStopPlaying(song: item, for: self, animated: animated) } } } - - func notifyCompleteQueue() { - for listener in listeners { + + func notifyCompleteQueue(animated: Bool) { + for listener in self.listeners { if let value = listener.value as? APDelegate { - value.audioDidCompleteQueue(for: self) + value.audioDidCompleteQueue(for: self, animated: animated) } } } + + private let streamable: Bool + var baseRate: Double { + set { + state.baseRate = newValue + } + get { + return state.baseRate + } + } + var volume: Float { + set { + state.volume = newValue + } + get { + return state.volume + } + } + fileprivate var _commandCenter: Any? = nil - init(account:Account) { - self.account = account - super.init() + @available(macOS 10.12.2, *) + private func commandCenter()->AudioCommandCenter? { + return self._commandCenter as? AudioCommandCenter } + init(context: AccountContext, streamable: Bool, baseRate: Double, volume: Float) { + self.context = context + self.state.volume = volume + self.streamable = streamable + self.state.baseRate = baseRate + super.init() + + } + + @objc open func windowDidBecomeKey() { + + } + + + @objc open func windowDidResignKey() { + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + func start() { globalAudio?.stop() globalAudio?.cleanup() + globalAudio = self - account.context.mediaKeyTap?.startWatchingMediaKeys() } - - + + fileprivate func merge(with transition:APTransition) { - - var previous:[APItem] = self.items.modify({$0}) + + let previous:[APItem] = self.items.modify({$0}) let current = self.current let items = self.items.modify { items -> [APItem] in var new:[APItem] = items @@ -489,8 +736,8 @@ class APController : NSObject, AudioPlayerDelegate { } return new } - - if current != -1, current > 0 { + + if current != -1, current >= 0 { if current < previous.count { let previousCurrent = previous[current] var foundIndex:Int? = nil @@ -512,248 +759,379 @@ class APController : NSObject, AudioPlayerDelegate { } } } - + } - + fileprivate var pullItems:[APItem] { - return items.modify({$0}) + return items.with { $0 } } - func toggleRepeat() { - needRepeat = !needRepeat + func nextOrderState() { + switch self.state.orderState { + case .normal: + self.state.orderState = .reversed + case .reversed: + self.state.orderState = .random + case .random: + self.state.orderState = .normal + } } - - var needLoop:Bool { - return true + func nextRepeatState() { + switch self.state.repeatState { + case .none: + self.state.repeatState = .circle + case .circle: + self.state.repeatState = .one + case .one: + self.state.repeatState = .none + } } - func next() { - if !nextEnabled { - return - } - if current == 0 { - current = pullItems.count - 1 - } else { - current -= 1 + var canMakeRepeat: Bool { + return false + } + var canMakeOrder: Bool { + return false + } + + func playOrPause() { + if let _ = song { + if case .playing = state.status { + mediaPlayer?.pause() + state.status = .paused + } else if case .paused = state.status { + mediaPlayer?.play() + state.status = .playing + } else if state.status == .stopped { + dequeueCurrent() + } } - dequeueCurrent() } - func playOrPause() { - if let song = song { - if case .playing = song.state { - player?.pause() - } else if case .paused = song.state { - player?.play() - } else if song.state == .stoped { + func playOrPause(_ id: APSingleWrapper) -> Bool { + return playOrPause(pullItems.firstIndex(where: { $0.entry.isEqual(to: id) })) + } + + func playOrPause(_ id: MessageId) -> Bool { + return playOrPause(pullItems.firstIndex(where: { $0.entry.isEqual(to: id) })) + } + + private func playOrPause(_ index: Int?) -> Bool { + if let index = index { + if index != self.current { + self.current = index dequeueCurrent() + } else { + playOrPause() } + return true + } else { + return false } } func pause() -> Bool { if let song = song { if case .playing = song.state { - player?.pause() + mediaPlayer?.pause() + state.status = .paused return true } } return false } - + func play() -> Bool { - if let song = song { - if case .paused = song.state { - player?.play() + if let _ = song { + if case .paused = state.status { + mediaPlayer?.play() + state.status = .playing return true } } return false } + + func next() { + if !nextEnabled { + return + } + switch self.state.orderState { + case .normal: + if current == 0 { + current = pullItems.count - 1 + } else { + current -= 1 + } + case .reversed: + if current == pullItems.count - 1 { + current = 0 + } else { + current += 1 + } + case .random: + played.append(current) + current = Int.random(in: 0 ..< pullItems.count) + } + + dequeueCurrent() + } + func prev() { if !prevEnabled { return } - if current == pullItems.count - 1 { - current = 0 - } else { - current += 1 + switch self.state.orderState { + case .normal: + if current == pullItems.count - 1 { + current = 0 + } else { + current += 1 + } + case .reversed: + if current == 0 { + current = pullItems.count - 1 + } else { + current -= 1 + } + case .random: + if !played.isEmpty { + current = played.removeLast() + } else { + current = Int.random(in: 0 ..< pullItems.count) + } } + dequeueCurrent() } - + var nextEnabled:Bool { return pullItems.count > 1 } - - + + var prevEnabled:Bool { return pullItems.count > 1 } - + var needNext:Bool { return true } - + func complete() { - notifyCompleteQueue() + notifyCompleteQueue(animated: true) + state.status = .stopped + cleanup() } - + var currentSong:APSongItem? { if !pullItems.isEmpty, pullItems.count > current, let song = pullItems[max(0, current)] as? APSongItem { return song } return nil } - + fileprivate func dequeueCurrent() { if let current = currentSong { self.song = current - notifySongChanged(item: current) play(with: current) + notifySongChanged(item: current, animated: true) } } - + + fileprivate func play(with item:APSongItem) { - itemDisposable.set(item.pullResource().start(next: { [weak self] resource in - if let strongSelf = self { - if resource.complete { - strongSelf.player = .player(for: resource.path) - strongSelf.player?.delegate = strongSelf - strongSelf.player?.play() - } else { - item.state = .fetching(resource.progress,true) - } + self.mediaPlayer?.seek(timestamp: 0) + + let player = MediaPlayer(postbox: account.postbox, reference: item.reference, streamable: streamable, video: false, preferSoftwareDecoding: false, enableSound: true, baseRate: baseRate, volume: self.volume, fetchAutomatically: false) + + player.play() + state.status = .playing + player.actionAtEnd = .action({ [weak self] in + Queue.mainQueue().async { + self?.audioPlayerDidFinishPlaying() + } + }) + + self.mediaPlayer = player + + + let size = item.resource.size ?? 0 + bufferingStatus = account.postbox.mediaBox.resourceRangesStatus(item.resource) + |> map { ranges -> (IndexSet, Int) in + return (ranges, size) + } + + timebaseDisposable.set((player.timebase |> deliverOnMainQueue).start(next: { [weak self] timebase in + self?._timebase = timebase + self?.notifySongDidChangedTimebase(item: item, animated: true) + })) + + self.statusDisposable.set((player.status |> deliverOnMainQueue).start(next: { [weak self] status in + guard let `self` = self else {return} + item.setStatus(status, rate: self.baseRate) + switch status.status { + case .paused: + self.stopTimer() + case .playing: + self.startTimer() + default: + self.stopTimer() } + self.updateUIAfterTick(status) })) + +// + if !streamable { + itemDisposable.set(item.pullResource().start(next: { [weak self] resource in + if let strongSelf = self { + if resource.complete { + let items = strongSelf.items.modify({$0}).filter({$0 is APSongItem}).map{$0 as! APSongItem} + if let index = items.firstIndex(of: item) { + let previous = index - 1 + let next = index + 1 + if previous >= 0 { + strongSelf.prevNextDisposable.add(fetchedMediaResource(mediaBox: strongSelf.account.postbox.mediaBox, reference: items[previous].reference, statsCategory: .audio).start()) + } + if next < items.count { + strongSelf.prevNextDisposable.add(fetchedMediaResource(mediaBox: strongSelf.account.postbox.mediaBox, reference: items[next].reference, statsCategory: .audio).start()) + } + } + + } else { + item.state = .fetching(resource.progress) + } + } + })) + } + } - - func audioPlayerDidStartPlaying(_ audioPlayer: AudioPlayer) { + + + var currentTime: TimeInterval { if let current = currentSong { - var progress:TimeInterval = (audioPlayer.currentTime / audioPlayer.duration) - if progress.isNaN { - progress = 1 + switch current.state { + case let .paused(current, _, _), let .playing(current, _, _): + return current + default: + break } - current.state = .playing(current: audioPlayer.currentTime, duration: audioPlayer.duration, progress:progress,animated: false) - notifySongDidStartPlaying(item: current) - startTimer() - if audioPlayer.duration == 0 { - audioPlayerDidFinishPlaying(audioPlayer) + } + return 0//self.player?.currentTime ?? 0 + } + + var duration: TimeInterval { + if let current = currentSong { + switch current.state { + case let .paused(_, duration, _), let .playing(_, duration, _): + return duration + default: + break } } + return 0//self.player?.currentTime ?? 0 } - + var isLatest:Bool { return current == 0 } - - func audioPlayerDidFinishPlaying(_ audioPlayer: AudioPlayer) { - stop() - - if needRepeat { - dequeueCurrent() - } else if needNext && nextEnabled { - if isLatest { - if needLoop { - next() - } else { - complete() - } + + func audioPlayerDidFinishPlaying() { + self.stop() + + switch self.state.repeatState { + case .one: + self.dequeueCurrent() + case .none: + if self.isLatest { + self.complete() + } else if needNext && self.nextEnabled { + self.next() } else { - next() + self.complete() } - } else { - complete() + case .circle: + next() } } - - func audioPlayerDidPaused(_ audioPlayer: AudioPlayer) { - if let song = currentSong { - var progress:TimeInterval = (audioPlayer.currentTime / audioPlayer.duration) - if progress.isNaN { - progress = 1 - } - song.state = .paused(current: audioPlayer.currentTime, duration: audioPlayer.duration, progress: progress, animated: false) - stopTimer() - } - } - - func audioPlayerDidChangedTimebase(_ audioPLayer: AudioPlayer) { + + + func audioPlayerDidChangedTimebase(_ audioPLayer: MediaPlayer) { if let current = currentSong { - notifySongDidChangedTimebase(item: current) + notifySongDidChangedTimebase(item: current, animated: true) } } - + func stop() { - player?.stop() + // player?.stop() + mediaPlayer = nil if let item = song { - notifySongDidStopPlaying(item: item) + notifySongDidStopPlaying(item: item, animated: false) } song?.state = .stoped stopTimer() } - + func set(trackProgress:Float) { - if let player = player, let song = song { - let current = player.duration * Double(trackProgress) - player.set(position:current) - if case .paused = song.state { - var progress:TimeInterval = (current / player.duration) - if progress.isNaN { - progress = 1 - } - song.state = .playing(current: current, duration: player.duration, progress: progress, animated: true) - song.state = .paused(current: current, duration: player.duration, progress: progress, animated: true) - } + if let player = mediaPlayer, let song = song { + let current: Double = song.status.duration * Double(trackProgress) + player.seek(timestamp: current) + song.setProgress(TimeInterval(trackProgress)) + notifyStateChanged(item: song, animated: false) } } - + func cleanup() { listeners.removeAll() globalAudio = nil - account.context.mediaKeyTap?.stopWatchingMediaKeys() + mainWindow.applyResponderIfNeeded() stop() } - - - + + private func updateUIAfterTick(_ status: MediaPlayerStatus) { + + } + + private func startTimer() { - if timer == nil { - timer = SwiftSignalKitMac.Timer(timeout: 0.2, repeat: true, completion: { [weak self] in - if let strongSelf = self, let player = strongSelf.player { - var progress:TimeInterval = (player.currentTime / player.duration) - if progress.isNaN { - progress = 1 - } - strongSelf.song?.state = .playing(current: player.currentTime, duration: player.duration, progress: progress, animated: true) - } - }, queue: Queue.mainQueue()) - timer?.start() - } + var additional: Double = 0.2 + let duration: TimeInterval = 0.2 + timer = SwiftSignalKit.Timer(timeout: duration, repeat: true, completion: { [weak self] in + if let `self` = self, let item = self.song { + let new = item.status.timestamp + additional * item.status.baseRate + item.state = .playing(current: new, duration: item.status.duration, progress: new / max((item.status.duration), 0.2)) + additional += duration + self.updateUIAfterTick(item.status) + } + }, queue: Queue.mainQueue()) + timer?.start() } private func stopTimer() { timer?.invalidate() timer = nil } - + deinit { disposable.dispose() itemDisposable.dispose() songStateDisposable.dispose() - cleanup() + prevNextDisposable.dispose() + readyDisposable.dispose() + statusDisposable.dispose() + timebaseDisposable.dispose() } - + fileprivate var tags:MessageTags { return .music } - + func add(listener:NSObject) { listeners.append(WeakReference(value: listener)) } - + func remove(listener:NSObject) { - let index = listeners.index(where: { (weakValue) -> Bool in + let index = listeners.firstIndex(where: { (weakValue) -> Bool in return listener == weakValue.value }) if let index = index { @@ -763,61 +1141,84 @@ class APController : NSObject, AudioPlayerDelegate { } class APChatController : APController { - - private let peerId:PeerId + + let chatLocationInput:ChatLocationInput + fileprivate let mode: ChatMode private let index:MessageIndex? - - init(account: Account, peerId: PeerId, index: MessageIndex?) { - self.peerId = peerId + let messages: [Message] + init(context: AccountContext, chatLocationInput: ChatLocationInput, mode: ChatMode, index: MessageIndex?, streamable: Bool, baseRate: Double = 1.0, volume: Float = 1.0, messages: [Message] = []) { + self.chatLocationInput = chatLocationInput + self.mode = mode self.index = index - super.init(account: account) + self.messages = messages + super.init(context: context, streamable: streamable, baseRate: baseRate, volume: volume) } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func start() { super.start() let tagMask:MessageTags = self.tags let list = self.entries let items = self.items - let account = self.account - let peerId = self.peerId + let account = self.context.account + let chatLocationInput = self.chatLocationInput let index = self.index - let apply = history.get() |> distinctUntilChanged |> mapToSignal { location -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), Void> in - switch location { - case .initial: - return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: MessageIndex.upperBound(peerId: peerId), count: 100, anchorIndex: MessageIndex.upperBound(peerId: peerId), fixedCombinedReadState: nil, tagMask: tagMask) - case let .index(index): - return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 100, anchorIndex: index, fixedCombinedReadState: nil, tagMask: tagMask) - } - - } |> map { view -> (APHistory?,APHistory) in - var entries:[APEntry] = [] - for viewEntry in view.0.entries { - switch viewEntry { - case let .MessageEntry(message, _, _, _): - entries.append(.song(message)) - case let .HoleEntry(hole, _): - entries.append(.hole(hole)) + let mode = self.mode + let apply: Signal + if messages.isEmpty { + apply = history.get() |> distinctUntilChanged |> mapToSignal { location -> Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> in + switch mode { + case .scheduled: + return account.viewTracker.scheduledMessagesViewForLocation(chatLocationInput, additionalData: []) + default: + switch location { + case .initial: + return account.viewTracker.aroundMessageHistoryViewForLocation(chatLocationInput, index: MessageHistoryAnchorIndex.upperBound, anchorIndex: MessageHistoryAnchorIndex.upperBound, count: 100, fixedCombinedReadStates: nil, tagMask: tagMask, orderStatistics: [], additionalData: []) + case let .index(index): + return account.viewTracker.aroundMessageHistoryViewForLocation(chatLocationInput, index: MessageHistoryAnchorIndex.message(index), anchorIndex: MessageHistoryAnchorIndex.message(index), count: 100, fixedCombinedReadStates: nil, tagMask: tagMask, orderStatistics: [], additionalData: []) + } } + + + } |> map { view -> (APHistory?,APHistory) in + var entries:[APEntry] = [] + for viewEntry in view.0.entries { + if let media = viewEntry.message.media.first as? TelegramMediaFile, media.isMusicFile || media.isInstantVideo || media.isVoice { + entries.append(.song(viewEntry.message)) + } + } + + let new = APHistory(original: view.0, filtred: entries) + return (list.swap(new),new) + } + |> mapToQueue { view -> Signal in + let transition = prepareItems(from: view.0?.filtred, to: view.1.filtred, account: account) + return transition + } |> deliverOnMainQueue + } else { + var entries:[APEntry] = [] + for message in messages { + entries.append(.song(message)) } - - let new = APHistory(original: view.0, filtred: entries) - return (list.swap(new),new) + apply = prepareItems(from: [], to: entries, account: account) |> deliverOnMainQueue } - |> mapToQueue { view -> Signal in - let transition = prepareItems(from: view.0?.filtred, to: view.1.filtred, account: account) - return transition - } |> deliverOnMainQueue + let first:Atomic = Atomic(value:true) disposable.set(apply.start(next: {[weak self] (transition) in - + let isFirst = first.swap(false) - + self?.merge(with: transition) - + if isFirst { if let index = index { - let list:[APItem] = items.modify({$0}) + let list:[APItem] = items.with { $0 } for i in 0 ..< list.count { if list[i].entry.index == index { self?.current = i @@ -825,75 +1226,104 @@ class APChatController : APController { } } } - + self?.dequeueCurrent() self?.ready.set(.single(true)) } - + let list = items.with({ $0 }) + if let song = self?.song, !list.contains(song) { + self?.audioPlayerDidFinishPlaying() + } + })) - + if let index = index { - history.set(.single(.index(index))) + history.set(.single(.index(index)) |> delay(0.1, queue: Queue.mainQueue())) } else { - history.set(.single(.initial)) + history.set(.single(.initial) |> delay(0.1, queue: Queue.mainQueue())) } } } class APChatMusicController : APChatController { - - override init(account: Account, peerId: PeerId, index: MessageIndex?) { - super.init(account: account, peerId: peerId, index: index) + + init(context: AccountContext, chatLocationInput: ChatLocationInput, mode: ChatMode, index: MessageIndex?, baseRate: Double = 1.0, volume: Float = 1.0, messages: [Message] = []) { + super.init(context: context, chatLocationInput: chatLocationInput, mode: mode, index: index, streamable: true, baseRate: baseRate, volume: volume, messages: messages) + if #available(macOS 10.12.2, *) { + self._commandCenter = AudioCommandCenter(self) + } } - + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + fileprivate override var tags: MessageTags { return .music } + + override var canMakeRepeat: Bool { + return true + } + override var canMakeOrder: Bool { + return true + } } class APChatVoiceController : APChatController { private let markAsConsumedDisposable = MetaDisposable() - override init(account: Account, peerId: PeerId, index: MessageIndex?) { - super.init(account: account, peerId: peerId, index:index) + init(context: AccountContext, chatLocationInput: ChatLocationInput, mode: ChatMode, index: MessageIndex?, baseRate: Double = 1.0, volume: Float = 1.0) { + super.init(context: context, chatLocationInput: chatLocationInput, mode: mode, index:index, streamable: false, baseRate: baseRate, volume: volume) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var nextEnabled: Bool { + return current > 0 } + + override var prevEnabled: Bool { + return current < pullItems.count - 1 + } + override func play(with item: APSongItem) { super.play(with: item) - markAsConsumedDisposable.set(markMessageContentAsConsumedInteractively(postbox: account.postbox, messageId: item.entry.index.id).start()) + markAsConsumedDisposable.set(context.engine.messages.markMessageContentAsConsumedInteractively(messageId: item.entry.index.id).start()) } - + deinit { markAsConsumedDisposable.dispose() } - + fileprivate override var tags: MessageTags { return .voiceOrInstantVideo } - - override var needLoop:Bool { - return false - } - + + } class APSingleResourceController : APController { let wrapper:APSingleWrapper - init(account: Account, wrapper:APSingleWrapper) { + init(context: AccountContext, wrapper:APSingleWrapper, streamable: Bool, baseRate: Double = 1.0, volume: Float = 1.0) { self.wrapper = wrapper - super.init(account: account) + super.init(context: context, streamable: streamable, baseRate: baseRate, volume: volume) merge(with: APTransition(inserted: [(0,APSongItem(.single(wrapper), account))], removed: [], updated: [])) } - + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func start() { super.start() ready.set(.single(true)) dequeueCurrent() } - - override var needLoop:Bool { - return false - } - + override var needNext: Bool { return false } } + diff --git a/Telegram-Mac/AudioRecorder.swift b/Telegram-Mac/AudioRecorder.swift index 166543dcb6..42e4f60de5 100644 --- a/Telegram-Mac/AudioRecorder.swift +++ b/Telegram-Mac/AudioRecorder.swift @@ -7,17 +7,18 @@ // import Cocoa - +import OpusBinding import Foundation -import SwiftSignalKitMac +import SwiftSignalKit import CoreMedia import AVFoundation -import TelegramCoreMac +import TelegramCore + private let kOutputBus: UInt32 = 0 private let kInputBus: UInt32 = 1 -private func audioRecorderNativeStreamDescription(_ sampleRate:Float64) -> AudioStreamBasicDescription { +func audioRecorderNativeStreamDescription(_ sampleRate:Float64) -> AudioStreamBasicDescription { var canonicalBasicStreamDescription = AudioStreamBasicDescription() canonicalBasicStreamDescription.mSampleRate = sampleRate canonicalBasicStreamDescription.mFormatID = kAudioFormatLinearPCM @@ -31,19 +32,23 @@ private func audioRecorderNativeStreamDescription(_ sampleRate:Float64) -> Audio } private var nextRecorderContextId: Int32 = 0 -private func getNextRecorderContextId() -> Int32 { +func getNextRecorderContextId() -> Int32 { return OSAtomicIncrement32(&nextRecorderContextId) } -private final class RecorderContextHolder { - weak var context: ManagedAudioRecorderContext? +protocol RecoderContextRenderer : class { + func processAndDisposeAudioBuffer(_ buffer: AudioBuffer) +} + +final class RecorderContextHolder { + weak var context: RecoderContextRenderer? - init(context: ManagedAudioRecorderContext?) { + init(context: RecoderContextRenderer?) { self.context = context } } -private final class AudioUnitHolder { +final class AudioUnitHolder { let queue: Queue let audioUnit: Atomic @@ -56,15 +61,15 @@ private final class AudioUnitHolder { private var audioRecorderContexts: [Int32: RecorderContextHolder] = [:] private var audioUnitHolders = Atomic<[Int32: AudioUnitHolder]>(value: [:]) -private func addAudioRecorderContext(_ id: Int32, _ context: ManagedAudioRecorderContext) { +func addAudioRecorderContext(_ id: Int32, _ context: RecoderContextRenderer) { audioRecorderContexts[id] = RecorderContextHolder(context: context) } -private func removeAudioRecorderContext(_ id: Int32) { +func removeAudioRecorderContext(_ id: Int32) { audioRecorderContexts.removeValue(forKey: id) } -private func addAudioUnitHolder(_ id: Int32, _ queue: Queue, _ audioUnit: Atomic) { +func addAudioUnitHolder(_ id: Int32, _ queue: Queue, _ audioUnit: Atomic) { _ = audioUnitHolders.modify { dict in var dict = dict dict[id] = AudioUnitHolder(queue: queue, audioUnit: audioUnit) @@ -72,7 +77,7 @@ private func addAudioUnitHolder(_ id: Int32, _ queue: Queue, _ audioUnit: Atomic } } -private func removeAudioUnitHolder(_ id: Int32) { +func removeAudioUnitHolder(_ id: Int32) { _ = audioUnitHolders.modify { dict in var dict = dict dict.removeValue(forKey: id) @@ -80,7 +85,7 @@ private func removeAudioUnitHolder(_ id: Int32) { } } -private func withAudioRecorderContext(_ id: Int32, _ f: (ManagedAudioRecorderContext?) -> Void) { +func withAudioRecorderContext(_ id: Int32, _ f: (RecoderContextRenderer?) -> Void) { if let holder = audioRecorderContexts[id], let context = holder.context { f(context) } else { @@ -88,7 +93,7 @@ private func withAudioRecorderContext(_ id: Int32, _ f: (ManagedAudioRecorderCon } } -private func withAudioUnitHolder(_ id: Int32, _ f: (Atomic, Queue) -> Void) { +func withAudioUnitHolder(_ id: Int32, _ f: (Atomic, Queue) -> Void) { let audioUnitAndQueue = audioUnitHolders.with { dict -> (Atomic, Queue)? in if let record = dict[id] { return (record.audioUnit, record.queue) @@ -101,7 +106,7 @@ private func withAudioUnitHolder(_ id: Int32, _ f: (Atomic, Queue) - } } -private func rendererInputProc(refCon: UnsafeMutableRawPointer, ioActionFlags: UnsafeMutablePointer, inTimeStamp: UnsafePointer, inBusNumber: UInt32, inNumberFrames: UInt32, ioData: UnsafeMutablePointer?) -> OSStatus { +func rendererInputProc(refCon: UnsafeMutableRawPointer, ioActionFlags: UnsafeMutablePointer, inTimeStamp: UnsafePointer, inBusNumber: UInt32, inNumberFrames: UInt32, ioData: UnsafeMutablePointer?) -> OSStatus { let id = Int32(intptr_t(bitPattern: refCon)) withAudioUnitHolder(id, { (holder, queue) in @@ -141,21 +146,31 @@ private func rendererInputProc(refCon: UnsafeMutableRawPointer, ioActionFlags: U } struct RecordedAudioData { - let path: String + let compressedData: Data let duration: Double let waveform: Data? + let id:Int64? + let path: String + init(compressedData: Data, duration: Double, waveform: Data?, id: Int64?, path: String) { + self.compressedData = compressedData + self.duration = duration + self.waveform = waveform + self.id = id + self.path = path + } } -final class ManagedAudioRecorderContext { + +final class ManagedAudioRecorderContext : RecoderContextRenderer { private let id: Int32 private let micLevel: ValuePromise private let recordingState: ValuePromise - + private let liveUploading:PreUploadManager? private var paused = true private let queue: Queue private let oggWriter: TGOggOpusWriter - private let dataItem: TGDataItem + private let dataItem: DataItem private var audioBuffer = Data() private let audioUnit = Atomic(value: nil) @@ -166,21 +181,22 @@ final class ManagedAudioRecorderContext { private var micLevelPeak: Int16 = 0 private var micLevelPeakCount: Int = 0 + private var sampleRate: Int32 = 0 fileprivate var isPaused = false private var recordingStateUpdateTimestamp: Double? - init(queue: Queue, micLevel: ValuePromise, recordingState: ValuePromise) { + init(queue: Queue, micLevel: ValuePromise, recordingState: ValuePromise, liveUploading: PreUploadManager?, dataItem: DataItem) { assert(queue.isCurrent()) - + self.liveUploading = liveUploading self.id = getNextRecorderContextId() self.micLevel = micLevel self.recordingState = recordingState self.queue = queue - self.dataItem = TGDataItem(tempFile: ()) + self.dataItem = dataItem self.oggWriter = TGOggOpusWriter() addAudioRecorderContext(self.id, self) @@ -271,6 +287,7 @@ final class ManagedAudioRecorderContext { } var audioStreamDescription = audioRecorderNativeStreamDescription(inputSampleRate.mMinimum) + sampleRate = Int32(inputSampleRate.mMinimum) guard AudioUnitSetProperty(audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &audioStreamDescription, UInt32(MemoryLayout.size)) == noErr else { AudioComponentInstanceDispose(audioUnit) return @@ -349,6 +366,24 @@ final class ManagedAudioRecorderContext { func processAndDisposeAudioBuffer(_ buffer: AudioBuffer) { assert(self.queue.isCurrent()) + var buffer = buffer + + if(sampleRate==16000){ + let initialBuffer=malloc(Int(buffer.mDataByteSize+2)); + memcpy(initialBuffer, buffer.mData, Int(buffer.mDataByteSize)); + buffer.mData=realloc(buffer.mData, Int(buffer.mDataByteSize*3)) + let values = initialBuffer!.assumingMemoryBound(to: Int16.self) + let resampled = buffer.mData!.assumingMemoryBound(to: Int16.self) + values[Int(buffer.mDataByteSize/2)]=values[Int(buffer.mDataByteSize/2)-1] + for i: Int in 0 ..< Int(buffer.mDataByteSize/2) { + resampled[i*3]=values[i] + resampled[i*3+1]=values[i]/3+values[i+1]/3*2 + resampled[i*3+2]=values[i]/3*2+values[i+1]/3 + } + free(initialBuffer) + buffer.mDataByteSize*=3 + } + defer { free(buffer.mData) } @@ -398,7 +433,7 @@ final class ManagedAudioRecorderContext { self.processWaveformPreview(samples: currentEncoderPacket.assumingMemoryBound(to: Int16.self), count: currentEncoderPacketSize / 2) self.oggWriter.writeFrame(currentEncoderPacket.assumingMemoryBound(to: UInt8.self), frameByteCount: UInt(currentEncoderPacketSize)) - + liveUploading?.fileDidChangedSize(false) let timestamp = CACurrentMediaTime() if self.recordingStateUpdateTimestamp == nil || self.recordingStateUpdateTimestamp! < timestamp + 0.1 { self.recordingStateUpdateTimestamp = timestamp @@ -498,8 +533,8 @@ final class ManagedAudioRecorderContext { } } - - return RecordedAudioData(path: self.dataItem.path(), duration: self.oggWriter.encodedDuration(), waveform: waveform) + liveUploading?.fileDidChangedSize(true) + return RecordedAudioData(compressedData: self.dataItem.data(), duration: self.oggWriter.encodedDuration(), waveform: waveform, id: liveUploading?.id, path: dataItem.path) } else { return nil } @@ -533,7 +568,6 @@ final class ManagedAudioRecorder { private var contextRef: Unmanaged? private let micLevelValue = ValuePromise(0.0) private let recordingStateValue = ValuePromise(.paused(duration: 0.0)) - var micLevel: Signal { return self.micLevelValue.get() } @@ -542,9 +576,10 @@ final class ManagedAudioRecorder { return self.recordingStateValue.get() } - init() { + init(liveUploading: PreUploadManager?, dataItem: DataItem) { + self.queue.async { - let context = ManagedAudioRecorderContext(queue: self.queue, micLevel: self.micLevelValue, recordingState: self.recordingStateValue) + let context = ManagedAudioRecorderContext(queue: self.queue, micLevel: self.micLevelValue, recordingState: self.recordingStateValue, liveUploading: liveUploading, dataItem: dataItem) self.contextRef = Unmanaged.passRetained(context) } } diff --git a/Telegram-Mac/AudioWaveformView.swift b/Telegram-Mac/AudioWaveformView.swift index 37fc7e9f6d..74b43915ce 100644 --- a/Telegram-Mac/AudioWaveformView.swift +++ b/Telegram-Mac/AudioWaveformView.swift @@ -10,7 +10,7 @@ import Cocoa import TGUIKit fileprivate class AudioWaveformContainerView : View { - var color:NSColor = .blueUI { + var color:NSColor = .accent { didSet { self.setNeedsDisplayLayer() } @@ -31,7 +31,7 @@ fileprivate class AudioWaveformContainerView : View { } override func draw(_ layer: CALayer, in ctx: CGContext) { - super.draw(layer, in: ctx) + //super.draw(layer, in: ctx) let sampleWidth:CGFloat = 2 let halfSampleWidth:CGFloat = 1 @@ -137,8 +137,10 @@ class AudioWaveformView: View { //foregroundClipingView.clipsToBounds = true; foregroundClipingView.addSubview(foregroundView) addSubview(foregroundClipingView) - - + } + + override func mouseDragged(with event: NSEvent) { + super.mouseDragged(with: event) } override func setFrameSize(_ newSize: NSSize) { diff --git a/Telegram-Mac/AuthController.swift b/Telegram-Mac/AuthController.swift index bf402ecbf3..b067bd21b9 100644 --- a/Telegram-Mac/AuthController.swift +++ b/Telegram-Mac/AuthController.swift @@ -1,8 +1,9 @@ import Foundation import Cocoa -import SwiftSignalKitMac -import TelegramCoreMac -import PostboxMac +import SwiftSignalKit +import TelegramCore + +import Postbox import TGUIKit private let manager = CountryManager() @@ -14,11 +15,165 @@ enum LoginAuthViewState { } +private enum QRTokenState { + case qr(CGImage) +} + +private final class ExportTokenOptionView : View { + private let textView: TextView = TextView() + private let optionText = TextView() + private let cap = View(frame: NSMakeRect(0, 0, 20, 20)) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + + cap.layer?.cornerRadius = cap.frame.height / 2 + + textView.isSelectable = false + textView.userInteractionEnabled = false + + optionText.isSelectable = false + optionText.userInteractionEnabled = false + addSubview(cap) + addSubview(self.textView) + addSubview(self.optionText) + } + + func update(title: String, number: String) { + let textAttr = NSMutableAttributedString() + _ = textAttr.append(string: title, color: theme.colors.text, font: .normal(.text)) + textAttr.detectBoldColorInString(with: .medium(.text)) + let text = TextViewLayout(textAttr, maximumNumberOfLines: 2) + text.measure(width: frame.width - cap.frame.width - 10) + textView.update(text) + + let option = TextViewLayout(.initialize(string: number, color: theme.colors.underSelectedColor, font: .normal(.text)), maximumNumberOfLines: 2) + option.measure(width: frame.width) + optionText.update(option) + + cap.backgroundColor = theme.colors.accent + + setFrameSize(NSMakeSize(frame.width, max(cap.frame.height, 4 + text.layoutSize.height + 4))) + } + + override func layout() { + super.layout() + + cap.setFrameOrigin(NSZeroPoint) + let offset: CGFloat = optionText.frame.width == 6 ? 7 : 6 + optionText.setFrameOrigin(NSMakePoint(offset, 2)) + textView.setFrameOrigin(NSMakePoint(cap.frame.maxX + 10, 2)) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private final class ExportTokenView : View { + fileprivate let imageView: ImageView = ImageView() + fileprivate let logoView = ImageView(frame: NSMakeRect(0, 0, 60, 60)) + private let containerView = View() + private let titleView = TextView() + fileprivate let cancelButton = TitleButton() + + private let firstHelp: ExportTokenOptionView + private let secondHelp: ExportTokenOptionView + private let thridHelp: ExportTokenOptionView + + required init(frame frameRect: NSRect) { + firstHelp = ExportTokenOptionView(frame: NSMakeRect(0, 0, frameRect.width, 0)) + secondHelp = ExportTokenOptionView(frame: NSMakeRect(0, 0, frameRect.width, 0)) + thridHelp = ExportTokenOptionView(frame: NSMakeRect(0, 0, frameRect.width, 0)) + super.init(frame: frameRect) + containerView.addSubview(self.imageView) + + self.imageView.addSubview(logoView) + containerView.addSubview(self.titleView) + containerView.addSubview(firstHelp) + containerView.addSubview(secondHelp) + containerView.addSubview(thridHelp) + containerView.addSubview(cancelButton) + addSubview(containerView) + titleView.isSelectable = false + titleView.userInteractionEnabled = false + updateLocalizationAndTheme(theme: theme) + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + let theme = theme as! TelegramPresentationTheme + super.updateLocalizationAndTheme(theme: theme) + self.backgroundColor = theme.colors.background + + let titleLayout = TextViewLayout(.initialize(string: L10n.loginQRTitle, color: theme.colors.text, font: .normal(.header)), maximumNumberOfLines: 2, alignment: .center) + titleLayout.measure(width: frame.width) + titleView.update(titleLayout) + + firstHelp.update(title: L10n.loginQRHelp1, number: "1") + secondHelp.update(title: L10n.loginQRHelp2, number: "2") + thridHelp.update(title: L10n.loginQRHelp3, number: "3") + + cancelButton.set(font: .medium(.text), for: .Normal) + cancelButton.set(color: theme.colors.accent, for: .Normal) + cancelButton.set(text: L10n.loginQRCancel, for: .Normal) + _ = cancelButton.sizeToFit() + logoView.image = theme.icons.login_qr_cap + logoView.sizeToFit() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + func update(state: QRTokenState) { + switch state { + case let .qr(image): + self.imageView.image = image + imageView.sizeToFit() + } + needsLayout = true + } + + override func layout() { + + containerView.setFrameSize(NSMakeSize(frame.width, imageView.frame.height + 20 + self.titleView.frame.height + 20 + firstHelp.frame.height + 10 + secondHelp.frame.height + 10 + thridHelp.frame.height + 30 + cancelButton.frame.height)) + containerView.center() + + imageView.centerX(y: 0) + logoView.center() + titleView.updateWithNewWidth(containerView.frame.width) + titleView.centerX(y: imageView.frame.maxY + 10) + firstHelp.centerX(y: titleView.frame.maxY + 10) + secondHelp.centerX(y: firstHelp.frame.maxY + 10) + thridHelp.centerX(y: secondHelp.frame.maxY + 10) + cancelButton.centerX(y: thridHelp.frame.maxY + 20) + + } +} + class AuthHeaderView : View { + fileprivate var isQrEnabled: Bool? = nil { + didSet { + updateLocalizationAndTheme(theme: theme) + } + } + + fileprivate var isLoading: Bool = false + + + private var progressView: ProgressIndicator? + + private let containerView = View(frame: NSMakeRect(0, 0, 300, 480)) + + fileprivate let proxyButton:ImageButton = ImageButton() + private let proxyConnecting: ProgressIndicator = ProgressIndicator(frame: NSMakeRect(0, 0, 12, 12)) + + fileprivate var exportTokenView:ExportTokenView? fileprivate var arguments:LoginAuthViewArguments? fileprivate let loginView:LoginAuthInfoView = LoginAuthInfoView(frame: NSZeroRect) fileprivate var state: UnauthorizedAccountStateContents = .empty + fileprivate var qrTokenState: QRTokenState? = nil private let logo:ImageView = ImageView() private let header:TextView = TextView() private let desc:TextView = TextView() @@ -26,37 +181,50 @@ class AuthHeaderView : View { let intro:View = View() private let switchLanguage:TitleButton = TitleButton() fileprivate let nextButton:TitleButton = TitleButton() + fileprivate let backButton = TitleButton() + fileprivate let cancelButton = TitleButton() + + + private let animatedLogoView = ImageView() + fileprivate var needShowSuggestedButton: Bool = false required init(frame frameRect: NSRect) { super.init(frame: frameRect) - intro.setFrameSize(frameRect.size) - addSubview(intro) + intro.setFrameSize(containerView.frame.size) - let logoImage = #imageLiteral(resourceName: "Icon_LegacyIntro").precomposed() - self.logo.image = logoImage - self.logo.sizeToFit() - - updateLocalizationAndTheme() + updateLocalizationAndTheme(theme: theme) intro.addSubview(logo) intro.addSubview(header) intro.addSubview(desc) - addSubview(loginView) + containerView.addSubview(intro) - addSubview(textHeaderView) + desc.userInteractionEnabled = false + desc.isSelectable = false + + header.userInteractionEnabled = false + header.isSelectable = false + + textHeaderView.userInteractionEnabled = false + textHeaderView.isSelectable = false + + containerView.addSubview(loginView) + + + containerView.addSubview(textHeaderView) textHeaderView.userInteractionEnabled = false textHeaderView.isSelected = false - nextButton.style = ControlStyle(font: NSFont.medium(.custom(16)), foregroundColor: .white, backgroundColor: NSColor(0x32A3E2), highlightColor: .white) - nextButton.set(background: .blueUI, for: .Highlight) - nextButton.set(text: tr(.loginNext), for: .Normal) - nextButton.sizeToFit() + nextButton.autohighlight = false + nextButton.style = ControlStyle(font: NSFont.medium(16.0), foregroundColor: .white, backgroundColor: NSColor(0x32A3E2), highlightColor: .white) + nextButton.set(text: L10n.loginNext, for: .Normal) + _ = nextButton.sizeToFit(thatFit: true) nextButton.setFrameSize(76, 36) nextButton.layer?.cornerRadius = 18 - + nextButton.disableActions() nextButton.set(handler: { [weak self] _ in if let strongSelf = self { switch strongSelf.state { @@ -67,23 +235,63 @@ class AuthHeaderView : View { strongSelf.arguments?.checkCode(strongSelf.loginView.code) case .passwordEntry: strongSelf.arguments?.checkPassword(strongSelf.loginView.password) + case .signUp: + strongSelf.loginView.trySignUp() default: break } } }, for: .Click) - addSubview(nextButton) - addSubview(switchLanguage) + containerView.addSubview(nextButton) + containerView.addSubview(switchLanguage) switchLanguage.isHidden = true switchLanguage.disableActions() switchLanguage.set(font: .medium(.title), for: .Normal) - switchLanguage.set(color: .blueUI, for: .Normal) switchLanguage.set(text: "Continue on English", for: .Normal) - switchLanguage.sizeToFit() + _ = switchLanguage.sizeToFit() + + addSubview(proxyButton) + proxyButton.addSubview(proxyConnecting) + containerView.addSubview(backButton) + + addSubview(containerView) + + + addSubview(cancelButton) + + needsLayout = true + } + + fileprivate func updateProxyPref(_ pref: ProxySettings, _ connection: ConnectionStatus, _ isForceHidden: Bool = true) { + proxyButton.isHidden = isForceHidden && pref.servers.isEmpty + switch connection { + case .connecting: + proxyConnecting.isHidden = pref.effectiveActiveServer == nil + proxyButton.set(image: pref.effectiveActiveServer == nil ? theme.icons.proxyEnable : theme.icons.proxyState, for: .Normal) + case .online: + proxyConnecting.isHidden = true + if pref.enabled { + proxyButton.set(image: theme.icons.proxyEnabled, for: .Normal) + } else { + proxyButton.set(image: theme.icons.proxyEnable, for: .Normal) + } + case .waitingForNetwork: + proxyConnecting.isHidden = pref.effectiveActiveServer == nil + proxyButton.set(image: pref.effectiveActiveServer == nil ? theme.icons.proxyEnable : theme.icons.proxyState, for: .Normal) + default: + proxyConnecting.isHidden = true + } + proxyConnecting.isEventLess = true + proxyConnecting.userInteractionEnabled = false + _ = proxyButton.sizeToFit() + proxyConnecting.centerX() + proxyConnecting.centerY(addition: -1) + needsLayout = true } + func hideSwitchButton() { needShowSuggestedButton = false switchLanguage.change(opacity: 0, removeOnCompletion: false) { [weak self] completed in @@ -94,7 +302,7 @@ class AuthHeaderView : View { func showLanguageButton(title: String, callback:@escaping()->Void) -> Void { needShowSuggestedButton = true switchLanguage.set(text: title, for: .Normal) - switchLanguage.sizeToFit() + _ = switchLanguage.sizeToFit() switchLanguage.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) switchLanguage.set(handler: { _ in callback() @@ -105,95 +313,346 @@ class AuthHeaderView : View { override func layout() { super.layout() - switchLanguage.centerX(y: frame.height - switchLanguage.frame.height - 35) + switchLanguage.centerX(y: containerView.frame.height - switchLanguage.frame.height - 20) + + logo.centerX(y: 0) header.centerX(y: logo.frame.maxY + 10) - desc.centerX(y: header.frame.maxY + 10) + desc.centerX(y: header.frame.maxY) + intro.setFrameSize(containerView.frame.width, desc.frame.maxY) + intro.centerX(y: 20) + loginView.setFrameSize(300, containerView.frame.height) + loginView.centerX(y: intro.frame.maxY) + nextButton.centerX(y: containerView.frame.height - nextButton.frame.height - 50) - logo.centerX() - - intro.centerX(y: 60) + proxyConnecting.centerX() + proxyConnecting.centerY(addition: -1) + proxyButton.setFrameOrigin(frame.width - proxyButton.frame.width - 15, 15) - intro.setFrameSize(frame.width, desc.frame.maxY) + containerView.center() - loginView.setFrameSize(400, frame.height) - - loginView.centerX(y: intro.frame.maxY + 60) + cancelButton.setFrameOrigin(15, 15) + + self.exportTokenView?.setFrameSize(NSMakeSize(300, 500)) + self.exportTokenView?.center() - nextButton.centerX(y: frame.height - nextButton.frame.height - 80) + self.progressView?.center() - updateState(state, animated: false) + updateState(state, qrTokenState: self.qrTokenState, isLoading: self.isLoading, animated: false) } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - let headerLayout = TextViewLayout(NSAttributedString.initialize(string: appName, color: NSColor.text, font: NSFont.normal(.custom(30))), maximumNumberOfLines: 1) - headerLayout.measure(width: CGFloat.greatestFiniteMagnitude) + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) + switchLanguage.set(color: theme.colors.accent, for: .Normal) + + + self.animatedLogoView.image = theme.icons.login_cap + self.animatedLogoView.sizeToFit() + + self.logo.image = theme.icons.login_cap + self.logo.sizeToFit() + + let headerLayout = TextViewLayout(.initialize(string: appName, color: theme.colors.text, font: NSFont.normal(30.0)), maximumNumberOfLines: 1) + headerLayout.measure(width: .greatestFiniteMagnitude) header.update(headerLayout) + header.backgroundColor = theme.colors.background + desc.backgroundColor = theme.colors.background + textHeaderView.backgroundColor = theme.colors.background - let descLayout = TextViewLayout(NSAttributedString.initialize(string: tr(.loginWelcomeDescription), color: .grayText, font: NSFont.normal(.custom(16))), maximumNumberOfLines: 1) - descLayout.measure(width: CGFloat.greatestFiniteMagnitude) + let descLayout = TextViewLayout(.initialize(string: tr(L10n.loginWelcomeDescription), color: theme.colors.grayText, font: .normal(16.0)), maximumNumberOfLines: 2, alignment: .center) + descLayout.measure(width: 300) desc.update(descLayout) - nextButton.set(text: tr(.loginNext), for: .Normal) + if let isQrEnabled = self.isQrEnabled, isQrEnabled { + nextButton.set(text: L10n.loginQRLogin, for: .Normal) + _ = nextButton.sizeToFit(NSMakeSize(30, 0), NSMakeSize(0, 36), thatFit: true) + nextButton.style = ControlStyle(font: .medium(15.0), foregroundColor: theme.colors.accent, backgroundColor: .clear) + } else { + nextButton.set(text: L10n.loginNext, for: .Normal) + _ = nextButton.sizeToFit(NSMakeSize(30, 0), NSMakeSize(0, 36), thatFit: true) + nextButton.style = ControlStyle(font: .medium(15.0), foregroundColor: theme.colors.underSelectedColor, backgroundColor: theme.colors.accent) + } + + + proxyConnecting.progressColor = theme.colors.accentIcon + + + backButton.set(font: .medium(.header), for: .Normal) + backButton.set(color: theme.colors.accent, for: .Normal) + backButton.set(image: theme.icons.chatNavigationBack, for: .Normal) + backButton.set(text: L10n.navigationBack, for: .Normal) + _ = backButton.sizeToFit() + + cancelButton.set(font: .medium(.header), for: .Normal) + cancelButton.set(color: theme.colors.accent, for: .Normal) + cancelButton.set(text: L10n.navigationCancel, for: .Normal) + _ = cancelButton.sizeToFit() + + progressView?.progressColor = theme.colors.text + + + updateState(self.state, qrTokenState: self.qrTokenState, isLoading: self.isLoading, animated: false) needsLayout = true + } - fileprivate func updateState(_ state:UnauthorizedAccountStateContents, animated: Bool) { + fileprivate func nextButtonAsForQr(_ isQrEnabled: Bool?) -> Void { + self.isQrEnabled = isQrEnabled + self.updateLocalizationAndTheme(theme: theme) + } + + private func animateAndCancelQr() { + + guard let exportTokenView = self.exportTokenView else { + return + } + CATransaction.begin() + + self.containerView.isHidden = false + + addSubview(animatedLogoView) + + exportTokenView.logoView.isHidden = true + let point = NSMakePoint(exportTokenView.frame.midX - 20, exportTokenView.frame.minY + exportTokenView.imageView.frame.height / 2 - 16) + animatedLogoView.frame = NSMakeRect(point.x, point.y, 60, 60) + + exportTokenView.imageView.layer?.animateScaleSpring(from: 1, to: animatedLogoView.frame.width / exportTokenView.imageView.frame.width, duration: 0.4, removeOnCompletion: false, bounce: true) + + animatedLogoView.layer?.animateScaleX(from: 1, to: logo.frame.width / animatedLogoView.frame.width, duration: 0.4, timingFunction: .spring, removeOnCompletion: false) + animatedLogoView.layer?.animateScaleY(from: 1, to: logo.frame.height / animatedLogoView.frame.height, duration: 0.4, timingFunction: .spring, removeOnCompletion: false) + + self.logo.isHidden = true + + animatedLogoView.layer?.animatePosition(from: animatedLogoView.frame.origin, to: NSMakePoint((round(frame.width / 2) - logo.frame.width / 2), containerView.frame.minY + 20), duration: 0.4, timingFunction: .spring, removeOnCompletion: false, completion: { [weak self] _ in + self?.exportTokenView?.logoView.isHidden = false + self?.animatedLogoView.removeFromSuperview() + self?.animatedLogoView.layer?.removeAllAnimations() + self?.logo.isHidden = false + }) + + CATransaction.commit() + + self.arguments?.cancelQrAuth() + } + + private func animateAndApplyQr() { + addSubview(animatedLogoView) + + guard let exportTokenView = self.exportTokenView else { + return + } + + + exportTokenView.logoView.isHidden = true + + let point = NSMakePoint(frame.width / 2 - logo.frame.width / 2 + 1, containerView.frame.minY + 20) + + animatedLogoView.frame = NSMakeRect(point.x, point.y, 60, 60) + + exportTokenView.imageView.layer?.animateScaleSpring(from: animatedLogoView.frame.height / exportTokenView.imageView.frame.width, to: 1, duration: 0.4, removeOnCompletion: false, bounce: true) + + animatedLogoView.layer?.animateScaleX(from: logo.frame.width / animatedLogoView.frame.width, to: 1, duration: 0.4, timingFunction: .spring, removeOnCompletion: false) + animatedLogoView.layer?.animateScaleY(from: logo.frame.height / animatedLogoView.frame.height, to: 1, duration: 0.4, timingFunction: .spring, removeOnCompletion: false) + + self.logo.isHidden = true + + + animatedLogoView.layer?.animatePosition(from: point, to: NSMakePoint(exportTokenView.frame.midX - 30, exportTokenView.frame.minY + exportTokenView.imageView.frame.height / 2 - 26), duration: 0.4, timingFunction: .spring, removeOnCompletion: false, completion: { [weak self] _ in + self?.exportTokenView?.logoView.isHidden = false + self?.animatedLogoView.removeFromSuperview() + self?.logo.isHidden = false + self?.containerView.isHidden = true + self?.animatedLogoView.layer?.removeAllAnimations() + }) + } + + fileprivate func updateState(_ state:UnauthorizedAccountStateContents, qrTokenState: QRTokenState?, isLoading: Bool, animated: Bool) { + + let prevIsLoading = self.isLoading + + self.isLoading = isLoading self.state = state + self.qrTokenState = qrTokenState + self.loginView.updateState(self.state, animated: animated) + + if let qrTokenState = qrTokenState { + + self.logo.change(opacity: 0, animated: animated) + var firstTime: Bool = false + if self.exportTokenView == nil { + self.exportTokenView = ExportTokenView(frame: NSMakeRect(0, 0, 300, 500)) + #if !APP_STORE + self.addSubview(self.exportTokenView!, positioned: .below, relativeTo: self.subviews.first(where: { $0 is UpdateTabView })) + #else + self.addSubview(self.exportTokenView!) + #endif + self.exportTokenView?.center() + + self.exportTokenView?.cancelButton.set(handler: { [weak self] _ in + self?.animateAndCancelQr() + }, for: .Click) + + if animated { + self.exportTokenView?.layer?.animateAlpha(from: 0, to: 1, duration: 0.35, timingFunction: .spring) + } + firstTime = true + } + + guard let exportTokenView = self.exportTokenView else { + return + } + exportTokenView.update(state: qrTokenState) + + if firstTime && animated { + self.animateAndApplyQr() + } else { + self.containerView.isHidden = true + } + + } else { + self.containerView.isHidden = false + if let exportTokenView = self.exportTokenView { + if animated { + self.exportTokenView = nil + exportTokenView.layer?.animateAlpha(from: 1, to: 0, duration: 0.35, timingFunction: .spring, removeOnCompletion: false, completion: { [weak exportTokenView] _ in + exportTokenView?.removeFromSuperview() + }) + } else { + exportTokenView.removeFromSuperview() + self.exportTokenView = nil + } + } + self.logo.change(opacity: 1, animated: animated) + } + + + + backButton.isHidden = true switch self.state { case .phoneEntry, .empty: nextButton.change(opacity: 1, animated: animated) textHeaderView.change(opacity: 0, animated: animated) - intro.change(pos: NSMakePoint(intro.frame.minX, 50), animated: animated) + intro.change(pos: NSMakePoint(intro.frame.minX, 20), animated: animated) intro.change(opacity: 1, animated: animated) - loginView.change(pos: NSMakePoint(loginView.frame.minX, intro.frame.maxY + 50), animated: animated) - textHeaderView.change(pos: NSMakePoint(textHeaderView.frame.minX, floorToScreenPixels((frame.height - textHeaderView.frame.height)/2)), animated: animated) + loginView.change(pos: NSMakePoint(loginView.frame.minX, intro.frame.maxY + 30), animated: animated) + textHeaderView.change(pos: NSMakePoint(textHeaderView.frame.minX, floorToScreenPixels(backingScaleFactor, (frame.height - textHeaderView.frame.height)/2)), animated: animated) switchLanguage.isHidden = !needShowSuggestedButton case .confirmationCodeEntry: nextButton.change(opacity: 1, animated: animated) - let headerLayout = TextViewLayout(.initialize(string: tr(.loginHeaderCode), color: .text, font: .normal(.custom(30)))) + let headerLayout = TextViewLayout(.initialize(string: L10n.loginHeaderCode, color: theme.colors.text, font: .normal(25))) headerLayout.measure(width: .greatestFiniteMagnitude) textHeaderView.update(headerLayout) textHeaderView.centerX() textHeaderView.change(opacity: 1, animated: animated) - textHeaderView.change(pos: NSMakePoint(textHeaderView.frame.minX, 90), animated: animated) + textHeaderView.change(pos: NSMakePoint(textHeaderView.frame.minX, 30), animated: animated) intro.change(pos: NSMakePoint(intro.frame.minX, -intro.frame.height), animated: animated) intro.change(opacity: 0, animated: animated) - loginView.change(pos: NSMakePoint(loginView.frame.minX, 160), animated: animated) + loginView.change(pos: NSMakePoint(loginView.frame.minX, textHeaderView.frame.maxY + 30), animated: animated) switchLanguage.isHidden = true - break case .passwordEntry: nextButton.change(opacity: 1, animated: animated) - let headerLayout = TextViewLayout(.initialize(string: tr(.loginHeaderPassword), color: .text, font: .normal(.custom(30)))) + let headerLayout = TextViewLayout(.initialize(string: L10n.loginHeaderPassword, color: theme.colors.text, font: .normal(25))) headerLayout.measure(width: .greatestFiniteMagnitude) textHeaderView.update(headerLayout) - textHeaderView.centerX(y: 90) + textHeaderView.centerX(y: 30) textHeaderView.change(opacity: 1, animated: animated) intro.change(pos: NSMakePoint(intro.frame.minX, -intro.frame.height), animated: animated) intro.change(opacity: 0, animated: animated) - loginView.change(pos: NSMakePoint(loginView.frame.minX, 160), animated: animated) + loginView.change(pos: NSMakePoint(loginView.frame.minX, textHeaderView.frame.maxY + 30), animated: animated) switchLanguage.isHidden = true - break case .signUp: - nextButton.change(opacity: 0, animated: animated) - let headerLayout = TextViewLayout(.initialize(string: tr(.loginHeaderSignUp), color: .text, font: .normal(.custom(30)))) + nextButton.change(opacity: 1, animated: animated) + let headerLayout = TextViewLayout(.initialize(string: L10n.loginHeaderSignUp, color: theme.colors.text, font: .normal(25))) headerLayout.measure(width: .greatestFiniteMagnitude) textHeaderView.update(headerLayout) textHeaderView.centerX() textHeaderView.change(opacity: 1, animated: animated) - textHeaderView.change(pos: NSMakePoint(textHeaderView.frame.minX, 90), animated: animated) + textHeaderView.change(pos: NSMakePoint(textHeaderView.frame.minX, 50), animated: animated) intro.change(pos: NSMakePoint(intro.frame.minX, -intro.frame.height), animated: animated) intro.change(opacity: 0, animated: animated) - loginView.change(pos: NSMakePoint(loginView.frame.minX, 160), animated: animated) + loginView.change(pos: NSMakePoint(loginView.frame.minX, textHeaderView.frame.maxY + 50), animated: animated) switchLanguage.isHidden = true + backButton.isHidden = false + backButton.setFrameOrigin(loginView.frame.minX, textHeaderView.frame.minY + floorToScreenPixels(backingScaleFactor, (textHeaderView.frame.height - backButton.frame.height) / 2)) + case .passwordRecovery: + break + case .awaitingAccountReset: + let headerLayout = TextViewLayout(.initialize(string: L10n.loginResetAccountText, color: theme.colors.text, font: .normal(25))) + headerLayout.measure(width: .greatestFiniteMagnitude) + intro.change(pos: NSMakePoint(intro.frame.minX, -intro.frame.height), animated: animated) + intro.change(opacity: 0, animated: animated) + switchLanguage.isHidden = true + + textHeaderView.update(headerLayout) + textHeaderView.centerX() + textHeaderView.change(pos: NSMakePoint(textHeaderView.frame.minX, 50), animated: animated) + textHeaderView.change(opacity: 1, animated: animated) + + loginView.change(pos: NSMakePoint(loginView.frame.minX, textHeaderView.frame.maxY + 20), animated: animated) + + nextButton.change(opacity: 0, animated: animated) + backButton.isHidden = false + backButton.setFrameOrigin(loginView.frame.minX, textHeaderView.frame.minY + floorToScreenPixels(backingScaleFactor, (textHeaderView.frame.height - backButton.frame.height) / 2)) + } + + if prevIsLoading != isLoading { + exportTokenView?.layer?.opacity = isLoading ? 0 : 1 + containerView.layer?.opacity = isLoading ? 0 : 1 + + if animated { + if isLoading { + if let exportTokenView = self.exportTokenView { + exportTokenView.layer?.animateAlpha(from: 1, to: 0, duration: 0.35, timingFunction: .spring) + exportTokenView.layer?.animateScaleSpring(from: 1, to: 0.2, duration: 0.35) + } else { + containerView.layer?.animateAlpha(from: 1, to: 0, duration: 0.35, timingFunction: .spring) + containerView.layer?.animateScaleSpring(from: 1, to: 0.2, duration: 0.35) + } + } else { + if let exportTokenView = self.exportTokenView { + exportTokenView.layer?.animateAlpha(from: 0, to: 1, duration: 0.35, timingFunction: .spring) + exportTokenView.layer?.animateScaleSpring(from: 0.2, to: 1, duration: 0.35) + } else { + containerView.layer?.animateAlpha(from: 0, to: 1, duration: 0.35, timingFunction: .spring) + containerView.layer?.animateScaleSpring(from: 0.2, to: 1, duration: 0.35) + } + } + } + + if isLoading { + if self.progressView == nil { + let progressView = ProgressIndicator(frame: NSMakeRect(0, 0, 40, 40)) + self.progressView = progressView + + progressView.progressColor = theme.colors.text + addSubview(progressView) + } + + self.progressView?.center() + + if animated { + progressView?.layer?.animateAlpha(from: 0, to: 1, duration: 0.35, timingFunction: .spring) + } + } else { + if let progressView = self.progressView { + self.progressView = nil + if animated { + progressView.layer?.animateAlpha(from: 1, to: 0, duration: 0.35, timingFunction: .spring, removeOnCompletion: false, completion: { [weak progressView] _ in + progressView?.removeFromSuperview() + }) + } else { + progressView.removeFromSuperview() + } + } + } } } @@ -204,19 +663,53 @@ class AuthHeaderView : View { class AuthController : GenericViewController { - private let navigation:NavigationViewController private let disposable:MetaDisposable = MetaDisposable() private let actionDisposable = MetaDisposable() + private let proxyDisposable = DisposableSet() private let suggestedLanguageDisposable = MetaDisposable() private let localizationDisposable = MetaDisposable() + private let exportTokenDisposable = MetaDisposable() + private let tokenEventsDisposable = MetaDisposable() + private let configurationDisposable = MetaDisposable() private var account:UnauthorizedAccount - init(_ account:UnauthorizedAccount) { + private let sharedContext: SharedAccountContext + private let engine: TelegramEngineUnauthorized + #if !APP_STORE + private let updateController: UpdateTabController + #endif + + private var state: UnauthorizedAccountStateContents = .empty + private var qrType: QRLoginType = .disabled + private var qrTokenState: (state: QRTokenState?, animated: Bool) = (state: nil, animated: false) { + didSet { + self.genericView.updateState(self.state, qrTokenState: self.qrTokenState.state, isLoading: self.isLoading.value, animated: self.qrTokenState.animated) + } + } + private var isLoading: (value: Bool, update: Bool) = (value: true, update: true) { + didSet { + if isLoading.update { + self.genericView.updateState(self.state, qrTokenState: self.qrTokenState.state, isLoading: isLoading.value, animated: !isFirst) + isFirst = false + } + } + } + + + + private let otherAccountPhoneNumbers: ((String, AccountRecordId, Bool)?, [(String, AccountRecordId, Bool)]) + + init(_ account:UnauthorizedAccount, sharedContext: SharedAccountContext, otherAccountPhoneNumbers: ((String, AccountRecordId, Bool)?, [(String, AccountRecordId, Bool)])) { self.account = account - self.navigation = NavigationViewController(ViewController()) + self.sharedContext = sharedContext + self.engine = .init(account: account) + self.otherAccountPhoneNumbers = otherAccountPhoneNumbers + #if !APP_STORE + updateController = UpdateTabController(sharedContext) + #endif super.init() - self.disposable.set((account.postbox.stateView() |> deliverOnMainQueue).start(next: { [weak self] view in - self?.updateState(state: view.state ?? UnauthorizedAccountState(masterDatacenterId: account.masterDatacenterId, contents: .empty)) + self.disposable.set(combineLatest(account.postbox.stateView() |> deliverOnMainQueue, appearanceSignal).start(next: { [weak self] view, _ in + self?.updateState(state: view.state ?? UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .empty)) })) bar = .init(height: 0) } @@ -225,9 +718,11 @@ class AuthController : GenericViewController { func updateState(state: PostboxCoding?) { if let state = state as? UnauthorizedAccountState { - self.genericView.updateState(state.contents, animated: !isFirst) + self.state = state.contents + self.genericView.updateState(self.state, qrTokenState: self.qrTokenState.state, isLoading: self.isLoading.value, animated: !isFirst) } isFirst = false + readyOnce() } @@ -235,53 +730,217 @@ class AuthController : GenericViewController { disposable.dispose() actionDisposable.dispose() suggestedLanguageDisposable.dispose() + proxyDisposable.dispose() + exportTokenDisposable.dispose() + configurationDisposable.dispose() } + override func returnKeyAction() -> KeyHandlerResult { + return .invokeNext + } + + override func escapeKeyAction() -> KeyHandlerResult { + if !self.otherAccountPhoneNumbers.1.isEmpty { + _ = sharedContext.accountManager.transaction({ transaction in + transaction.removeAuth() + }).start() + } + return .invoked + } override func firstResponder() -> NSResponder? { - return genericView.loginView.firstResponder() + return genericView.exportTokenView ?? genericView.loginView.firstResponder() } override var canBecomeResponder: Bool { return true } + private func openProxySettings() { + + var pushController:((ViewController)->Void)? = nil + + let controller = proxyListController(accountManager: sharedContext.accountManager, network: account.network, showUseCalls: false, pushController: { controller in + pushController?(controller) + }) + let navigation:NavigationViewController = NavigationViewController(controller, mainWindow) + navigation._frameRect = NSMakeRect(0, 0, 350, 440) + navigation.readyOnce() + + pushController = { [weak navigation] controller in + navigation?.push(controller) + } + + showModal(with: navigation, for: mainWindow) + + } + + override func viewDidResized(_ size: NSSize) { + super.viewDidResized(size) + + #if !APP_STORE + updateController.frame = NSMakeRect(0, frame.height - 40, frame.width, 40) + #endif + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + #if !APP_STORE + updateController.frame = NSMakeRect(0, frame.height - 40, frame.width, 40) + #endif + } + override func viewDidLoad() { super.viewDidLoad() - + #if !APP_STORE + addSubview(updateController.view) + + updateController.frame = NSMakeRect(0, frame.height - 40, frame.width, 40) + #endif + + var arguments: LoginAuthViewArguments? + + let again:(String)-> Void = { number in + arguments?.sendCode(number) + } + + + let sharedContext = self.sharedContext + var forceHide = true + + + let engine = self.engine + + var settings:(ProxySettings, ConnectionStatus)? = nil + + + let updateProxyUI:()->Void = { [weak self] in + if let settings = settings { + self?.genericView.updateProxyPref(settings.0, settings.1, forceHide) + } + } + + let openProxySettings:()->Void = { [weak self] in + self?.openProxySettings() + forceHide = false + updateProxyUI() + } + + proxyDisposable.add(combineLatest(proxySettings(accountManager: sharedContext.accountManager) |> deliverOnMainQueue, account.network.connectionStatus |> deliverOnMainQueue).start(next: { pref, connection in + settings = (pref, connection) + updateProxyUI() + })) + + + + let disposable = MetaDisposable() + proxyDisposable.add(disposable) + + + let defaultProxyVisibles: [String] = ["RU"] + + if defaultProxyVisibles.firstIndex(where: {$0 == Locale.current.regionCode}) != nil { + forceHide = false + updateProxyUI() + } - let arguments = LoginAuthViewArguments(sendCode: { [weak self] phoneNumber in - if let strongSelf = self { - self?.actionDisposable.set((showModalProgress(signal: sendAuthorizationCode(account: strongSelf.account, phoneNumber: phoneNumber, apiId: API_ID, apiHash: API_HASH) - |> map {Optional($0)} - |> deliverOnMainQueue, for: mainWindow) - |> filter({$0 != nil}) |> map {$0!} |> deliverOnMainQueue).start(next: { [weak strongSelf] account in - strongSelf?.account = account - }, error: { [weak self] error in - self?.genericView.loginView.updatePhoneError(error) - })) - _ = markSuggestedLocalizationAsSeenInteractively(postbox: strongSelf.account.postbox, languageCode: Locale.current.languageCode ?? "en").start() + + let resetState:()->Void = { [weak self] in + guard let `self` = self else {return} + _ = resetAuthorizationState(account: self.account, to: .empty).start() + } + + genericView.proxyButton.set(handler: { _ in + if let _ = settings { + openProxySettings() } - },resendCode: { [weak self] in + }, for: .Click) + + arguments = LoginAuthViewArguments(sendCode: { [weak self] phoneNumber in if let strongSelf = self { - _ = resendAuthorizationCode(account: strongSelf.account).start() + + if let isQrEnabled = strongSelf.genericView.isQrEnabled, isQrEnabled { + strongSelf.refreshQrToken(true) + } else { + let logInNumber = formatPhoneNumber(phoneNumber) + for (number, accountId, isTestingEnvironment) in strongSelf.otherAccountPhoneNumbers.1 { + if isTestingEnvironment == strongSelf.account.testingEnvironment && formatPhoneNumber(number) == logInNumber { + confirm(for: mainWindow, information: L10n.loginPhoneNumberAlreadyAuthorized, okTitle: L10n.modalOK, cancelTitle: "", thridTitle: L10n.loginPhoneNumberAlreadyAuthorizedSwitch, successHandler: { result in + switch result { + case .thrid: + _ = (sharedContext.accountManager.transaction({ transaction in + transaction.removeAuth() + }) |> deliverOnMainQueue).start(completed: { + sharedContext.switchToAccount(id: accountId, action: nil) + }) + default: + break + } + }) + return + } + } + + + self?.actionDisposable.set((showModalProgress(signal: sendAuthorizationCode(accountManager: sharedContext.accountManager, account: strongSelf.account, phoneNumber: phoneNumber, apiId: ApiEnvironment.apiId, apiHash: ApiEnvironment.apiHash, syncContacts: false) + |> map {Optional($0)} + |> mapError {Optional($0)} + |> timeout(20, queue: Queue.mainQueue(), alternate: .fail(nil)) + |> deliverOnMainQueue, for: mainWindow) + |> filter({$0 != nil}) |> map {$0!} |> deliverOnMainQueue).start(next: { [weak strongSelf] account in + strongSelf?.account = account + }, error: { [weak self] error in + if let error = error { + self?.genericView.loginView.updatePhoneError(error) + } else { + confirm(for: mainWindow, header: L10n.loginConnectionErrorHeader, information: L10n.loginConnectionErrorInfo, okTitle: L10n.loginConnectionErrorTryAgain, thridTitle: L10n.loginConnectionErrorUseProxy, successHandler: { result in + switch result { + case .basic: + again(phoneNumber) + case .thrid: + openProxySettings() + } + }) + } + })) + _ = self?.engine.localization.markSuggestedLocalizationAsSeenInteractively(languageCode: Locale.current.languageCode ?? "en").start() + } } - }, editPhone: { [weak self] in - if let strongSelf = self { - _ = resetAuthorizationState(account: strongSelf.account, to: .empty).start() + },resendCode: { [weak self] in + if let window = self?.window { + confirm(for: window, information: L10n.loginSmsAppErr, cancelTitle: L10n.loginSmsAppErrGotoSite, successHandler: { _ in + + }, cancelHandler:{ + execute(inapp: .external(link: "https://telegram.org", false)) + }) + } +// if let strongSelf = self { +// _ = resendAuthorizationCode(account: strongSelf.account).start() +// } + }, editPhone: { + resetState() }, checkCode: { [weak self] code in if let strongSelf = self { - _ = (authorizeWithCode(account: strongSelf.account, code: code) |> deliverOnMainQueue ).start(error: { [weak self] error in + _ = (authorizeWithCode(accountManager: sharedContext.accountManager, account: strongSelf.account, code: code, termsOfService: nil) |> deliverOnMainQueue ).start(next: { [weak strongSelf] value in + if let strongSelf = strongSelf { + switch value { + case let .signUp(data): + _ = beginSignUp(account: strongSelf.account, data: data).start() + default: + break + } + } + }, error: { [weak self] error in self?.genericView.loginView.updateCodeError(error) }) } }, checkPassword: { [weak self] password in if let strongSelf = self { - _ = (authorizeWithPassword(account: strongSelf.account, password: password) + _ = (authorizeWithPassword(accountManager: sharedContext.accountManager, account: strongSelf.account, password: password, syncContacts: false) |> map { () -> AuthorizationPasswordVerificationError? in return nil } @@ -295,31 +954,191 @@ class AuthController : GenericViewController { }) } + }, requestPasswordRecovery: { [weak self] f in + guard let `self` = self else {return} + + _ = showModalProgress(signal: engine.auth.requestTwoStepVerificationPasswordRecoveryCode() |> deliverOnMainQueue, for: mainWindow).start(next: { [weak self] pattern in + guard let `self` = self else {return} + f(pattern) + showModal(with: ForgotUnauthorizedPasswordController(accountManager: sharedContext.accountManager, engine: self.engine, emailPattern: pattern), for: mainWindow) + }, error: { error in + alert(for: mainWindow, info: L10n.unknownError) + }) + }, resetAccount: { [weak self] in + guard let `self` = self else {return} + confirm(for: mainWindow, information: L10n.loginResetAccountDescription, okTitle: L10n.loginResetAccount, successHandler: { _ in + _ = showModalProgress(signal: performAccountReset(account: self.account) |> deliverOnMainQueue, for: mainWindow).start(error: { error in + alert(for: mainWindow, info: L10n.unknownError) + }) + }) + }, signUp: { [weak self] firstName, lastName, photo in + guard let `self` = self else {return} + _ = showModalProgress(signal: signUpWithName(accountManager: sharedContext.accountManager, account: self.account, firstName: firstName, lastName: lastName, avatarData: photo != nil ? try? Data(contentsOf: photo!) : nil, avatarVideo: nil, videoStartTimestamp: nil) |> deliverOnMainQueue, for: mainWindow).start(error: { error in + let text: String + switch error { + case .limitExceeded: + text = L10n.loginFloodWait + case .codeExpired: + text = L10n.phoneCodeExpired + case .invalidFirstName: + text = L10n.loginInvalidFirstNameError + case .invalidLastName: + text = L10n.loginInvalidLastNameError + case .generic: + text = L10n.unknownError + } + alert(for: mainWindow, info: text) + }) + }, cancelQrAuth: { [weak self] in + self?.cancelQrToken() + }, updatePhoneNumberField: { [weak self] updated in + if self?.qrType != .disabled { + self?.genericView.nextButtonAsForQr(updated.isEmpty) + } }) - - // genericView.loginView.arguments = arguments genericView.arguments = arguments + genericView.backButton.set(handler: { _ in + resetState() + }, for: .Click) - suggestedLanguageDisposable.set((currentlySuggestedLocalization(network: account.network, extractKeys: ["Login.ContinueOnLanguage"]) |> deliverOnMainQueue).start(next: { [weak self] info in - if let strongSelf = self, let info = info, info.languageCode != appCurrentLanguage.languageCode { - - strongSelf.genericView.showLanguageButton(title: info.localizedKey("Login.ContinueOnLanguage"), callback: { [weak strongSelf] in - if let strongSelf = strongSelf { - strongSelf.genericView.hideSwitchButton() - _ = showModalProgress(signal: downoadAndApplyLocalization(postbox: strongSelf.account.postbox, network: strongSelf.account.network, languageCode: info.languageCode), for: mainWindow).start() - } - }) - } - })) + genericView.cancelButton.isHidden = otherAccountPhoneNumbers.1.isEmpty + + genericView.cancelButton.set(handler: { _ in + _ = sharedContext.accountManager.transaction({ transaction in + transaction.removeAuth() + }).start() + }, for: .Click) + if otherAccountPhoneNumbers.1.isEmpty { + suggestedLanguageDisposable.set((engine.localization.currentlySuggestedLocalization(extractKeys: ["Login.ContinueOnLanguage"]) |> deliverOnMainQueue).start(next: { [weak self] info in + if let strongSelf = self, let info = info, info.languageCode != appCurrentLanguage.baseLanguageCode { + + strongSelf.genericView.showLanguageButton(title: info.localizedKey("Login.ContinueOnLanguage"), callback: { [weak strongSelf] in + if let strongSelf = strongSelf { + strongSelf.genericView.hideSwitchButton() + _ = showModalProgress(signal: strongSelf.engine.localization.downloadAndApplyLocalization(accountManager: sharedContext.accountManager, languageCode: info.languageCode), for: mainWindow).start() + } + }) + } + })) + } + + localizationDisposable.set(appearanceSignal.start(next: { [weak self] _ in - self?.updateLocalizationAndTheme() + self?.updateLocalizationAndTheme(theme: theme) + })) + + self.tokenEventsDisposable.set((self.account.updateLoginTokenEvents |> deliverOnMainQueue).start(next: { [weak self] _ in + self?.refreshQrToken() + })) + + + configurationDisposable.set((unauthorizedConfiguration(accountManager: self.sharedContext.accountManager) |> take(1) |> castError(Void.self) |> timeout(25.0, queue: .mainQueue(), alternate: .fail(Void())) |> deliverOnMainQueue).start(next: { [weak self] value in + + self?.qrType = value.qr + self?.genericView.isQrEnabled = value.qr != .disabled + switch value.qr { + case .disabled: + self?.isLoading = (value: false, update: true) + case .secondary: + self?.isLoading = (value: false, update: true) + case .primary: + self?.refreshQrToken() + } + + }, error: { [weak self] in + self?.qrType = .disabled + self?.isLoading = (value: false, update: true) + forceHide = false + updateProxyUI() })) + + } + + private func cancelQrToken() { + self.exportTokenDisposable.set(nil) + self.tokenEventsDisposable.set(nil) + self.qrTokenState = (state: nil, animated: true) + } + + private func refreshQrToken(_ showProgress: Bool = false) { + + let sharedContext = self.sharedContext + let engine = self.engine + + var tokenSignal: Signal = sharedContext.activeAccounts |> castError(ExportAuthTransferTokenError.self) |> take(1) |> mapToSignal { accounts in + return engine.auth.exportAuthTransferToken(accountManager: sharedContext.accountManager, otherAccountUserIds: accounts.accounts.map { $0.1.peerId.id }, syncContacts: false) + } + + if showProgress { + tokenSignal = showModalProgress(signal: tokenSignal |> take(1), for: mainWindow) + } + + self.exportTokenDisposable.set((tokenSignal + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + + switch result { + case let .displayToken(token): + var tokenString = token.value.base64EncodedString() + tokenString = tokenString.replacingOccurrences(of: "+", with: "-") + tokenString = tokenString.replacingOccurrences(of: "/", with: "_") + let urlString = "tg://login?token=\(tokenString)" + let _ = (qrCode(string: urlString, color: theme.colors.text, backgroundColor: theme.colors.background, icon: .custom(theme.icons.login_qr_empty_cap)) + |> deliverOnMainQueue).start(next: { _, generate in + guard let strongSelf = self else { + return + } + + let context = generate(TransformImageArguments(corners: ImageCorners(), imageSize: CGSize(width: 280, height: 280), boundingSize: CGSize(width: 280, height: 280), intrinsicInsets: NSEdgeInsets(), scale: 2.0)) + if let image = context?.generateImage() { + strongSelf.qrTokenState = (state: .qr(image), animated: !strongSelf.isLoading.value) + strongSelf.isLoading = (value: false, update: true) + } + }) + + let timestamp = Int32(Date().timeIntervalSince1970) + let timeout = max(5, token.validUntil - timestamp) + strongSelf.exportTokenDisposable.set((Signal.complete() + |> delay(Double(timeout), queue: .mainQueue())).start(completed: { + guard let strongSelf = self else { + return + } + strongSelf.refreshQrToken() + })) + case let .passwordRequested(account): + strongSelf.account = account + strongSelf.genericView.isQrEnabled = false + strongSelf.exportTokenDisposable.set(nil) + strongSelf.tokenEventsDisposable.set(nil) + strongSelf.qrTokenState = (state: nil, animated: true) + case let .changeAccountAndRetry(account): + strongSelf.exportTokenDisposable.set(nil) + strongSelf.account = account + strongSelf.tokenEventsDisposable.set((account.updateLoginTokenEvents + |> deliverOnMainQueue).start(next: { _ in + self?.refreshQrToken() + })) + strongSelf.refreshQrToken() + strongSelf.qrTokenState = (state: nil, animated: true) + case .loggedIn: + strongSelf.exportTokenDisposable.set(nil) + strongSelf.qrTokenState = (state: nil, animated: true) + } + })) } + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + #if !APP_STORE + updateController.updateLocalizationAndTheme(theme: theme) + #endif + } } diff --git a/Telegram-Mac/AutoDeleteContextMenuView.swift b/Telegram-Mac/AutoDeleteContextMenuView.swift new file mode 100644 index 0000000000..01907a04a1 --- /dev/null +++ b/Telegram-Mac/AutoDeleteContextMenuView.swift @@ -0,0 +1,14 @@ +// +// AutoDeleteContextMenuView.swift +// Telegram +// +// Created by Mikhail Filimonov on 19.02.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit + +//final class AutoDeleteContextMenuView : View { +// +//} diff --git a/Telegram-Mac/AutoNightThemePreferences.swift b/Telegram-Mac/AutoNightThemePreferences.swift new file mode 100644 index 0000000000..908f5716a7 --- /dev/null +++ b/Telegram-Mac/AutoNightThemePreferences.swift @@ -0,0 +1,138 @@ +// +// AutoNightThemePreferences.swift +// Telegram +// +// Created by Mikhail Filimonov on 23/08/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import Postbox +import SwiftSignalKit +import TGUIKit +import TelegramCore + + +enum AutoNightSchedule : Equatable { + case sunrise(latitude: Double, longitude: Double, localizedGeo: String?) + case timeSensitive(from: Int32, to: Int32) + + fileprivate var typeValue: Int32 { + switch self { + case .sunrise: + return 1 + case .timeSensitive: + return 2 + } + } +} + +struct AutoNightThemePreferences: PreferencesEntry, Equatable { + let schedule: AutoNightSchedule? + let systemBased: Bool + let theme: DefaultTheme + static var defaultSettings: AutoNightThemePreferences { + return AutoNightThemePreferences() + } + + init() { + self.schedule = nil + self.theme = DefaultTheme(local: .nightAccent, cloud: nil) + if #available(OSX 10.14, *) { + self.systemBased = true + } else { + self.systemBased = false + } + } + + init(schedule: AutoNightSchedule?, theme: DefaultTheme, systemBased: Bool) { + self.schedule = schedule + self.theme = theme + self.systemBased = systemBased + } + + init(decoder: PostboxDecoder) { + let type = decoder.decodeInt32ForKey("t", orElse: 0) + + let defaultTheme = DefaultTheme(local: .nightAccent, cloud: nil) + + self.theme = decoder.decodeObjectForKey("defTheme", decoder: { DefaultTheme(decoder: $0) }) as? DefaultTheme ?? defaultTheme + self.systemBased = decoder.decodeBoolForKey("sb", orElse: false) + switch type { + case 1: + let latitude = decoder.decodeDoubleForKey("la", orElse: 0) + let longitude = decoder.decodeDoubleForKey("lo", orElse: 0) + let localizedGeo = decoder.decodeOptionalStringForKey("lg") + self.schedule = .sunrise(latitude: latitude, longitude: longitude, localizedGeo: localizedGeo) + case 2: + let from = decoder.decodeInt32ForKey("from", orElse: 22) + let to = decoder.decodeInt32ForKey("to", orElse: 9) + self.schedule = .timeSensitive(from: from, to: to) + default: + self.schedule = nil + } + } + + func encode(_ encoder: PostboxEncoder) { + encoder.encodeObject(self.theme, forKey: "defTheme") + encoder.encodeBool(self.systemBased, forKey: "sb") + if let schedule = schedule { + encoder.encodeInt32(schedule.typeValue, forKey: "t") + switch schedule { + case let .sunrise(location): + encoder.encodeDouble(location.latitude, forKey: "la") + encoder.encodeDouble(location.longitude, forKey: "lo") + if let localizedGeo = location.localizedGeo { + encoder.encodeString(localizedGeo, forKey: "lg") + } else { + encoder.encodeNil(forKey: "lg") + } + case let .timeSensitive(from, to): + encoder.encodeInt32(from, forKey: "from") + encoder.encodeInt32(to, forKey: "to") + } + } else { + encoder.encodeInt32(0, forKey: "t") + } + } + + + func withUpdatedSchedule(_ schedule: AutoNightSchedule?) -> AutoNightThemePreferences { + return AutoNightThemePreferences(schedule: schedule, theme: self.theme, systemBased: self.systemBased) + } + + func withUpdatedTheme(_ theme: DefaultTheme) -> AutoNightThemePreferences { + return AutoNightThemePreferences(schedule: self.schedule, theme: theme, systemBased: self.systemBased) + } + func withUpdatedSystemBased(_ systemBased: Bool) -> AutoNightThemePreferences { + return AutoNightThemePreferences(schedule: self.schedule, theme: self.theme, systemBased: systemBased) + } + + func isEqual(to: PreferencesEntry) -> Bool { + if let to = to as? AutoNightThemePreferences { + return self == to + } else { + return false + } + } + +} + + +func autoNightSettings(accountManager: AccountManager) -> Signal { + return accountManager.sharedData(keys: [ApplicationSharedPreferencesKeys.autoNight]) |> map { $0.entries[ApplicationSharedPreferencesKeys.autoNight] as? AutoNightThemePreferences ?? AutoNightThemePreferences.defaultSettings } +} + +func updateAutoNightSettingsInteractively(accountManager: AccountManager, _ f: @escaping (AutoNightThemePreferences) -> AutoNightThemePreferences) -> Signal { + return accountManager.transaction { transaction -> Void in + transaction.updateSharedData(ApplicationSharedPreferencesKeys.autoNight, { entry in + let currentSettings: AutoNightThemePreferences + if let entry = entry as? AutoNightThemePreferences { + currentSettings = entry + } else { + currentSettings = AutoNightThemePreferences.defaultSettings + } + return f(currentSettings) + }) + } +} diff --git a/Telegram-Mac/AutoNightViewController.swift b/Telegram-Mac/AutoNightViewController.swift new file mode 100644 index 0000000000..aced8c8ce7 --- /dev/null +++ b/Telegram-Mac/AutoNightViewController.swift @@ -0,0 +1,333 @@ +// +// AutoNightViewController.swift +// Telegram +// +// Created by Mikhail Filimonov on 23/08/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + +fileprivate let _id_disabled = InputDataIdentifier("disabled") +fileprivate let _id_scheduled = InputDataIdentifier("enabled") + +fileprivate let _id_from = InputDataIdentifier("from") +fileprivate let _id_to = InputDataIdentifier("to") + +fileprivate let _id_sunrise = InputDataIdentifier("sunrise") + +fileprivate let _id_night_blue = InputDataIdentifier(nightAccentPalette.name) +fileprivate let _id_dark = InputDataIdentifier(darkPalette.name) +fileprivate let _id_update = InputDataIdentifier("update") + +private let _id_system_based = InputDataIdentifier("_id_system_based") +private let _id_list = InputDataIdentifier("_id_list") + +private final class AutoNightThemeArguments { + let context: AccountContext + let selectTheme:(InstallThemeSource)->Void + let disable:()->Void + let scheduled:()->Void + let sunrise:(Bool)->Void + let systemBased:()->Void + let selectTimeFrom:(Int32)->Void + let selectTimeTo:(Int32)->Void + let updateLocation:()->Void + init(context: AccountContext, selectTheme: @escaping(InstallThemeSource)->Void, disable:@escaping()->Void, scheduled: @escaping()->Void, sunrise:@escaping(Bool)->Void, systemBased: @escaping()->Void, selectTimeFrom: @escaping(Int32)->Void, selectTimeTo: @escaping(Int32)->Void, updateLocation:@escaping()->Void) { + self.context = context + self.disable = disable + self.selectTheme = selectTheme + self.scheduled = scheduled + self.sunrise = sunrise + self.selectTimeFrom = selectTimeFrom + self.selectTimeTo = selectTimeTo + self.systemBased = systemBased + self.updateLocation = updateLocation + } +} + +private func autoNightEntries(appearance: Appearance, settings: AutoNightThemePreferences, cloudThemes: [TelegramTheme], arguments: AutoNightThemeArguments) -> [InputDataEntry] { + var entries: [InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index:Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_disabled, data: InputDataGeneralData(name: L10n.autoNightSettingsDisabled, color: theme.colors.text, icon: nil, type: .selectable(settings.schedule == nil && !settings.systemBased), viewType: .firstItem, action: arguments.disable))) + index += 1 + + + + if #available(OSX 10.14, *) { + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_scheduled, data: InputDataGeneralData(name: L10n.autoNightSettingsScheduled, color: theme.colors.text, icon: nil, type: .selectable(settings.schedule != nil), viewType: .innerItem, action: arguments.scheduled))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_system_based, data: InputDataGeneralData(name: L10n.autoNightSettingsSystemBased, color: theme.colors.text, icon: nil, type: .selectable(settings.systemBased), viewType: .lastItem, action: arguments.systemBased))) + index += 1 + if settings.systemBased { + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.autoNightSettingsSystemBasedDesc), data: InputDataGeneralTextData(viewType: .textTopItem))) + index += 1 + } + } else { + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_scheduled, data: InputDataGeneralData(name: L10n.autoNightSettingsScheduled, color: theme.colors.text, icon: nil, type: .selectable(settings.schedule != nil), viewType: .lastItem, action: arguments.scheduled))) + index += 1 + } + + + + + if let schedule = settings.schedule { + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + let sunriseEnabled: Bool + switch schedule { + case .sunrise: + sunriseEnabled = true + default: + sunriseEnabled = false + } + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_sunrise, data: InputDataGeneralData(name: L10n.autoNightSettingsSunsetAndSunrise, color: theme.colors.text, icon: nil, type: .switchable(sunriseEnabled), viewType: .firstItem, action: { + arguments.sunrise(!sunriseEnabled) + }))) + index += 1 + + switch schedule { + case let .sunrise(latitude, longitude, localizedGeo): + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_update, data: InputDataGeneralData(name: L10n.autoNightSettingsUpdateLocation, color: theme.colors.accent, icon: nil, type: .context(localizedGeo ?? ""), viewType: .lastItem, action: arguments.updateLocation))) + index += 1 + + let sunriseSet = EDSunriseSet(date: Date(), timezone: NSTimeZone.local, latitude: latitude, longitude: longitude) + if let sunriseSet = sunriseSet { + let formatter = DateFormatter() + formatter.timeStyle = .short + formatter.timeZone = NSTimeZone.local + formatter.dateStyle = .none + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.autoNightSettingsSunriseDesc(latitude == 0 ? "N/A" : formatter.string(from: sunriseSet.sunset), longitude == 0 ? "N/A" : formatter.string(from: sunriseSet.sunrise))), data: InputDataGeneralTextData(viewType: .textBottomItem))) + index += 1 + } else { + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.autoNightSettingsSunriseDescNA), data: InputDataGeneralTextData(viewType: .textBottomItem))) + index += 1 + } + + case let .timeSensitive(from, to): + + func items(from:Int32, to:Int32, isTo: Bool) -> [SPopoverItem] { + var items:[SPopoverItem] = [] + for i in from ..< to { + items.append(SPopoverItem(i < 10 ? "0\(i):00" : "\(i):00", { + if isTo { + arguments.selectTimeTo(i) + } else { + arguments.selectTimeFrom(i) + } + })) + } + return items + } + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_from, data: InputDataGeneralData(name: L10n.autoNightSettingsFrom, color: theme.colors.text, icon: nil, type: .contextSelector(from < 10 ? "0\(from):00" : "\(from):00", items(from: 0, to: 24, isTo: false)), viewType: .innerItem, action: nil))) + index += 1 + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_to, data: InputDataGeneralData(name: L10n.autoNightSettingsTo, color: theme.colors.text, icon: nil, type: .contextSelector(to < 10 ? "0\(to):00" : "\(to):00", items(from: 0, to: 24, isTo: true)), viewType: .lastItem, action: nil))) + index += 1 + } + } + + if settings.schedule != nil || settings.systemBased { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.autoNightSettingsPreferredTheme), data: InputDataGeneralTextData(viewType: .textTopItem))) + index += 1 + + var cloudThemes = Array(cloudThemes.filter { cloud in + return cloud.file != nil + }.reversed()) + + let selected: ThemeSource + if let theme = settings.theme.cloud { + selected = .cloud(theme.cloud) + } else { + selected = .local(settings.theme.local.palette, nil) + } + + if let cloud = settings.theme.cloud?.cloud { + if !cloudThemes.contains(where: {$0.id == cloud.id}) { + cloudThemes.append(cloud) + } + } + + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_list, equatable: InputDataEquatable(settings), comparable: nil, item: { initialSize, stableId in + return ThemeListRowItem(initialSize, stableId: stableId, context: arguments.context, theme: appearance.presentation, selected: selected, local: [LocalPaletteWithReference(palette: nightAccentPalette, cloud: nil), LocalPaletteWithReference(palette: systemPalette, cloud: nil)], cloudThemes: cloudThemes, viewType: .singleItem, togglePalette: arguments.selectTheme, menuItems: { source in + return [] + }) + })) + index += 1 + + } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + return entries +} + + +/* + if location.latitude == 0 && location.longitude == 0 { + return requestUserLocation() + |> map {Optional($0)} + |> `catch` { error -> Signal in + return .single(nil) + } |> mapToSignal { value in + if let value = value { + return updateAutoNightSettingsInteractively(accountManager: sharedContext.accountManager, { pref -> AutoNightThemePreferences in + switch value { + case let .success(location): + return pref.withUpdatedSchedule(.sunrise(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude)) + } + }) |> map { set in + return autoNightEntries(set) + } + } else { + return .single(autoNightEntries(settings)) + } + } + } + */ + +func AutoNightSettingsController(context: AccountContext) -> InputDataController { + + let updateDisposable = MetaDisposable() + let updateLocationDisposable = MetaDisposable() + + + let updateLocation:(Bool)->Void = { inBackground in + var signal: Signal<(Double, Double, String?), UserLocationError> = requestUserLocation() |> take(1) |> mapToSignal { value in + switch value { + case let .success(location): + return reverseGeocodeLocation(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude) + |> mapError { _ in return UserLocationError.denied } |> map { geocode in + return (location.coordinate.latitude, location.coordinate.longitude, geocode?.city) + } + } + } + if !inBackground { + signal = showModalProgress(signal: signal, for: context.window) + } + updateLocationDisposable.set(signal.start(next: { location in + updateDisposable.set(updateAutoNightSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in + return current.withUpdatedSchedule(.sunrise(latitude: location.0, longitude: location.1, localizedGeo: location.2)) + }).start()) + }, error: { error in + if !inBackground { + alert(for: context.window, info: L10n.autoNightSettingsUpdateLocationError) + } + })) + } + + let arguments = AutoNightThemeArguments(context: context, selectTheme: { source in + updateDisposable.set(updateAutoNightSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in + var settings = settings + switch source { + case let .local(palette): + settings = settings.withUpdatedTheme(DefaultTheme(local: palette.parent, cloud: nil)) + case let .cloud(theme, cachedData): + if let cached = cachedData { + settings = settings.withUpdatedTheme(DefaultTheme(local: cached.palette.parent, cloud: DefaultCloudTheme(cloud: theme, palette: cached.palette, wallpaper: AssociatedWallpaper(cloud: cached.cloudWallpaper, wallpaper: cached.wallpaper)))) + } + } + return settings + }).start()) + }, disable: { + updateDisposable.set(updateAutoNightSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in + return current.withUpdatedSchedule(nil).withUpdatedSystemBased(false) + }).start()) + }, scheduled: { + updateDisposable.set(updateAutoNightSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in + return current.withUpdatedSchedule(.timeSensitive(from: 22, to: 9)).withUpdatedSystemBased(false) + }).start()) + }, sunrise: { enable in + updateDisposable.set(updateAutoNightSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in + if enable { + return current.withUpdatedSchedule(.sunrise(latitude: 0, longitude: 0, localizedGeo: nil)) + } else { + return current.withUpdatedSchedule(.timeSensitive(from: 22, to: 9)) + } + }).start()) + + if enable { + updateLocation(true) + } + }, systemBased: { + updateDisposable.set(updateAutoNightSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in + return current.withUpdatedSchedule(nil).withUpdatedSystemBased(true) + }).start()) + }, selectTimeFrom: { value in + updateDisposable.set(updateAutoNightSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in + if let schedule = current.schedule { + switch schedule { + case .sunrise: + return current + case let .timeSensitive(interval): + return current.withUpdatedSchedule(.timeSensitive(from: value, to: interval.to)) + } + } + return current + + }).start()) + }, selectTimeTo: { value in + updateDisposable.set(updateAutoNightSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in + if let schedule = current.schedule { + switch schedule { + case .sunrise: + return current + case let .timeSensitive(interval): + return current.withUpdatedSchedule(.timeSensitive(from: interval.from, to: value)) + } + } + return current + }).start()) + }, updateLocation: { + updateLocation(false) + }) + + + + + + + let autoNight = autoNightSettings(accountManager: context.sharedContext.accountManager) + let cloudThemes = telegramThemes(postbox: context.account.postbox, network: context.account.network, accountManager: context.sharedContext.accountManager) + + let signal: Signal<[InputDataEntry], NoError> = combineLatest(queue: prepareQueue, appearanceSignal, autoNight, cloudThemes) |> map { + autoNightEntries(appearance: $0, settings: $1, cloudThemes: $2, arguments: arguments) + } + + return InputDataController(dataSignal: signal |> map { InputDataSignalValue(entries: $0, animated: false) }, + title: L10n.autoNightSettingsTitle, + afterDisappear: { + updateDisposable.dispose() + updateLocationDisposable.dispose() + }, + removeAfterDisappear: true, + hasDone: false, + identifier: "auto-night") +} + + + diff --git a/Telegram-Mac/AutomaticMediaDownloadCategoryPeers.swift b/Telegram-Mac/AutomaticMediaDownloadCategoryPeers.swift index 9b4a7153bd..c4814ade6f 100644 --- a/Telegram-Mac/AutomaticMediaDownloadCategoryPeers.swift +++ b/Telegram-Mac/AutomaticMediaDownloadCategoryPeers.swift @@ -7,41 +7,66 @@ // import Cocoa -import PostboxMac -import SwiftSignalKitMac +import Postbox +import SwiftSignalKit public struct AutomaticMediaDownloadCategoryPeers: PostboxCoding, Equatable { public let privateChats: Bool - public let groupsAndChannels: Bool - - public init(privateChats: Bool, groupsAndChannels: Bool) { + public let groupChats: Bool + public let channels: Bool + public let fileSize: Int32? + public init(privateChats: Bool, groupChats: Bool, channels: Bool, fileSize: Int32?) { self.privateChats = privateChats - self.groupsAndChannels = groupsAndChannels + self.groupChats = groupChats + self.channels = channels + self.fileSize = fileSize } public init(decoder: PostboxDecoder) { - self.privateChats = decoder.decodeInt32ForKey("p", orElse: 0) != 0 - self.groupsAndChannels = decoder.decodeInt32ForKey("g", orElse: 0) != 0 + self.privateChats = decoder.decodeInt32ForKey("pc", orElse: 0) != 0 + self.groupChats = decoder.decodeInt32ForKey("g", orElse: 0) != 0 + self.channels = decoder.decodeInt32ForKey("c", orElse: 0) != 0 + self.fileSize = decoder.decodeOptionalInt32ForKey("fs") + } public func encode(_ encoder: PostboxEncoder) { - encoder.encodeInt32(self.privateChats ? 1 : 0, forKey: "p") - encoder.encodeInt32(self.groupsAndChannels ? 1 : 0, forKey: "g") + encoder.encodeInt32(self.privateChats ? 1 : 0, forKey: "pc") + encoder.encodeInt32(self.groupChats ? 1 : 0, forKey: "g") + encoder.encodeInt32(self.channels ? 1 : 0, forKey: "c") + if let fileSize = self.fileSize { + encoder.encodeInt32(fileSize, forKey: "fs") + } else { + encoder.encodeNil(forKey: "fs") + } } public func withUpdatedPrivateChats(_ privateChats: Bool) -> AutomaticMediaDownloadCategoryPeers { - return AutomaticMediaDownloadCategoryPeers(privateChats: privateChats, groupsAndChannels: self.groupsAndChannels) + return AutomaticMediaDownloadCategoryPeers(privateChats: privateChats, groupChats: self.groupChats, channels: self.channels, fileSize: self.fileSize) } - public func withUpdatedGroupsAndChannels(_ groupsAndChannels: Bool) -> AutomaticMediaDownloadCategoryPeers { - return AutomaticMediaDownloadCategoryPeers(privateChats: self.privateChats, groupsAndChannels: groupsAndChannels) + public func withUpdatedGroupChats(_ groupChats: Bool) -> AutomaticMediaDownloadCategoryPeers { + return AutomaticMediaDownloadCategoryPeers(privateChats: self.privateChats, groupChats: groupChats, channels: self.channels, fileSize: self.fileSize) + } + + public func withUpdatedChannels(_ channels: Bool) -> AutomaticMediaDownloadCategoryPeers { + return AutomaticMediaDownloadCategoryPeers(privateChats: self.privateChats, groupChats: self.groupChats, channels: channels, fileSize: self.fileSize) + } + public func withUpdatedSizeLimit(_ sizeLimit: Int32?) -> AutomaticMediaDownloadCategoryPeers { + return AutomaticMediaDownloadCategoryPeers(privateChats: self.privateChats, groupChats: self.groupChats, channels: channels, fileSize: sizeLimit) } public static func ==(lhs: AutomaticMediaDownloadCategoryPeers, rhs: AutomaticMediaDownloadCategoryPeers) -> Bool { if lhs.privateChats != rhs.privateChats { return false } - if lhs.groupsAndChannels != rhs.groupsAndChannels { + if lhs.channels != rhs.channels { + return false + } + if lhs.groupChats != rhs.groupChats { + return false + } + if lhs.fileSize != rhs.fileSize { return false } return true @@ -50,58 +75,65 @@ public struct AutomaticMediaDownloadCategoryPeers: PostboxCoding, Equatable { public struct AutomaticMediaDownloadCategories: PostboxCoding, Equatable { public let photo: AutomaticMediaDownloadCategoryPeers - public let voice: AutomaticMediaDownloadCategoryPeers - public let instantVideo: AutomaticMediaDownloadCategoryPeers - public let gif: AutomaticMediaDownloadCategoryPeers + public let video: AutomaticMediaDownloadCategoryPeers + public let files: AutomaticMediaDownloadCategoryPeers +// public let instantVideo: AutomaticMediaDownloadCategoryPeers +// public let gif: AutomaticMediaDownloadCategoryPeers - public init(photo: AutomaticMediaDownloadCategoryPeers, voice: AutomaticMediaDownloadCategoryPeers, instantVideo: AutomaticMediaDownloadCategoryPeers, gif: AutomaticMediaDownloadCategoryPeers) { + public init(photo: AutomaticMediaDownloadCategoryPeers, video: AutomaticMediaDownloadCategoryPeers, files: AutomaticMediaDownloadCategoryPeers) { self.photo = photo - self.voice = voice - self.instantVideo = instantVideo - self.gif = gif + self.video = video + self.files = files +// self.instantVideo = instantVideo +// self.gif = gif } public init(decoder: PostboxDecoder) { self.photo = decoder.decodeObjectForKey("p", decoder: { AutomaticMediaDownloadCategoryPeers(decoder: $0) }) as! AutomaticMediaDownloadCategoryPeers - self.voice = decoder.decodeObjectForKey("v", decoder: { AutomaticMediaDownloadCategoryPeers(decoder: $0) }) as! AutomaticMediaDownloadCategoryPeers - self.instantVideo = decoder.decodeObjectForKey("iv", decoder: { AutomaticMediaDownloadCategoryPeers(decoder: $0) }) as! AutomaticMediaDownloadCategoryPeers - self.gif = decoder.decodeObjectForKey("g", decoder: { AutomaticMediaDownloadCategoryPeers(decoder: $0) }) as! AutomaticMediaDownloadCategoryPeers + self.video = decoder.decodeObjectForKey("vd", decoder: { AutomaticMediaDownloadCategoryPeers(decoder: $0) }) as! AutomaticMediaDownloadCategoryPeers + self.files = decoder.decodeObjectForKey("f", decoder: { AutomaticMediaDownloadCategoryPeers(decoder: $0) }) as! AutomaticMediaDownloadCategoryPeers +// self.instantVideo = decoder.decodeObjectForKey("iv", decoder: { AutomaticMediaDownloadCategoryPeers(decoder: $0) }) as! AutomaticMediaDownloadCategoryPeers +// self.gif = decoder.decodeObjectForKey("g", decoder: { AutomaticMediaDownloadCategoryPeers(decoder: $0) }) as! AutomaticMediaDownloadCategoryPeers } public func encode(_ encoder: PostboxEncoder) { encoder.encodeObject(self.photo, forKey: "p") - encoder.encodeObject(self.voice, forKey: "v") - encoder.encodeObject(self.instantVideo, forKey: "iv") - encoder.encodeObject(self.gif, forKey: "g") + encoder.encodeObject(self.video, forKey: "vd") + encoder.encodeObject(self.files, forKey: "f") +// encoder.encodeObject(self.instantVideo, forKey: "iv") +// encoder.encodeObject(self.gif, forKey: "g") } public func withUpdatedPhoto(_ photo: AutomaticMediaDownloadCategoryPeers) -> AutomaticMediaDownloadCategories { - return AutomaticMediaDownloadCategories(photo: photo, voice: self.voice, instantVideo: self.instantVideo, gif: self.gif) + return AutomaticMediaDownloadCategories(photo: photo, video: video, files: files) + } + public func withUpdatedVideo(_ video: AutomaticMediaDownloadCategoryPeers) -> AutomaticMediaDownloadCategories { + return AutomaticMediaDownloadCategories(photo: photo, video: video, files: files) + } + public func withUpdatedFiles(_ files: AutomaticMediaDownloadCategoryPeers) -> AutomaticMediaDownloadCategories { + return AutomaticMediaDownloadCategories(photo: photo, video: video, files: files) } public func withUpdatedVoice(_ voice: AutomaticMediaDownloadCategoryPeers) -> AutomaticMediaDownloadCategories { - return AutomaticMediaDownloadCategories(photo: self.photo, voice: voice, instantVideo: self.instantVideo, gif: self.gif) + return AutomaticMediaDownloadCategories(photo: photo, video: video, files: files) } public func withUpdatedInstantVideo(_ instantVideo: AutomaticMediaDownloadCategoryPeers) -> AutomaticMediaDownloadCategories { - return AutomaticMediaDownloadCategories(photo: self.photo, voice: self.voice, instantVideo: instantVideo, gif: self.gif) + return AutomaticMediaDownloadCategories(photo: photo, video: video, files: files) } public func withUpdatedGif(_ gif: AutomaticMediaDownloadCategoryPeers) -> AutomaticMediaDownloadCategories { - return AutomaticMediaDownloadCategories(photo: self.photo, voice: self.voice, instantVideo: self.instantVideo, gif: gif) + return AutomaticMediaDownloadCategories(photo: photo, video: video, files: files) } public static func ==(lhs: AutomaticMediaDownloadCategories, rhs: AutomaticMediaDownloadCategories) -> Bool { if lhs.photo != rhs.photo { return false } - if lhs.voice != rhs.voice { + if lhs.video != rhs.video { return false } - if lhs.instantVideo != rhs.instantVideo { - return false - } - if lhs.gif != rhs.gif { + if lhs.files != rhs.files { return false } return true @@ -110,25 +142,37 @@ public struct AutomaticMediaDownloadCategories: PostboxCoding, Equatable { public struct AutomaticMediaDownloadSettings: PreferencesEntry, Equatable { public let categories: AutomaticMediaDownloadCategories - public let saveIncomingPhotos: Bool - + public let automaticDownload: Bool + public let downloadFolder: String + public let automaticSaveDownloadedFiles: Bool public static var defaultSettings: AutomaticMediaDownloadSettings { - return AutomaticMediaDownloadSettings(categories: AutomaticMediaDownloadCategories(photo: AutomaticMediaDownloadCategoryPeers(privateChats: true, groupsAndChannels: true), voice: AutomaticMediaDownloadCategoryPeers(privateChats: true, groupsAndChannels: true), instantVideo: AutomaticMediaDownloadCategoryPeers(privateChats: true, groupsAndChannels: true), gif: AutomaticMediaDownloadCategoryPeers(privateChats: true, groupsAndChannels: true)), saveIncomingPhotos: false) + let categories = AutomaticMediaDownloadCategories(photo: AutomaticMediaDownloadCategoryPeers(privateChats: true, groupChats: true, channels: true, fileSize: nil), video: AutomaticMediaDownloadCategoryPeers(privateChats: true, groupChats: true, channels: true, fileSize: 10 * 1024 * 1024), files: AutomaticMediaDownloadCategoryPeers(privateChats: false, groupChats: false, channels: false, fileSize: 10 * 1024 * 1024)) + return AutomaticMediaDownloadSettings(categories: categories, automaticDownload: true, downloadFolder: "~/Downloads/".nsstring.expandingTildeInPath, automaticSaveDownloadedFiles: false) } + - init(categories: AutomaticMediaDownloadCategories, saveIncomingPhotos: Bool) { + + init(categories: AutomaticMediaDownloadCategories, automaticDownload: Bool, downloadFolder: String, automaticSaveDownloadedFiles: Bool) { self.categories = categories - self.saveIncomingPhotos = saveIncomingPhotos + self.automaticDownload = automaticDownload + self.downloadFolder = downloadFolder + self.automaticSaveDownloadedFiles = automaticSaveDownloadedFiles } public init(decoder: PostboxDecoder) { self.categories = decoder.decodeObjectForKey("c", decoder: { AutomaticMediaDownloadCategories(decoder: $0) }) as! AutomaticMediaDownloadCategories - self.saveIncomingPhotos = decoder.decodeInt32ForKey("siph", orElse: 0) != 0 + self.automaticDownload = decoder.decodeBoolForKey("a", orElse: true) + self.downloadFolder = decoder.decodeStringForKey("d", orElse: "~/Downloads/".nsstring.expandingTildeInPath) + self.automaticSaveDownloadedFiles = decoder.decodeBoolForKey("ad", orElse: false) + } public func encode(_ encoder: PostboxEncoder) { encoder.encodeObject(self.categories, forKey: "c") - encoder.encodeInt32(self.saveIncomingPhotos ? 1 : 0, forKey: "siph") + encoder.encodeBool(self.automaticDownload, forKey: "a") + encoder.encodeString(self.downloadFolder, forKey: "d") + encoder.encodeBool(self.automaticSaveDownloadedFiles, forKey: "ad") + } public func isEqual(to: PreferencesEntry) -> Bool { @@ -140,21 +184,29 @@ public struct AutomaticMediaDownloadSettings: PreferencesEntry, Equatable { } public static func ==(lhs: AutomaticMediaDownloadSettings, rhs: AutomaticMediaDownloadSettings) -> Bool { - return lhs.categories == rhs.categories && lhs.saveIncomingPhotos == rhs.saveIncomingPhotos + return lhs.categories == rhs.categories && lhs.automaticDownload == rhs.automaticDownload && lhs.downloadFolder == rhs.downloadFolder && lhs.automaticSaveDownloadedFiles == rhs.automaticSaveDownloadedFiles } func withUpdatedCategories(_ categories: AutomaticMediaDownloadCategories) -> AutomaticMediaDownloadSettings { - return AutomaticMediaDownloadSettings(categories: categories, saveIncomingPhotos: self.saveIncomingPhotos) + return AutomaticMediaDownloadSettings(categories: categories, automaticDownload: automaticDownload, downloadFolder: self.downloadFolder, automaticSaveDownloadedFiles: self.automaticSaveDownloadedFiles) } - func withUpdatedSaveIncomingPhotos(_ saveIncomingPhotos: Bool) -> AutomaticMediaDownloadSettings { - return AutomaticMediaDownloadSettings(categories: self.categories, saveIncomingPhotos: saveIncomingPhotos) + func withUpdatedAutomaticDownload(_ automaticDownload: Bool) -> AutomaticMediaDownloadSettings { + return AutomaticMediaDownloadSettings(categories: categories, automaticDownload: automaticDownload, downloadFolder: self.downloadFolder, automaticSaveDownloadedFiles: self.automaticSaveDownloadedFiles) + } + + func withUpdatedDownloadFolder(_ folder: String) -> AutomaticMediaDownloadSettings { + return AutomaticMediaDownloadSettings(categories: categories, automaticDownload: automaticDownload, downloadFolder: folder, automaticSaveDownloadedFiles: self.automaticSaveDownloadedFiles) + } + + func withUpdatedAutomaticSaveDownloadedFiles(_ automaticSaveDownloadedFiles: Bool) -> AutomaticMediaDownloadSettings { + return AutomaticMediaDownloadSettings(categories: categories, automaticDownload: automaticDownload, downloadFolder: self.downloadFolder, automaticSaveDownloadedFiles: automaticSaveDownloadedFiles) } } func updateMediaDownloadSettingsInteractively(postbox: Postbox, _ f: @escaping (AutomaticMediaDownloadSettings) -> AutomaticMediaDownloadSettings) -> Signal { - return postbox.modify { modifier -> Void in - modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.automaticMediaDownloadSettings, { entry in + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.automaticMediaDownloadSettings, { entry in let currentSettings: AutomaticMediaDownloadSettings if let entry = entry as? AutomaticMediaDownloadSettings { currentSettings = entry @@ -165,3 +217,9 @@ func updateMediaDownloadSettingsInteractively(postbox: Postbox, _ f: @escaping ( }) } } + +func automaticDownloadSettings(postbox: Postbox) -> Signal { + return postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.automaticMediaDownloadSettings]) |> map { value in + return value.values[ApplicationSpecificPreferencesKeys.automaticMediaDownloadSettings] as? AutomaticMediaDownloadSettings ?? AutomaticMediaDownloadSettings.defaultSettings + } +} diff --git a/Telegram-Mac/AutoplayPreferences.swift b/Telegram-Mac/AutoplayPreferences.swift new file mode 100644 index 0000000000..8d3126aa59 --- /dev/null +++ b/Telegram-Mac/AutoplayPreferences.swift @@ -0,0 +1,98 @@ +// +// AutoplayPreferences.swift +// Telegram +// +// Created by Mikhail Filimonov on 11/02/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import Postbox +import SwiftSignalKit + + +class AutoplayMediaPreferences : PreferencesEntry, Equatable { + let gifs: Bool + let videos: Bool + let soundOnHover: Bool + let preloadVideos: Bool + let loopAnimatedStickers: Bool + init(gifs: Bool, videos: Bool, soundOnHover: Bool, preloadVideos: Bool, loopAnimatedStickers: Bool ) { + self.gifs = gifs + self.videos = videos + self.soundOnHover = soundOnHover + self.preloadVideos = preloadVideos + self.loopAnimatedStickers = loopAnimatedStickers + } + + static var defaultSettings: AutoplayMediaPreferences { + return AutoplayMediaPreferences(gifs: true, videos: true, soundOnHover: true, preloadVideos: true, loopAnimatedStickers: true) + } + + required init(decoder: PostboxDecoder) { + self.gifs = decoder.decodeInt32ForKey("g", orElse: 0) == 1 + self.videos = decoder.decodeInt32ForKey("v", orElse: 0) == 1 + self.soundOnHover = decoder.decodeInt32ForKey("soh", orElse: 0) == 1 + self.preloadVideos = decoder.decodeInt32ForKey("pv", orElse: 0) == 1 + self.loopAnimatedStickers = decoder.decodeInt32ForKey("las", orElse: 0) == 1 + } + + func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(gifs ? 1 : 0, forKey: "g") + encoder.encodeInt32(videos ? 1 : 0, forKey: "v") + encoder.encodeInt32(soundOnHover ? 1 : 0, forKey: "soh") + encoder.encodeInt32(preloadVideos ? 1 : 0, forKey: "pv") + encoder.encodeInt32(loopAnimatedStickers ? 1 : 0, forKey: "las") + } + + static func == (lhs: AutoplayMediaPreferences, rhs: AutoplayMediaPreferences) -> Bool { + return lhs.gifs == rhs.gifs && lhs.videos == rhs.videos && lhs.soundOnHover == rhs.soundOnHover && lhs.preloadVideos == rhs.preloadVideos && lhs.loopAnimatedStickers == rhs.loopAnimatedStickers + } + + func isEqual(to: PreferencesEntry) -> Bool { + if let to = to as? AutoplayMediaPreferences { + return self == to + } else { + return false + } + } + + func withUpdatedAutoplayGifs(_ gifs: Bool) -> AutoplayMediaPreferences { + return AutoplayMediaPreferences(gifs: gifs, videos: self.videos, soundOnHover: self.soundOnHover, preloadVideos: self.preloadVideos, loopAnimatedStickers: self.loopAnimatedStickers) + } + func withUpdatedAutoplayVideos(_ videos: Bool) -> AutoplayMediaPreferences { + return AutoplayMediaPreferences(gifs: self.gifs, videos: videos, soundOnHover: self.soundOnHover, preloadVideos: self.preloadVideos, loopAnimatedStickers: self.loopAnimatedStickers) + } + func withUpdatedAutoplaySoundOnHover(_ soundOnHover: Bool) -> AutoplayMediaPreferences { + return AutoplayMediaPreferences(gifs: self.gifs, videos: self.videos, soundOnHover: soundOnHover, preloadVideos: self.preloadVideos, loopAnimatedStickers: self.loopAnimatedStickers) + } + func withUpdatedAutoplayPreloadVideos(_ preloadVideos: Bool) -> AutoplayMediaPreferences { + return AutoplayMediaPreferences(gifs: self.gifs, videos: self.videos, soundOnHover: self.soundOnHover, preloadVideos: preloadVideos, loopAnimatedStickers: self.loopAnimatedStickers) + } + func withUpdatedLoopAnimatedStickers(_ loopAnimatedStickers: Bool) -> AutoplayMediaPreferences { + return AutoplayMediaPreferences(gifs: self.gifs, videos: self.videos, soundOnHover: self.soundOnHover, preloadVideos: self.preloadVideos, loopAnimatedStickers: loopAnimatedStickers) + } +} + + +func updateAutoplayMediaSettingsInteractively(postbox: Postbox, _ f: @escaping (AutoplayMediaPreferences) -> AutoplayMediaPreferences) -> Signal { + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.autoplayMedia, { entry in + let currentSettings: AutoplayMediaPreferences + if let entry = entry as? AutoplayMediaPreferences { + currentSettings = entry + } else { + currentSettings = AutoplayMediaPreferences.defaultSettings + } + + return f(currentSettings) + }) + } +} + + +func autoplayMediaSettings(postbox: Postbox) -> Signal { + return postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.autoplayMedia]) |> map { views in + return views.values[ApplicationSpecificPreferencesKeys.autoplayMedia] as? AutoplayMediaPreferences ?? AutoplayMediaPreferences.defaultSettings + } +} diff --git a/Telegram-Mac/AutoremoMessagesController.swift b/Telegram-Mac/AutoremoMessagesController.swift new file mode 100644 index 0000000000..4f76836eb3 --- /dev/null +++ b/Telegram-Mac/AutoremoMessagesController.swift @@ -0,0 +1,225 @@ +// +// AutoremoMessagesController.swift +// Telegram +// +// Created by Mikhail Filimonov on 03.02.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore +import Postbox +import SwiftSignalKit + + +private final class Arguments { + let context: AccountContext + let setTimeout:(Int32)->Void + let clearHistory: ()->Void + init(context: AccountContext, setTimeout:@escaping(Int32)->Void, clearHistory: @escaping()->Void) { + self.context = context + self.setTimeout = setTimeout + self.clearHistory = clearHistory + } +} + +private struct State : Equatable { + var autoremoveTimeout: CachedPeerAutoremoveTimeout? + var timeout: Int32 + let peer: PeerEquatable +} + +private let _id_sticker = InputDataIdentifier("_id_sticker") +private let _id_preview = InputDataIdentifier("_id_preview") +private let _id_never = InputDataIdentifier("_id_never") +private let _id_day = InputDataIdentifier("_id_day") +private let _id_week = InputDataIdentifier("_id_week") +private let _id_clear = InputDataIdentifier("_id_clear") +private let _id_global = InputDataIdentifier("_id_global") +private let _id_clear_both = InputDataIdentifier("_id_clear_both") +private func entries(_ state: State, arguments: Arguments, onlyDelete: Bool) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + if state.peer.peer.canClearHistory, !onlyDelete { + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_clear, data: .init(name: L10n.chatContextClearHistory, color: theme.colors.redUI, icon: theme.icons.destruct_clear_history, type: .none, viewType: .singleItem, enabled: true, action: arguments.clearHistory))) + index += 1 + + } else { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_sticker, equatable: nil, comparable: nil, item: { initialSize, stableId in + return AnimtedStickerHeaderItem(initialSize, stableId: stableId, context: arguments.context, sticker: LocalAnimatedSticker.destructor, text: .init()) + })) + index += 1 + + } + + + if state.peer.peer.canManageDestructTimer && state.peer.peer.id != arguments.context.peerId { + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.autoremoveMessagesHeader), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_preview, equatable: InputDataEquatable(state), comparable: nil, item: { [weak arguments] initialSize, stableId in + let values:[Int32] = [0, .secondsInDay, .secondsInWeek, .secondsInMonth] + + + return SelectSizeRowItem(initialSize, stableId: stableId, current: state.timeout, sizes: values, hasMarkers: false, titles: [L10n.autoremoveMessagesNever, L10n.autoremoveMessagesDay1, L10n.autoremoveMessagesWeek1, L10n.autoremoveMessagesMonth1], viewType: .singleItem, selectAction: { index in + arguments?.setTimeout(values[index]) + }) + })) + index += 1 + + if let _ = state.autoremoveTimeout?.timeout?.peerValue { + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.autoremoveMessagesDesc), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + } else { + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.autoremoveMessagesDesc), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + } + + } + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + return entries +} + + +func AutoremoveMessagesController(context: AccountContext, peer: Peer, onlyDelete: Bool = false) -> InputDataModalController { + + + let peerId = peer.id + + let actionsDisposable = DisposableSet() + + let initialState = State(autoremoveTimeout: nil, timeout: 0, peer: PeerEquatable(peer)) + + var close:(()->Void)? = nil + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let arguments = Arguments(context: context, setTimeout: { timeout in + updateState { current in + var current = current + current.timeout = timeout + return current + } + }, clearHistory: { + var thridTitle: String? = nil + var canRemoveGlobally: Bool = false + if peerId.namespace == Namespaces.Peer.CloudUser && peerId != context.account.peerId && !peer.isBot { + if context.limitConfiguration.maxMessageRevokeIntervalInPrivateChats == LimitsConfiguration.timeIntervalForever { + canRemoveGlobally = true + } + } + if canRemoveGlobally { + thridTitle = L10n.chatMessageDeleteForMeAndPerson(peer.displayTitle) + } + modernConfirm(for: context.window, account: context.account, peerId: peer.id, information: peer is TelegramUser ? peer.id == context.peerId ? L10n.peerInfoConfirmClearHistorySavedMesssages : canRemoveGlobally || peerId.namespace == Namespaces.Peer.SecretChat ? L10n.peerInfoConfirmClearHistoryUserBothSides : L10n.peerInfoConfirmClearHistoryUser : L10n.peerInfoConfirmClearHistoryGroup, okTitle: L10n.peerInfoConfirmClear, thridTitle: thridTitle, thridAutoOn: false, successHandler: { result in + context.chatUndoManager.clearHistoryInteractively(engine: context.engine, peerId: peerId, type: result == .thrid ? .forEveryone : .forLocalPeer) + close?() + }) + }) + + + let signal = statePromise.get() |> map { state in + return InputDataSignalValue(entries: entries(state, arguments: arguments, onlyDelete: onlyDelete)) + } + + let controller = InputDataController(dataSignal: signal, title: onlyDelete ? L10n.autoremoveMessagesTitleDeleteOnly : L10n.autoremoveMessagesTitle, hasDone: false) + + + controller.onDeinit = { + actionsDisposable.dispose() + } + + actionsDisposable.add(context.account.viewTracker.peerView(peer.id).start(next: { peerView in + let autoremoveTimeout: CachedPeerAutoremoveTimeout? + if let cachedData = peerView.cachedData as? CachedGroupData { + autoremoveTimeout = cachedData.autoremoveTimeout + } else if let cachedData = peerView.cachedData as? CachedChannelData { + autoremoveTimeout = cachedData.autoremoveTimeout + } else if let cachedData = peerView.cachedData as? CachedUserData { + autoremoveTimeout = cachedData.autoremoveTimeout + } else { + autoremoveTimeout = nil + } + updateState { current in + var current = current + current.autoremoveTimeout = autoremoveTimeout + current.timeout = autoremoveTimeout?.timeout?.peerValue ?? 0 + return current + } + })) + + controller.validateData = { _ in + return .fail(.doSomething(next: { f in + + let state = stateValue.with { $0 } + + if let timeout = state.autoremoveTimeout?.timeout?.peerValue { + if timeout == state.timeout { + close?() + return + } + } + +// var text: String? = nil +// if state.timeout != 0 { +// switch state.timeout { +// case .secondsInDay: +// text = L10n.tipAutoDeleteTimerSetForDay +// case .secondsInWeek: +// text = L10n.tipAutoDeleteTimerSetForWeek +// default: +// break +// } +// } else { +// text = L10n.tipAutoDeleteTimerSetOff +// } + + _ = showModalProgress(signal: context.engine.peers.setChatMessageAutoremoveTimeoutInteractively(peerId: peerId, timeout: state.timeout == 0 ? nil : state.timeout), for: context.window).start(completed: { + f(.success(.custom({ + // if let text = text { + // showModalText(for: context.window, text: text) + // } + close?() + }))) + }) + })) + } + + let modalInteractions = ModalInteractions(acceptTitle: L10n.modalDone, accept: { [weak controller] in + _ = controller?.returnKeyAction() + }, drawBorder: true, height: 50, singleButton: true) + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) + + close = { [weak modalController] in + modalController?.modal?.close() + } + + + return modalController + +} + diff --git a/Telegram-Mac/AvatarContentView.swift b/Telegram-Mac/AvatarContentView.swift new file mode 100644 index 0000000000..741d4967f8 --- /dev/null +++ b/Telegram-Mac/AvatarContentView.swift @@ -0,0 +1,150 @@ +// +// AvatarContentView.swift +// Telegram +// +// Created by Mikhail Filimonov on 05.09.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit +import TelegramCore +import Postbox + + +final class AvatarContentView: View { + private let unclippedView: ImageView + private let clippedView: ImageView + + private var disposable: Disposable? + private var audioLevelView: VoiceBlobView? + + let peerId: PeerId + + private var scaleAnimator: DisplayLinkAnimator? + private let inset: CGFloat + init(context: AccountContext, peer: Peer, message: Message?, synchronousLoad: Bool, size: NSSize, inset: CGFloat = 3) { + self.peerId = peer.id + self.inset = inset + self.unclippedView = ImageView() + self.clippedView = ImageView() + + super.init(frame: CGRect(origin: .zero, size: size)) + + self.addSubview(self.unclippedView) + self.addSubview(self.clippedView) + + + + let signal = peerAvatarImage(account: context.account, photo: .peer(peer, peer.smallProfileImage, peer.displayLetters, nil), displayDimensions: size, scale: System.backingScale, font: .avatar(size.height / 3 + 3), genCap: true, synchronousLoad: synchronousLoad) + + let disposable = (signal + |> deliverOnMainQueue).start(next: { [weak self] image in + guard let strongSelf = self else { + return + } + if let image = image.0 { + strongSelf.updateImage(image: image) + } + }) + self.disposable = disposable + } + + func updateAudioLevel(color: NSColor, value: Float) { + if self.audioLevelView == nil, value > 0.0 { + let blobFrame = NSMakeRect(0, 0, frame.width + 8, frame.height + 8) + + let audioLevelView = VoiceBlobView( + frame: blobFrame, + maxLevel: 0.3, + smallBlobRange: (0, 0), + mediumBlobRange: (0.7, 0.8), + bigBlobRange: (0.8, 0.9) + ) + + + audioLevelView.setColor(color) + self.audioLevelView = audioLevelView + self.addSubview(audioLevelView, positioned: .below, relativeTo: self.subviews.first) + audioLevelView.center() + } + + let level = min(1.0, max(0.0, CGFloat(value))) + if let audioLevelView = self.audioLevelView { + audioLevelView.updateLevel(CGFloat(value) * 2.0) + + let avatarScale: CGFloat + let audioLevelScale: CGFloat + if value > 0.0 { + audioLevelView.startAnimating() + avatarScale = 1.03 + level * 0.07 + audioLevelScale = 1.0 + } else { + audioLevelView.stopAnimating() + avatarScale = 1.0 + audioLevelScale = 0.01 + } + let t = clippedView.layer!.transform + let scale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) + self.scaleAnimator = DisplayLinkAnimator(duration: 0.1, from: scale, to: avatarScale, update: { [weak self] value in + guard let `self` = self else { + return + } + + let rect = self.clippedView.bounds + + var fr = CATransform3DIdentity + fr = CATransform3DTranslate(fr, rect.width / 2, rect.width / 2, 0) + fr = CATransform3DScale(fr, value, value, 1) + fr = CATransform3DTranslate(fr, -(rect.width / 2), -(rect.height / 2), 0) + + self.clippedView.layer?.transform = fr + self.unclippedView.layer?.transform = fr + + }, completion: { + + }) + } + } + + + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + private func updateImage(image: CGImage) { + self.unclippedView.image = image + let frameSize = NSMakeSize(frame.height, frame.height) + self.clippedView.image = generateImage(frameSize, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + context.draw(image, in: CGRect(origin: CGPoint(), size: size)) + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + context.setBlendMode(.copy) + context.setFillColor(NSColor.clear.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: -(inset / 2), dy: -(inset / 2)).offsetBy(dx: -(frameSize.width - inset), dy: 0.0)) + }) + } + + deinit { + self.disposable?.dispose() + } + + func updateLayout(size: CGSize, isClipped: Bool, animated: Bool) { + self.unclippedView.frame = CGRect(origin: focus(size).origin, size: size) + self.clippedView.frame = CGRect(origin: focus(size).origin, size: size) + self.unclippedView.change(opacity: isClipped ? 0.0 : 1.0, animated: animated) + self.clippedView.change(opacity: isClipped ? 1.0 : 0.0, animated: animated) + } +} + diff --git a/Telegram-Mac/AvatarLayer.swift b/Telegram-Mac/AvatarLayer.swift index 35b2b35749..de960d1a8e 100644 --- a/Telegram-Mac/AvatarLayer.swift +++ b/Telegram-Mac/AvatarLayer.swift @@ -7,16 +7,19 @@ // import Cocoa -import TelegramCoreMac -import PostboxMac +import TelegramCore + +import Postbox import TGUIKit -import SwiftSignalKitMac +import SwiftSignalKit + + private class AvatarNodeParameters: NSObject { let account: Account - let peerId: PeerId + let peerId: Peer let letters: [String] let font: NSFont - init(account: Account, peerId: PeerId, letters: [String], font: NSFont) { + init(account: Account, peerId: Peer, letters: [String], font: NSFont) { self.account = account self.peerId = peerId self.letters = letters @@ -27,17 +30,21 @@ private class AvatarNodeParameters: NSObject { -private enum AvatarNodeState: Equatable { +enum AvatarNodeState: Equatable { case Empty - case PeerAvatar(PeerId, [String], TelegramMediaImageRepresentation?, CGFloat) + case PeerAvatar(Peer, [String], TelegramMediaImageRepresentation?, Message?, NSSize?) + case ArchivedChats + } -private func ==(lhs: AvatarNodeState, rhs: AvatarNodeState) -> Bool { +func ==(lhs: AvatarNodeState, rhs: AvatarNodeState) -> Bool { switch (lhs, rhs) { case (.Empty, .Empty): return true - case let (.PeerAvatar(lhsPeerId, lhsLetters, lhsPhotoRepresentations, lhsScale), .PeerAvatar(rhsPeerId, rhsLetters, rhsPhotoRepresentations, rhsScale)): - return lhsPeerId == rhsPeerId && lhsLetters == rhsLetters && lhsPhotoRepresentations == rhsPhotoRepresentations && lhsScale == rhsScale + case let (.PeerAvatar(lhsPeer, lhsLetters, lhsPhotoRepresentations, _, lhsSize), .PeerAvatar(rhsPeer, rhsLetters, rhsPhotoRepresentations, _, rhsSize)): + return lhsPeer.isEqual(rhsPeer) && lhsLetters == rhsLetters && lhsPhotoRepresentations == rhsPhotoRepresentations && lhsSize == rhsSize + case (.ArchivedChats, .ArchivedChats): + return true default: return false } @@ -54,9 +61,6 @@ class AvatarControl: NSView { var font: NSFont { didSet { if oldValue !== font { - if let parameters = self.parameters { - self.parameters = AvatarNodeParameters(account: parameters.account, peerId: parameters.peerId, letters: parameters.letters, font: self.font) - } if !self.displaySuspended { self.needsDisplay = true @@ -64,15 +68,24 @@ class AvatarControl: NSView { } } } - private var parameters: AvatarNodeParameters? private let disposable = MetaDisposable() private var state: AvatarNodeState = .Empty private var account:Account? - private var peer:Peer? - + private var contentScale: CGFloat = 0 public var animated: Bool = false + private var _attemptLoadNextSynchronous: Bool = false + public var attemptLoadNextSynchronous: Bool { + get { + let result = _attemptLoadNextSynchronous + _attemptLoadNextSynchronous = false + return result + } + set { + _attemptLoadNextSynchronous = newValue + } + } public init(font: NSFont) { self.font = font @@ -101,10 +114,28 @@ class AvatarControl: NSView { } } - public func setPeer(account: Account, peer: Peer?) { + public func setState(account: Account, state: AvatarNodeState) { self.account = account - self.peer = peer - self.viewDidChangeBackingProperties() + if state != self.state { + contentScale = 0 + self.state = state + self.viewDidChangeBackingProperties() + } + } + + public func setPeer(account: Account, peer: Peer?, message: Message? = nil, size: NSSize? = nil) { + self.account = account + let state: AvatarNodeState + if let peer = peer { + state = .PeerAvatar(peer, peer.displayLetters, peer.smallProfileImage, message, size) + } else { + state = .Empty + } + if self.state != state || self.layer?.contents == nil { + self.state = state + contentScale = 0 + self.viewDidChangeBackingProperties() + } } func set(handler:@escaping (AvatarControl) -> Void, for event:ControlEvent) -> Void { @@ -165,35 +196,53 @@ class AvatarControl: NSView { override func viewDidChangeBackingProperties() { layer?.contentsScale = backingScaleFactor + layer?.contentsGravity = .resizeAspectFill - if let account = account, let peer = peer { - let updatedState = AvatarNodeState.PeerAvatar(peer.id, peer.displayLetters, peer.smallProfileImage, backingScaleFactor) - if updatedState != self.state { - self.state = updatedState - - let parameters = AvatarNodeParameters(account: account, peerId: peer.id, letters: peer.displayLetters, font: self.font) - + if let account = account, self.state != .Empty { + if contentScale != backingScaleFactor { + contentScale = backingScaleFactor self.displaySuspended = true self.layer?.contents = nil - - if let signal = peerAvatarImage(account: account, peer: peer, displayDimensions:frame.size, scale:backingScaleFactor, font: self.font) { - setSignal(signal, animated: animated) - + let photo: PeerPhoto? + var updatedSize: NSSize = self.frame.size + switch state { + case let .PeerAvatar(peer, letters, representation, message, size): + if let peer = peer as? TelegramUser, peer.firstName == nil && peer.lastName == nil { + photo = nil + self.setState(account: account, state: .Empty) + let icon = theme.icons.deletedAccount + self.setSignal(generateEmptyPhoto(updatedSize, type: .icon(colors: theme.colors.peerColors(Int(peer.id.id._internalGetInt64Value() % 7)), icon: icon, iconSize: icon.backingSize.aspectFitted(NSMakeSize(min(50, updatedSize.width - 20), min(updatedSize.height - 20, 50))), cornerRadius: nil)) |> map {($0, false)}) + return + } else { + photo = .peer(peer, representation, letters, message) + } + updatedSize = size ?? frame.size + case .Empty: + photo = nil + default: + photo = nil + } + if let photo = photo { + setSignal(peerAvatarImage(account: account, photo: photo, displayDimensions: updatedSize, scale:backingScaleFactor, font: self.font, synchronousLoad: attemptLoadNextSynchronous), force: false) } else { + let content = self.layer?.contents self.displaySuspended = false + self.layer?.contents = content } - if self.parameters == nil || self.parameters != parameters { - self.parameters = parameters - self.needsDisplay = true - } + } + } else { + self.state = .Empty } } - public func setSignal(_ signal: Signal, animated: Bool) { - self.disposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] next in + public func setSignal(_ signal: Signal<(CGImage?, Bool), NoError>, force: Bool = true) { + if force { + self.state = .Empty + } + self.disposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] image, animated in if let strongSelf = self { - strongSelf.layer?.contents = next + strongSelf.layer?.contents = image if animated { strongSelf.layer?.animateContents() } @@ -212,7 +261,7 @@ class AvatarControl: NSView { trackingArea = nil if let _ = window { - let options:NSTrackingArea.Options = [.cursorUpdate, .mouseEnteredAndExited, .mouseMoved, .activeInKeyWindow, .inVisibleRect] + let options:NSTrackingArea.Options = [.cursorUpdate, .mouseEnteredAndExited, .mouseMoved, .activeAlways, .inVisibleRect] self.trackingArea = NSTrackingArea(rect: self.bounds, options: options, owner: self, userInfo: nil) self.addTrackingArea(self.trackingArea!) diff --git a/Telegram-Mac/Base.lproj/MainMenu.xib b/Telegram-Mac/Base.lproj/MainMenu.xib new file mode 100644 index 0000000000..4af32c9af5 --- /dev/null +++ b/Telegram-Mac/Base.lproj/MainMenu.xib @@ -0,0 +1,313 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Telegram-Mac/BaseApplicationSettings.swift b/Telegram-Mac/BaseApplicationSettings.swift index 619c28587f..f47b3fe93a 100644 --- a/Telegram-Mac/BaseApplicationSettings.swift +++ b/Telegram-Mac/BaseApplicationSettings.swift @@ -7,47 +7,81 @@ // import Cocoa -import PostboxMac -import SwiftSignalKitMac +import Postbox +import SwiftSignalKit +import TelegramCore + class BaseApplicationSettings: PreferencesEntry, Equatable { - let fontSize: Int32 let handleInAppKeys: Bool let sidebar: Bool - + let showCallsTab: Bool + let latestArticles: Bool + let predictEmoji: Bool + let bigEmoji: Bool + let statusBar: Bool static var defaultSettings: BaseApplicationSettings { - return BaseApplicationSettings(fontSize: 13, handleInAppKeys: false, sidebar: true) + return BaseApplicationSettings(handleInAppKeys: false, sidebar: true, showCallsTab: true, latestArticles: true, predictEmoji: true, bigEmoji: true, statusBar: true) } - init(fontSize:Int32, handleInAppKeys: Bool, sidebar: Bool) { - self.fontSize = fontSize + init(handleInAppKeys: Bool, sidebar: Bool, showCallsTab: Bool, latestArticles: Bool, predictEmoji: Bool, bigEmoji: Bool, statusBar: Bool) { self.handleInAppKeys = handleInAppKeys self.sidebar = sidebar + self.showCallsTab = showCallsTab + self.latestArticles = latestArticles + self.predictEmoji = predictEmoji + self.bigEmoji = bigEmoji + self.statusBar = statusBar } required init(decoder: PostboxDecoder) { - self.fontSize = decoder.decodeInt32ForKey("f", orElse: 0) + self.showCallsTab = decoder.decodeInt32ForKey("c", orElse: 1) != 0 self.handleInAppKeys = decoder.decodeInt32ForKey("h", orElse: 0) != 0 self.sidebar = decoder.decodeInt32ForKey("e", orElse: 0) != 0 + self.latestArticles = decoder.decodeInt32ForKey("la", orElse: 1) != 0 + self.predictEmoji = decoder.decodeInt32ForKey("pe", orElse: 1) != 0 + self.bigEmoji = decoder.decodeInt32ForKey("bi", orElse: 1) != 0 + self.statusBar = decoder.decodeInt32ForKey("sb", orElse: 1) != 0 } func encode(_ encoder: PostboxEncoder) { - encoder.encodeInt32(self.fontSize, forKey: "f") + encoder.encodeInt32(self.showCallsTab ? 1 : 0, forKey: "c") encoder.encodeInt32(self.handleInAppKeys ? 1 : 0, forKey: "h") encoder.encodeInt32(self.sidebar ? 1 : 0, forKey: "e") + encoder.encodeInt32(self.latestArticles ? 1 : 0, forKey: "la") + encoder.encodeInt32(self.predictEmoji ? 1 : 0, forKey: "pe") + encoder.encodeInt32(self.bigEmoji ? 1 : 0, forKey: "bi") + encoder.encodeInt32(self.statusBar ? 1 : 0, forKey: "sb") } - func withUpdatedFontSize(_ fontSize: Int32) -> BaseApplicationSettings { - return BaseApplicationSettings(fontSize: fontSize, handleInAppKeys: self.handleInAppKeys, sidebar: self.sidebar) + func withUpdatedShowCallsTab(_ showCallsTab: Bool) -> BaseApplicationSettings { + return BaseApplicationSettings(handleInAppKeys: self.handleInAppKeys, sidebar: self.sidebar, showCallsTab: showCallsTab, latestArticles: self.latestArticles, predictEmoji: self.predictEmoji, bigEmoji: self.bigEmoji, statusBar: self.statusBar) } func withUpdatedSidebar(_ sidebar: Bool) -> BaseApplicationSettings { - return BaseApplicationSettings(fontSize: self.fontSize, handleInAppKeys: self.handleInAppKeys, sidebar: sidebar) + return BaseApplicationSettings(handleInAppKeys: self.handleInAppKeys, sidebar: sidebar, showCallsTab: self.showCallsTab, latestArticles: self.latestArticles, predictEmoji: self.predictEmoji, bigEmoji: self.bigEmoji, statusBar: self.statusBar) } func withUpdatedInAppKeyHandle(_ handleInAppKeys: Bool) -> BaseApplicationSettings { - return BaseApplicationSettings(fontSize: self.fontSize, handleInAppKeys: handleInAppKeys, sidebar: self.sidebar) + return BaseApplicationSettings(handleInAppKeys: handleInAppKeys, sidebar: self.sidebar, showCallsTab: self.showCallsTab, latestArticles: self.latestArticles, predictEmoji: self.predictEmoji, bigEmoji: self.bigEmoji, statusBar: self.statusBar) + } + + func withUpdatedLatestArticles(_ latestArticles: Bool) -> BaseApplicationSettings { + return BaseApplicationSettings(handleInAppKeys: self.handleInAppKeys, sidebar: self.sidebar, showCallsTab: self.showCallsTab, latestArticles: latestArticles, predictEmoji: self.predictEmoji, bigEmoji: self.bigEmoji, statusBar: self.statusBar) + } + + func withUpdatedPredictEmoji(_ predictEmoji: Bool) -> BaseApplicationSettings { + return BaseApplicationSettings(handleInAppKeys: self.handleInAppKeys, sidebar: self.sidebar, showCallsTab: self.showCallsTab, latestArticles: self.latestArticles, predictEmoji: predictEmoji, bigEmoji: self.bigEmoji, statusBar: self.statusBar) } + func withUpdatedBigEmoji(_ bigEmoji: Bool) -> BaseApplicationSettings { + return BaseApplicationSettings(handleInAppKeys: self.handleInAppKeys, sidebar: self.sidebar, showCallsTab: self.showCallsTab, latestArticles: self.latestArticles, predictEmoji: self.predictEmoji, bigEmoji: bigEmoji, statusBar: self.statusBar) + } + + func withUpdatedStatusBar(_ statusBar: Bool) -> BaseApplicationSettings { + return BaseApplicationSettings(handleInAppKeys: self.handleInAppKeys, sidebar: self.sidebar, showCallsTab: self.showCallsTab, latestArticles: self.latestArticles, predictEmoji: self.predictEmoji, bigEmoji: self.bigEmoji, statusBar: statusBar) + } + + func isEqual(to: PreferencesEntry) -> Bool { if let to = to as? BaseApplicationSettings { return self == to @@ -57,7 +91,7 @@ class BaseApplicationSettings: PreferencesEntry, Equatable { } static func ==(lhs: BaseApplicationSettings, rhs: BaseApplicationSettings) -> Bool { - if lhs.fontSize != rhs.fontSize { + if lhs.showCallsTab != rhs.showCallsTab { return false } if lhs.handleInAppKeys != rhs.handleInAppKeys { @@ -66,15 +100,33 @@ class BaseApplicationSettings: PreferencesEntry, Equatable { if lhs.sidebar != rhs.sidebar { return false } + if lhs.latestArticles != rhs.latestArticles { + return false + } + if lhs.predictEmoji != rhs.predictEmoji { + return false + } + if lhs.bigEmoji != rhs.bigEmoji { + return false + } + if lhs.statusBar != rhs.statusBar { + return false + } return true } } -func updateBaseAppSettingsInteractively(postbox: Postbox, _ f: @escaping (BaseApplicationSettings) -> BaseApplicationSettings) -> Signal { - return postbox.modify { modifier -> Void in - modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.baseAppSettings, { entry in +func baseAppSettings(accountManager: AccountManager) -> Signal { + return accountManager.sharedData(keys: [ApplicationSharedPreferencesKeys.baseAppSettings]) |> map { prefs in + return prefs.entries[ApplicationSharedPreferencesKeys.baseAppSettings] as? BaseApplicationSettings ?? BaseApplicationSettings.defaultSettings + } +} + +func updateBaseAppSettingsInteractively(accountManager: AccountManager, _ f: @escaping (BaseApplicationSettings) -> BaseApplicationSettings) -> Signal { + return accountManager.transaction { transaction -> Void in + transaction.updateSharedData(ApplicationSharedPreferencesKeys.baseAppSettings, { entry in let currentSettings: BaseApplicationSettings if let entry = entry as? BaseApplicationSettings { currentSettings = entry diff --git a/Telegram-Mac/Beta.xcconfig b/Telegram-Mac/Beta.xcconfig index d9d535c665..701281c37d 100644 --- a/Telegram-Mac/Beta.xcconfig +++ b/Telegram-Mac/Beta.xcconfig @@ -9,4 +9,5 @@ DSA_PEM_FILE = dsa_pub.pem SIMPLE_SLASH=/ -SFEED_URL = https:${SIMPLE_SLASH}/rink.hockeyapp.net/api/2/apps/6ed2ac3049e1407387c2f1ffcb74e81f +SFEED_URL = https:${SIMPLE_SLASH}/api.appcenter.ms/v0.1/public/sparkle/apps/6ed2ac30-49e1-4073-87c2-f1ffcb74e81f +APPCENTER_SECRET = 6ed2ac30-49e1-4073-87c2-f1ffcb74e81f diff --git a/Telegram-Mac/BioViewController.swift b/Telegram-Mac/BioViewController.swift deleted file mode 100644 index 3656dc6c52..0000000000 --- a/Telegram-Mac/BioViewController.swift +++ /dev/null @@ -1,206 +0,0 @@ -// -// BioViewController.swift -// Telegram -// -// Created by keepcoder on 12/07/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - -import Cocoa -import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac - -private let bioLimit: Int32 = 70 -private final class BioArguments { - let account: Account - let updateText:(String)->Void - init(account:Account, updateText:@escaping(String)->Void) { - self.account = account - self.updateText = updateText - } -} - -private enum BioEntry : TableItemListNodeEntry { - case section(Int32) - case text(Int32, String) - case description(Int32) - - var stableId: Int32 { - switch self { - case .section(let id): - return (id + 1) * 1000 - id - case .text: - return 1 - case .description: - return 2 - } - } - - var index:Int32 { - switch self { - case .section(let id): - return (id + 1) * 1000 - id - case .text(let sectionId, _): - return (sectionId * 1000) + stableId - case .description(let sectionId): - return (sectionId * 1000) + stableId - } - } - - func item(_ arguments: BioArguments, initialSize: NSSize) -> TableRowItem { - switch self { - case .section: - return GeneralRowItem(initialSize, height: 20, stableId: stableId) - case .text(_, let text): - return GeneralInputRowItem(initialSize, stableId: stableId, placeholder: tr(.bioPlaceholder), text: text, limit: bioLimit, textChangeHandler: { updated in - arguments.updateText(updated) - }) - case .description: - return GeneralTextRowItem(initialSize, stableId: stableId, text: tr(.bioDescription)) - } - } -} - -private func <(lhs: BioEntry, rhs: BioEntry) -> Bool { - return lhs.index < rhs.index -} - -private func ==(lhs: BioEntry, rhs: BioEntry) -> Bool { - return lhs.index == rhs.index -} - -private func BioEntries(_ cachedData: CachedUserData?, state: BioState) -> [BioEntry] { - var entries:[BioEntry] = [] - - var sectionId:Int32 = 1 - entries.append(.section(sectionId)) - sectionId += 1 - - entries.append(.text(sectionId, state.updatedText ?? cachedData?.about ?? "")) - entries.append(.description(sectionId)) - return entries -} - -private final class BioState : Equatable { - let updatedText:String? - let updating: Bool - let initiated: Bool - init(updatedText: String? = nil, updating: Bool = false, initiated: Bool = false) { - self.updatedText = updatedText - self.updating = updating - self.initiated = initiated - } - func withUpdateUpdating(_ updating: Bool) -> BioState { - return BioState(updatedText: self.updatedText, updating: updating, initiated: self.initiated) - } - - func withUpdatedInitiated(_ initiated:Bool) -> BioState { - return BioState(updatedText: self.updatedText, updating: self.updating, initiated: initiated) - } - - func withUpdatedText(_ updatedText:String) -> BioState { - return BioState(updatedText: updatedText, updating: self.updating, initiated: self.initiated) - } -} -private func ==(lhs:BioState, rhs: BioState) -> Bool { - return lhs.updatedText == rhs.updatedText && lhs.updating == rhs.updating && lhs.initiated == rhs.initiated -} - -fileprivate func prepareTransition(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], initialSize:NSSize, arguments:BioArguments) -> TableUpdateTransition { - let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in - return entry.entry.item(arguments, initialSize: initialSize) - } - - return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true) -} - -class BioViewController: EditableViewController { - private let disposable = MetaDisposable() - private let stateValue = Atomic(value: BioState()) - private let statePromise = ValuePromise(BioState()) - - override var removeAfterDisapper:Bool { - return true - } - - override func viewDidLoad() { - super.viewDidLoad() - - let previous:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) - let initialSize = atomicSize - - let arguments = BioArguments(account: account, updateText: { [weak self] text in - _ = self?.updateState({$0.withUpdatedText(text).withUpdatedInitiated(true)}) - - }) - - genericView.merge(with: combineLatest(account.viewTracker.peerView( account.peerId) |> deliverOnMainQueue, statePromise.get() |> deliverOnMainQueue, appearanceSignal |> deliverOnMainQueue) |> map { [weak self] view, state, appearance in - - - let userData = view.cachedData as? CachedUserData - let about = userData?.about ?? "" - self?.set(enabled: !state.updating && state.updatedText != about && state.initiated) - if state.updatedText == nil { - _ = self?.stateValue.modify({$0.withUpdatedText(about)}) - } - self?.requestUpdateCenterBar() - let entries = BioEntries(userData, state: state).map{AppearanceWrapperEntry(entry: $0, appearance: appearance)} - return prepareTransition(left: previous.swap(entries), right: entries, initialSize: initialSize.modify{$0}, arguments: arguments) - - } |> deliverOnMainQueue) - readyOnce() - } - - override func requestUpdateCenterBar() { - super.requestUpdateCenterBar() - let length = stateValue.modify({$0}).updatedText?.length ?? 0 - setCenterTitle(defaultBarTitle + " (\(bioLimit - Int32(length)))") - } - - private func updateState(_ f:(BioState)->BioState) -> BioState { - let updatedState = stateValue.modify(f) - statePromise.set(updatedState) - return updatedState - } - override func backKeyAction() -> KeyHandlerResult { - return .invokeNext - } - - override func firstResponder() -> NSResponder? { - if let item = genericView.item(stableId: AnyHashable(Int32(1))) { - if let view = genericView.viewNecessary(at: item.index) as? GeneralInputRowView { - return view.textView.inputView - } - } - return nil - } - - override func returnKeyAction() -> KeyHandlerResult { - changeState() - return .invoked - } - - override func becomeFirstResponder() -> Bool? { - return true - } - - override var normalString: String { - return tr(.bioSave) - } - - override func changeState() { - - let state = updateState { state -> BioState in - return state.withUpdateUpdating(true) - } - - disposable.set(showModalProgress(signal: (updateAbout(account: account, about: state.updatedText) |> deliverOnMainQueue), for: mainWindow).start(error: { [weak self] error in - _ = self?.updateState({$0.withUpdateUpdating(false).withUpdatedInitiated(true)}) - }, completed: { [weak self] in - _ = self?.updateState({$0.withUpdateUpdating(false).withUpdatedInitiated(false)}) - })) - } - -} diff --git a/Telegram-Mac/BlockedPeersViewController.swift b/Telegram-Mac/BlockedPeersViewController.swift index d9a4a15748..bb0b4315bd 100644 --- a/Telegram-Mac/BlockedPeersViewController.swift +++ b/Telegram-Mac/BlockedPeersViewController.swift @@ -1,92 +1,78 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac +import Postbox +import TelegramCore + +import SwiftSignalKit private final class BlockedPeerControllerArguments { - let account: Account + let context: AccountContext let removePeer: (PeerId) -> Void - - init(account: Account, removePeer: @escaping (PeerId) -> Void) { - self.account = account + let openPeer:(PeerId) -> Void + init(context: AccountContext, removePeer: @escaping (PeerId) -> Void, openPeer: @escaping(PeerId)->Void) { + self.context = context self.removePeer = removePeer + self.openPeer = openPeer } } private enum BlockedPeerEntryStableId: Hashable { case peer(PeerId) case empty - case whiteSpace + case sectionId(Int32) var hashValue: Int { switch self { case let .peer(peerId): return peerId.hashValue case .empty: return 0 - case .whiteSpace: + case .sectionId: return 1 } } - - static func ==(lhs: BlockedPeerEntryStableId, rhs: BlockedPeerEntryStableId) -> Bool { - switch lhs { - case let .peer(peerId): - if case .peer(peerId) = rhs { - return true - } else { - return false - } - case .empty: - if case .empty = rhs { - return true - } else { - return false - } - case .whiteSpace: - if case .whiteSpace = rhs { - return true - } else { - return false - } - } - } + } private enum BlockedPeerEntry: Identifiable, Comparable { - case peerItem(Int32, Peer, ShortPeerDeleting?, Bool) + case section(Int32) + case peerItem(Int32, Int32, Peer, ShortPeerDeleting?, Bool, GeneralViewType) case empty(Bool) - case whiteSpace(CGFloat) var stableId: BlockedPeerEntryStableId { switch self { - case let .peerItem(_, peer, _, _): + case let .peerItem(_, _, peer, _, _, _): return .peer(peer.id) case .empty: return .empty - case .whiteSpace: - return .whiteSpace + case let .section(id): + return .sectionId(id) } } static func ==(lhs: BlockedPeerEntry, rhs: BlockedPeerEntry) -> Bool { switch lhs { - case let .peerItem(lhsIndex, lhsPeer, lhsEditing, lhsEnabled): - if case let .peerItem(rhsIndex, rhsPeer, rhsEditing, rhsEnabled) = rhs { + case let .peerItem(lhsSectionId, lhsIndex, lhsPeer, lhsEditing, lhsEnabled, lhsViewType): + if case let .peerItem(rhsSectionId, rhsIndex, rhsPeer, rhsEditing, rhsEnabled, rhsViewType) = rhs { if lhsIndex != rhsIndex { return false } if !lhsPeer.isEqual(rhsPeer) { return false } + if lhsSectionId != rhsSectionId { + return false + } if lhsEditing != rhsEditing { return false } if lhsEnabled != rhsEnabled { return false } + if rhsViewType != lhsViewType { + return false + } return true } else { return false @@ -97,8 +83,8 @@ private enum BlockedPeerEntry: Identifiable, Comparable { } else { return false } - case let .whiteSpace(height): - if case .whiteSpace(height) = rhs { + case let .section(id): + if case .section(id) = rhs { return true } else { return false @@ -106,35 +92,24 @@ private enum BlockedPeerEntry: Identifiable, Comparable { } } - static func <(lhs: BlockedPeerEntry, rhs: BlockedPeerEntry) -> Bool { - switch lhs { - case let .peerItem(index, _, _, _): - switch rhs { - case let .peerItem(rhsIndex, _, _, _): - return index < rhsIndex - case .empty: - return false - case .whiteSpace: - return false - } + var index: Int32 { + switch self { case .empty: - if case .empty = rhs { - return true - } else { - return false - } - case .whiteSpace: - if case .whiteSpace = rhs { - return true - } else { - return false - } + return 0 + case let .peerItem(sectionId, index, _, _, _, _): + return (sectionId * 1000) + index + case let .section(sectionId): + return (sectionId * 1000) + sectionId } } + static func <(lhs: BlockedPeerEntry, rhs: BlockedPeerEntry) -> Bool { + return lhs.index < rhs.index + } + func item(_ arguments: BlockedPeerControllerArguments, initialSize:NSSize) -> TableRowItem { switch self { - case let .peerItem(_, peer, editing, enabled): + case let .peerItem(_, _, peer, editing, enabled, viewType): let interactionType:ShortPeerItemInteractionType if let editing = editing { @@ -146,11 +121,22 @@ private enum BlockedPeerEntry: Identifiable, Comparable { interactionType = .plain } - return ShortPeerRowItem(initialSize, peer: peer, account: arguments.account, stableId: stableId, enabled: enabled, height:44, photoSize: NSMakeSize(32, 32), drawLastSeparator: true, inset: NSEdgeInsets(left: 30, right: 30), interactionType: interactionType, generalType: .none, action: {}) + return ShortPeerRowItem(initialSize, peer: peer, account: arguments.context.account, stableId: stableId, enabled: enabled, height: 46, photoSize: NSMakeSize(32, 32), inset: NSEdgeInsets(left: 30, right: 30), interactionType: interactionType, generalType: .none, viewType: viewType, action: { + arguments.openPeer(peer.id) + }, contextMenuItems: { + if case .plain = interactionType { + return .single([ContextMenuItem(tr(L10n.chatInputUnblock), handler: { + arguments.removePeer(peer.id) + })]) + } else { + return .single([]) + } + + }) case let .empty(progress): - return SearchEmptyRowItem(initialSize, stableId: stableId, isLoading: progress, text: tr(.blockedPeersEmptyDescrpition)) - case let .whiteSpace(height): - return GeneralRowItem(initialSize, height: height, stableId: stableId) + return SearchEmptyRowItem(initialSize, stableId: stableId, isLoading: progress, text: L10n.blockedPeersEmptyDescrpition, viewType: .singleItem) + case .section: + return GeneralRowItem(initialSize, height: 30, stableId: stableId, viewType: .separator) } } } @@ -193,30 +179,33 @@ private struct BlockedPeerControllerState: Equatable { } } -private func blockedPeersControllerEntries(state: BlockedPeerControllerState, peers: [Peer]?) -> [BlockedPeerEntry] { +private func blockedPeersControllerEntries(state: BlockedPeerControllerState, blockedState: BlockedPeersContextState) -> [BlockedPeerEntry] { var entries: [BlockedPeerEntry] = [] - if let peers = peers { - var index: Int32 = 0 - - if !peers.isEmpty { - entries.append(.whiteSpace(16)) - } - - for peer in peers { + var index: Int32 = 0 + var sectionId: Int32 = 0 + if !blockedState.peers.isEmpty { + entries.append(.section(sectionId)) + sectionId += 1 + } + for rendered in blockedState.peers { + if let peer = rendered.peer { var deleting:ShortPeerDeleting? = nil if state.editing { deleting = ShortPeerDeleting(editable: true) } - - entries.append(.peerItem(index, peer, deleting, state.removingPeerId != peer.id)) + + entries.append(.peerItem(sectionId, index, peer, deleting, state.removingPeerId != peer.id, bestGeneralViewType(blockedState.peers, for: rendered))) index += 1 } - } - if entries.isEmpty { - entries.append(.empty(peers == nil)) + + if blockedState.peers.isEmpty { + entries.append(.empty(blockedState.peers.isEmpty)) + } else { + entries.append(.section(sectionId)) + sectionId += 1 } return entries @@ -228,7 +217,7 @@ fileprivate func prepareTransition(left:[AppearanceWrapperEntry { private let disposable:MetaDisposable = MetaDisposable() - - override func viewDidLoad() { super.viewDidLoad() - let account = self.account + + genericView.getBackgroundColor = { + theme.colors.listBackground + } + + let context = self.context let updateState: ((BlockedPeerControllerState) -> BlockedPeerControllerState) -> Void = { [weak self] f in if let strongSelf = self { @@ -253,34 +245,15 @@ class BlockedPeersViewController: EditableViewController { } } - let peersPromise = Promise<[Peer]?>(nil) - - let arguments = BlockedPeerControllerArguments(account: account, removePeer: { [weak self] memberId in - + let arguments = BlockedPeerControllerArguments(context: context, removePeer: { [weak self] memberId in updateState { return $0.withUpdatedRemovingPeerId(memberId) } - - let applyPeers: Signal = peersPromise.get() - |> filter { $0 != nil } - |> take(1) - |> deliverOnMainQueue - |> mapToSignal { peers -> Signal in - if let peers = peers { - var updatedPeers = peers - for i in 0 ..< updatedPeers.count { - if updatedPeers[i].id == memberId { - updatedPeers.remove(at: i) - break - } - } - peersPromise.set(.single(updatedPeers)) - } - - return .complete() - } - - self?.removePeerDisposable.set((requestUpdatePeerIsBlocked(account: account, peerId: memberId, isBlocked: false) |> then(applyPeers) |> deliverOnMainQueue).start(error: { _ in + self?.removePeerDisposable.set((context.blockedPeersContext.remove(peerId: memberId) |> deliverOnMainQueue).start(error: { error in + switch error { + case .generic: + alert(for: context.window, info: L10n.unknownError) + } updateState { return $0.withUpdatedRemovingPeerId(nil) } @@ -288,25 +261,24 @@ class BlockedPeersViewController: EditableViewController { updateState { return $0.withUpdatedRemovingPeerId(nil) } - })) + }, openPeer: { [weak self] peerId in + guard let `self` = self else {return} + self.navigationController?.push(PeerInfoController(context: self.context, peerId: peerId)) }) - let peersSignal: Signal<[Peer]?, NoError> = .single(nil) |> then(requestBlockedPeers(account: account) |> map { Optional($0) }) - - peersPromise.set(peersSignal) let initialSize = atomicSize let previousEntries:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) - let signal = combineLatest(statePromise.get(), peersPromise.get(), appearanceSignal) + let signal = combineLatest(statePromise.get(), context.blockedPeersContext.state, appearanceSignal) |> deliverOnMainQueue - |> map { state, peers, appearance -> TableUpdateTransition in - let entries = blockedPeersControllerEntries(state: state, peers: peers).map{AppearanceWrapperEntry(entry: $0, appearance: appearance)} + |> map { state, blockedState, appearance -> TableUpdateTransition in + let entries = blockedPeersControllerEntries(state: state, blockedState: blockedState).map{AppearanceWrapperEntry(entry: $0, appearance: appearance)} return prepareTransition(left: previousEntries.swap(entries), right: entries, initialSize: initialSize.modify{$0}, arguments: arguments) - } + } disposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] transition in if let strongSelf = self { @@ -315,6 +287,15 @@ class BlockedPeersViewController: EditableViewController { strongSelf.rightBarView.isHidden = strongSelf.genericView.item(at: 0) is SearchEmptyRowItem } })) + + genericView.setScrollHandler { position in + switch position.direction { + case .bottom: + context.blockedPeersContext.loadMore() + default: + break + } + } } deinit { diff --git a/Telegram-Mac/CGChatListIndicator.swift b/Telegram-Mac/CGChatListIndicator.swift new file mode 100644 index 0000000000..0b9bf8b822 --- /dev/null +++ b/Telegram-Mac/CGChatListIndicator.swift @@ -0,0 +1,138 @@ +// +// CGChatListIndicator.swift +// Telegram +// +// Created by Mikhail Filimonov on 09.12.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit + +final class GCChatListIndicator : View { + + var color: NSColor = NSColor.white { + didSet { + CATransaction.begin() + CATransaction.setDisableActions(true) + (self.layer as? CAShapeLayer)?.fillColor = color.cgColor + CATransaction.commit() + } + } + + init(color: NSColor) { + self.color = color + super.init(frame: NSMakeRect(0, 0, 10, 20)) + self.layer = CAShapeLayer() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + private var animator: DisplayLinkAnimator? + + private let keyDispose = MetaDisposable() + + deinit { + keyDispose.dispose() + } + + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + + if window == nil { + stopAnimation() + keyDispose.set(nil) + } else { + startAnimation() + if let window = window as? Window { + keyDispose.set(window.visibility.start(next: { [weak self] value in + if value { + self?.startAnimation() + } else { + self?.stopAnimation() + } + })) + } + } + } + + + func startAnimation() { + self.animator = DisplayLinkAnimator(duration: 0.4, from: innerProgress, to: 1, update: { [weak self] value in + self?.innerProgress = value + }, completion: { [weak self] in + self?.startAnimation() + }) + + } + func stopAnimation() { + self.animator = nil + } + + private var progressStage: Int = 0 + private var innerProgress: CGFloat = 0 { + didSet { + if (innerProgress >= 1.0) { + progressStage += 1 + if (progressStage >= 8) { + progressStage = 0 + } + innerProgress = 0 + } + if visibleRect != .zero { + (self.layer as? CAShapeLayer)?.path = genPath() + } + } + } + + private func genPath() -> CGPath { + let rect = self.bounds + + var size1: CGFloat = 0; + var size2: CGFloat = 0; + if (progressStage == 0) { + size1 = 2 + 8 * innerProgress + size2 = 6 - 4 * innerProgress + } else if (progressStage == 1) { + size1 = 10 - 8 * innerProgress + size2 = 2 + 8 * innerProgress + } else if (progressStage == 2) { + size1 = 2 + 4 * innerProgress + size2 = 10 - 8 * innerProgress + } else if (progressStage == 3) { + size1 = 6 - 4 * innerProgress + size2 = 2 + 4 * innerProgress + } else if (progressStage == 4) { + size1 = 2 + 8 * innerProgress + size2 = 6 - 4 * innerProgress + } else if (progressStage == 5) { + size1 = 10 - 8 * innerProgress + size2 = 2 + 8 * innerProgress + } else if (progressStage == 6) { + size1 = 2 + 8 * innerProgress + size2 = 10 - 8 * innerProgress + } else { + size1 = 10 - 8 * innerProgress + size2 = 2 + 4 * innerProgress + } + + + let p1 = CGMutablePath() + + p1.addRoundedRect(in: .init(origin: NSMakePoint(0, rect.midY - size2 / 2), size: NSMakeSize(2, size2)), cornerWidth: 1, cornerHeight: 1) + p1.addRoundedRect(in: .init(origin: NSMakePoint(4, rect.midY - size1 / 2), size: NSMakeSize(2, size1)), cornerWidth: 1, cornerHeight: 1) + p1.addRoundedRect(in: .init(origin: NSMakePoint(8, rect.midY - size2 / 2), size: NSMakeSize(2, size2)), cornerWidth: 1, cornerHeight: 1) + + return p1 + } + + +} + diff --git a/Telegram-Mac/CachedAdminIds.swift b/Telegram-Mac/CachedAdminIds.swift deleted file mode 100644 index d5d380cd9b..0000000000 --- a/Telegram-Mac/CachedAdminIds.swift +++ /dev/null @@ -1,104 +0,0 @@ -// -// CachedAdminIds.swift -// Telegram -// -// Created by keepcoder on 11/10/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - -import Cocoa -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac - -private final class CachedAdminIdsContext { - var hash:Int32 = 0 - var ids:[PeerId] = [] - let subscribers = Bag<([PeerId]) -> Void>() -} - -private func hashForIdsReverse(_ ids: [Int32]) -> Int32 { - var acc: UInt32 = 0 - - for id in ids { - let low = UInt32(UInt32(bitPattern: id) & (0xffffffff as UInt32)) - let high = UInt32((UInt32(bitPattern: id) >> 32) & (0xffffffff as UInt32)) - - acc = (acc &* 20261) &+ high - acc = (acc &* 20261) &+ low - } - return Int32(bitPattern: acc & UInt32(0x7FFFFFFF)) -} - -class CachedAdminIds: NSObject { - private let statusQueue = Queue() - - - private var idsContexts: [PeerId: CachedAdminIdsContext] = [:] - - private var disposableTokens:[PeerId: Disposable] = [:] - func ids(postbox: Postbox, network:Network, peerId:PeerId) -> Signal<[PeerId], Void> { - if peerId.namespace != Namespaces.Peer.CloudChannel { - return .single([]) - } - return Signal { subscriber in - let disposable = MetaDisposable() - self.statusQueue.async { - - let idsContexts: CachedAdminIdsContext - if let current = self.idsContexts[peerId] { - idsContexts = current - } else { - idsContexts = CachedAdminIdsContext() - self.idsContexts[peerId] = idsContexts - } - - let index = idsContexts.subscribers.add { ids in - subscriber.putNext(ids) - } - - subscriber.putNext(idsContexts.ids) - - - self.disposableTokens[peerId]?.dispose() - - - let signal = channelAdminIds(postbox: postbox, network: network, peerId: peerId, hash: idsContexts.hash) |> deliverOn(self.statusQueue) |> then( deferred { - return channelAdminIds(postbox: postbox, network: network, peerId: peerId, hash: idsContexts.hash) |> delay(60, queue: self.statusQueue) - } |> restart) - - self.disposableTokens[peerId] = signal.start(next: { ids in - idsContexts.ids = ids - idsContexts.hash = hashForIdsReverse(ids.map({$0.id})) - for subscriber in idsContexts.subscribers.copyItems() { - subscriber(idsContexts.ids) - } - }) - - disposable.set(ActionDisposable { - self.statusQueue.async { - if let current = self.idsContexts[peerId] { - current.subscribers.remove(index) - } - } - }) - } - - return disposable - } - - } - - - - func remove(for peerId:PeerId) { - disposableTokens[peerId]?.dispose() - } - - deinit { - for (_, value) in disposableTokens { - value.dispose() - } - } - -} diff --git a/Telegram-Mac/CachedChannelAdmins.swift b/Telegram-Mac/CachedChannelAdmins.swift new file mode 100644 index 0000000000..eb124ef732 --- /dev/null +++ b/Telegram-Mac/CachedChannelAdmins.swift @@ -0,0 +1,101 @@ +// +// CachedChannelAdmins.swift +// Telegram +// +// Created by Mikhail Filimonov on 02/01/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Foundation +import Postbox +import TelegramCore + +import SwiftSignalKit + + +struct CachedChannelAdminRank : PostboxCoding, Equatable { + + + let peerId: PeerId + let type: CachedChannelAdminRankType + + init(peerId: PeerId, type: CachedChannelAdminRankType) { + self.peerId = peerId + self.type = type + } + + init(decoder: PostboxDecoder) { + self.peerId = PeerId(decoder.decodeInt64ForKey("peerId", orElse: 0)) + self.type = decoder.decodeObjectForKey("type", decoder: { CachedChannelAdminRankType(decoder: $0) }) as! CachedChannelAdminRankType + } + func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt64(self.peerId.toInt64(), forKey: "peerId") + encoder.encodeObject(self.type, forKey: "type") + } +} + +enum CachedChannelAdminRankType: PostboxCoding, Equatable { + case owner + case admin + case custom(String) + + init(decoder: PostboxDecoder) { + let value: Int32 = decoder.decodeInt32ForKey("v", orElse: 0) + switch value { + case 0: + self = .owner + case 1: + self = .admin + case 2: + self = .custom(decoder.decodeStringForKey("s", orElse: "")) + default: + self = .admin + } + } + + func encode(_ encoder: PostboxEncoder) { + switch self { + case .owner: + encoder.encodeInt32(0, forKey: "v") + case .admin: + encoder.encodeInt32(1, forKey: "v") + case let .custom(rank): + encoder.encodeInt32(2, forKey: "v") + encoder.encodeString(rank, forKey: "s") + } + } +} + +final class CachedChannelAdminRanks: PostboxCoding { + let ranks: [CachedChannelAdminRank] + + init(ranks: [CachedChannelAdminRank]) { + self.ranks = ranks + } + + init(decoder: PostboxDecoder) { + self.ranks = decoder.decodeObjectArrayForKey("ranks1").compactMap { $0 as? CachedChannelAdminRank } + } + + func encode(_ encoder: PostboxEncoder) { + encoder.encodeObjectArray(self.ranks, forKey: "ranks1") + } + + static func cacheKey(peerId: PeerId) -> ValueBoxKey { + let key = ValueBoxKey(length: 8) + key.setInt64(0, value: peerId.toInt64()) + return key + } +} + +private let collectionSpec = ItemCacheCollectionSpec(lowWaterItemCount: 100, highWaterItemCount: 200) + +func cachedChannelAdminRanksEntryId(peerId: PeerId) -> ItemCacheEntryId { + return ItemCacheEntryId(collectionId: 100, key: CachedChannelAdminRanks.cacheKey(peerId: peerId)) +} + +func updateCachedChannelAdminRanks(postbox: Postbox, peerId: PeerId, ranks: Dictionary) -> Signal { + return postbox.transaction { transaction -> Void in + transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: 100, key: CachedChannelAdminRanks.cacheKey(peerId: peerId)), entry: CachedChannelAdminRanks(ranks: ranks.map { CachedChannelAdminRank(peerId: $0.key, type: $0.value)}), collectionSpec: collectionSpec) + } +} diff --git a/Telegram-Mac/CachedFaqInstantPage.swift b/Telegram-Mac/CachedFaqInstantPage.swift new file mode 100644 index 0000000000..da203bad41 --- /dev/null +++ b/Telegram-Mac/CachedFaqInstantPage.swift @@ -0,0 +1,135 @@ +// +// CachedFaqInstantPage.swift +// Telegram +// +// Created by Mikhail Filimonov on 02.12.2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import SwiftSignalKit +import Postbox +import TelegramCore + + + +private func extractAnchor(string: String) -> (String, String?) { + var anchorValue: String? + if let anchorRange = string.range(of: "#") { + let anchor = string[anchorRange.upperBound...] + if !anchor.isEmpty { + anchorValue = String(anchor) + } + } + var trimmedUrl = string + if let anchor = anchorValue, let anchorRange = string.range(of: "#\(anchor)") { + let url = string[.. Signal { + let faqUrl = "https://telegram.org/faq#general-questions" + + + let (cachedUrl, anchor) = extractAnchor(string: faqUrl) + + return cachedInstantPage(postbox: context.account.postbox, url: cachedUrl) + |> mapToSignal { cachedInstantPage -> Signal in + let updated = resolveInstantViewUrl(account: context.account, url: faqUrl) + |> afterNext { result in + if case let .instantView(_, webPage, _) = result, case let .Loaded(content) = webPage.content, let instantPage = content.instantPage { + if instantPage.isComplete { + let _ = updateCachedInstantPage(postbox: context.account.postbox, url: cachedUrl, webPage: webPage).start() + } else { + let _ = (actualizedWebpage(postbox: context.account.postbox, network: context.account.network, webpage: webPage) + |> mapToSignal { webPage -> Signal in + if case let .Loaded(content) = webPage.content, let instantPage = content.instantPage, instantPage.isComplete { + return updateCachedInstantPage(postbox: context.account.postbox, url: cachedUrl, webPage: webPage) + } else { + return .complete() + } + }).start() + } + } + } + + let now = Int32(CFAbsoluteTimeGetCurrent()) + if let cachedInstantPage = cachedInstantPage, case let .Loaded(content) = cachedInstantPage.webPage.content, let instantPage = content.instantPage, instantPage.isComplete { + let current: Signal = .single(.instantView(link: faqUrl, webpage: cachedInstantPage.webPage, anchor: anchor)) + if now > cachedInstantPage.timestamp + refreshTimeout { + return current + |> then(updated) + } else { + return current + } + } else { + return updated + } + } +} + +func faqSearchableItems(context: AccountContext) -> Signal<[SettingsSearchableItem], NoError> { + return cachedFaqInstantPage(context: context) + |> map { resolvedUrl -> [SettingsSearchableItem] in + var results: [SettingsSearchableItem] = [] + var nextIndex: Int32 = 2 + if case let .instantView(_, webPage, _) = resolvedUrl { + if case let .Loaded(content) = webPage.content, let instantPage = content.instantPage { + var processingQuestions = false + var currentSection: String? + outer: for block in instantPage.blocks { + if !processingQuestions { + switch block { + case .blockQuote: + if results.isEmpty { + processingQuestions = true + } + default: + break + } + } else { + switch block { + case let .paragraph(text): + if case .bold = text { + currentSection = text.plainText + } else if case .concat = text { + processingQuestions = false + } + case let .list(items, false): + if let currentSection = currentSection { + for item in items { + if case let .text(itemText, _) = item, case let .url(text, url, _) = itemText { + let (_, anchor) = extractAnchor(string: url) + var index = nextIndex + if anchor?.contains("delete-my-account") ?? false { + index = 1 + } else { + nextIndex += 1 + } + let item = SettingsSearchableItem(id: .faq(index), title: text.plainText, alternate: [], icon: .faq, breadcrumbs: [L10n.accountSettingsFAQ, currentSection], present: { context, _, present in + showInstantPage(InstantPageViewController(context, webPage: webPage, message: nil, anchor: anchor)) + }) + if index == 1 { + results.insert(item, at: 0) + } else { + results.append(item) + } + } + } + } + default: + break + } + } + } + } + } + return results + } +} diff --git a/Telegram-Mac/CachedInstantPages.swift b/Telegram-Mac/CachedInstantPages.swift new file mode 100644 index 0000000000..d8636bbcf4 --- /dev/null +++ b/Telegram-Mac/CachedInstantPages.swift @@ -0,0 +1,61 @@ +// +// CachedInstantPages.swift +// Telegram +// +// Created by Mikhail Filimonov on 02.12.2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import SwiftSignalKit +import Postbox +import TelegramCore + + + +public final class CachedInstantPage: PostboxCoding { + public let webPage: TelegramMediaWebpage + public let timestamp: Int32 + + public init(webPage: TelegramMediaWebpage, timestamp: Int32) { + self.webPage = webPage + self.timestamp = timestamp + } + + public init(decoder: PostboxDecoder) { + self.webPage = decoder.decodeObjectForKey("webpage", decoder: { TelegramMediaWebpage(decoder: $0) }) as! TelegramMediaWebpage + self.timestamp = decoder.decodeInt32ForKey("timestamp", orElse: 0) + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeObject(self.webPage, forKey: "webpage") + encoder.encodeInt32(self.timestamp, forKey: "timestamp") + } +} + +public func cachedInstantPage(postbox: Postbox, url: String) -> Signal { + return postbox.transaction { transaction -> CachedInstantPage? in + let key = ValueBoxKey(length: 8) + key.setInt64(0, value: Int64(bitPattern: url.persistentHashValue)) + if let entry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: ApplicationSpecificItemCacheCollectionId.cachedInstantPages, key: key)) as? CachedInstantPage { + return entry + } else { + return nil + } + } +} + +private let collectionSpec = ItemCacheCollectionSpec(lowWaterItemCount: 5, highWaterItemCount: 10) + +public func updateCachedInstantPage(postbox: Postbox, url: String, webPage: TelegramMediaWebpage?) -> Signal { + return postbox.transaction { transaction -> Void in + let key = ValueBoxKey(length: 8) + key.setInt64(0, value: Int64(bitPattern: url.persistentHashValue)) + let id = ItemCacheEntryId(collectionId: ApplicationSpecificItemCacheCollectionId.cachedInstantPages, key: key) + if let webPage = webPage { + transaction.putItemCacheEntry(id: id, entry: CachedInstantPage(webPage: webPage, timestamp: Int32(CFAbsoluteTimeGetCurrent())), collectionSpec: collectionSpec) + } else { + transaction.removeItemCacheEntry(id: id) + } + } +} diff --git a/Telegram-Mac/CachedResourceRepresentations.swift b/Telegram-Mac/CachedResourceRepresentations.swift index 4812c4fc09..48c4f85d75 100644 --- a/Telegram-Mac/CachedResourceRepresentations.swift +++ b/Telegram-Mac/CachedResourceRepresentations.swift @@ -8,17 +8,19 @@ import Cocoa -import PostboxMac -import SwiftSignalKitMac +import Postbox +import SwiftSignalKit +import TelegramCore + final class CachedStickerAJpegRepresentation: CachedMediaResourceRepresentation { let size: CGSize? - + var keepDuration: CachedMediaRepresentationKeepDuration = .general var uniqueId: String { if let size = self.size { - return "sticker-ajpeg-\(Int(size.width))x\(Int(size.height))" + return "sticker-v1-png-\(Int(size.width))x\(Int(size.height))" } else { - return "sticker-ajpeg" + return "sticker-v1-png" } } @@ -35,9 +37,9 @@ final class CachedStickerAJpegRepresentation: CachedMediaResourceRepresentation } } -final class CachedScaledImageRepresentation: CachedMediaResourceRepresentation { +class CachedScaledImageRepresentation: CachedMediaResourceRepresentation { let size: CGSize - + var keepDuration: CachedMediaRepresentationKeepDuration = .general var uniqueId: String { return "scaled-image-\(Int(self.size.width))x\(Int(self.size.height))" } @@ -55,7 +57,11 @@ final class CachedScaledImageRepresentation: CachedMediaResourceRepresentation { } } + + final class CachedVideoFirstFrameRepresentation: CachedMediaResourceRepresentation { + var keepDuration: CachedMediaRepresentationKeepDuration = .general + var uniqueId: String { return "first-frame" } @@ -68,3 +74,178 @@ final class CachedVideoFirstFrameRepresentation: CachedMediaResourceRepresentati } } } + +final class CachedScaledVideoFirstFrameRepresentation: CachedMediaResourceRepresentation { + let size: CGSize + var keepDuration: CachedMediaRepresentationKeepDuration = .general + var uniqueId: String { + return "scaled-frame-\(Int(self.size.width))x\(Int(self.size.height))" + } + + init(size: CGSize) { + self.size = size + } + + func isEqual(to: CachedMediaResourceRepresentation) -> Bool { + if let to = to as? CachedScaledVideoFirstFrameRepresentation { + return self.size == to.size + } else { + return false + } + } +} +final class CachedBlurredWallpaperRepresentation: CachedMediaResourceRepresentation { + var keepDuration: CachedMediaRepresentationKeepDuration = .general + var uniqueId: String { + return CachedBlurredWallpaperRepresentation.uniqueId + } + + static var uniqueId: String { + return "blurred-wallpaper" + } + + func isEqual(to: CachedMediaResourceRepresentation) -> Bool { + if to is CachedBlurredWallpaperRepresentation { + return true + } else { + return false + } + } +} + + +final class CachedAnimatedStickerRepresentation: CachedMediaResourceRepresentation { + var keepDuration: CachedMediaRepresentationKeepDuration = .general + var uniqueId: String { + let version: Int = 1 + if let fitzModifier = self.fitzModifier { + return "animated-sticker-v\(version)-\(self.thumb ? 1 : 0)-w:\(size.width)-h:\(size.height)-fitz\(fitzModifier.rawValue)" + } else { + return "animated-sticker-v\(version)-\(self.thumb ? 1 : 0)-w:\(size.width)-h:\(size.height)" + } + } + let thumb: Bool + let size: NSSize + let fitzModifier: EmojiFitzModifier? + init(thumb: Bool, size: NSSize, fitzModifier: EmojiFitzModifier? = nil) { + self.thumb = thumb + self.size = size + self.fitzModifier = fitzModifier + } + + func isEqual(to: CachedMediaResourceRepresentation) -> Bool { + if let to = to as? CachedAnimatedStickerRepresentation { + return self.thumb == to.thumb && self.size == to.size && self.fitzModifier == to.fitzModifier + } else { + return false + } + } +} + +final class CachedPatternWallpaperMaskRepresentation: CachedMediaResourceRepresentation { + let keepDuration: CachedMediaRepresentationKeepDuration = .general + + let size: CGSize? + let settings: WallpaperSettings? + var uniqueId: String { + + var color:String = "" + + if let settings = settings { + color += settings.stringValue + } + + if let size = self.size { + return "pattern-wallpaper-mask--------\(Int(size.width))x\(Int(size.height))" + color + } else { + return "pattern-wallpaper-mask--------" + color + } + } + + init(size: CGSize? = nil, settings: WallpaperSettings? = nil) { + self.size = size + self.settings = settings + } + + func isEqual(to: CachedMediaResourceRepresentation) -> Bool { + if let to = to as? CachedPatternWallpaperMaskRepresentation { + return self.size == to.size && self.settings == to.settings + } else { + return false + } + } +} + + +final class CachedDiceRepresentation: CachedMediaResourceRepresentation { + let keepDuration: CachedMediaRepresentationKeepDuration = .general + let emoji: String + let value: String + let size: NSSize + var uniqueId: String { + return emoji + value + ":dice2" + } + + init(emoji: String, value: String, size: NSSize) { + self.value = value + self.size = size + self.emoji = emoji + } + + func isEqual(to: CachedMediaResourceRepresentation) -> Bool { + if let to = to as? CachedDiceRepresentation { + return self.value == to.value && self.size == to.size && self.emoji == to.emoji + } else { + return false + } + } +} + +final class CachedSlotMachineRepresentation: CachedMediaResourceRepresentation { + let keepDuration: CachedMediaRepresentationKeepDuration = .general + let value: SlotMachineValue + let size: NSSize + var uniqueId: String { + return "l: \(value.left.hashValue)" + ", c: \(value.center.hashValue)" + ", c: \(value.right.hashValue)" + " :slot1" + } + + init(value: SlotMachineValue, size: NSSize) { + self.value = value + self.size = size + } + + func isEqual(to: CachedMediaResourceRepresentation) -> Bool { + if let to = to as? CachedSlotMachineRepresentation { + return self.value == to.value && self.size == to.size + } else { + return false + } + } +} + + + +public enum EmojiFitzModifier: Int32, Equatable { + case type12 + case type3 + case type4 + case type5 + case type6 + + public init?(emoji: String) { + switch emoji.unicodeScalars.first?.value { + case 0x1f3fb: + self = .type12 + case 0x1f3fc: + self = .type3 + case 0x1f3fd: + self = .type4 + case 0x1f3fe: + self = .type5 + case 0x1f3ff: + self = .type6 + default: + return nil + } + } +} diff --git a/Telegram-Mac/CalendarController.swift b/Telegram-Mac/CalendarController.swift index b537ebc5b0..da9906b4d3 100644 --- a/Telegram-Mac/CalendarController.swift +++ b/Telegram-Mac/CalendarController.swift @@ -11,39 +11,40 @@ import TGUIKit class CalendarControllerView : View { +} + +private final class CalendarNavigation : NavigationViewController { + + + } class CalendarController: GenericViewController { - private var navigation:NavigationViewController! + private var navigation:CalendarNavigation! private var interactions:CalendarMonthInteractions! + private let onlyFuture: Bool + private let current: Date + private let limitedBy: Date? override func viewDidLoad() { super.viewDidLoad() addSubview(navigation.view) readyOnce() } - override init(frame frameRect: NSRect) { - super.init(frame: frameRect) - bar = .init(height: 0) - } - override init() { - super.init() - bar = .init(height: 0) - } - + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + self.navigation.viewDidAppear(animated) - - self.window?.set(handler: { [weak self] () -> KeyHandlerResult in + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in if let current = self?.navigation.controller as? CalendarMonthController, current.isPrevEnabled, let backAction = self?.interactions.backAction { backAction(current.month.month) } return .invoked }, with: self, for: .LeftArrow, priority: .modal) - self.window?.set(handler: { [weak self] () -> KeyHandlerResult in + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in if let current = self?.navigation.controller as? CalendarMonthController, current.isNextEnabled, let nextAction = self?.interactions.nextAction { nextAction(current.month.month) } @@ -53,11 +54,24 @@ class CalendarController: GenericViewController { override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + self.navigation.viewWillDisappear(animated) self.window?.remove(object: self, for: .LeftArrow) self.window?.remove(object: self, for: .RightArrow) } - init(_ frameRect:NSRect, selectHandler:@escaping (Date)->Void) { + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + self.navigation.viewDidDisappear(animated) + } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + self.navigation.viewWillAppear(animated) + } + + init(_ frameRect:NSRect, _ window: Window, current: Date = Date(), onlyFuture: Bool = false, limitedBy: Date? = nil, selectHandler:@escaping (Date)->Void) { + self.onlyFuture = onlyFuture + self.current = current + self.limitedBy = limitedBy super.init(frame: frameRect) bar = .init(height: 0) self.interactions = CalendarMonthInteractions(selectAction: { [weak self] (selected) in @@ -71,14 +85,23 @@ class CalendarController: GenericViewController { if let strongSelf = self { strongSelf.navigation.push(strongSelf.stepMonth(date: CalendarUtils.stepMonth(1, date: date)), style: .push) } + }, changeYear: { [weak self] year, date in + if let strongSelf = self { + strongSelf.navigation.push(strongSelf.stepMonth(date: CalendarUtils.year(Int(year), date: date)), style: .push) + } }) - self.navigation = NavigationViewController(stepMonth(date: Date())) + self.navigation = CalendarNavigation(stepMonth(date: current), window) self.navigation._frameRect = frameRect + } func stepMonth(date:Date) -> CalendarMonthController { - return CalendarMonthController(date, interactions: interactions) + return CalendarMonthController(date, onlyFuture: self.onlyFuture, limitedBy: limitedBy, selectDayAnyway: CalendarUtils.isSameDate(current, date: date, checkDay: false), interactions: interactions) + } + + override var isAutoclosePopover: Bool { + return false } } diff --git a/Telegram-Mac/CalendarMonthController.swift b/Telegram-Mac/CalendarMonthController.swift index fcc7215d73..4a3771b4f8 100644 --- a/Telegram-Mac/CalendarMonthController.swift +++ b/Telegram-Mac/CalendarMonthController.swift @@ -13,14 +13,16 @@ struct CalendarMonthInteractions { let selectAction:(Date)->Void let backAction:((Date)->Void)? let nextAction:((Date)->Void)? - init(selectAction:@escaping (Date)->Void, backAction:((Date)->Void)? = nil, nextAction:((Date)->Void)? = nil) { + let changeYear: (Int32, Date)->Void + init(selectAction:@escaping (Date)->Void, backAction:((Date)->Void)? = nil, nextAction:((Date)->Void)? = nil, changeYear: @escaping(Int32, Date)->Void) { self.selectAction = selectAction self.backAction = backAction self.nextAction = nextAction + self.changeYear = changeYear } } -struct CalendarMonthStruct { +final class CalendarMonthStruct { let month:Date let prevMonth:Date let nextMonth:Date @@ -30,12 +32,16 @@ struct CalendarMonthStruct { let lastDayOfNextMonth:Int let currentStartDay:Int - let selectedDay:Int? + var selectedDay:Int? let components:DateComponents let dayHandler:(Int)->Void - init(month:Date, dayHandler:@escaping (Int)->Void) { + let onlyFuture: Bool + let limitedBy: Date? + init(month:Date, selectDayAnyway: Bool, onlyFuture: Bool, limitedBy: Date?, dayHandler:@escaping (Int)->Void) { self.month = month + self.onlyFuture = onlyFuture + self.limitedBy = limitedBy self.dayHandler = dayHandler self.prevMonth = CalendarUtils.stepMonth(-1, date: month) self.nextMonth = CalendarUtils.stepMonth(1, date: month) @@ -44,11 +50,11 @@ struct CalendarMonthStruct { self.lastDayOfNextMonth = CalendarUtils.lastDay(ofTheMonth: month) var calendar = NSCalendar.current - calendar.timeZone = TimeZone(abbreviation: "UTC")! +// calendar.timeZone = TimeZone(abbreviation: "UTC")! let components = calendar.dateComponents([.year, .month, .day], from: month) self.currentStartDay = CalendarUtils.weekDay(Date(timeIntervalSince1970: month.timeIntervalSince1970 - TimeInterval(components.day! * 24*60*60))) - if CalendarUtils.isSameDate(month, date: Date(), checkDay: false) { + if selectDayAnyway { selectedDay = components.day! } else { selectedDay = nil @@ -77,41 +83,75 @@ class CalendarMonthView : View { let day = TitleButton() day.set(font: .normal(.text), for: .Normal) day.set(background: theme.colors.background, for: .Normal) + let current:Int + if i + 1 < month.currentStartDay { current = (month.lastDayOfPrevMonth - month.currentStartDay) + i + 2 - day.set(color: .grayText, for: .Normal) + day.set(color: theme.colors.grayText, for: .Normal) } else if (i + 2) - month.currentStartDay > month.lastDayOfMonth { current = (i + 2) - (month.currentStartDay + month.lastDayOfMonth) - day.set(color: .grayText, for: .Normal) + day.set(color: theme.colors.grayText, for: .Normal) } else { current = (i + 1) - month.currentStartDay + 1 - day.set(color: .white, for: .Highlight) - - if (i + 1) % 7 == 0 || (i + 2) % 7 == 0 { - day.set(color: theme.colors.redUI, for: .Normal) - } else { - day.set(color: theme.colors.text, for: .Normal) - } + var skipDay: Bool = false - day.layer?.cornerRadius = .cornerRadius + var calendar = NSCalendar.current +// calendar.timeZone = TimeZone(abbreviation: "UTC")! + let components = calendar.dateComponents([.day, .year, .month], from: Date()) - if let selectedDay = month.selectedDay, current == selectedDay { - day.isSelected = true - day.set(background: theme.colors.blueSelect, for: .Highlight) - day.apply(state: .Highlight) - } else { - day.set(background: theme.colors.blueUI, for: .Highlight) + if month.onlyFuture, CalendarUtils.isSameDate(month.month, date: Date(), checkDay: false) { + if current < components.day! { + skipDay = true + } + } else if month.onlyFuture, components.year! + 1 == month.components.year! && components.month! == month.components.month! { + if current > components.day! { + skipDay = true + } + } else if CalendarUtils.isSameDate(month.month, date: Date(), checkDay: false), current > components.day! { + skipDay = true } - day.set(handler: { (control) in + if let limitedBy = month.limitedBy { + let limited = calendar.dateComponents([.year, .month, .day], from: limitedBy) + if limited.year! < month.components.year! || limited.month! < month.components.month! || limited.day! < current { + skipDay = true + } - month.dayHandler(current) + } + if !skipDay { + day.set(color: theme.colors.underSelectedColor, for: .Highlight) + + if (i + 1) % 7 == 0 || (i + 2) % 7 == 0 { + day.set(color: theme.colors.redUI, for: .Normal) + } else { + day.set(color: theme.colors.text, for: .Normal) + } - }, for: .Click) - + day.layer?.cornerRadius = .cornerRadius + + if let selectedDay = month.selectedDay, current == selectedDay { + // day.isSelected = true + day.set(color: theme.colors.underSelectedColor, for: .Normal) + + day.set(background: theme.colors.accent, for: .Normal) + day.set(background: theme.colors.accent, for: .Highlight) + } else { + day.set(background: theme.colors.background, for: .Normal) + day.set(background: theme.colors.accent, for: .Highlight) + } + + day.set(handler: { [weak self] (control) in + month.selectedDay = current + month.dayHandler(current) + self?.layout(for: month) + + }, for: .Click) + } else { + day.set(color: theme.colors.grayText, for: .Normal) + } } day.set(text: "\(current)", for: .Normal) @@ -123,7 +163,7 @@ class CalendarMonthView : View { override func layout() { super.layout() - let oneSize:NSSize = NSMakeSize(floorToScreenPixels(frame.width / 7), floorToScreenPixels(frame.height / 6)) + let oneSize:NSSize = NSMakeSize(floorToScreenPixels(backingScaleFactor, frame.width / 7), floorToScreenPixels(backingScaleFactor, frame.height / 6)) var inset:NSPoint = NSMakePoint(0, 0) for i in 0 ..< subviews.count { subviews[i].frame = NSMakeRect(inset.x, inset.y, oneSize.width, oneSize.height) @@ -147,8 +187,12 @@ class CalendarMonthView : View { class CalendarMonthController: GenericViewController { let interactions:CalendarMonthInteractions let month:CalendarMonthStruct - init(_ month:Date, interactions:CalendarMonthInteractions) { - self.month = CalendarMonthStruct(month: month, dayHandler: { day in + let onlyFuture: Bool + let limitedBy: Date? + init(_ month:Date, onlyFuture: Bool, limitedBy: Date?, selectDayAnyway: Bool, interactions:CalendarMonthInteractions) { + self.onlyFuture = onlyFuture + self.limitedBy = limitedBy + self.month = CalendarMonthStruct(month: month, selectDayAnyway: selectDayAnyway, onlyFuture: self.onlyFuture, limitedBy: self.limitedBy, dayHandler: { day in interactions.selectAction(CalendarUtils.monthDay(day, date: month)) }) self.interactions = interactions @@ -159,20 +203,68 @@ class CalendarMonthController: GenericViewController { override func getCenterBarViewOnce() -> TitledBarView { let formatter:DateFormatter = DateFormatter() - formatter.locale = Locale(identifier: "en_US") + formatter.locale = Locale(identifier: appAppearance.language.languageCode) formatter.dateFormat = "MMMM" let monthString:String = formatter.string(from: month.month) formatter.dateFormat = "yyyy" let yearString:String = formatter.string(from: month.month) - return TitledBarView(controller: self, .initialize(string: monthString, color: theme.colors.text, font:.medium(.text)), .initialize(string:yearString, color: theme.colors.grayText, font:.normal(.small))) + let barView = TitledBarView(controller: self, .initialize(string: monthString, color: theme.colors.text, font:.medium(.text)), .initialize(string:yearString, color: theme.colors.grayText, font:.normal(.small))) + + barView.set(handler: { [weak self] control in + + guard let `self` = self else { + return + } + + let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + + var now: time_t = time_t(nowTimestamp) + var timeinfoNow: tm = tm() + localtime_r(&now, &timeinfoNow) + + var items:[SPopoverItem] = [] + + for i in stride(from: 1900 + timeinfoNow.tm_year - 1, to: 2012, by: -1) { + items.append(.init("\(i)", { [weak self] in + guard let `self` = self else { + return + } + self.interactions.changeYear(i, self.month.month) + })) + } + if !items.isEmpty && !self.onlyFuture { + showPopover(for: control, with: SPopoverViewController(items: items), edge: .maxY, inset: NSMakePoint(30, -50)) + } + + }, for: .Click) + + return barView } var isNextEnabled:Bool { + if self.onlyFuture { + + var calendar = NSCalendar.current + +// calendar.timeZone = TimeZone(abbreviation: "UTC")! + let components = calendar.dateComponents([.year, .month, .day], from: Date()) + + + if month.components.year! == components.year! { + return true + } else if components.year! + 1 == month.components.year! { + return month.components.month! < components.month! + } + return true + } return !CalendarUtils.isSameDate(month.month, date: Date(), checkDay: false) } var isPrevEnabled:Bool { + if self.onlyFuture { + return !CalendarUtils.isSameDate(month.month, date: Date(), checkDay: false) + } return month.components.year! > 2013 || (month.components.year == 2013 && month.components.month! >= 9) } @@ -224,7 +316,10 @@ class CalendarMonthController: GenericViewController { readyOnce() } - + deinit { + var bp:Int = 0 + bp += 1 + } } diff --git a/Telegram-Mac/CalendarUtils.h b/Telegram-Mac/CalendarUtils.h index c0304bc20c..a4da0ab786 100644 --- a/Telegram-Mac/CalendarUtils.h +++ b/Telegram-Mac/CalendarUtils.h @@ -12,5 +12,5 @@ + (NSDate*) monthDay:(NSInteger)day date:(NSDate *)date; + (NSInteger)weekDay:(NSDate *)date; + (NSDate *) stepMonth:(NSInteger)dm date:(NSDate *)date; - ++ (NSDate *) year:(NSInteger)dm date:(NSDate *)date; @end diff --git a/Telegram-Mac/CalendarUtils.m b/Telegram-Mac/CalendarUtils.m index 0d3e4a59c8..82e8b08ac0 100644 --- a/Telegram-Mac/CalendarUtils.m +++ b/Telegram-Mac/CalendarUtils.m @@ -7,7 +7,7 @@ @implementation CalendarUtils + (BOOL) isSameDate:(NSDate*)d1 date:(NSDate*)d2 checkDay:(BOOL)checkDay { if(d1 && d2) { NSCalendar *cal = [NSCalendar currentCalendar]; - cal.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; + //cal.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; unsigned unitFlags = NSCalendarUnitDay | NSCalendarUnitYear | NSCalendarUnitMonth; NSDateComponents *components = [cal components:unitFlags fromDate:d1]; NSInteger ry = components.year; @@ -26,7 +26,7 @@ + (BOOL) isSameDate:(NSDate*)d1 date:(NSDate*)d2 checkDay:(BOOL)checkDay { + (NSDate*) monthDay:(NSInteger)day date:(NSDate *)date { NSCalendar *cal = [NSCalendar currentCalendar]; - cal.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; +// cal.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; unsigned unitFlags = NSCalendarUnitDay| NSCalendarUnitYear | NSCalendarUnitMonth; NSDateComponents *components = [cal components:unitFlags fromDate:date]; NSDateComponents *comps = [[NSDateComponents alloc] init]; @@ -52,14 +52,14 @@ +(NSInteger)weekDay:(NSDate *)date { + (NSInteger) lastDayOfTheMonth:(NSDate *)date { NSCalendar *cal = [NSCalendar currentCalendar]; - cal.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; + // cal.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; NSRange daysRange = [cal rangeOfUnit:NSCalendarUnitDay inUnit:NSCalendarUnitMonth forDate:date]; return daysRange.length; } + (NSInteger) colForDay:(NSInteger)day { NSCalendar *cal = [NSCalendar currentCalendar]; - cal.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; + // cal.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; NSInteger idx = day - cal.firstWeekday; if(idx < 0) idx = 7 + idx; @@ -68,7 +68,7 @@ + (NSInteger) colForDay:(NSInteger)day { + (NSString*) dd:(NSDate*)d { NSCalendar *cal = [NSCalendar currentCalendar]; - cal.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; + // cal.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; unsigned unitFlags = NSCalendarUnitDay | NSCalendarUnitYear | NSCalendarUnitMonth; NSDateComponents *cpt = [cal components:unitFlags fromDate:d]; return [NSString stringWithFormat:@"%ld-%ld-%ld",cpt.year, cpt.month, cpt.day]; @@ -76,7 +76,7 @@ + (NSString*) dd:(NSDate*)d { + (NSDate *) stepMonth:(NSInteger)dm date:(NSDate *)date { NSCalendar *cal = [NSCalendar currentCalendar]; - cal.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; + // cal.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; unsigned unitFlags = NSCalendarUnitDay| NSCalendarUnitYear | NSCalendarUnitMonth; NSDateComponents *components = [cal components:unitFlags fromDate:date]; NSInteger month = components.month + dm; @@ -89,9 +89,20 @@ + (NSDate *) stepMonth:(NSInteger)dm date:(NSDate *)date { month = 12; year--; } + components.day = 1; components.year = year; components.month = month; return [cal dateFromComponents:components]; } ++ (NSDate *) year:(NSInteger)dm date:(NSDate *)date { + NSCalendar *cal = [NSCalendar currentCalendar]; + unsigned unitFlags = NSCalendarUnitDay| NSCalendarUnitYear | NSCalendarUnitMonth; + NSDateComponents *components = [cal components:unitFlags fromDate:date]; + components.day = 1; + components.year = dm; + components.month = components.month; + return [cal dateFromComponents:components]; +} + @end diff --git a/Telegram-Mac/CallAudioPlayer.swift b/Telegram-Mac/CallAudioPlayer.swift index 7b5f720fd0..44e4f4c3f7 100644 --- a/Telegram-Mac/CallAudioPlayer.swift +++ b/Telegram-Mac/CallAudioPlayer.swift @@ -14,7 +14,10 @@ class CallAudioPlayer : NSObject, AVAudioPlayerDelegate { private var player: AVAudioPlayer?; public var completion:(()->Void)? + let tone: URL + init(_ url:URL, loops:Int, completion:(()->Void)? = nil) { + self.tone = url self.player = try? AVAudioPlayer(contentsOf: url) self.completion = completion super.init() diff --git a/Telegram-Mac/CallBridge.h b/Telegram-Mac/CallBridge.h deleted file mode 100644 index 2985138b9b..0000000000 --- a/Telegram-Mac/CallBridge.h +++ /dev/null @@ -1,33 +0,0 @@ -// -// CallsBridge.h -// Telegram -// -// Created by keepcoder on 03/05/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - -#import -#import "TGCallConnectionDescription.h" - -@interface AudioDevice : NSObject -@property(nonatomic, strong, readonly) NSString *deviceId; -@property(nonatomic, strong, readonly) NSString *deviceName; --(id)initWithDeviceId:(NSString*)deviceId deviceName:(NSString *)deviceName; -@end - -@interface CallBridge : NSObject --(void)startTransmissionIfNeeded:(bool)outgoing connection:(TGCallConnection *)connection; - --(void)mute; --(void)unmute; --(BOOL)isMuted; - --(NSString *)currentOutputDeviceId; --(NSString *)currentInputDeviceId; --(NSArray *)outputDevices; --(NSArray *)inputDevices; --(void)setCurrentOutputDeviceId:(NSString *)deviceId; --(void)setCurrentInputDeviceId:(NSString *)deviceId; -@property (nonatomic, copy) void (^stateChangeHandler)(int); - -@end diff --git a/Telegram-Mac/CallBridge.mm b/Telegram-Mac/CallBridge.mm index dd1f25f8f2..6982e49af5 100644 --- a/Telegram-Mac/CallBridge.mm +++ b/Telegram-Mac/CallBridge.mm @@ -6,19 +6,97 @@ // Copyright © 2017 Telegram. All rights reserved. // -#import "CallBridge.h" +#import "OngoingCallThreadLocalContext.h" #import "VoIPController.h" #import "VoIPServerConfig.h" #import "TGCallUtils.h" -#import "TGCallConnectionDescription.h" +#import "OngoingCallConnectionDescription.h" +#import "TgVoip.h" #define CVoIPController tgvoip::VoIPController +#import +#import + +void TGCallAesIgeEncrypt(uint8_t *inBytes, uint8_t *outBytes, size_t length, uint8_t *key, uint8_t *iv) { + MTAesEncryptRaw(inBytes, outBytes, length, key, iv); +} + +void TGCallAesIgeDecrypt(uint8_t *inBytes, uint8_t *outBytes, size_t length, uint8_t *key, uint8_t *iv) { + MTAesDecryptRaw(inBytes, outBytes, length, key, iv); +} + +void TGCallSha1(uint8_t *msg, size_t length, uint8_t *output) { + MTRawSha1(msg, length, output); +} + +void TGCallSha256(uint8_t *msg, size_t length, uint8_t *output) { + MTRawSha256(msg, length, output); +} + +void TGCallAesCtrEncrypt(uint8_t *inOut, size_t length, uint8_t *key, uint8_t *iv, uint8_t *ecount, uint32_t *num) { + uint8_t *outData = (uint8_t *)malloc(length); + MTAesCtr *aesCtr = [[MTAesCtr alloc] initWithKey:key keyLength:32 iv:iv ecount:ecount num:*num]; + [aesCtr encryptIn:inOut out:outData len:length]; + memcpy(inOut, outData, length); + free(outData); + + [aesCtr getIv:iv]; + + memcpy(ecount, [aesCtr ecount], 16); + *num = [aesCtr num]; +} + +void TGCallRandomBytes(uint8_t *buffer, size_t length) { + arc4random_buf(buffer, length); +} + +static TgVoipNetworkType callControllerNetworkTypeForType(OngoingCallNetworkType type) { + switch (type) { + case OngoingCallNetworkTypeWifi: + return TgVoipNetworkType::WiFi; + case OngoingCallNetworkTypeCellularGprs: + return TgVoipNetworkType::Gprs; + case OngoingCallNetworkTypeCellular3g: + return TgVoipNetworkType::ThirdGeneration; + case OngoingCallNetworkTypeCellularLte: + return TgVoipNetworkType::Lte; + default: + return TgVoipNetworkType::ThirdGeneration; + } +} + +static TgVoipDataSaving callControllerDataSavingForType(OngoingCallDataSaving type) { + switch (type) { + case OngoingCallDataSavingNever: + return TgVoipDataSaving::Never; + case OngoingCallDataSavingCellular: + return TgVoipDataSaving::Mobile; + case OngoingCallDataSavingAlways: + return TgVoipDataSaving::Always; + default: + return TgVoipDataSaving::Never; + } +} + + + +@implementation CProxy +-(id)initWithHost:(NSString*)host port:(int32_t)port user:(NSString *)user pass:(NSString *)pass { + self = [super init]; + _host = host; + _port = port; + _user = user; + _pass = pass; + return self; +} +@end + @interface VoIPControllerHolder : NSObject { tgvoip::VoIPController *_controller; } - + @property (nonatomic, assign, readonly) tgvoip::VoIPController *controller; - + @end @implementation AudioDevice @@ -37,7 +115,7 @@ -(id)initWithDeviceId:(NSString *)deviceId deviceName:(NSString *)deviceName { const NSTimeInterval TGCallPacketTimeout = 10; @implementation VoIPControllerHolder - + - (instancetype)initWithController:( tgvoip::VoIPController *)controller { self = [super init]; if (self != nil) { @@ -45,51 +123,247 @@ - (instancetype)initWithController:( tgvoip::VoIPController *)controller { } return self; } - + - ( tgvoip::VoIPController *)controller { return _controller; } -(void)dealloc { + _controller->Stop(); delete _controller; int bp = 0; bp++; } - + @end -@interface CallBridge () +@interface OngoingCallThreadLocalContext () { + int32_t _contextId; + + OngoingCallNetworkType _networkType; + NSTimeInterval _callReceiveTimeout; + NSTimeInterval _callRingTimeout; + NSTimeInterval _callConnectTimeout; + NSTimeInterval _callPacketTimeout; + + TgVoip *_tgVoip; + + OngoingCallState _state; + int32_t _signalBars; + NSData *_lastDerivedState; + +} @property (nonatomic, strong) VoIPControllerHolder *controller; @property (nonatomic, assign) BOOL _isMuted; - (void)controllerStateChanged:(int)state; + + + @end static void controllerStateCallback(tgvoip::VoIPController *controller, int state) { - CallBridge *session = (__bridge CallBridge *)controller->implData; + OngoingCallThreadLocalContext *session = (__bridge OngoingCallThreadLocalContext *)controller->implData; [session controllerStateChanged:state]; } +static MTAtomic *callContexts() { + static MTAtomic *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[MTAtomic alloc] initWithValue:[[NSMutableDictionary alloc] init]]; + }); + return instance; +} + + +@interface OngoingCallThreadLocalContextReference : NSObject + +@property (nonatomic, weak) OngoingCallThreadLocalContext *context; +@property (nonatomic, strong, readonly) id queue; + +@end + + +@implementation OngoingCallThreadLocalContextReference + +- (instancetype)initWithContext:(OngoingCallThreadLocalContext *)context queue:(id)queue { + self = [super init]; + if (self != nil) { + self.context = context; + _queue = queue; + } + return self; +} + +@end + + +static int32_t nextId = 1; + +static int32_t addContext(OngoingCallThreadLocalContext *context, id queue) { + int32_t contextId = OSAtomicIncrement32(&nextId); + [callContexts() with:^id(NSMutableDictionary *dict) { + dict[@(contextId)] = [[OngoingCallThreadLocalContextReference alloc] initWithContext:context queue:queue]; + return nil; + }]; + return contextId; +} + +static void removeContext(int32_t contextId) { + [callContexts() with:^id(NSMutableDictionary *dict) { + [dict removeObjectForKey:@(contextId)]; + return nil; + }]; +} + +static void withContext(int32_t contextId, void (^f)(OngoingCallThreadLocalContext *)) { + __block OngoingCallThreadLocalContextReference *reference = nil; + [callContexts() with:^id(NSMutableDictionary *dict) { + reference = dict[@(contextId)]; + return nil; + }]; + if (reference != nil) { + [reference.queue dispatch:^{ + __strong OngoingCallThreadLocalContext *context = reference.context; + if (context != nil) { + f(context); + } + }]; + } +} + -@implementation CallBridge +@implementation OngoingCallThreadLocalContext --(id)init { ++ (int32_t)maxLayer { + return 92; +} + + +- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue networkType:(OngoingCallNetworkType)networkType dataSaving:(OngoingCallDataSaving)dataSaving derivedState:(NSData * _Nonnull)derivedState key:(NSData * _Nonnull)key isOutgoing:(bool)isOutgoing primaryConnection:(OngoingCallConnectionDescription * _Nonnull)primaryConnection alternativeConnections:(NSArray * _Nonnull)alternativeConnections maxLayer:(int32_t)maxLayer allowP2P:(BOOL)allowP2P logPath:(NSString * _Nonnull)logPath { self = [super init]; if (self != nil) { - CVoIPController *controller = new CVoIPController(); - controller->implData = (__bridge void *)self; - controller->SetStateCallback(&controllerStateCallback); - - CVoIPController::crypto.sha1 = &TGCallSha1; - CVoIPController::crypto.sha256 = &TGCallSha256; - CVoIPController::crypto.rand_bytes = &TGCallRandomBytes; - CVoIPController::crypto.aes_ige_encrypt = &TGCallAesIgeEncryptInplace; - CVoIPController::crypto.aes_ige_decrypt = &TGCallAesIgeDecryptInplace; - CVoIPController::crypto.aes_ctr_encrypt = &TGCallAesCtrEncrypt; - _controller = [[VoIPControllerHolder alloc] initWithController:controller]; + + _contextId = addContext(self, queue); + + _callReceiveTimeout = 20.0; + _callRingTimeout = 90.0; + _callConnectTimeout = 30.0; + _callPacketTimeout = 10.0; + _networkType = networkType; + + + + std::unique_ptr proxyValue = nullptr; + if (proxy != nil) { + TgVoipProxy *proxyObject = new TgVoipProxy(); + proxyObject->host = proxy.host.UTF8String; + proxyObject->port = (uint16_t)proxy.port; + proxyObject->login = proxy.user.UTF8String ?: ""; + proxyObject->password = proxy.pass.UTF8String ?: ""; + proxyValue = std::unique_ptr(proxyObject); + } + + + + TgVoipCrypto crypto; + crypto.sha1 = &TGCallSha1; + crypto.sha256 = &TGCallSha256; + crypto.rand_bytes = &TGCallRandomBytes; + crypto.aes_ige_encrypt = &TGCallAesIgeEncrypt; + crypto.aes_ige_decrypt = &TGCallAesIgeDecrypt; + crypto.aes_ctr_encrypt = &TGCallAesCtrEncrypt; + + std::vector endpoints; + NSArray *connections = [@[primaryConnection] arrayByAddingObjectsFromArray:alternativeConnections]; + for (OngoingCallConnectionDescription *connection in connections) { + unsigned char peerTag[16]; + [connection.peerTag getBytes:peerTag length:16]; + + TgVoipEndpoint endpoint; + endpoint.endpointId = connection.identifier; + endpoint.host = { + .ipv4 = std::string(connection.ipv4.UTF8String), + .ipv6 = std::string(connection.ipv6.UTF8String) + }; + endpoint.port = (uint16_t)connection.port; + endpoint.type = TgVoipEndpointType::UdpRelay; + memcpy(endpoint.peerTag, peerTag, 16); + endpoints.push_back(endpoint); + } + + TgVoipConfig config = { + .initializationTimeout = _callConnectTimeout, + .receiveTimeout = _callPacketTimeout, + .dataSaving = callControllerDataSavingForType(dataSaving), + .enableP2P = static_cast(allowP2P), + .enableAEC = false, + .enableNS = true, + .enableAGC = true, + .enableCallUpgrade = false, + .logPath = logPath.length == 0 ? "" : std::string(logPath.UTF8String), + .maxApiLayer = [OngoingCallThreadLocalContext maxLayer] + }; + + std::vector encryptionKeyValue; + encryptionKeyValue.resize(key.length); + memcpy(encryptionKeyValue.data(), key.bytes, key.length); + + TgVoipEncryptionKey encryptionKey = { + .value = encryptionKeyValue, + .isOutgoing = isOutgoing, + }; + + + _tgVoip = TgVoip::makeInstance( + config, + { derivedStateValue }, + endpoints, + proxyValue, + callControllerNetworkTypeForType(networkType), + encryptionKey, + crypto + ); + + _state = OngoingCallStateInitializing; + _signalBars = -1; + + __weak OngoingCallThreadLocalContext *weakSelf = self; + _tgVoip->setOnStateUpdated([weakSelf](TgVoipState state) { + __strong OngoingCallThreadLocalContext *strongSelf = weakSelf; + if (strongSelf) { + [strongSelf controllerStateChanged:state]; + } + }); + _tgVoip->setOnSignalBarsUpdated([weakSelf](int signalBars) { + __strong OngoingCallThreadLocalContext *strongSelf = weakSelf; + if (strongSelf) { + [strongSelf signalBarsChanged:signalBars]; + } + }); + + +// +// CVoIPController *controller = new CVoIPController(); +// controller->implData = (__bridge void *)self; +// tgvoip::VoIPController::Callbacks callbacks={0}; +// callbacks.connectionStateChanged=&controllerStateCallback; +// controller->SetCallbacks(callbacks); +// if (proxy != nil) { +// controller->SetProxy(tgvoip::PROXY_SOCKS5, std::string([proxy.host UTF8String]), proxy.port, std::string([proxy.user UTF8String]), std::string([proxy.pass UTF8String])); +// } +// +// CVoIPController::crypto.sha1 = &TGCallSha1; +// CVoIPController::crypto.sha256 = &TGCallSha256; +// CVoIPController::crypto.rand_bytes = &TGCallRandomBytes; +// CVoIPController::crypto.aes_ige_encrypt = &TGCallAesIgeEncryptInplace; +// CVoIPController::crypto.aes_ige_decrypt = &TGCallAesIgeDecryptInplace; +// CVoIPController::crypto.aes_ctr_encrypt = &TGCallAesCtrEncrypt; +// _controller = [[VoIPControllerHolder alloc] initWithController:controller]; } return self; @@ -116,13 +390,19 @@ - (void)controllerStateChanged:(int)state } } ++(int32_t)voipMaxLayer { + return tgvoip::VoIPController::GetConnectionMaxLayer(); +} ++(NSString *)voipVersion { + return [NSString stringWithUTF8String:tgvoip::VoIPController::GetVersion()]; +} --(NSArray *)inputDevices { ++(NSArray *)inputDevices { - std::vector vector = _controller.controller->EnumerateAudioInputs(); + std::vector vector = tgvoip::VoIPController::EnumerateAudioInputs(); NSMutableArray * devices = [[NSMutableArray alloc] init]; - [devices addObject:[[AudioDevice alloc] initWithDeviceId:@"default" deviceName:@"Default"]]; + [devices addObject:[[AudioDevice alloc] initWithDeviceId:nil deviceName:@"Default"]]; for(std::vector::iterator it = vector.begin(); it != vector.end(); ++it) { std::string deviceId = it->id; std::string deviceName = it->displayName; @@ -132,12 +412,12 @@ - (void)controllerStateChanged:(int)state return devices; } --(NSArray *)outputDevices { ++(NSArray *)outputDevices { - std::vector vector = _controller.controller->EnumerateAudioOutputs(); + std::vector vector = tgvoip::VoIPController::EnumerateAudioOutputs(); NSMutableArray * devices = [[NSMutableArray alloc] init]; - [devices addObject:[[AudioDevice alloc] initWithDeviceId:@"default" deviceName:@"Default"]]; + [devices addObject:[[AudioDevice alloc] initWithDeviceId:nil deviceName:@"Default"]]; for(std::vector::iterator it = vector.begin(); it != vector.end(); ++it) { std::string deviceId = it->id; std::string deviceName = it->displayName; @@ -161,22 +441,28 @@ -(void)setCurrentInputDeviceId:(NSString *)deviceId { -(void)setCurrentOutputDeviceId:(NSString *)deviceId { _controller.controller->SetCurrentAudioOutput(std::string([deviceId UTF8String])); } - + +-(void)setMutedOtherSounds:(BOOL)mute { + _controller.controller->SetAudioOutputDuckingEnabled(mute); +} + // --(void)startTransmissionIfNeeded:(bool)outgoing connection:(TGCallConnection *)connection { +-(void)startTransmissionIfNeeded:(bool)outgoing allowP2p:(bool)allowP2p serializedData:(NSString *)serializedData connection:(TGCallConnection *)connection { - voip_config_t config = { 0 }; - config.init_timeout = TGCallConnectTimeout; - config.recv_timeout = TGCallPacketTimeout; - config.data_saving = false; + tgvoip::VoIPController::Config config = tgvoip::VoIPController::Config(); + config.initTimeout = TGCallConnectTimeout; + config.recvTimeout = TGCallPacketTimeout; + config.dataSaving = tgvoip::DATA_SAVING_NEVER; config.enableAEC = false; config.enableNS = true; config.enableAGC = true; - strncpy(config.logFilePath, [[@"~/Library/Group Containers/6N38VWS5BX.ru.keepcoder.Telegram/voip.log" stringByExpandingTildeInPath] UTF8String], sizeof(config.logFilePath)); //memset(config.logFilePath, 0, sizeof(config.logFilePath)); - - _controller.controller->SetConfig(&config); + config.logFilePath = [[@"~/Library/Group Containers/6N38VWS5BX.ru.keepcoder.Telegram/voip.log" stringByExpandingTildeInPath] UTF8String]; + // strncpy(config.logFilePath, [[@"~/Library/Group Containers/6N38VWS5BX.ru.keepcoder.Telegram/voip.log" stringByExpandingTildeInPath] UTF8String], sizeof(config.logFilePath)); //memset(config.logFilePath, 0, sizeof(config.logFilePath)); + + _controller.controller->SetConfig(config); + tgvoip::ServerConfig::GetSharedInstance()->Update(serializedData.UTF8String); std::vector endpoints {}; std::vector::iterator it = endpoints.begin(); @@ -184,35 +470,42 @@ -(void)startTransmissionIfNeeded:(bool)outgoing connection:(TGCallConnection *)c NSArray *connections = [@[connection.defaultConnection] arrayByAddingObjectsFromArray:connection.alternativeConnections]; for (NSUInteger i = 0; i < connections.count; i++) { - TGCallConnectionDescription *desc = connections[i]; + OngoingCallConnectionDescription *desc = connections[i]; tgvoip::Endpoint endpoint {}; - + endpoint.id = desc.identifier; endpoint.port = (uint32_t)desc.port; - endpoint.address = tgvoip::IPv4Address(desc.ipv4.UTF8String); - endpoint.v6address = tgvoip::IPv6Address(desc.ipv6.UTF8String); - endpoint.type = EP_TYPE_UDP_RELAY; + + tgvoip::IPv4Address address(std::string(desc.ipv4.UTF8String)); + tgvoip::IPv6Address addressv6(std::string(desc.ipv6.UTF8String)); + + +// endpoint.address = tgvoip::NetworkAddress::IPv4(desc.ipv4.UTF8String); +// endpoint.v6address = tgvoip::NetworkAddress::IPv4(desc.ipv6.UTF8String); + endpoint.type = tgvoip::Endpoint::Type::UDP_RELAY; + endpoint.address = address; + endpoint.v6address = addressv6; [desc.peerTag getBytes:&endpoint.peerTag length:16]; it = endpoints.insert ( it , endpoint ); } _controller.controller->SetEncryptionKey((char *)connection.key.bytes, outgoing); - _controller.controller->SetRemoteEndpoints(endpoints, true); - + _controller.controller->SetRemoteEndpoints(endpoints, allowP2p, connection.maxLayer); + _controller.controller->Start(); _controller.controller->Connect(); } - + -(void)dealloc { int bp = 0; bp += 1; } - + @end diff --git a/Telegram-Mac/CallControl.swift b/Telegram-Mac/CallControl.swift new file mode 100644 index 0000000000..fa0d3b6c52 --- /dev/null +++ b/Telegram-Mac/CallControl.swift @@ -0,0 +1,245 @@ +// +// CallControl.swift +// Telegram +// +// Created by Mikhail Filimonov on 14/08/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +struct CallControlData { + enum Mode: Equatable { + case normal(NSColor, CGImage) + case visualEffect(CGImage) + case animated(LocalAnimatedSticker, NSColor) + } + + let text: String? + let mode: Mode + let iconSize: NSSize + + func isProperView(_ view: NSView?) -> Bool { + if view == nil { + return false + } + switch mode { + case .visualEffect: + return view is NSVisualEffectView + case .animated: + return view is LottiePlayerView + case .normal: + return !(view is LottiePlayerView) && !(view is NSVisualEffectView) + } + } +} + +final class CallControl : Control { + private let imageView: ImageView = ImageView() + private var imageBackgroundView:NSView? = nil + private var textView: TextView? + + private var progressView: RadialProgressView? + + private var isLoading: Bool = false + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + self.imageView.isEventLess = true + } + + func updateLoading(_ isLoading: Bool, animated: Bool) { + self.isLoading = isLoading + if isLoading { + if progressView == nil { + progressView = RadialProgressView(theme: RadialProgressTheme(backgroundColor: .clear, foregroundColor: .grayIcon), twist: true) + + addSubview(progressView!) + + if animated { + progressView?.layer?.animateAlpha(from: 0, to: 1, duration: 0.4) + } + } + let rect = imageBackgroundView?.bounds ?? NSMakeRect(0, 0, 40, 40) + + progressView?.frame = NSMakeRect(rect.minX + 1, rect.minY + 1, rect.width - 2, rect.height - 2) + progressView?.state = .ImpossibleFetching(progress: 0.2, force: !animated) + } else { + if let progressView = self.progressView { + self.progressView = nil + progressView.state = .ImpossibleFetching(progress: 1.0, force: false) + if animated { + progressView.layer?.animateAlpha(from: 1, to: 0, duration: 0.4, removeOnCompletion: false, completion: { [weak progressView] _ in + progressView?.removeFromSuperview() + }) + } else { + progressView.removeFromSuperview() + } + } + } + } + + override var isEnabled: Bool { + get { + return super.isEnabled && !isLoading + } + set { + super.isEnabled = newValue + } + } + + override var mouseDownCanMoveWindow: Bool { + return false + } + private var previousState: ControlState? + + override func stateDidUpdated( _ state: ControlState) { + switch controlState { + case .Highlight: + imageBackgroundView?._change(opacity: 0.9) + textView?.change(opacity: 0.9) + imageBackgroundView?.layer?.animateScaleCenter(from: 1, to: 0.95, duration: 0.2, removeOnCompletion: false) + default: + imageBackgroundView?._change(opacity: 1.0) + textView?.change(opacity: 1.0) + if let previousState = previousState, previousState == .Highlight { + imageBackgroundView?.layer?.animateScaleCenter(from: 0.95, to: 1.0, duration: 0.2) + } + } + previousState = state + } + + func updateEnabled(_ enabled: Bool, animated: Bool) { + self.isEnabled = enabled + + change(opacity: enabled ? 1 : 0.7, animated: animated) + } + + var size: NSSize { + return imageBackgroundView?.frame.size ?? frame.size + } + + func updateWithData(_ data: CallControlData, animated: Bool) { + + if let text = data.text { + let current: TextView + if let textView = self.textView { + current = textView + } else { + current = TextView() + self.textView = current + current.isSelectable = false + current.userInteractionEnabled = false + current.isEventLess = true + addSubview(current) + current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + let layout = TextViewLayout(.initialize(string: text, color: .white, font: .normal(12)), maximumNumberOfLines: 1) + layout.measure(width: max(data.iconSize.width, 100)) + current.update(layout) + } else { + if let textView = self.textView { + self.textView = nil + textView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak textView] _ in + textView?.removeFromSuperview() + }) + } + } + + switch data.mode { + case let .visualEffect(icon): + if !data.isProperView(self.imageBackgroundView) { + self.imageBackgroundView?.removeFromSuperview() + self.imageBackgroundView = NSVisualEffectView(frame: NSMakeRect(0, 0, data.iconSize.width, data.iconSize.height)) + self.imageBackgroundView?.wantsLayer = true + self.addSubview(self.imageBackgroundView!) + } + let view = self.imageBackgroundView as! NSVisualEffectView + + view.material = .light + view.state = .active + view.blendingMode = .withinWindow + + imageView.isHidden = false + imageView.animates = animated + imageView.image = icon + imageView.setFrameSize(data.iconSize) + + case let .normal(color, icon): + if !data.isProperView(self.imageBackgroundView) { + self.imageBackgroundView?.removeFromSuperview() + self.imageBackgroundView = NSView(frame: NSMakeRect(0, 0, data.iconSize.width, data.iconSize.height)) + self.imageBackgroundView?.wantsLayer = true + self.addSubview(self.imageBackgroundView!) + } + imageView.isHidden = false + imageView.animates = animated + imageView.image = icon + imageView.setFrameSize(data.iconSize) + + self.imageBackgroundView?.background = color + case let .animated(value, color): + if !data.isProperView(self.imageBackgroundView) { + self.imageBackgroundView?.removeFromSuperview() + self.imageBackgroundView = LottiePlayerView(frame: NSMakeRect(0, 0, data.iconSize.width, data.iconSize.height)) + self.imageBackgroundView?.wantsLayer = true + self.addSubview(self.imageBackgroundView!) + } + imageView.isHidden = true + let player = self.imageBackgroundView as? LottiePlayerView + if let animationData = value.data { + let policy: LottiePlayPolicy + if animated { + policy = .toEnd(from: 1) + } else { + policy = .toEnd(from: .max) + } + player?.set(LottieAnimation(compressed: animationData, key: .init(key: .bundle(value.rawValue), size: data.iconSize), type: .lottie, cachePurpose: .none, playPolicy: policy, runOnQueue: .mainQueue())) + } + self.imageBackgroundView?.background = color + } + + + imageView.removeFromSuperview() + self.imageBackgroundView?.addSubview(imageView) + + imageBackgroundView!._change(size: data.iconSize, animated: animated) + imageBackgroundView!.layer?.cornerRadius = data.iconSize.height / 2 + + + if let textView = self.textView { + change(size: NSMakeSize(max(data.iconSize.width, textView.frame.width), data.iconSize.height + 5 + textView.frame.height), animated: animated) + + imageView._change(pos: imageBackgroundView!.focus(imageView.frame.size).origin, animated: animated) + textView._change(pos: NSMakePoint(floorToScreenPixels(backingScaleFactor, (frame.width - textView.frame.width) / 2), imageBackgroundView!.frame.height + 5), animated: animated) + imageBackgroundView!._change(pos: NSMakePoint(floorToScreenPixels(backingScaleFactor, (frame.width - imageBackgroundView!.frame.width) / 2), 0), animated: animated) + } else { + change(size: data.iconSize, animated: animated) + imageView._change(pos: imageBackgroundView!.focus(imageView.frame.size).origin, animated: animated) + imageBackgroundView!._change(pos: focus(imageBackgroundView!.frame.size).origin, animated: animated) + } + + needsLayout = true + } + + override func layout() { + super.layout() + + imageView.center() + if let imageBackgroundView = imageBackgroundView { + if let textView = textView { + imageBackgroundView.centerX(y: 0) + textView.setFrameOrigin(NSMakePoint(floorToScreenPixels(backingScaleFactor, (frame.width - textView.frame.width) / 2), imageBackgroundView.frame.height + 5)) + } else { + imageBackgroundView.center() + } + } + + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/CallFeedbackController.swift b/Telegram-Mac/CallFeedbackController.swift new file mode 100644 index 0000000000..a5efe4346e --- /dev/null +++ b/Telegram-Mac/CallFeedbackController.swift @@ -0,0 +1,264 @@ +// +// CallFeedbackController.swift +// Telegram +// +// Created by Mikhail Filimonov on 05/10/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import Postbox +import SwiftSignalKit + +import TelegramCore + + +private final class CallFeedbackControllerArguments { + let updateComment: (String) -> Void + let scrollToComment: () -> Void + let toggleReason: (CallFeedbackReason, Bool) -> Void + let toggleIncludeLogs: (Bool) -> Void + + init(updateComment: @escaping (String) -> Void, scrollToComment: @escaping () -> Void, toggleReason: @escaping (CallFeedbackReason, Bool) -> Void, toggleIncludeLogs: @escaping (Bool) -> Void) { + self.updateComment = updateComment + self.scrollToComment = scrollToComment + self.toggleReason = toggleReason + self.toggleIncludeLogs = toggleIncludeLogs + } +} + + +private enum CallFeedbackReason: Int32, CaseIterable { + case videoDistorted + case videoLowQuality + + case echo + case noise + case interruption + case distortedSpeech + case silentLocal + case silentRemote + case dropped + + var hashtag: String { + switch self { + case .echo: + return "echo" + case .noise: + return "noise" + case .interruption: + return "interruptions" + case .distortedSpeech: + return "distorted_speech" + case .silentLocal: + return "silent_local" + case .silentRemote: + return "silent_remote" + case .dropped: + return "dropped" + case .videoDistorted: + return "distorted_video" + case .videoLowQuality: + return "pixelated_video" + } + } + + var isVideoRelated: Bool { + switch self { + case .videoDistorted, .videoLowQuality: + return true + default: + return false + } + } + + var localizedString: String { + switch self { + case .echo: + return L10n.callFeedbackReasonEcho + case .noise: + return L10n.callFeedbackReasonNoise + case .interruption: + return L10n.callFeedbackReasonInterruption + case .distortedSpeech: + return L10n.callFeedbackReasonDistortedSpeech + case .silentLocal: + return L10n.callFeedbackReasonSilentLocal + case .silentRemote: + return L10n.callFeedbackReasonSilentRemote + case .dropped: + return L10n.callFeedbackReasonDropped + case .videoDistorted: + return L10n.callFeedbackVideoReasonDistorted + case .videoLowQuality: + return L10n.callFeedbackVideoReasonLowQuality + } + } +} + +private struct CallFeedbackState: Equatable { + let reasons: Set + let comment: String + let includeLogs: Bool + + init(reasons: Set = Set(), comment: String = "", includeLogs: Bool = true) { + self.reasons = reasons + self.comment = comment + self.includeLogs = includeLogs + } + + func withUpdatedReasons(_ reasons: Set) -> CallFeedbackState { + return CallFeedbackState(reasons: reasons, comment: self.comment, includeLogs: self.includeLogs) + } + + func withUpdatedComment(_ comment: String) -> CallFeedbackState { + return CallFeedbackState(reasons: self.reasons, comment: comment, includeLogs: self.includeLogs) + } + + func withUpdatedIncludeLogs(_ includeLogs: Bool) -> CallFeedbackState { + return CallFeedbackState(reasons: self.reasons, comment: self.comment, includeLogs: includeLogs) + } +} + +private func _id_reason(_ reason: CallFeedbackReason) -> InputDataIdentifier { + return InputDataIdentifier.init("_id_reason_\(reason.hashtag)") +} +private let _id_comment: InputDataIdentifier = InputDataIdentifier("_id_comment") +private let _id_logs: InputDataIdentifier = InputDataIdentifier("_id_logs") + +private func callFeedbackControllerEntries(state: CallFeedbackState, isVideo: Bool, arguments: CallFeedbackControllerArguments) -> [InputDataEntry] { + var entries: [InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.callFeedbackWhatWentWrong), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + let reasons = CallFeedbackReason.allCases.filter { value in + if isVideo { + return true + } else if !isVideo && !value.isVideoRelated { + return true + } + return false + } + for reason in reasons { + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_reason(reason), data: .init(name: reason.localizedString, color: theme.colors.text, type: .switchable(state.reasons.contains(reason)), viewType: bestGeneralViewType(reasons, for: reason), action: { + arguments.toggleReason(reason, !state.reasons.contains(reason)) + }))) + index += 1 + } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.input(sectionId: sectionId, index: index, value: .string(state.comment), error: nil, identifier: _id_comment, mode: .plain, data: .init(viewType: .singleItem, canMakeTransformations: false), placeholder: nil, inputPlaceholder: L10n.callFeedbackAddComment, filter: { $0 }, limit: 255)) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_logs, data: .init(name: L10n.callFeedbackIncludeLogs, color: theme.colors.text, type: .switchable(state.includeLogs), viewType: .singleItem, action: { + arguments.toggleIncludeLogs(!state.includeLogs) + }))) + index += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.callFeedbackIncludeLogsInfo), data: .init(color: theme.colors.listGrayText, viewType: .textBottomItem))) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries + +} + +func CallFeedbackController(context: AccountContext, callId: CallId, starsCount: Int, userInitiated: Bool, isVideo: Bool) -> ModalViewController { + + let initialState = CallFeedbackState() + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((CallFeedbackState) -> CallFeedbackState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + let arguments = CallFeedbackControllerArguments.init(updateComment: { value in + updateState { + $0.withUpdatedComment(value) + } + }, scrollToComment: { + + }, toggleReason: { reason, value in + updateState { current in + var reasons = current.reasons + if value { + reasons.insert(reason) + } else { + reasons.remove(reason) + } + return current.withUpdatedReasons(reasons) + } + }, toggleIncludeLogs: { value in + updateState { $0.withUpdatedIncludeLogs(value) } + }) + + let signal = statePromise.get() |> map { state in + return InputDataSignalValue(entries: callFeedbackControllerEntries(state: state, isVideo: isVideo, arguments: arguments)) + } + + let controller = InputDataController(dataSignal: signal, title: "Call Feedback") + + var close: (()->Void)? = nil + + let modalInteractions = ModalInteractions(acceptTitle: L10n.modalSend, accept: { [weak controller] in + controller?.validateInputValues() + close?() + }, height: 50, singleButton: true) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { + close?() + }) + + controller.updateDatas = { data in + + return .none + } + + controller.validateData = { data in + + let state = stateValue.with { $0 } + var comment = state.comment + var hashtags = "" + for reason in CallFeedbackReason.allCases { + if state.reasons.contains(reason) { + if !hashtags.isEmpty { + hashtags.append(" ") + } + hashtags.append("#\(reason.hashtag)") + } + } + if !comment.isEmpty && !state.reasons.isEmpty { + comment.append("\n") + } + comment.append(hashtags) + + let _ = rateCallAndSendLogs(context: context, callId: callId, starsCount: starsCount, comment: comment, userInitiated: userInitiated, includeLogs: state.includeLogs).start() + + return .success(.custom({ + close?() + })) + } + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions, closeHandler: { f in f() }, size: NSMakeSize(300, 300)) + + close = { [weak modalController] in + modalController?.close() + } + + return modalController +} diff --git a/Telegram-Mac/CallNavigationHeaderView.swift b/Telegram-Mac/CallNavigationHeaderView.swift index 0469cbdae0..bce908097c 100644 --- a/Telegram-Mac/CallNavigationHeaderView.swift +++ b/Telegram-Mac/CallNavigationHeaderView.swift @@ -8,186 +8,868 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac -import TelegramCoreMac -import PostboxMac - -class CallNavigationHeaderView: NavigationHeaderView { - private let backgroundView = NSView() - private let callInfo:TitleButton = TitleButton() - private let endCall:TitleButton = TitleButton() - private let durationView:NSTextField = NSTextField() - private let muteControl:ImageButton = ImageButton() - private let dropCall:ImageButton = ImageButton() - private let durationDisposable = MetaDisposable() - private let stateDisposable = MetaDisposable() - private var session:PCallSession? = nil { +import SwiftSignalKit +import TelegramCore + +import Postbox + + +private let blue = NSColor(rgb: 0x0078ff) +private let lightBlue = NSColor(rgb: 0x59c7f8) +private let green = NSColor(rgb: 0x33c659) + +private let purple = NSColor(rgb: 0x766EE9) +private let lightPurple = NSColor(rgb: 0xF05459) + + +class CallStatusBarBackgroundViewLegacy : View, CallStatusBarBackground { + var audioLevel: Float = 0 + var speaking:(Bool, Bool, Bool)? = nil { didSet { - if let session = session { - durationDisposable.set(session.durationPromise.get().start(next: { [weak self] duration in - self?.durationView.stringValue = String.durationTransformed(elapsed: Int(duration)) - self?.durationView.sizeToFit() - self?.needsLayout = true - })) - - stateDisposable.set((session.state.get() |> deliverOnMainQueue).start(next: { [weak self] state in - switch state { - case .terminated, .dropping: - self?.hide() - default: - break + if let speaking = self.speaking, (speaking.0 != oldValue?.0 || speaking.1 != oldValue?.1 || speaking.2 != oldValue?.2) { + let targetColors: [NSColor] + if speaking.1 { + if speaking.2 { + if speaking.0 { + targetColors = [green, blue] + } else { + targetColors = [blue, lightBlue] + } + } else { + targetColors = [purple, lightPurple] } - })) - } else { - durationDisposable.set(nil) - stateDisposable.set(nil) + + } else { + targetColors = [theme.colors.grayIcon, theme.colors.grayIcon.lighter()] + } + self.backgroundColor = targetColors.first ?? theme.colors.accent } } } +} + +class CallStatusBarBackgroundView: View, CallStatusBarBackground { + private let foregroundView: View + private let foregroundGradientLayer: CAGradientLayer + private let maskCurveLayer: VoiceCurveLayer + var audioLevel: Float = 0.0 { + didSet { + self.maskCurveLayer.updateLevel(CGFloat(audioLevel)) + } + } - override func mouseUp(with event: NSEvent) { - super.mouseUp(with: event) - if let session = session { - showPhoneCallWindow(session) + + var speaking:(Bool, Bool, Bool)? = nil { + didSet { + if let speaking = self.speaking, (speaking.0 != oldValue?.0 || speaking.1 != oldValue?.1 || speaking.2 != oldValue?.2) { + let initialColors = self.foregroundGradientLayer.colors + let targetColors: [CGColor] + if speaking.1 { + if speaking.2 { + if speaking.0 { + targetColors = [green.cgColor, blue.cgColor] + } else { + targetColors = [blue.cgColor, lightBlue.cgColor] + } + } else { + targetColors = [purple.cgColor, lightPurple.cgColor] + } + + } else { + targetColors = [theme.colors.grayIcon.cgColor, theme.colors.grayIcon.lighter().cgColor] + } + + self.foregroundGradientLayer.colors = targetColors + self.foregroundGradientLayer.animate(from: initialColors as AnyObject, to: targetColors as AnyObject, keyPath: "colors", timingFunction: .linear, duration: 0.3) + } + } + } + + + + override init() { + self.foregroundView = View() + self.foregroundGradientLayer = CAGradientLayer() + self.maskCurveLayer = VoiceCurveLayer(frame: CGRect(), maxLevel: 2.5, smallCurveRange: (0.0, 0.0), mediumCurveRange: (0.1, 0.55), bigCurveRange: (0.1, 1.0)) + self.maskCurveLayer.setColor(NSColor(rgb: 0xffffff)) + + + super.init() + + + self.addSubview(self.foregroundView) + self.foregroundView.layer?.addSublayer(self.foregroundGradientLayer) + + + self.foregroundGradientLayer.colors = [theme.colors.grayIcon.cgColor, theme.colors.grayIcon.lighter().cgColor] + self.foregroundGradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5) + self.foregroundGradientLayer.endPoint = CGPoint(x: 2.0, y: 0.5) + + self.foregroundView.layer?.mask = maskCurveLayer + //layer?.addSublayer(maskCurveLayer) + + self.updateAnimations() + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + + override func layout() { + super.layout() + + CATransaction.begin() + CATransaction.setDisableActions(true) + self.foregroundView.frame = NSMakeRect(0, 0, frame.width, frame.height) + self.foregroundGradientLayer.frame = foregroundView.bounds + self.maskCurveLayer.frame = NSMakeRect(0, 0, frame.width, frame.height) + CATransaction.commit() + } + private let occlusionDisposable = MetaDisposable() + private var isCurrentlyInHierarchy: Bool = false { + didSet { + updateAnimations() } } - func hide() { - header?.hide(true) - stateDisposable.set(nil) - durationDisposable.set(nil) + deinit { + occlusionDisposable.dispose() } - func update(with session: PCallSession) { - self.session = session - - let signal = session.account.viewTracker.peerView( session.peerId) |> deliverOnMainQueue |> beforeNext { [weak self] peerView in - - if let peer = peerViewMainPeer(peerView), let strongSelf = self { - strongSelf.callInfo.set(text: peer.displayTitle, for: .Normal) - strongSelf.needsLayout = true + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + if let window = window as? Window { + occlusionDisposable.set(window.takeOcclusionState.start(next: { [weak self] value in + self?.isCurrentlyInHierarchy = value.contains(.visible) + })) + } else { + occlusionDisposable.set(nil) + isCurrentlyInHierarchy = false + } + } + + func updateAnimations() { + if !isCurrentlyInHierarchy { + self.foregroundGradientLayer.removeAllAnimations() + self.maskCurveLayer.stopAnimating() + return + } + self.maskCurveLayer.startAnimating() + } +} + + + +private final class VoiceCurveLayer: CALayer { + private let smallCurve: CurveLayer + private let mediumCurve: CurveLayer + private let bigCurve: CurveLayer + + + private let maxLevel: CGFloat + + private var displayLinkAnimator: ConstantDisplayLinkAnimator? + + private var audioLevel: CGFloat = 0.0 + var presentationAudioLevel: CGFloat = 0.0 + + private(set) var isAnimating = false + + public typealias CurveRange = (min: CGFloat, max: CGFloat) + + public init( + frame: CGRect, + maxLevel: CGFloat, + smallCurveRange: CurveRange, + mediumCurveRange: CurveRange, + bigCurveRange: CurveRange + ) { + self.maxLevel = maxLevel + + self.smallCurve = CurveLayer( + pointsCount: 7, + minRandomness: 1, + maxRandomness: 1.3, + minSpeed: 0.9, + maxSpeed: 3.2, + minOffset: smallCurveRange.min, + maxOffset: smallCurveRange.max + ) + self.mediumCurve = CurveLayer( + pointsCount: 7, + minRandomness: 1.2, + maxRandomness: 1.5, + minSpeed: 1.0, + maxSpeed: 4.4, + minOffset: mediumCurveRange.min, + maxOffset: mediumCurveRange.max + ) + self.bigCurve = CurveLayer( + pointsCount: 7, + minRandomness: 1.2, + maxRandomness: 1.7, + minSpeed: 1.0, + maxSpeed: 5.8, + minOffset: bigCurveRange.min, + maxOffset: bigCurveRange.max + ) + + super.init() + + self.addSublayer(bigCurve) + self.addSublayer(mediumCurve) + self.addSublayer(smallCurve) + + displayLinkAnimator = ConstantDisplayLinkAnimator() { [weak self] in + guard let strongSelf = self else { return } + + strongSelf.presentationAudioLevel = strongSelf.presentationAudioLevel * 0.9 + strongSelf.audioLevel * 0.1 + + strongSelf.smallCurve.level = strongSelf.presentationAudioLevel + strongSelf.mediumCurve.level = strongSelf.presentationAudioLevel + strongSelf.bigCurve.level = strongSelf.presentationAudioLevel + } + } + + override init(layer: Any) { + let maxLevel: CGFloat = 2.5 + let smallCurveRange:CurveRange = (0.0, 0.0) + let mediumCurveRange:CurveRange = (0.1, 0.55) + let bigCurveRange:CurveRange = (0.1, 1.0) + self.maxLevel = maxLevel + + self.smallCurve = CurveLayer( + pointsCount: 7, + minRandomness: 1, + maxRandomness: 1.3, + minSpeed: 0.9, + maxSpeed: 3.2, + minOffset: smallCurveRange.min, + maxOffset: smallCurveRange.max + ) + self.mediumCurve = CurveLayer( + pointsCount: 7, + minRandomness: 1.2, + maxRandomness: 1.5, + minSpeed: 1.0, + maxSpeed: 4.4, + minOffset: mediumCurveRange.min, + maxOffset: mediumCurveRange.max + ) + self.bigCurve = CurveLayer( + pointsCount: 7, + minRandomness: 1.2, + maxRandomness: 1.7, + minSpeed: 1.0, + maxSpeed: 5.8, + minOffset: bigCurveRange.min, + maxOffset: bigCurveRange.max + ) + super.init(layer: layer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + public func setColor(_ color: NSColor) { + smallCurve.setColor(color.withAlphaComponent(1.0)) + mediumCurve.setColor(color.withAlphaComponent(0.55)) + bigCurve.setColor(color.withAlphaComponent(0.35)) + } + + public func updateLevel(_ level: CGFloat) { + let normalizedLevel = min(1, max(level / maxLevel, 0)) + + smallCurve.updateSpeedLevel(to: normalizedLevel) + mediumCurve.updateSpeedLevel(to: normalizedLevel) + bigCurve.updateSpeedLevel(to: normalizedLevel) + + audioLevel = normalizedLevel + } + + public func startAnimating() { + guard !isAnimating else { return } + isAnimating = true + + updateCurvesState() + + displayLinkAnimator?.isPaused = false + } + + public func stopAnimating() { + self.stopAnimating(duration: 0.15) + } + + public func stopAnimating(duration: Double) { + guard isAnimating else { return } + isAnimating = false + + updateCurvesState() + + displayLinkAnimator?.isPaused = true + } + + private func updateCurvesState() { + if isAnimating { + if smallCurve.frame.size != .zero { + smallCurve.startAnimating() + mediumCurve.startAnimating() + bigCurve.startAnimating() } - } |> map {_ in return true} - - self.ready.set(signal) - updateMutedBg(session, animated: false) + } else { + smallCurve.stopAnimating() + mediumCurve.stopAnimating() + bigCurve.stopAnimating() + } + } + + override var frame: NSRect { + didSet { + if oldValue != frame { + smallCurve.frame = bounds + mediumCurve.frame = bounds + bigCurve.frame = bounds + + updateCurvesState() + } + } + } +} + +final class CurveLayer: CAShapeLayer { + let pointsCount: Int + let smoothness: CGFloat + + let minRandomness: CGFloat + let maxRandomness: CGFloat + + let minSpeed: CGFloat + let maxSpeed: CGFloat + + let minOffset: CGFloat + let maxOffset: CGFloat + + var level: CGFloat = 0 { + didSet { + guard self.minOffset > 0.0 else { + return + } + CATransaction.begin() + CATransaction.setDisableActions(true) + let lv = minOffset + (maxOffset - minOffset) * level + self.transform = CATransform3DMakeTranslation(0.0, lv * 16.0, 0.0) + CATransaction.commit() + } + } + + private var curveAnimation: DisplayLinkAnimator? + + + private var speedLevel: CGFloat = 0 + private var lastSpeedLevel: CGFloat = 0 + + + + private var transition: CGFloat = 0 { + didSet { + guard let currentPoints = currentPoints else { return } + self.path = CGPath.smoothCurve(through: currentPoints, length: bounds.width, smoothness: smoothness, curve: true) + } + } + + override var frame: CGRect { + didSet { + + if oldValue != frame { + CATransaction.begin() + CATransaction.setDisableActions(true) + self.position = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0) + self.bounds = self.bounds + CATransaction.commit() + } + + if self.frame.size != oldValue.size { + self.fromPoints = nil + self.toPoints = nil + self.curveAnimation = nil + self.animateToNewShape() + } + } + } + + + private var fromPoints: [CGPoint]? + private var toPoints: [CGPoint]? + + private var currentPoints: [CGPoint]? { + guard let fromPoints = fromPoints, let toPoints = toPoints else { return nil } + + return fromPoints.enumerated().map { offset, fromPoint in + let toPoint = toPoints[offset] + return CGPoint( + x: fromPoint.x + (toPoint.x - fromPoint.x) * transition, + y: fromPoint.y + (toPoint.y - fromPoint.y) * transition + ) + } + } + + init( + pointsCount: Int, + minRandomness: CGFloat, + maxRandomness: CGFloat, + minSpeed: CGFloat, + maxSpeed: CGFloat, + minOffset: CGFloat, + maxOffset: CGFloat + ) { + self.pointsCount = pointsCount + self.minRandomness = minRandomness + self.maxRandomness = maxRandomness + self.minSpeed = minSpeed + self.maxSpeed = maxSpeed + self.minOffset = minOffset + self.maxOffset = maxOffset + + self.smoothness = 0.35 + + super.init() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + func setColor(_ color: NSColor) { + self.fillColor = color.cgColor + } + + func updateSpeedLevel(to newSpeedLevel: CGFloat) { + speedLevel = max(speedLevel, newSpeedLevel) + } + + func startAnimating() { + animateToNewShape() + } + + func stopAnimating() { + fromPoints = currentPoints + toPoints = nil + self.curveAnimation?.invalidate() + curveAnimation = nil + } + + private func animateToNewShape() { + + if curveAnimation != nil { + fromPoints = currentPoints + toPoints = nil + curveAnimation = nil + } + + if fromPoints == nil { + fromPoints = generateNextCurve(for: bounds.size) + } + if toPoints == nil { + toPoints = generateNextCurve(for: bounds.size) + } + + + let duration = CGFloat(1 / (minSpeed + (maxSpeed - minSpeed) * speedLevel)) + let fromValue: CGFloat = 0 + let toValue: CGFloat = 1 + + let animation = DisplayLinkAnimator(duration: Double(duration), from: fromValue, to: toValue, update: { [weak self] value in + self?.transition = value + }, completion: { [weak self] in + guard let `self` = self else { + return + } + self.fromPoints = self.currentPoints + self.toPoints = nil + self.curveAnimation = nil + self.animateToNewShape() + }) + self.curveAnimation = animation + + lastSpeedLevel = speedLevel + speedLevel = 0 + } + + private func generateNextCurve(for size: CGSize) -> [CGPoint] { + let randomness = minRandomness + (maxRandomness - minRandomness) * speedLevel + return curve(pointsCount: pointsCount, randomness: randomness).map { + return CGPoint(x: $0.x * CGFloat(size.width), y: size.height - 18.0 + $0.y * 12.0) + } + } + + private func curve(pointsCount: Int, randomness: CGFloat) -> [CGPoint] { + let segment = 1.0 / CGFloat(pointsCount - 1) + + let rgen = { () -> CGFloat in + let accuracy: UInt32 = 1000 + let random = arc4random_uniform(accuracy) + return CGFloat(random) / CGFloat(accuracy) + } + let rangeStart: CGFloat = 1.0 / (1.0 + randomness / 10.0) + + let points = (0 ..< pointsCount).map { i -> CGPoint in + let randPointOffset = (rangeStart + CGFloat(rgen()) * (1 - rangeStart)) / 2 + let segmentRandomness: CGFloat = randomness + + let pointX: CGFloat + let pointY: CGFloat + let randomXDelta: CGFloat + if i == 0 { + pointX = 0.0 + pointY = 0.0 + randomXDelta = 0.0 + } else if i == pointsCount - 1 { + pointX = 1.0 + pointY = 0.0 + randomXDelta = 0.0 + } else { + pointX = segment * CGFloat(i) + pointY = ((segmentRandomness * CGFloat(arc4random_uniform(100)) / CGFloat(100)) - segmentRandomness * 0.5) * randPointOffset + randomXDelta = segment - segment * randPointOffset + } + + return CGPoint(x: pointX + randomXDelta, y: pointY) + } + + return points + } + +} + + +protocol CallStatusBarBackground : View { + var audioLevel: Float { get set } + var speaking:(Bool, Bool, Bool)? { get set } +} + + +class CallHeaderBasicView : NavigationHeaderView { + + + private let _backgroundView: CallStatusBarBackground + + var backgroundView: CallStatusBarBackground { + return _backgroundView + } + + private let container = View() + + fileprivate let callInfo:TitleButton = TitleButton() + fileprivate let endCall:ImageButton = ImageButton() + fileprivate let statusTextView:TextView = TextView() + fileprivate let muteControl:ImageButton = ImageButton() + fileprivate let capView = View() + let disposable = MetaDisposable() + let hideDisposable = MetaDisposable() + + + override func hide(_ animated: Bool) { + super.hide(true) + disposable.set(nil) + hideDisposable.set(nil) + } + + private var statusTimer: SwiftSignalKit.Timer? + + + var status: CallControllerStatusValue = .text("", nil) { + didSet { + if self.status != oldValue { + self.statusTimer?.invalidate() + + if self.status.hasTimer == true { + self.statusTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in + self?.updateStatus() + }, queue: Queue.mainQueue()) + self.statusTimer?.start() + self.updateStatus() + } else { + self.updateStatus() + } + } + } + } + + private func updateStatus(animated: Bool = true) { + var statusText: String = "" + switch self.status { + case let .text(text, _): + statusText = text + case let .timer(referenceTime, _): + let duration = Int32(CFAbsoluteTimeGetCurrent() - referenceTime) + 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]) + } + statusText = durationString + case let .startsIn(time): + statusText = L10n.chatHeaderVoiceChatStartsIn(timerText(time - Int(Date().timeIntervalSince1970))) + } + let layout = TextViewLayout.init(.initialize(string: statusText, color: .white, font: .normal(13))) + layout.measure(width: .greatestFiniteMagnitude) + self.statusTextView.update(layout) + needsLayout = true } deinit { - stateDisposable.dispose() - durationDisposable.dispose() + disposable.dispose() + hideDisposable.dispose() } override init(_ header: NavigationHeader) { + if #available(OSX 10.12, *) { + self._backgroundView = CallStatusBarBackgroundView() + } else { + self._backgroundView = CallStatusBarBackgroundViewLegacy() + } super.init(header) backgroundView.frame = bounds backgroundView.wantsLayer = true + addSubview(capView) addSubview(backgroundView) - - durationView.font = .normal(.text) - durationView.drawsBackground = false - durationView.backgroundColor = .clear - durationView.isSelectable = false - durationView.isEditable = false - durationView.isBordered = false - durationView.focusRingType = .none - durationView.maximumNumberOfLines = 1 - - addSubview(durationView) - - + addSubview(container) + statusTextView.backgroundColor = .clear + statusTextView.userInteractionEnabled = false + statusTextView.isSelectable = false callInfo.set(font: .medium(.text), for: .Normal) callInfo.disableActions() - addSubview(callInfo) + container.addSubview(callInfo) callInfo.userInteractionEnabled = false - endCall.set(font: .medium(.text), for: .Normal) endCall.disableActions() - addSubview(endCall) - - dropCall.autohighlight = false - - addSubview(dropCall) + container.addSubview(endCall) + endCall.scaleOnClick = true + muteControl.scaleOnClick = true + + container.addSubview(statusTextView) + callInfo.set(handler: { [weak self] _ in - if let session = self?.session { - showPhoneCallWindow(session) - self?.hide() - } - }, for: .Click) - - dropCall.set(handler: { [weak self] _ in - self?.session?.hangUpCurrentCall() + self?.showInfoWindow() }, for: .Click) + endCall.set(handler: { [weak self] _ in - self?.session?.hangUpCurrentCall() + self?.hangUp() }, for: .Click) muteControl.autohighlight = false - addSubview(muteControl) + container.addSubview(muteControl) - muteControl.set(handler: { [weak self] control in - if let session = self?.session { - session.toggleMute() - self?.updateMutedBg(session, animated: true) - } + muteControl.set(handler: { [weak self] _ in + self?.toggleMute() }, for: .Click) - updateLocalizationAndTheme() + updateLocalizationAndTheme(theme: theme) } - private var blueColor:NSColor { - return theme.colors.blueSelect + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } - private var grayColor:NSColor { - return theme.colors.grayText + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") } - private func updateMutedBg(_ session:PCallSession, animated: Bool) { - backgroundView.background = session.isMute ? grayColor : blueColor - if animated { - backgroundView.layer?.animateBackground() + + func toggleMute() { + + } + func showInfoWindow() { + + } + func hangUp() { + + } + + func setInfo(_ text: String) { + self.callInfo.set(text: text, for: .Normal) + } + func setMicroIcon(_ image: CGImage) { + muteControl.set(image: image, for: .Normal) + _ = muteControl.sizeToFit() + } + + override func mouseUp(with event: NSEvent) { + super.mouseUp(with: event) + let point = self.convert(event.locationInWindow, from: nil) + if let header = header, point.y <= header.height { + showInfoWindow() } - muteControl.set(image: !session.isMute ? theme.icons.callInlineUnmuted : theme.icons.callInlineMuted, for: .Normal) - muteControl.sizeToFit() - needsLayout = true + } + + var blueColor:NSColor { + return theme.colors.accentSelect + } + var grayColor:NSColor { + return theme.colors.grayText + } + + func getEndText() -> String { + return L10n.callHeaderEndCall } override func layout() { super.layout() + capView.frame = NSMakeRect(0, 0, frame.width, height) + backgroundView.frame = bounds - muteControl.centerY(x:20) - durationView.centerY(x: muteControl.frame.maxX + 6) - callInfo.center() - dropCall.centerY(x: frame.width - dropCall.frame.width - 20) - endCall.centerY(x: dropCall.frame.minX - 6 - endCall.frame.width) - callInfo.sizeToFit(NSZeroSize, NSMakeSize(frame.width - durationView.frame.maxX - endCall.frame.width - 90, callInfo.frame.height), thatFit: true) - callInfo.center() + container.frame = NSMakeRect(0, 0, frame.width, height) + muteControl.centerY(x:18) + statusTextView.centerY(x: muteControl.frame.maxX + 6) + endCall.centerY(x: frame.width - endCall.frame.width - 20) + _ = callInfo.sizeToFit(NSZeroSize, NSMakeSize(frame.width - statusTextView.frame.width - 60 - 20 - endCall.frame.width - 10, callInfo.frame.height), thatFit: false) + + let rect = container.focus(callInfo.frame.size) + callInfo.setFrameOrigin(NSMakePoint(max(statusTextView.frame.maxX + 10, min(rect.minX, endCall.frame.minX - 10 - callInfo.frame.width)), rect.minY)) + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) + self.capView.backgroundColor = theme.colors.background + endCall.set(image: theme.icons.callInlineDecline, for: .Normal) + endCall.set(image: theme.icons.callInlineDecline, for: .Highlight) + _ = endCall.sizeToFit(NSMakeSize(10, 10), thatFit: false) + callInfo.set(color: .white, for: .Normal) + + needsLayout = true + + } + +} + +class CallNavigationHeaderView: CallHeaderBasicView { + + var session: PCallSession? { + get { + self.header?.contextObject as? PCallSession + } + } + private let audioLevelDisposable = MetaDisposable() + + deinit { + audioLevelDisposable.dispose() + } + + fileprivate weak var accountPeer: Peer? + fileprivate var state: CallState? + + override func showInfoWindow() { + if let session = self.session { + showCallWindow(session) + } + } + override func hangUp() { + self.session?.hangUpCurrentCall() } + override func toggleMute() { + session?.toggleMute() + } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() + override func update(with contextObject: Any) { + super.update(with: contextObject) + let session = contextObject as! PCallSession + let account = session.account + let signal = Signal.single(session.peer) |> then(session.account.postbox.loadedPeerWithId(session.peerId) |> map(Optional.init) |> deliverOnMainQueue) + + let accountPeer: Signal = session.sharedContext.activeAccounts |> mapToSignal { accounts in + if accounts.accounts.count == 1 { + return .single(nil) + } else { + return account.postbox.loadedPeerWithId(account.peerId) |> map(Optional.init) + } + } + + disposable.set(combineLatest(queue: .mainQueue(), session.state, signal, accountPeer).start(next: { [weak self] state, peer, accountPeer in + if let peer = peer { + self?.setInfo(peer.displayTitle) + } + self?.updateState(state, accountPeer: accountPeer, animated: false) + self?.needsLayout = true + self?.ready.set(.single(true)) + })) - dropCall.set(image: theme.icons.callInlineDecline, for: .Normal) - dropCall.sizeToFit() - endCall.set(text: tr(.callHeaderEndCall), for: .Normal) - endCall.sizeToFit(NSZeroSize, NSMakeSize(80, 20), thatFit: true) - durationView.textColor = .white - callInfo.set(color: .white, for: .Normal) - endCall.set(color: .white, for: .Normal) + audioLevelDisposable.set((session.audioLevel |> deliverOnMainQueue).start(next: { [weak self] value in + self?.backgroundView.audioLevel = value + })) + + hideDisposable.set((session.canBeRemoved |> deliverOnMainQueue).start(next: { [weak self] value in + if value { + self?.hide(true) + } + })) + } + + private func updateState(_ state:CallState, accountPeer: Peer?, animated: Bool) { + self.state = state + self.status = state.state.statusText(accountPeer, state.videoState) + var isConnected: Bool = false + let isMuted = state.isMuted + switch state.state { + case .active: + isConnected = true + default: + isConnected = false + } + self.backgroundView.speaking = (isConnected && !isMuted, isConnected, true) + if animated { + backgroundView.layer?.animateBackground() + } + setMicroIcon(!state.isMuted ? theme.icons.callInlineUnmuted : theme.icons.callInlineMuted) + needsLayout = true - if let session = session { - updateMutedBg(session, animated: false) + switch state.state { + case let .terminated(_, reason, _): + if let reason = reason, reason.recall { + + } else { + muteControl.removeAllHandlers() + endCall.removeAllHandlers() + callInfo.removeAllHandlers() + muteControl.change(opacity: 0.8, animated: animated) + endCall.change(opacity: 0.8, animated: animated) + statusTextView._change(opacity: 0.8, animated: animated) + callInfo._change(opacity: 0.8, animated: animated) + } + default: + break } + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) - needsLayout = true - + if let state = state { + self.updateState(state, accountPeer: accountPeer, animated: false) + } } required init(frame frameRect: NSRect) { @@ -198,6 +880,10 @@ class CallNavigationHeaderView: NavigationHeaderView { fatalError("init(coder:) has not been implemented") } + override init(_ header: NavigationHeader) { + super.init(header) + } + override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) @@ -205,3 +891,4 @@ class CallNavigationHeaderView: NavigationHeaderView { } } + diff --git a/Telegram-Mac/CallRatingModalViewController.swift b/Telegram-Mac/CallRatingModalViewController.swift index 90b18a09ac..32f55bd8fd 100644 --- a/Telegram-Mac/CallRatingModalViewController.swift +++ b/Telegram-Mac/CallRatingModalViewController.swift @@ -8,90 +8,81 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac +import TelegramCore + +import SwiftSignalKit +import Postbox private enum CallRatingState { case stars - case feedback } private class CallRatingModalView: View { let rating:View = View() + let textView = TextView() var starsChangeHandler:((Int32?)->Void)? = nil private(set) var stars:Int32? = nil var state:CallRatingState = .stars { didSet { if oldValue != state { - feedback.setString("", animated: true) - updateState(state, animated: true) } } } - let feedback:TGModernGrowingTextView = TGModernGrowingTextView() required init(frame frameRect: NSRect) { super.init(frame: frameRect) var x:CGFloat = 0 for i in 0 ..< 5 { let star = ImageButton() - star.set(image: #imageLiteral(resourceName: "Icon_CallStar").precomposed(), for: .Normal) + star.set(image: #imageLiteral(resourceName: "Icon_CallStar").precomposed(theme.colors.accent), for: .Normal) star.sizeToFit() star.setFrameOrigin(x, 0) rating.addSubview(star) - x += floorToScreenPixels(star.frame.width) + 10 + x += floorToScreenPixels(backingScaleFactor, star.frame.width) + 10 star.set(handler: { [weak self] current in for j in 0 ... i { - (self?.rating.subviews[j] as? ImageButton)?.set(image: #imageLiteral(resourceName: "Icon_CallStar_Highlighted").precomposed(), for: .Normal) + (self?.rating.subviews[j] as? ImageButton)?.set(image: #imageLiteral(resourceName: "Icon_CallStar_Highlighted").precomposed(theme.colors.accent), for: .Normal) } for j in i + 1 ..< 5 { - (self?.rating.subviews[j] as? ImageButton)?.set(image: #imageLiteral(resourceName: "Icon_CallStar").precomposed(), for: .Normal) + (self?.rating.subviews[j] as? ImageButton)?.set(image: #imageLiteral(resourceName: "Icon_CallStar").precomposed(theme.colors.accent), for: .Normal) } - self?.state = i < 4 ? .feedback : .stars - self?.starsChangeHandler?( Int32(i + 1) ) + self?.state = .stars + delay(0.15, closure: { + self?.starsChangeHandler?( Int32(i + 1) ) + }) }, for: .Click) } - rating.setFrameSize(x - 10, floorToScreenPixels(rating.subviews[0].frame.height)) + rating.setFrameSize(x - 10, floorToScreenPixels(backingScaleFactor, rating.subviews[0].frame.height)) addSubview(rating) - rating.center() + addSubview(textView) - feedback.setPlaceholderAttributedString(NSAttributedString.initialize(string: tr(.callRatingModalPlaceholder), color: .grayText, font: .normal(.text)), update: false) + textView.isSelectable = false + textView.userInteractionEnabled = false - feedback.textFont = NSFont.normal(FontSize.text) - feedback.textColor = .text - feedback.linkColor = .link - feedback.max_height = 120 + updateState(.stars) - feedback.setFrameSize(NSMakeSize(rating.frame.width, 34)) + let layout = TextViewLayout(.initialize(string: L10n.callRatingModalText, color: theme.colors.text, font: .medium(.text)), alignment: .center) + layout.measure(width: frame.width - 60) - addSubview(feedback) + textView.update(layout) - updateState(.stars) + needsLayout = true } override func layout() { super.layout() - feedback.centerX(y: frame.height - feedback.frame.height - 10) + rating.centerX(y: frame.midY + 5) + textView.centerX(y: frame.midY - textView.frame.height - 5) } private func updateState(_ state:CallRatingState, animated: Bool = false) { switch state { case .stars: rating.change(pos: focus(rating.frame.size).origin, animated: animated) - feedback._change(opacity: 0, animated: animated, completion: { [weak self] completed in - if completed { - self?.feedback.isHidden = true - } - }) - case .feedback: - rating.change(pos: NSMakePoint(rating.frame.minX, 20), animated: animated) - feedback.isHidden = false - feedback._change(opacity: 1, animated: animated) } } @@ -104,14 +95,18 @@ private class CallRatingModalView: View { } } -class CallRatingModalViewController: ModalViewController, TGModernGrowingDelegate { - private let account:Account - private let report:ReportCallRating +class CallRatingModalViewController: ModalViewController { + + private let context:AccountContext + private let callId:CallId private var starsCount:Int32? = nil - private var comment:String = "" - init(_ account:Account, report:ReportCallRating) { - self.account = account - self.report = report + private let isVideo: Bool + private let userInitiated: Bool + init(_ context: AccountContext, callId:CallId, userInitiated: Bool, isVideo: Bool) { + self.context = context + self.callId = callId + self.isVideo = isVideo + self.userInitiated = userInitiated super.init(frame: NSMakeRect(0, 0, 260, 100)) bar = .init(height: 0) } @@ -126,70 +121,63 @@ class CallRatingModalViewController: ModalViewController, TGModernGrowingDelegat } override var modalInteractions: ModalInteractions? { - return ModalInteractions(acceptTitle: tr(.modalOK), accept: { [weak self] in - if let strongSelf = self, let stars = strongSelf.starsCount { - _ = rateCall(account: strongSelf.account, report: strongSelf.report, starsCount: stars, comment: strongSelf.comment).start() - } - self?.close() - }, cancelTitle: tr(.modalCancel), drawBorder: true, height: 40) - } - - func textViewHeightChanged(_ height: CGFloat, animated: Bool) { - modal?.resize(with:NSMakeSize(genericView.frame.width, genericView.feedback.frame.height + genericView.rating.frame.height + 40), animated: animated) + return ModalInteractions(acceptTitle: L10n.callRatingModalNotNow, drawBorder: true, height: 50, singleButton: true) } + override func becomeFirstResponder() -> Bool? { return true } - override func firstResponder() -> NSResponder? { - return genericView.feedback - } - - func textViewEnterPressed(_ event: NSEvent!) -> Bool { - if FastSettings.checkSendingAbility(for: event) { - return true - } - return false - } - - func textViewTextDidChange(_ string: String!) { - comment = string - } - - func textViewTextDidChangeSelectedRange(_ range: NSRange) { - - } - - func textViewDidPaste(_ pasteboard: NSPasteboard!) -> Bool { - return false - } - - func textViewSize() -> NSSize { - return NSMakeSize(genericView.feedback.frame.width, genericView.feedback.frame.height) - } - - func textViewIsTypingEnabled() -> Bool { - return true - } - - func maxCharactersLimit() -> Int32 { - return 200 - } + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - modal?.interactions?.updateEnables(false) } override func viewDidLoad() { super.viewDidLoad() - genericView.feedback.delegate = self - textViewHeightChanged(34, animated: false) genericView.starsChangeHandler = { [weak self] stars in - self?.modal?.interactions?.updateEnables(true) - self?.starsCount = stars + if let stars = stars { + self?.saveRating(Int(stars)) + } } readyOnce() } + + private func saveRating(_ starsCount: Int) { + self.close() + if starsCount < 4, let window = self.window { + showModal(with: CallFeedbackController(context: context, callId: callId, starsCount: starsCount, userInitiated: userInitiated, isVideo: isVideo), for: window) + } else { + let _ = rateCallAndSendLogs(context: context, callId: self.callId, starsCount: starsCount, comment: "", userInitiated: userInitiated, includeLogs: false).start() + } + } +} + + +func rateCallAndSendLogs(context: AccountContext, callId: CallId, starsCount: Int, comment: String, userInitiated: Bool, includeLogs: Bool) -> Signal { + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(4244000)) + + let rate = context.engine.calls.rateCall(callId: callId, starsCount: Int32(starsCount), comment: comment, userInitiated: userInitiated) + if includeLogs { + let id = arc4random64() + let name = "\(callId.id)_\(callId.accessHash).log.json" + let path = callLogsPath(account: context.account) + "/" + name + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: nil, attributes: [.FileName(fileName: name)]) + let message = EnqueueMessage.message(text: comment, attributes: [], mediaReference: .standalone(media: file), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil) + return rate + |> then(enqueueMessages(account: context.account, peerId: peerId, messages: [message]) + |> mapToSignal({ _ -> Signal in + return .single(Void()) + })) + } else if !comment.isEmpty { + return rate + |> then(enqueueMessages(account: context.account, peerId: peerId, messages: [.message(text: comment, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)]) + |> mapToSignal({ _ -> Signal in + return .single(Void()) + })) + } else { + return rate + } } diff --git a/Telegram-Mac/CallReceptionControl.swift b/Telegram-Mac/CallReceptionControl.swift new file mode 100644 index 0000000000..a98784854f --- /dev/null +++ b/Telegram-Mac/CallReceptionControl.swift @@ -0,0 +1,41 @@ +// +// CallReceptionControl.swift +// Telegram +// +// Created by Mikhail Filimonov on 13/08/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +class CallReceptionControl: View { + + var reception: Int32 = 4 { + didSet { + self.needsDisplay = true + } + } + + override func draw(_ layer: CALayer, in context: CGContext) { + super.draw(layer, in: context) + + context.setFillColor(NSColor.white.cgColor) + + let width: CGFloat = 3.0 + let spacing: CGFloat = 2.0 + + for i in 0 ..< 4 { + let height = 4.0 + 2.0 * CGFloat(i) + let rect = CGRect(x: bounds.minX + CGFloat(i) * (width + spacing), y: frame.height - height, width: width, height: height) + + if i >= reception { + context.setAlpha(0.4) + } + let path = NSBezierPath(roundedRect: rect, xRadius: 0.5, yRadius: 0.5) + context.addPath(path.cgPath) + context.fillPath() + } + } + +} diff --git a/Telegram-Mac/CallSettingsController.swift b/Telegram-Mac/CallSettingsController.swift new file mode 100644 index 0000000000..7466b3f8e5 --- /dev/null +++ b/Telegram-Mac/CallSettingsController.swift @@ -0,0 +1,162 @@ +// +// CallSettingsController.swift +// Telegram +// +// Created by Mikhail Filimonov on 06/10/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore +import SwiftSignalKit + +import Postbox +import TgVoipWebrtc + +final class CallSettingsArguments { + let sharedContext: SharedAccountContext + let toggleInputAudioDevice:(String?)->Void + let toggleOutputAudioDevice:(String?)->Void + let toggleInputVideoDevice:(String?)->Void + let finishCall:()->Void + init(sharedContext: SharedAccountContext, toggleInputAudioDevice: @escaping(String?)->Void, toggleOutputAudioDevice:@escaping(String?)->Void, toggleInputVideoDevice:@escaping(String?)->Void, finishCall:@escaping()->Void) { + self.sharedContext = sharedContext + self.toggleInputAudioDevice = toggleInputAudioDevice + self.toggleOutputAudioDevice = toggleOutputAudioDevice + self.toggleInputVideoDevice = toggleInputVideoDevice + self.finishCall = finishCall + } +} + +private let _id_input_camera = InputDataIdentifier("_id_input_camera") +private let _id_camera = InputDataIdentifier("_id_camera") +private let _id_input_audio = InputDataIdentifier("_id_input_audio") +private let _id_output_audio = InputDataIdentifier("_id_output_audio") +private let _id_micro = InputDataIdentifier("_id_micro") + +private func callSettingsEntries(settings: VoiceCallSettings, devices: IODevices, arguments: CallSettingsArguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId: Int32 = 0 + var index: Int32 = 0 + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + var cameraDevice = devices.camera.first(where: { $0.uniqueID == settings.cameraInputDeviceId }) + var microDevice = devices.audioInput.first(where: { $0.uniqueID == settings.audioInputDeviceId }) + + let activeCameraDevice: AVCaptureDevice? + if let cameraDevice = cameraDevice { + if cameraDevice.isConnected && !cameraDevice.isSuspended { + activeCameraDevice = cameraDevice + } else { + activeCameraDevice = nil + } + } else if settings.cameraInputDeviceId == nil { + activeCameraDevice = AVCaptureDevice.default(for: .video) + } else { + cameraDevice = devices.camera.first(where: { $0.isConnected && !$0.isSuspended }) + activeCameraDevice = cameraDevice + } + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.callSettingsCameraTitle), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_input_camera, data: .init(name: L10n.callSettingsInputText, color: theme.colors.text, type: .contextSelector(cameraDevice?.localizedName ?? L10n.callSettingsDeviceDefault, [SPopoverItem(L10n.callSettingsDeviceDefault, { + arguments.toggleInputVideoDevice(nil) + })] + devices.camera.map { value in + return SPopoverItem(value.localizedName, { + arguments.toggleInputVideoDevice(value.uniqueID) + }) + }), viewType: activeCameraDevice == nil ? .singleItem : .firstItem))) + index += 1 + + if let activeCameraDevice = activeCameraDevice { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_camera, equatable: InputDataEquatable(activeCameraDevice.uniqueID), comparable: nil, item: { initialSize, stableId -> TableRowItem in + return CameraPreviewRowItem(initialSize, stableId: stableId, device: activeCameraDevice, viewType: .lastItem) + })) + index += 1 + } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + let activeMicroDevice: AVCaptureDevice? + if let microDevice = microDevice { + if microDevice.isConnected && !microDevice.isSuspended { + activeMicroDevice = microDevice + } else { + activeMicroDevice = nil + } + } else if settings.audioInputDeviceId == nil { + activeMicroDevice = AVCaptureDevice.default(for: .audio) + } else { + microDevice = devices.audioInput.first(where: { $0.isConnected && !$0.isSuspended }) + activeMicroDevice = microDevice + } + + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.callSettingsInputTitle), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_input_audio, data: .init(name: L10n.callSettingsInputText, color: theme.colors.text, type: .contextSelector(microDevice?.localizedName ?? L10n.callSettingsDeviceDefault, [SPopoverItem(L10n.callSettingsDeviceDefault, { + arguments.toggleInputAudioDevice(nil) + })] + devices.audioInput.map { value in + return SPopoverItem(value.localizedName, { + arguments.toggleInputAudioDevice(value.uniqueID) + }) + }), viewType: activeMicroDevice == nil ? .singleItem : .firstItem))) + index += 1 + + if let activeMicroDevice = activeMicroDevice { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_micro, equatable: InputDataEquatable(activeMicroDevice.uniqueID), comparable: nil, item: { initialSize, stableId -> TableRowItem in + return MicrophonePreviewRowItem(initialSize, stableId: stableId, context: arguments.sharedContext, viewType: .lastItem) + })) + index += 1 + } + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + + + + +func CallSettingsController(sharedContext: SharedAccountContext) -> InputDataController { + + let devicesContext = sharedContext.devicesContext + + let arguments = CallSettingsArguments(sharedContext: sharedContext, toggleInputAudioDevice: { value in + _ = updateVoiceCallSettingsSettingsInteractively(accountManager: sharedContext.accountManager, { + $0.withUpdatedAudioInputDeviceId(value) + }).start() + }, toggleOutputAudioDevice: { value in + _ = updateVoiceCallSettingsSettingsInteractively(accountManager: sharedContext.accountManager, { + $0.withUpdatedAudioOutputDeviceId(value) + }).start() + }, toggleInputVideoDevice: { value in + _ = updateVoiceCallSettingsSettingsInteractively(accountManager: sharedContext.accountManager, { + $0.withUpdatedCameraInputDeviceId(value) + }).start() + }, finishCall: { + + }) + + let signal = combineLatest(voiceCallSettings(sharedContext.accountManager), devicesContext.signal) |> map { settings, devices in + return InputDataSignalValue(entries: callSettingsEntries(settings: settings, devices: devices, arguments: arguments)) + } + + let controller = InputDataController(dataSignal: signal, title: L10n.callSettingsTitle, hasDone: false) + + + controller.contextOject = combineLatest(requestCameraPermission(), requestMicrophonePermission()).start() + + return controller +} + + diff --git a/Telegram-Mac/CallSettingsModalController.swift b/Telegram-Mac/CallSettingsModalController.swift new file mode 100644 index 0000000000..4ad5777a6e --- /dev/null +++ b/Telegram-Mac/CallSettingsModalController.swift @@ -0,0 +1,36 @@ +// +// CallSettingsModalController.swift +// Telegram +// +// Created by Mikhail Filimonov on 21/02/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import SwiftSignalKit +import Postbox +import TGUIKit + + +func CallSettingsModalController(_ sharedContext: SharedAccountContext) -> InputDataModalController { + + var close: (()->Void)? = nil + + + let controller = CallSettingsController(sharedContext: sharedContext) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { + close?() + }) + + let modalController = InputDataModalController(controller) + + close = { [weak modalController] in + modalController?.close() + } + + return modalController + +} diff --git a/Telegram-Mac/CallStatusView.swift b/Telegram-Mac/CallStatusView.swift new file mode 100644 index 0000000000..0fc78a9dc4 --- /dev/null +++ b/Telegram-Mac/CallStatusView.swift @@ -0,0 +1,123 @@ +// +// CallStatusView.swift +// Telegram +// +// Created by Mikhail Filimonov on 13/08/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit + +enum CallControllerStatusValue: Equatable { + case text(String, Int32?) + case timer(Double, Int32?) + case startsIn(Int) + + var hasTimer: Bool { + switch self { + case .timer, .startsIn: + return true + default: + return false + } + } +} + + +class CallStatusView: View { + + + private var statusTimer: SwiftSignalKit.Timer? + + var status: CallControllerStatusValue = .text("", nil) { + didSet { + if self.status != oldValue { + self.statusTimer?.invalidate() + if case .timer = self.status { + self.statusTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in + self?.updateStatus() + }, queue: Queue.mainQueue()) + self.statusTimer?.start() + self.updateStatus() + } else { + self.updateStatus() + } + } + } + } + + private let statusTextView:TextView = TextView() + private let receptionView = CallReceptionControl(frame: NSMakeRect(0, 0, 24, 10)) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + statusTextView.userInteractionEnabled = false + statusTextView.isSelectable = false + statusTextView.disableBackgroundDrawing = true + addSubview(statusTextView) + addSubview(receptionView) + } + + override func layout() { + super.layout() + if receptionView.isHidden { + statusTextView.center() + } else { + receptionView.centerY(x: 0) + statusTextView.centerY(x: receptionView.frame.maxX) + } + + } + + func sizeThatFits(_ size: NSSize) -> NSSize { + if let layout = self.statusTextView.layout { + layout.measure(width: size.width) + statusTextView.update(layout) + return NSMakeSize(max(layout.layoutSize.width, 60) + 28, size.height) + } + return size + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + deinit { + statusTimer?.invalidate() + } + + func updateStatus() { + var statusText: String = "" + switch self.status { + case let .text(text, reception): + statusText = text + if let reception = reception { + self.receptionView.reception = reception + } + self.receptionView.isHidden = reception == nil + case let .timer(referenceTime, reception): + let duration = Int32(CFAbsoluteTimeGetCurrent() - referenceTime) + 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]) + } + statusText = durationString + if let reception = reception { + self.receptionView.reception = reception + } + self.receptionView.isHidden = reception == nil + case let .startsIn(time): + statusText = L10n.chatHeaderVoiceChatStartsIn(timerText(time - Int(Date().timeIntervalSince1970))) + self.receptionView.isHidden = true + } + let layout = TextViewLayout.init(.initialize(string: statusText, color: .white, font: .normal(18)), alignment: .center) + layout.measure(width: .greatestFiniteMagnitude) + self.statusTextView.update(layout) + needsLayout = true + } + +} diff --git a/Telegram-Mac/CallTooltip.swift b/Telegram-Mac/CallTooltip.swift new file mode 100644 index 0000000000..6fd9364213 --- /dev/null +++ b/Telegram-Mac/CallTooltip.swift @@ -0,0 +1,90 @@ +// +// CallTooltip.swift +// Telegram +// +// Created by Mikhail Filimonov on 14/08/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +imp + +private enum CallTooltipType : Int32 { + case cameraOff + case microOff + case batteryLow + + var icon: CGImage { + switch self { + case .cameraOff: + return theme.icons.call_tooltip_camera_off + case .microOff: + return theme.icons.call_tooltip_micro_off + case .batteryLow: + return theme.icons.call_tooltip_battery_low + } + } + func text(_ title: String) -> String { + switch self { + case .cameraOff: + return L10n.callToastCameraOff(title) + case .microOff: + return L10n.callToastMicroOff(title) + case .batteryLow: + return L10n.callToastLowBattery(title) + } + } +} + + +private final class CallTooltipView : Control { + private let textView: TextView = TextView() + private let icon: ImageView = ImageView() + + fileprivate var type: CallTooltipType? = nil + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(textView) + addSubview(icon) + + textView.disableBackgroundDrawing = true + textView.isSelectable = false + textView.userInteractionEnabled = false + + backgroundColor = NSColor.grayText.withAlphaComponent(0.7) + + } + + func update(type: CallTooltipType, icon: CGImage, text: String, maxWidth: CGFloat) { + + self.type = type + + self.icon.image = icon + self.icon.sizeToFit() + + let attr: NSAttributedString = .initialize(string: text, color: .white, font: .medium(.title)) + + let layout = TextViewLayout(attr, maximumNumberOfLines: 1) + layout.measure(width: maxWidth - 30 - icon.backingSize.width) + textView.update(layout) + + setFrameSize(NSMakeSize(30 + self.icon.frame.width + self.textView.frame.width, 26)) + layer?.cornerRadius = frame.height / 2 + + needsLayout = true + } + + + override func layout() { + super.layout() + icon.centerY(x: 10) + textView.centerY(x: icon.frame.maxX + 10) + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + diff --git a/Telegram-Mac/CallTooltipView.swift b/Telegram-Mac/CallTooltipView.swift new file mode 100644 index 0000000000..b95210631a --- /dev/null +++ b/Telegram-Mac/CallTooltipView.swift @@ -0,0 +1,93 @@ +// +// CallTooltip.swift +// Telegram +// +// Created by Mikhail Filimonov on 14/08/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +enum CallTooltipType : Int32 { + case cameraOff + case microOff + case batteryLow + + var icon: CGImage { + switch self { + case .cameraOff: + return theme.icons.call_tooltip_camera_off + case .microOff: + return theme.icons.call_tooltip_micro_off + case .batteryLow: + return theme.icons.call_tooltip_battery_low + } + } + func text(_ title: String) -> String { + switch self { + case .cameraOff: + return L10n.callToastCameraOff(title) + case .microOff: + return L10n.callToastMicroOff(title) + case .batteryLow: + return L10n.callToastLowBattery(title) + } + } +} + + +final class CallTooltipView : Control { + private let textView: TextView = TextView() + private let icon: ImageView = ImageView() + + fileprivate(set) var type: CallTooltipType? = nil + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(textView) + addSubview(icon) + + textView.disableBackgroundDrawing = true + textView.isSelectable = false + textView.userInteractionEnabled = false + + backgroundColor = NSColor.grayText.withAlphaComponent(0.7) + + // wantsLayer = true + // self.material = .light + // self.state = .active + } + + func update(type: CallTooltipType, icon: CGImage, text: String, maxWidth: CGFloat) { + + self.type = type + + self.icon.image = icon + self.icon.sizeToFit() + + let attr: NSAttributedString = .initialize(string: text, color: .white, font: .medium(.title)) + + let layout = TextViewLayout(attr, maximumNumberOfLines: 1) + layout.measure(width: maxWidth - 30 - icon.backingSize.width) + textView.update(layout) + + setFrameSize(NSMakeSize(30 + self.icon.frame.width + self.textView.frame.width, 26)) + layer?.cornerRadius = frame.height / 2 + + needsLayout = true + } + + + override func layout() { + super.layout() + icon.centerY(x: 10) + textView.centerY(x: icon.frame.maxX + 10) + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + diff --git a/Telegram-Mac/CallWindowController.swift b/Telegram-Mac/CallWindowController.swift new file mode 100644 index 0000000000..78db4c9901 --- /dev/null +++ b/Telegram-Mac/CallWindowController.swift @@ -0,0 +1,1450 @@ +// +// PhoneCallWindow.swift +// Telegram +// +// Created by keepcoder on 24/04/2017. +// Copyright © 2017 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit +import TgVoipWebrtc +import TelegramVoip + +private let defaultWindowSize = NSMakeSize(720, 560) + +extension CallState { + func videoIsAvailable(_ isVideo: Bool) -> Bool { + switch state { + case .active: + switch videoState { + case .notAvailable: + return false + default: + return true + } + case .ringing, .requesting, .connecting: + switch videoState { + case .notAvailable: + return false + default: + if isVideo { + return true + } else { + return false + } + } + + case .terminating, .terminated: + return false + default: + return true + } + } + + var muteIsAvailable: Bool { + switch state { + case .active: + return true + case .ringing, .requesting, .connecting: + return true + case .terminating, .terminated: + return false + default: + return true + } + } +} + +extension CallSessionTerminationReason { + var recall: Bool { + let recall:Bool + switch self { + case .ended(let reason): + switch reason { + case .busy: + recall = true + default: + recall = false + } + case .error(let reason): + switch reason { + case .disconnected: + recall = true + default: + recall = false + } + } + return recall + } +} + + +private class PhoneCallWindowView : View { + fileprivate let imageView:TransformImageView = TransformImageView() + fileprivate let controls:View = View() + fileprivate let backgroundView:Control = Control() + fileprivate let settings:ImageButton = ImageButton() + + let acceptControl:CallControl = CallControl(frame: .zero) + let declineControl:CallControl = CallControl(frame: .zero) + + private var tooltips: [CallTooltipView] = [] + private var displayToastsAfterTimestamp: Double? + + + + let b_Mute:CallControl = CallControl(frame: .zero) + let b_VideoCamera:CallControl = CallControl(frame: .zero) + let b_ScreenShare:CallControl = CallControl(frame: .zero) + + let muteControl:ImageButton = ImageButton() + private var textNameView: NSTextField = NSTextField() + + private var statusView: CallStatusView = CallStatusView(frame: .zero) + + private let secureTextView:TextView = TextView() + + fileprivate let incomingVideoView: IncomingVideoView + fileprivate let outgoingVideoView: OutgoingVideoView + private var outgoingVideoViewRequested: Bool = false + private var incomingVideoViewRequested: Bool = false + + private var imageDimension: NSSize? = nil + + private var basicControls: View = View() + + private var state: CallState? + + private let fetching = MetaDisposable() + + var updateIncomingAspectRatio:((Float)->Void)? = nil + + + private var outgoingAspectRatio: CGFloat = 0 + + required init(frame frameRect: NSRect) { + outgoingVideoView = OutgoingVideoView(frame: NSMakeRect(0, 0, frameRect.width, frameRect.height)) + incomingVideoView = IncomingVideoView(frame: NSMakeRect(0, 0, frameRect.width, frameRect.height)) + super.init(frame: frameRect) + addSubview(imageView) + + settings.set(image: theme.icons.call_screen_settings, for: .Normal) + settings.sizeToFit() + + imageView.layer?.contentsGravity = .resizeAspectFill + imageView.addSubview(incomingVideoView) + + addSubview(backgroundView) + + backgroundView.forceMouseDownCanMoveWindow = true + addSubview(outgoingVideoView) + + + + controls.isEventLess = true + basicControls.isEventLess = true + + + let shadow = NSShadow() + shadow.shadowBlurRadius = 4 + shadow.shadowColor = NSColor.black.withAlphaComponent(0.6) + shadow.shadowOffset = NSMakeSize(0, 0) + outgoingVideoView.shadow = shadow + + addSubview(controls) + controls.addSubview(basicControls) + + self.backgroundColor = NSColor(0x000000, 0.8) + + secureTextView.backgroundColor = .clear + secureTextView.isSelectable = false + secureTextView.userInteractionEnabled = false + addSubview(secureTextView) + + backgroundView.backgroundColor = NSColor(0x000000, 0.4) + backgroundView.frame = NSMakeRect(0, 0, frameRect.width, frameRect.height) + + + self.addSubview(textNameView) + self.addSubview(statusView) + + + controls.addSubview(acceptControl) + controls.addSubview(declineControl) + + textNameView.font = .medium(36) + textNameView.drawsBackground = false + textNameView.backgroundColor = .clear + textNameView.textColor = nightAccentPalette.text + textNameView.isSelectable = false + textNameView.isEditable = false + textNameView.isBordered = false + textNameView.focusRingType = .none + textNameView.maximumNumberOfLines = 1 + textNameView.alignment = .center + textNameView.cell?.truncatesLastVisibleLine = true + textNameView.lineBreakMode = .byTruncatingTail + + + imageView.setFrameSize(frameRect.size.width, frameRect.size.height) + + + acceptControl.updateWithData(CallControlData(text: L10n.callAccept, mode: .normal(.greenUI, theme.icons.callWindowAccept), iconSize: NSMakeSize(50, 50)), animated: false) + declineControl.updateWithData(CallControlData(text: L10n.callDecline, mode: .normal(.redUI, theme.icons.callWindowDecline), iconSize: NSMakeSize(50, 50)), animated: false) + + + basicControls.addSubview(b_VideoCamera) + basicControls.addSubview(b_ScreenShare) + basicControls.addSubview(b_Mute) + + + var start: NSPoint? = nil + var resizeOutgoingVideoDirection: OutgoingVideoView.ResizeDirection? = nil + outgoingVideoView.overlay.set(handler: { [weak self] control in + guard let `self` = self, let window = self.window, self.outgoingVideoView.frame != self.bounds else { + start = nil + return + } + start = self.convert(window.mouseLocationOutsideOfEventStream, from: nil) + resizeOutgoingVideoDirection = self.outgoingVideoView.runResizer(at: self.outgoingVideoView.convert(window.mouseLocationOutsideOfEventStream, from: nil)) + + + }, for: .Down) + + outgoingVideoView.overlay.set(handler: { [weak self] control in + guard let `self` = self, let window = self.window, let startPoint = start else { + return + } + + self.outgoingVideoView.isMoved = true + + let current = self.convert(window.mouseLocationOutsideOfEventStream, from: nil) + let difference = current - startPoint + + if let resizeDirection = resizeOutgoingVideoDirection { + let frame = self.outgoingVideoView.frame + let size: NSSize + let point: NSPoint + let value_w = difference.x + let value_h = difference.x * (frame.height / frame.width) + + switch resizeDirection { + case .topLeft: + size = NSMakeSize(frame.width - value_w, frame.height - value_h) + point = NSMakePoint(frame.minX + value_w, frame.minY + value_h) + case .topRight: + size = NSMakeSize(frame.width + value_w, frame.height + value_h) + point = NSMakePoint(frame.minX, frame.minY - value_h) + case .bottomLeft: + size = NSMakeSize(frame.width - value_w, frame.height - value_h) + point = NSMakePoint(frame.minX + value_w, frame.minY) + case .bottomRight: + size = NSMakeSize(frame.width + value_w, frame.height + value_h) + point = NSMakePoint(frame.minX, frame.minY) + } + + if point.x < 20 || + point.y < 20 || + (self.frame.width - (point.x + size.width)) < 20 || + (self.frame.height - (point.y + size.height)) < 20 || + size.width > (window.frame.width - 40) || + size.height > (window.frame.height - 40) { + return + } + if size.width < 50 || size.height < 50 { + return + } + self.outgoingVideoView.updateFrame(CGRect(origin: point, size: size), animated: false) + + } else { + self.outgoingVideoView.setFrameOrigin(self.outgoingVideoView.frame.origin + difference) + } + start = current + + }, for: .MouseDragging) + + outgoingVideoView.overlay.set(handler: { [weak self] control in + guard let `self` = self, let _ = start else { + return + } + + + let frame = self.outgoingVideoView.frame + var point = self.outgoingVideoView.frame.origin + + let size = frame.size + if (size.width + point.x) > self.frame.width - 20 { + point.x = self.frame.width - size.width - 20 + } else if point.x - 20 < 0 { + point.x = 20 + } + + if (size.height + point.y) > self.frame.height - 20 { + point.y = self.frame.height - size.height - 20 + } else if point.y - 20 < 0 { + point.y = 20 + } + + let updatedRect = CGRect(origin: point, size: size) + self.outgoingVideoView.updateFrame(updatedRect, animated: true) + + start = nil + resizeOutgoingVideoDirection = nil + }, for: .Up) + + + outgoingVideoView.frame = NSMakeRect(frame.width - outgoingVideoView.frame.width - 20, frame.height - 140 - outgoingVideoView.frame.height, outgoingVideoView.frame.width, outgoingVideoView.frame.height) + + } + + // func updateOutgoingAspectRatio(_ aspectRatio: CGFloat, animated: Bool) { + // if aspectRatio > 0, !outgoingVideoView.isEventLess, self.outgoingAspectRatio != aspectRatio { + // var rect = outgoingVideoView.frame + // let closest: CGFloat = 150 + // rect.size = NSMakeSize(floor(closest * aspectRatio), closest) + // + // let dif = outgoingVideoView.frame.size - rect.size + // + // rect.origin = rect.origin.offsetBy(dx: dif.width / 2, dy: dif.height / 2) + // + // if !outgoingVideoView.isMoved { + // let addition = max(0, CGFloat(tooltips.count) * 40 - 5) + // rect.origin = NSMakePoint(frame.width - rect.width - 20, frame.height - 140 - rect.height - addition) + // } + // + // outgoingVideoView.updateFrame(rect, animated: animated) + // } + // self.outgoingAspectRatio = aspectRatio + // } + // + + private func mainControlY(_ control: NSView) -> CGFloat { + return controls.frame.height - control.frame.height - 40 + } + + private func mainControlCenter(_ control: NSView) -> CGFloat { + return floorToScreenPixels(backingScaleFactor, (controls.frame.width - control.frame.width) / 2) + } + + private var previousFrame: NSRect = .zero + + override func layout() { + super.layout() + + settings.setFrameOrigin(NSMakePoint(frame.width - settings.frame.width - 5, 5)) + + + backgroundView.frame = bounds + imageView.frame = bounds + + incomingVideoView.frame = bounds + + NSLog("frame \(self.outgoingVideoView.frame)") + + if self.outgoingVideoView.videoView == nil { + self.outgoingVideoView.frame = bounds + } + + + textNameView.setFrameSize(NSMakeSize(controls.frame.width - 40, 45)) + textNameView.centerX(y: 50) + statusView.setFrameSize(statusView.sizeThatFits(NSMakeSize(controls.frame.width - 40, 25))) + statusView.centerX(y: textNameView.frame.maxY + 8) + + secureTextView.centerX(y: statusView.frame.maxY + 8) + + let controlsSize = NSMakeSize(frame.width, 220) + controls.frame = NSMakeRect(0, frame.height - controlsSize.height, controlsSize.width, controlsSize.height) + + basicControls.frame = controls.bounds + + guard let state = self.state else { + return + } + + if !outgoingVideoView.isViewHidden { + if outgoingVideoView.isEventLess { + let videoFrame = bounds + outgoingVideoView.updateFrame(videoFrame, animated: false) + } else { + var point = outgoingVideoView.frame.origin + let size = outgoingVideoView.frame.size + + if previousFrame.size != frame.size { + point.x += (frame.width - point.x) - (previousFrame.width - point.x) + point.y += (frame.height - point.y) - (previousFrame.height - point.y) + + point.x = max(min(frame.width - size.width - 20, point.x), 20) + point.y = max(min(frame.height - size.height - 20, point.y), 20) + } + + let videoFrame = NSMakeRect(point.x, point.y, size.width, size.height) + outgoingVideoView.updateFrame(videoFrame, animated: false) + } + } + + + switch state.state { + case .connecting, .active, .requesting, .terminating: + let activeViews = self.allActiveControlsViews + let restWidth = self.allControlRestWidth + var x: CGFloat = floor(restWidth / 2) + for activeView in activeViews { + activeView.setFrameOrigin(NSMakePoint(x, mainControlY(acceptControl))) + x += activeView.size.width + 45 + } + case let .terminated(_, reason, _): + if let reason = reason, reason.recall { + + let activeViews = self.activeControlsViews + let restWidth = self.controlRestWidth + var x: CGFloat = floor(restWidth / 2) + for activeView in activeViews { + activeView.setFrameOrigin(NSMakePoint(x, 0)) + x += activeView.size.width + 45 + } + + acceptControl.setFrameOrigin(frame.midX + 25, mainControlY(acceptControl)) + declineControl.setFrameOrigin(frame.midX - 25 - declineControl.frame.width, mainControlY(acceptControl)) + } else { + let activeViews = self.allActiveControlsViews + let restWidth = self.allControlRestWidth + var x: CGFloat = floor(restWidth / 2) + for activeView in activeViews { + activeView.setFrameOrigin(NSMakePoint(x, mainControlY(acceptControl))) + x += activeView.size.width + 45 + } + } + case .ringing: + declineControl.setFrameOrigin(frame.midX - 25 - declineControl.frame.width, mainControlY(declineControl)) + acceptControl.setFrameOrigin(frame.midX + 25, mainControlY(acceptControl)) + + let activeViews = self.activeControlsViews + + let restWidth = self.controlRestWidth + var x: CGFloat = floor(restWidth / 2) + for activeView in activeViews { + activeView.setFrameOrigin(NSMakePoint(x, 0)) + x += activeView.size.width + 45 + } + + case .waiting: + break + case .reconnecting(_, _, _): + break + } + + if let dimension = imageDimension { + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: dimension, boundingSize: self.imageView.frame.size, intrinsicInsets: NSEdgeInsets()) + self.imageView.set(arguments: arguments) + } + + + var y: CGFloat = self.controls.frame.minY - 40 + (self.allActiveControlsViews.first?.frame.minY ?? 0) + + for view in tooltips { + let x = focus(view.frame.size).minX + view.setFrameOrigin(NSMakePoint(x, y)) + y -= (view.frame.height + 10) + } + + + previousFrame = self.frame + } + + var activeControlsViews:[CallControl] { + return basicControls.subviews.filter { + !$0.isHidden + }.compactMap { $0 as? CallControl } + } + + var allActiveControlsViews: [CallControl] { + let values = basicControls.subviews.filter { + !$0.isHidden + }.compactMap { $0 as? CallControl } + return values + controls.subviews.filter { + $0 is CallControl && !$0.isHidden + }.compactMap { $0 as? CallControl } + } + + var controlRestWidth: CGFloat { + return controls.frame.width - CGFloat(activeControlsViews.count - 1) * 45 - CGFloat(activeControlsViews.count) * 50 + } + var allControlRestWidth: CGFloat { + return controls.frame.width - CGFloat(allActiveControlsViews.count - 1) * 45 - CGFloat(allActiveControlsViews.count) * 50 + } + + + func updateName(_ name:String) { + textNameView.stringValue = name + needsLayout = true + } + + + func updateControlsVisibility() { + if let state = state { + switch state.state { + case .active: + self.backgroundView.change(opacity: self.mouseInside() ? 1.0 : 0.0) + self.controls.change(opacity: self.mouseInside() ? 1.0 : 0.0) + self.textNameView._change(opacity: self.mouseInside() ? 1.0 : 0.0) + self.secureTextView._change(opacity: self.mouseInside() ? 1.0 : 0.0) + self.statusView._change(opacity: self.mouseInside() ? 1.0 : 0.0) + + for tooltip in tooltips { + tooltip.change(opacity: self.mouseInside() ? 1.0 : 0.0) + } + self.settings._change(opacity: self.mouseInside() ? 1.0 : 0.0) + + default: + self.backgroundView.change(opacity: 1.0) + self.controls.change(opacity: 1.0) + self.textNameView._change(opacity: 1.0) + self.secureTextView._change(opacity: 1.0) + self.statusView._change(opacity: 1.0) + self.settings._change(opacity: 1.0) + + for tooltip in tooltips { + tooltip.change(opacity: 1.0) + } + } + } + + } + + func updateState(_ state:CallState, session:PCallSession, outgoingCameraInitialized: CameraState, incomingCameraInitialized: CameraState, accountPeer: Peer?, peer: TelegramUser?, animated: Bool) { + + + var incomingCameraInitialized = incomingCameraInitialized + var outgoingCameraInitialized = outgoingCameraInitialized + + + + switch state.remoteVideoState { + case .active: + if !self.incomingVideoViewRequested { + self.incomingVideoViewRequested = true + self.incomingVideoView._cameraInitialized.set(.initializing) + incomingCameraInitialized = .initializing + session.makeIncomingVideoView(completion: { [weak self] view in + if let view = view, let `self` = self { + self.incomingVideoView.videoView = view + self.incomingVideoView.updateAspectRatio = self.updateIncomingAspectRatio + self.incomingVideoView.firstFrameHandler = { [weak self] in + self?.incomingVideoView.unhideView(animated: animated) + } + } + }) + } + case .inactive, .paused: + self.incomingVideoViewRequested = false + self.incomingVideoView.hideView(animated: animated) + self.incomingVideoView._cameraInitialized.set(.notInited) + incomingCameraInitialized = .notInited + } + + + switch state.videoState { + case let .active(possible): + if !self.outgoingVideoViewRequested { + self.outgoingVideoViewRequested = true + self.outgoingVideoView._cameraInitialized.set(.initializing) + outgoingCameraInitialized = .initializing + session.makeOutgoingVideoView(completion: { [weak self] view in + if let view = view, let `self` = self { + self.outgoingVideoView.videoView = (view, possible) + self.outgoingVideoView.firstFrameHandler = { [weak self] in + self?.outgoingVideoView.unhideView(animated: animated) + } + } + }) + } + case .inactive, .paused: + self.outgoingVideoViewRequested = false + self.outgoingVideoView.hideView(animated: animated) + self.outgoingVideoView._cameraInitialized.set(.notInited) + outgoingCameraInitialized = .notInited + default: + break + } + + let isMirrored = !state.isScreenCapture + self.outgoingVideoView.isMirrored = isMirrored + +// +// self.outgoingVideoView.videoView?.0?.setOnIsMirroredUpdated = { f in +// f(isMirrored) +// } + + let inputCameraIsActive: Bool + switch state.videoState { + case let .active(possible): + inputCameraIsActive = !state.isOutgoingVideoPaused && possible + default: + inputCameraIsActive = false + } + + if outgoingCameraInitialized == .initializing { + self.b_VideoCamera.updateEnabled(false, animated: animated) + self.b_ScreenShare.updateEnabled(false, animated: animated) + } else { + self.b_VideoCamera.updateEnabled(state.videoIsAvailable(session.isVideo), animated: animated) + self.b_ScreenShare.updateEnabled(state.videoIsAvailable(session.isVideo), animated: animated) + } + + let vcBg: CallControlData.Mode + if !inputCameraIsActive { + vcBg = .visualEffect(inputCameraIsActive ? theme.icons.callWindowVideoActive : theme.icons.callWindowVideo) + } else { + vcBg = .normal(.white, inputCameraIsActive ? theme.icons.callWindowVideoActive : theme.icons.callWindowVideo) + } + let mBg: CallControlData.Mode + if !state.isMuted { + mBg = .visualEffect(state.isMuted ? theme.icons.callWindowMuteActive : theme.icons.callWindowMute) + } else { + mBg = .normal(.white, state.isMuted ? theme.icons.callWindowMuteActive : theme.icons.callWindowMute) + } + + self.b_VideoCamera.updateWithData(CallControlData(text: L10n.callCamera, mode: vcBg, iconSize: NSMakeSize(50, 50)), animated: false) + + self.b_Mute.updateWithData(CallControlData(text: L10n.callMute, mode: mBg, iconSize: NSMakeSize(50, 50)), animated: false) + + self.b_Mute.updateEnabled(state.muteIsAvailable, animated: animated) + + self.b_VideoCamera.updateLoading(outgoingCameraInitialized == .initializing && !state.isScreenCapture, animated: animated) + self.b_VideoCamera.isHidden = !session.isVideoPossible + + + let ssBg: CallControlData.Mode + if !state.isScreenCapture { + ssBg = .visualEffect(state.isScreenCapture ? theme.icons.call_screen_sharing_active : theme.icons.call_screen_sharing) + } else { + ssBg = .normal(.white, state.isScreenCapture ? theme.icons.call_screen_sharing_active : theme.icons.call_screen_sharing) + } + self.b_ScreenShare.updateWithData(CallControlData(text: L10n.callScreen, mode: ssBg, iconSize: NSMakeSize(50, 50)), animated: false) + self.b_ScreenShare.updateLoading(outgoingCameraInitialized == .initializing && state.isScreenCapture, animated: animated) + + self.b_ScreenShare.isHidden = !session.isVideoPossible + + + + + + self.state = state + self.statusView.status = state.state.statusText(accountPeer, state.videoState) + + switch state.state { + case let .active(_, _, visual): + let layout = TextViewLayout(.initialize(string: ObjcUtils.callEmojies(visual), color: .black, font: .normal(16.0)), alignment: .center) + layout.measure(width: .greatestFiniteMagnitude) + let wasEmpty = secureTextView.layout == nil + secureTextView.update(layout) + if wasEmpty { + secureTextView.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + secureTextView.layer?.animateScaleSpring(from: 0.2, to: 1.0, duration: 0.4) + } + default: + break + } + + + + switch state.state { + case .active, .connecting, .requesting, .reconnecting, .waiting: + self.acceptControl.isHidden = true + let activeViews = self.allActiveControlsViews + let restWidth = self.allControlRestWidth + var x: CGFloat = floor(restWidth / 2) + for activeView in activeViews { + activeView._change(pos: NSMakePoint(x, mainControlY(acceptControl)), animated: animated, duration: 0.3, timingFunction: .spring) + x += activeView.size.width + 45 + } + + declineControl.updateWithData(CallControlData(text: L10n.callEnd, mode: .normal(.redUI, theme.icons.callWindowDeclineSmall), iconSize: NSMakeSize(50, 50)), animated: animated) + + case .ringing: + declineControl.updateWithData(CallControlData(text: L10n.callDecline, mode: .normal(.redUI, theme.icons.callWindowDeclineSmall), iconSize: NSMakeSize(50, 50)), animated: animated) + case .terminated(_, let reason, _): + if let reason = reason, reason.recall { + self.acceptControl.isHidden = false + + let activeViews = self.activeControlsViews + let restWidth = self.controlRestWidth + var x: CGFloat = floor(restWidth / 2) + for activeView in activeViews { + activeView._change(pos: NSMakePoint(x, 0), animated: animated, duration: 0.3, timingFunction: .spring) + x += activeView.size.width + 45 + } + acceptControl.updateWithData(CallControlData(text: L10n.callRecall, mode: .normal(.greenUI, theme.icons.callWindowAccept), iconSize: NSMakeSize(50, 50)), animated: animated) + + declineControl.updateWithData(CallControlData(text: L10n.callClose, mode: .normal(.redUI, theme.icons.callWindowCancel), iconSize: NSMakeSize(50, 50)), animated: animated) + + + acceptControl.change(pos: NSMakePoint(frame.midX + 25, mainControlY(acceptControl)), animated: animated, duration: 0.3, timingFunction: .spring) + declineControl.change(pos: NSMakePoint(frame.midX - 25 - declineControl.frame.width, mainControlY(acceptControl)), animated: animated, duration: 0.3, timingFunction: .spring) + + _ = activeControlsViews.map { + $0.updateEnabled(false, animated: animated) + } + + } else { + self.acceptControl.isHidden = true + + declineControl.updateWithData(CallControlData(text: L10n.callDecline, mode: .normal(.redUI, theme.icons.callWindowDeclineSmall), iconSize: NSMakeSize(50, 50)), animated: false) + + let activeViews = self.allActiveControlsViews + let restWidth = self.allControlRestWidth + var x: CGFloat = floor(restWidth / 2) + for activeView in activeViews { + activeView._change(pos: NSMakePoint(x, mainControlY(acceptControl)), animated: animated, duration: 0.3, timingFunction: .spring) + x += activeView.size.width + 45 + } + + _ = allActiveControlsViews.map { + $0.updateEnabled(false, animated: animated) + } + } + case .terminating: + let activeViews = self.activeControlsViews + let restWidth = self.controlRestWidth + var x: CGFloat = floor(restWidth / 2) + for activeView in activeViews { + activeView.setFrameOrigin(NSMakePoint(x, 0)) + x += activeView.size.width + 45 + } + + acceptControl.setFrameOrigin(frame.midX + 25, mainControlY(acceptControl)) + declineControl.setFrameOrigin(frame.midX - 25 - declineControl.frame.width, mainControlY(acceptControl)) + + _ = allActiveControlsViews.map { + $0.updateEnabled(false, animated: animated) + } + } + + incomingVideoView.setIsPaused(state.remoteVideoState == .paused, peer: peer, animated: animated) + + let wasEventLess = outgoingVideoView.isEventLess + + switch state.state { + case .ringing, .requesting, .terminating, .terminated: + outgoingVideoView.isEventLess = true + default: + switch state.videoState { + case .active: + outgoingVideoView.isEventLess = false + default: + outgoingVideoView.isEventLess = true + } + if state.remoteVideoState == .inactive || incomingCameraInitialized == .notInited || incomingCameraInitialized == .initializing { + outgoingVideoView.isEventLess = true + } + } + + + + var point = outgoingVideoView.frame.origin + var size = outgoingVideoView.frame.size + + if !outgoingVideoView.isEventLess { + if !self.outgoingVideoView.isMoved { + if outgoingAspectRatio > 0 { + size = NSMakeSize(150 * outgoingAspectRatio, 150) + } else { + size = OutgoingVideoView.defaultSize + } + let addition = max(0, CGFloat(tooltips.count) * 40 - 5) + size = NSMakeSize(floor(size.width), floor(size.height)) + point = NSMakePoint(frame.width - size.width - 20, frame.height - 140 - size.height - addition) + } + } else { + self.outgoingVideoView.isMoved = false + point = .zero + size = frame.size + } + let videoFrame = CGRect(origin: point, size: size) + if !outgoingVideoView.isViewHidden { + outgoingVideoView.updateFrame(videoFrame, animated: animated) + } + + if let peer = peer { + updatePeerUI(peer, session: session) + self.updateTooltips(state, session: session, peer: peer, animated: animated, updateOutgoingVideo: !wasEventLess && !outgoingVideoView.isEventLess) + } + + if videoFrame == bounds { + addSubview(backgroundView, positioned: .above, relativeTo: outgoingVideoView) + } else { + addSubview(backgroundView, positioned: .below, relativeTo: outgoingVideoView) + } + + addSubview(settings) + + + needsLayout = true + } + + private func updateTooltips(_ state:CallState, session:PCallSession, peer: TelegramUser, animated: Bool, updateOutgoingVideo: Bool) { + var tooltips: [CallTooltipType] = [] + + let maxWidth = defaultWindowSize.width - 40 + + if let displayToastsAfterTimestamp = self.displayToastsAfterTimestamp { + if CACurrentMediaTime() > displayToastsAfterTimestamp { + switch state.state { + case .active: + if state.remoteVideoState == .inactive { + switch state.videoState { + case .active: + tooltips.append(.cameraOff) + default: + break + } + } + if state.remoteAudioState == .muted { + tooltips.append(.microOff) + } + if state.remoteBatteryLevel == .low { + tooltips.append(.batteryLow) + } + default: + break + } + } + } else { + switch state.state { + case .active: + self.displayToastsAfterTimestamp = CACurrentMediaTime() + 2.0 + default: + break + } + } + + + let updated = self.tooltips + + let removeTips = updated.filter { value in + if let type = value.type { + return !tooltips.contains(type) + } + return true + } + + let updateTips = updated.filter { value in + if let type = value.type { + return tooltips.contains(type) + } + return false + } + + let newTips: [CallTooltipView] = tooltips.filter { tip -> Bool in + for view in updated { + if view.type == tip { + return false + } + } + return true + }.map { tip in + let view = CallTooltipView(frame: .zero) + view.update(type: tip, icon: tip.icon, text: tip.text(peer.compactDisplayTitle), maxWidth: maxWidth) + return view + } + + for updated in updateTips { + if let tip = updated.type { + updated.update(type: tip, icon: tip.icon, text: tip.text(peer.compactDisplayTitle), maxWidth: maxWidth) + } + } + + for view in removeTips { + if animated { + view.layer?.animateScaleCenter(from: 1, to: 0.3, duration: 0.2) + view.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, timingFunction: .spring, removeOnCompletion: false, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } else { + view.removeFromSuperview() + } + } + + var y: CGFloat = self.controls.frame.minY - 40 + (self.allActiveControlsViews.first?.frame.minY ?? 0) + + let sorted = (updateTips + newTips).sorted(by: { lhs, rhs in + return lhs.type!.rawValue < rhs.type!.rawValue + }) + + + for view in sorted { + let x = focus(view.frame.size).minX + if view.superview == nil { + addSubview(view) + view.layer?.animateScaleSpring(from: 0.3, to: 1.0, duration: 0.2) + view.layer?.animateAlpha(from: 0, to: 1, duration: 0.4, timingFunction: .spring) + } else { + if animated { + view.change(pos: NSMakePoint(x, y), animated: animated) + } + } + view.setFrameOrigin(NSMakePoint(x, y)) + + y -= (view.frame.height + 10) + } + + self.tooltips = sorted + + if !outgoingVideoView.isMoved && outgoingVideoView.savedFrame != bounds { + let addition = max(0, CGFloat(tooltips.count) * 40 - 5) + let size = self.outgoingVideoView.frame.size + let point = NSMakePoint(frame.width - size.width - 20, frame.height - 140 - size.height - addition) + self.outgoingVideoView.updateFrame(CGRect(origin: point, size: size), animated: animated) + } + } + + + private func updatePeerUI(_ user:TelegramUser, session: PCallSession) { + + let id = user.profileImageRepresentations.first?.resource.id.hashValue ?? Int(user.id.toInt64()) + + let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: MediaId.Id(id)), representations: user.profileImageRepresentations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + + if let dimension = user.profileImageRepresentations.last?.dimensions.size { + + self.imageDimension = dimension + + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: dimension, boundingSize: defaultWindowSize, intrinsicInsets: NSEdgeInsets()) + self.imageView.setSignal(signal: cachedMedia(media: media, arguments: arguments, scale: self.backingScaleFactor), clearInstantly: false) + self.imageView.setSignal(chatMessagePhoto(account: session.account, imageReference: ImageMediaReference.standalone(media: media), peer: user, scale: self.backingScaleFactor), clearInstantly: false, animate: true, cacheImage: { result in + cacheMedia(result, media: media, arguments: arguments, scale: System.backingScale) + }) + self.imageView.set(arguments: arguments) + + if let reference = PeerReference(user) { + fetching.set(fetchedMediaResource(mediaBox: session.account.postbox.mediaBox, reference: .avatar(peer: reference, resource: media.representations.last!.resource)).start()) + } + + } else { + self.imageDimension = nil + self.imageView.setSignal(signal: generateEmptyRoundAvatar(self.imageView.frame.size, font: .avatar(90.0), account: session.account, peer: user) |> map { TransformImageResult($0, true) }) + } + self.updateName(user.displayTitle) + + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + fetching.dispose() + } +} + +class PhoneCallWindowController { + let window:Window + fileprivate var view:PhoneCallWindowView + + let updateLocalizationAndThemeDisposable = MetaDisposable() + fileprivate var session:PCallSession! { + didSet { + first = false + sessionDidUpdated() + + if let monitor = eventLocalMonitor { + NSEvent.removeMonitor(monitor) + } + if let monitor = eventGlobalMonitor { + NSEvent.removeMonitor(monitor) + } + + eventLocalMonitor = NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved, .mouseEntered, .mouseExited, .leftMouseDown, .leftMouseUp], handler: { [weak self] event in + self?.view.updateControlsVisibility() + return event + }) + // + eventGlobalMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved, .mouseEntered, .mouseExited, .leftMouseDown, .leftMouseUp], handler: { [weak self] event in + self?.view.updateControlsVisibility() + }) + + } + } + + var first:Bool = true + + private func sessionDidUpdated() { + + + let account = session.account + + let accountPeer: Signal = session.sharedContext.activeAccounts |> mapToSignal { accounts in + if accounts.accounts.count == 1 { + return .single(nil) + } else { + return account.postbox.loadedPeerWithId(account.peerId) |> map(Optional.init) + } + } + + let peer = session.account.viewTracker.peerView(session.peerId) |> map { + return $0.peers[$0.peerId] as? TelegramUser + } + + let outgoingCameraInitialized: Signal = .single(.notInited) |> then(view.outgoingVideoView.cameraInitialized) + let incomingCameraInitialized: Signal = .single(.notInited) |> then(view.incomingVideoView.cameraInitialized) + + stateDisposable.set(combineLatest(queue: .mainQueue(), session.state, accountPeer, peer, outgoingCameraInitialized, incomingCameraInitialized).start(next: { [weak self] state, accountPeer, peer, outgoingCameraInitialized, incomingCameraInitialized in + if let strongSelf = self { + strongSelf.applyState(state, session: strongSelf.session!, outgoingCameraInitialized: outgoingCameraInitialized, incomingCameraInitialized: incomingCameraInitialized, accountPeer: accountPeer, peer: peer, animated: !strongSelf.first) + strongSelf.first = false + + // strongSelf.updateOutgoingAspectRatio(state.remoteAspectRatio) + } + })) + + let signal = session.canBeRemoved |> deliverOnMainQueue + closeDisposable.set(signal.start(next: { value in + if value { + closeCall() + } + })) + } + private var state:CallState? = nil + private let disposable:MetaDisposable = MetaDisposable() + private let stateDisposable = MetaDisposable() + private let durationDisposable = MetaDisposable() + private let recallDisposable = MetaDisposable() + private let keyStateDisposable = MetaDisposable() + private let readyDisposable = MetaDisposable() + private let fullReadyDisposable = MetaDisposable() + + private var cameraInitialized: Promise = Promise() + + private let ready: Promise = Promise() + + fileprivate var eventLocalMonitor: Any? + fileprivate var eventGlobalMonitor: Any? + + + init(_ session:PCallSession) { + self.session = session + + let size = defaultWindowSize + if let screen = NSScreen.main { + self.window = Window(contentRect: NSMakeRect(floorToScreenPixels(System.backingScale, (screen.frame.width - size.width) / 2), floorToScreenPixels(System.backingScale, (screen.frame.height - size.height) / 2), size.width, size.height), styleMask: [.fullSizeContentView, .borderless, .resizable, .miniaturizable, .titled], backing: .buffered, defer: true, screen: screen) + self.window.minSize = NSMakeSize(400, 580) + self.window.isOpaque = true + self.window.backgroundColor = .black + } else { + fatalError("screen not found") + } + view = PhoneCallWindowView(frame: NSMakeRect(0, 0, size.width, size.height)) + + NotificationCenter.default.addObserver(self, selector: #selector(windowDidBecomeKey), name: NSWindow.didBecomeKeyNotification, object: window) + NotificationCenter.default.addObserver(self, selector: #selector(windowDidResignKey), name: NSWindow.didResignKeyNotification, object: window) + + view.acceptControl.set(handler: { [weak self] _ in + if let state = self?.state { + switch state.state { + case .ringing: + self?.session.acceptCallSession() + case .terminated(_, let reason, _): + if let reason = reason, reason.recall { + self?.recall() + } + default: + break + } + } + }, for: .Click) + + view.settings.set(handler: { [weak window, weak session] _ in + guard let window = window, let session = session else { + return + } + showModal(with: CallSettingsModalController(session.sharedContext), for: window) + }, for: .SingleClick) + + self.view.b_VideoCamera.set(handler: { [weak self] _ in + if let `self` = self, let callState = self.state { + switch callState.videoState { + case let .active(available), let .paused(available), let .inactive(available): + if available { + if callState.isOutgoingVideoPaused || callState.isScreenCapture || callState.videoState == .inactive(available) { + self.session.requestVideo() + } else { + self.session.disableVideo() + } + } else { + confirm(for: self.window, information: L10n.callCameraError, okTitle: L10n.modalOK, cancelTitle: "", thridTitle: L10n.requestAccesErrorConirmSettings, successHandler: { result in + switch result { + case .thrid: + openSystemSettings(.camera) + default: + break + } + }) + } + default: + break + } + } + }, for: .Click) + + self.view.b_ScreenShare.set(handler: { [weak self] _ in + guard let window = self?.window else { + return + } + let result = self?.session.toggleScreenCapture() + + if let result = result { + switch result { + case .permission: + confirm(for: window, information: L10n.callScreenError, okTitle: L10n.modalOK, cancelTitle: "", thridTitle: L10n.requestAccesErrorConirmSettings, successHandler: { result in + switch result { + case .thrid: + openSystemSettings(.sharing) + default: + break + } + }) + } + } + }, for: .Click) + + self.view.b_Mute.set(handler: { [weak self] _ in + if let session = self?.session { + session.toggleMute() + } + }, for: .Click) + + + view.declineControl.set(handler: { [weak self] _ in + if let state = self?.state { + switch state.state { + case let .terminated(_, reason, _): + if let reason = reason, reason.recall { + self?.session.setToRemovableState() + } + default: + self?.session.hangUpCurrentCall().start() + } + } else { + self?.session.setToRemovableState() + } + }, for: .Click) + + + self.window.contentView = view + self.window.titlebarAppearsTransparent = true + self.window.isMovableByWindowBackground = true + + sessionDidUpdated() + + + window.set(mouseHandler: { [weak self] _ -> KeyHandlerResult in + guard let `self` = self else {return .rejected} + self.view.updateControlsVisibility() + return .rejected + }, with: self.view, for: .mouseMoved) + + window.set(mouseHandler: { [weak self] _ -> KeyHandlerResult in + guard let `self` = self else {return .rejected} + self.view.updateControlsVisibility() + return .rejected + }, with: self.view, for: .mouseEntered) + + window.set(mouseHandler: { [weak self] _ -> KeyHandlerResult in + guard let `self` = self else {return .rejected} + self.view.updateControlsVisibility() + return .rejected + }, with: self.view, for: .mouseExited) + + + + window.onToggleFullScreen = { [weak self] value in + self?.view.incomingVideoView.videoView?.setVideoContentMode(.resizeAspect) + } + + + self.view.backgroundView.set(handler: { [weak self] _ in + self?.view.updateControlsVisibility() + }, for: .Click) + + window.animationBehavior = .utilityWindow + // updateIncomingAspectRatio(Float(System.cameraAspectRatio)) + } + + private func recall() { + recallDisposable.set((phoneCall(account: session.account, sharedContext: session.sharedContext, peerId: session.peerId, ignoreSame: true) |> deliverOnMainQueue).start(next: { [weak self] result in + switch result { + case let .success(session): + self?.session = session + case .fail: + break + case .samePeer: + break + } + })) + } + + private var incomingAspectRatio: Float = 0 + private func updateIncomingAspectRatio(_ aspectRatio: Float) { + if aspectRatio > 0 && self.incomingAspectRatio != aspectRatio, let screen = window.screen { + var closestSide: CGFloat + if aspectRatio > 1 { + closestSide = min(window.frame.width, window.frame.height) + } else { + closestSide = max(window.frame.width, window.frame.height) + } + + closestSide = max(400, closestSide) + + var updatedSize = NSMakeSize(floor(closestSide * CGFloat(aspectRatio)), closestSide) + + if screen.frame.width <= updatedSize.width || screen.frame.height <= updatedSize.height { + let closest = min(updatedSize.width, updatedSize.height) + updatedSize = NSMakeSize(floor(closest * CGFloat(aspectRatio)), closest) + } + + window.setFrame(CGRect(origin: window.frame.origin.offsetBy(dx: (window.frame.width - updatedSize.width) / 2, dy: (window.frame.height - updatedSize.height) / 2), size: updatedSize), display: true, animate: true) + window.aspectRatio = updatedSize + self.incomingAspectRatio = aspectRatio + } + } + + // private var outgoingAspectRatio: Float = 0 + // private func updateOutgoingAspectRatio(_ aspectRatio: Float) { + // if aspectRatio > 0 && self.outgoingAspectRatio != aspectRatio { + // self.outgoingAspectRatio = aspectRatio + // self.view.updateOutgoingAspectRatio(CGFloat(aspectRatio), animated: true) + // } + // } + + + @objc open func windowDidBecomeKey() { + keyStateDisposable.set(nil) + } + + @objc open func windowDidResignKey() { + keyStateDisposable.set((session.state |> deliverOnMainQueue).start(next: { [weak self] state in + if let strongSelf = self { + if case .active = state.state, !strongSelf.session.isVideo, !strongSelf.window.isKeyWindow { + switch state.videoState { + case .active, .paused: + break + default: + closeCall() + } + } + } + })) + } + + private func applyState(_ state:CallState, session: PCallSession, outgoingCameraInitialized: CameraState, incomingCameraInitialized: CameraState, accountPeer: Peer?, peer: TelegramUser?, animated: Bool) { + self.state = state + view.updateState(state, session: session, outgoingCameraInitialized: outgoingCameraInitialized, incomingCameraInitialized: incomingCameraInitialized, accountPeer: accountPeer, peer: peer, animated: animated) + session.sharedContext.showCall(with: session) + switch state.state { + case .ringing: + break + case .connecting: + break + case .requesting: + break + case .active: + break + case .terminating: + break + case .terminated(_, let error, _): + switch error { + case .ended(let reason)?: + break + case let .error(error)?: + disposable.set((session.account.postbox.loadedPeerWithId(session.peerId) |> deliverOnMainQueue).start(next: { peer in + switch error { + case .privacyRestricted: + alert(for: self.window, info: L10n.callPrivacyErrorMessage(peer.compactDisplayTitle)) + case .notSupportedByPeer: + alert(for: self.window, info: L10n.callParticipantVersionOutdatedError(peer.compactDisplayTitle)) + case .serverProvided(let serverError): + alert(for: self.window, info: serverError) + case .generic: + alert(for: self.window, info: L10n.callUndefinedError) + default: + break + } + })) + case .none: + break + } + case .waiting: + break + case .reconnecting: + break + } + self.ready.set(.single(true)) + + switch state.state { + case .terminating, .terminated: + self.window.styleMask.insert(.closable) + self.window.closeInterceptor = { [weak self] in + self?.session.setToRemovableState() + return true + } + case .waiting, .ringing: + self.window.closeInterceptor = { [weak self] in + self?.session.setToRemovableState() + return true + } + default: + self.window.styleMask.remove(.closable) + } + + } + + deinit { + cleanup() + } + + fileprivate func cleanup() { + disposable.dispose() + stateDisposable.dispose() + durationDisposable.dispose() + recallDisposable.dispose() + keyStateDisposable.dispose() + readyDisposable.dispose() + fullReadyDisposable.dispose() + updateLocalizationAndThemeDisposable.dispose() + NotificationCenter.default.removeObserver(self) + self.window.removeAllHandlers(for: self.view) + + _ = self.view.allActiveControlsViews.map { + $0.updateEnabled(false, animated: true) + } + } + + func show() { + let ready = self.ready.get() |> filter { $0 } |> take(1) + + readyDisposable.set(ready.start(next: { [weak self] _ in + if let `self` = self, self.window.isVisible == false { + self.window.makeKeyAndOrderFront(self) + self.window.orderFrontRegardless() + self.window.alphaValue = 0 + + let fullReady: Signal + if self.session.isVideo, self.session.isVideoPossible { + fullReady = self.view.outgoingVideoView.cameraInitialized + |> timeout(5.0, queue: .mainQueue(), alternate: .single(.inited)) + |> map { $0 == .inited } + |> filter { $0 } + |> take(1) + } else { + fullReady = .single(true) + } + + self.fullReadyDisposable.set(fullReady.start(next: { [weak self] _ in + self?.window.animator().alphaValue = 1 + self?.window.orderFrontRegardless() + self?.view.layer?.animateScaleSpring(from: 0.2, to: 1.0, duration: 0.4) + self?.view.layer?.animateAlpha(from: 0.2, to: 1.0, duration: 0.3) + })) + } else { + self?.window.makeKeyAndOrderFront(self) + self?.window.orderFrontRegardless() + } + })) + } +} + +private let controller:Atomic = Atomic(value: nil) +private let closeDisposable = MetaDisposable() + +func makeKeyAndOrderFrontCallWindow() -> Bool { + return controller.with { value in + if let value = value { + value.window.makeKeyAndOrderFront(nil) + value.window.orderFrontRegardless() + return true + } else { + return false + } + } +} + +func showCallWindow(_ session:PCallSession) { + _ = controller.modify { controller in + if session.peerId != controller?.session.peerId { + _ = controller?.session.hangUpCurrentCall().start() + if let controller = controller { + controller.session = session + return controller + } else { + return PhoneCallWindowController(session) + } + } + return controller + } + controller.with { $0?.show() } + +} + +func closeCall(minimisize: Bool = false) { + _ = controller.modify { controller in + if let controller = controller { + controller.cleanup() + let sharedContext = controller.session.sharedContext + let isVideo = controller.session.isVideo + _ = (controller.session.state |> take(1) |> deliverOnMainQueue).start(next: { [weak sharedContext] state in + switch state.state { + case let .terminated(callId, _, report): + if report, let callId = callId, let window = sharedContext?.bindings.rootNavigation().window { + let context = sharedContext?.bindings.getContext() + if let context = context { + showModal(with: CallRatingModalViewController(context, callId: callId, userInitiated: false, isVideo: isVideo), for: window) + } + } + default: + break + } + }) + if controller.window.isFullScreen { + controller.window.toggleFullScreen(nil) + delay(0.8, closure: { + NSAnimationContext.runAnimationGroup({ ctx in + controller.window.animator().alphaValue = 0 + }, completionHandler: { + controller.window.orderOut(nil) + }) + }) + } else { + NSAnimationContext.runAnimationGroup({ ctx in + controller.window.animator().alphaValue = 0 + }, completionHandler: { + controller.window.orderOut(nil) + }) + } + } + return nil + } +} + + +func applyUIPCallResult(_ sharedContext: SharedAccountContext, _ result:PCallResult) { + assertOnMainThread() + switch result { + case let .success(session): + showCallWindow(session) + case .fail: + break + case let .samePeer(session): + if let header = sharedContext.bindings.rootNavigation().callHeader, header.needShown { + showCallWindow(session) + } else { + controller.with { $0?.window.orderFront(nil) } + } + } +} diff --git a/Telegram-Mac/CameraPreviewRowItem.swift b/Telegram-Mac/CameraPreviewRowItem.swift new file mode 100644 index 0000000000..8bd555a15e --- /dev/null +++ b/Telegram-Mac/CameraPreviewRowItem.swift @@ -0,0 +1,79 @@ +// +// CameraPreviewRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 06/10/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit +import TelegramCore + +class CameraPreviewRowItem: GeneralRowItem { + fileprivate let device: AVCaptureDevice + fileprivate let session: AVCaptureSession + init(_ initialSize: NSSize, stableId: AnyHashable, device: AVCaptureDevice, viewType: GeneralViewType) { + self.device = device + self.session = AVCaptureSession() + let input = try? AVCaptureDeviceInput(device: device) + if let input = input { + self.session.addInput(input) + } + super.init(initialSize, height: 220, stableId: stableId, viewType: viewType) + + self.session.startRunning() + + } + deinit { + self.session.stopRunning() + } + + override func viewClass() -> AnyClass { + return CameraPreviewRowView.self + } +} + +private final class CameraPreviewRowView : GeneralContainableRowView { + private let captureLayer: AVCaptureVideoPreviewLayer = AVCaptureVideoPreviewLayer() + private let view = View() + private let progressIndicator = ProgressIndicator(frame: NSMakeRect(0, 0, 40, 40)) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(progressIndicator) + addSubview(view) + view.layer?.addSublayer(self.captureLayer) + } + + override func updateColors() { + super.updateColors() + progressIndicator.progressColor = theme.colors.text + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? CameraPreviewRowItem else { + return + } + + captureLayer.session = item.session + captureLayer.connection?.automaticallyAdjustsVideoMirroring = false + captureLayer.connection?.isVideoMirrored = true + captureLayer.videoGravity = .resizeAspectFill + + } + + override func layout() { + super.layout() + view.frame = containerView.bounds + captureLayer.frame = view.bounds + progressIndicator.center() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + diff --git a/Telegram-Mac/CameraViews.swift b/Telegram-Mac/CameraViews.swift new file mode 100644 index 0000000000..26797758e1 --- /dev/null +++ b/Telegram-Mac/CameraViews.swift @@ -0,0 +1,420 @@ +// +// CameraViews.swift +// Telegram +// +// Created by Mikhail Filimonov on 14/08/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit +import TelegramCore +import TelegramVoip + +enum CameraState : Equatable { + case notInited + case initializing + case inited +} + +final class OutgoingVideoView : Control { + + private var progressIndicator: ProgressIndicator? = nil + private let videoContainer = Control() + var isMirrored: Bool = false { + didSet { + CATransaction.begin() + if isMirrored { + let rect = videoContainer.bounds + var fr = CATransform3DIdentity + fr = CATransform3DTranslate(fr, rect.width / 2, 0, 0) + fr = CATransform3DScale(fr, -1, 1, 1) + fr = CATransform3DTranslate(fr, -(rect.width / 2), 0, 0) + videoContainer.layer?.sublayerTransform = fr + } else { + videoContainer.layer?.sublayerTransform = CATransform3DIdentity + } + + CATransaction.commit() + } + } + + var isMoved: Bool = false + + var updateAspectRatio:((Float)->Void)? = nil + + let _cameraInitialized: ValuePromise = ValuePromise(.notInited, ignoreRepeated: true) + + var cameraInitialized: Signal { + return _cameraInitialized.get() + } + + var firstFrameHandler:(()->Void)? = nil + + var videoView: (OngoingCallContextPresentationCallVideoView?, Bool)? { + didSet { + self._cameraInitialized.set(.initializing) + if let value = videoView, let videoView = value.0 { + + videoView.setVideoContentMode(.resizeAspectFill) + + videoContainer.addSubview(videoView.view) + videoView.view.frame = self.bounds + videoView.view.layer?.cornerRadius = .cornerRadius + + let oldView = oldValue?.0?.view + + videoView.setOnFirstFrameReceived({ [weak self, weak oldView] aspectRatio in + guard let `self` = self else { + return + } + self._cameraInitialized.set(.inited) + if !self._hidden { + self.backgroundColor = .clear + oldView?.removeFromSuperview() + if let progressIndicator = self.progressIndicator { + self.progressIndicator = nil + progressIndicator.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak progressIndicator] _ in + progressIndicator?.removeFromSuperview() + }) + } + self.updateAspectRatio?(aspectRatio) + } + self.firstFrameHandler?() + }) + + if !value.1 { + self._cameraInitialized.set(.inited) + } + } else { + self._cameraInitialized.set(.notInited) + } + + + needsLayout = true + } + } + + private var _hidden: Bool = false + + var isViewHidden: Bool { + return _hidden + } + + func unhideView(animated: Bool) { + if let view = videoView?.0?.view, _hidden { + subviews.enumerated().forEach { _, view in + if !(view is Control) { + view.removeFromSuperview() + } + } + videoContainer.addSubview(view, positioned: .below, relativeTo: self.subviews.first) + view.layer?.animateScaleCenter(from: 0.2, to: 1.0, duration: 0.2) + view.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + _hidden = false + } + + func hideView(animated: Bool) { + if let view = self.videoView?.0?.view, !_hidden { + view.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] completed in + view?.removeFromSuperview() + view?.layer?.removeAllAnimations() + }) + view.layer?.animateScaleCenter(from: 1, to: 0.2, duration: 0.2) + } + _hidden = true + } + + override var isEventLess: Bool { + didSet { + self.userInteractionEnabled = !isEventLess + //overlay.isEventLess = isEventLess + } + } + + + + static var defaultSize: NSSize = NSMakeSize(floor(100 * System.aspectRatio), 100) + + enum ResizeDirection { + case topLeft + case topRight + case bottomLeft + case bottomRight + } + + let overlay: Control = Control() + + + private var disabledView: NSVisualEffectView? + private var notAvailableView: TextView? + + private var animation:DisplayLinkAnimator? = nil + + + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + self.overlay.forceMouseDownCanMoveWindow = true + self.layer?.cornerRadius = .cornerRadius + self.layer?.masksToBounds = true + self.addSubview(videoContainer) + self.addSubview(overlay) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + + self.videoContainer.frame = bounds + self.overlay.frame = bounds + self.videoView?.0?.view.frame = bounds + self.progressIndicator?.center() + self.disabledView?.frame = bounds + let isMirrored = self.isMirrored + self.isMirrored = isMirrored + + if let textView = notAvailableView { + textView.resize(frame.width - 40) + textView.center() + } + } + + func setIsPaused(_ paused: Bool, animated: Bool) { + if paused { + if disabledView == nil { + let current = NSVisualEffectView() + current.material = .dark + current.state = .active + current.blendingMode = .withinWindow + current.wantsLayer = true + current.layer?.cornerRadius = .cornerRadius + current.frame = bounds + self.disabledView = current + self.addSubview(current, positioned: .below, relativeTo: overlay) + + if animated { + current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } else { + self.disabledView?.frame = bounds + } + } else { + if let disabledView = self.disabledView { + self.disabledView = nil + if animated { + disabledView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak disabledView] _ in + disabledView?.removeFromSuperview() + }) + } else { + disabledView.removeFromSuperview() + } + } + } + } + + override func setFrameSize(_ newSize: NSSize) { + super.setFrameSize(newSize) + } + private(set) var savedFrame: NSRect? = nil + + func updateFrame(_ frame: NSRect, animated: Bool) { + if self.savedFrame != frame && animation == nil { + let duration: Double = 0.15 + + if animated { + + let fromFrame = self.frame + let toFrame = frame + + + let animation = DisplayLinkAnimator(duration: duration, from: 0.0, to: 1.0, update: { [weak self] value in + let x = fromFrame.minX - (fromFrame.minX - toFrame.minX) * value + let y = fromFrame.minY - (fromFrame.minY - toFrame.minY) * value + let w = fromFrame.width - (fromFrame.width - toFrame.width) * value + let h = fromFrame.height - (fromFrame.height - toFrame.height) * value + let updated = NSMakeRect(x, y, w, h) + self?.frame = updated + }, completion: { [weak self] in + guard let `self` = self else { + return + } + self.animation = nil + self.frame = frame + self.savedFrame = frame + }) + self.animation = animation + } else { + self.frame = frame + self.animation = nil + } + } + updateCursorRects() + savedFrame = frame + } + + private func updateCursorRects() { + + } + + override func cursorUpdate(with event: NSEvent) { + super.cursorUpdate(with: event) + updateCursorRects() + } + + func runResizer(at point: NSPoint) -> ResizeDirection? { + let rects: [(NSRect, ResizeDirection)] = [(NSMakeRect(0, frame.height - 10, 10, 10), .bottomLeft), + (NSMakeRect(frame.width - 10, 0, 10, 10), .topRight), + (NSMakeRect(0, 0, 10, 10), .topLeft), + (NSMakeRect(frame.width - 10, frame.height - 10, 10, 10), .bottomRight)] + for rect in rects { + if NSPointInRect(point, rect.0) { + return rect.1 + } + } + return nil + } + + override var mouseDownCanMoveWindow: Bool { + return isEventLess + } +} + +final class IncomingVideoView : Control { + + var updateAspectRatio:((Float)->Void)? = nil + + let _cameraInitialized: ValuePromise = ValuePromise(.notInited, ignoreRepeated: true) + + var cameraInitialized: Signal { + return _cameraInitialized.get() + } + + var firstFrameHandler:(()->Void)? = nil + + private var disabledView: NSVisualEffectView? + var videoView: OngoingCallContextPresentationCallVideoView? { + didSet { + _cameraInitialized.set(.initializing) + + if let videoView = videoView { + + addSubview(videoView.view, positioned: .below, relativeTo: self.subviews.first) + videoView.view.background = .clear + + videoView.setOnFirstFrameReceived({ [weak self, weak oldValue] aspectRatio in + if let videoView = oldValue { + videoView.view.removeFromSuperview() + } + self?._cameraInitialized.set(.inited) + self?.videoView?.view.background = .black + self?.updateAspectRatio?(aspectRatio) + self?.firstFrameHandler?() + }) + } else { + _cameraInitialized.set(.notInited) + self.firstFrameHandler?() + } + needsLayout = true + } + } + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + self.layer?.cornerRadius = .cornerRadius + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + for subview in subviews { + subview.frame = bounds + } + + if let textView = disabledView?.subviews.first as? TextView { + let layout = textView.layout + layout?.measure(width: frame.width - 40) + textView.update(layout) + textView.center() + } + } + + func setIsPaused(_ paused: Bool, peer: TelegramUser?, animated: Bool) { + if paused { + if disabledView == nil { + let current = NSVisualEffectView() + current.material = .dark + current.state = .active + current.blendingMode = .withinWindow + current.wantsLayer = true + current.frame = bounds + + self.disabledView = current + addSubview(current) + + if animated { + current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } else { + self.disabledView?.frame = bounds + } + } else { + if let disabledView = self.disabledView { + self.disabledView = nil + if animated { + disabledView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak disabledView] _ in + disabledView?.removeFromSuperview() + }) + } else { + disabledView.removeFromSuperview() + } + } + } + needsLayout = true + } + + private var _hidden: Bool = false + + var isViewHidden: Bool { + return _hidden + } + + func unhideView(animated: Bool) { + if let view = videoView?.view, _hidden { + self.subviews.enumerated().forEach { _, view in + if !(view is Control) { + view.removeFromSuperview() + } + } + addSubview(view, positioned: .below, relativeTo: self.subviews.first) + view._change(opacity: 1, animated: animated) +// view.layer?.animateScaleCenter(from: 0.2, to: 1.0, duration: 0.2) + } + _hidden = false + } + + func hideView(animated: Bool) { + if let view = self.videoView?.view, !_hidden { + view._change(opacity: 1, animated: animated, removeOnCompletion: false, completion: { [weak view] completed in + view?.removeFromSuperview() + view?.layer?.removeAllAnimations() + }) + // view.layer?.animateScaleCenter(from: 1, to: 0.2, duration: 0.2) + } + _hidden = true + } + + + override var mouseDownCanMoveWindow: Bool { + return true + } +} + diff --git a/Telegram-Mac/CancelResetAccountController.swift b/Telegram-Mac/CancelResetAccountController.swift new file mode 100644 index 0000000000..705e4ae04b --- /dev/null +++ b/Telegram-Mac/CancelResetAccountController.swift @@ -0,0 +1,331 @@ +// +// CancelResetAccountController.swift +// Telegram +// +// Created by Mikhail Filimonov on 25/12/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa + + +import Cocoa +import TGUIKit +import SwiftSignalKit +import TelegramCore + + +private let _id_input_code = InputDataIdentifier("_id_input_code") + +private struct CancelResetAccountState : Equatable { + let code: String + let error: InputDataValueError? + let checking: Bool + let limit: Int32 + init(code: String, error: InputDataValueError?, checking: Bool, limit: Int32) { + self.code = code + self.error = error + self.checking = checking + self.limit = limit + } + func withUpdatedCode(_ code: String) -> CancelResetAccountState { + return CancelResetAccountState(code: code, error: self.error, checking: self.checking, limit: self.limit) + } + func withUpdatedError(_ error: InputDataValueError?) -> CancelResetAccountState { + return CancelResetAccountState(code: self.code, error: error, checking: self.checking, limit: self.limit) + } + func withUpdatedChecking(_ checking: Bool) -> CancelResetAccountState { + return CancelResetAccountState(code: self.code, error: self.error, checking: checking, limit: self.limit) + } + func withUpdatedCodeLimit(_ limit: Int32) -> CancelResetAccountState { + return CancelResetAccountState(code: self.code, error: self.error, checking: self.checking, limit: limit) + } +} + + + + + +func authorizationNextOptionText(currentType: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, timeout: Int32?) -> (current: String, next: String, codeLength: Int) { + + var codeLength: Int = 255 + var basic: String = "" + var nextText: String = "" + + switch currentType { + case let .otherSession(length: length): + codeLength = Int(length) + basic = L10n.loginEnterCodeFromApp + nextText = L10n.loginSendSmsIfNotReceivedAppCode + case let .sms(length: length): + codeLength = Int(length) + basic = L10n.loginJustSentSms + case let .call(length: length): + codeLength = Int(length) + basic = L10n.loginPhoneCalledCode + default: + break + } + + + if let nextType = nextType { + if let timeout = timeout { + let timeout = Int(timeout) + let minutes = timeout / 60; + let sec = timeout % 60; + let secValue = sec > 9 ? "\(sec)" : "0\(sec)" + if timeout > 0 { + switch nextType { + case .call: + nextText = L10n.loginWillCall(minutes, secValue) + break + case .sms: + nextText = L10n.loginWillSendSms(minutes, secValue) + break + default: + break + } + } else { + switch nextType { + case .call: + basic = L10n.loginPhoneCalledCode + nextText = L10n.loginPhoneDialed + break + default: + break + } + } + + } else { + nextText = L10n.loginSendSmsIfNotReceivedAppCode + } + } + + return (current: basic, next: nextText, codeLength: codeLength) +} + + +private func timeoutSignal(codeData: CancelAccountResetData) -> Signal { + if let _ = codeData.nextType, let timeout = codeData.timeout { + return Signal { subscriber in + let value = Atomic(value: timeout) + subscriber.putNext(timeout) + + let timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { + subscriber.putNext(value.modify { value in + return max(0, value - 1) + }) + }, queue: Queue.mainQueue()) + timer.start() + + return ActionDisposable { + timer.invalidate() + } + } + } else { + return .single(nil) + } +} + +private func cancelResetAccountEntries(state: CancelResetAccountState, data: CancelAccountResetData, timeout: Int32?, phone: String) -> [InputDataEntry] { + var entries: [InputDataEntry] = [] + + var sectionId: Int32 = 0 + var index:Int32 = 0 + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + +// + entries.append(.input(sectionId: sectionId, index: index, value: .string(state.code), error: state.error, identifier: _id_input_code, mode: .plain, data: InputDataRowData(), placeholder: nil, inputPlaceholder: L10n.twoStepAuthRecoveryCode, filter: {String($0.unicodeScalars.filter { CharacterSet.decimalDigits.contains($0)})}, limit: state.limit)) + index += 1 + + var nextOptionText = "" + if let nextType = data.nextType { + nextOptionText += authorizationNextOptionText(currentType: data.type, nextType: nextType, timeout: timeout).next + } + + let phoneNumber = phone.hasPrefix("+") ? phone : "+\(phone)" + + let formattedNumber = formatPhoneNumber(phoneNumber) + var result = L10n.cancelResetAccountTextSMS(formattedNumber) + + if !nextOptionText.isEmpty { + result += "\n\n" + nextOptionText + } + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(result), data: InputDataGeneralTextData())) + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + + +func cancelResetAccountController(context: AccountContext, phone: String, data: CancelAccountResetData) -> InputDataModalController { + + + let initialState = CancelResetAccountState(code: "", error: nil, checking: false, limit: 255) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((CancelResetAccountState) -> CancelResetAccountState) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + + let actionsDisposable = DisposableSet() + + let updateCodeLimitDisposable = MetaDisposable() + actionsDisposable.add(updateCodeLimitDisposable) + + let confirmPhoneDisposable = MetaDisposable() + actionsDisposable.add(confirmPhoneDisposable) + + let nextTypeDisposable = MetaDisposable() + actionsDisposable.add(nextTypeDisposable) + + let currentDataPromise = Promise() + currentDataPromise.set(.single(data)) + + let timeout = Promise() + timeout.set(currentDataPromise.get() |> mapToSignal(timeoutSignal)) + + + updateCodeLimitDisposable.set((currentDataPromise.get() |> deliverOnMainQueue).start(next: { data in + updateState { current in + var limit:Int32 = 255 + switch data.type { + case let .call(length): + limit = length + case let .otherSession(length): + limit = length + case let .sms(length): + limit = length + default: + break + } + return current.withUpdatedCodeLimit(limit) + } + })) + + var close: (() -> Void)? = nil + + let checkCode: (String) -> InputDataValidation = { code in + return .fail(.doSomething { f in + + let checking = stateValue.with {$0.checking} + let code = stateValue.with { $0.code } + + updateState { current in + return current.withUpdatedChecking(true) + } + + if !checking { + + confirmPhoneDisposable.set(showModalProgress(signal: context.engine.auth.requestCancelAccountReset(phoneCodeHash: data.hash, phoneCode: code) |> deliverOnMainQueue, for: mainWindow).start(error: { error in + + let errorText: String + switch error { + case .generic: + errorText = L10n.twoStepAuthGenericError + case .invalidCode: + errorText = L10n.twoStepAuthRecoveryCodeInvalid + case .codeExpired: + errorText = L10n.twoStepAuthRecoveryCodeExpired + case .limitExceeded: + errorText = L10n.twoStepAuthFloodError + } + + updateState { + return $0.withUpdatedError(InputDataValueError(description: errorText, target: .data)).withUpdatedChecking(false) + } + + f(.fail(.fields([_id_input_code : .shake]))) + + }, completed: { + updateState { + return $0.withUpdatedChecking(false) + } + close?() + alert(for: mainWindow, info: L10n.cancelResetAccountSuccess(formatPhoneNumber(phone.hasPrefix("+") ? phone : "+\(phone)"))) + })) + } + }) + } + + + + + + let signal = combineLatest(statePromise.get(), currentDataPromise.get(), timeout.get()) |> map { state, data, timeout in + return InputDataSignalValue(entries: cancelResetAccountEntries(state: state, data: data, timeout: timeout, phone: phone)) + } + + let resendCode = currentDataPromise.get() + |> mapToSignal { [weak currentDataPromise] data -> Signal in + if let _ = data.nextType { + return timeout.get() + |> filter { $0 == 0 } + |> take(1) + |> mapToSignal { _ -> Signal in + return Signal { subscriber in + return context.engine.auth.requestNextCancelAccountResetOption(phoneNumber: phone, phoneCodeHash: data.hash).start(next: { next in + currentDataPromise?.set(.single(next)) + }, error: { error in + + }) + } + } + } else { + return .complete() + } + } + nextTypeDisposable.set(resendCode.start()) + + let controller = InputDataController(dataSignal: signal, title: L10n.cancelResetAccountTitle, validateData: { data in + + return checkCode(stateValue.with { $0.code }) + }, updateDatas: { data in + updateState { current in + return current.withUpdatedCode(data[_id_input_code]?.stringValue ?? current.code).withUpdatedError(nil) + } + + let codeLimit = stateValue.with { $0.limit } + let code = stateValue.with { $0.code } + + if code.length == codeLimit { + return checkCode(code) + } + return .none + }, afterDisappear: { + actionsDisposable.dispose() + }, updateDoneValue: { data in + return { f in + let checking = stateValue.with { $0.checking } + f(checking ? .loading : .invisible) + } + }, hasDone: true) + + controller.getBackgroundColor = { + theme.colors.background + } + + let modalInteractions = ModalInteractions(acceptTitle: L10n.modalSend, accept: { [weak controller] in + _ = controller?.returnKeyAction() + }, drawBorder: true, height: 50, singleButton: true) + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) + + close = { [weak modalController] in + modalController?.modal?.close() + } + + return modalController + +} diff --git a/Telegram-Mac/ChangePhoneNumberContainerView.swift b/Telegram-Mac/ChangePhoneNumberContainerView.swift new file mode 100644 index 0000000000..3bb34645d0 --- /dev/null +++ b/Telegram-Mac/ChangePhoneNumberContainerView.swift @@ -0,0 +1,309 @@ +// +// ChangePhoneNumberContainerView.swift +// Telegram +// +// Created by Mikhail Filimonov on 02/04/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import SwiftSignalKit + + +final class ChangePhoneNumberArguments { + let sendCode:(String)->Void + init(sendCode:@escaping(String)->Void) { + self.sendCode = sendCode + } +} + +class ChangePhoneNumberContainerView : View, NSTextFieldDelegate { + + var arguments:ChangePhoneNumberArguments? + + + private let countrySelector:TitleButton = TitleButton() + + let countryLabel:TextViewLabel = TextViewLabel() + let numberLabel:TextViewLabel = TextViewLabel() + + fileprivate let errorLabel:LoginErrorStateView = LoginErrorStateView() + + let codeText:NSTextField = NSTextField() + let numberText:NSTextField = NSTextField() + + fileprivate var selectedItem:CountryItem? + private let manager: CountryManager + + required init(frame frameRect: NSRect, manager: CountryManager) { + self.manager = manager + super.init(frame: frameRect) + + + countrySelector.style = ControlStyle(font: NSFont.medium(.title), foregroundColor: theme.colors.accent, backgroundColor: theme.colors.background) + countrySelector.set(text: "France", for: .Normal) + _ = countrySelector.sizeToFit() + addSubview(countrySelector) + + + + + addSubview(countryLabel) + addSubview(numberLabel) + + countrySelector.set(handler: { [weak self] _ in + self?.showCountrySelector() + }, for: .Click) + + updateLocalizationAndTheme(theme: theme) + + codeText.stringValue = "+" + + codeText.textColor = theme.colors.text + codeText.font = NSFont.normal(.title) + numberText.textColor = theme.colors.text + numberText.font = NSFont.normal(.title) + + numberText.isBordered = false + numberText.isBezeled = false + numberText.drawsBackground = false + numberText.focusRingType = .none + + codeText.drawsBackground = false + codeText.isBordered = false + codeText.isBezeled = false + codeText.focusRingType = .none + + codeText.delegate = self + codeText.nextResponder = numberText + codeText.nextKeyView = numberText + + numberText.delegate = self + numberText.nextResponder = codeText + numberText.nextKeyView = codeText + addSubview(codeText) + addSubview(numberText) + + errorLabel.layer?.opacity = 0 + addSubview(errorLabel) + + let code = NSLocale.current.regionCode ?? "US" + update(selectedItem: manager.item(bySmallCountryName: code), update: true) + + + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + backgroundColor = theme.colors.background + countryLabel.attributedString = .initialize(string: tr(L10n.loginCountryLabel), color: theme.colors.grayText, font: NSFont.normal(FontSize.title)) + countryLabel.sizeToFit() + + numberLabel.attributedString = .initialize(string: tr(L10n.loginYourPhoneLabel), color: theme.colors.grayText, font: NSFont.normal(FontSize.title)) + numberLabel.sizeToFit() + + numberText.placeholderAttributedString = NSAttributedString.initialize(string: tr(L10n.loginPhoneFieldPlaceholder), color: theme.colors.grayText, font: NSFont.normal(.header), coreText: false) + + needsLayout = true + } + + func setPhoneError(_ error: AuthorizationCodeRequestError) { + let text:String + switch error { + case .invalidPhoneNumber: + text = tr(L10n.phoneNumberInvalid) + case .limitExceeded: + text = tr(L10n.loginFloodWait) + case .generic: + text = "undefined error" + case .phoneLimitExceeded: + text = "undefined error" + case .phoneBanned: + text = "PHONE BANNED" + case .timeout: + text = "timeout" + } + errorLabel.state.set(.single(.error(text))) + } + + func update(countryCode: Int32, number: String) { + self.codeText.stringValue = "\(countryCode)" + self.numberText.stringValue = formatPhoneNumber(number) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + override func layout() { + super.layout() + codeText.sizeToFit() + numberText.sizeToFit() + + let maxInset = max(countryLabel.frame.width,numberLabel.frame.width) + let contentInset = maxInset + 20 + 5 + countrySelector.setFrameOrigin(contentInset, floorToScreenPixels(backingScaleFactor, 25 - countrySelector.frame.height/2)) + + countryLabel.setFrameOrigin(maxInset - countryLabel.frame.width, floorToScreenPixels(backingScaleFactor, 25 - countryLabel.frame.height/2)) + numberLabel.setFrameOrigin(maxInset - numberLabel.frame.width, floorToScreenPixels(backingScaleFactor, 75 - numberLabel.frame.height/2)) + + codeText.setFrameOrigin(contentInset, floorToScreenPixels(backingScaleFactor, 75 - codeText.frame.height/2)) + numberText.setFrameOrigin(contentInset + separatorInset, floorToScreenPixels(backingScaleFactor, 75 - codeText.frame.height/2)) + errorLabel.centerX(y: 120) + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + super.draw(layer, in: ctx) + + + let maxInset = max(countryLabel.frame.width,numberLabel.frame.width) + 20 + ctx.setFillColor(theme.colors.border.cgColor) + ctx.fill(NSMakeRect(maxInset, 50, frame.width - maxInset, .borderSize)) + ctx.fill(NSMakeRect(maxInset, 100, frame.width - maxInset, .borderSize)) + // ctx.fill(NSMakeRect(maxInset + separatorInset, 50, .borderSize, 50)) + } + + + func showCountrySelector() { + + var items:[ContextMenuItem] = [] + for country in manager.countries { + let item = ContextMenuItem(country.fullName, handler: { [weak self] in + self?.update(selectedItem: country, update: true) + }) + items.append(item) + } + if let currentEvent = NSApp.currentEvent { + ContextMenu.show(items: items, view: countrySelector, event: currentEvent, onShow: {(menu) in + + }, onClose: {}) + } + + } + + func controlTextDidChange(_ obj: Notification) { + + if let field = obj.object as? NSTextField { + let code = codeText.stringValue.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() + let dec = code.prefix(4) + + if field == codeText { + + + if code.length > 4 { + let list = Array(code).map {String($0)} + let reduced = list.reduce([], { current, value -> [String] in + var current = current + current.append((current.last ?? "") + value) + return current + }).map({Int($0)}).filter({$0 != nil}).map({$0!}) + + var found: Bool = false + for _code in reduced { + if let item = manager.item(byCodeNumber: _code) { + codeText.stringValue = "+" + String(_code) + update(selectedItem: item, update: true, updateCode: false) + + let codeString = String(_code) + var formated = formatPhoneNumber(codeString + String(code[codeString.endIndex.. Bool { + if commandSelector == #selector(insertNewline(_:)) { + if control == codeText { + self.window?.makeFirstResponder(self.numberText) + self.numberText.selectText(nil) + } else if !numberText.stringValue.isEmpty { + arguments?.sendCode(number) + } + //Queue.mainQueue().justDispatch { + (control as? NSTextField)?.setCursorToEnd() + //} + return true + } else if commandSelector == #selector(deleteBackward(_:)) { + if control == numberText { + if numberText.stringValue.isEmpty { + Queue.mainQueue().justDispatch { + self.window?.makeFirstResponder(self.codeText) + self.codeText.setCursorToEnd() + } + } + } + return false + + } + return false + } + + func update(selectedItem:CountryItem?, update:Bool, updateCode:Bool = true) -> Void { + self.selectedItem = selectedItem + if update { + countrySelector.set(text: selectedItem?.shortName ?? tr(L10n.loginInvalidCountryCode), for: .Normal) + _ = countrySelector.sizeToFit() + if updateCode { + codeText.stringValue = selectedItem != nil ? "+\(selectedItem!.code)" : "+" + } + needsLayout = true + setNeedsDisplayLayer() + + } + } + + + + var separatorInset:CGFloat { + return codeText.frame.width + 10 + } + +} diff --git a/Telegram-Mac/ChannelAdminController.swift b/Telegram-Mac/ChannelAdminController.swift index 2151b0431f..9031c2c5fc 100644 --- a/Telegram-Mac/ChannelAdminController.swift +++ b/Telegram-Mac/ChannelAdminController.swift @@ -8,74 +8,53 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac +import Postbox +import TelegramCore + +import SwiftSignalKit private final class ChannelAdminControllerArguments { - let account: Account - let toggleRight: (TelegramChannelAdminRightsFlags, TelegramChannelAdminRightsFlags) -> Void + let context: AccountContext + let toggleRight: (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void let dismissAdmin: () -> Void - - init(account: Account, toggleRight: @escaping (TelegramChannelAdminRightsFlags, TelegramChannelAdminRightsFlags) -> Void, dismissAdmin: @escaping () -> Void) { - self.account = account + let cantEditError: () -> Void + let transferOwnership:()->Void + let updateRank:(String)->Void + init(context: AccountContext, toggleRight: @escaping (TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags) -> Void, dismissAdmin: @escaping () -> Void, cantEditError: @escaping() -> Void, transferOwnership: @escaping()->Void, updateRank: @escaping(String)->Void) { + self.context = context self.toggleRight = toggleRight self.dismissAdmin = dismissAdmin + self.cantEditError = cantEditError + self.transferOwnership = transferOwnership + self.updateRank = updateRank } } private enum ChannelAdminEntryStableId: Hashable { case info - case right(TelegramChannelAdminRightsFlags) + case right(TelegramChatAdminRightsFlags) case description(Int32) + case changeOwnership case section(Int32) + case roleHeader + case role + case roleDesc + case dismiss var hashValue: Int { - switch self { - case .info: - return 0 - case .description(let index): - return Int(index) - case .section(let section): - return Int(section) - case let .right(flags): - return flags.rawValue.hashValue - } - } - - static func ==(lhs: ChannelAdminEntryStableId, rhs: ChannelAdminEntryStableId) -> Bool { - switch lhs { - case .info: - if case .info = rhs { - return true - } else { - return false - } - case let .right(flags): - if case .right(flags) = rhs { - return true - } else { - return false - } - case let .section(section): - if case .section(section) = rhs { - return true - } else { - return false - } - case .description(let text): - if case .description(text) = rhs { - return true - } else { - return false - } - } + return 0 } + } private enum ChannelAdminEntry: TableItemListNodeEntry { - case info(Int32, Peer, TelegramUserPresence?) - case rightItem(Int32, Int, String, TelegramChannelAdminRightsFlags, TelegramChannelAdminRightsFlags, Bool, Bool) - case description(Int32, Int32, String) + case info(Int32, Peer, TelegramUserPresence?, GeneralViewType) + case rightItem(Int32, Int, String, TelegramChatAdminRightsFlags, TelegramChatAdminRightsFlags, Bool, Bool, GeneralViewType) + case roleHeader(Int32, GeneralViewType) + case roleDesc(Int32, GeneralViewType) + case role(Int32, String, String, GeneralViewType) + case description(Int32, Int32, String, GeneralViewType) + case changeOwnership(Int32, Int32, String, GeneralViewType) + case dismiss(Int32, Int32, String, GeneralViewType) case section(Int32) @@ -83,10 +62,20 @@ private enum ChannelAdminEntry: TableItemListNodeEntry { switch self { case .info: return .info - case let .rightItem(_, _, _, right, _, _, _): + case let .rightItem(_, _, _, right, _, _, _, _): return .right(right) - case .description(_, let index, _): + case .description(_, let index, _, _): return .description(index) + case .changeOwnership: + return .changeOwnership + case .dismiss: + return .dismiss + case .roleHeader: + return .roleHeader + case .roleDesc: + return .roleDesc + case .role: + return .role case .section(let sectionId): return .section(sectionId) } @@ -94,57 +83,59 @@ private enum ChannelAdminEntry: TableItemListNodeEntry { static func ==(lhs: ChannelAdminEntry, rhs: ChannelAdminEntry) -> Bool { switch lhs { - case let .info(lhsSectionId, lhsPeer, lhsPresence): - if case let .info(rhsSectionId, rhsPeer, rhsPresence) = rhs { - if lhsSectionId != rhsSectionId { - return false - } + case let .info(sectionId, lhsPeer, presence, viewType): + if case .info(sectionId, let rhsPeer, presence, viewType) = rhs { if !arePeersEqual(lhsPeer, rhsPeer) { return false } - if lhsPresence != rhsPresence { - return false - } - return true } else { return false } - case let .rightItem(lhsSectionId, lhsIndex, lhsText, lhsRight, lhsFlags, lhsValue, lhsEnabled): - if case let .rightItem(rhsSectionId, rhsIndex, rhsText, rhsRight, rhsFlags, rhsValue, rhsEnabled) = rhs { - if lhsSectionId != rhsSectionId { - return false - } - if lhsIndex != rhsIndex { - return false - } - if lhsText != rhsText { - return false - } - if lhsRight != rhsRight { - return false - } - if lhsFlags != rhsFlags { - return false - } - if lhsValue != rhsValue { - return false - } - if lhsEnabled != rhsEnabled { - return false - } + case let .rightItem(sectionId, index, text, right, flags, value, enabled, viewType): + if case .rightItem(sectionId, index, text, right, flags, value, enabled, viewType) = rhs { + return true + } else { + return false + } + case let .description(sectionId, index, text, viewType): + if case .description(sectionId, index, text, viewType) = rhs{ return true } else { return false } - case let .description(sectionId, index, text): - if case .description(sectionId, index, text) = rhs{ + case let .changeOwnership(sectionId, index, text, viewType): + if case .changeOwnership(sectionId, index, text, viewType) = rhs{ + return true + } else { + return false + } + case let .dismiss(sectionId, index, text, viewType): + if case .dismiss(sectionId, index, text, viewType) = rhs{ + return true + } else { + return false + } + case let .roleHeader(section, viewType): + if case .roleHeader(section, viewType) = rhs { + return true + } else { + return false + } + case let .roleDesc(section, viewType): + if case .roleDesc(section, viewType) = rhs { + return true + } else { + return false + } + case let .role(section, text, placeholder, viewType): + if case .role(section, text, placeholder, viewType) = rhs { return true } else { return false } case let .section(sectionId): - if case .section(sectionId) = rhs{ + if case .section(sectionId) = rhs { return true } else { return false @@ -154,13 +145,23 @@ private enum ChannelAdminEntry: TableItemListNodeEntry { var index:Int32 { switch self { - case .info(let sectionId, _, _): + case .info(let sectionId, _, _, _): return (sectionId * 1000) + 0 - case .description(let sectionId, let index, _): + case .description(let sectionId, let index, _, _): + return (sectionId * 1000) + index + case let .changeOwnership(sectionId, index, _, _): + return (sectionId * 1000) + index + case let .dismiss(sectionId, index, _, _): return (sectionId * 1000) + index - case .rightItem(let sectionId, let index, _, _, _, _, _): + case .rightItem(let sectionId, let index, _, _, _, _, _, _): return (sectionId * 1000) + Int32(index) + 10 - case .section(let sectionId): + case let .roleHeader(sectionId, _): + return (sectionId * 1000) + case let .role(sectionId, _, _, _): + return (sectionId * 1000) + 1 + case let .roleDesc(sectionId, _): + return (sectionId * 1000) + 2 + case let .section(sectionId): return (sectionId + 1) * 1000 - sectionId } } @@ -172,90 +173,112 @@ private enum ChannelAdminEntry: TableItemListNodeEntry { func item(_ arguments: ChannelAdminControllerArguments, initialSize: NSSize) -> TableRowItem { switch self { case .section: - return GeneralRowItem(initialSize, height: 20, stableId: stableId) - case .info(_, let peer, let presence): - var string:String = peer.isBot ? tr(.presenceBot) : tr(.peerStatusRecently) + return GeneralRowItem(initialSize, height: 30, stableId: stableId, viewType: .separator) + case let .info(_, peer, presence, viewType): + var string:String = peer.isBot ? L10n.presenceBot : L10n.peerStatusRecently var color:NSColor = theme.colors.grayText - if let presence = presence { + if let presence = presence, !peer.isBot { let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - (string, _, color) = stringAndActivityForUserPresence(presence, relativeTo: Int32(timestamp)) + (string, _, color) = stringAndActivityForUserPresence(presence, timeDifference: arguments.context.timeDifference, relativeTo: Int32(timestamp)) } - return ShortPeerRowItem(initialSize, peer: peer, account: arguments.account, stableId: stableId, enabled: true, height: 60, photoSize: NSMakeSize(50, 50), statusStyle: ControlStyle(font: NSFont.normal(.custom(14)), foregroundColor: color), status: string, borderType: [], drawCustomSeparator: false, drawLastSeparator: false, inset: NSEdgeInsets(left: 25, right: 25), drawSeparatorIgnoringInset: false, action: {}) - case let .rightItem(_, _, name, right, flags, value, enabled): + return ShortPeerRowItem(initialSize, peer: peer, account: arguments.context.account, stableId: stableId, enabled: true, height: 60, photoSize: NSMakeSize(40, 40), statusStyle: ControlStyle(font: .normal(.title), foregroundColor: color), status: string, inset: NSEdgeInsets(left: 30, right: 30), viewType: viewType, action: {}) + case let .rightItem(_, _, name, right, flags, value, enabled, viewType): //ControlStyle(font: NSFont.) - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: name, nameStyle: ControlStyle(font: .normal(.title), foregroundColor: enabled ? theme.colors.text : theme.colors.grayText), type: .switchable(stateback: { () -> Bool in - return value - }), action: { + + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: name, nameStyle: ControlStyle(font: .normal(.title), foregroundColor: enabled ? theme.colors.text : theme.colors.grayText), type: .switchable(value), viewType: viewType, action: { arguments.toggleRight(right, flags) - }, enabled: enabled, switchAppearance: SwitchViewAppearance(backgroundColor: theme.colors.background, stateOnColor: enabled ? theme.colors.blueUI : theme.colors.blueUI.withAlphaComponent(0.6), stateOffColor: enabled ? theme.colors.redUI : theme.colors.redUI.withAlphaComponent(0.6), disabledColor: .grayBackground, borderColor: .clear)) - case .description(_, _, let name): - return GeneralTextRowItem(initialSize, stableId: stableId, text: name)//GeneralInteractedRowItem(initialSize, stableId: stableId, name: name) + }, enabled: enabled, switchAppearance: SwitchViewAppearance(backgroundColor: theme.colors.background, stateOnColor: enabled ? theme.colors.accent : theme.colors.accent.withAlphaComponent(0.6), stateOffColor: enabled ? theme.colors.redUI : theme.colors.redUI.withAlphaComponent(0.6), disabledColor: .grayBackground, borderColor: .clear), disabledAction: { + arguments.cantEditError() + }) + case let .changeOwnership(_, _, text, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: text, nameStyle: blueActionButton, type: .next, viewType: viewType, action: arguments.transferOwnership) + case let .dismiss(_, _, text, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: text, nameStyle: redActionButton, type: .next, viewType: viewType, action: arguments.dismissAdmin) + case let .roleHeader(_, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.channelAdminRoleHeader, viewType: viewType) + case let .role(_, text, placeholder, viewType): + return InputDataRowItem(initialSize, stableId: stableId, mode: .plain, error: nil, viewType: viewType, currentText: text, placeholder: nil, inputPlaceholder: placeholder, filter: { text in + let filtered = text.filter { character -> Bool in + return !String(character).containsOnlyEmoji + } + return filtered + }, updated: arguments.updateRank, limit: 16) + case let .roleDesc(_, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: "", viewType: viewType) + case let .description(_, _, name, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: name, viewType: viewType) } //return TableRowItem(initialSize) } } private struct ChannelAdminControllerState: Equatable { - let updatedFlags: TelegramChannelAdminRightsFlags? + let updatedFlags: TelegramChatAdminRightsFlags? let updating: Bool let editable:Bool - init(updatedFlags: TelegramChannelAdminRightsFlags? = nil, updating: Bool = false, editable: Bool = false) { + let rank:String? + let initialRank:String? + init(updatedFlags: TelegramChatAdminRightsFlags? = nil, updating: Bool = false, editable: Bool = false, rank: String?, initialRank: String?) { self.updatedFlags = updatedFlags self.updating = updating self.editable = editable + self.rank = rank + self.initialRank = initialRank } - static func ==(lhs: ChannelAdminControllerState, rhs: ChannelAdminControllerState) -> Bool { - if lhs.updatedFlags != rhs.updatedFlags { - return false - } - if lhs.updating != rhs.updating { - return false - } - if lhs.editable != rhs.editable { - return false - } - return true - } - - func withUpdatedUpdatedFlags(_ updatedFlags: TelegramChannelAdminRightsFlags?) -> ChannelAdminControllerState { - return ChannelAdminControllerState(updatedFlags: updatedFlags, updating: self.updating, editable: self.editable) + func withUpdatedUpdatedFlags(_ updatedFlags: TelegramChatAdminRightsFlags?) -> ChannelAdminControllerState { + return ChannelAdminControllerState(updatedFlags: updatedFlags, updating: self.updating, editable: self.editable, rank: self.rank, initialRank: self.initialRank) } func withUpdatedEditable(_ editable:Bool) -> ChannelAdminControllerState { - return ChannelAdminControllerState(updatedFlags: updatedFlags, updating: self.updating, editable: editable) + return ChannelAdminControllerState(updatedFlags: updatedFlags, updating: self.updating, editable: editable, rank: self.rank, initialRank: self.initialRank) } func withUpdatedUpdating(_ updating: Bool) -> ChannelAdminControllerState { - return ChannelAdminControllerState(updatedFlags: self.updatedFlags, updating: updating, editable: self.editable) + return ChannelAdminControllerState(updatedFlags: self.updatedFlags, updating: updating, editable: self.editable, rank: self.rank, initialRank: self.initialRank) + } + + func withUpdatedRank(_ rank: String?) -> ChannelAdminControllerState { + return ChannelAdminControllerState(updatedFlags: self.updatedFlags, updating: updating, editable: self.editable, rank: rank, initialRank: self.initialRank) } } -private func stringForRight(right: TelegramChannelAdminRightsFlags, isGroup: Bool) -> String { +private func stringForRight(right: TelegramChatAdminRightsFlags, isGroup: Bool, defaultBannedRights: TelegramChatBannedRights?) -> String { if right.contains(.canChangeInfo) { - return isGroup ? tr(.groupEditAdminPermissionChangeInfo) : tr(.channelEditAdminPermissionChangeInfo) + return isGroup ? L10n.groupEditAdminPermissionChangeInfo : L10n.channelEditAdminPermissionChangeInfo } else if right.contains(.canPostMessages) { - return tr(.channelEditAdminPermissionPostMessages) + return L10n.channelEditAdminPermissionPostMessages } else if right.contains(.canEditMessages) { - return tr(.channelEditAdminPermissionEditMessages) + return L10n.channelEditAdminPermissionEditMessages } else if right.contains(.canDeleteMessages) { - return tr(.channelEditAdminPermissionDeleteMessages) + return L10n.channelEditAdminPermissionDeleteMessages } else if right.contains(.canBanUsers) { - return tr(.channelEditAdminPermissionBanUsers) + return L10n.channelEditAdminPermissionBanUsers } else if right.contains(.canInviteUsers) { - return tr(.channelEditAdminPermissionInviteUsers) - } else if right.contains(.canChangeInviteLink) { - return "tr(.channelEditAdminPermissionInviteViaLink)" + if isGroup { + if let defaultBannedRights = defaultBannedRights, defaultBannedRights.flags.contains(.banAddMembers) { + return L10n.channelEditAdminPermissionInviteMembers + } else { + return L10n.channelEditAdminPermissionInviteViaLink + } + } else { + return L10n.channelEditAdminPermissionInviteSubscribers + } + } else if right.contains(.canPinMessages) { - return tr(.channelEditAdminPermissionPinMessages) + return L10n.channelEditAdminPermissionPinMessages } else if right.contains(.canAddAdmins) { - return tr(.channelEditAdminPermissionAddNewAdmins) + return L10n.channelEditAdminPermissionAddNewAdmins + } else if right.contains(.canBeAnonymous) { + return L10n.channelEditAdminPermissionAnonymous + } else if right.contains(.canManageCalls) { + return L10n.channelEditAdminManageCalls } else { return "" } } -private func rightDependencies(_ right: TelegramChannelAdminRightsFlags) -> [TelegramChannelAdminRightsFlags] { +private func rightDependencies(_ right: TelegramChatAdminRightsFlags) -> [TelegramChatAdminRightsFlags] { if right.contains(.canChangeInfo) { return [] } else if right.contains(.canPostMessages) { @@ -268,8 +291,6 @@ private func rightDependencies(_ right: TelegramChannelAdminRightsFlags) -> [Tel return [] } else if right.contains(.canInviteUsers) { return [] - } else if right.contains(.canChangeInviteLink) { - return [.canInviteUsers] } else if right.contains(.canPinMessages) { return [] } else if right.contains(.canAddAdmins) { @@ -287,22 +308,29 @@ private func canEditAdminRights(accountPeerId: PeerId, channelView: PeerView, in switch initialParticipant { case .creator: return false - case let .member(_, _, adminInfo, _): + case let .member(_, _, adminInfo, _, _): if let adminInfo = adminInfo { return adminInfo.canBeEditedByAccountPeer || adminInfo.promotedBy == accountPeerId } else { - return true + return channel.hasPermission(.addAdmins) } } } else { - return channel.hasAdminRights(.canAddAdmins) + return channel.hasPermission(.addAdmins) + } + } else if let group = channelView.peers[channelView.peerId] as? TelegramGroup { + if case .creator = group.role { + return true + } else { + return false } } else { return false } } -private func channelAdminControllerEntries(state: ChannelAdminControllerState, accountPeerId: PeerId, channelView: PeerView, adminView: PeerView, initialParticipant: ChannelParticipant?) -> ([ChannelAdminEntry], TelegramChannelAdminRightsFlags) { + +private func channelAdminControllerEntries(state: ChannelAdminControllerState, accountPeerId: PeerId, channelView: PeerView, adminView: PeerView, initialParticipant: ChannelParticipant?) -> [ChannelAdminEntry] { var entries: [ChannelAdminEntry] = [] var sectionId:Int32 = 1 @@ -310,19 +338,15 @@ private func channelAdminControllerEntries(state: ChannelAdminControllerState, a entries.append(.section(sectionId)) sectionId += 1 + var descId: Int32 = 0 + var addAdminsEnabled: Bool = false - var rights:TelegramChannelAdminRightsFlags = [] if let channel = channelView.peers[channelView.peerId] as? TelegramChannel, let admin = adminView.peers[adminView.peerId] { - entries.append(.info(sectionId, admin, adminView.peerPresences[admin.id] as? TelegramUserPresence)) - - entries.append(.section(sectionId)) - sectionId += 1 - - entries.append(.description(sectionId, 1, tr(.channelAdminWhatCanAdminDo))) + entries.append(.info(sectionId, admin, adminView.peerPresences[admin.id] as? TelegramUserPresence, .singleItem)) let isGroup: Bool - let maskRightsFlags: TelegramChannelAdminRightsFlags - var rightsOrder: [TelegramChannelAdminRightsFlags] = [] + let maskRightsFlags: TelegramChatAdminRightsFlags + let rightsOrder: [TelegramChatAdminRightsFlags] switch channel.info { case .broadcast: @@ -330,72 +354,270 @@ private func channelAdminControllerEntries(state: ChannelAdminControllerState, a maskRightsFlags = .broadcastSpecific rightsOrder = [ .canChangeInfo, - .canInviteUsers, .canPostMessages, .canEditMessages, .canDeleteMessages, + .canManageCalls, + .canInviteUsers, .canAddAdmins ] - case let .group(info): + case .group: isGroup = true maskRightsFlags = .groupSpecific + rightsOrder = [ + .canChangeInfo, + .canDeleteMessages, + .canBanUsers, + .canInviteUsers, + .canPinMessages, + .canManageCalls, + .canBeAnonymous, + .canAddAdmins + ] + } + + if canEditAdminRights(accountPeerId: accountPeerId, channelView: channelView, initialParticipant: initialParticipant) { - rightsOrder.append(.canChangeInfo) - rightsOrder.append(.canDeleteMessages) - rightsOrder.append(.canBanUsers) - if !info.flags.contains(.everyMemberCanInviteMembers) { - rightsOrder.append(.canInviteUsers) + var isCreator = false + if let initialParticipant = initialParticipant, case .creator = initialParticipant { + isCreator = true } - rightsOrder.append(.canPinMessages) - rightsOrder.append(.canAddAdmins) - } - if canEditAdminRights(accountPeerId: accountPeerId, channelView: channelView, initialParticipant: initialParticipant) { - let accountUserRightsFlags: TelegramChannelAdminRightsFlags - if channel.flags.contains(.isCreator) { - accountUserRightsFlags = maskRightsFlags - } else if let adminRights = channel.adminRights { - accountUserRightsFlags = maskRightsFlags.intersection(adminRights.flags) - } else { - accountUserRightsFlags = [] + if channel.isSupergroup { + entries.append(.section(sectionId)) + sectionId += 1 + let placeholder = isCreator ? L10n.channelAdminRolePlaceholderOwner : L10n.channelAdminRolePlaceholderAdmin + entries.append(.roleHeader(sectionId, .textTopItem)) + entries.append(.role(sectionId, state.rank ?? "", placeholder, .singleItem)) + entries.append(.description(sectionId, descId, isCreator ? L10n.channelAdminRoleOwnerDesc : L10n.channelAdminRoleAdminDesc, .textBottomItem)) + descId += 1 } + entries.append(.section(sectionId)) + sectionId += 1 - var currentRightsFlags: TelegramChannelAdminRightsFlags - if let updatedFlags = state.updatedFlags { - currentRightsFlags = updatedFlags - } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminRights, _) = initialParticipant, let adminRights = maybeAdminRights { - currentRightsFlags = adminRights.rights.flags - } else { - currentRightsFlags = accountUserRightsFlags.subtracting(.canAddAdmins) + + if (channel.isSupergroup) || channel.isChannel { + if !isCreator || channel.isChannel { + entries.append(.description(sectionId, descId, L10n.channelAdminWhatCanAdminDo, .textTopItem)) + descId += 1 + } + + + var accountUserRightsFlags: TelegramChatAdminRightsFlags + if channel.flags.contains(.isCreator) { + accountUserRightsFlags = maskRightsFlags + } else if let adminRights = channel.adminRights { + accountUserRightsFlags = maskRightsFlags.intersection(adminRights.rights) + } else { + accountUserRightsFlags = [] + } + + let currentRightsFlags: TelegramChatAdminRightsFlags + if let updatedFlags = state.updatedFlags { + currentRightsFlags = updatedFlags + } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminRights, _, _) = initialParticipant, let adminRights = maybeAdminRights { + currentRightsFlags = adminRights.rights.rights + } else if let adminRights = channel.adminRights { + currentRightsFlags = adminRights.rights + } else { + currentRightsFlags = accountUserRightsFlags.subtracting([.canAddAdmins, .canBeAnonymous]) + } + + if accountUserRightsFlags.contains(.canAddAdmins) { + addAdminsEnabled = currentRightsFlags.contains(.canAddAdmins) + } + + var index = 0 + + + let list = rightsOrder.filter { + accountUserRightsFlags.contains($0) + }.filter { right in + if channel.isSupergroup, isCreator, right != .canBeAnonymous { + return false + } + return true + } + + + + for (i, right) in list.enumerated() { + entries.append(.rightItem(sectionId, index, stringForRight(right: right, isGroup: isGroup, defaultBannedRights: channel.defaultBannedRights), right, currentRightsFlags, currentRightsFlags.contains(right), !state.updating, bestGeneralViewType(list, for: i))) + index += 1 + } + if !isCreator || channel.isChannel { + entries.append(.description(sectionId, descId, addAdminsEnabled ? L10n.channelAdminAdminAccess : L10n.channelAdminAdminRestricted, .textBottomItem)) + descId += 1 + } + if channel.flags.contains(.isCreator), !admin.isBot && currentRightsFlags.contains(TelegramChatAdminRightsFlags.all) { + if admin.id != accountPeerId { + entries.append(.section(sectionId)) + sectionId += 1 + entries.append(.changeOwnership(sectionId, descId, channel.isChannel ? L10n.channelAdminTransferOwnershipChannel : L10n.channelAdminTransferOwnershipGroup, .singleItem)) + } + } } - rights = currentRightsFlags + } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminInfo, _, _) = initialParticipant, let adminInfo = maybeAdminInfo { - if accountUserRightsFlags.contains(.canAddAdmins) { - addAdminsEnabled = currentRightsFlags.contains(.canAddAdmins) + entries.append(.section(sectionId)) + sectionId += 1 + + if let rank = state.rank { + entries.append(.section(sectionId)) + sectionId += 1 + entries.append(.roleHeader(sectionId, .textTopItem)) + entries.append(.description(sectionId, descId, rank, .textTopItem)) + descId += 1 + entries.append(.section(sectionId)) + sectionId += 1 } var index = 0 - for right in rightsOrder { - if accountUserRightsFlags.contains(right) { - - entries.append(.rightItem(sectionId, index, stringForRight(right: right, isGroup: isGroup), right, currentRightsFlags, currentRightsFlags.contains(right), !state.updating)) - index += 1 - } + for (i, right) in rightsOrder.enumerated() { + entries.append(.rightItem(sectionId, index, stringForRight(right: right, isGroup: isGroup, defaultBannedRights: channel.defaultBannedRights), right, adminInfo.rights.rights, adminInfo.rights.rights.contains(right), false, bestGeneralViewType(rightsOrder, for: i))) + index += 1 + } + entries.append(.description(sectionId, descId, L10n.channelAdminCantEditRights, .textBottomItem)) + descId += 1 + } else if let initialParticipant = initialParticipant, case .creator = initialParticipant { + + entries.append(.section(sectionId)) + sectionId += 1 + + if let rank = state.rank { + entries.append(.section(sectionId)) + sectionId += 1 + entries.append(.roleHeader(sectionId, .textTopItem)) + entries.append(.description(sectionId, descId, rank, .textBottomItem)) + descId += 1 + entries.append(.section(sectionId)) + sectionId += 1 } - entries.append(.description(sectionId, 50, addAdminsEnabled ? tr(.channelAdminAdminAccess) : tr(.channelAdminAdminRestricted))) - } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminInfo, _) = initialParticipant, let adminInfo = maybeAdminInfo { + var index = 0 for right in rightsOrder { - entries.append(.rightItem(sectionId, index, stringForRight(right: right, isGroup: isGroup), right, adminInfo.rights.flags, adminInfo.rights.flags.contains(right), false)) + entries.append(.rightItem(sectionId, index, stringForRight(right: right, isGroup: isGroup, defaultBannedRights: channel.defaultBannedRights), right, TelegramChatAdminRightsFlags(rightsOrder), true, false, bestGeneralViewType(rightsOrder, for: right))) index += 1 } - entries.append(.description(sectionId, 50, tr(.channelAdminCantEditRights))) + entries.append(.description(sectionId, descId, L10n.channelAdminCantEditRights, .textBottomItem)) + descId += 1 + } + + + + } else if let group = channelView.peers[channelView.peerId] as? TelegramGroup, let admin = adminView.peers[adminView.peerId] { + entries.append(.info(sectionId, admin, adminView.peerPresences[admin.id] as? TelegramUserPresence, .singleItem)) + + var isCreator = false + if let initialParticipant = initialParticipant, case .creator = initialParticipant { + isCreator = true + } + + let placeholder = isCreator ? L10n.channelAdminRolePlaceholderOwner : L10n.channelAdminRolePlaceholderAdmin + + + entries.append(.section(sectionId)) + sectionId += 1 + + entries.append(.roleHeader(sectionId, .textTopItem)) + entries.append(.role(sectionId, state.rank ?? "", placeholder, .singleItem)) + entries.append(.description(sectionId, descId, isCreator ? L10n.channelAdminRoleOwnerDesc : L10n.channelAdminRoleAdminDesc, .textBottomItem)) + descId += 1 + + entries.append(.section(sectionId)) + sectionId += 1 + + if !isCreator { + entries.append(.description(sectionId, descId, L10n.channelAdminWhatCanAdminDo, .textTopItem)) + descId += 1 + + let isGroup = true + let maskRightsFlags: TelegramChatAdminRightsFlags = .groupSpecific + let rightsOrder: [TelegramChatAdminRightsFlags] = [ + .canChangeInfo, + .canDeleteMessages, + .canBanUsers, + .canInviteUsers, + .canManageCalls, + .canPinMessages, + .canBeAnonymous, + .canAddAdmins + ] + + let accountUserRightsFlags: TelegramChatAdminRightsFlags = maskRightsFlags + + let currentRightsFlags: TelegramChatAdminRightsFlags + if let updatedFlags = state.updatedFlags { + currentRightsFlags = updatedFlags + } else if let initialParticipant = initialParticipant, case let .member(_, _, maybeAdminRights, _, _) = initialParticipant, let adminRights = maybeAdminRights { + currentRightsFlags = adminRights.rights.rights.subtracting(.canAddAdmins) + } else { + currentRightsFlags = accountUserRightsFlags.subtracting([.canAddAdmins, .canBeAnonymous]) + } + + var index = 0 + + let list = rightsOrder.filter { + accountUserRightsFlags.contains($0) + } + + for (i, right) in list.enumerated() { + entries.append(.rightItem(sectionId, index, stringForRight(right: right, isGroup: isGroup, defaultBannedRights: group.defaultBannedRights), right, currentRightsFlags, currentRightsFlags.contains(right), !state.updating, bestGeneralViewType(list, for: i))) + index += 1 + } + + if accountUserRightsFlags.contains(.canAddAdmins) { + entries.append(.description(sectionId, descId, currentRightsFlags.contains(.canAddAdmins) ? L10n.channelAdminAdminAccess : L10n.channelAdminAdminRestricted, .textBottomItem)) + descId += 1 + } + + if case .creator = group.role, !admin.isBot { + if currentRightsFlags.contains(maskRightsFlags) { + if admin.id != accountPeerId { + entries.append(.section(sectionId)) + sectionId += 1 + entries.append(.changeOwnership(sectionId, descId, L10n.channelAdminTransferOwnershipGroup, .singleItem)) + } + } + } } } - return (entries, rights) + var canDismiss: Bool = false + if let channel = peerViewMainPeer(channelView) as? TelegramChannel { + + if let initialParticipant = initialParticipant { + if channel.flags.contains(.isCreator) { + canDismiss = initialParticipant.adminInfo != nil + } else { + switch initialParticipant { + case .creator: + break + case let .member(_, _, adminInfo, _, _): + if let adminInfo = adminInfo { + if adminInfo.promotedBy == accountPeerId || adminInfo.canBeEditedByAccountPeer { + canDismiss = true + } + } + } + } + } + } + + if canDismiss { + entries.append(.section(sectionId)) + sectionId += 1 + + entries.append(.dismiss(sectionId, descId, L10n.channelAdminDismiss, .singleItem)) + descId += 1 + } + + entries.append(.section(sectionId)) + sectionId += 1 + + return entries } fileprivate func prepareTransition(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], initialSize:NSSize, arguments:ChannelAdminControllerArguments) -> TableUpdateTransition { @@ -408,117 +630,244 @@ fileprivate func prepareTransition(left:[AppearanceWrapperEntry Void + private let updated:(TelegramChatAdminRights?) -> Void private let disposable = MetaDisposable() - private let currentRightFlags:Atomic = Atomic(value: []) - private let stateValue = Atomic(value: ChannelAdminControllerState()) - init(account: Account, peerId: PeerId, adminId: PeerId, initialParticipant: ChannelParticipant?, updated: @escaping (TelegramChannelAdminRights) -> Void) { - self.account = account + private let upgradedToSupergroup: (PeerId, @escaping () -> Void) -> Void + private var okClick: (()-> Void)? + + init(_ context: AccountContext, peerId: PeerId, adminId: PeerId, initialParticipant: ChannelParticipant?, updated: @escaping (TelegramChatAdminRights?) -> Void, upgradedToSupergroup: @escaping (PeerId, @escaping () -> Void) -> Void) { + self.context = context self.peerId = peerId + self.upgradedToSupergroup = upgradedToSupergroup self.adminId = adminId self.initialParticipant = initialParticipant self.updated = updated - super.init(frame: NSMakeRect(0, 0, 300, 360)) + super.init(frame: NSMakeRect(0, 0, 350, 360)) bar = .init(height : 0) } - override var dynamicSize: Bool { - return true - } - - override func measure(size: NSSize) { - self.modal?.resize(with:NSMakeSize(genericView.frame.width, min(size.height - 70, genericView.listHeight)), animated: false) - } - - override func viewClass() -> AnyClass { - return TableView.self - } - - private var genericView:TableView { - return self.view as! TableView - } override func viewDidLoad() { super.viewDidLoad() - let account = self.account - let peerId = self.peerId + genericView.getBackgroundColor = { + theme.colors.listBackground + } + + let combinedPromise: Promise = Promise() + + let context = self.context + var peerId = self.peerId let adminId = self.adminId let initialParticipant = self.initialParticipant let updated = self.updated + let upgradedToSupergroup = self.upgradedToSupergroup + - let stateValue = self.stateValue - let statePromise = ValuePromise(ChannelAdminControllerState(), ignoreRepeated: true) + let initialValue = ChannelAdminControllerState(rank: initialParticipant?.rank, initialRank: initialParticipant?.rank) + let stateValue = Atomic(value: initialValue) + let statePromise = ValuePromise(initialValue, ignoreRepeated: true) let updateState: ((ChannelAdminControllerState) -> ChannelAdminControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } + let dismissImpl:()-> Void = { [weak self] in + self?.close() + } + let actionsDisposable = DisposableSet() let updateRightsDisposable = MetaDisposable() actionsDisposable.add(updateRightsDisposable) - let arguments = ChannelAdminControllerArguments(account: account, toggleRight: { right, flags in + let arguments = ChannelAdminControllerArguments(context: context, toggleRight: { right, flags in updateState { current in var updated = flags if flags.contains(right) { updated.remove(right) } else { - if right.contains(.canInviteUsers) { - updated.insert(.canChangeInviteLink) - } updated.insert(right) } return current.withUpdatedUpdatedFlags(updated) } - }, dismissAdmin: { [weak self] in - if let strongSelf = self { - updateState { current in - return current.withUpdatedUpdating(true) - } - updateRightsDisposable.set((updatePeerAdminRights(account: account, peerId: peerId, adminId: adminId, rights: TelegramChannelAdminRights(flags: [])) |> deliverOnMainQueue).start(error: { _ in + }, dismissAdmin: { + updateState { current in + return current.withUpdatedUpdating(true) + } + if peerId.namespace == Namespaces.Peer.CloudGroup { + updateRightsDisposable.set((context.engine.peers.removeGroupAdmin(peerId: peerId, adminId: adminId) + |> deliverOnMainQueue).start(error: { _ in + }, completed: { + updated(nil) + dismissImpl() + })) + } else { + updateRightsDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(peerId: peerId, memberId: adminId, adminRights: nil, rank: stateValue.with { $0.rank }) |> deliverOnMainQueue).start(error: { _ in - }, completed: { [weak strongSelf] in - updated(TelegramChannelAdminRights(flags: [])) - strongSelf?.close() + }, completed: { + updated(nil) + dismissImpl() })) } + }, cantEditError: { [weak self] in + self?.show(toaster: ControllerToaster(text: L10n.channelAdminCantEdit)) + }, transferOwnership: { + _ = (combineLatest(queue: .mainQueue(), context.account.postbox.loadedPeerWithId(peerId), context.account.postbox.loadedPeerWithId(adminId))).start(next: { peer, admin in + + let header: String + let text: String + if peer.isChannel { + header = L10n.channelAdminTransferOwnershipConfirmChannelTitle + text = L10n.channelAdminTransferOwnershipConfirmChannelText(peer.displayTitle, admin.displayTitle) + } else { + header = L10n.channelAdminTransferOwnershipConfirmGroupTitle + text = L10n.channelAdminTransferOwnershipConfirmGroupText(peer.displayTitle, admin.displayTitle) + } + + let checkPassword:(PeerId)->Void = { peerId in + showModal(with: InputPasswordController(context: context, title: L10n.channelAdminTransferOwnershipPasswordTitle, desc: L10n.channelAdminTransferOwnershipPasswordDesc, checker: { pwd in + return context.peerChannelMemberCategoriesContextsManager.transferOwnership(peerId: peerId, memberId: admin.id, password: pwd) + |> deliverOnMainQueue + |> ignoreValues + |> `catch` { error -> Signal in + switch error { + case .generic: + return .fail(.generic) + case .invalidPassword: + return .fail(.wrong) + default: + return .fail(.generic) + } + } |> afterCompleted { + dismissImpl() + _ = showModalSuccess(for: context.window, icon: theme.icons.successModalProgress, delay: 2.0) + } + }), for: context.window) + } + + let transfer:(PeerId, Bool, Bool)->Void = { _peerId, isGroup, convert in + actionsDisposable.add(showModalProgress(signal: context.engine.peers.checkOwnershipTranfserAvailability(memberId: adminId), for: context.window).start(error: { error in + let errorText: String? + var install2Fa = false + switch error { + case .generic: + errorText = L10n.unknownError + case .tooMuchJoined: + errorText = L10n.inviteChannelsTooMuch + case .authSessionTooFresh: + errorText = L10n.channelTransferOwnerErrorText + case .twoStepAuthMissing: + errorText = L10n.channelTransferOwnerErrorText + install2Fa = true + case .twoStepAuthTooFresh: + errorText = L10n.channelTransferOwnerErrorText + case .invalidPassword: + preconditionFailure() + case .requestPassword: + errorText = nil + case .restricted, .userBlocked: + errorText = isGroup ? L10n.groupTransferOwnerErrorPrivacyRestricted : L10n.channelTransferOwnerErrorPrivacyRestricted + case .adminsTooMuch: + errorText = isGroup ? L10n.groupTransferOwnerErrorAdminsTooMuch : L10n.channelTransferOwnerErrorAdminsTooMuch + case .userPublicChannelsTooMuch: + errorText = L10n.channelTransferOwnerErrorPublicChannelsTooMuch + case .limitExceeded: + errorText = L10n.loginFloodWait + case .userLocatedGroupsTooMuch: + errorText = L10n.groupOwnershipTransferErrorLocatedGroupsTooMuch + } + + if let errorText = errorText { + confirm(for: context.window, header: L10n.channelTransferOwnerErrorTitle, information: errorText, okTitle: L10n.modalOK, cancelTitle: L10n.modalCancel, thridTitle: install2Fa ? L10n.channelTransferOwnerErrorEnable2FA : nil, successHandler: { result in + switch result { + case .basic: + break + case .thrid: + dismissImpl() + context.sharedContext.bindings.rootNavigation().removeUntil(EmptyChatViewController.self) + context.sharedContext.bindings.rootNavigation().push(twoStepVerificationUnlockController(context: context, mode: .access(nil), presentController: { (controller, isRoot, animated) in + let navigation = context.sharedContext.bindings.rootNavigation() + if isRoot { + navigation.removeUntil(EmptyChatViewController.self) + } + if !animated { + navigation.stackInsert(controller, at: navigation.stackCount) + } else { + navigation.push(controller) + } + })) + } + }) + } else { + if convert { + actionsDisposable.add(showModalProgress(signal: context.engine.peers.convertGroupToSupergroup(peerId: peer.id), for: context.window).start(next: { upgradedPeerId in + upgradedToSupergroup(upgradedPeerId, { + peerId = upgradedPeerId + combinedPromise.set(context.account.postbox.combinedView(keys: [.peer(peerId: upgradedPeerId, components: .all), .peer(peerId: adminId, components: .all)])) + checkPassword(upgradedPeerId) + }) + }, error: { error in + switch error { + case .tooManyChannels: + showInactiveChannels(context: context, source: .upgrade) + case .generic: + alert(for: context.window, info: L10n.unknownError) + } + })) + } else { + checkPassword(peer.id) + } + } + })) + } + + confirm(for: context.window, header: header, information: text, okTitle: L10n.channelAdminTransferOwnershipConfirmOK, successHandler: { _ in + transfer(peerId, peer.isSupergroup || peer.isGroup, peer.isGroup) + }) + }) + }, updateRank: { rank in + updateState { + $0.withUpdatedRank(rank) + } }) self.arguments = arguments - let combinedView = account.postbox.combinedView(keys: [.peer(peerId: peerId), .peer(peerId: adminId)]) + + combinedPromise.set(context.account.postbox.combinedView(keys: [.peer(peerId: peerId, components: .all), .peer(peerId: adminId, components: .all)])) let previous:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) let initialSize = atomicSize - let signal = combineLatest(statePromise.get(), combinedView, appearanceSignal) + let signal = combineLatest(statePromise.get(), combinedPromise.get(), appearanceSignal) |> deliverOn(prepareQueue) - |> map { state, combinedView, appearance -> (transition: TableUpdateTransition, canEdit: Bool, canDismiss: Bool) in - let channelView = combinedView.views[.peer(peerId: peerId)] as! PeerView - let adminView = combinedView.views[.peer(peerId: adminId)] as! PeerView - let canEdit = canEditAdminRights(accountPeerId: account.peerId, channelView: channelView, initialParticipant: initialParticipant) + |> map { state, combinedView, appearance -> (transition: TableUpdateTransition, canEdit: Bool, canDismiss: Bool, channelView: PeerView) in + let channelView = combinedView.views[.peer(peerId: peerId, components: .all)] as! PeerView + let adminView = combinedView.views[.peer(peerId: adminId, components: .all)] as! PeerView + var canEdit = canEditAdminRights(accountPeerId: context.account.peerId, channelView: channelView, initialParticipant: initialParticipant) + + var canDismiss = false if let channel = peerViewMainPeer(channelView) as? TelegramChannel { if let initialParticipant = initialParticipant { if channel.flags.contains(.isCreator) { - canDismiss = true + canDismiss = initialParticipant.adminInfo != nil } else { switch initialParticipant { case .creator: break - case let .member(_, _, adminInfo, _): + case let .member(_, _, adminInfo, _, _): if let adminInfo = adminInfo { - if adminInfo.promotedBy == account.peerId || adminInfo.canBeEditedByAccountPeer { + if adminInfo.promotedBy == context.account.peerId || adminInfo.canBeEditedByAccountPeer { canDismiss = true } } @@ -526,10 +875,10 @@ class ChannelAdminController: ModalViewController { } } } - let result = channelAdminControllerEntries(state: state, accountPeerId: account.peerId, channelView: channelView, adminView: adminView, initialParticipant: initialParticipant) - let entries = result.0.map({AppearanceWrapperEntry(entry: $0, appearance: appearance)}) - _ = stateValue.modify({$0.withUpdatedUpdatedFlags(result.1).withUpdatedEditable(canEdit)}) - return (transition: prepareTransition(left: previous.swap(entries), right: entries, initialSize: initialSize.modify{$0}, arguments: arguments), canEdit: canEdit, canDismiss: canDismiss) + let result = channelAdminControllerEntries(state: state, accountPeerId: context.account.peerId, channelView: channelView, adminView: adminView, initialParticipant: initialParticipant) + let entries = result.map({AppearanceWrapperEntry(entry: $0, appearance: appearance)}) + _ = stateValue.modify({$0.withUpdatedEditable(canEdit)}) + return (transition: prepareTransition(left: previous.swap(entries), right: entries, initialSize: initialSize.modify{$0}, arguments: arguments), canEdit: canEdit, canDismiss: canDismiss, channelView: channelView) } |> afterDisposed { actionsDisposable.dispose() @@ -543,78 +892,225 @@ class ChannelAdminController: ModalViewController { self?.updateSize(updatedSize.swap(true)) self?.modal?.interactions?.updateDone { button in - button.set(text: tr(.modalOK), for: .Normal) - let flags = (stateValue.modify({$0}).updatedFlags ?? []).subtracting(.canChangeInviteLink) - button.isEnabled = !flags.isEmpty - if !values.canEdit { - button.isEnabled = true - button.set(text: tr(.navigationDone), for: .Normal) - } + + button.isEnabled = values.canEdit + button.set(text: L10n.navigationDone, for: .Normal) } - self?.modal?.interactions?.updateCancel { button in - button.set(text: values.canDismiss ? tr(.channelAdminDismiss) : "", for: .Normal) - button.set(color: values.canDismiss ? theme.colors.redUI : theme.colors.blueText, for: .Normal) + + self?.okClick = { + if let channel = values.channelView.peers[values.channelView.peerId] as? TelegramChannel { + if let initialParticipant = initialParticipant { + var updateFlags: TelegramChatAdminRightsFlags? + updateState { current in + updateFlags = current.updatedFlags + if let _ = updateFlags { + return current.withUpdatedUpdating(true) + } else { + return current + } + } + + if updateFlags == nil { + switch initialParticipant { + case let .creator(_, info, _): + if stateValue.with ({ $0.rank != $0.initialRank }) { + updateFlags = info?.rights.rights ?? .groupSpecific + } + case let .member(member): + if member.adminInfo?.rights == nil { + let maskRightsFlags: TelegramChatAdminRightsFlags + switch channel.info { + case .broadcast: + maskRightsFlags = .broadcastSpecific + case .group: + maskRightsFlags = .groupSpecific + } + + if channel.flags.contains(.isCreator) { + updateFlags = maskRightsFlags.subtracting([.canAddAdmins, .canBeAnonymous]) + } else if let adminRights = channel.adminRights { + updateFlags = maskRightsFlags.intersection(adminRights.rights).subtracting([.canAddAdmins, .canBeAnonymous]) + } else { + updateFlags = [] + } + } + } + } + if updateFlags == nil && stateValue.with ({ $0.rank != $0.initialRank }) { + updateFlags = initialParticipant.adminInfo?.rights.rights + } + + if let updateFlags = updateFlags { + updateState { current in + return current.withUpdatedUpdating(true) + } + updateRightsDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(peerId: peerId, memberId: adminId, adminRights: TelegramChatAdminRights(rights: updateFlags), rank: stateValue.with { $0.rank }) |> deliverOnMainQueue).start(error: { error in + + }, completed: { + updated(TelegramChatAdminRights(rights: updateFlags)) + dismissImpl() + })) + } else { + dismissImpl() + } + } else if values.canEdit { + var updateFlags: TelegramChatAdminRightsFlags? + updateState { current in + updateFlags = current.updatedFlags + return current.withUpdatedUpdating(true) + } + + if updateFlags == nil { + let maskRightsFlags: TelegramChatAdminRightsFlags + switch channel.info { + case .broadcast: + maskRightsFlags = .broadcastSpecific + case .group: + maskRightsFlags = .groupSpecific + } + + if channel.flags.contains(.isCreator) { + updateFlags = maskRightsFlags.subtracting(.canAddAdmins) + } else if let adminRights = channel.adminRights { + updateFlags = maskRightsFlags.intersection(adminRights.rights).subtracting(.canAddAdmins) + } else { + updateFlags = [] + } + } + + + + if let updateFlags = updateFlags { + updateState { current in + return current.withUpdatedUpdating(true) + } + updateRightsDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(peerId: peerId, memberId: adminId, adminRights: TelegramChatAdminRights(rights: updateFlags), rank: stateValue.with { $0.rank }) |> deliverOnMainQueue).start(error: { _ in + + }, completed: { + updated(TelegramChatAdminRights(rights: updateFlags)) + dismissImpl() + })) + } + } + } else if let _ = values.channelView.peers[values.channelView.peerId] as? TelegramGroup { + var updateFlags: TelegramChatAdminRightsFlags? + updateState { current in + updateFlags = current.updatedFlags + return current + } + + let maskRightsFlags: TelegramChatAdminRightsFlags = .groupSpecific + let defaultFlags = maskRightsFlags.subtracting(.canAddAdmins) + + if updateFlags == nil { + updateFlags = defaultFlags + } + + if let updateFlags = updateFlags { + if initialParticipant?.adminInfo == nil && updateFlags == defaultFlags && stateValue.with ({ $0.rank == $0.initialRank }) { + updateState { current in + return current.withUpdatedUpdating(true) + } + updateRightsDisposable.set((context.engine.peers.addGroupAdmin(peerId: peerId, adminId: adminId) + |> deliverOnMainQueue).start(completed: { + dismissImpl() + })) + } else if updateFlags != defaultFlags || stateValue.with ({ $0.rank != $0.initialRank }) { + let signal = context.engine.peers.convertGroupToSupergroup(peerId: peerId) + |> map(Optional.init) + |> deliverOnMainQueue + |> `catch` { error -> Signal in + switch error { + case .tooManyChannels: + showInactiveChannels(context: context, source: .upgrade) + case .generic: + alert(for: context.window, info: L10n.unknownError) + } + updateState { current in + return current.withUpdatedUpdating(false) + } + return .single(nil) + } + |> mapToSignal { upgradedPeerId -> Signal in + guard let upgradedPeerId = upgradedPeerId else { + return .single(nil) + } + + return context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(peerId: upgradedPeerId, memberId: adminId, adminRights: TelegramChatAdminRights(rights: updateFlags), rank: stateValue.with { $0.rank }) + |> mapToSignal { _ -> Signal in + return .complete() + } + |> then(.single(upgradedPeerId)) + } + |> deliverOnMainQueue + + updateState { current in + return current.withUpdatedUpdating(true) + } + + + updateRightsDisposable.set(showModalProgress(signal: signal, for: mainWindow).start(next: { upgradedPeerId in + if let upgradedPeerId = upgradedPeerId { + let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.admins(peerId: upgradedPeerId, updated: { state in + if case .ready = state.loadingState { + upgradedToSupergroup(upgradedPeerId, { + + }) + dismissImpl() + } + }) + actionsDisposable.add(disposable) + + } + }, error: { _ in + updateState { current in + return current.withUpdatedUpdating(false) + } + })) + } else { + dismissImpl() + } + } else { + dismissImpl() + } + } } })) } - private func updateSize(_ animated: Bool) { - if let contentSize = self.window?.contentView?.frame.size { - self.modal?.resize(with:NSMakeSize(genericView.frame.width, min(contentSize.height - 70, genericView.listHeight)), animated: animated) - } - } deinit { disposable.dispose() } - func updateRights(_ updateFlags: TelegramChannelAdminRightsFlags) { - close() - - if !stateValue.modify({$0}).editable { - return - } - - let updated = self.updated - - _ = showModalProgress(signal: updatePeerAdminRights(account: account, peerId: peerId, adminId: adminId, rights: TelegramChannelAdminRights(flags: updateFlags)) |> deliverOnMainQueue, for: mainWindow).start(error: { error in - alert(for: mainWindow, info: tr(.channelAdminsAddAdminError)) - }, completed: { - updated(TelegramChannelAdminRights(flags: updateFlags)) - }) + override func close(animationType: ModalAnimationCloseBehaviour = .common) { + disposable.set(nil) + super.close(animationType: animationType) } - func addAdmin(_ updateFlags: TelegramChannelAdminRightsFlags) { - close() - _ = showModalProgress(signal: addPeerAdmin(account: account, peerId: peerId, adminId: adminId, adminRightsFlags: updateFlags) |> deliverOnMainQueue, for: mainWindow).start(error: { error in - - }, completed: { [weak self] in - self?.updated(TelegramChannelAdminRights(flags: updateFlags)) - }) - + override func firstResponder() -> NSResponder? { + let view = self.genericView.item(stableId: ChannelAdminEntryStableId.role)?.view as? InputDataRowView + return view?.textView + } + + + override func returnKeyAction() -> KeyHandlerResult { + self.okClick?() + return .invoked + } + + override var modalHeader: (left: ModalHeaderData?, center: ModalHeaderData?, right: ModalHeaderData?)? { + return (left: nil, center: ModalHeaderData(title: L10n.adminsAdmin), right: nil) } override var modalInteractions: ModalInteractions? { - return ModalInteractions(acceptTitle: tr(.modalOK), accept: { [weak self] in - if let _ = self?.initialParticipant { - if let updatedFlags = self?.stateValue.modify({$0}).updatedFlags { - self?.updateRights(updatedFlags) - } else { - self?.close() - } - } else { - if let updatedFlags = self?.stateValue.modify({$0}).updatedFlags { - self?.addAdmin(updatedFlags) - } - } - - }, cancelTitle: tr(.modalCancel), cancel: { [weak self] in - self?.arguments?.dismissAdmin() - }, height: 40) + return ModalInteractions(acceptTitle: tr(L10n.modalOK), accept: { [weak self] in + self?.okClick?() + }, drawBorder: true, height: 50, singleButton: true) } } diff --git a/Telegram-Mac/ChannelAdminsViewController.swift b/Telegram-Mac/ChannelAdminsViewController.swift index dadc3a4c3c..2508cff2e3 100644 --- a/Telegram-Mac/ChannelAdminsViewController.swift +++ b/Telegram-Mac/ChannelAdminsViewController.swift @@ -8,21 +8,19 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac +import Postbox +import TelegramCore + +import SwiftSignalKit fileprivate final class ChannelAdminsControllerArguments { - let account: Account - - let updateCurrentAdministrationType: () -> Void + let context: AccountContext let addAdmin: () -> Void let openAdmin: (RenderedChannelParticipant) -> Void let removeAdmin: (PeerId) -> Void let eventLogs:() -> Void - init(account:Account, updateCurrentAdministrationType:@escaping()->Void, addAdmin:@escaping()->Void, openAdmin:@escaping(RenderedChannelParticipant) -> Void, removeAdmin:@escaping(PeerId)->Void, eventLogs: @escaping()->Void) { - self.account = account - self.updateCurrentAdministrationType = updateCurrentAdministrationType + init(context: AccountContext, addAdmin:@escaping()->Void, openAdmin:@escaping(RenderedChannelParticipant) -> Void, removeAdmin:@escaping(PeerId)->Void, eventLogs: @escaping()->Void) { + self.context = context self.addAdmin = addAdmin self.openAdmin = openAdmin self.removeAdmin = removeAdmin @@ -41,42 +39,19 @@ fileprivate enum ChannelAdminsEntryStableId: Hashable { return peerId.hashValue } } - - static func ==(lhs: ChannelAdminsEntryStableId, rhs: ChannelAdminsEntryStableId) -> Bool { - switch lhs { - case let .index(index): - if case .index(index) = rhs { - return true - } else { - return false - } - case let .peer(peerId): - if case .peer(peerId) = rhs { - return true - } else { - return false - } - } - } } fileprivate enum ChannelAdminsEntry : Identifiable, Comparable { - case administrationType(sectionId:Int32, CurrentAdministrationType) - case administrationInfo(sectionId:Int32, String) - case eventLogs(sectionId:Int32) - case adminsHeader(sectionId:Int32, String) - case adminPeerItem(sectionId:Int32, Int32, RenderedChannelParticipant, ShortPeerDeleting?) - case addAdmin(sectionId:Int32) - case adminsInfo(sectionId:Int32, String) + case eventLogs(sectionId:Int32, GeneralViewType) + case adminsHeader(sectionId:Int32, String, GeneralViewType) + case adminPeerItem(sectionId:Int32, Int32, RenderedChannelParticipant, ShortPeerDeleting?, GeneralViewType) + case addAdmin(sectionId:Int32, GeneralViewType) + case adminsInfo(sectionId:Int32, String, GeneralViewType) case section(Int32) case loading var stableId: ChannelAdminsEntryStableId { switch self { - case .administrationType: - return .index(0) - case .administrationInfo: - return .index(1) case .adminsHeader: return .index(2) case .addAdmin: @@ -89,96 +64,25 @@ fileprivate enum ChannelAdminsEntry : Identifiable, Comparable { return .index(6) case let .section(sectionId): return .index((sectionId + 1) * 1000 - sectionId) - case let .adminPeerItem(_, _, participant, _): + case let .adminPeerItem(_, _, participant, _, _): return .peer(participant.peer.id) } } - - static func ==(lhs: ChannelAdminsEntry, rhs: ChannelAdminsEntry) -> Bool { - switch lhs { - case let .administrationType(_,type): - if case .administrationType(_,type) = rhs { - return true - } else { - return false - } - case let .administrationInfo(_,text): - if case .administrationInfo(_,text) = rhs { - return true - } else { - return false - } - case let .loading: - if case .loading = rhs { - return true - } else { - return false - } - case let .eventLogs(sectionId): - if case .eventLogs(sectionId) = rhs { - return true - } else { - return false - } - case let .adminsHeader(_,title): - if case .adminsHeader(_,title) = rhs { - return true - } else { - return false - } - case let .adminPeerItem(_,lhsIndex, lhsParticipant, lhsEditing): - if case let .adminPeerItem(_,rhsIndex, rhsParticipant, rhsEditing) = rhs { - if lhsIndex != rhsIndex { - return false - } - if lhsParticipant != rhsParticipant { - return false - } - if lhsEditing != rhsEditing { - return false - } - return true - } else { - return false - } - case let .adminsInfo(_,text): - if case .adminsInfo(_,text) = rhs { - return true - } else { - return false - } - case .addAdmin: - if case .addAdmin = rhs { - return true - } else { - return false - } - case let .section(section): - if case .section(section) = rhs { - return true - } else { - return false - } - } - } + var index:Int32 { switch self { case .loading: return 0 - case let .eventLogs(sectionId): + case let .eventLogs(sectionId, _): return (sectionId * 1000) + 1 - case let .administrationType(sectionId, _): + case let .adminsHeader(sectionId, _, _): return (sectionId * 1000) + 2 - case let .administrationInfo(sectionId, _): + case let .addAdmin(sectionId, _): return (sectionId * 1000) + 3 - case let .adminsHeader(sectionId, _): + case let .adminsInfo(sectionId, _, _): return (sectionId * 1000) + 4 - case let .addAdmin(sectionId): - return (sectionId * 1000) + 5 - case let .adminsInfo(sectionId, _): - return (sectionId * 1000) + 6 - case let .adminPeerItem(sectionId, index, _, _): + case let .adminPeerItem(sectionId, index, _, _, _): return (sectionId * 1000) + index + 20 case let .section(sectionId): return (sectionId + 1) * 1000 - sectionId @@ -190,86 +94,55 @@ fileprivate enum ChannelAdminsEntry : Identifiable, Comparable { } } -fileprivate enum CurrentAdministrationType { - case everyoneCanAddMembers - case adminsCanAddMembers -} fileprivate struct ChannelAdminsControllerState: Equatable { - let selectedType: CurrentAdministrationType? let editing: Bool let removingPeerId: PeerId? let removedPeerIds: Set let temporaryAdmins: [RenderedChannelParticipant] init() { - self.selectedType = nil self.editing = false self.removingPeerId = nil self.removedPeerIds = Set() self.temporaryAdmins = [] } - init(selectedType: CurrentAdministrationType?, editing: Bool, removingPeerId: PeerId?, removedPeerIds: Set, temporaryAdmins: [RenderedChannelParticipant]) { - self.selectedType = selectedType + init(editing: Bool, removingPeerId: PeerId?, removedPeerIds: Set, temporaryAdmins: [RenderedChannelParticipant]) { self.editing = editing self.removingPeerId = removingPeerId self.removedPeerIds = removedPeerIds self.temporaryAdmins = temporaryAdmins } - static func ==(lhs: ChannelAdminsControllerState, rhs: ChannelAdminsControllerState) -> Bool { - if lhs.selectedType != rhs.selectedType { - return false - } - if lhs.editing != rhs.editing { - return false - } - if lhs.removingPeerId != rhs.removingPeerId { - return false - } - if lhs.removedPeerIds != rhs.removedPeerIds { - return false - } - if lhs.temporaryAdmins != rhs.temporaryAdmins { - return false - } - - return true - } - - func withUpdatedSelectedType(_ selectedType: CurrentAdministrationType?) -> ChannelAdminsControllerState { - return ChannelAdminsControllerState(selectedType: selectedType, editing: self.editing, removingPeerId: self.removingPeerId, removedPeerIds: self.removedPeerIds, temporaryAdmins: self.temporaryAdmins) - } func withUpdatedEditing(_ editing: Bool) -> ChannelAdminsControllerState { - return ChannelAdminsControllerState(selectedType: self.selectedType, editing: editing, removingPeerId: self.removingPeerId, removedPeerIds: self.removedPeerIds, temporaryAdmins: self.temporaryAdmins) + return ChannelAdminsControllerState(editing: editing, removingPeerId: self.removingPeerId, removedPeerIds: self.removedPeerIds, temporaryAdmins: self.temporaryAdmins) } func withUpdatedPeerIdWithRevealedOptions(_ peerIdWithRevealedOptions: PeerId?) -> ChannelAdminsControllerState { - return ChannelAdminsControllerState(selectedType: self.selectedType, editing: self.editing, removingPeerId: self.removingPeerId, removedPeerIds: self.removedPeerIds, temporaryAdmins: self.temporaryAdmins) + return ChannelAdminsControllerState(editing: self.editing, removingPeerId: self.removingPeerId, removedPeerIds: self.removedPeerIds, temporaryAdmins: self.temporaryAdmins) } func withUpdatedRemovingPeerId(_ removingPeerId: PeerId?) -> ChannelAdminsControllerState { - return ChannelAdminsControllerState(selectedType: self.selectedType, editing: self.editing, removingPeerId: removingPeerId, removedPeerIds: self.removedPeerIds, temporaryAdmins: self.temporaryAdmins) + return ChannelAdminsControllerState(editing: self.editing, removingPeerId: removingPeerId, removedPeerIds: self.removedPeerIds, temporaryAdmins: self.temporaryAdmins) } func withUpdatedRemovedPeerIds(_ removedPeerIds: Set) -> ChannelAdminsControllerState { - return ChannelAdminsControllerState(selectedType: self.selectedType, editing: self.editing, removingPeerId: self.removingPeerId, removedPeerIds: removedPeerIds, temporaryAdmins: self.temporaryAdmins) + return ChannelAdminsControllerState(editing: self.editing, removingPeerId: self.removingPeerId, removedPeerIds: removedPeerIds, temporaryAdmins: self.temporaryAdmins) } func withUpdatedTemporaryAdmins(_ temporaryAdmins: [RenderedChannelParticipant]) -> ChannelAdminsControllerState { - return ChannelAdminsControllerState(selectedType: self.selectedType, editing: self.editing, removingPeerId: self.removingPeerId, removedPeerIds: self.removedPeerIds, temporaryAdmins: temporaryAdmins) + return ChannelAdminsControllerState(editing: self.editing, removingPeerId: self.removingPeerId, removedPeerIds: self.removedPeerIds, temporaryAdmins: temporaryAdmins) } } -private func ChannelAdminsControllerEntries(view: PeerView, state: ChannelAdminsControllerState, participants: [RenderedChannelParticipant]?, isCreator: Bool) -> [ChannelAdminsEntry] { +private func channelAdminsControllerEntries(accountPeerId: PeerId, view: PeerView, state: ChannelAdminsControllerState, participants: [RenderedChannelParticipant]?, isCreator: Bool) -> [ChannelAdminsEntry] { var entries: [ChannelAdminsEntry] = [] - guard let participants = participants else { - return [.loading] - } + let participants = participants ?? [] + var sectionId:Int32 = 1 entries.append(.section(sectionId)) @@ -277,47 +150,45 @@ private func ChannelAdminsControllerEntries(view: PeerView, state: ChannelAdmins if let peer = view.peers[view.peerId] as? TelegramChannel { var isGroup = false - if case let .group(info) = peer.info { + if case .group = peer.info { isGroup = true - - if isCreator { - let selectedType: CurrentAdministrationType - if let current = state.selectedType { - selectedType = current - } else { - if info.flags.contains(.everyMemberCanInviteMembers) { - selectedType = .everyoneCanAddMembers - } else { - selectedType = .adminsCanAddMembers - } - } - - entries.append(.administrationType(sectionId: sectionId, selectedType)) - let infoText: String - switch selectedType { - case .everyoneCanAddMembers: - infoText = tr(.adminsEverbodyCanAddMembers) - case .adminsCanAddMembers: - infoText = tr(.adminsOnlyAdminsCanAddMembers) - } - entries.append(.administrationInfo(sectionId: sectionId, infoText)) - - } - } - entries.append(.eventLogs(sectionId: sectionId)) + entries.append(.eventLogs(sectionId: sectionId, .singleItem)) entries.append(.section(sectionId)) sectionId += 1 - entries.append(.adminsHeader(sectionId: sectionId, isGroup ? tr(.adminsGroupAdmins) : tr(.adminsChannelAdmins))) + entries.append(.adminsHeader(sectionId: sectionId, isGroup ? L10n.adminsGroupAdmins : L10n.adminsChannelAdmins, .textTopItem)) + + + if peer.hasPermission(.addAdmins) { + entries.append(.addAdmin(sectionId: sectionId, .singleItem)) + entries.append(.adminsInfo(sectionId: sectionId, isGroup ? L10n.adminsGroupDescription : L10n.adminsChannelDescription, .textBottomItem)) + + entries.append(.section(sectionId)) + sectionId += 1 + } + var index: Int32 = 0 - for participant in participants.sorted(by: <) { + for (i, participant) in participants.sorted(by: <).enumerated() { var editable = true - if case .creator = participant.participant { + switch participant.participant { + case .creator: editable = false + case let .member(id, _, adminInfo, _, _): + if id == accountPeerId { + editable = false + } else if let adminInfo = adminInfo { + if peer.flags.contains(.isCreator) || adminInfo.promotedBy == accountPeerId { + editable = true + } else { + editable = false + } + } else { + editable = false + } } let editing:ShortPeerDeleting? @@ -327,18 +198,79 @@ private func ChannelAdminsControllerEntries(view: PeerView, state: ChannelAdmins editing = nil } - entries.append(.adminPeerItem(sectionId: sectionId, index, participant, editing)) + entries.append(.adminPeerItem(sectionId: sectionId, index, participant, editing, bestGeneralViewType(participants, for: i))) index += 1 } + if index > 0 { + entries.append(.section(sectionId)) + sectionId += 1 + + } + } else if let peer = view.peers[view.peerId] as? TelegramGroup { + + entries.append(.adminsHeader(sectionId: sectionId, L10n.adminsGroupAdmins, .textTopItem)) - if peer.hasAdminRights(.canAddAdmins) { + if case .creator = peer.role { + entries.append(.addAdmin(sectionId: sectionId, .singleItem)) + entries.append(.adminsInfo(sectionId: sectionId, L10n.adminsGroupDescription, .textBottomItem)) + + entries.append(.section(sectionId)) + sectionId += 1 + } + + + var combinedParticipants: [RenderedChannelParticipant] = participants + var existingParticipantIds = Set() + for participant in participants { + existingParticipantIds.insert(participant.peer.id) + } + + for participant in state.temporaryAdmins { + if !existingParticipantIds.contains(participant.peer.id) { + combinedParticipants.append(participant) + } + } + + var index: Int32 = 0 + let participants = combinedParticipants.sorted(by: <).filter { + !state.removedPeerIds.contains($0.peer.id) + } + for (i, participant) in participants.enumerated() { + var editable = true + switch participant.participant { + case .creator: + editable = false + case let .member(id, _, adminInfo, _, _): + if id == accountPeerId { + editable = false + } else if let adminInfo = adminInfo { + var creator: Bool = false + if case .creator = peer.role { + creator = true + } + if creator || adminInfo.promotedBy == accountPeerId { + editable = true + } else { + editable = false + } + } else { + editable = false + } + } + let editing:ShortPeerDeleting? + if state.editing { + editing = ShortPeerDeleting(editable: editable) + } else { + editing = nil + } + entries.append(.adminPeerItem(sectionId: sectionId, index, participant, editing, bestGeneralViewType(participants, for: i))) + index += 1 + } + if index > 0 { entries.append(.section(sectionId)) sectionId += 1 - - entries.append(.addAdmin(sectionId: sectionId)) - entries.append(.adminsInfo(sectionId: sectionId, isGroup ? tr(.adminsGroupDescription) : tr(.adminsChannelDescription))) } } @@ -350,32 +282,16 @@ fileprivate func prepareTransition(left:[AppearanceWrapperEntry TableRowItem in switch entry.entry { - case let .administrationType(_, type): - let label: String - switch type { - case .adminsCanAddMembers: - label = tr(.adminsWhoCanInviteAdmins) - case .everyoneCanAddMembers: - label = tr(.adminsWhoCanInviteEveryone) - } - return GeneralInteractedRowItem(initialSize, stableId: entry.stableId, name: tr(.adminsWhoCanInviteText), type: .context(stateback: { () -> String in - return label - }), action: { - arguments.updateCurrentAdministrationType() - }) - - case let .administrationInfo(_, text), let .adminsHeader(_, text), let .adminsInfo(_, text): - return GeneralTextRowItem(initialSize, stableId: entry.stableId, text: text) - case let .adminPeerItem(_, _, participant, editing): + case let .adminPeerItem(_, _, participant, editing, viewType): let peerText: String switch participant.participant { case .creator: - peerText = tr(.adminsCreator) - case let .member(_, _, adminInfo, _): + peerText = L10n.adminsOwner + case let .member(_, _, adminInfo, _, _): if let adminInfo = adminInfo, let peer = participant.peers[adminInfo.promotedBy] { - peerText = tr(.channelAdminsPromotedBy(peer.displayTitle)) + peerText = L10n.channelAdminsPromotedBy(peer.displayTitle) } else { - peerText = tr(.adminsAdmin) + peerText = L10n.adminsAdmin } } @@ -389,24 +305,26 @@ fileprivate func prepareTransition(left:[AppearanceWrapperEntry { private let disposable:MetaDisposable = MetaDisposable() private let removeAdminDisposable:MetaDisposable = MetaDisposable() private let openPeerDisposable:MetaDisposable = MetaDisposable() - init(account:Account, peerId:PeerId) { + init( _ context:AccountContext, peerId:PeerId) { self.peerId = peerId - super.init(account) + super.init(context) } let actionsDisposable = DisposableSet() @@ -436,10 +354,22 @@ class ChannelAdminsViewController: EditableViewController { override func viewDidLoad() { super.viewDidLoad() - let account = self.account + genericView.getBackgroundColor = { + theme.colors.listBackground + } + + let context = self.context let peerId = self.peerId - let adminsPromise = Promise<[RenderedChannelParticipant]?>(nil) + + var upgradedToSupergroupImpl: ((PeerId, @escaping () -> Void) -> Void)? + + let upgradedToSupergroup: (PeerId, @escaping () -> Void) -> Void = { upgradedPeerId, f in + upgradedToSupergroupImpl?(upgradedPeerId, f) + } + + + let adminsPromise = ValuePromise<[RenderedChannelParticipant]?>(nil) let updateState: ((ChannelAdminsControllerState) -> ChannelAdminsControllerState) -> Void = { [weak self] f in if let strongSelf = self { @@ -449,209 +379,116 @@ class ChannelAdminsViewController: EditableViewController { let viewValue:Atomic = Atomic(value: nil) - let applyAdmin:(RenderedChannelParticipant, PeerId, TelegramChannelAdminRights) -> Void = { [weak self] participant, adminId, updatedRights in - - - let applyAdmin: Signal = combineLatest(adminsPromise.get(), account.postbox.loadedPeerWithId(adminId)) - |> filter { $0.0 != nil } - |> take(1) - |> deliverOnMainQueue - |> mapToSignal { admins, peer -> Signal in - if let admins = admins { - let additionalPeers = viewValue.modify({$0})?.peers ?? [:] - var updatedAdmins = admins - if updatedRights.isEmpty { - for i in 0 ..< updatedAdmins.count { - if updatedAdmins[i].peer.id == adminId { - updatedAdmins.remove(at: i) - break - } - } - } else { - var found = false - for i in 0 ..< updatedAdmins.count { - if updatedAdmins[i].peer.id == adminId { - if case let .member(id, date, _, banInfo) = updatedAdmins[i].participant { - updatedAdmins[i] = RenderedChannelParticipant(participant: .member(id: id, invitedAt: date, adminInfo: ChannelParticipantAdminInfo(rights: updatedRights, promotedBy: account.peerId, canBeEditedByAccountPeer: true), banInfo: nil), peer: updatedAdmins[i].peer, peers: participant.peers + additionalPeers) - } - found = true - break - } - } - if !found { - updatedAdmins.append(RenderedChannelParticipant(participant: .member(id: adminId, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: updatedRights, promotedBy: account.peerId, canBeEditedByAccountPeer: true), banInfo: nil), peer: peer, peers: participant.peers + additionalPeers)) - } - } - adminsPromise.set(.single(updatedAdmins)) - } - - return account.context.cachedAdminIds.ids(postbox: account.postbox, network: account.network, peerId: peerId) |> take(1) |> mapToSignal { _ in - return Signal.complete() - } - } - self?.addAdminDisposable.set(applyAdmin.start()) - } - let arguments = ChannelAdminsControllerArguments(account: account, updateCurrentAdministrationType: { [weak self] in + let arguments = ChannelAdminsControllerArguments(context: context, addAdmin: { + let behavior = peerId.namespace == Namespaces.Peer.CloudGroup ? SelectGroupMembersBehavior(peerId: peerId, limit: 1) : SelectChannelMembersBehavior(peerId: peerId, peerChannelMemberContextsManager: context.peerChannelMemberCategoriesContextsManager, limit: 1) - if let item = self?.genericView.item(stableId: AnyHashable(ChannelAdminsEntryStableId.index(0))) { - if let view = (self?.genericView.viewNecessary(at: item.index) as? GeneralInteractedRowView)?.textView { - let result = ValuePromise() - - let items = [SPopoverItem(tr(.adminsWhoCanInviteEveryone), { - result.set(true) - - }), SPopoverItem(tr(.adminsWhoCanInviteAdmins), { - result.set(false) - })] - - let updateSignal = result.get() - |> take(1) - |> mapToSignal { value -> Signal in - updateState { state in - return state.withUpdatedSelectedType(value ? .everyoneCanAddMembers : .adminsCanAddMembers) - } - - return account.postbox.loadedPeerWithId(peerId) - |> mapToSignal { peer -> Signal in - if let peer = peer as? TelegramChannel, case let .group(info) = peer.info { - var updatedValue: Bool? - if value && !info.flags.contains(.everyMemberCanInviteMembers) { - updatedValue = true - } else if !value && info.flags.contains(.everyMemberCanInviteMembers) { - updatedValue = false - } - if let updatedValue = updatedValue { - return updateGroupManagementType(account: account, peerId: peerId, type: updatedValue ? .unrestricted : .restrictedToAdmins) - } else { - return .complete() - } - } else { - return .complete() - } - } - } - self?.updateAdministrationDisposable.set(updateSignal.start()) - - showPopover(for: view, with: SPopoverViewController(items: items), edge: .minX, inset: NSMakePoint(0,-30)) - } - } - - }, addAdmin: { - let behavior = SelectChannelMembersBehavior(peerId: peerId, limit: 1) - - _ = (selectModalPeers(account: account, title: "", limit: 1, behavior: behavior, confirmation: { peerIds in - if let peerId = peerIds.first, let peerView = viewValue.modify({$0}), let channel = peerViewMainPeer(peerView) as? TelegramChannel { - if let participant = behavior.participants[peerId] { - switch participant.participant { - case .creator: - return .single(false) - case .member(_, _, let adminInfo, let banInfo): - if let adminInfo = adminInfo { - //if channel.flags.contains(.isCreator) && adminInfo.promotedBy != account.peerId && !adminInfo.canBeEditedByAccountPeer { - //alert(for: mainWindow, info: tr(.channelAdminsAddAdminError)) - // return .single(false) - //} - return .single(true) - } else { - if let _ = channel.adminRights { - if let _ = banInfo { - if !channel.hasAdminRights(.canBanUsers) { - alert(for: mainWindow, info: tr(.channelAdminsPromoteBannedAdminError)) - return .single(false) - } - } - } - - return .single(true) - } - } - } else { - if !channel.hasAdminRights(.canInviteUsers) { - alert(for: mainWindow, info: tr(.channelAdminsPromoteUnmemberAdminError)) - return .single(false) - } - } + _ = (selectModalPeers(window: context.window, context: context, title: L10n.adminsAddAdmin, limit: 1, behavior: behavior, confirmation: { peerIds in + if let _ = behavior.participants[peerId] { + return .single(true) + } else { + return .single(true) } - return .single(true) }) |> map {$0.first}).start(next: { adminId in if let adminId = adminId { - - showModal(with: ChannelAdminController(account: account, peerId: peerId, adminId: adminId, initialParticipant: behavior.participants[adminId]?.participant, updated: { updatedRights in - if let participant = behavior.participants[adminId] { - applyAdmin(participant, adminId, updatedRights) - } - - }), for: mainWindow) + showModal(with: ChannelAdminController(context, peerId: peerId, adminId: adminId, initialParticipant: behavior.participants[adminId]?.participant, updated: { _ in }, upgradedToSupergroup: upgradedToSupergroup), for: mainWindow) } }) }, openAdmin: { participant in - if case let .member(adminId, _, _, _) = participant.participant { - showModal(with: ChannelAdminController(account: account, peerId: peerId, adminId: participant.peer.id, initialParticipant: participant.participant, updated: { updatedRights in - applyAdmin(participant, adminId, updatedRights) - - }), for: mainWindow) - } + showModal(with: ChannelAdminController(context, peerId: peerId, adminId: participant.peer.id, initialParticipant: participant.participant, updated: { _ in }, upgradedToSupergroup: upgradedToSupergroup), for: mainWindow) }, removeAdmin: { [weak self] adminId in updateState { return $0.withUpdatedRemovingPeerId(adminId) } - let applyPeers: Signal = adminsPromise.get() - |> filter { $0 != nil } - |> take(1) - |> deliverOnMainQueue - |> mapToSignal { peers -> Signal in - if let peers = peers { - var updatedPeers = peers - for i in 0 ..< updatedPeers.count { - if updatedPeers[i].peer.id == adminId { - updatedPeers.remove(at: i) + if peerId.namespace == Namespaces.Peer.CloudGroup { + self?.removeAdminDisposable.set((context.engine.peers.removeGroupAdmin(peerId: peerId, adminId: adminId) + |> deliverOnMainQueue).start(completed: { + updateState { + return $0.withUpdatedRemovingPeerId(nil) + } + })) + } else { + self?.removeAdminDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(peerId: peerId, memberId: adminId, adminRights: nil, rank: nil) + |> deliverOnMainQueue).start(completed: { + updateState { + return $0.withUpdatedRemovingPeerId(nil) + } + })) + } + + }, eventLogs: { [weak self] in + self?.navigationController?.push(ChannelEventLogController(context, peerId: peerId)) + }) + + let peerView = Promise() + peerView.set(context.account.viewTracker.peerView(peerId)) + + + + let membersAndLoadMoreControl: (Disposable, PeerChannelMemberCategoryControl?) + if peerId.namespace == Namespaces.Peer.CloudChannel { + membersAndLoadMoreControl = context.peerChannelMemberCategoriesContextsManager.admins(peerId: peerId, updated: { membersState in + if case .loading = membersState.loadingState, membersState.list.isEmpty { + adminsPromise.set(nil) + } else { + adminsPromise.set(membersState.list) + } + }) + } else { + let membersDisposable = (peerView.get() + |> map { peerView -> [RenderedChannelParticipant]? in + guard let cachedData = peerView.cachedData as? CachedGroupData, let participants = cachedData.participants else { + return nil + } + var result: [RenderedChannelParticipant] = [] + var creatorPeer: Peer? + for participant in participants.participants { + if let peer = peerView.peers[participant.peerId] { + switch participant { + case .creator: + creatorPeer = peer + default: break } } - adminsPromise.set(.single(updatedPeers)) } - - return account.context.cachedAdminIds.ids(postbox: account.postbox, network: account.network, peerId: peerId) |> take(1) |> mapToSignal { _ in - return Signal.complete() + guard let creator = creatorPeer else { + return nil } - } - - self?.removeAdminDisposable.set((removePeerAdmin(account: account, peerId: peerId, adminId: adminId) - |> then(applyPeers |> mapError { _ -> RemovePeerAdminError in return .generic }) |> deliverOnMainQueue).start(error: { _ in - updateState { - return $0.withUpdatedRemovingPeerId(nil) - } - }, completed: { - updateState { state in - var updatedTemporaryAdmins = state.temporaryAdmins - for i in 0 ..< updatedTemporaryAdmins.count { - if updatedTemporaryAdmins[i].peer.id == adminId { - updatedTemporaryAdmins.remove(at: i) + for participant in participants.participants { + if let peer = peerView.peers[participant.peerId] { + switch participant { + case .creator: + result.append(RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: nil), peer: peer)) + case .admin: + var peers: [PeerId: Peer] = [:] + peers[creator.id] = creator + peers[peer.id] = peer + result.append(RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: .groupSpecific), promotedBy: creator.id, canBeEditedByAccountPeer: creator.id == context.account.peerId), banInfo: nil, rank: nil), peer: peer, peers: peers)) + case .member: break } } - return state.withUpdatedRemovingPeerId(nil).withUpdatedTemporaryAdmins(updatedTemporaryAdmins) } - })) - }, eventLogs: { [weak self] in - self?.navigationController?.push(ChannelEventLogController(account, peerId: peerId)) - }) - - let peerView = account.viewTracker.peerView(peerId) - + return result + }).start(next: { members in + adminsPromise.set(members) + }) + membersAndLoadMoreControl = (membersDisposable, nil) + } - let adminsSignal: Signal<[RenderedChannelParticipant]?, NoError> = .single(nil) |> then(channelAdmins(account: account, peerId: peerId) |> map { Optional($0) }) + let (membersDisposable, _) = membersAndLoadMoreControl + actionsDisposable.add(membersDisposable) - adminsPromise.set(adminsSignal) + let initialSize = atomicSize let previousEntries:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) - let signal = combineLatest(statePromise.get(), peerView, adminsPromise.get(), appearanceSignal) + let signal = combineLatest(statePromise.get(), peerView.get(), adminsPromise.get(), appearanceSignal) |> map { state, view, admins, appearance -> (TableUpdateTransition, Bool) in var isCreator = false @@ -661,7 +498,7 @@ class ChannelAdminsViewController: EditableViewController { isSupergroup = channel.isSupergroup } _ = viewValue.swap(view) - let entries = ChannelAdminsControllerEntries(view: view, state: state, participants: admins, isCreator: isCreator).map{AppearanceWrapperEntry(entry: $0, appearance: appearance)} + let entries = channelAdminsControllerEntries(accountPeerId: context.peerId, view: view, state: state, participants: admins, isCreator: isCreator).map{AppearanceWrapperEntry(entry: $0, appearance: appearance)} return (prepareTransition(left: previousEntries.swap(entries), right: entries, initialSize: initialSize.modify{$0}, arguments: arguments, isSupergroup: isSupergroup), isCreator) } @@ -672,6 +509,25 @@ class ChannelAdminsViewController: EditableViewController { })) + upgradedToSupergroupImpl = { [weak self] upgradedPeerId, f in + guard let `self` = self, let navigationController = self.navigationController else { + return + } + + let chatController = ChatController(context: context, chatLocation: .peer(upgradedPeerId)) + + navigationController.removeAll() + navigationController.push(chatController, false, style: .none) + let signal = chatController.ready.get() |> filter {$0} |> take(1) |> deliverOnMainQueue |> ignoreValues + + _ = signal.start(completed: { [weak navigationController] in + navigationController?.push(ChannelAdminsViewController(context, peerId: upgradedPeerId), false, style: .none) + f() + }) + + } + + } override func update(with state: ViewControllerState) { @@ -685,5 +541,6 @@ class ChannelAdminsViewController: EditableViewController { removeAdminDisposable.dispose() updateAdministrationDisposable.dispose() openPeerDisposable.dispose() + actionsDisposable.dispose() } } diff --git a/Telegram-Mac/ChannelBlacklistViewController.swift b/Telegram-Mac/ChannelBlacklistViewController.swift deleted file mode 100644 index 6dd818746c..0000000000 --- a/Telegram-Mac/ChannelBlacklistViewController.swift +++ /dev/null @@ -1,532 +0,0 @@ -// -// GroupBlackListViewController.swift -// Telegram -// -// Created by keepcoder on 22/02/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - -import Cocoa -import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac - - -private final class ChannelBlacklistControllerArguments { - let account: Account - - let removePeer: (PeerId) -> Void - let restrict:(RenderedChannelParticipant, Bool) -> Void - let addMember:()->Void - init(account: Account, removePeer: @escaping (PeerId) -> Void, restrict:@escaping(RenderedChannelParticipant, Bool) -> Void, addMember:@escaping()->Void) { - self.account = account - self.removePeer = removePeer - self.restrict = restrict - self.addMember = addMember - } -} - -private enum ChannelBlacklistEntryStableId: Hashable { - case peer(PeerId) - case empty - case addMember - case section(Int32) - case header(Int32) - var hashValue: Int { - switch self { - case let .peer(peerId): - return peerId.hashValue - case .empty: - return 0 - case .section: - return 1 - case .header: - return 2 - case .addMember: - return 3 - } - } - - static func ==(lhs: ChannelBlacklistEntryStableId, rhs: ChannelBlacklistEntryStableId) -> Bool { - switch lhs { - case let .peer(peerId): - if case .peer(peerId) = rhs { - return true - } else { - return false - } - case .empty: - if case .empty = rhs { - return true - } else { - return false - } - case .addMember: - if case .addMember = rhs { - return true - } else { - return false - } - case .section(let section): - if case .section(section) = rhs { - return true - } else { - return false - } - case .header(let index): - if case .header(index) = rhs { - return true - } else { - return false - } - - } - } -} - -private enum ChannelBlacklistEntry: Identifiable, Comparable { - case peerItem(Int32, Int32, RenderedChannelParticipant, ShortPeerDeleting?, Bool) - case empty(Bool) - case header(Int32, Int32, String) - case section(Int32) - case addMember(Int32, Int32) - var stableId: ChannelBlacklistEntryStableId { - switch self { - case let .peerItem(_, _, participant, _, _): - return .peer(participant.peer.id) - case .empty: - return .empty - case .section(let section): - return .section(section) - case .header(_, let index, _): - return .header(index) - case .addMember: - return .addMember - } - } - - static func ==(lhs: ChannelBlacklistEntry, rhs: ChannelBlacklistEntry) -> Bool { - switch lhs { - case let .peerItem(lhsSectionId, lhsIndex, lhsParticipant, lhsEditing, lhsEnabled): - if case let .peerItem(rhsSectionId, rhsIndex, rhsParticipant, rhsEditing, rhsEnabled) = rhs { - if lhsIndex != rhsIndex { - return false - } - if lhsSectionId != rhsSectionId { - return false - } - if lhsParticipant != rhsParticipant { - return false - } - if lhsEditing != rhsEditing { - return false - } - if lhsEnabled != rhsEnabled { - return false - } - return true - } else { - return false - } - case let .empty(loading): - if case .empty(loading) = rhs { - return true - } else { - return false - } - case let .section(id): - if case .section(id) = rhs { - return true - } else { - return false - } - case let .addMember(sectionId, index): - if case .addMember(sectionId, index) = rhs { - return true - } else { - return false - } - case let .header(sectionId, index, text): - if case .header(sectionId, index, text) = rhs { - return true - } else { - return false - } - } - } - - var index:Int32 { - switch self { - case let .section(section): - return (section * 1000) - section - case let .header(section, index, _): - return (section * 1000) + index - case let .addMember(section, index): - return (section * 1000) + index - case .empty: - return 0 - case let .peerItem(section, index, _, _, _): - return (section * 1000) + index - - } - } - - static func <(lhs: ChannelBlacklistEntry, rhs: ChannelBlacklistEntry) -> Bool { - return lhs.index < rhs.index - } - - func item(_ arguments: ChannelBlacklistControllerArguments, initialSize:NSSize) -> TableRowItem { - switch self { - case let .peerItem(_, _, participant, editing, enabled): - - let interactionType:ShortPeerItemInteractionType - if let editing = editing { - - interactionType = .deletable(onRemove: { peerId in - arguments.removePeer(peerId) - }, deletable: editing.editable) - } else { - interactionType = .plain - } - - var string:String = tr(.peerStatusRecently) - - if case let .member(_, _, _, banInfo) = participant.participant { - if let banInfo = banInfo, let peer = participant.peers[banInfo.restrictedBy] { - if banInfo.rights.flags.contains(.banReadMessages) { - string = tr(.channelBlacklistBlockedBy(peer.displayTitle)) - } else { - string = tr(.channelBlacklistRestrictedBy(peer.displayTitle)) - } - } else { - if let presence = participant.presences[participant.peer.id] as? TelegramUserPresence { - let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - (string,_, _) = stringAndActivityForUserPresence(presence, relativeTo: Int32(timestamp)) - } else if let peer = participant.peer as? TelegramUser, let botInfo = peer.botInfo { - string = botInfo.flags.contains(.hasAccessToChatHistory) ? tr(.peerInfoBotStatusHasAccess) : tr(.peerInfoBotStatusHasNoAccess) - } - } - } - - - - return ShortPeerRowItem(initialSize, peer: participant.peer, account: arguments.account, stableId: stableId, enabled: enabled, height:44, photoSize: NSMakeSize(32, 32), status: string, drawLastSeparator: true, inset: NSEdgeInsets(left: 30, right: 30), interactionType: interactionType, generalType: .none, action: { - if case .plain = interactionType { - arguments.restrict(participant, true) - } - }) - case let .empty(progress): - return SearchEmptyRowItem(initialSize, stableId: stableId, isLoading: progress, text: tr(.channelBlacklistEmptyDescrpition)) - case .section: - return GeneralRowItem(initialSize, height: 20, stableId: stableId) - case .header(_, _, let text): - return GeneralTextRowItem(initialSize, stableId: stableId, text: text, drawCustomSeparator: true, inset: NSEdgeInsets(left: 30.0, right: 30.0, top:2, bottom:6)) - case .addMember: - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.channelBlacklistAddMember), nameStyle: blueActionButton, action: { - arguments.addMember() - }) - } - } -} - -private struct ChannelBlacklistControllerState: Equatable { - let editing: Bool - let removingPeerId: PeerId? - - init() { - self.editing = false - self.removingPeerId = nil - } - - init(editing: Bool, removingPeerId: PeerId?) { - self.editing = editing - self.removingPeerId = removingPeerId - } - - static func ==(lhs: ChannelBlacklistControllerState, rhs: ChannelBlacklistControllerState) -> Bool { - if lhs.editing != rhs.editing { - return false - } - if lhs.removingPeerId != rhs.removingPeerId { - return false - } - - return true - } - - func withUpdatedEditing(_ editing: Bool) -> ChannelBlacklistControllerState { - return ChannelBlacklistControllerState(editing: editing, removingPeerId: self.removingPeerId) - } - - func withUpdatedPeerIdWithRevealedOptions(_ peerIdWithRevealedOptions: PeerId?) -> ChannelBlacklistControllerState { - return ChannelBlacklistControllerState(editing: self.editing, removingPeerId: self.removingPeerId) - } - - func withUpdatedRemovingPeerId(_ removingPeerId: PeerId?) -> ChannelBlacklistControllerState { - return ChannelBlacklistControllerState(editing: self.editing, removingPeerId: removingPeerId) - } -} - -private func channelBlacklistControllerEntries(view: PeerView, state: ChannelBlacklistControllerState, participants: ChannelBlacklist?) -> [ChannelBlacklistEntry] { - - var entries: [ChannelBlacklistEntry] = [] - - var index:Int32 = 0 - var sectionId:Int32 = 1 - - - - if let peer = peerViewMainPeer(view) as? TelegramChannel { - if peer.hasAdminRights(.canBanUsers) { - - entries.append(.section(sectionId)) - sectionId += 1 - - entries.append(.addMember(sectionId, index)) - index += 1 - } - - if let participants = participants { - if !participants.isEmpty { - entries.append(.section(sectionId)) - sectionId += 1 - } - - if !participants.restricted.isEmpty { - entries.append(.header(sectionId, index, tr(.channelBlacklistRestricted))) - index += 1 - for participant in participants.restricted.sorted(by: <) { - - let editable = peer.hasAdminRights(.canBanUsers) - - var deleting:ShortPeerDeleting? = nil - if state.editing { - deleting = ShortPeerDeleting(editable: editable) - } - - entries.append(.peerItem(sectionId, index, participant, deleting, state.removingPeerId != participant.peer.id)) - index += 1 - } - } - - - - if !participants.banned.isEmpty { - - if !participants.restricted.isEmpty { - entries.append(.section(sectionId)) - sectionId += 1 - } - - entries.append(.header(sectionId, index, tr(.channelBlacklistBlocked))) - index += 1 - for participant in participants.banned.sorted(by: <) { - - var editable = true - if case .creator = participant.participant { - editable = false - } - - var deleting:ShortPeerDeleting? = nil - if state.editing { - deleting = ShortPeerDeleting(editable: editable) - } - - entries.append(.peerItem(sectionId, index, participant, deleting, state.removingPeerId != participant.peer.id)) - index += 1 - } - } - } - } - if entries.isEmpty { - entries.append(.empty(participants == nil)) - } - - return entries -} - -fileprivate func prepareTransition(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], initialSize:NSSize, arguments:ChannelBlacklistControllerArguments) -> TableUpdateTransition { - - let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in - return entry.entry.item(arguments, initialSize: initialSize) - } - - return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true) -} - - -class ChannelBlacklistViewController: EditableViewController { - - private let peerId:PeerId - - private let statePromise = ValuePromise(ChannelBlacklistControllerState(), ignoreRepeated: true) - private let stateValue = Atomic(value: ChannelBlacklistControllerState()) - private let removePeerDisposable:MetaDisposable = MetaDisposable() - private let updatePeerDisposable = MetaDisposable() - private let disposable:MetaDisposable = MetaDisposable() - - init(account:Account, peerId:PeerId) { - self.peerId = peerId - super.init(account) - } - - override func viewDidLoad() { - super.viewDidLoad() - let account = self.account - let peerId = self.peerId - - let updateState: ((ChannelBlacklistControllerState) -> ChannelBlacklistControllerState) -> Void = { [weak self] f in - if let strongSelf = self { - strongSelf.statePromise.set(strongSelf.stateValue.modify { f($0) }) - } - } - - let peersPromise = Promise(nil) - let viewValue:Atomic = Atomic(value: nil) - - let restrict:(RenderedChannelParticipant, Bool) -> Void = { [weak self] participant, unban in - let strongSelf = self - showModal(with: RestrictedModalViewController(account: account, peerId: peerId, participant: participant, unban: unban, updated: { [weak strongSelf] updatedRights in - let additional = viewValue.modify({$0})?.peers ?? [:] - switch participant.participant { - case let .member(memberId, _, _, _): - //if banInfo != updatedRights { - - let applyPeer: Signal = peersPromise.get() - |> filter { $0 != nil } - |> map {$0!} - |> take(1) - |> deliverOnMainQueue - |> mapToSignal { peers -> Signal in - peersPromise.set(.single(peers.withRemovedParticipant(participant.withUpdatedBannedRights(ChannelParticipantBannedInfo(rights: updatedRights, restrictedBy: account.peerId, isMember: true)).withUpdatedAdditionalPeers(additional)))) - return .complete() - } - - let peerUpdate = account.postbox.modify { modifier -> Void in - updatePeers(modifier: modifier, peers: [participant.peer], update: { (_, updated) -> Peer? in - return updated - }) - } - - strongSelf?.updatePeerDisposable.set(showModalProgress(signal: peerUpdate |> then(updateChannelMemberBannedRights(account: account, peerId: peerId, memberId: memberId, rights: updatedRights)) |> then(applyPeer), for: mainWindow).start()) - // } - default: - break - } - - - }), for: mainWindow) - } - - let arguments = ChannelBlacklistControllerArguments(account: account, removePeer: { [weak self] memberId in - - updateState { - return $0.withUpdatedRemovingPeerId(memberId) - } - - let applyPeers: Signal = peersPromise.get() - |> filter { $0 != nil } - |> map {$0!} - |> take(1) - |> deliverOnMainQueue - |> mapToSignal { peers -> Signal in - peersPromise.set(.single(peers.withRemovedPeerId(memberId))) - return .complete() - } - - self?.removePeerDisposable.set((updateChannelMemberBannedRights(account: account, peerId: peerId, memberId: memberId, rights: TelegramChannelBannedRights(flags: [], untilDate: 0)) |> then(applyPeers) |> deliverOnMainQueue).start(error: { _ in - updateState { - return $0.withUpdatedRemovingPeerId(nil) - } - }, completed: { - updateState { - return $0.withUpdatedRemovingPeerId(nil) - } - - })) - }, restrict: restrict, addMember: { - let behavior = SelectChannelMembersBehavior(peerId: peerId, limit: 1) - - _ = (selectModalPeers(account: account, title: tr(.channelBlacklistSelectNewUserTitle), limit: 1, behavior: behavior, confirmation: { peerIds in - if let peerId = peerIds.first { - var adminError:Bool = false - if let participant = behavior.participants[peerId] { - if case let .member(_, _, adminInfo, _) = participant.participant { - if let adminInfo = adminInfo { - if !adminInfo.canBeEditedByAccountPeer && adminInfo.promotedBy != account.peerId { - adminError = true - } - } - } else { - adminError = true - } - } - if adminError { - alert(for: mainWindow, info: tr(.channelBlacklistDemoteAdminError)) - return .single(false) - } - } - return .single(true) - }) |> map {$0.first} |> filter {$0 != nil} |> map {$0!}).start(next: { memberId in - var participant:RenderedChannelParticipant? - if let p = behavior.participants[memberId] { - participant = p - } else if let temporary = behavior.result[memberId] { - participant = RenderedChannelParticipant(participant: ChannelParticipant.member(id: memberId, invitedAt: 0, adminInfo: nil, banInfo: nil), peer: temporary.peer, peers: [memberId: temporary.peer], presences: temporary.presence != nil ? [memberId: temporary.presence!] : [:]) - } - if let participant = participant { - if case .member(_, _, _, let banInfo) = participant.participant { - let info = ChannelParticipantBannedInfo(rights: TelegramChannelBannedRights(flags: [.banSendMessages, .banReadMessages, .banSendMedia, .banSendStickers, .banEmbedLinks], untilDate: .max), restrictedBy: account.peerId, isMember: true) - restrict(participant.withUpdatedBannedRights(info), !(banInfo == nil || !banInfo!.rights.flags.isEmpty)) - } - } - }) - }) - - let peerView = account.viewTracker.peerView(peerId) - - - - let peersSignal: Signal = .single(nil) |> then(channelBlacklistParticipants(account: account, peerId: peerId) |> map { Optional($0) }) - - peersPromise.set(peersSignal) - - let initialSize = atomicSize - let previousEntries:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) - - - let signal = combineLatest(statePromise.get(), peerView, peersPromise.get(), appearanceSignal) - |> deliverOnMainQueue - |> map { state, view, blacklist, appearance -> (TableUpdateTransition, PeerView) in - _ = viewValue.swap(view) - let entries = channelBlacklistControllerEntries(view: view, state: state, participants: blacklist).map {AppearanceWrapperEntry(entry: $0, appearance: appearance)} - return (prepareTransition(left: previousEntries.swap(entries), right: entries, initialSize: initialSize.modify{$0}, arguments: arguments), view) - } - - disposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] transition, peerView in - if let strongSelf = self { - strongSelf.genericView.merge(with: transition) - strongSelf.readyOnce() - strongSelf.rightBarView.isHidden = strongSelf.genericView.item(at: 0) is SearchEmptyRowItem - if let peer = peerViewMainPeer(peerView) as? TelegramChannel { - strongSelf.rightBarView.isHidden = strongSelf.rightBarView.isHidden || !peer.hasAdminRights(.canBanUsers) - } - } - })) - } - - deinit { - disposable.dispose() - removePeerDisposable.dispose() - updatePeerDisposable.dispose() - } - - override func update(with state: ViewControllerState) { - super.update(with: state) - self.statePromise.set(stateValue.modify({$0.withUpdatedEditing(state == .Edit)})) - } - -} - - diff --git a/Telegram-Mac/ChannelBlocklistViewController.swift b/Telegram-Mac/ChannelBlocklistViewController.swift new file mode 100644 index 0000000000..9d85125df6 --- /dev/null +++ b/Telegram-Mac/ChannelBlocklistViewController.swift @@ -0,0 +1,514 @@ +// +// GroupBlackListViewController.swift +// Telegram +// +// Created by keepcoder on 22/02/2017. +// Copyright © 2017 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import Postbox +import TelegramCore + +import SwiftSignalKit + + + + + +private final class ChannelBlacklistControllerArguments { + let context: AccountContext + + let removePeer: (PeerId) -> Void + let openInfo:(PeerId) -> Void + let addMember:()->Void + let returnToGroup:(PeerId) -> Void + init(context: AccountContext, removePeer: @escaping (PeerId) -> Void, openInfo:@escaping(PeerId) -> Void, addMember:@escaping()->Void, returnToGroup: @escaping(PeerId) -> Void) { + self.context = context + self.removePeer = removePeer + self.openInfo = openInfo + self.addMember = addMember + self.returnToGroup = returnToGroup + } +} + +private enum ChannelBlacklistEntryStableId: Hashable { + case peer(PeerId) + case empty + case addMember + case section(Int32) + case header(Int32) + var hashValue: Int { + switch self { + case let .peer(peerId): + return peerId.hashValue + case .empty: + return 0 + case .section: + return 1 + case .header: + return 2 + case .addMember: + return 3 + } + } + +} + +private enum ChannelBlacklistEntry: Identifiable, Comparable { + case peerItem(Int32, Int32, RenderedChannelParticipant, ShortPeerDeleting?, Bool, Bool, GeneralViewType) + case empty(Bool) + case header(Int32, Int32, String, GeneralViewType) + case section(Int32) + case addMember(Int32, Int32, GeneralViewType) + var stableId: ChannelBlacklistEntryStableId { + switch self { + case let .peerItem(_, _, participant, _, _, _, _): + return .peer(participant.peer.id) + case .empty: + return .empty + case let .section(section): + return .section(section) + case let .header(_, index, _, _): + return .header(index) + case .addMember: + return .addMember + } + } + + + + var index:Int32 { + switch self { + case let .section(section): + return (section * 1000) - section + case let .header(section, index, _, _): + return (section * 1000) + index + case let .addMember(section, index, _): + return (section * 1000) + index + case .empty: + return 0 + case let .peerItem(section, index, _, _, _, _, _): + return (section * 1000) + index + + } + } + + static func <(lhs: ChannelBlacklistEntry, rhs: ChannelBlacklistEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(_ arguments: ChannelBlacklistControllerArguments, initialSize:NSSize) -> TableRowItem { + switch self { + case let .peerItem(_, _, participant, editing, enabled, isChannel, viewType): + + let interactionType:ShortPeerItemInteractionType + if let editing = editing { + + interactionType = .deletable(onRemove: { peerId in + arguments.removePeer(peerId) + }, deletable: editing.editable) + } else { + interactionType = .plain + } + + var string:String = L10n.peerStatusRecently + + if case let .member(_, _, _, banInfo, _) = participant.participant { + if let banInfo = banInfo, let peer = participant.peers[banInfo.restrictedBy] { + if banInfo.rights.flags.contains(.banReadMessages) { + string = L10n.channelBlacklistBlockedBy(peer.displayTitle) + } else { + string = L10n.channelBlacklistRestrictedBy(peer.displayTitle) + } + } else { + if let peer = participant.peer as? TelegramUser, let botInfo = peer.botInfo { + string = botInfo.flags.contains(.hasAccessToChatHistory) ? L10n.peerInfoBotStatusHasAccess : L10n.peerInfoBotStatusHasNoAccess + } else if let presence = participant.presences[participant.peer.id] as? TelegramUserPresence { + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + (string,_, _) = stringAndActivityForUserPresence(presence, timeDifference: arguments.context.timeDifference, relativeTo: Int32(timestamp)) + } + } + } + + return ShortPeerRowItem(initialSize, peer: participant.peer, account: arguments.context.account, stableId: stableId, enabled: enabled, height:50, photoSize: NSMakeSize(36, 36), status: string, drawLastSeparator: true, inset: NSEdgeInsets(left: 30, right: 30), interactionType: interactionType, generalType: .none, viewType: viewType, action: { + if case .plain = interactionType { + arguments.openInfo(participant.peer.id) + } + }, contextMenuItems: { + var items:[ContextMenuItem] = [] + items.append(ContextMenuItem(L10n.channelBlacklistContextRemove, handler: { + arguments.removePeer(participant.peer.id) + })) + if !isChannel { + items.append(ContextMenuItem(L10n.channelBlacklistContextAddToGroup, handler: { + arguments.returnToGroup(participant.peer.id) + })) + } + + return .single(items) + }) + case let .empty(progress): + return SearchEmptyRowItem(initialSize, stableId: stableId, isLoading: progress, text: L10n.channelBlacklistEmptyDescrpition) + case let .header(_, _, text, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, viewType: viewType) + case let .addMember(_, _, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.channelBlacklistRemoveUser, nameStyle: blueActionButton, viewType: viewType, action: { + arguments.addMember() + }) + case .section: + return GeneralRowItem(initialSize, height: 30, stableId: stableId, viewType: .separator) + } + } +} + +private struct ChannelBlacklistControllerState: Equatable { + let editing: Bool + let removingPeerId: PeerId? + + init() { + self.editing = false + self.removingPeerId = nil + } + + init(editing: Bool, removingPeerId: PeerId?) { + self.editing = editing + self.removingPeerId = removingPeerId + } + + + func withUpdatedEditing(_ editing: Bool) -> ChannelBlacklistControllerState { + return ChannelBlacklistControllerState(editing: editing, removingPeerId: self.removingPeerId) + } + + func withUpdatedPeerIdWithRevealedOptions(_ peerIdWithRevealedOptions: PeerId?) -> ChannelBlacklistControllerState { + return ChannelBlacklistControllerState(editing: self.editing, removingPeerId: self.removingPeerId) + } + + func withUpdatedRemovingPeerId(_ removingPeerId: PeerId?) -> ChannelBlacklistControllerState { + return ChannelBlacklistControllerState(editing: self.editing, removingPeerId: removingPeerId) + } +} + +private func channelBlacklistControllerEntries(view: PeerView, state: ChannelBlacklistControllerState, participants: [RenderedChannelParticipant]?, inSearch: Bool) -> [ChannelBlacklistEntry] { + + var entries: [ChannelBlacklistEntry] = [] + + var index:Int32 = 10 + var sectionId:Int32 = 1 + + + if let peer = peerViewMainPeer(view) as? TelegramChannel { + + entries.append(.section(sectionId)) + sectionId += 1 + + if peer.hasPermission(.banMembers), !inSearch { + entries.append(.addMember(sectionId, 0, .singleItem)) + entries.append(.header(sectionId, 1, peer.isGroup || peer.isSupergroup ? L10n.channelBlacklistDescGroup : L10n.channelBlacklistDescChannel, .textBottomItem)) + } + if let participants = participants { + if !participants.isEmpty, peer.hasPermission(.banMembers) || inSearch { + entries.append(.section(sectionId)) + sectionId += 1 + } + + if !participants.isEmpty { + + entries.append(.header(sectionId, index, L10n.channelBlacklistBlocked, .textTopItem)) + index += 1 + for (i, participant) in participants.sorted(by: <).enumerated() { + var editable = true + if case .creator = participant.participant { + editable = false + } + + var deleting:ShortPeerDeleting? = nil + if state.editing { + deleting = ShortPeerDeleting(editable: editable) + } + + entries.append(.peerItem(sectionId, index, participant, deleting, state.removingPeerId != participant.peer.id, peer.isChannel, bestGeneralViewType(participants, for: i))) + index += 1 + } + } + } + entries.append(.section(sectionId)) + sectionId += 1 + } + if entries.isEmpty { + entries.append(.empty(participants == nil)) + } + + return entries +} + +fileprivate func prepareTransition(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], initialSize:NSSize, arguments:ChannelBlacklistControllerArguments, inSearch: Bool, searchData: TableSearchVisibleData) -> TableUpdateTransition { + + let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in + return entry.entry.item(arguments, initialSize: initialSize) + } + let searchState: TableSearchViewState? + if inSearch { + searchState = .visible(searchData) + } else { + searchState = .none + } + + return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true, searchState: searchState) +} + + +class ChannelBlacklistViewController: EditableViewController { + + private let peerId:PeerId + + private let statePromise = ValuePromise(ChannelBlacklistControllerState(), ignoreRepeated: true) + private let stateValue = Atomic(value: ChannelBlacklistControllerState()) + private let removePeerDisposable:MetaDisposable = MetaDisposable() + private let updatePeerDisposable = MetaDisposable() + private let disposable:MetaDisposable = MetaDisposable() + + private let _inSearch: ValuePromise = ValuePromise(false) + private var inSearch: Bool = false { + didSet { + _inSearch.set(self.inSearch) + } + } + init(_ context:AccountContext, peerId:PeerId) { + self.peerId = peerId + super.init(context) + } + + override func viewDidLoad() { + super.viewDidLoad() + let context = self.context + let peerId = self.peerId + + + + genericView.getBackgroundColor = { + theme.colors.listBackground + } + + let actionsDisposable = DisposableSet() + + let updateState: ((ChannelBlacklistControllerState) -> ChannelBlacklistControllerState) -> Void = { [weak self] f in + if let strongSelf = self { + strongSelf.statePromise.set(strongSelf.stateValue.modify { f($0) }) + } + } + + let blacklistPromise = Promise<[RenderedChannelParticipant]?>(nil) + let listDisposable = MetaDisposable() + + let viewValue:Atomic = Atomic(value: nil) + + let restrict:(PeerId, Bool) -> Void = { [weak self] memberId, unban in + let signal = context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(peerId: peerId, memberId: memberId, bannedRights: unban ? nil : TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)) |> ignoreValues + + self?.updatePeerDisposable.set(showModalProgress(signal: signal, for: context.window).start(error: { _ in + updateState { + return $0.withUpdatedRemovingPeerId(nil) + } + }, completed: { + updateState { + return $0.withUpdatedRemovingPeerId(nil) + } + })) + } + + let arguments = ChannelBlacklistControllerArguments(context: context, removePeer: { memberId in + + updateState { + return $0.withUpdatedRemovingPeerId(memberId) + } + + restrict(memberId, true) + }, openInfo: { [weak self] peerId in + self?.navigationController?.push(PeerInfoController(context: context, peerId: peerId)) + }, addMember: { + let behavior = SelectChannelMembersBehavior(peerId: peerId, peerChannelMemberContextsManager: context.peerChannelMemberCategoriesContextsManager, limit: 1) + + _ = (selectModalPeers(window: context.window, context: context, title: L10n.channelBlacklistSelectNewUserTitle, limit: 1, behavior: behavior, confirmation: { peerIds in + if let peerId = peerIds.first { + var adminError:Bool = false + if let participant = behavior.participants[peerId] { + if case let .member(_, _, adminInfo, _, _) = participant.participant { + if let adminInfo = adminInfo { + if !adminInfo.canBeEditedByAccountPeer && adminInfo.promotedBy != context.account.peerId { + adminError = true + } + } + } else { + adminError = true + } + } + if adminError { + alert(for: mainWindow, info: L10n.channelBlacklistDemoteAdminError) + return .single(false) + } + } + return .single(true) + }) |> map {$0.first} |> filter {$0 != nil} |> map {$0!}).start(next: { memberId in + restrict(memberId, false) + }) + }, returnToGroup: { [weak self] memberId in + updateState { + return $0.withUpdatedRemovingPeerId(memberId) + } + + let signal = context.peerChannelMemberCategoriesContextsManager.addMember(peerId: peerId, memberId: memberId) |> ignoreValues + + self?.updatePeerDisposable.set(showModalProgress(signal: signal, for: mainWindow).start(error: { _ in + updateState { + return $0.withUpdatedRemovingPeerId(nil) + } + }, completed: { + updateState { + return $0.withUpdatedRemovingPeerId(nil) + } + })) + }) + + let peerView = context.account.viewTracker.peerView(peerId) + + + var (disposable, loadMoreControl) = context.peerChannelMemberCategoriesContextsManager.banned(peerId: peerId, updated: { listState in + if case .loading(true) = listState.loadingState, listState.list.isEmpty { + blacklistPromise.set(.single(nil)) + } else { + blacklistPromise.set(.single(listState.list)) + } + }) + + listDisposable.set(disposable) + + actionsDisposable.add(listDisposable) + + let initialSize = atomicSize + let previousEntries:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) + + + let searchData = TableSearchVisibleData(cancelImage: theme.icons.chatSearchCancel, cancel: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.inSearch = !strongSelf.inSearch + + (disposable, loadMoreControl) = context.peerChannelMemberCategoriesContextsManager.banned(peerId: peerId, updated: { listState in + if case .loading(true) = listState.loadingState, listState.list.isEmpty { + blacklistPromise.set(.single(nil)) + } else { + blacklistPromise.set(.single(listState.list)) + } + }) + listDisposable.set(disposable) + + }, updateState: { state in + if !state.request.isEmpty { + (disposable, loadMoreControl) = context.peerChannelMemberCategoriesContextsManager.restrictedAndBanned(peerId: peerId, searchQuery: state.request, updated: { listState in + blacklistPromise.set(.single(listState.list)) + }) + listDisposable.set(disposable) + } else { + (disposable, loadMoreControl) = context.peerChannelMemberCategoriesContextsManager.banned(peerId: peerId, updated: { listState in + if case .loading(true) = listState.loadingState, listState.list.isEmpty { + blacklistPromise.set(.single(nil)) + } else { + blacklistPromise.set(.single(listState.list)) + } + }) + listDisposable.set(disposable) + } + }) + + + let signal = combineLatest(statePromise.get(), peerView, blacklistPromise.get(), appearanceSignal, _inSearch.get()) + |> deliverOnMainQueue + |> map { state, view, blacklist, appearance, inSearch -> (TableUpdateTransition, PeerView) in + _ = viewValue.swap(view) + let entries = channelBlacklistControllerEntries(view: view, state: state, participants: blacklist, inSearch: inSearch).map {AppearanceWrapperEntry(entry: $0, appearance: appearance)} + return (prepareTransition(left: previousEntries.swap(entries), right: entries, initialSize: initialSize.modify{$0}, arguments: arguments, inSearch: inSearch, searchData: searchData), view) + } |> afterDisposed { + actionsDisposable.dispose() + } |> deliverOnMainQueue + + self.disposable.set(signal.start(next: { [weak self] transition, peerView in + guard let `self` = self else { + return + } + self.genericView.merge(with: transition) + self.readyOnce() + self.rightBarView.isHidden = self.genericView.item(at: 0) is SearchEmptyRowItem + if let peer = peerViewMainPeer(peerView) as? TelegramChannel { + self.rightBarView.isHidden = self.rightBarView.isHidden || !peer.hasPermission(.banMembers) + } + + var hasItems: Bool = false + self.genericView.enumerateItems(with: { item -> Bool in + if item is ShortPeerRowItem { + hasItems = true + } + return !hasItems + }) + (self.centerBarView as? SearchTitleBarView)?.updateSearchVisibility(hasItems || self.inSearch) + })) + + genericView.setScrollHandler { position in + if let loadMoreControl = loadMoreControl { + switch position.direction { + case .bottom: + context.peerChannelMemberCategoriesContextsManager.loadMore(peerId: peerId, control: loadMoreControl) + default: + break + } + } + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + window?.set(handler: { [weak self] _ -> KeyHandlerResult in + guard let `self` = self else { + return .rejected + } + self.inSearch = !self.inSearch + return .invoked + }, with: self, for: .F, modifierFlags: [.command]) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + window?.removeAllHandlers(for: self) + } + + override var defaultBarTitle: String { + return L10n.peerInfoRemovedUsers + } + + override func getCenterBarViewOnce() -> TitledBarView { + return SearchTitleBarView(controller: self, title:.initialize(string: defaultBarTitle, color: theme.colors.text, font: .medium(.title)), handler: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.inSearch = !strongSelf.inSearch + }) + } + + deinit { + disposable.dispose() + removePeerDisposable.dispose() + updatePeerDisposable.dispose() + } + + override func update(with state: ViewControllerState) { + super.update(with: state) + self.statePromise.set(stateValue.modify({$0.withUpdatedEditing(state == .Edit)})) + } + +} + + diff --git a/Telegram-Mac/ChannelCommentsControls.swift b/Telegram-Mac/ChannelCommentsControls.swift new file mode 100644 index 0000000000..a55172ba31 --- /dev/null +++ b/Telegram-Mac/ChannelCommentsControls.swift @@ -0,0 +1,704 @@ +// +// ChatCommentsBubbleControl.swift +// Telegram +// +// Created by Mikhail Filimonov on 02/09/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +import Postbox +import SwiftSignalKit + +private let duration: TimeInterval = 0.4 +private let timingFunction: CAMediaTimingFunctionName = .spring + + + + + +protocol ChannelCommentRenderer { + func update(data: ChannelCommentsRenderData, size: NSSize, animated: Bool) + var firstTextPosition: NSPoint { get } + var lastTextPosition: NSPoint { get } + var progressIndicatorPosition: NSPoint { get } + var progressIndicatorSize: NSSize { get } + var progressIndicatorColor: NSColor { get } +} + + +class CommentsBasicControl : Control, ChannelCommentRenderer { + + fileprivate var textViews: [ChannelCommentsRenderData.Text : (TextView, ChannelCommentsRenderData.Text)] = [:] + fileprivate var renderData: ChannelCommentsRenderData? + fileprivate var size: NSSize = .zero + fileprivate var progressView: ProgressIndicator? + func update(data: ChannelCommentsRenderData, size: NSSize, animated: Bool) { + let previousLastTextPosition = lastTextPosition + self.size = size + self.renderData = data + + self.removeAllHandlers() + + self.set(handler: { [weak data] _ in + data?.handler() + }, for: .SingleClick) + + + enum NumericAnimation { + case forward + case backward + } + + var addition: [Int : NumericAnimation] = [:] + var previousTextPos:[Int: NSPoint] = [:] + for (key, textView) in textViews { + let title = data._title.first(where: { $0.hashValue == key.hashValue }) + if textView.1 != title { + let updated = title ?? key + if let title = title { + addition[key.hashValue] = title < key ? .backward : .forward + } + + textViews[key] = nil + let field = textView.0 + previousTextPos[key.hashValue] = field.frame.origin + if animated { + switch updated.animation { + case .crossFade: + field.layer?.animateAlpha(from: 1, to: 0, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { [weak field] _ in + field?.removeFromSuperview() + }) + case .numeric: + field.layer?.animateAlpha(from: 1, to: 0, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { [weak field] _ in + field?.removeFromSuperview() + }) + + let direction = addition[key.hashValue] + switch direction { + case .forward?: + field.layer?.animatePosition(from: field.frame.origin, to: NSMakePoint(field.frame.minX, field.frame.maxY), timingFunction: timingFunction, removeOnCompletion: false) + case .backward?: + field.layer?.animatePosition(from: field.frame.origin, to: NSMakePoint(field.frame.minX, field.frame.minY - field.frame.height), timingFunction: timingFunction, removeOnCompletion: false) + case .none: + break + } + } + } else { + field.removeFromSuperview() + } + } + } + var pos = firstTextPosition + for layout in data.titleLayout { + if let view = textViews[layout.1] { + if animated { + view.0.layer?.animatePosition(from: view.0.frame.origin - pos, to: .zero, timingFunction: timingFunction, removeOnCompletion: true, additive: true) + } + view.0.setFrameOrigin(pos) + } else { + let current = TextView() + current.userInteractionEnabled = false + current.isSelectable = false + current.disableBackgroundDrawing = true + self.textViews[layout.1] = (current, layout.1) + current.update(layout.0, origin: pos) + addSubview(current) + if animated { + switch layout.1.animation { + case .crossFade: + current.layer?.animateAlpha(from: 0, to: 1, duration: duration) + case .numeric: + let prevPos = previousTextPos[layout.1.hashValue] ?? pos + let direction = addition[layout.1.hashValue] + switch direction { + case .forward?: + current.layer?.animatePosition(from: NSMakePoint(pos.x, pos.y - layout.0.layoutSize.height), to: pos, timingFunction: timingFunction) + case .backward?: + current.layer?.animatePosition(from: NSMakePoint(pos.x, pos.y + layout.0.layoutSize.height), to: pos, timingFunction: timingFunction) + case .none: + break + } + + current.layer?.animateAlpha(from: 0, to: 1, duration: duration) + } + } + } + pos.x += max(layout.0.layoutSize.width, 4) + } + + if data.isLoading { + if progressView == nil { + let indicator = ProgressIndicator(frame: NSMakeRect(0, 0, progressIndicatorSize.width, progressIndicatorSize.height)) + self.progressView = indicator + self.progressView?.progressColor = progressIndicatorColor + addSubview(indicator) + if animated { + indicator.layer?.animateAlpha(from: 0, to: 1, duration: duration, timingFunction: timingFunction) + } + } + self.progressView?.progressColor = progressIndicatorColor + } else { + if animated { + if let progressView = self.progressView { + self.progressView = nil + progressView.layer?.animateAlpha(from: 1, to: 0, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { [weak progressView] _ in + progressView?.removeFromSuperview() + }) + } + } else { + self.progressView?.removeFromSuperview() + self.progressView = nil + } + } + self.progressView?.setFrameOrigin(progressIndicatorPosition) + } + + var firstTextPosition: NSPoint { + return .zero + } + + var progressIndicatorSize: NSSize { + return NSMakeSize(16, 16) + } + + var progressIndicatorPosition: NSPoint { + var rect = focus(progressIndicatorSize) + rect.origin.x = 0 + return rect.origin + } + + var progressIndicatorColor: NSColor { + return theme.colors.accentIcon + } + + override func layout() { + super.layout() + progressView?.setFrameOrigin(progressIndicatorPosition) + } + + var lastTextPosition: NSPoint { + guard let render = renderData, !render.titleLayout.isEmpty else { + return .zero + } + + return firstTextPosition + NSMakePoint(render.titleLayout.reduce(0, { + $0 + max($1.0.layoutSize.width, 4) + }), 0) + } + +} + + +final class ChannelCommentsRenderData { + + struct Text : Hashable, Comparable { + enum Animation : Equatable { + case crossFade + case numeric + } + let text: NSAttributedString + let animation: Animation + let index: Int + + init(text: NSAttributedString, animation: Animation, index: Int) { + self.text = text + self.animation = animation + self.index = index + } + + static func <(lhs: Text, rhs: Text) -> Bool { + let lhsInt: Int? = Int(lhs.text.string) + let rhsInt: Int? = Int(rhs.text.string) + + if let lhsInt = lhsInt, let rhsInt = rhsInt { + return lhsInt < rhsInt + } + return false + } + + + func hash(into hasher: inout Hasher) { + hasher.combine(index) + } + } + + struct Avatar : Comparable, Identifiable { + static func < (lhs: Avatar, rhs: Avatar) -> Bool { + return lhs.index < rhs.index + } + + var stableId: PeerId { + return peer.id + } + + static func == (lhs: ChannelCommentsRenderData.Avatar, rhs: ChannelCommentsRenderData.Avatar) -> Bool { + if lhs.index != rhs.index { + return false + } + if !lhs.peer.isEqual(rhs.peer) { + return false + } + return true + } + + let peer: Peer + let index: Int + } + + var titleSize: NSSize { + return titleLayout.reduce(NSZeroSize, { current, value in + var current = current + current.width += max(value.0.layoutSize.width, 4) + current.height = max(value.0.layoutSize.height, current.height) + return current + }) + } + + let _title: [Text] + let peers:[Avatar] + let drawBorder: Bool + let context: AccountContext + let message: Message? + let hasUnread: Bool + let isLoading: Bool + fileprivate var titleLayout:[(TextViewLayout, Text)] = [] + fileprivate let handler: ()->Void + + init(context: AccountContext, message: Message?, hasUnread: Bool, title: [Text], peers: [Peer], drawBorder: Bool, isLoading: Bool, handler: @escaping()->Void = {}) { + self.context = context + self.message = message + self._title = title + self.isLoading = isLoading + var index: Int = 0 + self.peers = peers.map { peer in + let avatar = Avatar(peer: peer, index: index) + index += 1 + return avatar + } + self.drawBorder = drawBorder + self.hasUnread = hasUnread + self.handler = handler + } + + func makeSize() { + self.titleLayout = _title.map { + return (TextViewLayout($0.text, maximumNumberOfLines: 1, truncationType: .end), $0) + } + var mw: CGFloat = 200 + for layout in self.titleLayout { + layout.0.measure(width: mw) + mw -= max(layout.0.layoutSize.width, 4) - 2 + } + } + + func size(_ bubbled: Bool, _ isOverlay: Bool = false) -> NSSize { + var width: CGFloat = 0 + var height: CGFloat = 0 + if isOverlay { + let iconSize = theme.chat_comments_overlay.backingSize + if titleSize.width > 0 { + width += titleSize.width + width += 10 + width = max(width, 31) + height = max(iconSize.height + titleSize.height + 16, width) + } else { + width = 31 + height = 31 + } + } else if bubbled, titleSize.width > 0 { + width += titleSize.width + width += (6 * 4) + 13 + if peers.isEmpty { + width += theme.icons.channel_comments_bubble.backingSize.width + 2 + } else { + if peers.count == 1 { + width += 24 + } else { + width += 22 + (22 * CGFloat(peers.count - 1)) + } + } + width += theme.icons.channel_comments_bubble_next.backingSize.width + height = ChatRowItem.channelCommentsBubbleHeight + + if hasUnread { + width += 10 + } + } else if titleSize.width > 0 { + width += titleSize.width + width += 3 + width += theme.icons.channel_comments_list.backingSize.width + height = ChatRowItem.channelCommentsHeight + } + return NSMakeSize(width, height) + } +} + +class ChannelCommentsBubbleControl: CommentsBasicControl { + private var peers:[ChannelCommentsRenderData.Avatar] = [] + private var avatars:[AvatarContentView] = [] + private let avatarsContainer = View(frame: NSMakeRect(0, 0, 22 * 3, 22)) + private let arrowView = ImageView() + private var dotView: View? = nil + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(avatarsContainer) + addSubview(arrowView) + arrowView.isEventLess = true + avatarsContainer.isEventLess = true + + arrowView.image = theme.icons.channel_comments_bubble_next + arrowView.sizeToFit() + + } + + + override var firstTextPosition: NSPoint { + guard let render = renderData, !render.titleLayout.isEmpty else { + return .zero + } + var rect: CGRect = .zero + + if render.peers.isEmpty { + var f = focus(theme.icons.channel_comments_bubble.backingSize) + f.origin.x = 15 + 6 + rect = f + } else { + if render.peers.count == 1 { + rect = focus(NSMakeSize(24 * CGFloat(render.peers.count), 22)) + } else { + rect = focus(NSMakeSize(22 + (22 * CGFloat(render.peers.count - 1)), 22)) + } + rect.origin.x = 13 + 6 + } + + var f = focus(render.titleSize) + f.origin.x = rect.maxX + 6 + f.origin.y -= 1 + rect = f + + return rect.origin + } + + override var progressIndicatorPosition: NSPoint { + var rect = focus(progressIndicatorSize) + rect.origin.x = size.width - 6 - arrowView.frame.width + return rect.origin + } + + override var progressIndicatorColor: NSColor { + return theme.colors.accentIcon + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + super.draw(layer, in: ctx) + + + if let render = renderData { + if render.drawBorder { + ctx.setFillColor(theme.colors.accentIconBubble_incoming.withAlphaComponent(0.15).cgColor) + ctx.fill(NSMakeRect(0, 0, frame.width, .borderSize)) + } + if render.peers.isEmpty { + var f = focus(theme.icons.channel_comments_bubble.backingSize) + f.origin.x = 13 + 6 + ctx.draw(theme.icons.channel_comments_bubble, in: f) + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func update(data: ChannelCommentsRenderData, size: NSSize, animated: Bool) { + let previousLastTextPosition = lastTextPosition + + super.update(data: data, size: size, animated: animated) + + let (removed, inserted, updated) = mergeListsStableWithUpdates(leftList: self.peers, rightList: data.peers) + + for removed in removed.reversed() { + let control = avatars.remove(at: removed) + let peer = self.peers[removed] + let haveNext = data.peers.contains(where: { $0.stableId == peer.stableId }) + control.updateLayout(size: NSMakeSize(22, 22), isClipped: false, animated: animated) + if animated && !haveNext { + control.layer?.animateAlpha(from: 1, to: 0, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { [weak control] _ in + control?.removeFromSuperview() + }) + control.layer?.animateScaleSpring(from: 1.0, to: 0.2, duration: duration) + } else { + control.removeFromSuperview() + } + } + for inserted in inserted { + let control = AvatarContentView(context: data.context, peer: inserted.1.peer, message: data.message, synchronousLoad: false, size: NSMakeSize(22, 22)) + control.updateLayout(size: NSMakeSize(22, 22), isClipped: inserted.0 != 0, animated: animated) + control.userInteractionEnabled = false + control.setFrameSize(NSMakeSize(22, 22)) + control.setFrameOrigin(NSMakePoint(CGFloat(inserted.0) * 19, 0)) + avatars.insert(control, at: inserted.0) + avatarsContainer.subviews.insert(control, at: inserted.0) + if animated { + if let index = inserted.2 { + control.layer?.animatePosition(from: NSMakePoint(CGFloat(index) * 19, 0), to: control.frame.origin, timingFunction: timingFunction) + } else { + control.layer?.animateAlpha(from: 0, to: 1, duration: duration, timingFunction: timingFunction) + control.layer?.animateScaleSpring(from: 0.2, to: 1.0, duration: duration) + } + } + } + for updated in updated { + let control = avatars[updated.0] + control.updateLayout(size: NSMakeSize(22, 22), isClipped: updated.0 != 0, animated: animated) + let updatedPoint = NSMakePoint(CGFloat(updated.0) * 19, 0) + if animated { + control.layer?.animatePosition(from: control.frame.origin - updatedPoint, to: .zero, duration: duration, timingFunction: timingFunction, additive: true) + } + control.setFrameOrigin(updatedPoint) + } + var index: CGFloat = 10 + for control in avatarsContainer.subviews.compactMap({ $0 as? AvatarContentView }) { + control.layer?.zPosition = index + index -= 1 + } + + self.peers = data.peers + + enum NumericAnimation { + case forward + case backward + } + + arrowView.isHidden = data.isLoading + + if animated { + var f = focus(arrowView.frame.size) + f.origin.x = size.width - 6 - f.width + arrowView.layer?.animatePosition(from: arrowView.frame.origin - f.origin, to: .zero, timingFunction: timingFunction, additive: true) + } + + + if data.hasUnread { + let size = NSMakeSize(6, 6) + var f = focus(size) + f.origin.x = lastTextPosition.x + 6 + f.origin.y += 1 + if self.dotView == nil { + let effectivePos = previousLastTextPosition != .zero ? previousLastTextPosition : f.origin + self.dotView = View(frame: CGRect(origin: effectivePos, size: f.size)) + self.dotView?.layer?.cornerRadius = size.height / 2 + addSubview(self.dotView!) + if animated { + self.dotView?.layer?.animateAlpha(from: 0, to: 1, duration: duration, timingFunction: timingFunction) + self.dotView?.layer?.animateScaleSpring(from: 0.2, to: 1.0, duration: duration, bounce: true) + } + } + guard let dotView = self.dotView else { + return + } + if animated { + dotView.layer?.animatePosition(from: dotView.frame.origin - f.origin, to: .zero, timingFunction: timingFunction, additive: true) + } + dotView.backgroundColor = theme.colors.accentIcon + } else { + if let dotView = dotView { + self.dotView = nil + if animated { + dotView.layer?.animateAlpha(from: 1, to: 0, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { [weak dotView] _ in + dotView?.removeFromSuperview() + }) + dotView.layer?.animateScaleSpring(from: 1, to: 0.2, duration: duration) + } else { + dotView.removeFromSuperview() + } + } + } + + needsDisplay = true + needsLayout = true + } + + override func layout() { + super.layout() + self.avatarsContainer.centerY(x: 13 + 6) + self.arrowView.centerY(x: frame.width - 6 - arrowView.frame.width) + self.dotView?.centerY(x: lastTextPosition.x + 6, addition: 1) + } + +} + + + + +class ChannelCommentsControl: CommentsBasicControl { + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + } + + override var firstTextPosition: NSPoint { + guard let render = renderData else { + return .zero + } + var f = focus(render.titleSize) + f.origin.x = 0 + return f.origin + } + + + override func draw(_ layer: CALayer, in ctx: CGContext) { + super.draw(layer, in: ctx) + + if let render = renderData { + + var rect: CGRect = .zero + + var f = focus(render.titleSize) + f.origin.x = 0 + rect = f + + f = focus(theme.icons.channel_comments_list.backingSize) + f.origin.x = rect.maxX + 3 + rect = f + if !render.isLoading { + ctx.draw(theme.icons.channel_comments_list, in: rect) + } + } + + } + + override var progressIndicatorPosition: NSPoint { + if let render = renderData { + var rect: CGRect = .zero + + var f = focus(render.titleSize) + f.origin.x = 0 + rect = f + + f = focus(progressIndicatorSize) + f.origin.x = rect.maxX + 3 + rect = f + return rect.origin + } + return .zero + } + + override var progressIndicatorColor: NSColor { + return theme.colors.accent + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func update(data: ChannelCommentsRenderData, size: NSSize, animated: Bool) { + super.update(data: data, size: size, animated: animated) + + + needsDisplay = true + needsLayout = true + } + + override func layout() { + super.layout() + } + +} + + +final class ChannelCommentsSmallControl : CommentsBasicControl { + + private let imageView = ImageView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + imageView.isEventLess = true + addSubview(imageView) + } + + override var firstTextPosition: NSPoint { + guard let render = renderData, !render.titleLayout.isEmpty else { + return .zero + } + let size = theme.chat_comments_overlay.backingSize + var iconFrame = focus(size) + iconFrame.origin.y = 5 + + var titleFrame = focus(render.titleSize) + titleFrame.origin.y = iconFrame.maxY + 2 + return titleFrame.origin + } + + var imagePosition: NSPoint { + if let renderData = renderData { + let size = theme.chat_comments_overlay.backingSize + if !renderData.titleLayout.isEmpty { + var iconFrame = focus(size) + iconFrame.origin.y = 5 + return iconFrame.origin + } else { + return focus(size).origin + } + } + return .zero + } + + override var progressIndicatorPosition: NSPoint { + return imagePosition + } + + override var progressIndicatorSize: NSSize { + return theme.chat_comments_overlay.backingSize + } + + override var progressIndicatorColor: NSColor { + if theme.bubbled && theme.backgroundMode.hasWallpaper { + return theme.chatServiceItemTextColor + } else { + return theme.colors.accent + } + } + + override func update(data: ChannelCommentsRenderData, size: NSSize, animated: Bool) { + + super.update(data: data, size: size, animated: animated) + + imageView.isHidden = data.isLoading + + + if theme.bubbled && theme.backgroundMode.hasWallpaper { + imageView.image = theme.chat_comments_overlay + } else { + imageView.image = theme.icons.channel_comments_overlay + } + _ = imageView.sizeToFit() + + layer?.cornerRadius = min(size.height, size.width) / 2 + + let rect = CGRect(origin: .zero, size: size) + let iconSize = theme.chat_comments_overlay.backingSize + let iconPosition: NSPoint + if !data.titleLayout.isEmpty { + var iconFrame = rect.focus(iconSize) + iconFrame.origin.y = 5 + iconPosition = iconFrame.origin + } else { + iconPosition = rect.focus(iconSize).origin + } + + imageView.change(pos: iconPosition, animated: animated) + change(size: size, animated: animated) + needsDisplay = true + } + + override func layout() { + super.layout() + imageView.setFrameOrigin(imagePosition) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/ChannelDiscussionInputView.swift b/Telegram-Mac/ChannelDiscussionInputView.swift new file mode 100644 index 0000000000..28fb2499b6 --- /dev/null +++ b/Telegram-Mac/ChannelDiscussionInputView.swift @@ -0,0 +1,100 @@ +// +// ChannelDiscussionInputView.swift +// Telegram +// +// Created by Mikhail Filimonov on 24/05/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import Postbox +import TelegramCore + +import SwiftSignalKit + +class ChannelDiscussionInputView: View { + private let leftButton: TitleButton = TitleButton() + private let rightButton: TitleButton = TitleButton() + private var badge:BadgeNode? + private var badgeView:View = View() + private let disposable = MetaDisposable() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + leftButton.disableActions() + rightButton.disableActions() + addSubview(leftButton) + addSubview(rightButton) + addSubview(badgeView) + } + + func update(with chatInteraction: ChatInteraction, discussionGroupId: PeerId?, leftAction: String, rightAction: String) { + leftButton.style = ControlStyle(font: .normal(.title),foregroundColor: theme.colors.accent) + leftButton.set(text: leftAction, for: .Normal) + leftButton.set(background: theme.colors.grayBackground, for: .Highlight) + + rightButton.style = ControlStyle(font: .normal(.title),foregroundColor: theme.colors.accent) + rightButton.set(text: rightAction, for: .Normal) + rightButton.set(background: theme.colors.grayBackground, for: .Highlight) + + + leftButton.removeAllHandlers() + leftButton.set(handler: { [weak chatInteraction] _ in + chatInteraction?.toggleNotifications(nil) + }, for: .Click) + + rightButton.removeAllHandlers() + rightButton.set(handler: { [weak chatInteraction] _ in + chatInteraction?.openDiscussion() + }, for: .Click) + + let context = chatInteraction.context + if let discussionGroupId = discussionGroupId { + self.disposable.set((context.account.postbox.unreadMessageCountsView(items: [.peer(discussionGroupId)]) |> deliverOnMainQueue).start(next: { [weak self] unreadView in + if let strongSelf = self { + let count = unreadView.count(for: .peer(discussionGroupId)) ?? 0 + if count > 0 { + strongSelf.badge = BadgeNode(.initialize(string: Int(count).prettyNumber, color: .white, font: .bold(.small)), theme.colors.accent) + strongSelf.badge!.view = strongSelf.badgeView + strongSelf.badgeView.setFrameSize(strongSelf.badge!.size) + strongSelf.addSubview(strongSelf.badgeView) + } else { + strongSelf.badgeView.removeFromSuperview() + } + strongSelf.needsLayout = true + + } + })) + } else { + self.badgeView.removeFromSuperview() + self.disposable.set(nil) + } + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + leftButton.style = ControlStyle(font: .normal(.title),foregroundColor: theme.colors.accent) + leftButton.set(background: theme.colors.grayBackground, for: .Highlight) + rightButton.style = ControlStyle(font: .normal(.title),foregroundColor: theme.colors.accent) + rightButton.set(background: theme.colors.grayBackground, for: .Highlight) + + } + + override func layout() { + super.layout() + leftButton.frame = NSMakeRect(0, 0, frame.width / 2, frame.height) + rightButton.frame = NSMakeRect(frame.width / 2, 0, frame.width / 2, frame.height) + badgeView.centerY(x: rightButton.frame.maxX - (rightButton.frame.width - rightButton.textSize.width) / 2 + 5) + + } + + + deinit { + disposable.dispose() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/Telegram-Mac/ChannelDisscussionGroup.swift b/Telegram-Mac/ChannelDisscussionGroup.swift new file mode 100644 index 0000000000..0615e4d37d --- /dev/null +++ b/Telegram-Mac/ChannelDisscussionGroup.swift @@ -0,0 +1,524 @@ +// +// ChannelDiscussionGroup.swift +// Telegram +// +// Created by Mikhail Filimonov on 23/05/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import Postbox +import TelegramCore + +import SwiftSignalKit + + +private final class DiscussionArguments { + let context: AccountContext + let createGroup:()->Void + let setup:(Peer)->Void + let openInfo:(PeerId) -> Void + let unlinkGroup: (Peer)->Void + init(context: AccountContext, createGroup: @escaping()->Void, setup: @escaping(Peer)->Void, openInfo: @escaping(PeerId)->Void, unlinkGroup: @escaping(Peer)->Void) { + self.context = context + self.createGroup = createGroup + self.setup = setup + self.openInfo = openInfo + self.unlinkGroup = unlinkGroup + } +} + +private func generateDiscussIcon() -> CGImage { + let image: CGImage + switch theme.colors.name { + case systemPalette.name: + image = NSImage(named: "DiscussDarkPreview")!.precomposed() + case nightAccentPalette.name: + image = NSImage(named: "DiscussDarkBluePreview")!.precomposed() + default: + if theme.colors.isDark { + image = NSImage(named: "DiscussDarkBluePreview")!.precomposed() + } else { + image = NSImage(named: "DiscussDayPreview")!.precomposed() + } + } + + + return generateImage(image.backingSize, contextGenerator: { size, ctx in + ctx.clear(NSMakeRect(0, 0, size.width, size.height)) + ctx.draw(image, in: NSMakeRect(0, 0, size.width, size.height)) + + let palette = theme.colors + + let attributeString: NSAttributedString = .initialize(string: L10n.discussionControllerIconText, color: palette.accentIcon, font: .normal(12)) + + let node = TextNode.layoutText(maybeNode: nil, attributeString, palette.background, 1, .end, NSMakeSize(size.width - 10, size.height), nil, false, .center) + + ctx.translateBy(x: size.width / 2.0, y: size.height / 2.0) + ctx.scaleBy(x: 1.0, y: -1.0) + ctx.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + let xpos: CGFloat = (size.width - node.0.size.width) / 2 + node.1.draw(NSMakeRect(xpos, size.height - 26, node.0.size.width, node.0.size.height), in: ctx, backingScaleFactor: System.backingScale, backgroundColor: palette.background) + })! +} + +private enum DiscussionType { + case group + case channel +} + +private final class DiscussionState : Equatable { + let type: DiscussionType + let availablePeers:[Peer] + let associatedPeer: Peer? + let unlinkAbility: Bool + let searchState: SearchState? + init(type: DiscussionType, availablePeers: [Peer], associatedPeer: Peer?, unlinkAbility: Bool, searchState: SearchState?) { + self.type = type + self.searchState = searchState + self.availablePeers = availablePeers + self.associatedPeer = associatedPeer + self.unlinkAbility = unlinkAbility + } + + var filteredPeers: [Peer] { + return self.availablePeers.filter { peer in + if let search = self.searchState, !search.request.isEmpty { + return peer.displayTitle.lowercased().hasPrefix(search.request.lowercased()) || !peer.displayTitle.lowercased().components(separatedBy: " ").filter {$0.hasPrefix(search.request.lowercased())}.isEmpty + + } else { + return true + } + } + } + + func withUpdatedassociatedPeer(_ associatedPeer: Peer?) -> DiscussionState { + return DiscussionState(type: self.type, availablePeers: self.availablePeers, associatedPeer: associatedPeer, unlinkAbility: self.unlinkAbility, searchState: self.searchState) + } + func withUpdatedAvailablePeers(_ availablePeers: [Peer]) -> DiscussionState { + return DiscussionState(type: self.type, availablePeers: availablePeers, associatedPeer: self.associatedPeer, unlinkAbility: self.unlinkAbility, searchState: self.searchState) + } + + func withUpdatedUnlinkAbility(_ unlinkAbility: Bool) -> DiscussionState { + return DiscussionState(type: self.type, availablePeers: self.availablePeers, associatedPeer: self.associatedPeer, unlinkAbility: unlinkAbility, searchState: self.searchState) + } + + func withUpdatedSearchState(_ searchState: SearchState) -> DiscussionState { + return DiscussionState(type: self.type, availablePeers: self.availablePeers, associatedPeer: self.associatedPeer, unlinkAbility: self.unlinkAbility, searchState: searchState) + } + + static func == (lhs: DiscussionState, rhs: DiscussionState) -> Bool { + if let lhsassociatedPeer = lhs.associatedPeer, let rhsassociatedPeer = rhs.associatedPeer { + if !lhsassociatedPeer.isEqual(rhsassociatedPeer) { + return false + } + } else if (lhs.associatedPeer != nil) != (rhs.associatedPeer != nil) { + return false + } + + if lhs.searchState != rhs.searchState { + return false + } + + if lhs.availablePeers.count != rhs.availablePeers.count { + return false + } else { + for (i, lhsPeer) in lhs.availablePeers.enumerated() { + if !lhsPeer.isEqual(rhs.availablePeers[i]) { + return false + } + } + } + return true + } + +} +private let _id_channel_header = InputDataIdentifier("_id_channel_header") +private let _id_group_header = InputDataIdentifier("_id_group_header") + +private let _id_create_group = InputDataIdentifier("_id_create_group") +private let _id_unlink_group = InputDataIdentifier("_id_unlink_group") +private func _id_peer(_ peerId: PeerId) -> InputDataIdentifier { + return InputDataIdentifier("_id_peer_\(peerId.toInt64())") +} +private func _id_peer_info(_ peerId: PeerId) -> InputDataIdentifier { + return InputDataIdentifier("_id_peer_\(peerId.toInt64())_info") +} + + + +private func channelDiscussionEntries(state: DiscussionState, arguments: DiscussionArguments) -> [InputDataEntry] { + var entries: [InputDataEntry] = [] + + var sectionId: Int32 = 0 + var index: Int32 = 0 + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + let applyList:()->Void = { + + let peers = state.filteredPeers + + for (i, peer) in peers.enumerated() { + + let status = peer.addressName != nil ? "@\(peer.addressName!)" : (peer.isSupergroup || peer.isGroup ? L10n.discussionControllerPrivateGroup : L10n.discussionControllerPrivateChannel) + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_peer(peer.id), equatable: InputDataEquatable(PeerEquatable(peer: peer)), comparable: nil, item: { initialSize, stableId in + return ShortPeerRowItem(initialSize, peer: peer, account: arguments.context.account, status: status, inset: NSEdgeInsetsMake(0, 30, 0, 30), viewType: i == 0 ? .innerItem : bestGeneralViewType(peers, for: i), action: { + arguments.setup(peer) + }) + })) + index += 1 + } + } + + switch state.type { + case .channel: + if let associatedPeer = state.associatedPeer { + let text = L10n.discussionControllerChannelSetHeader1(associatedPeer.displayTitle) + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_channel_header, equatable: InputDataEquatable(text), comparable: nil, item: { initialSize, stableId in + + let attributedString = NSMutableAttributedString() + _ = attributedString.append(string: text, color: theme.colors.grayText, font: .normal(.text)) + attributedString.detectBoldColorInString(with: .medium(.text)) + + return AnimtedStickerHeaderItem(initialSize, stableId: stableId, context: arguments.context, sticker: LocalAnimatedSticker.discussion, text: attributedString) + })) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + let status = associatedPeer.addressName != nil ? "@\(associatedPeer.addressName!)" : (associatedPeer.isSupergroup ? L10n.discussionControllerPrivateGroup : L10n.discussionControllerPrivateChannel) + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_peer_info(associatedPeer.id), equatable: InputDataEquatable(PeerEquatable(peer: associatedPeer)), comparable: nil, item: { initialSize, stableId in + return ShortPeerRowItem(initialSize, peer: associatedPeer, account: arguments.context.account, status: status, inset: NSEdgeInsetsMake(0, 30, 0, 30), viewType: .singleItem, action: { + arguments.openInfo(associatedPeer.id) + }) + })) + index += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.discussionControllerChannelSetDescription), data: InputDataGeneralTextData(viewType: .textBottomItem))) + index += 1 + + if state.unlinkAbility { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_unlink_group, data: InputDataGeneralData(name: L10n.discussionControllerChannelSetUnlinkGroup, color: theme.colors.redUI, viewType: .singleItem, action: { + arguments.unlinkGroup(associatedPeer) + }))) + index += 1 + } + + } else { + let text = L10n.discussionControllerChannelEmptyHeader1 + + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_channel_header, equatable: InputDataEquatable(text), comparable: nil, item: { initialSize, stableId in + + let attributedString = NSMutableAttributedString() + _ = attributedString.append(string: text, color: theme.colors.grayText, font: .normal(.text)) + return AnimtedStickerHeaderItem(initialSize, stableId: stableId, context: arguments.context, sticker: LocalAnimatedSticker.discussion, text: attributedString) + })) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + if state.searchState == nil || state.searchState!.request.isEmpty { + + let viewType: GeneralViewType = state.filteredPeers.isEmpty ? .singleItem : .firstItem + + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_create_group, equatable: InputDataEquatable(viewType), comparable: nil, item: { initialSize, stableId in + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.discussionControllerChannelEmptyCreateGroup, nameStyle: blueActionButton, viewType: viewType, action: arguments.createGroup, thumb: GeneralThumbAdditional(thumb: theme.icons.peerInfoAddMember, textInset: 52, thumbInset: 5)) + })) + index += 1 + } + + applyList() + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.discussionControllerChannelEmptyDescription), data: InputDataGeneralTextData(viewType: .textBottomItem))) + index += 1 + + } + case .group: + if let associatedPeer = state.associatedPeer { + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_channel_header, equatable: nil, comparable: nil, item: { initialSize, stableId in + + let attributedString = NSMutableAttributedString() + _ = attributedString.append(string: L10n.discussionControllerGroupSetHeader(associatedPeer.displayTitle), color: theme.colors.grayText, font: .normal(.text)) + attributedString.detectBoldColorInString(with: .medium(.text)) + + return AnimtedStickerHeaderItem(initialSize, stableId: stableId, context: arguments.context, sticker: LocalAnimatedSticker.discussion, text: attributedString) + })) + + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + let status = associatedPeer.addressName != nil ? "@\(associatedPeer.addressName!)" : (associatedPeer.isSupergroup ? L10n.discussionControllerPrivateGroup : L10n.discussionControllerPrivateChannel) + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_peer_info(associatedPeer.id), equatable: InputDataEquatable(PeerEquatable(peer: associatedPeer)), comparable: nil, item: { initialSize, stableId in + return ShortPeerRowItem(initialSize, peer: associatedPeer, account: arguments.context.account, status: status, inset: NSEdgeInsetsMake(0, 30, 0, 30), viewType: .singleItem, action: { + arguments.openInfo(associatedPeer.id) + }) + })) + index += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.discussionControllerGroupSetDescription), data: InputDataGeneralTextData(viewType: .textBottomItem))) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_unlink_group, data: InputDataGeneralData(name: L10n.discussionControllerGroupSetUnlinkChannel, color: theme.colors.redUI, viewType: .singleItem, action: { + arguments.unlinkGroup(associatedPeer) + }))) + index += 1 + } else { + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_group_header, equatable: nil, comparable: nil, item: { initialSize, stableId in + + let attributedString = NSMutableAttributedString() + _ = attributedString.append(string: L10n.discussionControllerGroupUnsetDescription, color: theme.colors.grayText, font: .normal(.text)) + return GeneralTextRowItem(initialSize, stableId: stableId, text: attributedString, alignment: .center, centerViewAlignment: true, viewType: .textBottomItem) + })) + + } + } + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func ChannelDiscussionSetupController(context: AccountContext, peer: Peer)-> InputDataController { + + let initialState = DiscussionState(type: peer.isChannel ? .channel : .group, availablePeers: [], associatedPeer: nil, unlinkAbility: false, searchState: nil) + + let stateValue: Atomic = Atomic(value: initialState) + let statePromise:ValuePromise = ValuePromise(ignoreRepeated: true) + + let updateState:((DiscussionState)->DiscussionState)->Void = { f in + statePromise.set(stateValue.modify(f)) + } + + + let searchValue:Atomic = Atomic(value: .none) + let searchPromise: ValuePromise = ValuePromise(.none, ignoreRepeated: true) + let updateSearchValue:((TableSearchViewState)->TableSearchViewState)->Void = { f in + searchPromise.set(searchValue.modify(f)) + } + + + let searchData = TableSearchVisibleData(cancelImage: theme.icons.chatSearchCancel, cancel: { + updateSearchValue { _ in + return .none + } + }, updateState: { searchState in + updateState { + $0.withUpdatedSearchState(searchState) + } + }) + + let actionsDisposable = DisposableSet() + + func setup(_ channelId: PeerId, _ groupId: PeerId?, updatePreHistory: Bool = false) -> Void { + let signal: Signal + + if let groupId = groupId, groupId.namespace == Namespaces.Peer.CloudGroup { + signal = context.engine.peers.convertGroupToSupergroup(peerId: groupId) + |> mapError { value in + return (value, nil) + } + |> mapToSignal { upgradedPeerId in + return context.engine.peers.updateGroupDiscussionForChannel(channelId: channelId, groupId: upgradedPeerId) |> mapError { value in return (nil, value) } + } + } else if updatePreHistory, let groupId = groupId { + signal = context.engine.peers.updateChannelHistoryAvailabilitySettingsInteractively(peerId: groupId, historyAvailableForNewMembers: true) + |> mapError { error -> (ConvertGroupToSupergroupError?, ChannelDiscussionGroupError?) in + switch error { + case .generic: + return (nil, .generic) + case .hasNotPermissions: + return (nil, .hasNotPermissions) + } + } |> mapToSignal { _ in + return context.engine.peers.updateGroupDiscussionForChannel(channelId: channelId, groupId: groupId) |> mapError { value in return (nil, value) } + } + } else { + signal = context.engine.peers.updateGroupDiscussionForChannel(channelId: channelId, groupId: groupId) |> mapError { value in return (nil, value) } + } + + actionsDisposable.add(showModalProgress(signal: signal |> deliverOnMainQueue, for: context.window).start(next: { result in + if result && groupId == nil && initialState.type == .group { + context.sharedContext.bindings.rootNavigation().back() + } + updateSearchValue { current in + return .none + } + }, error: { upgradeError, discussError in + if let error = upgradeError { + switch error { + case .tooManyChannels: + showInactiveChannels(context: context, source: .upgrade) + case .generic: + alert(for: context.window, info: L10n.unknownError) + } + } else if let error = discussError { + switch error { + case .groupHistoryIsCurrentlyPrivate: + confirm(for: context.window, information: L10n.discussionControllerErrorPreHistory, okTitle: L10n.discussionControllerErrorOK, successHandler: { _ in + setup(channelId, groupId, updatePreHistory: true) + }) + case .hasNotPermissions: + alert(for: context.window, info: L10n.channelErrorDontHavePermissions) + default: + alert(for: context.window, info: L10n.unknownError) + } + } + + })) + } + + let arguments = DiscussionArguments(context: context, createGroup: { + let controller = context.sharedContext.bindings.rootNavigation().controller + actionsDisposable.add(createSupergroup(with: context, defaultText: peer.displayTitle + " Chat").start(next: { [weak controller] peerId in + if let peerId = peerId, let controller = controller { + setup(peer.id, peerId) + context.sharedContext.bindings.rootNavigation().removeUntil(InputDataController.self) + context.sharedContext.bindings.rootNavigation().push(controller) + } + })) + }, setup: { selected in + showModal(with: DiscussionSetModalController(context: context, channel: peer, group: selected, accept: { + if selected.isChannel { + setup(selected.id, peer.id) + } else { + setup(peer.id, selected.id) + } + }), for: context.window) + }, openInfo: { peerId in + context.sharedContext.bindings.rootNavigation().push(ChatAdditionController(context: context, chatLocation: .peer(peerId))) + }, unlinkGroup: { associated in + if associated.isChannel { + confirm(for: context.window, information: L10n.discussionControllerConfrimUnlinkChannel, successHandler: { _ in + setup(associated.id, nil) + }) + } else { + confirm(for: context.window, information: L10n.discussionControllerConfrimUnlinkGroup, successHandler: { _ in + setup(peer.id, nil) + }) + } + }) + + + let dataSignal = statePromise.get() |> map { state in + return channelDiscussionEntries(state: state, arguments: arguments) + } + + var updateBarIsHidden:((Bool)->Void)? = nil + + + actionsDisposable.add(context.account.postbox.peerView(id: peer.id).start(next: { peerView in + updateState { current in + var current = current + let peer = peerViewMainPeer(peerView) + if let cachedData = peerView.cachedData as? CachedChannelData, let linkedDiscussionPeerId = cachedData.linkedDiscussionPeerId.peerId { + current = current.withUpdatedassociatedPeer(peerView.peers[linkedDiscussionPeerId]) + } else { + current = current.withUpdatedassociatedPeer(nil) + } + if let linkedPeer = current.associatedPeer as? TelegramChannel, linkedPeer.isChannel { + current = current.withUpdatedUnlinkAbility(linkedPeer.hasPermission(.pinMessages)) + } else if let peer = peer as? TelegramChannel { + current = current.withUpdatedUnlinkAbility(peer.hasPermission(.pinMessages)) + } + return current + } + })) + + + let availableSignal = peer.isChannel ? context.engine.peers.availableGroupsForChannelDiscussion() : .single([]) + + actionsDisposable.add(availableSignal.start(next: { peers in + updateState { + $0.withUpdatedAvailablePeers(peers) + } + }, error: { error in + + })) + + + + + + + return InputDataController(dataSignal: combineLatest(dataSignal, searchPromise.get()) |> map { InputDataSignalValue(entries: $0, searchState: $1) }, title: peer.isChannel ? L10n.discussionControllerChannelTitle : L10n.discussionControllerGroupTitle, afterDisappear: { + actionsDisposable.dispose() + }, removeAfterDisappear: false, hasDone: false, customRightButton: { controller in + let bar = ImageBarView(controller: controller, theme.icons.chatSearch) + bar.button.set(handler: { _ in + updateSearchValue { current in + switch current { + case .none: + return .visible(searchData) + case .visible: + return .none + } + } + }, for: .Click) + updateBarIsHidden = { [weak bar] isHidden in + bar?.button.alphaValue = isHidden ? 0 : 1 + } + //let isHidden = stateValue.with {$0.associatedPeer != nil && $0.availablePeers.count > 5} + + + return bar + }, afterTransaction: { controller in + + let isHidden = stateValue.with {$0.associatedPeer != nil || $0.availablePeers.count < 5} + updateBarIsHidden?(isHidden) + + }, returnKeyInvocation: { _, _ in + let state = stateValue.with { $0 } + + if state.associatedPeer == nil, state.type == .channel, state.filteredPeers.count == 1, let searchState = state.searchState, !searchState.request.isEmpty { + arguments.setup(state.filteredPeers[0]) + return .nothing + } + + return .default + }, deleteKeyInvocation: { _ in + + let state = stateValue.with { $0 } + + if let peer = state.associatedPeer, state.unlinkAbility { + arguments.unlinkGroup(peer) + return .invoked + } + + return .default + }, searchKeyInvocation: { + + let state = stateValue.with { $0 } + + if state.associatedPeer == nil, state.availablePeers.count > 5 { + updateSearchValue { current in + switch current { + case .none: + return .visible(searchData) + case .visible: + return .none + } + } + } + + + return .invoked + }) +} diff --git a/Telegram-Mac/ChannelEventFilterModalController.swift b/Telegram-Mac/ChannelEventFilterModalController.swift index 1c361e10af..15ae479f81 100644 --- a/Telegram-Mac/ChannelEventFilterModalController.swift +++ b/Telegram-Mac/ChannelEventFilterModalController.swift @@ -8,20 +8,21 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac +import TelegramCore + +import SwiftSignalKit +import Postbox private final class ChannelFilterArguments { - let account:Account + let context: AccountContext let toggleFlags:(FilterEvents)->Void let toggleAdmin:(PeerId)->Void let toggleAllAdmins:()->Void let toggleAllEvents:()->Void - init(account:Account, toggleFlags:@escaping(FilterEvents)->Void, toggleAdmin:@escaping(PeerId)->Void, toggleAllAdmins:@escaping()->Void, toggleAllEvents:@escaping()->Void) { - self.account = account + init(context: AccountContext, toggleFlags:@escaping(FilterEvents)->Void, toggleAdmin:@escaping(PeerId)->Void, toggleAllAdmins:@escaping()->Void, toggleAllEvents:@escaping()->Void) { + self.context = context self.toggleFlags = toggleFlags self.toggleAdmin = toggleAdmin self.toggleAllAdmins = toggleAllAdmins @@ -55,52 +56,6 @@ private enum ChannelEventFilterEntryId : Hashable { return 6 } } - static func ==(lhs: ChannelEventFilterEntryId, rhs: ChannelEventFilterEntryId) -> Bool { - switch lhs { - case .section(let value): - if case .section(value) = rhs { - return true - } else { - return false - } - case .header(let value): - if case .header(value) = rhs { - return true - } else { - return false - } - case .allEvents: - if case .allEvents = rhs { - return true - } else { - return false - } - case .filter(let value): - if case .filter(value) = rhs { - return true - } else { - return false - } - case .allAdmins: - if case .allAdmins = rhs { - return true - } else { - return false - } - case .admin(let value): - if case .admin(value) = rhs { - return true - } else { - return false - } - case .adminsLoading: - if case .adminsLoading = rhs { - return true - } else { - return false - } - } - } } private enum ChannelEventFilterEntry : TableItemListNodeEntry { @@ -156,21 +111,15 @@ private enum ChannelEventFilterEntry : TableItemListNodeEntry { case .header(_, _, let text): return GeneralTextRowItem(initialSize, stableId: stableId, text: text) case .allAdmins(_, _, let enabled): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: "All Admins", type: .switchable (stateback: { - return enabled - }), action: { + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.chanelEventFilterAllAdmins, type: .switchable (enabled), action: { arguments.toggleAllAdmins() }) case .allEvents(_, _, let enabled): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: "All Events", type: .switchable (stateback: { - return enabled - }), action: { + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.chanelEventFilterAllEvents, type: .switchable (enabled), action: { arguments.toggleAllEvents() }) case let .filter(_, _, flag, name, enabled): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: name, type: .selectable(stateback: { () -> Bool in - return enabled - }), action: { + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: name, type: .selectable(enabled), action: { arguments.toggleFlags(flag) }) case .adminsLoading: @@ -180,66 +129,17 @@ private enum ChannelEventFilterEntry : TableItemListNodeEntry { let status:String switch participant.participant { case .creator: - status = tr(.adminsCreator) + status = L10n.adminsOwner case .member: - status = tr(.adminsAdmin) + status = L10n.adminsAdmin } - return ShortPeerRowItem(initialSize, peer: participant.peer, account: arguments.account, stableId: stableId, height: 40, photoSize: NSMakeSize(30, 30), status: status, inset: NSEdgeInsets(left: 30, right: 30), interactionType: .plain, generalType: .selectable(stateback: { () -> Bool in - return enabled - }), action: { + return ShortPeerRowItem(initialSize, peer: participant.peer, account: arguments.context.account, stableId: stableId, height: 40, photoSize: NSMakeSize(30, 30), status: status, inset: NSEdgeInsets(left: 30, right: 30), interactionType: .plain, generalType: .selectable(enabled), action: { arguments.toggleAdmin(participant.peer.id) }) } } } -private func ==(lhs:ChannelEventFilterEntry, rhs:ChannelEventFilterEntry) -> Bool { - switch lhs { - case let .section(section): - if case .section(section) = rhs { - return true - } else { - return false - } - case let .header(section, index, text): - if case .header(section, index, text) = rhs { - return true - } else { - return false - } - case let .allEvents(section, index, enabled): - if case .allEvents(section, index, enabled) = rhs { - return true - } else { - return false - } - case let .filter(section, index, flags, text, enabled): - if case .filter(section, index, flags, text, enabled) = rhs { - return true - } else { - return false - } - case let .allAdmins(section, index, enabled): - if case .allAdmins(section, index, enabled) = rhs { - return true - } else { - return false - } - case let .adminsLoading(section, index): - if case .adminsLoading(section, index) = rhs { - return true - } else { - return false - } - case let .admin(section, index, participant, enabled): - if case .admin(section, index, participant, enabled) = rhs { - return true - } else { - return false - } - } -} - private func <(lhs:ChannelEventFilterEntry, rhs: ChannelEventFilterEntry) -> Bool { return lhs.index < rhs.index } @@ -327,48 +227,57 @@ private enum FilterEvents { case groupInfo case deletedMessages case editedMessages + case voiceChats case pinnedMessages case leavingMembers - + case invites var flags:AdminLogEventsFlags { switch self { case .newMembers: - return [AdminLogEventsFlags.join, AdminLogEventsFlags.unban] + return [.join, .unban] case .newAdmins: - return [AdminLogEventsFlags.promote] + return [.promote] case .leavingMembers: - return [AdminLogEventsFlags.leave, AdminLogEventsFlags.kick] + return [.leave, .kick] case .restrictions: - return [AdminLogEventsFlags.unban, AdminLogEventsFlags.ban] + return [.unban, .ban] case .groupInfo: - return [AdminLogEventsFlags.info, AdminLogEventsFlags.settings] + return [.info, .settings] case .pinnedMessages: - return [AdminLogEventsFlags.pinnedMessages] + return [.pinnedMessages] case .editedMessages: - return [AdminLogEventsFlags.editMessages] + return [.editMessages] case .deletedMessages: - return [AdminLogEventsFlags.deleteMessages] + return [.deleteMessages] + case .voiceChats: + return [.calls] + case .invites: + return [.invites] } } func localizedString(_ broadcast:Bool) -> String { switch self { case .newMembers: - return tr(.channelEventFilterNewMembers) + return tr(L10n.channelEventFilterNewMembers) case .newAdmins: - return tr(.channelEventFilterNewAdmins) + return tr(L10n.channelEventFilterNewAdmins) case .leavingMembers: - return tr(.channelEventFilterLeavingMembers) + return tr(L10n.channelEventFilterLeavingMembers) case .restrictions: - return tr(.channelEventFilterNewRestrictions) + return tr(L10n.channelEventFilterNewRestrictions) case .groupInfo: - return broadcast ? tr(.channelEventFilterChannelInfo) : tr(.channelEventFilterGroupInfo) + return broadcast ? tr(L10n.channelEventFilterChannelInfo) : tr(L10n.channelEventFilterGroupInfo) case .pinnedMessages: - return tr(.channelEventFilterPinnedMessages) + return tr(L10n.channelEventFilterPinnedMessages) case .editedMessages: - return tr(.channelEventFilterEditedMessages) + return tr(L10n.channelEventFilterEditedMessages) case .deletedMessages: - return tr(.channelEventFilterDeletedMessages) + return tr(L10n.channelEventFilterDeletedMessages) + case .voiceChats: + return tr(L10n.channelEventFilterVoiceChats) + case .invites: + return tr(L10n.channelEventFilterInvites) } } } @@ -377,7 +286,7 @@ private func eventFilters(_ channel: Bool) -> [FilterEvents] { if channel { return [.newMembers, .newAdmins, .groupInfo, .deletedMessages, .editedMessages, .leavingMembers] } else { - return [.restrictions, .newMembers, .newAdmins, .groupInfo, .deletedMessages, .editedMessages, .pinnedMessages, .leavingMembers] + return [.restrictions, .newMembers, .newAdmins, .groupInfo, .invites, .deletedMessages, .editedMessages, .voiceChats, .pinnedMessages, .leavingMembers] } } @@ -391,7 +300,7 @@ private func channelEventFilterEntries(state: ChannelEventFilterState, peer:Peer entries.append(.section(section)) section += 1 - entries.append(.header(section, index, text: tr(.channelEventFilterEventsHeader))) + entries.append(.header(section, index, text: tr(L10n.channelEventFilterEventsHeader))) index += 1 entries.append(.allEvents(section, index, enabled: state.eventsException.isEmpty)) index += 1 @@ -404,7 +313,7 @@ private func channelEventFilterEntries(state: ChannelEventFilterState, peer:Peer entries.append(.section(section)) section += 1 - entries.append(.header(section, index, text: tr(.channelEventFilterAdminsHeader))) + entries.append(.header(section, index, text: tr(L10n.channelEventFilterAdminsHeader))) index += 1 entries.append(.allAdmins(section, index, enabled: state.adminsException.isEmpty)) @@ -433,14 +342,16 @@ fileprivate func prepareTransition(left:[ChannelEventFilterEntry], right: [Chann class ChannelEventFilterModalController: ModalViewController { private let peerId:PeerId - private let account:Account + private let context:AccountContext private let stateValue = Atomic(value: ChannelEventFilterState()) private let disposable = MetaDisposable() private let updated:(ChannelEventFilterState) -> Void - init(account:Account, peerId:PeerId, state: ChannelEventFilterState = ChannelEventFilterState(), updated:@escaping(ChannelEventFilterState) -> Void) { - self.account = account + private let admins: [RenderedChannelParticipant] + init(context: AccountContext, peerId:PeerId, admins: [RenderedChannelParticipant], state: ChannelEventFilterState = ChannelEventFilterState(), updated:@escaping(ChannelEventFilterState) -> Void) { + self.context = context self.peerId = peerId + self.admins = admins self.updated = updated _ = self.stateValue.swap(state) super.init(frame: NSMakeRect(0, 0, 300, 300)) @@ -475,12 +386,12 @@ class ChannelEventFilterModalController: ModalViewController { super.viewDidLoad() let stateValue = self.stateValue - let statePromise = ValuePromise(stateValue.modify({$0}), ignoreRepeated: true) + let statePromise = ValuePromise(stateValue.with { $0 }, ignoreRepeated: true) let updateState: ((ChannelEventFilterState) -> ChannelEventFilterState) -> Void = { f in - statePromise.set(stateValue.modify { f($0) }) + statePromise.set(stateValue.modify(f)) } - let arguments = ChannelFilterArguments(account: account, toggleFlags: { flags in + let arguments = ChannelFilterArguments(context: context, toggleFlags: { flags in updateState({$0.withToggledEventsException(flags)}) }, toggleAdmin: { peerId in updateState({$0.withToggledAdminsException(peerId)}) @@ -493,11 +404,11 @@ class ChannelEventFilterModalController: ModalViewController { let previous: Atomic<[ChannelEventFilterEntry]> = Atomic(value: []) let initialSize = self.atomicSize - let adminsSignal = Signal<[RenderedChannelParticipant]?, Void>.single(nil) |> then ( channelAdmins(account: account, peerId: peerId) |> map {Optional($0)}) + let adminsSignal = Signal<[RenderedChannelParticipant], NoError>.single(admins) let updatedSize:Atomic = Atomic(value: false) - let signal:Signal = combineLatest(statePromise.get(), account.postbox.loadedPeerWithId(peerId), adminsSignal) |> map { state, peer, admins -> (ChannelEventFilterState, Peer, [RenderedChannelParticipant]?) in + let signal:Signal = combineLatest(statePromise.get(), context.account.postbox.loadedPeerWithId(peerId), adminsSignal) |> map { state, peer, admins -> (ChannelEventFilterState, Peer, [RenderedChannelParticipant]?) in - let state = stateValue.swap(state.withUpdatedAllAdmins(Set(admins?.map {$0.peer.id} ?? [])).withUpdatedAllEvents(Set(eventFilters(peer.isChannel)))) + let state = stateValue.swap(state.withUpdatedAllAdmins(Set(admins.map {$0.peer.id})).withUpdatedAllEvents(Set(eventFilters(peer.isChannel)))) return (state, peer, admins) } |> map { state, peer, admins in @@ -526,9 +437,9 @@ class ChannelEventFilterModalController: ModalViewController { } override var modalInteractions: ModalInteractions? { - return ModalInteractions(acceptTitle: tr(.modalOK), accept: { [weak self] in + return ModalInteractions(acceptTitle: tr(L10n.modalOK), accept: { [weak self] in self?.noticeUpdated() - }, cancelTitle: tr(.modalCancel), drawBorder: true, height: 40) + }, cancelTitle: L10n.modalCancel, drawBorder: true, height: 40) } deinit { diff --git a/Telegram-Mac/ChannelEventLogController.swift b/Telegram-Mac/ChannelEventLogController.swift index deddd5418f..5449ddf8df 100644 --- a/Telegram-Mac/ChannelEventLogController.swift +++ b/Telegram-Mac/ChannelEventLogController.swift @@ -11,10 +11,17 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore +import Postbox +import SwiftSignalKit + +extension ChannelAdminEventLogEntry : Identifiable { + + //private func eventLogItems(_ entries:[ChannelAdminEventLogEntry], isGroup: Bool, peerId: PeerId, initialSize: NSSize, chatInteraction: ChatInteraction) -> [TableRowItem] { + + +} class ChannelEventLogTitledView : TitledBarView { private var titleNode:TextNode = TextNode() @@ -27,23 +34,22 @@ class ChannelEventLogTitledView : TitledBarView { override func draw(_ layer: CALayer, in ctx: CGContext) { super.draw(layer, in: ctx) - ctx.setFillColor(theme.colors.background.cgColor) - ctx.fill(bounds) + layer.backgroundColor = theme.colors.background.cgColor - let (textLayout, textApply) = TextNode.layoutText(maybeNode: titleNode, attributedText, nil, 1, .end, NSMakeSize(bounds.width - 40, bounds.height), nil,false, .left) + let (textLayout, textApply) = TextNode.layoutText(maybeNode: titleNode, attributedText, nil, 1, .end, NSMakeSize(bounds.width - 40, bounds.height), nil,false, .left) let textRect = focus(textLayout.size) - textApply.draw(textRect, in: ctx, backingScaleFactor: backingScaleFactor) + textApply.draw(textRect, in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) var iconRect = focus(theme.icons.eventLogTriangle.backingSize) iconRect.origin.x = textRect.maxX + 6 iconRect.origin.y += 1 ctx.draw(theme.icons.eventLogTriangle, in: iconRect) - + } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) backgroundColor = theme.colors.background needsDisplay = true } @@ -74,20 +80,20 @@ private class SearchContainerView : View { addSubview(searchView) addSubview(separator) cancelButton.set(font: .medium(.text), for: .Normal) - cancelButton.set(text: tr(.chatCancel), for: .Normal) - cancelButton.sizeToFit() + cancelButton.set(text: tr(L10n.chatCancel), for: .Normal) + _ = cancelButton.sizeToFit() cancelButton.set(handler: { [weak self] _ in self?.hideSearch?() - }, for: .Click) + }, for: .Click) addSubview(cancelButton) - updateLocalizationAndTheme() + updateLocalizationAndTheme(theme: theme) } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) cancelButton.set(background: theme.colors.background, for: .Normal) - cancelButton.set(color: theme.colors.blueUI, for: .Normal) + cancelButton.set(color: theme.colors.accent, for: .Normal) separator.backgroundColor = theme.colors.border backgroundColor = theme.colors.background @@ -135,21 +141,22 @@ class ChannelEventLogView : View { emptyTextView.isSelectable = false separator.backgroundColor = .border whatButton.set(font: .medium(.title), for: .Normal) - whatButton.set(text: tr(.channelEventLogWhat), for: .Normal) + whatButton.set(text: tr(L10n.channelEventLogWhat), for: .Normal) whatButton.set(handler: { _ in - alert(for: mainWindow, header: tr(.channelEventLogAlertHeader), info: tr(.channelEventLogAlertInfo)) + alert(for: mainWindow, header: tr(L10n.channelEventLogAlertHeader), info: tr(L10n.channelEventLogAlertInfo)) }, for: .Click) setFrameSize(frameRect.size) - updateLocalizationAndTheme() + updateLocalizationAndTheme(theme: theme) } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - whatButton.set(color: theme.colors.blueUI, for: .Normal) + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + self.backgroundColor = theme.colors.chatBackground + whatButton.set(color: theme.colors.accent, for: .Normal) whatButton.set(background: theme.colors.grayTransparent, for: .Highlight) whatButton.set(background: theme.colors.background, for: .Normal) - emptyTextView.backgroundColor = theme.colors.background + emptyTextView.backgroundColor = theme.colors.chatBackground separator.backgroundColor = theme.colors.border } @@ -226,6 +233,7 @@ private struct EventLogTableTransition { let state:ChannelEventFilterState let maxId:AdminLogEventId let eventLog:AdminLogEventsResult + let fullyLoaded: Bool } extension AdminLogEventsResult { @@ -235,86 +243,48 @@ extension AdminLogEventsResult { } return false } - - var banHelp:[TelegramChannelBannedRightsFlags] { - var order:[TelegramChannelBannedRightsFlags] = [] - order.append(.banSendMessages) - order.append(.banSendMedia) - order.append(.banSendStickers) - order.append(.banEmbedLinks) - return order - } - - var rightsHelp:(specific: TelegramChannelAdminRightsFlags, order: [TelegramChannelAdminRightsFlags]) { - if let peer = peers[peerId] as? TelegramChannel { - let maskRightsFlags: TelegramChannelAdminRightsFlags - let rightsOrder: [TelegramChannelAdminRightsFlags] - - switch peer.info { - case .broadcast: - maskRightsFlags = .broadcastSpecific - rightsOrder = [ - .canChangeInfo, - .canPostMessages, - .canEditMessages, - .canDeleteMessages, - .canAddAdmins - ] - case .group: - maskRightsFlags = .groupSpecific - rightsOrder = [ - .canChangeInfo, - .canDeleteMessages, - .canBanUsers, - .canInviteUsers, - .canChangeInviteLink, - .canPinMessages, - .canAddAdmins - ] - - } - return (specific: maskRightsFlags, order: rightsOrder) - } - return (specific: [], order: []) - } - + } -private func eventLogItems(_ result:AdminLogEventsResult, initialSize: NSSize, chatInteraction: ChatInteraction) -> [TableRowItem] { +private func eventLogItems(_ entries:[ChannelAdminEventLogEntry], isGroup: Bool, peerId: PeerId, initialSize: NSSize, chatInteraction: ChatInteraction) -> [TableRowItem] { var items:[TableRowItem] = [] var index:Int = 0 - let timeDifference = Int32(chatInteraction.account.context.timeDifference) - for event in result.events { - switch event.action { + let timeDifference = Int32(chatInteraction.context.timeDifference) + for entry in entries { + switch entry.event.action { case let .editMessage(prev, new): - let item = ChatRowItem.item(initialSize, from: .MessageEntry(new.withUpdatedStableId(arc4random()), true, .Full(isAdmin: false), nil, nil), with: chatInteraction.account, interaction: chatInteraction) - items.append(ChannelEventLogEditedPanelItem(initialSize, previous: prev, item: item)) - items.append(item) + let item = ChatRowItem.item(initialSize, from: .MessageEntry(new.withUpdatedStableId(arc4random()), MessageIndex(new), true, .list, .Full(rank: nil, header: .normal), nil, ChatHistoryEntryData(nil, MessageEntryAdditionalData(), AutoplayMediaPreferences.defaultSettings)), interaction: chatInteraction, theme: theme) as? ChatRowItem + if let item = item { + if !(new.media.first is TelegramMediaAction) { + items.append(ChannelEventLogEditedPanelItem(initialSize, previous: prev, item: item)) + items.append(item) + } + } case let .deleteMessage(message): - items.append(ChatRowItem.item(initialSize, from: .MessageEntry(message.withUpdatedStableId(arc4random()), true, .Full(isAdmin: false), nil, nil), with: chatInteraction.account, interaction: chatInteraction)) + items.append(ChatRowItem.item(initialSize, from: .MessageEntry(message.withUpdatedStableId(arc4random()).withUpdatedTimestamp(entry.event.date), MessageIndex(message), true, .list, .Full(rank: nil, header: .normal), nil, ChatHistoryEntryData(nil, MessageEntryAdditionalData(), AutoplayMediaPreferences.defaultSettings)), interaction: chatInteraction, theme: theme)) case let .updatePinned(message): if let message = message?.withUpdatedStableId(arc4random()) { - items.append(ChatRowItem.item(initialSize, from: .MessageEntry(message, true, .Full(isAdmin: false), nil, nil), with: chatInteraction.account, interaction: chatInteraction)) + items.append(ChatRowItem.item(initialSize, from: .MessageEntry(message, MessageIndex(message), true, .list, .Full(rank: nil, header: .normal), nil, ChatHistoryEntryData(nil, MessageEntryAdditionalData(), AutoplayMediaPreferences.defaultSettings)), interaction: chatInteraction, theme: theme)) } default: break } - items.append(ServiceEventLogItem(initialSize, event: event, result: result, chatInteraction: chatInteraction)) + items.append(ServiceEventLogItem(initialSize, entry: entry, isGroup: isGroup, chatInteraction: chatInteraction)) - let nextEvent = index == result.events.count - 1 ? nil : result.events[index + 1] + let nextEvent = index == entries.count - 1 ? nil : entries[index + 1].event if let nextEvent = nextEvent { - let dateId = chatDateId(for: event.date - timeDifference) + let dateId = chatDateId(for: entry.event.date - timeDifference) let nextDateId = chatDateId(for: nextEvent.date - timeDifference) if dateId != nextDateId { - let messageIndex = MessageIndex(id: MessageId(peerId: result.peerId, namespace: 0, id: INT_MAX), timestamp: Int32(dateId)) - items.append(ChatDateStickItem(initialSize, .DateEntry(messageIndex), interaction: chatInteraction)) + let messageIndex = MessageIndex(id: MessageId(peerId: peerId, namespace: 0, id: INT_MAX), timestamp: Int32(dateId)) + items.append(ChatDateStickItem(initialSize, .DateEntry(messageIndex, .list, theme), interaction: chatInteraction, theme: theme)) } } index += 1 - + } for item in items { _ = item.makeSize(initialSize.width, oldWidth: initialSize.width) @@ -325,12 +295,19 @@ private func eventLogItems(_ result:AdminLogEventsResult, initialSize: NSSize, c class ChannelEventLogController: TelegramGenericViewController { private let peerId:PeerId private let chatInteraction:ChatInteraction - private let promise:Promise = Promise() - private let history:Promise<(AdminLogEventId, ChannelEventFilterState)> = Promise() - private var state:Atomic = Atomic(value: nil) private let disposable = MetaDisposable() - private let openPeerDisposable = MetaDisposable() private let searchState:ValuePromise = ValuePromise(SearchState(state: .None, request: nil), ignoreRepeated: true) + private let filterDisposable = MetaDisposable() + private let updateFilterDisposable = MetaDisposable() + + private let filterStateValue:ValuePromise = ValuePromise(ChannelEventFilterState()) + private let filterState:Atomic = Atomic(value: ChannelEventFilterState()) + + private func updateFilter(_ f:(ChannelEventFilterState)->ChannelEventFilterState) -> Void { + self.filterStateValue.set(filterState.modify(f)) + } + + override func viewClass() -> AnyClass { return ChannelEventLogView.self } @@ -351,18 +328,37 @@ class ChannelEventLogController: TelegramGenericViewController(nil) + _ = context.peerChannelMemberCategoriesContextsManager.admins(peerId: peerId, updated: { membersState in + if case .loading = membersState.loadingState, membersState.list.isEmpty { + adminsPromise.set(nil) + } else { + adminsPromise.set(membersState.list) + } + }) + + let admins = adminsPromise.get() |> filter { $0 != nil } |> take(1) |> map { $0! } + filterDisposable.set(showModalProgress(signal: admins, for: context.window).start(next: { [weak self] admins in + showModal(with: ChannelEventFilterModalController(context: context, peerId: peerId, admins: admins, state: state, updated: { [weak self] updatedState in + + self?.updateFilter { _ in + return updatedState + } + + }), for: context.window) + + })) } - init(_ account:Account, peerId:PeerId) { + init(_ context: AccountContext, peerId:PeerId) { self.peerId = peerId - chatInteraction = ChatInteraction(peerId: peerId, account: account, isLogInteraction: true) - super.init(account) + chatInteraction = ChatInteraction(chatLocation: .peer(peerId), context: context, isLogInteraction: true) + + super.init(context) + } override var enableBack: Bool { @@ -371,7 +367,8 @@ class ChannelEventLogController: TelegramGenericViewController KeyHandlerResult in + window?.set(handler: { [weak self] _ -> KeyHandlerResult in if let strongSelf = self { if !strongSelf.genericView.inSearch { strongSelf.genericView.showSearch() @@ -406,108 +403,110 @@ class ChannelEventLogController: TelegramGenericViewController deliverOnMainQueue).start(next: { [weak strongSelf] peer in - if let strongSelf = strongSelf { - strongSelf.navigationController?.push(PeerInfoController(account: strongSelf.account, peer: peer)) - } - })) + strongSelf.navigationController?.push(PeerInfoController(context: context, peerId: peerId)) } } self.chatInteraction.inlineAudioPlayer = { [weak self] controller in - if let navigation = self?.navigationController { - if let header = navigation.header, let strongSelf = self { - header.show(true) - if let view = header.view as? InlineAudioPlayerView { - view.update(with: controller, tableView: strongSelf.genericView.tableView) - } - } - } + let object = InlineAudioPlayerView.ContextObject(controller: controller, context: context, tableView: self?.genericView.tableView, supportTableView: nil) + self?.navigationController?.header?.show(true, contextObject: object) } + + - let currentMaxId:Atomic = Atomic(value: 0) + let updateFilter = combineLatest(queue: .mainQueue(),searchState.get(), filterStateValue.get()) - let initialSize = self.atomicSize - let chatInteraction = self.chatInteraction - let account = self.account - let peerId = self.peerId + updateFilterDisposable.set(updateFilter.start(next: { search, filter in + eventLogContext.setFilter(.init(query: search.request, events: filter.selectedFlags, adminPeerIds: filter.selectedAdmins)) + })) + + let isGroup: Signal = context.account.postbox.transaction { + let peer = $0.getPeer(peerId) + if let peer = peer { + return peer.isGroup || peer.isSupergroup + } else { + return false + } + } - let previousState:Atomic = Atomic(value: nil) - let previousAppearance:Atomic = Atomic(value: nil) - let previousSearchState:Atomic = Atomic(value: SearchState(state: .None, request: nil)) - disposable.set((combineLatest(searchState.get() |> map {SearchState(state: .None, request: $0.request)} |> distinctUntilChanged, history.get() |> filter {$0.0 != -1}) |> mapToSignal { values -> Signal in + + + let signal: Signal<([TableRowItem], ([ChannelAdminEventLogEntry], Bool, ChannelAdminEventLogUpdateType, Bool), Bool), NoError> = combineLatest(eventLogContext.get(), isGroup) |> map { result, isGroup in + + let items = eventLogItems(result.0.reversed(), isGroup: isGroup, peerId: peerId, initialSize: initialSize.with { $0 }, chatInteraction: chatInteraction) - let state = values.1.1 - let searchState = values.0 - return .single(nil) |> then (combineLatest(channelAdminLogEvents(account, peerId: peerId, maxId: values.1.0, minId: -1, limit: 50, query: searchState.request, filter: state.selectedFlags, admins: state.selectedAdmins) |> mapError { _ in} |> deliverOnPrepareQueue, appearanceSignal) |> map { result, appearance in - - let maxId = result.events.min(by: { (lhs, rhs) -> Bool in - return lhs.id < rhs.id - })?.id ?? -1 - - let items = eventLogItems(result, initialSize: initialSize.modify({$0}), chatInteraction: chatInteraction) - let _previousState = previousState.swap(state) - let _previousAppearance = previousAppearance.swap(appearance) - let _previousSearchState = previousSearchState.swap(searchState) - return EventLogTableTransition(result: items, addition: _previousState == state && _previousSearchState == searchState && _previousAppearance == appearance, state: state, maxId: maxId, eventLog: result) + return (items, result, isGroup) - } |> map {Optional($0)}) + } |> deliverOnMainQueue + + // subscriber.putNext((strongSelf.entries.0, strongSelf.hasEarlier, .initial, strongSelf.hasEntries)) + + + disposable.set(signal.start(next: { [weak self] items, result, isGroup in + self?.genericView.tableView.beginTableUpdates() + self?.genericView.tableView.removeAll() + self?.genericView.tableView.insert(items: items) + self?.genericView.tableView.endTableUpdates() - } - |> deliverOnMainQueue).start(next: { [weak self] transition in - if let tableView = self?.genericView.tableView { - if let transition = transition, let peer = transition.eventLog.peers[transition.eventLog.peerId] { - if !transition.addition { - tableView.removeAll() - _ = tableView.addItem(item: GeneralRowItem(initialSize.modify{$0}, height: 20, stableId: arc4random())) - } - tableView.insert(items: transition.result, at: tableView.count) - self?.genericView.updateState(tableView.isEmpty ? (transition.state.isEmpty && previousSearchState.modify({$0}).request.isEmpty ? .empty(peer.isChannel ? tr(.channelEventLogEmptyText) : tr(.groupEventLogEmptyText)) : .empty(tr(.channelEventLogEmptySearch))) : .history) + switch result.2 { + case .initial: + self?.genericView.updateState(.loading) + case .load, .generic: + if items.isEmpty { + self?.genericView.updateState(.empty(!isGroup ? L10n.channelEventLogEmptyText : L10n.groupEventLogEmptyText)) } else { - self?.genericView.updateState(.loading) - self?.genericView.tableView.removeAll() - _ = tableView.addItem(item: GeneralRowItem(initialSize.modify{$0}, height: 20, stableId: arc4random())) + self?.genericView.updateState(.history) } - - tableView.resetScrollNotifies() - _ = currentMaxId.swap(transition?.maxId ?? -1) } + })) - genericView.tableView.setScrollHandler { [weak self] scroll in - if let strongSelf = self { - switch scroll.direction { - case .bottom: - strongSelf.history.set(strongSelf.history.get() |> take(1) |> map { (_, state) in - return (currentMaxId.modify({$0}), state) - }) - default: - break - } + + genericView.tableView.setScrollHandler { scroll in + switch scroll.direction { + case .bottom: + eventLogContext.loadMoreEntries() + default: + break } } + + self.genericView.tableView.removeAll() + _ = self.genericView.tableView.addItem(item: GeneralRowItem(initialSize.modify{$0}, height: 20, stableId: arc4random(), backgroundColor: theme.colors.chatBackground)) + readyOnce() - history.set(.single((0, ChannelEventFilterState()))) - _ = state.swap(ChannelEventFilterState()) + } } + diff --git a/Telegram-Mac/ChannelEventLogItem.swift b/Telegram-Mac/ChannelEventLogItem.swift index b75845c721..cbbbff4683 100644 --- a/Telegram-Mac/ChannelEventLogItem.swift +++ b/Telegram-Mac/ChannelEventLogItem.swift @@ -8,8 +8,55 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac +import Postbox +import TelegramCore + + +private var banHelp:[TelegramChatBannedRightsFlags] { + var order:[TelegramChatBannedRightsFlags] = [] + order.append(.banSendMessages) + order.append(.banReadMessages) + order.append(.banChangeInfo) + order.append(.banSendMedia) + order.append(.banSendStickers) + order.append(.banSendGifs) + order.append(.banAddMembers) + order.append(.banPinMessages) + order.append(.banSendInline) + order.append(.banSendPolls) + order.append(.banEmbedLinks) + return order +} + +func rightsHelp(_ isGroup: Bool) -> (specific: TelegramChatAdminRightsFlags, order: [TelegramChatAdminRightsFlags]) { + let maskRightsFlags: TelegramChatAdminRightsFlags + let rightsOrder: [TelegramChatAdminRightsFlags] + + if isGroup { + maskRightsFlags = .broadcastSpecific + rightsOrder = [ + .canChangeInfo, + .canPostMessages, + .canEditMessages, + .canDeleteMessages, + .canAddAdmins, + .canBeAnonymous + ] + } else { + maskRightsFlags = .groupSpecific + rightsOrder = [ + .canChangeInfo, + .canDeleteMessages, + .canBanUsers, + .canInviteUsers, + .canPinMessages, + .canManageCalls, + .canAddAdmins, + .canBeAnonymous + ] + } + return (specific: maskRightsFlags, order: rightsOrder) +} private struct ServiceEventLogMessagePanel { let header:TextViewLayout @@ -91,7 +138,7 @@ private class ServiceEventLogMessagePanelView : View { } override func draw(_ layer: CALayer, in ctx: CGContext) { super.draw(layer, in: ctx) - ctx.setFillColor(theme.colors.blueFill.cgColor) + ctx.setFillColor(theme.colors.accent.cgColor) let radius:CGFloat = 1.0 ctx.fill(NSMakeRect(0, radius, 2, layer.bounds.height - radius * 2)) ctx.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: radius + radius, height: radius + radius))) @@ -222,30 +269,32 @@ class ServiceEventLogItem: TableRowItem { fileprivate private(set) var contentMessageItem: ServiceEventLogMessageContentItem? - fileprivate let event: AdminLogEvent - fileprivate let result: AdminLogEventsResult + fileprivate let entry: ChannelAdminEventLogEntry fileprivate let chatInteraction: ChatInteraction - init(_ initialSize: NSSize, event: AdminLogEvent, result:AdminLogEventsResult, chatInteraction: ChatInteraction) { - self.event = event + fileprivate let isGroup: Bool + fileprivate let peerId: PeerId + init(_ initialSize: NSSize, entry: ChannelAdminEventLogEntry, isGroup: Bool, chatInteraction: ChatInteraction) { + self.entry = entry + self.isGroup = isGroup + self.peerId = chatInteraction.peerId self.chatInteraction = chatInteraction - self.result = result let attributedString = NSMutableAttributedString() - if let peer = result.peers[event.peerId] { + if let peer = entry.peers[entry.event.peerId] { let contentName = NSMutableAttributedString() - let date:NSAttributedString = .initialize(string: DateUtils.string(forMessageListDate: event.date), color: theme.colors.grayText, font: .normal(.short)) + let date:NSAttributedString = .initialize(string: DateUtils.string(forMessageListDate: entry.event.date), color: theme.colors.grayText, font: .normal(.short)) var nameColor:NSColor - if chatInteraction.account.peerId == peer.id { + if chatInteraction.context.peerId == peer.id { nameColor = theme.colors.link } else { - let value = ObjcUtils.colorMask(peer.id.id, mainId: chatInteraction.account.peerId.id) - nameColor = userChatColors[Int(value) % userChatColors.count] ?? .blueText + let value = abs(Int(peer.id.id._internalGetInt64Value()) % 7) + nameColor = theme.chat.peerName(value) } let range = contentName.append(string: peer.displayTitle, color: nameColor, font: .medium(.text)) - contentName.add(link: inAppLink.peerInfo(peerId: peer.id, action: nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range, color: nameColor) + contentName.add(link: inAppLink.peerInfo(link: "", peerId: peer.id, action: nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range, color: nameColor) struct ChangedInfo { @@ -262,104 +311,239 @@ class ServiceEventLogItem: TableRowItem { var changedInfo:ChangedInfo? = nil var serviceInfo: ServiceTextInfo? - let peerLink = (range: peer.displayTitle, link: inAppLink.peerInfo(peerId:peer.id, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo)) + let peerLink = (range: peer.displayTitle, link: inAppLink.peerInfo(link: "", peerId:peer.id, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo)) - switch event.action { + switch entry.event.action { case let .changeTitle(prev, new): - changedInfo = ChangedInfo(prev: prev, new: new, panelText: !prev.isEmpty ? tr(.eventLogServicePreviousTitle) : nil) - serviceInfo = ServiceTextInfo(text: !result.isGroup ? tr(.channelEventLogServiceTitleUpdated(peer.displayTitle)) : tr(.groupEventLogServiceTitleUpdated(peer.displayTitle)), firstLink: peerLink, secondLink: nil) + changedInfo = ChangedInfo(prev: prev, new: new, panelText: !prev.isEmpty ? L10n.eventLogServicePreviousTitle : nil) + serviceInfo = ServiceTextInfo(text: !isGroup ? L10n.channelEventLogServiceTitleUpdated(peer.displayTitle) : L10n.groupEventLogServiceTitleUpdated(peer.displayTitle), firstLink: peerLink, secondLink: nil) case let .changeAbout(prev, new): let text:String if !new.isEmpty { - text = !result.isGroup ? tr(.channelEventLogServiceAboutUpdated(peer.displayTitle)) : tr(.groupEventLogServiceAboutUpdated(peer.displayTitle)) + text = !isGroup ? L10n.channelEventLogServiceAboutUpdated(peer.displayTitle) : L10n.groupEventLogServiceAboutUpdated(peer.displayTitle) } else { - text = !result.isGroup ? tr(.channelEventLogServiceAboutRemoved(peer.displayTitle)) : tr(.groupEventLogServiceAboutRemoved(peer.displayTitle)) + text = !isGroup ? L10n.channelEventLogServiceAboutRemoved(peer.displayTitle) : L10n.groupEventLogServiceAboutRemoved(peer.displayTitle) } - changedInfo = ChangedInfo(prev: prev, new: new, panelText: !prev.isEmpty ? tr(.eventLogServicePreviousDesc) : nil) + changedInfo = ChangedInfo(prev: prev, new: new, panelText: !prev.isEmpty ? L10n.eventLogServicePreviousDesc : nil) serviceInfo = ServiceTextInfo(text: text, firstLink: peerLink, secondLink: nil) case let .changeUsername(prev, new): let text:String if !new.isEmpty { - text = !result.isGroup ? tr(.channelEventLogServiceLinkUpdated(peer.displayTitle)) : tr(.groupEventLogServiceLinkUpdated(peer.displayTitle)) + text = !isGroup ? L10n.channelEventLogServiceLinkUpdated(peer.displayTitle) : L10n.groupEventLogServiceLinkUpdated(peer.displayTitle) } else { - text = !result.isGroup ? tr(.channelEventLogServiceLinkRemoved(peer.displayTitle)) : tr(.groupEventLogServiceLinkRemoved(peer.displayTitle)) + text = !isGroup ? L10n.channelEventLogServiceLinkRemoved(peer.displayTitle) : L10n.groupEventLogServiceLinkRemoved(peer.displayTitle) } - changedInfo = ChangedInfo(prev: "https://t.me/\(prev)", new: new.isEmpty ? "" : "https://t.me/\(new)", panelText: !prev.isEmpty ? tr(.eventLogServicePreviousLink) : nil) + changedInfo = ChangedInfo(prev: "https://t.me/\(prev)", new: new.isEmpty ? "" : "https://t.me/\(new)", panelText: !prev.isEmpty ? L10n.eventLogServicePreviousLink : nil) serviceInfo = ServiceTextInfo(text: text, firstLink: peerLink, secondLink: nil) case let .changeStickerPack(_, new): let text:String if let _ = new { - text = tr(.eventLogServiceChangedStickerSet(peer.displayTitle)) + text = L10n.eventLogServiceChangedStickerSet(peer.displayTitle) } else { - text = tr(.eventLogServiceRemovedStickerSet(peer.displayTitle)) + text = L10n.eventLogServiceRemovedStickerSet(peer.displayTitle) } serviceInfo = ServiceTextInfo(text: text, firstLink: peerLink, secondLink: nil) + case let .linkedPeerUpdated(previous, updated): + let text: String + var secondaryLink:(range: String, link: inAppLink)? + if let updated = updated { + if isGroup { + text = L10n.channelEventLogMessageChangedLinkedChannel(peer.displayTitle, updated.displayTitle) + secondaryLink = (range: updated.displayTitle, link: inAppLink.peerInfo(link: "", peerId: updated.id, action:nil, openChat: true, postId: nil, callback: chatInteraction.openInfo)) + } else { + text = L10n.channelEventLogMessageChangedLinkedGroup(peer.displayTitle, updated.displayTitle) + secondaryLink = (range: updated.displayTitle, link: inAppLink.peerInfo(link: "", peerId: updated.id, action:nil, openChat: true, postId: nil, callback: chatInteraction.openInfo)) + } + } else if let previous = previous { + if isGroup { + text = L10n.channelEventLogMessageChangedUnlinkedChannel(peer.displayTitle, previous.displayTitle) + secondaryLink = (range: previous.displayTitle, link: inAppLink.peerInfo(link: "", peerId: previous.id, action:nil, openChat: true, postId: nil, callback: chatInteraction.openInfo)) + } else { + text = L10n.channelEventLogMessageChangedUnlinkedGroup(peer.displayTitle) + } + } else { + text = "" + } + serviceInfo = ServiceTextInfo(text: text, firstLink: peerLink, secondLink: secondaryLink) case let .participantToggleAdmin(prev, new): switch prev.participant { - case let .member(memberId, _, adminInfo: prevAdminInfo, banInfo: _): + case let .member(memberId, _, adminInfo: prevAdminInfo, banInfo: _, rank: prevRank): switch new.participant { - case let .member(_, _, adminInfo: newAdminInfo, banInfo: _): - if let memberPeer = result.peers[memberId] { + case let .member(_, _, adminInfo: newAdminInfo, banInfo: _, rank: newRank): + if let memberPeer = entry.peers[memberId] { let message = NSMutableAttributedString() - var addedRights = newAdminInfo?.rights.flags ?? [] - var removedRights:TelegramChannelAdminRightsFlags = [] + var addedRights = newAdminInfo?.rights.rights ?? [] + var removedRights:TelegramChatAdminRightsFlags = [] if let prevAdminInfo = prevAdminInfo { - addedRights = addedRights.subtracting(prevAdminInfo.rights.flags) - removedRights = prevAdminInfo.rights.flags.subtracting(newAdminInfo?.rights.flags ?? []) + addedRights = addedRights.subtracting(prevAdminInfo.rights.rights) + removedRights = prevAdminInfo.rights.rights.subtracting(newAdminInfo?.rights.rights ?? []) } - _ = message.append(string: prevAdminInfo != nil ? tr(.eventLogServicePromotedChanged(memberPeer.displayTitle, memberPeer.addressName != nil ? "(@\(memberPeer.addressName!))" : "")) : tr(.eventLogServicePromoted(memberPeer.displayTitle, memberPeer.addressName != nil ? "(@\(memberPeer.addressName!))" : "")), color: theme.colors.text) + var justRankUpdated: Bool = false + if prevRank != newRank { + let rank = newRank ?? L10n.chatAdminBadge + if removedRights.isEmpty && addedRights.isEmpty { + _ = message.append(string: L10n.channelEventLogMessageRankName(memberPeer.addressName != nil ? "(@\(memberPeer.addressName!))" : "", rank), color: theme.colors.text) + justRankUpdated = true + } + } + if !justRankUpdated { + _ = message.append(string: prevAdminInfo != nil ? L10n.eventLogServicePromotedChanged1(memberPeer.displayTitle, memberPeer.addressName != nil ? "(@\(memberPeer.addressName!))" : "") : L10n.eventLogServicePromoted1(memberPeer.displayTitle, memberPeer.addressName != nil ? "(@\(memberPeer.addressName!))" : ""), color: theme.colors.text) + + + for right in rightsHelp(isGroup).order { + if addedRights.contains(right) { + _ = message.append(string: "\n+ \(right.localizedString)", color: theme.colors.text) + } + } + if !removedRights.isEmpty { + for right in rightsHelp(isGroup).order { + if removedRights.contains(right) { + _ = message.append(string: "\n- \(right.localizedString)", color: theme.colors.text) + } + } + } + + if prevRank != newRank { + if let rank = newRank, !rank.isEmpty { + _ = message.append(string: "\n" + L10n.channelEventLogServicePlusTitle(rank), color: theme.colors.text) + } else { + _ = message.append(string: "\n" + L10n.channelEventLogServiceMinusTitle, color: theme.colors.text) + } + } + } + - for right in result.rightsHelp.order { - if addedRights.contains(right) { - _ = message.append(string: "\n+ \(right.localizedString)") + message.addAttribute(NSAttributedString.Key.font, value: NSFont.italic(.text), range: message.range) + message.detectLinks(type: [.Mentions, .Hashtags], context: chatInteraction.context, color: theme.colors.link, openInfo: chatInteraction.openInfo, hashtag: nil, command: nil) + + message.add(link: inAppLink.peerInfo(link: "", peerId: memberId, action: nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: message.string.nsstring.range(of: memberPeer.displayTitle)) + self.contentMessageItem = ServiceEventLogMessageContentItem(peer: peer, chatInteraction: chatInteraction, name: TextViewLayout(contentName, maximumNumberOfLines: 1), date: TextViewLayout(date), content: TextViewLayout(message)) + + } + case let .creator(memberId, _, _): + if let memberPeer = entry.peers[memberId] { + let message = NSMutableAttributedString() + + + _ = message.append(string: L10n.channelEventLogMessageTransferedName1(memberPeer.displayTitle, memberPeer.addressName != nil ? "(@\(memberPeer.addressName!))" : ""), color: theme.colors.text) + + + + message.addAttribute(NSAttributedString.Key.font, value: NSFont.italic(.text), range: message.range) + message.detectLinks(type: [.Mentions, .Hashtags], context: chatInteraction.context, color: theme.colors.link, openInfo: chatInteraction.openInfo, hashtag: nil, command: nil) + + message.add(link: inAppLink.peerInfo(link: "", peerId: memberId, action: nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: message.string.nsstring.range(of: memberPeer.displayTitle)) + self.contentMessageItem = ServiceEventLogMessageContentItem(peer: peer, chatInteraction: chatInteraction, name: TextViewLayout(contentName, maximumNumberOfLines: 1), date: TextViewLayout(date), content: TextViewLayout(message)) + + } + } + case let .creator(memberId, prevAdminInfo, prevRank): + switch new.participant { + case .creator(memberId, let newAdminInfo, let newRank): + if let memberPeer = entry.peers[memberId] { + let message = NSMutableAttributedString() + + var addedRights = newAdminInfo?.rights.rights ?? [] + var removedRights:TelegramChatAdminRightsFlags = [] + if let prevAdminInfo = prevAdminInfo { + addedRights = addedRights.subtracting(prevAdminInfo.rights.rights) + removedRights = prevAdminInfo.rights.rights.subtracting(newAdminInfo?.rights.rights ?? []) + } + + var justRankUpdated: Bool = false + + if prevRank != newRank { + let rank = newRank ?? L10n.chatAdminBadge + if removedRights.isEmpty && addedRights.isEmpty { + _ = message.append(string: L10n.channelEventLogMessageRankName(memberPeer.addressName != nil ? "(@\(memberPeer.addressName!))" : "", rank), color: theme.colors.text) + justRankUpdated = true } } - if !removedRights.isEmpty { - for right in result.rightsHelp.order { - if removedRights.contains(right) { - _ = message.append(string: "\n- \(right.localizedString)") + if !justRankUpdated { + _ = message.append(string: prevAdminInfo != nil ? L10n.eventLogServicePromotedChanged1(memberPeer.displayTitle, memberPeer.addressName != nil ? "(@\(memberPeer.addressName!))" : "") : L10n.eventLogServicePromoted1(memberPeer.displayTitle, memberPeer.addressName != nil ? "(@\(memberPeer.addressName!))" : ""), color: theme.colors.text) + + + for right in rightsHelp(isGroup).order { + if addedRights.contains(right) { + _ = message.append(string: "\n+ \(right.localizedString)", color: theme.colors.text) + } + } + if !removedRights.isEmpty { + for right in rightsHelp(isGroup).order { + if removedRights.contains(right) { + _ = message.append(string: "\n- \(right.localizedString)", color: theme.colors.text) + } + } + } + + if prevRank != newRank { + if let rank = newRank, !rank.isEmpty { + _ = message.append(string: "\n" + L10n.channelEventLogServicePlusTitle(rank), color: theme.colors.text) + } else { + _ = message.append(string: "\n" + L10n.channelEventLogServiceMinusTitle, color: theme.colors.text) } } } - message.addAttribute(NSAttributedStringKey.font, value: NSFont.italic(.text), range: message.range) - message.detectLinks(type: [.Mentions, .Hashtags], account: chatInteraction.account, color: theme.colors.link, openInfo: chatInteraction.openInfo, hashtag: nil, command: nil) + message.addAttribute(NSAttributedString.Key.font, value: NSFont.italic(.text), range: message.range) + message.detectLinks(type: [.Mentions, .Hashtags], context: chatInteraction.context, color: theme.colors.link, openInfo: chatInteraction.openInfo, hashtag: nil, command: nil) - message.add(link: inAppLink.peerInfo(peerId: memberId, action: nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: message.string.nsstring.range(of: memberPeer.displayTitle)) + message.add(link: inAppLink.peerInfo(link: "", peerId: memberId, action: nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: message.string.nsstring.range(of: memberPeer.displayTitle)) self.contentMessageItem = ServiceEventLogMessageContentItem(peer: peer, chatInteraction: chatInteraction, name: TextViewLayout(contentName, maximumNumberOfLines: 1), date: TextViewLayout(date), content: TextViewLayout(message)) } default: break } - default: - break } case .deleteMessage: - serviceInfo = ServiceTextInfo(text: tr(.eventLogServiceDeletedMessage(peer.displayTitle)), firstLink: peerLink, secondLink: nil) - case .editMessage: - serviceInfo = ServiceTextInfo(text: tr(.eventLogServiceEditedMessage(peer.displayTitle)), firstLink: peerLink, secondLink: nil) + serviceInfo = ServiceTextInfo(text: L10n.eventLogServiceDeletedMessage(peer.displayTitle), firstLink: peerLink, secondLink: nil) + case let .editMessage(prev, new): + if new.media.first is TelegramMediaImage || new.media.first is TelegramMediaFile { + if !new.media[0].isSemanticallyEqual(to: prev.media[0]) { + serviceInfo = ServiceTextInfo(text: L10n.eventLogServiceEditedMedia(peer.displayTitle), firstLink: peerLink, secondLink: nil) + } else { + serviceInfo = ServiceTextInfo(text: L10n.eventLogServiceEditedCaption(peer.displayTitle), firstLink: peerLink, secondLink: nil) + } + } else if let media = new.media.first as? TelegramMediaAction { + switch media.action { + case let .groupPhoneCall(_, _, _, duration): + if let duration = duration { + let text: String + if new.author?.id == chatInteraction.context.peerId { + text = L10n.chatServiceVoiceChatFinishedYou(autoremoveLocalized(Int(duration))) + } else { + text = L10n.chatServiceVoiceChatFinished(peer.displayTitle, autoremoveLocalized(Int(duration))) + } + serviceInfo = ServiceTextInfo(text: text, firstLink: peerLink, secondLink: nil) + } + default: + serviceInfo = ServiceTextInfo(text: L10n.eventLogServiceEditedMessage(peer.displayTitle), firstLink: peerLink, secondLink: nil) + } + } else { + serviceInfo = ServiceTextInfo(text: L10n.eventLogServiceEditedMessage(peer.displayTitle), firstLink: peerLink, secondLink: nil) + } case let .participantToggleBan(prev, new): switch prev.participant { - case let .member(memberId, _, adminInfo: _, banInfo: prevBanInfo): + case let .member(memberId, _, adminInfo: _, banInfo: prevBanInfo, rank: _): switch new.participant { - case let .member(_, _, adminInfo: _, banInfo: newBanInfo): + case let .member(_, _, adminInfo: _, banInfo: newBanInfo, rank: _): let message = NSMutableAttributedString() - if let memberPeer = result.peers[memberId] { + if let memberPeer = entry.peers[memberId] { var addedRights = newBanInfo?.rights.flags ?? [] - var removedRights:TelegramChannelBannedRightsFlags = [] + var removedRights:TelegramChatBannedRightsFlags = [] if let prevBanInfo = prevBanInfo { addedRights = addedRights.subtracting(prevBanInfo.rights.flags) removedRights = prevBanInfo.rights.flags.subtracting(newBanInfo?.rights.flags ?? []) @@ -371,45 +555,42 @@ class ServiceEventLogItem: TableRowItem { if let _ = prevBanInfo { if let newBanInfo = newBanInfo { - text = newBanInfo.rights.untilDate != .max && newBanInfo.rights.untilDate != 0 ? tr(.eventLogServiceDemotedChangedUntil(memberPeer.displayTitle, memberPeer.addressName != nil ? "(@\(memberPeer.addressName!))" : "", newBanInfo.rights.formattedUntilDate)) : tr(.eventLogServiceDemotedChanged(memberPeer.displayTitle, memberPeer.addressName != nil ? "(@\(memberPeer.addressName!))" : "")) + text = newBanInfo.rights.untilDate != .max && newBanInfo.rights.untilDate != 0 ? L10n.eventLogServiceDemotedChangedUntil1(memberPeer.displayTitle, memberPeer.addressName != nil ? "(@\(memberPeer.addressName!))" : "", newBanInfo.rights.formattedUntilDate) : L10n.eventLogServiceDemotedChanged1(memberPeer.displayTitle, memberPeer.addressName != nil ? "(@\(memberPeer.addressName!))" : "") } else { - text = tr(.eventLogServiceDemotedChanged(memberPeer.displayTitle, memberPeer.addressName != nil ? "(@\(memberPeer.addressName!))" : "")) + text = L10n.eventLogServiceDemotedChanged1(memberPeer.displayTitle, memberPeer.addressName != nil ? "(@\(memberPeer.addressName!))" : "") } } else { if let newBanInfo = newBanInfo { - text = newBanInfo.rights.untilDate != .max && newBanInfo.rights.untilDate != 0 ? tr(.eventLogServiceDemotedUntil(memberPeer.displayTitle, memberPeer.addressName != nil ? "(@\(memberPeer.addressName!))" : "", newBanInfo.rights.formattedUntilDate)) : tr(.eventLogServiceDemoted(memberPeer.displayTitle, memberPeer.addressName != nil ? "(@\(memberPeer.addressName!))" : "")) + text = newBanInfo.rights.untilDate != .max && newBanInfo.rights.untilDate != 0 ? L10n.eventLogServiceDemotedUntil1(memberPeer.displayTitle, memberPeer.addressName != nil ? "(@\(memberPeer.addressName!))" : "", newBanInfo.rights.formattedUntilDate) : L10n.eventLogServiceDemoted1(memberPeer.displayTitle, memberPeer.addressName != nil ? "(@\(memberPeer.addressName!))" : "") } else { - text = tr(.eventLogServiceDemotedChanged(memberPeer.displayTitle, memberPeer.addressName != nil ? "(@\(memberPeer.addressName!))" : "")) + text = L10n.eventLogServiceDemotedChanged1(memberPeer.displayTitle, memberPeer.addressName != nil ? "(@\(memberPeer.addressName!))" : "") } } } else { - text = tr(.eventLogServiceBanned(memberPeer.displayTitle, memberPeer.addressName != nil ? "(@\(memberPeer.addressName!))" : "")) + text = L10n.eventLogServiceBanned1(memberPeer.displayTitle, memberPeer.addressName != nil ? "(@\(memberPeer.addressName!))" : "") } _ = message.append(string: text, color: theme.colors.text) + if !addedRights.contains(.banReadMessages) { - for right in result.banHelp { + for right in banHelp { if addedRights.contains(right) { - _ = message.append(string: "\n- \(right.localizedString)") + _ = message.append(string: "\n- \(right.localizedString)", color: theme.colors.text) } } if !removedRights.isEmpty { - for right in result.banHelp { + for right in banHelp { if removedRights.contains(right) { - _ = message.append(string: "\n+ \(right.localizedString)") + _ = message.append(string: "\n+ \(right.localizedString)", color: theme.colors.text) } } } } + message.addAttribute(NSAttributedString.Key.font, value: NSFont.italic(.text), range: message.range) + message.detectLinks(type: [.Mentions, .Hashtags], context: chatInteraction.context, color: theme.colors.link, openInfo: chatInteraction.openInfo, hashtag: nil, command: nil) - - - - message.addAttribute(NSAttributedStringKey.font, value: NSFont.italic(.text), range: message.range) - message.detectLinks(type: [.Mentions, .Hashtags], account: chatInteraction.account, color: theme.colors.link, openInfo: chatInteraction.openInfo, hashtag: nil, command: nil) - - message.add(link: inAppLink.peerInfo(peerId: memberId, action: nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: message.string.nsstring.range(of: memberPeer.displayTitle)) + message.add(link: inAppLink.peerInfo(link: "", peerId: memberId, action: nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: message.string.nsstring.range(of: memberPeer.displayTitle)) self.contentMessageItem = ServiceEventLogMessageContentItem(peer: peer, chatInteraction: chatInteraction, name: TextViewLayout(contentName, maximumNumberOfLines: 1), date: TextViewLayout(date), content: TextViewLayout(message)) } @@ -420,40 +601,111 @@ class ServiceEventLogItem: TableRowItem { break } case .updatePinned(let message): - serviceInfo = ServiceTextInfo(text: message != nil ? tr(.eventLogServiceUpdatePinned(peer.displayTitle)) : tr(.eventLogServiceRemovePinned(peer.displayTitle)), firstLink: peerLink, secondLink: nil) + serviceInfo = ServiceTextInfo(text: message != nil ? L10n.eventLogServiceUpdatePinned(peer.displayTitle) : L10n.eventLogServiceRemovePinned(peer.displayTitle), firstLink: peerLink, secondLink: nil) case let .toggleInvites(value): let text:String if value { - text = tr(.groupEventLogServiceEnableInvites(peer.displayTitle)) + text = L10n.groupEventLogServiceEnableInvites(peer.displayTitle) } else { - text = tr(.groupEventLogServiceDisableInvites(peer.displayTitle)) + text = L10n.groupEventLogServiceDisableInvites(peer.displayTitle) } serviceInfo = ServiceTextInfo(text: text, firstLink: peerLink, secondLink: nil) case let .toggleSignatures(value): let text:String if value { - text = tr(.channelEventLogServiceEnableSignatures(peer.displayTitle)) + text = L10n.channelEventLogServiceEnableSignatures(peer.displayTitle) } else { - text = tr(.channelEventLogServiceDisableSignatures(peer.displayTitle)) + text = L10n.channelEventLogServiceDisableSignatures(peer.displayTitle) } serviceInfo = ServiceTextInfo(text: text, firstLink: peerLink, secondLink: nil) case let .changePhoto(_, new): let text:String - if new.isEmpty { - text = result.isGroup ? tr(.groupEventLogServicePhotoRemoved(peer.displayTitle)) : tr(.channelEventLogServicePhotoRemoved(peer.displayTitle)) + if new.0.isEmpty { + text = isGroup ? tr(L10n.groupEventLogServicePhotoRemoved(peer.displayTitle)) : tr(L10n.channelEventLogServicePhotoRemoved(peer.displayTitle)) } else { - text = result.isGroup ? tr(.groupEventLogServicePhotoUpdated(peer.displayTitle)) : tr(.channelEventLogServicePhotoUpdated(peer.displayTitle)) + text = isGroup ? tr(L10n.groupEventLogServicePhotoUpdated(peer.displayTitle)) : tr(L10n.channelEventLogServicePhotoUpdated(peer.displayTitle)) let size = NSMakeSize(70, 70) imageArguments = TransformImageArguments(corners: ImageCorners(radius: size.width / 2), imageSize: size, boundingSize: size, intrinsicInsets: NSEdgeInsets()) - image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: new) + image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: new.0, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) } serviceInfo = ServiceTextInfo(text: text, firstLink: peerLink, secondLink: nil) case .participantLeave: - let text:String = result.isGroup ? tr(.groupEventLogServiceUpdateLeft(peer.displayTitle)) : tr(.channelEventLogServiceUpdateLeft(peer.displayTitle)) + let text:String = isGroup ? tr(L10n.groupEventLogServiceUpdateLeft(peer.displayTitle)) : tr(L10n.channelEventLogServiceUpdateLeft(peer.displayTitle)) serviceInfo = ServiceTextInfo(text: text, firstLink: peerLink, secondLink: nil) case .participantJoin: - let text:String = result.isGroup ? tr(.groupEventLogServiceUpdateJoin(peer.displayTitle)) : tr(.channelEventLogServiceUpdateJoin(peer.displayTitle)) + let text:String = isGroup ? L10n.groupEventLogServiceUpdateJoin(peer.displayTitle) : L10n.channelEventLogServiceUpdateJoin(peer.displayTitle) + serviceInfo = ServiceTextInfo(text: text, firstLink: peerLink, secondLink: nil) + case let .updateSlowmode(_, newValue): + let text:String = newValue == nil || newValue == 0 ? L10n.channelEventLogServiceDisabledSlowMode(peer.displayTitle) : L10n.channelEventLogServiceSetSlowMode1(peer.displayTitle, autoremoveLocalized(Int(newValue!))) + serviceInfo = ServiceTextInfo(text: text, firstLink: peerLink, secondLink: nil) + case let .updateDefaultBannedRights(prev, new): + let message = NSMutableAttributedString() + _ = message.append(string: L10n.eventLogServiceChangedDefaultsRights, color: theme.colors.text) + var addedRights = new.flags + var removedRights:TelegramChatBannedRightsFlags = [] + addedRights = addedRights.subtracting(prev.flags) + removedRights = prev.flags.subtracting(new.flags) + + for right in banHelp { + if addedRights.contains(right) { + _ = message.append(string: "\n- \(right.localizedString)", color: theme.colors.text) + } + } + if !removedRights.isEmpty { + for right in banHelp { + if removedRights.contains(right) { + _ = message.append(string: "\n+ \(right.localizedString)", color: theme.colors.text) + } + } + } + + message.addAttribute(NSAttributedString.Key.font, value: NSFont.italic(.text), range: message.range) + self.contentMessageItem = ServiceEventLogMessageContentItem(peer: peer, chatInteraction: chatInteraction, name: TextViewLayout(contentName, maximumNumberOfLines: 1), date: TextViewLayout(date), content: TextViewLayout(message)) + case .startGroupCall: + let text = L10n.channelAdminLogStartedVoiceChat(peer.displayTitle) + serviceInfo = ServiceTextInfo(text: text, firstLink: peerLink, secondLink: nil) + case .endGroupCall: + let text = L10n.channelAdminLogEndedVoiceChat(peer.displayTitle) + serviceInfo = ServiceTextInfo(text: text, firstLink: peerLink, secondLink: nil) + case let .groupCallUpdateParticipantMuteStatus(peerId, isMuted): + if let secondary = entry.peers[peerId] { + let secondaryLink = (range: secondary.displayTitle, link: inAppLink.peerInfo(link: "", peerId: secondary.id, action:nil, openChat: true, postId: nil, callback: chatInteraction.openInfo)) + let text: String + if isMuted { + text = L10n.channelAdminLogMutedParticipant(peer.displayTitle, secondary.displayTitle) + } else { + text = L10n.channelAdminLogUnmutedMutedParticipant(peer.displayTitle, secondary.displayTitle) + } + serviceInfo = ServiceTextInfo(text: text, firstLink: peerLink, secondLink: secondaryLink) + } + case let .updateGroupCallSettings(joinMuted): + let text: String + if joinMuted { + text = L10n.channelAdminLogMutedNewMembers(peer.displayTitle) + } else { + text = L10n.channelAdminLogAllowedNewMembersToSpeak(peer.displayTitle) + } + serviceInfo = ServiceTextInfo(text: text, firstLink: peerLink, secondLink: nil) + case let .deleteExportedInvitation(invite): + let text = L10n.channelAdminLogDeletedInviteLink(peer.displayTitle, invite.link.replacingOccurrences(of: "https://", with: "")) + serviceInfo = ServiceTextInfo(text: text, firstLink: peerLink, secondLink: nil) + case let .editExportedInvitation(_, invite): + let text = L10n.channelAdminLogEditedInviteLink(peer.displayTitle, invite.link.replacingOccurrences(of: "https://", with: "")) + serviceInfo = ServiceTextInfo(text: text, firstLink: peerLink, secondLink: nil) + case let .revokeExportedInvitation(invite): + let text = L10n.channelAdminLogRevokedInviteLink(peer.displayTitle, invite.link.replacingOccurrences(of: "https://", with: "")) + serviceInfo = ServiceTextInfo(text: text, firstLink: peerLink, secondLink: nil) + case let .participantJoinedViaInvite(invite): + let text = L10n.channelAdminLogJoinedViaInviteLink(peer.displayTitle, invite.link.replacingOccurrences(of: "https://", with: "")) + serviceInfo = ServiceTextInfo(text: text, firstLink: peerLink, secondLink: nil) + case let .changeHistoryTTL(_, updatedValue): + let text: String + if let updatedValue = updatedValue, updatedValue > 0 { + text = L10n.channelAdminLogMessageChangedAutoremoveTimeoutSet(peer.displayTitle, timeIntervalString(Int(updatedValue))) + } else { + text = L10n.channelAdminLogMessageChangedAutoremoveTimeoutRemove(peer.displayTitle) + } serviceInfo = ServiceTextInfo(text: text, firstLink: peerLink, secondLink: nil) default: break @@ -464,11 +716,11 @@ class ServiceEventLogItem: TableRowItem { let range = attributedString.string.nsstring.range(of: serviceInfo.firstLink.range) attributedString.add(link: serviceInfo.firstLink.link, for: range) - attributedString.addAttribute(NSAttributedStringKey.font, value: NSFont.medium(.text), range: range) + attributedString.addAttribute(NSAttributedString.Key.font, value: NSFont.medium(.text), range: range) if let second = serviceInfo.secondLink { let range = attributedString.string.nsstring.range(of: second.range) attributedString.add(link: second.link, for: range) - attributedString.addAttribute(NSAttributedStringKey.font, value: NSFont.medium(.text), range: range) + attributedString.addAttribute(NSAttributedString.Key.font, value: NSFont.medium(.text), range: range) } } @@ -478,15 +730,15 @@ class ServiceEventLogItem: TableRowItem { let newContentAttributed = NSMutableAttributedString() _ = newContentAttributed.append(string: changedInfo.new, color: theme.colors.text, font: .normal(.text)) - newContentAttributed.detectLinks(type: [.Mentions, .Hashtags], account: chatInteraction.account, color: theme.colors.link, openInfo: chatInteraction.openInfo, hashtag: nil, command: nil) + newContentAttributed.detectLinks(type: [.Mentions, .Hashtags], context: chatInteraction.context, color: theme.colors.link, openInfo: chatInteraction.openInfo, hashtag: nil, command: nil) let prevContentAttributed = NSMutableAttributedString() - _ = prevContentAttributed.append(string: changedInfo.prev, color: theme.colors.text, font: .normal(.custom(12.5))) - prevContentAttributed.detectLinks(type: [.Mentions, .Hashtags], account: chatInteraction.account, color: theme.colors.link, openInfo: chatInteraction.openInfo, hashtag: nil, command: nil) + _ = prevContentAttributed.append(string: changedInfo.prev, color: theme.colors.text, font: .normal(12.5)) + prevContentAttributed.detectLinks(type: [.Mentions, .Hashtags], context: chatInteraction.context, color: theme.colors.link, openInfo: chatInteraction.openInfo, hashtag: nil, command: nil) let panel:ServiceEventLogMessagePanel? if let _ = changedInfo.panelText { - panel = ServiceEventLogMessagePanel(header: TextViewLayout(.initialize(string: changedInfo.panelText, color: theme.colors.blueUI, font: .medium(.text)), maximumNumberOfLines: 1), content: TextViewLayout(prevContentAttributed)) + panel = ServiceEventLogMessagePanel(header: TextViewLayout(.initialize(string: changedInfo.panelText, color: theme.colors.accent, font: .medium(.text)), maximumNumberOfLines: 1), content: TextViewLayout(prevContentAttributed)) } else { panel = nil } @@ -500,13 +752,14 @@ class ServiceEventLogItem: TableRowItem { } override var stableId: AnyHashable { - return event.id + return entry.event.id } override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) textLayout.measure(width: width - (defaultContentInset.left + defaultContentInset.right)) contentMessageItem?.measure(width - (defaultContentInset.left + defaultContentInset.right)) - return super.makeSize(width, oldWidth: oldWidth) + return success } } @@ -520,6 +773,10 @@ private class ServiceEventLogRowView : TableRowView { textView.isSelectable = false } + override var backdorColor: NSColor { + return theme.colors.chatBackground + } + override func updateColors() { super.updateColors() textView.backgroundColor = backdorColor @@ -559,7 +816,7 @@ private class ServiceEventLogRowView : TableRowView { if messageContent == nil { messageContent = ServiceEventLogMessageContainerView(frame: NSZeroRect) } - messageContent?.update(with: content, account: item.chatInteraction.account) + messageContent?.update(with: content, account: item.chatInteraction.context.account) messageContent?.setFrameSize(frame.width - (defaultContentInset.left + defaultContentInset.right), content.height) addSubview(messageContent!) } else { @@ -573,7 +830,8 @@ private class ServiceEventLogRowView : TableRowView { imageView?.setFrameSize(NSMakeSize(70, 70)) self.addSubview(imageView!) } - imageView?.setSignal(account: item.chatInteraction.account, signal: chatMessagePhoto(account: item.chatInteraction.account, photo: image, toRepresentationSize:NSMakeSize(100,100), scale: backingScaleFactor)) + + imageView?.setSignal(chatMessagePhoto(account: item.chatInteraction.context.account, imageReference: ImageMediaReference.standalone(media: image), toRepresentationSize:NSMakeSize(100,100), scale: backingScaleFactor)) } else { imageView?.removeFromSuperview() imageView = nil @@ -601,9 +859,9 @@ class ChannelEventLogEditedPanelItem : TableRowItem { init(_ initialSize: NSSize, previous:Message, item:ChatRowItem) { self.previous = previous self.associatedItem = item - let header = TextViewLayout(.initialize(string: tr(.channelEventLogOriginalMessage), color: theme.colors.blueUI, font: .medium(.text)), maximumNumberOfLines: 1) + let header = TextViewLayout(.initialize(string: tr(L10n.channelEventLogOriginalMessage), color: theme.colors.accent, font: .medium(.text)), maximumNumberOfLines: 1) - let text = TextViewLayout(.initialize(string: previous.text.isEmpty ? tr(.channelEventLogEmpty) : previous.text, color: theme.colors.text, font: .italic(.text))) + let text = TextViewLayout(.initialize(string: previous.text.isEmpty ? tr(L10n.channelEventLogEmpty) : previous.text, color: theme.colors.text, font: .italic(.text))) panel = ServiceEventLogMessagePanel(header: header, content: text) super.init(initialSize) @@ -614,14 +872,15 @@ class ChannelEventLogEditedPanelItem : TableRowItem { } override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) _ = associatedItem?.makeSize(width, oldWidth: oldWidth) if let item = associatedItem { - panel.content.measure(width: item.blockSize.width - 8) - panel.header.measure(width: item.blockSize.width - 8) + panel.content.measure(width: item.blockWidth - 8) + panel.header.measure(width: item.blockWidth - 8) } - return super.makeSize(width, oldWidth: oldWidth) + return success } override var height: CGFloat { @@ -640,6 +899,10 @@ class ChannelEventLogEditedPanelView : TableRowView { addSubview(panel) } + override var backdorColor: NSColor { + return theme.colors.chatBackground + } + override func updateColors() { super.updateColors() panel.backgroundColor = backdorColor @@ -650,7 +913,7 @@ class ChannelEventLogEditedPanelView : TableRowView { super.set(item: item) if let item = item as? ChannelEventLogEditedPanelItem, let associatedItem = item.associatedItem { panel.update(with: item.panel) - panel.setFrameSize(associatedItem.blockSize.width, item.panel.height) + panel.setFrameSize(associatedItem.blockWidth, item.panel.height) panel.setFrameOrigin(associatedItem.contentOffset.x, 0) } } diff --git a/Telegram-Mac/ChannelInfoEntries.swift b/Telegram-Mac/ChannelInfoEntries.swift index e7cb9e2b4a..d96a2db4f5 100644 --- a/Telegram-Mac/ChannelInfoEntries.swift +++ b/Telegram-Mac/ChannelInfoEntries.swift @@ -7,10 +7,12 @@ // import Cocoa -import PostboxMac -import TelegramCoreMac +import Postbox +import TelegramCore + import TGUIKit -import SwiftSignalKitMac +import SwiftSignalKit + struct ChannelInfoEditingState: Equatable { let editingName: String? @@ -118,6 +120,18 @@ private func valuesRequiringUpdate(state: ChannelInfoState, view: PeerView) -> ( class ChannelInfoArguments : PeerInfoArguments { + + private var _linksManager:InviteLinkPeerManager? + var linksManager: InviteLinkPeerManager { + if let _linksManager = _linksManager { + return _linksManager + } else { + _linksManager = InviteLinkPeerManager(context: context, peerId: peerId) + _linksManager!.loadNext() + return _linksManager! + } + } + private let reportPeerDisposable = MetaDisposable() private let updatePeerNameDisposable = MetaDisposable() private let toggleSignaturesDisposable = MetaDisposable() @@ -134,9 +148,9 @@ class ChannelInfoArguments : PeerInfoArguments { } } - override func updateEditable(_ editable: Bool, peerView: PeerView) { + override func updateEditable(_ editable:Bool, peerView:PeerView, controller: PeerInfoController) -> Bool { - let account = self.account + let context = self.context let peerId = self.peerId let updateState:((ChannelInfoState)->ChannelInfoState)->Void = { [weak self] f in self?.updateState(f) @@ -152,34 +166,40 @@ class ChannelInfoArguments : PeerInfoArguments { var updateValues: (title: String?, description: String?) = (nil, nil) updateState { state in updateValues = valuesRequiringUpdate(state: state, view: peerView) + return state + } + + if let titleValue = updateValues.title, titleValue.isEmpty { + controller.genericView.item(stableId: IntPeerInfoEntryStableId(value: 1).hashValue)?.view?.shakeView() + return false + } + + updateState { state in if updateValues.0 != nil || updateValues.1 != nil { return state.withUpdatedSavingData(true) } else { return state.withUpdatedEditingState(nil) } } - - - - let updateTitle: Signal + let updateTitle: Signal if let titleValue = updateValues.title { - updateTitle = updatePeerTitle(account: account, peerId: peerId, title: titleValue) - |> mapError { _ in return Void() } + updateTitle = context.engine.peers.updatePeerTitle(peerId: peerId, title: titleValue) + |> `catch` { _ in return .complete() } } else { updateTitle = .complete() } - let updateDescription: Signal + let updateDescription: Signal if let descriptionValue = updateValues.description { - updateDescription = updatePeerDescription(account: account, peerId: peerId, description: descriptionValue.isEmpty ? nil : descriptionValue) - |> mapError { _ in return Void() } + updateDescription = context.engine.peers.updatePeerDescription(peerId: peerId, description: descriptionValue.isEmpty ? nil : descriptionValue) + |> `catch` { _ in return .complete() } } else { updateDescription = .complete() } let signal = combineLatest(updateTitle, updateDescription) - updatePeerNameDisposable.set(showModalProgress(signal: (signal |> deliverOnMainQueue), for: mainWindow).start(error: { _ in + updatePeerNameDisposable.set(showModalProgress(signal: (signal |> deliverOnMainQueue), for: context.window).start(error: { _ in updateState { state in return state.withUpdatedSavingData(false) } @@ -189,28 +209,246 @@ class ChannelInfoArguments : PeerInfoArguments { } })) } - - + return true } func visibilitySetup() { - pushViewController(ChannelVisibilityController(account: account, peerId: peerId)) + let setup = ChannelVisibilityController(context, peerId: peerId, isChannel: true) + _ = (setup.onComplete.get() |> deliverOnMainQueue).start(next: { [weak self] _ in + self?.pullNavigation()?.back() + }) + pushViewController(setup) + } + func openInviteLinks() { + pushViewController(InviteLinksController(context: context, peerId: peerId, manager: linksManager)) + } + + func setupDiscussion() { + _ = (self.context.account.postbox.loadedPeerWithId(self.peerId) |> deliverOnMainQueue).start(next: { [weak self] peer in + if let `self` = self { + self.pushViewController(ChannelDiscussionSetupController(context: self.context, peer: peer)) + } + }) } func toggleSignatures( _ enabled: Bool) -> Void { - toggleSignaturesDisposable.set(toggleShouldChannelMessagesSignatures(account: account, peerId: peerId, enabled: enabled).start()) + toggleSignaturesDisposable.set(context.engine.peers.toggleShouldChannelMessagesSignatures(peerId: peerId, enabled: enabled).start()) } func members() -> Void { - pushViewController(ChannelMembersViewController(account: account, peerId: peerId)) + pushViewController(ChannelMembersViewController(context, peerId: peerId)) } func admins() -> Void { - pushViewController(ChannelAdminsViewController(account: account, peerId: peerId)) + pushViewController(ChannelAdminsViewController(context, peerId: peerId)) } + func makeVoiceChat(_ current: CachedChannelData.ActiveCall?, callJoinPeerId: PeerId?) { + let context = self.context + let peerId = self.peerId + if let activeCall = current { + let join:(PeerId, Date?)->Void = { joinAs, _ in + _ = showModalProgress(signal: requestOrJoinGroupCall(context: context, peerId: peerId, joinAs: joinAs, initialCall: activeCall, initialInfo: nil, joinHash: nil), for: context.window).start(next: { result in + switch result { + case let .samePeer(callContext): + applyGroupCallResult(context.sharedContext, callContext) + case let .success(callContext): + applyGroupCallResult(context.sharedContext, callContext) + default: + alert(for: context.window, info: L10n.errorAnError) + } + }) + } + if let callJoinPeerId = callJoinPeerId { + join(callJoinPeerId, nil) + } else { + selectGroupCallJoiner(context: context, peerId: peerId, completion: join) + } + } else { + createVoiceChat(context: context, peerId: peerId, canBeScheduled: true) + } + } + func blocked() -> Void { - pushViewController(ChannelBlacklistViewController(account: account, peerId: peerId)) + pushViewController(ChannelBlacklistViewController(context, peerId: peerId)) + } + + func updateChannelPhoto(_ custom: NSImage?, control: Control?) { + + let context = self.context + + let updatePhoto:(NSImage) -> Void = { image in + _ = (putToTemp(image: image, compress: true) |> deliverOnMainQueue).start(next: { path in + let controller = EditImageModalController(URL(fileURLWithPath: path), settings: .disableSizes(dimensions: .square)) + showModal(with: controller, for: mainWindow, animationType: .scaleCenter) + _ = controller.result.start(next: { [weak self] url, _ in + self?.updatePhoto(url.path) + }) + controller.onClose = { + removeFile(at: path) + } + }) + } + if let image = custom { + updatePhoto(image) + } else { + let context = self.context + let updateVideo = self.updateVideo + + + var items:[SPopoverItem] = [] + + items.append(.init(L10n.editAvatarPhotoOrVideo, { + filePanel(with: photoExts + videoExts, allowMultiple: false, canChooseDirectories: false, for: context.window, completion: { paths in + if let path = paths?.first, let image = NSImage(contentsOfFile: path) { + updatePhoto(image) + } else if let path = paths?.first { + selectVideoAvatar(context: context, path: path, localize: L10n.videoAvatarChooseDescChannel, signal: { signal in + updateVideo(signal) + }) + } + }) + })) + + items.append(.init(L10n.editAvatarStickerOrGif, { [weak control] in + let controller = EntertainmentViewController(size: NSMakeSize(350, 350), context: context, mode: .selectAvatar) + controller._frameRect = NSMakeRect(0, 0, 350, 400) + + let interactions = ChatInteraction(chatLocation: .peer(context.peerId), context: context) + + let runConvertor:(MediaObjectToAvatar)->Void = { [weak control] convertor in + _ = showModalProgress(signal: convertor.start(), for: context.window).start(next: { [weak control] result in + switch result { + case let .image(image): + updatePhoto(image) + case let .video(path): + selectVideoAvatar(context: context, path: path, localize: L10n.videoAvatarChooseDescChannel, quality: AVAssetExportPresetHighestQuality, signal: { signal in + updateVideo(signal) + }) + } + control?.contextObject = nil + }) + control?.contextObject = convertor + } + + interactions.sendAppFile = { file, _, _ in + let object: MediaObjectToAvatar.Object + if file.isAnimatedSticker { + object = .animated(file) + } else if file.isSticker { + object = .sticker(file) + } else { + object = .gif(file) + } + let convertor = MediaObjectToAvatar(context: context, object: object) + runConvertor(convertor) + } + interactions.sendInlineResult = { [] collection, result in + switch result { + case let .internalReference(reference): + if let file = reference.file { + let convertor = MediaObjectToAvatar(context: context, object: .gif(file)) + runConvertor(convertor) + } + case .externalReference: + break + } + } + + control?.contextObject = interactions + controller.update(with: interactions) + if let control = control { + showPopover(for: control, with: controller, edge: .maxY, inset: NSMakePoint(0, -110), static: true) + } + })) + + if let control = control { + showPopover(for: control, with: SPopoverViewController(items: items), edge: .maxY, inset: NSMakePoint(0, -60)) + } else { + filePanel(with: photoExts + videoExts, allowMultiple: false, canChooseDirectories: false, for: context.window, completion: { paths in + if let path = paths?.first, let image = NSImage(contentsOfFile: path) { + updatePhoto(image) + } else if let path = paths?.first { + selectVideoAvatar(context: context, path: path, localize: L10n.videoAvatarChooseDescChannel, signal: { signal in + updateVideo(signal) + }) + } + }) + } + } + } + + func updateVideo(_ signal:Signal) -> Void { + + let updateState:((ChannelInfoState)->ChannelInfoState)->Void = { [weak self] f in + self?.updateState(f) + } + + let cancel = { [weak self] in + self?.updatePhotoDisposable.set(nil) + updateState { state -> ChannelInfoState in + return state.withoutUpdatingPhotoState() + } + } + + let context = self.context + let peerId = self.peerId + + + let updateSignal: Signal = signal + |> mapError { _ in return UploadPeerPhotoError.generic } + |> mapToSignal { state in + switch state { + case .error: + return .fail(.generic) + case let .start(path): + updateState { (state) -> ChannelInfoState in + return state.withUpdatedUpdatingPhotoState { previous -> PeerInfoUpdatingPhotoState? in + return PeerInfoUpdatingPhotoState(progress: 0, image: NSImage(contentsOfFile: path)?._cgImage, cancel: cancel) + } + } + return .next(.progress(0)) + case let .progress(value): + return .next(.progress(value * 0.2)) + case let .complete(thumb, video, keyFrame): + let (thumbResource, videoResource) = (LocalFileReferenceMediaResource(localFilePath: thumb, randomId: arc4random64(), isUniquelyReferencedTemporaryFile: true), + LocalFileReferenceMediaResource(localFilePath: video, randomId: arc4random64(), isUniquelyReferencedTemporaryFile: true)) + + return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: thumbResource), video: context.engine.peers.uploadedPeerVideo(resource: videoResource) |> map(Optional.init), videoStartTimestamp: keyFrame, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: context.account.postbox, resource: resource, representations: representations) + }) |> map { result in + switch result { + case let .progress(current): + return .progress(0.2 + (current * 0.8)) + default: + return result + } + } + } + } + + updatePhotoDisposable.set((updateSignal |> deliverOnMainQueue).start(next: { status in + updateState { state -> ChannelInfoState in + switch status { + case .complete: + return state.withoutUpdatingPhotoState() + case let .progress(progress): + return state.withUpdatedUpdatingPhotoState { previous -> PeerInfoUpdatingPhotoState? in + return previous?.withUpdatedProgress(progress) + } + } + } + }, error: { error in + updateState { (state) -> ChannelInfoState in + return state.withoutUpdatingPhotoState() + } + }, completed: { + updateState { (state) -> ChannelInfoState in + return state.withoutUpdatingPhotoState() + } + })) + + } func updatePhoto(_ path:String) -> Void { @@ -220,38 +458,29 @@ class ChannelInfoArguments : PeerInfoArguments { } let cancel = { [weak self] in - self?.updatePhotoDisposable.dispose() + self?.updatePhotoDisposable.set(nil) updateState { state -> ChannelInfoState in return state.withoutUpdatingPhotoState() } } - let account = self.account + let context = self.context let peerId = self.peerId - /* - filethumb(with: URL(fileURLWithPath: path), account: account, scale: System.backingScale) |> mapToSignal { res -> Signal in - guard let image = NSImage(contentsOf: URL(fileURLWithPath: path)) else { - return .complete() - } - let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: image.size, boundingSize: NSMakeSize(640, 640), intrinsicInsets: NSEdgeInsets()) - if let image = res(arguments)?.generateImage() { - return putToTemp(image: NSImage(cgImage: image, size: image.backingSize)) - } - return .complete() - } - */ - let updateSignal = Signal.single(path) |> map { path -> TelegramMediaResource in + + let updateSignal = Signal.single(path) |> map { path -> TelegramMediaResource in return LocalFileReferenceMediaResource(localFilePath: path, randomId: arc4random64()) } |> beforeNext { resource in updateState { (state) -> ChannelInfoState in return state.withUpdatedUpdatingPhotoState { previous -> PeerInfoUpdatingPhotoState? in - return PeerInfoUpdatingPhotoState(progress: 0, cancel: cancel) + return PeerInfoUpdatingPhotoState(progress: 0, image: NSImage(contentsOfFile: path)?.cgImage(forProposedRect: nil, context: nil, hints: nil), cancel: cancel) } } } |> mapError {_ in return UploadPeerPhotoError.generic} |> mapToSignal { resource -> Signal in - return updatePeerPhoto(account: account, peerId: peerId, resource: resource) + return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: resource), mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: context.account.postbox, resource: resource, representations: representations) + }) } @@ -279,16 +508,51 @@ class ChannelInfoArguments : PeerInfoArguments { } + func stats(_ datacenterId: Int32) { + self.pushViewController(ChannelStatsViewController(context, peerId: peerId, datacenterId: datacenterId)) + } + func share() { + let peer = context.account.postbox.peerView(id: peerId) |> take(1) |> deliverOnMainQueue + let context = self.context + + _ = peer.start(next: { peerView in + if let peer = peerViewMainPeer(peerView) { + var link: String = "https://t.me/\(peer.id.id)" + if let address = peer.addressName, !address.isEmpty { + link = "https://t.me/\(address)" + } else if let cachedData = peerView.cachedData as? CachedChannelData, let invitation = cachedData.exportedInvitation { + link = invitation.link + } + showModal(with: ShareModalController(ShareLinkObject(context, link: link)), for: context.window) + } + + }) + + } + func report() -> Void { - let account = self.account + let context = self.context let peerId = self.peerId - - let report = reportReasonSelector() |> mapToSignal { reason -> Signal in - return showModalProgress(signal: reportPeer(account: account, peerId: peerId, reason: reason), for: mainWindow) + + let report = reportReasonSelector(context: context) |> map { value -> (ChatController?, ReportReasonValue) in + switch value.reason { + case .fake: + return (nil, value) + default: + return (ChatController(context: context, chatLocation: .peer(peerId), initialAction: .selectToReport(reason: value)), value) + } } |> deliverOnMainQueue - - reportPeerDisposable.set(report.start(next: { [weak self] in - self?.pullNavigation()?.controller.show(toaster: ControllerToaster(text: tr(.peerInfoChannelReported))) + + reportPeerDisposable.set(report.start(next: { [weak self] controller, value in + if let controller = controller { + self?.pullNavigation()?.push(controller) + } else { + showModal(with: ReportDetailsController(context: context, reason: value, updated: { value in + _ = showModalProgress(signal: context.engine.peers.reportPeer(peerId: peerId, reason: value.reason, message: value.comment), for: context.window).start(completed: { + showModalText(for: context.window, text: L10n.peerInfoChannelReported) + }) + }), for: context.window) + } })) } @@ -310,6 +574,7 @@ class ChannelInfoArguments : PeerInfoArguments { } } } + deinit { reportPeerDisposable.dispose() @@ -320,25 +585,52 @@ class ChannelInfoArguments : PeerInfoArguments { } enum ChannelInfoEntry: PeerInfoEntry { - case info(sectionId:Int, peerView: PeerView, editable:Bool, updatingPhotoState:PeerInfoUpdatingPhotoState?) - case about(sectionId:Int, text: String) - case userName(sectionId:Int, value: String) - case setPhoto(sectionId:Int) - case sharedMedia(sectionId:Int) - case notifications(sectionId:Int, settings: PeerNotificationSettings?) - case admins(sectionId:Int, count:Int32?) - case blocked(sectionId:Int, count:Int32?) - case members(sectionId:Int, count:Int32?) - case link(sectionId:Int, addressName:String) - case aboutInput(sectionId:Int, description:String) - case aboutDesc(sectionId:Int) - case signMessages(sectionId:Int, sign:Bool) - case signDesc(sectionId:Int) - case report(sectionId:Int) - case leave(sectionId:Int, isCreator: Bool) + case info(sectionId: ChannelInfoSection, peerView: PeerView, editable:Bool, updatingPhotoState:PeerInfoUpdatingPhotoState?, viewType: GeneralViewType) + case scam(sectionId: ChannelInfoSection, title: String, text: String, viewType: GeneralViewType) + case about(sectionId: ChannelInfoSection, text: String, viewType: GeneralViewType) + case userName(sectionId: ChannelInfoSection, value: String, viewType: GeneralViewType) + case setTitle(sectionId: ChannelInfoSection, text: String, viewType: GeneralViewType) + case admins(sectionId: ChannelInfoSection, count:Int32?, viewType: GeneralViewType) + case blocked(sectionId: ChannelInfoSection, count:Int32?, viewType: GeneralViewType) + case members(sectionId: ChannelInfoSection, count:Int32?, viewType: GeneralViewType) + case link(sectionId: ChannelInfoSection, addressName:String, viewType: GeneralViewType) + case inviteLinks(section: ChannelInfoSection, count: Int32, viewType: GeneralViewType) + case discussion(sectionId: ChannelInfoSection, group: Peer?, participantsCount: Int32?, viewType: GeneralViewType) + case discussionDesc(sectionId: ChannelInfoSection, viewType: GeneralViewType) + case aboutInput(sectionId: ChannelInfoSection, description:String, viewType: GeneralViewType) + case aboutDesc(sectionId: ChannelInfoSection, viewType: GeneralViewType) + case signMessages(sectionId: ChannelInfoSection, sign:Bool, viewType: GeneralViewType) + case signDesc(sectionId: ChannelInfoSection, viewType: GeneralViewType) + case report(sectionId: ChannelInfoSection, viewType: GeneralViewType) + case leave(sectionId: ChannelInfoSection, isCreator: Bool, viewType: GeneralViewType) + + case media(sectionId: ChannelInfoSection, controller: PeerMediaController, isVisible: Bool, viewType: GeneralViewType) case section(Int) - + func withUpdatedViewType(_ viewType: GeneralViewType) -> ChannelInfoEntry { + switch self { + case let .info(sectionId, peerView, editable, updatingPhotoState, _): return .info(sectionId: sectionId, peerView: peerView, editable: editable, updatingPhotoState: updatingPhotoState, viewType: viewType) + case let .scam(sectionId, title, text, _): return .scam(sectionId: sectionId, title: title, text: text, viewType: viewType) + case let .about(sectionId, text, _): return .about(sectionId: sectionId, text: text, viewType: viewType) + case let .userName(sectionId, value, _): return .userName(sectionId: sectionId, value: value, viewType: viewType) + case let .setTitle(sectionId, text, _): return .setTitle(sectionId: sectionId, text: text, viewType: viewType) + case let .admins(sectionId, count, _): return .admins(sectionId: sectionId, count: count, viewType: viewType) + case let .blocked(sectionId, count, _): return .blocked(sectionId: sectionId, count: count, viewType: viewType) + case let .members(sectionId, count, _): return .members(sectionId: sectionId, count: count, viewType: viewType) + case let .link(sectionId, addressName, _): return .link(sectionId: sectionId, addressName: addressName, viewType: viewType) + case let .inviteLinks(section, count, _): return .inviteLinks(section: section, count: count, viewType: viewType) + case let .discussion(sectionId, group, participantsCount, _): return .discussion(sectionId: sectionId, group: group, participantsCount: participantsCount, viewType: viewType) + case let .discussionDesc(sectionId, _): return .discussionDesc(sectionId: sectionId, viewType: viewType) + case let .aboutInput(sectionId, description, _): return .aboutInput(sectionId: sectionId, description: description, viewType: viewType) + case let .aboutDesc(sectionId, _): return .aboutDesc(sectionId: sectionId, viewType: viewType) + case let .signMessages(sectionId, sign, _): return .signMessages(sectionId: sectionId, sign: sign, viewType: viewType) + case let .signDesc(sectionId, _): return .signDesc(sectionId: sectionId, viewType: viewType) + case let .report(sectionId, _): return .report(sectionId: sectionId, viewType: viewType) + case let .leave(sectionId, isCreator, _): return .leave(sectionId: sectionId, isCreator: isCreator, viewType: viewType) + case let .media(sectionId, controller, isVisible, _): return .media(sectionId: sectionId, controller: controller, isVisible: isVisible, viewType: viewType) + case .section: return self + } + } var stableId: PeerInfoEntryStableId { return IntPeerInfoEntryStableId(value: self.stableIndex) @@ -349,144 +641,152 @@ enum ChannelInfoEntry: PeerInfoEntry { return false } switch self { - case let .info(lhsSectionId, lhsPeerView, lhsEditable, lhsUpdatingPhotoState): + case let .info(sectionId, lhsPeerView, editable, updatingPhotoState, viewType): switch entry { - case let .info(rhsSectionId, rhsPeerView, rhsEditable, rhsUpdatingPhotoState): - - if lhsSectionId != rhsSectionId || lhsEditable != rhsEditable { - return false - } - - if lhsUpdatingPhotoState != rhsUpdatingPhotoState { - return false - } + case .info(sectionId, let rhsPeerView, editable, updatingPhotoState, viewType): let lhsPeer = peerViewMainPeer(lhsPeerView) let lhsCachedData = lhsPeerView.cachedData + let lhsNotificationSettings = lhsPeerView.notificationSettings let rhsPeer = peerViewMainPeer(rhsPeerView) let rhsCachedData = rhsPeerView.cachedData - + let rhsNotificationSettings = rhsPeerView.notificationSettings if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer { if !lhsPeer.isEqual(rhsPeer) { return false } - } else if (lhsPeer == nil) != (rhsPeer != nil) { + } else if (lhsPeer != nil) != (rhsPeer != nil) { + return false + } + + if let lhsNotificationSettings = lhsNotificationSettings, let rhsNotificationSettings = rhsNotificationSettings { + if !lhsNotificationSettings.isEqual(to: rhsNotificationSettings) { + return false + } + } else if (lhsNotificationSettings != nil) != (rhsNotificationSettings != nil) { return false } if let lhsCachedData = lhsCachedData, let rhsCachedData = rhsCachedData { if !lhsCachedData.isEqual(to: rhsCachedData) { return false } - } else if (lhsCachedData == nil) != (rhsCachedData != nil) { + } else if (lhsCachedData != nil) != (rhsCachedData != nil) { return false } return true default: return false } - case let .about(sectionId, text): + case let .scam(sectionId, title, text, viewType): switch entry { - case .about(sectionId, text): + case .scam(sectionId, title, text, viewType): return true default: return false } - case let .userName(sectionId, value): + case let .about(sectionId, text, viewType): switch entry { - case .userName(sectionId, value): + case .about(sectionId, text, viewType): return true default: return false } - case let .setPhoto(sectionId): + case let .userName(sectionId, value, viewType): switch entry { - case .setPhoto(sectionId): + case .userName(sectionId, value, viewType): return true default: return false } - case let .sharedMedia(sectionId): + case let .setTitle(sectionId, text, viewType): switch entry { - case .sharedMedia(sectionId): + case .setTitle(sectionId, text, viewType): return true default: return false } - case let .notifications(lhsSectionId, lhsSettings): + case let .report(sectionId, viewType): switch entry { - case let .notifications(rhsSectionId, rhsSettings): - - if lhsSectionId != rhsSectionId { - return false - } - if let lhsSettings = lhsSettings, let rhsSettings = rhsSettings { - return lhsSettings.isEqual(to: rhsSettings) - } else if (lhsSettings != nil) != (rhsSettings != nil) { - return false - } + case .report(sectionId, viewType): return true default: return false } - case .report: - switch entry { - case .report: + case let .admins(sectionId, count, viewType): + if case .admins(sectionId, count, viewType) = entry { return true - default: + } else { return false } - case let .admins(lhsSectionId, lhsCount): - if case let .admins(rhsSectionId, rhsCount) = entry { - return lhsSectionId == rhsSectionId && lhsCount == rhsCount + case let .blocked(sectionId, count, viewType): + if case .blocked(sectionId, count, viewType) = entry { + return true } else { return false } - case let .blocked(lhsSectionId, lhsCount): - if case let .blocked(rhsSectionId, rhsCount) = entry { - return lhsSectionId == rhsSectionId && lhsCount == rhsCount + case let .members(sectionId, count, viewType): + if case .members(sectionId, count, viewType) = entry { + return true } else { return false } - case let .members(lhsSectionId, lhsCount): - if case let .members(rhsSectionId, rhsCount) = entry { - return lhsSectionId == rhsSectionId && lhsCount == rhsCount + case let .link(sectionId, addressName, viewType): + if case .link(sectionId, addressName, viewType) = entry { + return true } else { return false } - case let .link(sectionId, addressName): - if case .link(sectionId, addressName) = entry { + case let .inviteLinks(sectionId, count, viewType): + if case .inviteLinks(sectionId, count, viewType) = entry { return true } else { return false } - case let .aboutInput(sectionId, _): - if case .aboutInput(sectionId, _) = entry { + case let .discussion(sectionId, lhsGroup, participantsCount, viewType): + if case .discussion(sectionId, let rhsGroup, participantsCount, viewType) = entry { + if let lhsGroup = lhsGroup, let rhsGroup = rhsGroup { + return lhsGroup.isEqual(rhsGroup) + } else if (lhsGroup != nil) != (rhsGroup != nil) { + return false + } return true } else { return false } - case let .aboutDesc(sectionId): - if case .aboutDesc(sectionId) = entry { + case let .discussionDesc(sectionId, viewType): + if case .discussionDesc(sectionId, viewType) = entry { return true } else { return false } - case let .signMessages(sectionId, sign): - if case .signMessages(sectionId, sign) = entry { + case let .aboutInput(sectionId, text, viewType): + if case .aboutInput(sectionId, text, viewType) = entry { return true } else { return false } - case let .signDesc(sectionId): - if case .signDesc(sectionId) = entry { + case let .aboutDesc(sectionId, viewType): + if case .aboutDesc(sectionId, viewType) = entry { return true } else { return false } - case .leave: + case let .signMessages(sectionId, sign, viewType): + if case .signMessages(sectionId, sign, viewType) = entry { + return true + } else { + return false + } + case let .signDesc(sectionId, viewType): + if case .signDesc(sectionId, viewType) = entry { + return true + } else { + return false + } + case let .leave(sectionId, isCreator, viewType): switch entry { - case .leave: + case .leave(sectionId, isCreator, viewType): return true default: return false @@ -498,6 +798,13 @@ enum ChannelInfoEntry: PeerInfoEntry { default: return false } + case let .media(sectionId, _, isVisible, viewType): + switch entry { + case .media(sectionId, _, isVisible, viewType): + return true + default: + return false + } } } @@ -505,75 +812,132 @@ enum ChannelInfoEntry: PeerInfoEntry { switch self { case .info: return 0 - case .setPhoto: + case .setTitle: return 1 - case .about: + case .scam: return 2 - case .userName: + case .about: return 3 - case .sharedMedia: + case .userName: return 4 - case .notifications: - return 5 case .admins: - return 6 - case .blocked: - return 7 - case .members: return 8 - case .link: + case .members: return 9 - case .aboutInput: + case .blocked: return 10 - case .aboutDesc: + case .link: return 11 - case .signMessages: + case .inviteLinks: return 12 - case .signDesc: + case .discussion: return 13 - case .report: + case .discussionDesc: return 14 - case .leave: + case .aboutInput: return 15 + case .aboutDesc: + return 16 + case .signMessages: + return 17 + case .signDesc: + return 18 + case .report: + return 19 + case .leave: + return 20 + case .media: + return 21 case let .section(id): return (id + 1) * 1000 - id } } + fileprivate var sectionId: Int { + switch self { + case let .info(sectionId, _, _, _, _): + return sectionId.rawValue + case let .setTitle(sectionId, _, _): + return sectionId.rawValue + case let .scam(sectionId, _, _, _): + return sectionId.rawValue + case let .about(sectionId, _, _): + return sectionId.rawValue + case let .userName(sectionId, _, _): + return sectionId.rawValue + case let .admins(sectionId, _, _): + return sectionId.rawValue + case let .blocked(sectionId, _, _): + return sectionId.rawValue + case let .members(sectionId, _, _): + return sectionId.rawValue + case let .link(sectionId, _, _): + return sectionId.rawValue + case let .inviteLinks(sectionId, _, _): + return sectionId.rawValue + case let .discussion(sectionId, _, _, _): + return sectionId.rawValue + case let .discussionDesc(sectionId, _): + return sectionId.rawValue + case let .aboutInput(sectionId, _, _): + return sectionId.rawValue + case let .aboutDesc(sectionId, _): + return sectionId.rawValue + case let .signMessages(sectionId, _, _): + return sectionId.rawValue + case let .signDesc(sectionId, _): + return sectionId.rawValue + case let .report(sectionId, _): + return sectionId.rawValue + case let .leave(sectionId, _, _): + return sectionId.rawValue + case let .media(sectionId, _, _, _): + return sectionId.rawValue + case let .section(sectionId): + return sectionId + } + } + private var sortIndex: Int { switch self { - case let .info(sectionId, _, _, _): - return (sectionId * 1000) + stableIndex - case let .setPhoto(sectionId): - return (sectionId * 1000) + stableIndex - case let .about(sectionId, _): - return (sectionId * 1000) + stableIndex - case let .userName(sectionId, _): - return (sectionId * 1000) + stableIndex - case let .sharedMedia(sectionId): - return (sectionId * 1000) + stableIndex - case let .notifications(sectionId, _): - return (sectionId * 1000) + stableIndex - case let .admins(sectionId, _): - return (sectionId * 1000) + stableIndex - case let .blocked(sectionId, _): - return (sectionId * 1000) + stableIndex - case let .members(sectionId, _): - return (sectionId * 1000) + stableIndex - case let .link(sectionId, _): - return (sectionId * 1000) + stableIndex - case let .aboutInput(sectionId, _): - return (sectionId * 1000) + stableIndex - case let .aboutDesc(sectionId): - return (sectionId * 1000) + stableIndex - case let .signMessages(sectionId, _): - return (sectionId * 1000) + stableIndex - case let .signDesc(sectionId): - return (sectionId * 1000) + stableIndex - case let .report(sectionId): - return (sectionId * 1000) + stableIndex - case let .leave(sectionId, _): - return (sectionId * 1000) + stableIndex + case let .info(sectionId, _, _, _, _): + return (sectionId.rawValue * 1000) + stableIndex + case let .setTitle(sectionId, _, _): + return (sectionId.rawValue * 1000) + stableIndex + case let .scam(sectionId, _, _, _): + return (sectionId.rawValue * 1000) + stableIndex + case let .about(sectionId, _, _): + return (sectionId.rawValue * 1000) + stableIndex + case let .userName(sectionId, _, _): + return (sectionId.rawValue * 1000) + stableIndex + case let .admins(sectionId, _, _): + return (sectionId.rawValue * 1000) + stableIndex + case let .blocked(sectionId, _, _): + return (sectionId.rawValue * 1000) + stableIndex + case let .members(sectionId, _, _): + return (sectionId.rawValue * 1000) + stableIndex + case let .link(sectionId, _, _): + return (sectionId.rawValue * 1000) + stableIndex + case let .inviteLinks(sectionId, _, _): + return (sectionId.rawValue * 1000) + stableIndex + case let .discussion(sectionId, _, _, _): + return (sectionId.rawValue * 1000) + stableIndex + case let .discussionDesc(sectionId, _): + return (sectionId.rawValue * 1000) + stableIndex + case let .aboutInput(sectionId, _, _): + return (sectionId.rawValue * 1000) + stableIndex + case let .aboutDesc(sectionId, _): + return (sectionId.rawValue * 1000) + stableIndex + case let .signMessages(sectionId, _, _): + return (sectionId.rawValue * 1000) + stableIndex + case let .signDesc(sectionId, _): + return (sectionId.rawValue * 1000) + stableIndex + case let .report(sectionId, _): + return (sectionId.rawValue * 1000) + stableIndex + case let .leave(sectionId, _, _): + return (sectionId.rawValue * 1000) + stableIndex + case let .media(sectionId, _, _, _): + return (sectionId.rawValue * 1000) + stableIndex case let .section(sectionId): return (sectionId + 1) * 1000 - sectionId } @@ -583,153 +947,151 @@ enum ChannelInfoEntry: PeerInfoEntry { guard let entry = entry as? ChannelInfoEntry else { return false } - return self.sortIndex > entry.sortIndex + return self.sortIndex < entry.sortIndex } func item(initialSize:NSSize, arguments:PeerInfoArguments) -> TableRowItem { let arguments = arguments as! ChannelInfoArguments - let state = arguments.state as! ChannelInfoState switch self { - case let .info(_, peerView, editable, updatingPhotoState): - return PeerInfoHeaderItem(initialSize, stableId: stableId.hashValue, account:arguments.account, peerView:peerView, editable: editable, updatingPhotoState: updatingPhotoState, firstNameEditableText: state.editingState?.editingName, textChangeHandler: { name, _ in - arguments.updateEditingName(name) - }) - case let .about(_, text): - return TextAndLabelItem(initialSize, stableId: stableId.hashValue, label:tr(.peerInfoInfo), text:text, account: arguments.account, detectLinks:true, openInfo: { peerId, toChat, _, _ in + case let .info(_, peerView, editable, updatingPhotoState, viewType): + return PeerInfoHeadItem(initialSize, stableId: stableId.hashValue, context: arguments.context, arguments: arguments, peerView:peerView, viewType: viewType, editing: editable, updatingPhotoState: updatingPhotoState, updatePhoto: arguments.updateChannelPhoto) + case let .scam(_, title, text, viewType): + return TextAndLabelItem(initialSize, stableId:stableId.hashValue, label: title, copyMenuText: L10n.textCopy, labelColor: theme.colors.redUI, text: text, context: arguments.context, viewType: viewType, detectLinks:false) + case let .about(_, text, viewType): + return TextAndLabelItem(initialSize, stableId: stableId.hashValue, label: L10n.peerInfoInfo, copyMenuText: L10n.textCopyLabelAbout, text:text, context: arguments.context, viewType: viewType, detectLinks:true, openInfo: { peerId, toChat, postId, _ in if toChat { - arguments.peerChat(peerId) + arguments.peerChat(peerId, postId: postId) } else { arguments.peerInfo(peerId) } - }, hashtag: arguments.account.context.globalSearch) - case let .userName(_, value): - let link = "https://t.me/\(value)" - return TextAndLabelItem(initialSize, stableId: stableId.hashValue, label:tr(.peerInfoSharelink), text: link, account: arguments.account, isTextSelectable:false, callback:{ - showModal(with: ShareModalController(ShareLinkObject(arguments.account, link: link)), for: mainWindow) - }) - case .sharedMedia: - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoSharedMedia), type: .none, action: { () in - arguments.sharedMedia() + }, hashtag: arguments.context.sharedContext.bindings.globalSearch) + case let .userName(_, value, viewType): + return TextAndLabelItem(initialSize, stableId: stableId.hashValue, label: L10n.peerInfoSharelink, copyMenuText: L10n.textCopyLabelShareLink, text: value, context: arguments.context, viewType: viewType, isTextSelectable:false, callback: arguments.share, selectFullWord: true, _copyToClipboard: { + arguments.copy(value) }) - case let .notifications(_, settings): - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoNotifications), type: .switchable(stateback: { () -> Bool in - - if let settings = settings as? TelegramPeerNotificationSettings, case .muted = settings.muteState { - return false - } else { - return true - } - - }), action: { - arguments.toggleNotifications() - }) - case .report: - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoReport), type: .none, action: { () in + case let .report(_, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoReport, type: .none, viewType: viewType, action: { () in arguments.report() }) - case let .members(_, count: count): - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoMembers), type: .context(stateback: { () -> String in - if let count = count { - return "\(count)" - } else { - return "" - } - }), action: { () in - arguments.members() - }) - case let .admins(_, count: count): - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoAdmins), type: .context(stateback: { () -> String in - if let count = count { - return "\(count)" - } else { - return "" - } - }), action: { () in - arguments.admins() - }) - case let .blocked(_, count): - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoBlackList), type: .context(stateback: { () -> String in - if let count = count { - return "\(count)" + case let .members(_, count, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoSubscribers, icon: theme.icons.peerInfoMembers, type: .nextContext(count != nil && count! > 0 ? "\(count!)" : ""), viewType: viewType, action: arguments.members) + case let .admins(_, count, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoAdministrators, icon: theme.icons.peerInfoAdmins, type: .nextContext(count != nil && count! > 0 ? "\(count!)" : ""), viewType: viewType, action: arguments.admins) + case let .blocked(_, count, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoRemovedUsers, icon: theme.icons.profile_removed, type: .nextContext(count != nil && count! > 0 ? "\(count!)" : ""), viewType: viewType, action: arguments.blocked) + case let .link(_, addressName: addressName, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoChannelType, icon: theme.icons.profile_channel_type, type: .context(addressName.isEmpty ? L10n.channelPrivate : L10n.channelPublic), viewType: viewType, action: arguments.visibilitySetup) + case let .inviteLinks(_, count, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoInviteLinks, icon: theme.icons.profile_links, type: .nextContext(count > 0 ? "\(count)" : ""), viewType: viewType, action: arguments.openInviteLinks) + case let .discussion(_, group, _, viewType): + let title: String + if let group = group { + if let address = group.addressName { + title = "@\(address)" } else { - return "" + title = group.displayTitle } - }), action: { () in - arguments.blocked() - }) - case let .link(_, addressName: addressName): - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoChannelType), type: .context(stateback: { () -> String in - return addressName.isEmpty ? tr(.channelPrivate) : tr(.channelPublic) - }), action: { () in - arguments.visibilitySetup() - }) - case .setPhoto: - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoSetChannelPhoto), nameStyle: blueActionButton, type: .none, action: { - pickImage(for: mainWindow, completion: { image in - if let image = image { - _ = (putToTemp(image: image) |> deliverOnMainQueue).start(next: { path in - arguments.updatePhoto(path) - }) - } - }) - - }) - case let .aboutInput(_, text): - return GeneralInputRowItem(initialSize, stableId: stableId.hashValue, placeholder: tr(.peerInfoAboutPlaceholder), text: text, limit: 255, insets: NSEdgeInsets(left:25,right:25,top:8,bottom:3), textChangeHandler: { updatedText in - arguments.updateEditingDescriptionText(updatedText) - }) - case .aboutDesc: - return GeneralTextRowItem(initialSize, stableId: stableId.hashValue, text: tr(.peerInfoSetAboutDescription)) - case let .signMessages(_, sign): - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoSignMessages), type: .switchable(stateback: { () -> Bool in - return sign - }), action: { - arguments.toggleSignatures(!sign) - }) - case .signDesc: - return GeneralTextRowItem(initialSize, stableId: stableId.hashValue, text: tr(.peerInfoSignMessagesDesc)) - case let .leave(_, isCreator): - - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: isCreator ? tr(.peerInfoDeleteChannel) : tr(.peerInfoLeaveChannel), nameStyle:redActionButton, type: .none, action: { () in - arguments.delete() + } else { + title = L10n.peerInfoDiscussionAdd + } + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoDiscussion, icon: theme.icons.profile_group_discussion, type: .nextContext(title), viewType: viewType, action: arguments.setupDiscussion) + case let .discussionDesc(_, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId.hashValue, text: L10n.peerInfoDiscussionDesc, viewType: viewType) + case let .setTitle(_, text, viewType): + return InputDataRowItem(initialSize, stableId: stableId.hashValue, mode: .plain, error: nil, viewType: viewType, currentText: text, placeholder: nil, inputPlaceholder: L10n.peerInfoChannelTitlePleceholder, filter: { $0 }, updated: arguments.updateEditingName, limit: 255) + case let .aboutInput(_, text, viewType): + return InputDataRowItem(initialSize, stableId: stableId.hashValue, mode: .plain, error: nil, viewType: viewType, currentText: text, placeholder: nil, inputPlaceholder: L10n.peerInfoAboutPlaceholder, filter: { $0 }, updated: arguments.updateEditingDescriptionText, limit: 255) + case let .aboutDesc(_, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId.hashValue, text: L10n.channelDescriptionHolderDescrpiton, viewType: viewType) + case let .signMessages(_, sign, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoSignMessages, icon: theme.icons.profile_channel_sign, type: .switchable(sign), viewType: viewType, action: { [weak arguments] in + arguments?.toggleSignatures(!sign) }) + case let .signDesc(_, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId.hashValue, text: L10n.peerInfoSignMessagesDesc, viewType: viewType) + case let .leave(_, isCreator, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: isCreator ? L10n.peerInfoDeleteChannel : L10n.peerInfoLeaveChannel, nameStyle:redActionButton, type: .none, viewType: viewType, action: arguments.delete) + case let .media(_, controller, isVisible, viewType): + return PeerMediaBlockRowItem(initialSize, stableId: stableId.hashValue, controller: controller, isVisible: isVisible, viewType: viewType) case .section(_): - return GeneralRowItem(initialSize, height:20, stableId: stableId.hashValue) + return GeneralRowItem(initialSize, height:30, stableId: stableId.hashValue, viewType: .separator) } } } -func channelInfoEntries(view: PeerView, arguments:PeerInfoArguments) -> [PeerInfoEntry] { +enum ChannelInfoSection : Int { + case header = 1 + case desc = 2 + case info = 3 + case type = 4 + case sign = 5 + case manage = 6 + case addition = 7 + case destruct = 8 + case media = 9 +} + +func channelInfoEntries(view: PeerView, arguments:PeerInfoArguments, mediaTabsData: PeerMediaTabsData, inviteLinksCount: Int32) -> [PeerInfoEntry] { let arguments = arguments as! ChannelInfoArguments - let state = arguments.state as! ChannelInfoState + var state:ChannelInfoState { + return arguments.state as! ChannelInfoState + } + var entries: [ChannelInfoEntry] = [] - var entries: [PeerInfoEntry] = [] - var sectionId:Int = 1 + var infoBlock:[ChannelInfoEntry] = [] - entries.append(ChannelInfoEntry.info(sectionId: sectionId, peerView: view, editable: state.editingState != nil, updatingPhotoState: state.updatingPhotoState)) + func applyBlock(_ block:[ChannelInfoEntry]) { + var block = block + for (i, item) in block.enumerated() { + block[i] = item.withUpdatedViewType(bestGeneralViewType(block, for: i)) + } + entries.append(contentsOf: block) + } + + infoBlock.append(.info(sectionId: .header, peerView: view, editable: state.editingState != nil, updatingPhotoState: state.updatingPhotoState, viewType: .singleItem)) + if let channel = peerViewMainPeer(view) as? TelegramChannel { if let editingState = state.editingState { - if channel.hasAdminRights(.canChangeInfo) { - entries.append(ChannelInfoEntry.setPhoto(sectionId:sectionId)) - entries.append(ChannelInfoEntry.section(sectionId)) - sectionId += 1 - } - if channel.flags.contains(.isCreator) { - entries.append(ChannelInfoEntry.link(sectionId:sectionId, addressName: channel.username ?? "")) + if channel.hasPermission(.changeInfo) { + infoBlock.append(.setTitle(sectionId: .header, text: editingState.editingName ?? "", viewType: .singleItem)) } - if channel.hasAdminRights(.canChangeInfo) { - entries.append(ChannelInfoEntry.aboutInput(sectionId:sectionId, description: editingState.editingDescriptionText)) - entries.append(ChannelInfoEntry.aboutDesc(sectionId: sectionId)) + if channel.hasPermission(.changeInfo) && !channel.isScam && !channel.isFake { + infoBlock.append(.aboutInput(sectionId: .header, description: editingState.editingDescriptionText, viewType: .singleItem)) + } + applyBlock(infoBlock) + entries.append(.aboutDesc(sectionId: .header, viewType: .textBottomItem)) + + if channel.adminRights != nil || channel.flags.contains(.isCreator) { + var block: [ChannelInfoEntry] = [] + if channel.flags.contains(.isCreator) { + block.append(.link(sectionId: .type, addressName: channel.username ?? "", viewType: .singleItem)) + } + + let group: Peer? + if let cachedData = view.cachedData as? CachedChannelData, let linkedDiscussionPeerId = cachedData.linkedDiscussionPeerId.peerId { + group = view.peers[linkedDiscussionPeerId] + } else { + group = nil + } + if channel.canInviteUsers { + block.append(.inviteLinks(section: .type, count: inviteLinksCount, viewType: .singleItem)) + } + if channel.adminRights?.rights.contains(.canChangeInfo) == true { + block.append(.discussion(sectionId: .type, group: group, participantsCount: nil, viewType: .singleItem)) + } + applyBlock(block) - entries.append(ChannelInfoEntry.section(sectionId)) - sectionId += 1 + if channel.adminRights?.rights.contains(.canChangeInfo) == true { + entries.append(.discussionDesc(sectionId: .type, viewType: .textBottomItem)) + } + } let messagesShouldHaveSignatures:Bool @@ -740,72 +1102,78 @@ func channelInfoEntries(view: PeerView, arguments:PeerInfoArguments) -> [PeerInf messagesShouldHaveSignatures = false } - if channel.hasAdminRights(.canChangeInfo) { - entries.append(ChannelInfoEntry.signMessages(sectionId: sectionId, sign: messagesShouldHaveSignatures)) - entries.append(ChannelInfoEntry.signDesc(sectionId: sectionId)) - - entries.append(ChannelInfoEntry.section(sectionId)) - sectionId += 1 + if channel.hasPermission(.changeInfo) { + entries.append(.signMessages(sectionId: .sign, sign: messagesShouldHaveSignatures, viewType: .singleItem)) + entries.append(.signDesc(sectionId: .sign, viewType: .textBottomItem)) + } + if channel.flags.contains(.isCreator) { + entries.append(.leave(sectionId: .destruct, isCreator: channel.flags.contains(.isCreator), viewType: .singleItem)) } - - - entries.append(ChannelInfoEntry.leave(sectionId:sectionId, isCreator: channel.flags.contains(.isCreator))) } else { + applyBlock(infoBlock) + + var aboutBlock:[ChannelInfoEntry] = [] + if channel.isScam { + aboutBlock.append(.scam(sectionId: .desc, title: L10n.peerInfoScam, text: L10n.channelInfoScamWarning, viewType: .singleItem)) + } else if channel.isFake { + aboutBlock.append(.scam(sectionId: .desc, title: L10n.peerInfoFake, text: L10n.channelInfoFakeWarning, viewType: .singleItem)) + } if let cachedData = view.cachedData as? CachedChannelData { - if let about = cachedData.about, !about.isEmpty { - entries.append(ChannelInfoEntry.about(sectionId:sectionId, text: about)) + if let about = cachedData.about, !about.isEmpty, !channel.isScam && !channel.isFake { + aboutBlock.append(.about(sectionId: .desc, text: about, viewType: .singleItem)) } } if let username = channel.username, !username.isEmpty { - entries.append(ChannelInfoEntry.userName(sectionId:sectionId, value: username)) + aboutBlock.append(.userName(sectionId: .desc, value: "https://t.me/\(username)", viewType: .singleItem)) + } else if let cachedData = view.cachedData as? CachedChannelData, let invitation = cachedData.exportedInvitation { + aboutBlock.append(.userName(sectionId: .desc, value: invitation.link, viewType: .singleItem)) } - if entries.count > 1 { - entries.append(ChannelInfoEntry.section(sectionId)) - sectionId += 1 - } - - if channel.groupAccess.canManageGroup { - var membersCount:Int32? = nil - var adminsCount:Int32? = nil - var blockedCount:Int32? = nil - if let cachedData = view.cachedData as? CachedChannelData { - membersCount = cachedData.participantsSummary.memberCount - adminsCount = cachedData.participantsSummary.adminCount - blockedCount = cachedData.participantsSummary.kickedCount - } - entries.append(ChannelInfoEntry.admins(sectionId: sectionId, count: adminsCount)) - entries.append(ChannelInfoEntry.members(sectionId: sectionId, count: membersCount)) - - if let blockedCount = blockedCount { - entries.append(ChannelInfoEntry.blocked(sectionId: sectionId, count: blockedCount)) - } - - entries.append(ChannelInfoEntry.section(sectionId)) - sectionId += 1 - } - - - - entries.append(ChannelInfoEntry.sharedMedia(sectionId:sectionId)) - entries.append(ChannelInfoEntry.notifications(sectionId:sectionId, settings: view.notificationSettings)) - - entries.append(ChannelInfoEntry.section(sectionId)) - sectionId += 1 - - if !channel.flags.contains(.isCreator) { - entries.append(ChannelInfoEntry.report(sectionId:sectionId)) - if channel.participationStatus == .member { - entries.append(ChannelInfoEntry.leave(sectionId:sectionId, isCreator: false)) - } + applyBlock(aboutBlock) + + } + + if channel.flags.contains(.isCreator) || channel.adminRights != nil { + var membersCount:Int32? = nil + var adminsCount:Int32? = nil + var blockedCount:Int32? = nil + + if let cachedData = view.cachedData as? CachedChannelData { + membersCount = cachedData.participantsSummary.memberCount + adminsCount = cachedData.participantsSummary.adminCount + blockedCount = cachedData.participantsSummary.kickedCount } - + entries.append(.admins(sectionId: .manage, count: adminsCount, viewType: .firstItem)) + entries.append(.members(sectionId: .manage, count: membersCount, viewType: .innerItem)) + entries.append(.blocked(sectionId: .manage, count: blockedCount, viewType: .lastItem)) + } } - return entries.sorted(by: { (p1, p2) -> Bool in + + if mediaTabsData.loaded && !mediaTabsData.collections.isEmpty, let controller = arguments.mediaController() { + entries.append(.media(sectionId: ChannelInfoSection.media, controller: controller, isVisible: state.editingState == nil, viewType: .singleItem)) + } + + var items:[ChannelInfoEntry] = [] + var sectionId:Int = 0 + let sorted = entries.sorted(by: { (p1, p2) -> Bool in return p1.isOrderedBefore(p2) }) + for entry in sorted { + if entry.sectionId != sectionId { + if entry.sectionId == ChannelInfoSection.media.rawValue { + sectionId = entry.sectionId + } else { + items.append(.section(sectionId)) + sectionId = entry.sectionId + } + } + items.append(entry) + } + sectionId += 1 + items.append(.section(sectionId)) + return items } diff --git a/Telegram-Mac/ChannelIntroViewController.swift b/Telegram-Mac/ChannelIntroViewController.swift index bb4e29ef5c..4a166f9ae8 100644 --- a/Telegram-Mac/ChannelIntroViewController.swift +++ b/Telegram-Mac/ChannelIntroViewController.swift @@ -7,10 +7,11 @@ // import Cocoa -import SwiftSignalKitMac +import SwiftSignalKit import TGUIKit -import TelegramCoreMac -import PostboxMac +import TelegramCore + +import Postbox class ChannelIntroView : NSScrollView, AppearanceViewProtocol { let imageView:ImageView = ImageView() @@ -23,22 +24,30 @@ class ChannelIntroView : NSScrollView, AppearanceViewProtocol { wantsLayer = true documentView?.addSubview(imageView) documentView?.addSubview(textView) - - updateLocalizationAndTheme() + documentView?.addSubview(button) + + button.set(font: .medium(.title), for: .Normal) + updateLocalizationAndTheme(theme: theme) } - func updateLocalizationAndTheme() { - + func updateLocalizationAndTheme(theme: PresentationTheme) { + let theme = (theme as! TelegramPresentationTheme) imageView.image = theme.icons.channelIntro imageView.sizeToFit() + + button.set(text: L10n.channelIntroCreateChannel, for: .Normal) + + button.set(color: theme.colors.accent, for: .Normal) + _ = button.sizeToFit() + backgroundColor = theme.colors.background textView.background = theme.colors.background documentView?.background = theme.colors.background let attr = NSMutableAttributedString() - _ = attr.append(string: tr(.channelIntroDescriptionHeader), color: theme.colors.text, font: .medium(.header)) + _ = attr.append(string: tr(L10n.channelIntroDescriptionHeader), color: theme.colors.text, font: .medium(.header)) _ = attr.append(string:"\n\n") - _ = attr.append(string: tr(.channelIntroDescription), color: theme.colors.grayText, font: .normal(.text)) + _ = attr.append(string: tr(L10n.channelIntroDescription), color: theme.colors.grayText, font: .normal(.text)) textView.set(layout: TextViewLayout(attr, alignment:.center)) } @@ -52,7 +61,11 @@ class ChannelIntroView : NSScrollView, AppearanceViewProtocol { textView.update(textView.layout) imageView.centerX(y:30) textView.centerX(y:imageView.frame.maxY + 30) - containerView.setFrameSize(frame.width, textView.frame.maxY + 30) + + button.centerX(y: textView.frame.maxY + 30) + + containerView.setFrameSize(frame.width, button.frame.maxY + 30) + } required init?(coder: NSCoder) { @@ -65,7 +78,7 @@ class ChannelIntroViewController: EmptyComposeController BarView { - return TextButtonBarView(controller: self, text: tr(.channelCreate), style: navigationButtonStyle, alignment:.Right) + return TextButtonBarView(controller: self, text: tr(L10n.channelCreate), style: navigationButtonStyle, alignment:.Right) } override var removeAfterDisapper: Bool { @@ -80,6 +93,7 @@ class ChannelIntroViewController: EmptyComposeController KeyHandlerResult { executeNext() return .rejected @@ -88,7 +102,11 @@ class ChannelIntroViewController: EmptyComposeController ChannelMemberListState { + return ChannelMemberListState(list: list, loadingState: self.loadingState) + } + + func withUpdatedLoadingState(_ loadingState: ChannelMemberListLoadingState) -> ChannelMemberListState { + return ChannelMemberListState(list: self.list, loadingState: loadingState) + } +} + +enum ChannelMemberListCategory { + case recent + case recentSearch(String) + case mentions(MessageId?, String?) + case admins(String?) + case contacts(String?) + case bots(String?) + case restricted(String?) + case banned(String?) +} + +private protocol ChannelMemberCategoryListContext { + var listStateValue: ChannelMemberListState { get } + var listState: Signal { get } + func loadMore() + func reset(_ force: Bool) + func replayUpdates(_ updates: [(ChannelParticipant?, RenderedChannelParticipant?, Bool?)]) + func forceUpdateHead() +} + +private func isParticipantMember(_ participant: ChannelParticipant, infoIsMember: Bool?) -> Bool { + if let banInfo = participant.banInfo { + return !banInfo.rights.flags.contains(.banReadMessages) && banInfo.isMember + } else if let infoIsMember = infoIsMember { + return infoIsMember + } else { + return true + } +} + +private final class ChannelMemberSingleCategoryListContext: ChannelMemberCategoryListContext { + private let engine: TelegramEngine + private let account: Account + private let peerId: PeerId + private let category: ChannelMemberListCategory + + var listStateValue: ChannelMemberListState { + didSet { + self.listStatePromise.set(.single(self.listStateValue)) + if case .admins(nil) = self.category, case .ready = self.listStateValue.loadingState { + let ranks: [PeerId: CachedChannelAdminRankType] = self.listStateValue.list.reduce([:]) { (ranks, participant) in + var ranks = ranks + ranks[participant.participant.peerId] = CachedChannelAdminRankType(participant: participant.participant) + return ranks + } + let previousRanks: [PeerId: CachedChannelAdminRankType] = oldValue.list.reduce([:]) { (ranks, participant) in + var ranks = ranks + ranks[participant.participant.peerId] = CachedChannelAdminRankType(participant: participant.participant) + return ranks + } + if ranks != previousRanks { + + let _ = updateCachedChannelAdminRanks(postbox: account.postbox, peerId: self.peerId, ranks: ranks).start() + } + } + } + } + + private var listStatePromise: Promise + var listState: Signal { + return self.listStatePromise.get() + } + + private let loadingDisposable = MetaDisposable() + private let headUpdateDisposable = MetaDisposable() + + private var headUpdateTimer: SwiftSignalKit.Timer? + + init(engine: TelegramEngine, account: Account, peerId: PeerId, category: ChannelMemberListCategory) { + self.engine = engine + self.peerId = peerId + self.category = category + self.account = account + self.listStateValue = ChannelMemberListState(list: [], loadingState: .ready(hasMore: true)) + self.listStatePromise = Promise(self.listStateValue) + self.loadMoreInternal(initial: true) + } + + deinit { + self.loadingDisposable.dispose() + self.headUpdateDisposable.dispose() + self.headUpdateTimer?.invalidate() + } + + func loadMore() { + self.loadMoreInternal(initial: false) + } + + private func loadMoreInternal(initial: Bool) { + guard case .ready(true) = self.listStateValue.loadingState else { + return + } + + let loadCount: Int32 + if case .ready(true) = self.listStateValue.loadingState, self.listStateValue.list.isEmpty { + loadCount = initialBatchSize + } else { + loadCount = requestBatchSize + } + + self.listStateValue = self.listStateValue.withUpdatedLoadingState(.loading(initial: initial)) + + self.loadingDisposable.set((self.loadMoreSignal(count: loadCount) + |> deliverOnMainQueue).start(next: { [weak self] members in + self?.appendMembersAndFinishLoading(members) + })) + } + + func reset(_ force: Bool) { + if case .loading = self.listStateValue.loadingState, self.listStateValue.list.isEmpty { + } else { + var list = self.listStateValue.list + var loadingState: ChannelMemberListLoadingState = .ready(hasMore: true) + if list.count > Int(initialBatchSize) && !force { + list.removeSubrange(Int(initialBatchSize) ..< list.count) + loadingState = .ready(hasMore: true) + } + + self.loadingDisposable.set(nil) + self.listStateValue = self.listStateValue.withUpdatedLoadingState(loadingState).withUpdatedList(list) + } + } + + private func loadSignal(offset: Int32, count: Int32, hash: Int64) -> Signal<[RenderedChannelParticipant]?, NoError> { + let requestCategory: ChannelMembersCategory + var adminQuery: String? = nil + switch self.category { + case .recent: + requestCategory = .recent(.all) + case let .recentSearch(query): + requestCategory = .recent(.search(query)) + case let .mentions(threadId, query): + if let query = query, !query.isEmpty { + requestCategory = .mentions(threadId: threadId, filter: .search(query)) + } else { + requestCategory = .mentions(threadId: threadId, filter: .all) + } + case let .admins(query): + requestCategory = .admins + adminQuery = query + case let .contacts(query): + requestCategory = .contacts(query.flatMap(ChannelMembersCategoryFilter.search) ?? .all) + case let .bots(query): + requestCategory = .bots(query.flatMap(ChannelMembersCategoryFilter.search) ?? .all) + case let .restricted(query): + requestCategory = .restricted(query.flatMap(ChannelMembersCategoryFilter.search) ?? .all) + case let .banned(query): + requestCategory = .banned(query.flatMap(ChannelMembersCategoryFilter.search) ?? .all) + } + return engine.peers.channelMembers(peerId: self.peerId, category: requestCategory, offset: offset, limit: count, hash: hash) |> map { members in + switch requestCategory { + case .admins: + if let query = adminQuery { + return members?.filter({$0.peer.displayTitle.lowercased().components(separatedBy: " ").contains(where: {$0.hasPrefix(query.lowercased())})}) + } + default: + break + } + return members + } + } + + private func loadMoreSignal(count: Int32) -> Signal<[RenderedChannelParticipant], NoError> { + return self.loadSignal(offset: Int32(self.listStateValue.list.count), count: count, hash: 0) + |> map { value -> [RenderedChannelParticipant] in + return value ?? [] + } + } + + private func updateHeadMembers(_ headMembers: [RenderedChannelParticipant]?) { + if let headMembers = headMembers { + var existingIds = Set() + var list = headMembers + for member in list { + existingIds.insert(member.peer.id) + } + for member in self.listStateValue.list { + if !existingIds.contains(member.peer.id) { + list.append(member) + } + } + self.loadingDisposable.set(nil) + self.listStateValue = self.listStateValue.withUpdatedList(list) + if case .loading = self.listStateValue.loadingState { + self.loadMore() + } + } + + self.headUpdateTimer?.invalidate() + self.headUpdateTimer = nil + self.checkUpdateHead() + } + + private func appendMembersAndFinishLoading(_ members: [RenderedChannelParticipant]) { + var firstLoad = false + if case .loading = self.listStateValue.loadingState, self.listStateValue.list.isEmpty { + firstLoad = true + } + var existingIds = Set() + var list = self.listStateValue.list + for member in list { + existingIds.insert(member.peer.id) + } + for member in members { + if !existingIds.contains(member.peer.id) { + list.append(member) + } + } + self.listStateValue = self.listStateValue.withUpdatedList(list).withUpdatedLoadingState(.ready(hasMore: members.count >= requestBatchSize)) + if firstLoad { + self.checkUpdateHead() + } + } + + func forceUpdateHead() { + self.headUpdateTimer = nil + self.checkUpdateHead() + } + + private func checkUpdateHead() { + if self.listStateValue.list.isEmpty { + return + } + + if self.headUpdateTimer == nil { + let headUpdateTimer = SwiftSignalKit.Timer(timeout: headUpdateTimeout, repeat: false, completion: { [weak self] in + guard let strongSelf = self else { + return + } + + var hash: UInt64 = 0 + + for i in 0 ..< min(strongSelf.listStateValue.list.count, Int(initialBatchSize)) { + let peerId = strongSelf.listStateValue.list[i].peer.id + hash = (hash &* 20261) &+ UInt64(bitPattern: peerId.id._internalGetInt64Value()) + } + hash = hash % 0x7FFFFFFF + strongSelf.headUpdateDisposable.set((strongSelf.loadSignal(offset: 0, count: initialBatchSize, hash: Int64(bitPattern: hash)) + |> deliverOnMainQueue).start(next: { members in + self?.updateHeadMembers(members) + })) + }, queue: Queue.mainQueue()) + self.headUpdateTimer = headUpdateTimer + headUpdateTimer.start() + } + } + + fileprivate func replayUpdates(_ updates: [(ChannelParticipant?, RenderedChannelParticipant?, Bool?)]) { + var list = self.listStateValue.list + var updatedList = false + for (maybePrevious, updated, infoIsMember) in updates { + var previous: ChannelParticipant? = maybePrevious + if let participantId = maybePrevious?.peerId ?? updated?.peer.id { + inner: for participant in list { + if participant.peer.id == participantId { + previous = participant.participant + break inner + } + } + } + switch self.category { + case let .admins(query): + if let updated = updated, (query == nil || updated.peer.indexName.matchesByTokens(query!)) { + if case let .member(_, _, adminInfo, _, _) = updated.participant, adminInfo == nil { + loop: for i in 0 ..< list.count { + if list[i].peer.id == updated.peer.id { + list.remove(at: i) + updatedList = true + break loop + } + } + } else { + var found = false + loop: for i in 0 ..< list.count { + if list[i].peer.id == updated.peer.id { + list[i] = updated + found = true + updatedList = true + break loop + } + } + if !found { + list.insert(updated, at: 0) + updatedList = true + } + } + } else if let previous = previous, let _ = previous.adminInfo { + loop: for i in 0 ..< list.count { + if list[i].peer.id == previous.peerId { + list.remove(at: i) + updatedList = true + break loop + } + } + if let updated = updated, case .creator = updated.participant { + list.insert(updated, at: 0) + updatedList = true + } + } + case .restricted: + if let updated = updated, let banInfo = updated.participant.banInfo, !banInfo.rights.flags.contains(.banReadMessages) { + var found = false + loop: for i in 0 ..< list.count { + if list[i].peer.id == updated.peer.id { + list[i] = updated + found = true + updatedList = true + break loop + } + } + if !found { + list.insert(updated, at: 0) + updatedList = true + } + } else if let previous = previous, let banInfo = previous.banInfo, !banInfo.rights.flags.contains(.banReadMessages) { + loop: for i in 0 ..< list.count { + if list[i].peer.id == previous.peerId { + list.remove(at: i) + updatedList = true + break loop + } + } + } + case .banned: + if let updated = updated, let banInfo = updated.participant.banInfo, banInfo.rights.flags.contains(.banReadMessages) { + var found = false + loop: for i in 0 ..< list.count { + if list[i].peer.id == updated.peer.id { + list[i] = updated + found = true + updatedList = true + break loop + } + } + if !found { + list.insert(updated, at: 0) + updatedList = true + } + } else if let previous = previous, let banInfo = previous.banInfo, banInfo.rights.flags.contains(.banReadMessages) { + loop: for i in 0 ..< list.count { + if list[i].peer.id == previous.peerId { + list.remove(at: i) + updatedList = true + break loop + } + } + } + case .recent: + if let updated = updated, isParticipantMember(updated.participant, infoIsMember: infoIsMember) { + var found = false + loop: for i in 0 ..< list.count { + if list[i].peer.id == updated.peer.id { + list[i] = updated + found = true + updatedList = true + break loop + } + } + if !found { + list.insert(updated, at: 0) + updatedList = true + } + } else if let previous = previous, isParticipantMember(previous, infoIsMember: nil) { + loop: for i in 0 ..< list.count { + if list[i].peer.id == previous.peerId { + list.remove(at: i) + updatedList = true + break loop + } + } + } + case let .contacts(query): + if query == nil { + if let updated = updated, isParticipantMember(updated.participant, infoIsMember: infoIsMember) { + var found = false + loop: for i in 0 ..< list.count { + if list[i].peer.id == updated.peer.id { + list[i] = updated + found = true + updatedList = true + break loop + } + } + if !found { + //list.insert(updated, at: 0) + //updatedList = true + } + } else if let previous = previous, isParticipantMember(previous, infoIsMember: nil) { + loop: for i in 0 ..< list.count { + if list[i].peer.id == previous.peerId { + list.remove(at: i) + updatedList = true + break loop + } + } + } + } + case let .bots(query): + if query == nil { + if let updated = updated, isParticipantMember(updated.participant, infoIsMember: infoIsMember) { + var found = false + loop: for i in 0 ..< list.count { + if list[i].peer.id == updated.peer.id { + list[i] = updated + found = true + updatedList = true + break loop + } + } + if !found { + //list.insert(updated, at: 0) + //updatedList = true + } + } else if let previous = previous, isParticipantMember(previous, infoIsMember: nil) { + loop: for i in 0 ..< list.count { + if list[i].peer.id == previous.peerId { + list.remove(at: i) + updatedList = true + break loop + } + } + } + } + case let .recentSearch(query): + if let updated = updated, isParticipantMember(updated.participant, infoIsMember: infoIsMember), updated.peer.indexName.matchesByTokens(query) { + var found = false + loop: for i in 0 ..< list.count { + if list[i].peer.id == updated.peer.id { + list[i] = updated + found = true + updatedList = true + break loop + } + } + if !found { + list.insert(updated, at: 0) + updatedList = true + } + } else if let previous = previous, isParticipantMember(previous, infoIsMember: nil) { + loop: for i in 0 ..< list.count { + if list[i].peer.id == previous.peerId { + list.remove(at: i) + updatedList = true + break loop + } + } + } + case .mentions: + break + } + } + if updatedList { + self.listStateValue = self.listStateValue.withUpdatedList(list) + } + } + +} + +private final class ChannelMemberMultiCategoryListContext: ChannelMemberCategoryListContext { + private var contexts: [ChannelMemberSingleCategoryListContext] = [] + + var listStateValue: ChannelMemberListState { + return ChannelMemberMultiCategoryListContext.reduceListStates(self.contexts.map { $0.listStateValue }) + } + + private static func reduceListStates(_ listStates: [ChannelMemberListState]) -> ChannelMemberListState { + var allReady = true + for listState in listStates { + if case .loading(true) = listState.loadingState, listState.list.isEmpty { + allReady = false + break + } + } + if !allReady { + return ChannelMemberListState(list: [], loadingState: .loading(initial: true)) + } + + var list: [RenderedChannelParticipant] = [] + var existingIds = Set() + var loadingState: ChannelMemberListLoadingState = .ready(hasMore: false) + loop: for i in 0 ..< listStates.count { + for item in listStates[i].list { + if !existingIds.contains(item.peer.id) { + existingIds.insert(item.peer.id) + list.append(item) + } + } + switch listStates[i].loadingState { + case let .loading(initial): + loadingState = .loading(initial: initial) + break loop + case let .ready(hasMore): + if hasMore { + loadingState = .ready(hasMore: true) + break loop + } + } + } + return ChannelMemberListState(list: list, loadingState: loadingState) + } + + var listState: Signal { + let signals: [Signal] = self.contexts.map { context in + return context.listState + } + return combineLatest(signals) |> map { listStates -> ChannelMemberListState in + return ChannelMemberMultiCategoryListContext.reduceListStates(listStates) + } + } + + init(engine: TelegramEngine, account: Account, peerId: PeerId, categories: [ChannelMemberListCategory]) { + self.contexts = categories.map { category in + return ChannelMemberSingleCategoryListContext(engine: engine, account: account, peerId: peerId, category: category) + } + } + + func loadMore() { + loop: for context in self.contexts { + switch context.listStateValue.loadingState { + case .loading: + break loop + case let .ready(hasMore): + if hasMore { + context.loadMore() + } + } + } + } + + func reset(_ force: Bool) { + for context in self.contexts { + context.reset(force) + } + } + + func forceUpdateHead() { + for context in self.contexts { + context.forceUpdateHead() + } + } + + func replayUpdates(_ updates: [(ChannelParticipant?, RenderedChannelParticipant?, Bool?)]) { + for context in self.contexts { + context.replayUpdates(updates) + } + } +} + +struct PeerChannelMemberCategoryControl { + fileprivate let key: PeerChannelMemberContextKey +} + +private final class PeerChannelMemberContextWithSubscribers { + let context: ChannelMemberCategoryListContext + private let emptyTimeout: Double + private let subscribers = Bag<(ChannelMemberListState) -> Void>() + private let disposable = MetaDisposable() + private let becameEmpty: () -> Void + + private var emptyTimer: SwiftSignalKit.Timer? + + init(context: ChannelMemberCategoryListContext, emptyTimeout: Double, becameEmpty: @escaping () -> Void) { + self.context = context + self.emptyTimeout = emptyTimeout + self.becameEmpty = becameEmpty + self.disposable.set((context.listState + |> deliverOnMainQueue).start(next: { [weak self] value in + if let strongSelf = self { + for f in strongSelf.subscribers.copyItems() { + f(value) + } + } + })) + } + + deinit { + self.disposable.dispose() + self.emptyTimer?.invalidate() + } + + private func resetAndBeginEmptyTimer() { + self.context.reset(false) + self.emptyTimer?.invalidate() + let emptyTimer = SwiftSignalKit.Timer(timeout: self.emptyTimeout, repeat: false, completion: { [weak self] in + if let strongSelf = self { + if strongSelf.subscribers.isEmpty { + strongSelf.becameEmpty() + } + } + }, queue: Queue.mainQueue()) + self.emptyTimer = emptyTimer + emptyTimer.start() + } + + func subscribe(requestUpdate: Bool, updated: @escaping (ChannelMemberListState) -> Void) -> Disposable { + let wasEmpty = self.subscribers.isEmpty + let index = self.subscribers.add(updated) + updated(self.context.listStateValue) + if wasEmpty { + self.emptyTimer?.invalidate() + if requestUpdate { + self.context.forceUpdateHead() + } + } + return ActionDisposable { [weak self] in + Queue.mainQueue().async { + if let strongSelf = self { + strongSelf.subscribers.remove(index) + if strongSelf.subscribers.isEmpty { + strongSelf.resetAndBeginEmptyTimer() + } + } + } + } + } +} + +final class PeerChannelMemberCategoriesContext { + private let engine: TelegramEngine + private let account: Account + private let peerId: PeerId + private var becameEmpty: (Bool) -> Void + + private var contexts: [PeerChannelMemberContextKey: PeerChannelMemberContextWithSubscribers] = [:] + + init(engine: TelegramEngine, account: Account, peerId: PeerId, becameEmpty: @escaping (Bool) -> Void) { + self.engine = engine + self.account = account + self.peerId = peerId + self.becameEmpty = becameEmpty + } + + func reset(_ key: PeerChannelMemberContextKey) { + for (contextKey, context) in contexts { + if contextKey == key { + context.context.reset(true) + context.context.loadMore() + } + } + } + + func getContext(key: PeerChannelMemberContextKey, requestUpdate: Bool, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl) { + assert(Queue.mainQueue().isCurrent()) + if let current = self.contexts[key] { + return (current.subscribe(requestUpdate: requestUpdate, updated: updated), PeerChannelMemberCategoryControl(key: key)) + } + let context: ChannelMemberCategoryListContext + let emptyTimeout: Double + switch key { + case .admins(nil), .banned(nil), .recentSearch(nil), .restricted(nil), .restrictedAndBanned(nil), .recent: + emptyTimeout = defaultEmptyTimeout + default: + emptyTimeout = 0.0 + } + switch key { + case .recent, .recentSearch, .admins, .contacts, .bots, .mentions: + let mappedCategory: ChannelMemberListCategory + switch key { + case .recent: + mappedCategory = .recent + case let .recentSearch(query): + mappedCategory = .recentSearch(query) + case let .admins(query): + mappedCategory = .admins(query) + case let .contacts(query): + mappedCategory = .contacts(query) + case let .bots(query): + mappedCategory = .bots(query) + case let .mentions(threadId, query): + mappedCategory = .mentions(threadId, query) + default: + mappedCategory = .recent + } + context = ChannelMemberSingleCategoryListContext(engine: engine, account: self.account, peerId: self.peerId, category: mappedCategory) + case let .restrictedAndBanned(query): + context = ChannelMemberMultiCategoryListContext(engine: engine, account: self.account, peerId: self.peerId, categories: [.restricted(query), .banned(query)]) + case let .restricted(query): + context = ChannelMemberSingleCategoryListContext(engine: engine, account: self.account, peerId: self.peerId, category: .restricted(query)) + case let .banned(query): + context = ChannelMemberSingleCategoryListContext(engine: engine, account: self.account, peerId: self.peerId, category: .banned(query)) + } + let contextWithSubscribers = PeerChannelMemberContextWithSubscribers(context: context, emptyTimeout: emptyTimeout, becameEmpty: { [weak self] in + assert(Queue.mainQueue().isCurrent()) + if let strongSelf = self { + strongSelf.contexts.removeValue(forKey: key) + } + }) + self.contexts[key] = contextWithSubscribers + return (contextWithSubscribers.subscribe(requestUpdate: requestUpdate, updated: updated), PeerChannelMemberCategoryControl(key: key)) + } + + func loadMore(_ control: PeerChannelMemberCategoryControl) { + assert(Queue.mainQueue().isCurrent()) + if let context = self.contexts[control.key] { + context.context.loadMore() + } + } + + func replayUpdates(_ updates: [(ChannelParticipant?, RenderedChannelParticipant?, Bool?)]) { + for (_, context) in self.contexts { + context.context.replayUpdates(updates) + } + } +} diff --git a/Telegram-Mac/ChannelMembersViewController.swift b/Telegram-Mac/ChannelMembersViewController.swift index 28bd139388..35b583bda8 100644 --- a/Telegram-Mac/ChannelMembersViewController.swift +++ b/Telegram-Mac/ChannelMembersViewController.swift @@ -8,21 +8,22 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac +import Postbox +import TelegramCore + +import SwiftSignalKit private final class ChannelMembersControllerArguments { - let account: Account + let context: AccountContext let removePeer: (PeerId) -> Void let addMembers:()-> Void let inviteLink:()-> Void let openInfo:(Peer)->Void - init(account: Account, removePeer: @escaping (PeerId) -> Void, addMembers:@escaping()->Void, inviteLink:@escaping()->Void, openInfo:@escaping(Peer)->Void) { - self.account = account + init(context: AccountContext, removePeer: @escaping (PeerId) -> Void, addMembers:@escaping()->Void, inviteLink:@escaping()->Void, openInfo:@escaping(Peer)->Void) { + self.context = context self.removePeer = removePeer self.addMembers = addMembers self.inviteLink = inviteLink @@ -36,7 +37,7 @@ private enum ChannelMembersEntryStableId: Hashable { case inviteLink case membersDesc case section(Int) - + case loading var hashValue: Int { switch self { case let .peer(peerId): @@ -47,57 +48,26 @@ private enum ChannelMembersEntryStableId: Hashable { return 1 case .membersDesc: return 2 + case .loading: + return 3 case let .section(sectionId): return -(sectionId) } } - static func ==(lhs: ChannelMembersEntryStableId, rhs: ChannelMembersEntryStableId) -> Bool { - switch lhs { - case let .peer(peerId): - if case .peer(peerId) = rhs { - return true - } else { - return false - } - case .addMembers: - if case .addMembers = rhs { - return true - } else { - return false - } - case .membersDesc: - if case .membersDesc = rhs { - return true - } else { - return false - } - case .inviteLink: - if case .inviteLink = rhs { - return true - } else { - return false - } - case let .section(sectionId): - if case .section(sectionId) = rhs { - return true - } else { - return false - } - } - } } private enum ChannelMembersEntry: Identifiable, Comparable { - case peerItem(sectionId:Int, Int32, RenderedChannelParticipant, ShortPeerDeleting?, Bool) - case addMembers(sectionId:Int) - case inviteLink(sectionId:Int) - case membersDesc(sectionId:Int) + case peerItem(sectionId:Int, Int32, RenderedChannelParticipant, ShortPeerDeleting?, Bool, GeneralViewType) + case addMembers(sectionId:Int, Bool, GeneralViewType) + case inviteLink(sectionId:Int, GeneralViewType) + case membersDesc(sectionId:Int, GeneralViewType) case section(sectionId:Int) + case loading(sectionId: Int) var stableId: ChannelMembersEntryStableId { switch self { - case let .peerItem(_, _, participant, _, _): + case let .peerItem(_, _, participant, _, _, _): return .peer(participant.peer.id) case .addMembers: return .addMembers @@ -105,6 +75,8 @@ private enum ChannelMembersEntry: Identifiable, Comparable { return .inviteLink case .membersDesc: return .membersDesc + case .loading: + return .loading case let .section(sectionId): return .section(sectionId) } @@ -113,65 +85,21 @@ private enum ChannelMembersEntry: Identifiable, Comparable { var index:Int { switch self { - case let .peerItem(sectionId, index, _, _, _): + case let .peerItem(sectionId, index, _, _, _, _): return (sectionId * 1000) + Int(index) + 100 - case let .addMembers(sectionId): + case let .addMembers(sectionId, _, _): return (sectionId * 1000) + 0 - case let .inviteLink(sectionId): + case let .inviteLink(sectionId, _): return (sectionId * 1000) + 1 - case let .membersDesc(sectionId): + case let .membersDesc(sectionId, _): return (sectionId * 1000) + 2 + case let .loading(sectionId): + return (sectionId * 1000) + 4 case let .section(sectionId): return (sectionId + 1) * 1000 - sectionId } } - static func ==(lhs: ChannelMembersEntry, rhs: ChannelMembersEntry) -> Bool { - switch lhs { - case let .peerItem(_, lhsIndex, lhsParticipant, lhsEditing, lhsEnabled): - if case let .peerItem(_, rhsIndex, rhsParticipant, rhsEditing, rhsEnabled) = rhs { - if lhsIndex != rhsIndex { - return false - } - if lhsParticipant != rhsParticipant { - return false - } - if lhsEditing != rhsEditing { - return false - } - if lhsEnabled != rhsEnabled { - return false - } - return true - } else { - return false - } - case .addMembers: - if case .addMembers = rhs { - return true - } else { - return false - } - case .inviteLink: - if case .inviteLink = rhs { - return true - } else { - return false - } - case .membersDesc: - if case .membersDesc = rhs { - return true - } else { - return false - } - case let .section(sectionId): - if case .section(sectionId) = rhs { - return true - } else { - return false - } - } - } static func <(lhs: ChannelMembersEntry, rhs: ChannelMembersEntry) -> Bool { return lhs.index < rhs.index @@ -179,7 +107,7 @@ private enum ChannelMembersEntry: Identifiable, Comparable { func item(_ arguments: ChannelMembersControllerArguments, initialSize:NSSize) -> TableRowItem { switch self { - case let .peerItem(_, _, participant, editing, enabled): + case let .peerItem(_, _, participant, editing, enabled, viewType): let interactionType:ShortPeerItemInteractionType if let editing = editing { @@ -191,24 +119,26 @@ private enum ChannelMembersEntry: Identifiable, Comparable { interactionType = .plain } - return ShortPeerRowItem(initialSize, peer: participant.peer, account: arguments.account, stableId: stableId, enabled: enabled, height:44, photoSize: NSMakeSize(32, 32), drawLastSeparator: true, inset: NSEdgeInsets(left: 30, right: 30), interactionType: interactionType, generalType: .none, action: { + return ShortPeerRowItem(initialSize, peer: participant.peer, account: arguments.context.account, stableId: stableId, enabled: enabled, height:46, photoSize: NSMakeSize(32, 32), drawLastSeparator: true, inset: NSEdgeInsets(left: 30, right: 30), interactionType: interactionType, generalType: .none, viewType: viewType, action: { if case .plain = interactionType { arguments.openInfo(participant.peer) } }) - case .addMembers: - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.channelMembersAddMembers), nameStyle: blueActionButton, type: .none, action: { + case let .addMembers(_, isChannel, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: isChannel ? L10n.channelMembersAddSubscribers : L10n.channelMembersAddMembers, nameStyle: blueActionButton, type: .none, viewType: viewType, action: { arguments.addMembers() }) - case .inviteLink: - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.channelMembersInviteLink), nameStyle: blueActionButton, type: .none, action: { + case let .inviteLink(_, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.channelMembersInviteLink, nameStyle: blueActionButton, type: .none, viewType: viewType, action: { arguments.inviteLink() }) - case .membersDesc: - return GeneralTextRowItem(initialSize, stableId: stableId, text: tr(.channelMembersMembersListDesc)) + case let .membersDesc(_, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.channelMembersMembersListDesc, viewType: viewType) + case .loading: + return SearchEmptyRowItem(initialSize, stableId: stableId, isLoading: true) case .section: - return GeneralRowItem(initialSize, height: 20, stableId: stableId) + return GeneralRowItem(initialSize, height: 30, stableId: stableId, viewType: .separator) } } } @@ -251,51 +181,37 @@ private struct ChannelMembersControllerState: Equatable { } } -private func channelMembersControllerEntries(view: PeerView, account:Account, state: ChannelMembersControllerState, participants: [RenderedChannelParticipant]?) -> [ChannelMembersEntry] { +private func channelMembersControllerEntries(view: PeerView, context: AccountContext, state: ChannelMembersControllerState, participants: [RenderedChannelParticipant]?) -> [ChannelMembersEntry] { var entries: [ChannelMembersEntry] = [] + var sectionId:Int = 1 + if let participants = participants { - var sectionId:Int = 1 - - - if !participants.isEmpty { - entries.append(.section(sectionId: sectionId)) - sectionId += 1 - } + entries.append(.section(sectionId: sectionId)) + sectionId += 1 if let peer = peerViewMainPeer(view) as? TelegramChannel { - var usersManage:Bool = false - if peer.hasAdminRights(.canInviteUsers) { - entries.append(.addMembers(sectionId: sectionId)) - usersManage = true - } - if peer.hasAdminRights(.canChangeInviteLink) { - entries.append(.inviteLink(sectionId: sectionId)) - usersManage = true - } - - if usersManage { + if peer.hasPermission(.inviteMembers) { + entries.append(.addMembers(sectionId: sectionId, peer.isChannel, .singleItem)) + entries.append(.membersDesc(sectionId: sectionId, .textBottomItem)) entries.append(.section(sectionId: sectionId)) sectionId += 1 - entries.append(.membersDesc(sectionId: sectionId)) } var index: Int32 = 0 - for participant in participants.sorted(by: <) { - - + for (i, participant) in participants.sorted(by: <).enumerated() { let editable:Bool switch participant.participant { - case let .member(_, _, adminInfo, _): + case let .member(_, _, adminInfo, _, _): if let adminInfo = adminInfo { editable = adminInfo.canBeEditedByAccountPeer } else { - editable = participant.participant.peerId != account.peerId + editable = participant.participant.peerId != context.account.peerId } default: editable = false @@ -305,14 +221,15 @@ private func channelMembersControllerEntries(view: PeerView, account:Account, st if state.editing { deleting = ShortPeerDeleting(editable: editable) } - - entries.append(.peerItem(sectionId: sectionId, index, participant, deleting, state.removingPeerId != participant.peer.id)) + entries.append(.peerItem(sectionId: sectionId, index, participant, deleting, state.removingPeerId != participant.peer.id, bestGeneralViewType(participants, for: i))) index += 1 } } - - + entries.append(.section(sectionId: sectionId)) + sectionId += 1 + } else { + entries.append(.loading(sectionId: sectionId)) } return entries @@ -337,79 +254,122 @@ class ChannelMembersViewController: EditableViewController { private let removePeerDisposable:MetaDisposable = MetaDisposable() private let disposable:MetaDisposable = MetaDisposable() - - init(account:Account, peerId:PeerId) { + init(_ context: AccountContext, peerId:PeerId) { self.peerId = peerId - super.init(account) + super.init(context) + } + + override var defaultBarTitle: String { + return L10n.peerInfoSubscribers + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + window?.set(handler: { [weak self] _ -> KeyHandlerResult in + guard let `self` = self else {return .rejected} + self.searchChannelUsers() + return .invoked + }, with: self, for: .F, priority: .low, modifierFlags: [.command]) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + window?.removeAllHandlers(for: self) } override func viewDidLoad() { super.viewDidLoad() - let account = self.account + let context = self.context let peerId = self.peerId + genericView.getBackgroundColor = { + theme.colors.listBackground + } + let updateState: ((ChannelMembersControllerState) -> ChannelMembersControllerState) -> Void = { [weak self] f in if let strongSelf = self { strongSelf.statePromise.set(strongSelf.stateValue.modify { f($0) }) } } + let actionsDisposable = DisposableSet() let peersPromise = Promise<[RenderedChannelParticipant]?>(nil) - let arguments = ChannelMembersControllerArguments(account: account, removePeer: { [weak self] memberId in + let arguments = ChannelMembersControllerArguments(context: context, removePeer: { [weak self] memberId in updateState { return $0.withUpdatedRemovingPeerId(memberId) } - let applyPeers: Signal = peersPromise.get() - |> filter { $0 != nil } - |> take(1) - |> deliverOnMainQueue - |> mapToSignal { peers -> Signal in - if let peers = peers { - var updatedPeers = peers - for i in 0 ..< updatedPeers.count { - if updatedPeers[i].peer.id == memberId { - updatedPeers.remove(at: i) - break - } - } - peersPromise.set(.single(updatedPeers)) - } - - return .complete() - } - - self?.removePeerDisposable.set((removePeerMember(account: account, peerId: peerId, memberId: memberId) |> then(applyPeers) |> deliverOnMainQueue).start(error: { _ in - updateState { - return $0.withUpdatedRemovingPeerId(nil) - } - }, completed: { + self?.removePeerDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(peerId: peerId, memberId: memberId, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: 0)) |> deliverOnMainQueue).start(completed: { updateState { return $0.withUpdatedRemovingPeerId(nil) } })) }, addMembers: { - peersPromise.set(selectModalPeers(account: account, title: tr(.channelMembersSelectTitle), settings: [.contacts, .remote]) |> mapToSignal { peers -> Signal<[RenderedChannelParticipant]?, Void> in - return showModalProgress(signal: addChannelMembers(account: account, peerId: peerId, memberIds: peers) |> mapToSignal { - return channelMembers(account: account, peerId: peerId) |> map { Optional($0) } - }, for: mainWindow) - }) + let signal = selectModalPeers(window: context.window, context: context, title: L10n.channelMembersSelectTitle, settings: [.contacts, .remote, .excludeBots]) |> mapError { _ in return AddChannelMemberError.generic} |> mapToSignal { peers -> Signal in + return showModalProgress(signal: context.peerChannelMemberCategoriesContextsManager.addMembers(peerId: peerId, memberIds: peers), for: mainWindow) + } |> deliverOnMainQueue + + actionsDisposable.add(signal.start(error: { error in + let text: String + switch error { + case .notMutualContact: + text = L10n.channelInfoAddUserLeftError + case .limitExceeded: + text = L10n.channelErrorAddTooMuch + case .botDoesntSupportGroups: + text = L10n.channelBotDoesntSupportGroups + case .tooMuchBots: + text = L10n.channelTooMuchBots + case .tooMuchJoined: + text = L10n.inviteChannelsTooMuch + case .generic: + text = L10n.unknownError + case let .bot(memberId): + let _ = (context.account.postbox.transaction { transaction in + return transaction.getPeer(peerId) + } + |> deliverOnMainQueue).start(next: { peer in + guard let peer = peer as? TelegramChannel else { + alert(for: context.window, info: L10n.unknownError) + return + } + if peer.hasPermission(.addAdmins) { + confirm(for: context.window, information: L10n.channelAddBotErrorHaveRights, okTitle: L10n.channelAddBotAsAdmin, successHandler: { _ in + showModal(with: ChannelAdminController(context, peerId: peerId, adminId: memberId, initialParticipant: nil, updated: { _ in }, upgradedToSupergroup: { _, f in f() }), for: context.window) + }) + } else { + alert(for: context.window, info: L10n.channelAddBotErrorHaveRights) + } + }) + return + case .restricted: + text = L10n.channelErrorAddBlocked + } + alert(for: mainWindow, info: text) + }, completed: { + _ = showModalSuccess(for: mainWindow, icon: theme.icons.successModalProgress, delay: 1.0).start() + })) }, inviteLink: { [weak self] in if let strongSelf = self { - strongSelf.navigationController?.push(LinkInvationController(account: strongSelf.account, peerId: strongSelf.peerId)) + strongSelf.navigationController?.push(LinkInvationController(strongSelf.context, peerId: strongSelf.peerId)) } }, openInfo: { [weak self] peer in - self?.navigationController?.push(PeerInfoController(account: account, peer: peer)) + self?.navigationController?.push(PeerInfoController(context: context, peerId: peer.id)) }) - let peerView = account.viewTracker.peerView(peerId) + let peerView = context.account.viewTracker.peerView(peerId) + + + let (disposable, loadMoreControl) = context.peerChannelMemberCategoriesContextsManager.recent(peerId: peerId, updated: { state in + peersPromise.set(.single(state.list)) + }) + actionsDisposable.add(disposable) + - let peersSignal: Signal<[RenderedChannelParticipant]?, NoError> = .single(nil) |> then(channelMembers(account: account, peerId: peerId) |> map { Optional($0) }) - peersPromise.set(peersSignal) let initialSize = atomicSize let previousEntries:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) @@ -418,16 +378,29 @@ class ChannelMembersViewController: EditableViewController { let signal = combineLatest(statePromise.get(), peerView, peersPromise.get(), appearanceSignal) |> deliverOnMainQueue |> map { state, view, peers, appearance -> TableUpdateTransition in - let entries = channelMembersControllerEntries(view: view, account: account, state: state, participants: peers).map{AppearanceWrapperEntry(entry: $0, appearance: appearance)} + let entries = channelMembersControllerEntries(view: view, context: context, state: state, participants: peers).map{AppearanceWrapperEntry(entry: $0, appearance: appearance)} return prepareTransition(left: previousEntries.swap(entries), right: entries, initialSize: initialSize.modify{$0}, arguments: arguments) + } |> afterDisposed { + actionsDisposable.dispose() } - disposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] transition in + self.disposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] transition in if let strongSelf = self { strongSelf.genericView.merge(with: transition) strongSelf.readyOnce() } })) + + genericView.setScrollHandler { position in + if let loadMoreControl = loadMoreControl { + switch position.direction { + case .bottom: + context.peerChannelMemberCategoriesContextsManager.loadMore(peerId: peerId, control: loadMoreControl) + default: + break + } + } + } } deinit { @@ -440,4 +413,18 @@ class ChannelMembersViewController: EditableViewController { self.statePromise.set(stateValue.modify({$0.withUpdatedEditing(state == .Edit)})) } + private func searchChannelUsers() { + _ = (selectModalPeers(window: context.window, context: context, title: L10n.selectPeersTitleSearchMembers, behavior: SelectChannelMembersBehavior(peerId: peerId, peerChannelMemberContextsManager: context.peerChannelMemberCategoriesContextsManager, limit: 1, settings: [])) |> deliverOnMainQueue |> map {$0.first}).start(next: { [weak self] peerId in + if let peerId = peerId, let context = self?.context { + self?.navigationController?.push(PeerInfoController(context: context, peerId: peerId)) + } + }) + } + + override func getCenterBarViewOnce() -> TitledBarView { + return SearchTitleBarView(controller: self, title:.initialize(string: defaultBarTitle, color: theme.colors.text, font: .medium(.title)), handler: { [weak self] in + self?.searchChannelUsers() + }) + } + } diff --git a/Telegram-Mac/ChannelOverviewStatsRowItem.swift b/Telegram-Mac/ChannelOverviewStatsRowItem.swift new file mode 100644 index 0000000000..52ca9cdce4 --- /dev/null +++ b/Telegram-Mac/ChannelOverviewStatsRowItem.swift @@ -0,0 +1,247 @@ +// +// ChannelOverviewStatsRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 28.02.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import Postbox +import SwiftSignalKit +import TelegramCore + +import GraphCore + +struct ChannelOverviewItem : Equatable { + let title: String + let value: NSAttributedString +} + +extension StatsValue { + var attributedString: NSAttributedString { + + let deltaValue = self.current - self.previous + let deltaCompact = abs(Int(deltaValue)).prettyNumber + var delta = deltaValue == 0 ? "" : deltaValue > 0 ? " +\(deltaCompact)" : " -\(deltaCompact)" + var deltaPercentage = 0.0 + if self.previous > 0.0, deltaValue != 0 { + deltaPercentage = abs(deltaValue / self.previous) + delta += String(format: " (%.02f%%)", deltaPercentage * 100) + } + + let attr = NSMutableAttributedString() + + _ = attr.append(string: Int(self.current).prettyNumber, color: theme.colors.text, font: .medium(.header)) + if !delta.isEmpty { + _ = attr.append(string: delta, color: deltaValue < 0 ? theme.colors.redUI : theme.colors.greenUI, font: .normal(.small)) + } + + return attr + + } +} +extension StatsPercentValue { + var attributedString: NSAttributedString { + let attr = NSMutableAttributedString() + + let deltaPercentage = abs(self.value / self.total) + + _ = attr.append(string: String(format: "%.02f%%", deltaPercentage * 100), color: theme.colors.text, font: .medium(.header)) + + return attr + } +} + +private struct ChannelOverviewLayoutItem { + let title: TextViewLayout + let name: TextViewLayout + + init(item: ChannelOverviewItem) { + self.name = TextViewLayout(.initialize(string: item.title, color: theme.colors.grayText, font: .normal(.text)), maximumNumberOfLines: 1) + + self.title = TextViewLayout(item.value, maximumNumberOfLines: 1) + } + + func measure(_ width: CGFloat) { + self.title.measure(width: width) + self.name.measure(width: width) + } + + var size: NSSize { + return NSMakeSize(max(self.title.layoutSize.width, self.name.layoutSize.width), title.layoutSize.height + 3 + name.layoutSize.height) + } +} + +class ChannelOverviewStatsRowItem: GeneralRowItem { + + + fileprivate let layoutItems:[ChannelOverviewLayoutItem] + + + init(_ initialSize: NSSize, stableId: AnyHashable, items: [ChannelOverviewItem], viewType: GeneralViewType) { + self.layoutItems = items.map { + return ChannelOverviewLayoutItem(item: $0) + } + super.init(initialSize, stableId: stableId, viewType: viewType) + + _ = makeSize(initialSize.width) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + + for item in layoutItems { + item.measure((blockWidth - viewType.innerInset.left - viewType.innerInset.right - 20) / 2) + } + + return true + } + + override var height: CGFloat { + + var height: CGFloat = 0 + + for (i, item) in layoutItems.enumerated() { + if i % 2 == 0 { + height += item.size.height + if i < layoutItems.count - 2 { + height += 10 + } + } + } + + return height + viewType.innerInset.bottom + viewType.innerInset.top + } + + override func viewClass() -> AnyClass { + return ChannelOverviewStatsRowView.self + } +} + +private final class ChannelOverviewLayoutView : View { + private let titleView: TextView = TextView() + private let nameView: TextView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(titleView) + addSubview(nameView) + + nameView.isSelectable = false + nameView.userInteractionEnabled = false + } + + override func layout() { + super.layout() + self.titleView.setFrameOrigin(.zero) + self.nameView.setFrameOrigin(NSMakePoint(0, self.titleView.frame.maxY + 3)) + } + + func update(_ item: ChannelOverviewLayoutItem) { + self.titleView.update(item.title) + self.nameView.update(item.name) + needsLayout = true + } + + func updateColors() { + + let backgroundColor = theme.colors.background + + self.backgroundColor = backgroundColor + self.titleView.backgroundColor = backgroundColor + self.nameView.backgroundColor = backgroundColor + } + + required init?(coder: NSCoder) { + fatalError("init(coder :) has not been implemented") + } +} + +private final class ChannelOverviewStatsRowView : TableRowView { + private let containerView = GeneralRowContainerView(frame: NSZeroRect) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(self.containerView) + } + + override func layout() { + super.layout() + guard let item = item as? GeneralRowItem else { + return + } + + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) + self.containerView.setCorners(item.viewType.corners) + + + var point: CGPoint = NSMakePoint(item.viewType.innerInset.left, item.viewType.innerInset.top) + for (i, subview) in self.containerView.subviews.enumerated() { + subview.setFrameOrigin(point) + if i < self.containerView.subviews.count - 1 { + if (i + 1) % 2 == 0 { + point.x = item.viewType.innerInset.left + point.y += self.containerView.subviews[i + 1].frame.height + if i < self.containerView.subviews.count - 1 { + point.y += 10 + } + } else { + var width: CGFloat = self.containerView.subviews[i + 1].frame.width + if i > 1 { + width = max(width, self.containerView.subviews[i - 1].frame.width) + } + if i + 3 < self.containerView.subviews.count { + width = max(width, self.containerView.subviews[i + 3].frame.width) + } + + point.x = self.containerView.frame.width - width - item.viewType.innerInset.right + + } + } + + } + } + + override var backdorColor: NSColor { + return theme.colors.background + } + + override func updateColors() { + guard let item = item as? GeneralRowItem else { + return + } + self.backgroundColor = item.viewType.rowBackground + self.containerView.backgroundColor = backdorColor + + for subview in self.containerView.subviews { + (subview as? ChannelOverviewLayoutView)?.updateColors() + } + } + + + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + + guard let item = item as? ChannelOverviewStatsRowItem else { + return + } + self.containerView.removeAllSubviews() + + for item in item.layoutItems { + let view = ChannelOverviewLayoutView(frame: CGRect(origin: .zero, size: item.size)) + view.update(item) + self.containerView.addSubview(view) + } + needsLayout = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + +} + + diff --git a/Telegram-Mac/ChannelPermissionsController.swift b/Telegram-Mac/ChannelPermissionsController.swift new file mode 100644 index 0000000000..0bfa287c3e --- /dev/null +++ b/Telegram-Mac/ChannelPermissionsController.swift @@ -0,0 +1,822 @@ +// +// ChannelPermissionsController.swift +// Telegram +// +// Created by Mikhail Filimonov on 03/01/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa + +import Foundation +import TGUIKit +import SwiftSignalKit +import Postbox +import TelegramCore + + +private final class ChannelPermissionsControllerArguments { + let context: AccountContext + + let updatePermission: (TelegramChatBannedRightsFlags, Bool) -> Void + let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void + let addPeer: () -> Void + let removePeer: (PeerId) -> Void + let openPeer: (ChannelParticipant) -> Void + let openPeerInfo: (Peer) -> Void + let openKicked: () -> Void + let presentRestrictedPublicGroupPermissionsAlert: () -> Void + let updateSlowMode:(Int32)->Void + let convert:()->Void + init(context: AccountContext, updatePermission: @escaping (TelegramChatBannedRightsFlags, Bool) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addPeer: @escaping () -> Void, removePeer: @escaping (PeerId) -> Void, openPeer: @escaping (ChannelParticipant) -> Void, openPeerInfo: @escaping (Peer) -> Void, openKicked: @escaping () -> Void, presentRestrictedPublicGroupPermissionsAlert: @escaping() -> Void, updateSlowMode:@escaping(Int32)->Void, convert: @escaping()->Void) { + self.context = context + self.updatePermission = updatePermission + self.addPeer = addPeer + self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions + self.removePeer = removePeer + self.openPeer = openPeer + self.openPeerInfo = openPeerInfo + self.openKicked = openKicked + self.presentRestrictedPublicGroupPermissionsAlert = presentRestrictedPublicGroupPermissionsAlert + self.updateSlowMode = updateSlowMode + self.convert = convert + } +} + +private enum ChannelPermissionsSection: Int32 { + case permissions + case kicked + case exceptions +} + +private enum ChannelPermissionsEntryStableId: Hashable { + case index(Int32) + case peer(PeerId) + case section(Int32) + case permission(Int32) +} + +private enum ChannelPermissionsEntry: TableItemListNodeEntry { + case section(Int32) + case permissionsHeader(Int32, Int32, String, GeneralViewType) + case permission(Int32, Int32, String, Bool, TelegramChatBannedRightsFlags, Bool?, GeneralViewType) + case convertHeader(Int32, Int32, GeneralViewType) + case convert(Int32, Int32, GeneralViewType) + case convertDesc(Int32, Int32, GeneralViewType) + case kicked(Int32, Int32, String, String, GeneralViewType) + case exceptionsHeader(Int32, Int32, String, GeneralViewType) + case add(Int32, Int32, String, GeneralViewType) + case peerItem(Int32, Int32, RenderedChannelParticipant, ShortPeerDeleting?, Bool, Bool, TelegramChatBannedRightsFlags, GeneralViewType) + case slowModeHeader(Int32, GeneralViewType) + case slowMode(Int32, Int32?, GeneralViewType) + case slowDesc(Int32, Int32?, GeneralViewType) + var stableId: ChannelPermissionsEntryStableId { + switch self { + case .permissionsHeader: + return .index(0) + case let .permission(_, index, _, _, _, _, _): + return .permission(1 + index) + case .convertHeader: + return .index(1000) + case .convert: + return .index(1001) + case .convertDesc: + return .index(1002) + case .kicked: + return .index(1003) + case .slowModeHeader: + return .index(1004) + case .slowMode: + return .index(1005) + case .slowDesc: + return .index(1006) + case .exceptionsHeader: + return .index(1007) + case .add: + return .index(1008) + case let .section(section): + return .section(section) + case let .peerItem( _, _, participant, _, _, _, _, _): + return .peer(participant.peer.id) + } + } + + var index: Int32 { + switch self { + case let .permissionsHeader(section, index, _, _): + return (section * 1000) + index + case let .permission(section, index, _, _, _, _, _): + return (section * 1000) + index + case let .kicked(section, index, _, _, _): + return (section * 1000) + index + case let .convertHeader(section, index, _): + return (section * 1000) + index + case let .convert(section, index, _): + return (section * 1000) + index + case let .convertDesc(section, index, _): + return (section * 1000) + index + case let .slowMode(section, _, _): + return (section * 1000) + 1 + case let .slowModeHeader(section, _): + return (section * 1000) + 2 + case let .slowDesc(section, _, _): + return (section * 1000) + 3 + case let .exceptionsHeader(section, index, _, _): + return (section * 1000) + index + case let .add(section, index, _, _): + return (section * 1000) + index + case let .section(section): + return (section + 1) * 1000 - section + case let .peerItem(section, index, _, _, _, _, _, _): + return (section * 1000) + index + } + } + + static func <(lhs: ChannelPermissionsEntry, rhs: ChannelPermissionsEntry) -> Bool { + return lhs.index < rhs.index + } + + + + func item(_ arguments: ChannelPermissionsControllerArguments, initialSize: NSSize) -> TableRowItem { + switch self { + case let .permissionsHeader(_, _, text, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, viewType: viewType) + case let .permission(_, _, title, value, rights, enabled, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: title, type: .switchable(value), viewType: viewType, action: { + if let _ = enabled { + arguments.updatePermission(rights, !value) + } else { + arguments.presentRestrictedPublicGroupPermissionsAlert() + } + }, enabled: enabled ?? true, switchAppearance: SwitchViewAppearance(backgroundColor: theme.colors.background, stateOnColor: enabled == true ? theme.colors.accent : theme.colors.accent.withAlphaComponent(0.6), stateOffColor: enabled == true ? theme.colors.redUI : theme.colors.redUI.withAlphaComponent(0.6), disabledColor: .grayBackground, borderColor: .clear), autoswitch: false) + case let .kicked(_, _, text, value, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: text, type: .nextContext(value), viewType: viewType, action: { + arguments.openKicked() + }) + case let .convertHeader(_, _, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.groupInfoPermissionsBroadcastTitle.uppercased(), viewType: viewType) + case let .convert(_, _, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.groupInfoPermissionsBroadcastConvert, nameStyle: blueActionButton, type: .none, viewType: viewType, action: { + arguments.convert() + }) + case let .convertDesc(_, _, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.groupInfoPermissionsBroadcastConvertInfo(Formatter.withSeparator.string(from: .init(value: arguments.context.limitConfiguration.maxSupergroupMemberCount))!), viewType: viewType) + case let .exceptionsHeader(_, _, text, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, viewType: viewType) + case let .add(_, _, text, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: text, nameStyle: blueActionButton, type: .none, viewType: viewType, action: { () in + arguments.addPeer() + }, thumb: GeneralThumbAdditional(thumb: theme.icons.peerInfoAddMember, textInset: 52, thumbInset: 5)) + + case let .peerItem(_, _, participant, _, enabled, canOpen, defaultBannedRights, viewType): + var text: String? + switch participant.participant { + case let .member(_, _, _, banInfo, _): + var exceptionsString = "" + if let banInfo = banInfo { + for rights in allGroupPermissionList { + if !defaultBannedRights.contains(rights) && banInfo.rights.flags.contains(rights) { + if !exceptionsString.isEmpty { + exceptionsString.append(", ") + } + exceptionsString.append(compactStringForGroupPermission(right: rights)) + } + } + text = exceptionsString + } + default: + break + } + + return ShortPeerRowItem(initialSize, peer: participant.peer, account: arguments.context.account, stableId: stableId, enabled: enabled, status: text, inset: NSEdgeInsetsMake(0, 30, 0, 30), viewType: viewType, action: { + if canOpen { + arguments.openPeer(participant.participant) + } else { + arguments.openPeerInfo(participant.peer) + } + }) + case let .slowModeHeader(_, viewType): + return GeneralTextRowItem(initialSize, text: L10n.channelPermissionsSlowModeHeader, viewType: viewType) + case let .slowMode(_, timeout, viewType): + let list:[Int32] = [0, 10, 30, 60, 300, 900, 3600] + let titles: [String] = [L10n.channelPermissionsSlowModeTimeoutOff, + L10n.channelPermissionsSlowModeTimeout10s, + L10n.channelPermissionsSlowModeTimeout30s, + L10n.channelPermissionsSlowModeTimeout1m, L10n.channelPermissionsSlowModeTimeout5m, + L10n.channelPermissionsSlowModeTimeout15m, + L10n.channelPermissionsSlowModeTimeout1h] + return SelectSizeRowItem(initialSize, stableId: stableId, current: timeout ?? 0, sizes: list, hasMarkers: false, titles: titles, viewType: viewType, selectAction: { index in + arguments.updateSlowMode(list[index]) + }) + case let .slowDesc(_, timeout, viewType): + let text: String + if let timeout = timeout, timeout > 0 { + text = L10n.channelPermissionsSlowModeTextSelected(autoremoveLocalized(Int(timeout))) + } else { + text = L10n.channelPermissionsSlowModeTextOff + } + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, viewType: viewType) + case .section: + return GeneralRowItem(initialSize, height: 30, stableId: stableId, viewType: .separator) + } + } +} + +private struct ChannelPermissionsControllerState: Equatable { + var peerIdWithRevealedOptions: PeerId? + var removingPeerId: PeerId? + var searchingMembers: Bool = false + var modifiedRightsFlags: TelegramChatBannedRightsFlags? +} + +func stringForGroupPermission(right: TelegramChatBannedRightsFlags) -> String { + if right.contains(.banSendMessages) { + return L10n.channelBanUserPermissionSendMessages + } else if right.contains(.banSendMedia) { + return L10n.channelBanUserPermissionSendMedia + } else if right.contains(.banSendGifs) { + return L10n.channelBanUserPermissionSendStickersAndGifs + } else if right.contains(.banEmbedLinks) { + return L10n.channelBanUserPermissionEmbedLinks + } else if right.contains(.banSendPolls) { + return L10n.channelBanUserPermissionSendPolls + } else if right.contains(.banChangeInfo) { + return L10n.channelBanUserPermissionChangeGroupInfo + } else if right.contains(.banAddMembers) { + return L10n.channelBanUserPermissionAddMembers + } else if right.contains(.banPinMessages) { + return L10n.channelEditAdminPermissionPinMessages + } else { + return "" + } +} + +func compactStringForGroupPermission(right: TelegramChatBannedRightsFlags) -> String { + if right.contains(.banSendMessages) { + return L10n.groupPermissionNoSendMessages + } else if right.contains(.banSendMedia) { + return L10n.groupPermissionNoSendMedia + } else if right.contains(.banSendGifs) { + return L10n.groupPermissionNoSendGifs + } else if right.contains(.banEmbedLinks) { + return L10n.groupPermissionNoSendLinks + } else if right.contains(.banSendPolls) { + return L10n.groupPermissionNoSendPolls + } else if right.contains(.banChangeInfo) { + return L10n.groupPermissionNoChangeInfo + } else if right.contains(.banAddMembers) { + return L10n.groupPermissionNoAddMembers + } else if right.contains(.banPinMessages) { + return L10n.groupPermissionNoPinMessages + } else { + return "" + } +} + +let allGroupPermissionList: [TelegramChatBannedRightsFlags] = [ + .banSendMessages, + .banSendMedia, + .banSendGifs, + .banEmbedLinks, + .banSendPolls, + .banAddMembers, + .banPinMessages, + .banChangeInfo +] + +let publicGroupRestrictedPermissions: TelegramChatBannedRightsFlags = [ + .banPinMessages, + .banChangeInfo +] + + +func groupPermissionDependencies(_ right: TelegramChatBannedRightsFlags) -> TelegramChatBannedRightsFlags { + if right.contains(.banSendMedia) { + return [.banSendMessages] + } else if right.contains(.banSendGifs) { + return [.banSendMessages] + } else if right.contains(.banEmbedLinks) { + return [.banSendMessages] + } else if right.contains(.banSendPolls) { + return [.banSendMessages] + } else if right.contains(.banChangeInfo) { + return [] + } else if right.contains(.banAddMembers) { + return [] + } else if right.contains(.banPinMessages) { + return [] + } else { + return [] + } +} + +private func completeRights(_ flags: TelegramChatBannedRightsFlags) -> TelegramChatBannedRightsFlags { + var result = flags + result.remove(.banReadMessages) + if result.contains(.banSendGifs) { + result.insert(.banSendStickers) + result.insert(.banSendGifs) + result.insert(.banSendGames) + result.insert(.banSendInline) + } else { + result.remove(.banSendStickers) + result.remove(.banSendGifs) + result.remove(.banSendGames) + result.remove(.banSendInline) + } + return result +} + +private func channelPermissionsControllerEntries(view: PeerView, state: ChannelPermissionsControllerState, participants: [RenderedChannelParticipant]?, limits: LimitsConfiguration) -> [ChannelPermissionsEntry] { + var entries: [ChannelPermissionsEntry] = [] + + var sectionId: Int32 = 0 + var index: Int32 = 0 + + entries.append(.section(sectionId)) + sectionId += 1 + + + if let channel = view.peers[view.peerId] as? TelegramChannel, let participants = participants, let cachedData = view.cachedData as? CachedChannelData, let defaultBannedRights = channel.defaultBannedRights { + + + let effectiveRightsFlags: TelegramChatBannedRightsFlags + if let modifiedRightsFlags = state.modifiedRightsFlags { + effectiveRightsFlags = modifiedRightsFlags + } else { + effectiveRightsFlags = defaultBannedRights.flags + } + + var permissionList = allGroupPermissionList + if channel.flags.contains(.isGigagroup) { + permissionList = [.banAddMembers] + } + + + entries.append(.permissionsHeader(sectionId, index, L10n.groupInfoPermissionsSectionTitle, .textTopItem)) + index += 1 + for (i, rights) in permissionList.enumerated() { + var enabled: Bool? = true + if channel.addressName != nil && publicGroupRestrictedPermissions.contains(rights) { + enabled = nil + } + entries.append(.permission(sectionId, index, stringForGroupPermission(right: rights), !effectiveRightsFlags.contains(rights), rights, enabled, bestGeneralViewType(permissionList, for: i))) + index += 1 + } + + entries.append(.section(sectionId)) + sectionId += 1 + + if let members = cachedData.participantsSummary.memberCount, limits.maxSupergroupMemberCount - members < 1000 { + if channel.groupAccess.isCreator && !channel.flags.contains(.isGigagroup) { + entries.append(.convertHeader(sectionId, index, .textTopItem)) + index += 1 + entries.append(.convert(sectionId, index, .singleItem)) + index += 1 + entries.append(.convertDesc(sectionId, index, .textBottomItem)) + index += 1 + + entries.append(.section(sectionId)) + sectionId += 1 + } + } + + if !channel.flags.contains(.isGigagroup) { + entries.append(.slowModeHeader(sectionId, .textTopItem)) + entries.append(.slowMode(sectionId, cachedData.slowModeTimeout, .singleItem)) + entries.append(.slowDesc(sectionId, cachedData.slowModeTimeout, .textBottomItem)) + + entries.append(.section(sectionId)) + sectionId += 1 + } + + + + entries.append(.kicked(sectionId, index, L10n.groupInfoPermissionsRemoved, cachedData.participantsSummary.kickedCount.flatMap({ "\($0 > 0 ? "\($0)" : "")" }) ?? "", .singleItem)) + index += 1 + + + if !channel.flags.contains(.isGigagroup) { + entries.append(.section(sectionId)) + sectionId += 1 + + + entries.append(.exceptionsHeader(sectionId, index, L10n.groupInfoPermissionsExceptions, .textTopItem)) + index += 1 + + entries.append(.add(sectionId, index, L10n.groupInfoPermissionsAddException, participants.isEmpty ? .singleItem : .firstItem)) + index += 1 + for (i, participant) in participants.enumerated() { + entries.append(.peerItem(sectionId, index, participant, ShortPeerDeleting(editable: true), state.removingPeerId != participant.peer.id, true, effectiveRightsFlags, i == 0 ? .innerItem : bestGeneralViewType(participants, for: i))) + index += 1 + } + } + + } else if let group = view.peers[view.peerId] as? TelegramGroup, let _ = view.cachedData as? CachedGroupData, let defaultBannedRights = group.defaultBannedRights { + let effectiveRightsFlags: TelegramChatBannedRightsFlags + if let modifiedRightsFlags = state.modifiedRightsFlags { + effectiveRightsFlags = modifiedRightsFlags + } else { + effectiveRightsFlags = defaultBannedRights.flags + } + + entries.append(.permissionsHeader(sectionId, index, L10n.groupInfoPermissionsSectionTitle, .textTopItem)) + index += 1 + + for (i, rights) in allGroupPermissionList.enumerated() { + entries.append(.permission(sectionId, index, stringForGroupPermission(right: rights), !effectiveRightsFlags.contains(rights), rights, true, bestGeneralViewType(allGroupPermissionList, for: i))) + index += 1 + } + + entries.append(.section(sectionId)) + sectionId += 1 + + entries.append(.slowModeHeader(sectionId, .textTopItem)) + entries.append(.slowMode(sectionId, nil, .singleItem)) + entries.append(.slowDesc(sectionId, nil, .textBottomItem)) + + entries.append(.section(sectionId)) + sectionId += 1 + + entries.append(.exceptionsHeader(sectionId, index, L10n.groupInfoPermissionsExceptions, .textTopItem)) + index += 1 + entries.append(.add(sectionId, index, L10n.groupInfoPermissionsAddException, .singleItem)) + index += 1 + + entries.append(.section(sectionId)) + sectionId += 1 + } + + entries.append(.section(sectionId)) + sectionId += 1 + + return entries +} +fileprivate func prepareTransition(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], initialSize:NSSize, arguments:ChannelPermissionsControllerArguments) -> TableUpdateTransition { + + let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in + return entry.entry.item(arguments, initialSize: initialSize) + } + + return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true) +} + + +final class ChannelPermissionsController : TableViewController { + + private let peerId: PeerId + private let disposable = MetaDisposable() + init(_ context: AccountContext, peerId: PeerId) { + self.peerId = peerId + super.init(context) + } + + fileprivate let interfaceFullReady: Promise = Promise() + + deinit { + disposable.dispose() + } + + override func viewDidLoad() { + super.viewDidLoad() + + let peerId = self.peerId + let context = self.context + + let statePromise = ValuePromise(ChannelPermissionsControllerState(), ignoreRepeated: true) + let stateValue = Atomic(value: ChannelPermissionsControllerState()) + let updateState: ((ChannelPermissionsControllerState) -> ChannelPermissionsControllerState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + var stopMerging: Bool = false + + let actionsDisposable = DisposableSet() + + let updateBannedDisposable = MetaDisposable() + actionsDisposable.add(updateBannedDisposable) + + let removePeerDisposable = MetaDisposable() + actionsDisposable.add(removePeerDisposable) + + + var upgradedToSupergroupImpl: ((PeerId, @escaping () -> Void) -> Void)? + + let upgradedToSupergroup: (PeerId, @escaping () -> Void) -> Void = { upgradedPeerId, f in + upgradedToSupergroupImpl?(upgradedPeerId, f) + } + + + let restrict:(ChannelParticipant, Bool) -> Void = { participant, unban in + showModal(with: RestrictedModalViewController(context, peerId: peerId, memberId: participant.peerId, initialParticipant: participant, updated: { updatedRights in + switch participant { + case let .member(memberId, _, _, _, _): + + + let signal: Signal + + if peerId.namespace == Namespaces.Peer.CloudGroup { + stopMerging = true + signal = context.engine.peers.convertGroupToSupergroup(peerId: peerId) + |> map(Optional.init) + |> mapToSignal { upgradedPeerId -> Signal in + guard let upgradedPeerId = upgradedPeerId else { + return .single(nil) + } + return context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(peerId: upgradedPeerId, memberId: memberId, bannedRights: updatedRights) + |> castError(ConvertGroupToSupergroupError.self) + |> mapToSignal { _ -> Signal in + return .complete() + } + |> then(.single(upgradedPeerId) |> castError(ConvertGroupToSupergroupError.self)) + } + |> deliverOnMainQueue + } else { + signal = context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(peerId: peerId, memberId: memberId, bannedRights: updatedRights) + |> map {_ in return nil} + |> castError(ConvertGroupToSupergroupError.self) + |> deliverOnMainQueue + } + + updateBannedDisposable.set(showModalProgress(signal: signal, for: context.window).start(next: { upgradedPeerId in + if let upgradedPeerId = upgradedPeerId { + upgradedToSupergroup(upgradedPeerId, { + + }) + } + }, error: { error in + switch error { + case .tooManyChannels: + showInactiveChannels(context: context, source: .upgrade) + case .generic: + alert(for: context.window, info: L10n.unknownError) + } + })) + default: + break + } + + + }), for: context.window) + } + + let peersPromise = Promise<[RenderedChannelParticipant]?>(nil) + let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.restricted(peerId: peerId, updated: { state in + peersPromise.set(.single(state.list)) + }) + actionsDisposable.add(disposable) + + let updateDefaultRightsDisposable = MetaDisposable() + actionsDisposable.add(updateDefaultRightsDisposable) + + let peerView = Promise() + peerView.set(context.account.viewTracker.peerView(peerId)) + + let arguments = ChannelPermissionsControllerArguments(context: context, updatePermission: { rights, value in + let _ = (peerView.get() + |> take(1) + |> deliverOnMainQueue).start(next: { view in + if let channel = view.peers[peerId] as? TelegramChannel, let _ = view.cachedData as? CachedChannelData { + updateState { state in + var state = state + var effectiveRightsFlags: TelegramChatBannedRightsFlags + if let modifiedRightsFlags = state.modifiedRightsFlags { + effectiveRightsFlags = modifiedRightsFlags + } else if let defaultBannedRightsFlags = channel.defaultBannedRights?.flags { + effectiveRightsFlags = defaultBannedRightsFlags + } else { + effectiveRightsFlags = TelegramChatBannedRightsFlags() + } + if value { + effectiveRightsFlags.remove(rights) + effectiveRightsFlags = effectiveRightsFlags.subtracting(groupPermissionDependencies(rights)) + } else { + effectiveRightsFlags.insert(rights) + for right in allGroupPermissionList { + if groupPermissionDependencies(right).contains(rights) { + effectiveRightsFlags.insert(right) + } + } + } + state.modifiedRightsFlags = effectiveRightsFlags + return state + } + let state = stateValue.with { $0 } + if let modifiedRightsFlags = state.modifiedRightsFlags { + updateDefaultRightsDisposable.set((context.engine.peers.updateDefaultChannelMemberBannedRights(peerId: peerId, rights: TelegramChatBannedRights(flags: completeRights(modifiedRightsFlags), untilDate: Int32.max)) + |> deliverOnMainQueue).start()) + } + } else if let group = view.peers[peerId] as? TelegramGroup, let _ = view.cachedData as? CachedGroupData { + updateState { state in + var state = state + var effectiveRightsFlags: TelegramChatBannedRightsFlags + if let modifiedRightsFlags = state.modifiedRightsFlags { + effectiveRightsFlags = modifiedRightsFlags + } else if let defaultBannedRightsFlags = group.defaultBannedRights?.flags { + effectiveRightsFlags = defaultBannedRightsFlags + } else { + effectiveRightsFlags = TelegramChatBannedRightsFlags() + } + if value { + effectiveRightsFlags.remove(rights) + effectiveRightsFlags = effectiveRightsFlags.subtracting(groupPermissionDependencies(rights)) + } else { + effectiveRightsFlags.insert(rights) + for right in allGroupPermissionList { + if groupPermissionDependencies(right).contains(rights) { + effectiveRightsFlags.insert(right) + } + } + } + state.modifiedRightsFlags = effectiveRightsFlags + return state + } + + let state = stateValue.with { $0 } + if let modifiedRightsFlags = state.modifiedRightsFlags { + updateDefaultRightsDisposable.set((context.engine.peers.updateDefaultChannelMemberBannedRights(peerId: peerId, rights: TelegramChatBannedRights(flags: completeRights(modifiedRightsFlags), untilDate: Int32.max)) + |> deliverOnMainQueue).start()) + } + } + }) + }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in + updateState { state in + var state = state + if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) { + state.peerIdWithRevealedOptions = peerId + } + return state + } + }, addPeer: { + let behavior = peerId.namespace == Namespaces.Peer.CloudGroup ? SelectGroupMembersBehavior(peerId: peerId, limit: 1) : SelectChannelMembersBehavior(peerId: peerId, peerChannelMemberContextsManager: context.peerChannelMemberCategoriesContextsManager, limit: 1) + + _ = (selectModalPeers(window: context.window, context: context, title: L10n.channelBlacklistSelectNewUserTitle, limit: 1, behavior: behavior, confirmation: { peerIds in + if let peerId = peerIds.first { + var adminError:Bool = false + if let participant = behavior.participants[peerId] { + if case let .member(_, _, adminInfo, _, _) = participant.participant { + if let adminInfo = adminInfo { + if !adminInfo.canBeEditedByAccountPeer && adminInfo.promotedBy != context.account.peerId { + adminError = true + } + } + } else { + adminError = true + } + } + if adminError { + alert(for: mainWindow, info: L10n.channelBlacklistDemoteAdminError) + return .single(false) + } + } + return .single(true) + }) |> map {$0.first} |> filter {$0 != nil} |> map {$0!}).start(next: { memberId in + + var participant:RenderedChannelParticipant? + if let p = behavior.participants[memberId] { + participant = p + } else if let temporary = behavior.result[memberId] { + participant = RenderedChannelParticipant(participant: .member(id: memberId, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil), peer: temporary.peer, peers: [memberId: temporary.peer], presences: temporary.presence != nil ? [memberId: temporary.presence!] : [:]) + } + if let participant = participant { + restrict(participant.participant, false) + } + }) + + }, removePeer: { memberId in + updateState { state in + var state = state + state.removingPeerId = memberId + return state + } + + removePeerDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(peerId: peerId, memberId: memberId, bannedRights: nil) + |> deliverOnMainQueue).start(error: { _ in + updateState { state in + var state = state + state.removingPeerId = nil + return state + } + }, completed: { + updateState { state in + var state = state + state.removingPeerId = nil + return state + } + })) + }, openPeer: { participant in + restrict(participant, true) + }, openPeerInfo: { [weak self] peer in + self?.navigationController?.push(PeerInfoController(context: context, peerId: peer.id)) + }, openKicked: { [weak self] in + self?.navigationController?.push(ChannelBlacklistViewController(context, peerId: peerId)) + }, presentRestrictedPublicGroupPermissionsAlert: { + alert(for: mainWindow, info: L10n.groupPermissionNotAvailableInPublicGroups) + }, updateSlowMode: { value in + let signal: Signal + + if peerId.namespace == Namespaces.Peer.CloudGroup { + stopMerging = true + signal = context.engine.peers.convertGroupToSupergroup(peerId: peerId) + |> map(Optional.init) + |> mapToSignal { upgradedPeerId -> Signal in + guard let upgradedPeerId = upgradedPeerId else { + return .fail(.generic) + } + return context.engine.peers.updateChannelSlowModeInteractively(peerId: upgradedPeerId, timeout: value) + |> map { _ in return Optional(upgradedPeerId) } + |> mapError { _ in + return ConvertGroupToSupergroupError.generic + } + } + + } else { + signal = context.engine.peers.updateChannelSlowModeInteractively(peerId: peerId, timeout: value) + |> mapError { _ in return ConvertGroupToSupergroupError.generic } + |> map { _ in return nil } + } + + _ = showModalProgress(signal: signal |> deliverOnMainQueue, for: context.window).start(next: { upgradedPeerId in + if let upgradedPeerId = upgradedPeerId { + upgradedToSupergroup(upgradedPeerId, { + + }) + } + }, error: { error in + switch error { + case .tooManyChannels: + showInactiveChannels(context: context, source: .upgrade) + case .generic: + alert(for: context.window, info: L10n.unknownError) + } + }) + + }, convert: { + showModal(with: GigagroupLandingController(context: context, peerId: peerId), for: context.window) + }) + + let previous = Atomic<[AppearanceWrapperEntry]>(value: []) + let initialSize = self.atomicSize + + let signal = combineLatest(queue: .mainQueue(), appearanceSignal, statePromise.get(), peerView.get(), peersPromise.get()) + |> deliverOnMainQueue + |> map { appearance, state, view, participants -> (TableUpdateTransition, Peer?) in + let entries = channelPermissionsControllerEntries(view: view, state: state, participants: participants, limits: context.limitConfiguration).map { AppearanceWrapperEntry(entry: $0, appearance: appearance) } + return (prepareTransition(left: previous.swap(entries), right: entries, initialSize: initialSize.with { $0 }, arguments: arguments), peerViewMainPeer(view)) + } |> afterDisposed { + actionsDisposable.dispose() + } + + interfaceFullReady.set(combineLatest(queue: .mainQueue(), peerView.get(), peersPromise.get()) |> map { view, participants in + return view.cachedData != nil && (participants != nil) + }) + + + upgradedToSupergroupImpl = { [weak self] upgradedPeerId, f in + guard let `self` = self, let navigationController = self.navigationController else { + return + } + + var chatController: ChatController? = ChatController(context: context, chatLocation: .peer(upgradedPeerId)) + + + chatController!.navigationController = navigationController + chatController!.loadViewIfNeeded(navigationController.bounds) + + var signal = chatController!.ready.get() |> filter {$0} |> take(1) |> ignoreValues + + var controller: ChannelPermissionsController? = ChannelPermissionsController(context, peerId: upgradedPeerId) + + controller!.navigationController = navigationController + controller!.loadViewIfNeeded(navigationController.bounds) + + let mainSignal = combineLatest(controller!.ready.get(), controller!.interfaceFullReady.get()) |> map { $0 && $1 } |> filter {$0} |> take(1) |> ignoreValues + + signal = combineLatest(queue: .mainQueue(), signal, mainSignal) |> ignoreValues + + _ = signal.start(completed: { [weak navigationController] in + navigationController?.removeAll() + navigationController?.push(chatController!, false, style: .none) + navigationController?.push(controller!, false, style: .none) + + chatController = nil + controller = nil + }) + + } + + self.disposable.set(signal.start(next: { [weak self] (transition, peer) in + guard let `self` = self, !stopMerging else { return } + + if let peer = peer as? TelegramChannel, peer.flags.contains(.isGigagroup) { + self.navigationController?.back() + } else { + self.genericView.merge(with: transition) + self.readyOnce() + } + })) + + } +} + diff --git a/Telegram-Mac/ChannelRecentPostRowItem.swift b/Telegram-Mac/ChannelRecentPostRowItem.swift new file mode 100644 index 0000000000..9ca56f337f --- /dev/null +++ b/Telegram-Mac/ChannelRecentPostRowItem.swift @@ -0,0 +1,218 @@ +// +// ChannelRecentPostRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 12.03.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import Postbox +import SwiftSignalKit +import TelegramCore + + + + +class ChannelRecentPostRowItem: GeneralRowItem { + fileprivate let viewsCountLayout: TextViewLayout + fileprivate let sharesCountLayout: TextViewLayout + fileprivate let titleLayout: TextViewLayout + fileprivate let dateLayout: TextViewLayout + fileprivate let message: Message + fileprivate let contentImageMedia: TelegramMediaImage? + fileprivate let context: AccountContext + init(_ initialSize: NSSize, stableId: AnyHashable, context: AccountContext, message: Message, interactions: ChannelStatsMessageInteractions?, viewType: GeneralViewType, action: @escaping()->Void) { + self.context = context + self.message = message + var contentImageMedia: TelegramMediaImage? + for media in message.media { + if let image = media as? TelegramMediaImage { + contentImageMedia = image + break + } else if let file = media as? TelegramMediaFile { + if file.isVideo && !file.isInstantVideo { + let iconImageRepresentation:TelegramMediaImageRepresentation? = smallestImageRepresentation(file.previewRepresentations) + if let iconImageRepresentation = iconImageRepresentation { + contentImageMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [iconImageRepresentation], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + } + break + } + } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { + if let image = content.image { + contentImageMedia = image + break + } else if let file = content.file { + if file.isVideo && !file.isInstantVideo { + let iconImageRepresentation:TelegramMediaImageRepresentation? = smallestImageRepresentation(file.previewRepresentations) + if let iconImageRepresentation = iconImageRepresentation { + contentImageMedia = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [iconImageRepresentation], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + } + break + } + } + } + } + self.contentImageMedia = contentImageMedia + + self.titleLayout = TextViewLayout(NSAttributedString.initialize(string: pullText(from: message) as String, color: theme.colors.text, font: .medium(.text)), maximumNumberOfLines: 1) + self.dateLayout = TextViewLayout(NSAttributedString.initialize(string: stringForFullDate(timestamp: message.timestamp), color: theme.colors.grayText, font: .normal(.text)), maximumNumberOfLines: 1) + + + let views = Int(max(message.channelViewsCount ?? 0, interactions?.views ?? 0)) + let shares = Int(interactions?.forwards ?? 0) + + let viewsString = L10n.channelStatsViewsCountCountable(views).replacingOccurrences(of: "\(views)", with: views.formattedWithSeparator) + let sharesString = L10n.channelStatsSharesCountCountable(shares).replacingOccurrences(of: "\(shares)", with: shares.formattedWithSeparator) + + viewsCountLayout = TextViewLayout(NSAttributedString.initialize(string: viewsString, color: theme.colors.text, font: .normal(.short)),maximumNumberOfLines: 1) + sharesCountLayout = TextViewLayout(NSAttributedString.initialize(string: sharesString, color: theme.colors.grayText, font: .normal(.short)),maximumNumberOfLines: 1) + + super.init(initialSize, height: 46, stableId: stableId, viewType: viewType, action: action) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + + viewsCountLayout.measure(width: .greatestFiniteMagnitude) + sharesCountLayout.measure(width: .greatestFiniteMagnitude) + + let titleAndDateWidth: CGFloat = blockWidth - viewType.innerInset.left - (contentImageMedia != nil ? 34 + 10 : 0) - max(viewsCountLayout.layoutSize.width, sharesCountLayout.layoutSize.width) - 10 - viewType.innerInset.right + + titleLayout.measure(width: titleAndDateWidth) + dateLayout.measure(width: titleAndDateWidth) + + return true + } + + override func viewClass() -> AnyClass { + return ChannelRecentPostRowView.self + } +} + + +private final class ChannelRecentPostRowView : GeneralContainableRowView { + private let viewCountView = TextView() + private let sharesCountView = TextView() + private let titleView = TextView() + private let dateView = TextView() + private let fetchDisposable = MetaDisposable() + private var imageView: TransformImageView? + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(viewCountView) + addSubview(sharesCountView) + addSubview(titleView) + addSubview(dateView) + + sharesCountView.userInteractionEnabled = false + sharesCountView.isSelectable = false + sharesCountView.isEventLess = true + + titleView.userInteractionEnabled = false + titleView.isSelectable = false + titleView.isEventLess = true + + viewCountView.userInteractionEnabled = false + viewCountView.isSelectable = false + viewCountView.isEventLess = true + + dateView.userInteractionEnabled = false + dateView.isSelectable = false + dateView.isEventLess = true + + containerView.set(handler: { [weak self] _ in + self?.updateColors() + }, for: .Highlight) + containerView.set(handler: { [weak self] _ in + self?.updateColors() + }, for: .Normal) + containerView.set(handler: { [weak self] _ in + self?.updateColors() + }, for: .Hover) + + containerView.set(handler: { [weak self] _ in + guard let item = self?.item as? ChannelRecentPostRowItem else { + return + } + item.action() + }, for: .Click) + } + + override func layout() { + super.layout() + + guard let item = item as? ChannelRecentPostRowItem else { + return + } + + viewCountView.setFrameOrigin(NSMakePoint(item.blockWidth - viewCountView.frame.width - item.viewType.innerInset.right, 5)) + sharesCountView.setFrameOrigin(NSMakePoint(item.blockWidth - sharesCountView.frame.width - item.viewType.innerInset.right, containerView.frame.height - sharesCountView.frame.height - 5)) + + let leftOffset: CGFloat = (imageView != nil ? 34 + 10 : 0) + item.viewType.innerInset.left + + titleView.setFrameOrigin(NSMakePoint(leftOffset, 5)) + dateView.setFrameOrigin(NSMakePoint(leftOffset, containerView.frame.height - dateView.frame.height - 5)) + + imageView?.centerY(x: item.viewType.innerInset.left) + } + + override var backdorColor: NSColor { + return isSelect ? theme.colors.accentSelect : theme.colors.background + } + + override func updateColors() { + super.updateColors() + if let item = item as? GeneralRowItem { + self.background = item.viewType.rowBackground + let highlighted = isSelect ? self.backdorColor : theme.colors.grayHighlight + titleView.backgroundColor = containerView.controlState == .Highlight && !isSelect ? .clear : self.backdorColor + dateView.backgroundColor = containerView.controlState == .Highlight && !isSelect ? .clear : self.backdorColor + viewCountView.backgroundColor = containerView.controlState == .Highlight && !isSelect ? .clear : self.backdorColor + sharesCountView.backgroundColor = containerView.controlState == .Highlight && !isSelect ? .clear : self.backdorColor + containerView.set(background: self.backdorColor, for: .Normal) + containerView.set(background: highlighted, for: .Highlight) + } + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? ChannelRecentPostRowItem else { + return + } + + viewCountView.update(item.viewsCountLayout) + sharesCountView.update(item.sharesCountLayout) + dateView.update(item.dateLayout) + titleView.update(item.titleLayout) + + + if let media = item.contentImageMedia { + if imageView == nil { + self.imageView = TransformImageView(frame: NSMakeRect(0, 0, 34, 34)) + imageView?.set(arguments: TransformImageArguments(corners: .init(radius: 4), imageSize: NSMakeSize(34, 34), boundingSize: NSMakeSize(34, 34), intrinsicInsets: NSEdgeInsets())) + addSubview(self.imageView!) + } + let updateIconImageSignal = chatWebpageSnippetPhoto(account: item.context.account, imageReference: ImageMediaReference.message(message: MessageReference(item.message), media: media), scale: backingScaleFactor, small:true) + imageView?.setSignal(updateIconImageSignal) + + fetchDisposable.set(chatMessagePhotoInteractiveFetched(account: item.context.account, imageReference: ImageMediaReference.message(message: MessageReference(item.message), media: media)).start()) + + } else { + imageView?.removeFromSuperview() + imageView = nil + fetchDisposable.set(nil) + } + } + + deinit { + fetchDisposable.dispose() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/ChannelStatisticsController.swift b/Telegram-Mac/ChannelStatisticsController.swift new file mode 100644 index 0000000000..2985f063e1 --- /dev/null +++ b/Telegram-Mac/ChannelStatisticsController.swift @@ -0,0 +1,75 @@ +// +// ChannelStatisticsController.swift +// Telegram +// +// Created by Mikhail Filimonov on 22/02/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit +import Postbox +import TelegramCore + +import WebKit + + + +class ChannelStatisticsController: TelegramGenericViewController { + private let peerId:PeerId + + private let uniqueId:String = "_\(arc4random())" + private let disposable = MetaDisposable() + private let statsUrl: String + init(_ context: AccountContext, _ peerId:PeerId, statsUrl: String) { + self.peerId = peerId + self.statsUrl = statsUrl + super.init(context) + load(with: statsUrl) + } + + override var enableBack: Bool { + return true + } + + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + genericView.mainFrame.load(URLRequest(url: URL(string:"file://blank")!)) + genericView.mainFrame.stopLoading() + } + + override func viewDidLoad() { + super.viewDidLoad() + genericView.wantsLayer = true + readyOnce() + } + + private func load(with url: String) { + + if let url = URL(string:url) { + genericView.mainFrame.load(URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 15)) + } + + } + + + + override func becomeFirstResponder() -> Bool? { + return true + } + + override func firstResponder() -> NSResponder? { + return genericView + } + + override func backKeyAction() -> KeyHandlerResult { + return .invokeNext + } + + deinit { + disposable.dispose() + } + +} diff --git a/Telegram-Mac/ChannelStatsViewController.swift b/Telegram-Mac/ChannelStatsViewController.swift new file mode 100644 index 0000000000..9901502956 --- /dev/null +++ b/Telegram-Mac/ChannelStatsViewController.swift @@ -0,0 +1,298 @@ +// +// ChannelStatsViewController.swift +// Telegram +// +// Created by Mikhail Filimonov on 24.02.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit +import TelegramCore +import Postbox + +import GraphCore + + + +struct UIStatsState : Equatable { + + enum RevealSection : Hashable { + case topPosters + case topAdmins + case topInviters + + var id: InputDataIdentifier { + switch self { + case .topPosters: + return InputDataIdentifier("_id_top_posters") + case .topAdmins: + return InputDataIdentifier("_id_top_admins") + case .topInviters: + return InputDataIdentifier("_id_top_inviters") + } + } + } + + let loading: Set + let revealed:Set + init(loading: Set, revealed: Set = Set()) { + self.loading = loading + self.revealed = revealed + } + func withAddedLoading(_ token: InputDataIdentifier) -> UIStatsState { + var loading = self.loading + loading.insert(token) + return UIStatsState(loading: loading, revealed: self.revealed) + } + func withRemovedLoading(_ token: InputDataIdentifier) -> UIStatsState { + var loading = self.loading + loading.remove(token) + return UIStatsState(loading: loading, revealed: self.revealed) + } + + func withRevealedSection(_ section: RevealSection) -> UIStatsState { + var revealed = self.revealed + revealed.insert(section) + return UIStatsState(loading: self.loading, revealed: revealed) + } +} + +private func _id_message(_ messageId: MessageId) -> InputDataIdentifier { + return InputDataIdentifier("_id_message_\(messageId)") +} + +private func statsEntries(_ state: ChannelStatsContextState, uiState: UIStatsState, messages: [Message]?, interactions: [MessageId : ChannelStatsMessageInteractions]?, updateIsLoading: @escaping(InputDataIdentifier, Bool)->Void, openMessage: @escaping(MessageId)->Void, context: ChannelStatsContext, accountContext: AccountContext, detailedDisposable: DisposableDict) -> [InputDataEntry] { + var entries: [InputDataEntry] = [] + + var sectionId: Int32 = 0 + var index: Int32 = 0 + + + if state.stats == nil { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("loading"), equatable: nil, comparable: nil, item: { initialSize, stableId in + return StatisticsLoadingRowItem(initialSize, stableId: stableId, context: accountContext, text: L10n.channelStatsLoading) + })) + } else if let stats = state.stats { + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.channelStatsOverview), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + var overviewItems:[ChannelOverviewItem] = [] + + if stats.followers.current > 0 { + overviewItems.append(ChannelOverviewItem(title: L10n.channelStatsOverviewFollowers, value: stats.followers.attributedString)) + } + if stats.enabledNotifications.total != 0 { + overviewItems.append(ChannelOverviewItem(title: L10n.channelStatsOverviewEnabledNotifications, value: stats.enabledNotifications.attributedString)) + } + if stats.viewsPerPost.current > 0 { + overviewItems.append(ChannelOverviewItem(title: L10n.channelStatsOverviewViewsPerPost, value: stats.viewsPerPost.attributedString)) + } + if stats.sharesPerPost.current > 0 { + overviewItems.append(ChannelOverviewItem(title: L10n.channelStatsOverviewSharesPerPost, value: stats.sharesPerPost.attributedString)) + } + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("overview"), equatable: InputDataEquatable(overviewItems), comparable: nil, item: { initialSize, stableId in + return ChannelOverviewStatsRowItem(initialSize, stableId: stableId, items: overviewItems, viewType: .singleItem) + })) + index += 1 + + + struct Graph { + let graph: StatsGraph + let title: String + let identifier: InputDataIdentifier + let type: ChartItemType + let load:(InputDataIdentifier)->Void + } + + var graphs: [Graph] = [] + graphs.append(Graph(graph: stats.growthGraph, title: L10n.channelStatsGraphGrowth, identifier: InputDataIdentifier("growthGraph"), type: .lines, load: { identifier in + context.loadGrowthGraph() + updateIsLoading(identifier, true) + })) + graphs.append(Graph(graph: stats.followersGraph, title: L10n.channelStatsGraphFollowers, identifier: InputDataIdentifier("followersGraph"), type: .lines, load: { identifier in + context.loadFollowersGraph() + updateIsLoading(identifier, true) + })) + + graphs.append(Graph(graph: stats.muteGraph, title: L10n.channelStatsGraphNotifications, identifier: InputDataIdentifier("muteGraph"), type: .lines, load: { identifier in + context.loadMuteGraph() + updateIsLoading(identifier, true) + })) + + graphs.append(Graph(graph: stats.topHoursGraph, title: L10n.channelStatsGraphViewsByHours, identifier: InputDataIdentifier("topHoursGraph"), type: .hourlyStep, load: { identifier in + context.loadTopHoursGraph() + updateIsLoading(identifier, true) + })) + + graphs.append(Graph(graph: stats.viewsBySourceGraph, title: L10n.channelStatsGraphViewsBySource, identifier: InputDataIdentifier("viewsBySourceGraph"), type: .bars, load: { identifier in + context.loadViewsBySourceGraph() + updateIsLoading(identifier, true) + })) + + + graphs.append(Graph(graph: stats.newFollowersBySourceGraph, title: L10n.channelStatsGraphNewFollowersBySource, identifier: InputDataIdentifier("newFollowersBySourceGraph"), type: .bars, load: { identifier in + context.loadNewFollowersBySourceGraph() + updateIsLoading(identifier, true) + })) + graphs.append(Graph(graph: stats.languagesGraph, title: L10n.channelStatsGraphLanguage, identifier: InputDataIdentifier("languagesGraph"), type: .pie, load: { identifier in + context.loadLanguagesGraph() + updateIsLoading(identifier, true) + })) + + + + graphs.append(Graph(graph: stats.interactionsGraph, title: L10n.channelStatsGraphInteractions, identifier: InputDataIdentifier("interactionsGraph"), type: .twoAxisStep, load: { identifier in + context.loadInteractionsGraph() + updateIsLoading(identifier, true) + })) + + for graph in graphs { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(graph.title), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + switch graph.graph { + case let .Loaded(_, string): + ChartsDataManager.readChart(data: string.data(using: .utf8)!, sync: true, success: { collection in + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: graph.identifier, equatable: InputDataEquatable(graph.graph), comparable: nil, item: { initialSize, stableId in + return StatisticRowItem(initialSize, stableId: stableId, context: accountContext, collection: collection, viewType: .singleItem, type: graph.type, getDetailsData: { date, completion in + detailedDisposable.set(context.loadDetailedGraph(graph.graph, x: Int64(date.timeIntervalSince1970) * 1000).start(next: { graph in + if let graph = graph, case let .Loaded(_, data) = graph { + completion(data) + } + }), forKey: graph.identifier) + }) + })) + }, failure: { error in + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: graph.identifier, equatable: InputDataEquatable(graph.graph), comparable: nil, item: { initialSize, stableId in + return StatisticLoadingRowItem(initialSize, stableId: stableId, error: error.localizedDescription) + })) + }) + + updateIsLoading(graph.identifier, false) + + index += 1 + case .OnDemand: + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: graph.identifier, equatable: InputDataEquatable(graph.graph), comparable: nil, item: { initialSize, stableId in + return StatisticLoadingRowItem(initialSize, stableId: stableId, error: nil) + })) + index += 1 + if !uiState.loading.contains(graph.identifier) { + graph.load(graph.identifier) + } + case let .Failed(error): + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: graph.identifier, equatable: InputDataEquatable(graph.graph), comparable: nil, item: { initialSize, stableId in + return StatisticLoadingRowItem(initialSize, stableId: stableId, error: error) + })) + index += 1 + updateIsLoading(graph.identifier, false) + case .Empty: + break + } + } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + if let messages = messages, let interactions = interactions, !messages.isEmpty { + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.channelStatsRecentHeader), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + for (i, message) in messages.enumerated() { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_message(message.id), equatable: InputDataEquatable(message), comparable: nil, item: { initialSize, stableId in + return ChannelRecentPostRowItem(initialSize, stableId: stableId, context: accountContext, message: message, interactions: interactions[message.id], viewType: bestGeneralViewType(messages, for: i), action: { + openMessage(message.id) + }) + })) + index += 1 + } + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + } + } + + + return entries +} + + +func ChannelStatsViewController(_ context: AccountContext, peerId: PeerId, datacenterId: Int32) -> ViewController { + + let initialState = UIStatsState(loading: []) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((UIStatsState) -> UIStatsState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + let statsContext = ChannelStatsContext(postbox: context.account.postbox, network: context.account.network, datacenterId: datacenterId, peerId: peerId) + + + let messagesPromise = Promise(nil) + + + let messageView = context.account.viewTracker.aroundMessageHistoryViewForLocation(.peer(peerId), index: .upperBound, anchorIndex: .upperBound, count: 100, fixedCombinedReadStates: nil) + |> map { messageHistoryView, _, _ -> MessageHistoryView? in + return messageHistoryView + } + messagesPromise.set(.single(nil) |> then(messageView)) + + let openMessage: (MessageId)->Void = { messageId in + context.sharedContext.bindings.rootNavigation().push(MessageStatsController(context, messageId: messageId, datacenterId: datacenterId)) + } + + let detailedDisposable = DisposableDict() + + let signal = combineLatest(queue: prepareQueue, statePromise.get(), statsContext.state, messagesPromise.get()) |> map { uiState, state, messageView in + + + let interactions = state.stats?.messageInteractions.reduce([MessageId : ChannelStatsMessageInteractions]()) { (map, interactions) -> [MessageId : ChannelStatsMessageInteractions] in + var map = map + map[interactions.messageId] = interactions + return map + } + + let messages = messageView?.entries.map { $0.message }.filter { interactions?[$0.id] != nil }.sorted(by: { (lhsMessage, rhsMessage) -> Bool in + return lhsMessage.timestamp > rhsMessage.timestamp + }) + + + + return statsEntries(state, uiState: uiState, messages: messages, interactions: interactions, updateIsLoading: { identifier, isLoading in + updateState { state in + if isLoading { + return state.withAddedLoading(identifier) + } else { + return state.withRemovedLoading(identifier) + } + } + }, openMessage: openMessage, context: statsContext, accountContext: context, detailedDisposable: detailedDisposable) + } |> map { + return InputDataSignalValue(entries: $0) + } + + + let controller = InputDataController(dataSignal: signal, title: L10n.channelStatsTitle, removeAfterDisappear: false, hasDone: false) + + controller.contextOject = statsContext + controller.didLoaded = { controller, _ in + controller.tableView.alwaysOpenRowsOnMouseUp = true + controller.tableView.needUpdateVisibleAfterScroll = true + } + + controller.onDeinit = { + detailedDisposable.dispose() + } + + return controller +} diff --git a/Telegram-Mac/ChannelVisibilityController.swift b/Telegram-Mac/ChannelVisibilityController.swift index fa77617362..d3077f5b46 100644 --- a/Telegram-Mac/ChannelVisibilityController.swift +++ b/Telegram-Mac/ChannelVisibilityController.swift @@ -8,9 +8,10 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac +import Postbox +import TelegramCore + +import SwiftSignalKit private enum CurrentChannelType { case publicChannel @@ -18,19 +19,28 @@ private enum CurrentChannelType { } private final class ChannelVisibilityControllerArguments { - let account: Account + let context: AccountContext let updateCurrentType: (CurrentChannelType) -> Void let updatePublicLinkText: (String?, String) -> Void let displayPrivateLinkMenu: (String) -> Void let revokePeerId: (PeerId) -> Void - - init(account: Account, updateCurrentType: @escaping (CurrentChannelType) -> Void, updatePublicLinkText: @escaping (String?, String) -> Void, displayPrivateLinkMenu: @escaping (String) -> Void, revokePeerId: @escaping (PeerId) -> Void) { - self.account = account + let copy:(String)->Void + let revokeLink: ()->Void + let share:(String)->Void + let manageLinks:()->Void + let open:(ExportedInvitation)->Void + init(context: AccountContext, updateCurrentType: @escaping (CurrentChannelType) -> Void, updatePublicLinkText: @escaping (String?, String) -> Void, displayPrivateLinkMenu: @escaping (String) -> Void, revokePeerId: @escaping (PeerId) -> Void, copy: @escaping(String)->Void, revokeLink: @escaping()->Void, share: @escaping(String)->Void, manageLinks:@escaping()->Void, open:@escaping(ExportedInvitation)->Void) { + self.context = context self.updateCurrentType = updateCurrentType self.updatePublicLinkText = updatePublicLinkText self.displayPrivateLinkMenu = displayPrivateLinkMenu self.revokePeerId = revokePeerId + self.revokeLink = revokeLink + self.copy = copy + self.share = share + self.manageLinks = manageLinks + self.open = open } } @@ -38,49 +48,27 @@ private final class ChannelVisibilityControllerArguments { fileprivate enum ChannelVisibilityEntryStableId: Hashable { case index(Int32) case peer(PeerId) - - var hashValue: Int { - switch self { - case let .index(index): - return index.hashValue - case let .peer(peerId): - return peerId.hashValue - } - } - - static func ==(lhs: ChannelVisibilityEntryStableId, rhs: ChannelVisibilityEntryStableId) -> Bool { - switch lhs { - case let .index(index): - if case .index(index) = rhs { - return true - } else { - return false - } - case let .peer(peerId): - if case .peer(peerId) = rhs { - return true - } else { - return false - } - } - } } -private enum ChannelVisibilityEntry: Identifiable, Comparable { - case typeHeader(sectionId:Int32, String) - case typePublic(sectionId:Int32, Bool) - case typePrivate(sectionId:Int32, Bool) - case typeInfo(sectionId:Int32, String) +private enum ChannelVisibilityEntry: TableItemListNodeEntry { + case typeHeader(sectionId:Int32, String, GeneralViewType) + case typePublic(sectionId:Int32, Bool, GeneralViewType) + case typePrivate(sectionId:Int32, Bool, GeneralViewType) + case typeInfo(sectionId:Int32, String, GeneralViewType) - case publicLinkAvailability(sectionId:Int32, Bool) - case privateLink(sectionId:Int32, String?) - case editablePublicLink(sectionId:Int32, String?, String, AddressNameValidationStatus?) - case privateLinkInfo(sectionId:Int32, String) - case publicLinkInfo(sectionId:Int32, String) - case publicLinkStatus(sectionId:Int32, String, AddressNameValidationStatus) + case publicLinkAvailability(sectionId:Int32, Bool, GeneralViewType) + case privateLinkHeader(sectionId:Int32, String, GeneralViewType) + case privateLink(sectionId:Int32, ExportedInvitation?, PeerInvitationImportersState?, Bool, GeneralViewType) + case editablePublicLink(sectionId:Int32, String?, String, AddressNameValidationStatus?, GeneralViewType) + case privateLinkInfo(sectionId:Int32, String, GeneralViewType) + case publicLinkInfo(sectionId:Int32, String, GeneralViewType) + case publicLinkStatus(sectionId:Int32, String, AddressNameValidationStatus, GeneralViewType) - case existingLinksInfo(sectionId:Int32, String) - case existingLinkPeerItem(sectionId:Int32, Int32, Peer, ShortPeerDeleting?, Bool) + case manageLinks(sectionId:Int32, GeneralViewType) + case manageLinksDesc(sectionId:Int32, GeneralViewType) + + case existingLinksInfo(sectionId:Int32, String, GeneralViewType) + case existingLinkPeerItem(sectionId:Int32, Int32, Peer, ShortPeerDeleting?, Bool, GeneralViewType) case section(sectionId:Int32) @@ -96,113 +84,127 @@ private enum ChannelVisibilityEntry: Identifiable, Comparable { return .index(3) case .publicLinkAvailability: return .index(4) - case .privateLink: + case .privateLinkHeader: return .index(5) - case .editablePublicLink: + case .privateLink: return .index(6) - case .privateLinkInfo: + case .editablePublicLink: return .index(7) - case .publicLinkStatus: + case .privateLinkInfo: return .index(8) - case .publicLinkInfo: + case .publicLinkStatus: return .index(9) - case .existingLinksInfo: + case .publicLinkInfo: return .index(10) - case let .existingLinkPeerItem(_,_, peer, _, _): + case .existingLinksInfo: + return .index(11) + case .manageLinks: + return .index(12) + case .manageLinksDesc: + return .index(13) + case let .existingLinkPeerItem(_,_, peer, _, _, _): return .peer(peer.id) case let .section(sectionId: sectionId): return .index((sectionId + 1) * 1000 - sectionId) } } - static func ==(lhs: ChannelVisibilityEntry, rhs: ChannelVisibilityEntry) -> Bool { switch lhs { - case let .typeHeader(_, title): - if case .typeHeader(_, title) = rhs { + case let .typeHeader(sectionId, title, viewType): + if case .typeHeader(sectionId, title, viewType) = rhs { return true } else { return false } - case let .typePublic(_, selected): - if case .typePublic(_, selected) = rhs { + case let .typePublic(sectionId, selected, viewType): + if case .typePublic(sectionId, selected, viewType) = rhs { return true } else { return false } - case let .typePrivate(_, selected): - if case .typePrivate(_, selected) = rhs { + case let .typePrivate(sectionId, selected, viewType): + if case .typePrivate(sectionId, selected, viewType) = rhs { return true } else { return false } - case let .typeInfo(_, text): - if case .typeInfo(_, text) = rhs { + case let .typeInfo(sectionId, text, viewType): + if case .typeInfo(sectionId, text, viewType) = rhs { return true } else { return false } - case let .publicLinkAvailability(_, value): - if case .publicLinkAvailability(_, value) = rhs { + case let .publicLinkAvailability(sectionId, value, viewType): + if case .publicLinkAvailability(sectionId, value, viewType) = rhs { return true } else { return false } - case let .privateLink(_, lhsLink): - if case let .privateLink(_, rhsLink) = rhs, lhsLink == rhsLink { + case let .privateLinkHeader(sectionId, title, viewType): + if case .privateLinkHeader(sectionId, title, viewType) = rhs { return true } else { return false } - case let .editablePublicLink(_, lhsCurrentText, lhsText, lhsStatus): - if case let .editablePublicLink(_, rhsCurrentText, rhsText, rhsStatus) = rhs, lhsCurrentText == rhsCurrentText, lhsText == rhsText, lhsStatus == rhsStatus { + case let .privateLink(sectionId, link, importers, isNew, viewType): + if case .privateLink(sectionId, link, importers, isNew, viewType) = rhs { return true } else { return false } - case let .privateLinkInfo(_, text): - if case .privateLinkInfo(_, text) = rhs { + case let .editablePublicLink(sectionId, currenttext, text, status, viewType): + if case .editablePublicLink(sectionId, currenttext, text, status, viewType) = rhs { return true } else { return false } - case let .publicLinkInfo(_, text): - if case .publicLinkInfo(_, text) = rhs { + case let .privateLinkInfo(sectionId, text, viewType): + if case .privateLinkInfo(sectionId, text, viewType) = rhs { return true } else { return false } - case let .publicLinkStatus(_, addressName, status): - if case .publicLinkStatus(_, addressName, status) = rhs { + case let .publicLinkInfo(sectionId, text, viewType): + if case .publicLinkInfo(sectionId, text, viewType) = rhs { return true } else { return false } - case let .existingLinksInfo(_, text): - if case .existingLinksInfo(_, text) = rhs { + case let .publicLinkStatus(sectionId, addressName, status, viewType): + if case .publicLinkStatus(sectionId, addressName, status, viewType) = rhs { return true } else { return false } - case let .existingLinkPeerItem(_, lhsIndex, lhsPeer, lhsEditing, lhsEnabled): - if case let .existingLinkPeerItem(_, rhsIndex, rhsPeer, rhsEditing, rhsEnabled) = rhs { - if lhsIndex != rhsIndex { - return false - } + case let .existingLinksInfo(sectionId, text, viewType): + if case .existingLinksInfo(sectionId, text, viewType) = rhs { + return true + } else { + return false + } + case let .existingLinkPeerItem(sectionId, index, lhsPeer, editing, enabled, viewType): + if case .existingLinkPeerItem(sectionId, index, let rhsPeer, editing, enabled, viewType) = rhs { if !lhsPeer.isEqual(rhsPeer) { return false } - if lhsEditing != rhsEditing { - return false - } - if lhsEnabled != rhsEnabled { - return false - } return true } else { return false } - case let .section(sectionId: sectionId): - if case .section(sectionId: sectionId) = rhs { + case let .manageLinks(sectionId, viewType): + if case .manageLinks(sectionId, viewType) = rhs { + return true + } else { + return false + } + case let .manageLinksDesc(sectionId, viewType): + if case .manageLinksDesc(sectionId, viewType) = rhs { + return true + } else { + return false + } + case let .section(sectionId): + if case .section(sectionId) = rhs { return true } else { return false @@ -212,29 +214,35 @@ private enum ChannelVisibilityEntry: Identifiable, Comparable { var index: Int32 { switch self { - case let .typeHeader(sectionId: sectionId, _): + case let .typeHeader(sectionId: sectionId, _, _): return (sectionId * 1000) + 0 - case let .typePublic(sectionId: sectionId, _): + case let .typePublic(sectionId: sectionId, _, _): return (sectionId * 1000) + 1 - case let .typePrivate(sectionId: sectionId, _): + case let .typePrivate(sectionId: sectionId, _, _): return (sectionId * 1000) + 2 - case let .typeInfo(sectionId: sectionId, _): + case let .typeInfo(sectionId: sectionId, _, _): return (sectionId * 1000) + 3 - case let .publicLinkAvailability(sectionId: sectionId, _): + case let .publicLinkAvailability(sectionId: sectionId, _, _): return (sectionId * 1000) + 4 - case let .privateLink(sectionId: sectionId, _): + case let .privateLinkHeader(sectionId: sectionId, _, _): return (sectionId * 1000) + 5 - case let .editablePublicLink(sectionId: sectionId, _, _, _): + case let .privateLink(sectionId: sectionId, _, _, _, _): return (sectionId * 1000) + 6 - case let .privateLinkInfo(sectionId: sectionId, _): + case let .editablePublicLink(sectionId: sectionId, _, _, _, _): return (sectionId * 1000) + 7 - case let .publicLinkStatus(sectionId: sectionId, _, _): + case let .privateLinkInfo(sectionId: sectionId, _, _): return (sectionId * 1000) + 8 - case let .publicLinkInfo(sectionId: sectionId, _): + case let .publicLinkStatus(sectionId: sectionId, _, _, _): return (sectionId * 1000) + 9 - case let .existingLinksInfo(sectionId: sectionId, _): + case let .publicLinkInfo(sectionId: sectionId, _, _): return (sectionId * 1000) + 10 - case let .existingLinkPeerItem(sectionId, index, _, _, _): + case let .existingLinksInfo(sectionId: sectionId, _, _): + return (sectionId * 1000) + 11 + case let .manageLinks(sectionId: sectionId, _): + return (sectionId * 1000) + 12 + case let .manageLinksDesc(sectionId: sectionId, _): + return (sectionId * 1000) + 13 + case let .existingLinkPeerItem(sectionId, index, _, _, _, _): return (sectionId * 1000) + index + 20 case let .section(sectionId: sectionId): return (sectionId + 1) * 1000 - sectionId @@ -247,57 +255,82 @@ private enum ChannelVisibilityEntry: Identifiable, Comparable { func item(_ arguments: ChannelVisibilityControllerArguments, initialSize:NSSize) -> TableRowItem { switch self { - case let .typeHeader(_, title): - return GeneralTextRowItem(initialSize, stableId: stableId, text: title) - case let .typePublic(_, selected): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.channelPublic), type: .selectable(stateback: { return selected}), action: { + case let .typeHeader(_, title, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: title, viewType: viewType) + case let .typePublic(_, selected, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.channelPublic, type: .selectable(selected), viewType: viewType, action: { arguments.updateCurrentType(.publicChannel) }) - case let .typePrivate(_, selected): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.channelPrivate), type: .selectable(stateback: { return selected}), action: { + case let .typePrivate(_, selected, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.channelPrivate, type: .selectable(selected), viewType: viewType, action: { arguments.updateCurrentType(.privateChannel) }) - case let .typeInfo(_, text): - return GeneralTextRowItem(initialSize, stableId: stableId, text: text) - case let .publicLinkAvailability(_, value): + case let .typeInfo(_, text, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, viewType: viewType) + case let .publicLinkAvailability(_, value, viewType): + let color: NSColor + let text: String if value { - return GeneralTextRowItem(initialSize, stableId: stableId, text: .initialize(string: tr(.channelVisibilityChecking), color: theme.colors.redUI, font:.normal(.text))) - } else { - return GeneralTextRowItem(initialSize, stableId: stableId, text: NSAttributedString.initialize(string: tr(.channelPublicNamesLimitError), color: theme.colors.redUI, font:.normal(.text))) - } - case let .privateLink(_, link): - let color:NSColor - if let _ = link { - color = theme.colors.link - } else { + text = L10n.channelVisibilityChecking color = theme.colors.grayText + } else { + text = L10n.channelPublicNamesLimitError + color = theme.colors.redUI } - return GeneralTextRowItem(initialSize, stableId: stableId, text: .initialize(string:link ?? tr(.channelVisibilityLoading), color: color, font:.normal(.text)), drawCustomSeparator: true, inset: NSEdgeInsets(left: 30.0, right: 30.0, top:5, bottom:8), action: { + return GeneralTextRowItem(initialSize, stableId: stableId, text: .initialize(string: text, color: color, font: .normal(.text)), viewType: viewType) + case let .privateLinkHeader(_, title, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: title, viewType: viewType) + case let .privateLink(_, link, importers, isNew, viewType): + + var peers = importers?.importers.map { $0.peer } ?? [] + peers = Array(peers.prefix(3)) + + return ExportedInvitationRowItem(initialSize, stableId: stableId, context: arguments.context, exportedLink: link, lastPeers: peers, viewType: viewType, mode: isNew ? .short : .normal, menuItems: { + + var items:[ContextMenuItem] = [] if let link = link { - arguments.displayPrivateLinkMenu(link) + items.append(ContextMenuItem(L10n.channelVisibiltiyContextCopy, handler: { + arguments.copy(link.link) + })) + items.append(ContextMenuItem(L10n.channelVisibiltiyContextRevoke, handler: { + arguments.revokeLink() + })) } - }) - case let .editablePublicLink(_, currentText, text, status): - return UsernameInputRowItem(initialSize, stableId: stableId, placeholder: "t.me/", limit: 30, status: status, text: text, changeHandler: { updatedText in + + return .single(items) + }, share: arguments.share, open: arguments.open, copyLink: arguments.copy) + + case let .editablePublicLink(_, currentText, text, status, viewType): + var rightItem: InputDataRightItem? = nil + if let status = status { + switch status { + case .checking: + rightItem = .loading + default: + break + } + } + return InputDataRowItem(initialSize, stableId: stableId, mode: .plain, error: nil, viewType: viewType, currentText: text, placeholder: nil, inputPlaceholder: "t.me", defaultText:"https://t.me/", rightItem: rightItem, filter: { $0 }, updated: { updatedText in arguments.updatePublicLinkText(currentText, updatedText) - }, holdText:true) - case let .privateLinkInfo(_, text): - return GeneralTextRowItem(initialSize, stableId: stableId, text: text) - case let .publicLinkInfo(_, text): - return GeneralTextRowItem(initialSize, stableId: stableId, text: text) - case let .publicLinkStatus(_, addressName, status): + }, limit: 33) + case let .privateLinkInfo(_, text, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, viewType: viewType) + case let .publicLinkInfo(_, text, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, viewType: viewType) + case let .publicLinkStatus(_, addressName, status, viewType): var text:String = "" var color:NSColor = .text + switch status { case let .invalidFormat(format): text = format.description color = theme.colors.redUI case let .availability(availability): - text = availability.description + text = availability.description(for: addressName) switch availability { case .available: - color = theme.colors.blueUI + color = theme.colors.accent default: color = theme.colors.redUI } @@ -305,15 +338,19 @@ private enum ChannelVisibilityEntry: Identifiable, Comparable { break } - return GeneralTextRowItem(initialSize, stableId: stableId, text: NSAttributedString.initialize(string: text, color: color, font: .normal(.text)), alignment: .left, inset:NSEdgeInsets(left: 30.0, right: 30.0, top:6, bottom:4)) - case let .existingLinksInfo(_, text): - return GeneralTextRowItem(initialSize, stableId: stableId, text: text) - case let .existingLinkPeerItem(_, _, peer, _, _): - return ShortPeerRowItem(initialSize, peer: peer, account: arguments.account, status:"t.me/\(peer.addressName ?? "unknown")", inset:NSEdgeInsets(left: 30, right:30), interactionType:.deletable(onRemove:{ peerId in + return GeneralTextRowItem(initialSize, stableId: stableId, text: NSAttributedString.initialize(string: text, color: color, font: .normal(.text)), viewType: viewType) + case let .existingLinksInfo(_, text, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, viewType: viewType) + case let .existingLinkPeerItem(_, _, peer, _, _, viewType): + return ShortPeerRowItem(initialSize, peer: peer, account: arguments.context.account, status: "t.me/\(peer.addressName ?? "unknown")", inset: NSEdgeInsets(left: 30, right:30), interactionType:.deletable(onRemove: { peerId in arguments.revokePeerId(peerId) - }, deletable: true)) + }, deletable: true), viewType: viewType) + case let .manageLinks(_, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.channelVisibiltiyManageLinks, icon: theme.icons.group_invite_via_link, nameStyle: blueActionButton, type: .none, viewType: viewType, action: arguments.manageLinks) + case let .manageLinksDesc(_, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.manageLinksEmptyDesc, detectBold: true, textColor: theme.colors.listGrayText, viewType: viewType) case .section: - return GeneralRowItem(initialSize, height: 20, stableId: stableId) + return GeneralRowItem(initialSize, height: 30, stableId: stableId, viewType: .separator) } } @@ -343,26 +380,6 @@ private struct ChannelVisibilityControllerState: Equatable { self.revokingPeerId = revokingPeerId } - static func ==(lhs: ChannelVisibilityControllerState, rhs: ChannelVisibilityControllerState) -> Bool { - if lhs.selectedType != rhs.selectedType { - return false - } - if lhs.editingPublicLinkText != rhs.editingPublicLinkText { - return false - } - if lhs.addressNameValidationStatus != rhs.addressNameValidationStatus { - return false - } - if lhs.updatingAddressName != rhs.updatingAddressName { - return false - } - if lhs.revokingPeerId != rhs.revokingPeerId { - return false - } - - return true - } - func withUpdatedSelectedType(_ selectedType: CurrentChannelType?) -> ChannelVisibilityControllerState { return ChannelVisibilityControllerState(selectedType: selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: self.updatingAddressName, revokingPeerId: self.revokingPeerId) } @@ -388,7 +405,7 @@ private struct ChannelVisibilityControllerState: Equatable { } } -private func channelVisibilityControllerEntries(view: PeerView, publicChannelsToRevoke: [Peer]?, state: ChannelVisibilityControllerState, onlyUsername: Bool) -> [ChannelVisibilityEntry] { +private func channelVisibilityControllerEntries(view: PeerView, publicChannelsToRevoke: [Peer]?, state: ChannelVisibilityControllerState, onlyUsername: Bool, importers: PeerInvitationImportersState?, isNew: Bool) -> [ChannelVisibilityEntry] { var entries: [ChannelVisibilityEntry] = [] var sectionId:Int32 = 0 @@ -424,25 +441,117 @@ private func channelVisibilityControllerEntries(view: PeerView, publicChannelsTo } } - entries.append(.typeHeader(sectionId: sectionId, isGroup ? tr(.channelTypeHeaderGroup) : tr(.channelTypeHeaderChannel))) - entries.append(.typePublic(sectionId: sectionId, selectedType == .publicChannel)) - entries.append(.typePrivate(sectionId: sectionId, selectedType == .privateChannel)) + entries.append(.typeHeader(sectionId: sectionId, isGroup ? L10n.channelTypeHeaderGroup : L10n.channelTypeHeaderChannel, .textTopItem)) + entries.append(.typePublic(sectionId: sectionId, selectedType == .publicChannel, .firstItem)) + entries.append(.typePrivate(sectionId: sectionId, selectedType == .privateChannel, .lastItem)) switch selectedType { case .publicChannel: - if isGroup { - entries.append(.typeInfo(sectionId: sectionId, tr(.channelPublicAboutGroup))) + entries.append(.typeInfo(sectionId: sectionId, isGroup ? L10n.channelPublicAboutGroup : L10n.channelPublicAboutChannel, .textBottomItem)) + case .privateChannel: + entries.append(.typeInfo(sectionId: sectionId, isGroup ? L10n.channelPrivateAboutGroup : L10n.channelPrivateAboutChannel, .textBottomItem)) + } + + entries.append(.section(sectionId: sectionId)) + sectionId += 1 + + switch selectedType { + case .publicChannel: + var displayAvailability = false + if peer.addressName == nil { + displayAvailability = publicChannelsToRevoke == nil || !(publicChannelsToRevoke!.isEmpty) + } + if displayAvailability { + if let publicChannelsToRevoke = publicChannelsToRevoke { + entries.append(.publicLinkAvailability(sectionId: sectionId, false, .textTopItem)) + var index: Int32 = 0 + + let sorted = publicChannelsToRevoke.sorted(by: { lhs, rhs in + var lhsDate: Int32 = 0 + var rhsDate: Int32 = 0 + if let lhs = lhs as? TelegramChannel { + lhsDate = lhs.creationDate + } + if let rhs = rhs as? TelegramChannel { + rhsDate = rhs.creationDate + } + return lhsDate > rhsDate + }) + + for (i, peer) in sorted.enumerated() { + entries.append(.existingLinkPeerItem(sectionId: sectionId, index, peer, nil, state.revokingPeerId == nil, bestGeneralViewType(sorted, for: i))) + index += 1 + } + } else { + entries.append(.publicLinkAvailability(sectionId: sectionId, true, .singleItem)) + } } else { - entries.append(.typeInfo(sectionId: sectionId, tr(.channelPublicAboutChannel))) + entries.append(.editablePublicLink(sectionId: sectionId, peer.addressName, currentAddressName, state.addressNameValidationStatus, .singleItem)) + if let status = state.addressNameValidationStatus { + switch status { + case .invalidFormat, .availability: + entries.append(.publicLinkStatus(sectionId: sectionId, currentAddressName, status, .textBottomItem)) + default: + break + } + } + entries.append(.publicLinkInfo(sectionId: sectionId, isGroup ? L10n.channelUsernameAboutGroup : L10n.channelUsernameAboutChannel, .textBottomItem)) + } + + + if peer.addressName != nil { + entries.append(.section(sectionId: sectionId)) + sectionId += 1 + entries.append(.manageLinks(sectionId: sectionId, .singleItem)) } + case .privateChannel: - if isGroup { - entries.append(.typeInfo(sectionId: sectionId, tr(.channelPrivateAboutGroup))) + entries.append(.privateLinkHeader(sectionId: sectionId, L10n.channelVisibiltiyPermanentLink, .textTopItem)) + entries.append(.privateLink(sectionId: sectionId, (view.cachedData as? CachedChannelData)?.exportedInvitation, importers, isNew, .singleItem)) + entries.append(.publicLinkInfo(sectionId: sectionId, isGroup ? L10n.channelExportLinkAboutGroup : L10n.channelExportLinkAboutChannel, .textBottomItem)) + + entries.append(.section(sectionId: sectionId)) + sectionId += 1 + entries.append(.manageLinks(sectionId: sectionId, .singleItem)) + entries.append(.manageLinksDesc(sectionId: sectionId, .textBottomItem)) + + } + } else if let peer = view.peers[view.peerId] as? TelegramGroup { + + let selectedType: CurrentChannelType + if let current = state.selectedType { + selectedType = current + } else { + if let addressName = peer.addressName, !addressName.isEmpty { + selectedType = .publicChannel + } else { + selectedType = .privateChannel + } + } + + let currentAddressName: String + if let current = state.editingPublicLinkText { + currentAddressName = current + } else { + if let addressName = peer.addressName { + currentAddressName = addressName } else { - entries.append(.typeInfo(sectionId: sectionId, tr(.channelPrivateAboutChannel))) + currentAddressName = "" } } + entries.append(.typeHeader(sectionId: sectionId, L10n.channelTypeHeaderGroup, .textTopItem)) + entries.append(.typePublic(sectionId: sectionId, selectedType == .publicChannel, .firstItem)) + entries.append(.typePrivate(sectionId: sectionId, selectedType == .privateChannel, .lastItem)) + + switch selectedType { + case .publicChannel: + entries.append(.typeInfo(sectionId: sectionId, L10n.channelPublicAboutGroup, .textBottomItem)) + + case .privateChannel: + entries.append(.typeInfo(sectionId: sectionId, L10n.channelPrivateAboutGroup, .textBottomItem)) + } + entries.append(.section(sectionId: sectionId)) sectionId += 1 @@ -457,43 +566,52 @@ private func channelVisibilityControllerEntries(view: PeerView, publicChannelsTo if let publicChannelsToRevoke = publicChannelsToRevoke { - entries.append(.publicLinkAvailability(sectionId: sectionId, false)) + entries.append(.publicLinkAvailability(sectionId: sectionId, false, .singleItem)) var index: Int32 = 0 for peer in publicChannelsToRevoke.sorted(by: { lhs, rhs in var lhsDate: Int32 = 0 var rhsDate: Int32 = 0 - if let lhs = lhs as? TelegramChannel { + if let lhs = lhs as? TelegramGroup { lhsDate = lhs.creationDate } - if let rhs = rhs as? TelegramChannel { + if let rhs = rhs as? TelegramGroup { rhsDate = rhs.creationDate } return lhsDate > rhsDate }) { - entries.append(.existingLinkPeerItem(sectionId: sectionId, index, peer, nil, state.revokingPeerId == nil)) + entries.append(.existingLinkPeerItem(sectionId: sectionId, index, peer, nil, state.revokingPeerId == nil, .singleItem)) index += 1 } } else { - entries.append(.publicLinkAvailability(sectionId: sectionId, true)) + entries.append(.publicLinkAvailability(sectionId: sectionId, true, .textTopItem)) } } else { - entries.append(.editablePublicLink(sectionId: sectionId, peer.addressName, currentAddressName, state.addressNameValidationStatus)) + entries.append(.editablePublicLink(sectionId: sectionId, peer.addressName, currentAddressName, state.addressNameValidationStatus, .singleItem)) if let status = state.addressNameValidationStatus { switch status { case .invalidFormat, .availability: - entries.append(.publicLinkStatus(sectionId: sectionId, currentAddressName, status)) + entries.append(.publicLinkStatus(sectionId: sectionId, currentAddressName, status, .singleItem)) default: break } } - entries.append(.publicLinkInfo(sectionId: sectionId, isGroup ? tr(.channelUsernameAboutGroup) : tr(.channelUsernameAboutChannel))) + entries.append(.publicLinkInfo(sectionId: sectionId, L10n.channelUsernameAboutGroup, .textBottomItem)) } + case .privateChannel: - entries.append(.privateLink(sectionId: sectionId, (view.cachedData as? CachedChannelData)?.exportedInvitation?.link)) - entries.append(.publicLinkInfo(sectionId: sectionId, isGroup ? tr(.channelExportLinkAboutGroup) : tr(.channelExportLinkAboutChannel))) + entries.append(.privateLinkHeader(sectionId: sectionId, L10n.channelVisibiltiyPermanentLink, .textTopItem)) + entries.append(.privateLink(sectionId: sectionId, (view.cachedData as? CachedGroupData)?.exportedInvitation, importers, isNew, .singleItem)) + entries.append(.publicLinkInfo(sectionId: sectionId, L10n.channelExportLinkAboutGroup, .textBottomItem)) + + entries.append(.section(sectionId: sectionId)) + sectionId += 1 + entries.append(.manageLinks(sectionId: sectionId, .singleItem)) + } } - + entries.append(.section(sectionId: sectionId)) + sectionId += 1 + return entries } private func effectiveChannelType(state: ChannelVisibilityControllerState, peer: TelegramChannel) -> CurrentChannelType { @@ -510,51 +628,62 @@ private func effectiveChannelType(state: ChannelVisibilityControllerState, peer: return selectedType } -private func updatedAddressName(state: ChannelVisibilityControllerState, peer: TelegramChannel) -> String? { - let selectedType = effectiveChannelType(state: state, peer: peer) - - let currentAddressName: String - - switch selectedType { - case .privateChannel: - currentAddressName = "" - case .publicChannel: - if let current = state.editingPublicLinkText { - currentAddressName = current - } else { - if let addressName = peer.addressName { - currentAddressName = addressName +private func updatedAddressName(state: ChannelVisibilityControllerState, peer: Peer) -> String? { + if let peer = peer as? TelegramChannel { + let selectedType = effectiveChannelType(state: state, peer: peer) + + let currentAddressName: String + + switch selectedType { + case .privateChannel: + currentAddressName = "" + case .publicChannel: + if let current = state.editingPublicLinkText { + currentAddressName = current } else { - currentAddressName = "" + if let addressName = peer.addressName { + currentAddressName = addressName + } else { + currentAddressName = "" + } } } - } - - if !currentAddressName.isEmpty { - if currentAddressName != peer.addressName { + + if !currentAddressName.isEmpty { + if currentAddressName != peer.addressName { + return currentAddressName + } else { + return nil + } + } else if peer.addressName != nil { + return "" + } else { + return nil + } + } else if let _ = peer as? TelegramGroup { + let currentAddressName = state.editingPublicLinkText ?? "" + if !currentAddressName.isEmpty { return currentAddressName } else { return nil } - } else if peer.addressName != nil { - return "" } else { return nil } } + + fileprivate func prepareTransition(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], initialSize:NSSize, arguments:ChannelVisibilityControllerArguments) -> TableUpdateTransition { - let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in return entry.entry.item(arguments, initialSize: initialSize) } - return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true) } -class ChannelVisibilityController: EmptyComposeController { +class ChannelVisibilityController: EmptyComposeController { fileprivate let statePromise = ValuePromise(ChannelVisibilityControllerState(), ignoreRepeated: true) fileprivate let stateValue = Atomic(value: ChannelVisibilityControllerState()) @@ -567,11 +696,25 @@ class ChannelVisibilityController: EmptyComposeController let exportedLinkDisposable = MetaDisposable() let peerId:PeerId let onlyUsername:Bool - - init(account:Account, peerId:PeerId, onlyUsername: Bool = false) { + let isChannel: Bool + let linksManager: InviteLinkPeerManager? + let isNew: Bool + init(_ context: AccountContext, peerId:PeerId, isChannel: Bool, onlyUsername: Bool = false, isNew: Bool = false, linksManager: InviteLinkPeerManager? = nil) { self.peerId = peerId self.onlyUsername = onlyUsername - super.init(account) + self.isChannel = isChannel + self.isNew = isNew + self.linksManager = linksManager + + super.init(context) + } + + override var defaultBarTitle: String { + if isChannel { + return L10n.telegramChannelVisibilityControllerChannel + } else { + return L10n.telegramChannelVisibilityControllerGroup + } } override var enableBack: Bool { @@ -581,14 +724,31 @@ class ChannelVisibilityController: EmptyComposeController return .invokeNext } - override var removeAfterDisapper: Bool { + override func becomeFirstResponder() -> Bool? { return true } + override func firstResponder() -> NSResponder? { + var responder: NSResponder? + genericView.enumerateViews { view -> Bool in + if responder == nil, let firstResponder = view.firstResponder { + responder = firstResponder + return false + } + return true + } + return responder + } + + override func viewDidLoad() { super.viewDidLoad() - let account = self.account + genericView.getBackgroundColor = { + theme.colors.listBackground + } + + let context = self.context let peerId = self.peerId let onlyUsername = self.onlyUsername @@ -599,17 +759,16 @@ class ChannelVisibilityController: EmptyComposeController } - peersDisablingAddressNameAssignment.set(.single(nil) |> then(channelAddressNameAssignmentAvailability(account: account, peerId: peerId) |> mapToSignal { result -> Signal<[Peer]?, NoError> in - + peersDisablingAddressNameAssignment.set(.single(nil) |> then(context.engine.peers.channelAddressNameAssignmentAvailability(peerId: peerId.namespace == Namespaces.Peer.CloudChannel ? peerId : nil) |> mapToSignal { result -> Signal<[Peer]?, NoError> in if case .addressNameLimitReached = result { - return adminedPublicChannels(account: account) + return context.engine.peers.adminedPublicChannels() |> map { Optional($0) } } else { return .single([]) } })) - let arguments = ChannelVisibilityControllerArguments(account: account, updateCurrentType: { type in + let arguments = ChannelVisibilityControllerArguments(context: context, updateCurrentType: { type in updateState { state in return state.withUpdatedSelectedType(type) } @@ -629,7 +788,7 @@ class ChannelVisibilityController: EmptyComposeController return state.withUpdatedEditingPublicLinkText(text) } - self?.checkAddressNameDisposable.set((validateAddressNameInteractive(account: account, domain: .peer(peerId), name: text) + self?.checkAddressNameDisposable.set((context.engine.peers.validateAddressNameInteractive(domain: .peer(peerId), name: text) |> deliverOnMainQueue).start(next: { result in updateState { state in return state.withUpdatedAddressNameValidationStatus(result) @@ -637,14 +796,22 @@ class ChannelVisibilityController: EmptyComposeController })) } }, displayPrivateLinkMenu: { [weak self] text in - self?.show(toaster: ControllerToaster(text: tr(.shareLinkCopied))) + self?.show(toaster: ControllerToaster(text: tr(L10n.shareLinkCopied))) copyToClipboard(text) }, revokePeerId: { [weak self] peerId in updateState { state in return state.withUpdatedRevokingPeerId(peerId) } - self?.revokeAddressNameDisposable.set((updateAddressName(account: account, domain: .peer(peerId), name: nil) |> deliverOnMainQueue).start(error: { _ in + self?.revokeAddressNameDisposable.set((confirmSignal(for: context.window, information: L10n.channelVisibilityConfirmRevoke) |> mapToSignalPromotingError { result -> Signal in + if !result { + return .fail(.generic) + } else { + return .single(true) + } + } |> mapToSignal { _ -> Signal in + return context.engine.peers.updateAddressName(domain: .peer(peerId), name: nil) + } |> deliverOnMainQueue).start(error: { _ in updateState { state in return state.withUpdatedRevokingPeerId(nil) } @@ -654,16 +821,55 @@ class ChannelVisibilityController: EmptyComposeController } self?.peersDisablingAddressNameAssignment.set(.single([])) })) + }, copy: { [weak self] link in + self?.show(toaster: ControllerToaster(text: L10n.shareLinkCopied)) + copyToClipboard(link) + }, revokeLink: { + confirm(for: context.window, header: L10n.channelRevokeLinkConfirmHeader, information: L10n.channelRevokeLinkConfirmText, okTitle: L10n.channelRevokeLinkConfirmOK, cancelTitle: L10n.modalCancel, successHandler: { _ in + _ = showModalProgress(signal: context.engine.peers.revokePersistentPeerExportedInvitation(peerId: peerId), for: context.window).start() + }) + }, share: { link in + showModal(with: ShareModalController(ShareLinkObject.init(context, link: link)), for: context.window) + }, manageLinks: { [weak self] in + self?.navigationController?.push(InviteLinksController(context: context, peerId: peerId, manager: self?.linksManager)) + }, open: { [weak self] invitation in + if let manager = self?.linksManager { + showModal(with: ExportedInvitationController(invitation: invitation, peerId: peerId, accountContext: context, manager: manager, context: manager.importer(for: invitation)), for: context.window) + } }) - let peerView = account.viewTracker.peerView(peerId) + let peerView = context.account.viewTracker.peerView(peerId) let initialSize = atomicSize let previousEntries:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) - let apply = combineLatest(statePromise.get(), peerView, peersDisablingAddressNameAssignment.get(), appearanceSignal) - |> map { state, view, publicChannelsToRevoke, appearance -> (TableUpdateTransition, Peer?, Bool) in + let permanentLink = peerView |> map { + ($0.cachedData as? CachedChannelData)?.exportedInvitation ?? ($0.cachedData as? CachedGroupData)?.exportedInvitation + } + + let manager = self.linksManager + let isNew = self.isNew + + let importers: Signal = permanentLink |> deliverOnMainQueue |> mapToSignal { [weak manager] permanent in + if let permanent = permanent { + if enableBetaFeatures { + if let state = manager?.importer(for: permanent).state { + return state |> map(Optional.init) + } else { + return .single(nil) + } + } else { + return .single(nil) + } + + } else { + return .single(nil) + } + } + + let apply = combineLatest(queue: .mainQueue(), statePromise.get(), peerView, peersDisablingAddressNameAssignment.get(), importers, appearanceSignal) + |> map { state, view, publicChannelsToRevoke, importers, appearance -> (TableUpdateTransition, Peer?, Bool) in let peer = peerViewMainPeer(view) var doneEnabled = true @@ -687,7 +893,7 @@ class ChannelVisibilityController: EmptyComposeController } } - let entries = channelVisibilityControllerEntries(view: view, publicChannelsToRevoke: publicChannelsToRevoke, state: state, onlyUsername: onlyUsername).map {AppearanceWrapperEntry(entry: $0, appearance: appearance)} + let entries = channelVisibilityControllerEntries(view: view, publicChannelsToRevoke: publicChannelsToRevoke, state: state, onlyUsername: onlyUsername, importers: importers, isNew: isNew).map {AppearanceWrapperEntry(entry: $0, appearance: appearance)} return (prepareTransition(left: previousEntries.swap(entries), right: entries, initialSize: initialSize.modify({$0}), arguments: arguments), peer, doneEnabled) } |> deliverOnMainQueue @@ -700,10 +906,9 @@ class ChannelVisibilityController: EmptyComposeController strongSelf.doneButton?.isEnabled = doneEnabled strongSelf.doneButton?.removeAllHandlers() strongSelf.doneButton?.set(handler: { [weak self] _ in - - var updatedAddressNameValue: String? - self?.updateState { state in - if let peer = peer as? TelegramChannel { + if let peer = peer { + var updatedAddressNameValue: String? + self?.updateState { state in updatedAddressNameValue = updatedAddressName(state: state, peer: peer) if updatedAddressNameValue != nil { @@ -711,55 +916,111 @@ class ChannelVisibilityController: EmptyComposeController } else { return state } - } else { - return state } - } - - if let updatedAddressNameValue = updatedAddressNameValue { - self?.updateAddressNameDisposable.set((updateAddressName(account: account, domain: .peer(peerId), name: updatedAddressNameValue.isEmpty ? nil : updatedAddressNameValue) - |> deliverOnMainQueue).start(error: { [weak self] _ in - self?.updateState { state in - return state.withUpdatedUpdatingAddressName(false) + + if let updatedAddressNameValue = updatedAddressNameValue { + + + let signal: Signal + + let csignal: Signal + + if updatedAddressNameValue.isEmpty && peer.addressName != updatedAddressNameValue, let address = peer.addressName { + let text: String + if peer.isChannel { + text = L10n.channelVisibilityConfirmMakePrivateChannel(address) + } else { + text = L10n.channelVisibilityConfirmMakePrivateGroup(address) } - }, completed: { [weak self] in - self?.updateState { state in + csignal = confirmSignal(for: context.window, information: text) |> filter { $0 } |> take(1) |> map { _ in + updateState { state in + return state.withUpdatedUpdatingAddressName(true) + } + } |> castError(UpdateAddressNameError.self) + } else { + csignal = .single(Void()) |> map { + updateState { state in + return state.withUpdatedUpdatingAddressName(true) + } + } + } + + if peer.isGroup { + + signal = context.engine.peers.convertGroupToSupergroup(peerId: peerId) + |> mapToSignal { upgradedPeerId -> Signal in + return csignal + |> mapToSignal { + + showModalProgress(signal: context.engine.peers.updateAddressName(domain: .peer(upgradedPeerId), name: updatedAddressNameValue.isEmpty ? nil : updatedAddressNameValue), for: context.window) + } + |> mapError {_ in return ConvertGroupToSupergroupError.generic} + |> mapToSignal { _ in + return .single(Optional(upgradedPeerId)) + } + } + |> deliverOnMainQueue + } else { + + signal = csignal + |> mapToSignal { + showModalProgress(signal: context.engine.peers.updateAddressName(domain: .peer(peerId), name: updatedAddressNameValue.isEmpty ? nil : updatedAddressNameValue), for: context.window) + } + |> mapToSignal { _ in + return .single(nil) + } + |> mapError {_ in + return ConvertGroupToSupergroupError.generic + } + } + + self?.updateAddressNameDisposable.set(signal.start(next: { updatedPeerId in + self?.onComplete.set(.single(updatedPeerId)) + }, error: { error in + switch error { + case .tooManyChannels: + showInactiveChannels(context: context, source: .upgrade) + case .generic: + alert(for: context.window, info: L10n.unknownError) + } + updateState { state in return state.withUpdatedUpdatingAddressName(false) } - self?.onComplete.set(.single(true)) })) - } else { - self?.onComplete.set(.single(true)) + } else { + self?.onComplete.set(.single(nil)) + } } + }, for: .SingleClick) } })) - exportedLinkDisposable.set((account.viewTracker.peerView(peerId) |> filter { $0.cachedData != nil } |> take(1) |> mapToSignal { _ in return ensuredExistingPeerExportedInvitation(account: account, peerId: peerId)}).start()) - + exportedLinkDisposable.set(context.account.viewTracker.peerView(peerId, updateData: true).start()) + } private func updateState (_ f:@escaping (ChannelVisibilityControllerState) -> ChannelVisibilityControllerState) -> Void { statePromise.set(stateValue.modify { f($0) }) } - var doneButton:Button? { - if let button = rightBarView as? TextButtonBarView { - return button.button - } - return nil + var doneButton:Control? { + return rightBarView } override func getRightBarViewOnce() -> BarView { - let button = TextButtonBarView(controller: self, text: tr(.navigationDone)) + let button = TextButtonBarView(controller: self, text: tr(L10n.navigationDone)) return button } deinit { - var bp:Int = 0 - bp += 1 + checkAddressNameDisposable.dispose() + updateAddressNameDisposable.dispose() + revokeAddressNameDisposable.dispose() + disposable.dispose() + exportedLinkDisposable.dispose() } } diff --git a/Telegram-Mac/ChatAccessoryModel.swift b/Telegram-Mac/ChatAccessoryModel.swift index f51dcac96b..b6456f1d23 100644 --- a/Telegram-Mac/ChatAccessoryModel.swift +++ b/Telegram-Mac/ChatAccessoryModel.swift @@ -8,12 +8,24 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac +import SwiftSignalKit class ChatAccessoryView : Control { var imageView: TransformImageView? } +struct ChatAccessoryPresentation { + let background: NSColor + let title: NSColor + let enabledText: NSColor + let disabledText: NSColor + let border: NSColor + + func withUpdatedBackground(_ backgroundColor: NSColor) -> ChatAccessoryPresentation { + return ChatAccessoryPresentation(background: backgroundColor, title: title, enabledText: enabledText, disabledText: disabledText, border: border) + } +} + class ChatAccessoryModel: NSObject, ViewDisplayDelegate { @@ -21,7 +33,22 @@ class ChatAccessoryModel: NSObject, ViewDisplayDelegate { open var backgroundColor:NSColor { - return view?.backgroundColor ?? theme.colors.background + didSet { + self.presentation = presentation.withUpdatedBackground(backgroundColor) + } + } + + var isSideAccessory: Bool = false + + private var _presentation: ChatAccessoryPresentation? = nil + var presentation: ChatAccessoryPresentation { + set { + _presentation = newValue + view?.needsDisplay = true + } + get { + return _presentation ?? ChatAccessoryPresentation(background: theme.colors.background, title: theme.colors.accent, enabledText: theme.colors.text, disabledText: theme.colors.grayText, border: theme.colors.accent) + } } private let _strongView:ChatAccessoryView? @@ -34,13 +61,16 @@ class ChatAccessoryModel: NSObject, ViewDisplayDelegate { } open var size:NSSize = NSZeroSize - + let drawLine: Bool + var width: CGFloat = 0 + var sizeToFit: Bool = false open var frame:NSRect { get { return self.view?.frame ?? NSZeroRect } set { self.view?.frame = newValue + self.view?.needsDisplay = true } } @@ -50,11 +80,14 @@ class ChatAccessoryModel: NSObject, ViewDisplayDelegate { } } - public init(_ view:ChatAccessoryView? = nil) { + public init(_ view:ChatAccessoryView? = nil, presentation: ChatAccessoryPresentation? = nil, drawLine: Bool = true) { _strongView = view + self.drawLine = drawLine + _presentation = presentation if view != nil { assertOnMainThread() } + backgroundColor = theme.colors.background super.init() self.view = view @@ -83,7 +116,7 @@ class ChatAccessoryModel: NSObject, ViewDisplayDelegate { let yInset:CGFloat = 2 var leftInset:CGFloat { - return 8 + return drawLine ? 6 : 8 } var headerAttr:NSAttributedString? @@ -95,11 +128,18 @@ class ChatAccessoryModel: NSObject, ViewDisplayDelegate { var header:(TextNodeLayout, TextNode)? var message:(TextNodeLayout, TextNode)? - func measureSize(_ width:CGFloat = 0) -> Void { + var topOffset: CGFloat = 0 + + + func measureSize(_ width:CGFloat = 0, sizeToFit: Bool = false) -> Void { + self.sizeToFit = sizeToFit header = TextNode.layoutText(maybeNode: headerNode, headerAttr, nil, 1, .end, NSMakeSize(width - leftInset, 20), nil,false, .left) message = TextNode.layoutText(maybeNode: messageNode, messageAttr, nil, 1, .end, NSMakeSize(width - leftInset, 20), nil,false, .left) //max(header!.0.size.width,message!.0.size.width) + leftInset - size = NSMakeSize(width, max(34, header!.0.size.height + message!.0.size.height + yInset)) + self.width = width + size = NSMakeSize(sizeToFit ? max(header!.0.size.width,message!.0.size.width) + leftInset + (isSideAccessory ? 20 : 0) : width, max(34, header!.0.size.height + message!.0.size.height + yInset + (isSideAccessory ? 10 : 0))) + size.height += topOffset + // super.measureSize(width) } @@ -108,22 +148,24 @@ class ChatAccessoryModel: NSObject, ViewDisplayDelegate { func draw(_ layer: CALayer, in ctx: CGContext) { if let view = view { - ctx.setFillColor(backgroundColor.cgColor) + ctx.setFillColor(presentation.background.cgColor) ctx.fill(layer.bounds) - ctx.setFillColor(theme.colors.blueFill.cgColor) + ctx.setFillColor(presentation.border.cgColor) - let radius:CGFloat = 1.0 - ctx.fill(NSMakeRect(0, radius, 2, layer.bounds.height - radius * 2)) - ctx.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: radius + radius, height: radius + radius))) - ctx.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: layer.bounds.height - radius * 2), size: CGSize(width: radius + radius, height: radius + radius))) + if drawLine { + let radius:CGFloat = 1.0 + ctx.fill(NSMakeRect((isSideAccessory ? 10 : 0), radius + (isSideAccessory ? 5 : 0) + topOffset, 2, size.height - topOffset - radius * 2 - (isSideAccessory ? 10 : 0))) + ctx.fillEllipse(in: CGRect(origin: CGPoint(x: (isSideAccessory ? 10 : 0), y: (isSideAccessory ? 5 : 0) + topOffset), size: CGSize(width: radius + radius, height: radius + radius))) + ctx.fillEllipse(in: CGRect(origin: CGPoint(x: (isSideAccessory ? 10 : 0), y: size.height - radius * 2 - (isSideAccessory ? 5 : 0)), size: CGSize(width: radius + radius, height: radius + radius))) + } if let header = header, let message = message { - header.1.draw(NSMakeRect(leftInset, 0, header.0.size.width, header.0.size.height), in: ctx, backingScaleFactor: view.backingScaleFactor) + header.1.draw(NSMakeRect(leftInset + (isSideAccessory ? 10 : 0), (isSideAccessory ? 5 : 0) + topOffset, header.0.size.width, header.0.size.height), in: ctx, backingScaleFactor: view.backingScaleFactor, backgroundColor: presentation.background) if headerAttr == nil { - message.1.draw(NSMakeRect(leftInset, floorToScreenPixels((size.height - message.0.size.height)/2), message.0.size.width, message.0.size.height), in: ctx, backingScaleFactor: view.backingScaleFactor) + message.1.draw(NSMakeRect(leftInset + (isSideAccessory ? 10 : 0), floorToScreenPixels(view.backingScaleFactor, topOffset + (size.height - topOffset - message.0.size.height)/2), message.0.size.width, message.0.size.height), in: ctx, backingScaleFactor: view.backingScaleFactor, backgroundColor: presentation.background) } else { - message.1.draw(NSMakeRect(leftInset, header.0.size.height + yInset, message.0.size.width, message.0.size.height), in: ctx, backingScaleFactor: view.backingScaleFactor) + message.1.draw(NSMakeRect(leftInset + (isSideAccessory ? 10 : 0), header.0.size.height + yInset + (isSideAccessory ? 5 : 0) + topOffset, message.0.size.width, message.0.size.height), in: ctx, backingScaleFactor: view.backingScaleFactor, backgroundColor: presentation.background) } } } diff --git a/Telegram-Mac/ChatActivitiesModel.swift b/Telegram-Mac/ChatActivitiesModel.swift index 383e7e102f..415e4b6870 100644 --- a/Telegram-Mac/ChatActivitiesModel.swift +++ b/Telegram-Mac/ChatActivitiesModel.swift @@ -8,15 +8,17 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac +import TelegramCore + +import SwiftSignalKit +import Postbox enum ChatActivityAnimation { case none case text case uploading case recording + case choosingSticker } @@ -25,7 +27,7 @@ class ChatActivitiesView : View { private let textView:TextView = TextView() - private let animationView:ImageView = ImageView(frame: NSMakeRect(0,-4,30,20)) + private let animationView:NSView = NSView(frame: NSMakeRect(0,-4,30,20)) private var isAnimating:Bool = false private var type:ChatActivityAnimation? @@ -33,8 +35,10 @@ class ChatActivitiesView : View { override init() { super.init() + animationView.wantsLayer = true addSubview(textView) addSubview(animationView) + layer?.disableActions() } override func layout() { @@ -56,32 +60,56 @@ class ChatActivitiesView : View { } else { startAnimation(params.0, theme:theme) } - if let layout = params.1 { setFrameSize(layout.layoutSize.width + animationView.frame.width, layout.layoutSize.height) } textView.update(params.1, origin:NSMakePoint(animationView.frame.width , 0)) + + if let type = type { + switch type { + case .choosingSticker: + animationView.setFrameOrigin(NSMakePoint(0, -1)) + default: + animationView.setFrameOrigin(NSMakePoint(0, -4)) + } + } } func startAnimation(_ type:ChatActivityAnimation, theme:ActivitiesTheme) { - self.type = type - self.theme = theme - isAnimating = true - let animation = CAKeyframeAnimation(keyPath: "contents") - switch type { - case .recording: - animation.values = theme.recording - animation.duration = 0.7 - case .uploading: - animation.values = theme.uploading - animation.duration = 1.75 - default: - animation.values = theme.text - animation.duration = 0.7 + if self.type != type || theme != self.theme { + self.type = type + self.theme = theme + isAnimating = true + let animation = CAKeyframeAnimation(keyPath: "contents") + switch type { + case .recording: + animationView.layer?.contents = theme.recording.first + animationView.setFrameSize(theme.recording.first!.backingSize) + + animation.values = theme.recording + animation.duration = 0.7 + case .uploading: + animationView.layer?.contents = theme.uploading.first + animationView.setFrameSize(theme.uploading.first!.backingSize) + animation.values = theme.uploading + animation.duration = 1.75 + case .choosingSticker: + animationView.layer?.contents = theme.choosingSticker.first + animationView.setFrameSize(theme.choosingSticker.first!.backingSize) + animation.values = theme.choosingSticker + animation.duration = 2.0 + default: + animationView.layer?.contents = theme.text.first + animationView.setFrameSize(theme.recording.first!.backingSize) + animation.values = theme.text + animation.duration = 0.7 + } + + animationView.layer?.removeAllAnimations() + animation.repeatCount = .infinity + animation.isRemovedOnCompletion = false + animationView.layer?.add(animation, forKey: "contents") } - animation.repeatCount = .greatestFiniteMagnitude - animationView.layer?.add(animation, forKey: "contents") - } override func viewDidMoveToWindow() { @@ -98,9 +126,6 @@ class ChatActivitiesView : View { if realyStop { isAnimating = false } - animationView.layer?.removeAllAnimations() - animationView.image = nil - animationView.sizeToFit() } required init(frame frameRect: NSRect) { @@ -120,9 +145,10 @@ class ChatActivitiesModel: Node { private(set) var isActive:Bool = false private let activityView:ChatActivitiesView private let disposable:MetaDisposable = MetaDisposable() - + private(set) var theme: ActivitiesTheme? func update(with activities:(PeerId, [(Peer, PeerInputActivity)]), for width:CGFloat, theme:ActivitiesTheme, layout:@escaping(Bool)->Void) { isActive = !activities.1.isEmpty + self.theme = theme activityView.updateBackground(theme.backgroundColor) disposable.set(renderedActivities(activities, for: width, theme:theme).start(next: { [weak self] data in self?.activityView.layout(with: data, width: width, theme: theme) @@ -130,11 +156,10 @@ class ChatActivitiesModel: Node { })) } - private func renderedActivities(_ activities:(PeerId, [(Peer, PeerInputActivity)]), for width:CGFloat, theme: ActivitiesTheme) -> Signal <(ChatActivityAnimation, TextViewLayout?), Void> { + private func renderedActivities(_ activities:(PeerId, [(Peer, PeerInputActivity)]), for width:CGFloat, theme: ActivitiesTheme) -> Signal <(ChatActivityAnimation, TextViewLayout?), NoError> { return Signal { subscriber in - if !activities.1.isEmpty { let layout:TextViewLayout var animation:ChatActivityAnimation = .text @@ -153,35 +178,40 @@ class ChatActivitiesModel: Node { } if isFew { + let firstTitle: String = activities.1[0].0.displayTitle if sameActivity { let activity = activities.1[0].1 switch activity { case .recordingVoice: animation = .recording - _ = text.append(string: tr(.peerActivityChatMultiRecordingAudio(activities.1.count)), color: theme.textColor, font: .normal(.text)) + _ = text.append(string: + tr(L10n.peerActivityChatMultiRecordingAudio1(firstTitle, activities.1.count - 1)), color: theme.textColor, font: .normal(.text)) case .uploadingFile: animation = .uploading - _ = text.append(string: tr(.peerActivityChatMultiSendingFile(activities.1.count)), color: theme.textColor, font: .normal(.text)) + _ = text.append(string: tr(L10n.peerActivityChatMultiSendingFile1(firstTitle, activities.1.count - 1)), color: theme.textColor, font: .normal(.text)) case .uploadingPhoto: animation = .uploading - _ = text.append(string: tr(.peerActivityChatMultiSendingPhoto(activities.1.count)), color: theme.textColor, font: .normal(.text)) + _ = text.append(string: tr(L10n.peerActivityChatMultiSendingPhoto1(firstTitle, activities.1.count - 1)), color: theme.textColor, font: .normal(.text)) case .uploadingVideo: animation = .uploading - _ = text.append(string: tr(.peerActivityChatMultiSendingVideo(activities.1.count)), color: theme.textColor, font: .normal(.text)) - //case .playingGame: - - //_ = text.append(string: tr(.peerActivityChatMultiPlayingGame(activities.1.count)), color: theme.textColor, font: .normal(.text)) + _ = text.append(string: tr(L10n.peerActivityChatMultiSendingVideo1(firstTitle, activities.1.count - 1)), color: theme.textColor, font: .normal(.text)) case .recordingInstantVideo: animation = .recording - _ = text.append(string: tr(.peerActivityChatMultiRecordingVideo(activities.1.count)), color: theme.textColor, font: .normal(.text)) + _ = text.append(string: tr(L10n.peerActivityChatMultiRecordingVideo1(firstTitle, activities.1.count - 1)), color: theme.textColor, font: .normal(.text)) + case .choosingSticker: + animation = .choosingSticker + _ = text.append(string: tr(L10n.peerActivityChatMultiChoosingSticker1(firstTitle, activities.1.count - 1)), color: theme.textColor, font: .normal(.text)) + case .playingGame: + animation = .text + _ = text.append(string: tr(L10n.peerActivityChatMultiPlayingGame1(firstTitle, activities.1.count - 1)), color: theme.textColor, font: .normal(.text)) default: animation = .text - break + _ = text.append(string: tr(L10n.peerActivityChatMultiTypingText1(firstTitle, activities.1.count - 1)), color: theme.textColor, font: .normal(.text)) } } else { animation = .text if activities.1.count > 2 { - _ = text.append(string: tr(.peerActivityChatMultiTypingText(activities.1.count)), color: theme.textColor, font: .normal(.text)) + _ = text.append(string: tr(L10n.peerActivityChatMultiTypingText1(firstTitle, activities.1.count - 1)), color: theme.textColor, font: .normal(.text)) } else { let names = activities.1.map({$0.0.compactDisplayTitle}).joined(separator: ", ") _ = text.append(string: names, color: theme.textColor, font: .normal(.text)) @@ -194,47 +224,70 @@ class ChatActivitiesModel: Node { case .recordingVoice: animation = .recording if activities.0.namespace == Namespaces.Peer.CloudUser || activities.0.namespace == Namespaces.Peer.SecretChat { - _ = text.append(string: tr(.peerActivityUserRecordingAudio), color: theme.textColor, font: .normal(.text)) + _ = text.append(string: tr(L10n.peerActivityUserRecordingAudio), color: theme.textColor, font: .normal(.text)) } else { - _ = text.append(string: peer.compactDisplayTitle, color: theme.textColor, font: .normal(.text)) + _ = text.append(string: L10n.peerActivityChatRecordingAudio(peer.compactDisplayTitle), color: theme.textColor, font: .normal(.text)) } case .uploadingFile: animation = .uploading if activities.0.namespace == Namespaces.Peer.CloudUser || activities.0.namespace == Namespaces.Peer.SecretChat { - _ = text.append(string:tr(.peerActivityUserSendingFile), color: theme.textColor, font: .normal(.text)) + _ = text.append(string:tr(L10n.peerActivityUserSendingFile), color: theme.textColor, font: .normal(.text)) } else { - _ = text.append(string: peer.compactDisplayTitle, color: theme.textColor, font: .normal(.text)) + _ = text.append(string: L10n.peerActivityChatSendingFile(peer.compactDisplayTitle), color: theme.textColor, font: .normal(.text)) } case .uploadingVideo: animation = .uploading if activities.0.namespace == Namespaces.Peer.CloudUser || activities.0.namespace == Namespaces.Peer.SecretChat { - _ = text.append(string:tr(.peerActivityUserSendingVideo), color: theme.textColor, font: .normal(.text)) + _ = text.append(string:tr(L10n.peerActivityUserSendingVideo), color: theme.textColor, font: .normal(.text)) } else { - _ = text.append(string: peer.compactDisplayTitle, color: theme.textColor, font: .normal(.text)) + _ = text.append(string: L10n.peerActivityChatSendingVideo(peer.compactDisplayTitle), color: theme.textColor, font: .normal(.text)) } case .uploadingPhoto: animation = .uploading if activities.0.namespace == Namespaces.Peer.CloudUser || activities.0.namespace == Namespaces.Peer.SecretChat { - _ = text.append(string:tr(.peerActivityUserSendingPhoto), color: theme.textColor, font: .normal(.text)) + _ = text.append(string:tr(L10n.peerActivityUserSendingPhoto), color: theme.textColor, font: .normal(.text)) + } else { + _ = text.append(string: L10n.peerActivityChatSendingPhoto(peer.compactDisplayTitle), color: theme.textColor, font: .normal(.text)) + } + case .choosingSticker: + animation = .choosingSticker + if activities.0.namespace == Namespaces.Peer.CloudUser || activities.0.namespace == Namespaces.Peer.SecretChat { + + _ = text.append(string:tr(L10n.peerActivityUserChoosingSticker), color: theme.textColor, font: .normal(.text)) + } else { + _ = text.append(string: L10n.peerActivityChatChoosingSticker(peer.compactDisplayTitle), color: theme.textColor, font: .normal(.text)) + } + case let .seeingEmojiInteraction(emoticon): + animation = .choosingSticker + if activities.0.namespace == Namespaces.Peer.CloudUser || activities.0.namespace == Namespaces.Peer.SecretChat { + + _ = text.append(string:tr(L10n.peerActivityUserEnjoyingAnimations(emoticon)), color: theme.textColor, font: .normal(.text)) } else { - _ = text.append(string: peer.compactDisplayTitle, color: theme.textColor, font: .normal(.text)) + _ = text.append(string: L10n.peerActivityChatEnjoyingAnimations(peer.compactDisplayTitle, emoticon), color: theme.textColor, font: .normal(.text)) } case .recordingInstantVideo: animation = .recording if activities.0.namespace == Namespaces.Peer.CloudUser || activities.0.namespace == Namespaces.Peer.SecretChat { - _ = text.append(string: tr(.peerActivityUserRecordingVideo), color: theme.textColor, font: .normal(.text)) + _ = text.append(string: tr(L10n.peerActivityUserRecordingVideo), color: theme.textColor, font: .normal(.text)) } else { - _ = text.append(string: peer.compactDisplayTitle, color: theme.textColor, font: .normal(.text)) + _ = text.append(string: L10n.peerActivityChatRecordingVideo(peer.compactDisplayTitle), color: theme.textColor, font: .normal(.text)) + } + case .playingGame: + animation = .text + if activities.0.namespace == Namespaces.Peer.CloudUser || activities.0.namespace == Namespaces.Peer.SecretChat { + _ = text.append(string: L10n.peerActivityUserPlayingGame, color: theme.textColor, font: .normal(.text)) + } else { + _ = text.append(string: L10n.peerActivityChatPlayingGame(peer.compactDisplayTitle), color: theme.textColor, font: .normal(.text)) } default: animation = .text if activities.0.namespace == Namespaces.Peer.CloudUser || activities.0.namespace == Namespaces.Peer.SecretChat { - _ = text.append(string: tr(.peerActivityUserTypingText), color: theme.textColor, font: .normal(.text)) + _ = text.append(string: tr(L10n.peerActivityUserTypingText), color: theme.textColor, font: .normal(.text)) } else { - _ = text.append(string: peer.compactDisplayTitle, color: theme.textColor, font: .normal(.text)) + _ = text.append(string: L10n.peerActivityChatTypingText(peer.compactDisplayTitle), color: theme.textColor, font: .normal(.text)) } } } @@ -264,7 +317,6 @@ class ChatActivitiesModel: Node { init() { self.activityView = ChatActivitiesView() super.init(activityView) - } } diff --git a/Telegram-Mac/ChatAnimatedStickerItem.swift b/Telegram-Mac/ChatAnimatedStickerItem.swift new file mode 100644 index 0000000000..5eb6cc2113 --- /dev/null +++ b/Telegram-Mac/ChatAnimatedStickerItem.swift @@ -0,0 +1,42 @@ +// +// ChatAnimatedStickerItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 13/05/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import SwiftSignalKit +import Postbox + +final class ChatAnimatedStickerMediaLayoutParameters : ChatMediaLayoutParameters { + let playPolicy: LottiePlayPolicy? + let alwaysAccept: Bool? + let cache: ASCachePurpose? + let hidePlayer: Bool? + let colors: [LottieColor] + let playOnHover: Bool? + init(playPolicy: LottiePlayPolicy?, alwaysAccept: Bool? = nil, cache: ASCachePurpose? = nil, hidePlayer: Bool = false, media: TelegramMediaFile, colors: [LottieColor] = [], playOnHover: Bool? = nil) { + self.playPolicy = playPolicy + self.alwaysAccept = alwaysAccept + self.cache = cache + self.hidePlayer = hidePlayer + self.colors = colors + self.playOnHover = playOnHover + super.init(presentation: .empty, media: media, automaticDownload: true, autoplayMedia: AutoplayMediaPreferences.defaultSettings) + } +} + +class ChatAnimatedStickerItem: ChatMediaItem { + override init(_ initialSize: NSSize, _ chatInteraction: ChatInteraction, _ context: AccountContext, _ object: ChatHistoryEntry, _ downloadSettings: AutomaticMediaDownloadSettings, theme: TelegramPresentationTheme) { + super.init(initialSize, chatInteraction, context, object, downloadSettings, theme: theme) + + let mirror = renderType == .bubble && isIncoming + parameters?.runEmojiScreenEffect = { [weak chatInteraction] emoji in + chatInteraction?.runEmojiScreenEffect(emoji, object.message!.id, mirror, false) + } + } +} diff --git a/Telegram-Mac/ChatAudioContentView.swift b/Telegram-Mac/ChatAudioContentView.swift index 1eddd8126f..d1ba4567a4 100644 --- a/Telegram-Mac/ChatAudioContentView.swift +++ b/Telegram-Mac/ChatAudioContentView.swift @@ -7,9 +7,10 @@ // import Cocoa -import SwiftSignalKitMac -import PostboxMac -import TelegramCoreMac +import SwiftSignalKit +import Postbox +import TelegramCore + import TGUIKit @@ -25,6 +26,7 @@ class ChatAudioContentView: ChatMediaContentView, APDelegate { let statusDisposable = MetaDisposable() let fetchDisposable = MetaDisposable() + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -32,6 +34,8 @@ class ChatAudioContentView: ChatMediaContentView, APDelegate { required init(frame frameRect: NSRect) { super.init(frame:frameRect) textView.isSelectable = false + textView.userInteractionEnabled = false + durationView.userInteractionEnabled = false self.addSubview(textView) self.addSubview(durationView) progressView.fetchControls = fetchControls @@ -39,25 +43,56 @@ class ChatAudioContentView: ChatMediaContentView, APDelegate { } + override var fetchStatus: MediaResourceStatus? { + didSet { + if let fetchStatus = fetchStatus { + + switch fetchStatus { + case let .Fetching(_, progress): + let sentGrouped = parent?.groupingKey != nil && (parent!.flags.contains(.Sending) || parent!.flags.contains(.Unsent)) + if progress == 1.0, sentGrouped { + progressView.state = .Success + } else { + progressView.state = .Fetching(progress: progress, force: false) + } + case .Remote: + progressView.state = .Remote + case .Local: + progressView.state = .Play + } + } + } + } + + override func mouseDown(with event: NSEvent) { +// if mouseInside(), userInteractionEnabled { +// progressView.fetchControls?.fetch() +// } else { +// super.mouseDown(with: event) +// } + } + + + override func layout() { super.layout() textView.centerY(x:leftInset) } + override func open() { - if let parameters = parameters as? ChatMediaMusicLayoutParameters, let account = account, let parent = parent { - if let controller = globalAudio, let song = controller.currentSong, song.entry.isEqual(to: parent) { - controller.playOrPause() - } else { + if let parameters = parameters as? ChatMediaMusicLayoutParameters, let context = context, let parent = parent { + if let controller = globalAudio, controller.playOrPause(parent.id) { + } else { let controller:APController + if parameters.isWebpage { - controller = APSingleResourceController(account: account, wrapper: APSingleWrapper(resource: parameters.resource, name: parameters.title, performer: parameters.performer, id: parent.chatStableId)) + controller = APSingleResourceController(context: context, wrapper: APSingleWrapper(resource: parameters.resource, mimeType: parameters.file.mimeType, name: parameters.title, performer: parameters.performer, duration: parameters.file.duration, id: parent.chatStableId), streamable: true, volume: FastSettings.volumeRate) } else { - controller = APChatMusicController(account: account, peerId: parent.id.peerId, index: MessageIndex(parent)) + controller = APChatMusicController(context: context, chatLocationInput: parameters.chatLocationInput(), mode: parameters.chatMode, index: MessageIndex(parent), volume: FastSettings.volumeRate) } parameters.showPlayer(controller) controller.start() - addGlobalAudioToVisible() } } } @@ -66,104 +101,86 @@ class ChatAudioContentView: ChatMediaContentView, APDelegate { override func fetch() { - if let account = account, let media = media as? TelegramMediaFile { - fetchDisposable.set(chatMessageFileInteractiveFetched(account: account, file: media).start()) + if let context = context, let media = media as? TelegramMediaFile, let parent = parent { + fetchDisposable.set(messageMediaFileInteractiveFetched(context: context, messageId: parent.id, fileReference: FileMediaReference.message(message: MessageReference(parent), media: media)).start()) } - open() } - override func cancelFetching() { - if let account = account, let media = media as? TelegramMediaFile { - chatMessageFileCancelInteractiveFetch(account: account, file: media) - } - } - func songDidChanged(song: APSongItem, for controller: APController) { - checkState() + func songDidChanged(song: APSongItem, for controller: APController, animated: Bool) { + checkState(animated: animated) } - func songDidChangedState(song: APSongItem, for controller: APController) { - checkState() + func songDidChangedState(song: APSongItem, for controller: APController, animated: Bool) { + checkState(animated: animated) } - func songDidStartPlaying(song:APSongItem, for controller:APController) { - + func songDidStartPlaying(song:APSongItem, for controller:APController, animated: Bool) { + checkState(animated: animated) } - func songDidStopPlaying(song:APSongItem, for controller:APController) { - + func songDidStopPlaying(song:APSongItem, for controller:APController, animated: Bool) { + checkState(animated: animated) } - func playerDidChangedTimebase(song:APSongItem, for controller:APController) { + func playerDidChangedTimebase(song:APSongItem, for controller:APController, animated: Bool) { } - func audioDidCompleteQueue(for controller:APController) { + func audioDidCompleteQueue(for controller:APController, animated: Bool) { } - func checkState() { + func checkState(animated: Bool) { + + let presentation: ChatMediaPresentation = parameters?.presentation ?? .Empty + if let parent = parent, let controller = globalAudio, let song = controller.currentSong { if song.entry.isEqual(to: parent), case .playing = song.state { - progressView.theme = RadialProgressTheme(backgroundColor: theme.colors.blueFill, foregroundColor: .white, icon: theme.icons.chatMusicPause, iconInset:NSEdgeInsets(left:1)) + progressView.theme = RadialProgressTheme(backgroundColor: presentation.activityBackground, foregroundColor: presentation.activityForeground, icon: presentation.pauseThumb, iconInset:NSEdgeInsets(left:0)) + progressView.state = .Icon(image: presentation.pauseThumb, mode: .normal) } else { - progressView.theme = RadialProgressTheme(backgroundColor: theme.colors.blueFill, foregroundColor: .white, icon: theme.icons.chatMusicPlay, iconInset:NSEdgeInsets(left:1)) + progressView.theme = RadialProgressTheme(backgroundColor: presentation.activityBackground, foregroundColor: presentation.activityForeground, icon: presentation.playThumb, iconInset:NSEdgeInsets(left:1)) + progressView.state = .Play } } else { - progressView.theme = RadialProgressTheme(backgroundColor: theme.colors.blueFill, foregroundColor: .white, icon: theme.icons.chatMusicPlay, iconInset:NSEdgeInsets(left:1)) + progressView.theme = RadialProgressTheme(backgroundColor: presentation.activityBackground, foregroundColor: presentation.activityForeground, icon: presentation.playThumb, iconInset:NSEdgeInsets(left:1)) } } - override func update(with media: Media, size:NSSize, account:Account, parent:Message?, table:TableView?, parameters:ChatMediaLayoutParameters? = nil, animated: Bool = false) { - - let file:TelegramMediaFile = media as! TelegramMediaFile - let mediaUpdated = self.media == nil || !self.media!.isEqual(media) + override func update(with media: Media, size:NSSize, context: AccountContext, parent:Message?, table:TableView?, parameters:ChatMediaLayoutParameters? = nil, animated: Bool = false, positionFlags: LayoutPositionFlags? = nil, approximateSynchronousValue: Bool = false) { - super.update(with: media, size: size, account: account, parent:parent,table:table, parameters:parameters, animated: animated) + super.update(with: media, size: size, context: context, parent:parent,table:table, parameters:parameters, animated: animated, positionFlags: positionFlags) var updatedStatusSignal: Signal? - if mediaUpdated { - - globalAudio?.add(listener: self) - - if let parent = parent, parent.flags.contains(.Unsent) && !parent.flags.contains(.Failed) { - updatedStatusSignal = combineLatest(chatMessageFileStatus(account: account, file: file), account.pendingMessageManager.pendingMessageStatus(parent.id)) - |> map { resourceStatus, pendingStatus -> MediaResourceStatus in - if let pendingStatus = pendingStatus { - return .Fetching(isActive: true, progress: pendingStatus.progress) - } else { - return resourceStatus - } - } |> deliverOnMainQueue - } else { - updatedStatusSignal = chatMessageFileStatus(account: account, file: file) |> deliverOnMainQueue - } - - - - self.setNeedsDisplay() - } - + + if let parent = parent, parent.flags.contains(.Unsent) && !parent.flags.contains(.Failed) { + updatedStatusSignal = context.account.pendingMessageManager.pendingMessageStatus(parent.id) |> map { pendingStatus in + if let pendingStatus = pendingStatus.0 { + return .Fetching(isActive: true, progress: pendingStatus.progress) + } else { + return .Local + } + } |> deliverOnMainQueue + } + if let updatedStatusSignal = updatedStatusSignal { self.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak self] status in - if let strongSelf = self { - strongSelf.fetchStatus = status - - switch status { - case let .Fetching(_, progress): - strongSelf.progressView.state = .Fetching(progress: progress, force: false) - case .Remote: - strongSelf.progressView.state = .Remote - case .Local: - strongSelf.progressView.state = .Play - } - } + self?.fetchStatus = status })) - checkState() } + + + globalAudio?.add(listener: self) + self.setNeedsDisplay() + + self.fetchStatus = .Local + progressView.state = .Play + checkState(animated: animated) + } var leftInset:CGFloat { @@ -184,7 +201,7 @@ class ChatAudioContentView: ChatMediaContentView, APDelegate { return view } - override var interactionContentView: NSView { + override func interactionContentView(for innerId: AnyHashable, animateIn: Bool ) -> NSView { return self.progressView } @@ -198,7 +215,7 @@ class ChatAudioContentView: ChatMediaContentView, APDelegate { } override func clean() { - fetchDisposable.dispose() + //fetchDisposable.dispose() statusDisposable.dispose() globalAudio?.remove(listener: self) } diff --git a/Telegram-Mac/ChatBackgroundView.swift b/Telegram-Mac/ChatBackgroundView.swift new file mode 100644 index 0000000000..de36a73e96 --- /dev/null +++ b/Telegram-Mac/ChatBackgroundView.swift @@ -0,0 +1,11 @@ +// +// BackgroundView.swift +// Telegram +// +// Created by Mikhail Filimonov on 12/01/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + diff --git a/Telegram-Mac/ChatBubbleAccessoryForward.swift b/Telegram-Mac/ChatBubbleAccessoryForward.swift new file mode 100644 index 0000000000..b3e9a9e853 --- /dev/null +++ b/Telegram-Mac/ChatBubbleAccessoryForward.swift @@ -0,0 +1,70 @@ +// +// ChatBubbleAccessoryForward.swift +// Telegram +// +// Created by keepcoder on 14/12/2017. +// Copyright © 2017 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +class ChatBubbleAccessoryForward: Control { + + private let textView: TextView = TextView() + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(textView) + textView.isSelectable = false + layer?.cornerRadius = .cornerRadius + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updateText(layout: TextViewLayout) { + textView.update(layout) + self.background = theme.colors.bubbleBackground_incoming + textView.backgroundColor = theme.colors.bubbleBackground_incoming + setFrameSize(textView.frame.width + 10, textView.frame.height + 10) + needsLayout = true + } + + override func layout() { + super.layout() + textView.center() + } +} + +class ChatBubbleViaAccessory : Control { + private let textView: TextView = TextView() + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(textView) + textView.isSelectable = false + layer?.cornerRadius = .cornerRadius + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updateText(layout: TextViewLayout) { + textView.update(layout) + self.background = theme.colors.bubbleBackground_incoming + textView.backgroundColor = theme.colors.bubbleBackground_incoming + setFrameSize(textView.frame.width + 10, textView.frame.height + 10) + needsLayout = true + } + + override func layout() { + super.layout() + textView.center() + } +} diff --git a/Telegram-Mac/ChatCallRowItem.swift b/Telegram-Mac/ChatCallRowItem.swift index efe3a72222..332942804d 100644 --- a/Telegram-Mac/ChatCallRowItem.swift +++ b/Telegram-Mac/ChatCallRowItem.swift @@ -8,9 +8,10 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac +import TelegramCore + +import SwiftSignalKit +import Postbox class ChatCallRowItem: ChatRowItem { @@ -19,36 +20,52 @@ class ChatCallRowItem: ChatRowItem { let outgoing:Bool let failed: Bool + let isVideo: Bool private let requestSessionId = MetaDisposable() override func viewClass() -> AnyClass { return ChatCallRowView.self } - override init(_ initialSize: NSSize, _ chatInteraction: ChatInteraction, _ account: Account, _ object: ChatHistoryEntry) { + private let callId: Int64? + + override init(_ initialSize: NSSize, _ chatInteraction: ChatInteraction, _ context: AccountContext, _ object: ChatHistoryEntry, _ downloadSettings: AutomaticMediaDownloadSettings, theme: TelegramPresentationTheme) { let message = object.message! let action = message.media[0] as! TelegramMediaAction + let isIncoming: Bool = message.isIncoming(context.account, object.renderType == .bubble) outgoing = !message.flags.contains(.Incoming) - headerLayout = TextViewLayout(.initialize(string: outgoing ? tr(.chatCallOutgoing) : tr(.chatCallIncoming), color: theme.colors.text, font: .medium(.text)), maximumNumberOfLines: 1) + + let video: Bool + switch action.action { + case let .phoneCall(callId, _, _, isVideo): + video = isVideo + self.callId = callId + default: + video = false + self.callId = nil + } + self.isVideo = video + + headerLayout = TextViewLayout(.initialize(string: outgoing ? (video ? L10n.chatVideoCallOutgoing : L10n.chatCallOutgoing) : (video ? L10n.chatVideoCallIncoming : L10n.chatCallIncoming), color: theme.chat.textColor(isIncoming, object.renderType == .bubble), font: .medium(.text)), maximumNumberOfLines: 1) switch action.action { - case let .phoneCall(_, reason, duration): + case let .phoneCall(_, reason, duration, _): let attr = NSMutableAttributedString() - + if let duration = duration, duration > 0 { - _ = attr.append(string: String.stringForShortCallDurationSeconds(for: duration), color: theme.colors.grayText, font: .normal(.text)) + _ = attr.append(string: String.stringForShortCallDurationSeconds(for: duration), color: theme.chat.grayText(isIncoming, object.renderType == .bubble), font: .normal(.text)) failed = false } else if let reason = reason { switch reason { case .busy: - _ = attr.append(string: outgoing ? "Cancelled" : "Missed", color: theme.colors.grayText, font: .normal(.text)) + _ = attr.append(string: outgoing ? tr(L10n.chatServiceCallCancelled) : tr(L10n.chatServiceCallMissed), color: theme.chat.grayText(isIncoming, object.renderType == .bubble), font: .normal(.text)) case .disconnect: - _ = attr.append(string: "Disconnected", color: theme.colors.grayText, font: .normal(.text)) + _ = attr.append(string: outgoing ? tr(L10n.chatServiceCallCancelled) : tr(L10n.chatServiceCallMissed), color: theme.chat.grayText(isIncoming, object.renderType == .bubble), font: .normal(.text)) case .hangup: - _ = attr.append(string: outgoing ? "Cancelled" : "Missed", color: theme.colors.grayText, font: .normal(.text)) + _ = attr.append(string: outgoing ? tr(L10n.chatServiceCallCancelled) : tr(L10n.chatServiceCallMissed), color: theme.chat.grayText(isIncoming, object.renderType == .bubble), font: .normal(.text)) case .missed: - _ = attr.append(string: outgoing ? "Cancelled" : "Missed", color: theme.colors.grayText, font: .normal(.text)) + _ = attr.append(string: outgoing ? tr(L10n.chatServiceCallCancelled) : tr(L10n.chatServiceCallMissed), color: theme.chat.grayText(isIncoming, object.renderType == .bubble), font: .normal(.text)) } failed = true } else { @@ -60,7 +77,7 @@ class ChatCallRowItem: ChatRowItem { failed = true } - super.init(initialSize, chatInteraction, account, object) + super.init(initialSize, chatInteraction, context, object, downloadSettings, theme: theme) } override func makeContentSize(_ width: CGFloat) -> NSSize { @@ -74,14 +91,70 @@ class ChatCallRowItem: ChatRowItem { func requestCall() { if let peerId = message?.id.peerId { - let account = self.account! + let context = self.context - requestSessionId.set((phoneCall(account, peerId: peerId) |> deliverOnMainQueue).start(next: { result in - applyUIPCallResult(account, result) + requestSessionId.set((phoneCall(account: context.account, sharedContext: context.sharedContext, peerId: peerId, isVideo: isVideo) |> deliverOnMainQueue).start(next: { result in + applyUIPCallResult(context.sharedContext, result) })) } } + override func menuItems(in location: NSPoint) -> Signal<[ContextMenuItem], NoError> { + + let context = self.context + let callId = self.callId ?? 0 + let message = self.message! + return super.menuItems(in: location) |> map { items in + var items = items + + + + var callId: CallId? + var isVideo: Bool = false + var logPath: String? + for media in message.media { + if let action = media as? TelegramMediaAction, case let .phoneCall(id, discardReason, _, isVideoValue) = action.action { + isVideo = isVideoValue + if discardReason != .busy && discardReason != .missed { + if let logName = callLogNameForId(id: id, account: context.account) { + let logsPath = callLogsPath(account: context.account) + logPath = logsPath + "/" + logName + let start = logName.index(logName.startIndex, offsetBy: "\(id)".count + 1) + let end: String.Index + if logName.hasSuffix(".log.json") { + end = logName.index(logName.endIndex, offsetBy: -4 - 5) + } else { + end = logName.index(logName.endIndex, offsetBy: -4) + } + let accessHash = logName[start.. 0 { + items.append(.init(L10n.shareCallLogs, handler: { + NSWorkspace.shared.activateFileViewerSelecting([URL(fileURLWithPath: foundLog)]) + })) + } + + } + + return items + } + } + deinit { requestSessionId.dispose() } @@ -98,6 +171,8 @@ private class ChatCallRowView : ChatRowView { required init(frame frameRect: NSRect) { super.init(frame: frameRect) + fallbackControl.animates = false + addSubview(fallbackControl) addSubview(imageView) addSubview(headerView) @@ -123,10 +198,10 @@ private class ChatCallRowView : ChatRowView { if let item = item as? ChatCallRowItem { - fallbackControl.set(image: theme.icons.chatFallbackCall, for: .Normal) - fallbackControl.sizeToFit() + fallbackControl.set(image: theme.chat.chatCallFallbackIcon(item), for: .Normal) + _ = fallbackControl.sizeToFit() - imageView.image = item.outgoing ? (item.failed ? theme.icons.chatOutgoingFailedCall : theme.icons.chatOutgoingCall) : (item.failed ? theme.icons.chatIncomingFailedCall : theme.icons.chatIncomingCall) + imageView.image = theme.chat.chatCallIcon(item) imageView.sizeToFit() headerView.update(item.headerLayout, origin: NSMakePoint(fallbackControl.frame.maxX + 10, 0)) timeView.update(item.timeLayout, origin: NSMakePoint(fallbackControl.frame.maxX + 14 + imageView.frame.width, item.headerLayout.layoutSize.height + 3)) diff --git a/Telegram-Mac/ChatCommentsHeaderItem.swift b/Telegram-Mac/ChatCommentsHeaderItem.swift new file mode 100644 index 0000000000..626272d098 --- /dev/null +++ b/Telegram-Mac/ChatCommentsHeaderItem.swift @@ -0,0 +1,127 @@ +// +// ChatCommentsHeaderItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 15/09/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox + + + +class ChatCommentsHeaderItem : TableStickItem { + + private let entry:ChatHistoryEntry + fileprivate let chatInteraction:ChatInteraction? + let isBubbled: Bool + let layout:TextViewLayout + init(_ initialSize:NSSize, _ entry:ChatHistoryEntry, interaction: ChatInteraction, theme: TelegramPresentationTheme) { + self.entry = entry + self.isBubbled = entry.renderType == .bubble + self.chatInteraction = interaction + + + let text: String + switch entry { + case let .commentsHeader(empty, _, _): + if empty { + text = L10n.chatCommentsHeaderEmpty + } else { + text = L10n.chatCommentsHeaderFull + } + default: + text = "" + } + + self.layout = TextViewLayout(.initialize(string: text, color: theme.chatServiceItemTextColor, font: .medium(theme.fontSize)), maximumNumberOfLines: 1, truncationType: .end, alignment: .center) + + + super.init(initialSize) + } + + override var canBeAnchor: Bool { + return false + } + + required init(_ initialSize: NSSize) { + entry = .commentsHeader(true, MessageIndex.absoluteLowerBound(), .list) + self.isBubbled = false + self.layout = TextViewLayout(NSAttributedString()) + self.chatInteraction = nil + super.init(initialSize) + } + + override func makeSize(_ width: CGFloat, oldWidth:CGFloat) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) + layout.measure(width: width - 40) + return success + } + + override var stableId: AnyHashable { + return entry.stableId + } + + override var height: CGFloat { + return 30 + } + + override func viewClass() -> AnyClass { + return ChatCommentsHeaderView.self + } + + +} + +class ChatCommentsHeaderView : TableRowView { + private let textView:TextView + private let containerView: Control = Control() + private var borderView: View = View() + required init(frame frameRect: NSRect) { + self.textView = TextView() + self.textView.isSelectable = false + self.containerView.wantsLayer = true + self.textView.disableBackgroundDrawing = true + super.init(frame: frameRect) + addSubview(textView) + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var backdorColor: NSColor { + return .clear + } + + + override func updateColors() { + super.updateColors() + textView.backgroundColor = theme.chatServiceItemColor + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + super.draw(layer, in: ctx) + } + + override func layout() { + super.layout() + textView.center() + } + + override func set(item: TableRowItem, animated: Bool) { + if let item = item as? ChatCommentsHeaderItem { + textView.update(item.layout) + textView.setFrameSize(item.layout.layoutSize.width + 16, item.layout.layoutSize.height + 6) + textView.layer?.cornerRadius = textView.frame.height / 2 + self.needsLayout = true + } + super.set(item: item, animated:animated) + } +} diff --git a/Telegram-Mac/ChatContactRowItem.swift b/Telegram-Mac/ChatContactRowItem.swift index 1fce1c82a9..8bd26378bd 100644 --- a/Telegram-Mac/ChatContactRowItem.swift +++ b/Telegram-Mac/ChatContactRowItem.swift @@ -8,43 +8,90 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit +import Contacts + class ChatContactRowItem: ChatRowItem { let contactPeer:Peer? - let text:TextViewLayout - override init(_ initialSize: NSSize, _ chatInteraction: ChatInteraction, _ account: Account, _ object: ChatHistoryEntry) { + let phoneLayout:TextViewLayout + let nameLayout: TextViewLayout + let vCard: CNContact? + let contact: TelegramMediaContact + let appearance: WPLayoutPresentation + override init(_ initialSize: NSSize, _ chatInteraction: ChatInteraction, _ context: AccountContext, _ object: ChatHistoryEntry, _ downloadSettings: AutomaticMediaDownloadSettings, theme: TelegramPresentationTheme) { if let message = object.message, let contact = message.media[0] as? TelegramMediaContact { let attr = NSMutableAttributedString() + + let isIncoming: Bool = message.isIncoming(context.account, object.renderType == .bubble) + + + self.appearance = WPLayoutPresentation(text: theme.chat.textColor(isIncoming, object.renderType == .bubble), activity: theme.chat.webPreviewActivity(isIncoming, object.renderType == .bubble), link: theme.chat.linkColor(isIncoming, object.renderType == .bubble), selectText: theme.chat.selectText(isIncoming, object.renderType == .bubble), ivIcon: theme.chat.instantPageIcon(isIncoming, object.renderType == .bubble, presentation: theme), renderType: object.renderType) + + + if let _ = contact.vCardData?.data(using: .utf8) { + //let contacts = try? CNContactVCardSerialization.contacts(with: vCard) + self.vCard = nil + } else { + self.vCard = nil + } + self.contact = contact + + let name = isNotEmptyStrings([contact.firstName + (!contact.firstName.isEmpty ? " " : "") + contact.lastName, vCard?.givenName, vCard?.organizationName]) + + if let peerId = contact.peerId { self.contactPeer = message.peers[peerId] - let range = attr.append(string: contact.firstName + " " + contact.lastName, color: theme.colors.link, font: .medium(.text)) - attr.add(link: inAppLink.peerInfo(peerId:peerId,action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range) - _ = attr.append(string: "\n") - _ = attr.append(string: formatPhoneNumber(contact.phoneNumber), color: theme.colors.text, font: .normal(.text)) + let range = attr.append(string: name, font: .medium(.text)) + attr.add(link: inAppLink.peerInfo(link: "", peerId:peerId,action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range, color: theme.chat.linkColor(isIncoming, object.renderType == .bubble)) + phoneLayout = TextViewLayout(.initialize(string: formatPhoneNumber(contact.phoneNumber), color: theme.chat.textColor(isIncoming, object.renderType == .bubble), font: .normal(.text)), maximumNumberOfLines: 1, truncationType: .end, alignment: .left) + } else { - self.contactPeer = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: 0), accessHash: nil, firstName: contact.firstName, lastName: contact.lastName, username: nil, phone: contact.phoneNumber, photo: [], botInfo: nil, flags: []) - _ = attr.append(string: contact.firstName + " " + contact.lastName, color: theme.colors.text, font: .medium(.text)) - _ = attr.append(string: "\n") - _ = attr.append(string: formatPhoneNumber(contact.phoneNumber), color: theme.colors.text, font: .normal(.text)) + self.contactPeer = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(0)), accessHash: nil, firstName: name.components(separatedBy: " ").first ?? name, lastName: name.components(separatedBy: " ").count == 2 ? name.components(separatedBy: " ").last : "", username: nil, phone: contact.phoneNumber, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + _ = attr.append(string: name, color: theme.chat.textColor(isIncoming, object.renderType == .bubble), font: .medium(.text)) + + phoneLayout = TextViewLayout(.initialize(string: formatPhoneNumber(contact.phoneNumber), color: theme.chat.textColor(isIncoming, object.renderType == .bubble), font: .normal(.text)), maximumNumberOfLines: 1, truncationType: .end, alignment: .left) } - text = TextViewLayout(attr, maximumNumberOfLines: 3, truncationType: .end, alignment: .left) - text.interactions = globalLinkExecutor - + nameLayout = TextViewLayout(attr, maximumNumberOfLines: 1) + nameLayout.interactions = globalLinkExecutor + } else { fatalError("contact not found for item") } - super.init(initialSize, chatInteraction, account, object) + super.init(initialSize, chatInteraction, context, object, downloadSettings, theme: theme) + } + + override var additionalLineForDateInBubbleState: CGFloat? { + if vCard != nil { + return rightSize.height + } + if let line = phoneLayout.lines.last, (line.frame.width + 50) > realContentSize.width - (rightSize.width + insetBetweenContentAndDate) { + return rightSize.height + } + return nil + } + + override var isFixedRightPosition: Bool { + if vCard != nil { + return super.isForceRightLine + } + + if let line = phoneLayout.lines.last, (line.frame.width + 50) < contentSize.width - (rightSize.width + insetBetweenContentAndDate) { + return true + } + return super.isForceRightLine } override func makeContentSize(_ width: CGFloat) -> NSSize { - text.measure(width: width - 60) - return NSMakeSize(text.layoutSize.width + 60, 50) + nameLayout.measure(width: width - 50) + phoneLayout.measure(width: width - 50) + return NSMakeSize(max(nameLayout.layoutSize.width, phoneLayout.layoutSize.width) + 50, 40 + (vCard != nil ? 36 : 0)) } override func viewClass() -> AnyClass { @@ -56,19 +103,28 @@ class ChatContactRowItem: ChatRowItem { class ChatContactRowView : ChatRowView { - private let photoView:AvatarControl = AvatarControl(font: .avatar(.title)) - private let textView:TextView = TextView() + private let contactPhotoView:AvatarControl = AvatarControl(font: .avatar(.title)) + private let nameView: TextView = TextView() + private let phoneView: TextView = TextView() + private var actionButton: TitleButton? + required init(frame frameRect: NSRect) { super.init(frame: frameRect) - addSubview(photoView) - photoView.setFrameSize(50,50) - textView.isSelectable = false - addSubview(textView) + addSubview(contactPhotoView) + contactPhotoView.setFrameSize(40,40) + nameView.isSelectable = false + addSubview(nameView) + + phoneView.isSelectable = false + addSubview(phoneView) + + } override func updateColors() { super.updateColors() - textView.backgroundColor = backdorColor + nameView.backgroundColor = contentColor + phoneView.backgroundColor = contentColor } required init?(coder: NSCoder) { @@ -77,27 +133,57 @@ class ChatContactRowView : ChatRowView { override func layout() { super.layout() - if let item = self.item as? ChatContactRowItem { - textView.update(item.text) - textView.centerY(x:60) - } + nameView.setFrameOrigin(50, contactPhotoView.frame.minY + 3) + phoneView.setFrameOrigin(50, nameView.frame.maxY + 1) + + actionButton?.setFrameOrigin(0, contactPhotoView.frame.maxY + 6) + } override func set(item: TableRowItem, animated: Bool) { super.set(item: item, animated: animated) if let item = item as? ChatContactRowItem { - photoView.setPeer(account: item.account, peer: item.contactPeer) - photoView.removeAllHandlers() + contactPhotoView.setPeer(account: item.context.account, peer: item.contactPeer) + contactPhotoView.removeAllHandlers() if let peerId = item.contactPeer?.id { - photoView.set(handler: { control in - item.chatInteraction.openInfo(peerId, false , nil, nil) + contactPhotoView.set(handler: { [weak item] control in + item?.chatInteraction.openInfo(peerId, false , nil, nil) }, for: .Click) } + + + nameView.update(item.nameLayout) + phoneView.update(item.phoneLayout) + + if let _ = item.vCard { + if actionButton == nil { + actionButton = TitleButton() + actionButton?.layer?.cornerRadius = .cornerRadius + actionButton?.layer?.borderWidth = 1 + actionButton?.disableActions() + actionButton?.set(font: .normal(.text), for: .Normal) + addSubview(actionButton!) + } + actionButton?.removeAllHandlers() +// actionButton?.set(handler: { [weak item] _ in +// guard let item = item, let vCard = item.vCard else {return} +// let controller = VCardModalController(item.account, vCard: vCard, contact: item.contact) +// showModal(with: controller, for: mainWindow) +// }, for: .Click) + actionButton?.set(text: L10n.chatViewContact, for: .Normal) + actionButton?.layer?.borderColor = item.appearance.activity.cgColor + actionButton?.set(color: item.appearance.activity, for: .Normal) + _ = actionButton?.sizeToFit(NSZeroSize, NSMakeSize(item.contentSize.width, 30), thatFit: true) + + } else { + actionButton?.removeFromSuperview() + actionButton = nil + } + } - } } diff --git a/Telegram-Mac/ChatController.swift b/Telegram-Mac/ChatController.swift index af90378662..fdc0491102 100644 --- a/Telegram-Mac/ChatController.swift +++ b/Telegram-Mac/ChatController.swift @@ -8,26 +8,152 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore +import Postbox +import SwiftSignalKit +private var nextClientId: Int32 = 1 -final class ChatHistoryView { - let originalView: MessageHistoryView - let filteredEntries: [AppearanceWrapperEntry] +enum ReplyThreadMode : Equatable { + case replies(origin: MessageId) + case comments(origin: MessageId) + + var originId: MessageId { + switch self { + case let .replies(id), let .comments(id): + return id + } + } +} +enum ChatMode : Equatable { + case history + case scheduled + case pinned + case preview + case replyThread(data: ChatReplyThreadMessage, mode: ReplyThreadMode) + var threadId: MessageId? { + switch self { + case let .replyThread(data, _): + return data.messageId + default: + return nil + } + } + + var threadId64: Int64? { + if let threadId = threadId { + return makeMessageThreadId(threadId) + } else { + return nil + } + } + + var activityCategory: PeerActivitySpace.Category { + let activityCategory: PeerActivitySpace.Category + if let threadId = threadId64 { + activityCategory = .thread(threadId) + } else { + activityCategory = .global + } + return activityCategory + } + + var tagMask: MessageTags? { + switch self { + case .pinned: + return .pinned + default: + return nil + } + } + + var isThreadMode: Bool { + switch self { + case .replyThread: + return true + default: + return false + } + } + + var originId: MessageId? { + switch self { + case let .replyThread(_, mode): + return mode.originId + default: + return nil + } + } +} + +extension ChatHistoryLocation { + var isAtUpperBound: Bool { + switch self { + case .Navigation(index: .upperBound, anchorIndex: .upperBound, count: _, side: _): + return true + case .Scroll(index: .upperBound, anchorIndex: .upperBound, sourceIndex: _, scrollPosition: _, count: _, animated: _): + return true + default: + return false + } + } + +} + + +private var temporaryTouchBar: Any? + + +final class ChatWrapperEntry : Comparable, Identifiable { + let appearance: AppearanceWrapperEntry + let automaticDownload: AutomaticMediaDownloadSettings + init(appearance: AppearanceWrapperEntry, automaticDownload: AutomaticMediaDownloadSettings) { + self.appearance = appearance + self.automaticDownload = automaticDownload + } + var stableId: AnyHashable { + return appearance.entry.stableId + } - init(originalView:MessageHistoryView, filteredEntries: [AppearanceWrapperEntry]) { + deinit { + var bp:Int = 0 + bp += 1 + } + + var entry: ChatHistoryEntry { + return appearance.entry + } +} + +func ==(lhs:ChatWrapperEntry, rhs: ChatWrapperEntry) -> Bool { + return lhs.appearance == rhs.appearance && lhs.automaticDownload == rhs.automaticDownload +} +func <(lhs:ChatWrapperEntry, rhs: ChatWrapperEntry) -> Bool { + return lhs.appearance.entry < rhs.appearance.entry +} + + +final class ChatHistoryView { + let originalView: MessageHistoryView? + let filteredEntries: [ChatWrapperEntry] + let theme: TelegramPresentationTheme + init(originalView:MessageHistoryView?, filteredEntries: [ChatWrapperEntry], theme: TelegramPresentationTheme) { self.originalView = originalView self.filteredEntries = filteredEntries + self.theme = theme + } + + deinit { + } } enum ChatControllerViewState { case visible case progress + //case IsNotAccessible } final class ChatHistoryState : Equatable { @@ -73,32 +199,104 @@ func ==(lhs:ChatHistoryState, rhs:ChatHistoryState) -> Bool { class ChatControllerView : View, ChatInputDelegate { + let tableView:TableView + + + var scroll: ScrollPosition { + return self.tableView.scrollPosition().current + } + + private var backgroundView: BackgroundView? + private weak var navigationView: NSView? + let inputView:ChatInputView let inputContextHelper:InputContextHelper private(set) var state:ChatControllerViewState = .visible private var searchInteractions:ChatSearchInteractions! private let scroller:ChatNavigateScroller private var mentions:ChatNavigationMention? - private var progressView:View? + private var failed:ChatNavigateFailed? + private var progressView:ProgressIndicator? private let header:ChatHeaderController private var historyState:ChatHistoryState? private let chatInteraction: ChatInteraction + + private var themeSelectorView: NSView? + + private let floatingPhotosView: View = View() + + private let gradientMaskView = BackgroundGradientView(frame: NSZeroRect) + var headerState: ChatHeaderState { return header.state } - required init(frame frameRect: NSRect, chatInteraction:ChatInteraction, account:Account) { + deinit { + var bp:Int = 0 + bp += 1 + } + + + func updateBackground(_ mode: TableBackgroundMode, navigationView: NSView?) { + if mode != theme.controllerBackgroundMode { + if backgroundView == nil, let navigationView = navigationView { + let point = NSMakePoint(0, -frame.minY) + backgroundView = BackgroundView(frame: CGRect.init(origin: point, size: navigationView.bounds.size)) + backgroundView?.useSharedAnimationPhase = false + addSubview(backgroundView!, positioned: .below, relativeTo: self.subviews.first) + } + backgroundView?.backgroundMode = mode + self.navigationView = navigationView + } else { + backgroundView?.removeFromSuperview() + backgroundView = nil + } + } + + func doBackgroundAction() -> Bool { + backgroundView?.doAction() + return backgroundView != nil + } + + + + func findItem(by messageId: MessageId) -> TableRowItem? { + var found: TableRowItem? = nil + self.tableView.enumerateVisibleItems(with: { item in + if let item = item as? ChatRowItem, item.message?.id == messageId { + found = item + return false + } else { + return true + } + }) + return found + } + + required init(frame frameRect: NSRect, chatInteraction:ChatInteraction) { self.chatInteraction = chatInteraction header = ChatHeaderController(chatInteraction) - scroller = ChatNavigateScroller(account, chatInteraction.peerId) - inputContextHelper = InputContextHelper(account: account, chatInteraction: chatInteraction) + + + scroller = ChatNavigateScroller(chatInteraction.context, contextHolder: chatInteraction.contextHolder(), chatLocation: chatInteraction.chatLocation, mode: chatInteraction.mode) + inputContextHelper = InputContextHelper(chatInteraction: chatInteraction) tableView = TableView(frame:NSMakeRect(0,0,frameRect.width,frameRect.height - 50), isFlipped:false) inputView = ChatInputView(frame: NSMakeRect(0,tableView.frame.maxY, frameRect.width,50), chatInteraction: chatInteraction) - inputView.autoresizingMask = [.width] + //inputView.autoresizingMask = [.width] super.init(frame: frameRect) + +// self.layer = CAGradientLayer() +// self.layer?.disableActions() + addSubview(tableView) + addSubview(floatingPhotosView) + + floatingPhotosView.flip = false + floatingPhotosView.isEventLess = true +// floatingPhotosView.backgroundColor = .random + addSubview(inputView) inputView.delegate = self self.autoresizesSubviews = false @@ -107,40 +305,137 @@ class ChatControllerView : View, ChatInputDelegate { chatInteraction.scrollToLatest(false) }, for: .Click) scroller.forceHide() - tableView.addSubview(scroller) + addSubview(scroller) + + let context = chatInteraction.context + - searchInteractions = ChatSearchInteractions( jump: { message in - chatInteraction.focusMessageId(nil, message.id, .center(id: 0, animated: false, focus: true, inset: 0)) + searchInteractions = ChatSearchInteractions(jump: { message in + chatInteraction.focusMessageId(nil, message.id, .center(id: 0, innerId: nil, animated: false, focus: .init(focus: true), inset: 0)) }, results: { query in chatInteraction.modalSearch(query) }, calendarAction: { date in chatInteraction.jumpToDate(date) }, cancel: { - chatInteraction.update({$0.updatedSearchMode(false)}) - }, searchRequest: { query, fromId -> Signal<[Message],Void> in - return searchMessages(account: account, peerId: chatInteraction.peerId, query: query, fromId: fromId) + chatInteraction.update({$0.updatedSearchMode((false, nil, nil))}) + }, searchRequest: { [weak chatInteraction] query, fromId, state in + guard let chatInteraction = chatInteraction else { + return .never() + } + let location: SearchMessagesLocation + switch chatInteraction.chatLocation { + case let .peer(peerId): + switch chatInteraction.mode { + case .pinned: + location = .peer(peerId: peerId, fromId: fromId, tags: .pinned, topMsgId: chatInteraction.mode.threadId, minDate: nil, maxDate: nil) + default: + location = .peer(peerId: peerId, fromId: fromId, tags: nil, topMsgId: chatInteraction.mode.threadId, minDate: nil, maxDate: nil) + } + case let .replyThread(data): + location = .peer(peerId: data.messageId.peerId, fromId: fromId, tags: nil, topMsgId: data.messageId, minDate: nil, maxDate: nil) + } + return context.engine.messages.searchMessages(location: location, query: query, state: state) |> map {($0.0.messages.filter({ !($0.media.first is TelegramMediaAction) }), $0.1)} }) - tableView.addScroll(listener: TableScrollListener { [weak self] position in + tableView.addScroll(listener: TableScrollListener(dispatchWhenVisibleRangeUpdated: false, { [weak self] position in if let state = self?.historyState { self?.updateScroller(state) } - }) - updateLocalizationAndTheme() + })) + + tableView.backgroundColor = .clear + tableView.layer?.backgroundColor = .clear + + // updateLocalizationAndTheme(theme: theme) tableView.set(stickClass: ChatDateStickItem.self, handler: { stick in - var bp:Int = 0 - bp += 1 + }) + + tableView.addScroll(listener: TableScrollListener(dispatchWhenVisibleRangeUpdated: false, { [weak self] position in + guard let `self` = self else { + return + } + self.tableView.enumerateVisibleViews(with: { view in + if let view = view as? ChatRowView { + view.updateBackground(animated: false, item: view.item) + } + }) + })) + + tableView.onCAScroll = { [weak self] from, to in + guard let strongSelf = self else { + return + } + for view in strongSelf.floatingPhotosView.subviews { + view.layer?.animatePosition(from: NSMakePoint(view.frame.minX, view.frame.minY - (from.minY - to.minY)), to: view.frame.origin, duration: 0.4, timingFunction: .spring) + } + } + } + + func updateFloating(_ values:[ChatFloatingPhoto], animated: Bool, currentAnimationRows: [TableAnimationInterface.AnimateItem] = []) { + CATransaction.begin() + var added:[NSView] = [] + for value in values { + if let view = value.photoView { + view._change(pos: value.point, animated: animated && view.superview == floatingPhotosView, duration: 0.2, timingFunction: .easeOut) + if view.superview != floatingPhotosView { + floatingPhotosView.addSubview(view) + if animated { + view.layer?.animateAlpha(from: 0, to: 1, duration: 0.2, timingFunction: .easeOut) + let moveAsNew = currentAnimationRows.first(where: { + $0.index == value.items.first?.index + }) + if let moveAsNew = moveAsNew { + + view.layer?.animatePosition(from: value.point - (moveAsNew.to - moveAsNew.from), to: value.point, duration: 0.2, timingFunction: .easeOut) + } + } + } + added.append(view) + } + } + let toRemove = floatingPhotosView.subviews.filter { + !added.contains($0) + } + for view in toRemove { + performSubviewRemoval(view, animated: animated, timingFunction: .easeOut) + } + CATransaction.commit() + } + + + func showChatThemeSelector(_ view: NSView, animated: Bool) { + self.themeSelectorView?.removeFromSuperview() + self.themeSelectorView = view + addSubview(view) + updateFrame(self.frame, transition: animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate) + } + + func hideChatThemeSelector(animated: Bool) { + if let view = self.themeSelectorView { + self.themeSelectorView = nil + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate + self.updateFrame(self.frame, transition: transition) + if animated { + transition.updateFrame(view: view, frame: CGRect(origin: CGPoint(x: 0, y: frame.maxY), size: view.frame.size), completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } else { + view.removeFromSuperview() + } + } } func updateScroller(_ historyState:ChatHistoryState) { self.historyState = historyState - let isHidden = tableView.documentOffset.y < 150 && historyState.isDownOfHistory + let isHidden = (tableView.documentOffset.y < 80 && historyState.isDownOfHistory) || tableView.isEmpty + if !isHidden { scroller.isHidden = false } + scroller.change(opacity: isHidden ? 0 : 1, animated: true) { [weak scroller] completed in if completed { scroller?.isHidden = isHidden @@ -150,29 +445,58 @@ class ChatControllerView : View, ChatInputDelegate { if let mentions = mentions { mentions.change(pos: NSMakePoint(frame.width - mentions.frame.width - 6, tableView.frame.maxY - mentions.frame.height - 6 - (scroller.controlIsHidden ? 0 : scroller.frame.height)), animated: true ) } + if let failed = failed { + var offset = (scroller.controlIsHidden ? 0 : scroller.frame.height) + if let mentions = mentions { + offset += (mentions.frame.height + 6) + } + failed.change(pos: NSMakePoint(frame.width - failed.frame.width - 6, tableView.frame.maxY - failed.frame.height - 6 - offset), animated: true ) + } } - + private var previousHeight:CGFloat = 50 func inputChanged(height: CGFloat, animated: Bool) { if previousHeight != height { - previousHeight = height - let header:CGFloat - if let currentView = self.header.currentView { - header = currentView.frame.height - } else { - header = 0 - } + let header:CGFloat = self.header.state.toleranceHeight + let size = NSMakeSize(frame.width, frame.height - height - header) + let resizeAnimated = animated && tableView.contentOffset.y < height + //(previousHeight < height || tableView.contentOffset.y < height) + tableView.change(size: size, animated: animated) - inputView.change(pos: NSMakePoint(frame.minX, tableView.frame.maxY), animated: animated) + + floatingPhotosView.change(size: size, animated: animated) + + if tableView.contentOffset.y > height { + // tableView.clipView.scroll(to: NSMakePoint(0, tableView.contentOffset.y - (previousHeight - height))) + } + + inputView.change(pos: NSMakePoint(0, tableView.frame.maxY), animated: animated) if let view = inputContextHelper.accessoryView { - view._change(pos: NSMakePoint(0, size.height - view.frame.height), animated: animated) + view._change(pos: NSMakePoint(0, frame.height - inputView.frame.height - view.frame.height), animated: animated) } + + scroller.change(pos: NSMakePoint(frame.width - scroller.frame.width - 6, frame.height - height - scroller.frame.height - 6), animated: animated) + if let mentions = mentions { mentions.change(pos: NSMakePoint(frame.width - mentions.frame.width - 6, tableView.frame.maxY - mentions.frame.height - 6 - (scroller.controlIsHidden ? 0 : scroller.frame.height)), animated: animated ) } - scroller.change(pos: NSMakePoint(frame.width - scroller.frame.width - 6, size.height - scroller.frame.height - 6), animated: animated) + if let failed = failed { + var offset = (scroller.controlIsHidden ? 0 : scroller.frame.height) + if let mentions = mentions { + offset += (mentions.frame.height + 6) + } + failed.change(pos: NSMakePoint(frame.width - failed.frame.width - 6, tableView.frame.maxY - failed.frame.height - 6 - offset), animated: animated) + } + + previousHeight = height + + self.tableView.enumerateVisibleViews(with: { view in + if let view = view as? ChatRowView { + view.updateBackground(animated: animated, item: view.item) + } + }) } } @@ -186,123 +510,225 @@ class ChatControllerView : View, ChatInputDelegate { } override func setFrameSize(_ newSize: NSSize) { + super.setFrameSize(newSize) - if let view = inputContextHelper.accessoryView { - view.setFrameSize(NSMakeSize(newSize.width, view.frame.height)) - } - - if let currentView = header.currentView { - currentView.setFrameSize(newSize.width, currentView.frame.height) - tableView.setFrameSize(newSize.width, newSize.height - inputView.frame.height - currentView.frame.height) - } else { - tableView.setFrameSize(newSize.width, newSize.height - inputView.frame.height) - } - inputView.setFrameSize(newSize.width, inputView.frame.height) - - super.setFrameSize(newSize) - + self.tableView.enumerateVisibleViews(with: { view in + if let view = view as? ChatRowView { + view.updateBackground(animated: false, item: view.item) + } + }) } + override func layout() { super.layout() + updateFrame(frame, transition: .immediate) + } + + func updateFrame(_ frame: NSRect, transition: ContainedViewLayoutTransition) { + + if let view = inputContextHelper.accessoryView { + transition.updateFrame(view: view, frame: NSMakeRect(0, frame.height - inputView.frame.height - view.frame.height, frame.width, view.frame.height)) + } if let currentView = header.currentView { - tableView.setFrameOrigin(0, currentView.frame.height) - currentView.needsDisplay = true - - } else { - tableView.setFrameOrigin(0, 0) + transition.updateFrame(view: currentView, frame: NSMakeRect(0, 0, frame.width, currentView.frame.height)) } - if let view = inputContextHelper.accessoryView { - view.setFrameOrigin(0, frame.height - inputView.frame.height - view.frame.height) + var tableHeight = frame.height - inputView.frame.height - header.state.toleranceHeight + + if let themeSelector = themeSelectorView { + tableHeight -= themeSelector.frame.height + tableHeight += inputView.frame.height } - inputView.setFrameOrigin(NSMakePoint(0, tableView.frame.maxY)) - if let indicator = progressView?.subviews.first { - indicator.center() + + transition.updateFrame(view: tableView, frame: NSMakeRect(0, header.state.toleranceHeight, frame.width, tableHeight)) + + let inputY: CGFloat = themeSelectorView != nil ? frame.height : tableView.frame.maxY + + transition.updateFrame(view: inputView, frame: NSMakeRect(0, inputY, frame.width, inputView.frame.height)) + + + transition.updateFrame(view: gradientMaskView, frame: tableView.frame) + + if let progressView = progressView?.subviews.first { + transition.updateFrame(view: progressView, frame: progressView.centerFrame()) } + if let progressView = progressView { + transition.updateFrame(view: progressView, frame: progressView.centerFrame()) + } + + + transition.updateFrame(view: scroller, frame: NSMakeRect(frame.width - scroller.frame.width - 6, frame.height - inputView.frame.height - 6 - scroller.frame.height, scroller.frame.width, scroller.frame.height)) - scroller.setFrameOrigin(frame.width - scroller.frame.width - 6, tableView.frame.height - 6 - scroller.frame.height) if let mentions = mentions { - mentions.change(pos: NSMakePoint(frame.width - mentions.frame.width - 6, tableView.frame.maxY - mentions.frame.height - 6 - (scroller.controlIsHidden ? 0 : scroller.frame.height)), animated: false ) + transition.updateFrame(view: mentions, frame: NSMakeRect(frame.width - mentions.frame.width - 6, frame.height - inputView.frame.height - mentions.frame.height - 6 - (scroller.controlIsHidden ? 0 : scroller.frame.height), mentions.frame.width, mentions.frame.height)) + } + if let failed = failed { + var offset = (scroller.controlIsHidden ? 0 : scroller.frame.height) + if let mentions = mentions { + offset += (mentions.frame.height + 6) + } + transition.updateFrame(view: failed, frame: NSMakeRect(frame.width - failed.frame.width - 6, frame.height - inputView.frame.height - failed.frame.height - 6 - offset, failed.frame.width, failed.frame.height)) + } + transition.updateFrame(view: floatingPhotosView, frame: tableView.frame) + + if let backgroundView = backgroundView, let navigationView = navigationView { + let size = NSMakeSize(navigationView.bounds.width, navigationView.bounds.height) + transition.updateFrame(view: backgroundView, frame: NSMakeRect(0, -frame.minY, size.width, size.height)) + } + + tableView.enumerateVisibleViews(with: { view in + if let view = view as? ChatRowView { + view.updateBackground(animated: transition.isAnimated, item: view.item) + } + }) + + if let themeSelectorView = self.themeSelectorView { + transition.updateFrame(view: themeSelectorView, frame: NSMakeRect(0, frame.height - themeSelectorView.frame.height, frame.width, themeSelectorView.frame.height)) } } - override var responder: NSResponder? { return inputView.responder } func change(state:ChatControllerViewState, animated:Bool) { + let state = chatInteraction.presentation.isNotAccessible ? .visible : state if state != self.state { self.state = state switch state { case .progress: if progressView == nil { - progressView = View(frame:tableView.bounds) - progressView?.autoresizingMask = [.width, .height] - let indicator = ProgressIndicator(frame: NSMakeRect(0,0,30,30)) - progressView?.addSubview(indicator) - indicator.animates = true - tableView.addSubview(progressView!) - indicator.center() - } - progressView?.backgroundColor = theme.colors.background - // (progressView?.subviews.first as? ProgressIndicator)?.color = theme.colors.indicatorColor - break + self.progressView = ProgressIndicator(frame: NSMakeRect(0,0,30,30)) + self.progressView?.innerInset = 6 + progressView!.animates = true + addSubview(progressView!) + progressView!.center() + } + progressView?.backgroundColor = theme.colors.background.withAlphaComponent(0.7) + progressView?.layer?.cornerRadius = 15 case .visible: if animated { progressView?.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak self] (completed) in self?.progressView?.removeFromSuperview() - (self?.progressView?.subviews.first as? ProgressIndicator)?.animates = false + self?.progressView?.animates = false self?.progressView = nil }) } else { progressView?.removeFromSuperview() - (progressView?.subviews.first as? ProgressIndicator)?.animates = false progressView = nil } - - break } } + if chatInteraction.presentation.isNotAccessible { + tableView.updateEmpties() + } } - func updateHeader(_ interfaceState:ChatPresentationInterfaceState, _ animated:Bool) { - - let state:ChatHeaderState - if interfaceState.isSearchMode { - state = .search(searchInteractions) - } else if interfaceState.reportStatus == .canReport { - state = .report - } else if let pinnedMessageId = interfaceState.pinnedMessageId, pinnedMessageId != interfaceState.interfaceState.dismissedPinnedMessageId { - state = .pinned(pinnedMessageId) + func updateHeader(_ interfaceState:ChatPresentationInterfaceState, _ animated:Bool, _ animateOnlyHeader: Bool = false) { + + + var voiceChat: ChatActiveGroupCallInfo? + if interfaceState.groupCall?.data?.groupCall == nil { + if let data = interfaceState.groupCall?.data, data.participantCount == 0 && interfaceState.groupCall?.activeCall.scheduleTimestamp == nil { + voiceChat = nil + } else { + voiceChat = interfaceState.groupCall + } + } else { + voiceChat = nil + } + + var state:ChatHeaderState + if interfaceState.reportMode != nil { + state = .none(nil) + } else if interfaceState.isSearchMode.0 { + state = .search(voiceChat, searchInteractions, interfaceState.isSearchMode.1, interfaceState.isSearchMode.2) + } else if let initialAction = interfaceState.initialAction, case let .ad(kind) = initialAction { + state = .promo(voiceChat, kind) + } else if let peerStatus = interfaceState.peerStatus, let settings = peerStatus.peerStatusSettings, !settings.flags.isEmpty { + if peerStatus.canAddContact && settings.contains(.canAddContact) { + state = .addContact(voiceChat, block: settings.contains(.canReport) || settings.contains(.canBlock), autoArchived: settings.contains(.autoArchived)) + } else if settings.contains(.canReport) { + state = .report(voiceChat, autoArchived: settings.contains(.autoArchived)) + } else if settings.contains(.canShareContact) { + state = .shareInfo(voiceChat) + } else if let pinnedMessageId = interfaceState.pinnedMessageId, !interfaceState.interfaceState.dismissedPinnedMessageId.contains(pinnedMessageId.messageId), !interfaceState.hidePinnedMessage, interfaceState.chatMode != .pinned { + if pinnedMessageId.message?.restrictedText(chatInteraction.context.contentSettings) == nil { + state = .pinned(voiceChat, pinnedMessageId, doNotChangeTable: interfaceState.chatMode.isThreadMode) + } else { + state = .none(voiceChat) + } + } else { + state = .none(voiceChat) + } + } else if let pinnedMessageId = interfaceState.pinnedMessageId, !interfaceState.interfaceState.dismissedPinnedMessageId.contains(pinnedMessageId.messageId), !interfaceState.hidePinnedMessage, interfaceState.chatMode != .pinned { + if pinnedMessageId.message?.restrictedText(chatInteraction.context.contentSettings) == nil { + state = .pinned(voiceChat, pinnedMessageId, doNotChangeTable: interfaceState.chatMode.isThreadMode) + } else { + state = .none(voiceChat) + } } else if let canAdd = interfaceState.canAddContact, canAdd { - state = .none + state = .none(voiceChat) } else { - state = .none + state = .none(voiceChat) } - CATransaction.begin() + header.updateState(state, animated: animated, for: self) - - tableView.change(size: NSMakeSize(frame.width, frame.height - state.height - inputView.frame.height), animated: animated) - tableView.change(pos: NSMakePoint(0, state.height), animated: animated) - - scroller.change(pos: NSMakePoint(frame.width - scroller.frame.width - 6, frame.height - state.height - inputView.frame.height - 6 - scroller.frame.height), animated: animated) + tableView.updateStickInset(state.height - state.toleranceHeight, animated: animated) - - if let mentions = mentions { - mentions.change(pos: NSMakePoint(frame.width - mentions.frame.width - 6, tableView.frame.maxY - mentions.frame.height - 6 - (scroller.controlIsHidden ? 0 : scroller.frame.height)), animated: animated ) - } - - if let view = inputContextHelper.accessoryView { - view._change(pos: NSMakePoint(0, frame.height - view.frame.height - inputView.frame.height), animated: animated) + updateFrame(frame, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate) + + } + + private(set) fileprivate var failedIds: Set = Set() + private var hasOnScreen: Bool = false + func updateFailedIds(_ ids: Set, hasOnScreen: Bool, animated: Bool) { + if hasOnScreen != self.hasOnScreen || self.failedIds != ids { + self.failedIds = ids + self.hasOnScreen = hasOnScreen + if !ids.isEmpty && !hasOnScreen { + if failed == nil { + failed = ChatNavigateFailed(chatInteraction.context) + if let failed = failed { + var offset = (scroller.controlIsHidden ? 0 : scroller.frame.height) + if let mentions = mentions { + offset += (mentions.frame.height + 6) + } + failed.setFrameOrigin(NSMakePoint(frame.width - failed.frame.width - 6, tableView.frame.maxY - failed.frame.height - 6 - offset)) + addSubview(failed) + } + if animated { + failed?.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } + failed?.removeAllHandlers() + failed?.set(handler: { [weak self] _ in + if let id = ids.min() { + self?.chatInteraction.focusMessageId(nil, id, .CenterEmpty) + } + }, for: .Click) + } else { + if animated { + if let failed = self.failed { + self.failed = nil + failed.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak failed] _ in + failed?.removeFromSuperview() + }) + } + } else { + failed?.removeFromSuperview() + failed = nil + } + + } + needsLayout = true } - CATransaction.commit() } func updateMentionsCount(_ count: Int32, animated: Bool) { @@ -312,8 +738,13 @@ class ChatControllerView : View, ChatInputDelegate { mentions?.set(handler: { [weak self] _ in self?.chatInteraction.mentionPressed() }, for: .Click) + + mentions?.set(handler: { [weak self] _ in + self?.chatInteraction.clearMentions() + }, for: .LongMouseDown) + if let mentions = mentions { - mentions.change(pos: NSMakePoint(frame.width - mentions.frame.width - 6, tableView.frame.maxY - mentions.frame.height - 6 - (scroller.controlIsHidden ? 0 : scroller.frame.height)), animated: animated ) + mentions.setFrameOrigin(NSMakePoint(frame.width - mentions.frame.width - 6, tableView.frame.maxY - mentions.frame.height - 6 - (scroller.controlIsHidden ? 0 : scroller.frame.height))) addSubview(mentions) } } @@ -325,12 +756,17 @@ class ChatControllerView : View, ChatInputDelegate { needsLayout = true } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - self.backgroundColor = theme.colors.background + func applySearchResponder() { + header.applySearchResponder() + } + + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) progressView?.backgroundColor = theme.colors.background - (progressView?.subviews.first as? ProgressIndicator)?.set(color: theme.colors.indicatorColor) - scroller.updateLocalizationAndTheme() + (progressView?.subviews.first as? NSProgressIndicator)?.set(color: theme.colors.indicatorColor) + scroller.updateLocalizationAndTheme(theme: theme) tableView.emptyItem = ChatEmptyPeerItem(tableView.frame.size, chatInteraction: chatInteraction) } @@ -340,52 +776,56 @@ class ChatControllerView : View, ChatInputDelegate { -fileprivate func prepareEntries(from fromView:ChatHistoryView?, to toView:ChatHistoryView, account:Account, initialSize:NSSize, interaction:ChatInteraction, animated:Bool, scrollPosition:ChatHistoryViewScrollPosition?, reason:ChatHistoryViewUpdateType, animationInterface:TableAnimationInterface?) -> Signal { +fileprivate func prepareEntries(from fromView:ChatHistoryView?, to toView:ChatHistoryView, timeDifference: TimeInterval, initialSize:NSSize, interaction:ChatInteraction, animated:Bool, scrollPosition:ChatHistoryViewScrollPosition?, reason:ChatHistoryViewUpdateType, animationInterface:TableAnimationInterface?, side: TableSavingSide?) -> Signal { return Signal { subscriber in - +// subscriber.putNext(TableUpdateTransition(deleted: [], inserted: [], updated: [], animated: animated, state: .none(nil), grouping: true)) +// subscriber.putCompletion() var scrollToItem:TableScrollState? = nil var animated = animated - + var offset:CGFloat = 0 if let scrollPosition = scrollPosition { switch scrollPosition { case let .unread(unreadIndex): var index = toView.filteredEntries.count - 1 for entry in toView.filteredEntries { - if case .UnreadEntry = entry.entry { - scrollToItem = .top(id: entry.stableId, animated: false, focus: false, inset: 0) + if case .UnreadEntry = entry.appearance.entry { + if interaction.mode.isThreadMode { + offset = 44 - 6 + } else { + offset - 6 + } + scrollToItem = .top(id: entry.stableId, innerId: nil, animated: false, focus: .init(focus: false), inset: offset) break } index -= 1 } if scrollToItem == nil { - var index = toView.filteredEntries.count - 1 - for entry in toView.filteredEntries { - if entry.entry.index >= unreadIndex { - scrollToItem = .top(id: entry.stableId, animated: false, focus: false, inset: 0) - break - } - index -= 1 - } + scrollToItem = .none(animationInterface) } if scrollToItem == nil { - var index = 0 - for entry in toView.filteredEntries.reversed() { - if entry.entry.index < unreadIndex { - scrollToItem = .top(id: entry.stableId, animated: false, focus: false, inset: 0) - break - } - index += 1 - } +// var index = 0 +// for entry in toView.filteredEntries.reversed() { +// if entry.appearance.entry.index < unreadIndex { +// scrollToItem = .top(id: entry.stableId, animated: false, focus: .init(focus: false), inset: 0) +// break +// } +// index += 1 +// } } case let .positionRestoration(scrollIndex, relativeOffset): + + let timestamp = Int32(min(TimeInterval(scrollIndex.timestamp) - timeDifference, TimeInterval(Int32.max))) + + + let scrollIndex = scrollIndex.withUpdatedTimestamp(timestamp) var index = toView.filteredEntries.count - 1 for entry in toView.filteredEntries { - if entry.entry.index >= scrollIndex { - scrollToItem = .top(id: entry.stableId, animated: false, focus: false, inset: relativeOffset) + if entry.appearance.entry.index >= scrollIndex { + scrollToItem = .top(id: entry.stableId, innerId: nil, animated: false, focus: .init(focus: false), inset: relativeOffset) break } index -= 1 @@ -394,28 +834,42 @@ fileprivate func prepareEntries(from fromView:ChatHistoryView?, to toView:ChatHi if scrollToItem == nil { var index = 0 for entry in toView.filteredEntries.reversed() { - if entry.entry.index < scrollIndex { - scrollToItem = .top(id: entry.stableId, animated: false, focus: false, inset: relativeOffset) + if entry.appearance.entry.index < scrollIndex { + scrollToItem = .top(id: entry.stableId, innerId: nil, animated: false, focus: .init(focus: false), inset: relativeOffset) break } index += 1 } } case let .index(scrollIndex, position, directionHint, animated): - var index = toView.filteredEntries.count - 1 + let scrollIndex = scrollIndex.withSubstractedTimestamp(Int32(timeDifference)) + for entry in toView.filteredEntries { - if entry.entry.index >= scrollIndex { - scrollToItem = position.swap(to: entry.entry.stableId) + if scrollIndex.isLessOrEqual(to: entry.appearance.entry.index) { + if case let .groupedPhotos(entries, _) = entry.appearance.entry { + for inner in entries { + if case let .MessageEntry(values) = inner { + let timestamp = Int32(min(TimeInterval(values.0.timestamp) - timeDifference, TimeInterval(Int32.max))) + + let messageIndex = MessageIndex(values.0.withUpdatedTimestamp(timestamp)) + + if !scrollIndex.isLess(than: messageIndex) && scrollIndex.isLessOrEqual(to: messageIndex) { + scrollToItem = position.swap(to: entry.appearance.entry.stableId, innerId: inner.stableId) + } + } + } + } else { + scrollToItem = position.swap(to: entry.appearance.entry.stableId) + } break } - index -= 1 } if scrollToItem == nil { var index = 0 for entry in toView.filteredEntries.reversed() { - if entry.entry.index < scrollIndex { - scrollToItem = position.swap(to: entry.entry.stableId) + if MessageHistoryAnchorIndex.message(entry.appearance.entry.index) < scrollIndex { + scrollToItem = position.swap(to: entry.appearance.entry.stableId) break } index += 1 @@ -425,7 +879,7 @@ fileprivate func prepareEntries(from fromView:ChatHistoryView?, to toView:ChatHi } if scrollToItem == nil { - scrollToItem = .saveVisible(.upper) + scrollToItem = .saveVisible(side ?? .upper) switch reason { case let .Generic(type): @@ -438,23 +892,14 @@ fileprivate func prepareEntries(from fromView:ChatHistoryView?, to toView:ChatHi default: break } - } + } - func makeItem(_ entry: ChatHistoryEntry) -> TableRowItem { - var item:TableRowItem; - switch entry { - case .HoleEntry: - item = ChatHoleRowItem(initialSize, interaction, account,entry) - case .UnreadEntry: - item = ChatUnreadRowItem(initialSize, interaction, account,entry) - case .MessageEntry: - item = ChatRowItem.item(initialSize, from:entry, with:account, interaction: interaction) - case .DateEntry: - item = ChatDateStickItem(initialSize,entry, interaction: interaction) - case .bottom: - item = GeneralRowItem(initialSize, height: 20, stableId: entry.stableId) - } + func makeItem(_ entry: ChatWrapperEntry) -> TableRowItem { + + let presentation: TelegramPresentationTheme = entry.entry.additionalData.chatTheme ?? theme + + let item:TableRowItem = ChatRowItem.item(initialSize, from: entry.appearance.entry, interaction: interaction, downloadSettings: entry.automaticDownload, theme: presentation) _ = item.makeSize(initialSize.width) return item; } @@ -463,19 +908,20 @@ fileprivate func prepareEntries(from fromView:ChatHistoryView?, to toView:ChatHi var cancelled = false if fromView == nil && firstTransition, let state = scrollToItem { - + var initialIndex:Int = 0 var height:CGFloat = 0 var firstInsertion:[(Int, TableRowItem)] = [] let entries = Array(toView.filteredEntries.reversed()) switch state { - case let .top(stableId, _, _, relativeOffset): + case let .top(stableId, _, _, _, relativeOffset): var index:Int? = nil height = relativeOffset for k in 0 ..< entries.count { if entries[k].stableId == stableId { - index = k + let x:Int = Int(ceil(abs(offset) / 28)) + index = min(entries.count - 1, k + x) break } } @@ -484,11 +930,11 @@ fileprivate func prepareEntries(from fromView:ChatHistoryView?, to toView:ChatHi var success:Bool = false var j:Int = index for i in stride(from: index, to: -1, by: -1) { - let item = makeItem(entries[i].entry) + let item = makeItem(entries[i]) height += item.height firstInsertion.append((index - j, item)) j -= 1 - if initialSize.height < height { + if initialSize.height + offset < height { success = true break } @@ -496,10 +942,10 @@ fileprivate func prepareEntries(from fromView:ChatHistoryView?, to toView:ChatHi if !success { for i in (index + 1) ..< entries.count { - let item = makeItem(entries[i].entry) + let item = makeItem(entries[i]) height += item.height firstInsertion.insert((0, item), at: 0) - if initialSize.height < height { + if initialSize.height + offset < height { success = true break } @@ -523,10 +969,10 @@ fileprivate func prepareEntries(from fromView:ChatHistoryView?, to toView:ChatHi } else { let alreadyInserted = firstInsertion.count for i in alreadyInserted ..< entries.count { - let item = makeItem(entries[i].entry) + let item = makeItem(entries[i]) height += item.height firstInsertion.append((i, item)) - if initialSize.height < height { + if initialSize.height + offset < height { break } } @@ -534,7 +980,7 @@ fileprivate func prepareEntries(from fromView:ChatHistoryView?, to toView:ChatHi } - case let .center(stableId, _, _, _): + case let .center(stableId, _, _, _, _): var index:Int? = nil for k in 0 ..< entries.count { @@ -544,7 +990,7 @@ fileprivate func prepareEntries(from fromView:ChatHistoryView?, to toView:ChatHi } } if let index = index { - let item = makeItem(entries[index].entry) + let item = makeItem(entries[index]) height += item.height firstInsertion.append((index, item)) @@ -559,32 +1005,28 @@ fileprivate func prepareEntries(from fromView:ChatHistoryView?, to toView:ChatHi while !lowSuccess || !highSuccess { - if initialSize.height / 2 > lowHeight && !lowSuccess { - let item = makeItem(entries[low].entry) + if ((initialSize.height + offset) / 2) >= lowHeight && !lowSuccess { + let item = makeItem(entries[low]) lowHeight += item.height firstInsertion.append((low, item)) } - if initialSize.height / 2 > highHeight && !highSuccess { - let item = makeItem(entries[high].entry) + if ((initialSize.height + offset) / 2) >= highHeight && !highSuccess { + let item = makeItem(entries[high]) highHeight += item.height firstInsertion.append((high, item)) } - if ((initialSize.height / 2 < lowHeight ) || low == entries.count - 1) { + if ((((initialSize.height + offset) / 2) <= lowHeight ) || low == entries.count - 1) { lowSuccess = true - } else { + } else if !lowSuccess { low += 1 } - if high == 0 { - var bp:Int = 0 - bp += 1 - } - if ((initialSize.height / 2 < highHeight) || high == 0) { + if ((((initialSize.height + offset) / 2) <= highHeight) || high == 0) { highSuccess = true - } else { + } else if !highSuccess { high -= 1 } @@ -603,14 +1045,14 @@ fileprivate func prepareEntries(from fromView:ChatHistoryView?, to toView:ChatHi for i in 0 ..< copy.count { firstInsertion.append((i, copy[i].1)) } - } + break default: for i in 0 ..< entries.count { - let item = makeItem(entries[i].entry) + let item = makeItem(entries[i]) firstInsertion.append((i, item)) height += item.height @@ -619,29 +1061,29 @@ fileprivate func prepareEntries(from fromView:ChatHistoryView?, to toView:ChatHi } } } - subscriber.putNext(TableUpdateTransition(deleted: [], inserted: firstInsertion, updated: [], state:state)) - + messagesViewQueue.async { if !cancelled { var firstInsertedRange:NSRange = NSMakeRange(0, 0) - + if !firstInsertion.isEmpty { firstInsertedRange = NSMakeRange(initialIndex, firstInsertion.count) } var insertions:[(Int, TableRowItem)] = [] - var updates:[(Int, TableRowItem)] = [] + let updates:[(Int, TableRowItem)] = [] + for i in 0 ..< entries.count { let item:TableRowItem if firstInsertedRange.indexIn(i) { - item = firstInsertion[i - initialIndex].1 - updates.append((i, item)) + //item = firstInsertion[i - initialIndex].1 + //updates.append((i, item)) } else { - item = makeItem(entries[i].entry) + item = makeItem(entries[i]) insertions.append((i, item)) } @@ -653,7 +1095,7 @@ fileprivate func prepareEntries(from fromView:ChatHistoryView?, to toView:ChatHi } else if let state = scrollToItem { let (removed,inserted,updated) = proccessEntries(fromView?.filteredEntries, right: toView.filteredEntries, { entry -> TableRowItem in - return makeItem(entry.entry) + return makeItem(entry) }) let grouping: Bool if case .none = state { @@ -661,6 +1103,8 @@ fileprivate func prepareEntries(from fromView:ChatHistoryView?, to toView:ChatHi } else { grouping = true } + + subscriber.putNext(TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: animated, state: state, grouping: grouping)) subscriber.putCompletion() } @@ -678,7 +1122,7 @@ fileprivate func prepareEntries(from fromView:ChatHistoryView?, to toView:ChatHi private func maxIncomingMessageIndexForEntries(_ entries: [ChatHistoryEntry], indexRange: (Int, Int)) -> MessageIndex? { if !entries.isEmpty { for i in (indexRange.0 ... indexRange.1).reversed() { - if case let .MessageEntry(message, _, _, _, _) = entries[i], message.flags.contains(.Incoming) { + if case let .MessageEntry(message, _, _, _, _, _, _) = entries[i], message.flags.contains(.Incoming) { return MessageIndex(message) } } @@ -690,18 +1134,28 @@ private func maxIncomingMessageIndexForEntries(_ entries: [ChatHistoryEntry], in enum ChatHistoryViewTransitionReason { case Initial(fadeIn: Bool) case InteractiveChanges - case HoleChanges(filledHoleDirections: [MessageIndex: HoleFillDirection], removeHoleDirections: [MessageIndex: HoleFillDirection]) + case HoleReload case Reload } +private struct ChatTopVisibleMessageRange: Equatable { + var lowerBound: MessageId + var upperBound: MessageId + var isLast: Bool +} -class ChatController: EditableViewController, Notifable { - - private var peerId:PeerId - private let peerView = Promise() - private var peer:Peer? +private struct ChatDismissedPins : Equatable { + let ids: [MessageId] + let tempMaxId: MessageId? +} + +class ChatController: EditableViewController, Notifable, TableViewDelegate { + private var chatLocation:ChatLocation + private let peerView = Promise() + private let emojiEffects: EmojiScreenEffect + private let historyDisposable:MetaDisposable = MetaDisposable() private let peerDisposable:MetaDisposable = MetaDisposable() private let updatedChannelParticipants:MetaDisposable = MetaDisposable() @@ -711,7 +1165,6 @@ class ChatController: EditableViewController, Notifable { private let peerInputActivitiesDisposable:MetaDisposable = MetaDisposable() private let connectionStatusDisposable:MetaDisposable = MetaDisposable() private let messagesActionDisposable:MetaDisposable = MetaDisposable() - private let openPeerInfoDisposable:MetaDisposable = MetaDisposable() private let unblockDisposable:MetaDisposable = MetaDisposable() private let updatePinnedDisposable:MetaDisposable = MetaDisposable() private let reportPeerDisposable:MetaDisposable = MetaDisposable() @@ -722,7 +1175,63 @@ class ChatController: EditableViewController, Notifable { private let navigationActionDisposable:MetaDisposable = MetaDisposable() private let messageIndexDisposable: MetaDisposable = MetaDisposable() private let dateDisposable:MetaDisposable = MetaDisposable() + private let interactiveReadingDisposable: MetaDisposable = MetaDisposable() + private let showRightControlsDisposable: MetaDisposable = MetaDisposable() + private let deleteChatDisposable: MetaDisposable = MetaDisposable() + private let loadSelectionMessagesDisposable: MetaDisposable = MetaDisposable() + private let updateMediaDisposable = MetaDisposable() + private let editCurrentMessagePhotoDisposable = MetaDisposable() + private let failedMessageEventsDisposable = MetaDisposable() + private let selectMessagePollOptionDisposables: DisposableDict = DisposableDict() + private let updateReqctionsDisposable: DisposableDict = DisposableDict() + private let failedMessageIdsDisposable = MetaDisposable() + private let hasScheduledMessagesDisposable = MetaDisposable() + private let onlineMemberCountDisposable = MetaDisposable() + private let chatUndoDisposable = MetaDisposable() + private let discussionDataLoadDisposable = MetaDisposable() + private let slowModeDisposable = MetaDisposable() + private let slowModeInProgressDisposable = MetaDisposable() + private let forwardMessagesDisposable = MetaDisposable() + private let shiftSelectedDisposable = MetaDisposable() + private let updateUrlDisposable = MetaDisposable() + private let loadSharedMediaDisposable = MetaDisposable() + private let pollChannelDiscussionDisposable = MetaDisposable() + private let peekDisposable = MetaDisposable() + private let loadThreadDisposable = MetaDisposable() + private let recordActivityDisposable = MetaDisposable() + private let suggestionsDisposable = MetaDisposable() + private let searchState: ValuePromise = ValuePromise(SearchMessagesResultState("", []), ignoreRepeated: true) + + private let pollAnswersLoading: ValuePromise<[MessageId : ChatPollStateData]> = ValuePromise([:], ignoreRepeated: true) + private let pollAnswersLoadingValue: Atomic<[MessageId : ChatPollStateData]> = Atomic(value: [:]) + + private let topVisibleMessageRange = ValuePromise(nil, ignoreRepeated: true) + private let dismissedPinnedIds = ValuePromise(ChatDismissedPins(ids: [], tempMaxId: nil), ignoreRepeated: true) + + + private var grouppedFloatingPhotos: [([ChatRowItem], NSView)] = [] + + private let chatThemeValue: Promise<(String?, TelegramPresentationTheme)> = Promise((nil, theme)) + private let chatThemeTempValue: Promise = Promise(nil) + + private var pollAnswersLoadingSignal: Signal<[MessageId : ChatPollStateData], NoError> { + return pollAnswersLoading.get() + } + private func updatePoll(_ f:([MessageId : ChatPollStateData])-> [MessageId : ChatPollStateData]) -> Void { + pollAnswersLoading.set(pollAnswersLoadingValue.modify(f)) + } + + private let threadLoading: ValuePromise = ValuePromise(nil, ignoreRepeated: true) + private let threadLoadingValue: Atomic = Atomic(value: nil) + private var threadLoadingSignal: Signal { + return threadLoading.get() + } + private func updateThread(_ f:(MessageId?)-> MessageId?) -> Void { + threadLoading.set(threadLoadingValue.modify(f)) + } + + var chatInteraction:ChatInteraction var nextTransaction:TransactionHandler = TransactionHandler() @@ -732,21 +1241,37 @@ class ChatController: EditableViewController, Notifable { private let location:Promise = Promise() + private let _locationValue:Atomic = Atomic(value: nil) + private var locationValue:ChatHistoryLocation? { + return _locationValue.with { $0 } + } + private func setLocation(_ location: ChatHistoryLocation) { + _ = _locationValue.swap(location) self.location.set(.single(location)) } + + private let chatHistoryLocationPromise = ValuePromise() + private var nextHistoryLocationId: Int32 = 1 + private func takeNextHistoryLocationId() -> Int32 { + let id = self.nextHistoryLocationId + self.nextHistoryLocationId += 5 + return id + } + private let maxVisibleIncomingMessageIndex = ValuePromise(ignoreRepeated: true) private let readHistoryDisposable = MetaDisposable() + private let chatLocationContextHolder: Atomic + private let initialDataHandler:Promise = Promise() - private let autoremovingUnreadMark:Promise = Promise(nil) let previousView = Atomic(value: nil) - private let botCallbackAlertMessage = Promise(nil) + private let botCallbackAlertMessage = Promise<(String?, Bool)>((nil, false)) private var botCallbackAlertMessageDisposable: Disposable? private var selectTextController:ChatSelectText! @@ -757,8 +1282,16 @@ class ChatController: EditableViewController, Notifable { let layoutDisposable:MetaDisposable = MetaDisposable() + private var afterNextTransaction:(()->Void)? + + private var currentAnimationRows:[TableAnimationInterface.AnimateItem] = [] + + private let adMessages: AdMessagesHistoryContext? + + private var themeSelector: ChatThemeSelectorController? = nil private let messageProcessingManager = ChatMessageThrottledProcessingManager() + private let unsupportedMessageProcessingManager = ChatMessageThrottledProcessingManager() private let messageMentionProcessingManager = ChatMessageThrottledProcessingManager(delay: 0.2) var historyState:ChatHistoryState = ChatHistoryState() { didSet { @@ -767,28 +1300,63 @@ class ChatController: EditableViewController, Notifable { //} } } + + func clearReplyStack() { + self.historyState = historyState.withClearReplies() + } + override var navigationController: NavigationViewController? { + didSet { + updateSidebar() + } + } - override func scrollup() -> Void { + override func scrollup(force: Bool = false) -> Void { + chatInteraction.update({ $0.withUpdatedTempPinnedMaxId(nil) }) if let reply = historyState.reply() { - if let message = messageInCurrentHistoryView(reply) { - let stableId = ChatHistoryEntryId.message(message) // - genericView.tableView.scroll(to: .center(id: stableId, animated: true, focus: true, inset: 0)) - } else { - chatInteraction.focusMessageId(nil, reply, .center(id: 0, animated: true, focus: true, inset: 0)) - } + chatInteraction.focusMessageId(nil, reply, .CenterEmpty) historyState = historyState.withRemovingReplies(max: reply) } else { - if previousView.modify({$0})?.originalView.laterId != nil { - setLocation(.Scroll(index: MessageIndex.upperBound(peerId: self.peerId), anchorIndex: MessageIndex.upperBound(peerId: self.peerId), sourceIndex: MessageIndex.lowerBound(peerId: self.peerId), scrollPosition: .down(true), animated: true)) + let laterId = previousView.with { $0?.originalView?.laterId } + if laterId != nil { + + let history: ChatHistoryLocation = .Scroll(index: MessageHistoryAnchorIndex.upperBound, anchorIndex: MessageHistoryAnchorIndex.upperBound, sourceIndex: MessageHistoryAnchorIndex.upperBound, scrollPosition: .down(true), count: requestCount, animated: true) + + let historyView = chatHistoryViewForLocation(history, context: context, chatLocation: chatLocation, fixedCombinedReadStates: nil, tagMask: mode.tagMask, additionalData: [], chatLocationContextHolder: chatLocationContextHolder) + + let signal = historyView + |> mapToSignal { historyView -> Signal in + switch historyView { + case .Loading: + return .single(true) + case let .HistoryView(view, _, _, _): + if !view.holeEarlier, view.laterId == nil, !view.isLoading { + return .single(false) + } + return .single(true) + } + } |> take(until: { index in + return SignalTakeAction(passthrough: !index, complete: !index) + }) + + messageIndexDisposable.set(showModalProgress(signal: signal, for: context.window).start(next: { [weak self] _ in + self?.setLocation(history) + }, completed: { + + })) } else { genericView.tableView.scroll(to: .down(true)) } + } } + private var requestCount: Int { + return Int(round(genericView.tableView.frame.height / 28)) + 10 + } + func readyHistory() { if !didSetHistoryReady { didSetHistoryReady = true @@ -797,35 +1365,233 @@ class ChatController: EditableViewController, Notifable { } override var sidebar:ViewController? { - return account.context.entertainment + return context.sharedContext.bindings.entertainment() } func updateSidebar() { if FastSettings.sidebarShown && FastSettings.sidebarEnabled { - (navigationController as? MajorNavigationController)?.genericView.setProportion(proportion: SplitProportion(min:380, max:800), state: .single) - (navigationController as? MajorNavigationController)?.genericView.setProportion(proportion: SplitProportion(min:380+350, max:700), state: .dual) + (navigationController as? MajorNavigationController)?.genericView.setProportion(proportion: SplitProportion(min:380, max:730), state: .single) + (navigationController as? MajorNavigationController)?.genericView.setProportion(proportion: SplitProportion(min:380+350, max:.greatestFiniteMagnitude), state: .dual) } else { (navigationController as? MajorNavigationController)?.genericView.removeProportion(state: .dual) (navigationController as? MajorNavigationController)?.genericView.setProportion(proportion: SplitProportion(min:380, max: .greatestFiniteMagnitude), state: .single) } } + private func updateFloatingPhotos(_ position: ScrollPosition, animated: Bool, currentAnimationRows: [TableAnimationInterface.AnimateItem] = []) { + + let offset = genericView.tableView.clipView.bounds.origin + + var floating: [ChatFloatingPhoto] = [] + for groupped in grouppedFloatingPhotos { + let photoView = groupped.1 + + let views = groupped.0.compactMap { $0.view as? ChatRowView }.filter { $0.visibleRect != .zero } + + guard !views.isEmpty else { + continue + } + + var point: NSPoint = .init(x: groupped.0[0].leftInset, y: 0) + + + let ph: CGFloat = 36 + let gap: CGFloat = 10 + let inset: CGFloat = 3 + + + let lastMax = views[views.count - 1].frame.maxY - inset + let firstMin = views[0].frame.minY + inset + + if offset.y >= lastMax - ph - gap { + point.y = lastMax - offset.y - ph + } else if offset.y + gap > firstMin { + point.y = gap + } else { + point.y = firstMin - offset.y + } + + let revealView = views.first(where: { + $0.hasRevealState + }) + + if let revealView = revealView { + + let maxOffset = revealView.frame.maxY - offset.y + let minOffset = revealView.frame.minY - offset.y + + let rect = NSMakeRect(0, minOffset, revealView.frame.width, maxOffset - minOffset) + if NSPointInRect(point, rect) { + point.x += revealView.containerX + } else if NSPointInRect(NSMakePoint(point.x, point.y + photoView.frame.height - 1), rect) { + point.x += revealView.containerX + } + + } + + let value: ChatFloatingPhoto = .init(point: point, items: groupped.0, photoView: photoView) + floating.append(value) + } + genericView.updateFloating(floating, animated: animated, currentAnimationRows: currentAnimationRows) + } + + private func collectFloatingPhotos(animated: Bool, currentAnimationRows: [TableAnimationInterface.AnimateItem]) { + guard let peer = self.chatInteraction.peer, let theme = self.previousView.with({ $0?.theme }) else { + self.grouppedFloatingPhotos = [] + return + } + guard peer.isGroup || peer.isSupergroup || peer.id == context.peerId, theme.bubbled else { + self.grouppedFloatingPhotos = [] + return + } + let cached:[MessageId : NSView] = grouppedFloatingPhotos.reduce([:], { current, value in + var current = current + let item = value.0[value.0.count - 1] + let view = value.1 + current[item.message!.id] = view + return current + }) + + var groupped:[[ChatRowItem]] = [] + var current:[ChatRowItem] = [] + self.genericView.tableView.enumerateItems { item in + var skipOrFill = true + if let item = item as? ChatRowItem { + if item.canHasFloatingPhoto { + let prev = current.last + let sameAuthor = prev?.message?.author?.id == item.message?.author?.id + var canGroup = false + if sameAuthor { + if case .Short = item.itemType { + canGroup = true + } + } + if prev == nil || canGroup { + skipOrFill = false + current.append(item) + } + } + } + if skipOrFill { + if !current.isEmpty { + groupped.append(current) + } + current = [] + + if let item = item as? ChatRowItem { + if item.canHasFloatingPhoto { + current.append(item) + } + } + } + return true + } + if !current.isEmpty { + groupped.append(current) + } + self.grouppedFloatingPhotos = groupped.compactMap { value in + let item = value[value.count - 1] + let view = cached[item.message!.id] ?? ChatRowView.makePhotoView(item) + let control = view as? AvatarControl + control?.removeAllHandlers() + control?.set(handler: { [weak item] _ in + item?.openInfo() + }, for: .Click) + if let control = control { + return (value, control) + } else { + return nil + } + } + + self.updateFloatingPhotos(genericView.scroll, animated: animated, currentAnimationRows: currentAnimationRows) + } + + + + override func viewDidResized(_ size: NSSize) { + super.viewDidResized(size) + self.updateFloatingPhotos(genericView.scroll, animated: false) + } + override func viewDidLoad() { super.viewDidLoad() - - updateSidebar() + genericView.tableView.addScroll(listener: emojiEffects.scrollUpdater) + + + self.genericView.tableView.addScroll(listener: .init(dispatchWhenVisibleRangeUpdated: false, { [weak self] position in + self?.updateFloatingPhotos(position, animated: false) + })) + + + let previousView = self.previousView + let context = self.context + let atomicSize = self.atomicSize + let chatInteraction = self.chatInteraction + let nextTransaction = self.nextTransaction + let chatLocation = self.chatLocation + let mode = self.mode + let peerId = self.chatInteraction.peerId + - // navigationController.genericView.setpropo + if chatInteraction.peerId.namespace == Namespaces.Peer.CloudChannel { + slowModeInProgressDisposable.set((context.account.postbox.unsentMessageIdsView() |> mapToSignal { view -> Signal<[MessageId], NoError> in + return context.account.postbox.messagesAtIds(Array(view.ids)) |> map { messages in + return messages.filter { $0.flags.contains(.Unsent) }.map { $0.id } + } + } |> deliverOnMainQueue).start(next: { [weak self] ids in + self?.chatInteraction.update({ $0.updateSlowMode { + $0?.withUpdatedSendingIds(ids) + }}) + })) + } + + + genericView.tableView.emptyChecker = { [weak self] items in + + let filtred = items.filter { item in + if let item = item as? ChatRowItem, let message = item.message { + if let action = message.media.first as? TelegramMediaAction { + switch action.action { + case .groupCreated: + return messageMainPeer(message)?.groupAccess.isCreator == false + case .groupMigratedToChannel: + return false + case .channelMigratedFromGroup: + return false + case .photoUpdated: + return true + default: + return true + } + } + return true + } + return false + } + + return filtred.isEmpty && self?.genericView.state != .progress + } - self.peerView.set(account.viewTracker.peerView(peerId)) + + genericView.tableView.delegate = self + + + switch chatLocation { + case let .peer(peerId): + self.peerView.set(context.account.viewTracker.peerView(peerId, updateData: true) |> map {Optional($0)}) + let _ = context.engine.peers.checkPeerChatServiceActions(peerId: peerId).start() + case let .replyThread(data): + self.peerView.set(context.account.viewTracker.peerView(data.messageId.peerId, updateData: true) |> map {Optional($0)}) + } + - globalPeerHandler.set(.single(self.peerId)) +// context.globalPeerHandler.set(.single(chatLocation)) - let layout:Atomic = Atomic(value:account.context.layout) - let fixedCombinedReadState = Atomic(value: nil) - layoutDisposable.set(account.context.layoutHandler.get().start(next: {[weak self] (state) in + let layout:Atomic = Atomic(value:context.sharedContext.layout) + layoutDisposable.set(context.sharedContext.layoutHandler.get().start(next: {[weak self] (state) in let previous = layout.swap(state) if previous != state, let navigation = self?.navigationController { self?.requestUpdateBackBar() @@ -837,737 +1603,2867 @@ class ChatController: EditableViewController, Notifable { selectTextController = ChatSelectText(genericView.tableView) - // let additionalData: [AdditionalMessageHistoryViewData] = [.cachedPeerData(peerId), .totalUnreadCount] - - let historyViewUpdate = location.get() |> distinctUntilChanged - |> mapToSignal { [weak self] location -> Signal in - if let strongSelf = self { + let maxReadIndex:ValuePromise = ValuePromise() + var didSetReadIndex: Bool = false + + var chatLocationContextHolder = self.chatLocationContextHolder - return chatHistoryViewForLocation(location, account: strongSelf.account, peerId: strongSelf.peerId, fixedCombinedReadState: fixedCombinedReadState.with { $0 }, tagMask: nil, additionalData: []) |> beforeNext { viewUpdate in - switch viewUpdate { - case let .HistoryView(view, _, _, _): - let _ = fixedCombinedReadState.swap(view.combinedReadState) - default: - break + let historyViewUpdate1 = location.get() |> deliverOn(messagesViewQueue) + |> mapToSignal { location -> Signal<(ChatHistoryViewUpdate, TableSavingSide?), NoError> in + + var additionalData: [AdditionalMessageHistoryViewData] = [] + additionalData.append(.cachedPeerData(peerId)) + additionalData.append(.peerNotificationSettings(peerId)) + additionalData.append(.preferencesEntry(PreferencesKeys.limitsConfiguration)) + additionalData.append(.preferencesEntry(ApplicationSpecificPreferencesKeys.autoplayMedia)) + additionalData.append(.preferencesEntry(ApplicationSpecificPreferencesKeys.automaticMediaDownloadSettings)) + if peerId.namespace == Namespaces.Peer.CloudChannel { + additionalData.append(.cacheEntry(cachedChannelAdminRanksEntryId(peerId: peerId))) + additionalData.append(.peer(peerId)) + } + if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.SecretChat { + additionalData.append(.peerIsContact(peerId)) + } + switch chatLocation { + case let .replyThread(data): + additionalData.append(.message(data.messageId)) + case .peer: + additionalData.append(.cachedPeerDataMessages(peerId)) + } + + return chatHistoryViewForLocation(location, context: context, chatLocation: chatLocation, fixedCombinedReadStates: { nil }, tagMask: mode.tagMask, mode: mode, additionalData: additionalData, chatLocationContextHolder: chatLocationContextHolder) |> beforeNext { viewUpdate in + switch viewUpdate { + case let .HistoryView(view, _, _, _): + if !didSetReadIndex { + if let index = view.maxReadIndex { + if let last = view.entries.last { + if index.id >= last.index.id { + maxReadIndex.set(nil) + } else { + maxReadIndex.set(index) + } + } else { + maxReadIndex.set(index) + } + } else { + maxReadIndex.set(view.maxReadIndex) + } + didSetReadIndex = true } + default: + maxReadIndex.set(nil) } + } |> map { view in + return (view, location.side) } - return .never() } - - - //let autoremovingUnreadRemoved:Atomic = Atomic(value: false) - let previousAppearance:Atomic = Atomic(value: appAppearance) - let firstInitialUpdate:Atomic = Atomic(value: true) + let historyViewUpdate = historyViewUpdate1 - let historyViewTransition = combineLatest(historyViewUpdate |> deliverOnMainQueue, autoremovingUnreadMark.get() |> deliverOnMainQueue, appearanceSignal |> deliverOnMainQueue, account.context.cachedAdminIds.ids(postbox: account.postbox, network: account.network, peerId: peerId) |> deliverOnMainQueue) |> mapToQueue { [weak self] update, autoremoving, appearance, adminIds -> Signal<(TableUpdateTransition, ChatHistoryCombinedInitialData), NoError> in - if let strongSelf = self { - - switch update { - case let .Loading(initialData): - let combinedInitialData = ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: nil, cachedData: nil, readStateData: nil) - strongSelf.initialDataHandler.set(.single(combinedInitialData) |> deliverOnMainQueue) - strongSelf.readyHistory() - strongSelf.genericView.change(state: .progress, animated: true) - - return .complete() - - case let .HistoryView(view, updateType, scrollPosition, initialData): - - var prepareOnMainQueue = previousAppearance.swap(appearance).presentation.dark != appearance.presentation.dark - switch updateType { - case .Initial: - prepareOnMainQueue = firstInitialUpdate.swap(false) || prepareOnMainQueue - default: - break + + let animatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false) + |> map { result -> [String: StickerPackItem] in + switch result { + case let .result(_, items, _): + var animatedEmojiStickers: [String: StickerPackItem] = [:] + for case let item as StickerPackItem in items { + if let emoji = item.getStringRepresentationsOfIndexKeys().first { + animatedEmojiStickers[emoji] = item + } } - + return animatedEmojiStickers + default: + return [:] + } + } - - let animated = autoremoving == nil ? false : autoremoving! - - if view.maxReadIndex != nil, autoremoving == nil { - strongSelf.autoremovingUnreadMark.set(.single(true) |> delay(5.0, queue: Queue.mainQueue()) |> then(.single(false))) + + let customChannelDiscussionReadState: Signal + if case let .peer(peerId) = chatLocation, peerId.namespace == Namespaces.Peer.CloudChannel { + let cachedDataKey = PostboxViewKey.cachedPeerData(peerId: chatLocation.peerId) + let peerKey = PostboxViewKey.basicPeer(peerId) + customChannelDiscussionReadState = context.account.postbox.combinedView(keys: [cachedDataKey, peerKey]) + |> mapToSignal { views -> Signal in + guard let view = views.views[cachedDataKey] as? CachedPeerDataView else { + return .single(nil) } - - let animationInterface: TableAnimationInterface = TableAnimationInterface(strongSelf.nextTransaction.isExutable) - - - - let proccesedView = ChatHistoryView(originalView: view, filteredEntries: messageEntries(view.entries, maxReadIndex: autoremoving == nil ? view.maxReadIndex : nil, dayGrouping: true, includeBottom: true, timeDifference: strongSelf.account.context.timeDifference, adminIds: adminIds).map({AppearanceWrapperEntry(entry: $0, appearance: appearance)})) - - - return prepareEntries(from: strongSelf.previousView.swap(proccesedView), to: proccesedView, account: strongSelf.account, initialSize: strongSelf.atomicSize.modify({$0}), interaction:strongSelf.chatInteraction, animated: animated, scrollPosition:scrollPosition, reason:updateType, animationInterface:animationInterface) |> map { transition in - return (transition,initialData) - } |> runOn(prepareOnMainQueue ? Queue.mainQueue(): messagesViewQueue) + guard let peer = (views.views[peerKey] as? BasicPeerView)?.peer as? TelegramChannel, case .broadcast = peer.info else { + return .single(nil) + } + guard let cachedData = view.cachedPeerData as? CachedChannelData else { + return .single(nil) + } + guard case let .known(value) = cachedData.linkedDiscussionPeerId else { + return .single(nil) + } + return .single(value) } + |> distinctUntilChanged + |> mapToSignal { discussionPeerId -> Signal in + guard let discussionPeerId = discussionPeerId else { + return .single(nil) + } + let key = PostboxViewKey.combinedReadState(peerId: discussionPeerId) + return context.account.postbox.combinedView(keys: [key]) + |> map { views -> MessageId? in + guard let view = views.views[key] as? CombinedReadStateView else { + return nil + } + guard let state = view.state else { + return nil + } + for (namespace, namespaceState) in state.states { + if namespace == Namespaces.Message.Cloud { + switch namespaceState { + case let .idBased(maxIncomingReadId, _, _, _, _): + return MessageId(peerId: discussionPeerId, namespace: Namespaces.Message.Cloud, id: maxIncomingReadId) + default: + break + } + } + } + return nil + } + |> distinctUntilChanged } - - return .never() - - } |> deliverOnMainQueue + } else { + customChannelDiscussionReadState = .single(nil) + } + let customThreadOutgoingReadState: Signal + if case .replyThread = chatLocation { + customThreadOutgoingReadState = context.chatLocationOutgoingReadState(for: chatLocation, contextHolder: chatLocationContextHolder) + } else { + customThreadOutgoingReadState = .single(nil) + } + + let animatedRows:([TableAnimationInterface.AnimateItem])->Void = { [weak self] items in + self?.currentAnimationRows = items + } - let appliedTransition = historyViewTransition |> map { [weak self] transition, initialData in - self?.applyTransition(transition, initialData: initialData) + let previousAppearance:Atomic = Atomic(value: appAppearance) + let firstInitialUpdate:Atomic = Atomic(value: true) + + let applyHole:() -> Void = { [weak self] in + guard let `self` = self else { return } + + let visibleRows = self.genericView.tableView.visibleRows() + var messageIndex: MessageIndex? + for i in stride(from: visibleRows.max - 1, to: -1, by: -1) { + if let item = self.genericView.tableView.item(at: i) as? ChatRowItem, let message = item.message { + messageIndex = MessageIndex(message) + break + } + } + if let messageIndex = messageIndex { + self.setLocation(.Navigation(index: MessageHistoryAnchorIndex.message(messageIndex), anchorIndex: MessageHistoryAnchorIndex.message(messageIndex), count: self.requestCount, side: .upper)) + } else if let location = self.locationValue { + self.setLocation(location) + } } - self.historyDisposable.set(appliedTransition.start()) + let _searchState: Atomic = Atomic(value: SearchMessagesResultState("", [])) - let previousMaxIncomingMessageIdByNamespace = Atomic<[MessageId.Namespace: MessageId]>(value: [:]) - let readHistory = combineLatest(self.maxVisibleIncomingMessageIndex.get(), self.isKeyWindow.get()) - |> map { [weak self] messageIndex, canRead in - if canRead { - var apply = false - let _ = previousMaxIncomingMessageIdByNamespace.modify { dict in - let previousIndex = dict[messageIndex.id.namespace] - if previousIndex == nil || previousIndex!.id < messageIndex.id.id { - apply = true - var dict = dict - dict[messageIndex.id.namespace] = messageIndex.id - return dict - } - return dict - } - if let account = self?.account, apply, let peerId = self?.peerId { - clearNotifies(peerId, maxId: messageIndex.id) - _ = applyMaxReadIndexInteractively(postbox: account.postbox, network: account.network, index: messageIndex).start() + let updatingMedia = context.account.pendingUpdateMessageManager.updatingMessageMedia + |> map { value -> [MessageId: ChatUpdatingMessageMedia] in + var result = value + for id in value.keys { + if id.peerId != peerId { + result.removeValue(forKey: id) } } - } - - self.readHistoryDisposable.set(readHistory.start()) + return result + } + |> distinctUntilChanged + let previousUpdatingMedia = Atomic<[MessageId: ChatUpdatingMessageMedia]?>(value: nil) - - chatInteraction.setupReplyMessage = { [weak self] (messageId) in - self?.chatInteraction.update({$0.updatedInterfaceState({$0.withUpdatedReplyMessageId(messageId)})}) + let adMessages:Signal<[Message], NoError> + if let ad = self.adMessages { + adMessages = ad.state + } else { + adMessages = .single([]) } - let scrollAfterSend:()->Void = { [weak self] in - self?.chatInteraction.scrollToLatest(true) + let themeEmoticon: Signal = self.peerView.get() |> map { + ($0 as? PeerView)?.cachedData + } |> map { cachedData in + var themeEmoticon: String? = nil + if let cachedData = cachedData as? CachedUserData { + themeEmoticon = cachedData.themeEmoticon + } else if let cachedData = cachedData as? CachedGroupData { + themeEmoticon = cachedData.themeEmoticon + } else if let cachedData = cachedData as? CachedChannelData { + themeEmoticon = cachedData.themeEmoticon + } + return themeEmoticon + } |> distinctUntilChanged + + + let chatTheme:Signal<(String?, TelegramPresentationTheme), NoError> = combineLatest(context.chatThemes, themeEmoticon, appearanceSignal) |> map { chatThemes, themeEmoticon, appearance in + + var theme: TelegramPresentationTheme = appearance.presentation + if let themeEmoticon = themeEmoticon { + let chatThemeData = chatThemes.first(where: { $0.0 == themeEmoticon})?.1 + theme = chatThemeData ?? appearance.presentation + } + return (themeEmoticon, theme) + } + + self.chatThemeValue.set(chatTheme) + + + let effectiveTheme = combineLatest(self.chatThemeValue.get() |> map { $0.1 }, chatThemeTempValue.get()) |> map { + $1 ?? $0 + } + + let historyViewTransition = combineLatest(queue: messagesViewQueue, + historyViewUpdate, + appearanceSignal, + combineLatest(maxReadIndex.get() |> deliverOnMessagesViewQueue, + pollAnswersLoadingSignal, threadLoadingSignal), + searchState.get(), + animatedEmojiStickers, + customChannelDiscussionReadState, + customThreadOutgoingReadState, + updatingMedia, + adMessages, + effectiveTheme +) |> mapToQueue { update, appearance, readIndexAndOther, searchState, animatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, updatingMedia, adMessages, chatTheme -> Signal<(TableUpdateTransition, MessageHistoryView?, ChatHistoryCombinedInitialData, Bool, ChatHistoryView), NoError> in + + let maxReadIndex = readIndexAndOther.0 + let pollAnswersLoading = readIndexAndOther.1 + let threadLoading = readIndexAndOther.2 + + let searchStateUpdated = _searchState.swap(searchState) != searchState + + let isLoading: Bool + let view: MessageHistoryView? + let initialData: ChatHistoryCombinedInitialData + var updateType: ChatHistoryViewUpdateType + let scrollPosition: ChatHistoryViewScrollPosition? + switch update.0 { + case let .Loading(data, ut): + view = nil + initialData = data + isLoading = true + updateType = ut + scrollPosition = nil + case let .HistoryView(values): + initialData = values.initialData + view = values.view + isLoading = values.view.isLoading + updateType = values.type + scrollPosition = searchStateUpdated ? nil : values.scrollPosition + } + + if let updatedValue = previousUpdatingMedia.swap(updatingMedia), updatingMedia != updatedValue { + updateType = .Generic(type: .Generic) + } + + switch updateType { + case let .Generic(type: type): + switch type { + case .FillHole: + Queue.mainQueue().async(applyHole) + return .complete() + default: + break + } + default: + break + } + + + let pAppearance = previousAppearance.swap(appearance) + var prepareOnMainQueue = pAppearance.presentation != appearance.presentation + switch updateType { + case .Initial: + prepareOnMainQueue = firstInitialUpdate.swap(false) || prepareOnMainQueue + default: + break + } + let animationInterface: TableAnimationInterface = TableAnimationInterface(nextTransaction.isExutable && view?.laterId == nil, true, animatedRows) + let timeDifference = context.timeDifference + let bigEmojiEnabled = context.sharedContext.baseSettings.bigEmoji + + + var ranks: CachedChannelAdminRanks? + if let view = view { + for additionalEntry in view.additionalData { + if case let .cacheEntry(id, data) = additionalEntry { + if id == cachedChannelAdminRanksEntryId(peerId: chatInteraction.peerId), let data = data as? CachedChannelAdminRanks { + ranks = data + } + break + } + } + } + + + let proccesedView:ChatHistoryView + if let view = view { + if let peer = chatInteraction.peer, peer.isRestrictedChannel(context.contentSettings) { + proccesedView = ChatHistoryView(originalView: view, filteredEntries: [], theme: chatTheme) + } else { + let msgEntries = view.entries + let topMessages: [Message]? + var addTopThreadInset: CGFloat? = nil + switch chatInteraction.chatLocation { + case let .replyThread(data): + if view.earlierId == nil, !view.isLoading, !view.holeEarlier { + topMessages = initialData.cachedDataMessages?[data.messageId] + } else { + topMessages = nil + addTopThreadInset = 44 + } + case .peer: + topMessages = nil + } + + var ads:[Message] = [] + if !view.isLoading && view.laterId == nil { + ads = adMessages + } + + let entries = messageEntries(msgEntries, maxReadIndex: maxReadIndex, dayGrouping: true, renderType: chatTheme.bubbled ? .bubble : .list, includeBottom: true, timeDifference: timeDifference, ranks: ranks, pollAnswersLoading: pollAnswersLoading, threadLoading: threadLoading, groupingPhotos: true, autoplayMedia: initialData.autoplayMedia, searchState: searchState, animatedEmojiStickers: bigEmojiEnabled ? animatedEmojiStickers : [:], topFixedMessages: topMessages, customChannelDiscussionReadState: customChannelDiscussionReadState, customThreadOutgoingReadState: customThreadOutgoingReadState, addRepliesHeader: peerId == repliesPeerId && view.earlierId == nil, addTopThreadInset: addTopThreadInset, updatingMedia: updatingMedia, adMessages: ads, chatTheme: chatTheme).map({ChatWrapperEntry(appearance: AppearanceWrapperEntry(entry: $0, appearance: appearance), automaticDownload: initialData.autodownloadSettings)}) + proccesedView = ChatHistoryView(originalView: view, filteredEntries: entries, theme: chatTheme) + } + } else { + proccesedView = ChatHistoryView(originalView: nil, filteredEntries: [], theme: chatTheme) + } + + + return prepareEntries(from: previousView.swap(proccesedView), to: proccesedView, timeDifference: timeDifference, initialSize: atomicSize.modify({$0}), interaction: chatInteraction, animated: false, scrollPosition:scrollPosition, reason: updateType, animationInterface: animationInterface, side: update.1) |> map { transition in + return (transition, view, initialData, isLoading, proccesedView) + } |> runOn(prepareOnMainQueue ? Queue.mainQueue(): messagesViewQueue) + + } |> deliverOnMainQueue + + + let appliedTransition = historyViewTransition |> map { [weak self] transition, view, initialData, isLoading, proccesedView in + self?.applyTransition(transition, initialData: initialData, isLoading: isLoading, processedView: proccesedView) + } + + + self.historyDisposable.set(appliedTransition.start()) + + let previousMaxIncomingMessageIdByNamespace = Atomic<[MessageId.Namespace: MessageIndex]>(value: [:]) + let readHistory = combineLatest(self.maxVisibleIncomingMessageIndex.get(), self.isKeyWindow.get()) + |> map { [weak self] messageIndex, canRead in + guard let `self` = self else {return} + if canRead { + var apply = false + let _ = previousMaxIncomingMessageIdByNamespace.modify { dict in + let previousIndex = dict[messageIndex.id.namespace] + if previousIndex == nil || previousIndex! < messageIndex { + apply = true + var dict = dict + dict[messageIndex.id.namespace] = messageIndex + return dict + } + return dict + } + if apply, let window = self.window { + let peerId = self.chatLocation.peerId + if !hasModals(window) { + UNUserNotifications.current?.clearNotifies(peerId, maxId: messageIndex.id) + + + context.applyMaxReadIndex(for: self.chatLocation, contextHolder: self.chatLocationContextHolder, messageIndex: messageIndex) + } + } + } + } + + self.readHistoryDisposable.set(readHistory.start()) + + + + + chatInteraction.setupReplyMessage = { [weak self] messageId in + guard let `self` = self else { return } + + switch self.mode { + case .scheduled, .pinned, .preview: + return + case .history: + break + case .replyThread: + break + } + + self.chatInteraction.focusInputField() + let signal:Signal = messageId == nil ? .single(nil) : self.chatInteraction.context.account.postbox.messageAtId(messageId!) + _ = (signal |> deliverOnMainQueue).start(next: { [weak self] message in + self?.chatInteraction.update({ current in + var current = current.updatedInterfaceState({$0.withUpdatedReplyMessageId(messageId).withUpdatedReplyMessage(message)}) + if messageId == current.keyboardButtonsMessage?.replyAttribute?.messageId { + current = current.updatedInterfaceState({$0.withUpdatedDismissedForceReplyId(messageId)}) + } + return current + }) + }) + + + } + + chatInteraction.startRecording = { [weak self] hold, view in + guard let chatInteraction = self?.chatInteraction else {return} + if let slowMode = chatInteraction.presentation.slowMode, slowMode.hasLocked { + if let last = slowMode.sendingIds.last { + chatInteraction.focusMessageId(nil, last, .CenterEmpty) + } + if let view = self?.genericView.inputView.currentActionView { + showSlowModeTimeoutTooltip(slowMode, for: view) + return + } + } + if chatInteraction.presentation.recordingState != nil || chatInteraction.presentation.state != .normal { + NSSound.beep() + return + } + if let peer = chatInteraction.presentation.peer { + if let permissionText = permissionText(from: peer, for: .banSendMedia) { + alert(for: context.window, info: permissionText) + return + } + if chatInteraction.presentation.effectiveInput.inputText.isEmpty { + + + + switch FastSettings.recordingState { + case .voice: + let permission: Signal = requestMediaPermission(.audio) |> deliverOnMainQueue + _ = permission.start(next: { [weak chatInteraction] access in + guard let chatInteraction = chatInteraction else { + return + } + if access { + let state = ChatRecordingAudioState(context: chatInteraction.context, liveUpload: chatInteraction.peerId.namespace != Namespaces.Peer.SecretChat, autohold: hold) + state.start() + delay(0.1, closure: { [weak chatInteraction] in + chatInteraction?.update({$0.withRecordingState(state)}) + }) + } else { + confirm(for: context.window, information: L10n.requestAccesErrorHaveNotAccessVoiceMessages, okTitle: L10n.modalOK, cancelTitle: "", thridTitle: L10n.requestAccesErrorConirmSettings, successHandler: { result in + switch result { + case .thrid: + openSystemSettings(.none) + default: + break + } + }) + } + }) + case .video: + let permission: Signal = combineLatest(requestMediaPermission(.video), requestMediaPermission(.audio)) |> map { $0 && $1 } |> deliverOnMainQueue + _ = permission.start(next: { [weak chatInteraction] access in + guard let chatInteraction = chatInteraction else { + return + } + if access { + let state = ChatRecordingVideoState(context: chatInteraction.context, liveUpload: chatInteraction.peerId.namespace != Namespaces.Peer.SecretChat, autohold: hold) + showModal(with: VideoRecorderModalController(chatInteraction: chatInteraction, pipeline: state.pipeline), for: context.window) + chatInteraction.update({$0.withRecordingState(state)}) + } else { + confirm(for: context.window, information: L10n.requestAccesErrorHaveNotAccessVideoMessages, okTitle: L10n.modalOK, cancelTitle: "", thridTitle: L10n.requestAccesErrorConirmSettings, successHandler: { result in + switch result { + case .thrid: + openSystemSettings(.none) + default: + break + } + }) + } + + }) + } + + + } + } + } + + let scrollAfterSend:()->Void = { [weak self] in + guard let `self` = self else { return } + self.chatInteraction.scrollToLatest(true) + self.context.sharedContext.bindings.entertainment().closePopover() + self.context.cancelGlobalSearch.set(true) } let afterSentTransition = { [weak self] in - self?.chatInteraction.update({$0.updatedInterfaceState({$0.withUpdatedReplyMessageId(nil).withUpdatedInputState(ChatTextInputState()).withUpdatedForwardMessageIds([]).withUpdatedComposeDisableUrlPreview(nil)}).updatedUrlPreview(nil)}) + self?.chatInteraction.update({ presentation in + return presentation.updatedInputQueryResult {_ in + return nil + }.updatedInterfaceState { current in + + var value: ChatInterfaceState = current.withUpdatedReplyMessageId(nil).withUpdatedInputState(ChatTextInputState()).withUpdatedForwardMessageIds([]).withUpdatedComposeDisableUrlPreview(nil) + + + if let message = presentation.keyboardButtonsMessage, let replyMarkup = message.replyMarkup { + if replyMarkup.flags.contains(.setupReply) { + value = value.withUpdatedDismissedForceReplyId(message.id) + } + } + return value + }.updatedUrlPreview(nil).updateBotMenu({ current in + var current = current + current?.revealed = false + return current + }) + + }) self?.chatInteraction.saveState(scrollState: self?.immediateScrollState()) + if self?.genericView.doBackgroundAction() != true { + self?.navigationController?.doBackgroundAction() + } } chatInteraction.jumpToDate = { [weak self] date in - if let window = self?.window, let account = self?.account, let peerId = self?.peerId { - let signal = searchMessageIdByTimestamp(account: account, peerId: peerId, timestamp: Int32(date.timeIntervalSince1970) - Int32(NSTimeZone.local.secondsFromGMT())) |> mapToSignal { messageId -> Signal in - if let messageId = messageId { - return downloadMessage(account: account, messageId: messageId) + if let strongSelf = self, let window = self?.window, let peerId = self?.chatInteraction.peerId { + + + switch strongSelf.mode { + case .history, .replyThread: + let signal = context.engine.messages.searchMessageIdByTimestamp(peerId: peerId, threadId: strongSelf.mode.threadId64, timestamp: Int32(date.timeIntervalSince1970)) + + self?.dateDisposable.set(showModalProgress(signal: signal, for: window).start(next: { messageId in + if let messageId = messageId { + self?.chatInteraction.focusMessageId(nil, messageId, .top(id: 0, innerId: nil, animated: true, focus: .init(focus: false), inset: 30)) + } + })) + case .pinned, .preview: + break + case .scheduled: + var previousItem: ChatRowItem? + strongSelf.genericView.tableView.enumerateItems(with: { item -> Bool in + + if let item = item as? ChatDateStickItem { + var calendar = NSCalendar.current + + calendar.timeZone = TimeZone(abbreviation: "UTC")! + let date = Date(timeIntervalSince1970: TimeInterval(item.timestamp + 86400)) + let components = calendar.dateComponents([.year, .month, .day], from: date) + + if CalendarUtils.monthDay(components.day!, date: date) == date { + return false + } + } else if let item = item as? ChatRowItem { + previousItem = item + } + + return true + }) + + if let previousItem = previousItem { + self?.genericView.tableView.scroll(to: .top(id: previousItem.stableId, innerId: nil, animated: true, focus: .init(focus: false), inset: 30)) + } + } + + + } + } + + let editMessage:(ChatEditState, Date?)->Void = { [weak self] state, atDate in + guard let `self` = self else {return} + let presentation = self.chatInteraction.presentation + + let inputState = state.inputState.subInputState(from: NSMakeRange(0, state.inputState.inputText.length)) + + let text = inputState.inputText.trimmed + if text.length > presentation.maxInputCharacters { + alert(for: context.window, info: L10n.chatInputErrorMessageTooLongCountable(text.length - Int(presentation.maxInputCharacters))) + return + } + + self.urlPreviewQueryState?.1.dispose() + + + if atDate == nil { + self.context.account.pendingUpdateMessageManager.add(messageId: state.message.id, text: inputState.inputText, media: state.editMedia, entities: TextEntitiesMessageAttribute(entities: inputState.messageTextEntities()), disableUrlPreview: presentation.interfaceState.composeDisableUrlPreview != nil) + + self.chatInteraction.beginEditingMessage(nil) + self.chatInteraction.update({ + $0.updatedInterfaceState({ + $0.withUpdatedComposeDisableUrlPreview(nil).updatedEditState({ + $0?.withUpdatedLoadingState(.none) + }) + }) + }) + + } else { + let scheduleTime:Int32? = atDate != nil ? Int32(atDate!.timeIntervalSince1970) : nil + + self.chatInteraction.update({$0.updatedUrlPreview(nil).updatedInterfaceState({$0.updatedEditState({$0?.withUpdatedLoadingState(state.editMedia == .keep ? .loading : .progress(0.2))})})}) + + self.chatInteraction.editDisposable.set((context.engine.messages.requestEditMessage(messageId: state.message.id, text: inputState.inputText, media: state.editMedia, entities: TextEntitiesMessageAttribute(entities: inputState.messageTextEntities()), disableUrlPreview: presentation.interfaceState.composeDisableUrlPreview != nil, scheduleTime: scheduleTime) |> deliverOnMainQueue).start(next: { [weak self] progress in + guard let `self` = self else {return} + switch progress { + case let .progress(progress): + if state.editMedia != .keep { + self.chatInteraction.update({$0.updatedInterfaceState({$0.updatedEditState({$0?.withUpdatedLoadingState(.progress(max(progress, 0.2)))})})}) + } + default: + break + } + + }, completed: { [weak self] in + guard let `self` = self else {return} + self.chatInteraction.beginEditingMessage(nil) + self.chatInteraction.update({ + $0.updatedInterfaceState({ + $0.withUpdatedComposeDisableUrlPreview(nil).updatedEditState({ + $0?.withUpdatedLoadingState(.none) + }) + }) + }) + })) + } + + } + + chatInteraction.sendMessage = { [weak self] silent, atDate in + if let strongSelf = self { + let presentation = strongSelf.chatInteraction.presentation + let peerId = strongSelf.chatInteraction.peerId + let threadId = strongSelf.chatInteraction.mode.threadId + if presentation.abilityToSend { + func apply(_ controller: ChatController, atDate: Date?) { + var invokeSignal:Signal = .complete() + + var setNextToTransaction = false + if let state = presentation.interfaceState.editState { + editMessage(state, atDate) + return + } else if !presentation.effectiveInput.inputText.trimmed.isEmpty { + setNextToTransaction = true + invokeSignal = Sender.enqueue(input: presentation.effectiveInput, context: context, peerId: controller.chatInteraction.peerId, replyId: presentation.interfaceState.replyMessageId ?? threadId, disablePreview: presentation.interfaceState.composeDisableUrlPreview != nil, silent: silent, atDate: atDate, mediaPreview: presentation.urlPreview?.1, emptyHandler: { [weak strongSelf] in + _ = strongSelf?.nextTransaction.execute() + }) |> deliverOnMainQueue |> ignoreValues + + } + + let fwdIds: [MessageId] = presentation.interfaceState.forwardMessageIds + let hideNames = presentation.interfaceState.hideSendersName + if !fwdIds.isEmpty { + setNextToTransaction = true + + + let fwd = combineLatest(queue: .mainQueue(), context.account.postbox.messagesAtIds(fwdIds), context.account.postbox.loadedPeerWithId(peerId)) |> mapToSignal { messages, peer -> Signal<[MessageId?], NoError> in + let errors:[String] = messages.compactMap { message in + + for attr in message.attributes { + if let _ = attr as? InlineBotMessageAttribute, peer.hasBannedRights(.banSendInline) { + return permissionText(from: peer, for: .banSendInline) + } + } + + if let media = message.media.first { + switch media { + case _ as TelegramMediaPoll: + return permissionText(from: peer, for: .banSendPolls) + case _ as TelegramMediaImage: + return permissionText(from: peer, for: .banSendMedia) + case let file as TelegramMediaFile: + if file.isAnimated && file.isVideo { + return permissionText(from: peer, for: .banSendGifs) + } else if file.isStaticSticker { + return permissionText(from: peer, for: .banSendStickers) + } else { + return permissionText(from: peer, for: .banSendMedia) + } + case _ as TelegramMediaGame: + return permissionText(from: peer, for: .banSendGames) + default: + return nil + } + } + + return nil + } + + if !errors.isEmpty { + alert(for: context.window, info: errors.joined(separator: "\n\n")) + return .complete() + } + + return Sender.forwardMessages(messageIds: messages.map {$0.id}, context: context, peerId: peerId, hideNames: hideNames, silent: silent, atDate: atDate) + } + + invokeSignal = invokeSignal |> then(fwd |> ignoreValues) + + } + + _ = (invokeSignal |> deliverOnMainQueue).start(completed: scrollAfterSend) + + if setNextToTransaction { + if atDate != nil { + afterSentTransition() + } else { + controller.nextTransaction.set(handler: afterSentTransition) + } + } + } + + switch strongSelf.mode { + case .scheduled: + if let atDate = atDate { + apply(strongSelf, atDate: atDate) + } else if presentation.state != .editing, let peer = chatInteraction.peer { + DispatchQueue.main.async { + showModal(with: DateSelectorModalController(context: context, mode: .schedule(peer.id), selectedAt: { [weak strongSelf] date in + if let strongSelf = strongSelf { + apply(strongSelf, atDate: date) + } + }), for: context.window) + } + } else { + apply(strongSelf, atDate: nil) + } + case .history, .replyThread: + delay(0.1, closure: { + if atDate != nil { + strongSelf.openScheduledChat() + } + }) + apply(strongSelf, atDate: atDate) + case .pinned, .preview: + break + } + + } else { + if let editState = presentation.interfaceState.editState, editState.inputState.inputText.isEmpty { + if editState.message.media.isEmpty || editState.message.media.first is TelegramMediaWebpage { + strongSelf.chatInteraction.deleteMessages([editState.message.id]) + return + } + } + let actionView = strongSelf.genericView.inputView.currentActionView + if let slowMode = presentation.slowMode { + if let errorText = presentation.slowModeErrorText { + if let slowMode = presentation.slowMode, slowMode.timeout != nil { + showSlowModeTimeoutTooltip(slowMode, for: actionView) + } else { + tooltip(for: actionView, text: errorText) + } + if let last = slowMode.sendingIds.last { + strongSelf.chatInteraction.focusMessageId(nil, last, .CenterEmpty) + } else { + strongSelf.genericView.inputView.textView.shake() + } + } else { + strongSelf.genericView.inputView.textView.shake() + } + + } else { + strongSelf.genericView.inputView.textView.shake() + } + } + } + } + + chatInteraction.updateEditingMessageMedia = { [weak self] exts, asMedia in + guard let `self` = self else {return} + + filePanel(with: exts, allowMultiple: false, for: context.window, completion: { [weak self] files in + guard let `self` = self else {return} + if let file = files?.first { + self.updateMediaDisposable.set((Sender.generateMedia(for: MediaSenderContainer(path: file, isFile: !asMedia), account: context.account, isSecretRelated: peerId.namespace == Namespaces.Peer.SecretChat) |> deliverOnMainQueue).start(next: { [weak self] media, _ in + self?.chatInteraction.update({$0.updatedInterfaceState({$0.updatedEditState({$0?.withUpdatedMedia(media)})})}) + })) + } + }) + } + + + chatInteraction.addContact = { [weak self] in + if let peerId = self?.chatInteraction.presentation.mainPeer?.id { + showModal(with: NewContactController(context: context, peerId: peerId), for: context.window) + } + } + chatInteraction.blockContact = { [weak self] in + if let chatInteraction = self?.chatInteraction, let peer = chatInteraction.presentation.mainPeer { + if peer.isUser || peer.isBot { + let options: [ModalOptionSet] = [ModalOptionSet(title: L10n.blockContactOptionsReport, selected: true, editable: true), ModalOptionSet(title: L10n.blockContactOptionsDeleteChat, selected: true, editable: true)] + + showModal(with: ModalOptionSetController(context: chatInteraction.context, options: options, actionText: (L10n.blockContactOptionsAction(peer.compactDisplayTitle), theme.colors.redUI), desc: L10n.blockContactTitle(peer.compactDisplayTitle), title: L10n.blockContactOptionsTitle, result: { result in + + var signals:[Signal] = [] + + + signals.append(context.blockedPeersContext.add(peerId: peer.id) |> `catch` { _ in return .complete() }) + + if result[1] == .selected { + signals.append(context.engine.peers.removePeerChat(peerId: chatInteraction.peerId, reportChatSpam: result[0] == .selected) |> ignoreValues) + } else if result[0] == .selected { + + signals.append(context.engine.peers.reportPeer(peerId: peer.id) |> ignoreValues) + } + let closeChat = result[1] == .selected + + _ = showModalProgress(signal: combineLatest(signals), for: context.window).start(completed: { + if closeChat { + context.sharedContext.bindings.rootNavigation().back() + } + }) + + }), for: context.window) + } else { + chatInteraction.reportSpamAndClose() + } + + } + + } + + chatInteraction.unarchive = { + _ = updatePeerGroupIdInteractively(postbox: context.account.postbox, peerId: peerId, groupId: .root).start() + let removeFlagsSignal = context.account.postbox.transaction { transaction in + transaction.updatePeerCachedData(peerIds: [peerId], update: { peerId, cachedData in + if let cachedData = cachedData as? CachedUserData { + let current = cachedData.peerStatusSettings + var flags = current?.flags ?? [] + flags.remove(.autoArchived) + flags.remove(.canBlock) + flags.remove(.canReport) + return cachedData.withUpdatedPeerStatusSettings(PeerStatusSettings(flags: flags, geoDistance: current?.geoDistance)) + } + if let cachedData = cachedData as? CachedChannelData { + let current = cachedData.peerStatusSettings + var flags = current?.flags ?? [] + flags.remove(.autoArchived) + flags.remove(.canBlock) + flags.remove(.canReport) + return cachedData.withUpdatedPeerStatusSettings(PeerStatusSettings(flags: flags, geoDistance: current?.geoDistance)) + } + if let cachedData = cachedData as? CachedGroupData { + let current = cachedData.peerStatusSettings + var flags = current?.flags ?? [] + flags.remove(.autoArchived) + flags.remove(.canBlock) + flags.remove(.canReport) + return cachedData.withUpdatedPeerStatusSettings(PeerStatusSettings(flags: flags, geoDistance: current?.geoDistance)) + } + return cachedData + }) + } + let unmuteSignal = context.engine.peers.updatePeerMuteSetting(peerId: peerId, muteInterval: nil) + + _ = combineLatest(unmuteSignal, removeFlagsSignal).start() + } + + chatInteraction.sendPlainText = { [weak self] text in + if let strongSelf = self, let peer = self?.chatInteraction.presentation.peer, peer.canSendMessage(strongSelf.mode.isThreadMode) { + let _ = (Sender.enqueue(input: ChatTextInputState(inputText: text), context: context, peerId: strongSelf.chatInteraction.peerId, replyId: strongSelf.chatInteraction.presentation.interfaceState.replyMessageId) |> deliverOnMainQueue).start(completed: scrollAfterSend) + } + } + + chatInteraction.sendLocation = { [weak self] coordinate, venue in + let media = TelegramMediaMap(latitude: coordinate.latitude, longitude: coordinate.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: venue, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) + self?.chatInteraction.sendMedias([media], ChatTextInputState(), false, nil, false, nil) + } + + chatInteraction.scrollToLatest = { [weak self] removeStack in + if let strongSelf = self { + if removeStack { + strongSelf.historyState = strongSelf.historyState.withClearReplies() + } + strongSelf.scrollup() + } + } + + chatInteraction.reportMessages = { [weak self] value, ids in + showModal(with: ReportDetailsController(context: context, reason: value, updated: { [weak self] value in + _ = showModalProgress(signal: context.engine.peers.reportPeerMessages(messageIds: ids, reason: value.reason, message: value.comment), for: context.window).start(completed: { [weak self] in + showModalText(for: context.window, text: L10n.peerInfoChannelReported) + self?.changeState() + }) + }), for: context.window) + + } + + chatInteraction.forwardMessages = { [weak self] ids in + guard let strongSelf = self else { + return + } + if let report = strongSelf.chatInteraction.presentation.reportMode { + strongSelf.chatInteraction.reportMessages(report, ids) + return + } + showModal(with: ShareModalController(ForwardMessagesObject(context, messageIds: ids)), for: context.window) + } + + chatInteraction.deleteMessages = { [weak self] messageIds in + if let strongSelf = self, let peer = strongSelf.chatInteraction.peer { + + if strongSelf.chatInteraction.presentation.reportMode != nil { + strongSelf.changeState() + return + } + + let channelAdmin:Promise<[ChannelParticipant]?> = Promise() + + if peer.isSupergroup { + let disposable: MetaDisposable = MetaDisposable() + let result = context.peerChannelMemberCategoriesContextsManager.admins(peerId: peer.id, updated: { state in + switch state.loadingState { + case .ready: + channelAdmin.set(.single(state.list.map({$0.participant}))) + disposable.dispose() + default: + break + } + }) + disposable.set(result.0) + } else { + channelAdmin.set(.single(nil)) + } + + + self?.messagesActionDisposable.set(combineLatest(context.account.postbox.messagesAtIds(messageIds) |> deliverOnMainQueue, channelAdmin.get() |> deliverOnMainQueue).start( next:{ [weak strongSelf] messages, admins in + if let strongSelf = strongSelf, let peer = strongSelf.chatInteraction.peer { + var canDelete:Bool = true + var canDeleteForEveryone = true + var otherCounter:Int32 = 0 + let peerId = peer.id + var _mustDeleteForEveryoneMessage: Bool = true + for message in messages { + if !canDeleteMessage(message, account: context.account, mode: strongSelf.chatInteraction.mode) { + canDelete = false + } + if !mustDeleteForEveryoneMessage(message) { + _mustDeleteForEveryoneMessage = false + } + if !canDeleteForEveryoneMessage(message, context: context) { + canDeleteForEveryone = false + } else { + if message.effectiveAuthor?.id != context.peerId && !(context.limitConfiguration.canRemoveIncomingMessagesInPrivateChats && message.peers[message.id.peerId] is TelegramUser) { + if let peer = message.peers[message.id.peerId] as? TelegramGroup { + inner: switch peer.role { + case .member: + otherCounter += 1 + default: + break inner + } + } else { + otherCounter += 1 + } + } + } + } + + if otherCounter > 0 || peer.id == context.peerId { + canDeleteForEveryone = false + } + if messages.isEmpty { + strongSelf.chatInteraction.update({$0.withoutSelectionState()}) + return + } + + if canDelete { + if mustManageDeleteMessages(messages, for: peer, account: context.account), let memberId = messages[0].author?.id { + + var options:[ModalOptionSet] = [] + + options.append(ModalOptionSet(title: L10n.supergroupDeleteRestrictionDeleteMessage, selected: true, editable: true)) + + var hasRestrict: Bool = false + + if let channel = peer as? TelegramChannel { + if channel.hasPermission(.banMembers) { + options.append(ModalOptionSet(title: L10n.supergroupDeleteRestrictionBanUser, selected: false, editable: true)) + hasRestrict = true + } + } + options.append(ModalOptionSet(title: L10n.supergroupDeleteRestrictionReportSpam, selected: false, editable: true)) + options.append(ModalOptionSet(title: L10n.supergroupDeleteRestrictionDeleteAllMessages, selected: false, editable: true)) + + + + showModal(with: ModalOptionSetController(context: context, options: options, actionText: (L10n.modalOK, theme.colors.accent), title: L10n.supergroupDeleteRestrictionTitle, result: { [weak strongSelf] result in + + var signals:[Signal] = [] + + var index:Int = 0 + if result[index] == .selected { + signals.append(context.engine.messages.deleteMessagesInteractively(messageIds: messageIds, type: .forEveryone)) + } + index += 1 + + if hasRestrict { + if result[index] == .selected { + signals.append(context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(peerId: peerId, memberId: memberId, bannedRights: .init(flags: [.banReadMessages], untilDate: Int32.max))) + } + index += 1 + } + + if result[index] == .selected { + signals.append(context.engine.peers.reportPeerMessages(messageIds: messageIds, reason: .spam, message: "")) + } + index += 1 + + if result[index] == .selected { + signals.append(context.engine.messages.clearAuthorHistory(peerId: peerId, memberId: memberId)) + } + index += 1 + + _ = showModalProgress(signal: combineLatest(signals), for: context.window).start() + strongSelf?.chatInteraction.update({$0.withoutSelectionState()}) + }), for: context.window) + + } else if let `self` = self { + let thrid:String? = self.mode == .scheduled ? nil : (canDeleteForEveryone ? peer.isUser ? L10n.chatMessageDeleteForMeAndPerson(peer.compactDisplayTitle) : L10n.chatConfirmDeleteMessagesForEveryone : nil) + + modernConfirm(for: context.window, account: context.account, peerId: nil, header: thrid == nil ? L10n.chatConfirmActionUndonable : L10n.chatConfirmDeleteMessages1Countable(messages.count), information: thrid == nil ? _mustDeleteForEveryoneMessage ? L10n.chatConfirmDeleteForEveryoneCountable(messages.count) : L10n.chatConfirmDeleteMessages1Countable(messages.count) : nil, okTitle: L10n.confirmDelete, thridTitle: thrid, successHandler: { [weak strongSelf] result in + + guard let strongSelf = strongSelf else {return} + + let type:InteractiveMessagesDeletionType + switch result { + case .basic: + type = .forLocalPeer + case .thrid: + type = .forEveryone + } + if let editingState = strongSelf.chatInteraction.presentation.interfaceState.editState { + if messageIds.contains(editingState.message.id) { + strongSelf.chatInteraction.cancelEditing() + } + } + _ = context.engine.messages.deleteMessagesInteractively(messageIds: messageIds, type: type).start() + strongSelf.chatInteraction.update({$0.withoutSelectionState()}) + }) + } + } + } + })) + } + } + + chatInteraction.openInfo = { [weak self] (peerId, toChat, postId, action) in + if let strongSelf = self { + if toChat || action != nil { + + if peerId == strongSelf.chatInteraction.peerId { + if let postId = postId { + + var fromId: MessageId? = nil + if let action = action { + switch action { + case let .source(id): + fromId = id + default: + break + } + } + + strongSelf.chatInteraction.focusMessageId(fromId, postId, TableScrollState.CenterEmpty) + } + if let action = action { + strongSelf.chatInteraction.update({ $0.updatedInitialAction(action) }) + strongSelf.chatInteraction.invokeInitialAction() + } + } else { + strongSelf.navigationController?.push(ChatAdditionController(context: context, chatLocation: .peer(peerId), messageId: postId, initialAction: action)) + } + } else { + strongSelf.navigationController?.push(PeerInfoController(context: context, peerId: peerId)) + } + } + } + + chatInteraction.showNextPost = { [weak self] in + guard let `self` = self else {return} + if let bottomVisibleRow = self.genericView.tableView.bottomVisibleRow { + if bottomVisibleRow > 0 { + var item = self.genericView.tableView.item(at: bottomVisibleRow - 1) + if item.view?.visibleRect.height != item.view?.frame.height { + item = self.genericView.tableView.item(at: bottomVisibleRow) + } + self.genericView.tableView.scroll(to: .center(id: item.stableId, innerId: nil, animated: true, focus: .init(focus: true), inset: 0), inset: NSEdgeInsets(), true) + } + + } + } + + chatInteraction.openFeedInfo = { [weak self] groupId in + guard let `self` = self else {return} + self.navigationController?.push(ChatListController(context, groupId: groupId)) + } + + chatInteraction.openProxySettings = { [weak self] in + let controller = proxyListController(accountManager: context.sharedContext.accountManager, network: context.account.network, pushController: { [weak self] controller in + self?.navigationController?.push(controller) + }) + self?.navigationController?.push(controller) + } + + chatInteraction.inlineAudioPlayer = { [weak self] controller in + let object = InlineAudioPlayerView.ContextObject(controller: controller, context: context, tableView: self?.genericView.tableView, supportTableView: nil) + self?.navigationController?.header?.show(true, contextObject: object) + } + + + + chatInteraction.searchPeerMessages = { [weak self] peer in + guard let `self` = self else { return } + self.chatInteraction.update({$0.updatedSearchMode((false, nil, nil))}) + self.chatInteraction.update({$0.updatedSearchMode((true, peer, nil))}) + } + chatInteraction.movePeerToInput = { [weak self] (peer) in + if let strongSelf = self { + let textInputState = strongSelf.chatInteraction.presentation.effectiveInput + if let (range, _, _) = textInputStateContextQueryRangeAndType(textInputState, includeContext: false) { + let inputText = textInputState.inputText + + let name:String = peer.addressName ?? peer.compactDisplayTitle + + let distance = inputText.distance(from: range.lowerBound, to: range.upperBound) + let replacementText = name + " " + + let atLength = peer.addressName != nil ? 0 : 1 + + let range = strongSelf.chatInteraction.appendText(replacementText, selectedRange: textInputState.selectionRange.lowerBound - distance - atLength ..< textInputState.selectionRange.upperBound) + + if peer.addressName == nil { + let state = strongSelf.chatInteraction.presentation.effectiveInput + var attributes = state.attributes + attributes.append(.uid(range.lowerBound ..< range.upperBound - 1, peer.id.id._internalGetInt64Value())) + let updatedState = ChatTextInputState(inputText: state.inputText, selectionRange: state.selectionRange, attributes: attributes) + strongSelf.chatInteraction.update({$0.withUpdatedEffectiveInputState(updatedState)}) + } + } + } + } + + + chatInteraction.sendInlineResult = { [weak self] (results,result) in + if let strongSelf = self { + func apply(_ controller: ChatController, atDate: Int32?) { + let chatInteraction = controller.chatInteraction + + let value = context.engine.messages.enqueueOutgoingMessageWithChatContextResult(to: chatInteraction.peerId, results: results, result: result, replyToMessageId: chatInteraction.presentation.interfaceState.replyMessageId ?? chatInteraction.mode.threadId) + + if value { + controller.nextTransaction.set(handler: afterSentTransition) + } + + } + switch strongSelf.mode { + case .history, .replyThread: + apply(strongSelf, atDate: nil) + case .scheduled: + if let peer = strongSelf.chatInteraction.peer { + showModal(with: DateSelectorModalController(context: context, mode: .schedule(peer.id), selectedAt: { [weak strongSelf] date in + if let strongSelf = strongSelf { + apply(strongSelf, atDate: Int32(date.timeIntervalSince1970)) + } + }), for: context.window) + } + case .pinned, .preview: + break + } + + } + + } + + chatInteraction.beginEditingMessage = { [weak self] (message) in + if let message = message { + self?.chatInteraction.update({$0.withEditMessage(message)}) + } else { + self?.chatInteraction.cancelEditing(true) + } + self?.chatInteraction.focusInputField() + } + + chatInteraction.mentionPressed = { [weak self] in + if let strongSelf = self { + let signal = context.engine.messages.earliestUnseenPersonalMentionMessage(peerId: strongSelf.chatInteraction.peerId) + strongSelf.navigationActionDisposable.set((signal |> deliverOnMainQueue).start(next: { [weak strongSelf] result in + if let strongSelf = strongSelf { + switch result { + case .loading: + break + case .result(let messageId): + if let messageId = messageId { + strongSelf.chatInteraction.focusMessageId(nil, messageId, .CenterEmpty) + } + } + } + })) + } + } + + chatInteraction.clearMentions = { [weak self] in + guard let `self` = self else {return} + _ = clearPeerUnseenPersonalMessagesInteractively(account: context.account, peerId: self.chatInteraction.peerId).start() + } + + chatInteraction.editEditingMessagePhoto = { [weak self] media in + guard let `self` = self else {return} + if let resource = media.representationForDisplayAtSize(PixelDimensions(1280, 1280))?.resource { + _ = (context.account.postbox.mediaBox.resourceData(resource) |> deliverOnMainQueue).start(next: { [weak self] resource in + guard let `self` = self else {return} + let url = URL(fileURLWithPath: link(path:resource.path, ext:kMediaImageExt)!) + let controller = EditImageModalController(url, defaultData: self.chatInteraction.presentation.interfaceState.editState?.editedData) + self.editCurrentMessagePhotoDisposable.set((controller.result |> deliverOnMainQueue).start(next: { [weak self] (new, data) in + guard let `self` = self else {return} + self.chatInteraction.update({$0.updatedInterfaceState({$0.updatedEditState({$0?.withUpdatedEditedData(data)})})}) + if new != url { + self.updateMediaDisposable.set((Sender.generateMedia(for: MediaSenderContainer(path: new.path, isFile: false), account: context.account, isSecretRelated: peerId.namespace == Namespaces.Peer.SecretChat) |> deliverOnMainQueue).start(next: { [weak self] media, _ in + self?.chatInteraction.update({$0.updatedInterfaceState({$0.updatedEditState({$0?.withUpdatedMedia(media)})})}) + })) + } else { + self.chatInteraction.update({$0.updatedInterfaceState({$0.updatedEditState({$0?.withUpdatedMedia(media)})})}) + } + + })) + showModal(with: controller, for: context.window, animationType: .scaleCenter) + }) + } + } + + /* + + let header: String + let text: String + if peer.isChannel { + header = L10n.channelAdminTransferOwnershipConfirmChannelTitle + text = L10n.channelAdminTransferOwnershipConfirmChannelText(peer.displayTitle, admin.displayTitle) + } else { + header = L10n.channelAdminTransferOwnershipConfirmGroupTitle + text = L10n.channelAdminTransferOwnershipConfirmGroupText(peer.displayTitle, admin.displayTitle) + } + + + */ + + + chatInteraction.requestMessageActionCallback = { [weak self] messageId, isGame, data in + if let strongSelf = self { + switch strongSelf.mode { + case .history, .replyThread: + let applyResult:(MessageActionCallbackResult) -> Void = { [weak strongSelf] result in + if let strongSelf = strongSelf { + switch result { + case .none: + strongSelf.botCallbackAlertMessage.set(.single(("", false))) + case let .toast(text): + strongSelf.botCallbackAlertMessage.set(.single((text, false))) + case let .alert(text): + strongSelf.botCallbackAlertMessage.set(.single((text, true))) + case let .url(url): + if isGame { + strongSelf.navigationController?.push(WebGameViewController(context, strongSelf.chatInteraction.peerId, messageId, url)) + } else { + execute(inapp: .external(link: url, !(strongSelf.chatInteraction.peer?.isVerified ?? false))) + } + } + } + } + strongSelf.botCallbackAlertMessage.set(.single((L10n.chatInlineRequestLoading, false))) + strongSelf.messageActionCallbackDisposable.set((context.engine.messages.requestMessageActionCallback(messageId: messageId, isGame:isGame, password: nil, data: data?.data) |> deliverOnMainQueue).start(next: applyResult, error: { [weak strongSelf] error in + + strongSelf?.botCallbackAlertMessage.set(.single(("", false))) + if let data = data, data.requiresPassword { + var errorText: String? = nil + var install2Fa = false + switch error { + case .invalidPassword: + showModal(with: InputPasswordController(context: context, title: L10n.botTransferOwnershipPasswordTitle, desc: L10n.botTransferOwnershipPasswordDesc, checker: { pwd in + return context.engine.messages.requestMessageActionCallback(messageId: messageId, isGame: isGame, password: pwd, data: data.data) + |> deliverOnMainQueue + |> beforeNext { result in + applyResult(result) + } + |> ignoreValues + |> `catch` { error -> Signal in + switch error { + case .generic: + return .fail(.generic) + case .invalidPassword: + return .fail(.wrong) + default: + return .fail(.generic) + } + } + }), for: context.window) + case .authSessionTooFresh: + errorText = L10n.botTransferOwnerErrorText + case .twoStepAuthMissing: + errorText = L10n.botTransferOwnerErrorText + install2Fa = true + case .twoStepAuthTooFresh: + errorText = L10n.botTransferOwnerErrorText + default: + break + } + if let errorText = errorText { + confirm(for: context.window, header: L10n.botTransferOwnerErrorTitle, information: errorText, okTitle: L10n.modalOK, cancelTitle: L10n.modalCancel, thridTitle: install2Fa ? L10n.botTransferOwnerErrorEnable2FA : nil, successHandler: { result in + switch result { + case .basic: + break + case .thrid: + context.sharedContext.bindings.rootNavigation().push(twoStepVerificationUnlockController(context: context, mode: .access(nil), presentController: { (controller, isRoot, animated) in + let navigation = context.sharedContext.bindings.rootNavigation() + if isRoot { + navigation.removeUntil(ChatController.self) + } + if !animated { + navigation.stackInsert(controller, at: navigation.stackCount) + } else { + navigation.push(controller) + } + })) + } + }) + } + } + + })) + case .scheduled: + break + case .pinned, .preview: + break + } + + } + } + + chatInteraction.updateSearchRequest = { [weak self] state in + self?.searchState.set(state) + } + + chatInteraction.setLocation = { [weak self] location in + self?.setLocation(location) + } + + + chatInteraction.scrollToTheFirst = { [weak self] in + guard let strongSelf = self else { + return + } + + let scroll: ChatHistoryLocation = .Scroll(index: .lowerBound, anchorIndex: .lowerBound, sourceIndex: .lowerBound, scrollPosition: .up(true), count: 50, animated: true) + + let historyView = chatHistoryViewForLocation(scroll, context: context, chatLocation: strongSelf.chatLocation, fixedCombinedReadStates: nil, tagMask: strongSelf.mode.tagMask, additionalData: [], chatLocationContextHolder: strongSelf.chatLocationContextHolder) + + struct FindSearchMessage { + let message:Message? + let loaded:Bool + } + + let signal = historyView + |> mapToSignal { historyView -> Signal in + switch historyView { + case .Loading: + return .single(true) + case let .HistoryView(view, _, _, _): + if !view.holeLater, !view.isLoading { + return .single(false) + } + return .single(true) + } + } |> take(until: { index in + return SignalTakeAction(passthrough: !index, complete: !index) + }) + + strongSelf.chatInteraction.loadingMessage.set(.single(true) |> delay(0.2, queue: Queue.mainQueue())) + strongSelf.messageIndexDisposable.set(showModalProgress(signal: signal, for: context.window).start(next: { [weak strongSelf] _ in + strongSelf?.setLocation(scroll) + }, completed: { + + })) + } + + chatInteraction.openFocusedMedia = { [weak self] timemark in + if let messageId = self?.messageId { + self?.genericView.tableView.enumerateItems(with: { item in + if let item = item as? ChatMediaItem, item.message?.id == messageId { + item.openMedia(timemark) + return false + } + return true + }) + } + } + + chatInteraction.focusPinnedMessageId = { [weak self] messageId in + self?.chatInteraction.focusMessageId(nil, messageId, .CenterActionEmpty { [weak self] _ in + self?.chatInteraction.update({$0.withUpdatedTempPinnedMaxId(messageId)}) + }) + } + + chatInteraction.runEmojiScreenEffect = { [weak self] emoji, messageId, mirror, isIncoming in + guard let strongSelf = self else { + return + } + strongSelf.emojiEffects.addAnimation(emoji.fixed, index: nil, mirror: mirror, isIncoming: isIncoming, messageId: messageId, animationSize: NSMakeSize(350, 350), viewFrame: context.window.bounds, for: context.window.contentView!) + } + + chatInteraction.focusMessageId = { [weak self] fromId, toId, state in + + if let strongSelf = self { + switch strongSelf.mode { + case let .replyThread(data, mode): + if mode.originId == toId { + let controller = strongSelf.navigationController?.previousController as? ChatController + if let controller = controller, case .peer(mode.originId.peerId) = controller.chatLocation { + strongSelf.navigationController?.back() + controller.chatInteraction.focusMessageId(fromId, mode.originId, state) + } else { + strongSelf.navigationController?.push(ChatAdditionController(context: strongSelf.context, chatLocation: .peer(toId.peerId), mode: .history, messageId: toId, initialAction: nil)) + } + return + } else if toId.peerId != peerId { + strongSelf.navigationController?.push(ChatAdditionController(context: strongSelf.context, chatLocation: .peer(toId.peerId), mode: .history, messageId: toId, initialAction: nil)) + } + default: + break + } + + switch strongSelf.mode { + case .history, .replyThread: + if let fromId = fromId { + strongSelf.historyState = strongSelf.historyState.withAddingReply(fromId) + } + + var fromIndex: MessageIndex? + + if let fromId = fromId, let message = strongSelf.messageInCurrentHistoryView(fromId) { + fromIndex = MessageIndex(message) + } else { + if let message = strongSelf.anchorMessageInCurrentHistoryView() { + fromIndex = MessageIndex(message) + } + } + if let fromIndex = fromIndex { + let historyView = chatHistoryViewForLocation(.InitialSearch(location: .id(toId), count: strongSelf.requestCount), context: context, chatLocation: strongSelf.chatLocation, fixedCombinedReadStates: nil, tagMask: strongSelf.mode.tagMask, additionalData: [], chatLocationContextHolder: strongSelf.chatLocationContextHolder) + + struct FindSearchMessage { + let message:Message? + let loaded:Bool + } + + let signal = historyView + |> mapToSignal { historyView -> Signal<(Message?, Bool), NoError> in + switch historyView { + case .Loading: + return .single((nil, true)) + case let .HistoryView(view, _, _, _): + for entry in view.entries { + if entry.message.id == toId { + return .single((entry.message, false)) + } + } + return .single((nil, false)) + } + } |> take(until: { index in + return SignalTakeAction(passthrough: index.0 != nil, complete: !index.1) + }) |> map { $0.0 } + + strongSelf.chatInteraction.loadingMessage.set(.single(true) |> delay(0.2, queue: Queue.mainQueue())) + strongSelf.messageIndexDisposable.set(showModalProgress(signal: signal, for: context.window).start(next: { [weak strongSelf] message in + self?.chatInteraction.loadingMessage.set(.single(false)) + if let strongSelf = strongSelf, let message = message { + let message = message + let toIndex = MessageIndex(message) + let requestCount = strongSelf.requestCount + delay(0.15, closure: { [weak strongSelf] in + strongSelf?.setLocation(.Scroll(index: .message(toIndex), anchorIndex: .message(toIndex), sourceIndex: .message(fromIndex), scrollPosition: state.swap(to: ChatHistoryEntryId.message(message)), count: requestCount, animated: state.animated)) + }) + } + })) + // } } - return .single(nil) + case .scheduled: + strongSelf.navigationController?.back() + (strongSelf.navigationController?.controller as? ChatController)?.chatInteraction.focusMessageId(fromId, toId, state) + case .pinned, .preview: + break } - - self?.dateDisposable.set(showModalProgress(signal: signal, for: window).start(next: { message in - if let message = message { - self?.chatInteraction.focusMessageId(nil, message.id, .top(id: 0, animated: true, focus: false, inset: 50)) - } - })) } + } - - chatInteraction.sendMessage = { [weak self] in - if let strongSelf = self { - let presentation = strongSelf.chatInteraction.presentation - if presentation.abilityToSend { - var setNextToTransaction = false - if let state = presentation.editState { - let inputState = state.inputState.subInputState(from: NSMakeRange(0, state.inputState.inputText.length)) - _ = (requestEditMessage(account: strongSelf.account, messageId:state.message.id, text: inputState.inputText, entities: TextEntitiesMessageAttribute(entities: inputState.messageTextEntities), disableUrlPreview: presentation.interfaceState.composeDisableUrlPreview != nil) |> deliverOnMainQueue).start(completed: { [weak self] in - self?.chatInteraction.update({$0.updatedInterfaceState({$0.withUpdatedComposeDisableUrlPreview(nil)})}) + chatInteraction.vote = { [weak self] messageId, opaqueIdentifiers, submit in + guard let `self` = self else {return} + + self.updatePoll { data -> [MessageId : ChatPollStateData] in + var data = data + data[messageId] = ChatPollStateData(identifiers: opaqueIdentifiers, isLoading: submit && !opaqueIdentifiers.isEmpty) + return data + } + + let signal:Signal - }) - strongSelf.chatInteraction.beginEditingMessage(nil) - } else if !presentation.effectiveInput.inputText.isEmpty { - setNextToTransaction = true - let _ = (Sender.enqueue(input: presentation.effectiveInput, account: strongSelf.account, peerId: strongSelf.peerId, replyId: presentation.interfaceState.replyMessageId, disablePreview: presentation.interfaceState.composeDisableUrlPreview != nil) |> deliverOnMainQueue).start(completed: scrollAfterSend) + if submit { + if opaqueIdentifiers.isEmpty { + signal = showModalProgress(signal: (context.engine.messages.requestMessageSelectPollOption(messageId: messageId, opaqueIdentifiers: []) |> deliverOnMainQueue), for: context.window) + } else { + signal = (context.engine.messages.requestMessageSelectPollOption(messageId: messageId, opaqueIdentifiers: opaqueIdentifiers) |> deliverOnMainQueue) + } + + self.selectMessagePollOptionDisposables.set(signal.start(next: { [weak self] poll in + if let poll = poll { + self?.updatePoll { data -> [MessageId : ChatPollStateData] in + var data = data + data.removeValue(forKey: messageId) + return data + } + var once: Bool = true + self?.afterNextTransaction = { [weak self] in + if let tableView = self?.genericView.tableView, once { + tableView.enumerateItems(with: { item -> Bool in + if let item = item as? ChatRowItem, let message = item.message, message.id == messageId, let `self` = self { + + if message.id == self.mode.threadId { + let entry = item.entry.withUpdatedMessageMedia(poll) + let size = self.atomicSize.with { $0 } + let updatedItem = ChatRowItem.item(size, from: entry, interaction: self.chatInteraction, theme: theme) + + _ = updatedItem.makeSize(size.width, oldWidth: 0) + + tableView.merge(with: .init(deleted: [], inserted: [], updated: [(item.index, updatedItem)], animated: true)) + + delay(0.25, closure: { [weak self] in + if let location = self?._locationValue.with({$0}) { + self?.setLocation(location) + } + }) + } + + let view = item.view as? ChatPollItemView + if let view = view, view.window != nil, view.visibleRect != .zero { + view.doAfterAnswer() + NSHapticFeedbackManager.defaultPerformer.perform(.alignment, performanceTime: .drawCompleted) + } + return false + } + return true + }) + once = false + } + } + + if opaqueIdentifiers.isEmpty { + self?.afterNextTransaction?() + } } - - if !presentation.interfaceState.forwardMessageIds.isEmpty { - setNextToTransaction = true - let _ = (Sender.forwardMessages(messageIds:presentation.interfaceState.forwardMessageIds,account: strongSelf.account,peerId: strongSelf.peerId) |> deliverOnMainQueue).start(completed: scrollAfterSend) + + }, error: { [weak self] error in + switch error { + case .generic: + alert(for: context.window, info: L10n.unknownError) + } + self?.updatePoll { data -> [MessageId : ChatPollStateData] in + var data = data + data.removeValue(forKey: messageId) + return data } - if setNextToTransaction { - strongSelf.nextTransaction.set(handler: afterSentTransition) + }), forKey: messageId) + } + + } + chatInteraction.closePoll = { [weak self] messageId in + guard let `self` = self else {return} + self.selectMessagePollOptionDisposables.set(context.engine.messages.requestClosePoll(messageId: messageId).start(), forKey: messageId) + } + + + chatInteraction.sendMedia = { [weak self] media in + if let strongSelf = self, let peer = strongSelf.chatInteraction.peer, peer.canSendMessage(strongSelf.mode.isThreadMode) { + + switch strongSelf.mode { + case .scheduled: + showModal(with: DateSelectorModalController(context: strongSelf.context, mode: .schedule(peer.id), selectedAt: { [weak strongSelf] date in + if let strongSelf = strongSelf { + let _ = (Sender.enqueue(media: media, context: context, peerId: strongSelf.chatInteraction.peerId, chatInteraction: strongSelf.chatInteraction, atDate: date) |> deliverOnMainQueue).start(completed: scrollAfterSend) + strongSelf.nextTransaction.set(handler: {}) + } + }), for: strongSelf.context.window) + case .history, .replyThread: + let _ = (Sender.enqueue(media: media, context: context, peerId: strongSelf.chatInteraction.peerId, chatInteraction: strongSelf.chatInteraction) |> deliverOnMainQueue).start(completed: scrollAfterSend) + strongSelf.nextTransaction.set(handler: {}) + case .pinned, .preview: + break + } + } + } + + chatInteraction.attachFile = { [weak self] asMedia in + if let `self` = self, let window = self.window { + if let slowMode = self.chatInteraction.presentation.slowMode, let errorText = slowMode.errorText { + tooltip(for: self.genericView.inputView.attachView, text: errorText) + if let last = slowMode.sendingIds.last { + self.chatInteraction.focusMessageId(nil, last, .CenterEmpty) } } else { - NSSound.beep() + filePanel(canChooseDirectories: true, for: window, completion:{ result in + if let result = result { + + let previous = result.count + + let result = result.filter { path -> Bool in + if let size = fs(path) { + return size <= 2000 * 1024 * 1024 + } + return false + } + + let afterSizeCheck = result.count + + if afterSizeCheck == 0 && previous != afterSizeCheck { + alert(for: context.window, info: L10n.appMaxFileSize1) + } else { + self.chatInteraction.showPreviewSender(result.map{URL(fileURLWithPath: $0)}, asMedia, nil) + } + + } + }) } } + } - - chatInteraction.forceSendMessage = { [weak self] (message) in - if let strongSelf = self, let peer = self?.chatInteraction.presentation.peer, peer.canSendMessage { - let _ = (Sender.enqueue(input: ChatTextInputState(inputText: message), account: strongSelf.account, peerId: strongSelf.peerId, replyId: strongSelf.chatInteraction.presentation.interfaceState.replyMessageId) |> deliverOnMainQueue).start(completed: scrollAfterSend) + chatInteraction.attachPhotoOrVideo = { [weak self] in + if let `self` = self, let window = self.window { + if let slowMode = self.chatInteraction.presentation.slowMode, let errorText = slowMode.errorText { + tooltip(for: self.genericView.inputView.attachView, text: errorText) + if let last = slowMode.sendingIds.last { + self.chatInteraction.focusMessageId(nil, last, .CenterEmpty) + } + } else { + filePanel(with: mediaExts, canChooseDirectories: true, for: window, completion:{ [weak self] result in + if let result = result { + let previous = result.count + + let result = result.filter { path -> Bool in + if let size = fs(path) { + return size <= 2000 * 1024 * 1024 + } + return false + } + + let afterSizeCheck = result.count + + if afterSizeCheck == 0 && previous != afterSizeCheck { + alert(for: context.window, info: L10n.appMaxFileSize1) + } else { + self?.chatInteraction.showPreviewSender(result.map{URL(fileURLWithPath: $0)}, true, nil) + } + } + }) + } + } + } + chatInteraction.attachPicture = { [weak self] in + guard let `self` = self else {return} + if let window = self.window { + pickImage(for: window, completion: { [weak self] image in + if let image = image { + self?.chatInteraction.mediaPromise.set(putToTemp(image: image) |> map({[MediaSenderContainer(path:$0)]})) + } + }) } } + chatInteraction.attachLocation = { [weak self] in + guard let `self` = self else {return} + showModal(with: LocationModalController(self.chatInteraction), for: context.window) + } - chatInteraction.scrollToLatest = { [weak self] removeStack in - if let strongSelf = self { - if removeStack { - strongSelf.historyState = strongSelf.historyState.withClearReplies() + chatInteraction.sendAppFile = { [weak self] file, silent, query in + if let strongSelf = self, let peer = strongSelf.chatInteraction.peer, peer.canSendMessage(strongSelf.mode.isThreadMode) { + func apply(_ controller: ChatController, atDate: Date?) { + let _ = (Sender.enqueue(media: file, context: context, peerId: controller.chatInteraction.peerId, chatInteraction: controller.chatInteraction, silent: silent, atDate: atDate, query: query) |> deliverOnMainQueue).start(completed: scrollAfterSend) + controller.nextTransaction.set(handler: {}) + } + switch strongSelf.mode { + case .scheduled: + showModal(with: DateSelectorModalController(context: context, mode: .schedule(peer.id), selectedAt: { [weak strongSelf] date in + if let controller = strongSelf { + apply(controller, atDate: date) + } + }), for: context.window) + default: + apply(strongSelf, atDate: nil) } - strongSelf.scrollup() } } - chatInteraction.forwardMessages = { [weak self] forwardMessages in - if let strongSelf = self, let navigation = strongSelf.navigationController { - - strongSelf.loadFwdMessagesDisposable.set((strongSelf.account.postbox.messagesAtIds(forwardMessages) |> deliverOnMainQueue).start(next: { [weak strongSelf] messages in - if let strongSelf = strongSelf { - - let displayName:String = strongSelf.peer?.compactDisplayTitle ?? "Unknown" - let action = FWDNavigationAction(messages: messages, displayName: displayName) - navigation.set(modalAction: action, strongSelf.account.context.layout != .single) - - if strongSelf.account.context.layout == .single { - navigation.push(ForwardChatListController(strongSelf.account)) - } - - action.afterInvoke = { [weak strongSelf] in - strongSelf?.chatInteraction.update(animated: false, {$0.withoutSelectionState()}) - strongSelf?.chatInteraction.saveState(scrollState: strongSelf?.immediateScrollState()) + chatInteraction.sendMedias = { [weak self] medias, caption, isCollage, additionText, silent, atDate in + if let strongSelf = self, let peer = strongSelf.chatInteraction.peer, peer.canSendMessage(strongSelf.mode.isThreadMode) { + func apply(_ controller: ChatController, atDate: Date?) { + let _ = (Sender.enqueue(media: medias, caption: caption, context: context, peerId: controller.chatInteraction.peerId, chatInteraction: controller.chatInteraction, isCollage: isCollage, additionText: additionText, silent: silent, atDate: atDate) |> deliverOnMainQueue).start(completed: scrollAfterSend) + controller.nextTransaction.set(handler: {}) + } + switch strongSelf.mode { + case .history, .replyThread: + DispatchQueue.main.async { [weak strongSelf] in + if let _ = atDate { + strongSelf?.openScheduledChat() } - + } + apply(strongSelf, atDate: atDate) + case .scheduled: + if let atDate = atDate { + apply(strongSelf, atDate: atDate) + } else { + showModal(with: DateSelectorModalController(context: context, mode: .schedule(peer.id), selectedAt: { [weak strongSelf] date in + if let strongSelf = strongSelf { + apply(strongSelf, atDate: date) + } + }), for: context.window) + } + case .pinned, .preview: + break + } + } + } + + chatInteraction.shareSelfContact = { [weak self] replyId in + if let strongSelf = self, let peer = strongSelf.chatInteraction.peer, peer.canSendMessage(strongSelf.mode.isThreadMode) { + strongSelf.shareContactDisposable.set((context.account.viewTracker.peerView(context.account.peerId) |> take(1)).start(next: { [weak strongSelf] peerView in + if let strongSelf = strongSelf, let peer = peerViewMainPeer(peerView) as? TelegramUser { + _ = Sender.enqueue(message: EnqueueMessage.message(text: "", attributes: [], mediaReference: AnyMediaReference.standalone(media: TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: peer.phone ?? "", peerId: peer.id, vCardData: nil)), replyToMessageId: replyId, localGroupingKey: nil, correlationId: nil), context: context, peerId: strongSelf.chatInteraction.peerId).start() } })) } } - chatInteraction.deleteMessages = { [weak self] messageIds in - if let strongSelf = self, let peer = strongSelf.peer { - let channelAdmin:Signal<[ChannelParticipant]?, Void> = peer.isSupergroup ? channelAdmins(account: strongSelf.account, peerId: strongSelf.peerId) - |> mapError {_ in return} |> map { admins -> [ChannelParticipant]? in - return admins.map({$0.participant}) - } : .single(nil) - - - self?.messagesActionDisposable.set(combineLatest(strongSelf.account.postbox.messagesAtIds(messageIds) |> deliverOnMainQueue, channelAdmin |> deliverOnMainQueue).start( next:{ [weak strongSelf] messages, admins in - if let strongSelf = strongSelf, let peer = strongSelf.peer { - var canDelete:Bool = true - var canDeleteForEveryone = true - - for message in messages { - if !canDeleteMessage(message, account: strongSelf.account) { - canDelete = false - } - if !canDeleteForEveryoneMessage(message, account: strongSelf.account) { - canDeleteForEveryone = false - } - } - - if canDelete { - let isAdmin = admins?.filter({$0.peerId == messages[0].author?.id}).first != nil - if mustManageDeleteMessages(messages, for: peer, account: strongSelf.account), let memberId = messages[0].author?.id, !isAdmin { - showModal(with: DeleteSupergroupMessagesModalController(account: strongSelf.account, messageIds: messages.map {$0.id}, peerId: peer.id, memberId: memberId, onComplete: { [weak strongSelf] in - strongSelf?.chatInteraction.update({$0.withoutSelectionState()}) - }), for: mainWindow) - } else { - let thrid:String? = canDeleteForEveryone ? tr(.chatConfirmDeleteMessagesForEveryone) : nil - - if let window = self?.window { - confirm(for: window, with: tr(.chatConfirmActionUndonable), and: tr(.chatConfirmDeleteMessages), thridTitle:thrid, successHandler: { result in - let type:InteractiveMessagesDeletionType - switch result { - case .basic: - type = .forLocalPeer - case .thrid: - type = .forEveryone - } - _ = deleteMessagesInteractively(postbox: strongSelf.account.postbox, messageIds: messageIds, type: type).start() - strongSelf.chatInteraction.update({$0.withoutSelectionState()}) - }) - } + chatInteraction.modalSearch = { [weak self] query in + if let strongSelf = self { + strongSelf.chatInteraction.update({$0.updatedSearchMode((true, nil, query))}) + +// let apply = showModalProgress(signal: searchMessages(account: context.account, location: .peer(peerId: strongSelf.chatInteraction.peerId, fromId: nil, tags: nil), query: query, state: nil), for: context.window) +// showModal(with: SearchResultModalController(context, request: apply |> map {$0.0.messages}, query: query, chatInteraction:strongSelf.chatInteraction), for: context.window) + } + } + + chatInteraction.sendCommand = { [weak self] command in + if let strongSelf = self, let peer = strongSelf.chatInteraction.peer, peer.canSendMessage(strongSelf.mode.isThreadMode) { + func apply(_ controller: ChatController, atDate: Date?) { + var commandText = "/" + command.command.text + if controller.chatInteraction.peerId.namespace != Namespaces.Peer.CloudUser { + commandText += "@" + (command.peer.username ?? "") + } + _ = Sender.enqueue(input: ChatTextInputState(inputText: commandText), context: context, peerId: controller.chatLocation.peerId, replyId: controller.chatInteraction.presentation.interfaceState.replyMessageId, atDate: atDate).start(completed: scrollAfterSend) + controller.chatInteraction.updateInput(with: "") + controller.nextTransaction.set(handler: afterSentTransition) + } + switch strongSelf.mode { + case .scheduled: + DispatchQueue.main.async { + showModal(with: DateSelectorModalController(context: context, mode: .schedule(peer.id), selectedAt: { [weak strongSelf] date in + if let strongSelf = strongSelf { + apply(strongSelf, atDate: date) } - } + }), for: context.window) } - })) + case .history, .replyThread: + apply(strongSelf, atDate: nil) + case .pinned, .preview: + break + } } } - chatInteraction.openInfo = { [weak self] (peerId, toChat, postId, action) in + chatInteraction.switchInlinePeer = { [weak self] switchId, initialAction in if let strongSelf = self { - if toChat { - strongSelf.navigationController?.push(ChatAdditionController(account: strongSelf.account, peerId: peerId, messageId: postId, initialAction: action)) + strongSelf.navigationController?.push(ChatSwitchInlineController(context: context, peerId: switchId, fallbackId:strongSelf.chatInteraction.peerId, fallbackMode: strongSelf.mode, initialAction: initialAction)) + } + } + + chatInteraction.setNavigationAction = { [weak self] action in + self?.navigationController?.set(modalAction: action) + } + + chatInteraction.showPreviewSender = { [weak self] urls, asMedia, attributedString in + if let `self` = self { + if let slowMode = self.chatInteraction.presentation.slowMode, let errorText = slowMode.errorText { + tooltip(for: self.genericView.inputView.attachView, text: errorText) + if !slowMode.sendingIds.isEmpty { + self.chatInteraction.focusMessageId(nil, slowMode.sendingIds.last!, .CenterEmpty) + } } else { - strongSelf.openPeerInfoDisposable.set((strongSelf.account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue).start(next: { [weak strongSelf] peer in - if let strongSelf = strongSelf { - strongSelf.navigationController?.push(PeerInfoController(account: strongSelf.account, peer: peer)) + var updated:[URL] = [] + for url in urls { + if url.path.contains("/T/TemporaryItems/") { + let newUrl = URL(fileURLWithPath: NSTemporaryDirectory() + url.path.nsstring.lastPathComponent) + try? FileManager.default.moveItem(at: url, to: newUrl) + if FileManager.default.fileExists(atPath: newUrl.path) { + updated.append(newUrl) + } + } else { + if FileManager.default.fileExists(atPath: url.path) { + updated.append(url) + } } - })) + } + if !updated.isEmpty { + if let _ = self.chatInteraction.presentation.interfaceState.editState { + alert(for: context.window, info: L10n.chatEditAttachError) + } else { + showModal(with: PreviewSenderController(urls: updated, chatInteraction: self.chatInteraction, asMedia: asMedia, attributedString: attributedString), for: context.window) + } + } } } } - chatInteraction.inlineAudioPlayer = { [weak self] controller in - if let navigation = self?.navigationController { - if let header = navigation.header, let strongSelf = self { - header.show(true) - if let view = header.view as? InlineAudioPlayerView { - view.update(with: controller, tableView: strongSelf.genericView.tableView) + chatInteraction.setChatMessageAutoremoveTimeout = { [weak self] seconds in + guard let strongSelf = self else { + return + } + if let peer = strongSelf.chatInteraction.peer, peer.canSendMessage(strongSelf.mode.isThreadMode) { + _ = context.engine.peers.setChatMessageAutoremoveTimeoutInteractively(peerId: peer.id, timeout: seconds).start() + } + scrollAfterSend() + } + + chatInteraction.showDeleterSetup = { [weak self] control in + guard let strongSelf = self else { + return + } + if let peer = strongSelf.chatInteraction.peer { + if !peer.canManageDestructTimer { + if let timeout = strongSelf.chatInteraction.presentation.messageSecretTimeout?.timeout?.effectiveValue { + switch timeout { + case .secondsInDay: + tooltip(for: control, text: L10n.chatInputAutoDelete1Day) + case .secondsInWeek: + tooltip(for: control, text: L10n.chatInputAutoDelete7Days) + default: + break + } } + } else { + + showModal(with: AutoremoveMessagesController(context: context, peer: peer, onlyDelete: true), for: context.window) } } } - chatInteraction.movePeerToInput = { [weak self] (peer) in + chatInteraction.toggleNotifications = { [weak self] isMuted in if let strongSelf = self { - let textInputState = strongSelf.chatInteraction.presentation.effectiveInput - if let (range, _, _) = textInputStateContextQueryRangeAndType(textInputState, includeContext: false) { - let inputText = textInputState.inputText - - let name:String = peer.addressName ?? peer.compactDisplayTitle - - let distance = inputText.distance(from: range.lowerBound, to: range.upperBound) - let replacementText = name + " " + if isMuted == nil || isMuted == true { + _ = context.engine.peers.togglePeerMuted(peerId: strongSelf.chatInteraction.peerId).start() + } else { + var options:[ModalOptionSet] = [] - let atLength = peer.addressName != nil ? 0 : 1 + options.append(ModalOptionSet(title: L10n.chatListMute1Hour, selected: false, editable: true)) + options.append(ModalOptionSet(title: L10n.chatListMute4Hours, selected: false, editable: true)) + options.append(ModalOptionSet(title: L10n.chatListMute8Hours, selected: false, editable: true)) + options.append(ModalOptionSet(title: L10n.chatListMute1Day, selected: false, editable: true)) + options.append(ModalOptionSet(title: L10n.chatListMute3Days, selected: false, editable: true)) + options.append(ModalOptionSet(title: L10n.chatListMuteForever, selected: true, editable: true)) - let range = strongSelf.chatInteraction.appendText(replacementText, selectedRange: textInputState.selectionRange.lowerBound - distance - atLength ..< textInputState.selectionRange.upperBound) + var intervals:[Int32] = [60 * 60, 60 * 60 * 4, 60 * 60 * 8, 60 * 60 * 24, 60 * 60 * 24 * 3, Int32.max] - if peer.addressName == nil { - let state = strongSelf.chatInteraction.presentation.effectiveInput - var attributes = state.attributes - attributes.append(.uid(range.lowerBound ..< range.upperBound - 1, peer.id.id)) - let updatedState = ChatTextInputState(inputText: state.inputText, selectionRange: state.selectionRange, attributes: attributes) - strongSelf.chatInteraction.update({$0.withUpdatedEffectiveInputState(updatedState)}) - } + showModal(with: ModalOptionSetController(context: context, options: options, selectOne: true, actionText: (L10n.chatInputMute, theme.colors.accent), title: L10n.peerInfoNotifications, result: { result in + + for (i, option) in result.enumerated() { + inner: switch option { + case .selected: + _ = context.engine.peers.updatePeerMuteSetting(peerId: strongSelf.chatInteraction.peerId, muteInterval: intervals[i]).start() + break + default: + break inner + } + } + + }), for: context.window) } } } + chatInteraction.openDiscussion = { [weak self] in + guard let `self` = self else { return } + let signal = showModalProgress(signal: context.account.viewTracker.peerView(self.chatLocation.peerId) |> filter { $0.cachedData is CachedChannelData } |> map { $0.cachedData as! CachedChannelData } |> take(1) |> deliverOnMainQueue, for: context.window) + self.discussionDataLoadDisposable.set(signal.start(next: { [weak self] cachedData in + if let linkedDiscussionPeerId = cachedData.linkedDiscussionPeerId.peerId { + self?.chatInteraction.openInfo(linkedDiscussionPeerId, true, nil, nil) + } + })) + } - chatInteraction.sendInlineResult = { [weak self] (results,result) in + chatInteraction.removeAndCloseChat = { [weak self] in + if let strongSelf = self, let window = strongSelf.window { + _ = showModalProgress(signal: context.engine.peers.removePeerChat(peerId: strongSelf.chatInteraction.peerId, reportChatSpam: false), for: window).start(next: { [weak strongSelf] in + strongSelf?.navigationController?.close() + }) + } + } + + chatInteraction.removeChatInteractively = { [weak self] in if let strongSelf = self { - if let message = outgoingMessageWithChatContextResult(results, result) { - _ = (Sender.enqueue(message: message.withUpdatedReplyToMessageId(strongSelf.chatInteraction.presentation.interfaceState.replyMessageId), account: strongSelf.account, peerId: strongSelf.peerId) |> deliverOnMainQueue).start(completed: scrollAfterSend) - strongSelf.nextTransaction.set(handler: afterSentTransition) - } + let signal = removeChatInteractively(context: context, peerId: strongSelf.chatInteraction.peerId, userId: strongSelf.chatInteraction.peer?.id) |> filter {$0} |> mapToSignal { _ -> Signal in + return context.globalPeerHandler.get() |> take(1) + } |> deliverOnMainQueue + + strongSelf.deleteChatDisposable.set(signal.start(next: { [weak strongSelf] location in + if location == strongSelf?.chatInteraction.chatLocation { + strongSelf?.context.sharedContext.bindings.rootNavigation().close() + } + })) } - } - chatInteraction.beginEditingMessage = { [weak self] (message) in - if let message = message { - self?.chatInteraction.update({$0.withEditMessage(message)}) - } else { - self?.chatInteraction.update({$0.withoutEditMessage()}) + chatInteraction.joinChannel = { [weak self] in + if let strongSelf = self, let window = strongSelf.window { + _ = showModalProgress(signal: context.engine.peers.joinChannel(peerId: strongSelf.chatInteraction.peerId, hash: nil) |> deliverOnMainQueue, for: window).start(error: { error in + let text: String + switch error { + case .generic: + text = L10n.unknownError + case .tooMuchJoined: + showInactiveChannels(context: context, source: .join) + return + case .tooMuchUsers: + text = L10n.groupUsersTooMuchError + } + alert(for: context.window, info: text) + }) } } - chatInteraction.mentionPressed = { [weak self] in - if let strongSelf = self { - let signal = earliestUnseenPersonalMentionMessage(postbox: strongSelf.account.postbox, network: strongSelf.account.network, peerId: strongSelf.peerId) - strongSelf.navigationActionDisposable.set((signal |> deliverOnMainQueue).start(next: { [weak strongSelf] result in - if let strongSelf = strongSelf { + chatInteraction.joinGroupCall = { [weak self] activeCall, joinHash in + let groupCall = self?.chatInteraction.presentation.groupCall + var currentActiveCall = groupCall?.activeCall + var activeCall: CachedChannelData.ActiveCall? = activeCall + if currentActiveCall == nil { + activeCall = nil + } + if activeCall != currentActiveCall { + currentActiveCall = activeCall + } + if let activeCall = currentActiveCall { + let join:(PeerId, Date?)->Void = { joinAs, _ in + _ = showModalProgress(signal: requestOrJoinGroupCall(context: context, peerId: peerId, joinAs: joinAs, initialCall: activeCall, initialInfo: groupCall?.data?.info, joinHash: joinHash), for: context.window).start(next: { result in switch result { - case .loading: - break - case .result(let messageId): - if let messageId = messageId { - strongSelf.chatInteraction.focusMessageId(nil, messageId, .center(id: 0, animated: true, focus: true, inset: 0)) + case let .samePeer(callContext): + applyGroupCallResult(context.sharedContext, callContext) + if let joinHash = joinHash { + callContext.call.joinAsSpeakerIfNeeded(joinHash) } + case let .success(callContext): + applyGroupCallResult(context.sharedContext, callContext) + default: + alert(for: context.window, info: L10n.errorAnError) } - } - })) + }) + } + if let callJoinPeerId = groupCall?.callJoinPeerId { + join(callJoinPeerId, nil) + } else { + selectGroupCallJoiner(context: context, peerId: peerId, completion: join) + } + } else if let peer = self?.chatInteraction.peer { + if peer.groupAccess.canMakeVoiceChat { + confirm(for: context.window, information: L10n.voiceChatChatStartNew, okTitle: L10n.voiceChatChatStartNewOK, successHandler: { _ in + createVoiceChat(context: context, peerId: peerId) + }) + } + } + } + + chatInteraction.returnGroup = { [weak self] in + if let strongSelf = self, let window = strongSelf.window { + _ = showModalProgress(signal: returnGroup(account: context.account, peerId: strongSelf.chatInteraction.peerId), for: window).start() } } - chatInteraction.requestMessageActionCallback = {[weak self] messageId, isGame, data in - if let strongSelf = self { - self?.messageActionCallbackDisposable.set((requestMessageActionCallback(account: strongSelf.account, messageId: messageId, isGame:isGame, data: data) |> deliverOnMainQueue).start(next: { [weak strongSelf] (result) in + chatInteraction.openScheduledMessages = { [weak self] in + self?.openScheduledChat() + } + + chatInteraction.openBank = { card in + _ = showModalProgress(signal: context.engine.payments.getBankCardInfo(cardNumber: card), for: context.window).start(next: { info in + if let info = info { - if let strongSelf = strongSelf { - switch result { - case .none: - break - case let .alert(text): - let message: Signal = .single(text) - let noMessage: Signal = .single(nil) - let delayedNoMessage: Signal = noMessage |> delay(1.0, queue: Queue.mainQueue()) - strongSelf.botCallbackAlertMessage.set(message |> then(delayedNoMessage)) - case let .url(url): - if isGame { - strongSelf.navigationController?.push(WebGameViewController(strongSelf.account, strongSelf.peerId, messageId, url)) - } else { - execute(inapp: .external(link: url, !(strongSelf.peer?.isVerified ?? false))) - } - } + let values: [ValuesSelectorValue] = info.urls.map { + return ValuesSelectorValue(localized: $0.title, value: $0.url) } - })) - } + + showModal(with: ValuesSelectorModalController(values: values, selected: nil, title: info.title, onComplete: { selected in + execute(inapp: .external(link: selected.value, false)) + }), for: context.window) + + } + }) } + chatInteraction.shareContact = { [weak self] peer in + if let strongSelf = self, let main = strongSelf.chatInteraction.peer, main.canSendMessage(strongSelf.mode.isThreadMode) { + _ = Sender.shareContact(context: context, peerId: strongSelf.chatInteraction.peerId, contact: peer).start() + } + } - chatInteraction.focusMessageId = { [weak self] fromId, toId, state in - + chatInteraction.unblock = { [weak self] in if let strongSelf = self { - if let fromId = fromId { - strongSelf.historyState = strongSelf.historyState.withAddingReply(fromId) - } - - var fromIndex: MessageIndex? + strongSelf.unblockDisposable.set(context.blockedPeersContext.remove(peerId: strongSelf.chatInteraction.peerId).start()) + } + } + + chatInteraction.updatePinned = { [weak self] pinnedId, dismiss, silent, forThisPeerOnlyIfPossible in + if let `self` = self { - if let fromId = fromId, let message = strongSelf.messageInCurrentHistoryView(fromId) { - fromIndex = MessageIndex(message) - } else { - if let message = strongSelf.anchorMessageInCurrentHistoryView() { - fromIndex = MessageIndex(message) + let pinnedUpdate: PinnedMessageUpdate = dismiss ? .clear(id: pinnedId) : .pin(id: pinnedId, silent: silent, forThisPeerOnlyIfPossible: forThisPeerOnlyIfPossible) + let peerId = self.chatInteraction.peerId + if let peer = self.chatInteraction.peer as? TelegramChannel { + if peer.hasPermission(.pinMessages) || (peer.isChannel && peer.hasPermission(.editAllMessages)) { + + self.updatePinnedDisposable.set(((dismiss ? confirmSignal(for: context.window, header: L10n.chatConfirmUnpinHeader, information: L10n.chatConfirmUnpin, okTitle: L10n.chatConfirmUnpinOK) : Signal.single(true)) |> filter {$0} |> mapToSignal { _ in return + showModalProgress(signal: context.engine.messages.requestUpdatePinnedMessage(peerId: peerId, update: pinnedUpdate) |> `catch` {_ in .complete() + }, for: context.window)}).start()) + } else { + self.chatInteraction.update({$0.updatedInterfaceState({$0.withAddedDismissedPinnedIds([pinnedId])})}) } - } - if let fromIndex = fromIndex { - if let message = strongSelf.messageInCurrentHistoryView(toId) { - strongSelf.genericView.tableView.scroll(to: state.swap(to: ChatHistoryEntryId.message(message))) + } else if self.chatInteraction.peerId.namespace == Namespaces.Peer.CloudUser { + if dismiss { + confirm(for: context.window, header: L10n.chatConfirmUnpinHeader, information: L10n.chatConfirmUnpin, okTitle: L10n.chatConfirmUnpinOK, successHandler: { [weak self] _ in + self?.updatePinnedDisposable.set(showModalProgress(signal: context.engine.messages.requestUpdatePinnedMessage(peerId: peerId, update: pinnedUpdate), for: context.window).start()) + }) } else { - let historyView = chatHistoryViewForLocation(.InitialSearch(location: .id(toId), count: 50), account: strongSelf.account, peerId: strongSelf.peerId, fixedCombinedReadState: nil, tagMask: nil, additionalData: []) - - struct FindSearchMessage { - let message:Message? - let loaded:Bool - } - - let signal = historyView - |> mapToSignal { historyView -> Signal in - switch historyView { - case .Loading: - return .complete() - case let .HistoryView(view, _, _, _): - for entry in view.entries { - if case let .MessageEntry(message, _, _, _) = entry { - if message.id == toId { - return .single(message) - } - } - } - return .single(nil) - } - } - |> take(1) - strongSelf.messageIndexDisposable.set((signal |> deliverOnMainQueue).start(next: { [weak strongSelf] message in - if let strongSelf = strongSelf, let message = message { - let toIndex = MessageIndex(message) - strongSelf.setLocation(.Scroll(index: toIndex, anchorIndex: toIndex, sourceIndex: fromIndex, scrollPosition: state.swap(to: ChatHistoryEntryId.message(message)), animated: state.animated)) - } - }, completed: { - - })) + self.updatePinnedDisposable.set(showModalProgress(signal: context.engine.messages.requestUpdatePinnedMessage(peerId: peerId, update: pinnedUpdate), for: context.window).start()) + } + } else if let peer = self.chatInteraction.peer as? TelegramGroup, peer.canPinMessage { + if dismiss { + confirm(for: context.window, header: L10n.chatConfirmUnpinHeader, information: L10n.chatConfirmUnpin, okTitle: L10n.chatConfirmUnpinOK, successHandler: { [weak self]_ in + self?.updatePinnedDisposable.set(showModalProgress(signal: context.engine.messages.requestUpdatePinnedMessage(peerId: peerId, update: pinnedUpdate), for: context.window).start()) + }) + } else { + self.updatePinnedDisposable.set(showModalProgress(signal: context.engine.messages.requestUpdatePinnedMessage(peerId: peerId, update: pinnedUpdate), for: context.window).start()) } } - } - } - chatInteraction.sendMedia = { [weak self] media in - if let strongSelf = self, let peer = strongSelf.peer, peer.canSendMessage { - let _ = (Sender.enqueue(media: media, account: strongSelf.account, peerId: strongSelf.peerId, chatInteraction: strongSelf.chatInteraction) |> deliverOnMainQueue).start(completed: scrollAfterSend) - strongSelf.nextTransaction.set(handler: {}) + chatInteraction.openPinnedMessages = { [weak self, unowned context] messageId in + guard let `self` = self else { + return } + self.navigationController?.push(ChatAdditionController(context: context, chatLocation: .peer(peerId), mode: .pinned, messageId: messageId)) } - chatInteraction.sendAppFile = { [weak self] file in - if let strongSelf = self, let peer = strongSelf.peer, peer.canSendMessage { - let _ = (Sender.enqueue(media: file, account: strongSelf.account, peerId: strongSelf.peerId, chatInteraction: strongSelf.chatInteraction) |> deliverOnMainQueue).start(completed: scrollAfterSend) - strongSelf.nextTransaction.set(handler: {}) - + chatInteraction.unpinAllMessages = { [weak self, unowned context] in + guard let `self` = self else { + return } - } - - chatInteraction.shareSelfContact = { [weak self] replyId in - if let strongSelf = self, let peer = strongSelf.peer, peer.canSendMessage { - strongSelf.shareContactDisposable.set((strongSelf.account.viewTracker.peerView(strongSelf.account.peerId) |> take(1)).start(next: { [weak strongSelf] peerView in - if let strongSelf = strongSelf, let peer = peerViewMainPeer(peerView) as? TelegramUser { - - _ = Sender.enqueue(message: EnqueueMessage.message(text: "", attributes: [], media: TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: peer.phone ?? "", peerId: peer.id), replyToMessageId: replyId), account: strongSelf.account, peerId: strongSelf.peerId).start() + + guard let peer = self.chatInteraction.presentation.peer else { + return + } + + var canManagePin = false + if let channel = peer as? TelegramChannel { + canManagePin = channel.hasPermission(.pinMessages) + } else if let group = peer as? TelegramGroup { + switch group.role { + case .creator, .admin: + canManagePin = true + default: + if let defaultBannedRights = group.defaultBannedRights { + canManagePin = !defaultBannedRights.flags.contains(.banPinMessages) + } else { + canManagePin = true } - })) + } + } else if let _ = peer as? TelegramUser, self.chatInteraction.presentation.canPinMessage { + canManagePin = true + } + + if canManagePin { + let count = self.chatInteraction.presentation.pinnedMessageId?.totalCount ?? 1 + + confirm(for: context.window, information: L10n.chatUnpinAllMessagesConfirmationCountable(count), okTitle: L10n.chatConfirmUnpinOK, cancelTitle: L10n.modalCancel, successHandler: { [weak self] _ in + let _ = (context.engine.messages.requestUnpinAllMessages(peerId: peerId) + |> deliverOnMainQueue).start(error: { _ in + + }, completed: { [weak self] in + self?.navigationController?.back() + }) + }) + } else { + self.chatInteraction.update({ state in + return state.updatedInterfaceState { $0.withAddedDismissedPinnedIds(state.pinnedMessageId?.others.map { $0 } ?? [] )} + }) + self.navigationController?.back() } + + + } - chatInteraction.modalSearch = { [weak self] query in - if let strongSelf = self { - let apply = showModalProgress(signal: searchMessages(account: strongSelf.account, peerId: strongSelf.peerId, query: query), for: mainWindow) - showModal(with: SearchResultModalController(strongSelf.account, request: apply, query: query, chatInteraction:strongSelf.chatInteraction), for: mainWindow) - } + chatInteraction.getCachedData = { [weak self] in + return ((self?.centerBarView as? ChatTitleBarView)?.postboxView as? PeerView)?.cachedData } - chatInteraction.sendCommand = { [weak self] command in - if let strongSelf = self, let peer = strongSelf.peer, peer.canSendMessage { - var commandText = "/" + command.command.text - if strongSelf.peerId.namespace != Namespaces.Peer.CloudUser { - commandText += "@" + (command.peer.username ?? "") + chatInteraction.reportSpamAndClose = { [weak self] in + let title: String + if let peer = self?.chatInteraction.peer { + if peer.isUser { + title = L10n.chatConfirmReportSpamUser + } else if peer.isChannel { + title = L10n.chatConfirmReportSpamChannel + } else if peer.isGroup || peer.isSupergroup { + title = L10n.chatConfirmReportSpamGroup + } else { + title = L10n.chatConfirmReportSpam } - strongSelf.chatInteraction.updateInput(with: "") - let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [EnqueueMessage.message(text: commandText, attributes:[], media: nil, replyToMessageId: nil)]).start() + } else { + title = L10n.chatConfirmReportSpam } + + self?.reportPeerDisposable.set((confirmSignal(for: context.window, header: L10n.chatConfirmReportSpamHeader, information: title, okTitle: L10n.messageContextReport, cancelTitle: L10n.modalCancel) |> filter {$0} |> mapToSignal { [weak self] _ in + return context.engine.peers.reportPeer(peerId: peerId) |> deliverOnMainQueue |> mapToSignal { [weak self] _ -> Signal in + if let peer = self?.chatInteraction.peer { + if peer.id.namespace == Namespaces.Peer.CloudUser { + return context.engine.peers.removePeerChat(peerId: peerId, reportChatSpam: true) |> deliverOnMainQueue + |> mapToSignal { _ in + return context.blockedPeersContext.add(peerId: peer.id) |> `catch` { _ in return .complete() } |> mapToSignal { _ in + return .single(Void()) + } + } + } else { + return context.engine.peers.removePeerChat(peerId: peerId, reportChatSpam: true) + } + } + return .complete() + } + |> deliverOnMainQueue + }).start(next: { [weak self] in + self?.navigationController?.back() + })) } - chatInteraction.switchInlinePeer = { [weak self] switchId, initialAction in + chatInteraction.dismissPeerStatusOptions = { [weak self] in if let strongSelf = self { - strongSelf.navigationController?.push(ChatSwitchInlineController(account: strongSelf.account, peerId: switchId, fallbackId:strongSelf.peerId, initialAction: initialAction)) + let peerId = strongSelf.chatInteraction.peerId + _ = context.engine.peers.dismissPeerStatusOptions(peerId: peerId).start() } } - chatInteraction.setNavigationAction = { [weak self] action in - self?.navigationController?.set(modalAction: action) + chatInteraction.toggleSidebar = { [weak self] in + FastSettings.toggleSidebarShown(!FastSettings.sidebarShown) + self?.updateSidebar() + (self?.navigationController as? MajorNavigationController)?.genericView.update() } - chatInteraction.showPreviewSender = { [weak self] urls, asMedia in - if let chatInteraction = self?.chatInteraction, let window = self?.navigationController?.window, let account = self?.account { - showModal(with: PreviewSenderController(urls: urls, account: account, chatInteraction: chatInteraction, asMedia: asMedia), for: window) - } + chatInteraction.focusInputField = { [weak self] in + _ = self?.context.window.makeFirstResponder(self?.firstResponder()) } - chatInteraction.setSecretChatMessageAutoremoveTimeout = { [weak self] seconds in - if let strongSelf = self, let peer = strongSelf.peer, peer.canSendMessage { - _ = setSecretChatMessageAutoremoveTimeoutInteractively(account: strongSelf.account, peerId: strongSelf.peerId, timeout:seconds).start() + chatInteraction.updateReactions = { [weak self] messageId, reaction, loading in + guard let `self` = self else { + return } + self.updateReqctionsDisposable.set((updateMessageReactionsInteractively(postbox: self.context.account.postbox, messageId: messageId, reaction: reaction) |> deliverOnMainQueue).start(), forKey: messageId) } - - chatInteraction.toggleNotifications = { [weak self] in - if let strongSelf = self { - _ = togglePeerMuted(account: strongSelf.account, peerId: strongSelf.peerId).start() + chatInteraction.withToggledSelectedMessage = { [weak self] f in + guard let `self` = self else { + return } - } - - chatInteraction.removeAndCloseChat = { [weak self] in - if let strongSelf = self, let window = strongSelf.window { - _ = showModalProgress(signal: removePeerChat(postbox: strongSelf.account.postbox, peerId: strongSelf.peerId, reportChatSpam: false), for: window).start(next: { [weak strongSelf] in - strongSelf?.navigationController?.close() - }) + let previous = self.chatInteraction.presentation.selectionState?.selectedIds + self.chatInteraction.update(f) + + if let event = NSApp.currentEvent, event.modifierFlags.contains(.shift) { + if let selectionState = self.chatInteraction.presentation.selectionState, let lastMessageId = selectionState.lastSelectedId, let previous = previous { + if let messageId = selectionState.selectedIds.subtracting(previous).first { + let minId = min(lastMessageId.id, messageId.id) + let maxId = max(lastMessageId.id, messageId.id) + let cloudNamespace = self.mode == .scheduled ? Namespaces.Message.ScheduledCloud : Namespaces.Message.Cloud + let localNamespace = self.mode == .scheduled ? Namespaces.Message.ScheduledLocal : Namespaces.Message.Local + let selectMessages = context.account.postbox.transaction { transaction -> [Message] in + var messages:[Message] = [] + for id in minId ..< maxId { + let cloudId = MessageId(peerId: lastMessageId.peerId, namespace: cloudNamespace, id: id) + let localId = MessageId(peerId: lastMessageId.peerId, namespace: localNamespace, id: id) + let message = transaction.getMessage(cloudId) ?? transaction.getMessage(localId) + if let message = message { + if minId > maxId { + messages.append(message) + } else { + messages.insert(message, at: 0) + } + } + } + return messages + } |> deliverOnMainQueue + + self.shiftSelectedDisposable.set(selectMessages.start(next: { [weak self] messages in + guard let `self` = self else { + return + } + self.chatInteraction.update({ current in + var current = current + if let selectionState = current.selectionState, selectionState.selectedIds.count >= 100 { + return current + } + for message in messages { + current = current.withUpdatedSelectedMessage(message.id) + } + + return current + }) + })) + } + } } } - chatInteraction.joinChannel = { [weak self] in - if let strongSelf = self, let window = strongSelf.window { - _ = showModalProgress(signal: joinChannel(account: strongSelf.account, peerId: strongSelf.peerId), for: window).start() + chatInteraction.getGradientOffsetRect = { [weak self] in + guard let `self` = self else { + return .zero } + let point = self.genericView.scroll.rect.origin + return CGRect(origin: point, size: self.frame.size) } - chatInteraction.returnGroup = { [weak self] in - if let strongSelf = self, let window = strongSelf.window { - _ = showModalProgress(signal: returnGroup(account: strongSelf.account, peerId: strongSelf.peerId), for: window).start() + var currentThreadId: MessageId? + + chatInteraction.openReplyThread = { [weak self] messageId, isChannelPost, modalProgress, mode in + let signal:Signal + + if modalProgress { + signal = showModalProgress(signal: fetchAndPreloadReplyThreadInfo(context: context, subject: isChannelPost ? .channelPost(messageId) : .groupMessage(messageId)) |> take(1) |> deliverOnMainQueue, for: context.window) + } else { + signal = fetchAndPreloadReplyThreadInfo(context: context, subject: isChannelPost ? .channelPost(messageId) : .groupMessage(messageId)) |> take(1) |> deliverOnMainQueue } + + currentThreadId = mode.originId + + delay(0.2, closure: { + if currentThreadId == mode.originId { + self?.updateThread { _ in + return mode.originId + } + } + }) + + + self?.loadThreadDisposable.set(signal.start(next: { [weak self] result in + let chatLocation: ChatLocation = .replyThread(result.message) + self?.updateThread { _ in + return nil + } + currentThreadId = nil + let updatedMode: ReplyThreadMode + if result.isChannelPost { + updatedMode = .comments(origin: mode.originId) + } else { + updatedMode = .replies(origin: mode.originId) + } + self?.navigationController?.push(ChatAdditionController(context: context, chatLocation: chatLocation, mode: .replyThread(data: result.message, mode: updatedMode), messageId: isChannelPost ? nil : mode.originId, initialAction: nil, chatLocationContextHolder: result.contextHolder)) + }, error: { error in + self?.updateThread { _ in + return nil + } + currentThreadId = nil + + switch error { + case .generic: + alert(for: context.window, info: L10n.chatDiscussionMessageDeleted) + } + })) } - chatInteraction.shareContact = { [weak self] peer in - if let strongSelf = self, let main = strongSelf.peer, main.canSendMessage { - _ = Sender.shareContact(account: strongSelf.account, peerId: strongSelf.peerId, contact: peer).start() - } - } - chatInteraction.unblock = { [weak self] in - if let strongSelf = self { - self?.unblockDisposable.set(requestUpdatePeerIsBlocked(account: strongSelf.account, peerId: strongSelf.peerId, isBlocked: false).start()) + chatInteraction.closeAfterPeek = { [weak self] peek in + + let showConfirm:()->Void = { + confirm(for: context.window, header: L10n.privateChannelPeekHeader, information: L10n.privateChannelPeekText, okTitle: L10n.privateChannelPeekOK, cancelTitle: L10n.privateChannelPeekCancel, successHandler: { _ in + self?.chatInteraction.joinChannel() + }, cancelHandler: { + self?.navigationController?.back() + }) + } + + let timeout = TimeInterval(peek) - Date().timeIntervalSince1970 + if timeout > 0 { + let signal = Signal.complete() |> delay(timeout, queue: .mainQueue()) + self?.peekDisposable.set(signal.start(completed: showConfirm)) + } else { + showConfirm() } } - chatInteraction.updatePinned = { [weak self] pinnedId, dismiss, silent in - if let strongSelf = self, let peer = strongSelf.peer as? TelegramChannel { - if peer.hasAdminRights(.canPinMessages) { - - let pinnedUpdate: PinnedMessageUpdate = dismiss ? .clear : .pin(id: pinnedId, silent: silent) - - strongSelf.updatePinnedDisposable.set(((dismiss ? confirmSignal(for: mainWindow, header: appName, information: tr(.chatConfirmUnpin)) : Signal.single(true)) |> filter {$0} |> mapToSignal { _ in return showModalProgress(signal: requestUpdatePinnedMessage(account: strongSelf.account, peerId: strongSelf.peerId, update: pinnedUpdate) |> mapError {_ in}, for: mainWindow)}).start()) - } else { - strongSelf.chatInteraction.update({$0.updatedInterfaceState({$0.withUpdatedDismissedPinnedId(pinnedId)})}) - } + + let topPinnedMessage: Signal + switch mode { + case .history: + switch self.chatLocation { + case let .peer(peerId): + let replyHistory: Signal = (chatHistoryViewForLocation(.Initial(count: 100), context: self.context, chatLocation: .peer(peerId), fixedCombinedReadStates: nil, tagMask: MessageTags.pinned, additionalData: []) + |> castError(Bool.self) + |> mapToSignal { update -> Signal in + switch update { + case let .Loading(_, type): + if case .Generic(.FillHole) = type { + return .fail(true) + } + case let .HistoryView(_, type, _, _): + if case .Generic(.FillHole) = type { + return .fail(true) + } + } + return .single(update) + }) + |> restartIfError + + topPinnedMessage = combineLatest( + replyHistory, + self.topVisibleMessageRange.get(), self.dismissedPinnedIds.get() + ) + |> map { update, topVisibleMessageRange, dismissed -> ChatPinnedMessage? in + var message: ChatPinnedMessage? + switch update { + case .Loading: + break + case let .HistoryView(view, _, _, _): + for i in 0 ..< view.entries.count { + let entry = view.entries[i] + var matches = false + if message == nil { + matches = !dismissed.ids.contains(entry.message.id) + } else if let topVisibleMessageRange = topVisibleMessageRange { + if entry.message.id <= topVisibleMessageRange.lowerBound { + matches = !dismissed.ids.contains(entry.message.id) + } + } + if let tempMaxId = dismissed.tempMaxId { + var effectiveMatches = matches && entry.message.id < tempMaxId + + if matches, message == nil, i == view.entries.count - 1 { + effectiveMatches = true + } + matches = effectiveMatches + } + if matches { + message = ChatPinnedMessage(messageId: entry.message.id, message: entry.message, others: view.entries.map { $0.message.id }, isLatest: i == view.entries.count - 1, index: view.entries.count - 1 - i, totalCount: view.entries.count) + } + } + break + } + return message + } + |> distinctUntilChanged + default: + topPinnedMessage = .single(nil) } + case .pinned: + let replyHistory: Signal = (chatHistoryViewForLocation(.Initial(count: 100), context: self.context, chatLocation: .peer(peerId), fixedCombinedReadStates: nil, tagMask: MessageTags.pinned, additionalData: []) + |> castError(Bool.self) + |> mapToSignal { update -> Signal in + switch update { + case let .Loading(_, type): + if case .Generic(.FillHole) = type { + return .fail(true) + } + case let .HistoryView(_, type, _, _): + if case .Generic(.FillHole) = type { + return .fail(true) + } + } + return .single(update) + }) + |> restartIfError + + topPinnedMessage = replyHistory + |> map { update -> ChatPinnedMessage? in + switch update { + case .Loading: + break + case let .HistoryView(view, _, _, _): + if let first = view.entries.first { + return ChatPinnedMessage(messageId: first.message.id, message: first.message, others: view.entries.map { $0.message.id }, isLatest: true, index: 0, totalCount: view.entries.count) + } + } + return nil + } + |> distinctUntilChanged + default: + topPinnedMessage = .single(nil) } - chatInteraction.reportSpamAndClose = { [weak self] in - if let strongSelf = self { - strongSelf.reportPeerDisposable.set((showModalProgress(signal: reportPeer(account: strongSelf.account, peerId: strongSelf.peerId) |> deliverOnMainQueue |> mapToSignal { [weak strongSelf] () -> Signal in - if let strongSelf = strongSelf, let peer = strongSelf.peer { - if peer.id.namespace == Namespaces.Peer.CloudUser { - return requestUpdatePeerIsBlocked(account: strongSelf.account, peerId: peer.id, isBlocked: true) |> deliverOnMainQueue |> mapToSignal { [weak strongSelf] () -> Signal in - if let strongSelf = strongSelf { - return removePeerChat(postbox: strongSelf.account.postbox, peerId: strongSelf.peerId, reportChatSpam: false) + + let initialData = initialDataHandler.get() |> take(1) |> beforeNext { [weak self] (combinedInitialData) in + + guard let `self` = self else { + return + } + guard let initialData = combinedInitialData.initialData else { + self.genericView.inputView.updateInterface(with: self.chatInteraction) + return + } + + let opaqueState = initialData.storedInterfaceState.flatMap(_internal_decodeStoredChatInterfaceState) + + let interfaceState = ChatInterfaceState.parse(opaqueState, peerId: self.chatLocation.peerId, context: context) + + if let interfaceState = interfaceState { + self.chatInteraction.update(animated:false,{$0.updatedInterfaceState({_ in return interfaceState})}) + } + switch self.chatInteraction.mode { + case let .replyThread(data, _): + self.chatInteraction.update(animated:false, { present in + var present = present + present = present.withUpdatedHidePinnedMessage(true) + if let cachedData = combinedInitialData.cachedData as? CachedChannelData { + if let peer = present.peer as? TelegramChannel { + switch peer.info { + case let .group(info): + if info.flags.contains(.slowModeEnabled), peer.adminRights == nil && !peer.flags.contains(.isCreator) { + present = present + .updateSlowMode({ value in + var value = value ?? SlowMode() + value = value + .withUpdatedValidUntil(cachedData.slowModeValidUntilTimestamp) + if let timeout = cachedData.slowModeValidUntilTimestamp { + if timeout > context.timestamp { + value = value.withUpdatedTimeout(timeout - context.timestamp) + } else { + value = value.withUpdatedTimeout(nil) + } + } + return value + }) + } else { + present = present.updateSlowMode { _ in return nil } } - return .complete() + default: + present = present.updateSlowMode { _ in return nil } } - } else { - return removePeerChat(postbox: strongSelf.account.postbox, peerId: strongSelf.peerId, reportChatSpam: true) } } - return .complete() - }, for: mainWindow) |> deliverOnMainQueue).start(completed: { [weak strongSelf] in - if let strongSelf = strongSelf { - strongSelf.navigationController?.back() + + var pinnedMessage: ChatPinnedMessage? + pinnedMessage = ChatPinnedMessage(messageId: data.messageId, message: combinedInitialData.cachedDataMessages?[data.messageId]?.first, isLatest: true) + + present = present.withUpdatedPinnedMessageId(pinnedMessage) + return present.withUpdatedLimitConfiguration(combinedInitialData.limitsConfiguration) + }) + case .history, .preview: + self.chatInteraction.update(animated:false, { present in + var present = present + + if peerId.namespace == Namespaces.Peer.SecretChat { + + } else if let cachedData = combinedInitialData.cachedData as? CachedChannelData { + present = present.withUpdatedMessageSecretTimeout(cachedData.autoremoveTimeout) + } else if let cachedData = combinedInitialData.cachedData as? CachedGroupData { + present = present.withUpdatedMessageSecretTimeout(cachedData.autoremoveTimeout) + } else if let cachedData = combinedInitialData.cachedData as? CachedUserData { + present = present.withUpdatedMessageSecretTimeout(cachedData.autoremoveTimeout) } - })) + + if let cachedData = combinedInitialData.cachedData as? CachedGroupData { + present = present.updatedGroupCall({ currentValue in + if let call = cachedData.activeCall { + return ChatActiveGroupCallInfo(activeCall: call, data: currentValue?.data, callJoinPeerId: cachedData.callJoinPeerId, joinHash: currentValue?.joinHash) + } else { + return nil + } + }) + } + if let cachedData = combinedInitialData.cachedData as? CachedUserData { + present = present + .withUpdatedBlocked(cachedData.isBlocked) + .withUpdatedCanPinMessage(cachedData.canPinMessages || context.peerId == peerId) + .updateBotMenu { current in + if let botInfo = cachedData.botInfo, !botInfo.commands.isEmpty { + var current = current ?? .init(commands: [], revealed: false) + current.commands = botInfo.commands + return current + } + return nil + } +// .withUpdatedHasScheduled(cachedData.hasScheduledMessages) + } else if let cachedData = combinedInitialData.cachedData as? CachedChannelData { + present = present + .withUpdatedIsNotAccessible(cachedData.isNotAccessible) + .updatedGroupCall({ currentValue in + if let call = cachedData.activeCall { + return ChatActiveGroupCallInfo(activeCall: call, data: currentValue?.data, callJoinPeerId: cachedData.callJoinPeerId, joinHash: currentValue?.joinHash) + } else { + return nil + } + }) + if let peer = present.peer as? TelegramChannel { + switch peer.info { + case let .group(info): + if info.flags.contains(.slowModeEnabled), peer.adminRights == nil && !peer.flags.contains(.isCreator) { + present = present.updateSlowMode({ value in + var value = value ?? SlowMode() + value = value.withUpdatedValidUntil(cachedData.slowModeValidUntilTimestamp) + if let timeout = cachedData.slowModeValidUntilTimestamp { + if timeout > context.timestamp { + value = value.withUpdatedTimeout(timeout - context.timestamp) + } else { + value = value.withUpdatedTimeout(nil) + } + } + return value + }) + } else { + present = present.updateSlowMode { _ in return nil } + } + default: + present = present.updateSlowMode { _ in return nil } + } + } + } + return present.withUpdatedLimitConfiguration(combinedInitialData.limitsConfiguration) + }) + case .scheduled: + break + case .pinned, .preview: + break } - } - - chatInteraction.dismissPeerReport = { [weak self] in - if let strongSelf = self { - _ = dismissReportPeer(account:strongSelf.account, peerId:strongSelf.peerId).start() + + if let modalAction = self.navigationController?.modalAction { + self.invokeNavigation(action: modalAction) } - } + + + self.state = self.chatInteraction.presentation.state == .selecting ? .Edit : .Normal + self.notify(with: self.chatInteraction.presentation, oldValue: ChatPresentationInterfaceState(chatLocation: self.chatInteraction.chatLocation, chatMode: self.chatInteraction.mode), animated: false, force: true) + + self.genericView.inputView.updateInterface(with: self.chatInteraction) + + } |> map {_ in} + + + + + let first:Atomic = Atomic(value: true) + - chatInteraction.toggleSidebar = { [weak self] in - FastSettings.toggleSidebarShown(!FastSettings.sidebarShown) - self?.updateSidebar() - (self?.navigationController as? MajorNavigationController)?.genericView.update() - } + let availableGroupCall: Signal = getGroupCallPanelData(context: context, peerId: peerId) - - let initialData = initialDataHandler.get() |> take(1) |> beforeNext { [weak self] (combinedInitialData) in + + peerDisposable.set((combineLatest(queue: .mainQueue(), topPinnedMessage, peerView.get(), availableGroupCall) |> beforeNext { [weak self] topPinnedMessage, postboxView, groupCallData in + + guard let `self` = self else {return} + (self.centerBarView as? ChatTitleBarView)?.postboxView = postboxView + let peerView = postboxView as? PeerView - if let strongSelf = self { - if let initialData = combinedInitialData.initialData { - if let interfaceState = initialData.chatInterfaceState as? ChatInterfaceState { - strongSelf.chatInteraction.update(animated:false,{$0.updatedInterfaceState({_ in return interfaceState})}) - strongSelf.chatInteraction.invokeInitialAction(includeAuto: true) - } - - if let modalAction = strongSelf.navigationController?.modalAction { - strongSelf.invokeNavigation(action: modalAction) + switch self.chatInteraction.mode { + case .history, .preview: + + if let cachedData = peerView?.cachedData as? CachedChannelData { + let onlineMemberCount:Signal + if (cachedData.participantsSummary.memberCount ?? 0) > 200 { + onlineMemberCount = context.peerChannelMemberCategoriesContextsManager.recentOnline(peerId: self.chatInteraction.peerId) |> map(Optional.init) |> deliverOnMainQueue + } else { + onlineMemberCount = context.peerChannelMemberCategoriesContextsManager.recentOnlineSmall(peerId: self.chatInteraction.peerId) |> map(Optional.init) |> deliverOnMainQueue } - strongSelf.state = strongSelf.chatInteraction.presentation.state == .selecting ? .Edit : .Normal - strongSelf.notify(with: strongSelf.chatInteraction.presentation, oldValue: ChatPresentationInterfaceState(), animated: false, force: true) - - strongSelf.genericView.inputView.updateInterface(with: strongSelf.chatInteraction, account: strongSelf.account) + self.onlineMemberCountDisposable.set(onlineMemberCount.start(next: { [weak self] count in + (self?.centerBarView as? ChatTitleBarView)?.onlineMemberCount = count + })) } - } - - } |> map {_ in} - - let first:Atomic = Atomic(value: true) - - peerDisposable.set((peerView.get() - |> deliverOnMainQueue |> beforeNext { [weak self] peerView in + var wasGroupChannel: Bool? + if let peer = self.chatInteraction.presentation.mainPeer as? TelegramChannel { + if case .group = peer.info { + wasGroupChannel = true + } else { + wasGroupChannel = false + } + } + var isGroupChannel: Bool? + if let peerView = peerView, let info = (peerView.peers[peerView.peerId] as? TelegramChannel)?.info { + if case .group = info { + isGroupChannel = true + } else { + isGroupChannel = false + } + } - if let strongSelf = self { - if let peer = peerViewMainPeer(peerView) { - strongSelf.peer = peer - (strongSelf.centerBarView as? ChatTitleBarView)?.peerView = peerView + if wasGroupChannel != isGroupChannel { + if let isGroupChannel = isGroupChannel, isGroupChannel { + let (recentDisposable, _) = context.peerChannelMemberCategoriesContextsManager.recent(peerId: chatInteraction.peerId, updated: { _ in }) + let (adminsDisposable, _) = context.peerChannelMemberCategoriesContextsManager.admins(peerId: chatInteraction.peerId, updated: { _ in }) + let disposable = DisposableSet() + disposable.add(recentDisposable) + disposable.add(adminsDisposable) + + self.updatedChannelParticipants.set(disposable) + } else { + self.updatedChannelParticipants.set(nil) } - strongSelf.chatInteraction.update(animated: !first.swap(false), { [weak peerView] presentation in - if let peerView = peerView { - var present = presentation.updatedPeer { [weak peerView] _ in - if let peerView = peerView { - return peerView.peers[peerView.peerId] + + } + + self.chatInteraction.update(animated: !first.swap(false), { [weak peerView] presentation in + if let peerView = peerView { + var present = presentation.updatedPeer { [weak peerView] _ in + if let peerView = peerView { + return peerView.peers[peerView.peerId] + } + return nil + }.updatedMainPeer(peerViewMainPeer(peerView)) + + var discussionGroupId:CachedChannelData.LinkedDiscussionPeerId = .unknown + if let cachedData = peerView.cachedData as? CachedChannelData { + if let peer = peerViewMainPeer(peerView) as? TelegramChannel { + switch peer.info { + case let .broadcast(info): + if info.flags.contains(.hasDiscussionGroup) { + discussionGroupId = cachedData.linkedDiscussionPeerId + } + case .group: + discussionGroupId = cachedData.linkedDiscussionPeerId } - return nil } - - if let cachedData = peerView.cachedData as? CachedUserData { - present = present.withUpdatedBlocked(cachedData.isBlocked).withUpdatedReportStatus(cachedData.reportStatus) + } + + if let peer = peerView.peers[peerId] { + if let peer = peer as? TelegramSecretChat { + if let value = peer.messageAutoremoveTimeout { + present = present.withUpdatedMessageSecretTimeout(.known(.init(peerValue: value))) + } else { + present = present.withUpdatedMessageSecretTimeout(.known(nil)) + } + } else if let cachedData = peerView.cachedData as? CachedUserData { + present = present.withUpdatedMessageSecretTimeout(cachedData.autoremoveTimeout) } else if let cachedData = peerView.cachedData as? CachedChannelData { - present = present.withUpdatedReportStatus(cachedData.reportStatus).withUpdatedPinnedMessageId(cachedData.pinnedMessageId) + present = present.withUpdatedMessageSecretTimeout(cachedData.autoremoveTimeout) } else if let cachedData = peerView.cachedData as? CachedGroupData { - present = present.withUpdatedReportStatus(cachedData.reportStatus) - } else if let cachedData = peerView.cachedData as? CachedSecretChatData { - present = present.withUpdatedReportStatus(cachedData.reportStatus) + present = present.withUpdatedMessageSecretTimeout(cachedData.autoremoveTimeout) } - var canAddContact:Bool? = nil - if let peer = peerViewMainPeer(peerView) as? TelegramUser { - if let _ = peer.phone, !peerView.peerIsContact { - canAddContact = true + } + + present = present.withUpdatedDiscussionGroupId(discussionGroupId) + present = present.withUpdatedPinnedMessageId(topPinnedMessage) + + var contactStatus: ChatPeerStatus? + if let cachedData = peerView.cachedData as? CachedUserData { + contactStatus = ChatPeerStatus(canAddContact: !peerView.peerIsContact, peerStatusSettings: cachedData.peerStatusSettings) + } else if let cachedData = peerView.cachedData as? CachedGroupData { + contactStatus = ChatPeerStatus(canAddContact: false, peerStatusSettings: cachedData.peerStatusSettings) + } else if let cachedData = peerView.cachedData as? CachedChannelData { + contactStatus = ChatPeerStatus(canAddContact: false, peerStatusSettings: cachedData.peerStatusSettings) + } else if let cachedData = peerView.cachedData as? CachedSecretChatData { + contactStatus = ChatPeerStatus(canAddContact: !peerView.peerIsContact, peerStatusSettings: cachedData.peerStatusSettings) + } + if let cachedData = peerView.cachedData as? CachedUserData { + present = present + .withUpdatedBlocked(cachedData.isBlocked) + .withUpdatedPeerStatusSettings(contactStatus) + .withUpdatedCanPinMessage(cachedData.canPinMessages || context.peerId == peerId) + .updateBotMenu { current in + if let botInfo = cachedData.botInfo, !botInfo.commands.isEmpty { + var current = current ?? .init(commands: [], revealed: false) + current.commands = botInfo.commands + return current + } + return nil + } + } else if let cachedData = peerView.cachedData as? CachedChannelData { + present = present + .withUpdatedPeerStatusSettings(contactStatus) + .withUpdatedIsNotAccessible(cachedData.isNotAccessible) + .updatedGroupCall({ current in + if let call = cachedData.activeCall { + return ChatActiveGroupCallInfo(activeCall: call, data: groupCallData, callJoinPeerId: cachedData.callJoinPeerId, joinHash: current?.joinHash) + } else { + return nil + } + }) + if let peer = peerViewMainPeer(peerView) as? TelegramChannel { + switch peer.info { + case let .group(info): + if info.flags.contains(.slowModeEnabled), peer.adminRights == nil && !peer.flags.contains(.isCreator) { + present = present.updateSlowMode({ value in + var value = value ?? SlowMode() + value = value.withUpdatedValidUntil(cachedData.slowModeValidUntilTimestamp) + if let timeout = cachedData.slowModeValidUntilTimestamp { + if timeout > context.timestamp { + value = value.withUpdatedTimeout(timeout - context.timestamp) + } else { + value = value.withUpdatedTimeout(nil) + } + } + return value + }) + } else { + present = present.updateSlowMode { _ in return nil } + } + default: + present = present.updateSlowMode { _ in return nil } } } - present = present.withUpdatedContactAdding(canAddContact) - - if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings { - present = present.updatedNotificationSettings(notificationSettings) + } else if let cachedData = peerView.cachedData as? CachedGroupData { + present = present + .withUpdatedPeerStatusSettings(contactStatus) + .updatedGroupCall({ current in + if let call = cachedData.activeCall { + return ChatActiveGroupCallInfo(activeCall: call, data: groupCallData, callJoinPeerId: cachedData.callJoinPeerId, joinHash: current?.joinHash) + } else { + return nil + } + }) + } else if let _ = peerView.cachedData as? CachedSecretChatData { + present = present + .withUpdatedPeerStatusSettings(contactStatus) + } + if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings { + present = present.updatedNotificationSettings(notificationSettings) + } + return present + } + return presentation + }) + case .scheduled: + self.chatInteraction.update(animated: !first.swap(false), { presentation in + return presentation.withUpdatedCanPinMessage(context.peerId == peerId).updatedPeer { _ in + if let peerView = peerView { + return peerView.peers[peerView.peerId] + } + return nil + }.updatedMainPeer(peerView != nil ? peerViewMainPeer(peerView!) : nil) + }) + case .pinned: + self.chatInteraction.update(animated: !first.swap(false), { presentation in + var pinnedMessage: ChatPinnedMessage? + pinnedMessage = topPinnedMessage + return presentation.withUpdatedPinnedMessageId(pinnedMessage).withUpdatedCanPinMessage((peerView?.cachedData as? CachedUserData)?.canPinMessages ?? true || context.peerId == peerId).updatedPeer { _ in + if let peerView = peerView { + return peerView.peers[peerView.peerId] + } + return nil + }.updatedMainPeer(peerView != nil ? peerViewMainPeer(peerView!) : nil) + }) + case .replyThread: + self.chatInteraction.update(animated: !first.swap(false), { [weak peerView] presentation in + if let peerView = peerView { + var present = presentation.updatedPeer { [weak peerView] _ in + if let peerView = peerView { + return peerView.peers[peerView.peerId] + } + return nil + }.updatedMainPeer(peerViewMainPeer(peerView)) + + if let cachedData = peerView.cachedData as? CachedChannelData { + present = present + .withUpdatedIsNotAccessible(cachedData.isNotAccessible) + if let peer = peerViewMainPeer(peerView) as? TelegramChannel { + switch peer.info { + case let .group(info): + if info.flags.contains(.slowModeEnabled), peer.adminRights == nil && !peer.flags.contains(.isCreator) { + present = present.updateSlowMode({ value in + var value = value ?? SlowMode() + value = value.withUpdatedValidUntil(cachedData.slowModeValidUntilTimestamp) + if let timeout = cachedData.slowModeValidUntilTimestamp { + if timeout > context.timestamp { + value = value.withUpdatedTimeout(timeout - context.timestamp) + } else { + value = value.withUpdatedTimeout(nil) + } + } + return value + }) + } else { + present = present.updateSlowMode { _ in return nil } + } + default: + present = present.updateSlowMode { _ in return nil } + } } - return present } - return presentation - }) - } - }).start()) - - if peerId.namespace == Namespaces.Peer.CloudChannel { - - - let fetchParticipants = peerView.get() |> filter {$0.cachedData != nil} |> take(1) |> deliverOnMainQueue |> mapToSignal { [weak self] _ -> Signal in - if let account = self?.account, let peerId = self?.peerId { - return account.viewTracker.updatedCachedChannelParticipants(peerId, forceImmediateUpdate: true) - } - return .complete() - + return present + } + return presentation + }) } - - updatedChannelParticipants.set(fetchParticipants.start()) - } + }).start()) + + + + let updating: Signal = context.account.stateManager.isUpdating |> mapToSignal { isUpdating in + return isUpdating ? .single(isUpdating) |> delay(1.0, queue: .mainQueue()) : .single(isUpdating) + } + let connecting: Signal = context.account.network.connectionStatus |> mapToSignal { status in + switch status { + case .online: + return .single(status) + default: + return .single(status) |> delay(1.0, queue: .mainQueue()) + } + } - let connectionStatus = account.network.connectionStatus |> deliverOnMainQueue |> beforeNext { [weak self] status -> Void in + let connectionStatus = combineLatest(queue: .mainQueue(), connecting, updating) |> deliverOnMainQueue |> beforeNext { [weak self] status, isUpdating -> Void in + var status = status + switch status { + case let .online(proxyAddress): + if isUpdating { + status = .updating(proxyAddress: proxyAddress) + } + default: + break + } (self?.centerBarView as? ChatTitleBarView)?.connectionStatus = status } - let combine = combineLatest(_historyReady.get() |> deliverOnMainQueue , peerView.get() |> deliverOnMainQueue |> take(1) |> map {_ in} |> then(initialData), genericView.inputView.ready.get()) + let combine = combineLatest(queue: .mainQueue(), _historyReady.get() , peerView.get() |> take(1) |> map { _ in } |> then(initialData), genericView.inputView.ready.get()) + + + //self.ready.set(.single(true)) self.ready.set(combine |> map { (hReady, _, iReady) in return hReady && iReady }) - connectionStatusDisposable.set((connectionStatus |> delay(0.5, queue: Queue.mainQueue())).start()) + connectionStatusDisposable.set((connectionStatus).start()) - var beginPendingTime:CFAbsoluteTime? - - self.sentMessageEventsDisposable.set((self.account.pendingMessageManager.deliveredMessageEvents(peerId: peerId)).start(next: { _ in - - if FastSettings.inAppSounds { - let afterSentSound:NSSound? = { - - let p = Bundle.main.path(forResource: "sent", ofType: "caf") - var sound:NSSound? - if let p = p { - sound = NSSound(contentsOfFile: p, byReference: true) - sound?.volume = 1.0 - } - - return sound - }() - - if let beginPendingTime = beginPendingTime { - if CFAbsoluteTimeGetCurrent() - beginPendingTime < 0.2 { - return - } - } - beginPendingTime = CFAbsoluteTimeGetCurrent() - afterSentSound?.play() - } - })) + botCallbackAlertMessageDisposable = (self.botCallbackAlertMessage.get() - |> deliverOnMainQueue).start(next: { [weak self] message in - if let strongSelf = self, let message = message, !message.isEmpty { - strongSelf.show(toaster: ControllerToaster(text:.initialize(string: message.fixed, color: theme.colors.text, font: .normal(.text)))) + |> deliverOnMainQueue).start(next: { [weak self] (message, isAlert) in + + if let strongSelf = self, let message = message { + if !message.isEmpty { + if isAlert { + alert(for: context.window, info: message) + } else { + strongSelf.show(toaster: ControllerToaster(text:.initialize(string: message.fixed, color: theme.colors.text, font: .normal(.text)))) + } + } else { + strongSelf.removeToaster() + } } + }) + switch mode { + case .history: + self.chatUnreadMentionCountDisposable.set((context.account.viewTracker.unseenPersonalMessagesCount(peerId: peerId) |> deliverOnMainQueue).start(next: { [weak self] count in + self?.genericView.updateMentionsCount(count, animated: true) + })) + default: + self.chatUnreadMentionCountDisposable.set(nil) + } + - self.chatUnreadMentionCountDisposable.set((self.account.viewTracker.unseenPersonalMessagesCount(peerId: self.peerId) |> deliverOnMainQueue).start(next: { [weak self] count in - self?.genericView.updateMentionsCount(count, animated: true) - })) - - let postbox = self.account.postbox let previousPeerCache = Atomic<[PeerId: Peer]>(value: [:]) - self.peerInputActivitiesDisposable.set((self.account.peerInputActivities(peerId: peerId) + + + self.peerInputActivitiesDisposable.set((context.account.peerInputActivities(peerId: .init(peerId: peerId, category: mode.activityCategory)) |> mapToSignal { activities -> Signal<[(Peer, PeerInputActivity)], NoError> in var foundAllPeers = true var cachedResult: [(Peer, PeerInputActivity)] = [] @@ -1584,11 +4480,11 @@ class ChatController: EditableViewController, Notifable { if foundAllPeers { return .single(cachedResult) } else { - return postbox.modify { modifier -> [(Peer, PeerInputActivity)] in + return context.account.postbox.transaction { transaction -> [(Peer, PeerInputActivity)] in var result: [(Peer, PeerInputActivity)] = [] var peerCache: [PeerId: Peer] = [:] for (peerId, activity) in activities { - if let peer = modifier.getPeer(peerId) { + if let peer = transaction.getPeer(peerId) { result.append((peer, activity)) peerCache[peerId] = peer } @@ -1599,41 +4495,116 @@ class ChatController: EditableViewController, Notifable { } } |> deliverOnMainQueue).start(next: { [weak self] activities in - if let strongSelf = self { - (strongSelf.centerBarView as? ChatTitleBarView)?.inputActivities = (strongSelf.peerId, activities) + if let strongSelf = self, strongSelf.chatInteraction.peerId != strongSelf.context.peerId { + (strongSelf.centerBarView as? ChatTitleBarView)?.inputActivities = (strongSelf.chatInteraction.peerId, activities) + + for activity in activities { + switch activity.1 { + case let .interactingWithEmoji(emoticon, messageId, interaction: interaction): + + let animations = interaction?.animations ?? [] + + let item = strongSelf.genericView.findItem(by: messageId) as? ChatRowItem + + if let item = item { + let mirror = item.isIncoming && item.renderType == .bubble + for animation in animations { + delay(Double(animation.timeOffset), closure: { [weak strongSelf] in + guard let strongSelf = strongSelf else { + return + } + strongSelf.emojiEffects.addAnimation(emoticon.fixed, index: animation.index, mirror: mirror, isIncoming: true, messageId: messageId, animationSize: NSMakeSize(350, 350), viewFrame: context.window.bounds, for: context.window.contentView!) + }) + } + } + + break + default: + break + } + } } })) + + + // var beginHistoryTime:CFAbsoluteTime? + genericView.tableView.setScrollHandler({ [weak self] scroll in - if let strongSelf = self { - let view = strongSelf.previousView.modify({$0}) - if let view = view { - var messageIndex:MessageIndex? - - switch scroll.direction { - case .bottom: - messageIndex = view.originalView.earlierId - case .top: - messageIndex = view.originalView.laterId - case .none: - break + guard let `self` = self else {return} + + let view = self.previousView.with { $0?.originalView } + if let view = view { + var messageIndex:MessageIndex? + + let visible = self.genericView.tableView.visibleRows() + + switch scroll.direction { + case .top: + if view.laterId != nil { + for i in visible.min ..< visible.max { + if let item = self.genericView.tableView.item(at: i) as? ChatRowItem { + messageIndex = item.entry.index + break + } + } + } else if view.laterId == nil, !view.holeLater, let locationValue = self.locationValue, !locationValue.isAtUpperBound, view.anchorIndex != .upperBound { + messageIndex = .upperBound(peerId: self.chatInteraction.peerId) + } + case .bottom: + if view.earlierId != nil { + for i in stride(from: visible.max - 1, to: -1, by: -1) { + if let item = self.genericView.tableView.item(at: i) as? ChatRowItem { + messageIndex = item.entry.index + break + } + } } - if let messageIndex = messageIndex { - strongSelf.setLocation(.Navigation(index: messageIndex, anchorIndex: messageIndex)) + case .none: + break + } + if let messageIndex = messageIndex { + let location: ChatHistoryLocation = .Navigation(index: MessageHistoryAnchorIndex.message(messageIndex), anchorIndex: MessageHistoryAnchorIndex.message(messageIndex), count: 100, side: scroll.direction == .bottom ? .upper : .lower) + guard location != self.locationValue else { + return } + self.setLocation(location) } } - + self.chatInteraction.update({$0.withUpdatedTempPinnedMaxId(nil)}) }) + genericView.tableView.addScroll(listener: TableScrollListener(dispatchWhenVisibleRangeUpdated: false, { [weak self] position in + guard let `self` = self else {return} + self.updateInteractiveReading() + })) + + genericView.tableView.addScroll(listener: TableScrollListener(dispatchWhenVisibleRangeUpdated: true, { [weak self] position in + guard let `self` = self else {return} + let tableView = self.genericView.tableView + let chatInteraction = self.chatInteraction + switch self.mode { + case .replyThread: + if let pinnedMessageId = chatInteraction.presentation.pinnedMessageId, position.visibleRows.location != NSNotFound { + var hidden: Bool = false + for row in position.visibleRows.min ..< position.visibleRows.max { + if let item = tableView.item(at: row) as? ChatRowItem, item.effectiveCommentMessage?.id == pinnedMessageId.messageId { + hidden = true + break + } + } + chatInteraction.update({$0.withUpdatedHidePinnedMessage(hidden)}) + } + default: + break + } + })) + genericView.tableView.addScroll(listener: TableScrollListener { [weak self] position in let tableView = self?.genericView.tableView - /* - - */ if let strongSelf = self, let tableView = tableView { if let row = tableView.topVisibleRow, let item = tableView.item(at: row) as? ChatRowItem, let id = item.message?.id { @@ -1644,20 +4615,35 @@ class ChatController: EditableViewController, Notifable { var messageIdsWithViewCount: [MessageId] = [] var messageIdsWithUnseenPersonalMention: [MessageId] = [] + var unsupportedMessagesIds: [MessageId] = [] + var topVisibleMessageRange: ChatTopVisibleMessageRange? + + var hasFailed: Bool = false + + var readAds:[Data] = [] tableView.enumerateVisibleItems(with: { item in if let item = item as? ChatRowItem { if message == nil { - message = item.message + message = item.lastMessage } - if let message = item.message { + + if let message = message, message.flags.contains(.Failed) { + hasFailed = !(message.media.first is TelegramMediaAction) + } + + for message in item.messages { var hasUncocumedMention: Bool = false var hasUncosumedContent: Bool = false - if message.tags.contains(.unseenPersonalMessage) { + if !hasFailed, message.flags.contains(.Failed) { + hasFailed = !(message.media.first is TelegramMediaAction) + } + + if message.tags.contains(.unseenPersonalMessage), item.chatInteraction.mode == .history { for attribute in message.attributes { if let attribute = attribute as? ConsumableContentMessageAttribute, !attribute.consumed { - hasUncosumedContent = true + hasUncosumedContent = true } if let attribute = attribute as? ConsumablePersonalMentionMessageAttribute, !attribute.pending { hasUncocumedMention = true @@ -1673,15 +4659,44 @@ class ChatController: EditableViewController, Notifable { break inner } } + if message.media.first is TelegramMediaUnsupported { + unsupportedMessagesIds.append(message.id) + } + + if let topVisibleMessageRangeValue = topVisibleMessageRange { + topVisibleMessageRange = ChatTopVisibleMessageRange(lowerBound: topVisibleMessageRangeValue.lowerBound, upperBound: message.id, isLast: item.index == tableView.count - 1) + } else { + topVisibleMessageRange = ChatTopVisibleMessageRange(lowerBound: message.id, upperBound: message.id, isLast: item.index == tableView.count - 1) + } + if let id = message.adAttribute?.opaqueId { + if item.height == item.view?.visibleRect.height { + readAds.append(id) + } + } } + if let msg = message, let currentMsg = item.messages.last { + if msg.id.namespace == Namespaces.Message.Local && currentMsg.id.namespace == Namespaces.Message.Local { + if msg.id < currentMsg.id { + message = currentMsg + } + } + } } return true }) + if topVisibleMessageRange != nil { + strongSelf.topVisibleMessageRange.set(topVisibleMessageRange) + } + + if !readAds.isEmpty { + for data in readAds { + strongSelf.adMessages?.markAsSeen(opaqueId: data) + } + } - - + strongSelf.genericView.updateFailedIds(strongSelf.genericView.failedIds, hasOnScreen: hasFailed, animated: true) if !messageIdsWithViewCount.isEmpty { strongSelf.messageProcessingManager.add(messageIdsWithViewCount) @@ -1690,6 +4705,9 @@ class ChatController: EditableViewController, Notifable { if !messageIdsWithUnseenPersonalMention.isEmpty { strongSelf.messageMentionProcessingManager.add(messageIdsWithUnseenPersonalMention) } + if !unsupportedMessagesIds.isEmpty { + strongSelf.unsupportedMessageProcessingManager.add(unsupportedMessagesIds) + } if let message = message { strongSelf.updateMaxVisibleReadIncomingMessageIndex(MessageIndex(message)) @@ -1698,25 +4716,130 @@ class ChatController: EditableViewController, Notifable { } }) + + switch self.mode { + case .history: + let failed = context.account.postbox.failedMessageIdsView(peerId: peerId) |> deliverOnMainQueue + + var failedAnimate: Bool = true + failedMessageIdsDisposable.set(failed.start(next: { [weak self] view in + var hasFailed: Bool = false + + self?.genericView.tableView.enumerateVisibleItems(with: { item in + if let item = item as? ChatRowItem { + if let message = item.message, message.flags.contains(.Failed) { + hasFailed = !(message.media.first is TelegramMediaAction) + } + for message in item.messages { + if !hasFailed, message.flags.contains(.Failed) { + hasFailed = !(message.media.first is TelegramMediaAction) + } + } + } + return !hasFailed + }) + + self?.genericView.updateFailedIds(view.ids, hasOnScreen: hasFailed, animated: !failedAnimate) + failedAnimate = true + })) + + + + let hasScheduledMessages = peerView.get() + |> take(1) + |> mapToSignal { view -> Signal in + if let view = view as? PeerView, let peer = peerViewMainPeer(view) as? TelegramChannel, !peer.hasPermission(.sendMessages) { + return .single(false) + } else { + return context.account.viewTracker.scheduledMessagesViewForLocation(.peer(peerId)) + |> map { view, _, _ in + return !view.entries.isEmpty + } + } + } |> deliverOnMainQueue + + hasScheduledMessagesDisposable.set(hasScheduledMessages.start(next: { [weak self] hasScheduledMessages in + self?.chatInteraction.update({ + $0.withUpdatedHasScheduled(hasScheduledMessages) + }) + })) + + default: + break + } + + + + let discussion: Signal = peerView.get() + |> map { view -> CachedChannelData? in + return (view as? PeerView)?.cachedData as? CachedChannelData + } |> mapToSignal { value in + if let threadId = mode.threadId { + return context.account.viewTracker.polledChannel(peerId: threadId.peerId) + } else if let peerDiscussionId = value?.linkedDiscussionPeerId { + switch peerDiscussionId { + case let .known(peerId): + if let peerId = peerId { + return context.account.viewTracker.polledChannel(peerId: peerId) + } + default: + break + } + } + return .single(Void()) + } + + + pollChannelDiscussionDisposable.set(discussion.start()) + + } + + override func updateFrame(_ frame: NSRect, animated: Bool) { + super.updateFrame(frame, animated: animated) + self.genericView.updateFrame(frame, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate) } + private func openScheduledChat() { + self.chatInteraction.saveState(scrollState: self.immediateScrollState()) + self.navigationController?.push(ChatScheduleController(context: context, chatLocation: self.chatLocation)) + } + + @available(OSX 10.12.2, *) + override func makeTouchBar() -> NSTouchBar? { + if let temporaryTouchBar = temporaryTouchBar as? ChatTouchBar { + temporaryTouchBar.updateChatInteraction(self.chatInteraction, textView: self.genericView.inputView.textView.inputView) + } else { + temporaryTouchBar = ChatTouchBar(chatInteraction: self.chatInteraction, textView: self.genericView.inputView.textView.inputView) + } + return temporaryTouchBar as? NSTouchBar + } override func windowDidBecomeKey() { super.windowDidBecomeKey() + if #available(OSX 10.12.2, *) { + (temporaryTouchBar as? ChatTouchBar)?.updateByKeyWindow() + } + updateInteractiveReading() chatInteraction.saveState(scrollState: immediateScrollState()) } override func windowDidResignKey() { super.windowDidResignKey() + if #available(OSX 10.12.2, *) { + (temporaryTouchBar as? ChatTouchBar)?.updateByKeyWindow() + } + updateInteractiveReading() chatInteraction.saveState(scrollState:immediateScrollState()) } private func anchorMessageInCurrentHistoryView() -> Message? { - if let historyView = self.previousView.modify({$0}) { + + let historyView = self.previousView.with { $0 } + if let historyView = historyView { let visibleRange = self.genericView.tableView.visibleRows() var index = 0 for entry in historyView.filteredEntries.reversed() { if index >= visibleRange.min && index <= visibleRange.max { - if case let .MessageEntry(message, _, _, _, _) = entry.entry { + if case let .MessageEntry(message, _, _, _, _, _, _) = entry.entry { return message } } @@ -1724,85 +4847,181 @@ class ChatController: EditableViewController, Notifable { } for entry in historyView.filteredEntries { - if let message = entry.entry.message { + if let message = entry.appearance.entry.message { return message } } } - return nil + return nil + } + + private func updateInteractiveReading() { + switch mode { + case .history: + let scroll = genericView.scroll + let hasEntries = self.previousView.with { $0?.filteredEntries.count ?? 0 } > 1 + if let window = window, window.isKeyWindow, self.historyState.isDownOfHistory && scroll.rect.minY == genericView.tableView.frame.height, hasEntries { + self.interactiveReadingDisposable.set(context.engine.messages.installInteractiveReadMessagesAction(peerId: chatInteraction.peerId)) + } else { + self.interactiveReadingDisposable.set(nil) + } + + default: + self.interactiveReadingDisposable.set(nil) + } + + } + + + + private func messageInCurrentHistoryView(_ id: MessageId) -> Message? { + return self.previousView.with { view in + if let historyView = view { + for entry in historyView.filteredEntries { + if let message = entry.appearance.entry.message, message.id == id { + return message + } + } + } + return nil + } + } + + func isSearchAvailable(_ presentation: ChatPresentationInterfaceState) -> Bool { + if presentation.reportMode != nil { + return false + } + var isEmpty: Bool = genericView.tableView.isEmpty + if chatInteraction.mode.isThreadMode { + isEmpty = genericView.tableView.count == (theme.bubbled ? 4 : 3) + } + if chatInteraction.mode == .scheduled || isEmpty { + return false + } else { + return true + } } - private func messageInCurrentHistoryView(_ id: MessageId) -> Message? { - if let historyView = self.previousView.modify({$0}) { - for entry in historyView.filteredEntries { - if let message = entry.entry.message, message.id == id { - return message - } - } - } - return nil + var searchAvailable: Bool { + isSearchAvailable(chatInteraction.presentation) } + private var firstLoad: Bool = true - func applyTransition(_ transition:TableUpdateTransition, initialData:ChatHistoryCombinedInitialData) { + override func updateBackgroundColor(_ backgroundMode: TableBackgroundMode) { + super.updateBackgroundColor(backgroundMode) + genericView.updateBackground(backgroundMode, navigationView: self.navigationController?.view) + } + + func applyTransition(_ transition:TableUpdateTransition, initialData:ChatHistoryCombinedInitialData, isLoading: Bool, processedView: ChatHistoryView) { - let view = previousView.modify({$0})! - // NSLog("1") - let _ = nextTransaction.execute() - // NSLog("2") + let wasEmpty = genericView.tableView.isEmpty + initialDataHandler.set(.single(initialData)) - // NSLog("3") + + historyState = historyState.withUpdatedStateOfHistory(processedView.originalView?.laterId == nil) + + let oldState = genericView.state + + genericView.change(state: isLoading ? .progress : .visible, animated: processedView.originalView != nil) + + + self.currentAnimationRows = [] genericView.tableView.merge(with: transition) - // NSLog("4") + + self.updateBackgroundColor(processedView.theme.controllerBackgroundMode) + + + let animated: Bool + switch transition.state { + case let .none(interface): + animated = interface != nil + default: + animated = transition.animated + } + + collectFloatingPhotos(animated: animated, currentAnimationRows: currentAnimationRows) + + let _ = nextTransaction.execute() + + + if oldState != genericView.state { + genericView.tableView.updateEmpties(animated: previousView.with { $0?.originalView != nil }) + } + genericView.tableView.notifyScrollHandlers() - // NSLog("5") - genericView.change(state: .visible, animated: true) - // NSLog("6") - historyState = historyState.withUpdatedStateOfHistory(view.originalView.laterId == nil) - // NSLog("7") - if !view.originalView.entries.isEmpty { + if !transition.isEmpty, let afterNextTransaction = self.afterNextTransaction { + delay(0.1, closure: afterNextTransaction) + self.afterNextTransaction = nil + } + + + + (self.centerBarView as? ChatTitleBarView)?.updateSearchButton(hidden: !searchAvailable, animated: transition.animated) + + if genericView.tableView.isEmpty, let peer = chatInteraction.peer, peer.isBot { + if chatInteraction.presentation.initialAction == nil && self.genericView.state == .visible { + chatInteraction.update(animated: false, {$0.updatedInitialAction(ChatInitialAction.start(parameter: "", behavior: .none))}) + } + } + chatInteraction.update(animated: !wasEmpty, { current in + var current = current.updatedHistoryCount(genericView.tableView.count - 1).updatedKeyboardButtonsMessage(initialData.buttonKeyboardMessage) - let tableView = genericView.tableView - if !tableView.isEmpty { - - var earliest:Message? - var latest:Message? - self.genericView.tableView.enumerateVisibleItems(reversed: true, with: { item -> Bool in - - if let item = item as? ChatRowItem { - earliest = item.message - } - return earliest == nil - }) - - self.genericView.tableView.enumerateVisibleItems { item -> Bool in - - if let item = item as? ChatRowItem { - latest = item.message + if let message = initialData.buttonKeyboardMessage { + if message.requestsSetupReply { + if message.id != current.interfaceState.dismissedForceReplyId { + current = current.updatedInterfaceState({$0.withUpdatedReplyMessageId(message.id)}) } - return latest == nil } - - if let earliest = earliest, let latest = latest { - account.postbox.updateMessageHistoryViewVisibleRange(view.originalView.id, earliestVisibleIndex: MessageIndex(earliest), latestVisibleIndex: MessageIndex(latest)) - } } - } else if let peer = peer, peer.isBot { - if chatInteraction.presentation.initialAction == nil && self.genericView.state == .visible { - chatInteraction.update(animated: false, {$0.updatedInitialAction(ChatInitialAction.start(parameter: "", behavior: .none))}) + return current + }) + + readyHistory() + + updateInteractiveReading() + + + self.centerBarView.animates = true + + self.chatInteraction.invokeInitialAction(includeAuto: true, animated: false) + + + genericView.tableView.enumerateVisibleViews(with: { view in + if let view = view as? ChatRowView { + view.updateBackground(animated: transition.animated, item: view.item) + } + }) + + + if firstLoad { + firstLoad = false + + let peerId = self.chatLocation.peerId + + let tags: [MessageTags] = [.photoOrVideo, .file, .webPage, .music, .voiceOrInstantVideo] + + let tabItems: [Signal] = tags.map { tags -> Signal in + return context.account.viewTracker.aroundMessageOfInterestHistoryViewForLocation(.peer(peerId), count: 20, tagMask: tags) + |> ignoreValues } +// + loadSharedMediaDisposable.set(combineLatest(tabItems).start()) } - // NSLog("8") - chatInteraction.update(animated: false, {$0.updatedHistoryCount(genericView.tableView.count - 1).updatedKeyboardButtonsMessage(initialData.buttonKeyboardMessage)}) - // NSLog("9") - if !didSetHistoryReady { - didSetHistoryReady = true - _historyReady.set(.single(true)) + + switch self.mode { + case .pinned: + if genericView.tableView.isEmpty { + navigationController?.back() + } + default: + break } } + override func getCenterBarViewOnce() -> TitledBarView { return ChatTitleBarView(controller: self, chatInteraction) } @@ -1816,26 +5035,30 @@ class ChatController: EditableViewController, Notifable { editButton?.set(image: theme.icons.chatActions, for: .Normal) editButton?.set(image: theme.icons.chatActionsActive, for: .Highlight) + editButton?.setFrameSize(70, 50) editButton?.center() - doneButton?.set(color: theme.colors.blueUI, for: .Normal) + doneButton?.set(color: theme.colors.accent, for: .Normal) doneButton?.style = navigationButtonStyle } + override func getRightBarViewOnce() -> BarView { let back = BarView(70, controller: self) //MajorBackNavigationBar(self, account: account, excludePeerId: peerId) let editButton = ImageButton() - editButton.disableActions() + // editButton.disableActions() back.addSubview(editButton) self.editButton = editButton // let doneButton = TitleButton() - doneButton.disableActions() + // doneButton.disableActions() doneButton.set(font: .medium(.text), for: .Normal) - doneButton.set(text: tr(.navigationDone), for: .Normal) - doneButton.sizeToFit() + doneButton.set(text: tr(L10n.navigationDone), for: .Normal) + + + _ = doneButton.sizeToFit() back.addSubview(doneButton) doneButton.center() @@ -1848,61 +5071,190 @@ class ChatController: EditableViewController, Notifable { editButton.userInteractionEnabled = false back.set(handler: { [weak self] _ in - if let state = self?.state { - switch state { - case .Normal: - if let button = self?.editButton, let strongSelf = self { - - let account = strongSelf.account - let peerId = strongSelf.peerId - - _ = (strongSelf.peerView.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak strongSelf] peerView in - if let strongSelf = strongSelf { - var items:[SPopoverItem] = [] - - items.append(SPopoverItem(tr(.chatContextInfo), { [weak strongSelf] in - if let strongSelf = strongSelf { - strongSelf.chatInteraction.openInfo(strongSelf.peerId, false, nil, nil) - } + self?.showRightControls() + }, for: .Click) + requestUpdateRightBar() + return back + } + + private func showRightControls() { + switch state { + case .Normal: + if let button = editButton { + let context = self.context + showRightControlsDisposable.set((peerView.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak self] view in + guard let `self` = self else {return} + var items:[SPopoverItem] = [] + let peerId = self.chatLocation.peerId + switch self.mode { + case .scheduled: + items.append(SPopoverItem(L10n.chatContextClearScheduled, { + confirm(for: context.window, header: L10n.chatContextClearScheduledConfirmHeader, information: L10n.chatContextClearScheduledConfirmInfo, okTitle: L10n.chatContextClearScheduledConfirmOK, successHandler: { _ in + _ = context.engine.messages.clearHistoryInteractively(peerId: peerId, type: .scheduledMessages).start() + }) + }, theme.icons.chatActionClearHistory)) + case .history: + switch self.chatLocation { + case let .peer(peerId): + guard let peerView = view as? PeerView else {return} + + items.append(SPopoverItem(tr(L10n.chatContextEdit1) + (FastSettings.tooltipAbility(for: .edit) ? " (\(L10n.chatContextEditHelp))" : ""), { [weak self] in + self?.changeState() + }, theme.icons.chatActionEdit)) + if peerId != repliesPeerId { + items.append(SPopoverItem(L10n.chatContextInfo, { [weak self] in + self?.chatInteraction.openInfo(peerId, false, nil, nil) }, theme.icons.chatActionInfo)) + } + + + + + if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings, !self.isAdChat { + if self.chatInteraction.peerId != context.peerId { + items.append(SPopoverItem(!notificationSettings.isMuted ? L10n.chatContextEnableNotifications : L10n.chatContextDisableNotifications, { [weak self] in + self?.chatInteraction.toggleNotifications(notificationSettings.isMuted) + }, !notificationSettings.isMuted ? theme.icons.chatActionUnmute : theme.icons.chatActionMute)) + } + } + + if let peer = peerView.peers[peerView.peerId], let mainPeer = peerViewMainPeer(peerView) { - items.append(SPopoverItem(tr(.chatContextEdit), { [weak strongSelf] in - strongSelf?.changeState() - }, theme.icons.chatActionEdit)) + var activeCall = (peerView.cachedData as? CachedGroupData)?.activeCall + activeCall = activeCall ?? (peerView.cachedData as? CachedChannelData)?.activeCall + + if peer.groupAccess.canMakeVoiceChat { + var isLiveStream: Bool = false + if let peer = peer as? TelegramChannel { + isLiveStream = peer.isChannel || peer.flags.contains(.isGigagroup) + } + items.append(SPopoverItem(isLiveStream ? L10n.peerInfoActionLiveStream : L10n.peerInfoActionVoiceChat, { [weak self] in + self?.makeVoiceChat(activeCall, callJoinPeerId: nil) + }, theme.icons.chat_info_voice_chat)) + } + if peer.isUser, peer.id != context.peerId { + items.append(SPopoverItem(L10n.chatContextCreateGroup, { [weak self] in + self?.createGroup() + }, theme.icons.chat_info_create_group)) + + items.append(SPopoverItem(L10n.peerInfoChatColors, { [weak self] in + self?.showChatThemeSelector() + }, theme.icons.chat_info_change_colors)) + } - if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings { - items.append(SPopoverItem(notificationSettings.isMuted ? tr(.chatContextEnableNotifications) : tr(.chatContextDisableNotifications), { [weak strongSelf] in - strongSelf?.chatInteraction.toggleNotifications() - }, notificationSettings.isMuted ? theme.icons.chatActionUnmute : theme.icons.chatActionMute)) + if let groupId = peerView.groupId, groupId != .root { + items.append(SPopoverItem(L10n.chatContextUnarchive, { + _ = updatePeerGroupIdInteractively(postbox: context.account.postbox, peerId: peerId, groupId: .root).start() + }, theme.icons.chatUnarchive)) + } else { + items.append(SPopoverItem(L10n.chatContextArchive, { + _ = updatePeerGroupIdInteractively(postbox: context.account.postbox, peerId: peerId, groupId: Namespaces.PeerGroup.archive).start() + }, theme.icons.chatArchive)) } - if let peer = peerViewMainPeer(peerView) { - if peer.isGroup || peer.isUser || (peer.isSupergroup && peer.addressName == nil) { - items.append(SPopoverItem(tr(.chatContextClearHistory), { - confirm(for: mainWindow, with: appName, and: tr(.confirmDeleteChatUser), successHandler: { _ in - _ = clearHistoryInteractively(postbox: account.postbox, peerId: peerId).start() - }) - }, theme.icons.chatActionClearHistory)) + if peer.canSendMessage(self.mode.isThreadMode), peerView.peerId.namespace != Namespaces.Peer.SecretChat { + let text: String + if peer.id != context.peerId { + text = L10n.chatRightContextScheduledMessages + } else { + text = L10n.chatRightContextReminder } + items.append(SPopoverItem(text, { [weak self] in + self?.openScheduledChat() + }, theme.icons.scheduledInputAction)) + } + + if peer.canClearHistory || (peer.canManageDestructTimer && context.peerId != peer.id) { + items.append(SPopoverItem(L10n.chatContextClearHistory, { + clearHistory(context: context, peer: peer, mainPeer: mainPeer) + }, theme.icons.chatActionClearHistory)) + } + + let deleteChat = { [weak self] in + guard let `self` = self else {return} + let signal = removeChatInteractively(context: context, peerId: self.chatInteraction.peerId, userId: self.chatInteraction.peer?.id) |> filter {$0} |> mapToSignal { _ -> Signal in + return context.globalPeerHandler.get() |> take(1) + } |> deliverOnMainQueue + + self.deleteChatDisposable.set(signal.start(next: { [weak self] location in + if location == self?.chatInteraction.chatLocation { + self?.context.sharedContext.bindings.rootNavigation().close() + } + })) + } + + let text: String + if peer.isGroup { + text = L10n.chatListContextDeleteAndExit + } else if peer.isChannel { + text = L10n.chatListContextLeaveChannel + } else if peer.isSupergroup { + text = L10n.chatListContextLeaveGroup + } else { + text = L10n.chatListContextDeleteChat } - showPopover(for: button, with: SPopoverViewController(items: items), edge: .maxY, inset: NSMakePoint(0, -65)) - //ContextMenu.show(items: items, view: button, event: event, onShow: {_ in }, onClose: {}) + + + items.append(SPopoverItem(text, deleteChat, theme.icons.chatActionDeleteChat)) + } - - }) - - + case .replyThread: + break + } + case .replyThread: + items.append(SPopoverItem(L10n.chatContextEdit1, { [weak self] in + self?.changeState() + }, theme.icons.chatActionEdit)) + case .pinned: + items.append(SPopoverItem(L10n.chatContextEdit1, { [weak self] in + self?.changeState() + }, theme.icons.chatActionEdit)) + case .preview: + break } - case .Edit: - self?.changeState() - case .Some: - break - } + if !items.isEmpty { + if let popover = button.popover { + popover.hide() + } else { + showPopover(for: button, with: SPopoverViewController(items: items, visibility: 10), edge: .maxY, inset: NSMakePoint(0, -65)) + } + } + })) } - //self?.navigationController?.back() - }, for: .Click) - requestUpdateRightBar() - return back + case .Edit: + changeState() + case .Some: + break + } + } + private func createGroup() { + context.composeCreateGroup(selectedPeers: [self.chatLocation.peerId]) + } + + private func makeVoiceChat(_ current: CachedChannelData.ActiveCall?, callJoinPeerId: PeerId?) { + let context = self.context + let peerId = self.chatLocation.peerId + if let activeCall = current { + let join:(PeerId, Date?)->Void = { joinAs, _ in + _ = showModalProgress(signal: requestOrJoinGroupCall(context: context, peerId: peerId, joinAs: joinAs, initialCall: activeCall, initialInfo: nil, joinHash: nil), for: context.window).start(next: { result in + switch result { + case let .samePeer(callContext): + applyGroupCallResult(context.sharedContext, callContext) + case let .success(callContext): + applyGroupCallResult(context.sharedContext, callContext) + default: + alert(for: context.window, info: L10n.errorAnError) + } + }) + } + if let callJoinPeerId = callJoinPeerId { + join(callJoinPeerId, nil) + } else { + selectGroupCallJoiner(context: context, peerId: peerId, completion: join) + } + } else { + createVoiceChat(context: context, peerId: peerId, canBeScheduled: true) + } } override func getLeftBarViewOnce() -> BarView { @@ -1913,13 +5265,44 @@ class ChatController: EditableViewController, Notifable { return back } +// override func invokeNavigationBack() -> Bool { +// return !context.closeFolderFirst +// } + override func escapeKeyAction() -> KeyHandlerResult { - var result:KeyHandlerResult = self.chatInteraction.presentation.effectiveInput.inputText.isEmpty ? .rejected : .invokeNext - if chatInteraction.presentation.state == .selecting { + +// +// if context.closeFolderFirst { +// return .rejected +// } + + if genericView.inputView.textView.inputView.hasMarkedText() { + return .invokeNext + } + + if chatInteraction.presentation.interfaceState.themeEditing { + self.themeSelector?.close(true) + return .invoked + } + + var result:KeyHandlerResult = .rejected + if chatInteraction.presentation.botMenu?.revealed == true { + self.chatInteraction.update({ + $0.updateBotMenu({ current in + var current = current + current?.revealed = false + return current + }) + }) + result = .invoked + } else if chatInteraction.presentation.reportMode != nil { + self.changeState() + result = .invoked + } else if chatInteraction.presentation.state == .selecting { self.changeState() result = .invoked } else if chatInteraction.presentation.state == .editing { - chatInteraction.update({$0.withoutEditMessage()}) + chatInteraction.cancelEditing() result = .invoked } else if case let .contextRequest(request) = chatInteraction.presentation.inputContext { if request.query.isEmpty { @@ -1928,21 +5311,63 @@ class ChatController: EditableViewController, Notifable { chatInteraction.clearContextQuery() } result = .invoked - } else if chatInteraction.presentation.isSearchMode { - chatInteraction.update({$0.updatedSearchMode(false)}) + } else if chatInteraction.presentation.isSearchMode.0 { + chatInteraction.update({$0.updatedSearchMode((false, nil, nil))}) result = .invoked + } else if chatInteraction.presentation.recordingState != nil { + chatInteraction.update({$0.withoutRecordingState()}) + return .invoked + } else if chatInteraction.presentation.interfaceState.replyMessageId != nil { + if chatInteraction.presentation.interfaceState.inputState.inputText.isEmpty { + chatInteraction.update({$0.updatedInterfaceState({$0.withUpdatedReplyMessageId(nil)})}) + return .invoked + } } return result } override func backKeyAction() -> KeyHandlerResult { - return !self.chatInteraction.presentation.isSearchMode && self.chatInteraction.presentation.effectiveInput.inputText.isEmpty ? .rejected : .invokeNext + + if let window = window, hasModals(window) { + return .invokeNext + } + if let event = NSApp.currentEvent, event.modifierFlags.contains(.shift) { + if !selectManager.isEmpty { + _ = selectManager.selectPrevChar() + return .invoked + } + } + + return !self.chatInteraction.presentation.isSearchMode.0 && self.chatInteraction.presentation.effectiveInput.inputText.isEmpty ? .rejected : .invokeNext + } + + override func returnKeyAction() -> KeyHandlerResult { + if let recordingState = chatInteraction.presentation.recordingState { + recordingState.stop() + chatInteraction.mediaPromise.set(recordingState.data) + closeAllModals() + chatInteraction.update({$0.withoutRecordingState()}) + return .invoked + } + return super.returnKeyAction() } override func nextKeyAction() -> KeyHandlerResult { - if !self.chatInteraction.presentation.isSearchMode && chatInteraction.presentation.effectiveInput.inputText.isEmpty { - chatInteraction.openInfo(peerId, false, nil, nil) + + if let window = window, hasModals(window) { + return .invokeNext + } + + if let event = NSApp.currentEvent, event.modifierFlags.contains(.shift) { + if !selectManager.isEmpty { + _ = selectManager.selectNextChar() + return .invoked + } + } + + if !self.chatInteraction.presentation.isSearchMode.0 && chatInteraction.presentation.effectiveInput.inputText.isEmpty { + chatInteraction.openInfo(chatInteraction.peerId, false, nil, nil) return .invoked } return .rejected @@ -1950,6 +5375,7 @@ class ChatController: EditableViewController, Notifable { deinit { + failedMessageEventsDisposable.dispose() historyDisposable.dispose() peerDisposable.dispose() updatedChannelParticipants.dispose() @@ -1965,101 +5391,465 @@ class ChatController: EditableViewController, Notifable { peerInputActivitiesDisposable.dispose() connectionStatusDisposable.dispose() messagesActionDisposable.dispose() - openPeerInfoDisposable.dispose() unblockDisposable.dispose() updatePinnedDisposable.dispose() reportPeerDisposable.dispose() focusMessageDisposable.dispose() updateFontSizeDisposable.dispose() - account.context.addRecentlyUsedPeer(peerId: peerId) + context.addRecentlyUsedPeer(peerId: chatInteraction.peerId) loadFwdMessagesDisposable.dispose() chatUnreadMentionCountDisposable.dispose() navigationActionDisposable.dispose() messageIndexDisposable.dispose() dateDisposable.dispose() - account.context.cachedAdminIds.remove(for: peerId) + interactiveReadingDisposable.dispose() + showRightControlsDisposable.dispose() + deleteChatDisposable.dispose() + loadSelectionMessagesDisposable.dispose() + updateMediaDisposable.dispose() + editCurrentMessagePhotoDisposable.dispose() + selectMessagePollOptionDisposables.dispose() + onlineMemberCountDisposable.dispose() + chatUndoDisposable.dispose() + chatInteraction.clean() + discussionDataLoadDisposable.dispose() + slowModeDisposable.dispose() + slowModeInProgressDisposable.dispose() + forwardMessagesDisposable.dispose() + updateReqctionsDisposable.dispose() + shiftSelectedDisposable.dispose() + failedMessageIdsDisposable.dispose() + hasScheduledMessagesDisposable.dispose() + updateUrlDisposable.dispose() + loadSharedMediaDisposable.dispose() + pollChannelDiscussionDisposable.dispose() + loadThreadDisposable.dispose() + recordActivityDisposable.dispose() + suggestionsDisposable.dispose() + peekDisposable.dispose() + _ = previousView.swap(nil) + + context.closeFolderFirst = false } public override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + + suggestionsDisposable.set(nil) + + sentMessageEventsDisposable.set(nil) + peekDisposable.set(nil) + genericView.inputContextHelper.viewWillRemove() self.chatInteraction.remove(observer: self) chatInteraction.saveState(scrollState: immediateScrollState()) - window?.removeAllHandlers(for: self) + context.window.removeAllHandlers(for: self) + + if let window = window { + selectTextController.removeHandlers(for: window) + } + } + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + } + + override func didRemovedFromStack() { + super.didRemovedFromStack() + chatInteraction.remove(observer: self) + } + + private var splitStateFirstUpdate: Bool = true + override func viewDidChangedNavigationLayout(_ state: SplitViewState) -> Void { + super.viewDidChangedNavigationLayout(state) + chatInteraction.update(animated: false, {$0.withUpdatedLayout(state).withToggledSidebarEnabled(FastSettings.sidebarEnabled).withToggledSidebarShown(FastSettings.sidebarShown)}) + if !splitStateFirstUpdate { + Queue.mainQueue().justDispatch { [weak self] in + self?.genericView.tableView.layoutItems() + } + } + splitStateFirstUpdate = false + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if let initialAction = self.chatInteraction.presentation.initialAction { + switch initialAction { + case let .closeAfter(peek): + self.chatInteraction.closeAfterPeek(peek) + default: + break + } + } + + let context = self.context + context.closeFolderFirst = false + + self.context.sharedContext.bindings.entertainment().update(with: self.chatInteraction) + + chatInteraction.update(animated: false, {$0.withToggledSidebarEnabled(FastSettings.sidebarEnabled).withToggledSidebarShown(FastSettings.sidebarShown)}) + //NSLog("chat apeeared") + + self.failedMessageEventsDisposable.set((context.account.pendingMessageManager.failedMessageEvents(peerId: chatInteraction.peerId) + |> deliverOnMainQueue).start(next: { [weak self] reason in + if let strongSelf = self { + let text: String + switch reason { + case .flood: + text = L10n.chatSendMessageErrorFlood + case .publicBan: + text = L10n.chatSendMessageErrorGroupRestricted + case .mediaRestricted: + text = L10n.chatSendMessageErrorGroupRestricted + case .slowmodeActive: + text = L10n.chatSendMessageSlowmodeError + case .tooMuchScheduled: + text = L10n.chatSendMessageErrorTooMuchScheduled + } + confirm(for: context.window, information: text, cancelTitle: "", thridTitle: L10n.genericErrorMoreInfo, successHandler: { [weak strongSelf] confirm in + guard let strongSelf = strongSelf else {return} + + switch confirm { + case .thrid: + execute(inapp: inAppLink.followResolvedName(link: "@spambot", username: "spambot", postId: nil, context: context, action: nil, callback: { [weak strongSelf] peerId, openChat, postid, initialAction in + strongSelf?.chatInteraction.openInfo(peerId, openChat, postid, initialAction) + })) + default: + break + } + }) + } + })) + + + if let peer = chatInteraction.peer { + if peer.isRestrictedChannel(context.contentSettings), let reason = peer.restrictionText { + alert(for: context.window, info: reason, completion: { [weak self] in + self?.dismiss() + }) + } else if chatInteraction.presentation.isNotAccessible { + alert(for: context.window, info: peer.isChannel ? L10n.chatChannelUnaccessible : L10n.chatGroupUnaccessible, completion: { [weak self] in + self?.dismiss() + }) + } + } + + + + self.context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + if let strongSelf = self, let window = strongSelf.window, !hasModals(window) { + let result:KeyHandlerResult = strongSelf.chatInteraction.presentation.effectiveInput.inputText.isEmpty && strongSelf.chatInteraction.presentation.state == .normal ? .invoked : .rejected + + if result == .invoked { + let setup = strongSelf.findAndSetEditableMessage() + if !setup { + strongSelf.genericView.tableView.scrollUp() + } + } else { + if strongSelf.chatInteraction.presentation.effectiveInput.inputText.isEmpty { + strongSelf.genericView.tableView.scrollUp() + } + } + + return result + } + return .rejected + }, with: self, for: .UpArrow, priority: .low) + + + self.context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + if let strongSelf = self, let window = strongSelf.window, !hasModals(window) { + let result:KeyHandlerResult = strongSelf.chatInteraction.presentation.effectiveInput.inputText.isEmpty ? .invoked : .invokeNext + + + if result == .invoked { + strongSelf.genericView.tableView.scrollDown() + } + + return result + } + return .rejected + }, with: self, for: .DownArrow, priority: .low) + + self.context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + if let `self` = self, let window = self.window, !hasModals(window), self.chatInteraction.presentation.interfaceState.editState == nil, self.chatInteraction.presentation.interfaceState.inputState.inputText.isEmpty { + var currentReplyId = self.chatInteraction.presentation.interfaceState.replyMessageId + self.genericView.tableView.enumerateItems(with: { item in + if let item = item as? ChatRowItem, let message = item.message { + if canReplyMessage(message, peerId: self.chatInteraction.peerId, mode: self.chatInteraction.mode), currentReplyId == nil || (message.id < currentReplyId!) { + currentReplyId = message.id + self.genericView.tableView.scroll(to: .center(id: item.stableId, innerId: nil, animated: true, focus: .init(focus: true), inset: 0), inset: NSEdgeInsetsZero, timingFunction: .linear) + return false + } + } + return true + }) + + let result:KeyHandlerResult = currentReplyId != nil ? .invoked : .rejected + self.chatInteraction.setupReplyMessage(currentReplyId) + + return result + } + return .rejected + }, with: self, for: .UpArrow, priority: .low, modifierFlags: [.command]) + + self.context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + if let `self` = self, let window = self.window, !hasModals(window), self.chatInteraction.presentation.interfaceState.editState == nil, self.chatInteraction.presentation.interfaceState.inputState.inputText.isEmpty { + var currentReplyId = self.chatInteraction.presentation.interfaceState.replyMessageId + self.genericView.tableView.enumerateItems(reversed: true, with: { item in + if let item = item as? ChatRowItem, let message = item.message { + if canReplyMessage(message, peerId: self.chatInteraction.peerId, mode: self.chatInteraction.mode), currentReplyId != nil && (message.id > currentReplyId!) { + currentReplyId = message.id + self.genericView.tableView.scroll(to: .center(id: item.stableId, innerId: nil, animated: true, focus: .init(focus: true), inset: 0), inset: NSEdgeInsetsZero, timingFunction: .linear) + return false + } + } + return true + }) + + let result:KeyHandlerResult = currentReplyId != nil ? .invoked : .rejected + self.chatInteraction.setupReplyMessage(currentReplyId) + + return result + } + return .rejected + }, with: self, for: .DownArrow, priority: .low, modifierFlags: [.command]) + - if let window = window { - selectTextController.removeHandlers(for: window) - } - } - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - } - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) + self.context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + guard let `self` = self, let window = self.window, !hasModals(window) else {return .rejected} + + if let selectionState = self.chatInteraction.presentation.selectionState, !selectionState.selectedIds.isEmpty { + self.chatInteraction.deleteSelectedMessages() + return .invoked + } + + return .rejected + }, with: self, for: .Delete, priority: .low) + self.context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + guard let `self` = self else {return .rejected} + + if let selectionState = self.chatInteraction.presentation.selectionState, !selectionState.selectedIds.isEmpty { + self.chatInteraction.deleteSelectedMessages() + return .invoked + } + + return .rejected + }, with: self, for: .ForwardDelete, priority: .low) - self.window?.set(handler: {[weak self] () -> KeyHandlerResult in - if let strongSelf = self, !hasModals() { - let result:KeyHandlerResult = strongSelf.chatInteraction.presentation.effectiveInput.inputText.isEmpty && strongSelf.chatInteraction.presentation.state == .normal ? .invoked : .rejected - - if result == .invoked { - strongSelf.findAndSetEditableMessage() - } - - return result + + + + self.context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + if let strongSelf = self, strongSelf.context.window.firstResponder != strongSelf.genericView.inputView.textView.inputView { + _ = strongSelf.context.window.makeFirstResponder(strongSelf.genericView.inputView) + return .invoked + } else if (self?.navigationController as? MajorNavigationController)?.genericView.state == .single { + return .invoked } return .rejected - }, with: self, for: .UpArrow, priority: .low) + }, with: self, for: .Tab, priority: .high) - self.window?.set(handler: { [weak self] () -> KeyHandlerResult in - if let strongSelf = self { - strongSelf.chatInteraction.update({$0.updatedSearchMode(!$0.isSearchMode)}) + + + self.context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + guard let `self` = self, self.mode != .scheduled, self.searchAvailable else {return .rejected} + if !self.chatInteraction.presentation.isSearchMode.0 { + self.chatInteraction.update({$0.updatedSearchMode((true, nil, nil))}) + } else { + self.genericView.applySearchResponder() } + return .invoked }, with: self, for: .F, priority: .medium, modifierFlags: [.command]) - self.window?.set(handler: { [weak self] () -> KeyHandlerResult in + +// #if DEBUG +// self.context.window.set(handler: { [weak self] _ -> KeyHandlerResult in +// guard let `self` = self else {return .rejected} +// showModal(with: GigagroupLandingController(context: context, peerId: self.chatLocation.peerId), for: context.window) +// return .invoked +// }, with: self, for: .E, priority: .medium, modifierFlags: [.command]) +// #endif + + self.context.window.set(handler: { [weak self] _ -> KeyHandlerResult in self?.genericView.inputView.makeBold() return .invoked }, with: self, for: .B, priority: .medium, modifierFlags: [.command]) + self.context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.genericView.inputView.removeAllAttributes() + return .invoked + }, with: self, for: .Backslash, priority: .medium, modifierFlags: [.command]) + + self.context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.genericView.inputView.makeUrl() + return .invoked + }, with: self, for: .U, priority: .medium, modifierFlags: [.command]) - self.window?.set(handler: { [weak self] () -> KeyHandlerResult in + self.context.window.set(handler: { [weak self] _ -> KeyHandlerResult in self?.genericView.inputView.makeItalic() return .invoked }, with: self, for: .I, priority: .medium, modifierFlags: [.command]) - self.window?.set(handler: { [weak self] () -> KeyHandlerResult in + self.context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + guard let `self` = self else { return .rejected } + self.chatInteraction.startRecording(true, nil) + return .invoked + }, with: self, for: .R, priority: .medium, modifierFlags: [.command]) + + + self.context.window.set(handler: { [weak self] _ -> KeyHandlerResult in self?.genericView.inputView.makeMonospace() return .invoked }, with: self, for: .K, priority: .medium, modifierFlags: [.command, .shift]) - if !(window?.firstResponder is NSTextView) { + + self.context.window.add(swipe: { [weak self] direction, _ -> SwipeHandlerResult in + guard let `self` = self, let window = self.window, self.chatInteraction.presentation.state == .normal else {return .failed} + let swipeState: SwipeState? + switch direction { + case .left: + return .failed + case let .right(_state): + swipeState = _state + case .none: + swipeState = nil + } + + guard let state = swipeState else {return .failed} + + + + switch state { + case .start: + let row = self.genericView.tableView.row(at: self.genericView.tableView.clipView.convert(window.mouseLocationOutsideOfEventStream, from: nil)) + if row != -1 { + guard let item = self.genericView.tableView.item(at: row) as? ChatRowItem, let message = item.message, canReplyMessage(message, peerId: self.chatInteraction.peerId, mode: self.chatInteraction.mode) else {return .failed} + (item.view as? RevealTableView)?.initRevealState() + return .success(RevealTableItemController(item: item)) + } else { + return .failed + } + + case let .swiping(_delta, controller): + let controller = controller as! RevealTableItemController + + guard let view = controller.item.view as? RevealTableView else {return .nothing} + + var delta:CGFloat + switch direction { + case .left: + delta = _delta//max(0, _delta) + case .right: + delta = -_delta//min(-_delta, 0) + default: + delta = _delta + } + + let newDelta = min(min(300, view.width) * log2(abs(delta) + 1) * log2(min(300, view.width)) / 100.0, abs(delta)) + + if delta < 0 { + delta = -newDelta + } else { + delta = newDelta + } + + + view.moveReveal(delta: delta) + self.updateFloatingPhotos(self.genericView.scroll, animated: false) + case let .success(_, controller), let .failed(_, controller): + let controller = controller as! RevealTableItemController + guard let view = (controller.item.view as? RevealTableView) else {return .nothing} + + view.completeReveal(direction: direction) + self.updateFloatingPhotos(self.genericView.scroll, animated: true) + } + + // return .success() + + return .nothing + }, with: self.genericView.tableView, identifier: "chat-reply-swipe") + + + + if !(context.window.firstResponder is NSTextView) { self.genericView.inputView.makeFirstResponder() } if let window = window { selectTextController.initializeHandlers(for: window, chatInteraction:chatInteraction) } + // if !context.isInGlobalSearch { + _ = context.window.makeFirstResponder(genericView.inputView.textView.inputView) + // } + + var beginPendingTime:CFAbsoluteTime? - window?.makeFirstResponder(genericView.inputView.textView.inputView) + self.sentMessageEventsDisposable.set((context.account.pendingMessageManager.deliveredMessageEvents(peerId: self.chatLocation.peerId) |> deliverOn(Queue.concurrentDefaultQueue())).start(next: { _ in + + if FastSettings.inAppSounds { + let afterSentSound:NSSound? = { + + let p = Bundle.main.path(forResource: "sent", ofType: "caf") + var sound:NSSound? + if let p = p { + sound = NSSound(contentsOfFile: p, byReference: true) + sound?.volume = 1.0 + } + + return sound + }() + + if let beginPendingTime = beginPendingTime { + if CFAbsoluteTimeGetCurrent() - beginPendingTime < 0.5 { + return + } + } + beginPendingTime = CFAbsoluteTimeGetCurrent() + afterSentSound?.play() + } + })) + + let suggestions = getPeerSpecificServerProvidedSuggestions(postbox: context.account.postbox, peerId: self.chatLocation.peerId) |> deliverOnMainQueue + let peerId = self.chatLocation.peerId + + suggestionsDisposable.set(suggestions.start(next: { suggestions in + for suggestion in suggestions { + switch suggestion { + case .convertToGigagroup: + confirm(for: context.window, header: L10n.broadcastGroupsLimitAlertTitle, information: L10n.broadcastGroupsLimitAlertText(Formatter.withSeparator.string(from: NSNumber(value: context.limitConfiguration.maxSupergroupMemberCount))!), okTitle: L10n.broadcastGroupsLimitAlertLearnMore, successHandler: { _ in + showModal(with: GigagroupLandingController(context: context, peerId: peerId), for: context.window) + }, cancelHandler: { + showModalText(for: context.window, text: L10n.broadcastGroupsLimitAlertSettingsTip) + }) + _ = dismissPeerSpecificServerProvidedSuggestion(account: context.account, peerId: peerId, suggestion: suggestion).start() + } + } + })) + } - func findAndSetEditableMessage() -> Void { - let view = self.previousView.modify({$0}) - if let view = view?.originalView, view.laterId == nil { - for entry in view.entries.reversed() { - if case let .MessageEntry(message,_,_,_) = entry { - if canEditMessage(message, account:account) { - chatInteraction.beginEditingMessage(message) - return + + func findAndSetEditableMessage(_ bottom: Bool = false) -> Bool { + if let view = self.previousView.with({ $0?.originalView }), view.laterId == nil { + for entry in (!bottom ? view.entries.reversed() : view.entries) { + if let messageId = chatInteraction.presentation.interfaceState.editState?.message.id { + if (messageId <= entry.message.id && !bottom) || (messageId >= entry.message.id && bottom) { + continue } } + if canEditMessage(entry.message, chatInteraction: chatInteraction, context: context) { + chatInteraction.beginEditingMessage(entry.message) + return true + } } } + return false } override func firstResponder() -> NSResponder? { @@ -2072,10 +5862,14 @@ class ChatController: EditableViewController, Notifable { public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - globalPeerHandler.set(.single(peerId)) - chatInteraction.update(animated: false, {$0.withToggledSidebarEnabled(FastSettings.sidebarEnabled).withToggledSidebarShown(FastSettings.sidebarShown)}) - account.context.entertainment.update(with: chatInteraction) self.chatInteraction.add(observer: self) + self.context.globalPeerHandler.set(.single(chatLocation)) + self.genericView.tableView.notifyScrollHandlers() + self.genericView.updateHeader(chatInteraction.presentation, false, false) + if let controller = globalAudio, let header = self.navigationController?.header, header.needShown { + let object = InlineAudioPlayerView.ContextObject(controller: controller, context: context, tableView: genericView.tableView, supportTableView: nil) + header.view.update(with: object) + } } private func updateMaxVisibleReadIncomingMessageIndex(_ index: MessageIndex) { @@ -2088,61 +5882,218 @@ class ChatController: EditableViewController, Notifable { chatInteraction.applyAction(action: action) } - public init(account:Account, peerId:PeerId, messageId:MessageId? = nil, initialAction:ChatInitialAction? = nil) { - self.peerId = peerId - self.chatInteraction = ChatInteraction(peerId:peerId, account:account) - super.init(account) - self.chatInteraction.update(animated: false, {$0.updatedInitialAction(initialAction)}) - account.context.checkFirstRecentlyForDuplicate(peerId:peerId) + private let isAdChat: Bool + private let messageId: MessageId? + let mode: ChatMode + + public init(context: AccountContext, chatLocation:ChatLocation, mode: ChatMode = .history, messageId:MessageId? = nil, initialAction: ChatInitialAction? = nil, chatLocationContextHolder: Atomic = Atomic(value: nil)) { + self.chatLocation = chatLocation + self.messageId = messageId + self.chatLocationContextHolder = chatLocationContextHolder + self.mode = mode + self.chatInteraction = ChatInteraction(chatLocation: chatLocation, context: context, mode: mode) + if let action = initialAction { + switch action { + case .ad: + isAdChat = true + default: + isAdChat = false + } + } else { + isAdChat = false + } - self.messageProcessingManager.process = { [weak account] messageIds in - account?.viewTracker.updateViewCountForMessageIds(messageIds: messageIds) + if chatLocation.peerId.namespace == Namespaces.Peer.CloudChannel { + self.adMessages = context.engine.messages.adMessages(peerId: chatLocation.peerId) + } else { + self.adMessages = nil } - self.messageMentionProcessingManager.process = { [weak account] messageIds in - account?.viewTracker.updateMarkMentionsSeenForMessageIds(messageIds: messageIds) + + var takeTableItem:((MessageId)->TableRowItem?)? = nil + + self.emojiEffects = EmojiScreenEffect(context: chatInteraction.context, takeTableItem: { msgId in + return takeTableItem?(msgId) + }) + super.init(context) + + self.chatInteraction.update(animated: false, {$0.updatedInitialAction(initialAction)}) + context.checkFirstRecentlyForDuplicate(peerId: chatInteraction.peerId) + + let clientId = nextClientId + nextClientId += 1 + + + self.messageProcessingManager.process = { messageIds in + context.account.viewTracker.updateViewCountForMessageIds(messageIds: messageIds.filter({$0.namespace == Namespaces.Message.Cloud}), clientId: clientId) + } + + self.unsupportedMessageProcessingManager.process = { messageIds in + context.account.viewTracker.updateUnsupportedMediaForMessageIds(messageIds: messageIds.filter({$0.namespace == Namespaces.Message.Cloud})) + } + self.messageMentionProcessingManager.process = { messageIds in + context.account.viewTracker.updateMarkMentionsSeenForMessageIds(messageIds: messageIds.filter({$0.namespace == Namespaces.Message.Cloud})) } + self.location.set(peerView.get() |> take(1) |> deliverOnMainQueue |> map { [weak self] view -> ChatHistoryLocation in if let strongSelf = self { - let count = Int(round(strongSelf.view.frame.height / 28)) + 30 + let count = Int(round(strongSelf.view.frame.height / 28)) + 2 let location:ChatHistoryLocation - if let messageId = messageId { - location = .InitialSearch(location: .id(messageId), count: count) - } else { - location = .Initial(count: count) + switch strongSelf.mode { + case let .replyThread(data, _): + switch data.initialAnchor { + case .automatic: + if let messageId = messageId { + location = .InitialSearch(location: .id(messageId), count: count + 10) + } else { + location = .Initial(count: count + 10) + } + case let .lowerBoundMessage(index): + location = .Scroll(index: .message(index), anchorIndex: .message(index), sourceIndex: .message(index), scrollPosition: .up(false), count: count + 10, animated: false) + } + default: + if let messageId = messageId { + location = .InitialSearch(location: .id(messageId), count: count + 10) + } else { + location = .Initial(count: count) + } } + return location } return .Initial(count: 30) }) - + _ = (self.location.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak self] location in + _ = self?._locationValue.swap(location) + }) + + chatInteraction.contextHolder = { [weak self] in + return self?.chatLocationContextHolder ?? Atomic(value: nil) + } + + takeTableItem = { [weak self] msgId in + if self?.isLoaded() == false { + return nil + } + var found: TableRowItem? = nil + self?.genericView.tableView.enumerateVisibleItems(with: { item in + if let item = item as? ChatRowItem, item.message?.id == msgId { + found = item + return false + } else { + return true + } + }) + return found + } } func notify(with value: Any, oldValue: Any, animated:Bool) { notify(with: value, oldValue: oldValue, animated: animated, force: false) } + private var isPausedGlobalPlayer: Bool = false func notify(with value: Any, oldValue: Any, animated:Bool, force:Bool) { if let value = value as? ChatPresentationInterfaceState, let oldValue = oldValue as? ChatPresentationInterfaceState { - if value.inputQueryResult != oldValue.inputQueryResult { + let context = self.context + let mode = self.chatInteraction.mode + + if value.selectionState != oldValue.selectionState { + if let selectionState = value.selectionState { + let ids = Array(selectionState.selectedIds) + loadSelectionMessagesDisposable.set((context.account.postbox.messagesAtIds(ids) |> deliverOnMainQueue).start( next:{ [weak self] messages in + var canDelete:Bool = !ids.isEmpty + var canForward:Bool = !ids.isEmpty + if let chatInteraction = self?.chatInteraction { + for message in messages { + if !canDeleteMessage(message, account: context.account, mode: mode) { + canDelete = false + } + if !canForwardMessage(message, chatInteraction: chatInteraction) { + canForward = false + } + } + chatInteraction.update({$0.withUpdatedBasicActions((canDelete, canForward))}) + } + + })) + } else { + DispatchQueue.main.async { [weak self] in + self?.chatInteraction.update({$0.withUpdatedBasicActions((false, false))}) + } + } + if value.selectionState != nil { + _ = window?.makeFirstResponder(selectManager) + } else { + _ = window?.makeFirstResponder(self.firstResponder()) + } + } + +// if #available(OSX 10.12.2, *) { +// self.context.window.touchBar = self.context.window.makeTouchBar() +// } + + if oldValue.recordingState == nil && value.recordingState != nil { + if let pause = globalAudio?.pause() { + isPausedGlobalPlayer = pause + } + } else if value.recordingState == nil && oldValue.recordingState != nil { + if isPausedGlobalPlayer { + _ = globalAudio?.play() + } + } + if let until = value.slowMode?.validUntil, until > self.context.timestamp { + let signal = Signal.single(Void()) |> then(.single(Void()) |> delay(0.2, queue: .mainQueue()) |> restart) + slowModeDisposable.set(signal.start(next: { [weak self] in + if let `self` = self { + if until < self.context.timestamp { + self.chatInteraction.update({$0.updateSlowMode({ $0?.withUpdatedTimeout(nil) })}) + } else { + self.chatInteraction.update({$0.updateSlowMode({ $0?.withUpdatedTimeout(until - self.context.timestamp) })}) + } + } + })) + + } else { + self.slowModeDisposable.set(nil) + if let slowMode = value.slowMode, slowMode.timeout != nil { + DispatchQueue.main.async { [weak self] in + self?.chatInteraction.update({$0.updateSlowMode({ $0?.withUpdatedTimeout(nil) })}) + } + } + } + + if value.inputQueryResult != oldValue.inputQueryResult || value.state != oldValue.state { genericView.inputContextHelper.context(with: value.inputQueryResult, for: genericView, relativeView: genericView.inputView, animated: animated) } if value.interfaceState.inputState != oldValue.interfaceState.inputState { chatInteraction.saveState(false, scrollState: immediateScrollState()) + } - if value.selectionState != oldValue.selectionState { - doneButton?.isHidden = value.selectionState == nil - editButton?.isHidden = value.selectionState != nil + if value.interfaceState.forwardMessageIds != oldValue.interfaceState.forwardMessageIds { + let signal = (context.account.postbox.messagesAtIds(value.interfaceState.forwardMessageIds)) |> deliverOnMainQueue + forwardMessagesDisposable.set(signal.start(next: { [weak self] messages in + self?.chatInteraction.update(animated: animated, { + $0.updatedInterfaceState { + $0.withUpdatedForwardMessages(messages) + } + }) + })) + } + + if value.selectionState != oldValue.selectionState || value.reportMode != oldValue.reportMode { + doneButton?.isHidden = value.selectionState == nil || value.reportMode != nil + editButton?.isHidden = value.selectionState != nil || value.reportMode != nil } - if value.effectiveInput != oldValue.effectiveInput || force { - if let (updatedContextQueryState, updatedContextQuerySignal) = contextQueryResultStateForChatInterfacePresentationState(chatInteraction.presentation, account: self.account, currentQuery: self.contextQueryState?.0) { + if value.effectiveInput != oldValue.effectiveInput || value.botMenu != oldValue.botMenu || force { + if let (updatedContextQueryState, updatedContextQuerySignal) = contextQueryResultStateForChatInterfacePresentationState(chatInteraction.presentation, context: self.context, currentQuery: self.contextQueryState?.0) { self.contextQueryState?.1.dispose() var inScope = true var inScopeResult: ((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?)? @@ -2172,48 +6123,104 @@ class ChatController: EditableViewController, Notifable { } - - if let (updatedUrlPreviewUrl, updatedUrlPreviewSignal) = urlPreviewStateForChatInterfacePresentationState(chatInteraction.presentation, account: self.account, currentQuery: self.urlPreviewQueryState?.0) { - self.urlPreviewQueryState?.1.dispose() - var inScope = true - var inScopeResult: ((TelegramMediaWebpage?) -> TelegramMediaWebpage?)? - self.urlPreviewQueryState = (updatedUrlPreviewUrl, (updatedUrlPreviewSignal |> deliverOnMainQueue).start(next: { [weak self] result in - if let strongSelf = self { - if Thread.isMainThread && inScope { - inScope = false - inScopeResult = result - } else { - strongSelf.chatInteraction.update(animated: animated, { - if let updatedUrlPreviewUrl = updatedUrlPreviewUrl, let webpage = result($0.urlPreview?.1) { - return $0.updatedUrlPreview((updatedUrlPreviewUrl, webpage)) - } else { - return $0.updatedUrlPreview(nil) - } - }) + var disableEditingPreview:((String)->Void)? = nil + if oldValue.interfaceState.editState == nil, value.interfaceState.editState != nil { + disableEditingPreview = { [weak self] value in + self?.chatInteraction.update({ $0.updatedInterfaceState{ + $0.withUpdatedComposeDisableUrlPreview(value) + }}) + } + } + + let updateUrl = urlPreviewStateForChatInterfacePresentationState(chatInteraction.presentation, context: context, currentQuery: self.urlPreviewQueryState?.0, disableEditingPreview: disableEditingPreview) |> delay(value.effectiveInput.inputText.isEmpty ? 0.0 : 0.1, queue: .mainQueue()) |> deliverOnMainQueue + + updateUrlDisposable.set(updateUrl.start(next: { [weak self] result in + if let `self` = self, let (updatedUrlPreviewUrl, updatedUrlPreviewSignal) = result { + self.urlPreviewQueryState?.1.dispose() + var inScope = true + var inScopeResult: ((TelegramMediaWebpage?) -> TelegramMediaWebpage?)? + self.urlPreviewQueryState = (updatedUrlPreviewUrl, (updatedUrlPreviewSignal |> deliverOnMainQueue).start(next: { [weak self] result in + if let strongSelf = self { + if Thread.isMainThread && inScope { + inScope = false + inScopeResult = result + } else { + strongSelf.chatInteraction.update(animated: true, { + if let updatedUrlPreviewUrl = updatedUrlPreviewUrl, let webpage = result($0.urlPreview?.1) { + return $0.updatedUrlPreview((updatedUrlPreviewUrl, webpage)) + } else { + return $0.updatedUrlPreview(nil) + } + }) + } } + })) + inScope = false + if let inScopeResult = inScopeResult { + self.chatInteraction.update(animated: true, { + if let updatedUrlPreviewUrl = updatedUrlPreviewUrl, let webpage = inScopeResult($0.urlPreview?.1) { + return $0.updatedUrlPreview((updatedUrlPreviewUrl, webpage)) + } else { + return $0.updatedUrlPreview(nil) + } + }) } - })) - inScope = false - if let inScopeResult = inScopeResult { - chatInteraction.update(animated: animated, { - if let updatedUrlPreviewUrl = updatedUrlPreviewUrl, let webpage = inScopeResult($0.urlPreview?.1) { - return $0.updatedUrlPreview((updatedUrlPreviewUrl, webpage)) - } else { - return $0.updatedUrlPreview(nil) - } - }) } - } + })) + + } } - if value.isSearchMode != oldValue.isSearchMode || value.pinnedMessageId != oldValue.pinnedMessageId || value.reportStatus != oldValue.reportStatus || value.interfaceState.dismissedPinnedMessageId != oldValue.interfaceState.dismissedPinnedMessageId || value.canAddContact != oldValue.canAddContact { - genericView.updateHeader(value, animated) + if value.isSearchMode.0 != oldValue.isSearchMode.0 || value.pinnedMessageId != oldValue.pinnedMessageId || value.peerStatus != oldValue.peerStatus || value.interfaceState.dismissedPinnedMessageId != oldValue.interfaceState.dismissedPinnedMessageId || value.initialAction != oldValue.initialAction || value.restrictionInfo != oldValue.restrictionInfo || value.hidePinnedMessage != oldValue.hidePinnedMessage || value.groupCall != oldValue.groupCall || value.reportMode != oldValue.reportMode { + genericView.updateHeader(value, animated, value.hidePinnedMessage != oldValue.hidePinnedMessage) + (centerBarView as? ChatTitleBarView)?.updateStatus(true, presentation: value) + } + + if value.reportMode != oldValue.reportMode { + (self.centerBarView as? ChatTitleBarView)?.updateSearchButton(hidden: !isSearchAvailable(value), animated: animated) + } + + if value.peer != nil && oldValue.peer == nil { + genericView.tableView.emptyItem = ChatEmptyPeerItem(genericView.tableView.frame.size, chatInteraction: chatInteraction) } + var upgradedToPeerId: PeerId? + if let previous = oldValue.peer, let group = previous as? TelegramGroup, group.migrationReference == nil, let updatedGroup = value.peer as? TelegramGroup, let migrationReference = updatedGroup.migrationReference { + upgradedToPeerId = migrationReference.peerId + } + self.state = value.selectionState != nil ? .Edit : .Normal + if let upgradedToPeerId = upgradedToPeerId { + let controller = ChatController(context: context, chatLocation: .peer(upgradedToPeerId)) + navigationController?.removeAll() + navigationController?.push(controller, false, style: ViewControllerStyle.none) + } + + if value.recordingState != oldValue.recordingState { + if let state = value.recordingState { + let activity: PeerInputActivity = state is ChatRecordingAudioState ? .recordingVoice : .recordingInstantVideo + + let recursive = (Signal.single(Void()) |> then(.single(Void()) |> suspendAwareDelay(4, queue: .mainQueue()))) |> restart + + recordActivityDisposable.set(recursive.start(next: { [weak self] in + guard let `self` = self else { + return + } + self.context.account.updateLocalInputActivity(peerId: .init(peerId: self.chatLocation.peerId, category: self.mode.activityCategory), activity: activity, isPresent: true) + })) + + } else if let state = oldValue.recordingState { + let activity: PeerInputActivity = state is ChatRecordingAudioState ? .recordingVoice : .recordingInstantVideo + self.context.account.updateLocalInputActivity(peerId: .init(peerId: self.chatLocation.peerId, category: self.mode.activityCategory), activity: activity, isPresent: false) + recordActivityDisposable.set(nil) + } + } + + dismissedPinnedIds.set(ChatDismissedPins(ids: value.interfaceState.dismissedPinnedMessageId, tempMaxId: value.tempPinnedMaxId)) + } } @@ -2254,35 +6261,87 @@ class ChatController: EditableViewController, Notifable { } + public override func draggingExited() { + super.draggingExited() + genericView.inputView.isHidden = false + } + public override func draggingEntered() { + super.draggingEntered() + genericView.inputView.isHidden = true + } public override func draggingItems(for pasteboard:NSPasteboard) -> [DragItem] { + if let window = self.window, hasModals(window) { + return [] + } + + let peerId = self.chatInteraction.peerId + if let types = pasteboard.types, types.contains(.kFilenames) { let list = pasteboard.propertyList(forType: .kFilenames) as? [String] - if let list = list, list.count > 0, let peer = peer, peer.canSendMessage { + if let list = list, list.count > 0, let peer = chatInteraction.peer, peer.canSendMessage(chatInteraction.mode.isThreadMode) { - if peer.mediaRestricted { - return [] + if let text = permissionText(from: peer, for: .banSendMedia) { + return [DragItem(title: "", desc: text, handler: { + + })] } var items:[DragItem] = [] let list = list.filter { path -> Bool in - if let size = fileSize(path) { - return size <= 1500000000 + if let size = fs(path) { + return size <= 2000 * 1024 * 1024 } return false } + if list.count == 1, let editState = chatInteraction.presentation.interfaceState.editState, editState.canEditMedia { + return [DragItem(title: L10n.chatDropEditTitle, desc: L10n.chatDropEditDesc, handler: { [weak self] in + guard let strongSelf = self else { + return + } + NSApp.activate(ignoringOtherApps: true) + _ = (Sender.generateMedia(for: MediaSenderContainer(path: list[0], isFile: false), account: strongSelf.chatInteraction.context.account, isSecretRelated: peerId.namespace == Namespaces.Peer.SecretChat) |> deliverOnMainQueue).start(next: { media, _ in + self?.chatInteraction.update({$0.updatedInterfaceState({$0.updatedEditState({$0?.withUpdatedMedia(media)})})}) + }) + })] + } + + if !list.isEmpty { - let asMediaItem = DragItem(title:tr(.chatDropTitle), desc: tr(.chatDropQuickDesc), handler:{ [weak self] in - self?.chatInteraction.showPreviewSender(list.map { URL(fileURLWithPath: $0) }, true) + + + let asMediaItem = DragItem(title:tr(L10n.chatDropTitle), desc: tr(L10n.chatDropQuickDesc), handler:{ [weak self] in + NSApp.activate(ignoringOtherApps: true) + let shift = NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false + if shift { + self?.chatInteraction.sendMedia(list.map{MediaSenderContainer(path: $0, caption: "", isFile: false)}) + } else { + self?.chatInteraction.showPreviewSender(list.map { URL(fileURLWithPath: $0) }, true, nil) + } }) + let fileTitle: String + let fileDesc: String - let asFileItem = DragItem(title:tr(.chatDropTitle), desc: tr(.chatDropAsFilesDesc), handler:{ [weak self] in - self?.chatInteraction.showPreviewSender(list.map { URL(fileURLWithPath: $0) }, false) + if list.count == 1, list[0].isDirectory { + fileTitle = L10n.chatDropFolderTitle + fileDesc = L10n.chatDropFolderDesc + } else { + fileTitle = L10n.chatDropTitle + fileDesc = L10n.chatDropAsFilesDesc + } + let asFileItem = DragItem(title: fileTitle, desc: fileDesc, handler: { [weak self] in + NSApp.activate(ignoringOtherApps: true) + let shift = NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false + if shift { + self?.chatInteraction.sendMedia(list.map{MediaSenderContainer(path: $0, caption: "", isFile: true)}) + } else { + self?.chatInteraction.showPreviewSender(list.map { URL(fileURLWithPath: $0) }, false, nil) + } }) items.append(asFileItem) @@ -2309,18 +6368,32 @@ class ChatController: EditableViewController, Notifable { let data = pasteboard.data(forType: .tiff) if let data = data, let image = NSImage(data: data) { + if let editState = chatInteraction.presentation.interfaceState.editState, editState.canEditMedia { + return [DragItem(title: L10n.chatDropEditTitle, desc: L10n.chatDropEditDesc, handler: { [weak self] in + guard let strongSelf = self else { + return + } + NSApp.activate(ignoringOtherApps: true) + _ = (putToTemp(image: image) |> mapToSignal {Sender.generateMedia(for: MediaSenderContainer(path: $0, isFile: false), account: strongSelf.chatInteraction.context.account, isSecretRelated: peerId.namespace == Namespaces.Peer.SecretChat) } |> deliverOnMainQueue).start(next: { media, _ in + self?.chatInteraction.update({$0.updatedInterfaceState({$0.updatedEditState({$0?.withUpdatedMedia(media)})})}) + }) + })] + } + var items:[DragItem] = [] - let asMediaItem = DragItem(title:tr(.chatDropTitle), desc: tr(.chatDropQuickDesc), handler:{ [weak self] in + let asMediaItem = DragItem(title:tr(L10n.chatDropTitle), desc: tr(L10n.chatDropQuickDesc), handler:{ [weak self] in + NSApp.activate(ignoringOtherApps: true) _ = (putToTemp(image: image) |> deliverOnMainQueue).start(next: { [weak self] path in - self?.chatInteraction.sendMedia([MediaSenderContainer(path:path, isFile:false)]) + self?.chatInteraction.showPreviewSender([URL(fileURLWithPath: path)], true, nil) }) }) - let asFileItem = DragItem(title:tr(.chatDropTitle), desc: tr(.chatDropAsFilesDesc), handler:{ [weak self] in + let asFileItem = DragItem(title:tr(L10n.chatDropTitle), desc: tr(L10n.chatDropAsFilesDesc), handler:{ [weak self] in + NSApp.activate(ignoringOtherApps: true) _ = (putToTemp(image: image) |> deliverOnMainQueue).start(next: { [weak self] path in - self?.chatInteraction.sendMedia([MediaSenderContainer(path:path, isFile: true)]) + self?.chatInteraction.showPreviewSender([URL(fileURLWithPath: path)], false, nil) }) }) @@ -2333,31 +6406,102 @@ class ChatController: EditableViewController, Notifable { return [] } + + override public var isOpaque: Bool { + return false + } + + override func updateController() { + genericView.tableView.enumerateVisibleViews(with: { view in + if let view = view as? ChatRowView { + view.updateBackground(animated: false, item: view.item) + } + }, force: true) + } override open func backSettings() -> (String,CGImage?) { - if account.context.layout == .single { + if context.sharedContext.layout == .single { return super.backSettings() } - return (tr(.navigationClose),nil) + return (tr(L10n.navigationClose),nil) } override public func update(with state:ViewControllerState) -> Void { super.update(with:state) - chatInteraction.update({state == .Normal ? $0.withoutSelectionState() : $0.withSelectionState()}) + chatInteraction.update({state == .Normal ? $0.withoutSelectionState().withUpdatedRepotMode(nil) : $0.withSelectionState()}) + context.window.applyResponderIfNeeded() } override func initializer() -> ChatControllerView { - return ChatControllerView.self.init(frame: NSMakeRect(_frameRect.minX, _frameRect.minY, _frameRect.width, _frameRect.height - self.bar.height), chatInteraction:chatInteraction, account:account); + return ChatControllerView(frame: NSMakeRect(_frameRect.minX, _frameRect.minY, _frameRect.width, _frameRect.height - self.bar.height), chatInteraction:chatInteraction); } override func requestUpdateCenterBar() { } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - self.centerBarView.updateLocalizationAndTheme() - (centerBarView as? ChatTitleBarView)?.updateStatus() + func showChatThemeSelector() { + self.themeSelector = ChatThemeSelectorController(context, chatTheme: chatThemeValue.get(), chatInteraction: self.chatInteraction) + self.themeSelector?.onReady = { [weak self] controller in + self?.genericView.showChatThemeSelector(controller.view, animated: true) + } + self.themeSelector?.close = { [weak self] drop in + if drop { + self?.chatThemeTempValue.set(.single(nil)) + } + self?.genericView.hideChatThemeSelector(animated: true) + self?.themeSelector = nil + self?.chatInteraction.update({ $0.updatedInterfaceState({ $0.withUpdatedThemeEditing(false) })}) + } + + self.themeSelector?.previewCurrent = { [weak self] theme in + self?.chatThemeTempValue.set(.single(theme)) + } + + self.themeSelector?._frameRect = NSMakeRect(0, self.frame.maxY, frame.width, 160) + self.themeSelector?.loadViewIfNeeded() + + self.chatInteraction.update({ $0.updatedInterfaceState({ $0.withUpdatedThemeEditing(true) })}) + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + (centerBarView as? ChatTitleBarView)?.updateStatus(presentation: chatInteraction.presentation) + } + + + func selectionDidChange(row:Int, item:TableRowItem, byClick:Bool, isNew:Bool) -> Void { + + } + func selectionWillChange(row:Int, item:TableRowItem, byClick: Bool) -> Bool { + return false + } + func isSelectable(row:Int, item:TableRowItem) -> Bool { + return false + } + func findGroupStableId(for stableId: AnyHashable) -> AnyHashable? { + return previousView.with { view in + if let view = view, let stableId = stableId.base as? ChatHistoryEntryId { + switch stableId { + case let .message(message): + for entry in view.filteredEntries { + s: switch entry.entry { + case let .groupedPhotos(entries, _): + for groupedEntry in entries { + if message.id == groupedEntry.message?.id { + return entry.stableId + } + } + default: + break s + } + } + default: + break + } + } + return nil + } } diff --git a/Telegram-Mac/ChatDiceContentView.swift b/Telegram-Mac/ChatDiceContentView.swift new file mode 100644 index 0000000000..311461b8f4 --- /dev/null +++ b/Telegram-Mac/ChatDiceContentView.swift @@ -0,0 +1,398 @@ +// +// ChatDiceContentView.swift +// Telegram +// +// Created by Mikhail Filimonov on 27.02.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import Postbox +import TelegramCore + +import TGUIKit +import SwiftSignalKit + +let diceSide1: String = "1️⃣" +let diceSide2: String = "2️⃣" +let diceSide3: String = "3️⃣" +let diceSide4: String = "4️⃣" +let diceSide5: String = "5️⃣" +let diceSide6: String = "6️⃣" +let diceSide7: String = "7️⃣" +let diceSide8: String = "8️⃣" +let diceSide9: String = "9️⃣" +let diceIdle: String = "#️⃣" + + + + + + +private extension Int32 { + var diceSide: String { + switch self { + case 1: + return diceSide1 + case 2: + return diceSide2 + case 3: + return diceSide3 + case 4: + return diceSide4 + case 5: + return diceSide5 + case 6: + return diceSide6 + case 7: + return diceSide7 + case 8: + return diceSide8 + case 9: + return diceSide9 + default: + preconditionFailure() + } + } +} + + +private enum DicePlay : Equatable { + case idle + case failed + case end(animated: Bool) +} + +private struct DiceState : Equatable { + let messageId: MessageId + let message: Message + let play: DicePlay + + static func ==(lhs: DiceState, rhs: DiceState) -> Bool { + return lhs.play == rhs.play && isEqualMessages(lhs.message, rhs.message) + } + + init(message: Message) { + self.message = message + self.messageId = message.id + if let dice = message.media.first as? TelegramMediaDice, dice.value == 0 { + play = .idle + } else if message.forwardInfo != nil { + play = .end(animated: false) + } else { + if message.flags.contains(.Failed) { + self.play = .failed + } else if message.flags.isSending { + play = .idle + } else { + if !FastSettings.diceHasAlreadyPlayed(message) { + play = .end(animated: true) + } else { + play = .end(animated: false) + } + } + } + + } +} + +class ChatDiceContentView: ChatMediaContentView { + private let playerView: LottiePlayerView = LottiePlayerView(frame: NSMakeRect(0, 0, 240, 240)) + private let thumbView = TransformImageView() + private let loadResourceDisposable = MetaDisposable() + private let stateDisposable = MetaDisposable() + private var diceState: DiceState? + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(self.playerView) + addSubview(self.thumbView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func clean() { + loadResourceDisposable.dispose() + } + + deinit { + clean() + } + + func removeNotificationListeners() { + NotificationCenter.default.removeObserver(self) + } + + override func viewDidUpdatedDynamicContent() { + super.viewDidUpdatedDynamicContent() + updatePlayerIfNeeded() + } + + + + + @objc func updatePlayerIfNeeded() { + + } + + private var nextForceAccept: Bool = false + + + override func executeInteraction(_ isControl: Bool) { + + let media = self.media as? TelegramMediaDice + + if let media = media, let message = self.parent { + let item = self.table?.item(stableId: ChatHistoryEntryId.message(message)) + + if let item = item as? ChatRowItem, let peer = item.peer, peer.canSendMessage(item.chatInteraction.mode.isThreadMode) { + let text: String + + switch media.emoji { + case diceSymbol: + text = L10n.chatEmojiDiceResultNew + case dartSymbol: + text = L10n.chatEmojiDartResultNew + default: + text = L10n.chatEmojiDefResultNew(media.emoji) + } + let view: NSView + if !thumbView.isHidden { + view = thumbView + } else { + view = playerView + } + tooltip(for: view, text: text, interactions: globalLinkExecutor, button: (L10n.chatEmojiSend, { [weak item] in + item?.chatInteraction.sendPlainText(media.emoji) + }), offset: NSMakePoint(0, -30)) + } + } + // alert(for: window, info: L10n.chatDiceResult) + } + + var chatLoopAnimated: Bool { + if let context = self.context { + return context.autoplayMedia.loopAnimatedStickers + } + return true + } + + func updateListeners() { + if let window = window { + NotificationCenter.default.removeObserver(self) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSWindow.didBecomeKeyNotification, object: window) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSWindow.didResignKeyNotification, object: window) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSView.boundsDidChangeNotification, object: table?.contentView) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSView.frameDidChangeNotification, object: self.enclosingScrollView?.documentView) + } else { + removeNotificationListeners() + } + } + + override func viewWillDraw() { + super.viewWillDraw() + updatePlayerIfNeeded() + } + + override func willRemove() { + super.willRemove() + updateListeners() + updatePlayerIfNeeded() + } + + override func viewDidMoveToSuperview() { + updateListeners() + updatePlayerIfNeeded() + } + + override func viewDidMoveToWindow() { + updateListeners() + updatePlayerIfNeeded() + } + + override func update(with media: Media, size: NSSize, context: AccountContext, parent: Message?, table: TableView?, parameters: ChatMediaLayoutParameters?, animated: Bool, positionFlags: LayoutPositionFlags?, approximateSynchronousValue: Bool) { + + + if parent?.stableId != self.parent?.stableId { + self.playerView.set(nil) + } + + super.update(with: media, size: size, context: context, parent: parent, table: table, parameters: parameters, animated: animated, positionFlags: positionFlags, approximateSynchronousValue: approximateSynchronousValue) + + guard let media = media as? TelegramMediaDice, let parent = parent else { + return + } + + let baseSymbol: String = media.emoji + let sideSymbol: String + + let currentValue = media.value + + if let currentValue = currentValue, currentValue > 0 && currentValue <= 9 { + sideSymbol = currentValue.diceSide + } else { + sideSymbol = diceIdle + } + + let settings = InteractiveEmojiConfiguration.with(appConfiguration: context.appConfiguration) + + let diceState = DiceState(message: parent) + + + self.diceState = diceState + + + let data: Signal<(Data?, TelegramMediaFile), NoError> = context.diceCache.interactiveSymbolData(baseSymbol: baseSymbol, synchronous: approximateSynchronousValue) |> mapToSignal { values in + for value in values { + if value.0 == sideSymbol { + return .single((value.1, value.2)) + } + } + return .never() + } + + + self.playerView.isHidden = true + self.thumbView.isHidden = false + + self.playerView.animation?.triggerOn = nil + self.playerView.animation?.onFinish = nil + + self.loadResourceDisposable.set((data |> deliverOnMainQueue).start(next: { [weak self] data in + guard let `self` = self else { + return + } + let playPolicy: LottiePlayPolicy + + var saveContext: Bool = false + switch diceState.play { + case .failed: + playPolicy = .framesCount(1) + case .idle: + playPolicy = .loop + case let .end(toEndWithAnimation): + if !toEndWithAnimation || approximateSynchronousValue || self.visibleRect.height == 0 { + if self.visibleRect.height == 0 && toEndWithAnimation && !approximateSynchronousValue { + let item = self.table?.item(stableId: ChatHistoryEntryId.message(parent)) + if let item = item, let table = self.table, table.visibleRows().contains(item.index) { + playPolicy = .toEnd(from: 0) + saveContext = true + } else { + playPolicy = .toEnd(from: .max) + FastSettings.markDiceAsPlayed(parent) + } + } else { + playPolicy = .toEnd(from: .max) + FastSettings.markDiceAsPlayed(parent) + } + + } else { + saveContext = true + playPolicy = .toEnd(from: 0) + } + + } + if let bytes = data.0 { + let animation = LottieAnimation(compressed: bytes, key: LottieAnimationEntryKey(key: .media(data.1.id), size: size), cachePurpose: .none, playPolicy: playPolicy, maximumFps: 60) + + animation.onFinish = { + if case .end = diceState.play { + FastSettings.markDiceAsPlayed(parent) + } + } + switch diceState.play { + case let .end(animated): + if let previous = self.playerView.animation, animated { + switch self.playerView.currentState { + case .playing: + previous.triggerOn = (.last, { [weak self] in + self?.playerView.set(animation, saveContext: saveContext) + if animated, let confetti = settings.playConfetti(baseSymbol), confetti.value == currentValue { + animation.triggerOn = (.custom(confetti.playAt), { [weak self] in + if self?.visibleRect.height == self?.frame.height { + PlayConfetti(for: context.window) + } + }, {}) + } + }, { [weak self] in + self?.playerView.set(animation) + }) + default: + self.playerView.set(animation) + } + + } else { + self.playerView.set(animation) + } + default: + self.playerView.set(animation) + } + } else { + self.playerView.set(nil) + } + + self.stateDisposable.set((self.playerView.state |> deliverOnMainQueue).start(next: { [weak self] state in + guard let `self` = self else { return } + switch state { + case .playing: + self.playerView.isHidden = false + self.thumbView.isHidden = true + case .initializing, .failed: + switch diceState.play { + case let .end(animated): + if animated { + self.playerView.isHidden = false + self.thumbView.isHidden = true + } else { + self.playerView.isHidden = true + self.thumbView.isHidden = false + } + default: + self.playerView.isHidden = false + self.thumbView.isHidden = true + } + case .stoped: + switch diceState.play { + case let .end(animated): + if animated { + self.playerView.isHidden = false + self.thumbView.isHidden = true + } else { + self.playerView.isHidden = true + self.thumbView.isHidden = false + } + default: + self.playerView.isHidden = false + self.thumbView.isHidden = false + } + } + })) + + + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: NSEdgeInsets()) + + self.thumbView.setSignal(signal: cachedMedia(media: data.1, arguments: arguments, scale: self.backingScaleFactor), clearInstantly: true) + //if !self.thumbView.isFullyLoaded { + self.thumbView.setSignal(chatMessageDiceSticker(postbox: context.account.postbox, file: data.1, emoji: baseSymbol, value: sideSymbol, scale: self.backingScaleFactor, size: size), cacheImage: { result in + cacheMedia(result, media: data.1, arguments: arguments, scale: System.backingScale) + }) + self.thumbView.set(arguments: arguments) + // } + })) + // } else { + var bp:Int = 0 + bp += 1 + // } + + + + } + + override func layout() { + super.layout() + self.playerView.frame = bounds + self.thumbView.frame = bounds + } + +} diff --git a/Telegram-Mac/ChatEmptyPeerItem.swift b/Telegram-Mac/ChatEmptyPeerItem.swift index 2226ee7f9c..4dcdbd7acb 100644 --- a/Telegram-Mac/ChatEmptyPeerItem.swift +++ b/Telegram-Mac/ChatEmptyPeerItem.swift @@ -8,11 +8,14 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac +import TelegramCore + +import Postbox +import SwiftSignalKit + class ChatEmptyPeerItem: TableRowItem { - let textViewLayout:TextViewLayout + private(set) var textViewLayout:TextViewLayout override var stableId: AnyHashable { return 0 @@ -23,6 +26,10 @@ class ChatEmptyPeerItem: TableRowItem { return false } + override var index: Int { + return -1000 + } + override var height: CGFloat { if let table = table { return table.frame.height @@ -30,31 +37,116 @@ class ChatEmptyPeerItem: TableRowItem { return initialSize.height } + private let peerViewDisposable = MetaDisposable() + init(_ initialSize: NSSize, chatInteraction:ChatInteraction) { self.chatInteraction = chatInteraction let attr = NSMutableAttributedString() - if chatInteraction.peerId.namespace == Namespaces.Peer.SecretChat { - _ = attr.append(string: tr(.chatSecretChatEmptyHeader), color: theme.colors.grayText, font: .normal(.text)) - _ = attr.append(string: "\n\n") - _ = attr.append(string: tr(.chatSecretChat1Feature), color: theme.colors.grayText, font: .normal(.text)) - _ = attr.append(string: "\n") - _ = attr.append(string: tr(.chatSecretChat2Feature), color: theme.colors.grayText, font: .normal(.text)) - _ = attr.append(string: "\n") - _ = attr.append(string: tr(.chatSecretChat3Feature), color: theme.colors.grayText, font: .normal(.text)) - _ = attr.append(string: "\n") - _ = attr.append(string: tr(.chatSecretChat4Feature), color: theme.colors.grayText, font: .normal(.text)) - - } else { - _ = attr.append(string: tr(.chatEmptyChat), color: theme.colors.grayText, font: .normal(.text)) + var lineSpacing: CGFloat? = 5 + switch chatInteraction.mode { + case .history, .preview: + if chatInteraction.peerId.namespace == Namespaces.Peer.SecretChat { + _ = attr.append(string: L10n.chatSecretChatEmptyHeader, color: theme.chatServiceItemTextColor, font: .medium(.text)) + _ = attr.append(string: "\n") + _ = attr.append(string: L10n.chatSecretChat1Feature, color: theme.chatServiceItemTextColor, font: .medium(.text)) + _ = attr.append(string: "\n") + _ = attr.append(string: L10n.chatSecretChat2Feature, color: theme.chatServiceItemTextColor, font: .medium(.text)) + _ = attr.append(string: "\n") + _ = attr.append(string: L10n.chatSecretChat3Feature, color: theme.chatServiceItemTextColor, font: .medium(.text)) + _ = attr.append(string: "\n") + _ = attr.append(string: L10n.chatSecretChat4Feature, color: theme.chatServiceItemTextColor, font: .medium(.text)) + + } else if let peer = chatInteraction.peer, peer.isGroup || peer.isSupergroup, peer.groupAccess.isCreator { + _ = attr.append(string: L10n.emptyGroupInfoTitle, color: theme.chatServiceItemTextColor, font: .medium(.text)) + _ = attr.append(string: "\n") + _ = attr.append(string: L10n.emptyGroupInfoSubtitle, color: theme.chatServiceItemTextColor, font: .medium(.text)) + _ = attr.append(string: "\n") + _ = attr.append(string: L10n.emptyGroupInfoLine1(chatInteraction.presentation.limitConfiguration.maxSupergroupMemberCount.formattedWithSeparator), color: theme.chatServiceItemTextColor, font: .medium(.text)) + _ = attr.append(string: "\n") + _ = attr.append(string: L10n.emptyGroupInfoLine2, color: theme.chatServiceItemTextColor, font: .medium(.text)) + _ = attr.append(string: "\n") + _ = attr.append(string: L10n.emptyGroupInfoLine3, color: theme.chatServiceItemTextColor, font: .medium(.text)) + _ = attr.append(string: "\n") + _ = attr.append(string: L10n.emptyGroupInfoLine4, color: theme.chatServiceItemTextColor, font: .medium(.text)) + } else { + if let restriction = chatInteraction.presentation.restrictionInfo { + var hasRule: Bool = false + for rule in restriction.rules { + #if APP_STORE + if rule.platform == "ios" || rule.platform == "all" { + if !chatInteraction.context.contentSettings.ignoreContentRestrictionReasons.contains(rule.reason) { + _ = attr.append(string: rule.text, color: theme.chatServiceItemTextColor, font: .medium(.text)) + hasRule = true + break + } + } + #endif + } + if !hasRule { + _ = attr.append(string: L10n.chatEmptyChat, color: theme.chatServiceItemTextColor, font: .medium(.text)) + lineSpacing = nil + } + + } else { + lineSpacing = nil + _ = attr.append(string: L10n.chatEmptyChat, color: theme.chatServiceItemTextColor, font: .medium(.text)) + } + } + case .scheduled: + lineSpacing = nil + _ = attr.append(string: L10n.chatEmptyChat, color: theme.chatServiceItemTextColor, font: .medium(.text)) + case let .replyThread(_, mode): + lineSpacing = nil + switch mode { + case .comments: + _ = attr.append(string: L10n.chatEmptyComments, color: theme.chatServiceItemTextColor, font: .medium(.text)) + case .replies: + _ = attr.append(string: L10n.chatEmptyReplies, color: theme.chatServiceItemTextColor, font: .medium(.text)) + } + case .pinned: + lineSpacing = nil + _ = attr.append(string: L10n.chatEmptyChat, color: theme.chatServiceItemTextColor, font: .medium(.text)) } - textViewLayout = TextViewLayout(attr, alignment: .center) + + + textViewLayout = TextViewLayout(attr, alignment: .center, lineSpacing: lineSpacing, alwaysStaticItems: true) + textViewLayout.interactions = globalLinkExecutor + super.init(initialSize) + + + if chatInteraction.peerId.namespace == Namespaces.Peer.CloudUser { + peerViewDisposable.set((chatInteraction.context.account.postbox.peerView(id: chatInteraction.peerId) |> deliverOnMainQueue).start(next: { [weak self] peerView in + if let cachedData = peerView.cachedData as? CachedUserData, let user = peerView.peers[peerView.peerId], let botInfo = cachedData.botInfo { + var about = botInfo.description + if about.isEmpty { + about = cachedData.about ?? L10n.chatEmptyChat + } + if about.isEmpty { + about = L10n.chatEmptyChat + } + if user.isScam { + about = L10n.peerInfoScamWarning + } + if user.isFake { + about = L10n.peerInfoFakeWarning + } + guard let `self` = self else {return} + let attr = NSMutableAttributedString() + _ = attr.append(string: about, color: theme.chatServiceItemTextColor, font: .medium(.text)) + attr.detectLinks(type: [.Links, .Mentions, .Hashtags, .Commands], context: chatInteraction.context, color: theme.colors.link, openInfo:chatInteraction.openInfo, hashtag: chatInteraction.context.sharedContext.bindings.globalSearch, command: chatInteraction.sendPlainText, applyProxy: chatInteraction.applyProxy, dotInMention: false) + self.textViewLayout = TextViewLayout(attr, alignment: .left) + self.textViewLayout.interactions = globalLinkExecutor + self.view?.layout() + } + })) + } + } - override func makeSize(_ width: CGFloat, oldWidth:CGFloat) -> Bool { - textViewLayout.measure(width: width - 40) - return super.makeSize(width) + deinit { + peerViewDisposable.dispose() } override func viewClass() -> AnyClass { @@ -69,19 +161,56 @@ class ChatEmptyPeerView : TableRowView { required init(frame frameRect: NSRect) { super.init(frame: frameRect) addSubview(textView) + //containerView.addSubview(textView) + textView.isSelectable = false + textView.userInteractionEnabled = true + textView.disableBackgroundDrawing = true + } override func updateColors() { super.updateColors() - textView.background = backdorColor + textView.background = theme.chatServiceItemColor + } + + override func setFrameSize(_ newSize: NSSize) { + super.setFrameSize(newSize) + } + + override var backdorColor: NSColor { + return theme.wallpaper.wallpaper != .none ? .clear : theme.chatBackground + } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item) + needsLayout = true } override func layout() { super.layout() if let item = item as? ChatEmptyPeerItem { - item.textViewLayout.measure(width: frame.width - 40) + item.textViewLayout.measure(width: frame.width / 2) + + if item.textViewLayout.lineSpacing != nil { + for (i, line) in item.textViewLayout.lines.enumerated() { + if i == 0 { + line.penFlush = 0.5 + } else { + line.penFlush = 0.0 + } + } + } + textView.update(item.textViewLayout) + + let singleLine = item.textViewLayout.lines.count == 1 + + textView.setFrameSize( singleLine ? item.textViewLayout.layoutSize.width + 16 : item.textViewLayout.layoutSize.width + 30, singleLine ? 24 : item.textViewLayout.layoutSize.height + 20) textView.center() + + + + textView.layer?.cornerRadius = singleLine ? textView.frame.height / 2 : 8 } } diff --git a/Telegram-Mac/ChatFileContentView.swift b/Telegram-Mac/ChatFileContentView.swift index 23977695d1..5fe3815666 100644 --- a/Telegram-Mac/ChatFileContentView.swift +++ b/Telegram-Mac/ChatFileContentView.swift @@ -7,34 +7,41 @@ // import Cocoa -import SwiftSignalKitMac -import PostboxMac -import TelegramCoreMac +import SwiftSignalKit +import Postbox +import TelegramCore + import TGUIKit class ChatFileContentView: ChatMediaContentView { - var actionsLayout:TextViewLayout? - - let progressView:RadialProgressView = RadialProgressView() + private var actionsLayout:TextViewLayout? + private var progressView:RadialProgressView? + private var thumbProgress: RadialProgressView? private let thumbView:TransformImageView = TransformImageView() - var titleNode:TextNode = TextNode() - var actionText:TextView = TextView() + private var titleNode:TextNode = TextNode() + private var actionText:TextView = TextView() - var actionInteractions:TextViewInteractions = TextViewInteractions() + private var actionInteractions:TextViewInteractions = TextViewInteractions() private let statusDisposable = MetaDisposable() private let fetchDisposable = MetaDisposable() - + private let openFileDisposable = MetaDisposable() required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + override func previewMediaIfPossible() -> Bool { + guard let context = self.context, let window = self.kitWindow, let table = self.table, media?.isGraphicFile == true, fetchStatus == .Local else {return false} + _ = startModalPreviewHandle(table, window: window, context: context) + return true + } + required init(frame frameRect: NSRect) { super.init(frame:frameRect) actionText.isSelectable = false @@ -42,15 +49,12 @@ class ChatFileContentView: ChatMediaContentView { self.thumbView.setFrameSize(70,70) addSubview(thumbView) - progressView.fetchControls = fetchControls - addSubview(progressView) - actionInteractions.processURL = {[weak self] (link) in if let link = link as? String, link.hasSuffix("download") { self?.executeInteraction(false) } else if let link = link as? String, link.hasSuffix("finder") { - if let account = self?.account, let file = self?.media as? TelegramMediaFile { - showInFinder(file, account:account) + if let context = self?.context, let file = self?.media as? TelegramMediaFile { + showInFinder(file, account: context.account) } } } @@ -58,7 +62,7 @@ class ChatFileContentView: ChatMediaContentView { } override func mouseUp(with event: NSEvent) { - if thumbView._mouseInside() { + if thumbView._mouseInside(), userInteractionEnabled { executeInteraction(false) } else { super.mouseUp(with: event) @@ -66,146 +70,371 @@ class ChatFileContentView: ChatMediaContentView { } override func fetch() { - if let account = account, let media = media as? TelegramMediaFile { - fetchDisposable.set((chatMessageFileInteractiveFetched(account: account, file: media) |> mapToSignal { source -> Signal in - if source == .remote { - return copyToDownloads(media, account: account) - } else { - return .single(Void()) - } - }).start()) + if let context = context, let media = media as? TelegramMediaFile { + if let parent = parent { + fetchDisposable.set(messageMediaFileInteractiveFetched(context: context, messageId: parent.id, fileReference: FileMediaReference.message(message: MessageReference(parent), media: media)).start()) + } else { + fetchDisposable.set(freeMediaFileInteractiveFetched(context: context, fileReference: FileMediaReference.standalone(media: media)).start()) + } } } override func open() { - if let account = account, let media = media, let parent = parent { - if media.isGraphicFile { - showChatGallery(account: account, message: parent, table, parameters as? ChatMediaGalleryParameters) + if let context = context, let media = media as? TelegramMediaFile, let parent = parent { + if media.isGraphicFile || media.isVideoFile { + showChatGallery(context: context, message: parent, table, parameters as? ChatMediaGalleryParameters, type: media.isVideoFile ? .alone : .history) } else { - QuickLookPreview.current.show(account: account, with: media, stableId:parent.chatStableId, table) + if media.mimeType.contains("svg") || (media.fileName ?? "").hasSuffix(".svg") { + confirm(for: context.window, information: L10n.chatFileQuickLookSvg, successHandler: { _ in + QuickLookPreview.current.show(context: context, with: media, stableId: parent.chatStableId, self.table) + }) + } else { + QuickLookPreview.current.show(context: context, with: media, stableId: parent.chatStableId, self.table) + } } } } override func cancelFetching() { - if let account = account, let media = media as? TelegramMediaFile { - chatMessageFileCancelInteractiveFetch(account: account, file: media) + if let context = context, let media = media as? TelegramMediaFile { + if let parameters = parameters, let parent = parent { + parameters.cancelOperation(parent, media) + } else { + cancelFreeMediaFileInteractiveFetch(context: context, resource: media.resource) + } } } override func draggingAbility(_ event:NSEvent) -> Bool { - return NSPointInRect(convert(event.locationInWindow, from: nil), progressView.frame) + return NSPointInRect(convert(event.locationInWindow, from: nil), progressView?.frame ?? NSZeroRect) + } + + deinit { + openFileDisposable.dispose() } - func actionLayout(status:MediaResourceStatus, file:TelegramMediaFile) -> TextViewLayout { + func actionLayout(status:MediaResourceStatus, archiveStatus: ArchiveStatus?, file:TelegramMediaFile, presentation: ChatMediaPresentation, paremeters: ChatFileLayoutParameters?) -> TextViewLayout? { let attr:NSMutableAttributedString = NSMutableAttributedString() - + if let archiveStatus = archiveStatus { + switch archiveStatus { + case let .progress(progress): + switch status { + case .Fetching: + if parent != nil { + _ = attr.append(string: progress == 0 ? L10n.messageStatusArchivePreparing : L10n.messageStatusArchiving(Int(progress * 100)), color: presentation.grayText, font: .normal(.text)) + let layout = TextViewLayout(attr, constrainedWidth:frame.width - leftInset, maximumNumberOfLines:1) + layout.measure() + return layout + } else { + _ = attr.append(string: L10n.messageStatusArchived, color: presentation.grayText, font: .normal(.text)) + let layout = TextViewLayout(attr, constrainedWidth:frame.width - leftInset, maximumNumberOfLines:1) + layout.measure() + return layout + } + + default: + break + } + case .none, .waiting: + _ = attr.append(string: L10n.messageStatusArchivePreparing, color: presentation.grayText, font: .normal(.text)) + let layout = TextViewLayout(attr, constrainedWidth:frame.width - leftInset, maximumNumberOfLines:1) + layout.measure() + return layout + case .done: + if parent == nil { + _ = attr.append(string: L10n.messageStatusArchived, color: presentation.grayText, font: .normal(.text)) + let layout = TextViewLayout(attr, constrainedWidth:frame.width - leftInset, maximumNumberOfLines:1) + layout.measure() + return layout + } + case let .fail(error): + if parent == nil { + let errorText: String + switch error { + case .sizeLimit: + errorText = L10n.messageStatusArchiveFailedSizeLimit + default: + errorText = L10n.messageStatusArchiveFailed + } + _ = attr.append(string: errorText, color: theme.colors.redUI, font: .normal(.text)) + let layout = TextViewLayout(attr, constrainedWidth:frame.width - leftInset, maximumNumberOfLines:1) + layout.measure() + return layout + } + } + + } switch status { case let .Fetching(_, progress): if let parent = parent, parent.flags.contains(.Unsent) && !parent.flags.contains(.Failed) { - let _ = attr.append(string: tr(.messagesFileStateFetchingOut1(Int(progress * 100.0))), color: theme.colors.grayText, font: NSFont.normal(FontSize.text)) + let _ = attr.append(string: tr(L10n.messagesFileStateFetchingOut1(Int(progress * 100.0))), color: presentation.grayText, font: .normal(.text)) } else { - let _ = attr.append(string: tr(.messagesFileStateFetchingIn1(Int(progress * 100.0))), color: theme.colors.grayText, font: NSFont.normal(FontSize.text)) + let current = String.prettySized(with: Int(Float(file.elapsedSize) * progress), removeToken: false) + let size = "\(current) / \(String.prettySized(with: file.elapsedSize))" + let _ = attr.append(string: size, color: presentation.grayText, font: .normal(.text)) } - case .Local: - - let _ = attr.append(string: .prettySized(with: file.elapsedSize) + " - ", color: theme.colors.grayText, font: NSFont.normal(FontSize.text)) + let layout = TextViewLayout(attr, constrainedWidth:frame.width - leftInset, maximumNumberOfLines:1) + layout.measure() + return layout - let range = attr.append(string: tr(.messagesFileStateLocal), color: theme.colors.link, font: NSFont.normal(FontSize.text)) - attr.addAttribute(NSAttributedStringKey.link, value: "chat://file/finder", range: range) + case .Local: + if let _ = archiveStatus { + let size = L10n.messageStatusArchived + let _ = attr.append(string: size, color: presentation.grayText, font: .normal(.text)) + let layout = TextViewLayout(attr, constrainedWidth:frame.width - leftInset, maximumNumberOfLines:1) + layout.measure() + return layout + } + return paremeters?.finderLayout case .Remote: - let _ = attr.append(string: .prettySized(with: file.elapsedSize) + " - ", color: theme.colors.grayText, font: NSFont.normal(FontSize.text)) - let range = attr.append(string: tr(.messagesFileStateRemote), color: theme.colors.link, font: NSFont.normal(FontSize.text)) - attr.addAttribute(NSAttributedStringKey.link, value: "chat://file/download", range: range) + return paremeters?.downloadLayout } - - return TextViewLayout(attr, constrainedWidth:frame.width - leftInset, maximumNumberOfLines:1) } - override func update(with media: Media, size:NSSize, account:Account, parent:Message?, table:TableView?, parameters:ChatMediaLayoutParameters? = nil, animated: Bool) { + override func update(with media: Media, size:NSSize, context: AccountContext, parent:Message?, table:TableView?, parameters:ChatMediaLayoutParameters? = nil, animated: Bool, positionFlags: LayoutPositionFlags? = nil, approximateSynchronousValue: Bool = false) { let file:TelegramMediaFile = media as! TelegramMediaFile - let mediaUpdated = true//self.media == nil || !self.media!.isEqual(media) + var semanticMedia = self.parent?.stableId == parent?.stableId + + + if parent == nil { + semanticMedia = file.id == self.media?.id + } + let presentation: ChatMediaPresentation = parameters?.presentation ?? .Empty - super.update(with: media, size: size, account: account, parent:parent,table:table, parameters:parameters, animated: animated) + super.update(with: media, size: size, context: context, parent:parent,table:table, parameters:parameters, animated: animated, positionFlags: positionFlags) - var updatedStatusSignal: Signal? + var updatedStatusSignal: Signal<(MediaResourceStatus, ArchiveStatus?), NoError>? let parameters = parameters as? ChatFileLayoutParameters - - if mediaUpdated { - if let parent = parent, parent.flags.contains(.Unsent) && !parent.flags.contains(.Failed) { - updatedStatusSignal = combineLatest(chatMessageFileStatus(account: account, file: file), account.pendingMessageManager.pendingMessageStatus(parent.id)) - |> map { resourceStatus, pendingStatus -> MediaResourceStatus in - if let pendingStatus = pendingStatus { - return .Fetching(isActive: true, progress: pendingStatus.progress) - } else { - return resourceStatus + var archiveSignal:Signal = .single(nil) + if let resource = file.resource as? LocalFileArchiveMediaResource { + archiveSignal = archiver.archive(.resource(resource)) |> map {Optional($0)} + } + if let parent = parent, parent.flags.contains(.Unsent) && !parent.flags.contains(.Failed) { + updatedStatusSignal = combineLatest(chatMessageFileStatus(account: context.account, file: file), context.account.pendingMessageManager.pendingMessageStatus(parent.id), archiveSignal) + |> map { resourceStatus, pendingStatus, archiveStatus in + if let archiveStatus = archiveStatus { + switch archiveStatus { + case let .progress(progress): + return (.Fetching(isActive: true, progress: Float(progress)), archiveStatus) + default: + break } - } |> deliverOnMainQueue - } else { - updatedStatusSignal = chatMessageFileStatus(account: account, file: file) |> deliverOnMainQueue - } + } + if let pendingStatus = pendingStatus.0 { + return (.Fetching(isActive: true, progress: pendingStatus.progress), archiveStatus) + } else { + return (resourceStatus, archiveStatus) + } + } |> deliverOnMainQueue + } else { + updatedStatusSignal = combineLatest(chatMessageFileStatus(account: context.account, file: file, approximateSynchronousValue: approximateSynchronousValue), archiveSignal) |> map { resourceStatus, archiveStatus in + if let archiveStatus = archiveStatus { + switch archiveStatus { + case let .progress(progress): + return (.Fetching(isActive: true, progress: Float(progress)), archiveStatus) + default: + break + } + } + return (resourceStatus, archiveStatus) + } |> deliverOnMainQueue + } + + let stableId:Int64 + if let sId = parent?.stableId { + stableId = Int64(sId) + } else { + stableId = file.id?.id ?? 0 + } + + if !file.previewRepresentations.isEmpty { - if !file.previewRepresentations.isEmpty { - thumbView.setSignal(account: account, signal: chatMessageImageFile(account: account, file: file, progressive: false, scale: backingScaleFactor)) - thumbView.set(arguments: TransformImageArguments(corners: ImageCorners(radius: 4), imageSize: file.previewRepresentations[0].dimensions, boundingSize: NSMakeSize(70, 70), intrinsicInsets: NSEdgeInsets())) - } else { - thumbView.setSignal(signal: .single(nil)) - } - - self.setNeedsDisplay() + let arguments = TransformImageArguments(corners: ImageCorners(radius: 8), imageSize: file.previewRepresentations[0].dimensions.size, boundingSize: NSMakeSize(70, 70), intrinsicInsets: NSEdgeInsets()) + thumbView.setSignal(signal: cachedMedia(messageId: stableId, arguments: arguments, scale: backingScaleFactor), clearInstantly: !semanticMedia) + + let reference = parent != nil ? FileMediaReference.message(message: MessageReference(parent!), media: file) : FileMediaReference.standalone(media: file) + thumbView.setSignal(chatMessageImageFile(account: context.account, fileReference: reference, progressive: false, scale: backingScaleFactor, synchronousLoad: false), clearInstantly: false, animate: true, synchronousLoad: false, cacheImage: { result in + cacheMedia(result, messageId: stableId, arguments: arguments, scale: System.backingScale) + }) + + + thumbView.set(arguments: arguments) + } else { + thumbView.setSignal(signal: .single(TransformImageResult(nil, false))) } + self.setNeedsDisplay() + + if let signal = updatedStatusSignal, let parent = parent, let parameters = parameters { + updatedStatusSignal = combineLatest(signal, parameters.getUpdatingMediaProgress(parent.id)) |> map { value, updating in + if let progress = updating { + return (.Fetching(isActive: true, progress: progress), value.1) + } else { + return value + } + } + } + if let updatedStatusSignal = updatedStatusSignal { - self.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak self] status in - if let strongSelf = self { - strongSelf.fetchStatus = status + self.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak self, weak file] status, archiveStatus in + guard let `self` = self, let file = file else { return } + let oldStatus = self.fetchStatus + self.fetchStatus = status + + var statusWasUpdated: Bool = false + if let oldStatus = oldStatus { + switch oldStatus { + case .Fetching: + if case .Fetching = status {} else { + statusWasUpdated = true + } + case .Local: + if case .Local = status {} else { + statusWasUpdated = true + } + case .Remote: + if case .Remote = status {} else { + statusWasUpdated = true + } + } + } + + let layout = self.actionLayout(status: status, archiveStatus: archiveStatus, file: file, presentation: presentation, paremeters: parameters) + if !self.actionText.isEqual(to: layout) { + layout?.interactions = self.actionInteractions + self.actionText.update(layout) + } + + var removeThumbProgress: Bool = false + if case .Local = status { + removeThumbProgress = true + } + + if !file.previewRepresentations.isEmpty { + self.progressView?.removeFromSuperview() + self.progressView = nil + if !removeThumbProgress { + if self.thumbProgress == nil { + let progressView = RadialProgressView(theme:RadialProgressTheme(backgroundColor: .blackTransparent, foregroundColor: .white, icon: playerPlayThumb)) + progressView.frame = CGRect(origin: CGPoint(), size: CGSize(width: 40.0, height: 40.0)) + self.thumbProgress = progressView + self.addSubview(progressView) + let f = self.thumbView.focus(progressView.frame.size) + self.thumbProgress?.setFrameOrigin(f.origin) + progressView.fetchControls = self.fetchControls + } + switch status { + case .Remote: + self.thumbProgress?.state = .Remote + case let .Fetching(_, progress): + let sentGrouped = parent?.groupingKey != nil && (parent!.flags.contains(.Sending) || parent!.flags.contains(.Unsent)) + if progress == 1.0, sentGrouped { + self.thumbProgress?.state = .Success + } else { + self.thumbProgress?.state = .Fetching(progress: progress, force: false) + } + default: + break + } + } else { + if let progressView = self.thumbProgress { + switch progressView.state { + case .Fetching: + progressView.state = .Fetching(progress:1.0, force: false) + default: + break + } + self.thumbProgress = nil + progressView.layer?.animateAlpha(from: 1, to: 0, duration: 0.25, timingFunction: .linear, removeOnCompletion: false, completion: { [weak progressView] completed in + if completed { + progressView?.removeFromSuperview() + } + }) + + } + } - let layout = strongSelf.actionLayout(status: status, file: file) - if !strongSelf.actionText.isEqual(to :layout) { - layout.interactions = strongSelf.actionInteractions - layout.measure() + } else { + self.thumbProgress?.removeFromSuperview() + self.thumbProgress = nil + + if self.progressView == nil { + self.progressView = RadialProgressView() + self.addSubview(self.progressView!) + } else if statusWasUpdated { + let progressView = self.progressView + self.progressView = RadialProgressView() + self.addSubview(self.progressView!) - strongSelf.actionText.update(layout) - var width = strongSelf.leftInset + layout.layoutSize.width - if let name = parameters?.name { - width = max(width, strongSelf.leftInset + name.0.size.width) - } + progressView?.layer?.animateAlpha(from: 1, to: 0.5, duration: 0.25, timingFunction: .linear, removeOnCompletion: false, completion: { [weak progressView] completed in + if completed { + progressView?.removeFromSuperview() + } + }) + self.progressView?.layer?.animateAlpha(from: 0.5, to: 1, duration: 0.25, timingFunction: .linear) } - switch status { - case let .Fetching(_, progress): - strongSelf.progressView.theme = RadialProgressTheme(backgroundColor: file.previewRepresentations.isEmpty ? theme.colors.blueFill : theme.colors.blackTransparent, foregroundColor: .white, icon: nil) - strongSelf.progressView.state = .Fetching(progress: progress, force: false) - case .Local: - strongSelf.progressView.theme = RadialProgressTheme(backgroundColor: file.previewRepresentations.isEmpty ? theme.colors.blueFill : .clear, foregroundColor: .white, icon: file.previewRepresentations.isEmpty ? theme.icons.chatFileThumb : nil) - strongSelf.progressView.state = .Play - case .Remote: - strongSelf.progressView.theme = RadialProgressTheme(backgroundColor: file.previewRepresentations.isEmpty ? theme.colors.blueFill : theme.colors.blackTransparent, foregroundColor: .white, icon: nil) - strongSelf.progressView.state = .Remote + } + + + guard let progressView = self.progressView else { + return + } + + progressView.fetchControls = self.fetchControls + + switch status { + case let .Fetching(_, progress): + var progress = progress + if let archiveStatus = archiveStatus { + switch archiveStatus { + case .progress: + if parent != nil { + progress = 0.1 + } + default: + break + } } + progress = max(progress, 0.1) + progressView.theme = RadialProgressTheme(backgroundColor: file.previewRepresentations.isEmpty ? presentation.activityBackground : theme.colors.blackTransparent, foregroundColor: file.previewRepresentations.isEmpty ? presentation.activityForeground : .white, icon: nil) - strongSelf.progressView.userInteractionEnabled = status != .Local + let sentGrouped = parent?.groupingKey != nil && (parent!.flags.contains(.Sending) || parent!.flags.contains(.Unsent)) + if progress == 1.0, sentGrouped { + progressView.state = .Success + } else { + progressView.state = archiveStatus != nil && self.parent == nil ? .Icon(image: presentation.fileThumb, mode: .normal) : .Fetching(progress: progress, force: false) + } + case .Local: + progressView.theme = RadialProgressTheme(backgroundColor: file.previewRepresentations.isEmpty ? presentation.activityBackground : .clear, foregroundColor: file.previewRepresentations.isEmpty ? presentation.activityForeground : .clear, icon: nil) + progressView.state = !file.previewRepresentations.isEmpty ? .None : .Icon(image: presentation.fileThumb, mode: .normal) + case .Remote: + progressView.theme = RadialProgressTheme(backgroundColor: file.previewRepresentations.isEmpty ? presentation.activityBackground : theme.colors.blackTransparent, foregroundColor: file.previewRepresentations.isEmpty ? presentation.activityForeground : .white, icon: nil) + progressView.state = archiveStatus != nil && self.parent == nil ? .Icon(image: presentation.fileThumb, mode: .normal) : .Remote } + + progressView.userInteractionEnabled = status != .Local })) } - } override func layout() { super.layout() if let parameters = parameters as? ChatFileLayoutParameters { - let center = floorToScreenPixels((parameters.hasThumb ? 70 : 40) / 2) + let center = floorToScreenPixels(backingScaleFactor, (parameters.hasThumb ? 70 : 40) / 2) actionText.setFrameOrigin(leftInset, parameters.hasThumb ? center + 2 : 20) if parameters.hasThumb { - let f = thumbView.focus(progressView.frame.size) - progressView.setFrameOrigin(f.origin) + if let thumbProgress = thumbProgress { + let f = thumbView.focus(thumbProgress.frame.size) + thumbProgress.setFrameOrigin(f.origin) + } } else { - progressView.setFrameOrigin(NSZeroPoint) + progressView?.setFrameOrigin(NSZeroPoint) } } @@ -234,8 +463,8 @@ class ChatFileContentView: ChatMediaContentView { let parameters = self.parameters as? ChatFileLayoutParameters if let name = parameters?.name { - let center = floorToScreenPixels(frame.height/2) - name.1.draw(NSMakeRect(leftInset, isHasThumb ? center - name.0.size.height - 2 : 1, name.0.size.width, name.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor) + let center = floorToScreenPixels(backingScaleFactor, frame.height/2) + name.1.draw(NSMakeRect(leftInset, isHasThumb ? center - name.0.size.height - 2 : 1, name.0.size.width, name.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) } } @@ -243,14 +472,25 @@ class ChatFileContentView: ChatMediaContentView { if let media = media as? TelegramMediaFile, !media.previewRepresentations.isEmpty { return thumbView.copy() } - return progressView.copy() + return progressView?.copy() ?? self + } + + override var contents: Any? { + return (copy() as? NSView)?.layer?.contents + } + + override var contentFrame: NSRect { + if let media = media as? TelegramMediaFile, !media.previewRepresentations.isEmpty { + return thumbView.frame + } + return progressView?.frame ?? frame } - override var interactionContentView: NSView { + override func interactionContentView(for innerId: AnyHashable, animateIn: Bool ) -> NSView { if let media = media as? TelegramMediaFile, !media.previewRepresentations.isEmpty { return thumbView } - return progressView + return progressView ?? self } override func setContent(size: NSSize) { diff --git a/Telegram-Mac/ChatFileMediaItem.swift b/Telegram-Mac/ChatFileMediaItem.swift index 5d02f888f0..4b11e0d30b 100644 --- a/Telegram-Mac/ChatFileMediaItem.swift +++ b/Telegram-Mac/ChatFileMediaItem.swift @@ -7,40 +7,149 @@ // import Cocoa -import TelegramCoreMac -import PostboxMac +import TelegramCore + +import Postbox import TGUIKit -class ChatFileLayoutParameters : ChatMediaLayoutParameters { +class ChatFileLayoutParameters : ChatMediaGalleryParameters { var nameNode:TextNode = TextNode() var name:(TextNodeLayout, TextNode)? let hasThumb:Bool let fileName:String - init(fileName:String, hasThumb: Bool) { + let finderLayout: TextViewLayout + let downloadLayout: TextViewLayout + fileprivate let uploadingLayout: TextViewLayout + fileprivate let downloadingLayout: TextViewLayout + init(fileName:String, hasThumb: Bool, presentation: ChatMediaPresentation, media: Media, automaticDownload: Bool, isIncoming: Bool, autoplayMedia: AutoplayMediaPreferences, isChatRelated: Bool = false) { self.fileName = fileName self.hasThumb = hasThumb + + let file = media as! TelegramMediaFile + + + self.uploadingLayout = TextViewLayout(.initialize(string: L10n.messagesFileStateFetchingOut1(100), font: .normal(.text)), alwaysStaticItems: true) + self.downloadingLayout = TextViewLayout(.initialize(string: L10n.messagesFileStateFetchingIn1(100), font: .normal(.text)), alwaysStaticItems: true) + + + var attr:NSMutableAttributedString = NSMutableAttributedString() + let _ = attr.append(string: .prettySized(with: file.elapsedSize), color: presentation.grayText, font: .normal(.text)) + if !(file.resource is LocalFileReferenceMediaResource) || isChatRelated { + let _ = attr.append(string: " - ", color: presentation.grayText, font: .normal(.text)) + + let range = attr.append(string: tr(L10n.messagesFileStateLocal), color: theme.bubbled && !isIncoming ? presentation.grayText : presentation.link, font: .medium(FontSize.text)) + attr.addAttribute(NSAttributedString.Key.link, value: "chat://file/finder", range: range) + } + finderLayout = TextViewLayout(attr, maximumNumberOfLines: 1, alwaysStaticItems: true) + + + attr = NSMutableAttributedString() + let _ = attr.append(string: .prettySized(with: file.elapsedSize), color: presentation.grayText, font: .normal(.text)) + if !(file.resource is LocalFileReferenceMediaResource) || isChatRelated { + let _ = attr.append(string: " - ", color: presentation.grayText, font: .normal(.text)) + let range = attr.append(string: tr(L10n.messagesFileStateRemote), color: theme.bubbled && !isIncoming ? presentation.grayText : presentation.link, font: .medium(.text)) + attr.addAttribute(NSAttributedString.Key.link, value: "chat://file/download", range: range) + } + downloadLayout = TextViewLayout(attr, maximumNumberOfLines: 1, alwaysStaticItems: true) + + + super.init(isWebpage: false, presentation: presentation, media: media, automaticDownload: automaticDownload, autoplayMedia: autoplayMedia) + + } + override func makeLabelsForWidth(_ width: CGFloat) -> CGFloat { + self.name = TextNode.layoutText(maybeNode: nameNode, .initialize(string: fileName , color: presentation.text, font: .medium(.text)), nil, 1, .middle, NSMakeSize(width - (hasThumb ? 80 : 50), 20), nil,false, .left) + + + uploadingLayout.measure(width: width - (hasThumb ? 80 : 50)) + downloadingLayout.measure(width: width - (hasThumb ? 80 : 50)) + + downloadLayout.measure(width: width - (hasThumb ? 80 : 50)) + finderLayout.measure(width: width - (hasThumb ? 80 : 50)) + + return max(downloadLayout.layoutSize.width, uploadingLayout.layoutSize.width, finderLayout.layoutSize.width, downloadingLayout.layoutSize.width, self.name!.0.size.width) + (hasThumb ? 80 : 50) + } } class ChatFileMediaItem: ChatMediaItem { - - override init(_ initialSize:NSSize, _ chatInteraction:ChatInteraction, _ account: Account, _ object: ChatHistoryEntry) { - super.init(initialSize, chatInteraction, account, object) - self.parameters = ChatMediaLayoutParameters.layout(for: (self.media as! TelegramMediaFile), isWebpage: false, chatInteraction: chatInteraction) + override init(_ initialSize:NSSize, _ chatInteraction:ChatInteraction, _ context: AccountContext, _ object: ChatHistoryEntry, _ downloadSettings: AutomaticMediaDownloadSettings, theme: TelegramPresentationTheme) { + super.init(initialSize, chatInteraction, context, object, downloadSettings, theme: theme) + self.parameters = ChatMediaLayoutParameters.layout(for: (self.media as! TelegramMediaFile), isWebpage: false, chatInteraction: chatInteraction, presentation: .make(for: object.message!, account: context.account, renderType: object.renderType, theme: theme), automaticDownload: downloadSettings.isDownloable(object.message!), isIncoming: object.message!.isIncoming(context.account, object.renderType == .bubble), isFile: true, autoplayMedia: object.autoplayMedia, isChatRelated: true) + + (self.parameters as? ChatFileLayoutParameters)?.showMedia = { [weak self] message in + guard let `self` = self else {return} + + var type:GalleryAppearType = .history + if let parameters = self.parameters as? ChatMediaGalleryParameters, parameters.isWebpage { + type = .alone + } else if message.containsSecretMedia { + type = .secret + } + showChatGallery(context: context, message: message, self.table, self.parameters as? ChatMediaGalleryParameters, type: type) + } + + (self.parameters as? ChatFileLayoutParameters)?.showMessage = { [weak self] message in + self?.chatInteraction.focusMessageId(nil, message.id, .CenterEmpty) + } + } override func makeContentSize(_ width: CGFloat) -> NSSize { + var width = width + let parameters = self.parameters as! ChatFileLayoutParameters + let file = media as! TelegramMediaFile + let optionalWidth = parameters.makeLabelsForWidth(width) - parameters.name = TextNode.layoutText(maybeNode: parameters.nameNode, NSAttributedString.initialize(string: parameters.fileName , color: theme.colors.text, font: .medium(.text)), nil, 1, .middle, NSMakeSize(width - (parameters.hasThumb ? 80 : 50), 20), nil,false, .left) + let progressMaxWidth = max(parameters.uploadingLayout.layoutSize.width, parameters.downloadingLayout.layoutSize.width) + + if !captionLayouts.isEmpty { + var tw: CGFloat = 0 + for captionLayout in captionLayouts { + captionLayout.layout.measure(width: width) + tw = max(max(optionalWidth, captionLayout.layout.layoutSize.width), tw) + } + width = tw + } else { + width = optionalWidth + } return NSMakeSize(width, parameters.hasThumb ? 70 : 40) } + override var additionalLineForDateInBubbleState: CGFloat? { + let file = media as! TelegramMediaFile + let parameters = self.parameters as! ChatFileLayoutParameters + + let progressMaxWidth = max(parameters.uploadingLayout.layoutSize.width, parameters.downloadingLayout.layoutSize.width) + + let accesoryWidth = max(max(parameters.finderLayout.layoutSize.width, parameters.downloadLayout.layoutSize.width), progressMaxWidth) + (file.previewRepresentations.isEmpty ? 50 : 80) + + if file.previewRepresentations.isEmpty, accesoryWidth > realContentSize.width - (rightSize.width + insetBetweenContentAndDate) { + return super.additionalLineForDateInBubbleState + } + + + return file.previewRepresentations.isEmpty || !captionLayouts.isEmpty ? super.additionalLineForDateInBubbleState : nil + } + override var isFixedRightPosition: Bool { + let file = media as! TelegramMediaFile + + let parameters = self.parameters as! ChatFileLayoutParameters + + let progressMaxWidth = max(parameters.uploadingLayout.layoutSize.width, parameters.downloadingLayout.layoutSize.width) + let accesoryWidth = max(max(parameters.finderLayout.layoutSize.width, parameters.downloadLayout.layoutSize.width), progressMaxWidth) + (file.previewRepresentations.isEmpty ? 50 : 80) + + if file.previewRepresentations.isEmpty, accesoryWidth < realContentSize.width - (rightSize.width + insetBetweenContentAndDate) { + return true + } + + return file.previewRepresentations.isEmpty || !captionLayouts.isEmpty ? super.isFixedRightPosition : true + } override func contentNode() -> ChatMediaContentView.Type { return ChatFileContentView.self diff --git a/Telegram-Mac/ChatGIFContentView.swift b/Telegram-Mac/ChatGIFContentView.swift index f7c482c02f..2ce4ad81c7 100644 --- a/Telegram-Mac/ChatGIFContentView.swift +++ b/Telegram-Mac/ChatGIFContentView.swift @@ -7,25 +7,42 @@ // import Cocoa -import TelegramCoreMac +import TelegramCore + import TGUIKit -import PostboxMac -import SwiftSignalKitMac +import Postbox +import SwiftSignalKit class ChatGIFContentView: ChatMediaContentView { - private var player:GIFPlayerView = GIFPlayerView() + private var player:GifPlayerBufferView = GifPlayerBufferView() private var progressView:RadialProgressView? + private let statusDisposable = MetaDisposable() private let fetchDisposable = MetaDisposable() private let playerDisposable = MetaDisposable() +// private let nextTimebase: Atomic = Atomic(value: nil) +// private var data:AVGifData? { +// didSet { +// updatePlayerIfNeeded() +// } +// } - private var path:String? { - didSet { - updatePlayerIfNeeded() + override var backgroundColor: NSColor { + set { + super.backgroundColor = .clear + } + get { + return super.backgroundColor } } + override func previewMediaIfPossible() -> Bool { + guard let context = self.context, let window = self.kitWindow, let table = self.table, fetchStatus == .Local else {return false} + _ = startModalPreviewHandle(table, window: window, context: context) + return true + } + required init(frame frameRect: NSRect) { super.init(frame: frameRect) addSubview(player) @@ -48,162 +65,265 @@ class ChatGIFContentView: ChatMediaContentView { } override func open() { - if let parent = parent, let account = account { - showChatGallery(account:account, message:parent, table) + if let parent = parent, let parameters = parameters { + if !parameters.autoplay { + parameters.autoplay = true + + if let status = fetchStatus { + switch status { + case .Local: + progressView?.change(opacity: 0) + default: + progressView?.change(opacity: 1) + } + } + + updatePlayerIfNeeded() + } else if !(parent.media.first is TelegramMediaGame) { + parameters.showMedia(parent) + } } } - - - override func cancelFetching() { - if let account = account, let media = media as? TelegramMediaFile { - chatMessageFileCancelInteractiveFetch(account: account, file: media) - } - } +// override func videoTimebase() -> CMTimebase? { +// return player.controlTimebase +// } +// override func applyTimebase(timebase: CMTimebase?) { +// _ = nextTimebase.swap(timebase) +// } + + override func fetch() { - if let account = account, let media = media as? TelegramMediaFile { - fetchDisposable.set(chatMessageFileInteractiveFetched(account: account, file: media).start()) + if let context = context, let media = media as? TelegramMediaFile { + if let parent = parent { + fetchDisposable.set(messageMediaFileInteractiveFetched(context: context, messageId: parent.id, fileReference: FileMediaReference.message(message: MessageReference(parent), media: media)).start()) + } else { + fetchDisposable.set(freeMediaFileInteractiveFetched(context: context, fileReference: FileMediaReference.standalone(media: media)).start()) + } } } override func layout() { super.layout() player.frame = bounds + + self.player.positionFlags = positionFlags progressView?.center() + updatePlayerIfNeeded() } func removeNotificationListeners() { NotificationCenter.default.removeObserver(self) } + override func viewDidUpdatedDynamicContent() { + super.viewDidUpdatedDynamicContent() + updatePlayerIfNeeded() + } @objc func updatePlayerIfNeeded() { - - let accept = window != nil && window!.isKeyWindow && !NSIsEmptyRect(visibleRect) - player.set(path: accept ? path : nil) + let accept = parameters?.autoplay == true && window != nil && window!.isKeyWindow && !NSIsEmptyRect(visibleRect) && !self.isDynamicContentLocked + + player.ticking = accept - - /* var s:Signal = .single() - s = s |> delay(0.01, queue: Queue.mainQueue()) - playerDisposable.set(s.start(next: {[weak self] (next) in - if let strongSelf = self { - let accept = strongSelf.window != nil && strongSelf.window!.isKeyWindow && !NSIsEmptyRect(strongSelf.visibleRect) - strongSelf.player.set(path: accept ? strongSelf.path : nil) - } - })) - */ } + func updateListeners() { if let window = window { + NotificationCenter.default.removeObserver(self) NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSWindow.didBecomeKeyNotification, object: window) NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSWindow.didResignKeyNotification, object: window) NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSView.boundsDidChangeNotification, object: table?.clipView) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSView.frameDidChangeNotification, object: table?.view) } else { removeNotificationListeners() } } + override func willRemove() { + super.willRemove() + updateListeners() + updatePlayerIfNeeded() + } + override func viewDidMoveToWindow() { updateListeners() updatePlayerIfNeeded() } deinit { - player.set(path: nil) + //player.set(data: nil) } - override func update(with media: Media, size: NSSize, account: Account, parent: Message?, table: TableView?, parameters:ChatMediaLayoutParameters? = nil, animated: Bool = false) { - let mediaUpdated = self.media == nil || !self.media!.isEqual(media) + var blurBackground: Bool { + return (parent != nil && parent?.groupingKey == nil) || parent == nil + } + + override func update(with media: Media, size: NSSize, context: AccountContext, parent: Message?, table: TableView?, parameters:ChatMediaLayoutParameters? = nil, animated: Bool = false, positionFlags: LayoutPositionFlags? = nil, approximateSynchronousValue: Bool = false) { + let mediaUpdated = self.media == nil || !self.media!.isSemanticallyEqual(to: media) - super.update(with: media, size: size, account: account, parent:parent,table:table, parameters:parameters, animated: animated) + super.update(with: media, size: size, context: context, parent:parent,table:table, parameters:parameters, animated: animated, positionFlags: positionFlags) - updateListeners() + + var topLeftRadius: CGFloat = .cornerRadius + var bottomLeftRadius: CGFloat = .cornerRadius + var topRightRadius: CGFloat = .cornerRadius + var bottomRightRadius: CGFloat = .cornerRadius + + + if let positionFlags = positionFlags { + if positionFlags.contains(.top) && positionFlags.contains(.left) { + topLeftRadius = topLeftRadius * 3 + 2 + } + if positionFlags.contains(.top) && positionFlags.contains(.right) { + topRightRadius = topRightRadius * 3 + 2 + } + if positionFlags.contains(.bottom) && positionFlags.contains(.left) { + bottomLeftRadius = bottomLeftRadius * 3 + 2 + } + if positionFlags.contains(.bottom) && positionFlags.contains(.right) { + bottomRightRadius = bottomRightRadius * 3 + 2 + } + } + + updateListeners() + self.player.positionFlags = positionFlags + if let media = media as? TelegramMediaFile { - if mediaUpdated { - - path = nil - - let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: media.previewRepresentations) - var updatedStatusSignal: Signal? - - player.setSignal(account: account, signal: chatMessagePhoto(account: account, photo: image, scale: backingScaleFactor)) - let arguments = TransformImageArguments(corners: ImageCorners(radius:.cornerRadius), imageSize: size, boundingSize: size, intrinsicInsets: NSEdgeInsets()) - player.set(arguments: arguments) - - if let parent = parent, parent.flags.contains(.Unsent) && !parent.flags.contains(.Failed) { - updatedStatusSignal = combineLatest(chatMessageFileStatus(account: account, file: media), account.pendingMessageManager.pendingMessageStatus(parent.id)) - |> map { resourceStatus, pendingStatus -> MediaResourceStatus in - if let pendingStatus = pendingStatus { - return .Fetching(isActive: true, progress: pendingStatus.progress) - } else { - return resourceStatus - } - } |> deliverOnMainQueue - } else { - updatedStatusSignal = chatMessageFileStatus(account: account, file: media) + let dimensions = media.dimensions?.size ?? size + var updatedStatusSignal: Signal? + + let reference = parent != nil ? FileMediaReference.message(message: MessageReference(parent!), media: media) : FileMediaReference.standalone(media: media) + let fitted = dimensions.aspectFilled(size) + player.setVideoLayerGravity(.resizeAspect) + + + let arguments = TransformImageArguments(corners: ImageCorners(topLeft: .Corner(topLeftRadius), topRight: .Corner(topRightRadius), bottomLeft: .Corner(bottomLeftRadius), bottomRight: .Corner(bottomRightRadius)), imageSize: fitted, boundingSize: size, intrinsicInsets: NSEdgeInsets(), resizeMode: .blurBackground) + + player.update(reference, context: context, resizeInChat: blurBackground) + + player.setSignal(signal: cachedMedia(media: media, arguments: arguments, scale: backingScaleFactor, positionFlags: positionFlags), clearInstantly: mediaUpdated) + + player.setSignal(chatMessageVideo(postbox: context.account.postbox, fileReference: reference, scale: backingScaleFactor), animate: true, cacheImage: { [weak media] result in + if let media = media { + cacheMedia(result, media: media, arguments: arguments, scale: System.backingScale, positionFlags: positionFlags) } + }) + player.set(arguments: arguments) + + + if let parent = parent, parent.flags.contains(.Unsent) && !parent.flags.contains(.Failed) { + updatedStatusSignal = combineLatest(chatMessageFileStatus(account: context.account, file: media), context.account.pendingMessageManager.pendingMessageStatus(parent.id)) + |> map { resourceStatus, pendingStatus -> MediaResourceStatus in + if let pendingStatus = pendingStatus.0 { + return .Fetching(isActive: true, progress: min(pendingStatus.progress, pendingStatus.progress * 85 / 100)) + } else { + return resourceStatus + } + } |> deliverOnMainQueue + } else { + updatedStatusSignal = chatMessageFileStatus(account: context.account, file: media, approximateSynchronousValue: approximateSynchronousValue, useVideoThumb: true) + } + + if let updatedStatusSignal = updatedStatusSignal { - if let updatedStatusSignal = updatedStatusSignal { - - - self.statusDisposable.set((combineLatest(updatedStatusSignal, account.postbox.mediaBox.resourceData(media.resource)) |> deliverOnMainQueue).start(next: { [weak self] (status,resource) in + self.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak self] status in if let strongSelf = self { - strongSelf.fetchStatus = status - if case .Local = status { + strongSelf.updatePlayerIfNeeded() + + let needSetStatus: Bool + if case .Local = status, parameters?.autoplay == true { if let progressView = strongSelf.progressView { - progressView.removeFromSuperview() + progressView.state = parent == nil ? .ImpossibleFetching(progress: 1, force: false) : .Fetching(progress: 1, force: false) strongSelf.progressView = nil + progressView.layer?.animateAlpha(from: 1, to: 0, duration: 0.25, timingFunction: .linear, removeOnCompletion: false, completion: { [weak progressView] completed in + if completed { + progressView?.removeFromSuperview() + } + }) } - strongSelf.path = resource.path - + needSetStatus = false } else { if strongSelf.progressView == nil { - let progressView = RadialProgressView() + let progressView = RadialProgressView(theme: RadialProgressTheme(backgroundColor: .blackTransparent, foregroundColor: .white, icon: playerPlayThumb)) progressView.frame = CGRect(origin: CGPoint(), size: CGSize(width: 40.0, height: 40.0)) strongSelf.progressView = progressView strongSelf.addSubview(progressView) strongSelf.progressView?.center() strongSelf.progressView?.fetchControls = strongSelf.fetchControls } + needSetStatus = true } - - switch status { - case let .Fetching(_, progress): - strongSelf.progressView?.state = .Fetching(progress: progress, force: false) - case .Local: - strongSelf.progressView?.state = .Play - case .Remote: - strongSelf.progressView?.state = .Remote + if needSetStatus { + switch status { + case let .Fetching(_, progress): + strongSelf.progressView?.state = parent == nil ? .ImpossibleFetching(progress: progress, force: false) : .Fetching(progress: progress, force: false) + case .Local: + if parent != nil { + strongSelf.progressView?.state = .Play + } + case .Remote: + if parent != nil { + strongSelf.progressView?.state = .Remote + } + } } } })) - } - - fetchDisposable.set(chatMessageFileInteractiveFetched(account: account, file: media).start()) - } } + + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + var bp:Int = 0 + bp += 1 } - override func copy() -> Any { - let view = View() - view.backgroundColor = .clear - let layer:CALayer = CALayer() - layer.frame = NSMakeRect(0, visibleRect.minY == 0 ? 0 : player.visibleRect.height - player.frame.height, player.frame.width, player.frame.height) - layer.contents = player.layer?.contents - layer.masksToBounds = true - view.frame = player.visibleRect - layer.shouldRasterize = true - layer.rasterizationScale = backingScaleFactor - view.layer?.addSublayer(layer) - return view + override var contents: Any? { + return player.layer?.contents + } + + override open func copy() -> Any { + return player.copy() + +// let view = NSView() +// view.wantsLayer = true +// +// view.background = .clear +// view.layer?.contents = player.layer?.contents +// view.frame = self.visibleRect +// view.layer?.masksToBounds = true +// +// +// if bounds != visibleRect { +// if let image = player.layer?.contents { +// view.layer?.contents = generateImage(player.bounds.size, contextGenerator: { size, ctx in +// ctx.clear(player.bounds) +// ctx.setFillColor(.clear) +// ctx.fill(player.bounds) +// +// if player.visibleRect.minY == 0 { +// ctx.clip(to: NSMakeRect(0, 0, player.bounds.width, player.bounds.height - ( player.bounds.height - player.visibleRect.height))) +// } else { +// ctx.clip(to: NSMakeRect(0, (player.bounds.height - player.visibleRect.height), player.bounds.width, player.bounds.height - ( player.bounds.height - player.visibleRect.height))) +// } +// ctx.draw(image as! CGImage, in: player.bounds) +// }, opaque: false) +// } +// } +// +// view.layer?.shouldRasterize = true +// view.layer?.rasterizationScale = backingScaleFactor +// +// return view } } diff --git a/Telegram-Mac/ChatGradientModel.swift b/Telegram-Mac/ChatGradientModel.swift new file mode 100644 index 0000000000..be511abc96 --- /dev/null +++ b/Telegram-Mac/ChatGradientModel.swift @@ -0,0 +1,107 @@ +// +// ChatGradientModel.swift +// Telegram +// +// Created by Mikhail Filimonov on 02.01.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +private let maskInset: CGFloat = 1.0 + + +final class ChatMessageBubbleBackdrop: NSView { + private let backgroundContent: NSView + private let borderView: SImageView = SImageView() + private var currentMaskMode: Bool? + + private var maskView: SImageView? + + override var frame: CGRect { + didSet { + if let maskView = self.maskView { + let maskFrame = self.bounds + if maskView.frame != maskFrame { + maskView.frame = maskFrame + } + } + } + } + + init() { + self.backgroundContent = NSView() + + super.init(frame: NSZeroRect) + autoresizingMask = [] + autoresizesSubviews = false + self.backgroundContent.wantsLayer = true + wantsLayer = true + self.layer?.masksToBounds = true + self.addSubview(self.backgroundContent) + self.addSubview(self.borderView) + self.layer?.disableActions() + } + + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func change(pos position: NSPoint, animated: Bool, _ save:Bool = true, removeOnCompletion: Bool = true, duration:Double = 0.2, timingFunction: CAMediaTimingFunctionName = CAMediaTimingFunctionName.easeOut, completion:((Bool)->Void)? = nil) -> Void { + super._change(pos: position, animated: animated, save, removeOnCompletion: removeOnCompletion, duration: duration, timingFunction: timingFunction, completion: completion) + + + } + + func change(size: NSSize, animated: Bool, _ save:Bool = true, removeOnCompletion: Bool = true, duration:Double = 0.2, timingFunction: CAMediaTimingFunctionName = CAMediaTimingFunctionName.easeOut, completion:((Bool)->Void)? = nil) { + maskView?._change(size: size, animated: animated, duration: duration, timingFunction: timingFunction) + super._change(size: size, animated: animated, save, removeOnCompletion: removeOnCompletion, duration: duration, timingFunction: timingFunction, completion: completion) + + self.borderView._change(size: size, animated: animated, save, removeOnCompletion: removeOnCompletion, duration: duration, timingFunction: timingFunction, completion: completion) + } + + + func setType(image: (CGImage, NSEdgeInsets)?, border: (CGImage, NSEdgeInsets)?, background: CGImage) { + if let _ = image { + let maskView: SImageView + if let current = self.maskView { + maskView = current + } else { + maskView = SImageView() + maskView.frame = self.bounds + self.maskView?.layer?.disableActions() + self.maskView = maskView + self.layer?.mask = maskView.layer + } + } else { + if let _ = self.maskView { + self.layer?.mask = nil + self.maskView = nil + } + } + self.borderView.data = border + self.backgroundContent.layer?.contents = background + if let maskView = self.maskView { + maskView.data = image + } + self.backgroundContent.isHidden = image == nil + } + + override func layout() { + super.layout() + self.borderView.frame = bounds + } + + func update(rect: CGRect, within containerSize: CGSize, animated: Bool, rotated: Bool = false) { + if self.backgroundContent.frame != CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize) { + self.backgroundContent._change(size: containerSize, animated: animated) + self.backgroundContent._change(pos: CGPoint(x: -rect.minX, y: -rect.minY), animated: animated, forceAnimateIfHasAnimation: true) + } + if rotated { + backgroundContent.rotate(byDegrees: 180) + } else { + backgroundContent.rotate(byDegrees: 0) + } + } +} diff --git a/Telegram-Mac/ChatGroupedItem.swift b/Telegram-Mac/ChatGroupedItem.swift new file mode 100644 index 0000000000..8eea0c3b28 --- /dev/null +++ b/Telegram-Mac/ChatGroupedItem.swift @@ -0,0 +1,1282 @@ +// +// ChatGroupedItem.swift +// Telegram +// +// Created by keepcoder on 31/10/2017. +// Copyright © 2017 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + +class ChatGroupedItem: ChatRowItem { + + fileprivate(set) var parameters: [ChatMediaLayoutParameters] = [] + fileprivate let layout: GroupedLayout + + override var messages: [Message] { + return layout.messages + } + + var layoutType: GroupedMediaType { + return layout.type + } + + + override init(_ initialSize: NSSize, _ chatInteraction: ChatInteraction, _ context: AccountContext, _ entry: ChatHistoryEntry, _ downloadSettings: AutomaticMediaDownloadSettings, theme: TelegramPresentationTheme) { + + var captionLayouts: [ChatRowItem.RowCaption] = [] + + if case let .groupedPhotos(messages, _) = entry { + + let messages = messages.map{$0.message!}.filter({!$0.media.isEmpty}) + let prettyCount = messages.filter { $0.media.first!.isInteractiveMedia }.count + self.layout = GroupedLayout(messages, type: prettyCount != messages.count ? .files : .photoOrVideo) + + var captionMessages: [Message] = [] + switch layout.type { + case .photoOrVideo: + for message in messages { + if !captionMessages.isEmpty, !message.text.isEmpty { + captionMessages.removeAll() + break + } + if !message.text.isEmpty { + captionMessages.append(message) + } + } + case .files: + captionMessages = messages.filter { !$0.text.isEmpty } + } + + + for message in captionMessages { + + let isIncoming: Bool = message.isIncoming(context.account, entry.renderType == .bubble) + + var caption:NSMutableAttributedString = NSMutableAttributedString() + NSAttributedString.initialize() + _ = caption.append(string: message.text, color: theme.chat.textColor(isIncoming, entry.renderType == .bubble), font: NSFont.normal(theme.fontSize)) + var types:ParsingType = [.Links, .Mentions, .Hashtags] + + if let peer = messageMainPeer(message) as? TelegramUser { + if peer.botInfo != nil { + types.insert(.Commands) + } + } else if let peer = messageMainPeer(message) as? TelegramChannel { + switch peer.info { + case .group: + types.insert(.Commands) + default: + break + } + } else { + types.insert(.Commands) + } + + var hasEntities: Bool = false + for attr in message.attributes { + if attr is TextEntitiesMessageAttribute { + hasEntities = true + break + } + } + if hasEntities { + caption = ChatMessageItem.applyMessageEntities(with: message.attributes, for: message.text.fixed, message: message, context: context, fontSize: theme.fontSize, openInfo:chatInteraction.openInfo, botCommand:chatInteraction.sendPlainText, hashtag: chatInteraction.modalSearch, applyProxy: chatInteraction.applyProxy, textColor: theme.chat.textColor(isIncoming, entry.renderType == .bubble), linkColor: theme.chat.linkColor(isIncoming, entry.renderType == .bubble), monospacedPre: theme.chat.monospacedPreColor(isIncoming, entry.renderType == .bubble), monospacedCode: theme.chat.monospacedCodeColor(isIncoming, entry.renderType == .bubble), openBank: chatInteraction.openBank).mutableCopy() as! NSMutableAttributedString + } + + if !hasEntities || message.flags.contains(.Failed) || message.flags.contains(.Unsent) || message.flags.contains(.Sending) { + caption.detectLinks(type: types, context: context, color: theme.chat.linkColor(isIncoming, entry.renderType == .bubble), openInfo:chatInteraction.openInfo, hashtag: context.sharedContext.bindings.globalSearch, command: chatInteraction.sendPlainText, applyProxy: chatInteraction.applyProxy) + } + let layout: ChatRowItem.RowCaption = .init(id: message.stableId, offset: .zero, layout: TextViewLayout(caption, alignment: .left, selectText: theme.chat.selectText(isIncoming, entry.renderType == .bubble), strokeLinks: entry.renderType == .bubble, alwaysStaticItems: true)) + layout.layout.interactions = globalLinkExecutor + captionLayouts.append(layout) + } + + } else { + fatalError("") + } + + super.init(initialSize, chatInteraction, context, entry, downloadSettings, theme: theme) + + self.captionLayouts = captionLayouts + + for (i, message) in layout.messages.enumerated() { + + switch layout.type { + case .files: + + // self.parameters.append(ChatMediaLayoutParameters.layout(for: (message.media.first as! TelegramMediaFile), isWebpage: false, chatInteraction: chatInteraction, presentation: .make(for: message, account: context.account, renderType: entry.renderType), automaticDownload: downloadSettings.isDownloable(message), isIncoming: message.isIncoming(context.account, entry.renderType == .bubble), isFile: true, autoplayMedia: entry.autoplayMedia, isChatRelated: true)) + + + self.parameters.append(ChatMediaLayoutParameters.layout(for: (message.media.first as! TelegramMediaFile), isWebpage: chatInteraction.isLogInteraction, chatInteraction: chatInteraction, presentation: .make(for: message, account: context.account, renderType: entry.renderType, theme: theme), automaticDownload: downloadSettings.isDownloable(message), isIncoming: message.isIncoming(context.account, entry.renderType == .bubble), autoplayMedia: entry.autoplayMedia)) + case .photoOrVideo: + self.parameters.append(ChatMediaGalleryParameters(showMedia: { [weak self] message in + guard let `self` = self else {return} + + var type:GalleryAppearType = .history + if let parameters = self.parameters[i] as? ChatMediaGalleryParameters, parameters.isWebpage { + type = .alone + } else if message.containsSecretMedia { + type = .secret + } + if self.chatInteraction.mode.threadId?.peerId == message.id.peerId { + type = .messages(self.messages) + } + showChatGallery(context: context, message: message, self.table, self.parameters[i], type: type, chatMode: self.chatInteraction.mode) + + }, showMessage: { [weak self] message in + self?.chatInteraction.focusMessageId(nil, message.id, .CenterEmpty) + }, isWebpage: chatInteraction.isLogInteraction, presentation: .make(for: message, account: context.account, renderType: entry.renderType, theme: theme), media: message.media.first!, automaticDownload: downloadSettings.isDownloable(message), autoplayMedia: entry.autoplayMedia)) + + self.parameters[i].automaticDownloadFunc = { message in + return downloadSettings.isDownloable(message) + } + } + self.parameters[i].chatLocationInput = chatInteraction.chatLocationInput + self.parameters[i].chatMode = chatInteraction.mode + self.parameters[i].getUpdatingMediaProgress = { messageId in + switch entry { + case let .groupedPhotos(entries, _): + let media = entries.first(where: { $0.message?.id == messageId})?.additionalData.updatingMedia + if let media = media { + switch media.media { + case .update: + return .single(media.progress) + default: + break + } + } + default: + break + } + return .single(nil) + } + self.parameters[i].cancelOperation = { [unowned context] message, media in + switch entry { + case let .groupedPhotos(entries, _): + if let entry = entries.first(where: { $0.message?.id == message.id }) { + if entry.additionalData.updatingMedia != nil { + context.account.pendingUpdateMessageManager.cancel(messageId: message.id) + } else if let media = media as? TelegramMediaFile { + messageMediaFileCancelInteractiveFetch(context: context, messageId: message.id, fileReference: FileMediaReference.message(message: MessageReference(message), media: media)) + if let resource = media.resource as? LocalFileArchiveMediaResource { + archiver.remove(.resource(resource)) + } + } else if let media = media as? TelegramMediaImage { + chatMessagePhotoCancelInteractiveFetch(account: context.account, photo: media) + } + } + default: + break + } + + + } + } + + if isBubbleFullFilled, layout.messages.count == 1 { + var positionFlags: LayoutPositionFlags = [] + if captionLayouts.isEmpty { + positionFlags.insert(.bottom) + positionFlags.insert(.left) + positionFlags.insert(.right) + } + if authorText == nil && replyModel == nil && forwardNameLayout == nil { + positionFlags.insert(.top) + positionFlags.insert(.left) + positionFlags.insert(.right) + } + self.positionFlags = positionFlags + } + + switch self.layout.type { + case .files: + var positionFlags: LayoutPositionFlags = [] + positionFlags.insert(.bottom) + positionFlags.insert(.top) + positionFlags.insert(.left) + positionFlags.insert(.right) + self.positionFlags = positionFlags + default: + break + } + } + + override func share() { + if let message = message { + showModal(with: ShareModalController(ShareMessageObject(context, message, layout.messages)), for: mainWindow) + } + + } + + override var hasBubble: Bool { + get { + if isBubbled, self.layout.type == .files { + return true + } + return isBubbled && (!captionLayouts.isEmpty || message?.replyAttribute != nil || forwardNameLayout != nil || layout.messages.count == 1 || commentsBubbleData != nil) + } + set { + super.hasBubble = newValue + } + } + + override var isBubbleFullFilled: Bool { + return isBubbled && self.layout.type != .files + } + + var mediaBubbleCornerInset: CGFloat { + return 1 + } + + override var bubbleFrame: NSRect { + var frame = super.bubbleFrame + + if isBubbleFullFilled { + frame.size.width = contentSize.width + additionBubbleInset + if hasBubble { + frame.size.width += self.mediaBubbleCornerInset * 2 + } + } + + return frame + } + + override var defaultContentTopOffset: CGFloat { + if isBubbled && !hasBubble { + return 2 + } + return super.defaultContentTopOffset + } + + fileprivate var positionFlags: LayoutPositionFlags? + + override var contentOffset: NSPoint { + var offset = super.contentOffset + // + if hasBubble { + if forwardNameLayout != nil { + offset.y += defaultContentInnerInset + } else if authorText == nil && replyModel == nil, !isBubbleFullFilled { + offset.y += (defaultContentInnerInset + 6) + } + } + + if hasBubble && authorText == nil && replyModel == nil && forwardNameLayout == nil { + offset.y -= (defaultContentInnerInset + self.mediaBubbleCornerInset * 2 - 1) + } else if hasBubble && authorText != nil { + offset.y += 2 + } + return offset + } + + override var elementsContentInset: CGFloat { + if hasBubble && isBubbleFullFilled { + return bubbleContentInset + } + return super.elementsContentInset + } + + override var _defaultHeight: CGFloat { + if hasBubble && isBubbleFullFilled && captionLayouts.isEmpty { + return contentOffset.y + defaultContentInnerInset - mediaBubbleCornerInset * 2 + } else if hasBubble && !isBubbleFullFilled { + return super._defaultHeight + 5 + } + + return super._defaultHeight + } + + override var realContentSize: NSSize { + var size = super.realContentSize + + if isBubbleFullFilled { + size.width -= bubbleContentInset * 2 + } + return size + } + + override var additionalLineForDateInBubbleState: CGFloat? { + let layout: TextViewLayout? + switch self.layout.type { + case .files: + layout = captionLayouts.last?.layout + case .photoOrVideo: + layout = captionLayouts.first?.layout + } + if let caption = layout { + if let line = caption.lines.last, line.frame.width > realContentSize.width - (rightSize.width + insetBetweenContentAndDate) { + return rightSize.height + } + } + return super.additionalLineForDateInBubbleState + } + + override var isFixedRightPosition: Bool { + return true + } + + override func makeContentSize(_ width: CGFloat) -> NSSize { + var _width: CGFloat = 0 + switch layout.type { + case .files: + for parameter in parameters { + let value = parameter.makeLabelsForWidth(min(width, 360)) + _width = max(_width, value) + } + case .photoOrVideo: + _width = min(width, 360) + } + + layout.measure(NSMakeSize(_width, min(_width, 320)), spacing: hasBubble ? 2 : 4) + + + var maxContentWidth = layout.dimensions.width + if hasBubble { + maxContentWidth -= bubbleDefaultInnerInset + } + for layout in captionLayouts { + layout.layout.measure(width: maxContentWidth) + } + self.captionLayouts = layout.applyCaptions(captionLayouts) + return layout.dimensions + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { + let result = super.makeSize(width, oldWidth: oldWidth) + return result + } + + override var topInset:CGFloat { + return 4 + } + + func contentNode(for index: Int) -> ChatMediaContentView.Type { + return ChatLayoutUtils.contentNode(for: layout.messages[index].media[0]) + } + + override func menuItems(in location: NSPoint) -> Signal<[ContextMenuItem], NoError> { + var _message: Message? = nil + let context = self.context + + for i in 0 ..< layout.count { + if NSPointInRect(location, layout.frame(at: i)) { + _message = layout.messages[i] + break + } + } + if let message = _message { + return chatMenuItems(for: message, item: self, chatInteraction: self.chatInteraction) + } + + guard let message = layout.messages.first else { + return .single([]) + } + + var items: [ContextMenuItem] = [] + + if canReplyMessage(message, peerId: chatInteraction.peerId, mode: chatInteraction.mode) { + items.append(ContextMenuItem(L10n.messageContextReply1, handler: { [weak chatInteraction] in + chatInteraction?.setupReplyMessage(message.id) + })) + } + + if chatInteraction.mode == .scheduled, let peer = chatInteraction.peer { + items.append(ContextMenuItem(L10n.chatContextScheduledSendNow, handler: { + _ = context.engine.messages.sendScheduledMessageNowInteractively(messageId: message.id).start() + })) + items.append(ContextMenuItem(L10n.chatContextScheduledReschedule, handler: { + showModal(with: DateSelectorModalController(context: context, defaultDate: Date(timeIntervalSince1970: TimeInterval(message.timestamp)), mode: .schedule(peer.id), selectedAt: { date in + _ = context.engine.messages.requestEditMessage(messageId: message.id, text: message.text, media: .keep, scheduleTime: Int32(date.timeIntervalSince1970)).start() + }), for: context.window) + })) + items.append(ContextSeparatorItem()) + } + + + items.append(ContextMenuItem(tr(L10n.messageContextSelect), handler: { [weak self] in + guard let `self` = self else {return} + let messageIds = self.layout.messages.map{$0.id} + self.chatInteraction.withToggledSelectedMessage({ current in + var current = current + for id in messageIds { + current = current.withToggledSelectedMessage(id) + } + return current + }) + })) + + var canDelete = true + for i in 0 ..< layout.count { + if !canDeleteMessage(layout.messages[i], account: context.account, mode: chatInteraction.mode) { + canDelete = false + break + } + } + + var canPin = true + for i in 0 ..< layout.count { + if let peer = peer { + if !canPinMessage(layout.messages[i], for: peer, account: context.account) { + canPin = false + break + } + } + } + + let chatInteraction = self.chatInteraction + let account = self.context.account + + if let peer = message.peers[message.id.peerId] as? TelegramChannel, peer.hasPermission(.pinMessages) || (peer.isChannel && peer.hasPermission(.editAllMessages)), chatInteraction.mode == .history { + if !message.flags.contains(.Unsent) && !message.flags.contains(.Failed) { + items.append(ContextMenuItem(tr(L10n.messageContextPin), handler: { + if peer.isSupergroup { + modernConfirm(for: mainWindow, account: account, peerId: nil, header: L10n.messageContextConfirmPin1, information: nil, thridTitle: L10n.messageContextConfirmNotifyPin, successHandler: { result in + chatInteraction.updatePinned(message.id, false, result != .thrid, false) + }) + } else { + chatInteraction.updatePinned(message.id, false, true, false) + } + })) + } + } else if message.id.peerId == account.peerId, chatInteraction.mode == .history { + items.append(ContextMenuItem(L10n.messageContextPin, handler: { + chatInteraction.updatePinned(message.id, false, true, false) + })) + } else if let peer = message.peers[message.id.peerId] as? TelegramGroup, peer.canPinMessage, chatInteraction.mode == .history { + items.append(ContextMenuItem(L10n.messageContextPin, handler: { + modernConfirm(for: mainWindow, account: account, peerId: nil, header: L10n.messageContextConfirmPin1, information: nil, thridTitle: L10n.messageContextConfirmNotifyPin, successHandler: { result in + chatInteraction.updatePinned(message.id, false, result == .thrid, false) + }) + })) + } + + + if canDelete { + items.append(ContextMenuItem(tr(L10n.messageContextDelete), handler: { [weak self] in + guard let `self` = self else {return} + self.chatInteraction.deleteMessages(self.layout.messages.map{$0.id}) + })) + } + + if let message = layout.messages.first, let peer = peer, canReplyMessage(message, peerId: peer.id, mode: chatInteraction.mode) { + items.append(ContextMenuItem(L10n.messageContextReply1, handler: { [weak self] in + self?.chatInteraction.setupReplyMessage(message.id) + })) + } + + if let message = layout.messages.last, !message.flags.contains(.Failed), !message.flags.contains(.Unsent), chatInteraction.mode == .history { + if let peer = message.peers[message.id.peerId] as? TelegramChannel { + items.append(ContextMenuItem(L10n.messageContextCopyMessageLink1, handler: { + _ = showModalProgress(signal: context.engine.messages.exportMessageLink(peerId: peer.id, messageId: message.id), for: context.window).start(next: { link in + if let link = link { + copyToClipboard(link) + } + }) + })) + } + } + + var editMessage: Message? = nil + for message in layout.messages { + if let _ = editMessage, !message.text.isEmpty { + editMessage = nil + break + } + if !message.text.isEmpty { + editMessage = message + } + } + if let editMessage = editMessage { + if canEditMessage(editMessage, chatInteraction: chatInteraction, context: context) { + items.append(ContextMenuItem(tr(L10n.messageContextEdit), handler: { [weak self] in + self?.chatInteraction.beginEditingMessage(editMessage) + })) + } + } + var canForward: Bool = true + for message in layout.messages { + if !canForwardMessage(message, chatInteraction: chatInteraction) { + canForward = false + break + } + } + + if canForward { + items.append(ContextMenuItem(tr(L10n.messageContextForward), handler: { [weak self] in + guard let `self` = self else {return} + self.chatInteraction.forwardMessages(self.layout.messages.map {$0.id}) + })) + } + + return .single(items) |> map { [weak self] items in + var items = items + if let captionLayout = self?.captionLayouts.first(where: { $0.id == _message?.stableId}) { + let text = captionLayout.layout.attributedString.string + items.insert(ContextMenuItem(tr(L10n.textCopy), handler: { + copyToClipboard(text) + }), at: 1) + +// if let view = self?.view as? ChatRowView, let textView = view.captionView, let window = textView.window { +// let point = textView.convert(window.mouseLocationOutsideOfEventStream, from: nil) +// if let layout = textView.layout { +// if let (link, _, range, _) = layout.link(at: point) { +// var text:String = layout.attributedString.string.nsstring.substring(with: range) +// if let link = link as? inAppLink { +// if case let .external(link, _) = link { +// text = link +// } +// } +// +// for i in 0 ..< items.count { +// if items[i].title == tr(L10n.messageContextCopyMessageLink1) { +// items.remove(at: i) +// break +// } +// } +// +// items.insert(ContextMenuItem(tr(L10n.messageContextCopyMessageLink1), handler: { +// copyToClipboard(text) +// }), at: 1) +// } +// } +// } + } + + return items + } + } + + override var instantlyResize: Bool { + return true + } + + override func viewClass() -> AnyClass { + return ChatGroupedView.self + } + +} + +class ChatGroupedView : ChatRowView , ModalPreviewRowViewProtocol { + + private(set) var contents: [ChatMediaContentView] = [] + private var selectionBackground: CornerView = CornerView() + + + private var forceClearContentBackground: Bool = false + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + selectionBackground.isDynamicColorUpdateLocked = true + selectionBackground.didChangeSuperview = { [weak self] in + self?.forceClearContentBackground = self?.selectionBackground.superview != nil + self?.updateColors() + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var contentColor: NSColor { + if forceClearContentBackground { + return .clear + } else { + return super.contentColor + } + } + + func fileAtPoint(_ point: NSPoint) -> (QuickPreviewMedia, NSView?)? { + guard let item = item as? ChatGroupedItem, let window = window as? Window else { return nil } + + let location = contentView.convert(window.mouseLocationOutsideOfEventStream, from: nil) + + for i in 0 ..< item.layout.count { + if NSPointInRect(location, item.layout.frame(at: i)) { + let contentNode = contents[i] + if contentNode is ChatGIFContentView { + if let file = contentNode.media as? TelegramMediaFile { + let reference = contentNode.parent != nil ? FileMediaReference.message(message: MessageReference(contentNode.parent!), media: file) : FileMediaReference.standalone(media: file) + return (.file(reference, GifPreviewModalView.self), contentNode) + } + } else if contentNode is ChatInteractiveContentView { + if let image = contentNode.media as? TelegramMediaImage { + let reference = contentNode.parent != nil ? ImageMediaReference.message(message: MessageReference(contentNode.parent!), media: image) : ImageMediaReference.standalone(media: image) + return (.image(reference, ImagePreviewModalView.self), contentNode) + } + } + } + } + + return nil + } + + override func forceClick(in location: NSPoint) { + if previewMediaIfPossible() { + + } else { + super.forceClick(in: location) + } + } + + override func previewMediaIfPossible() -> Bool { + guard let item = item as? ChatGroupedItem, let window = window as? Window else { return false } + + let location = contentView.convert(window.mouseLocationOutsideOfEventStream, from: nil) + + if contentView.mouseInside() { + for i in 0 ..< item.layout.count { + if NSPointInRect(location, item.layout.frame(at: i)) { + let result = contents[i].previewMediaIfPossible() + return result + } + } + } + return false + } + + override func updateColors() { + super.updateColors() + } + + override func notify(with value: Any, oldValue: Any, animated: Bool) { + super.notify(with: value, oldValue: oldValue, animated: animated) + } + + override func canDropSelection(in location: NSPoint) -> Bool { + let point = self.convert(location, from: nil) + return true//!NSPointInRect(point, contentView.frame) + } + + override func draw(_ dirtyRect: NSRect) { + + } + + override func updateMouse() { + super.updateMouse() + for content in contents { + content.updateMouse() + } + } + + + private func selectedIcon(_ item: ChatGroupedItem) -> CGImage { + return item.presentation.icons.chatGroupToggleSelected + } + + private func unselectedIcon(_ item: ChatGroupedItem) -> CGImage { + switch item.layout.type { + case .files: + return item.isBubbled ? (item.isIncoming ? item.presentation.icons.group_selection_foreground_bubble_incoming : item.presentation.icons.group_selection_foreground_bubble_outgoing) : item.presentation.icons.group_selection_foreground + case .photoOrVideo: + return item.presentation.icons.chatGroupToggleUnselected + } + } + + override func updateSelectingState(_ animated: Bool, selectingMode: Bool, item: ChatRowItem?, needUpdateColors: Bool) { + + + if let item = item as? ChatGroupedItem { + + if selectingMode { + if contents.count > 1 { + for content in contents { + let subviews = content.subviews + var selectingControl: SelectingControl? + for subview in subviews { + if subview is SelectingControl { + selectingControl = subview as? SelectingControl + break + } + } + if selectingControl == nil { + selectingControl = SelectingControl(unselectedImage: unselectedIcon(item), selectedImage: selectedIcon(item)) + content.addSubview(selectingControl!) + if animated { + selectingControl?.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + selectingControl?.layer?.animateScaleSpring(from: 0.2, to: 1.0, duration: 0.2) + } + } + if let selectingControl = selectingControl { + selectingControl.setFrameOrigin(selectionOrigin(content)) + } + } + } + } else { + for content in contents { + let subviews = content.subviews + for subview in subviews { + if subview is SelectingControl { + if animated { + subview.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false) + subview.layer?.animateScaleSpring(from: 1, to: 0.2, duration: 0.2, removeOnCompletion: false, completion: { [weak subview] completed in + subview?.removeFromSuperview() + }) + } else { + subview.removeFromSuperview() + } + } + } + } + } + if let selectionState = item.chatInteraction.presentation.selectionState { + for i in 0 ..< contents.count { + loop: for subview in contents[i].subviews { + if let select = subview as? SelectingControl { + select.set(selected: selectionState.selectedIds.contains(item.layout.messages[i].id), animated: animated) + break loop + } + } + } + } + } + super.updateSelectingState(animated, selectingMode: selectingMode, item: item, needUpdateColors: needUpdateColors) + } + + override func updateSelectionViewAfterUpdateState(item: ChatRowItem, animated: Bool) { + guard let item = item as? ChatGroupedItem else {return} + guard let selectingView = selectingView else {return} + + + + var selected: Bool = true + for message in item.layout.messages { + if !item.chatInteraction.presentation.isSelectedMessageId(message.id) { + selected = false + break + } + } + selectingView.set(selected: selected, animated: animated) + } + + override func set(item: TableRowItem, animated: Bool) { + + guard let item = item as? ChatGroupedItem else {return} + + + + if contents.count > item.layout.count { + let contentCount = contents.count + let layoutCount = item.layout.count + + for i in layoutCount ..< contentCount { + contents[i].removeFromSuperview() + } + contents = contents.subarray(with: NSMakeRange(0, layoutCount)) + } else if contents.count < item.layout.count { + let contentCount = contents.count + for i in contentCount ..< item.layout.count { + let node = item.contentNode(for: i) + let view = node.init(frame:NSZeroRect) + contents.append(view) + } + } + + for i in 0 ..< contents.count { + if contents[i].className != item.contentNode(for: i).className() { + let node = item.contentNode(for: i) + let view = node.init(frame:NSZeroRect) + contents[i] = view + } + } + + self.contentView.removeAllSubviews() + + for content in contents { + addSubview(content) + } + + super.set(item: item, animated: animated) + + assert(contents.count == item.layout.count) + + let approximateSynchronousValue = item.approximateSynchronousValue + + contentView.frame = self.contentFrame(item) + + var offset: CGFloat = 0 + + for i in 0 ..< item.layout.count { + contents[i].change(size: item.layout.frame(at: i).size, animated: animated) + var positionFlags: LayoutPositionFlags = item.isBubbled ? item.positionFlags ?? item.layout.position(at: i) : [] + + if item.hasBubble { + if item.captionLayouts.first(where: { $0.id == item.lastMessage?.stableId }) != nil || item.commentsBubbleData != nil { + positionFlags.remove(.bottom) + } + if item.authorText != nil || item.replyModel != nil || item.forwardNameLayout != nil { + positionFlags.remove(.top) + } + } + + + contents[i].update(with: item.layout.messages[i].media[0], size: item.layout.frame(at: i).size, context: item.context, parent: item.layout.messages[i], table: item.table, parameters: item.parameters[i], animated: animated, positionFlags: positionFlags, approximateSynchronousValue: approximateSynchronousValue) + + contents[i].change(pos: item.layout.frame(at: i).origin.offsetBy(dx: 0, dy: offset), animated: animated) + + } + + needsLayout = true + } + + override var needsDisplay: Bool { + get { + return super.needsDisplay + } + set { + super.needsDisplay = newValue + for content in contents { + content.needsDisplay = newValue + } + } + } + override var backgroundColor: NSColor { + didSet { + for content in contents { + content.backgroundColor = backdorColor + } + } + } + + + override func toggleSelected(_ select: Bool, in point: NSPoint) { + guard let item = item as? ChatGroupedItem else { return } + + let location = contentView.convert(point, from: nil) + var applied: Bool = contentView.mouseInside() + if contentView.mouseInside() { + for i in 0 ..< item.layout.count { + if NSPointInRect(location, item.layout.frame(at: i)) { + let id = item.layout.messages[i].id + item.chatInteraction.withToggledSelectedMessage({ current in + if (select && !current.isSelectedMessageId(id)) || (!select && current.isSelectedMessageId(id)) { + return current.withToggledSelectedMessage(id) + } + return current + }) + applied = true + break + } + } + } + + if !applied { + item.chatInteraction.withToggledSelectedMessage({ current in + return item.layout.messages.reduce(current, { current, message -> ChatPresentationInterfaceState in + if (select && !current.isSelectedMessageId(message.id)) || (!select && current.isSelectedMessageId(message.id)) { + return current.withToggledSelectedMessage(message.id) + } + return current + }) + }) + } + + } + + + + override func forceSelectItem(_ item: ChatRowItem, onRightClick: Bool) { + + guard let item = item as? ChatGroupedItem else {return} + guard let window = window as? Window else {return} + + if onRightClick { + item.chatInteraction.withToggledSelectedMessage({ current in + var current: ChatPresentationInterfaceState = current + for message in item.layout.messages { + current = current.withToggledSelectedMessage(message.id) + } + return current + }) + return + } + + guard item.chatInteraction.presentation.state == .selecting else {return} + + let location = contentView.convert(window.mouseLocationOutsideOfEventStream, from: nil) + + var selected: Bool = contentView.mouseInside() + if contentView.mouseInside() { + for i in 0 ..< item.layout.count { + if NSPointInRect(location, item.layout.frame(at: i)) { + item.chatInteraction.withToggledSelectedMessage({ + $0.withToggledSelectedMessage(item.layout.messages[i].id) + }) + selected = true + break + } + } + } + + + if !selected { + let select = !isHasSelectedItem + item.chatInteraction.withToggledSelectedMessage({ current in + return item.layout.messages.reduce(current, { current, message -> ChatPresentationInterfaceState in + if (select && !current.isSelectedMessageId(message.id)) || (!select && current.isSelectedMessageId(message.id)) { + return current.withToggledSelectedMessage(message.id) + } + return current + }) + }) + } + + } + + override func viewWillMove(toSuperview newSuperview: NSView?) { + if newSuperview == nil { + for content in contents { + content.willRemove() + } + } + } + + override func interactionContentView(for innerId: AnyHashable, animateIn: Bool ) -> NSView { + + if let innerId = innerId.base as? ChatHistoryEntryId { + switch innerId { + case .message(let message): + for content in contents { + if content.parent?.id == message.id { + return content.interactionContentView(for: innerId, animateIn: animateIn) + } + } + default: + break + } + } + + return super.interactionContentView(for: innerId, animateIn: animateIn) + } + + override func interactionControllerDidFinishAnimation(interactive: Bool, innerId: AnyHashable) { + + } + + override func addAccesoryOnCopiedView(innerId: AnyHashable, view: NSView) { + + guard let item = item as? ChatRowItem else {return} + if let innerId = innerId.base as? ChatHistoryEntryId { + switch innerId { + case .message(let message): + for content in contents { + if content.parent?.id == message.id { + let rect = rightView.convert(rightView.bounds, to: content.superview) + if NSIntersectsRect(rect, content.frame), item.isStateOverlayLayout { + let rightView = ChatRightView(frame: NSZeroRect) + rightView.set(item: item, animated: false) + var rect = self.rightView.convert(self.rightView.bounds, to: content) + + if content.visibleRect.minY < rect.midY && content.visibleRect.minY + content.visibleRect.height > rect.midY { + rect.origin.y = content.frame.height - rect.maxY + rightView.frame = rect + view.addSubview(rightView) + } + + } + content.addAccesoryOnCopiedView(view: view) + + } + } + default: + break + } + } + + + } + + + override func isSelectInGroup(_ location: NSPoint) -> Bool { + guard let item = item as? ChatGroupedItem else {return false} + + guard item.chatInteraction.presentation.state == .selecting else {return false} + + let location = contentView.convert(location, from: nil) + + for i in 0 ..< item.layout.count { + if NSPointInRect(location, item.layout.frame(at: i)) { + return item.chatInteraction.presentation.isSelectedMessageId(item.layout.messages[i].id) + } + } + return false + } + + private var isHasSelectedItem: Bool { + guard let item = item as? ChatGroupedItem else { + return false + } + for message in item.layout.messages { + if item.chatInteraction.presentation.isSelectedMessageId(message.id) { + return true + } + } + return false + } + + override var backdorColor: NSColor { + guard let item = item as? ChatGroupedItem, !item.isBubbled else { + return super.backdorColor + } + + + if let _ = contextMenu { + return item.presentation.colors.selectMessage + } + + + for message in item.layout.messages { + if item.chatInteraction.presentation.isSelectedMessageId(message.id) { + return item.presentation.colors.selectMessage + } + } + + return super.backdorColor + } + + + private func highlightFrameAndColor(_ item: ChatGroupedItem, at index: Int) -> (color: NSColor, frame: NSRect, flags: LayoutPositionFlags, superview: NSView) { + switch item.layout.type { + case .photoOrVideo: + return (color: NSColor.black.withAlphaComponent(0.4), frame: item.layout.frame(at: index), flags: item.isBubbled ? item.positionFlags ?? item.layout.position(at: index) : [], superview: self.contentView) + case .files: + var frame = item.layout.frame(at: index) + let contentFrame = self.contentFrame(item) + let bubbleFrame = self.bubbleFrame(item) + if item.hasBubble { + + frame.origin.x = 0 + frame.size.width = bubbleFrame.width + + var caption: CGFloat = 0 + + if let layout = item.captionLayouts.first(where: { $0.id == item.layout.messages[index].stableId }) { + caption = layout.layout.layoutSize.height + 6 + } + + + frame.size.height += 8 + if index == 0 { + frame.size.height += contentFrame.minY + } else if index == item.layout.count - 1 { + frame.origin.y += contentFrame.minY + frame.size.height += contentFrame.minY + } else { + frame.origin.y += contentFrame.minY + } + + frame.size.height += caption + + + frame.origin.y = bubbleFrame.height - frame.maxY + 6 + + return (item.isIncoming ? item.presentation.colors.bubbleBackground_incoming.darker().withAlphaComponent(0.5) : item.presentation.colors.blendedOutgoingColors.darker().withAlphaComponent(0.5) + , frame: frame, flags: [], superview: self.bubbleView) + } else { + + frame.origin.x = 0 + frame.size.width = self.frame.width + frame.size.height += 8 + + var caption: CGFloat = 0 + + if let layout = item.captionLayouts.first(where: { $0.id == item.layout.messages[index].stableId }) { + caption = layout.layout.layoutSize.height + 6 + } + + if index == 0 { + frame.size.height += contentFrame.minY + } else if index == item.layout.count - 1 { + frame.origin.y += contentFrame.minY + frame.size.height += contentFrame.minY + } else { + frame.origin.y += contentFrame.minY + } + + frame.size.height += caption + + frame.origin.y -= 4 + + return (color: item.presentation.colors.accentIcon.withAlphaComponent(0.15), frame: frame, flags: [], superview: self.rowView) + } + } + } + + override func focusAnimation(_ innerId: AnyHashable?) { + if let innerId = innerId { + guard let item = item as? ChatGroupedItem else {return} + + for i in 0 ..< item.layout.count { + if AnyHashable(ChatHistoryEntryId.message(item.layout.messages[i])) == innerId { + + let data = highlightFrameAndColor(item, at: i) + + selectionBackground.removeFromSuperview() + selectionBackground.frame = data.frame + selectionBackground.backgroundColor = data.color + + var positionFlags: LayoutPositionFlags = data.flags + + if item.hasBubble { + if item.captionLayouts.first(where: { $0.id == item.lastMessage?.stableId }) == nil { + positionFlags.remove(.bottom) + } + if item.authorText != nil || item.replyModel != nil || item.forwardNameLayout != nil { + positionFlags.remove(.top) + } + } + selectionBackground.layer?.opacity = 0 + + selectionBackground.positionFlags = positionFlags + data.superview.addSubview(selectionBackground) + + let animation: CABasicAnimation = makeSpringAnimation("opacity") + + animation.fromValue = selectionBackground.layer?.presentation()?.opacity ?? 0 + animation.toValue = 1.0 + animation.autoreverses = true + animation.isRemovedOnCompletion = true + animation.fillMode = .forwards + animation.delegate = CALayerAnimationDelegate(completion: { [weak self] completed in + self?.selectionBackground.removeFromSuperview() + }) + animation.isAdditive = false + + selectionBackground.layer?.add(animation, forKey: "opacity") + + break + } + } + } else { + super.focusAnimation(innerId) + } + } + + + override func onShowContextMenu() { + guard let window = window as? Window else {return} + guard let item = item as? ChatGroupedItem else {return} + + let point = contentView.convert(window.mouseLocationOutsideOfEventStream, from: nil) + + var selected: Bool = false + + for i in 0 ..< item.layout.count { + if NSPointInRect(point, item.layout.frame(at: i)) { + + let data = highlightFrameAndColor(item, at: i) + selectionBackground.removeFromSuperview() + selectionBackground.layer?.opacity = 1.0 + selectionBackground.frame = data.frame + selectionBackground.backgroundColor = data.color + var positionFlags: LayoutPositionFlags = data.flags + + if item.hasBubble { + if item.captionLayouts.first(where: { $0.id == item.lastMessage?.stableId }) != nil { + positionFlags.remove(.bottom) + } + if item.authorText != nil || item.replyModel != nil || item.forwardNameLayout != nil { + positionFlags.remove(.top) + } + } + + selectionBackground.positionFlags = positionFlags + data.superview.addSubview(selectionBackground) + selected = true + break + } + } + + if !selected { + super.onShowContextMenu() + } + } + + override func onCloseContextMenu() { + super.onCloseContextMenu() + selectionBackground.removeFromSuperview() + } + + override func canMultiselectTextIn(_ location: NSPoint) -> Bool { + let point = contentView.convert(location, from: nil) + for content in contents { + if NSPointInRect(point, content.frame) { + return false + } + } + return true + } + + override func contentFrame(_ item: ChatRowItem) -> NSRect { + var rect = super.contentFrame(item) + guard let item = item as? ChatGroupedItem else { + return rect + } + if item.isBubbled, item.isBubbleFullFilled { + rect.origin.x -= item.bubbleContentInset + if item.hasBubble { + rect.origin.x += item.mediaBubbleCornerInset + } + } + + return rect + } + + func selectionOrigin(_ content: ChatMediaContentView) -> CGPoint { + guard let item = item as? ChatGroupedItem else {return .zero} + + switch item.layout.type { + case .files: + let subviews = content.subviews + for subview in subviews { + if subview is SelectingControl { + if content is ChatAudioContentView { + return NSMakePoint(26, 18) + } else if let content = content as? ChatFileContentView { + if content.isHasThumb { + return NSMakePoint(40, 6) + } else { + return NSMakePoint(26, 18) + } + } + } + } + case .photoOrVideo: + let subviews = content.subviews + for subview in subviews { + if subview is SelectingControl { + return NSMakePoint(content.frame.width - subview.frame.width - 5, 5) + } + } + } + return .zero + } + + override func layout() { + super.layout() + guard let item = item as? ChatGroupedItem else {return} + + assert(contents.count == item.layout.count) + + for i in 0 ..< item.layout.count { + contents[i].setFrameOrigin(item.layout.frame(at: i).origin) + } + + for content in contents { + let subviews = content.subviews + for subview in subviews { + if subview is SelectingControl { + subview.setFrameOrigin(selectionOrigin(content)) + break + } + } + } + + } + +} diff --git a/Telegram-Mac/ChatHeaderController.swift b/Telegram-Mac/ChatHeaderController.swift index ba0d671351..f84ce48faa 100644 --- a/Telegram-Mac/ChatHeaderController.swift +++ b/Telegram-Mac/ChatHeaderController.swift @@ -6,20 +6,33 @@ // Copyright © 2016 Telegram. All rights reserved. // + import Cocoa import TGUIKit -import SwiftSignalKitMac -import TelegramCoreMac -import PostboxMac +import SwiftSignalKit +import TelegramCore + +import Postbox + + + + +protocol ChatHeaderProtocol { + func update(with state: ChatHeaderState, animated: Bool) + init(_ chatInteraction:ChatInteraction, state: ChatHeaderState, frame: NSRect) +} + enum ChatHeaderState : Identifiable, Equatable { - case none - case search(ChatSearchInteractions) - case addContact - case pinned(MessageId) - case report + case none(ChatActiveGroupCallInfo?) + case search(ChatActiveGroupCallInfo?, ChatSearchInteractions, Peer?, String?) + case addContact(ChatActiveGroupCallInfo?, block: Bool, autoArchived: Bool) + case shareInfo(ChatActiveGroupCallInfo?) + case pinned(ChatActiveGroupCallInfo?, ChatPinnedMessage, doNotChangeTable: Bool) + case report(ChatActiveGroupCallInfo?, autoArchived: Bool) + case promo(ChatActiveGroupCallInfo?, PromoChatListItem.Kind) var stableId:Int { switch self { case .none: @@ -32,101 +45,280 @@ enum ChatHeaderState : Identifiable, Equatable { return 3 case .pinned: return 4 + case .promo: + return 5 + case .shareInfo: + return 6 + } + } + + var voiceChat: ChatActiveGroupCallInfo? { + switch self { + case let .none(voiceChat): + return voiceChat + case let .search(voiceChat, _, _, _): + return voiceChat + case let .report(voiceChat, _): + return voiceChat + case let .addContact(voiceChat, _, _): + return voiceChat + case let .pinned(voiceChat, _, _): + return voiceChat + case let .promo(voiceChat, _): + return voiceChat + case let .shareInfo(voiceChat): + return voiceChat + } + } + + var primaryClass: AnyClass? { + switch self { + case .addContact: + return AddContactView.self + case .shareInfo: + return ShareInfoView.self + case .pinned: + return ChatPinnedView.self + case .search: + return ChatSearchHeader.self + case .report: + return ChatReportView.self + case .promo: + return ChatSponsoredView.self + case .none: + return nil + } + } + var secondaryClass: AnyClass? { + if let _ = voiceChat { + return ChatGroupCallView.self } + return nil } var height:CGFloat { + return primaryHeight + secondaryHeight + } + + var primaryHeight:CGFloat { + var height: CGFloat = 0 switch self { case .none: - return 0 + height += 0 case .search: - return 44 + height += 44 case .report: - return 44 + height += 44 case .addContact: - return 44 + height += 44 + case .shareInfo: + height += 44 case .pinned: - return 44 + height += 44 + case .promo: + height += 44 + } + return height + } + + var secondaryHeight:CGFloat { + var height: CGFloat = 0 + if let _ = voiceChat { + height += 44 + } + return height + } + + var toleranceHeight: CGFloat { + switch self { + case let .pinned(_, _, doNotChangeTable): + return doNotChangeTable ? height - primaryHeight : height + default: + return height } } static func ==(lhs:ChatHeaderState, rhs: ChatHeaderState) -> Bool { switch lhs { - case let .pinned(pinnedId): - if case .pinned(pinnedId) = rhs { + case let .pinned(call, pinnedId, value): + if case .pinned(call, pinnedId, value) = rhs { + return true + } else { + return false + } + case let .addContact(call, block, autoArchive): + if case .addContact(call, block, autoArchive) = rhs { return true } else { return false } default: - return lhs.stableId == rhs.stableId + return lhs.stableId == rhs.stableId && lhs.voiceChat == rhs.voiceChat } } } - - class ChatHeaderController { - private var _headerState:ChatHeaderState = .none + private var _headerState:ChatHeaderState = .none(nil) private let chatInteraction:ChatInteraction private(set) var currentView:View? - + + private var primaryView: View? + private var seconderyView : View? + var state:ChatHeaderState { return _headerState } func updateState(_ state:ChatHeaderState, animated:Bool, for view:View) -> Void { if _headerState != state { - let previousState = _headerState _headerState = state - - if let current = currentView { - if animated { - currentView?.layer?.animatePosition(from: NSZeroPoint, to: NSMakePoint(0, -previousState.height), duration: 0.2, removeOnCompletion:false, completion: { [weak current] complete in - if complete { - current?.removeFromSuperview() - } - - }) - } else { - currentView?.removeFromSuperview() - currentView = nil + + let (primary, secondary) = viewIfNecessary(primarySize: NSMakeSize(view.frame.width, state.primaryHeight), secondarySize: NSMakeSize(view.frame.width, state.secondaryHeight), animated: animated, p_v: self.primaryView, s_v: self.seconderyView) + + let previousPrimary = self.primaryView + let previousSecondary = self.seconderyView + + self.primaryView = primary + self.seconderyView = secondary + + var removed: [View] = [] + var added:[(View, NSPoint, NSPoint, View?)] = [] + var updated:[(View, NSPoint, View?)] = [] + + if previousSecondary == nil || previousSecondary != secondary { + if let previousSecondary = previousSecondary { + removed.append(previousSecondary) + } + if let secondary = secondary { + added.append((secondary, NSMakePoint(0, -state.secondaryHeight), NSMakePoint(0, 0), nil)) } } - - currentView = viewIfNecessary(NSMakeSize(view.frame.width, state.height)) - - if let newView = currentView { - view.addSubview(newView) - newView.layer?.removeAllAnimations() - if animated { - newView.layer?.animatePosition(from: NSMakePoint(0,-state.height), to: NSZeroPoint, duration: 0.2) + if previousPrimary == nil || previousPrimary != primary { + if let previousPrimary = previousPrimary { + removed.append(previousPrimary) + } + if let primary = primary { + added.append((primary, NSMakePoint(0, -(state.height - state.secondaryHeight)), NSMakePoint(0, state.secondaryHeight), secondary)) } } + + if (previousSecondary == nil && secondary != nil) || previousSecondary != nil && secondary == nil { + if let primary = primary, previousPrimary == primary { + updated.append((primary, NSMakePoint(0, state.secondaryHeight), secondary)) + } + } + if (previousPrimary == nil && primary != nil) || previousPrimary != nil && primary == nil { + if let secondary = secondary, previousSecondary == secondary { + updated.append((secondary, NSMakePoint(0, 0), nil)) + } + } + + if !added.isEmpty || primary != nil || secondary != nil { + let current: View + if let view = currentView { + current = view + current.change(size: NSMakeSize(view.frame.width, state.height), animated: animated) + } else { + current = View(frame: NSMakeRect(0, 0, view.frame.width, state.height)) + current.autoresizingMask = [.width] + current.autoresizesSubviews = true + view.addSubview(current) + self.currentView = current + } + for view in removed { + if animated { +// view.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in +// view?.removeFromSuperview() +// }) + view.layer?.animatePosition(from: view.frame.origin, to: NSMakePoint(0, view.frame.minY - view.frame.height), removeOnCompletion: false, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } else { + view.removeFromSuperview() + } + } + for (view, from, to, below) in added { + current.addSubview(view, positioned: .below, relativeTo: below) + view.setFrameOrigin(to) + + if animated { + view.layer?.animatePosition(from: from, to: to, duration: 0.2) + // view.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } + for (view, point, above) in updated { + current.addSubview(view, positioned: .below, relativeTo: above) + view.change(pos: point, animated: animated) + } + } else { + if let currentView = currentView { + self.currentView = nil + if animated { + currentView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false) + currentView.layer?.animatePosition(from: currentView.frame.origin, to: currentView.frame.origin - NSMakePoint(0, currentView.frame.height), removeOnCompletion:false, completion: { [weak currentView] _ in + currentView?.removeFromSuperview() + }) + } else { + currentView.removeFromSuperview() + } + } + } + + } } + + func applySearchResponder() { + (primaryView as? ChatSearchHeader)?.applySearchResponder(true) + } - private func viewIfNecessary(_ size:NSSize) -> View? { - let view:View? - switch _headerState { - case .addContact: - view = AddContactView(chatInteraction) - case let .pinned(messageId): - view = ChatPinnedView(messageId, chatInteraction: chatInteraction) - case let .search(interactions): - view = ChatSearchHeader(interactions, chatInteraction: chatInteraction) - case .report: - view = ChatReportView(chatInteraction) - case .none: - view = nil - + private func viewIfNecessary(primarySize: NSSize, secondarySize: NSSize, animated: Bool, p_v: View?, s_v: View?) -> (primary: View?, secondary: View?) { + let primary:View? + let secondary:View? + let primaryRect: NSRect = .init(origin: .zero, size: primarySize) + let secondaryRect: NSRect = .init(origin: .zero, size: secondarySize) + if p_v == nil || p_v?.className != NSStringFromClass(_headerState.primaryClass ?? NSView.self) { + switch _headerState { + case .addContact: + primary = AddContactView(chatInteraction, state: _headerState, frame: primaryRect) + case .shareInfo: + primary = ShareInfoView(chatInteraction, state: _headerState, frame: primaryRect) + case .pinned: + primary = ChatPinnedView(chatInteraction, state: _headerState, frame: primaryRect) + case .search: + primary = ChatSearchHeader(chatInteraction, state: _headerState, frame: primaryRect) + case .report: + primary = ChatReportView(chatInteraction, state: _headerState, frame: primaryRect) + case .promo: + primary = ChatSponsoredView(chatInteraction, state: _headerState, frame: primaryRect) + case .none: + primary = nil + } + primary?.autoresizingMask = [.width] + } else { + primary = p_v + (primary as? ChatHeaderProtocol)?.update(with: _headerState, animated: animated) } - view?.frame = NSMakeRect(0, 0, size.width, size.height) - return view + if let _ = self._headerState.voiceChat { + if s_v == nil || s_v?.className != NSStringFromClass(_headerState.secondaryClass ?? NSView.self) { + secondary = ChatGroupCallView(chatInteraction, state: _headerState, frame: secondaryRect) + secondary?.autoresizingMask = [.width] + } else { + secondary = s_v + (secondary as? ChatHeaderProtocol)?.update(with: _headerState, animated: animated) + } + } else { + secondary = nil + } + + primary?.setFrameSize(primarySize) + secondary?.setFrameSize(secondarySize) + return (primary: primary, secondary: secondary) } init(_ chatInteraction:ChatInteraction) { @@ -140,66 +332,307 @@ struct ChatSearchInteractions { let results:(String)->Void let calendarAction:(Date)->Void let cancel:()->Void - let searchRequest:(String, PeerId?) -> Signal<[Message],Void> + let searchRequest:(String, PeerId?, SearchMessagesState?) -> Signal<([Message], SearchMessagesState?), NoError> +} + +private class ChatSponsoredModel: ChatAccessoryModel { + + + init(title: String, text: String) { + super.init() + update(title: title, text: text) + } + + func update(title: String, text: String) { + //L10n.chatProxySponsoredCapTitle + self.headerAttr = .initialize(string: title, color: theme.colors.link, font: .medium(.text)) + self.messageAttr = .initialize(string: text, color: theme.colors.text, font: .normal(.text)) + nodeReady.set(.single(true)) + self.setNeedDisplay() + } } -class ChatPinnedView : Control { - private let node:ReplyModel +private extension PromoChatListItem.Kind { + var title: String { + switch self { + case .proxy: + return L10n.chatProxySponsoredCapTitle + case .psa: + return L10n.psaChatTitle + } + } + var text: String { + switch self { + case .proxy: + return L10n.chatProxySponsoredCapDesc + case let .psa(type, _): + return localizedPsa("psa.chat.text", type: type) + } + } + var learnMore: String? { + switch self { + case .proxy: + return nil + case let .psa(type, _): + let localized = localizedPsa("psa.chat.alert.learnmore", type: type) + return localized != localized ? localized : nil + } + } +} + +private final class ChatSponsoredView : Control, ChatHeaderProtocol { private let chatInteraction:ChatInteraction - private let readyDisposable = MetaDisposable() private let container:ChatAccessoryView = ChatAccessoryView() private let dismiss:ImageButton = ImageButton() - private let loadMessageDisposable = MetaDisposable() - init(_ messageId:MessageId, chatInteraction:ChatInteraction) { - node = ReplyModel(replyMessageId: messageId, account: chatInteraction.account, isPinned: true) + private var node: ChatSponsoredModel? + private var kind: PromoChatListItem.Kind? + required init(_ chatInteraction:ChatInteraction, state: ChatHeaderState, frame: NSRect) { self.chatInteraction = chatInteraction - super.init() + super.init(frame: frame) + dismiss.disableActions() self.dismiss.set(image: theme.icons.dismissPinned, for: .Normal) - self.dismiss.sizeToFit() + _ = self.dismiss.sizeToFit() self.set(handler: { [weak self] _ in - self?.chatInteraction.focusMessageId(nil, messageId, .center(id: 0, animated: true, focus: true, inset: 0)) + guard let chatInteraction = self?.chatInteraction, let kind = self?.kind else { + return + } + switch kind { + case .proxy: + confirm(for: chatInteraction.context.window, header: L10n.chatProxySponsoredAlertHeader, information: L10n.chatProxySponsoredAlertText, cancelTitle: "", thridTitle: L10n.chatProxySponsoredAlertSettings, successHandler: { [weak chatInteraction] result in + switch result { + case .thrid: + chatInteraction?.openProxySettings() + default: + break + } + }) + case .psa: + if let learnMore = kind.learnMore { + confirm(for: chatInteraction.context.window, header: kind.title, information: kind.text, cancelTitle: "", thridTitle: learnMore, successHandler: { result in + switch result { + case .thrid: + execute(inapp: .external(link: learnMore, false)) + default: + break + } + }) + } + + } + + + }, for: .Click) + + dismiss.set(handler: { _ in + FastSettings.removePromoTitle(for: chatInteraction.peerId) + chatInteraction.update({$0.withoutInitialAction()}) + }, for: .SingleClick) + + addSubview(dismiss) + container.userInteractionEnabled = false + self.style = ControlStyle(backgroundColor: theme.colors.background) + addSubview(container) + + update(with: state, animated: false) + + } + + func update(with state: ChatHeaderState, animated: Bool) { + switch state { + case let .promo(_, kind): + self.kind = kind + default: + self.kind = nil + } + if let kind = kind { + node = ChatSponsoredModel(title: kind.title, text: kind.text) + node?.view = container + } + + updateLocalizationAndTheme(theme: theme) + needsLayout = true + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) + self.backgroundColor = theme.colors.background + self.dismiss.set(image: theme.icons.dismissPinned, for: .Normal) + container.backgroundColor = theme.colors.background + if let kind = kind { + node?.update(title: kind.title, text: kind.text) + } + } + + override func layout() { + if let node = node { + node.measureSize(frame.width - 70) + container.setFrameSize(frame.width - 70, node.size.height) + } + container.centerY(x: 20) + dismiss.centerY(x: frame.width - 20 - dismiss.frame.width) + node?.setNeedDisplay() + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + ctx.setFillColor(theme.colors.border.cgColor) + ctx.fill(NSMakeRect(0, layer.frame.height - .borderSize, layer.frame.width, .borderSize)) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } +} + +class ChatPinnedView : Control, ChatHeaderProtocol { + private var node:ReplyModel? + private let chatInteraction:ChatInteraction + private let readyDisposable = MetaDisposable() + private var container:ChatAccessoryView = ChatAccessoryView() + private let dismiss:ImageButton = ImageButton() + private let loadMessageDisposable = MetaDisposable() + private var pinnedMessage: ChatPinnedMessage? + private let particleList: VerticalParticleListControl = VerticalParticleListControl() + required init(_ chatInteraction:ChatInteraction, state: ChatHeaderState, frame: NSRect) { + + self.chatInteraction = chatInteraction + super.init(frame: frame) + + dismiss.disableActions() + + self.set(handler: { [weak self] _ in + guard let `self` = self, let pinnedMessage = self.pinnedMessage else { + return + } + if self.chatInteraction.mode.threadId == pinnedMessage.messageId { + self.chatInteraction.scrollToTheFirst() + } else { + self.chatInteraction.focusPinnedMessageId(pinnedMessage.messageId) + } + }, for: .Click) dismiss.set(handler: { [weak self] _ in - self?.chatInteraction.updatePinned(messageId, true, false) + guard let `self` = self, let pinnedMessage = self.pinnedMessage else { + return + } + if pinnedMessage.totalCount > 1 { + self.chatInteraction.openPinnedMessages(pinnedMessage.messageId) + } else { + self.chatInteraction.updatePinned(pinnedMessage.messageId, true, false, false) + } }, for: .SingleClick) addSubview(dismiss) container.userInteractionEnabled = false self.style = ControlStyle(backgroundColor: theme.colors.background) addSubview(container) - node.view = container - readyDisposable.set(node.nodeReady.get().start(next: { [weak self] result in - self?.needsLayout = true + + + particleList.frame = NSMakeRect(22, 5, 2, 34) + + addSubview(particleList) + + update(with: state, animated: false) + } + + func update(with state: ChatHeaderState, animated: Bool) { + switch state { + case let .pinned(_, message, _): + self.update(message, animated: animated) + default: + break + } + } + + private func update(_ pinnedMessage: ChatPinnedMessage, animated: Bool) { + + let animated = animated && (self.pinnedMessage != nil && (!pinnedMessage.isLatest || (self.pinnedMessage?.isLatest != pinnedMessage.isLatest))) + + particleList.update(count: pinnedMessage.totalCount, selectedIndex: pinnedMessage.index, animated: animated) + + self.dismiss.set(image: pinnedMessage.totalCount <= 1 ? theme.icons.dismissPinned : theme.icons.chat_pinned_list, for: .Normal) + + if pinnedMessage.messageId != self.pinnedMessage?.messageId { + let oldContainer = self.container + let newContainer = ChatAccessoryView() + newContainer.userInteractionEnabled = false + + let newNode = ReplyModel(replyMessageId: pinnedMessage.messageId, context: chatInteraction.context, replyMessage: pinnedMessage.message, isPinned: true, headerAsName: chatInteraction.mode.threadId != nil, customHeader: pinnedMessage.isLatest ? nil : pinnedMessage.totalCount == 2 ? L10n.chatHeaderPinnedPrevious : L10n.chatHeaderPinnedMessageNumer(pinnedMessage.totalCount - pinnedMessage.index), drawLine: false) + + newNode.view = newContainer + + addSubview(newContainer) + + let width = frame.width - (40 + (dismiss.isHidden ? 0 : 30)) + newNode.measureSize(width) + newContainer.setFrameSize(width, newNode.size.height) + newContainer.centerY(x: 24) - if !result, let chatInteraction = self?.chatInteraction { - _ = requestUpdatePinnedMessage(account: chatInteraction.account, peerId: chatInteraction.peerId, update: .clear).start() + if animated { + let oldFrom = oldContainer.frame.origin + let oldTo = pinnedMessage.messageId > self.pinnedMessage!.messageId ? NSMakePoint(oldContainer.frame.minX, -oldContainer.frame.height) : NSMakePoint(oldContainer.frame.minX, frame.height) + + + oldContainer.layer?.animatePosition(from: oldFrom, to: oldTo, duration: 0.2, timingFunction: .easeInEaseOut, removeOnCompletion: false, completion: { [weak oldContainer] _ in + oldContainer?.removeFromSuperview() + }) + oldContainer.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, timingFunction: .easeInEaseOut, removeOnCompletion: false) + + + let newTo = newContainer.frame.origin + let newFrom = pinnedMessage.messageId < self.pinnedMessage!.messageId ? NSMakePoint(newContainer.frame.minX, -newContainer.frame.height) : NSMakePoint(newContainer.frame.minX, frame.height) + + + newContainer.layer?.animatePosition(from: newFrom, to: newTo, duration: 0.2, timingFunction: .easeInEaseOut) + newContainer.layer?.animateAlpha(from: 0, to: 1, duration: 0.2 + , timingFunction: .easeInEaseOut) + } else { + oldContainer.removeFromSuperview() } - })) - updateLocalizationAndTheme() + + self.container = newContainer + self.node = newNode + } + self.pinnedMessage = pinnedMessage + + updateLocalizationAndTheme(theme: theme) } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - node.update() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) + node?.update() self.backgroundColor = theme.colors.background - self.dismiss.set(image: theme.icons.dismissPinned, for: .Normal) + if let pinnedMessage = pinnedMessage { + self.dismiss.set(image: pinnedMessage.totalCount <= 1 ? theme.icons.dismissPinned : theme.icons.chat_pinned_list, for: .Normal) + } + self.dismiss.sizeToFit() container.backgroundColor = theme.colors.background } + override func setFrameSize(_ newSize: NSSize) { + super.setFrameSize(newSize) + } + override func layout() { - node.measureSize(frame.width - 70) - container.setFrameSize(frame.width - 70, node.size.height) - container.centerY(x: 20) - dismiss.centerY(x: frame.width - 21 - dismiss.frame.width) - node.setNeedDisplay() + if let node = node { + node.measureSize(frame.width - (40 + (dismiss.isHidden ? 0 : 30))) + container.setFrameSize(frame.width - (40 + (dismiss.isHidden ? 0 : 30)), node.size.height) + } + container.centerY(x: 24) + dismiss.centerY(x: frame.width - 20 - dismiss.frame.width) + node?.setNeedDisplay() } override func draw(_ layer: CALayer, in ctx: CGContext) { - super.draw(layer, in: ctx) ctx.setFillColor(theme.colors.border.cgColor) ctx.fill(NSMakeRect(0, layer.frame.height - .borderSize, layer.frame.width, .borderSize)) } @@ -218,45 +651,80 @@ class ChatPinnedView : Control { } } -class ChatReportView : Control { +class ChatReportView : Control, ChatHeaderProtocol { private let chatInteraction:ChatInteraction private let report:TitleButton = TitleButton() + private let unarchiveButton = TitleButton() private let dismiss:ImageButton = ImageButton() - init(_ chatInteraction:ChatInteraction) { + private let buttonsContainer = View() + + required init(_ chatInteraction:ChatInteraction, state: ChatHeaderState, frame: NSRect) { self.chatInteraction = chatInteraction - super.init() + super.init(frame: frame) + dismiss.disableActions() + self.style = ControlStyle(backgroundColor: theme.colors.background) - report.set(text: tr(.chatHeaderReportSpam), for: .Normal) - report.sizeToFit() + report.set(text: L10n.chatHeaderReportSpam, for: .Normal) + _ = report.sizeToFit() self.dismiss.set(image: theme.icons.dismissPinned, for: .Normal) - self.dismiss.sizeToFit() + _ = self.dismiss.sizeToFit() report.set(handler: { _ in - chatInteraction.reportSpamAndClose() + chatInteraction.blockContact() }, for: .SingleClick) dismiss.set(handler: { _ in - chatInteraction.dismissPeerReport() + chatInteraction.dismissPeerStatusOptions() + }, for: .SingleClick) + + unarchiveButton.set(handler: { _ in + chatInteraction.unarchive() }, for: .SingleClick) + + addSubview(buttonsContainer) + addSubview(dismiss) - addSubview(report) - updateLocalizationAndTheme() + update(with: state, animated: false) } + + - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) dismiss.set(image: theme.icons.dismissPinned, for: .Normal) - report.set(text: tr(.chatHeaderReportSpam), for: .Normal) - report.style = ControlStyle(font: .normal(.text), foregroundColor: theme.colors.blueUI, backgroundColor: theme.colors.background, highlightColor: theme.colors.blueSelect) - report.sizeToFit() + report.set(text: tr(L10n.chatHeaderReportSpam), for: .Normal) + report.style = ControlStyle(font: .normal(.text), foregroundColor: theme.colors.redUI, backgroundColor: theme.colors.background, highlightColor: theme.colors.accentSelect) + _ = report.sizeToFit() + + unarchiveButton.set(text: L10n.peerInfoUnarchive, for: .Normal) + + unarchiveButton.style = ControlStyle(font: .normal(.text), foregroundColor: theme.colors.accent, backgroundColor: theme.colors.background, highlightColor: theme.colors.accentSelect) + self.backgroundColor = theme.colors.background needsLayout = true } - + + + func update(with state: ChatHeaderState, animated: Bool) { + buttonsContainer.removeAllSubviews() + switch state { + case let .report(_, autoArchived): + buttonsContainer.addSubview(report) + + if autoArchived { + buttonsContainer.addSubview(unarchiveButton) + } + default: + break + } + updateLocalizationAndTheme(theme: theme) + } + override func draw(_ layer: CALayer, in ctx: CGContext) { super.draw(layer, in: ctx) ctx.setFillColor(theme.colors.border.cgColor) @@ -266,6 +734,23 @@ class ChatReportView : Control { override func layout() { report.center() dismiss.centerY(x: frame.width - dismiss.frame.width - 20) + + buttonsContainer.frame = NSMakeRect(0, 0, frame.width, frame.height - .borderSize) + + var buttons:[Control] = [] + if report.superview != nil { + buttons.append(report) + } + if unarchiveButton.superview != nil { + buttons.append(unarchiveButton) + } + + let buttonWidth: CGFloat = floor(buttonsContainer.frame.width / CGFloat(buttons.count)) + var x: CGFloat = 0 + for button in buttons { + button.frame = NSMakeRect(x, 0, buttonWidth, buttonsContainer.frame.height) + x += buttonWidth + } } required init?(coder: NSCoder) { @@ -277,39 +762,158 @@ class ChatReportView : Control { } } -class AddContactView : Control { +class ShareInfoView : Control, ChatHeaderProtocol { private let chatInteraction:ChatInteraction - private let add:TitleButton = TitleButton() + private let share:TitleButton = TitleButton() private let dismiss:ImageButton = ImageButton() - - init(_ chatInteraction:ChatInteraction) { + required init(_ chatInteraction:ChatInteraction, state: ChatHeaderState, frame: NSRect) { self.chatInteraction = chatInteraction - super.init() + super.init(frame: frame) self.style = ControlStyle(backgroundColor: theme.colors.background) + dismiss.disableActions() + + dismiss.set(image: theme.icons.dismissPinned, for: .Normal) + _ = dismiss.sizeToFit() - add.set(text: tr(.peerInfoAddContact), for: .Normal) - add.sizeToFit() + share.set(handler: { [weak self] _ in + self?.chatInteraction.shareSelfContact(nil) + self?.chatInteraction.dismissPeerStatusOptions() + }, for: .SingleClick) + + dismiss.set(handler: { [weak self] _ in + self?.chatInteraction.dismissPeerStatusOptions() + }, for: .SingleClick) + + + addSubview(share) + addSubview(dismiss) + updateLocalizationAndTheme(theme: theme) + } + + func update(with state: ChatHeaderState, animated: Bool) { + + } + + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) + dismiss.set(image: theme.icons.dismissPinned, for: .Normal) + share.style = ControlStyle(font: .normal(.text), foregroundColor: theme.colors.accent, backgroundColor: theme.colors.background, highlightColor: theme.colors.accentSelect) + + share.set(text: L10n.peerInfoShareMyInfo, for: .Normal) + + self.backgroundColor = theme.colors.background + needsLayout = true + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + super.draw(layer, in: ctx) + ctx.setFillColor(theme.colors.border.cgColor) + ctx.fill(NSMakeRect(0, layer.frame.height - .borderSize, layer.frame.width, .borderSize)) + } + + override func layout() { + super.layout() + dismiss.centerY(x: frame.width - dismiss.frame.width - 20) + share.center() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } +} + +class AddContactView : Control, ChatHeaderProtocol { + private let chatInteraction:ChatInteraction + private let add:TitleButton = TitleButton() + private let dismiss:ImageButton = ImageButton() + private let blockButton: TitleButton = TitleButton() + private let unarchiveButton = TitleButton() + private let buttonsContainer = View() + required init(_ chatInteraction:ChatInteraction, state: ChatHeaderState, frame: NSRect) { + self.chatInteraction = chatInteraction + super.init(frame: frame) + self.style = ControlStyle(backgroundColor: theme.colors.background) + dismiss.disableActions() dismiss.set(image: theme.icons.dismissPinned, for: .Normal) - dismiss.sizeToFit() + _ = dismiss.sizeToFit() - add.set(handler: { _ in - chatInteraction.addContact() + add.set(handler: { [weak self] _ in + self?.chatInteraction.addContact() }, for: .SingleClick) - dismiss.set(handler: { _ in - + dismiss.set(handler: { [weak self] _ in + self?.chatInteraction.dismissPeerStatusOptions() + }, for: .SingleClick) + + blockButton.set(handler: { [weak self] _ in + self?.chatInteraction.blockContact() + }, for: .SingleClick) + + unarchiveButton.set(handler: { [weak self] _ in + self?.chatInteraction.unarchive() }, for: .SingleClick) + + + addSubview(buttonsContainer) + addSubview(dismiss) + + update(with: state, animated: false) - addSubview(add) - updateLocalizationAndTheme() } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) dismiss.set(image: theme.icons.dismissPinned, for: .Normal) - add.style = ControlStyle(font: .normal(.text), foregroundColor: theme.colors.blueUI, backgroundColor: theme.colors.background, highlightColor: theme.colors.blueSelect) + add.style = ControlStyle(font: .normal(.text), foregroundColor: theme.colors.accent, backgroundColor: theme.colors.background, highlightColor: theme.colors.accentSelect) + blockButton.style = ControlStyle(font: .normal(.text), foregroundColor: theme.colors.redUI, backgroundColor: theme.colors.background, highlightColor: theme.colors.redUI) + + if blockButton.superview == nil, let peer = chatInteraction.peer { + add.set(text: L10n.peerInfoAddUserToContact(peer.compactDisplayTitle), for: .Normal) + } else { + add.set(text: L10n.peerInfoAddContact, for: .Normal) + } + blockButton.set(text: L10n.peerInfoBlockUser, for: .Normal) + unarchiveButton.set(text: L10n.peerInfoUnarchive, for: .Normal) + + unarchiveButton.style = ControlStyle(font: .normal(.text), foregroundColor: theme.colors.accent, backgroundColor: theme.colors.background, highlightColor: theme.colors.accentSelect) + self.backgroundColor = theme.colors.background + needsLayout = true + } + + func update(with state: ChatHeaderState, animated: Bool) { + switch state { + case let .addContact(_, canBlock, autoArchived): + buttonsContainer.removeAllSubviews() + + if canBlock { + buttonsContainer.addSubview(blockButton) + } + if autoArchived { + buttonsContainer.addSubview(unarchiveButton) + } + + if !autoArchived && canBlock { + buttonsContainer.addSubview(add) + } else if !autoArchived && !canBlock { + buttonsContainer.addSubview(add) + } + default: + break + } + updateLocalizationAndTheme(theme: theme) + needsLayout = true } override func draw(_ layer: CALayer, in ctx: CGContext) { @@ -319,7 +923,30 @@ class AddContactView : Control { } override func layout() { - add.center() + dismiss.centerY(x: frame.width - dismiss.frame.width - 20) + + var buttons:[Control] = [] + + + if add.superview != nil { + buttons.append(add) + } + if blockButton.superview != nil { + buttons.append(blockButton) + } + if unarchiveButton.superview != nil { + buttons.append(unarchiveButton) + } + + buttonsContainer.frame = NSMakeRect(0, 0, frame.width, frame.height - .borderSize) + + + let buttonWidth: CGFloat = floor(buttonsContainer.frame.width / CGFloat(buttons.count)) + var x: CGFloat = 0 + for button in buttons { + button.frame = NSMakeRect(x, 0, buttonWidth, buttonsContainer.frame.height) + x += buttonWidth + } } required init?(coder: NSCoder) { @@ -335,24 +962,49 @@ private final class CSearchContextState : Equatable { let inputQueryResult: ChatPresentationInputQueryResult? let tokenState: TokenSearchState let peerId:PeerId? - init(inputQueryResult: ChatPresentationInputQueryResult? = nil, tokenState: TokenSearchState = .none, peerId: PeerId? = nil) { + let messages: ([Message], SearchMessagesState?) + let selectedIndex: Int + let searchState: SearchState + + init(inputQueryResult: ChatPresentationInputQueryResult? = nil, messages: ([Message], SearchMessagesState?) = ([], nil), selectedIndex: Int = -1, searchState: SearchState = SearchState(state: .None, request: ""), tokenState: TokenSearchState = .none, peerId: PeerId? = nil) { self.inputQueryResult = inputQueryResult self.tokenState = tokenState self.peerId = peerId + self.messages = messages + self.selectedIndex = selectedIndex + self.searchState = searchState } func updatedInputQueryResult(_ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> CSearchContextState { - return CSearchContextState(inputQueryResult: f(self.inputQueryResult), tokenState: self.tokenState, peerId: self.peerId) + return CSearchContextState(inputQueryResult: f(self.inputQueryResult), messages: self.messages, selectedIndex: self.selectedIndex, searchState: self.searchState, tokenState: self.tokenState, peerId: self.peerId) } func updatedTokenState(_ token: TokenSearchState) -> CSearchContextState { - return CSearchContextState(inputQueryResult: self.inputQueryResult, tokenState: token, peerId: self.peerId) + return CSearchContextState(inputQueryResult: self.inputQueryResult, messages: self.messages, selectedIndex: self.selectedIndex, searchState: self.searchState, tokenState: token, peerId: self.peerId) } func updatedPeerId(_ peerId: PeerId?) -> CSearchContextState { - return CSearchContextState(inputQueryResult: self.inputQueryResult, tokenState: self.tokenState, peerId: peerId) + return CSearchContextState(inputQueryResult: self.inputQueryResult, messages: self.messages, selectedIndex: self.selectedIndex, searchState: self.searchState, tokenState: self.tokenState, peerId: peerId) + } + func updatedMessages(_ messages: ([Message], SearchMessagesState?)) -> CSearchContextState { + return CSearchContextState(inputQueryResult: self.inputQueryResult, messages: messages, selectedIndex: self.selectedIndex, searchState: self.searchState, tokenState: self.tokenState, peerId: self.peerId) + } + func updatedSelectedIndex(_ selectedIndex: Int) -> CSearchContextState { + return CSearchContextState(inputQueryResult: self.inputQueryResult, messages: self.messages, selectedIndex: selectedIndex, searchState: self.searchState, tokenState: self.tokenState, peerId: self.peerId) + } + func updatedSearchState(_ searchState: SearchState) -> CSearchContextState { + return CSearchContextState(inputQueryResult: self.inputQueryResult, messages: self.messages, selectedIndex: self.selectedIndex, searchState: searchState, tokenState: self.tokenState, peerId: self.peerId) } } private func ==(lhs: CSearchContextState, rhs: CSearchContextState) -> Bool { - return lhs.inputQueryResult == rhs.inputQueryResult && lhs.tokenState == rhs.tokenState + if lhs.messages.0.count != rhs.messages.0.count { + return false + } else { + for i in 0 ..< lhs.messages.0.count { + if !isEqualMessages(lhs.messages.0[i], rhs.messages.0[i]) { + return false + } + } + } + return lhs.inputQueryResult == rhs.inputQueryResult && lhs.tokenState == rhs.tokenState && lhs.selectedIndex == rhs.selectedIndex && lhs.searchState == rhs.searchState && lhs.messages.1 == rhs.messages.1 } private final class CSearchInteraction : InterfaceObserver { @@ -365,85 +1017,243 @@ private final class CSearchInteraction : InterfaceObserver { notifyObservers(value: state, oldValue:oldValue, animated: animated) } } + + var currentMessage: Message? { + if state.messages.0.isEmpty { + return nil + } else if state.messages.0.count <= state.selectedIndex || state.selectedIndex < 0 { + return nil + } + return state.messages.0[state.selectedIndex] + } +} + +struct SearchStateQuery : Equatable { + let query: String? + let state: SearchMessagesState? + init(_ query: String?, _ state: SearchMessagesState?) { + self.query = query + self.state = state + } } -class ChatSearchHeader : View, Notifable { +struct SearchMessagesResultState : Equatable { + static func == (lhs: SearchMessagesResultState, rhs: SearchMessagesResultState) -> Bool { + if lhs.query != rhs.query { + return false + } + if lhs.messages.count != rhs.messages.count { + return false + } else { + for i in 0 ..< lhs.messages.count { + if !isEqualMessages(lhs.messages[i], rhs.messages[i]) { + return false + } + } + } + return true + } + + let query: String + let messages: [Message] + init(_ query: String, _ messages: [Message]) { + self.query = query + self.messages = messages + } + + func containsMessage(_ message: Message) -> Bool { + return self.messages.contains(where: { $0.id == message.id }) + } +} + +class ChatSearchHeader : View, Notifable, ChatHeaderProtocol { private let searchView:ChatSearchView = ChatSearchView(frame: NSZeroRect) private let cancel:ImageButton = ImageButton() - private let prev:ImageButton = ImageButton() - private let next:ImageButton = ImageButton() private let from:ImageButton = ImageButton() private let calendar:ImageButton = ImageButton() + private let prev:ImageButton = ImageButton() + private let next:ImageButton = ImageButton() + + private let separator:View = View() private let interactions:ChatSearchInteractions private let chatInteraction: ChatInteraction - private let query:Promise = Promise() + private let query:ValuePromise = ValuePromise() + private let disposable:MetaDisposable = MetaDisposable() private var contextQueryState: (ChatPresentationInputQuery?, Disposable)? private let inputContextHelper: InputContextHelper private let inputInteraction: CSearchInteraction = CSearchInteraction() - - private var messages:[Message] = [] - private var currentIndex:Int = 0 { - didSet { - searchView.countValue = (current: currentIndex + 1, total: messages.count) + private let parentInteractions: ChatInteraction + private let loadingDisposable = MetaDisposable() + + private let calendarController: CalendarController + required init(_ chatInteraction: ChatInteraction, state: ChatHeaderState, frame: NSRect) { + + switch state { + case let .search(_, interactions, _, initialString): + self.interactions = interactions + self.parentInteractions = chatInteraction + self.calendarController = CalendarController(NSMakeRect(0, 0, 250, 250), chatInteraction.context.window, selectHandler: interactions.calendarAction) + self.chatInteraction = ChatInteraction(chatLocation: chatInteraction.chatLocation, context: chatInteraction.context, mode: chatInteraction.mode) + self.chatInteraction.update({$0.updatedPeer({_ in chatInteraction.presentation.peer})}) + self.inputContextHelper = InputContextHelper(chatInteraction: self.chatInteraction, highlightInsteadOfSelect: true) + + if let initialString = initialString { + searchView.setString(initialString) + self.query.set(SearchStateQuery(initialString, nil)) + } + default: + fatalError() } - } - init(_ interactions:ChatSearchInteractions, chatInteraction: ChatInteraction) { - self.interactions = interactions - self.chatInteraction = ChatInteraction(peerId: chatInteraction.peerId, account: chatInteraction.account) - self.chatInteraction.update({$0.updatedPeer({_ in chatInteraction.presentation.peer})}) - self.inputContextHelper = InputContextHelper(account: chatInteraction.account, chatInteraction: self.chatInteraction) + super.init() self.chatInteraction.movePeerToInput = { [weak self] peer in self?.searchView.completeToken(peer.compactDisplayTitle) self?.inputInteraction.update({$0.updatedPeerId(peer.id)}) } - - initialize() - inputInteraction.add(observer: self) + + + self.chatInteraction.focusMessageId = { [weak self] fromId, messageId, state in + self?.parentInteractions.focusMessageId(fromId, messageId, state) + self?.inputInteraction.update({$0.updatedSelectedIndex($0.messages.0.firstIndex(where: {$0.id == messageId}) ?? -1)}) + _ = self?.window?.makeFirstResponder(nil) + } + + + + initialize() + + + + parentInteractions.loadingMessage.set(.single(false)) + + inputInteraction.add(observer: self) + self.loadingDisposable.set((parentInteractions.loadingMessage.get() |> deliverOnMainQueue).start(next: { [weak self] loading in + self?.searchView.isLoading = loading + })) + switch state { + case let .search(_, _, initialPeer, _): + if let initialPeer = initialPeer { + self.chatInteraction.movePeerToInput(initialPeer) + } + default: + break + } + Queue.mainQueue().justDispatch { [weak self] in + self?.applySearchResponder(false) + } + } + + func update(with state: ChatHeaderState, animated: Bool) { + + } + + + func applySearchResponder(_ animated: Bool = false) { + // _ = window?.makeFirstResponder(searchView.input) + searchView.layout() + if searchView.state == .Focus && window?.firstResponder != searchView.input { + _ = window?.makeFirstResponder(searchView.input) + } + searchView.change(state: .Focus, animated) + } + + private var calendarAbility: Bool { + return chatInteraction.mode != .scheduled && chatInteraction.mode != .pinned + } + + private var fromAbility: Bool { + if let peer = chatInteraction.presentation.peer { + return (peer.isSupergroup || peer.isGroup) && (chatInteraction.mode == .history || chatInteraction.mode.isThreadMode) + } else { + return false + } } func notify(with value: Any, oldValue: Any, animated: Bool) { - let account = chatInteraction.account - if let value = value as? CSearchContextState, let oldValue = oldValue as? CSearchContextState, let view = superview { + let context = chatInteraction.context + if let value = value as? CSearchContextState, let oldValue = oldValue as? CSearchContextState, let superview = superview, let view = superview.superview { + + let stateValue = self.query + + prev.isEnabled = !value.messages.0.isEmpty && value.selectedIndex < value.messages.0.count - 1 + next.isEnabled = !value.messages.0.isEmpty && value.selectedIndex > 0 + next.set(image: next.isEnabled ? theme.icons.chatSearchDown : theme.icons.chatSearchDownDisabled, for: .Normal) + prev.set(image: prev.isEnabled ? theme.icons.chatSearchUp : theme.icons.chatSearchUpDisabled, for: .Normal) + + + if let peer = chatInteraction.presentation.peer { if value.inputQueryResult != oldValue.inputQueryResult { - inputContextHelper.context(with: value.inputQueryResult, for: view, relativeView: self, position: .below, animated: animated) + inputContextHelper.context(with: value.inputQueryResult, for: view, relativeView: superview, position: .below, selectIndex: value.selectedIndex != -1 ? value.selectedIndex : nil, animated: animated) } switch value.tokenState { case .none: - messages = [] - currentIndex = -1 - from.isHidden = false - calendar.isHidden = false + from.isHidden = !fromAbility + calendar.isHidden = !calendarAbility needsLayout = true searchView.change(size: NSMakeSize(searchWidth, searchView.frame.height), animated: animated) - inputInteraction.update(animated: animated, { - $0.updatedInputQueryResult { previousResult in - return .mentions([]) - }.updatedPeerId(nil) - }) + + if (peer.isSupergroup || peer.isGroup) && chatInteraction.mode == .history { + if let (updatedContextQueryState, updatedContextQuerySignal) = chatContextQueryForSearchMention(chatLocations: [chatInteraction.chatLocation], .mention(query: value.searchState.request, includeRecent: false), currentQuery: self.contextQueryState?.0, context: context) { + self.contextQueryState?.1.dispose() + self.contextQueryState = (updatedContextQueryState, (updatedContextQuerySignal |> deliverOnMainQueue).start(next: { [weak self] result in + if let strongSelf = self { + strongSelf.inputInteraction.update(animated: animated, { state in + return state.updatedInputQueryResult { previousResult in + let messages = state.searchState.responder ? state.messages : ([], nil) + var suggestedPeers:[Peer] = [] + let inputQueryResult = result(previousResult) + if let inputQueryResult = inputQueryResult, state.searchState.responder, !state.searchState.request.isEmpty, messages.1 != nil { + switch inputQueryResult { + case let .mentions(mentions): + suggestedPeers = mentions + default: + break + } + } + return .searchMessages((messages.0, messages.1, { searchMessagesState in + stateValue.set(SearchStateQuery(state.searchState.request, searchMessagesState)) + }), suggestedPeers, state.searchState.request) + } + }) + } + })) + } + } else { + inputInteraction.update(animated: animated, { state in + return state.updatedInputQueryResult { previousResult in + let result = state.searchState.responder ? state.messages : ([], nil) + return .searchMessages((result.0, result.1, { searchMessagesState in + stateValue.set(SearchStateQuery(state.searchState.request, searchMessagesState)) + }), [], state.searchState.request) + } + }) + } + + case let .from(query, complete): from.isHidden = true calendar.isHidden = true searchView.change(size: NSMakeSize(searchWidth, searchView.frame.height), animated: animated) needsLayout = true if complete { - inputInteraction.update(animated: animated, { - $0.updatedInputQueryResult { previousResult in - return .mentions([]) + inputInteraction.update(animated: animated, { state in + return state.updatedInputQueryResult { previousResult in + let result = state.searchState.responder ? state.messages : ([], nil) + return .searchMessages((result.0, result.1, { searchMessagesState in + stateValue.set(SearchStateQuery(state.searchState.request, searchMessagesState)) + }), [], state.searchState.request) } }) } else { - messages = [] - currentIndex = -1 - if let (updatedContextQueryState, updatedContextQuerySignal) = chatContextQueryForSearchMention(peer: peer, .mention(query: query, includeRecent: false), currentQuery: self.contextQueryState?.0, account: account) { + if let (updatedContextQueryState, updatedContextQuerySignal) = chatContextQueryForSearchMention(chatLocations: [chatInteraction.chatLocation], .mention(query: query, includeRecent: false), currentQuery: self.contextQueryState?.0, context: context) { self.contextQueryState?.1.dispose() var inScope = true var inScopeResult: ((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?)? @@ -456,7 +1266,7 @@ class ChatSearchHeader : View, Notifable { strongSelf.inputInteraction.update(animated: animated, { $0.updatedInputQueryResult { previousResult in return result(previousResult) - } + }.updatedMessages(([], nil)).updatedSelectedIndex(-1) }) } @@ -467,7 +1277,7 @@ class ChatSearchHeader : View, Notifable { inputInteraction.update(animated: animated, { $0.updatedInputQueryResult { previousResult in return inScopeResult(previousResult) - } + }.updatedMessages(([], nil)).updatedSelectedIndex(-1) }) } } @@ -487,135 +1297,168 @@ class ChatSearchHeader : View, Notifable { + + private func initialize() { - if let peer = chatInteraction.presentation.peer { - self.from.isHidden = !peer.isSupergroup && !peer.isGroup - } else { - self.from.isHidden = true - } + self.from.isHidden = !fromAbility _ = self.searchView.tokenPromise.get().start(next: { [weak self] state in self?.inputInteraction.update({$0.updatedTokenState(state)}) }) - self.searchView.searchInteractions = SearchInteractions({ [weak self] state in + self.searchView.searchInteractions = SearchInteractions({ [weak self] state, _ in if state.state == .None { - self?.searchView.isLoading = false + self?.parentInteractions.loadingMessage.set(.single(false)) + self?.parentInteractions.updateSearchRequest(SearchMessagesResultState(state.request, [])) + self?.inputInteraction.update({$0.updatedMessages(([], nil)).updatedSelectedIndex(-1).updatedSearchState(state)}) } }, { [weak self] state in - if let strongSelf = self { - strongSelf.messages = [] - strongSelf.currentIndex = -1 - strongSelf.updateSearchState() - switch strongSelf.searchView.tokenState { - case .none: - if state.request == tr(.chatSearchFrom), let peer = strongSelf.chatInteraction.presentation.peer, peer.isGroup || peer.isSupergroup { - strongSelf.query.set(.single("")) - strongSelf.searchView.initToken() - } else { - strongSelf.searchView.isLoading = true - strongSelf.query.set(.single(state.request)) - } - - case .from(_, let complete): - if complete { - strongSelf.searchView.isLoading = true - strongSelf.query.set(.single(state.request)) - } + guard let `self` = self else {return} + + self.inputInteraction.update({$0.updatedMessages(([], nil)).updatedSelectedIndex(-1).updatedSearchState(state)}) + + self.updateSearchState() + switch self.searchView.tokenState { + case .none: + if state.request == L10n.chatSearchFrom, let peer = self.chatInteraction.presentation.peer, peer.isGroup || peer.isSupergroup { + self.query.set(SearchStateQuery("", nil)) + self.parentInteractions.updateSearchRequest(SearchMessagesResultState("", [])) + self.searchView.initToken() + } else { + self.parentInteractions.updateSearchRequest(SearchMessagesResultState(state.request, [])) + self.parentInteractions.loadingMessage.set(.single(true)) + self.query.set(SearchStateQuery(state.request, nil)) + } + + case .from(_, let complete): + if complete { + self.parentInteractions.updateSearchRequest(SearchMessagesResultState(state.request, [])) + self.parentInteractions.loadingMessage.set(.single(true)) + self.query.set(SearchStateQuery(state.request, nil)) } - } + }, responderModified: { [weak self] state in + self?.inputInteraction.update({$0.updatedSearchState(state)}) }) - let apply = query.get() |> mapToSignal { [weak self] query -> Signal<[Message], Void> in - if let strongSelf = self, let query = query { - return .single(Void()) |> delay(0.3, queue: Queue.mainQueue()) |> mapToSignal { [weak strongSelf] () -> Signal<[Message], Void> in - if let strongSelf = strongSelf { - return strongSelf.interactions.searchRequest(query, strongSelf.inputInteraction.state.peerId) + let apply = query.get() |> mapToSignal { [weak self] state -> Signal<([Message], SearchMessagesState?, String), NoError> in + + guard let `self` = self else { return .single(([], nil, "")) } + if let query = state.query { + + let stateSignal: Signal + if state.state == nil { + stateSignal = .single(state.state) |> delay(0.3, queue: Queue.mainQueue()) + } else { + stateSignal = .single(state.state) + } + + return stateSignal |> mapToSignal { [weak self] state in + + guard let `self` = self else { return .single(([], nil, "")) } + + let emptyRequest: Bool + if case .from = self.inputInteraction.state.tokenState { + emptyRequest = true + } else { + emptyRequest = !query.isEmpty + } + if emptyRequest { + return self.interactions.searchRequest(query, self.inputInteraction.state.peerId, state) |> map { ($0.0, $0.1, query) } + } else { + return .single(([], nil, "")) } - return .single([]) } } else { - return .single([]) + return .single(([], nil, "")) } } |> deliverOnMainQueue self.disposable.set(apply.start(next: { [weak self] messages in - self?.messages = messages - self?.currentIndex = -1 - self?.prevAction() - self?.searchView.isLoading = false - - }, error: { [weak self] in - self?.messages = [] - self?.currentIndex = -1 - self?.prevAction() - self?.searchView.isLoading = false + guard let `self` = self else {return} + self.parentInteractions.updateSearchRequest(SearchMessagesResultState(messages.2, messages.0)) + self.inputInteraction.update({$0.updatedMessages((messages.0, messages.1)).updatedSelectedIndex(-1)}) + self.parentInteractions.loadingMessage.set(.single(false)) })) - + next.autohighlight = false prev.autohighlight = false - calendar.sizeToFit() + + + _ = calendar.sizeToFit() addSubview(next) addSubview(prev) + + addSubview(from) + + addSubview(calendar) - cancel.sizeToFit() + calendar.isHidden = !calendarAbility + + _ = cancel.sizeToFit() let interactions = self.interactions let searchView = self.searchView cancel.set(handler: { [weak self] _ in - self?.inputInteraction.update {$0.updatedTokenState(.none)} + self?.inputInteraction.update {$0.updatedTokenState(.none).updatedSelectedIndex(-1).updatedMessages(([], nil)).updatedSearchState(SearchState(state: .None, request: ""))} + self?.parentInteractions.updateSearchRequest(SearchMessagesResultState("", [])) interactions.cancel() }, for: .Click) next.set(handler: { [weak self] _ in self?.nextAction() - }, for: .Click) + }, for: .Click) prev.set(handler: { [weak self] _ in self?.prevAction() }, for: .Click) + + from.set(handler: { [weak self] _ in self?.searchView.initToken() }, for: .Click) + + calendar.set(handler: { [weak self] calendar in - if let strongSelf = self { - showPopover(for: calendar, with: CalendarController(NSMakeRect(0,0,250,250), selectHandler: strongSelf.interactions.calendarAction), edge: .maxY, inset: NSMakePoint(-160, -40)) - } + guard let `self` = self else {return} + showPopover(for: calendar, with: self.calendarController, edge: .maxY, inset: NSMakePoint(-160, -40)) }, for: .Click) addSubview(searchView) addSubview(cancel) addSubview(separator) - updateLocalizationAndTheme() + updateLocalizationAndTheme(theme: theme) } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) backgroundColor = theme.colors.background + next.set(image: theme.icons.chatSearchDown, for: .Normal) - next.sizeToFit() + _ = next.sizeToFit() prev.set(image: theme.icons.chatSearchUp, for: .Normal) - prev.sizeToFit() + _ = prev.sizeToFit() + calendar.set(image: theme.icons.chatSearchCalendar, for: .Normal) - calendar.sizeToFit() + _ = calendar.sizeToFit() cancel.set(image: theme.icons.chatSearchCancel, for: .Normal) - cancel.sizeToFit() + _ = cancel.sizeToFit() from.set(image: theme.icons.chatSearchFrom, for: .Normal) - from.sizeToFit() + _ = from.sizeToFit() separator.backgroundColor = theme.colors.border self.backgroundColor = theme.colors.background @@ -624,31 +1467,24 @@ class ChatSearchHeader : View, Notifable { } func updateSearchState() { - prev.isEnabled = !messages.isEmpty && currentIndex < messages.count - 1 - next.isEnabled = !messages.isEmpty && currentIndex > 0 - next.set(image: next.isEnabled ? theme.icons.chatSearchDown : theme.icons.chatSearchDownDisabled, for: .Normal) - prev.set(image: prev.isEnabled ? theme.icons.chatSearchUp : theme.icons.chatSearchUpDisabled, for: .Normal) + } func prevAction() { - if !messages.isEmpty { - currentIndex += 1 - currentIndex = min(messages.count - 1, currentIndex) - perform() - } + inputInteraction.update({$0.updatedSelectedIndex(min($0.selectedIndex + 1, $0.messages.0.count - 1))}) + perform() } func perform() { - interactions.jump(messages[min(max(0,currentIndex), messages.count - 1)]) - updateSearchState() + _ = window?.makeFirstResponder(nil) + if let currentMessage = inputInteraction.currentMessage { + interactions.jump(currentMessage) + } } func nextAction() { - if !messages.isEmpty { - currentIndex -= 1 - currentIndex = max(0, currentIndex) - perform() - } + inputInteraction.update({$0.updatedSelectedIndex(max($0.selectedIndex - 1, 0))}) + perform() } private var searchWidth: CGFloat { @@ -658,13 +1494,16 @@ class ChatSearchHeader : View, Notifable { override func layout() { super.layout() + prev.centerY(x:10) next.centerY(x:prev.frame.maxX) + cancel.centerY(x:frame.width - cancel.frame.width - 20) searchView.setFrameSize(NSMakeSize(searchWidth, 30)) - searchView.centerY(x:80) + inputContextHelper.controller.view.setFrameSize(frame.width, inputContextHelper.controller.frame.height) + searchView.centerY(x: 80) separator.frame = NSMakeRect(0, frame.height - .borderSize, frame.width, .borderSize) from.centerY(x: searchView.frame.maxX + 20) @@ -675,35 +1514,28 @@ class ChatSearchHeader : View, Notifable { override func viewDidMoveToWindow() { if let _ = window { layout() - self.searchView.change(state: .Focus, false) + //self.searchView.change(state: .Focus, false) } } override func viewWillMove(toWindow newWindow: NSWindow?) { - if let window = newWindow as? Window { - window.set(handler: { [weak self] () -> KeyHandlerResult in - self?.prevAction() - return .invoked - }, with: self, for: .UpArrow, priority: .medium) - - window.set(handler: { [weak self] () -> KeyHandlerResult in - self?.nextAction() - return .invoked - }, with: self, for: .DownArrow, priority: .medium) - } else { - if let window = window as? Window { - window.remove(object: self, for: .UpArrow) - window.remove(object: self, for: .DownArrow) - self.searchView.change(state: .None, false) - } - + if newWindow == nil { + // self.searchView.change(state: .None, false) } } deinit { + inputInteraction.update(animated: false, { state in + return state.updatedInputQueryResult( { _ in return nil } ) + }) + parentInteractions.updateSearchRequest(SearchMessagesResultState("", [])) disposable.dispose() inputInteraction.remove(observer: self) + loadingDisposable.set(nil) + if let window = window as? Window { + window.removeAllHandlers(for: self) + } } required init?(coder: NSCoder) { @@ -713,7 +1545,9 @@ class ChatSearchHeader : View, Notifable { init(frame frameRect: NSRect, interactions:ChatSearchInteractions, chatInteraction: ChatInteraction) { self.interactions = interactions self.chatInteraction = chatInteraction - self.inputContextHelper = InputContextHelper(account: chatInteraction.account, chatInteraction: chatInteraction) + self.parentInteractions = chatInteraction + self.inputContextHelper = InputContextHelper(chatInteraction: chatInteraction, highlightInsteadOfSelect: true) + self.calendarController = CalendarController(NSMakeRect(0,0,250,250), chatInteraction.context.window, selectHandler: interactions.calendarAction) super.init(frame: frameRect) initialize() } @@ -722,3 +1556,446 @@ class ChatSearchHeader : View, Notifable { fatalError("init(frame:) has not been implemented") } } + + +private final class FakeAudioLevelGenerator { + private var isFirstTime: Bool = true + private var nextTarget: Float = 0.0 + private var nextTargetProgress: Float = 0.0 + private var nextTargetProgressNorm: Float = 1.0 + + func get() -> Float { + let wasFirstTime = self.isFirstTime + self.isFirstTime = false + + self.nextTargetProgress *= 0.82 + if self.nextTargetProgress <= 0.01 { + if Int.random(in: 0 ... 4) <= 1 && !wasFirstTime { + self.nextTarget = 0.0 + self.nextTargetProgressNorm = Float.random(in: 0.1 ..< 0.3) + } else { + self.nextTarget = Float.random(in: 0.0 ..< 20.0) + self.nextTargetProgressNorm = Float.random(in: 0.2 ..< 0.7) + } + self.nextTargetProgress = self.nextTargetProgressNorm + return self.nextTarget + } else { + let value = self.nextTarget * max(0.0, self.nextTargetProgress / self.nextTargetProgressNorm) + return value + } + } +} + +private final class TimerButtonView : Control { + private var nextTimer: SwiftSignalKit.Timer? + private let counter = DynamicCounterTextView(frame: .zero) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(counter) + scaleOnClick = true + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + super.draw(layer, in: ctx) + + let purple = NSColor(rgb: 0x3252ef) + let pink = NSColor(rgb: 0xef436c) + + let colorSpace = CGColorSpaceCreateDeviceRGB() + var locations:[CGFloat] = [0.0, 0.85, 1.0] + let gradient = CGGradient(colorsSpace: colorSpace, colors: [pink.cgColor, purple.cgColor, purple.cgColor] as CFArray, locations: &locations)! + ctx.drawLinearGradient(gradient, start: CGPoint(x: 0, y: 0.0), end: CGPoint(x: frame.width, y: frame.height), options: []) + } + + func setTime(_ timeValue: Int32, animated: Bool, layout: @escaping(NSSize, NSView)->Void) { + let time = Int(timeValue - Int32(Date().timeIntervalSince1970)) + + let text = timerText(time) + let value = DynamicCounterTextView.make(for: text, count: text, font: .avatar(13), textColor: .white, width: .greatestFiniteMagnitude) + + counter.update(value, animated: animated) + counter.change(size: value.size, animated: animated) + + layout(value.size, self) + var point = focus(value.size).origin + point = point.offset(dx: 2, dy: 0) + counter.change(pos: point, animated: animated) + + + self.nextTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: false, completion: { [weak self] in + self?.setTime(timeValue, animated: true, layout: layout) + }, queue: .mainQueue()) + + nextTimer?.start() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + +private final class ChatGroupCallView : Control, ChatHeaderProtocol { + + struct Avatar : Comparable, Identifiable { + static func < (lhs: Avatar, rhs: Avatar) -> Bool { + return lhs.index < rhs.index + } + var stableId: PeerId { + return peer.id + } + static func == (lhs: Avatar, rhs: Avatar) -> Bool { + if lhs.index != rhs.index { + return false + } + if !lhs.peer.isEqual(rhs.peer) { + return false + } + return true + } + + let peer: Peer + let index: Int + } + private var topPeers: [Avatar] = [] + private var avatars:[AvatarContentView] = [] + private let avatarsContainer = View(frame: NSMakeRect(0, 0, 25 * 3 + 10, 38)) + + private let joinButton = TitleButton() + private var data: ChatActiveGroupCallInfo? + private let chatInteraction: ChatInteraction + private let headerView = TextView() + private let membersCountView = DynamicCounterTextView() + private let button = Control() + + private var scheduleButton: TimerButtonView? + + private var audioLevelGenerators: [PeerId: FakeAudioLevelGenerator] = [:] + private var audioLevelGeneratorTimer: SwiftSignalKit.Timer? + + + required init(_ chatInteraction: ChatInteraction, state: ChatHeaderState, frame: NSRect) { + self.chatInteraction = chatInteraction + super.init(frame: frame) + addSubview(headerView) + addSubview(membersCountView) + addSubview(avatarsContainer) + addSubview(button) + addSubview(joinButton) + avatarsContainer.isEventLess = true + + + headerView.userInteractionEnabled = false + headerView.isSelectable = false + membersCountView.userInteractionEnabled = false + + joinButton.set(handler: { [weak self] _ in + if let `self` = self, let data = self.data { + self.chatInteraction.joinGroupCall(data.activeCall, data.joinHash) + } + }, for: .SingleClick) + + + button.set(handler: { [weak self] _ in + if let `self` = self, let data = self.data { + self.chatInteraction.joinGroupCall(data.activeCall, data.joinHash) + } + }, for: .SingleClick) + + button.set(handler: { [weak self] _ in + self?.headerView.change(opacity: 0.6, animated: true) + self?.membersCountView.change(opacity: 0.6, animated: true) + self?.avatarsContainer.change(opacity: 0.6, animated: true) + }, for: .Highlight) + + button.set(handler: { [weak self] _ in + self?.headerView.change(opacity: 1, animated: true) + self?.membersCountView.change(opacity: 1, animated: true) + self?.avatarsContainer.change(opacity: 1.0, animated: true) + }, for: .Normal) + + button.set(handler: { [weak self] _ in + self?.headerView.change(opacity: 1, animated: true) + self?.membersCountView.change(opacity: 1, animated: true) + self?.avatarsContainer.change(opacity: 1.0, animated: true) + }, for: .Hover) + + joinButton.scaleOnClick = true + + self.avatarsContainer.center() + + self.update(with: state, animated: false) + updateLocalizationAndTheme(theme: theme) + } + + + func update(with state: ChatHeaderState, animated: Bool) { + if let data = state.voiceChat { + self.update(data, animated: animated) + } + } + + func update(_ data: ChatActiveGroupCallInfo, animated: Bool) { + + let context = self.chatInteraction.context + + let activeCall = data.data?.groupCall != nil + joinButton.change(opacity: activeCall ? 0 : 1, animated: animated) + joinButton.userInteractionEnabled = !activeCall + joinButton.isEventLess = activeCall + + let duration: Double = 0.2 + let timingFunction: CAMediaTimingFunctionName = .easeInEaseOut + + + var topPeers: [Avatar] = [] + if let participants = data.data?.topParticipants { + var index:Int = 0 + let participants = participants + for participant in participants { + topPeers.append(Avatar(peer: participant.peer, index: index)) + index += 1 + } + + + } + let (removed, inserted, updated) = mergeListsStableWithUpdates(leftList: self.topPeers, rightList: topPeers) + + let avatarSize = NSMakeSize(38, 38) + + for removed in removed.reversed() { + let control = avatars.remove(at: removed) + let peer = self.topPeers[removed] + let haveNext = topPeers.contains(where: { $0.stableId == peer.stableId }) + control.updateLayout(size: avatarSize - NSMakeSize(8, 8), isClipped: false, animated: animated) + control.layer?.opacity = 0 + if animated && !haveNext { + control.layer?.animateAlpha(from: 1, to: 0, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { [weak control] _ in + control?.removeFromSuperview() + }) + control.layer?.animateScaleSpring(from: 1.0, to: 0.2, duration: duration, bounce: false) + } else { + control.removeFromSuperview() + } + } + for inserted in inserted { + let control = AvatarContentView(context: context, peer: inserted.1.peer, message: nil, synchronousLoad: false, size: avatarSize, inset: 6) + control.updateLayout(size: avatarSize - NSMakeSize(8, 8), isClipped: inserted.0 != 0, animated: animated) + control.userInteractionEnabled = false + control.setFrameSize(avatarSize) + control.setFrameOrigin(NSMakePoint(CGFloat(inserted.0) * (avatarSize.width - 14), 0)) + avatars.insert(control, at: inserted.0) + avatarsContainer.subviews.insert(control, at: inserted.0) + if animated { + if let index = inserted.2 { + control.layer?.animatePosition(from: NSMakePoint(CGFloat(index) * (avatarSize.width - 14), 0), to: control.frame.origin, timingFunction: timingFunction) + } else { + control.layer?.animateAlpha(from: 0, to: 1, duration: duration, timingFunction: timingFunction) + control.layer?.animateScaleSpring(from: 0.2, to: 1.0, duration: duration, bounce: false) + } + } + } + for updated in updated { + let control = avatars[updated.0] + control.updateLayout(size: avatarSize - NSMakeSize(8, 8), isClipped: updated.0 != 0, animated: animated) + let updatedPoint = NSMakePoint(CGFloat(updated.0) * (avatarSize.width - 14), 0) + if animated { + control.layer?.animatePosition(from: control.frame.origin - updatedPoint, to: .zero, duration: duration, timingFunction: timingFunction, additive: true) + } + control.setFrameOrigin(updatedPoint) + } + var index: CGFloat = 10 + for control in avatarsContainer.subviews.compactMap({ $0 as? AvatarContentView }) { + control.layer?.zPosition = index + index -= 1 + } + + + + if let data = data.data, data.groupCall == nil { + + var activeSpeakers = data.activeSpeakers + + for peerId in activeSpeakers { + if self.audioLevelGenerators[peerId] == nil { + self.audioLevelGenerators[peerId] = FakeAudioLevelGenerator() + } + } + var removeGenerators: [PeerId] = [] + for peerId in self.audioLevelGenerators.keys { + if !activeSpeakers.contains(peerId) { + removeGenerators.append(peerId) + } + } + for peerId in removeGenerators { + self.audioLevelGenerators.removeValue(forKey: peerId) + } + + if self.audioLevelGenerators.isEmpty { + self.audioLevelGeneratorTimer?.invalidate() + self.audioLevelGeneratorTimer = nil + self.sampleAudioGenerators() + } else if self.audioLevelGeneratorTimer == nil { + let audioLevelGeneratorTimer = SwiftSignalKit.Timer(timeout: 1.0 / 30.0, repeat: true, completion: { [weak self] in + self?.sampleAudioGenerators() + }, queue: .mainQueue()) + self.audioLevelGeneratorTimer = audioLevelGeneratorTimer + audioLevelGeneratorTimer.start() + } + } + + let subviewsCount = max(avatarsContainer.subviews.filter { $0.layer?.opacity == 1.0 }.count, 1) + + if subviewsCount == 3 { + self.avatarsContainer.setFrameOrigin(self.focus(self.avatarsContainer.frame.size).origin) + } else { + let count = CGFloat(subviewsCount) + if count != 0 { + let animated = animated && self.data?.data?.activeSpeakers.count != 0 + let avatarSize: CGFloat = avatarsContainer.subviews.map { $0.frame.maxX }.max() ?? 0 + let pos = NSMakePoint(floorToScreenPixels(backingScaleFactor, (frame.width - avatarSize) / 2), self.avatarsContainer.frame.minY) + self.avatarsContainer.change(pos: pos, animated: animated) + } + } + let participantsCount = data.data?.participantCount ?? 0 + + var text: String + let pretty: String + if let scheduledDate = data.activeCall.scheduleTimestamp, participantsCount == 0 { + text = L10n.chatGroupCallScheduledStatus(stringForMediumDate(timestamp: scheduledDate)) + pretty = "" + var presented = false + let current: TimerButtonView + if let button = self.scheduleButton { + current = button + } else { + current = TimerButtonView(frame: NSMakeRect(0, 0, 60, 24)) + self.scheduleButton = current + current.layer?.cornerRadius = current.frame.height / 2 + addSubview(current) + presented = true + + current.set(handler: { [weak self] _ in + if let `self` = self, let data = self.data { + self.chatInteraction.joinGroupCall(data.activeCall, data.joinHash) + } + }, for: .SingleClick) + + } + current.setTime(scheduledDate, animated: animated, layout: { [weak self] size, button in + guard let strongSelf = self else { + return + } + let animated = animated && !presented + let size = NSMakeSize(size.width + 10, button.frame.height) + button._change(size: size, animated: animated) + button._change(pos: button.centerFrameY(x: strongSelf.frame.width - button.frame.width - 23).origin, animated: animated) + presented = false + }) + joinButton.isHidden = true + } else { + text = L10n.chatGroupCallMembersCountable(participantsCount) + pretty = "\(Int(participantsCount).formattedWithSeparator)" + text = text.replacingOccurrences(of: "\(participantsCount)", with: pretty) + joinButton.isHidden = false + + self.scheduleButton?.removeFromSuperview() + self.scheduleButton = nil + } + let dynamicValues = DynamicCounterTextView.make(for: text, count: pretty, font: .normal(.short), textColor: theme.colors.grayText, width: frame.midX) + + self.membersCountView.update(dynamicValues, animated: animated) + self.membersCountView.change(size: dynamicValues.size, animated: animated) + + + self.topPeers = topPeers + self.data = data + + + var title: String = data.activeCall.scheduleTimestamp != nil ? L10n.chatGroupCallScheduledTitle : L10n.chatGroupCallTitle + + + if data.activeCall.scheduleTimestamp == nil, let peer = self.chatInteraction.presentation.peer as? TelegramChannel { + if peer.flags.contains(.isGigagroup) || peer.isChannel { + title = L10n.chatGroupCallLiveTitle + } + } + + + let headerLayout = TextViewLayout(.initialize(string: title, color: theme.colors.text, font: .medium(.text))) + headerLayout.measure(width: frame.width - 100) + headerView.update(headerLayout) + + needsLayout = true + + } + + private func sampleAudioGenerators() { + var levels: [PeerId: Float] = [:] + for (peerId, generator) in self.audioLevelGenerators { + levels[peerId] = generator.get() + } + let avatars = avatarsContainer.subviews.compactMap { $0 as? AvatarContentView } + for avatar in avatars { + if let level = levels[avatar.peerId] { + avatar.updateAudioLevel(color: theme.colors.accent, value: level) + } else { + avatar.updateAudioLevel(color: theme.colors.accent, value: 0) + } + } + } + + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + backgroundColor = theme.colors.background + border = [.Bottom] + borderColor = theme.colors.border + joinButton.set(font: .medium(.text), for: .Normal) + joinButton.set(text: L10n.chatGroupCallJoin, for: .Normal) + joinButton.sizeToFit(NSMakeSize(14, 8), .zero, thatFit: false) + joinButton.layer?.cornerRadius = joinButton.frame.height / 2 + joinButton.set(color: theme.colors.underSelectedColor, for: .Normal) + joinButton.set(background: theme.colors.accent, for: .Normal) + joinButton.set(background: theme.colors.accent.highlighted, for: .Highlight) + + + } + + override func layout() { + super.layout() + + if let scheduleButton = scheduleButton { + scheduleButton.centerY(x: frame.width - scheduleButton.frame.width - 23) + } + joinButton.centerY(x: frame.width - joinButton.frame.width - 23) + + let subviewsCount = max(avatarsContainer.subviews.filter { $0.layer?.opacity == 1.0 }.count, 1) + + if subviewsCount == 3 || subviewsCount == 0 { + self.avatarsContainer.center() + } else { + let count = CGFloat(subviewsCount) + let avatarSize: CGFloat = (count * 30) - ((count - 1) * 3) + self.avatarsContainer.centerY(x: floorToScreenPixels(backingScaleFactor, (frame.width - avatarSize) / 2)) + } + + headerView.layout?.measure(width: frame.width - 100) + headerView.update(headerView.layout) + + + headerView.setFrameOrigin(.init(x: 22, y: frame.midY - headerView.frame.height)) + membersCountView.setFrameOrigin(.init(x: 22, y: frame.midY)) + + button.frame = bounds + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } +} diff --git a/Telegram-Mac/ChatHistoryEntry.swift b/Telegram-Mac/ChatHistoryEntry.swift index d8ec593ed6..652ecc3c02 100644 --- a/Telegram-Mac/ChatHistoryEntry.swift +++ b/Telegram-Mac/ChatHistoryEntry.swift @@ -7,48 +7,40 @@ // import Cocoa +import TGUIKit +import Postbox +import TelegramCore -import PostboxMac -import TelegramCoreMac +import MtProtoKit enum ChatHistoryEntryId : Hashable { - case hole(MessageHistoryHole) case message(Message) + case groupedPhotos(groupInfo: MessageGroupInfo) case unread case date(MessageIndex) case undefined case maybeId(AnyHashable) - var hashValue: Int { - switch self { - case let .hole(index): - return index.stableId.hashValue - case .message(let message): - return message.stableId.hashValue - case .unread: - return 2 << 1 - case .date(let index): - return index.hashValue - case .undefined: - return 3 << 1 - case .maybeId(let id): - return id.hashValue - } + case commentsHeader + case repliesHeader + case topThreadInset + func hash(into hasher: inout Hasher) { + } static func ==(lhs:ChatHistoryEntryId, rhs: ChatHistoryEntryId) -> Bool { switch lhs { - case let .hole(index): - if case .hole(index) = rhs { - return true - } else { - return false - } case .message(let lhsMessage): if case .message(let rhsMessage) = rhs { return lhsMessage.stableId == rhsMessage.stableId } else { return false } + case let .groupedPhotos(groupingKey): + if case .groupedPhotos(groupingKey) = rhs { + return true + } else { + return false + } case .unread: if case .unread = rhs { return true @@ -73,148 +65,308 @@ enum ChatHistoryEntryId : Hashable { } else { return false } + case .commentsHeader: + if case .commentsHeader = rhs { + return true + } else { + return false + } + case .repliesHeader: + if case .repliesHeader = rhs { + return true + } else { + return false + } + case .topThreadInset: + if case .topThreadInset = rhs { + return true + } else { + return false + } } } var stableIndex: UInt64 { switch self { - case .hole: - return UInt64(0) << 40 case .message: return UInt64(1) << 40 + case .groupedPhotos: + return UInt64(2) << 40 case .unread: - return UInt64(2) << 40 + return UInt64(3) << 40 case .date: - return UInt64(3) << 40 - case .undefined: return UInt64(4) << 40 - case .maybeId: + case .undefined: return UInt64(5) << 40 + case .maybeId: + return UInt64(6) << 40 + case .commentsHeader: + return UInt64(7) << 40 + case .repliesHeader: + return UInt64(8) << 40 + case .topThreadInset: + return UInt64(9) << 40 } } } +struct ChatPollStateData : Equatable { + let identifiers: [Data] + let isLoading: Bool + init(identifiers: [Data] = [], isLoading: Bool = false) { + self.identifiers = identifiers + self.isLoading = isLoading + } +} + +struct MessageEntryAdditionalData : Equatable { + let pollStateData: ChatPollStateData + let highlightFoundText: HighlightFoundText? + let isThreadLoading: Bool + let updatingMedia: ChatUpdatingMessageMedia? + let chatTheme: TelegramPresentationTheme? + init(pollStateData: ChatPollStateData = ChatPollStateData(), highlightFoundText: HighlightFoundText? = nil, isThreadLoading: Bool = false, updatingMedia: ChatUpdatingMessageMedia? = nil, chatTheme: TelegramPresentationTheme? = nil) { + self.pollStateData = pollStateData + self.highlightFoundText = highlightFoundText + self.isThreadLoading = isThreadLoading + self.updatingMedia = updatingMedia + self.chatTheme = chatTheme + } +} + +struct HighlightFoundText : Equatable { + let query: String + let isMessage: Bool + init(query: String, isMessage: Bool) { + self.query = query + self.isMessage = isMessage + } +} + +final class ChatHistoryEntryData : Equatable { + let location: MessageHistoryEntryLocation? + let additionData: MessageEntryAdditionalData + let autoPlay: AutoplayMediaPreferences? + init(_ location: MessageHistoryEntryLocation?, _ additionData: MessageEntryAdditionalData, _ autoPlay: AutoplayMediaPreferences?) { + self.location = location + self.additionData = additionData + self.autoPlay = autoPlay + } + static func ==(lhs: ChatHistoryEntryData, rhs: ChatHistoryEntryData) -> Bool { + return lhs.location == rhs.location && lhs.additionData == rhs.additionData && lhs.autoPlay == rhs.autoPlay + } +} + enum ChatHistoryEntry: Identifiable, Comparable { - case HoleEntry(MessageHistoryHole) - case MessageEntry(Message, Bool, ChatItemType, ForwardItemType?, MessageHistoryEntryLocation?) - case UnreadEntry(MessageIndex) - case DateEntry(MessageIndex) + case MessageEntry(Message, MessageIndex, Bool, ChatItemRenderType, ChatItemType, ForwardItemType?, ChatHistoryEntryData) + case groupedPhotos([ChatHistoryEntry], groupInfo: MessageGroupInfo) + case UnreadEntry(MessageIndex, ChatItemRenderType, TelegramPresentationTheme) + case DateEntry(MessageIndex, ChatItemRenderType, TelegramPresentationTheme) case bottom + case commentsHeader(Bool, MessageIndex, ChatItemRenderType) + case repliesHeader(Bool, MessageIndex, ChatItemRenderType) + case topThreadInset(CGFloat, MessageIndex, ChatItemRenderType) var message:Message? { switch self { - case let .MessageEntry(message,_,_,_,_): + case let .MessageEntry(message,_, _,_,_,_,_): return message default: return nil } } + + var autoplayMedia: AutoplayMediaPreferences { + switch self { + case let .MessageEntry(_,_,_,_,_,_,data): + return data.autoPlay ?? AutoplayMediaPreferences.defaultSettings + case let .groupedPhotos(entries, _): + return entries.first?.autoplayMedia ?? AutoplayMediaPreferences.defaultSettings + default: + return AutoplayMediaPreferences.defaultSettings + } + } + + var renderType: ChatItemRenderType { + switch self { + case let .MessageEntry(_,_,_, renderType,_,_,_): + return renderType + case .groupedPhotos(let entries, _): + return entries.first!.renderType + case let .DateEntry(_, renderType, _): + return renderType + case .UnreadEntry(_, let renderType, _): + return renderType + case .bottom: + return .list + case let .commentsHeader(_, _, renderType): + return renderType + case let .repliesHeader(_, _, renderType): + return renderType + case let .topThreadInset(_, _, renderType): + return renderType + } + } + var itemType: ChatItemType? { + switch self { + case let .MessageEntry(_, _, _, _, itemType, _, _): + return itemType + default: + return nil + } + } + var location:MessageHistoryEntryLocation? { switch self { - case let .MessageEntry(_,_,_,_,location): - return location + case let .MessageEntry(_,_,_,_,_,_,data): + return data.location default: return nil } } + var additionalData: MessageEntryAdditionalData { + switch self { + case let .MessageEntry(_,_,_,_,_,_,data): + return data.additionData + case let .groupedPhotos(entries,_): + return entries.first?.additionalData ?? MessageEntryAdditionalData() + default: + return MessageEntryAdditionalData() + } + } + var stableId: ChatHistoryEntryId { switch self { - case let .HoleEntry(hole): - return .hole(hole) - case let .MessageEntry(message,_,_,_,_): + case let .MessageEntry(message, _, _, _, _, _, _): return .message(message) - case let .DateEntry(index): + case .groupedPhotos(_, let info): + return .groupedPhotos(groupInfo: info) + case let .DateEntry(index, _, _): return .date(index) case .UnreadEntry: return .unread case .bottom: return .undefined + case .commentsHeader: + return .commentsHeader + case .repliesHeader: + return .repliesHeader + case .topThreadInset: + return .topThreadInset } } var index: MessageIndex { switch self { - case let .HoleEntry(hole): - return hole.maxIndex - case let .MessageEntry(message,_,_, _,_): + case let .MessageEntry(_,index, _, _, _, _,_): + return index + case let .groupedPhotos(entries, _): + return entries.last!.index + case let .UnreadEntry(index, _, _): + return index + case let .DateEntry(index, _, _): + return index + case .bottom: + return MessageIndex.absoluteUpperBound() + case let .commentsHeader(_, index, _): + return index + case let .repliesHeader(_, index, _): + return index + case let .topThreadInset(_, index, _): + return index + } + } + + + var scrollIndex: MessageIndex { + switch self { + case let .MessageEntry(message, _, _, _, _, _, _): return MessageIndex(message) - case let .UnreadEntry(index): + case let .groupedPhotos(entries, _): + return entries.last!.index + case let .UnreadEntry(index, _, _): return index - case let .DateEntry(index): + case let .DateEntry(index, _, _): return index case .bottom: return MessageIndex.absoluteUpperBound() + case let .commentsHeader(_, index, _): + return index + case let .repliesHeader(_, index, _): + return index + case let .topThreadInset(_, index, _): + return index + } + } + + func withUpdatedItemType(_ itemType: ChatItemType) -> ChatHistoryEntry { + switch self { + case let .MessageEntry(values): + return .MessageEntry(values.0, values.1, values.2, values.3, itemType, values.5, values.6) + default: + return self } } + func withUpdatedMessageMedia(_ media: Media) -> ChatHistoryEntry { + switch self { + case let .MessageEntry(values): + return .MessageEntry(values.0.withUpdatedMedia([media]), values.1, values.2, values.3, values.4, values.5, values.6) + default: + return self + } + } +} +func isEqualMessageList(lhs:[Message], rhs:[Message]) -> Bool { + if lhs.count != rhs.count { + return false + } else { + for (i, message) in lhs.enumerated() { + if !isEqualMessages(message, rhs[i]) { + return false + } + } + } + return true } + func ==(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool { switch lhs { - case let .HoleEntry(lhsHole): + case let .MessageEntry(message, index, read, renderType, type, fwdType, data): switch rhs { - case let .HoleEntry(rhsHole) where lhsHole == rhsHole: + case .MessageEntry(message, index, read, renderType, type, fwdType, data): return true default: return false } - case let .MessageEntry(lhsMessage,lhsRead,lhsType, lhsFwdType, _): - switch rhs { - case let .MessageEntry(rhsMessage,rhsRead,rhsType, rhsFwdType, _) where MessageIndex(lhsMessage) == MessageIndex(rhsMessage) && lhsMessage.stableVersion == rhsMessage.stableVersion && lhsRead == rhsRead && lhsType == rhsType && lhsFwdType == rhsFwdType: - if lhsMessage.media.count != rhsMessage.media.count { - return false - } - for i in 0 ..< lhsMessage.media.count { - if !lhsMessage.media[i].isEqual(rhsMessage.media[i]) { - return false - } - } - - - if lhsMessage.associatedMessages.count != rhsMessage.associatedMessages.count { - return false - } else { - for (messageId, lhsAssociatedMessage) in lhsMessage.associatedMessages { - if let rhsAssociatedMessage = rhsMessage.associatedMessages[messageId] { - if lhsAssociatedMessage.stableVersion != rhsAssociatedMessage.stableVersion { - return false - } - } else { - return false - } - } - } - - if lhsMessage.peers.count != rhsMessage.peers.count { + case let .groupedPhotos(lhsEntries, lhsGroupingKey): + if case let .groupedPhotos(rhsEntries, rhsGroupingKey) = rhs { + if lhsEntries.count != rhsEntries.count { return false } else { - for (lhsPeerId, lhsPeer) in lhsMessage.peers { - if let rhsPeer = rhsMessage.peers[lhsPeerId] { - if !lhsPeer.isEqual(rhsPeer) { - return false - } - } else { + for i in 0 ..< lhsEntries.count { + if lhsEntries[i] != rhsEntries[i] { return false } } + return lhsGroupingKey == rhsGroupingKey } - - return true - default: + } else { return false } - case let .UnreadEntry(lhsIndex): + case let .UnreadEntry(index, renderType, theme): switch rhs { - case let .UnreadEntry(rhsIndex) where lhsIndex == rhsIndex: + case .UnreadEntry(index, renderType, theme): return true default: return false } - case let .DateEntry(lhsIndex): + case let .DateEntry(index, renderType, lhsTheme): switch rhs { - case let .DateEntry(rhsIndex) where lhsIndex == rhsIndex: + case .DateEntry(index, renderType, lhsTheme): return true default: return false @@ -226,6 +378,27 @@ func ==(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool { default: return false } + case let .commentsHeader(empty, index, type): + switch rhs { + case .commentsHeader(empty, index, type): + return true + default: + return false + } + case let .repliesHeader(empty, index, type): + switch rhs { + case .repliesHeader(empty, index, type): + return true + default: + return false + } + case let .topThreadInset(value, index, type): + switch rhs { + case .topThreadInset(value, index, type): + return true + default: + return false + } } } @@ -241,56 +414,162 @@ func <(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool { } -func messageEntries(_ messagesEntries: [MessageHistoryEntry], maxReadIndex:MessageIndex? = nil, includeHoles: Bool = true, dayGrouping: Bool = false, includeBottom:Bool = false, timeDifference: TimeInterval = 0, adminIds:[PeerId] = []) -> [ChatHistoryEntry] { +func messageEntries(_ messagesEntries: [MessageHistoryEntry], maxReadIndex:MessageIndex? = nil, includeHoles: Bool = true, dayGrouping: Bool = false, renderType: ChatItemRenderType = .list, includeBottom:Bool = false, timeDifference: TimeInterval = 0, ranks:CachedChannelAdminRanks? = nil, pollAnswersLoading: [MessageId : ChatPollStateData] = [:], threadLoading: MessageId? = nil, groupingPhotos: Bool = false, autoplayMedia: AutoplayMediaPreferences? = nil, searchState: SearchMessagesResultState? = nil, animatedEmojiStickers: [String: StickerPackItem] = [:], topFixedMessages: [Message]? = nil, customChannelDiscussionReadState: MessageId? = nil, customThreadOutgoingReadState: MessageId? = nil, addRepliesHeader: Bool = false, addTopThreadInset: CGFloat? = nil, updatingMedia: [MessageId: ChatUpdatingMessageMedia] = [:], adMessages:[Message] = [], chatTheme: TelegramPresentationTheme = theme) -> [ChatHistoryEntry] { var entries: [ChatHistoryEntry] = [] - + - var i:Int = 0 - for entry in messagesEntries { - switch entry { - case let .HoleEntry(hole, _): - if includeHoles { - entries.append(.HoleEntry(hole)) - } - case let .MessageEntry(message,read, location, _): - - var disableEntry = false - if let action = message.media.first as? TelegramMediaAction { - switch action.action { - case .historyCleared: - disableEntry = true - default: - break + var groupedPhotos:[ChatHistoryEntry] = [] + var groupInfo: MessageGroupInfo? + + var messagesEntries = messagesEntries + var topMessageIndex: Int? = nil + if let topMessages = topFixedMessages, !topMessages.isEmpty { + messagesEntries.insert(contentsOf: topMessages.map { MessageHistoryEntry(message: $0, isRead: true, location: nil, monthLocation: nil, attributes: .init(authorIsContact: false))}, at: 0) + topMessageIndex = topMessages.count - 1 + } + + for (i, entry) in messagesEntries.enumerated() { + var message = entry.message + + + + if message.media.isEmpty { + if message.text.length <= 7 { + let original = message.text.fixed + let unmodified = original.emojiUnmodified + if original.isSingleEmoji, let item = animatedEmojiStickers[unmodified] { + var file = item.file + var attributes = file.attributes + attributes.removeAll { attr in + if case .FileName = attr { + return true + } else { + return false + } + } + attributes = attributes.map { attribute -> TelegramMediaFileAttribute in + switch attribute { + case let .Sticker(_, packReference, maskData): + return .Sticker(displayText: original, packReference: packReference, maskData: maskData) + default: + return attribute + } + } + var disableStickers: Bool = false + if let peer = messageMainPeer(message) as? TelegramChannel { + if permissionText(from: peer, for: [.banSendGifs, .banSendStickers]) != nil { + disableStickers = true + } + } + if !disableStickers { + attributes.append(.FileName(fileName: "telegram-animoji.tgs")) + file = file.withUpdatedAttributes(attributes) + message = message.withUpdatedMedia([file]) + } } } - - if disableEntry { + } + + if let updating = updatingMedia[message.id] { + message = message.withUpdatedText(updating.text) + var attributes = message.attributes + if let entities = updating.entities, let index = attributes.firstIndex(where: { $0 is TextEntitiesMessageAttribute }) { + attributes[index] = entities + } + message = message.withUpdatedAttributes(attributes) + inner: switch updating.media { + case let .update(media): + message = message.withUpdatedMedia([media.media]) + default: + break inner + } + } + + + var disableEntry = false + if let action = message.media.first as? TelegramMediaAction { + switch action.action { + case .historyCleared: + disableEntry = true + case .groupMigratedToChannel: + disableEntry = true + case .channelMigratedFromGroup: + disableEntry = true + case .peerJoined: + disableEntry = false + default: break } + } + + if disableEntry { + continue + } + + + + var prev:MessageHistoryEntry? = nil + var next:MessageHistoryEntry? = nil + + if i > 0 { + loop: for k in stride(from: i - 1, to: -1, by: -1) { + let current = messagesEntries[k] + if let groupInfo = message.groupInfo { + if current.message.groupInfo == groupInfo { + continue loop + } else { + prev = current + break loop + } + } else { + prev = current + break loop + } + } - var prev:MessageHistoryEntry? = nil - var next:MessageHistoryEntry? = nil - - if i > 0 { - prev = messagesEntries[i - 1] + } + if i < messagesEntries.count - 1 { + loop: for k in i + 1 ..< messagesEntries.count { + let current = messagesEntries[k] + if let groupInfo = message.groupInfo { + if current.message.groupInfo == groupInfo { + continue loop + } else { + next = current + break loop + } + } else { + next = current + break loop + } } - if i < messagesEntries.count - 1 { - next = messagesEntries[i + 1] + } + + + let rawRank = ranks?.ranks.first(where: { $0.peerId == message.author?.id }) + var rank:String? = nil + if let rawRank = rawRank { + switch rawRank.type { + case .admin: + rank = L10n.chatAdminBadge + case .owner: + rank = L10n.chatOwnerBadge + case let .custom(string): + rank = string } - - let isAdmin = adminIds.contains(message.author?.id ?? PeerId(0)) - - var itemType:ChatItemType = .Full(isAdmin: isAdmin) - var fwdType:ForwardItemType? = nil - - - - - if let prev = prev, case let .MessageEntry(prevMessage,_, _, _) = prev { - - + } + + var itemType:ChatItemType = .Full(rank: rank, header: .normal) + var fwdType:ForwardItemType? = nil + + if message.itHasRestrictedContent { + message = message.withUpdatedMedia([]).withUpdatedText(" ") + } + + if renderType == .list { + if let prev = prev { var actionShortAccess: Bool = true - if let action = prevMessage.media.first as? TelegramMediaAction { + if let action = prev.message.media.first as? TelegramMediaAction { switch action.action { case .phoneCall: actionShortAccess = true @@ -299,111 +578,314 @@ func messageEntries(_ messagesEntries: [MessageHistoryEntry], maxReadIndex:Messa } } - if message.author?.id == prevMessage.author?.id, (message.timestamp - prevMessage.timestamp) < simpleDif, actionShortAccess, let peer = message.peers[message.id.peerId] { + if message.author?.id == prev.message.author?.id, (message.timestamp - prev.message.timestamp) < simpleDif, actionShortAccess, let peer = message.peers[message.id.peerId] { if let peer = peer as? TelegramChannel, case .broadcast(_) = peer.info { - itemType = .Full(isAdmin: isAdmin) + itemType = .Full(rank: rank, header: .normal) } else { - var canShort:Bool = true - for attr in message.attributes { - if !(attr is OutgoingMessageInfoAttribute) && !(attr is TextEntitiesMessageAttribute) && !(attr is EditedMessageAttribute) && !(attr is ForwardSourceInfoAttribute) && !(attr is ViewCountMessageAttribute) && !(attr is ConsumableContentMessageAttribute) && !(attr is NotificationInfoMessageAttribute) && !(attr is ChannelMessageStateVersionAttribute) { + var canShort:Bool = (message.media.isEmpty || message.media.first?.isInteractiveMedia == false) || message.forwardInfo == nil || renderType == .list + + let allowAttributes:[MessageAttribute.Type] = [ReplyThreadMessageAttribute.self, OutgoingMessageInfoAttribute.self, TextEntitiesMessageAttribute.self, EditedMessageAttribute.self, ForwardSourceInfoAttribute.self, ViewCountMessageAttribute.self, ConsumableContentMessageAttribute.self, NotificationInfoMessageAttribute.self, ChannelMessageStateVersionAttribute.self, AutoremoveTimeoutMessageAttribute.self] + + attrsLoop: for attr in message.attributes { + let contains = allowAttributes.contains(where: { type(of: attr) == $0 }) + if !contains { canShort = false - break + break attrsLoop } } - itemType = !canShort ? .Full(isAdmin: isAdmin) : .Short + itemType = !canShort ? .Full(rank: rank, header: .normal) : .Short(rank: rank, header: .normal) } } else { - itemType = .Full(isAdmin: isAdmin) + itemType = .Full(rank: rank, header: .normal) } } else { - itemType = .Full(isAdmin: isAdmin) + itemType = .Full(rank: rank, header: .normal) } + } else { + let isSameGroup:(Message, Message) -> Bool = { lhs, rhs in + var accept = abs(lhs.timestamp - rhs.timestamp) < simpleDif + accept = accept && chatDateId(for: lhs.timestamp) == chatDateId(for: rhs.timestamp) + accept = accept && lhs.author?.id == rhs.author?.id + if let maxReadIndex = maxReadIndex { + if maxReadIndex >= rhs.index && maxReadIndex < lhs.index { + accept = false + } else if maxReadIndex < rhs.index && maxReadIndex >= lhs.index { + accept = false + } + } + if lhs.media.first is TelegramMediaAction { + accept = false + } + if rhs.media.first is TelegramMediaAction { + accept = false + } + if lhs.isAnonymousMessage { + accept = false + } + if rhs.isAnonymousMessage { + accept = false + } + return accept + } - if message.forwardInfo != nil { - if case .Short = itemType { - if let prev = prev, case let .MessageEntry(prevMessage,_, _, _) = prev { - if prevMessage.forwardInfo != nil, message.timestamp - prevMessage.timestamp < simpleDif { - fwdType = .Inside - if let next = next, case let .MessageEntry(nextMessage,_, _, _) = next { - - if message.author?.id != nextMessage.author?.id || nextMessage.timestamp - message.timestamp > simpleDif || nextMessage.forwardInfo == nil { - fwdType = .Bottom - } - - } else { + if let next = next { + if isSameGroup(message, next.message) { + if let prev = prev { + itemType = .Short(rank: rank, header: isSameGroup(message, prev.message) ? .short : .normal) + } else { + itemType = .Short(rank: rank, header: .normal) + } + } else { + if let prev = prev { + let shouldGroup = isSameGroup(message, prev.message) + itemType = .Full(rank: rank, header: shouldGroup ? .short : .normal) + } else { + itemType = .Full(rank: rank, header: .normal) + } + } + } else { + if let prev = prev { + let shouldGroup = isSameGroup(message, prev.message) + itemType = .Full(rank: rank, header: shouldGroup ? .short : .normal) + } else { + itemType = .Full(rank: rank, header: .normal) + } + } + } + + + + + if message.forwardInfo != nil, !message.isImported { + if case .Short = itemType { + if let prev = prev { + if prev.message.forwardInfo != nil, message.timestamp - prev.message.timestamp < simpleDif { + fwdType = .Inside + if let next = next { + if message.author?.id != next.message.author?.id || next.message.timestamp - message.timestamp > simpleDif || next.message.forwardInfo == nil { fwdType = .Bottom } - } else { - fwdType = .ShortHeader + fwdType = .Bottom } + } else { + fwdType = .ShortHeader } - } else { - fwdType = .ShortHeader } + } else { + fwdType = .ShortHeader } - - if let forwardType = fwdType, forwardType == .ShortHeader || forwardType == .FullHeader { - itemType = .Full(isAdmin: isAdmin) - if forwardType == .ShortHeader { - if let next = next, case let .MessageEntry(nextMessage,_, _, _) = next { - if nextMessage.forwardInfo != nil && (message.author?.id == nextMessage.author?.id || nextMessage.timestamp - message.timestamp < simpleDif) { - fwdType = .FullHeader - } - + } + + if let forwardType = fwdType, forwardType == .ShortHeader || forwardType == .FullHeader, renderType != .bubble { + itemType = .Full(rank: rank, header: .normal) + if forwardType == .ShortHeader { + if let next = next { + if next.message.forwardInfo != nil && (message.author?.id == next.message.author?.id || next.message.timestamp - message.timestamp < simpleDif) { + fwdType = .FullHeader } + } } - - - if prev == nil && dayGrouping { - var time = TimeInterval(message.timestamp) - time -= timeDifference - let dateId = chatDateId(for: Int32(time)) - let index = MessageIndex(id: message.id, timestamp: Int32(dateId)) - entries.append(.DateEntry(index)) + } + + let additionalData: MessageEntryAdditionalData + var highlightFoundText: HighlightFoundText? = nil + + + if let searchState = searchState, !message.text.isEmpty { + highlightFoundText = HighlightFoundText(query: searchState.query, isMessage: searchState.containsMessage(message)) + } + + if let data = pollAnswersLoading[message.id] { + additionalData = MessageEntryAdditionalData(pollStateData: data, highlightFoundText: highlightFoundText, isThreadLoading: threadLoading == message.id, updatingMedia: updatingMedia[message.id], chatTheme: chatTheme) + } else { + additionalData = MessageEntryAdditionalData(pollStateData: ChatPollStateData(), highlightFoundText: highlightFoundText, isThreadLoading: threadLoading == message.id, updatingMedia: updatingMedia[message.id], chatTheme: chatTheme) + } + let data = ChatHistoryEntryData(entry.location, additionalData, autoplayMedia) + + + + let timestamp = Int32(min(TimeInterval(message.timestamp) - timeDifference, TimeInterval(Int32.max))) + + + var isRead = entry.isRead + if !message.flags.contains(.Incoming) { + var k = i + loop: while k < messagesEntries.count - 1 { + let next = messagesEntries[k + 1] + if next.message.flags.contains(.Incoming) { + isRead = true + break loop + } + k += 1 } + } + + if let customThreadOutgoingReadState = customThreadOutgoingReadState { + isRead = customThreadOutgoingReadState >= message.id + } + + if let customChannelDiscussionReadState = customChannelDiscussionReadState { + attibuteLoop: for i in 0 ..< message.attributes.count { + if let attribute = message.attributes[i] as? ReplyThreadMessageAttribute { + if let maxReadMessageId = attribute.maxReadMessageId { + if maxReadMessageId < customChannelDiscussionReadState.id { + var attributes = message.attributes + attributes[i] = ReplyThreadMessageAttribute(count: attribute.count, latestUsers: attribute.latestUsers, commentsPeerId: attribute.commentsPeerId, maxMessageId: attribute.maxMessageId, maxReadMessageId: customChannelDiscussionReadState.id) + message = message.withUpdatedAttributes(attributes) + } + } + break attibuteLoop + } + } + } - entries.append(.MessageEntry(message,read,itemType,fwdType, location)) - - if let next = next, case let .MessageEntry(nextMessage,_, _, _) = next, dayGrouping { - let dateId = chatDateId(for: message.timestamp - Int32(timeDifference)) - let nextDateId = chatDateId(for: nextMessage.timestamp - Int32(timeDifference)) - if dateId != nextDateId { - let index = MessageIndex(id: MessageId(peerId: message.id.peerId, namespace: message.id.namespace, id: INT_MAX), timestamp: Int32(nextDateId)) - entries.append(.DateEntry(index)) + + + let entry: ChatHistoryEntry = .MessageEntry(message, MessageIndex(message.withUpdatedTimestamp(timestamp)), isRead, renderType, itemType, fwdType, data) + + if let key = message.groupInfo, groupingPhotos, message.id.peerId.namespace == Namespaces.Peer.SecretChat || !message.containsSecretMedia, !message.media.isEmpty { + if groupInfo == nil { + groupInfo = key + groupedPhotos.append(entry.withUpdatedItemType(.Full(rank: rank, header: .normal))) + } else if groupInfo == key { + groupedPhotos.append(entry.withUpdatedItemType(.Full(rank: rank, header: .normal))) + } else { + if groupedPhotos.count > 0 { + if let groupInfo = groupInfo { + if groupedPhotos.count > 1 { + entries.append(.groupedPhotos(groupedPhotos, groupInfo: groupInfo)) + } else { + entries.append(groupedPhotos[0]) + } + } + groupedPhotos.removeAll() } + + groupInfo = key + groupedPhotos.append(entry.withUpdatedItemType(.Full(rank: rank, header: .normal))) } - + } else { + entries.append(entry) + } + + prev = nil + next = nil + + if i > 0 { + prev = messagesEntries[i - 1] + } + if i < messagesEntries.count - 1 { + next = messagesEntries[i + 1] } - i += 1 + if prev == nil && dayGrouping { + let timestamp = Int32(min(TimeInterval(message.timestamp) - timeDifference, TimeInterval(Int32.max))) + + let dateId = chatDateId(for: timestamp) + let index = MessageIndex(id: MessageId(peerId: message.id.peerId, namespace: Namespaces.Message.Local, id: 0), timestamp: Int32(dateId)) + entries.append(.DateEntry(index, renderType, chatTheme)) + } + + if let next = next, dayGrouping { + let timestamp = Int32(min(TimeInterval(message.timestamp) - timeDifference, TimeInterval(Int32.max))) + let nextTimestamp = Int32(min(TimeInterval(next.message.timestamp) - timeDifference, TimeInterval(Int32.max))) + + let dateId = chatDateId(for: timestamp) + let nextDateId = chatDateId(for: nextTimestamp) + + + if dateId != nextDateId { + let index = MessageIndex(id: MessageId(peerId: message.id.peerId, namespace: Namespaces.Message.Local, id: INT_MAX), timestamp: Int32(nextDateId)) + entries.append(.DateEntry(index, renderType, chatTheme)) + } + } + if let topMessageIndex = topMessageIndex, topMessageIndex == i { + let timestamp = Int32(min(TimeInterval(message.timestamp) - timeDifference, TimeInterval(Int32.max))) + + entries.append(.commentsHeader(i == messagesEntries.count - 1, MessageIndex(id: message.id, timestamp: timestamp).peerLocalSuccessor(), renderType)) + } } + var hasUnread = false if let maxReadIndex = maxReadIndex { - entries.append(.UnreadEntry(maxReadIndex)) + let timestamp = Int32(min(TimeInterval(maxReadIndex.timestamp) - timeDifference, TimeInterval(Int32.max))) + entries.append(.UnreadEntry(maxReadIndex.withUpdatedTimestamp(timestamp), renderType, chatTheme)) hasUnread = true } + if includeBottom { entries.append(.bottom) } + if !groupedPhotos.isEmpty, let key = groupInfo { + if groupedPhotos.count == 1 { + entries.append(groupedPhotos[0]) + } else { + entries.append(.groupedPhotos(groupedPhotos, groupInfo: key)) + } + } + + if addRepliesHeader { + entries.insert(.repliesHeader(true, MessageIndex.absoluteLowerBound().globalSuccessor(), renderType), at: 0) + } + if let addTopThreadInset = addTopThreadInset { + entries.insert(.topThreadInset(addTopThreadInset, MessageIndex.absoluteLowerBound(), renderType), at: 0) + } + + + + if let lastMessage = entries.last(where: { $0.message != nil })?.message { + var nextAdMessageId: Int32 = 1 + for message in adMessages { + let updatedMessage = Message( + stableId: UInt32.max - 1 - UInt32(nextAdMessageId), + stableVersion: message.stableVersion, + id: MessageId(peerId: message.id.peerId, namespace: message.id.namespace, id: nextAdMessageId), + globallyUniqueId: nil, + groupingKey: nil, + groupInfo: nil, + threadId: nil, + timestamp: lastMessage.timestamp, + flags: message.flags, + tags: message.tags, + globalTags: message.globalTags, + localTags: message.localTags, + forwardInfo: message.forwardInfo, + author: message.author, + text: message.text, + attributes: message.attributes, + media: message.media, + peers: message.peers, + associatedMessages: message.associatedMessages, + associatedMessageIds: message.associatedMessageIds + ) + nextAdMessageId += 1 + + let timestamp = Int32(min(TimeInterval(updatedMessage.timestamp) - timeDifference, TimeInterval(Int32.max))) + entries.append(.MessageEntry(updatedMessage, MessageIndex(updatedMessage.withUpdatedTimestamp(timestamp)), true, renderType, .Full(rank: nil, header: .normal), nil, .init(nil, .init(), autoplayMedia))) + + //add entry + } + } var sorted = entries.sorted() + if hasUnread, sorted.count >= 2 { if case .UnreadEntry = sorted[sorted.count - 2] { sorted.remove(at: sorted.count - 2) } } - + return sorted } diff --git a/Telegram-Mac/ChatHistoryViewForLocation.swift b/Telegram-Mac/ChatHistoryViewForLocation.swift index 8c89e74a4c..e4c279513a 100644 --- a/Telegram-Mac/ChatHistoryViewForLocation.swift +++ b/Telegram-Mac/ChatHistoryViewForLocation.swift @@ -8,9 +8,10 @@ import Cocoa -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac +import Postbox +import TelegramCore + +import SwiftSignalKit import TGUIKit enum ChatHistoryInitialSearchLocation { @@ -18,18 +19,51 @@ enum ChatHistoryInitialSearchLocation { case id(MessageId) } + +struct ChatHistoryLocationInput: Equatable { + var content: ChatHistoryLocation + var id: Int32 + + init(content: ChatHistoryLocation, id: Int32) { + self.content = content + self.id = id + } +} + enum ChatHistoryLocation: Equatable { case Initial(count: Int) case InitialSearch(location: ChatHistoryInitialSearchLocation, count: Int) - case Navigation(index: MessageIndex, anchorIndex: MessageIndex) - case Scroll(index: MessageIndex, anchorIndex: MessageIndex, sourceIndex: MessageIndex, scrollPosition: TableScrollState, animated: Bool) + case Navigation(index: MessageHistoryAnchorIndex, anchorIndex: MessageHistoryAnchorIndex, count: Int, side: TableSavingSide) + case Scroll(index: MessageHistoryAnchorIndex, anchorIndex: MessageHistoryAnchorIndex, sourceIndex: MessageHistoryAnchorIndex, scrollPosition: TableScrollState, count: Int, animated: Bool) + + var count: Int { + switch self { + case let .Initial(count): + return count + case let .InitialSearch(_, count): + return count + case let .Navigation(_, _, count, _): + return count + case let .Scroll(_, _, _, _, count, _): + return count + } + } + + var side: TableSavingSide? { + switch self { + case let .Navigation(_, _, _, side): + return side + default: + return nil + } + } } func ==(lhs: ChatHistoryLocation, rhs: ChatHistoryLocation) -> Bool { switch lhs { - case let .Navigation(lhsIndex, lhsAnchorIndex): + case let .Navigation(lhsIndex, lhsAnchorIndex, lhsCount, lhsSide): switch rhs { - case let .Navigation(rhsIndex, rhsAnchorIndex) where lhsIndex == rhsIndex && lhsAnchorIndex == rhsAnchorIndex: + case let .Navigation(rhsIndex, rhsAnchorIndex, rhsCount, rhsSide) where lhsIndex == rhsIndex && lhsAnchorIndex == rhsAnchorIndex && lhsCount == rhsCount && lhsSide == rhsSide: return true default: return false @@ -41,17 +75,44 @@ func ==(lhs: ChatHistoryLocation, rhs: ChatHistoryLocation) -> Bool { -enum ChatHistoryViewScrollPosition { +enum ChatHistoryViewScrollPosition : Equatable { case unread(index: MessageIndex) case positionRestoration(index: MessageIndex, relativeOffset: CGFloat) - case index(index: MessageIndex, position: TableScrollState, directionHint: ListViewScrollToItemDirectionHint, animated: Bool) + case index(index: MessageHistoryAnchorIndex, position: TableScrollState, directionHint: ListViewScrollToItemDirectionHint, animated: Bool) +} + +func ==(lhs: ChatHistoryViewScrollPosition, rhs: ChatHistoryViewScrollPosition) -> Bool { + switch lhs { + case let .unread(index): + if case .unread(index: index) = rhs { + return true + } else { + return false + } + case let .positionRestoration(index, relativeOffset): + if case .positionRestoration(index: index, relativeOffset: relativeOffset) = rhs { + return true + } else { + return false + } + case let .index(index, position, directionHint, animated): + if case .index(index: index, position: position, directionHint: directionHint, animated: animated) = rhs { + return true + } else { + return false + } + } } public struct ChatHistoryCombinedInitialData { let initialData: InitialMessageHistoryData? let buttonKeyboardMessage: Message? let cachedData: CachedPeerData? - let readStateData: ChatHistoryCombinedInitialReadStateData? + let cachedDataMessages:[MessageId: [Message]]? + let readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]? + let limitsConfiguration: LimitsConfiguration + let autoplayMedia: AutoplayMediaPreferences + let autodownloadSettings: AutomaticMediaDownloadSettings } enum ChatHistoryViewUpdateType { @@ -62,46 +123,51 @@ enum ChatHistoryViewUpdateType { public struct ChatHistoryCombinedInitialReadStateData { public let unreadCount: Int32 public let totalUnreadCount: Int32 + public let notificationSettings: PeerNotificationSettings? } + + enum ChatHistoryViewUpdate { - case Loading(initialData: InitialMessageHistoryData?) + case Loading(initialData: ChatHistoryCombinedInitialData, type: ChatHistoryViewUpdateType) case HistoryView(view: MessageHistoryView, type: ChatHistoryViewUpdateType, scrollPosition: ChatHistoryViewScrollPosition?, initialData: ChatHistoryCombinedInitialData) } -func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Account, peerId: PeerId, fixedCombinedReadState: CombinedPeerReadState?, tagMask: MessageTags?, additionalData: [AdditionalMessageHistoryViewData] = [], orderStatistics: MessageHistoryViewOrderStatistics = []) -> Signal { +func chatHistoryViewForLocation(_ location: ChatHistoryLocation, context: AccountContext, chatLocation _chatLocation: ChatLocation, fixedCombinedReadStates: (()->MessageHistoryViewReadState?)?, tagMask: MessageTags?, mode: ChatMode = .history, additionalData: [AdditionalMessageHistoryViewData] = [], orderStatistics: MessageHistoryViewOrderStatistics = [], chatLocationContextHolder: Atomic = Atomic(value: nil), chatLocationInput: ChatLocationInput? = nil) -> Signal { + + + let account = context.account + + let chatLocation = chatLocationInput ?? context.chatLocationInput(for: _chatLocation, contextHolder: chatLocationContextHolder) + + switch location { case let .Initial(count): var preloaded = false var fadeIn = false let signal: Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> - if let tagMask = tagMask { - signal = account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: MessageIndex.upperBound(peerId: peerId), count: count, anchorIndex: MessageIndex.upperBound(peerId: peerId), fixedCombinedReadState: nil, tagMask: tagMask, orderStatistics: orderStatistics) - } else { - signal = account.viewTracker.aroundMessageOfInterestHistoryViewForPeerId(peerId, count: count, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) + + switch mode { + case .history, .replyThread, .pinned, .preview: + if let tagMask = tagMask { + signal = account.viewTracker.aroundMessageHistoryViewForLocation(chatLocation, index: .upperBound, anchorIndex: .upperBound, count: count, fixedCombinedReadStates: nil, tagMask: tagMask, orderStatistics: orderStatistics) + } else { + signal = account.viewTracker.aroundMessageOfInterestHistoryViewForLocation(chatLocation, count: count, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) + } + case .scheduled: + signal = account.viewTracker.scheduledMessagesViewForLocation(chatLocation) } + + return signal |> map { view, updateType, initialData -> ChatHistoryViewUpdate in - var cachedData: CachedPeerData? - var readStateData: ChatHistoryCombinedInitialReadStateData? - for data in view.additionalData { - switch data { - case let .cachedPeerData(peerIdValue, value): - if peerIdValue == peerId { - cachedData = value - } - case let .totalUnreadCount(totalUnreadCount): - if let readState = view.combinedReadState { - readStateData = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadCount) - } - default: - break - } - } + let (cachedData, cachedDataMessages, readStateData, limitsConfiguration, autoplayMedia, autodownloadSettings) = extractAdditionalData(view: view, chatLocation: chatLocation) + let combinedInitialData = ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData, limitsConfiguration: limitsConfiguration, autoplayMedia: autoplayMedia, autodownloadSettings: autodownloadSettings) if preloaded { - return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, readStateData: readStateData)) + //NSLog("entriescount: \(view.entries.count)") + return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: combinedInitialData) } else { var scrollPosition: ChatHistoryViewScrollPosition? @@ -117,49 +183,47 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } } - let maxIndex = min(view.entries.count, targetIndex + count / 2) + let maxIndex = targetIndex + count / 2 + let minIndex = targetIndex - count / 2 + if minIndex <= 0 && view.holeEarlier { + fadeIn = true + return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType)) + } if maxIndex >= targetIndex { - for i in targetIndex ..< maxIndex { - if case .HoleEntry = view.entries[i] { - var incomingCount: Int32 = 0 - inner: for entry in view.entries.reversed() { - switch entry { - case .HoleEntry: - break inner - case let .MessageEntry(message, _, _, _): - if message.flags.contains(.Incoming) { - incomingCount += 1 - } - } - } - if let combinedReadState = view.combinedReadState, combinedReadState.count == incomingCount { - - } else { - fadeIn = true - return .Loading(initialData: initialData) + if view.holeLater { + fadeIn = true + return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType)) + } + if view.holeEarlier { + var incomingCount: Int32 = 0 + inner: for entry in view.entries.reversed() { + if entry.message.flags.contains(.Incoming) { + incomingCount += 1 } } + if case let .peer(peerId) = chatLocation, let combinedReadStates = view.fixedReadStates, case let .peer(readStates) = combinedReadStates, let readState = readStates[peerId], readState.count == incomingCount { + } else { + fadeIn = true + return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType)) + } } } - } else if let historyScrollState = (initialData?.chatInterfaceState as? ChatInterfaceState)?.historyScrollState { - scrollPosition = .positionRestoration(index: historyScrollState.messageIndex, relativeOffset: CGFloat(historyScrollState.relativeOffset)) + } else if let opaqueState = (initialData?.storedInterfaceState).flatMap(_internal_decodeStoredChatInterfaceState) { + + let interfaceState = ChatInterfaceState.parse(opaqueState, peerId: _chatLocation.peerId, context: context) + + if let historyScrollState = interfaceState?.historyScrollState { + scrollPosition = .positionRestoration(index: historyScrollState.messageIndex, relativeOffset: CGFloat(historyScrollState.relativeOffset)) + } } else { - var messageCount = 0 - for entry in view.entries.reversed() { - if case .HoleEntry = entry { - fadeIn = true - return .Loading(initialData: initialData) - } else { - messageCount += 1 - } - if messageCount >= 1 { - break - } + if view.entries.isEmpty && (view.holeEarlier || view.holeLater) { + fadeIn = true + return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType)) } } preloaded = true - return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: scrollPosition, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, readStateData: readStateData)) + return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: scrollPosition, initialData: combinedInitialData) } } case let .InitialSearch(searchLocation, count): @@ -167,86 +231,98 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun var fadeIn = false let signal: Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> - switch searchLocation { - case let .index(index): - signal = account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: count, anchorIndex: index, fixedCombinedReadState: nil, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) - case let .id(id): - signal = account.viewTracker.aroundIdMessageHistoryViewForPeerId(peerId, count: count, messageId: id, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) + + switch mode { + case .history, .replyThread, .pinned, .preview: + switch searchLocation { + case let .index(index): + signal = account.viewTracker.aroundMessageHistoryViewForLocation(chatLocation, index: MessageHistoryAnchorIndex.message(index), anchorIndex: MessageHistoryAnchorIndex.message(index), count: count, fixedCombinedReadStates: nil, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) + case let .id(id): + signal = account.viewTracker.aroundIdMessageHistoryViewForLocation(chatLocation, count: count, ignoreRelatedChats: false, messageId: id, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) + } + case .scheduled: + signal = account.viewTracker.scheduledMessagesViewForLocation(chatLocation) } + + return signal |> map { view, updateType, initialData -> ChatHistoryViewUpdate in - var cachedData: CachedPeerData? - var readStateData: ChatHistoryCombinedInitialReadStateData? - for data in view.additionalData { - switch data { - case let .cachedPeerData(peerIdValue, value): - if peerIdValue == peerId { - cachedData = value - } - case let .totalUnreadCount(totalUnreadCount): - if let readState = view.combinedReadState { - readStateData = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadCount) - } - default: - break - } - } + let (cachedData, cachedDataMessages, readStateData, limitsConfiguration, autoplayMedia, autodownloadSettings) = extractAdditionalData(view: view, chatLocation: chatLocation) + let combinedInitialData = ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData, limitsConfiguration: limitsConfiguration, autoplayMedia: autoplayMedia, autodownloadSettings: autodownloadSettings) + if preloaded { - return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, readStateData: readStateData)) + return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: combinedInitialData) } else { let anchorIndex = view.anchorIndex var targetIndex = 0 for i in 0 ..< view.entries.count { - if view.entries[i].index >= anchorIndex { + //if view.entries[i].index >= anchorIndex + if anchorIndex.isLessOrEqual(to: view.entries[i].index) { targetIndex = i break } } - let maxIndex = min(view.entries.count, targetIndex + count / 2) - if maxIndex >= targetIndex { - for i in targetIndex ..< maxIndex { - if case .HoleEntry = view.entries[i] { - fadeIn = true - return .Loading(initialData: initialData) - } + + if !view.entries.isEmpty { + let minIndex = max(0, targetIndex - count / 2) + let maxIndex = min(view.entries.count, targetIndex + count / 2) + if minIndex == 0 && view.holeEarlier { + fadeIn = true + return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType)) + } + if maxIndex == view.entries.count && view.holeLater { + fadeIn = true + return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType)) } + } else if view.holeEarlier || view.holeLater { + fadeIn = true + return .Loading(initialData: combinedInitialData, type: .Generic(type: updateType)) } + + var reportUpdateType: ChatHistoryViewUpdateType = .Initial(fadeIn: fadeIn) + if case .FillHole = updateType { + reportUpdateType = .Generic(type: updateType) + } + preloaded = true var scroll: TableScrollState - - if view.entries.count > targetIndex, let message = view.entries[targetIndex].message { - scroll = .center(id: ChatHistoryEntryId.message(message), animated: false, focus: true, inset: 0) + if view.entries.count > targetIndex { + let focusMessage = view.entries[targetIndex].message + let mustToFocus: Bool + switch searchLocation { + case let .index(index): + mustToFocus = view.entries[targetIndex].index == index + case let .id(id): + mustToFocus = view.entries[targetIndex].message.id == id + } + scroll = .center(id: ChatHistoryEntryId.message(focusMessage), innerId: nil, animated: false, focus: .init(focus: mustToFocus), inset: 0) } else { scroll = .none(nil) } - return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: .index(index: anchorIndex, position: scroll, directionHint: .Down, animated: false), initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, readStateData: readStateData)) + return .HistoryView(view: view, type: reportUpdateType, scrollPosition: .index(index: anchorIndex, position: scroll, directionHint: .Down, animated: false), initialData: combinedInitialData) } } - case let .Navigation(index, anchorIndex): + case let .Navigation(index, anchorIndex, count, _): var first = true - return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 140, anchorIndex: anchorIndex, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in - var cachedData: CachedPeerData? - var readStateData: ChatHistoryCombinedInitialReadStateData? - for data in view.additionalData { - switch data { - case let .cachedPeerData(peerIdValue, value): - if peerIdValue == peerId { - cachedData = value - } - case let .totalUnreadCount(totalUnreadCount): - if let readState = view.combinedReadState { - readStateData = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadCount) - } - default: - break - } - } + + let signal:Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> + switch mode { + case .history, .replyThread, .pinned, .preview: + signal = account.viewTracker.aroundMessageHistoryViewForLocation(chatLocation, index: index, anchorIndex: anchorIndex, count: count, fixedCombinedReadStates: fixedCombinedReadStates?(), tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) + case .scheduled: + signal = account.viewTracker.scheduledMessagesViewForLocation(chatLocation) + } + + return signal |> map { view, updateType, initialData -> ChatHistoryViewUpdate in + + let (cachedData, cachedDataMessages, readStateData, limitsConfiguration, autoplayMedia, autodownloadSettings) = extractAdditionalData(view: view, chatLocation: chatLocation) + let combinedInitialData = ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData, limitsConfiguration: limitsConfiguration, autoplayMedia: autoplayMedia, autodownloadSettings: autodownloadSettings) let genericType: ViewUpdateType if first { @@ -255,29 +331,24 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } else { genericType = updateType } - return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, readStateData: readStateData)) + return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: nil, initialData: combinedInitialData) } - case let .Scroll(index, anchorIndex, sourceIndex, scrollPosition, animated): + case let .Scroll(index, anchorIndex, sourceIndex, scrollPosition, count, animated): let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up let chatScrollPosition = ChatHistoryViewScrollPosition.index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated) var first = true - return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 140, anchorIndex: anchorIndex, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in - var cachedData: CachedPeerData? - var readStateData: ChatHistoryCombinedInitialReadStateData? - for data in view.additionalData { - switch data { - case let .cachedPeerData(peerIdValue, value): - if peerIdValue == peerId { - cachedData = value - } - case let .totalUnreadCount(totalUnreadCount): - if let readState = view.combinedReadState { - readStateData = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: totalUnreadCount) - } - default: - break - } - } + + let signal:Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> + switch mode { + case .history, .replyThread, .pinned, .preview: + signal = account.viewTracker.aroundMessageHistoryViewForLocation(chatLocation, index: index, anchorIndex: anchorIndex, count: count, fixedCombinedReadStates: fixedCombinedReadStates?(), tagMask: tagMask, orderStatistics: orderStatistics, additionalData: additionalData) + case .scheduled: + signal = account.viewTracker.scheduledMessagesViewForLocation(chatLocation) + } + + return signal |> map { view, updateType, initialData -> ChatHistoryViewUpdate in + let (cachedData, cachedDataMessages, readStateData, limitsConfiguration, autoplayMedia, autodownloadSettings) = extractAdditionalData(view: view, chatLocation: chatLocation) + let combinedInitialData = ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData, limitsConfiguration: limitsConfiguration, autoplayMedia: autoplayMedia, autodownloadSettings: autodownloadSettings) let genericType: ViewUpdateType let scrollPosition: ChatHistoryViewScrollPosition? = first ? chatScrollPosition : nil @@ -287,7 +358,162 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } else { genericType = updateType } - return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: scrollPosition, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, readStateData: readStateData)) + return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: scrollPosition, initialData: combinedInitialData) + } + } +} + +private func extractAdditionalData(view: MessageHistoryView, chatLocation: ChatLocationInput) -> ( + cachedData: CachedPeerData?, + cachedDataMessages: [MessageId: [Message]]?, + readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]?, + limitsConfiguration: LimitsConfiguration, + autoplayMedia: AutoplayMediaPreferences, + autodownloadSettings: AutomaticMediaDownloadSettings + ) { + var cachedData: CachedPeerData? + var cachedDataMessages: [MessageId: [Message]]? + var readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData] = [:] + var notificationSettings: PeerNotificationSettings? + var limitsConfiguration: LimitsConfiguration = LimitsConfiguration.defaultValue + var autoplayMedia: AutoplayMediaPreferences = AutoplayMediaPreferences.defaultSettings + var autodownloadSettings: AutomaticMediaDownloadSettings = AutomaticMediaDownloadSettings.defaultSettings + loop: for data in view.additionalData { + switch data { + case let .peerNotificationSettings(value): + notificationSettings = value + break loop + default: + break + } + } + + for data in view.additionalData { + switch data { + case let .peerNotificationSettings(value): + notificationSettings = value + case let .cachedPeerData(peerIdValue, value): + if case .peer(peerIdValue) = chatLocation { + cachedData = value + } + case let .cachedPeerDataMessages(peerIdValue, value): + if case .peer(peerIdValue) = chatLocation { + cachedDataMessages = value?.mapValues { [$0] } + } + case let .message(messageId, messages): + cachedDataMessages = [messageId : messages] + case let .preferencesEntry(key, value): + if key == PreferencesKeys.limitsConfiguration { + limitsConfiguration = value as? LimitsConfiguration ?? LimitsConfiguration.defaultValue + } + if key == ApplicationSpecificPreferencesKeys.autoplayMedia { + autoplayMedia = value as? AutoplayMediaPreferences ?? AutoplayMediaPreferences.defaultSettings + + } + + if key == ApplicationSpecificPreferencesKeys.automaticMediaDownloadSettings { + autodownloadSettings = value as? AutomaticMediaDownloadSettings ?? AutomaticMediaDownloadSettings.defaultSettings + } + case let .totalUnreadState(unreadState): + if let combinedReadStates = view.fixedReadStates { + if case let .peer(peerId) = chatLocation, case let .peer(readStates) = combinedReadStates, let readState = readStates[peerId] { + readStateData[peerId] = ChatHistoryCombinedInitialReadStateData(unreadCount: readState.count, totalUnreadCount: 0, notificationSettings: notificationSettings) + } + } + default: + break + } } + + autoplayMedia = autoplayMedia.withUpdatedAutoplayPreloadVideos(autoplayMedia.preloadVideos && autodownloadSettings.automaticDownload && (autodownloadSettings.categories.video.fileSize ?? 0) >= 5 * 1024 * 1024) + + + return (cachedData, cachedDataMessages, readStateData, limitsConfiguration, autoplayMedia, autodownloadSettings) +} + + +func preloadedChatHistoryViewForLocation(_ location: ChatHistoryLocation, context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic, tagMask: MessageTags?, additionalData: [AdditionalMessageHistoryViewData]) -> Signal { + return (chatHistoryViewForLocation(location, context: context, chatLocation: chatLocation, fixedCombinedReadStates: nil, tagMask: tagMask, additionalData: additionalData, chatLocationContextHolder: chatLocationContextHolder) + |> castError(Bool.self) + |> mapToSignal { update -> Signal in + switch update { + case let .Loading(value): + if case .Generic(.FillHole) = value.type { + return .fail(true) + } + case let .HistoryView(value): + if case .Generic(.FillHole) = value.type { + return .fail(true) + } + } + return .single(update) + }) + |> restartIfError +} + + + + +struct ReplyThreadInfo { + var message: ChatReplyThreadMessage + var isChannelPost: Bool + var isEmpty: Bool + var contextHolder: Atomic +} + +enum ReplyThreadSubject { + case channelPost(MessageId) + case groupMessage(MessageId) +} + + +func fetchAndPreloadReplyThreadInfo(context: AccountContext, subject: ReplyThreadSubject, atMessageId: MessageId? = nil) -> Signal { + let message: Signal + switch subject { + case let .channelPost(messageId): + message = context.engine.messages.fetchChannelReplyThreadMessage(messageId: messageId, atMessageId: atMessageId) + case let .groupMessage(messageId): + message = context.engine.messages.fetchChannelReplyThreadMessage(messageId: messageId, atMessageId: atMessageId) + } + + return message + |> mapToSignal { replyThreadMessage -> Signal in + let chatLocationContextHolder = Atomic(value: nil) + + let preloadSignal = preloadedChatHistoryViewForLocation( + .Initial(count: 60), + context: context, + chatLocation: .replyThread(replyThreadMessage), + chatLocationContextHolder: chatLocationContextHolder, + tagMask: nil, + additionalData: [] + ) + return preloadSignal + |> map { historyView -> Bool? in + switch historyView { + case .Loading: + return nil + case let .HistoryView(values): + return values.view.entries.isEmpty + } + } + |> mapToSignal { value -> Signal in + if let value = value { + return .single(value) + } else { + return .complete() + } + } + |> take(1) + |> map { isEmpty -> ReplyThreadInfo in + return ReplyThreadInfo( + message: replyThreadMessage, + isChannelPost: replyThreadMessage.isChannelPost, + isEmpty: isEmpty, + contextHolder: chatLocationContextHolder + ) + } + |> castError(FetchChannelReplyThreadMessageError.self) } } + diff --git a/Telegram-Mac/ChatHoleRowItem.swift b/Telegram-Mac/ChatHoleRowItem.swift index 5b0962f940..debd72df4d 100644 --- a/Telegram-Mac/ChatHoleRowItem.swift +++ b/Telegram-Mac/ChatHoleRowItem.swift @@ -8,23 +8,26 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac +import Postbox +import TelegramCore + class ChatHoleRowItem: ChatRowItem { - + override var canBeAnchor: Bool { + return false + } override var height: CGFloat { - return 20 + return 0 } override open var animatable:Bool { return false } - override init(_ initialSize:NSSize, _ chatInteraction:ChatInteraction, _ account:Account, _ entry:ChatHistoryEntry) { - super.init(initialSize, chatInteraction, entry) + override init(_ initialSize:NSSize, _ chatInteraction:ChatInteraction, _ context: AccountContext, _ entry:ChatHistoryEntry, _ downloadSettings: AutomaticMediaDownloadSettings, theme: TelegramPresentationTheme) { + super.init(initialSize, chatInteraction, entry, downloadSettings, theme: theme) } @@ -32,3 +35,27 @@ class ChatHoleRowItem: ChatRowItem { return ChatHoleRowView.self } } + + +class ChatHoleRowView: TableRowView { + + // private let progress: ProgressIndicator = ProgressIndicator() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + // addSubview(progress) + } + + override var backdorColor: NSColor { + return .clear + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + // progress.center() + } + +} diff --git a/Telegram-Mac/ChatHoleRowView.swift b/Telegram-Mac/ChatHoleRowView.swift deleted file mode 100644 index 18a2712cb1..0000000000 --- a/Telegram-Mac/ChatHoleRowView.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// ChatHoleRowView.swift -// Telegram-Mac -// -// Created by keepcoder on 13/09/16. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import TGUIKit -class ChatHoleRowView: TableRowView { - - private let progress: ProgressIndicator = ProgressIndicator() - required init(frame frameRect: NSRect) { - super.init(frame: frameRect) - addSubview(progress) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func layout() { - super.layout() - progress.center() - } - -} diff --git a/Telegram-Mac/ChatInfoTouchbar.swift b/Telegram-Mac/ChatInfoTouchbar.swift new file mode 100644 index 0000000000..e87c095eda --- /dev/null +++ b/Telegram-Mac/ChatInfoTouchbar.swift @@ -0,0 +1,156 @@ +// +// ChatInfoTouchbar.swift +// Telegram +// +// Created by Mikhail Filimonov on 18/09/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import SwiftSignalKit +import TGUIKit + +@available(OSX 10.12.2, *) +fileprivate extension NSTouchBarItem.Identifier { + static let edit = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.chat-info.edit") + static let share = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.chat-info.share") + + static let sharedMediaAndInfo = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.chat-info.sharedMediaAndInfo") + static let userActions = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.chat-info.userActions") + +} + +@available(OSX 10.12.2, *) +class ChatInfoTouchbar: NSTouchBar, NSTouchBarDelegate { + private let chatInteraction: ChatInteraction + private let dismiss:()->Void + init(chatInteraction: ChatInteraction, dismiss: @escaping()->Void) { + self.chatInteraction = chatInteraction + self.dismiss = dismiss + super.init() + self.delegate = self + guard let peer = chatInteraction.peer else {return} + var items: [NSTouchBarItem.Identifier] = [] + items.append(.edit) + if peer.isBot || (peer.isUser && (peer as! TelegramUser).phone != nil) || peer.addressName != nil { + items.append(.share) + } + items.append(.flexibleSpace) + items.append(.sharedMediaAndInfo) + if peer.isUser && peer.id != chatInteraction.context.peerId, !peer.isBot { + items.append(.userActions) + } + items.append(.flexibleSpace) + self.defaultItemIdentifiers = items + self.customizationAllowedItemIdentifiers = self.defaultItemIdentifiers + self.customizationIdentifier = .popoverBar + } + + @objc private func userInfoActions(_ sender: Any?) { + guard let segment = sender as? NSSegmentedControl else {return} + switch segment.selectedSegment { + case 0: + + _ = showModalProgress(signal: chatInteraction.context.engine.peers.createSecretChat(peerId: chatInteraction.peerId) |> deliverOnMainQueue, for: chatInteraction.context.window).start(next: { [weak self] peerId in + if let strongSelf = self { + strongSelf.chatInteraction.push(ChatController(context: strongSelf.chatInteraction.context, chatLocation: .peer(peerId))) + } + }) + case 1: + let context = chatInteraction.context + _ = (phoneCall(account: context.account, sharedContext: context.sharedContext, peerId: chatInteraction.peerId) |> deliverOnMainQueue).start(next: { result in + applyUIPCallResult(context.sharedContext, result) + }) + default: + break + } + dismiss() + } + @objc private func editChat() { + chatInteraction.update({$0.selectionState == nil ? $0.withSelectionState() : $0.withoutSelectionState()}) + dismiss() + } + @objc private func shareAction() { + guard let peer = chatInteraction.peer else {return} + + if peer.isUser, let peer = peer as? TelegramUser { + showModal(with: ShareModalController(ShareContactObject(chatInteraction.context, user: peer)), for: mainWindow) + } else if let address = peer.addressName { + showModal(with: ShareModalController(ShareLinkObject(chatInteraction.context, link: "https://t.me/\(address)")), for: mainWindow) + } + + dismiss() + } + @objc private func sharedMediaAction() { + chatInteraction.push(PeerMediaController(context: chatInteraction.context, peerId: chatInteraction.peerId)) + dismiss() + } + @objc private func peerInfoActions(_ sender: Any?) { + guard let segment = sender as? NSSegmentedControl else {return} + switch segment.selectedSegment { + case 0: + chatInteraction.push(PeerMediaController(context: chatInteraction.context, peerId: chatInteraction.peerId)) + case 1: + chatInteraction.push(PeerInfoController(context: chatInteraction.context, peerId: chatInteraction.peerId)) + default: + break + } + dismiss() + } + + func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? { + switch identifier { + case .edit: + let item = NSCustomTouchBarItem(identifier: identifier) + let button = NSButton(title: chatInteraction.presentation.selectionState != nil ? L10n.navigationCancel : L10n.navigationEdit, target: self, action: #selector(editChat)) + item.view = button + item.customizationLabel = button.title + return item + case .share: + let item = NSCustomTouchBarItem(identifier: identifier) + let button = NSButton(image: NSImage(named: NSImage.Name("Icon_TouchBar_Share"))!, target: self, action: #selector(shareAction)) + item.view = button + item.customizationLabel = button.title + return item + case .sharedMediaAndInfo: + let item = NSCustomTouchBarItem(identifier: identifier) + let segment = NSSegmentedControl() + segment.segmentStyle = .separated + segment.segmentCount = 1 + segment.setImage(NSImage(named: NSImage.Name("Icon_TouchBar_AttachPhotoOrVideo"))!, forSegment: 0) + segment.setLabel(L10n.telegramPeerMediaController, forSegment: 0) + segment.trackingMode = .momentary + segment.target = self + segment.action = #selector(peerInfoActions(_:)) + item.view = segment + return item + case .userActions: + let item = NSCustomTouchBarItem(identifier: identifier) + guard let peer = chatInteraction.peer as? TelegramUser else {return nil} + let segment = NSSegmentedControl() + segment.segmentStyle = .separated + segment.segmentCount = peer.canCall ? 2 : 1 + segment.setImage(NSImage(named: NSImage.Name("Icon_TouchBar_ComposeSecretChat"))!, forSegment: 0) + segment.setImage(NSImage(named: NSImage.Name("Icon_TouchBar_Call"))!, forSegment: 1) + segment.setLabel(L10n.touchBarStartSecretChat, forSegment: 0) + segment.setLabel(L10n.touchBarCall, forSegment: 1) + segment.trackingMode = .momentary + segment.target = self + segment.action = #selector(userInfoActions(_:)) + item.view = segment + return item + default: + break + } + return nil + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + +} diff --git a/Telegram-Mac/ChatInputAccessory.swift b/Telegram-Mac/ChatInputAccessory.swift index 7d92a748df..12198ab1d2 100644 --- a/Telegram-Mac/ChatInputAccessory.swift +++ b/Telegram-Mac/ChatInputAccessory.swift @@ -7,10 +7,11 @@ // import Cocoa -import PostboxMac -import TelegramCoreMac +import Postbox +import TelegramCore + import TGUIKit -import SwiftSignalKitMac +import SwiftSignalKit class ChatInputAccessory: Node { @@ -20,9 +21,10 @@ class ChatInputAccessory: Node { private var displayNode:ChatAccessoryModel? private let dismiss:ImageButton = ImageButton() + private let iconView = ImageView() + private var progress: Control? let container:ChatAccessoryView = ChatAccessoryView() - var dismissForward:(()->Void)! var dismissReply:(()->Void)! var dismissEdit:(()->Void)! @@ -35,10 +37,10 @@ class ChatInputAccessory: Node { self?.chatInteraction.update({$0.updatedInterfaceState({$0.withoutForwardMessages()})}) } dismissReply = { [weak self] in - self?.chatInteraction.update({$0.updatedInterfaceState({$0.withUpdatedReplyMessageId(nil)})}) + self?.chatInteraction.update({$0.updatedInterfaceState({$0.withUpdatedReplyMessageId(nil).withUpdatedDismissedForceReplyId($0.replyMessageId)})}) } dismissEdit = { [weak self] in - self?.chatInteraction.update({$0.withoutEditMessage()}) + self?.chatInteraction.cancelEditing() } dismissUrlPreview = { [weak self] in self?.chatInteraction.update({ state -> ChatPresentationInterfaceState in @@ -47,10 +49,10 @@ class ChatInputAccessory: Node { } dismiss.set(image: theme.icons.dismissAccessory, for: .Normal) - dismiss.sizeToFit() + _ = dismiss.sizeToFit() + view?.addSubview(iconView) view?.addSubview(dismiss) - self.view = view } @@ -65,29 +67,94 @@ class ChatInputAccessory: Node { func update(with state:ChatPresentationInterfaceState, account:Account, animated:Bool) -> Void { + dismiss.isHidden = false + progress?.isHidden = true + iconView.isHidden = false + displayNode = nil dismiss.removeAllHandlers() + container.removeAllHandlers() + container.removeAllStateHandlers() + if let urlPreview = state.urlPreview, state.interfaceState.composeDisableUrlPreview != urlPreview.0, let peer = state.peer, !peer.webUrlRestricted { + iconView.image = theme.icons.chat_action_url_preview displayNode = ChatUrlPreviewModel(account: account, webpage: urlPreview.1, url:urlPreview.0) dismiss.set(handler: { [weak self ] _ in self?.dismissUrlPreview() - }, for: .Click) - - } else if let editState = state.editState { - displayNode = EditMessageModel(message:editState.message, account:account) + }, for: .Click) + } else if let editState = state.interfaceState.editState { + displayNode = EditMessageModel(state: editState, account:account) + iconView.image = theme.icons.chat_action_edit_message + iconView.isHidden = editState.loadingState != .none + progress?.isHidden = editState.loadingState == .none + updateProgress(editState.loadingState) dismiss.set(handler: { [weak self] _ in self?.dismissEdit() }, for: .Click) - } else if !state.interfaceState.forwardMessageIds.isEmpty { - displayNode = ForwardPanelModel(forwardIds:state.interfaceState.forwardMessageIds,account:account) + progress?.set(handler: { [weak self] _ in + self?.dismiss.send(event: .Click) + }, for: .Click) + + } else if !state.interfaceState.forwardMessages.isEmpty && !state.interfaceState.forwardMessageIds.isEmpty { + displayNode = ForwardPanelModel(forwardMessages:state.interfaceState.forwardMessages, hideNames: state.interfaceState.hideSendersName, account:account) + + iconView.image = theme.icons.chat_action_forward_message + + let anotherAction = { [weak self] in + guard let context = self?.chatInteraction.context else { + return + } + let fwdMessages = state.interfaceState.forwardMessageIds + showModal(with: ShareModalController(ForwardMessagesObject(context, messageIds: fwdMessages, emptyPerformOnClose: true)), for: context.window) + delay(0.15, closure: { + self?.chatInteraction.update({$0.updatedInterfaceState({$0.withoutForwardMessages()})}) + }) + } + let setHideAction = { [weak self] hide in + self?.chatInteraction.update { + $0.updatedInterfaceState { + $0.withUpdatedHideSendersName(hide) + } + } + } + + var items:[SPopoverItem] = [] + + let authors = state.interfaceState.forwardMessages.compactMap { $0.author?.id }.uniqueElements.count + + + items.append(SPopoverItem(L10n.chatAlertForwardActionShow1Countable(authors), { + setHideAction(false) + }, !state.interfaceState.hideSendersName ? theme.icons.chat_action_menu_selected : nil)) + + items.append(SPopoverItem(L10n.chatAlertForwardActionHide1Countable(authors), { + setHideAction(true) + }, state.interfaceState.hideSendersName ? theme.icons.chat_action_menu_selected : nil)) + + items.append(SPopoverItem(true)) + + items.append(SPopoverItem(L10n.chatAlertForwardActionAnother, anotherAction, theme.icons.chat_action_menu_update_chat)) + + + container.set(handler: { control in + showPopover(for: control, with: SPopoverViewController(items: items), inset: NSMakePoint(-5, 3)) + }, for: .Hover) + dismiss.set(handler: { [weak self] _ in self?.dismissForward() }, for: .Click) + + } else if let replyMessageId = state.interfaceState.replyMessageId { - displayNode = ReplyModel(replyMessageId: replyMessageId, account:account) + displayNode = ReplyModel(replyMessageId: replyMessageId, context: chatInteraction.context, replyMessage: state.interfaceState.replyMessage) + iconView.image = theme.icons.chat_action_reply_message dismiss.set(handler: { [weak self ] _ in self?.dismissReply() }, for: .Click) + + container.set(handler: { [weak self] _ in + self?.chatInteraction.focusMessageId(nil, replyMessageId, .CenterEmpty) + }, for: .Click) } if let displayNode = displayNode { @@ -95,10 +162,40 @@ class ChatInputAccessory: Node { } else { nodeReady.set(.single(animated)) } + iconView.sizeToFit() container.removeAllSubviews() displayNode?.view = container } + private func updateProgress(_ loadingState: EditStateLoading) { + switch loadingState { + case .none: + progress?.removeFromSuperview() + progress = nil + case .loading: + + let indicator:ProgressIndicator + if let _indicator = progress as? ProgressIndicator { + indicator = _indicator + } else { + indicator = ProgressIndicator(frame: NSMakeRect(0, 0, 20, 20)) + progress = indicator + view?.addSubview(indicator) + } + indicator.progressColor = theme.colors.text + case let .progress(progress): + let radial: RadialProgressView + if let _radial = self.progress as? RadialProgressView { + radial = _radial + } else { + radial = RadialProgressView(theme: RadialProgressTheme(backgroundColor: .clear, foregroundColor: theme.colors.accent), twist: true, size: NSMakeSize(20, 20)) + self.progress = radial + view?.addSubview(radial) + } + radial.state = .ImpossibleFetching(progress: progress, force: false) + } + } + override var frame: NSRect { get { @@ -107,8 +204,10 @@ class ChatInputAccessory: Node { } set { super.frame = newValue - self.container.frame = NSMakeRect(49, 0, newValue.width, size.height) - dismiss.centerY(x: 0) + self.container.frame = NSMakeRect(49, 0, measuredWidth, size.height) + iconView.centerY(x: 2) + dismiss.centerY(x: newValue.width - dismiss.frame.width) + progress?.centerY(x: 5) displayNode?.setNeedDisplay() } } @@ -122,7 +221,7 @@ class ChatInputAccessory: Node { if let view = newValue { if container.superview != newValue { container.removeFromSuperview() - view.addSubview(container) + view.addSubview(container, positioned: .below, relativeTo: dismiss) } container.frame = view.bounds container.setNeedsDisplay() @@ -134,6 +233,9 @@ class ChatInputAccessory: Node { } } + deinit { + } + override func setNeedDisplay() { super.setNeedDisplay() displayNode?.setNeedDisplay() diff --git a/Telegram-Mac/ChatInputActionsView.swift b/Telegram-Mac/ChatInputActionsView.swift index 7364d3b20c..bc380db19f 100644 --- a/Telegram-Mac/ChatInputActionsView.swift +++ b/Telegram-Mac/ChatInputActionsView.swift @@ -8,8 +8,9 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac +import TelegramCore + +import SwiftSignalKit // @@ -22,66 +23,61 @@ class ChatInputActionsView: View, Notifable { private let voice:ImageButton = ImageButton() private let muteChannelMessages:ImageButton = ImageButton() private let entertaiments:ImageButton = ImageButton() + private let slowModeTimeout:TitleButton = TitleButton() private let inlineCancel:ImageButton = ImageButton() private let keyboard:ImageButton = ImageButton() + private var scheduled:ImageButton? + private var secretTimer:ImageButton? + private var inlineProgress: ProgressIndicator? = nil + + private var prevView: View init(frame frameRect: NSRect, chatInteraction:ChatInteraction) { self.chatInteraction = chatInteraction - + self.prevView = self.send super.init(frame: frameRect) + keyboard.autohighlight = false + addSubview(keyboard) addSubview(send) addSubview(voice) addSubview(inlineCancel) addSubview(muteChannelMessages) + addSubview(slowModeTimeout) inlineCancel.isHidden = true send.isHidden = true voice.isHidden = true muteChannelMessages.isHidden = true - + slowModeTimeout.isHidden = true voice.autohighlight = false muteChannelMessages.autohighlight = false + send.autohighlight = false voice.set(handler: { [weak self] _ in + guard let `self` = self else { return } + FastSettings.toggleRecordingState() - self?.voice.set(image: FastSettings.recordingState == .voice ? theme.icons.chatRecordVoice : theme.icons.chatRecordVideo, for: .Normal) + + self.voice.set(image: FastSettings.recordingState == .voice ? theme.icons.chatRecordVoice : theme.icons.chatRecordVideo, for: .Normal) + + getAppTooltip(for: FastSettings.recordingState == .voice ? .voiceRecording : .videoRecording, callback: { value in + tooltip(for: self.voice, text: value) + }) + }, for: .Click) - voice.set(handler: { [weak self] _ in - if let peer = self?.chatInteraction.presentation.peer, peer.mediaRestricted { - alertForMediaRestriction(peer) - } - }, for: .Up) - voice.set(handler: { [weak self] _ in - self?.stop() - }, for: .Up) - - - - voice.set(handler: { [weak self] _ in - if let strongSelf = self, let peer = strongSelf.chatInteraction.presentation.peer { - if peer.mediaRestricted { - return alertForMediaRestriction(peer) - } - if strongSelf.chatInteraction.presentation.effectiveInput.inputText.isEmpty { - strongSelf.start() - } - } + voice.set(handler: { [weak self] control in + self?.chatInteraction.startRecording(false, control) }, for: .LongMouseDown) - - voice.set(handler: { [weak self] _ in - if let peer = self?.chatInteraction.presentation.peer, peer.mediaRestricted { - alertForMediaRestriction(peer) - } - }, for: .Up) + muteChannelMessages.set(handler: { [weak self] _ in if let chatInteraction = self?.chatInteraction { FastSettings.toggleChannelMessagesMuted(chatInteraction.peerId) - self?.updateLocalizationAndTheme() + (self?.superview?.superview as? View)?.updateLocalizationAndTheme(theme: theme) } }, for: .Click) @@ -106,61 +102,96 @@ class ChatInputActionsView: View, Notifable { addHoverObserver() addClickObserver() entertaiments.canHighlight = false + muteChannelMessages.hideAnimated = false - updateLocalizationAndTheme() + updateLocalizationAndTheme(theme: theme) } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - send.set(image: theme.icons.chatSendMessage, for: .Normal) - send.sizeToFit() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) + send.set(image: self.chatInteraction.presentation.state == .editing ? theme.icons.chatSaveEditedMessage : theme.icons.chatSendMessage, for: .Normal) + _ = send.sizeToFit() voice.set(image: FastSettings.recordingState == .voice ? theme.icons.chatRecordVoice : theme.icons.chatRecordVideo, for: .Normal) - voice.sizeToFit() + _ = voice.sizeToFit() let muted = FastSettings.isChannelMessagesMuted(chatInteraction.peerId) muteChannelMessages.set(image: !muted ? theme.icons.inputChannelMute : theme.icons.inputChannelUnmute, for: .Normal) - muteChannelMessages.sizeToFit() + _ = muteChannelMessages.sizeToFit() + + updateEntertainmentIcon() keyboard.set(image: theme.icons.chatActiveReplyMarkup, for: .Normal) - keyboard.sizeToFit() + _ = keyboard.sizeToFit() inlineCancel.set(image: theme.icons.chatInlineDismiss, for: .Normal) - inlineCancel.sizeToFit() - entertaiments.set(image: chatInteraction.presentation.isEmojiSection ? theme.icons.chatEntertainment : theme.icons.chatEntertainmentSticker, for: .Normal) - entertaiments.sizeToFit() - secretTimer?.set(image: theme.icons.chatSecretTimer, for: .Normal) + _ = inlineCancel.sizeToFit() + + + if let timeout = chatInteraction.presentation.messageSecretTimeout?.timeout?.effectiveValue { + secretTimer?.set(image: theme.chat.messageSecretTimer(shortTimeIntervalString(value: timeout)), for: .Normal) + } else { + secretTimer?.set(image: theme.icons.chatSecretTimer, for: .Normal) + } + + + scheduled?.set(image: theme.icons.scheduledInputAction, for: .Normal) + + + slowModeTimeout.set(font: .normal(.text), for: .Normal) + slowModeTimeout.set(color: theme.colors.grayIcon, for: .Normal) + _ = self.slowModeTimeout.sizeToFit(NSZeroSize, NSMakeSize(38, 30), thatFit: true) } + private func updateEntertainmentIcon() { + entertaiments.set(image: chatInteraction.presentation.isEmojiSection || chatInteraction.presentation.state == .editing ? theme.icons.chatEntertainment : theme.icons.chatEntertainmentSticker, for: .Normal) + entertaiments.setFrameSize(60, 40) + } + + var entertaimentsPopover: ViewController { + if chatInteraction.presentation.state == .editing { + let emoji = EmojiViewController(chatInteraction.context) + if let interactions = chatInteraction.context.sharedContext.bindings.entertainment().interactions { + emoji.update(with: interactions) + } + return emoji + } + return chatInteraction.context.sharedContext.bindings.entertainment() + } + private func addHoverObserver() { entertaiments.set(handler: { [weak self] (state) in - if let strongSelf = self { - let chatInteraction = strongSelf.chatInteraction - var enabled = false - - if let sidebarEnabled = chatInteraction.presentation.sidebarEnabled { - enabled = sidebarEnabled - } - if !((mainWindow.frame.width >= 1100 && chatInteraction.account.context.layout == .dual) || (mainWindow.frame.width >= 880 && chatInteraction.account.context.layout == .minimisize)) || !enabled { - if !hasPopover(mainWindow) { - let rect = NSMakeRect(0, 0, 350, 350) - chatInteraction.account.context.entertainment._frameRect = rect - chatInteraction.account.context.entertainment.view.frame = rect - showPopover(for: strongSelf.entertaiments, with: chatInteraction.account.context.entertainment, edge: .maxX, inset:NSMakePoint(strongSelf.frame.width - strongSelf.entertaiments.frame.maxX + 15, 10), delayBeforeShown: 0.0) - } - - } + guard let `self` = self else {return} + let chatInteraction = self.chatInteraction + var enabled = false + + if let sidebarEnabled = chatInteraction.presentation.sidebarEnabled { + enabled = sidebarEnabled + } + let window = chatInteraction.context.window + let sharedContext = chatInteraction.context.sharedContext + if !((window.frame.width >= 1030 && sharedContext.layout == .dual) || (window.frame.width >= 880 && sharedContext.layout == .minimisize)) || !enabled { + self.showEntertainment() } }, for: .Hover) } + private func showEntertainment() { + let rect = NSMakeRect(0, 0, 350, min(max(chatInteraction.context.window.frame.height - 250, 300), 550)) + entertaimentsPopover._frameRect = rect + entertaimentsPopover.view.frame = rect + showPopover(for: entertaiments, with: entertaimentsPopover, edge: .maxX, inset:NSMakePoint(frame.width - entertaiments.frame.maxX + 38, 10), delayBeforeShown: 0.0) + } + private func addClickObserver() { entertaiments.set(handler: { [weak self] (state) in if let strongSelf = self { let chatInteraction = strongSelf.chatInteraction + let window = chatInteraction.context.window if let sidebarEnabled = chatInteraction.presentation.sidebarEnabled, sidebarEnabled { - if mainWindow.frame.width >= 1100 && chatInteraction.account.context.layout == .dual || mainWindow.frame.width >= 880 && chatInteraction.account.context.layout == .minimisize { + if window.frame.width >= 1030 && chatInteraction.context.sharedContext.layout == .dual || mainWindow.frame.width >= 880 && chatInteraction.context.sharedContext.layout == .minimisize { chatInteraction.toggleSidebar() } @@ -168,7 +199,9 @@ class ChatInputActionsView: View, Notifable { } }, for: .Click) } - + override func setFrameOrigin(_ newOrigin: NSPoint) { + super.setFrameOrigin(newOrigin) + } func toggleKeyboard() { let keyboardId = chatInteraction.presentation.keyboardButtonsMessage?.id @@ -180,14 +213,44 @@ class ChatInputActionsView: View, Notifable { override func layout() { super.layout() - inlineCancel.centerY(x:frame.width - inlineCancel.frame.width - iconsInset) + + + + inlineCancel.centerY(x:frame.width - inlineCancel.frame.width - iconsInset - 6) + inlineProgress?.centerY(x: frame.width - inlineCancel.frame.width - iconsInset - 10) voice.centerY(x:frame.width - voice.frame.width - iconsInset) send.centerY(x: frame.width - send.frame.width - iconsInset) - entertaiments.centerY(x: voice.frame.minX - entertaiments.frame.width - iconsInset) - secretTimer?.centerY(x: entertaiments.frame.minX - keyboard.frame.width - iconsInset) - keyboard.centerY(x: entertaiments.frame.minX - keyboard.frame.width - iconsInset) - muteChannelMessages.centerY(x: entertaiments.frame.minX - muteChannelMessages.frame.width - iconsInset) + slowModeTimeout.centerY(x: frame.width - slowModeTimeout.frame.width - iconsInset) + entertaiments.centerY(x: voice.frame.minX - entertaiments.frame.width - 0) + keyboard.centerY(x: entertaiments.frame.minX - keyboard.frame.width) + muteChannelMessages.centerY(x: entertaiments.frame.minX - muteChannelMessages.frame.width) + if let scheduled = scheduled { + if muteChannelMessages.isHidden { + scheduled.centerY(x: (keyboard.isHidden ? entertaiments.frame.minX : keyboard.frame.minX) - scheduled.frame.width) + } else { + scheduled.centerY(x: muteChannelMessages.frame.minX - scheduled.frame.width - iconsInset) + } + } + + let views = [inlineCancel, + inlineProgress, + voice, + send, + slowModeTimeout, + entertaiments, + keyboard, + muteChannelMessages, + scheduled].filter { $0 != nil && !$0!.isHidden }.map { $0! } + + let minView = views.min(by: { $0.frame.minX < $1.frame.minX }) + if let minView = minView, let secretTimer = secretTimer { + if minView == entertaiments { + secretTimer.centerY(x: minView.frame.minX - secretTimer.frame.width) + } else { + secretTimer.centerY(x: minView.frame.minX - secretTimer.frame.width - iconsInset) + } + } } func stop() { @@ -215,48 +278,98 @@ class ChatInputActionsView: View, Notifable { return false } - func start() { - let state: ChatRecordingState - - switch FastSettings.recordingState { - case .voice: - state = ChatRecordingAudioState() - state.start() - case .video: - state = ChatRecordingVideoState() - showModal(with: VideoRecorderModalController(chatInteraction: chatInteraction, pipeline: (state as! ChatRecordingVideoState).pipeline), for: mainWindow) + var currentActionView: NSView { + if !self.send.isHidden { + return self.send + } else if !self.voice.isHidden { + return self.voice + } else if !self.slowModeTimeout.isHidden { + return self.slowModeTimeout + } else { + return self } - - chatInteraction.update({$0.withRecordingState(state)}) } + private var first:Bool = true func notify(with value: Any, oldValue: Any, animated:Bool) { if let value = value as? ChatPresentationInterfaceState, let oldValue = oldValue as? ChatPresentationInterfaceState { - if value.interfaceState != oldValue.interfaceState || value.editState != oldValue.editState || !animated || value.inputQueryResult != oldValue.inputQueryResult || value.inputContext != oldValue.inputContext || value.sidebarEnabled != oldValue.sidebarEnabled || value.sidebarShown != oldValue.sidebarShown || value.layout != oldValue.layout { + if value.interfaceState != oldValue.interfaceState || !animated || value.inputQueryResult != oldValue.inputQueryResult || value.inputContext != oldValue.inputContext || value.sidebarEnabled != oldValue.sidebarEnabled || value.sidebarShown != oldValue.sidebarShown || value.layout != oldValue.layout || value.isKeyboardActive != oldValue.isKeyboardActive || value.isKeyboardShown != oldValue.isKeyboardShown || value.slowMode != oldValue.slowMode || value.hasScheduled != oldValue.hasScheduled || value.messageSecretTimeout != oldValue.messageSecretTimeout { - var size:NSSize = NSMakeSize(send.frame.width + iconsInset + entertaiments.frame.width + iconsInset * 2, frame.height) + var size:NSSize = NSMakeSize(send.frame.width + iconsInset + entertaiments.frame.width, frame.height) - if chatInteraction.peerId.namespace == Namespaces.Peer.SecretChat { + if chatInteraction.hasSetDestructiveTimer { size.width += theme.icons.chatSecretTimer.backingSize.width + iconsInset } + + if chatInteraction.hasSetDestructiveTimer { + if secretTimer == nil { + secretTimer = ImageButton() + secretTimer?.set(image: theme.icons.chatSecretTimer, for: .Normal) + _ = secretTimer?.sizeToFit() + addSubview(secretTimer!) + + secretTimer?.set(handler: { [weak self] control in + if let strongSelf = self { + if let peer = strongSelf.chatInteraction.peer { + if peer.isSecretChat { + showPopover(for: control, with: SPopoverViewController(items:strongSelf.secretTimerItems(), visibility: 6), edge: .maxX, inset:NSMakePoint(120, 10)) + } else if let control = strongSelf.secretTimer { + strongSelf.chatInteraction.showDeleterSetup(control) + } + } + } + }, for: .Click) + } + } else { + secretTimer?.removeFromSuperview() + secretTimer = nil + } + + + send.animates = false + send.set(image: value.state == .editing ? theme.icons.chatSaveEditedMessage : theme.icons.chatSendMessage, for: .Normal) + send.animates = true + + if let timeout = value.messageSecretTimeout?.timeout?.effectiveValue { + secretTimer?.set(image: theme.chat.messageSecretTimer(shortTimeIntervalString(value: timeout)), for: .Normal) + } else { + secretTimer?.set(image: theme.icons.chatSecretTimer, for: .Normal) + } if let peer = value.peer { - muteChannelMessages.isHidden = !peer.isChannel || !peer.canSendMessage + muteChannelMessages.isHidden = !peer.isChannel || !peer.canSendMessage(value.chatMode.isThreadMode) || !value.effectiveInput.inputText.isEmpty || value.interfaceState.editState != nil } if !muteChannelMessages.isHidden { - size.width += muteChannelMessages.frame.width + iconsInset + size.width += muteChannelMessages.frame.width } var newInlineRequest = value.inputQueryResult != oldValue.inputQueryResult var oldInlineRequest = newInlineRequest + var newInlineLoading: Bool = false + var oldInlineLoading: Bool = false + + if let query = value.inputQueryResult, case .contextRequestResult(_, let data) = query { + newInlineLoading = data == nil && !value.effectiveInput.inputText.isEmpty + } + + if let query = value.inputQueryResult, case .contextRequestResult = query, newInlineRequest || first { newInlineRequest = true } else { newInlineRequest = false } + + + if let query = oldValue.inputQueryResult, case .contextRequestResult(_, let data) = query { + oldInlineLoading = data == nil + } + + let newSlowModeCounter: Bool = value.slowMode?.timeout != nil && value.interfaceState.editState == nil && !newInlineLoading && !newInlineRequest + let oldSlowModeCounter: Bool = oldValue.slowMode?.timeout != nil && oldValue.interfaceState.editState == nil && !oldInlineLoading && !oldInlineRequest + if let query = oldValue.inputQueryResult, case .contextRequestResult = query, oldInlineRequest || first { oldInlineRequest = true @@ -264,27 +377,32 @@ class ChatInputActionsView: View, Notifable { oldInlineRequest = false } +// newInlineLoading = newInlineLoading && newInlineRequest +// oldInlineLoading = oldInlineLoading && oldInlineRequest + + let sNew = !value.effectiveInput.inputText.isEmpty || !value.interfaceState.forwardMessageIds.isEmpty || value.state == .editing let sOld = !oldValue.effectiveInput.inputText.isEmpty || !oldValue.interfaceState.forwardMessageIds.isEmpty || oldValue.state == .editing - let anim = animated && (sNew != sOld || newInlineRequest != oldInlineRequest) - if sNew != sOld || first || newInlineRequest != oldInlineRequest { + if sNew != sOld || first || newInlineRequest != oldInlineRequest || oldInlineLoading != newInlineLoading || newSlowModeCounter != oldSlowModeCounter { first = false - let prevView:View + let prevView:View = self.prevView let newView:View - if newInlineRequest { - prevView = !sOld ? voice : send + if newSlowModeCounter { + newView = slowModeTimeout + } else if newInlineRequest { newView = inlineCancel } else if oldInlineRequest { - prevView = inlineCancel newView = sNew ? send : voice } else { - prevView = sNew ? voice : send newView = sNew ? send : voice } + self.prevView = newView + + let anim = animated && prevView != newView newView.isHidden = false newView.layer?.opacity = 1.0 @@ -297,13 +415,46 @@ class ChatInputActionsView: View, Notifable { prevView.isHidden = true } }) - } else { + } else if prevView != newView { prevView.isHidden = true + } else { + prevView.isHidden = false + prevView.layer?.opacity = 1.0 } } + inlineCancel.isHidden = inlineCancel.isHidden || newInlineLoading + + if newInlineLoading { + if inlineProgress == nil { + inlineProgress = ProgressIndicator(frame: NSMakeRect(0, 0, 22, 22)) + inlineProgress?.progressColor = theme.colors.grayIcon + addSubview(inlineProgress!, positioned: .below, relativeTo: inlineCancel) + inlineProgress?.set(handler: { [weak self] _ in + if let inputContext = self?.chatInteraction.presentation.inputContext, case let .contextRequest(request) = inputContext { + if request.query.isEmpty { + self?.chatInteraction.clearInput() + } else { + self?.chatInteraction.clearContextQuery() + } + } + }, for: .Click) + } + } else { + if let inlineProgress = inlineProgress { + self.inlineProgress = nil + if animated { + inlineProgress.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak inlineProgress] _ in + inlineProgress?.removeFromSuperview() + }) + } else { + inlineProgress.removeFromSuperview() + } + } + } + entertaiments.apply(state: .Normal) - entertaiments.isSelected = value.isShowSidebar || (chatInteraction.account.context.entertainment.popover?.isShown ?? false) + entertaiments.isSelected = value.isShowSidebar keyboard.isHidden = !value.isKeyboardActive @@ -318,13 +469,35 @@ class ChatInputActionsView: View, Notifable { } } - self.change(size: size, animated: false) + if let slowMode = value.slowMode, let timeout = slowMode.timeout, timeout >= 0 { + let minutes = timeout / 60 + let seconds = timeout % 60 + let string = String(format: "%@:%@", minutes < 10 ? "0\(minutes)" : "\(minutes)", seconds < 10 ? "0\(seconds)" : "\(seconds)") + self.slowModeTimeout.set(text: string, for: .Normal) + } - + if value.hasScheduled && value.effectiveInput.inputText.isEmpty && value.interfaceState.editState == nil { + if scheduled == nil { + scheduled = ImageButton() + addSubview(scheduled!) + } + scheduled?.removeAllHandlers() + scheduled?.set(handler: { [weak self] _ in + self?.chatInteraction.openScheduledMessages() + }, for: .Click) + scheduled!.set(image: theme.icons.chatInputScheduled, for: .Normal) + _ = scheduled!.sizeToFit() + size.width += scheduled!.frame.width + iconsInset + (muteChannelMessages.isHidden ? 0 : iconsInset) + } else { + scheduled?.removeFromSuperview() + scheduled = nil + } - self.needsLayout = true + setFrameSize(size) + updateEntertainmentIcon() + needsLayout = true } else if value.isEmojiSection != oldValue.isEmojiSection { - entertaiments.set(image: value.isEmojiSection ? theme.icons.chatEntertainment : theme.icons.chatEntertainmentSticker, for: .Normal) + updateEntertainmentIcon() } } } @@ -341,25 +514,65 @@ class ChatInputActionsView: View, Notifable { } func prepare(with chatInteraction:ChatInteraction) -> Void { - send.set(handler: { _ in - chatInteraction.sendMessage() + + let handler:(Control)->Void = { [weak chatInteraction] control in + if let chatInteraction = chatInteraction, let peer = chatInteraction.peer { + let context = chatInteraction.context + if let slowMode = chatInteraction.presentation.slowMode, slowMode.hasLocked { + return + } + if chatInteraction.presentation.state != .normal { + return + } + var items:[SPopoverItem] = [] + + if peer.id != chatInteraction.context.account.peerId { + items.append(SPopoverItem(L10n.chatSendWithoutSound, { [weak chatInteraction] in + chatInteraction?.sendMessage(true, nil) + })) + } + switch chatInteraction.mode { + case .history: + if !peer.isSecretChat { + items.append(SPopoverItem(peer.id == chatInteraction.context.peerId ? L10n.chatSendSetReminder : L10n.chatSendScheduledMessage, { + showModal(with: DateSelectorModalController(context: context, mode: .schedule(peer.id), selectedAt: { [weak chatInteraction] date in + chatInteraction?.sendMessage(false, date) + }), for: context.window) + })) + } + case .scheduled: + break + case .replyThread: + break + case .pinned, .preview: + break + } + + if !items.isEmpty { + showPopover(for: control, with: SPopoverViewController(items: items)) + } + } + } + + send.set(handler: handler, for: .RightDown) + send.set(handler: handler, for: .LongMouseDown) + + + send.set(handler: { [weak chatInteraction] control in + chatInteraction?.sendMessage(false, nil) + }, for: .Click) + + slowModeTimeout.set(handler: { [weak chatInteraction] control in + if let slowMode = chatInteraction?.presentation.slowMode { + showSlowModeTimeoutTooltip(slowMode, for: control) + } }, for: .Click) chatInteraction.add(observer: self) - notify(with: chatInteraction.presentation, oldValue: chatInteraction.presentation, animated: false) - if chatInteraction.peerId.namespace == Namespaces.Peer.SecretChat { - secretTimer = ImageButton() - secretTimer?.set(image: theme.icons.chatSecretTimer, for: .Normal) - secretTimer?.sizeToFit() - addSubview(secretTimer!) - - secretTimer?.set(handler: { [weak self] control in - if let strongSelf = self { - showPopover(for: control, with: SPopoverViewController(items:strongSelf.secretTimerItems(), visibility: 6), edge: .maxX, inset:NSMakePoint(120, 10)) - } - }, for: .Click) - } + + + notify(with: chatInteraction.presentation, oldValue: chatInteraction.presentation, animated: false) } func performSendMessage() { @@ -378,38 +591,37 @@ class ChatInputActionsView: View, Notifable { var items:[SPopoverItem] = [] - if let peer = chatInteraction.presentation.peer as? TelegramSecretChat { - if peer.messageAutoremoveTimeout != nil { - - items.append(SPopoverItem(tr(.secretTimerOff), { [weak self] in - self?.chatInteraction.setSecretChatMessageAutoremoveTimeout(nil) + if chatInteraction.hasSetDestructiveTimer { + if chatInteraction.presentation.messageSecretTimeout != nil { + items.append(SPopoverItem(L10n.secretTimerOff, { [weak self] in + self?.chatInteraction.setChatMessageAutoremoveTimeout(nil) })) } } - - - for i in 0 ..< 30 { - - items.append(SPopoverItem(tr(.timerSecondsCountable(i + 1)), { [weak self] in - self?.chatInteraction.setSecretChatMessageAutoremoveTimeout(Int32(i + 1)) + if chatInteraction.peerId.namespace == Namespaces.Peer.SecretChat { + for i in 0 ..< 30 { + items.append(SPopoverItem(L10n.timerSecondsCountable(i + 1), { [weak self] in + self?.chatInteraction.setChatMessageAutoremoveTimeout(Int32(i + 1)) + })) + } + + items.append(SPopoverItem(L10n.timerMinutesCountable(1), { [weak self] in + self?.chatInteraction.setChatMessageAutoremoveTimeout(60) + })) + + items.append(SPopoverItem(L10n.timerHoursCountable(1), { [weak self] in + self?.chatInteraction.setChatMessageAutoremoveTimeout(60 * 60) + })) + + items.append(SPopoverItem(L10n.timerDaysCountable(1), { [weak self] in + self?.chatInteraction.setChatMessageAutoremoveTimeout(60 * 60 * 24) + })) + + items.append(SPopoverItem(L10n.timerWeeksCountable(1), { [weak self] in + self?.chatInteraction.setChatMessageAutoremoveTimeout(60 * 60 * 24 * 7) })) } - - items.append(SPopoverItem(tr(.timerMinutesCountable(1)), { [weak self] in - self?.chatInteraction.setSecretChatMessageAutoremoveTimeout(60) - })) - - items.append(SPopoverItem(tr(.timerHoursCountable(1)), { [weak self] in - self?.chatInteraction.setSecretChatMessageAutoremoveTimeout(60 * 60) - })) - - items.append(SPopoverItem(tr(.timerDaysCountable(1)), { [weak self] in - self?.chatInteraction.setSecretChatMessageAutoremoveTimeout(60 * 60 * 24) - })) - - items.append(SPopoverItem(tr(.timerWeeksCountable(1)), { [weak self] in - self?.chatInteraction.setSecretChatMessageAutoremoveTimeout(60 * 60 * 24 * 7) - })) + return items } diff --git a/Telegram-Mac/ChatInputAttachView.swift b/Telegram-Mac/ChatInputAttachView.swift index 60ef4bf23f..b423e31f5a 100644 --- a/Telegram-Mac/ChatInputAttachView.swift +++ b/Telegram-Mac/ChatInputAttachView.swift @@ -1,3 +1,4 @@ + // // ChatInputAttachView.swift // Telegram-Mac @@ -8,15 +9,19 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac -import TelegramCoreMac -import PostboxMac +import SwiftSignalKit +import TelegramCore +import Postbox -class ChatInputAttachView: ImageButton { - + +class ChatInputAttachView: ImageButton, Notifable { + + + private var chatInteraction:ChatInteraction private var controller:SPopoverViewController? + private let editMediaAccessory: ImageView = ImageView() init(frame frameRect: NSRect, chatInteraction:ChatInteraction) { self.chatInteraction = chatInteraction super.init(frame: frameRect) @@ -27,107 +32,204 @@ class ChatInputAttachView: ImageButton { updateLayout() - set(handler: { (event) in - - }, for: .Click) - let attachPhotoOrVideo = { [weak self] in - if let strongSelf = self, let window = strongSelf.kitWindow { - filePanel(with:mediaExts, for:window, completion:{(result) in - if let result = result { - let previous = result.count - - let result = result.filter { path -> Bool in - if let size = fileSize(path) { - return size <= 1500000000 - } - return false - } + set(handler: { [weak self] control in + + guard let `self` = self else {return} + if let peer = chatInteraction.presentation.peer { + + var items:[SPopoverItem] = [] + if let editState = chatInteraction.presentation.interfaceState.editState, let media = editState.originalMedia, media is TelegramMediaFile || media is TelegramMediaImage { + if editState.message.groupingKey == nil { + items.append(SPopoverItem(L10n.inputAttachPopoverPhotoOrVideo, { [weak self] in + self?.chatInteraction.updateEditingMessageMedia(mediaExts, true) + }, theme.icons.chatAttachPhoto)) - let afterSizeCheck = result.count + items.append(SPopoverItem(L10n.inputAttachPopoverFile, { [weak self] in + self?.chatInteraction.updateEditingMessageMedia(nil, false) + }, theme.icons.chatAttachFile)) - if afterSizeCheck == 0 && previous != afterSizeCheck { - alert(for: mainWindow, header: appName, info: tr(.appMaxFileSize)) - } else { - strongSelf.chatInteraction.showPreviewSender(result.map{URL(fileURLWithPath: $0)}, true) + if media is TelegramMediaImage { + items.append(SPopoverItem(L10n.editMessageEditCurrentPhoto, { [weak self] in + self?.chatInteraction.editEditingMessagePhoto(media as! TelegramMediaImage) + }, theme.icons.editMessageCurrentPhoto)) + } + } else { + if let _ = editState.message.media.first as? TelegramMediaImage { + items.append(SPopoverItem(L10n.inputAttachPopoverPhotoOrVideo, { [weak self] in + self?.chatInteraction.updateEditingMessageMedia(mediaExts, true) + }, theme.icons.chatAttachPhoto)) + } else if let file = editState.message.media.first as? TelegramMediaFile { + if file.isVideoFile { + items.append(SPopoverItem(L10n.inputAttachPopoverPhotoOrVideo, { [weak self] in + self?.chatInteraction.updateEditingMessageMedia(mediaExts, true) + }, theme.icons.chatAttachPhoto)) + } + if file.isMusic { + items.append(SPopoverItem(L10n.inputAttachPopoverMusic, { [weak self] in + self?.chatInteraction.updateEditingMessageMedia(audioExts, false) + }, theme.icons.chatAttachFile)) + } else { + items.append(SPopoverItem(L10n.inputAttachPopoverFile, { [weak self] in + self?.chatInteraction.updateEditingMessageMedia(nil, false) + }, theme.icons.chatAttachFile)) + } } } - }) - } - } - - set(handler: { [weak self] (state) in - if let strongSelf = self, let peer = strongSelf.chatInteraction.presentation.peer { - - if let peer = peer as? TelegramChannel { - if peer.hasBannedRights(.banSendMedia) { + + + + + } else if chatInteraction.presentation.interfaceState.editState == nil { + + if let slowMode = self.chatInteraction.presentation.slowMode, slowMode.hasLocked { + showSlowModeTimeoutTooltip(slowMode, for: control) return } - } - - let items = [SPopoverItem(tr(.inputAttachPopoverPhotoOrVideo), { - attachPhotoOrVideo() - }, theme.icons.chatAttachPhoto), SPopoverItem(tr(.inputAttachPopoverPicture), { [weak strongSelf] in - if let strongSelf = strongSelf, let window = strongSelf.kitWindow { - pickImage(for: window, completion: { (image) in - if let image = image { - strongSelf.chatInteraction.mediaPromise.set(putToTemp(image: image) |> map({[MediaSenderContainer(path:$0)]})) - } - }) + + items.append(SPopoverItem(L10n.inputAttachPopoverPhotoOrVideo, { [weak self] in + if let permissionText = permissionText(from: peer, for: .banSendMedia) { + alert(for: mainWindow, info: permissionText) + return + } + self?.chatInteraction.attachPhotoOrVideo() + }, theme.icons.chatAttachPhoto)) + + items.append(SPopoverItem(L10n.inputAttachPopoverPicture, { [weak self] in + guard let `self` = self else {return} + if let permissionText = permissionText(from: peer, for: .banSendMedia) { + alert(for: mainWindow, info: permissionText) + return + } + self.chatInteraction.attachPicture() + }, theme.icons.chatAttachCamera)) + + var canAttachPoll: Bool = false + if let peer = chatInteraction.presentation.peer, peer.isGroup || peer.isSupergroup { + canAttachPoll = true + } + if let peer = chatInteraction.presentation.mainPeer, peer.isBot { + canAttachPoll = true } - }, theme.icons.chatAttachCamera), SPopoverItem(tr(.inputAttachPopoverFile), { [weak strongSelf] in - if let strongSelf = strongSelf, let window = strongSelf.kitWindow { - filePanel(for:window, completion:{(result) in - if let result = result { - - let previous = result.count - - let result = result.filter { path -> Bool in - if let size = fileSize(path) { - return size <= 1500000000 - } - return false - } - - let afterSizeCheck = result.count - - if afterSizeCheck == 0 && previous != afterSizeCheck { - alert(for: mainWindow, header: appName, info: tr(.appMaxFileSize)) - } else { - strongSelf.chatInteraction.showPreviewSender(result.map{URL(fileURLWithPath: $0)}, false) - } - + if let peer = chatInteraction.presentation.peer as? TelegramChannel { + if peer.hasPermission(.sendMessages) { + canAttachPoll = true + } + } + if canAttachPoll && permissionText(from: peer, for: .banSendPolls) != nil { + canAttachPoll = false + } + + if canAttachPoll { + items.append(SPopoverItem(L10n.inputAttachPopoverPoll, { [weak self] in + guard let `self` = self else {return} + if let permissionText = permissionText(from: peer, for: .banSendPolls) { + alert(for: mainWindow, info: permissionText) + return } - }) + showModal(with: NewPollController(chatInteraction: self.chatInteraction), for: mainWindow) + }, theme.icons.chatAttachPoll)) } - }, theme.icons.chatAttachFile)] + + items.append(SPopoverItem(L10n.inputAttachPopoverFile, { [weak self] in + if let permissionText = permissionText(from: peer, for: .banSendMedia) { + alert(for: mainWindow, info: permissionText) + return + } + self?.chatInteraction.attachFile(false) + }, theme.icons.chatAttachFile)) + + items.append(SPopoverItem(L10n.inputAttachPopoverLocation, { [weak self] in + self?.chatInteraction.attachLocation() + }, theme.icons.chatAttachLocation)) + } + - strongSelf.controller = SPopoverViewController(items: items) - showPopover(for: strongSelf, with: strongSelf.controller!, edge: nil, inset: NSMakePoint(0,0)) + if !items.isEmpty { + self.controller = SPopoverViewController(items: items, visibility: 10) + showPopover(for: self, with: self.controller!, edge: nil, inset: NSMakePoint(0,0)) + } + } }, for: .Hover) - set(handler: { [weak self] _ in - if let peer = self?.chatInteraction.presentation.peer { - if peer.mediaRestricted { - alertForMediaRestriction(peer) + set(handler: { [weak self] control in + guard let `self` = self else {return} + + if let editState = chatInteraction.presentation.interfaceState.editState { + return + } + + if let peer = self.chatInteraction.presentation.peer { + if let permissionText = permissionText(from: peer, for: .banSendMedia) { + alert(for: mainWindow, info: permissionText) return } - self?.controller?.popover?.hide() - Queue.mainQueue().justDispatch { - attachPhotoOrVideo() - } - + self.controller?.popover?.hide() + // Queue.mainQueue().justDispatch { + if self.chatInteraction.presentation.interfaceState.editState != nil { + self.chatInteraction.updateEditingMessageMedia(nil, true) + } else { + self.chatInteraction.attachFile(true) + } + // } } - - }, for: .Click) + chatInteraction.add(observer: self) + addSubview(editMediaAccessory) + editMediaAccessory.layer?.opacity = 0 + updateLocalizationAndTheme(theme: theme) + } + + func isEqual(to other: Notifable) -> Bool { + if let view = other as? ChatInputAttachView { + return view === self + } else { + return false + } + } + + func notify(with value: Any, oldValue: Any, animated: Bool) { + let value = value as? ChatPresentationInterfaceState + let oldValue = oldValue as? ChatPresentationInterfaceState + + if value?.interfaceState.editState != oldValue?.interfaceState.editState { + if let editState = value?.interfaceState.editState { + let isMedia = editState.message.media.first is TelegramMediaFile || editState.message.media.first is TelegramMediaImage + editMediaAccessory.change(opacity: isMedia ? 1 : 0) + self.highlightHovered = isMedia + self.autohighlight = isMedia + } else { + editMediaAccessory.change(opacity: 0) + self.highlightHovered = true + self.autohighlight = true + } + } + +// if let slowMode = value?.slowMode { +// if slowMode.hasError { +// self.highlightHovered = false +// self.autohighlight = false +// } +// } + } + + override func layout() { + super.layout() + editMediaAccessory.setFrameOrigin(46 - editMediaAccessory.frame.width, 23) + } + + deinit { + chatInteraction.remove(observer: self) } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) + editMediaAccessory.image = theme.icons.editMessageMedia + editMediaAccessory.sizeToFit() set(image: theme.icons.chatAttach, for: .Normal) } diff --git a/Telegram-Mac/ChatInputMenuView.swift b/Telegram-Mac/ChatInputMenuView.swift new file mode 100644 index 0000000000..3e0415be81 --- /dev/null +++ b/Telegram-Mac/ChatInputMenuView.swift @@ -0,0 +1,95 @@ +// +// ChatInputMenuView.swift +// Telegram +// +// Created by Mikhail Filimonov on 17.06.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit + + +final class ChatInputMenuView : View { + private let button = Control() + private let animationView: LottiePlayerView = LottiePlayerView(frame: NSMakeRect(0, 0, 30, 30)) + weak var chatInteraction: ChatInteraction? + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + + addSubview(button) + + animationView.background = .random + + button.addSubview(animationView) + button.scaleOnClick = true + button.layer?.cornerRadius = 15 + + updateLocalizationAndTheme(theme: theme) + + button.set(handler: { [weak self] _ in + self?.chatInteraction?.update { + $0.updateBotMenu { current in + var current = current + if let value = current { + current?.revealed = !value.revealed + } + return current + } + } + }, for: .Click) + + } + + private var botMenu: ChatPresentationInterfaceState.BotMenu? + + func update(_ botMenu: ChatPresentationInterfaceState.BotMenu, animated: Bool) { + + let previous = self.botMenu + self.botMenu = botMenu + + let sticker: LocalAnimatedSticker + let playPolicy: LottiePlayPolicy + + if botMenu.revealed { + sticker = .bot_menu_close + } else { + sticker = .bot_close_menu + } + + if previous == nil || previous?.revealed == botMenu.revealed || !animated { + playPolicy = .toEnd(from: .max) + } else { + playPolicy = .toEnd(from: 0) + } + + if let data = sticker.data { + animationView.set(.init(compressed: data, key: .init(key: .bundle(sticker.rawValue + theme.colors.name), size: NSMakeSize(30, 30)), cachePurpose: .none, playPolicy: playPolicy, runOnQueue: .mainQueue())) + } + } + + deinit { + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) +// + button.set(background: theme.colors.accent, for: .Normal) + button.set(background: theme.colors.accent, for: .Hover) + button.set(background: theme.colors.accent.withAlphaComponent(0.8), for: .Highlight) + + } + + override func layout() { + super.layout() + + button.setFrameSize(NSMakeSize(40, 30)) + button.centerY(x: frame.width - button.frame.width) + animationView.center() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/ChatInputRecordingView.swift b/Telegram-Mac/ChatInputRecordingView.swift index ad66b40c68..562c11a0a6 100644 --- a/Telegram-Mac/ChatInputRecordingView.swift +++ b/Telegram-Mac/ChatInputRecordingView.swift @@ -8,7 +8,7 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac +import SwiftSignalKit enum ChatInputRecodingState { case none case recoding(TimeInterval) @@ -18,27 +18,23 @@ enum ChatInputRecodingState { class ChatInputRecordingView: View { - let descView:TextView = TextView() - let timerView:TextView = TextView() - let peakLayer:CALayer = CALayer() + private let descView:TextView = TextView() + private let timerView:TextView = TextView() + private let statusImage:ImageView = ImageView() + private let recView: View = View(frame: NSMakeRect(0, 0, 14, 14)) - let statusImage:ImageView = ImageView() + private let chatInteraction:ChatInteraction + private let recorder:ChatRecordingState - var state:ChatInputRecodingState = .none - let chatInteraction:ChatInteraction - let recorder:ChatRecordingState - var inside:Bool = false - var currentLevel:CGFloat = 1 - - let disposable:MetaDisposable = MetaDisposable() + private let disposable:MetaDisposable = MetaDisposable() + private let overlayController: ChatRecorderOverlayWindowController init(frame frameRect: NSRect, chatInteraction:ChatInteraction, recorder:ChatRecordingState) { self.chatInteraction = chatInteraction self.recorder = recorder + overlayController = ChatRecorderOverlayWindowController(parent: mainWindow, chatInteraction: chatInteraction) super.init(frame: frameRect) - peakLayer.frame = NSMakeRect(0, 0, 14, 14) - peakLayer.cornerRadius = peakLayer.frame.width / 2 statusImage.image = FastSettings.recordingState == .voice ? theme.icons.chatVoiceRecording : theme.icons.chatVideoRecording statusImage.animates = true @@ -46,62 +42,59 @@ class ChatInputRecordingView: View { - layer?.addSublayer(peakLayer) + // layer?.addSublayer(peakLayer) addSubview(descView) addSubview(timerView) - addSubview(statusImage) + // addSubview(statusImage) + addSubview(recView) + + recView.layer?.cornerRadius = recView.frame.width / 2 - disposable.set((combineLatest(recorder.micLevel, recorder.status) |> deliverOnMainQueue).start(next: { [weak self] (micLevel, state) in + disposable.set(combineLatest(recorder.status |> deliverOnMainQueue, recorder.holdpromise.get() |> deliverOnMainQueue).start(next: { [weak self] state, hold in if case let .recording(duration) = state { - self?.update(duration, CGFloat(micLevel), true) + self?.update(duration, true, hold) } })) - updateLocalizationAndTheme() + + + updateLocalizationAndTheme(theme: theme) } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) statusImage.image = FastSettings.recordingState == .voice ? theme.icons.chatVoiceRecording : theme.icons.chatVideoRecording backgroundColor = theme.colors.background descView.backgroundColor = theme.colors.background timerView.backgroundColor = theme.colors.background - peakLayer.backgroundColor = theme.colors.redUI.cgColor - + recView.backgroundColor = theme.colors.accent } override func viewWillMove(toWindow newWindow: NSWindow?) { - if let window = newWindow as? Window { - window.set(mouseHandler: { [weak self] event -> KeyHandlerResult in - self?.updateInside() - return .rejected - }, with: self, for: .leftMouseDragged, priority: .modal) + if let _ = newWindow as? Window { + overlayController.show(animated: true) + let animate = CABasicAnimation(keyPath: "opacity") + animate.fromValue = 1.0 + animate.toValue = 0.3 + animate.repeatCount = 10000 + animate.duration = 1.5 + + animate.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) + recView.layer?.add(animate, forKey: "opacity") } else { (window as? Window)?.removeAllHandlers(for: self) + overlayController.hide(animated: true) + recView.layer?.removeAllAnimations() } } override func viewDidMoveToWindow() { - update(0, 0, false) + update(0, false, false) } - private func updateInside() { - guard let superview = superview, let window = window else { - return; - } - let mouse = superview.convert(window.mouseLocationOutsideOfEventStream, from: nil) - let inside = mouse.x > 0 && mouse.y > 0 && (mouse.x < superview.frame.width && mouse.y < superview.frame.height) - - - if inside != self.inside { - self.inside = inside - let descLayout = TextViewLayout(.initialize(string:tr(.audioRecordReleaseOut), color: inside ? theme.colors.text : theme.colors.redUI, font: .normal(.text)), maximumNumberOfLines: 2, truncationType: .middle, alignment: .center) - descLayout.measure(width: frame.width - 50 - 100 - 60) - descView.update(descLayout) - } - } - func update(_ duration:TimeInterval, _ peakLevel:CGFloat, _ animated:Bool) { + func update(_ duration:TimeInterval, _ animated:Bool, _ hold: Bool) { let intDuration:Int = Int(duration) let ms = duration - TimeInterval(intDuration); @@ -110,20 +103,9 @@ class ChatInputRecordingView: View { timerLayout.measure(width: .greatestFiniteMagnitude) timerView.update(timerLayout) - - updateInside() - - - //let scale = min(max(currentLevel * 0.8 + peakLevel * 0.2,1),2); - let power = min(mappingRange(Double(peakLevel), 0, 1, 1, 1.5),1.5); - // mappingRange(<#T##x: Double##Double#>, <#T##in_min: Double##Double#>, <#T##in_max: Double##Double#>, <#T##out_min: Double##Double#>, <#T##out_max: Double##Double#>) - - - //if peakLayer.presentation()?.animation(forKey: "transform") == nil { - peakLayer.animateScale(from:currentLevel, to: CGFloat(power), duration: 0.1, removeOnCompletion:false) - self.currentLevel = CGFloat(power) - // } - + let descLayout = TextViewLayout(.initialize(string: hold ? L10n.audioRecordHelpFixed : L10n.audioRecordHelpPlain, color: theme.colors.text, font: .normal(.text)), maximumNumberOfLines: 2, truncationType: .middle, alignment: .center) + descLayout.measure(width: frame.width - 50 - 100 - 60) + descView.update(descLayout) self.needsLayout = true @@ -143,12 +125,12 @@ class ChatInputRecordingView: View { override func layout() { super.layout() - peakLayer.frame = NSMakeRect(20, floorToScreenPixels((frame.height - peakLayer.frame.height) / 2), 14, 14) - timerView.centerY(x:peakLayer.frame.maxX + 10) + recView.centerY(x: 20) + timerView.centerY(x: recView.frame.maxX + 10) statusImage.centerY(x: frame.width - statusImage.frame.width - 20) let max = (frame.width - (statusImage.frame.width + 20 + 50)) - descView.centerY(x:60 + floorToScreenPixels((max - descView.frame.width)/2)) + descView.centerY(x:60 + floorToScreenPixels(backingScaleFactor, (max - descView.frame.width)/2)) } diff --git a/Telegram-Mac/ChatInputView.swift b/Telegram-Mac/ChatInputView.swift index b3ce755004..5058a999c4 100644 --- a/Telegram-Mac/ChatInputView.swift +++ b/Telegram-Mac/ChatInputView.swift @@ -5,28 +5,28 @@ // Created by keepcoder on 24/09/2016. // Copyright © 2016 Telegram. All rights reserved. // - import Cocoa import TGUIKit -import SwiftSignalKitMac -import TelegramCoreMac -import PostboxMac +import SwiftSignalKit +import TelegramCore + +import Postbox -protocol ChatInputDelegate : class { +protocol ChatInputDelegate : AnyObject { func inputChanged(height:CGFloat, animated:Bool); } let yInset:CGFloat = 8; -class ChatInputView: Control, TGModernGrowingDelegate, Notifable { +class ChatInputView: View, TGModernGrowingDelegate, Notifable { private let sendActivityDisposable = MetaDisposable() public let ready = Promise() - + weak var delegate:ChatInputDelegate? let accessoryDispose:MetaDisposable = MetaDisposable() @@ -34,7 +34,6 @@ class ChatInputView: Control, TGModernGrowingDelegate, Notifable { var chatInteraction:ChatInteraction var accessory:ChatInputAccessory! - var account:Account! private var _ts:View! @@ -47,18 +46,21 @@ class ChatInputView: Control, TGModernGrowingDelegate, Notifable { private var messageActionsPanelView:MessageActionsPanelView? private var recordingPanelView:ChatInputRecordingView? private var blockedActionView:TitleButton? + private var additionBlockedActionView: ImageButton? + private var chatDiscussionView: ChannelDiscussionInputView? private var restrictedView:RestrictionWrappedView? //views private(set) var textView:TGModernGrowingTextView! private var actionsView:ChatInputActionsView! - private var attachView:ChatInputAttachView! + private(set) var attachView:ChatInputAttachView! - - private let emojiReplacementDisposable:MetaDisposable = MetaDisposable() + + private let slowModeUntilDisposable = MetaDisposable() + private var replyMarkupModel:ReplyMarkupNode? override var isFlipped: Bool { return false @@ -67,12 +69,20 @@ class ChatInputView: Control, TGModernGrowingDelegate, Notifable { private var standart:CGFloat = 50.0 private var bottomHeight:CGFloat = 0 + static let bottomPadding:CGFloat = 10 + static let maxBottomHeight = ReplyMarkupNode.rowHeight * 3 + ReplyMarkupNode.buttonHeight / 2 + + + private let rtfAttachmentsDisposable = MetaDisposable() + + private var botMenuView: ChatInputMenuView? + init(frame frameRect: NSRect, chatInteraction:ChatInteraction) { self.chatInteraction = chatInteraction super.init(frame: frameRect) self.animates = true - + _ts = View(frame: NSMakeRect(0, 0, NSWidth(frameRect), .borderSize)) _ts.backgroundColor = .border; @@ -84,7 +94,6 @@ class ChatInputView: Control, TGModernGrowingDelegate, Notifable { actionsView = ChatInputActionsView(frame: NSMakeRect(contentView.frame.width - 100, 0, 100, contentView.frame.height), chatInteraction:chatInteraction); - contentView.addSubview(actionsView) attachView = ChatInputAttachView(frame: NSMakeRect(0, 0, 60, contentView.frame.height), chatInteraction:chatInteraction) contentView.addSubview(attachView) @@ -94,66 +103,83 @@ class ChatInputView: Control, TGModernGrowingDelegate, Notifable { textView = TGModernGrowingTextView(frame: NSMakeRect(attachView.frame.width, yInset, contentView.frame.width - actionsView.frame.width, contentView.frame.height - yInset * 2.0)) textView.textFont = .normal(.text) + contentView.addSubview(textView) + contentView.addSubview(actionsView) self.background = theme.colors.background accessory = ChatInputAccessory(accessoryView, chatInteraction:chatInteraction) self.addSubview(accessoryView) - + self.addSubview(contentView) self.addSubview(bottomView) - + bottomView.documentView = View() - + self.addSubview(_ts) - updateLocalizationAndTheme() + updateLocalizationAndTheme(theme: theme) } public override var responder:NSResponder? { - return textView + return textView.inputView } - func updateInterface(with interaction:ChatInteraction, account:Account) -> Void { + func updateInterface(with interaction:ChatInteraction) -> Void { self.chatInteraction = interaction - self.account = account - actionsView.prepare(with: chatInteraction) - - needUpdateChatState(with: chatState, false) needUpdateReplyMarkup(with: interaction.presentation, false) - - setFrameSize(frame.size) + setFrameSize(frame.size) textView.textColor = theme.colors.text textView.linkColor = theme.colors.link - textView.textFont = .normal(.custom(theme.fontSize)) + textView.textFont = .normal(CGFloat(theme.fontSize)) - updateInput(interaction.presentation, prevState: ChatPresentationInterfaceState(),false) - textView.setPlaceholderAttributedString(.initialize(string: textPlaceholder, color: theme.colors.grayText, font: NSFont.normal(.custom(theme.fontSize)), coreText: false), update: false) + updateInput(interaction.presentation, prevState: ChatPresentationInterfaceState(chatLocation: interaction.chatLocation, chatMode: interaction.mode), false) + textView.setPlaceholderAttributedString(.initialize(string: textPlaceholder, color: theme.colors.grayText, font: NSFont.normal(theme.fontSize), coreText: false), update: false) textView.delegate = self - + updateAdditions(interaction.presentation, false) - + chatInteraction.add(observer: self) ready.set(accessory.nodeReady.get() |> map {_ in return true} |> take(1) ) } private var textPlaceholder: String { + + if case let .replyThread(_, mode) = chatInteraction.mode { + switch mode { + case .comments: + return L10n.messagesPlaceholderComment + case .replies: + return L10n.messagesPlaceholderReply + } + } + if let replyMarkup = chatInteraction.presentation.keyboardButtonsMessage?.replyMarkup { + if let placeholder = replyMarkup.placeholder { + return placeholder + } + } if let peer = chatInteraction.presentation.peer { + if let peer = peer as? TelegramChannel { + if peer.hasPermission(.canBeAnonymous) { + return L10n.messagesPlaceholderAnonymous + } + } if peer.isChannel { - return tr(.messagesPlaceholderBroadcast) + return FastSettings.isChannelMessagesMuted(peer.id) ? L10n.messagesPlaceholderSilentBroadcast : L10n.messagesPlaceholderBroadcast } } - return tr(.messagesPlaceholderSentMessage) + return L10n.messagesPlaceholderSentMessage } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - textView.setPlaceholderAttributedString(.initialize(string: textPlaceholder, color: theme.colors.grayText, font: NSFont.normal(.custom(theme.fontSize)), coreText: false), update: false) + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) + textView.setPlaceholderAttributedString(.initialize(string: textPlaceholder, color: theme.colors.grayText, font: NSFont.normal(theme.fontSize), coreText: false), update: false) _ts.backgroundColor = theme.colors.border backgroundColor = theme.colors.background contentView.backgroundColor = theme.colors.background @@ -161,32 +187,32 @@ class ChatInputView: Control, TGModernGrowingDelegate, Notifable { textView.textColor = theme.colors.text actionsView.backgroundColor = theme.colors.background blockedActionView?.disableActions() - textView.textFont = .normal(.custom(theme.fontSize)) - - blockedActionView?.style = ControlStyle(font: .normal(.title), foregroundColor: theme.colors.blueUI,backgroundColor: theme.colors.background, highlightColor: theme.colors.grayBackground) + textView.textFont = .normal(theme.fontSize) + chatDiscussionView?.updateLocalizationAndTheme(theme: theme) + blockedActionView?.style = ControlStyle(font: .normal(.title), foregroundColor: theme.colors.accent,backgroundColor: theme.colors.background, highlightColor: theme.colors.grayBackground) bottomView.backgroundColor = theme.colors.background bottomView.documentView?.background = theme.colors.background replyMarkupModel?.layout() + accessory.update(with: chatInteraction.presentation, account: chatInteraction.context.account, animated: false) accessoryView.backgroundColor = theme.colors.background accessory.container.backgroundColor = theme.colors.background + textView.setBackgroundColor(theme.colors.background) + } func notify(with value: Any, oldValue:Any, animated:Bool) { if let value = value as? ChatPresentationInterfaceState, let oldValue = oldValue as? ChatPresentationInterfaceState { - if value.effectiveInput != oldValue.effectiveInput { updateInput(value, prevState: oldValue, animated) } - - - updateAttachments(value.interfaceState,animated) + updateAttachments(value,animated) var urlPreviewChanged:Bool if value.urlPreview?.0 != oldValue.urlPreview?.0 { urlPreviewChanged = true } else if let valuePreview = value.urlPreview?.1, let oldValuePreview = oldValue.urlPreview?.1 { - urlPreviewChanged = !valuePreview.isEqual(oldValuePreview) + urlPreviewChanged = !valuePreview.isEqual(to: oldValuePreview) } else if (value.urlPreview?.1 == nil) != (oldValue.urlPreview?.1 == nil) { urlPreviewChanged = true } else { @@ -196,7 +222,7 @@ class ChatInputView: Control, TGModernGrowingDelegate, Notifable { urlPreviewChanged = urlPreviewChanged || value.interfaceState.composeDisableUrlPreview != oldValue.interfaceState.composeDisableUrlPreview - if value.interfaceState.forwardMessageIds != oldValue.interfaceState.forwardMessageIds || value.interfaceState.replyMessageId != oldValue.interfaceState.replyMessageId || value.editState?.message.id != oldValue.editState?.message.id || urlPreviewChanged { + if !isEqualMessageList(lhs: value.interfaceState.forwardMessages, rhs: oldValue.interfaceState.forwardMessages) || value.interfaceState.forwardMessageIds != oldValue.interfaceState.forwardMessageIds || value.interfaceState.replyMessageId != oldValue.interfaceState.replyMessageId || value.interfaceState.editState != oldValue.interfaceState.editState || urlPreviewChanged || value.interfaceState.hideSendersName != oldValue.interfaceState.hideSendersName { updateAdditions(value,animated) } @@ -222,17 +248,20 @@ class ChatInputView: Control, TGModernGrowingDelegate, Notifable { needUpdateReplyMarkup(with: value, animated) textViewHeightChanged(defaultContentHeight, animated: animated) } + update() } } + func needUpdateReplyMarkup(with state:ChatPresentationInterfaceState, _ animated:Bool) { if let keyboardMessage = state.keyboardButtonsMessage, let attribute = keyboardMessage.replyMarkup, state.isKeyboardShown { - replyMarkupModel = ReplyMarkupNode(attribute.rows, attribute.flags, chatInteraction.processBotKeyboard(with: keyboardMessage), bottomView.documentView as? View) + replyMarkupModel = ReplyMarkupNode(attribute.rows, attribute.flags, chatInteraction.processBotKeyboard(with: keyboardMessage), bottomView.documentView as? View, true) replyMarkupModel?.measureSize(frame.width - 40) replyMarkupModel?.redraw() replyMarkupModel?.layout() - } + bottomView.contentView.scroll(to: NSZeroPoint) + } } func isEqual(to other: Notifable) -> Bool { @@ -247,30 +276,29 @@ class ChatInputView: Control, TGModernGrowingDelegate, Notifable { } var defaultContentHeight:CGFloat { - return chatState == .normal || chatState == .editing ? textView.frame.height : CGFloat(textView.min_height) } - - func needUpdateChatState(with state:ChatState, _ animated:Bool) -> Void { - CATransaction.begin() - if animated { textViewHeightChanged(defaultContentHeight, animated: animated) } - + recordingPanelView?.removeFromSuperview() recordingPanelView = nil blockedActionView?.removeFromSuperview() blockedActionView = nil + additionBlockedActionView?.removeFromSuperview() + additionBlockedActionView = nil + chatDiscussionView?.removeFromSuperview() + chatDiscussionView = nil restrictedView?.removeFromSuperview() restrictedView = nil messageActionsPanelView?.removeFromSuperview() messageActionsPanelView = nil textView.isHidden = false - + let chatInteraction = self.chatInteraction switch state { case .normal, .editing: @@ -291,25 +319,51 @@ class ChatInputView: Control, TGModernGrowingDelegate, Notifable { break case .block(_): break - - case let .action(text,action): + case let .action(text, action, addition): self.messageActionsPanelView?.removeFromSuperview() + self.blockedActionView?.removeFromSuperview() self.blockedActionView = TitleButton(frame: bounds) - self.blockedActionView?.style = ControlStyle(font: .normal(.title),foregroundColor: theme.colors.blueUI) + self.blockedActionView?.style = ControlStyle(font: .normal(.title),foregroundColor: theme.colors.accent) self.blockedActionView?.set(text: text, for: .Normal) self.blockedActionView?.set(background: theme.colors.grayBackground, for: .Highlight) if animated { self.blockedActionView?.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) } - self.blockedActionView?.set(handler: {_ in + self.blockedActionView?.set(handler: {_ in action(chatInteraction) }, for:.Click) - + + + self.addSubview(self.blockedActionView!, positioned: .below, relativeTo: _ts) + + if let addition = addition { + additionBlockedActionView = ImageButton() + additionBlockedActionView?.animates = false + additionBlockedActionView?.set(image: addition.icon, for: .Normal) + additionBlockedActionView?.sizeToFit() + addSubview(additionBlockedActionView!, positioned: .above, relativeTo: self.blockedActionView) + + additionBlockedActionView?.set(handler: { control in + addition.action(control) + }, for: .Click) + } else { + additionBlockedActionView?.removeFromSuperview() + additionBlockedActionView = nil + } + + self.contentView.isHidden = true + self.contentView.change(opacity: 0.0, animated: animated) + self.accessoryView.change(opacity: 0.0, animated: animated) + case let .channelWithDiscussion(discussionGroupId, leftAction, rightAction): + self.messageActionsPanelView?.removeFromSuperview() + self.chatDiscussionView = ChannelDiscussionInputView(frame: bounds) + self.chatDiscussionView?.update(with: chatInteraction, discussionGroupId: discussionGroupId, leftAction: leftAction, rightAction: rightAction) + + self.addSubview(self.chatDiscussionView!, positioned: .below, relativeTo: _ts) self.contentView.isHidden = true self.contentView.change(opacity: 0.0, animated: animated) self.accessoryView.change(opacity: 0.0, animated: animated) - break case let .recording(recorder): textView.isHidden = true recordingPanelView = ChatInputRecordingView(frame: NSMakeRect(0,0,frame.width,standart), chatInteraction:chatInteraction, recorder:recorder) @@ -317,15 +371,12 @@ class ChatInputView: Control, TGModernGrowingDelegate, Notifable { if animated { self.recordingPanelView?.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) } - break case let.restricted( text): self.messageActionsPanelView?.removeFromSuperview() self.restrictedView = RestrictionWrappedView(text) if animated { self.restrictedView?.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) } - - self.addSubview(self.restrictedView!, positioned: .below, relativeTo: _ts) self.contentView.isHidden = true self.contentView.change(opacity: 0.0, animated: animated) @@ -343,19 +394,32 @@ class ChatInputView: Control, TGModernGrowingDelegate, Notifable { if textView.selectedRange().location != range.location || textView.selectedRange().length != range.length { textView.setSelectedRange(range) } + if prevState.effectiveInput.inputText.isEmpty { + self.textView.scrollToCursor() + } + } - + private var updateFirstTime: Bool = true func updateAdditions(_ state:ChatPresentationInterfaceState, _ animated:Bool = true) -> Void { - accessory.update(with: state, account: account, animated: animated) + accessory.update(with: state, account: chatInteraction.context.account, animated: animated) accessoryDispose.set(accessory.nodeReady.get().start(next: { [weak self] (animated) in if let strongSelf = self { strongSelf.accessory.measureSize(strongSelf.frame.width - 40.0) strongSelf.textViewHeightChanged(strongSelf.defaultContentHeight, animated: animated) strongSelf.update() + if strongSelf.updateFirstTime { + strongSelf.updateFirstTime = false + strongSelf.textView.scrollToCursor() + } } })) - + + } + + override func mouseUp(with event: NSEvent) { + super.mouseUp(with: event) + textView.setSelectedRange(NSMakeRange(textView.string().length, 0)) } func update() { @@ -368,54 +432,102 @@ class ChatInputView: Control, TGModernGrowingDelegate, Notifable { } - func updateAttachments(_ inputState:ChatInterfaceState, _ animated:Bool = true) -> Void { - + func updateAttachments(_ inputState:ChatPresentationInterfaceState, _ animated:Bool = true) -> Void { + if let botMenu = inputState.botMenu, !inputState.interfaceState.inputState.inputText.isEmpty { + let current: ChatInputMenuView + if let view = self.botMenuView { + current = view + } else { + current = ChatInputMenuView(frame: NSMakeRect(0, 0, 60, contentView.frame.height)) + self.botMenuView = current + contentView.addSubview(current) + } + current.chatInteraction = self.chatInteraction + current.update(botMenu, animated: animated) + } else { + if let view = self.botMenuView { + self.botMenuView = nil + if animated { + view.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } else { + view.removeFromSuperview() + } + } + } } - + override func setFrameSize(_ newSize: NSSize) { super.setFrameSize(newSize) let keyboardWidth = frame.width - 40 bottomView.setFrameSize( NSMakeSize(keyboardWidth, bottomHeight)) - - if let markup = replyMarkupModel, markup.hasButtons { markup.measureSize(keyboardWidth) markup.view?.setFrameSize(NSMakeSize(markup.size.width, markup.size.height + 5)) markup.layout() } contentView.setFrameSize(frame.width, contentView.frame.height) - textView.setFrameSize(textViewSize()) + textView.setFrameSize(textViewSize(textView)) actionsView.setFrameSize(NSWidth(actionsView.frame), NSHeight(actionsView.frame)) attachView.setFrameSize(NSWidth(attachView.frame), NSHeight(attachView.frame)) + + if let botMenuView = botMenuView { + botMenuView.setFrameSize(NSWidth(botMenuView.frame), NSHeight(botMenuView.frame)) + } + _ts.setFrameSize(frame.width, .borderSize) - accessory.measureSize(frame.width - 40.0) - accessory.frame = NSMakeRect(15, contentView.frame.maxY, accessory.measuredWidth, accessory.size.height) + accessory.measureSize(frame.width - 64) + accessory.frame = NSMakeRect(15, contentView.frame.maxY, frame.width - 39, accessory.size.height) messageActionsPanelView?.setFrameSize(frame.size) blockedActionView?.setFrameSize(frame.size) + chatDiscussionView?.setFrameSize(frame.size) restrictedView?.setFrameSize(frame.size) + + guard let superview = superview else {return} + textView.max_height = Int32(superview.frame.height / 2 + 50) + + if textView.placeholderAttributedString?.string != self.textPlaceholder { + textView.setPlaceholderAttributedString(.initialize(string: textPlaceholder, color: theme.colors.grayText, font: NSFont.normal(theme.fontSize), coreText: false), update: false) + } + + } override func layout() { super.layout() - let bottomInset = chatInteraction.presentation.isKeyboardShown ? bottomHeight : 0 bottomView.setFrameOrigin(20, chatInteraction.presentation.isKeyboardShown ? 0 : -bottomHeight) + contentView.setFrameOrigin(0, bottomInset) + actionsView.setFrameOrigin(frame.width - actionsView.frame.width, 0) + + var leftInset: CGFloat = 0 + + if let botMenuView = botMenuView { + botMenuView.setFrameOrigin(.zero) + leftInset += botMenuView.frame.width + } + + attachView.setFrameOrigin(NSMakePoint(leftInset, 0)) + leftInset += attachView.frame.width + + textView.setFrameOrigin(NSMakePoint(leftInset, yInset)) - contentView.setFrameOrigin(0, bottomInset) - actionsView.setFrameOrigin(NSMaxX(textView.frame), 0) - attachView.setFrameOrigin(0, 0) _ts.setFrameOrigin(0, frame.height - .borderSize) + if let additionBlockedActionView = additionBlockedActionView { + additionBlockedActionView.centerY(x: frame.width - additionBlockedActionView.frame.width - 22) + } } override func setFrameOrigin(_ newOrigin: NSPoint) { super.setFrameOrigin(newOrigin) } - + var stringValue:String { return textView.string() @@ -435,7 +547,10 @@ class ChatInputView: Control, TGModernGrowingDelegate, Notifable { let contentHeight:CGFloat = defaultContentHeight + yInset * 2.0 var sumHeight:CGFloat = contentHeight + (accessory.isVisibility() ? accessory.size.height + 5 : 0) if let markup = replyMarkupModel { - bottomHeight = min(110,markup.size.height) + 10 + bottomHeight = min( + ChatInputView.maxBottomHeight, + markup.size.height + ChatInputView.bottomPadding + ) } else { bottomHeight = 0 } @@ -456,37 +571,54 @@ class ChatInputView: Control, TGModernGrowingDelegate, Notifable { bottomView._change(size: NSMakeSize(frame.width - 40, bottomHeight), animated: animated) bottomView._change(pos: NSMakePoint(20, chatInteraction.presentation.isKeyboardShown ? 0 : -bottomHeight), animated: animated) - - accessory.view?.change(opacity: accessory.isVisibility() ? 1.0 : 0.0, animated: animated) - accessory.view?.change(pos: NSMakePoint(15, contentHeight), animated: animated) + accessory.view?.change(pos: NSMakePoint(15, contentHeight + bottomHeight), animated: animated) change(size: NSMakeSize(NSWidth(frame), sumHeight), animated: animated) delegate?.inputChanged(height: sumHeight, animated: animated) } - + } public func textViewEnterPressed(_ event: NSEvent) -> Bool { if FastSettings.checkSendingAbility(for: event) { - if !textView.string().trimmed.isEmpty || !chatInteraction.presentation.interfaceState.forwardMessageIds.isEmpty { - chatInteraction.sendMessage() - chatInteraction.account.updateLocalInputActivity(peerId: chatInteraction.peerId, activity: .typingText, isPresent: false) + let text = textView.string().trimmed + if text.length > chatInteraction.presentation.maxInputCharacters { + alert(for: chatInteraction.context.window, info: L10n.chatInputErrorMessageTooLongCountable(text.length - Int(chatInteraction.presentation.maxInputCharacters))) + return true + } + if !text.isEmpty || !chatInteraction.presentation.interfaceState.forwardMessageIds.isEmpty || chatInteraction.presentation.state == .editing { + chatInteraction.sendMessage(false, nil) + + chatInteraction.context.account.updateLocalInputActivity(peerId: chatInteraction.activitySpace, activity: .typingText, isPresent: false) + markNextTextChangeToFalseActivity = true + } else if text.isEmpty { + chatInteraction.scrollToLatest(true) } return true } - return false } - + var currentActionView: NSView { + return self.actionsView.currentActionView + } + + + func makeBold() { self.textView.boldWord() } + func removeAllAttributes() { + self.textView.removeAllAttributes() + } + func makeUrl() { + self.makeUrl(of: textView.selectedRange()) + } func makeItalic() { self.textView.italicWord() } @@ -501,67 +633,171 @@ class ChatInputView: Control, TGModernGrowingDelegate, Notifable { func makeFirstResponder() { self.window?.makeFirstResponder(self.textView.inputView) } - + private var previousString: String = "" func textViewTextDidChange(_ string: String) { - if FastSettings.isPossibleReplaceEmojies { - let replacedEmojies = string.stringEmojiReplacements - if string != replacedEmojies { - self.textView.setString(replacedEmojies) - } - } + + + let attributed = self.textView.attributedString() + let range = self.textView.selectedRange() + let state = ChatTextInputState(inputText: attributed.string, selectionRange: range.location ..< range.location + range.length, attributes: chatTextAttributes(from: attributed)) + chatInteraction.update({$0.withUpdatedEffectiveInputState(state)}) + } func canTransformInputText() -> Bool { - if let editState = chatInteraction.presentation.editState { - return editState.message.media.isEmpty - } return true } - + private var markNextTextChangeToFalseActivity: Bool = false public func textViewTextDidChangeSelectedRange(_ range: NSRange) { let attributed = self.textView.attributedString() - let state = ChatTextInputState(inputText: attributed.string, selectionRange: range.location ..< range.location + range.length, attributes: chatTextAttributes(from: attributed)) - chatInteraction.update({$0.withUpdatedEffectiveInputState(state)}) + let state = ChatTextInputState(inputText: attributed.string, selectionRange: range.min ..< range.max, attributes: chatTextAttributes(from: attributed)) - if chatInteraction.account.peerId != chatInteraction.peerId, let peer = chatInteraction.presentation.peer, !peer.isChannel { + chatInteraction.update({ current in + var current = current + current = current.withUpdatedEffectiveInputState(state) + if let disabledPreview = current.interfaceState.composeDisableUrlPreview { + if !current.effectiveInput.inputText.contains(disabledPreview) { + + var detectedUrl: String? + current.effectiveInput.attributedString.enumerateAttribute(NSAttributedString.Key(rawValue: TGCustomLinkAttributeName), in: current.effectiveInput.attributedString.range, options: NSAttributedString.EnumerationOptions(rawValue: 0), using: { (value, range, stop) in + if let tag = value as? TGInputTextTag, let url = tag.attachment as? String { + detectedUrl = url + } + let s: ObjCBool = (detectedUrl != nil) ? true : false + stop.pointee = s + }) + if detectedUrl == nil { + current = current.updatedUrlPreview(nil).updatedInterfaceState {$0.withUpdatedComposeDisableUrlPreview(nil)} + } + } + } + return current + }) + + if chatInteraction.context.peerId != chatInteraction.peerId, let peer = chatInteraction.presentation.peer, !peer.isChannel && !markNextTextChangeToFalseActivity { - sendActivityDisposable.set((Signal.single(true) |> then(Signal.single(false) |> delay(4.0, queue: Queue.mainQueue()))).start(next: { [weak self] isPresent in - if let chatInteraction = self?.chatInteraction, let peer = chatInteraction.presentation.peer, !peer.isChannel { - chatInteraction.account.updateLocalInputActivity(peerId: chatInteraction.peerId, activity: .typingText, isPresent: isPresent) + sendActivityDisposable.set((Signal.single(!state.inputText.isEmpty) |> then(Signal.single(false) |> delay(4.0, queue: Queue.mainQueue()))).start(next: { [weak self] isPresent in + if let chatInteraction = self?.chatInteraction, let peer = chatInteraction.presentation.peer, !peer.isChannel && chatInteraction.presentation.state != .editing { + chatInteraction.context.account.updateLocalInputActivity(peerId: .init(peerId: peer.id, category: chatInteraction.mode.activityCategory), activity: .typingText, isPresent: isPresent) } })) } + markNextTextChangeToFalseActivity = false } + deinit { chatInteraction.remove(observer: self) self.accessoryDispose.dispose() - emojiReplacementDisposable.dispose() + rtfAttachmentsDisposable.dispose() + slowModeUntilDisposable.dispose() } - func textViewSize() -> NSSize { - return NSMakeSize(NSWidth(contentView.frame) - NSWidth(actionsView.frame) - NSWidth(attachView.frame), NSHeight(textView.frame)) + func textViewSize(_ textView: TGModernGrowingTextView!) -> NSSize { + var leftInset: CGFloat = attachView.frame.width + if let botMenu = self.botMenuView { + leftInset += botMenu.frame.width + } + return NSMakeSize(contentView.frame.width - actionsView.frame.width - leftInset, textView.frame.height) } func textViewIsTypingEnabled() -> Bool { + if let editState = chatInteraction.presentation.interfaceState.editState { + if editState.loadingState != .none { + return false + } + } return self.chatState == .normal || self.chatState == .editing } - func maxCharactersLimit() -> Int32 { - return chatInteraction.presentation.maxInputCharacters + func makeUrl(of range: NSRange) { + guard range.min != range.max, let window = kitWindow else { + return + } + var effectiveRange:NSRange = NSMakeRange(NSNotFound, 0) + let defaultTag: TGInputTextTag? = self.textView.attributedString().attribute(NSAttributedString.Key(rawValue: TGCustomLinkAttributeName), at: range.location, effectiveRange: &effectiveRange) as? TGInputTextTag + + + let defaultUrl = defaultTag?.attachment as? String + + if effectiveRange.location == NSNotFound || defaultTag == nil { + effectiveRange = range + } + + showModal(with: InputURLFormatterModalController(string: self.textView.string().nsstring.substring(with: effectiveRange), defaultUrl: defaultUrl, completion: { [weak self] url in + self?.textView.addLink(url, range: effectiveRange) + }), for: window) + + } + + func maxCharactersLimit(_ textView: TGModernGrowingTextView!) -> Int32 { + return ChatPresentationInterfaceState.maxInput + } + + @available(OSX 10.12.2, *) + func textView(_ textView: NSTextView!, shouldUpdateTouchBarItemIdentifiers identifiers: [NSTouchBarItem.Identifier]!) -> [NSTouchBarItem.Identifier]! { + return inputChatTouchBarItems(presentation: chatInteraction.presentation) + } + + func supportContinuityCamera() -> Bool { + return true + } + + func copyText(withRTF rtf: NSAttributedString!) -> Bool { + return globalLinkExecutor.copyAttributedString(rtf) } func textViewDidPaste(_ pasteboard: NSPasteboard) -> Bool { - if let window = kitWindow, self.chatState == .normal { - return !InputPasteboardParser.proccess(pasteboard: pasteboard, account: self.account, chatInteraction:self.chatInteraction, window: window) + if let window = kitWindow, self.chatState == .normal || self.chatState == .editing { + + if let string = pasteboard.string(forType: .string) { + chatInteraction.update { current in + if let disabled = current.interfaceState.composeDisableUrlPreview, disabled.lowercased() == string.lowercased() { + return current.updatedInterfaceState {$0.withUpdatedComposeDisableUrlPreview(nil)} + } + return current + } + } + + let result = InputPasteboardParser.proccess(pasteboard: pasteboard, chatInteraction:self.chatInteraction, window: window) + if result { + if let data = pasteboard.data(forType: .rtf) { + if let attributed = (try? NSAttributedString(data: data, options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.rtfd], documentAttributes: nil)) ?? (try? NSAttributedString(data: data, options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.rtf], documentAttributes: nil)) { + + let (attributed, attachments) = attributed.applyRtf() + + if !attachments.isEmpty { + rtfAttachmentsDisposable.set((prepareTextAttachments(attachments) |> deliverOnMainQueue).start(next: { [weak self] urls in + if !urls.isEmpty, let chatInteraction = self?.chatInteraction { + chatInteraction.showPreviewSender(urls, true, attributed) + } + })) + } else { + let current = textView.attributedString().copy() as! NSAttributedString + let currentRange = textView.selectedRange() + let (attributedString, range) = current.appendAttributedString(attributed, selectedRange: currentRange) + let item = SimpleUndoItem(attributedString: current, be: attributedString, wasRange: currentRange, be: range) + self.textView.addSimpleItem(item) + } + Queue.mainQueue().async { [weak self] in + self?.textView.scrollToCursor() + } + return true + } + } + } + + + return !result } - return self.chatState == .normal + + return self.chatState != .normal } - + } diff --git a/Telegram-Mac/ChatInteractiveContentView.swift b/Telegram-Mac/ChatInteractiveContentView.swift index dc822527a9..e0b80ecbc2 100644 --- a/Telegram-Mac/ChatInteractiveContentView.swift +++ b/Telegram-Mac/ChatInteractiveContentView.swift @@ -7,192 +7,716 @@ // import Cocoa -import SwiftSignalKitMac -import PostboxMac -import TelegramCoreMac +import SwiftSignalKit +import Postbox +import TelegramCore + import TGUIKit +extension AutoremoveTimeoutMessageAttribute : Equatable { + public static func == (lhs: AutoremoveTimeoutMessageAttribute, rhs: AutoremoveTimeoutMessageAttribute) -> Bool { + return lhs.timeout == rhs.timeout && lhs.countdownBeginTime == rhs.countdownBeginTime && lhs.associatedMessageIds == rhs.associatedMessageIds + } + + +} + +final class ChatVideoAutoplayView { + let mediaPlayer: MediaPlayer + let view: MediaPlayerView + + fileprivate var playTimer: SwiftSignalKit.Timer? + var status: MediaPlayerStatus? + + private var timer: SwiftSignalKit.Timer? = nil + + init(mediaPlayer: MediaPlayer, view: MediaPlayerView) { + self.mediaPlayer = mediaPlayer + self.view = view + mediaPlayer.actionAtEnd = .loop(nil) + + + } + + func toggleVolume(_ enabled: Bool, animated: Bool) { + if !animated { + mediaPlayer.setVolume(enabled ? 1 : 0) + timer?.invalidate() + timer = nil + } else { + timer = nil + + let start:(Float) -> Void = { [weak self] volume in + let fps = Float(1000 / 60) + var current:Float = volume + + let tick = (enabled ? 1 - current : -current) / (fps * 0.3) + + self?.timer = SwiftSignalKit.Timer(timeout: abs(Double(tick)), repeat: true, completion: { [weak self] in + current += tick + self?.mediaPlayer.setVolume(min(1, max(0, current))) + + if current >= 1 || current <= 0 { + self?.timer?.invalidate() + } + }, queue: .mainQueue()) + + self?.timer?.start() + } + + mediaPlayer.getVolume { volume in + Queue.mainQueue().justDispatch { + start(volume) + } + } + } + } + + deinit { + view.removeFromSuperview() + timer?.invalidate() + playTimer?.invalidate() + } +} + + class ChatInteractiveContentView: ChatMediaContentView { private let image:TransformImageView = TransformImageView() - private var videoAccessory: ChatVideoAccessoryView? = nil + private var videoAccessory: ChatMessageAccessoryView? = nil private var progressView:RadialProgressView? private var timableProgressView: TimableProgressView? = nil private let statusDisposable = MetaDisposable() private let fetchDisposable = MetaDisposable() + private let partDisposable = MetaDisposable() + + private var authenticFetchStatus: MediaResourceStatus? + + + private let mediaPlayerStatusDisposable = MetaDisposable() + private var autoplayVideoView: ChatVideoAutoplayView? + + override var backgroundColor: NSColor { + get { + return super.backgroundColor + } + set { + super.backgroundColor = .clear + } + } + + override func previewMediaIfPossible() -> Bool { + guard let context = self.context, let window = self.kitWindow, let table = self.table, parent == nil || parent?.containsSecretMedia == false, fetchStatus == .Local else {return false} + _ = startModalPreviewHandle(table, window: window, context: context) + return true + } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } required init(frame frameRect: NSRect) { super.init(frame:frameRect) + //background = .random self.addSubview(image) } + + + override func updateMouse() { + + } + override func open() { - if let parent = parent, let account = account { - let parameters = self.parameters as? ChatMediaGalleryParameters - var type:GalleryAppearType = .history - if let parameters = parameters, parameters.isWebpage { - type = .alone - } else if parent.containsSecretMedia { - type = .secret + if let parent = parent { + parameters?.showMedia(parent) + autoplayVideoView?.toggleVolume(false, animated: false) + } + } + + private func updateMediaStatus(_ status: MediaPlayerStatus, animated: Bool = false) { + if let autoplayVideoView = autoplayVideoView, let media = self.media as? TelegramMediaFile { + autoplayVideoView.status = status + updateVideoAccessory(.Local, file: media, mediaPlayerStatus: status, animated: animated) + + switch status.status { + case .playing: + autoplayVideoView.playTimer?.invalidate() + autoplayVideoView.playTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in + self?.updateVideoAccessory(.Local, file: media, mediaPlayerStatus: status, animated: animated) + }, queue: .mainQueue()) + + autoplayVideoView.playTimer?.start() + default: + autoplayVideoView.playTimer?.invalidate() + } + + + } + } + + + override func interactionControllerDidFinishAnimation(interactive: Bool) { + + } + + override func addAccesoryOnCopiedView(view: NSView) { + if let videoAccessory = videoAccessory?.copy() as? NSView { + if visibleRect.minY < videoAccessory.frame.midY && visibleRect.minY + visibleRect.height > videoAccessory.frame.midY { + videoAccessory.frame.origin.y = frame.height - videoAccessory.frame.maxY + view.addSubview(videoAccessory) + } + + } + if let progressView = progressView { + let pView = RadialProgressView(theme: progressView.theme, twist: true) + pView.state = progressView.state + pView.frame = progressView.frame + if visibleRect.minY < progressView.frame.midY && visibleRect.minY + visibleRect.height > progressView.frame.midY { + pView.frame.origin.y = frame.height - progressView.frame.maxY + view.addSubview(pView) } - showChatGallery(account: account,message: parent, table, parameters, type: type) } + self.autoplayVideoView?.mediaPlayer.seek(timestamp: 0) } + func removeNotificationListeners() { + NotificationCenter.default.removeObserver(self) + } + override func viewDidUpdatedDynamicContent() { + super.viewDidUpdatedDynamicContent() + updatePlayerIfNeeded() + } + + deinit { + removeNotificationListeners() + mediaPlayerStatusDisposable.dispose() + partDisposable.dispose() + } + + + @objc func updatePlayerIfNeeded() { + let accept = window != nil && window!.isKeyWindow && !NSIsEmptyRect(visibleRect) && !self.isDynamicContentLocked + + + if let autoplayView = autoplayVideoView { + if accept { + autoplayView.mediaPlayer.play() + } else { + autoplayView.mediaPlayer.pause() + autoplayVideoView?.playTimer?.invalidate() + } + } + } + + + func updateListeners() { + if let window = window { + NotificationCenter.default.removeObserver(self) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSWindow.didBecomeKeyNotification, object: window) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSWindow.didResignKeyNotification, object: window) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSView.boundsDidChangeNotification, object: table?.clipView) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSView.boundsDidChangeNotification, object: self) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSView.frameDidChangeNotification, object: table?.view) + } else { + removeNotificationListeners() + } + } + override func willRemove() { + super.willRemove() + updateListeners() + updatePlayerIfNeeded() + } + + override func viewDidMoveToWindow() { + updateListeners() + self.updatePlayerIfNeeded() + } + override func viewDidMoveToSuperview() { + super.viewDidMoveToSuperview() + self.updatePlayerIfNeeded() + } + override func layout() { super.layout() progressView?.center() timableProgressView?.center() - videoAccessory?.setFrameOrigin(5, 5) - + videoAccessory?.setFrameOrigin(8, 8) self.image.setFrameSize(frame.size) + + if let file = media as? TelegramMediaFile { + let dimensions = file.dimensions?.size ?? frame.size + let size = blurBackground ? dimensions.aspectFitted(frame.size) : frame.size + self.autoplayVideoView?.view.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - size.width) / 2), floorToScreenPixels(backingScaleFactor, (frame.height - size.height) / 2), size.width, size.height) + let positionFlags = self.autoplayVideoView?.view.positionFlags + self.autoplayVideoView?.view.positionFlags = positionFlags + + } + } + + private func updateVideoAccessory(_ status: MediaResourceStatus, file: TelegramMediaFile, mediaPlayerStatus: MediaPlayerStatus? = nil, animated: Bool = false) { + let maxWidth = frame.width - 10 + let text: String + + var isBuffering: Bool = false + if let fetchStatus = self.fetchStatus, let status = mediaPlayerStatus { + switch status.status { + case .buffering: + switch fetchStatus { + case .Local: + break + default: + isBuffering = true + } + default: + break + } + + } + + switch status { + case let .Fetching(_, progress): + let current = String.prettySized(with: Int(Float(file.elapsedSize) * progress), afterDot: 1) + var size = "\(current) / \(String.prettySized(with: file.elapsedSize))" + if (maxWidth < 100 && parent?.groupingKey != nil) || file.elapsedSize == 0 { + size = "\(Int(progress * 100))%" + } + if file.isStreamable, parent?.groupingKey == nil, maxWidth > 100 { + if let parent = parent { + if !parent.flags.contains(.Unsent) && !parent.flags.contains(.Failed) { + size = String.durationTransformed(elapsed: file.videoDuration) + ", \(size)" + } + } else { + size = String.durationTransformed(elapsed: file.videoDuration) + ", \(size)" + } + } + text = size + case .Remote: + var size = String.durationTransformed(elapsed: file.videoDuration) + if file.isStreamable, parent?.groupingKey == nil, maxWidth > 100 { + size = size + ", " + String.prettySized(with: file.elapsedSize) + } + text = size + case .Local: + if let status = mediaPlayerStatus, status.generationTimestamp > 0, status.duration > 0 { + text = String.durationTransformed(elapsed: Int(status.duration - (status.timestamp + (CACurrentMediaTime() - status.generationTimestamp)))) + } else { + text = String.durationTransformed(elapsed: file.videoDuration) + } + } + + let isStreamable: Bool + if let parent = parent { + isStreamable = !parent.flags.contains(.Unsent) && !parent.flags.contains(.Failed) && file.isStreamable + } else { + isStreamable = file.isStreamable + } + + videoAccessory?.updateText(text, maxWidth: maxWidth, status: status, isStreamable: isStreamable, isCompact: parent?.groupingKey != nil, soundOffOnImage: nil, isBuffering: isBuffering, animated: animated, fetch: { [weak self] in + self?.fetch() + }, cancelFetch: { [weak self] in + self?.cancelFetching() + }, click: { + + }) + + } + + override func executeInteraction(_ isControl: Bool) { + if let progressView = progressView { + switch progressView.state { + case .Fetching: + if isControl { + if let parent = parent, parent.flags.contains(.Unsent) && !parent.flags.contains(.Failed) { + delete() + } + cancelFetching() + } + default: + super.executeInteraction(isControl) + } + } else { + if autoplayVideo { + open() + } else { + super.executeInteraction(isControl) + } + } + } + + var autoplayVideo: Bool { + if #available(OSX 10.12, *) { + } else { + return false + } + + if let autoremoveAttribute = parent?.autoremoveAttribute, autoremoveAttribute.timeout <= 60 { + return false + } - override func update(with media: Media, size:NSSize, account:Account, parent:Message?, table:TableView?, parameters:ChatMediaLayoutParameters? = nil, animated: Bool) { + if let media = media as? TelegramMediaFile, let parameters = self.parameters { + return (media.isStreamable || authenticFetchStatus == .Local) && (autoDownload || authenticFetchStatus == .Local) && parameters.autoplay && (parent?.groupingKey == nil || self.frame.width == superview?.frame.width) + } + return false + } + + var blurBackground: Bool { + return (parent != nil && parent?.groupingKey == nil) || parent == nil + } + + override func update(size: NSSize) { + + var topLeftRadius: CGFloat = .cornerRadius + var bottomLeftRadius: CGFloat = .cornerRadius + var topRightRadius: CGFloat = .cornerRadius + var bottomRightRadius: CGFloat = .cornerRadius - let mediaUpdated = true//self.media == nil || !self.media!.isEqual(media) - super.update(with: media, size: size, account: account, parent:parent, table:table, parameters:parameters) + if let positionFlags = positionFlags { + if positionFlags.contains(.top) && positionFlags.contains(.left) { + topLeftRadius = topLeftRadius * 3 + 2 + } + if positionFlags.contains(.top) && positionFlags.contains(.right) { + topRightRadius = topRightRadius * 3 + 2 + } + if positionFlags.contains(.bottom) && positionFlags.contains(.left) { + bottomLeftRadius = bottomLeftRadius * 3 + 2 + } + if positionFlags.contains(.bottom) && positionFlags.contains(.right) { + bottomRightRadius = bottomRightRadius * 3 + 2 + } + } + + var dimensions: NSSize = size + + if let image = media as? TelegramMediaImage { + dimensions = image.representationForDisplayAtSize(PixelDimensions(size))?.dimensions.size ?? size + } else if let file = media as? TelegramMediaFile { + dimensions = file.dimensions?.size ?? size + } + + let arguments = TransformImageArguments(corners: ImageCorners(topLeft: .Corner(topLeftRadius), topRight: .Corner(topRightRadius), bottomLeft: .Corner(bottomLeftRadius), bottomRight: .Corner(bottomRightRadius)), imageSize: blurBackground ? dimensions.aspectFitted(size) : dimensions.aspectFilled(size), boundingSize: size, intrinsicInsets: NSEdgeInsets(), resizeMode: blurBackground ? .blurBackground : .none) + + self.image.set(arguments: arguments) + + if self.image.isFullyLoaded { + + } + } + + override func update(with media: Media, size:NSSize, context:AccountContext, parent:Message?, table:TableView?, parameters:ChatMediaLayoutParameters? = nil, animated: Bool, positionFlags: LayoutPositionFlags? = nil, approximateSynchronousValue: Bool = false) { + + partDisposable.set(nil) + + let versionUpdated = parent?.stableVersion != self.parent?.stableVersion + + + let mediaUpdated = self.media == nil || !media.isSemanticallyEqual(to: self.media!) || (parent?.autoremoveAttribute != self.parent?.autoremoveAttribute) || positionFlags != self.positionFlags || animated || self.frame.size != size + if mediaUpdated, let rhs = media as? TelegramMediaFile, let lhs = self.media as? TelegramMediaFile { + if !lhs.isSemanticallyEqual(to: rhs) { + self.autoplayVideoView = nil + } + } + + var clearInstantly: Bool = mediaUpdated + if clearInstantly, parent?.stableId == self.parent?.stableId { + clearInstantly = false + } - var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? - var updatedStatusSignal: Signal? + super.update(with: media, size: size, context: context, parent:parent, table: table, parameters:parameters, positionFlags: positionFlags) + + + var topLeftRadius: CGFloat = .cornerRadius + var bottomLeftRadius: CGFloat = .cornerRadius + var topRightRadius: CGFloat = .cornerRadius + var bottomRightRadius: CGFloat = .cornerRadius + + + if let positionFlags = positionFlags { + if positionFlags.contains(.top) && positionFlags.contains(.left) { + topLeftRadius = topLeftRadius * 3 + 2 + } + if positionFlags.contains(.top) && positionFlags.contains(.right) { + topRightRadius = topRightRadius * 3 + 2 + } + if positionFlags.contains(.bottom) && positionFlags.contains(.left) { + bottomLeftRadius = bottomLeftRadius * 3 + 2 + } + if positionFlags.contains(.bottom) && positionFlags.contains(.right) { + bottomRightRadius = bottomRightRadius * 3 + 2 + } + } + + var dimensions: NSSize = size + + if let image = media as? TelegramMediaImage { + dimensions = image.representationForDisplayAtSize(PixelDimensions(size))?.dimensions.size ?? size + } else if let file = media as? TelegramMediaFile { + dimensions = file.dimensions?.size ?? size + } - if mediaUpdated { + let arguments = TransformImageArguments(corners: ImageCorners(topLeft: .Corner(topLeftRadius), topRight: .Corner(topRightRadius), bottomLeft: .Corner(bottomLeftRadius), bottomRight: .Corner(bottomRightRadius)), imageSize: blurBackground ? dimensions.aspectFitted(size) : dimensions.aspectFilled(size), boundingSize: size, intrinsicInsets: NSEdgeInsets(), resizeMode: blurBackground ? .blurBackground : .none) + + + + + var updateImageSignal: Signal? + var updatedStatusSignal: Signal<(MediaResourceStatus, MediaResourceStatus), NoError>? + + if mediaUpdated /*mediaUpdated*/ { - var dimensions: NSSize = size if let image = media as? TelegramMediaImage { + + autoplayVideoView = nil videoAccessory?.removeFromSuperview() videoAccessory = nil - dimensions = image.representationForDisplayAtSize(size)?.dimensions ?? size + dimensions = image.representationForDisplayAtSize(PixelDimensions(size))?.dimensions.size ?? size if let parent = parent, parent.containsSecretMedia { - updateImageSignal = chatSecretPhoto(account: account, photo: image, scale: backingScaleFactor) + updateImageSignal = chatSecretPhoto(account: context.account, imageReference: ImageMediaReference.message(message: MessageReference(parent), media: image), scale: backingScaleFactor, synchronousLoad: approximateSynchronousValue) } else { - updateImageSignal = chatMessagePhoto(account: account, photo: image, scale: backingScaleFactor) + updateImageSignal = chatMessagePhoto(account: context.account, imageReference: parent != nil ? ImageMediaReference.message(message: MessageReference(parent!), media: image) : ImageMediaReference.standalone(media: image), scale: backingScaleFactor, synchronousLoad: approximateSynchronousValue) } if let parent = parent, parent.flags.contains(.Unsent) && !parent.flags.contains(.Failed) { - updatedStatusSignal = combineLatest(chatMessagePhotoStatus(account: account, photo: image), account.pendingMessageManager.pendingMessageStatus(parent.id)) - |> map { resourceStatus, pendingStatus -> MediaResourceStatus in - if let pendingStatus = pendingStatus { - return .Fetching(isActive: true, progress: pendingStatus.progress) + updatedStatusSignal = combineLatest(chatMessagePhotoStatus(account: context.account, photo: image), context.account.pendingMessageManager.pendingMessageStatus(parent.id)) + |> map { resourceStatus, pendingStatus in + if let pendingStatus = pendingStatus.0, parent.forwardInfo == nil || resourceStatus != .Local { + return (.Fetching(isActive: true, progress: min(pendingStatus.progress, pendingStatus.progress * 85 / 100)), .Fetching(isActive: true, progress: min(pendingStatus.progress, pendingStatus.progress * 85 / 100))) } else { - return resourceStatus + return (resourceStatus, resourceStatus) } } |> deliverOnMainQueue } else { - updatedStatusSignal = chatMessagePhotoStatus(account: account, photo: image) |> deliverOnMainQueue + updatedStatusSignal = chatMessagePhotoStatus(account: context.account, photo: image, approximateSynchronousValue: approximateSynchronousValue) |> map {($0, $0)} |> deliverOnMainQueue } } else if let file = media as? TelegramMediaFile { - if file.isVideo { + + let fileReference = parent != nil ? FileMediaReference.message(message: MessageReference(parent!), media: file) : FileMediaReference.standalone(media: file) + + + + if file.isVideo, size.height > 80 { if videoAccessory == nil { - videoAccessory = ChatVideoAccessoryView(frame: NSZeroRect) + videoAccessory = ChatMessageAccessoryView(frame: NSMakeRect(5, 5, 0, 0)) addSubview(videoAccessory!) } - videoAccessory?.updateText(String.durationTransformed(elapsed: file.videoDuration) + ", \(String.prettySized(with: file.size ?? 0))", maxWidth: size.width - 20) } else { videoAccessory?.removeFromSuperview() videoAccessory = nil } if let parent = parent, parent.containsSecretMedia { - updateImageSignal = chatSecretMessageVideo(account: account, video: file, scale: backingScaleFactor) + updateImageSignal = chatSecretMessageVideo(account: context.account, fileReference: fileReference, scale: backingScaleFactor) } else { - updateImageSignal = chatMessageVideo(account: account, video: file, scale: backingScaleFactor) + updateImageSignal = chatMessageVideo(postbox: context.account.postbox, fileReference: fileReference, scale: backingScaleFactor) //chatMessageVideo(account: account, video: file, scale: backingScaleFactor) } - dimensions = file.dimensions ?? size + dimensions = file.dimensions?.size ?? size + + if let parent = parent, parent.flags.contains(.Unsent) && !parent.flags.contains(.Failed) { - updatedStatusSignal = combineLatest(chatMessageFileStatus(account: account, file: file), account.pendingMessageManager.pendingMessageStatus(parent.id)) - |> map { resourceStatus, pendingStatus -> MediaResourceStatus in - if let pendingStatus = pendingStatus { - return .Fetching(isActive: true, progress: pendingStatus.progress) + updatedStatusSignal = combineLatest(chatMessageFileStatus(account: context.account, file: file), context.account.pendingMessageManager.pendingMessageStatus(parent.id)) + |> map { resourceStatus, pendingStatus in + if let pendingStatus = pendingStatus.0 { + return (.Fetching(isActive: true, progress: pendingStatus.progress), .Fetching(isActive: true, progress: pendingStatus.progress)) } else { - return resourceStatus + if file.isStreamable && parent.id.peerId.namespace != Namespaces.Peer.SecretChat { + return (.Local, resourceStatus) + } + return (resourceStatus, resourceStatus) } - } |> deliverOnMainQueue + } |> deliverOnMainQueue } else { - updatedStatusSignal = chatMessageFileStatus(account: account, file: file) |> deliverOnMainQueue + if file.resource is LocalFileVideoMediaResource { + updatedStatusSignal = .single((.Local, .Local)) + } else { + updatedStatusSignal = chatMessageFileStatus(account: context.account, file: file, approximateSynchronousValue: approximateSynchronousValue) |> deliverOnMainQueue |> map { [weak parent, weak file] status in + if let parent = parent, let file = file { + if file.isStreamable && parent.id.peerId.namespace != Namespaces.Peer.SecretChat { + return (.Local, status) + } + } + return (status, status) + } + } } } - let arguments = TransformImageArguments(corners: ImageCorners(radius:.cornerRadius), imageSize: dimensions, boundingSize: frame.size, intrinsicInsets: NSEdgeInsets()) - - self.image.set(arguments: arguments) - - if !animated { - self.image.setSignal(signal: cachedMedia(media: media, size: arguments.imageSize, scale: backingScaleFactor)) - } - + self.image.setSignal(signal: cachedMedia(media: media, arguments: arguments, scale: backingScaleFactor, positionFlags: positionFlags), clearInstantly: clearInstantly) + if let updateImageSignal = updateImageSignal { - self.image.setSignal(account: account, signal: updateImageSignal, clearInstantly: false, animate: true, cacheImage: { [weak self] image in - if let strongSelf = self { - return cacheMedia(signal: image, media: media, size: arguments.imageSize, scale: strongSelf.backingScaleFactor) - } else { - return .complete() + self.image.ignoreFullyLoad = true + self.image.setSignal( updateImageSignal, animate: !versionUpdated, cacheImage: { [weak media] result in + if let media = media { + cacheMedia(result, media: media, arguments: arguments, scale: System.backingScale, positionFlags: positionFlags) } }) } } + if let signal = updatedStatusSignal, let parent = parent, let parameters = parameters { + updatedStatusSignal = combineLatest(signal, parameters.getUpdatingMediaProgress(parent.id)) |> map { value, updating in + if let progress = updating { + return (.Fetching(isActive: true, progress: progress), .Fetching(isActive: true, progress: progress)) + } else { + return value + } + } + } - + self.image.set(arguments: arguments) + + self.image._change(size: size, animated: animated) + + if arguments.imageSize.width == arguments.boundingSize.width { + if let positionFlags = positionFlags { + autoplayVideoView?.view.positionFlags = positionFlags + } else { + autoplayVideoView?.view.positionFlags = nil + autoplayVideoView?.view.layer?.cornerRadius = .cornerRadius + } + } else { + autoplayVideoView?.view.positionFlags = nil + autoplayVideoView?.view.layer?.cornerRadius = 0 + } + + + var first: Bool = true if let updateStatusSignal = updatedStatusSignal { - self.statusDisposable.set(updateStatusSignal.start(next: { [weak self] (status) in + self.statusDisposable.set(updateStatusSignal.start(next: { [weak self] (status, authentic) in if let strongSelf = self { - strongSelf.fetchStatus = status + strongSelf.authenticFetchStatus = authentic + + + var authentic = authentic + if strongSelf.autoplayVideo { + strongSelf.fetchStatus = authentic + authentic = .Local + } else { + switch authentic { + case .Fetching: + strongSelf.fetchStatus = status + default: + strongSelf.fetchStatus = status + } + } + + + if let file = strongSelf.media as? TelegramMediaFile, strongSelf.autoplayVideo { + if strongSelf.autoplayVideoView == nil { + let autoplay: ChatVideoAutoplayView + + let fileReference = parent != nil ? FileMediaReference.message(message: MessageReference(parent!), media: file) : FileMediaReference.standalone(media: file) + + autoplay = ChatVideoAutoplayView(mediaPlayer: MediaPlayer(postbox: context.account.postbox, reference: fileReference.resourceReference(fileReference.media.resource), streamable: file.isStreamable, video: true, preferSoftwareDecoding: false, enableSound: false, volume: 0.0, fetchAutomatically: true), view: MediaPlayerView(backgroundThread: true)) + + strongSelf.autoplayVideoView = autoplay + if !strongSelf.blurBackground { + strongSelf.autoplayVideoView?.view.setVideoLayerGravity(.resizeAspectFill) + } else { + strongSelf.autoplayVideoView?.view.setVideoLayerGravity(.resize) + } + strongSelf.updatePlayerIfNeeded() + } + if let autoplay = strongSelf.autoplayVideoView { + let dimensions = (file.dimensions?.size ?? size) + let value = strongSelf.blurBackground ? dimensions.aspectFitted(size) : size + + autoplay.view.frame = NSMakeRect(0, 0, value.width, value.height) + if let positionFlags = positionFlags { + autoplay.view.positionFlags = positionFlags + } else { + autoplay.view.layer?.cornerRadius = .cornerRadius + } + strongSelf.addSubview(autoplay.view, positioned: .above, relativeTo: strongSelf.image) + autoplay.mediaPlayer.attachPlayerView(autoplay.view) + autoplay.view.center() + } + + } else { + strongSelf.autoplayVideoView = nil + } + + if let autoplay = strongSelf.autoplayVideoView { + strongSelf.mediaPlayerStatusDisposable.set((autoplay.mediaPlayer.status |> deliverOnMainQueue).start(next: { [weak strongSelf] status in + strongSelf?.updateMediaStatus(status, animated: !first) + })) + } + + + + if let file = media as? TelegramMediaFile, strongSelf.autoplayVideoView == nil { + strongSelf.updateVideoAccessory(parent == nil ? .Local : authentic, file: file, animated: !first) + first = false + } var containsSecretMedia:Bool = false if let message = parent { containsSecretMedia = message.containsSecretMedia } - if let _ = parent?.autoremoveAttribute?.countdownBeginTime { + if let autoremoveAttribute = parent?.autoremoveAttribute, autoremoveAttribute.timeout <= 60, autoremoveAttribute.countdownBeginTime != nil { strongSelf.progressView?.removeFromSuperview() strongSelf.progressView = nil if strongSelf.timableProgressView == nil { - strongSelf.timableProgressView = TimableProgressView() + strongSelf.timableProgressView = TimableProgressView(size: NSMakeSize(parent?.groupingKey != nil ? 30 : 40.0, parent?.groupingKey != nil ? 30 : 40.0)) strongSelf.addSubview(strongSelf.timableProgressView!) } } else { strongSelf.timableProgressView?.removeFromSuperview() strongSelf.timableProgressView = nil - if case .Local = status, media is TelegramMediaImage, !containsSecretMedia { + switch status { + case .Local: self?.image.animatesAlphaOnFirstTransition = false - - if let progressView = strongSelf.progressView { - progressView.state = .Fetching(progress:1.0, force: false) - progressView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion:false, completion: { [weak strongSelf] (completion) in - if completion { - progressView.removeFromSuperview() - strongSelf?.progressView = nil + default: + self?.image.animatesAlphaOnFirstTransition = false + } + + var removeProgress: Bool = strongSelf.autoplayVideo + if case .Local = status, media is TelegramMediaImage, !containsSecretMedia { + removeProgress = true + } + + if removeProgress { + if let progressView = strongSelf.progressView { + switch progressView.state { + case .Fetching: + progressView.state = .Fetching(progress:1.0, force: false) + case .ImpossibleFetching: + progressView.state = .ImpossibleFetching(progress:1.0, force: false) + default: + break + } + strongSelf.progressView = nil + progressView.layer?.animateAlpha(from: 1, to: 0, duration: 0.25, timingFunction: .linear, removeOnCompletion: false, completion: { [weak progressView] completed in + if completed { + progressView?.removeFromSuperview() } }) + } } else { - self?.image.animatesAlphaOnFirstTransition = true strongSelf.progressView?.layer?.removeAllAnimations() if strongSelf.progressView == nil { let progressView = RadialProgressView(theme:RadialProgressTheme(backgroundColor: .blackTransparent, foregroundColor: .white, icon: playerPlayThumb)) - progressView.frame = CGRect(origin: CGPoint(), size: CGSize(width: 40.0, height: 40.0)) + progressView.frame = CGRect(origin: CGPoint(), size: CGSize(width: parent?.groupingKey != nil ? 30 : 40.0, height: parent?.groupingKey != nil ? 30 : 40.0)) strongSelf.progressView = progressView strongSelf.addSubview(progressView) strongSelf.progressView?.center() @@ -202,15 +726,29 @@ class ChatInteractiveContentView: ChatMediaContentView { } + let progressStatus: MediaResourceStatus + if strongSelf.parent?.groupingKey != nil { + switch authentic { + case .Fetching: + progressStatus = authentic + default: + progressStatus = status + } + } else { + progressStatus = status + } + - - switch status { + switch progressStatus { case let .Fetching(_, progress): - strongSelf.progressView?.state = .Fetching(progress: progress, force: false) + + let sentGrouped = parent?.groupingKey != nil && (parent!.flags.contains(.Sending) || parent!.flags.contains(.Unsent)) + + strongSelf.progressView?.state = parent == nil ? .ImpossibleFetching(progress: progress, force: false) : (progress == 1.0 && sentGrouped ? .Success : .Fetching(progress: progress, force: false)) case .Local: var state: RadialProgressState = .None if containsSecretMedia { - state = .Icon(image: theme.icons.chatSecretThumb, mode:.destinationOut) + state = .Icon(image: parent?.groupingKey != nil ? theme.icons.chatSecretThumbSmall : theme.icons.chatSecretThumb, mode:.normal) if let attribute = parent?.autoremoveAttribute, let countdownBeginTime = attribute.countdownBeginTime { let difference:TimeInterval = TimeInterval((countdownBeginTime + attribute.timeout)) - (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) @@ -234,16 +772,18 @@ class ChatInteractiveContentView: ChatMediaContentView { } strongSelf.needsLayout = true } - })) - if media is TelegramMediaImage { - fetch() - } } } + override func change(size: NSSize, animated: Bool, _ save:Bool = true, removeOnCompletion: Bool = true, duration:Double = 0.2, timingFunction: CAMediaTimingFunctionName = CAMediaTimingFunctionName.easeOut, completion:((Bool)->Void)? = nil) { + super._change(size: size, animated: animated, save, removeOnCompletion: removeOnCompletion, duration: duration, timingFunction: timingFunction, completion: completion) + + image._change(size: size, animated: animated, save, removeOnCompletion: removeOnCompletion, duration: duration, timingFunction: timingFunction, completion: completion) + } + override func setContent(size: NSSize) { super.setContent(size: size) } @@ -257,22 +797,32 @@ class ChatInteractiveContentView: ChatMediaContentView { statusDisposable.set(nil) } - override func cancelFetching() { - if let account = account { - if let media = media as? TelegramMediaFile { - chatMessageFileCancelInteractiveFetch(account: account, file: media) - } else if let media = media as? TelegramMediaImage { - chatMessagePhotoCancelInteractiveFetch(account: account, photo: media) + + override func fetch() { + if let context = context { + if let media = media as? TelegramMediaFile, !media.isLocalResource { + if let parent = parent { + fetchDisposable.set(messageMediaFileInteractiveFetched(context: context, messageId: parent.id, fileReference: FileMediaReference.message(message: MessageReference(parent), media: media)).start()) + } else { + fetchDisposable.set(freeMediaFileInteractiveFetched(context: context, fileReference: FileMediaReference.standalone(media: media)).start()) + } + } else if let media = media as? TelegramMediaImage, !media.isLocalResource { + fetchDisposable.set(chatMessagePhotoInteractiveFetched(account: context.account, imageReference: parent != nil ? ImageMediaReference.message(message: MessageReference(parent!), media: media) : ImageMediaReference.standalone(media: media)).start()) } } - } - override func fetch() { - if let account = account { - if let media = media as? TelegramMediaFile { - fetchDisposable.set(chatMessageFileInteractiveFetched(account: account, file: media).start()) - } else if let media = media as? TelegramMediaImage { - fetchDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photo: media).start()) + + + override func preloadStreamblePart() { + if let context = context { + if let media = media as? TelegramMediaFile, let fileSize = media.size { + let reference = parent != nil ? FileMediaReference.message(message: MessageReference(parent!), media: media) : FileMediaReference.standalone(media: media) + + + let preload = combineLatest(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: reference.resourceReference(media.resource), range: (0 ..< Int(2.0 * 1024 * 1024), .default), statsCategory: .video), fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: reference.resourceReference(media.resource), range: (max(0, fileSize - Int(256 * 1024)) ..< Int(Int32.max), .default), statsCategory: .video)) + + partDisposable.set(preload.start()) + } } } @@ -281,6 +831,8 @@ class ChatInteractiveContentView: ChatMediaContentView { override func copy() -> Any { return image.copy() } - + override var contents: Any? { + return image.layer?.contents + } } diff --git a/Telegram-Mac/ChatInterfaceInputContext.swift b/Telegram-Mac/ChatInterfaceInputContext.swift index 8f70a0942d..a9c2ffeb14 100644 --- a/Telegram-Mac/ChatInterfaceInputContext.swift +++ b/Telegram-Mac/ChatInterfaceInputContext.swift @@ -7,8 +7,9 @@ // import Cocoa -import TelegramCoreMac -import PostboxMac +import TelegramCore + +import Postbox struct PossibleContextQueryTypes: OptionSet { var rawValue: Int32 @@ -27,6 +28,7 @@ struct PossibleContextQueryTypes: OptionSet { static let contextRequest = PossibleContextQueryTypes(rawValue: (1 << 3)) static let stickers = PossibleContextQueryTypes(rawValue: (1 << 4)) static let emoji = PossibleContextQueryTypes(rawValue: (1 << 5)) + static let emojiFast = PossibleContextQueryTypes(rawValue: (1 << 6)) } private func makeScalar(_ c: Character) -> Character { @@ -80,15 +82,8 @@ func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState, in } } - if inputText.isSingleEmoji { - var inputText = inputText - if inputText.canHaveSkinToneModifier { - inputText = inputText.emojiUnmodified - } - return (inputText.startIndex ..< inputText.endIndex, [.stickers], nil) - } - let maxUtfIndex = inputText.utf16.index(inputText.utf16.startIndex, offsetBy: inputState.selectionRange.lowerBound) + let maxUtfIndex = inputText.utf16.index(inputText.utf16.startIndex, offsetBy: min(inputState.selectionRange.lowerBound, inputText.utf16.count)) guard let maxIndex = maxUtfIndex.samePosition(in: inputText) else { return nil } @@ -97,9 +92,19 @@ func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState, in } var index = inputText.index(before: maxIndex) + if inputText.length <= 7, inputText.isSingleEmoji { + var inputText = inputText + if inputText.canHaveSkinToneModifier { + inputText = inputText.emojiUnmodified + } + return (inputText.startIndex ..< maxIndex, [.stickers], nil) + } + + + var possibleQueryRange: Range? - var possibleTypes = PossibleContextQueryTypes([.command, .mention, .emoji]) + var possibleTypes = PossibleContextQueryTypes([.command, .mention, .emoji, .hashtag, .emojiFast]) //var possibleTypes = PossibleContextQueryTypes([.command, .mention]) @@ -107,15 +112,38 @@ func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState, in func check() { if inputText.startIndex != inputText.index(before: index) { let prev = inputText.index(before: inputText.index(before: index)) - if (inputText[prev] != spaceScalar && inputText[prev] != newlineScalar) { + let scalars:CharacterSet = CharacterSet.alphanumerics + if let scalar = inputText[prev].unicodeScalars.first, scalars.contains(scalar) && inputText[prev] != newlineScalar { possibleTypes = [] } + switch possibleTypes { + case .emoji: + if index != inputText.endIndex { + if let scalar = inputText[index].unicodeScalars.first { + if !scalars.contains(scalar) { + possibleTypes = [] + } + } else { + possibleTypes = [] + } + } else { + // possibleTypes = [] + } + + default: + break + } } } var definedType = false - while true { + var characterSet = CharacterSet.alphanumerics + characterSet.insert(hashScalar.unicodeScalars.first!) + characterSet.insert(atScalar.unicodeScalars.first!) + characterSet.insert(slashScalar.unicodeScalars.first!) + characterSet.insert(emojiScalar.unicodeScalars.first!) + for _ in 0 ..< 20 { let c = inputText[index] @@ -123,7 +151,7 @@ func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState, in //if index == inputText.startIndex { //|| (inputText[inputText.index(before: index)] == spaceScalar || inputText[inputText.index(before: index)] == newlineScalar) - if c == spaceScalar || c == newlineScalar { + if !characterSet.contains(c.unicodeScalars.first!) { possibleTypes = [] } else if c == hashScalar { possibleTypes = possibleTypes.intersection([.hashtag]) @@ -173,6 +201,13 @@ func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState, in } } + if inputText.trimmingCharacters(in: CharacterSet.letters).isEmpty, !inputText.isEmpty { + possibleTypes = possibleTypes.intersection([.emojiFast]) + definedType = true + possibleQueryRange = index ..< maxIndex + } + + if let possibleQueryRange = possibleQueryRange, definedType && !possibleTypes.isEmpty { return (possibleQueryRange, possibleTypes, nil) } @@ -183,23 +218,57 @@ func textInputStateContextQueryRangeAndType(_ inputState: ChatTextInputState, in func inputContextQueryForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, includeContext: Bool) -> ChatPresentationInputQuery { let inputState = chatPresentationInterfaceState.effectiveInput if let (possibleQueryRange, possibleTypes, additionalStringRange) = textInputStateContextQueryRangeAndType(inputState, includeContext: includeContext) { - let query = String(inputState.inputText[possibleQueryRange]) //.substring(with: possibleQueryRange) + + if chatPresentationInterfaceState.state == .editing && (possibleTypes != [.contextRequest] && possibleTypes != [.mention] && possibleTypes != [.emoji]) { + return .none + } + + var possibleQueryRange = possibleQueryRange +// if possibleQueryRange.upperBound > inputState.inputText.endIndex { +// possibleQueryRange = possibleQueryRange.lowerBound ..< inputState.inputText.endIndex +// } + +// possibleQueryRange.lowerBound.encodedOffset +// +// if let index = inputState.inputText.index(possibleQueryRange.upperBound, offsetBy: 0, limitedBy: inputState.inputText.endIndex) { +// possibleQueryRange = possibleQueryRange.lowerBound ..< index +// } else { +// return .none +// } + + if chatPresentationInterfaceState.botMenu?.revealed == true { + return .command("") + } + + let value = inputState.inputText[possibleQueryRange] + let query = String(value) if possibleTypes == [.hashtag] { return .hashtag(query) } else if possibleTypes == [.mention] { - return .mention(query: query, includeRecent: inputState.inputText.startIndex == inputState.inputText.index(before: possibleQueryRange.lowerBound)) + return .mention(query: query, includeRecent: inputState.inputText.startIndex == inputState.inputText.index(before: possibleQueryRange.lowerBound) && chatPresentationInterfaceState.state == .normal) } else if possibleTypes == [.command] { return .command(query) } else if possibleTypes == [.contextRequest], let additionalStringRange = additionalStringRange { - let additionalString = inputState.inputText.substring(with: additionalStringRange) + let additionalString = String(inputState.inputText[additionalStringRange]) return .contextRequest(addressName: query, query: additionalString) - } else if possibleTypes == [.stickers], chatPresentationInterfaceState.editState == nil { - return .stickers(query) + } else if possibleTypes == [.stickers] { + return .stickers(query.emojiUnmodified) } else if possibleTypes == [.emoji] { - return .emoji(query) + if query.trimmingCharacters(in: CharacterSet.letters).isEmpty { + return .emoji(query, firstWord: false) + } else { + return .none + } + } else if possibleTypes == [.emojiFast] { + return .emoji(query, firstWord: true) } return .none + } else { - return .none + if chatPresentationInterfaceState.botMenu?.revealed == true { + return .command("") + } else { + return .none + } } } diff --git a/Telegram-Mac/ChatInterfaceInteraction.swift b/Telegram-Mac/ChatInterfaceInteraction.swift index 36ab82a8d2..2d14a63c5f 100644 --- a/Telegram-Mac/ChatInterfaceInteraction.swift +++ b/Telegram-Mac/ChatInterfaceInteraction.swift @@ -7,17 +7,18 @@ // import Cocoa -import PostboxMac -import TelegramCoreMac -import TGUIKit -import SwiftSignalKitMac +import Postbox +import TelegramCore +import TGUIKit +import SwiftSignalKit +import MapKit final class ReplyMarkupInteractions { - let proccess:(ReplyMarkupButton, (Bool)->Void) -> Void + let proccess:(ReplyMarkupButton, @escaping(Bool)->Void) -> Void - init(proccess:@escaping (ReplyMarkupButton, (Bool)->Void)->Void) { + init(proccess:@escaping (ReplyMarkupButton, @escaping(Bool)->Void)->Void) { self.proccess = proccess } @@ -26,24 +27,43 @@ final class ReplyMarkupInteractions { final class ChatInteraction : InterfaceObserver { - let peerId:PeerId - let account:Account + let chatLocation: ChatLocation + let mode: ChatMode + var peerId : PeerId { + return chatLocation.peerId + } + + var activitySpace: PeerActivitySpace { + return .init(peerId: peerId, category: mode.activityCategory) + } + + var peer: Peer? { + return presentation.peer + } + + let context: AccountContext let isLogInteraction:Bool + let disableSelectAbility: Bool + let isGlobalSearchMessage: Bool private let modifyDisposable:MetaDisposable = MetaDisposable() private let mediaDisposable:MetaDisposable = MetaDisposable() private let startBotDisposable:MetaDisposable = MetaDisposable() private let addContactDisposable:MetaDisposable = MetaDisposable() private let requestSessionId:MetaDisposable = MetaDisposable() + let editDisposable = MetaDisposable() private let disableProxyDisposable = MetaDisposable() private let enableProxyDisposable = MetaDisposable() - init(peerId:PeerId, account:Account, isLogInteraction: Bool = false) { - self.peerId = peerId - self.account = account + init(chatLocation: ChatLocation, context: AccountContext, mode: ChatMode = .history, isLogInteraction: Bool = false, disableSelectAbility: Bool = false, isGlobalSearchMessage: Bool = false) { + self.chatLocation = chatLocation + self.context = context + self.disableSelectAbility = disableSelectAbility self.isLogInteraction = isLogInteraction - self.presentation = ChatPresentationInterfaceState() + self.isGlobalSearchMessage = isGlobalSearchMessage + self.presentation = ChatPresentationInterfaceState(chatLocation: chatLocation, chatMode: mode) + self.mode = mode super.init() - let signal = mediaPromise.get() |> deliverOnMainQueue |> mapToQueue { [weak self] (media) -> Signal in + let signal = mediaPromise.get() |> deliverOnMainQueue |> mapToQueue { [weak self] (media) -> Signal in self?.sendMedia(media) return .single(Void()) } @@ -60,20 +80,27 @@ final class ChatInteraction : InterfaceObserver { } } + var withToggledSelectedMessage:((ChatPresentationInterfaceState)->ChatPresentationInterfaceState)->Void = { _ in } + var setupReplyMessage: (MessageId?) -> Void = {_ in} var beginMessageSelection: (MessageId?) -> Void = {_ in} var deleteMessages: ([MessageId]) -> Void = {_ in } var forwardMessages: ([MessageId]) -> Void = {_ in} - var sendMessage: () -> Void = {} - var forceSendMessage: (String) -> Void = {_ in} + var reportMessages:(ReportReasonValue, [MessageId]) -> Void = { _, _ in } + var sendMessage: (Bool, Date?) -> Void = { _, _ in } + var sendPlainText: (String) -> Void = {_ in} + // var focusMessageId: (MessageId?, MessageId, TableScrollState) -> Void = {_,_,_ in} // from, to, animated, position + var focusPinnedMessageId: (MessageId) -> Void = { _ in} // from, to, animated, position var sendMedia:([MediaSenderContainer]) -> Void = {_ in} - var sendAppFile:(TelegramMediaFile) -> Void = {_ in} + var sendAppFile:(TelegramMediaFile, Bool, String?) -> Void = { _,_, _ in} + var sendMedias:([Media], ChatTextInputState, Bool, ChatTextInputState?, Bool, Date?) -> Void = {_,_,_,_,_,_ in} + var focusInputField:()->Void = {} var openInfo:(PeerId, Bool, MessageId?, ChatInitialAction?) -> Void = {_,_,_,_ in} // peerId, isNeedOpenChat, postId, initialAction var beginEditingMessage:(Message?) -> Void = {_ in} - var requestMessageActionCallback:(MessageId, Bool, MemoryBuffer?) -> Void = {_,_,_ in} + var requestMessageActionCallback:(MessageId, Bool, (requiresPassword: Bool, data: MemoryBuffer)?) -> Void = {_,_,_ in} var inlineAudioPlayer:(APController) -> Void = {_ in} var movePeerToInput:(Peer) -> Void = {_ in} var sendInlineResult:(ChatContextResultCollection,ChatContextResult) -> Void = {_,_ in} @@ -84,40 +111,149 @@ final class ChatInteraction : InterfaceObserver { var sendCommand:(PeerCommand)->Void = {_ in } var setNavigationAction:(NavigationModalAction)->Void = {_ in} var switchInlinePeer:(PeerId, ChatInitialAction)->Void = {_,_ in} - var showPreviewSender:([URL], Bool)->Void = {_,_ in} - var setSecretChatMessageAutoremoveTimeout:(Int32?)->Void = {_ in} - var toggleNotifications:()->Void = {} + var showPreviewSender:([URL], Bool, NSAttributedString?)->Void = {_,_,_ in} + var setChatMessageAutoremoveTimeout:(Int32?)->Void = {_ in} + var toggleNotifications:(Bool?)->Void = { _ in } var removeAndCloseChat:()->Void = {} var joinChannel:()->Void = {} var returnGroup:()->Void = {} var shareContact:(TelegramUser)->Void = {_ in} var unblock:()->Void = {} - var updatePinned:(MessageId, Bool, Bool)->Void = {_,_,_ in} + var updatePinned:(MessageId, Bool, Bool, Bool)->Void = {_,_,_,_ in} var reportSpamAndClose:()->Void = {} - var dismissPeerReport:()->Void = {} + var dismissPeerStatusOptions:()->Void = {} var toggleSidebar:()->Void = {} var mentionPressed:()->Void = {} var jumpToDate:(Date)->Void = {_ in} + var openFeedInfo: (PeerGroupId)->Void = {_ in} + var showNextPost:()->Void = {} + var startRecording:(Bool, NSView?)->Void = {_,_ in} + var openProxySettings: ()->Void = {} + var sendLocation: (CLLocationCoordinate2D, MapVenue?) -> Void = {_, _ in} + var clearMentions:()->Void = {} + var attachFile:(Bool)->Void = { _ in } + var attachPhotoOrVideo:()->Void = {} + var attachPicture:()->Void = {} + var attachLocation:()->Void = {} + var updateEditingMessageMedia:([String]?, Bool) -> Void = { _, _ in} + var editEditingMessagePhoto:(TelegramMediaImage) -> Void = { _ in} + var removeChatInteractively:()->Void = { } + var updateSearchRequest: (SearchMessagesResultState)->Void = { _ in } + var searchPeerMessages: (Peer) -> Void = { _ in } + var vote:(MessageId, [Data], Bool) -> Void = { _, _, _ in } + var closePoll:(MessageId) -> Void = { _ in } + var openDiscussion:()->Void = { } + var addContact:()->Void = {} + var blockContact: ()->Void = {} + var openScheduledMessages: ()->Void = {} + var openBank: (String)->Void = { _ in } + var getGradientOffsetRect:()->NSRect = { return .zero } + var contextHolder:()->Atomic = { Atomic(value: nil) } + + var openFocusedMedia:(Int32?)->Void = { _ in return } + + var push:(ViewController)->Void = { _ in } + var back:()->Void = { } + + var openPinnedMessages: (MessageId)->Void = { _ in } + var unpinAllMessages: ()->Void = {} + var setLocation: (ChatHistoryLocation)->Void = { _ in } + var scrollToTheFirst: () -> Void = {} + var openReplyThread:(MessageId, Bool, Bool, ReplyThreadMode)->Void = { _, _, _, _ in } + + var joinGroupCall:(CachedChannelData.ActiveCall, String?)->Void = { _, _ in } + + var runEmojiScreenEffect:(String, MessageId, Bool, Bool)->Void = { _, _, _, _ in } + + + var getCachedData:()->CachedPeerData? = { return nil } + + var showDeleterSetup:(Control)->Void = { _ in } + + func chatLocationInput() -> ChatLocationInput { + return context.chatLocationInput(for: self.chatLocation, contextHolder: contextHolder()) + } + + var unarchive: ()->Void = { } + + var closeAfterPeek:(Int32)->Void = { _ in } + + var updateReactions: (MessageId, String, @escaping(Bool)->Void)->Void = { _, _, _ in } + + let loadingMessage: Promise = Promise() let mediaPromise:Promise<[MediaSenderContainer]> = Promise() - func addContact() { - addContactDisposable.set(addContactPeerInteractively(account: account, peerId: peerId, phone: (presentation.peer as? TelegramUser)?.phone).start()) + var hasSetDestructiveTimer: Bool { + + if mode != .history { + return false + } + + if !self.presentation.interfaceState.inputState.inputText.isEmpty { + return false + } + if self.peerId.namespace == Namespaces.Peer.SecretChat { + return true + } + if let value = self.presentation.messageSecretTimeout { + switch value { + case let .known(value): + if value != nil { + return true + } + default: + return false + } + } + + return false } + + /* + var hasSetDestructiveTimer: Bool { + if self.peerId.namespace == Namespaces.Peer.SecretChat { + return true + } + if let peer = presentation.peer { + if let peer = peer as? TelegramChannel, peer.isSupergroup { + return peer.groupAccess.canEditGroupInfo + } + if let value = self.presentation.messageSecretTimeout { + switch value { + case let .known(value): + if value != nil { + return true + } + default: + return false + } + } + if let peer = peer as? TelegramGroup { + switch peer.role { + case .admin, .creator: + return true + default: + break + } + } + } + + return false + } + + + */ + func disableProxy() { - let account = self.account - disableProxyDisposable.set((account.postbox.preferencesView(keys: [PreferencesKeys.proxySettings]) |> take(1) |> map { prefs -> ProxySettings? in - return prefs.values[PreferencesKeys.proxySettings] as? ProxySettings - } |> deliverOnMainQueue |> mapToSignal { setting in - return confirmSignal(for: mainWindow, header: appName, information: tr(.proxyForceDisable(setting?.host ?? ""))) - } |> filter {$0} |> mapToSignal { _ in - return applyProxySettings(postbox: account.postbox, network: account.network, settings: nil) + disableProxyDisposable.set(updateProxySettingsInteractively(accountManager: context.sharedContext.accountManager, { current -> ProxySettings in + return current.withUpdatedEnabled(false) }).start()) } - func applyProxy(_ proxy:ProxySettings) -> Void { - applyExternalProxy(proxy, postbox: account.postbox, network: account.network) + func applyProxy(_ server:ProxyServerSettings) -> Void { + applyExternalProxy(server, accountManager: context.sharedContext.accountManager) } @@ -129,16 +265,15 @@ final class ChatInteraction : InterfaceObserver { } else { peerId = peer.id } - requestSessionId.set((phoneCall(account, peerId: peerId) |> deliverOnMainQueue).start(next: { [weak self] result in - if let strongSelf = self { - applyUIPCallResult(strongSelf.account, result) - } + let context = self.context + requestSessionId.set((phoneCall(account: context.account, sharedContext: context.sharedContext, peerId: peerId) |> deliverOnMainQueue).start(next: { result in + applyUIPCallResult(context.sharedContext, result) })) } } func startBot(_ payload:String? = nil) { - startBotDisposable.set((requestStartBot(account: self.account, botPeerId: self.peerId, payload: payload) |> deliverOnMainQueue).start(completed: { [weak self] in + startBotDisposable.set((context.engine.messages.requestStartBot(botPeerId: self.peerId, payload: payload) |> deliverOnMainQueue).start(completed: { [weak self] in self?.update({$0.updatedInitialAction(nil)}) })) } @@ -167,15 +302,22 @@ final class ChatInteraction : InterfaceObserver { } func updateInput(with text:String) { - let state = ChatTextInputState(inputText: text, selectionRange: text.length ..< text.length, attributes: []) - self.update({$0.updatedInterfaceState({$0.withUpdatedInputState(state)})}) + if self.presentation.state == .normal { + let state = ChatTextInputState(inputText: text, selectionRange: text.length ..< text.length, attributes: []) + self.update({$0.updatedInterfaceState({$0.withUpdatedInputState(state)})}) + } } func appendText(_ text:String, selectedRange:Range? = nil) -> Range { + + + var selectedRange = selectedRange ?? presentation.effectiveInput.selectionRange let inputText = presentation.effectiveInput.attributedString.mutableCopy() as! NSMutableAttributedString - + if self.presentation.state != .normal && presentation.state != .editing { + return selectedRange.lowerBound ..< selectedRange.lowerBound + } if selectedRange.upperBound - selectedRange.lowerBound > 0 { // let minUtfIndex = inputText.utf16.index(inputText.utf16.startIndex, offsetBy: selectedRange.lowerBound) @@ -187,7 +329,7 @@ final class ChatInteraction : InterfaceObserver { inputText.replaceCharacters(in: NSMakeRange(selectedRange.lowerBound, selectedRange.upperBound - selectedRange.lowerBound), with: NSAttributedString(string: text)) selectedRange = selectedRange.lowerBound ..< selectedRange.lowerBound } else { - inputText.insert(NSAttributedString(string: text), at: selectedRange.lowerBound) + inputText.insert(NSAttributedString(string: text, font: .normal(theme.fontSize)), at: selectedRange.lowerBound) } @@ -206,7 +348,32 @@ final class ChatInteraction : InterfaceObserver { return selectedRange.lowerBound ..< selectedRange.lowerBound + text.length } - func invokeInitialAction(includeAuto:Bool = false) { + func cancelEditing(_ force: Bool = false) { + if let editState = self.presentation.interfaceState.editState { + let oldState = ChatEditState(message: editState.message) + if force { + self.update({$0.withoutEditMessage().updatedUrlPreview(nil)}) + } else { + switch editState.loadingState { + case .loading, .progress: + editDisposable.set(nil) + self.update({$0.updatedInterfaceState({$0.updatedEditState({$0?.withUpdatedLoadingState(.none)})})}) + return + default: + if oldState.inputState.attributedString != editState.inputState.attributedString, !editState.inputState.attributedString.string.isEmpty { + confirm(for: context.window, information: L10n.chatEditCancelText, okTitle: L10n.alertDiscard, cancelTitle: L10n.alertNO, successHandler: { [weak self] _ in + self?.update({$0.withoutEditMessage().updatedUrlPreview(nil)}) + }) + } else { + self.update({$0.withoutEditMessage().updatedUrlPreview(nil)}) + } + } + } + } + + } + + func invokeInitialAction(includeAuto:Bool = false, animated: Bool = true) { if let action = presentation.initialAction { switch action { case let .start(parameter: parameter, behavior: behavior): @@ -221,8 +388,10 @@ final class ChatInteraction : InterfaceObserver { } if invoke { startBot(parameter) + update({ + $0.withoutInitialAction() + }) } - case let .inputText(text: text, behavior: behavior): var invoke:Bool = !includeAuto if includeAuto { @@ -235,6 +404,9 @@ final class ChatInteraction : InterfaceObserver { } if invoke { updateInput(with: text) + update({ + $0.withoutInitialAction() + }) } case let .files(list: list, behavior: behavior): var invoke:Bool = !includeAuto @@ -247,9 +419,76 @@ final class ChatInteraction : InterfaceObserver { } } if invoke { - showPreviewSender( list.map { URL(fileURLWithPath: $0) }, true ) + showPreviewSender( list.map { URL(fileURLWithPath: $0) }, true, nil ) + update({ + $0.withoutInitialAction() + }) + } + case let .forward(messageIds, inputState, _): + update(animated: animated, {$0.updatedInterfaceState({$0.withUpdatedForwardMessageIds(messageIds).withUpdatedInputState(inputState ?? $0.inputState)})}) + update({ + $0.withoutInitialAction() + }) + case .ad: + break + case .source: + break + case let .closeAfter(peek): + break + case let .openMedia(timemark): + self.openFocusedMedia(timemark) + update({ + $0.withoutInitialAction() + }) + case let .selectToReport(reason): + update(animated: animated, { + $0.withSelectionState().withoutInitialAction().withUpdatedRepotMode(reason) + }) + case let .joinVoiceChat(joinHash): + update(animated: animated, { + $0.updatedGroupCall { $0?.withUpdatedJoinHash(joinHash) }.withoutInitialAction() + }) + + let peerId = self.peerId + let context = self.context + + let joinCall:(GroupCallPanelData)->Void = { [weak self] data in + if data.groupCall?.call.peerId != peerId, let peer = self?.peer { + showModal(with: JoinVoiceChatAlertController(context: context, groupCall: data, peer: peer, join: { [weak self] in + if let call = data.info { + self?.joinGroupCall(CachedChannelData.ActiveCall(id: call.id, accessHash: call.accessHash, title: call.title, scheduleTimestamp: call.scheduleTimestamp, subscribedToScheduled: call.subscribedToScheduled), joinHash) + } + }), for: context.window) + } else { + if let call = data.info { + self?.joinGroupCall(CachedChannelData.ActiveCall(id: call.id, accessHash: call.accessHash, title: call.title, scheduleTimestamp: call.scheduleTimestamp, subscribedToScheduled: call.subscribedToScheduled), joinHash) + } + } + } + let call: Signal = context.engine.calls.updatedCurrentPeerGroupCall(peerId: peerId) |> mapToSignalPromotingError { call -> Signal in + if let call = call { + return context.engine.calls.getCurrentGroupCall(callId: call.id, accessHash: call.accessHash, peerId: peerId) + } else { + return .single(nil) + } + } |> mapToSignal { data in + if let data = data { + return context.sharedContext.groupCallContext |> take(1) |> mapToSignalPromotingError { groupCallContext in + return .single(GroupCallPanelData(peerId: peerId, info: data.info, topParticipants: data.topParticipants, participantCount: data.info.participantCount, activeSpeakers: [], groupCall: groupCallContext)) + } + } else { + return .single(nil) + } } + _ = showModalProgress(signal: call, for: context.window).start(next: { data in + if let data = data { + joinCall(data) + } else { + alert(for: context.window, info: L10n.chatVoiceChatJoinLinkUnavailable) + } + }) } + } } @@ -261,31 +500,69 @@ final class ChatInteraction : InterfaceObserver { if let strongSelf = self { switch button.action { case let .url(url): - execute(inapp: inApp(for: url.nsstring, account: strongSelf.account, openInfo: strongSelf.openInfo, hashtag: strongSelf.modalSearch, command: strongSelf.forceSendMessage)) + execute(inapp: inApp(for: url.nsstring, context: strongSelf.context, openInfo: strongSelf.openInfo, hashtag: strongSelf.modalSearch, command: strongSelf.sendPlainText, applyProxy: strongSelf.applyProxy, confirm: true)) case .text: - _ = (enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [EnqueueMessage.message(text: button.title, attributes: [], media: nil, replyToMessageId: strongSelf.presentation.interfaceState.messageActionsState.processedSetupReplyMessageId)]) |> deliverOnMainQueue).start(next: { [weak strongSelf] _ in + _ = (enqueueMessages(account: strongSelf.context.account, peerId: strongSelf.peerId, messages: [EnqueueMessage.message(text: button.title, attributes: [], mediaReference: nil, replyToMessageId: strongSelf.presentation.interfaceState.messageActionsState.processedSetupReplyMessageId, localGroupingKey: nil, correlationId: nil)]) |> deliverOnMainQueue).start(next: { [weak strongSelf] _ in strongSelf?.scrollToLatest(true) }) case .requestPhone: - strongSelf.shareSelfContact(nil) + FastSettings.requstPermission(with: .contact, for: keyboardMessage.id.peerId, success: { [weak strongSelf] in + strongSelf?.shareSelfContact(nil) + if attribute.flags.contains(.once) { + strongSelf?.update({$0.updatedInterfaceState({$0.withUpdatedMessageActionsState({$0.withUpdatedClosedButtonKeyboardMessageId(keyboardMessage.id)})})}) + } + }) + + return case .openWebApp: strongSelf.requestMessageActionCallback(keyboardMessage.id, true, nil) case let .callback(data): strongSelf.requestMessageActionCallback(keyboardMessage.id, false, data) case let .switchInline(samePeer: same, query: query): - let text = "@\(keyboardMessage.inlinePeer?.username ?? "") \(query)" + let text = "@\(keyboardMessage.inlinePeer?.username ?? keyboardMessage.author?.username ?? "") \(query)" if same { strongSelf.updateInput(with: text) } else { - if let peer = keyboardMessage.inlinePeer { - strongSelf.account.context.mainNavigation?.set(modalAction: ShareInlineResultNavigationAction(payload: text, botName: peer.displayTitle), strongSelf.account.context.layout != .single) - if strongSelf.account.context.layout == .single { - strongSelf.account.context.mainNavigation?.push(ForwardChatListController(strongSelf.account)) + if let peer = keyboardMessage.inlinePeer ?? keyboardMessage.effectiveAuthor { + strongSelf.context.sharedContext.bindings.rootNavigation().set(modalAction: ShareInlineResultNavigationAction(payload: text, botName: peer.displayTitle), strongSelf.context.sharedContext.layout != .single) + if strongSelf.context.sharedContext.layout == .single { + strongSelf.context.sharedContext.bindings.rootNavigation().push(ForwardChatListController(strongSelf.context)) } } + } case .payment: - alert(for: mainWindow, info: tr(.paymentsUnsupported)) + let receiptMessageId = (keyboardMessage.media.first as? TelegramMediaInvoice)?.receiptMessageId + if let receiptMessageId = receiptMessageId { + showModal(with: PaymentsReceiptController(context: strongSelf.context, messageId: receiptMessageId, message: keyboardMessage), for: strongSelf.context.window) + } else { + showModal(with: PaymentsCheckoutController(context: strongSelf.context, message: keyboardMessage), for: strongSelf.context.window) + } + case let .urlAuth(url, buttonId): + let context = strongSelf.context + _ = showModalProgress(signal: context.engine.messages.requestMessageActionUrlAuth(subject: .message(id: keyboardMessage.id, buttonId: buttonId)), for: context.window).start(next: { result in + switch result { + case let .accepted(url): + execute(inapp: inApp(for: url.nsstring, context: strongSelf.context, openInfo: strongSelf.openInfo, hashtag: strongSelf.modalSearch, command: strongSelf.sendPlainText, applyProxy: strongSelf.applyProxy)) + case .default: + execute(inapp: inApp(for: url.nsstring, context: strongSelf.context, openInfo: strongSelf.openInfo, hashtag: strongSelf.modalSearch, command: strongSelf.sendPlainText, applyProxy: strongSelf.applyProxy, confirm: true)) + case let .request(requestURL, peer, writeAllowed): + showModal(with: InlineLoginController(context: context, url: requestURL, originalURL: url, writeAllowed: writeAllowed, botPeer: peer, authorize: { allowWriteAccess in + _ = showModalProgress(signal: context.engine.messages.acceptMessageActionUrlAuth(subject: .message(id: keyboardMessage.id, buttonId: buttonId), allowWriteAccess: allowWriteAccess), for: context.window).start(next: { result in + switch result { + case .default: + execute(inapp: inApp(for: url.nsstring, context: strongSelf.context, openInfo: strongSelf.openInfo, hashtag: strongSelf.modalSearch, command: strongSelf.sendPlainText, applyProxy: strongSelf.applyProxy, confirm: true)) + case let .accepted(url): + execute(inapp: inApp(for: url.nsstring, context: strongSelf.context, openInfo: strongSelf.openInfo, hashtag: strongSelf.modalSearch, command: strongSelf.sendPlainText, applyProxy: strongSelf.applyProxy)) + default: + break + } + }) + }), for: context.window) + } + }) + case let .setupPoll(isQuiz): + showModal(with: NewPollController(chatInteraction: strongSelf, isQuiz: isQuiz), for: strongSelf.context.window) default: break } @@ -298,29 +575,54 @@ final class ChatInteraction : InterfaceObserver { return ReplyMarkupInteractions(proccess: {_,_ in}) } - public func saveState(_ force:Bool = true, scrollState: ChatInterfaceHistoryScrollState? = nil) { - + + + public func saveState(_ force:Bool = true, scrollState: ChatInterfaceHistoryScrollState? = nil, sync: Bool = false) { + let peerId = self.peerId + let context = self.context let timestamp = Int32(Date().timeIntervalSince1970) let interfaceState = presentation.interfaceState.withUpdatedTimestamp(timestamp).withUpdatedHistoryScrollState(scrollState) - var s:Signal = updatePeerChatInterfaceState(account: account, peerId: peerId, state: interfaceState) - if !force { + let updatedOpaqueData = try? EngineEncoder.encode(interfaceState) + + var s:Signal = context.engine.peers.setOpaqueChatInterfaceState(peerId: peerId, threadId: mode.threadId64, state: .init(opaqueData: updatedOpaqueData, historyScrollMessageIndex: interfaceState.historyScrollMessageIndex, synchronizeableInputState: interfaceState.synchronizeableInputState)) + + if !force && !interfaceState.inputState.inputText.isEmpty { s = s |> delay(10, queue: Queue.mainQueue()) } - modifyDisposable.set(s.start()) + let semaphore = DispatchSemaphore(value: 0) + + let disposable = s.start(completed: { + context.setChatInterfaceTempState(ChatInterfaceTempState(editState: interfaceState.editState), for: peerId) + semaphore.signal() + }) + modifyDisposable.set(disposable) + + if sync { + semaphore.wait() + } } deinit { + clean() + } + + func clean() { + addContactDisposable.dispose() mediaDisposable.dispose() startBotDisposable.dispose() requestSessionId.dispose() disableProxyDisposable.dispose() enableProxyDisposable.dispose() + editDisposable.dispose() + update({ _ in + return ChatPresentationInterfaceState(chatLocation: self.chatLocation, chatMode: self.mode) + }) } diff --git a/Telegram-Mac/ChatInterfaceState.swift b/Telegram-Mac/ChatInterfaceState.swift index 4e02c130a7..34ec6fe15c 100644 --- a/Telegram-Mac/ChatInterfaceState.swift +++ b/Telegram-Mac/ChatInterfaceState.swift @@ -8,49 +8,55 @@ // import Cocoa +import TGUIKit +import Postbox +import SwiftSignalKit +import TelegramCore -import PostboxMac -import SwiftSignalKitMac -import TelegramCoreMac +struct ChatTextFontAttributes: OptionSet { + var rawValue: Int32 = 0 -struct ChatInterfaceSelectionState: PostboxCoding, Equatable { + static let bold = ChatTextFontAttributes(rawValue: 1 << 0) + static let italic = ChatTextFontAttributes(rawValue: 1 << 1) + static let monospace = ChatTextFontAttributes(rawValue: 1 << 2) + static let blockQuote = ChatTextFontAttributes(rawValue: 1 << 3) +} + + + +struct ChatInterfaceSelectionState: Equatable { let selectedIds: Set - - static func ==(lhs: ChatInterfaceSelectionState, rhs: ChatInterfaceSelectionState) -> Bool { - return lhs.selectedIds == rhs.selectedIds - } - - init(selectedIds: Set) { + let lastSelectedId: MessageId? + + init(selectedIds: Set, lastSelectedId: MessageId?) { self.selectedIds = selectedIds + self.lastSelectedId = lastSelectedId } - - init(decoder: PostboxDecoder) { - if let data = decoder.decodeBytesForKeyNoCopy("i") { - self.selectedIds = Set(MessageId.decodeArrayFromBuffer(data)) - } else { - self.selectedIds = Set() - } + func withUpdatedSelectedIds(_ ids: Set) -> ChatInterfaceSelectionState { + return ChatInterfaceSelectionState(selectedIds: ids, lastSelectedId: self.lastSelectedId) } - - func encode(_ encoder: PostboxEncoder) { - let buffer = WriteBuffer() - MessageId.encodeArrayToBuffer(Array(selectedIds), buffer: buffer) - encoder.encodeBytes(buffer, forKey: "i") + func withUpdatedLastSelected(_ lastSelectedId: MessageId?) -> ChatInterfaceSelectionState { + return ChatInterfaceSelectionState(selectedIds: self.selectedIds, lastSelectedId: lastSelectedId) } } -enum ChatTextInputAttribute : Equatable, PostboxCoding { +enum ChatTextInputAttribute : Equatable, Comparable, Codable { case bold(Range) + case strikethrough(Range) case italic(Range) case pre(Range) case code(Range) - case uid(Range, Int32) - - init(decoder: PostboxDecoder) { - let range = Range(Int(decoder.decodeInt32ForKey("start", orElse: 0)) ..< Int(decoder.decodeInt32ForKey("end", orElse: 0))) + case uid(Range, Int64) + case url(Range, String) + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) - let type: Int32 = decoder.decodeInt32ForKey("_rawValue", orElse: 0) + let lowerBound = Int(try container.decode(Int32.self, forKey: "start")) + let upperBound = Int(try container.decode(Int32.self, forKey: "end")) + let range = lowerBound ..< upperBound + + let type: Int32 = try container.decode(Int32.self, forKey: "_rawValue") switch type { case 0: self = .bold(range) @@ -59,94 +65,123 @@ enum ChatTextInputAttribute : Equatable, PostboxCoding { case 2: self = .pre(range) case 3: - self = .uid(range, decoder.decodeInt32ForKey("uid", orElse: 0)) + self = .uid(range, try container.decode(Int64.self, forKey: "uid")) case 4: self = .code(range) + case 5: + self = .url(range, try container.decode(String.self, forKey: "url")) + case 6: + self = .strikethrough(range) default: fatalError("input attribute not supported") } } + var weight: Int { + switch self { + case .bold: + return 0 + case .italic: + return 1 + case .pre: + return 2 + case .code: + return 3 + case .strikethrough: + return 4 + case .uid: + return 5 + case .url: + return 6 + } + } - func encode(_ encoder: PostboxEncoder) { - encoder.encodeInt32(Int32(self.range.lowerBound), forKey: "start") - encoder.encodeInt32(Int32(self.range.upperBound), forKey: "end") + static func <(lhs: ChatTextInputAttribute, rhs: ChatTextInputAttribute) -> Bool { + if lhs.weight != rhs.weight { + return lhs.weight < rhs.weight + } + return lhs.range.lowerBound < rhs.range.lowerBound + } + + func encode(to encoder: Encoder) throws { + + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encode(Int32(self.range.lowerBound), forKey: "start") + try container.encode(Int32(self.range.upperBound), forKey: "end") switch self { case .bold: - encoder.encodeInt32(0, forKey: "_rawValue") + try container.encode(Int32(0), forKey: "_rawValue") case .italic: - encoder.encodeInt32(1, forKey: "_rawValue") + try container.encode(Int32(1), forKey: "_rawValue") case .pre: - encoder.encodeInt32(2, forKey: "_rawValue") + try container.encode(Int32(2), forKey: "_rawValue") case .code: - encoder.encodeInt32(4, forKey: "_rawValue") + try container.encode(Int32(4), forKey: "_rawValue") + case .strikethrough: + try container.encode(Int32(6), forKey: "_rawValue") case let .uid(_, uid): - encoder.encodeInt32(3, forKey: "_rawValue") - encoder.encodeInt32(uid, forKey: "uid") + try container.encode(Int32(3), forKey: "_rawValue") + try container.encode(uid, forKey: "uid") + case let .url(_, url): + try container.encode(Int32(5), forKey: "_rawValue") + try container.encode(url, forKey: "url") } } - + } extension ChatTextInputAttribute { var attribute:(String, Any, NSRange) { switch self { case let .bold(range): - return (NSAttributedStringKey.font.rawValue, NSFont.bold(.text), NSMakeRange(range.lowerBound, range.upperBound - range.lowerBound)) + return (NSAttributedString.Key.font.rawValue, NSFont.bold(.text), NSMakeRange(range.lowerBound, range.upperBound - range.lowerBound)) + case let .strikethrough(range): + return (NSAttributedString.Key.font.rawValue, NSFont.normal(.text), NSMakeRange(range.lowerBound, range.upperBound - range.lowerBound)) case let .italic(range): - return (NSAttributedStringKey.font.rawValue, NSFontManager.shared.convert(.normal(.text), toHaveTrait: .italicFontMask), NSMakeRange(range.lowerBound, range.upperBound - range.lowerBound)) + return (NSAttributedString.Key.font.rawValue, NSFontManager.shared.convert(.normal(.text), toHaveTrait: .italicFontMask), NSMakeRange(range.lowerBound, range.upperBound - range.lowerBound)) case let .pre(range), let .code(range): - return (NSAttributedStringKey.font.rawValue, NSFont.code(.text), NSMakeRange(range.lowerBound, range.upperBound - range.lowerBound)) + return (NSAttributedString.Key.font.rawValue, NSFont.code(.text), NSMakeRange(range.lowerBound, range.upperBound - range.lowerBound)) case let .uid(range, uid): - let tag = TGInputTextTag(uniqueId: Int64(arc4random()), attachment: NSNumber(value: uid), attribute: TGInputTextAttribute(name: NSAttributedStringKey.foregroundColor.rawValue, value: theme.colors.link)) - return (TGMentionUidAttributeName, tag, NSMakeRange(range.lowerBound, range.upperBound - range.lowerBound)) + let tag = TGInputTextTag(uniqueId: Int64(arc4random()), attachment: NSNumber(value: uid), attribute: TGInputTextAttribute(name: NSAttributedString.Key.foregroundColor.rawValue, value: theme.colors.link)) + return (TGCustomLinkAttributeName, tag, NSMakeRange(range.lowerBound, range.upperBound - range.lowerBound)) + case let .url(range, url): + let tag = TGInputTextTag(uniqueId: Int64(arc4random()), attachment: url, attribute: TGInputTextAttribute(name: NSAttributedString.Key.foregroundColor.rawValue, value: theme.colors.link)) + return (TGCustomLinkAttributeName, tag, NSMakeRange(range.lowerBound, range.upperBound - range.lowerBound)) } } - + var range:Range { switch self { - case let .bold(range), let .italic(range), let .pre(range), let .code(range): + case let .bold(range), let .italic(range), let .pre(range), let .code(range), let .strikethrough(range): return range case let .uid(range, _): return range + case let .url(range, _): + return range } } -} - -func ==(lhs: ChatTextInputAttribute, rhs: ChatTextInputAttribute) -> Bool { - switch lhs { - case let .bold(range): - if case .bold(range) = rhs { - return true - } else { - return false - } - case let .italic(range): - if case .italic(range) = rhs { - return true - } else { - return false - } - case let .pre(range): - if case .pre(range) = rhs { - return true - } else { - return false - } - case let .code(range): - if case .code(range) = rhs { - return true - } else { - return false - } - case let .uid(range, uid): - if case .uid(range, uid) = rhs { - return true - } else { - return false + + func updateRange(_ range: Range) -> ChatTextInputAttribute { + switch self { + case .bold: + return .bold(range) + case .italic: + return .italic(range) + case .pre: + return .pre(range) + case .code: + return .code(range) + case .strikethrough: + return .strikethrough(range) + case let .uid(_, uid): + return .uid(range, uid) + case let .url(_, url): + return .url(range, url) } } } + func chatTextAttributes(from entities:TextEntitiesMessageAttribute) -> [ChatTextInputAttribute] { var inputAttributes:[ChatTextInputAttribute] = [] for entity in entities.entities { @@ -160,7 +195,11 @@ func chatTextAttributes(from entities:TextEntitiesMessageAttribute) -> [ChatText case .Pre: inputAttributes.append(.pre(entity.range)) case let .TextMention(peerId: peerId): - inputAttributes.append(.uid(entity.range, peerId.id)) + inputAttributes.append(.uid(entity.range, peerId.id._internalGetInt64Value())) + case let .TextUrl(url): + inputAttributes.append(.url(entity.range, url)) + case .Strikethrough: + inputAttributes.append(.strikethrough(entity.range)) default: break } @@ -169,169 +208,393 @@ func chatTextAttributes(from entities:TextEntitiesMessageAttribute) -> [ChatText } func chatTextAttributes(from attributed:NSAttributedString) -> [ChatTextInputAttribute] { - + var inputAttributes:[ChatTextInputAttribute] = [] - - attributed.enumerateAttribute(NSAttributedStringKey.font, in: NSMakeRange(0, attributed.length), options: .init(rawValue: 0)) { font, range, _ in - if let font = font as? NSFont { - let descriptor = font.fontDescriptor - let symTraits = descriptor.symbolicTraits - let traitSet = NSFontTraitMask(rawValue: UInt(symTraits.rawValue)) - let isBold = traitSet.contains(.boldFontMask) - let isItalic = traitSet.contains(.italicFontMask) - let isMonospace = font.fontName == "Menlo-Regular" - - if isBold { - inputAttributes.append(.bold(range.location ..< range.location + range.length)) - } else if isItalic { - inputAttributes.append(.italic(range.location ..< range.location + range.length)) - } else if isMonospace { - inputAttributes.append(.code(range.location ..< range.location + range.length)) + + + attributed.enumerateAttributes(in: attributed.range, options: []) { (keys, range, _) in + for (_, value) in keys { + if let font = value as? NSFont { + let descriptor = font.fontDescriptor + let symTraits = descriptor.symbolicTraits + let traitSet = NSFontTraitMask(rawValue: UInt(symTraits.rawValue)) + let isBold = traitSet.contains(.boldFontMask) + let isItalic = traitSet.contains(.italicFontMask) + let isMonospace = font.fontName == "Menlo-Regular" + + if isItalic { + inputAttributes.append(.italic(range.location ..< range.location + range.length)) + } + if isBold { + inputAttributes.append(.bold(range.location ..< range.location + range.length)) + } + if isMonospace { + inputAttributes.append(.code(range.location ..< range.location + range.length)) + } + } else if let tag = value as? TGInputTextTag { + if let uid = tag.attachment as? NSNumber { + inputAttributes.append(.uid(range.location ..< range.location + range.length, uid.int64Value)) + } else if let url = tag.attachment as? String { + inputAttributes.append(.url(range.location ..< range.location + range.length, url)) + } } } } - - attributed.enumerateAttribute(NSAttributedStringKey(rawValue: TGMentionUidAttributeName), in: NSMakeRange(0, attributed.length), options: .init(rawValue: 0)) { tag, range, _ in - if let tag = tag as? TGInputTextTag, let uid = tag.attachment as? NSNumber { - inputAttributes.append(.uid(range.location ..< range.location + range.length, uid.int32Value)) - } - } - return inputAttributes + + + return Array(inputAttributes.prefix(100)) } -private let markdownRegexFormat = "(^|\\s)(````?)([\\s\\S]+?)(````?)([\\s\\n\\.,:?!;]|$)|(^|\\s)(`)([^\\n]+?)\\7([\\s\\.,:?!;]|$)" +//x/m +private let markdownRegexFormat = "(^|\\s|\\n)(````?)([\\s\\S]+?)(````?)([\\s\\n\\.,:?!;]|$)|(^|\\s)(`|\\*\\*|__|~~)([^\\n]+?)\\7([\\s\\.,:?!;]|$)|@(\\d+)\\s*\\((.+?)\\)" + + private let markdownRegex = try? NSRegularExpression(pattern: markdownRegexFormat, options: [.caseInsensitive, .anchorsMatchLines]) -struct ChatTextInputState: PostboxCoding, Equatable { +final class ChatTextInputState: Codable, Equatable { + static func == (lhs: ChatTextInputState, rhs: ChatTextInputState) -> Bool { + return lhs.selectionRange == rhs.selectionRange && lhs.attributes == rhs.attributes && lhs.inputText == rhs.inputText + } + let inputText: String let attributes:[ChatTextInputAttribute] let selectionRange: Range - - static func ==(lhs: ChatTextInputState, rhs: ChatTextInputState) -> Bool { - return lhs.inputText == rhs.inputText && lhs.selectionRange == rhs.selectionRange && lhs.attributes == rhs.attributes - } - + init() { self.inputText = "" self.selectionRange = 0 ..< 0 self.attributes = [] } - + init(inputText: String, selectionRange: Range, attributes:[ChatTextInputAttribute]) { self.inputText = inputText self.selectionRange = selectionRange - self.attributes = attributes + self.attributes = attributes.sorted(by: <) } - + init(inputText: String) { self.inputText = inputText self.selectionRange = inputText.length ..< inputText.length self.attributes = [] } - - init(decoder: PostboxDecoder) { - self.inputText = decoder.decodeStringForKey("t", orElse: "") - self.selectionRange = Int(decoder.decodeInt32ForKey("s0", orElse: 0)) ..< Int(decoder.decodeInt32ForKey("s1", orElse: 0)) - self.attributes = decoder.decodeObjectArrayWithDecoderForKey("t.a") + + init(from decoder: Decoder) throws { + + let container = try decoder.container(keyedBy: StringCodingKey.self) + + self.inputText = try container.decode(String.self, forKey: "t") + let lowerBound = try container.decode(Int32.self, forKey: "s0") + let upperBound = try container.decode(Int32.self, forKey: "s1") + + self.selectionRange = Int(lowerBound) ..< Int(upperBound) + self.attributes = try container.decode([ChatTextInputAttribute].self, forKey: "t.a") } - - func encode(_ encoder: PostboxEncoder) { - encoder.encodeString(self.inputText, forKey: "t") - encoder.encodeInt32(Int32(self.selectionRange.lowerBound), forKey: "s0") - encoder.encodeInt32(Int32(self.selectionRange.upperBound), forKey: "s1") - encoder.encodeObjectArray(self.attributes, forKey: "t.a") + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encode(self.inputText, forKey: "t") + try container.encode(Int32(self.selectionRange.lowerBound), forKey: "s0") + try container.encode(Int32(self.selectionRange.upperBound), forKey: "s1") + try container.encode(self.attributes, forKey: "t.a") } - + var attributedString:NSAttributedString { let string = NSMutableAttributedString() - _ = string.append(string: inputText, color: theme.colors.text, font: .normal(.text), coreText: false) + _ = string.append(string: inputText, color: theme.colors.text, font: .normal(theme.fontSize), coreText: false) + + + string.fixEmojiesFont(theme.fontSize) + + var fontAttributes: [NSRange: ChatTextFontAttributes] = [:] + + loop: for attribute in attributes { + let attr = attribute.attribute + + inner: switch attribute { + case .bold: + if let fontAttribute = fontAttributes[attr.2] { + fontAttributes[attr.2] = fontAttribute.union(.bold) + } else { + fontAttributes[attr.2] = .bold + } + continue loop + case .italic: + if let fontAttribute = fontAttributes[attr.2] { + fontAttributes[attr.2] = fontAttribute.union(.italic) + } else { + fontAttributes[attr.2] = .italic + } + continue loop + case .pre, .code: + if let fontAttribute = fontAttributes[attr.2] { + fontAttributes[attr.2] = fontAttribute.union(.monospace) + } else { + fontAttributes[attr.2] = .monospace + } + continue loop + default: + break inner + } + + string.addAttribute(NSAttributedString.Key(rawValue: attr.0), value: attr.1, range: attr.2) + } + for (range, fontAttributes) in fontAttributes { + var font: NSFont? + if fontAttributes.contains(.blockQuote) { + font = .code(theme.fontSize) + } else if fontAttributes == [.bold, .italic] { + font = .boldItalic(theme.fontSize) + } else if fontAttributes == [.bold] { + font = .bold(theme.fontSize) + } else if fontAttributes == [.italic] { + font = .italic(theme.fontSize) + }else if fontAttributes == [.monospace] { + font = .code(theme.fontSize) + } + if let font = font { + string.addAttribute(.font, value: font, range: range) + } + } + return string.copy() as! NSAttributedString + } + + func makeAttributeString(addPreAsBlock: Bool = false) -> NSAttributedString { + let string = NSMutableAttributedString() + _ = string.append(string: inputText, color: theme.colors.text, font: .normal(theme.fontSize), coreText: false) + var pres:[Range] = [] + var strikethrough:[Range] = [] + for attribute in attributes { let attr = attribute.attribute - string.addAttribute(NSAttributedStringKey(rawValue: attr.0), value: attr.1, range: attr.2) + + switch attribute { + case let .pre(range): + if addPreAsBlock { + pres.append(range) + } else { + string.addAttribute(NSAttributedString.Key(rawValue: attr.0), value: attr.1, range: attr.2) + } + case let .strikethrough(range): + strikethrough.append(range) + default: + string.addAttribute(NSAttributedString.Key(rawValue: attr.0), value: attr.1, range: attr.2) + } } + if addPreAsBlock { + var offset: Int = 0 + for pre in pres.sorted(by: { $0.lowerBound < $1.lowerBound }) { + let symbols = "```" + string.insert(.initialize(string: symbols, color: theme.colors.text, font: .normal(theme.fontSize), coreText: false), at: pre.lowerBound + offset) + offset += symbols.count + string.insert(.initialize(string: symbols, color: theme.colors.text, font: .normal(theme.fontSize), coreText: false), at: pre.upperBound + offset) + offset += symbols.count + } + for strikethrough in strikethrough.sorted(by: { $0.lowerBound < $1.lowerBound }) { + let symbols = "~~" + string.insert(.initialize(string: symbols, color: theme.colors.text, font: .normal(theme.fontSize), coreText: false), at: strikethrough.lowerBound + offset) + offset += symbols.count + string.insert(.initialize(string: symbols, color: theme.colors.text, font: .normal(theme.fontSize), coreText: false), at: strikethrough.upperBound + offset) + offset += symbols.count + } + } + return string.copy() as! NSAttributedString } - - + + func subInputState(from range: NSRange) -> ChatTextInputState { - - var subText = inputText.nsstring.substring(with: range) - - var raw:String = subText - + + var subText = attributedString.attributedSubstring(from: range).trimmed + + let localAttributes = chatTextAttributes(from: subText) + + + var raw:String = subText.string + var appliedText = subText.string var attributes:[ChatTextInputAttribute] = [] - - + + var offsetRanges:[NSRange] = [] if let regex = markdownRegex { - + + var skipIndexes:Set = Set() + if !localAttributes.isEmpty { + var index: Int = 0 + let matches = regex.matches(in: subText.string, range: NSMakeRange(0, subText.string.length)) + for match in matches { + for attr in localAttributes { + let range = match.range + let attrRange = NSMakeRange(attr.range.lowerBound, attr.range.upperBound - attr.range.lowerBound) + if attrRange.intersection(range) != nil { + skipIndexes.insert(index) + } + } + index += 1 + } + } + + var rawOffset:Int = 0 var newText:[String] = [] + var index: Int = 0 while let match = regex.firstMatch(in: raw, range: NSMakeRange(0, raw.length)) { - let matchIndex = rawOffset + match.range.location - newText.append(raw.nsstring.substring(with: NSMakeRange(0, match.range.location))) + + let matchIndex = rawOffset + match.range.location + + + + newText.append(raw.nsstring.substring(with: NSMakeRange(0, match.range.location))) + var pre = match.range(at: 3) - - + + if pre.location != NSNotFound { - let text = raw.nsstring.substring(with: pre) - - rawOffset -= match.range(at: 2).length + match.range(at: 4).length - newText.append(raw.nsstring.substring(with: match.range(at: 1)) + text + raw.nsstring.substring(with: match.range(at: 5))) - attributes.append(.pre(matchIndex + match.range(at: 1).length ..< matchIndex + match.range(at: 1).length + text.length)) + if !skipIndexes.contains(index) { + let text = raw.nsstring.substring(with: pre) + + rawOffset -= match.range(at: 2).length + match.range(at: 4).length + newText.append(raw.nsstring.substring(with: match.range(at: 1)) + text + raw.nsstring.substring(with: match.range(at: 5))) + attributes.append(.pre(matchIndex + match.range(at: 1).length ..< matchIndex + match.range(at: 1).length + text.length)) + offsetRanges.append(NSMakeRange(matchIndex + match.range(at: 1).length, 3)) + offsetRanges.append(NSMakeRange(matchIndex + match.range(at: 1).length + text.length + 3, 3)) + } else { + let text = raw.nsstring.substring(with: pre) + let entity = raw.nsstring.substring(with: match.range(at: 2)) + newText.append(raw.nsstring.substring(with: match.range(at: 1)) + entity + text + entity + raw.nsstring.substring(with: match.range(at: 5))) + } } - + pre = match.range(at: 8) if pre.location != NSNotFound { let text = raw.nsstring.substring(with: pre) - - newText.append(raw.nsstring.substring(with: match.range(at: 6)) + text + raw.nsstring.substring(with: match.range(at: 9))) - attributes.append(.code(matchIndex + match.range(at: 6).length ..< matchIndex + match.range(at: 6).length + text.length)) - - rawOffset -= match.range(at: 7).length * 2 + if !skipIndexes.contains(index) { + + let left = match.range(at: 6) + + let entity = raw.nsstring.substring(with: match.range(at: 7)) + newText.append(raw.nsstring.substring(with: left) + text + raw.nsstring.substring(with: match.range(at: 9))) + + + switch entity { + case "`": + attributes.append(.code(matchIndex + left.length ..< matchIndex + left.length + text.length)) + case "**": + attributes.append(.bold(matchIndex + left.length ..< matchIndex + left.length + text.length)) + case "~~": + attributes.append(.strikethrough(matchIndex + left.length ..< matchIndex + left.length + text.length)) + case "__": + attributes.append(.italic(matchIndex + left.length ..< matchIndex + left.length + text.length)) + default: + break + } + + offsetRanges.append(NSMakeRange(matchIndex + left.length, entity.length)) + offsetRanges.append(NSMakeRange(matchIndex + left.length + text.length, entity.length)) + + rawOffset -= match.range(at: 7).length * 2 + } else { + let entity = raw.nsstring.substring(with: match.range(at: 7)) + newText.append(raw.nsstring.substring(with: match.range(at: 6)) + entity + text + entity + raw.nsstring.substring(with: match.range(at: 9))) + } } - - raw = raw.nsstring.substring(from: match.range.location + match.range(at: 0).length) rawOffset += match.range.location + match.range(at: 0).length - + + index += 1 } - + newText.append(raw) - subText = newText.joined() + appliedText = newText.joined() } - - - - for attr in self.attributes { - let newRange = Range(attr.range.lowerBound - range.location ..< attr.range.upperBound - range.location) - if newRange.lowerBound >= range.location && newRange.upperBound <= range.location + range.length { + + + + for attr in localAttributes { + var newRange = NSMakeRange(attr.range.lowerBound, (attr.range.upperBound - attr.range.lowerBound)) + for offsetRange in offsetRanges { + if offsetRange.location < newRange.location { + newRange.location -= offsetRange.length + } +// if newRange.intersection(offsetRange) != nil { +// newRange.length -= offsetRange.length +// } + } + + + //if newRange.lowerBound >= range.location && newRange.upperBound <= range.location + range.length { switch attr { case .bold: - attributes.append(.bold(newRange)) + attributes.append(.bold(newRange.min ..< newRange.max)) case .italic: - attributes.append(.italic(newRange)) + attributes.append(.italic(newRange.min ..< newRange.max)) case .pre: - attributes.append(.pre(newRange)) + attributes.append(.pre(newRange.min ..< newRange.max)) case .code: - attributes.append(.code(newRange)) + attributes.append(.code(newRange.min ..< newRange.max)) + case .strikethrough: + attributes.append(.strikethrough(newRange.min ..< newRange.max)) case let .uid(_, uid): - attributes.append(.uid(newRange, uid)) + attributes.append(.uid(newRange.min ..< newRange.max, uid)) + case let .url(_, url): + attributes.append(.url(newRange.min ..< newRange.max, url)) + } + // } + } + + let charset = CharacterSet.whitespacesAndNewlines + + while !appliedText.isEmpty, let range = appliedText.rangeOfCharacter(from: charset), range.lowerBound == appliedText.startIndex { + + let oldLength = appliedText.length + appliedText.removeSubrange(range) + let newLength = appliedText.length + + let symbolLength = oldLength - newLength + + for (i, attr) in attributes.enumerated() { + let updated: ChatTextInputAttribute + if attr.range.lowerBound == 0 { + updated = attr.updateRange(0 ..< max(attr.range.upperBound - symbolLength, 0)) + } else { + updated = attr.updateRange(attr.range.lowerBound - symbolLength ..< max(attr.range.upperBound - symbolLength, attr.range.lowerBound - symbolLength)) } + attributes[i] = updated } } - return ChatTextInputState(inputText: subText, selectionRange: 0 ..< 0, attributes: attributes) - } - + + + while !appliedText.isEmpty, let range = appliedText.rangeOfCharacter(from: charset, options: [], range: appliedText.index(before: appliedText.endIndex) ..< appliedText.endIndex), range.upperBound == appliedText.endIndex { + + let oldLength = appliedText.length + appliedText.removeSubrange(range) + let newLength = appliedText.length + + let symbolLength = oldLength - newLength + + for (i, attr) in attributes.enumerated() { + let updated: ChatTextInputAttribute + NSLog("\(attr.range.lowerBound), \(attr.range.upperBound)") + updated = attr.updateRange(attr.range.lowerBound ..< max(attr.range.upperBound - symbolLength, attr.range.lowerBound)) + attributes[i] = updated + } + } - var messageTextEntities:[MessageTextEntity] { + + return ChatTextInputState(inputText: appliedText, selectionRange: 0 ..< 0, attributes: attributes) + } + + + func messageTextEntities(_ detectLinks: ParsingType = [.Hashtags]) -> [MessageTextEntity] { var entities:[MessageTextEntity] = [] for attribute in attributes { switch attribute { case let .bold(range): entities.append(.init(range: range, type: .Bold)) + case let .strikethrough(range): + entities.append(.init(range: range, type: .Strikethrough)) case let .italic(range): entities.append(.init(range: range, type: .Italic)) case let .pre(range): @@ -339,134 +602,168 @@ struct ChatTextInputState: PostboxCoding, Equatable { case let .code(range): entities.append(.init(range: range, type: .Code)) case let .uid(range, uid): - entities.append(.init(range: range, type: .TextMention(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: uid)))) + entities.append(.init(range: range, type: .TextMention(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(uid))))) + case let .url(range, url): + entities.append(.init(range: range, type: .TextUrl(url: url))) } } + + let attr = NSMutableAttributedString(string: inputText) + attr.detectLinks(type: detectLinks) + + attr.enumerateAttribute(NSAttributedString.Key.link, in: attr.range, options: NSAttributedString.EnumerationOptions(rawValue: 0), using: { (value, range, stop) in + if let value = value as? inAppLink { + switch value { + case let .external(link, _): + if link.hasPrefix("#") { + entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Hashtag)) + } else if detectLinks.contains(.Links) { + entities.append(MessageTextEntity(range: range.lowerBound ..< range.upperBound, type: .Url)) + } + default: + break + } + } + }) + return entities } - - + + } -struct ChatInterfaceMessageActionsState: PostboxCoding, Equatable { +struct ChatInterfaceMessageActionsState: Codable, Equatable { let closedButtonKeyboardMessageId: MessageId? let processedSetupReplyMessageId: MessageId? - + var isEmpty: Bool { return self.closedButtonKeyboardMessageId == nil && self.processedSetupReplyMessageId == nil } - + init() { self.closedButtonKeyboardMessageId = nil self.processedSetupReplyMessageId = nil } - + init(closedButtonKeyboardMessageId: MessageId?, processedSetupReplyMessageId: MessageId?) { self.closedButtonKeyboardMessageId = closedButtonKeyboardMessageId self.processedSetupReplyMessageId = processedSetupReplyMessageId } - - init(decoder: PostboxDecoder) { - if let closedMessageIdPeerId = (decoder.decodeOptionalInt64ForKey("cb.p") as Int64?), let closedMessageIdNamespace = (decoder.decodeOptionalInt32ForKey("cb.n") as Int32?), let closedMessageIdId = (decoder.decodeOptionalInt32ForKey("cb.i") as Int32?) { + + init(from decoder: Decoder) throws { + + let container = try decoder.container(keyedBy: StringCodingKey.self) + + if let closedMessageIdPeerId = (try? container.decodeIfPresent(Int64.self, forKey: "cb.p")), let closedMessageIdNamespace = (try? container.decodeIfPresent(Int32.self, forKey: "cb.n")), let closedMessageIdId = (try? container.decodeIfPresent(Int32.self, forKey: "cb.i")) { self.closedButtonKeyboardMessageId = MessageId(peerId: PeerId(closedMessageIdPeerId), namespace: closedMessageIdNamespace, id: closedMessageIdId) } else { self.closedButtonKeyboardMessageId = nil } - - if let processedMessageIdPeerId = (decoder.decodeOptionalInt64ForKey("pb.p") as Int64?), let processedMessageIdNamespace = (decoder.decodeOptionalInt32ForKey("pb.n") as Int32?), let processedMessageIdId = (decoder.decodeOptionalInt32ForKey("pb.i") as Int32?) { + + if let processedMessageIdPeerId = (try? container.decodeIfPresent(Int64.self, forKey: "pb.p")), let processedMessageIdNamespace = (try? container.decodeIfPresent(Int32.self, forKey: "pb.n")), let processedMessageIdId = (try? container.decodeIfPresent(Int32.self, forKey: "pb.i")) { self.processedSetupReplyMessageId = MessageId(peerId: PeerId(processedMessageIdPeerId), namespace: processedMessageIdNamespace, id: processedMessageIdId) } else { self.processedSetupReplyMessageId = nil } } - - func encode(_ encoder: PostboxEncoder) { - if let closedButtonKeyboardMessageId = self.closedButtonKeyboardMessageId { - encoder.encodeInt64(closedButtonKeyboardMessageId.peerId.toInt64(), forKey: "cb.p") - encoder.encodeInt32(closedButtonKeyboardMessageId.namespace, forKey: "cb.n") - encoder.encodeInt32(closedButtonKeyboardMessageId.id, forKey: "cb.i") + + func encode(to encoder: Encoder) throws { + + var container = encoder.container(keyedBy: StringCodingKey.self) + + + + if let id = self.closedButtonKeyboardMessageId { + try container.encode(id.peerId.toInt64(), forKey: "cb.p") + try container.encode(id.namespace, forKey: "cb.n") + try container.encode(id.id, forKey: "cb.i") } else { - encoder.encodeNil(forKey: "cb.p") - encoder.encodeNil(forKey: "cb.n") - encoder.encodeNil(forKey: "cb.i") + try container.encodeNil(forKey: "cb.p") + try container.encodeNil(forKey: "cb.n") + try container.encodeNil(forKey: "cb.i") } - + if let processedSetupReplyMessageId = self.processedSetupReplyMessageId { - encoder.encodeInt64(processedSetupReplyMessageId.peerId.toInt64(), forKey: "pb.p") - encoder.encodeInt32(processedSetupReplyMessageId.namespace, forKey: "pb.n") - encoder.encodeInt32(processedSetupReplyMessageId.id, forKey: "pb.i") + try container.encode(processedSetupReplyMessageId.peerId.toInt64(), forKey: "pb.p") + try container.encode(processedSetupReplyMessageId.namespace, forKey: "pb.n") + try container.encode(processedSetupReplyMessageId.id, forKey: "pb.i") } else { - encoder.encodeNil(forKey: "pb.p") - encoder.encodeNil(forKey: "pb.n") - encoder.encodeNil(forKey: "pb.i") + try container.encodeNil(forKey: "pb.p") + try container.encodeNil(forKey: "pb.n") + try container.encodeNil(forKey: "pb.i") } } - - static func ==(lhs: ChatInterfaceMessageActionsState, rhs: ChatInterfaceMessageActionsState) -> Bool { - return lhs.closedButtonKeyboardMessageId == rhs.closedButtonKeyboardMessageId && lhs.processedSetupReplyMessageId == rhs.processedSetupReplyMessageId - } - + + func withUpdatedClosedButtonKeyboardMessageId(_ closedButtonKeyboardMessageId: MessageId?) -> ChatInterfaceMessageActionsState { return ChatInterfaceMessageActionsState(closedButtonKeyboardMessageId: closedButtonKeyboardMessageId, processedSetupReplyMessageId: self.processedSetupReplyMessageId) } - + func withUpdatedProcessedSetupReplyMessageId(_ processedSetupReplyMessageId: MessageId?) -> ChatInterfaceMessageActionsState { return ChatInterfaceMessageActionsState(closedButtonKeyboardMessageId: self.closedButtonKeyboardMessageId, processedSetupReplyMessageId: processedSetupReplyMessageId) } } -final class ChatEmbeddedInterfaceState: PeerChatListEmbeddedInterfaceState { +final class ChatEmbeddedInterfaceState { let timestamp: Int32 let text: String - + init(timestamp: Int32, text: String) { self.timestamp = timestamp self.text = text } - + init(decoder: PostboxDecoder) { self.timestamp = decoder.decodeInt32ForKey("d", orElse: 0) self.text = decoder.decodeStringForKey("t", orElse: "") } - + func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.timestamp, forKey: "d") encoder.encodeString(self.text, forKey: "t") } - - public func isEqual(to: PeerChatListEmbeddedInterfaceState) -> Bool { - if let to = to as? ChatEmbeddedInterfaceState { - return self.timestamp == to.timestamp && self.text == to.text - } else { - return false - } + + public func isEqual(to: ChatEmbeddedInterfaceState) -> Bool { + return self.timestamp == to.timestamp && self.text == to.text } } -struct ChatInterfaceHistoryScrollState: PostboxCoding, Equatable { +struct ChatInterfaceHistoryScrollState: Codable, Equatable { let messageIndex: MessageIndex let relativeOffset: Double - + init(messageIndex: MessageIndex, relativeOffset: Double) { self.messageIndex = messageIndex self.relativeOffset = relativeOffset } - - init(decoder: PostboxDecoder) { - self.messageIndex = MessageIndex(id: MessageId(peerId: PeerId(decoder.decodeInt64ForKey("m.p", orElse: 0)), namespace: decoder.decodeInt32ForKey("m.n", orElse: 0), id: decoder.decodeInt32ForKey("m.i", orElse: 0)), timestamp: decoder.decodeInt32ForKey("m.t", orElse: 0)) - self.relativeOffset = decoder.decodeDoubleForKey("ro", orElse: 0.0) + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + + let peerId = PeerId(try container.decode(Int64.self, forKey: "m.p")) + let namespace = try container.decode(Int32.self, forKey: "m.n") + let id = try container.decode(Int32.self, forKey: "m.i") + let messageId = MessageId(peerId: peerId, namespace: namespace, id: id) + let timestamp = try container.decode(Int32.self, forKey: "m.t") + + self.messageIndex = MessageIndex(id: messageId, timestamp: timestamp) + self.relativeOffset = try container.decode(Double.self, forKey: "ro") } - - func encode(_ encoder: PostboxEncoder) { - encoder.encodeInt32(self.messageIndex.timestamp, forKey: "m.t") - encoder.encodeInt64(self.messageIndex.id.peerId.toInt64(), forKey: "m.p") - encoder.encodeInt32(self.messageIndex.id.namespace, forKey: "m.n") - encoder.encodeInt32(self.messageIndex.id.id, forKey: "m.i") - encoder.encodeDouble(self.relativeOffset, forKey: "ro") + + func encode(to encoder: Encoder) throws { + + var container = encoder.container(keyedBy: StringCodingKey.self) + + + try container.encode(self.messageIndex.timestamp, forKey: "m.t") + try container.encode(self.messageIndex.id.peerId.toInt64(), forKey: "m.p") + try container.encode(self.messageIndex.id.namespace, forKey: "m.n") + try container.encode(self.messageIndex.id.id, forKey: "m.i") + try container.encode(self.relativeOffset, forKey: "ro") } - + static func ==(lhs: ChatInterfaceHistoryScrollState, rhs: ChatInterfaceHistoryScrollState) -> Bool { if lhs.messageIndex != rhs.messageIndex { return false @@ -478,58 +775,231 @@ struct ChatInterfaceHistoryScrollState: PostboxCoding, Equatable { } } +enum EditStateLoading : Equatable { + case none + case loading + case progress(Float) +} + +final class ChatEditState : Equatable { + let inputState:ChatTextInputState + let originalMedia: Media? + let message:Message + let editMedia: RequestEditMessageMedia + let loadingState: EditStateLoading + let editedData: EditedImageData? + + init(message:Message, originalMedia: Media? = nil, state:ChatTextInputState? = nil, loadingState: EditStateLoading = .none, editMedia: RequestEditMessageMedia = .keep, editedData: EditedImageData? = nil) { + self.message = message + if originalMedia == nil { + self.originalMedia = message.media.first + } else { + self.originalMedia = originalMedia + } + if let state = state { + self.inputState = state + } else { + var attribute:TextEntitiesMessageAttribute? + for attr in message.attributes { + if let attr = attr as? TextEntitiesMessageAttribute { + attribute = attr + } + } + var attributes:[ChatTextInputAttribute] = [] + if let attribute = attribute { + attributes = chatTextAttributes(from: attribute) + } + let temporaryState = ChatTextInputState(inputText:message.text, selectionRange: 0 ..< 0, attributes: attributes) + + + let newText = temporaryState.makeAttributeString(addPreAsBlock: true) + + self.inputState = ChatTextInputState(inputText: newText.string, selectionRange: newText.string.length ..< newText.string.length, attributes: chatTextAttributes(from: newText)) + + } + self.loadingState = loadingState + self.editMedia = editMedia + self.editedData = editedData + } + + var canEditMedia: Bool { + return !message.media.isEmpty && (message.media[0] is TelegramMediaImage || message.media[0] is TelegramMediaFile) + } + func withUpdatedMedia(_ media: Media) -> ChatEditState { + + return ChatEditState(message: self.message.withUpdatedMedia([media]), originalMedia: self.originalMedia ?? self.message.media.first, state: self.inputState, loadingState: loadingState, editMedia: .update(AnyMediaReference.standalone(media: media)), editedData: self.editedData) + } + func withUpdatedLoadingState(_ loadingState: EditStateLoading) -> ChatEditState { + return ChatEditState(message: self.message, originalMedia: self.originalMedia, state: self.inputState, loadingState: loadingState, editMedia: self.editMedia, editedData: self.editedData) + } + func withUpdated(state:ChatTextInputState) -> ChatEditState { + return ChatEditState(message: self.message, originalMedia: self.originalMedia, state: state, loadingState: loadingState, editMedia: self.editMedia, editedData: self.editedData) + } + + func withUpdatedEditedData(_ editedData: EditedImageData?) -> ChatEditState { + return ChatEditState(message: self.message, originalMedia: self.originalMedia, state: self.inputState, loadingState: self.loadingState, editMedia: self.editMedia, editedData: editedData) + } + + static func ==(lhs:ChatEditState, rhs:ChatEditState) -> Bool { + return lhs.message.id == rhs.message.id && lhs.inputState == rhs.inputState && lhs.loadingState == rhs.loadingState && lhs.editMedia == rhs.editMedia && lhs.editedData == rhs.editedData + } + +} + + +struct ChatInterfaceTempState: Equatable { + let editState: ChatEditState? +} + + +struct ChatInterfaceState: Codable, Equatable { + static func == (lhs: ChatInterfaceState, rhs: ChatInterfaceState) -> Bool { + return lhs.associatedMessageIds == rhs.associatedMessageIds && lhs.historyScrollMessageIndex == rhs.historyScrollMessageIndex && lhs.historyScrollState == rhs.historyScrollState && lhs.editState == rhs.editState && lhs.timestamp == rhs.timestamp && lhs.inputState == rhs.inputState && lhs.replyMessageId == rhs.replyMessageId && lhs.forwardMessageIds == rhs.forwardMessageIds && lhs.dismissedPinnedMessageId == rhs.dismissedPinnedMessageId && lhs.composeDisableUrlPreview == rhs.composeDisableUrlPreview && lhs.dismissedForceReplyId == rhs.dismissedForceReplyId && lhs.messageActionsState == rhs.messageActionsState && isEqualMessageList(lhs: lhs.forwardMessages, rhs: rhs.forwardMessages) && lhs.hideSendersName == rhs.hideSendersName && lhs.themeEditing == rhs.themeEditing + } + + + + var associatedMessageIds: [MessageId] { + return [] + } + -final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable { - var historyScrollMessageIndex: MessageIndex? { return self.historyScrollState?.messageIndex } - + let historyScrollState: ChatInterfaceHistoryScrollState? - + let editState:ChatEditState? let timestamp: Int32 let inputState: ChatTextInputState let replyMessageId: MessageId? + let replyMessage: Message? + let themeEditing: Bool + let forwardMessageIds: [MessageId] - let dismissedPinnedMessageId:MessageId? + let hideSendersName: Bool + let forwardMessages: [Message] + let dismissedPinnedMessageId:[MessageId] let composeDisableUrlPreview: String? + let dismissedForceReplyId: MessageId? + + let messageActionsState: ChatInterfaceMessageActionsState - let messageActionsState: ChatInterfaceMessageActionsState - var chatListEmbeddedState: PeerChatListEmbeddedInterfaceState? { - if !self.inputState.inputText.isEmpty && self.timestamp != 0 { - return ChatEmbeddedInterfaceState(timestamp: self.timestamp, text: self.inputState.inputText) - } else { + static func parse(_ state: OpaqueChatInterfaceState?, peerId: PeerId?, context: AccountContext?) -> ChatInterfaceState? { + guard let state = state else { return nil } + guard let opaqueData = state.opaqueData else { + return ChatInterfaceState().withUpdatedSynchronizeableInputState(state.synchronizeableInputState).updatedEditState({ _ in + return context?.getChatInterfaceTempState(peerId)?.editState + }) + } + guard var decodedState = try? EngineDecoder.decode(ChatInterfaceState.self, from: opaqueData) else { + return ChatInterfaceState().withUpdatedSynchronizeableInputState(state.synchronizeableInputState).updatedEditState({ _ in + return context?.getChatInterfaceTempState(peerId)?.editState + }) + } + decodedState = decodedState + .withUpdatedSynchronizeableInputState(state.synchronizeableInputState) + .updatedEditState({ _ in + return context?.getChatInterfaceTempState(peerId)?.editState + }) + return decodedState } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + + try container.encode(self.timestamp, forKey: "ts") + try container.encode(self.inputState, forKey: "is") + if let replyMessageId = self.replyMessageId { + try container.encode(replyMessageId.peerId.toInt64(), forKey: "r.p") + try container.encode(replyMessageId.namespace, forKey: "r.n") + try container.encode(replyMessageId.id, forKey: "r.i") + } else { + try container.encodeNil(forKey: "r.p") + try container.encodeNil(forKey: "r.n") + try container.encodeNil(forKey: "r.i") + } + + try container.encode(EngineMessage.Id.encodeArrayToData(forwardMessageIds), forKey: "fm") + + + if self.messageActionsState.isEmpty { + try container.encodeNil(forKey: "as") + } else { + try container.encode(self.messageActionsState, forKey: "as") + } + + try container.encode(EngineMessage.Id.encodeArrayToData(dismissedPinnedMessageId), forKey: "dpl") + + + if let composeDisableUrlPreview = self.composeDisableUrlPreview { + try container.encode(composeDisableUrlPreview, forKey: "dup") + } else { + try container.encodeNil(forKey: "dup") + } + + if let historyScrollState = self.historyScrollState { + try container.encode(historyScrollState, forKey: "hss") + } else { + try container.encodeNil(forKey: "hss") + } + + if let dismissedForceReplyId = self.dismissedForceReplyId { + try container.encode(dismissedForceReplyId.peerId.toInt64(), forKey: "d.f.p") + try container.encode(dismissedForceReplyId.namespace, forKey: "d.f.n") + try container.encode(dismissedForceReplyId.id, forKey: "d.f.i") + } else { + try container.encodeNil(forKey: "d.f.p") + try container.encodeNil(forKey: "d.f.n") + try container.encodeNil(forKey: "d.f.i") + } + + } + + var synchronizeableInputState: SynchronizeableChatInputState? { - if self.inputState.inputText.isEmpty { + if self.inputState.inputText.isEmpty && self.replyMessageId == nil { return nil } else { - return SynchronizeableChatInputState(replyToMessageId: self.replyMessageId, text: self.inputState.inputText, timestamp: self.timestamp) + return SynchronizeableChatInputState(replyToMessageId: self.replyMessageId, text: self.inputState.inputText, entities: self.inputState.messageTextEntities(), timestamp: self.timestamp, textSelection: self.inputState.selectionRange) } } - - func withUpdatedSynchronizeableInputState(_ state: SynchronizeableChatInputState?) -> SynchronizeableChatInterfaceState { - return self.withUpdatedInputState(ChatTextInputState(inputText: state?.text ?? "")).withUpdatedReplyMessageId(state?.replyToMessageId) + + func withUpdatedSynchronizeableInputState(_ state: SynchronizeableChatInputState?) -> ChatInterfaceState { + var result = self + if let state = state { + let selectRange = state.textSelection ?? state.text.length ..< state.text.length + result = result.withUpdatedInputState(ChatTextInputState(inputText: state.text, selectionRange: selectRange, attributes: chatTextAttributes(from: TextEntitiesMessageAttribute(entities: state.entities)))) + .withUpdatedReplyMessageId(state.replyToMessageId) + .withUpdatedTimestamp(timestamp) + } + return result } - - + + init() { self.timestamp = 0 self.inputState = ChatTextInputState() self.replyMessageId = nil + self.replyMessage = nil self.forwardMessageIds = [] + self.forwardMessages = [] self.messageActionsState = ChatInterfaceMessageActionsState() - self.dismissedPinnedMessageId = nil + self.dismissedPinnedMessageId = [] self.composeDisableUrlPreview = nil self.historyScrollState = nil + self.dismissedForceReplyId = nil + self.editState = nil + self.hideSendersName = false + self.themeEditing = false } - - init(timestamp: Int32, inputState: ChatTextInputState, replyMessageId: MessageId?, forwardMessageIds: [MessageId], messageActionsState:ChatInterfaceMessageActionsState, dismissedPinnedMessageId: MessageId?, composeDisableUrlPreview: String?, historyScrollState: ChatInterfaceHistoryScrollState?) { + + init(timestamp: Int32, inputState: ChatTextInputState, replyMessageId: MessageId?, replyMessage: Message?, forwardMessageIds: [MessageId], messageActionsState:ChatInterfaceMessageActionsState, dismissedPinnedMessageId: [MessageId], composeDisableUrlPreview: String?, historyScrollState: ChatInterfaceHistoryScrollState?, dismissedForceReplyId: MessageId?, editState: ChatEditState?, forwardMessages:[Message], hideSendersName: Bool, themeEditing: Bool) { self.timestamp = timestamp self.inputState = inputState self.replyMessageId = replyMessageId @@ -538,153 +1008,146 @@ final class ChatInterfaceState: SynchronizeableChatInterfaceState, Equatable { self.dismissedPinnedMessageId = dismissedPinnedMessageId self.composeDisableUrlPreview = composeDisableUrlPreview self.historyScrollState = historyScrollState + self.dismissedForceReplyId = dismissedForceReplyId + self.editState = editState + self.replyMessage = replyMessage + self.forwardMessages = forwardMessages + self.hideSendersName = hideSendersName + self.themeEditing = themeEditing } - - init(decoder: PostboxDecoder) { - self.timestamp = decoder.decodeInt32ForKey("ts", orElse: 0) - if let inputState = decoder.decodeObjectForKey("is", decoder: { return ChatTextInputState(decoder: $0) }) as? ChatTextInputState { + + init(from decoder: Decoder) throws { + + let container = try decoder.container(keyedBy: StringCodingKey.self) + + + + self.timestamp = (try? container.decode(Int32.self, forKey: "ts")) ?? 0 + if let inputState = try? container.decode(ChatTextInputState.self, forKey: "is") { self.inputState = inputState } else { self.inputState = ChatTextInputState() } - let replyMessageIdPeerId: Int64? = decoder.decodeOptionalInt64ForKey("r.p") - let replyMessageIdNamespace: Int32? = decoder.decodeOptionalInt32ForKey("r.n") - let replyMessageIdId: Int32? = decoder.decodeOptionalInt32ForKey("r.i") + + let replyMessageIdPeerId: Int64? = try? container.decodeIfPresent(Int64.self, forKey: "r.p") + let replyMessageIdNamespace: Int32? = try? container.decodeIfPresent(Int32.self, forKey: "r.n") + let replyMessageIdId: Int32? = try? container.decodeIfPresent(Int32.self, forKey: "r.i") if let replyMessageIdPeerId = replyMessageIdPeerId, let replyMessageIdNamespace = replyMessageIdNamespace, let replyMessageIdId = replyMessageIdId { - self.replyMessageId = MessageId(peerId: PeerId(replyMessageIdPeerId), namespace: replyMessageIdNamespace, id: replyMessageIdId) + self.replyMessageId = EngineMessage.Id(peerId: EnginePeer.Id(replyMessageIdPeerId), namespace: replyMessageIdNamespace, id: replyMessageIdId) } else { self.replyMessageId = nil } - if let forwardMessageIdsData = decoder.decodeBytesForKeyNoCopy("fm") { - self.forwardMessageIds = MessageId.decodeArrayFromBuffer(forwardMessageIdsData) + + if let forwardMessageIdsData = try? container.decodeIfPresent(Data.self, forKey: "fm") { + self.forwardMessageIds = EngineMessage.Id.decodeArrayFromData(forwardMessageIdsData) } else { self.forwardMessageIds = [] } - - - if let messageActionsState = decoder.decodeObjectForKey("as", decoder: { ChatInterfaceMessageActionsState(decoder: $0) }) as? ChatInterfaceMessageActionsState { + + if let messageActionsState = try? container.decodeIfPresent(ChatInterfaceMessageActionsState.self, forKey: "as") { self.messageActionsState = messageActionsState } else { self.messageActionsState = ChatInterfaceMessageActionsState() } - - let dismissedPinnedIdPeerId: Int64? = decoder.decodeOptionalInt64ForKey("d.p.p") - let dismissedPinnedIdNamespace: Int32? = decoder.decodeOptionalInt32ForKey("d.p.n") - let dismissedPinnedIdId: Int32? = decoder.decodeOptionalInt32ForKey("d.p.i") - if let dismissedPinnedIdPeerId = dismissedPinnedIdPeerId, let dismissedPinnedIdNamespace = dismissedPinnedIdNamespace, let dismissedPinnedIdId = dismissedPinnedIdId { - self.dismissedPinnedMessageId = MessageId(peerId: PeerId(dismissedPinnedIdPeerId), namespace: dismissedPinnedIdNamespace, id: dismissedPinnedIdId) + + + if let dismissedPinnedData = try? container.decodeIfPresent(Data.self, forKey: "dpl") { + self.dismissedPinnedMessageId = EngineMessage.Id.decodeArrayFromData(dismissedPinnedData) } else { - self.dismissedPinnedMessageId = nil + self.dismissedPinnedMessageId = [] } + + self.composeDisableUrlPreview = try? container.decodeIfPresent(String.self, forKey: "dup") - if let composeDisableUrlPreview = decoder.decodeOptionalStringForKey("dup") as String? { - self.composeDisableUrlPreview = composeDisableUrlPreview + + self.historyScrollState = try? container.decodeIfPresent(ChatInterfaceHistoryScrollState.self, forKey: "hss") + + + let dismissedForceReplyIdPeerId: Int64? = try? container.decodeIfPresent(Int64.self, forKey: "d.f.p") + let dismissedForceReplyIdNamespace: Int32? = try? container.decodeIfPresent(Int32.self, forKey: "d.f.n") + let dismissedForceReplyIdId: Int32? = try? container.decodeIfPresent(Int32.self, forKey: "d.f.i") + if let dismissedForceReplyIdPeerId = dismissedForceReplyIdPeerId, let dismissedForceReplyIdNamespace = dismissedForceReplyIdNamespace, let dismissedForceReplyIdId = dismissedForceReplyIdId { + self.dismissedForceReplyId = MessageId(peerId: PeerId(dismissedForceReplyIdPeerId), namespace: dismissedForceReplyIdNamespace, id: dismissedForceReplyIdId) } else { - self.composeDisableUrlPreview = nil + self.dismissedForceReplyId = nil } - - self.historyScrollState = decoder.decodeObjectForKey("hss", decoder: { ChatInterfaceHistoryScrollState(decoder: $0) }) as? ChatInterfaceHistoryScrollState - - + //TODO + self.editState = nil + self.replyMessage = nil + self.forwardMessages = [] + self.hideSendersName = false + self.themeEditing = false } - - func encode(_ encoder: PostboxEncoder) { - encoder.encodeInt32(self.timestamp, forKey: "ts") - encoder.encodeObject(self.inputState, forKey: "is") - if let replyMessageId = self.replyMessageId { - encoder.encodeInt64(replyMessageId.peerId.toInt64(), forKey: "r.p") - encoder.encodeInt32(replyMessageId.namespace, forKey: "r.n") - encoder.encodeInt32(replyMessageId.id, forKey: "r.i") - } else { - encoder.encodeNil(forKey: "r.p") - encoder.encodeNil(forKey: "r.n") - encoder.encodeNil(forKey: "r.i") - } - - let buffer = WriteBuffer() - MessageId.encodeArrayToBuffer(forwardMessageIds, buffer: buffer) - encoder.encodeBytes(buffer, forKey: "fm") - - - - if self.messageActionsState.isEmpty { - encoder.encodeNil(forKey: "as") - } else { - encoder.encodeObject(self.messageActionsState, forKey: "as") - } - - if let dismissedPinnedMessageId = self.dismissedPinnedMessageId { - encoder.encodeInt64(dismissedPinnedMessageId.peerId.toInt64(), forKey: "d.p.p") - encoder.encodeInt32(dismissedPinnedMessageId.namespace, forKey: "d.p.n") - encoder.encodeInt32(dismissedPinnedMessageId.id, forKey: "d.p.i") - } else { - encoder.encodeNil(forKey: "d.p.p") - encoder.encodeNil(forKey: "d.p.n") - encoder.encodeNil(forKey: "d.p.i") - } - - if let composeDisableUrlPreview = self.composeDisableUrlPreview { - encoder.encodeString(composeDisableUrlPreview, forKey: "dup") - } else { - encoder.encodeNil(forKey: "dup") - } - - if let historyScrollState = self.historyScrollState { - encoder.encodeObject(historyScrollState, forKey: "hss") - } else { - encoder.encodeNil(forKey: "hss") - } - + + + func withUpdatedInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState { + return ChatInterfaceState(timestamp: self.timestamp, inputState: self.editState == nil ? inputState : self.inputState, replyMessageId: self.replyMessageId, replyMessage: self.replyMessage, forwardMessageIds: self.forwardMessageIds, messageActionsState:self.messageActionsState, dismissedPinnedMessageId: self.dismissedPinnedMessageId, composeDisableUrlPreview: self.composeDisableUrlPreview, historyScrollState: self.historyScrollState, dismissedForceReplyId: self.dismissedForceReplyId, editState: self.editState?.withUpdated(state: inputState), forwardMessages: self.forwardMessages, hideSendersName: self.hideSendersName, themeEditing: self.themeEditing) } - - func isEqual(to: PeerChatInterfaceState) -> Bool { - if let to = to as? ChatInterfaceState, self == to { - return true - } else { - return false - } + + func withAddedDismissedPinnedIds(_ dismissedPinnedId: [MessageId]) -> ChatInterfaceState { + return ChatInterfaceState(timestamp: self.timestamp, inputState: self.inputState, replyMessageId: self.replyMessageId, replyMessage: self.replyMessage, forwardMessageIds: self.forwardMessageIds, messageActionsState:self.messageActionsState, dismissedPinnedMessageId: (self.dismissedPinnedMessageId + dismissedPinnedId).uniqueElements, composeDisableUrlPreview: self.composeDisableUrlPreview, historyScrollState: self.historyScrollState, dismissedForceReplyId: self.dismissedForceReplyId, editState: self.editState, forwardMessages: self.forwardMessages, hideSendersName: self.hideSendersName, themeEditing: self.themeEditing) } - - static func ==(lhs: ChatInterfaceState, rhs: ChatInterfaceState) -> Bool { - return lhs.inputState == rhs.inputState && lhs.replyMessageId == rhs.replyMessageId && lhs.forwardMessageIds == rhs.forwardMessageIds && lhs.messageActionsState == rhs.messageActionsState && lhs.timestamp == rhs.timestamp && lhs.dismissedPinnedMessageId == rhs.dismissedPinnedMessageId && lhs.composeDisableUrlPreview == rhs.composeDisableUrlPreview && lhs.historyScrollState == rhs.historyScrollState + + func withUpdatedDismissedForceReplyId(_ dismissedId: MessageId?) -> ChatInterfaceState { + return ChatInterfaceState(timestamp: self.timestamp, inputState: self.inputState, replyMessageId: self.replyMessageId, replyMessage: self.replyMessage, forwardMessageIds: self.forwardMessageIds, messageActionsState:self.messageActionsState, dismissedPinnedMessageId: self.dismissedPinnedMessageId, composeDisableUrlPreview: self.composeDisableUrlPreview, historyScrollState: self.historyScrollState, dismissedForceReplyId: dismissedId, editState: self.editState, forwardMessages: self.forwardMessages, hideSendersName: self.hideSendersName, themeEditing: self.themeEditing) } - - func withUpdatedInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, inputState: inputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, messageActionsState:self.messageActionsState, dismissedPinnedMessageId: self.dismissedPinnedMessageId, composeDisableUrlPreview: self.composeDisableUrlPreview, historyScrollState: self.historyScrollState) + + func updatedEditState(_ f:(ChatEditState?)->ChatEditState?) -> ChatInterfaceState { + return ChatInterfaceState(timestamp: self.timestamp, inputState: self.inputState, replyMessageId: self.replyMessageId, replyMessage: self.replyMessage, forwardMessageIds: self.forwardMessageIds, messageActionsState:self.messageActionsState, dismissedPinnedMessageId: self.dismissedPinnedMessageId, composeDisableUrlPreview: self.composeDisableUrlPreview, historyScrollState: self.historyScrollState, dismissedForceReplyId: self.dismissedForceReplyId, editState: f(self.editState), forwardMessages: self.forwardMessages, hideSendersName: self.hideSendersName, themeEditing: self.themeEditing) } - - func withUpdatedDismissedPinnedId(_ dismissedPinnedId: MessageId?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, inputState: self.inputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, messageActionsState:self.messageActionsState, dismissedPinnedMessageId: dismissedPinnedId, composeDisableUrlPreview: self.composeDisableUrlPreview, historyScrollState: self.historyScrollState) + + func withEditMessage(_ message:Message) -> ChatInterfaceState { + return ChatInterfaceState(timestamp: self.timestamp, inputState: self.inputState, replyMessageId: self.replyMessageId, replyMessage: self.replyMessage, forwardMessageIds: self.forwardMessageIds, messageActionsState:self.messageActionsState, dismissedPinnedMessageId: self.dismissedPinnedMessageId, composeDisableUrlPreview: self.composeDisableUrlPreview, historyScrollState: self.historyScrollState, dismissedForceReplyId: self.dismissedForceReplyId, editState: ChatEditState(message: message), forwardMessages: self.forwardMessages, hideSendersName: self.hideSendersName, themeEditing: self.themeEditing) } - + + func withoutEditMessage() -> ChatInterfaceState { + return ChatInterfaceState(timestamp: self.timestamp, inputState: self.inputState, replyMessageId: self.replyMessageId, replyMessage: self.replyMessage, forwardMessageIds: self.forwardMessageIds, messageActionsState:self.messageActionsState, dismissedPinnedMessageId: self.dismissedPinnedMessageId, composeDisableUrlPreview: self.composeDisableUrlPreview, historyScrollState: self.historyScrollState, dismissedForceReplyId: self.dismissedForceReplyId, editState: nil, forwardMessages: self.forwardMessages, hideSendersName: self.hideSendersName, themeEditing: self.themeEditing) + } + func withUpdatedReplyMessageId(_ replyMessageId: MessageId?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, inputState: self.inputState, replyMessageId: replyMessageId, forwardMessageIds: self.forwardMessageIds, messageActionsState:self.messageActionsState, dismissedPinnedMessageId: self.dismissedPinnedMessageId, composeDisableUrlPreview: self.composeDisableUrlPreview, historyScrollState: self.historyScrollState) + return ChatInterfaceState(timestamp: self.timestamp, inputState: self.inputState, replyMessageId: replyMessageId, replyMessage: nil, forwardMessageIds: self.forwardMessageIds, messageActionsState:self.messageActionsState, dismissedPinnedMessageId: self.dismissedPinnedMessageId, composeDisableUrlPreview: self.composeDisableUrlPreview, historyScrollState: self.historyScrollState, dismissedForceReplyId: self.dismissedForceReplyId, editState: self.editState, forwardMessages: self.forwardMessages, hideSendersName: self.hideSendersName, themeEditing: self.themeEditing) } - + + func withUpdatedReplyMessage(_ replyMessage: Message?) -> ChatInterfaceState { + return ChatInterfaceState(timestamp: self.timestamp, inputState: self.inputState, replyMessageId: self.replyMessageId, replyMessage: replyMessage, forwardMessageIds: self.forwardMessageIds, messageActionsState:self.messageActionsState, dismissedPinnedMessageId: self.dismissedPinnedMessageId, composeDisableUrlPreview: self.composeDisableUrlPreview, historyScrollState: self.historyScrollState, dismissedForceReplyId: self.dismissedForceReplyId, editState: self.editState, forwardMessages: self.forwardMessages, hideSendersName: self.hideSendersName, themeEditing: self.themeEditing) + } + func withUpdatedForwardMessageIds(_ forwardMessageIds: [MessageId]) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, inputState: self.inputState, replyMessageId: self.replyMessageId, forwardMessageIds: forwardMessageIds, messageActionsState:self.messageActionsState, dismissedPinnedMessageId: self.dismissedPinnedMessageId, composeDisableUrlPreview: self.composeDisableUrlPreview, historyScrollState: self.historyScrollState) + return ChatInterfaceState(timestamp: self.timestamp, inputState: self.inputState, replyMessageId: self.replyMessageId, replyMessage: self.replyMessage, forwardMessageIds: forwardMessageIds, messageActionsState:self.messageActionsState, dismissedPinnedMessageId: self.dismissedPinnedMessageId, composeDisableUrlPreview: self.composeDisableUrlPreview, historyScrollState: self.historyScrollState, dismissedForceReplyId: self.dismissedForceReplyId, editState: self.editState, forwardMessages: self.forwardMessages, hideSendersName: self.hideSendersName, themeEditing: self.themeEditing) + } + + func withUpdatedForwardMessages(_ forwardMessages: [Message]) -> ChatInterfaceState { + return ChatInterfaceState(timestamp: self.timestamp, inputState: self.inputState, replyMessageId: self.replyMessageId, replyMessage: self.replyMessage, forwardMessageIds: forwardMessageIds, messageActionsState:self.messageActionsState, dismissedPinnedMessageId: self.dismissedPinnedMessageId, composeDisableUrlPreview: self.composeDisableUrlPreview, historyScrollState: self.historyScrollState, dismissedForceReplyId: self.dismissedForceReplyId, editState: self.editState, forwardMessages: forwardMessages, hideSendersName: hideSendersName, themeEditing: self.themeEditing) } + func withUpdatedHideSendersName(_ hideSendersName: Bool) -> ChatInterfaceState { + return ChatInterfaceState(timestamp: self.timestamp, inputState: self.inputState, replyMessageId: self.replyMessageId, replyMessage: self.replyMessage, forwardMessageIds: forwardMessageIds, messageActionsState:self.messageActionsState, dismissedPinnedMessageId: self.dismissedPinnedMessageId, composeDisableUrlPreview: self.composeDisableUrlPreview, historyScrollState: self.historyScrollState, dismissedForceReplyId: self.dismissedForceReplyId, editState: self.editState, forwardMessages: self.forwardMessages, hideSendersName: hideSendersName, themeEditing: self.themeEditing) + } + func withoutForwardMessages() -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, inputState: self.inputState, replyMessageId: self.replyMessageId, forwardMessageIds: [], messageActionsState:self.messageActionsState, dismissedPinnedMessageId: self.dismissedPinnedMessageId, composeDisableUrlPreview: self.composeDisableUrlPreview, historyScrollState: self.historyScrollState) + return ChatInterfaceState(timestamp: self.timestamp, inputState: self.inputState, replyMessageId: self.replyMessageId, replyMessage: self.replyMessage, forwardMessageIds: [], messageActionsState:self.messageActionsState, dismissedPinnedMessageId: self.dismissedPinnedMessageId, composeDisableUrlPreview: self.composeDisableUrlPreview, historyScrollState: self.historyScrollState, dismissedForceReplyId: self.dismissedForceReplyId, editState: self.editState, forwardMessages: self.forwardMessages, hideSendersName: self.hideSendersName, themeEditing: self.themeEditing) } - + func withUpdatedTimestamp(_ timestamp: Int32) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: timestamp, inputState: self.inputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, messageActionsState:self.messageActionsState, dismissedPinnedMessageId: self.dismissedPinnedMessageId, composeDisableUrlPreview: self.composeDisableUrlPreview, historyScrollState: self.historyScrollState) + return ChatInterfaceState(timestamp: timestamp, inputState: self.inputState, replyMessageId: self.replyMessageId, replyMessage: self.replyMessage, forwardMessageIds: self.forwardMessageIds, messageActionsState:self.messageActionsState, dismissedPinnedMessageId: self.dismissedPinnedMessageId, composeDisableUrlPreview: self.composeDisableUrlPreview, historyScrollState: self.historyScrollState, dismissedForceReplyId: self.dismissedForceReplyId, editState: self.editState, forwardMessages: self.forwardMessages, hideSendersName: self.hideSendersName, themeEditing: self.themeEditing) } - - + + func withUpdatedMessageActionsState(_ f: (ChatInterfaceMessageActionsState) -> ChatInterfaceMessageActionsState) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, inputState: self.inputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, messageActionsState:f(self.messageActionsState), dismissedPinnedMessageId: self.dismissedPinnedMessageId, composeDisableUrlPreview: self.composeDisableUrlPreview, historyScrollState: self.historyScrollState) + return ChatInterfaceState(timestamp: self.timestamp, inputState: self.inputState, replyMessageId: self.replyMessageId, replyMessage: self.replyMessage, forwardMessageIds: self.forwardMessageIds, messageActionsState:f(self.messageActionsState), dismissedPinnedMessageId: self.dismissedPinnedMessageId, composeDisableUrlPreview: self.composeDisableUrlPreview, historyScrollState: self.historyScrollState, dismissedForceReplyId: self.dismissedForceReplyId, editState: self.editState, forwardMessages: self.forwardMessages, hideSendersName: self.hideSendersName, themeEditing: self.themeEditing) } - + func withUpdatedComposeDisableUrlPreview(_ disableUrlPreview: String?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, inputState: self.inputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, messageActionsState: self.messageActionsState, dismissedPinnedMessageId: self.dismissedPinnedMessageId, composeDisableUrlPreview: disableUrlPreview, historyScrollState: self.historyScrollState) + return ChatInterfaceState(timestamp: self.timestamp, inputState: self.inputState, replyMessageId: self.replyMessageId, replyMessage: self.replyMessage, forwardMessageIds: self.forwardMessageIds, messageActionsState: self.messageActionsState, dismissedPinnedMessageId: self.dismissedPinnedMessageId, composeDisableUrlPreview: disableUrlPreview, historyScrollState: self.historyScrollState, dismissedForceReplyId: self.dismissedForceReplyId, editState: self.editState, forwardMessages: self.forwardMessages, hideSendersName: self.hideSendersName, themeEditing: self.themeEditing) } - + func withUpdatedHistoryScrollState(_ historyScrollState: ChatInterfaceHistoryScrollState?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, inputState: self.inputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, messageActionsState: self.messageActionsState, dismissedPinnedMessageId: self.dismissedPinnedMessageId, composeDisableUrlPreview: self.composeDisableUrlPreview, historyScrollState: historyScrollState) + return ChatInterfaceState(timestamp: self.timestamp, inputState: self.inputState, replyMessageId: self.replyMessageId, replyMessage: self.replyMessage, forwardMessageIds: self.forwardMessageIds, messageActionsState: self.messageActionsState, dismissedPinnedMessageId: self.dismissedPinnedMessageId, composeDisableUrlPreview: self.composeDisableUrlPreview, historyScrollState: historyScrollState, dismissedForceReplyId: self.dismissedForceReplyId, editState: self.editState, forwardMessages: self.forwardMessages, hideSendersName: self.hideSendersName, themeEditing: self.themeEditing) } + func withUpdatedThemeEditing(_ themeEditing: Bool) -> ChatInterfaceState { + return ChatInterfaceState(timestamp: self.timestamp, inputState: self.inputState, replyMessageId: self.replyMessageId, replyMessage: self.replyMessage, forwardMessageIds: self.forwardMessageIds, messageActionsState: self.messageActionsState, dismissedPinnedMessageId: self.dismissedPinnedMessageId, composeDisableUrlPreview: self.composeDisableUrlPreview, historyScrollState: historyScrollState, dismissedForceReplyId: self.dismissedForceReplyId, editState: self.editState, forwardMessages: self.forwardMessages, hideSendersName: self.hideSendersName, themeEditing: themeEditing) + } + } diff --git a/Telegram-Mac/ChatInterfaceStateContextQueries.swift b/Telegram-Mac/ChatInterfaceStateContextQueries.swift index 7ec10fe526..99fea2e3c3 100644 --- a/Telegram-Mac/ChatInterfaceStateContextQueries.swift +++ b/Telegram-Mac/ChatInterfaceStateContextQueries.swift @@ -1,4 +1,4 @@ -// + // // ChatInterfaceStateContextQueries.swift // TelegramMac // @@ -8,45 +8,128 @@ import Cocoa -import SwiftSignalKitMac -import TelegramCoreMac -import PostboxMac +import SwiftSignalKit +import TelegramCore + +import Postbox -func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentQuery: ChatPresentationInputQuery?) -> (ChatPresentationInputQuery?, Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError>)? { +func contextQueryResultStateForChatInterfacePresentationState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentQuery: ChatPresentationInputQuery?) -> (ChatPresentationInputQuery?, Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError>)? { let inputQuery = chatPresentationInterfaceState.inputContext - if inputQuery != .none { - if inputQuery == currentQuery { - return nil + switch chatPresentationInterfaceState.state { + case .normal, .editing: + if inputQuery != .none { + if inputQuery == currentQuery { + return nil + } else { + return makeInlineResult(inputQuery, chatPresentationInterfaceState: chatPresentationInterfaceState, currentQuery: currentQuery, context: context) + } } else { - return makeInlineResult(inputQuery, chatPresentationInterfaceState: chatPresentationInterfaceState, currentQuery: currentQuery, account: account) - + return (nil, .single({ _ in return nil })) } - } else { + default: return (nil, .single({ _ in return nil })) } + } -private func makeInlineResult(_ inputQuery: ChatPresentationInputQuery, chatPresentationInterfaceState: ChatPresentationInterfaceState, currentQuery: ChatPresentationInputQuery?, account:Account) -> (ChatPresentationInputQuery?, Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError>)? { +private func makeInlineResult(_ inputQuery: ChatPresentationInputQuery, chatPresentationInterfaceState: ChatPresentationInterfaceState, currentQuery: ChatPresentationInputQuery?, context: AccountContext) -> (ChatPresentationInputQuery?, Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError>)? { switch inputQuery { case .none: return (nil, .single({ _ in return nil })) - case .hashtag(_): + case let .hashtag(query): + + var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .complete() + if let currentQuery = currentQuery { + switch currentQuery { + case .hashtag: + break + default: + signal = .single({ _ in return nil }) + } + } + + let hashtags: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = context.engine.messages.recentlyUsedHashtags() |> map { hashtags -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in + let normalizedQuery = query.lowercased() + var result: [String] = [] + for hashtag in hashtags { + if hashtag.lowercased().hasPrefix(normalizedQuery) { + result.append(hashtag) + } + } + return { _ in return .hashtags(result) } + } + + return (inputQuery, signal |> then(hashtags)) - return (nil, .single({ _ in return nil })) case let .stickers(query): - return (inputQuery, searchStickers(postbox: account.postbox, query: query) |> map { stickers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in - return { _ in return .stickers(stickers) } + return (inputQuery, context.account.postbox.transaction { transaction -> StickerSettings in + let stickerSettings: StickerSettings = (transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.stickerSettings) as? StickerSettings) ?? .defaultSettings + return stickerSettings + } + |> mapToSignal { stickerSettings -> Signal<[FoundStickerItem], NoError> in + let scope: SearchStickersScope + switch stickerSettings.emojiStickerSuggestionMode { + case .none: + scope = [] + case .all: + scope = [.installed, .remote] + case .installed: + scope = [.installed] + } + return context.engine.stickers.searchStickers(query: query, scope: scope) + } + |> map { stickers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in + return { _ in + return .stickers(stickers) + } }) - case let .emoji(query): - return (inputQuery, searchEmojiClue(query: query, postbox: account.postbox) |> map { clues -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in - return { _ in return .emoji(clues) } - }) +// return (inputQuery, searchStickers(account: account, query: query) |> map { stickers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in +// return { _ in return .stickers(stickers) } +// }) + case let .emoji(query, firstWord): + if !query.isEmpty { + let signal = context.sharedContext.inputSource.searchEmoji(postbox: context.account.postbox, engine: context.engine, sharedContext: context.sharedContext, query: query, completeMatch: query.length < 3, checkPrediction: firstWord) |> delay(firstWord ? 0.3 : 0, queue: .concurrentDefaultQueue()) + + if firstWord { + return (inputQuery, .single({ _ in return nil }) |> then(combineLatest(signal, recentUsedEmoji(postbox: context.account.postbox)) |> map { matches, emojies -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in + let sorted = matches.sorted(by: { lhs, rhs in + let lhsIndex = emojies.emojies.firstIndex(of: lhs) ?? Int.max + let rhsIndex = emojies.emojies.firstIndex(of: rhs) ?? Int.max + return lhsIndex < rhsIndex + }) + + return { _ in return .emoji(sorted, firstWord) } + })) + } else { + return (inputQuery, combineLatest(signal, recentUsedEmoji(postbox: context.account.postbox)) |> map { matches, emojies -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in + let sorted = matches.sorted(by: { lhs, rhs in + let lhsIndex = emojies.emojies.firstIndex(of: lhs) ?? Int.max + let rhsIndex = emojies.emojies.firstIndex(of: rhs) ?? Int.max + return lhsIndex < rhsIndex + }) + + + return { _ in return .emoji(sorted, firstWord) } + }) + } + + + } else { + if firstWord { + return (nil, .single({ _ in return nil })) + } else { + return (inputQuery, recentUsedEmoji(postbox: context.account.postbox) |> map { emojis -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in + return { _ in return .emoji(emojis.emojies, firstWord) } + }) + } + } + case let .mention(query: query, includeRecent: includeRecent): let normalizedQuery = query.lowercased() - if let peer = chatPresentationInterfaceState.peer { + if let global = chatPresentationInterfaceState.peer { var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .complete() if let currentQuery = currentQuery { switch currentQuery { @@ -59,10 +142,41 @@ private func makeInlineResult(_ inputQuery: ChatPresentationInputQuery, chatPres var inlineSignal: Signal<[(Peer, Double)], NoError> = .single([]) if includeRecent { - inlineSignal = recentlyUsedInlineBots(postbox: account.postbox) + inlineSignal = context.engine.peers.recentlyUsedInlineBots() |> take(1) |> map { + $0.map { ($0.0._asPeer(), $0.1) } + } } - let participants = combineLatest(inlineSignal, peerParticipants(account: account, id: peer.id)) + let members: Signal<[Peer], NoError> = searchPeerMembers(context: context, peerId: global.id, chatLocation: chatPresentationInterfaceState.chatLocation, query: query) + + let participants = combineLatest(inlineSignal, members |> take(1) |> mapToSignal { participants -> Signal<[Peer], NoError> in + return context.account.viewTracker.aroundMessageOfInterestHistoryViewForLocation(.peer(global.id), count: 100, tagMask: nil, orderStatistics: [], additionalData: []) |> take(1) |> map { view in + let latestIds:[PeerId] = view.0.entries.reversed().compactMap({ entry in + if entry.message.media.first is TelegramMediaAction { + return nil + } + return entry.message.author?.id + }) + + let sorted = participants.sorted{ lhs, rhs in + let lhsIndex = latestIds.firstIndex(where: {$0 == lhs.id}) + let rhsIndex = latestIds.firstIndex(where: {$0 == rhs.id}) + if let lhsIndex = lhsIndex, let rhsIndex = rhsIndex { + return lhsIndex < rhsIndex + } else if lhsIndex == nil && rhsIndex != nil { + return false + } else if lhsIndex != nil && rhsIndex == nil { + return true + } else { + return lhs.displayTitle < rhs.displayTitle + } + + } + + return sorted + } + + }) |> map { recent, participants -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in let filteredRecent = recent.filter ({ recent in @@ -79,6 +193,16 @@ private func makeInlineResult(_ inputQuery: ChatPresentationInputQuery, chatPres }).map {$0.0} let filteredParticipants = participants.filter ({ peer in + if peer.id == context.peerId { + return false + } + if peer.rawDisplayTitle.isEmpty { + return false + } + + if global.isChannel, let peer = peer as? TelegramUser, peer.botInfo?.inlinePlaceholder == nil { + return false + } if peer.indexName.matchesByTokens(normalizedQuery) { return true } @@ -86,9 +210,6 @@ private func makeInlineResult(_ inputQuery: ChatPresentationInputQuery, chatPres return true } return peer.addressName == nil && normalizedQuery.isEmpty - }).sorted(by: { lhs, rhs in - let result = lhs.indexName.indexName(.lastNameFirst).compare(rhs.indexName.indexName(.lastNameFirst)) - return result == .orderedAscending }) return { _ in return .mentions(filteredRecent + filteredParticipants) } @@ -111,8 +232,7 @@ private func makeInlineResult(_ inputQuery: ChatPresentationInputQuery, chatPres signal = .single({ _ in return nil }) } } - - let participants = peerCommands(account: account, id: peer.id) + let participants = context.engine.peers.peerCommands(id: peer.id) |> map { commands -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in let filteredCommands = commands.commands.filter { command in if command.command.text.hasPrefix(normalizedQuery) { @@ -134,7 +254,7 @@ private func makeInlineResult(_ inputQuery: ChatPresentationInputQuery, chatPres } var delayRequest = true - var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .complete() + var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> = .single({ _ in return nil }) if let currentQuery = currentQuery { switch currentQuery { case let .contextRequest(currentAddressName, currentContextQuery) where currentAddressName == addressName: @@ -146,11 +266,10 @@ private func makeInlineResult(_ inputQuery: ChatPresentationInputQuery, chatPres signal = .single({ _ in return nil }) } } - - let contextBot = resolvePeerByName(account: account, name: addressName) - |> mapToSignal { peerId -> Signal in - if let peerId = peerId { - return account.postbox.loadedPeerWithId(peerId) + let contextBot = context.engine.peers.resolvePeerByName(name: addressName) + |> mapToSignal { peer -> Signal in + if let peer = peer { + return context.account.postbox.loadedPeerWithId(peer._asPeer().id) |> map { peer -> Peer? in return peer } @@ -161,10 +280,10 @@ private func makeInlineResult(_ inputQuery: ChatPresentationInputQuery, chatPres } |> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> in if let user = peer as? TelegramUser, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder { - let contextResults = requestChatContextResults(account: account, botId: user.id, peerId: chatPeer.id, query: query, offset: "") + let contextResults = context.engine.messages.requestChatContextResults(botId: user.id, peerId: chatPeer.id, query: query, offset: "") |> map { results -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in return { _ in - return .contextRequestResult(user, results) + return .contextRequestResult(user, results?.results) } } @@ -182,9 +301,9 @@ private func makeInlineResult(_ inputQuery: ChatPresentationInputQuery, chatPres let maybeDelayedContextResults: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError> if delayRequest { - maybeDelayedContextResults = contextResults |> delay(0.4, queue: Queue.concurrentDefaultQueue()) + maybeDelayedContextResults = contextResults |> `catch` { _ in return .complete() } |> delay(0.4, queue: Queue.concurrentDefaultQueue()) } else { - maybeDelayedContextResults = contextResults + maybeDelayedContextResults = contextResults |> `catch` { _ in return .complete() } } return botResult |> then(maybeDelayedContextResults) @@ -195,10 +314,40 @@ private func makeInlineResult(_ inputQuery: ChatPresentationInputQuery, chatPres case let .mention(query: query, includeRecent: _): let normalizedQuery = query.lowercased() - if let peer = chatPresentationInterfaceState.peer { - return peerParticipants(account: account, id: peer.id) - |> map { participants -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in + if let global = chatPresentationInterfaceState.peer { + return searchPeerMembers(context: context, peerId: global.id, chatLocation: chatPresentationInterfaceState.chatLocation, query: normalizedQuery) |> take(1) |> mapToSignal { participants -> Signal<[Peer], NoError> in + return context.account.viewTracker.aroundMessageOfInterestHistoryViewForLocation(.peer(global.id), count: 100, tagMask: nil, orderStatistics: [], additionalData: []) |> take(1) |> map { view in + let latestIds:[PeerId] = view.0.entries.reversed().compactMap({ entry in + if entry.message.media.first is TelegramMediaAction { + return nil + } + return entry.message.author?.id + }) + let sorted = participants.sorted{ lhs, rhs in + let lhsIndex = latestIds.firstIndex(where: {$0 == lhs.id}) + let rhsIndex = latestIds.firstIndex(where: {$0 == rhs.id}) + if let lhsIndex = lhsIndex, let rhsIndex = rhsIndex { + return lhsIndex < rhsIndex + } else if lhsIndex == nil && rhsIndex != nil { + return false + } else if lhsIndex != nil && rhsIndex == nil { + return true + } else { + return lhs.displayTitle < rhs.displayTitle + } + } + return sorted + } + + } |> map { participants -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in let filteredParticipants = participants.filter ({ peer in + if peer.id == context.peerId { + return false + } + if global.isChannel, let peer = peer as? TelegramUser, peer.botInfo?.inlinePlaceholder == nil { + return false + } + if peer.indexName.matchesByTokens(normalizedQuery) { return true } @@ -206,9 +355,6 @@ private func makeInlineResult(_ inputQuery: ChatPresentationInputQuery, chatPres return true } return peer.addressName == nil && normalizedQuery.isEmpty - }).sorted(by: { lhs, rhs in - let result = lhs.indexName.indexName(.lastNameFirst).compare(rhs.indexName.indexName(.lastNameFirst)) - return result == .orderedAscending }) return { _ in return .mentions(filteredParticipants) } @@ -226,8 +372,13 @@ private func makeInlineResult(_ inputQuery: ChatPresentationInputQuery, chatPres } } +enum ContextQueryForSearchMentionFilter { + case plain(includeNameless: Bool, includeInlineBots: Bool) + case filterSelf(includeNameless: Bool, includeInlineBots: Bool) +} + -func chatContextQueryForSearchMention(peer: Peer, _ inputQuery: ChatPresentationInputQuery, currentQuery: ChatPresentationInputQuery?, account:Account) -> (ChatPresentationInputQuery?, Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError>)? { +func chatContextQueryForSearchMention(chatLocations: [ChatLocation], _ inputQuery: ChatPresentationInputQuery, currentQuery: ChatPresentationInputQuery?, context: AccountContext, filter: ContextQueryForSearchMentionFilter = .plain(includeNameless: true, includeInlineBots: false)) -> (ChatPresentationInputQuery?, Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, NoError>)? { switch inputQuery { case let .mention(query: query, includeRecent: _): let normalizedQuery = query.lowercased() @@ -242,25 +393,135 @@ func chatContextQueryForSearchMention(peer: Peer, _ inputQuery: ChatPresentation } } - let participants = peerParticipants(account: account, id: peer.id) - |> map { participants -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in - let filteredParticipants = participants.filter ({ peer in - if peer.indexName.matchesByTokens(normalizedQuery) { - return true + let participants: Signal<[Peer], NoError> = combineLatest(chatLocations.map { chatLocation in + searchPeerMembers(context: context, peerId: chatLocation.peerId, chatLocation: chatLocation, query: normalizedQuery) |> take(1) |> mapToSignal { participants -> Signal<[Peer], NoError> in + return context.account.viewTracker.aroundMessageOfInterestHistoryViewForLocation(.peer(chatLocation.peerId), count: 100, tagMask: nil, orderStatistics: [], additionalData: []) |> take(1) |> map { view in + let latestIds:[PeerId] = view.0.entries.reversed().compactMap({ entry in + if entry.message.media.first is TelegramMediaAction { + return nil + } + return entry.message.author?.id + }) + + var sorted = participants.sorted{ lhs, rhs in + let lhsIndex = latestIds.firstIndex(where: {$0 == lhs.id}) + let rhsIndex = latestIds.firstIndex(where: {$0 == rhs.id}) + if let lhsIndex = lhsIndex, let rhsIndex = rhsIndex { + return lhsIndex < rhsIndex + } else if lhsIndex == nil && rhsIndex != nil { + return false + } else if lhsIndex != nil && rhsIndex == nil { + return true + } else { + return lhs.displayTitle < rhs.displayTitle + } + } - if let addressName = peer.addressName, addressName.lowercased().hasPrefix(normalizedQuery) { - return true + + if let index = sorted.firstIndex(where: {$0.id == context.peerId}) { + sorted.move(at: index, to: 0) } - return peer.addressName == nil && normalizedQuery.isEmpty - }).sorted(by: { lhs, rhs in - let result = lhs.indexName.indexName(.lastNameFirst).compare(rhs.indexName.indexName(.lastNameFirst)) - return result == .orderedAscending - }) + + return sorted + } + + } + }) |> map { values in + var result:[Peer] = [] + for value in values { + result.append(contentsOf: value) + } + return uniquePeers(from: result) + } + + let peers = combineLatest(chatLocations.map { context.account.postbox.loadedPeerWithId($0.peerId) }) + + let result = combineLatest(participants, peers) |> map { participants, peers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in + + var participants = participants + + for peer in peers { + if peer.isSupergroup { + participants.append(peer) + } + } + + let filteredParticipants = participants.filter { peer in + + switch filter { + case let .plain(includeNameless, includeInlineBots): + if !includeNameless, peer.addressName == nil || peer.addressName!.isEmpty { + return false + } + if !includeInlineBots, let peer = peer as? TelegramUser, peer.botInfo?.inlinePlaceholder != nil { + return false + } + case let .filterSelf(includeNameless, includeInlineBots): + if !includeNameless, peer.addressName == nil || peer.addressName!.isEmpty { + return false + } + if peer.id == context.peerId { + return false + } + + if !includeInlineBots, let peer = peer as? TelegramUser, peer.botInfo?.inlinePlaceholder != nil { + return false + } + } + if peer.displayTitle == L10n.peerDeletedUser { + return false + } + if peer.indexName.matchesByTokens(normalizedQuery) { + return true + } + if let addressName = peer.addressName, addressName.lowercased().hasPrefix(normalizedQuery) { + return true + } - return { _ in return .mentions(filteredParticipants) } + return peer.addressName == nil && normalizedQuery.isEmpty + } + + return { _ in return .mentions(filteredParticipants) } } - return (inputQuery, signal |> then(participants)) + return (inputQuery, signal |> then(result)) + case let .emoji(query, firstWord): + if !query.isEmpty { + let signal = context.sharedContext.inputSource.searchEmoji(postbox: context.account.postbox, engine: context.engine, sharedContext: context.sharedContext, query: query, completeMatch: query.length < 3, checkPrediction: firstWord) |> delay(firstWord ? 0.3 : 0, queue: .concurrentDefaultQueue()) + + if firstWord { + return (inputQuery, .single({ _ in return nil }) |> then(combineLatest(signal, recentUsedEmoji(postbox: context.account.postbox)) |> map { matches, emojies -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in + let sorted = matches.sorted(by: { lhs, rhs in + let lhsIndex = emojies.emojies.firstIndex(of: lhs) ?? Int.max + let rhsIndex = emojies.emojies.firstIndex(of: rhs) ?? Int.max + return lhsIndex < rhsIndex + }) + + return { _ in return .emoji(sorted, firstWord) } + })) + } else { + return (inputQuery, combineLatest(signal, recentUsedEmoji(postbox: context.account.postbox)) |> map { matches, emojies -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in + let sorted = matches.sorted(by: { lhs, rhs in + let lhsIndex = emojies.emojies.firstIndex(of: lhs) ?? Int.max + let rhsIndex = emojies.emojies.firstIndex(of: rhs) ?? Int.max + return lhsIndex < rhsIndex + }) + + + return { _ in return .emoji(sorted, firstWord) } + }) + } + + + } else { + if firstWord { + return (nil, .single({ _ in return nil })) + } else { + return (inputQuery, recentUsedEmoji(postbox: context.account.postbox) |> map { emojis -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in + return { _ in return .emoji(emojis.emojies, firstWord) } + }) + } + } default: return (nil, .single({ _ in return nil })) } @@ -269,32 +530,167 @@ func chatContextQueryForSearchMention(peer: Peer, _ inputQuery: ChatPresentation private let dataDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType([.link]).rawValue) -func urlPreviewStateForChatInterfacePresentationState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentQuery: String?) -> (String?, Signal<(TelegramMediaWebpage?) -> TelegramMediaWebpage?, NoError>)? { + func urlPreviewStateForChatInterfacePresentationState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, context: AccountContext, currentQuery: String?, disableEditingPreview: ((String)->Void)? = nil) -> Signal<(String?, Signal<(TelegramMediaWebpage?) -> TelegramMediaWebpage?, NoError>)?, NoError> { - if let dataDetector = dataDetector { - let text = chatPresentationInterfaceState.effectiveInput.inputText - let utf16 = text.utf16 + return Signal { subscriber in + + var detector = dataDetector + - var detectedUrl: String? + if chatPresentationInterfaceState.state == .editing, let media = chatPresentationInterfaceState.interfaceState.editState?.message.media.first { + if media is TelegramMediaFile || media is TelegramMediaImage { + subscriber.putNext((nil, .single({ _ in return nil }))) + subscriber.putCompletion() + detector = nil + } + } - let matches = dataDetector.matches(in: text, options: [], range: NSRange(location: 0, length: utf16.count)) - if let match = matches.first { - let urlText = (text as NSString).substring(with: match.range) - detectedUrl = urlText + if let peer = chatPresentationInterfaceState.peer, peer.webUrlRestricted { + subscriber.putNext((nil, .single({ _ in return nil }))) + subscriber.putCompletion() + detector = nil } - if detectedUrl != currentQuery { - if let detectedUrl = detectedUrl { - return (detectedUrl, webpagePreview(account: account, url: detectedUrl) |> map { value in - return { _ in return value } - }) + if chatPresentationInterfaceState.state == .editing, let media = chatPresentationInterfaceState.interfaceState.editState?.message.media.first { + if let media = media as? TelegramMediaWebpage { + let url: String? + switch media.content { + case let .Loaded(content): + url = content.url + case let .Pending(content): + url = content.1 + } + subscriber.putNext((url, .single({ _ in return media }))) + subscriber.putCompletion() + detector = nil + } + } + + if let dataDetector = detector { + + var detectedUrl: String? + + var detectedRange: NSRange = NSMakeRange(NSNotFound, 0) + let text = chatPresentationInterfaceState.effectiveInput.inputText.prefix(4096) + + var attr = chatPresentationInterfaceState.effectiveInput.attributedString + attr = attr.attributedSubstring(from: NSMakeRange(0, min(attr.length, 4096))) + attr.enumerateAttribute(NSAttributedString.Key(rawValue: TGCustomLinkAttributeName), in: attr.range, options: NSAttributedString.EnumerationOptions(rawValue: 0), using: { (value, range, stop) in + + if let tag = value as? TGInputTextTag, let url = tag.attachment as? String { + detectedUrl = url + detectedRange = range + } + let s: ObjCBool = (detectedUrl != nil) ? true : false + stop.pointee = s + + }) + + let utf16 = text.utf16 + let matches = dataDetector.matches(in: text, options: [], range: NSRange(location: 0, length: utf16.count)) + if let match = matches.first { + let urlText = (text as NSString).substring(with: match.range) + if match.range.location < detectedRange.location { + detectedUrl = urlText + } + } + + if let disableEditingPreview = disableEditingPreview { + if let editState = chatPresentationInterfaceState.interfaceState.editState { + if editState.message.media.isEmpty, let detectedUrl = detectedUrl { + disableEditingPreview(detectedUrl) + subscriber.putNext((nil, .single({ _ in return nil }))) + subscriber.putCompletion() + return EmptyDisposable + } + } + } + + if detectedUrl != currentQuery { + if let detectedUrl = detectedUrl { + let link = inApp(for: detectedUrl.nsstring, context: context, peerId: nil, openInfo: { _, _, _, _ in }, hashtag: { _ in }, command: { _ in }, applyProxy: { _ in }, confirm: false) + + + let invoke:(inAppLink)->Void = { link in + switch link { + case let .external(detectedUrl, _): + subscriber.putNext((detectedUrl, webpagePreview(account: context.account, url: detectedUrl) |> map { value in + return { _ in return value } + })) + case let .followResolvedName(_, username, _, _, _, _): + if username.hasPrefix("_private_") { + subscriber.putNext((nil, .single({ _ in return nil }))) + subscriber.putCompletion() + } else { + subscriber.putNext((detectedUrl, webpagePreview(account: context.account, url: detectedUrl) |> map { value in + return { _ in return value } + })) + } + default: + subscriber.putNext((nil, .single({ _ in return nil }))) + subscriber.putCompletion() + } + } + + if chatPresentationInterfaceState.chatLocation.peerId.namespace == Namespaces.Peer.SecretChat { + let value = FastSettings.isSecretChatWebPreviewAvailable(for: context.account.id.int64) + + if let value = value { + if !value { + subscriber.putNext((nil, .single({ _ in return nil }))) + subscriber.putCompletion() + return EmptyDisposable + } else { + invoke(link) + } + } else { + + var canLoad: Bool = false + switch link { + case .external: + canLoad = true + case let .followResolvedName(_, username, _, _, _, _): + if !username.hasPrefix("_private_") { + canLoad = true + } + default: + canLoad = false + } + + if canLoad { + confirm(for: context.window, header: L10n.chatSecretChatPreviewHeader, information: L10n.chatSecretChatPreviewText, okTitle: L10n.chatSecretChatPreviewOK, cancelTitle: L10n.chatSecretChatPreviewNO, successHandler: { result in + FastSettings.setSecretChatWebPreviewAvailable(for: context.account.id.int64, value: true) + invoke(link) + }, cancelHandler: { + FastSettings.setSecretChatWebPreviewAvailable(for: context.account.id.int64, value: false) + subscriber.putNext((nil, .single({ _ in return nil }))) + subscriber.putCompletion() + }) + } + + } + } else { + invoke(link) + } + + + } else { + subscriber.putNext((nil, .single({ _ in return nil }))) + subscriber.putCompletion() + } } else { - return (nil, .single({ _ in return nil })) + subscriber.putNext(nil) + subscriber.putCompletion() } } else { - return nil + subscriber.putNext((nil, .single({ _ in return nil }))) + subscriber.putCompletion() + } + + return ActionDisposable { + } - } else { - return (nil, .single({ _ in return nil })) } + + } diff --git a/Telegram-Mac/ChatInvoiceItem.swift b/Telegram-Mac/ChatInvoiceItem.swift index 5f4f99d795..5e957d3564 100644 --- a/Telegram-Mac/ChatInvoiceItem.swift +++ b/Telegram-Mac/ChatInvoiceItem.swift @@ -7,28 +7,117 @@ // import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit class ChatInvoiceItem: ChatRowItem { fileprivate let media:TelegramMediaInvoice fileprivate let textLayout:TextViewLayout fileprivate var arguments:TransformImageArguments? - override init(_ initialSize: NSSize, _ chatInteraction: ChatInteraction, _ account: Account, _ object: ChatHistoryEntry) { + override init(_ initialSize: NSSize, _ chatInteraction: ChatInteraction, _ context: AccountContext, _ object: ChatHistoryEntry, _ downloadSettings: AutomaticMediaDownloadSettings, theme: TelegramPresentationTheme) { let message = object.message! + + let isIncoming: Bool = message.isIncoming(context.account, object.renderType == .bubble) + self.media = message.media[0] as! TelegramMediaInvoice let attr = NSMutableAttributedString() - _ = attr.append(string: media.description, color: theme.colors.text, font: .normal(.text)) - attr.detectLinks(type: [.Links]) + _ = attr.append(string: media.title, color: theme.chat.linkColor(isIncoming, object.renderType == .bubble), font: .medium(.text)) + _ = attr.append(string: "\n") + _ = attr.append(string: media.description, color: theme.chat.textColor(isIncoming, object.renderType == .bubble), font: .normal(.text)) + attr.detectLinks(type: [.Links], color: theme.chat.linkColor(isIncoming, object.renderType == .bubble)) textLayout = TextViewLayout(attr) - super.init(initialSize, chatInteraction, account, object) + super.init(initialSize, chatInteraction, context, object, downloadSettings, theme: theme) } + override var isBubbleFullFilled: Bool { + if let _ = media.photo { + return true + } else { + return super.isBubbleFullFilled + } + } + + var mediaBubbleCornerInset: CGFloat { + return 1 + } + + override var instantlyResize: Bool { + return true + } + + override var contentOffset: NSPoint { + var offset = super.contentOffset + // + if hasBubble { + if forwardNameLayout != nil { + offset.y += defaultContentInnerInset + } else if !isBubbleFullFilled { + offset.y += (defaultContentInnerInset + 2) + } + } + + if hasBubble && authorText == nil && replyModel == nil && forwardNameLayout == nil { + offset.y -= (defaultContentInnerInset + self.mediaBubbleCornerInset * 2 - (isBubbleFullFilled ? 1 : 0)) + } + return offset + } + + override var elementsContentInset: CGFloat { + if hasBubble && isBubbleFullFilled { + return bubbleContentInset + } + return super.elementsContentInset + } + + override var realContentSize: NSSize { + var size = super.realContentSize + + if isBubbleFullFilled { + size.width -= bubbleContentInset * 2 + } + return size + } + + override var additionalLineForDateInBubbleState: CGFloat? { + if isForceRightLine { + return rightSize.height + } + if let line = textLayout.lines.last, line.frame.width > realContentSize.width - (rightSize.width + insetBetweenContentAndDate ) { + return rightSize.height + } + if postAuthor != nil { + return isStateOverlayLayout ? nil : rightSize.height + } + return super.additionalLineForDateInBubbleState + } + + + override var bubbleFrame: NSRect { + var frame = super.bubbleFrame + + if isBubbleFullFilled { + frame.size.width = contentSize.width + additionBubbleInset + if hasBubble { + frame.size.width += self.mediaBubbleCornerInset * 2 + } + } + + return frame + } + + override var defaultContentTopOffset: CGFloat { + if isBubbled && !hasBubble { + return 2 + } + return isBubbled && !isBubbleFullFilled ? 14 : super.defaultContentTopOffset + } + override func makeContentSize(_ width: CGFloat) -> NSSize { var contentSize = NSMakeSize(width, 0) @@ -37,18 +126,39 @@ class ChatInvoiceItem: ChatRowItem { for attr in photo.attributes { switch attr { - case .ImageSize(let size): - //videoSize.fitted() - contentSize = size.fitted(NSMakeSize(200, 200)) - arguments = TransformImageArguments(corners: ImageCorners(radius: .cornerRadius), imageSize: size, boundingSize: contentSize, intrinsicInsets: NSEdgeInsets()) + case let .ImageSize(size): + contentSize = size.size.aspectFitted(NSMakeSize(width, 200)) + var topLeftRadius: CGFloat = .cornerRadius + let bottomLeftRadius: CGFloat = .cornerRadius + var topRightRadius: CGFloat = .cornerRadius + let bottomRightRadius: CGFloat = .cornerRadius + if isBubbled { + if !hasHeader { + topLeftRadius = topLeftRadius * 3 + 2 + topRightRadius = topRightRadius * 3 + 2 + } + } + let corners = ImageCorners(topLeft: .Corner(topLeftRadius), topRight: .Corner(topRightRadius), bottomLeft: .Corner(bottomLeftRadius), bottomRight: .Corner(bottomRightRadius)) + arguments = TransformImageArguments(corners: corners, imageSize: size.size, boundingSize: contentSize, intrinsicInsets: NSEdgeInsets()) default: break } } } - textLayout.measure(width: contentSize.width) - contentSize.height += textLayout.layoutSize.height + defaultContentTopOffset + + var maxWidth: CGFloat = contentSize.width + if hasBubble { + maxWidth -= bubbleDefaultInnerInset + } + + textLayout.measure(width: maxWidth) + if arguments == nil { + contentSize.width = textLayout.layoutSize.width + } else { + contentSize.height += defaultContentTopOffset + } + contentSize.height += textLayout.layoutSize.height return contentSize } @@ -64,7 +174,24 @@ class ChatInvoiceView : ChatRowView { required init(frame frameRect: NSRect) { super.init(frame: frameRect) addSubview(textView) - addSubview(imageView) + } + override func contentFrame(_ item: ChatRowItem) -> NSRect { + var rect = super.contentFrame(item) + guard let item = item as? ChatInvoiceItem else { + return rect + } + if item.isBubbled, item.isBubbleFullFilled { + rect.origin.x -= item.bubbleContentInset + if item.hasBubble { + rect.origin.x += item.mediaBubbleCornerInset + } + } + + return rect + } + + override var selectableTextViews: [TextView] { + return [textView] } required init?(coder: NSCoder) { @@ -74,7 +201,7 @@ class ChatInvoiceView : ChatRowView { override func layout() { super.layout() if let item = item as? ChatInvoiceItem { - textView.update(item.textLayout, origin: NSMakePoint(0, contentView.frame.height - item.textLayout.layoutSize.height)) + textView.setFrameOrigin(NSMakePoint(item.elementsContentInset, (item.arguments == nil ? 0 : imageView.frame.maxY + item.defaultContentInnerInset))) } } @@ -83,17 +210,18 @@ class ChatInvoiceView : ChatRowView { if let item = item as? ChatInvoiceItem { textView.update(item.textLayout) - if let photo = item.media.photo, let arguments = item.arguments { + if let photo = item.media.photo, let arguments = item.arguments, let message = item.message { addSubview(imageView) - imageView.setSignal(account: item.account, signal: chatMessageWebFilePhoto(account: item.account, photo: photo, scale: backingScaleFactor)) + imageView.setSignal(chatMessageWebFilePhoto(account: item.context.account, photo: photo, scale: backingScaleFactor)) imageView.set(arguments: arguments) imageView.setFrameSize(arguments.boundingSize) - _ = item.account.postbox.mediaBox.fetchedResource(photo.resource, tag: nil).start() + _ = fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, reference: MediaResourceReference.media(media: AnyMediaReference.message(message: MessageReference(message), media: photo), resource: photo.resource)).start() } else { imageView.removeFromSuperview() } } + needsLayout = true } } diff --git a/Telegram-Mac/ChatLayoutUtils.swift b/Telegram-Mac/ChatLayoutUtils.swift index 1080e6d095..fafefd6af1 100644 --- a/Telegram-Mac/ChatLayoutUtils.swift +++ b/Telegram-Mac/ChatLayoutUtils.swift @@ -7,39 +7,97 @@ // import Cocoa -import TelegramCoreMac -import PostboxMac +import TelegramCore + +import Postbox class ChatLayoutUtils: NSObject { - static func contentSize(for media:Media, with width: CGFloat) -> NSSize { + static func contentSize(for media:Media, with width: CGFloat, hasText: Bool = false) -> NSSize { var size:NSSize = NSMakeSize(width, 40.0) let maxSize = NSMakeSize(min(width,320), min(width,320)) if let image = media as? TelegramMediaImage { - size = image.representationForDisplayAtSize(maxSize)?.dimensions.fitted(maxSize) ?? maxSize - size = NSMakeSize(max(40, size.width), max(40, size.height)) + size = image.representationForDisplayAtSize(PixelDimensions(maxSize))?.dimensions.size.fitted(maxSize) ?? maxSize + if size.width < 100 && size.height < 100 { + size = size.aspectFitted(NSMakeSize(200, 200)) + } + if hasText { + size.width = max(maxSize.width, size.width) + } + size.width = max(size.width, 100) + size = NSMakeSize(max(46, size.width), max(46, size.height)) } else if let file = media as? TelegramMediaFile { var contentSize:NSSize = NSZeroSize for attr in file.attributes { if case let .ImageSize(size) = attr { - contentSize = size + contentSize = size.size } else if case let .Video(_,video, _) = attr { - contentSize = video + contentSize = video.size + if contentSize.width < 50 && contentSize.height < 50 { + contentSize = maxSize + } + } else if case .Audio = attr { + return NSMakeSize(width, 40) } } - - if file.isSticker { - size = contentSize.aspectFitted(NSMakeSize(180, 180)) + if file.isAnimatedSticker { + let dimensions = file.dimensions?.size + size = NSMakeSize(240, 240) + if file.isEmojiAnimatedSticker { + size = NSMakeSize(112, 112) + } + if let dimensions = dimensions { + size = dimensions.aspectFitted(size) + } + } else if file.isStaticSticker { + if contentSize == NSZeroSize { + return NSMakeSize(210, 210) + } + size = contentSize.aspectFitted(NSMakeSize(210, 210)) + size = NSMakeSize(max(size.width, 40), max(size.height, 40)) } else if file.isInstantVideo { - size = contentSize.fitted(NSMakeSize(200, 200)) - } else if file.isVideo || file.isAnimated { - size = contentSize.fitted(maxSize) + size = NSMakeSize(280, 280) + } else if file.isVideo || (file.isAnimated && !file.mimeType.lowercased().hasSuffix("gif")) { + + var contentSize = contentSize + + + if contentSize.width == 0 || contentSize.height == 0 { + contentSize = NSMakeSize(300, 300) + } + + let aspectRatio = contentSize.width / contentSize.height + let addition = max(300 - contentSize.width, 300 - contentSize.height) + + if addition > 0 { + contentSize.width += addition * aspectRatio + contentSize.height += addition + } + + if file.isVideo && contentSize.width > contentSize.height { + size = contentSize.aspectFitted(NSMakeSize(min(420, width), contentSize.height)) + } else { + size = contentSize.fitted(maxSize) + if hasText { + // size.width = max(maxSize.width, size.width) + } + } + + + + + if hasText { + size.width = max(maxSize.width, size.width) + } + } else if contentSize.height > 0 { size = NSMakeSize(width, 70) + } else if !file.previewRepresentations.isEmpty { + size = NSMakeSize(width, 70) } } else if let media = media as? TelegramMediaMap { @@ -52,23 +110,29 @@ class ChatLayoutUtils: NSObject { if let file = media.file { return contentSize(for: file, with: width) } + } else if media is TelegramMediaDice { + size = NSMakeSize(128, 128) } return size } - static func contentNode(for media:Media) -> ChatMediaContentView.Type { + static func contentNode(for media:Media, packs: Bool = false) -> ChatMediaContentView.Type { if media is TelegramMediaImage { return ChatInteractiveContentView.self } else if let file = media as? TelegramMediaFile { - if file.isSticker { + if file.mimeType == "image/webp" && !packs { + return MediaAnimatedStickerView.self + } else if file.isAnimatedSticker { + return MediaAnimatedStickerView.self + } else if file.isStaticSticker { return ChatStickerContentView.self } else if file.isInstantVideo { return ChatVideoMessageContentView.self } else if file.isVideo && !file.isAnimated { return ChatInteractiveContentView.self - } else if file.isAnimated { + } else if file.isAnimated && !file.mimeType.lowercased().hasSuffix("gif") { return ChatGIFContentView.self } else if file.isVoice { return ChatVoiceContentView.self @@ -79,6 +143,12 @@ class ChatLayoutUtils: NSObject { } } else if media is TelegramMediaMap { return ChatMapContentView.self + } else if let media = media as? TelegramMediaDice { + if media.emoji == slotsEmoji { + return SlotsMediaContentView.self + } else { + return ChatDiceContentView.self + } } else if let media = media as? TelegramMediaGame { if let file = media.file { return contentNode(for: file) diff --git a/Telegram-Mac/ChatListController.swift b/Telegram-Mac/ChatListController.swift index b5417509a8..dc05c0c19d 100644 --- a/Telegram-Mac/ChatListController.swift +++ b/Telegram-Mac/ChatListController.swift @@ -8,114 +8,686 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac -import PostboxMac -import TelegramCoreMac +import SwiftSignalKit +import Postbox +import TelegramCore -extension ChatListEntry: Identifiable { - public var stableId: AnyHashable { + +enum UIChatListEntryId : Hashable { + case chatId(PeerId, Int32?) + case groupId(PeerGroupId) + case reveal + case empty + case loading +} + +struct ChatListInputActivity : Equatable { + let peer: PeerEquatable + let activity: PeerInputActivity + init(_ peer: Peer, _ activity: PeerInputActivity) { + self.peer = PeerEquatable(peer) + self.activity = activity + } +} + +struct ChatListPeerInputActivities : Equatable { + let activities: [PeerId: [ChatListInputActivity]] + + init(activities: [PeerId: [ChatListInputActivity]]) { + self.activities = activities + } + func withUpdatedActivities(_ activities: [PeerId: [ChatListInputActivity]]) -> ChatListPeerInputActivities { + return ChatListPeerInputActivities(activities: activities) + } +} + +struct ChatListState: Equatable { + let activities: ChatListPeerInputActivities + + func updateActivities(_ f:(ChatListPeerInputActivities)->ChatListPeerInputActivities) -> ChatListState { + return ChatListState(activities: f(self.activities)) + } +} + +struct UIChatAdditionalItem : Equatable { + static func == (lhs: UIChatAdditionalItem, rhs: UIChatAdditionalItem) -> Bool { + return lhs.item.isEqual(to: rhs.item) && lhs.index == rhs.index + } + + let item: AdditionalChatListItem + let index: Int +} + + +enum UIChatListEntry : Identifiable, Comparable { + case chat(ChatListEntry, [ChatListInputActivity], UIChatAdditionalItem?, filter: ChatListFilter?) + case group(Int, PeerGroupId, [ChatListGroupReferencePeer], Message?, PeerGroupUnreadCountersCombinedSummary, TotalUnreadCountDisplayCategory, Bool, HiddenArchiveStatus) + case reveal([ChatListFilter], ChatListFilter?, ChatListFilterBadges) + case empty(ChatListFilter?) + case loading(ChatListFilter?) + static func == (lhs: UIChatListEntry, rhs: UIChatListEntry) -> Bool { + switch lhs { + case let .chat(entry, activity, additionItem, filter): + if case .chat(entry, activity, additionItem, filter) = rhs { + return true + } else { + return false + } + case let .group(index, groupId, peers, lhsMessage, unreadState, unreadCountDisplayCategory, animated, isHidden): + if case .group(index, groupId, peers, let rhsMessage, unreadState, unreadCountDisplayCategory, animated, isHidden) = rhs { + if let lhsMessage = lhsMessage, let rhsMessage = rhsMessage { + return isEqualMessages(lhsMessage, rhsMessage) + } else if (lhsMessage != nil) != (rhsMessage != nil) { + return false + } else { + return true + } + } else { + return false + } + case let .reveal(filters, current, counters): + if case .reveal(filters, current, counters) = rhs { + return true + } else { + return false + } + case let .empty(filter): + if case .empty(filter) = rhs { + return true + } else { + return false + } + case let .loading(filter): + if case .loading(filter) = rhs { + return true + } else { + return false + } + } + } + + var index: ChatListIndex { switch self { - case let .HoleEntry(hole): - return Int64(hole.index.id.id) - default: - return index.messageIndex.id.peerId.toInt64() + case let .chat(entry, _, additionItem, _): + if let additionItem = additionItem { + var current = MessageIndex.absoluteUpperBound().globalPredecessor() + for _ in 0 ..< additionItem.index { + current = current.globalPredecessor() + } + return ChatListIndex(pinningIndex: 0, messageIndex: current) + } + switch entry { + case let .HoleEntry(hole): + return ChatListIndex(pinningIndex: nil, messageIndex: hole.index) + case let .MessageEntry(values): + return values.0 + } + case .reveal: + return ChatListIndex(pinningIndex: 0, messageIndex: MessageIndex.absoluteUpperBound()) + case let .group(values): + var index = MessageIndex.absoluteUpperBound().globalPredecessor() + for _ in 0 ..< values.0 { + index = index.peerLocalPredecessor() + } + return ChatListIndex(pinningIndex: 0, messageIndex: index) + case .empty: + return ChatListIndex(pinningIndex: 0, messageIndex: MessageIndex.absoluteUpperBound().globalPredecessor()) + case .loading: + return ChatListIndex(pinningIndex: 0, messageIndex: MessageIndex.absoluteUpperBound().globalPredecessor()) } } + + static func < (lhs: UIChatListEntry, rhs: UIChatListEntry) -> Bool { + return lhs.index < rhs.index + } + + var stableId: UIChatListEntryId { + switch self { + case let .chat(entry, _, _, filterId): + return .chatId(entry.index.messageIndex.id.peerId, filterId?.id) + case let .group(_, groupId, _, _, _, _, _, _): + return .groupId(groupId) + case .reveal: + return .reveal + case .empty: + return .empty + case .loading: + return .loading + } + } + } -fileprivate func prepareEntries(from:[AppearanceWrapperEntry]?, to:[AppearanceWrapperEntry], account:Account, initialSize:NSSize, animated:Bool, scrollState:TableScrollState? = nil, onMainQueue: Bool = false) -> Signal { +fileprivate func prepareEntries(from:[AppearanceWrapperEntry]?, to:[AppearanceWrapperEntry], adIndex: UInt16?, context: AccountContext, initialSize:NSSize, animated:Bool, scrollState:TableScrollState? = nil, groupId: PeerGroupId, setupFilter: @escaping(ChatListFilter?)->Void, openFilterSettings: @escaping(ChatListFilter?)->Void, tabsMenuItems: @escaping(ChatListFilter?)->[ContextMenuItem]) -> Signal { return Signal { subscriber in - let (deleted,inserted,updated) = proccessEntries(from, right: to, { entry -> TableRowItem in - + var cancelled: Bool = false + + func makeItem(_ entry: AppearanceWrapperEntry) -> TableRowItem { switch entry.entry { - case let .HoleEntry(hole): - return ChatListHoleRowItem(initialSize, account, hole) - case let .MessageEntry(index, message, readState, notifySettings,embeddedState, renderedPeer, summaryInfo): - - var pinnedType: ChatListPinnedType = .some - if let i = to.index(of: entry) { - if index.pinningIndex != nil { - if i > 0 { - if case let .MessageEntry(index, _, _, _ ,_ , _, _) = to[i - 1].entry, index.pinningIndex == nil { - pinnedType = .last - } - } - } else { + case let .chat(inner, activities, addition, filter): + switch inner { + case let .HoleEntry(hole): + return ChatListHoleRowItem(initialSize, context, hole) + case let .MessageEntry(index, messages, readState, isMuted, embeddedState, renderedPeer, peerPresence, summaryInfo, hasFailed, isContact): + var pinnedType: ChatListPinnedType = .some + if let addition = addition { + pinnedType = .ad(addition.item) + } else if index.pinningIndex == nil { pinnedType = .none } + return ChatListRowItem(initialSize, context: context, messages: messages, index: inner.index, readState: readState, isMuted: isMuted, embeddedState: embeddedState, pinnedType: pinnedType, renderedPeer: renderedPeer, peerPresence: peerPresence, summaryInfo: summaryInfo, activities: activities, associatedGroupId: groupId, hasFailed: hasFailed, filter: filter) } - return ChatListRowItem(initialSize, account: account, message: message, readState:readState, notificationSettings: notifySettings, embeddedState: embeddedState, pinnedType: pinnedType, renderedPeer: renderedPeer, summaryInfo: summaryInfo) + case let .group(_, groupId, peers, message, unreadState, unreadCountDisplayCategory, animated, archiveStatus): + return ChatListRowItem(initialSize, context: context, pinnedType: .none, groupId: groupId, peers: peers, messages: message != nil ? [message!] : [], unreadState: unreadState, unreadCountDisplayCategory: unreadCountDisplayCategory, animateGroup: animated, archiveStatus: archiveStatus) + case let .reveal(tabs, selected, counters): + return ChatListRevealItem(initialSize, context: context, tabs: tabs, selected: selected, counters: counters, action: setupFilter, openSettings: { + openFilterSettings(nil) + }, menuItems: tabsMenuItems) + case let .empty(filter): + return ChatListEmptyRowItem(initialSize, stableId: entry.stableId, filter: filter, context: context, openFilterSettings: openFilterSettings) + case let .loading(filter): + return ChatListLoadingRowItem(initialSize, stableId: entry.stableId, filter: filter, context: context) } - - }) - let nState = scrollState ?? (animated ? .none(nil) : .saveVisible(.lower)) - let transition = TableUpdateTransition(deleted: deleted, inserted: inserted, updated:updated, animated:animated, state: nState) + } + + let (deleted,inserted,updated) = proccessEntries(from, right: to, { entry -> TableRowItem in + return makeItem(entry) + }) + + let nState = scrollState ?? (animated ? .none(nil) : .saveVisible(.lower)) + let transition = TableUpdateTransition(deleted: deleted, inserted: inserted, updated:updated, animated: animated, state: nState, animateVisibleOnly: false) + subscriber.putNext(transition) subscriber.putCompletion() - return EmptyDisposable - } |> runOn(onMainQueue ? Queue.mainQueue() : prepareQueue) + return ActionDisposable { + cancelled = true + } + } +} + +enum HiddenArchiveStatus : Equatable { + case normal + case collapsed + case hidden(Bool) + + var rawValue: Int { + switch self { + case .normal: + return 0 + case .collapsed: + return 1 + case .hidden: + return 2 + } + } + var isHidden: Bool { + switch self { + case .hidden: + return true + default: + return false + } + } + + init?(rawValue: Int) { + switch rawValue { + case 0: + self = .normal + case 1: + self = .collapsed + case 2: + self = .hidden(true) + default: + return nil + } + } } +struct FilterData : Equatable { + let filter: ChatListFilter? + let tabs: [ChatListFilter] + let sidebar: Bool + let request: ChatListIndexRequest + init(filter: ChatListFilter?, tabs: [ChatListFilter], sidebar: Bool, request: ChatListIndexRequest) { + self.filter = filter + self.tabs = tabs + self.sidebar = sidebar + self.request = request + } + func withUpdatedFilter(_ filter: ChatListFilter?) -> FilterData { + return FilterData(filter: filter, tabs: self.tabs, sidebar: self.sidebar, request: self.request) + } + func withUpdatedTabs(_ tabs: [ChatListFilter]) -> FilterData { + return FilterData(filter: self.filter, tabs: tabs, sidebar: self.sidebar, request: self.request) + } + func withUpdatedSidebar(_ sidebar: Bool) -> FilterData { + return FilterData(filter: self.filter, tabs: self.tabs, sidebar: sidebar, request: self.request) + } + func withUpdatedRequest(_ request: ChatListIndexRequest) -> FilterData { + return FilterData(filter: self.filter, tabs: self.tabs, sidebar: sidebar, request: request) + } +} +private struct HiddenItems : Equatable { + let archive: HiddenArchiveStatus + let promo: Set +} class ChatListController : PeersListController { - - private let request = Promise() + + private let filter = ValuePromise(ignoreRepeated: true) + private let _filterValue = Atomic(value: FilterData(filter: nil, tabs: [], sidebar: false, request: .Initial(50, nil))) + private var filterValue: FilterData? { + return _filterValue.with { $0 } + } + + var filterSignal : Signal { + return self.filter.get() + } + + func updateFilter(_ f:(FilterData)->FilterData) { + var changedFolder = false + filter.set(_filterValue.modify { previous in + var current = f(previous) + if previous.filter?.id != current.filter?.id { + current = current.withUpdatedRequest(.Initial(max(Int(context.window.frame.height / 70) + 3, 12), nil)) + changedFolder = true + } + return current + }) + if changedFolder { + self.removeRevealStateIfNeeded(nil) + } + self.genericView.searchView.change(state: .None, true) + setCenterTitle(self.defaultBarTitle) + } + private let previousChatList:Atomic = Atomic(value: nil) private let first = Atomic(value:true) - + private let animated = Atomic(value: false) + private let removePeerIdGroupDisposable = MetaDisposable() + private let disposable = MetaDisposable() + private let scrollDisposable = MetaDisposable() + private let reorderDisposable = MetaDisposable() + private let globalPeerDisposable = MetaDisposable() + private let archivationTooltipDisposable = MetaDisposable() + private let undoTooltipControl: UndoTooltipControl + private let animateGroupNextTransition:Atomic = Atomic(value: nil) + private var activityStatusesDisposable:Disposable? + + private let suggestAutoarchiveDisposable = MetaDisposable() + + private var didSuggestAutoarchive: Bool = false + + private let hiddenItemsValue: Atomic = Atomic(value: HiddenItems(archive: FastSettings.archiveStatus, promo: Set())) + private let hiddenItemsState: ValuePromise = ValuePromise(HiddenItems(archive: FastSettings.archiveStatus, promo: Set()), ignoreRepeated: true) + + private let filterDisposable = MetaDisposable() + + private func updateHiddenStateState(_ f:(HiddenItems)->HiddenItems) { + let result = hiddenItemsValue.modify(f) + FastSettings.archiveStatus = result.archive + hiddenItemsState.set(result) + } + + override func viewDidResized(_ size: NSSize) { + super.viewDidResized(size) + } + override func viewDidLoad() { super.viewDidLoad() + + let initialSize = self.atomicSize - let account = self.account + let context = self.context let previousChatList = self.previousChatList - let first = self.first - let onMainQueue:Atomic = Atomic(value: true) - let previousEntries:Atomic<[AppearanceWrapperEntry]?> = Atomic(value: nil) - let list:Signal = (request.get() |> distinctUntilChanged |> mapToSignal { (location) -> Signal in - - var signal:Signal<(ChatListView,ViewUpdateType),Void> - var scroll:TableScrollState? = nil - switch(location) { + let first = Atomic<(ChatListIndex?, ChatListIndex?)>(value: (nil, nil)) + let scrollUp:Atomic = self.first + let groupId = self.mode.groupId + let previousEntries:Atomic<[AppearanceWrapperEntry]?> = Atomic(value: nil) + let animated: Atomic = self.animated + let animateGroupNextTransition = self.animateGroupNextTransition + var scroll:TableScrollState? = nil + + let initialState = ChatListState(activities: ChatListPeerInputActivities(activities: [:])) + let statePromise:ValuePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue: Atomic = Atomic(value: initialState) + + let updateState:((ChatListState)->ChatListState)->Void = { f in + statePromise.set(stateValue.modify(f)) + } + + + let postbox = context.account.postbox + let previousPeerCache = Atomic<[PeerId: Peer]>(value: [:]) + let previousActivities = Atomic(value: nil) + self.activityStatusesDisposable = (context.account.allPeerInputActivities() + |> mapToSignal { activitiesByPeerId -> Signal<[PeerId: [ChatListInputActivity]], NoError> in + var foundAllPeers = true + var cachedResult: [PeerId: [ChatListInputActivity]] = [:] + previousPeerCache.with { dict -> Void in + for (chatPeerId, activities) in activitiesByPeerId { + guard case .global = chatPeerId.category else { + continue + } + var cachedChatResult: [ChatListInputActivity] = [] + for (peerId, activity) in activities { + if let peer = dict[peerId] { + cachedChatResult.append(ChatListInputActivity(peer, activity)) + } else { + foundAllPeers = false + break + } + cachedResult[chatPeerId.peerId] = cachedChatResult + } + } + } + if foundAllPeers { + return .single(cachedResult) + } else { + return postbox.transaction { transaction -> [PeerId: [ChatListInputActivity]] in + var result: [PeerId: [ChatListInputActivity]] = [:] + var peerCache: [PeerId: Peer] = [:] + for (chatPeerId, activities) in activitiesByPeerId { + guard case .global = chatPeerId.category else { + continue + } + + var chatResult: [ChatListInputActivity] = [] + + for (peerId, activity) in activities { + if let peer = transaction.getPeer(peerId) { + chatResult.append(ChatListInputActivity(peer, activity)) + peerCache[peerId] = peer + } + } + + result[chatPeerId.peerId] = chatResult + } + let _ = previousPeerCache.swap(peerCache) + return result + } + } + } + |> map { activities -> ChatListPeerInputActivities? in + return previousActivities.modify { current in + var updated = false + let currentList: [PeerId: [ChatListInputActivity]] = current?.activities ?? [:] + if currentList.count != activities.count { + updated = true + } else { + outer: for (peerId, currentValue) in currentList { + if let value = activities[peerId] { + if currentValue.count != value.count { + updated = true + break outer + } else { + for i in 0 ..< currentValue.count { + if currentValue[i] != value[i] { + updated = true + break outer + } + } + } + } else { + updated = true + break outer + } + } + } + if updated { + if activities.isEmpty { + return nil + } else { + return ChatListPeerInputActivities(activities: activities) + } + } else { + return current + } + } + } + |> deliverOnMainQueue).start(next: { activities in + updateState { + $0.updateActivities { _ in + activities ?? ChatListPeerInputActivities(activities: [:]) + } + } + }) + + let previousLocation: Atomic = Atomic(value: nil) + globalPeerDisposable.set(context.globalPeerHandler.get().start(next: { [weak self] location in + if previousLocation.swap(location) != location { + self?.removeRevealStateIfNeeded(nil) + } + + self?.removeHighlightEvents() + + if let searchController = self?.searchController { + searchController.updateHighlightEvents(location != nil) + } + if location == nil { + self?.setHighlightEvents() + } + })) + + + let signal = filter.get() + + let previousfilter = Atomic(value: self.filterValue) + var firstSwitch: Bool = false + + let chatHistoryView: Signal<(ChatListView, ViewUpdateType, Bool, FilterData, Bool), NoError> = signal |> mapToSignal { data -> Signal<(ChatListView, ViewUpdateType, Bool, FilterData, Bool), NoError> in + + var signal:Signal<(ChatListView,ViewUpdateType), NoError> + var removeNextAnimation: Bool = false + switch data.request { case let .Initial(count, st): - signal = account.viewTracker.tailChatListView(count: count) + signal = context.account.viewTracker.tailChatListView(groupId: groupId, filterPredicate: chatListFilterPredicate(for: data.filter), count: count) + scroll = st + case let .Index(index, st): + signal = context.account.viewTracker.aroundChatListView(groupId: groupId, filterPredicate: chatListFilterPredicate(for: data.filter), index: index, count: 100) scroll = st - case let .Index(index): - signal = account.viewTracker.aroundChatListView(index: index, count: 100) + removeNextAnimation = st != nil } + firstSwitch = previousfilter.swap(data)?.filter?.id != data.filter?.id + return signal |> map { ($0.0, $0.1, removeNextAnimation, data, firstSwitch) } + } + + let setupFilter:(ChatListFilter?)->Void = { [weak self] filter in + self?.updateFilter { + $0.withUpdatedFilter(filter) + } + self?.scrollup(force: true) + } + let openFilterSettings:(ChatListFilter?)->Void = { filter in + if let filter = filter { + context.sharedContext.bindings.rootNavigation().push(ChatListFilterController(context: context, filter: filter)) + } else { + context.sharedContext.bindings.rootNavigation().push(ChatListFiltersListController(context: context)) + } + } + + let previousLayout: Atomic = Atomic(value: context.sharedContext.layout) + + let list:Signal = combineLatest(queue: prepareQueue, chatHistoryView, appearanceSignal, statePromise.get(), hiddenItemsState.get(), appNotificationSettings(accountManager: context.sharedContext.accountManager), chatListFilterItems(engine: context.engine, accountManager: context.sharedContext.accountManager)) |> mapToQueue { value, appearance, state, hiddenItems, inAppSettings, filtersCounter -> Signal in + + let filterData = value.3 - return combineLatest(signal, appearanceSignal |> deliverOnPrepareQueue) |> mapToQueue { (value, appearance) -> Signal in - - if !first.modify({$0}) { - scroll = nil + let removeNextAnimation = value.2 + + let previous = first.swap((value.0.earlierIndex, value.0.laterIndex)) + + let ignoreFlags = scrollUp.swap(false) + + if !ignoreFlags || (!ignoreFlags && (previous.0 != value.0.earlierIndex || previous.1 != value.0.laterIndex) && !removeNextAnimation) { + scroll = nil + } + + + _ = previousChatList.swap(value.0) + + var prepare:[(ChatListEntry, UIChatAdditionalItem?)] = [] + for value in value.0.entries { + prepare.append((value, nil)) + } + if value.0.laterIndex == nil, filterData.filter == nil { + let items = value.0.additionalItemEntries.filter { + !hiddenItems.promo.contains($0.info.peerId) } - _ = previousChatList.swap(value.0) - let entries = value.0.entries.map({AppearanceWrapperEntry(entry: $0, appearance: appearance)}) - - return prepareEntries(from: previousEntries.swap(entries), to: entries, account: account, initialSize: initialSize.modify({$0}), animated: !first.swap(false), scrollState: scroll, onMainQueue: onMainQueue.swap(false)) + for (i, current) in items.enumerated() { + prepare.append((current.entry, UIChatAdditionalItem(item: current.info, index: i + value.0.groupEntries.count))) + } + } + var mapped: [UIChatListEntry] = prepare.map { + return .chat($0, state.activities.activities[$0.index.messageIndex.id.peerId] ?? [], $1, filter: filterData.filter) + } + + if filterData.filter != nil, mapped.isEmpty {} else { + if value.0.laterIndex == nil { + for (i, group) in value.0.groupEntries.reversed().enumerated() { + mapped.append(.group(i, group.groupId, group.renderedPeers, group.message, group.unreadState, inAppSettings.totalUnreadCountDisplayCategory, animateGroupNextTransition.swap(nil) == group.groupId, hiddenItems.archive)) + } + } + } + + + if mapped.isEmpty { + let hasHole = !value.0.entries.filter({ value in + switch value { + case .HoleEntry: + return true + default: + return false + } + }).isEmpty + if !hasHole { + mapped.append(.empty(filterData.filter)) + } + } else { + let isLoading = mapped.filter { value in + switch value { + case let .chat(entry, _, _, _): + if case .HoleEntry = entry { + return false + } else { + return true + } + default: + return true + } + }.isEmpty + if isLoading { + mapped.append(.loading(filterData.filter)) + + } + } + + + if !filterData.tabs.isEmpty && !filterData.sidebar { + mapped.append(.reveal(filterData.tabs, filterData.filter, filtersCounter)) + } + + let entries = mapped.sorted().compactMap { entry -> AppearanceWrapperEntry? in + switch entry { + case let .chat(inner, activities, additionItem, filter): + switch inner { + case .HoleEntry: + return nil + case let .MessageEntry(values): + return AppearanceWrapperEntry(entry: entry, appearance: appearance) + } + case .group: + return AppearanceWrapperEntry(entry: entry, appearance: appearance) + case .reveal: + return AppearanceWrapperEntry(entry: entry, appearance: appearance) + case .empty: + return AppearanceWrapperEntry(entry: entry, appearance: appearance) + case .loading: + return AppearanceWrapperEntry(entry: entry, appearance: appearance) + } + } + + let prev = previousEntries.swap(entries) + + + var animated = animated.swap(true) + + if value.4 && firstSwitch { + animated = false + scroll = .up(true) + firstSwitch = false } + let layoutUpdated = previousLayout.swap(context.sharedContext.layout) != context.sharedContext.layout + + if layoutUpdated { + scroll = .up(false) + animated = false + } + + return prepareEntries(from: prev, to: entries, adIndex: nil, context: context, initialSize: initialSize.with { $0 }, animated: animated, scrollState: scroll, groupId: groupId, setupFilter: setupFilter, openFilterSettings: openFilterSettings, tabsMenuItems: { filter in + return filterContextMenuItems(filter, context: context) + }) + } + + + let appliedTransition = list |> deliverOnMainQueue |> mapToQueue { [weak self] transition -> Signal in + self?.enqueueTransition(transition) + return .complete() + } + + disposable.set(appliedTransition.start()) + + + + var pinnedCount: Int = 0 + self.genericView.tableView.enumerateItems { item -> Bool in + guard let item = item as? ChatListRowItem, item.isFixedItem else {return false} + pinnedCount += 1 + return item.isFixedItem + } + + genericView.tableView.resortController = TableResortController(resortRange: NSMakeRange(0, pinnedCount), start: { row in + + }, resort: { row in + + }, complete: { [weak self] from, to in + self?.resortPinned(from, to) }) - |> deliverOnMainQueue - genericView.tableView.merge(with: list) - request.set(.single(.Initial(50, nil))) + genericView.tableView.addScroll(listener: TableScrollListener(dispatchWhenVisibleRangeUpdated: false, { [weak self] scroll in + guard let `self` = self else { + return + } + self.removeRevealStateIfNeeded(nil) + })) + + genericView.tableView.set(stickClass: ChatListRevealItem.self, handler: { _ in + + }) + + genericView.tableView.emptyChecker = { items in + let filter = items.filter { !($0 is ChatListEmptyRowItem) } + return filter.isEmpty + } + genericView.tableView.setScrollHandler({ [weak self] scroll in let view = previousChatList.modify({$0}) - + self?.removeRevealStateIfNeeded(nil) + if let strongSelf = self, let view = view { var messageIndex:ChatListIndex? @@ -128,50 +700,809 @@ class ChatListController : PeersListController { break } if let messageIndex = messageIndex { - _ = first.swap(true) - strongSelf.request.set(.single(.Index(messageIndex))) + _ = animated.swap(false) + strongSelf.updateFilter { + $0.withUpdatedRequest(.Index(messageIndex, nil)) + } } } + }) + + + + + let filterView = chatListFilterPreferences(engine: context.engine) |> deliverOnMainQueue + switch mode { + case .folder: + self.updateFilter( { + $0.withUpdatedTabs([]).withUpdatedFilter(nil) + } ) + case let .filter(filterId): + filterDisposable.set(filterView.start(next: { [weak self] filters in + var shouldBack: Bool = false + self?.updateFilter { current in + var current = current + if let updated = filters.list.first(where: { $0.id == filterId }) { + current = current.withUpdatedFilter(updated) + } else { + shouldBack = true + current = current.withUpdatedFilter(nil) + } + current = current.withUpdatedTabs([]) + return current + } + if shouldBack { + self?.navigationController?.back() + } + })) + default: + filterDisposable.set(combineLatest(filterView, context.sharedContext.layoutHandler.get()).start(next: { [weak self] filters, layout in + self?.updateFilter( { current in + var current = current + if let filter = current.filter { + if let updated = filters.list.first(where: { $0.id == filter.id }) { + current = current.withUpdatedFilter(updated) + } else { + current = current.withUpdatedFilter(nil) + } + } + + current = current.withUpdatedTabs(filters.list).withUpdatedSidebar(filters.sidebar || layout == .minimisize) + return current + } ) + })) + } + } + + func collapseOrExpandArchive() { + updateHiddenStateState { current in + switch current.archive { + case .collapsed: + return HiddenItems(archive: .normal, promo: current.promo) + default: + return HiddenItems(archive: .collapsed, promo: current.promo) + } + } } - override func scrollup() { + func hidePromoItem(_ peerId: PeerId) { + updateHiddenStateState { current in + var promo = current.promo + promo.insert(peerId) + return HiddenItems(archive: current.archive, promo: promo) + } + _ = hideAccountPromoInfoChat(account: self.context.account, peerId: peerId).start() + } + + func toggleHideArchive() { + updateHiddenStateState { current in + switch current.archive { + case .hidden: + return HiddenItems(archive: .normal, promo: current.promo) + default: + return HiddenItems(archive: .hidden(true), promo: current.promo) + } + } + } + + + func setAnimateGroupNextTransition(_ groupId: PeerGroupId) { + _ = self.animateGroupNextTransition.swap(groupId) - let view = previousChatList.modify({$0}) - if view?.laterIndex != nil { - _ = first.swap(true) - request.set(.single(.Initial(100, .up(true)))) + } + + func addUndoAction(_ action:ChatUndoAction) { + let context = self.context + guard self.context.sharedContext.layout != .minimisize else { return } + self.undoTooltipControl.add(controller: self) + } + + private func enqueueTransition(_ transition: TableUpdateTransition) { + self.genericView.tableView.merge(with: transition) + readyOnce() + switch self.mode { + case .folder: + if self.genericView.tableView.isEmpty { + self.navigationController?.close() + } + default: + break + } + + var first: ChatListRowItem? + self.genericView.tableView.enumerateItems { item -> Bool in + if let item = item as? ChatListRowItem, item.archiveStatus != nil { + first = item + } + + return first == nil + } + + if let first = first, let archiveStatus = first.archiveStatus { + self.genericView.tableView.autohide = TableAutohide(item: first, hideUntilOverscroll: archiveStatus.isHidden, hideHandler: { [weak self] hidden in + self?.updateHiddenStateState { current in + return HiddenItems(archive: .hidden(hidden), promo: current.promo) + } + }) + } else { + self.genericView.tableView.autohide = nil + } + + var pinnedRange: NSRange = NSMakeRange(NSNotFound, 0) + self.genericView.tableView.enumerateItems { item -> Bool in + guard let item = item as? ChatListRowItem else {return true} + switch item.pinnedType { + case .some, .last: + if pinnedRange.location == NSNotFound { + pinnedRange.location = item.index + } + pinnedRange.length += 1 + default: + break + } + return item.isFixedItem || item.groupId != .root + } + + self.searchController?.pinnedItems = self.collectPinnedItems + self.genericView.tableView.resortController?.resortRange = pinnedRange + + + let needPreload = previousChatList.with { $0?.laterIndex == nil } + if needPreload { + var preloadItems:[ChatHistoryPreloadItem] = [] + self.genericView.tableView.enumerateItems(with: { item -> Bool in + guard let item = item as? ChatListRowItem, let index = item.chatListIndex else {return true} + preloadItems.append(.init(index: index, isMuted: item.isMuted, hasUnread: item.hasUnread)) + return preloadItems.count < 30 + }) + context.account.viewTracker.chatListPreloadItems.set(.single(preloadItems) |> delay(0.2, queue: prepareQueue)) + } else { + context.account.viewTracker.chatListPreloadItems.set(.single([])) + } + } + + private func resortPinned(_ from: Int, _ to: Int) { + + var items:[PinnedItemId] = [] + + var offset: Int = 0 + + let groupId: PeerGroupId = self.mode.groupId + + let location: TogglePeerChatPinnedLocation + + if let filter = self.filterValue?.filter { + location = .filter(filter.id) } else { - genericView.tableView.scroll(to: .up(true)) + location = .group(groupId) } + self.genericView.tableView.enumerateItems { item -> Bool in + guard let item = item as? ChatListRowItem else { + offset += 1 + return true + } + if item.groupId != .root || item.isAd { + offset += 1 + } + if let location = item.chatLocation { + switch item.pinnedType { + case .some, .last: + items.append(location.pinnedItemId) + default: + break + } + } + + return item.isFixedItem || item.groupId != .root + } + + items.move(at: from - offset, to: to - offset) + reorderDisposable.set(context.engine.peers.reorderPinnedItemIds(location: location, itemIds: items).start()) + } + + override var collectPinnedItems:[PinnedItemId] { + var items:[PinnedItemId] = [] + + + self.genericView.tableView.enumerateItems { item -> Bool in + guard let item = item as? ChatListRowItem else {return false} + if let location = item.chatLocation { + switch item.pinnedType { + case .some, .last: + items.append(location.pinnedItemId) + default: + break + } + } + return item.isFixedItem || item.groupId != .root + } + return items } - init(_ account:Account, modal:Bool = false) { - super.init(account, followGlobal:!modal) + private var lastScrolledIndex: ChatListIndex? = nil + + + override func scrollup(force: Bool = false) { + + if force { + self.genericView.tableView.scroll(to: .up(true), ignoreLayerAnimation: true) + return + } + + if searchController != nil { + self.genericView.searchView.change(state: .None, true) + return + } + + let view = self.previousChatList.with { $0 } + + if self.genericView.tableView.contentOffset.y == 0, view?.laterIndex == nil { + switch mode { + case .folder: + navigationController?.back() + return + case .filter: + navigationController?.back() + return + case .plain: + break + } + + } + + + let scrollToTop:()->Void = { [weak self] in + guard let `self` = self else {return} + + let view = self.previousChatList.modify({$0}) + if view?.laterIndex != nil { + _ = self.first.swap(true) + self.updateFilter { + $0.withUpdatedRequest(.Initial(50, .up(true))) + } + } else { + if self.genericView.tableView.documentOffset.y == 0 { + if self.filterValue?.filter != nil { + self.updateFilter { + $0.withUpdatedFilter(nil) + } + } else { + self.context.sharedContext.bindings.mainController().showFastChatSettings() + } + } else { + self.genericView.tableView.scroll(to: .up(true), ignoreLayerAnimation: true) + } + } + } + scrollToTop() + + } + + var filterMenuItems: Signal<[SPopoverItem], NoError> { + let context = self.context + + let isEnabled = context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) + |> map { view -> Bool in + let configuration = ChatListFilteringConfiguration(appConfiguration: view.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? .defaultValue) + return configuration.isEnabled + } + + return combineLatest(chatListFilterPreferences(engine: context.engine), isEnabled) + |> take(1) + |> deliverOnMainQueue + |> map { [weak self] filters, isEnabled -> [SPopoverItem] in + var items:[SPopoverItem] = [] + if isEnabled { + items.append(SPopoverItem(filters.list.isEmpty ? L10n.chatListFilterSetupEmpty : L10n.chatListFilterSetup, { + context.sharedContext.bindings.rootNavigation().push(ChatListFiltersListController(context: context)) + }, filters.list.isEmpty ? theme.icons.chat_filter_add : theme.icons.chat_filter_edit)) + + if self?.filterValue?.filter != nil { + items.append(SPopoverItem(L10n.chatListFilterAll, { + self?.updateFilter { + $0.withUpdatedFilter(nil) + } + })) + } + + if !filters.list.isEmpty { + items.append(SPopoverItem(false)) + } + for filter in filters.list { + let badge = GlobalBadgeNode(context.account, sharedContext: context.sharedContext, view: View(), layoutChanged: { + + }, getColor: { isSelected in + return isSelected ? .white : theme.colors.accent + }, filter: filter) + let additionView: SPopoverAdditionItemView = SPopoverAdditionItemView(context: badge, view: badge.view!, updateIsSelected: { [weak badge] isSelected in + badge?.isSelected = isSelected + }) + + items.append(SPopoverItem(filter.title, { [weak self] in + guard let `self` = self, filter.id != self.filterValue?.filter?.id else { + return + } + self.updateFilter { + $0.withUpdatedFilter(filter) + } + self.scrollup(force: true) + }, filter.icon, additionView: additionView)) + } + } + return items + } + + } + + + func globalSearch(_ query: String) { + let invoke = { [weak self] in + self?.genericView.searchView.change(state: .Focus, false) + self?.genericView.searchView.setString(query) + } + + switch context.sharedContext.layout { + case .single: + context.sharedContext.bindings.rootNavigation().back() + Queue.mainQueue().justDispatch(invoke) + case .minimisize: + context.sharedContext.bindings.needFullsize() + Queue.mainQueue().justDispatch(invoke) + default: + invoke() + } + } + + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + + let isLocked = (NSApp.delegate as? AppDelegate)?.passlock ?? .single(false) + + + + self.suggestAutoarchiveDisposable.set(combineLatest(queue: .mainQueue(), isLocked, context.isKeyWindow, getServerProvidedSuggestions(account: self.context.account)).start(next: { [weak self] locked, isKeyWindow, values in + guard let strongSelf = self, let navigation = strongSelf.navigationController else { + return + } + if strongSelf.didSuggestAutoarchive { + return + } + if !values.contains(.autoarchivePopular) { + return + } + if !isKeyWindow { + return + } + if navigation.stackCount > 1 { + return + } + if locked { + return + } + strongSelf.didSuggestAutoarchive = true + + let context = strongSelf.context + + _ = dismissServerProvidedSuggestion(account: strongSelf.context.account, suggestion: .autoarchivePopular).start() + + confirm(for: context.window, header: L10n.alertHideNewChatsHeader, information: L10n.alertHideNewChatsText, okTitle: L10n.alertHideNewChatsOK, cancelTitle: L10n.alertHideNewChatsCancel, successHandler: { _ in + execute(inapp: .settings(link: "tg://settings/privacy", context: context, section: .privacy)) + }) + + })) + - override func selectionWillChange(row:Int, item:TableRowItem) -> Bool { - if let item = item as? ChatListRowItem, let peer = item.peer, let modalAction = navigationController?.modalAction { + context.window.set(mouseHandler: { [weak self] event -> KeyHandlerResult in + guard let `self` = self else {return .rejected} + if event.modifierFlags.contains(.control) { + if self.genericView.tableView._mouseInside() { + let row = self.genericView.tableView.row(at: self.genericView.tableView.clipView.convert(event.locationInWindow, from: nil)) + if row >= 0 { + let view = self.genericView.hitTest(self.genericView.convert(event.locationInWindow, from: nil)) + if view?.className.contains("Segment") == false { + self.genericView.tableView.item(at: row).view?.mouseDown(with: event) + return .invoked + } else { + return .rejected + } + } + } + } + return .rejected + }, with: self, for: .leftMouseDown, priority: .high) + + + context.window.add(swipe: { [weak self] direction, _ -> SwipeHandlerResult in + guard let `self` = self, let window = self.window else {return .failed} + let swipeState: SwipeState? + + var checkFolder: Bool = true + let row = self.genericView.tableView.row(at: self.genericView.tableView.clipView.convert(window.mouseLocationOutsideOfEventStream, from: nil)) + if row != -1 { + + let hitTestView = self.genericView.hitTest(self.genericView.convert(window.mouseLocationOutsideOfEventStream, from: nil)) + if let view = hitTestView, view.isInSuperclassView(ChatListRevealView.self) { + return .failed + } + let item = self.genericView.tableView.item(at: row) as? ChatListRowItem + if let item = item { + let view = item.view as? ChatListRowView + if view?.endRevealState != nil { + checkFolder = false + } + + if !item.hasRevealState { + return .failed + } + } else { + return .failed + } + + } + + + switch direction { + case let .left(_state): + if !self.mode.isPlain && checkFolder { + swipeState = nil + } else { + swipeState = _state + } + + case let .right(_state): + swipeState = _state + case .none: + swipeState = nil + } + + + guard let state = swipeState, self.context.sharedContext.layout != .minimisize else {return .failed} + + switch state { + case .start: + let row = self.genericView.tableView.row(at: self.genericView.tableView.clipView.convert(window.mouseLocationOutsideOfEventStream, from: nil)) + if row != -1 { + let item = self.genericView.tableView.item(at: row) as! ChatListRowItem + guard !item.isAd else {return .failed} + self.removeRevealStateIfNeeded(item.peerId) + (item.view as? RevealTableView)?.initRevealState() + return .success(RevealTableItemController(item: item)) + } else { + return .failed + } + + case let .swiping(_delta, controller): + let controller = controller as! RevealTableItemController + + guard let view = controller.item.view as? RevealTableView else {return .nothing} + + var delta:CGFloat + switch direction { + case .left: + delta = _delta//max(0, _delta) + case .right: + delta = -_delta//min(-_delta, 0) + default: + delta = _delta + } + + + delta -= view.additionalRevealDelta + + let newDelta = min(view.width * log2(abs(delta) + 1) * log2(delta < 0 ? view.width * 8 : view.width) / 100.0, abs(delta)) + + if delta < 0 { + delta = -newDelta + } else { + delta = newDelta + } + + + + view.moveReveal(delta: delta) + case let .success(_, controller), let .failed(_, controller): + let controller = controller as! RevealTableItemController + guard let view = (controller.item.view as? RevealTableView) else {return .nothing} + + var direction = direction + + switch direction { + case let .left(state): + + if view.containerX < 0 && abs(view.containerX) > view.rightRevealWidth / 2 { + direction = .right(state.withAlwaysSuccess()) + } else if abs(view.containerX) < view.rightRevealWidth / 2 && view.containerX < view.leftRevealWidth / 2 { + direction = .left(state.withAlwaysFailed()) + } else { + direction = .left(state.withAlwaysSuccess()) + } + case .right: + if view.containerX > 0 && view.containerX > view.leftRevealWidth / 2 { + direction = .left(state.withAlwaysSuccess()) + } else if abs(view.containerX) < view.rightRevealWidth / 2 && view.containerX < view.leftRevealWidth / 2 { + direction = .right(state.withAlwaysFailed()) + } else { + direction = .right(state.withAlwaysSuccess()) + } + default: + break + } + + view.completeReveal(direction: direction) + } + + // return .success() + + return .nothing + }, with: self.genericView.tableView, identifier: "chat-list", priority: .high) + + + + if context.sharedContext.bindings.rootNavigation().stackCount == 1 { + setHighlightEvents() + } + } + + private func setHighlightEvents() { + + removeHighlightEvents() + + context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + if let item = self?.genericView.tableView.highlightedItem(), item.index > 0 { + self?.genericView.tableView.highlightPrev(turnDirection: false) + while self?.genericView.tableView.highlightedItem() is PopularPeersRowItem || self?.genericView.tableView.highlightedItem() is SeparatorRowItem { + self?.genericView.tableView.highlightNext(turnDirection: false) + } + } + return .invoked + }, with: self, for: .UpArrow, priority: .low) + + + context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.genericView.tableView.highlightNext(turnDirection: false) + while self?.genericView.tableView.highlightedItem() is PopularPeersRowItem || self?.genericView.tableView.highlightedItem() is SeparatorRowItem { + self?.genericView.tableView.highlightNext(turnDirection: false) + } + return .invoked + }, with: self, for: .DownArrow, priority: .low) + + } + + private func removeHighlightEvents() { + genericView.tableView.cancelHighlight() + context.window.remove(object: self, for: .DownArrow, forceCheckFlags: true) + context.window.remove(object: self, for: .UpArrow, forceCheckFlags: true) + } + + private func removeRevealStateIfNeeded(_ ignoreId: PeerId?) { + genericView.tableView.enumerateItems { item -> Bool in + if let item = item as? ChatListRowItem, item.peerId != ignoreId { + (item.view as? ChatListRowView)?.endRevealState = nil + } + return true + } + } + + private func _openChat(_ index: Int) { + if !genericView.tableView.isEmpty { + let archiveItem = genericView.tableView.item(at: 0) as? ChatListRowItem + var index: Int = index + if let item = archiveItem, item.isAutohidden || item.archiveStatus == .collapsed { + index += 1 + } + if archiveItem == nil { + index += 1 + if genericView.tableView.count > 1 { + let archiveItem = genericView.tableView.item(at: 1) as? ChatListRowItem + if let item = archiveItem, item.isAutohidden || item.archiveStatus == .collapsed { + index += 1 + } + } + } + + if genericView.tableView.count > index { + _ = genericView.tableView.select(item: genericView.tableView.item(at: index), notify: true, byClick: true) + } + } + } + + func openChat(_ index: Int, force: Bool = false) { + if case .folder = self.mode { + _openChat(index) + } else if force { + _openChat(index) + } else { + let prefs = chatListFilterPreferences(engine: context.engine) |> deliverOnMainQueue |> take(1) + + _ = prefs.start(next: { [weak self] filters in + if filters.list.isEmpty { + self?._openChat(index) + } else if index == 0 { + self?.updateFilter { + $0.withUpdatedFilter(nil) + } + self?.scrollup(force: true) + } else if filters.list.count >= index { + self?.updateFilter { + $0.withUpdatedFilter(filters.list[index - 1]) + } + self?.scrollup(force: true) + } else { + self?._openChat(index) + } + }) + } + } + + override var removeAfterDisapper: Bool { + switch self.mode { + case .plain: + return false + default: + return true + } + } + + + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + context.window.removeAllHandlers(for: self) + context.window.removeAllHandlers(for: genericView.tableView) + + removeRevealStateIfNeeded(nil) + + suggestAutoarchiveDisposable.set(nil) + } + +// override func getLeftBarViewOnce() -> BarView { +// return MajorBackNavigationBar(self, context: context, excludePeerId: context.peerId) +// } + + + deinit { + removePeerIdGroupDisposable.dispose() + disposable.dispose() + scrollDisposable.dispose() + reorderDisposable.dispose() + globalPeerDisposable.dispose() + archivationTooltipDisposable.dispose() + activityStatusesDisposable?.dispose() + filterDisposable.dispose() + suggestAutoarchiveDisposable.dispose() + } + + + override var enableBack: Bool { + switch mode { + case .folder, .filter: + return true + default: + return false + } + } + + override var defaultBarTitle: String { + switch mode { + case .plain: + return super.defaultBarTitle + case .folder: + return L10n.chatListArchivedChats + case .filter: + return _filterValue.with { $0.filter?.title ?? "Filter" } + } + } + + override func escapeKeyAction() -> KeyHandlerResult { + if !mode.isPlain, let navigation = navigationController { + navigation.back() + return .invoked + } + if self.filterValue?.filter != nil { + updateFilter { + $0.withUpdatedFilter(nil) + } + return .invoked + } + return super.escapeKeyAction() + } + + + init(_ context: AccountContext, modal:Bool = false, groupId: PeerGroupId? = nil, filterId: Int32? = nil) { + self.undoTooltipControl = UndoTooltipControl(context: context) + + let mode: PeerListMode + if let filterId = filterId { + mode = .filter(filterId) + } else if let groupId = groupId { + mode = .folder(groupId) + } else { + mode = .plain + } + + super.init(context, followGlobal: !modal, mode: mode) + + if groupId != nil { + context.closeFolderFirst = true + } + } + + override func selectionWillChange(row:Int, item:TableRowItem, byClick: Bool) -> Bool { + if let item = item as? ChatListRowItem, let peer = item.peer, let modalAction = context.sharedContext.bindings.rootNavigation().modalAction { if !modalAction.isInvokable(for: peer) { - modalAction.alertError(for: peer, with:window!) + modalAction.alertError(for: peer, with:mainWindow) return false } modalAction.afterInvoke() + + if let modalAction = modalAction as? FWDNavigationAction { + if item.peerId == context.peerId { + _ = Sender.forwardMessages(messageIds: modalAction.messages.map{$0.id}, context: context, peerId: context.peerId).start() + _ = showModalSuccess(for: mainWindow, icon: theme.icons.successModalProgress, delay: 1.0).start() + navigationController?.removeModalAction() + return false + } + } + + } + if let item = item as? ChatListRowItem { + if item.groupId != .root { + if byClick { + item.view?.focusAnimation(nil) + open(with: item.entryId, initialAction: nil, addition: false) + } + return false + } + } + if item is ChatListRevealItem { + return false } return true } override func selectionDidChange(row:Int, item:TableRowItem, byClick:Bool, isNew:Bool) -> Void { - if let item = item as? ChatListRowItem, let navigation = navigationController { - if !isNew && navigation.controller is ChatController { - if let modalAction = navigation.modalAction { - navigation.controller.invokeNavigation(action: modalAction) + let navigation = context.sharedContext.bindings.rootNavigation() + if let item = item as? ChatListRowItem { + if !isNew, let controller = navigation.controller as? ChatController { + switch controller.mode { + case .history, .replyThread: + if let modalAction = navigation.modalAction { + navigation.controller.invokeNavigation(action: modalAction) + } + controller.clearReplyStack() + controller.scrollup(force: true) + case .scheduled, .pinned, .preview: + navigation.back() } - navigation.controller.scrollup() + } else { - open(with: item.peerId) + + let context = self.context + + _ = (context.globalPeerHandler.get() |> take(1)).start(next: { location in + context.globalPeerHandler.set(.single(location)) + }) + + let initialAction: ChatInitialAction? + + switch item.pinnedType { + case let .ad(info): + if let info = info as? PromoChatListItem { + initialAction = .ad(info.kind) + } else { + initialAction = nil + } + default: + initialAction = nil + } + + open(with: item.entryId, initialAction: initialAction, addition: false) } } } diff --git a/Telegram-Mac/ChatListEmptyRowItem.swift b/Telegram-Mac/ChatListEmptyRowItem.swift new file mode 100644 index 0000000000..613898f89f --- /dev/null +++ b/Telegram-Mac/ChatListEmptyRowItem.swift @@ -0,0 +1,274 @@ +// +// ChatListEmptyRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 12/03/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import TGUIKit +import Cocoa +import SwiftSignalKit +import Postbox +import TelegramCore + + +class ChatListEmptyRowItem: TableRowItem { + private let _stableId: AnyHashable + + override var stableId: AnyHashable { + return _stableId + } + let context: AccountContext + let filter: ChatListFilter? + let openFilterSettings: (ChatListFilter?)->Void + init(_ initialSize: NSSize, stableId: AnyHashable, filter: ChatListFilter?, context: AccountContext, openFilterSettings: @escaping(ChatListFilter?)->Void) { + self.context = context + self.filter = filter + self._stableId = stableId + self.openFilterSettings = openFilterSettings + super.init(initialSize) + } + + override var height: CGFloat { + if let table = table { + var tableHeight: CGFloat = 0 + table.enumerateItems { item -> Bool in + if item.index < self.index { + tableHeight += item.height + } + return true + } + let height = table.frame.height == 0 ? initialSize.height : table.frame.height + return height - tableHeight + } + return initialSize.height + } + + override func viewClass() -> AnyClass { + return ChatListEmptyRowView.self + } +} + + +private class ChatListEmptyRowView : TableRowView { + private let disposable = MetaDisposable() + private let textView = TextView() + private let separator = View() + private let sticker: MediaAnimatedStickerView = MediaAnimatedStickerView(frame: NSZeroRect) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(textView) + textView.isSelectable = false + + addSubview(separator) + addSubview(sticker) + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + + guard let item = item as? ChatListEmptyRowItem else { + return + } + + let animatedSticker: LocalAnimatedSticker + + if let _ = item.filter { + animatedSticker = .folder_empty + } else { + animatedSticker = .chiken_born + } + sticker.update(with: animatedSticker.file, size: NSMakeSize(112, 112), context: item.context, parent: nil, table: item.table, parameters: animatedSticker.parameters, animated: animated, positionFlags: nil, approximateSynchronousValue: false) + + needsLayout = true + } + + deinit { + disposable.dispose() + } + + + override func layout() { + super.layout() + + separator.background = theme.colors.border + + guard let item = item as? ChatListEmptyRowItem else { + return + } + + + let text: String + if let _ = item.filter { + text = L10n.chatListFilterEmpty + } else { + text = L10n.chatListEmptyText + } + + + let attr = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: .normal(.title), textColor: theme.colors.text), bold: MarkdownAttributeSet(font: .medium(.title), textColor: theme.colors.text), link: MarkdownAttributeSet(font: .normal(.title), textColor: theme.colors.link), linkAttribute: { [weak item] contents in + return (NSAttributedString.Key.link.rawValue, inAppLink.callback(contents, { [weak item] value in + if value == "filter" { + item?.openFilterSettings(item?.filter) + } + + })) + })).mutableCopy() as! NSMutableAttributedString + + + attr.detectBoldColorInString(with: .medium(.title)) + + let layout = TextViewLayout(attr, alignment: .center) + + layout.measure(width: frame.width - 40) + layout.interactions = globalLinkExecutor + textView.update(layout) + textView.center() + + textView.isHidden = frame.width <= 70 + sticker.isHidden = frame.width <= 70 + + separator.frame = NSMakeRect(frame.width - .borderSize, 0, .borderSize, frame.height) + + sticker.centerX(y: textView.frame.minY - sticker.frame.height - 20) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + + + + + + + + + + +class ChatListLoadingRowItem: TableRowItem { + private let _stableId: AnyHashable + + override var stableId: AnyHashable { + return _stableId + } + let context: AccountContext + let filter: ChatListFilter? + init(_ initialSize: NSSize, stableId: AnyHashable, filter: ChatListFilter?, context: AccountContext) { + self.context = context + self.filter = filter + self._stableId = stableId + super.init(initialSize) + } + + override var height: CGFloat { + if let table = table { + var tableHeight: CGFloat = 0 + table.enumerateItems { item -> Bool in + if item.index < self.index { + tableHeight += item.height + } + return true + } + let height = table.frame.height == 0 ? initialSize.height : table.frame.height + return height - tableHeight + } + return initialSize.height + } + + override func viewClass() -> AnyClass { + return ChatListLoadingRowView.self + } +} + + +private class ChatListLoadingRowView : TableRowView { + private let disposable = MetaDisposable() + private let textView = TextView() + private let separator = View() + private let sticker: MediaAnimatedStickerView = MediaAnimatedStickerView(frame: NSZeroRect) + private let indicator: ProgressIndicator = ProgressIndicator(frame: NSMakeRect(0, 0, 30, 30)) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(textView) + textView.isSelectable = false + + addSubview(separator) + addSubview(sticker) + addSubview(indicator) + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + + guard let item = item as? ChatListLoadingRowItem else { + return + } + + + if let _ = item.filter { + let animatedSticker: LocalAnimatedSticker = LocalAnimatedSticker.new_folder + sticker.update(with: animatedSticker.file, size: NSMakeSize(112, 112), context: item.context, parent: nil, table: item.table, parameters: animatedSticker.parameters, animated: animated, positionFlags: nil, approximateSynchronousValue: false) + sticker.isHidden = false + indicator.isHidden = true + } else { + sticker.isHidden = true + indicator.isHidden = false + } + + needsLayout = true + } + + deinit { + disposable.dispose() + } + + + override func layout() { + super.layout() + + separator.background = theme.colors.border + + guard let item = item as? ChatListLoadingRowItem else { + return + } + + let text: String + if let _ = item.filter { + text = L10n.chatListFilterLoading + } else { + text = "Loading" + } + + let attr = NSAttributedString.initialize(string: text, color: theme.colors.text, font: .normal(.text)).mutableCopy() as! NSMutableAttributedString + + attr.detectBoldColorInString(with: .medium(.text)) + + let layout = TextViewLayout(attr, alignment: .center) + + layout.measure(width: frame.width - 40) + layout.interactions = globalLinkExecutor + textView.update(layout) + textView.center() + + textView.isHidden = frame.width <= 70 || item.filter == nil + sticker.isHidden = frame.width <= 70 || item.filter == nil + + indicator.isHidden = item.filter != nil + + separator.frame = NSMakeRect(frame.width - .borderSize, 0, .borderSize, frame.height) + + sticker.centerX(y: textView.frame.minY - sticker.frame.height - 20) + indicator.center() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/Telegram-Mac/ChatListFilterController.swift b/Telegram-Mac/ChatListFilterController.swift new file mode 100644 index 0000000000..25e75ada7a --- /dev/null +++ b/Telegram-Mac/ChatListFilterController.swift @@ -0,0 +1,823 @@ +// +// ChatListPresetController.swift +// Telegram +// +// Created by Mikhail Filimonov on 29/01/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import SwiftSignalKit +import Postbox +import TelegramCore + +import TGUIKit + + + +enum ChatListFilterType { + case generic + case unmuted + case unread + case channels + case groups + case bots + case contacts + case nonContacts +} + +func chatListFilterType(_ filter: ChatListFilter) -> ChatListFilterType { + let filterType: ChatListFilterType + if filter.data.includePeers.peers.isEmpty { + if filter.data.categories == .all { + if filter.data.excludeRead { + filterType = .unread + } else if filter.data.excludeMuted { + filterType = .unmuted + } else { + filterType = .generic + } + } else { + if filter.data.categories == .channels { + filterType = .channels + } else if filter.data.categories == .groups { + filterType = .groups + } else if filter.data.categories == .bots { + filterType = .bots + } else if filter.data.categories == .contacts { + filterType = .contacts + } else if filter.data.categories == .nonContacts { + filterType = .nonContacts + } else { + filterType = .generic + } + } + } else { + filterType = .generic + } + return filterType +} + + + +private let maximumPeers: Int = 100 + +private extension ChatListFilter { + var additionIncludeItems: [ShareAdditionItem] { + var items:[ShareAdditionItem] = [] + + items.append(.init(peer: TelegramFilterCategory(category: .contacts), status: "")) + items.append(.init(peer: TelegramFilterCategory(category: .nonContacts), status: "")) + items.append(.init(peer: TelegramFilterCategory(category: .groups), status: "")) + items.append(.init(peer: TelegramFilterCategory(category: .channels), status: "")) + items.append(.init(peer: TelegramFilterCategory(category: .bots), status: "")) + return items + } + var selectedIncludeItems: [ShareAdditionItem] { + var items:[ShareAdditionItem] = [] + + if self.data.categories.contains(.contacts) { + items.append(.init(peer: TelegramFilterCategory(category: .contacts), status: "")) + } + if self.data.categories.contains(.nonContacts) { + items.append(.init(peer: TelegramFilterCategory(category: .nonContacts), status: "")) + } + if self.data.categories.contains(.groups) { + items.append(.init(peer: TelegramFilterCategory(category: .groups), status: "")) + } + if self.data.categories.contains(.channels) { + items.append(.init(peer: TelegramFilterCategory(category: .channels), status: "")) + } + if self.data.categories.contains(.bots) { + items.append(.init(peer: TelegramFilterCategory(category: .bots), status: "")) + } + return items + } + var additionExcludeItems: [ShareAdditionItem] { + var items:[ShareAdditionItem] = [] + items.append(.init(peer: TelegramFilterCategory(category: .excludeMuted), status: "")) + items.append(.init(peer: TelegramFilterCategory(category: .excludeRead), status: "")) + items.append(.init(peer: TelegramFilterCategory(category: .excludeArchived), status: "")) + return items + } + var selectedExcludeItems: [ShareAdditionItem] { + var items:[ShareAdditionItem] = [] + + if self.data.excludeMuted { + items.append(.init(peer: TelegramFilterCategory(category: .excludeMuted), status: "")) + } + if self.data.excludeRead { + items.append(.init(peer: TelegramFilterCategory(category: .excludeRead), status: "")) + } + if self.data.excludeArchived { + items.append(.init(peer: TelegramFilterCategory(category: .excludeArchived), status: "")) + } + return items + } +} + +//extension ChatListFiltersState { +// mutating func withAddedFilter(_ filter: ChatListFilter, onlyReplace: Bool = false) { +// if let index = filters.firstIndex(where: {$0.id == filter.id}) { +// filters[index] = filter +// } else if !onlyReplace { +// filters.append(filter) +// } +// } +// +// mutating func withRemovedFilter(_ filter: ChatListFilter) { +// filters.removeAll(where: {$0.id == filter.id }) +// } +// +// mutating func withMoveFilter(_ from: Int, _ to: Int) { +// filters.insert(filters.remove(at: from), at: to) +// } +//} + +class SelectCallbackObject : ShareObject { + private let callback:([PeerId])->Signal + private let limitReachedText: String + init(_ context: AccountContext, defaultSelectedIds: Set, additionTopItems: ShareAdditionItems?, limit: Int?, limitReachedText: String, callback:@escaping([PeerId])->Signal) { + self.callback = callback + self.limitReachedText = limitReachedText + super.init(context, defaultSelectedIds: defaultSelectedIds, additionTopItems: additionTopItems, limit: limit) + } + + override var hasCaptionView: Bool { + return false + } + + override func perform(to peerIds:[PeerId], comment: ChatTextInputState? = nil) -> Signal { + return callback(peerIds) |> mapError { _ in return String() } + } + override func limitReached() { + alert(for: context.window, info: limitReachedText) + } + override var searchPlaceholderKey: String { + return "ChatList.Add.Placeholder" + } + override var interactionOk: String { + return L10n.chatListFilterAddDone + } + override var alwaysEnableDone: Bool { + return true + } + override func possibilityPerformTo(_ peer: Peer) -> Bool { + if peer is TelegramSecretChat { + return false + } + return true + } + +} + +private struct ChatListFiltersListState: Equatable { + var filter: ChatListFilter + var showAllInclude: Bool + var showAllExclude: Bool + let isNew: Bool + var changedName: Bool + init(filter: ChatListFilter, isNew: Bool, showAllInclude: Bool, showAllExclude: Bool, changedName: Bool) { + self.filter = filter + self.isNew = isNew + self.showAllInclude = showAllInclude + self.showAllExclude = showAllExclude + self.changedName = changedName + } + + + + mutating func withUpdatedFilter(_ f:(ChatListFilter)->ChatListFilter) { + self.filter = f(self.filter) + } +} + +private final class ChatListPresetArguments { + let context: AccountContext + let toggleOption:(ChatListFilterPeerCategories)->Void + let toggleExcludeMuted:(Bool)->Void + let toggleExcludeRead:(Bool)->Void + let addInclude:()->Void + let addExclude:()->Void + let removeIncluded:(PeerId)->Void + let removeExcluded:(PeerId)->Void + let openInfo:(PeerId)->Void + let showAllInclude: ()->Void + let showAllExclude: ()->Void + let updateIcon:(FolderIcon)->Void + init(context: AccountContext, toggleOption:@escaping(ChatListFilterPeerCategories)->Void, addInclude: @escaping()->Void, addExclude: @escaping()->Void, removeIncluded: @escaping(PeerId)->Void, removeExcluded: @escaping(PeerId)->Void, openInfo: @escaping(PeerId)->Void, toggleExcludeMuted:@escaping(Bool)->Void, toggleExcludeRead: @escaping(Bool)->Void, showAllInclude:@escaping()->Void, showAllExclude:@escaping()->Void, updateIcon: @escaping(FolderIcon)->Void) { + self.context = context + self.toggleOption = toggleOption + self.toggleExcludeMuted = toggleExcludeMuted + self.toggleExcludeRead = toggleExcludeRead + self.addInclude = addInclude + self.addExclude = addExclude + self.removeIncluded = removeIncluded + self.removeExcluded = removeExcluded + self.openInfo = openInfo + self.showAllInclude = showAllInclude + self.showAllExclude = showAllExclude + self.updateIcon = updateIcon + } +} + +private let _id_name_input = InputDataIdentifier("_id_name_input") +private let _id_private_chats = InputDataIdentifier("_id_private_chats") + +private let _id_public_groups = InputDataIdentifier("_id_public_groups") +private let _id_private_groups = InputDataIdentifier("_id_private_groups") +private let _id_secret_chats = InputDataIdentifier("_id_secret_chats") + + +private let _id_channels = InputDataIdentifier("_id_channels") +private let _id_bots = InputDataIdentifier("_id_bots") +private let _id_exclude_muted = InputDataIdentifier("_id_exclude_muted") +private let _id_exclude_read = InputDataIdentifier("_id_exclude_read") + +private let _id_add_include = InputDataIdentifier("_id_add_include") +private let _id_add_exclude = InputDataIdentifier("_id_add_exclude") + +private let _id_show_all_include = InputDataIdentifier("_id_show_all_include") +private let _id_show_all_exclude = InputDataIdentifier("_id_show_all_exclude") +private let _id_header = InputDataIdentifier("_id_header") +private func _id_include(_ peerId: PeerId) -> InputDataIdentifier { + return InputDataIdentifier("_id_include_\(peerId)") +} +private func _id_exclude(_ peerId: PeerId) -> InputDataIdentifier { + return InputDataIdentifier("_id_exclude_\(peerId)") +} +private func chatListFilterEntries(state: ChatListFiltersListState, includePeers: [Peer], excludePeers: [Peer], arguments: ChatListPresetArguments) -> [InputDataEntry] { + var entries: [InputDataEntry] = [] + + var includePeers:[Peer] = includePeers + + if state.filter.data.categories.contains(.groups) { + includePeers.insert(TelegramFilterCategory(category: .groups), at: 0) + } + if state.filter.data.categories.contains(.channels) { + includePeers.insert(TelegramFilterCategory(category: .channels), at: 0) + } + if state.filter.data.categories.contains(.contacts) { + includePeers.insert(TelegramFilterCategory(category: .contacts), at: 0) + } + if state.filter.data.categories.contains(.nonContacts) { + includePeers.insert(TelegramFilterCategory(category: .nonContacts), at: 0) + } + if state.filter.data.categories.contains(.bots) { + includePeers.insert(TelegramFilterCategory(category: .bots), at: 0) + } + + + var excludePeers:[Peer] = excludePeers + + if state.filter.data.excludeMuted { + excludePeers.insert(TelegramFilterCategory(category: .excludeMuted), at: 0) + } + if state.filter.data.excludeRead { + excludePeers.insert(TelegramFilterCategory(category: .excludeRead), at: 0) + } + if state.filter.data.excludeArchived { + excludePeers.insert(TelegramFilterCategory(category: .excludeArchived), at: 0) + } + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + if state.isNew { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_header, equatable: nil, comparable: nil, item: { initialSize, stableId in + let attributedString = NSMutableAttributedString() + return ChatListFiltersHeaderItem(initialSize, context: arguments.context, stableId: stableId, sticker: LocalAnimatedSticker.new_folder, text: attributedString) + })) + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + } + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.chatListFilterNameHeader), data: .init(color: theme.colors.listGrayText, detectBold: true, viewType: .textTopItem))) + index += 1 + + + entries.append(.input(sectionId: sectionId, index: index, value: .string(state.filter.title), error: nil, identifier: _id_name_input, mode: .plain, data: .init(viewType: .singleItem, rightItem: InputDataRightItem.action(FolderIcon(state.filter).icon(for: .settings), .custom{ item, control in + showPopover(for: control, with: ChatListFilterFolderIconController(arguments.context, select: arguments.updateIcon), edge: .minX, inset: NSMakePoint(0,-45)) + })), placeholder: nil, inputPlaceholder: L10n.chatListFilterNamePlaceholder, filter: { $0 }, limit: 12)) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.chatListFilterIncludeHeader), data: .init(color: theme.colors.listGrayText, detectBold: true, viewType: .textTopItem))) + index += 1 + + let hasAddInclude = state.filter.data.includePeers.peers.count < maximumPeers || state.filter.data.categories != .all + + if hasAddInclude { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_add_include, equatable: InputDataEquatable(state), comparable: nil, item: { initialSize, stableId in + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.chatListFilterIncludeAddChat, nameStyle: blueActionButton, type: .none, viewType: includePeers.isEmpty ? .singleItem : .firstItem, action: arguments.addInclude, thumb: GeneralThumbAdditional(thumb: theme.icons.chat_filter_add, textInset: 46, thumbInset: 4)) + })) + index += 1 + } + + + + var fake:[Int] = [] + fake.append(0) + for (i, _) in includePeers.enumerated() { + if hasAddInclude { + fake.append(i + 1) + } else { + fake.append(i) + } + } + + for (i, peer) in includePeers.enumerated() { + + struct E : Equatable { + let viewType: GeneralViewType + let peer: PeerEquatable + } + + if i > 10, !state.showAllInclude { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_show_all_include, equatable: InputDataEquatable(includePeers.count), comparable: nil, item: { initialSize, stableId in + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.chatListFilterShowMoreCountable(includePeers.count - i), nameStyle: blueActionButton, type: .none, viewType: .lastItem, action: arguments.showAllInclude, thumb: GeneralThumbAdditional(thumb: theme.icons.chatSearchUp, textInset: 52, thumbInset: 4)) + })) + index += 1 + break + } else { + var viewType = bestGeneralViewType(fake, for: hasAddInclude ? i + 1 : i) + + if excludePeers.count > 10, i == includePeers.count - 1, state.showAllInclude { + viewType = .innerItem + } + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_include(peer.id), equatable: InputDataEquatable(E(viewType: viewType, peer: PeerEquatable(peer))), comparable: nil, item: { initialSize, stableId in + return ShortPeerRowItem(initialSize, peer: peer, account: arguments.context.account, stableId: stableId, height: 44, photoSize: NSMakeSize(30, 30), inset: NSEdgeInsets(left: 30, right: 30), viewType: viewType, action: { + arguments.openInfo(peer.id) + }, contextMenuItems: { + return .single([ContextMenuItem(L10n.chatListFilterIncludeRemoveChat, handler: { + arguments.removeIncluded(peer.id) + })]) + }) + })) + index += 1 + } + } + + if includePeers.count > 10, state.showAllInclude { + struct T: Equatable { + let a: Bool + let b: Int + } + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_show_all_include, equatable: InputDataEquatable(T(a: state.showAllInclude, b: includePeers.count)), comparable: nil, item: { initialSize, stableId in + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.chatListFilterHideCountable(includePeers.count - 11), nameStyle: blueActionButton, type: .none, viewType: .lastItem, action: arguments.showAllInclude, thumb: GeneralThumbAdditional(thumb: theme.icons.chatSearchDown, textInset: 52, thumbInset: 4)) + })) + index += 1 + } + + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.chatListFilterIncludeDesc), data: .init(color: theme.colors.listGrayText, detectBold: true, viewType: .textBottomItem))) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.chatListFilterExcludeHeader), data: .init(color: theme.colors.listGrayText, detectBold: true, viewType: .textTopItem))) + index += 1 + + let hasAddExclude = state.filter.data.excludePeers.count < maximumPeers || !state.filter.data.excludeRead || !state.filter.data.excludeMuted || !state.filter.data.excludeArchived + + + if hasAddExclude { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_add_exclude, equatable: InputDataEquatable(state), comparable: nil, item: { initialSize, stableId in + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.chatListFilterExcludeAddChat, nameStyle: blueActionButton, type: .none, viewType: excludePeers.isEmpty ? .singleItem : .firstItem, action: arguments.addExclude, thumb: GeneralThumbAdditional(thumb: theme.icons.chat_filter_add, textInset: 46, thumbInset: 2)) + })) + index += 1 + } + + + + fake = [] + fake.append(0) + for (i, _) in excludePeers.enumerated() { + if hasAddExclude { + fake.append(i + 1) + } else { + fake.append(i) + } + } + + for (i, peer) in excludePeers.enumerated() { + struct E : Equatable { + let viewType: GeneralViewType + let peer: PeerEquatable + } + if i > 10, !state.showAllExclude { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_show_all_exclude, equatable: InputDataEquatable(excludePeers.count), comparable: nil, item: { initialSize, stableId in + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.chatListFilterShowMoreCountable(excludePeers.count - i), nameStyle: blueActionButton, type: .none, viewType: .lastItem, action: arguments.showAllExclude, thumb: GeneralThumbAdditional(thumb: theme.icons.chatSearchUp, textInset: 52, thumbInset: 4)) + })) + index += 1 + break + } else { + var viewType = bestGeneralViewType(fake, for: hasAddExclude ? i + 1 : i) + + if excludePeers.count > 10, i == excludePeers.count - 1, state.showAllExclude { + viewType = .innerItem + } + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_exclude(peer.id), equatable: InputDataEquatable(E(viewType: viewType, peer: PeerEquatable(peer))), comparable: nil, item: { initialSize, stableId in + return ShortPeerRowItem(initialSize, peer: peer, account: arguments.context.account, stableId: stableId, height: 44, photoSize: NSMakeSize(30, 30), inset: NSEdgeInsets(left: 30, right: 30), viewType: viewType, action: { + arguments.openInfo(peer.id) + }, contextMenuItems: { + return .single([ContextMenuItem.init(L10n.chatListFilterExcludeRemoveChat, handler: { + arguments.removeExcluded(peer.id) + })]) + }) + })) + index += 1 + } + + } + + if excludePeers.count > 10, state.showAllExclude { + + struct T: Equatable { + let a: Bool + let b: Int + } + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_show_all_exclude, equatable: InputDataEquatable(T(a: state.showAllExclude, b: excludePeers.count)), comparable: nil, item: { initialSize, stableId in + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.chatListFilterHideCountable(excludePeers.count - 11), nameStyle: blueActionButton, type: .none, viewType: .lastItem, action: arguments.showAllExclude, thumb: GeneralThumbAdditional(thumb: theme.icons.chatSearchDown, textInset: 52, thumbInset: 4)) + })) + index += 1 + } + + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.chatListFilterExcludeDesc), data: .init(color: theme.colors.listGrayText, detectBold: true, viewType: .textBottomItem))) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func ChatListFilterController(context: AccountContext, filter: ChatListFilter, isNew: Bool = false) -> InputDataController { + + + let initialState = ChatListFiltersListState(filter: filter, isNew: isNew, showAllInclude: false, showAllExclude: false, changedName: !isNew) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((ChatListFiltersListState) -> ChatListFiltersListState) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let updateDisposable = MetaDisposable() + + let save:(Bool)->Void = { replace in + _ = context.engine.peers.updateChatListFiltersInteractively({ filters in + let filter = stateValue.with { $0.filter } + var filters = filters + if let index = filters.firstIndex(where: {$0.id == filter.id}) { + filters[index] = filter + } else if !replace { + filters.append(filter) + } + return filters + }).start() + } + + + let arguments = ChatListPresetArguments(context: context, toggleOption: { option in + updateState { state in + var state = state + state.withUpdatedFilter { filter in + var filter = filter + if filter.data.categories.contains(option) { + filter.data.categories.remove(option) + } else { + filter.data.categories.insert(option) + } + return filter + } + return state + } + // save(true) + + }, addInclude: { + + let items = stateValue.with { $0.filter.additionIncludeItems } + + let additionTopItems = items.isEmpty ? nil : ShareAdditionItems(items: items, topSeparator: L10n.chatListAddTopSeparator, bottomSeparator: L10n.chatListAddBottomSeparator) + + showModal(with: ShareModalController(SelectCallbackObject(context, defaultSelectedIds: Set(stateValue.with { $0.filter.data.includePeers.peers + $0.filter.selectedIncludeItems.map { $0.peer.id } }), additionTopItems: additionTopItems, limit: maximumPeers, limitReachedText: L10n.chatListFilterIncludeLimitReached, callback: { peerIds in + updateState { state in + var state = state + + let categories = peerIds.filter { + $0.namespace._internalGetInt32Value() == ChatListFilterPeerCategories.Namespace + } + let peerIds = Set(peerIds).subtracting(categories) + + state.withUpdatedFilter { filter in + var filter = filter + filter.data.includePeers.setPeers(Array(peerIds.uniqueElements.prefix(maximumPeers))) + var updatedCats: ChatListFilterPeerCategories = [] + let cats = categories.map { ChatListFilterPeerCategories(rawValue: Int32($0.id._internalGetInt64Value())) } + for cat in cats { + updatedCats.insert(cat) + } + filter.data.categories = updatedCats + return filter + } + return state + } + // save(true) + return .complete() + })), for: context.window) + }, addExclude: { + + let items = stateValue.with { $0.filter.additionExcludeItems } + let additionTopItems = items.isEmpty ? nil : ShareAdditionItems(items: items, topSeparator: L10n.chatListAddTopSeparator, bottomSeparator: L10n.chatListAddBottomSeparator) + + showModal(with: ShareModalController(SelectCallbackObject(context, defaultSelectedIds: Set(stateValue.with { $0.filter.data.excludePeers + $0.filter.selectedExcludeItems.map { $0.peer.id } }), additionTopItems: additionTopItems, limit: maximumPeers, limitReachedText: L10n.chatListFilterExcludeLimitReached, callback: { peerIds in + updateState { state in + var state = state + state.withUpdatedFilter { filter in + var filter = filter + + let categories = peerIds.filter { + $0.namespace._internalGetInt32Value() == ChatListFilterPeerCategories.Namespace + } + let peerIds = Set(peerIds).subtracting(categories) + filter.data.excludePeers = Array(peerIds.uniqueElements.prefix(maximumPeers)) + for cat in categories { + if ChatListFilterPeerCategories(rawValue: Int32(cat.id._internalGetInt64Value())) == .excludeMuted { + filter.data.excludeMuted = true + } + if ChatListFilterPeerCategories(rawValue: Int32(cat.id._internalGetInt64Value())) == .excludeRead { + filter.data.excludeRead = true + } + if ChatListFilterPeerCategories(rawValue: Int32(cat.id._internalGetInt64Value())) == .excludeArchived { + filter.data.excludeArchived = true + } + } + + return filter + } + return state + } + // save(true) + return .complete() + })), for: context.window) + }, removeIncluded: { peerId in + updateState { state in + var state = state + state.withUpdatedFilter { filter in + var filter = filter + var peers = filter.data.includePeers.peers + peers.removeAll(where: { $0 == peerId }) + filter.data.includePeers.setPeers(peers) + if peerId.namespace._internalGetInt32Value() == ChatListFilterPeerCategories.Namespace { + filter.data.categories.remove(ChatListFilterPeerCategories(rawValue: Int32(peerId.id._internalGetInt64Value()))) + } + return filter + } + return state + } + //save(true) + }, removeExcluded: { peerId in + updateState { state in + var state = state + state.withUpdatedFilter { filter in + var filter = filter + var peers = filter.data.excludePeers + peers.removeAll(where: { $0 == peerId }) + filter.data.excludePeers = peers + if peerId.namespace._internalGetInt32Value() == ChatListFilterPeerCategories.Namespace { + if ChatListFilterPeerCategories(rawValue: Int32(peerId.id._internalGetInt64Value())) == .excludeMuted { + filter.data.excludeMuted = false + } + if ChatListFilterPeerCategories(rawValue: Int32(peerId.id._internalGetInt64Value())) == .excludeRead { + filter.data.excludeRead = false + } + if ChatListFilterPeerCategories(rawValue: Int32(peerId.id._internalGetInt64Value())) == .excludeArchived { + filter.data.excludeArchived = false + } + } + return filter + } + return state + } + //save(true) + }, openInfo: { peerId in + context.sharedContext.bindings.rootNavigation().push(PeerInfoController(context: context, peerId: peerId)) + }, toggleExcludeMuted: { updated in + updateState { state in + var state = state + state.withUpdatedFilter { filter in + var filter = filter + filter.data.excludeMuted = updated + return filter + } + return state + } + // save(true) + }, toggleExcludeRead: { updated in + updateState { state in + var state = state + state.withUpdatedFilter { filter in + var filter = filter + filter.data.excludeRead = updated + return filter + } + return state + } + //save(true) + }, showAllInclude: { + updateState { state in + var state = state + state.showAllInclude = !state.showAllInclude + return state + } + }, showAllExclude: { + updateState { state in + var state = state + state.showAllExclude = !state.showAllExclude + return state + } + }, updateIcon: { icon in + updateState { state in + var state = state + state.withUpdatedFilter { filter in + var filter = filter + filter.emoticon = icon.emoticon.emoji + return filter + } + return state + } + }) + + + let dataSignal = combineLatest(queue: prepareQueue, appearanceSignal, statePromise.get()) |> mapToSignal { _, state -> Signal<(ChatListFiltersListState, ([Peer], [Peer])), NoError> in + return context.account.postbox.transaction { transaction -> ([Peer], [Peer]) in + return (state.filter.data.includePeers.peers.compactMap { transaction.getPeer($0) }, state.filter.data.excludePeers.compactMap { transaction.getPeer($0) }) + } |> map { + (state, $0) + } + } |> map { + return chatListFilterEntries(state: $0, includePeers: $1.0, excludePeers: $1.1, arguments: arguments) + } |> map { + return InputDataSignalValue(entries: $0) + } + + let controller = InputDataController(dataSignal: dataSignal, title: isNew ? L10n.chatListFilterNewTitle : L10n.chatListFilterTitle, removeAfterDisappear: false) + + controller.updateDatas = { data in + + if let name = data[_id_name_input]?.stringValue { + updateState { state in + var state = state + if state.filter.title != name { + state.changedName = true + } + state.withUpdatedFilter { filter in + var filter = filter + filter.title = name + return filter + } + return state + } + } + + return .none + } + + controller.backInvocation = { data, f in + if stateValue.with({ $0.filter != filter }) { + confirm(for: context.window, header: L10n.chatListFilterDiscardHeader, information: L10n.chatListFilterDiscardText, okTitle: L10n.chatListFilterDiscardOK, cancelTitle: L10n.chatListFilterDiscardCancel, successHandler: { _ in + f(true) + }) + } else { + f(true) + } + + } + + controller.updateDoneValue = { data in + return { f in + if isNew { + f(.enabled(L10n.chatListFilterDone)) + } else { + f(.enabled(L10n.navigationDone)) + } + } + } + + controller.onDeinit = { + updateDisposable.dispose() + } + + + controller.afterTransaction = { controller in + let type = stateValue.with { chatListFilterType($0.filter) } + let nameIsUpdated = stateValue.with { $0.changedName } + if !nameIsUpdated { + switch type { + case .generic: + break + case .unmuted: + //state.name = presentationData.strings.ChatListFolder_NameNonMuted + updateState { state in + var state = state + state.filter.title = L10n.chatListFilterTilteDefaultUnmuted + // state.filter.emoticon = + return state + } + case .unread: + updateState { state in + var state = state + state.filter.title = L10n.chatListFilterTilteDefaultUnread + return state + } + case .channels: + updateState { state in + var state = state + state.filter.title = L10n.chatListFilterTilteDefaultChannels + return state + } + case .groups: + updateState { state in + var state = state + state.filter.title = L10n.chatListFilterTilteDefaultGroups + return state + } + case .bots: + updateState { state in + var state = state + state.filter.title = L10n.chatListFilterTilteDefaultBots + return state + } + case .contacts: + updateState { state in + var state = state + state.filter.title = L10n.chatListFilterTilteDefaultContacts + return state + } + case .nonContacts: + updateState { state in + var state = state + state.filter.title = L10n.chatListFilterTilteDefaultNonContacts + return state + } + } + + } + } + + controller.validateData = { data in + + return .fail(.doSomething(next: { f in + let emptyTitle = stateValue.with { $0.filter.title.isEmpty } + if emptyTitle { + f(.fail(.fields([_id_name_input : .shake]))) + return + } + + let filter = stateValue.with { $0.filter } + + if filter.isFullfilled { + alert(for: context.window, info: L10n.chatListFilterErrorLikeChats) + } else if filter.isEmpty { + alert(for: context.window, info: L10n.chatListFilterErrorEmpty) + f(.fail(.fields([_id_add_include : .shake]))) + } else { + _ = showModalProgress(signal: context.engine.peers.requestUpdateChatListFilter(id: filter.id, filter: filter), for: context.window).start(error: { error in + switch error { + case .generic: + alert(for: context.window, info: L10n.unknownError) + } + }, completed: { + save(false) + f(.success(.navigationBack)) + }) + } + + + })) + + + } + + return controller + +} + + + diff --git a/Telegram-Mac/ChatListFilterFolderIconController.swift b/Telegram-Mac/ChatListFilterFolderIconController.swift new file mode 100644 index 0000000000..708648293a --- /dev/null +++ b/Telegram-Mac/ChatListFilterFolderIconController.swift @@ -0,0 +1,71 @@ +// +// ChatListFilterFolderIconController.swift +// Telegram +// +// Created by Mikhail Filimonov on 08/04/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +final class ChatListFolderIconsView : View { + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func initialize(callback: @escaping(FolderIcon)->Void) { + removeAllSubviews() + + for icon in allSidebarFolderIcons { + let control = ImageButton(frame: NSMakeRect(0, 0, 40, 40)) + control.set(image: icon.icon(for: .settings), for: .Normal) + addSubview(control) + control.set(handler: { _ in + callback(icon) + }, for: .Click) + } + + needsLayout = true + } + + override func layout() { + super.layout() + + var x: CGFloat = 10 + var y: CGFloat = 10 + for (i, subview) in subviews.enumerated() { + subview.setFrameOrigin(NSMakePoint(x, y)) + x += subview.frame.width + if (i + 1) % 5 == 0 { + x = 10 + y += subview.frame.height + } + } + } +} + +class ChatListFilterFolderIconController: TelegramGenericViewController { + private let select:(FolderIcon)->Void + init(_ context: AccountContext, select: @escaping(FolderIcon)->Void) { + self.select = select + super.init(context) + _frameRect = NSMakeRect(0, 0, 40 * 5 + 20, ceil(CGFloat(allSidebarFolderIcons.count) / 5) * 40 + 20) + bar = .init(height: 0) + } + + override func viewDidLoad() { + super.viewDidLoad() + + genericView.initialize(callback: { [weak self] value in + self?.select(value) + self?.closePopover() + }) + readyOnce() + } +} diff --git a/Telegram-Mac/ChatListFilterPredicate.swift b/Telegram-Mac/ChatListFilterPredicate.swift new file mode 100644 index 0000000000..932e594aa3 --- /dev/null +++ b/Telegram-Mac/ChatListFilterPredicate.swift @@ -0,0 +1,98 @@ +// +// ChatListFilterPredicate.swift +// Telegram +// +// Created by Mikhail Filimonov on 02.03.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit +import Postbox +import TelegramCore + + + +func chatListFilterPredicate(for filter: ChatListFilter?) -> ChatListFilterPredicate? { + let filterPredicate: ((Peer, PeerNotificationSettings?, Bool) -> Bool) + + guard let filter = filter?.data else { + return nil + } + let includePeers = Set(filter.includePeers.peers) + let excludePeers = Set(filter.excludePeers) + var includeAdditionalPeerGroupIds: [PeerGroupId] = [] + if !filter.excludeArchived { + includeAdditionalPeerGroupIds.append(Namespaces.PeerGroup.archive) + } + var messageTagSummary: ChatListMessageTagSummaryResultCalculation? + if filter.excludeRead || filter.excludeMuted { + messageTagSummary = ChatListMessageTagSummaryResultCalculation(addCount: ChatListMessageTagSummaryResultComponent(tag: .unseenPersonalMessage, namespace: Namespaces.Message.Cloud), subtractCount: ChatListMessageTagActionsSummaryResultComponent(type: PendingMessageActionType.consumeUnseenPersonalMessage, namespace: Namespaces.Message.Cloud)) + } + + return ChatListFilterPredicate(includePeerIds: includePeers, excludePeerIds: excludePeers, pinnedPeerIds: filter.includePeers.pinnedPeers, messageTagSummary: messageTagSummary, includeAdditionalPeerGroupIds: includeAdditionalPeerGroupIds, include: { peer, isMuted, isUnread, isContact, messageTagSummaryResult in + if filter.excludeRead { + var effectiveUnread = isUnread + if let messageTagSummaryResult = messageTagSummaryResult, messageTagSummaryResult { + effectiveUnread = true + } + if !effectiveUnread { + return false + } + } + if filter.excludeMuted { + if isMuted { + if let messageTagSummaryResult = messageTagSummaryResult, messageTagSummaryResult { + } else { + return false + } + } + + } + if !filter.categories.contains(.contacts) && isContact { + if let user = peer as? TelegramUser { + if user.botInfo == nil { + return false + } + } else if let _ = peer as? TelegramSecretChat { + return false + } + } + if !filter.categories.contains(.nonContacts) && !isContact { + if let user = peer as? TelegramUser { + if user.botInfo == nil { + return false + } + } else if let _ = peer as? TelegramSecretChat { + return false + } + } + if !filter.categories.contains(.bots) { + if let user = peer as? TelegramUser { + if user.botInfo != nil { + return false + } + } + } + if !filter.categories.contains(.groups) { + if let _ = peer as? TelegramGroup { + return false + } else if let channel = peer as? TelegramChannel { + if case .group = channel.info { + return false + } + } + } + if !filter.categories.contains(.channels) { + if let channel = peer as? TelegramChannel { + if case .broadcast = channel.info { + return false + } + } + } + return true + }) + + +} diff --git a/Telegram-Mac/ChatListFilterPreferences.swift b/Telegram-Mac/ChatListFilterPreferences.swift new file mode 100644 index 0000000000..e21c67f905 --- /dev/null +++ b/Telegram-Mac/ChatListFilterPreferences.swift @@ -0,0 +1,315 @@ +// +// ChatListFilterPreferences.swift +// Telegram +// +// Created by Mikhail Filimonov on 24.01.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Postbox +import SwiftSignalKit +import TelegramCore +import Postbox + + + +extension ChatListFilter { + + + var isFullfilled: Bool { + return self.data.categories == .all && data.includePeers.peers.isEmpty && data.excludePeers.isEmpty && !data.excludeMuted && !data.excludeRead && data.excludeArchived + } + var isEmpty: Bool { + return self.data.categories.isEmpty && data.includePeers.peers.isEmpty && data.excludePeers.isEmpty && !data.excludeMuted && !data.excludeRead + } + var icon: CGImage { + + if data.categories == .all && data.excludeMuted && !data.excludeRead { + return theme.icons.chat_filter_unmuted + } else if data.categories == .all && !data.excludeMuted && data.excludeRead { + return theme.icons.chat_filter_unread + } else if data.categories == .groups { + return theme.icons.chat_filter_groups + } else if data.categories == .channels { + return theme.icons.chat_filter_channels + } else if data.categories == .contacts { + return theme.icons.chat_filter_private_chats + } else if data.categories == .nonContacts { + return theme.icons.chat_filter_non_contacts + } else if data.categories == .bots { + return theme.icons.chat_filter_bots + } + return theme.icons.chat_filter_custom + } + + static func new(excludeIds: [Int32]) -> ChatListFilter { + var id:Int32! = nil + while id == nil { + let tempId = abs(Int32(bitPattern: arc4random())) % 255 + if tempId != 0 && tempId != 1 && !excludeIds.contains(tempId) { + id = tempId + } + } + return ChatListFilter(id: id, title: "", emoticon: nil, data: ChatListFilterData(categories: [], excludeMuted: false, excludeRead: false, excludeArchived: false, includePeers: ChatListFilterIncludePeers(), excludePeers: [])) + } +} + + + + +struct ChatListFoldersSettings: PreferencesEntry, Equatable { + + let sidebar: Bool + + static var defaultValue: ChatListFoldersSettings { + return ChatListFoldersSettings(sidebar: false) + } + + init(sidebar: Bool) { + self.sidebar = sidebar + } + + func isEqual(to: PreferencesEntry) -> Bool { + if let other = to as? ChatListFoldersSettings { + return other == self + } else { + return false + } + } + + init(decoder: PostboxDecoder) { + self.sidebar = decoder.decodeOptionalInt32ForKey("t") == 1 + } + + func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(sidebar ? 1 : 0, forKey: "t") + } + + func withUpdatedSidebar(_ sidebar: Bool) -> ChatListFoldersSettings { + return ChatListFoldersSettings(sidebar: sidebar) + } +} + + + +func chatListFolderSettings(_ postbox: Postbox) -> Signal { + return postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.chatListSettings]) |> map { view in + return view.values[ApplicationSpecificPreferencesKeys.chatListSettings] as? ChatListFoldersSettings ?? ChatListFoldersSettings.defaultValue + } +} + +func updateChatListFolderSettings(_ postbox: Postbox, _ f: @escaping(ChatListFoldersSettings) -> ChatListFoldersSettings) -> Signal { + return postbox.transaction { transaction in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.chatListSettings, { entry in + let current = entry as? ChatListFoldersSettings ?? ChatListFoldersSettings.defaultValue + return f(current) + }) + } |> ignoreValues +} + + + +struct ChatListFolders : Equatable { + let list: [ChatListFilter] + let sidebar: Bool +} + +func chatListFilterPreferences(engine: TelegramEngine) -> Signal { + + return combineLatest(engine.peers.updatedChatListFilters(), chatListFolderSettings(engine.account.postbox)) |> map { + return ChatListFolders(list: $0, sidebar: $1.sidebar) + } +} + +struct ChatListFilterBadge : Equatable { + let filter: ChatListFilter + let count: Int + let hasUnmutedUnread: Bool +} +struct ChatListFilterBadges : Equatable { + let total:Int + let filters:[ChatListFilterBadge] + + func count(for filter: ChatListFilter?) -> ChatListFilterBadge? { + return filters.first(where: { $0.filter.id == filter?.id }) + } +} + +func chatListFilterItems(engine: TelegramEngine, accountManager: AccountManager) -> Signal { + + let settings = appNotificationSettings(accountManager: accountManager) |> distinctUntilChanged(isEqual: { lhs, rhs in + return lhs.badgeEnabled == rhs.badgeEnabled + }) + return combineLatest(engine.peers.updatedChatListFilters(), settings) + |> mapToSignal { filters, inAppSettings -> Signal<(Int, [(ChatListFilter, Int, Bool)]), NoError> in + + if !inAppSettings.badgeEnabled { + return .single((0, [])) + } + + var unreadCountItems: [UnreadMessageCountsItem] = [] + unreadCountItems.append(.totalInGroup(.root)) + var additionalPeerIds = Set() + var additionalGroupIds = Set() + for filter in filters { + additionalPeerIds.formUnion(filter.data.includePeers.peers) + additionalPeerIds.formUnion(filter.data.excludePeers) + if !filter.data.excludeArchived { + additionalGroupIds.insert(Namespaces.PeerGroup.archive) + } + } + if !additionalPeerIds.isEmpty { + for peerId in additionalPeerIds { + unreadCountItems.append(.peer(peerId)) + } + } + for groupId in additionalGroupIds { + unreadCountItems.append(.totalInGroup(groupId)) + } + let unreadKey: PostboxViewKey = .unreadCounts(items: unreadCountItems) + var keys: [PostboxViewKey] = [] + keys.append(unreadKey) + for peerId in additionalPeerIds { + keys.append(.basicPeer(peerId)) + } + + return combineLatest(queue: engine.account.postbox.queue, + engine.account.postbox.combinedView(keys: keys), + Signal.single(true) + ) + |> map { view, _ -> (Int, [(ChatListFilter, Int, Bool)]) in + guard let unreadCounts = view.views[unreadKey] as? UnreadMessageCountsView else { + return (0, []) + } + + var result: [(ChatListFilter, Int, Bool)] = [] + + var peerTagAndCount: [PeerId: (PeerSummaryCounterTags, Int, Bool)] = [:] + + var totalStates: [PeerGroupId: ChatListTotalUnreadState] = [:] + for entry in unreadCounts.entries { + switch entry { + case let .total(_, state): + totalStates[.root] = state + case let .totalInGroup(groupId, state): + totalStates[groupId] = state + case let .peer(peerId, state): + if let state = state, state.isUnread { + if let peerView = view.views[.basicPeer(peerId)] as? BasicPeerView, let peer = peerView.peer { + let tag = engine.account.postbox.seedConfiguration.peerSummaryCounterTags(peer, peerView.isContact) + + var peerCount = Int(state.count) + if state.isUnread { + peerCount = max(1, peerCount) + } + + if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings, case .muted = notificationSettings.muteState { + peerTagAndCount[peerId] = (tag, peerCount, false) + } else { + peerTagAndCount[peerId] = (tag, peerCount, true) + } + } + } + } + } + + let totalBadge = 0 + + for filter in filters { + var tags: [PeerSummaryCounterTags] = [] + if filter.data.categories.contains(.contacts) { + tags.append(.contact) + } + if filter.data.categories.contains(.nonContacts) { + tags.append(.nonContact) + } + if filter.data.categories.contains(.groups) { + tags.append(.group) + } + if filter.data.categories.contains(.bots) { + tags.append(.bot) + } + if filter.data.categories.contains(.channels) { + tags.append(.channel) + } + + var count = 0 + var hasUnmutedUnread = false + if let totalState = totalStates[.root] { + for tag in tags { + if filter.data.excludeMuted { + if let value = totalState.filteredCounters[tag] { + if value.chatCount != 0 { + count += Int(value.chatCount) + hasUnmutedUnread = true + } + } + } else { + if let value = totalState.absoluteCounters[tag] { + count += Int(value.chatCount) + } + if let value = totalState.filteredCounters[tag] { + if value.chatCount != 0 { + hasUnmutedUnread = true + } + } + } + } + } + if !filter.data.excludeArchived { + if let totalState = totalStates[Namespaces.PeerGroup.archive] { + for tag in tags { + if filter.data.excludeMuted { + if let value = totalState.filteredCounters[tag] { + if value.chatCount != 0 { + count += Int(value.chatCount) + hasUnmutedUnread = true + } + } + } else { + if let value = totalState.absoluteCounters[tag] { + count += Int(value.chatCount) + } + if let value = totalState.filteredCounters[tag] { + if value.chatCount != 0 { + hasUnmutedUnread = true + } + } + } + } + } + } + for peerId in filter.data.includePeers.peers { + if let (tag, peerCount, hasUnmuted) = peerTagAndCount[peerId] { + if !tags.contains(tag) { + if peerCount != 0 { + count += 1 + if hasUnmuted { + hasUnmutedUnread = true + } + } + } + } + } + for peerId in filter.data.excludePeers { + if let (tag, peerCount, _) = peerTagAndCount[peerId] { + if tags.contains(tag) { + if peerCount != 0 { + count -= 1 + } + } + } + } + result.append((filter, count, hasUnmutedUnread)) + } + + return (totalBadge, result) + } + } |> map { value -> ChatListFilterBadges in + return ChatListFilterBadges(total: value.0, filters: value.1.map { ChatListFilterBadge(filter: $0.0, count: max(0, $0.1), hasUnmutedUnread: $0.2) }) + } |> mapToSignal { badges -> Signal in + return renderedTotalUnreadCount(accountManager: accountManager, postbox: engine.account.postbox) |> map { + return ChatListFilterBadges(total: Int(max($0.0, 0)), filters: badges.filters) + } + } +} diff --git a/Telegram-Mac/ChatListFilterRecommendedItem.swift b/Telegram-Mac/ChatListFilterRecommendedItem.swift new file mode 100644 index 0000000000..a9e420f526 --- /dev/null +++ b/Telegram-Mac/ChatListFilterRecommendedItem.swift @@ -0,0 +1,104 @@ +// +// ChatListFilterRecommendedItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 04.03.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import SwiftSignalKit + +class ChatListFilterRecommendedItem: GeneralRowItem { + fileprivate let textLayout: TextViewLayout + init(_ initialSize: NSSize, stableId: AnyHashable, title: String, description: String, viewType: GeneralViewType, add: @escaping()->Void) { + + let attr = NSMutableAttributedString() + + _ = attr.append(string: title, color: theme.colors.text, font: .normal(.title)) + _ = attr.append(string: "\n", color: theme.colors.text, font: .normal(.title)) + _ = attr.append(string: description, color: theme.colors.grayText, font: .normal(.text)) + self.textLayout = TextViewLayout(attr) + super.init(initialSize, height: 40, stableId: stableId, viewType: viewType, action: add) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + + self.textLayout.measure(width: self.blockWidth - viewType.innerInset.left - viewType.innerInset.right - 60) + + return true + } + + override var instantlyResize: Bool { + return false + } + + override var height: CGFloat { + return self.textLayout.layoutSize.height + viewType.innerInset.top + viewType.innerInset.bottom + } + + override func viewClass() -> AnyClass { + return ChatListFilterRecommendedView.self + } + +} + +private final class ChatListFilterRecommendedView : GeneralContainableRowView { + private let textView: TextView = TextView() + private let button = TitleButton() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(textView) + addSubview(button) + button.autohighlight = true + textView.userInteractionEnabled = false + textView.isSelectable = false + } + + override func layout() { + super.layout() + guard let item = item as? GeneralRowItem else { + return + } + textView.centerY(x: item.viewType.innerInset.left) + button.centerY(x: item.blockWidth - button.frame.width - item.viewType.innerInset.right) + } + + override func updateColors() { + super.updateColors() + + textView.backgroundColor = backdorColor + button.set(background: theme.colors.accent, for: .Normal) + button.set(background: theme.colors.accent.withAlphaComponent(0.85), for: .Highlight) + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? ChatListFilterRecommendedItem else { + return + } + textView.update(item.textLayout) + + button.set(font: .medium(.text), for: .Normal) + button.set(color: theme.colors.underSelectedColor, for: .Normal) + button.set(text: L10n.chatListFilterRecommendedAdd, for: .Normal) + _ = button.sizeToFit(NSMakeSize(8, 8)) + button.layer?.cornerRadius = button.frame.height / 2 + + + button.removeAllHandlers() + button.set(handler: { [weak item] _ in + item?.action() + }, for: .Click) + needsLayout = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/ChatListFiltersHeaderItem.swift b/Telegram-Mac/ChatListFiltersHeaderItem.swift new file mode 100644 index 0000000000..db4db693ef --- /dev/null +++ b/Telegram-Mac/ChatListFiltersHeaderItem.swift @@ -0,0 +1,81 @@ +// +// ChatListFiltersHeaderItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 03.03.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +class ChatListFiltersHeaderItem: GeneralRowItem { + fileprivate let textLayout: TextViewLayout + fileprivate let context: AccountContext + fileprivate let sticker: LocalAnimatedSticker + init(_ initialSize: NSSize, context: AccountContext, stableId: AnyHashable, sticker: LocalAnimatedSticker, text: NSAttributedString) { + self.textLayout = TextViewLayout(text, alignment: .center, alwaysStaticItems: true) + self.context = context + self.sticker = sticker + super.init(initialSize, stableId: stableId, inset: NSEdgeInsets(left: 30.0, right: 30.0, top: 0, bottom: 10)) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { + textLayout.measure(width: width - inset.left - inset.right) + return super.makeSize(width, oldWidth: oldWidth) + } + + override func viewClass() -> AnyClass { + return ChatListFiltersHeaderView.self + } + + override var height: CGFloat { + return 112 + textLayout.layoutSize.height + (textLayout.layoutSize.height > 0 ? inset.bottom : 0) + } +} + + +private final class ChatListFiltersHeaderView : TableRowView { + private let stickerView: MediaAnimatedStickerView = MediaAnimatedStickerView(frame: NSZeroRect) + private let textView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(stickerView) + addSubview(textView) + textView.userInteractionEnabled = false + textView.isSelectable = false + } + + override var backdorColor: NSColor { + return theme.colors.listBackground + } + + override func updateColors() { + super.updateColors() + textView.backgroundColor = backdorColor + } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + + guard let item = item as? ChatListFiltersHeaderItem else { return } + + self.stickerView.update(with: item.sticker.file, size: NSMakeSize(112, 112), context: item.context, parent: nil, table: item.table, parameters: item.sticker.parameters, animated: animated, positionFlags: nil, approximateSynchronousValue: false) + + self.textView.update(item.textLayout) + + needsLayout = true + } + + override func layout() { + super.layout() + guard let item = item as? ChatListFiltersHeaderItem else { return } + + self.stickerView.centerX(y: 0) + self.textView.centerX(y: self.stickerView.frame.maxY + item.inset.bottom) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/ChatListFiltersListController.swift b/Telegram-Mac/ChatListFiltersListController.swift new file mode 100644 index 0000000000..3016cb3323 --- /dev/null +++ b/Telegram-Mac/ChatListFiltersListController.swift @@ -0,0 +1,282 @@ +// +// ChatListPresentController.swift +// Telegram +// +// Created by Mikhail Filimonov on 28.01.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import SwiftSignalKit +import Postbox +import TelegramCore + +import TGUIKit + + +private final class ChatListPresetArguments { + let context: AccountContext + let openPreset:(ChatListFilter, Bool)->Void + let removePreset: (ChatListFilter)->Void + let addFeatured: (ChatListFeaturedFilter)->Void + let toggleSidebar: (Bool)->Void + init(context: AccountContext, openPreset: @escaping(ChatListFilter, Bool)->Void, removePreset: @escaping(ChatListFilter)->Void, addFeatured: @escaping(ChatListFeaturedFilter)->Void, toggleSidebar: @escaping(Bool)->Void) { + self.context = context + self.openPreset = openPreset + self.removePreset = removePreset + self.addFeatured = addFeatured + self.toggleSidebar = toggleSidebar + } +} +private func _id_preset(_ filter: ChatListFilter) -> InputDataIdentifier { + return InputDataIdentifier("_id_filter_\(filter.id)") +} +private func _id_recommended(_ index: Int32) -> InputDataIdentifier { + return InputDataIdentifier("_id_recommended\(index)") +} +private let _id_add_new = InputDataIdentifier("_id_add_new") +private let _id_add_tabs = InputDataIdentifier("_id_add_tabs") +private let _id_badge_tabs = InputDataIdentifier("_id_badge_tabs") + +private let _id_header = InputDataIdentifier("_id_header") + +private func chatListPresetEntries(filtersWithCounts: [(ChatListFilter, Int)], sidebar: Bool, suggested: ChatListFiltersFeaturedState?, arguments: ChatListPresetArguments) -> [InputDataEntry] { + var entries: [InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_header, equatable: nil, comparable: nil, item: { initialSize, stableId in + + let attributedString = NSMutableAttributedString() + + _ = attributedString.append(string: L10n.chatListFilterHeader, color: theme.colors.listGrayText, font: .normal(.text)) + + return ChatListFiltersHeaderItem(initialSize, context: arguments.context, stableId: stableId, sticker: LocalAnimatedSticker.folder, text: attributedString) + })) + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.chatListFilterListHeader), data: .init(color: theme.colors.listGrayText, detectBold: true, viewType: .textTopItem))) + index += 1 + + for (filter, count) in filtersWithCounts { + var viewType = bestGeneralViewType(filtersWithCounts.map { $0.0 }, for: filter) + if filtersWithCounts.count == 1 { + viewType = .firstItem + } else if filter == filtersWithCounts.last?.0, filtersWithCounts.count < 10 { + viewType = .innerItem + } + + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_preset(filter), data: .init(name: filter.title, color: theme.colors.text, icon: FolderIcon(filter).icon(for: .preview), type: .nextContext(count > 0 ? "\(count)" : ""), viewType: viewType, enabled: true, description: nil, justUpdate: arc4random64(), action: { + arguments.openPreset(filter, false) + }, menuItems: { + return [ContextMenuItem(L10n.chatListFilterListRemove, handler: { + arguments.removePreset(filter) + })] + }))) + index += 1 + } + + if filtersWithCounts.count < 10 { + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_add_new, data: InputDataGeneralData(name: L10n.chatListFilterListAddNew, color: theme.colors.accent, type: .next, viewType: filtersWithCounts.isEmpty ? .singleItem : .lastItem, action: { + arguments.openPreset(ChatListFilter.new(excludeIds: filtersWithCounts.map { $0.0.id }), true) + }))) + index += 1 + } + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.chatListFilterListDesc), data: .init(color: theme.colors.listGrayText, detectBold: true, viewType: .textBottomItem))) + index += 1 + + + + + + + if let suggested = suggested, filtersWithCounts.count < 10 { + + let filtered = suggested.filters.filter { value -> Bool in + return filtersWithCounts.first(where: { $0.0.data == value.data }) == nil + } + if !filtered.isEmpty { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.chatListFilterRecommendedHeader), data: .init(color: theme.colors.listGrayText, detectBold: true, viewType: .textTopItem))) + index += 1 + + var suggeted_index:Int32 = 0 + for filter in filtered { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_recommended(suggeted_index), equatable: InputDataEquatable(filter), comparable: nil, item: { initialSize, stableId in + return ChatListFilterRecommendedItem(initialSize, stableId: stableId, title: filter.title, description: filter.description, viewType: bestGeneralViewType(filtered, for: filter), add: { + arguments.addFeatured(filter) + }) + })) + suggeted_index += 1 + index += 1 + } + } + + + } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + if !filtersWithCounts.isEmpty { + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.chatListFilterTabBarHeader), data: .init(color: theme.colors.listGrayText, detectBold: true, viewType: .textTopItem))) + index += 1 + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("sidebar"), equatable: InputDataEquatable(sidebar), comparable: nil, item: { initialSize, stableId in + return ChatListFilterVisibilityItem(initialSize, stableId: stableId, sidebar: sidebar, viewType: .singleItem, toggle: { sidebar in + arguments.toggleSidebar(sidebar) + }) + })) + index += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.chatListFilterTabBarDesc), data: .init(color: theme.colors.listGrayText, detectBold: true, viewType: .textBottomItem))) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + } + + + return entries +} + +func ChatListFiltersListController(context: AccountContext) -> InputDataController { + + let arguments = ChatListPresetArguments(context: context, openPreset: { filter, isNew in + context.sharedContext.bindings.rootNavigation().push(ChatListFilterController(context: context, filter: filter, isNew: isNew)) + }, removePreset: { filter in + confirm(for: context.window, header: L10n.chatListFilterConfirmRemoveHeader, information: L10n.chatListFilterConfirmRemoveText, okTitle: L10n.chatListFilterConfirmRemoveOK, successHandler: { _ in + _ = context.engine.peers.updateChatListFiltersInteractively({ filters in + var filters = filters + filters.removeAll(where: { $0.id == filter.id }) + return filters + }).start() + }) + + }, addFeatured: { featured in + _ = context.engine.peers.updateChatListFiltersInteractively({ filters in + var filters = filters + var new = ChatListFilter.new(excludeIds: filters.map { $0.id }) + new.data = featured.data + new.title = featured.title + filters.append(new) + return filters + }).start() + }, toggleSidebar: { sidebar in + _ = updateChatListFolderSettings(context.account.postbox, { + $0.withUpdatedSidebar(sidebar) + }).start() + }) + + + let chatCountCache = Atomic<[ChatListFilterData: Int]>(value: [:]) + + let filtersWithCounts = chatListFilterPreferences(engine: context.engine) + |> distinctUntilChanged + |> mapToSignal { filters -> Signal<([(ChatListFilter, Int)], Bool), NoError> in + return context.account.postbox.transaction { transaction -> ([(ChatListFilter, Int)], Bool) in + return (filters.list.map { filter -> (ChatListFilter, Int) in + let count: Int + if let cachedValue = chatCountCache.with({ dict -> Int? in + return dict[filter.data] + }) { + count = cachedValue + } else if let predicate = chatListFilterPredicate(for: filter) { + count = transaction.getChatCountMatchingPredicate(predicate) + let _ = chatCountCache.modify { dict in + var dict = dict + dict[filter.data] = count + return dict + } + } else { + count = 0 + } + return (filter, count) + }, filters.sidebar) + } + } + + let suggested: Signal = context.account.postbox.preferencesView(keys: [PreferencesKeys.chatListFiltersFeaturedState]) |> map { view in + return view.values[PreferencesKeys.chatListFiltersFeaturedState] as? ChatListFiltersFeaturedState + } + + + let dataSignal = combineLatest(queue: prepareQueue, appearanceSignal, filtersWithCounts, suggested) |> map { _, filtersWithCounts, suggested in + return chatListPresetEntries(filtersWithCounts: filtersWithCounts.0, sidebar: filtersWithCounts.1, suggested: suggested, arguments: arguments) + } |> map { entries in + return InputDataSignalValue(entries: entries) + } + + + let controller = InputDataController(dataSignal: dataSignal, title: L10n.chatListFilterListTitle, removeAfterDisappear: false, hasDone: false, identifier: "filters") + + + controller.updateDatas = { data in + return .none + } + + + controller.validateData = { data in + return .success(.custom { + + }) + } + + + controller.afterTransaction = { controller in + var range: NSRange = NSMakeRange(NSNotFound, 0) + + controller.tableView.enumerateItems(with: { item in + if let stableId = item.stableId.base as? InputDataEntryId { + switch stableId { + case let .general(identifier): + if identifier.identifier.hasPrefix("_id_filter") { + if range.location == NSNotFound { + range.location = item.index + } + range.length += 1 + } + default: + if range.location != NSNotFound { + return false + } + } + } + return true + }) + + if range.location != NSNotFound { + controller.tableView.resortController = TableResortController(resortRange: range, start: { row in + + }, resort: { row in + + }, complete: { from, to in + _ = context.engine.peers.updateChatListFiltersInteractively({ filters in + var filters = filters + filters.move(at: from - range.location, to: to - range.location) + return filters + }).start() + + }) + } else { + controller.tableView.resortController = nil + } + + + } + + return controller + +} diff --git a/Telegram-Mac/ChatListHoleRowItem.swift b/Telegram-Mac/ChatListHoleRowItem.swift index ca63194efd..806c7aa110 100644 --- a/Telegram-Mac/ChatListHoleRowItem.swift +++ b/Telegram-Mac/ChatListHoleRowItem.swift @@ -8,11 +8,12 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac +import Postbox +import TelegramCore + class ChatListHoleRowItem: TableRowItem { - private var account:Account + private var context: AccountContext private var hole:ChatListHole override var stableId: AnyHashable { @@ -23,16 +24,18 @@ class ChatListHoleRowItem: TableRowItem { return 20 } - public init(_ initialSize:NSSize, _ account:Account, _ object: ChatListHole) { + public init(_ initialSize:NSSize, _ context: AccountContext, _ object: ChatListHole) { self.hole = object - self.account = account + self.context = context super.init(initialSize) } override func viewClass() -> AnyClass { return ChatListHoleRowView.self } - - +} + + +class ChatListHoleRowView: TableRowView { } diff --git a/Telegram-Mac/ChatListHoleRowView.swift b/Telegram-Mac/ChatListHoleRowView.swift index eaba6cee31..3ebdd1eba1 100644 --- a/Telegram-Mac/ChatListHoleRowView.swift +++ b/Telegram-Mac/ChatListHoleRowView.swift @@ -8,6 +8,3 @@ import Cocoa import TGUIKit -class ChatListHoleRowView: TableRowView { - -} diff --git a/Telegram-Mac/ChatListMessageRowItem.swift b/Telegram-Mac/ChatListMessageRowItem.swift index 0f38c4e6f7..6b2f85c447 100644 --- a/Telegram-Mac/ChatListMessageRowItem.swift +++ b/Telegram-Mac/ChatListMessageRowItem.swift @@ -8,11 +8,17 @@ import Cocoa import TGUIKit +import TelegramCore + +import Postbox + class ChatListMessageRowItem: ChatListRowItem { + init(_ initialSize:NSSize, context: AccountContext, message: Message, query: String, renderedPeer:RenderedPeer, readState: CombinedPeerReadState?) { + super.init(initialSize, context: context, messages: [message], readState: readState, renderedPeer: renderedPeer, highlightText: query, showBadge: false) + } + override var stableId: AnyHashable { return message!.id } - - } diff --git a/Telegram-Mac/ChatListNothingItem.swift b/Telegram-Mac/ChatListNothingItem.swift index 1c8dc1381f..6df0b9745c 100644 --- a/Telegram-Mac/ChatListNothingItem.swift +++ b/Telegram-Mac/ChatListNothingItem.swift @@ -8,8 +8,9 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac +import TelegramCore + +import Postbox class ChatListNothingItem: TableRowItem { let stableIndex:ChatListIndex diff --git a/Telegram-Mac/ChatListPresetListController.swift b/Telegram-Mac/ChatListPresetListController.swift new file mode 100644 index 0000000000..c670b2c990 --- /dev/null +++ b/Telegram-Mac/ChatListPresetListController.swift @@ -0,0 +1,160 @@ +// +// ChatListPresentController.swift +// Telegram +// +// Created by Mikhail Filimonov on 28.01.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import SwiftSignalKit +import Postbox +import TelegramCore +import SyncCore +import TGUIKit + + +private final class ChatListPresetArguments { + let context: AccountContext + let openPreset:(ChatListFilter)->Void + let removePreset: (ChatListFilter)->Void + init(context: AccountContext, openPreset: @escaping(ChatListFilter)->Void, removePreset: @escaping(ChatListFilter)->Void) { + self.context = context + self.openPreset = openPreset + self.removePreset = removePreset + } +} +private func _id_preset(_ filter: ChatListFilter) -> InputDataIdentifier { + return InputDataIdentifier("_id_preset_\(filter.id)") +} +private let _id_add_new = InputDataIdentifier("_id_add_new") +private let _id_add_tabs = InputDataIdentifier("_id_add_tabs") +private let _id_badge_tabs = InputDataIdentifier("_id_badge_tabs") + +private func chatListPresetEntries(state: ChatListFiltersState, arguments: ChatListPresetArguments) -> [InputDataEntry] { + var entries: [InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain("FILTERS"), data: .init(color: theme.colors.listGrayText, detectBold: true, viewType: .textTopItem))) + index += 1 + + for filter in state.filters { + var viewType = bestGeneralViewType(state.filters, for: filter) + if state.filters.count == 1 { + viewType = .firstItem + } else if filter == state.filters.last, state.filters.count < 10 { + viewType = .innerItem + } + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_preset(filter), data: .init(name: filter.title, color: theme.colors.text, type: .nextContext(filter.desc), viewType: viewType, enabled: true, description: nil, justUpdate: arc4random64(), action: { + arguments.openPreset(filter) + }, menuItems: { + return [ContextMenuItem("Remove", handler: { + arguments.removePreset(filter) + })] + }))) + index += 1 + } + + if state.filters.count < 10 { + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_add_new, data: InputDataGeneralData(name: L10n.chatListFilterListAddNew, color: theme.colors.accent, type: .next, viewType: state.filters.isEmpty ? .singleItem : .lastItem, action: { + // arguments.openPreset(ChatListFilter.new) + }))) + index += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain("You can add \(10 - state.filters.count) more filters. Drag and drop filter to sort it."), data: .init(color: theme.colors.listGrayText, detectBold: true, viewType: .textBottomItem))) + index += 1 + } else { + entries.append(.desc(sectionId: sectionId, index: index, text: .plain("Drag and drop filter to sort it. Right click to remove."), data: .init(color: theme.colors.listGrayText, detectBold: true, viewType: .textBottomItem))) + index += 1 + } + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + return entries +} + +func ChatListFiltersListController(context: AccountContext) -> InputDataController { + + let arguments = ChatListPresetArguments(context: context, openPreset: { filter in + context.sharedContext.bindings.rootNavigation().push(ChatListFilterController(context: context, filter: filter)) + }, removePreset: { filter in + _ = updateChatListFilterSettingsInteractively(postbox: context.account.postbox, { + $0.withRemovedFilter(filter) + }).start() + }) + + let dataSignal = combineLatest(queue: prepareQueue, appearanceSignal, chatListFilterPreferences(postbox: context.account.postbox)) |> map { _, state in + return chatListPresetEntries(state: state, arguments: arguments) + } |> map { entries in + return InputDataSignalValue(entries: entries) + } + + + let controller = InputDataController(dataSignal: dataSignal, title: L10n.chatListFilterListTitle, removeAfterDisappear: false, hasDone: false) + + controller._abolishWhenNavigationSame = true + + controller.updateDatas = { data in + return .none + } + + + controller.validateData = { data in + return .success(.custom { + + }) + } + + + controller.afterTransaction = { controller in + var range: NSRange = NSMakeRange(NSNotFound, 0) + + controller.tableView.enumerateItems(with: { item in + if let stableId = item.stableId.base as? InputDataEntryId { + switch stableId { + case let .general(identifier): + if identifier.identifier.hasPrefix("_id_preset") { + if range.location == NSNotFound { + range.location = item.index + } + range.length += 1 + } + default: + if range.location != NSNotFound { + return false + } + } + } + return true + }) + + if range.location != NSNotFound { + controller.tableView.resortController = TableResortController(resortRange: range, start: { row in + + }, resort: { row in + + }, complete: { from, to in + _ = updateChatListFilterSettingsInteractively(postbox: context.account.postbox, { + $0.withMoveFilter(from - range.location, to - range.location) + }).start() + }) + } else { + controller.tableView.resortController = nil + } + + + } + + return controller + +} diff --git a/Telegram-Mac/ChatListRevealItem.swift b/Telegram-Mac/ChatListRevealItem.swift new file mode 100644 index 0000000000..0cf10f603c --- /dev/null +++ b/Telegram-Mac/ChatListRevealItem.swift @@ -0,0 +1,281 @@ +// +// ChatListRevealItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 27.01.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore +import Postbox +import SwiftSignalKit + + +class ChatListRevealItem: TableStickItem { + fileprivate let action:((ChatListFilter?)->Void)? + fileprivate let context: AccountContext? + fileprivate let tabs: [ChatListFilter] + fileprivate let selected: ChatListFilter? + fileprivate let openSettings: (()->Void)? + fileprivate let counters: ChatListFilterBadges + fileprivate let _menuItems: ((ChatListFilter?)->[ContextMenuItem])? + init(_ initialSize: NSSize, context: AccountContext, tabs: [ChatListFilter], selected: ChatListFilter?, counters: ChatListFilterBadges, action: ((ChatListFilter?)->Void)? = nil, openSettings: (()->Void)? = nil, menuItems: ((ChatListFilter?)->[ContextMenuItem])? = nil) { + self.action = action + self.context = context + self.tabs = tabs + self.selected = selected + self.openSettings = openSettings + self.counters = counters + self._menuItems = menuItems + super.init(initialSize) + } + + required init(_ initialSize: NSSize) { + self.action = nil + self.context = nil + self.tabs = [] + self.selected = nil + self.openSettings = nil + self._menuItems = nil + self.counters = ChatListFilterBadges(total: 0, filters: []) + super.init(initialSize) + } + + override var singletonItem: Bool { + return true + } + + func menuItems(for item: ChatListFilter?) -> [ContextMenuItem] { + return self._menuItems?(item) ?? [] + } + + override var stableId: AnyHashable { + return UIChatListEntryId.reveal + } + + override func viewClass() -> AnyClass { + return ChatListRevealView.self + } + + override var identifier: String { + return "ChatListRevealView" + } + + override var height: CGFloat { + return 36 + } +} + + +final class ChatListRevealView : TableStickView { + private let containerView = View() + private var animated: Bool = false + let segmentView: ScrollableSegmentView = ScrollableSegmentView(frame: NSZeroRect) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(containerView) + containerView.addSubview(segmentView) + border = [.Right] + + NotificationCenter.default.addObserver(forName: NSView.boundsDidChangeNotification, object: segmentView.scrollView.contentView, queue: OperationQueue.main, using: { [weak self] notification in + guard let `self` = self else { + return + } + guard let item = self.item else { + return + } + guard let view = item.view as? ChatListRevealView else { + return + } + if !self.segmentView.scrollView.clipView.isAnimateScrolling { + if view !== self { + view.segmentView.scrollView.contentView.scroll(to: self.segmentView.scrollView.documentOffset) + } else if let view = item.table?.p_stickView as? ChatListRevealView, view !== self { + view.segmentView.scrollView.contentView.scroll(to: self.segmentView.scrollView.documentOffset) + } + } + }) + + } + + + override func mouseUp(with event: NSEvent) { + + } + override func mouseDown(with event: NSEvent) { + if mouseInside() { + } + } + + + override func updateIsVisible(_ visible: Bool, animated: Bool) { + super.updateIsVisible(visible, animated: animated) + +// var visible = visible +// if let table = item?.table { +// visible = visible && table.documentOffset.y > 0 +// } +// separator.change(opacity: visible ? 1 : 0, animated: false) + } + + override var backdorColor: NSColor { + return theme.colors.background + } + + override func updateColors() { + super.updateColors() + backgroundColor = backdorColor + segmentView.updateLocalizationAndTheme(theme: theme) + needsDisplay = true + } + + private var splitViewState: SplitViewState? + + private var removeAnimationForNextTransition: Bool = false + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? ChatListRevealItem else { + return + } + + var animated = (self.animated || animated) + self.animated = true + + + guard let context = item.context else { + return + } + + let generateIcon:(ChatListFilter?)->CGImage? = { tab in + let unreadCount:ChatListFilterBadge? = item.counters.count(for: tab) + let icon: CGImage? + if let unreadCount = unreadCount, unreadCount.count > 0 { + let attributedString = NSAttributedString.initialize(string: "\(unreadCount.count.prettyNumber)", color: theme.colors.background, font: .medium(.short), coreText: true) + let textLayout = TextNode.layoutText(maybeNode: nil, attributedString, nil, 1, .start, NSMakeSize(CGFloat.greatestFiniteMagnitude, CGFloat.greatestFiniteMagnitude), nil, false, .center) + var size = NSMakeSize(textLayout.0.size.width + 8, textLayout.0.size.height + 5) + size = NSMakeSize(max(size.height,size.width), size.height) + + icon = generateImage(size, rotatedContext: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + if item.selected == tab || unreadCount.hasUnmutedUnread { + ctx.setFillColor(theme.colors.accent.cgColor) + } else { + ctx.setFillColor(theme.colors.grayText.cgColor) + } + + + ctx.round(size, size.height/2.0) + ctx.fill(rect) + + let focus = rect.focus(textLayout.0.size) + textLayout.1.draw(focus.offsetBy(dx: 0, dy: -1), in: ctx, backingScaleFactor: 2.0, backgroundColor: .white) + + })! + } else if let _ = tab { + icon = nil + } else { + icon = nil + } + return icon + } + + animated = animated && splitViewState == context.sharedContext.layout + self.splitViewState = context.sharedContext.layout + + let segmentTheme = ScrollableSegmentTheme(background: presentation.colors.background, border: presentation.colors.border, selector: presentation.colors.accent, inactiveText: presentation.colors.grayText, activeText: presentation.colors.accent, textFont: .normal(.title)) + var index: Int = 0 + let insets = NSEdgeInsets(left: 10, right: 10, bottom: 6) + var items:[ScrollableSegmentItem] = [.init(title: L10n.chatListFilterAllChats, index: 0, uniqueId: -1, selected: item.selected == nil, insets: insets, icon: generateIcon(nil), theme: segmentTheme, equatable: UIEquatable(L10n.chatListFilterAllChats))] + index += 1 + for tab in item.tabs { + let unreadCount = item.counters.count(for: tab) + let icon: CGImage? = generateIcon(tab) + let title: String = tab.title + + items.append(ScrollableSegmentItem(title: title, index: index, uniqueId: tab.id, selected: item.selected == tab, insets: insets, icon: icon, theme: segmentTheme, equatable: UIEquatable(unreadCount))) + index += 1 + } +// if let _ = item.openSettings { +// items.append(.init(title: "", index: index, uniqueId: -2, selected: false, insets: NSEdgeInsets(left: 5, right: 10, bottom: 6), icon: theme.icons.chat_filter_add, theme: segmentTheme, equatable: UIEquatable(0))) +// index += 1 +// } +// + + + segmentView.updateItems(items, animated: animated) + + segmentView.resortRange = NSMakeRange(1, items.count - 1) + segmentView.resortHandler = { from, to in + _ = context.engine.peers.updateChatListFiltersInteractively({ state in + var state = state + state.move(at: from - 1, to: to - 1) + return state + }).start() + } + segmentView.didChangeSelectedItem = { [weak item] selected in + if let item = item { + if selected.uniqueId == -1 { + item.action?(nil) + } else if selected.uniqueId == -2 { + item.openSettings?() + } else { + item.action?(item.tabs[selected.index - 1]) + } + } + } + segmentView.menuItems = { [weak item] selected in + if let item = item, selected.uniqueId != -1 && selected.uniqueId != -2 { + return item.menuItems(for: item.tabs[selected.index - 1]) + } else if let item = item, selected.uniqueId == -1 { + return item.menuItems(for: nil) + } else { + return [] + } + } + + } + + + override var isHidden: Bool { + didSet { + if isHidden { + var bp:Int = 0 + bp += 1 + } + } + } + + override var isAlwaysUp: Bool { + return true + } + + override func removeFromSuperview() { + super.removeFromSuperview() + } + + public override func setFrameOrigin(_ newOrigin: NSPoint) { + super.setFrameOrigin(newOrigin) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + override func layout() { + super.layout() + + containerView.frame = NSMakeRect(0, 0, bounds.width - 1, bounds.height) + + segmentView.frame = containerView.bounds + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/ChatListRowItem.swift b/Telegram-Mac/ChatListRowItem.swift index 390342413e..14dc8c51f8 100644 --- a/Telegram-Mac/ChatListRowItem.swift +++ b/Telegram-Mac/ChatListRowItem.swift @@ -8,32 +8,154 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac +import Postbox +import TelegramCore + +import SwiftSignalKit enum ChatListPinnedType { case some case last case none + case ad(AdditionalChatListItem) +} + + +final class SelectChatListItemPresentation : Equatable { + let selected:Set + static func ==(lhs:SelectChatListItemPresentation, rhs:SelectChatListItemPresentation) -> Bool { + return lhs.selected == rhs.selected + } + + init(_ selected:Set = Set()) { + self.selected = selected + } + + func deselect(chatLocation:ChatLocation) -> SelectChatListItemPresentation { + var chatLocations:Set = Set() + chatLocations.formUnion(selected) + let _ = chatLocations.remove(chatLocation) + return SelectChatListItemPresentation(chatLocations) + } + + func withToggledSelected(_ chatLocation: ChatLocation) -> SelectChatListItemPresentation { + var chatLocations:Set = Set() + chatLocations.formUnion(selected) + if chatLocations.contains(chatLocation) { + let _ = chatLocations.remove(chatLocation) + } else { + chatLocations.insert(chatLocation) + } + return SelectChatListItemPresentation(chatLocations) + } + +} + +final class SelectChatListInteraction : InterfaceObserver { + private(set) var presentation:SelectChatListItemPresentation = SelectChatListItemPresentation() + + func update(animated:Bool = true, _ f:(SelectChatListItemPresentation)->SelectChatListItemPresentation)->Void { + let oldValue = self.presentation + presentation = f(presentation) + if oldValue != presentation { + notifyObservers(value: presentation, oldValue:oldValue, animated:animated) + } + } + +} + +enum ChatListRowState : Equatable { + case plain + case deletable(onRemove:(ChatLocation)->Void, deletable:Bool) + + static func ==(lhs: ChatListRowState, rhs: ChatListRowState) -> Bool { + switch lhs { + case .plain: + if case .plain = rhs { + return true + } else { + return false + } + case .deletable(_, let deletable): + if case .deletable(_, deletable) = rhs { + return true + } else { + return false + } + } + } } + + class ChatListRowItem: TableRowItem { - public private(set) var message:Message? + struct Badge { + let dynamicValue: DynamicCounterTextView.Value + let backgroundColor: NSColor + let size: NSSize + init(dynamicValue: DynamicCounterTextView.Value, backgroundColor: NSColor, size: NSSize) { + self.dynamicValue = dynamicValue + self.backgroundColor = backgroundColor + var mapped = NSMakeSize(max(CGFloat(dynamicValue.values.count) * 10 - 10 + 7, size.width + 8), size.height + 7) + mapped = NSMakeSize(max(mapped.height,mapped.width), mapped.height) + self.size = mapped + } + } + + public private(set) var messages:[Message] + + var message: Message? { + var effective: Message? + + let filtered = messages.filter { !$0.text.isEmpty } + if filtered.count == 1 { + effective = filtered[0] + } + + if effective == nil { + effective = messages.first + } + return effective + } + + let context: AccountContext + let peer:Peer? + let renderedPeer:RenderedPeer? + let groupId: PeerGroupId + //let groupUnreadCounters: GroupReferenceUnreadCounters? + let chatListIndex:ChatListIndex? + var peerId:PeerId? { + return renderedPeer?.peerId + } - var account:Account - var peer:Peer? - let renderedPeer:RenderedPeer - var peerId:PeerId { - return renderedPeer.peerId + let photo: AvatarNodeState + + var isGroup: Bool { + return groupId != .root } - private let requestSessionId:MetaDisposable = MetaDisposable() override var stableId: AnyHashable { - return renderedPeer.peerId + return entryId + } + + var entryId: UIChatListEntryId { + if groupId != .root { + return .groupId(groupId) + } else if let index = chatListIndex { + return .chatId(index.messageIndex.id.peerId, nil) + } else { + preconditionFailure() + } + } + + var chatLocation: ChatLocation? { + if let index = chatListIndex { + return ChatLocation.peer(index.messageIndex.id.peerId) + } + return nil } let mentionsCount: Int32? @@ -41,58 +163,115 @@ class ChatListRowItem: TableRowItem { private var date:NSAttributedString? private var displayLayout:(TextNodeLayout, TextNode)? + private var chatNameLayout:(TextNodeLayout, TextNode)? + private var messageLayout:(TextNodeLayout, TextNode)? private var displaySelectedLayout:(TextNodeLayout, TextNode)? private var messageSelectedLayout:(TextNodeLayout, TextNode)? private var dateLayout:(TextNodeLayout, TextNode)? private var dateSelectedLayout:(TextNodeLayout, TextNode)? - + private var chatNameSelectedLayout:(TextNodeLayout, TextNode)? + private var displayNode:TextNode = TextNode() private var messageNode:TextNode = TextNode() private var displaySelectedNode:TextNode = TextNode() private var messageSelectedNode:TextNode = TextNode() - - private let messageText:NSAttributedString? + private var chatNameSelectedNode:TextNode = TextNode() + private var chatNameNode:TextNode = TextNode() + + private var messageText:NSAttributedString? private let titleText:NSAttributedString? - + private var chatTitleAttributed: NSAttributedString? private(set) var peerNotificationSettings:PeerNotificationSettings? private(set) var readState:CombinedPeerReadState? + + +// private var badge: Badge? = nil +// private var badgeSelected: Badge? = nil + + private var badgeNode:BadgeNode? = nil private var badgeSelectedNode:BadgeNode? = nil + private var additionalBadgeNode:BadgeNode? = nil + private var additionalBadgeSelectedNode:BadgeNode? = nil + + private var typingLayout:(TextNodeLayout, TextNode)? private var typingSelectedLayout:(TextNodeLayout, TextNode)? private let clearHistoryDisposable = MetaDisposable() private let deleteChatDisposable = MetaDisposable() + private let _animateArchive:Atomic = Atomic(value: false) - var isMuted:Bool { - if let peerNotificationSettings = peerNotificationSettings as? TelegramPeerNotificationSettings { - if case .muted(_) = peerNotificationSettings.muteState { - return true + var animateArchive:Bool { + return _animateArchive.swap(false) + } + + let filter: ChatListFilter? + + var isCollapsed: Bool { + if let archiveStatus = archiveStatus { + switch archiveStatus { + case .collapsed: + return context.sharedContext.layout != .minimisize + default: + return false } } return false } - let isVerified: Bool + var hasRevealState: Bool { + return canArchive || (groupId != .root && !isCollapsed) + } + var canArchive: Bool { + if groupId != .root { + return false + } + if context.peerId == peerId { + return false + } + if case .ad = pinnedType { + return false + } + let supportId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(777000)) + if self.peer?.id == supportId { + return false + } + + return true + } + + let associatedGroupId: PeerGroupId + + let isMuted:Bool + + var hasUnread: Bool { + return ctxBadgeNode != nil + } + + let isVerified: Bool + let isScam: Bool + let isFake: Bool + var isOutMessage:Bool { if let message = message { - return !message.flags.contains(.Incoming) + return !message.flags.contains(.Incoming) && message.id.peerId != context.peerId } return false } var isRead:Bool { if let peer = peer as? TelegramUser { if let _ = peer.botInfo { - return true + return !peer.flags.contains(.isSupport) } - if peer.id == account.peerId { + if peer.id == context.peerId { return true } } @@ -110,8 +289,21 @@ class ChatListRowItem: TableRowItem { return false } + + + var isUnreadMarked: Bool { + if let readState = readState { + return readState.markedUnread + } + return false + } + var isSecret:Bool { - return renderedPeer.peers[renderedPeer.peerId] is TelegramSecretChat + if let renderedPeer = renderedPeer { + return renderedPeer.peers[renderedPeer.peerId] is TelegramSecretChat + } else { + return false + } } var isSending:Bool { @@ -122,55 +314,360 @@ class ChatListRowItem: TableRowItem { } var isFailed: Bool { + return self.hasFailed + } + + var isSavedMessage: Bool { + return peer?.id == context.peerId + } + var isRepliesChat: Bool { + return peer?.id == repliesPeerId + } + + + + let hasDraft:Bool + private let hasFailed: Bool + let pinnedType:ChatListPinnedType + let activities: [ChatListInputActivity] + + var toolTip: String? { + return messageText?.string + } + + private(set) var isOnline: Bool? + + private(set) var hasActiveGroupCall: Bool = false + + private var presenceManager:PeerPresenceStatusManager? + + let archiveStatus: HiddenArchiveStatus? + + private var groupLatestPeers:[ChatListGroupReferencePeer] = [] + + private var textLeftCutout: CGFloat = 0.0 + let contentImageSize = CGSize(width: 16, height: 16) + let contentImageSpacing: CGFloat = 2.0 + let contentImageTrailingSpace: CGFloat = 5.0 + private(set) var contentImageSpecs: [(message: Message, media: Media, size: CGSize)] = [] + + + + init(_ initialSize:NSSize, context: AccountContext, pinnedType: ChatListPinnedType, groupId: PeerGroupId, peers: [ChatListGroupReferencePeer], messages: [Message], unreadState: PeerGroupUnreadCountersCombinedSummary, unreadCountDisplayCategory: TotalUnreadCountDisplayCategory, activities: [ChatListInputActivity] = [], animateGroup: Bool = false, archiveStatus: HiddenArchiveStatus = .normal, hasFailed: Bool = false, filter: ChatListFilter? = nil) { + self.groupId = groupId + self.peer = nil + self.messages = messages + self.chatListIndex = nil + self.activities = activities + self.context = context + self.mentionsCount = nil + self.pinnedType = pinnedType + self.renderedPeer = nil + self.associatedGroupId = .root + self.isMuted = false + self.isOnline = nil + self.archiveStatus = archiveStatus + self.groupLatestPeers = peers + self.isVerified = false + self.isScam = false + self.isFake = false + self.filter = filter + self.hasFailed = hasFailed + let titleText:NSMutableAttributedString = NSMutableAttributedString() + let _ = titleText.append(string: L10n.chatListArchivedChats, color: theme.chatList.textColor, font: .medium(.title)) + titleText.setSelected(color: theme.colors.underSelectedColor ,range: titleText.range) + + + var message: Message? + + let filtered = messages.filter { !$0.text.isEmpty } + if filtered.count == 1 { + message = filtered[0] + } + if message == nil { + message = messages.first + } + + self.titleText = titleText + if peers.count == 1 { + self.messageText = chatListText(account: context.account, for: message, messagesCount: messages.count, folder: true) + } else { + let textString = NSMutableAttributedString(string: "") + var isFirst = true + for peer in peers { + if let chatMainPeer = peer.peer.chatMainPeer { + let peerTitle = chatMainPeer.compactDisplayTitle + if !peerTitle.isEmpty { + if isFirst { + isFirst = false + } else { + textString.append(.initialize(string: ", ", color: theme.chatList.textColor, font: .normal(.text))) + } + textString.append(.initialize(string: peerTitle, color: peer.isUnread ? theme.chatList.textColor : theme.chatList.grayTextColor, font: .normal(.text))) + } + } + } + self.messageText = textString + } + hasDraft = false + + + + if let message = message { - return message.flags.contains(.Failed) + let date:NSMutableAttributedString = NSMutableAttributedString() + var time:TimeInterval = TimeInterval(message.timestamp) + time -= context.timeDifference + let range = date.append(string: DateUtils.string(forMessageListDate: Int32(time)), color: theme.colors.grayText, font: .normal(.short)) + date.setSelected(color: theme.colors.underSelectedColor,range: range) + self.date = date.copy() as? NSAttributedString + + dateLayout = TextNode.layoutText(maybeNode: nil, date, nil, 1, .end, NSMakeSize( .greatestFiniteMagnitude, 20), nil, false, .left) + dateSelectedLayout = TextNode.layoutText(maybeNode: nil, date, nil, 1, .end, NSMakeSize( .greatestFiniteMagnitude, 20), nil, true, .left) } - return false + + + let mutedCount = unreadState.count(countingCategory: unreadCountDisplayCategory == .chats ? .chats : .messages, mutedCategory: .all) + + self.highlightText = nil + self.embeddedState = nil + + photo = .ArchivedChats + + super.init(initialSize) + + if case .hidden(true) = archiveStatus { + hideItem(animated: false, reload: false) + } + + + _ = _animateArchive.swap(animateGroup) + + if mutedCount > 0 { + +// var dynamicValue = DynamicCounterTextView.make(for: "\(mutedCount)", count: "\(mutedCount)", font: .medium(.small), textColor: theme.chatList.badgeTextColor, width: 100) +// badge = Badge(dynamicValue: dynamicValue, backgroundColor: theme.chatList.badgeMutedBackgroundColor, size: dynamicValue.size) +// +// dynamicValue = DynamicCounterTextView.make(for: "\(mutedCount)", count: "\(mutedCount)", font: .medium(.small), textColor: theme.chatList.badgeSelectedTextColor, width: 100) +// badgeSelected = Badge(dynamicValue: dynamicValue, backgroundColor: theme.chatList.badgeSelectedBackgroundColor, size: dynamicValue.size) + + + badgeNode = BadgeNode(.initialize(string: "\(mutedCount)", color: theme.chatList.badgeTextColor, font: .medium(.small)), theme.chatList.badgeMutedBackgroundColor) + badgeSelectedNode = BadgeNode(.initialize(string: "\(mutedCount)", color: theme.chatList.badgeSelectedTextColor, font: .medium(.small)), theme.chatList.badgeSelectedBackgroundColor) + } + + + //theme.chatList.badgeBackgroundColor + + + + _ = makeSize(initialSize.width, oldWidth: 0) } - let hasDraft:Bool + private let highlightText: String? - let pinnedType:ChatListPinnedType + private let embeddedState:StoredPeerChatInterfaceState? + init(_ initialSize:NSSize, context: AccountContext, messages: [Message], index: ChatListIndex? = nil, readState:CombinedPeerReadState? = nil, isMuted:Bool = false, embeddedState:StoredPeerChatInterfaceState? = nil, pinnedType:ChatListPinnedType = .none, renderedPeer:RenderedPeer, peerPresence: PeerPresence? = nil, summaryInfo: ChatListMessageTagSummaryInfo = ChatListMessageTagSummaryInfo(), activities: [ChatListInputActivity] = [], highlightText: String? = nil, associatedGroupId: PeerGroupId = .root, hasFailed: Bool = false, showBadge: Bool = true, filter: ChatListFilter? = nil) { + + + var embeddedState = embeddedState + + if let peer = renderedPeer.chatMainPeer as? TelegramChannel { + if !peer.hasPermission(.sendMessages) { + embeddedState = nil + } + } + let interfaceState = embeddedState.flatMap(_internal_decodeStoredChatInterfaceState).flatMap({ + ChatInterfaceState.parse($0, peerId: nil, context: nil) + }) + if let interfaceState = interfaceState { + if interfaceState.inputState.inputText.isEmpty { + embeddedState = nil + } + } + let supportId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(777000)) - init(_ initialSize:NSSize, account:Account, message: Message?, readState:CombinedPeerReadState? = nil, notificationSettings:PeerNotificationSettings? = nil, embeddedState:PeerChatListEmbeddedInterfaceState? = nil, pinnedType:ChatListPinnedType = .none, renderedPeer:RenderedPeer, summaryInfo: ChatListMessageTagSummaryInfo = ChatListMessageTagSummaryInfo()) { + if let peerPresence = peerPresence, context.peerId != renderedPeer.peerId, renderedPeer.peerId != supportId { + if let peerPresence = peerPresence as? TelegramUserPresence { + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + let relative = relativeUserPresenceStatus(peerPresence, timeDifference: context.timeDifference, relativeTo: Int32(timestamp)) + switch relative { + case .online: + self.isOnline = true + default: + self.isOnline = false + } + } else { + self.isOnline = nil + } + } else { + self.isOnline = nil + } + + if let peer = renderedPeer.chatMainPeer as? TelegramChannel, peer.flags.contains(.hasActiveVoiceChat) { + self.hasActiveGroupCall = true + } + + + var message: Message? + let filtered = messages.filter { !$0.text.isEmpty } + if filtered.count == 1 { + message = filtered[0] + } + if message == nil { + message = messages.first + } + + self.chatListIndex = index self.renderedPeer = renderedPeer - self.account = account - self.message = message + self.context = context + self.messages = messages + self.activities = activities self.pinnedType = pinnedType + self.archiveStatus = nil self.hasDraft = embeddedState != nil + self.embeddedState = embeddedState self.peer = renderedPeer.chatMainPeer - + self.groupId = .root + self.hasFailed = hasFailed + self.filter = filter + self.associatedGroupId = associatedGroupId + self.highlightText = highlightText if let peer = peer { - isVerified = peer.isVerified + self.isVerified = peer.isVerified + self.isScam = peer.isScam + self.isFake = peer.isFake } else { - isVerified = false + self.isVerified = false + self.isScam = false + self.isFake = false } + - self.peerNotificationSettings = notificationSettings + self.isMuted = isMuted self.readState = readState let titleText:NSMutableAttributedString = NSMutableAttributedString() - let _ = titleText.append(string: peer?.displayTitle, color: renderedPeer.peers[renderedPeer.peerId] is TelegramSecretChat ? theme.chatList.secretChatTextColor : theme.chatList.textColor, font: .medium(.title)) - titleText.setSelected(color: .white ,range: titleText.range) + let _ = titleText.append(string: peer?.id == context.peerId ? L10n.peerSavedMessages : peer?.displayTitle, color: renderedPeer.peers[renderedPeer.peerId] is TelegramSecretChat ? theme.chatList.secretChatTextColor : theme.chatList.textColor, font: .medium(.title)) + titleText.setSelected(color: theme.colors.underSelectedColor ,range: titleText.range) self.titleText = titleText - self.messageText = chatListText(account: account, for: message, renderedPeer: renderedPeer, embeddedState:embeddedState) - + - if let message = message { + if case let .ad(item) = pinnedType, let promo = item as? PromoChatListItem { + let sponsored:NSMutableAttributedString = NSMutableAttributedString() + let range: NSRange + switch promo.kind { + case let .psa(type, _): + range = sponsored.append(string: localizedPsa("psa.chatlist", type: type), color: theme.colors.grayText, font: .normal(.short)) + case .proxy: + range = sponsored.append(string: L10n.chatListSponsoredChannel, color: theme.colors.grayText, font: .normal(.short)) + } + sponsored.setSelected(color: theme.colors.underSelectedColor, range: range) + self.date = sponsored + dateLayout = TextNode.layoutText(maybeNode: nil, sponsored, nil, 1, .end, NSMakeSize( .greatestFiniteMagnitude, 20), nil, false, .left) + dateSelectedLayout = TextNode.layoutText(maybeNode: nil, sponsored, nil, 1, .end, NSMakeSize( .greatestFiniteMagnitude, 20), nil, true, .left) + } else if let message = message { let date:NSMutableAttributedString = NSMutableAttributedString() var time:TimeInterval = TimeInterval(message.timestamp) - time -= account.context.timeDifference + time -= context.timeDifference let range = date.append(string: DateUtils.string(forMessageListDate: Int32(time)), color: theme.colors.grayText, font: .normal(.short)) - date.setSelected(color: .white,range: range) + date.setSelected(color: theme.colors.underSelectedColor, range: range) self.date = date.copy() as? NSAttributedString dateLayout = TextNode.layoutText(maybeNode: nil, date, nil, 1, .end, NSMakeSize( .greatestFiniteMagnitude, 20), nil, false, .left) dateSelectedLayout = TextNode.layoutText(maybeNode: nil, date, nil, 1, .end, NSMakeSize( .greatestFiniteMagnitude, 20), nil, true, .left) + + + var author: Peer? + if message.isImported, let info = message.forwardInfo { + if let peer = info.author { + author = peer + } else if let signature = info.authorSignature { + + author = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(0)), accessHash: nil, firstName: signature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + } + } else { + author = message.author + } + + if let author = author as? TelegramUser, let peer = peer, peer as? TelegramUser == nil, !peer.isChannel, embeddedState == nil { + if !(message.media.first is TelegramMediaAction) { + let peerText: String = (author.id == context.account.peerId ? "\(L10n.chatListYou)" : author.displayTitle) + + let attr = NSMutableAttributedString() + _ = attr.append(string: peerText, color: theme.chatList.peerTextColor, font: .normal(.text)) + attr.setSelected(color: theme.colors.underSelectedColor, range: attr.range) + + self.chatTitleAttributed = attr + } + } + + let contentImageFillSize = CGSize(width: 8.0, height: contentImageSize.height) + _ = contentImageFillSize + let isSecret: Bool + isSecret = renderedPeer.peers[renderedPeer.peerId] is TelegramSecretChat + + if embeddedState == nil, !isSecret { + for message in messages { + inner: for media in message.media { + if !message.containsSecretMedia { + if let image = media as? TelegramMediaImage { + if let _ = largestImageRepresentation(image.representations) { + //let imageSize = largest.dimensions.cgSize + //let fitSize = imageSize.aspectFilled(contentImageFillSize) + let fitSize = contentImageSize + contentImageSpecs.append((message, image, fitSize)) + } + break inner + } else if let file = media as? TelegramMediaFile { + if file.isVideo, !file.isInstantVideo, let _ = file.dimensions { + //let imageSize = dimensions.cgSize + //let fitSize = imageSize.aspectFilled(contentImageFillSize) + let fitSize = contentImageSize + contentImageSpecs.append((message, file, fitSize)) + } + break inner + } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, false { + let imageTypes = ["photo", "video", "embed", "gif", "document", "telegram_album"] + if let image = content.image, let type = content.type, imageTypes.contains(type) { + if let _ = largestImageRepresentation(image.representations) { + //let imageSize = largest.dimensions.cgSize + let fitSize = contentImageSize + contentImageSpecs.append((message, image, fitSize)) + } + break inner + } else if let file = content.file { + if file.isVideo, !file.isInstantVideo, let _ = file.dimensions { + //let imageSize = dimensions.cgSize + let fitSize = contentImageSize + contentImageSpecs.append((message, file, fitSize)) + } + break inner + } + } + } + } + } + } + } + + contentImageSpecs = Array(contentImageSpecs.prefix(3)) + + for i in 0 ..< contentImageSpecs.count { + if i != 0 { + textLeftCutout += contentImageSpacing + } + textLeftCutout += contentImageSpecs[i].size.width + if i == contentImageSpecs.count - 1 { + textLeftCutout += contentImageTrailingSpace + } } + + let tagSummaryCount = summaryInfo.tagSummaryCount ?? 0 let actionsSummaryCount = summaryInfo.actionsSummaryCount ?? 0 @@ -181,166 +678,546 @@ class ChatListRowItem: TableRowItem { self.mentionsCount = nil } + if let peer = peer, peer.id != context.peerId && peer.id != repliesPeerId { + self.photo = .PeerAvatar(peer, peer.displayLetters, peer.smallProfileImage, nil, nil) + } else { + self.photo = .Empty + } + super.init(initialSize) - if let unreadCount = readState?.count, unreadCount > 0, mentionsCount == nil || (unreadCount > 1 || mentionsCount! != unreadCount) { + if showBadge { + if let unreadCount = readState?.count, unreadCount > 0, mentionsCount == nil || (unreadCount > 1 || mentionsCount! != unreadCount) { + +// var dynamicValue = DynamicCounterTextView.make(for: "\(unreadCount)", count: "\(unreadCount)", font: .medium(.small), textColor: theme.chatList.badgeTextColor, width: 100) +// badge = Badge(dynamicValue: dynamicValue, backgroundColor: isMuted ? theme.chatList.badgeMutedBackgroundColor : theme.chatList.badgeBackgroundColor, size: dynamicValue.size) +// +// dynamicValue = DynamicCounterTextView.make(for: "\(unreadCount)", count: "\(unreadCount)", font: .medium(.small), textColor: theme.chatList.badgeSelectedTextColor, width: 100) +// badgeSelected = Badge(dynamicValue: dynamicValue, backgroundColor: theme.chatList.badgeSelectedBackgroundColor, size: dynamicValue.size) + + + + badgeNode = BadgeNode(.initialize(string: "\(unreadCount)", color: theme.chatList.badgeTextColor, font: .medium(.small)), isMuted ? theme.chatList.badgeMutedBackgroundColor : theme.chatList.badgeBackgroundColor) + badgeSelectedNode = BadgeNode(.initialize(string: "\(unreadCount)", color: theme.chatList.badgeSelectedTextColor, font: .medium(.small)), theme.chatList.badgeSelectedBackgroundColor) + } else if isUnreadMarked && mentionsCount == nil { + + +// var dynamicValue = DynamicCounterTextView.make(for: " ", count: " ", font: .medium(.small), textColor: theme.chatList.badgeTextColor, width: 100) +// badge = Badge(dynamicValue: dynamicValue, backgroundColor: isMuted ? theme.chatList.badgeMutedBackgroundColor : theme.chatList.badgeBackgroundColor, size: dynamicValue.size + NSSize(width: 8, height: 7)) +// +// dynamicValue = DynamicCounterTextView.make(for: " ", count: " ", font: .medium(.small), textColor: theme.chatList.badgeSelectedTextColor, width: 100) +// badgeSelected = Badge(dynamicValue: dynamicValue, backgroundColor: theme.chatList.badgeSelectedBackgroundColor, size: dynamicValue.size + NSSize(width: 8, height: 7)) +// + badgeNode = BadgeNode(.initialize(string: " ", color: theme.chatList.badgeTextColor, font: .medium(.small)), isMuted ? theme.chatList.badgeMutedBackgroundColor : theme.chatList.badgeBackgroundColor) + badgeSelectedNode = BadgeNode(.initialize(string: " ", color: theme.chatList.badgeSelectedTextColor, font: .medium(.small)), theme.chatList.badgeSelectedBackgroundColor) + } + } + + + + if let _ = self.isOnline, let presence = peerPresence as? TelegramUserPresence { + presenceManager = PeerPresenceStatusManager(update: { [weak self] in + self?.isOnline = false + self?.redraw(animated: true) + }) - badgeNode = BadgeNode(.initialize(string: "\(unreadCount)", color: theme.chatList.badgeTextColor, font: .medium(.small)), isMuted ? theme.chatList.badgeMutedBackgroundColor : theme.chatList.badgeBackgroundColor) - badgeSelectedNode = BadgeNode(.initialize(string: "\(unreadCount)", color: theme.chatList.badgeSelectedTextColor, font: .medium(.small)), theme.chatList.badgeSelectedBackgroundColor) + presenceManager?.reset(presence: presence, timeDifference: Int32(context.timeDifference)) } + _ = makeSize(initialSize.width, oldWidth: 0) } let margin:CGFloat = 9 + + var isPinned: Bool { + switch pinnedType { + case .some: + return true + case .last: + return true + default: + return false + } + } + + var isLastPinned: Bool { + switch pinnedType { + case .last: + return true + default: + return false + } + } + + + var isFixedItem: Bool { + switch pinnedType { + case .some, .ad, .last: + return true + default: + return false + } + } + +// var contentDimensions: NSSize? { +// var dimensions: CGSize? +// if let contentImageMedia = contentImageMedia as? TelegramMediaImage { +// dimensions = largestRepresentationForPhoto(contentImageMedia)?.dimensions.size +// } else if let contentImageMedia = contentImageMedia as? TelegramMediaFile { +// dimensions = contentImageMedia.dimensions?.size +// } +// return dimensions +// } + + var isAd: Bool { + switch pinnedType { + case .ad: + return true + default: + return false + } + } + + var badIcon: CGImage { + return isScam ? theme.icons.scam : theme.icons.fake + } + var badHighlightIcon: CGImage { + return isScam ? theme.icons.scamActive : theme.icons.fakeActive + } var titleWidth:CGFloat { var dateSize:CGFloat = 0 if let dateLayout = dateLayout { dateSize = dateLayout.0.size.width } - - return max(300, size.width) - 50 - margin * 4 - dateSize - (isMuted ? theme.icons.dialogMuteImage.backingSize.width + 4 : 0) - (isOutMessage ? isRead ? 14 : 8 : 0) - (isVerified ? 10 : 0) - (isSecret ? 10 : 0) + var offset: CGFloat = 0 + if isScam || isFake { + offset += badIcon.backingSize.width + 4 + } + if isMuted { + offset += theme.icons.dialogMuteImage.backingSize.width + 4 + } + if isVerified { + offset += 20 + } + if isSecret { + offset += 10 + } + return max(300, size.width) - 50 - margin * 4 - dateSize - (isOutMessage ? isRead ? 14 : 8 : 0) - offset } var messageWidth:CGFloat { if let badgeNode = badgeNode { - return (max(300, size.width) - 50 - margin * 3) - badgeNode.size.width - 5 - (mentionsCount != nil ? 24 : 0) + return (max(300, size.width) - 50 - margin * 3) - (badgeNode.size.width + 5) - (mentionsCount != nil ? 30 : 0) - (additionalBadgeNode != nil ? additionalBadgeNode!.size.width + 15 : 0) - (chatTitleAttributed != nil ? textLeftCutout : 0) } - return (max(300, size.width) - 50 - margin * 4) - (pinnedType != .none ? 20 : 0) - (mentionsCount != nil ? 24 : 0) + + return (max(300, size.width) - 50 - margin * 4) - (isPinned ? 20 : 0) - (mentionsCount != nil ? 24 : 0) - (additionalBadgeNode != nil ? additionalBadgeNode!.size.width + 15 : 0) - (chatTitleAttributed != nil ? textLeftCutout : 0) } let leftInset:CGFloat = 50 + (10 * 2.0); override func makeSize(_ width: CGFloat, oldWidth:CGFloat) -> Bool { - if self.oldWidth == 0 || self.oldWidth != width { - - if displayLayout == nil || !displayLayout!.0.isPerfectSized || self.oldWidth > width { - displayLayout = TextNode.layoutText(maybeNode: displayNode, titleText, nil, 1, .end, NSMakeSize(titleWidth, size.height), nil, false, .left) - } - if messageLayout == nil || !messageLayout!.0.isPerfectSized || self.oldWidth > width { - messageLayout = TextNode.layoutText(maybeNode: messageNode, messageText, nil, 2, .end, NSMakeSize(messageWidth, size.height), nil, false, .left) - } - if displaySelectedLayout == nil || !displaySelectedLayout!.0.isPerfectSized || self.oldWidth > width { - displaySelectedLayout = TextNode.layoutText(maybeNode: displaySelectedNode, titleText, nil, 1, .end, NSMakeSize(titleWidth, size.height), nil, true, .left) + let result = super.makeSize(width, oldWidth: oldWidth) + + if self.groupId == .root { + var text: NSAttributedString? + if case let .ad(promo) = pinnedType, message == nil { + if let promo = promo as? PromoChatListItem { + switch promo.kind { + case let .psa(_, message): + if let message = message { + let attr = NSMutableAttributedString() + _ = attr.append(string: message, color: theme.colors.grayText, font: .normal(.text)) + attr.setSelected(color: theme.colors.underSelectedColor, range: attr.range) + text = attr + } + default: + break + } + } } - if messageSelectedLayout == nil || !messageSelectedLayout!.0.isPerfectSized || self.oldWidth > width { - messageSelectedLayout = TextNode.layoutText(maybeNode: messageSelectedNode, messageText, nil, 2, .end, NSMakeSize(messageWidth, size.height), nil, true, .left) + if text == nil { + var messageText = chatListText(account: context.account, for: message, messagesCount: self.messages.count, renderedPeer: renderedPeer, embeddedState: embeddedState) + if let query = highlightText, let copy = messageText.mutableCopy() as? NSMutableAttributedString, let range = rangeOfSearch(query, in: copy.string) { + if copy.range.contains(range.min) && copy.range.contains(range.max - 1), copy.range != range { + copy.addAttribute(.foregroundColor, value: theme.colors.text, range: range) + copy.addAttribute(.font, value: NSFont.medium(.text), range: range) + messageText = copy + } + } + text = messageText } + self.messageText = text! + } + + + + if displayLayout == nil || !displayLayout!.0.isPerfectSized || self.oldWidth > width { + displayLayout = TextNode.layoutText(maybeNode: displayNode, titleText, nil, 1, .end, NSMakeSize(titleWidth, size.height), nil, false, .left) } - return super.makeSize(width, oldWidth: oldWidth) + + if displaySelectedLayout == nil || !displaySelectedLayout!.0.isPerfectSized || self.oldWidth > width { + displaySelectedLayout = TextNode.layoutText(maybeNode: displaySelectedNode, titleText, nil, 1, .end, NSMakeSize(titleWidth, size.height), nil, true, .left) + } + + if chatNameLayout == nil || !chatNameLayout!.0.isPerfectSized || self.oldWidth > width, let chatTitleAttributed = chatTitleAttributed { + chatNameLayout = TextNode.layoutText(maybeNode: chatNameNode, chatTitleAttributed, nil, 1, .end, NSMakeSize(titleWidth, size.height), nil, false, .left) + } + + if chatNameSelectedLayout == nil || !chatNameSelectedLayout!.0.isPerfectSized || self.oldWidth > width, let chatTitleAttributed = chatTitleAttributed { + chatNameSelectedLayout = TextNode.layoutText(maybeNode: chatNameSelectedNode, chatTitleAttributed, nil, 1, .end, NSMakeSize(titleWidth, size.height), nil, true, .left) + } + + var textCutout: TextNodeCutout? + if !textLeftCutout.isZero { + textCutout = TextNodeCutout(position: .TopLeft, size: CGSize(width: textLeftCutout, height: 14)) + } + + + if messageLayout == nil || !messageLayout!.0.isPerfectSized || self.oldWidth > width { + messageLayout = TextNode.layoutText(maybeNode: messageNode, messageText, nil, chatTitleAttributed != nil ? 1 : 2, .end, NSMakeSize(messageWidth, size.height), textCutout, false, .left, 1) + } + if messageSelectedLayout == nil || !messageSelectedLayout!.0.isPerfectSized || self.oldWidth > width { + messageSelectedLayout = TextNode.layoutText(maybeNode: messageSelectedNode, messageText, nil, chatTitleAttributed != nil ? 1 : 2, .end, NSMakeSize(messageWidth, size.height), textCutout, true, .left, 1) + } + return result } + + var markAsUnread: Bool { + return !isSecret && !isUnreadMarked && badgeNode == nil && mentionsCount == nil + } + + func collapseOrExpandArchive() { + ChatListRowItem.collapseOrExpandArchive(context: context) + } + + static func collapseOrExpandArchive(context: AccountContext) { + context.sharedContext.bindings.mainController().chatList.collapseOrExpandArchive() + } + + static func toggleHideArchive(context: AccountContext) { + context.sharedContext.bindings.mainController().chatList.toggleHideArchive() + } + + func toggleHideArchive() { + ChatListRowItem.toggleHideArchive(context: context) + } + func toggleUnread() { + if let peerId = peerId { + _ = togglePeerUnreadMarkInteractively(postbox: context.account.postbox, viewTracker: context.account.viewTracker, peerId: peerId).start() + } + } - override func menuItems() -> Signal<[ContextMenuItem], Void> { - - if let peer = peer { - var items:[ContextMenuItem] = [] + func toggleMuted() { + if let peerId = peerId { + ChatListRowItem.toggleMuted(context: context, peerId: peerId, isMuted: isMuted) + } + } + + static func toggleMuted(context: AccountContext, peerId: PeerId, isMuted: Bool) { + if isMuted { + _ = context.engine.peers.togglePeerMuted(peerId: peerId).start() + } else { + var options:[ModalOptionSet] = [] - let deleteChat = {[weak self] in - if let strongSelf = self { - let signal = removeChatInteractively(account: strongSelf.account, peerId: strongSelf.peerId) |> filter {$0} |> mapToSignal { _ -> Signal in - return globalPeerHandler.get() |> take(1) - } |> deliverOnMainQueue - - strongSelf.deleteChatDisposable.set(signal.start(next: { [weak self] peerId in - if peerId == self?.peerId { - self?.account.context.mainNavigation?.close() - } - })) + options.append(ModalOptionSet(title: L10n.chatListMute1Hour, selected: false, editable: true)) + options.append(ModalOptionSet(title: L10n.chatListMute4Hours, selected: false, editable: true)) + options.append(ModalOptionSet(title: L10n.chatListMute8Hours, selected: false, editable: true)) + options.append(ModalOptionSet(title: L10n.chatListMute1Day, selected: false, editable: true)) + options.append(ModalOptionSet(title: L10n.chatListMute3Days, selected: false, editable: true)) + options.append(ModalOptionSet(title: L10n.chatListMuteForever, selected: true, editable: true)) + + let intervals:[Int32] = [60 * 60, 60 * 60 * 4, 60 * 60 * 8, 60 * 60 * 24, 60 * 60 * 24 * 3, Int32.max] + + showModal(with: ModalOptionSetController(context: context, options: options, selectOne: true, actionText: (L10n.chatInputMute, theme.colors.accent), title: L10n.peerInfoNotifications, result: { result in + + for (i, option) in result.enumerated() { + inner: switch option { + case .selected: + _ = context.engine.peers.updatePeerMuteSetting(peerId: peerId, muteInterval: intervals[i]).start() + break + default: + break inner + } } + + }), for: context.window) + } + } + + func togglePinned() { + ChatListRowItem.togglePinned(context: context, chatLocation: chatLocation, filter: filter, associatedGroupId: associatedGroupId) + } + + static func togglePinned(context: AccountContext, chatLocation: ChatLocation?, filter: ChatListFilter?, associatedGroupId: PeerGroupId) { + if let chatLocation = chatLocation { + let location: TogglePeerChatPinnedLocation + + if let filter = filter { + location = .filter(filter.id) + } else { + location = .group(associatedGroupId) } + let context = context - let clearHistory = { [weak self] in - if let strongSelf = self { - confirm(for: mainWindow, with: appName, and: tr(.confirmDeleteChatUser), successHandler: { _ in - strongSelf.clearHistoryDisposable.set(clearHistoryInteractively(postbox: strongSelf.account.postbox, peerId: strongSelf.peerId).start()) - }) + _ = (context.engine.peers.toggleItemPinned(location: location, itemId: chatLocation.pinnedItemId) |> deliverOnMainQueue).start(next: { result in + switch result { + case .limitExceeded: + confirm(for: context.window, information: L10n.chatListContextPinErrorNew2, okTitle: L10n.alertOK, cancelTitle: "", thridTitle: L10n.chatListContextPinErrorNewSetupFolders, successHandler: { result in + + switch result { + case .thrid: + context.sharedContext.bindings.rootNavigation().push(ChatListFiltersListController(context: context)) + default: + break + } + + }) + default: + break } + }) + } + + } + + func toggleArchive() { + ChatListRowItem.toggleArchive(context: context, associatedGroupId: associatedGroupId, peerId: peerId) + } + + static func toggleArchive(context: AccountContext, associatedGroupId: PeerGroupId?, peerId: PeerId?) { + if let peerId = peerId { + switch associatedGroupId { + case .root: + let postbox = context.account.postbox + context.sharedContext.bindings.mainController().chatList.setAnimateGroupNextTransition(Namespaces.PeerGroup.archive) + _ = updatePeerGroupIdInteractively(postbox: postbox, peerId: peerId, groupId: Namespaces.PeerGroup.archive).start() + default: + _ = updatePeerGroupIdInteractively(postbox: context.account.postbox, peerId: peerId, groupId: .root).start() } + } + } + + func delete() { + if let peerId = peerId { + let signal = removeChatInteractively(context: context, peerId: peerId, userId: peer?.id) + _ = signal.start() + } + } + + override func menuItems(in location: NSPoint) -> Signal<[ContextMenuItem], NoError> { + var items:[ContextMenuItem] = [] + + let context = self.context + let peerId = self.peerId + let filter = self.filter + let isMuted = self.isMuted + let chatLocation = self.chatLocation + let associatedGroupId = self.associatedGroupId + + if let mainPeer = peer, let peerId = self.peerId, let peer = renderedPeer?.peers[peerId] { - let call = { [weak self] in - if let peerId = self?.peer?.id, let account = self?.account { - self?.requestSessionId.set((phoneCall(account, peerId: peerId) |> deliverOnMainQueue).start(next: { result in - applyUIPCallResult(account, result) - })) - } + let deleteChat:()->Void = { [weak self] in + self?.delete() } - let togglePin = {[weak self] in - if let strongSelf = self { - _ = togglePeerChatPinned(postbox: strongSelf.account.postbox, peerId: strongSelf.peerId).start() - } + + let call:()->Void = { + _ = (phoneCall(account: context.account, sharedContext: context.sharedContext, peerId: mainPeer.id) |> deliverOnMainQueue).start(next: { result in + applyUIPCallResult(context.sharedContext, result) + }) } - let toggleMute = {[weak self] in - if let strongSelf = self { - _ = togglePeerMuted(account: strongSelf.account, peerId: strongSelf.peerId).start() - } + let togglePin:()->Void = { + ChatListRowItem.togglePinned(context: context, chatLocation: chatLocation, filter: filter, associatedGroupId: associatedGroupId) } - let leaveGroup = { [weak self] in - if let strongSelf = self { - confirm(for: mainWindow, with: appName, and: tr(.confirmLeaveGroup), successHandler: { _ in - strongSelf.deleteChatDisposable.set(leftGroup(account: strongSelf.account, peerId: strongSelf.peerId).start()) - }) - } + let toggleArchive:()->Void = { + ChatListRowItem.toggleArchive(context: context, associatedGroupId: associatedGroupId, peerId: peerId) } - let rGroup = { [weak self] in - if let strongSelf = self { - _ = returnGroup(account: strongSelf.account, peerId: strongSelf.peerId).start() - } + let toggleMute:()->Void = { + ChatListRowItem.toggleMuted(context: context, peerId: peerId, isMuted: isMuted) } - items.append(ContextMenuItem(pinnedType == .none ? tr(.chatListContextPin) : tr(.chatListContextUnpin), handler: togglePin)) + let leaveGroup = { + modernConfirm(for: context.window, account: context.account, peerId: peerId, information: L10n.confirmLeaveGroup, okTitle: L10n.peerInfoConfirmLeave, successHandler: { _ in + _ = leftGroup(account: context.account, peerId: peerId).start() + }) + } + + let rGroup = { + _ = returnGroup(account: context.account, peerId: peerId).start() + } + + if !isAd && groupId == .root { + items.append(ContextMenuItem(!isPinned ? tr(L10n.chatListContextPin) : tr(L10n.chatListContextUnpin), handler: togglePin)) + } + + if groupId == .root, (canArchive || associatedGroupId != .root), filter == nil { + items.append(ContextMenuItem(associatedGroupId == .root ? L10n.chatListSwipingArchive : L10n.chatListSwipingUnarchive, handler: toggleArchive)) + } + + if context.peerId != peer.id, !isAd { + items.append(ContextMenuItem(isMuted ? tr(L10n.chatListContextUnmute) : tr(L10n.chatListContextMute), handler: toggleMute)) + } - items.append(ContextMenuItem(isMuted ? tr(.chatListContextUnmute) : tr(.chatListContextMute), handler: toggleMute)) + if mainPeer is TelegramUser { + if mainPeer.canCall && mainPeer.id != context.peerId { + items.append(ContextMenuItem(tr(L10n.chatListContextCall), handler: call)) + } + items.append(ContextMenuItem(L10n.chatListContextClearHistory, handler: { + clearHistory(context: context, peer: peer, mainPeer: mainPeer) + })) + items.append(ContextMenuItem(L10n.chatListContextDeleteChat, handler: deleteChat)) + } - if peer is TelegramUser { - if peer.canCall && peer.id != account.peerId { - items.append(ContextMenuItem(tr(.chatListContextCall), handler: call)) + if !isSecret { + if markAsUnread { + items.append(ContextMenuItem(tr(L10n.chatListContextMaskAsUnread), handler: { + _ = togglePeerUnreadMarkInteractively(postbox: context.account.postbox, viewTracker: context.account.viewTracker, peerId: peerId).start() + + })) + + } else if badgeNode != nil || mentionsCount != nil || isUnreadMarked { + items.append(ContextMenuItem(tr(L10n.chatListContextMaskAsRead), handler: { + _ = togglePeerUnreadMarkInteractively(postbox: context.account.postbox, viewTracker: context.account.viewTracker, peerId: peerId).start() + })) } - items.append(ContextMenuItem(tr(.chatListContextClearHistory), handler: clearHistory)) - items.append(ContextMenuItem(tr(.chatListContextDeleteChat), handler: deleteChat)) } + + if isAd { + items.append(ContextMenuItem(tr(L10n.chatListContextHidePromo), handler: { + context.sharedContext.bindings.mainController().chatList.hidePromoItem(peerId) + })) + } + - if let peer = peer as? TelegramGroup { - items.append(ContextMenuItem(tr(.chatListContextClearHistory), handler: clearHistory)) + if let peer = peer as? TelegramGroup, !isAd { + items.append(ContextMenuItem(tr(L10n.chatListContextClearHistory), handler: { + clearHistory(context: context, peer: peer, mainPeer: mainPeer) + })) switch peer.membership { case .Member: - items.append(ContextMenuItem(tr(.chatListContextLeaveGroup), handler: leaveGroup)) + items.append(ContextMenuItem(L10n.chatListContextLeaveGroup, handler: leaveGroup)) case .Left: - items.append(ContextMenuItem(tr(.chatListContextReturnGroup), handler: rGroup)) + items.append(ContextMenuItem(L10n.chatListContextReturnGroup, handler: rGroup)) default: break } - items.append(ContextMenuItem(tr(.chatListContextDeleteAndExit), handler: deleteChat)) - } else if let peer = peer as? TelegramChannel { + items.append(ContextMenuItem(L10n.chatListContextDeleteAndExit, handler: deleteChat)) + } else if let peer = peer as? TelegramChannel, !isAd, !peer.flags.contains(.hasGeo) { if case .broadcast = peer.info { - items.append(ContextMenuItem(tr(.chatListContextLeaveChannel), handler: deleteChat)) - } else { + items.append(ContextMenuItem(L10n.chatListContextLeaveChannel, handler: deleteChat)) + } else if !isAd { if peer.addressName == nil { - items.append(ContextMenuItem(tr(.chatListContextClearHistory), handler: clearHistory)) + items.append(ContextMenuItem(L10n.chatListContextClearHistory, handler: { + clearHistory(context: context, peer: peer, mainPeer: mainPeer) + })) } - items.append(ContextMenuItem(tr(.chatListContextLeaveGroup), handler: deleteChat)) + items.append(ContextMenuItem(L10n.chatListContextLeaveGroup, handler: deleteChat)) } } - return .single(items) + } else { + if !isAd, groupId == .root { + items.append(ContextMenuItem(!isPinned ? L10n.chatListContextPin : L10n.chatListContextUnpin, handler: { + ChatListRowItem.togglePinned(context: context, chatLocation: chatLocation, filter: filter, associatedGroupId: associatedGroupId) + })) + } + } + + if groupId != .root, context.sharedContext.layout != .minimisize, let archiveStatus = archiveStatus { + switch archiveStatus { + case .collapsed: + items.append(ContextMenuItem(L10n.chatListRevealActionExpand , handler: { + ChatListRowItem.collapseOrExpandArchive(context: context) + })) + default: + items.append(ContextMenuItem(L10n.chatListRevealActionCollapse, handler: { + ChatListRowItem.collapseOrExpandArchive(context: context) + })) + } } - return .single([]) + + return .single(items) |> mapToSignal { items in + return chatListFilterPreferences(engine: context.engine) |> deliverOnMainQueue |> take(1) |> map { filters -> [ContextMenuItem] in + + var items = items + + var submenu: [ContextMenuItem] = [] + + + + if let peerId = peerId, peerId.namespace != Namespaces.Peer.SecretChat { + for item in filters.list { + + let menuItem = ContextMenuItem(item.title, handler: { + let isEnabled = item.data.includePeers.peers.contains(peerId) || item.data.includePeers.peers.count < 100 + if isEnabled { + _ = context.engine.peers.updateChatListFiltersInteractively({ list in + var list = list + for (i, folder) in list.enumerated() { + var folder = folder + if folder.id == item.id { + if item.data.includePeers.peers.contains(peerId) { + var peers = folder.data.includePeers.peers + peers.removeAll(where: { $0 == peerId }) + folder.data.includePeers.setPeers(peers) + } else { + folder.data.includePeers.setPeers(folder.data.includePeers.peers + [peerId]) + } + list[i] = folder + + } + } + return list + }).start() + } else { + alert(for: context.window, info: L10n.chatListFilterIncludeLimitReached) + } + + }, state: item.data.includePeers.peers.contains(peerId) ? NSControl.StateValue.on : nil) + + submenu.append(menuItem) + } + } + + if !submenu.isEmpty { + items.append(ContextSeparatorItem()) + let item = ContextMenuItem(L10n.chatListFilterAddToFolder) + let menu = NSMenu() + for item in submenu { + menu.addItem(item) + } + item.submenu = menu + items.append(item) + } + + return items + } + } } var ctxDisplayLayout:(TextNodeLayout, TextNode)? { - if isSelected && account.context.layout != .single { + if isSelected && context.sharedContext.layout != .single { return displaySelectedLayout } return displayLayout } + + var ctxChatNameLayout:(TextNodeLayout, TextNode)? { + if isSelected && context.sharedContext.layout != .single { + return chatNameSelectedLayout + } + return chatNameLayout + } + var ctxMessageLayout:(TextNodeLayout, TextNode)? { - if isSelected && account.context.layout != .single { + if isSelected && context.sharedContext.layout != .single { if let typingSelectedLayout = typingSelectedLayout { return typingSelectedLayout } @@ -352,19 +1229,34 @@ class ChatListRowItem: TableRowItem { return messageLayout } var ctxDateLayout:(TextNodeLayout, TextNode)? { - if isSelected && account.context.layout != .single { + if isSelected && context.sharedContext.layout != .single { return dateSelectedLayout } return dateLayout } var ctxBadgeNode:BadgeNode? { - if isSelected && account.context.layout != .single { + if isSelected && context.sharedContext.layout != .single { return badgeSelectedNode } return badgeNode } +// var ctxBadge: Badge? { +// if isSelected && context.sharedContext.layout != .single { +// return badgeSelected +// } +// return badge +// } + + var ctxAdditionalBadgeNode:BadgeNode? { + if isSelected && context.sharedContext.layout != .single { + return additionalBadgeSelectedNode + } + return additionalBadgeNode + } + + override var instantlyResize: Bool { return true } @@ -372,7 +1264,6 @@ class ChatListRowItem: TableRowItem { deinit { clearHistoryDisposable.dispose() deleteChatDisposable.dispose() - requestSessionId.dispose() } override func viewClass() -> AnyClass { @@ -380,7 +1271,15 @@ class ChatListRowItem: TableRowItem { } override var height: CGFloat { - return 66; + if let archiveStatus = archiveStatus, context.sharedContext.layout != .minimisize { + switch archiveStatus { + case .collapsed: + return 30 + default: + return 70 + } + } + return 70 } } diff --git a/Telegram-Mac/ChatListRowView.swift b/Telegram-Mac/ChatListRowView.swift index c99b682101..12d61d3e1f 100644 --- a/Telegram-Mac/ChatListRowView.swift +++ b/Telegram-Mac/ChatListRowView.swift @@ -8,78 +8,336 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac -import TelegramCoreMac -import PostboxMac +import SwiftSignalKit +import TelegramCore -class ChatListRowView: TableRowView { +import Postbox + + + +private class ChatListDraggingContainerView : View { + fileprivate var item: ChatListRowItem? + fileprivate var activeDragging:Bool = false + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + registerForDraggedTypes([.tiff, .string, .kUrl, .kFileUrl]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + override public func performDragOperation(_ sender: NSDraggingInfo) -> Bool { + if activeDragging { + activeDragging = false + needsDisplay = true + if let tiff = sender.draggingPasteboard.data(forType: .tiff), let image = NSImage(data: tiff) { + _ = (putToTemp(image: image) |> deliverOnMainQueue).start(next: { [weak item] path in + guard let item = item, let chatLocation = item.chatLocation else {return} + item.context.sharedContext.bindings.rootNavigation().push(ChatController(context: item.context, chatLocation: chatLocation, initialAction: .files(list: [path], behavior: .automatic))) + }) + } else { + let list = sender.draggingPasteboard.propertyList(forType: .kFilenames) as? [String] + if let item = item, let list = list { + let list = list.filter { path -> Bool in + if let size = fs(path) { + return size <= 2000 * 1024 * 1024 + } + return false + } + if !list.isEmpty, let chatLocation = item.chatLocation { + item.context.sharedContext.bindings.rootNavigation().push(ChatController(context: item.context, chatLocation: chatLocation, initialAction: .files(list: list, behavior: .automatic))) + } + } + } + + + return true + } + return false + } + + override public func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { + if let item = item, let peer = item.peer, peer.canSendMessage(false), mouseInside() { + activeDragging = true + needsDisplay = true + } + superview?.draggingEntered(sender) + return .generic + + } + + + + override public func draggingExited(_ sender: NSDraggingInfo?) { + activeDragging = false + needsDisplay = true + superview?.draggingExited(sender) + } + + public override func draggingEnded(_ sender: NSDraggingInfo) { + activeDragging = false + needsDisplay = true + superview?.draggingEnded(sender) + } +} + +private final class ChatListExpandView: View { + private let titleView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + + titleView.userInteractionEnabled = false + titleView.isSelectable = false + + self.addSubview(titleView) + updateLocalizationAndTheme(theme: theme) + } + override func updateLocalizationAndTheme(theme: PresentationTheme) { + let titleLayout = TextViewLayout(.initialize(string: L10n.chatListArchivedChats, color: theme.colors.grayText, font: .medium(12)), maximumNumberOfLines: 1, alwaysStaticItems: true) + titleLayout.measure(width: .greatestFiniteMagnitude) + titleView.update(titleLayout) + needsLayout = true + } + + override func layout() { + super.layout() + titleView.center() + } + + func animateOnce() { + titleView.layer?.animateScaleSpring(from: 0.7, to: 1, duration: 0.35, removeOnCompletion: true, bounce: true, completion: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + +private final class ChatListMediaPreviewView: View { + private let context: AccountContext + private let message: Message + private let media: Media + + private let imageView: TransformImageView + + private let playIcon: ImageView = ImageView() + + private var requestedImage: Bool = false + private var disposable: Disposable? + + init(context: AccountContext, message: Message, media: Media) { + self.context = context + self.message = message + self.media = media + + self.imageView = TransformImageView() + self.playIcon.image = theme.icons.chat_list_thumb_play + self.playIcon.sizeToFit() + super.init() + + self.addSubview(self.imageView) + self.addSubview(self.playIcon) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + deinit { + self.disposable?.dispose() + } + + func updateLayout(size: CGSize) { + var dimensions = CGSize(width: 100.0, height: 100.0) + if let image = self.media as? TelegramMediaImage { + playIcon.isHidden = true + if let largest = largestImageRepresentation(image.representations) { + dimensions = largest.dimensions.size + if !self.requestedImage { + self.requestedImage = true + let signal = mediaGridMessagePhoto(account: self.context.account, imageReference: .message(message: MessageReference(self.message), media: image), scale: backingScaleFactor) + self.imageView.setSignal(signal) + } + } + } else if let file = self.media as? TelegramMediaFile { + if file.isAnimated { + self.playIcon.isHidden = true + } else { + self.playIcon.isHidden = false + } + + if let mediaDimensions = file.dimensions { + dimensions = mediaDimensions.size + if !self.requestedImage { + self.requestedImage = true + let signal = mediaGridMessageVideo(postbox: self.context.account.postbox, fileReference: .message(message: MessageReference(self.message), media: file), scale: backingScaleFactor) + self.imageView.setSignal(signal) + } + } + } + + self.imageView.frame = CGRect(origin: CGPoint(), size: size) + //self.playIcon.center() + self.imageView.set(arguments: TransformImageArguments(corners: ImageCorners(radius: 2.0), imageSize: dimensions.aspectFilled(size), boundingSize: size, intrinsicInsets: NSEdgeInsets())) + + } +} + + +private final class GroupCallActivity : View { + private let animation:GCChatListIndicator = GCChatListIndicator(color: .white) + private let backgroundView = ImageView() + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(backgroundView) + addSubview(animation) + animation.center() + isEventLess = true + animation.isEventLess = true + backgroundView.isEventLess = true + } + + + func update(context: AccountContext, tableView: TableView?, foregroundColor: NSColor, backgroundColor: NSColor, animColor: NSColor) { + self.animation.color = animColor + backgroundView.image = generateImage(frame.size, contextGenerator: { size, ctx in + let rect = NSRect(origin: .zero, size: size) + ctx.clear(rect) + ctx.setFillColor(backgroundColor.cgColor) + ctx.fillEllipse(in: rect) + + ctx.setFillColor(foregroundColor.cgColor) + ctx.fillEllipse(in: NSMakeRect(2, 2, frame.width - 4, frame.height - 4)) + }) + backgroundView.sizeToFit() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class ChatListRowView: TableRowView, ViewDisplayDelegate, RevealTableView { + + private let revealLeftView: View = View() - private var titleText:TextNode - private var messageText:TextNode + private var internalDelta: CGFloat? + + private let revealRightView: View = View() + private var titleText:TextNode = TextNode() + private var messageText:TextNode = TextNode() + + private var badgeView:View? + private var additionalBadgeView:View? + private var mentionsView: ImageView? + + + private var activeImage: ImageView? + private var groupActivityView: GroupCallActivity? private var activitiesModel:ChatActivitiesModel? - private var photo:AvatarControl = AvatarControl(font: .avatar(.custom(22))) - private var activeDragging:Bool = false + private var photo:AvatarControl = AvatarControl(font: .avatar(22)) private var hiddemMessage:Bool = false private let peerInputActivitiesDisposable:MetaDisposable = MetaDisposable() + private var removeControl:ImageButton? = nil + private var animatedView: RowAnimateView? + private var archivedPhoto: LAnimationButton? + private let containerView: ChatListDraggingContainerView = ChatListDraggingContainerView(frame: NSZeroRect) + private var expandView: ChatListExpandView? + + + private var currentTextLeftCutout: CGFloat = 0.0 + private var currentMediaPreviewSpecs: [(message: Message, media: Media, size: CGSize)] = [] + private var mediaPreviewViews: [MessageId: ChatListMediaPreviewView] = [:] + + + private var revealActionInvoked: Bool = false { + didSet { + animateOnceAfterDelta = true + } + } + var endRevealState: SwipeDirection? { + didSet { + internalDelta = nil + if let oldValue = oldValue, endRevealState == nil { + switch oldValue { + case .left, .right: + revealActionInvoked = true + completeReveal(direction: .none) + default: + break + } + } + } + } override var isFlipped: Bool { return true } - /* - let theme:ChatActivitiesTheme - if item.isSelected && item.account.context.layout != .single { - theme = ChatActivitiesWhiteTheme() - } else if item.isSelected || item.isPinned { - theme = ChatActivitiesTheme(backgroundColor: .grayUI) - } else if contextMenu != nil { - theme = ChatActivitiesTheme(backgroundColor: .grayBackground) - } else { - theme = ChatActivitiesBlueTheme() - } - - */ var inputActivities:(PeerId, [(Peer, PeerInputActivity)])? { didSet { + + for (message, _, _) in self.currentMediaPreviewSpecs { + if let previewView = self.mediaPreviewViews[message.id] { + previewView.isHidden = inputActivities != nil && !inputActivities!.1.isEmpty + } + } + if let inputActivities = inputActivities, let item = item as? ChatListRowItem { + let oldValue = oldValue?.1.map { + ChatListInputActivity($0, $1) + } if inputActivities.1.isEmpty { activitiesModel?.clean() activitiesModel?.view?.removeFromSuperview() activitiesModel = nil - self.needsLayout = true self.hiddemMessage = false - self.needsDisplay = true + containerView.needsDisplay = true } else if activitiesModel == nil { activitiesModel = ChatActivitiesModel() - addSubview(activitiesModel!.view!) + containerView.addSubview(activitiesModel!.view!) } + let activity:ActivitiesTheme - if item.isSelected && item.account.context.layout != .single { - activity = theme.activity(key: 10 + (theme.dark ? 10 : 20), foregroundColor: theme.chatList.activitySelectedColor, backgroundColor: theme.chatList.selectedBackgroundColor) + if item.isSelected && item.context.sharedContext.layout != .single { + activity = theme.activity(key: 10, foregroundColor: theme.chatList.activitySelectedColor, backgroundColor: theme.chatList.selectedBackgroundColor) } else if item.isSelected { - activity = theme.activity(key: 11 + (theme.dark ? 10 : 20), foregroundColor: theme.chatList.activityPinnedColor, backgroundColor: theme.chatList.singleLayoutSelectedBackgroundColor) - } else if item.pinnedType != .none { - activity = theme.activity(key: 12 + (theme.dark ? 10 : 20), foregroundColor: theme.chatList.activityPinnedColor, backgroundColor: theme.chatList.pinnedBackgroundColor) - } else if contextMenu != nil { - activity = theme.activity(key: 13 + (theme.dark ? 10 : 20), foregroundColor: theme.chatList.activityContextMenuColor, backgroundColor: theme.chatList.contextMenuBackgroundColor) + activity = theme.activity(key: 11, foregroundColor: theme.chatList.activityPinnedColor, backgroundColor: theme.chatList.singleLayoutSelectedBackgroundColor) + } else if self.containerView.activeDragging || item.isHighlighted { + activity = theme.activity(key: 13, foregroundColor: theme.chatList.activityColor, backgroundColor: theme.chatList.activeDraggingBackgroundColor) + } else if item.isFixedItem { + activity = theme.activity(key: 12, foregroundColor: theme.chatList.activityPinnedColor, backgroundColor: theme.chatList.pinnedBackgroundColor) } else { - activity = theme.activity(key: 14 + (theme.dark ? 10 : 20), foregroundColor: theme.chatList.activityColor, backgroundColor: theme.colors.background) + activity = theme.activity(key: 14, foregroundColor: theme.chatList.activityColor, backgroundColor: theme.colors.background) } + if oldValue != item.activities || activity != activitiesModel?.theme { + activitiesModel?.update(with: inputActivities, for: item.messageWidth, theme: activity, layout: { [weak self] show in + if let item = self?.item as? ChatListRowItem, let displayLayout = item.ctxDisplayLayout { + self?.activitiesModel?.view?.setFrameOrigin(item.leftInset, displayLayout.0.size.height + item.margin + 3) + } + self?.hiddemMessage = show + self?.containerView.needsDisplay = true + }) + } + - activitiesModel?.update(with: inputActivities, for: item.messageWidth, theme: activity, layout: { [weak self] show in - self?.needsLayout = true - self?.hiddemMessage = show - self?.needsDisplay = true - }) - - activitiesModel?.view?.isHidden = item.account.context.layout == .minimisize + activitiesModel?.view?.isHidden = item.context.sharedContext.layout == .minimisize } else { activitiesModel?.clean() activitiesModel?.view?.removeFromSuperview() activitiesModel = nil + hiddemMessage = false } } } @@ -96,15 +354,57 @@ class ChatListRowView: TableRowView { self.inputActivities = inputActivities } + + override func focusAnimation(_ innerId: AnyHashable?) { + + if animatedView == nil { + self.animatedView = RowAnimateView(frame:bounds) + self.animatedView?.isEventLess = true + containerView.addSubview(animatedView!) + animatedView?.backgroundColor = theme.colors.focusAnimationColor + animatedView?.layer?.opacity = 0 + + } + animatedView?.stableId = item?.stableId + + + let animation: CABasicAnimation = makeSpringAnimation("opacity") + + animation.fromValue = animatedView?.layer?.presentation()?.opacity ?? 0 + animation.toValue = 0.5 + animation.autoreverses = true + animation.isRemovedOnCompletion = true + animation.fillMode = CAMediaTimingFillMode.forwards + + animation.delegate = CALayerAnimationDelegate(completion: { [weak self] completed in + if completed { + self?.animatedView?.removeFromSuperview() + self?.animatedView = nil + } + }) + animation.isAdditive = false + + animatedView?.layer?.add(animation, forKey: "opacity") + + } + + + override var backdorColor: NSColor { if let item = item as? ChatListRowItem { - if item.account.context.layout == .single, item.isSelected { + if item.isCollapsed { + return theme.colors.grayBackground + } + if item.isHighlighted && !item.isSelected { + return theme.chatList.activeDraggingBackgroundColor + } + if item.context.sharedContext.layout == .single, item.isSelected { return theme.chatList.singleLayoutSelectedBackgroundColor } - if !item.isSelected && activeDragging { + if !item.isSelected && containerView.activeDragging { return theme.chatList.activeDraggingBackgroundColor } - if item.pinnedType != .none && !item.isSelected { + if item.isFixedItem && !item.isSelected { return theme.chatList.pinnedBackgroundColor } return item.isSelected ? theme.chatList.selectedBackgroundColor : contextMenu != nil ? theme.chatList.contextMenuBackgroundColor : theme.colors.background @@ -114,92 +414,107 @@ class ChatListRowView: TableRowView { override func draw(_ layer: CALayer, in ctx: CGContext) { - - ctx.setFillColor(theme.colors.background.cgColor) - ctx.fill(bounds) + super.draw(layer, in: ctx) + // if let item = self.item as? ChatListRowItem { - - if(!item.isSelected) { - ctx.setFillColor(theme.colors.border.cgColor) - ctx.fill(NSMakeRect(item.pinnedType == .last ? 0 : item.leftInset, NSHeight(layer.bounds) - .borderSize, item.pinnedType == .last ? layer.frame.width : layer.bounds.width - item.leftInset, .borderSize)) + if !item.isSelected { - ctx.setFillColor(theme.colors.border.cgColor) - ctx.fill(NSMakeRect(layer.bounds.width - .borderSize, 0, .borderSize, NSHeight(self.frame))) - } - - if let context = item.account.applicationContext as? TelegramApplicationContext { - if context.layout == .minimisize { - return + if layer != containerView.layer { + ctx.setFillColor(theme.colors.border.cgColor) + ctx.fill(NSMakeRect(frame.width - .borderSize, 0, .borderSize, frame.height)) + } else { + + if item.context.sharedContext.layout == .minimisize { + return + } + + if backingScaleFactor == 1.0 { + ctx.setFillColor(backdorColor.cgColor) + ctx.fill(layer.bounds) + } + + ctx.setFillColor(theme.colors.border.cgColor) + ctx.fill(NSMakeRect(item.isLastPinned ? 0 : item.leftInset, NSHeight(layer.bounds) - .borderSize, item.isLastPinned ? layer.frame.width : layer.bounds.width - item.leftInset, .borderSize)) } } - let highlighted = item.isSelected && item.account.context.layout != .single - - - if item.ctxBadgeNode == nil && (item.pinnedType == .some || item.pinnedType == .last) { - ctx.draw(highlighted ? theme.icons.pinnedImageSelected : theme.icons.pinnedImage, in: NSMakeRect(frame.width - theme.icons.pinnedImage.backingSize.width - item.margin, frame.height - theme.icons.pinnedImage.backingSize.height - item.margin + 1, theme.icons.pinnedImage.backingSize.width, theme.icons.pinnedImage.backingSize.height)) + if item.context.sharedContext.layout == .minimisize { + return } - if let displayLayout = item.ctxDisplayLayout { - - var addition:CGFloat = 0 - if item.isSecret { - ctx.draw(item.isSelected ? theme.icons.secretImageSelected : theme.icons.secretImage, in: NSMakeRect(item.leftInset, item.margin + 3, theme.icons.secretImage.backingSize.width, theme.icons.secretImage.backingSize.height)) - addition += theme.icons.secretImage.backingSize.height - - } - displayLayout.1.draw(NSMakeRect(item.leftInset + addition, item.margin - 1, displayLayout.0.size.width, displayLayout.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor) - - - var mutedInset:CGFloat = item.isSecret ? theme.icons.secretImage.backingSize.width + 2 : 0 - - if item.isVerified { - ctx.draw(highlighted ? theme.icons.verifiedImageSelected : theme.icons.verifiedImage, in: NSMakeRect(displayLayout.0.size.width + item.leftInset + addition + 2, item.margin + 1, theme.icons.verifiedImage.backingSize.width, theme.icons.verifiedImage.backingSize.height)) - mutedInset += theme.icons.verifiedImage.backingSize.width + 3 - } + if layer == containerView.layer { - if let messageLayout = item.ctxMessageLayout, !hiddemMessage { - messageLayout.1.draw(NSMakeRect(item.leftInset, displayLayout.0.size.height + item.margin + 1 , messageLayout.0.size.width, messageLayout.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor) - } + let highlighted = item.isSelected && item.context.sharedContext.layout != .single - if item.isMuted { - ctx.draw(highlighted ? theme.icons.dialogMuteImageSelected : theme.icons.dialogMuteImage, in: NSMakeRect(item.leftInset + displayLayout.0.size.width + 4 + mutedInset, item.margin + round((displayLayout.0.size.height - theme.icons.dialogMuteImage.backingSize.height) / 2.0) - 1, theme.icons.dialogMuteImage.backingSize.width, theme.icons.dialogMuteImage.backingSize.height)) - } - if let _ = item.mentionsCount { - ctx.draw(highlighted ? theme.icons.chatListMentionActive : theme.icons.chatListMention, in: NSMakeRect(frame.width - (item.ctxBadgeNode != nil ? item.ctxBadgeNode!.size.width + item.margin : 0) - theme.icons.chatListMentionActive.backingSize.width - item.margin, frame.height - theme.icons.chatListMention.backingSize.height - item.margin + 1, theme.icons.chatListMention.backingSize.width, theme.icons.chatListMention.backingSize.height)) + if item.ctxBadgeNode == nil && item.mentionsCount == nil && (item.isPinned || item.isLastPinned) { + ctx.draw(highlighted ? theme.icons.pinnedImageSelected : theme.icons.pinnedImage, in: NSMakeRect(frame.width - theme.icons.pinnedImage.backingSize.width - item.margin - 1, frame.height - theme.icons.pinnedImage.backingSize.height - (item.margin + 1), theme.icons.pinnedImage.backingSize.width, theme.icons.pinnedImage.backingSize.height)) } - if let dateLayout = item.ctxDateLayout, !item.hasDraft { - let dateX = frame.width - dateLayout.0.size.width - item.margin - dateLayout.1.draw(NSMakeRect(dateX, item.margin, dateLayout.0.size.width, dateLayout.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor) + if let displayLayout = item.ctxDisplayLayout { + + var addition:CGFloat = 0 + if item.isSecret { + ctx.draw(highlighted ? theme.icons.secretImageSelected : theme.icons.secretImage, in: NSMakeRect(item.leftInset, item.margin + 3, theme.icons.secretImage.backingSize.width, theme.icons.secretImage.backingSize.height)) + addition += theme.icons.secretImage.backingSize.height + + } + displayLayout.1.draw(NSMakeRect(item.leftInset + addition, item.margin - 1, displayLayout.0.size.width, displayLayout.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) + + + var mutedInset:CGFloat = item.isSecret ? theme.icons.secretImage.backingSize.width + 2 : 0 + + if item.isVerified { + ctx.draw(highlighted ? theme.icons.verifyDialogActive : theme.icons.verifyDialog, in: NSMakeRect(displayLayout.0.size.width + item.leftInset + addition - 2, item.margin - 3, 24, 24)) + mutedInset += 15 + 3 + } - if item.isOutMessage { + if item.isScam || item.isFake { + ctx.draw(highlighted ? item.badHighlightIcon : item.badIcon, in: NSMakeRect(displayLayout.0.size.width + item.leftInset + addition + 2, item.margin + 1, theme.icons.scam.backingSize.width, theme.icons.scam.backingSize.height)) + mutedInset += item.badIcon.backingSize.width + 3 + } + var messageOffset: CGFloat = 0 + if let chatNameLayout = item.ctxChatNameLayout, !hiddemMessage { + chatNameLayout.1.draw(NSMakeRect(item.leftInset, displayLayout.0.size.height + item.margin + 2, chatNameLayout.0.size.width, chatNameLayout.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) + messageOffset += chatNameLayout.0.size.height + 2 + } + if let messageLayout = item.ctxMessageLayout, !hiddemMessage { + messageLayout.1.draw(NSMakeRect(item.leftInset, displayLayout.0.size.height + item.margin + 1 + messageOffset, messageLayout.0.size.width, messageLayout.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) + } + + if item.isMuted { + ctx.draw(highlighted ? theme.icons.dialogMuteImageSelected : theme.icons.dialogMuteImage, in: NSMakeRect(item.leftInset + displayLayout.0.size.width + 4 + mutedInset, item.margin + round((displayLayout.0.size.height - theme.icons.dialogMuteImage.backingSize.height) / 2.0) - 1, theme.icons.dialogMuteImage.backingSize.width, theme.icons.dialogMuteImage.backingSize.height)) + } + + + + if let dateLayout = item.ctxDateLayout, !item.hasDraft { + let dateX = frame.width - dateLayout.0.size.width - item.margin + dateLayout.1.draw(NSMakeRect(dateX, item.margin, dateLayout.0.size.width, dateLayout.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) + if !item.isFailed { if item.isSending { let outX = dateX - theme.icons.sendingImage.backingSize.width - 4 ctx.draw(highlighted ? theme.icons.sendingImageSelected : theme.icons.sendingImage, in: NSMakeRect(outX,item.margin + 2, theme.icons.sendingImage.backingSize.width, theme.icons.sendingImage.backingSize.height)) } else { - let outX = dateX - theme.icons.outgoingMessageImage.backingSize.width - (item.isRead ? 4.0 : 0.0) - 2 - ctx.draw(highlighted ? theme.icons.outgoingMessageImageSelected : theme.icons.outgoingMessageImage, in: NSMakeRect(outX, item.margin + 2, theme.icons.outgoingMessageImage.backingSize.width, theme.icons.outgoingMessageImage.backingSize.height)) - if item.isRead { - ctx.draw(highlighted ? theme.icons.readMessageImageSelected : theme.icons.readMessageImage, in: NSMakeRect(outX + 4, item.margin + 2, theme.icons.readMessageImage.backingSize.width, theme.icons.readMessageImage.backingSize.height)) + if item.isOutMessage { + let outX = dateX - theme.icons.outgoingMessageImage.backingSize.width - (item.isRead ? 4.0 : 0.0) - 2 + ctx.draw(highlighted ? theme.icons.outgoingMessageImageSelected : theme.icons.outgoingMessageImage, in: NSMakeRect(outX, item.margin + 2, theme.icons.outgoingMessageImage.backingSize.width, theme.icons.outgoingMessageImage.backingSize.height)) + if item.isRead { + ctx.draw(highlighted ? theme.icons.readMessageImageSelected : theme.icons.readMessageImage, in: NSMakeRect(outX + 4, item.margin + 2, theme.icons.readMessageImage.backingSize.width, theme.icons.readMessageImage.backingSize.height)) + } } - } } else { let outX = dateX - theme.icons.errorImageSelected.backingSize.width - 4 ctx.draw(highlighted ? theme.icons.errorImageSelected : theme.icons.errorImage, in: NSMakeRect(outX,item.margin, theme.icons.errorImage.backingSize.width, theme.icons.errorImage.backingSize.height)) - } - + } - } } - } } @@ -208,14 +523,23 @@ class ChatListRowView: TableRowView { required init(frame frameRect: NSRect) { - titleText = TextNode(); - messageText = TextNode(); + super.init(frame: frameRect) + + + addSubview(revealRightView) + addSubview(revealLeftView) + self.layerContentsRedrawPolicy = .onSetNeedsDisplay photo.userInteractionEnabled = false - photo.frame = NSMakeRect(10, 8, 50, 50) - addSubview(photo) - self.registerForDraggedTypes([.tiff, .string, .kUrl, .kFilenames]) - + photo.frame = NSMakeRect(10, 10, 50, 50) + containerView.addSubview(photo) + addSubview(containerView) + + containerView.displayDelegate = self + containerView.frame = bounds + + + } required init?(coder: NSCoder) { @@ -223,118 +547,978 @@ class ChatListRowView: TableRowView { } - override public func performDragOperation(_ sender: NSDraggingInfo) -> Bool { - if activeDragging { - activeDragging = false - needsDisplay = true - let list = sender.draggingPasteboard().propertyList(forType: .kFilenames) as? [String] - if let item = item as? ChatListRowItem, let context = item.account.applicationContext as? TelegramApplicationContext, let list = list { - let list = list.filter { path -> Bool in - if let size = fileSize(path) { - return size <= 1500000000 - } - - return false - } - if !list.isEmpty { - context.mainNavigation?.push(ChatController(account: item.account, peerId: item.peerId, initialAction: .files(list: list, behavior: .automatic))) - } - } - return true - } - return false + override func mouseDown(with event: NSEvent) { + super.mouseDown(with: event) } override public func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { - if let item = item as? ChatListRowItem, let peer = item.peer, peer.canSendMessage { - activeDragging = true - needsDisplay = true - } + needsDisplay = true + updateColors() return .generic - + } override public func draggingExited(_ sender: NSDraggingInfo?) { - activeDragging = false needsDisplay = true + updateColors() } - public override func draggingEnded(_ sender: NSDraggingInfo?) { - activeDragging = false + public override func draggingEnded(_ sender: NSDraggingInfo) { needsDisplay = true + updateColors() } + override func updateColors() { + super.updateColors() + let inputActivities = self.inputActivities + self.inputActivities = inputActivities + self.containerView.background = backdorColor + expandView?.backgroundColor = theme.colors.grayBackground + } override func set(item:TableRowItem, animated:Bool = false) { + + if let item = item as? ChatListRowItem { + if item.isCollapsed { + if expandView == nil { + expandView = ChatListExpandView(frame: NSMakeRect(0, frame.height, frame.width, item.height)) + } + self.addSubview(expandView!, positioned: .below, relativeTo: containerView) + expandView?.updateLocalizationAndTheme(theme: theme) + } else { + if let expandView = expandView { + expandView.removeFromSuperview() + } + } + } + let wasHidden: Bool = (self.item as? ChatListRowItem)?.isCollapsed ?? false super.set(item:item, animated:animated) + - if let item = self.item as? ChatListRowItem { + if let item = item as? ChatListRowItem { - if let peer = item.peer { - photo.setPeer(account: item.account, peer: peer) - } + self.currentMediaPreviewSpecs = item.contentImageSpecs - if let badgeNode = item.ctxBadgeNode { - if badgeView == nil { - badgeView = View() - addSubview(badgeView!) + var validMediaIds: [MessageId] = [] + for (message, media, mediaSize) in item.contentImageSpecs { + guard item.context.sharedContext.layout != .minimisize else { + continue } - badgeView?.setFrameSize(badgeNode.size) - badgeNode.view = badgeView - badgeNode.setNeedDisplay() - } else { - badgeView?.removeFromSuperview() - badgeView = nil - } - - if !(item is ChatListMessageRowItem) { - let postbox = item.account.postbox - let peerId = item.peerId - let previousPeerCache = Atomic<[PeerId: Peer]>(value: [:]) - self.peerInputActivitiesDisposable.set((item.account.peerInputActivities(peerId: peerId) - |> mapToSignal { activities -> Signal<[(Peer, PeerInputActivity)], NoError> in - var foundAllPeers = true - var cachedResult: [(Peer, PeerInputActivity)] = [] - previousPeerCache.with { dict -> Void in - for (peerId, activity) in activities { - if let peer = dict[peerId] { - cachedResult.append((peer, activity)) - } else { - foundAllPeers = false - break - } - } - } - if foundAllPeers { - return .single(cachedResult) - } else { - return postbox.modify { modifier -> [(Peer, PeerInputActivity)] in - var result: [(Peer, PeerInputActivity)] = [] - var peerCache: [PeerId: Peer] = [:] - for (peerId, activity) in activities { - if let peer = modifier.getPeer(peerId) { - result.append((peer, activity)) - peerCache[peerId] = peer - } - } - _ = previousPeerCache.swap(peerCache) - return result - } - } + validMediaIds.append(message.id) + let previewView: ChatListMediaPreviewView + if let current = self.mediaPreviewViews[message.id] { + previewView = current + } else { + previewView = ChatListMediaPreviewView(context: item.context, message: message, media: media) + self.mediaPreviewViews[message.id] = previewView + self.containerView.addSubview(previewView) + } + previewView.updateLayout(size: mediaSize) + } + var removeMessageIds: [MessageId] = [] + for (messageId, itemView) in self.mediaPreviewViews { + if !validMediaIds.contains(messageId) { + removeMessageIds.append(messageId) + itemView.removeFromSuperview() + } + } + for messageId in removeMessageIds { + self.mediaPreviewViews.removeValue(forKey: messageId) + } + + if item.isCollapsed != wasHidden { + expandView?.change(pos: NSMakePoint(0, item.isCollapsed ? 0 : item.height), animated: animated) + containerView.change(pos: NSMakePoint(0, item.isCollapsed ? -70 : 0), animated: !revealActionInvoked && animated) + } + + if let isOnline = item.isOnline, item.context.sharedContext.layout != .minimisize { + if isOnline { + var animate: Bool = false + if activeImage == nil { + activeImage = ImageView() + self.containerView.addSubview(activeImage!) + animate = true + } + guard let activeImage = self.activeImage else { return } + activeImage.image = item.isSelected && item.context.sharedContext.layout != .single ? theme.icons.hintPeerActiveSelected : theme.icons.hintPeerActive + activeImage.sizeToFit() + + activeImage.setFrameOrigin(photo.frame.maxX - activeImage.frame.width - 3, photo.frame.maxY - 12) + + if animated && animate { + activeImage.layer?.animateAlpha(from: 0.5, to: 1.0, duration: 0.2) + activeImage.layer?.animateScaleSpring(from: 0.1, to: 1.0, duration: 0.3) + } + } else { + if animated { + let activeImage = self.activeImage + self.activeImage = nil + activeImage?.layer?.animateAlpha(from: 1, to: 0.5, duration: 0.2) + activeImage?.layer?.animateScaleSpring(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak activeImage] completed in + activeImage?.removeFromSuperview() + }) + } else { + activeImage?.removeFromSuperview() + activeImage = nil + } + } + } else { + activeImage?.removeFromSuperview() + activeImage = nil + } + + if item.hasActiveGroupCall, item.context.sharedContext.layout != .minimisize { + var animate: Bool = false + + if self.groupActivityView == nil { + self.groupActivityView = GroupCallActivity(frame: .init(origin: .zero, size: NSMakeSize(20, 20))) + self.containerView.addSubview(self.groupActivityView!) + animate = true + } + + let groupActivityView = self.groupActivityView! + + groupActivityView.setFrameOrigin(photo.frame.maxX - groupActivityView.frame.width + 3, photo.frame.maxY - 18) + + let isActive = item.context.sharedContext.layout != .single && item.isSelected + + groupActivityView.update(context: item.context, tableView: item.table, foregroundColor: isActive ? theme.colors.underSelectedColor : theme.colors.accentSelect, backgroundColor: backdorColor, animColor: isActive ? theme.colors.accentSelect : theme.colors.underSelectedColor) + if animated && animate { + groupActivityView.layer?.animateAlpha(from: 0.5, to: 1.0, duration: 0.2) + groupActivityView.layer?.animateScaleSpring(from: 0.1, to: 1.0, duration: 0.3) + } + } else { + if animated { + if let groupActivityView = self.groupActivityView { + self.groupActivityView = nil + groupActivityView.layer?.animateAlpha(from: 1, to: 0.5, duration: 0.2) + groupActivityView.layer?.animateScaleSpring(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak groupActivityView] completed in + groupActivityView?.removeFromSuperview() + }) + } + } else { + groupActivityView?.removeFromSuperview() + groupActivityView = nil + } + } + + + containerView.item = item + if self.animatedView != nil && self.animatedView?.stableId != item.stableId { + self.animatedView?.removeFromSuperview() + self.animatedView = nil + } + + + photo.setState(account: item.context.account, state: item.photo) + + if item.isSavedMessage { + self.archivedPhoto?.removeFromSuperview() + self.archivedPhoto = nil + let icon = theme.icons.searchSaved + photo.setState(account: item.context.account, state: .Empty) + photo.setSignal(generateEmptyPhoto(photo.frame.size, type: .icon(colors: theme.colors.peerColors(5), icon: icon, iconSize: icon.backingSize.aspectFitted(NSMakeSize(photo.frame.size.width - 20, photo.frame.size.height - 20)), cornerRadius: nil)) |> map {($0, false)}) + } else if item.isRepliesChat { + self.archivedPhoto?.removeFromSuperview() + self.archivedPhoto = nil + let icon = theme.icons.chat_replies_avatar + photo.setState(account: item.context.account, state: .Empty) + photo.setSignal(generateEmptyPhoto(photo.frame.size, type: .icon(colors: theme.colors.peerColors(5), icon: icon, iconSize: icon.backingSize.aspectFitted(NSMakeSize(photo.frame.size.width - 22, photo.frame.size.height - 22)), cornerRadius: nil)) |> map {($0, false)}) + } else if case .ArchivedChats = item.photo { + if self.archivedPhoto == nil { + self.archivedPhoto = LAnimationButton(animation: "archiveAvatar", size: NSMakeSize(46, 46), offset: NSMakeSize(0, 0)) + containerView.addSubview(self.archivedPhoto!, positioned: .above, relativeTo: self.photo) + } + self.archivedPhoto?.frame = self.photo.frame + self.archivedPhoto?.userInteractionEnabled = false + self.archivedPhoto?.set(keysToColor: ["box2.box2.Fill 1"], color: item.archiveStatus?.isHidden == false ? theme.colors.revealAction_accent_background : theme.colors.grayForeground) + self.archivedPhoto?.background = item.archiveStatus?.isHidden == false ? theme.colors.revealAction_accent_background : theme.colors.grayForeground + self.archivedPhoto?.layer?.cornerRadius = photo.frame.height / 2 + + let animateArchive = item.animateArchive && animated + if animateArchive { + archivedPhoto?.loop() + if item.isCollapsed { + self.expandView?.animateOnce() + } + } + + // let icon = theme.icons.archivedChats + photo.setState(account: item.context.account, state: .Empty) + // photo.setSignal(generateEmptyPhoto(photo.frame.size, type: .icon(colors: (theme.colors.grayForeground, theme.colors.grayForeground), icon: icon, iconSize: icon.backingSize.aspectFitted(NSMakeSize(photo.frame.size.width - 17, photo.frame.size.height - 17)), cornerRadius: nil)) |> map {($0, false)}) + } else { + self.archivedPhoto?.removeFromSuperview() + self.archivedPhoto = nil + } + +// if let badge = item.ctxBadge { +// var presented: Bool = false +// if self.badge == nil { +// self.badge = AnimatedBadgeView() +// containerView.addSubview(self.badge!) +// presented = true +// } +// +// let origin = NSMakePoint(self.containerView.frame.width - badge.size.width - item.margin, self.containerView.frame.height - badge.size.height - (item.margin + 1)) +// +// self.badge?.update(dynamicValue: badge.dynamicValue, backgroundColor: badge.backgroundColor, animated: animated, frame: CGRect(origin: origin, size: badge.size)) +// +// if presented && animated { +// self.badge?.layer?.animateScaleSpring(from: 0.1, to: 1, duration: 0.3) +// } +// } else { +// if animated { +// if let badge = self.badge { +// self.badge = nil +// badge.layer?.animateScaleSpring(from: 1, to: 0.1, duration: 0.3, removeOnCompletion: false) +// badge.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak badge] _ in +// badge?.removeFromSuperview() +// }) +// } +// } else { +// self.badge?.removeFromSuperview() +// self.badge = nil +// } +// } + + var additionBadgeOffset: CGFloat = 0 + + if let badgeNode = item.ctxAdditionalBadgeNode { + var presented: Bool = false + if additionalBadgeView == nil { + additionalBadgeView = View() + containerView.addSubview(additionalBadgeView!) + presented = true + } + additionalBadgeView?.setFrameSize(badgeNode.size) + badgeNode.view = additionalBadgeView + badgeNode.setNeedDisplay() + + let point = NSMakePoint(self.containerView.frame.width - badgeNode.size.width - item.margin, self.containerView.frame.height - badgeNode.size.height - (item.margin + 1)) + additionBadgeOffset += (badgeNode.size.width + item.margin) + + if presented { + self.additionalBadgeView?.setFrameOrigin(point) + if animated { + self.additionalBadgeView?.layer?.animateScaleSpring(from: 0.1, to: 1, duration: 0.4) + self.additionalBadgeView?.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } else { + self.additionalBadgeView?.change(pos: point, animated: animated) + } + } else { + if animated { + if let badge = self.additionalBadgeView { + self.additionalBadgeView = nil + badge.layer?.animateScaleSpring(from: 1, to: 0.1, duration: 0.3, removeOnCompletion: false) + badge.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak badge] _ in + badge?.removeFromSuperview() + }) + } + } else { + self.additionalBadgeView?.removeFromSuperview() + self.additionalBadgeView = nil + } + } + + if let badgeNode = item.ctxBadgeNode { + var presented: Bool = false + if badgeView == nil { + badgeView = View() + containerView.addSubview(badgeView!) + presented = true + } + badgeView?.setFrameSize(badgeNode.size) + badgeNode.view = badgeView + badgeNode.setNeedDisplay() + + let point = NSMakePoint(self.containerView.frame.width - badgeNode.size.width - item.margin - additionBadgeOffset, self.containerView.frame.height - badgeNode.size.height - (item.margin + 1)) + + if presented { + self.badgeView?.setFrameOrigin(point) + if animated { + self.badgeView?.layer?.animateScaleSpring(from: 0.1, to: 1, duration: 0.4) + self.badgeView?.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) } - |> deliverOnMainQueue).start(next: { [weak self] activities in - self?.inputActivities = (peerId, activities) - })) + } else { + self.badgeView?.change(pos: point, animated: false) + } - let inputActivities = self.inputActivities - self.inputActivities = inputActivities + } else { + if animated { + if let badge = self.badgeView { + self.badgeView = nil + badge.layer?.animateScaleSpring(from: 1, to: 0.1, duration: 0.3, removeOnCompletion: false) + badge.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak badge] _ in + badge?.removeFromSuperview() + }) + } + } else { + self.badgeView?.removeFromSuperview() + self.badgeView = nil + } } + if let _ = item.mentionsCount { + + let highlighted = item.isSelected && item.context.sharedContext.layout != .single + let icon: CGImage + if item.associatedGroupId == .root { + icon = highlighted ? theme.icons.chatListMentionActive : theme.icons.chatListMention + } else { + icon = highlighted ? theme.icons.chatListMentionArchivedActive : theme.icons.chatListMentionArchived + } + + var presented: Bool = false + if self.mentionsView == nil { + self.mentionsView = ImageView() + self.containerView.addSubview(self.mentionsView!) + presented = true + } + + self.mentionsView?.image = icon + self.mentionsView?.sizeToFit() + + let point = NSMakePoint(self.containerView.frame.width - (item.ctxBadgeNode != nil ? item.ctxBadgeNode!.size.width + item.margin : 0) - icon.backingSize.width - item.margin, self.containerView.frame.height - icon.backingSize.height - (item.margin + 1)) + + if presented { + self.mentionsView?.setFrameOrigin(point) + if animated { + self.mentionsView?.layer?.animateScaleSpring(from: 0.1, to: 1, duration: 0.4) + self.mentionsView?.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } else { + self.mentionsView?.change(pos: point, animated: animated) + } + } else { + if let mentionsView = self.mentionsView { + self.mentionsView = nil + if animated { + mentionsView.layer?.animateScaleSpring(from: 1, to: 0.1, duration: 0.3, removeOnCompletion: false) + mentionsView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak mentionsView] _ in + mentionsView?.removeFromSuperview() + }) + } else { + mentionsView.removeFromSuperview() + } + } + } + if let peerId = item.peerId { + let activities = item.activities.map { + ($0.peer.peer, $0.activity) + } + self.inputActivities = (peerId, activities) + } else { + self.inputActivities = nil + } } + if let _ = endRevealState { + initRevealState() + } + containerView.needsDisplay = true + + containerView.customHandler.layout = { [weak self] _ in + guard let `self` = self else { return } + + if let item = self.item as? ChatListRowItem, let displayLayout = item.ctxDisplayLayout { + self.activitiesModel?.view?.setFrameOrigin(item.leftInset, displayLayout.0.size.height + item.margin + 3) + + var additionalOffset: CGFloat = 0 + + if let badgeNode = item.ctxAdditionalBadgeNode { + self.additionalBadgeView?.setFrameOrigin(self.containerView.frame.width - badgeNode.size.width - item.margin, self.containerView.frame.height - badgeNode.size.height - (item.margin + 1)) + additionalOffset += (badgeNode.size.width + item.margin) + } + + if let badgeNode = item.ctxBadgeNode { + self.badgeView?.setFrameOrigin(self.containerView.frame.width - badgeNode.size.width - item.margin - additionalOffset, self.containerView.frame.height - badgeNode.size.height - (item.margin + 1)) + } + + if let mentionsView = self.mentionsView { + let point = NSMakePoint(self.containerView.frame.width - (item.ctxBadgeNode != nil ? item.ctxBadgeNode!.size.width + item.margin : 0) - mentionsView.frame.width - item.margin, self.containerView.frame.height - mentionsView.frame.height - (item.margin + 1)) + mentionsView.setFrameOrigin(point) + } + + if let activeImage = self.activeImage { + activeImage.setFrameOrigin(self.photo.frame.maxX - activeImage.frame.width - 3, self.photo.frame.maxY - 12) + } + if let groupActivityView = self.groupActivityView { + groupActivityView.setFrameOrigin(self.photo.frame.maxX - groupActivityView.frame.width + 3, self.photo.frame.maxY - 18) + } + } + } + + containerView.needsLayout = true + revealActionInvoked = false needsDisplay = true + needsLayout = true + } + + func initRevealState() { + guard let item = item as? ChatListRowItem, endRevealState == nil else {return} + + revealLeftView.removeAllSubviews() + revealRightView.removeAllSubviews() + + revealLeftView.backgroundColor = backdorColor + revealRightView.backgroundColor = backdorColor + + if item.groupId == .root { + + let unreadBackground = !item.markAsUnread ? theme.colors.revealAction_inactive_background : theme.colors.revealAction_accent_background + let unreadForeground = !item.markAsUnread ? theme.colors.revealAction_inactive_foreground : theme.colors.revealAction_accent_foreground + + let unread: LAnimationButton = LAnimationButton(animation: !item.markAsUnread ? "anim_read" : "anim_unread", size: NSMakeSize(frame.height, frame.height), keysToColor: !item.markAsUnread ? nil : ["Oval.Oval.Stroke 1"], color: unreadBackground, offset: NSMakeSize(0, 0), autoplaySide: .right) + let unreadTitle = TextViewLabel() + unreadTitle.attributedString = .initialize(string: !item.markAsUnread ? L10n.chatListSwipingRead : L10n.chatListSwipingUnread, color: unreadForeground, font: .medium(12)) + unreadTitle.sizeToFit() + unread.addSubview(unreadTitle) + unread.set(background: unreadBackground, for: .Normal) + unread.customHandler.layout = { [weak unreadTitle] view in + if let unreadTitle = unreadTitle { + unreadTitle.centerX(y: view.frame.height - unreadTitle.frame.height - 10) + } + } + + let mute: LAnimationButton = LAnimationButton(animation: item.isMuted ? "anim_unmute" : "anim_mute", size: NSMakeSize(frame.height, frame.height), keysToColor: item.isMuted ? nil : ["un Outlines.Group 1.Stroke 1"], color: theme.colors.revealAction_neutral2_background, offset: NSMakeSize(0, 0), autoplaySide: .right) + let muteTitle = TextViewLabel() + muteTitle.attributedString = .initialize(string: item.isMuted ? L10n.chatListSwipingUnmute : L10n.chatListSwipingMute, color: theme.colors.revealAction_neutral2_foreground, font: .medium(12)) + muteTitle.sizeToFit() + mute.addSubview(muteTitle) + mute.set(background: theme.colors.revealAction_neutral2_background, for: .Normal) + mute.customHandler.layout = { [weak muteTitle] view in + if let muteTitle = muteTitle { + muteTitle.centerX(y: view.frame.height - muteTitle.frame.height - 10) + } + } + + + let pin: LAnimationButton = LAnimationButton(animation: !item.isPinned ? "anim_pin" : "anim_unpin", size: NSMakeSize(frame.height, frame.height), keysToColor: !item.isPinned ? nil : ["un Outlines.Group 1.Stroke 1"], color: theme.colors.revealAction_constructive_background, offset: NSMakeSize(0, 0), autoplaySide: .left) + let pinTitle = TextViewLabel() + pinTitle.attributedString = .initialize(string: !item.isPinned ? L10n.chatListSwipingPin : L10n.chatListSwipingUnpin, color: theme.colors.revealAction_constructive_foreground, font: .medium(12)) + pinTitle.sizeToFit() + pin.addSubview(pinTitle) + pin.set(background: theme.colors.revealAction_constructive_background, for: .Normal) + pin.customHandler.layout = { [weak pinTitle] view in + if let pinTitle = pinTitle { + pinTitle.centerX(y: view.frame.height - pinTitle.frame.height - 10) + } + } + + pin.set(handler: { [weak self] _ in + guard let item = self?.item as? ChatListRowItem else {return} + item.togglePinned() + self?.endRevealState = nil + }, for: .Click) + unread.set(handler: { [weak self] _ in + guard let item = self?.item as? ChatListRowItem else {return} + item.toggleUnread() + self?.endRevealState = nil + }, for: .Click) + + + + + + + let archive: LAnimationButton = LAnimationButton(animation: item.associatedGroupId != .root ? "anim_unarchive" : "anim_archive", size: item.associatedGroupId != .root ? NSMakeSize(45, 45) : NSMakeSize(frame.height, frame.height), keysToColor: ["box2.box2.Fill 1"], color: theme.colors.revealAction_inactive_background, offset: NSMakeSize(0, item.associatedGroupId != .root ? 9.0 : 0.0), autoplaySide: .left) + let archiveTitle = TextViewLabel() + archiveTitle.attributedString = .initialize(string: item.associatedGroupId != .root ? L10n.chatListSwipingUnarchive : L10n.chatListSwipingArchive, color: theme.colors.revealAction_inactive_foreground, font: .medium(12)) + archiveTitle.sizeToFit() + archive.addSubview(archiveTitle) + archive.set(background: theme.colors.revealAction_inactive_background, for: .Normal) + archive.customHandler.layout = { [weak archiveTitle] view in + if let archiveTitle = archiveTitle { + archiveTitle.centerX(y: view.frame.height - archiveTitle.frame.height - 10) + } + } + + + + + let delete: LAnimationButton = LAnimationButton(animation: "anim_delete", size: NSMakeSize(frame.height, frame.height), keysToColor: nil, offset: NSMakeSize(0, 0), autoplaySide: .left) + let deleteTitle = TextViewLabel() + deleteTitle.attributedString = .initialize(string: L10n.chatListSwipingDelete, color: theme.colors.revealAction_destructive_foreground, font: .medium(12)) + deleteTitle.sizeToFit() + delete.addSubview(deleteTitle) + delete.set(background: theme.colors.revealAction_destructive_background, for: .Normal) + delete.customHandler.layout = { [weak deleteTitle] view in + if let deleteTitle = deleteTitle { + deleteTitle.centerX(y: view.frame.height - deleteTitle.frame.height - 10) + } + } + + + archive.set(handler: { [weak self] _ in + guard let item = self?.item as? ChatListRowItem else {return} + self?.endRevealState = nil + item.toggleArchive() + }, for: .Click) + + mute.set(handler: { [weak self] _ in + guard let item = self?.item as? ChatListRowItem else {return} + self?.endRevealState = nil + item.toggleMuted() + }, for: .Click) + + delete.set(handler: { [weak self] _ in + guard let item = self?.item as? ChatListRowItem else {return} + self?.endRevealState = nil + item.delete() + }, for: .Click) + + + revealRightView.addSubview(pin) + + revealRightView.addSubview(delete) + + if item.filter == nil { + revealRightView.addSubview(archive) + } + + + + revealLeftView.addSubview(mute) + revealLeftView.addSubview(unread) + + + + revealLeftView.backgroundColor = unreadBackground + revealRightView.backgroundColor = item.filter == nil ? theme.colors.revealAction_inactive_background : theme.colors.revealAction_destructive_background + + + unread.setFrameSize(frame.height, frame.height) + mute.setFrameSize(frame.height, frame.height) + + + archive.setFrameSize(frame.height, frame.height) + pin.setFrameSize(frame.height, frame.height) + delete.setFrameSize(frame.height, frame.height) + + delete.setFrameOrigin(archive.frame.maxX, 0) + archive.setFrameOrigin(delete.frame.maxX, 0) + + + mute.setFrameOrigin(unread.frame.maxX, 0) + + + revealRightView.setFrameSize(rightRevealWidth, frame.height) + revealLeftView.setFrameSize(leftRevealWidth, frame.height) + } else { + + + let collapse: LAnimationButton = LAnimationButton(animation: "anim_hide", size: NSMakeSize(frame.height, frame.height), keysToColor: ["Path 2.Path 2.Fill 1"], color: theme.colors.revealAction_inactive_background, offset: NSMakeSize(0, 0), autoplaySide: .left) + let collapseTitle = TextViewLabel() + collapseTitle.attributedString = .initialize(string: L10n.chatListRevealActionCollapse, color: theme.colors.revealAction_inactive_foreground, font: .medium(12)) + collapseTitle.sizeToFit() + collapse.addSubview(collapseTitle) + collapse.set(background: theme.colors.revealAction_inactive_background, for: .Normal) + collapse.customHandler.layout = { [weak collapseTitle] view in + if let collapseTitle = collapseTitle { + collapseTitle.centerX(y: view.frame.height - collapseTitle.frame.height - 10) + } + } + + collapse.setFrameSize(frame.height, frame.height) + revealRightView.addSubview(collapse) + revealRightView.backgroundColor = theme.colors.revealAction_inactive_background + revealRightView.setFrameSize(rightRevealWidth, frame.height) + + collapse.set(handler: { [weak self] _ in + guard let item = self?.item as? ChatListRowItem else {return} + item.collapseOrExpandArchive() + self?.endRevealState = nil + }, for: .Click) + + + + if let archiveStatus = item.archiveStatus { + + + let hideOrPin: LAnimationButton + let hideOrPinTitle = TextViewLabel() + + switch archiveStatus { + case .hidden: + hideOrPin = LAnimationButton(animation: "anim_hide", size: NSMakeSize(frame.height, frame.height), keysToColor: ["Path 2.Path 2.Fill 1"], color: theme.colors.revealAction_accent_background, offset: NSMakeSize(0, 0), autoplaySide: .left, rotated: true) + hideOrPinTitle.attributedString = .initialize(string: L10n.chatListRevealActionPin, color: theme.colors.revealAction_accent_foreground, font: .medium(12)) + hideOrPin.set(background: theme.colors.revealAction_accent_background, for: .Normal) + default: + hideOrPin = LAnimationButton(animation: "anim_hide", size: NSMakeSize(frame.height, frame.height), keysToColor: ["Path 2.Path 2.Fill 1"], color: theme.colors.revealAction_inactive_background, offset: NSMakeSize(0, 0), autoplaySide: .left, rotated: false) + hideOrPinTitle.attributedString = .initialize(string: L10n.chatListRevealActionHide, color: theme.colors.revealAction_inactive_foreground, font: .medium(12)) + hideOrPin.set(background: theme.colors.revealAction_inactive_background, for: .Normal) + } + + hideOrPinTitle.sizeToFit() + hideOrPin.addSubview(hideOrPinTitle) + hideOrPin.customHandler.layout = { [weak hideOrPinTitle] view in + if let hideOrPinTitle = hideOrPinTitle { + hideOrPinTitle.centerX(y: view.frame.height - hideOrPinTitle.frame.height - 10) + } + } + + hideOrPin.setFrameSize(frame.height, frame.height) + revealLeftView.addSubview(hideOrPin) + revealLeftView.backgroundColor = item.archiveStatus?.isHidden == true ? theme.colors.revealAction_accent_background : theme.colors.revealAction_inactive_background + revealLeftView.setFrameSize(leftRevealWidth, frame.height) + + hideOrPin.set(handler: { [weak self] _ in + guard let item = self?.item as? ChatListRowItem else {return} + item.toggleHideArchive() + self?.endRevealState = nil + }, for: .Click) + + } + + + } + + + } + + var additionalRevealDelta: CGFloat { + let additionalDelta: CGFloat + if let state = endRevealState { + switch state { + case .left: + additionalDelta = -leftRevealWidth + case .right: + additionalDelta = rightRevealWidth + case .none: + additionalDelta = 0 + } + } else { + additionalDelta = 0 + } + return additionalDelta + } + + var containerX: CGFloat { + return containerView.frame.minX + } + + var width: CGFloat { + return containerView.frame.width + } + + var rightRevealWidth: CGFloat { + return revealRightView.subviewsSize.width + } + + var leftRevealWidth: CGFloat { + return revealLeftView.subviewsSize.width + } + + private var animateOnceAfterDelta: Bool = true + func moveReveal(delta: CGFloat) { + + + if revealLeftView.subviews.isEmpty && revealRightView.subviews.isEmpty { + initRevealState() + } + + self.internalDelta = delta + + let delta = delta// - additionalRevealDelta + + containerView.change(pos: NSMakePoint(delta, containerView.frame.minY), animated: false) + revealLeftView.change(pos: NSMakePoint(min(-leftRevealWidth + delta, 0), revealLeftView.frame.minY), animated: false) + revealRightView.change(pos: NSMakePoint(frame.width + delta, revealRightView.frame.minY), animated: false) + + + revealLeftView.change(size: NSMakeSize(max(leftRevealWidth, delta), revealLeftView.frame.height), animated: false) + + revealRightView.change(size: NSMakeSize(max(rightRevealWidth, abs(delta)), revealRightView.frame.height), animated: false) + + + + if delta > 0, !revealLeftView.subviews.isEmpty { + let action = revealLeftView.subviews.last! + + let subviews = revealLeftView.subviews + let leftPercent: CGFloat = max(min(delta / leftRevealWidth, 1), 0) + + if delta > frame.width - (frame.width / 3) { + if animateOnceAfterDelta { + animateOnceAfterDelta = false + action.layer?.animatePosition(from: NSMakePoint(-(revealLeftView.frame.width - action.frame.width), action.frame.minY), to: NSMakePoint(0, 0), duration: 0.2, timingFunction: CAMediaTimingFunctionName.spring, removeOnCompletion: true, additive: true) + + for i in 0 ..< subviews.count - 1 { + let action = revealLeftView.subviews[i] + action.layer?.animatePosition(from: NSMakePoint(-(action.frame.width), action.frame.minY), to: NSMakePoint(0, 0), duration: 0.2, timingFunction: CAMediaTimingFunctionName.spring, removeOnCompletion: true, additive: true) + } + + NSHapticFeedbackManager.defaultPerformer.perform(.alignment, performanceTime: .drawCompleted) + } + + for i in 0 ..< subviews.count - 1 { + revealLeftView.subviews[i].setFrameOrigin(NSMakePoint(revealLeftView.frame.width, 0)) + } + + action.setFrameOrigin(NSMakePoint((revealLeftView.frame.width - action.frame.width), action.frame.minY)) + + + } else { + + if !animateOnceAfterDelta { + animateOnceAfterDelta = true + action.layer?.animatePosition(from: NSMakePoint(revealLeftView.frame.width - action.frame.width - (leftRevealWidth - action.frame.width), action.frame.minY), to: NSMakePoint(0, 0), duration: 0.2, timingFunction: CAMediaTimingFunctionName.spring, removeOnCompletion: true, additive: true) + + for i in stride(from: revealLeftView.subviews.count - 1, to: 0, by: -1) { + let action = revealLeftView.subviews[i] + action.layer?.animatePosition(from: NSMakePoint((action.frame.width), action.frame.minY), to: NSMakePoint(0, 0), duration: 0.2, timingFunction: CAMediaTimingFunctionName.spring, removeOnCompletion: true, additive: true) + } + + NSHapticFeedbackManager.defaultPerformer.perform(.alignment, performanceTime: .drawCompleted) + } + if subviews.count == 1 { + action.setFrameOrigin(NSMakePoint(min(revealLeftView.frame.width - action.frame.width, 0), action.frame.minY)) + } else { + action.setFrameOrigin(NSMakePoint(action.frame.width - action.frame.width * leftPercent, action.frame.minY)) + for i in 0 ..< subviews.count - 1 { + let action = subviews[i] + subviews[i].setFrameOrigin(NSMakePoint(revealLeftView.frame.width - action.frame.width, 0)) + } + } + } + } + + var rightPercent: CGFloat = delta / rightRevealWidth + if rightPercent < 0, !revealRightView.subviews.isEmpty { + rightPercent = 1 - min(1, abs(rightPercent)) + let subviews = revealRightView.subviews + + + let action = subviews.last! + + if rightPercent == 0 , delta < 0 { + if delta + action.frame.width * CGFloat(max(1, revealRightView.subviews.count - 1)) - 35 < -frame.midX { + if animateOnceAfterDelta { + animateOnceAfterDelta = false + NSHapticFeedbackManager.defaultPerformer.perform(.alignment, performanceTime: .default) + action.layer?.animatePosition(from: NSMakePoint((revealRightView.frame.width - rightRevealWidth), action.frame.minY), to: NSMakePoint(0, 0), duration: 0.2, timingFunction: CAMediaTimingFunctionName.spring, removeOnCompletion: true, additive: true) + + for i in 0 ..< subviews.count - 1 { + subviews[i].layer?.animatePosition(from: NSMakePoint((subviews[i].frame.width * CGFloat(i + 1)), subviews[i].frame.minY), to: NSMakePoint(0, 0), duration: 0.2, timingFunction: CAMediaTimingFunctionName.spring, removeOnCompletion: true, additive: true) + } + + } + + for i in 0 ..< subviews.count - 1 { + subviews[i].setFrameOrigin(NSMakePoint(-subviews[i].frame.width, 0)) + } + + action.setFrameOrigin(NSMakePoint(0, action.frame.minY)) + + } else { + if !animateOnceAfterDelta { + animateOnceAfterDelta = true + NSHapticFeedbackManager.defaultPerformer.perform(.alignment, performanceTime: .default) + + action.layer?.animatePosition(from: NSMakePoint(-(revealRightView.frame.width - rightRevealWidth), action.frame.minY), to: NSMakePoint(0, 0), duration: 0.2, timingFunction: CAMediaTimingFunctionName.spring, removeOnCompletion: true, additive: true) + + for i in 0 ..< subviews.count - 1 { + subviews[i].layer?.animatePosition(from: NSMakePoint(-(subviews[i].frame.width * CGFloat(i + 1)), subviews[i].frame.minY), to: NSMakePoint(0, 0), duration: 0.2, timingFunction: CAMediaTimingFunctionName.spring, removeOnCompletion: true, additive: true) + } + + } + action.setFrameOrigin(NSMakePoint((revealRightView.frame.width - action.frame.width), action.frame.minY)) + + for i in 0 ..< subviews.count - 1 { + subviews[i].setFrameOrigin(NSMakePoint(CGFloat(i) * subviews[i].frame.width, 0)) + } + } + } else { + for (i, subview) in subviews.enumerated() { + let i = CGFloat(i) + subview.setFrameOrigin(subview.frame.width * i - subview.frame.width * i * rightPercent, 0) + } +// subviews[0].setFrameOrigin(0, 0) +// subviews[1].setFrameOrigin(subviews[0].frame.width - subviews[1].frame.width * rightPercent, 0) +// subviews[2].setFrameOrigin((subviews[0].frame.width * 2) - (subviews[2].frame.width * 2) * rightPercent, 0) + } + } + } + + func completeReveal(direction: SwipeDirection) { + self.endRevealState = direction + + if revealLeftView.subviews.isEmpty || revealRightView.subviews.isEmpty { + initRevealState() + } + + + let updateRightSubviews:(Bool) -> Void = { [weak self] animated in + guard let `self` = self else {return} + let subviews = self.revealRightView.subviews + var x: CGFloat = 0 + for subview in subviews { + if subview != subviews.last { + subview._change(pos: NSMakePoint(x, 0), animated: animated, timingFunction: .spring) + x += subview.frame.width + } else { + subview._change(pos: NSMakePoint(self.rightRevealWidth - subview.frame.width, 0), animated: animated, timingFunction: .spring) + } + } + } + + let updateLeftSubviews:(Bool) -> Void = { [weak self] animated in + guard let `self` = self else {return} + let subviews = self.revealLeftView.subviews + var x: CGFloat = 0 + for subview in subviews.reversed() { + subview._change(pos: NSMakePoint(x, 0), animated: animated, timingFunction: .spring) + x += subview.frame.width + } + } + + let failed:(@escaping(Bool)->Void)->Void = { [weak self] completion in + guard let `self` = self else {return} + self.containerView.change(pos: NSMakePoint(0, self.containerView.frame.minY), animated: true, timingFunction: .spring) + self.revealLeftView.change(pos: NSMakePoint(-self.revealLeftView.frame.width, self.revealLeftView.frame.minY), animated: true, timingFunction: .spring) + self.revealRightView.change(pos: NSMakePoint(self.frame.width, self.revealRightView.frame.minY), animated: true, timingFunction: .spring, completion: completion) + + updateRightSubviews(true) + updateLeftSubviews(true) + self.endRevealState = nil + } + + let animateRightLongReveal:(@escaping(Bool)->Void)->Void = { [weak self] completion in + guard let `self` = self else {return} + updateRightSubviews(true) + self.endRevealState = nil + let duration: Double = 0.2 + + self.containerView.change(pos: NSMakePoint(-self.containerView.frame.width, self.containerView.frame.minY), animated: true, duration: duration, timingFunction: .spring) + self.revealRightView.change(size: NSMakeSize(self.frame.width + self.rightRevealWidth, self.revealRightView.frame.height), animated: true, duration: duration, timingFunction: .spring) + self.revealRightView.change(pos: NSMakePoint(-self.rightRevealWidth, self.revealRightView.frame.minY), animated: true, duration: duration, timingFunction: .spring, completion: completion) + + } + + + + + switch direction { + case let .left(state): + + if revealLeftView.subviews.isEmpty { + failed( { [weak self] _ in + self?.revealRightView.removeAllSubviews() + self?.revealLeftView.removeAllSubviews() + } ) + return + } + + switch state { + case .success: + + let invokeLeftAction = containerX > frame.width - (frame.width / 3) + + let duration: Double = 0.2 + + containerView.change(pos: NSMakePoint(leftRevealWidth, containerView.frame.minY), animated: true, duration: duration, timingFunction: .spring) + revealLeftView.change(size: NSMakeSize(leftRevealWidth, revealLeftView.frame.height), animated: true, duration: duration, timingFunction: .spring) + + revealRightView.change(pos: NSMakePoint(frame.width, revealRightView.frame.minY), animated: true) + updateLeftSubviews(true) + + var last = self.revealLeftView.subviews.last as? Control + + revealLeftView.change(pos: NSMakePoint(0, revealLeftView.frame.minY), animated: true, duration: duration, timingFunction: .spring, completion: { [weak self] completed in + if completed, invokeLeftAction { + last?.send(event: .Click) + last = nil + self?.needsLayout = true + } + }) + case .failed: + failed( { [weak self] _ in + self?.revealRightView.removeAllSubviews() + self?.revealLeftView.removeAllSubviews() + } ) + default: + break + } + case let .right(state): + + if revealRightView.subviews.isEmpty { + failed( { [weak self] _ in + self?.revealRightView.removeAllSubviews() + self?.revealLeftView.removeAllSubviews() + } ) + return + } + + switch state { + case .success: + let invokeRightAction = containerX + revealRightView.subviews.last!.frame.minX < -frame.midX + + var last = self.revealRightView.subviews.last as? Control + + + if invokeRightAction { + if self.revealRightView.subviews.count < 3 { + failed({ completed in + if invokeRightAction { + DispatchQueue.main.async { + last?.send(event: .Click) + last = nil + } + } + }) + } else { + animateRightLongReveal({ completed in + if invokeRightAction { + DispatchQueue.main.async { + last?.send(event: .Click) + last = nil + } + } + }) + } + + } else { + revealRightView.change(pos: NSMakePoint(frame.width - rightRevealWidth, revealRightView.frame.minY), animated: true, timingFunction: .spring) + revealRightView.change(size: NSMakeSize(rightRevealWidth, revealRightView.frame.height), animated: true, timingFunction: .spring) + containerView.change(pos: NSMakePoint(-rightRevealWidth, containerView.frame.minY), animated: true, timingFunction: .spring) + revealLeftView.change(pos: NSMakePoint(-leftRevealWidth, revealLeftView.frame.minY), animated: true, timingFunction: .spring) + + + let handler = (revealRightView.subviews.last as? Control)?.removeLastHandler() + (revealRightView.subviews.last as? Control)?.set(handler: { control in + var _control:Control? = control + animateRightLongReveal({ completed in + if let control = _control { + DispatchQueue.main.async { + handler?(control) + _control = nil + } + + } + }) + }, for: .Click) + + } + updateRightSubviews(true) + case .failed: + failed( { [weak self] _ in + self?.revealRightView.removeAllSubviews() + self?.revealLeftView.removeAllSubviews() + } ) + default: + break + } + default: + self.endRevealState = nil + failed( { [weak self] _ in + self?.revealRightView.removeAllSubviews() + self?.revealLeftView.removeAllSubviews() + } ) + } + // } deinit { @@ -343,15 +1527,52 @@ class ChatListRowView: TableRowView { override func layout() { super.layout() - if let item = self.item as? ChatListRowItem, let displayLayout = item.ctxDisplayLayout { - self.activitiesModel?.view?.setFrameOrigin(item.leftInset, displayLayout.0.size.height + item.margin + 3) + + guard let item = item as? ChatListRowItem else { return } + + expandView?.frame = NSMakeRect(0, item.isCollapsed ? 0 : item.height, frame.width - .borderSize, frame.height) + + if let delta = internalDelta { + moveReveal(delta: delta) + } else { + let additionalDelta: CGFloat + if let state = endRevealState { + switch state { + case .left: + additionalDelta = -leftRevealWidth + case .right: + additionalDelta = rightRevealWidth + case .none: + additionalDelta = 0 + } + } else { + additionalDelta = 0 + } - if let badgeNode = item.ctxBadgeNode { - badgeView?.setFrameOrigin(self.frame.width - badgeNode.size.width - item.margin, self.frame.height - badgeNode.size.height - item.margin + 1) + containerView.frame = NSMakeRect(-additionalDelta, item.isCollapsed ? -70 : 0, frame.width - .borderSize, 70) + revealLeftView.frame = NSMakeRect(-leftRevealWidth - additionalDelta, 0, leftRevealWidth, frame.height) + revealRightView.frame = NSMakeRect(frame.width - additionalDelta, 0, rightRevealWidth, frame.height) + + + if let displayLayout = item.ctxDisplayLayout { + var offset: CGFloat = 0 + if let chatName = item.ctxChatNameLayout { + offset += chatName.0.size.height + 1 + } + + var mediaPreviewOffset = NSMakePoint(item.leftInset, displayLayout.0.size.height + item.margin + 2 + offset) + let contentImageSpacing: CGFloat = 2.0 + + for (message, _, mediaSize) in self.currentMediaPreviewSpecs { + if let previewView = self.mediaPreviewViews[message.id] { + previewView.frame = CGRect(origin: mediaPreviewOffset, size: mediaSize) + } + mediaPreviewOffset.x += mediaSize.width + contentImageSpacing + } + } } } - } diff --git a/Telegram-Mac/ChatListTouchBar.swift b/Telegram-Mac/ChatListTouchBar.swift new file mode 100644 index 0000000000..fac36c8701 --- /dev/null +++ b/Telegram-Mac/ChatListTouchBar.swift @@ -0,0 +1,457 @@ +// +// ChatListTouchBar.swift +// Telegram +// +// Created by Mikhail Filimonov on 12/09/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import TGUIKit +import TelegramCore + +import SwiftSignalKit +import Postbox + +@available(OSX 10.12.2, *) +private extension NSTouchBarItem.Identifier { + static let chatListSearch = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.chatListSearch") + static let chatListNewChat = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.chatListNewChat") + static let chatListRecent = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.chatListRecent") + + static let composeNewGroup = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.composeNewGroup") + static let composeNewChannel = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.composeNewChannel") + static let composeNewSecretChat = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.composeNewSecretChat") + +} + + +@available(OSX 10.12.2, *) +private class TouchBarRecentPeerItemView: NSScrubberItemView { + private let selectView = View() + private var imageView: AvatarControl = AvatarControl.init(font: .avatar(12)) + private let fetchDisposable = MetaDisposable() + + private var badgeNode: BadgeNode? + private var badgeView:View? + + required override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(selectView) + addSubview(imageView) + imageView.setFrameSize(NSMakeSize(30, 30)) + selectView.setFrameSize(NSMakeSize(30, 30)) + + selectView.layer?.cornerRadius = 15 + selectView.layer?.borderColor = theme.colors.accent.cgColor + selectView.layer?.borderWidth = 1.5 + selectView.layer?.opacity = 0 + + selectView.isEventLess = true + } + + private(set) var peerId: PeerId? + + func update(context: AccountContext, peer: TouchBarPeerItem, selected: Bool) { + + self.peerId = peer.peer.id + + if peer.unreadCount > 0 { + if badgeView == nil { + badgeView = View() + self.addSubview(badgeView!) + } + guard let badgeView = self.badgeView else { + return + } + badgeView.removeAllSubviews() + + if peer.muted { + self.badgeNode = BadgeNode(.initialize(string: "\(peer.unreadCount)", color: theme.chatList.badgeTextColor, font: .medium(8)), theme.colors.grayText) + } else { + self.badgeNode = BadgeNode(.initialize(string: "\(peer.unreadCount)", color: theme.chatList.badgeTextColor, font: .medium(8)), theme.colors.accent) + } + guard let badgeNode = self.badgeNode else { + return + } + + badgeNode.additionSize = NSMakeSize(0, 0) + + + + badgeView.setFrameSize(badgeNode.size) + badgeNode.view = badgeView + badgeNode.setNeedDisplay() + needsLayout = true + } else { + self.badgeView?.removeFromSuperview() + self.badgeView = nil + } + + imageView.setPeer(account: context.account, peer: peer.peer) + } + private var _selected: Bool = false + func updateSelected(_ selected: Bool) { + if self._selected != selected { + self._selected = selected + selectView.change(opacity: selected ? 1 : 0, animated: true, duration: 0.1, timingFunction: .spring) + if selected { + selectView.layer?.animateScaleSpring(from: 0.2, to: 1.0, duration: 0.2, removeOnCompletion: true) + imageView.layer?.animateScaleSpring(from: 1, to: 0.75, duration: 0.2, removeOnCompletion: false) + badgeView?.layer?.animateScaleSpring(from: 1, to: 0.75, duration: 0.2, removeOnCompletion: false) + } else { + selectView.layer?.animateScaleSpring(from: 1.0, to: 0.2, duration: 0.2, removeOnCompletion: false) + imageView.layer?.animateScaleSpring(from: 0.75, to: 1.0, duration: 0.2, removeOnCompletion: true) + badgeView?.layer?.animateScaleSpring(from: 0.75, to: 1.0, duration: 0.2, removeOnCompletion: true) + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func updateLayer() { + } + + deinit { + fetchDisposable.dispose() + } + + override func layout() { + super.layout() + selectView.center() + imageView.center() + + guard let badgeView = self.badgeView else { + return + } + badgeView.setFrameOrigin(NSMakePoint(frame.width - badgeView.frame.width, badgeView.frame.height - 11)) + + } +} + + +@available(OSX 10.12.2, *) +private class RecentPeersScrubberBarItem: NSCustomTouchBarItem, NSScrubberDelegate, NSScrubberDataSource, NSScrubberFlowLayoutDelegate { + + private static let peerIdentifier = "peerIdentifier" + + var entries: [TouchBarPeerItem] + private let context: AccountContext + private let selected: ChatLocation? + init(identifier: NSTouchBarItem.Identifier, context: AccountContext, entries: [TouchBarPeerItem], selected: ChatLocation?) { + self.entries = entries + self.context = context + self.selected = selected + super.init(identifier: identifier) + + let scrubber = NSScrubber() + scrubber.register(TouchBarRecentPeerItemView.self, forItemIdentifier: NSUserInterfaceItemIdentifier(rawValue: RecentPeersScrubberBarItem.peerIdentifier)) + + scrubber.mode = .free + scrubber.selectionBackgroundStyle = .none + scrubber.floatsSelectionViews = true + scrubber.delegate = self + scrubber.dataSource = self + + + let gesture = NSPressGestureRecognizer(target: self, action: #selector(self.pressGesture(_:))) + gesture.allowedTouchTypes = NSTouch.TouchTypeMask.direct + gesture.allowableMovement = 0 + gesture.minimumPressDuration = 0 + scrubber.addGestureRecognizer(gesture) + + self.view = scrubber + } + + @objc private func pressGesture(_ gesture: NSPressGestureRecognizer) { + + let context = self.context + + let runSelector:(Bool, Bool)->Void = { [weak self] cancelled, navigate in + guard let `self` = self else { + return + } + let scrollView = HackUtils.findElements(byClass: "NSScrollView", in: self.view)?.first as? NSScrollView + + guard let container = scrollView?.documentView?.subviews.first else { + return + } + var point = gesture.location(in: container) + point.y = 0 + for itemView in container.subviews { + if let itemView = itemView as? TouchBarRecentPeerItemView { + if NSPointInRect(point, itemView.frame) { + itemView.updateSelected(!cancelled) + if navigate, let peerId = itemView.peerId { + context.sharedContext.bindings.rootNavigation().push(ChatController(context: context, chatLocation: .peer(peerId))) + } + } else { + itemView.updateSelected(false) + } + } + } + } + + switch gesture.state { + case .began: + runSelector(false, false) + case .failed, .cancelled: + runSelector(true, false) + case .ended: + runSelector(true, true) + case .changed: + runSelector(false, false) + case .possible: + break + @unknown default: + runSelector(false, false) + } + } + fileprivate var modalPreview: PreviewModalController? + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + + func numberOfItems(for scrubber: NSScrubber) -> Int { + return entries.count + } + + func scrubber(_ scrubber: NSScrubber, didHighlightItemAt highlightedIndex: Int) { + scrubber.selectionBackgroundStyle = .none + } + + func scrubber(_ scrubber: NSScrubber, viewForItemAt index: Int) -> NSScrubberItemView { + let itemView: NSScrubberItemView + + let peer = self.entries[index] + + let view = scrubber.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: RecentPeersScrubberBarItem.peerIdentifier), owner: nil) as! TouchBarRecentPeerItemView + view.update(context: context, peer: peer, selected: self.selected?.peerId == peer.peer.id) + itemView = view + + return itemView + } + + func scrubber(_ scrubber: NSScrubber, layout: NSScrubberFlowLayout, sizeForItemAt itemIndex: Int) -> NSSize { + return NSSize(width: 40, height: 40) + } + + + func scrubber(_ scrubber: NSScrubber, didSelectItemAt index: Int) { + + } +} + + + +@available(OSX 10.12.2, *) +final class ComposePopoverTouchBar : NSTouchBar, NSTouchBarDelegate { + + private let newGroup:()->Void + private let newSecretChat:()->Void + private let newChannel:()->Void + init(newGroup:@escaping()->Void, newSecretChat:@escaping()->Void, newChannel:@escaping()->Void) { + self.newGroup = newGroup + self.newSecretChat = newSecretChat + self.newChannel = newChannel + super.init() + + delegate = self + defaultItemIdentifiers = [.flexibleSpace, .composeNewGroup, .composeNewSecretChat, .composeNewChannel, .flexibleSpace] + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func newGroupAction() { + newGroup() + } + @objc private func newSecretChatAction() { + newSecretChat() + } + @objc private func newChannelAction() { + newChannel() + } + + func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? { + switch identifier { + case .composeNewGroup: + let item: NSCustomTouchBarItem = NSCustomTouchBarItem(identifier: identifier) + let image = NSImage(named: NSImage.Name("Icon_TouchBar_ComposeGroup"))! + let button = NSButton(title: L10n.composePopoverNewGroup, image: image, target: self, action: #selector(newGroupAction)) + item.view = button + item.customizationLabel = L10n.composePopoverNewGroup + return item + case .composeNewChannel: + let item: NSCustomTouchBarItem = NSCustomTouchBarItem(identifier: identifier) + let image = NSImage(named: NSImage.Name("Icon_TouchBar_ComposeChannel"))! + let button = NSButton(title: L10n.composePopoverNewChannel, image: image, target: self, action: #selector(newChannelAction)) + item.view = button + item.customizationLabel = L10n.composePopoverNewChannel + return item + case .composeNewSecretChat: + let item: NSCustomTouchBarItem = NSCustomTouchBarItem(identifier: identifier) + let image = NSImage(named: NSImage.Name("Icon_TouchBar_ComposeSecretChat"))! + let button = NSButton(title: L10n.composePopoverNewSecretChat, image: image, target: self, action: #selector(newSecretChatAction)) + item.view = button + item.customizationLabel = L10n.composePopoverNewSecretChat + return item + default: + break + } + return nil + } +} + +private struct TouchBarPeerItem : Equatable { + let peer: Peer + let unreadCount: Int32 + let muted: Bool + static func ==(lhs: TouchBarPeerItem, rhs: TouchBarPeerItem) -> Bool { + return lhs.peer.id == rhs.peer.id + } +} + +@available(OSX 10.12.2, *) +class ChatListTouchBar: NSTouchBar, NSTouchBarDelegate { + + private let search:()->Void + private let newGroup:()->Void + private let newSecretChat:()->Void + private let newChannel:()->Void + private let context: AccountContext + private var peers:[TouchBarPeerItem] = [] + private var selected: ChatLocation? + private let disposable = MetaDisposable() + init(context: AccountContext, search:@escaping()->Void, newGroup:@escaping()->Void, newSecretChat:@escaping()->Void, newChannel:@escaping()->Void) { + self.search = search + self.newGroup = newGroup + self.newSecretChat = newSecretChat + self.newChannel = newChannel + self.context = context + super.init() + delegate = self + customizationIdentifier = .windowBar + defaultItemIdentifiers = [.chatListNewChat, .flexibleSpace, .chatListSearch, .flexibleSpace] + customizationAllowedItemIdentifiers = defaultItemIdentifiers + + +// let recent:Signal<[TouchBarPeerItem], NoError> = recentlySearchedPeers(postbox: context.account.postbox) |> map { recent in +// return recent.prefix(10).compactMap { $0.peer.peer != nil ? TouchBarPeerItem(peer: $0.peer.peer!, unreadCount: $0.unreadCount, muted: $0.notificationSettings?.isMuted ?? false) : nil } +// } +// let top:Signal<[TouchBarPeerItem], NoError> = recentPeers(account: context.account) |> mapToSignal { top in +// switch top { +// case .disabled: +// return .single([]) +// case let .peers(peers): +// let peers = Array(peers.prefix(7)) +// return combineLatest(peers.map {context.account.viewTracker.peerView($0.id)}) |> mapToSignal { peerViews -> Signal<[TouchBarPeerItem], NoError> in +// return context.account.postbox.unreadMessageCountsView(items: peerViews.map {.peer($0.peerId)}) |> map { values in +// var peers:[TouchBarPeerItem] = [] +// for peerView in peerViews { +// if let peer = peerViewMainPeer(peerView) { +// let isMuted = peerView.isMuted +// let unreadCount = values.count(for: .peer(peerView.peerId)) +// peers.append(TouchBarPeerItem(peer: peer, unreadCount: unreadCount ?? 0, muted: isMuted)) +// } +// } +// return peers +// } +// } +// } +// } +// + +// let signal = combineLatest(queue: .mainQueue(), recent, top) +// disposable.set(signal.start(next: { [weak self] recent, top in +// self?.peers = (top + recent).prefix(14).uniqueElements +// self?.updateInterface() +// })) + } + + private func identifiers() -> [NSTouchBarItem.Identifier] { + var items:[NSTouchBarItem.Identifier] = [] + + items.append(.chatListNewChat) + if peers.isEmpty { + items.append(.flexibleSpace) + items.append(.chatListSearch) + items.append(.flexibleSpace) + } else { + items.append(.fixedSpaceSmall) + items.append(.chatListRecent) + items.append(.fixedSpaceSmall) + } + return items + } + + private func updateInterface() { + defaultItemIdentifiers = identifiers() + customizationAllowedItemIdentifiers = defaultItemIdentifiers + + for identifier in itemIdentifiers { + switch identifier { + case .chatListRecent: + let view = (item(forIdentifier: identifier) as? RecentPeersScrubberBarItem) + view?.entries = self.peers + (view?.view as? NSScrubber)?.reloadData() + default: + break + } + } + + + } + + deinit { + disposable.dispose() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? { + switch identifier { + case .chatListNewChat: + let item = NSPopoverTouchBarItem(identifier: identifier) + let button = NSButton(image: NSImage(named: NSImage.Name("Icon_TouchBar_Compose"))!, target: item, action: #selector(NSPopoverTouchBarItem.showPopover(_:))) + + item.popoverTouchBar = ComposePopoverTouchBar(newGroup: self.newGroup, newSecretChat: self.newSecretChat, newChannel: self.newChannel) + item.collapsedRepresentation = button + item.customizationLabel = L10n.touchBarLabelNewChat + return item + case .chatListSearch: + let item = NSCustomTouchBarItem(identifier: identifier) + let image = NSImage(named: NSImage.Name("Icon_TouchBar_Search"))! + let button = NSButton(title: L10n.touchBarSearchUsersOrMessages, image: image, target: self, action: #selector(searchAction)) + button.imagePosition = .imageLeft + button.imageHugsTitle = true + button.addWidthConstraint(relation: .equal, size: 350) + item.view = button + item.customizationLabel = button.title + return item + case .chatListRecent: + let scrubberItem: NSCustomTouchBarItem = RecentPeersScrubberBarItem(identifier: identifier, context: context, entries: self.peers, selected: self.selected) + return scrubberItem + default: + break + } + return nil + } + + @objc private func composeAction() { + + } + + @objc private func searchAction() { + self.search() + } +} diff --git a/Telegram-Mac/ChatMapContentView.swift b/Telegram-Mac/ChatMapContentView.swift index 60b6519b33..fa2862de12 100644 --- a/Telegram-Mac/ChatMapContentView.swift +++ b/Telegram-Mac/ChatMapContentView.swift @@ -8,9 +8,10 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac -import PostboxMac -import TelegramCoreMac +import SwiftSignalKit +import Postbox +import TelegramCore + class ChatMapContentView: ChatMediaContentView { @@ -30,7 +31,7 @@ class ChatMapContentView: ChatMediaContentView { override func executeInteraction(_ isControl: Bool) { if let parameters = self.parameters as? ChatMediaMapLayoutParameters { - execute(inapp: .external(link: parameters.url, false)) + parameters.execute() } } @@ -45,15 +46,65 @@ class ChatMapContentView: ChatMediaContentView { } } - override func update(with media: Media, size: NSSize, account: Account, parent: Message?, table: TableView?, parameters: ChatMediaLayoutParameters?, animated: Bool = false) { - let mediaUpdated = self.media == nil || !self.media!.isEqual(media) + override func update(with media: Media, size: NSSize, context: AccountContext, parent: Message?, table: TableView?, parameters: ChatMediaLayoutParameters?, animated: Bool = false, positionFlags: LayoutPositionFlags? = nil, approximateSynchronousValue: Bool = false) { + let versionUpdated = parent?.stableVersion != self.parent?.stableVersion + var mediaUpdated = self.media == nil || !media.isSemanticallyEqual(to: self.media!) || versionUpdated iconView.image = theme.icons.chatMapPin iconView.sizeToFit() - super.update(with: media, size: size, account: account, parent: parent, table: table, parameters: parameters, animated: animated) + super.update(with: media, size: size, context: context, parent: parent, table: table, parameters: parameters, animated: animated, positionFlags: positionFlags) - if mediaUpdated, let parameters = parameters as? ChatMediaMapLayoutParameters { - imageView.setSignal(account: account, signal: chatWebpageSnippetPhoto(account: account, photo: parameters.image, scale: backingScaleFactor, small: parameters.isVenue)) + if let positionFlags = positionFlags { + let path = CGMutablePath() + + let minx:CGFloat = 0, midx = frame.width/2.0, maxx = frame.width + let miny:CGFloat = 0, midy = frame.height/2.0, maxy = frame.height + + path.move(to: NSMakePoint(minx, midy)) + + var topLeftRadius: CGFloat = .cornerRadius + var bottomLeftRadius: CGFloat = .cornerRadius + var topRightRadius: CGFloat = .cornerRadius + var bottomRightRadius: CGFloat = .cornerRadius + + + if positionFlags.contains(.bottom) && positionFlags.contains(.left) { + topLeftRadius = topLeftRadius * 3 + 2 + } + if positionFlags.contains(.bottom) && positionFlags.contains(.right) { + topRightRadius = topRightRadius * 3 + 2 + } + if positionFlags.contains(.top) && positionFlags.contains(.left) { + bottomLeftRadius = bottomLeftRadius * 3 + 2 + } + if positionFlags.contains(.top) && positionFlags.contains(.right) { + bottomRightRadius = bottomRightRadius * 3 + 2 + } + + path.addArc(tangent1End: NSMakePoint(minx, miny), tangent2End: NSMakePoint(midx, miny), radius: bottomLeftRadius) + path.addArc(tangent1End: NSMakePoint(maxx, miny), tangent2End: NSMakePoint(maxx, midy), radius: bottomRightRadius) + path.addArc(tangent1End: NSMakePoint(maxx, maxy), tangent2End: NSMakePoint(midx, maxy), radius: topLeftRadius) + path.addArc(tangent1End: NSMakePoint(minx, maxy), tangent2End: NSMakePoint(minx, midy), radius: topRightRadius) + + let maskLayer: CAShapeLayer = CAShapeLayer() + maskLayer.path = path + layer?.mask = maskLayer + } else { + layer?.mask = nil + } + + + + if let parameters = parameters as? ChatMediaMapLayoutParameters, let resource = parameters.resource as? MapSnapshotMediaResource { + + imageView.setSignal(signal: cachedMedia(media: media, arguments: parameters.arguments, scale: backingScaleFactor, positionFlags: positionFlags), clearInstantly: false) + mediaUpdated = mediaUpdated && !self.imageView.hasImage + + imageView.setSignal( chatMapSnapshotImage(account: context.account, resource: resource), clearInstantly: false, animate: mediaUpdated, cacheImage: { [weak media] result in + if let media = media { + cacheMedia(result, media: media, arguments: parameters.arguments, scale: System.backingScale, positionFlags: positionFlags) + } + }) if parameters.isVenue { if textView == nil { @@ -65,6 +116,7 @@ class ChatMapContentView: ChatMediaContentView { } } + needsLayout = true } } diff --git a/Telegram-Mac/ChatMapRowItem.swift b/Telegram-Mac/ChatMapRowItem.swift index 08dd90e1b2..62f076b03b 100644 --- a/Telegram-Mac/ChatMapRowItem.swift +++ b/Telegram-Mac/ChatMapRowItem.swift @@ -8,52 +8,98 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac +import Postbox +import TelegramCore + final class ChatMediaMapLayoutParameters : ChatMediaLayoutParameters { let map:TelegramMediaMap - let resource:HttpReferenceMediaResource + let resource:TelegramMediaResource let image:TelegramMediaImage let venueText:TextViewLayout? let isVenue:Bool let defaultImageSize:NSSize let url:String + + let execute: ()->Void + fileprivate(set) var arguments:TransformImageArguments - init(map:TelegramMediaMap, resource:HttpReferenceMediaResource) { + init(map:TelegramMediaMap, resource:TelegramMediaResource, presentation: ChatMediaPresentation, automaticDownload: Bool, execute: @escaping() -> Void) { self.map = map self.isVenue = map.venue != nil self.resource = resource + self.execute = execute self.defaultImageSize = isVenue ? NSMakeSize(60, 60) : NSMakeSize(320, 120) - self.url = "https://maps.google.com/maps?q=\(map.latitude),\(map.longitude)" - let representation = TelegramMediaImageRepresentation(dimensions: defaultImageSize, resource: resource) - self.image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [representation]) + self.url = "https://maps.google.com/maps?q=\(String(format:"%f", map.latitude)),\(String(format:"%f", map.longitude))" + let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(defaultImageSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil) - self.arguments = TransformImageArguments(corners: ImageCorners(radius: .cornerRadius), imageSize: defaultImageSize, boundingSize: defaultImageSize, intrinsicInsets: NSEdgeInsets()) + self.image = TelegramMediaImage(imageId: map.id ?? MediaId(namespace: 0, id: arc4random64()), representations: [representation], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + + self.arguments = TransformImageArguments(corners: ImageCorners(radius: 8), imageSize: defaultImageSize, boundingSize: defaultImageSize, intrinsicInsets: NSEdgeInsets()) if let venue = map.venue { let attr = NSMutableAttributedString() - _ = attr.append(string: venue.title, color: theme.colors.text, font: .normal(.text)) + _ = attr.append(string: venue.title, color: presentation.text, font: .normal(.text)) _ = attr.append(string: "\n") - _ = attr.append(string: venue.address, color: theme.colors.grayText, font: .normal(.text)) + _ = attr.append(string: venue.address, color: presentation.grayText, font: .normal(.text)) venueText = TextViewLayout(attr, maximumNumberOfLines: 4, truncationType: .middle, alignment: .left) } else { venueText = nil } + super.init(presentation: presentation, media: map, automaticDownload: automaticDownload, autoplayMedia: AutoplayMediaPreferences.defaultSettings) } } func ==(lhs:ChatMediaMapLayoutParameters, rhs:ChatMediaMapLayoutParameters) -> Bool { - return lhs.resource.url == rhs.resource.url + return lhs.resource.isEqual(to: rhs.resource) } class ChatMapRowItem: ChatMediaItem { - - override init(_ initialSize: NSSize, _ chatInteraction: ChatInteraction, _ account: Account, _ object: ChatHistoryEntry) { - super.init(initialSize, chatInteraction, account, object) + fileprivate var liveText: TextViewLayout? + fileprivate var updatedText: TextViewLayout? + override init(_ initialSize: NSSize, _ chatInteraction: ChatInteraction, _ context: AccountContext, _ object: ChatHistoryEntry, _ downloadSettings: AutomaticMediaDownloadSettings, theme: TelegramPresentationTheme) { + super.init(initialSize, chatInteraction, context, object, downloadSettings, theme: theme) let map = media as! TelegramMediaMap - let isVenue = map.venue != nil - let resource = HttpReferenceMediaResource(url: "https://maps.googleapis.com/maps/api/staticmap?center=\(map.latitude),\(map.longitude)&zoom=15&size=\(isVenue ? 60 * Int(2.0) : 320 * Int(2.0))x\(isVenue ? 60 * Int(2.0) : 120 * Int(2.0))&sensor=true", size: 0) - self.parameters = ChatMediaMapLayoutParameters(map: map, resource: resource) + // let isVenue = map.venue != nil + let resource = MapSnapshotMediaResource(latitude: map.latitude, longitude: map.longitude, width: 320 * 2, height: 120 * 2, zoom: 15) + //let resource = HttpReferenceMediaResource(url: "https://maps.googleapis.com/maps/api/staticmap?center=\(map.latitude),\(map.longitude)&zoom=15&size=\(isVenue ? 60 * Int(2.0) : 320 * Int(2.0))x\(isVenue ? 60 * Int(2.0) : 120 * Int(2.0))&sensor=true", size: 0) + self.parameters = ChatMediaMapLayoutParameters(map: map, resource: resource, presentation: .make(for: object.message!, account: context.account, renderType: object.renderType, theme: theme), automaticDownload: downloadSettings.isDownloable(object.message!), execute: { + + if #available(OSX 10.13, *) { + showModal(with: LocationModalPreview(context, map: map, peer: object.message!.effectiveAuthor, messageId: object.message!.id), for: context.window) + } else { + execute(inapp: .external(link: "https://maps.google.com/maps?q=\(String(format:"%f", map.latitude)),\(String(format:"%f", map.longitude))", false)) + } + }) + + if isLiveLocationView { + liveText = TextViewLayout(.initialize(string: L10n.chatLiveLocation, color: theme.chat.textColor(isIncoming, object.renderType == .bubble), font: .bold(.text)), maximumNumberOfLines: 1, truncationType: .end) + + var editedDate:Int32 = object.message!.timestamp + for attr in object.message!.attributes { + if let attr = attr as? EditedMessageAttribute { + editedDate = attr.date + } + } + + var time:TimeInterval = Date().timeIntervalSince1970 + time -= context.timeDifference + let timeUpdated = Int32(time) - editedDate + + updatedText = TextViewLayout(.initialize(string: timeUpdated < 60 ? L10n.chatLiveLocationUpdatedNow : L10n.chatLiveLocationUpdatedCountable(Int(timeUpdated / 60)), color: theme.chat.grayText(isIncoming, object.renderType == .bubble), font: .normal(.text)), maximumNumberOfLines: 1) + } + } + + override var additionalLineForDateInBubbleState: CGFloat? { + if let parameters = parameters as? ChatMediaMapLayoutParameters { + if parameters.isVenue { + return rightSize.width > (_contentSize.width - 70) ? rightSize.height : nil + } + } + return nil + } + + override var isFixedRightPosition: Bool { + return true } override var instantlyResize:Bool { @@ -63,6 +109,56 @@ class ChatMapRowItem: ChatMediaItem { return false } + override var isBubbleFullFilled: Bool { + if let media = media as? TelegramMediaMap { + return media.venue == nil && isBubbled + } + return false + } + + var isLiveLocationView: Bool { + if let media = media as? TelegramMediaMap, let message = message { + if let liveBroadcastingTimeout = media.liveBroadcastingTimeout { + var time:TimeInterval = Date().timeIntervalSince1970 + time -= context.timeDifference + if Int32(time) < message.timestamp + liveBroadcastingTimeout { + return true + } + } + } + return false + } + + var liveLocationTimeout: Int32 { + if let media = media as? TelegramMediaMap { + if let liveBroadcastingTimeout = media.liveBroadcastingTimeout { + return liveBroadcastingTimeout + } + } + return 0 + } + + var liveLocationProgress: TimeInterval { + if let media = media as? TelegramMediaMap { + if let liveBroadcastingTimeout = media.liveBroadcastingTimeout, let message = message { + var time:TimeInterval = Date().timeIntervalSince1970 + time -= context.timeDifference + return 100.0 - (Double(time) - Double(message.timestamp)) / Double(liveBroadcastingTimeout) * 100.0 + } + } + return 0 + } + + override func viewClass() -> AnyClass { + return isLiveLocationView ? LiveLocationRowView.self : super.viewClass() + } + + override var isStateOverlayLayout: Bool { + return !isLiveLocationView && hasBubble && isBubbleFullFilled + } + + + override func makeContentSize(_ width: CGFloat) -> NSSize { if let parameters = parameters as? ChatMediaMapLayoutParameters { parameters.venueText?.measure(width: width - 70) @@ -71,9 +167,100 @@ class ChatMapRowItem: ChatMediaItem { size = parameters.defaultImageSize.aspectFitted(NSMakeSize(min(width,parameters.defaultImageSize.width), parameters.defaultImageSize.height)) parameters.arguments = TransformImageArguments(corners: ImageCorners(radius: .cornerRadius), imageSize: size, boundingSize: size, intrinsicInsets: NSEdgeInsets()) } - return NSMakeSize(width, size.height) + var venueSize: CGFloat = 0 + if let venueText = parameters.venueText { + venueSize = venueText.layoutSize.width + 10 + } + + return NSMakeSize(venueSize + size.width, size.height) } return super.makeContentSize(width) } + override var height: CGFloat { + if isLiveLocationView { + liveText?.measure(width: _contentSize.width - elementsContentInset * 2) + updatedText?.measure(width: _contentSize.width - elementsContentInset * 2) + return super.height + (renderType == .bubble ? 46 : 40) + } + return super.height + } + +} + +private class LiveLocationRowView : ChatMediaView { + private let liveText: TextView = TextView() + private let updatedText: TextView = TextView() + private let progress:TimableProgressView = TimableProgressView(theme: TimableProgressTheme(seconds: 20)) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + rowView.addSubview(updatedText) + rowView.addSubview(liveText) + rowView.addSubview(progress) + } + + + override func set(item: TableRowItem, animated: Bool) { + + guard let item = item as? ChatMapRowItem else {return} + + liveText.update(item.liveText) + updatedText.update(item.updatedText) + +// let difference:()->TimeInterval = { +// return TimeInterval((countdownBeginTime + attribute.timeout)) - (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) +// } +// let start = difference() / Double(attribute.timeout) * 100.0 + if item.isLiveLocationView { + progress.theme = TimableProgressTheme(backgroundColor: .clear, foregroundColor: theme.chat.textColor(item.isIncoming, item.entry.renderType == .bubble), seconds: Double(item.liveLocationTimeout), start: item.liveLocationProgress, borderWidth: 2) + progress.progress = 0 + progress.isHidden = false + rightView.isHidden = true + } else { + progress.isHidden = true + rightView.isHidden = false + } + + + progress.startAnimation() + super.set(item: item, animated: animated) + + } + + override func updateColors() { + super.updateColors() + liveText.backgroundColor = contentColor + updatedText.backgroundColor = contentColor + } + + private func textFrame(_ item: ChatRowItem) -> NSRect { + guard let item = item as? ChatMapRowItem else { return .zero } + guard let liveText = item.liveText else {return NSZeroRect} + let contentFrame = self.contentFrame(item) + return NSMakeRect(contentFrame.minX + item.elementsContentInset, contentFrame.maxY + item.defaultContentInnerInset, liveText.layoutSize.width, liveText.layoutSize.height) + } + private func updateFrame(_ item: ChatRowItem) -> NSRect { + guard let item = item as? ChatMapRowItem else { return .zero } + guard let updatedText = item.updatedText else {return NSZeroRect} + let contentFrame = self.contentFrame(item) + return NSMakeRect(contentFrame.minX + item.elementsContentInset, contentFrame.maxY + item.defaultContentInnerInset + liveText.frame.height, updatedText.layoutSize.width, updatedText.layoutSize.height) + } + + private func progressFrame(_ item: ChatRowItem) -> NSRect { + let contentFrame = self.contentFrame(item) + return NSMakeRect(contentFrame.maxX - progress.frame.width - (item.isBubbled ? item.defaultContentInnerInset : 0) - 3, contentFrame.maxY + item.defaultContentInnerInset + 5, 25, 25) + } + + override func layout() { + super.layout() + guard let item = item as? ChatMapRowItem else { return } + + liveText.frame = textFrame(item) + updatedText.frame = updateFrame(item) + progress.frame = progressFrame(item) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } } diff --git a/Telegram-Mac/ChatMediaAnimatedStickerView.swift b/Telegram-Mac/ChatMediaAnimatedStickerView.swift new file mode 100644 index 0000000000..f7ff5b60ee --- /dev/null +++ b/Telegram-Mac/ChatMediaAnimatedStickerView.swift @@ -0,0 +1,259 @@ +// +// ChatMediaAnimatedSticker.swift +// Telegram +// +// Created by Mikhail Filimonov on 13/05/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import PostboxMac +import TelegramCoreMac +import TGUIKit +import SwiftSignalKitMac +import Lottie + + +class MediaAnimatedStickerView: ChatMediaContentView { + + private let loadResourceDisposable = MetaDisposable() + private let stateDisposable = MetaDisposable() + private let playThrottleDisposable = MetaDisposable() + private let fetchDisposable = MetaDisposable() + private let playerView: LottiePlayerView = LottiePlayerView(frame: NSMakeRect(0, 0, 240, 240)) + private let thumbView = TransformImageView() + private var sticker:LottieAnimation? = nil { + didSet { + if oldValue != sticker { + self.previousAccept = false + } + updatePlayerIfNeeded() + } + } + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(self.playerView) + addSubview(self.thumbView) + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func clean() { + stateDisposable.set(nil) + loadResourceDisposable.set(nil) + playThrottleDisposable.set(nil) + fetchDisposable.set(nil) + } + + deinit { + loadResourceDisposable.dispose() + stateDisposable.dispose() + playThrottleDisposable.dispose() + fetchDisposable.dispose() + } + + func removeNotificationListeners() { + NotificationCenter.default.removeObserver(self) + } + + override func viewDidUpdatedDynamicContent() { + super.viewDidUpdatedDynamicContent() + updatePlayerIfNeeded() + } + + private var previousAccept: Bool = false + + + @objc func updatePlayerIfNeeded() { + let accept = ((self.window != nil && self.window!.isKeyWindow) || (self.window != nil && !(self.window is Window))) && !NSIsEmptyRect(self.visibleRect) && !self.isDynamicContentLocked && self.sticker != nil + + var signal = Signal.single(Void()) + if accept && !nextForceAccept { + signal = signal |> delay(accept ? 0.25 : 0, queue: .mainQueue()) + } + if accept && self.sticker != nil { + nextForceAccept = false + } + + if let sticker = self.sticker, previousAccept { + switch sticker.playPolicy { + case .once: + return + default: + break + } + } + + if previousAccept != accept { + self.playThrottleDisposable.set(signal.start(next: { [weak self] in + guard let `self` = self else { + return + } + self.playerView.set(accept ? self.sticker : nil) + self.previousAccept = accept + })) + } + previousAccept = accept + + + } + + private var nextForceAccept: Bool = false + + + override func previewMediaIfPossible() -> Bool { + if let table = table, let context = context, let window = window as? Window { + _ = startModalPreviewHandle(table, window: window, context: context) + } + return true + } + + override func executeInteraction(_ isControl: Bool) { + if let window = window as? Window { + if let context = context, let peerId = parent?.id.peerId, let media = media as? TelegramMediaFile, !media.isEmojiAnimatedSticker, let reference = media.stickerReference { + showModal(with:StickersPackPreviewModalController(context, peerId: peerId, reference: reference), for:window) + } else { + self.playerView.playIfNeeded() + } + } + } + + func updateListeners() { + if let window = window { + NotificationCenter.default.removeObserver(self) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSWindow.didBecomeKeyNotification, object: window) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSWindow.didResignKeyNotification, object: window) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSView.boundsDidChangeNotification, object: self.enclosingScrollView?.contentView) + } else { + removeNotificationListeners() + } + } + + override func viewWillDraw() { + super.viewWillDraw() + updatePlayerIfNeeded() + } + + override func willRemove() { + super.willRemove() + updateListeners() + updatePlayerIfNeeded() + } + + override func viewDidMoveToSuperview() { + updateListeners() + updatePlayerIfNeeded() + } + + override func viewDidMoveToWindow() { + updateListeners() + updatePlayerIfNeeded() + } + + override func update(with media: Media, size: NSSize, context: AccountContext, parent: Message?, table: TableView?, parameters: ChatMediaLayoutParameters?, animated: Bool, positionFlags: LayoutPositionFlags?, approximateSynchronousValue: Bool) { + + + guard let file = media as? TelegramMediaFile else { return } + + let updated = self.media != nil ? !file.isSemanticallyEqual(to: self.media!) : true + + if parent?.stableId != self.parent?.stableId { + self.sticker = nil + } else if parent == nil, updated { + self.sticker = nil + } + self.nextForceAccept = approximateSynchronousValue || parent?.id.namespace == Namespaces.Message.Local + + + super.update(with: media, size: size, context: context, parent: parent, table: table, parameters: parameters, animated: animated, positionFlags: positionFlags, approximateSynchronousValue: approximateSynchronousValue) + + + let reference: MediaResourceReference + + if let message = parent { + reference = FileMediaReference.message(message: MessageReference(message), media: file).resourceReference(file.resource) + } else if let stickerReference = file.stickerReference { + if file.resource is CloudStickerPackThumbnailMediaResource { + reference = MediaResourceReference.stickerPackThumbnail(stickerPack: stickerReference, resource: file.resource) + } else { + reference = FileMediaReference.stickerPack(stickerPack: stickerReference, media: file).resourceReference(file.resource) + } + } else { + reference = FileMediaReference.standalone(media: file).resourceReference(file.resource) + } + + let data: Signal + if let resource = file.resource as? LocalBundleResource { + data = Signal { subscriber in + if let path = Bundle.main.path(forResource: resource.name, ofType: resource.ext), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) { + subscriber.putNext(MediaResourceData(path: path, offset: 0, size: data.count, complete: true)) + subscriber.putCompletion() + } + return EmptyDisposable + } |> runOn(resourcesQueue) + } else { + data = context.account.postbox.mediaBox.resourceData(file.resource, attemptSynchronously: approximateSynchronousValue) + } + + self.loadResourceDisposable.set((data |> map { resourceData -> Data? in + + if resourceData.complete, let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) { + return data + } + return nil + } |> deliverOnMainQueue).start(next: { [weak file, weak self] data in + if let data = data, let file = file { + let playPolicy: LottiePlayPolicy = file.isEmojiAnimatedSticker ? .once : .loop + let maximumFps: Int = size.width < 200 && !file.isEmojiAnimatedSticker ? 30 : 60 + let fitzModifier = file.animatedEmojiFitzModifier + self?.sticker = LottieAnimation(compressed: data, key: LottieAnimationEntryKey(key: .media(file.id), size: size, fitzModifier: fitzModifier), cachePurpose: size.width < 200 ? .temporaryLZ4(.thumb) : self?.parent != nil ? .temporaryLZ4(.chat) : .none, playPolicy: playPolicy, maximumFps: maximumFps) + self?.fetchStatus = .Local + } else { + self?.sticker = nil + self?.fetchStatus = .Remote + } + })) + + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: NSEdgeInsets()) + + + self.thumbView.setSignal(signal: cachedMedia(media: file, arguments: arguments, scale: backingScaleFactor), clearInstantly: updated) + if !self.thumbView.isFullyLoaded { + self.thumbView.setSignal(chatMessageAnimatedSticker(postbox: context.account.postbox, file: file, small: false, scale: backingScaleFactor, size: size, fetched: false), cacheImage: { [weak file] result in + if let file = file { + cacheMedia(result, media: file, arguments: arguments, scale: System.backingScale) + } + }) + self.thumbView.set(arguments: arguments) + } else { + self.thumbView.dispose() + } + + fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: reference).start()) + stateDisposable.set((self.playerView.state |> deliverOnMainQueue).start(next: { [weak self] state in + guard let `self` = self else { return } + switch state { + case .playing: + self.playerView.isHidden = false + self.thumbView.isHidden = true + default: + self.playerView.isHidden = true + self.thumbView.isHidden = false + } + })) + } + + override var contents: Any? { + return self.thumbView.image + } + + override func layout() { + super.layout() + self.playerView.frame = bounds + self.thumbView.frame = bounds + } + +} diff --git a/Telegram-Mac/ChatMediaContentView.swift b/Telegram-Mac/ChatMediaContentView.swift index f61ad69621..5aa4424e17 100644 --- a/Telegram-Mac/ChatMediaContentView.swift +++ b/Telegram-Mac/ChatMediaContentView.swift @@ -7,26 +7,29 @@ // import Cocoa -import SwiftSignalKitMac -import PostboxMac -import TelegramCoreMac +import SwiftSignalKit +import Postbox +import TelegramCore + import TGUIKit +class ChatMediaContentView: Control, NSDraggingSource, NSPasteboardItemDataProvider, PinchableView { -class ChatMediaContentView: Control, NSDraggingSource, NSPasteboardItemDataProvider { + + private var acceptDragging:Bool = false private var inDragging:Bool = false private var mouseDownPoint: NSPoint = NSZeroPoint var parent:Message? var media:Media? - var account:Account? + var context:AccountContext? var parameters:ChatMediaLayoutParameters? private(set) var fetchControls:FetchControls! - var fetchStatus: MediaResourceStatus? + var fetchStatus: MediaResourceStatus? var dragDisposable:MetaDisposable = MetaDisposable() - + var positionFlags: LayoutPositionFlags? override var backgroundColor: NSColor { get { return super.backgroundColor @@ -34,7 +37,7 @@ class ChatMediaContentView: Control, NSDraggingSource, NSPasteboardItemDataProvi set { super.backgroundColor = newValue for view in subviews { - if !(view is TransformImageView) { + if !(view is TransformImageView) && !(view is SelectingControl) && !(view is GIFPlayerView) && !(view is ChatMessageAccessoryView) && !(view is MediaPreviewEditControl) && !(view is ProgressIndicator) { view.background = newValue } } @@ -43,11 +46,15 @@ class ChatMediaContentView: Control, NSDraggingSource, NSPasteboardItemDataProvi weak var table:TableView? - + override func updateTrackingAreas() { + + } + override init() { super.init() fetchControls = FetchControls(fetch: { [weak self] in self?.executeInteraction(true) + self?.open() }) } @@ -62,23 +69,8 @@ class ChatMediaContentView: Control, NSDraggingSource, NSPasteboardItemDataProvi fatalError("init(coder:) has not been implemented") } - func addGlobalAudioToVisible() { - if let controller = globalAudio { - table?.enumerateViews(with: { (view) in - if let view = (view as? ChatRowView)?.contentView.subviews.last as? ChatAudioContentView { - controller.add(listener: view) - } else if let view = (view as? ChatRowView)?.contentView.subviews.last as? ChatVideoMessageContentView { - controller.add(listener: view) - } else if let view = (view as? ChatRowView)?.contentView.subviews.last as? WPMediaContentView { - if let contentNode = view.contentNode as? ChatAudioContentView { - controller.add(listener: contentNode) - } - } - return true - }) - } - } - + + func willRemove() -> Void { //self.cancel() } @@ -92,15 +84,31 @@ class ChatMediaContentView: Control, NSDraggingSource, NSPasteboardItemDataProvi } func delete() -> Void { - if let parent = parent { - _ = account?.postbox.modify({ modifier -> Void in - modifier.deleteMessages([parent.id]) + cancel() + let engine = context?.engine.messages + if let parentId = parent?.id { + _ = context?.account.postbox.transaction({ transaction -> Void in + engine?.deleteMessages(transaction: transaction, ids: [parentId]) }).start() } } + override var allowsVibrancy: Bool { + return true + } + func cancelFetching() { - + if let context = context, let media = media { + if let parent = parent, let parameters = parameters { + parameters.cancelOperation(parent, media) + } else { + if let media = media as? TelegramMediaFile { + cancelFreeMediaFileInteractiveFetch(context: context, resource: media.resource) + } else if let media = media as? TelegramMediaImage { + chatMessagePhotoCancelInteractiveFetch(account: context.account, photo: media) + } + } + } } func open() -> Void { @@ -111,8 +119,16 @@ class ChatMediaContentView: Control, NSDraggingSource, NSPasteboardItemDataProvi } + func preloadStreamblePart() { + + } + + func updateMouse() { + + } + func executeInteraction(_ isControl:Bool) -> Void { - if let fetchStatus = self.fetchStatus { + if let fetchStatus = self.fetchStatus, userInteractionEnabled { switch fetchStatus { case .Fetching: if isControl { @@ -125,7 +141,7 @@ class ChatMediaContentView: Control, NSDraggingSource, NSPasteboardItemDataProvi } case .Remote: fetch() - //open() + //open() case .Local: open() break @@ -133,18 +149,63 @@ class ChatMediaContentView: Control, NSDraggingSource, NSPasteboardItemDataProvi } } + func previewMediaIfPossible() -> Bool { + return false + } + deinit { self.clean() dragDisposable.dispose() } + + func update(size: NSSize) { + + } - func update(with media: Media, size:NSSize, account:Account, parent:Message?, table:TableView?, parameters:ChatMediaLayoutParameters? = nil, animated: Bool = false) -> Void { + func update(with media: Media, size:NSSize, context:AccountContext, parent:Message?, table:TableView?, parameters:ChatMediaLayoutParameters? = nil, animated: Bool = false, positionFlags: LayoutPositionFlags? = nil, approximateSynchronousValue: Bool = false) -> Void { self.setContent(size: size) - self.media = media self.parameters = parameters - self.account = account + self.positionFlags = positionFlags + self.context = context self.parent = parent self.table = table + + + + self.media = media + + if let parameters = parameters { + if let parent = parent { + if parameters.automaticDownloadFunc(parent) { + fetch() + preloadStreamblePart() + } else { + if parameters.preload { + preloadStreamblePart() + } + } + } else if parameters.automaticDownload { + fetch() + preloadStreamblePart() + } else if parameters.preload { + preloadStreamblePart() + } + + } + + } + + var autoDownload: Bool { + if let parameters = parameters { + if let parent = parent { + if parameters.automaticDownloadFunc(parent) { + return true + } + } else if parameters.automaticDownload { + return true + } + } + return false } func addSublayer(_ layer:CALayer) -> Void { @@ -161,10 +222,25 @@ class ChatMediaContentView: Control, NSDraggingSource, NSPasteboardItemDataProvi return view } - var interactionContentView: NSView { + func interactionContentView(for innerId: AnyHashable, animateIn: Bool ) -> NSView { return self } + func interactionControllerDidFinishAnimation(interactive: Bool) { + + } + + func videoTimebase() -> CMTimebase? { + return nil + } + func applyTimebase(timebase: CMTimebase?) { + + } + + func addAccesoryOnCopiedView(view: NSView) { + + } + func draggingAbility(_ event:NSEvent) -> Bool { if let superview = superview { return NSPointInRect(superview.convert(event.locationInWindow, from: nil), frame) @@ -173,13 +249,19 @@ class ChatMediaContentView: Control, NSDraggingSource, NSPasteboardItemDataProvi } override func mouseDown(with event: NSEvent) { + + if event.modifierFlags.contains(.control) { + super.mouseDown(with: event) + return + } + if userInteractionEnabled { inDragging = false - acceptDragging = false dragpath = nil mouseDownPoint = convert(event.locationInWindow, from: nil) - acceptDragging = draggingAbility(event) - if let parent = parent, parent.id.peerId.id == Namespaces.Peer.SecretChat { + acceptDragging = draggingAbility(event) && parent != nil && !parent!.containsSecretMedia + + if let parent = parent, parent.id.peerId.namespace == Namespaces.Peer.SecretChat { acceptDragging = false } } @@ -188,19 +270,22 @@ class ChatMediaContentView: Control, NSDraggingSource, NSPasteboardItemDataProvi super.superview?.mouseDown(with: event) } } - + func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation { switch context { case .outsideApplication: return .copy case .withinApplication: return [] + @unknown default: + return [] } } private var dragpath:String? = nil - + func pasteboard(_ pasteboard: NSPasteboard?, item: NSPasteboardItem, provideDataForType type: NSPasteboard.PasteboardType) { if let dragpath = dragpath { + pasteboard?.clearContents() pasteboard?.declareTypes([.kFilenames, .string], owner: self) pasteboard?.setPropertyList([dragpath], forType: .kFilenames) pasteboard?.setString(dragpath, forType: .string) @@ -209,7 +294,7 @@ class ChatMediaContentView: Control, NSDraggingSource, NSPasteboardItemDataProvi } - + override func mouseDragged(with event: NSEvent) { if self.fetchStatus == .Local { @@ -219,10 +304,10 @@ class ChatMediaContentView: Control, NSDraggingSource, NSPasteboardItemDataProvi return } - if let account = account, let resource = mediaResource(from: media), let mimeType = mediaResourceMIMEType(from: media) { - let result = account.postbox.mediaBox.resourceData(resource) |> mapToSignal { [weak media] resource -> Signal in + if let context = context, let resource = mediaResource(from: media), let mimeType = mediaResourceMIMEType(from: media) { + let result = context.account.postbox.mediaBox.resourceData(resource) |> mapToSignal { [weak media] resource -> Signal in if resource.complete { - return resourceType( mimeType: mimeType) |> mapToSignal { [weak media] ext -> Signal in + return resourceType( mimeType: mimeType) |> mapToSignal { [weak media] ext -> Signal in return putFileToTemp(from: resource.path, named: mediaResourceName(from: media, ext: ext)) } } else { @@ -234,14 +319,14 @@ class ChatMediaContentView: Control, NSDraggingSource, NSPasteboardItemDataProvi dragDisposable.set(result.start(next: { [weak self] path in if let strongSelf = self, let path = path { strongSelf.dragpath = path - if let copy = (strongSelf.copy() as? NSView), let cgImage = copy.layer?.contents { - let image = NSImage(cgImage: cgImage as! CGImage, size: copy.frame.size) + if let cgImage = strongSelf.contents { + let image = NSImage(cgImage: cgImage as! CGImage, size: strongSelf.contentFrame.size) let writer = NSPasteboardItem() writer.setDataProvider(strongSelf, forTypes: [.kFileUrl]) let item = NSDraggingItem( pasteboardWriter: writer ) - item.setDraggingFrame(copy.bounds, contents: image) + item.setDraggingFrame(strongSelf.contentFrame, contents: image) strongSelf.beginDraggingSession(with: [item], event: event, source: strongSelf) } @@ -252,14 +337,24 @@ class ChatMediaContentView: Control, NSDraggingSource, NSPasteboardItemDataProvi } else { super.mouseDragged(with: event) } - + + } else { + super.superview?.mouseDragged(with: event) } - + + } else { + super.mouseDragged(with: event) } } override func mouseUp(with event: NSEvent) { + if event.modifierFlags.contains(.control) { + super.mouseUp(with: event) + return + } + + if !inDragging && draggingAbility(event) && userInteractionEnabled, event.clickCount <= 1 { executeInteraction(false) } else { @@ -271,6 +366,14 @@ class ChatMediaContentView: Control, NSDraggingSource, NSPasteboardItemDataProvi acceptDragging = false } + var contents: Any? { + return nil + } + var contentFrame: NSRect { + return bounds + } + } + diff --git a/Telegram-Mac/ChatMediaDice.swift b/Telegram-Mac/ChatMediaDice.swift new file mode 100644 index 0000000000..8f05b0595f --- /dev/null +++ b/Telegram-Mac/ChatMediaDice.swift @@ -0,0 +1,38 @@ +// +// ChatMediaDice.swift +// Telegram +// +// Created by Mikhail Filimonov on 27.02.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + +class ChatMediaDice: ChatMediaItem { + override var additionalLineForDateInBubbleState: CGFloat? { + return rightSize.height + 5 + } + override var isFixedRightPosition: Bool { + return true + } + override var isBubbleFullFilled: Bool { + return true + } + + override func menuItems(in location: NSPoint) -> Signal<[ContextMenuItem], NoError> { + return super.menuItems(in: location) |> map { [weak self] items in + var items = items + items.insert(ContextMenuItem(L10n.textCopyText, handler: { + if let media = self?.media as? TelegramMediaDice { + copyToClipboard(media.emoji) + } + }), at: 0) + return items + } + } +} diff --git a/Telegram-Mac/ChatMediaItem.swift b/Telegram-Mac/ChatMediaItem.swift index 40101f4e0b..462235e6cc 100644 --- a/Telegram-Mac/ChatMediaItem.swift +++ b/Telegram-Mac/ChatMediaItem.swift @@ -7,15 +7,99 @@ // import Cocoa -import TelegramCoreMac -import PostboxMac +import TelegramCore + +import Postbox import TGUIKit -import SwiftSignalKitMac +import SwiftSignalKit class ChatMediaLayoutParameters : Equatable { - static func layout(for media:TelegramMediaFile, isWebpage: Bool, chatInteraction:ChatInteraction) -> ChatMediaLayoutParameters { - if media.isInstantVideo { + var showMedia:(Message)->Void = {_ in } + var showMessage:(Message)->Void = {_ in } + + + var chatLocationInput:()->ChatLocationInput = { fatalError() } + var chatMode:ChatMode = .history + + var getUpdatingMediaProgress:(MessageId)->Signal = { _ in return .single(nil) } + var cancelOperation:(Message, Media)->Void = { _, _ in } + + let presentation: ChatMediaPresentation + let media: Media + + + var runEmojiScreenEffect:(String)->Void = { _ in } + + private var _timeCodeInitializer: Double? = nil + + var timeCodeInitializer:Double? { + let current = self._timeCodeInitializer + self._timeCodeInitializer = nil + return current + } + + func remove_timeCodeInitializer() { + self._timeCodeInitializer = nil + } + + func set_timeCodeInitializer(_ timecode: Double?) { + self._timeCodeInitializer = timecode + } + + private var _automaticDownload: Bool + + var automaticDownload: Bool { + get { + let value = _automaticDownload +// _automaticDownload = false + return value + } + } + + let autoplayMedia: AutoplayMediaPreferences + + var autoplay: Bool + var soundOnHover: Bool { + return autoplayMedia.soundOnHover + } + var preload: Bool { + return autoplayMedia.preloadVideos + } + +// var autoplay: Bool { +// get { +// let value = _automaticDownload +// return value +// } +// } +// + + var automaticDownloadFunc:(Message)->Bool + + + init(presentation: ChatMediaPresentation, media: Media, automaticDownload: Bool, autoplayMedia: AutoplayMediaPreferences) { + self.automaticDownloadFunc = { _ in + return automaticDownload + } + self.presentation = presentation + self.media = media + self.autoplayMedia = autoplayMedia + self._automaticDownload = automaticDownload + if let media = media as? TelegramMediaFile { + if media.isVideo && media.isAnimated { + self.autoplay = autoplayMedia.gifs + } else { + self.autoplay = autoplayMedia.videos + } + } else { + self.autoplay = false + } + } + + + static func layout(for media:TelegramMediaFile, isWebpage: Bool, chatInteraction:ChatInteraction, presentation: ChatMediaPresentation, automaticDownload: Bool, isIncoming: Bool, isFile: Bool = false, autoplayMedia: AutoplayMediaPreferences, isChatRelated: Bool = false) -> ChatMediaLayoutParameters { + if media.isInstantVideo && !isFile { var duration:Int = 0 for attr in media.attributes { switch attr { @@ -26,8 +110,8 @@ class ChatMediaLayoutParameters : Equatable { } } - return ChatMediaVideoMessageLayoutParameters(showPlayer:chatInteraction.inlineAudioPlayer, duration: duration, isMarked: true, isWebpage: isWebpage || chatInteraction.isLogInteraction, resource: media.resource) - } else if media.isVoice { + return ChatMediaVideoMessageLayoutParameters(showPlayer:chatInteraction.inlineAudioPlayer, duration: duration, isMarked: true, isWebpage: isWebpage || chatInteraction.isLogInteraction, resource: media.resource, presentation: presentation, media: media, automaticDownload: automaticDownload, autoplayMedia: autoplayMedia) + } else if media.isVoice && !isFile { var waveform:AudioWaveform? = nil var duration:Int = 0 for attr in media.attributes { @@ -35,15 +119,15 @@ class ChatMediaLayoutParameters : Equatable { case let .Audio(params): if let data = params.waveform?.makeData() { waveform = AudioWaveform(bitstream: data, bitsPerSample: 5) - duration = params.duration } + duration = params.duration default: break } } - return ChatMediaVoiceLayoutParameters(showPlayer:chatInteraction.inlineAudioPlayer, waveform:waveform, duration:duration, isMarked: true, isWebpage: isWebpage || chatInteraction.isLogInteraction, resource: media.resource) - } else if media.isMusic { + return ChatMediaVoiceLayoutParameters(showPlayer:chatInteraction.inlineAudioPlayer, waveform:waveform, duration:duration, isMarked: true, isWebpage: isWebpage || chatInteraction.isLogInteraction, resource: media.resource, presentation: presentation, media: media, automaticDownload: automaticDownload) + } else if media.isMusic && !isFile { var audioTitle:String? var audioPerformer:String? @@ -62,36 +146,40 @@ class ChatMediaLayoutParameters : Equatable { if let _audioTitle = audioTitle, let audioPerformer = audioPerformer { if _audioTitle.isEmpty && audioPerformer.isEmpty { - _ = attr.append(string: media.fileName, color: theme.colors.text, font: NSFont.normal(.title)) + _ = attr.append(string: media.fileName, color: presentation.text, font: NSFont.medium(.title)) audioTitle = media.fileName } else { - _ = attr.append(string: _audioTitle + " - " + audioPerformer, color: theme.colors.text, font: NSFont.normal(.title)) + _ = attr.append(string: _audioTitle + " - " + audioPerformer, color: presentation.text, font: NSFont.medium(.title)) } } else { - _ = attr.append(string: media.fileName, color: theme.colors.text, font: NSFont.normal(.title)) + _ = attr.append(string: media.fileName, color: presentation.text, font: NSFont.medium(.title)) audioTitle = media.fileName } - return ChatMediaMusicLayoutParameters(nameLayout: TextViewLayout(attr, maximumNumberOfLines: 1, truncationType: .middle), durationLayout: TextViewLayout(.initialize(string: String.durationTransformed(elapsed: duration), color: theme.colors.grayText, font: .normal(.title)), maximumNumberOfLines: 1, truncationType: .middle), sizeLayout: TextViewLayout(.initialize(string: (media.size ?? 0).prettyNumber, color: theme.colors.grayText, font: .normal(.title)), maximumNumberOfLines: 1, truncationType: .middle), resource: media.resource, isWebpage: isWebpage, title: audioTitle, performer: audioPerformer, showPlayer:chatInteraction.inlineAudioPlayer) + return ChatMediaMusicLayoutParameters(nameLayout: TextViewLayout(attr, maximumNumberOfLines: 1, truncationType: .end), durationLayout: TextViewLayout(.initialize(string: String.durationTransformed(elapsed: duration), color: presentation.grayText, font: .normal(.title)), maximumNumberOfLines: 1, truncationType: .end), sizeLayout: TextViewLayout(.initialize(string: (media.size ?? 0).prettyNumber, color: presentation.grayText, font: .normal(.title)), maximumNumberOfLines: 1, truncationType: .middle), resource: media.resource, isWebpage: isWebpage, title: audioTitle, performer: audioPerformer, showPlayer:chatInteraction.inlineAudioPlayer, presentation: presentation, media: media, automaticDownload: automaticDownload) } else { var fileName:String = "Unknown.file" if let name = media.fileName { fileName = name } - return ChatFileLayoutParameters(fileName: fileName, hasThumb: !media.previewRepresentations.isEmpty) + return ChatFileLayoutParameters(fileName: fileName, hasThumb: !media.previewRepresentations.isEmpty, presentation: presentation, media: media, automaticDownload: automaticDownload, isIncoming: isIncoming, autoplayMedia: autoplayMedia, isChatRelated: isChatRelated) } } + @discardableResult func makeLabelsForWidth(_ width: CGFloat) -> CGFloat { + return 0 + } + } class ChatMediaGalleryParameters : ChatMediaLayoutParameters { let isWebpage: Bool - let showMedia:()->Void - let showMessage:(Message)->Void - init(showMedia:@escaping()->Void, showMessage:@escaping(Message)->Void, isWebpage: Bool) { + + init(showMedia:@escaping(Message)->Void = { _ in }, showMessage:@escaping(Message)->Void = { _ in }, isWebpage: Bool, presentation: ChatMediaPresentation = .Empty, media: Media, automaticDownload: Bool, autoplayMedia: AutoplayMediaPreferences = AutoplayMediaPreferences.defaultSettings) { + self.isWebpage = isWebpage + super.init(presentation: presentation, media: media, automaticDownload: automaticDownload, autoplayMedia: autoplayMedia) self.showMedia = showMedia self.showMessage = showMessage - self.isWebpage = isWebpage } } @@ -102,51 +190,215 @@ func ==(lhs:ChatMediaLayoutParameters, rhs:ChatMediaLayoutParameters) -> Bool { class ChatMediaItem: ChatRowItem { - var _media:Media - var media:Media { - if let _media = _media as? TelegramMediaGame { - if let file = _media.file { - return file - } else if let image = _media.image { - return image - } + let media:Media + + + var parameters:ChatMediaLayoutParameters? = nil { + didSet { + updateParameters() } - return _media } - var parameters:ChatMediaLayoutParameters? - - let gameTitleLayout:TextViewLayout? + private func updateParameters() { + parameters?.chatLocationInput = chatInteraction.chatLocationInput + parameters?.chatMode = chatInteraction.mode + + parameters?.getUpdatingMediaProgress = { [weak self] messageId in + if let media = self?.entry.additionalData.updatingMedia { + switch media.media { + case .update: + return .single(media.progress) + default: + break + } + } + return .single(nil) + } + + + + parameters?.cancelOperation = { [unowned context, weak self] message, media in + if self?.entry.additionalData.updatingMedia != nil { + context.account.pendingUpdateMessageManager.cancel(messageId: message.id) + } else if let media = media as? TelegramMediaFile { + messageMediaFileCancelInteractiveFetch(context: context, messageId: message.id, fileReference: FileMediaReference.message(message: MessageReference(message), media: media)) + if let resource = media.resource as? LocalFileArchiveMediaResource { + archiver.remove(.resource(resource)) + } + } else if let media = media as? TelegramMediaImage { + chatMessagePhotoCancelInteractiveFetch(account: context.account, photo: media) + } + } + } override var topInset:CGFloat { return 4 } + var mediaBubbleCornerInset: CGFloat { + return 1 + } + + override var bubbleFrame: NSRect { + var frame = super.bubbleFrame + + if isBubbleFullFilled { + frame.size.width = contentSize.width + additionBubbleInset + if hasBubble { + frame.size.width += self.mediaBubbleCornerInset * 2 + } + } + + return frame + } + + override var defaultContentTopOffset: CGFloat { + if isBubbled && !hasBubble { + return 2 + } + return isBubbled && !isBubbleFullFilled ? 14 : super.defaultContentTopOffset + } + + + + override var contentOffset: NSPoint { + var offset = super.contentOffset + // + if hasBubble { + if forwardNameLayout != nil { + offset.y += defaultContentInnerInset + } else if !isBubbleFullFilled { + offset.y += (defaultContentInnerInset + 2) + } + } + + if hasBubble && authorText == nil && replyModel == nil && forwardNameLayout == nil { + offset.y -= (defaultContentInnerInset + self.mediaBubbleCornerInset * 2 - (isBubbleFullFilled ? 1 : 0)) + } + return offset + } + + + override var elementsContentInset: CGFloat { + if hasBubble && isBubbleFullFilled { + return bubbleContentInset + } + return super.elementsContentInset + } + - override init(_ initialSize:NSSize, _ chatInteraction:ChatInteraction, _ account: Account, _ object: ChatHistoryEntry) { + override var _defaultHeight: CGFloat { + if hasBubble && isBubbleFullFilled && captionLayouts.isEmpty { + return contentOffset.y + defaultContentInnerInset - mediaBubbleCornerInset * 2 - 1 + } + + return super._defaultHeight + } + + override var realContentSize: NSSize { + var size = super.realContentSize - if case let .MessageEntry(message,_,_,_,_) = object { - _media = message.media[0] + if isBubbleFullFilled { + size.width -= bubbleContentInset * 2 + } + return size + } + + override var additionalLineForDateInBubbleState: CGFloat? { + if isForceRightLine { + return rightSize.height + } + if let file = self.media as? TelegramMediaFile, file.isEmojiAnimatedSticker { + return rightSize.height + 3 + } + if let caption = captionLayouts.last?.layout { + if let line = caption.lines.last, line.frame.width > realContentSize.width - (rightSize.width + insetBetweenContentAndDate) { + return rightSize.height + } + } + if postAuthor != nil { + return isStateOverlayLayout ? nil : rightSize.height + } + return super.additionalLineForDateInBubbleState + } + + override var isFixedRightPosition: Bool { + if media is TelegramMediaImage { + return true + } else if let media = media as? TelegramMediaFile { - if let media = _media as? TelegramMediaGame { - gameTitleLayout = TextViewLayout(.initialize(string: media.name, color: theme.colors.blueText, font: .medium(.text))) - } else { - gameTitleLayout = nil + if let captionLayout = captionLayouts.last?.layout, let line = captionLayout.lines.last, line.frame.width < realContentSize.width - (rightSize.width + insetBetweenContentAndDate) { + return true } + return media.isVideo || media.isAnimated || media.isVoice || media.isMusic || media.isStaticSticker || media.isAnimatedSticker + } + return super.isFixedRightPosition + } + + override var instantlyResize: Bool { + if !captionLayouts.isEmpty && media.isInteractiveMedia { + return true } else { - fatalError("no media for message") + return super.instantlyResize } + } + + + override var isBubbleFullFilled: Bool { + return (media.isInteractiveMedia || isSticker) && isBubbled + } + + var positionFlags: LayoutPositionFlags? = nil + + override init(_ initialSize:NSSize, _ chatInteraction:ChatInteraction, _ context: AccountContext, _ object: ChatHistoryEntry, _ downloadSettings: AutomaticMediaDownloadSettings, theme: TelegramPresentationTheme) { + + let message = object.message! + + let isIncoming: Bool = message.isIncoming(context.account, object.renderType == .bubble) + + media = message.media[0] + - super.init(initialSize, chatInteraction, account, object) + super.init(initialSize, chatInteraction, context, object, downloadSettings, theme: theme) + var canAddCaption: Bool = true + if let media = media as? TelegramMediaFile, media.isAnimatedSticker || media.isStaticSticker { + canAddCaption = false + } + if media is TelegramMediaDice { + canAddCaption = false + } - if let message = message, !message.text.isEmpty { + + let parameters = ChatMediaGalleryParameters(showMedia: { [weak self] message in + guard let `self` = self else {return} + + var type:GalleryAppearType = .history + if let parameters = self.parameters as? ChatMediaGalleryParameters, parameters.isWebpage { + type = .alone + } else if message.containsSecretMedia { + type = .secret + } + + showChatGallery(context: context, message: message, self.table, self.parameters, type: type, chatMode: self.chatInteraction.mode, contextHolder: self.chatInteraction.contextHolder()) + + }, showMessage: { [weak self] message in + self?.chatInteraction.focusMessageId(nil, message.id, .CenterEmpty) + }, isWebpage: chatInteraction.isLogInteraction, presentation: .make(for: message, account: context.account, renderType: object.renderType, theme: theme), media: media, automaticDownload: downloadSettings.isDownloable(message), autoplayMedia: object.autoplayMedia) + + self.parameters = parameters + + self.updateParameters() + + if !message.text.isEmpty, canAddCaption { + + + var caption:NSMutableAttributedString = NSMutableAttributedString() - NSAttributedString.initialize() - _ = caption.append(string: message.text, color: theme.colors.text, font: NSFont.normal(.custom(theme.fontSize))) + _ = caption.append(string: message.text, color: theme.chat.textColor(isIncoming, object.renderType == .bubble), font: .normal(theme.fontSize)) var types:ParsingType = [.Links, .Mentions, .Hashtags] if let peer = messageMainPeer(message) as? TelegramUser { @@ -171,119 +423,361 @@ class ChatMediaItem: ChatRowItem { break } } - if hasEntities { - caption = ChatMessageItem.applyMessageEntities(with: message.attributes, for: message.text.fixed, account:account, fontSize: theme.fontSize, openInfo:chatInteraction.openInfo, botCommand:chatInteraction.forceSendMessage, hashtag:chatInteraction.modalSearch, applyProxy: chatInteraction.applyProxy).mutableCopy() as! NSMutableAttributedString + var mediaDuration: Double? = nil + if let file = message.media.first as? TelegramMediaFile, file.isVideo && !file.isAnimated, let duration = file.duration { + mediaDuration = Double(duration) } - caption.detectLinks(type: types, account: account, openInfo:chatInteraction.openInfo, hashtag: chatInteraction.modalSearch, command: chatInteraction.forceSendMessage) - captionLayout = TextViewLayout(caption, alignment: .left) - captionLayout?.interactions = globalLinkExecutor - - } - self.parameters = ChatMediaGalleryParameters(showMedia: { - }, showMessage: { [weak self] message in - self?.chatInteraction.focusMessageId(nil, message.id, .center(id: 0, animated: true, focus: true, inset: 0)) - }, isWebpage: chatInteraction.isLogInteraction) + caption = ChatMessageItem.applyMessageEntities(with: message.attributes, for: message.text.fixed, message: message, context: context, fontSize: theme.fontSize, openInfo:chatInteraction.openInfo, botCommand:chatInteraction.sendPlainText, hashtag: chatInteraction.modalSearch, applyProxy: chatInteraction.applyProxy, textColor: theme.chat.textColor(isIncoming, object.renderType == .bubble), linkColor: theme.chat.linkColor(isIncoming, object.renderType == .bubble), monospacedPre: theme.chat.monospacedPreColor(isIncoming, entry.renderType == .bubble), monospacedCode: theme.chat.monospacedCodeColor(isIncoming, entry.renderType == .bubble), mediaDuration: mediaDuration, timecode: { [weak self] timecode in + self?.parameters?.set_timeCodeInitializer(timecode) + self?.parameters?.showMedia(message) + }, openBank: chatInteraction.openBank).mutableCopy() as! NSMutableAttributedString + + + if !hasEntities || message.flags.contains(.Failed) || message.flags.contains(.Unsent) || message.flags.contains(.Sending) { + caption.detectLinks(type: types, context: context, color: theme.chat.linkColor(isIncoming, object.renderType == .bubble), openInfo:chatInteraction.openInfo, hashtag: context.sharedContext.bindings.globalSearch, command: chatInteraction.sendPlainText, applyProxy: chatInteraction.applyProxy) + } + if !(self is ChatVideoMessageItem) { + captionLayouts = [.init(id: message.stableId, offset: CGPoint(x: 0, y: 0), layout: TextViewLayout(caption, alignment: .left, selectText: theme.chat.selectText(isIncoming, object.renderType == .bubble), strokeLinks: object.renderType == .bubble, alwaysStaticItems: true, disableTooltips: false))] + } + + let interactions = globalLinkExecutor + + interactions.copyToClipboard = { text in + copyToClipboard(text) + context.sharedContext.bindings.rootNavigation().controller.show(toaster: ControllerToaster(text: L10n.shareLinkCopied)) + } + for textLayout in self.captionLayouts.map ({ $0.layout }) { + textLayout.interactions = interactions + if let highlightFoundText = entry.additionalData.highlightFoundText { + if highlightFoundText.isMessage { + if let range = rangeOfSearch(highlightFoundText.query, in: caption.string) { + textLayout.additionalSelections = [TextSelectedRange(range: range, color: theme.colors.accentIcon.withAlphaComponent(0.5), def: false)] + } + } else { + var additionalSelections:[TextSelectedRange] = [] + let string = caption.string.lowercased().nsstring + var searchRange = NSMakeRange(0, string.length) + var foundRange:NSRange = NSMakeRange(NSNotFound, 0) + while (searchRange.location < string.length) { + searchRange.length = string.length - searchRange.location + foundRange = string.range(of: highlightFoundText.query.lowercased(), options: [], range: searchRange) + if (foundRange.location != NSNotFound) { + additionalSelections.append(TextSelectedRange(range: foundRange, color: theme.colors.grayIcon.withAlphaComponent(0.5), def: false)) + searchRange.location = foundRange.location+foundRange.length; + } else { + break + } + } + textLayout.additionalSelections = additionalSelections + } + } + } + } + + if isBubbleFullFilled { + var positionFlags: LayoutPositionFlags = [] + if captionLayouts.isEmpty && commentsBubbleData == nil { + positionFlags.insert(.bottom) + positionFlags.insert(.left) + positionFlags.insert(.right) + } + if authorText == nil && replyModel == nil && forwardNameLayout == nil { + positionFlags.insert(.top) + positionFlags.insert(.left) + positionFlags.insert(.right) + } + self.positionFlags = positionFlags + } } + func openMedia(_ timemark: Int32? = nil) { + if let message = self.message { + if let timemark = timemark { + self.parameters?.set_timeCodeInitializer(Double(timemark)) + } + self.parameters?.showMedia(message) + } + } + override func makeSize(_ width: CGFloat, oldWidth:CGFloat) -> Bool { return super.makeSize(width, oldWidth: oldWidth) } override func makeContentSize(_ width: CGFloat) -> NSSize { - gameTitleLayout?.measure(width: width) - if let gameTitleLayout = gameTitleLayout { - var contentSize = ChatLayoutUtils.contentSize(for: media, with: width) - contentSize.height += gameTitleLayout.layoutSize.height + 6 - return contentSize - } else { - return ChatLayoutUtils.contentSize(for: media, with: width) - } + let size = ChatLayoutUtils.contentSize(for: media, with: width, hasText: message?.text.isEmpty == false || (isBubbled && (commentsBubbleData != nil || message?.isImported == true))) + return size } - override func menuItems() -> Signal<[ContextMenuItem], Void> { - - if self.chatInteraction.isLogInteraction { - return .single([]) + override func menuItems(in location: NSPoint) -> Signal<[ContextMenuItem], NoError> { + var items:Signal<[ContextMenuItem], NoError> = .complete() + if let message = message { + items = chatMenuItems(for: message, item: self, chatInteraction: chatInteraction) } - - let signal = super.menuItems() - if let account = account { - if let file = self.media as? TelegramMediaFile { - return signal |> mapToSignal { items -> Signal<[ContextMenuItem], Void> in - var items = items - return account.postbox.mediaBox.resourceData(file.resource) |> deliverOnMainQueue |> mapToSignal { data in - if data.complete { - items.append(ContextMenuItem(tr(.contextCopyMedia), handler: { - saveAs(file, account: account) - })) - } - - if file.isSticker, let fileId = file.id { - return account.postbox.modify { modifier -> [ContextMenuItem] in - let saved = getIsStickerSaved(modifier: modifier, fileId: fileId) - items.append(ContextMenuItem( !saved ? tr(.chatContextAddFavoriteSticker) : tr(.chatContextRemoveFavoriteSticker), handler: { - - if !saved { - _ = addSavedSticker(postbox: account.postbox, network: account.network, file: file).start() - } else { - _ = removeSavedSticker(postbox: account.postbox, mediaId: fileId).start() - } - })) - - return items + return items |> map { [weak self] items in + var items = items + if let captionLayout = self?.captionLayouts.first(where: { $0.id == self?.lastMessage?.stableId }) { + let text = captionLayout.layout.attributedString.string + items.insert(ContextMenuItem(L10n.textCopyText, handler: { + copyToClipboard(text) + }), at: min(items.count, 1)) + + if let view = self?.view as? ChatRowView, let textView = view.captionViews.first(where: { $0.id == self?.lastMessage?.stableId})?.view, let window = textView.window { + let point = textView.convert(window.mouseLocationOutsideOfEventStream, from: nil) + if let layout = textView.layout { + if let (link, _, range, _) = layout.link(at: point) { + var text:String = layout.attributedString.string.nsstring.substring(with: range) + if let link = link as? inAppLink { + if case let .external(link, _) = link { + text = link + } } - } - - return .single(items) - } - } - } else if let image = self.media as? TelegramMediaImage { - - return signal |> mapToSignal { items -> Signal<[ContextMenuItem], Void> in - var items = items - if let resource = image.representations.last?.resource { - return account.postbox.mediaBox.resourceData(resource) |> take(1) |> deliverOnMainQueue |> map { data in - if data.complete { - items.append(ContextMenuItem(tr(.galleryContextCopyToClipboard), handler: { - if let path = link(path: data.path, ext: "jpg") { - let pb = NSPasteboard.general - pb.clearContents() - pb.writeObjects([NSURL(fileURLWithPath: path)]) - } - })) - items.append(ContextMenuItem(tr(.contextCopyMedia), handler: { - savePanel(file: data.path, ext: "jpg", for: mainWindow) - })) + + for i in 0 ..< items.count { + if items[i].title == tr(L10n.messageContextCopyMessageLink1) { + items.remove(at: i) + break + } } - return items + + items.insert(ContextMenuItem(tr(L10n.messageContextCopyMessageLink1), handler: { + copyToClipboard(text) + }), at: 1) } - } else { - return .single(items) } } + } - + if let media = self?.media as? TelegramMediaFile, media.isMusic, let name = media.fileName { + items.insert(ContextMenuItem(L10n.messageTextCopyMusicTitle, handler: { + copyToClipboard(name) + }), at: 1) + } + return items } - - return signal + } + + override func canMultiselectTextIn(_ location: NSPoint) -> Bool { + if let view = view as? ChatMediaView, let content = view.contentNode { + let point = view.contentView.convert(location, from: nil) + return !NSPointInRect(point, content.frame) + } + return false } override var identifier: String { - return super.identifier + "\(stableId)" + return super.identifier } public func contentNode() -> ChatMediaContentView.Type { + if let file = media as? TelegramMediaFile, message?.id.peerId.namespace == Namespaces.Peer.SecretChat, file.isAnimatedSticker, file.stickerReference == nil { + return ChatFileContentView.self + } return ChatLayoutUtils.contentNode(for: media) } override func viewClass() -> AnyClass { - if _media is TelegramMediaGame { - return ChatMediaGameView.self + return ChatMediaView.self + } + + var isPinchable: Bool { + return contentNode() == ChatInteractiveContentView.self || contentNode() == ChatGIFContentView.self + } +} + + + +class ChatMediaView: ChatRowView, ModalPreviewRowViewProtocol { + + private var pinchToZoom: PinchToZoom? + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + pinchToZoom = PinchToZoom(parentView: contentView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + func fileAtPoint(_ point: NSPoint) -> (QuickPreviewMedia, NSView?)? { + if let contentNode = contentNode { + if contentNode is ChatStickerContentView { + if let file = contentNode.media as? TelegramMediaFile { + let reference = contentNode.parent != nil ? FileMediaReference.message(message: MessageReference(contentNode.parent!), media: file) : FileMediaReference.standalone(media: file) + return (.file(reference, StickerPreviewModalView.self), contentNode) + } + } else if contentNode is ChatGIFContentView { + if let file = contentNode.media as? TelegramMediaFile { + let reference = contentNode.parent != nil ? FileMediaReference.message(message: MessageReference(contentNode.parent!), media: file) : FileMediaReference.standalone(media: file) + return (.file(reference, GifPreviewModalView.self), contentNode) + } + } else if contentNode is ChatInteractiveContentView { + if let image = contentNode.media as? TelegramMediaImage { + let reference = contentNode.parent != nil ? ImageMediaReference.message(message: MessageReference(contentNode.parent!), media: image) : ImageMediaReference.standalone(media: image) + return (.image(reference, ImagePreviewModalView.self), contentNode) + } else if let file = contentNode.media as? TelegramMediaFile { + let reference = contentNode.parent != nil ? FileMediaReference.message(message: MessageReference(contentNode.parent!), media: file) : FileMediaReference.standalone(media: file) + return (.file(reference, VideoPreviewModalView.self), contentNode) + } + } else if contentNode is ChatFileContentView { + if let file = contentNode.media as? TelegramMediaFile, file.isGraphicFile, let mediaId = file.id, let dimension = file.dimensions { + var representations: [TelegramMediaImageRepresentation] = [] + representations.append(contentsOf: file.previewRepresentations) + representations.append(TelegramMediaImageRepresentation(dimensions: dimension, resource: file.resource, progressiveSizes: [], immediateThumbnailData: nil)) + let image = TelegramMediaImage(imageId: mediaId, representations: representations, immediateThumbnailData: file.immediateThumbnailData, reference: nil, partialReference: file.partialReference, flags: []) + let reference = contentNode.parent != nil ? ImageMediaReference.message(message: MessageReference(contentNode.parent!), media: image) : ImageMediaReference.standalone(media: image) + return (.image(reference, ImagePreviewModalView.self), contentNode) + } + } else if contentNode is MediaAnimatedStickerView { + if let file = contentNode.media as? TelegramMediaFile { + let reference = contentNode.parent != nil ? FileMediaReference.message(message: MessageReference(contentNode.parent!), media: file) : FileMediaReference.standalone(media: file) + return (.file(reference, AnimatedStickerPreviewModalView.self), contentNode) + } + } + } + + return nil + } + + override func previewMediaIfPossible() -> Bool { + + return contentNode?.previewMediaIfPossible() ?? false + } + + override func forceClick(in location: NSPoint) { + + if contentNode?.mouseInside() == true { + let result = previewMediaIfPossible() + if !result { + super.forceClick(in: location) + } } else { - return ChatMediaView.self + super.forceClick(in: location) + } + + } + + + fileprivate(set) var contentNode:ChatMediaContentView? + + override var needsDisplay: Bool { + get { + return super.needsDisplay + } + set { + super.needsDisplay = true + contentNode?.needsDisplay = true + } + } + + override var backgroundColor: NSColor { + didSet { + + contentNode?.backgroundColor = contentColor + } + } + + override func shakeView() { + contentNode?.shake() + } + + + override func updateMouse() { + super.updateMouse() + self.contentNode?.updateMouse() + } + + override func contentFrame(_ item: ChatRowItem) -> NSRect { + var rect = super.contentFrame(item) + guard let item = item as? ChatMediaItem else { + return rect + } + if item.isBubbled, item.isBubbleFullFilled { + rect.origin.x -= item.bubbleContentInset + if item.hasBubble { + rect.origin.x += item.mediaBubbleCornerInset + } + } + + return rect + } + + override func viewWillMove(toSuperview newSuperview: NSView?) { + if newSuperview == nil { + self.contentNode?.willRemove() } } + override func draw(_ dirtyRect: NSRect) { + + } + + override func set(item:TableRowItem, animated:Bool = false) { + if let item:ChatMediaItem = item as? ChatMediaItem { + if contentNode == nil || !contentNode!.isKind(of: item.contentNode()) { + self.contentNode?.removeFromSuperview() + let node = item.contentNode() + self.contentNode = node.init(frame:NSZeroRect) + self.addSubview(self.contentNode!) + + } + + self.contentNode?.update(with: item.media, size: item.contentSize, context: item.context, parent:item.message, table:item.table, parameters:item.parameters, animated: animated, positionFlags: item.positionFlags, approximateSynchronousValue: item.approximateSynchronousValue) + + if item.isPinchable { + self.pinchToZoom?.add(to: contentNode!, size: item.contentSize) + } else { + self.pinchToZoom?.remove() + } + } + super.set(item: item, animated: animated) + } + + open override func interactionContentView(for innerId: AnyHashable, animateIn: Bool ) -> NSView { + if let content = self.contentNode?.interactionContentView(for: innerId, animateIn: animateIn) { + return content + } + return self + } + + override func videoTimebase(for innerId: AnyHashable) -> CMTimebase? { + return self.contentNode?.videoTimebase() + } + override func applyTimebase(for stableId: AnyHashable, timebase: CMTimebase?) { + self.contentNode?.applyTimebase(timebase: timebase) + } + + override func interactionControllerDidFinishAnimation(interactive: Bool, innerId: AnyHashable) { + + if interactive { + self.contentNode?.interactionControllerDidFinishAnimation(interactive: interactive) + } + } + + override func addAccesoryOnCopiedView(innerId: AnyHashable, view: NSView) { + guard let item = item as? ChatRowItem, let contentNode = contentNode else {return} + + + + + let rightView = ChatRightView(frame: NSZeroRect) + rightView.set(item: item, animated: false) + var rect = self.rightView.convert(self.rightView.bounds, to: contentNode) + + if contentNode.visibleRect.minY < rect.midY && contentNode.visibleRect.minY + contentNode.visibleRect.height > rect.midY { + rect.origin.y = contentNode.frame.height - rect.maxY + rightView.frame = rect + view.addSubview(rightView) + } + + + contentNode.addAccesoryOnCopiedView(view: view) + } + } + + + diff --git a/Telegram-Mac/ChatMediaView.swift b/Telegram-Mac/ChatMediaView.swift deleted file mode 100644 index 3ba8f5afd8..0000000000 --- a/Telegram-Mac/ChatMediaView.swift +++ /dev/null @@ -1,134 +0,0 @@ -// -// ChatMediaView.swift -// Telegram-Mac -// -// Created by keepcoder on 18/09/16. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import TGUIKit -import TelegramCoreMac -class ChatMediaView: ChatRowView { - - var contentNode:ChatMediaContentView? - - override var needsDisplay: Bool { - get { - return super.needsDisplay - } - set { - super.needsDisplay = true - contentNode?.needsDisplay = true - } - } - - override func draw(_ dirtyRect: NSRect) { - - } - - override func set(item:TableRowItem, animated:Bool = false) { - if let item:ChatMediaItem = item as? ChatMediaItem { - if contentNode == nil || !contentNode!.isKind(of: item.contentNode()) { - self.contentNode?.removeFromSuperview() - let node = item.contentNode() - self.contentNode = node.init(frame:NSZeroRect) - self.addSubview(self.contentNode!) - } - - self.contentNode?.update(with: item.media, size: item.contentSize, account: item.account!, parent:item.message, table:item.table, parameters:item.parameters, animated: animated) - } - super.set(item: item, animated: animated) - } - - open override var interactionContentView:NSView { - if let content = self.contentNode?.interactionContentView { - return content - } - return self - } - - - override var backgroundColor: NSColor { - didSet { - contentNode?.backgroundColor = backdorColor - } - } - - override func viewWillMove(toSuperview newSuperview: NSView?) { - if newSuperview == nil { - self.contentNode?.willRemove() - } - } - -} - - -class ChatMediaGameView: ChatRowView { - - var contentNode:ChatMediaContentView? - private let title:TextView = TextView() - - required init(frame frameRect: NSRect) { - super.init(frame: frameRect) - - let layout = TextViewLayout(.initialize(string: "supergame", color: .blueUI, font: .normal(.text))) - layout.measure(width: 1000) - title.update(layout) - title.userInteractionEnabled = false - addSubview(title) - } - - - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func draw(_ dirtyRect: NSRect) { - - } - - override func layout() { - super.layout() - - if let item = item as? ChatMediaItem { - title.update(item.gameTitleLayout) - } - } - - override func set(item:TableRowItem, animated:Bool = false) { - if let item:ChatMediaItem = item as? ChatMediaItem { - if contentNode == nil || !contentNode!.isKind(of: item.contentNode()) { - self.contentNode?.removeFromSuperview() - let node = item.contentNode() - self.contentNode = node.init(frame:NSZeroRect) - self.addSubview(self.contentNode!) - } - self.contentNode?.userInteractionEnabled = false - self.contentNode?.update(with: item.media, size: NSMakeSize(item.contentSize.width, item.contentSize.height - item.gameTitleLayout!.layoutSize.height - 6), account: item.account!, parent:item.message, table:item.table, parameters:item.parameters) - - title.update(item.gameTitleLayout) - - self.contentNode?.setFrameOrigin(0, item.gameTitleLayout!.layoutSize.height + 6) - } - super.set(item: item, animated: animated) - } - - - override var backgroundColor: NSColor { - didSet { - contentNode?.backgroundColor = backgroundColor - } - } - - override func viewWillMove(toSuperview newSuperview: NSView?) { - if newSuperview == nil { - self.contentNode?.willRemove() - } - } - -} - - - diff --git a/Telegram-Mac/ChatMessageAccessoryView.swift b/Telegram-Mac/ChatMessageAccessoryView.swift new file mode 100644 index 0000000000..b96f0fda21 --- /dev/null +++ b/Telegram-Mac/ChatMessageAccessoryView.swift @@ -0,0 +1,259 @@ +// +// ChatMessageAccessoryView.swift +// Telegram +// +// Created by keepcoder on 05/10/2017. +// Copyright © 2017 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox + + + +class ChatMessageAccessoryView: Control { + + private let textView:TextView = TextView() + private let backgroundView = View() + private var maxWidth: CGFloat = 0 + private let unread = View() + private var stringValue: String = "" + private let progress: RadialProgressView = RadialProgressView(theme: RadialProgressTheme(backgroundColor: .clear, foregroundColor: .white, cancelFetchingIcon: stopFetchStreamableControl), twist: true, size: NSMakeSize(24, 24)) + private let bufferingIndicator: ProgressIndicator = ProgressIndicator(frame: NSMakeRect(0, 0, 10, 10)) + private let download: ImageButton = ImageButton(frame: NSMakeRect(0, 0, 24, 24)) + + private var status: MediaResourceStatus? + private var isStreamable: Bool = true + private var isCompact: Bool = false + + private var imageView: ImageView? + + var soundOffOnImage: CGImage? { + didSet { + if let soundOffOnImage = soundOffOnImage { + if imageView == nil { + imageView = ImageView() + imageView?.animates = true + addSubview(imageView!) + } + imageView?.image = soundOffOnImage + imageView?.sizeToFit() + } else { + imageView?.removeFromSuperview() + imageView = nil + } + } + } + + private let progressCap: View = View() + var isUnread: Bool = false + override func draw(_ layer: CALayer, in ctx: CGContext) { + + } + + + override func layout() { + super.layout() + download.centerY(x: 6) + progress.centerY(x: 6) + backgroundView.frame = bounds + + bufferingIndicator.centerY(x: frame.width - bufferingIndicator.frame.width - 7) + if let imageView = imageView { + imageView.centerY(x: frame.width - imageView.frame.width - 6) + } + + if let textLayout = textView.layout { + var rect = focus(textLayout.layoutSize) + rect.origin.x = 6 + if hasStremingControls { + rect.origin.x += download.frame.width + 6 + } + if backingScaleFactor == 2 { + rect.origin.y += 0.5 + } else { + rect.origin.y += 1 + } + textView.frame = rect + + unread.centerY(x: rect.maxX + 2) + + } + + } + + var hasStremingControls: Bool { + return !download.isHidden || !progress.isHidden + } + + private var fetch:(()->Void)? + private var cancelFetch:(()->Void)? + private var click:(()->Void)? + + private var isVideoMessage: Bool = false + + func updateText(_ text: String, maxWidth: CGFloat, status: MediaResourceStatus?, isStreamable: Bool, isCompact: Bool = false, soundOffOnImage: CGImage? = nil, isBuffering: Bool = false, isUnread: Bool = false, animated: Bool = false, isVideoMessage: Bool = false, fetch: @escaping()-> Void = { }, cancelFetch: @escaping()-> Void = { }, click: @escaping()-> Void = { }) -> Void { + + + let animated = animated && self.isCompact != isCompact + + let updatedText = TextViewLayout(.initialize(string: isStreamable ? text.components(separatedBy: ", ").joined(separator: "\n") : text, color: isVideoMessage ? theme.chatServiceItemTextColor : .white, font: .normal(10.0)), maximumNumberOfLines: isStreamable && !isCompact ? 2 : 1, truncationType: .end, alwaysStaticItems: true) //TextNode.layoutText(maybeNode: textNode, .initialize(string: isStreamable ? text.components(separatedBy: ", ").joined(separator: "\n") : text, color: isVideoMessage ? theme.chatServiceItemTextColor : .white, font: .normal(10.0)), nil, isStreamable && !isCompact ? 2 : 1, .end, NSMakeSize(maxWidth, 20), nil, false, .left) + updatedText.measure(width: maxWidth) + textView.update(updatedText) + + backgroundView.backgroundColor = isVideoMessage ? theme.chatServiceItemColor : .blackTransparent + + + self.isStreamable = isStreamable + self.status = status + self.stringValue = text + self.maxWidth = maxWidth + self.fetch = fetch + self.isCompact = isCompact + self.cancelFetch = cancelFetch + self.click = click + self.soundOffOnImage = soundOffOnImage + self.isUnread = isUnread + + self.bufferingIndicator.isHidden = !isBuffering + self.unread.isHidden = !isUnread + + if let status = status, isStreamable { + + download.set(image: isCompact ? theme.icons.videoCompactFetching : theme.icons.streamingVideoDownload, for: .Normal) + + + switch status { + case .Remote: + progress.isHidden = true + download.isHidden = false + progress.state = .None + case .Local: + progress.isHidden = true + download.isHidden = true + progress.state = .None + case let .Fetching(_, progress): + self.progress.state = !isCompact ? .Fetching(progress: progress, force: false) : .None + self.progress.isHidden = isCompact + download.isHidden = !isCompact + download.set(image: isCompact ? theme.icons.compactStreamingFetchingCancel : theme.icons.streamingVideoDownload, for: .Normal) + } + if isCompact { + download.setFrameSize(10, 10) + } else { + download.setFrameSize(28, 28) + + } + } else { + progress.isHidden = true + download.isHidden = true + progress.state = .None + } + + let newSize = NSMakeSize(min(max(soundOffOnImage != nil ? 30 : updatedText.layoutSize.width, updatedText.layoutSize.width) + 12 + (isUnread ? 8 : 0) + (hasStremingControls ? download.frame.width + 6 : 0) + (soundOffOnImage != nil ? soundOffOnImage!.backingSize.width + 2 : 0) + (isBuffering ? bufferingIndicator.frame.width + 4 : 0), maxWidth), hasStremingControls && !isCompact ? 36 : updatedText.layoutSize.height + 6) + change(size: newSize, animated: animated) + backgroundView.change(size: newSize, animated: animated) + + + backgroundView.layer?.cornerRadius = isStreamable ? 8 : newSize.height / 2 + + + var rect = focus(updatedText.layoutSize) + rect.origin.x = 6 + if hasStremingControls { + rect.origin.x += download.frame.width + 6 + } + if backingScaleFactor == 2 { + rect.origin.y += 0.5 + } + textView.change(pos: rect.origin, animated: animated) + + if animated, let layer = backgroundView.layer { + let cornerAnimation = CABasicAnimation(keyPath: "cornerRadius") + cornerAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut) + cornerAnimation.fromValue = layer.presentation()?.cornerRadius ?? layer.cornerRadius + cornerAnimation.toValue = isStreamable ? 8 : newSize.height / 2 + cornerAnimation.duration = 0.2 + layer.add(cornerAnimation, forKey: "cornerRadius") + } + + needsLayout = true + } + + override func copy() -> Any { + let view = ChatMessageAccessoryView(frame: frame) + view.updateText(self.stringValue, maxWidth: self.maxWidth, status: self.status, isStreamable: self.isStreamable, isCompact: self.isCompact) + return view + } + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + + + unread.setFrameSize(NSMakeSize(6, 6)) + unread.layer?.cornerRadius = 3 + unread.backgroundColor = isVideoMessage ? theme.chatServiceItemTextColor : .white + textView.isSelectable = false + textView.userInteractionEnabled = false + textView.disableBackgroundDrawing = true + + addSubview(backgroundView) + addSubview(textView) + addSubview(progress) + addSubview(download) + addSubview(unread) + bufferingIndicator.background = .clear + bufferingIndicator.progressColor = isVideoMessage ? theme.chatServiceItemTextColor : .white + bufferingIndicator.layer?.cornerRadius = bufferingIndicator.frame.height / 2 +// bufferingIndicator.lineWidth = 1.0 + bufferingIndicator.isHidden = true + progress.isHidden = true + download.isHidden = true + download.autohighlight = false + progress.fetchControls = FetchControls(fetch: { [weak self] in + self?.cancelFetch?() + }) + + progressCap.layer?.borderColor = NSColor.white.withAlphaComponent(0.3).cgColor + progressCap.layer?.borderWidth = 2.0 + progressCap.frame = NSMakeRect(2, 2, progress.frame.width - 4, progress.frame.height - 4) + progressCap.layer?.cornerRadius = progressCap.frame.width / 2 + + progress.addSubview(progressCap) + + addSubview(bufferingIndicator) + + + download.set(handler: { [weak self] _ in + guard let `self` = self, let status = self.status else {return} + switch status { + case .Remote: + self.fetch?() + case .Fetching: + self.cancelFetch?() + default: + break + } + }, for: .Click) + + set(handler: { [weak self] _ in + guard let `self` = self, let status = self.status else {return} + switch status { + case .Remote: + self.fetch?() + case .Fetching: + self.cancelFetch?() + default: + self.click?() + } + }, for: .Click) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/Telegram-Mac/ChatMessageBubbleImages.swift b/Telegram-Mac/ChatMessageBubbleImages.swift new file mode 100644 index 0000000000..eb1474df11 --- /dev/null +++ b/Telegram-Mac/ChatMessageBubbleImages.swift @@ -0,0 +1,169 @@ +// +// ChatMessageBubbleImages.swift +// Telegram +// +// Created by keepcoder on 04/12/2017. +// Copyright © 2017 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + + + +enum MessageBubbleImageNeighbors { + case none + case top + case bottom + case both +} + +func messageSingleBubbleLikeImage(fillColor: NSColor, strokeColor: NSColor) -> CGImage { + let diameter: CGFloat = 36.0 + return generateImage(CGSize(width: 36.0, height: diameter), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + let lineWidth: CGFloat = 0.5 + + context.setFillColor(strokeColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + context.setFillColor(fillColor.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: lineWidth, y: lineWidth), size: CGSize(width: size.width - lineWidth * 2.0, height: size.height - lineWidth * 2.0))) + })! +} + + +func messageBubbleImageModern(incoming: Bool, fillColor: NSColor, strokeColor: NSColor, neighbors: MessageBubbleImageNeighbors, mask: Bool = false) -> (CGImage, NSEdgeInsets) { + + let diameter: CGFloat = 36.0 + let corner: CGFloat = 7.0 + + let image = generateImage(CGSize(width: 42.0, height: diameter), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + let additionalOffset: CGFloat + switch neighbors { + case .none, .bottom: + additionalOffset = 0.0 + case .both, .top: + additionalOffset = 6.0 + } + + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: incoming ? 1.0 : -1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0 + 0.5 + additionalOffset, y: -size.height / 2.0 + 0.5) + + let lineWidth: CGFloat = 1.0 + + if mask { + context.setBlendMode(.copy) + context.setFillColor(NSColor.clear.cgColor) + context.setStrokeColor(NSColor.clear.cgColor) + } else { + context.setFillColor(fillColor.cgColor) + context.setLineWidth(lineWidth) + context.setStrokeColor(strokeColor.cgColor) + } + + + switch neighbors { + case .none: + let _ = try? drawSvgPath(context, path: "M6,17.5 C6,7.83289181 13.8350169,0 23.5,0 C33.1671082,0 41,7.83501688 41,17.5 C41,27.1671082 33.1649831,35 23.5,35 C19.2941198,35 15.4354328,33.5169337 12.4179496,31.0453367 C9.05531719,34.9894816 -2.41102995e-08,35 0,35 C5.972003,31.5499861 6,26.8616169 6,26.8616169 L6,17.5 L6,17.5 ") + context.strokePath() + let _ = try? drawSvgPath(context, path: "M6,17.5 C6,7.83289181 13.8350169,0 23.5,0 C33.1671082,0 41,7.83501688 41,17.5 C41,27.1671082 33.1649831,35 23.5,35 C19.2941198,35 15.4354328,33.5169337 12.4179496,31.0453367 C9.05531719,34.9894816 -2.41102995e-08,35 0,35 C5.972003,31.5499861 6,26.8616169 6,26.8616169 L6,17.5 L6,17.5 ") + context.fillPath() + case .top: + let _ = try? drawSvgPath(context, path: "M35,17.5 C35,7.83501688 27.1671082,0 17.5,0 L17.5,0 C7.83501688,0 0,7.83289181 0,17.5 L0,29.0031815 C0,32.3151329 2.6882755,35 5.99681848,35 L17.5,35 C27.1649831,35 35,27.1671082 35,17.5 L35,17.5 L35,17.5 ") + context.strokePath() + let _ = try? drawSvgPath(context, path: "M35,17.5 C35,7.83501688 27.1671082,0 17.5,0 L17.5,0 C7.83501688,0 0,7.83289181 0,17.5 L0,29.0031815 C0,32.3151329 2.6882755,35 5.99681848,35 L17.5,35 C27.1649831,35 35,27.1671082 35,17.5 L35,17.5 L35,17.5 ") + context.fillPath() + case .bottom: + let _ = try? drawSvgPath(context, path: "M6,17.5 L6,5.99681848 C6,2.6882755 8.68486709,0 11.9968185,0 L23.5,0 C33.1671082,0 41,7.83501688 41,17.5 C41,27.1671082 33.1649831,35 23.5,35 C19.2941198,35 15.4354328,33.5169337 12.4179496,31.0453367 C9.05531719,34.9894816 -2.41103066e-08,35 0,35 C5.972003,31.5499861 6,26.8616169 6,26.8616169 L6,17.5 L6,17.5 ") + context.strokePath() + let _ = try? drawSvgPath(context, path: "M6,17.5 L6,5.99681848 C6,2.6882755 8.68486709,0 11.9968185,0 L23.5,0 C33.1671082,0 41,7.83501688 41,17.5 C41,27.1671082 33.1649831,35 23.5,35 C19.2941198,35 15.4354328,33.5169337 12.4179496,31.0453367 C9.05531719,34.9894816 -2.41103066e-08,35 0,35 C5.972003,31.5499861 6,26.8616169 6,26.8616169 L6,17.5 L6,17.5 ") + context.fillPath() + case .both: + + let _ = try? drawSvgPath(context, path: "M17.5,0 C27.1649831,1.94289029e-15 35,7.83501688 35,17.5 C35,27.1649831 27.1649831,35 17.5,35 C7.83501688,35 0,27.1649831 0,17.5 C3.88578059e-15,7.83501688 7.83501688,-1.94289029e-15 17.5,0 ") + context.strokePath() + + let _ = try? drawSvgPath(context, path: "M17.5,0 C27.1649831,1.94289029e-15 35,7.83501688 35,17.5 C35,27.1649831 27.1649831,35 17.5,35 C7.83501688,35 0,27.1649831 0,17.5 C3.88578059e-15,7.83501688 7.83501688,-1.94289029e-15 17.5,0 ") + context.fillPath() + + } + + })! + + + let leftCapWidth: CGFloat = CGFloat(incoming ? Int(corner + diameter / 2.0) : Int(diameter / 2.0)) + let topCapHeight: CGFloat = diameter / 2.0 + let rightCapWidth: CGFloat = image.backingSize.width - leftCapWidth - 1.0 + let bottomCapHeight: CGFloat = image.backingSize.height - topCapHeight - 1.0 + + return (image, NSEdgeInsetsMake(topCapHeight, leftCapWidth, bottomCapHeight, rightCapWidth)) +} + + +func drawNinePartImage(_ context: CGContext, frame: NSRect, topLeftCorner: CGImage, topEdgeFill: CGImage, topRightCorner: CGImage, leftEdgeFill: CGImage, centerFill: CGImage, rightEdgeFill: CGImage, bottomLeftCorner: CGImage, bottomEdgeFill: CGImage, bottomRightCorner: CGImage){ + + let imageWidth: CGFloat = frame.size.width; + let imageHeight: CGFloat = frame.size.height; + + let leftCapWidth: CGFloat = topLeftCorner.backingSize.width; + let topCapHeight: CGFloat = topLeftCorner.backingSize.height; + let rightCapWidth: CGFloat = bottomRightCorner.backingSize.width; + let bottomCapHeight: CGFloat = bottomRightCorner.backingSize.height; + + let centerSize = NSMakeSize(imageWidth - leftCapWidth - rightCapWidth, imageHeight - topCapHeight - bottomCapHeight); + + let topLeftCornerRect: NSRect = NSMakeRect(0.0, imageHeight - topCapHeight, leftCapWidth, topCapHeight); + let topEdgeFillRect: NSRect = NSMakeRect(leftCapWidth, imageHeight - topCapHeight, centerSize.width, topCapHeight); + let topRightCornerRect: NSRect = NSMakeRect(imageWidth - rightCapWidth, imageHeight - topCapHeight, rightCapWidth, topCapHeight); + + let leftEdgeFillRect: NSRect = NSMakeRect(0.0, bottomCapHeight, leftCapWidth, centerSize.height); + let centerFillRect: NSRect = NSMakeRect(leftCapWidth, bottomCapHeight, centerSize.width, centerSize.height); + let rightEdgeFillRect: NSRect = NSMakeRect(imageWidth - rightCapWidth, bottomCapHeight, rightCapWidth, centerSize.height); + + let bottomLeftCornerRect: NSRect = NSMakeRect(0.0, 0.0, leftCapWidth, bottomCapHeight); + let bottomEdgeFillRect: NSRect = NSMakeRect(leftCapWidth, 0.0, centerSize.width, bottomCapHeight); + let bottomRightCornerRect: NSRect = NSMakeRect(imageWidth - rightCapWidth, 0.0, rightCapWidth, bottomCapHeight); + + + drawStretchedImageInRect(topLeftCorner, context: context, rect: topLeftCornerRect); + drawStretchedImageInRect(topEdgeFill, context: context, rect: topEdgeFillRect); + drawStretchedImageInRect(topRightCorner, context: context, rect: topRightCornerRect); + + drawStretchedImageInRect(leftEdgeFill, context: context, rect: leftEdgeFillRect); + drawStretchedImageInRect(centerFill, context: context, rect: centerFillRect); + drawStretchedImageInRect(rightEdgeFill, context: context, rect: rightEdgeFillRect); + + drawStretchedImageInRect(bottomLeftCorner, context: context, rect: bottomLeftCornerRect); + drawStretchedImageInRect(bottomEdgeFill, context: context, rect: bottomEdgeFillRect); + drawStretchedImageInRect(bottomRightCorner, context: context, rect: bottomRightCornerRect); + +} + + +func imageByReferencingRectOfExistingImage(_ image: CGImage, _ rect: NSRect) -> CGImage { + if (!NSIsEmptyRect(rect)){ + + let pixelsHigh = CGFloat(image.height) + + let scaleFactor:CGFloat = pixelsHigh / image.backingSize.height + var captureRect = NSMakeRect(scaleFactor * rect.origin.x, scaleFactor * rect.origin.y, scaleFactor * rect.size.width, scaleFactor * rect.size.height) + + captureRect.origin.y = pixelsHigh - captureRect.origin.y - captureRect.size.height; + + return image.cropping(to: captureRect)! + } + return image.cropping(to: NSMakeRect(0, 0, image.size.width, image.size.height))! +} + +func drawStretchedImageInRect(_ image: CGImage, context: CGContext, rect: NSRect) -> Void { + context.saveGState() + context.setBlendMode(.normal) //NSCompositeSourceOver + context.clip(to: rect) + + context.draw(image, in: rect) + context.restoreGState() +} diff --git a/Telegram-Mac/ChatMessageDateHeader.swift b/Telegram-Mac/ChatMessageDateHeader.swift index 8ef7fb95db..7cf52f86c2 100644 --- a/Telegram-Mac/ChatMessageDateHeader.swift +++ b/Telegram-Mac/ChatMessageDateHeader.swift @@ -8,8 +8,9 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac +import TelegramCore + +import Postbox private let timezoneOffset: Int32 = { let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) @@ -24,26 +25,28 @@ private let granularity: Int32 = 60 * 60 * 24 func chatDateId(for timestamp:Int32) -> Int64 { - /* var roundedTimestamp:Int32 - if timestamp == Int32.max { - roundedTimestamp = timestamp / (granularity) * (granularity) - } else { - roundedTimestamp = ((timestamp + timezoneOffset) / (granularity)) * (granularity) - } */ - - return Int64(Calendar.current.startOfDay(for: Date(timeIntervalSince1970: TimeInterval(timestamp))).timeIntervalSince1970) + return Int64(Calendar.autoupdatingCurrent.startOfDay(for: Date(timeIntervalSince1970: TimeInterval(timestamp))).timeIntervalSince1970) +} +func mediaDateId(for timestamp:Int32) -> Int64 { + let startMonth = Calendar.autoupdatingCurrent.date(from: Calendar.current.dateComponents([.year, .month], from: Date(timeIntervalSince1970: TimeInterval(timestamp))))! + let endMonth = Calendar.autoupdatingCurrent.date(byAdding: DateComponents(month: 1, day: -1), to: startMonth)! + return Int64(endMonth.timeIntervalSince1970) } class ChatDateStickItem : TableStickItem { private let entry:ChatHistoryEntry - fileprivate let timestamp:Int32 + let timestamp:Int32 fileprivate let chatInteraction:ChatInteraction? + let isBubbled: Bool let layout:TextViewLayout - init(_ initialSize:NSSize, _ entry:ChatHistoryEntry, interaction: ChatInteraction) { + let presentation: TelegramPresentationTheme + init(_ initialSize:NSSize, _ entry:ChatHistoryEntry, interaction: ChatInteraction, theme: TelegramPresentationTheme) { self.entry = entry + self.isBubbled = entry.renderType == .bubble self.chatInteraction = interaction - if case let .DateEntry(index) = entry { + self.presentation = theme + if case let .DateEntry(index, _, _) = entry { self.timestamp = index.timestamp } else { fatalError() @@ -59,35 +62,66 @@ class ChatDateStickItem : TableStickItem { var timeinfoNow: tm = tm() localtime_r(&now, &timeinfoNow) - let text: String + var text: String if timeinfo.tm_year == timeinfoNow.tm_year && timeinfo.tm_yday == timeinfoNow.tm_yday { - text = tr(.dateToday) + + switch interaction.mode { + case .scheduled: + text = L10n.chatDateScheduledForToday + default: + text = L10n.dateToday + } + } else { - let dateFormatter = DateFormatter() - dateFormatter.locale = Locale(identifier: appCurrentLanguage.languageCode) + let dateFormatter = makeNewDateFormatter() + + dateFormatter.calendar = Calendar.autoupdatingCurrent + //dateFormatter.timeZone = NSTimeZone.local dateFormatter.dateFormat = "dd MMMM"; - text = dateFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(timestamp))) - + //&& (timeinfoNow.tm_mon >= timeinfo.tm_mon || (timeinfoNow.tm_year - timeinfo.tm_year) >= 2) + if timeinfoNow.tm_year > timeinfo.tm_year { + dateFormatter.dateFormat = "dd MMMM yyyy"; + } else if timeinfoNow.tm_year < timeinfo.tm_year { + dateFormatter.dateFormat = "dd MMMM yyyy"; + } + let dateString = dateFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(timestamp))) + switch interaction.mode { + case .scheduled: + if timestamp == 2147457600 { + text = L10n.chatDateScheduledUntilOnline + } else { + text = L10n.chatDateScheduledFor(dateString) + } + default: + text = dateString + } } - let attributedString = NSAttributedString.initialize(string: text, color: theme.colors.grayText, font: NSFont.normal(FontSize.text)) - self.layout = TextViewLayout(attributedString, maximumNumberOfLines: 1, truncationType: .end, alignment: .center) + + self.layout = TextViewLayout(.initialize(string: text, color: presentation.chatServiceItemTextColor, font: .medium(presentation.fontSize)), maximumNumberOfLines: 1, truncationType: .end, alignment: .center) super.init(initialSize) } + override var canBeAnchor: Bool { + return false + } + required init(_ initialSize: NSSize) { - entry = .DateEntry(MessageIndex.absoluteLowerBound()) + entry = .DateEntry(MessageIndex.absoluteLowerBound(), .list, theme) timestamp = 0 + self.isBubbled = false self.layout = TextViewLayout(NSAttributedString()) self.chatInteraction = nil + self.presentation = theme super.init(initialSize) } override func makeSize(_ width: CGFloat, oldWidth:CGFloat) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) layout.measure(width: width - 40) - return super.makeSize(width, oldWidth: oldWidth) + return success } override var stableId: AnyHashable { @@ -95,7 +129,7 @@ class ChatDateStickItem : TableStickItem { } override var height: CGFloat { - return 50 + return 30 } override func viewClass() -> AnyClass { @@ -112,40 +146,79 @@ class ChatDateStickView : TableStickView { required init(frame frameRect: NSRect) { self.textView = TextView() self.textView.isSelectable = false - self.textView.userInteractionEnabled = false + // self.textView.userInteractionEnabled = false self.containerView.wantsLayer = true - textView.isEventLess = true + self.textView.disableBackgroundDrawing = true + // textView.isEventLess = false super.init(frame: frameRect) - addSubview(containerView) addSubview(textView) - addSubview(borderView) - containerView.set(handler: { [weak self] _ in - if let strongSelf = self, let item = strongSelf.item as? ChatDateStickItem, strongSelf.header { - - var calendar = NSCalendar.current + textView.set(handler: { [weak self] control in + if let strongSelf = self, let item = strongSelf.item as? ChatDateStickItem, let table = item.table { - calendar.timeZone = TimeZone(abbreviation: "UTC")! - let date = Date(timeIntervalSince1970: TimeInterval(item.timestamp + 86400)) - let components = calendar.dateComponents([.year, .month, .day], from: date) + let row = table.visibleRows() + var ignore: Bool = false + if row.length > 1 { + if let underItem = table.item(at: row.location + row.length - 1) as? ChatDateStickItem { + ignore = item.timestamp == underItem.timestamp + } + } - item.chatInteraction?.jumpToDate(CalendarUtils.monthDay(components.day!, date: date)) + if strongSelf.header && !ignore { + var calendar = NSCalendar.current + + calendar.timeZone = TimeZone(abbreviation: "UTC")! + let date = Date(timeIntervalSince1970: TimeInterval(item.timestamp + 86400)) + let components = calendar.dateComponents([.year, .month, .day], from: date) + + item.chatInteraction?.jumpToDate(CalendarUtils.monthDay(components.day!, date: date)) + } else if let chatInteraction = item.chatInteraction, chatInteraction.mode == .history { + if !hasPopover(chatInteraction.context.window) { + let controller = CalendarController(NSMakeRect(0, 0, 250, 250), chatInteraction.context.window, current: Date(timeIntervalSince1970: TimeInterval(item.timestamp)), selectHandler: chatInteraction.jumpToDate) + showPopover(for: control, with: controller, edge: .maxY, inset: NSMakePoint(-84, -40)) + } + } + } }, for: .Click) } + override func hitTest(_ point: NSPoint) -> NSView? { + return header && textView.layer?.opacity == 0 ? nil : super.hitTest(point) + } + + override func mouseDown(with event: NSEvent) { + guard header, let tableView = superview as? TableView else { + super.mouseDown(with: event) + return + } + + tableView.documentView!.hitTest(tableView.documentView!.convert(event.locationInWindow, from: nil))?.mouseDown(with: event) + + } + + override func mouseUp(with event: NSEvent) { + guard header, let tableView = superview as? TableView else { + super.mouseUp(with: event) + return + } + + tableView.documentView!.hitTest(tableView.documentView!.convert(event.locationInWindow, from: nil))?.mouseUp(with: event) + + } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override var backdorColor: NSColor { - return header ? .clear : theme.colors.background + return .clear } override func updateIsVisible(_ visible: Bool, animated: Bool) { textView.change(opacity: visible ? 1 : 0, animated: animated) - containerView.change(opacity: visible ? 1 : 0, animated: animated) } + override var header: Bool { didSet { updateColors() @@ -154,16 +227,24 @@ class ChatDateStickView : TableStickView { override func updateColors() { super.updateColors() - textView.backgroundColor = theme.colors.background - - containerView.backgroundColor = .clear - containerView.layer?.borderColor = theme.colors.border.cgColor - containerView.layer?.borderWidth = header ? 1.0 : 0 - - containerView.backgroundColor = theme.colors.background + if let item = item as? ChatDateStickItem { + var presentation = item.presentation + if let table = item.table { + table.enumerateItems(with: { item in + if let item = item as? ChatRowItem { + presentation = item.presentation + return false + } else if let item = item as? ChatDateStickItem { + presentation = item.presentation + return false + } + return true + }) + } + textView.backgroundColor = presentation.chatServiceItemColor + } - } override func draw(_ layer: CALayer, in ctx: CGContext) { @@ -173,19 +254,16 @@ class ChatDateStickView : TableStickView { override func layout() { super.layout() textView.center() - containerView.center() - borderView.center() } override func set(item: TableRowItem, animated: Bool) { if let item = item as? ChatDateStickItem { textView.update(item.layout) - containerView.setFrameSize(textView.frame.width + 16, textView.frame.height + 8) - containerView.layer?.cornerRadius = containerView.frame.height / 2 - borderView.layer?.cornerRadius = containerView.frame.height / 2 - if animated { - containerView.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) - } + textView.setFrameSize(item.layout.layoutSize.width + 16, item.layout.layoutSize.height + 6) + textView.layer?.cornerRadius = textView.frame.height / 2 +// if animated { +// containerView.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) +// } self.needsLayout = true } diff --git a/Telegram-Mac/ChatMessageItem.swift b/Telegram-Mac/ChatMessageItem.swift index ce43e32d17..a2f8f33519 100644 --- a/Telegram-Mac/ChatMessageItem.swift +++ b/Telegram-Mac/ChatMessageItem.swift @@ -8,13 +8,19 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit + + + class ChatMessageItem: ChatRowItem { public private(set) var messageText:NSAttributedString public private(set) var textLayout:TextViewLayout + private let youtubeExternalLoader = MetaDisposable() + override var selectableLayout:[TextViewLayout] { return [textLayout] } @@ -23,28 +29,204 @@ class ChatMessageItem: ChatRowItem { webpageLayout?.table = self.table } + override var isSharable: Bool { + if let webpage = webpageLayout { + if webpage.content.type == "proxy" { + return true + } + } + return super.isSharable + } + + override var isBubbleFullFilled: Bool { + return containsBigEmoji || super.isBubbleFullFilled + } + + override var isStateOverlayLayout: Bool { + return containsBigEmoji && renderType == .bubble || super.isStateOverlayLayout + } + + override var bubbleContentInset: CGFloat { + return containsBigEmoji && renderType == .bubble ? 0 : super.bubbleContentInset + } + + override var defaultContentTopOffset: CGFloat { + if isBubbled && !hasBubble { + return 2 + } + return super.defaultContentTopOffset + } + + override var hasBubble: Bool { + get { + if containsBigEmoji { + return false + } else { + return super.hasBubble + } + } + set { + super.hasBubble = newValue + } + } + + let containsBigEmoji: Bool + + var unsupported: Bool { + + if let message = message, message.text.isEmpty && (message.media.isEmpty || message.media.first is TelegramMediaUnsupported) { + return message.inlinePeer == nil + } else { + return false + } + } + + var actionButtonWidth: CGFloat { + if let webpage = webpageLayout { + if webpage.isTheme { + return webpage.size.width + } + } + return self.contentSize.width + } + + var actionButtonText: String? { + if let _ = message?.adAttribute, let author = message?.author { + if author.isBot { + return L10n.chatMessageViewBot + } else if author.isGroup || author.isSupergroup { + return L10n.chatMessageViewGroup + } else { + return L10n.chatMessageViewChannel + } + } + if let webpage = webpageLayout, !webpage.hasInstantPage { + let link = inApp(for: webpage.content.url.nsstring, context: context, openInfo: chatInteraction.openInfo) + switch link { + case let .followResolvedName(_, _, postId, _, action, _): + if let action = action { + inner: switch action { + case let .joinVoiceChat(hash): + if hash != nil { + return L10n.chatMessageJoinVoiceChatAsSpeaker + } else { + return L10n.chatMessageJoinVoiceChatAsListener + } + default: + break inner + } + } + if let postId = postId, postId > 0 { + return L10n.chatMessageActionShowMessage + } + default: + break + } + if webpage.wallpaper != nil { + return L10n.chatViewBackground + } + if webpage.isTheme { + return L10n.chatActionViewTheme + } + } + + if unsupported { + return L10n.chatUnsupportedUpdatedApp + } + + return nil + } + + override var isEditMarkVisible: Bool { + if containsBigEmoji { + return false + } else { + return super.isEditMarkVisible + } + } + + func invokeAction() { + if let _ = message?.adAttribute, let peer = peer { + let link = inAppLink.peerInfo(link: "", peerId: peer.id, action:nil, openChat: peer.isChannel, postId: nil, callback: chatInteraction.openInfo) + execute(inapp: link) + } else if let webpage = webpageLayout { + let link = inApp(for: webpage.content.url.nsstring, context: context, openInfo: chatInteraction.openInfo) + execute(inapp: link) + } else if unsupported { + #if APP_STORE + execute(inapp: inAppLink.external(link: "https://apps.apple.com/us/app/telegram/id747648890", false)) + #else + (NSApp.delegate as? AppDelegate)?.checkForUpdates("") + #endif + } + } + + let wpPresentation: WPLayoutPresentation + var webpageLayout:WPLayout? - override init(_ initialSize:NSSize, _ chatInteraction:ChatInteraction,_ account:Account, _ entry: ChatHistoryEntry) { + override init(_ initialSize:NSSize, _ chatInteraction:ChatInteraction,_ context: AccountContext, _ entry: ChatHistoryEntry, _ downloadSettings: AutomaticMediaDownloadSettings, theme: TelegramPresentationTheme) { - if let message = entry.message { - + if let message = entry.message { + + let isIncoming: Bool = message.isIncoming(context.account, entry.renderType == .bubble) + + var openSpecificTimecodeFromReply:((Double?)->Void)? = nil + let messageAttr:NSMutableAttributedString - if message.text.isEmpty && message.media.isEmpty { + if message.inlinePeer == nil, message.text.isEmpty && (message.media.isEmpty || message.media.first is TelegramMediaUnsupported) { let attr = NSMutableAttributedString() - _ = attr.append(string: tr(.chatMessageUnsupported), color: theme.colors.text, font: .code(.custom(theme.fontSize))) + _ = attr.append(string: L10n.chatMessageUnsupportedNew, color: theme.chat.textColor(isIncoming, entry.renderType == .bubble), font: .code(theme.fontSize)) messageAttr = attr } else { - messageAttr = ChatMessageItem.applyMessageEntities(with: message.attributes, for: message.text, account:account, fontSize: theme.fontSize, openInfo:chatInteraction.openInfo, botCommand:chatInteraction.forceSendMessage, hashtag:account.context.globalSearch ?? {_ in }, applyProxy: chatInteraction.applyProxy).mutableCopy() as! NSMutableAttributedString -// - if message.flags.contains(.Sending) { - messageAttr.detectLinks(type: [.Links, .Mentions, .Hashtags], account: account, openInfo:chatInteraction.openInfo, applyProxy: chatInteraction.applyProxy) + + var mediaDuration: Double? = nil + var mediaDurationMessage:Message? + + var canAssignToReply: Bool = true + + if let media = message.media.first as? TelegramMediaWebpage { + switch media.content { + case let .Loaded(content): + canAssignToReply = !ExternalVideoLoader.isPlayable(content) + default: + break + } + } + + if canAssignToReply, let reply = message.replyAttribute { + mediaDurationMessage = message.associatedMessages[reply.messageId] + } else { + mediaDurationMessage = message + } + if let message = mediaDurationMessage { + if let file = message.media.first as? TelegramMediaFile, file.isVideo && !file.isAnimated, let duration = file.duration { + mediaDuration = Double(duration) + } else if let media = message.media.first as? TelegramMediaWebpage { + switch media.content { + case let .Loaded(content): + if ExternalVideoLoader.isPlayable(content) { + mediaDuration = 10 * 60 * 60 + } + default: + break + } + } + } + + let openInfo:(PeerId, Bool, MessageId?, ChatInitialAction?)->Void = { [weak chatInteraction] peerId, toChat, postId, initialAction in + chatInteraction?.openInfo(peerId, toChat, postId, initialAction ?? .source(message.id)) } + + messageAttr = ChatMessageItem.applyMessageEntities(with: message.attributes, for: message.text, message: message, context: context, fontSize: theme.fontSize, openInfo:openInfo, botCommand:chatInteraction.sendPlainText, hashtag: chatInteraction.modalSearch, applyProxy: chatInteraction.applyProxy, textColor: theme.chat.textColor(isIncoming, entry.renderType == .bubble), linkColor: theme.chat.linkColor(isIncoming, entry.renderType == .bubble), monospacedPre: theme.chat.monospacedPreColor(isIncoming, entry.renderType == .bubble), monospacedCode: theme.chat.monospacedCodeColor(isIncoming, entry.renderType == .bubble), mediaDuration: mediaDuration, timecode: { timecode in + openSpecificTimecodeFromReply?(timecode) + }).mutableCopy() as! NSMutableAttributedString + messageAttr.fixUndefinedEmojies() - var formatting: Bool = true + var formatting: Bool = messageAttr.length > 0 var index:Int = 0 while formatting { var effectiveRange:NSRange = NSMakeRange(NSNotFound, 0) @@ -64,15 +246,15 @@ class ChatMessageItem: ChatRowItem { } if effectiveRange.min > 0 { - let increment = beforeAndAfter(effectiveRange.min - 1) + let increment = beforeAndAfter(effectiveRange.min) if increment { - effectiveRange = NSMakeRange(effectiveRange.location - 1, effectiveRange.length) + effectiveRange = NSMakeRange(effectiveRange.location, effectiveRange.length + 1) } } if effectiveRange.max < messageAttr.length - 1 { let increment = beforeAndAfter(effectiveRange.max) if increment { - effectiveRange = NSMakeRange(effectiveRange.location + 1, effectiveRange.length) + effectiveRange = NSMakeRange(effectiveRange.location, effectiveRange.length + 1) } } } @@ -86,97 +268,290 @@ class ChatMessageItem: ChatRowItem { formatting = index < messageAttr.length } - - +// if message.isScam { +// _ = messageAttr.append(string: "\n\n") +// _ = messageAttr.append(string: L10n.chatScamWarning, color: theme.chat.textColor(isIncoming, entry.renderType == .bubble), font: .normal(theme.fontSize)) +// } } + + let copy = messageAttr.mutableCopy() as! NSMutableAttributedString if let peer = message.peers[message.id.peerId] { if peer is TelegramSecretChat { - copy.detectLinks(type: .Links, account: account) + copy.detectLinks(type: [.Links, .Mentions], context: context, color: theme.chat.linkColor(isIncoming, entry.renderType == .bubble), openInfo: chatInteraction.openInfo) } } + + let containsBigEmoji: Bool + if message.media.first == nil, bigEmojiMessage(context.sharedContext, message: message) { + switch copy.string.glyphCount { + case 1: + copy.addAttribute(.font, value: NSFont.normal(theme.fontSize * 5.8), range: copy.range) + containsBigEmoji = true + case 2: + copy.addAttribute(.font, value: NSFont.normal(theme.fontSize * 4.8), range: copy.range) + containsBigEmoji = true + case 3: + copy.addAttribute(.font, value: NSFont.normal(theme.fontSize * 3.8), range: copy.range) + containsBigEmoji = true + default: + containsBigEmoji = false + } + } else { + containsBigEmoji = false + } - self.messageText = copy + self.containsBigEmoji = containsBigEmoji + + if message.flags.contains(.Failed) || message.flags.contains(.Unsent) || message.flags.contains(.Sending) { + copy.detectLinks(type: [.Links, .Mentions, .Hashtags, .Commands], context: context, color: theme.chat.linkColor(isIncoming, entry.renderType == .bubble), openInfo: chatInteraction.openInfo, hashtag: { _ in }, command: { _ in }, applyProxy: chatInteraction.applyProxy) + } + if let text = message.restrictedText(context.contentSettings) { + self.messageText = .initialize(string: text, color: theme.colors.grayText, font: .italic(theme.fontSize)) + } else { + self.messageText = copy + } + - textLayout = TextViewLayout(self.messageText) - + textLayout = TextViewLayout(self.messageText, selectText: theme.chat.selectText(isIncoming, entry.renderType == .bubble), strokeLinks: entry.renderType == .bubble && !containsBigEmoji, alwaysStaticItems: true, disableTooltips: false) + textLayout.mayBlocked = entry.renderType != .bubble + + if let highlightFoundText = entry.additionalData.highlightFoundText { + if highlightFoundText.isMessage { + let range = copy.string.lowercased().nsstring.range(of: highlightFoundText.query.lowercased()) + if range.location != NSNotFound { + textLayout.additionalSelections = [TextSelectedRange(range: range, color: theme.colors.accentIcon.withAlphaComponent(0.5), def: false)] + } + } else { + var additionalSelections:[TextSelectedRange] = [] + let string = copy.string.lowercased().nsstring + var searchRange = NSMakeRange(0, string.length) + var foundRange:NSRange = NSMakeRange(NSNotFound, 0) + while (searchRange.location < string.length) { + searchRange.length = string.length - searchRange.location + foundRange = string.range(of: highlightFoundText.query.lowercased(), options: [], range: searchRange) + if (foundRange.location != NSNotFound) { + additionalSelections.append(TextSelectedRange(range: foundRange, color: theme.colors.grayIcon.withAlphaComponent(0.5), def: false)) + searchRange.location = foundRange.location+foundRange.length; + } else { + break + } + } + textLayout.additionalSelections = additionalSelections + } + + } + if let range = selectManager.find(entry.stableId) { textLayout.selectedRange.range = range } + var media = message.media.first + if let game = media as? TelegramMediaGame { + media = TelegramMediaWebpage(webpageId: MediaId(namespace: 0, id: 0), content: TelegramMediaWebpageContent.Loaded(TelegramMediaWebpageLoadedContent(url: "", displayUrl: "", hash: 0, type: "photo", websiteName: game.name, title: game.name, text: game.description, embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, image: game.image, file: game.file, attributes: [], instantPage: nil))) + } - if let webpage = message.media.first as? TelegramMediaWebpage { + self.wpPresentation = WPLayoutPresentation(text: theme.chat.textColor(isIncoming, entry.renderType == .bubble), activity: theme.chat.webPreviewActivity(isIncoming, entry.renderType == .bubble), link: theme.chat.linkColor(isIncoming, entry.renderType == .bubble), selectText: theme.chat.selectText(isIncoming, entry.renderType == .bubble), ivIcon: theme.chat.instantPageIcon(isIncoming, entry.renderType == .bubble, presentation: theme), renderType: entry.renderType) + + + if let webpage = media as? TelegramMediaWebpage { switch webpage.content { case let .Loaded(content): - if content.file == nil { - webpageLayout = WPArticleLayout(with: content, account:account, chatInteraction: chatInteraction, parent:message, fontSize: theme.fontSize) + var forceArticle: Bool = false + if let instantPage = content.instantPage { + if instantPage.blocks.count == 3 { + switch instantPage.blocks[2] { + case .collage, .slideshow: + forceArticle = true + default: + break + } + } + } + if content.type == "telegram_background" { + forceArticle = true + } + if content.file == nil || forceArticle { + webpageLayout = WPArticleLayout(with: content, context: context, chatInteraction: chatInteraction, parent:message, fontSize: theme.fontSize, presentation: wpPresentation, approximateSynchronousValue: Thread.isMainThread, downloadSettings: downloadSettings, autoplayMedia: entry.autoplayMedia, theme: theme) } else { - webpageLayout = WPMediaLayout(with: content, account:account, chatInteraction: chatInteraction, parent:message, fontSize: theme.fontSize) + webpageLayout = WPMediaLayout(with: content, context: context, chatInteraction: chatInteraction, parent:message, fontSize: theme.fontSize, presentation: wpPresentation, approximateSynchronousValue: Thread.isMainThread, downloadSettings: downloadSettings, autoplayMedia: entry.autoplayMedia, theme: theme) } default: break } } - super.init(initialSize,chatInteraction,account,entry) + super.init(initialSize, chatInteraction, context, entry, downloadSettings, theme: theme) + - textLayout.interactions = TextViewInteractions(processURL:{ link in - if let link = link as? inAppLink { - execute(inapp:link) + (webpageLayout as? WPMediaLayout)?.parameters?.showMedia = { [weak self] message in + if let webpage = message.media.first as? TelegramMediaWebpage { + switch webpage.content { + case let .Loaded(content): + if content.embedType == "iframe" && content.type != kBotInlineTypeGif { + showModal(with: WebpageModalController(content: content, context: context), for: mainWindow) + return + } + default: + break + } + } + showChatGallery(context: context, message: message, self?.table, (self?.webpageLayout as? WPMediaLayout)?.parameters, type: .alone) + } + + openSpecificTimecodeFromReply = { [weak self] timecode in + if let timecode = timecode { + var canAssignToReply: Bool = true + if let media = message.media.first as? TelegramMediaWebpage { + switch media.content { + case let .Loaded(content): + canAssignToReply = !ExternalVideoLoader.isPlayable(content) + default: + break + } + } + var assignMessage: Message? + if canAssignToReply, let reply = message.replyAttribute { + assignMessage = message.associatedMessages[reply.messageId] + } else { + assignMessage = message + } + if let message = assignMessage { + let id = ChatHistoryEntryId.message(message) + if let item = self?.table?.item(stableId: id) as? ChatMediaItem { + item.parameters?.set_timeCodeInitializer(timecode) + item.parameters?.showMedia(message) + } else if let groupInfo = message.groupInfo { + let id = ChatHistoryEntryId.groupedPhotos(groupInfo: groupInfo) + if let item = self?.table?.item(stableId: id) as? ChatGroupedItem { + item.parameters.first?.set_timeCodeInitializer(timecode) + item.parameters.first?.showMedia(message) + } + } else if let item = self?.table?.item(stableId: id) as? ChatMessageItem { + if let content = item.webpageLayout?.content { + self?.youtubeExternalLoader.set((sharedVideoLoader.status(for: content) |> deliverOnMainQueue).start(next: { [weak item] status in + if let item = item, let message = item.message { + if let status = status { + let content = content.withUpdatedYoutubeTimecode(timecode) + if let media = message.media.first as? TelegramMediaWebpage { + switch status { + case .fail: + execute(inapp: .external(link: content.url, false)) + case .loaded: + let message = message.withUpdatedMedia([TelegramMediaWebpage(webpageId: media.webpageId, content: .Loaded(content))]) + showChatGallery(context: item.context, message: message, item.table) + default: + break + } + } + + + } + } + + })) + } + } + } } - }, copy: { + } + + let interactions = globalLinkExecutor + interactions.copy = { selectManager.copy(selectManager) return !selectManager.isEmpty - }, menuItems: { [weak self] in + } + interactions.copyToClipboard = { text in + copyToClipboard(text) + context.sharedContext.bindings.rootNavigation().controller.show(toaster: ControllerToaster(text: L10n.shareLinkCopied)) + } + interactions.menuItems = { [weak self] type in var items:[ContextMenuItem] = [] - if let strongSelf = self { - items.append(ContextMenuItem(tr(.textCopy), handler: { [weak strongSelf] in - let result = strongSelf?.textLayout.interactions.copy?() - if let result = result, let strongSelf = strongSelf, !result { + if let strongSelf = self, let layout = self?.textLayout { + + let text: String + if let type = type { + text = copyContextText(from: type) + items.append(ContextMenuItem(text, handler: { + if let strongSelf = self { + let pb = NSPasteboard.general + pb.clearContents() + pb.declareTypes([.string], owner: strongSelf) + let layout = strongSelf.textLayout + var effectiveRange = layout.selectedRange.range + if layout.attributedString.range.intersection(effectiveRange) != nil { + let selectedText = layout.attributedString.attributedSubstring(from: effectiveRange) + let attribute = layout.attributedString.attribute(NSAttributedString.Key.link, at: layout.selectedRange.range.location, effectiveRange: &effectiveRange) + if let attribute = attribute as? inAppLink { + pb.setString(attribute.link.isEmpty ? selectedText.string : attribute.link, forType: .string) + } else { + pb.setString(selectedText.string, forType: .string) + } + } + } + })) + + } + + items.append(ContextMenuItem(layout.selectedRange.hasSelectText ? L10n.chatCopySelectedText : L10n.textCopy, handler: { + let result = self?.textLayout.interactions.copy?() + if let result = result, let strongSelf = self, !result { if strongSelf.textLayout.selectedRange.hasSelectText { let pb = NSPasteboard.general + pb.clearContents() pb.declareTypes([.string], owner: strongSelf) var effectiveRange = strongSelf.textLayout.selectedRange.range - - let attribute = strongSelf.textLayout.attributedString.attribute(NSAttributedStringKey.link, at: strongSelf.textLayout.selectedRange.range.location, effectiveRange: &effectiveRange) - - if let attribute = attribute as? inAppLink, case let .external(link, confirm) = attribute { - if confirm { - pb.setString(link, forType: .string) - return + let selectedText = strongSelf.textLayout.attributedString.attributedSubstring(from: strongSelf.textLayout.selectedRange.range) + let isCopied = globalLinkExecutor.copyAttributedString(selectedText) + if !isCopied { + let attribute = strongSelf.textLayout.attributedString.attribute(NSAttributedString.Key.link, at: strongSelf.textLayout.selectedRange.range.location, effectiveRange: &effectiveRange) + + if let attribute = attribute as? inAppLink { + pb.setString(attribute.link.isEmpty ? selectedText.string : attribute.link, forType: .string) + } else { + pb.setString(selectedText.string, forType: .string) } } - pb.setString(strongSelf.textLayout.attributedString.string.nsstring.substring(with: strongSelf.textLayout.selectedRange.range), forType: .string) } } })) + if strongSelf.textLayout.selectedRange.hasSelectText { var effectiveRange: NSRange = NSMakeRange(NSNotFound, 0) if let _ = strongSelf.textLayout.attributedString.attribute(.preformattedPre, at: strongSelf.textLayout.selectedRange.range.location, effectiveRange: &effectiveRange) { let blockText = strongSelf.textLayout.attributedString.attributedSubstring(from: effectiveRange).string - items.append(ContextMenuItem(tr(.chatContextCopyBlock), handler: { + items.append(ContextMenuItem(tr(L10n.chatContextCopyBlock), handler: { copyToClipboard(blockText) })) } } - return strongSelf.menuItems() |> map { basic in + return strongSelf.menuItems(in: NSZeroPoint) |> map { basic in var basic = basic - basic.remove(at: 1) - return items + basic + if basic.count > 1 { + basic.remove(at: 1) + basic.insert(contentsOf: items, at: 1) + } + + return basic } } return .complete() + } + + interactions.hoverOnLink = { value in - }) + } + + textLayout.interactions = interactions return } @@ -192,74 +567,202 @@ class ChatMessageItem: ChatRowItem { } } + override var isForceRightLine: Bool { + if self.webpageLayout?.content.type == "proxy" { + return true + } else { + return super.isForceRightLine + } + } + + override var isFixedRightPosition: Bool { + if containsBigEmoji { + return true + } + if let webpageLayout = webpageLayout { + if let webpageLayout = webpageLayout as? WPArticleLayout, let textLayout = webpageLayout.textLayout { + if textLayout.lines.count > 1, let line = textLayout.lines.last, line.frame.width < contentSize.width - (rightSize.width + insetBetweenContentAndDate) { + return true + } + } + return super.isFixedRightPosition + } + + if textLayout.lines.count > 1, let line = textLayout.lines.last, line.frame.width < contentSize.width - (rightSize.width + insetBetweenContentAndDate) { + return true + } + return super.isForceRightLine + } + + override var additionalLineForDateInBubbleState: CGFloat? { + + if containsBigEmoji { + return rightSize.height + 3 + } + if isForceRightLine { + return rightSize.height + } + if unsupported { + return rightSize.height + } + if rightSize.width + insetBetweenContentAndDate + bubbleDefaultInnerInset + contentSize.width + 30 > self.width { + // return rightSize.height + } + + if let webpageLayout = webpageLayout { + if let webpageLayout = webpageLayout as? WPArticleLayout { + if let textLayout = webpageLayout.textLayout { + if webpageLayout.hasInstantPage { + return rightSize.height + 4 + } + if textLayout.lines.count > 1, let line = textLayout.lines.last, line.frame.width > realContentSize.width - (rightSize.width + insetBetweenContentAndDate) { + return rightSize.height + } + if let _ = webpageLayout.imageSize, webpageLayout.isFullImageSize || textLayout.layoutSize.height - 10 <= webpageLayout.contrainedImageSize.height { + return rightSize.height + } + if actionButtonText != nil { + return rightSize.height + 4 + } + if webpageLayout.groupLayout != nil { + return rightSize.height + } + } else { + return rightSize.height + } + + + } else if webpageLayout is WPMediaLayout { + return rightSize.height + } + return nil + } + + if textLayout.lines.count == 1 { + if contentOffset.x + textLayout.layoutSize.width - (rightSize.width + insetBetweenContentAndDate) > width { + return rightSize.height + } + } else if let line = textLayout.lines.last, max(realContentSize.width, maxTitleWidth) < line.frame.width + (rightSize.width + insetBetweenContentAndDate) { + return rightSize.height + } + return nil + } + override func makeContentSize(_ width: CGFloat) -> NSSize { let size:NSSize = super.makeContentSize(width) - textLayout.measure(width: width) - webpageLayout?.measure(width: min(width, 400)) + webpageLayout?.measure(width: min(width, 380)) + let textBlockWidth: CGFloat = isBubbled ? max((webpageLayout?.size.width ?? width), min(240, width)) : width + textLayout.measure(width: textBlockWidth, isBigEmoji: containsBigEmoji) + var contentSize = NSMakeSize(max(webpageLayout?.contentRect.width ?? 0, textLayout.layoutSize.width), size.height + textLayout.layoutSize.height) if let webpageLayout = webpageLayout { - contentSize.height += webpageLayout.size.height + defaultContentTopOffset + contentSize.height += webpageLayout.size.height + defaultContentInnerInset contentSize.width = max(webpageLayout.size.width, contentSize.width) + + } + if let _ = actionButtonText { + contentSize.height += actionButtonHeight } - return contentSize } - override func menuItems() -> Signal<[ContextMenuItem], Void> { - var items = super.menuItems() + var actionButtonHeight: CGFloat { + return 36 + } + + + override var instantlyResize: Bool { + return true + } + + override var bubbleFrame: NSRect { + var frame = super.bubbleFrame + + + if isBubbleFullFilled { + frame.size.width = contentSize.width + additionBubbleInset + return frame + } + + if replyMarkupModel != nil, webpageLayout == nil, textLayout.layoutSize.width < 200 { + frame.size.width = max(blockWidth, frame.width) + } + return frame + } + + + + override func menuItems(in location: NSPoint) -> Signal<[ContextMenuItem], NoError> { + var items = super.menuItems(in: location) + + if message?.adAttribute != nil { + return items + } + let text = messageText.string - let account = self.account! + let context = self.context + + var media: Media? = webpageLayout?.content.file ?? webpageLayout?.content.image + + if let groupLayout = (webpageLayout as? WPArticleLayout)?.groupLayout { + if let message = groupLayout.message(at: location) { + media = message.media.first + } + } - if let file = webpageLayout?.content.file { - items = items |> mapToSignal { items -> Signal<[ContextMenuItem], Void> in + if let file = media as? TelegramMediaFile, let message = message { + items = items |> mapToSignal { items -> Signal<[ContextMenuItem], NoError> in var items = items - return account.postbox.mediaBox.resourceData(file.resource) |> deliverOnMainQueue |> mapToSignal { data in + return context.account.postbox.mediaBox.resourceData(file.resource) |> deliverOnMainQueue |> mapToSignal { data in if data.complete { - items.append(ContextMenuItem(tr(.contextCopyMedia), handler: { - saveAs(file, account: account) + items.append(ContextMenuItem(L10n.contextCopyMedia, handler: { + saveAs(file, account: context.account) })) } - if file.isSticker, let fileId = file.id { - return account.postbox.modify { modifier -> [ContextMenuItem] in - let saved = getIsStickerSaved(modifier: modifier, fileId: fileId) - items.append(ContextMenuItem( !saved ? tr(.chatContextAddFavoriteSticker) : tr(.chatContextRemoveFavoriteSticker), handler: { + if file.isStaticSticker, let fileId = file.id { + return context.account.postbox.transaction { transaction -> [ContextMenuItem] in + let saved = getIsStickerSaved(transaction: transaction, fileId: fileId) + items.append(ContextMenuItem( !saved ? L10n.chatContextAddFavoriteSticker : L10n.chatContextRemoveFavoriteSticker, handler: { if !saved { - _ = addSavedSticker(postbox: account.postbox, network: account.network, file: file).start() + _ = addSavedSticker(postbox: context.account.postbox, network: context.account.network, file: file).start() } else { - _ = removeSavedSticker(postbox: account.postbox, mediaId: fileId).start() + _ = removeSavedSticker(postbox: context.account.postbox, mediaId: fileId).start() } })) return items } + } else if file.isVideo && file.isAnimated { + items.append(ContextMenuItem(L10n.messageContextSaveGif, handler: { + let _ = addSavedGif(postbox: context.account.postbox, fileReference: FileMediaReference.message(message: MessageReference(message), media: file)).start() + })) } - return .single(items) } } - } else if let image = webpageLayout?.content.image { - items = items |> mapToSignal { items -> Signal<[ContextMenuItem], Void> in + } else if let image = media as? TelegramMediaImage { + items = items |> mapToSignal { items -> Signal<[ContextMenuItem], NoError> in var items = items if let resource = image.representations.last?.resource { - return account.postbox.mediaBox.resourceData(resource) |> take(1) |> deliverOnMainQueue |> map { data in + return context.account.postbox.mediaBox.resourceData(resource) |> take(1) |> deliverOnMainQueue |> map { data in if data.complete { - items.append(ContextMenuItem(tr(.galleryContextCopyToClipboard), handler: { + items.append(ContextMenuItem(L10n.galleryContextCopyToClipboard, handler: { if let path = link(path: data.path, ext: "jpg") { let pb = NSPasteboard.general pb.clearContents() pb.writeObjects([NSURL(fileURLWithPath: path)]) } })) - items.append(ContextMenuItem(tr(.contextCopyMedia), handler: { + items.append(ContextMenuItem(L10n.contextCopyMedia, handler: { savePanel(file: data.path, ext: "jpg", for: mainWindow) })) } @@ -272,97 +775,272 @@ class ChatMessageItem: ChatRowItem { } - return items |> map { items in + return items |> deliverOnMainQueue |> map { [weak self] items in var items = items - items.insert(ContextMenuItem(tr(.textCopy), handler: { - copyToClipboard(text) - }), at: 1) + + var index: Int? = nil + for i in 0 ..< items.count { + if items[i].title == tr(L10n.messageContextCopyMessageLink1) { + index = i + } + } + + if index == nil { + for i in 0 ..< items.count { + if items[i].title == L10n.messageContextReply1 { + index = i + 1 + } + } + } + + let insert = min(index ?? 0, items.count) + items.insert(ContextMenuItem(L10n.textCopyText, handler: { [weak self] in + if let string = self?.textLayout.attributedString { + if !globalLinkExecutor.copyAttributedString(string) { + copyToClipboard(string.string) + } + } + }), at: insert) + + + + if let view = self?.view as? ChatRowView, let textView = view.selectableTextViews.first, let window = textView.window, index == nil { + let point = textView.convert(window.mouseLocationOutsideOfEventStream, from: nil) + if let layout = textView.layout { + if let (link, _, range, _) = layout.link(at: point) { + var text:String = layout.attributedString.string.nsstring.substring(with: range) + if let link = link as? inAppLink { + if case let .external(link, _) = link { + text = link + } + } + + for i in 0 ..< items.count { + if items[i].title == tr(L10n.messageContextCopyMessageLink1) { + items.remove(at: i) + break + } + } + + items.insert(ContextMenuItem(tr(L10n.messageContextCopyMessageLink1), handler: { + copyToClipboard(text) + }), at: min(1, items.count)) + + + } + } + } + if let content = self?.webpageLayout?.content, content.type == "proxy" { + items.insert(ContextMenuItem(L10n.chatCopyProxyConfiguration, handler: { + copyToClipboard(content.url) + }), at: items.isEmpty ? 0 : 1) + } return items } } + deinit { + youtubeExternalLoader.dispose() + } + override func viewClass() -> AnyClass { return ChatMessageView.self } - static func applyMessageEntities(with attributes:[MessageAttribute], for text:String, account:Account, fontSize: CGFloat, openInfo:@escaping (PeerId, Bool, MessageId?, ChatInitialAction?)->Void, botCommand:@escaping (String)->Void, hashtag:@escaping (String)->Void, applyProxy:@escaping (ProxySettings)->Void) -> NSAttributedString { - var entities: TextEntitiesMessageAttribute? + static func applyMessageEntities(with attributes:[MessageAttribute], for text:String, message: Message?, context: AccountContext, fontSize: CGFloat, openInfo:@escaping (PeerId, Bool, MessageId?, ChatInitialAction?)->Void, botCommand:@escaping (String)->Void = { _ in }, hashtag:@escaping (String)->Void = { _ in }, applyProxy:@escaping (ProxyServerSettings)->Void = { _ in }, textColor: NSColor = theme.colors.text, linkColor: NSColor = theme.colors.link, monospacedPre:NSColor = theme.colors.monospacedPre, monospacedCode: NSColor = theme.colors.monospacedCode, mediaDuration: Double? = nil, timecode: @escaping(Double?)->Void = { _ in }, openBank: @escaping(String)->Void = { _ in }) -> NSAttributedString { + var entities: [MessageTextEntity] = [] for attribute in attributes { if let attribute = attribute as? TextEntitiesMessageAttribute { - entities = attribute + entities = attribute.entities break } } + var fontAttributes: [NSRange: ChatTextFontAttributes] = [:] - let string = NSMutableAttributedString(string: text, attributes: [NSAttributedStringKey.font: NSFont.normal(.custom(fontSize)), NSAttributedStringKey.foregroundColor: theme.colors.text]) - if let entities = entities { - var nsString: NSString? - for entity in entities.entities { - let range = string.trimRange(NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)) - switch entity.type { - case .Url: - string.addAttribute(NSAttributedStringKey.foregroundColor, value: theme.colors.link, range: range) - if nsString == nil { - nsString = text as NSString - } - let link = inApp(for:nsString!.substring(with: range) as NSString, account:account, openInfo:openInfo, applyProxy: applyProxy) - string.addAttribute(NSAttributedStringKey.link, value: link, range: range) - case .Email: - string.addAttribute(NSAttributedStringKey.foregroundColor, value: theme.colors.link, range: range) - if nsString == nil { - nsString = text as NSString - } - string.addAttribute(NSAttributedStringKey.link, value: inAppLink.external(link: "mailto:\(nsString!.substring(with: range))", false), range: range) - case let .TextUrl(url): - string.addAttribute(NSAttributedStringKey.foregroundColor, value: theme.colors.link, range: range) - if nsString == nil { - nsString = text as NSString - } + + let string = NSMutableAttributedString(string: text, attributes: [NSAttributedString.Key.font: NSFont.normal(fontSize), NSAttributedString.Key.foregroundColor: textColor]) + + let new = addLocallyGeneratedEntities(text, enabledTypes: [.timecode], entities: entities, mediaDuration: mediaDuration) + var nsString: NSString? + entities = entities + (new ?? []) + for entity in entities { + let range = string.trimRange(NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)) + + switch entity.type { + case .Url: + string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) + if nsString == nil { + nsString = text as NSString + } + let link = inApp(for:nsString!.substring(with: range) as NSString, context:context, openInfo:openInfo, applyProxy: applyProxy) + string.addAttribute(NSAttributedString.Key.link, value: link, range: range) + case .Email: + string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) + if nsString == nil { + nsString = text as NSString + } + string.addAttribute(NSAttributedString.Key.link, value: inAppLink.external(link: "mailto:\(nsString!.substring(with: range))", false), range: range) + case let .TextUrl(url): + string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) + if nsString == nil { + nsString = text as NSString + } + + string.addAttribute(NSAttributedString.Key.link, value: inApp(for: url as NSString, context: context, openInfo: openInfo, hashtag: hashtag, command: botCommand, applyProxy: applyProxy, confirm: nsString?.substring(with: range).trimmed != url), range: range) + case .Bold: + if let fontAttribute = fontAttributes[range] { + fontAttributes[range] = fontAttribute.union(.bold) + } else { + fontAttributes[range] = .bold + } + case .Italic: + if let fontAttribute = fontAttributes[range] { + fontAttributes[range] = fontAttribute.union(.italic) + } else { + fontAttributes[range] = .italic + } + case .Mention: + string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) + if nsString == nil { + nsString = text as NSString + } + string.addAttribute(NSAttributedString.Key.link, value: inAppLink.followResolvedName(link: nsString!.substring(with: range), username: nsString!.substring(with: range), postId:nil, context:context, action:nil, callback: openInfo), range: range) + case let .TextMention(peerId): + string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) + string.addAttribute(NSAttributedString.Key.link, value: inAppLink.peerInfo(link: "", peerId: peerId, action:nil, openChat: false, postId: nil, callback: openInfo), range: range) + case .BotCommand: + string.addAttribute(NSAttributedString.Key.foregroundColor, value: textColor, range: range) + if nsString == nil { + nsString = text as NSString + } + string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) + string.addAttribute(NSAttributedString.Key.link, value: inAppLink.botCommand(nsString!.substring(with: range), botCommand), range: range) + case .Code: + string.addAttribute(.preformattedCode, value: 4.0, range: range) + if let fontAttribute = fontAttributes[range] { + fontAttributes[range] = fontAttribute.union(.monospace) + } else { + fontAttributes[range] = .monospace + } + string.addAttribute(NSAttributedString.Key.foregroundColor, value: monospacedCode, range: range) + string.addAttribute(NSAttributedString.Key.link, value: inAppLink.code(text.nsstring.substring(with: range), { link in + copyToClipboard(link) + context.sharedContext.bindings.showControllerToaster(ControllerToaster(text: L10n.shareLinkCopied), true) + }), range: range) + case .Pre: + string.addAttribute(.preformattedCode, value: 4.0, range: range) + if let fontAttribute = fontAttributes[range] { + fontAttributes[range] = fontAttribute.union(.monospace) + } else { + fontAttributes[range] = .monospace + } + // string.addAttribute(.preformattedPre, value: 4.0, range: range) + string.addAttribute(NSAttributedString.Key.foregroundColor, value: monospacedPre, range: range) + case .Hashtag: + string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) + if nsString == nil { + nsString = text as NSString + } + string.addAttribute(NSAttributedString.Key.link, value: inAppLink.hashtag(nsString!.substring(with: range), hashtag), range: range) + if let color = NSColor(hexString: nsString!.substring(with: range)) { - string.addAttribute(NSAttributedStringKey.link, value: inApp(for: url as NSString, account: account, openInfo: openInfo, hashtag: hashtag, command: botCommand, applyProxy: applyProxy, confirm: true), range: range) - case .Bold: - string.addAttribute(NSAttributedStringKey.font, value: NSFont.medium(.custom(fontSize)), range: range) - case .Italic: - string.addAttribute(NSAttributedStringKey.font, value: NSFontManager.shared.convert(.normal(.custom(fontSize)), toHaveTrait: .italicFontMask), range: range) - case .Mention: - string.addAttribute(NSAttributedStringKey.foregroundColor, value: theme.colors.link, range: range) - if nsString == nil { - nsString = text as NSString + struct RunStruct { + let ascent: CGFloat + let descent: CGFloat + let width: CGFloat } - string.addAttribute(NSAttributedStringKey.link, value: inAppLink.followResolvedName(username:nsString!.substring(with: range), postId:nil, account:account, action:nil, callback: openInfo), range: range) - case let .TextMention(peerId): - string.addAttribute(NSAttributedStringKey.foregroundColor, value: theme.colors.link, range: range) - string.addAttribute(NSAttributedStringKey.link, value: inAppLink.peerInfo(peerId: peerId, action:nil, openChat: false, postId: nil, callback: openInfo), range: range) - case .BotCommand: - string.addAttribute(NSAttributedStringKey.foregroundColor, value: theme.colors.link, range: range) + + let dimensions = NSMakeSize(theme.fontSize + 6, theme.fontSize + 6) + let extentBuffer = UnsafeMutablePointer.allocate(capacity: 1) + extentBuffer.initialize(to: RunStruct(ascent: 0.0, descent: 0.0, width: dimensions.width)) + var callbacks = CTRunDelegateCallbacks(version: kCTRunDelegateVersion1, dealloc: { (pointer) in + }, getAscent: { (pointer) -> CGFloat in + let d = pointer.assumingMemoryBound(to: RunStruct.self) + return d.pointee.ascent + }, getDescent: { (pointer) -> CGFloat in + let d = pointer.assumingMemoryBound(to: RunStruct.self) + return d.pointee.descent + }, getWidth: { (pointer) -> CGFloat in + let d = pointer.assumingMemoryBound(to: RunStruct.self) + return d.pointee.width + }) + let delegate = CTRunDelegateCreate(&callbacks, extentBuffer) + let key = kCTRunDelegateAttributeName as String + let attrDictionaryDelegate:[NSAttributedString.Key : Any] = [NSAttributedString.Key(key): delegate as Any, .hexColorMark : color, .hexColorMarkDimensions: dimensions] + + string.addAttributes(attrDictionaryDelegate, range: NSMakeRange(range.upperBound - 1, 1)) + } + + case .Strikethrough: + string.addAttribute(NSAttributedString.Key.strikethroughStyle, value: true, range: range) + case .Underline: + string.addAttribute(NSAttributedString.Key.underlineStyle, value: true, range: range) + case .BankCard: + if nsString == nil { + nsString = text as NSString + } + string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) + string.addAttribute(NSAttributedString.Key.link, value: inAppLink.callback(nsString!.substring(with: range), { bankCard in + openBank(bankCard) + }), range: range) + case let .Custom(type): + if type == ApplicationSpecificEntityType.Timecode { + string.addAttribute(NSAttributedString.Key.foregroundColor, value: linkColor, range: range) if nsString == nil { nsString = text as NSString } - string.addAttribute(NSAttributedStringKey.link, value: inAppLink.botCommand(nsString!.substring(with: range), botCommand), range: range) - case .Code: - string.addAttribute(.preformattedCode, value: 4.0, range: range) - string.addAttribute(NSAttributedStringKey.font, value: NSFont.code(.custom(fontSize)), range: range) - string.addAttribute(NSAttributedStringKey.foregroundColor, value: theme.colors.redUI, range: range) - case .Pre: - string.addAttribute(.preformattedPre, value: 4.0, range: range) - string.addAttribute(NSAttributedStringKey.font, value: NSFont.code(.custom(fontSize)), range: range) - string.addAttribute(NSAttributedStringKey.foregroundColor, value: theme.colors.text, range: range) - case .Hashtag: - string.addAttribute(NSAttributedStringKey.foregroundColor, value: theme.colors.link, range: range) - if nsString == nil { - nsString = text as NSString + let code = parseTimecodeString(nsString!.substring(with: range)) + + var link = "" + if let message = message { + var peer: Peer? + var messageId: MessageId? + if let info = message.forwardInfo { + peer = info.author + messageId = info.sourceMessageId + } else { + peer = message.effectiveAuthor + messageId = message.id + } + if let peer = peer, let messageId = messageId { + if let code = code, peer.isChannel || peer.isSupergroup { + let code = Int(round(code)) + let address = peer.addressName ?? "\(messageId.peerId.id)" + link = "t.me/\(address)/\(messageId.id)?t=\(code)" + } + } } - string.addAttribute(NSAttributedStringKey.link, value: inAppLink.hashtag(nsString!.substring(with: range), hashtag), range: range) - break - default: - break + + string.addAttribute(NSAttributedString.Key.link, value: inAppLink.callback(link, { _ in + timecode(code) + }), range: range) + } + default: + break } - } + for (range, fontAttributes) in fontAttributes { + var font: NSFont? + if fontAttributes.contains(.blockQuote) { + font = .code(fontSize) + } else if fontAttributes == [.bold, .italic] { + font = .boldItalic(fontSize) + } else if fontAttributes == [.bold] { + font = .bold(fontSize) + } else if fontAttributes == [.italic] { + font = .italic(fontSize) + } else if fontAttributes == [.monospace] { + font = .code(fontSize) + } + if let font = font { + string.addAttribute(.font, value: font, range: range) + } + } + return string.copy() as! NSAttributedString } } diff --git a/Telegram-Mac/ChatMessageThrottledProcessingManager.swift b/Telegram-Mac/ChatMessageThrottledProcessingManager.swift index 923e026e89..cc6ab9b3fc 100644 --- a/Telegram-Mac/ChatMessageThrottledProcessingManager.swift +++ b/Telegram-Mac/ChatMessageThrottledProcessingManager.swift @@ -7,23 +7,27 @@ // import Cocoa -import PostboxMac -import SwiftSignalKitMac +import Postbox +import SwiftSignalKit + +private let queue = Queue(name: "ChatMessageThrottledProcessingManager") final class ChatMessageThrottledProcessingManager { - private let queue = Queue(target: Queue.concurrentBackgroundQueue()) + private let queue = Queue() - private let delay: TimeInterval - init(delay: TimeInterval = 1.0) { - self.delay = delay - } + private let delay: Double var process: ((Set) -> Void)? - private var timer: SwiftSignalKitMac.Timer? + private var timer: SwiftSignalKit.Timer? + private var processedList: [MessageId] = [] private var processed = Set() private var buffer = Set() + init(delay: Double = 1.0) { + self.delay = delay + } + func setProcess(process: @escaping (Set) -> Void) { self.queue.async { self.process = process @@ -35,13 +39,21 @@ final class ChatMessageThrottledProcessingManager { for id in messageIds { if !self.processed.contains(id) { self.processed.insert(id) + self.processedList.append(id) self.buffer.insert(id) } } + if self.processedList.count > 1000 { + for i in 0 ..< 200 { + self.processed.remove(self.processedList[i]) + } + self.processedList.removeSubrange(0 ..< 200) + } + if self.timer == nil { var completionImpl: (() -> Void)? - let timer = SwiftSignalKitMac.Timer(timeout: self.delay, repeat: false, completion: { + let timer = SwiftSignalKit.Timer(timeout: self.delay, repeat: false, completion: { completionImpl?() }, queue: self.queue) completionImpl = { [weak self, weak timer] in @@ -60,3 +72,50 @@ final class ChatMessageThrottledProcessingManager { } } } + + +final class ChatMessageVisibleThrottledProcessingManager { + private let queue = Queue() + + private let delay: Double + + private var currentIds = Set() + + var process: ((Set) -> Void)? + + private var timer: SwiftSignalKit.Timer? + + init(delay: Double = 1.0) { + self.delay = delay + } + + func setProcess(process: @escaping (Set) -> Void) { + self.queue.async { + self.process = process + } + } + + func update(_ ids: Set) { + self.queue.async { + if self.currentIds != ids { + self.currentIds = ids + if self.timer == nil { + var completionImpl: (() -> Void)? + let timer = SwiftSignalKit.Timer(timeout: self.delay, repeat: false, completion: { + completionImpl?() + }, queue: self.queue) + completionImpl = { [weak self, weak timer] in + if let strongSelf = self { + if let timer = timer, strongSelf.timer === timer { + strongSelf.timer = nil + } + strongSelf.process?(strongSelf.currentIds) + } + } + self.timer = timer + timer.start() + } + } + } + } +} diff --git a/Telegram-Mac/ChatMessageView.swift b/Telegram-Mac/ChatMessageView.swift index 81be3be4ed..0d595f4133 100644 --- a/Telegram-Mac/ChatMessageView.swift +++ b/Telegram-Mac/ChatMessageView.swift @@ -8,12 +8,35 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac -class ChatMessageView: ChatRowView { - private var text:TextView = TextView() - - private var webpageContent:WPContentView? +import SwiftSignalKit +class ChatMessageView: ChatRowView, ModalPreviewRowViewProtocol { + + + + func fileAtPoint(_ point: NSPoint) -> (QuickPreviewMedia, NSView?)? { + if let webpageContent = webpageContent { + return webpageContent.fileAtPoint(convert(point, from: self)) + } + + return nil + } + + override func forceClick(in location: NSPoint) { + if previewMediaIfPossible() { + + } else { + super.forceClick(in: location) + } + } + override func previewMediaIfPossible() -> Bool { + return webpageContent?.previewMediaIfPossible() ?? false + } + + private let text:TextView = TextView() + + private(set) var webpageContent:WPContentView? + private var actionButton: TitleButton? override func draw(_ dirtyRect: NSRect) { // Drawing code here. @@ -22,18 +45,28 @@ class ChatMessageView: ChatRowView { required init(frame frameRect: NSRect) { super.init(frame: frameRect) + // self.layerContentsRedrawPolicy = .never self.addSubview(text) } override func layout() { super.layout() if let item = self.item as? ChatMessageItem { - self.text.update(item.textLayout) - + if let webpageLayout = item.webpageLayout { - webpageContent?.frame = NSMakeRect(0, text.frame.maxY + item.defaultContentTopOffset, webpageLayout.size.width, webpageLayout.size.height) + webpageContent?.frame = NSMakeRect(0, text.frame.maxY + item.defaultContentInnerInset, webpageLayout.size.width, webpageLayout.size.height) } - } + if let actionButton = actionButton { + var add = item.additionalLineForDateInBubbleState ?? 0 + if !item.isBubbled { + add = 0 + } else if webpageContent != nil { + add = 0 + } + let contentRect = self.contentFrame(item) + actionButton.setFrameOrigin(contentRect.minX, contentRect.maxY - actionButton.frame.height + add) + } + } } override func canStartTextSelecting(_ event: NSEvent) -> Bool { @@ -48,16 +81,33 @@ class ChatMessageView: ChatRowView { } override var selectableTextViews: [TextView] { - let views:[TextView] = [text] -// if let webpage = webpageContent { -// views += webpage.selectableTextViews -// } + var views:[TextView] = [text] + if let webpage = webpageContent { + views += webpage.selectableTextViews + } return views } + + override func updateMouse() { + super.updateMouse() + webpageContent?.updateMouse() + } + + override func canMultiselectTextIn(_ location: NSPoint) -> Bool { + let point = self.contentView.convert(location, from: nil) +// if let webpageContent = webpageContent { +// return !NSPointInRect(point, webpageContent.frame) +// } + return true + } override func set(item:TableRowItem, animated:Bool = false) { if let item = item as? ChatMessageItem { + + self.text.update(item.textLayout) + + if let webpageLayout = item.webpageLayout { let updated = webpageContent == nil || !webpageContent!.isKind(of: webpageLayout.viewClass()) @@ -72,21 +122,64 @@ class ChatMessageView: ChatRowView { webpageContent?.removeFromSuperview() webpageContent = nil } + + + if let text = item.actionButtonText { + if actionButton == nil { + actionButton = TitleButton() + actionButton?.layer?.cornerRadius = .cornerRadius + actionButton?.layer?.borderWidth = 1 + actionButton?.disableActions() + actionButton?.set(font: .normal(.text), for: .Normal) + self.rowView.addSubview(actionButton!) + } + actionButton?.scaleOnClick = true + actionButton?.removeAllHandlers() + actionButton?.set(handler: { [weak item] _ in + item?.invokeAction() + }, for: .Click) + actionButton?.set(text: text, for: .Normal) + actionButton?.layer?.borderColor = item.wpPresentation.activity.cgColor + actionButton?.set(color: item.wpPresentation.activity, for: .Normal) + _ = actionButton?.sizeToFit(NSZeroSize, NSMakeSize(item.actionButtonWidth, 30), thatFit: true) + + } else { + actionButton?.removeFromSuperview() + actionButton = nil + } + } super.set(item: item, animated: animated) } + override func clickInContent(point: NSPoint) -> Bool { + guard let item = item as? ChatMessageItem else {return true} + + let point = text.convert(point, from: self) + let layout = item.textLayout + + let index = layout.findIndex(location: point) + return index >= 0 && point.x < layout.lines[index].frame.maxX + } - - override var interactionContentView: NSView { + override func interactionContentView(for innerId: AnyHashable, animateIn: Bool ) -> NSView { if let webpageContent = webpageContent { - return webpageContent.interactionContentView + return webpageContent.interactionContentView(for: innerId, animateIn: animateIn) } return self } + override func convertWindowPointToContent(_ point: NSPoint) -> NSPoint { + let main = super.convertWindowPointToContent(point) + + if let webpageContent = webpageContent, NSPointInRect(main, webpageContent.frame) { + return webpageContent.convertWindowPointToContent(point) + } else { + return main + } + } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") diff --git a/Telegram-Mac/ChatMusicContentView.swift b/Telegram-Mac/ChatMusicContentView.swift index 720d6f439c..0a8e3e8e16 100644 --- a/Telegram-Mac/ChatMusicContentView.swift +++ b/Telegram-Mac/ChatMusicContentView.swift @@ -7,26 +7,150 @@ // import Cocoa -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit import TGUIKit class ChatMusicContentView: ChatAudioContentView { - - override func update(with media: Media, size: NSSize, account: Account, parent: Message?, table: TableView?, parameters: ChatMediaLayoutParameters?, animated: Bool) { - super.update(with: media, size: size, account: account, parent: parent, table: table, parameters: parameters, animated: animated) + private let imageView: TransformImageView = TransformImageView(frame: NSMakeRect(0, 0, 40, 40)) + private var playAnimationView: PeerMediaPlayerAnimationView? + private let partHeaderDisposable = MetaDisposable() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(imageView, positioned: .below, relativeTo: progressView) + progressView.theme = RadialProgressTheme(backgroundColor: theme.colors.blackTransparent, foregroundColor: .white, icon: nil) + } + + override var fetchStatus: MediaResourceStatus? { + didSet { + if let fetchStatus = fetchStatus { + switch fetchStatus { + case let .Fetching(_, progress): + let sentGrouped = parent?.groupingKey != nil && (parent!.flags.contains(.Sending) || parent!.flags.contains(.Unsent)) + if progress == 1.0, sentGrouped { + progressView.state = .Success + } else { + progressView.state = .Fetching(progress: progress, force: false) + } + progressView.isHidden = false + case .Remote: + progressView.isHidden = true + case .Local: + progressView.isHidden = true + } + } + } + } + + override func viewDidMoveToWindow() { + if window != nil { + if let playAnimationView = playAnimationView { + if playAnimationView.isPlaying { + playAnimationView.animateToPlaying() + } else { + playAnimationView.animateToPaused() + } + } + } else { + playAnimationView?.animateToPaused() + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func update(with media: Media, size: NSSize, context: AccountContext, parent: Message?, table: TableView?, parameters: ChatMediaLayoutParameters?, animated: Bool, positionFlags: LayoutPositionFlags? = nil, approximateSynchronousValue: Bool = false) { + super.update(with: media, size: size, context: context, parent: parent, table: table, parameters: parameters, animated: animated, positionFlags: positionFlags) if let parameters = parameters as? ChatMediaMusicLayoutParameters { textView.update(parameters.nameLayout) durationView.update(parameters.durationLayout) } + + let iconSize = CGSize(width: 40, height: 40) + let imageCorners = ImageCorners(radius: 20) + let arguments = TransformImageArguments(corners: imageCorners, imageSize: iconSize, boundingSize: iconSize, intrinsicInsets: NSEdgeInsets()) + + let file = media as! TelegramMediaFile + + let resource: TelegramMediaResource? + if file.previewRepresentations.isEmpty { + if !file.mimeType.contains("ogg") { + resource = ExternalMusicAlbumArtResource(title: file.musicText.0, performer: file.musicText.1, isThumbnail: true) + } else { + resource = nil + } + } else { + resource = file.previewRepresentations.first!.resource + } + imageView.layer?.contents = theme.icons.chatMusicPlaceholder + + if let resource = resource { + let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [TelegramMediaImageRepresentation(dimensions: PixelDimensions(iconSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + + imageView.setSignal(signal: cachedMedia(media: media, arguments: arguments, scale: backingScaleFactor, positionFlags: positionFlags), clearInstantly: false) + + if !imageView.isFullyLoaded { + imageView.setSignal( chatMessagePhotoThumbnail(account: context.account, imageReference: parent != nil ? ImageMediaReference.message(message: MessageReference(parent!), media: image) : ImageMediaReference.standalone(media: image)), animate: true, cacheImage: { [weak media] result in + if let media = media { + cacheMedia(result, media: media, arguments: arguments, scale: System.backingScale, positionFlags: positionFlags) + } + }) + } + + imageView.set(arguments: arguments) + } + + + // imageView.layer?.cornerRadius = 20 } + override func checkState(animated: Bool) { + if let parent = parent, let controller = globalAudio, let song = controller.currentSong { + if song.entry.isEqual(to: parent) { + if playAnimationView == nil { + playAnimationView = PeerMediaPlayerAnimationView() + playAnimationView?.layer?.cornerRadius = 20 + imageView.addSubview(playAnimationView!) + } + if case .playing = song.state { + playAnimationView?.isPlaying = true + } else if case .stoped = song.state { + playAnimationView?.removeFromSuperview() + playAnimationView = nil + } else { + playAnimationView?.isPlaying = false + } + } else { + playAnimationView?.removeFromSuperview() + playAnimationView = nil + } + } else { + playAnimationView?.removeFromSuperview() + playAnimationView = nil + } + } + + override func preloadStreamblePart() { + if let context = context { + if let media = media as? TelegramMediaFile { + let reference = parent != nil ? FileMediaReference.message(message: MessageReference(parent!), media: media) : FileMediaReference.standalone(media: media) + partHeaderDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: reference.resourceReference(media.resource), range: (0 ..< 500 * 1024, .default), statsCategory: .audio).start()) + + } + } + } + + deinit { + partHeaderDisposable.dispose() + } override func layout() { super.layout() - let center = floorToScreenPixels(frame.height / 2.0) + let center = floorToScreenPixels(backingScaleFactor, frame.height / 2.0) textView.setFrameOrigin(leftInset, center - textView.frame.height - 2) durationView.setFrameOrigin(leftInset, center + 2) } diff --git a/Telegram-Mac/ChatMusicRowItem.swift b/Telegram-Mac/ChatMusicRowItem.swift index bea4cfac5c..8a06b1676a 100644 --- a/Telegram-Mac/ChatMusicRowItem.swift +++ b/Telegram-Mac/ChatMusicRowItem.swift @@ -8,9 +8,10 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac +import TelegramCore + +import SwiftSignalKit +import Postbox class ChatMediaMusicLayoutParameters : ChatMediaLayoutParameters { let resource: TelegramMediaResource let title:String? @@ -20,7 +21,7 @@ class ChatMediaMusicLayoutParameters : ChatMediaLayoutParameters { let showPlayer:(APController) -> Void let durationLayout:TextViewLayout let sizeLayout:TextViewLayout - init(nameLayout:TextViewLayout, durationLayout:TextViewLayout, sizeLayout:TextViewLayout, resource:TelegramMediaResource, isWebpage: Bool, title:String?, performer:String?, showPlayer:@escaping(APController) -> Void) { + init(nameLayout:TextViewLayout, durationLayout:TextViewLayout, sizeLayout:TextViewLayout, resource:TelegramMediaResource, isWebpage: Bool, title:String?, performer:String?, showPlayer:@escaping(APController) -> Void, presentation: ChatMediaPresentation, media: Media, automaticDownload: Bool) { self.nameLayout = nameLayout self.sizeLayout = sizeLayout self.durationLayout = durationLayout @@ -29,16 +30,48 @@ class ChatMediaMusicLayoutParameters : ChatMediaLayoutParameters { self.title = title self.performer = performer self.resource = resource + super.init(presentation: presentation, media: media, automaticDownload: automaticDownload, autoplayMedia: AutoplayMediaPreferences.defaultSettings) + } + + var file: TelegramMediaFile { + return media as! TelegramMediaFile } + override func makeLabelsForWidth(_ width: CGFloat) -> CGFloat { + nameLayout.measure(width: width - 50) + durationLayout.measure(width: width - 50) + sizeLayout.measure(width: width - 50) + + + return max(nameLayout.layoutSize.width, durationLayout.layoutSize.width, sizeLayout.layoutSize.width) + 50 + } } class ChatMusicRowItem: ChatMediaItem { - override init(_ initialSize:NSSize, _ chatInteraction:ChatInteraction, _ account: Account, _ object: ChatHistoryEntry) { - super.init(initialSize, chatInteraction, account, object) - self.parameters = ChatMediaLayoutParameters.layout(for: (self.media as! TelegramMediaFile), isWebpage: chatInteraction.isLogInteraction, chatInteraction: chatInteraction) + override init(_ initialSize:NSSize, _ chatInteraction:ChatInteraction, _ context: AccountContext, _ object: ChatHistoryEntry, _ downloadSettings: AutomaticMediaDownloadSettings, theme: TelegramPresentationTheme) { + super.init(initialSize, chatInteraction, context, object, downloadSettings, theme: theme) + + + self.parameters = ChatMediaLayoutParameters.layout(for: (self.media as! TelegramMediaFile), isWebpage: chatInteraction.isLogInteraction, chatInteraction: chatInteraction, presentation: .make(for: object.message!, account: context.account, renderType: object.renderType, theme: theme), automaticDownload: downloadSettings.isDownloable(object.message!), isIncoming: object.message!.isIncoming(context.account, object.renderType == .bubble), autoplayMedia: object.autoplayMedia) + } + + override var additionalLineForDateInBubbleState: CGFloat? { + if isForceRightLine { + return rightSize.height + } + if let parameters = parameters as? ChatMediaMusicLayoutParameters { + if parameters.durationLayout.layoutSize.width + 50 + rightSize.width + insetBetweenContentAndDate > contentSize.width { + return rightSize.height + } + } + if let caption = captionLayouts.last?.layout { + if let line = caption.lines.last, line.frame.width > realContentSize.width - (rightSize.width + insetBetweenContentAndDate) { + return rightSize.height + } + } + return super.additionalLineForDateInBubbleState } override var instantlyResize: Bool { @@ -47,10 +80,19 @@ class ChatMusicRowItem: ChatMediaItem { override func makeContentSize(_ width: CGFloat) -> NSSize { if let parameters = parameters as? ChatMediaMusicLayoutParameters { - parameters.nameLayout.measure(width: width - 20) - parameters.durationLayout.measure(width: width - 20) - parameters.sizeLayout.measure(width: width - 20) - return NSMakeSize(parameters.nameLayout.layoutSize.width + 50, 40) + + + let width = min(320, width - 80) + + for layout in captionLayouts { + if layout.layout.layoutSize == .zero { + layout.layout.measure(width: width) + } + } + let captionsWidth = captionLayouts.max(by: { $0.layout.layoutSize.width < $1.layout.layoutSize.width }).map { $0.layout.layoutSize.width } + + let labelsWidth = parameters.makeLabelsForWidth(width) + return NSMakeSize(max(captionsWidth ?? 0, labelsWidth) + 50, 40) } return NSZeroSize } diff --git a/Telegram-Mac/ChatNavigateFailed.swift b/Telegram-Mac/ChatNavigateFailed.swift new file mode 100644 index 0000000000..50ab8dbfb5 --- /dev/null +++ b/Telegram-Mac/ChatNavigateFailed.swift @@ -0,0 +1,66 @@ +// +// ChatNavigateFailed.swift +// Telegram +// +// Created by Mikhail Filimonov on 20.12.2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import SwiftSignalKit +import Postbox + + +class ChatNavigateFailed: ImageButton { + + private let context:AccountContext + init(_ context: AccountContext) { + self.context = context + super.init() + autohighlight = false + set(image: theme.icons.chat_failed_scroller, for: .Normal) + set(image: theme.icons.chat_failed_scroller_active, for: .Highlight) + self.setFrameSize(60,60) + + let shadow = NSShadow() + shadow.shadowBlurRadius = 5 + shadow.shadowColor = NSColor.black.withAlphaComponent(0.1) + shadow.shadowOffset = NSMakeSize(0, 2) + self.shadow = shadow + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) + set(image: theme.icons.chat_failed_scroller, for: .Normal) + set(image: theme.icons.chat_failed_scroller_active, for: .Highlight) + } + + func updateCount(_ count: Int) { + //needsLayout = true + } + + override func scrollWheel(with event: NSEvent) { + + } + + override func layout() { + super.layout() + } + + deinit { + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + +} diff --git a/Telegram-Mac/ChatNavigateScroller.swift b/Telegram-Mac/ChatNavigateScroller.swift index 54e68f139a..25b1c30f7c 100644 --- a/Telegram-Mac/ChatNavigateScroller.swift +++ b/Telegram-Mac/ChatNavigateScroller.swift @@ -8,9 +8,10 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac +import TelegramCore + +import SwiftSignalKit +import Postbox class ChatNavigateScroller: ImageButton { @@ -18,22 +19,22 @@ class ChatNavigateScroller: ImageButton { private let disposable:MetaDisposable = MetaDisposable() private var badge:BadgeNode? private var badgeView:View = View() - private let peerId:PeerId - private let account:Account - init(_ account:Account, _ peerId:PeerId) { - self.account = account - self.peerId = peerId + private let context:AccountContext + init(_ context: AccountContext, contextHolder: Atomic, chatLocation: ChatLocation, mode: ChatMode) { + self.context = context super.init() autohighlight = false set(image: theme.icons.chatScrollUp, for: .Normal) set(image: theme.icons.chatScrollUpActive, for: .Highlight) self.setFrameSize(60,60) - self.disposable.set((account.postbox.unreadMessageCountsView(items: [.peer(peerId)]) |> deliverOnMainQueue).start(next: { [weak self] unreadView in + let unreadCount = context.chatLocationUnreadCount(for: chatLocation, contextHolder: contextHolder) + |> deliverOnMainQueue + + self.disposable.set(unreadCount.start(next: { [weak self] count in if let strongSelf = self { - let count = unreadView.count(for: .peer(peerId)) ?? 0 if count > 0 { - strongSelf.badge = BadgeNode(.initialize(string: Int(count).prettyNumber, color: .white, font: .bold(.small)), theme.colors.blueUI) + strongSelf.badge = BadgeNode(.initialize(string: Int(count).prettyNumber, color: theme.colors.underSelectedColor, font: .bold(.small)), theme.colors.accent) strongSelf.badge!.view = strongSelf.badgeView strongSelf.badgeView.setFrameSize(strongSelf.badge!.size) strongSelf.addSubview(strongSelf.badgeView) @@ -41,16 +42,28 @@ class ChatNavigateScroller: ImageButton { strongSelf.badgeView.removeFromSuperview() } strongSelf.needsLayout = true - } })) + + updateLocalizationAndTheme(theme: theme) } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) set(image: theme.icons.chatScrollUp, for: .Normal) set(image: theme.icons.chatScrollUpActive, for: .Highlight) - badge?.fillColor = theme.colors.blueUI + badge?.fillColor = theme.colors.accent + + if theme.colors.chatBackground == theme.colors.background && theme.colors.isDark { + + + } + let shadow = NSShadow() + shadow.shadowBlurRadius = 5 + shadow.shadowColor = NSColor.black.withAlphaComponent(0.1) + shadow.shadowOffset = NSMakeSize(0, 2) + self.shadow = shadow } override func scrollWheel(with event: NSEvent) { diff --git a/Telegram-Mac/ChatNavigationMention.swift b/Telegram-Mac/ChatNavigationMention.swift index 4fa0d158cc..8c6d06201b 100644 --- a/Telegram-Mac/ChatNavigationMention.swift +++ b/Telegram-Mac/ChatNavigationMention.swift @@ -8,8 +8,9 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac +import TelegramCore + +import Postbox class ChatNavigationMention: ImageButton { @@ -24,11 +25,17 @@ class ChatNavigationMention: ImageButton { set(image: theme.icons.chatMentionActive, for: .Highlight) self.setFrameSize(60,60) + let shadow = NSShadow() + shadow.shadowBlurRadius = 5 + shadow.shadowColor = NSColor.black.withAlphaComponent(0.1) + shadow.shadowOffset = NSMakeSize(0, 2) + self.shadow = shadow + } func updateCount(_ count: Int32) { if count > 0 { - badge = BadgeNode(.initialize(string: Int(count).prettyNumber, color: .white, font: .bold(.small)), theme.colors.blueUI) + badge = BadgeNode(.initialize(string: Int(count).prettyNumber, color: .white, font: .bold(.small)), theme.colors.accent) badge!.view = badgeView badgeView.setFrameSize(badge!.size) addSubview(badgeView) @@ -38,8 +45,9 @@ class ChatNavigationMention: ImageButton { needsLayout = true } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) set(image: theme.icons.chatMention, for: .Normal) set(image: theme.icons.chatMentionActive, for: .Highlight) } diff --git a/Telegram-Mac/ChatPollItem.swift b/Telegram-Mac/ChatPollItem.swift new file mode 100644 index 0000000000..d10b3054fc --- /dev/null +++ b/Telegram-Mac/ChatPollItem.swift @@ -0,0 +1,1264 @@ +// +// ChatPollItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 18/12/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + + + + +func isPollEffectivelyClosed(message: Message, poll: TelegramMediaPoll) -> Bool { + if poll.isClosed { + return true + } /*else if let deadlineTimeout = poll.deadlineTimeout, message.id.namespace == Namespaces.Message.Cloud { + let startDate: Int32 + if let forwardInfo = message.forwardInfo { + startDate = forwardInfo.date + } else { + startDate = message.timestamp + } + + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + if timestamp >= startDate + deadlineTimeout { + return true + } else { + return false + } + }*/ else { + return false + } +} + + + + +extension TelegramMediaPoll { + var title: String { + if isClosed { + return L10n.chatPollTypeClosed + } else { + switch self.kind { + case .quiz: + switch self.publicity { + case .anonymous: + return L10n.chatPollTypeAnonymousQuiz + case .public: + return L10n.chatPollTypeQuiz + } + default: + switch self.publicity { + case .anonymous: + return L10n.chatPollTypeAnonymous + case .public: + return L10n.chatPollTypePublic + } + } + } + } + + var isMultiple: Bool { + switch kind { + case let .poll(multipleAnswers): + return multipleAnswers + default: + return false + } + } + var isQuiz: Bool { + switch kind { + case .poll: + return false + default: + return true + } + } +} + +private struct PercentCounterItem : Comparable { + var index: Int = 0 + var percent: Int = 0 + var remainder: Int = 0 + + static func <(lhs: PercentCounterItem, rhs: PercentCounterItem) -> Bool { + if lhs.remainder > rhs.remainder { + return true + } else if lhs.remainder < rhs.remainder { + return false + } + return lhs.percent < rhs.percent + } + +} + +private func adjustPercentCount(_ items: [PercentCounterItem], left: Int) -> [PercentCounterItem] { + var left = left + var items = items.sorted(by: <) + var i:Int = 0 + while i != items.count { + let item = items[i] + var j = i + 1 + loop: while j != items.count { + if items[j].percent != item.percent || items[j].remainder != item.remainder { + break loop + } + j += 1 + } + if items[i].remainder == 0 { + break + } + let equal = j - i + if equal <= left { + left -= equal + while i != j { + items[i].percent += 1 + i += 1 + } + } else { + i = j + } + } + return items +} + +func countNicePercent(votes:[Int], total: Int) -> [Int] { + var result:[Int] = Array(repeating: 0, count: votes.count) + var items:[PercentCounterItem] = Array(repeating: PercentCounterItem(), count: votes.count) + + guard total > 0 else { + return result + } + + let count = votes.count + + var left:Int = 100 + for i in 0 ..< votes.count { + let votes = votes[i] + items[i].index = i + items[i].percent = Int((Float(votes) * 100) / Float(total)) + items[i].remainder = (votes * 100) - (items[i].percent * total) + left -= items[i].percent + } + + if left > 0 && left <= count { + items = adjustPercentCount(items, left: left) + } + for item in items { + result[item.index] = item.percent + } + + return result +} + + + +private final class PollOption : Equatable { + let option: TelegramMediaPollOption + let nameText: TextViewLayout + let percent: Float? + let voteCount: Int32 + let realPercent: Float + let isSelected: Bool + let voted: Bool + let isIncoming: Bool + let isBubbled: Bool + let isLoading: Bool + let presentation: TelegramPresentationTheme + let contentSize: NSSize + let vote:(Control)-> Void + let isCorrect: Bool? + let isQuiz: Bool + let isMultipleSelected: Bool + init(option:TelegramMediaPollOption, nameText: TextViewLayout, percent: Float?, realPercent: Float, voteCount: Int32, isSelected: Bool, isIncoming: Bool, isBubbled: Bool, voted: Bool, isLoading: Bool, presentation: TelegramPresentationTheme, isCorrect: Bool?, isQuiz: Bool, isMultipleSelected: Bool, vote: @escaping(Control)->Void = { _ in }, contentSize: NSSize = NSZeroSize) { + self.option = option + self.nameText = nameText + self.percent = percent + self.realPercent = realPercent + self.isSelected = isSelected + self.voted = voted + self.presentation = presentation + self.isIncoming = isIncoming + self.isBubbled = isBubbled + self.isLoading = isLoading + self.vote = vote + self.voteCount = voteCount + self.contentSize = contentSize + self.isCorrect = isCorrect + self.isQuiz = isQuiz + self.isMultipleSelected = isMultipleSelected + } + + func withUpdatedLoading(_ isLoading: Bool) -> PollOption { + return PollOption(option: self.option, nameText: self.nameText, percent: self.percent, realPercent: self.realPercent, voteCount: self.voteCount, isSelected: self.isSelected, isIncoming: self.isIncoming, isBubbled: self.isBubbled, voted: self.voted, isLoading: isLoading, presentation: self.presentation, isCorrect: self.isCorrect, isQuiz: self.isQuiz, isMultipleSelected: self.isMultipleSelected, vote: self.vote, contentSize: self.contentSize) + } + func withUpdatedContentSize(_ contentSize: NSSize) -> PollOption { + return PollOption(option: self.option, nameText: self.nameText, percent: self.percent, realPercent: self.realPercent, voteCount: self.voteCount, isSelected: self.isSelected, isIncoming: self.isIncoming, isBubbled: self.isBubbled, voted: self.voted, isLoading: self.isLoading, presentation: self.presentation, isCorrect: self.isCorrect, isQuiz: self.isQuiz, isMultipleSelected: self.isMultipleSelected, vote: self.vote, contentSize: contentSize) + } + func withUpdatedSelected(_ isSelected: Bool) -> PollOption { + return PollOption(option: self.option, nameText: self.nameText, percent: self.percent, realPercent: self.realPercent, voteCount: self.voteCount, isSelected: isSelected, isIncoming: self.isIncoming, isBubbled: self.isBubbled, voted: self.voted, isLoading: self.isLoading, presentation: self.presentation, isCorrect: self.isCorrect, isQuiz: self.isQuiz, isMultipleSelected: self.isMultipleSelected, vote: self.vote, contentSize: self.contentSize) + } + + + static func ==(lhs: PollOption, rhs: PollOption) -> Bool { + return lhs.option == rhs.option && lhs.percent == rhs.percent && lhs.isSelected == rhs.isSelected && lhs.isIncoming == rhs.isIncoming && lhs.isLoading == rhs.isLoading && lhs.contentSize == rhs.contentSize && lhs.voted == rhs.voted && lhs.realPercent == rhs.realPercent && lhs.voteCount == rhs.voteCount && lhs.isCorrect == rhs.isCorrect && lhs.isQuiz == rhs.isQuiz && lhs.isMultipleSelected == rhs.isMultipleSelected + } + + + var leftOptionInset: CGFloat { + return 40 + PollOption.spaceBetweenTexts + } + var currentPercentImage: CGImage? { + return presentation.chat.pollPercentAnimatedIcon(isIncoming, isBubbled, value: Int(realPercent)) + } + + static var spaceBetweenTexts: CGFloat { + return 6 + } + static var spaceBetweenOptions: CGFloat { + return 5 + } + + var tooltip: String { + var totalOptionVotes = self.isQuiz ? L10n.chatQuizTooltipVotesCountable(Int(self.voteCount)) : L10n.chatPollTooltipVotesCountable(Int(self.voteCount)) + totalOptionVotes = totalOptionVotes.replacingOccurrences(of: "\(self.voteCount)", with: Int(self.voteCount).separatedNumber) + return self.voteCount == 0 ? (self.isQuiz ? L10n.chatQuizTooltipNoVotes : L10n.chatPollTooltipNoVotes) : totalOptionVotes + } + + func measure(width: CGFloat) -> NSSize { + nameText.measure(width: width - leftOptionInset) + let contentSize = NSMakeSize(nameText.layoutSize.width + leftOptionInset, 10 + nameText.layoutSize.height + PollOption.spaceBetweenOptions) + return contentSize + } +} + +class ChatPollItem: ChatRowItem { + private(set) fileprivate var titleText:TextViewLayout! + private(set) fileprivate var titleTypeText:TextViewLayout! + + private(set) fileprivate var options:[PollOption] = [] + private(set) fileprivate var totalVotesText:TextViewLayout? + + fileprivate let poll: TelegramMediaPoll + + var actionButtonText: String? { + if isBotQuiz { + return nil + } + if self.isClosed { + if poll.results.totalVoters == 0 || poll.results.totalVoters == nil { + return nil + } + if poll.publicity != .anonymous { + return L10n.chatPollViewResults + } else { + return nil + } + } + let hasSelected = options.contains(where: { $0.isSelected }) + if poll.isMultiple { + if !hasSelected { + return L10n.chatPollSubmitVote + } else { + if poll.publicity != .anonymous { + if hasSelected { + return L10n.chatPollViewResults + } + } + } + } else { + if poll.publicity != .anonymous { + if hasSelected { + return L10n.chatPollViewResults + } + } + } + return nil + } + + var actionButtonIsEnabled: Bool { + guard let message = message else { + return false + } + if message.flags.contains(.Failed) || message.flags.contains(.Sending) || message.flags.contains(.Unsent) { + return false + } + let hasSelected = options.contains(where: { $0.isMultipleSelected }) || options.contains(where: { $0.isSelected }) + if poll.isMultiple { + return hasSelected + } else { + return true + } + } + + var isClosed: Bool { + return isPollEffectivelyClosed(message: message!, poll: poll) + } + var isBotQuiz: Bool { + if let message = message { + if self.poll.isQuiz { + return messageMainPeer(message)?.isBot == true + } + } + return false + } + + override init(_ initialSize: NSSize, _ chatInteraction: ChatInteraction, _ context: AccountContext, _ object: ChatHistoryEntry, _ downloadSettings: AutomaticMediaDownloadSettings, theme: TelegramPresentationTheme) { + + let poll = object.message!.media[0] as! TelegramMediaPoll + self.poll = poll + + super.init(initialSize, chatInteraction, context, object, downloadSettings, theme: theme) + + + + var options: [PollOption] = [] + + + var votes:[Int] = [] + + for option in poll.options { + let count = Int(poll.results.voters?.first(where: {$0.opaqueIdentifier == option.opaqueIdentifier})?.count ?? 0) + votes.append(count) + } + + + + let percents = countNicePercent(votes: votes, total: Int(poll.results.totalVoters ?? 0)) + let maximum: Int = percents.max() ?? 0 + + + for (i, option) in poll.options.enumerated() { + + let percent: Float? + let realPercent: Float + let isSelected: Bool + let isCorrect: Bool? + let voted = poll.results.voters?.first(where: {$0.selected}) != nil + + var votedCount: Int32 = 0 + if let vote = poll.results.voters?.first(where: {$0.opaqueIdentifier == option.opaqueIdentifier}), let totalVoters = poll.results.totalVoters, (voted || self.isClosed) { + percent = maximum == 0 ? 0 : (Float(percents[i]) / Float(maximum)) + realPercent = totalVoters == 0 ? 0 : Float(percents[i]) + isSelected = vote.selected + votedCount = vote.count + if poll.kind == .quiz { + isCorrect = vote.isCorrect + } else { + isCorrect = nil + } + } else { + percent = poll.results.totalVoters == nil || poll.results.totalVoters == 0 ? (isClosed ? 0 : nil) : voted ? 0 : (isClosed ? 0 : nil) + realPercent = 0 + isSelected = false + isCorrect = nil + } + + let nameFont: NSFont = .normal(.text)//voted && isSelected ? .bold(.text) : .normal(.text) + let nameLayout = TextViewLayout(.initialize(string: option.text, color: self.presentation.chat.textColor(isIncoming, renderType == .bubble), font: nameFont), alwaysStaticItems: true) + + + let wrapper = PollOption(option: option, nameText: nameLayout, percent: percent, realPercent: realPercent, voteCount: votedCount, isSelected: isSelected, isIncoming: isIncoming, isBubbled: renderType == .bubble, voted: voted, isLoading: object.additionalData.pollStateData.identifiers.contains(option.opaqueIdentifier) && object.additionalData.pollStateData.isLoading, presentation: self.presentation, isCorrect: isCorrect, isQuiz: poll.kind == .quiz, isMultipleSelected: object.additionalData.pollStateData.identifiers.contains(option.opaqueIdentifier), vote: { [weak self] control in + self?.voteOption(option, for: control) + }) + + options.append(wrapper) + } + self.options = options + + + let totalCount = poll.results.totalVoters ?? 0 + + var totalText = poll.isQuiz ? L10n.chatQuizTotalVotesCountable(Int(totalCount)) : L10n.chatPollTotalVotes1Countable(Int(totalCount)) + totalText = totalText.replacingOccurrences(of: "\(totalCount)", with: Int(totalCount).separatedNumber) + + if actionButtonText == nil && !isBotQuiz { + let text: String + if totalCount > 0 { + text = totalText + } else { + if poll.isQuiz { + text = self.isClosed ? L10n.chatQuizTotalVotesResultEmpty : L10n.chatQuizTotalVotesEmpty + } else { + text = self.isClosed ? L10n.chatPollTotalVotesResultEmpty : L10n.chatPollTotalVotesEmpty + } + } + self.totalVotesText = TextViewLayout(.initialize(string: text, color: self.presentation.chat.grayText(isIncoming, renderType == .bubble), font: .normal(12)), maximumNumberOfLines: 1, alwaysStaticItems: true) + } else { + self.totalVotesText = nil + } + + + + self.titleText = TextViewLayout(.initialize(string: poll.text, color: self.presentation.chat.textColor(isIncoming, renderType == .bubble), font: .medium(.text)), alwaysStaticItems: true) + + let typeText: String = self.isBotQuiz ? L10n.chatQuizTextType : poll.title + + self.titleTypeText = TextViewLayout(.initialize(string: typeText, color: self.presentation.chat.grayText(isIncoming, renderType == .bubble), font: .normal(12)), maximumNumberOfLines: 1, alwaysStaticItems: true) + } + + override var additionalLineForDateInBubbleState: CGFloat? { + var size: NSSize = .zero + if let action = self.actionButtonText { + size = TitleButton.size(with: action, font: .normal(.text)) + } else if let totalVotesText = self.totalVotesText { + size = totalVotesText.layoutSize + } + + if size.width > 0 { + let dif = contentSize.width - (contentSize.width / 2 + size.width / 2) + if dif < (rightSize.width + insetBetweenContentAndDate) { + return 20 + } + + } + + if isBotQuiz { + return 10 + } + + return super.additionalLineForDateInBubbleState + } + + + override var isFixedRightPosition: Bool { + return true + } + + override func menuItems(in location: NSPoint) -> Signal<[ContextMenuItem], NoError> { + return super.menuItems(in: location) |> map { [weak self] items in + guard let `self` = self, let message = self.message else { return items } + var items = items + if let poll = message.media.first as? TelegramMediaPoll { + if !self.isClosed && !message.flags.contains(.Unsent) && !message.flags.contains(.Failed) { + var index: Int = 0 + if let _ = poll.results.voters?.first(where: {$0.selected}), poll.kind != .quiz { + items.insert(ContextMenuItem(L10n.chatPollUnvote, handler: { [weak self] in + self?.unvote() + }), at: index) + index += 1 + } + if message.forwardInfo == nil { + var canClose: Bool = message.author?.id == self.context.peerId + if let peer = self.peer as? TelegramChannel { + canClose = peer.hasPermission(.sendMessages) || peer.hasPermission(.editAllMessages) + } + if canClose { + + items.insert(ContextMenuItem(poll.kind == .quiz ? L10n.chatQuizStop : L10n.chatPollStop, handler: { [weak self] in + confirm(for: mainWindow, header: poll.kind == .quiz ? L10n.chatQuizStopConfirmHeader : L10n.chatPollStopConfirmHeader, information: poll.kind == .quiz ? L10n.chatQuizStopConfirmText : L10n.chatPollStopConfirmText, okTitle: L10n.alertConfirmStop, successHandler: { [weak self] _ in + self?.stop() + }) + }), at: index) + index += 1 + } + } + if index != 0 { + items.insert(ContextSeparatorItem(), at: index) + } + } + + } + return items + } + } + + private func stop() { + if let message = message { + chatInteraction.closePoll(message.id) + } + } + + private func unvote() { + + if canInvokeVote { + guard let message = message else { return } + self.chatInteraction.vote(message.id, [], true) + } + + } + + private func voteOption(_ option: TelegramMediaPollOption, for control: Control) { + if canInvokeVote, !self.options.contains(where: { $0.isSelected }) { + guard let message = message else { return } + var identifiers = self.entry.additionalData.pollStateData.identifiers + if let index = identifiers.firstIndex(of: option.opaqueIdentifier) { + identifiers.remove(at: index) + } else { + identifiers.append(option.opaqueIdentifier) + } + chatInteraction.vote(message.id, identifiers, !self.poll.isMultiple) + } else { + if self.options.contains(where: { $0.isSelected }) || self.isClosed, self.poll.publicity == .public { + guard let message = message else { + return + } + if message.flags.contains(.Failed) || message.flags.contains(.Unsent) || message.flags.contains(.Sending) || self.options.contains(where: { $0.isLoading }) { + return + } + self.invokeAction(fromOption: option.opaqueIdentifier) + } else if let option = self.options.first(where: { $0.option.opaqueIdentifier == option.opaqueIdentifier }) { + tooltip(for: control, text: option.tooltip) + } + } + } + + + private var canInvokeVote: Bool { + guard let message = message else { + return false + } + if message.flags.contains(.Failed) || message.flags.contains(.Unsent) || message.flags.contains(.Sending) { + return false + } + if self.isClosed { + return false + } + if self.options.contains(where: { $0.isLoading }) { + return false + } + + return true + } + + fileprivate func invokeAction(fromOption: Data? = nil) { + + guard let message = message else { return } + let hasSelected = self.options.contains(where: { $0.isSelected }) + if canInvokeVote, !hasSelected { + let identifiers = self.entry.additionalData.pollStateData.identifiers + chatInteraction.vote(message.id, identifiers, true) + } else { + if !isBotQuiz, let totalVoters = self.poll.results.totalVoters, totalVoters > 0 { + showModal(with: PollResultController(context: context, message: message, scrollToOption: fromOption), for: context.window) + } + } + } + + override func viewClass() -> AnyClass { + return ChatPollItemView.self + } + + override var instantlyResize: Bool { + return true + } + + override func makeContentSize(_ width: CGFloat) -> NSSize { + + let width = min(width, 320) + + + var rightInset: CGFloat = 0 + + if let _ = poll.results.solution, options.contains(where: { $0.isSelected }) || self.isClosed { + rightInset += 10 + } + let deadlineTimeout = poll.deadlineTimeout + let displayDeadline = !options.contains(where: { $0.isSelected }) + + + + titleText.measure(width: width - bubbleContentInset - rightInset) + titleTypeText.measure(width: width - bubbleContentInset - rightInset) + totalVotesText?.measure(width: width - bubbleContentInset) + + + + var maxOptionNameWidth: CGFloat = 0 + for (i, option) in options.enumerated() { + let size = option.measure(width: width) + self.options[i] = option.withUpdatedContentSize(size) + if maxOptionNameWidth < size.width { + maxOptionNameWidth = size.width + } + } + + + let contentWidth:CGFloat = max(max(maxOptionNameWidth, titleText.layoutSize.width), titleTypeText.layoutSize.width) + + var contentHeight: CGFloat = 0 + + contentHeight += titleText.layoutSize.height + defaultContentInnerInset + contentHeight += titleTypeText.layoutSize.height + defaultContentInnerInset + contentHeight += options.reduce(0, { $0 + $1.contentSize.height }) + (CGFloat(options.count - 1) * PollOption.spaceBetweenOptions) + + if let totalVotesText = totalVotesText { + contentHeight += defaultContentInnerInset + contentHeight += totalVotesText.layoutSize.height + } + if let _ = self.actionButtonText { + contentHeight += defaultContentInnerInset + contentHeight += 15 + } + + return NSMakeSize(max(width, contentWidth), contentHeight) + } + + override func copyAndUpdate(animated: Bool) { + if let table = self.table { + let item = ChatRowItem.item(table.frame.size, from: self.entry, interaction: self.chatInteraction, downloadSettings: self.downloadSettings, theme: self.presentation) + _ = item.makeSize(table.frame.width, oldWidth: 0) + let transaction = TableUpdateTransition(deleted: [], inserted: [], updated: [(self.index, item)], animated: animated) + table.merge(with: transaction) + } + } + +} + + +final class ChatPollItemView : ChatRowView { + private var contentNode:PollView = PollView(frame: NSZeroRect) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(contentNode) + } + + override func contentFrameModifier(_ item: ChatRowItem) -> NSRect { + if item.isBubbled { + var frame = bubbleFrame(item) + let contentFrame = self.contentFrame(item) + let contentFrameModifier = super.contentFrameModifier(item) + frame.size.height = contentFrame.height + frame.size.width -= item.additionBubbleInset + frame.origin.y = contentFrameModifier.minY + if item.isIncoming { + frame.origin.x += item.additionBubbleInset + } + return frame + } else { + var frame = super.contentFrameModifier(item) + frame.origin.x -= item.bubbleContentInset + return frame + } + } + + + func doAfterAnswer() { + guard let item = item as? ChatPollItem else { return } + + let selected = item.options.first(where: { $0.isSelected }) + + if let selected = selected { + if item.poll.kind == .quiz { + if let isCorrect = selected.isCorrect { + if isCorrect { + doWhenCorrectAnswer() + } else { + doWhenIncorrectAnswer() + } + } + } + } + } + + func doWhenCorrectAnswer() { + guard let item = item as? ChatPollItem else { return } + PlayConfetti(for: item.context.window) + if FastSettings.inAppSounds { + playSoundEffect(.confetti) + } + } + func doWhenIncorrectAnswer() { + shakeContentView() + + if FastSettings.inAppSounds { + NSSound.beep() + } + self.contentNode.showSolution() + } + + override func set(item: TableRowItem, animated: Bool) { + + guard let item = item as? ChatPollItem else { return } + super.set(item: item, animated: animated) + + contentNode.change(size: NSMakeSize(contentFrameModifier(item).width, item.contentSize.height), animated: animated) + contentNode.update(with: item, animated: animated) + + } + + override func mouseDown(with event: NSEvent) { + super.mouseDown(with: event) + } + + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func canStartTextSelecting(_ event: NSEvent) -> Bool { + + let point = contentView.convert(event.locationInWindow, from: nil) + return NSPointInRect(point, NSMakeRect(0, contentNode.titleView.frame.minY, contentNode.frame.width, contentNode.titleView.frame.height)) + } + + override var selectableTextViews: [TextView] { + return [contentNode.titleView] + } + + override func canMultiselectTextIn(_ location: NSPoint) -> Bool { + let point = contentView.convert(location, from: nil) + return NSPointInRect(point, NSMakeRect(0, contentNode.titleView.frame.minY, contentNode.frame.width, contentNode.titleView.frame.height)) + } + + override var needsDisplay: Bool { + get { + return super.needsDisplay + } + set { + super.needsDisplay = true + contentNode.needsDisplay = true + } + } + + override var backgroundColor: NSColor { + didSet { + + contentNode.backgroundColor = .clear//contentColor + } + } + + override func shakeView() { + contentNode.shake() + } + + + override func draw(_ dirtyRect: NSRect) { + + } + + override func updateColors() { + super.updateColors() + contentNode.backgroundColor = .clear//contentColor + } + + +} + + +private final class PollOptionView : Control { + private var percentView: ImageView? + private let nameView: TextView = TextView() + private var selectingView:ImageView? + private let progressView: LinearProgressControl = LinearProgressControl(progressHeight: 5) + private var progressIndicator: ProgressIndicator? + private let borderView: View = View(frame: NSZeroRect) + + private var selectedImageView: ImageView? + + private var option: PollOption? + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + nameView.userInteractionEnabled = false + nameView.isSelectable = false + progressView.hasMinumimVisibility = true + addSubview(nameView) + addSubview(progressView) + addSubview(borderView) + borderView.userInteractionEnabled = false + progressView.userInteractionEnabled = false + progressView.roundCorners = true + + progressView.isEventLess = true + + set(handler: { [weak self] control in + self?.option?.vote(control) + }, for: .Click) + } + + var defaultInset: CGFloat { + return 13 + } + + func update(with option: PollOption, animated: Bool) { + let animated = animated && self.option != option + let previousOption = self.option + + let previousPercent = self.option?.realPercent + + self.option = option + + + let duration: Double = 0.4 + let timingFunction: CAMediaTimingFunctionName = .spring + + nameView.update(option.nameText, origin: NSMakePoint(option.leftOptionInset, 0)) + progressView.setFrameOrigin(NSMakePoint(nameView.frame.minX, nameView.frame.maxY + 5)) + borderView.backgroundColor = option.presentation.chat.pollOptionBorder(option.isIncoming, option.isBubbled) + borderView.frame = NSMakeRect(nameView.frame.minX, nameView.frame.maxY + 5 - .borderSize + progressView.progressHeight, frame.width - nameView.frame.minX, .borderSize) + borderView.change(opacity: option.percent != nil ? 0 : 1, animated: animated, duration: duration, timingFunction: timingFunction) + progressView.change(opacity: option.percent == nil ? 0 : 1, animated: animated, duration: duration, timingFunction: timingFunction) + + let votedColor: NSColor + + + if option.isSelected { + var justAdded = false + if self.selectedImageView == nil { + self.selectedImageView = ImageView() + addSubview(self.selectedImageView!) + justAdded = true + } + + guard let selectedImageView = self.selectedImageView else { + return + } + + if option.isQuiz, let isCorrect = option.isCorrect { + if isCorrect { + selectedImageView.image = option.presentation.chat.pollSelectedCorrect(option.isIncoming, option.isBubbled, icons: option.presentation.icons) + } else { + selectedImageView.image = option.presentation.chat.pollSelectedIncorrect(option.isIncoming, option.isBubbled, icons: option.presentation.icons) + } + } else { + selectedImageView.image = option.presentation.chat.pollSelected(option.isIncoming, option.isBubbled, icons: option.presentation.icons) + } + selectedImageView.setFrameSize(NSMakeSize(12, 12)) + + selectedImageView.setFrameOrigin(NSMakePoint(progressView.frame.minX - selectedImageView.frame.width - 4, floorToScreenPixels(backingScaleFactor, progressView.frame.midY - selectedImageView.frame.height / 2))) + + if justAdded && animated { + selectedImageView.layer?.animateScaleSpring(from: 0.2, to: 1.0, duration: duration) + selectedImageView.layer?.animateAlpha(from: 0, to: 1, duration: duration, timingFunction: timingFunction) + } + } else { + if option.isQuiz, let isCorrect = option.isCorrect, isCorrect { + var justAdded = false + if self.selectedImageView == nil { + self.selectedImageView = ImageView() + addSubview(self.selectedImageView!) + justAdded = true + } + + guard let selectedImageView = self.selectedImageView else { + return + } + + selectedImageView.image = option.presentation.chat.pollSelected(option.isIncoming, option.isBubbled, icons: option.presentation.icons) + + selectedImageView.setFrameSize(NSMakeSize(12, 12)) + + selectedImageView.setFrameOrigin(NSMakePoint(progressView.frame.minX - selectedImageView.frame.width - 4, floorToScreenPixels(backingScaleFactor, progressView.frame.midY - selectedImageView.frame.height / 2))) + + if justAdded && animated { + selectedImageView.layer?.animateScaleSpring(from: 0.2, to: 1.0, duration: duration) + selectedImageView.layer?.animateAlpha(from: 0, to: 1, duration: duration, timingFunction: timingFunction) + } + + } else { + if let selectedImageView = self.selectedImageView { + self.selectedImageView = nil + if animated { + selectedImageView.layer?.animateScaleSpring(from: 1, to: 0.2, duration: duration, removeOnCompletion: false) + selectedImageView.layer?.animateAlpha(from: 1, to: 0, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { [weak selectedImageView] _ in + selectedImageView?.removeFromSuperview() + }) + } else { + selectedImageView.removeFromSuperview() + } + } + } + + + + } + + if option.isSelected, let isCorrect = option.isCorrect { + votedColor = isCorrect ? option.presentation.chat.greenUI(option.isIncoming, option.isBubbled) : option.presentation.chat.redUI(option.isIncoming, option.isBubbled) + } else { + votedColor = option.presentation.chat.webPreviewActivity(option.isIncoming, option.isBubbled) + } + progressView.style = ControlStyle(foregroundColor: votedColor, backgroundColor: .clear) + + if let progress = option.percent { + //toolTip = option.tooltip + + progressView.frame = NSMakeRect(nameView.frame.minX, nameView.frame.maxY + 5, frame.width - nameView.frame.minX - defaultInset, progressView.frame.height) + progressView.set(progress: CGFloat(progress), animated: animated, duration: duration / 2, timingFunction: .spring, bounce: true) + if percentView == nil { + percentView = ImageView() + addSubview(percentView!) + if animated { + percentView!.layer?.animateAlpha(from: 0, to: 1, duration: duration / 2) + } + } + + + percentView?.animates = animated + percentView?.image = option.currentPercentImage + percentView?.setFrameSize(36, 16) + percentView?.setFrameOrigin(NSMakePoint(nameView.frame.minX - percentView!.frame.width - PollOption.spaceBetweenTexts, nameView.frame.minY + 1)) + + if previousPercent != option.realPercent, animated { + let images = option.presentation.chat.pollPercentAnimatedIcons(option.isIncoming, option.isBubbled, from: CGFloat(previousPercent ?? 0), to: CGFloat(option.realPercent), duration: duration / 2) + if !images.isEmpty { + let animation = CAKeyframeAnimation(keyPath: "contents") + animation.values = images + animation.duration = duration / 2 + animation.calculationMode = .discrete + percentView?.layer?.add(animation, forKey: "image") + } + + } + + if let selectingView = selectingView { + self.selectingView = nil + if animated { + selectingView.layer?.animateAlpha(from: 1, to: 0, duration: duration / 2, removeOnCompletion: false, completion: { [weak selectingView] completed in + if completed { + selectingView?.removeFromSuperview() + } + }) + } else { + selectingView.removeFromSuperview() + } + } + if let progressIndicator = progressIndicator { + self.progressIndicator = nil + if animated { + progressIndicator.layer?.animateAlpha(from: 1, to: 0, duration: duration / 2, removeOnCompletion: false, completion: { [weak progressIndicator] completed in + if completed { + progressIndicator?.removeFromSuperview() + } + }) + } else { + progressIndicator.removeFromSuperview() + } + } + } else { + toolTip = nil + if let percentView = self.percentView { + self.percentView = nil + if animated { + + if previousPercent != 0 { + let images = option.presentation.chat.pollPercentAnimatedIcons(option.isIncoming, option.isBubbled, from: CGFloat(previousPercent ?? 0), to: CGFloat(0), duration: duration / 2) + if !images.isEmpty { + let animation = CAKeyframeAnimation(keyPath: "contents") + animation.values = images + animation.duration = duration / 2 + animation.calculationMode = .discrete + percentView.layer?.add(animation, forKey: "image") + } + } + + percentView.layer?.animateAlpha(from: 1, to: 0, duration: duration / 2, removeOnCompletion: false, completion: { [weak percentView] completed in + if completed { + percentView?.removeFromSuperview() + } + }) + } else { + percentView.removeFromSuperview() + } + } + + progressView.set(progress: 0, animated: animated) + + if option.isLoading { + if let selectingView = selectingView { + self.selectingView = nil + if animated { + selectingView.layer?.animateAlpha(from: 1, to: 0, duration: duration / 2, removeOnCompletion: false, completion: { [weak selectingView] completed in + if completed { + selectingView?.removeFromSuperview() + } + }) + } else { + selectingView.removeFromSuperview() + } + } + if progressIndicator == nil { + progressIndicator = ProgressIndicator(frame: NSMakeRect(0, 0, 18, 18)) + addSubview(progressIndicator!) + if animated { + progressIndicator?.layer?.animateAlpha(from: 0, to: 1, duration: duration / 2) + } + } +// progressIndicator?.lineWidth = 1.0 + progressIndicator?.progressColor = option.presentation.chat.webPreviewActivity(option.isIncoming, option.isBubbled) + progressIndicator?.setFrameOrigin(NSMakePoint(defaultInset, 0)) + + } else { + if let progressIndicator = progressIndicator { + self.progressIndicator = nil + if animated { + progressIndicator.layer?.animateAlpha(from: 1, to: 0, duration: duration / 2, removeOnCompletion: false, completion: { [weak progressIndicator] completed in + if completed { + progressIndicator?.removeFromSuperview() + } + }) + } else { + progressIndicator.removeFromSuperview() + } + } + + if selectingView == nil { + selectingView = ImageView(frame: NSMakeRect(0, 0, 22, 22)) + addSubview(selectingView!) + if animated { + selectingView?.layer?.animateAlpha(from: 0, to: 1, duration: duration / 2) + } + } + selectingView?.animates = animated || (previousOption != nil && previousOption?.isMultipleSelected != option.isMultipleSelected) + if option.isMultipleSelected { + selectingView?.image = option.presentation.chat.pollSelection(option.isIncoming, option.isBubbled, icons: option.presentation.icons) + } else { + selectingView?.image = option.presentation.chat.pollOptionUnselectedImage(option.isIncoming, option.isBubbled) + } + selectingView?.sizeToFit() + selectingView?.setFrameOrigin(NSMakePoint(defaultInset, 0)) + } + + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private final class PollView : Control { + fileprivate let titleView: TextView = TextView() + private let typeView: TextView = TextView() + private var actionButton: TitleButton? + private var totalVotesTextView: TextView? + + private var mergedAvatarsView: MergedAvatarsView? + + private var solutionButton: ImageButton? + private var timerView: PollBubbleTimerView? + + private var options:[PollOptionView] = [] + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + typeView.isSelectable = false + typeView.userInteractionEnabled = false + addSubview(titleView) + addSubview(typeView) + } + + func update(with item: ChatPollItem, animated: Bool) { + + titleView.update(item.titleText) + typeView.update(item.titleTypeText) + + var y: CGFloat = 0 + + titleView.setFrameOrigin(NSMakePoint(item.bubbleContentInset, y)) + y += titleView.frame.height + item.defaultContentInnerInset + typeView.setFrameOrigin(NSMakePoint(item.bubbleContentInset, y)) + y += typeView.frame.height + item.defaultContentInnerInset + + while options.count < item.options.count { + let option = PollOptionView(frame: NSZeroRect) + options.append(option) + addSubview(option) + } + while options.count > item.options.count { + let option = options.removeLast() + option.removeFromSuperview() + } + for (i, option) in item.options.enumerated() { + + + self.options[i].frame = NSMakeRect(0, y - (i > 0 ? PollOption.spaceBetweenOptions : 0), frame.width, option.contentSize.height) + self.options[i].update(with: option, animated: animated) + y += option.contentSize.height + if i != item.options.count - 1 { + y += PollOption.spaceBetweenOptions + } + } + + if let totalVotesText = item.totalVotesText { + y += item.defaultContentInnerInset + if totalVotesTextView == nil { + totalVotesTextView = TextView() + totalVotesTextView!.userInteractionEnabled = false + totalVotesTextView!.isSelectable = false + addSubview(totalVotesTextView!) + } + guard let totalVotesTextView = self.totalVotesTextView else { + return + } + totalVotesTextView.update(totalVotesText, origin: NSMakePoint(floorToScreenPixels(backingScaleFactor, (frame.width - totalVotesText.layoutSize.width) / 2), y)) + } else { + totalVotesTextView?.removeFromSuperview() + totalVotesTextView = nil + } + + if let actionText = item.actionButtonText { + y += item.defaultContentInnerInset - 4 + if self.actionButton == nil { + self.actionButton = TitleButton() + self.addSubview(self.actionButton!) + } + guard let actionButton = self.actionButton else { + return + } + + actionButton.isEnabled = item.actionButtonIsEnabled + + actionButton.removeAllHandlers() + actionButton.set(handler: { [weak item] _ in + item?.invokeAction() + }, for: .SingleClick) + + actionButton.set(font: .normal(.text), for: .Normal) + actionButton.set(color: item.presentation.chat.webPreviewActivity(item.isIncoming, item.isBubbled), for: .Normal) + actionButton.set(text: actionText, for: .Normal) + _ = actionButton.sizeToFit(NSMakeSize(10, 4), thatFit: false) + actionButton.centerX(y: y) + } else { + self.actionButton?.removeFromSuperview() + self.actionButton = nil + } + + guard let message = item.message else { + return + } + + var avatarPeers: [Peer] = [] + if !item.isBotQuiz { + for peerId in item.poll.results.recentVoters { + if let peer = message.peers[peerId] { + avatarPeers.append(peer) + } + } + } + + if !avatarPeers.isEmpty { + if self.mergedAvatarsView == nil { + self.mergedAvatarsView = MergedAvatarsView() + self.mergedAvatarsView!.setFrameSize(NSMakeSize(self.mergedAvatarsView!.mergedImageSpacing * CGFloat(avatarPeers.count) + 2, self.mergedAvatarsView!.mergedImageSize)) + addSubview(self.mergedAvatarsView!) + } + self.mergedAvatarsView?.frame = CGRect(origin: NSMakePoint(typeView.frame.maxX + 6, typeView.frame.minY), size: NSMakeSize(self.mergedAvatarsView!.mergedImageSpacing * CGFloat(avatarPeers.count) + 2, self.mergedAvatarsView!.mergedImageSize)) + self.mergedAvatarsView?.update(context: item.context, peers: avatarPeers, message: message, synchronousLoad: false) + self.mergedAvatarsView?.removeAllHandlers() + + self.mergedAvatarsView?.set(handler: { [weak item] _ in + if item?.actionButtonText == L10n.chatPollViewResults, item?.actionButtonIsEnabled == true { + item?.invokeAction() + } + }, for: .Click) + } else { + self.mergedAvatarsView?.removeFromSuperview() + self.mergedAvatarsView = nil + } + + if let solution = item.poll.results.solution, item.options.contains(where: { $0.isSelected }) || item.isClosed { + var mayApplyAnimation = false + if solutionButton == nil { + solutionButton = ImageButton() + addSubview(solutionButton!) + mayApplyAnimation = animated + } + if let solutionButton = self.solutionButton { + solutionButton.set(image: item.presentation.chat.quizSolution(item), for: .Normal) + solutionButton.style = ControlStyle(font: nil, foregroundColor: theme.colors.accent, highlightColor: theme.colors.accent.withAlphaComponent(0.7)) + _ = solutionButton.sizeToFit() + solutionButton.setFrameOrigin(NSMakePoint(frame.width - solutionButton.frame.width - 6, typeView.frame.minY - 6)) + + if mayApplyAnimation { + solutionButton.layer?.animateScaleSpring(from: 0.2, to: 1.0, duration: 0.3) + } + } + solutionButton?.removeAllHandlers() + + + solutionButton?.set(handler: { [weak item] control in + + guard let item = item else { + return + } + let text = ChatMessageItem.applyMessageEntities(with: [TextEntitiesMessageAttribute(entities: solution.entities)], for: solution.text, message: item.message, context: item.context, fontSize: .text, openInfo: item.chatInteraction.openInfo, textColor: .white, linkColor: nightAccentPalette.link, monospacedPre: .redUI, monospacedCode: .greenUI, mediaDuration: nil) + + tooltip(for: control, text: solution.text, attributedText: text, interactions: globalLinkExecutor, timeout: 10.0) + }, for: .Click) + + } else { + solutionButton?.removeFromSuperview() + solutionButton = nil + } + + let deadlineTimeout = item.poll.deadlineTimeout + var displayDeadline = true + if let voters = item.poll.results.voters { + for voter in voters { + if voter.selected { + displayDeadline = false + break + } + } + } + + + if let deadlineTimeout = deadlineTimeout, displayDeadline, !item.isClosed { + let timerView: PollBubbleTimerView + if let current = self.timerView { + timerView = current + } else { + timerView = PollBubbleTimerView(frame: NSMakeRect(frame.width - 70 - 8, typeView.frame.minY, 70, 22)) + self.addSubview(timerView) + self.timerView = timerView + + if animated { + timerView.layer?.animateScaleSpring(from: 0.2, to: 1.0, duration: 0.3) + timerView.layer?.animateAlpha(from: 0, to: 1, duration: 0.3) + } + } + + timerView.reachedTimeout = { [weak item] in + item?.copyAndUpdate(animated: true) + } + var endDate: Int32? + if message.id.namespace == Namespaces.Message.Cloud { + let startDate: Int32 + if let forwardInfo = message.forwardInfo { + startDate = forwardInfo.date + } else { + startDate = message.timestamp + } + endDate = startDate + deadlineTimeout + } + timerView.update(regularColor: item.presentation.chat.textColor(item.isIncoming, item.isBubbled), proximityColor: item.presentation.chat.redUI(item.isIncoming, item.isBubbled), timeout: deadlineTimeout, deadlineTimestamp: endDate) + + } else if let timerView = self.timerView { + self.timerView = nil + if animated { + timerView.layer?.animateScaleSpring(from: 1, to: 0, duration: 0.3, removeOnCompletion: false, completion: { [weak timerView] _ in + timerView?.removeFromSuperview() + }) + timerView.layer?.animateAlpha(from: 1, to: 0.2, duration: 0.3) + } else { + timerView.removeFromSuperview() + } + } + + + + + + } + + func showSolution() { + self.solutionButton?.send(event: .Click) + } + + override func layout() { + super.layout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/ChatPresentationInputQueryResult.swift b/Telegram-Mac/ChatPresentationInputQueryResult.swift new file mode 100644 index 0000000000..629030ea3c --- /dev/null +++ b/Telegram-Mac/ChatPresentationInputQueryResult.swift @@ -0,0 +1,105 @@ +// +// ChatPresentationInputQueryResult.swift +// Telegram +// +// Created by Mikhail Filimonov on 02.11.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import Postbox + +enum ChatPresentationInputQueryResult: Equatable { + case hashtags([String]) + case mentions([Peer]) + case commands([PeerCommand]) + case stickers([FoundStickerItem]) + case emoji([String], Bool) + case searchMessages(([Message], SearchMessagesState?, (SearchMessagesState?)-> Void), [Peer], String) + case contextRequestResult(Peer, ChatContextResultCollection?) + + static func ==(lhs: ChatPresentationInputQueryResult, rhs: ChatPresentationInputQueryResult) -> Bool { + switch lhs { + case let .hashtags(lhsResults): + if case let .hashtags(rhsResults) = rhs { + return lhsResults == rhsResults + } else { + return false + } + case let .stickers(lhsResults): + if case let .stickers(rhsResults) = rhs { + return lhsResults == rhsResults + } else { + return false + } + case let .emoji(lhsResults, lhsFirstWord): + if case let .emoji(rhsResults, rhsFirstWord) = rhs { + return lhsResults == rhsResults && lhsFirstWord == rhsFirstWord + } else { + return false + } + case let .searchMessages(lhsMessages, lhsPeers, lhsSearchText): + if case let .searchMessages(rhsMessages, rhsPeers, rhsSearchText) = rhs { + if lhsPeers.count == rhsPeers.count { + for i in 0 ..< rhsPeers.count { + if !lhsPeers[i].isEqual(rhsPeers[i]) { + return false + } + } + } else { + return false + } + if lhsMessages.0.count == rhsMessages.0.count { + for i in 0 ..< lhsMessages.0.count { + if !isEqualMessages(lhsMessages.0[i], rhsMessages.0[i]) { + return false + } + } + return lhsSearchText == rhsSearchText && lhsMessages.1 == rhsMessages.1 + } else { + return false + } + } else { + return false + } + case let .mentions(lhsPeers): + if case let .mentions(rhsPeers) = rhs { + if lhsPeers.count != rhsPeers.count { + return false + } else { + for i in 0 ..< lhsPeers.count { + if !lhsPeers[i].isEqual(rhsPeers[i]) { + return false + } + } + return true + } + } else { + return false + } + case let .commands(lhsCommands): + if case let .commands(rhsCommands) = rhs { + if lhsCommands != rhsCommands { + return false + } + return true + } else { + return false + } + case let .contextRequestResult(lhsPeer, lhsCollection): + if case let .contextRequestResult(rhsPeer, rhsCollection) = rhs { + if !lhsPeer.isEqual(rhsPeer) { + return false + } + if lhsCollection != rhsCollection { + return false + } + return true + } else { + return false + } + } + } +} diff --git a/Telegram-Mac/ChatPresentationInterfaceState.swift b/Telegram-Mac/ChatPresentationInterfaceState.swift index e8bbb3e972..665723d500 100644 --- a/Telegram-Mac/ChatPresentationInterfaceState.swift +++ b/Telegram-Mac/ChatPresentationInterfaceState.swift @@ -7,11 +7,12 @@ // import Cocoa +import OpusBinding +import Postbox +import TelegramCore -import PostboxMac -import TelegramCoreMac import TGUIKit -import SwiftSignalKitMac +import SwiftSignalKit enum ChatPresentationInputContext { case none @@ -21,6 +22,100 @@ enum ChatPresentationInputContext { case emoji } +enum RestrictedMediaType { + case stickers + case media + +} + +final class ChatPinnedMessage: Equatable { + let messageId: MessageId + let message: Message? + let isLatest: Bool + let index: Int + let totalCount: Int + let others:[MessageId] + init(messageId: MessageId, message: Message?, others: [MessageId] = [], isLatest: Bool, index:Int = 0, totalCount: Int = 1) { + self.messageId = messageId + self.message = message + self.others = others + self.isLatest = isLatest + self.index = index + self.totalCount = totalCount + } + + static func ==(lhs: ChatPinnedMessage, rhs: ChatPinnedMessage) -> Bool { + if lhs === rhs { + return true + } + if lhs.messageId != rhs.messageId { + return false + } + if lhs.message?.id != rhs.message?.id { + return false + } + if lhs.message?.stableVersion != rhs.message?.stableVersion { + return false + } + if lhs.isLatest != rhs.isLatest { + return false + } + if lhs.index != rhs.index { + return false + } + if lhs.totalCount != rhs.totalCount { + return false + } + return true + } +} + + +struct GroupCallPanelData : Equatable { + let peerId: PeerId + let info: GroupCallInfo? + let topParticipants: [GroupCallParticipantsContext.Participant] + let participantCount: Int + let activeSpeakers: Set + private(set) weak var groupCall: GroupCallContext? + init( + peerId: PeerId, + info: GroupCallInfo?, + topParticipants: [GroupCallParticipantsContext.Participant], + participantCount: Int, + activeSpeakers: Set, + groupCall: GroupCallContext? + ) { + self.peerId = peerId + self.info = info + self.topParticipants = topParticipants + self.participantCount = participantCount + self.activeSpeakers = activeSpeakers + self.groupCall = groupCall + } + + static func ==(lhs: GroupCallPanelData, rhs: GroupCallPanelData) -> Bool { + if lhs.peerId != rhs.peerId { + return false + } + if lhs.info != rhs.info { + return false + } + if lhs.topParticipants != rhs.topParticipants { + return false + } + if lhs.activeSpeakers != rhs.activeSpeakers { + return false + } + if (lhs.groupCall != nil) != (rhs.groupCall != nil) { + return false + } + if lhs.participantCount != rhs.participantCount { + return false + } + return true + } +} @@ -30,180 +125,47 @@ enum ChatPresentationInputQuery: Equatable { case mention(query: String, includeRecent: Bool) case command(String) case contextRequest(addressName: String, query: String) - case emoji(String) + case emoji(String, firstWord: Bool) case stickers(String) - static func ==(lhs: ChatPresentationInputQuery, rhs: ChatPresentationInputQuery) -> Bool { - switch lhs { - case let .hashtag(query): - if case .hashtag(query) = rhs { - return true - } else { - return false - } - case let .stickers(query): - if case .stickers(query) = rhs { - return true - } else { - return false - } - case let .emoji(query): - if case .emoji(query) = rhs { - return true - } else { - return false - } - case let .mention(query, includeInline): - if case .mention(query, includeInline) = rhs { - return true - } else { - return false - } - case let .command(query): - if case .command(query) = rhs { - return true - } else { - return false - } - case let .contextRequest(addressName, query): - if case .contextRequest(addressName, query) = rhs { - return true - } else { - return false - } - case .none: - if case .none = rhs { - return true - } else { - return false - } - } - } -} - -enum ChatPresentationInputQueryResult: Equatable { - case hashtags([String]) - case mentions([Peer]) - case commands([PeerCommand]) - case stickers([FoundStickerItem]) - case emoji([EmojiClue]) - case contextRequestResult(Peer, ChatContextResultCollection?) - - static func ==(lhs: ChatPresentationInputQueryResult, rhs: ChatPresentationInputQueryResult) -> Bool { - switch lhs { - case let .hashtags(lhsResults): - if case let .hashtags(rhsResults) = rhs { - return lhsResults == rhsResults - } else { - return false - } - case let .stickers(lhsResults): - if case let .stickers(rhsResults) = rhs { - return lhsResults == rhsResults - } else { - return false - } - case let .emoji(lhsResults): - if case let .emoji(rhsResults) = rhs { - return lhsResults == rhsResults - } else { - return false - } - case let .mentions(lhsPeers): - if case let .mentions(rhsPeers) = rhs { - if lhsPeers.count != rhsPeers.count { - return false - } else { - for i in 0 ..< lhsPeers.count { - if !lhsPeers[i].isEqual(rhsPeers[i]) { - return false - } - } - return true - } - } else { - return false - } - case let .commands(lhsCommands): - if case let .commands(rhsCommands) = rhs { - if lhsCommands != rhsCommands { - return false - } - return true - } else { - return false - } - case let .contextRequestResult(lhsPeer, lhsCollection): - if case let .contextRequestResult(rhsPeer, rhsCollection) = rhs { - if !lhsPeer.isEqual(rhsPeer) { - return false - } - if lhsCollection != rhsCollection { - return false - } - return true - } else { - return false - } - } - } } -final class ChatEditState : Equatable { - let inputState:ChatTextInputState - let message:Message - init(message:Message, state:ChatTextInputState? = nil) { - self.message = message - if let state = state { - self.inputState = state - } else { - var attribute:TextEntitiesMessageAttribute? - for attr in message.attributes { - if let attr = attr as? TextEntitiesMessageAttribute { - attribute = attr - } - } - var attributes:[ChatTextInputAttribute] = [] - if let attribute = attribute { - attributes = chatTextAttributes(from: attribute) - } - self.inputState = ChatTextInputState(inputText:message.text, selectionRange:message.text.length ..< message.text.length, attributes: attributes ) - } +struct ChatActiveGroupCallInfo: Equatable { + var activeCall: CachedChannelData.ActiveCall + let data: GroupCallPanelData? + let callJoinPeerId: PeerId? + let joinHash: String? + func withUpdatedData(_ data: GroupCallPanelData?) -> ChatActiveGroupCallInfo { + return ChatActiveGroupCallInfo(activeCall: self.activeCall, data: data, callJoinPeerId: self.callJoinPeerId, joinHash: self.joinHash) } - - func withUpdated(state:ChatTextInputState) -> ChatEditState { - return ChatEditState(message:message, state:state) + func withUpdatedActiveCall(_ activeCall: CachedChannelData.ActiveCall) -> ChatActiveGroupCallInfo { + return ChatActiveGroupCallInfo(activeCall: activeCall, data: self.data, callJoinPeerId: self.callJoinPeerId, joinHash: self.joinHash) } - - static func ==(lhs:ChatEditState, rhs:ChatEditState) -> Bool { - return lhs.message.id == rhs.message.id && lhs.inputState == rhs.inputState + func withUpdatedCallJoinPeerId(_ callJoinPeerId: PeerId?) -> ChatActiveGroupCallInfo { + return ChatActiveGroupCallInfo(activeCall: activeCall, data: self.data, callJoinPeerId: callJoinPeerId, joinHash: self.joinHash) + } + func withUpdatedJoinHash(_ joinHash: String?) -> ChatActiveGroupCallInfo { + return ChatActiveGroupCallInfo(activeCall: self.activeCall, data: self.data, callJoinPeerId: self.callJoinPeerId, joinHash: joinHash) } } + enum ChatRecordingStatus : Equatable { case paused case recording(duration: Double) } -func ==(lhs: ChatRecordingStatus, rhs: ChatRecordingStatus) -> Bool { - switch lhs { - case .paused: - if case .paused = rhs { - return true - } else { - return false - } - case .recording(let duration): - if case .recording(duration) = rhs { - return true - } else { - return false - } - } -} class ChatRecordingState : Equatable { + + let autohold: Bool + let holdpromise: ValuePromise = ValuePromise() + init(autohold: Bool) { + self.autohold = autohold + holdpromise.set(autohold) + } + var micLevel: Signal { return .complete() } @@ -236,9 +198,12 @@ func ==(lhs:ChatRecordingState, rhs:ChatRecordingState) -> Bool { final class ChatRecordingVideoState : ChatRecordingState { let pipeline: VideoRecorderPipeline - private let path: String = NSTemporaryDirectory() + "video_message\(arc4random()).mp4" - override init() { - pipeline = VideoRecorderPipeline(url: URL(fileURLWithPath: path)) + private let path: String + init(context: AccountContext, liveUpload:Bool, autohold: Bool) { + let id:Int64 = arc4random64() + self.path = NSTemporaryDirectory() + "video_message\(id).mp4" + self.pipeline = VideoRecorderPipeline(url: URL(fileURLWithPath: path), config: VideoMessageConfig.with(appConfiguration: context.appConfiguration), liveUploading: liveUpload ? PreUploadManager(path, context: context, id: id) : nil) + super.init(autohold: autohold) } override var micLevel: Signal { @@ -259,8 +224,8 @@ final class ChatRecordingVideoState : ChatRecordingState { } } |> take(1) |> map { state in switch state { - case let .finishRecording(path, duration, _): - return [VideoMessageSenderContainer(path: path, duration: duration, size: CGSize(width: 200, height: 200))] + case let .finishRecording(path, duration, id, _): + return [VideoMessageSenderContainer(path: path, duration: duration, size: CGSize(width: 200, height: 200), id: id)] default: return [] } @@ -300,7 +265,7 @@ final class ChatRecordingAudioState : ChatRecordingState { override var data: Signal<[MediaSenderContainer], NoError> { return recorder.takenRecordedData() |> map { value in if let value = value, value.duration > 0.5 { - return [VoiceSenderContainer(data: value)] + return [VoiceSenderContainer(data: value, id: value.id)] } return [] } @@ -312,10 +277,17 @@ final class ChatRecordingAudioState : ChatRecordingState { - override init() { - recorder = ManagedAudioRecorder() + init(context: AccountContext, liveUpload: Bool, autohold: Bool) { + let id = arc4random64() + let path = NSTemporaryDirectory() + "voice_message\(id).ogg" + let uploadManager:PreUploadManager? = liveUpload ? PreUploadManager(path, context: context, id: id) : nil + let dataItem = DataItem(path: path) + recorder = ManagedAudioRecorder(liveUploading: uploadManager, dataItem: dataItem) + super.init(autohold: autohold) } + + override func start() { recorder.start() } @@ -328,7 +300,7 @@ final class ChatRecordingAudioState : ChatRecordingState { recorder.stop() _ = data.start(next: { data in for container in data { - try? FileManager.default.removeItem(atPath: container.path) + // try? FileManager.default.removeItem(atPath: container.path) } }) } @@ -342,10 +314,17 @@ final class ChatRecordingAudioState : ChatRecordingState { enum ChatState : Equatable { + + struct AdditionAction { + let icon: CGImage + let action: (NSView)->Void + } + case normal case selecting case block(String) - case action(String, (ChatInteraction)->Void) + case action(String, (ChatInteraction)->Void, AdditionAction?) + case channelWithDiscussion(discussionGroupId: PeerId?, leftAction: String, rightAction: String) case editing case recording(ChatRecordingState) case restricted(String) @@ -359,6 +338,12 @@ func ==(lhs:ChatState, rhs:ChatState) -> Bool { } else { return false } + case let .channelWithDiscussion(discussionGroupId, leftAction, rightAction): + if case .channelWithDiscussion(discussionGroupId, leftAction, rightAction) = rhs { + return true + } else { + return false + } case .selecting: if case .selecting = rhs { return true @@ -383,8 +368,8 @@ func ==(lhs:ChatState, rhs:ChatState) -> Bool { } else { return false } - case let .action(lhsAction,_): - if case let .action(rhsAction,_) = rhs { + case let .action(lhsAction,_, _): + if case let .action(rhsAction, _, _) = rhs { return lhsAction == rhsAction } else { return false @@ -398,30 +383,103 @@ func ==(lhs:ChatState, rhs:ChatState) -> Bool { } } +struct ChatPeerStatus : Equatable { + let canAddContact: Bool + let peerStatusSettings: PeerStatusSettings? + init(canAddContact: Bool, peerStatusSettings: PeerStatusSettings?) { + self.canAddContact = canAddContact + self.peerStatusSettings = peerStatusSettings + } +} + +struct SlowMode : Equatable { + let validUntil: Int32? + let timeout: Int32? + let sendingIds: [MessageId] + init(validUntil: Int32? = nil, timeout: Int32? = nil, sendingIds: [MessageId] = []) { + self.validUntil = validUntil + self.timeout = timeout + self.sendingIds = sendingIds + } + func withUpdatedValidUntil(_ validUntil: Int32?) -> SlowMode { + return SlowMode(validUntil: validUntil, timeout: self.timeout, sendingIds: self.sendingIds) + } + func withUpdatedTimeout(_ timeout: Int32?) -> SlowMode { + if timeout == nil { + var bp:Int = 0 + bp += 1 + } + return SlowMode(validUntil: self.validUntil, timeout: timeout, sendingIds: self.sendingIds) + } + func withUpdatedSendingIds(_ sendingIds: [MessageId]) -> SlowMode { + return SlowMode(validUntil: self.validUntil, timeout: self.timeout, sendingIds: sendingIds) + } + + var hasLocked: Bool { + return timeout != nil || !sendingIds.isEmpty + } + + var sendingLocked: Bool { + return timeout != nil + } + + var errorText: String? { + if let timeout = timeout { + return slowModeTooltipText(timeout) + } else if !sendingIds.isEmpty { + return L10n.slowModeMultipleError + } else { + return nil + } + } +} + struct ChatPresentationInterfaceState: Equatable { + + struct BotMenu : Equatable { + var commands: [BotCommand] + var revealed: Bool + } + let interfaceState: ChatInterfaceState let peer: Peer? - let isSearchMode:Bool + let mainPeer: Peer? + let chatLocation: ChatLocation + let chatMode: ChatMode + let isSearchMode:(Bool, Peer?, String?) let notificationSettings: TelegramPeerNotificationSettings? let inputQueryResult: ChatPresentationInputQueryResult? let keyboardButtonsMessage: Message? let initialAction:ChatInitialAction? let historyCount:Int? let isBlocked:Bool? - let editState:ChatEditState? let recordingState:ChatRecordingState? - let reportStatus:PeerReportStatus - let pinnedMessageId:MessageId? + let peerStatus:ChatPeerStatus? + let pinnedMessageId:ChatPinnedMessage? let urlPreview: (String, TelegramMediaWebpage)? let selectionState: ChatInterfaceSelectionState? - + let limitConfiguration: LimitsConfiguration + let sidebarEnabled:Bool? let sidebarShown:Bool? let layout:SplitViewState? - + let discussionGroupId: CachedChannelData.LinkedDiscussionPeerId let canAddContact:Bool? let isEmojiSection: Bool - + let canInvokeBasicActions:(delete: Bool, forward: Bool) + let isNotAccessible: Bool + let hasScheduled: Bool + let slowMode: SlowMode? + let failedMessageIds:Set + let hidePinnedMessage: Bool + let canPinMessage: Bool + let tempPinnedMaxId: MessageId? + let restrictionInfo: PeerAccessRestrictionInfo? + let groupCall: ChatActiveGroupCallInfo? + let messageSecretTimeout: CachedPeerAutoremoveTimeout? + let reportMode: ReportReasonValue? + + let botMenu:BotMenu? var inputContext: ChatPresentationInputQuery { return inputContextQueryForChatPresentationIntefaceState(self, includeContext: true) @@ -435,82 +493,188 @@ struct ChatPresentationInterfaceState: Equatable { return reply.rows.count > 0 } + var canPinMessageInPeer: Bool { + if let peer = peer as? TelegramChannel, peer.hasPermission(.pinMessages) || (peer.isChannel && peer.hasPermission(.editAllMessages)) { + return true + } else if let peer = peer as? TelegramGroup, peer.canPinMessage { + return true + } else if let _ = peer as? TelegramSecretChat { + return false + } else { + return canPinMessage + } + } + var state:ChatState { if self.selectionState == nil { - if self.editState != nil { + if let initialAction = initialAction, case .start = initialAction { + return .action(L10n.chatInputStartBot, { chatInteraction in + chatInteraction.invokeInitialAction() + }, nil) + } + + if let recordingState = recordingState { + return .recording(recordingState) + } + + if self.interfaceState.editState != nil { return .editing } + if self.chatMode == .preview { + return .block("") + } + if self.interfaceState.themeEditing { + return .block("") + } + + + switch chatMode { + case .pinned: + if canPinMessageInPeer { + return .action(L10n.chatPinnedUnpinAllCountable(pinnedMessageId?.totalCount ?? 0), { chatInteraction in + let navigation = chatInteraction.context.sharedContext.bindings.rootNavigation() + (navigation.previousController as? ChatController)?.chatInteraction.unpinAllMessages() + }, nil) + } else { + return .action(L10n.chatPinnedDontShow, { chatInteraction in + let navigation = chatInteraction.context.sharedContext.bindings.rootNavigation() + (navigation.previousController as? ChatController)?.chatInteraction.unpinAllMessages() + }, nil) + } + + default: + break + } + if let peer = peer as? TelegramChannel { + #if APP_STORE + if let restrictionInfo = restrictionInfo { + for rule in restrictionInfo.rules { + if rule.platform == "ios" || rule.platform == "all" { + return .action(L10n.chatInputClose, { chatInteraction in + chatInteraction.back() + }, nil) + } + } + } + #endif + + if peer.flags.contains(.isGigagroup) { + if peer.participationStatus == .left { + return .action(L10n.chatInputJoin, { chatInteraction in + chatInteraction.joinChannel() + }, nil) + } else if peer.adminRights == nil && !peer.groupAccess.isCreator { + if let notificationSettings = notificationSettings { + return .action(notificationSettings.isMuted ? L10n.chatInputUnmute : L10n.chatInputMute, { chatInteraction in + chatInteraction.toggleNotifications(nil) + }, .init(icon: theme.icons.chat_gigagroup_info, action: { control in + tooltip(for: control, text: L10n.chatGigagroupHelp) + })) + } else { + return .action(L10n.chatInputMute, { chatInteraction in + chatInteraction.toggleNotifications(nil) + }, .init(icon: theme.icons.chat_gigagroup_info, action: { control in + + })) + } + } + + } + + switch chatMode { + case .replyThread: + if let permissionText = permissionText(from: peer, for: .banSendMessages) { + return .restricted(permissionText) + } else if peer.participationStatus == .left { + return .normal + } else if peer.participationStatus == .kicked { + return .restricted(L10n.chatCommentsKicked) + } else if peer.participationStatus == .member { + return .normal + } + default: + break + } + + + if peer.participationStatus == .left { - return .action(tr(.chatInputJoin), { chatInteraction in + return .action(L10n.chatInputJoin, { chatInteraction in chatInteraction.joinChannel() - }) + }, nil) } else if peer.participationStatus == .kicked { - return .action(tr(.chatInputDelete), { chatInteraction in + return .action(L10n.chatInputDelete, { chatInteraction in chatInteraction.removeAndCloseChat() - }) - } else if peer.hasBannedRights(.banSendMessages), let bannedRights = peer.bannedRights { - - return .restricted(bannedRights.untilDate != Int32.max ? tr(.channelPersmissionDeniedSendMessagesUntil(bannedRights.formattedUntilDate)) : tr(.channelPersmissionDeniedSendMessagesForever)) - } else if !peer.canSendMessage, let notificationSettings = notificationSettings { - return .action(notificationSettings.isMuted ? tr(.chatInputUnmute) : tr(.chatInputMute), { chatInteraction in - chatInteraction.toggleNotifications() - }) + }, nil) + } else if let permissionText = permissionText(from: peer, for: .banSendMessages) { + return .restricted(permissionText) + } else if !peer.canSendMessage(chatMode.isThreadMode), let notificationSettings = notificationSettings { + return .action(notificationSettings.isMuted ? L10n.chatInputUnmute : L10n.chatInputMute, { chatInteraction in + chatInteraction.toggleNotifications(nil) + }, nil) } } else if let peer = peer as? TelegramGroup { if peer.membership == .Left { - return .action(tr(.chatInputReturn),{ chatInteraction in + return .action(L10n.chatInputReturn,{ chatInteraction in chatInteraction.returnGroup() - }) + }, nil) } else if peer.membership == .Removed { - return .action(tr(.chatInputDelete), { chatInteraction in + return .action(L10n.chatInputDelete, { chatInteraction in chatInteraction.removeAndCloseChat() - }) + }, nil) } - } else if let peer = peer as? TelegramSecretChat { + } else if let peer = peer as? TelegramSecretChat, let mainPeer = mainPeer { switch peer.embeddedState { case .terminated: - return .action(tr(.chatInputDelete), { chatInteraction in + return .action(L10n.chatInputDelete, { chatInteraction in chatInteraction.removeAndCloseChat() - }) + }, nil) case .handshake: - return .action(tr(.chatInputSecretChatWaitingToOnline), { chatInteraction in - - }) + return .restricted(L10n.chatInputSecretChatWaitingToUserOnline(mainPeer.compactDisplayTitle)) default: break } } + if let peer = peer, !peer.canSendMessage(chatMode.isThreadMode), let notificationSettings = notificationSettings { + return .action(notificationSettings.isMuted ? L10n.chatInputUnmute : L10n.chatInputMute, { chatInteraction in + chatInteraction.toggleNotifications(nil) + }, nil) + } + if let blocked = isBlocked, blocked { - return .action(tr(.chatInputUnblock), { chatInteraction in + + if let peer = peer, peer.isBot { + return .action(L10n.chatInputRestart, { chatInteraction in + chatInteraction.unblock() + chatInteraction.startBot() + }, nil) + } + + return .action(tr(L10n.chatInputUnblock), { chatInteraction in chatInteraction.unblock() - }) + }, nil) } - if self.editState != nil { - return .editing + if let peer = peer, let permissionText = permissionText(from: peer, for: .banSendMessages) { + return .restricted(permissionText) } - if let recordingState = recordingState { - return .recording(recordingState) - } - if let initialAction = initialAction, case .start(_) = initialAction { - return .action(tr(.chatInputStartBot), { chatInteraction in - chatInteraction.invokeInitialAction() - }) - } + + if let peer = peer as? TelegramUser { if peer.botInfo != nil, let historyCount = historyCount, historyCount == 0 { - return .action(tr(.chatInputStartBot), { chatInteraction in + return .action(tr(L10n.chatInputStartBot), { chatInteraction in chatInteraction.startBot() - }) + }, nil) } } + return .normal } else { @@ -518,6 +682,7 @@ struct ChatPresentationInterfaceState: Equatable { } } + var isKeyboardShown:Bool { if let keyboard = keyboardButtonsMessage, let attribute = keyboard.replyMarkup { return interfaceState.messageActionsState.closedButtonKeyboardMessageId != keyboard.id && attribute.hasButtons && state == .normal @@ -528,17 +693,59 @@ struct ChatPresentationInterfaceState: Equatable { var isShowSidebar: Bool { if let sidebarEnabled = sidebarEnabled, let peer = peer, let sidebarShown = sidebarShown, let layout = layout { - return sidebarEnabled && peer.canSendMessage && sidebarShown && layout == .dual + return sidebarEnabled && peer.canSendMessage(chatMode.isThreadMode) && sidebarShown && layout == .dual } return false } - + var slowModeMultipleLocked: Bool { + if let _ = self.slowMode { + + var keys:[Int64:Int64] = [:] + var forwardMessages:[Message] = [] + for message in self.interfaceState.forwardMessages { + if let groupingKey = message.groupingKey { + if keys[groupingKey] == nil { + keys[groupingKey] = groupingKey + forwardMessages.append(message) + } + } else { + forwardMessages.append(message) + } + } + if forwardMessages.count > 1 || (!effectiveInput.inputText.isEmpty && forwardMessages.count == 1) { + return true + } else if effectiveInput.inputText.length > 4096 { + return true + } + } + return false + } + + var slowModeErrorText: String? { + if let slowMode = self.slowMode, slowMode.hasLocked { + return slowMode.errorText + } else if slowModeMultipleLocked { + if effectiveInput.inputText.length > 4096 { + return L10n.slowModeTooLongError + } + return L10n.slowModeForwardCommentError + } else { + return nil + } + } var abilityToSend:Bool { if state == .normal { - return !effectiveInput.inputText.isEmpty || !interfaceState.forwardMessageIds.isEmpty - } else if let editState = editState { + if let slowMode = self.slowMode { + if slowMode.hasLocked { + return false + } else if self.slowModeMultipleLocked { + return false + } + } + return (!effectiveInput.inputText.isEmpty || !interfaceState.forwardMessageIds.isEmpty) + } else if let editState = interfaceState.editState { if editState.message.media.count == 0 { return !effectiveInput.inputText.isEmpty } else { @@ -554,37 +761,37 @@ struct ChatPresentationInterfaceState: Equatable { return false } - let maxInput:Int32 = 10000 - let maxShortInput:Int32 = 200 - + static let maxInput:Int32 = 50000 + static let maxShortInput:Int32 = 1024 + static let textLimit: Int32 = 4096 var maxInputCharacters:Int32 { if state == .normal { - return maxInput - } else if let editState = editState { + return ChatPresentationInterfaceState.maxInput + } else if let editState = interfaceState.editState { if editState.message.media.count == 0 { - return maxInput + return ChatPresentationInterfaceState.textLimit } else { for media in editState.message.media { if !(media is TelegramMediaWebpage) { - return maxShortInput + return ChatPresentationInterfaceState.maxShortInput } } - return maxInput + return ChatPresentationInterfaceState.textLimit } } - return 0 + return ChatPresentationInterfaceState.maxInput } var effectiveInput:ChatTextInputState { - if let editState = editState { + if let editState = interfaceState.editState { return editState.inputState } else { return interfaceState.inputState } } - init() { + init(chatLocation: ChatLocation, chatMode: ChatMode) { self.interfaceState = ChatInterfaceState() self.peer = nil self.notificationSettings = nil @@ -592,11 +799,10 @@ struct ChatPresentationInterfaceState: Equatable { self.keyboardButtonsMessage = nil self.initialAction = nil self.historyCount = 0 - self.isSearchMode = false + self.isSearchMode = (false, nil, nil) self.recordingState = nil - self.editState = nil self.isBlocked = nil - self.reportStatus = .unknown + self.peerStatus = nil self.pinnedMessageId = nil self.urlPreview = nil self.selectionState = nil @@ -605,9 +811,27 @@ struct ChatPresentationInterfaceState: Equatable { self.layout = nil self.canAddContact = nil self.isEmojiSection = FastSettings.entertainmentState == .emoji - } - - init(interfaceState: ChatInterfaceState, peer: Peer?, notificationSettings:TelegramPeerNotificationSettings?, inputQueryResult: ChatPresentationInputQueryResult?, keyboardButtonsMessage:Message?, initialAction:ChatInitialAction?, historyCount:Int?, isSearchMode:Bool, editState: ChatEditState?, recordingState: ChatRecordingState?, isBlocked:Bool?, reportStatus: PeerReportStatus, pinnedMessageId:MessageId?, urlPreview: (String, TelegramMediaWebpage)?, selectionState: ChatInterfaceSelectionState?, sidebarEnabled: Bool?, sidebarShown: Bool?, layout:SplitViewState?, canAddContact:Bool?, isEmojiSection: Bool) { + self.chatLocation = chatLocation + self.chatMode = chatMode + self.canInvokeBasicActions = (delete: false, forward: false) + self.isNotAccessible = false + self.restrictionInfo = nil + self.mainPeer = nil + self.limitConfiguration = LimitsConfiguration.defaultValue + self.discussionGroupId = .unknown + self.slowMode = nil + self.hasScheduled = false + self.failedMessageIds = Set() + self.hidePinnedMessage = false + self.canPinMessage = false + self.tempPinnedMaxId = nil + self.groupCall = nil + self.messageSecretTimeout = nil + self.reportMode = nil + self.botMenu = nil + } + + init(interfaceState: ChatInterfaceState, peer: Peer?, notificationSettings:TelegramPeerNotificationSettings?, inputQueryResult: ChatPresentationInputQueryResult?, keyboardButtonsMessage:Message?, initialAction:ChatInitialAction?, historyCount:Int?, isSearchMode:(Bool, Peer?, String?), recordingState: ChatRecordingState?, isBlocked:Bool?, peerStatus: ChatPeerStatus?, pinnedMessageId:ChatPinnedMessage?, urlPreview: (String, TelegramMediaWebpage)?, selectionState: ChatInterfaceSelectionState?, sidebarEnabled: Bool?, sidebarShown: Bool?, layout:SplitViewState?, canAddContact:Bool?, isEmojiSection: Bool, chatLocation: ChatLocation, chatMode: ChatMode, canInvokeBasicActions: (delete: Bool, forward: Bool), isNotAccessible: Bool, restrictionInfo: PeerAccessRestrictionInfo?, mainPeer: Peer?, limitConfiguration: LimitsConfiguration, discussionGroupId: CachedChannelData.LinkedDiscussionPeerId, slowMode: SlowMode?, hasScheduled: Bool, failedMessageIds: Set, hidePinnedMessage: Bool, canPinMessage: Bool, tempPinnedMaxId: MessageId?, groupCall: ChatActiveGroupCallInfo?, messageSecretTimeout: CachedPeerAutoremoveTimeout?, reportMode: ReportReasonValue?, botMenu: BotMenu?) { self.interfaceState = interfaceState self.peer = peer self.notificationSettings = notificationSettings @@ -616,10 +840,9 @@ struct ChatPresentationInterfaceState: Equatable { self.initialAction = initialAction self.historyCount = historyCount self.isSearchMode = isSearchMode - self.editState = editState self.recordingState = recordingState self.isBlocked = isBlocked - self.reportStatus = reportStatus + self.peerStatus = peerStatus self.pinnedMessageId = pinnedMessageId self.urlPreview = urlPreview self.selectionState = selectionState @@ -628,12 +851,33 @@ struct ChatPresentationInterfaceState: Equatable { self.layout = layout self.canAddContact = canAddContact self.isEmojiSection = isEmojiSection + self.chatLocation = chatLocation + self.canInvokeBasicActions = canInvokeBasicActions + self.isNotAccessible = isNotAccessible + self.restrictionInfo = restrictionInfo + self.mainPeer = mainPeer + self.limitConfiguration = limitConfiguration + self.discussionGroupId = discussionGroupId + self.slowMode = slowMode + self.hasScheduled = hasScheduled + self.failedMessageIds = failedMessageIds + self.chatMode = chatMode + self.hidePinnedMessage = hidePinnedMessage + self.canPinMessage = canPinMessage + self.tempPinnedMaxId = tempPinnedMaxId + self.groupCall = groupCall + self.messageSecretTimeout = messageSecretTimeout + self.reportMode = reportMode + self.botMenu = botMenu } static func ==(lhs: ChatPresentationInterfaceState, rhs: ChatPresentationInterfaceState) -> Bool { if lhs.interfaceState != rhs.interfaceState { return false } + if lhs.discussionGroupId != rhs.discussionGroupId { + return false + } if let lhsPeer = lhs.peer, let rhsPeer = rhs.peer { if !lhsPeer.isEqual(rhsPeer) { return false @@ -642,15 +886,48 @@ struct ChatPresentationInterfaceState: Equatable { return false } - if lhs.inputContext != rhs.inputContext { + if let lhsPeer = lhs.mainPeer, let rhsPeer = rhs.mainPeer { + if !lhsPeer.isEqual(rhsPeer) { + return false + } + } else if (lhs.mainPeer == nil) != (rhs.mainPeer == nil) { + return false + } + if lhs.tempPinnedMaxId != rhs.tempPinnedMaxId { return false } + if lhs.restrictionInfo != rhs.restrictionInfo { + return false + } + if lhs.limitConfiguration != rhs.limitConfiguration { + return false + } + if lhs.botMenu != rhs.botMenu { + return false + } + if lhs.chatLocation != rhs.chatLocation { + return false + } + if lhs.hasScheduled != rhs.hasScheduled { + return false + } + if lhs.chatMode != rhs.chatMode { + return false + } + if lhs.canPinMessage != rhs.canPinMessage { + return false + } + if lhs.hidePinnedMessage != rhs.hidePinnedMessage { + return false + } if lhs.state != rhs.state { return false } - - if lhs.isSearchMode != rhs.isSearchMode { + if lhs.slowMode != rhs.slowMode { + return false + } + if lhs.isSearchMode.0 != rhs.isSearchMode.0 { return false } if lhs.sidebarEnabled != rhs.sidebarEnabled { @@ -665,12 +942,11 @@ struct ChatPresentationInterfaceState: Equatable { if lhs.canAddContact != rhs.canAddContact { return false } - - if lhs.recordingState != rhs.recordingState { + if lhs.reportMode != rhs.reportMode { return false } - if lhs.editState != rhs.editState { + if lhs.recordingState != rhs.recordingState { return false } @@ -690,7 +966,10 @@ struct ChatPresentationInterfaceState: Equatable { return false } - if lhs.reportStatus != rhs.reportStatus { + if lhs.peerStatus != rhs.peerStatus { + return false + } + if lhs.isNotAccessible != rhs.isNotAccessible { return false } @@ -698,18 +977,28 @@ struct ChatPresentationInterfaceState: Equatable { return false } + if lhs.groupCall != rhs.groupCall { + return false + } + if lhs.messageSecretTimeout != rhs.messageSecretTimeout { + return false + } + if lhs.pinnedMessageId != rhs.pinnedMessageId { return false } if lhs.isEmojiSection != rhs.isEmojiSection { return false } + if lhs.failedMessageIds != rhs.failedMessageIds { + return false + } if let lhsUrlPreview = lhs.urlPreview, let rhsUrlPreview = rhs.urlPreview { if lhsUrlPreview.0 != rhsUrlPreview.0 { return false } - if !lhsUrlPreview.1.isEqual(rhsUrlPreview.1) { + if !lhsUrlPreview.1.isEqual(to: rhsUrlPreview.1) { return false } } else if (lhs.urlPreview != nil) != (rhs.urlPreview != nil) { @@ -724,16 +1013,22 @@ struct ChatPresentationInterfaceState: Equatable { return false } + if lhs.inputContext != rhs.inputContext { + return false + } + if lhs.canInvokeBasicActions != rhs.canInvokeBasicActions { + return false + } return true } func updatedInterfaceState(_ f: (ChatInterfaceState) -> ChatInterfaceState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, editState: self.editState, recordingState: self.recordingState, isBlocked: self.isBlocked, reportStatus: self.reportStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection) + return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) } - + func updatedKeyboardButtonsMessage(_ message: Message?) -> ChatPresentationInterfaceState { - let interface = ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:message, initialAction:self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, editState: self.editState, recordingState: self.recordingState, isBlocked: self.isBlocked, reportStatus: self.reportStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection) + let interface = ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:message, initialAction:self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) if let peerId = peer?.id, let keyboardMessage = interface.keyboardButtonsMessage { if keyboardButtonsMessage?.id != keyboardMessage.id || keyboardButtonsMessage?.stableVersion != keyboardMessage.stableVersion { @@ -747,69 +1042,81 @@ struct ChatPresentationInterfaceState: Equatable { } func updatedPeer(_ f: (Peer?) -> Peer?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: f(self.peer), notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, editState: self.editState, recordingState: self.recordingState, isBlocked: self.isBlocked, reportStatus: self.reportStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection) + + let peer = f(self.peer) + + var restrictionInfo: PeerAccessRestrictionInfo? = self.restrictionInfo + if let peer = peer as? TelegramChannel, let info = peer.restrictionInfo { + restrictionInfo = info + } + + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction: self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) + } + + func updatedMainPeer(_ mainPeer: Peer?) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction: self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: restrictionInfo, mainPeer: mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) } func updatedNotificationSettings(_ notificationSettings:TelegramPeerNotificationSettings?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer:self.peer, notificationSettings: notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, editState: self.editState, recordingState: self.recordingState, isBlocked: self.isBlocked, reportStatus: self.reportStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer:self.peer, notificationSettings: notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) } func updatedHistoryCount(_ historyCount:Int?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer:self.peer, notificationSettings: notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:self.initialAction, historyCount: historyCount, isSearchMode: self.isSearchMode, editState: self.editState, recordingState: self.recordingState, isBlocked: self.isBlocked, reportStatus: self.reportStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer:self.peer, notificationSettings: notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:self.initialAction, historyCount: historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) } - func updatedSearchMode(_ searchMode: Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer:self.peer, notificationSettings: notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:self.initialAction, historyCount: historyCount, isSearchMode: searchMode, editState: self.editState, recordingState: self.recordingState, isBlocked: self.isBlocked, reportStatus: self.reportStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection) + func updatedSearchMode(_ isSearchMode: (Bool, Peer?, String?)) -> ChatPresentationInterfaceState { + + + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer:self.peer, notificationSettings: notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:self.initialAction, historyCount: historyCount, isSearchMode: isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) } func updatedInputQueryResult(_ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: f(self.inputQueryResult), keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, editState: self.editState, recordingState: self.recordingState, isBlocked: self.isBlocked, reportStatus: self.reportStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: f(self.inputQueryResult), keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) } func updatedInitialAction(_ initialAction:ChatInitialAction?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, editState: self.editState, recordingState: self.recordingState, isBlocked: self.isBlocked, reportStatus: self.reportStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) } - func withEditMessage(_ message:Message) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction: self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, editState: ChatEditState(message: message), recordingState: self.recordingState, isBlocked: self.isBlocked, reportStatus: self.reportStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection) - } - - func withoutEditMessage() -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, editState: nil, recordingState: self.recordingState, isBlocked: self.isBlocked, reportStatus: self.reportStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection) - } func withRecordingState(_ state:ChatRecordingState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction: self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, editState: self.editState, recordingState: state, isBlocked: self.isBlocked, reportStatus: self.reportStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction: self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: state, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) } func withoutRecordingState() -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, editState: self.editState, recordingState: nil, isBlocked: self.isBlocked, reportStatus: self.reportStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: nil, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) } func withUpdatedBlocked(_ blocked:Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, editState: self.editState, recordingState: self.recordingState, isBlocked: blocked, reportStatus: self.reportStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: blocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) } - func withUpdatedPinnedMessageId(_ messageId:MessageId?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, editState: self.editState, recordingState: self.recordingState, isBlocked: self.isBlocked, reportStatus: self.reportStatus, pinnedMessageId: messageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection) + func withUpdatedPinnedMessageId(_ pinnedMessageId:ChatPinnedMessage?) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) } - func withUpdatedReportStatus(_ reportStatus:PeerReportStatus) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, editState: self.editState, recordingState: self.recordingState, isBlocked: self.isBlocked, reportStatus: reportStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection) + + func withUpdatedPeerStatusSettings(_ peerStatus:ChatPeerStatus?) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) + } + + func withEditMessage(_ message:Message) -> ChatPresentationInterfaceState { + return self.updatedInterfaceState({$0.withEditMessage(message)}) + } + + func withoutEditMessage() -> ChatPresentationInterfaceState { + return self.updatedInterfaceState({$0.withoutEditMessage()}) } func withUpdatedEffectiveInputState(_ inputState: ChatTextInputState) -> ChatPresentationInterfaceState { - if let editState = self.editState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage: self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, editState: ChatEditState(message: editState.message, state: inputState), recordingState: self.recordingState, isBlocked: self.isBlocked, reportStatus: self.reportStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection) - } else { - return self.updatedInterfaceState({$0.withUpdatedInputState(inputState)}) - } + return self.updatedInterfaceState({$0.withUpdatedInputState(inputState)}) } func updatedUrlPreview(_ urlPreview: (String, TelegramMediaWebpage)?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, editState: self.editState, recordingState: self.recordingState, isBlocked: self.isBlocked, reportStatus: self.reportStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) } @@ -825,13 +1132,17 @@ struct ChatPresentationInterfaceState: Equatable { if let selectionState = self.selectionState { selectedIds.formUnion(selectionState.selectedIds) } - selectedIds.insert(messageId) + if selectedIds.count < 100 { + selectedIds.insert(messageId) + } else { + NSSound.beep() + } - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, editState: self.editState, recordingState: self.recordingState, isBlocked: self.isBlocked, reportStatus: self.reportStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState?.withUpdatedSelectedIds(selectedIds), sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) } func withUpdatedSelectedMessages(_ ids:Set) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, editState: self.editState, recordingState: self.recordingState, isBlocked: self.isBlocked, reportStatus: self.reportStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: ChatInterfaceSelectionState(selectedIds: ids), sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState?.withUpdatedSelectedIds(ids), sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) } func withToggledSelectedMessage(_ messageId: MessageId) -> ChatPresentationInterfaceState { @@ -839,41 +1150,144 @@ struct ChatPresentationInterfaceState: Equatable { if let selectionState = self.selectionState { selectedIds.formUnion(selectionState.selectedIds) } - if selectedIds.contains(messageId) { + let isSelected: Bool = selectedIds.contains(messageId) + if isSelected { let _ = selectedIds.remove(messageId) } else { - selectedIds.insert(messageId) + if selectedIds.count < 100 { + selectedIds.insert(messageId) + } else { + NSSound.beep() + } + } + + var selectionState: ChatInterfaceSelectionState = self.selectionState?.withUpdatedSelectedIds(selectedIds) ?? ChatInterfaceSelectionState(selectedIds: selectedIds, lastSelectedId: nil) + + if let event = NSApp.currentEvent { + if !event.modifierFlags.contains(.shift) { + if !isSelected { + selectionState = selectionState.withUpdatedLastSelected(messageId) + } else { + var foundBestOption: Bool = false + for id in selectedIds { + if id > messageId { + selectionState = selectionState.withUpdatedLastSelected(id) + foundBestOption = true + break + } + } + if !foundBestOption { + selectionState = selectionState.withUpdatedLastSelected(messageId) + } + } + } } - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, editState: self.editState, recordingState: self.recordingState, isBlocked: self.isBlocked, reportStatus: self.reportStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) } + func withRemovedSelectedMessage(_ messageId: MessageId) -> ChatPresentationInterfaceState { + var selectedIds = Set() + if let selectionState = self.selectionState { + selectedIds.formUnion(selectionState.selectedIds) + } + let _ = selectedIds.remove(messageId) + + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState?.withUpdatedSelectedIds(selectedIds), sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) + } + + func withoutSelectionState() -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, editState: self.editState, recordingState: self.recordingState, isBlocked: self.isBlocked, reportStatus: self.reportStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState:nil, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: nil, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) } func withSelectionState() -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, editState: self.editState, recordingState: self.recordingState, isBlocked: self.isBlocked, reportStatus: self.reportStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: ChatInterfaceSelectionState(selectedIds: []), sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: ChatInterfaceSelectionState(selectedIds: [], lastSelectedId: nil), sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) } func withToggledSidebarEnabled(_ enabled: Bool?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, editState: self.editState, recordingState: self.recordingState, isBlocked: self.isBlocked, reportStatus: self.reportStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: enabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: enabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) } func withToggledSidebarShown(_ shown: Bool?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, editState: self.editState, recordingState: self.recordingState, isBlocked: self.isBlocked, reportStatus: self.reportStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: shown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: shown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) } func withUpdatedLayout(_ layout: SplitViewState?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, editState: self.editState, recordingState: self.recordingState, isBlocked: self.isBlocked, reportStatus: self.reportStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) + } + + func withoutInitialAction() -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction: nil, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) } func withUpdatedContactAdding(_ canAddContact:Bool?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, editState: self.editState, recordingState: self.recordingState, isBlocked: self.isBlocked, reportStatus: self.reportStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: canAddContact, isEmojiSection: self.isEmojiSection) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) } func withUpdatedIsEmojiSection(_ isEmojiSection:Bool) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, editState: self.editState, recordingState: self.recordingState, isBlocked: self.isBlocked, reportStatus: self.reportStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: isEmojiSection) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction:initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) + } + + func withUpdatedBasicActions(_ canInvokeBasicActions:(delete: Bool, forward: Bool)) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction: self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) } + + func withUpdatedIsNotAccessible(_ isNotAccessible:Bool) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction: self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) + } + + func withUpdatedRestrictionInfo(_ restrictionInfo:PeerAccessRestrictionInfo?) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction: self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: self.limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) + } + + func withUpdatedLimitConfiguration(_ limitConfiguration:LimitsConfiguration) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction: self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) + } + + func withUpdatedDiscussionGroupId(_ discussionGroupId: CachedChannelData.LinkedDiscussionPeerId) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction: self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: limitConfiguration, discussionGroupId: discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) + } + func updateSlowMode(_ f: (SlowMode?)->SlowMode?) -> ChatPresentationInterfaceState { + + let updated = f(self.slowMode) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction: self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: limitConfiguration, discussionGroupId: self.discussionGroupId, slowMode: updated, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) + } + func withUpdatedHasScheduled(_ hasScheduled: Bool) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction: self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: limitConfiguration, discussionGroupId: discussionGroupId, slowMode: self.slowMode, hasScheduled: hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) + } + func withUpdatedFailedMessageIds(_ failedMessageIds: Set) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction: self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: limitConfiguration, discussionGroupId: discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) + } + + func withUpdatedHidePinnedMessage(_ hidePinnedMessage: Bool) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction: self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: limitConfiguration, discussionGroupId: discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) + } + + func withUpdatedCanPinMessage(_ canPinMessage: Bool) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction: self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: limitConfiguration, discussionGroupId: discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) + } + + func withUpdatedTempPinnedMaxId(_ tempPinnedMaxId: MessageId?) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction: self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: limitConfiguration, discussionGroupId: discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) + } + + func updatedGroupCall(_ f: (ChatActiveGroupCallInfo?)->ChatActiveGroupCallInfo?) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction: self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: limitConfiguration, discussionGroupId: discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: f(self.groupCall), messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) + } + + func withUpdatedMessageSecretTimeout(_ messageSecretTimeout: CachedPeerAutoremoveTimeout?) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction: self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: limitConfiguration, discussionGroupId: discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: messageSecretTimeout, reportMode: self.reportMode, botMenu: self.botMenu) + } + + func withUpdatedRepotMode(_ reportMode: ReportReasonValue?) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction: self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: limitConfiguration, discussionGroupId: discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: reportMode, botMenu: self.botMenu) + } + + func updateBotMenu(_ f:(BotMenu?)->BotMenu?) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, notificationSettings: self.notificationSettings, inputQueryResult: self.inputQueryResult, keyboardButtonsMessage:self.keyboardButtonsMessage, initialAction: self.initialAction, historyCount: self.historyCount, isSearchMode: self.isSearchMode, recordingState: self.recordingState, isBlocked: self.isBlocked, peerStatus: self.peerStatus, pinnedMessageId: self.pinnedMessageId, urlPreview: self.urlPreview, selectionState: self.selectionState, sidebarEnabled: self.sidebarEnabled, sidebarShown: self.sidebarShown, layout: self.layout, canAddContact: self.canAddContact, isEmojiSection: self.isEmojiSection, chatLocation: self.chatLocation, chatMode: self.chatMode, canInvokeBasicActions: self.canInvokeBasicActions, isNotAccessible: self.isNotAccessible, restrictionInfo: self.restrictionInfo, mainPeer: self.mainPeer, limitConfiguration: limitConfiguration, discussionGroupId: discussionGroupId, slowMode: self.slowMode, hasScheduled: self.hasScheduled, failedMessageIds: self.failedMessageIds, hidePinnedMessage: self.hidePinnedMessage, canPinMessage: self.canPinMessage, tempPinnedMaxId: self.tempPinnedMaxId, groupCall: self.groupCall, messageSecretTimeout: self.messageSecretTimeout, reportMode: self.reportMode, botMenu: f(self.botMenu)) + } + + } diff --git a/Telegram-Mac/ChatPresentationUtils.swift b/Telegram-Mac/ChatPresentationUtils.swift new file mode 100644 index 0000000000..f444b758d3 --- /dev/null +++ b/Telegram-Mac/ChatPresentationUtils.swift @@ -0,0 +1,495 @@ +// +// ChatPresentationUtils.swift +// Telegram +// +// Created by keepcoder on 23/12/2017. +// Copyright © 2017 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import SwiftSignalKit +import Postbox + +final class ChatMediaPresentation : Equatable { + + private let isIncoming: Bool + private let isBubble: Bool + + let activityBackground: NSColor + let activityForeground: NSColor + let waveformBackground: NSColor + let waveformForeground: NSColor + let text: NSColor + let grayText: NSColor + let link: NSColor + + init(isIncoming: Bool, isBubble: Bool, activityBackground: NSColor, activityForeground: NSColor, text: NSColor, grayText: NSColor, link: NSColor, waveformBackground: NSColor, waveformForeground: NSColor) { + self.isIncoming = isIncoming + self.isBubble = isBubble + self.activityForeground = activityForeground + self.activityBackground = activityBackground + self.text = text + self.grayText = grayText + self.link = link + self.waveformBackground = waveformBackground + self.waveformForeground = waveformForeground + } + + static func make(for message: Message, account: Account, renderType: ChatItemRenderType, theme: TelegramPresentationTheme) -> ChatMediaPresentation { + let isIncoming: Bool = message.isIncoming(account, renderType == .bubble) + return ChatMediaPresentation(isIncoming: isIncoming, + isBubble: renderType == .bubble, + activityBackground: theme.chat.activityBackground(isIncoming, renderType == .bubble), + activityForeground: theme.chat.activityForeground(isIncoming, renderType == .bubble), + text: theme.chat.textColor(isIncoming, renderType == .bubble), + grayText: theme.chat.grayText(isIncoming, renderType == .bubble), + link: theme.chat.linkColor(isIncoming, renderType == .bubble), + waveformBackground: theme.chat.waveformBackground(isIncoming, renderType == .bubble), + waveformForeground: theme.chat.waveformForeground(isIncoming, renderType == .bubble)) + } + + static var empty: ChatMediaPresentation { + return .init(isIncoming: true, isBubble: true, activityBackground: .clear, activityForeground: .clear, text: .clear, grayText: .clear, link: .clear, waveformBackground: .clear, waveformForeground: .clear) + } + + var fileThumb: CGImage { + if isBubble { + return isIncoming ? theme.icons.chatFileThumbBubble_incoming : theme.icons.chatFileThumbBubble_outgoing + } else { + return theme.icons.chatFileThumb + } + } + + + var pauseThumb: CGImage { + if isBubble { + return isIncoming ? theme.icons.chatMusicPauseBubble_incoming : theme.icons.chatMusicPauseBubble_outgoing + } else { + return theme.icons.chatMusicPause + } + } + var playThumb: CGImage { + if isBubble { + return isIncoming ? theme.icons.chatMusicPlayBubble_incoming : theme.icons.chatMusicPlayBubble_outgoing + } else { + return theme.icons.chatMusicPlay + } + } + + static var Empty: ChatMediaPresentation { + return ChatMediaPresentation(isIncoming: false, isBubble: false, activityBackground: theme.colors.accent, activityForeground: theme.colors.underSelectedColor, text: theme.colors.text, grayText: theme.colors.grayText, link: theme.colors.link, waveformBackground: theme.colors.waveformBackground, waveformForeground: theme.colors.waveformForeground) + } + + static func ==(lhs: ChatMediaPresentation, rhs: ChatMediaPresentation) -> Bool { + return lhs === rhs + } +} + +private func generatePercentageImage(color: NSColor, value: Int, font: NSFont) -> CGImage { + return generateImage(CGSize(width: 36.0, height: 16.0), rotatedContext: { size, context in + + + context.clear(CGRect(origin: CGPoint(), size: size)) + + + let layout = TextViewLayout(.initialize(string: "\(value)%", color: color, font: font), maximumNumberOfLines: 1, alignment: .right) + layout.measure(width: size.width) + if !layout.lines.isEmpty { + let line = layout.lines[0] + context.textMatrix = CGAffineTransform(scaleX: 1.0, y: -1.0) + let penOffset = CGFloat( CTLineGetPenOffsetForFlush(line.line, layout.penFlush, Double(size.width))) + line.frame.minX + + context.setAllowsFontSubpixelPositioning(true) + context.setShouldSubpixelPositionFonts(true) + context.setAllowsAntialiasing(true) + context.setShouldAntialias(true) + context.setAllowsFontSmoothing(System.backingScale == 1.0) + context.setShouldSmoothFonts(System.backingScale == 1.0) + + context.textPosition = CGPoint(x: penOffset, y: line.frame.minY) + + CTLineDraw(line.line, context) + } + + })! +} + + +final class TelegramChatColors { + + + + private var _generatedPercentageAnimationImages:[CGImage]? + private var _generatedPercentageAnimationImagesIncomingBubbled:[CGImage]? + private var _generatedPercentageAnimationImagesOutgoingBubbled:[CGImage]? + private var _generatedPercentageAnimationImagesPlain:[CGImage]? + private var _generatedPercentageAnimationImagesIncomingBubbledPlain:[CGImage]? + private var _generatedPercentageAnimationImagesOutgoingBubbledPlain:[CGImage]? + + private var generatedPercentageAnimationImages:[CGImage] { + if let _generatedPercentageAnimationImages = self._generatedPercentageAnimationImages { + return _generatedPercentageAnimationImages + } else { + var images:[CGImage] = [] + for i in 0 ... 100 { + images.append(generatePercentageImage(color: palette.text, value: i, font: .bold(12))) + } + self._generatedPercentageAnimationImages = images + return images + } + } + private var generatedPercentageAnimationImagesIncomingBubbled:[CGImage] { + if let value = self._generatedPercentageAnimationImagesIncomingBubbled { + return value + } else { + var images:[CGImage] = [] + for i in 0 ... 100 { + images.append(generatePercentageImage(color: palette.textBubble_incoming, value: i, font: .bold(12))) + } + self._generatedPercentageAnimationImagesIncomingBubbled = images + return images + } + } + private var generatedPercentageAnimationImagesOutgoingBubbled:[CGImage] { + if let value = self._generatedPercentageAnimationImagesOutgoingBubbled { + return value + } else { + var images:[CGImage] = [] + for i in 0 ... 100 { + images.append(generatePercentageImage(color: palette.textBubble_outgoing, value: i, font: .bold(12))) + } + self._generatedPercentageAnimationImagesOutgoingBubbled = images + return images + } + } + private var generatedPercentageAnimationImagesPlain:[CGImage] { + if let value = self._generatedPercentageAnimationImagesPlain { + return value + } else { + var images:[CGImage] = [] + for i in 0 ... 100 { + images.append(generatePercentageImage(color: palette.text, value: i, font: .bold(12))) + } + self._generatedPercentageAnimationImagesPlain = images + return images + } + } + private var generatedPercentageAnimationImagesIncomingBubbledPlain:[CGImage] { + if let value = self._generatedPercentageAnimationImagesIncomingBubbledPlain { + return value + } else { + var images:[CGImage] = [] + for i in 0 ... 100 { + images.append(generatePercentageImage(color: palette.textBubble_incoming, value: i, font: .normal(12))) + } + self._generatedPercentageAnimationImagesIncomingBubbledPlain = images + return images + } + } + private var generatedPercentageAnimationImagesOutgoingBubbledPlain:[CGImage] { + if let value = self._generatedPercentageAnimationImagesOutgoingBubbledPlain { + return value + } else { + var images:[CGImage] = [] + for i in 0 ... 100 { + images.append(generatePercentageImage(color: palette.textBubble_outgoing, value: i, font: .normal(12))) + } + self._generatedPercentageAnimationImagesOutgoingBubbledPlain = images + return images + } + } + + + private let palette: ColorPalette + init(_ palette: ColorPalette, _ bubbled: Bool) { + self.palette = palette + } + + private var cacheDict: [String: CGImage] = [:] + + func messageSecretTimer(_ value: String) -> CGImage { + if let value = cacheDict[value] { + return value + } else { + let node = TextNode.layoutText(.initialize(string: value, color: theme.colors.grayIcon, font: .normal(15)), nil, 1, .end, NSMakeSize(30, 30), nil, false, .left) + + let image = generateImage(NSMakeSize(30, 30), rotatedContext: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + node.1.draw(rect.focus(node.0.size), in: ctx, backingScaleFactor: 1.0, backgroundColor: .clear) + })! + cacheDict[value] = image + + return image + } + + } + + //chatGotoMessageWallpaper / chatShareWallpaper / chatSwipeReplyWallpaper + + func chat_goto_message_bubble(theme: TelegramPresentationTheme) -> CGImage { + if let value = cacheDict["chat_goto_message_bubble"] { + return value + } else { + let image = NSImage(named: "Icon_GotoBubbleMessage")!.precomposed(theme.chatServiceItemTextColor) + cacheDict["chat_goto_message_bubble"] = image + return image + } + } + func chat_share_bubble(theme: TelegramPresentationTheme) -> CGImage { + if let value = cacheDict["chat_share_bubble"] { + return value + } else { + let image = NSImage(named: "Icon_ChannelShare")!.precomposed(theme.chatServiceItemTextColor) + cacheDict["chat_share_bubble"] = image + return image + } + } + func chat_reply_swipe_bubble(theme: TelegramPresentationTheme) -> CGImage { + if let value = cacheDict["chat_reply_swipe_bubble"] { + return value + } else { + let image = NSImage(named: "Icon_ChannelShare")!.precomposed(theme.chatServiceItemTextColor) + cacheDict["chat_reply_swipe_bubble"] = image + return image + } + } + func chat_like_message_bubble(theme: TelegramPresentationTheme) -> CGImage { + if let value = cacheDict["chat_like_message_bubble"] { + return value + } else { + let image = NSImage(named: "Icon_Like_MessageButton")!.precomposed(theme.chatServiceItemTextColor) + cacheDict["chat_like_message_bubble"] = image + return image + } + } + func chat_like_message_unlike_bubble(theme: TelegramPresentationTheme) -> CGImage { + if let value = cacheDict["chat_like_message_unlike_bubble"] { + return value + } else { + let image = NSImage(named: "Icon_Like_MessageButtonUnlike")!.precomposed(theme.chatServiceItemTextColor) + cacheDict["chat_like_message_unlike_bubble"] = image + return image + } + } + + private var _chatActionUrl: CGImage? + func chatActionUrl(theme: TelegramPresentationTheme) -> CGImage { + if let chatActionUrl = _chatActionUrl { + return chatActionUrl + } else { + let image = #imageLiteral(resourceName: "Icon_InlineBotUrl").precomposed(theme.chatServiceItemTextColor) + _chatActionUrl = image + return image + } + } + + private var _chatInvoiceAction: CGImage? + func chatInvoiceAction(theme: TelegramPresentationTheme) -> CGImage { + if let _chatInvoiceAction = _chatInvoiceAction { + return _chatInvoiceAction + } else { + let image = NSImage(named: "Icon_ChatInvoice")!.precomposed(theme.chatServiceItemTextColor) + _chatInvoiceAction = image + return image + } + } + + func pollPercentAnimatedIcons(_ incoming: Bool, _ bubbled: Bool, from fromValue: CGFloat, to toValue: CGFloat, duration: Double) -> [CGImage] { + let minimumFrameDuration = 1.0 / 60 + let numberOfFrames = max(1, Int(duration / minimumFrameDuration)) + var images: [CGImage] = [] + + let generated = bubbled ? incoming ? generatedPercentageAnimationImagesIncomingBubbledPlain : generatedPercentageAnimationImagesOutgoingBubbledPlain : generatedPercentageAnimationImagesPlain + + for i in 0 ..< numberOfFrames { + let t = CGFloat(i) / CGFloat(numberOfFrames) + let value = (1.0 - t) * fromValue + t * toValue + images.append(generated[Int(round(value))]) + } + return images + } + + func pollPercentAnimatedIcon(_ incoming: Bool, _ bubbled: Bool, value: Int) -> CGImage { + let generated = bubbled ? incoming ? generatedPercentageAnimationImagesIncomingBubbledPlain : generatedPercentageAnimationImagesOutgoingBubbledPlain : generatedPercentageAnimationImagesPlain + return generated[max(min(generated.count - 1, value), 0)] + } + + func activityBackground(_ incoming: Bool, _ bubbled: Bool) -> NSColor { + return bubbled ? incoming ? palette.fileActivityBackgroundBubble_incoming : palette.fileActivityBackgroundBubble_outgoing : palette.fileActivityBackground + } + func activityForeground(_ incoming: Bool, _ bubbled: Bool) -> NSColor { + return bubbled ? incoming ? palette.fileActivityForegroundBubble_incoming : palette.fileActivityForegroundBubble_outgoing : palette.fileActivityForeground + } + + func webPreviewActivity(_ incoming: Bool, _ bubbled: Bool) -> NSColor { + return bubbled ? incoming ? palette.webPreviewActivityBubble_incoming : palette.webPreviewActivityBubble_outgoing : palette.webPreviewActivity + } + func pollOptionBorder(_ incoming: Bool, _ bubbled: Bool) -> NSColor { + return (bubbled ? incoming ? grayText(incoming, bubbled) : grayText(incoming, bubbled) : palette.grayText).withAlphaComponent(0.2) + } + func pollOptionUnselectedImage(_ incoming: Bool, _ bubbled: Bool) -> CGImage { + return bubbled ? incoming ? theme.icons.chatPollVoteUnselectedBubble_incoming : theme.icons.chatPollVoteUnselectedBubble_outgoing : theme.icons.chatPollVoteUnselected + } + func waveformBackground(_ incoming: Bool, _ bubbled: Bool) -> NSColor { + return bubbled ? incoming ? palette.waveformBackgroundBubble_incoming : palette.waveformBackgroundBubble_outgoing : palette.waveformBackground + } + func waveformForeground(_ incoming: Bool, _ bubbled: Bool) -> NSColor { + return bubbled ? incoming ? palette.waveformForegroundBubble_incoming : palette.waveformForegroundBubble_outgoing : palette.waveformForeground + } + + + + func backgroundColor(_ incoming: Bool, _ bubbled: Bool) -> NSColor { + return bubbled ? incoming ? System.supportsTransparentFontDrawing ? .clear : palette.bubbleBackground_incoming : System.supportsTransparentFontDrawing ? .clear : palette.blendedOutgoingColors : palette.chatBackground + } + + func backgoundSelectedColor(_ incoming: Bool, _ bubbled: Bool) -> NSColor { + return bubbled ? incoming ? palette.bubbleBackgroundHighlight_incoming : palette.bubbleBackgroundHighlight_outgoing : palette.background + } + + func bubbleBorderColor(_ incoming: Bool, _ bubbled: Bool) -> NSColor { + return incoming ? palette.bubbleBorder_incoming : palette.bubbleBorder_outgoing//.clear//palette.bubbleBorder_outgoing + } + func bubbleBackgroundColor(_ incoming: Bool, _ bubbled: Bool) -> NSColor { + return bubbled ? incoming ? palette.bubbleBackground_incoming : palette.blendedOutgoingColors : .clear//.clear//palette.bubbleBorder_outgoing + } + + func textColor(_ incoming: Bool, _ bubbled: Bool) -> NSColor { + return bubbled ? incoming ? palette.textBubble_incoming : palette.textBubble_outgoing : palette.text + } + + func monospacedPreColor(_ incoming: Bool, _ bubbled: Bool) -> NSColor { + return bubbled ? incoming ? palette.monospacedPreBubble_incoming : palette.monospacedPreBubble_outgoing : palette.monospacedPre + } + func monospacedCodeColor(_ incoming: Bool, _ bubbled: Bool) -> NSColor { + return bubbled ? incoming ? palette.monospacedCodeBubble_incoming : palette.monospacedCodeBubble_outgoing : palette.monospacedCode + } + + func selectText(_ incoming: Bool, _ bubbled: Bool) -> NSColor { + return bubbled ? incoming ? palette.selectTextBubble_incoming : palette.selectTextBubble_outgoing : palette.selectText + } + + func grayText(_ incoming: Bool, _ bubbled: Bool) -> NSColor { + return bubbled ? incoming ? palette.grayTextBubble_incoming : palette.grayTextBubble_outgoing : palette.grayText + } + func redUI(_ incoming: Bool, _ bubbled: Bool) -> NSColor { + return bubbled ? incoming ? palette.redBubble_incoming : palette.redBubble_outgoing : palette.redUI + } + func greenUI(_ incoming: Bool, _ bubbled: Bool) -> NSColor { + return bubbled ? incoming ? palette.greenBubble_incoming : palette.greenBubble_outgoing : palette.greenUI + } + func linkColor(_ incoming: Bool, _ bubbled: Bool) -> NSColor { + return bubbled ? incoming ? palette.linkBubble_incoming : palette.linkBubble_outgoing : palette.accent + } + + func pollSelected(_ incoming: Bool, _ bubbled: Bool, icons: TelegramIconsTheme) -> CGImage { + return bubbled ? incoming ? icons.poll_selected_incoming : icons.poll_selected_outgoing : icons.poll_selected + } + func pollSelection(_ incoming: Bool, _ bubbled: Bool, icons: TelegramIconsTheme) -> CGImage { + return bubbled ? incoming ? icons.poll_selection_incoming : icons.poll_selection_outgoing : icons.poll_selection + } + func pollSelectedCorrect(_ incoming: Bool, _ bubbled: Bool, icons: TelegramIconsTheme) -> CGImage { + return bubbled ? incoming ? icons.poll_selected_correct_incoming : icons.poll_selected_correct_outgoing : icons.poll_selected_correct + } + func pollSelectedIncorrect(_ incoming: Bool, _ bubbled: Bool, icons: TelegramIconsTheme) -> CGImage { + return bubbled ? incoming ? icons.poll_selected_incorrect_incoming : icons.poll_selected_incorrect_outgoing : icons.poll_selected_incorrect + } + + func channelInfoPromo(_ incoming: Bool, _ bubbled: Bool, icons: TelegramIconsTheme) -> CGImage { + return bubbled ? incoming ? icons.channel_info_promo_bubble_incoming : icons.channel_info_promo_bubble_outgoing : icons.channel_info_promo + } + + func channelViewsIcon(_ item: ChatRowItem) -> CGImage { + return item.isStateOverlayLayout ? !item.isInteractiveMedia ? item.presentation.chatChannelViewsOverlayServiceBubble : item.presentation.icons.chatChannelViewsOverlayBubble : item.hasBubble ? item.isIncoming ? item.presentation.icons.chatChannelViewsInBubble_incoming : item.presentation.icons.chatChannelViewsInBubble_outgoing : item.presentation.icons.chatChannelViewsOutBubble + } + + func messagePinnedIcon(_ item: ChatRowItem) -> CGImage { + if item.isStateOverlayLayout { + if !item.isInteractiveMedia { + return item.presentation.chat_pinned_message_overlay_service_bubble + } else { + return item.presentation.icons.chat_pinned_message_overlay_bubble + } + } else { + return item.hasBubble ? item.isIncoming ? item.presentation.icons.chat_pinned_message_bubble_incoming : item.presentation.icons.chat_pinned_message_bubble_outgoing : item.presentation.icons.chat_pinned_message + } + } + + func repliesCountIcon(_ item: ChatRowItem) -> CGImage { + return item.isStateOverlayLayout ? !item.isInteractiveMedia ? item.presentation.chat_reply_count_overlay_service_bubble : item.presentation.icons.chat_reply_count_overlay : item.hasBubble ? item.isIncoming ? item.presentation.icons.chat_reply_count_bubble_incoming : item.presentation.icons.chat_reply_count_bubble_outgoing : item.presentation.icons.chat_reply_count + } + func likedIcon(_ item: ChatRowItem) -> CGImage { + if item.isLiked { + return item.isStateOverlayLayout ? !item.isInteractiveMedia ? item.presentation.chat_like_inside_bubble_service : item.presentation.icons.chat_like_inside_bubble_overlay : item.hasBubble ? item.isIncoming ? item.presentation.icons.chat_like_inside_bubble_incoming : item.presentation.icons.chat_like_inside_bubble_outgoing : item.presentation.icons.chat_like_inside + } else { + return item.isStateOverlayLayout ? !item.isInteractiveMedia ? item.presentation.chat_like_inside_empty_bubble_service : item.presentation.icons.chat_like_inside_empty_bubble_overlay : item.hasBubble ? item.isIncoming ? item.presentation.icons.chat_like_inside_empty_bubble_incoming : item.presentation.icons.chat_like_inside_empty_bubble_outgoing : item.presentation.icons.chat_like_inside_empty + } + } + + func stateStateIcon(_ item: ChatRowItem) -> CGImage { + return item.isFailed ? item.presentation.icons.sentFailed : (item.isStateOverlayLayout ? !item.isInteractiveMedia ? item.presentation.chatReadMarkServiceOverlayBubble1 : theme.icons.chatReadMarkOverlayBubble1 : item.hasBubble ? item.isIncoming ? item.presentation.icons.chatReadMarkInBubble1_incoming : item.presentation.icons.chatReadMarkInBubble1_outgoing : item.presentation.icons.chatReadMarkOutBubble1) + } + func readStateIcon(_ item: ChatRowItem) -> CGImage { + return item.isStateOverlayLayout ? !item.isInteractiveMedia ? item.presentation.chatReadMarkServiceOverlayBubble2 : item.presentation.icons.chatReadMarkOverlayBubble2 : item.hasBubble ? item.isIncoming ? item.presentation.icons.chatReadMarkInBubble2_incoming : item.presentation.icons.chatReadMarkInBubble2_outgoing : item.presentation.icons.chatReadMarkOutBubble2 + } + + func quizSolution(_ item: ChatRowItem) -> CGImage { + return item.hasBubble ? item.isIncoming ? item.presentation.icons.chat_quiz_explanation_bubble_incoming : item.presentation.icons.chat_quiz_explanation_bubble_outgoing : item.presentation.icons.chat_quiz_explanation + } + + func instantPageIcon(_ incoming: Bool, _ bubbled: Bool, presentation: TelegramPresentationTheme) -> CGImage { + return bubbled ? incoming ? presentation.icons.chatInstantViewBubble_incoming : presentation.icons.chatInstantViewBubble_outgoing : presentation.icons.chatInstantView + } + + func sendingFrameIcon(_ item: ChatRowItem) -> CGImage { + return item.isStateOverlayLayout ? !item.isInteractiveMedia ? item.presentation.chatSendingOverlayServiceFrame : item.presentation.icons.chatSendingOverlayFrame : item.hasBubble ? item.isIncoming ? item.presentation.icons.chatSendingInFrame_incoming : item.presentation.icons.chatSendingInFrame_outgoing : item.presentation.icons.chatSendingOutFrame + } + func sendingHourIcon(_ item: ChatRowItem) -> CGImage { + return item.isStateOverlayLayout ? !item.isInteractiveMedia ? item.presentation.chatSendingOverlayServiceHour : item.presentation.icons.chatSendingOverlayHour : item.hasBubble ? item.isIncoming ? item.presentation.icons.chatSendingInHour_incoming : item.presentation.icons.chatSendingInHour_outgoing : item.presentation.icons.chatSendingOutHour + } + func sendingMinIcon(_ item: ChatRowItem) -> CGImage { + return item.isStateOverlayLayout ? !item.isInteractiveMedia ? item.presentation.chatSendingOverlayServiceMin : item.presentation.icons.chatSendingOverlayMin : item.hasBubble ? item.isIncoming ? item.presentation.icons.chatSendingInMin_incoming : item.presentation.icons.chatSendingInMin_outgoing : item.presentation.icons.chatSendingOutMin + } + + func chatCallIcon(_ item: ChatCallRowItem) -> CGImage { + if item.hasBubble { + return !item.isIncoming ? (item.failed ? item.presentation.icons.chatFailedCallBubble_outgoing : item.presentation.icons.chatCallBubble_outgoing) : (item.failed ? item.presentation.icons.chatFailedCallBubble_incoming : item.presentation.icons.chatCallBubble_outgoing) + } else { + return !item.isIncoming ? (item.failed ? item.presentation.icons.chatFailedCall_outgoing : item.presentation.icons.chatCall_outgoing) : (item.failed ? item.presentation.icons.chatFailedCall_incoming : item.presentation.icons.chatCall_outgoing) + } + } + + func chatCallFallbackIcon(_ item: ChatCallRowItem) -> CGImage { + if item.isVideo { + return item.hasBubble ? item.isIncoming ? item.presentation.icons.chatFallbackVideoCallBubble_incoming : item.presentation.icons.chatFallbackVideoCallBubble_outgoing : item.presentation.icons.chatFallbackVideoCall + } else { + return item.hasBubble ? item.isIncoming ? item.presentation.icons.chatFallbackCallBubble_incoming : item.presentation.icons.chatFallbackCallBubble_outgoing : item.presentation.icons.chatFallbackCall + } + } + + func peerName(_ index: Int) -> NSColor { + let array = [theme.colors.groupPeerNameRed, + theme.colors.groupPeerNameOrange, + theme.colors.groupPeerNameViolet, + theme.colors.groupPeerNameGreen, + theme.colors.groupPeerNameCyan, + theme.colors.groupPeerNameLightBlue, + theme.colors.groupPeerNameBlue] + + return array[index] + } + + func replyTitle(_ item: ChatRowItem) -> NSColor { + return item.hasBubble ? (item.isIncoming ? item.presentation.colors.chatReplyTitleBubble_incoming : item.presentation.colors.chatReplyTitleBubble_outgoing) : item.presentation.colors.chatReplyTitle + } + func replyText(_ item: ChatRowItem) -> NSColor { + return item.hasBubble ? (item.isIncoming ? item.presentation.colors.chatReplyTextEnabledBubble_incoming : item.presentation.colors.chatReplyTextEnabledBubble_outgoing) : item.presentation.colors.chatReplyTextEnabled + } + func replyDisabledText(_ item: ChatRowItem) -> NSColor { + return item.hasBubble ? (item.isIncoming ? item.presentation.colors.chatReplyTextDisabledBubble_incoming : item.presentation.colors.chatReplyTextDisabledBubble_outgoing) : item.presentation.colors.chatReplyTextDisabled + } +} diff --git a/Telegram-Mac/ChatRecorderOverlayWindow.swift b/Telegram-Mac/ChatRecorderOverlayWindow.swift new file mode 100644 index 0000000000..950fa9f6a7 --- /dev/null +++ b/Telegram-Mac/ChatRecorderOverlayWindow.swift @@ -0,0 +1,357 @@ +// +// ChatRecorderOverlayWindow.swift +// Telegram +// +// Created by Mikhail Filimonov on 07/05/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import SwiftSignalKit + +private enum ChatRecordingOverlayState { + case voice + case video + case fixed +} + +private final class LockControl : View { + private let head: ImageView = ImageView() + private let body: ImageView = ImageView() + private let arrow: ImageView = ImageView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + layer?.cornerRadius = frameRect.width / 2 + addSubview(head) + addSubview(arrow) + addSubview(body) + updateLocalizationAndTheme(theme: theme) + } + + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) + backgroundColor = theme.colors.background + head.image = theme.icons.chatOverlayLockerHeadRecording + head.sizeToFit() + body.image = theme.icons.chatOverlayLockerBodyRecording + body.sizeToFit() + arrow.image = theme.icons.chatOverlayLockArrowRecording + arrow.sizeToFit() + layer?.borderColor = theme.colors.accent.cgColor + layer?.borderWidth = .borderSize + } + + private var currentPercent: CGFloat = 1.0 + + override func layout() { + super.layout() + arrow.centerX(y: frame.height - arrow.frame.height - 8) + body.centerX(y: floorToScreenPixels(backingScaleFactor, (30 - body.frame.height)/2) + 3) + head.centerX(y: 4) + } + + fileprivate func updatePercent(_ percent: CGFloat) { + arrow.change(opacity: percent, animated: true) + +// let dh: CGFloat = 4 +// let dm: CGFloat = 7 +// let y = max(dm, min(dh, dm + percent * dm)) +// head.centerX(y: y) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +private class ChatRecorderOverlayView : Control { + private let innerContainer: Control = Control() + private let outerContainer: Control = Control() + private let stateView: ImageView = ImageView() + private var currentLevel: Double = 1.0 + private var previousTime: Date = Date() + private let playbackAudioLevelView: VoiceBlobView + required init(frame frameRect: NSRect) { + playbackAudioLevelView = VoiceBlobView( + frame: NSMakeRect(0, 0, frameRect.width, frameRect.height), + maxLevel: 0.3, + smallBlobRange: (0, 0), + mediumBlobRange: (0.7, 0.8), + bigBlobRange: (0.8, 0.9) + ) + + super.init(frame: frameRect) + layer?.cornerRadius = frameRect.width / 2 + backgroundColor = .clear +// +// outerContainer.setFrameSize(NSMakeSize(frameRect.width - 30, frameRect.height - 30)) +// outerContainer.backgroundColor = theme.colors.accent.withAlphaComponent(0.5) +// outerContainer.layer?.cornerRadius = outerContainer.frame.width / 2 +// addSubview(outerContainer) +// outerContainer.center() +// // self.outerContainer.animates = true +// + + addSubview(playbackAudioLevelView) + + playbackAudioLevelView.center() + + innerContainer.setFrameSize(NSMakeSize(frameRect.width - 40, frameRect.height - 40)) + innerContainer.backgroundColor = theme.colors.accent + innerContainer.layer?.cornerRadius = innerContainer.frame.width / 2 + addSubview(innerContainer) + innerContainer.center() + // self.innerContainer.animates = true + addSubview(stateView) +// + + + self.playbackAudioLevelView.startAnimating() + + } + + fileprivate func updateState(_ overlayState: ChatRecordingOverlayState) { + switch overlayState { + case .voice: + stateView.image = theme.icons.chatOverlayVoiceRecording + case .video: + stateView.image = theme.icons.chatOverlayVideoRecording + case .fixed: + stateView.image = theme.icons.chatOverlaySendRecording + } + stateView.sizeToFit() + stateView.center() + + updateInside() + + } + + func updatePeakLevel(_ peakLevel: Float) { + //NSLog("\(peakLevel)") + + let power = mappingRange(Double(peakLevel), 0.3, 3, 0, 1); + if (Date().timeIntervalSinceNow - previousTime.timeIntervalSinceNow) > 0.2 { + playbackAudioLevelView.updateLevel(CGFloat(power)) + self.previousTime = Date() + } + } + + func updateInside() { + innerContainer.backgroundColor = mouseInside() ? theme.colors.accent : theme.colors.redUI + let animation = CABasicAnimation(keyPath: "backgroundColor") + animation.duration = 0.1 + innerContainer.layer?.add(animation, forKey: "backgroundColor") + + self.playbackAudioLevelView.setColor(mouseInside() ? theme.colors.accent : theme.colors.redUI) + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class ChatRecorderOverlayWindowController : NSObject { + let window: Window + private let parent: Window + private let disposable = MetaDisposable() + private let chatInteraction: ChatInteraction + private var state: ChatRecordingOverlayState + private let startMouseLocation: NSPoint + private let lockWindow: Window + init(parent: Window, chatInteraction: ChatInteraction) { + self.parent = parent + self.chatInteraction = chatInteraction + self.state = chatInteraction.presentation.recordingState is ChatRecordingAudioState ? .voice : .video + let size = NSMakeSize(120, 120) + + window = Window(contentRect: NSMakeRect(parent.frame.maxX - size.width + 25, parent.frame.minY - 35, size.width, size.height), styleMask: [], backing: .buffered, defer: true) + window.backgroundColor = .clear + window.contentView = ChatRecorderOverlayView(frame: NSMakeRect(0, 0, size.width, size.height)) + + lockWindow = Window(contentRect: NSMakeRect(window.frame.midX - 12.5, parent.frame.minY + 160, 26, 50), styleMask: [], backing: .buffered, defer: true) + lockWindow.contentView?.addSubview(LockControl(frame: NSMakeRect(0, 0, 26, 50))) + lockWindow.backgroundColor = .clear + startMouseLocation = window.mouseLocationOutsideOfEventStream + super.init() + self.view.updateState(state) + window.setFrameOrigin(NSMakePoint(minX, window.frame.minY)) + } + + private var view: ChatRecorderOverlayView { + return window.contentView as! ChatRecorderOverlayView + } + + func stopAndSend() { + if let recorder = chatInteraction.presentation.recordingState { + recorder.stop() + delay(0.1, closure: { + self.chatInteraction.mediaPromise.set(recorder.data) + closeAllModals() + }) + } + chatInteraction.update({$0.withoutRecordingState()}) + } + + func stopAndCancel() { + let proccess = { [weak self] in + guard let `self` = self else {return} + if let recorder = self.chatInteraction.presentation.recordingState { + recorder.stop() + recorder.dispose() + closeAllModals() + } + self.chatInteraction.update({$0.withoutRecordingState()}) + } + if state == .fixed { + confirm(for: parent, information: L10n.chatRecordingCancel, okTitle: L10n.alertDiscard, cancelTitle: L10n.alertNO, successHandler: { _ in + proccess() + }) + } else { + proccess() + } + } + + var minX: CGFloat { + let navigation = chatInteraction.context.sharedContext.bindings.rootNavigation() + if navigation.genericView.state == .dual, let sidebar = navigation.sidebar { + return parent.frame.maxX - window.frame.width - sidebar.frame.width + 35 + } + return parent.frame.maxX - window.frame.width + 25 + } + + private func moveWindow() -> Void { + let location = window.mouseLocationOutsideOfEventStream + let defaultY = parent.frame.minY - 35 + window.setFrameOrigin(NSMakePoint(minX, max(window.frame.minY - (startMouseLocation.y - location.y), defaultY))) + let dif = window.frame.minY - defaultY + let maxDif: CGFloat = 100 + if dif > maxDif { + hold(animated: false) + } else { + let view = self.lockControl + let dh: CGFloat = 50 + let dm: CGFloat = 30 + let percent = 1.0 - dif / 100 + let h = max(dm, min(dh, dm + percent * dm)) + view.frame = NSMakeRect(0, view.superview!.frame.height - h, view.frame.width, h) + view.updatePercent(percent) + } + } + + private func hold(animated: Bool) { + + chatInteraction.presentation.recordingState?.holdpromise.set(true) + + view.updateInside() + + let defaultY = parent.frame.minY - 35 + + self.state = .fixed + view.updateState(.fixed) + window.animator().setFrame(window.frame.offsetBy(dx: 0, dy: -(window.frame.minY - defaultY)), display: true) + parent.remove(object: self, for: .leftMouseDown) + parent.remove(object: self, for: .leftMouseUp) + window.remove(object: self, for: .leftMouseUp) + + let proccessMouseUp:(NSEvent)->KeyHandlerResult = { [weak self] _ in + guard let `self` = self else {return .rejected} + return self.proccessMouseUp() + } + parent.set(mouseHandler: proccessMouseUp, with: self, for: .leftMouseDown, priority: .modal) + window.set(mouseHandler: proccessMouseUp, with: self, for: .leftMouseDown, priority: .modal) + + parent.removeChildWindow(lockWindow) + + lockControl.change(opacity: 0, animated: animated) { [weak self] _ in + self?.lockWindow.orderOut(nil) + } + } + + private var lockControl: LockControl { + return lockWindow.contentView!.subviews.first! as! LockControl + } + + private func proccessMouseUp()-> KeyHandlerResult { + if self.view.mouseInside() { + self.stopAndSend() + } else { + self.stopAndCancel() + } + return .invoked + } + + func show(animated: Bool) { + + guard let recorder = chatInteraction.presentation.recordingState else { return } + + + parent.addChildWindow(lockWindow, ordered: .above) + parent.addChildWindow(window, ordered: .above) + + disposable.set((recorder.micLevel |> deliverOnMainQueue).start(next: { [weak self] value in + self?.view.updatePeakLevel(value) + })) + + view.layer?.animateScaleSpring(from: 0.1, to: 1.0, duration: 0.4) + view.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + + parent.set(mouseHandler: { [weak self] _ -> KeyHandlerResult in + self?.view.updateInside() + return .invoked + }, with: self, for: .mouseMoved, priority: .modal) + + window.set(mouseHandler: { [weak self] _ -> KeyHandlerResult in + self?.view.updateInside() + return .invoked + }, with: self, for: .mouseMoved, priority: .modal) + + parent.set(mouseHandler: { [weak self] _ -> KeyHandlerResult in + guard let `self` = self else {return .rejected} + self.view.updateInside() + if self.state != .fixed { + self.moveWindow() + } + return .invoked + }, with: self, for: .leftMouseDragged, priority: .modal) + + let proccessMouseUp:(NSEvent)->KeyHandlerResult = { [weak self] _ in + guard let `self` = self else {return .rejected} + return self.proccessMouseUp() + } + + parent.set(mouseHandler: { _ in return .invoked}, with: self, for: .leftMouseDown, priority: .modal) + + parent.set(mouseHandler: proccessMouseUp, with: self, for: .leftMouseUp, priority: .modal) + window.set(mouseHandler: proccessMouseUp, with: self, for: .leftMouseUp, priority: .modal) + + if recorder.autohold { + hold(animated: false) + } + + } + + func hide(animated: Bool) { + parent.removeChildWindow(window) + parent.removeChildWindow(lockWindow) + lockWindow.contentView?._change(opacity: 0, animated: true) + var strongSelf:ChatRecorderOverlayWindowController? = self + view.layer?.animateAlpha(from: 1.0, to: 0, duration: 0.2, removeOnCompletion: false, completion: { complete in + strongSelf?.window.orderOut(nil) + strongSelf?.lockWindow.orderOut(nil) + strongSelf = nil + }) + parent.removeAllHandlers(for: self) + } + + deinit { + var bp:Int = 0 + bp += 1 + } +} diff --git a/Telegram-Mac/ChatReplyPreviewController.swift b/Telegram-Mac/ChatReplyPreviewController.swift deleted file mode 100644 index bf09c8ff7c..0000000000 --- a/Telegram-Mac/ChatReplyPreviewController.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// ChatReplyPreviewController.swift -// Telegram -// -// Created by keepcoder on 04/09/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - -import Cocoa -import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac - -class ChatReplyPreviewView : View { - var container: ChatRowView? - - required init(frame frameRect: NSRect) { - super.init(frame: frameRect) - } - - func update(_ message: Message, account: Account, chatInteraction: ChatInteraction) { - let item = ChatRowItem.item(frame.size, from: .MessageEntry(message, true, .Full(isAdmin: false), .FullHeader, nil), with: account, interaction: chatInteraction) - _ = item.makeSize(frame.width, oldWidth: 0) - - container?.removeFromSuperview() - let vz = item.viewClass() as! TableRowView.Type - - container = vz.init(frame:NSMakeRect(0, 0, NSWidth(self.frame), item.height)) as? ChatRowView - - container?.identifier = identifier - addSubview(container!) - - container!.setFrameSize(NSMakeSize(frame.width, item.height)) - container!.set(item: item, animated: false) - setFrameSize(NSMakeSize(frame.width, container!.frame.height + 12)) - needsLayout = true - } - - override func layout() { - super.layout() - container?.center() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -class ChatReplyPreviewController: TelegramGenericViewController { - private let messageId: MessageId - private let disposable = MetaDisposable() - private let chatInteraction: ChatInteraction - init(_ account: Account, messageId: MessageId, width: CGFloat) { - self.messageId = messageId - self.chatInteraction = ChatInteraction(peerId: messageId.peerId, account: account, isLogInteraction: true) - super.init(account) - _frameRect = NSMakeRect(0, 0, width, 0) - bar = .init(height: 0) - } - - - - override func viewDidLoad() { - super.viewDidLoad() - - disposable.set((account.postbox.messageView(messageId) |> deliverOnMainQueue).start(next: { [weak self] view in - if let message = view.message, let strongSelf = self { - self?.genericView.update(message, account: strongSelf.account, chatInteraction: strongSelf.chatInteraction) - self?.readyOnce() - } - })) - } - - deinit { - disposable.dispose() - } - -} diff --git a/Telegram-Mac/ChatRightView.swift b/Telegram-Mac/ChatRightView.swift index 351299e3b8..f83758679f 100644 --- a/Telegram-Mac/ChatRightView.swift +++ b/Telegram-Mac/ChatRightView.swift @@ -8,7 +8,9 @@ import Cocoa import TGUIKit -import TelegramCoreMac +import TelegramCore + + class ChatRightView: View { @@ -16,76 +18,79 @@ class ChatRightView: View { private var stateView:ImageView? private var readImageView:ImageView? private var sendingView:SendingClockProgress? - private var channelsViewsImage:ImageView? private weak var item:ChatRowItem? + var isReversed: Bool { + guard let item = item else {return false} + + return item.isBubbled && !item.isIncoming + } + func set(item:ChatRowItem, animated:Bool) { self.item = item self.toolTip = item.fullDate - if let message = item.message { - if !message.flags.contains(.Incoming) && !item.chatInteraction.isLogInteraction { + item.updateTooltip = { [weak self] value in + self?.toolTip = value + } + if !item.isIncoming || item.isUnsent || item.isFailed + && !item.chatInteraction.isLogInteraction { + if item.isUnsent { + stateView?.removeFromSuperview() + stateView = nil + readImageView?.removeFromSuperview() + readImageView = nil + if sendingView == nil { + sendingView = SendingClockProgress() + addSubview(sendingView!) + needsLayout = true + } + } else { - if message.flags.contains(.Unsent) { + sendingView?.removeFromSuperview() + sendingView = nil + + + if let peer = item.peer as? TelegramChannel, peer.isChannel && !item.isFailed { stateView?.removeFromSuperview() stateView = nil readImageView?.removeFromSuperview() readImageView = nil - sendingView?.removeFromSuperview() - sendingView = nil - - if sendingView == nil { - sendingView = SendingClockProgress() - sendingView?.setFrameOrigin(0,2) - addSubview(sendingView!) - } } else { + let stateImage = item.presentation.chat.stateStateIcon(item) - sendingView?.removeFromSuperview() - sendingView = nil - + if stateView == nil { + stateView = ImageView() + self.addSubview(stateView!) + } - if let peer = item.peer as? TelegramChannel, case .broadcast = peer.info { - stateView?.removeFromSuperview() - stateView = nil - readImageView?.removeFromSuperview() - readImageView = nil - } else { - let stateImage = message.flags.contains(.Failed) ? theme.icons.sentFailed : theme.icons.chatReadMark1 - - if stateView == nil { - stateView = ImageView() - self.addSubview(stateView!) - } - - if item.isRead && !message.flags.contains(.Failed) { - if readImageView == nil { - readImageView = ImageView(frame: NSMakeRect(0, 0, theme.icons.chatReadMark2.backingSize.width, theme.icons.chatReadMark2.backingSize.height)) - addSubview(readImageView!) - } - - } else { - readImageView?.removeFromSuperview() - readImageView = nil + if item.isRead && !item.isFailed && !item.hasSource { + if readImageView == nil { + readImageView = ImageView() + addSubview(readImageView!) } - stateView?.image = stateImage - stateView?.setFrameSize(NSMakeSize(stateImage.backingSize.width, stateImage.backingSize.height)) + } else { + readImageView?.removeFromSuperview() + readImageView = nil } - + + stateView?.image = stateImage + stateView?.setFrameSize(NSMakeSize(stateImage.backingSize.width, stateImage.backingSize.height)) } - } else { - stateView?.removeFromSuperview() - stateView = nil - readImageView?.removeFromSuperview() - readImageView = nil - sendingView?.removeFromSuperview() - sendingView = nil + } - readImageView?.image = theme.icons.chatReadMark2 - self.sendingView?.backgroundColor = theme.colors.background + } else { + stateView?.removeFromSuperview() + stateView = nil + readImageView?.removeFromSuperview() + readImageView = nil + sendingView?.removeFromSuperview() + sendingView = nil } - + readImageView?.image = item.presentation.chat.readStateIcon(item) + readImageView?.sizeToFit() + sendingView?.set(item: item) self.needsLayout = true } @@ -93,48 +98,108 @@ class ChatRightView: View { override func layout() { super.layout() - if let item = item, let message = item.message { + if let item = item { var rightInset:CGFloat = 0 if let date = item.date { - rightInset = date.0.size.width + 20 + if !isReversed { + rightInset = date.0.size.width + (item.isBubbled ? 16 : 20) + } } if let stateView = stateView { - stateView.setFrameOrigin(frame.width - rightInset, message.flags.contains(.Failed) ? 0 : 2) + rightInset += (isReversed ? stateView.frame.width : 0) + if isReversed { + rightInset += 3 + } + if item.isFailed { + rightInset -= 2 + } + stateView.setFrameOrigin(frame.width - rightInset - item.stateOverlayAdditionCorner, item.isFailed ? (item.isStateOverlayLayout ? 2 : 1) : (item.isStateOverlayLayout ? 3 : 2)) + } + + if let sendingView = sendingView { + if isReversed { + sendingView.setFrameOrigin(frame.width - sendingView.frame.width - item.stateOverlayAdditionCorner, (item.isStateOverlayLayout ? 2 : 1)) + } else { + sendingView.setFrameOrigin(frame.width - rightInset - item.stateOverlayAdditionCorner, (item.isStateOverlayLayout ? 2 : 1)) + } } + + if let readImageView = readImageView { - readImageView.setFrameOrigin((frame.width - rightInset) + 4, 2) + readImageView.setFrameOrigin((frame.width - rightInset) + 4 - item.stateOverlayAdditionCorner, (item.isStateOverlayLayout ? 3 : 2)) } } self.setNeedsDisplay() } + override func draw(_ layer: CALayer, in ctx: CGContext) { - super.draw(layer, in: ctx) if let item = item { + + if item.isStateOverlayLayout { + ctx.round(frame.size, frame.height/2) + ctx.setFillColor(item.stateOverlayBackgroundColor.cgColor) + ctx.fill(layer.bounds) + } + + // super.draw(layer, in: ctx) + + let additional: CGFloat = 0 + if let date = item.date { - date.1.draw(NSMakeRect(NSWidth(layer.bounds) - date.0.size.width, 0, date.0.size.width, date.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor) + date.1.draw(NSMakeRect(frame.width - date.0.size.width - (isReversed ? 16 : 0) - item.stateOverlayAdditionCorner - additional, item.isBubbled ? (item.isStateOverlayLayout ? 2 : 1) : 0, date.0.size.width, date.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) + + if let editLabel = item.editedLabel { + editLabel.1.draw(NSMakeRect(frame.width - date.0.size.width - editLabel.0.size.width - item.stateOverlayAdditionCorner - (isReversed || (stateView != nil) ? 23 : 5), item.isBubbled ? (item.isStateOverlayLayout ? 2 : 1) : 0, editLabel.0.size.width, editLabel.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) + } } + + var viewsOffset: CGFloat = 0 + + if let likes = item.likes { + viewsOffset += likes.0.size.width + 18 + let icon = item.presentation.chat.likedIcon(item) + ctx.draw(icon, in: NSMakeRect(likes.0.size.width + 2 + item.stateOverlayAdditionCorner, item.isBubbled ? (item.isStateOverlayLayout ? 1 : 0) : 0, icon.backingSize.width, icon.backingSize.height)) + likes.1.draw(NSMakeRect(item.stateOverlayAdditionCorner, item.isBubbled ? (item.isStateOverlayLayout ? 2 : 1) : 0, likes.0.size.width, likes.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) + + } + if item.isPinned { + let icon = item.presentation.chat.messagePinnedIcon(item) + ctx.draw(icon, in: NSMakeRect(viewsOffset + (item.isStateOverlayLayout ? 4 : 0), item.isBubbled ? (item.isStateOverlayLayout ? 3 : 2) : 2, icon.backingSize.width, icon.backingSize.height)) + viewsOffset += icon.backingSize.width + (item.isStateOverlayLayout ? 4 : 4) + } + if let channelViews = item.channelViews { - ctx.draw(theme.icons.chatChannelViews, in: NSMakeRect(channelViews.0.size.width + 2, 0, theme.icons.chatChannelViews.backingSize.width, theme.icons.chatChannelViews.backingSize.height)) - - channelViews.1.draw(NSMakeRect(0, 0, channelViews.0.size.width, channelViews.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor) + let icon = item.presentation.chat.channelViewsIcon(item) + ctx.draw(icon, in: NSMakeRect(channelViews.0.size.width + 2 + item.stateOverlayAdditionCorner + viewsOffset, item.isBubbled ? (item.isStateOverlayLayout ? 1 : 0) : 0, icon.backingSize.width, icon.backingSize.height)) + channelViews.1.draw(NSMakeRect(item.stateOverlayAdditionCorner + viewsOffset, item.isBubbled ? (item.isStateOverlayLayout ? 2 : 1) : 0, channelViews.0.size.width, channelViews.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) if let postAuthor = item.postAuthor { - postAuthor.1.draw(NSMakeRect(theme.icons.chatChannelViews.backingSize.width + channelViews.0.size.width + 8, 0, postAuthor.0.size.width, postAuthor.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor) - } - - } else { - if let editLabel = item.editedLabel { - editLabel.1.draw(NSMakeRect(0, 0, editLabel.0.size.width, editLabel.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor) + postAuthor.1.draw(NSMakeRect(icon.backingSize.width + channelViews.0.size.width + 8 + item.stateOverlayAdditionCorner + viewsOffset, item.isBubbled ? (item.isStateOverlayLayout ? 2 : 1) : 0, postAuthor.0.size.width, postAuthor.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) + viewsOffset += postAuthor.0.size.width + 8 } + viewsOffset += channelViews.0.size.width + 22 + } else if let postAuthor = item.postAuthor { + postAuthor.1.draw(NSMakeRect(item.stateOverlayAdditionCorner + viewsOffset, item.isBubbled ? (item.isStateOverlayLayout ? 2 : 1) : 0, postAuthor.0.size.width, postAuthor.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) + viewsOffset += postAuthor.0.size.width + 8 } + if let replyCount = item.replyCount { + let icon = item.presentation.chat.repliesCountIcon(item) + ctx.draw(icon, in: NSMakeRect(replyCount.0.size.width + 2 + item.stateOverlayAdditionCorner + viewsOffset, item.isBubbled ? (item.isStateOverlayLayout ? 3 : 2) : 2, icon.backingSize.width, icon.backingSize.height)) + replyCount.1.draw(NSMakeRect(item.stateOverlayAdditionCorner + viewsOffset, item.isBubbled ? (item.isStateOverlayLayout ? 2 : 1) : 0, replyCount.0.size.width, replyCount.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) + } + } } + override func mouseUp(with event: NSEvent) { + superview?.mouseUp(with: event) + } + deinit { var bp:Int = 0 diff --git a/Telegram-Mac/ChatRowItem.swift b/Telegram-Mac/ChatRowItem.swift index 14543a3d55..eaf99dcb68 100644 --- a/Telegram-Mac/ChatRowItem.swift +++ b/Telegram-Mac/ChatRowItem.swift @@ -8,10 +8,18 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore +import Postbox +import SwiftSignalKit + + +struct ChatFloatingPhoto { + var point: NSPoint + var items:[ChatRowItem] + weak var photoView: NSView? + +} let simpleDif:Int32 = 10 * 60 let forwardDif:Int32 = 10 * 60 @@ -34,16 +42,6 @@ func makeChatItems(items:[(Int,TableRowItem)], maxHeight:CGFloat? = nil) -> [(In -let userChatColors:[Int:NSColor] = { - var colors:[Int:NSColor] = [:] - colors[0] = NSColor(0xce5247); - colors[1] = NSColor(0xcda322); - colors[2] = NSColor(0x5eaf33); - colors[3] = NSColor(0x468ec4); - colors[4] = NSColor(0xac6bc8); - colors[5] = NSColor(0xe28941); - return colors -}() enum ForwardItemType { @@ -54,53 +52,95 @@ enum ForwardItemType { } enum ChatItemType : Equatable { - case Full(isAdmin: Bool) - case Short + + enum Header { + case normal + case short + } + + case Full(rank: String?, header: Header) + case Short(rank: String?, header: Header) } -func ==(lhs: ChatItemType, rhs: ChatItemType) -> Bool { - switch lhs { - case .Full(let isAdmin): - if case .Full(isAdmin: isAdmin) = rhs { - return true - } else { - return false - } - case .Short: - if case .Short = rhs { - return true - } else { - return false - } - } +enum ChatItemRenderType { + case bubble + case list } + + class ChatRowItem: TableRowItem { + struct RowCaption { + let id: UInt32 + let offset: NSPoint + let layout: TextViewLayout + + func withUpdatedOffset(_ offset: CGFloat) -> RowCaption { + return RowCaption(id: self.id, offset: .init(x: 0, y: offset), layout: self.layout) + } + } + private(set) var chatInteraction:ChatInteraction - var account:Account! + let context: AccountContext private(set) var peer:Peer? private(set) var entry:ChatHistoryEntry private(set) var message:Message? - private(set) var fontSize:Int32 = 13 - private(set) var itemType:ChatItemType = .Full(isAdmin: false) + + private var updateCountDownTimer: SwiftSignalKit.Timer? + var updateTooltip:((String)->Void)? = nil + var firstMessage: Message? { + return messages.first + } + var lastMessage: Message? { + return messages.last + } + + var messages: [Message] { + if let message = message { + return [message] + } + return [] + } + + private(set) var itemType:ChatItemType = .Full(rank: nil, header: .normal) + + var isFullItemType: Bool { + if case .Full = itemType { + return true + } else { + return false + } + } //right view private(set) var date:(TextNodeLayout,TextNode)? + private(set) var likesNode:TextNode? + private(set) var likes:(TextNodeLayout,TextNode)? + private(set) var likesAttributed:NSAttributedString? + private(set) var channelViewsNode:TextNode? private(set) var channelViews:(TextNodeLayout,TextNode)? private(set) var channelViewsAttributed:NSAttributedString? + private(set) var replyCountNode:TextNode? + private(set) var replyCount:(TextNodeLayout,TextNode)? + private(set) var replyCountAttributed:NSAttributedString? + + + private(set) var postAuthorNode:TextNode? private(set) var postAuthor:(TextNodeLayout,TextNode)? private(set) var postAuthorAttributed:NSAttributedString? private(set) var editedLabel:(TextNodeLayout,TextNode)? - var fullDate:String? - + private(set) var fullDate:String? + private(set) var forwardHid: String? + private(set) var nameHide: String? + var forwardType:ForwardItemType? { didSet { @@ -108,10 +148,7 @@ class ChatRowItem: TableRowItem { } var selectableLayout:[TextViewLayout] { - if let caption = captionLayout { - return [caption] - } - return [] + return self.captionLayouts.map { $0.layout } } var sending: Bool { @@ -121,9 +158,10 @@ class ChatRowItem: TableRowItem { private var forwardHeaderNode:TextNode? private(set) var forwardHeader:(TextNodeLayout, TextNode)? var forwardNameLayout:TextViewLayout? - var captionLayout:TextViewLayout? + var captionLayouts:[RowCaption] = [] private(set) var authorText:TextViewLayout? - + private(set) var adminBadge:TextViewLayout? + var replyModel:ReplyModel? var replyMarkupModel:ReplyMarkupNode? @@ -137,10 +175,20 @@ class ChatRowItem: TableRowItem { var topInset:CGFloat { return 2 } - let defaultContentTopOffset:CGFloat = 6 + var defaultContentTopOffset:CGFloat { + if isBubbled { + return 10 + } else { + return 6 + } + } var rightInset:CGFloat { - return chatInteraction.presentation.selectionState != nil ? 42.0 : 20.0 + if isBubbled { + return 15 + } else { + return chatInteraction.presentation.selectionState != nil ? 42.0 : 20.0 + } } let leftInset:CGFloat = 20 @@ -150,9 +198,43 @@ class ChatRowItem: TableRowItem { } var _contentSize:NSSize = NSZeroSize; + var previousBlockWidth:CGFloat = 0; + + var bubbleDefaultInnerInset: CGFloat { + return bubbleContentInset * 2 + additionBubbleInset + } - public var blockSize:NSSize { - return NSMakeSize(width - contentOffset.x - rightSize.width - 44, height) + var blockWidth:CGFloat { + + var widthForContent: CGFloat = 0 + + if isBubbled { + + var tempWidth: CGFloat = width - self.contentOffset.x - bubbleDefaultInnerInset - (20 + 10 + additionBubbleInset) - 20 + + if isSharable || hasSource { + tempWidth -= 35 + } + if isLikable { + tempWidth -= 35 + } + widthForContent = min(tempWidth, 450) + + + } else { + if case .Full = itemType { + let additionWidth:CGFloat = date?.0.size.width ?? 20 + widthForContent = width - self.contentOffset.x - 44 - additionWidth + } else { + widthForContent = width - self.contentOffset.x - rightSize.width - 44 + } + } + + if forwardType != nil { + widthForContent -= leftContentInset + } + + return widthForContent } public var rightSize:NSSize { @@ -160,81 +242,194 @@ class ChatRowItem: TableRowItem { var size:NSSize = NSZeroSize if let date = date { - size = NSMakeSize(date.0.size.width, 16) + size = NSMakeSize(date.0.size.width, isBubbled && !isFailed ? 15 : 16) } - if let message = message { - if let peer = peer as? TelegramChannel, case .broadcast = peer.info { - size.width += 0 - } else { - if !message.flags.contains(.Incoming) { + if let peer = peer as? TelegramChannel, case .broadcast = peer.info, (!isUnsent && !isFailed) { + size.width += 0 + } else { + if (!isIncoming || (isUnsent || isFailed)) && date != nil { + if isBubbled { + size.width += 16 + if isFailed { + size.width += 4 + } + } else { size.width += 20 } } - - - if let channelViews = channelViews { - size.width += channelViews.0.size.width + 8 + 16 - } - if let postAuthor = postAuthor { - size.width += postAuthor.0.size.width + 8 - } - - if let editedLabel = editedLabel { - size.width += editedLabel.0.size.width + 8 + } + + + + if let channelViews = channelViews { + size.width += channelViews.0.size.width + 8 + 16 + } + if let replyCount = replyCount { + size.width += replyCount.0.size.width + 18 + } + if let likes = likes { + size.width += likes.0.size.width + 18 + } + if isPinned { + if self.isStateOverlayLayout { + size.width += 14 + } else { + size.width += 18 } } - size.width = max(50,size.width) + if let postAuthor = postAuthor { + size.width += postAuthor.0.size.width + 8 + } + + if let editedLabel = editedLabel { + size.width += editedLabel.0.size.width + 7 + } + + size.width = max(isBubbled ? size.width : 54, size.width) + size.width += stateOverlayAdditionCorner * 2 + size.height = isStateOverlayLayout ? 17 : size.height return size } - public var contentSize:NSSize { + var stateOverlayAdditionCorner: CGFloat { + return isStateOverlayLayout ? 5 : 0 + } + + var contentSize:NSSize { + return _contentSize + } + + var realContentSize: NSSize { return _contentSize } + var isSticker: Bool { + let file = message?.media.first as? TelegramMediaFile + return file?.isStaticSticker == true || file?.isAnimatedSticker == true + } + override var height: CGFloat { var height:CGFloat = self.contentSize.height + _defaultHeight - if let captionLayout = captionLayout { - height += captionLayout.layoutSize.height + defaultContentTopOffset + + if !isBubbled, case .Full = self.itemType, self is ChatMessageItem { + height += 2 + } + + if !captionLayouts.isEmpty { + let captionHeight: CGFloat = captionLayouts.reduce(0, { $0 + $1.layout.layoutSize.height }) + defaultContentInnerInset * CGFloat(captionLayouts.count) + if let item = self as? ChatGroupedItem { + switch item.layoutType { + case .photoOrVideo: + height += captionHeight + case .files: + break + } + } else { + height += captionHeight + } } if let replyMarkupModel = replyMarkupModel { - height += replyMarkupModel.size.height + defaultContentTopOffset + height += replyMarkupModel.size.height + defaultReplyMarkupInset + } + + if isBubbled { + if let additional = additionalLineForDateInBubbleState { + height += additional + } + + if replyModel?.isSideAccessory == true { + height = max(48, height) + } + + if let _ = commentsBubbleData, hasBubble { + height += ChatRowItem.channelCommentsBubbleHeight + } } + return max(rightSize.height + 8, height) } + var defaultReplyMarkupInset: CGFloat { + return (isBubbled ? 4 : defaultContentInnerInset) + } + + var defaultContentInnerInset: CGFloat { + return 6 + } + + var elementsContentInset: CGFloat { + return 0 + } + var replyOffset:CGFloat { var top:CGFloat = defaultContentTopOffset - + if isBubbled && authorText != nil { + top -= topInset + } if let author = authorText { - top += author.layoutSize.height + defaultContentTopOffset + top += author.layoutSize.height + defaultContentInnerInset } return top } + var isBubbleFullFilled: Bool { + return false + } + + var isStateOverlayLayout: Bool { + if let message = message, let media = message.media.first { + if let file = media as? TelegramMediaFile { + if file.isStaticSticker || file.isAnimatedSticker { + return isBubbled + } + } + if media is TelegramMediaDice { + return isBubbled + } + if let media = media as? TelegramMediaMap { + if let liveBroadcastingTimeout = media.liveBroadcastingTimeout { + var time:TimeInterval = Date().timeIntervalSince1970 + time -= context.timeDifference + if Int32(time) < message.timestamp + liveBroadcastingTimeout { + return false + } + } + return media.venue == nil + } + return isBubbled && media.isInteractiveMedia && captionLayouts.isEmpty + } + return false + } + + private(set) var isForceRightLine: Bool = false + var forwardHeaderInset:NSPoint { - var top:CGFloat = 0 + var top:CGFloat = defaultContentTopOffset + + if !isBubbled, forwardHeader == nil { + top -= topInset + } if let author = authorText { - top += author.layoutSize.height + 7 + top += author.layoutSize.height } return NSMakePoint(defLeftInset, top) } var forwardNameInset:NSPoint { + var top:CGFloat = forwardHeaderInset.y - var top:CGFloat = forwardHeaderInset.y + 4 - - if let header = forwardHeader { - top += header.0.size.height + 4 + if let header = forwardHeader, !isBubbled { + top += header.0.size.height + defaultContentInnerInset } return NSMakePoint(self.contentOffset.x, top) @@ -245,7 +440,61 @@ class ChatRowItem: TableRowItem { } var defLeftInset:CGFloat { - return leftInset + 36 + 10 + var inset: CGFloat = leftInset + if isBubbled { + if hasPhoto { + inset += 36 + 6 + } else if self.isIncoming, let message = message { + if let peer = message.peers[message.id.peerId] { + if peer.isGroup || peer.isSupergroup { + inset += 36 + 6 + } + } + } + } else { + inset += 36 + 10 + } + + return inset + } + + var hasPhoto: Bool { + if !isBubbled { + if case .Full = itemType { + return true + } else { + return false + } + } else { + if case .Full = itemType, let message = message, let peer = message.peers[message.id.peerId] { + switch chatInteraction.chatLocation { + case .peer, .replyThread: + if chatInteraction.mode.threadId == effectiveCommentMessage?.id { + return false + } + if (isIncoming && message.id.peerId == context.peerId) { + return true + } + if message.id.peerId == repliesPeerId && message.author?.id != context.peerId { + return true + } + if !peer.isUser && !peer.isSecretChat && !peer.isChannel && isIncoming { + return true + } + } + } + } + if chatInteraction.isGlobalSearchMessage { + return true + } + return false + } + + var isInstantVideo: Bool { + if let media = message?.media.first as? TelegramMediaFile { + return media.isInstantVideo + } + return false } var contentOffset:NSPoint { @@ -254,210 +503,1184 @@ class ChatRowItem: TableRowItem { var top:CGFloat = defaultContentTopOffset + if let author = authorText { - top += author.layoutSize.height + topInset + top += author.layoutSize.height + if !isBubbled { + top += topInset + } } if let replyModel = replyModel { - top += max(34, replyModel.size.height) + 8 + var apply: Bool = true + if isBubbled { + if !hasBubble { + apply = false + } + } + if apply { + top += max(34, replyModel.size.height) + ((!isBubbleFullFilled && isBubbled && self is ChatMediaItem) ? 0 : 8) + if (authorText != nil) && self is ChatMessageItem { + top += topInset + //top -= defaultContentInnerInset + } else if hasBubble && self is ChatMessageItem { + top -= topInset + } + } } - if let forwardNameLayout = forwardNameLayout { - top += forwardNameLayout.layoutSize.height + topInset + if let forwardNameLayout = forwardNameLayout, !isBubbled || !isInstantVideo { + top += forwardNameLayout.layoutSize.height + //if !isBubbled { + top += 2 + //} } - if let forwardType = forwardType { + if let forwardType = forwardType, !isBubbled { if forwardType == .FullHeader || forwardType == .ShortHeader { if let forwardHeader = forwardHeader { - top += forwardHeader.0.size.height + 6 - } - } else { - if self is ChatMessageItem { - top -= topInset + top += forwardHeader.0.size.height + defaultContentInnerInset + } else { + top += bubbleDefaultInnerInset } } - + } + + if isBubbled, self is ChatMessageItem { + top -= 1 } if forwardNameLayout != nil { - left += 10 + left += leftContentInset } - if isGame { - left += 10 + if let item = self as? ChatMessageItem, item.containsBigEmoji { + if commentsBubbleDataOverlay != nil || isSharable || hasSource { + top += 20 + } } return NSMakePoint(left, top) } + var leftContentInset: CGFloat { + return 10 + } + private(set) var isRead:Bool = false - private(set) var isGame:Bool = false override var stableId: AnyHashable { return entry.stableId } - var isSharable: Bool { - var peers:[Peer] = [] - if let peer = peer { - peers.append(peer) - } - if let info = message?.forwardInfo { - peers.append(info.author) - - if let peer = info.source { - peers.append(peer) - } - } - - for peer in peers { - if let peer = peer as? TelegramChannel { - switch peer.info { - case .broadcast: - return !chatInteraction.isLogInteraction - default: - break - } - } - if let peer = peer as? TelegramUser { - if peer.botInfo != nil { - return self is ChatMediaItem && !chatInteraction.isLogInteraction + var hasSource: Bool { + switch chatInteraction.mode { + case .pinned: + return true + default: + if let message = message { + for attr in message.attributes { + if let attr = attr as? SourceReferenceMessageAttribute { + if authorIsChannel { + return true + } + return (chatInteraction.peerId == context.peerId && context.peerId != attr.messageId.peerId) || message.id.peerId == repliesPeerId + } } } } - return false } - var isEditMarkVisible: Bool { - var peers:[Peer] = [] - if let peer = peer { - peers.append(peer) + var isSelectedMessage: Bool { + if let message = message { + return chatInteraction.presentation.isSelectedMessageId(message.id) } - if let info = message?.forwardInfo { - peers.append(info.author) + return false + } + + override var isSelectable: Bool { + switch chatInteraction.mode { + case .preview: + return false + default: + return chatInteraction.mode.threadId != effectiveCommentMessage?.id } - - for peer in peers { - if let peer = peer as? TelegramChannel { - switch peer.info { - case .broadcast: - return false - default: - break - } - } - if let peer = peer as? TelegramUser { - if peer.botInfo != nil { - return false - } - } + } + + var disableInteractions: Bool { + switch chatInteraction.mode { + case .preview: + return true + default: + return false } - + } + + + func openReplyMessage() { if let message = message { - for attr in message.attributes { - if attr is InlineBotMessageAttribute { - return false - } - } - for media in message.media { - if media is TelegramMediaMap { - return false + if let replyAttribute = message.replyAttribute { + if message.id.peerId == repliesPeerId, let threadMessageId = message.replyAttribute?.threadMessageId { + chatInteraction.openReplyThread(threadMessageId, false, true, .comments(origin: replyAttribute.messageId)) + } else { + chatInteraction.focusMessageId(message.id, replyAttribute.messageId, .CenterEmpty) } } } - return !chatInteraction.isLogInteraction } - init(_ initialSize:NSSize, _ chatInteraction:ChatInteraction, _ account:Account, _ object: ChatHistoryEntry) { - self.entry = object - self.account = account - self.chatInteraction = chatInteraction - - if case let .MessageEntry(message,isRead,itemType, fwdType, _) = object { - + func gotoSourceMessage() { + if let message = message { + switch chatInteraction.mode { + case .pinned: + let navigation = chatInteraction.context.sharedContext.bindings.rootNavigation() + let controller = navigation.previousController as? ChatController + controller?.chatInteraction.focusPinnedMessageId(message.id) + navigation.back() + default: + for attr in message.attributes { + if let attr = attr as? SourceReferenceMessageAttribute { + if message.id.peerId == repliesPeerId, let threadMessageId = message.replyAttribute?.threadMessageId { + chatInteraction.openReplyThread(threadMessageId, false, true, .comments(origin: attr.messageId)) + } else { + switch chatInteraction.mode { + case .replyThread: + chatInteraction.focusMessageId(nil, attr.messageId, .CenterEmpty) + default: + chatInteraction.openInfo(attr.messageId.peerId, true, attr.messageId, nil) + } + } + } + } + } + + } + } + + var isVideoOrBigEmoji: Bool { + return self is ChatVideoMessageItem || (message != nil && bigEmojiMessage(context.sharedContext, message: message!)) + } + + func share() { + if let message = message { + showModal(with: ShareModalController(ShareMessageObject(context, message)), for: mainWindow) + } + } + + var authorIsChannel: Bool { + guard let message = message else { + return false + } + return ChatRowItem.authorIsChannel(message: message, account: context.account) + } + + private static func authorIsChannel(message: Message, account: Account) -> Bool { + + let isCrosspostFromChannel = message.isCrosspostFromChannel(account: account) + + var sourceReference: SourceReferenceMessageAttribute? + for attribute in message.attributes { + if let attribute = attribute as? SourceReferenceMessageAttribute { + sourceReference = attribute + break + } + } + + var authorIsChannel: Bool = false + if let peer = message.peers[message.id.peerId] as? TelegramChannel { + if case .broadcast = peer.info { + + } else { + if isCrosspostFromChannel, let sourceReference = sourceReference, let _ = message.peers[sourceReference.messageId.peerId] as? TelegramChannel { + authorIsChannel = true + } + } + } else { + if isCrosspostFromChannel, let _ = message.forwardInfo?.source as? TelegramChannel { + authorIsChannel = true + } + } + + + return authorIsChannel + } + + var isLikable: Bool { + return false + } + + var isLiked: Bool { + return false + } + + func toggleLike() { + + } + + override func copyAndUpdate(animated: Bool) { + if let table = self.table { + let item = ChatRowItem.item(table.frame.size, from: self.entry, interaction: self.chatInteraction, downloadSettings: self.downloadSettings, theme: self.presentation) + _ = item.makeSize(table.frame.width, oldWidth: 0) + let transaction = TableUpdateTransition(deleted: [], inserted: [], updated: [(self.index, item)], animated: animated) + table.merge(with: transaction) + } + } + + var shareVisible: Bool { + + guard let message = message else { + return false + } + + + + if isSharable { + if message.isScheduledMessage || message.flags.contains(.Sending) || message.flags.contains(.Failed) || message.flags.contains(.Unsent) { + return false + } else { + return true + } + } + return false + } + + var isSharable: Bool { + var peers:[Peer] = [] + if let peer = peer { + peers.append(peer) + } + + guard let message = message else { + return false + } + if message.adAttribute != nil { + return false + } + + if authorIsChannel { + return false + } + + + if let info = message.forwardInfo { + if let author = info.author { + peers.append(author) + } + + if let peer = info.source { + peers.append(peer) + } + } + + for peer in peers { + if let peer = peer as? TelegramChannel { + switch peer.info { + case .broadcast: + return !chatInteraction.isLogInteraction + default: + break + } + } + if let peer = peer as? TelegramUser { + if peer.botInfo != nil { + if self is ChatMediaItem && !chatInteraction.isLogInteraction { + return true + } else if let item = self as? ChatMessageItem { + return item.webpageLayout != nil + } + return false + } + } + } + + + return false + } + + let isScam: Bool + let isFake: Bool + private(set) var isForwardScam: Bool + private(set) var isForwardFake: Bool + + var isFailed: Bool { + for message in messages { + if message.flags.contains(.Failed) { + return true + } + } + return false + } + + var isPinned: Bool { + for message in messages { + if message.tags.contains(.pinned) { + return true + } + } + return false + } + + let isIncoming: Bool + + var canHasFloatingPhoto: Bool { + if chatInteraction.mode.isThreadMode, chatInteraction.mode.threadId == message?.id { + return false + } else { + return isIncoming + } + } + + var isUnsent: Bool { + if entry.additionalData.updatingMedia != nil { + return true + } + if let message = message { + return message.flags.contains(.Unsent) + } + return false + } + + var isEditMarkVisible: Bool { + var peers:[Peer] = [] + if let peer = peer { + peers.append(peer) + } + if let info = message?.forwardInfo?.author { + peers.append(info) + } + + for peer in peers { + if let peer = peer as? TelegramUser { + if peer.botInfo != nil { + return false + } + } + } + + for message in messages { + if message.isScheduledMessage { + return false + } + for attr in message.attributes { + if attr is InlineBotMessageAttribute { + return false + } + } + for media in message.media { + if media is TelegramMediaMap { + return false + } + } + if message.isImported { + return true + } + } + + return !chatInteraction.isLogInteraction && message?.id.peerId != context.peerId + } + + private static func canFillAuthorName(_ message: Message, chatInteraction: ChatInteraction, renderType: ChatItemRenderType, isIncoming: Bool, hasBubble: Bool) -> Bool { + var canFillAuthorName: Bool = true + switch chatInteraction.chatLocation { + case .peer, .replyThread: + if renderType == .bubble, let peer = messageMainPeer(message) { + canFillAuthorName = isIncoming && (peer.isGroup || peer.isSupergroup || message.id.peerId == chatInteraction.context.peerId || message.id.peerId == repliesPeerId || message.adAttribute != nil) + if let media = message.media.first { + canFillAuthorName = canFillAuthorName && !media.isInteractiveMedia && hasBubble && isIncoming + } else if bigEmojiMessage(chatInteraction.context.sharedContext, message: message) { + canFillAuthorName = false + } + if message.isAnonymousMessage, !isIncoming { + var disable: Bool = false + if let media = message.media.first as? TelegramMediaFile { + if media.isSticker || media.isAnimatedSticker { + disable = true + } + } + if !disable { + canFillAuthorName = true + } + } + } + } + return canFillAuthorName + } + + var canFillAuthorName: Bool { + if let message = message { + return ChatRowItem.canFillAuthorName(message, chatInteraction: chatInteraction, renderType: renderType, isIncoming: isIncoming, hasBubble: hasBubble) + } + return true + } + + var isBubbled: Bool { + return renderType == .bubble + } + + var psaButton: NSAttributedString? { + if let info = message?.forwardInfo?.psaType { + let text = localizedPsa("psa.text", type: info) + + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: .normal(.text), textColor: .white), bold: MarkdownAttributeSet(font: .bold(.text), textColor: .white), link: MarkdownAttributeSet(font: .normal(.text), textColor: .link), linkAttribute: { contents in + return (NSAttributedString.Key.link.rawValue, inAppLink.callback(contents, { url in + execute(inapp: .external(link: url, false)) + })) + })) + return attributedText + } + return nil + } + + var isPsa: Bool { + return message?.forwardInfo?.psaType != nil + } + + var hasHeader: Bool { + return !(hasBubble && authorText == nil && replyModel == nil && forwardNameLayout == nil) + } + + var hasBubble: Bool + + static func hasBubble(_ message: Message?, entry: ChatHistoryEntry, type: ChatItemType, sharedContext: SharedAccountContext) -> Bool { + if let message = message, let media = message.media.first { + + if let file = media as? TelegramMediaFile { + if file.isStaticSticker { + return false + } + if file.isAnimatedSticker { + return false + } + if file.isInstantVideo { + return false //!message.text.isEmpty || (message.replyAttribute != nil && !file.isInstantVideo) || (message.forwardInfo != nil && !file.isInstantVideo) + } + } + if media is TelegramMediaDice { + return false + } + + for attr in message.attributes { + if let _ = attr as? InlineBotMessageAttribute { + return true + } + } + + var peer: Peer? + for attr in message.attributes { + if let _ = attr as? SourceReferenceMessageAttribute { + if let info = message.forwardInfo { + peer = info.author + } + break + } + } + + if let _peer = messageMainPeer(message) as? TelegramChannel, case let .broadcast(info) = _peer.info { + if info.flags.contains(.hasDiscussionGroup) { + return true + } + peer = _peer + } else if let author = message.effectiveAuthor, peer == nil { + if author is TelegramSecretChat { + peer = messageMainPeer(message) + } else { + peer = author + } + } + + if message.groupInfo != nil { + switch entry { + case .groupedPhotos(let entries, _): + let prettyCount = entries.filter { $0.message?.media.first?.isInteractiveMedia ?? false }.count + return !message.text.isEmpty || message.replyAttribute != nil || message.forwardInfo != nil || entries.count == 1 || prettyCount != entries.count + default: + return true + } + } + + } else if let message = message { + return !bigEmojiMessage(sharedContext, message: message) + } + return true + } + + let renderType: ChatItemRenderType + var bubbleImage:(CGImage, NSEdgeInsets)? = nil + var bubbleBorderImage:(CGImage, NSEdgeInsets)? = nil + + let downloadSettings: AutomaticMediaDownloadSettings + + let presentation: TelegramPresentationTheme + + + + private var _approximateSynchronousValue: Bool = false + var approximateSynchronousValue: Bool { + get { + let result = _approximateSynchronousValue + _approximateSynchronousValue = false + return result + } + } + + private var _avatarSynchronousValue: Bool = false + var avatarSynchronousValue: Bool { + get { + let result = _avatarSynchronousValue + _avatarSynchronousValue = false + return result + } + } + + static var channelCommentsBubbleHeight: CGFloat { + return 42 + } + static var channelCommentsHeight: CGFloat { + return 16 + } + + var effectiveCommentMessage: Message? { + switch entry { + case let .MessageEntry(message, _, _, _, _, _, _): + return message + case let .groupedPhotos(entries, groupInfo: _): + return entries.first?.message + default: + return nil + } + } + + var channelHasCommentButton: Bool { + if chatInteraction.mode == .scheduled || chatInteraction.isLogInteraction { + return false + } + if let message = effectiveCommentMessage, let peer = message.peers[message.id.peerId] as? TelegramChannel { + switch peer.info { + case let .broadcast(info): + if info.flags.contains(.hasDiscussionGroup) { + if message.flags.contains(.Sending) || message.flags.contains(.Failed) || message.flags.contains(.Unsent) { + switch chatInteraction.presentation.discussionGroupId { + case .unknown: + return false + case .known: + return true + } + } + for attr in message.attributes { + if let attr = attr as? ReplyThreadMessageAttribute { + switch chatInteraction.presentation.discussionGroupId { + case .unknown: + return true + case let .known(peerId): + return attr.commentsPeerId == peerId + } + } + } + + } + default: + break + } + } + + return false + } + + private var _commentsBubbleData: ChannelCommentsRenderData? + private var _commentsBubbleDataOverlay: ChannelCommentsRenderData? + + var commentsBubbleDataOverlay: ChannelCommentsRenderData? { + if let commentsBubbleDataOverlay = _commentsBubbleDataOverlay { + return commentsBubbleDataOverlay + } + + if chatInteraction.isLogInteraction { + return nil + } + + if !isStateOverlayLayout || hasBubble || !channelHasCommentButton { + return nil + } + if let message = effectiveCommentMessage, let peer = message.peers[message.id.peerId] as? TelegramChannel { + switch peer.info { + case let .broadcast(info): + if info.flags.contains(.hasDiscussionGroup) { + var count: Int32 = 0 + var hasUnread: Bool = false + for attr in message.attributes { + if let attribute = attr as? ReplyThreadMessageAttribute { + count = attribute.count + if let maxMessageId = attribute.maxMessageId, let maxReadMessageId = attribute.maxReadMessageId { + hasUnread = maxReadMessageId < maxMessageId + } + break + } + } + let title: String = "\(Int(count).prettyRounded)" + let textColor = isBubbled && presentation.backgroundMode.hasWallpaper ? presentation.chatServiceItemTextColor : presentation.colors.accent + + var texts:[ChannelCommentsRenderData.Text] = [] + if count > 0 { + texts.append(ChannelCommentsRenderData.Text.init(text: .initialize(string: title, color: textColor, font: .normal(.short)), animation: .numeric, index: 0)) + } + + _commentsBubbleDataOverlay = ChannelCommentsRenderData(context: chatInteraction.context, message: message, hasUnread: hasUnread, title: texts, peers: [], drawBorder: true, isLoading: entry.additionalData.isThreadLoading, handler: { [weak self] in + self?.chatInteraction.openReplyThread(message.id, true, false, .comments(origin: message.id)) + }) + } + default: + break + } + } + return _commentsBubbleDataOverlay + } + + var commentsBubbleData: ChannelCommentsRenderData? { + if let commentsBubbleData = _commentsBubbleData { + return commentsBubbleData + } + if chatInteraction.isLogInteraction { + return nil + } + if !isBubbled || !channelHasCommentButton { + return nil + } + if isStateOverlayLayout, let media = effectiveCommentMessage?.media.first, !media.isInteractiveMedia { + return nil + } else if (self is ChatVideoMessageItem) { + return nil + } + if let message = effectiveCommentMessage, let peer = message.peers[message.id.peerId] as? TelegramChannel { + + if let messageItem = self as? ChatMessageItem, messageItem.containsBigEmoji { + return nil + } + + switch peer.info { + case let .broadcast(info): + if info.flags.contains(.hasDiscussionGroup) { + + var latestPeers:[Peer] = [] + var count: Int32 = 0 + var hasUnread = false + for attr in message.attributes { + if let attribute = attr as? ReplyThreadMessageAttribute { + count = attribute.count + if let maxMessageId = attribute.maxMessageId { + if let maxReadMessageId = attribute.maxReadMessageId { + hasUnread = maxReadMessageId < maxMessageId + } else { + hasUnread = false + } + } + latestPeers = message.peers.filter { peerId, _ -> Bool in + return attribute.latestUsers.contains(peerId) + }.map { $0.1 } + break + } + } + + var title: [(String, ChannelCommentsRenderData.Text.Animation, Int)] = [] + if count == 0 { + title = [(L10n.channelCommentsLeaveComment, .crossFade, 0)] + } else { + var text = L10n.channelCommentsCountCountable(Int(count)) + let pretty = "\(Int(count).formattedWithSeparator)" + text = text.replacingOccurrences(of: "\(count)", with: pretty) + + let range = text.nsstring.range(of: pretty) + if range.location != NSNotFound { + title.append((text.nsstring.substring(to: range.location), .crossFade, 0)) + var index: Int = 0 + for _ in range.lowerBound ..< range.upperBound { + let symbol = text.nsstring.substring(with: NSMakeRange(range.location + index, 1)) + title.append((symbol, .numeric, index + 1)) + index += 1 + } + title.append((text.nsstring.substring(from: range.upperBound), .crossFade, range.length + 1)) + } else { + title.append((text, .crossFade, 0)) + } + } + + title = title.filter { !$0.0.isEmpty } + + let texts:[ChannelCommentsRenderData.Text] = title.map { + return ChannelCommentsRenderData.Text(text: .initialize(string: $0.0, color: presentation.colors.accentIcon, font: .normal(.title)), animation: $0.1, index: $0.2) + } + + _commentsBubbleData = ChannelCommentsRenderData(context: chatInteraction.context, message: message, hasUnread: hasUnread, title: texts, peers: latestPeers, drawBorder: !isBubbleFullFilled || !captionLayouts.isEmpty, isLoading: entry.additionalData.isThreadLoading, handler: { [weak self] in + self?.chatInteraction.openReplyThread(message.id, true, false, .comments(origin: message.id)) + }) + } + default: + break + } + } + return _commentsBubbleData + } + + private var _commentsData: ChannelCommentsRenderData? + var commentsData: ChannelCommentsRenderData? { + if let commentsData = _commentsData { + return commentsData + } + if chatInteraction.isLogInteraction { + return nil + } + if isBubbled || !channelHasCommentButton { + return nil + } + if let message = effectiveCommentMessage, let peer = message.peers[message.id.peerId] as? TelegramChannel { + switch peer.info { + case let .broadcast(info): + if info.flags.contains(.hasDiscussionGroup) { + var count: Int32 = 0 + var hasUnread: Bool = false + for attr in message.attributes { + if let attribute = attr as? ReplyThreadMessageAttribute { + count = attribute.count + if let maxMessageId = attribute.maxMessageId, let maxReadMessageId = attribute.maxReadMessageId { + hasUnread = maxReadMessageId < maxMessageId + } + break + } + } + var title: [(String, ChannelCommentsRenderData.Text.Animation, Int)] = [] + if count == 0 { + title = [(L10n.channelCommentsShortLeaveComment, .crossFade, 0)] + } else { + var text = L10n.channelCommentsShortCountCountable(Int(count)) + let pretty = "\(Int(count).prettyRounded)" + text = text.replacingOccurrences(of: "\(count)", with: pretty) + + let range = text.nsstring.range(of: pretty) + if range.location != NSNotFound { + title.append((text.nsstring.substring(to: range.location), .crossFade, 0)) + title.append((text.nsstring.substring(with: range), .numeric, 1)) + title.append((text.nsstring.substring(from: range.upperBound), .crossFade, 2)) + } else { + title.append((text, .crossFade, 0)) + } + } + + title = title.filter { !$0.0.isEmpty } + + let texts:[ChannelCommentsRenderData.Text] = title.map { + return ChannelCommentsRenderData.Text(text: .initialize(string: $0.0, color: presentation.colors.accent, font: .normal(.short)), animation: $0.1, index: $0.2) + } + + _commentsData = ChannelCommentsRenderData(context: chatInteraction.context, message: message, hasUnread: hasUnread, title: texts, peers: [], drawBorder: false, isLoading: entry.additionalData.isThreadLoading, handler: { [weak self] in + self?.chatInteraction.openReplyThread(message.id, true, false, .comments(origin: message.id)) + }) + } + default: + break + } + } + return _commentsData + } + + var forceBackgroundColor: NSColor? = nil + + init(_ initialSize:NSSize, _ chatInteraction:ChatInteraction, _ context: AccountContext, _ object: ChatHistoryEntry, _ downloadSettings: AutomaticMediaDownloadSettings, theme: TelegramPresentationTheme) { + self.entry = object + self.context = chatInteraction.context + self.presentation = theme + self.chatInteraction = chatInteraction + self.downloadSettings = downloadSettings + self._approximateSynchronousValue = Thread.isMainThread + self._avatarSynchronousValue = Thread.isMainThread + var message: Message? + var isRead: Bool = true + var itemType: ChatItemType = .Full(rank: nil, header: .normal) + var fwdType: ForwardItemType? = nil + var renderType:ChatItemRenderType = .list + var object = object + + var hiddenFwdTooltip:(()->Void)? = nil + + var captionMessage: Message? = object.message + + var hasGroupCaption: Bool = object.message?.text.isEmpty == false + if case let .groupedPhotos(entries, _) = object { + object = entries.filter({!$0.message!.media.isEmpty}).last! + + loop: for entry in entries { + if let _ = captionMessage, !entry.message!.text.isEmpty { + captionMessage = nil + hasGroupCaption = false + break loop + } + if !entry.message!.text.isEmpty { + captionMessage = entry.message! + hasGroupCaption = true + } + } + if captionMessage == nil { + captionMessage = object.message! + } + } + + if case let .MessageEntry(_message, _, _isRead, _renderType, _itemType, _fwdType, _) = object { + message = _message + isRead = _isRead + itemType = _itemType + switch _itemType { + case .Full: + fwdType = .FullHeader + default: + fwdType = _fwdType + } + renderType = _renderType + } + + var stateOverlayTextColor: NSColor { + if let media = message?.media.first, media.isInteractiveMedia || media is TelegramMediaMap { + return NSColor(0xffffff) + } else { + return theme.chatServiceItemTextColor + } + } + + var isStateOverlayLayout: Bool { + if renderType == .bubble, let message = captionMessage, let media = message.media.first { + if let file = media as? TelegramMediaFile { + if file.isStaticSticker || file.isAnimatedSticker { + return renderType == .bubble + } + if file.isInstantVideo { + return renderType == .bubble + } + + } + if media is TelegramMediaDice { + return renderType == .bubble + } + if let media = media as? TelegramMediaMap { + if let liveBroadcastingTimeout = media.liveBroadcastingTimeout { + var time:TimeInterval = Date().timeIntervalSince1970 + time -= context.timeDifference + if Int32(time) < message.timestamp + liveBroadcastingTimeout { + return false + } + } + return media.venue == nil + } + return media.isInteractiveMedia && !hasGroupCaption + } else if let message = message, bigEmojiMessage(context.sharedContext, message: message), renderType == .bubble { + return true + } + return false + } + + if message?.id.peerId == context.peerId { + itemType = .Full(rank: nil, header: .normal) + } + self.renderType = renderType + self.message = message + + var isForwardScam: Bool = false + var isScam = false + var isForwardFake: Bool = false + var isFake = false + if let message = message, let peer = messageMainPeer(message) { + if peer.isGroup || peer.isSupergroup { + if let author = message.forwardInfo?.author { + isForwardScam = author.isScam + } + if let author = message.author, case .Full = itemType { + isScam = author.isScam + } + if let author = message.forwardInfo?.author { + isForwardFake = author.isFake + } + if let author = message.author, case .Full = itemType { + isFake = author.isFake + } + } + } + self.isScam = isScam + self.isForwardScam = isForwardScam + + self.isFake = isFake + self.isForwardFake = isForwardFake + + if let message = message { + let isBubbled = renderType == .bubble + let hasBubble = ChatRowItem.hasBubble(captionMessage ?? message, entry: entry, type: itemType, sharedContext: context.sharedContext) + self.hasBubble = isBubbled && hasBubble + + let isIncoming: Bool = message.isIncoming(context.account, renderType == .bubble) + self.isIncoming = isIncoming + + + if case .bubble = renderType , hasBubble{ + let isFull: Bool + if case .Full = itemType { + switch entry { + case let .MessageEntry(message, _, _, _, _, _, _): + isFull = chatInteraction.mode.threadId != message.id + case let .groupedPhotos(entries, groupInfo: _): + isFull = chatInteraction.mode.threadId != entries.first?.message?.id + default: + isFull = true + } + } else { + isFull = false + } + let icons = presentation.icons + let neighbors: MessageBubbleImageNeighbors = isFull && !message.isHasInlineKeyboard ? .none : .both + bubbleImage = isIncoming ? (neighbors == .none ? icons.chatBubble_none_incoming_withInset : icons.chatBubble_both_incoming_withInset) : (neighbors == .none ? icons.chatBubble_none_outgoing_withInset : icons.chatBubble_both_outgoing_withInset) + if !isIncoming && theme.colors.bubbleBackground_outgoing.count > 1 { + bubbleBorderImage = nil + } else { + bubbleBorderImage = isIncoming ? (neighbors == .none ? icons.chatBubbleBorder_none_incoming_withInset : icons.chatBubbleBorder_both_incoming_withInset) : (neighbors == .none ? icons.chatBubbleBorder_none_outgoing_withInset : icons.chatBubbleBorder_both_outgoing_withInset) + } + } + self.itemType = itemType - self.message = message self.isRead = isRead - self.isGame = message.media.first is TelegramMediaGame - if let peer = messageMainPeer(message) as? TelegramChannel, case .broadcast(_) = peer.info { - self.peer = peer - - if let author = message.author, author.id != peer.id, !message.flags.contains(.Unsent), !message.flags.contains(.Failed) { - postAuthorAttributed = .initialize(string: author.displayTitle, color: theme.colors.grayText, font: NSFont.normal(.short)) + + if let info = message.forwardInfo, message.isImported { + if let author = info.author { + self.peer = author + } else if let signature = info.authorSignature { + + self.peer = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(0)), accessHash: nil, firstName: signature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + } else { + self.peer = message.chatPeer(context.peerId) } - - } else if let author = message.author { - if author is TelegramSecretChat { - peer = messageMainPeer(message) + } else if let info = message.forwardInfo, chatInteraction.peerId == context.account.peerId || (object.renderType == .list && info.psaType != nil) { + if info.author == nil, let signature = info.authorSignature { + self.peer = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(0)), accessHash: nil, firstName: signature, lastName: nil, username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + } else if (object.renderType == .list && info.psaType != nil) { + self.peer = info.author ?? message.chatPeer(context.peerId) } else { - peer = author + self.peer = message.chatPeer(context.peerId) + } + } else { + self.peer = message.chatPeer(context.peerId) + } + + var isHasSource: Bool = false + + for attr in message.attributes { + if let _ = attr as? SourceReferenceMessageAttribute { + isHasSource = true + break + } + } + + if let peer = peer, peer.isChannel { + for attr in message.attributes { + if let attr = attr as? AuthorSignatureMessageAttribute { + if !message.flags.contains(.Failed) { + postAuthorAttributed = .initialize(string: attr.signature, color: isStateOverlayLayout ? stateOverlayTextColor : !hasBubble ? presentation.colors.grayText : presentation.chat.grayText(isIncoming, object.renderType == .bubble), font: renderType == .bubble ? .italic(.small) : .normal(.short)) + } + break + } } +// if let _ = message.adAttribute { +// //TODOLANG +// postAuthorAttributed = .initialize(string: "sponsored", color: isStateOverlayLayout ? stateOverlayTextColor : !hasBubble ? presentation.colors.grayText : presentation.chat.grayText(isIncoming, object.renderType == .bubble), font: renderType == .bubble ? .italic(.small) : .normal(.short)) +// } } + if let peer = peer, peer.isSupergroup, message.isAnonymousMessage { + for attr in message.attributes { + if let attr = attr as? AuthorSignatureMessageAttribute { + if !message.flags.contains(.Failed) { + let badge: NSAttributedString = .initialize(string: " " + attr.signature, color: !hasBubble ? presentation.colors.grayText : presentation.chat.grayText(isIncoming, object.renderType == .bubble), font: .normal(.short)) + + adminBadge = TextViewLayout(badge, maximumNumberOfLines: 1, truncationType: .end, alignment: .left) + adminBadge?.mayItems = false + adminBadge?.measure(width: .greatestFiniteMagnitude) + } + break + } + } + } + if postAuthorAttributed == nil, ChatRowItem.authorIsChannel(message: message, account: context.account) { + if let author = message.forwardInfo?.authorSignature { + postAuthorAttributed = .initialize(string: author, color: isStateOverlayLayout ? stateOverlayTextColor : !hasBubble ? presentation.colors.grayText : presentation.chat.grayText(isIncoming, object.renderType == .bubble), font: renderType == .bubble ? .italic(.small) : .normal(.short)) + } + } + - if let peer = messageMainPeer(message) as? TelegramUser, peer.botInfo != nil || peer.id == account.peerId { - self.isRead = true + if let peer = messageMainPeer(message) as? TelegramUser, peer.botInfo != nil || peer.id == context.peerId { + if !peer.flags.contains(.isSupport) { + self.isRead = true + } } - if let info = message.forwardInfo { + if let info = message.forwardInfo, !message.isImported { + - var accept:Bool = true + var accept:Bool = !isHasSource && message.id.peerId != context.peerId if let media = message.media.first as? TelegramMediaFile { + if media.isAnimatedSticker { + accept = false + } + for attr in media.attributes { switch attr { case .Sticker: accept = false case let .Audio(isVoice, _, _, _, _): - accept = isVoice + if !isVoice, let forwardInfo = message.forwardInfo, let source = forwardInfo.source, source.isChannel { + accept = accept && forwardInfo.author?.id == forwardInfo.source?.id + } else { + accept = accept && isVoice + } default: break } } } + if !hasBubble && renderType == .bubble, message.forwardInfo?.psaType != nil { + accept = false + } else if (entry.renderType == .list && message.forwardInfo?.psaType != nil) { + accept = false + } - if accept { + if accept || (ChatRowItem.authorIsChannel(message: message, account: context.account) && info.author?.id != message.chatPeer(context.peerId)?.id) { forwardType = fwdType - let attr = NSMutableAttributedString() - if let source = info.source, source.isChannel { - var range = attr.append(string: source.displayTitle, color: theme.colors.link, font: .medium(.text)) - if info.author.id != source.id { - let subrange = attr.append(string: " (\(info.author.displayTitle))", color: theme.colors.link, font: .medium(.text)) - range.length += subrange.length - } - attr.add(link: inAppLink.peerInfo(peerId: source.id, action:nil, openChat: true, postId: nil, callback:chatInteraction.openInfo), for: range) - + + var attr = NSMutableAttributedString() + + if ChatRowItem.authorIsChannel(message: message, account: context.account) { + if let author = info.author { + let range = attr.append(string: author.displayTitle, color: presentation.chat.linkColor(isIncoming, object.renderType == .bubble), font: .medium(.text)) + + let appLink = inAppLink.peerInfo(link: "", peerId: author.id, action: nil, openChat: !(author is TelegramUser), postId: info.sourceMessageId?.id, callback: chatInteraction.openInfo) + attr.add(link: appLink, for: range, color: presentation.chat.linkColor(isIncoming, object.renderType == .bubble)) + } else { + let range = attr.append(string: info.authorTitle, color: presentation.chat.linkColor(isIncoming, object.renderType == .bubble), font: .normal(.text)) + attr.add(link: inAppLink.callback("hid", { _ in + hiddenFwdTooltip?() + }), for: range) + } } else { - let range = attr.append(string: info.author.displayTitle, color: theme.colors.link, font: .medium(.text)) - var linkAbility: Bool = true - if let channel = info.author as? TelegramChannel { - if channel.username == nil && channel.participationStatus != .member { - linkAbility = false - } + + let color: NSColor + if message.forwardInfo?.psaType != nil { + color = presentation.chat.greenUI(isIncoming, object.renderType == .bubble) + } else { + color = presentation.chat.linkColor(isIncoming, object.renderType == .bubble) } - if linkAbility { - attr.add(link: inAppLink.peerInfo(peerId: info.author.id, action:nil, openChat: info.author.isChannel, postId: info.sourceMessageId?.id, callback:chatInteraction.openInfo), for: range) + + if let source = info.source, source.isChannel { + var range = attr.append(string: source.displayTitle, color: color, font: .medium(.text)) + if info.author?.id != source.id { + let subrange = attr.append(string: " (\(info.authorTitle))", color: color, font: .medium(.text)) + range.length += subrange.length + } + + let link = source.addressName == nil ? "https://t.me/c/\(source.id.id)/\(info.sourceMessageId?.id != nil ? "\(info.sourceMessageId!.id)" : "")" : "https://t.me/\(source.addressName!)/\(info.sourceMessageId?.id != nil ? "\(info.sourceMessageId!.id)" : "")" + let appLink = inApp(for: link.nsstring, context: context, peerId: nil, openInfo: chatInteraction.openInfo) + attr.add(link: appLink, for: range, color: color) + + } else { + let range = attr.append(string: info.authorTitle, color: color, font: info.author == nil ? .normal(.text) : .medium(.text)) + + var linkAbility: Bool = true + if let channel = info.author as? TelegramChannel { + if channel.username == nil && channel.participationStatus != .member { + linkAbility = false + } + } + if linkAbility, let author = info.author { + attr.add(link: inAppLink.peerInfo(link: "", peerId: author.id, action:nil, openChat: author.isChannel, postId: info.sourceMessageId?.id, callback:chatInteraction.openInfo), for: range) + } else if info.author == nil { + attr.add(link: inAppLink.callback("hid", { _ in + hiddenFwdTooltip?() + }), for: range) + + } } } + var isInstantVideo: Bool { + if let media = message.media.first as? TelegramMediaFile { + return media.isInstantVideo + } + return false + } + + let forwardNameColor: NSColor + if message.forwardInfo?.psaType != nil { + forwardNameColor = theme.chat.greenUI(isIncoming, object.renderType == .bubble) + } else if isForwardScam { + forwardNameColor = theme.chat.redUI(isIncoming, object.renderType == .bubble) + } else if !hasBubble { + forwardNameColor = presentation.colors.grayText + } else if isIncoming { + forwardNameColor = presentation.chat.linkColor(isIncoming, object.renderType == .bubble) + } else { + forwardNameColor = presentation.chat.grayText(isIncoming || isInstantVideo, object.renderType == .bubble) + } - _ = attr.append(string: " ") - _ = attr.append(string: DateUtils.string(forLastSeen: info.date), color: theme.colors.grayText, font: .normal(.short)) + if renderType == .bubble { + + let text: String + if let psaType = message.forwardInfo?.psaType { + text = localizedPsa("psa.title.bubbles", type: psaType, args: [attr.string]) + } else { + var fullName = attr.string + if let signature = message.forwardInfo?.authorSignature, message.isAnonymousMessage { + fullName += " (\(signature))" + } + text = L10n.chatBubblesForwardedFrom(fullName) + } + + let newAttr = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: .normal(.short), textColor: forwardNameColor), link: MarkdownAttributeSet(font: hasBubble && info.author != nil ? .medium(.short) : .normal(.short), textColor: forwardNameColor), linkAttribute: { [weak attr] contents in + if let attr = attr, !attr.string.isEmpty, let link = attr.attribute(NSAttributedString.Key.link, at: 0, effectiveRange: nil) { + return (NSAttributedString.Key.link.rawValue, link) + } + return nil + })) + attr = newAttr.mutableCopy() as! NSMutableAttributedString + } else { + _ = attr.append(string: " ") + _ = attr.append(string: DateUtils.string(forLastSeen: info.date), color: renderType == .bubble ? forwardNameColor : presentation.colors.grayText, font: .normal(.short)) + } + - forwardNameLayout = TextViewLayout(attr, maximumNumberOfLines: 1, truncationType: .end) + forwardNameLayout = TextViewLayout(attr, maximumNumberOfLines: renderType == .bubble ? 2 : 1, truncationType: .end, alwaysStaticItems: true) forwardNameLayout?.interactions = globalLinkExecutor - } + } + } + + let fillName: Bool + let rank: String? + switch itemType { + case let .Full(r, header): + rank = r + fillName = header == .normal + case let .Short(r, header): + rank = r + fillName = header == .normal && theme.bubbled } - if case .Full(let isAdmin) = itemType { + if fillName { + + let canFillAuthorName: Bool = ChatRowItem.canFillAuthorName(message, chatInteraction: chatInteraction, renderType: renderType, isIncoming: isIncoming, hasBubble: hasBubble) + + if isForwardScam || canFillAuthorName { + self.isForwardScam = false + } var titlePeer:Peer? = self.peer + var title:String = self.peer?.displayTitle ?? "" - var title:String = peer?.displayTitle ?? "" - if let peer = messageMainPeer(message) as? TelegramChannel, case .broadcast(_) = peer.info { + if object.renderType == .list, let _ = message.forwardInfo?.psaType { + + } else if let peer = messageMainPeer(message) as? TelegramChannel, case .broadcast(_) = peer.info, message.adAttribute == nil { title = peer.displayTitle titlePeer = peer } @@ -465,214 +1688,738 @@ class ChatRowItem: TableRowItem { let attr:NSMutableAttributedString = NSMutableAttributedString() if let peer = titlePeer { - var nameColor:NSColor = theme.colors.link + var nameColor:NSColor = presentation.chat.linkColor(isIncoming, object.renderType == .bubble) if messageMainPeer(message) is TelegramChannel || messageMainPeer(message) is TelegramGroup { if let peer = messageMainPeer(message) as? TelegramChannel, case .broadcast(_) = peer.info { - nameColor = theme.colors.link - } else if account.peerId != peer.id { - let value = ObjcUtils.colorMask(peer.id.id, mainId: account.peerId.id) - nameColor = userChatColors[Int(value) % userChatColors.count] ?? theme.colors.blueText + nameColor = presentation.chat.linkColor(isIncoming, object.renderType == .bubble) + } else if context.peerId != peer.id { + if object.renderType == .bubble, message.isAnonymousMessage, !isIncoming { + nameColor = presentation.colors.accentIconBubble_outgoing + } else { + let value = abs(Int(peer.id.id._internalGetInt64Value()) % 7) + nameColor = presentation.chat.peerName(value) + } } } - let range = attr.append(string: title, color: nameColor, font:.medium(.text)) - attr.addAttribute(NSAttributedStringKey.link, value: inAppLink.peerInfo(peerId:peer.id, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), range: range) + if message.forwardInfo?.psaType != nil, object.renderType == .list { + nameColor = presentation.colors.greenUI + } + + if canFillAuthorName { + let range = attr.append(string: title, color: nameColor, font: .medium(.text)) + if peer.id.id._internalGetInt64Value() != 0 { + attr.addAttribute(NSAttributedString.Key.link, value: inAppLink.peerInfo(link: "", peerId:peer.id, action:nil, openChat: peer.isChannel, postId: nil, callback: chatInteraction.openInfo), range: range) + } else { + nameHide = L10n.chatTooltipHiddenForwardName + } + } - for attribute in message.attributes { - if let attribute = attribute as? InlineBotMessageAttribute, let bot = message.peers[attribute.peerId] as? TelegramUser, let address = bot.username { - _ = attr.append(string: " \(tr(.chatMessageVia)) ", color: theme.colors.grayText, font:.medium(.text)) - let range = attr.append(string: "@" + address, color: theme.colors.blueText, font:.medium(.text)) - attr.addAttribute(NSAttributedStringKey.link, value: inAppLink.callback("@" + address, { (parameter) in + if let bot = message.inlinePeer, message.hasInlineAttribute, let address = bot.username { + if message.forwardInfo?.psaType == nil, !isBubbled || hasBubble { + if attr.length > 0 { + _ = attr.append(string: " ") + } + _ = attr.append(string: "\(L10n.chatMessageVia) ", color: !hasBubble ? presentation.colors.grayText : presentation.chat.grayText(isIncoming, object.renderType == .bubble), font:.medium(.text)) + let range = attr.append(string: "@" + address, color: presentation.chat.linkColor(isIncoming, hasBubble && isBubbled), font:.medium(.text)) + attr.addAttribute(NSAttributedString.Key.link, value: inAppLink.callback("@" + address, { (parameter) in chatInteraction.updateInput(with: parameter + " ") }), range: range) } } - - if isAdmin { - _ = attr.append(string: " \(tr(.chatAdminBadge))", color: theme.colors.grayText, font: .normal(.short)) + if canFillAuthorName { + var badge: NSAttributedString? = nil + if let rank = rank { + badge = .initialize(string: " " + rank, color: !hasBubble ? presentation.colors.grayText : presentation.chat.grayText(isIncoming, object.renderType == .bubble), font: .normal(.short)) + + } + else if ChatRowItem.authorIsChannel(message: message, account: context.account) { + badge = .initialize(string: " " + L10n.chatChannelBadge, color: !hasBubble ? presentation.colors.grayText : presentation.chat.grayText(isIncoming, object.renderType == .bubble), font: .normal(.short)) + } + if let badge = badge { + adminBadge = TextViewLayout(badge, maximumNumberOfLines: 1, truncationType: .end, alignment: .left) + adminBadge?.mayItems = false + adminBadge?.measure(width: .greatestFiniteMagnitude) + } } - authorText = TextViewLayout(attr, maximumNumberOfLines: 1, truncationType: .end, alignment: .left) - - authorText?.interactions = globalLinkExecutor - + if attr.length > 0 { + authorText = TextViewLayout(attr, maximumNumberOfLines: 1, truncationType: .end, alignment: .left) + authorText?.mayItems = false + authorText?.interactions = globalLinkExecutor + } } + } - var time:TimeInterval = TimeInterval(message.timestamp) - time -= account.context.timeDifference - date = TextNode.layoutText(maybeNode: nil, NSAttributedString.initialize(string: DateUtils.string(forMessageListDate: Int32(time)), color: theme.colors.grayText, font: NSFont.normal(.short)), nil, 1, .end, NSMakeSize(CGFloat.greatestFiniteMagnitude, 20), nil, false, .left) - - } + if message.timestamp != scheduleWhenOnlineTimestamp && message.adAttribute == nil { + var time:TimeInterval = TimeInterval(message.timestamp) + time -= context.timeDifference + + let dateFormatter = DateFormatter() + dateFormatter.timeStyle = .short + dateFormatter.dateStyle = .none + dateFormatter.timeZone = NSTimeZone.local + + date = TextNode.layoutText(maybeNode: nil, .initialize(string: dateFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(time))), color: isStateOverlayLayout ? stateOverlayTextColor : (!hasBubble ? presentation.colors.grayText : presentation.chat.grayText(isIncoming, object.renderType == .bubble)), font: renderType == .bubble ? .italic(.small) : .normal(.short)), nil, 1, .end, NSMakeSize(.greatestFiniteMagnitude, 20), nil, false, .left) + } else if message.adAttribute != nil { + date = TextNode.layoutText(maybeNode: nil, .initialize(string: L10n.chatMessageSponsored, color: isStateOverlayLayout ? stateOverlayTextColor : (!hasBubble ? presentation.colors.grayText : presentation.chat.grayText(isIncoming, object.renderType == .bubble)), font: renderType == .bubble ? .italic(.small) : .normal(.short)), nil, 1, .end, NSMakeSize(.greatestFiniteMagnitude, 20), nil, false, .left) + } + + } else { + self.isIncoming = false + self.hasBubble = false + } + + super.init(initialSize) + hiddenFwdTooltip = { [weak self] in + guard let view = self?.view as? ChatRowView, let forwardName = view.forwardName else { return } + tooltip(for: forwardName, text: L10n.chatTooltipHiddenForwardName, autoCorner: false) + } + + let editedAttribute = messages.compactMap({ + return $0.editedAttribute + }).sorted(by: { + $0.date < $1.date + }).first + + if let message = message { let formatter = DateFormatter() formatter.dateStyle = .medium formatter.timeStyle = .medium - formatter.locale = Locale(identifier: appCurrentLanguage.languageCode) - var fullDate: String = formatter.string(from: Date(timeIntervalSince1970: TimeInterval(message.timestamp) - account.context.timeDifference)) + formatter.timeZone = NSTimeZone.local + // + var fullDate: String = message.timestamp == scheduleWhenOnlineTimestamp ? "" : formatter.string(from: Date(timeIntervalSince1970: TimeInterval(message.timestamp) - context.timeDifference)) + + let threadId: MessageId? = chatInteraction.mode.threadId + + if let message = effectiveCommentMessage { + for attribute in message.attributes { + if let attribute = attribute as? ReplyThreadMessageAttribute, attribute.count > 0 { + if let peer = chatInteraction.peer, peer.isSupergroup, !chatInteraction.mode.isThreadMode { + replyCountAttributed = .initialize(string: Int(attribute.count).prettyNumber, color: isStateOverlayLayout ? stateOverlayTextColor : !hasBubble ? presentation.colors.grayText : presentation.chat.grayText(isIncoming, object.renderType == .bubble), font: renderType == .bubble ? .italic(.small) : .normal(.short)) + } + break + } + } + } + + if let attribute = editedAttribute { + if isEditMarkVisible { + editedLabel = TextNode.layoutText(maybeNode: nil, .initialize(string: L10n.chatMessageEdited, color: isStateOverlayLayout ? stateOverlayTextColor : !hasBubble ? presentation.colors.grayText : presentation.chat.grayText(isIncoming, object.renderType == .bubble), font: renderType == .bubble ? .italic(.small) : .normal(.short)), nil, 1, .end, NSMakeSize(.greatestFiniteMagnitude, 20), nil, false, .left) + } + + let formatterEdited = DateFormatter() + formatterEdited.dateStyle = .medium + formatterEdited.timeStyle = .medium + formatterEdited.timeZone = NSTimeZone.local + fullDate = "\(fullDate) (\(formatterEdited.string(from: Date(timeIntervalSince1970: TimeInterval(attribute.date)))))" + } else if message.isImported, let forwardInfo = message.forwardInfo { + let formatter = DateFormatter() + formatter.dateStyle = .short + formatter.timeStyle = .short + formatter.timeZone = NSTimeZone.local + formatter.doesRelativeDateFormatting = true + let text: String + if forwardInfo.date == message.timestamp { + text = L10n.chatMessageImportedShort + } else { + text = L10n.chatMessageImported(formatter.string(from: Date(timeIntervalSince1970: TimeInterval(forwardInfo.date)))) + } + editedLabel = TextNode.layoutText(maybeNode: nil, .initialize(string: text, color: isStateOverlayLayout ? stateOverlayTextColor : !hasBubble ? presentation.colors.grayText : presentation.chat.grayText(isIncoming, object.renderType == .bubble), font: renderType == .bubble ? .italic(.small) : .normal(.short)), nil, 1, .end, NSMakeSize(.greatestFiniteMagnitude, 20), nil, false, .left) + + fullDate = L10n.chatMessageImportedText + "\n\n" + fullDate + } else if let forwardInfo = message.forwardInfo { + let formatterEdited = DateFormatter() + formatterEdited.dateStyle = .medium + formatterEdited.timeStyle = .medium + formatterEdited.timeZone = NSTimeZone.local + fullDate = "\(fullDate) (\(formatterEdited.string(from: Date(timeIntervalSince1970: TimeInterval(forwardInfo.date)))))" + } for attribute in message.attributes { - if let attribute = attribute as? ReplyMessageAttribute { - self.replyModel = ReplyModel(replyMessageId: attribute.messageId, account:account, replyMessage:message.associatedMessages[attribute.messageId]) + if let attribute = attribute as? ReplyMessageAttribute, threadId != attribute.messageId, let replyMessage = message.associatedMessages[attribute.messageId] { + let replyPresentation = ChatAccessoryPresentation(background: hasBubble ? presentation.chat.backgroundColor(isIncoming, object.renderType == .bubble) : isBubbled ? presentation.colors.grayForeground : presentation.colors.background, title: presentation.chat.replyTitle(self), enabledText: presentation.chat.replyText(self), disabledText: presentation.chat.replyDisabledText(self), border: presentation.chat.replyTitle(self)) + + self.replyModel = ReplyModel(replyMessageId: attribute.messageId, context: context, replyMessage:replyMessage, autodownload: downloadSettings.isDownloable(replyMessage), presentation: replyPresentation, makesizeCallback: { [weak self] in + guard let `self` = self else {return} + _ = self.makeSize(self.oldWidth, oldWidth: 0) + Queue.mainQueue().async { [weak self] in + self?.redraw() + } + }) + replyModel?.isSideAccessory = isBubbled && !hasBubble } if let attribute = attribute as? ViewCountMessageAttribute { - channelViewsAttributed = NSAttributedString.initialize(string: attribute.count.prettyNumber, color: theme.colors.grayText, font: NSFont.normal(.short)) + channelViewsAttributed = .initialize(string: max(1, attribute.count).prettyNumber, color: isStateOverlayLayout ? stateOverlayTextColor : !hasBubble ? presentation.colors.grayText : presentation.chat.grayText(isIncoming, object.renderType == .bubble), font: renderType == .bubble ? .italic(.small) : .normal(.short)) + + var author: String = "" + loop: for attr in message.attributes { + if let attr = attr as? AuthorSignatureMessageAttribute { + author = "\(attr.signature), " + break loop + } + } + if attribute.count >= 1000 { - fullDate = "\(attribute.count.separatedNumber) \(tr(.chatMessageTooltipViews)), \(fullDate)" + fullDate = "\(author)\(attribute.count.separatedNumber) \(tr(L10n.chatMessageTooltipViews)), \(fullDate)" + } else { + fullDate = "\(author)\(fullDate)" } } - if let attribute = attribute as? EditedMessageAttribute { - if isEditMarkVisible { - editedLabel = TextNode.layoutText(maybeNode: nil, NSAttributedString.initialize(string: tr(.chatMessageEdited), color: theme.colors.grayText, font: NSFont.normal(.short)), nil, 1, .end, NSMakeSize(CGFloat.greatestFiniteMagnitude, 20), nil, false, .left) - } - - let formatterEdited = DateFormatter() - formatterEdited.dateStyle = .short - formatterEdited.timeStyle = .medium - formatterEdited.locale = Locale(identifier: appCurrentLanguage.languageCode) - fullDate = "\(fullDate) (\(formatterEdited.string(from: Date(timeIntervalSince1970: TimeInterval(attribute.date)))))" + +// if FastSettings.isTestLiked(message.id) { +// likesAttributed = .initialize(string: "1", color: isStateOverlayLayout ? stateOverlayTextColor : !hasBubble ? presentation.colors.grayText : presentation.chat.grayText(isIncoming, object.renderType == .bubble), font: renderType == .bubble ? .italic(.small) : .normal(.short)) +// } + + let paid: Bool + if let invoice = message.media.first as? TelegramMediaInvoice { + paid = invoice.receiptMessageId != nil + } else { + paid = false } if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline) { - replyMarkupModel = ReplyMarkupNode(attribute.rows, attribute.flags, chatInteraction.processBotKeyboard(with: message)) + if message.restrictedText(context.contentSettings) == nil { + replyMarkupModel = ReplyMarkupNode(attribute.rows, attribute.flags, chatInteraction.processBotKeyboard(with: message), paid: paid) + } } +// else if let attribute = attribute as? ReactionsMessageAttribute { +// var buttons:[ReplyMarkupButton] = [] +// let sorted = attribute.reactions.sorted(by: { $0.count > $1.count }) +// for reaction in sorted { +// buttons.append(ReplyMarkupButton(title: reaction.value + " \(reaction.count)", titleWhenForwarded: nil, action: .url(reaction.value))) +// } +// if !buttons.isEmpty { +// replyMarkupModel = ReplyMarkupNode([ReplyMarkupRow(buttons: buttons)], [], ReplyMarkupInteractions(proccess: { (button, _) in +// switch button.action { +// case let .url(buttonReaction): +// if let index = sorted.firstIndex(where: { $0.value == buttonReaction}) { +// let reaction = sorted[index] +// var newValues = sorted +// if reaction.isSelected { +// newValues.remove(at: index) +// } else { +// newValues[index] = MessageReaction(value: reaction.value, count: reaction.count + 1, isSelected: true) +// } +// chatInteraction.updateReactions(message.id, buttonReaction, { value in +// }) +// } +// +// default: +// break +// } +// })) +// } +// } + + /* + let reactions = object.message?.attributes.first(where: { attr -> Bool in + return attr is ReactionsMessageAttribute + }) + + if let reactions = reactions as? ReactionsMessageAttribute { + var bp:Int = 0 + bp += 1 + } + */ + + } + + if let attr = message.autoremoveAttribute, let begin = attr.countdownBeginTime { + self.updateCountDownTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in + let left = Int(begin + attr.timeout - context.timestamp) + if left >= 0 { + let leftText = "\n\n" + L10n.chatContextMenuAutoDelete(smartTimeleftText(left)) + self?.fullDate = fullDate + leftText + self?.updateTooltip?(fullDate + leftText) + } else { + self?.updateCountDownTimer = nil + } + }, queue: .mainQueue()) + self.updateCountDownTimer?.start() + } else { + updateCountDownTimer = nil } self.fullDate = fullDate } } - init(_ initialSize:NSSize, _ chatInteraction:ChatInteraction, _ entry: ChatHistoryEntry) { + init(_ initialSize:NSSize, _ chatInteraction:ChatInteraction, _ entry: ChatHistoryEntry, _ downloadSettings: AutomaticMediaDownloadSettings, theme: TelegramPresentationTheme) { self.entry = entry + self.context = chatInteraction.context self.message = entry.message self.chatInteraction = chatInteraction + self.renderType = entry.renderType + self.downloadSettings = downloadSettings + self.presentation = theme + self.isIncoming = false + self.hasBubble = false + self.isScam = false + self.isForwardScam = false + self.isFake = false + self.isForwardFake = false super.init(initialSize) } - public static func item(_ initialSize:NSSize, from entry:ChatHistoryEntry, with account:Account, interaction:ChatInteraction) -> ChatRowItem { + public static func item(_ initialSize:NSSize, from entry:ChatHistoryEntry, interaction:ChatInteraction, downloadSettings: AutomaticMediaDownloadSettings = AutomaticMediaDownloadSettings.defaultSettings, theme: TelegramPresentationTheme) -> TableRowItem { + + switch entry { + case .UnreadEntry: + return ChatUnreadRowItem(initialSize, interaction, interaction.context, entry, downloadSettings, theme: theme) + case .groupedPhotos: + return ChatGroupedItem(initialSize, interaction, interaction.context, entry, downloadSettings, theme: theme) + case .DateEntry: + return ChatDateStickItem(initialSize, entry, interaction: interaction, theme: theme) + case .bottom: + return GeneralRowItem(initialSize, height: theme.bubbled ? 10 : 20, stableId: entry.stableId, backgroundColor: .clear) + case .commentsHeader: + return ChatCommentsHeaderItem(initialSize, entry, interaction: interaction, theme: theme) + case .repliesHeader: + return RepliesHeaderRowItem(initialSize, entry: entry) + case let .topThreadInset(height, _, _): + return GeneralRowItem(initialSize, height: height, stableId: entry.stableId, backgroundColor: .clear) + default: + break + } if let message = entry.message { - if message.media.count == 0 || (message.media.count == 1 && message.media[0] is TelegramMediaWebpage) { - return ChatMessageItem(initialSize, interaction, account,entry) + if message.media.count == 0 || message.media.first is TelegramMediaWebpage { + return ChatMessageItem(initialSize, interaction, interaction.context, entry, downloadSettings, theme: theme) } else { - if message.id.peerId.namespace == Namespaces.Peer.CloudUser, let _ = message.autoremoveAttribute { - return ChatServiceItem(initialSize,interaction,account,entry) - } else if let file = message.media[0] as? TelegramMediaFile { + if let action = message.media[0] as? TelegramMediaAction { + switch action.action { + case .phoneCall: + return ChatCallRowItem(initialSize, interaction, interaction.context, entry, downloadSettings, theme: theme) + default: + return ChatServiceItem(initialSize, interaction, interaction.context, entry, downloadSettings, theme: theme) + } + } else if let file = message.media[0] as? TelegramMediaFile { if file.isInstantVideo { - return ChatVideoMessageItem(initialSize,interaction,account,entry) + return ChatVideoMessageItem(initialSize, interaction, interaction.context,entry, downloadSettings, theme: theme) } else if file.isVideo && !file.isAnimated { - return ChatMediaItem(initialSize,interaction,account,entry) - } else if file.isSticker { - return ChatMediaItem(initialSize,interaction,account,entry) + return ChatMediaItem(initialSize, interaction, interaction.context, entry, downloadSettings, theme: theme) + } else if file.isStaticSticker { + return ChatMediaItem(initialSize, interaction, interaction.context, entry, downloadSettings, theme: theme) } else if file.isVoice { - return ChatVoiceRowItem(initialSize,interaction,account,entry) + return ChatVoiceRowItem(initialSize,interaction, interaction.context,entry, downloadSettings, theme: theme) } else if file.isVideo && file.isAnimated { - return ChatGIFMediaItem(initialSize,interaction,account,entry) - } else if !file.isVideo && file.isAnimated { - return ChatMediaItem(initialSize,interaction,account,entry) + return ChatMediaItem(initialSize, interaction, interaction.context, entry, downloadSettings, theme: theme) + } else if !file.isVideo && (file.isAnimated && !file.mimeType.hasSuffix("gif")) { + return ChatMediaItem(initialSize, interaction, interaction.context, entry, downloadSettings, theme: theme) } else if file.isMusic { - return ChatMusicRowItem(initialSize,interaction,account,entry) + return ChatMusicRowItem(initialSize,interaction, interaction.context, entry, downloadSettings, theme: theme) + } else if file.isAnimatedSticker { + return ChatAnimatedStickerItem(initialSize,interaction, interaction.context, entry, downloadSettings, theme: theme) } - return ChatFileMediaItem(initialSize,interaction,account,entry) - } else if let action = message.media[0] as? TelegramMediaAction { - switch action.action { - case .phoneCall: - return ChatCallRowItem(initialSize, interaction, account, entry) - default: - return ChatServiceItem(initialSize, interaction, account, entry) - } - + return ChatFileMediaItem(initialSize,interaction, interaction.context, entry, downloadSettings, theme: theme) } else if message.media[0] is TelegramMediaMap { - return ChatMapRowItem(initialSize,interaction,account,entry) + return ChatMapRowItem(initialSize,interaction, interaction.context, entry, downloadSettings, theme: theme) } else if message.media[0] is TelegramMediaContact { - return ChatContactRowItem(initialSize,interaction,account,entry) + return ChatContactRowItem(initialSize, interaction, interaction.context, entry, downloadSettings, theme: theme) } else if message.media[0] is TelegramMediaInvoice { - return ChatInvoiceItem(initialSize,interaction,account,entry) + return ChatInvoiceItem(initialSize, interaction, interaction.context, entry, downloadSettings, theme: theme) } else if message.media[0] is TelegramMediaExpiredContent { - return ChatServiceItem(initialSize,interaction,account,entry) + return ChatServiceItem(initialSize, interaction,interaction.context, entry, downloadSettings, theme: theme) + } else if message.media.first is TelegramMediaGame { + return ChatMessageItem(initialSize, interaction, interaction.context, entry, downloadSettings, theme: theme) + } else if message.media.first is TelegramMediaPoll { + return ChatPollItem(initialSize, interaction, interaction.context, entry, downloadSettings, theme: theme) + } else if message.media.first is TelegramMediaUnsupported { + return ChatMessageItem(initialSize, interaction, interaction.context,entry, downloadSettings, theme: theme) + } else if message.media.first is TelegramMediaDice { + return ChatMediaDice(initialSize, interaction, interaction.context, entry, downloadSettings, theme: theme) + } + + return ChatMediaItem(initialSize, interaction, interaction.context, entry, downloadSettings, theme: theme) + } + + } + + fatalError("no item for entry") + + } + + override func makeSize(_ width: CGFloat, oldWidth:CGFloat) -> Bool { + + let result = super.makeSize(width, oldWidth: oldWidth) + isForceRightLine = false + + commentsBubbleData?.makeSize() + commentsBubbleDataOverlay?.makeSize() + commentsData?.makeSize() + + if !(self is ChatGroupedItem) { + for layout in captionLayouts { + layout.layout.dropLayoutSize() + } + } + + if let channelViewsAttributed = channelViewsAttributed { + channelViews = TextNode.layoutText(maybeNode: channelViewsNode, channelViewsAttributed, !hasBubble ? presentation.colors.grayText : presentation.chat.grayText(isIncoming, renderType == .bubble), 1, .end, NSMakeSize(hasBubble ? 60 : max(150,width - contentOffset.x - 44 - 150), 20), nil, false, .left) + } + + if let replyCountAttributed = replyCountAttributed { + replyCount = TextNode.layoutText(maybeNode: replyCountNode, replyCountAttributed, !hasBubble ? presentation.colors.grayText : presentation.chat.grayText(isIncoming, renderType == .bubble), 1, .end, NSMakeSize(hasBubble ? 60 : max(150,width - contentOffset.x - 44 - 150), 20), nil, false, .left) + } + + if let likesAttributed = likesAttributed { + likes = TextNode.layoutText(maybeNode: likesNode, likesAttributed, !hasBubble ? presentation.colors.grayText : presentation.chat.grayText(isIncoming, renderType == .bubble), 1, .end, NSMakeSize(hasBubble ? 60 : max(150,width - contentOffset.x - 44 - 150), 20), nil, false, .left) + } + + var widthForContent: CGFloat = blockWidth + if previousBlockWidth != widthForContent { + self.previousBlockWidth = widthForContent + _contentSize = self.makeContentSize(widthForContent) + } + + + func layout() -> Bool { + if additionalLineForDateInBubbleState == nil && !isFixedRightPosition { + if _contentSize.width + rightSize.width + insetBetweenContentAndDate > widthForContent { + // widthForContent = _contentSize.width - 5 + self.isForceRightLine = true + //_contentSize = self.makeContentSize(widthForContent) + return true + } + } + return true + } + + if hasBubble { + + while !layout() {} + } + + + + var maxContentWidth = _contentSize.width + if hasBubble { + maxContentWidth -= bubbleDefaultInnerInset + } + + if isBubbled && isBubbleFullFilled { + widthForContent = maxContentWidth + } + if !(self is ChatGroupedItem) { + for layout in captionLayouts { + if layout.layout.layoutSize == .zero { + layout.layout.measure(width: maxContentWidth) + } + } + } + + + + + if let forwardNameLayout = forwardNameLayout { + var w = widthForContent + if isBubbled && !hasBubble { + w = width - _contentSize.width - 85 + } + forwardNameLayout.measure(width: min(w, 250)) + } + + if (forwardType == .FullHeader || forwardType == .ShortHeader) && (entry.renderType == .bubble || message?.forwardInfo?.psaType == nil) { + + let color: NSColor + let text: String + if let psaType = message?.forwardInfo?.psaType { + color = presentation.chat.greenUI(isIncoming, isBubbled) + text = localizedPsa("psa.title", type: psaType) + } else { + color = !hasBubble ? presentation.colors.grayText : presentation.chat.grayText(isIncoming, renderType == .bubble) + text = L10n.messagesForwardHeader + } + + forwardHeader = TextNode.layoutText(maybeNode: forwardHeaderNode, .initialize(string: text, color: color, font: .normal(.text)), nil, 1, .end, NSMakeSize(width - self.contentOffset.x - 44, 20), nil,false, .left) + } else { + forwardHeader = nil + } + + if !isBubbled { + replyModel?.measureSize(widthForContent, sizeToFit: true) + } else if let replyModel = replyModel { + if let item = self as? ChatMessageItem, item.webpageLayout == nil && !replyModel.isSideAccessory { + if isBubbled { + replyModel.measureSize(max(blockWidth, 200), sizeToFit: true) + } else { + replyModel.measureSize(max(contentSize.width, 200), sizeToFit: true) + } + } else { + if !hasBubble { + replyModel.measureSize(min(width - _contentSize.width - contentOffset.x - 80, 300), sizeToFit: true) + } else { + replyModel.measureSize(_contentSize.width - bubbleDefaultInnerInset, sizeToFit: true) + } + } + } + + + + if !canFillAuthorName, let replyModel = replyModel, let authorText = authorText, replyModel.isSideAccessory { + var adminWidth: CGFloat = 0 + if let adminBadge = adminBadge { + adminWidth = adminBadge.layoutSize.width + } + + authorText.measure(width: replyModel.size.width - 10 - adminWidth) + + replyModel.topOffset = authorText.layoutSize.height + 6 + replyModel.measureSize(replyModel.width, sizeToFit: replyModel.sizeToFit) + } else { + var adminWidth: CGFloat = 0 + if let adminBadge = adminBadge { + adminWidth = adminBadge.layoutSize.width + } + + var supplyOffset: CGFloat = 0 + +// if let channelViews = channelViews { +// supplyOffset += channelViews.0.size.width + 16 +// } +// if let replyCount = replyCount { +// supplyOffset += replyCount.0.size.width + 16 +// } +// +// if let _ = postAuthorAttributed { +// supplyOffset += 50 +// } +// if let commentsData = commentsData { +// supplyOffset += commentsData.size(false).width +// } + if !isBubbled { + supplyOffset += rightSize.width + } + + authorText?.measure(width: widthForContent - adminWidth - supplyOffset) + + } + + + if let postAuthorAttributed = postAuthorAttributed { + + postAuthor = TextNode.layoutText(maybeNode: postAuthorNode, postAuthorAttributed, !hasBubble ? presentation.colors.grayText : presentation.chat.grayText(isIncoming, renderType == .bubble), 1, .end, NSMakeSize(hasBubble ? 60 : (width - (authorText != nil ? authorText!.layoutSize.width : 0) - contentOffset.x - 44) / 2, 20), nil, false, .left) + } + + if hasBubble && isBubbleFullFilled { + if let postAuthor = postAuthor, let postAuthorAttributed = postAuthorAttributed { + let width: CGFloat = _contentSize.width - (rightSize.width - postAuthor.0.size.width - 8) - bubbleContentInset - additionBubbleInset - 10 + if width < 0 { + self.postAuthor = nil + } else { + self.postAuthor = TextNode.layoutText(maybeNode: postAuthorNode, postAuthorAttributed, !hasBubble ? presentation.colors.grayText : presentation.chat.grayText(isIncoming, renderType == .bubble), 1, .end, NSMakeSize( width, 20), nil, false, .left) + } + } + } + + if hasBubble && !isBubbleFullFilled { + if let postAuthorAttributed = postAuthorAttributed, let postAuthor = postAuthor { + if bubbleFrame.width < width - 150 { + let size = rightSize.width - postAuthor.0.size.width - 8 + var w = width - bubbleFrame.width - 150 + if let _ = self as? ChatMessageItem, additionalLineForDateInBubbleState != nil { + w = _contentSize.width - size + } + self.postAuthor = TextNode.layoutText(maybeNode: postAuthorNode, postAuthorAttributed, !hasBubble ? presentation.colors.grayText : presentation.chat.grayText(isIncoming, renderType == .bubble), 1, .end, NSMakeSize( w, 20), nil, false, .left) + } else if bubbleFrame.width > _contentSize.width + rightSize.width + bubbleDefaultInnerInset { + var size = bubbleFrame.width - (_contentSize.width + rightSize.width + bubbleDefaultInnerInset) + if !postAuthor.0.isPerfectSized { + size = bubbleFrame.width - (_contentSize.width + bubbleDefaultInnerInset) + } + self.postAuthor = TextNode.layoutText(maybeNode: postAuthorNode, postAuthorAttributed, !hasBubble ? presentation.colors.grayText : presentation.chat.grayText(isIncoming, renderType == .bubble), 1, .end, NSMakeSize( size, 20), nil, false, .left) + while !layout() {} + } + + } + + + + if _contentSize.width < rightSize.width { + if !(self is ChatMessageItem) { + _contentSize.width = rightSize.width + } else if additionalLineForDateInBubbleState != nil { + _contentSize.width = rightSize.width + } + } + } + + if isBubbled { + replyMarkupModel?.measureSize(bubbleFrame.width - additionBubbleInset) + } else { + if let item = self as? ChatMessageItem { + if item.webpageLayout != nil { + replyMarkupModel?.measureSize(_contentSize.width) + } else if _contentSize.width < 200 { + replyMarkupModel?.measureSize(max(_contentSize.width, blockWidth)) + } else { + replyMarkupModel?.measureSize(_contentSize.width) + } + } else { + replyMarkupModel?.measureSize(_contentSize.width) + } + } + + + return result + } + + deinit { + var bp:Int = 0 + bp += 1 + } + + var bubbleContentInset: CGFloat { + return 13 + } + + var additionBubbleInset: CGFloat { + return 6 + } + + var insetBetweenContentAndDate: CGFloat { + return 10 + } + + var bubbleCornerInset: CGFloat { + if isIncoming { + if let message = message, let peer = message.peers[message.id.peerId] { + if peer.isGroup || peer.isSupergroup { + return additionBubbleInset + 36 } - - return ChatMediaItem(initialSize,interaction,account,entry) } - } + return additionBubbleInset + } + + var maxTitleWidth: CGFloat { + let nameWidth:CGFloat + if hasBubble { + nameWidth = (authorText?.layoutSize.width ?? 0) + additionBad + (adminBadge?.layoutSize.width ?? 0) + } else { + nameWidth = 0 + } + + let forwardWidth = hasBubble ? (forwardNameLayout?.layoutSize.width ?? 0) + additionForwardBad + (isPsa ? 30 : 0) : 0 - fatalError("no item for entry") + let replyWidth = min(hasBubble ? (replyModel?.size.width ?? 0) : 0, 200) + return min(max(max(nameWidth, forwardWidth), replyWidth), contentSize.width) } - override func makeSize(_ width: CGFloat, oldWidth:CGFloat) -> Bool { + var additionBad: CGFloat { + return (isScam ? theme.icons.chatScam.backingSize.width + 3 : 0) + (isFake ? theme.icons.chatFake.backingSize.width + 3 : 0) + } + var additionForwardBad: CGFloat { + return (isForwardScam ? theme.icons.chatScam.backingSize.width + 3 : 0) + (isForwardFake ? theme.icons.chatFake.backingSize.width + 3 : 0) + } + + var badIcon: CGImage { + return isScam ? theme.icons.chatScam : theme.icons.chatFake + } + var forwardBadIcon: CGImage { + return isForwardScam ? theme.icons.chatScam : theme.icons.chatFake + } + + var bubbleFrame: NSRect { + let nameWidth:CGFloat + if hasBubble { + nameWidth = (authorText?.layoutSize.width ?? 0) + additionBad + (adminBadge?.layoutSize.width ?? 0) + } else { + nameWidth = 0 + } + let forwardWidth = hasBubble ? (forwardNameLayout?.layoutSize.width ?? 0) + additionForwardBad + (isPsa ? 30 : 0) : 0 + let replyWidth: CGFloat = hasBubble ? (replyModel?.size.width ?? 0) : 0 + var rect = NSMakeRect(defLeftInset, 2, contentSize.width, height - 4) - if let channelViewsAttributed = channelViewsAttributed { - channelViews = TextNode.layoutText(maybeNode: channelViewsNode, channelViewsAttributed, theme.colors.grayText, 1, .end, NSMakeSize(max(150,width - contentOffset.x - 44 - 150), 20), nil, false, .left) - } - if let postAuthorAttributed = postAuthorAttributed { - postAuthor = TextNode.layoutText(maybeNode: postAuthorNode, postAuthorAttributed, theme.colors.grayText, 1, .end, NSMakeSize((width - contentOffset.x - 44) / 2, 20), nil, false, .left) + + if isBubbled, let replyMarkup = replyMarkupModel { + rect.size.height -= (replyMarkup.size.height + defaultContentInnerInset) } - //let additionWidth:CGFloat = date?.0.size.width ?? 20 - // _contentSize = self.makeContentSize(width - self.contentOffset.x - rightSize.width - 44) - if case .Full = itemType { - let additionWidth:CGFloat = date?.0.size.width ?? 20 - _contentSize = self.makeContentSize(width - self.contentOffset.x - 44 - additionWidth) + //if forwardType != nil { + // rect.origin.x -= leftContentInset + //} + + if additionalLineForDateInBubbleState == nil && !isFixedRightPosition && rightSize.width > 0 { + rect.size.width += rightSize.width + insetBetweenContentAndDate + bubbleDefaultInnerInset } else { - _contentSize = self.makeContentSize(width - self.contentOffset.x - rightSize.width - 44) + rect.size.width += bubbleContentInset * 2 + insetBetweenContentAndDate } - if let captionLayout = captionLayout { - captionLayout.measure(width: _contentSize.width) - } - authorText?.measure(width: blockSize.width) - - if let forwardNameLayout = forwardNameLayout { - forwardNameLayout.measure(width: width - self.contentOffset.x - rightSize.width - 20) - } + rect.size.width = max(nameWidth + bubbleDefaultInnerInset, rect.width) - if forwardType == .FullHeader || forwardType == .ShortHeader { - forwardHeader = TextNode.layoutText(maybeNode: forwardHeaderNode, NSAttributedString.initialize(string: tr(.messagesForwardHeader), color: theme.colors.grayText, font: NSFont.normal(FontSize.text)), nil, 1, .end, NSMakeSize(width - self.contentOffset.x - 44, 20), nil,false, .left) - } else { - forwardHeader = nil - } + rect.size.width = max(rect.size.width, replyWidth + bubbleDefaultInnerInset) - - replyModel?.measureSize(width - self.contentOffset.x - 44) + rect.size.width = max(rect.size.width, forwardWidth + bubbleDefaultInnerInset) - if !(self is ChatMessageItem) { - replyMarkupModel?.measureSize(_contentSize.width) - } else { - replyMarkupModel?.measureSize(max(_contentSize.width, blockSize.width)) + if let commentsBubbleData = commentsBubbleData { + rect.size.width = max(rect.size.width, commentsBubbleData.size(hasBubble, false).width) } - - - return super.makeSize(width, oldWidth: oldWidth) + return rect } - deinit { - var bp:Int = 0 - bp += 1 + var isFixedRightPosition: Bool { + return additionalLineForDateInBubbleState != nil + } + + var additionalLineForDateInBubbleState: CGFloat? { + return isForceRightLine ? rightSize.height : nil } func deleteMessage() { - _ = account.postbox.modify { [weak message] modifier -> Void in + _ = context.account.postbox.transaction { [weak message] transaction -> Void in if let message = message { - modifier.deleteMessages([message.id]) + if let _ = message.groupingKey { + let messages = transaction.getMessageGroup(message.id) + if let messages = messages { + transaction.deleteMessages(messages.map { $0.id }, forEachMedia: { media in + + }) + } + } else { + transaction.deleteMessages([message.id], forEachMedia: { media in + + }) + } + } }.start() } - func resendMessage() { - if let message = message { - _ = resendMessages(account: account, messageIds: [message.id]).start() + func openInfo() { + switch chatInteraction.chatLocation { + case .peer, .replyThread: + if let peer = peer { + let messageId: MessageId? + if chatInteraction.isGlobalSearchMessage { + messageId = self.message?.id + } else { + messageId = nil + } + if peer.id == self.message?.id.peerId, messageId == nil { + chatInteraction.openInfo(peer.id, false, nil, nil) + } else { + chatInteraction.openInfo(peer.id, !(peer is TelegramUser), messageId, nil) + } + } } + + } + + func resendMessage(_ ids: [MessageId]) { + _ = resendMessages(account: context.account, messageIds: ids).start() } func makeContentSize(_ width:CGFloat) -> NSSize { @@ -685,15 +2432,15 @@ class ChatRowItem: TableRowItem { } func replyAction() -> Bool { - if chatInteraction.presentation.state == .normal { + if chatInteraction.presentation.state == .normal, chatInteraction.mode.threadId != effectiveCommentMessage?.id { chatInteraction.setupReplyMessage(message?.id) return true } return false } func editAction() -> Bool { - if chatInteraction.presentation.state == .normal || chatInteraction.presentation.state == .editing { - if let message = message, canEditMessage(message, account: account) { + if chatInteraction.presentation.state == .normal || chatInteraction.presentation.state == .editing, chatInteraction.mode.threadId != effectiveCommentMessage?.id { + if let message = message, canEditMessage(message, chatInteraction: chatInteraction, context: context) { chatInteraction.beginEditingMessage(message) return true } @@ -702,7 +2449,7 @@ class ChatRowItem: TableRowItem { } func forwardAction() -> Bool { if chatInteraction.presentation.state != .selecting, let message = message { - if canForwardMessage(message, account: account) { + if canForwardMessage(message, chatInteraction: chatInteraction) { chatInteraction.forwardMessages([message.id]) return true } @@ -710,94 +2457,602 @@ class ChatRowItem: TableRowItem { return false } - override func menuItems() -> Signal<[ContextMenuItem], Void> { + override var instantlyResize: Bool { + return forwardType != nil + } + + override func menuItems(in location: NSPoint) -> Signal<[ContextMenuItem], NoError> { + if chatInteraction.disableSelectAbility { + return super.menuItems(in: location) + } + if let message = message { + return chatMenuItems(for: message, item: self, chatInteraction: chatInteraction) + } + return super.menuItems(in: location) + } + + var stateOverlayBackgroundColor: NSColor { + guard let media = self.message?.media.first else { + return self.presentation.chatServiceItemColor + } + if media is TelegramMediaImage { + return self.presentation.colors.blackTransparent.withAlphaComponent(0.5) + } else if let media = media as? TelegramMediaFile, media.isVideo && !media.isInstantVideo { + return self.presentation.colors.blackTransparent.withAlphaComponent(0.5) + } else if media is TelegramMediaMap { + return self.presentation.colors.blackTransparent.withAlphaComponent(0.5) + } else { + return self.presentation.chatServiceItemColor + } + } + + var stateOverlayTextColor: NSColor { + guard let media = self.message?.media.first else { + return self.presentation.chatServiceItemTextColor + } + if let file = media as? TelegramMediaFile, file.isInstantVideo { + return self.presentation.chatServiceItemTextColor + } else if media is TelegramMediaMap { + return NSColor(0xffffff) + } - if self.chatInteraction.isLogInteraction { - return .single([]) + if media.isInteractiveMedia { + return NSColor(0xffffff) + } else { + return self.presentation.chatServiceItemTextColor + } + } + var isInteractiveMedia: Bool { + guard let media = self.message?.media.first else { + return false } + return media.isInteractiveMedia + } +} + +func chatMenuItems(for message: Message, item: ChatRowItem, chatInteraction: ChatInteraction) -> Signal<[ContextMenuItem], NoError> { + + if let _ = message.adAttribute { + return .single([ContextMenuItem(L10n.chatMessageSponsoredWhat, handler: { + execute(inapp: .external(link: L10n.chatMessageSponsoredLink, false)) + })]) + } + + + + let account = chatInteraction.context.account + let context = chatInteraction.context + let peerId = chatInteraction.peerId + let peer = chatInteraction.peer + let messageId = message.id + if chatInteraction.isLogInteraction || chatInteraction.presentation.state == .selecting { + return .single([]) + } + switch chatInteraction.mode { + case .preview: + return .single([]) + default: + break + } + + var items:[ContextMenuItem] = [] + + + if (MessageReadMenuItem.canViewReadStats(message: message, chatInteraction: chatInteraction, appConfig: chatInteraction.context.appConfiguration)), message.flags.contains(.Incoming) || item.isRead { + let stats = MessageReadMenuItem(context: context, message: message) + + let item = ContextMenuItem("-") - var items:[ContextMenuItem] = [] - let chatInteraction = self.chatInteraction - if chatInteraction.presentation.state != .selecting { - if let message = message, let peer = peer { - let account = self.account! - - if peer.canSendMessage { - items.append(ContextMenuItem(tr(.messageContextReply), handler: { - chatInteraction.setupReplyMessage(message.id) - })) + item.contextObject = stats + item.view = stats.view + items.append(item) + + items.append(ContextSeparatorItem()) + + } + + if message.id.peerId == repliesPeerId, let author = message.chatPeer(context.peerId), author.id != context.peerId { + + let text = author.isUser ? L10n.chatContextBlockUser : L10n.chatContextBlockGroup + + items.append(ContextMenuItem(text, handler: { + + let header = author.isUser ? L10n.chatContextBlockUserHeader : L10n.chatContextBlockGroupHeader + let info = author.isUser ? L10n.chatContextBlockUserInfo(author.displayTitle) : L10n.chatContextBlockGroupInfo(author.displayTitle) + let third = author.isUser ? L10n.chatContextBlockUserThird : L10n.chatContextBlockGroupThird + let ok = author.isUser ? L10n.chatContextBlockUserOK : L10n.chatContextBlockGroupOK + let cancel = author.isUser ? L10n.chatContextBlockUserCancel : L10n.chatContextBlockGroupCancel + + modernConfirm(for: context.window, account: account, peerId: author.id, header: header, information: info, okTitle: ok, cancelTitle: cancel, thridTitle: third, thridAutoOn: true, successHandler: { result in + switch result { + case .thrid: + let block: Signal = context.blockedPeersContext.add(peerId: author.id) |> `catch` { _ in return .complete() } + + _ = showModalProgress(signal: combineLatest(context.engine.peers.reportPeerMessages(messageIds: [message.id], reason: .spam, message: ""), block), for: context.window).start() + case .basic: + _ = showModalProgress(signal: context.blockedPeersContext.add(peerId: author.id), for: context.window).start() } - if let peer = message.peers[message.id.peerId] as? TelegramChannel, peer.isSupergroup { - if let address = peer.addressName { - items.append(ContextMenuItem(tr(.messageContextCopyMessageLink), handler: { - copyToClipboard("t.me/\(address)/\(message.id.id)") - })) - } - if peer.hasAdminRights(.canPinMessages) { - items.append(ContextMenuItem(tr(.messageContextPin), handler: { - confirm(for: mainWindow, with: appName, and: tr(.messageContextConfirmPin), thridTitle: tr(.messageContextConfirmOnlyPin), successHandler: { result in - chatInteraction.updatePinned(message.id, false, result == .thrid) - }) - })) + }) + })) + items.append(ContextSeparatorItem()) + } + + + if message.isScheduledMessage, let peer = peer { + items.append(ContextMenuItem(L10n.chatContextScheduledSendNow, handler: { + _ = context.engine.messages.sendScheduledMessageNowInteractively(messageId: message.id).start() + })) + items.append(ContextMenuItem(L10n.chatContextScheduledReschedule, handler: { + showModal(with: DateSelectorModalController(context: context, defaultDate: Date(timeIntervalSince1970: TimeInterval(message.timestamp)), mode: .schedule(peer.id), selectedAt: { date in + _ = showModalProgress(signal: context.engine.messages.requestEditMessage(messageId: message.id, text: message.text, media: .keep, entities: message.textEntities, scheduleTime: Int32(min(date.timeIntervalSince1970, Double(scheduleWhenOnlineTimestamp)))), for: context.window).start(next: { result in + + }, error: { error in + + }) + }), for: context.window) + })) + items.append(ContextSeparatorItem()) + } + + if canReplyMessage(message, peerId: chatInteraction.peerId, mode: chatInteraction.mode) { + items.append(ContextMenuItem(tr(L10n.messageContextReply1) + (FastSettings.tooltipAbility(for: .edit) ? " (\(L10n.messageContextReplyHelp))" : ""), handler: { [unowned chatInteraction] in + chatInteraction.setupReplyMessage(message.id) + })) + } + if chatInteraction.mode.threadId == nil, let peer = message.peers[message.id.peerId] as? TelegramChannel, peer.isSupergroup { + if let attr = message.replyThread, attr.count > 0 { + var messageId: MessageId = message.id + var modeIsReplies = true + if let source = message.sourceReference { + messageId = source.messageId + if let peer = message.peers[source.messageId.peerId] { + if peer.isChannel { + modeIsReplies = false } } - - items.append(ContextSeparatorItem()) - - if canEditMessage(message, account:account) { - items.append(ContextMenuItem(tr(.messageContextEdit), handler: { - chatInteraction.beginEditingMessage(message) + } + + items.append(ContextMenuItem(modeIsReplies ? L10n.messageContextViewRepliesCountable(Int(attr.count)) : L10n.messageContextViewCommentsCountable(Int(attr.count)), handler: { [unowned chatInteraction] in + chatInteraction.openReplyThread(messageId, !modeIsReplies, true, modeIsReplies ? .replies(origin: messageId) : .comments(origin: messageId)) + })) + } +// if let attr = message.replyAttribute, let threadId = attr.threadMessageId, threadId != message.id { +// switch chatInteraction.presentation.discussionGroupId { +// case let .known(peerId): +// if peerId != nil { +// items.append(ContextMenuItem(L10n.messageContextViewThread, handler: { +// chatInteraction.openReplyThread(threadId, true, true, .replies(origin: message.id)) +// })) +// } +// default: +// break +// } +// } + } + + if let file = message.media.first as? TelegramMediaFile, file.isEmojiAnimatedSticker { + items.append(ContextMenuItem(L10n.textCopyText, handler: { + copyToClipboard(message.text) + })) + } + + if let peer = message.peers[message.id.peerId] as? TelegramChannel { + if !message.flags.contains(.Failed), !message.flags.contains(.Unsent), !message.isScheduledMessage { + items.append(ContextMenuItem(tr(L10n.messageContextCopyMessageLink1), handler: { [unowned chatInteraction] in + _ = showModalProgress(signal: context.engine.messages.exportMessageLink(peerId: peer.id, messageId: message.id, isThread: chatInteraction.mode.threadId != nil), for: context.window).start(next: { link in + if let link = link { + copyToClipboard(link) + } + }) + + })) + } + } + + items.append(ContextSeparatorItem()) + + if canEditMessage(message, chatInteraction: chatInteraction, context: context), chatInteraction.mode != .pinned { + items.append(ContextMenuItem(tr(L10n.messageContextEdit), handler: { [unowned chatInteraction] in + chatInteraction.beginEditingMessage(message) + })) + } + + if !message.isScheduledMessage, let peer = message.peers[message.id.peerId], !peer.isDeleted, message.id.namespace == Namespaces.Message.Cloud, peerId == message.id.peerId { + + let needUnpin = chatInteraction.presentation.pinnedMessageId?.others.contains(message.id) == true + let pinAndOld: Bool + if let pinnedMessage = chatInteraction.presentation.pinnedMessageId, let last = pinnedMessage.others.last { + pinAndOld = last > message.id + } else { + pinAndOld = false + } + let pinText = message.tags.contains(.pinned) ? L10n.messageContextUnpin : L10n.messageContextPin + + if let peer = message.peers[message.id.peerId] as? TelegramChannel, peer.hasPermission(.pinMessages) || (peer.isChannel && peer.hasPermission(.editAllMessages)) { + if !message.flags.contains(.Unsent) && !message.flags.contains(.Failed) { + if !chatInteraction.mode.isThreadMode, (needUnpin || chatInteraction.mode != .pinned) { + items.append(ContextMenuItem(pinText, handler: { + if peer.isSupergroup, !needUnpin { + modernConfirm(for: context.window, account: account, peerId: nil, information: pinAndOld ? L10n.chatConfirmPinOld : L10n.messageContextConfirmPin1, okTitle: L10n.messageContextPin, thridTitle: pinAndOld ? nil : L10n.messageContextConfirmNotifyPin, successHandler: { [unowned chatInteraction] result in + chatInteraction.updatePinned(message.id, chatInteraction.presentation.pinnedMessageId?.others.contains(message.id) == true, result != .thrid, false) + }) + } else { + chatInteraction.updatePinned(message.id, needUnpin, true, false) + } })) } - - if canForwardMessage(message, account: account) { - items.append(ContextMenuItem(tr(.messageContextForward), handler: { - chatInteraction.forwardMessages([message.id]) - })) + } + } else if message.id.peerId == account.peerId { + items.append(ContextMenuItem(pinText, handler: { [unowned chatInteraction] in + chatInteraction.updatePinned(message.id, needUnpin, true, false) + })) + } else if let peer = message.peers[message.id.peerId] as? TelegramGroup, peer.canPinMessage, (needUnpin || chatInteraction.mode != .pinned) { + items.append(ContextMenuItem(pinText, handler: { [unowned chatInteraction] in + if !needUnpin { + modernConfirm(for: context.window, account: account, peerId: nil, information: pinAndOld ? L10n.chatConfirmPinOld : L10n.messageContextConfirmPin1, okTitle: L10n.messageContextPin, thridTitle: pinAndOld ? nil : L10n.messageContextConfirmNotifyPin, successHandler: { result in + chatInteraction.updatePinned(message.id, needUnpin, result == .thrid, false) + }) + } else { + chatInteraction.updatePinned(message.id, needUnpin, false, false) } - - if canDeleteMessage(message, account: account) { - items.append(ContextMenuItem(tr(.messageContextDelete), handler: { - chatInteraction.deleteMessages([message.id]) - })) + })) + } else if chatInteraction.presentation.canPinMessage, let peer = chatInteraction.peer, (needUnpin || chatInteraction.mode != .pinned) { + items.append(ContextMenuItem(pinText, handler: { + if !needUnpin { + modernConfirm(for: context.window, account: account, peerId: nil, information: pinAndOld ? L10n.chatConfirmPinOld : L10n.messageContextConfirmPin1, okTitle: L10n.messageContextPin, thridTitle: L10n.chatConfirmPinFor(peer.displayTitle), thridAutoOn: false, successHandler: { result in + chatInteraction.updatePinned(message.id, needUnpin, false, result != .thrid) + }) + } else { + chatInteraction.updatePinned(message.id, needUnpin, false, false) } + })) + } + } + + + if canForwardMessage(message, chatInteraction: chatInteraction) { + let forwardItem = ContextMenuItem(L10n.messageContextForward, handler: { [unowned chatInteraction] in + chatInteraction.forwardMessages([message.id]) + }) + let forwardMenu = NSMenu() + + let dialogs: Signal<[Peer], NoError> = context.account.postbox.tailChatListView(groupId: .root, count: 25, summaryComponents: .init()) + |> take(1) + |> map { view in + return view.0.entries.compactMap { entry in + switch entry { + case let .MessageEntry(_, _, _, _, _, renderedPeer, _, _, _, _): + return renderedPeer.peer + default: + return nil + } + } + } + |> deliverOnMainQueue + + + let recent: Signal<[Peer], NoError> = context.recentlyUserPeerIds |> mapToSignal { ids in + return context.account.postbox.transaction { transaction in + let peers = ids.compactMap { transaction.getPeer($0) } + return Array(peers.map { $0 }) + } + } + |> take(1) + |> deliverOnMainQueue + let favorite: Signal<[Peer], NoError> = context.engine.peers.recentPeers() |> map { recent in + switch recent { + case .disabled: + return [] + case let .peers(peers): + return Array(peers.map { $0 }) + } + } + |> take(1) + |> deliverOnMainQueue + + let accountPeer = context.account.postbox.loadedPeerWithId(context.peerId) |> deliverOnMainQueue - - items.append(ContextMenuItem(tr(.messageContextSelect), handler: { - chatInteraction.update({$0.withToggledSelectedMessage(message.id)}) - })) - + _ = combineLatest(queue: .mainQueue(), dialogs, recent, favorite, accountPeer).start(next: { dialogs, recent, favorite, accountPeer in + + let forwardObject = ForwardMessagesObject(context, messageIds: [message.id]) + + let recent = recent.filter { + $0.id != context.peerId && $0.canSendMessage() + }.prefix(5) + + let favorite = favorite.filter { + !recent.map { $0.id }.contains($0.id) && $0.id != context.peerId && $0.canSendMessage() + }.prefix(5) + + let dialogs = dialogs.reversed().filter { + !(recent + favorite).map { $0.id }.contains($0.id) + && $0.id != context.peerId + && $0.canSendMessage() + }.prefix(5) + + var items:[ContextMenuItem] = [] + + func makeItem(_ peer: Peer) -> ContextMenuItem { + let title = peer.id == context.peerId ? L10n.peerSavedMessages : peer.displayTitle.prefixWithDots(25) + let item = ContextMenuItem(title, handler: { + _ = forwardObject.perform(to: [peer.id]).start() + }) + let signal:Signal<(CGImage?, Bool), NoError> + if peer.id == context.peerId { + let icon = theme.icons.searchSaved + signal = generateEmptyPhoto(NSMakeSize(15, 15), type: .icon(colors: theme.colors.peerColors(5), icon: icon, iconSize: icon.backingSize.aspectFitted(NSMakeSize(10, 10)), cornerRadius: nil)) |> deliverOnMainQueue |> map { ($0, true) } + } else { + signal = peerAvatarImage(account: context.account, photo: .peer(peer, peer.smallProfileImage, peer.displayLetters, message), displayDimensions: NSMakeSize(30, 30), scale: 1, font: .avatar(7), genCap: true, synchronousLoad: false) |> deliverOnMainQueue + } - if canForwardMessage(message, account: account) { - items.append(ContextSeparatorItem()) - items.append(ContextMenuItem(tr(.messageContextForwardToCloud), handler: { - _ = Sender.forwardMessages(messageIds: [message.id], account: account, peerId: account.peerId).start() - })) - + _ = signal.start(next: { [weak item] image, _ in + DispatchQueue.main.async { + item?.image = image?._NSImage + } + }) + return item + } + + + items.append(makeItem(accountPeer)) + if !recent.isEmpty || !dialogs.isEmpty || !favorite.isEmpty { + items.append(ContextSeparatorItem()) + } + for peer in recent { + items.append(makeItem(peer)) + } + if !recent.isEmpty { + items.append(ContextSeparatorItem()) + } + for peer in favorite { + items.append(makeItem(peer)) + } + if (!favorite.isEmpty || !recent.isEmpty) && !dialogs.isEmpty { + items.append(ContextSeparatorItem()) + } + for peer in dialogs { + items.append(makeItem(peer)) + } + if !items.isEmpty { + items.append(ContextSeparatorItem()) + let more = ContextMenuItem(L10n.chatContextForwardMore, handler: { [unowned chatInteraction] in + chatInteraction.forwardMessages([message.id]) + }) + items.append(more) + } + for item in items { + forwardMenu.addItem(item) + } + }) + forwardItem.submenu = forwardMenu + items.append(forwardItem) + } else if message.id.peerId.namespace == Namespaces.Peer.SecretChat, !message.containsSecretMedia { + items.append(ContextMenuItem(L10n.messageContextShare, handler: { [unowned chatInteraction] in + chatInteraction.forwardMessages([message.id]) + })) + } + + if canDeleteMessage(message, account: account, mode: chatInteraction.mode) { + items.append(ContextMenuItem(tr(L10n.messageContextDelete), handler: { [unowned chatInteraction] in + chatInteraction.deleteMessages([message.id]) + })) + } + + if chatInteraction.mode.threadId != message.id { + items.append(ContextMenuItem(tr(L10n.messageContextSelect), handler: { [unowned chatInteraction] in + chatInteraction.withToggledSelectedMessage({$0.withToggledSelectedMessage(message.id)}) + })) + } + + + + + if canForwardMessage(message, chatInteraction: chatInteraction), chatInteraction.peerId != account.peerId, chatInteraction.mode == .history { + items.append(ContextMenuItem(tr(L10n.messageContextForwardToCloud), handler: { [unowned chatInteraction] in + _ = Sender.forwardMessages(messageIds: [message.id], context: chatInteraction.context, peerId: account.peerId).start() + })) + items.append(ContextSeparatorItem()) + } + + + + + + + var signal:Signal<[ContextMenuItem], NoError> = .single(items) + + + if let file = message.media.first as? TelegramMediaFile, let mediaId = file.id { + signal = signal |> mapToSignal { items -> Signal<[ContextMenuItem], NoError> in + var items = items + + return account.postbox.transaction { transaction -> [ContextMenuItem] in + if file.isAnimated && file.isVideo { + let gifItems = transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs).compactMap {$0.contents as? RecentMediaItem} + if let _ = gifItems.firstIndex(where: {$0.media.id == mediaId}) { + items.append(ContextMenuItem(L10n.messageContextRemoveGif, handler: { + let _ = removeSavedGif(postbox: account.postbox, mediaId: mediaId).start() + })) + } else { + items.append(ContextMenuItem(L10n.messageContextSaveGif, handler: { + let _ = addSavedGif(postbox: account.postbox, fileReference: FileMediaReference.message(message: MessageReference(message), media: file)).start() + })) + } } - + return items + } |> mapToSignal { items in + var items = items - for media in message.media { - if let file = media as? TelegramMediaFile { - if file.isVideo && file.isAnimated { - - if !canForwardMessage(message, account: account) { - items.append(ContextSeparatorItem()) + return combineLatest(queue: .mainQueue(), account.postbox.mediaBox.resourceData(file.resource), fileFinderPath(file, context.account.postbox)) |> mapToSignal { data, downloadPath in + if !file.isInteractiveMedia && !file.isVoice && !file.isMusic && !file.isStaticSticker && !file.isGraphicFile && !file.isAnimatedSticker { + let quickLook = ContextMenuItem(L10n.contextOpenInQuickLook, handler: { + FastSettings.toggleOpenInQuickLook(fileExtenstion(file)) + }) + quickLook.state = FastSettings.openInQuickLook(fileExtenstion(file)) ? .on : .off + items.append(quickLook) + } + + if data.complete, !message.containsSecretMedia { + items.append(ContextMenuItem(tr(L10n.contextCopyMedia), handler: { + saveAs(file, account: account) + })) + + #if BETA || ALPHA || DEBUG + if file.isAnimatedSticker, let data = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { + items.append(ContextMenuItem("Copy thumbnail (Dev.)", handler: { + _ = getAnimatedStickerThumb(data: data).start(next: { path in + if let path = path { + let pb = NSPasteboard.general + pb.clearContents() + pb.writeObjects([NSURL(fileURLWithPath: path)]) + } + }) + })) + } + #endif + + if let downloadPath = downloadPath { + if !file.isVoice { + let path: String + if FileManager.default.fileExists(atPath: downloadPath) { + path = downloadPath + } else { + path = data.path + "." + fileExtenstion(file) + try? FileManager.default.removeItem(atPath: path) + try? FileManager.default.linkItem(atPath: data.path, toPath: path) + } + let result = ObjcUtils.apps(forFileUrl: path) + if let result = result, !result.isEmpty { + let item = ContextMenuItem(L10n.messageContextOpenWith, handler: {}) + let menu = NSMenu() + item.submenu = menu + for item in result { + menu.addItem(ContextMenuItem(item.fullname, handler: { + NSWorkspace.shared.openFile(path, withApplication: item.app.path) + }, image: item.icon)) + } + items.append(item) + } } - - items.append(ContextMenuItem(tr(.messageContextSaveGif), handler: { - let _ = addSavedGif(postbox: account.postbox, file: file).start() + } + + } + + if file.isStaticSticker, let fileId = file.id { + return account.postbox.transaction { transaction -> [ContextMenuItem] in + let saved = getIsStickerSaved(transaction: transaction, fileId: fileId) + items.append(ContextMenuItem( !saved ? tr(L10n.chatContextAddFavoriteSticker) : tr(L10n.chatContextRemoveFavoriteSticker), handler: { + + if !saved { + _ = addSavedSticker(postbox: account.postbox, network: account.network, file: file).start() + } else { + _ = removeSavedSticker(postbox: account.postbox, mediaId: fileId).start() + } })) + + return items } } + + return .single(items) } - + } + + + } + } else if let image = message.media.first as? TelegramMediaImage, !message.containsSecretMedia { + signal = signal |> mapToSignal { items -> Signal<[ContextMenuItem], NoError> in + var items = items + if let resource = image.representations.last?.resource { + return account.postbox.mediaBox.resourceData(resource) |> take(1) |> deliverOnMainQueue |> map { data in + if data.complete { + items.append(ContextMenuItem(tr(L10n.galleryContextCopyToClipboard), handler: { + if let path = link(path: data.path, ext: "jpg") { + let pb = NSPasteboard.general + pb.clearContents() + pb.writeObjects([NSURL(fileURLWithPath: path)]) + } + })) + items.append(ContextMenuItem(tr(L10n.contextCopyMedia), handler: { + savePanel(file: data.path, ext: "jpg", for: mainWindow) + })) + } + return items + } + } else { + return .single(items) } } - - return .single(items) } -} - + + + signal = signal |> map { [unowned chatInteraction] items in + if let peer = chatInteraction.peer as? TelegramChannel, peer.isSupergroup, chatInteraction.mode == .history { + if peer.hasPermission(.banMembers), let author = message.author, author.id != account.peerId, message.isIncoming(account, theme.bubbled) { + var items = items + items.append(ContextMenuItem(L10n.chatContextRestrict, handler: { + _ = showModalProgress(signal: context.engine.peers.fetchChannelParticipant(peerId: chatInteraction.peerId, participantId: author.id), for: mainWindow).start(next: { participant in + if let participant = participant { + switch participant { + case let .member(memberId, _, _, _, _): + showModal(with: RestrictedModalViewController(context, peerId: peerId, memberId: memberId, initialParticipant: participant, updated: { updatedRights in + _ = context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(peerId: peerId, memberId: author.id, bannedRights: updatedRights).start() + }), for: context.window) + default: + break + } + } + }) + })) + return items + } + } + return items + } +// + signal = signal |> map { [unowned chatInteraction] items in + var items = items + if canReportMessage(message, account), chatInteraction.mode != .pinned { + items.append(ContextMenuItem(L10n.messageContextReport, handler: { + _ = reportReasonSelector(context: context).start(next: { value in + _ = showModalProgress(signal: context.engine.peers.reportPeerMessages(messageIds: [message.id], reason: value.reason, message: value.comment), for: context.window).start(completed: { + alert(for: context.window, info: L10n.messageContextReportAlertOK) + }) + }) + })) + } + return items + } + + signal = signal |> map { [unowned chatInteraction] items in + var items = items + if let peer = peer, peer.isGroup || peer.isSupergroup, let author = message.author, chatInteraction.mode == .history { + items.append(ContextSeparatorItem()) + items.append(ContextMenuItem(L10n.chatServiceSearchAllMessages(author.compactDisplayTitle), handler: { + chatInteraction.searchPeerMessages(author) + })) + } + return items + } + + signal = signal |> mapToSignal { items in + return account.pendingUpdateMessageManager.updatingMessageMedia |> take(1) |> deliverOnMainQueue |> map { + $0[messageId] != nil + } |> map { editing in + if editing { + var items = items + items.append(ContextSeparatorItem()) + items.append(ContextMenuItem(L10n.chatContextCancelEditing, handler: { + account.pendingUpdateMessageManager.cancel(messageId: messageId) + })) + return items + } else { + return items + } + } + } + + return signal +} diff --git a/Telegram-Mac/ChatRowView.swift b/Telegram-Mac/ChatRowView.swift index 21f2170068..b4456751ca 100644 --- a/Telegram-Mac/ChatRowView.swift +++ b/Telegram-Mac/ChatRowView.swift @@ -8,14 +8,19 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac +import TelegramCore + +import Postbox +import SwiftSignalKit -private class ChatRowAnimateView: View { - var stableId:AnyHashable? -} -class ChatRowView: TableRowView, Notifable, MultipleSelectable { + +class ChatRowView: TableRowView, Notifable, MultipleSelectable, ViewDisplayDelegate, RevealTableView { + + struct CaptionView { + let id: UInt32 + let view: TextView + } var header: String? { @@ -29,34 +34,108 @@ class ChatRowView: TableRowView, Notifable, MultipleSelectable { private var avatar:AvatarControl? - var contentView:View = View() + private(set) var contentView:View = View() private var replyView:ChatAccessoryView? private var replyMarkupView:View? - private var forwardName:TextView? - private var captionView:TextView? - private var shareControl:ImageButton? + private(set) var forwardName:TextView? + private(set) var captionViews: [CaptionView] = [] + private var shareView:ImageButton? + private var likeView:ImageButton? + private var channelCommentsBubbleControl: ChannelCommentsBubbleControl? + private var channelCommentsBubbleSmallControl: ChannelCommentsSmallControl? + private var channelCommentsControl: ChannelCommentsControl? + private var nameView:TextView? - private var rightView:ChatRightView = ChatRightView(frame:NSZeroRect) - private var selectingView:SelectingControl? + private var adminBadge: TextView? + let rightView:ChatRightView = ChatRightView(frame:NSZeroRect) + private(set) var selectingView:SelectingControl? + private var mouseDragged: Bool = false + private var animatedView:RowAnimateView? + + private var forwardAccessory: ChatBubbleAccessoryForward? = nil + private var viaAccessory: ChatBubbleViaAccessory? = nil + + let bubbleView = ChatMessageBubbleBackdrop() - private var animatedView:ChatRowAnimateView? + private var scamButton: ImageButton? = nil + private var scamForwardButton: ImageButton? = nil + private var psaButton: ImageButton? = nil + private var hasBeenLayout: Bool = false + + let rowView: View + var photoView: NSView? { + return self.avatar + } + required init(frame frameRect: NSRect) { + rowView = View(frame: NSMakeRect(0, 0, frameRect.width, frameRect.height)) super.init(frame: frameRect) - super.addSubview(rightView) - super.addSubview(contentView) + super.addSubview(rowView) + + + + rowView.addSubview(bubbleView) + rowView.addSubview(contentView) + rowView.addSubview(rightView) + + rowView.displayDelegate = self + + super.addSubview(swipingRightView) + - } - var selectableTextViews: [TextView] { - if let captionView = captionView { - return [captionView] + override func setFrameSize(_ newSize: NSSize) { + if !inLiveResize || !NSIsEmptyRect(visibleRect) { + super.setFrameSize(newSize) + rowView.setFrameSize(newSize) + } + + } + + override func setFrameOrigin(_ newOrigin: NSPoint) { + let oldOrigin = self.frame.origin + super.setFrameOrigin(newOrigin) + + if oldOrigin != newOrigin, oldOrigin == .zero { + updateBackground(animated: false, item: self.item) + } + } + + func updateBackground(animated: Bool, item: TableRowItem?, rotated: Bool = false, clean: Bool = false) -> Void { + + guard let item = item as? ChatRowItem else { + return } - return [] + + let gradientRect = item.chatInteraction.getGradientOffsetRect() + let size = NSMakeSize(gradientRect.width, gradientRect.height + 60) + + let inset = size.height - gradientRect.minY + (frame.height - bubbleFrame(item).maxY) - 30 + let animated = animated && visibleRect.height > 0 && !clean && self.layer?.animation(forKey: "position") == nil + let rect = self.frame + bubbleView.update(rect: rect.offsetBy(dx: 0, dy: inset), within: size, animated: animated, rotated: rotated) + } + +// func updateFloatingPhoto() { +// if let item = self.item as? ChatRowItem, let table = item.table { +// +// } +// } +// + var selectableTextViews: [TextView] { + return captionViews.map { $0.view } + } + + func clickInContent(point: NSPoint) -> Bool { + guard let item = item as? ChatRowItem, let layout = item.captionLayouts.first?.layout, let captionView = captionViews.first else {return true} + let point = captionView.view.convert(point, from: self) + let index = layout.findIndex(location: point) + return point.x < layout.lines[index].frame.maxX } func isEqual(to other: Notifable) -> Bool { @@ -68,9 +147,8 @@ class ChatRowView: TableRowView, Notifable, MultipleSelectable { func notify(with value: Any, oldValue: Any, animated:Bool) { if let value = value as? ChatPresentationInterfaceState, let oldValue = oldValue as? ChatPresentationInterfaceState { - if (value.selectionState != nil && oldValue.selectionState == nil) || (value.selectionState == nil && oldValue.selectionState != nil) { - updateSelectingState(!NSIsEmptyRect(visibleRect), selectingMode:value.selectionState != nil, item: self.item as? ChatRowItem) - updateColors() + if (value.selectionState != oldValue.selectionState) { + updateSelectingState(!NSIsEmptyRect(visibleRect), selectingMode:value.selectionState != nil, item: self.item as? ChatRowItem, needUpdateColors: true) self.needsLayout = true } else if let item = item as? ChatRowItem, let message = item.message { if value.selectionState?.selectedIds.contains(message.id) != oldValue.selectionState?.selectedIds.contains(message.id) { @@ -86,52 +164,81 @@ class ChatRowView: TableRowView, Notifable, MultipleSelectable { } - func updateSelectingState(_ animated:Bool = false, selectingMode:Bool, item: ChatRowItem?) { + func updateSelectingState(_ animated:Bool = false, selectingMode:Bool, item: ChatRowItem?, needUpdateColors: Bool) { + + let selectingMode = selectingMode && item?.chatInteraction.mode.threadId != item?.message?.id + if let item = item { let defRight = frame.width - item.rightSize.width - item.rightInset - rightView.change(pos: NSMakePoint(defRight, rightView.frame.minY), animated: animated) + + if !item.isBubbled { + rightView.change(pos: NSMakePoint(defRight, rightView.frame.minY), animated: animated) + if let control = channelCommentsControl { + let x = defRight - control.frame.width - 4 + control.change(pos: NSMakePoint(x, control.frame.minY), animated: animated) + } + } else { + if rowView.frame.origin != rowPoint(item) { + rowView.change(pos: rowPoint(item), animated: animated) + } + } + + + updateMouse() if selectingMode { + let force: Bool = selectingView == nil if selectingView == nil { - selectingView = SelectingControl(unselectedImage: theme.icons.chatToggleUnselected, selectedImage: theme.icons.chatToggleSelected) - selectingView?.setFrameOrigin(NSMakePoint(frame.width, item.defaultContentTopOffset - 1)) + selectingView = SelectingControl(unselectedImage: item.presentation.chat_toggle_unselected, selectedImage: item.presentation.chat_toggle_selected, selected: item.isSelectedMessage) + selectingView?.setFrameOrigin(NSMakePoint(frame.width, selectingPoint(item).y)) + selectingView?.layer?.opacity = 0 super.addSubview(selectingView!) } - if animated { - selectingView?.layer?.removeAnimation(forKey: "opacity") - selectingView?.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + if selectingView!.isSelected != item.isSelectedMessage || force { + selectingView?.change(opacity: 1.0, animated: animated) + selectingView?.change(pos: selectingPoint(item), animated: animated) } - - selectingView?.change(pos: NSMakePoint(rightView.frame.maxX + 4,item.defaultContentTopOffset - 1), animated: animated) - } else { + } else { if animated { selectingView?.layer?.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion:false, completion:{ [weak self] (completed) in - if completed { + //if completed { self?.selectingView?.removeFromSuperview() self?.selectingView = nil - } + //} }) } else { self.selectingView?.removeFromSuperview() self.selectingView = nil } - - selectingView?.change(pos: NSMakePoint(frame.width,item.defaultContentTopOffset - 1), animated: animated) + selectingView?.change(pos: NSMakePoint(frame.width, selectingPoint(item).y), animated: animated) } - if let selectionState = item.chatInteraction.presentation.selectionState, let message = item.message { - selectingView?.set(selected: selectionState.selectedIds.contains(message.id), animated: animated) + + updateSelectionViewAfterUpdateState(item: item, animated: animated) + if needUpdateColors { + renderLayoutType(item, animated: animated) updateColors() } - if item.chatInteraction.presentation.state == .selecting { + if item.chatInteraction.presentation.state == .selecting || item.disableInteractions { disableHierarchyInteraction() } else { restoreHierarchyInteraction() } + + self.channelCommentsControl?.isEnabled = !item.isFailed && !item.isUnsent && item.chatInteraction.presentation.state != .selecting + self.channelCommentsBubbleSmallControl?.isEnabled = !item.isFailed && !item.isUnsent && item.chatInteraction.presentation.state != .selecting + self.channelCommentsBubbleControl?.isEnabled = !item.isFailed && !item.isUnsent && item.chatInteraction.presentation.state != .selecting } } + func updateSelectionViewAfterUpdateState(item: ChatRowItem, animated: Bool) { + + if let selectionState = item.chatInteraction.presentation.selectionState, let message = item.message { + selectingView?.set(selected: selectionState.selectedIds.contains(message.id), animated: animated) + } + } + func canStartTextSelecting(_ event:NSEvent) -> Bool { return false } @@ -141,87 +248,198 @@ class ChatRowView: TableRowView, Notifable, MultipleSelectable { } override var isSelect: Bool { - if let item = item as? ChatRowItem, let message = item.message, let selectionState = item.chatInteraction.presentation.selectionState { + if let item = item as? ChatRowItem { + return isSelectedItem(item) + } + return false + } + + private func isSelectedItem(_ item: ChatRowItem) -> Bool { + if let message = item.message, let selectionState = item.chatInteraction.presentation.selectionState { return selectionState.selectedIds.contains(message.id) } return false } + func isSelectInGroup(_ location: NSPoint) -> Bool { + return isSelect + } + override var backdorColor: NSColor { - return contextMenu != nil || isSelect ? theme.colors.selectMessage : theme.colors.background + guard let item = item as? ChatRowItem else {return super.backdorColor} + if let forceBackgroundColor = item.forceBackgroundColor { + return forceBackgroundColor + } + return item.renderType == .bubble ? .clear : contextMenu != nil || isSelect ? item.presentation.colors.selectMessage : item.presentation.chatBackground + } + + var contentColor: NSColor { + guard let item = item as? ChatRowItem else {return backdorColor} + + if item.hasBubble { + //System.supportsTransparentFontDrawing ? .clear : + return item.presentation.chat.backgroundColor(item.isIncoming, item.renderType == .bubble) + //return .clear//isSelect || contextMenu != nil ? item.presentation.chat.backgoundSelectedColor(item.isIncoming, item.renderType == .bubble) : item.presentation.chat.backgroundColor(item.isIncoming, item.renderType == .bubble) + } else { + return backdorColor//backdorColor + } } + override func updateColors() -> Void { + super.updateColors() - rightView.backgroundColor = backdorColor - contentView.backgroundColor = backdorColor - replyView?.backgroundColor = backdorColor - nameView?.backgroundColor = backdorColor - forwardName?.backgroundColor = backdorColor - captionView?.backgroundColor = backdorColor + guard let item = item as? ChatRowItem else {return} + + rowView.backgroundColor = backdorColor + rightView.backgroundColor = item.isStateOverlayLayout ? .clear : contentColor + contentView.backgroundColor = .clear + item.replyModel?.backgroundColor = item.hasBubble ? contentColor : item.isBubbled ? item.presentation.colors.bubbleBackground_incoming : contentColor + nameView?.backgroundColor = contentColor + forwardName?.backgroundColor = contentColor + for captionView in captionViews { + captionView.view.backgroundColor = contentColor + } replyMarkupView?.backgroundColor = backdorColor - self.backgroundColor = backdorColor + bubbleView.background = item.presentation.chat.bubbleBackgroundColor(item.isIncoming, item.hasBubble) + + if let control = channelCommentsControl { + control.set(background: contentColor, for: .Normal) + } + if let control = channelCommentsBubbleControl { + control.set(background: .clear, for: .Normal) + control.set(background: item.presentation.colors.accent.withAlphaComponent(0.08), for: .Hover) + control.set(background: item.presentation.colors.accent.withAlphaComponent(0.16), for: .Highlight) + } + if let control = channelCommentsBubbleSmallControl { + control.set(background: item.presentation.chatServiceItemColor, for: .Normal) + } + + for view in contentView.subviews { - if let view = view as? View { - view.backgroundColor = backdorColor + if let view = view as? View, !view.isDynamicColorUpdateLocked { + view.backgroundColor = contentColor } } - if let item = item as? ChatRowItem { - item.replyModel?.setNeedDisplay() - } } + override func mouseDragged(with event: NSEvent) { + super.mouseDragged(with: event) + mouseDragged = true + } + override func mouseDown(with event: NSEvent) { super.mouseDown(with: event) + mouseDragged = false + } + + override func mouseUp(with event: NSEvent) { + super.mouseUp(with: event) - if let item = item as? ChatRowItem, !item.chatInteraction.isLogInteraction, !item.sending { + if let item = item as? ChatRowItem, !item.chatInteraction.isLogInteraction && !item.chatInteraction.disableSelectAbility, !item.sending, mouseInside(), !mouseDragged { - if item.chatInteraction.presentation.state == .selecting, let message = item.message { - item.chatInteraction.update({$0.withToggledSelectedMessage(message.id)}) - } else if let message = item.message { + if item.chatInteraction.presentation.state == .selecting { + forceSelectItem(item, onRightClick: false) + } else { let location = self.convert(event.locationInWindow, from: nil) if NSPointInRect(location, rightView.frame) { - if message.flags.contains(.Failed) { - confirm(for: mainWindow, with: tr(.alertSendErrorHeader), and: tr(.alertSendErrorText), okTitle: tr(.alertSendErrorResend), cancelTitle: tr(.alertSendErrorIgnore), thridTitle: tr(.alertSendErrorDelete), successHandler: { result in + if item.isFailed, let messageId = item.message?.id { + + + + let signal = item.context.account.postbox.transaction { transaction -> [MessageId] in + return transaction.getMessageFailedGroup(messageId)?.compactMap({$0.id}) ?? [] + } |> deliverOnMainQueue + + + _ = signal.start(next: { ids in + let alert:NSAlert = NSAlert() + alert.window.appearance = theme.appearance + alert.alertStyle = .informational + alert.messageText = L10n.alertSendErrorHeader + alert.informativeText = L10n.alertSendErrorText + + - switch result { - case .thrid: - item.deleteMessage() - default: - item.resendMessage() + alert.addButton(withTitle: L10n.alertSendErrorResend) + + if ids.count > 1 { + alert.addButton(withTitle: L10n.alertSendErrorResendItemsCountable(ids.count)) } + alert.addButton(withTitle: L10n.alertSendErrorDelete) + + + alert.addButton(withTitle: L10n.alertSendErrorIgnore) + + + alert.beginSheetModal(for: mainWindow, completionHandler: { [weak item] response in + switch response.rawValue { + case 1000: + item?.resendMessage([messageId]) + case 1001: + if ids.count > 1 { + item?.resendMessage(ids) + } else { + item?.deleteMessage() + } + case 1002: + if ids.count > 1 { + item?.deleteMessage() + } + default: + break + } + }) }) } else { - item.chatInteraction.update({$0.withToggledSelectedMessage(message.id)}) + forceSelectItem(item, onRightClick: true) } } } } } + func forceSelectItem(_ item: ChatRowItem, onRightClick: Bool) { + if let message = item.message, item.isSelectable { + item.chatInteraction.withToggledSelectedMessage({$0.withToggledSelectedMessage(message.id)}) + } + } + override func onShowContextMenu() { + guard let item = item as? ChatRowItem else {return} + renderLayoutType(item, animated: true) + updateColors() + item.chatInteraction.focusInputField() super.onCloseContextMenu() } override func onCloseContextMenu() { + guard let item = item as? ChatRowItem else {return} + renderLayoutType(item, animated: true) + self.rowView.change(pos: NSZeroPoint, animated: true) updateColors() super.onCloseContextMenu() } override func draw(_ layer: CALayer, in ctx: CGContext) { - super.draw(layer, in: ctx) + // super.draw(layer, in: ctx) if let item = self.item as? ChatRowItem { - if let fwdHeader = item.forwardHeader { - fwdHeader.1.draw(NSMakeRect(item.defLeftInset, item.forwardHeaderInset.y, fwdHeader.0.size.width, fwdHeader.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor) + if let fwdHeader = item.forwardHeader, !item.isBubbled, layer == rowView.layer { + let rect = NSMakeRect(item.defLeftInset, item.forwardHeaderInset.y, fwdHeader.0.size.width, fwdHeader.0.size.height) + if backingScaleFactor == 1.0 { + ctx.setFillColor(contentColor.cgColor) + ctx.fill(rect) + } + fwdHeader.1.draw(rect, in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) } let radius:CGFloat = 1.0 @@ -230,8 +448,15 @@ class ChatRowView: TableRowView, Notifable, MultipleSelectable { // ctx.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: layer.bounds.height - radius * 2), size: CGSize(width: radius + radius, height: radius + radius))) //draw separator - if let fwdType = item.forwardType { - ctx.setFillColor(theme.colors.blueFill.cgColor) + if let fwdType = item.forwardType, !item.isBubbled, layer == rowView.layer { + + let color: NSColor + if item.isPsa { + color = item.presentation.colors.greenUI + } else { + color = item.presentation.colors.link + } + ctx.setFillColor(color.cgColor) switch fwdType { case .ShortHeader: let height = frame.height - item.forwardNameInset.y - item.defaultContentTopOffset @@ -239,8 +464,6 @@ class ChatRowView: TableRowView, Notifable, MultipleSelectable { ctx.fillEllipse(in: CGRect(origin: CGPoint(x: item.defLeftInset, y: item.forwardNameInset.y), size: CGSize(width: radius + radius, height: radius + radius))) ctx.fillEllipse(in: CGRect(origin: CGPoint(x: item.defLeftInset, y: item.forwardNameInset.y + height - radius * 2), size: CGSize(width: radius + radius, height: radius + radius))) - - break case .FullHeader: ctx.fill(NSMakeRect(item.defLeftInset, item.forwardNameInset.y + radius, 2, frame.height - item.forwardNameInset.y - radius)) @@ -256,28 +479,24 @@ class ChatRowView: TableRowView, Notifable, MultipleSelectable { } } - - if item.isGame { - ctx.setFillColor(theme.colors.blueFill.cgColor) - let height = frame.height - item.gameInset.y - item.defaultContentTopOffset - ctx.fill(NSMakeRect(item.gameInset.x, item.gameInset.y + radius, 2, height - radius * 2)) - - ctx.fillEllipse(in: CGRect(origin: CGPoint(x: item.gameInset.x, y: item.gameInset.y), size: CGSize(width: radius + radius, height: radius + radius))) - ctx.fillEllipse(in: CGRect(origin: CGPoint(x: item.gameInset.x, y: item.gameInset.y + height - radius * 2), size: CGSize(width: radius + radius, height: radius + radius))) - } + } } override func updateMouse() { - if let shareControl = self.shareControl, let item = item as? ChatRowItem { - shareControl.change(opacity: item.chatInteraction.presentation.state != .selecting && mouseInside() ? 1.0 : 0.0, animated: true) + if let shareView = self.shareView, let item = item as? ChatRowItem { + shareView.change(opacity: item.chatInteraction.presentation.state != .selecting && mouseInside() ? 1.0 : 0.0, animated: true) + } + if let commentsView = self.channelCommentsBubbleSmallControl, let item = item as? ChatRowItem { + commentsView.change(opacity: item.chatInteraction.presentation.state != .selecting && mouseInside() ? 1.0 : 0.0, animated: true) + } + if let likeControl = self.likeView, let item = item as? ChatRowItem { + likeControl.change(opacity: item.chatInteraction.presentation.state != .selecting && mouseInside() ? 1.0 : 0.0, animated: true) } } - var contentFrame:NSRect { - return self.contentView.frame - } + override func addSubview(_ view: NSView) { self.contentView.addSubview(view) @@ -289,26 +508,25 @@ class ChatRowView: TableRowView, Notifable, MultipleSelectable { if replyView == nil { replyView = ChatAccessoryView() - replyView?.backgroundColor = backdorColor - super.addSubview(replyView!) + rowView.addSubview(replyView!) + } + + if reply.isSideAccessory { + replyView?.layer?.cornerRadius = .cornerRadius + } else { + replyView?.layer?.cornerRadius = 0 } replyView?.removeAllHandlers() - replyView?.set(handler: { [weak item, weak reply] _ in - if let replyMessage = reply?.replyMessage, let fromMessage = item?.message { - item?.chatInteraction.focusMessageId(fromMessage.id, replyMessage.id, .center(id: 0, animated: true, focus: true, inset: 0)) - } + replyView?.set(handler: { [weak item] _ in + item?.chatInteraction.focusInputField() + item?.openReplyMessage() + }, for: .Click) -// replyView?.set(handler: { [weak reply, weak item] control in -// if let replyMessageId = reply?.replyMessage?.id, let item = item { -// showPopover(for: control, with: ChatReplyPreviewController(item.account, messageId: replyMessageId, width: min(item.width - 160, 500)), inset: NSMakePoint(-8, 1)) -// } -// }, for: .LongOver) - reply.view = replyView - reply.view?.needsDisplay = true + //reply.view?.needsDisplay = true } else { replyView?.removeFromSuperview() replyView = nil @@ -316,237 +534,1146 @@ class ChatRowView: TableRowView, Notifable, MultipleSelectable { } + func bubbleFrame(_ item: ChatRowItem) -> NSRect { + var bubbleFrame = item.bubbleFrame + bubbleFrame = NSMakeRect(item.isIncoming ? bubbleFrame.minX : frame.width - bubbleFrame.width - item.leftInset, bubbleFrame.minY, bubbleFrame.width, bubbleFrame.height) + + if item.chatInteraction.mode.isThreadMode, item.chatInteraction.mode.threadId == item.message?.id { + bubbleFrame.origin.x = focus(NSMakeSize(bubbleFrame.size.width + 8, bubbleFrame.size.height)).minX + } + + return bubbleFrame + } - - override func layout() { - super.layout() - if let item = item as? ChatRowItem { - forwardName?.setFrameOrigin(item.forwardNameInset.x, item.forwardNameInset.y) - contentView.frame = NSMakeRect(item.contentOffset.x, item.contentOffset.y, item.contentSize.width, item.contentSize.height) - rightView.frame = NSMakeRect(frame.width - item.rightSize.width - item.rightInset, item.defaultContentTopOffset, item.rightSize.width, item.rightSize.height) - if let reply = item.replyModel { - reply.frame = NSMakeRect(contentFrame.minX, item.replyOffset, reply.size.width,reply.size.height) - } - avatar?.frame = NSMakeRect(item.leftInset, item.defaultContentTopOffset, 36, 36) + func rightFrame(_ item: ChatRowItem) -> NSRect { + + let rightSize = item.rightSize + let bubbleFrame = self.bubbleFrame(item) + let contentFrame = self.contentFrame(item) + var rect = NSMakeRect(frame.width - rightSize.width - item.rightInset, item.defaultContentTopOffset, rightSize.width, rightSize.height) + let hasBubble = item.hasBubble + if item.isBubbled { + rect.origin = NSMakePoint((hasBubble ? bubbleFrame.maxX : contentFrame.maxX) - rightSize.width - item.bubbleContentInset - (item.isIncoming ? 0 : item.additionBubbleInset), bubbleFrame.maxY - rightSize.height - 6 - (item.isStateOverlayLayout && !hasBubble ? 2 : 0)) - var additionInset:CGFloat = contentView.frame.maxY + item.defaultContentTopOffset - if let captionLayout = item.captionLayout { - captionView?.frame = NSMakeRect(contentView.frame.minX, additionInset, captionLayout.layoutSize.width, captionLayout.layoutSize.height) - additionInset += captionLayout.layoutSize.height + item.defaultContentTopOffset + if item.isStateOverlayLayout { + if item.isInstantVideo { + rect.origin.y = contentFrame.maxY - rect.height - 3 + } else { + rect.origin.x += 5 + rect.origin.y -= 2 + rect.origin.x = max(20, rect.origin.x) + } } - - item.replyModel?.view?.needsDisplay = true - - if let replyMarkup = item.replyMarkupModel { - replyMarkupView?.frame = NSMakeRect(contentView.frame.minX, additionInset, replyMarkup.size.width, replyMarkup.size.height) - replyMarkup.layout() + if item is ChatVideoMessageItem { + rect.origin.x = item.isIncoming ? contentFrame.maxX - 40 : contentFrame.maxX - rightSize.width + rect.origin.y += 3 + } + if let item = item as? ChatMessageItem { + if item.containsBigEmoji { + rect.origin.y = bubbleFrame.maxY - rightSize.height + } else if item.actionButtonText != nil { + if item.webpageLayout == nil { + rect.origin.y = bubbleFrame.maxY - rightSize.height - item.actionButtonHeight - 6; + } + } } - selectingView?.setFrameOrigin(rightView.frame.maxX + 4,item.defaultContentTopOffset - 1) - if let shareControl = shareControl { - shareControl.setFrameOrigin(frame.width - 20.0 - shareControl.frame.width, rightView.frame.maxY + 5) + if item.hasBubble, let _ = item.commentsBubbleData { + rect.origin.y -= ChatRowItem.channelCommentsBubbleHeight } + } + + return rect } + func avatarFrame(_ item: ChatRowItem) -> NSRect { + var rect = NSMakeRect(item.leftInset, 6, 36, 36) + + if item.isBubbled { + rect.origin.y = frame.height - 36 + } + + return rect + } + func captionFrame(_ item: ChatRowItem, caption: ChatRowItem.RowCaption) -> NSRect { + let contentFrame = self.contentFrame(item) + return NSMakeRect(contentFrame.minX + item.elementsContentInset, contentFrame.maxY + item.defaultContentInnerInset + caption.offset.y, caption.layout.layoutSize.width, caption.layout.layoutSize.height) + } - func fillForward(_ item:ChatRowItem) -> Void { - if let forwardNameLayout = item.forwardNameLayout { - if forwardName == nil { - forwardName = TextView() - forwardName?.isSelectable = false - super.addSubview(forwardName!) - } - if !forwardName!.isEqual(to: forwardNameLayout) { - forwardName?.update(forwardNameLayout) - } - } else { - forwardName?.removeFromSuperview() - forwardName = nil + func replyMarkupFrame(_ item: ChatRowItem) -> NSRect { + guard let replyMarkup = item.replyMarkupModel else {return NSZeroRect} + + let contentFrame = self.contentFrame(item) + + var frame = NSMakeRect(contentFrame.minX + item.elementsContentInset, contentFrame.maxY + item.defaultReplyMarkupInset, replyMarkup.size.width, replyMarkup.size.height) + + if let captionLayout = item.captionLayouts.first?.layout { + frame.origin.y += captionLayout.layoutSize.height + item.defaultContentInnerInset } + + let bubbleFrame = self.bubbleFrame(item) + + if item.hasBubble { + frame.origin.y = bubbleFrame.maxY + item.defaultReplyMarkupInset + frame.origin.x = bubbleFrame.minX + (item.isIncoming ? item.additionBubbleInset : 0) + } else if item.isBubbled { + frame.origin.y = bubbleFrame.maxY + } + + return frame } - func fillPhoto(_ item:ChatRowItem) -> Void { - if case .Full = item.itemType, item.peer != nil { - - if avatar == nil { - avatar = AvatarControl(font: .avatar(.text)) - avatar?.setFrameSize(36,36) - super.addSubview(avatar!) + func replyFrame(_ item: ChatRowItem) -> NSRect { + guard let reply = item.replyModel else {return NSZeroRect} + + let contentFrame = self.contentFrame(item) + + var frame: NSRect = NSMakeRect(contentFrame.minX + item.elementsContentInset, item.replyOffset, reply.size.width, reply.size.height) + if item.isBubbled, !item.hasBubble { + if item.isIncoming { + frame.origin.x = contentFrame.maxX + 10 + } else { + frame.origin.x = contentFrame.minX - reply.size.width - 10 } - avatar?.removeAllHandlers() - avatar?.set(handler: { control in - if let peerId = item.peer?.id { - item.chatInteraction.openInfo(peerId, false, nil, nil) - } - }, for: .Click) - - avatar?.set(handler: { control in - if let peerId = item.peer?.id { - showDetailInfoPopover(forPeerId: peerId, account: item.account, fromView: control) + if item.isSharable || item.hasSource || item.commentsBubbleDataOverlay != nil { + if item.isIncoming { + frame.origin.x += 46 + } else { + frame.origin.x -= 46 } - }, for: .LongOver) - - self.avatar?.setPeer(account: item.account, peer: item.peer!) - - } else { - avatar?.removeFromSuperview() - avatar = nil + } } + return frame } - func fillCaption(_ item:ChatRowItem) -> Void { - if let layout = item.captionLayout { - if captionView == nil { - captionView = TextView() - super.addSubview(captionView!) + func viaAccesoryPoint(_ item: ChatRowItem) -> NSPoint { + guard let viaAccessory = viaAccessory else {return NSZeroPoint} + + if viaAccessory.superview == replyView { + return NSMakePoint(5, 0) + } + + let contentFrame = self.contentFrame(item) + + var point: NSPoint = NSMakePoint(contentFrame.minX + item.elementsContentInset, item.defaultContentTopOffset) + if item.isBubbled, !item.hasBubble { + if item.isIncoming { + point.x = contentFrame.maxX + 10 + } else { + point.x = contentFrame.minX - viaAccessory.frame.width - 10 } - captionView?.update(layout) - } else { - captionView?.removeFromSuperview() - captionView = nil } + return point } - func fillShareControl(_ item:ChatRowItem) -> Void { - if item.isSharable { - if shareControl == nil { - shareControl = ImageButton() - shareControl?.disableActions() - shareControl?.change(opacity: 0, animated: false) - super.addSubview(shareControl!) - } - shareControl?.set(image: theme.icons.chatShare, for: .Normal) - shareControl?.sizeToFit() - shareControl?.removeAllHandlers() - shareControl?.set(handler: { [weak self] _ in - if let window = self?.contentView.kitWindow, let message = item.message { - showModal(with: ShareModalController(ShareMessageObject(item.account, message)), for: window) - } - }, for: .Click) + func namePoint(_ item: ChatRowItem) -> NSPoint { + + let contentFrame = self.contentFrame(item) + + var point = NSMakePoint(contentFrame.minX, item.defaultContentTopOffset) + if item.isBubbled { + point.y -= item.topInset } else { - shareControl?.removeFromSuperview() - shareControl = nil + if item.forwardType != nil { + point.x -= item.leftContentInset + } } + point.x += item.elementsContentInset + return point + } - func fillReplyMarkup(_ item:ChatRowItem) -> Void { - if let replyMarkup = item.replyMarkupModel { - if replyMarkupView == nil { - replyMarkupView = View() - super.addSubview(replyMarkupView!) - } - - replyMarkupView?.setFrameSize(replyMarkup.size.width, replyMarkup.size.height) - replyMarkup.view = replyMarkupView - replyMarkup.view?.backgroundColor = theme.colors.background - replyMarkup.redraw() - } else { - replyMarkupView?.removeFromSuperview() - replyMarkupView = nil - } + func scamPoint(_ item: ChatRowItem) -> NSPoint { + guard let authorText = item.authorText else {return NSZeroPoint} + + var point = self.namePoint(item) + point.x += authorText.layoutSize.width + 3 + point.y += 1 + return point } - func fillName(_ item:ChatRowItem) -> Void { - if let author = item.authorText { - if nameView == nil { - nameView = TextView() - nameView?.isSelectable = false - super.addSubview(nameView!) - } - nameView?.update(author, origin:NSMakePoint(item.defLeftInset, item.defaultContentTopOffset)) - } else { - nameView?.removeFromSuperview() - nameView = nil + func psaPoint(_ item: ChatRowItem) -> NSPoint { + var point: NSPoint = .zero + if item.isBubbled, let _ = item.forwardNameLayout { + point.x = item.bubbleFrame.width - 20 + point.y = self.forwardNamePoint(item).y + } else if item.entry.renderType == .list, let name = item.authorText { + point = self.namePoint(item) + point.x += name.layoutSize.width + point.y -= 6 } + + // point.y -= 7 + return point } - override func focusAnimation() { + func scamForwardPoint(_ item: ChatRowItem) -> NSPoint { + guard let forwardName = item.forwardNameLayout else {return NSZeroPoint} - if animatedView == nil { - self.animatedView = ChatRowAnimateView(frame:bounds) - self.animatedView?.isEventLess = true - super.addSubview(animatedView!) - animatedView?.backgroundColor = NSColor(0x68A8E2) - animatedView?.layer?.opacity = 0 - + var point = self.forwardNamePoint(item) + point.x += forwardName.layoutSize.width + 3 + //point.y += 1 + return point + } + + func adminBadgePoint(_ item: ChatRowItem) -> NSPoint { + guard let adminBadge = item.adminBadge, let authorText = item.authorText else {return NSZeroPoint} + let bubbleFrame = self.bubbleFrame(item) + let namePoint = self.namePoint(item) + var point = NSMakePoint( item.isBubbled ? bubbleFrame.maxX - item.bubbleContentInset - adminBadge.layoutSize.width : namePoint.x + authorText.layoutSize.width, item.defaultContentTopOffset + 1) + if item.isBubbled { + point.y -= item.topInset } - animatedView?.stableId = item?.stableId - animatedView?.change(opacity: 0.5, animated: true, false, removeOnCompletion: false, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] completed in - if completed { - self?.animatedView?.change(opacity: 0, animated: true, false, removeOnCompletion: true, duration: 1.5, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] completed in - if completed { - self?.animatedView?.removeFromSuperview() - self?.animatedView = nil - } - }) - } - }) - + return point + } + + func selectingPoint(_ item: ChatRowItem) -> NSPoint { + + var point = NSZeroPoint + + let rightFrame = self.rightFrame(item) + + if let selectingView = selectingView { + if item.isBubbled { + let f = focus(selectingView.frame.size) + point.y = f.minY + point.x = frame.width - selectingView.frame.width - 15 + } else { + point = NSMakePoint(rightFrame.maxX + 4, item.defaultContentTopOffset - 3) + } + } + return point } - - override func rightMouseDown(with event: NSEvent) { - if let item = self.item as? ChatRowItem { - if item.chatInteraction.presentation.state == .selecting { - return + + func contentFrame(_ item: ChatRowItem) -> NSRect { + var rect = NSMakeRect(item.contentOffset.x, item.contentOffset.y, item.contentSize.width, item.contentSize.height) + if item.isBubbled { + let bubbleFrame = self.bubbleFrame(item) + if !item.isIncoming { + rect.origin.x = bubbleFrame.minX + item.bubbleContentInset + } else { + rect.origin.x = bubbleFrame.minX + item.bubbleContentInset + item.additionBubbleInset } + } - super.rightMouseDown(with: event) + return rect } - override func set(item:TableRowItem, animated:Bool = false) { + func contentFrameModifier(_ item: ChatRowItem) -> NSRect { + return self.contentFrame(item) + } + + func rowPoint(_ item: ChatRowItem) -> NSPoint { - if let item = self.item as? ChatRowItem { - item.chatInteraction.remove(observer: self) + if item.isBubbled { + return NSMakePoint((item.chatInteraction.presentation.state == .selecting && !item.isIncoming ? -20 : 0), 0) + } else { + return NSMakePoint(0, 0) } - if self.animatedView != nil && self.animatedView?.stableId != item.stableId { - self.animatedView?.removeFromSuperview() - self.animatedView = nil + } + + func forwardNamePoint(_ item: ChatRowItem) -> NSPoint { + + var point = item.forwardNameInset + + if item.isBubbled && item.hasBubble { + let bubbleFrame = self.bubbleFrame(item) + point.x = bubbleFrame.minX + (item.isIncoming ? item.bubbleContentInset + item.additionBubbleInset : item.bubbleContentInset) + } else if item.isBubbled, let forwardAccessory = forwardAccessory { + let contentFrame = self.contentFrame(item) + point.x = item.isIncoming ? contentFrame.maxX : contentFrame.minX - forwardAccessory.frame.width } + return point + } + + override func layout() { + // super.layout() if let item = item as? ChatRowItem { - rightView.set(item:item, animated:animated) - fillName(item) - fillReplyIfNeeded(item.replyModel, item) - fillForward(item) - fillPhoto(item) - fillCaption(item) - fillReplyMarkup(item) - fillShareControl(item) - item.chatInteraction.add(observer: self) + hasBeenLayout = true + + bubbleView.frame = bubbleFrame(item) + contentView.frame = contentFrameModifier(item) - updateSelectingState(selectingMode:item.chatInteraction.presentation.selectionState != nil, item: item) - } - - super.set(item: item, animated: animated) - self.needsLayout = true - } - open override var interactionContentView:NSView { - return self.contentView - } - - override func doubleClick(in location: NSPoint) { - if let item = self.item as? ChatRowItem, item.chatInteraction.presentation.state == .normal { - if self.hitTest(location) == nil || self.hitTest(location) == self || self.hitTest(location) == replyView { - if let avatar = avatar { + + rowView.setFrameOrigin(rowPoint(item)) + + forwardName?.setFrameOrigin(forwardNamePoint(item)) + forwardAccessory?.setFrameOrigin(forwardNamePoint(item)) + + rightView.frame = rightFrame(item) + + nameView?.setFrameOrigin(namePoint(item)) + + adminBadge?.setFrameOrigin(adminBadgePoint(item)) + + viaAccessory?.setFrameOrigin(viaAccesoryPoint(item)) + item.replyModel?.frame = replyFrame(item) + + + scamButton?.setFrameOrigin(scamPoint(item)) + scamForwardButton?.setFrameOrigin(scamForwardPoint(item)) + + psaButton?.setFrameOrigin(psaPoint(item)) + + avatar?.frame = avatarFrame(item) + + for captionView in captionViews { + if let caption = item.captionLayouts.first(where: { $0.id == captionView.id }) { + captionView.view.frame = captionFrame(item, caption: caption) + } + } + + + replyMarkupView?.frame = replyMarkupFrame(item) + item.replyMarkupModel?.layout() + + + selectingView?.setFrameOrigin(selectingPoint(item)) + + animatedView?.frame = bounds + + channelCommentsBubbleControl?.frame = channelCommentsBubbleFrame(item) + channelCommentsControl?.frame = channelCommentsFrame(item) + channelCommentsBubbleSmallControl?.frame = channelCommentsOverlayFrame(item) + + swipingRightView.frame = NSMakeRect(frame.width, 0, rightRevealWidth, frame.height) + + shareView?.setFrameOrigin(shareViewPoint(item)) + likeView?.setFrameOrigin(likeViewPoint(item)) + + } + } + + func shareViewPoint(_ item: ChatRowItem) -> NSPoint { + guard let shareView = self.shareView else { + return .zero + } + var point: NSPoint + if item.isBubbled { + let bubbleFrame = self.bubbleFrame(item) + let rightFrame = self.rightFrame(item) + point = NSMakePoint(item.isIncoming ? max(bubbleFrame.maxX + 10, rightFrame.maxX + 10) : bubbleFrame.minX - shareView.frame.width - 10, bubbleFrame.maxY - (shareView.frame.height)) + } else { + let rightFrame = self.rightFrame(item) + point = NSMakePoint(frame.width - 20.0 - shareView.frame.width, rightFrame.maxY) + } + return point + } + + func likeViewPoint(_ item: ChatRowItem) -> NSPoint { + guard let likeView = self.likeView else { + return .zero + } + var controlOffset: CGFloat = 0 + if let shareView = shareView { + controlOffset += shareView.frame.width + 10 + } + if item.isBubbled { + let bubbleFrame = self.bubbleFrame(item) + let rightFrame = self.rightFrame(item) + return NSMakePoint(item.isIncoming ? max(bubbleFrame.maxX + 10 + controlOffset, item.isStateOverlayLayout ? rightFrame.width + 10 + controlOffset : 0) : bubbleFrame.minX - likeView.frame.width - 10 - controlOffset, bubbleFrame.maxY - (likeView.frame.height - 2) - (item.isVideoOrBigEmoji ? rightFrame.height + 14 : 0)) + } else { + return NSMakePoint(frame.width - 20.0 - likeView.frame.width, rightView.frame.maxY) + } + } + + + + func fillForward(_ item:ChatRowItem) -> Void { + if let forwardNameLayout = item.forwardNameLayout { + if item.isBubbled && !item.hasBubble { + forwardName?.removeFromSuperview() + forwardName = nil + + if forwardAccessory == nil { + forwardAccessory = ChatBubbleAccessoryForward(frame: NSZeroRect) + rowView.addSubview(forwardAccessory!) + } + + forwardAccessory?.updateText(layout: forwardNameLayout) + + } else { + forwardAccessory?.removeFromSuperview() + forwardAccessory = nil + + if forwardName == nil { + forwardName = TextView() + forwardName?.isSelectable = false + rowView.addSubview(forwardName!) + } + forwardName?.update(forwardNameLayout) + + } + + } else { + forwardName?.removeFromSuperview() + forwardName = nil + forwardAccessory?.removeFromSuperview() + forwardAccessory = nil + } + } + + static func makePhotoView(_ item: ChatRowItem) -> NSView { + let avatar = AvatarControl(font: .avatar(.text)) + avatar.setFrameSize(36,36) + + avatar.toolTip = item.nameHide + if let peer = item.peer { + avatar.setPeer(account: item.context.account, peer: peer, message: item.message) + } + return avatar + } + + func fillPhoto(_ item:ChatRowItem) -> Void { + if item.hasPhoto, let peer = item.peer, !item.presentation.bubbled { + + if avatar == nil { + avatar = AvatarControl(font: .avatar(.text)) + avatar?.setFrameSize(36,36) + rowView.addSubview(avatar!) + } + avatar?.removeAllHandlers() + avatar?.set(handler: { [weak item] control in + item?.openInfo() + }, for: .Click) + avatar?.toolTip = item.nameHide + self.avatar?.setPeer(account: item.context.account, peer: peer, message: item.message) + + } else { + avatar?.removeFromSuperview() + avatar = nil + } + } + + func fillPsaButton(_ item: ChatRowItem) -> Void { + if let text = item.psaButton, item.forwardNameLayout != nil || !item.isBubbled { + + let icon = item.presentation.chat.channelInfoPromo(item.isIncoming, item.isBubbled, icons: theme.icons) + + if psaButton == nil { + psaButton = ImageButton() + psaButton?.autohighlight = false + psaButton?.setFrameSize(icon.backingSize) + rowView.addSubview(psaButton!) + psaButton?.set(handler: { control in + tooltip(for: control, text: "", attributedText: text, interactions: globalLinkExecutor) + }, for: .Click) + } + psaButton?.set(image: icon, for: .Normal) + + } else { + psaButton?.removeFromSuperview() + psaButton = nil + } + } + + func fillScamButton(_ item: ChatRowItem) -> Void { + if item.isScam || item.isFake, item.canFillAuthorName { + if scamButton == nil { + let text: String = !item.isScam ? L10n.peerInfoFakeWarning : L10n.peerInfoScamWarning + scamButton = ImageButton() + scamButton?.autohighlight = false + scamButton?.setFrameSize(item.badIcon.backingSize) + rowView.addSubview(scamButton!) + scamButton?.set(handler: { control in + tooltip(for: control, text: text) + }, for: .Click) + } + scamButton?.set(image: item.badIcon, for: .Normal) + + } else { + scamButton?.removeFromSuperview() + scamButton = nil + } + } + + func fillScamForwardButton(_ item: ChatRowItem) -> Void { + if item.isForwardScam || item.isForwardFake { + if scamForwardButton == nil { + let text: String = !item.isForwardScam ? L10n.peerInfoFakeWarning : L10n.peerInfoScamWarning + scamForwardButton = ImageButton() + scamForwardButton?.autohighlight = false + scamForwardButton?.setFrameSize(item.forwardBadIcon.backingSize) + rowView.addSubview(scamForwardButton!) + scamForwardButton?.set(handler: { control in + tooltip(for: control, text: text) + }, for: .Click) + } + scamForwardButton?.set(image: item.forwardBadIcon, for: .Normal) + + } else { + scamForwardButton?.removeFromSuperview() + scamForwardButton = nil + } + } + + func fillCaption(_ item:ChatRowItem, animated: Bool) -> Void { + + var removeIndexes:[Int] = [] + for (i, view) in captionViews.enumerated() { + if !item.captionLayouts.contains(where: { $0.id == view.id}) { + let captionView = view.view + if animated { + captionView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak captionView] _ in + captionView?.removeFromSuperview() + }) + } else { + captionView.removeFromSuperview() + } + removeIndexes.append(i) + } + } + + for index in removeIndexes.reversed() { + captionViews.remove(at: index) + } + + for (i, layout) in item.captionLayouts.enumerated() { + var view = captionViews.first(where: { $0.id == layout.id }) + if view == nil { + view = CaptionView(id: layout.id, view: TextView()) + rowView.addSubview(view!.view, positioned: .below, relativeTo: rightView) + view?.view.frame = captionFrame(item, caption: layout) + captionViews.append(view!) + } + if let index = captionViews.firstIndex(where: { $0.id == layout.id }), index != i { + captionViews.move(at: index, to: i) + } + view?.view.update(layout.layout) + } + + + +// if let layout = item.captionLayout { +// if captionView == nil { +// captionView = TextView() +// rowView.addSubview(captionView!) +// rowView.addSubview(rightView) +// captionView?.frame = captionFrame(item) +// } +// captionView?.update(layout) +// } else { +// if animated, let captionView = self.captionView { +// self.captionView = nil +// captionView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak captionView] _ in +// captionView?.removeFromSuperview() +// }) +// } else { +// captionView?.removeFromSuperview() +// captionView = nil +// } +// } + } + + func channelCommentsBubbleFrame(_ item: ChatRowItem) -> CGRect { + guard let _ = item.commentsBubbleData else { + return .zero + } + return NSMakeRect(0, 0, item.bubbleFrame.width, ChatRowItem.channelCommentsBubbleHeight) + } + func channelCommentsOverlayFrame(_ item: ChatRowItem) -> CGRect { + guard let commentsData = item.commentsBubbleDataOverlay else { + return .zero + } + let size = commentsData.size(false, true) + let rightFrame = self.rightFrame(item) + var rect = NSMakeRect(rightFrame.maxX + 19, rightFrame.minY - size.height - 15, size.width, size.height) + if item.isInstantVideo { + rect = NSMakeRect(rightFrame.maxX + 12, rightFrame.minY - size.height - 23, size.width, size.height) + } else if let item = item as? ChatMessageItem, item.containsBigEmoji { + rect.origin.x -= 8 + rect.origin.y -= 8 + } + return rect + } + func channelCommentsFrame(_ item: ChatRowItem) -> CGRect { + guard let commentsData = item.commentsData else { + return .zero + } + let size = commentsData.size(false) + let rightFrame = self.rightFrame(item) + return CGRect(origin: CGPoint(x: rightFrame.minX - size.width - 4, y: rightFrame.minY - 1), size: size) + } + + func fillChannelComments(_ item: ChatRowItem, animated: Bool) { + if let commentsBubbleData = item.commentsBubbleData { + let current: ChannelCommentsBubbleControl + if let channelCommentsBubbleControl = self.channelCommentsBubbleControl { + current = channelCommentsBubbleControl + } else { + current = ChannelCommentsBubbleControl(frame: NSMakeRect(0, 0, item.bubbleFrame.width, ChatRowItem.channelCommentsBubbleHeight)) + + current.set(background: .clear, for: .Normal) + current.set(background: item.presentation.colors.accent.withAlphaComponent(0.08), for: .Hover) + current.set(background: item.presentation.colors.accent.withAlphaComponent(0.16), for: .Highlight) + + self.channelCommentsBubbleControl = current + bubbleView.addSubview(current) + } + current.update(data: commentsBubbleData, size: channelCommentsBubbleFrame(item).size, animated: animated) + } else { + if let channelCommentsBubbleControl = self.channelCommentsBubbleControl { + self.channelCommentsBubbleControl = nil + if animated { + channelCommentsBubbleControl.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak channelCommentsBubbleControl] _ in + channelCommentsBubbleControl?.removeFromSuperview() + }) + } else { + channelCommentsBubbleControl.removeFromSuperview() + } + } + } + if let data = item.commentsBubbleDataOverlay { + let current: ChannelCommentsSmallControl + if let channelCommentsBubbleSmallControl = self.channelCommentsBubbleSmallControl { + current = channelCommentsBubbleSmallControl + } else { + current = ChannelCommentsSmallControl(frame: CGRect(origin: .zero, size: data.size(false, true))) + current.set(background: item.presentation.chatServiceItemColor, for: .Normal) + + current.change(opacity: 0, animated: animated) + self.channelCommentsBubbleSmallControl = current + rowView.addSubview(current) + } + current.update(data: data, size: channelCommentsOverlayFrame(item).size, animated: animated) + current.change(pos: channelCommentsOverlayFrame(item).origin, animated: animated) + } else { + if let channelCommentsBubbleSmallControl = self.channelCommentsBubbleSmallControl { + self.channelCommentsBubbleSmallControl = nil + if animated { + channelCommentsBubbleSmallControl.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak channelCommentsBubbleSmallControl] _ in + channelCommentsBubbleSmallControl?.removeFromSuperview() + }) + } else { + channelCommentsBubbleSmallControl.removeFromSuperview() + } + } + } + if let commentsData = item.commentsData { + let current: ChannelCommentsControl + if let channelCommentsControl = self.channelCommentsControl { + current = channelCommentsControl + } else { + current = ChannelCommentsControl(frame: NSMakeRect(0, 0, commentsData.size(false).width, ChatRowItem.channelCommentsHeight)) + current.set(background: contentColor, for: .Normal) + + self.channelCommentsControl = current + rowView.addSubview(current) + } + current.update(data: commentsData, size: channelCommentsFrame(item).size, animated: animated) + } else { + if let channelCommentsControl = self.channelCommentsControl { + self.channelCommentsControl = nil + if animated { + channelCommentsControl.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak channelCommentsControl] _ in + channelCommentsControl?.removeFromSuperview() + }) + } else { + channelCommentsControl.removeFromSuperview() + } + } + } + self.channelCommentsControl?.isEnabled = !item.isFailed && !item.isUnsent + self.channelCommentsBubbleControl?.isEnabled = !item.isFailed && !item.isUnsent + self.channelCommentsBubbleSmallControl?.isEnabled = !item.isFailed && !item.isUnsent + + } + + func fillShareView(_ item:ChatRowItem, animated: Bool) -> Void { + if item.shareVisible || item.hasSource { + var isPresented: Bool = true + if shareView == nil { + shareView = ImageButton() + shareView?.set(hoverAdditionPolicy: .enlarge(value: 1.05), for: .Hover) + shareView?.set(hoverAdditionPolicy: .enlarge(value: 1.0), for: .Normal) + shareView?.set(hoverAdditionPolicy: .enlarge(value: 1.05), for: .Highlight) + shareView?.set(additionBackgroundMultiplier: 0.95, for: .Normal) + shareView?.set(additionBackgroundMultiplier: 0.95, for: .Hover) + shareView?.set(additionBackgroundMultiplier: 0.95, for: .Highlight) + shareView?.disableActions() + shareView?.change(opacity: 0, animated: false) + rowView.addSubview(shareView!) + isPresented = false + } + + guard let control = shareView else {return} + control.autohighlight = false + + + if animated && isPresented { + control.change(pos: shareViewPoint(item), animated: true) + } else { + control.setFrameOrigin(shareViewPoint(item)) + } + + if item.isBubbled && item.presentation.backgroundMode.hasWallpaper { + + control.set(image: item.hasSource ? item.presentation.chat.chat_goto_message_bubble(theme: item.presentation) : item.presentation.chat.chat_share_bubble(theme: item.presentation), for: .Normal) + control.setFrameSize(NSMakeSize(29, 29)) + let size = NSMakeSize(control.frame.width, control.frame.height) + control.setFrameSize(NSMakeSize(floorToScreenPixels(backingScaleFactor, (size.width + 4) * 1.05), floorToScreenPixels(backingScaleFactor, (size.height + 4) * 1.05))) + control.set(additionBackgroundColor: item.presentation.chatServiceItemColor, for: .Normal) + control.set(additionBackgroundColor: item.presentation.chatServiceItemColor, for: .Hover) + + control.set(cornerRadius: .half, for: .Normal) + } else { + control.set(image: item.hasSource ? item.presentation.icons.chat_goto_message : item.presentation.icons.chat_share_message, for: .Normal) + control.setFrameSize(NSMakeSize(29, 29)) + control.background = .clear + } + + control.removeAllHandlers() + control.set(handler: { [ weak item] _ in + if let item = item { + if item.hasSource { + item.gotoSourceMessage() + } else { + item.share() + } + } + }, for: .Click) + } else { + shareView?.removeFromSuperview() + shareView = nil + } + } + + private func likeImage(_ item: ChatRowItem) -> CGImage { + if item.isLiked { + return item.presentation.chat.chat_like_message_unlike_bubble(theme: item.presentation) + } else { + return item.presentation.chat.chat_like_message_bubble(theme: item.presentation) + } + } + + override func change(size: NSSize, animated: Bool, _ save: Bool = true, removeOnCompletion: Bool = true, duration: Double = 0.2, timingFunction: CAMediaTimingFunctionName = CAMediaTimingFunctionName.easeOut, completion: ((Bool) -> Void)? = nil) { + + rowView.change(size: size, animated: animated, save, removeOnCompletion: removeOnCompletion, duration: duration, timingFunction: timingFunction, completion: completion) + + super.change(size: size, animated: animated, save, removeOnCompletion: removeOnCompletion, duration: duration, timingFunction: timingFunction, completion: completion) + + } + + func fillLikeView(_ item: ChatRowItem, animated: Bool) { + if item.isLikable { + var isPresented: Bool = true + if likeView == nil { + likeView = ImageButton() + likeView?.set(hoverAdditionPolicy: .enlarge(value: 1.05), for: .Hover) + likeView?.set(hoverAdditionPolicy: .enlarge(value: 1.0), for: .Normal) + likeView?.set(hoverAdditionPolicy: .enlarge(value: 1.05), for: .Highlight) + likeView?.set(additionBackgroundMultiplier: 0.95, for: .Normal) + likeView?.set(additionBackgroundMultiplier: 0.95, for: .Hover) + likeView?.set(additionBackgroundMultiplier: 0.95, for: .Highlight) + likeView?.autohighlight = false + likeView?.disableActions() + likeView?.change(opacity: 0, animated: false) + rowView.addSubview(likeView!) + isPresented = false + } + + guard let control = likeView else {return} + + if animated && isPresented { + control.change(pos: likeViewPoint(item), animated: true) + } + + let isLiked = item.isLiked + + if item.isBubbled && item.presentation.backgroundMode.hasWallpaper { + control.set(image: likeImage(item), for: .Normal) + + _ = control.sizeToFit() + let size = NSMakeSize(control.frame.width, control.frame.height) + control.setFrameSize(NSMakeSize(floorToScreenPixels(backingScaleFactor, (size.width + 4) * 1.05), floorToScreenPixels(backingScaleFactor, (size.height + 4) * 1.05))) + control.set(additionBackgroundColor: item.presentation.chatServiceItemColor, for: .Normal) + + control.set(cornerRadius: .half, for: .Normal) + } else { + control.set(image: item.presentation.icons.chat_like_message, for: .Normal) + _ = control.sizeToFit() + control.background = .clear + } + + control.removeAllHandlers() + control.set(handler: { [weak item] control in + if let item = item { + let presentation = item.presentation.chat + let from = isLiked ? presentation.chat_like_message_unlike_bubble(theme: item.presentation) : presentation.chat_like_message_bubble(theme: item.presentation) + let to = isLiked ? presentation.chat_like_message_bubble(theme: item.presentation) : presentation.chat_like_message_unlike_bubble(theme: item.presentation) + + (control as? ImageButton)?.applyAnimation(from: from, to: to, animation: .replaceScale) + + item.toggleLike() + } + + + }, for: .Click) + } else { + likeView?.removeFromSuperview() + likeView = nil + } + } + + func fillReplyMarkup(_ item:ChatRowItem, animated: Bool) -> Void { + if let replyMarkup = item.replyMarkupModel { + if replyMarkupView == nil { + replyMarkupView = View() + rowView.addSubview(replyMarkupView!) + replyMarkupView?.frame = replyMarkupFrame(item) + } + + replyMarkupView?.setFrameSize(replyMarkup.size.width, replyMarkup.size.height) + replyMarkup.view = replyMarkupView + replyMarkup.redraw() + } else { + if let replyMarkupView = self.replyMarkupView, animated { + self.replyMarkupView = nil + replyMarkupView.layer?.animateScaleCenter(from: 1, to: 0.1, duration: 0.2, removeOnCompletion: false) + replyMarkupView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak replyMarkupView] _ in + replyMarkupView?.removeFromSuperview() + }) + } else { + replyMarkupView?.removeFromSuperview() + replyMarkupView = nil + } + } + } + + + + func fillName(_ item:ChatRowItem, animated: Bool) -> Void { + if let author = item.authorText { + if item.isBubbled && !item.hasBubble { + nameView?.removeFromSuperview() + nameView = nil + + adminBadge?.removeFromSuperview() + adminBadge = nil + + if viaAccessory == nil { + viaAccessory = ChatBubbleViaAccessory(frame: NSZeroRect) + } + + guard let viaAccessory = viaAccessory else {return} + + viaAccessory.removeFromSuperview() + if replyView != nil { + replyView?.addSubview(viaAccessory) + } else { + rowView.addSubview(viaAccessory) + } + + viaAccessory.updateText(layout: author) + + + } else { + + viaAccessory?.removeFromSuperview() + viaAccessory = nil + + if nameView == nil { + nameView = TextView() + nameView?.isSelectable = false + + rowView.addSubview(nameView!) + } + + if let adminBadge = item.adminBadge { + if self.adminBadge == nil { + self.adminBadge = TextView() + self.adminBadge?.isSelectable = false + rowView.addSubview(self.adminBadge!) + } + self.adminBadge?.update(adminBadge, origin: adminBadgePoint(item)) + } else { + adminBadge?.removeFromSuperview() + adminBadge = nil + } + + nameView?.update(author) + nameView?.change(pos: namePoint(item), animated: animated) + nameView?.toolTip = item.nameHide + } + + } else { + + viaAccessory?.removeFromSuperview() + viaAccessory = nil + + nameView?.removeFromSuperview() + nameView = nil + + adminBadge?.removeFromSuperview() + adminBadge = nil + } + } + + override func focusAnimation(_ innerId: AnyHashable?) { + + if animatedView == nil { + self.animatedView = RowAnimateView(frame:bounds) + self.animatedView?.isEventLess = true + rowView.addSubview(animatedView!) + animatedView?.backgroundColor = theme.colors.focusAnimationColor + animatedView?.layer?.opacity = 0 + + } + animatedView?.stableId = item?.stableId + + + let animation: CABasicAnimation = makeSpringAnimation("opacity") + + animation.fromValue = animatedView?.layer?.presentation()?.opacity ?? 0 + animation.toValue = 0.5 + animation.autoreverses = true + animation.isRemovedOnCompletion = true + animation.fillMode = CAMediaTimingFillMode.forwards + + animation.delegate = CALayerAnimationDelegate(completion: { [weak self] completed in + if completed { + self?.animatedView?.removeFromSuperview() + self?.animatedView = nil + } + }) + animation.isAdditive = false + + animatedView?.layer?.add(animation, forKey: "opacity") + } + + func canDropSelection(in location: NSPoint) -> Bool { + return true + } + + override func rightMouseDown(with event: NSEvent) { + if let item = self.item as? ChatRowItem { + if item.chatInteraction.presentation.state == .selecting { + return + } + } + super.rightMouseDown(with: event) + } + + + private func renderLayoutType(_ item: ChatRowItem, animated: Bool) { + if item.isBubbled, item.hasBubble { + bubbleView.setType(image: item.bubbleImage, border: item.bubbleBorderImage, background: item.isIncoming ? item.presentation.icons.chatGradientBubble_incoming : item.presentation.icons.chatGradientBubble_outgoing) + } else { + bubbleView.setType(image: nil, border: nil, background: item.isIncoming ? item.presentation.icons.chatGradientBubble_incoming : item.presentation.icons.chatGradientBubble_outgoing) + } + } + + func animateInStateView() { + rightView.layer?.animateAlpha(from: 0, to: 1.0, duration: 0.15) + } + + func shakeContentView() { + + guard let item = item as? ChatRowItem else { return } + + if bubbleView.layer?.animation(forKey: "shake") != nil { + return + } + + let translation = CAKeyframeAnimation(keyPath: "transform.translation.x"); + translation.timingFunction = CAMediaTimingFunction(name: .linear) + translation.values = [-2, 2, -2, 2, -2, 2, -2, 2, 0] + + let rotation = CAKeyframeAnimation(keyPath: "transform.rotation.z") + rotation.values = [-0.5, -0.5, -0.5, 0.5, -0.5, 0.5, -0.5, -0.5, 0].map { + ( degrees: Double) -> Double in + let radians: Double = (.pi * degrees) / 180.0 + return radians + } + + let shakeGroup: CAAnimationGroup = CAAnimationGroup() + shakeGroup.isRemovedOnCompletion = true + shakeGroup.animations = [rotation] + shakeGroup.timingFunction = .init(name: .easeInEaseOut) + shakeGroup.duration = 0.5 + + + + let frame = bubbleFrame(item) + let contentFrame = self.contentFrameModifier(item) + + + contentView.layer?.position = NSMakePoint(contentFrame.minX + contentFrame.width / 2, contentFrame.minY + contentFrame.height / 2) + contentView.layer?.anchorPoint = NSMakePoint(0.5, 0.5); + + if item.hasBubble { + + struct ShakeItem { + let view: NSView + let rect: NSRect + let tempRect: NSRect + } + var views:[NSView] = [self.rightView, self.nameView, self.scamButton, self.replyView, self.adminBadge, self.forwardName, self.scamForwardButton, self.viaAccessory].compactMap { $0 } + views.append(contentsOf: self.captionViews.map { $0.view }) + let shakeItems = views.map { view -> ShakeItem in + return ShakeItem(view: view, rect: view.frame, tempRect: self.bubbleView.convert(view.frame, from: view.superview)) + } + + for item in shakeItems { + item.view.removeFromSuperview() + item.view.frame = item.tempRect + bubbleView.addSubview(item.view) + } + + + shakeGroup.delegate = CALayerAnimationDelegate(completion: { [weak self] _ in + guard let `self` = self else { + return + } + for item in shakeItems { + item.view.removeFromSuperview() + item.view.frame = item.rect + self.rowView.addSubview(item.view) + } + }) + } + + bubbleView.layer?.position = NSMakePoint(frame.minX + frame.width / 2, frame.minY + frame.height / 2) + bubbleView.layer?.anchorPoint = NSMakePoint(0.5, 0.5); + + + bubbleView.layer?.add(shakeGroup, forKey: "shake") + contentView.layer?.add(shakeGroup, forKey: "shake") + + + } + + override func set(item:TableRowItem, animated:Bool = false) { + + let previousItem = self.item as? ChatRowItem + + if let item = previousItem { + item.chatInteraction.remove(observer: self) + } + + guard let item = item as? ChatRowItem else { + return + } + + + if self.animatedView != nil && self.animatedView?.stableId != item.stableId { + self.animatedView?.removeFromSuperview() + self.animatedView = nil + } + + let animated = animated && item.isBubbled && hasBeenLayout && bubbleView.layer?.animation(forKey: "shake") == nil && previousItem?.message?.id == item.message?.id && self.layer?.animation(forKey: "position") == nil + + if previousItem?.message?.id != item.message?.id { + updateBackground(animated: false, item: item, clean: true) + } + + renderLayoutType(item, animated: animated) + + + item.chatInteraction.add(observer: self) + + updateSelectingState(selectingMode:item.chatInteraction.presentation.selectionState != nil, item: item, needUpdateColors: false) + + rightView.set(item:item, animated:animated) + fillReplyIfNeeded(item.replyModel, item) + fillName(item, animated: animated) + fillForward(item) + fillPhoto(item) + fillForward(item) + fillScamButton(item) + fillScamForwardButton(item) + fillPsaButton(item) + fillShareView(item, animated: animated) + fillLikeView(item, animated: animated) + fillReplyMarkup(item, animated: animated) + fillCaption(item, animated: animated) + fillChannelComments(item, animated: animated) + + super.set(item: item, animated: animated) + + if animated { + + let bubbleFrame = self.bubbleFrame + let contentFrameModifier = self.contentFrameModifier + + nameView?.change(pos: namePoint(item), animated: animated) + + bubbleView.change(pos: bubbleFrame(item).origin, animated: animated) + bubbleView.change(size: bubbleFrame(item).size, animated: animated) + contentView.change(pos: contentFrameModifier(item).origin, animated: animated) + contentView.change(size: contentFrameModifier(item).size, animated: animated) + updateBackground(animated: animated, item: item) + + let rightFrame = self.rightFrame(item) + +// if rightFrame.width != rightView.frame.width && rightFrame.minX < rightView.frame.minX { +// rightView.setFrameOrigin(NSMakePoint(rightFrame.minX, rightView.frame.minY)) +// } + rightView.change(pos: rightFrame.origin, animated: animated) + rightView.change(size: rightFrame.size, animated: animated) + + replyView?._change(pos: replyFrame(item).origin, animated: animated) + replyMarkupView?.change(pos: replyMarkupFrame(item).origin, animated: animated) + for view in captionViews { + if let caption = item.captionLayouts.first(where: { $0.id == view.id }) { + view.view._change(pos: captionFrame(item, caption: caption).origin, animated: animated) + } + } + } + + rowView.needsDisplay = true + needsLayout = true + } + + open override func interactionContentView(for innerId: AnyHashable, animateIn: Bool ) -> NSView { + return self.contentView + } + + override func doubleClick(in location: NSPoint) { + if let item = self.item as? ChatRowItem, item.chatInteraction.presentation.state == .normal { + if self.hitTest(location) == nil || self.hitTest(location) == self || !clickInContent(point: location) || self.hitTest(location) == rowView || self.hitTest(location) == bubbleView || self.hitTest(location) == replyView { + if let avatar = avatar { if NSPointInRect(location, avatar.frame) { return } } - item.chatInteraction.setupReplyMessage(item.message?.id) + if NSPointInRect(location, bubbleFrame(item)), item.isBubbled { + return + } + if let message = item.message, canReplyMessage(message, peerId: item.chatInteraction.peerId, mode: item.chatInteraction.mode) { + item.chatInteraction.setupReplyMessage(item.message?.id) + } } } } + func toggleSelected(_ select: Bool, in point: NSPoint) { + guard let item = item as? ChatRowItem else { return } + + if item.isSelectable { + item.chatInteraction.withToggledSelectedMessage({ current in + if let message = item.message { + if (select && !current.isSelectedMessageId(message.id)) || (!select && current.isSelectedMessageId(message.id)) { + return current.withToggledSelectedMessage(message.id) + } + } + return current + }) + } + } + override func forceClick(in location: NSPoint) { guard let item = item as? ChatRowItem else { return } + let hitTestView = self.hitTest(location) - if hitTestView == nil || hitTestView == self || hitTestView == replyView || hitTestView?.isDescendant(of: contentView) == true { + if hitTestView == nil || hitTestView == self || hitTestView == replyView || hitTestView?.isDescendant(of: contentView) == true || hitTestView == rowView || hitTestView == self.animatedView { if let avatar = avatar { if NSPointInRect(location, avatar.frame) { return @@ -560,14 +1687,22 @@ class ChatRowView: TableRowView, Notifable, MultipleSelectable { result = item.replyAction() case .forward: result = item.forwardAction() + case .previewMedia: + result = false } if result { - focusAnimation() + focusAnimation(nil) + } else { + // NSSound.beep() } } } + func previewMediaIfPossible() -> Bool { + return false + } + deinit { if let item = self.item as? ChatRowItem { item.chatInteraction.remove(observer: self) @@ -575,6 +1710,10 @@ class ChatRowView: TableRowView, Notifable, MultipleSelectable { contentView.removeAllSubviews() } + override func convertWindowPointToContent(_ point: NSPoint) -> NSPoint { + return contentView.convert(point, from: nil) + } + override func viewDidMoveToWindow() { super.viewDidMoveToWindow() if let item = self.item as? ChatRowItem { @@ -586,4 +1725,159 @@ class ChatRowView: TableRowView, Notifable, MultipleSelectable { } } + + // swiping methods + + private var swipingRightView: View = View() + + private var animateOnceAfterDelta: Bool = true + + var additionalRevealDelta: CGFloat { + return 0 + } + + var containerX: CGFloat { + return rowView.frame.minX + } + var width: CGFloat { + return rowView.frame.width + } + + var rightRevealWidth: CGFloat { + return 40 + } + + var leftRevealWidth: CGFloat { + return 0 + } + + var endRevealState: SwipeDirection? + + func initRevealState() { + swipingRightView.removeAllSubviews() + swipingRightView.setFrameSize(rightRevealWidth, frame.height) + + + guard let item = item as? ChatRowItem else {return} + + let control = ImageButton() + control.disableActions() + + + if item.isBubbled && item.presentation.backgroundMode.hasWallpaper { + control.set(image: item.presentation.chat.chat_reply_swipe_bubble(theme: item.presentation), for: .Normal) + control.autohighlight = false + _ = control.sizeToFit() + control.setFrameSize(NSMakeSize(control.frame.width + 4, control.frame.height + 4)) + control.set(background: item.presentation.chatServiceItemColor, for: .Normal) + control.set(background: item.presentation.chatServiceItemColor.withAlphaComponent(0.8), for: .Highlight) + + + + control.layer?.cornerRadius = control.frame.height / 2 + } else { + control.set(image: item.presentation.icons.chat_swipe_reply, for: .Normal) + _ = control.sizeToFit() + control.background = .clear + } + swipingRightView.addSubview(control) + + control.centerY() + + } + + var hasRevealState: Bool { + return !swipingRightView.subviews.isEmpty + } + + func moveReveal(delta: CGFloat) { + if swipingRightView.subviews.isEmpty { + initRevealState() + } + + let delta = delta - additionalRevealDelta + + + rowView.setFrameOrigin(NSMakePoint(delta, rowView.frame.minY)) + swipingRightView.change(pos: NSMakePoint(frame.width + delta, swipingRightView.frame.minY), animated: false) + + swipingRightView.change(size: NSMakeSize(max(rightRevealWidth, -delta), swipingRightView.frame.height), animated: false) + + + + let subviews = swipingRightView.subviews + let action = subviews[0] + action.centerY() + + if swipingRightView.frame.width > 100 { + if animateOnceAfterDelta { + animateOnceAfterDelta = false + NSHapticFeedbackManager.defaultPerformer.perform(.alignment, performanceTime: .drawCompleted) + action.layer?.animatePosition(from: NSMakePoint((swipingRightView.frame.width - action.frame.width), 0), to: NSMakePoint(0, 0), duration: 0.2, timingFunction: CAMediaTimingFunctionName.spring, removeOnCompletion: true, additive: true) + } + action.setFrameOrigin(NSMakePoint(0, action.frame.minY)) + } else { + if !animateOnceAfterDelta { + animateOnceAfterDelta = true + NSHapticFeedbackManager.defaultPerformer.perform(.alignment, performanceTime: .drawCompleted) + action.layer?.animatePosition(from: NSMakePoint(-(swipingRightView.frame.width), 0), to: NSMakePoint(0, 0), duration: 0.2, timingFunction: CAMediaTimingFunctionName.spring, removeOnCompletion: true, additive: true) + } + action.setFrameOrigin(NSMakePoint(max(swipingRightView.frame.width, 0), action.frame.minY)) + } + + } + + func completeReveal(direction: SwipeDirection) { + + if swipingRightView.subviews.isEmpty { + initRevealState() + } + + CATransaction.begin() + + let updateRightSubviews:(Bool) -> Void = { [weak self] animated in + guard let `self` = self else {return} + let subviews = self.swipingRightView.subviews + subviews[0]._change(pos: NSMakePoint(0, subviews[0].frame.minY), animated: animated, completion: { [weak self] completed in + self?.swipingRightView.removeAllSubviews() + }) + } + + let failed:(@escaping(Bool)->Void)->Void = { [weak self] completion in + guard let `self` = self else {return} + self.rowView.change(pos: NSMakePoint(0, self.rowView.frame.minY), animated: true) + self.swipingRightView.change(pos: NSMakePoint(self.frame.width, self.swipingRightView.frame.minY), animated: true, completion: completion) + updateRightSubviews(true) + self.endRevealState = nil + } + + + + + switch direction { + case .left: + failed({_ in}) + case .right: + let invokeRightAction = swipingRightView.frame.width > 100 + if invokeRightAction { + _ = (item as? ChatRowItem)?.replyAction() + } + failed({ completed in }) + default: + self.endRevealState = nil + failed({_ in}) + } + + CATransaction.commit() + } + + + override var interactableView: NSView { + return self.rightView + } + + override func removeFromSuperview() { + super.removeFromSuperview() + } + } diff --git a/Telegram-Mac/ChatScheduleController.swift b/Telegram-Mac/ChatScheduleController.swift new file mode 100644 index 0000000000..ed45f6f56e --- /dev/null +++ b/Telegram-Mac/ChatScheduleController.swift @@ -0,0 +1,53 @@ +// +// ChatScheduleController.swift +// Telegram +// +// Created by Mikhail Filimonov on 13/08/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import Postbox +import SwiftSignalKit + +class ChatScheduleController: ChatController { + public override init(context: AccountContext, chatLocation:ChatLocation, mode: ChatMode = .scheduled, messageId:MessageId? = nil, initialAction:ChatInitialAction? = nil, chatLocationContextHolder: Atomic = Atomic(value: nil)) { + super.init(context: context, chatLocation: chatLocation, mode: mode, messageId: messageId, initialAction: initialAction, chatLocationContextHolder: chatLocationContextHolder) + } + + + override var removeAfterDisapper: Bool { + return true + } + + override func viewDidLoad() { + super.viewDidLoad() + chatInteraction.sendPlainText = { _ in + + } + let context = self.context + + chatInteraction.requestMessageActionCallback = { _, _, _ in + alert(for: context.window, info: L10n.chatScheduledInlineButtonError) + } + + chatInteraction.vote = { _, _, _ in + alert(for: context.window, info: L10n.chatScheduledInlineButtonError) + } + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + let controller = self.navigationController?.controller as? ChatController + let current = self.chatInteraction.presentation.interfaceState + + let count = self.genericView.tableView.count + + controller?.chatInteraction.update(animated: false, { $0.withUpdatedHasScheduled(count > 1).updatedInterfaceState { _ in return current } }) + + } + +} diff --git a/Telegram-Mac/ChatSearchView.swift b/Telegram-Mac/ChatSearchView.swift index f45f743a53..c8ea24661b 100644 --- a/Telegram-Mac/ChatSearchView.swift +++ b/Telegram-Mac/ChatSearchView.swift @@ -8,29 +8,13 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac +import SwiftSignalKit enum TokenSearchState : Equatable { case none case from(query: String, complete: Bool) } -func ==(lhs: TokenSearchState, rhs: TokenSearchState) -> Bool { - switch lhs { - case .none: - if case .none = rhs { - return true - } else { - return false - } - case let .from(query, complete): - if case .from(query, complete) = rhs { - return true - } else { - return false - } - } -} class ChatSearchView: SearchView { private let fromView: TextView = TextView() @@ -49,7 +33,7 @@ class ChatSearchView: SearchView { countView.removeFromSuperview() updateClearVisibility(true) } - updateLocalizationAndTheme() + updateLocalizationAndTheme(theme: theme) self.needsLayout = true } } @@ -69,8 +53,8 @@ class ChatSearchView: SearchView { fatalError("init(coder:) has not been implemented") } - override func updateLocalizationAndTheme() { - let fromLayout = TextViewLayout(.initialize(string: "\(tr(.chatSearchFrom)) ", color: theme.colors.text, font: .normal(.text))) + override func updateLocalizationAndTheme(theme: PresentationTheme) { + let fromLayout = TextViewLayout(.initialize(string: "\(tr(L10n.chatSearchFrom)) ", color: theme.colors.text, font: .normal(.text))) fromLayout.measure(width: .greatestFiniteMagnitude) fromView.update(fromLayout) fromView.backgroundColor = theme.colors.grayBackground @@ -78,10 +62,10 @@ class ChatSearchView: SearchView { countView.backgroundColor = theme.colors.grayBackground - let countLayout = TextViewLayout(.initialize(string: tr(.chatSearchCount(countValue.current, countValue.total)), color: theme.search.placeholderColor, font: .normal(.text))) + let countLayout = TextViewLayout(.initialize(string: tr(L10n.chatSearchCount(countValue.current, countValue.total)), color: theme.search.placeholderColor, font: .normal(.text))) countLayout.measure(width: .greatestFiniteMagnitude) countView.update(countLayout) - super.updateLocalizationAndTheme() + super.updateLocalizationAndTheme(theme: theme) } override func cancelSearch() { @@ -104,7 +88,7 @@ class ChatSearchView: SearchView { } } - func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { + override func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { if commandSelector == #selector(deleteBackward(_:)) { if query.isEmpty { switch tokenState { @@ -180,7 +164,7 @@ class ChatSearchView: SearchView { tokenView.removeFromSuperview() } } - updateLocalizationAndTheme() + updateLocalizationAndTheme(theme: theme) self.needsLayout = true } } diff --git a/Telegram-Mac/ChatSelectText.swift b/Telegram-Mac/ChatSelectText.swift index 6beeb5d5ef..5f40ecaf26 100644 --- a/Telegram-Mac/ChatSelectText.swift +++ b/Telegram-Mac/ChatSelectText.swift @@ -8,80 +8,201 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit struct SelectContainer { - let text:String + let text:NSAttributedString let range:NSRange let header:String? } class SelectManager : NSResponder { + fileprivate weak var chatInteraction: ChatInteraction? + override init() { + super.init() + } - private var ranges:[(AnyHashable,WeakReference, SelectContainer)] = [] + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } - func add(range:NSRange, textView: TextView, text: String, header: String?, stableId: AnyHashable) { - ranges.append((stableId, WeakReference(value: textView), SelectContainer(text: text, range: range, header: header))) + private var ranges:Atomic<[(AnyHashable,WeakReference, SelectContainer)]> = Atomic(value: []) + + func add(range:NSRange, textView: TextView, text: NSAttributedString, header: String?, stableId: AnyHashable) { + _ = ranges.modify { ranges in + var ranges = ranges + ranges.append((stableId, WeakReference(value: textView), SelectContainer(text: text, range: range, header: header))) + return ranges + } } func removeAll() { - for selection in ranges { - if let value = selection.1.value { - value.layout?.clearSelect() - value.canBeResponder = true - value.setNeedsDisplay() + _ = ranges.modify { ranges in + for selection in ranges { + if let value = selection.1.value { + value.layout?.clearSelect() + value.canBeResponder = true + value.setNeedsDisplay() + } } + return [] } - ranges.removeAll() } func remove(for id:Int64) { } var isEmpty:Bool { - return ranges.isEmpty + return ranges.with { $0.isEmpty } } - @objc func copy(_ sender:Any) { - - var string:String = "" - - for i in stride(from: ranges.count - 1, to: -1, by: -1) { - let container = ranges[i].2 - if let header = container.header, ranges.count > 1 { - string += header + "\n" - } - - if container.range.location != NSNotFound { - if container.range.location != 0, ranges.count > 1 { - string += "..." + var selectedText: NSAttributedString { + let string:NSMutableAttributedString = NSMutableAttributedString() + _ = ranges.with { ranges in + for i in stride(from: ranges.count - 1, to: -1, by: -1) { + let container = ranges[i].2 + if let header = container.header, ranges.count > 1 { + _ = string.append(string: header + "\n", color: nil, font: .normal(.text)) } - string += container.text.nsstring.substring(with: container.range) - if container.range.location + container.range.length != container.text.length, ranges.count > 1 { - string += "..." + + if container.range.location != NSNotFound { + if container.range.location != 0, ranges.count > 1 { + _ = string.append(string: "...", color: nil, font: .normal(.text)) + } + string.append(container.text.attributedSubstring(from: container.range)) + if container.range.location + container.range.length != container.text.length, ranges.count > 1 { + _ = string.append(string: "...", color: nil, font: .normal(.text)) + } + } + + if i != 0 { + _ = string.append(string: "\n\n", color: nil, font: .normal(.text)) } } - - if i != 0 { - string += "\n\n" + } + return string + } + + @objc func copy(_ sender:Any) { + let selectedText = self.selectedText + if !selectedText.string.isEmpty { + if !globalLinkExecutor.copyAttributedString(selectedText) { + NSPasteboard.general.declareTypes([.string], owner: self) + NSPasteboard.general.setString(selectedText.string, forType: .string) + } + } else if let chatInteraction = self.chatInteraction { + if let selectionState = chatInteraction.presentation.selectionState { + _ = chatInteraction.context.account.postbox.messagesAtIds(Array(selectionState.selectedIds.sorted(by: <))).start(next: { messages in + var text: String = "" + for message in messages { + if !text.isEmpty { + text += "\n\n" + } + if let forwardInfo = message.forwardInfo { + text += "> " + forwardInfo.authorTitle + ":" + } else { + text += "> " + (message.effectiveAuthor?.displayTitle ?? "") + ":" + } + text += "\n" + text += pullText(from: message) as String + } + copyToClipboard(text) + }) } } - let pb = NSPasteboard.general - pb.declareTypes([.string], owner: self) - pb.setString(string, forType: .string) - + } + + func selectNextChar() -> Bool { + var result: Bool = false + _ = ranges.modify { ranges in + var ranges = ranges + if let last = ranges.last, let textView = last.1.value { + if last.2.range.max < last.2.text.length, let layout = textView.layout { + + var range = last.2.range + + switch layout.selectedRange.cursorAlignment { + case let .min(cursorAlignment), let .max(cursorAlignment): + if range.min >= cursorAlignment { + range.length += 1 + } else { + range.location += 1 + if range.length > 1 { + range.length -= 1 + } + } + } + let location = min(max(0, range.location), last.2.text.length) + let length = max(min(range.length, last.2.text.length - location), 0) + range = NSMakeRange(location, length) + + layout.selectedRange.range = range + ranges[ranges.count - 1] = (last.0, last.1, SelectContainer(text: last.2.text, range: range, header: last.2.header)) + textView.needsDisplay = true + result = true + return ranges + } + } + result = false + return ranges + } + return result + } + + func selectPrevChar() -> Bool { + var result: Bool = false + _ = ranges.modify { ranges in + var ranges = ranges + if let first = ranges.first, let textView = first.1.value { + if let layout = textView.layout { + + var range = first.2.range + + switch layout.selectedRange.cursorAlignment { + case let .min(cursorAlignment), let .max(cursorAlignment): + if range.location >= cursorAlignment { + if range.length > 1 { + range.length -= 1 + } else { + range.location -= 1 + } + } else { + if range.location > 0 { + range.location -= 1 + range.length += 1 + } + } + } + + let location = min(max(0, range.location), first.2.text.length) + let length = max(min(range.length, first.2.text.length - location), 0) + range = NSMakeRange(location, length) + layout.selectedRange.range = range + ranges[0] = (first.0, first.1, SelectContainer(text: first.2.text, range: range, header: first.2.header)) + textView.needsDisplay = true + result = true + return ranges + } + } + result = false + return ranges + } + return result } func find(_ stableId:AnyHashable) -> NSRange? { - for range in ranges { - if range.0 == stableId { - return range.2.range + return ranges.with { ranges -> NSRange? in + for range in ranges { + if range.0 == stableId { + return range.2.range + } } + return nil } - return nil } override func becomeFirstResponder() -> Bool { @@ -94,10 +215,11 @@ class SelectManager : NSResponder { } } -let selectManager:SelectManager = { - let manager = SelectManager() - return manager -}() +let selectManager:SelectManager = SelectManager() + +func initializeSelectManager() { + _ = selectManager.isEmpty +} protocol MultipleSelectable { var selectableTextViews:[TextView] { get } @@ -114,16 +236,24 @@ class ChatSelectText : NSObject { private var startMessageId:MessageId? = nil private var lastPressureEventStage = 0 private var inPressedState = false + private var locationInWindow: NSPoint? = nil + + private var lastSelectdMessageId: MessageId? init(_ table:TableView) { self.table = table } - + deinit { + var bp:Int = 0 + bp += 1 + } func initializeHandlers(for window:Window, chatInteraction:ChatInteraction) { - table.addScroll(listener: TableScrollListener ({ [weak table] _ in + selectManager.chatInteraction = chatInteraction + + table.addScroll(listener: TableScrollListener (dispatchWhenVisibleRangeUpdated: false, { [weak table] _ in table?.enumerateVisibleViews(with: { view in view.updateMouse() }) @@ -135,108 +265,177 @@ class ChatSelectText : NSObject { view.updateMouse() }) - return .invokeNext + return .rejected }, with: self, for: .mouseMoved, priority:.medium) window.set(mouseHandler: { [weak self] event -> KeyHandlerResult in self?.started = false self?.inPressedState = false + self?.locationInWindow = event.locationInWindow if let table = self?.table, let superview = table.superview, let documentView = table.documentView { - let point = superview.convert(window.mouseLocationOutsideOfEventStream, from: nil) - let documentPoint = documentView.convert(window.mouseLocationOutsideOfEventStream, from: nil) + let point = superview.convert(event.locationInWindow, from: nil) + let documentPoint = documentView.convert(event.locationInWindow, from: nil) let row = table.row(at: documentPoint) - if !NSPointInRect(point, table.frame) { - self?.beginInnerLocation = NSZeroPoint + + var isCurrentTableView: (NSView?)->Bool = { _ in return false} + + isCurrentTableView = { [weak table] view in + if view === table { + return true + } else if let superview = view?.superview { + if superview is TableView, view is TableRowView || view is NSClipView { + return isCurrentTableView(superview) + } else if superview is TableView { + return false + } else { + return isCurrentTableView(superview) + } + } else { + return false + } + } + + if row < 0 || (!NSPointInRect(point, table.frame) || hasModals(window) || (!table.item(at: row).canMultiselectTextIn(event.locationInWindow) && chatInteraction.presentation.state != .selecting)) || !isCurrentTableView(window.contentView?.hitTest(event.locationInWindow)) { self?.beginInnerLocation = NSZeroPoint } else { self?.beginInnerLocation = documentPoint } - Queue.mainQueue().justDispatch { [weak self] in - if chatInteraction.presentation.state == .selecting { - if let beginInnerLocation = self?.beginInnerLocation, let selectionState = chatInteraction.presentation.selectionState { - let row = table.row(at: beginInnerLocation) - if row != -1, let item = table.item(at: row) as? ChatRowItem, let message = item.message { - if self?.startMessageId == nil { - self?.startMessageId = message.id - } - self?.deselect = !selectionState.selectedIds.contains(message.id) - } - } - } else { - if let view = table.viewNecessary(at: row) as? ChatRowView, !view.canStartTextSelecting(event) { - self?.beginInnerLocation = NSZeroPoint + + + if row != -1, let item = table.item(at: row) as? ChatRowItem, let view = item.view as? ChatRowView { + if chatInteraction.presentation.state == .selecting || (theme.bubbled && !NSPointInRect(view.convert(event.locationInWindow, from: nil), view.bubbleFrame(item))) { + if self?.startMessageId == nil { + self?.startMessageId = item.message?.id } - self?.startMessageId = nil + self?.deselect = !view.isSelectInGroup(event.locationInWindow) } - self?.started = self?.beginInnerLocation != NSZeroPoint - } + + self?.started = self?.beginInnerLocation != NSZeroPoint } return .invokeNext - }, with: self, for: .leftMouseDown, priority:.medium) + }, with: self, for: .leftMouseDown, priority:.medium) - window.set(mouseHandler: { [weak self] (_) -> KeyHandlerResult in + window.set(mouseHandler: { [weak self] event -> KeyHandlerResult in + self?.beginInnerLocation = NSZeroPoint + self?.locationInWindow = nil - let point = self?.table.documentView?.convert(window.mouseLocationOutsideOfEventStream, from: nil) ?? NSZeroPoint - if let index = self?.table.row(at: point), index > 0, let item = self?.table.item(at: index) as? ChatRowItem { + Queue.mainQueue().justDispatch { + guard let table = self?.table else {return} + guard let documentView = table.documentView else {return} - if item.message?.id == self?.startMessageId { - if let result = item.chatInteraction.presentation.selectionState?.selectedIds.isEmpty, result { - self?.startMessageId = nil - item.chatInteraction.update({$0.withoutSelectionState()}) + var cleanStartId: Bool = false + let documentPoint = documentView.convert(event.locationInWindow, from: nil) + let row = table.row(at: documentPoint) + if chatInteraction.presentation.state != .selecting { + if let view = table.viewNecessary(at: row) as? ChatRowView, !view.canStartTextSelecting(event) { + self?.beginInnerLocation = NSZeroPoint } + cleanStartId = true } + + let point = self?.table.documentView?.convert(event.locationInWindow, from: nil) ?? NSZeroPoint + if let index = self?.table.row(at: point), index > 0, let item = self?.table.item(at: index), let view = item.view as? ChatRowView { + + if event.clickCount > 1, selectManager.isEmpty { + var set: Bool = false + inner: for view in view.selectableTextViews { + if view == window.firstResponder { + _ = window.makeFirstResponder(view) + set = true + break inner + } + } + if !set { + _ = window.makeFirstResponder(view.selectableTextViews.first) + } + } + + if chatInteraction.presentation.reportMode == nil { + if view.canDropSelection(in: event.locationInWindow) { + if let result = chatInteraction.presentation.selectionState?.selectedIds.isEmpty, result { + self?.startMessageId = nil + chatInteraction.update({$0.withoutSelectionState()}) + } + } + } + } else { + if chatInteraction.presentation.reportMode == nil { + if let result = chatInteraction.presentation.selectionState?.selectedIds.isEmpty, result { + self?.startMessageId = nil + chatInteraction.update({$0.withoutSelectionState()}) + } + } + } + if cleanStartId { + self?.startMessageId = nil + } } - - return .invokeNext - }, with: self, for: .leftMouseUp, priority:.medium) + }, with: self, for: .leftMouseUp, priority:.medium) window.set(mouseHandler: { [weak self] event -> KeyHandlerResult in - self?.endInnerLocation = self?.table.documentView?.convert(window.mouseLocationOutsideOfEventStream, from: nil) ?? NSZeroPoint - if let overView = window.contentView?.hitTest(window.mouseLocationOutsideOfEventStream) as? Control { - if overView.userInteractionEnabled == false { - self?.started = false - } + guard let `self` = self else {return .rejected} + +// if let locationInWindow = self.locationInWindow { +// let old = (ceil(locationInWindow.x), ceil(locationInWindow.y)) +// let new = (ceil(event.locationInWindow.x), round(event.locationInWindow.y)) +// if abs(old.0 - new.0) <= 1 && abs(old.1 - new.1) <= 1 { +// return .rejected +// } +// } + + self.endInnerLocation = self.table.documentView?.convert(window.mouseLocationOutsideOfEventStream, from: nil) ?? NSZeroPoint + +// if let overView = window.contentView?.hitTest(window.mouseLocationOutsideOfEventStream) as? Control { +// self?.started = overView.userInteractionEnabled == true +// } + if self.started { + self.started = !hasPopover(window) && self.beginInnerLocation != NSZeroPoint } - if self?.started == true { - self?.started = !hasPopover(window) + if event.clickCount > 1 { + self.started = false } - if self?.started == true { - - self?.table.clipView.autoscroll(with: event) - + // NSLog("\(!NSPointInRect(event.locationInWindow, window.bounds))") + + if self.started { + self.table.clipView.autoscroll(with: event) if chatInteraction.presentation.state != .selecting { - if window.firstResponder != selectManager { - window.makeFirstResponder(selectManager) - } - if self?.inPressedState == false { - self?.runSelector(window: window, chatInteraction: chatInteraction) + if !self.inPressedState { + self.runSelector(window: window, chatInteraction: chatInteraction) + if window.firstResponder != selectManager { + _ = window.makeFirstResponder(selectManager) + } } return .invoked } else if chatInteraction.presentation.state == .selecting { - self?.runSelector(false, window: window, chatInteraction:chatInteraction) - return .invoked + self.runSelector(false, window: window, chatInteraction: chatInteraction) + return .invokeNext } } return .invokeNext - }, with: self, for: .leftMouseDragged, priority:.medium) + }, with: self, for: .leftMouseDragged, priority:.medium) window.set(mouseHandler: { [weak self] (event) -> KeyHandlerResult in - guard let `self` = self else { return .invokeNext } + guard let `self` = self else { return .rejected } if event.stage == 2 && self.lastPressureEventStage < 2 { self.inPressedState = true } self.lastPressureEventStage = event.stage - return .invokeNext + return .rejected }, with: self, for: .pressure, priority: .medium) + + window.set(handler: { _ -> KeyHandlerResult in + + return .rejected + }, with: self, for: .A, priority: .medium, modifierFlags: [.command]) } private func runSelector(_ selectingText:Bool = true, window: Window, chatInteraction:ChatInteraction) { @@ -245,6 +444,7 @@ class ChatSelectText : NSObject { var startIndex = table.row(at: beginInnerLocation) var endIndex = table.row(at: endInnerLocation) + let reversed = endIndex < startIndex; if(endIndex < startIndex) { @@ -253,10 +453,26 @@ class ChatSelectText : NSObject { startIndex = startIndex - endIndex; } - if startIndex < 0 && endIndex < 0 { + if startIndex < 0 || endIndex < 0 { return } + let beginRow = table.row(at: beginInnerLocation) + if let view = table.item(at: beginRow).view as? ChatRowView, let item = view.item as? ChatRowItem, selectingText, table._mouseInside() { + let rowPoint = view.convert(beginInnerLocation, from: table.documentView) + if (!NSPointInRect(rowPoint, view.bubbleFrame(item)) && theme.bubbled) { + if startIndex != endIndex || abs(beginInnerLocation.y - endInnerLocation.y) > 10 { + for i in max(0,startIndex) ... min(endIndex,table.count - 1) { + let item = table.item(at: i) as? ChatRowItem + if let view = item?.view as? ChatRowView { + view.toggleSelected(deselect, in: window.mouseLocationOutsideOfEventStream) + } + } + } + return + } + } + if selectingText { selectManager.removeAll() @@ -266,6 +482,29 @@ class ChatSelectText : NSObject { for i in startIndex ... endIndex { let view = table.viewNecessary(at: i) as? MultipleSelectable if let views = view?.selectableTextViews { + + var start_j:Int? = nil + var end_j:Int? = nil + + inner: for j in 0 ..< views.count { + let selectableView = views[j] + let viewRect = selectableView.convert(CGRect(origin: .zero, size: selectableView.frame.size), to: table.documentView) + let rect = NSRect(x: viewRect.midX, y: min(beginInnerLocation.y, endInnerLocation.y), width: abs(endInnerLocation.x - beginInnerLocation.x), height: abs(endInnerLocation.y - beginInnerLocation.y)) + + if rect.intersects(viewRect) { + if start_j == nil { + start_j = j + } else { + start_j = min(start_j!, j) + } + if end_j == nil { + end_j = j + } else { + end_j = max(end_j!, j) + } + } + } + for j in 0 ..< views.count { let selectableView = views[j] @@ -280,6 +519,9 @@ class ChatSelectText : NSObject { } + + + if (i > startIndex && i < endIndex) { startPoint = NSMakePoint(0, 0); endPoint = NSMakePoint(layout.layoutSize.width, .greatestFiniteMagnitude); @@ -307,9 +549,33 @@ class ChatSelectText : NSObject { } } + if let start_j = start_j, let end_j = end_j, i == endIndex || i == startIndex { + if j < start_j || j > end_j { + continue + } else { + if end_j - start_j > 0 { + if beginInnerLocation.y > endInnerLocation.y { + if j <= start_j { + endPoint = NSMakePoint(layout.layoutSize.width, .greatestFiniteMagnitude); + } else { + startPoint = .zero + } + } else if beginInnerLocation.y < endInnerLocation.y { + if j > start_j { + endPoint = .zero + } else { + startPoint = NSMakePoint(layout.layoutSize.width, .greatestFiniteMagnitude); + } + } + } + + } + } + selectableView.canBeResponder = false layout.selectedRange.range = layout.selectedRange(startPoint:startPoint, currentPoint:endPoint) - selectManager.add(range: layout.selectedRange.range, textView: selectableView, text:layout.attributedString.string, header: view?.header, stableId: table.item(at: i).stableId) + layout.selectedRange.cursorAlignment = startPoint.x > endPoint.x ? .min(layout.selectedRange.range.max) : .max(layout.selectedRange.range.min) + selectManager.add(range: layout.selectedRange.range, textView: selectableView, text:layout.attributedString, header: view?.header, stableId: table.item(at: i).stableId) selectableView.setNeedsDisplay() @@ -319,23 +585,13 @@ class ChatSelectText : NSObject { } } else { - if let selectionState = chatInteraction.presentation.selectionState { - - var ids:Set = selectionState.selectedIds + if chatInteraction.presentation.state == .selecting { for i in max(0,startIndex) ... min(endIndex,table.count - 1) { - if let item = table.item(at: i) as? ChatRowItem, let message = item.message { - - if deselect { - ids.remove(message.id) - } else { - ids.insert(message.id) - } + let item = table.item(at: i) as? ChatRowItem + if let view = item?.view as? ChatRowView { + view.toggleSelected(deselect, in: window.mouseLocationOutsideOfEventStream) } - - } - chatInteraction.update({$0.withUpdatedSelectedMessages(ids)}) - } } diff --git a/Telegram-Mac/ChatServiceItem.swift b/Telegram-Mac/ChatServiceItem.swift index a047594a3e..9121a951c4 100644 --- a/Telegram-Mac/ChatServiceItem.swift +++ b/Telegram-Mac/ChatServiceItem.swift @@ -8,26 +8,53 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit class ChatServiceItem: ChatRowItem { + + static var photoSize = NSMakeSize(200, 200) let text:TextViewLayout private(set) var imageArguments:TransformImageArguments? private(set) var image:TelegramMediaImage? - override init(_ initialSize:NSSize, _ chatInteraction:ChatInteraction, _ account:Account, _ entry: ChatHistoryEntry) { + override init(_ initialSize:NSSize, _ chatInteraction:ChatInteraction, _ context: AccountContext, _ entry: ChatHistoryEntry, _ downloadSettings: AutomaticMediaDownloadSettings, theme: TelegramPresentationTheme) { let message:Message = entry.message! + + + + let linkColor: NSColor = theme.controllerBackgroundMode.hasWallpaper ? theme.chatServiceItemTextColor : entry.renderType == .bubble ? theme.chat.linkColor(true, entry.renderType == .bubble) : theme.colors.link + let grayTextColor: NSColor = theme.chatServiceItemTextColor let authorId:PeerId? = message.author?.id var authorName:String = "" if let displayTitle = message.author?.displayTitle { authorName = displayTitle - if account.peerId == message.author?.id { - authorName = tr(.chatServiceYou) + } + + let isIncoming: Bool = message.isIncoming(context.account, entry.renderType == .bubble) + + + let nameColor:(PeerId) -> NSColor = { peerId in + + if theme.controllerBackgroundMode.hasWallpaper { + return theme.chatServiceItemTextColor + } + + if messageMainPeer(message) is TelegramChannel || messageMainPeer(message) is TelegramGroup { + if let peer = messageMainPeer(message) as? TelegramChannel, case .broadcast(_) = peer.info { + return theme.chat.linkColor(isIncoming, entry.renderType == .bubble) + } else if context.peerId != peerId { + let value = abs(Int(peerId.id._internalGetInt64Value()) % 7) + return theme.chat.peerName(value) + } } + return theme.chat.linkColor(isIncoming, false) } + + let attributedString:NSMutableAttributedString = NSMutableAttributedString() if let media = message.media[0] as? TelegramMediaAction { @@ -36,32 +63,30 @@ class ChatServiceItem: ChatRowItem { switch media.action { case let .groupCreated(title: title): if !peer.isChannel { - let _ = attributedString.append(string: tr(.chatServiceGroupCreated(authorName, title)), color: theme.colors.grayText, font: NSFont.normal(.custom(theme.fontSize))) + let _ = attributedString.append(string: L10n.chatServiceGroupCreated1(authorName, title), color: grayTextColor, font: .normal(theme.fontSize)) if let authorId = authorId { let range = attributedString.string.nsstring.range(of: authorName) - if account.peerId != authorId { - attributedString.add(link:inAppLink.peerInfo(peerId:authorId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range) - } - attributedString.addAttribute(NSAttributedStringKey.font, value: NSFont.medium(.custom(theme.fontSize)), range: range) + attributedString.add(link:inAppLink.peerInfo(link: "", peerId:authorId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range, color: nameColor(authorId)) + attributedString.addAttribute(.font, value: NSFont.medium(theme.fontSize), range: range) } } else { - let _ = attributedString.append(string: tr(.chatServiceChannelCreated), color: theme.colors.grayText, font: NSFont.normal(.custom(theme.fontSize))) + let _ = attributedString.append(string: tr(L10n.chatServiceChannelCreated), color: grayTextColor, font: .normal(theme.fontSize)) } case let .addedMembers(peerIds): if peerIds.first == authorId { - let _ = attributedString.append(string: tr(.chatServiceGroupAddedSelf(authorName)), color: theme.colors.grayText, font: NSFont.normal(.custom(theme.fontSize))) + let _ = attributedString.append(string: tr(L10n.chatServiceGroupAddedSelf(authorName)), color: grayTextColor, font: NSFont.normal(theme.fontSize)) } else { - let _ = attributedString.append(string: tr(.chatServiceGroupAddedMembers(authorName, "")), color: theme.colors.grayText, font: NSFont.normal(.custom(theme.fontSize))) + let _ = attributedString.append(string: tr(L10n.chatServiceGroupAddedMembers1(authorName, "")), color: grayTextColor, font: NSFont.normal(theme.fontSize)) for peerId in peerIds { if let peer = message.peers[peerId] { - let range = attributedString.append(string: peer.displayTitle, color: theme.colors.link, font: .medium(.custom(theme.fontSize))) - attributedString.add(link:inAppLink.peerInfo(peerId:peerId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range) + let range = attributedString.append(string: peer.displayTitle, color: nameColor(peer.id), font: .medium(theme.fontSize)) + attributedString.add(link:inAppLink.peerInfo(link: "", peerId:peerId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range, color: nameColor(peerId)) if peerId != peerIds.last { - _ = attributedString.append(string: ", ", color: theme.colors.grayText, font: .normal(.custom(theme.fontSize))) + _ = attributedString.append(string: ", ", color: grayTextColor, font: .normal(theme.fontSize)) } } @@ -69,25 +94,22 @@ class ChatServiceItem: ChatRowItem { } if let authorId = authorId { let range = attributedString.string.nsstring.range(of: authorName) - if account.peerId != authorId { - attributedString.add(link:inAppLink.peerInfo(peerId:authorId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range) - } - attributedString.addAttribute(NSAttributedStringKey.font, value: NSFont.medium(.custom(theme.fontSize)), range: range) - + attributedString.add(link:inAppLink.peerInfo(link: "", peerId:authorId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range, color: nameColor(authorId)) + attributedString.addAttribute(.font, value: NSFont.medium(theme.fontSize), range: range) } case let .removedMembers(peerIds): if peerIds.first == message.author?.id { - let _ = attributedString.append(string: tr(.chatServiceGroupRemovedSelf(authorName)), color: theme.colors.grayText, font: NSFont.normal(.custom(theme.fontSize))) + let _ = attributedString.append(string: tr(L10n.chatServiceGroupRemovedSelf(authorName)), color: grayTextColor, font: .normal(theme.fontSize)) } else { - let _ = attributedString.append(string: tr(.chatServiceGroupRemovedMembers(authorName, "")), color: theme.colors.grayText, font: NSFont.normal(.custom(theme.fontSize))) + let _ = attributedString.append(string: tr(L10n.chatServiceGroupRemovedMembers1(authorName, "")), color: grayTextColor, font: .normal(theme.fontSize)) for peerId in peerIds { if let peer = message.peers[peerId] { - let range = attributedString.append(string: peer.displayTitle, color: theme.colors.link, font: .medium(.custom(theme.fontSize))) - attributedString.add(link:inAppLink.peerInfo(peerId:peerId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range) + let range = attributedString.append(string: peer.displayTitle, color: nameColor(peerId), font: .medium(theme.fontSize)) + attributedString.add(link:inAppLink.peerInfo(link: "", peerId:peerId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range, color: nameColor(peer.id)) if peerId != peerIds.last { - _ = attributedString.append(string: ", ", color: theme.colors.grayText, font: .normal(.custom(theme.fontSize))) + _ = attributedString.append(string: ", ", color: grayTextColor, font: .normal(theme.fontSize)) } } @@ -95,120 +117,153 @@ class ChatServiceItem: ChatRowItem { } if let authorId = authorId { let range = attributedString.string.nsstring.range(of: authorName) - if account.peerId != authorId { - attributedString.add(link:inAppLink.peerInfo(peerId:authorId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range) - } - attributedString.addAttribute(NSAttributedStringKey.font, value: NSFont.medium(.custom(theme.fontSize)), range: range) - + attributedString.add(link:inAppLink.peerInfo(link: "", peerId:authorId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range, color: nameColor(authorId)) + attributedString.addAttribute(NSAttributedString.Key.font, value: NSFont.medium(theme.fontSize), range: range) } case let .photoUpdated(image): - if let _ = image { - let _ = attributedString.append(string: peer.isChannel ? tr(.chatServiceChannelUpdatedPhoto) : tr(.chatServiceGroupUpdatedPhoto(authorName)), color: theme.colors.grayText, font: .normal(.custom(theme.fontSize))) - let size = NSMakeSize(70, 70) - imageArguments = TransformImageArguments(corners: ImageCorners(radius: size.width / 2), imageSize: size, boundingSize: size, intrinsicInsets: NSEdgeInsets()) + if let image = image { + + let text: String + if image.videoRepresentations.isEmpty { + text = peer.isChannel ? L10n.chatServiceChannelUpdatedPhoto : L10n.chatServiceGroupUpdatedPhoto(authorName) + } else { + text = peer.isChannel ? L10n.chatServiceChannelUpdatedVideo : L10n.chatServiceGroupUpdatedVideo(authorName) + } + + let _ = attributedString.append(string: text, color: grayTextColor, font: .normal(theme.fontSize)) + let size = ChatServiceItem.photoSize + imageArguments = TransformImageArguments(corners: ImageCorners(radius: 10), imageSize: size, boundingSize: size, intrinsicInsets: NSEdgeInsets()) } else { - let _ = attributedString.append(string: peer.isChannel ? tr(.chatServiceChannelRemovedPhoto) : tr(.chatServiceGroupRemovedPhoto(authorName)), color: theme.colors.grayText, font: NSFont.normal(.custom(theme.fontSize))) + let _ = attributedString.append(string: peer.isChannel ? L10n.chatServiceChannelRemovedPhoto : L10n.chatServiceGroupRemovedPhoto(authorName), color: grayTextColor, font: NSFont.normal(theme.fontSize)) } if let authorId = authorId { let range = attributedString.string.nsstring.range(of: authorName) - if account.peerId != authorId { - attributedString.add(link:inAppLink.peerInfo(peerId:authorId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range) - } - attributedString.addAttribute(NSAttributedStringKey.font, value: NSFont.medium(.custom(theme.fontSize)), range: range) - + attributedString.add(link:inAppLink.peerInfo(link: "", peerId:authorId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range, color: nameColor(authorId)) + attributedString.addAttribute(NSAttributedString.Key.font, value: NSFont.medium(theme.fontSize), range: range) } self.image = image case let .titleUpdated(title): - let _ = attributedString.append(string: peer.isChannel ? tr(.chatServiceChannelUpdatedTitle(title)) : tr(.chatServiceGroupUpdatedTitle(authorName, title)), color: theme.colors.grayText, font: NSFont.normal(.custom(theme.fontSize))) + let _ = attributedString.append(string: peer.isChannel ? tr(L10n.chatServiceChannelUpdatedTitle(title)) : tr(L10n.chatServiceGroupUpdatedTitle1(authorName, title)), color: grayTextColor, font: NSFont.normal(theme.fontSize)) + if let authorId = authorId { + + let range = attributedString.string.nsstring.range(of: authorName) + attributedString.add(link:inAppLink.peerInfo(link: "", peerId:authorId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range, color: nameColor(authorId)) + attributedString.addAttribute(.font, value: NSFont.medium(theme.fontSize), range: range) + } + case .customText(let text, _): + let _ = attributedString.append(string: text, color: grayTextColor, font: NSFont.normal(theme.fontSize)) case .pinnedMessageUpdated: var replyMessageText = "" + var pinnedId: MessageId? for attribute in message.attributes { if let attribute = attribute as? ReplyMessageAttribute, let message = message.associatedMessages[attribute.messageId] { - replyMessageText = pullText(from: message) as String + replyMessageText = message.restrictedText(context.contentSettings) ?? pullText(from: message) as String + pinnedId = attribute.messageId } } - var cutted = replyMessageText.prefix(30) - if cutted.length != replyMessageText.length { - cutted += "..." + let cutted = replyMessageText.prefixWithDots(30) + _ = attributedString.append(string: tr(L10n.chatServiceGroupUpdatedPinnedMessage1(authorName, cutted)), color: grayTextColor, font: NSFont.normal(theme.fontSize)) + let pinnedRange = attributedString.string.nsstring.range(of: cutted) + if pinnedRange.location != NSNotFound { + attributedString.add(link: inAppLink.callback("", { [weak chatInteraction] _ in + if let pinnedId = pinnedId { + chatInteraction?.focusMessageId(nil, pinnedId, .CenterEmpty) + } + }), for: pinnedRange, color: grayTextColor) + attributedString.addAttribute(NSAttributedString.Key.font, value: NSFont.medium(theme.fontSize), range: pinnedRange) } - let _ = attributedString.append(string: tr(.chatServiceGroupUpdatedPinnedMessage(authorName, cutted)), color: theme.colors.grayText, font: NSFont.normal(.custom(theme.fontSize))) + + + if let authorId = authorId { let range = attributedString.string.nsstring.range(of: authorName) - attributedString.add(link:inAppLink.peerInfo(peerId:authorId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range) - attributedString.addAttribute(NSAttributedStringKey.font, value: NSFont.medium(.custom(theme.fontSize)), range: range) + attributedString.add(link:inAppLink.peerInfo(link: "", peerId:authorId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range, color: nameColor(authorId)) + attributedString.addAttribute(NSAttributedString.Key.font, value: NSFont.medium(theme.fontSize), range: range) + } case .joinedByLink: - let _ = attributedString.append(string: tr(.chatServiceGroupJoinedByLink(authorName)), color: theme.colors.grayText, font: .normal(.custom(theme.fontSize))) + let _ = attributedString.append(string: tr(L10n.chatServiceGroupJoinedByLink(authorName)), color: grayTextColor, font: .normal(theme.fontSize)) if let authorId = authorId { let range = attributedString.string.nsstring.range(of: authorName) - if account.peerId != authorId { - attributedString.add(link:inAppLink.peerInfo(peerId:authorId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range) - } - attributedString.addAttribute(NSAttributedStringKey.font, value: NSFont.medium(.custom(theme.fontSize)), range: range) - + attributedString.add(link:inAppLink.peerInfo(link: "", peerId:authorId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range, color: nameColor(authorId)) + attributedString.addAttribute(.font, value: NSFont.medium(theme.fontSize), range: range) } case .channelMigratedFromGroup, .groupMigratedToChannel: - let _ = attributedString.append(string: tr(.chatServiceGroupMigratedToSupergroup), color: theme.colors.grayText, font: NSFont.normal(.custom(theme.fontSize))) + let _ = attributedString.append(string: tr(L10n.chatServiceGroupMigratedToSupergroup), color: grayTextColor, font: NSFont.normal(theme.fontSize)) case let .messageAutoremoveTimeoutUpdated(seconds): if let authorId = authorId { - if authorId == account.peerId { + if authorId == context.peerId { if seconds > 0 { - let _ = attributedString.append(string: tr(.chatServiceSecretChatSetTimerSelf(autoremoveLocalized(Int(seconds)))), color: theme.colors.grayText, font: NSFont.normal(.custom(theme.fontSize))) + let _ = attributedString.append(string: tr(L10n.chatServiceSecretChatSetTimerSelf1(autoremoveLocalized(Int(seconds)))), color: grayTextColor, font: NSFont.normal(theme.fontSize)) } else { - let _ = attributedString.append(string: tr(.chatServiceSecretChatDisabledTimerSelf), color: theme.colors.grayText, font: NSFont.normal(.custom(theme.fontSize))) + let _ = attributedString.append(string: tr(L10n.chatServiceSecretChatDisabledTimerSelf1), color: grayTextColor, font: NSFont.normal(theme.fontSize)) } } else { - if seconds > 0 { - let _ = attributedString.append(string: tr(.chatServiceSecretChatSetTimer(authorName, autoremoveLocalized(Int(seconds)))), color: theme.colors.grayText, font: NSFont.normal(.custom(theme.fontSize))) - } else { - let _ = attributedString.append(string: tr(.chatServiceSecretChatDisabledTimer(authorName)), color: theme.colors.grayText, font: NSFont.normal(.custom(theme.fontSize))) + if let peer = messageMainPeer(message) { + if peer.isGroup || peer.isSupergroup { + if seconds > 0 { + let _ = attributedString.append(string: L10n.chatServiceGroupSetTimer(autoremoveLocalized(Int(seconds))), color: grayTextColor, font: .normal(theme.fontSize)) + } else { + let _ = attributedString.append(string: tr(L10n.chatServiceGroupDisabledTimer), color: grayTextColor, font: .normal(theme.fontSize)) + } + } else if peer.isChannel { + if seconds > 0 { + let _ = attributedString.append(string: L10n.chatServiceChannelSetTimer(autoremoveLocalized(Int(seconds))), color: grayTextColor, font: .normal(theme.fontSize)) + } else { + let _ = attributedString.append(string: L10n.chatServiceChannelDisabledTimer, color: grayTextColor, font: .normal(theme.fontSize)) + } + } else { + if seconds > 0 { + let _ = attributedString.append(string: tr(L10n.chatServiceSecretChatSetTimer1(authorName, autoremoveLocalized(Int(seconds)))), color: grayTextColor, font: .normal(theme.fontSize)) + } else { + let _ = attributedString.append(string: tr(L10n.chatServiceSecretChatDisabledTimer1(authorName)), color: grayTextColor, font: .normal(theme.fontSize)) + } + } + let range = attributedString.string.nsstring.range(of: authorName) + attributedString.add(link:inAppLink.peerInfo(link: "", peerId:authorId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range, color: nameColor(authorId)) + attributedString.addAttribute(NSAttributedString.Key.font, value: NSFont.medium(theme.fontSize), range: range) } - let range = attributedString.string.nsstring.range(of: authorName) - attributedString.add(link:inAppLink.peerInfo(peerId:authorId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range) - attributedString.addAttribute(NSAttributedStringKey.font, value: NSFont.medium(.custom(theme.fontSize)), range: range) + } } case .historyScreenshot: - let _ = attributedString.append(string: tr(.chatServiceGroupTookScreenshot(authorName)), color: theme.colors.grayText, font: NSFont.normal(.custom(theme.fontSize))) + let _ = attributedString.append(string: tr(L10n.chatServiceGroupTookScreenshot(authorName)), color: grayTextColor, font: NSFont.normal(theme.fontSize)) if let authorId = authorId { let range = attributedString.string.nsstring.range(of: authorName) - if account.peerId != authorId { - attributedString.add(link:inAppLink.peerInfo(peerId:authorId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range) - } - attributedString.addAttribute(NSAttributedStringKey.font, value: NSFont.medium(.custom(theme.fontSize)), range: range) - + attributedString.add(link:inAppLink.peerInfo(link: "", peerId:authorId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range, color: nameColor(authorId)) + attributedString.addAttribute(.font, value: NSFont.medium(theme.fontSize), range: range) } - case let .phoneCall(callId: _, discardReason: reason, duration: duration): + case let .phoneCall(callId: _, discardReason: reason, duration: duration, _): if let reason = reason { switch reason { case .busy: - _ = attributedString.append(string: tr(.chatListServiceCallCancelled), color: theme.colors.grayText, font: NSFont.normal(.custom(theme.fontSize))) + _ = attributedString.append(string: tr(L10n.chatListServiceCallCancelled), color: grayTextColor, font: NSFont.normal(theme.fontSize)) case .disconnect: - _ = attributedString.append(string: tr(.chatListServiceCallDisconnected), color: theme.colors.grayText, font: NSFont.normal(.custom(theme.fontSize))) + _ = attributedString.append(string: tr(L10n.chatListServiceCallMissed), color: grayTextColor, font: NSFont.normal(theme.fontSize)) case .hangup: if let duration = duration { - if message.author?.id == account.peerId { - _ = attributedString.append(string: tr(.chatListServiceCallOutgoing(.durationTransformed(elapsed: Int(duration)))), color: theme.colors.grayText, font: NSFont.normal(.custom(theme.fontSize))) + if message.author?.id == context.peerId { + _ = attributedString.append(string: tr(L10n.chatListServiceCallOutgoing(.durationTransformed(elapsed: Int(duration)))), color: grayTextColor, font: NSFont.normal(theme.fontSize)) } else { - _ = attributedString.append(string: tr(.chatListServiceCallIncoming(.durationTransformed(elapsed: Int(duration)))), color: theme.colors.grayText, font: NSFont.normal(.custom(theme.fontSize))) + _ = attributedString.append(string: tr(L10n.chatListServiceCallIncoming(.durationTransformed(elapsed: Int(duration)))), color: grayTextColor, font: NSFont.normal(theme.fontSize)) } } case .missed: - _ = attributedString.append(string: tr(.chatListServiceCallMissed), color: theme.colors.grayText, font: NSFont.normal(.custom(theme.fontSize))) + _ = attributedString.append(string: tr(L10n.chatListServiceCallMissed), color: grayTextColor, font: NSFont.normal(theme.fontSize)) } } else if let duration = duration { - if authorId == account.peerId { - _ = attributedString.append(string: tr(.chatListServiceCallOutgoing(.durationTransformed(elapsed: Int(duration)))), color: theme.colors.grayText, font: NSFont.normal(.custom(theme.fontSize))) + if authorId == context.peerId { + _ = attributedString.append(string: tr(L10n.chatListServiceCallOutgoing(.durationTransformed(elapsed: Int(duration)))), color: grayTextColor, font: NSFont.normal(theme.fontSize)) } else { - _ = attributedString.append(string: tr(.chatListServiceCallIncoming(.durationTransformed(elapsed: Int(duration)))), color: theme.colors.grayText, font: NSFont.normal(.custom(theme.fontSize))) + _ = attributedString.append(string: tr(L10n.chatListServiceCallIncoming(.durationTransformed(elapsed: Int(duration)))), color: grayTextColor, font: NSFont.normal(theme.fontSize)) } } case let .gameScore(gameId: _, score: score): @@ -222,15 +277,12 @@ class ChatServiceItem: ChatRowItem { } } - if authorId == account.peerId { - _ = attributedString.append(string: authorName, color: theme.colors.grayText, font: NSFont.medium(.custom(theme.fontSize))) - _ = attributedString.append(string: " ") - } else if let authorId = authorId { - let range = attributedString.append(string: authorName, color: theme.colors.link, font: NSFont.medium(.custom(theme.fontSize))) - attributedString.add(link:inAppLink.peerInfo(peerId:authorId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range) + if let authorId = authorId { + let range = attributedString.append(string: authorName, color: linkColor, font: NSFont.medium(theme.fontSize)) + attributedString.add(link:inAppLink.peerInfo(link: "", peerId:authorId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range, color: nameColor(authorId)) _ = attributedString.append(string: " ") } - _ = attributedString.append(string: tr(.chatListServiceGameScored(Int(score), gameName)), color: theme.colors.grayText, font: NSFont.normal(.custom(theme.fontSize))) + _ = attributedString.append(string: tr(L10n.chatListServiceGameScored1Countable(Int(score), gameName)), color: grayTextColor, font: NSFont.normal(theme.fontSize)) case let .paymentSent(currency, totalAmount): var paymentMessage:Message? for attr in message.attributes { @@ -241,14 +293,186 @@ class ChatServiceItem: ChatRowItem { } } - if let message = paymentMessage, let media = message.media.first as? TelegramMediaInvoice, let peer = messageMainPeer(message) { - _ = attributedString.append(string: tr(.chatServicePaymentSent(TGCurrencyFormatter.shared().formatAmount(totalAmount, currency: currency), peer.displayTitle, media.title)), color: theme.colors.grayText, font: NSFont.normal(.custom(theme.fontSize))) - attributedString.detectBoldColorInString(with: NSFont.medium(.custom(theme.fontSize))) + if let paymentMessage = paymentMessage, let media = paymentMessage.media.first as? TelegramMediaInvoice, let peer = paymentMessage.peers[paymentMessage.id.peerId] { + _ = attributedString.append(string: tr(L10n.chatServicePaymentSent1(TGCurrencyFormatter.shared().formatAmount(totalAmount, currency: currency), peer.displayTitle, media.title)), color: grayTextColor, font: NSFont.normal(theme.fontSize)) + attributedString.detectBoldColorInString(with: .medium(theme.fontSize)) + + attributedString.add(link:inAppLink.callback("", { _ in + showModal(with: PaymentsReceiptController(context: context, messageId: message.id, message: paymentMessage), for: context.window) + }), for: attributedString.range, color: grayTextColor) + } else { - _ = attributedString.append(string: tr(.chatServicePaymentSent("", "", "")), color: theme.colors.grayText, font: NSFont.normal(.custom(theme.fontSize))) + _ = attributedString.append(string: L10n.chatServicePaymentSent1("", "", ""), color: grayTextColor, font: NSFont.normal(theme.fontSize)) } - default: + case let .botDomainAccessGranted(domain): + _ = attributedString.append(string: L10n.chatServiceBotPermissionAllowed(domain), color: grayTextColor, font: NSFont.normal(theme.fontSize)) + case let .botSentSecureValues(types): + let permissions = types.map({$0.rawValue}).joined(separator: ", ") + _ = attributedString.append(string: L10n.chatServiceSecureIdAccessGranted(peer.displayTitle, permissions), color: grayTextColor, font: NSFont.normal(theme.fontSize)) + case .peerJoined: + let _ = attributedString.append(string: L10n.chatServicePeerJoinedTelegram(authorName), color: grayTextColor, font: NSFont.normal(theme.fontSize)) + + if let authorId = authorId { + let range = attributedString.string.nsstring.range(of: authorName) + attributedString.add(link:inAppLink.peerInfo(link: "", peerId:authorId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range, color: nameColor(authorId)) + attributedString.addAttribute(.font, value: NSFont.medium(theme.fontSize), range: range) + } + case let .geoProximityReached(fromId, toId, distance): + let distanceString = stringForDistance(distance: Double(distance)) + let text: String + if fromId == context.peerId { + text = L10n.notificationProximityYouReached1(distanceString, message.peers[toId]?.displayTitle ?? "") + } else if toId == context.peerId { + text = L10n.notificationProximityReachedYou1(message.peers[fromId]?.displayTitle ?? "", distanceString) + } else { + text = L10n.notificationProximityReached1(message.peers[fromId]?.displayTitle ?? "", distanceString, message.peers[toId]?.displayTitle ?? "") + } + let _ = attributedString.append(string: text, color: grayTextColor, font: NSFont.normal(theme.fontSize)) + + if let authorId = authorId { + let range = attributedString.string.nsstring.range(of: authorName) + if range.location != NSNotFound { + attributedString.add(link:inAppLink.peerInfo(link: "", peerId:authorId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range, color: nameColor(authorId)) + attributedString.addAttribute(.font, value: NSFont.medium(theme.fontSize), range: range) + } + } + if let peer = message.peers[toId], !peer.displayTitle.isEmpty { + let range = attributedString.string.nsstring.range(of: peer.displayTitle) + if range.location != NSNotFound { + attributedString.add(link:inAppLink.peerInfo(link: "", peerId: peer.id, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range, color: nameColor(peer.id)) + attributedString.addAttribute(.font, value: NSFont.medium(theme.fontSize), range: range) + } + } + if let peer = message.peers[fromId], !peer.displayTitle.isEmpty { + let range = attributedString.string.nsstring.range(of: peer.displayTitle) + if range.location != NSNotFound { + attributedString.add(link:inAppLink.peerInfo(link: "", peerId: peer.id, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range, color: nameColor(peer.id)) + attributedString.addAttribute(.font, value: NSFont.medium(theme.fontSize), range: range) + } + } + case let .groupPhoneCall(callId, accessHash, scheduleDate, duration): + let text: String + if let duration = duration { + if peer.isChannel { + text = L10n.chatServiceVoiceChatFinishedChannel(autoremoveLocalized(Int(duration))) + } else if authorId == context.peerId { + text = L10n.chatServiceVoiceChatFinishedYou(autoremoveLocalized(Int(duration))) + } else { + text = L10n.chatServiceVoiceChatFinished(authorName, autoremoveLocalized(Int(duration))) + } + let _ = attributedString.append(string: text, color: grayTextColor, font: NSFont.normal(theme.fontSize)) + } else { + if peer.isChannel { + if let scheduled = scheduleDate { + text = L10n.chatServiceVoiceChatScheduledChannel(stringForMediumDate(timestamp: scheduled)) + } else { + text = L10n.chatServiceVoiceChatStartedChannel + } + } else if authorId == context.peerId { + if let scheduled = scheduleDate { + text = L10n.chatServiceVoiceChatScheduledYou(stringForMediumDate(timestamp: scheduled)) + } else { + text = L10n.chatServiceVoiceChatStartedYou + } + } else { + if let scheduled = scheduleDate { + text = L10n.chatServiceVoiceChatScheduled(authorName, stringForMediumDate(timestamp: scheduled)) + } else { + text = L10n.chatServiceVoiceChatStarted(authorName) + } + } + let parsed = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes.init(body: MarkdownAttributeSet(font: .normal(theme.fontSize), textColor: grayTextColor), bold: MarkdownAttributeSet(font: .medium(theme.fontSize), textColor: grayTextColor), link: MarkdownAttributeSet(font: .medium(theme.fontSize), textColor: linkColor), linkAttribute: { [weak chatInteraction] link in + return (NSAttributedString.Key.link.rawValue, inAppLink.callback("", { _ in + chatInteraction?.joinGroupCall(CachedChannelData.ActiveCall(id: callId, accessHash: accessHash, title: nil, scheduleTimestamp: scheduleDate, subscribedToScheduled: false), nil) + })) + })) + attributedString.append(parsed) + } + + if let authorId = authorId { + let range = attributedString.string.nsstring.range(of: authorName) + if range.location != NSNotFound { + attributedString.add(link:inAppLink.peerInfo(link: "", peerId:authorId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range, color: nameColor(authorId)) + attributedString.addAttribute(.font, value: NSFont.medium(theme.fontSize), range: range) + } + } + case let .setChatTheme(emoji): + let text: String + + if message.author?.id == context.peerId { + if emoji.isEmpty { + text = L10n.chatServiceDisabledThemeYou + } else { + text = L10n.chatServiceUpdateThemeYou(emoji) + } + } else { + if emoji.isEmpty { + text = L10n.chatServiceDisabledTheme(authorName) + } else { + text = L10n.chatServiceUpdateTheme(authorName, emoji) + } + } + + let parsed = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes.init(body: MarkdownAttributeSet(font: .normal(theme.fontSize), textColor: grayTextColor), bold: MarkdownAttributeSet(font: .medium(theme.fontSize), textColor: grayTextColor), link: MarkdownAttributeSet(font: .medium(theme.fontSize), textColor: linkColor), linkAttribute: { link in + return (NSAttributedString.Key.link.rawValue, inAppLink.callback("", { _ in + + })) + })) + attributedString.append(parsed) + + if let authorId = authorId { + let range = attributedString.string.nsstring.range(of: authorName) + if range.location != NSNotFound { + attributedString.add(link:inAppLink.peerInfo(link: "", peerId:authorId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range, color: nameColor(authorId)) + attributedString.addAttribute(.font, value: NSFont.medium(theme.fontSize), range: range) + } + } + + + case let .inviteToGroupPhoneCall(callId, accessHash, peerIds): + let text: String + + let list = NSMutableAttributedString() + for peerId in peerIds { + + if let peer = message.peers[peerId] { + let range = list.append(string: peer.displayTitle, color: nameColor(peerId), font: .medium(theme.fontSize)) + list.add(link:inAppLink.peerInfo(link: "", peerId:peerId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range, color: nameColor(peer.id)) + if peerId != peerIds.last { + _ = list.append(string: ", ", color: grayTextColor, font: .normal(theme.fontSize)) + } + } + } + + if message.author?.id == context.peerId { + text = L10n.chatServiceVoiceChatInvitationByYou("%mark%") + } else if peerIds.first == context.peerId { + text = L10n.chatServiceVoiceChatInvitationForYou(authorName) + } else { + text = L10n.chatServiceVoiceChatInvitation(authorName, "%mark%") + } + + let parsed = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes.init(body: MarkdownAttributeSet(font: .normal(theme.fontSize), textColor: grayTextColor), bold: MarkdownAttributeSet(font: .medium(theme.fontSize), textColor: grayTextColor), link: MarkdownAttributeSet(font: .medium(theme.fontSize), textColor: linkColor), linkAttribute: { [weak chatInteraction] link in + return (NSAttributedString.Key.link.rawValue, inAppLink.callback("", { _ in + chatInteraction?.joinGroupCall(CachedChannelData.ActiveCall(id: callId, accessHash: accessHash, title: nil, scheduleTimestamp: nil, subscribedToScheduled: false), nil) + })) + })) + attributedString.append(parsed) + let markRange = attributedString.string.nsstring.range(of: "%mark%") + if markRange.location != NSNotFound { + attributedString.replaceCharacters(in: markRange, with: list) + } + + if let authorId = authorId { + let range = attributedString.string.nsstring.range(of: authorName) + if range.location != NSNotFound { + attributedString.add(link:inAppLink.peerInfo(link: "", peerId:authorId, action:nil, openChat: false, postId: nil, callback: chatInteraction.openInfo), for: range, color: nameColor(authorId)) + attributedString.addAttribute(.font, value: NSFont.medium(theme.fontSize), range: range) + } + } + + default: break } } @@ -256,45 +480,52 @@ class ChatServiceItem: ChatRowItem { let text:String switch media.data { case .image: - text = tr(.serviceMessageExpiredPhoto) + text = tr(L10n.serviceMessageExpiredPhoto) case .file: if message.id.peerId.namespace == Namespaces.Peer.SecretChat { - text = tr(.serviceMessageExpiredFile) + text = tr(L10n.serviceMessageExpiredVideo) } else { - text = tr(.serviceMessageExpiredVideo) + text = tr(L10n.serviceMessageExpiredVideo) } } - _ = attributedString.append(string: text, color: theme.colors.grayText, font: .normal(.custom(theme.fontSize))) + _ = attributedString.append(string: text, color: grayTextColor, font: .normal(theme.fontSize)) } else if message.id.peerId.namespace == Namespaces.Peer.CloudUser, let _ = message.autoremoveAttribute { let isPhoto: Bool = message.media.first is TelegramMediaImage - if authorId == account.peerId { - _ = attributedString.append(string: isPhoto ? tr(.serviceMessageDesturctingPhotoYou(authorName)) : tr(.serviceMessageDesturctingVideoYou(authorName)), color: theme.colors.grayText, font: .normal(.custom(theme.fontSize))) + if authorId == context.peerId { + _ = attributedString.append(string: isPhoto ? tr(L10n.serviceMessageDesturctingPhotoYou(authorName)) : tr(L10n.serviceMessageDesturctingVideoYou(authorName)), color: grayTextColor, font: .normal(theme.fontSize)) } else if let _ = authorId { - _ = attributedString.append(string: isPhoto ? tr(.serviceMessageDesturctingPhoto(authorName)) : tr(.serviceMessageDesturctingVideo(authorName)), color: theme.colors.grayText, font: .normal(.custom(theme.fontSize))) + _ = attributedString.append(string: isPhoto ? tr(L10n.serviceMessageDesturctingPhoto(authorName)) : tr(L10n.serviceMessageDesturctingVideo(authorName)), color: grayTextColor, font: .normal(theme.fontSize)) } } text = TextViewLayout(attributedString, truncationType: .end, cutout: nil, alignment: .center) + text.mayItems = false text.interactions = globalLinkExecutor - super.init(initialSize, chatInteraction, entry) - self.account = account + super.init(initialSize, chatInteraction, entry, downloadSettings, theme: theme) } override func makeContentSize(_ width: CGFloat) -> NSSize { return NSZeroSize } + override var isBubbled: Bool { + return presentation.wallpaper.wallpaper != .none + } + override var height: CGFloat { - var height:CGFloat = text.layoutSize.height + 12 + var height:CGFloat = text.layoutSize.height + (isBubbled ? 0 : 12) if let imageArguments = imageArguments { - height += imageArguments.imageSize.height + 6 + height += imageArguments.imageSize.height + (isBubbled ? 9 : 6) } return height } override func makeSize(_ width: CGFloat, oldWidth:CGFloat) -> Bool { text.measure(width: width - 40) + if isBubbled { + text.generateAutoBlock(backgroundColor: presentation.chatServiceItemColor) + } return true } @@ -302,20 +533,20 @@ class ChatServiceItem: ChatRowItem { return ChatServiceRowView.self } - override func menuItems() -> Signal<[ContextMenuItem], Void> { + override func menuItems(in location: NSPoint) -> Signal<[ContextMenuItem], NoError> { var items:[ContextMenuItem] = [] let chatInteraction = self.chatInteraction if chatInteraction.presentation.state != .selecting { if let message = message, let peer = messageMainPeer(message) { - if peer.canSendMessage, !message.containsSecretMedia { - items.append(ContextMenuItem(tr(.messageContextReply), handler: { + if !message.containsSecretMedia, canReplyMessage(message, peerId: peer.id, mode: chatInteraction.mode) { + items.append(ContextMenuItem(L10n.messageContextReply1, handler: { chatInteraction.setupReplyMessage(message.id) })) } - if canDeleteMessage(message, account: account) { - items.append(ContextMenuItem(tr(.messageContextDelete), handler: { + if canDeleteMessage(message, account: context.account, mode: chatInteraction.mode) { + items.append(ContextMenuItem(L10n.messageContextDelete, handler: { chatInteraction.deleteMessages([message.id]) })) } @@ -326,3 +557,227 @@ class ChatServiceItem: ChatRowItem { } } + +class ChatServiceRowView: TableRowView { + + private var textView:TextView + private var imageView:TransformImageView? + + private var photoVideoView: MediaPlayerView? + private var photoVideoPlayer: MediaPlayer? + + required init(frame frameRect: NSRect) { + textView = TextView() + textView.isSelectable = false + //textView.userInteractionEnabled = false + //do not enable + // textView.isEventLess = true + super.init(frame: frameRect) + //layerContentsRedrawPolicy = .onSetNeedsDisplay + addSubview(textView) + + + textView.set(handler: { [weak self] control in + if let item = self?.item as? ChatServiceItem { + if let message = item.message, let action = message.media.first as? TelegramMediaAction { + switch action.action { + case let .messageAutoremoveTimeoutUpdated(timeout): + if let peer = item.chatInteraction.peer { + if peer.canManageDestructTimer, timeout > 0 { + item.chatInteraction.showDeleterSetup(control) + } + } + default: + break + } + } + } + }, for: .Click) + } + + override var backdorColor: NSColor { + if let item = item as? ChatServiceItem { + return item.isBubbled ? .clear : item.presentation.chatBackground + } else { + return .clear + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + + if let item = item as? ChatServiceItem { + textView.update(item.text) + textView.centerX(y:6) + if let imageArguments = item.imageArguments { + imageView?.setFrameSize(imageArguments.imageSize) + imageView?.centerX(y:textView.frame.maxY + (item.isBubbled ? 0 : 6)) + self.imageView?.set(arguments: imageArguments) + self.photoVideoView?.centerX(y:textView.frame.maxY + (item.isBubbled ? 0 : 6)) + } + + } + } + + + override func doubleClick(in location: NSPoint) { + if let item = self.item as? ChatRowItem, item.chatInteraction.presentation.state == .normal { + if self.hitTest(location) == nil || self.hitTest(location) == self { + item.chatInteraction.setupReplyMessage(item.message?.id) + } + } + } + + + @objc func updatePlayerIfNeeded() { + let accept = window != nil && window!.isKeyWindow && !NSIsEmptyRect(visibleRect) && !self.isDynamicContentLocked + if let photoVideoPlayer = photoVideoPlayer { + if accept { + photoVideoPlayer.play() + } else { + photoVideoPlayer.pause() + } + } + } + + override func addAccesoryOnCopiedView(innerId: AnyHashable, view: NSView) { + photoVideoPlayer?.seek(timestamp: 0) + } + + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + updateListeners() + updatePlayerIfNeeded() + } + + override func viewDidMoveToSuperview() { + super.viewDidMoveToSuperview() + updateListeners() + updatePlayerIfNeeded() + } + + override func viewDidUpdatedDynamicContent() { + super.viewDidUpdatedDynamicContent() + updatePlayerIfNeeded() + } + + func updateListeners() { + if let window = window { + NotificationCenter.default.removeObserver(self) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSWindow.didBecomeKeyNotification, object: window) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSWindow.didResignKeyNotification, object: window) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSView.boundsDidChangeNotification, object: item?.table?.clipView) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSView.boundsDidChangeNotification, object: self) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSView.frameDidChangeNotification, object: item?.table?.view) + } else { + removeNotificationListeners() + } + } + + func removeNotificationListeners() { + NotificationCenter.default.removeObserver(self) + } + + deinit { + removeNotificationListeners() + } + + + override func mouseUp(with event: NSEvent) { + if let imageView = imageView, imageView._mouseInside() { + if let item = self.item as? ChatServiceItem { + showPhotosGallery(context: item.context, peerId: item.chatInteraction.peerId, firstStableId: item.stableId, item.table, nil) + } + } else { + super.mouseUp(with: event) + } + } + + override func interactionContentView(for innerId: AnyHashable, animateIn: Bool) -> NSView { + return imageView ?? self + } + + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated:animated) + textView.disableBackgroundDrawing = true + + + var interactiveTextView: Bool = false + + if let item = item as? ChatServiceItem, let message = item.message, let action = message.media.first as? TelegramMediaAction { + switch action.action { + case let .messageAutoremoveTimeoutUpdated(timeout): + if let peer = item.chatInteraction.peer { + interactiveTextView = peer.canManageDestructTimer && timeout > 0 + } + default: + break + } + } + textView.scaleOnClick = interactiveTextView + + + if let item = item as? ChatServiceItem, let arguments = item.imageArguments { + + + if let image = item.image { + if imageView == nil { + self.imageView = TransformImageView() + self.addSubview(imageView!) + } + imageView?.setSignal(signal: cachedMedia(media: image, arguments: arguments, scale: backingScaleFactor)) + imageView?.setSignal( chatMessagePhoto(account: item.context.account, imageReference: ImageMediaReference.message(message: MessageReference(item.message!), media: image), toRepresentationSize:NSMakeSize(300, 300), scale: backingScaleFactor, autoFetchFullSize: true), cacheImage: { [weak image] result in + if let media = image { + cacheMedia(result, media: media, arguments: arguments, scale: System.backingScale, positionFlags: nil) + } + }) + + + imageView?.set(arguments: arguments) + + + if let video = image.videoRepresentations.last { + if self.photoVideoView == nil { + self.photoVideoView = MediaPlayerView() + self.photoVideoView!.layer?.cornerRadius = 10 + self.addSubview(self.photoVideoView!) + self.photoVideoView!.isEventLess = true + } + self.photoVideoView!.frame = NSMakeRect(0, 0, ChatServiceItem.photoSize.width, ChatServiceItem.photoSize.height) + + let file = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: image.representations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: video.resource.size, attributes: []) + + let mediaPlayer = MediaPlayer(postbox: item.context.account.postbox, reference: MediaResourceReference.standalone(resource: file.resource), streamable: true, video: true, preferSoftwareDecoding: false, enableSound: false, fetchAutomatically: true) + + mediaPlayer.actionAtEnd = .loop(nil) + self.photoVideoPlayer = mediaPlayer + mediaPlayer.play() + + if let seekTo = video.startTimestamp { + mediaPlayer.seek(timestamp: seekTo) + } + mediaPlayer.attachPlayerView(self.photoVideoView!) + + } else { + self.photoVideoView?.removeFromSuperview() + self.photoVideoView = nil + } + + } else { + imageView?.removeFromSuperview() + imageView = nil + } + } else { + imageView?.removeFromSuperview() + imageView = nil + } + self.needsLayout = true + updateListeners() + } + +} diff --git a/Telegram-Mac/ChatServiceRowView.swift b/Telegram-Mac/ChatServiceRowView.swift index 4ef0b1a981..6a92e2b76c 100644 --- a/Telegram-Mac/ChatServiceRowView.swift +++ b/Telegram-Mac/ChatServiceRowView.swift @@ -8,65 +8,4 @@ import Cocoa import TGUIKit -class ChatServiceRowView: TableRowView { - - private var textView:TextView - private var imageView:TransformImageView? - required init(frame frameRect: NSRect) { - textView = TextView() - textView.isSelectable = false - super.init(frame: frameRect) - addSubview(textView) - } - - override var backdorColor: NSColor { - return theme.colors.background - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func layout() { - super.layout() - - if let item = item as? ChatServiceItem { - textView.update(item.text) - textView.centerX(y:6) - if let imageArguments = item.imageArguments { - imageView?.setFrameSize(imageArguments.imageSize) - imageView?.centerX(y:textView.frame.maxY + 6) - self.imageView?.set(arguments: imageArguments) - } - - } - } - - override func doubleClick(in location: NSPoint) { - if let item = self.item as? ChatRowItem, item.chatInteraction.presentation.state == .normal { - if self.hitTest(location) == nil || self.hitTest(location) == self { - item.chatInteraction.setupReplyMessage(item.message?.id) - } - } - } - - override func set(item: TableRowItem, animated: Bool) { - super.set(item: item, animated:animated) - - if let item = item as? ChatServiceItem { - if let image = item.image { - if imageView == nil { - self.imageView = TransformImageView() - self.addSubview(imageView!) - } - imageView?.setSignal(account: item.account, signal: chatMessagePhoto(account: item.account, photo: image, toRepresentationSize:NSMakeSize(100,100), scale: backingScaleFactor)) - } else { - imageView?.removeFromSuperview() - imageView = nil - } - textView.backgroundColor = backdorColor - self.needsLayout = true - } - } - -} + diff --git a/Telegram-Mac/ChatStickerContentView.swift b/Telegram-Mac/ChatStickerContentView.swift index b01cf5646d..d8077a7ca8 100644 --- a/Telegram-Mac/ChatStickerContentView.swift +++ b/Telegram-Mac/ChatStickerContentView.swift @@ -8,62 +8,147 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac -class ChatStickerContentView: ChatMediaContentView { +import TelegramCore +import SwiftSignalKit +import Postbox +class ChatStickerContentView: ChatMediaContentView { + private let statusDisposable = MetaDisposable() private var image:TransformImageView = TransformImageView() - + private var placeholderView: StickerShimmerEffectView? required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + deinit { + statusDisposable.dispose() + } + + + override func clean() { + statusDisposable.set(nil) + } + + override var backgroundColor: NSColor { + didSet { + + } + } + + override func previewMediaIfPossible() -> Bool { + if let table = table, let context = context, let window = window as? Window { + _ = startModalPreviewHandle(table, window: window, context: context) + } + return true + } + required init(frame frameRect: NSRect) { super.init(frame:frameRect) self.addSubview(image) + } override func executeInteraction(_ isControl: Bool) { if let window = window as? Window { - if let account = account, let peerId = parent?.id.peerId, let media = media as? TelegramMediaFile, let reference = media.stickerReference { + if let context = context, let peerId = parent?.id.peerId, let media = media as? TelegramMediaFile, let reference = media.stickerReference { - showModal(with:StickersPackPreviewModalController(account, peerId: peerId, reference: reference), for:window) + showModal(with:StickerPackPreviewModalController(context, peerId: peerId, reference: reference), for:window) } } } - override func update(with media: Media, size: NSSize, account: Account, parent:Message?, table:TableView?, parameters:ChatMediaLayoutParameters? = nil, animated: Bool = false) { + override func update(with media: Media, size: NSSize, context: AccountContext, parent:Message?, table:TableView?, parameters:ChatMediaLayoutParameters? = nil, animated: Bool = false, positionFlags: LayoutPositionFlags? = nil, approximateSynchronousValue: Bool = false) { - let mediaUpdated = self.media == nil || !self.media!.isEqual(media) - - super.update(with: media, size: size, account: account, parent:parent,table:table, parameters:parameters, animated: animated) + let previous = self.parent - if let file = media as? TelegramMediaFile, mediaUpdated { - let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: NSEdgeInsets()) + super.update(with: media, size: size, context: context, parent:parent,table:table, parameters:parameters, animated: animated, positionFlags: positionFlags) + + if let file = media as? TelegramMediaFile { + + let reference = parent != nil ? FileMediaReference.message(message: MessageReference(parent!), media: file) : stickerPackFileReference(file) + + let dimensions = file.dimensions?.size.aspectFitted(size) ?? size - self.image.animatesAlphaOnFirstTransition = true + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: NSEdgeInsets()) + self.image.animatesAlphaOnFirstTransition = false - self.image.setSignal(signal: cachedMedia(media: file, size: arguments.imageSize, scale: backingScaleFactor)) - if self.image.layer?.contents == nil { - self.image.setSignal(account: account, signal: chatMessageSticker(account: account, file: file, type: .chatMessage, scale: backingScaleFactor), cacheImage: { [weak self] signal in - if let strongSelf = self { - return cacheMedia(signal: signal, media: file, size: arguments.imageSize, scale: strongSelf.backingScaleFactor) - } else { - return .complete() + self.image.setSignal(signal: cachedMedia(media: reference.media, arguments: arguments, scale: backingScaleFactor), clearInstantly: parent?.stableId != previous?.stableId) + + let hasPlaceholder = (parent == nil || file.immediateThumbnailData != nil) && self.image.image == nil + + if hasPlaceholder { + let current: StickerShimmerEffectView + if let local = self.placeholderView { + current = local + } else { + current = StickerShimmerEffectView() + current.frame = bounds + self.placeholderView = current + addSubview(current, positioned: .below, relativeTo: image) + if animated { + current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } + current.update(backgroundColor: nil, foregroundColor: NSColor(rgb: 0x748391, alpha: 0.2), shimmeringColor: NSColor(rgb: 0x748391, alpha: 0.35), data: file.immediateThumbnailData, size: size) + current.updateAbsoluteRect(bounds, within: size) + } else { + self.removePlaceholder(animated: animated) + } + + self.image.imageUpdated = { [weak self] value in + if value != nil { + self?.removePlaceholder(animated: animated) + } + } + + + if !self.image.isFullyLoaded { + self.image.setSignal( chatMessageSticker(postbox: context.account.postbox, file: reference, small: size.width < 120, scale: backingScaleFactor, fetched: true), cacheImage: { [weak file] result in + if let media = file { + return cacheMedia(result, media: media, arguments: arguments, scale: System.backingScale) } }) + self.image.set(arguments: arguments) + } else { + self.image.dispose() } - self.image.set(arguments: arguments) - self.image.setFrameSize(arguments.imageSize) - _ = fileInteractiveFetched(account: account, file: file).start() + self.image.setFrameSize(dimensions) + self.image.center() + self.fetchStatus = .Local + + let signal = context.account.postbox.mediaBox.resourceStatus(file.resource) |> deliverOnMainQueue + + statusDisposable.set(signal.start(next: { [weak self] status in + self?.fetchStatus = status + })) } } - + private func removePlaceholder(animated: Bool) { + if let placeholderView = self.placeholderView { + if animated { + placeholderView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak placeholderView] _ in + placeholderView?.removeFromSuperview() + }) + } else { + placeholderView.removeFromSuperview() + } + self.placeholderView = nil + } + } + + override func layout() { + super.layout() + self.image.center() + self.placeholderView?.frame = bounds + } + + override var contents: Any? { + return self.image.layer?.contents + } } diff --git a/Telegram-Mac/ChatStickersTouchBarPopover.swift b/Telegram-Mac/ChatStickersTouchBarPopover.swift new file mode 100644 index 0000000000..60e403abab --- /dev/null +++ b/Telegram-Mac/ChatStickersTouchBarPopover.swift @@ -0,0 +1,201 @@ +// +// ChatStickersTouchBarPopover.swift +// Telegram +// +// Created by Mikhail Filimonov on 14/09/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + +@available(OSX 10.12.2, *) +fileprivate extension NSTouchBarItem.Identifier { + static let sticker = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.chat.sticker") +} + +@available(OSX 10.12.2, *) +private extension NSTouchBar.CustomizationIdentifier { + static let stickersScrubber = NSTouchBar.CustomizationIdentifier("\(Bundle.main.bundleIdentifier!).touchBar.chat.StickersScrubber") +} + +enum TouchBarStickerEntry { + case header(TextViewLayout) + case sticker(TelegramMediaFile) +} + + + +@available(OSX 10.12.2, *) +class StickersScrubberBarItem: NSCustomTouchBarItem, NSScrubberDelegate, NSScrubberDataSource, NSScrubberFlowLayoutDelegate { + + private static let stickerItemViewIdentifier = "StickersItemViewIdentifier" + private static let headerItemViewIdentifier = "HeaderItemViewIdentifier" + + private let entries: [TouchBarStickerEntry] + private let context: AccountContext + private let sendSticker: (TelegramMediaFile)->Void + private let animated: Bool + init(identifier: NSTouchBarItem.Identifier, context: AccountContext, animated: Bool, sendSticker:@escaping(TelegramMediaFile)->Void, entries: [TouchBarStickerEntry]) { + self.entries = entries + self.context = context + self.sendSticker = sendSticker + self.animated = animated + super.init(identifier: identifier) + + let scrubber = TGScrubber() + scrubber.register(TouchBarStickerItemView.self, forItemIdentifier: NSUserInterfaceItemIdentifier(rawValue: StickersScrubberBarItem.stickerItemViewIdentifier)) + scrubber.register(TouchBarScrubberHeaderItemView.self, forItemIdentifier: NSUserInterfaceItemIdentifier(rawValue: StickersScrubberBarItem.headerItemViewIdentifier)) + + scrubber.mode = .free + scrubber.selectionBackgroundStyle = .roundedBackground + scrubber.floatsSelectionViews = true + scrubber.delegate = self + scrubber.dataSource = self + + let gesture = NSPressGestureRecognizer(target: self, action: #selector(self.pressGesture(_:))) + gesture.allowedTouchTypes = NSTouch.TouchTypeMask.direct + gesture.minimumPressDuration = 0.3 + gesture.allowableMovement = 0 + scrubber.addGestureRecognizer(gesture) + + + self.view = scrubber + } + + fileprivate var modalPreview: PreviewModalController? + + @objc private func pressGesture(_ gesture: NSPressGestureRecognizer) { + + let runSelector:()->Void = { [weak self] in + guard let `self` = self else { + return + } + let scrollView = HackUtils.findElements(byClass: "NSScrollView", in: self.view)?.first as? NSScrollView + + guard let container = scrollView?.documentView?.subviews.first else { + return + } + var point = gesture.location(in: container) + point.y = 0 + for itemView in container.subviews { + if NSPointInRect(point, itemView.frame) { + if let itemView = itemView as? TouchBarStickerItemView { + self.modalPreview?.update(with: itemView.quickPreview) + } + } + } + } + + switch gesture.state { + case .began: + modalPreview = PreviewModalController(context) + showModal(with: modalPreview!, for: context.window) + runSelector() + case .failed, .cancelled, .ended: + modalPreview?.close() + modalPreview = nil + case .changed: + runSelector() + case .possible: + break + @unknown default: + break + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + + func numberOfItems(for scrubber: NSScrubber) -> Int { + return entries.count + } + + func scrubber(_ scrubber: NSScrubber, didHighlightItemAt highlightedIndex: Int) { + switch entries[highlightedIndex] { + case .header: + scrubber.selectionBackgroundStyle = nil + scrubber.selectedIndex = -1 + default: + scrubber.selectionBackgroundStyle = .roundedBackground + } + } + + func scrubber(_ scrubber: NSScrubber, viewForItemAt index: Int) -> NSScrubberItemView { + let itemView: NSScrubberItemView + switch entries[index] { + case let .header(title): + let view = scrubber.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: StickersScrubberBarItem.headerItemViewIdentifier), owner: nil) as! TouchBarScrubberHeaderItemView + view.update(title) + itemView = view + case let .sticker(file): + let view = scrubber.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: StickersScrubberBarItem.stickerItemViewIdentifier), owner: nil) as! TouchBarStickerItemView + view.update(context: context, file: file, animated: self.animated) + itemView = view + } + + return itemView + } + + func scrubber(_ scrubber: NSScrubber, layout: NSScrubberFlowLayout, sizeForItemAt itemIndex: Int) -> NSSize { + switch entries[itemIndex] { + case let .header(layout): + return NSMakeSize(layout.layoutSize.width + 20, 30) + case .sticker: + return NSSize(width: 40, height: 30) + } + } + + + func scrubber(_ scrubber: NSScrubber, didSelectItemAt index: Int) { + switch entries[index] { + case let .sticker(file): + sendSticker(file) + default: + break + } + } +} + + +@available(OSX 10.12.2, *) +final class ChatStickersTouchBarPopover : NSTouchBar, NSTouchBarDelegate { + private let chatInteraction: ChatInteraction + private let entries: [TouchBarStickerEntry] + private let dismiss:(TelegramMediaFile?) -> Void + init(chatInteraction: ChatInteraction, dismiss:@escaping(TelegramMediaFile?)->Void, entries: [TouchBarStickerEntry]) { + self.dismiss = dismiss + + self.entries = entries + self.chatInteraction = chatInteraction + super.init() + delegate = self + customizationIdentifier = .stickersScrubber + defaultItemIdentifiers = [.sticker] + customizationAllowedItemIdentifiers = [.sticker] + } + + func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? { + switch identifier { + case .sticker: + let scrubberItem: NSCustomTouchBarItem = StickersScrubberBarItem(identifier: identifier, context: chatInteraction.context, animated: true, sendSticker: { [weak self] file in + self?.dismiss(file) + }, entries: self.entries) + return scrubberItem + default: + return nil + } + } + + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/ChatStorageManagmentModalController.swift b/Telegram-Mac/ChatStorageManagmentModalController.swift index 146fad6e63..69d5b2d9db 100644 --- a/Telegram-Mac/ChatStorageManagmentModalController.swift +++ b/Telegram-Mac/ChatStorageManagmentModalController.swift @@ -8,119 +8,11 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac +import Postbox +import TelegramCore +import SwiftSignalKit -/*let controller = ActionSheetController() - let dismissAction: () -> Void = { [weak controller] in - controller?.dismissAnimated() - } - - var sizeIndex: [PeerCacheUsageCategory: (Bool, Int64)] = [:] - - var itemIndex = 0 - - let updateTotalSize: () -> Void = { [weak controller] in - controller?.updateItem(groupIndex: 0, itemIndex: itemIndex, { item in - let title: String - let filteredSize = sizeIndex.values.reduce(0, { $0 + ($1.0 ? $1.1 : 0) }) - - if filteredSize == 0 { - title = "Clear" - } else { - title = "Clear (\(dataSizeString(Int(filteredSize))))" - } - - if let item = item as? ActionSheetButtonItem { - return ActionSheetButtonItem(title: title, color: filteredSize != 0 ? .accent : .disabled, enabled: filteredSize != 0, action: item.action) - } - return item - }) - } - - let toggleCheck: (PeerCacheUsageCategory, Int) -> Void = { [weak controller] category, itemIndex in - if let (value, size) = sizeIndex[category] { - sizeIndex[category] = (!value, size) - } - controller?.updateItem(groupIndex: 0, itemIndex: itemIndex, { item in - if let item = item as? ActionSheetCheckboxItem { - return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action) - } - return item - }) - updateTotalSize() - } - var items: [ActionSheetItem] = [] - - let validCategories: [PeerCacheUsageCategory] = [.image, .video, .audio, .file] - - var totalSize: Int64 = 0 - - for categoryId in validCategories { - if let media = categories[categoryId] { - var categorySize: Int64 = 0 - for (_, size) in media { - categorySize += size - } - sizeIndex[categoryId] = (true, categorySize) - totalSize += categorySize - let index = itemIndex - items.append(ActionSheetCheckboxItem(title: stringForCategory(categoryId), label: dataSizeString(Int(categorySize)), value: true, action: { value in - toggleCheck(categoryId, index) - })) - itemIndex += 1 - } - } - - if !items.isEmpty { - items.append(ActionSheetButtonItem(title: "Clear (\(dataSizeString(Int(totalSize))))", action: { - if let statsPromise = statsPromise { - var clearCategories = sizeIndex.keys.filter({ sizeIndex[$0]!.0 }) - //var clearSize: Int64 = 0 - - var clearMediaIds = Set() - - var media = stats.media - if var categories = media[peerId] { - for category in clearCategories { - if let contents = categories[category] { - for (mediaId, size) in contents { - clearMediaIds.insert(mediaId) - //clearSize += size - } - } - categories.removeValue(forKey: category) - } - - media[peerId] = categories - } - - var clearResourceIds = Set() - for id in clearMediaIds { - if let ids = stats.mediaResourceIds[id] { - for resourceId in ids { - clearResourceIds.insert(WrappedMediaResourceId(resourceId)) - } - } - } - - statsPromise.set(.single(.result(CacheUsageStats(media: media, mediaResourceIds: stats.mediaResourceIds, peers: stats.peers)))) - - clearDisposable.set(clearCachedMediaResources(account: account, mediaResourceIds: clearResourceIds).start()) - } - - dismissAction() - })) - - controller.setItemGroups([ - ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: "Cancel", action: { dismissAction() })]) - ]) - presentControllerImpl?(controller) - } */ - class ChatStorageManagmentModalController: ModalViewController { @@ -131,30 +23,50 @@ class ChatStorageManagmentModalController: ModalViewController { self.categories = categories self.clear = clear super.init(frame: NSMakeRect(0, 0, 300, CGFloat(categories.count) * 40 + 40 + 50)) + bar = .init(height: 0) + } + + override var dynamicSize: Bool { + return true + } + + override func measure(size: NSSize) { + self.modal?.resize(with:NSMakeSize(genericView.frame.width, min(size.height - 70, genericView.listHeight)), animated: false) } override func viewDidLoad() { super.viewDidLoad() + genericView.getBackgroundColor = { + theme.colors.listBackground + } + reloadData() + readyOnce() + } + + private func reloadData() { let initialSize = atomicSize.modify({$0}) - _ = genericView.addItem(item: GeneralRowItem(initialSize, height: 20, stableId: arc4random())) + genericView.removeAll() + _ = genericView.addItem(item: GeneralRowItem(initialSize, height: 30, stableId: arc4random(), viewType: .separator)) - let validCategories: [PeerCacheUsageCategory] = [.image, .video, .audio, .file] + + let validCategories: [PeerCacheUsageCategory] = [.image, .video, .audio, .file].filter { + categories[$0] != nil + } var totalSize: Int64 = 0 var itemIndex = 0 - - for categoryId in validCategories { + for (i, categoryId) in validCategories.enumerated() { if let media = categories[categoryId] { var categorySize: Int64 = 0 for (_, size) in media { categorySize += size } - sizeIndex[categoryId] = (true, categorySize) + sizeIndex[categoryId] = (sizeIndex[categoryId]?.0 ?? true, categorySize) totalSize += categorySize let index = itemIndex @@ -167,21 +79,18 @@ class ChatStorageManagmentModalController: ModalViewController { let filteredSize = strongSelf.sizeIndex.values.reduce(0, { $0 + ($1.0 ? $1.1 : 0) }) if filteredSize == 0 { - title = "Clear" + title = L10n.storageUsageClear } else { - title = "Clear (\(dataSizeString(Int(filteredSize))))" + title = "\(L10n.storageUsageClear) (\(dataSizeString(Int(filteredSize), formatting: DataSizeStringFormatting.current)))" } strongSelf.modal?.interactions?.updateDone( { button in button.set(text: title, for: .Normal) }) - strongSelf.genericView.reloadData() + strongSelf.reloadData() } } - - _ = genericView.addItem(item: GeneralInteractedRowItem(initialSize, stableId: index, name: stringForCategory(categoryId) + " (\(dataSizeString(Int(categorySize))))" , type: .selectable(stateback: { [weak self] () -> Bool in - return self?.sizeIndex[categoryId]?.0 ?? false - }), action: { + _ = genericView.addItem(item: GeneralInteractedRowItem(initialSize, stableId: index, name: stringForCategory(categoryId) + " (\(dataSizeString(Int(categorySize), formatting: DataSizeStringFormatting.current)))" , type: .selectable(sizeIndex[categoryId]?.0 ?? false), viewType: bestGeneralViewType(validCategories, for: i), action: { toggleCheck(categoryId, index) })) @@ -190,25 +99,27 @@ class ChatStorageManagmentModalController: ModalViewController { } - _ = genericView.addItem(item: GeneralRowItem(initialSize, height: 20, stableId: arc4random())) - - readyOnce() + _ = genericView.addItem(item: GeneralRowItem(initialSize, height: 30, stableId: arc4random(), viewType: .separator)) } private func stringForCategory(_ category: PeerCacheUsageCategory) -> String { switch category { case .image: - return tr(.storageClearPhotos) + return L10n.storageClearPhotos case .video: - return tr(.storageClearVideos) + return L10n.storageClearVideos case .audio: - return tr(.storageClearAudio) + return L10n.storageClearAudio case .file: - return tr(.storageClearDocuments) + return L10n.storageClearDocuments } } + override var modalHeader: (left: ModalHeaderData?, center: ModalHeaderData?, right: ModalHeaderData?)? { + return (left: nil, center: ModalHeaderData.init(title: L10n.telegramStorageUsageController), right: nil) + } + override var modalInteractions: ModalInteractions? { var totalSize: Int64 = 0 @@ -222,13 +133,13 @@ class ChatStorageManagmentModalController: ModalViewController { } - return ModalInteractions(acceptTitle: tr(.storageClear(dataSizeString(Int(totalSize)))), accept: { [weak self] in + + return ModalInteractions(acceptTitle: L10n.storageClear(dataSizeString(Int(totalSize), formatting: DataSizeStringFormatting.current)), accept: { [weak self] in if let strongSelf = self { self?.clear(strongSelf.sizeIndex) } - self?.close() - }, cancelTitle: tr(.modalCancel), drawBorder: true, height: 40) + }, cancelTitle: L10n.modalCancel, drawBorder: true, height: 50) } private var genericView:TableView { diff --git a/Telegram-Mac/ChatSwitchInlineController.swift b/Telegram-Mac/ChatSwitchInlineController.swift index c0385e08aa..44b415fad9 100644 --- a/Telegram-Mac/ChatSwitchInlineController.swift +++ b/Telegram-Mac/ChatSwitchInlineController.swift @@ -8,17 +8,20 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac +import Postbox +import TelegramCore + +import SwiftSignalKit class ChatSwitchInlineController: ChatController { private let fallbackId:PeerId - init(account:Account, peerId:PeerId, fallbackId:PeerId, initialAction:ChatInitialAction? = nil) { + private let fallbackMode: ChatMode + init(context:AccountContext, peerId:PeerId, fallbackId:PeerId, fallbackMode: ChatMode, initialAction:ChatInitialAction? = nil) { self.fallbackId = fallbackId - super.init(account: account, peerId: peerId, initialAction: initialAction) + self.fallbackMode = fallbackMode + super.init(context: context, chatLocation: .peer(peerId), initialAction: initialAction) } override var removeAfterDisapper: Bool { @@ -26,11 +29,11 @@ class ChatSwitchInlineController: ChatController { } override open func backSettings() -> (String,CGImage?) { - return (tr(.navigationCancel),nil) + return (L10n.navigationCancel,nil) } - override func applyTransition(_ transition: TableUpdateTransition, initialData: ChatHistoryCombinedInitialData) { - super.applyTransition(transition, initialData: initialData) + override func applyTransition(_ transition:TableUpdateTransition, initialData:ChatHistoryCombinedInitialData, isLoading: Bool, processedView: ChatHistoryView) { + super.applyTransition(transition, initialData: initialData, isLoading: isLoading, processedView: processedView) if case let .none(interface) = transition.state, let _ = interface { for (_, item) in transition.inserted { @@ -41,7 +44,16 @@ class ChatSwitchInlineController: ChatController { for button in row.buttons { if case let .switchInline(samePeer: _, query: query) = button.action { let text = "@\(message.inlinePeer?.username ?? "") \(query)" - self.navigationController?.push(ChatController(account: account, peerId: fallbackId, initialAction: .inputText(text: text, behavior: .automatic))) + let controller: ChatController + switch self.fallbackMode { + case .history, .pinned, .preview: + controller = ChatController(context: context, chatLocation: .peer(fallbackId), initialAction: .inputText(text: text, behavior: .automatic)) + case let .replyThread(data, mode): + controller = ChatController.init(context: context, chatLocation: .replyThread(data), mode: .replyThread(data: data, mode: mode), messageId: nil, initialAction: .inputText(text: text, behavior: .automatic), chatLocationContextHolder: Atomic(value: nil)) + case .scheduled: + controller = ChatScheduleController(context: context, chatLocation: .peer(fallbackId), initialAction: .inputText(text: text, behavior: .automatic)) + } + self.navigationController?.push(controller) } } } diff --git a/Telegram-Mac/ChatThemeRowItem.swift b/Telegram-Mac/ChatThemeRowItem.swift new file mode 100644 index 0000000000..71996d241a --- /dev/null +++ b/Telegram-Mac/ChatThemeRowItem.swift @@ -0,0 +1,163 @@ +// +// ChatThemeRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 17.09.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit +import TelegramCore +import Postbox + + +final class ChatThemeRowItem : GeneralRowItem { + fileprivate let theme: (String, CGImage, TelegramPresentationTheme)? + fileprivate let context: AccountContext + fileprivate let selected: Bool + fileprivate let select:((String, TelegramPresentationTheme)?)->Void + fileprivate let emojies: [String: StickerPackItem] + init(_ initialSize: NSSize, context: AccountContext, stableId: AnyHashable, emojies: [String: StickerPackItem], theme: (String, CGImage, TelegramPresentationTheme)?, selected: Bool, select:@escaping((String, TelegramPresentationTheme)?)->Void) { + self.theme = theme + self.select = select + self.selected = selected + self.context = context + self.emojies = emojies + super.init(initialSize, stableId: stableId) + } + + override var width: CGFloat { + return 90 + } + override var height: CGFloat { + return 80 + } + + override func viewClass() -> AnyClass { + return ChatThemeRowView.self + } +} + +private final class ChatThemeRowView: HorizontalRowView { + private let imageView: ImageView = ImageView() + private let emojiView = MediaAnimatedStickerView(frame: NSMakeRect(0, 0, 25, 25)) + private let textView = TextView() + private let selectionView: View = View() + + private let overlay = OverlayControl() + + private var noThemeTextView: TextView? + + private var currentEmoji: String? = nil + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(imageView) + addSubview(emojiView) + addSubview(textView) + addSubview(selectionView) + addSubview(overlay) + imageView.isEventLess = true + emojiView.userInteractionEnabled = false + selectionView.isEventLess = true + + textView.isSelectable = false + textView.userInteractionEnabled = false + + + selectionView.layer?.cornerRadius = 12 + selectionView.layer?.borderWidth = 2.5 + + + overlay.set(handler: { [weak self] _ in + guard let item = self?.item as? ChatThemeRowItem else { + return + } + if let theme = item.theme { + item.select((theme.0, theme.2)) + } else { + item.select(nil) + } + }, for: .Click) + + overlay.set(handler: { [weak self] _ in + self?.emojiView.overridePlayValue = true + }, for: .Hover) + + overlay.set(handler: { [weak self] _ in + self?.emojiView.overridePlayValue = false + }, for: .Normal) + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + + } + + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + + guard let item = item as? ChatThemeRowItem else { + return + } + selectionView.layer?.borderColor = item.selected ? theme.colors.accentSelect.cgColor : theme.colors.border.cgColor + + let dBorder: CGFloat = item.theme?.2.bubbled == true ? 0 : 1 + + selectionView.layer?.borderWidth = item.selected ? 2 : (item.theme == nil ? 1 : dBorder) + + if let current = item.theme { + if self.currentEmoji != current.0 { + self.currentEmoji = current.0 + + let context = item.context + + if let first = item.emojies[current.0.fixed] { + let params = ChatAnimatedStickerMediaLayoutParameters(playPolicy: nil, media: first.file) + self.emojiView.update(with: first.file, size: NSMakeSize(25, 25), context: context, table: nil, parameters: params, animated: animated) + } + + self.emojiView.overridePlayValue = false + self.imageView.image = current.1 + self.imageView.sizeToFit() + self.imageView.isHidden = false + self.noThemeTextView?.removeFromSuperview() + self.noThemeTextView = nil + + } + } else { + let layout = TextViewLayout(.initialize(string: "❌", color: theme.colors.text, font: .normal(15))) + layout.measure(width: .greatestFiniteMagnitude) + self.textView.update(layout) + self.imageView.isHidden = true + self.noThemeTextView?.removeFromSuperview() + self.noThemeTextView = TextView() + self.noThemeTextView?.userInteractionEnabled = false + self.noThemeTextView?.isSelectable = false + self.addSubview(self.noThemeTextView!) + let noTheme = TextViewLayout(.initialize(string: L10n.chatChatThemeNoTheme, color: theme.colors.text, font: .medium(.text)), alignment: .center) + noTheme.measure(width: 80) + self.noThemeTextView?.update(noTheme) + } + + needsLayout = true + } + + override func layout() { + super.layout() + self.imageView.setFrameSize(NSMakeSize(70, 80)) + self.imageView.centerX(y: 5) + self.selectionView.frame = self.imageView.frame.insetBy(dx: -3, dy: -3) + self.textView.centerX(y: 60) + self.emojiView.centerX(y: 55) + self.noThemeTextView?.centerX(y: 15) + self.overlay.frame = bounds + } +} diff --git a/Telegram-Mac/ChatThemeSelectorController.swift b/Telegram-Mac/ChatThemeSelectorController.swift new file mode 100644 index 0000000000..8a30d6a028 --- /dev/null +++ b/Telegram-Mac/ChatThemeSelectorController.swift @@ -0,0 +1,202 @@ +// +// ChatThemeSelectorController.swift +// Telegram +// +// Created by Mikhail Filimonov on 17.09.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit +import TelegramCore +import Postbox + + +final class ChatThemeSelectorView : View { + + private let tableView: HorizontalTableView = HorizontalTableView(frame: .zero) + + private let controls:View = View() + + fileprivate let accept = TitleButton() + fileprivate let cancel = TitleButton() + + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(tableView) + addSubview(controls) + self.border = [.Top] + + controls.addSubview(cancel) + controls.addSubview(accept) + + cancel.autohighlight = false + cancel.scaleOnClick = true + + accept.autohighlight = false + accept.scaleOnClick = true + + accept.layer?.cornerRadius = 4 + cancel.layer?.cornerRadius = 4 + + + cancel.layer?.borderWidth = 1 + } + + fileprivate func updateThemes(_ themes: [(String, CGImage, TelegramPresentationTheme)], emojies: [String: StickerPackItem], context: AccountContext, chatTheme: (String?, TelegramPresentationTheme)?, previewCurrent: @escaping((String, TelegramPresentationTheme)?) -> Void) { + tableView.beginTableUpdates() + tableView.removeAll() + + _ = tableView.addItem(item: GeneralRowItem(.zero, height: 10)) + _ = tableView.addItem(item: ChatThemeRowItem(frame.size, context: context, stableId: arc4random(), emojies: emojies, theme: nil, selected: chatTheme?.0 == nil, select: { _ in + previewCurrent(nil) + })) + + for theme in themes { + _ = tableView.addItem(item: ChatThemeRowItem(frame.size, context: context, stableId: theme.0, emojies: emojies, theme: theme, selected: chatTheme?.0 == theme.0, select: { theme in + previewCurrent(theme) + })) + } + _ = tableView.addItem(item: GeneralRowItem(.zero, height: 10)) + + tableView.endTableUpdates() + + accept.set(color: theme.colors.underSelectedColor, for: .Normal) + accept.set(background: theme.colors.accent, for: .Normal) + accept.set(font: .medium(.text), for: .Normal) + accept.set(text: L10n.chatChatThemeApplyTheme, for: .Normal) + + cancel.set(color: theme.colors.text, for: .Normal) + cancel.set(font: .medium(.text), for: .Normal) + cancel.set(background: theme.colors.background, for: .Normal) + cancel.set(text: L10n.chatChatThemeCancel, for: .Normal) + cancel.layer?.borderColor = theme.colors.border.cgColor + + accept.sizeToFit(NSMakeSize(20, 15), .zero, thatFit: false) + cancel.sizeToFit(.zero, NSMakeSize(accept.frame.width, accept.frame.size.height), thatFit: true) + + needsLayout = true + } + + override func layout() { + super.layout() + + tableView.frame = NSMakeRect(0, 10, frame.width, 90) + controls.setFrameSize(NSMakeSize(accept.frame.width + 10 + cancel.frame.width, 60)) + + cancel.centerY(x: 0) + accept.centerY(x: cancel.frame.maxX + 10) + + + controls.centerX(y: tableView.frame.maxY - 5) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +final class ChatThemeSelectorController : TelegramGenericViewController { + private let chatInteraction: ChatInteraction + private let readyDisposable = MetaDisposable() + private let disposable = MetaDisposable() + + private let chatTheme: Signal<(String?, TelegramPresentationTheme), NoError> + private var currentSelected: (String?, TelegramPresentationTheme)? { + didSet { + currentSelectedValue.set(.single(currentSelected)) + } + } + + private let currentSelectedValue: Promise<(String?, TelegramPresentationTheme)?> = Promise(nil) + + var onReady:(ChatThemeSelectorController)->Void = { _ in } + var close: (Bool)->Void = { _ in } + + var previewCurrent: (TelegramPresentationTheme?) -> Void = { _ in } + + init(_ context: AccountContext, chatTheme: Signal<(String?, TelegramPresentationTheme), NoError>, chatInteraction: ChatInteraction) { + self.chatTheme = chatTheme + self.chatInteraction = chatInteraction + super.init(context) + _frameRect = NSMakeRect(0, 0, 0, 160) + self.bar = .init(height: 0) + } + + deinit { + + } + + override func viewDidLoad() { + super.viewDidLoad() + + let context = self.context + let peerId = chatInteraction.peerId + + let readySignal = self.ready.get() |> take(1) |> deliverOnMainQueue + + let themesAndThumbs: Signal<[(String, CGImage, TelegramPresentationTheme)], NoError> = context.chatThemes |> mapToSignal { themes in + var signals:[Signal<(String, CGImage, TelegramPresentationTheme), NoError>] = [] + + for theme in themes { + signals.append(generateChatThemeThumb(palette: theme.1.colors, bubbled: theme.1.bubbled, backgroundMode: theme.1.controllerBackgroundMode) |> map { + (theme.0, $0, theme.1) + }) + } + return combineLatest(signals) + } |> deliverOnMainQueue + + + currentSelectedValue.set(chatTheme |> take(1) |> map { $0 }) + + let animatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false) + |> map { result -> [String: StickerPackItem] in + switch result { + case let .result(_, items, _): + var animatedEmojiStickers: [String: StickerPackItem] = [:] + for case let item as StickerPackItem in items { + if let emoji = item.getStringRepresentationsOfIndexKeys().first { + animatedEmojiStickers[emoji] = item + } + } + return animatedEmojiStickers + default: + return [:] + } + } |> deliverOnMainQueue + + disposable.set(combineLatest(queue: .mainQueue(), themesAndThumbs, chatTheme, currentSelectedValue.get(), animatedEmojiStickers).start(next: { [weak self] themes, chatTheme, currentSelected, emojies in + + let selected: (String?, TelegramPresentationTheme)? = currentSelected + + self?.genericView.updateThemes(themes, emojies: emojies, context: context, chatTheme: selected, previewCurrent: { preview in + self?.previewCurrent(preview?.1 ?? theme) + self?.currentSelected = preview + }) + self?.readyOnce() + })) + + readyDisposable.set(readySignal.start(next: { [weak self] _ in + guard let controller = self else { + return + } + self?.onReady(controller) + })) + + genericView.cancel.set(handler: { [weak self] _ in + self?.close(true) + }, for: .Click) + + genericView.accept.set(handler: { [weak self] _ in + let updateSignal = context.engine.themes.setChatTheme(peerId: peerId, emoticon: self?.currentSelected?.0) + |> deliverOnMainQueue + _ = updateSignal.start(next: { [weak self] in + self?.close(true) + }) + self?.close(false) + }, for: .SingleClick) + } + +} diff --git a/Telegram-Mac/ChatTitleBarView.swift b/Telegram-Mac/ChatTitleBarView.swift index a65c51a550..db44b3939e 100644 --- a/Telegram-Mac/ChatTitleBarView.swift +++ b/Telegram-Mac/ChatTitleBarView.swift @@ -8,77 +8,135 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac +import Postbox +import TelegramCore + +import SwiftSignalKit +import AVFoundation + + +private final class SelectMessagesPlaceholderView: View { + private let textView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(textView) + updateLocalizationAndTheme(theme: theme) + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + background = theme.colors.background + let layout = TextViewLayout(.initialize(string: L10n.chatTitleReportMessages, color: theme.colors.text, font: .medium(.header))) + layout.measure(width: .greatestFiniteMagnitude) + textView.update(layout) + } + + override func layout() { + super.layout() + textView.centerY(x: 0) + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } +} private class ConnectionStatusView : View { private var textViewLayout:TextViewLayout? private var disableProxyButton: TitleButton? + private(set) var backButton: ImageButton? + + var isSingleLayout: Bool = false { + didSet { + updateBackButton() + } + } + var disableProxy:(()->Void)? - var status:ConnectionStatus = .online { + var status:ConnectionStatus = .online(proxyAddress: nil) { didSet { let attr:NSAttributedString - if case .connecting(true) = status { - disableProxyButton = TitleButton() - disableProxyButton?.set(color: theme.colors.grayText, for: .Normal) - disableProxyButton?.set(font: .medium(.text), for: .Normal) - disableProxyButton?.set(text: tr(.connectingStatusDisableProxy), for: .Normal) - disableProxyButton?.sizeToFit() - addSubview(disableProxyButton!) - - disableProxyButton?.set(handler: { [weak self] _ in - self?.disableProxy?() - }, for: .Click) + if case let .connecting(proxy, _) = status { + if let _ = proxy { + if disableProxyButton == nil { + disableProxyButton = TitleButton() + } + disableProxyButton?.set(color: theme.colors.grayText, for: .Normal) + disableProxyButton?.set(font: .medium(.text), for: .Normal) + disableProxyButton?.set(text: tr(L10n.connectingStatusDisableProxy), for: .Normal) + _ = disableProxyButton?.sizeToFit() + addSubview(disableProxyButton!) + + disableProxyButton?.set(handler: { [weak self] _ in + self?.disableProxy?() + }, for: .Click) + } else { + disableProxyButton?.removeFromSuperview() + disableProxyButton = nil + } } else { disableProxyButton?.removeFromSuperview() disableProxyButton = nil } switch status { - case .connecting(let toProxy): - attr = .initialize(string: toProxy ? tr(.chatConnectingStatusConnectingToProxy) : tr(.chatConnectingStatusConnecting), color: theme.colors.text, font: .medium(.header)) + case let .connecting(proxy, _): + attr = .initialize(string: proxy != nil ? L10n.chatConnectingStatusConnectingToProxy : L10n.chatConnectingStatusConnecting, color: theme.colors.text, font: .medium(.header)) case .updating: - attr = .initialize(string: tr(.chatConnectingStatusUpdating), color: theme.colors.text, font: .medium(.header)) + attr = .initialize(string: L10n.chatConnectingStatusUpdating, color: theme.colors.text, font: .medium(.header)) case .waitingForNetwork: - attr = .initialize(string: tr(.chatConnectingStatusWaitingNetwork), color: theme.colors.text, font: .medium(.header)) + attr = .initialize(string: L10n.chatConnectingStatusWaitingNetwork, color: theme.colors.text, font: .medium(.header)) case .online: attr = NSAttributedString() } textViewLayout = TextViewLayout(attr, maximumNumberOfLines: 1) needsLayout = true - // indicator.animates = true } } private let textView:TextView = TextView() private let indicator:ProgressIndicator = ProgressIndicator() required init(frame frameRect: NSRect) { super.init(frame: frameRect) - // indicator.setFrameSize(18,18) -// indicator.numberOfLines = 8 -// indicator.innerMargin = 3 -// indicator.widthOfLine = 3 -// indicator.lengthOfLine = 6 textView.userInteractionEnabled = false textView.isSelectable = false addSubview(textView) addSubview(indicator) - updateLocalizationAndTheme() + updateLocalizationAndTheme(theme: theme) } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) backgroundColor = theme.colors.background textView.backgroundColor = theme.colors.background disableProxyButton?.set(background: theme.colors.background, for: .Normal) - // indicator.color = theme.colors.indicatorColor + indicator.progressColor = theme.colors.text let status = self.status self.status = status } + + private func updateBackButton() { + if isSingleLayout { + let button: ImageButton + if let b = self.backButton { + button = b + } else { + button = ImageButton() + self.backButton = button + addSubview(button) + } + button.autohighlight = false + button.set(image: theme.icons.chatNavigationBack, for: .Normal) + _ = button.sizeToFit() + } else { + backButton?.removeFromSuperview() + backButton = nil + } + needsLayout = true + } deinit { //indicator.animates = false @@ -92,20 +150,22 @@ private class ConnectionStatusView : View { super.layout() if let textViewLayout = textViewLayout { + + let offset: CGFloat = backButton != nil ? 16 : 0 + textViewLayout.measure(width: frame.width) let f = focus(textViewLayout.layoutSize, inset:NSEdgeInsets(left: 12, top: 3)) - indicator.centerY(x:0) - + indicator.centerY(x: offset) textView.update(textViewLayout) if let disableProxyButton = disableProxyButton { - disableProxyButton.setFrameOrigin(indicator.frame.maxX + 4, floorToScreenPixels(frame.height / 2) + 2) - textView.setFrameOrigin(indicator.frame.maxX + 8, floorToScreenPixels(frame.height / 2) - textView.frame.height + 2) + disableProxyButton.setFrameOrigin(indicator.frame.maxX + 3, floorToScreenPixels(backingScaleFactor, frame.height / 2) + 2) + textView.setFrameOrigin(indicator.frame.maxX + 8, floorToScreenPixels(backingScaleFactor, frame.height / 2) - textView.frame.height + 2) } else { textView.setFrameOrigin(NSMakePoint(indicator.frame.maxX + 4, f.origin.y)) } - + backButton?.centerY(x: 0) } } @@ -113,9 +173,148 @@ private class ConnectionStatusView : View { } -class ChatTitleBarView: TitledBarView { +private final class VideoAvatarProgressView: View { + private let progressView = ProgressIndicator(frame: NSMakeRect(0, 0, 20, 20)) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(self.progressView) + backgroundColor = .blackTransparent + progressView.progressColor = .white + layer?.cornerRadius = frameRect.width / 2 + } + + override func layout() { + progressView.center() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + +private final class VideoAvatarContainer : View { + let circle: View = View() - private var isSingleLayout:Bool = false + private var mediaPlayer: MediaPlayer? + private var view: MediaPlayerView? + + private let fetchDisposable = MetaDisposable() + private let statusDisposable = MetaDisposable() + + private var progressView: VideoAvatarProgressView? + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(circle) + circle.frame = bounds + + circle.layer?.cornerRadius = bounds.width / 2 + circle.layer?.borderWidth = 1 + circle.layer?.borderColor = theme.colors.accent.cgColor + + + isEventLess = true + + } + + func animateIn() { + // circle.layer?.animateScaleCenter(from: 0.2, to: 1.0, duration: 0.2) + } + func animateOut() { + // circle.layer?.animateScaleCenter(from: 1.0, to: 0.2, duration: 0.2) + } + + func updateWith(file: TelegramMediaFile, seekTo: TimeInterval?, reference: PeerReference?, context: AccountContext) { + // player.update(FileMediaReference.standalone(media: file), context: context) + if let reference = reference { + fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: MediaResourceReference.avatar(peer: reference, resource: file.resource)).start()) + } else { + fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: MediaResourceReference.standalone(resource: file.resource)).start()) + } + + let mediaReference: MediaResourceReference + if let reference = reference { + mediaReference = MediaResourceReference.avatar(peer: reference, resource: file.resource) + } else { + mediaReference = MediaResourceReference.standalone(resource: file.resource) + } + + let mediaPlayer = MediaPlayer(postbox: context.account.postbox, reference: mediaReference, streamable: true, video: true, preferSoftwareDecoding: false, enableSound: false, fetchAutomatically: false) + + + let view = MediaPlayerView() + + view.setVideoLayerGravity(.resizeAspectFill) + + mediaPlayer.attachPlayerView(view) + + mediaPlayer.actionAtEnd = .loop(nil) + + view.frame = NSMakeRect(2, 2, frame.width - 4, frame.height - 4) + view.layer?.cornerRadius = bounds.width / 2 + + addSubview(view) + + self.mediaPlayer = mediaPlayer + self.view = view + + mediaPlayer.play() + if let seekTo = seekTo { + mediaPlayer.seek(timestamp: seekTo) + } + + let statusSignal = context.account.postbox.mediaBox.resourceStatus(file.resource) |> deliverOnMainQueue + + statusDisposable.set(statusSignal.start(next: { [weak self] status in + switch status { + case .Local: + if let progressView = self?.progressView { + self?.progressView = nil + progressView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak progressView] _ in + progressView?.removeFromSuperview() + }) + } + default: + if self?.progressView == nil, let frame = self?.frame { + let view = VideoAvatarProgressView(frame: NSMakeRect(2, 2, frame.width - 4, frame.height - 4)) + self?.progressView = view + self?.addSubview(view) + view.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } + })) + + } + + deinit { + fetchDisposable.dispose() + statusDisposable.dispose() + } + + override func layout() { + super.layout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + +class ChatTitleBarView: TitledBarView, InteractionContentViewProtocol { + + + private var isSingleLayout:Bool = false { + didSet { + connectionStatusView?.isSingleLayout = isSingleLayout + connectionStatusView?.backButton?.removeAllHandlers() + connectionStatusView?.backButton?.set(handler: { [weak self] _ in + self?.chatInteraction.context.sharedContext.bindings.rootNavigation().back() + }, for: .Click) + } + } + private var reportPlaceholder: SelectMessagesPlaceholderView? private var connectionStatusView:ConnectionStatusView? = nil private let activities:ChatActivitiesModel private let searchButton:ImageButton = ImageButton() @@ -125,50 +324,114 @@ class ChatTitleBarView: TitledBarView { private let badgeNode:GlobalBadgeNode private let disposable = MetaDisposable() private let closeButton = ImageButton() - var connectionStatus:ConnectionStatus = .online { + private var lastestUsersController: ViewController? + private let fetchPeerAvatar = DisposableSet() + + private var videoAvatarView: VideoAvatarContainer? + + var connectionStatus:ConnectionStatus = .online(proxyAddress: nil) { didSet { if connectionStatus != oldValue { - if connectionStatus == .online { - containerView.change(pos: NSMakePoint(0, 0), animated: true) + if case .online = connectionStatus { + + //containerView.change(pos: NSMakePoint(0, 0), animated: true) if let connectionStatusView = connectionStatusView { connectionStatusView.change(pos: NSMakePoint(0, -frame.height), animated: true) - connectionStatusView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion:false, completion:{ [weak self] _ in + connectionStatusView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion:false, completion:{ [weak self] completed in self?.connectionStatusView?.removeFromSuperview() self?.connectionStatusView = nil }) } + } else { if connectionStatusView == nil { connectionStatusView = ConnectionStatusView(frame: NSMakeRect(0, -frame.height, frame.width, frame.height)) + connectionStatusView?.isSingleLayout = isSingleLayout connectionStatusView?.disableProxy = chatInteraction.disableProxy addSubview(connectionStatusView!) connectionStatusView?.change(pos: NSMakePoint(0,0), animated: true) - containerView.change(pos: NSMakePoint(0, frame.height), animated: true) } - connectionStatusView?.status = connectionStatus - + applyVideoAvatarIfNeeded(nil) } } } } + + private var rootRepliesCount: Int = 0 { + didSet { + updateTitle(presentation: chatInteraction.presentation) + } + } - var peerView:PeerView? { + var postboxView:PostboxView? { didSet { - updateStatus() + updateStatus(true, presentation: chatInteraction.presentation) + switch chatInteraction.mode { + case let .replyThread(data, _): + let answersCount = chatInteraction.context.account.postbox.messageView(data.messageId) + |> map { + $0.message?.attributes.compactMap { $0 as? ReplyThreadMessageAttribute }.first + } + |> map { + Int($0?.count ?? 0) + } + |> deliverOnMainQueue + + answersCountDisposable.set(answersCount.start(next: { [weak self] count in + self?.rootRepliesCount = count + })) + default: + answersCountDisposable.set(nil) + } + + } + } + + var onlineMemberCount:Int32? = nil { + didSet { + updateStatus(presentation: chatInteraction.presentation) } } + + var inputActivities:(PeerId, [(Peer, PeerInputActivity)])? { didSet { - if let inputActivities = inputActivities { - activities.update(with: inputActivities, for: max(frame.width - 60, 160), theme:theme.activity(key: 4, foregroundColor: theme.colors.blueUI, backgroundColor: theme.colors.background), layout: { [weak self] show in - self?.needsLayout = true - self?.hiddenStatus = show - self?.setNeedsDisplay() - self?.activities.view?.isHidden = !show + if let inputActivities = inputActivities, self.chatInteraction.mode != .scheduled && self.chatInteraction.mode != .pinned { + activities.update(with: inputActivities, for: max(frame.width - inset, 160), theme:theme.activity(key: 4, foregroundColor: theme.colors.accent, backgroundColor: theme.colors.background), layout: { [weak self] show in + guard let `self` = self else { return } + self.needsLayout = true + self.hiddenStatus = show + self.setNeedsDisplay() + + + + if let view = self.activities.view { + if self.animates { + if show { + if view.isHidden { + + } + view.isHidden = false + view.change(opacity: 1, duration: 0.2) + } else { + view.change(opacity: 0, completion: { [weak view] completed in + if completed { + view?.isHidden = true + } + }) + } + + } else { + view.layer?.opacity = 1 + view.layer?.removeAllAnimations() + view.isHidden = !show + } + } + }) } else { activities.clean() @@ -186,33 +449,40 @@ class ChatTitleBarView: TitledBarView { searchButton.disableActions() callButton.disableActions() - var layoutChanged:(()->Void)? + videoAvatarDisposable.set(peerPhotos(context: chatInteraction.context, peerId: chatInteraction.chatLocation.peerId).start()) - badgeNode = GlobalBadgeNode(chatInteraction.account, excludePeerId: self.chatInteraction.peerId, layoutChanged: { - layoutChanged?() + badgeNode = GlobalBadgeNode(chatInteraction.context.account, sharedContext: chatInteraction.context.sharedContext, excludePeerId: self.chatInteraction.peerId, view: View(), layoutChanged: { }) - - super.init(controller: controller, textInset: 46) - layoutChanged = { - //self?.needsLayout = true - } + super.init(controller: controller, textInset: 46) + addSubview(activities.view!) + + searchButton.isHidden = chatInteraction.mode == .preview searchButton.set(handler: { [weak self] _ in - self?.chatInteraction.update({$0.updatedSearchMode(!$0.isSearchMode)}) + self?.chatInteraction.update({$0.updatedSearchMode((!$0.isSearchMode.0, nil, nil))}) }, for: .Click) addSubview(searchButton) self.presenceManager = PeerPresenceStatusManager(update: { [weak self] in - self?.updateStatus() + guard let strongSelf = self else { + return + } + strongSelf.updateStatus(presentation: strongSelf.chatInteraction.presentation) }) - callButton.set(handler: { _ in - chatInteraction.call() + callButton.set(handler: { [weak self] _ in + guard let chatInteraction = self?.chatInteraction else { + return + } + if let groupCall = chatInteraction.presentation.groupCall { + chatInteraction.joinGroupCall(groupCall.activeCall, groupCall.joinHash) + } else { + chatInteraction.call() + } }, for: .Click) - addSubview(activities.view!) activities.view?.isHidden = true callButton.isHidden = true addSubview(callButton) @@ -220,7 +490,7 @@ class ChatTitleBarView: TitledBarView { avatarControl.setFrameSize(36,36) addSubview(avatarControl) - disposable.set(chatInteraction.account.context.layoutHandler.get().start(next: { [weak self] state in + disposable.set(chatInteraction.context.sharedContext.layoutHandler.get().start(next: { [weak self] state in if let strongSelf = self { switch state { case .single: @@ -228,11 +498,13 @@ class ChatTitleBarView: TitledBarView { strongSelf.badgeNode.view?.isHidden = false strongSelf.closeButton.isHidden = false default: - strongSelf.isSingleLayout = strongSelf.controller is ChatAdditionController + strongSelf.isSingleLayout = strongSelf.controller?.className != "Telegram.ChatController" strongSelf.badgeNode.view?.isHidden = true - strongSelf.closeButton.isHidden = !(strongSelf.controller is ChatAdditionController) + strongSelf.closeButton.isHidden = strongSelf.controller?.className == "Telegram.ChatController" && strongSelf.chatInteraction.mode.threadId == nil } - strongSelf.textInset = strongSelf.isSingleLayout ? 66 : 46 + strongSelf.avatarControl.isHidden = strongSelf.controller is ChatScheduleController || strongSelf.chatInteraction.mode.threadId != nil || strongSelf.chatInteraction.mode == .pinned + + strongSelf.textInset = strongSelf.avatarControl.isHidden ? 24 : strongSelf.isSingleLayout ? 66 : 46 strongSelf.needsLayout = true } })) @@ -241,19 +513,28 @@ class ChatTitleBarView: TitledBarView { closeButton.autohighlight = false closeButton.set(image: theme.icons.chatNavigationBack, for: .Normal) closeButton.set(handler: { [weak self] _ in - self?.chatInteraction.account.context.mainNavigation?.back() + self?.chatInteraction.context.sharedContext.bindings.rootNavigation().back() }, for: .Click) - closeButton.sizeToFit() + _ = closeButton.sizeToFit() closeButton.setFrameSize(closeButton.frame.width, frame.height) addSubview(closeButton) avatarControl.userInteractionEnabled = false - + addSubview(badgeNode.view!) - updateLocalizationAndTheme() + updateLocalizationAndTheme(theme: theme) + + self.continuesAction = true + + } + + func updateSearchButton(hidden: Bool, animated: Bool) { + searchButton.isHidden = hidden + needsLayout = true } + override func setFrameSize(_ newSize: NSSize) { super.setFrameSize(newSize) self.connectionStatusView?.setFrameSize(newSize) @@ -262,35 +543,186 @@ class ChatTitleBarView: TitledBarView { } + + func contentInteractionView(for stableId: AnyHashable, animateIn: Bool) -> NSView? { + if chatInteraction.presentation.mainPeer?.largeProfileImage?.resource.id.uniqueId == stableId.base as? String { + return avatarControl + } + return nil + } + func interactionControllerDidFinishAnimation(interactive: Bool, for stableId: AnyHashable) { + + } + func addAccesoryOnCopiedView(for stableId: AnyHashable, view: NSView) { + + } + func videoTimebase(for stableId: AnyHashable) -> CMTimebase? { + return nil + } + public func applyTimebase(for stableId: AnyHashable, timebase: CMTimebase?) { + + } + + private let videoAvatarDisposable = MetaDisposable() + private let answersCountDisposable = MetaDisposable() + + override func mouseEntered(with event: NSEvent) { + super.mouseEntered(with: event) + applyVideoAvatarIfNeeded(nil) + } + override func mouseExited(with event: NSEvent) { + super.mouseExited(with: event) + applyVideoAvatarIfNeeded(nil) + } + + override func mouseMoved(with event: NSEvent) { + super.mouseMoved(with: event) + + let point = convert(event.locationInWindow, from: nil) + + + if NSPointInRect(point, avatarControl.frame), chatInteraction.mode == .history, let peer = chatInteraction.presentation.mainPeer { + let signal = peerPhotos(context: chatInteraction.context, peerId: peer.id) |> deliverOnMainQueue + videoAvatarDisposable.set(signal.start(next: { [weak self] photos in + self?.applyVideoAvatarIfNeeded(photos.first) + })) + } else { + videoAvatarDisposable.set(nil) + applyVideoAvatarIfNeeded(nil) + } + } + + private var currentPhoto: TelegramPeerPhoto? + + private func applyVideoAvatarIfNeeded(_ photo: TelegramPeerPhoto?) { + guard let window = self.window as? Window, currentPhoto?.image != photo?.image else { + return + } + + currentPhoto = photo + + let point = convert(window.mouseLocationOutsideOfEventStream, from: nil) + + + let file: TelegramMediaFile? + let seekTo: TimeInterval? + if let photo = photo, let video = photo.image.videoRepresentations.last { + file = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: photo.image.representations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: video.resource.size, attributes: []) + seekTo = video.startTimestamp + } else { + seekTo = nil + file = nil + } + + if NSPointInRect(point, avatarControl.frame), chatInteraction.mode != .scheduled, chatInteraction.peerId != chatInteraction.context.peerId, self.connectionStatusView == nil, let file = file, let peer = chatInteraction.presentation.mainPeer { + let control: VideoAvatarContainer + if let view = self.videoAvatarView { + control = view + } else { + control = VideoAvatarContainer(frame: NSMakeRect(avatarControl.frame.minX - 2, avatarControl.frame.minY - 2, avatarControl.frame.width + 4, avatarControl.frame.height + 4)) + addSubview(control, positioned: .below, relativeTo: badgeNode.view) + control.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + control.animateIn() + self.videoAvatarView = control + } + control.updateWith(file: file, seekTo: seekTo, reference: PeerReference(peer), context: chatInteraction.context) + + } else { + if let view = self.videoAvatarView { + self.videoAvatarView = nil + view.animateOut() + view.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } + } + + } + override func mouseUp(with event: NSEvent) { + super.mouseUp(with: event) + + let point = convert(event.locationInWindow, from: nil) + + + if NSPointInRect(point, avatarControl.frame), chatInteraction.mode == .history, chatInteraction.peerId != chatInteraction.context.peerId { + if let peer = chatInteraction.presentation.mainPeer, let large = peer.largeProfileImage { + showPhotosGallery(context: chatInteraction.context, peerId: peer.id, firstStableId: AnyHashable(large.resource.id.uniqueId), self, nil) + return + } + } + if isSingleLayout { - let point = convert(event.locationInWindow, from: nil) if point.x > 20 { - chatInteraction.openInfo(chatInteraction.peerId, false, nil, nil) + if chatInteraction.mode == .history { + if chatInteraction.presentation.reportMode != nil { + + } else if chatInteraction.peerId == repliesPeerId { + + } else if chatInteraction.peerId == chatInteraction.context.peerId { + chatInteraction.context.sharedContext.bindings.rootNavigation().push(PeerMediaController(context: chatInteraction.context, peerId: chatInteraction.peerId)) + } else { + switch chatInteraction.chatLocation { + case let .peer(peerId): + chatInteraction.openInfo(peerId, false, nil, nil) + case .replyThread: + break + } + } + } + } else { - chatInteraction.account.context.mainNavigation?.back() + chatInteraction.context.sharedContext.bindings.rootNavigation().back() } } else { - chatInteraction.openInfo(chatInteraction.peerId, false, nil, nil) + if chatInteraction.presentation.reportMode != nil { + + } else if chatInteraction.peerId == repliesPeerId { + + } else if chatInteraction.peerId == chatInteraction.context.peerId { + chatInteraction.context.sharedContext.bindings.rootNavigation().push(PeerMediaController(context: chatInteraction.context, peerId: chatInteraction.peerId)) + } else { + switch chatInteraction.chatLocation { + case let .peer(peerId): + chatInteraction.openInfo(peerId, false, nil, nil) + case .replyThread: + break + } + } } } deinit { disposable.dispose() + fetchPeerAvatar.dispose() + videoAvatarDisposable.dispose() + answersCountDisposable.dispose() + } + + + override func setFrameOrigin(_ newOrigin: NSPoint) { + super.setFrameOrigin(newOrigin) } override func layout() { super.layout() - let additionInset:CGFloat = isSingleLayout ? 20 : 0 + let additionInset:CGFloat = isSingleLayout ? 20 : 2 avatarControl.centerY(x: additionInset) searchButton.centerY(x:frame.width - searchButton.frame.width) callButton.centerY(x: searchButton.isHidden ? frame.width - callButton.frame.width : searchButton.frame.minX - callButton.frame.width - 20) - activities.view?.setFrameOrigin(avatarControl.frame.maxX + 8, 25) + if !avatarControl.isHidden { + activities.view?.setFrameOrigin(avatarControl.frame.maxX + 8, 25) + } else { + activities.view?.setFrameOrigin(24, 25) + } badgeNode.view!.setFrameOrigin(6,4) closeButton.centerY() + + reportPlaceholder?.frame = bounds + } @@ -303,42 +735,159 @@ class ChatTitleBarView: TitledBarView { } override var inset:CGFloat { - return 36 + 50 + (callButton.isHidden ? 20 : callButton.frame.width + 30) + return 36 + 50 + (callButton.isHidden ? 10 : callButton.frame.width + 35) } + + private var currentRepresentations: [TelegramMediaImageRepresentation] = [] + + private func checkPhoto(_ peer: Peer?) { + if let peer = peer { + var representations:[TelegramMediaImageRepresentation] = []//peer.profileImageRepresentations + if let representation = peer.smallProfileImage { + representations.append(representation) + } + if let representation = peer.largeProfileImage { + representations.append(representation) + } + + if self.currentRepresentations != representations { + applyVideoAvatarIfNeeded(nil) + videoAvatarDisposable.set(peerPhotos(context: chatInteraction.context, peerId: peer.id, force: true).start()) + + + if let peerReference = PeerReference(peer) { + if let largeProfileImage = peer.largeProfileImage { + fetchPeerAvatar.add(fetchedMediaResource(mediaBox: chatInteraction.context.account.postbox.mediaBox, reference: .avatar(peer: peerReference, resource: largeProfileImage.resource)).start()) + } + if let smallProfileImage = peer.smallProfileImage { + fetchPeerAvatar.add(fetchedMediaResource(mediaBox: chatInteraction.context.account.postbox.mediaBox, reference: .avatar(peer: peerReference, resource: smallProfileImage.resource)).start()) + } + } + } + self.currentRepresentations = representations + } + } - func updateStatus(_ force:Bool = false) { - var shouldUpdateLayout = false - if let peerView = self.peerView { + func updateStatus(_ force:Bool = false, presentation: ChatPresentationInterfaceState) { + + if presentation.reportMode != nil { + if self.reportPlaceholder == nil { + self.reportPlaceholder = SelectMessagesPlaceholderView(frame: bounds) + addSubview(self.reportPlaceholder!) + } + } else { + self.reportPlaceholder?.removeFromSuperview() + self.reportPlaceholder = nil + } + + + if let peerView = self.postboxView as? PeerView { - if let peer = peerViewMainPeer(peerView) { - callButton.isHidden = !peer.canCall || chatInteraction.peerId == chatInteraction.account.peerId - } else { + checkPhoto(peerViewMainPeer(peerView)) + + switch chatInteraction.mode { + case .history: + if let peer = peerViewMainPeer(peerView) { + if peer.isGroup || peer.isSupergroup || peer.isChannel { + if let groupCall = presentation.groupCall { + if let data = groupCall.data, data.participantCount == 0 && groupCall.activeCall.scheduleTimestamp == nil { + callButton.isHidden = presentation.reportMode != nil + } else { + callButton.isHidden = true + } + } else { + callButton.isHidden = true + } + } else { + callButton.isHidden = !peer.canCall || chatInteraction.peerId == chatInteraction.context.peerId || presentation.reportMode != nil + } + } else { + callButton.isHidden = true + } + + default: callButton.isHidden = true } + - // if let peer = peerView.peers[peerView.peerId] { - // searchButton.isHidden = peer is TelegramSecretChat - // } if let peer = peerViewMainPeer(peerView) { - avatarControl.setPeer(account: chatInteraction.account, peer: peer) + if peer.id == repliesPeerId { + let icon = theme.icons.chat_replies_avatar + avatarControl.setSignal(generateEmptyPhoto(avatarControl.frame.size, type: .icon(colors: theme.colors.peerColors(5), icon: icon, iconSize: icon.backingSize.aspectFitted(NSMakeSize(avatarControl.frame.size.width - 17, avatarControl.frame.size.height - 17)), cornerRadius: nil)) |> map {($0, false)}) + } else if peer.id == chatInteraction.context.peerId { + let icon = theme.icons.searchSaved + avatarControl.setSignal(generateEmptyPhoto(avatarControl.frame.size, type: .icon(colors: theme.colors.peerColors(5), icon: icon, iconSize: icon.backingSize.aspectFitted(NSMakeSize(avatarControl.frame.size.width - 15, avatarControl.frame.size.height - 15)), cornerRadius: nil)) |> map {($0, false)}) + } else { + avatarControl.setPeer(account: chatInteraction.context.account, peer: peer) + } } if peerView.peers[peerView.peerId] is TelegramSecretChat { - titleImage = theme.icons.chatSecretTitle + titleImage = (theme.icons.chatSecretTitle, .left(topInset: 0)) + callButton.set(image: theme.icons.chatCall, for: .Normal) + callButton.set(image: theme.icons.chatCallActive, for: .Highlight) + } else if let peer = peerViewMainPeer(peerView), chatInteraction.mode == .history { + if peer.isVerified { + titleImage = (theme.icons.verifiedImage, .right(topInset: 0)) + } else if peer.isScam { + titleImage = (theme.icons.scam, .right(topInset: 0)) + } else if peer.isFake { + titleImage = (theme.icons.fake, .right(topInset: 0)) + } else if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings, notificationSettings.isMuted { + titleImage = (theme.icons.dialogMuteImage, .right(topInset: 3)) + } else { + titleImage = nil + } + + if peer.isGroup || peer.isSupergroup || peer.isChannel { + callButton.set(image: theme.icons.chat_voice_chat, for: .Normal) + callButton.set(image: theme.icons.chat_voice_chat_active, for: .Highlight) + } else { + callButton.set(image: theme.icons.chatCall, for: .Normal) + callButton.set(image: theme.icons.chatCallActive, for: .Highlight) + } } else { titleImage = nil } + callButton.sizeToFit() + + updateTitle(force, presentation: chatInteraction.presentation) + } + } + + private func updateTitle(_ force: Bool = false, presentation: ChatPresentationInterfaceState) { + var shouldUpdateLayout = false + if let peerView = self.postboxView as? PeerView { + var result = stringStatus(for: peerView, context: chatInteraction.context, theme: PeerStatusStringTheme(titleFont: .medium(.title)), onlineMemberCount: self.onlineMemberCount) - var result = stringStatus(for: peerView, theme: PeerStatusStringTheme(titleFont: .medium(.title))) - if chatInteraction.account.peerId == peerView.peerId { - result = PeerStatusStringResult(result.title, .initialize(string: tr(.chatTitleSelf), color: theme.colors.grayText, font: .normal(.short)), presence: result.presence) + if chatInteraction.mode == .pinned { + result = result.withUpdatedTitle(L10n.chatTitlePinnedMessagesCountable(presentation.pinnedMessageId?.totalCount ?? 0)) + status = nil + } else if chatInteraction.context.peerId == peerView.peerId { + if chatInteraction.mode == .scheduled { + result = result.withUpdatedTitle(L10n.chatTitleReminder) + } else { + result = result.withUpdatedTitle(L10n.peerSavedMessages) + } + } else if chatInteraction.mode == .scheduled { + result = result.withUpdatedTitle(L10n.chatTitleScheduledMessages) + } else if case .replyThread(_, let mode) = chatInteraction.mode { + switch mode { + case .comments: + result = result.withUpdatedTitle(L10n.chatTitleCommentsCountable(self.rootRepliesCount)) + case .replies: + result = result.withUpdatedTitle(L10n.chatTitleRepliesCountable(self.rootRepliesCount)) + } + status = .initialize(string: result.title.string, color: theme.colors.grayText, font: .normal(12)) + result = result.withUpdatedTitle(L10n.chatTitleDiscussion) } - - if status == nil || !status!.isEqual(to: result.status) || force { + if chatInteraction.context.peerId == peerView.peerId { + status = nil + } else if (status == nil || !status!.isEqual(to: result.status) || force) && chatInteraction.mode != .scheduled && chatInteraction.mode.threadId == nil && chatInteraction.mode != .pinned { status = result.status shouldUpdateLayout = true } @@ -347,35 +896,29 @@ class ChatTitleBarView: TitledBarView { text = result.title shouldUpdateLayout = true } - if let presence = result.presence { - self.presenceManager?.reset(presence: presence) - } - if shouldUpdateLayout { - self.setNeedsDisplay() + self.presenceManager?.reset(presence: presence, timeDifference: Int32(chatInteraction.context.timeDifference)) } } + + if shouldUpdateLayout { + setNeedsDisplay() + } } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) searchButton.set(image: theme.icons.chatSearch, for: .Normal) - searchButton.sizeToFit() - - callButton.set(image: theme.icons.chatCall, for: .Normal) - callButton.sizeToFit() + searchButton.set(image: theme.icons.chatSearchActive, for: .Highlight) + + _ = searchButton.sizeToFit() closeButton.set(image: theme.icons.chatNavigationBack, for: .Normal) let inputActivities = self.inputActivities self.inputActivities = inputActivities - if let peerView = peerView, peerView.peers[peerView.peerId] is TelegramSecretChat { - titleImage = theme.icons.chatSecretTitle - } else { - titleImage = nil - } - + updateStatus(true, presentation: chatInteraction.presentation) } } diff --git a/Telegram-Mac/ChatTouchBar.swift b/Telegram-Mac/ChatTouchBar.swift new file mode 100644 index 0000000000..3fe3f8dfc0 --- /dev/null +++ b/Telegram-Mac/ChatTouchBar.swift @@ -0,0 +1,652 @@ +// +// ChatTouchBar.swift +// Telegram +// +// Created by Mikhail Filimonov on 13/09/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import TGUIKit +import SwiftSignalKit +import Postbox +import TelegramCore + + +@available(OSX 10.12.2, *) +extension NSTouchBar.CustomizationIdentifier { + static let windowBar = NSTouchBar.CustomizationIdentifier("\(Bundle.main.bundleIdentifier!).windowBar") + static let popoverBar = NSTouchBar.CustomizationIdentifier("\(Bundle.main.bundleIdentifier!).popoverBar") +} + + +@available(OSX 10.12.2, *) +private extension NSTouchBarItem.Identifier { + static let chatNextAndPrev = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.chat.chatNextAndPrev") + + static let chatStickersAndEmojiPicker = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.chat.StickerAndEmojiPicker") + + static let chatInfoAndAttach = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.chat.chatInfoAndAttach") + static let markdown = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.chat.markdown") + + static func chatInputAction(_ key:String) -> NSTouchBarItem.Identifier { + return NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.chat.InputAction\(key)") + } + static let chatDeleteMessages = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.chat.DeleteMessages") + static let chatForwardMessages = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.chat.ForwardMessages") + + static let chatEditMessageDone = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.chat.EditMessageDone") + static let chatEditMessageCancel = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.chat.EditMessageCancel") + static let chatEditMessageUpdateMedia = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.chat.EditMessage.UpdateMedia") + static let chatEditMessageUpdateFile = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.chat.EditMessage.UpdateFile") + + static let chatSuggestStickers = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.chat.SuggestStickers") + +} +@available(OSX 10.12.2, *) +func inputChatTouchBarItems(presentation: ChatPresentationInterfaceState) -> [NSTouchBarItem.Identifier] { + if let result = presentation.inputQueryResult { + switch result { + case .stickers: + return [] + default: + break + } + } + + if presentation.state == .editing { + return [] + } else { + switch presentation.state { + case .normal: + return [.candidateList] + default: + return [] + } + } +} +@available(OSX 10.12.2, *) +func touchBarChatItems(presentation: ChatPresentationInterfaceState, layout: SplitViewState, isKeyWindow: Bool) -> (items: [NSTouchBarItem.Identifier], escapeReplacement: NSTouchBarItem.Identifier?) { + + if presentation.isSearchMode.0 { + return (items: [], escapeReplacement: nil) + } + if presentation.state == .editing { + var items: [NSTouchBarItem.Identifier] = [] + items.append(.chatEditMessageDone) + + if let editState = presentation.interfaceState.editState, let media = editState.message.media.first, media is TelegramMediaFile || media is TelegramMediaImage { + items.append(.flexibleSpace) + items.append(.chatEditMessageUpdateMedia) + if editState.message.groupingKey == nil { + items.append(.chatEditMessageUpdateFile) + } + items.append(.flexibleSpace) + } + if !presentation.effectiveInput.selectionRange.isEmpty { + items.append(.flexibleSpace) + items.append(.markdown) + items.append(.flexibleSpace) + } + if isKeyWindow { + items.append(.otherItemsProxy) + } + + return (items: items, escapeReplacement: .chatEditMessageCancel) + } else { + //if presentation.effectiveInput.inputText.isEmpty { + var items: [NSTouchBarItem.Identifier] = [] + if layout != .single { + // items.append(.chatNextAndPrev) + } + // items.append(.chatInfoAndSearch) + //items.append(.fixedSpaceSmall) + switch presentation.state { + case .normal: + // items.append(.characterPicker) + if let peer = presentation.peer, permissionText(from: peer, for: .banSendStickers) == nil { + items.append(.chatStickersAndEmojiPicker) + // items.append(.fixedSpaceSmall) + } + + + if let peer = presentation.peer, permissionText(from: peer, for: .banSendMedia) == nil { + // items.append(.flexibleSpace) + var appendAttachment: Bool = true + if let result = presentation.inputQueryResult { + switch result { + case .stickers: + if permissionText(from: peer, for: .banSendStickers) == nil { + items.append(.chatSuggestStickers) + appendAttachment = false + } + default: + break + } + } + if appendAttachment { + items.append(.chatInfoAndAttach) + } + items.append(.flexibleSpace) + } + + if !presentation.effectiveInput.selectionRange.isEmpty { + //items.append(.flexibleSpace) + items.append(.markdown) + items.append(.flexibleSpace) + } + + if isKeyWindow { + items.append(.otherItemsProxy) + } + + case .selecting: + if presentation.reportMode == nil { + items.append(.flexibleSpace) + items.append(.chatDeleteMessages) + items.append(.chatForwardMessages) + items.append(.flexibleSpace) + } + + case let .action(text, _, _): + if !(presentation.peer is TelegramSecretChat) { + items.append(.flexibleSpace) + items.append(.chatInputAction(text)) + items.append(.flexibleSpace) + } + case let .channelWithDiscussion(_, leftAction, rightAction): + items.append(.flexibleSpace) + items.append(.chatInputAction(leftAction)) + items.append(.chatInputAction(rightAction)) + items.append(.flexibleSpace) + default: + break + } + return (items: items, escapeReplacement: nil) + } +} + + + + +@available(OSX 10.12.2, *) +class ChatTouchBar: NSTouchBar, NSTouchBarDelegate, Notifable { + + private let loadStickersDisposable = MetaDisposable() + private let loadRecentEmojiDisposable = MetaDisposable() + + private var chatInteraction: ChatInteraction? + private var textView: NSTextView + private let candidateListItem = NSCandidateListTouchBarItem(identifier: .candidateList) + private let layoutStateDisposable = MetaDisposable() + init(chatInteraction: ChatInteraction, textView: NSTextView) { + self.chatInteraction = chatInteraction + self.textView = textView + super.init() + self.delegate = self + let result = touchBarChatItems(presentation: chatInteraction.presentation, layout: chatInteraction.context.sharedContext.layout, isKeyWindow: true) + self.defaultItemIdentifiers = result.items + self.escapeKeyReplacementItemIdentifier = result.escapeReplacement + self.customizationAllowedItemIdentifiers = self.defaultItemIdentifiers + self.textView.updateTouchBarItemIdentifiers() + self.customizationIdentifier = .windowBar + layoutStateDisposable.set(chatInteraction.context.sharedContext.layoutHandler.get().start(next: { [weak self] _ in + guard let `self` = self, let chatInteraction = self.chatInteraction else {return} + self.notify(with: chatInteraction.presentation, oldValue: chatInteraction.presentation, animated: true) + })) + } + + func updateChatInteraction(_ chatInteraction: ChatInteraction, textView: NSTextView) -> Void { + self.chatInteraction?.remove(observer: self) + prevIsKeyWindow = nil + chatInteraction.add(observer: self) + self.chatInteraction = chatInteraction + textView.updateTouchBarItemIdentifiers() + self.textView = textView + // self.notify(with: chatInteraction.presentation, oldValue: chatInteraction.presentation, animated: false) + } + + func updateByKeyWindow() { + if let chatInteraction = self.chatInteraction { + self.notify(with: chatInteraction.presentation, oldValue: chatInteraction.presentation, animated: false) + } + } + + func isEqual(to other: Notifable) -> Bool { + return false + } + + deinit { + chatInteraction?.remove(observer: self) + loadRecentEmojiDisposable.dispose() + loadStickersDisposable.dispose() + layoutStateDisposable.dispose() + } + private var prevIsKeyWindow: Bool? = nil + + func notify(with value: Any, oldValue: Any, animated: Bool) { + if let value = value as? ChatPresentationInterfaceState, let oldValue = oldValue as? ChatPresentationInterfaceState, let chatInteraction = self.chatInteraction { + if !animated || oldValue.state != value.state || oldValue.effectiveInput.selectionRange.isEmpty != value.effectiveInput.selectionRange.isEmpty || prevIsKeyWindow != textView.window?.isKeyWindow || oldValue.inputQueryResult != value.inputQueryResult || oldValue.selectionState != value.selectionState || oldValue.canInvokeBasicActions != value.canInvokeBasicActions { + self.prevIsKeyWindow = textView.window?.isKeyWindow + let result = touchBarChatItems(presentation: value, layout: chatInteraction.context.sharedContext.layout, isKeyWindow: textView.window?.isKeyWindow ?? false) + self.defaultItemIdentifiers = result.items + self.escapeKeyReplacementItemIdentifier = result.escapeReplacement + self.customizationAllowedItemIdentifiers = self.defaultItemIdentifiers + self.textView.updateTouchBarItemIdentifiers() + updateUserInterface() + } + } + } + + + @objc private func chatInfoAction() { + guard let item = self.item(forIdentifier: .chatInfoAndAttach) as? NSPopoverTouchBarItem, let chatInteraction = self.chatInteraction else {return} + item.popoverTouchBar = ChatInfoTouchbar(chatInteraction: chatInteraction, dismiss: { [weak item] in + item?.dismissPopover(nil) + }) + item.showPopover(item) + } + @objc private func searchAction() { + chatInteraction?.update({$0.updatedSearchMode((!$0.isSearchMode.0, nil, nil))}) + } + + @objc private func attachPhotoOrVideo() { + chatInteraction?.attachPhotoOrVideo() + } + @objc private func attachPicture() { + chatInteraction?.attachPicture() + } + @objc private func attachFile() { + chatInteraction?.attachFile(false) + } + @objc private func attachLocation() { + chatInteraction?.attachLocation() + } + @objc private func invokeInputAction(_ sender: Any?) { + if let chatInteraction = self.chatInteraction { + switch chatInteraction.presentation.state { + case .action(_, let action, _): + action(chatInteraction) + case let .channelWithDiscussion(_, leftAction, rightAction): + if let sender = sender as? NSButton { + switch sender.title { + case leftAction: + chatInteraction.toggleNotifications(nil) + case rightAction: + chatInteraction.openDiscussion() + default: + break + } + } + default: + break + } + } + + } + + private func showEmojiPickerPopover(recent: [String], segments: [EmojiSegment : [String]]) { + guard let item = self.item(forIdentifier: .chatStickersAndEmojiPicker) as? NSPopoverTouchBarItem else {return} + + item.popoverTouchBar = TouchBarEmojiPicker(recent: recent, segments: segments, selectedEmoji: { [weak self, weak item] emoji in + guard let chatInteraction = self?.chatInteraction else {return} + if chatInteraction.presentation.effectiveInput.inputText.isEmpty { + item?.dismissPopover(nil) + } + _ = chatInteraction.appendText(emoji) + }) + item.showPopover(item) + } + + private func showStickersPopover(_ itemCollectionView: ItemCollectionsView) { + guard let item = self.item(forIdentifier: .chatStickersAndEmojiPicker) as? NSPopoverTouchBarItem, let chatInteraction = self.chatInteraction else {return} + var stickers: (favorite: [TelegramMediaFile], recent: [TelegramMediaFile], packs: [(StickerPackCollectionInfo, [TelegramMediaFile])]) = (favorite: [], recent: [], packs: []) + + stickers.favorite = Array(itemCollectionView.orderedItemListsViews[0].items.compactMap {($0.contents as? SavedStickerItem)?.file}.prefix(5)) + stickers.recent = Array(itemCollectionView.orderedItemListsViews[1].items.compactMap {($0.contents as? RecentMediaItem)?.media as? TelegramMediaFile}.prefix(20)) + + var collections: [ItemCollectionId : [TelegramMediaFile]] = [:] + + for entry in itemCollectionView.entries { + var collection = collections[entry.index.collectionId] + if collection == nil { + collection = [] + collections[entry.index.collectionId] = collection + } + if let item = entry.item as? StickerPackItem { + collections[entry.index.collectionId]?.append(item.file) + } + } + + for (key, value) in collections { + let info = itemCollectionView.collectionInfos.first(where: {$0.0 == key}) + if let info = info?.1 as? StickerPackCollectionInfo { + stickers.packs.append((info, value)) + } + } + + var entries: [TouchBarStickerEntry] = [] + if !stickers.favorite.isEmpty { + let layout = TextViewLayout(.initialize(string: L10n.touchBarFavorite, color: .grayText, font: .normal(.header))) + layout.measure(width: .greatestFiniteMagnitude) + entries.append(.header(layout)) + entries.append(contentsOf: stickers.favorite.map {.sticker($0)}) + } + if !stickers.recent.isEmpty { + let layout = TextViewLayout(.initialize(string: L10n.touchBarRecent, color: .grayText, font: .normal(.header))) + layout.measure(width: .greatestFiniteMagnitude) + entries.append(.header(layout)) + entries.append(contentsOf: stickers.recent.map {.sticker($0)}) + } + for pack in stickers.packs { + let layout = TextViewLayout(.initialize(string: "\(pack.0.title)", color: .grayText, font: .normal(.header))) + layout.measure(width: .greatestFiniteMagnitude) + entries.append(.header(layout)) + entries.append(contentsOf: pack.1.map {.sticker($0)}) + } + + item.popoverTouchBar = ChatStickersTouchBarPopover(chatInteraction: chatInteraction, dismiss: { [weak item, weak self] file in + if let file = file { + self?.chatInteraction?.sendAppFile(file, false, nil) + } + item?.dismissPopover(nil) + }, entries: entries) + item.showPopover(item) + } + + + @objc private func openEmojiOrStickersPicker(_ sender: Any?) { + if let segmentControl = sender as? NSSegmentedControl, let chatInteraction = self.chatInteraction { + switch segmentControl.selectedSegment { + case 0: + loadRecentEmojiDisposable.set((recentUsedEmoji(postbox: chatInteraction.context.account.postbox) |> deliverOnPrepareQueue |> map { ($0, emojiesInstance)} |> take(1) |> deliverOnMainQueue).start(next: { [weak self] recent, segments in + self?.showEmojiPickerPopover(recent: recent.emojies, segments: segments) + })) + case 1: + loadStickersDisposable.set((chatInteraction.context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: nil, count: 200) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] itemCollectionView in + self?.showStickersPopover(itemCollectionView) + })) + default: + break + } + } + + } + + @objc private func forwardMessages() { + chatInteraction?.forwardSelectedMessages() + } + @objc private func deleteMessages() { + chatInteraction?.deleteSelectedMessages() + } + + @objc private func saveEditingMessage() { + chatInteraction?.sendMessage(false, nil) + } + @objc private func replaceWithFile() { + chatInteraction?.updateEditingMessageMedia(nil, false) + } + @objc private func replaceWithMedia() { + chatInteraction?.updateEditingMessageMedia(mediaExts, true) + } + @objc private func cancelMessageEditing() { + chatInteraction?.cancelEditing() + } + @objc private func infoAndAttach(_ sender: Any?) { + + if let segmentControl = sender as? NSSegmentedControl { + switch segmentControl.selectedSegment { + case 1: + chatInfoAction() + case 0: + attachFile() + default: + break + } + } + } + @objc private func upOrNext(_ sender: Any?) { + if let segmentControl = sender as? NSSegmentedControl { + switch segmentControl.selectedSegment { + case 0: + mainWindow.sendKeyEvent(KeyboardKey.Tab, modifierFlags: [.control, .shift]) + case 1: + mainWindow.sendKeyEvent(KeyboardKey.Tab, modifierFlags: [.control]) + default: + break + } + } + } + @objc private func markdown(_ sender: Any?) { + if let segmentControl = sender as? NSSegmentedControl { + switch segmentControl.selectedSegment { + case 0: + mainWindow.sendKeyEvent(KeyboardKey.B, modifierFlags: [.command]) + case 1: + mainWindow.sendKeyEvent(KeyboardKey.I, modifierFlags: [.command]) + case 2: + mainWindow.sendKeyEvent(KeyboardKey.U, modifierFlags: [.command]) + default: + break + } + } + } + + func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? { + +// let actionKey: String +// switch chatInteraction.presentation.state { +// case let .action(title, _): +// actionKey = title +// default: +// actionKey = "" +// } + + if let range = identifier.rawValue.range(of: NSTouchBarItem.Identifier.chatInputAction("").rawValue) { + let actionKey = String(identifier.rawValue[range.upperBound...]) + let item = NSCustomTouchBarItem(identifier: identifier) + let button = NSButton(title: actionKey, target: self, action: #selector(invokeInputAction(_:))) + button.addWidthConstraint(size: 200) + button.bezelColor = actionKey == L10n.chatInputMute || actionKey == L10n.chatInputUnmute ? nil : theme.colors.accent + item.view = button + item.customizationLabel = button.title + return item + } + + switch identifier { + case .chatNextAndPrev: + let item = NSPopoverTouchBarItem(identifier: identifier) + + let segment = NSSegmentedControl() + segment.segmentStyle = .separated + segment.segmentCount = 2 + segment.setImage(NSImage(named: NSImage.touchBarGoUpTemplateName)!, forSegment: 0) + segment.setImage(NSImage(named: NSImage.touchBarGoDownTemplateName)!, forSegment: 1) + segment.setWidth(93, forSegment: 0) + segment.setWidth(93, forSegment: 1) + segment.trackingMode = .momentary + segment.target = self + segment.action = #selector(upOrNext(_:)) + item.collapsedRepresentation = segment + return item +// case .chatInfoAndSearch: +// let item = NSPopoverTouchBarItem(identifier: identifier) +// +// let segment = NSSegmentedControl() +// segment.segmentStyle = .separated +// segment.segmentCount = 2 +// segment.setImage(NSImage(named: NSImage.Name("Icon_TouchBar_Info"))!, forSegment: 0) +// segment.setImage(NSImage(named: NSImage.Name("Icon_TouchBar_Search"))!, forSegment: 1) +// segment.setWidth(93, forSegment: 0) +// segment.setWidth(93, forSegment: 1) +// segment.trackingMode = .momentary +// segment.target = self +// segment.action = #selector(infoOrSearchAction(_:)) +// item.collapsedRepresentation = segment +// return item + case .chatStickersAndEmojiPicker: + + let item = NSPopoverTouchBarItem(identifier: identifier) + + let segment = NSSegmentedControl() + segment.segmentStyle = .separated + segment.segmentCount = 2 + segment.setImage(NSImage(named: NSImage.Name("Icon_TouchBar_Emoji"))!, forSegment: 0) + segment.setImage(NSImage(named: NSImage.Name("Icon_TouchBar_Stickers"))!, forSegment: 1) + segment.setWidth(92, forSegment: 0) + segment.setWidth(92, forSegment: 1) + segment.target = self + segment.action = #selector(openEmojiOrStickersPicker(_:)) + segment.trackingMode = .momentary + item.visibilityPriority = .high + item.collapsedRepresentation = segment + item.customizationLabel = L10n.touchBarLabelEmojiAndStickers; + return item + +// let item = NSPopoverTouchBarItem(identifier: identifier) +// +// let icon = NSImage(named: NSImage.Name("Icon_TouchBar_Stickers"))! +// let button = NSButton(image: icon, target: self, action: #selector(loadStickers)) +// +// item.collapsedRepresentation = button +// item.customizationLabel = button.title +// return item + + case .chatInfoAndAttach: + let item = NSPopoverTouchBarItem(identifier: identifier) + + let segment = NSSegmentedControl() + segment.segmentStyle = .separated + segment.segmentCount = 2 + segment.setImage(NSImage(named: NSImage.Name("Icon_TouchBar_ChatAttach"))!, forSegment: 0) + segment.setImage(NSImage(named: NSImage.Name("Icon_TouchBar_ChatMore"))!, forSegment: 1) + segment.setWidth(98, forSegment: 0) + segment.setWidth(98, forSegment: 1) + segment.trackingMode = .momentary + segment.target = self + segment.action = #selector(infoAndAttach(_:)) + item.collapsedRepresentation = segment + item.customizationLabel = L10n.touchBarLabelChatActions; + return item + case .markdown: + let item = NSPopoverTouchBarItem(identifier: identifier) + + let segment = NSSegmentedControl() + segment.segmentStyle = .separated + segment.segmentCount = 3 + segment.setImage(NSImage(named: NSImage.touchBarTextBoldTemplateName)!, forSegment: 0) + segment.setImage(NSImage(named: NSImage.touchBarTextItalicTemplateName)!, forSegment: 1) + segment.setImage(NSImage(named: NSImage.Name("Icon_ChatTouchBarAddLink"))!, forSegment: 2) + + +// segment.setWidth(98, forSegment: 0) +// segment.setWidth(98, forSegment: 1) + segment.trackingMode = .momentary + segment.target = self + segment.action = #selector(markdown(_:)) + item.collapsedRepresentation = segment + return item + case .chatEditMessageDone: + let item = NSCustomTouchBarItem(identifier: identifier) + let button = NSButton(title: L10n.navigationDone, target: self, action: #selector(saveEditingMessage)) + button.bezelColor = theme.colors.accent + item.view = button + item.customizationLabel = button.title + return item + case .chatEditMessageUpdateMedia: + let item = NSCustomTouchBarItem(identifier: identifier) + let icon = NSImage(named: NSImage.Name("Icon_TouchBar_AttachPhotoOrVideo"))! + let button = NSButton(title: L10n.touchBarEditMessageReplaceWithMedia, image: icon, target: self, action: #selector(replaceWithMedia)) + button.imageHugsTitle = true + item.view = button + item.customizationLabel = button.title + return item + case .chatEditMessageUpdateFile: + let item = NSCustomTouchBarItem(identifier: identifier) + let icon = NSImage(named: NSImage.Name("Icon_TouchBar_AttachFile"))! + let button = NSButton(title: L10n.touchBarEditMessageReplaceWithFile, image: icon, target: self, action: #selector(replaceWithFile)) + button.imageHugsTitle = true + item.view = button + item.customizationLabel = button.title + return item + case .chatEditMessageDone: + let item = NSCustomTouchBarItem(identifier: identifier) + let button = NSButton(title: L10n.navigationDone, target: self, action: #selector(attachFile)) + button.bezelColor = theme.colors.accent + item.view = button + item.customizationLabel = button.title + return item + case .chatEditMessageCancel: + let item = NSCustomTouchBarItem(identifier: identifier) + let button = NSButton(title: L10n.navigationCancel, target: self, action: #selector(cancelMessageEditing)) + item.view = button + item.customizationLabel = button.title + return item +// case chatInputAction(actionKey): +// let item = NSCustomTouchBarItem(identifier: identifier) +// let button = NSButton(title: actionKey, target: self, action: #selector(invokeInputAction)) +// button.addWidthConstraint(size: 200) +// button.bezelColor = actionKey == L10n.chatInputMute || actionKey == L10n.chatInputUnmute ? nil : theme.colors.accent +// item.view = button +// item.customizationLabel = button.title +// return item + case .chatForwardMessages: + let item = NSCustomTouchBarItem(identifier: identifier) + let button = NSButton(title: L10n.messageActionsPanelForward, target: self, action: #selector(forwardMessages)) + button.addWidthConstraint(size: 160) + button.bezelColor = theme.colors.accent + button.imageHugsTitle = true + button.isEnabled = self.chatInteraction?.presentation.canInvokeBasicActions.forward ?? false + item.view = button + item.customizationLabel = button.title + return item + case .chatDeleteMessages: + let item = NSCustomTouchBarItem(identifier: identifier) + let button = NSButton(title: L10n.messageActionsPanelDelete, target: self, action: #selector(deleteMessages)) + button.addWidthConstraint(size: 160) + button.bezelColor = theme.colors.redUI + button.imageHugsTitle = true + button.isEnabled = self.chatInteraction?.presentation.canInvokeBasicActions.delete ?? false + item.view = button + item.customizationLabel = button.title + return item + case .chatSuggestStickers: + if let result = self.chatInteraction?.presentation.inputQueryResult, let chatInteraction = self.chatInteraction { + switch result { + case let .stickers(stickers): + return StickersScrubberBarItem(identifier: identifier, context: chatInteraction.context, animated: false, sendSticker: { [weak self] file in + self?.chatInteraction?.sendAppFile(file, false, nil) + self?.chatInteraction?.clearInput() + }, entries: stickers.map({.sticker($0.file)})) + default: + break + } + } + + default: + break + } + return nil + } + + private func updateUserInterface() { + for identifier in itemIdentifiers { + switch identifier { + case .chatForwardMessages: + let button = (item(forIdentifier: identifier) as? NSCustomTouchBarItem)?.view as? NSButton + button?.bezelColor = self.chatInteraction?.presentation.canInvokeBasicActions.forward ?? false ? theme.colors.accent : nil + button?.isEnabled = self.chatInteraction?.presentation.canInvokeBasicActions.forward ?? false + + case .chatDeleteMessages: + let button = (item(forIdentifier: identifier) as? NSCustomTouchBarItem)?.view as? NSButton + button?.bezelColor = self.chatInteraction?.presentation.canInvokeBasicActions.delete ?? false ? theme.colors.redUI : nil + button?.isEnabled = self.chatInteraction?.presentation.canInvokeBasicActions.delete ?? false + default: + break + } + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/ChatUndoManager.swift b/Telegram-Mac/ChatUndoManager.swift new file mode 100644 index 0000000000..2b5566cb9f --- /dev/null +++ b/Telegram-Mac/ChatUndoManager.swift @@ -0,0 +1,447 @@ +// +// ChatUndoManager.swift +// Telegram +// +// Created by Mikhail Filimonov on 09/01/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import SwiftSignalKit +import Postbox +import TelegramCore + + + +private let queue: Queue = Queue() + + +enum ChatUndoActionType : Equatable { + case clearHistory + case deleteChat + case deleteChannel + case leftChat + case leftChannel + case archiveChat +} + + +enum ChatUndoActionStatus : Equatable { + case processing + case success + case cancelled + case none +} + +private final class ChatUndoActionStatusContext { + var status: ChatUndoActionStatus? { + didSet { + for subscriber in subscribers.copyItems() { + subscriber(status) + } + } + } + let subscribers = Bag<(ChatUndoActionStatus?) -> Void>() +} + +private struct ChatUndoActionKey : Hashable { + private let peerId: PeerId + private let type: ChatUndoActionType + init(peerId: PeerId, type: ChatUndoActionType) { + self.peerId = peerId + self.type = type + } + var hashValue: Int { + return Int(peerId.toInt64()) + } +} + +struct ChatUndoAction : Hashable { + static func == (lhs: ChatUndoAction, rhs: ChatUndoAction) -> Bool { + return lhs.peerId == rhs.peerId && lhs.type == rhs.type + } + fileprivate let endpoint: Double + fileprivate let duration: Double + fileprivate let peerId: PeerId + let type: ChatUndoActionType + fileprivate let action: (ChatUndoActionStatus) -> Void + init(peerId: PeerId, type: ChatUndoActionType, duration: Double = 5, action: @escaping(ChatUndoActionStatus) -> Void = { _ in}) { + self.peerId = peerId + self.type = type + self.action = action + self.duration = duration + self.endpoint = Date().timeIntervalSince1970 + duration + } + + + func withUpdatedEndpoint(_ endpoint: Double) -> ChatUndoAction { + return ChatUndoAction.init(peerId: self.peerId, type: self.type, duration: endpoint - Date().timeIntervalSince1970, action: self.action) + } + + func isEqual(with peerId: PeerId, type: ChatUndoActionType) -> Bool { + return self.peerId == peerId && self.type == type + } + + var hashValue: Int { + return Int(peerId.toInt64()) + } +} + +struct ChatUndoStatuses { + private let statuses: [ChatUndoAction : ChatUndoActionStatus] + fileprivate init(_ statuses: [ChatUndoAction : ChatUndoActionStatus]) { + self.statuses = statuses + } + + func contains(peerId: PeerId, type: ChatUndoActionType) -> Bool { + return statuses.first(where: { key, _ -> Bool in + return key.isEqual(with: peerId, type: type) + }) != nil + } + + func isActive(peerId: PeerId, types: [ChatUndoActionType]) -> Bool { + for type in types { + let result = statuses.first(where: { current -> Bool in + return current.key.isEqual(with: peerId, type: type) && (current.value == .processing || (current.value == .success)) + }) != nil + + if result { + return result + } + } + return false + } + + func status(for peerId: PeerId, type: ChatUndoActionType) -> ChatUndoActionStatus? { + return statuses.first(where: { key, _ -> Bool in + return key.isEqual(with: peerId, type: type) + })?.value + } + + var hasProcessingActions: Bool { + return !statuses.filter ({ _, value in + return value == .processing + }).isEmpty + } + + var maximumDuration: Double { + var max: Double = 0 + for (action, value) in statuses { + if value == .processing, max < action.duration { + max = action.duration + } + } + return max + } + + var actionsCount: Int { + return statuses.filter {$0.value == .processing}.count + } + + var endpoint: Double { + var max: Double = 0 + for (action, value) in statuses { + if value == .processing, max < action.duration { + max = action.endpoint + } + } + return max + } + var secondsUntilFinish: Double { + return endpoint - Date().timeIntervalSince1970 + } + + var activeDescription: String { + let clearingCount = statuses.filter {$0.key.type == .clearHistory && $0.value == .processing}.count + let deleteCount = statuses.filter {$0.key.type == .deleteChat && $0.value == .processing}.count + let deleteChannelCount = statuses.filter {$0.key.type == .deleteChannel && $0.value == .processing}.count + + let leftChatCount = statuses.filter {$0.key.type == .leftChat && $0.value == .processing}.count + let leftChannelCount = statuses.filter {$0.key.type == .leftChannel && $0.value == .processing}.count + let archiveChatCount = statuses.filter {$0.key.type == .archiveChat && $0.value == .processing}.count + + + var text: String = "" + + if archiveChatCount > 0 { + if !text.isEmpty { + text += ", " + } + text += L10n.chatUndoManagerChatsArchivedCountable(archiveChatCount) + } + + if leftChatCount > 0 { + if !text.isEmpty { + text += ", " + } + text += L10n.chatUndoManagerChatLeftCountable(leftChatCount) + } + + if leftChannelCount > 0 { + if !text.isEmpty { + text += ", " + } + text += L10n.chatUndoManagerChannelLeftCountable(leftChannelCount) + } + if deleteCount > 0 { + if !text.isEmpty { + text += ", " + } + text += L10n.chatUndoManagerChatsDeletedCountable(deleteCount) + } + if deleteChannelCount > 0 { + if !text.isEmpty { + text += ", " + } + text += L10n.chatUndoManagerChannelDeletedCountable(deleteChannelCount) + } + if clearingCount > 0 { + if !text.isEmpty { + text += ", " + } + text += L10n.chatUndoManagerChatsHistoryClearedCountable(clearingCount) + } + return text + } +} + +private final class ChatUndoManagerContext { + private let disposableDict: DisposableDict = DisposableDict() + private var actions: Set = Set() + private var statuses:[ChatUndoAction : ChatUndoActionStatusContext] = [:] + private let allSubscribers = Bag<(ChatUndoStatuses) -> Void>() + init() { + + } + + deinit { + disposableDict.dispose() + } + + private func restartProcessingActions(_ except: ChatUndoAction) { + self.actions = Set(self.actions.map { action in + if action == except { + return action + } else { + return action.withUpdatedEndpoint(except.endpoint) + } + }) + + for action in self.actions { + if except != action { + run(for: action) + } + } + } + + func add(action: ChatUndoAction) { + if let previous = actions.first(where: { $0 == action }) { + previous.action(.cancelled) + } + + actions.insert(action) + if statuses[action] == nil { + statuses[action] = ChatUndoActionStatusContext() + } + + statuses[action]?.status = .processing + + + restartProcessingActions(action) + notifyAllSubscribers() + run(for: action) + } + + private func run(for action: ChatUndoAction) { + disposableDict.set((Signal.complete() |> delay(action.endpoint - Date().timeIntervalSince1970, queue: queue)).start(completed: { [weak self] in + self?.statuses[action]?.status = .success + self?.notifyAllSubscribers() + action.action(.success) + }), forKey: action) + } + + + + func cancel(action: ChatUndoAction) { + actions.remove(action) + statuses[action]?.status = .cancelled + notifyAllSubscribers() + action.action(.cancelled) + disposableDict.set(nil, forKey: action) + } + + private func notifyAllSubscribers() { + var values:[ChatUndoAction : ChatUndoActionStatus] = [:] + + for action in self.actions { + if let status = self.statuses[action]?.status { + values[action] = status + } + } + for subscribers in allSubscribers.copyItems() { + subscribers(ChatUndoStatuses(values)) + } + } + + private func status(for peerId: PeerId, type: ChatUndoActionType) -> ChatUndoAction? { + return actions.first(where: {$0.isEqual(with: peerId, type: type)}) + } + + func status(for peerId: PeerId, type: ChatUndoActionType) -> Signal { + return Signal { [weak self] subscriber -> Disposable in + + let keyAction = ChatUndoAction(peerId: peerId, type: type, action: {_ in}) + + if self?.statuses[keyAction] == nil { + self?.statuses[keyAction] = ChatUndoActionStatusContext() + } + + let index = self?.statuses[keyAction]?.subscribers.add { status in + subscriber.putNext(status) + } + subscriber.putNext(self?.statuses[keyAction]?.status) + + return ActionDisposable { [weak self] in + if let index = index, let status = self?.statuses[keyAction] { + status.subscribers.remove(index) + if status.subscribers.copyItems().count == 0 { + self?.statuses.removeValue(forKey: keyAction) + } + } + } + } + } + + func cancelAll() { + for action in actions.reversed() { + if statuses[action] == nil { + disposableDict.set(nil, forKey: action) + statuses[action]?.status = .cancelled + action.action(.cancelled) + actions.remove(action) + } else if let status = statuses[action], status.status == .processing { + disposableDict.set(nil, forKey: action) + statuses[action]?.status = .cancelled + action.action(.cancelled) + actions.remove(action) + } + } + notifyAllSubscribers() + } + + func allStatuses() -> Signal { + return Signal { [weak self] subscriber -> Disposable in + + guard let `self` = self else { return EmptyDisposable } + + var values:[ChatUndoAction : ChatUndoActionStatus] = [:] + + for action in self.actions { + if let status = self.statuses[action]?.status { + values[action] = status + } + } + + let index = self.allSubscribers.add { statuses in + subscriber.putNext(statuses) + } + + subscriber.putNext(ChatUndoStatuses(values)) + + return ActionDisposable { [weak self] in + self?.allSubscribers.remove(index) + } + } + } + + fileprivate func finishAction(for peerId: PeerId, type: ChatUndoActionType) { + let keyAction = ChatUndoAction(peerId: peerId, type: type) + statuses[keyAction]?.status = nil + actions.remove(keyAction) + disposableDict.set(nil, forKey: keyAction) + notifyAllSubscribers() + } + fileprivate func invokeNow(for peerId: PeerId, type: ChatUndoActionType) { + let keyAction = ChatUndoAction(peerId: peerId, type: type) + if let action = actions.first(where: {$0 == keyAction}) { + statuses[keyAction]?.status = .success + action.action(.success) + self.actions.remove(action) + disposableDict.set(nil, forKey: keyAction) + notifyAllSubscribers() + } + } + + fileprivate func invokeAll() { + let actions = self.actions + for action in actions { + invokeNow(for: action.peerId, type: action.type) + } + } +} + + + +final class ChatUndoManager { + + private let context: ChatUndoManagerContext + init() { + context = ChatUndoManagerContext() + } + + func cancel(action: ChatUndoAction) { + queue.async { [weak context] in + context?.cancel(action: action) + } + } + + func cancelAll() -> Void { + queue.async { [weak context] in + context?.cancelAll() + } + } + + func add(action: ChatUndoAction) { + queue.async { [weak context] in + context?.add(action: action) + } + } + + func allStatuses() -> Signal { + var status: Signal = .complete() + queue.sync { + status = context.allStatuses() + } + return status + } + + func status(for peerId: PeerId, type: ChatUndoActionType) -> Signal { + var status: Signal = .complete() + queue.sync { + status = context.status(for: peerId, type: type) + } + return status + } + + func clearHistoryInteractively(engine: TelegramEngine, peerId: PeerId, type: InteractiveHistoryClearingType = .forLocalPeer) { + _ = engine.messages.clearHistoryInteractively(peerId: peerId, type: type).start() + } + func removePeerChat(engine: TelegramEngine, peerId: PeerId, type: ChatUndoActionType, reportChatSpam: Bool, deleteGloballyIfPossible: Bool = false) { + _ = engine.peers.removePeerChat(peerId: peerId, reportChatSpam: false, deleteGloballyIfPossible: deleteGloballyIfPossible).start() + } + + func invokeNow(for peerId: PeerId, type: ChatUndoActionType) { + queue.sync { [weak context] in + context?.invokeNow(for: peerId, type: .clearHistory) + } + } + + func invokeAll() { + queue.sync { [weak context] in + context?.invokeAll() + } + } +} + + + diff --git a/Telegram-Mac/ChatUnreadRowItem.swift b/Telegram-Mac/ChatUnreadRowItem.swift index 991ca659f7..b6ffa0a862 100644 --- a/Telegram-Mac/ChatUnreadRowItem.swift +++ b/Telegram-Mac/ChatUnreadRowItem.swift @@ -8,30 +8,34 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac +import TelegramCore + +import Postbox class ChatUnreadRowItem: ChatRowItem { override var height: CGFloat { - return 20 + return 32 } + override var canBeAnchor: Bool { + return false + } public var text:NSAttributedString; - override init(_ initialSize:NSSize, _ chatInteraction:ChatInteraction, _ account:Account, _ entry:ChatHistoryEntry) { + override init(_ initialSize:NSSize, _ chatInteraction:ChatInteraction, _ context: AccountContext, _ entry:ChatHistoryEntry, _ downloadSettings: AutomaticMediaDownloadSettings, theme: TelegramPresentationTheme) { let titleAttr:NSMutableAttributedString = NSMutableAttributedString() - let _ = titleAttr.append(string:tr(.messagesUnreadMark), color: theme.colors.grayText, font: .normal(.text)) + let _ = titleAttr.append(string:tr(L10n.messagesUnreadMark), color: theme.colors.grayText, font: .normal(.text)) text = titleAttr.copy() as! NSAttributedString - super.init(initialSize,chatInteraction,entry) + super.init(initialSize,chatInteraction,entry, downloadSettings, theme: theme) } override var messageIndex:MessageIndex? { switch entry { - case .UnreadEntry(let index): + case .UnreadEntry(let index, _, _): return index default: break @@ -39,8 +43,61 @@ class ChatUnreadRowItem: ChatRowItem { return super.messageIndex } + override var instantlyResize: Bool { + return true + } + override func viewClass() -> AnyClass { return ChatUnreadRowView.self } } + +private class ChatUnreadRowView: TableRowView { + + private var text:TextNode = TextNode() + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + self.layerContentsRedrawPolicy = .onSetNeedsDisplay + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func draw(_ dirtyRect: NSRect) { + + // Drawing code here. + } + + override func updateColors() { + layer?.backgroundColor = .clear + } + + + override func setFrameSize(_ newSize: NSSize) { + super.setFrameSize(newSize) + needsDisplay = true + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + + + ctx.setFillColor(theme.colors.grayBackground.cgColor) + ctx.fill(NSMakeRect(0, 6, frame.width, frame.height - 12)) + + if let item = self.item as? ChatUnreadRowItem { + let (layout, apply) = TextNode.layoutText(maybeNode: text, item.text, nil, 1, .end, NSMakeSize(NSWidth(self.frame), NSHeight(self.frame)), nil,false, .left) + apply.draw(NSMakeRect(round((NSWidth(layer.bounds) - layout.size.width)/2.0), round((NSHeight(layer.bounds) - layout.size.height)/2.0), layout.size.width, layout.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) + } + + } + + deinit { + var bp:Int = 0 + bp += 1 + } + +} diff --git a/Telegram-Mac/ChatUnreadRowView.swift b/Telegram-Mac/ChatUnreadRowView.swift index b40b9794cf..afdafb5b79 100644 --- a/Telegram-Mac/ChatUnreadRowView.swift +++ b/Telegram-Mac/ChatUnreadRowView.swift @@ -8,32 +8,4 @@ import Cocoa import TGUIKit -class ChatUnreadRowView: TableRowView { - - private var text:TextNode = TextNode() - override func draw(_ dirtyRect: NSRect) { - - // Drawing code here. - } - - - override func draw(_ layer: CALayer, in ctx: CGContext) { - ctx.setFillColor(theme.colors.grayBackground.cgColor) - - ctx.fill(self.bounds) - - - if let item = self.item as? ChatUnreadRowItem { - let (layout, apply) = TextNode.layoutText(maybeNode: text, item.text, nil, 1, .end, NSMakeSize(NSWidth(self.frame), NSHeight(self.frame)), nil,false, .left) - apply.draw(NSMakeRect(round((NSWidth(layer.bounds) - layout.size.width)/2.0), round((NSHeight(layer.bounds) - layout.size.height)/2.0), layout.size.width, layout.size.height), in: ctx, backingScaleFactor: backingScaleFactor) - } - - } - - deinit { - var bp:Int = 0 - bp += 1 - } - -} diff --git a/Telegram-Mac/ChatUrlPreviewModel.swift b/Telegram-Mac/ChatUrlPreviewModel.swift index 45bb366f38..b8e08e529a 100644 --- a/Telegram-Mac/ChatUrlPreviewModel.swift +++ b/Telegram-Mac/ChatUrlPreviewModel.swift @@ -8,8 +8,9 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac -import TelegramCoreMac +import SwiftSignalKit +import TelegramCore + class ChatUrlPreviewModel: ChatAccessoryModel { private let webpageDisposable = MetaDisposable() @@ -30,9 +31,10 @@ class ChatUrlPreviewModel: ChatAccessoryModel { private func updateWebpage() { var authorName = "" var text = "" + var isEmptyText: Bool = false switch self.webpage.content { case .Pending: - authorName = "Loading..." + authorName = L10n.chatInlineRequestLoading text = self.url case let .Loaded(content): if let title = content.websiteName { @@ -42,11 +44,14 @@ class ChatUrlPreviewModel: ChatAccessoryModel { } else { authorName = content.displayUrl } - text = content.text ?? content.title ?? "" + if content.text == nil && content.title == nil { + isEmptyText = true + } + text = content.text ?? content.title ?? L10n.chatEmptyLinkPreview } - self.headerAttr = .initialize(string: authorName, color: theme.colors.link, font: .medium(.text)) - self.messageAttr = .initialize(string: text, color: theme.colors.text, font: .normal(.text)) + self.headerAttr = .initialize(string: authorName, color: theme.colors.accent, font: .medium(.text)) + self.messageAttr = .initialize(string: text, color: isEmptyText ? theme.colors.grayText : theme.colors.text, font: .normal(.text)) nodeReady.set(.single(true)) self.setNeedDisplay() diff --git a/Telegram-Mac/ChatUserPopover.swift b/Telegram-Mac/ChatUserPopover.swift deleted file mode 100644 index 51f69fa911..0000000000 --- a/Telegram-Mac/ChatUserPopover.swift +++ /dev/null @@ -1,163 +0,0 @@ -// -// ChatUserPopover.swift -// Telegram -// -// Created by keepcoder on 5/6/17. -// Copyright © 2017 Telegram. All rights reserved. -// - -import Cocoa -import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac - - - - - -private class ChatUserPopoverView : View { - private let avatar:AvatarControl = AvatarControl(font: .avatar(.text)) - private let nameView:TextView = TextView() - private let lastSeen:TextView = TextView() - private let callButton: ImageButton = ImageButton() - private let messageButton:ImageButton = ImageButton() - private let infoButton:ImageButton = ImageButton() - private let buttonsContainer:View = View() - required init(frame frameRect: NSRect) { - super.init(frame: frameRect) - avatar.setFrameSize(NSMakeSize(40, 40)) - addSubview(avatar) - addSubview(nameView) - addSubview(lastSeen) - addSubview(buttonsContainer) - buttonsContainer.addSubview(callButton) - buttonsContainer.addSubview(messageButton) - buttonsContainer.addSubview(infoButton) - - - callButton.set(image: #imageLiteral(resourceName: "Icon_DetailedCall").precomposed(.blueUI), for: .Normal) - callButton.setFrameSize(NSMakeSize(26, 26)) - callButton.layer?.cornerRadius = 13 - callButton.layer?.borderColor = NSColor(0xe7e7ec).cgColor - callButton.layer?.borderWidth = 1 - callButton.set(background: NSColor(0xf8f8fe), for: .Normal) - - - messageButton.set(image: #imageLiteral(resourceName: "Icon_DetailedMessage").precomposed(.blueUI), for: .Normal) - messageButton.setFrameSize(NSMakeSize(26, 26)) - messageButton.layer?.cornerRadius = 13 - messageButton.layer?.borderColor = NSColor(0xe7e7ec).cgColor - messageButton.layer?.borderWidth = 1 - messageButton.set(background: NSColor(0xf8f8fe), for: .Normal) - - - infoButton.set(image: #imageLiteral(resourceName: "Icon_DetailedInfo").precomposed(.blueUI), for: .Normal) - infoButton.setFrameSize(NSMakeSize(26, 26)) - infoButton.layer?.cornerRadius = 13 - infoButton.layer?.borderColor = NSColor(0xe7e7ec).cgColor - infoButton.layer?.borderWidth = 1 - infoButton.set(background: NSColor(0xf8f8fe), for: .Normal) - - messageButton.setFrameOrigin(0, 0) - callButton.setFrameOrigin(messageButton.frame.maxX + 20, 0) - infoButton.setFrameOrigin(callButton.frame.maxX + 20, 0) - - buttonsContainer.setFrameSize(infoButton.frame.maxX, 26) - - nameView.userInteractionEnabled = false - lastSeen.userInteractionEnabled = false - } - - override func layout() { - super.layout() - avatar.setFrameOrigin(10, 10) - nameView.setFrameOrigin(avatar.frame.maxX + 10, 10 + 20 - nameView.frame.height - 2) - lastSeen.setFrameOrigin(avatar.frame.maxX + 10, 10 + 20 + 2) - - buttonsContainer.centerX(y: 55) - - - } - - func update(with peerView:PeerView, account:Account) { - if let peer = peerViewMainPeer(peerView) { - avatar.setPeer(account: account, peer: peer) - let result = stringStatus(for: peerView) - let statusLayout = TextViewLayout(result.status, maximumNumberOfLines: 1) - let titleLayout = TextViewLayout(result.title, maximumNumberOfLines: 1) - statusLayout.measure(width: frame.width - 80) - titleLayout.measure(width: frame.width - 80) - nameView.update(titleLayout) - lastSeen.update(statusLayout) - } - - needsLayout = true - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -private class ChatUserPopover: NSObject { - private let controller:NSViewController = NSViewController() - private let ready:Promise = Promise() - private let popover:NSPopover - private let account:Account - private let peerId:PeerId - private weak var parentView:NSView? - private let peerDisposable = MetaDisposable() - init(account:Account, peerId:PeerId, parentView:NSView) { - self.account = account - self.peerId = peerId - self.popover = NSPopover() - self.controller.view = ChatUserPopoverView(frame: NSMakeRect(0, 0, 200, 90)) - self.popover.contentViewController = controller - - self.popover.behavior = .transient - self.parentView = parentView - } - - private var view:ChatUserPopoverView { - return controller.view as! ChatUserPopoverView - } - - deinit { - peerDisposable.dispose() - mainWindow.removeAllHandlers(for: self) - } - - func show() { - if let parentView = parentView { - popover.show(relativeTo: NSMakeRect(0, 0, 200, 200), of: parentView, preferredEdge: .maxX) - peerDisposable.set((account.viewTracker.peerView(peerId) |> deliverOnMainQueue).start(next: { [weak self] peerView in - if let strongSelf = self { - strongSelf.view.update(with: peerView, account: strongSelf.account) - } - })) - } - } - - func close() { - popover.close() - } -} - -private var popover:ChatUserPopover? -func showDetailInfoPopover(forPeerId peerId:PeerId, account: Account, fromView:NSView) { - // popover = ChatUserPopover(account: account, peerId: peerId, parentView: fromView) - // popover?.show() - - -// -// mainWindow.set(handler: { () -> KeyHandlerResult in -// popover?.close() -// return .invoked -// }, with: popover!, for: .Escape, priority: .modal) -// mainWindow.set(handler: { () -> KeyHandlerResult in -// popover?.close() -// return .invoked -// }, with: popover!, for: .Space, priority: .modal) -} - diff --git a/Telegram-Mac/ChatVideoAccessoryView.swift b/Telegram-Mac/ChatVideoAccessoryView.swift deleted file mode 100644 index d082f37fa3..0000000000 --- a/Telegram-Mac/ChatVideoAccessoryView.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// ChatVideoAccessoryView.swift -// Telegram -// -// Created by keepcoder on 05/10/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - -import Cocoa -import TGUIKit - - -class ChatVideoAccessoryView: View { - - private var text:(TextNodeLayout, TextNode)? - private var textNode:TextNode? - - override func draw(_ layer: CALayer, in ctx: CGContext) { - - ctx.round(frame.size, frame.height / 2) - - ctx.setFillColor(NSColor.blackTransparent.cgColor) - ctx.fill(bounds) - - if let text = text { - text.1.draw(focus(text.0.size), in: ctx, backingScaleFactor: backingScaleFactor) - } - } - - func updateText(_ text: String, maxWidth: CGFloat) -> Void { - let updatedText = TextNode.layoutText(maybeNode: textNode, .initialize(string: text, color: .white, font: .normal(.custom(11))), nil, 1, .end, NSMakeSize(maxWidth, 20), nil, false, .left) - self.text = updatedText - setFrameSize(NSMakeSize(updatedText.0.size.width + 12, updatedText.0.size.height + 4)) - } - - required init(frame frameRect: NSRect) { - super.init(frame: frameRect) - - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - -} diff --git a/Telegram-Mac/ChatVideoMessageContentView.swift b/Telegram-Mac/ChatVideoMessageContentView.swift index 3271c8dabc..024746b0cc 100644 --- a/Telegram-Mac/ChatVideoMessageContentView.swift +++ b/Telegram-Mac/ChatVideoMessageContentView.swift @@ -8,17 +8,18 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit /* - func songDidStopPlaying(song:APSongItem, for controller:APController) { + func songDidStopPlaying(song:APSongItem, for controller:APController, animated: Bool) { if song.stableId == parent?.chatStableId { updatePlayerIfNeeded() } } - func playerDidChangedTimebase(song:APSongItem, for controller:APController) { + func playerDidChangedTimebase(song:APSongItem, for controller:APController, animated: Bool) { if song.stableId == parent?.chatStableId { if acceptVisibility && !player.isHasPath { player.set(path: path, timebase: controller.timebase) @@ -36,9 +37,35 @@ private let instantVideoMutedThumb = generateImage(NSMakeSize(30, 30), contextGe ctx.round(size, size.width / 2.0) ctx.fill(CGRect(origin: CGPoint(), size: size)) let icon = #imageLiteral(resourceName: "Icon_VideoMessageMutedIcon").precomposed() - ctx.draw(icon, in: NSMakeRect(floorToScreenPixels((size.width - icon.backingSize.width) / 2), floorToScreenPixels((size.height - icon.backingSize.height) / 2), icon.backingSize.width, icon.backingSize.height)) + ctx.draw(icon, in: NSMakeRect(floorToScreenPixels(System.backingScale, (size.width - icon.backingSize.width) / 2), floorToScreenPixels(System.backingScale, (size.height - icon.backingSize.height) / 2), icon.backingSize.width, icon.backingSize.height)) }) +final class VideoMessageCorner : View { + + override var backgroundColor: NSColor { + set { + super.backgroundColor = .clear + borderBackground = newValue + } + get { + return super.backgroundColor + } + } + private var borderBackground: NSColor = .black { + didSet { + needsLayout = true + } + } + override func draw(_ layer: CALayer, in ctx: CGContext) { + //ctx.round(frame.size, frame.size.height / 2) + ctx.setStrokeColor(theme.colors.background.cgColor) + ctx.setLineWidth(2.0) + ctx.setLineCap(.round) + + ctx.strokeEllipse(in: NSMakeRect(1, 1, bounds.width - 2, bounds.height - 2)) + } +} + class ChatVideoMessageContentView: ChatMediaContentView, APDelegate { @@ -49,10 +76,11 @@ class ChatVideoMessageContentView: ChatMediaContentView, APDelegate { private let statusDisposable = MetaDisposable() private let fetchDisposable = MetaDisposable() private let playerDisposable = MetaDisposable() + private let updateMouseDisposable = MetaDisposable() - private var durationView:TextView = TextView() - - private var path:String? { + private var durationView:ChatMessageAccessoryView = ChatMessageAccessoryView(frame: NSZeroRect) + private let videoCorner: VideoMessageCorner = VideoMessageCorner() + private var data:AVGifData? { didSet { updatePlayerIfNeeded() } @@ -61,12 +89,14 @@ class ChatVideoMessageContentView: ChatMediaContentView, APDelegate { required init(frame frameRect: NSRect) { super.init(frame: frameRect) addSubview(player) + videoCorner.userInteractionEnabled = false playingProgressView.userInteractionEnabled = false stateThumbView.image = instantVideoMutedThumb stateThumbView.sizeToFit() player.addSubview(stateThumbView) - addSubview(durationView) + player.addSubview(videoCorner) addSubview(playingProgressView) + addSubview(durationView) } required init?(coder: NSCoder) { @@ -75,18 +105,6 @@ class ChatVideoMessageContentView: ChatMediaContentView, APDelegate { override func draw(_ layer: CALayer, in ctx: CGContext) { super.draw(layer, in: ctx) - - if let parent = parent, let parameters = parameters as? ChatMediaVideoMessageLayoutParameters { - for attr in parent.attributes { - if let attr = attr as? ConsumableContentMessageAttribute { - if !attr.consumed { - ctx.setFillColor(theme.colors.blueUI.cgColor) - ctx.fillEllipse(in: NSMakeRect(parameters.durationLayout.layoutSize.width + 3, frame.height - floorToScreenPixels((durationView.frame.height - 5)/2) - 5, 5, 5)) - } - break - } - } - } } var isIncomingConsumed:Bool { @@ -109,6 +127,7 @@ class ChatVideoMessageContentView: ChatMediaContentView, APDelegate { removeNotificationListeners() } + override func cancel() { fetchDisposable.set(nil) statusDisposable.set(nil) @@ -116,74 +135,72 @@ class ChatVideoMessageContentView: ChatMediaContentView, APDelegate { private var singleWrapper:APSingleWrapper? { if let media = media as? TelegramMediaFile { - return APSingleWrapper(resource: media.resource, name: tr(.audioControllerVideoMessage), performer: parent?.author?.displayTitle, id: media.fileId) + return APSingleWrapper(resource: media.resource, mimeType: media.mimeType, name: L10n.audioControllerVideoMessage, performer: parent?.author?.displayTitle, duration: media.duration, id: media.fileId) } return nil } override func open() { - if let parent = parent, let account = account { + if let parent = parent, let context = context { if let parameters = parameters as? ChatMediaVideoMessageLayoutParameters { - if let controller = globalAudio, let song = controller.currentSong, song.entry.isEqual(to: parent) { - controller.playOrPause() + if let controller = globalAudio, controller.playOrPause(parent.id) { } else { let controller:APController if parameters.isWebpage, let wrapper = singleWrapper { - controller = APSingleResourceController(account: account, wrapper: wrapper) + controller = APSingleResourceController(context: context, wrapper: wrapper, streamable: false) } else { - controller = APChatVoiceController(account: account, peerId: parent.id.peerId, index: MessageIndex(parent)) + controller = APChatVoiceController(context: context, chatLocationInput: parameters.chatLocationInput(), mode: parameters.chatMode, index: MessageIndex(parent), volume: FastSettings.volumeRate) } parameters.showPlayer(controller) controller.start() - addGlobalAudioToVisible() } } } } - func songDidChanged(song: APSongItem, for controller: APController) { + func songDidChanged(song: APSongItem, for controller: APController, animated: Bool) { } - func songDidChangedState(song: APSongItem, for controller: APController) { - - + func songDidChangedState(song: APSongItem, for controller: APController, animated: Bool) { if let parent = parent, let controller = globalAudio, let song = controller.currentSong, let parameters = parameters as? ChatMediaVideoMessageLayoutParameters { - if song.entry.isEqual(to: parent) { - + var singleEqual: Bool = false + if let single = singleWrapper { + singleEqual = song.entry.isEqual(to: single) + } + if song.entry.isEqual(to: parent) || singleEqual { switch song.state { - case let .playing(data): - playingProgressView.state = .ImpossibleFetching(progress: Float(data.progress), force: false) - let layout = parameters.duration(for: data.current) - layout.measure(width: frame.width - 50) - durationView.update(layout) - break + case let .playing(current, _, progress): + playingProgressView.state = .ImpossibleFetching(progress: Float(progress), force: false) + durationView.updateText(String.durationTransformed(elapsed: Int(current)), maxWidth: 50, status: nil, isStreamable: false, isUnread: !isIncomingConsumed, animated: true, isVideoMessage: true) + stateThumbView.isHidden = true case .stoped, .waiting, .fetching: playingProgressView.state = .None - durationView.update(parameters.durationLayout) - case let .paused(data): - playingProgressView.state = .ImpossibleFetching(progress: Float(data.progress), force: true) - let layout = parameters.duration(for: data.current) - layout.measure(width: frame.width - 50) - durationView.update(layout) + durationView.updateText(String.durationTransformed(elapsed: parameters.duration), maxWidth: 50, status: nil, isStreamable: false, isUnread: !isIncomingConsumed, animated: true, isVideoMessage: true) + stateThumbView.isHidden = false + case let .paused(current, _, progress): + playingProgressView.state = .ImpossibleFetching(progress: Float(progress), force: true) + durationView.updateText(String.durationTransformed(elapsed: Int(current)), maxWidth: 50, status: nil, isStreamable: false, isUnread: !isIncomingConsumed, animated: true, isVideoMessage: true) + stateThumbView.isHidden = false } } else { playingProgressView.state = .None - durationView.update(parameters.durationLayout) + durationView.updateText(String.durationTransformed(elapsed: parameters.duration), maxWidth: 50, status: nil, isStreamable: false, isUnread: !isIncomingConsumed, animated: true, isVideoMessage: true) + stateThumbView.isHidden = false } } } - func songDidStartPlaying(song:APSongItem, for controller:APController) { + func songDidStartPlaying(song:APSongItem, for controller:APController, animated: Bool) { if song.stableId == parent?.chatStableId { stateThumbView.isHidden = true } else if let wrapper = singleWrapper, song.entry.isEqual(to: wrapper) { stateThumbView.isHidden = true } } - func songDidStopPlaying(song:APSongItem, for controller:APController) { + func songDidStopPlaying(song:APSongItem, for controller:APController, animated: Bool) { if song.stableId == parent?.chatStableId { player.reset(with: nil) stateThumbView.isHidden = false @@ -192,7 +209,7 @@ class ChatVideoMessageContentView: ChatMediaContentView, APDelegate { stateThumbView.isHidden = false } } - func playerDidChangedTimebase(song:APSongItem, for controller:APController) { + func playerDidChangedTimebase(song:APSongItem, for controller:APController, animated: Bool) { if song.stableId == parent?.chatStableId { player.reset(with: controller.timebase) } else if let wrapper = singleWrapper, song.entry.isEqual(to: wrapper) { @@ -201,31 +218,32 @@ class ChatVideoMessageContentView: ChatMediaContentView, APDelegate { } - func audioDidCompleteQueue(for controller:APController) { - + func audioDidCompleteQueue(for controller:APController, animated: Bool) { + if let parameters = parameters as? ChatMediaVideoMessageLayoutParameters { + playingProgressView.state = .None + durationView.updateText(String.durationTransformed(elapsed: parameters.duration), maxWidth: 50, status: nil, isStreamable: false, isUnread: !isIncomingConsumed, animated: true, isVideoMessage: true) + stateThumbView.isHidden = false + } } - func checkState() { + func checkState(animated: Bool) { } - override func cancelFetching() { - if let account = account, let media = media as? TelegramMediaFile { - chatMessageFileCancelInteractiveFetch(account: account, file: media) - } - } + override func fetch() { - if let account = account, let media = media as? TelegramMediaFile { - fetchDisposable.set(chatMessageFileInteractiveFetched(account: account, file: media).start()) + if let context = context, let media = media as? TelegramMediaFile, let parent = parent { + fetchDisposable.set(messageMediaFileInteractiveFetched(context: context, messageId: parent.id, fileReference: FileMediaReference.message(message: MessageReference(parent), media: media)).start()) } } override func layout() { super.layout() player.frame = bounds - playingProgressView.frame = bounds + videoCorner.frame = NSMakeRect(bounds.minX - 0.5, bounds.minY - 0.5, bounds.width + 1.0, bounds.height + 1.0) + playingProgressView.frame = NSMakeRect(1.5, 1.5, bounds.width - 3, bounds.height - 3) progressView?.center() stateThumbView.centerX(y: 10) durationView.setFrameOrigin(0, frame.height - durationView.frame.height) @@ -239,12 +257,18 @@ class ChatVideoMessageContentView: ChatMediaContentView, APDelegate { var acceptVisibility:Bool { - return window != nil && !NSIsEmptyRect(visibleRect) + return window != nil && window!.isKeyWindow && !NSIsEmptyRect(visibleRect) && !isDynamicContentLocked + } + + override func viewDidUpdatedDynamicContent() { + super.viewDidUpdatedDynamicContent() + updatePlayerIfNeeded() } @objc func updatePlayerIfNeeded() { let timebase:CMTimebase? = globalAudio?.currentSong?.stableId == parent?.chatStableId ? globalAudio?.timebase : nil - player.set(path: acceptVisibility ? path : nil, timebase: timebase) + player.set(data: acceptVisibility ? data : nil, timebase: timebase) + } func updateListeners() { @@ -252,6 +276,8 @@ class ChatVideoMessageContentView: ChatMediaContentView, APDelegate { NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSWindow.didBecomeKeyNotification, object: window) NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSWindow.didResignKeyNotification, object: window) NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSView.boundsDidChangeNotification, object: table?.clipView) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSView.frameDidChangeNotification, object: table?.view) + } else { removeNotificationListeners() } @@ -263,65 +289,84 @@ class ChatVideoMessageContentView: ChatMediaContentView, APDelegate { } deinit { - player.set(path: nil) + player.set(data: nil) + updateMouseDisposable.dispose() + } - override func update(with media: Media, size: NSSize, account: Account, parent: Message?, table: TableView?, parameters:ChatMediaLayoutParameters? = nil, animated: Bool = false) { - let mediaUpdated = self.media == nil || !self.media!.isEqual(media) - - - super.update(with: media, size: size, account: account, parent:parent,table:table, parameters:parameters, animated: animated) + override func update(with media: Media, size: NSSize, context: AccountContext, parent: Message?, table: TableView?, parameters:ChatMediaLayoutParameters? = nil, animated: Bool = false, positionFlags: LayoutPositionFlags? = nil, approximateSynchronousValue: Bool = false) { + let mediaUpdated = self.media == nil || !self.media!.isSemanticallyEqual(to: media) + super.update(with: media, size: size, context: context, parent:parent,table:table, parameters:parameters, animated: animated, positionFlags: positionFlags) + updateListeners() if let media = media as? TelegramMediaFile { if let parameters = parameters as? ChatMediaVideoMessageLayoutParameters { - durationView.update(parameters.durationLayout) + durationView.updateText(String.durationTransformed(elapsed: parameters.duration), maxWidth: 50, status: nil, isStreamable: false, isUnread: !isIncomingConsumed, animated: animated, isVideoMessage: true) } + if mediaUpdated { - player.layer?.cornerRadius = size.height / 2 - path = nil + globalAudio?.add(listener: self) - let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: media.previewRepresentations) + player.layer?.cornerRadius = size.height / 2 + data = nil var updatedStatusSignal: Signal? - - player.setSignal(account: account, signal: chatMessagePhoto(account: account, photo: image, scale: backingScaleFactor)) let arguments = TransformImageArguments(corners: ImageCorners(radius:size.width/2), imageSize: size, boundingSize: size, intrinsicInsets: NSEdgeInsets()) + + player.setSignal(signal: cachedMedia(media: media, arguments: arguments, scale: backingScaleFactor), clearInstantly: mediaUpdated) + + player.setSignal(chatMessageVideo(postbox: context.account.postbox, fileReference: parent != nil ? FileMediaReference.message(message: MessageReference(parent!), media: media) : FileMediaReference.standalone(media: media), scale: backingScaleFactor), cacheImage: { [weak media] result in + if let media = media { + cacheMedia(result, media: media, arguments: arguments, scale: System.backingScale) + } + }) + + player.set(arguments: arguments) if let parent = parent, parent.flags.contains(.Unsent) && !parent.flags.contains(.Failed) { - updatedStatusSignal = combineLatest(chatMessageFileStatus(account: account, file: media), account.pendingMessageManager.pendingMessageStatus(parent.id)) + updatedStatusSignal = combineLatest(chatMessageFileStatus(account: context.account, file: media), context.account.pendingMessageManager.pendingMessageStatus(parent.id)) |> map { resourceStatus, pendingStatus -> MediaResourceStatus in - if let pendingStatus = pendingStatus { + if let pendingStatus = pendingStatus.0 { return .Fetching(isActive: true, progress: pendingStatus.progress) } else { return resourceStatus } } |> deliverOnMainQueue } else { - updatedStatusSignal = chatMessageFileStatus(account: account, file: media) + updatedStatusSignal = chatMessageFileStatus(account: context.account, file: media, approximateSynchronousValue: approximateSynchronousValue) } if let updatedStatusSignal = updatedStatusSignal { - self.statusDisposable.set((combineLatest(updatedStatusSignal, account.postbox.mediaBox.resourceData(media.resource)) |> deliverOnMainQueue).start(next: { [weak self] (status,resource) in + self.statusDisposable.set((combineLatest(updatedStatusSignal, context.account.postbox.mediaBox.resourceData(media.resource)) |> deliverOnResourceQueue |> map { status, resource -> (MediaResourceStatus, AVGifData?) in + if resource.complete { + return (status, AVGifData.dataFrom(resource.path)) + } else if status == .Local, let resource = media.resource as? LocalFileReferenceMediaResource { + return (status, AVGifData.dataFrom(resource.localFilePath)) + } else { + return (status, nil) + } + } |> deliverOnMainQueue).start(next: { [weak self] status,data in if let strongSelf = self { - if resource.complete { - strongSelf.path = resource.path - } else { - strongSelf.path = nil - } + strongSelf.data = data strongSelf.fetchStatus = status if case .Local = status { if let progressView = strongSelf.progressView { - progressView.removeFromSuperview() + progressView.state = .Fetching(progress: 1.0, force: false) strongSelf.progressView = nil + progressView.layer?.animateAlpha(from: 1, to: 0, duration: 0.25, timingFunction: .linear, removeOnCompletion: false, completion: { [weak progressView] completed in + if completed { + progressView?.removeFromSuperview() + } + }) } } else { @@ -346,14 +391,16 @@ class ChatVideoMessageContentView: ChatMediaContentView, APDelegate { } })) } - - fetchDisposable.set(chatMessageFileInteractiveFetched(account: account, file: media).start()) - + } } } + override var contents: Any? { + return player.layer?.contents + } + override func copy() -> Any { let view = View() view.backgroundColor = .clear diff --git a/Telegram-Mac/ChatVideoMessageItem.swift b/Telegram-Mac/ChatVideoMessageItem.swift index 2d784030ae..478cdf94f3 100644 --- a/Telegram-Mac/ChatVideoMessageItem.swift +++ b/Telegram-Mac/ChatVideoMessageItem.swift @@ -8,8 +8,9 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac +import TelegramCore + +import Postbox class ChatMediaVideoMessageLayoutParameters : ChatMediaLayoutParameters { @@ -19,13 +20,14 @@ class ChatMediaVideoMessageLayoutParameters : ChatMediaLayoutParameters { let isMarked:Bool let duration:Int let durationLayout:TextViewLayout - init(showPlayer:@escaping(APController) -> Void, duration:Int, isMarked:Bool, isWebpage: Bool, resource: TelegramMediaResource) { + init(showPlayer:@escaping(APController) -> Void, duration:Int, isMarked:Bool, isWebpage: Bool, resource: TelegramMediaResource, presentation: ChatMediaPresentation, media: Media, automaticDownload: Bool, autoplayMedia: AutoplayMediaPreferences) { self.showPlayer = showPlayer self.duration = duration self.isMarked = isMarked self.isWebpage = isWebpage self.resource = resource self.durationLayout = TextViewLayout(NSAttributedString.initialize(string: String.durationTransformed(elapsed: duration), color: theme.colors.grayText, font: .normal(.text)), maximumNumberOfLines: 1, truncationType:.end, alignment: .left) + super.init(presentation: presentation, media: media, automaticDownload: automaticDownload, autoplayMedia: autoplayMedia) } func duration(for duration:TimeInterval) -> TextViewLayout { @@ -35,11 +37,15 @@ class ChatMediaVideoMessageLayoutParameters : ChatMediaLayoutParameters { class ChatVideoMessageItem: ChatMediaItem { - override init(_ initialSize:NSSize, _ chatInteraction:ChatInteraction, _ account: Account, _ object: ChatHistoryEntry) { - super.init(initialSize, chatInteraction, account, object) + override init(_ initialSize:NSSize, _ chatInteraction:ChatInteraction, _ context: AccountContext, _ object: ChatHistoryEntry, _ downloadSettings: AutomaticMediaDownloadSettings, theme: TelegramPresentationTheme) { + super.init(initialSize, chatInteraction, context, object, downloadSettings, theme: theme) - self.parameters = ChatMediaLayoutParameters.layout(for: media as! TelegramMediaFile, isWebpage: false, chatInteraction: chatInteraction) + self.parameters = ChatMediaLayoutParameters.layout(for: media as! TelegramMediaFile, isWebpage: false, chatInteraction: chatInteraction, presentation: .make(for: object.message!, account: context.account, renderType: object.renderType, theme: theme), automaticDownload: downloadSettings.isDownloable(object.message!), isIncoming: object.message!.isIncoming(context.account, object.renderType == .bubble), autoplayMedia: object.autoplayMedia) + } + + override var instantlyResize: Bool { + return true } override func makeContentSize(_ width: CGFloat) -> NSSize { diff --git a/Telegram-Mac/ChatVoiceContentView.swift b/Telegram-Mac/ChatVoiceContentView.swift index e9e28589f8..2ee5b1151e 100644 --- a/Telegram-Mac/ChatVoiceContentView.swift +++ b/Telegram-Mac/ChatVoiceContentView.swift @@ -7,10 +7,11 @@ // import Cocoa -import PostboxMac -import TelegramCoreMac +import Postbox +import TelegramCore + import TGUIKit -import SwiftSignalKitMac +import SwiftSignalKit class ChatVoiceContentView: ChatAudioContentView { @@ -29,6 +30,11 @@ class ChatVoiceContentView: ChatAudioContentView { } let waveformView:AudioWaveformView + private var acceptDragging: Bool = false + private var playAfterDragging: Bool = false + + private var downloadingView: RadialProgressView? + required init(frame frameRect: NSRect) { waveformView = AudioWaveformView(frame: NSMakeRect(0, 20, 100, 20)) super.init(frame: frameRect) @@ -41,63 +47,64 @@ class ChatVoiceContentView: ChatAudioContentView { } override func open() { - if let parameters = parameters as? ChatMediaVoiceLayoutParameters, let account = account, let parent = parent { - if let controller = globalAudio, let song = controller.currentSong, song.entry.isEqual(to: parent) { - controller.playOrPause() + if let parameters = parameters as? ChatMediaVoiceLayoutParameters, let context = context, let parent = parent { + if let controller = globalAudio, controller.playOrPause(parent.id) { } else { - let controller:APController if parameters.isWebpage { - controller = APSingleResourceController(account: account, wrapper: APSingleWrapper(resource: parameters.resource, name: tr(.audioControllerVoiceMessage), performer: parent.author?.displayTitle, id: parent.chatStableId)) + controller = APSingleResourceController(context: context, wrapper: APSingleWrapper(resource: parameters.resource, name: L10n.audioControllerVoiceMessage, performer: parent.author?.displayTitle, duration: Int32(parameters.duration), id: parent.chatStableId), streamable: false, volume: FastSettings.volumeRate) } else { - controller = APChatVoiceController(account: account, peerId: parent.id.peerId, index: MessageIndex(parent)) + controller = APChatVoiceController(context: context, chatLocationInput: parameters.chatLocationInput(), mode: parameters.chatMode, index: MessageIndex(parent), volume: FastSettings.volumeRate) } parameters.showPlayer(controller) controller.start() - addGlobalAudioToVisible() } } } var wBackgroundColor:NSColor { + if let parameters = parameters { + return parameters.presentation.waveformBackground + } return theme.colors.grayIcon.withAlphaComponent(0.7) } var wForegroundColor:NSColor { - return theme.colors.blueFill + if let parameters = parameters { + return parameters.presentation.waveformForeground + } + return theme.colors.accent } - override func checkState() { - super.checkState() + override func checkState(animated: Bool) { + super.checkState(animated: animated) if let parameters = parameters as? ChatMediaVoiceLayoutParameters { if let parent = parent, let controller = globalAudio, let song = controller.currentSong { if song.entry.isEqual(to: parent) { - - switch song.state { - case let .playing(data): + case let .playing(current, _, progress): waveformView.set(foregroundColor: wForegroundColor, backgroundColor: wBackgroundColor) - let width = floorToScreenPixels(parameters.waveformWidth * CGFloat(data.progress)) - waveformView.foregroundClipingView.change(size: NSMakeSize(width, waveformView.frame.height), animated: data.animated) - let layout = parameters.duration(for: data.current) + let width = floorToScreenPixels(backingScaleFactor, parameters.waveformWidth * CGFloat(progress)) + waveformView.foregroundClipingView.change(size: NSMakeSize(width, waveformView.frame.height), animated: animated && !acceptDragging) + let layout = parameters.duration(for: current) layout.measure(width: frame.width - 50) durationView.update(layout) break - case let .fetching(progress, animated): + case let .fetching(progress): waveformView.set(foregroundColor: wForegroundColor, backgroundColor: wBackgroundColor) - let width = floorToScreenPixels(parameters.waveformWidth * CGFloat(progress)) - waveformView.foregroundClipingView.change(size: NSMakeSize(width, waveformView.frame.height), animated: animated) + let width = floorToScreenPixels(backingScaleFactor, parameters.waveformWidth * CGFloat(progress)) + waveformView.foregroundClipingView.change(size: NSMakeSize(width, waveformView.frame.height), animated: animated && !acceptDragging) durationView.update(parameters.durationLayout) case .stoped, .waiting: waveformView.set(foregroundColor: isIncomingConsumed ? wBackgroundColor : wForegroundColor, backgroundColor: wBackgroundColor) waveformView.foregroundClipingView.change(size: NSMakeSize(parameters.waveformWidth, waveformView.frame.height), animated: false) durationView.update(parameters.durationLayout) - case let .paused(data): + case let .paused(current, _, progress): waveformView.set(foregroundColor: wForegroundColor, backgroundColor: wBackgroundColor) - let width = floorToScreenPixels(parameters.waveformWidth * CGFloat(data.progress)) - waveformView.foregroundClipingView.change(size: NSMakeSize(width, waveformView.frame.height), animated: data.animated) - let layout = parameters.duration(for: data.current) + let width = floorToScreenPixels(backingScaleFactor, parameters.waveformWidth * CGFloat(progress)) + waveformView.foregroundClipingView.change(size: NSMakeSize(width, waveformView.frame.height), animated: animated && !acceptDragging) + let layout = parameters.duration(for: current) layout.measure(width: frame.width - 50) durationView.update(layout) } @@ -117,20 +124,117 @@ class ChatVoiceContentView: ChatAudioContentView { } - override func update(with media: Media, size: NSSize, account: Account, parent: Message?, table: TableView?, parameters: ChatMediaLayoutParameters?, animated: Bool = false) { - super.update(with: media, size: size, account: account, parent: parent, table: table, parameters: parameters, animated: animated) + override func mouseDragged(with event: NSEvent) { + super.mouseDragged(with: event) + + if acceptDragging, let parent = parent, let controller = globalAudio, let song = controller.currentSong { + if song.entry.isEqual(to: parent) { + let point = waveformView.convert(event.locationInWindow, from: nil) + let progress = Float(min(max(point.x, 0), waveformView.frame.width)/waveformView.frame.width) + switch song.state { + case .playing: + _ = controller.pause() + playAfterDragging = true + default: + break + } + controller.set(trackProgress: progress) + } else { + super.mouseDragged(with: event) + } + } + } + + override func mouseDown(with event: NSEvent) { + acceptDragging = waveformView.mouseInside() + if !acceptDragging { + super.mouseDown(with: event) + } + } + + override func mouseUp(with event: NSEvent) { + super.mouseUp(with: event) + if acceptDragging && playAfterDragging { + _ = globalAudio?.play() + } + playAfterDragging = false + acceptDragging = false + } + + override func update(with media: Media, size: NSSize, context: AccountContext, parent: Message?, table: TableView?, parameters: ChatMediaLayoutParameters?, animated: Bool = false, positionFlags: LayoutPositionFlags? = nil, approximateSynchronousValue: Bool = false) { + super.update(with: media, size: size, context: context, parent: parent, table: table, parameters: parameters, animated: animated, positionFlags: positionFlags) + + + var updatedStatusSignal: Signal + + let file:TelegramMediaFile = media as! TelegramMediaFile + + // self.progressView.state = .None + + if let parent = parent, parent.flags.contains(.Unsent) && !parent.flags.contains(.Failed) { + updatedStatusSignal = combineLatest(chatMessageFileStatus(account: context.account, file: file), context.account.pendingMessageManager.pendingMessageStatus(parent.id)) + |> map { resourceStatus, pendingStatus -> MediaResourceStatus in + if let pendingStatus = pendingStatus.0 { + return .Fetching(isActive: true, progress: pendingStatus.progress) + } else { + return resourceStatus + } + } |> deliverOnMainQueue + } else { + updatedStatusSignal = chatMessageFileStatus(account: context.account, file: file, approximateSynchronousValue: approximateSynchronousValue) |> deliverOnMainQueue + } + + self.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak self] status in + if let strongSelf = self { + strongSelf.fetchStatus = status + + var state: RadialProgressState? = nil + switch status { + case let .Fetching(_, progress): + state = .Fetching(progress: progress, force: false) + strongSelf.progressView.state = .Fetching(progress: progress, force: false) + case .Remote: + state = .Remote + strongSelf.progressView.state = .Remote + case .Local: + strongSelf.progressView.state = .Play + } + if let state = state { + let current: RadialProgressView + if let value = strongSelf.downloadingView { + current = value + } else { + current = RadialProgressView(theme: strongSelf.progressView.theme, twist: true, size: NSMakeSize(40, 40)) + current.fetchControls = strongSelf.fetchControls + strongSelf.downloadingView = current + strongSelf.addSubview(current) + current.frame = strongSelf.progressView.frame + + if !approximateSynchronousValue && animated { + current.layer?.animateAlpha(from: 0.2, to: 1, duration: 0.3) + } + } + current.state = state + } else if let download = strongSelf.downloadingView { + download.state = .Fetching(progress: 1.0, force: false) + strongSelf.downloadingView = nil + download.layer?.animateAlpha(from: 1, to: 0.2, duration: 0.25, removeOnCompletion: false, completion: { [weak download] _ in + download?.removeFromSuperview() + }) + } + } + })) if let parameters = parameters as? ChatMediaVoiceLayoutParameters { waveformView.waveform = parameters.waveform waveformView.set(foregroundColor: isIncomingConsumed ? wBackgroundColor : wForegroundColor, backgroundColor: wBackgroundColor) - checkState() + checkState(animated: animated) } + + needsLayout = true - if let media = media as? TelegramMediaFile { - fetchDisposable.set(chatMessageFileInteractiveFetched(account: account, file: media).start()) - } } override func draw(_ layer: CALayer, in ctx: CGContext) { @@ -140,8 +244,8 @@ class ChatVoiceContentView: ChatAudioContentView { for attr in parent.attributes { if let attr = attr as? ConsumableContentMessageAttribute { if !attr.consumed { - let center = floorToScreenPixels(frame.height / 2.0) - ctx.setFillColor(theme.colors.blueUI.cgColor) + let center = floorToScreenPixels(backingScaleFactor, frame.height / 2.0) + ctx.setFillColor(parameters.presentation.activityBackground.cgColor) ctx.fillEllipse(in: NSMakeRect(leftInset + parameters.durationLayout.layoutSize.width + 3, center + 8, 5, 5)) } break @@ -153,7 +257,7 @@ class ChatVoiceContentView: ChatAudioContentView { override func layout() { super.layout() - let center = floorToScreenPixels(frame.height / 2.0) + let center = floorToScreenPixels(backingScaleFactor, frame.height / 2.0) if let parameters = parameters as? ChatMediaVoiceLayoutParameters { waveformView.setFrameSize(parameters.waveformWidth, waveformView.frame.height) } diff --git a/Telegram-Mac/ChatVoiceRowItem.swift b/Telegram-Mac/ChatVoiceRowItem.swift index 4383c69abe..442f4b6361 100644 --- a/Telegram-Mac/ChatVoiceRowItem.swift +++ b/Telegram-Mac/ChatVoiceRowItem.swift @@ -9,9 +9,10 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac +import TelegramCore + +import SwiftSignalKit +import Postbox class ChatMediaVoiceLayoutParameters : ChatMediaLayoutParameters { let showPlayer:(APController) -> Void let waveform:AudioWaveform? @@ -21,29 +22,45 @@ class ChatMediaVoiceLayoutParameters : ChatMediaLayoutParameters { let resource: TelegramMediaResource fileprivate(set) var waveformWidth:CGFloat = 120 let duration:Int - init(showPlayer:@escaping(APController) -> Void, waveform:AudioWaveform?, duration:Int, isMarked:Bool, isWebpage: Bool, resource: TelegramMediaResource) { + init(showPlayer:@escaping(APController) -> Void, waveform:AudioWaveform?, duration:Int, isMarked:Bool, isWebpage: Bool, resource: TelegramMediaResource, presentation: ChatMediaPresentation, media: Media, automaticDownload: Bool) { self.showPlayer = showPlayer self.waveform = waveform self.duration = duration self.isMarked = isMarked self.isWebpage = isWebpage self.resource = resource - durationLayout = TextViewLayout(NSAttributedString.initialize(string: String.durationTransformed(elapsed: duration), color: theme.colors.grayText, font: .normal(.text)), maximumNumberOfLines: 1, truncationType:.end, alignment: .left) - - + durationLayout = TextViewLayout(NSAttributedString.initialize(string: String.durationTransformed(elapsed: duration), color: presentation.grayText, font: .normal(.text)), maximumNumberOfLines: 1, truncationType:.end, alignment: .left) + super.init(presentation: presentation, media: media, automaticDownload: automaticDownload, autoplayMedia: AutoplayMediaPreferences.defaultSettings) } func duration(for duration:TimeInterval) -> TextViewLayout { - return TextViewLayout(NSAttributedString.initialize(string: String.durationTransformed(elapsed: Int(duration)), color: theme.colors.grayText, font: .normal(.text)), maximumNumberOfLines: 1, truncationType:.end, alignment: .left) + return TextViewLayout(NSAttributedString.initialize(string: String.durationTransformed(elapsed: Int(round(duration))), color: presentation.grayText, font: .normal(.text)), maximumNumberOfLines: 1, truncationType:.end, alignment: .left) } } class ChatVoiceRowItem: ChatMediaItem { - override init(_ initialSize:NSSize, _ chatInteraction:ChatInteraction, _ account: Account, _ object: ChatHistoryEntry) { - super.init(initialSize, chatInteraction, account, object) + override init(_ initialSize:NSSize, _ chatInteraction:ChatInteraction, _ context: AccountContext, _ object: ChatHistoryEntry, _ downloadSettings: AutomaticMediaDownloadSettings, theme: TelegramPresentationTheme) { + super.init(initialSize, chatInteraction, context, object, downloadSettings, theme: theme) - self.parameters = ChatMediaLayoutParameters.layout(for: media as! TelegramMediaFile, isWebpage: false, chatInteraction: chatInteraction) + self.parameters = ChatMediaLayoutParameters.layout(for: media as! TelegramMediaFile, isWebpage: false, chatInteraction: chatInteraction, presentation: .make(for: object.message!, account: context.account, renderType: object.renderType, theme: theme), automaticDownload: downloadSettings.isDownloable(object.message!), isIncoming: object.message!.isIncoming(context.account, object.renderType == .bubble), autoplayMedia: object.autoplayMedia) + } + + override func canMultiselectTextIn(_ location: NSPoint) -> Bool { + return super.canMultiselectTextIn(location) + } + + override var additionalLineForDateInBubbleState: CGFloat? { + if isForceRightLine { + return rightSize.height + } + if let parameters = parameters as? ChatMediaVoiceLayoutParameters { + if parameters.durationLayout.layoutSize.width + 50 + rightSize.width + insetBetweenContentAndDate > contentSize.width { + return rightSize.height + } + } + + return super.additionalLineForDateInBubbleState } override func makeContentSize(_ width: CGFloat) -> NSSize { @@ -61,8 +78,12 @@ class ChatVoiceRowItem: ChatMediaItem { parameters.waveformWidth = floor(min(w, 200)) - return NSMakeSize(parameters.waveformWidth + 60, 40) + return NSMakeSize(parameters.waveformWidth + 50, 40) } return NSZeroSize } + + override var instantlyResize: Bool { + return true + } } diff --git a/Telegram-Mac/ChatWallpaperModalController.swift b/Telegram-Mac/ChatWallpaperModalController.swift new file mode 100644 index 0000000000..a03224a4d6 --- /dev/null +++ b/Telegram-Mac/ChatWallpaperModalController.swift @@ -0,0 +1,341 @@ +// +// ChatBackgroundModalController.swift +// Telegram +// +// Created by Mikhail Filimonov on 11/01/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import Postbox +import TGUIKit +import SwiftSignalKit + + +final class ThemeGridControllerInteraction { + let openWallpaper: (Wallpaper, TelegramWallpaper?) -> Void + let deleteWallpaper: (Wallpaper, TelegramWallpaper) -> Void + init(openWallpaper: @escaping (Wallpaper, TelegramWallpaper?) -> Void, deleteWallpaper: @escaping (Wallpaper, TelegramWallpaper) -> Void) { + self.openWallpaper = openWallpaper + self.deleteWallpaper = deleteWallpaper + } +} + +private struct ThemeGridControllerEntry: Comparable, Identifiable { + let index: Int + let wallpaper: Wallpaper + let telegramWallapper: TelegramWallpaper? + let selected: Bool + + + static func <(lhs: ThemeGridControllerEntry, rhs: ThemeGridControllerEntry) -> Bool { + return lhs.index < rhs.index + } + + var stableId: Int { + return self.index + } + + func item(account: Account, interaction: ThemeGridControllerInteraction) -> ThemeGridControllerItem { + return ThemeGridControllerItem(account: account, wallpaper: self.wallpaper, telegramWallpaper: self.telegramWallapper, interaction: interaction, isSelected: selected) + } +} + +private struct ThemeGridEntryTransition { + let deletions: [Int] + let insertions: [GridNodeInsertItem] + let updates: [GridNodeUpdateItem] + let updateFirstIndexInSectionOffset: Int? + let stationaryItems: GridNodeStationaryItems + let scrollToItem: GridNodeScrollToItem? +} + +private func preparedThemeGridEntryTransition(context: AccountContext, from fromEntries: [ThemeGridControllerEntry], to toEntries: [ThemeGridControllerEntry], interaction: ThemeGridControllerInteraction) -> ThemeGridEntryTransition { + let stationaryItems: GridNodeStationaryItems = .none + let scrollToItem: GridNodeScrollToItem? = nil + + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) + + let deletions = deleteIndices + let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: context.account, interaction: interaction), previousIndex: $0.2) } + let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: context.account, interaction: interaction)) } + + return ThemeGridEntryTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: nil, stationaryItems: stationaryItems, scrollToItem: scrollToItem) +} + +private final class ChatWallpaperView : View { + fileprivate let gridNode: GridNode = GridNode() + fileprivate let header:TextView = TextView() + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(gridNode) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + gridNode.frame = NSMakeRect(0, 10, frame.width, frame.height - 10) + } +} + +class ChatWallpaperModalController: ModalViewController { + private let context: AccountContext + + + override func viewClass() -> AnyClass { + return ChatWallpaperView.self + } + + private var genericView: ChatWallpaperView { + return self.view as! ChatWallpaperView + } + + var gridNode: GridNode { + return genericView.gridNode + } + + private var queuedTransitions: [ThemeGridEntryTransition] = [] + private var disposable: Disposable? + + init(_ context: AccountContext) { + self.context = context + + super.init(frame: NSMakeRect(0, 0, 380, 400)) + } + + override var modalInteractions: ModalInteractions? { + let context = self.context + let interactions = ModalInteractions(acceptTitle: L10n.chatWPSelectFromFile, accept: { + filePanel(with: photoExts, allowMultiple: false, for: mainWindow, completion: { paths in + if let path = paths?.first { + let size = fs(path) + if let size = size, size < 10 * 1024 * 1024, let image = NSImage(contentsOf: URL(fileURLWithPath: path))?.cgImage(forProposedRect: nil, context: nil, hints: nil), image.size.width > 500 && image.size.height > 500 { + + let options = NSMutableDictionary() + options.setValue(90 as NSNumber, forKey: kCGImageDestinationImageMaxPixelSize as String) + var representations: [TelegramMediaImageRepresentation] = [] + let colorQuality: Float = 0.1 + options.setObject(colorQuality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString) + let mutableData: CFMutableData = NSMutableData() as CFMutableData + + if let colorDestination = CGImageDestinationCreateWithData(mutableData, kUTTypeJPEG, 1, nil) { + CGImageDestinationAddImage(colorDestination, image, options as CFDictionary) + if CGImageDestinationFinalize(colorDestination) { + let thumdResource = LocalFileMediaResource(fileId: arc4random64()) + context.account.postbox.mediaBox.storeResourceData(thumdResource.id, data: mutableData as Data) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(image.size.aspectFitted(NSMakeSize(90, 90))), resource: thumdResource, progressiveSizes: [], immediateThumbnailData: nil)) + } + } + + let resource = LocalFileReferenceMediaResource(localFilePath: path, randomId: arc4random64()) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(image.size), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) + + showModal(with: WallpaperPreviewController(context, wallpaper: .image(representations, settings: WallpaperSettings()), source: .none), for: context.window) + + } else { + alert(for: context.window, header: appName, info: L10n.appearanceCustomBackgroundFileError) + } + } + }) + }, drawBorder: true, height: 50, singleButton: true) + + return interactions + } + + override var dynamicSize: Bool { + return true + } + public override var modalHeader: (left: ModalHeaderData?, center: ModalHeaderData?, right: ModalHeaderData?)? { + return (left: ModalHeaderData(image: theme.icons.modalClose, handler: { [weak self] in + self?.close() + }), center: ModalHeaderData(title: L10n.chatWPBackgroundTitle), right: nil) + } + + override func measure(size: NSSize) { + self.modal?.resize(with:NSMakeSize(frame.width, size.height - 150), animated: false) + } + + override func viewDidResized(_ size: NSSize) { + containerLayoutUpdated() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } + + + override func viewDidLoad() { + super.viewDidLoad() + + + containerLayoutUpdated() + + let context = self.context + let previousEntries = Atomic<[ThemeGridControllerEntry]?>(value: nil) + + let close = { [weak self] in + self?.close() + } + + let deleted: Promise<[Wallpaper]> = Promise([]) + let deletedValue:Atomic<[Wallpaper]> = Atomic(value: []) + + let updateDeleted: (([Wallpaper]) -> [Wallpaper]) -> Void = { f in + deleted.set(.single(deletedValue.modify(f))) + } + + let interaction = ThemeGridControllerInteraction(openWallpaper: { wallpaper, telegramWallpaper in + switch wallpaper { + case .image, .file, .color, .gradient: + showModal(with: WallpaperPreviewController(context, wallpaper: wallpaper, source: telegramWallpaper != nil ? .gallery(telegramWallpaper!) : .none), for: context.window) + default: + _ = updateThemeInteractivetly(accountManager: context.sharedContext.accountManager, f: { settings in + return settings.updateWallpaper{ $0.withUpdatedWallpaper(wallpaper) }.saveDefaultWallpaper() + }).start() + delay(0.15, closure: { + close() + }) + } + + }, deleteWallpaper: { wallpaper, telegramWallpaper in + if wallpaper.isSemanticallyEqual(to: theme.wallpaper.wallpaper) { + _ = updateThemeInteractivetly(accountManager: context.sharedContext.accountManager, f: { settings in + return settings.updateWallpaper({ $0.withUpdatedWallpaper(settings.palette.wallpaper.wallpaper) }).saveDefaultWallpaper() + }).start() + } + + _ = deleteWallpaper(account: context.account, wallpaper: telegramWallpaper).start() + + updateDeleted { current in + return current + [wallpaper] + } + }) + + + let transition = combineLatest(queue: prepareQueue, telegramWallpapers(postbox: context.account.postbox, network: context.account.network), deleted.get(), appearanceSignal) + |> map { wallpapers, deletedWallpapers, appearance -> (ThemeGridEntryTransition, Bool) in + var entries: [ThemeGridControllerEntry] = [] + var index = 0 + + entries.append(ThemeGridControllerEntry(index: index, wallpaper: .none, telegramWallapper: nil, selected: appearance.presentation.wallpaper.wallpaper.isSemanticallyEqual(to: .none))) + index += 1 + + + let telegramWallpaper: TelegramWallpaper? = wallpapers.first(where: { wallpaper -> Bool in + let wallpaper: Wallpaper = Wallpaper(wallpaper) + return wallpaper.isSemanticallyEqual(to: theme.wallpaper.wallpaper) + }) + let selected: Wallpaper = theme.wallpaper.wallpaper + + + let wallpaper: Wallpaper + + switch theme.wallpaper.wallpaper { + case .gradient: + entries.append(ThemeGridControllerEntry(index: index, wallpaper: theme.wallpaper.wallpaper, telegramWallapper: nil, selected: true)) + default: + if theme.colors.accent != theme.colors.basicAccent { + wallpaper = .color(theme.colors.basicAccent.argb) + } else { + wallpaper = .color(theme.colors.basicAccent.lighter(amount: 0.25).argb) + } + entries.append(ThemeGridControllerEntry(index: index, wallpaper: wallpaper, telegramWallapper: nil, selected: theme.wallpaper.wallpaper.isSemanticallyEqual(to: wallpaper))) + } + + + + switch selected { + case .none, .color, .gradient: + break + default: + entries.append(ThemeGridControllerEntry(index: index, wallpaper: selected, telegramWallapper: telegramWallpaper, selected: true)) + index += 1 + } + + for item in wallpapers { + let wallpaper = Wallpaper(item) + if !deletedWallpapers.contains(where: {$0.isSemanticallyEqual(to: wallpaper)}) { + switch item { + case let .file(file): + if file.isPattern, file.settings.colors.isEmpty { + continue + } + default: + break + } + if selected.isSemanticallyEqual(to: wallpaper) { + continue + } + entries.append(ThemeGridControllerEntry(index: index, wallpaper: wallpaper, telegramWallapper: item, selected: appearance.presentation.wallpaper.wallpaper.isSemanticallyEqual(to: wallpaper))) + index += 1 + } + } + let previous = previousEntries.swap(entries) + return (preparedThemeGridEntryTransition(context: context, from: previous ?? [], to: entries, interaction: interaction), previous == nil) + } + + self.disposable = (transition |> deliverOnMainQueue).start(next: { [weak self] (transition, _) in + if let strongSelf = self { + strongSelf.enqueueTransition(transition) + } + }) + } + + deinit { + self.disposable?.dispose() + } + + + private func enqueueTransition(_ transition: ThemeGridEntryTransition) { + self.queuedTransitions.append(transition) + self.dequeueTransitions() + } + + private func dequeueTransitions() { + while !self.queuedTransitions.isEmpty { + let transition = self.queuedTransitions.removeFirst() + self.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: transition.scrollToItem, updateLayout: nil, itemTransition: .immediate, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: transition.updateFirstIndexInSectionOffset), completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.readyOnce() + } + }) + } + } + + func containerLayoutUpdated() { + var insets: NSEdgeInsets = NSEdgeInsets() + let scrollIndicatorInsets = insets + + let referenceImageSize = CGSize(width: 108.0, height: 163.0) + + let minSpacing: CGFloat = 10.0 + + let imageCount = Int((frame.width - minSpacing * 2.0) / (referenceImageSize.width + minSpacing)) + + let imageSize = referenceImageSize.aspectFilled(CGSize(width: floor((frame.width - CGFloat(imageCount + 1) * minSpacing) / CGFloat(imageCount)), height: referenceImageSize.height)) + + let spacing = floor((frame.width - CGFloat(imageCount) * imageSize.width) / CGFloat(imageCount + 1)) + + insets.top += 0 + + self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: frame.size, insets: insets, scrollIndicatorInsets: scrollIndicatorInsets, preloadSize: 380, type: .fixed(itemSize: imageSize, lineSpacing: spacing)), transition: .immediate), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + + self.gridNode.frame = CGRect(x: 0.0, y: 0.0, width: frame.width, height: frame.height) + + let dequeue = true + if dequeue { + self.dequeueTransitions() + } + } + + + func scrollToTop() { + self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: GridNodeScrollToItem(index: 0, position: .top, transition: .animated(duration: 0.25, curve: .easeInOut), directionHint: .up, adjustForSection: true, adjustForTopInset: true), updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + } +} diff --git a/Telegram-Mac/ChatlistFilterVisibilityItem.swift b/Telegram-Mac/ChatlistFilterVisibilityItem.swift new file mode 100644 index 0000000000..d4f1dfa761 --- /dev/null +++ b/Telegram-Mac/ChatlistFilterVisibilityItem.swift @@ -0,0 +1,178 @@ +// +// ChatListFilterVisibilityItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 08/04/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + + +private func generateThumb(_ basic: CGImage, active: CGImage?) -> CGImage { + return generateImage(basic.backingSize, contextGenerator: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + + ctx.draw(basic, in: rect) + + if let active = active { + ctx.draw(active, in: rect) + } + })! +} + +class ChatListFilterVisibilityItem: GeneralRowItem { + + fileprivate let topViewLayout: TextViewLayout + fileprivate let leftViewLayout: TextViewLayout + + fileprivate let topThumb: CGImage + fileprivate let leftThumb: CGImage + + fileprivate let sidebar: Bool + fileprivate let toggle:(Bool)->Void + init(_ initialSize: NSSize, stableId: AnyHashable, sidebar: Bool, viewType: GeneralViewType, toggle:@escaping(Bool)->Void) { + + self.sidebar = sidebar + self.toggle = toggle + topViewLayout = TextViewLayout.init(.initialize(string: L10n.chatListFilterTabBarOnTheTop, color: sidebar ? theme.colors.grayText : theme.colors.accent, font: .normal(.text))) + + leftViewLayout = TextViewLayout.init(.initialize(string: L10n.chatListFilterTabBarOnTheLeft, color: sidebar ? theme.colors.accent : theme.colors.grayText, font: .normal(.text))) + + topThumb = generateThumb(NSImage(named: "tabsselect_top_gray")!.precomposed(theme.colors.grayIcon.withAlphaComponent(0.8)), active: NSImage(named: "tabsselect_top_systemcol")!.precomposed(theme.colors.accent)) + + leftThumb = generateThumb(NSImage(named: "tabsselect_left_gray")!.precomposed(theme.colors.grayIcon.withAlphaComponent(0.8)), active: NSImage(named: "tabsselect_left_systemcol")!.precomposed(theme.colors.accent)) + + + + super.init(initialSize, stableId: stableId, viewType: viewType) + } + + override var height: CGFloat { + if blockWidth < (150 * 2) + viewType.innerInset.right * 3 { + + + + return viewType.innerInset.top + viewType.innerInset.bottom + viewType.innerInset.top + 120 * 2 + 20 + leftViewLayout.layoutSize.height + topViewLayout.layoutSize.height + } + + return viewType.innerInset.top + viewType.innerInset.bottom + max(leftViewLayout.layoutSize.height, topViewLayout.layoutSize.height) + 10 + 120 + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + + topViewLayout.measure(width: 140) + leftViewLayout.measure(width: 140) + + return true + } + + + override func viewClass() -> AnyClass { + return ChatlistFilterVisibilityView.self + } +} + +private final class VisibilityContainerView : Control { + private let imageView: ImageView = ImageView() + private let textView: TextView = TextView() + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(imageView) + addSubview(textView) + + imageView.isEventLess = true + textView.isEventLess = true + textView.userInteractionEnabled = false + textView.isSelectable = false + } + + func update( _ text: TextViewLayout, image: CGImage, selected: Bool) -> Void { + imageView.image = image + imageView.sizeToFit() + textView.update(text) + + imageView.layer?.cornerRadius = 8 + imageView.layer?.borderWidth = selected ? 2 : 0 + imageView.layer?.borderColor = selected ? theme.colors.accent.cgColor : theme.colors.grayIcon.cgColor + } + + override func layout() { + super.layout() + imageView.centerX(y: 0) + textView.centerX(y: imageView.frame.maxY + 10) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + +private final class ChatlistFilterVisibilityView : GeneralContainableRowView { + private let leftItem:VisibilityContainerView = VisibilityContainerView(frame: .zero) + private let topItem:VisibilityContainerView = VisibilityContainerView(frame: .zero) + + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(leftItem) + addSubview(topItem) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? ChatListFilterVisibilityItem else { + return + } + + leftItem.setFrameSize(NSMakeSize(150, 120 + 10 + item.leftViewLayout.layoutSize.height)) + topItem.setFrameSize(NSMakeSize(150, 120 + 10 + item.topViewLayout.layoutSize.height)) + + leftItem.update(item.leftViewLayout, image: item.leftThumb, selected: item.sidebar) + topItem.update(item.topViewLayout, image: item.topThumb, selected: !item.sidebar) + + leftItem.removeAllHandlers() + topItem.removeAllHandlers() + + + topItem.set(handler: { [weak item] _ in + item?.toggle(false) + }, for: .Click) + + leftItem.set(handler: { [weak item] _ in + item?.toggle(true) + }, for: .Click) + + needsLayout = true + } + + override func layout() { + super.layout() + + guard let item = item as? ChatListFilterVisibilityItem else { + return + } + + if containerView.frame.width < (leftItem.frame.width + topItem.frame.width) + item.viewType.innerInset.right * 3 { + topItem.centerX(y: item.viewType.innerInset.top) + leftItem.centerX(y: topItem.frame.maxY + item.viewType.innerInset.bottom) + } else { + let inset = (containerView.frame.width - (leftItem.frame.width + topItem.frame.width)) / 3 + topItem.setFrameOrigin(NSMakePoint(inset, item.viewType.innerInset.top)) + leftItem.setFrameOrigin(NSMakePoint(containerView.frame.width - leftItem.frame.width - inset, item.viewType.innerInset.top)) + } + } + + +} diff --git a/Telegram-Mac/ClearCache.swift b/Telegram-Mac/ClearCache.swift new file mode 100644 index 0000000000..7ee1296fc2 --- /dev/null +++ b/Telegram-Mac/ClearCache.swift @@ -0,0 +1,212 @@ +// +// ClearCache.swift +// Telegram +// +// Created by Mikhail Filimonov on 03/08/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import SwiftSignalKit +import Postbox +import TelegramCore + +private let cacheQueue = Queue(name: "org.telegram.clearCacheQueue") +private let cleanQueue = Queue(name: "org.telegram.cleanupQueue") + + +func scanFiles(at path: String, anyway: ((String, Int)) -> Void) { + guard let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: path), includingPropertiesForKeys: [.isDirectoryKey, .fileSizeKey], options: [.skipsSubdirectoryDescendants], errorHandler: nil) else { + return + } + while let item = enumerator.nextObject() { + guard let url = item as? NSURL else { + continue + } + guard let resourceValues = try? url.resourceValues(forKeys: [.isDirectoryKey, .fileSizeKey]) else { + continue + } + if let value = resourceValues[.isDirectoryKey] as? Bool, value { + continue + } + if let file = url.path { + anyway((file, (resourceValues[.fileSizeKey] as? NSNumber)?.intValue ?? 0)) + } + } +} + +private func clearCache(_ files: [(String, Int)], excludes: [(partial: String, complete: String)], start: TimeInterval) -> Signal { + return Signal { subscriber in + + var cancelled = false + + let files = files.filter { file in + return !excludes.contains(where: { + $0.partial == file.0 || $0.complete == file.0 + }) + } + + let total: Int = files.reduce(0, { + $0 + $1.1 + }) + + var cleaned: Int = 0 + + for file in files { + if !cancelled { + let url = URL(fileURLWithPath: file.0) + guard let resourceValues = try? url.resourceValues(forKeys: [.contentModificationDateKey]) else { + continue + } + let date = resourceValues.contentModificationDate?.timeIntervalSince1970 ?? start + if date <= start { + unlink(file.0) + } + cleaned += file.1 + subscriber.putNext(Float(cleaned) / Float(total)) + } else { + break + } + } + + subscriber.putNext(1.0) + subscriber.putCompletion() + + return ActionDisposable { + cancelled = true + } + } |> runOn(cleanQueue) +} + + +private final class CCTask : Equatable { + static func == (lhs: CCTask, rhs: CCTask) -> Bool { + return lhs === rhs + } + private let disposable = MetaDisposable() + private var progressValue: ValuePromise = ValuePromise(0) + private var progress: Atomic = Atomic(value: 0) + + init(_ account: Account, completion: @escaping()->Void) { + let signal: Signal = account.postbox.mediaBox.allFileContexts() + |> deliverOn(cacheQueue) + |> mapToSignal { excludes in + var files:[(String, Int)] = [] + scanFiles(at: account.postbox.mediaBox.basePath, anyway: { value in + files.append(value) + }) + scanFiles(at: account.postbox.mediaBox.basePath + "/cache", anyway: { value in + files.append(value) + }) + return clearCache(files, excludes: excludes, start: Date().timeIntervalSince1970) + } |> deliverOn(cacheQueue) + + self.disposable.set(signal.start(next: { [weak self] value in + guard let `self` = self else { + return + } + self.progressValue.set(self.progress.with { _ in return value }) + if value == 1.0 { + cacheQueue.after(0.2, completion) + } + })) + } + + func getCurrentProgress() -> Float { + return progress.with { $0 } + } + + + func updatedProgress() -> Signal { + return progressValue.get() + } +} + +final class CCTaskData : Equatable { + static func == (lhs: CCTaskData, rhs: CCTaskData) -> Bool { + return lhs === rhs + } + + private let task: CCTask + fileprivate init?(_ task: CCTask?) { + guard let task = task else { + return nil + } + self.task = task + } + + var currentProgress: Float { + return self.task.getCurrentProgress() + } + + var progress: Signal { + return self.task.updatedProgress() + } +} + +private class CCContext { + private let account: Account + + private let currentTaskValue: Atomic = Atomic(value: nil) + private let currentTask: ValuePromise = ValuePromise(nil) + + init(account: Account) { + self.account = account + } + + func makeAndRunTask() { + let account = self.account + currentTask.set(currentTaskValue.modify { task in + if let task = task { + return task + } + return CCTask(account, completion: { [weak self] in + if let `self` = self { + cacheQueue.justDispatch { + self.currentTask.set(self.currentTaskValue.modify { _ in return nil } ) + } + } + }) + }) + } + func cancel() { + self.currentTask.set(self.currentTaskValue.modify { _ in return nil } ) + } + + func getTask() -> Signal { + return currentTask.get() |> map { CCTaskData($0) } + } +} + + + +final class AccountClearCache { + private let context: QueueLocalObject + init(account: Account) { + context = QueueLocalObject(queue: cacheQueue, generate: { + return CCContext(account: account) + }) + } + + var task: Signal { + let signal: Signal, NoError> = context.signalWith({ ctx, subscriber in + subscriber.putNext(ctx.getTask()) + subscriber.putCompletion() + + return EmptyDisposable + }) + return signal |> switchToLatest + } + + func run() { + context.with { ctx in + ctx.makeAndRunTask() + } + } + + func cancel() { + context.with { ctx in + ctx.cancel() + } + } +} diff --git a/Telegram-Mac/ClearUserNotifies.swift b/Telegram-Mac/ClearUserNotifies.swift index 199349c372..fc0365b725 100644 --- a/Telegram-Mac/ClearUserNotifies.swift +++ b/Telegram-Mac/ClearUserNotifies.swift @@ -7,26 +7,8 @@ // import Cocoa -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore -private let queue:Queue = Queue(name: "clearUserNotifiesQueue", target: nil) +import Postbox +import SwiftSignalKit -func clearNotifies(_ peerId:PeerId, maxId:MessageId) { - queue.async { - let deliveredNotifications = NSUserNotificationCenter.default.deliveredNotifications - - for notification in deliveredNotifications { - if let encodedMessageId = notification.userInfo?["encodedMessageId"] as? Data, let namespace = notification.userInfo?["peerId.namespace"] as? Int32, let id = notification.userInfo?["peerId.id"] as? Int32 { - let notificationMessageId = MessageId(ReadBuffer(memoryBufferNoCopy: MemoryBuffer(data: encodedMessageId))) - let notificationPeerId = PeerId(namespace: namespace, id: id) - - if notificationPeerId == peerId, notificationMessageId <= maxId { - NSUserNotificationCenter.default.removeDeliveredNotification(notification) - } - - } - } - } -} diff --git a/Telegram-Mac/ClosureInviteLinkController.swift b/Telegram-Mac/ClosureInviteLinkController.swift new file mode 100644 index 0000000000..e588fb8cec --- /dev/null +++ b/Telegram-Mac/ClosureInviteLinkController.swift @@ -0,0 +1,309 @@ +// +// ClosureInviteLinkController.swift +// Telegram +// +// Created by Mikhail Filimonov on 14.01.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Cocoa +import Postbox +import TelegramCore +import TGUIKit +import SwiftSignalKit + +import Postbox + +private final class InviteLinkArguments { + let context: AccountContext + let usageLimit:(Int32)->Void + let limitDate: (Int32)->Void + let tempCount:(Int32?)->Void + let tempDate:(Int32?)->Void + init(context: AccountContext, usageLimit: @escaping(Int32)->Void, limitDate: @escaping(Int32)->Void, tempCount:@escaping(Int32?)->Void, tempDate: @escaping(Int32?)->Void) { + self.context = context + self.usageLimit = usageLimit + self.limitDate = limitDate + self.tempCount = tempCount + self.tempDate = tempDate + } +} + +struct ClosureInviteLinkState: Equatable { + fileprivate(set) var date:Int32 + fileprivate(set) var count: Int32 + fileprivate var tempCount: Int32? + fileprivate var tempDate: Int32? +} + +// +private let _id_period = InputDataIdentifier("_id_period") +private let _id_period_precise = InputDataIdentifier("_id_period_precise") + +private let _id_count = InputDataIdentifier("_id_count") +private let _id_count_precise = InputDataIdentifier("_id_count_precise") + +private func inviteLinkEntries(state: ClosureInviteLinkState, arguments: InviteLinkArguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId: Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.editInvitationLimitedByPeriod), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_period, equatable: InputDataEquatable(state), comparable: nil, item: { initialSize, stableId in + let hour: Int32 = 60 * 60 + let day: Int32 = hour * 24 * 1 + var sizes:[Int32] = [hour, day, day * 7, Int32.max] + + if let temp = state.tempDate { + var bestIndex: Int = 0 + for (i, size) in sizes.enumerated() { + if size < temp { + bestIndex = i + } + } + sizes[bestIndex] = temp + } + + let current = state.date + if sizes.firstIndex(where: { $0 == current }) == nil { + var bestIndex: Int = 0 + for (i, size) in sizes.enumerated() { + if size < current { + bestIndex = i + } + } + sizes[bestIndex] = current + } + let titles: [String] = sizes.map { value in + if value == Int32.max { + return "∞" + } else { + return autoremoveLocalized(Int(value)) + } + } + return SelectSizeRowItem(initialSize, stableId: stableId, current: current, sizes: sizes, hasMarkers: false, titles: titles, viewType: .firstItem, selectAction: { index in + arguments.limitDate(sizes[index]) + }) + })) + index += 1 + + + let dateFormatter = makeNewDateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .short + let dateString = state.date == .max ? L10n.editInvitationNever : dateFormatter.string(from: Date(timeIntervalSinceNow: TimeInterval(state.date))) + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_period_precise, data: .init(name: L10n.editInvitationExpiryDate, color: theme.colors.text, type: .context(dateString), viewType: .lastItem, action: { + showModal(with: DateSelectorModalController(context: arguments.context, defaultDate: Date(timeIntervalSinceNow: TimeInterval(state.date == .max ? Int32.secondsInWeek : state.date)), mode: .date(title: L10n.editInvitationExpiryDate, doneTitle: L10n.editInvitationSave), selectedAt: { date in + arguments.limitDate(Int32(date.timeIntervalSinceNow)) + arguments.tempDate(Int32(date.timeIntervalSinceNow)) + + }), for: arguments.context.window) + }))) + index += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.editInvitationExpiryDesc), data: .init(color: theme.colors.listGrayText, viewType: .textBottomItem))) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.editInvitationLimitedByCount), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_count, equatable: InputDataEquatable(state), comparable: nil, item: { initialSize, stableId in + var sizes:[Int32] = [1, 10, 50, 100, Int32.max] + + if let temp = state.tempCount { + var bestIndex: Int = 0 + for (i, size) in sizes.enumerated() { + if size < temp { + bestIndex = i + } + } + sizes[bestIndex] = temp + } + + let current: Int32 = state.count + if sizes.firstIndex(where: { $0 == current }) == nil { + var bestIndex: Int = 0 + for (i, size) in sizes.enumerated() { + if size < current { + bestIndex = i + } + } + sizes[bestIndex] = current + } + let titles: [String] = sizes.map { value in + if value == Int32.max { + return "∞" + } else { + return Int(value).prettyNumber + } + } + return SelectSizeRowItem(initialSize, stableId: stableId, current: current, sizes: sizes, hasMarkers: false, titles: titles, viewType: .firstItem, selectAction: { index in + arguments.usageLimit(sizes[index]) + }) + })) + index += 1 + + let value = state.count == .max ? L10n.editInvitationUnlimited : Int(state.count).prettyNumber + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_count_precise, data: .init(name: L10n.editInvitationNumberOfUsers, color: theme.colors.text, type: .context(value), viewType: .lastItem, action: { + showModal(with: NumberSelectorController(base: state.count == .max ? nil : Int(state.count), title: L10n.editInvitationNumberOfUsers, placeholder: L10n.editInvitationEnterNumber, okTitle: L10n.editInvitationSave, updated: { updated in + if let updated = updated { + arguments.usageLimit(Int32(updated)) + } else { + arguments.usageLimit(.max) + } + arguments.tempCount(updated != nil ? Int32(updated!) : nil) + }), for: arguments.context.window) + }))) + index += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.editInvitationLimitDesc), data: .init(color: theme.colors.listGrayText, viewType: .textBottomItem))) + index += 1 + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +enum InviteLinkClosureMode { + case new + case edit(ExportedInvitation) + + var title: String { + switch self { + case .new: + return L10n.editInvitationNewTitle + case .edit: + return L10n.editInvitationEditTitle + } + } + var done: String { + switch self { + case .new: + return L10n.editInvitationOKCreate + case .edit: + return L10n.editInvitationOKSave + } + } + var doneColor: NSColor { + switch self { + case .new: + return theme.colors.accent + case .edit: + return theme.colors.redUI + } + } +} + +func ClosureInviteLinkController(context: AccountContext, peerId: PeerId, mode: InviteLinkClosureMode, save:@escaping(ClosureInviteLinkState)->Void) -> InputDataModalController { + var initialState = ClosureInviteLinkState(date: 0, count: 0) + let week: Int32 = 60 * 60 * 24 * 1 * 7 + switch mode { + case .new: + initialState.date = .max + initialState.count = .max + case let .edit(invitation): + if let expireDate = invitation.expireDate { + initialState.date = invitation.isExpired ? week : Int32(TimeInterval(expireDate) - Date().timeIntervalSince1970) + } else { + initialState.date = week + } + initialState.tempDate = initialState.date + if let alreadyCount = invitation.count, let usageLimit = invitation.usageLimit { + initialState.count = usageLimit - alreadyCount + } else if let usageLimit = invitation.usageLimit { + initialState.count = usageLimit + } else { + initialState.count = .max + } + if initialState.count != .max { + initialState.tempCount = initialState.count + } + } + let state: ValuePromise = ValuePromise(initialState) + let stateValue: Atomic = Atomic(value: initialState) + + let updateState:((ClosureInviteLinkState)->ClosureInviteLinkState) -> Void = { f in + state.set(stateValue.modify(f)) + } + + let arguments = InviteLinkArguments(context: context, usageLimit: { value in + updateState { current in + var current = current + current.count = value + return current + } + }, limitDate: { value in + updateState { current in + var current = current + current.date = value + return current + } + }, tempCount: { value in + updateState { current in + var current = current + current.tempCount = value + return current + } + }, tempDate: { value in + updateState { current in + var current = current + current.tempDate = value + return current + } + }) + + let dataSignal = state.get() |> deliverOnPrepareQueue |> map { state in + return inviteLinkEntries(state: state, arguments: arguments) + } |> map { entries in + return InputDataSignalValue(entries: entries) + } + + var getModalController:(()->InputDataModalController?)? = nil + + + let controller = InputDataController(dataSignal: dataSignal, title: mode.title) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { + getModalController?()?.close() + }) + + controller.updateDatas = { data in + + return .none + } + + + let modalInteractions = ModalInteractions(acceptTitle: mode.done, accept: { [weak controller] in + controller?.validateInputValues() + }, drawBorder: true, singleButton: true) + + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions, closeHandler: { f in + f() + }, size: NSMakeSize(340, 350)) + + getModalController = { [weak modalController] in + return modalController + } + + controller.validateData = { data in + return .success(.custom { + save(stateValue.with { $0 }) + getModalController?()?.close() + }) + } + + + return modalController +} diff --git a/Telegram-Mac/ColdStartPasslockController.swift b/Telegram-Mac/ColdStartPasslockController.swift new file mode 100644 index 0000000000..19adcb66c5 --- /dev/null +++ b/Telegram-Mac/ColdStartPasslockController.swift @@ -0,0 +1,138 @@ +// +// ColdStartPasslockController.swift +// Telegram +// +// Created by Mikhail Filimonov on 03.11.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit + + +class ColdStartPasslockController: ModalViewController { + private let valueDisposable = MetaDisposable() + private let logoutDisposable = MetaDisposable() + + private let logoutImpl:() -> Signal + private let checkNextValue: (String)->Bool + init(checkNextValue:@escaping(String)->Bool, logoutImpl:@escaping()->Signal) { + self.checkNextValue = checkNextValue + self.logoutImpl = logoutImpl + super.init(frame: NSMakeRect(0, 0, 350, 350)) + self.bar = .init(height: 0) + } + + override var isVisualEffectBackground: Bool { + return false + } + + override var isFullScreen: Bool { + return true + } + + override var background: NSColor { + return self.containerBackground + } + + private var genericView:PasscodeLockView { + return self.view as! PasscodeLockView + } + + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + } + + + override func viewDidLoad() { + super.viewDidLoad() + + + genericView.logoutImpl = { [weak self] in + guard let window = self?.window else { return } + + confirm(for: window, information: L10n.accountConfirmLogoutText, successHandler: { [weak self] _ in + guard let `self` = self else { return } + + _ = showModalProgress(signal: self.logoutImpl(), for: window).start(completed: { [weak self] in + delay(0.2, closure: { [weak self] in + self?.close() + }) + }) + }) + } + + var locked = false + + valueDisposable.set((genericView.value.get() |> deliverOnMainQueue).start(next: { [weak self] value in + guard let `self` = self, !locked else { + return + } + if !self.checkNextValue(value) { + self.genericView.input.shake() + } else { + locked = true + } + })) + + genericView.update(hasTouchId: false) + readyOnce() + + + } + + override var closable: Bool { + return false + } + + + override func escapeKeyAction() -> KeyHandlerResult { + return .invoked + } + + override func firstResponder() -> NSResponder? { + if !(window?.firstResponder is NSText) { + return genericView.input + } + let editor = self.window?.fieldEditor(true, for: genericView.input) + if window?.firstResponder != editor { + return genericView.input + } + return editor + + } + + override var containerBackground: NSColor { + return theme.colors.background.withAlphaComponent(1.0) + } + override var handleEvents: Bool { + return true + } + + override var cornerRadius: CGFloat { + return 0 + } + + override var handleAllEvents: Bool { + return true + } + + + override var responderPriority: HandlerPriority { + return .supreme + } + + deinit { + logoutDisposable.dispose() + valueDisposable.dispose() + self.window?.removeAllHandlers(for: self) + } + + override func viewClass() -> AnyClass { + return PasscodeLockView.self + } + +} + diff --git a/Telegram-Mac/ComposeActions.swift b/Telegram-Mac/ComposeActions.swift index d12f142934..9e217dd956 100644 --- a/Telegram-Mac/ComposeActions.swift +++ b/Telegram-Mac/ComposeActions.swift @@ -7,40 +7,142 @@ // import Cocoa -import SwiftSignalKitMac -import PostboxMac -import TelegramCoreMac +import SwiftSignalKit +import Postbox +import TelegramCore + import TGUIKit -func createGroup(with account:Account, for navigation:NavigationViewController) { +func createGroup(with context: AccountContext, selectedPeers:Set = Set()) { + let select = { SelectPeersController(titles: ComposeTitles(L10n.composeSelectUsers, L10n.composeNext), context: context, settings: [.contacts, .remote], isNewGroup: true, selectedPeers: selectedPeers) } + let chooseName = { CreateGroupViewController(titles: ComposeTitles(L10n.groupNewGroup, L10n.composeCreate), context: context) } + let signal = execute(context: context, select, chooseName) |> mapError { _ in return CreateGroupError.generic } |> mapToSignal { (_, result) -> Signal<(PeerId?, String?), CreateGroupError> in + let signal = showModalProgress(signal: context.engine.peers.createGroup(title: result.title, peerIds: result.peerIds) |> map { return ($0, result.picture)}, for: mainWindow, disposeAfterComplete: false) + return signal + } |> mapToSignal{ peerId, picture -> Signal<(PeerId?, Bool), CreateGroupError> in + if let peerId = peerId, let picture = picture { + let resource = LocalFileReferenceMediaResource(localFilePath: picture, randomId: arc4random64()) + let signal:Signal<(PeerId?, Bool), NoError> = context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: resource), mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: context.account.postbox, resource: resource, representations: representations) + }) |> `catch` {_ in .complete()} |> map { value in + switch value { + case .complete: + return (Optional(peerId), false) + default: + return (nil, false) + } + } + + return .single((peerId, true)) |> then(signal |> mapError { _ in return CreateGroupError.generic}) + } + return .single((peerId, true)) + } |> deliverOnMainQueue |> filter {$0.1} - let select = SelectPeersController(titles: ComposeTitles(tr(.composeSelectUsers), tr(.composeNext)), account: account, settings: [.contacts, .remote]) - let chooseName = CreateGroupViewController(titles: ComposeTitles(tr(.groupNewGroup), tr(.composeCreate)), account: account) - let signal = execute(navigation:navigation, select, chooseName) |> mapToSignal { (_, result) -> Signal in - return showModalProgress(signal: createGroup(account: account, title: result.title, peerIds: result.peerIds), for: mainWindow) - } - _ = signal.start(next: { [weak navigation] (peerId) in - if let peerId = peerId { - navigation?.push(ChatController(account: account, peerId: peerId)) + + _ = signal.start(next: { peerId, complete in + if let peerId = peerId, complete { + context.sharedContext.bindings.rootNavigation().push(ChatController(context: context, chatLocation: .peer(peerId))) } + }, error: { error in + let text: String + switch error { + case .privacy: + text = L10n.privacyGroupsAndChannelsInviteToChannelMultipleError + case .generic: + text = L10n.unknownError + case .restricted: + text = L10n.unknownError + case .tooMuchLocationBasedGroups: + text = L10n.unknownError + case let .serverProvided(error): + text = error + case .tooMuchJoined: + text = L10n.channelErrorAddTooMuch + } + alert(for: context.window, info: text) }) } -func createChannel(with account:Account, for navigation:NavigationViewController) { - let intro = ChannelIntroViewController(account) - navigation.push(intro) + +func createSupergroup(with context: AccountContext, defaultText: String = "") -> Signal { + let chooseName = CreateGroupViewController(titles: ComposeTitles(L10n.groupNewGroup, L10n.composeCreate), context: context, defaultText: defaultText) + context.sharedContext.bindings.rootNavigation().push(chooseName) + chooseName.restart(with: ComposeState([])) + let signal = chooseName.onComplete.get() |> mapToSignal { result -> Signal<(PeerId?, Bool), NoError> in + + let createSignal: Signal<(PeerId?, String?), CreateChannelError> = showModalProgress(signal: context.engine.peers.createSupergroup(title: result.title, description: nil) |> map { return ($0, result.picture) }, for: mainWindow, disposeAfterComplete: false) + + return createSignal + |> `catch` { _ in + return .single((nil, nil)) + } + |> mapToSignal { peerId, picture -> Signal<(PeerId?, Bool), NoError> in + if let peerId = peerId { + var additionalSignals:[Signal] = [] + + if let picture = picture { + let resource = LocalFileReferenceMediaResource(localFilePath: picture, randomId: arc4random64()) + let signal:Signal = context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: resource), mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: context.account.postbox, resource: resource, representations: representations) + }) |> `catch` { _ in .complete() } |> map { _ in } + additionalSignals.append(signal) + } + + let combined:Signal<(PeerId?, Bool), NoError> = combineLatest(additionalSignals) |> map { _ in (nil, false) } + + + + return .single((peerId, true)) |> then(combined) + } + return .single((peerId, true)) + } |> deliverOnMainQueue + } - let create = intro.onComplete.get() |> mapToSignal{ () -> Signal in - let create = CreateChannelViewController(titles: ComposeTitles(tr(.channelNewChannel), tr(.composeNext)), account: account) - navigation.push(create) - return create.onComplete.get() |> deliverOnMainQueue |> mapToSignal { peerId -> Signal in + return signal |> filter { $0.1 } |> map { $0.0 } +} + +func createChannel(with context: AccountContext) { + + let intro = ChannelIntroViewController(context) + if FastSettings.needShowChannelIntro { + context.sharedContext.bindings.rootNavigation().push(intro) + } + + let introCompletion: Signal = FastSettings.needShowChannelIntro ? intro.onComplete.get() : Signal.single(Void()) + + let create = introCompletion |> mapToSignal { () -> Signal in + let create = CreateChannelViewController(titles: ComposeTitles(L10n.channelNewChannel, L10n.composeNext), context: context) + context.sharedContext.bindings.rootNavigation().push(create) + return create.onComplete.get() |> deliverOnMainQueue |> filter {$0.1} |> mapToSignal { peerId, _ -> Signal in if let peerId = peerId { - navigation.push(ChatController(account: account, peerId: peerId), style: .none) - let visibility = ChannelVisibilityController(account: account, peerId: peerId) - navigation.push(visibility) - return visibility.onComplete.get() |> filter {$0} |> map {_ in return peerId} + FastSettings.markChannelIntroHasSeen() + context.sharedContext.bindings.rootNavigation().removeAll() + + var chat: ChatController? = ChatController(context: context, chatLocation: .peer(peerId)) + var visibility: ChannelVisibilityController? = ChannelVisibilityController(context, peerId: peerId, isChannel: true, isNew: true) + + chat!.navigationController = context.sharedContext.bindings.rootNavigation() + visibility!.navigationController = context.sharedContext.bindings.rootNavigation() + + chat!.loadViewIfNeeded(context.sharedContext.bindings.rootNavigation().bounds) + visibility!.loadViewIfNeeded(context.sharedContext.bindings.rootNavigation().bounds) + + + + let chatSignal = chat!.ready.get() |> filter { $0 } |> take(1) |> ignoreValues + let visibilitySignal = visibility!.ready.get() |> filter { $0 } |> take(1) |> ignoreValues + + _ = combineLatest(queue: .mainQueue(), chatSignal, visibilitySignal).start(completed: { + context.sharedContext.bindings.rootNavigation().push(chat!) + context.sharedContext.bindings.rootNavigation().push(visibility!) + + chat = nil + visibility = nil + }) + + return visibility!.onComplete.get() |> map {_ in return peerId} } return .single(nil) } @@ -48,42 +150,44 @@ func createChannel(with account:Account, for navigation:NavigationViewController _ = create.start(next: { peerId in if let peerId = peerId { - navigation.push(ChatController(account: account, peerId: peerId)) + context.sharedContext.bindings.rootNavigation().push(ChatController(context: context, chatLocation: .peer(peerId))) } else { - navigation.close() + context.sharedContext.bindings.rootNavigation().close() } }) } -private func execute(navigation:NavigationViewController, _ c1:EmptyComposeController, _ c2: EmptyComposeController) -> Signal<(T1,T2), Void> { +private func execute(context: AccountContext, _ c1: @escaping() -> SelectPeersMainController, _ c2: @escaping() -> EmptyComposeController) -> Signal<(T1,T2), NoError> { - navigation.push(c1) - return c1.onComplete.get() |> mapToSignal { (c1Next) -> Signal<(T1,T2), Void> in - navigation.push(c2) - c2.restart(with: ComposeState(c1Next)) - return c2.onComplete.get() |> mapToSignal{ (c2Next) -> Signal<(T1,T2), Void> in + let c1Controller = c1() + context.sharedContext.bindings.rootNavigation().push(c1Controller) + return c1Controller.onComplete.get() |> mapToSignal { (c1Next) -> Signal<(T1,T2), NoError> in + let c2Controller = c2() + context.sharedContext.bindings.rootNavigation().push(c2Controller) + c2Controller.restart(with: ComposeState(c1Next)) + return c2Controller.onComplete.get() |> mapToSignal{ (c2Next) -> Signal<(T1,T2), NoError> in return .single((c1Next,c2Next)) } } } -private func push(navigation:NavigationViewController, controller:EmptyComposeController) -> Signal { - navigation.push(controller) +private func push(context: AccountContext, controller:EmptyComposeController) -> Signal { + context.sharedContext.bindings.rootNavigation().push(controller) return controller.onComplete.get() } -private func push(navigation:NavigationViewController, controller:EmptyComposeController, input:I) -> Signal { - navigation.push(controller) +private func push(context: AccountContext, controller:EmptyComposeController, input:I) -> Signal { + context.sharedContext.bindings.rootNavigation().push(controller) controller.restart(with: ComposeState(input)) return controller.onComplete.get() } -private func execute(navigation:NavigationViewController, _ c1:EmptyComposeController, _ c2: EmptyComposeController, _ c3: EmptyComposeController) -> Signal { +private func execute(context: AccountContext, _ c1: @escaping () -> EmptyComposeController, _ c2: @escaping() -> EmptyComposeController, _ c3: @escaping() -> EmptyComposeController) -> Signal { - return push(navigation: navigation, controller: c1) |> mapToSignal { (c1Next) -> Signal in - return push(navigation: navigation, controller: c2, input:c1Next) |> mapToSignal{ (c2Next) -> Signal in - return push(navigation: navigation, controller: c3, input:c2Next) |> mapToSignal{ (c3Next) -> Signal in + return push(context: context, controller: c1()) |> mapToSignal { (c1Next) -> Signal in + return push(context: context, controller: c2(), input:c1Next) |> mapToSignal{ (c2Next) -> Signal in + return push(context: context, controller: c3(), input:c2Next) |> mapToSignal{ (c3Next) -> Signal in return .single(c3Next) } } diff --git a/Telegram-Mac/ComposeViewController.swift b/Telegram-Mac/ComposeViewController.swift index e2f1a496b6..944a18838e 100644 --- a/Telegram-Mac/ComposeViewController.swift +++ b/Telegram-Mac/ComposeViewController.swift @@ -8,8 +8,9 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac -import TelegramCoreMac +import SwiftSignalKit +import TelegramCore + struct ComposeTitles { let center:String let done:String @@ -31,13 +32,13 @@ class ComposeViewController: EmptyComposeController where V: N } override func executeReturn() -> Void { - onCancel.set(Signal.single(Void())) + onCancel.set(Signal.single(Void())) super.executeReturn() } override func requestUpdateRightBar() { super.requestUpdateRightBar() - (self.rightBarView as? TextButtonBarView)?.button.style = navigationButtonStyle + rightBarView.style = navigationButtonStyle } public override func returnKeyAction() -> KeyHandlerResult { @@ -47,7 +48,7 @@ class ComposeViewController: EmptyComposeController where V: N func nextEnabled(_ enable:Bool) { self.enableNext = enable - (self.rightBarView as? TextButtonBarView)?.button.isEnabled = enable + rightBarView.isEnabled = enable } func executeNext() -> Void { @@ -62,15 +63,15 @@ class ComposeViewController: EmptyComposeController where V: N super.loadView() setCenterTitle(titles.center) - (self.rightBarView as? TextButtonBarView)?.button.set(handler:{ [weak self] _ in + self.rightBarView.set(handler:{ [weak self] _ in self?.executeNext() }, for: .Click) } - public init(titles:ComposeTitles, account:Account) { + public init(titles:ComposeTitles, context: AccountContext) { self.titles = titles - super.init(account) + super.init(context) } } diff --git a/Telegram-Mac/Confetti.swift b/Telegram-Mac/Confetti.swift new file mode 100644 index 0000000000..3e146ce194 --- /dev/null +++ b/Telegram-Mac/Confetti.swift @@ -0,0 +1,369 @@ +// +// Confetti.swift +// Telegram +// +// Created by Mikhail Filimonov on 09.01.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import CoreGraphics +import QuartzCore + + +private enum Colors { + static var red: NSColor { + return theme.colors.redUI + } + static var blue: NSColor { + return theme.colors.accent + } + static var green: NSColor { + return theme.colors.greenUI + } + static var yellow: NSColor { + return theme.colors.peerAvatarOrangeTop + } +} + +private enum Images { + static let box = NSImage(named: "Confetti_Box")! + static let triangle = NSImage(named: "Confetti_Triangle")! + static let circle = NSImage(named: "Confetti_Circle")! + static let swirl = NSImage(named: "Confetti_Spiral")! +} + +private let colors:[NSColor] = [ + Colors.red, + Colors.blue, + Colors.green, + Colors.yellow +] + +private let images:[NSImage] = [ + Images.box, + Images.triangle, + Images.circle, + Images.swirl +] + +private let velocities:[Int] = [ + 150, + 135, + 200, + 250 +] + +private func getRandomVelocity() -> Int { + return velocities[getRandomNumber()] * 2 +} + +private func getRandomNumber() -> Int { + return Int(arc4random_uniform(4)) +} + +private func getNextColor(i:Int) -> CGColor { + if i <= 4 { + return colors[0].cgColor + } else if i <= 8 { + return colors[1].cgColor + } else if i <= 12 { + return colors[2].cgColor + } else { + return colors[3].cgColor + } +} + +private func getNextImage(i:Int) -> NSImage { + return images[i % 4] +} + + +func PlayConfetti(for window: Window, playEffect: Bool = false) { + let contentView = window.contentView! + + contentView.addSubview(ConfettiView(frame: contentView.bounds)) + +// +// let rightBottomView = View(frame: contentView.bounds) +// rightBottomView.isEventLess = true +// let rightEmitter = CAEmitterLayer() +// rightEmitter.emitterPosition = CGPoint(x: contentView.frame.size.width , y: contentView.frame.size.height) +// rightEmitter.emitterShape = .point +// rightEmitter.emitterSize = CGSize(width: contentView.frame.size.width, height: 2.0) +// rightEmitter.emitterCells = generateEmitterCells(left: false) +// +// rightBottomView.layer = rightEmitter +// contentView.addSubview(rightBottomView) +// +// let leftBottomView = View(frame: contentView.bounds) +// leftBottomView.isEventLess = true +// let leftEmitter = CAEmitterLayer() +// leftEmitter.emitterPosition = CGPoint(x: 0, y: contentView.frame.size.height) +// leftEmitter.emitterShape = .point +// leftEmitter.emitterSize = CGSize(width: contentView.frame.size.width, height: 2.0) +// leftEmitter.emitterCells = generateEmitterCells(left: true) +// +// leftBottomView.layer = leftEmitter +// contentView.addSubview(leftBottomView) +// +// +// delay(0.1, closure: { +// rightEmitter.birthRate = 0 +// leftEmitter.birthRate = 0 +// }) +// +// delay(2.0, closure: { +// rightBottomView.removeFromSuperview() +// leftBottomView.removeFromSuperview() +// }) +} +private func generateEmitterCells(left: Bool) -> [CAEmitterCell] { + var cells:[CAEmitterCell] = [CAEmitterCell]() + for index in 0 ..< 16 { + let cell = CAEmitterCell() + cell.birthRate = 20 + cell.lifetime = 2.0 + cell.lifetimeRange = 0 + cell.velocity = CGFloat(getRandomVelocity()) * 1.5 + cell.velocityRange = -CGFloat(arc4random() % 300) + + cell.alphaSpeed = -1.0/4.0 + cell.alphaRange = cell.lifetime * cell.alphaSpeed + + // cell.emissionRange = CGFloat.pi / 8 + // cell.emissionLongitude = CGFloat.pi * 2 + + cell.emissionLongitude = left ? -60 * (.pi / 180) : CGFloat(-Double.pi + 1.0) + cell.emissionRange = 30 * (.pi / 180) + cell.yAcceleration = max(400, CGFloat(arc4random() % 1000)) + cell.spin = max(3.5, CGFloat(arc4random() % 14)) + cell.spinRange = 10 + cell.color = getNextColor(i: index) + cell.contents = getNextImage(i: index).cgImage(forProposedRect: nil, context: nil, hints: nil) + cell.scaleRange = 0.25 + cell.scale = 0.1 + cells.append(cell) + } + return cells +} + + + + +private struct Vector2 { + var x: Float + var y: Float +} + +private final class NullActionClass: NSObject, CAAction { + @objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) { + } +} + +private let nullAction = NullActionClass() + +private final class ParticleLayer: CALayer { + let mass: Float + var velocity: Vector2 + var angularVelocity: Float + var rotationAngle: Float = 0.0 + + init(image: CGImage, size: CGSize, position: CGPoint, mass: Float, velocity: Vector2, angularVelocity: Float) { + self.mass = mass + self.velocity = velocity + self.angularVelocity = angularVelocity + + super.init() + + self.contents = image + self.bounds = CGRect(origin: CGPoint(), size: size) + self.position = position + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func action(forKey event: String) -> CAAction? { + return nullAction + } +} + +final class ConfettiView: View { + private var particles: [ParticleLayer] = [] + private var displayLink: ConstantDisplayLinkAnimator? + + private var localTime: Float = 0.0 + + + required init(frame: CGRect) { + super.init(frame: frame) + + self.isEventLess = true + + let colors: [NSColor] = ([ + 0x56CE6B, + 0xCD89D0, + 0x1E9AFF, + 0xFF8724 + ] as [UInt32]).map(NSColor.init(rgb:)) + let imageSize = CGSize(width: 8.0, height: 8.0) + var images: [(CGImage, CGSize)] = [] + for imageType in 0 ..< 2 { + for color in colors { + if imageType == 0 { + images.append((generateFilledCircleImage(diameter: imageSize.width, color: color), imageSize)) + } else { + let spriteSize = CGSize(width: 2.0, height: 6.0) + images.append((generateImage(spriteSize, opaque: false, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(color.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.width))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: size.height - size.width), size: CGSize(width: size.width, height: size.width))) + context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.width / 2.0), size: CGSize(width: size.width, height: size.height - size.width))) + })!, spriteSize)) + } + } + } + let imageCount = images.count + + let originXRange = 0 ..< Int(frame.width) + let originYRange = Int(-frame.height) ..< Int(0) + let topMassRange: Range = 40.0 ..< 50.0 + let velocityYRange = Float(3.0) ..< Float(5.0) + let angularVelocityRange = Float(1.0) ..< Float(6.0) + let sizeVariation = Float(0.8) ..< Float(1.6) + + for i in 0 ..< 70 { + let (image, size) = images[i % imageCount] + let sizeScale = CGFloat(Float.random(in: sizeVariation)) + let particle = ParticleLayer(image: image, size: CGSize(width: size.width * sizeScale, height: size.height * sizeScale), position: CGPoint(x: CGFloat(Int.random(in: originXRange)), y: CGFloat(Int.random(in: originYRange))), mass: Float.random(in: topMassRange), velocity: Vector2(x: 0.0, y: Float.random(in: velocityYRange)), angularVelocity: Float.random(in: angularVelocityRange)) + self.particles.append(particle) + self.layer?.addSublayer(particle) + } + + let sideMassRange: Range = 100.0 ..< 110.0 + let sideOriginYBase: Float = Float(frame.size.height * 8.5 / 10.0) + let sideOriginYVariation: Float = Float(frame.size.height / 12.0) + let sideOriginYRange = Float(sideOriginYBase - sideOriginYVariation) ..< Float(sideOriginYBase + sideOriginYVariation) + let sideOriginXRange = Float(0.0) ..< Float(100.0) + let sideOriginVelocityValueRange = Float(1.1) ..< Float(1.6) + let sideOriginVelocityValueScaling: Float = 1200.0 + let sideOriginVelocityBase: Float = Float.pi / 2.0 + atanf(Float(CGFloat(sideOriginYBase) / (frame.size.width * 0.8))) + let sideOriginVelocityVariation: Float = 0.2 + let sideOriginVelocityAngleRange = Float(sideOriginVelocityBase - sideOriginVelocityVariation) ..< Float(sideOriginVelocityBase + sideOriginVelocityVariation) + + for sideIndex in 0 ..< 2 { + let sideSign: Float = sideIndex == 0 ? 1.0 : -1.0 + let originX: CGFloat = sideIndex == 0 ? -5.0 : (frame.width + 5.0) + for i in 0 ..< 40 { + let offsetX = CGFloat(Float.random(in: sideOriginXRange) * (-sideSign)) + let velocityValue = Float.random(in: sideOriginVelocityValueRange) * sideOriginVelocityValueScaling + let velocityAngle = Float.random(in: sideOriginVelocityAngleRange) + let velocityX = sideSign * velocityValue * sinf(velocityAngle) + let velocityY = velocityValue * cosf(velocityAngle) + let (image, size) = images[i % imageCount] + let sizeScale = CGFloat(Float.random(in: sizeVariation)) + let particle = ParticleLayer(image: image, size: CGSize(width: size.width * sizeScale, height: size.height * sizeScale), position: CGPoint(x: originX + offsetX, y: CGFloat(Float.random(in: sideOriginYRange))), mass: Float.random(in: sideMassRange), velocity: Vector2(x: velocityX, y: velocityY), angularVelocity: Float.random(in: angularVelocityRange)) + self.particles.append(particle) + self.layer?.addSublayer(particle) + } + } + + self.displayLink = ConstantDisplayLinkAnimator(update: { [weak self] in + self?.step() + }) + + self.displayLink?.isPaused = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func step() { + var haveParticlesAboveGround = false + let minPositionY: CGFloat = 0.0 + let maxPositionY = self.bounds.height + 30.0 + let minDampingX: CGFloat = 40.0 + let maxDampingX: CGFloat = self.bounds.width - 40.0 + let centerX: CGFloat = self.bounds.width / 2.0 + let currentTime = self.localTime + let dt: Float = 1.0 / 60.0 + let slowdownDt: Float + let slowdownStart: Float = 0.27 + let slowdownDuration: Float = 0.9 + let damping: Float + if currentTime >= slowdownStart && currentTime <= slowdownStart + slowdownDuration { + let slowdownTimestamp: Float = currentTime - slowdownStart + + let slowdownRampInDuration: Float = 0.15 + let slowdownRampOutDuration: Float = 0.5 + let slowdownTransition: Float + if slowdownTimestamp < slowdownRampInDuration { + slowdownTransition = slowdownTimestamp / slowdownRampInDuration + } else if slowdownTimestamp >= slowdownDuration - slowdownRampOutDuration { + let reverseTransition = (slowdownTimestamp - (slowdownDuration - slowdownRampOutDuration)) / slowdownRampOutDuration + slowdownTransition = 1.0 - reverseTransition + } else { + slowdownTransition = 1.0 + } + + let slowdownFactor: Float = 0.3 * slowdownTransition + 1.0 * (1.0 - slowdownTransition) + slowdownDt = dt * slowdownFactor + let dampingFactor: Float = 0.94 * slowdownTransition + 1.0 * (1.0 - slowdownTransition) + damping = dampingFactor + } else { + slowdownDt = dt + damping = 1.0 + } + self.localTime += 1.0 / 60.0 + + let g: Vector2 = Vector2(x: 0.0, y: 9.8) + CATransaction.begin() + CATransaction.setDisableActions(true) + var turbulenceVariation: [Float] = [] + for _ in 0 ..< 20 { + turbulenceVariation.append(Float.random(in: -9.0 ..< 9.0)) + } + let turbulenceVariationCount = turbulenceVariation.count + var index = 0 + for particle in self.particles { + var position = particle.position + + let localDt: Float = slowdownDt + + position.x += CGFloat(particle.velocity.x * localDt) + position.y += CGFloat(particle.velocity.y * localDt) + particle.position = position + + particle.rotationAngle += particle.angularVelocity * localDt + particle.transform = CATransform3DMakeRotation(CGFloat(particle.rotationAngle), 0.0, 0.0, 1.0) + + let acceleration = g + + var velocity = particle.velocity + velocity.x += acceleration.x * particle.mass * localDt + velocity.y += acceleration.y * particle.mass * localDt + velocity.x += turbulenceVariation[index % turbulenceVariationCount] + if position.y > minPositionY { + velocity.x *= damping + velocity.y *= damping + } + particle.velocity = velocity + + index += 1 + + if position.y < maxPositionY { + haveParticlesAboveGround = true + } + } + CATransaction.commit() + if !haveParticlesAboveGround { + self.displayLink?.isPaused = true + self.removeFromSuperview() + } + } +} diff --git a/Telegram-Mac/Config.swift b/Telegram-Mac/Config.swift new file mode 100644 index 0000000000..12309dee4e --- /dev/null +++ b/Telegram-Mac/Config.swift @@ -0,0 +1,98 @@ +final class ApiEnvironment { + static var apiId:Int32 { + return 9 + } + static var apiHash:String { + return "3975f648bb682ee889f35483bc618d1c" + } + + static var bundleId: String { + return "ru.keepcoder.Telegram" + } + static var teamId: String { + return "6N38VWS5BX" + } + + static var containerURL: URL? { + let appGroupName = ApiEnvironment.group + let containerUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)?.appendingPathComponent(prefix) + if let containerUrl = containerUrl { + try? FileManager.default.createDirectory(at: containerUrl, withIntermediateDirectories: true, attributes: nil) + return containerUrl + } + return nil + } + + static func migrate() { + if let containerURL = containerURL, let legacy = legacyContainerURL, let sequence = FileManager.default.enumerator(atPath: legacy.path) { + let contents = try? FileManager.default.contentsOfDirectory(at: containerURL, includingPropertiesForKeys: nil, options: []) + if let contents = contents, !contents.isEmpty { + return + } + for value in sequence { + if let value = value as? String { + if !prefixList.contains(value) { + try? FileManager.default.moveItem(at: legacy.appendingPathComponent(value), to: containerURL.appendingPathComponent(value)) + } + } + } + } + } + + static var legacyContainerURL: URL? { + let appGroupName = ApiEnvironment.group + let containerUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName) + return containerUrl + } + + static var group: String { + return teamId + "." + bundleId + } + + static var appData: Data { + let apiData = evaluateApiData() ?? "" + let dict:[String: String] = ["bundleId": bundleId, "data": apiData] + return try! JSONSerialization.data(withJSONObject: dict, options: []) + } + static var language: String { + return "macos" + } + + static var prefixList:[String] { + return ["debug", "stable", "appstore", "beta"] + } + + static var prefix: String { + var prefix: String = "" + #if DEBUG + prefix = "debug" + #elseif STABLE + prefix = "stable" + #elseif APP_STORE + prefix = "appstore" + #else + prefix = "beta" + #endif + return prefix + } + + static var version: String { + var suffix: String = "" + #if STABLE + suffix = "STABLE" + #elseif APP_STORE + suffix = "APPSTORE" + #elseif ALPHA + suffix = "ALPHA" + #elseif GITHUB + suffix = "GITHUB" + #else + suffix = "BETA" + #endif + let shortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] ?? "" + return "\(shortVersion) \(suffix)" + } +} + + + diff --git a/Telegram-Mac/ContactsController.swift b/Telegram-Mac/ContactsController.swift index 7409526bf1..86eadd726a 100644 --- a/Telegram-Mac/ContactsController.swift +++ b/Telegram-Mac/ContactsController.swift @@ -8,23 +8,18 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac -import PostboxMac -import TelegramCoreMac +import SwiftSignalKit +import Postbox +import TelegramCore + private enum ContactsControllerEntryId: Hashable { - case vcard - case separator case peerId(Int64) case addContact var hashValue: Int { switch self { - case .vcard: - return 1 - case .separator: - return 2 case .addContact: - return 3 + return 0 case let .peerId(peerId): return peerId.hashValue @@ -32,91 +27,45 @@ private enum ContactsControllerEntryId: Hashable { } } -private func <(lhs: ContactsControllerEntryId, rhs: ContactsControllerEntryId) -> Bool { - return lhs.hashValue < rhs.hashValue -} - -private func ==(lhs: ContactsControllerEntryId, rhs: ContactsControllerEntryId) -> Bool { - switch lhs { - case .vcard: - switch rhs { - case .vcard: - return true - default: - return false - } - case .separator: - switch rhs { - case .separator: - return true - default: - return false - } - case .addContact: - switch rhs { - case .addContact: - return true - default: - return false - } - case let .peerId(lhsId): - switch rhs { - case let .peerId(rhsId): - return lhsId == rhsId - default: - return false - } - } -} private enum ContactsEntry: Comparable, Identifiable { - case vcard(Peer) - case separator(String) - case peer(Peer, PeerPresence?) + case peer(Peer, PeerPresence?, Int32) case addContact var stableId: ContactsControllerEntryId { switch self { - case .vcard: - return .vcard - case .separator: - return .separator case .addContact: return .addContact - case let .peer(peer,_): + case let .peer(peer,_, _): return .peerId(peer.id.toInt64()) } } + + var index: Int32 { + switch self { + case .addContact: + return -1 + case let .peer(_, _, index): + return index + } + } } private func ==(lhs: ContactsEntry, rhs: ContactsEntry) -> Bool { switch lhs { - - case let .vcard(lhsPeer): - switch rhs { - case let .vcard(rhsPeer): - return lhsPeer.id == rhsPeer.id - default: - return false - } - case let .separator(ls): - switch rhs { - case let .separator(rs): - return ls == rs - default: - return false - } case .addContact: - switch rhs { - case .addContact: + if case .addContact = rhs { return true - default: + } else { return false } - case let .peer(lhsPeer, lhsPresence): + case let .peer(lhsPeer, lhsPresence, lhsIndex): switch rhs { - case let .peer(rhsPeer, rhsPresence): - if lhsPeer.id != rhsPeer.id { + case let .peer(rhsPeer, rhsPresence, rhsIndex): + if !lhsPeer.isEqual(rhsPeer) { + return false + } + if lhsIndex != rhsIndex { return false } if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { @@ -134,62 +83,44 @@ private func ==(lhs: ContactsEntry, rhs: ContactsEntry) -> Bool { } private func <(lhs: ContactsEntry, rhs: ContactsEntry) -> Bool { - switch lhs { - case .vcard(_): - return false - case .separator: - switch rhs { - case .vcard, .separator: - return true - case .peer: - return false - case .addContact: - return false - } - case let .peer(lhsPeer, lhsPresence): - switch rhs { - case .separator, .vcard, .addContact: - return true - case let .peer(rhsPeer, rhsPresence): + return lhs.index < rhs.index +} + + +private func entriesForView(_ view: ContactPeersView) -> [ContactsEntry] { + var entries: [ContactsEntry] = [] + if let accountPeer = view.accountPeer { + + entries.append(.addContact) + + var peerIds: Set = Set() + var index: Int32 = 0 + + let orderedPeers = view.peers.sorted(by: { lhsPeer, rhsPeer in + let lhsPresence = view.peerPresences[lhsPeer.id] + let rhsPresence = view.peerPresences[rhsPeer.id] if let lhsPresence = lhsPresence as? TelegramUserPresence, let rhsPresence = rhsPresence as? TelegramUserPresence { if lhsPresence.status < rhsPresence.status { - return true - } else if lhsPresence.status > rhsPresence.status { return false + } else if lhsPresence.status > rhsPresence.status { + return true } } else if let _ = lhsPresence { - return false - } else if let _ = rhsPresence { return true + } else if let _ = rhsPresence { + return false } return lhsPeer.id < rhsPeer.id - } - case .addContact: - switch rhs { - case .vcard, .separator, .addContact: - return true - case .peer: - return false - } - } -} - -private func entriesForView(_ view: ContactPeersView) -> [ContactsEntry] { - var entries: [ContactsEntry] = [] - if let accountPeer = view.accountPeer { + }) - for peer in view.peers { - if !peer.isEqual(accountPeer) { - entries.append(.peer(peer,view.peerPresences[peer.id])) + for peer in orderedPeers { + if !peer.isEqual(accountPeer), !peerIds.contains(peer.id) { + entries.append(.peer(peer, view.peerPresences[peer.id], index)) + peerIds.insert(peer.id) + index += 1 } } - entries.append(.addContact) - entries.append(.separator(tr(.contactsContacsSeparator))) - entries.append(.vcard(accountPeer)) - - entries.sort() - } return entries @@ -202,55 +133,89 @@ private final class ContactsArguments { } } -fileprivate func prepareEntries(from:[AppearanceWrapperEntry]?, to:[AppearanceWrapperEntry], account:Account, initialSize:NSSize, arguments: ContactsArguments, animated:Bool) -> Signal { +fileprivate func prepareEntries(from:[AppearanceWrapperEntry]?, to:[AppearanceWrapperEntry], context: AccountContext, initialSize:NSSize, arguments: ContactsArguments, animated:Bool) -> Signal { return Signal { subscriber in - let (deleted,inserted,updated) = proccessEntries(from, right: to, { (entry) -> TableRowItem in - - var item:TableRowItem + + func makeItem(_ entry: ContactsEntry) -> TableRowItem { + let item:TableRowItem - switch entry.entry { - case let .vcard(peer): - - var status:String? = nil - let phone = (peer as! TelegramUser).phone - if let phone = phone { - status = formatPhoneNumber( phone ) - } - - item = ShortPeerRowItem(initialSize, peer: peer, account:account, height:60, photoSize:NSMakeSize(50,50), status: status, borderType: [.Right], drawCustomSeparator:false) - case let .peer(peer, presence): - + switch entry { + case let .peer(peer, presence, _): var color:NSColor = theme.colors.grayText - var string:String = tr(.peerStatusRecently) + var string:String = L10n.peerStatusRecently if let presence = presence as? TelegramUserPresence { let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - (string, _, color) = stringAndActivityForUserPresence(presence, relativeTo: Int32(timestamp)) + (string, _, color) = stringAndActivityForUserPresence(presence, timeDifference: context.timeDifference, relativeTo: Int32(timestamp)) } - - item = ShortPeerRowItem(initialSize, peer: peer, account:account,statusStyle: ControlStyle(foregroundColor:color), status: string, borderType: [.Right]) - case let .separator(str): - item = SeparatorRowItem(initialSize, 1, string: str.uppercased()) + item = ShortPeerRowItem(initialSize, peer: peer, account: context.account, stableId: entry.stableId,statusStyle: ControlStyle(foregroundColor:color), status: string, borderType: [.Right], contextMenuItems: { + return .single([ContextMenuItem(L10n.chatListContextPreview, handler: { + showModal(with: ChatModalPreviewController(location: .peer(peer.id), context: context), for: context.window) + })]) + }) case .addContact: - return AddContactTableItem(initialSize, stableId: entry.stableId, addContact: { + item = AddContactTableItem(initialSize, stableId: entry.stableId, addContact: { arguments.addContact() }) } + return item + } + + var cancelled = false + + + if Thread.isMainThread { + var initialIndex:Int = 0 + var height:CGFloat = 0 + var firstInsertion:[(Int, TableRowItem)] = [] + let entries = Array(to) - let _ = item.makeSize(initialSize.width) + let index:Int = 0 + + for i in index ..< entries.count { + let item = makeItem(entries[i].entry) + height += item.height + firstInsertion.append((i, item)) + if initialSize.height < height { + break + } + } - return item + initialIndex = firstInsertion.count + subscriber.putNext(TableUpdateTransition(deleted: [], inserted: firstInsertion, updated: [], state: .none(nil))) - }) - - subscriber.putNext(TableUpdateTransition(deleted: deleted, inserted: inserted, updated:updated, animated:animated, state: .none(nil))) - subscriber.putCompletion() - - return EmptyDisposable + prepareQueue.async { + if !cancelled { + + + var insertions:[(Int, TableRowItem)] = [] + let updates:[(Int, TableRowItem)] = [] - + for i in initialIndex ..< entries.count { + let item:TableRowItem + item = makeItem(entries[i].entry) + insertions.append((i, item)) + } + + + subscriber.putNext(TableUpdateTransition(deleted: [], inserted: insertions, updated: updates, state: .none(nil))) + subscriber.putCompletion() + } + } + } else { + let (deleted,inserted,updated) = proccessEntriesWithoutReverse(from, right: to, { entry -> TableRowItem in + return makeItem(entry.entry) + }) + + subscriber.putNext(TableUpdateTransition(deleted: deleted, inserted: inserted, updated:updated, animated:animated, state: .none(nil))) + subscriber.putCompletion() + } + + return ActionDisposable { + cancelled = true + } } } @@ -260,7 +225,7 @@ class ContactsController: PeersListController { private var previousEntries:Atomic<[AppearanceWrapperEntry]?> = Atomic(value:nil) private let index: PeerNameIndex = .lastNameFirst - + private let disposable = MetaDisposable() override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) @@ -271,62 +236,77 @@ class ContactsController: PeersListController { } - override func loadView() { - super.loadView() + override func viewDidLoad() { + super.viewDidLoad() backgroundColor = theme.colors.background } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - genericView.tableView.startMerge() - genericView.tableView.clipView.scroll(to: NSZeroPoint) - let account = self.account + let context = self.context let previousEntries = self.previousEntries let initialSize = self.atomicSize let first:Atomic = Atomic(value:false) let arguments = ContactsArguments(addContact: { - showModal(with: AddContactModalController(account: account), for: mainWindow) + showModal(with: AddContactModalController(context), for: mainWindow) }) - let transition = combineLatest(account.postbox.contactPeersView(accountPeerId: account.peerId), appearanceSignal) |> deliverOn(prepareQueue) - |> mapToQueue { view, appearance -> Signal in + + let transition = combineLatest(queue: prepareQueue, context.account.postbox.contactPeersView(accountPeerId: context.peerId, includePresences: true), appearanceSignal) + |> mapToQueue { view, appearance -> Signal in let first:Bool = !first.swap(true) let entries = entriesForView(view).map({AppearanceWrapperEntry(entry: $0, appearance: appearance)}) - if first { - let subEntries = Array(entries.suffix(50)) - let firstSignal = prepareEntries(from: previousEntries.swap(subEntries), to: subEntries, account: account, initialSize: initialSize.modify({$0}), arguments: arguments, animated: !first) - let secondSignal = prepareEntries(from: previousEntries.swap(entries), to: entries, account: account, initialSize: initialSize.modify({$0}), arguments: arguments, animated: !first) - return firstSignal |> then(secondSignal) - } else { - return prepareEntries(from: previousEntries.swap(entries), to: entries, account: account, initialSize: initialSize.modify({$0}), arguments: arguments, animated: !first) - } + return prepareEntries(from: previousEntries.swap(entries), to: entries, context: context, initialSize: initialSize.modify({$0}), arguments: arguments, animated: !first) |> runOn(first ? .mainQueue() : prepareQueue) + } |> deliverOnMainQueue - genericView.tableView.merge(with: transition) + disposable.set(transition.start(next: { [weak self] transition in + self?.genericView.tableView.merge(with: transition) + self?.readyOnce() + })) + } - override func scrollup() { + override func scrollup(force: Bool = false) { genericView.tableView.scroll(to: .up(true)) } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - genericView.tableView.stopMerge() _ = previousEntries.swap(nil) + genericView.tableView.cancelSelection() genericView.tableView.removeAll() + genericView.tableView.documentView?.removeAllSubviews() + disposable.set(nil) } + deinit { + disposable.dispose() + } - init(_ account:Account) { - super.init(account) + init(_ context:AccountContext) { + super.init(context, searchOptions: [.chats]) } - override func selectionWillChange(row:Int, item:TableRowItem) -> Bool { + override func changeSelection(_ location: ChatLocation?) { + if let location = location { + switch location { + case let .peer(peerId): + genericView.tableView.changeSelection(stableId: ContactsControllerEntryId.peerId(peerId.toInt64())) + case .replyThread: + break + } + } else { + genericView.tableView.cancelSelection() + } + } + + override func selectionWillChange(row:Int, item:TableRowItem, byClick: Bool) -> Bool { if let item = item as? ShortPeerRowItem, let modalAction = navigationController?.modalAction { if !modalAction.isInvokable(for: item.peer) { modalAction.alertError(for: item.peer, with:window!) @@ -347,7 +327,14 @@ class ContactsController: PeersListController { navigation.controller.invokeNavigation(action: modalAction) } } else { - let chat:ChatController = ChatController(account: self.account, peerId:item.peer.id) + + let context = self.context + + _ = (context.globalPeerHandler.get() |> take(1)).start(next: { location in + context.globalPeerHandler.set(.single(location)) + }) + + let chat:ChatController = ChatController(context: self.context, chatLocation: .peer(item.peer.id)) navigation.push(chat) } diff --git a/Telegram-Mac/ContextClueRowItem.swift b/Telegram-Mac/ContextClueRowItem.swift index a6caa2e50d..daf245bbff 100644 --- a/Telegram-Mac/ContextClueRowItem.swift +++ b/Telegram-Mac/ContextClueRowItem.swift @@ -12,28 +12,32 @@ import TGUIKit class ContextClueRowItem: TableRowItem { private let _stableId:AnyHashable - let clue:EmojiClue - + let clues:[String] + var selectedIndex:Int? = nil + override var stableId: AnyHashable { return _stableId } - - fileprivate let clueLayout: TextViewLayout - fileprivate let emojiLayout: TextViewLayout - init(_ initialSize: NSSize, stableId:AnyHashable, clue: EmojiClue) { + fileprivate let context: AccountContext + fileprivate let canDisablePrediction: Bool + fileprivate let callback:((String)->Void)? + fileprivate let selected: String? + init(_ initialSize: NSSize, stableId:AnyHashable, context: AccountContext, clues: [String], selected: String?, canDisablePrediction: Bool, callback:((String)->Void)? = nil) { self._stableId = stableId - self.clue = clue - clueLayout = TextViewLayout(.initialize(string: clue.label, color: theme.colors.text, font: .normal(.title))) - emojiLayout = TextViewLayout(.initialize(string: clue.emoji, color: theme.colors.text, font: .normal(.title))) - emojiLayout.measure(width: .greatestFiniteMagnitude) + self.clues = clues + self.context = context + self.callback = callback + self.selected = selected + + if let selected = selected, let index = clues.firstIndex(of: selected) { + self.selectedIndex = index + } + + self.canDisablePrediction = canDisablePrediction super.init(initialSize) _ = makeSize(initialSize.width, oldWidth: 0) } - override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { - clueLayout.measure(width: width - 50) - return super.makeSize(width, oldWidth: oldWidth) - } override var height: CGFloat { return 40 @@ -45,63 +49,172 @@ class ContextClueRowItem: TableRowItem { } -private class ContextClueRowView : TableRowView { - private let clueTextView:TextView = TextView() - private let emojiTextView: TextView = TextView() +private final class ClueRowItem : TableRowItem { + private let _stableId = arc4random() + override var stableId: AnyHashable { + return _stableId + } + let layout: TextViewLayout + + init(_ initialSize: NSSize, clue: String) { + self.layout = TextViewLayout(.initialize(string: clue, color: nil, font: .normal(17))) + super.init(initialSize) + layout.measure(width: .greatestFiniteMagnitude) + } + + + override func viewClass() -> AnyClass { + return ClueRowView.self + } + + override var height: CGFloat { + return 40 + } + override var width: CGFloat { + return 40 + } +} + +private final class ClueRowView : HorizontalRowView { + private let textView: TextView = TextView() + private let containerView = View() required init(frame frameRect: NSRect) { super.init(frame: frameRect) - clueTextView.userInteractionEnabled = false - clueTextView.isSelectable = false - emojiTextView.userInteractionEnabled = false - emojiTextView.isSelectable = false - addSubview(clueTextView) - addSubview(emojiTextView) + textView.userInteractionEnabled = false + textView.isSelectable = false + addSubview(containerView) + addSubview(textView) + containerView.layer?.cornerRadius = .cornerRadius } override var backdorColor: NSColor { - if let item = item { - return item.isSelected ? theme.colors.blueSelect : theme.colors.background - } else { - return theme.colors.background + return theme.colors.background + } + + override func updateColors() { + super.updateColors() + containerView.backgroundColor = item?.isSelected == true ? theme.colors.accent : theme.colors.background + } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + if let item = item as? ClueRowItem { + textView.update(item.layout) + } + } + + override func layout() { + super.layout() + containerView.frame = NSMakeRect(4, 4, frame.width - 8, frame.height - 8) + textView.center() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private class ContextClueRowView : TableRowView, TableViewDelegate { + func selectionDidChange(row: Int, item: TableRowItem, byClick: Bool, isNew: Bool) { + if let clues = self.item as? ContextClueRowItem { + clues.selectedIndex = row + if byClick, let window = window as? Window { + if let callback = clues.callback { + callback(clues.clues[row]) + } else { + window.sendKeyEvent(.Return, modifierFlags: []) + } + } } } + func selectionWillChange(row: Int, item: TableRowItem, byClick: Bool) -> Bool { + return true + } + + func isSelectable(row: Int, item: TableRowItem) -> Bool { + return true + } + + func findGroupStableId(for stableId: AnyHashable) -> AnyHashable? { + return nil + } + private let button = ImageButton() + + private let tableView = HorizontalTableView(frame: NSZeroRect) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(tableView) + layerContentsRedrawPolicy = .onSetNeedsDisplay + tableView.delegate = self + addSubview(button) + + button.set(handler: { [weak self] _ in + self?.disablePrediction() + }, for: .Click) + } + + private func disablePrediction() { + guard let window = self.window as? Window, let item = item as? ContextClueRowItem else { return } + let sharedContext = item.context.sharedContext + confirm(for: window, information: L10n.generalSettingsEmojiPredictionDisableText, okTitle: L10n.generalSettingsEmojiPredictionDisable, successHandler: { _ in + _ = updateBaseAppSettingsInteractively(accountManager: sharedContext.accountManager, { current in + return current.withUpdatedPredictEmoji(false) + }).start() + }) + } + + + override var backdorColor: NSColor { + return theme.colors.background + } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func draw(_ layer: CALayer, in ctx: CGContext) { super.draw(layer, in: ctx) - - if let item = item { - if !item.isSelected { - ctx.setFillColor(theme.colors.border.cgColor) - ctx.fill(NSMakeRect(40, frame.height - .borderSize, frame.width - 20, .borderSize)) - } - } } override func layout() { super.layout() - clueTextView.update(clueTextView.layout) - clueTextView.centerY(x: 40) - - emojiTextView.update(emojiTextView.layout) - emojiTextView.centerY(x: 10) + tableView.frame = NSMakeRect(0, 0, frame.width - (button.isHidden ? 0 : button.frame.width), frame.height) + button.centerY(x: frame.width - button.frame.width) } override func updateColors() { super.updateColors() - self.emojiTextView.backgroundColor = backdorColor - self.clueTextView.backgroundColor = backdorColor } override func set(item: TableRowItem, animated: Bool) { super.set(item: item, animated: animated) + + + + button.set(image: theme.icons.disableEmojiPrediction, for: .Normal) + _ = button.sizeToFit(NSZeroSize, NSMakeSize(40, 40), thatFit: true) + + tableView.beginTableUpdates() + tableView.removeAll(redraw: true, animation: .none) if let item = item as? ContextClueRowItem { - clueTextView.update(item.clueLayout) - emojiTextView.update(item.emojiLayout) + + button.isHidden = !item.canDisablePrediction + + for clue in item.clues { + _ = tableView.addItem(item: ClueRowItem(bounds.size, clue: clue), animation: .none) + } + if let selectedIndex = item.selectedIndex { + let item = tableView.item(at: selectedIndex) + _ = tableView.select(item: item) + } + } + tableView.endTableUpdates() + + if let selectedItem = tableView.selectedItem() { + tableView.scroll(to: .center(id: selectedItem.stableId, innerId: nil, animated: animated, focus: .init(focus: false), inset: 0)) } + needsLayout = true } } diff --git a/Telegram-Mac/ContextCommandRowItem.swift b/Telegram-Mac/ContextCommandRowItem.swift index 6cdcb09c7f..bff6019c76 100644 --- a/Telegram-Mac/ContextCommandRowItem.swift +++ b/Telegram-Mac/ContextCommandRowItem.swift @@ -8,14 +8,15 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac +import Postbox +import TelegramCore + +import SwiftSignalKit class ContextCommandRowItem: TableRowItem { fileprivate let _stableId:Int64 - fileprivate let account:Account + fileprivate let account: Account let command:PeerCommand private let title:TextViewLayout @@ -35,8 +36,8 @@ class ContextCommandRowItem: TableRowItem { title = TextViewLayout(.initialize(string: "/" + command.command.text, color: theme.colors.text, font: .medium(.text)), maximumNumberOfLines: 1, truncationType: .end) desc = TextViewLayout(.initialize(string: command.command.description, color: theme.colors.grayText, font: .normal(.text)), maximumNumberOfLines: 1, truncationType: .end) - titleSelected = TextViewLayout(.initialize(string: "/" + command.command.text, color: .white, font: .medium(.text)), maximumNumberOfLines: 1, truncationType: .end) - descSelected = TextViewLayout(.initialize(string: command.command.description, color: .white, font: .normal(.text)), maximumNumberOfLines: 1, truncationType: .end) + titleSelected = TextViewLayout(.initialize(string: "/" + command.command.text, color: theme.colors.underSelectedColor, font: .medium(.text)), maximumNumberOfLines: 1, truncationType: .end) + descSelected = TextViewLayout(.initialize(string: command.command.description, color: theme.colors.underSelectedColor, font: .normal(.text)), maximumNumberOfLines: 1, truncationType: .end) super.init(initialSize) _ = makeSize(initialSize.width, oldWidth: initialSize.width) } @@ -50,11 +51,12 @@ class ContextCommandRowItem: TableRowItem { } override func makeSize(_ width: CGFloat, oldWidth:CGFloat) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) title.measure(width: width - 60) desc.measure(width: width - 60) titleSelected.measure(width: width - 60) descSelected.measure(width: width - 60) - return super.makeSize(width, oldWidth: oldWidth) + return success } var ctxTitle:TextViewLayout { @@ -75,6 +77,7 @@ class ContextCommandRowView : TableRowView { private let photoView:AvatarControl = AvatarControl(font: .avatar(.title)) required init(frame frameRect: NSRect) { super.init(frame: frameRect) + layerContentsRedrawPolicy = .onSetNeedsDisplay textView.userInteractionEnabled = false descView.userInteractionEnabled = false photoView.userInteractionEnabled = false @@ -87,8 +90,8 @@ class ContextCommandRowView : TableRowView { override func layout() { super.layout() if let item = item as? ContextCommandRowItem { - textView.update(item.ctxTitle, origin:NSMakePoint(50, floorToScreenPixels(frame.height / 2 - item.ctxTitle.layoutSize.height))) - descView.update(item.ctxDesc, origin:NSMakePoint(50, floorToScreenPixels(frame.height / 2))) + textView.update(item.ctxTitle, origin:NSMakePoint(50, floorToScreenPixels(backingScaleFactor, frame.height / 2 - item.ctxTitle.layoutSize.height))) + descView.update(item.ctxDesc, origin:NSMakePoint(50, floorToScreenPixels(backingScaleFactor, frame.height / 2))) } } @@ -98,7 +101,7 @@ class ContextCommandRowView : TableRowView { override var backdorColor: NSColor { if let item = item { - return item.isSelected ? theme.colors.blueSelect : theme.colors.background + return item.isSelected ? theme.colors.accentSelect : theme.colors.background } else { return theme.colors.background } diff --git a/Telegram-Mac/ContextHashtagRowItem.swift b/Telegram-Mac/ContextHashtagRowItem.swift new file mode 100644 index 0000000000..1883c20daa --- /dev/null +++ b/Telegram-Mac/ContextHashtagRowItem.swift @@ -0,0 +1,95 @@ +// +// ContextHashtagRowItem.swift +// Telegram +// +// Created by keepcoder on 24/10/2017. +// Copyright © 2017 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +class ContextHashtagRowItem: TableRowItem { + + let hashtag: String + fileprivate let selectedTextLayout: TextViewLayout + fileprivate let textLayout: TextViewLayout + init(_ initialSize: NSSize, hashtag:String) { + self.hashtag = hashtag + textLayout = TextViewLayout(.initialize(string: hashtag, color: theme.colors.text, font: .normal(.text)), maximumNumberOfLines: 1) + selectedTextLayout = TextViewLayout(.initialize(string: hashtag, color: .white, font: .normal(.text)), maximumNumberOfLines: 1) + super.init(initialSize) + _ = makeSize(initialSize.width, oldWidth: 0) + } + + override var height: CGFloat { + return 40 + } + + override var stableId: AnyHashable { + return "hashtag_\(hashtag)".hashValue + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) + textLayout.measure(width: width - 40) + selectedTextLayout.measure(width: width - 40) + return success + } + + override func viewClass() -> AnyClass { + return ContextHashtagRowView.self + } + +} + + +private class ContextHashtagRowView : TableRowView { + private let textView: TextView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + layerContentsRedrawPolicy = .onSetNeedsDisplay + textView.userInteractionEnabled = false + textView.isSelectable = false + addSubview(textView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + textView.centerY(x: 20) + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + super.draw(layer, in: ctx) + if let item = item, !item.isSelected, !item.isLast { + ctx.setFillColor(theme.colors.border.cgColor) + ctx.fill(NSMakeRect(20, frame.height - .borderSize, frame.width - 20, .borderSize)) + } + } + + override var backdorColor: NSColor { + if let item = item { + return item.isSelected ? theme.colors.accentSelect : theme.colors.background + } else { + return theme.colors.background + } + } + + override func updateColors() { + super.updateColors() + textView.backgroundColor = backdorColor + } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + + guard let item = item as? ContextHashtagRowItem else {return} + + textView.update(item.isSelected ? item.selectedTextLayout : item.textLayout) + needsLayout = true + } +} diff --git a/Telegram-Mac/ContextListRowItem.swift b/Telegram-Mac/ContextListRowItem.swift index 49f4ecca17..5febf21ddf 100644 --- a/Telegram-Mac/ContextListRowItem.swift +++ b/Telegram-Mac/ContextListRowItem.swift @@ -8,9 +8,10 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac +import TelegramCore + +import SwiftSignalKit +import Postbox @@ -19,83 +20,79 @@ class ContextListRowItem: TableRowItem { let result:ChatContextResult let results:ChatContextResultCollection private let _index:Int64 - let account:Account - let iconSignal:Signal<(TransformImageArguments)->DrawingContext?,Void> + let context: AccountContext + let iconSignal:Signal let arguments:TransformImageArguments? var textLayout:(TextNodeLayout, TextNode)? let capImage:CGImage? var fileResource:TelegramMediaResource? let chatInteraction:ChatInteraction var audioWrapper:APSingleWrapper? + private(set) var file: TelegramMediaFile? private var vClass:AnyClass = ContextListImageView.self private let text:NSAttributedString override var stableId: AnyHashable { return Int64(_index) } - init(_ initialSize: NSSize, _ results:ChatContextResultCollection, _ result:ChatContextResult, _ index:Int64, _ account:Account, _ chatInteraction:ChatInteraction) { + init(_ initialSize: NSSize, _ results:ChatContextResultCollection, _ result:ChatContextResult, _ index:Int64, _ context: AccountContext, _ chatInteraction:ChatInteraction) { self.result = result self.results = results self.chatInteraction = chatInteraction self._index = index - self.account = account - var imageResource: TelegramMediaResource? + self.context = context + var representation: TelegramMediaImageRepresentation? var iconText:NSAttributedString? = nil switch result { - case let .externalReference(_, _, title, description, url, thumbnailUrl, contentUrl, contentType, _, _, _): - if let thumbnailUrl = thumbnailUrl { - imageResource = HttpReferenceMediaResource(url: thumbnailUrl, size: nil) + case let .externalReference(values): + if let thumbnail = values.thumbnail { + representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(NSMakeSize(50, 50)), resource: thumbnail.resource, progressiveSizes: [], immediateThumbnailData: nil) } - if let contentUrl = contentUrl { - fileResource = HttpReferenceMediaResource(url: contentUrl, size: nil) - if let contentType = contentType { - if contentType.hasPrefix("audio") { - vClass = ContextListAudioView.self - audioWrapper = APSingleWrapper(resource: fileResource!, name: title, performer: description, id:result.maybeId) - } else if contentType == "video/mp4" { - vClass = ContextListGIFView.self - } + if let content = values.content { + if content.mimeType.hasPrefix("audio") { + vClass = ContextListAudioView.self + audioWrapper = APSingleWrapper(resource: content.resource, name: values.title, performer: values.description, duration: content.duration, id: result.maybeId) + } else if content.mimeType == "video/mp4" { + vClass = ContextListGIFView.self } } var selectedUrl: String? - if let url = url { + if let url = values.url { selectedUrl = url - } else if let contentUrl = contentUrl { - selectedUrl = contentUrl } if let selectedUrl = selectedUrl, let parsedUrl = URL(string: selectedUrl) { if let host = parsedUrl.host, !host.isEmpty { - iconText = NSAttributedString.initialize(string: host.substring(to: host.index(after: host.startIndex)).uppercased(), color: .white, font: .medium(.custom(25))) + iconText = NSAttributedString.initialize(string: host.substring(to: host.index(after: host.startIndex)).uppercased(), color: .white, font: .medium(25.0)) } } - case let .internalReference(_, _, title, description, image, file, _): + case let .internalReference(values): if let file = file { + self.file = file fileResource = file.resource if file.isMusic || file.isVoice { vClass = ContextListAudioView.self - audioWrapper = APSingleWrapper(resource: fileResource!, name: title, performer: description, id:result.maybeId) + audioWrapper = APSingleWrapper(resource: fileResource!, name: values.title, performer: values.description, duration: file.duration, id: result.maybeId) } else if file.isVideo && file.isAnimated { vClass = ContextListGIFView.self } } - if let image = image { - imageResource = smallestImageRepresentation(image.representations)?.resource + if let image = values.image { + representation = smallestImageRepresentation(image.representations) } else if let file = file { - imageResource = smallestImageRepresentation(file.previewRepresentations)?.resource + representation = smallestImageRepresentation(file.previewRepresentations) } } - if let imageResource = imageResource { - let iconRepresentation = TelegramMediaImageRepresentation(dimensions: CGSize(width: 55.0, height: 55.0), resource: imageResource) - let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [iconRepresentation]) - iconSignal = chatWebpageSnippetPhoto(account: account, photo: tmpImage, scale: 2.0, small:true) + if let representation = representation { + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [representation], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + iconSignal = chatWebpageSnippetPhoto(account: context.account, imageReference: ImageMediaReference.standalone(media: tmpImage), scale: 2.0, small:true) - let iconSize = iconRepresentation.dimensions.aspectFilled(CGSize(width: 50, height: 50)) + let iconSize = representation.dimensions.size.aspectFilled(CGSize(width: 50, height: 50)) let imageCorners = ImageCorners(topLeft: .Corner(2.0), topRight: .Corner(2.0), bottomLeft: .Corner(2.0), bottomRight: .Corner(2.0)) - arguments = TransformImageArguments(corners: imageCorners, imageSize: iconSize, boundingSize: iconSize, intrinsicInsets: NSEdgeInsets()) + arguments = TransformImageArguments(corners: imageCorners, imageSize: representation.dimensions.size, boundingSize: iconSize, intrinsicInsets: NSEdgeInsets()) iconText = nil } else { arguments = nil @@ -104,7 +101,7 @@ class ContextListRowItem: TableRowItem { if iconText == nil { if let title = result.title, !title.isEmpty { let titleText = title.substring(to: title.index(after: title.startIndex)).uppercased() - iconText = NSAttributedString.initialize(string: titleText, color: .white, font: .medium(.custom(25))) + iconText = .initialize(string: titleText, color: .white, font: .medium(25.0)) } } } @@ -131,12 +128,13 @@ class ContextListRowItem: TableRowItem { self.text = attr.copy() as! NSAttributedString super.init(initialSize) - prepare(isSelected) + _ = makeSize(initialSize.width, oldWidth: 0) } override func makeSize(_ width: CGFloat, oldWidth:CGFloat) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) prepare(isSelected) - return super.makeSize(width, oldWidth: oldWidth) + return success } override func prepare(_ selected: Bool) { @@ -157,7 +155,7 @@ class ContextListRowItem: TableRowItem { class ContextListRowView : TableRowView { override var backdorColor: NSColor { - return item?.isSelected ?? false ? theme.colors.blueSelect : theme.colors.background + return item?.isSelected ?? false ? theme.colors.accentSelect : theme.colors.background } override func draw(_ layer: CALayer, in ctx: CGContext) { super.draw(layer, in: ctx) @@ -171,18 +169,25 @@ class ContextListRowView : TableRowView { if let layout = item.textLayout { let f = focus(layout.0.size) - layout.1.draw(NSMakeRect(item.textInset.left, f.minY, layout.0.size.width, layout.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor) + layout.1.draw(NSMakeRect(item.textInset.left, f.minY, layout.0.size.width, layout.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) } - needsLayout = true + } } + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + needsDisplay = true + needsLayout = true + } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } required init(frame frameRect: NSRect) { super.init(frame: frameRect) + layerContentsRedrawPolicy = .onSetNeedsDisplay } } @@ -190,12 +195,13 @@ class ContextListImageView : TableRowView { let image:TransformImageView = TransformImageView() required init(frame frameRect: NSRect) { super.init(frame:frameRect) + layerContentsRedrawPolicy = .onSetNeedsDisplay image.setFrameSize(NSMakeSize(50, 50)) addSubview(image) } override var backdorColor: NSColor { - return item?.isSelected ?? false ? theme.colors.blueSelect : theme.colors.background + return item?.isSelected ?? false ? theme.colors.accentSelect : theme.colors.background } override func layout() { @@ -218,7 +224,7 @@ class ContextListImageView : TableRowView { if let layout = item.textLayout { let f = focus(layout.0.size) - layout.1.draw(NSMakeRect(item.textInset.left, f.minY, layout.0.size.width, layout.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor) + layout.1.draw(NSMakeRect(item.textInset.left, f.minY, layout.0.size.width, layout.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) } needsLayout = true } @@ -232,9 +238,10 @@ class ContextListImageView : TableRowView { if let capImage = item.capImage { self.image.layer?.contents = capImage } else { - image.setSignal(account: item.account, signal: item.iconSignal) + image.setSignal( item.iconSignal) } } + needsDisplay = true } required init?(coder: NSCoder) { @@ -264,10 +271,10 @@ class ContextListGIFView : ContextListRowView { override func set(item: TableRowItem, animated: Bool) { let updated = self.item != item - super.set(item: item) + super.set(item: item, animated: animated) - if let item = item as? ContextListRowItem, updated, let resource = item.fileResource { - player.update(with: resource, size: NSMakeSize(50,50), viewSize: NSMakeSize(50,50), account: item.account, table: item.table, iconSignal: item.iconSignal) + if let item = item as? ContextListRowItem, updated, let file = item.file { + player.update(with: FileMediaReference.standalone(media: file), size: NSMakeSize(50,50), viewSize: NSMakeSize(50,50), context: item.context, table: item.table, iconSignal: item.iconSignal) player.needsLayout = true } } @@ -286,6 +293,7 @@ class ContextListAudioView : ContextListRowView, APDelegate { progressView.fetchControls = FetchControls(fetch: { [weak self] in self?.checkOperation() }) + layerContentsRedrawPolicy = .onSetNeedsDisplay addSubview(progressView) } @@ -297,10 +305,9 @@ class ContextListAudioView : ContextListRowView, APDelegate { break case .Local, .Remote: if let wrapper = item.audioWrapper { - if let controller = globalAudio, let song = controller.currentSong, song.entry.isEqual(to: wrapper) { - controller.playOrPause() + if let controller = globalAudio, controller.playOrPause(wrapper) { } else { - let controller = APSingleResourceController(account: item.account, wrapper: wrapper) + let controller = APSingleResourceController(context: item.context, wrapper: wrapper, streamable: false) controller.add(listener: self) item.chatInteraction.inlineAudioPlayer(controller) controller.start() @@ -313,36 +320,36 @@ class ContextListAudioView : ContextListRowView, APDelegate { } } - func songDidChanged(song: APSongItem, for controller: APController) { + func songDidChanged(song: APSongItem, for controller: APController, animated: Bool) { checkState() } - func songDidChangedState(song: APSongItem, for controller: APController) { + func songDidChangedState(song: APSongItem, for controller: APController, animated: Bool) { checkState() } - func songDidStartPlaying(song:APSongItem, for controller:APController) { + func songDidStartPlaying(song:APSongItem, for controller:APController, animated: Bool) { } - func songDidStopPlaying(song:APSongItem, for controller:APController) { + func songDidStopPlaying(song:APSongItem, for controller:APController, animated: Bool) { } - func playerDidChangedTimebase(song:APSongItem, for controller:APController) { + func playerDidChangedTimebase(song:APSongItem, for controller:APController, animated: Bool) { } - func audioDidCompleteQueue(for controller:APController) { + func audioDidCompleteQueue(for controller:APController, animated: Bool) { } func checkState() { if let item = item as? ContextListRowItem, let wrapper = item.audioWrapper, let controller = globalAudio, let song = controller.currentSong { if song.entry.isEqual(to: wrapper), case .playing = song.state { - progressView.theme = RadialProgressTheme(backgroundColor: theme.colors.blueFill, foregroundColor: .white, icon: theme.icons.chatMusicPause, iconInset:NSEdgeInsets(left:1)) + progressView.theme = RadialProgressTheme(backgroundColor: theme.colors.accent, foregroundColor: .white, icon: theme.icons.chatMusicPause, iconInset:NSEdgeInsets(left:1)) } else { - progressView.theme = RadialProgressTheme(backgroundColor: theme.colors.blueFill, foregroundColor: .white, icon: theme.icons.chatMusicPlay, iconInset:NSEdgeInsets(left:1)) + progressView.theme = RadialProgressTheme(backgroundColor: theme.colors.accent, foregroundColor: .white, icon: theme.icons.chatMusicPlay, iconInset:NSEdgeInsets(left:1)) } } else { - progressView.theme = RadialProgressTheme(backgroundColor: theme.colors.blueFill, foregroundColor: .white, icon: theme.icons.chatMusicPlay, iconInset:NSEdgeInsets(left:1)) + progressView.theme = RadialProgressTheme(backgroundColor: theme.colors.accent, foregroundColor: .white, icon: theme.icons.chatMusicPlay, iconInset:NSEdgeInsets(left:1)) } } @@ -353,11 +360,11 @@ class ContextListAudioView : ContextListRowView, APDelegate { override func set(item: TableRowItem, animated: Bool) { let updated = self.item != item - super.set(item: item) + super.set(item: item, animated: animated) if let item = item as? ContextListRowItem, updated, let resource = item.fileResource { - let updatedStatusSignal = item.account.postbox.mediaBox.resourceStatus(resource) |> deliverOnMainQueue + let updatedStatusSignal = item.context.account.postbox.mediaBox.resourceStatus(resource) |> deliverOnMainQueue statusDisposable.set(updatedStatusSignal.start(next: { [weak self] status in if let strongSelf = self { diff --git a/Telegram-Mac/ContextMediaRowItem.swift b/Telegram-Mac/ContextMediaRowItem.swift index 23b8d5f8ef..363b0eeca3 100644 --- a/Telegram-Mac/ContextMediaRowItem.swift +++ b/Telegram-Mac/ContextMediaRowItem.swift @@ -8,27 +8,41 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac +import TelegramCore + +import SwiftSignalKit +import Postbox + +final class ContextMediaArguments { + let sendResult: (ChatContextResult, NSView) -> Void + let menuItems: (TelegramMediaFile, NSView) -> Signal<[ContextMenuItem], NoError> + let openMessage: (Message) -> Void + let messageMenuItems: (Message, NSView) -> Signal<[ContextMenuItem], NoError> + + init(sendResult: @escaping(ChatContextResult, NSView) -> Void = { _, _ in }, menuItems: @escaping(TelegramMediaFile, NSView) -> Signal<[ContextMenuItem], NoError> = { _, _ in return .single([]) }, openMessage: @escaping(Message) -> Void = { _ in }, messageMenuItems:@escaping (Message, NSView) -> Signal<[ContextMenuItem], NoError> = { _, _ in return .single([]) }) { + self.sendResult = sendResult + self.menuItems = menuItems + self.openMessage = openMessage + self.messageMenuItems = messageMenuItems + } +} + class ContextMediaRowItem: TableRowItem { let result:InputMediaContextRow - let results:ChatContextResultCollection private let _index:Int64 - let account:Account - let chatInteraction:ChatInteraction + let context: AccountContext + let arguments: ContextMediaArguments override var stableId: AnyHashable { return Int64(_index) } - init(_ initialSize: NSSize, _ results:ChatContextResultCollection, _ result:InputMediaContextRow, _ index:Int64, _ account:Account, _ chatInteraction:ChatInteraction) { + init(_ initialSize: NSSize, _ result:InputMediaContextRow, _ index:Int64, _ context: AccountContext, _ arguments: ContextMediaArguments) { self.result = result - self.results = results - self.chatInteraction = chatInteraction + self.arguments = arguments self._index = index - self.account = account + self.context = context dif = 0 super.init(initialSize) } @@ -41,56 +55,230 @@ class ContextMediaRowItem: TableRowItem { return height } + func contains(_ messageId: MessageId) -> Bool { + if self.result.messages.contains(where: { $0.id == messageId }) { + return true + } + return false + } + override func viewClass() -> AnyClass { return ContextMediaRowView.self } + override func menuItems(in location: NSPoint) -> Signal<[ContextMenuItem], NoError> { + var inset:CGFloat = 0 + var i:Int = 0 + for size in result.sizes { + if location.x > inset && location.x < inset + size.width { + if !result.messages.isEmpty { + if let view = self.view { + let items = arguments.messageMenuItems(result.messages[i], view.subviews[i]) + return items + } + } else { + switch result.results[i] { + case let .internalReference(values): + if let file = values.file, let view = self.view { + let items = arguments.menuItems(file, view.subviews[i]) + return items + } + default: + break + } + } + break + } + inset += size.width + i += 1 + } + return .single([]) + } + } private var dif:CGFloat = 0 -class ContextMediaRowView: TableRowView { - private let stickerFetchedDisposable:MetaDisposable = MetaDisposable() +class ContextMediaRowView: TableRowView, ModalPreviewRowViewProtocol { + + private let stickerFetchedDisposable:MetaDisposable = MetaDisposable() + private let longDisposable = MetaDisposable() deinit { stickerFetchedDisposable.dispose() + longDisposable.dispose() + } + + func previewMediaIfPossible() -> Bool { + if let item = self.item as? ContextMediaRowItem, let table = item.table, let window = window as? Window { + _ = startModalPreviewHandle(table, window: window, context: item.context) + } + return true + } + + override func mouseDown(with event: NSEvent) { + super.mouseDown(with: event) + + let signal = Signal.complete() |> delay(0.2, queue: .mainQueue()) + + let downIndex = self.index(at: convert(event.locationInWindow, to: nil)) + + longDisposable.set(signal.start(completed: { [weak self] in + guard let `self` = self, let window = self.window else { + return + } + let nextIndex = self.index(at: self.convert(window.mouseLocationOutsideOfEventStream, to: nil)) + if nextIndex == downIndex { + _ = self.previewMediaIfPossible() + } + })) + } + + + override func forceClick(in location: NSPoint) { + if mouseInside() == true { + let result = previewMediaIfPossible() + if !result { + super.forceClick(in: location) + } + } else { + super.forceClick(in: location) + } + + } + + func fileAtPoint(_ point: NSPoint) -> (QuickPreviewMedia, NSView?)? { + guard let item = item as? ContextMediaRowItem else {return nil} + for i in 0 ..< self.subviews.count { + if NSPointInRect(point, self.subviews[i].frame) { + switch item.result.entries[i] { + case let .gif(data): + return (.file(data.file, GifPreviewModalView.self), self.subviews[i]) + case let .sticker(_, file): + let reference = file.stickerReference != nil ? FileMediaReference.stickerPack(stickerPack: file.stickerReference!, media: file) : FileMediaReference.standalone(media: file) + if file.isAnimatedSticker { + return (.file(reference, AnimatedStickerPreviewModalView.self), self.subviews[i]) + } else { + return (.file(reference, StickerPreviewModalView.self), self.subviews[i]) + } + default: + break + } + } + } + return nil } + override func interactionContentView(for innerId: AnyHashable, animateIn: Bool) -> NSView { + if let innerId = innerId.base as? MessageId { + let view = self.subviews.first(where: { + ($0 as? GIFContainerView)?.associatedMessageId == innerId + }) + return view ?? self + } + return self + } + + + override func set(item: TableRowItem, animated: Bool) { super.set(item: item, animated: animated) + + var subviews = self.subviews + + self.removeAllSubviews() - removeAllSubviews() if let item = item as? ContextMediaRowItem { var inset:CGFloat = 0 for i in 0 ..< item.result.entries.count { let container:NSView switch item.result.entries[i] { case let .gif(data): - let view = GIFContainerView() - let signal:Signal<(TransformImageArguments) -> DrawingContext?, NoError> - if let thumb = data.thumb { - signal = chatWebpageSnippetPhoto(account: item.account, photo: thumb, scale: backingScaleFactor, small:true) + let view: GIFContainerView + let index = subviews.firstIndex(where: { $0 is GIFContainerView }) + if let index = index { + view = subviews.remove(at: index) as! GIFContainerView + inner: for view in view.subviews { + if view.identifier == NSUserInterfaceItemIdentifier("gif-separator") { + view.removeFromSuperview() + break inner + } + } } else { - signal = .never() + view = GIFContainerView() } - view.update(with: data.file, size: NSMakeSize(item.result.sizes[i].width, item.height), viewSize: item.result.sizes[i], account: item.account, table: item.table, iconSignal: signal) - container = view - case let .sticker(data): - let view = TransformImageView() - view.setSignal(account: item.account, signal: chatMessageSticker(account: item.account, file: data.file, type: .small, scale: backingScaleFactor)) - _ = fileInteractiveFetched(account: item.account, file: data.file).start() + var effectiveFile = data.file - let imageSize = item.result.sizes[i].aspectFitted(NSMakeSize(item.height, item.height - 8)) - view.set(arguments: TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: NSEdgeInsets())) + if let preview = data.file.media.videoThumbnails.first { + + let file = effectiveFile.media.withUpdatedResource(preview.resource) + + switch data.file { + case let .message(message, _): + effectiveFile = FileMediaReference.message(message: message, media: file) + case .standalone: + effectiveFile = FileMediaReference.standalone(media: file) + case .savedGif: + effectiveFile = FileMediaReference.savedGif(media: file) + case let .stickerPack(stickerPack, _): + effectiveFile = FileMediaReference.stickerPack(stickerPack: stickerPack, media: file) + case let .webPage(webPage, _): + effectiveFile = FileMediaReference.webPage(webPage: webPage, media: file) + case let .avatarList(peer: reference, media: media): + effectiveFile = FileMediaReference.avatarList(peer: reference, media: media) + } + + } + let signal = chatMessageVideo(postbox: item.context.account.postbox, fileReference: effectiveFile, scale: backingScaleFactor) - view.setFrameSize(imageSize) + + view.update(with: effectiveFile, size: NSMakeSize(item.result.sizes[i].width, item.height - 2), viewSize: item.result.sizes[i], context: item.context, table: item.table, iconSignal: signal) + if i != (item.result.entries.count - 1) { + let layer = View() + layer.identifier = NSUserInterfaceItemIdentifier("gif-separator") + layer.frame = NSMakeRect(view.frame.width - 2.0, 0, 2.0, view.frame.height) + layer.background = theme.colors.background + view.addSubview(layer) + } + view.userInteractionEnabled = false container = view + case let .sticker(data): + if data.file.isAnimatedSticker { + let view: MediaAnimatedStickerView + let index = subviews.firstIndex(where: { $0 is MediaAnimatedStickerView}) + if let index = index { + view = subviews.remove(at: index) as! MediaAnimatedStickerView + } else { + view = MediaAnimatedStickerView(frame: NSZeroRect) + } + let size = NSMakeSize(round(item.result.sizes[i].width), round(item.result.sizes[i].height)) + view.update(with: data.file, size: size, context: item.context, parent: nil, table: item.table, parameters: nil, animated: false, positionFlags: nil, approximateSynchronousValue: false) + view.userInteractionEnabled = false + + container = view + } else { + let view: TransformImageView + let index = subviews.firstIndex(where: { $0 is TransformImageView}) + if let index = index { + view = subviews.remove(at: index) as! TransformImageView + } else { + view = TransformImageView() + } + + view.setSignal(chatMessageSticker(postbox: item.context.account.postbox, file: stickerPackFileReference(data.file), small: true, scale: backingScaleFactor, fetched: true)) + let imageSize = item.result.sizes[i].aspectFitted(NSMakeSize(item.height, item.height - 8)) + view.set(arguments: TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: NSEdgeInsets())) + + view.setFrameSize(imageSize) + container = view + } + case let .photo(data): let view = View() let imageView = TransformImageView() - imageView.setSignal(account: item.account, signal: chatWebpageSnippetPhoto(account: item.account, photo: data, scale: backingScaleFactor, small:false)) - _ = chatMessagePhotoInteractiveFetched(account: item.account, photo: data).start() + imageView.setSignal(chatWebpageSnippetPhoto(account: item.context.account, imageReference: ImageMediaReference.standalone(media: data), scale: backingScaleFactor, small:false)) + _ = chatMessagePhotoInteractiveFetched(account: item.context.account, imageReference: ImageMediaReference.standalone(media: data)).start() let imageSize = item.result.sizes[i] imageView.set(arguments: TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: NSEdgeInsets())) @@ -101,9 +289,8 @@ class ContextMediaRowView: TableRowView { imageView.center() view.addSubview(imageView) container = view - - break } + container.setFrameOrigin(inset, 0) container.background = theme.colors.background addSubview(container) @@ -114,22 +301,30 @@ class ContextMediaRowView: TableRowView { } } - + func index(at point: NSPoint) -> Int? { + if let _ = item as? ContextMediaRowItem { + for (i, subview) in self.subviews.enumerated() { + if NSPointInRect(point, subview.frame) { + return i + } + } + } + return nil + } override func mouseUp(with event: NSEvent) { super.mouseUp(with: event) - let point = convert(event.locationInWindow, from: nil) - if let item = item as? ContextMediaRowItem { - var inset:CGFloat = 0 - var i:Int = 0 - for size in item.result.sizes { - - if point.x > inset && point.x < inset + size.width { - item.chatInteraction.sendInlineResult(item.results, item.result.results[i]) - break + + longDisposable.set(nil) + + if let item = item as? ContextMediaRowItem, event.clickCount == 1 { + let point = convert(event.locationInWindow, from: nil) + if let index = self.index(at: point) { + if !item.result.messages.isEmpty { + item.arguments.openMessage(item.result.messages[index]) + } else { + item.arguments.sendResult(item.result.results[index], self.subviews[index]) } - inset += size.width - i += 1 } } } @@ -160,4 +355,7 @@ class ContextMediaRowView: TableRowView { } } + + + } diff --git a/Telegram-Mac/ContextSearchMessageItem.swift b/Telegram-Mac/ContextSearchMessageItem.swift new file mode 100644 index 0000000000..0c273dcdbc --- /dev/null +++ b/Telegram-Mac/ContextSearchMessageItem.swift @@ -0,0 +1,301 @@ +// +// ContextSearchMessageItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 06/11/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import TGUIKit +import Postbox + + +class ContextSearchMessageItem: GeneralRowItem { + + let message:Message + + let context: AccountContext + let peer:Peer + var peerId:PeerId { + return peer.id + } + + let photo: AvatarNodeState + + + override var stableId: AnyHashable { + return message.id + } + + + private var date:NSAttributedString? + + private var displayLayout:(TextNodeLayout, TextNode)? + + private var displaySelectedLayout:(TextNodeLayout, TextNode)? + private var dateLayout:(TextNodeLayout, TextNode)? + private var dateSelectedLayout:(TextNodeLayout, TextNode)? + + private var displayNode:TextNode = TextNode() + private var displaySelectedNode:TextNode = TextNode() + + private let titleText:NSAttributedString + + private var messageLayout: TextViewLayout + private var messageSelectedLayout: TextViewLayout + + + init(_ initialSize:NSSize, context: AccountContext, message: Message, searchText: String, action: @escaping()->Void) { + self.context = context + self.message = message + + + self.peer = message.chatPeer(context.peerId)! + + var peer:Peer = self.peer + + var title:String = peer.displayTitle + if let _peer = messageMainPeer(message) as? TelegramChannel, case .broadcast(_) = _peer.info { + title = _peer.displayTitle + peer = _peer + } + + + var nameColor:NSColor = theme.chat.linkColor(true, false) + + if messageMainPeer(message) is TelegramChannel || messageMainPeer(message) is TelegramGroup { + if let peer = messageMainPeer(message) as? TelegramChannel, case .broadcast(_) = peer.info { + nameColor = theme.chat.linkColor(true, false) + } else if context.peerId != peer.id { + let value = abs(Int(peer.id.id._internalGetInt64Value()) % 7) + nameColor = theme.chat.peerName(value) + } + } + + let titleText:NSMutableAttributedString = NSMutableAttributedString() + let _ = titleText.append(string: title, color: nameColor, font: .medium(.text)) + titleText.setSelected(color: theme.colors.underSelectedColor ,range: titleText.range) + + self.titleText = titleText + let messageTitle = NSMutableAttributedString() + + var text = pullText(from: message) as String + if text.isEmpty { + text = serviceMessageText(message, account: context.account) + } + _ = messageTitle.append(string: text, color: theme.colors.text, font: .normal(.text)) + + + self.messageLayout = TextViewLayout(messageTitle, maximumNumberOfLines: 1, truncationType: .end, strokeLinks: true) + let selectRange = messageTitle.string.lowercased().nsstring.range(of: searchText.lowercased()) + if selectRange.location != NSNotFound { + self.messageLayout.additionalSelections = [TextSelectedRange(range: selectRange, color: theme.colors.accentIcon.withAlphaComponent(0.5), def: false)] + } + + + let selectedAttrText = messageTitle.mutableCopy() as! NSMutableAttributedString + selectedAttrText.addAttribute(.foregroundColor, value: NSColor.white, range: selectedAttrText.range) + self.messageSelectedLayout = TextViewLayout(selectedAttrText, maximumNumberOfLines: 1, truncationType: .end, strokeLinks: true) + + + let date:NSMutableAttributedString = NSMutableAttributedString() + var time:TimeInterval = TimeInterval(message.timestamp) + time -= context.timeDifference + let range = date.append(string: DateUtils.string(forMessageListDate: Int32(time)), color: theme.colors.grayText, font: .normal(.short)) + date.setSelected(color: theme.colors.underSelectedColor, range: range) + self.date = date.copy() as? NSAttributedString + + dateLayout = TextNode.layoutText(maybeNode: nil, date, nil, 1, .end, NSMakeSize( .greatestFiniteMagnitude, 20), nil, false, .left) + dateSelectedLayout = TextNode.layoutText(maybeNode: nil, date, nil, 1, .end, NSMakeSize( .greatestFiniteMagnitude, 20), nil, true, .left) + + self.photo = .PeerAvatar(peer, peer.displayLetters, peer.smallProfileImage, message, nil) + + super.init(initialSize, height: 44, action: action) + + _ = makeSize(initialSize.width, oldWidth: 0) + } + + let margin:CGFloat = 5 + + var titleWidth:CGFloat { + var dateSize:CGFloat = 0 + if let dateLayout = dateLayout { + dateSize = dateLayout.0.size.width + } + + return size.width - 50 - margin * 4 - dateSize + } + var messageWidth:CGFloat { + var dateSize:CGFloat = 0 + if let dateLayout = dateLayout { + dateSize = dateLayout.0.size.width + } + return size.width - 70 - margin * 4 - dateSize + } + + let leftInset:CGFloat = 40 + 20; + + override func makeSize(_ width: CGFloat, oldWidth:CGFloat) -> Bool { + let result = super.makeSize(width, oldWidth: oldWidth) + if displayLayout == nil || !displayLayout!.0.isPerfectSized || self.oldWidth > width { + displayLayout = TextNode.layoutText(maybeNode: displayNode, titleText, nil, 1, .end, NSMakeSize(titleWidth, size.height), nil, false, .left) + } + + messageLayout.measure(width: messageWidth) + messageSelectedLayout.measure(width: messageWidth) + + if displaySelectedLayout == nil || !displaySelectedLayout!.0.isPerfectSized || self.oldWidth > width { + displaySelectedLayout = TextNode.layoutText(maybeNode: displaySelectedNode, titleText, nil, 1, .end, NSMakeSize(titleWidth, size.height), nil, true, .left) + } + + return result + } + + + + + var ctxDisplayLayout:(TextNodeLayout, TextNode)? { + if isSelected { + return displaySelectedLayout + } + return displayLayout + } + var ctxMessageLayout: TextViewLayout { + if isSelected { + return messageSelectedLayout + } + + return messageLayout + } + var ctxDateLayout:(TextNodeLayout, TextNode)? { + if isSelected { + return dateSelectedLayout + } + return dateLayout + } + + override var instantlyResize: Bool { + return true + } + + + override func viewClass() -> AnyClass { + return ContextSearchMessageView.self + } +} + +private class ContextSearchMessageView : GeneralRowView { + + + private var titleText:TextNode = TextNode() + private var messageText:TextView = TextView() + private var photo:AvatarControl = AvatarControl(font: .avatar(22)) + + + + override var isFlipped: Bool { + return true + } + + + + + override var backdorColor: NSColor { + if let item = item { + if item.isHighlighted && !item.isSelected { + return theme.colors.grayHighlight + } else if item.isSelected { + return theme.chatList.selectedBackgroundColor + } + + } + + return theme.colors.background + } + + + override func draw(_ layer: CALayer, in ctx: CGContext) { + + + super.draw(layer, in: ctx) + // + if let item = self.item as? ContextSearchMessageItem { + + + if !item.isSelected { + + if backingScaleFactor == 1.0 { + ctx.setFillColor(backdorColor.cgColor) + ctx.fill(layer.bounds) + } + + ctx.setFillColor(theme.colors.border.cgColor) + ctx.fill(NSMakeRect(item.leftInset, NSHeight(layer.bounds) - .borderSize, layer.bounds.width - item.leftInset, .borderSize)) + } + + + + if let displayLayout = item.ctxDisplayLayout { + + displayLayout.1.draw(NSMakeRect(item.leftInset, item.margin - 1, displayLayout.0.size.width, displayLayout.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) + + + + if let dateLayout = item.ctxDateLayout { + let dateX = frame.width - dateLayout.0.size.width - 20 + let dateFrame = focus(dateLayout.0.size) + dateLayout.1.draw(NSMakeRect(dateX, dateFrame.minY, dateLayout.0.size.width, dateLayout.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) + + } + } + } + + } + + + + required init(frame frameRect: NSRect) { + + + super.init(frame: frameRect) + layerContentsRedrawPolicy = .onSetNeedsDisplay + photo.userInteractionEnabled = false + photo.frame = NSMakeRect(20, 8, 30, 30) + addSubview(photo) + addSubview(messageText) + messageText.userInteractionEnabled = false + messageText.isSelectable = false + + } + + override func layout() { + super.layout() + guard let item = item as? ContextSearchMessageItem else {return} + photo.centerY(x: 20) + messageText.setFrameOrigin(item.leftInset, frame.height - messageText.frame.height - item.margin - 1) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + override func updateColors() { + super.updateColors() + messageText.backgroundColor = backdorColor + } + + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + + guard let item = item as? ContextSearchMessageItem else {return} + + photo.setState(account: item.context.account, state: item.photo) + messageText.update(item.ctxMessageLayout) + } + +} diff --git a/Telegram-Mac/ContextShowPeersHolder.swift b/Telegram-Mac/ContextShowPeersHolder.swift new file mode 100644 index 0000000000..c6528a4de3 --- /dev/null +++ b/Telegram-Mac/ContextShowPeersHolder.swift @@ -0,0 +1,66 @@ +// +// ContextShowPeersHolder.swift +// Telegram +// +// Created by Mikhail Filimonov on 06.12.2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + + +class ContextShowPeersHolderItem: GeneralRowItem { + fileprivate let textLayout: TextViewLayout + init(_ initialSize: NSSize, stableId: AnyHashable, action: @escaping()->Void) { + textLayout = TextViewLayout.init(.initialize(string: "Show All Users", color: theme.colors.accent, font: .normal(.text)), maximumNumberOfLines: 1) + super.init(initialSize, height: 40, stableId: stableId, action: action) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + self.textLayout.measure(width: width - 60) + return true + } + + override func viewClass() -> AnyClass { + return ContextShowPeersHolderView.self + } +} + + +private final class ContextShowPeersHolderView : TableRowView { + private let textView: TextView = TextView() + private let borderView = View() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(textView) + addSubview(borderView) + textView.isSelectable = false + textView.userInteractionEnabled = false + } + + override func updateColors() { + super.updateColors() + self.textView.backgroundColor = theme.colors.background + borderView.backgroundColor = theme.colors.border + } + + override func layout() { + super.layout() + self.textView.center() + self.borderView.frame = NSMakeRect(0, frame.height - .borderSize, frame.width, .borderSize) + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + guard let item = item as? ContextShowPeersHolderItem else { + return + } + self.textView.update(item.textLayout) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/ContextStickerRowItem.swift b/Telegram-Mac/ContextStickerRowItem.swift index d12767ad3f..2a95c3f4ac 100644 --- a/Telegram-Mac/ContextStickerRowItem.swift +++ b/Telegram-Mac/ContextStickerRowItem.swift @@ -7,23 +7,24 @@ // import Cocoa -import SwiftSignalKitMac -import TelegramCoreMac -import PostboxMac +import SwiftSignalKit +import TelegramCore + +import Postbox import TGUIKit class ContextStickerRowItem: TableRowItem { let result:InputMediaStickersRow - fileprivate let account:Account + fileprivate let context:AccountContext fileprivate let _stableId:Int64 fileprivate let chatInteraction:ChatInteraction var selectedIndex:Int? = nil override var stableId: AnyHashable { return _stableId } - init(_ initialSize:NSSize, _ account:Account, _ entry:InputMediaStickersRow, _ stableId:Int64, _ chatInteraction:ChatInteraction) { - self.account = account + init(_ initialSize:NSSize, _ context: AccountContext, _ entry:InputMediaStickersRow, _ stableId:Int64, _ chatInteraction:ChatInteraction) { + self.context = context self.result = entry self.chatInteraction = chatInteraction self._stableId = stableId @@ -42,15 +43,21 @@ class ContextStickerRowItem: TableRowItem { -class ContextStickerRowView : TableRowView, StickerPreviewRowViewProtocol { +class ContextStickerRowView : TableRowView, ModalPreviewRowViewProtocol { - func fileAtPoint(_ point:NSPoint) -> TelegramMediaFile? { + func fileAtPoint(_ point:NSPoint) -> (QuickPreviewMedia, NSView?)? { if let item = item as? ContextStickerRowItem { var i:Int = 0 for subview in subviews { if point.x > subview.frame.minX && point.x < subview.frame.maxX { - return item.result.results[i].file + let file = item.result.results[i].file + let reference = file.stickerReference != nil ? FileMediaReference.stickerPack(stickerPack: file.stickerReference!, media: file) : FileMediaReference.standalone(media: file) + if file.isAnimatedSticker { + return (.file(reference, AnimatedStickerPreviewModalView.self), subview) + } else { + return (.file(reference, StickerPreviewModalView.self), subview) + } } i += 1 } @@ -58,56 +65,136 @@ class ContextStickerRowView : TableRowView, StickerPreviewRowViewProtocol { return nil } + override func menu(for event: NSEvent) -> NSMenu? { + let menu = NSMenu() + if let item = item as? ContextStickerRowItem { + + let reference = fileAtPoint(convert(event.locationInWindow, from: nil)) + + if let reference = reference?.0.fileReference?.media.stickerReference { + menu.addItem(ContextMenuItem(L10n.contextViewStickerSet, handler: { + showModal(with: StickerPackPreviewModalController(item.context, peerId: item.chatInteraction.peerId, reference: reference), for: mainWindow) + })) + } + if let file = reference?.0.fileReference?.media { + menu.addItem(ContextMenuItem(L10n.chatSendWithoutSound, handler: { [weak item] in + item?.chatInteraction.sendAppFile(file, true, nil) + item?.chatInteraction.clearInput() + })) + } + + } + return menu + } + override func set(item: TableRowItem, animated: Bool) { super.set(item: item, animated: animated) - removeAllSubviews() if let item = item as? ContextStickerRowItem { + + while subviews.count > item.result.entries.count { + subviews.last?.removeFromSuperview() + } + while subviews.count < item.result.entries.count { + addSubview(Control()) + } + + for i in 0 ..< item.result.entries.count { - let container:Control = Control() - - - container.set(background: theme.colors.grayBackground, for: .Highlight) + let container:Control = self.subviews[i] as! Control + container.removeAllHandlers() + + if item.selectedIndex == i { - container.set(background: theme.colors.blueSelect, for: .Normal) - container.set(background: theme.colors.blueSelect, for: .Hover) - container.set(background: theme.colors.blueUI, for: .Highlight) + container.set(background: theme.colors.grayBackground, for: .Normal) + container.set(background: theme.colors.grayBackground, for: .Hover) + container.set(background: theme.colors.grayBackground, for: .Highlight) + container.apply(state: .Normal) + } else { + container.set(background: theme.colors.background, for: .Normal) + container.set(background: theme.colors.background, for: .Hover) + container.set(background: theme.colors.background, for: .Highlight) container.apply(state: .Normal) } + container.layer?.cornerRadius = .cornerRadius switch item.result.entries[i] { case let .sticker(data): - container.set(handler: { [weak item] (control) in - item?.chatInteraction.sendAppFile(data.file) - item?.chatInteraction.clearInput() + container.set(handler: { [weak item] control in + if let slowMode = item?.chatInteraction.presentation.slowMode, slowMode.hasLocked { + showSlowModeTimeoutTooltip(slowMode, for: control) + } else { + item?.chatInteraction.sendAppFile(data.file, false, nil) + item?.chatInteraction.clearInput() + } }, for: .Click) container.set(handler: { [weak self, weak item] (control) in if let window = self?.window as? Window, let item = item, let table = item.table { - _ = startStickerPreviewHandle(table, window: window, account: item.account) + _ = startModalPreviewHandle(table, window: window, context: item.context) } }, for: .LongMouseDown) - let view = TransformImageView() - view.setSignal(account: item.account, signal: chatMessageSticker(account: item.account, file: data.file, type: .small, scale: backingScaleFactor)) - _ = fileInteractiveFetched(account: item.account, file: data.file).start() - - let imageSize = data.file.dimensions?.aspectFitted(NSMakeSize(item.result.sizes[i].width - 8, item.result.sizes[i].height - 8)) ?? item.result.sizes[i] - view.set(arguments: TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: NSEdgeInsets())) + + if data.file.isAnimatedSticker { + let view: MediaAnimatedStickerView + if container.subviews.isEmpty { + view = MediaAnimatedStickerView(frame: .zero) + container.addSubview(view) + } else { + let temp = container.subviews.first as? MediaAnimatedStickerView + if temp == nil { + view = MediaAnimatedStickerView(frame: .zero) + container.subviews.removeFirst() + container.addSubview(view, positioned: .below, relativeTo: container.subviews.first) + } else { + view = temp! + } + } + let size = NSMakeSize(round(item.result.sizes[i].width - 8), round(item.result.sizes[i].height - 8)) + view.update(with: data.file, size: size, context: item.context, parent: nil, table: item.table, parameters: nil, animated: false, positionFlags: nil, approximateSynchronousValue: false) + view.userInteractionEnabled = false + } else { + let file = data.file + let imageSize = file.dimensions?.size.aspectFitted(NSMakeSize(item.result.sizes[i].width - 8, item.result.sizes[i].height - 8)) ?? item.result.sizes[i] + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: NSEdgeInsets()) + + let view: TransformImageView + if container.subviews.isEmpty { + view = TransformImageView() + container.addSubview(view) + } else { + let temp = container.subviews.first as? TransformImageView + if temp == nil { + view = TransformImageView() + container.subviews.removeFirst() + container.addSubview(view, positioned: .below, relativeTo: container.subviews.first) + } else { + view = temp! + } + } + + view.setSignal(signal: cachedMedia(media: file, arguments: arguments, scale: backingScaleFactor), clearInstantly: false) + view.setSignal( chatMessageSticker(postbox: item.context.account.postbox, file: stickerPackFileReference(data.file), small: false, scale: backingScaleFactor, fetched: true), cacheImage: { [weak file] result in + if let file = file { + cacheMedia(result, media: file, arguments: arguments, scale: System.backingScale) + } + }) + + view.set(arguments: arguments) + + view.setFrameSize(imageSize) + } - view.setFrameSize(imageSize) - container.addSubview(view) container.setFrameSize(NSMakeSize(item.result.sizes[i].width - 4, item.result.sizes[i].height - 4)) default: fatalError("ContextStickerRowItem support only stickers") } - addSubview(container) - } needsLayout = true @@ -122,7 +209,7 @@ class ContextStickerRowView : TableRowView, StickerPreviewRowViewProtocol { if let item = item as? ContextStickerRowItem { let defSize = NSMakeSize( item.result.sizes[0].width - 4, item.result.sizes[0].height - 4) - let defInset = floorToScreenPixels((frame.width - defSize.width * CGFloat(item.result.maxCount)) / CGFloat(item.result.maxCount + 1)) + let defInset = floorToScreenPixels(backingScaleFactor, (frame.width - defSize.width * CGFloat(item.result.maxCount)) / CGFloat(item.result.maxCount + 1)) var inset = defInset for i in 0 ..< item.result.entries.count { diff --git a/Telegram-Mac/ContextSwitchPeerRowItem.swift b/Telegram-Mac/ContextSwitchPeerRowItem.swift index c59dc3f247..845d0e580f 100644 --- a/Telegram-Mac/ContextSwitchPeerRowItem.swift +++ b/Telegram-Mac/ContextSwitchPeerRowItem.swift @@ -8,9 +8,10 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac +import TelegramCore + +import SwiftSignalKit +import Postbox class ContextSwitchPeerRowItem: TableRowItem { fileprivate let account:Account fileprivate let peerId:PeerId @@ -28,8 +29,9 @@ class ContextSwitchPeerRowItem: TableRowItem { } override func makeSize(_ width: CGFloat, oldWidth:CGFloat) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) layout.measure(width: width - 40) - return super.makeSize(width, oldWidth: oldWidth) + return success } override func viewClass() -> AnyClass { @@ -47,6 +49,7 @@ class ContextSwitchPeerRowView: TableRowView { private let overlay:OverlayControl = OverlayControl() required init(frame frameRect: NSRect) { super.init(frame: frameRect) + layerContentsRedrawPolicy = .onSetNeedsDisplay textView.userInteractionEnabled = false addSubview(overlay) addSubview(textView) diff --git a/Telegram-Mac/ControllerExtension.swift b/Telegram-Mac/ControllerExtension.swift index 3e75d88088..b22ca01f6e 100644 --- a/Telegram-Mac/ControllerExtension.swift +++ b/Telegram-Mac/ControllerExtension.swift @@ -7,35 +7,33 @@ // import Cocoa -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit import TGUIKit class TelegramGenericViewController: GenericViewController where T:NSView { - let account:Account + let context:AccountContext private let languageDisposable:MetaDisposable = MetaDisposable() - init(_ account:Account) { - self.account = account + init(_ context:AccountContext) { + self.context = context super.init() } - + override func viewDidLoad() { super.viewDidLoad() - let ignore:Atomic = Atomic(value: true) - languageDisposable.set(combineLatest(appearanceSignal, ready.get() |> deliverOnMainQueue |> take(1)).start(next: { [weak self] _ in - if !ignore.swap(false) { - self?.updateLocalizationAndTheme() - } + languageDisposable.set(appearanceSignal.start(next: { [weak self] appearance in + self?.updateLocalizationAndTheme(theme: appearance.presentation) })) } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) - self.genericView.background = theme.colors.background + (self.genericView as? AppearanceViewProtocol)?.updateLocalizationAndTheme(theme: theme) requestUpdateBackBar() requestUpdateCenterBar() requestUpdateRightBar() @@ -46,7 +44,9 @@ class TelegramGenericViewController: GenericViewController where T:NSView } } -class TelegramViewController: TelegramGenericViewController { + + +class TelegramViewController: TelegramGenericViewController { } @@ -55,6 +55,7 @@ class TelegramViewController: TelegramGenericViewController { class TableViewController: TelegramGenericViewController, TableViewDelegate { + override func loadView() { super.loadView() @@ -63,20 +64,34 @@ class TableViewController: TelegramGenericViewController, TableViewDe override func viewDidLoad() { super.viewDidLoad() - + genericView.getBackgroundColor = { + return theme.colors.listBackground + } + } + + override func scrollup(force: Bool = false) { + if isLoaded() { + self.genericView.scroll(to: .up(!force)) + } + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) } func selectionDidChange(row:Int, item:TableRowItem, byClick:Bool, isNew:Bool) -> Void { } - func selectionWillChange(row:Int, item:TableRowItem) -> Bool { + func selectionWillChange(row:Int, item:TableRowItem, byClick: Bool) -> Bool { return false } func isSelectable(row:Int, item:TableRowItem) -> Bool { return false } - + func findGroupStableId(for stableId: AnyHashable) -> AnyHashable? { + return nil + } override var enableBack: Bool { return true @@ -115,13 +130,14 @@ class EditableViewController: TelegramGenericViewController where T: NSVie func changeState() ->Void { + let new: ViewControllerState if case .Normal = state { - self.state = .Edit + new = .Edit } else { - self.state = .Normal + new = .Normal } - update(with:state) + update(with: new) } var doneString:String { @@ -147,13 +163,13 @@ class EditableViewController: TelegramGenericViewController where T: NSVie func updateEditStateTitles() -> Void { switch state { case .Edit: - editBar.button.set(text: doneString, for: .Normal) + editBar.set(text: doneString, for: .Normal) case .Normal: - editBar.button.set(text: normalString, for: .Normal) + editBar.set(text: normalString, for: .Normal) case .Some: - editBar.button.set(text: someString, for: .Normal) + editBar.set(text: someString, for: .Normal) } - editBar.button.set(color: presentation.colors.blueUI, for: .Normal) + editBar.set(color: presentation.colors.accent, for: .Normal) self.editBar.needsLayout = true } @@ -163,36 +179,42 @@ class EditableViewController: TelegramGenericViewController where T: NSVie } func addHandler() -> Void { - editBar.button.set (handler:{[weak self] _ in + editBar.set (handler:{[weak self] _ in if let strongSelf = self { strongSelf.changeState() } }, for:.Click) } - override init(_ account:Account) { - super.init(account) + override func loadView() { editBar = TextButtonBarView(controller: self, text: "", style: navigationButtonStyle, alignment:.Right) addHandler() + rightBarView = editBar + updateEditStateTitles() + super.loadView() + } + + override init(_ context:AccountContext) { + super.init(context) } func update(with state:ViewControllerState) -> Void { + self.state = state updateEditStateTitles() } public func set(editable: Bool) ->Void { - editBar.button.isHidden = !editable + editBar.isHidden = !editable } public func set(enabled: Bool) ->Void { - editBar.button.isEnabled = enabled + editBar.isEnabled = enabled } override func updateNavigation(_ navigation: NavigationViewController?) { super.updateNavigation(navigation) if navigation != nil { - rightBarView = editBar - updateEditStateTitles() + } } @@ -203,16 +225,24 @@ class EditableViewController: TelegramGenericViewController where T: NSVie } final class Appearance : Equatable { - let language:Language + let language: TelegramLocalization var presentation: TelegramPresentationTheme - init(language: Language, presentation: TelegramPresentationTheme) { + init(language: TelegramLocalization, presentation: TelegramPresentationTheme) { self.language = language self.presentation = presentation } + + var locale: Locale { + return Locale(identifier: appAppearance.language.languageCode) + } + + var newAllocation: Appearance { + return Appearance(language: language, presentation: presentation) + } } func ==(lhs:Appearance, rhs:Appearance) -> Bool { - return lhs.language === rhs.language && lhs.presentation == rhs.presentation + return lhs === rhs //lhs.language === rhs.language && lhs.presentation === rhs.presentation } var theme: TelegramPresentationTheme { @@ -227,16 +257,52 @@ var appAppearance:Appearance { return Appearance(language: appCurrentLanguage, presentation: theme) } -var appearanceSignal:Signal { - return combineLatest(languageSignal, themeSignal) |> map { +var appearanceSignal:Signal { + + var timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + + let dateSignal:Signal = Signal { subscriber in + + let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + + + var now: time_t = time_t(nowTimestamp) + var timeinfoNow: tm = tm() + localtime_r(&now, &timeinfoNow) + + + var t: time_t = time_t(timestamp) + var timeinfo: tm = tm() + localtime_r(&t, &timeinfo) + + + if timeinfo.tm_year != timeinfoNow.tm_year || timeinfo.tm_yday != timeinfoNow.tm_yday { + timestamp = nowTimestamp + subscriber.putNext(true) + } else { + subscriber.putNext(false) + } + subscriber.putCompletion() + + return EmptyDisposable + } + + let dateUpdateSignal: Signal = .single(true) |> then(dateSignal |> delay(1.0, queue: resourcesQueue) |> restart) + + let updateSignal = dateUpdateSignal |> filter {$0} + + return combineLatest(languageSignal, themeSignal, updateSignal |> deliverOnMainQueue) |> map { return Appearance(language: $0.0, presentation: $0.1) } } -struct AppearanceWrapperEntry: Comparable, Identifiable where E: Comparable, E:Identifiable { +final class AppearanceWrapperEntry: Comparable, Identifiable where E: Comparable, E:Identifiable { let entry: E let appearance: Appearance - + init(entry: E, appearance: Appearance) { + self.entry = entry + self.appearance = appearance + } var stableId: AnyHashable { return entry.stableId } diff --git a/Telegram-Mac/ConvertGroupViewController.swift b/Telegram-Mac/ConvertGroupViewController.swift deleted file mode 100644 index 226541a25f..0000000000 --- a/Telegram-Mac/ConvertGroupViewController.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// ConvertToSupergroupViewController.swift -// Telegram -// -// Created by keepcoder on 21/02/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - -import Cocoa -import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac - -class ConvertGroupViewController: TableViewController { - - private let peerId:PeerId - private let convertDisposable:MetaDisposable = MetaDisposable() - init(account:Account, peerId:PeerId) { - self.peerId = peerId - super.init(account) - } - - override func viewDidLoad() { - super.viewDidLoad() - - let initialSize = atomicSize.modify({$0}) - - let desc = NSMutableAttributedString() - _ = desc.append(string: tr(.supergroupConvertDescription), color: theme.colors.grayText, font: .normal(.text)) - desc.detectBoldColorInString(with: .medium(.text)) - - _ = genericView.addItem(item: GeneralRowItem(initialSize, height: 16)) - _ = genericView.addItem(item: GeneralTextRowItem(initialSize, text: desc)) - _ = genericView.addItem(item: GeneralInteractedRowItem(initialSize, name: tr(.supergroupConvertButton), nameStyle: blueActionButton, type: .none, action: { [weak self] in - self?.convert() - })) - let undone = NSMutableAttributedString() - _ = undone.append(string: tr(.supergroupConvertUndone), color: theme.colors.grayText, font: .normal(.text)) - undone.detectBoldColorInString(with: .medium(.text)) - _ = genericView.addItem(item: GeneralTextRowItem(initialSize, text: undone)) - - readyOnce() - } - - func convert() { - - - confirm(for: mainWindow, with: appName, and: tr(.convertToSuperGroupConfirm)) { [weak self] result in - - if let strongSelf = self { - let signal = convertGroupToSupergroup(account: strongSelf.account, peerId: strongSelf.peerId) - |> map { Optional($0) } - |> `catch` { error -> Signal in - return .single(nil) - } |> mapError {_ in} - - - self?.convertDisposable.set(showModalProgress(signal: signal |> deliverOnMainQueue, for: mainWindow).start(next: { [weak strongSelf] peerId in - if let peerId = peerId, let account = strongSelf?.account { - strongSelf?.navigationController?.push(ChatController(account: account, peerId: peerId)) - } else { - alert(for: mainWindow, info: tr(.convertToSupergroupAlertError)) - } - })) - } - - } - - - } - - deinit { - convertDisposable.dispose() - } - -} diff --git a/Telegram-Mac/CoreExtension.swift b/Telegram-Mac/CoreExtension.swift index f60deb2e4f..5313e8c4d9 100644 --- a/Telegram-Mac/CoreExtension.swift +++ b/Telegram-Mac/CoreExtension.swift @@ -8,185 +8,23 @@ // import Cocoa -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac -import TGUIKit -extension Peer { - - var mediaRestricted:Bool { - if let peer = self as? TelegramChannel { - if peer.hasBannedRights(.banSendMedia) { - return true - } - } - return false - } - - var stickersRestricted: Bool { - if let peer = self as? TelegramChannel { - if peer.hasBannedRights([.banSendStickers, .banSendGifs]) { - return true - } - } - return false - } - - var inlineRestricted: Bool { - if let peer = self as? TelegramChannel { - if peer.hasBannedRights([.banSendInline]) { - return true - } - } - return false - } - - var webUrlRestricted: Bool { - if let peer = self as? TelegramChannel { - if peer.hasBannedRights([.banEmbedLinks]) { - return true - } - } - return false - } - - - var canSendMessage: Bool { - if let channel = self as? TelegramChannel { - if case .broadcast(_) = channel.info { - return channel.hasAdminRights(.canPostMessages) - } else if case .group(_) = channel.info { - return !channel.hasBannedRights(.banSendMessages) - } - } else if let group = self as? TelegramGroup { - return group.membership == .Member - } else if let secret = self as? TelegramSecretChat { - switch secret.embeddedState { - case .terminated: - return false - case .handshake: - return false - default: - return true - } - } - - return true - } - - var username:String? { - if let peer = self as? TelegramChannel { - return peer.username - } else if let peer = self as? TelegramGroup { - return peer.username - } else if let peer = self as? TelegramUser { - return peer.username - } - return nil - } - - public var displayTitle: String { - switch self { - case let user as TelegramUser: - return user.name.isEmpty ? tr(.peerDeletedUser) : user.name - case let group as TelegramGroup: - return group.title - case let channel as TelegramChannel: - return channel.title - default: - return "" - } - } - - public var compactDisplayTitle: String { - switch self { - case let user as TelegramUser: - if let firstName = user.firstName { - return firstName - } else if let lastName = user.lastName { - return lastName - } else { - return tr(.peerDeletedUser) - } - case let group as TelegramGroup: - return group.title - case let channel as TelegramChannel: - return channel.title - default: - return "" - } - } +import TelegramCore - public var displayLetters: [String] { - switch self { - case let user as TelegramUser: - if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty && !lastName.isEmpty { - return [firstName.substring(to: firstName.index(after: firstName.startIndex)).uppercased(), lastName.substring(to: lastName.index(after: lastName.startIndex)).uppercased()] - } else if let firstName = user.firstName, !firstName.isEmpty { - return [firstName.substring(to: firstName.index(after: firstName.startIndex)).uppercased()] - } else if let lastName = user.lastName, !lastName.isEmpty { - return [lastName.substring(to: lastName.index(after: lastName.startIndex)).uppercased()] - } else { - let name = tr(.peerDeletedUser) - if !name.isEmpty { - return [name.substring(to: name.index(after: name.startIndex)).uppercased()] - } - } - - return [] - case let group as TelegramGroup: - if group.title.startIndex != group.title.endIndex { - return [group.title.substring(to: group.title.index(after: group.title.startIndex)).uppercased()] - } else { - return [] - } - case let channel as TelegramChannel: - if channel.title.startIndex != channel.title.endIndex { - return [channel.title.substring(to: channel.title.index(after: channel.title.startIndex)).uppercased()] - } else { - return [] - } - default: - return [] - } - } +import Postbox +import SwiftSignalKit +import TGUIKit - var isVerified: Bool { - if let peer = self as? TelegramUser { - return peer.flags.contains(.isVerified) - } else if let peer = self as? TelegramChannel { - return peer.flags.contains(.isVerified) - } else { - return false - } - } - -} +import MtProtoKit -extension AdminLogEventsFlags { - - /* - "ChannelEventFilter.NewRestrictions" = "New Restrictions"; - "ChannelEventFilter.NewAdmins" = "New Admins"; - "ChannelEventFilter.NewMembers" = "New Members"; - "ChannelEventFilter.GroupInfo" = "Group Info"; - "ChannelEventFilter.DeletedMessages" = "Deleted Messages"; - "ChannelEventFilter.EditedMessages" = "Edited Messages"; - "ChannelEventFilter.PinnedMessages" = "Pinned Messages"; - "ChannelEventFilter.LeavingMembers" = "Leaving Members"; - */ - - -} extension RenderedChannelParticipant { func withUpdatedBannedRights(_ info: ChannelParticipantBannedInfo) -> RenderedChannelParticipant { let updated: ChannelParticipant switch participant { - case let.member(id, invitedAt, adminInfo, _): - updated = ChannelParticipant.member(id: id, invitedAt: invitedAt, adminInfo: adminInfo, banInfo: info) - case let.creator(id): - updated = ChannelParticipant.creator(id: id) + case let.member(id, invitedAt, adminInfo, _, rank): + updated = ChannelParticipant.member(id: id, invitedAt: invitedAt, adminInfo: adminInfo, banInfo: info, rank: rank) + case let .creator(id, info, rank): + updated = ChannelParticipant.creator(id: id, adminInfo: info, rank: rank) } return RenderedChannelParticipant(participant: updated, peer: peer, presences: presences) } @@ -211,60 +49,70 @@ extension ChannelParticipant { } } -extension TelegramChannelAdminRightsFlags { +extension TelegramChatAdminRightsFlags { var localizedString:String { switch self { //EventLog.Service.Restriction.AddNewAdmins - case TelegramChannelAdminRightsFlags.canAddAdmins: - return tr(.eventLogServicePromoteAddNewAdmins) - case TelegramChannelAdminRightsFlags.canBanUsers: - return tr(.eventLogServicePromoteBanUsers) - case TelegramChannelAdminRightsFlags.canChangeInfo: - return tr(.eventLogServicePromoteChangeInfo) - case TelegramChannelAdminRightsFlags.canInviteUsers: - return tr(.eventLogServicePromoteAddUsers) - case TelegramChannelAdminRightsFlags.canChangeInviteLink: - return tr(.eventLogServicePromoteInviteViaLink) - case TelegramChannelAdminRightsFlags.canDeleteMessages: - return tr(.eventLogServicePromoteDeleteMessages) - case TelegramChannelAdminRightsFlags.canEditMessages: - return tr(.eventLogServicePromoteEditMessages) - case TelegramChannelAdminRightsFlags.canPinMessages: - return tr(.eventLogServicePromotePinMessages) - case TelegramChannelAdminRightsFlags.canPostMessages: - return tr(.eventLogServicePromotePostMessages) + case TelegramChatAdminRightsFlags.canAddAdmins: + return tr(L10n.eventLogServicePromoteAddNewAdmins) + case TelegramChatAdminRightsFlags.canBanUsers: + return tr(L10n.eventLogServicePromoteBanUsers) + case TelegramChatAdminRightsFlags.canChangeInfo: + return tr(L10n.eventLogServicePromoteChangeInfo) + case TelegramChatAdminRightsFlags.canInviteUsers: + return tr(L10n.eventLogServicePromoteAddUsers) + case TelegramChatAdminRightsFlags.canDeleteMessages: + return tr(L10n.eventLogServicePromoteDeleteMessages) + case TelegramChatAdminRightsFlags.canEditMessages: + return tr(L10n.eventLogServicePromoteEditMessages) + case TelegramChatAdminRightsFlags.canPinMessages: + return tr(L10n.eventLogServicePromotePinMessages) + case TelegramChatAdminRightsFlags.canPostMessages: + return tr(L10n.eventLogServicePromotePostMessages) + case TelegramChatAdminRightsFlags.canBeAnonymous: + return tr(L10n.eventLogServicePromoteRemainAnonymous) + case TelegramChatAdminRightsFlags.canManageCalls: + return L10n.channelAdminLogCanManageCalls default: return "Undefined Promotion" } } } -extension TelegramChannelBannedRightsFlags { +extension TelegramChatBannedRightsFlags { var localizedString:String { switch self { - case TelegramChannelBannedRightsFlags.banSendGifs: - return tr(.eventLogServiceDemoteSendStickers) - case TelegramChannelBannedRightsFlags.banEmbedLinks: - return tr(.eventLogServiceDemoteEmbedLinks) - case TelegramChannelBannedRightsFlags.banReadMessages: + case TelegramChatBannedRightsFlags.banSendGifs: + return L10n.eventLogServiceDemoteSendGifs + case TelegramChatBannedRightsFlags.banPinMessages: + return L10n.eventLogServiceDemotePinMessages + case TelegramChatBannedRightsFlags.banAddMembers: + return L10n.eventLogServiceDemoteAddMembers + case TelegramChatBannedRightsFlags.banSendPolls: + return L10n.eventLogServiceDemotePostPolls + case TelegramChatBannedRightsFlags.banEmbedLinks: + return L10n.eventLogServiceDemoteEmbedLinks + case TelegramChatBannedRightsFlags.banReadMessages: return "" - case TelegramChannelBannedRightsFlags.banSendGames: - return tr(.eventLogServiceDemoteEmbedLinks) - case TelegramChannelBannedRightsFlags.banSendInline: - return tr(.eventLogServiceDemoteSendInline) - case TelegramChannelBannedRightsFlags.banSendMedia: - return tr(.eventLogServiceDemoteSendMedia) - case TelegramChannelBannedRightsFlags.banSendMessages: - return tr(.eventLogServiceDemoteSendMessages) - case TelegramChannelBannedRightsFlags.banSendStickers: - return tr(.eventLogServiceDemoteSendStickers) + case TelegramChatBannedRightsFlags.banSendGames: + return L10n.eventLogServiceDemoteEmbedLinks + case TelegramChatBannedRightsFlags.banSendInline: + return L10n.eventLogServiceDemoteSendInline + case TelegramChatBannedRightsFlags.banSendMedia: + return L10n.eventLogServiceDemoteSendMedia + case TelegramChatBannedRightsFlags.banSendMessages: + return L10n.eventLogServiceDemoteSendMessages + case TelegramChatBannedRightsFlags.banSendStickers: + return L10n.eventLogServiceDemoteSendStickers + case TelegramChatBannedRightsFlags.banChangeInfo: + return L10n.eventLogServiceDemoteChangeInfo default: return "" } } } /* - public struct TelegramChannelBannedRightsFlags: OptionSet { + public struct TelegramChatBannedRightsFlags: OptionSet { public var rawValue: Int32 public init(rawValue: Int32) { @@ -275,48 +123,148 @@ extension TelegramChannelBannedRightsFlags { self.rawValue = 0 } - public static let banReadMessages = TelegramChannelBannedRightsFlags(rawValue: 1 << 0) - public static let banSendMessages = TelegramChannelBannedRightsFlags(rawValue: 1 << 1) - public static let banSendMedia = TelegramChannelBannedRightsFlags(rawValue: 1 << 2) - public static let banSendStickers = TelegramChannelBannedRightsFlags(rawValue: 1 << 3) - public static let banSendGifs = TelegramChannelBannedRightsFlags(rawValue: 1 << 4) - public static let banSendGames = TelegramChannelBannedRightsFlags(rawValue: 1 << 5) - public static let banSendInline = TelegramChannelBannedRightsFlags(rawValue: 1 << 6) - public static let banEmbedLinks = TelegramChannelBannedRightsFlags(rawValue: 1 << 7) + public static let banReadMessages = TelegramChatBannedRightsFlags(rawValue: 1 << 0) + public static let banSendMessages = TelegramChatBannedRightsFlags(rawValue: 1 << 1) + public static let banSendMedia = TelegramChatBannedRightsFlags(rawValue: 1 << 2) + public static let banSendStickers = TelegramChatBannedRightsFlags(rawValue: 1 << 3) + public static let banSendGifs = TelegramChatBannedRightsFlags(rawValue: 1 << 4) + public static let banSendGames = TelegramChatBannedRightsFlags(rawValue: 1 << 5) + public static let banSendInline = TelegramChatBannedRightsFlags(rawValue: 1 << 6) + public static let banEmbedLinks = TelegramChatBannedRightsFlags(rawValue: 1 << 7) } */ -extension TelegramChannelBannedRights { +extension TelegramChatBannedRights { var formattedUntilDate: String { let formatter = DateFormatter() formatter.dateStyle = .short - formatter.locale = Locale(identifier: appCurrentLanguage.languageCode) + //formatter.timeZone = NSTimeZone.local + + formatter.timeZone = NSTimeZone.local formatter.timeStyle = .short return formatter.string(from: Date(timeIntervalSince1970: TimeInterval(untilDate))) } } -func alertForMediaRestriction(_ peer:Peer) { - if let peer = peer as? TelegramChannel, let bannedRights = peer.bannedRights { - alert(for: mainWindow, info: bannedRights.untilDate != .max ? tr(.channelPersmissionDeniedSendMediaUntil(bannedRights.formattedUntilDate)) : tr(.channelPersmissionDeniedSendMediaForever)) + +func permissionText(from peer: Peer, for flags: TelegramChatBannedRightsFlags) -> String? { + let bannedPermission: (Int32, Bool)? + if let channel = peer as? TelegramChannel { + bannedPermission = channel.hasBannedPermission(flags) + } else if let group = peer as? TelegramGroup { + if group.hasBannedPermission(flags) { + bannedPermission = (Int32.max, false) + } else { + bannedPermission = nil + } + } else { + bannedPermission = nil + } + + if let (untilDate, personal) = bannedPermission { + + switch flags { + case .banSendMessages: + if personal && untilDate != 0 && untilDate != Int32.max { + return L10n.channelPersmissionDeniedSendMessagesUntil(stringForFullDate(timestamp: untilDate)) + } else if personal { + return L10n.channelPersmissionDeniedSendMessagesForever + } else { + return L10n.channelPersmissionDeniedSendMessagesDefaultRestrictedText + } + case .banSendStickers: + if personal && untilDate != 0 && untilDate != Int32.max { + return L10n.channelPersmissionDeniedSendStickersUntil(stringForFullDate(timestamp: untilDate)) + } else if personal { + return L10n.channelPersmissionDeniedSendStickersForever + } else { + return L10n.channelPersmissionDeniedSendStickersDefaultRestrictedText + } + case .banSendGifs: + if personal && untilDate != 0 && untilDate != Int32.max { + return L10n.channelPersmissionDeniedSendGifsUntil(stringForFullDate(timestamp: untilDate)) + } else if personal { + return L10n.channelPersmissionDeniedSendGifsForever + } else { + return L10n.channelPersmissionDeniedSendGifsDefaultRestrictedText + } + case .banSendMedia: + if personal && untilDate != 0 && untilDate != Int32.max { + return L10n.channelPersmissionDeniedSendMediaUntil(stringForFullDate(timestamp: untilDate)) + } else if personal { + return L10n.channelPersmissionDeniedSendMediaForever + } else { + return L10n.channelPersmissionDeniedSendMediaDefaultRestrictedText + } + case .banSendPolls: + if personal && untilDate != 0 && untilDate != Int32.max { + return L10n.channelPersmissionDeniedSendPollUntil(stringForFullDate(timestamp: untilDate)) + } else if personal { + return L10n.channelPersmissionDeniedSendPollForever + } else { + return L10n.channelPersmissionDeniedSendPollDefaultRestrictedText + } + case .banSendInline: + if personal && untilDate != 0 && untilDate != Int32.max { + return L10n.channelPersmissionDeniedSendInlineUntil(stringForFullDate(timestamp: untilDate)) + } else if personal { + return L10n.channelPersmissionDeniedSendInlineForever + } else { + return L10n.channelPersmissionDeniedSendInlineDefaultRestrictedText + } + default: + return nil + } + + } + + return nil } +extension RenderedPeer { + convenience init(_ foundPeer: FoundPeer) { + self.init(peerId: foundPeer.peer.id, peers: SimpleDictionary([foundPeer.peer.id : foundPeer.peer])) + } +} extension TelegramMediaFile { var videoSize:NSSize { for attr in attributes { if case let .Video(_,size, _) = attr { - return size + return size.size } } return NSZeroSize } + var isStreamable: Bool { + for attr in attributes { + if case let .Video(_, _, flags) = attr { + return flags.contains(.supportsStreaming) + } + } + return true + } + +// var streaming: MediaPlayerStreaming { +// for attr in attributes { +// if case let .Video(_, _, flags) = attr { +// if flags.contains(.supportsStreaming) { +// return .earlierStart +// } else { +// return .none +// } +// } +// } +// return .none +// } + + var imageSize:NSSize { for attr in attributes { if case let .ImageSize(size) = attr { - return size + return size.size } } return NSZeroSize @@ -330,6 +278,14 @@ extension TelegramMediaFile { } return 0 } + + var isTheme: Bool { + return mimeType == "application/x-tgtheme-macos" + } + + func withUpdatedResource(_ resource: TelegramMediaResource) -> TelegramMediaFile { + return TelegramMediaFile(fileId: self.fileId, partialReference: self.partialReference, resource: resource, previewRepresentations: self.previewRepresentations, videoThumbnails: self.videoThumbnails, immediateThumbnailData: self.immediateThumbnailData, mimeType: self.mimeType, size: self.size, attributes: self.attributes) + } } extension ChatContextResult { @@ -347,50 +303,66 @@ extension ChatContextResult { } } -extension Account { - var context:TelegramApplicationContext { - return self.applicationContext as! TelegramApplicationContext - } -} extension TelegramMediaFile { var elapsedSize:Int { if let size = size { return size } + if let resource = resource as? LocalFileReferenceMediaResource, let size = resource.size { + return Int(size) + } return 0 } } -enum ChatListIndexRequest :Equatable { - case Initial(Int, TableScrollState?) - case Index(ChatListIndex) -} - -func ==(lhs:ChatListIndexRequest, rhs:ChatListIndexRequest) -> Bool { - switch lhs { - case let .Initial(lhsCount, _): - if case let .Initial(rhsCount, _) = rhs { - return rhsCount == lhsCount - } - - case let .Index(lhsIndex): - if case let .Index(rhsIndex) = rhs { - return lhsIndex == rhsIndex +extension Media { + var isInteractiveMedia: Bool { + if self is TelegramMediaImage { + return true + } else if let file = self as? TelegramMediaFile { + return file.isVideo || (file.isAnimated && !file.mimeType.lowercased().hasSuffix("gif")) + } else if let map = self as? TelegramMediaMap { + return map.venue == nil + } else if self is TelegramMediaDice { + return false } + return false } - return false + var canHaveCaption: Bool { + if supposeToBeSticker { + return false + } + if self is TelegramMediaImage { + return true + } else if let file = self as? TelegramMediaFile { + if file.isInstantVideo || file.isAnimatedSticker || file.isStaticSticker || file.isVoice { + return false + } else { + return true + } + } + return false + } +} + +enum ChatListIndexRequest :Equatable { + case Initial(Int, TableScrollState?) + case Index(ChatListIndex, TableScrollState?) } + public extension PeerView { var isMuted:Bool { if let settings = self.notificationSettings as? TelegramPeerNotificationSettings { switch settings.muteState { - case .muted: - return true + case let .muted(until): + return until > Int32(Date().timeIntervalSince1970) case .unmuted: return false + case .default: + return false } } else { return false @@ -401,10 +373,12 @@ public extension PeerView { public extension TelegramPeerNotificationSettings { var isMuted:Bool { switch self.muteState { - case .muted: - return true + case let .muted(until): + return until > Int32(Date().timeIntervalSince1970) case .unmuted: return false + case .default: + return false } } } @@ -429,6 +403,32 @@ public extension TelegramMediaFile { return nil } + var maskData: StickerMaskCoords? { + for attr in attributes { + if case let .Sticker(_, _, mask) = attr { + return mask + } + } + return nil + } + + var isEmojiAnimatedSticker: Bool { + if let fileName = fileName { + return fileName.hasPrefix("telegram-animoji") && fileName.hasSuffix("tgs") && isSticker + } + return false + } + + var animatedEmojiFitzModifier: EmojiFitzModifier? { + if isEmojiAnimatedSticker, let fitz = self.stickerText?.basicEmoji.1 { + return EmojiFitzModifier(emoji: fitz) + } else { + return nil + } + } + + + var musicText:(String,String) { var audioTitle:String? @@ -456,36 +456,12 @@ public extension TelegramMediaFile { } } -public extension MessageHistoryEntry { - var location:MessageHistoryEntryLocation? { - switch self { - case let .MessageEntry(_, _, location, _): - return location - case let .HoleEntry(_, location): - return location - } - } - - var message:Message? { - switch self { - case let .MessageEntry(message, _, _, _): - return message - default: - return nil - } - } -} public extension MessageHistoryView { func index(for messageId: MessageId) -> Int? { for i in 0 ..< entries.count { - switch entries[i] { - case let .MessageEntry(lhsMessage,_, _, _): - if lhsMessage.id == messageId { - return i - } - default: - break + if entries[i].index.id == messageId { + return i } } return nil @@ -504,71 +480,325 @@ public extension Message { return nil } - var autoremoveAttribute:AutoremoveTimeoutMessageAttribute? { + var isImported: Bool { + if let forwardInfo = self.forwardInfo, forwardInfo.flags.contains(.isImported) { + return true + } + return false + } + var itHasRestrictedContent: Bool { + #if APP_STORE || DEBUG for attr in attributes { - if let attr = attr as? AutoremoveTimeoutMessageAttribute { - return attr + if let attr = attr as? RestrictedContentMessageAttribute { + for rule in attr.rules { + if rule.platform == "ios" || rule.platform == "macos" { + return true + } + } + } + } + #endif + + return false + } + func restrictedText(_ contentSettings: ContentSettings) -> String? { + #if APP_STORE || DEBUG + for attr in attributes { + if let attr = attr as? RestrictedContentMessageAttribute { + for rule in attr.rules { + if rule.platform == "ios" || rule.platform == "all" { + return !contentSettings.ignoreContentRestrictionReasons.contains(rule.reason) ? rule.text : nil + } + } } } + #endif return nil } - var inlinePeer:Peer? { - for attribute in attributes { - if let attribute = attribute as? InlineBotMessageAttribute { - return peers[attribute.peerId] + var textEntities: TextEntitiesMessageAttribute? { + for attr in attributes { + if let attr = attr as? TextEntitiesMessageAttribute { + return attr } } - return author + return nil } - func withUpdatedStableId(_ stableId:UInt32) -> Message { - return Message(stableId: stableId, stableVersion: stableVersion, id: id, globallyUniqueId: globallyUniqueId, timestamp: timestamp, flags: flags, tags: tags, globalTags: globalTags, forwardInfo: forwardInfo, author: author, text: text, attributes: attributes, media: media, peers: peers, associatedMessages: associatedMessages, associatedMessageIds: associatedMessageIds) + var isAnonymousMessage: Bool { + if let author = self.author as? TelegramChannel, sourceReference == nil, self.id.peerId == author.id { + return true + } else { + return false + } } - func possibilityForwardTo(_ peer:Peer) -> Bool { - if !peer.canSendMessage { - return false - } else if let peer = peer as? TelegramChannel { - - if let media = media.first, !(media is TelegramMediaWebpage) { - if let media = media as? TelegramMediaFile { - if media.isSticker { - return !peer.hasBannedRights(.banSendStickers) - } else if media.isVideo && media.isAnimated { - return !peer.hasBannedRights(.banSendGifs) - } - } - return !peer.hasBannedRights(.banSendMedia) + var replyThread: ReplyThreadMessageAttribute? { + for attr in attributes { + if let attr = attr as? ReplyThreadMessageAttribute { + return attr } } - return true + return nil } -} - -extension SuggestedLocalizationInfo { - func localizedKey(_ key:String) -> String { - for entry in extractedEntries { - switch entry { - case let.string(_key, _value): - if _key == key { - return _value - } - default: + + func isCrosspostFromChannel(account: Account) -> Bool { + + var sourceReference: SourceReferenceMessageAttribute? + for attribute in self.attributes { + if let attribute = attribute as? SourceReferenceMessageAttribute { + sourceReference = attribute break } } - return NSLocalizedString(key, comment: "") - } -} + + var isCrosspostFromChannel = false + if let _ = sourceReference { + if self.id.peerId != account.peerId { + isCrosspostFromChannel = true + } + } -public extension MessageId { - func toInt64() -> Int64 { - return (Int64(id) << 32) | Int64(peerId.id) + return isCrosspostFromChannel } -} - - + + var channelViewsCount: Int32? { + for attribute in self.attributes { + if let attribute = attribute as? ViewCountMessageAttribute { + return Int32(attribute.count) + } + } + return nil + } + + var isScheduledMessage: Bool { + return self.id.namespace == Namespaces.Message.ScheduledCloud || self.id.namespace == Namespaces.Message.ScheduledLocal + } + + var wasScheduled: Bool { + for attr in attributes { + if attr is OutgoingScheduleInfoMessageAttribute { + return true + } + } + return self.flags.contains(.WasScheduled) + } + + var isPublicPoll: Bool { + if let media = self.media.first as? TelegramMediaPoll { + return media.publicity == .public + } + return false + } + + var isHasInlineKeyboard: Bool { + return replyMarkup?.flags.contains(.inline) ?? false + } + + func isIncoming(_ account: Account, _ isBubbled: Bool) -> Bool { + if isBubbled, let peer = chatPeer(account.peerId), peer.isChannel { + return true + } + + if id.peerId == account.peerId { + if let _ = forwardInfo { + return true + } + return false + } + return flags.contains(.Incoming) + } + + func chatPeer(_ accountPeerId: PeerId) -> Peer? { + var _peer: Peer? + if let _ = adAttribute { + return author + } + for attr in attributes { + if let source = attr as? SourceReferenceMessageAttribute { + if let info = forwardInfo { + if let peer = peers[source.messageId.peerId], peer is TelegramChannel, accountPeerId != id.peerId, repliesPeerId != id.peerId { + _peer = peer + } else { + _peer = info.author + } + } + break + } + } + + if let peer = messageMainPeer(self) as? TelegramChannel, case .broadcast(_) = peer.info { + _peer = peer + } else if let author = effectiveAuthor, _peer == nil { + if author is TelegramSecretChat { + return messageMainPeer(self) + } else { + _peer = author + } + } + return _peer + } + + var replyAttribute: ReplyMessageAttribute? { + for attr in attributes { + if let attr = attr as? ReplyMessageAttribute { + return attr + } + } + return nil + } + + var editedAttribute: EditedMessageAttribute? { + for attr in attributes { + if let attr = attr as? EditedMessageAttribute, !attr.isHidden { + return attr + } + } + return nil + } + + var autoremoveAttribute:AutoremoveTimeoutMessageAttribute? { + for attr in attributes { + if let attr = attr as? AutoremoveTimeoutMessageAttribute { + return attr + } + } + return nil + } + + var hasInlineAttribute: Bool { + for attribute in attributes { + if let _ = attribute as? InlineBotMessageAttribute { + return true + } + } + return false + } + + var inlinePeer:Peer? { + for attribute in attributes { + if let attribute = attribute as? InlineBotMessageAttribute, let peerId = attribute.peerId { + return peers[peerId] + } + } + if let peer = messageMainPeer(self), peer.isBot { + return peer + } + return nil + } + + func withUpdatedStableId(_ stableId:UInt32) -> Message { + return Message(stableId: stableId, stableVersion: stableVersion, id: id, globallyUniqueId: globallyUniqueId, groupingKey: groupingKey, groupInfo: groupInfo, threadId: threadId, timestamp: timestamp, flags: flags, tags: tags, globalTags: globalTags, localTags: localTags, forwardInfo: forwardInfo, author: author, text: text, attributes: attributes, media: media, peers: peers, associatedMessages: associatedMessages, associatedMessageIds: associatedMessageIds) + } + func withUpdatedId(_ messageId:MessageId) -> Message { + return Message(stableId: stableId, stableVersion: stableVersion, id: messageId, globallyUniqueId: globallyUniqueId, groupingKey: groupingKey, groupInfo: groupInfo, threadId: threadId, timestamp: timestamp, flags: flags, tags: tags, globalTags: globalTags, localTags: localTags, forwardInfo: forwardInfo, author: author, text: text, attributes: attributes, media: media, peers: peers, associatedMessages: associatedMessages, associatedMessageIds: associatedMessageIds) + } + + func withUpdatedGroupingKey(_ groupingKey:Int64?) -> Message { + return Message(stableId: stableId, stableVersion: stableVersion, id: id, globallyUniqueId: globallyUniqueId, groupingKey: groupingKey, groupInfo: groupInfo, threadId: threadId, timestamp: timestamp, flags: flags, tags: tags, globalTags: globalTags, localTags: localTags, forwardInfo: forwardInfo, author: author, text: text, attributes: attributes, media: media, peers: peers, associatedMessages: associatedMessages, associatedMessageIds: associatedMessageIds) + } + + func withUpdatedTimestamp(_ timestamp: Int32) -> Message { + return Message(stableId: self.stableId, stableVersion: self.stableVersion, id: self.id, globallyUniqueId: self.globallyUniqueId, groupingKey: self.groupingKey, groupInfo: self.groupInfo, threadId: threadId, timestamp: timestamp, flags: self.flags, tags: self.tags, globalTags: self.globalTags, localTags: self.localTags, forwardInfo: self.forwardInfo, author: self.author, text: self.text, attributes: self.attributes, media: self.media, peers: self.peers, associatedMessages: self.associatedMessages, associatedMessageIds: self.associatedMessageIds) + } + + + func withUpdatedText(_ text:String) -> Message { + return Message(stableId: stableId, stableVersion: stableVersion, id: id, globallyUniqueId: globallyUniqueId, groupingKey: groupingKey, groupInfo: groupInfo, threadId: threadId, timestamp: timestamp, flags: flags, tags: tags, globalTags: globalTags, localTags: localTags, forwardInfo: forwardInfo, author: author, text: text, attributes: attributes, media: media, peers: peers, associatedMessages: associatedMessages, associatedMessageIds: associatedMessageIds) + } + + func possibilityForwardTo(_ peer:Peer) -> Bool { + if !peer.canSendMessage(false) { + return false + } else if let peer = peer as? TelegramChannel { + if let media = media.first, !(media is TelegramMediaWebpage) { + if let media = media as? TelegramMediaFile { + if media.isStaticSticker { + return !peer.hasBannedRights(.banSendStickers) + } else if media.isVideo && media.isAnimated { + return !peer.hasBannedRights(.banSendGifs) + } + } + return !peer.hasBannedRights(.banSendMedia) + } + } + return true + } + + convenience init(_ media: Media, stableId: UInt32, messageId: MessageId) { + self.init(stableId: stableId, stableVersion: 0, id: messageId, globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [media], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: []) + } +} + +extension ChatLocation { + var unreadMessageCountsItem: UnreadMessageCountsItem { + switch self { + case let .peer(peerId): + return .peer(peerId) + case let .replyThread(data): + return .peer(data.messageId.peerId) + } + } + + var postboxViewKey: PostboxViewKey { + switch self { + case let .peer(peerId): + return .peer(peerId: peerId, components: []) + case let .replyThread(data): + return .peer(peerId: data.messageId.peerId, components: []) + } + } + + var pinnedItemId: PinnedItemId { + switch self { + case let .peer(peerId): + return .peer(peerId) + case let .replyThread(data): + return .peer(data.messageId.peerId) + } + } + + var peerId: PeerId { + switch self { + case let .peer(peerId): + return peerId + case let .replyThread(data): + return data.messageId.peerId + } + } +} + +extension ChatLocation : Hashable { + + func hash(into hasher: inout Hasher) { + + } + +} + +extension SuggestedLocalizationInfo { + func localizedKey(_ key:String) -> String { + for entry in extractedEntries { + switch entry { + case let.string(_key, _value): + if _key == key { + return _value + } + default: + break + } + } + return NSLocalizedString(key, comment: "") + } +} + +public extension MessageId { + func toInt64() -> Int64 { + return (Int64(id) << 32) | peerId.id._internalGetInt64Value() + } +} + + public extension ReplyMarkupMessageAttribute { var hasButtons:Bool { return !self.rows.isEmpty @@ -577,16 +807,20 @@ public extension ReplyMarkupMessageAttribute { fileprivate let edit_limit_time:Int32 = 48*60*60 -func canDeleteMessage(_ message:Message, account:Account) -> Bool { +func canDeleteMessage(_ message:Message, account:Account, mode: ChatMode) -> Bool { + + if mode.threadId == message.id { + return false + } if let channel = message.peers[message.id.peerId] as? TelegramChannel { if case .broadcast = channel.info { if !message.flags.contains(.Incoming) { - return channel.hasAdminRights(.canPostMessages) + return channel.hasPermission(.sendMessages) } - return channel.hasAdminRights(.canDeleteMessages) + return channel.hasPermission(.deleteAllMessages) } - return channel.hasAdminRights(.canDeleteMessages) || !message.flags.contains(.Incoming) + return channel.hasPermission(.deleteAllMessages) || !message.flags.contains(.Incoming) } else if message.peers[message.id.peerId] is TelegramSecretChat { return true } else { @@ -606,50 +840,138 @@ func uniquePeers(from peers:[Peer], defaultExculde:[PeerId] = []) -> [Peer] { } } -func canForwardMessage(_ message:Message, account:Account) -> Bool { - +func canForwardMessage(_ message:Message, chatInteraction: ChatInteraction) -> Bool { + if message.peers[message.id.peerId] is TelegramSecretChat { return false } + + if message.flags.contains(.Failed) || message.flags.contains(.Unsent) { + return false + } + if message.isScheduledMessage { + return false + } + if message.media.first is TelegramMediaAction { return false } + + + if let peer = message.peers[message.id.peerId] as? TelegramUser { - if peer.isUser, let _ = message.autoremoveAttribute { - return false + if peer.isUser, let timer = message.autoremoveAttribute { + if !chatInteraction.hasSetDestructiveTimer { + return false + } else if timer.timeout <= 60 { + return false; + } } } return true } -func canDeleteForEveryoneMessage(_ message:Message, account:Account) -> Bool { +public struct ChatAvailableMessageActionOptions: OptionSet { + public var rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public init() { + self.rawValue = 0 + } + + public static let deleteLocally = ChatAvailableMessageActionOptions(rawValue: 1 << 0) + public static let deleteGlobally = ChatAvailableMessageActionOptions(rawValue: 1 << 1) + public static let unsendPersonal = ChatAvailableMessageActionOptions(rawValue: 1 << 7) +} + + + +func canDeleteForEveryoneMessage(_ message:Message, context: AccountContext) -> Bool { if message.peers[message.id.peerId] is TelegramChannel || message.peers[message.id.peerId] is TelegramSecretChat { return false } else if message.peers[message.id.peerId] is TelegramUser || message.peers[message.id.peerId] is TelegramGroup { - if message.author?.id == account.peerId && edit_limit_time + message.timestamp > Int32(Date().timeIntervalSince1970) { - if account.peerId != messageMainPeer(message)?.id { - return !(message.media.first is TelegramMediaAction) + if message.id.peerId == repliesPeerId { + return false + } + if context.limitConfiguration.canRemoveIncomingMessagesInPrivateChats && message.peers[message.id.peerId] is TelegramUser { + + if message.media.first is TelegramMediaDice, message.peers[message.id.peerId] is TelegramUser { + if Int(message.timestamp) + 24 * 60 * 60 > context.timestamp { + return false + } } - } else if let peer = message.peers[message.id.peerId] as? TelegramGroup { + + return true + } + if let peer = message.peers[message.id.peerId] as? TelegramGroup { switch peer.role { case .creator, .admin: return true default: + if Int(context.limitConfiguration.maxMessageEditingInterval) + Int(message.timestamp) > Int(Date().timeIntervalSince1970) { + if context.account.peerId == message.effectiveAuthor?.id { + return !(message.media.first is TelegramMediaAction) + } + } return false } + } else if Int(context.limitConfiguration.maxMessageEditingInterval) + Int(message.timestamp) > Int(Date().timeIntervalSince1970) { + if context.account.peerId == message.author?.id { + return !(message.media.first is TelegramMediaAction) + } + } + } + return false +} + +func mustDeleteForEveryoneMessage(_ message:Message) -> Bool { + if message.peers[message.id.peerId] is TelegramChannel || message.peers[message.id.peerId] is TelegramSecretChat { + return true + } + return false +} + +func canReplyMessage(_ message: Message, peerId: PeerId, mode: ChatMode) -> Bool { + if let peer = messageMainPeer(message) { + if message.isScheduledMessage { + return false + } + if peerId == message.id.peerId, !message.flags.contains(.Unsent) && !message.flags.contains(.Failed) && (message.id.namespace != Namespaces.Message.Local || message.id.peerId.namespace == Namespaces.Peer.SecretChat) { + + switch mode { + case .history: + return peer.canSendMessage(false) + case .scheduled: + return false + case let .replyThread(data, mode): + switch mode { + case .comments: + if message.id == data.messageId { + return false + } + return peer.canSendMessage(true) + case .replies: + return peer.canSendMessage(true) + } + case .pinned, .preview: + return false + } } } return false } -func canEditMessage(_ message:Message, account:Account) -> Bool { +func canEditMessage(_ message:Message, chatInteraction: ChatInteraction, context: AccountContext) -> Bool { if message.forwardInfo != nil { return false } - if message.flags.contains(.Unsent) || message.flags.contains(.Failed) { + if message.flags.contains(.Unsent) || message.flags.contains(.Failed) || message.id.namespace == Namespaces.Message.Local { return false } @@ -659,29 +981,70 @@ func canEditMessage(_ message:Message, account:Account) -> Bool { if let media = message.media.first { if let file = media as? TelegramMediaFile { - if file.isSticker { + if file.isStaticSticker || (file.isAnimatedSticker && !file.isEmojiAnimatedSticker) { return false } if file.isInstantVideo { return false } +// if file.isVoice { +// return false +// } + } + if media is TelegramMediaContact { + return false } if media is TelegramMediaAction { return false } + if media is TelegramMediaMap { + return false + } + if media is TelegramMediaPoll { + return false + } + if media is TelegramMediaDice { + return false + } + } + + for attr in message.attributes { + if attr is InlineBotMessageAttribute { + return false + } else if attr is AutoremoveTimeoutMessageAttribute { + if !chatInteraction.hasSetDestructiveTimer { + return false + } + } } + var timeInCondition = Int(message.timestamp) + Int(context.limitConfiguration.maxMessageEditingInterval) > context.account.network.getApproximateRemoteTimestamp() + if let peer = messageMainPeer(message) as? TelegramChannel { if case .broadcast = peer.info { - if peer.hasAdminRights(.canEditMessages) { - return message.timestamp + edit_limit_time > Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - } else if !peer.hasAdminRights(.canPostMessages) { - return false + if message.isScheduledMessage { + return peer.hasPermission(.sendMessages) || peer.hasPermission(.editAllMessages) + } + if peer.hasPermission(.pinMessages) { + timeInCondition = true + } + if peer.hasPermission(.editAllMessages) { + return timeInCondition + } else if peer.hasPermission(.sendMessages) { + return timeInCondition && message.author?.id == chatInteraction.context.peerId + } + return false + } else if case .group = peer.info { + if !message.flags.contains(.Incoming) { + if peer.hasPermission(.pinMessages) { + return true + } + return timeInCondition } } } - if message.id.peerId == account.peerId { + if message.id.peerId == context.account.peerId { return true } @@ -690,22 +1053,38 @@ func canEditMessage(_ message:Message, account:Account) -> Bool { return false } - if message.timestamp + edit_limit_time < Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { + + if Int(message.timestamp) + Int(context.limitConfiguration.maxMessageEditingInterval) < context.account.network.getApproximateRemoteTimestamp() { return false } - return true + + + return !message.flags.contains(.Unsent) && !message.flags.contains(.Failed) } + func canPinMessage(_ message:Message, for peer:Peer, account:Account) -> Bool { return false } +func canReportMessage(_ message: Message, _ account: Account) -> Bool { + if message.isScheduledMessage || message.flags.contains(.Failed) || message.flags.contains(.Sending) { + return false + } + if let peer = messageMainPeer(message), message.author?.id != account.peerId { + return peer.isChannel || peer.isGroup || peer.isSupergroup || (message.chatPeer(account.peerId)?.isBot == true) + } else { + return false + } +} + func mustManageDeleteMessages(_ messages:[Message], for peer:Peer, account: Account) -> Bool { - if peer.isSupergroup, peer.groupAccess.canManageGroup { + + if let peer = peer as? TelegramChannel, peer.isSupergroup, peer.hasPermission(.deleteAllMessages) { let peerId:PeerId? = messages[0].author?.id if account.peerId != peerId { for message in messages { @@ -723,38 +1102,67 @@ func mustManageDeleteMessages(_ messages:[Message], for peer:Peer, account: Acco extension Media { var isGraphicFile:Bool { if let media = self as? TelegramMediaFile { - return media.mimeType.hasPrefix("image") + return media.mimeType.hasPrefix("image") && (media.mimeType.contains("png") || media.mimeType.contains("jpg") || media.mimeType.contains("jpeg") || media.mimeType.contains("tiff") || media.mimeType.contains("heic")) } return false } -} - -extension AddressNameFormatError { - var description:String { - switch self { - case .startsWithUnderscore: - return tr(.errorUsernameUnderscopeStart) - case .endsWithUnderscore: - return tr(.errorUsernameUnderscopeEnd) - case .startsWithDigit: - return tr(.errorUsernameNumberStart) - case .invalidCharacters: - return tr(.errorUsernameInvalid) + var isVideoFile:Bool { + if let media = self as? TelegramMediaFile { + return media.mimeType.hasPrefix("video/mp4") || media.mimeType.hasPrefix("video/mov") || media.mimeType.hasPrefix("video/avi") + } + return false + } + var isMusicFile: Bool { + if let media = self as? TelegramMediaFile { + for attr in media.attributes { + switch attr { + case let .Audio(isVoice, _, _, _, _): + return !isVoice + default: + return false + } + } + } + return false + } + + var supposeToBeSticker:Bool { + if let media = self as? TelegramMediaFile { + if media.mimeType.hasPrefix("image/webp") { + return true + } + } + return false + } +} + +extension AddressNameFormatError { + var description:String { + switch self { + case .startsWithUnderscore: + return tr(L10n.errorUsernameUnderscopeStart) + case .endsWithUnderscore: + return tr(L10n.errorUsernameUnderscopeEnd) + case .startsWithDigit: + return tr(L10n.errorUsernameNumberStart) + case .invalidCharacters: + return tr(L10n.errorUsernameInvalid) case .tooShort: - return tr(.errorUsernameMinimumLength) + return tr(L10n.errorUsernameMinimumLength) } } } extension AddressNameAvailability { - var description:String { + + func description(for username: String) -> String { switch self { case .available: - return "available" + return L10n.usernameSettingsAvailable(username) case .invalid: - return tr(.errorUsernameInvalid) + return L10n.errorUsernameInvalid case .taken: - return tr(.errorUsernameAlreadyTaken) + return L10n.errorUsernameAlreadyTaken } } } @@ -766,19 +1174,25 @@ func <(lhs:RenderedChannelParticipant, rhs: RenderedChannelParticipant) -> Bool switch lhs.participant { case .creator: lhsInvitedAt = Int32.min - case .member(_, let invitedAt, _, _): + case .member(_, let invitedAt, _, _, _): lhsInvitedAt = invitedAt } switch rhs.participant { case .creator: rhsInvitedAt = Int32.min - case .member(_, let invitedAt, _, _): + case .member(_, let invitedAt, _, _, _): rhsInvitedAt = invitedAt } return lhsInvitedAt < rhsInvitedAt } +extension TelegramGroup { + var canPinMessage: Bool { + return !hasBannedRights(.banPinMessages) + } +} + extension Peer { var isUser:Bool { return self is TelegramUser @@ -789,6 +1203,71 @@ extension Peer { var isGroup:Bool { return self is TelegramGroup } + var canManageDestructTimer: Bool { + if self is TelegramSecretChat { + return true + } + if self.isUser && !self.isBot { + return true + } + if let peer = self as? TelegramChannel { + if let adminRights = peer.adminRights, adminRights.rights.contains(.canDeleteMessages) { + return true + } else if peer.groupAccess.isCreator { + return true + } + return false + } + if let peer = self as? TelegramGroup { + switch peer.role { + case .admin, .creator: + return true + default: + break + } + } + return false + } + + var canClearHistory: Bool { + if self.isGroup || self.isUser || (self.isSupergroup && self.addressName == nil) { + if let peer = self as? TelegramChannel, peer.flags.contains(.hasGeo) {} else { + return true + } + } + if self is TelegramSecretChat { + return true + } + return false + } + + func isRestrictedChannel(_ contentSettings: ContentSettings) -> Bool { + if let peer = self as? TelegramChannel { + if let restrictionInfo = peer.restrictionInfo { + for rule in restrictionInfo.rules { + #if APP_STORE + if rule.platform == "ios" || rule.platform == "all" { + return !contentSettings.ignoreContentRestrictionReasons.contains(rule.reason) + } + #endif + } + } + } + return false + } + + var restrictionText:String? { + if let peer = self as? TelegramChannel { + if let restrictionInfo = peer.restrictionInfo { + for rule in restrictionInfo.rules { + if rule.platform == "ios" || rule.platform == "all" { + return rule.text + } + } + } + } + return nil + } var isSupergroup:Bool { if let peer = self as? TelegramChannel { @@ -822,6 +1301,12 @@ extension Peer { } return false } + var isGigagroup:Bool { + if let peer = self as? TelegramChannel { + return peer.flags.contains(.isGigagroup) + } + return false + } } @@ -871,106 +1356,1996 @@ public func ==(lhs:AddressNameAvailabilityState, rhs:AddressNameAvailabilityStat } } - - -public func peerCompactDisplayTitles(_ peerIds: [PeerId], _ dict: SimpleDictionary) -> String { - var names:String = "" - for peerId in peerIds { - if let peer = dict[peerId] { - names += peer.compactDisplayTitle - if peerId != peerIds.last { - names += ", " - } +extension Signal { + + public static func next(_ value: T) -> Signal { + return Signal { subscriber in + subscriber.putNext(value) + + return EmptyDisposable } } - return names } -func mediaResource(from media:Media?) -> TelegramMediaResource? { - if let media = media as? TelegramMediaFile { - return media.resource - } else if let media = media as? TelegramMediaImage { - return largestImageRepresentation(media.representations)?.resource +extension SentSecureValueType { + var rawValue: String { + switch self { + case .email: + return L10n.secureIdRequestPermissionEmail + case .phone: + return L10n.secureIdRequestPermissionPhone + case .passport: + return L10n.secureIdRequestPermissionPassport + case .address: + return L10n.secureIdRequestPermissionResidentialAddress + case .personalDetails: + return L10n.secureIdRequestPermissionPersonalDetails + case .driversLicense: + return L10n.secureIdRequestPermissionDriversLicense + case .utilityBill: + return L10n.secureIdRequestPermissionUtilityBill + case .rentalAgreement: + return L10n.secureIdRequestPermissionTenancyAgreement + case .idCard: + return L10n.secureIdRequestPermissionIDCard + case .bankStatement: + return L10n.secureIdRequestPermissionBankStatement + case .internalPassport: + return L10n.secureIdRequestPermissionInternalPassport + case .passportRegistration: + return L10n.secureIdRequestPermissionPassportRegistration + case .temporaryRegistration: + return L10n.secureIdRequestPermissionTemporaryRegistration + } } - return nil } - -func mediaResourceMIMEType(from media:Media?) -> String? { - if let media = media as? TelegramMediaFile { - return media.mimeType - } else if media is TelegramMediaImage { - return "image/jpeg" +extension TwoStepVerificationPendingEmail : Equatable { + public static func == (lhs: TwoStepVerificationPendingEmail, rhs: TwoStepVerificationPendingEmail) -> Bool { + return lhs.codeLength == rhs.codeLength && lhs.pattern == rhs.pattern } - return nil + + } -func mediaResourceName(from media:Media?, ext:String?) -> String { - - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" - let ext = ext ?? ".file" - if let media = media as? TelegramMediaFile { - return media.fileName ?? "FILE " + dateFormatter.string(from: Date()) + "." + ext - } else if media is TelegramMediaImage { - return "IMAGE " + dateFormatter.string(from: Date()) + "." + ext +extension UpdateTwoStepVerificationPasswordResult : Equatable { + public static func ==(lhs: UpdateTwoStepVerificationPasswordResult, rhs: UpdateTwoStepVerificationPasswordResult) -> Bool { + switch lhs { + case .none: + if case .none = rhs { + return true + } else { + return false + } + case let .password(password, lhsPendingEmailPattern): + if case .password(password, let rhsPendingEmailPattern) = rhs { + return lhsPendingEmailPattern == rhsPendingEmailPattern + } else { + return false + } + } } - return "FILE " + dateFormatter.string(from: Date()) + "." + ext } -func removeChatInteractively(account:Account, peerId:PeerId) -> Signal { - return account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue |> mapToSignal { peer -> Signal in - let text:String - if let peer = peer as? TelegramChannel { - switch peer.info { - case .broadcast: - if peer.flags.contains(.isCreator) { - text = tr(.confirmDeleteAdminedChannel) - } else { - text = tr(.peerInfoConfirmLeaveChannel) - } - case .group: - text = tr(.peerInfoConfirmLeaveGroup) + +extension SecureIdGender { + static func gender(from mrz: TGPassportMRZ) -> SecureIdGender { + switch mrz.gender.lowercased() { + case "f": + return .female + default: + return .male + } + } +} + +extension SecureIdRequestedFormField { + var isIdentityField: Bool { + switch self { + case let .just(field): + switch field { + case .idCard, .passport, .driversLicense, .internalPassport: + return true + default: + return false + } + case let .oneOf(fields): + switch fields[0] { + case .idCard, .passport, .driversLicense, .internalPassport: + return true + default: + return false } - } else if let peer = peer as? TelegramGroup { - text = tr(.peerInfoConfirmDeleteChat(peer.title)) - } else { - text = tr(.confirmDeleteChatUser) } - - return confirmSignal(for: mainWindow, header: appName, information: text) |> mapToSignal { result -> Signal in - if result { - return removePeerChat(postbox: account.postbox, peerId: peerId, reportChatSpam: false) |> map {_ in return true} + } + + var valueKey: SecureIdValueKey? { + switch self { + case let .just(field): + return field.valueKey + default: + return nil + } + } + + var fieldValue: SecureIdRequestedFormFieldValue? { + switch self { + case let .just(field): + return field + default: + return nil + } + } + + var isAddressField: Bool { + switch self { + case let .just(field): + switch field { + case .utilityBill, .bankStatement, .rentalAgreement, .passportRegistration, .temporaryRegistration: + return true + default: + return false + } + case let .oneOf(fields): + switch fields[0] { + case .utilityBill, .bankStatement, .rentalAgreement, .passportRegistration, .temporaryRegistration: + return true + default: + return false } - return .single(false) } } - } -func applyExternalProxy(_ proxy:ProxySettings, postbox:Postbox, network: Network) { - var textInfo = tr(.proxyForceEnableTextIP(proxy.host)) + "\n" + tr(.proxyForceEnableTextPort(Int(proxy.port))) - if let user = proxy.username { - textInfo += "\n" + tr(.proxyForceEnableTextUsername(user)) - } - if let pass = proxy.password { - textInfo += "\n" + tr(.proxyForceEnableTextPassword(pass)) +extension SecureIdForm { + func searchContext(for field: SecureIdRequestedFormFieldValue) -> SecureIdValueWithContext? { + let index = values.index(where: { context -> Bool in + switch context.value { + case .address: + if case .address = field { + return true + } else { + return false + } + case .bankStatement: + if case .bankStatement = field { + return true + } else { + return false + } + case .driversLicense: + if case .driversLicense = field { + return true + } else { + return false + } + case .idCard: + if case .idCard = field { + return true + } else { + return false + } + case .passport: + if case .passport = field { + return true + } else { + return false + } + case .personalDetails: + if case .personalDetails = field { + return true + } else { + return false + } + case .rentalAgreement: + if case .rentalAgreement = field { + return true + } else { + return false + } + case .utilityBill: + if case .utilityBill = field { + return true + } else { + return false + } + case .phone: + if case .phone = field { + return true + } else { + return false + } + case .email: + if case .email = field { + return true + } else { + return false + } + case .internalPassport: + if case .internalPassport = field { + return true + } else { + return false + } + case .passportRegistration: + if case .passportRegistration = field { + return true + } else { + return false + } + case .temporaryRegistration: + if case .temporaryRegistration = field { + return true + } else { + return false + } + } + }) + if let index = index { + return values[index] + } else { + return nil + } } - textInfo += "\n\n" + tr(.proxyForceEnableText) - _ = (confirmSignal(for: mainWindow, header: tr(.proxyForceEnableHeader), information: textInfo) - |> filter {$0} |> map {_ in} |> mapToSignal { - return applyProxySettings(postbox: postbox, network: network, settings: proxy) - }).start() + } -extension PostboxAccessChallengeData { - var timeout:Int32? { +extension SecureIdValue { + func isSame(of value: SecureIdValue) -> Bool { switch self { - case .none: - return nil - case let .numericalPassword(_, timeout, _), let .plaintextPassword(_, timeout, _): - return timeout - } + case .address: + if case .address = value { + return true + } else { + return false + } + case .bankStatement: + if case .bankStatement = value { + return true + } else { + return false + } + case .driversLicense: + if case .driversLicense = value { + return true + } else { + return false + } + case .idCard: + if case .idCard = value { + return true + } else { + return false + } + case .passport: + if case .passport = value { + return true + } else { + return false + } + case .personalDetails: + if case .personalDetails = value { + return true + } else { + return false + } + case .rentalAgreement: + if case .rentalAgreement = value { + return true + } else { + return false + } + case .utilityBill: + if case .utilityBill = value { + return true + } else { + return false + } + case .phone: + if case .phone = value { + return true + } else { + return false + } + case .email: + if case .email = value { + return true + } else { + return false + } + case .internalPassport(_): + if case .internalPassport = value { + return true + } else { + return false + } + case .passportRegistration(_): + if case .passportRegistration = value { + return true + } else { + return false + } + case .temporaryRegistration(_): + if case .temporaryRegistration = value { + return true + } else { + return false + } + } + } + func isSame(of value: SecureIdValueKey) -> Bool { + return self.key == value + } + + + + var secureIdValueAccessContext: SecureIdValueAccessContext? { + switch self { + case .email: + return generateSecureIdValueEmptyAccessContext() + case .phone: + return generateSecureIdValueEmptyAccessContext() + default: + return generateSecureIdValueAccessContext() + } + } + + + var addressValue: SecureIdAddressValue? { + switch self { + case let .address(value): + return value + default: + return nil + } + } + + var identifier: String? { + switch self { + case let .passport(value): + return value.identifier + case let .driversLicense(value): + return value.identifier + case let .idCard(value): + return value.identifier + case let .internalPassport(value): + return value.identifier + default: + return nil + } + } + + var personalDetails: SecureIdPersonalDetailsValue? { + switch self { + case let .personalDetails(value): + return value + default: + return nil + } + } + + var selfieVerificationDocument: SecureIdVerificationDocumentReference? { + switch self { + case let .idCard(value): + return value.selfieDocument + case let .passport(value): + return value.selfieDocument + case let .driversLicense(value): + return value.selfieDocument + case let .internalPassport(value): + return value.selfieDocument + default: + return nil + } + } + + var verificationDocuments: [SecureIdVerificationDocumentReference]? { + switch self { + case let .bankStatement(value): + return value.verificationDocuments + case let .rentalAgreement(value): + return value.verificationDocuments + case let .utilityBill(value): + return value.verificationDocuments + case let .passportRegistration(value): + return value.verificationDocuments + case let .temporaryRegistration(value): + return value.verificationDocuments + default: + return nil + } + } + + var translations: [SecureIdVerificationDocumentReference]? { + switch self { + case let .passport(value): + return value.translations + case let .idCard(value): + return value.translations + case let .driversLicense(value): + return value.translations + case let .internalPassport(value): + return value.translations + case let .utilityBill(value): + return value.translations + case let .rentalAgreement(value): + return value.translations + case let .temporaryRegistration(value): + return value.translations + case let .passportRegistration(value): + return value.translations + case let .bankStatement(value): + return value.translations + default: + return nil + } + } + + var frontSideVerificationDocument: SecureIdVerificationDocumentReference? { + switch self { + case let .idCard(value): + return value.frontSideDocument + case let .passport(value): + return value.frontSideDocument + case let .driversLicense(value): + return value.frontSideDocument + case let .internalPassport(value): + return value.frontSideDocument + default: + return nil + } + } + + var backSideVerificationDocument: SecureIdVerificationDocumentReference? { + switch self { + case let .idCard(value): + return value.backSideDocument + case let .driversLicense(value): + return value.backSideDocument + default: + return nil + } + } + + var hasBacksideDocument: Bool { + switch self { + case .idCard: + return true + case .driversLicense: + return true + default: + return false + } + } + + var passportValue: SecureIdPassportValue? { + switch self { + case let .passport(value): + return value + default: + return nil + } + } + + var phoneValue: SecureIdPhoneValue? { + switch self { + case let .phone(value): + return value + default: + return nil + } + } + var emailValue: SecureIdEmailValue? { + switch self { + case let .email(value): + return value + default: + return nil + } + } + + var requestFieldType: SecureIdRequestedFormFieldValue { + return key.requestFieldType + } + + var expiryDate: SecureIdDate? { + switch self { + case let .idCard(value): + return value.expiryDate + case let .passport(value): + return value.expiryDate + case let .driversLicense(value): + return value.expiryDate + default: + return nil + } + } +} + +extension SecureIdValueKey { + var requestFieldType: SecureIdRequestedFormFieldValue { + switch self { + case .address: + return .address + case .bankStatement: + return .bankStatement(translation: true) + case .driversLicense: + return .driversLicense(selfie: true, translation: true) + case .email: + return .email + case .idCard: + return .idCard(selfie: true, translation: true) + case .internalPassport: + return .internalPassport(selfie: true, translation: true) + case .passport: + return .passport(selfie: true, translation: true) + case .passportRegistration: + return .passportRegistration(translation: true) + case .personalDetails: + return .personalDetails(nativeName: true) + case .phone: + return .phone + case .rentalAgreement: + return .rentalAgreement(translation: true) + case .temporaryRegistration: + return .temporaryRegistration(translation: true) + case .utilityBill: + return .utilityBill(translation: true) + } + } +} + + +extension SecureIdRequestedFormFieldValue { + var rawValue: String { + switch self { + case .email: + return L10n.secureIdRequestPermissionEmail + case .phone: + return L10n.secureIdRequestPermissionPhone + case .address: + return L10n.secureIdRequestPermissionResidentialAddress + case .utilityBill: + return L10n.secureIdRequestPermissionUtilityBill + case .bankStatement: + return L10n.secureIdRequestPermissionBankStatement + case .rentalAgreement: + return L10n.secureIdRequestPermissionTenancyAgreement + case .passport: + return L10n.secureIdRequestPermissionPassport + case .idCard: + return L10n.secureIdRequestPermissionIDCard + case .driversLicense: + return L10n.secureIdRequestPermissionDriversLicense + case .personalDetails: + return L10n.secureIdRequestPermissionPersonalDetails + case .internalPassport: + return L10n.secureIdRequestPermissionInternalPassport + case .passportRegistration: + return L10n.secureIdRequestPermissionPassportRegistration + case .temporaryRegistration: + return L10n.secureIdRequestPermissionTemporaryRegistration + } + } + + func isKindOf(_ fieldValue: SecureIdRequestedFormFieldValue) -> Bool { + switch self { + case .email: + if case .email = fieldValue { + return true + } else { + return false + } + case .phone: + if case .phone = fieldValue { + return true + } else { + return false + } + case .address: + if case .address = fieldValue { + return true + } else { + return false + } + case .utilityBill: + if case .utilityBill = fieldValue { + return true + } else { + return false + } + case .bankStatement: + if case .bankStatement = fieldValue { + return true + } else { + return false + } + case .rentalAgreement: + if case .rentalAgreement = fieldValue { + return true + } else { + return false + } + case .passport: + if case .passport = fieldValue { + return true + } else { + return false + } + case .idCard: + if case .idCard = fieldValue { + return true + } else { + return false + } + case .driversLicense: + if case .driversLicense = fieldValue { + return true + } else { + return false + } + case .personalDetails: + if case .personalDetails = fieldValue { + return true + } else { + return false + } + case .internalPassport: + if case .internalPassport = fieldValue { + return true + } else { + return false + } + case .passportRegistration: + if case .passportRegistration = fieldValue { + return true + } else { + return false + } + case .temporaryRegistration: + if case .temporaryRegistration = fieldValue { + return true + } else { + return false + } + } + } + + var uploadFrontTitleText: String { + switch self { + case .idCard: + return L10n.secureIdUploadFront + case .driversLicense: + return L10n.secureIdUploadFront + default: + return L10n.secureIdUploadMain + } + } + var uploadBackTitleText: String { + switch self { + case .idCard: + return L10n.secureIdUploadReverse + case .driversLicense: + return L10n.secureIdUploadReverse + default: + return L10n.secureIdUploadMain + } + } + + var hasBacksideDocument: Bool { + switch self { + case .idCard: + return true + case .driversLicense: + return true + default: + return false + } + } + + var hasSelfie: Bool { + switch self { + case let .passport(selfie, _), let .idCard(selfie, _), let .driversLicense(selfie, _), let .internalPassport(selfie, _): + return selfie + default: + return false + } + } + + var hasTranslation: Bool { + switch self { + case let .passport(_, translation), let .idCard(_, translation), let .driversLicense(_, translation), let .internalPassport(_, translation): + return translation + case let .utilityBill(translation), let .rentalAgreement(translation), let .bankStatement(translation), let .passportRegistration(translation), let .temporaryRegistration(translation): + return translation + default: + return false + } + } + + var emptyDescription: String { + switch self { + case .email: + return L10n.secureIdRequestPermissionEmailEmpty + case .phone: + return L10n.secureIdRequestPermissionPhoneEmpty + case .utilityBill: + return L10n.secureIdEmptyDescriptionUtilityBill + case .bankStatement: + return L10n.secureIdEmptyDescriptionBankStatement + case .rentalAgreement: + return L10n.secureIdEmptyDescriptionTenancyAgreement + case .passportRegistration: + return L10n.secureIdEmptyDescriptionPassportRegistration + case .temporaryRegistration: + return L10n.secureIdEmptyDescriptionTemporaryRegistration + case .passport: + return L10n.secureIdEmptyDescriptionPassport + case .driversLicense: + return L10n.secureIdEmptyDescriptionDriversLicense + case .idCard: + return L10n.secureIdEmptyDescriptionIdentityCard + case .internalPassport: + return L10n.secureIdEmptyDescriptionInternalPassport + case .personalDetails: + return L10n.secureIdEmptyDescriptionPersonalDetails + case .address: + return L10n.secureIdEmptyDescriptionAddress + } + } + + var descAdd: String { + switch self { + case .email: + return "" + case .phone: + return "" + case .address: + return L10n.secureIdAddResidentialAddress + case .utilityBill: + return L10n.secureIdAddUtilityBill + case .bankStatement: + return L10n.secureIdAddBankStatement + case .rentalAgreement: + return L10n.secureIdAddTenancyAgreement + case .passport: + return L10n.secureIdAddPassport + case .idCard: + return L10n.secureIdAddID + case .driversLicense: + return L10n.secureIdAddDriverLicense + case .personalDetails: + return L10n.secureIdAddPersonalDetails + case .internalPassport: + return L10n.secureIdAddInternalPassport + case .passportRegistration: + return L10n.secureIdAddPassportRegistration + case .temporaryRegistration: + return L10n.secureIdAddTemporaryRegistration + } + } + + var descEdit: String { + switch self { + case .email: + return "" + case .phone: + return "" + case .address: + return L10n.secureIdEditResidentialAddress + case .utilityBill: + return L10n.secureIdEditUtilityBill + case .bankStatement: + return L10n.secureIdEditBankStatement + case .rentalAgreement: + return L10n.secureIdEditTenancyAgreement + case .passport: + return L10n.secureIdEditPassport + case .idCard: + return L10n.secureIdEditID + case .driversLicense: + return L10n.secureIdEditDriverLicense + case .personalDetails: + return L10n.secureIdEditPersonalDetails + case .internalPassport: + return L10n.secureIdEditInternalPassport + case .passportRegistration: + return L10n.secureIdEditPassportRegistration + case .temporaryRegistration: + return L10n.secureIdEditTemporaryRegistration + } + } +} + +var dateFormatter: DateFormatter { + let formatter = DateFormatter() + formatter.dateFormat = "dd.MM.yyyy" + // formatter.timeZone = TimeZone(secondsFromGMT: 0) + formatter.locale = Locale(identifier: "en_US_POSIX") + return formatter +} + +extension SecureIdRequestedFormFieldValue { + var valueKey: SecureIdValueKey { + switch self { + case .address: + return .address + case .bankStatement: + return .bankStatement + case .driversLicense: + return .driversLicense + case .email: + return .email + case .idCard: + return .idCard + case .passport: + return .passport + case .personalDetails: + return .personalDetails + case .phone: + return .phone + case .rentalAgreement: + return .rentalAgreement + case .utilityBill: + return .utilityBill + case .internalPassport: + return .internalPassport + case .passportRegistration: + return .passportRegistration + case .temporaryRegistration: + return .temporaryRegistration + } + } + + var primary: SecureIdRequestedFormFieldValue { + if SecureIdRequestedFormField.just(self).isIdentityField { + return .personalDetails(nativeName: true) + } + if SecureIdRequestedFormField.just(self).isAddressField { + return .address + } + return self + } + + func isEqualToMRZ(_ mrz: TGPassportMRZ) -> Bool { + switch mrz.documentType.lowercased() { + case "p": + if case .passport = self { + return true + } else { + return false + } + default: + return false + } + } + +} + + + +extension InputDataValue { + var secureIdDate: SecureIdDate? { + switch self { + case let .date(day, month, year): + if let day = day, let month = month, let year = year { + return SecureIdDate(day: day, month: month, year: year) + } + + return nil + default: + return nil + } + } +} + +extension SecureIdDate { + var inputDataValue: InputDataValue { + return .date(day, month, year) + } +} + + +public func peerCompactDisplayTitles(_ peerIds: [PeerId], _ dict: SimpleDictionary) -> String { + var names:String = "" + for peerId in peerIds { + if let peer = dict[peerId] { + names += peer.compactDisplayTitle + if peerId != peerIds.last { + names += ", " + } + } + } + return names +} + +func mediaResource(from media:Media?) -> TelegramMediaResource? { + if let media = media as? TelegramMediaFile { + return media.resource + } else if let media = media as? TelegramMediaImage { + return largestImageRepresentation(media.representations)?.resource + } + return nil +} + +func mediaResourceMIMEType(from media:Media?) -> String? { + if let media = media as? TelegramMediaFile { + return media.mimeType + } else if media is TelegramMediaImage { + return "image/jpeg" + } + return nil +} + +func mediaResourceName(from media:Media?, ext:String?) -> String { + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + let ext = ext ?? ".file" + if let media = media as? TelegramMediaFile { + return media.fileName ?? "FILE " + dateFormatter.string(from: Date()) + "." + ext + } else if media is TelegramMediaImage { + return "IMAGE " + dateFormatter.string(from: Date()) + "." + ext + } + return "FILE " + dateFormatter.string(from: Date()) + "." + ext +} + + +func removeChatInteractively(context: AccountContext, peerId:PeerId, userId: PeerId? = nil, deleteGroup: Bool = false) -> Signal { + return context.account.postbox.peerView(id: peerId) + |> take(1) + |> map { peerViewMainPeer($0) } + |> filter { $0 != nil } + |> map { $0! } + |> deliverOnMainQueue + |> mapToSignal { peer -> Signal in + + + let text:String + var okTitle: String? = nil + if let peer = peer as? TelegramChannel { + switch peer.info { + case .broadcast: + if peer.flags.contains(.isCreator) && deleteGroup { + text = L10n.confirmDeleteAdminedChannel + okTitle = L10n.confirmDelete + } else { + text = L10n.peerInfoConfirmLeaveChannel + } + case .group: + if deleteGroup && peer.flags.contains(.isCreator) { + text = L10n.peerInfoConfirmDeleteGroupConfirmation + okTitle = L10n.confirmDelete + } else { + text = L10n.confirmLeaveGroup + okTitle = L10n.peerInfoConfirmLeave + } + } + } else if let peer = peer as? TelegramGroup { + text = L10n.peerInfoConfirmDeleteChat(peer.title) + okTitle = L10n.confirmDelete + } else { + text = L10n.peerInfoConfirmDeleteUserChat + okTitle = L10n.confirmDelete + } + + + let type: ChatUndoActionType + + if let peer = peer as? TelegramChannel { + switch peer.info { + case .broadcast: + if peer.flags.contains(.isCreator) && deleteGroup { + type = .deleteChannel + } else { + type = .leftChannel + } + case .group: + if peer.flags.contains(.isCreator) && deleteGroup { + type = .deleteChat + } else { + type = .leftChat + } + } + } else { + type = .deleteChat + } + + var thridTitle: String? = nil + + var canRemoveGlobally: Bool = false + if peerId.namespace == Namespaces.Peer.CloudUser && peerId != context.account.peerId && !peer.isBot { + if context.limitConfiguration.maxMessageRevokeIntervalInPrivateChats == LimitsConfiguration.timeIntervalForever { + canRemoveGlobally = true + } + } + if peerId.namespace == Namespaces.Peer.SecretChat { + canRemoveGlobally = false + } + + if canRemoveGlobally { + thridTitle = L10n.chatMessageDeleteForMeAndPerson(peer.displayTitle) + } else if peer.isBot { + thridTitle = L10n.peerInfoStopBot + } + + if peer.groupAccess.isCreator, deleteGroup { + canRemoveGlobally = true + thridTitle = L10n.deleteChatDeleteGroupForAll + } + + + return combineLatest(modernConfirmSignal(for: context.window, account: context.account, peerId: userId ?? peerId, information: text, okTitle: okTitle ?? L10n.alertOK, thridTitle: thridTitle, thridAutoOn: false), context.globalPeerHandler.get() |> take(1)) |> mapToSignal { result, location -> Signal in + + context.chatUndoManager.removePeerChat(engine: context.engine, peerId: peerId, type: type, reportChatSpam: false, deleteGloballyIfPossible: deleteGroup || result == .thrid) + if peer.isBot && result == .thrid { + _ = context.blockedPeersContext.add(peerId: peerId).start() + } + + if location?.peerId == peerId { + context.sharedContext.bindings.rootNavigation().close() + } + + return .single(true) + } + } + +} + +func applyExternalProxy(_ server:ProxyServerSettings, accountManager: AccountManager) { + var textInfo = L10n.proxyForceEnableTextIP(server.host) + "\n" + L10n.proxyForceEnableTextPort(Int(server.port)) + switch server.connection { + case let .socks5(username, password): + if let user = username { + textInfo += "\n" + L10n.proxyForceEnableTextUsername(user) + } + if let pass = password { + textInfo += "\n" + L10n.proxyForceEnableTextPassword(pass) + } + case let .mtp(secret): + textInfo += "\n" + L10n.proxyForceEnableTextSecret(MTProxySecret.parseData(secret)?.serializeToString() ?? "") + } + + textInfo += "\n\n" + L10n.proxyForceEnableText + + if case .mtp = server.connection { + textInfo += "\n\n" + L10n.proxyForceEnableMTPDesc + } + + modernConfirm(for: mainWindow, account: nil, peerId: nil, header: L10n.proxyForceEnableHeader1, information: textInfo, okTitle: L10n.proxyForceEnableOK, thridTitle: L10n.proxyForceEnableEnable, successHandler: { result in + _ = updateProxySettingsInteractively(accountManager: accountManager, { current -> ProxySettings in + + var current = current.withAddedServer(server) + if result == .thrid { + current = current.withUpdatedActiveServer(server).withUpdatedEnabled(true) + } + return current + }).start() + }) + +// _ = (confirmSignal(for: mainWindow, header: tr(L10n.proxyForceEnableHeader), information: textInfo, okTitle: L10n.proxyForceEnableConnect) +// |> filter {$0} |> map {_ in} |> mapToSignal { +// return updateProxySettingsInteractively(postbox: postbox, network: network, { current -> ProxySettings in +// return current.withAddedServer(server).withUpdatedActiveServer(server).withUpdatedEnabled(true) +// }) +// }).start() +} + + +extension SecureIdGender { + var stringValue: String { + switch self { + case .female: + return L10n.secureIdGenderFemale + case .male: + return L10n.secureIdGenderMale + } + } +} + +extension SecureIdDate { + var stringValue: String { + return "\(day).\(month).\(year)" + } +} + + + +func clearCache(_ path: String, excludes: [(partial: String, complete: String)]) -> Signal { + return Signal { subscriber -> Disposable in + + let fileManager = FileManager.default + var enumerator = fileManager.enumerator(atPath: path + "/") + + while let file = enumerator?.nextObject() as? String { + if file != "cache" { + if excludes.filter ({ file.contains($0.partial.nsstring.lastPathComponent) || file.contains($0.complete.nsstring.lastPathComponent) }).isEmpty { + unlink(path + "/" + file) + } + } + } + + var p = path.nsstring.substring(to: path.nsstring.range(of: path.nsstring.lastPathComponent).location) + p = p.nsstring.substring(to: p.nsstring.range(of: p.nsstring.lastPathComponent).location) + "cached/" + + enumerator = fileManager.enumerator(atPath: p) + + while let file = enumerator?.nextObject() as? String { + + + if excludes.filter ({ file.contains($0.partial) || file.contains($0.complete) }).isEmpty { + unlink(p + file) + } + //try? fileManager.removeItem(atPath: p + file) + } + + subscriber.putNext(Void()) + subscriber.putCompletion() + return EmptyDisposable + } |> runOn(resourcesQueue) +} + +func moveWallpaperToCache(postbox: Postbox, resource: TelegramMediaResource, reference: WallpaperReference?, settings: WallpaperSettings, isPattern: Bool) -> Signal { + let resourceData: Signal + if isPattern { + resourceData = postbox.mediaBox.cachedResourceRepresentation(resource, representation: CachedPatternWallpaperMaskRepresentation(size: nil, settings: settings), complete: true) + } else if settings.blur { + resourceData = postbox.mediaBox.cachedResourceRepresentation(resource, representation: CachedBlurredWallpaperRepresentation(), complete: true) + } else { + resourceData = postbox.mediaBox.resourceData(resource) + } + + + return combineLatest(fetchedMediaResource(mediaBox: postbox.mediaBox, reference: MediaResourceReference.wallpaper(wallpaper: reference, resource: resource), reportResultStatus: true) |> `catch` { _ in return .complete() }, resourceData) |> mapToSignal { _, data in + if data.complete { + return moveWallpaperToCache(postbox: postbox, path: data.path, resource: resource, settings: settings) + } else { + return .complete() + } + } +} + +func moveWallpaperToCache(postbox: Postbox, wallpaper: Wallpaper) -> Signal { + switch wallpaper { + case let .image(reps, settings): + return moveWallpaperToCache(postbox: postbox, resource: largestImageRepresentation(reps)!.resource, reference: nil, settings: settings, isPattern: false) |> map { _ in return wallpaper} + case let .custom(representation, blurred): + return moveWallpaperToCache(postbox: postbox, resource: representation.resource, reference: nil, settings: WallpaperSettings(blur: blurred), isPattern: false) |> map { _ in return wallpaper} + case let .file(slug, file, settings, isPattern): + return moveWallpaperToCache(postbox: postbox, resource: file.resource, reference: .slug(slug), settings: settings, isPattern: isPattern) |> map { _ in return wallpaper} + default: + return .single(wallpaper) + } +} + +func moveWallpaperToCache(postbox: Postbox, path: String, resource: TelegramMediaResource, settings: WallpaperSettings) -> Signal { + return Signal { subscriber in + + let wallpapers = ApiEnvironment.containerURL!.appendingPathComponent("Wallpapers").path + try? FileManager.default.createDirectory(at: URL(fileURLWithPath: wallpapers), withIntermediateDirectories: true, attributes: nil) + + let out = wallpapers + "/" + resource.id.uniqueId + "\(settings.stringValue)" + ".png" + + if !FileManager.default.fileExists(atPath: out) { + try? FileManager.default.removeItem(atPath: out) + try? FileManager.default.copyItem(atPath: path, toPath: out) + } + subscriber.putNext(out) + + subscriber.putCompletion() + return EmptyDisposable + + } +} + +extension WallpaperSettings { + var stringValue: String { + var value: String = "" + if let top = self.colors.first { + value += "ctop\(top)" + } + if let top = self.colors.last, self.colors.count == 2 { + value += "cbottom\(top)" + } + if let rotation = self.rotation { + value += "rotation\(rotation)" + } + if self.blur { + value += "blur" + } + return value + } +} + +func wallpaperPath(_ resource: TelegramMediaResource, settings: WallpaperSettings) -> String { + return ApiEnvironment.containerURL!.appendingPathComponent("Wallpapers").path + "/" + resource.id.uniqueId + "\(settings.stringValue)" + ".png" +} + + +func canCollagesFromUrl(_ urls:[URL]) -> Bool { + var canCollage: Bool = urls.count > 1 && urls.count <= 10 + + var musicCount: Int = 0 + var voiceCount: Int = 0 + var gifCount: Int = 0 + if canCollage { + for url in urls { + let mime = MIMEType(url.path) + let attrs = Sender.fileAttributes(for: mime, path: url.path, isMedia: true) + let isGif = attrs.contains(where: { attr -> Bool in + switch attr { + case .Animated: + return true + default: + return false + } + }) + let isMusic = attrs.contains(where: { attr -> Bool in + switch attr { + case let .Audio(isVoice, _, _, _, _): + return !isVoice + default: + return false + } + }) + let isVoice = attrs.contains(where: { attr -> Bool in + switch attr { + case let .Audio(isVoice, _, _, _, _): + return isVoice + default: + return false + } + }) + if mime == "image/webp" { + return false + } + if isMusic { + musicCount += 1 + } + if isVoice { + voiceCount += 1 + } + if isGif { + gifCount += 1 + } + } + } + + if musicCount > 0 { + if musicCount == urls.count { + return true + } else { + return false + } + } + if voiceCount > 0 { + return false + } + if gifCount > 0 { + return false + } + + return canCollage +} + +extension AutomaticMediaDownloadSettings { + + func isDownloable(_ message: Message) -> Bool { + + if !automaticDownload { + return false + } + + + func ability(_ category: AutomaticMediaDownloadCategoryPeers, _ peer: Peer) -> Bool { + if peer.isGroup || peer.isSupergroup { + return category.groupChats + } else if peer.isChannel { + return category.channels + } else { + return category.privateChats + } + } + + func checkFile(_ media: TelegramMediaFile, _ peer: Peer, _ categories: AutomaticMediaDownloadCategories) -> Bool { + let size = Int32(media.size ?? 0) + + let dangerExts = "action app bin command csh osx workflow terminal url caction mpkg pkg xhtm webarchive" + + if let ext = media.fileName?.nsstring.pathExtension.lowercased(), dangerExts.components(separatedBy: " ").contains(ext) { + return false + } + + switch true { + case media.isInstantVideo: + return ability(categories.video, peer) && size <= (categories.video.fileSize ?? INT32_MAX) + case media.isVideo && media.isAnimated: + return ability(categories.video, peer) && size <= (categories.video.fileSize ?? INT32_MAX) + case media.isVideo: + return ability(categories.video, peer) && size <= (categories.video.fileSize ?? INT32_MAX) + case media.isVoice: + return size <= 1 * 1024 * 1024 + default: + return ability(categories.files, peer) && size <= (categories.files.fileSize ?? INT32_MAX) + } + } + + if let peer = messageMainPeer(message) { + if let _ = message.media.first as? TelegramMediaImage { + return ability(categories.photo, peer) + } else if let media = message.media.first as? TelegramMediaFile { + return checkFile(media, peer, categories) + } else if let media = message.media.first as? TelegramMediaWebpage { + switch media.content { + case let .Loaded(content): + if content.type == "telegram_background" { + return ability(categories.photo, peer) + } + if let file = content.file { + return checkFile(file, peer, categories) + } else if let _ = content.image { + return ability(categories.photo, peer) + } + default: + break + } + } else if let media = message.media.first as? TelegramMediaGame { + if let file = media.file { + return checkFile(file, peer, categories) + } else if let _ = media.image { + return ability(categories.photo, peer) + } + } + } + + return false + } +} + + +func fileExtenstion(_ file: TelegramMediaFile) -> String { + return fileExt(file.mimeType) ?? file.fileName?.nsstring.pathExtension ?? "" +} + +func proxySettings(accountManager: AccountManager) -> Signal { + return accountManager.sharedData(keys: [SharedDataKeys.proxySettings]) |> map { view in + return view.entries[SharedDataKeys.proxySettings] as? ProxySettings ?? ProxySettings.defaultSettings + } +} + +extension ProxySettings { + func withUpdatedActiveServer(_ activeServer: ProxyServerSettings?) -> ProxySettings { + return ProxySettings(enabled: self.enabled, servers: servers, activeServer: activeServer, useForCalls: self.useForCalls) + } + + func withUpdatedEnabled(_ enabled: Bool) -> ProxySettings { + return ProxySettings(enabled: enabled, servers: self.servers, activeServer: self.activeServer, useForCalls: self.useForCalls) + } + + func withAddedServer(_ proxy: ProxyServerSettings) -> ProxySettings { + var servers = self.servers + if servers.first(where: {$0 == proxy}) == nil { + servers.append(proxy) + } + return ProxySettings(enabled: self.enabled, servers: servers, activeServer: self.activeServer, useForCalls: self.useForCalls) + } + + func withUpdatedServer(_ current: ProxyServerSettings, with updated: ProxyServerSettings) -> ProxySettings { + var servers = self.servers + if let index = servers.index(where: {$0 == current}) { + servers[index] = updated + } else { + servers.append(updated) + } + var activeServer = self.activeServer + if activeServer == current { + activeServer = updated + } + return ProxySettings(enabled: self.enabled, servers: servers, activeServer: activeServer, useForCalls: self.useForCalls) + } + + func withUpdatedUseForCalls(_ enable: Bool) -> ProxySettings { + return ProxySettings(enabled: self.enabled, servers: servers, activeServer: self.activeServer, useForCalls: enable) + } + + func withRemovedServer(_ proxy: ProxyServerSettings) -> ProxySettings { + var servers = self.servers + var activeServer = self.activeServer + var enabled: Bool = self.enabled + if let index = servers.firstIndex(where: {$0 == proxy}) { + _ = servers.remove(at: index) + } + if proxy == activeServer { + activeServer = nil + enabled = false + } + return ProxySettings(enabled: enabled, servers: servers, activeServer: activeServer, useForCalls: self.useForCalls) + } +} + +extension ProxyServerSettings { + var link: String { + let prefix: String + switch self.connection { + case .mtp: + prefix = "proxy" + case .socks5: + prefix = "socks" + } + var link = "tg://\(prefix)?server=\(self.host)&port=\(self.port)" + switch self.connection { + case let .mtp(secret): + link += "&secret=\((secret as NSData).hexString)" + case let .socks5(username, password): + if let username = username { + link += "&user=\(username)" + } + if let password = password { + link += "&pass=\(password)" + } + } + return link + } + + var isEmpty: Bool { + if host.isEmpty { + return true + } + if port == 0 { + return true + } + switch self.connection { + case let .mtp(secret): + if secret.isEmpty { + return true + } + default: + break + } + return false + } +} + + +struct SecureIdDocumentValue { + let document: SecureIdVerificationDocument + let stableId: AnyHashable + let context: SecureIdAccessContext + init(document: SecureIdVerificationDocument, context: SecureIdAccessContext, stableId: AnyHashable) { + self.document = document + self.stableId = stableId + self.context = context + } + var image: TelegramMediaImage { + return TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [TelegramMediaImageRepresentation(dimensions: PixelDimensions(100, 100), resource: document.resource, progressiveSizes: [], immediateThumbnailData: nil)], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + } +} + +enum FaqDestination { + case telegram + case ton + case walletTOS + var url:String { + switch self { + case .telegram: + return "https://telegram.org/faq/" + case .ton: + return "https://telegram.org/faq/gram_wallet/" + case .walletTOS: + return "https://telegram.org/tos/wallet/" + } + } +} + +func openFaq(context: AccountContext, dest: FaqDestination = .telegram) { + let language = appCurrentLanguage.languageCode[appCurrentLanguage.languageCode.index(appCurrentLanguage.languageCode.endIndex, offsetBy: -2) ..< appCurrentLanguage.languageCode.endIndex] + + _ = showModalProgress(signal: webpagePreview(account: context.account, url: dest.url) |> deliverOnMainQueue, for: context.window).start(next: { webpage in + if let webpage = webpage { + showInstantPage(InstantPageViewController(context, webPage: webpage, message: nil)) + } else { + execute(inapp: .external(link: dest.url + language, true)) + } + }) +} + +func isNotEmptyStrings(_ strings: [String?]) -> String { + for string in strings { + if let string = string, !string.isEmpty { + return string + } + } + return "" +} + + +extension TelegramMediaImage { + var isLocalResource: Bool { + if let resource = representations.last?.resource { + if resource is LocalFileMediaResource { + return true + } + if resource is LocalFileReferenceMediaResource { + return true + } + } + return false + } +} + +extension TelegramMediaFile { + var isLocalResource: Bool { + if resource is LocalFileMediaResource { + return true + } + if resource is LocalFileReferenceMediaResource { + return true + } + return false + } +} + +extension MessageIndex { + func withUpdatedTimestamp(_ timestamp: Int32) -> MessageIndex { + return MessageIndex(id: self.id, timestamp: timestamp) + } + init(_ message: Message) { + self.init(id: message.id, timestamp: message.timestamp) + } + +} + +func requestMicrophonePermission() -> Signal { + return requestMediaPermission(.audio) +} +func requestCameraPermission() -> Signal { + return requestMediaPermission(.video) +} +func requestScreenCapturPermission() -> Signal { + return Signal { subscriber in + subscriber.putNext(requestScreenCaptureAccess()) + subscriber.putCompletion() + return EmptyDisposable + } |> runOn(.mainQueue()) +} + +func screenCaptureAvailable() -> Bool { + let stream = CGDisplayStream(dispatchQueueDisplay: CGMainDisplayID(), outputWidth: 1, outputHeight: 1, pixelFormat: Int32(kCVPixelFormatType_32BGRA), properties: nil, queue: DispatchQueue.main, handler: { _, _, _, _ in + }) + let result = stream != nil + return result +} + +func requestScreenCaptureAccess() -> Bool { + if #available(OSX 11.0, *) { + if !CGPreflightScreenCaptureAccess() { + return CGRequestScreenCaptureAccess() + } else { + return true + } + } else { + return screenCaptureAvailable() + } +} + + +func requestMediaPermission(_ type: AVFoundation.AVMediaType) -> Signal { + if #available(OSX 10.14, *) { + return Signal { subscriber in + let status = AVCaptureDevice.authorizationStatus(for: type) + var cancelled: Bool = false + switch status { + case .notDetermined: + AVCaptureDevice.requestAccess(for: type, completionHandler: { completed in + if !cancelled { + subscriber.putNext(completed) + subscriber.putCompletion() + } + }) + case .authorized: + subscriber.putNext(true) + subscriber.putCompletion() + case .denied: + subscriber.putNext(false) + subscriber.putCompletion() + case .restricted: + subscriber.putNext(false) + subscriber.putCompletion() + @unknown default: + subscriber.putNext(false) + subscriber.putCompletion() + } + return ActionDisposable { + cancelled = true + } + } |> runOn(.concurrentDefaultQueue()) |> deliverOnMainQueue + } else { + return .single(true) + } +} + +enum SystemSettingsCategory : String { + case microphone = "Privacy_Microphone" + case camera = "Privacy_Camera" + case storage = "Storage" + case sharing = "Privacy_ScreenCapture" + case accessibility = "Privacy_Accessibility" + case notifications = "Notifications" + case none = "" +} + +func openSystemSettings(_ category: SystemSettingsCategory) { + switch category { + case .storage: + //if let url = URL(string: "/System/Applications/Utilities/System%20Information.app") { + NSWorkspace.shared.launchApplication("/System/Applications/Utilities/System Information.app") + // [[NSWorkspace sharedWorkspace] launchApplication:@"/Applications/Safari.app"]; + // } + case .microphone, .camera, .sharing: + if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?\(category.rawValue)") { + NSWorkspace.shared.open(url) + } + case .notifications: + if let url = URL(string: "x-apple.systempreferences:com.apple.preference.notifications") { + NSWorkspace.shared.open(url) + } + default: + break + } +} + +extension MessageHistoryAnchorIndex { + func withSubstractedTimestamp(_ timestamp: Int32) -> MessageHistoryAnchorIndex { + switch self { + case let .message(index): + return MessageHistoryAnchorIndex.message(MessageIndex(id: index.id, timestamp: index.timestamp - timestamp)) + default: + return self + } + } +} + + +extension ChatContextResultCollection { + func withAdditionalCollection(_ collection: ChatContextResultCollection) -> ChatContextResultCollection { + return ChatContextResultCollection(botId: collection.botId, peerId: collection.peerId, query: collection.query, geoPoint: collection.geoPoint, queryId: collection.queryId, nextOffset: collection.nextOffset, presentation: collection.presentation, switchPeer: collection.switchPeer, results: self.results + collection.results, cacheTimeout: collection.cacheTimeout) + } +} + +extension LocalFileReferenceMediaResource : Equatable { + public static func ==(lhs: LocalFileReferenceMediaResource, rhs: LocalFileReferenceMediaResource) -> Bool { + return lhs.isEqual(to: rhs) + } +} + + +public func removeFile(at path: String) { + try? FileManager.default.removeItem(atPath: path) +} + + +extension FileManager { + + func modificationDateForFileAtPath(path:String) -> NSDate? { + guard let attributes = try? self.attributesOfItem(atPath: path) else { return nil } + return attributes[.modificationDate] as? NSDate + } + + func creationDateForFileAtPath(path:String) -> NSDate? { + guard let attributes = try? self.attributesOfItem(atPath: path) else { return nil } + return attributes[.creationDate] as? NSDate + } + + +} + + +extension MessageForwardInfo { + var authorTitle: String { + return author?.displayTitle ?? authorSignature ?? "" + } +} + + +func bigEmojiMessage(_ sharedContext: SharedAccountContext, message: Message) -> Bool { + return sharedContext.baseSettings.bigEmoji && message.media.isEmpty && message.replyMarkup == nil && message.text.count <= 3 && message.text.containsOnlyEmoji +} + + + +struct PeerEquatable: Equatable { + let peer: Peer + init(peer: Peer) { + self.peer = peer + } + init(_ peer: Peer) { + self.peer = peer + } + static func ==(lhs: PeerEquatable, rhs: PeerEquatable) -> Bool { + return lhs.peer.isEqual(rhs.peer) + } +} + + +extension CGImage { + var cvPixelBuffer: CVPixelBuffer? { + var pixelBuffer: CVPixelBuffer? = nil + let options: [NSObject: Any] = [ + kCVPixelBufferCGImageCompatibilityKey: false, + kCVPixelBufferCGBitmapContextCompatibilityKey: false, + ] + let status = CVPixelBufferCreate(kCFAllocatorDefault, Int(size.width), Int(size.height), kCVPixelFormatType_32BGRA, options as CFDictionary, &pixelBuffer) + CVPixelBufferLockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0)) + let pixelData = CVPixelBufferGetBaseAddress(pixelBuffer!) + let rgbColorSpace = CGColorSpaceCreateDeviceRGB() + let context = CGContext(data: pixelData, width: Int(size.width), height: Int(size.height), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer!), space: rgbColorSpace, bitmapInfo: CGBitmapInfo.byteOrder32Little.rawValue) + context?.draw(self, in: CGRect(origin: .zero, size: size)) + CVPixelBufferUnlockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0)) + return pixelBuffer + } +} + + +private let emojis: [String: (String, CGFloat)] = [ + "👍": ("thumbs_up_1", 450.0), + "👍🏻": ("thumbs_up_2", 450.0), + "👍🏼": ("thumbs_up_3", 450.0), + "👍🏽": ("thumbs_up_4", 450.0), + "👍🏾": ("thumbs_up_5", 450.0), + "👍🏿": ("thumbs_up_6", 450.0), + "😂": ("lol", 350.0), + "😒": ("meh", 350.0), + "❤️": ("heart", 350.0), + "♥️": ("heart", 350.0), + "🥳": ("celeb", 430.0), + "😳": ("confused", 350.0) +] +func animatedEmojiResource(emoji: String) -> (LocalBundleResource, CGFloat)? { + if let (name, size) = emojis[emoji] { + return (LocalBundleResource(name: name, ext: "tgs"), size) + } else { + return nil + } +} + + +extension TelegramMediaWebpageLoadedContent { + func withUpdatedYoutubeTimecode(_ timecode: Double) -> TelegramMediaWebpageLoadedContent { + var newUrl = self.url + if let range = self.url.range(of: "t=") { + let substr = String(newUrl[range.upperBound...]) + var parsed: String = "" + for char in substr { + if "0987654321".contains(char) { + parsed += String(char) + } else { + break + } + } + newUrl = newUrl.replacingOccurrences(of: parsed, with: "\(Int(timecode))", options: .caseInsensitive, range: range.lowerBound ..< newUrl.endIndex) + } else { + if url.contains("?") { + newUrl = self.url + "&t=\(Int(timecode))" + } else { + newUrl = self.url + "?t=\(Int(timecode))" + } + } + return TelegramMediaWebpageLoadedContent(url: newUrl, displayUrl: self.displayUrl, hash: self.hash, type: self.type, websiteName: self.websiteName, title: self.title, text: self.text, embedUrl: self.embedUrl, embedType: self.embedType, embedSize: self.embedSize, duration: self.duration, author: self.author, image: self.image, file: self.file, attributes: self.attributes, instantPage: self.instantPage) + } + func withUpdatedFile(_ file: TelegramMediaFile) -> TelegramMediaWebpageLoadedContent { + return TelegramMediaWebpageLoadedContent(url: self.url, displayUrl: self.displayUrl, hash: self.hash, type: self.type, websiteName: self.websiteName, title: self.title, text: self.text, embedUrl: self.embedUrl, embedType: self.embedType, embedSize: self.embedSize, duration: self.duration, author: self.author, image: self.image, file: file, attributes: self.attributes, instantPage: self.instantPage) + } + + var isCrossplatformTheme: Bool { + for attr in attributes { + switch attr { + case let .theme(theme): + var hasFile: Bool = false + for file in theme.files { + if file.mimeType == "application/x-tgtheme-macos", !file.previewRepresentations.isEmpty { + hasFile = true + } + } + if let _ = theme.settings, !hasFile { + return true + } + default: + break + } + } + return false + } + + var crossplatformPalette: ColorPalette? { + for attr in attributes { + switch attr { + case let .theme(theme): + return theme.settings?.palette + default: + break + } + } + return nil + } + var crossplatformWallpaper: Wallpaper? { + for attr in attributes { + switch attr { + case let .theme(theme): + return theme.settings?.background?.uiWallpaper + default: + break + } + } + return nil + } + + var themeSettings: TelegramThemeSettings? { + for attr in attributes { + switch attr { + case let .theme(theme): + return theme.settings + default: + break + } + } + return nil + } +} + +extension TelegramBaseTheme { + var palette: ColorPalette { + switch self { + case .classic: + return dayClassicPalette + case .day: + return whitePalette + case .night: + return darkPalette + case .tinted: + return nightAccentPalette + } + } +} +extension TelegramThemeSettings { + var palette: ColorPalette { + return baseTheme.palette.withAccentColor(accent) + } + + var accent: PaletteAccentColor { + let messages = self.messageColors.map { NSColor(rgb: UInt32(bitPattern: $0)) } + return PaletteAccentColor(NSColor(rgb: UInt32(bitPattern: self.accentColor)), messages) + } + + var background: TelegramWallpaper? { + if let wallpaper = self.wallpaper { + return wallpaper + } else { + if self.baseTheme == .classic { + return .builtin(WallpaperSettings()) + } + } + return nil + } + + var desc: String { + let wString: String + if let wallpaper = self.wallpaper { + wString = "\(wallpaper)" + } else { + wString = "" + } + let colors = messageColors.map { "\($0)" }.split(separator: "-").joined() + return "\(self.accentColor)-\(self.baseTheme)-\(colors)-\(wString)" + } +} + +extension TelegramWallpaper { + var uiWallpaper: Wallpaper { + let t: Wallpaper + switch self { + case .builtin: + t = .builtin + case let .color(color): + t = .color(color) + case let .file(values): + t = .file(slug: values.slug, file: values.file, settings: values.settings, isPattern: values.isPattern) + case let .gradient(gradient): + t = .gradient(gradient.id, gradient.colors, gradient.settings.rotation) + case let .image(reps, settings): + t = .image(reps, settings: settings) + } + return t + } +} + +extension Wallpaper { + var cloudWallpaper: TelegramWallpaper? { + switch self { + case .builtin: + return .builtin(WallpaperSettings()) + case let .color(color): + return .color(color) + case let .gradient(id, colors, rotation): + return .gradient(.init(id: id, colors: colors, settings: WallpaperSettings(rotation: rotation))) + default: + break + } + return nil + } +} + +// +extension CachedChannelData.LinkedDiscussionPeerId { + var peerId: PeerId? { + switch self { + case let .known(peerId): + return peerId + case .unknown: + return nil + } + } +} + + +func permanentExportedInvitation(context: AccountContext, peerId: PeerId) -> Signal { + return context.account.postbox.transaction { transaction -> ExportedInvitation? in + let cachedData = transaction.getPeerCachedData(peerId: peerId) + if let cachedData = cachedData as? CachedChannelData { + return cachedData.exportedInvitation + } + if let cachedData = cachedData as? CachedGroupData { + return cachedData.exportedInvitation + } + return nil + } |> mapToSignal { invitation in + if invitation == nil { + return context.engine.peers.revokePersistentPeerExportedInvitation(peerId: peerId) + } else { + return .single(invitation) + } + } +} + + + + +extension CachedPeerAutoremoveTimeout { + var timeout: CachedPeerAutoremoveTimeout.Value? { + switch self { + case let .known(timeout): + return timeout + case .unknown: + return nil + } + } + +} + + + +func clearHistory(context: AccountContext, peer: Peer, mainPeer: Peer) { + if peer.canClearHistory && (context.peerId != peer.id && peer.canManageDestructTimer) && !peer.isSecretChat { + showModal(with: AutoremoveMessagesController(context: context, peer: peer), for: context.window) + } else if peer.canClearHistory { + var thridTitle: String? = nil + var canRemoveGlobally: Bool = false + if peer.id.namespace == Namespaces.Peer.CloudUser && peer.id != context.account.peerId && !peer.isBot { + if context.limitConfiguration.maxMessageRevokeIntervalInPrivateChats == LimitsConfiguration.timeIntervalForever { + canRemoveGlobally = true + } + } + if canRemoveGlobally { + thridTitle = L10n.chatMessageDeleteForMeAndPerson(peer.displayTitle) + } + + + let information = mainPeer is TelegramUser || mainPeer is TelegramSecretChat ? peer.id == context.peerId ? L10n.peerInfoConfirmClearHistorySavedMesssages : canRemoveGlobally || peer.id.namespace == Namespaces.Peer.SecretChat ? L10n.peerInfoConfirmClearHistoryUserBothSides : L10n.peerInfoConfirmClearHistoryUser : L10n.peerInfoConfirmClearHistoryGroup + + modernConfirm(for: context.window, account: context.account, peerId: mainPeer.id, information:information , okTitle: L10n.peerInfoConfirmClear, thridTitle: thridTitle, thridAutoOn: false, successHandler: { result in + context.chatUndoManager.clearHistoryInteractively(engine: context.engine, peerId: peer.id, type: result == .thrid ? .forEveryone : .forLocalPeer) + }) + } else { + showModal(with: AutoremoveMessagesController(context: context, peer: peer), for: context.window) } } diff --git a/Telegram-Mac/CoreMediaVideoTest.swift b/Telegram-Mac/CoreMediaVideoTest.swift new file mode 100644 index 0000000000..47086e7e94 --- /dev/null +++ b/Telegram-Mac/CoreMediaVideoTest.swift @@ -0,0 +1,107 @@ +// +// CoreMediaVideoTest.swift +// Telegram +// +// Created by Mikhail Filimonov on 21.06.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import Cocoa +import TGUIKit +import SwiftSignalKit +import TelegramCore +import Postbox + +private final class Arguments { + let context: AccountContext + init(context: AccountContext) { + self.context = context + } +} + +private struct State : Equatable { +} + + +private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + // entries + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: .init(""), equatable: nil, comparable: nil, item: { initialSize, stableId in + return SoftwareGradientBackgroundItem(initialSize, stableId) + })) + sectionId += 1 + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func CoreMediaVideoIOTest(context: AccountContext) -> InputDataModalController { + + let actionsDisposable = DisposableSet() + + + + let initialState = State() + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + +// capturer.start() + + + let arguments = Arguments(context: context) + + let signal = statePromise.get() |> deliverOnPrepareQueue |> map { state in + return InputDataSignalValue(entries: entries(state, arguments: arguments)) + } + + let controller = InputDataController(dataSignal: signal, title: " ") + + controller.onDeinit = { + actionsDisposable.dispose() + } + + + let modalController = InputDataModalController(controller, modalInteractions: nil) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) + + + let stickers:Signal<[TelegramMediaFile], NoError> = context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: nil, count: 2000) + |> take(1) + |> map { view in + return view.entries.compactMap { + $0.item as? StickerPackItem + }.filter { + $0.file.isAnimatedSticker + }.map { + $0.file + } + } + + return modalController +} + + +/* + + */ + + + diff --git a/Telegram-Mac/CrashHandler.swift b/Telegram-Mac/CrashHandler.swift new file mode 100644 index 0000000000..74ca425ed8 --- /dev/null +++ b/Telegram-Mac/CrashHandler.swift @@ -0,0 +1,51 @@ +// +// CrashHandler.swift +// Telegram +// +// Created by Mikhail Filimonov on 07/02/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa + +func isCrashedLastTime(_ folder: String) -> Bool { + let url = folder + "/" + "crashhandler" + + if let dateString = try? String(contentsOf: URL(fileURLWithPath: url)) { + let components = dateString.components(separatedBy: " ") + if components.count == 2 { + let initedDate = Int32(components[0]) ?? 0 + let lastSavedDate = Int32(components[1]) ?? 0 + return lastSavedDate - initedDate < 10 + } else { + return true + } + } + + return FileManager.default.fileExists(atPath: url) +} + +func crashIntermediateDate(_ folder: String) { + let url = folder + "/" + "crashhandler" + if let dateString = try? String(contentsOf: URL(fileURLWithPath: url)) { + let time = "\(Int32(Date().timeIntervalSince1970))" + var components = dateString.components(separatedBy: " ") + if components.count == 2 { + components[1] = time + } else if components.count == 1 { + components.append(time) + } + try? FileManager.default.removeItem(atPath: url) + FileManager.default.createFile(atPath: url, contents: components.joined(separator: " ").data(using: .utf8), attributes: nil) + + } else { + let time = "\(Int32(Date().timeIntervalSince1970))".data(using: .utf8) + try? FileManager.default.removeItem(atPath: url) + FileManager.default.createFile(atPath: url, contents: time, attributes: nil) + } +} + +func deinitCrashHandler(_ folder: String) { + let url = folder + "/" + "crashhandler" + try? FileManager.default.removeItem(atPath: url) +} diff --git a/Telegram-Mac/CreateChannelViewController.swift b/Telegram-Mac/CreateChannelViewController.swift index f504171d02..8803b2f63b 100644 --- a/Telegram-Mac/CreateChannelViewController.swift +++ b/Telegram-Mac/CreateChannelViewController.swift @@ -7,39 +7,137 @@ // import Cocoa -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit import TGUIKit -class CreateChannelViewController: ComposeViewController { +class CreateChannelViewController: ComposeViewController<(PeerId?, Bool), Void, TableView> { private var nameItem:GroupNameRowItem! - private var descItem:GeneralInputRowItem! - + private var descItem:InputDataRowItem! + private let disposable = MetaDisposable() + private var picture: String? { + didSet { + nameItem.photo = picture + genericView.reloadData() + } + } override func viewDidLoad() { super.viewDidLoad() self.nextEnabled(false) - nameItem = GroupNameRowItem(atomicSize.modify({$0}), stableId: 0, placeholder: tr(.channelChannelNameHolder), limit: 140, textChangeHandler:{ [weak self] text in + + genericView.getBackgroundColor = { + theme.colors.listBackground + } + + let initialSize = atomicSize.with { $0 } + + nameItem = GroupNameRowItem(initialSize, stableId: 0, account: context.account, placeholder: L10n.channelChannelNameHolder, viewType: .singleItem, limit: 140, textChangeHandler:{ [weak self] text in self?.nextEnabled(!text.isEmpty) + }, pickPicture: { [weak self] select in + if select { + filePanel(with: photoExts, allowMultiple: false, canChooseDirectories: false, for: mainWindow, completion: { paths in + if let path = paths?.first, let image = NSImage(contentsOfFile: path) { + _ = (putToTemp(image: image, compress: true) |> deliverOnMainQueue).start(next: { path in + let controller = EditImageModalController(URL(fileURLWithPath: path), settings: .disableSizes(dimensions: .square)) + showModal(with: controller, for: mainWindow, animationType: .scaleCenter) + _ = (controller.result |> deliverOnMainQueue).start(next: { url, _ in + self?.picture = url.path + }) + + controller.onClose = { + removeFile(at: path) + } + }) + } + }) + } else { + self?.picture = nil + } }) - descItem = GeneralInputRowItem(atomicSize.modify({$0}), stableId: 2, placeholder: tr(.channelDescriptionHolder), limit: 300) + descItem = InputDataRowItem(initialSize, stableId: arc4random(), mode: .plain, error: nil, viewType: .singleItem, currentText: "", placeholder: nil, inputPlaceholder: L10n.channelDescriptionHolder, filter: { $0 }, updated: { _ in }, limit: 255) + + _ = genericView.addItem(item: GeneralRowItem(initialSize, height: 30, stableId: arc4random(), viewType: .separator)) + _ = genericView.addItem(item: GeneralTextRowItem(initialSize, stableId: arc4random(), text: L10n.channelNameHeader, viewType: .textTopItem)) _ = genericView.addItem(item: nameItem) - _ = genericView.addItem(item: GeneralRowItem(atomicSize.modify({$0}), height: 30, stableId: 1)) + _ = genericView.addItem(item: GeneralRowItem(initialSize, height: 30, stableId: arc4random(), viewType: .separator)) + _ = genericView.addItem(item: GeneralTextRowItem(initialSize, stableId: arc4random(), text: L10n.channelDescHeader, viewType: .textTopItem)) _ = genericView.addItem(item: descItem) - _ = genericView.addItem(item: GeneralTextRowItem(atomicSize.modify({$0}), stableId: 3, text: tr(.channelDescriptionHolderDescrpiton))) + _ = genericView.addItem(item: GeneralTextRowItem(initialSize, stableId: arc4random(), text: L10n.channelDescriptionHolderDescrpiton, viewType: .textBottomItem)) + _ = genericView.addItem(item: GeneralRowItem(initialSize, height: 30, stableId: arc4random(), viewType: .separator)) readyOnce() } + override func backKeyAction() -> KeyHandlerResult { + return .invokeNext + } + override var removeAfterDisapper: Bool { return true } + override func returnKeyAction() -> KeyHandlerResult { + if let event = NSApp.currentEvent, let descView = genericView.viewNecessary(at: descItem.index) as? GeneralInputRowView { + if !descView.textViewEnterPressed(event), window?.firstResponder == descView.textView.inputView { + return .invokeNext + } + } + + return super.returnKeyAction() + } + override func executeNext() { - onComplete.set(showModalProgress(signal: createChannel(account: account, title: nameItem.text, description: descItem.text), for: window!)) + let picture = self.picture + let context = self.context + + if nameItem.currentText.string.isEmpty { + nameItem.view?.shakeView() + return + } + let signal: Signal<(PeerId, Bool)?, CreateChannelError> = showModalProgress(signal: context.engine.peers.createChannel(title: nameItem.currentText.string, description: descItem.currentText.string), for: window!, disposeAfterComplete: false) |> mapToSignal { peerId in + if let picture = picture { + let resource = LocalFileReferenceMediaResource(localFilePath: picture, randomId: arc4random64()) + let signal:Signal<(PeerId, Bool)?, CreateChannelError> = context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: resource), mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: context.account.postbox, resource: resource, representations: representations) + }) |> mapError { _ in CreateChannelError.generic } |> map { value in + switch value { + case .complete: + return (peerId, false) + default: + return nil + } + } + + return .single((peerId, true)) |> then(signal) + } + return .single((peerId, true)) + } |> deliverOnMainQueue + + disposable.set(signal.start(next: { [weak self] value in + if let value = value { + self?.onComplete.set(.single((value.0, value.1))) + } + }, error: { error in + let text: String + switch error { + case .generic: + text = L10n.unknownError + case .tooMuchJoined: + showInactiveChannels(context: context, source: .create) + return + case let .serverProvided(t): + text = t + default: + text = L10n.unknownError + } + alert(for: context.window, info: text) + })) + } override var canBecomeResponder: Bool { @@ -47,12 +145,12 @@ class CreateChannelViewController: ComposeViewController Bool? { - return false + return true } override func firstResponder() -> NSResponder? { if let window = window { - if let nameView = genericView.viewNecessary(at: nameItem.index) as? GroupNameRowView, let descView = genericView.viewNecessary(at: descItem.index) as? GeneralInputRowView { + if let nameView = genericView.viewNecessary(at: nameItem.index) as? GroupNameRowView, let descView = genericView.viewNecessary(at: descItem.index) as? InputDataRowView { nameView.textView.inputView.nextKeyView = descView.textView.inputView nameView.textView.inputView.nextResponder = descView.textView.inputView if window.firstResponder != nameView.textView.inputView && window.firstResponder != descView.textView.inputView { @@ -64,5 +162,9 @@ class CreateChannelViewController: ComposeViewController Signal { - - switch reference { - case let .remoteImage(imageId, accesshash): - let api = Api.functions.photos.deletePhotos(id: [Api.InputPhoto.inputPhoto(id: imageId, accessHash: accesshash)]) - return account.network.request(api) |> map {_ in} |> retryRequest - case .none: - let api = Api.functions.photos.updateProfilePhoto(id: Api.InputPhoto.inputPhotoEmpty) - return account.network.request(api) |> map { _ in } |> retryRequest - } - -} - -func channelAdminIds(postbox: Postbox, network: Network, peerId: PeerId, hash: Int32) -> Signal<[PeerId], Void> { - return postbox.modify { modifier in - if let peer = modifier.getPeer(peerId) as? TelegramChannel, case .group = peer.info, let apiChannel = apiInputChannel(peer) { - let api = Api.functions.channels.getParticipants(channel: apiChannel, filter: .channelParticipantsAdmins, offset: 0, limit: 100, hash: hash) - return network.request(api) |> retryRequest |> mapToSignal { result in - switch result { - case let .channelParticipants(_, _, users): - return .single(users.map({TelegramUser(user: $0).id})) - default: - return .complete() - } - } - } - return .complete() - } |> switchToLatest -} diff --git a/Telegram-Mac/CreateGroupViewController.swift b/Telegram-Mac/CreateGroupViewController.swift index 71d466261c..1bcddc4918 100644 --- a/Telegram-Mac/CreateGroupViewController.swift +++ b/Telegram-Mac/CreateGroupViewController.swift @@ -7,52 +7,75 @@ // import Cocoa -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit import TGUIKit + +fileprivate final class CreateGroupArguments { + let context: AccountContext + let choicePicture:(Bool)->Void + let updatedText:(String)->Void + init(context: AccountContext, choicePicture:@escaping(Bool)->Void, updatedText:@escaping(String)->Void) { + self.context = context + self.updatedText = updatedText + self.choicePicture = choicePicture + } +} + fileprivate enum CreateGroupEntry : Comparable, Identifiable { - case info - case peer(Peer, Int, PeerPresence?) - + case info(Int32, String?, String, GeneralViewType) + case peer(Int32, Peer, Int32, PeerPresence?, GeneralViewType) + case section(Int32) fileprivate var stableId:AnyHashable { switch self { case .info: - return Int32(0) - case let .peer(peer, _, _): + return -1 + case let .peer(_, peer, _, _, _): return peer.id + case let .section(sectionId): + return sectionId } } - var index:Int { + var index:Int32 { switch self { - case .info: - return 0 - case let .peer(_, index, _): - return index + 1 + case let .info(sectionId, _, _, _): + return (sectionId * 1000) + 0 + case let .peer(sectionId, _, index, _, _): + return (sectionId * 1000) + index + case let .section(sectionId): + return (sectionId + 1) * 1000 - sectionId } } } fileprivate func ==(lhs:CreateGroupEntry, rhs:CreateGroupEntry) -> Bool { switch lhs { - case .info: - if case .info = rhs { + case let .info(section, photo, text, viewType): + if case .info(section, photo, text, viewType) = rhs { + return true + } else { + return false + } + case let .section(sectionId): + if case .section(sectionId) = rhs { return true } else { return false } - case let .peer(lhsPeer,lhsIndex, lhsPresence): - if case let .peer(rhsPeer,rhsIndex, rhsPresence) = rhs { + case let .peer(sectionId, lhsPeer, index, lhsPresence, viewType): + if case .peer(sectionId, let rhsPeer, index, let rhsPresence, viewType) = rhs { if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { if !lhsPresence.isEqual(to: rhsPresence) { return false } - } else if (lhsPresence != nil) != (rhsPresence != nil) { + } else if (lhsPresence != nil) != (rhsPresence != nil) { return false } - return lhsPeer.isEqual(rhsPeer) && lhsIndex == rhsIndex + return lhsPeer.isEqual(rhsPeer) } else { return false } @@ -65,26 +88,29 @@ fileprivate func <(lhs:CreateGroupEntry, rhs:CreateGroupEntry) -> Bool { struct CreateGroupResult { let title:String + let picture: String? let peerIds:[PeerId] } -fileprivate func prepareEntries(from:[AppearanceWrapperEntry], to:[AppearanceWrapperEntry], account:Account, initialSize:NSSize, animated:Bool) -> Signal { +fileprivate func prepareEntries(from:[AppearanceWrapperEntry], to:[AppearanceWrapperEntry], arguments: CreateGroupArguments, initialSize:NSSize, animated:Bool) -> Signal { return Signal { subscriber in let (deleted,inserted,updated) = proccessEntriesWithoutReverse(from, right: to, { entry -> TableRowItem in switch entry.entry { - case .info: - return GroupNameRowItem(initialSize, stableId:entry.stableId, placeholder:tr(.createGroupNameHolder), limit:140) - case let .peer(peer, _, presence): + case let .info(_, photo, currentText, viewType): + return GroupNameRowItem(initialSize, stableId:entry.stableId, account: arguments.context.account, placeholder: L10n.createGroupNameHolder, photo: photo, viewType: viewType, text: currentText, limit:140, textChangeHandler: arguments.updatedText, pickPicture: arguments.choicePicture) + case let .peer(_, peer, _, presence, viewType): var color:NSColor = theme.colors.grayText - var string:String = tr(.peerStatusRecently) + var string:String = L10n.peerStatusRecently if let presence = presence as? TelegramUserPresence { let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - (string, _, color) = stringAndActivityForUserPresence(presence, relativeTo: Int32(timestamp)) + (string, _, color) = stringAndActivityForUserPresence(presence, timeDifference: arguments.context.timeDifference, relativeTo: Int32(timestamp)) } - return ShortPeerRowItem(initialSize, peer: peer, account:account, height:50, photoSize:NSMakeSize(36, 36), statusStyle: ControlStyle(foregroundColor: color), status: string, inset:NSEdgeInsets(left: 30, right:30)) + return ShortPeerRowItem(initialSize, peer: peer, account: arguments.context.account, height:50, photoSize:NSMakeSize(36, 36), statusStyle: ControlStyle(foregroundColor: color), status: string, inset:NSEdgeInsets(left: 30, right:30), viewType: viewType) + case .section: + return GeneralRowItem(initialSize, height: 30, stableId: entry.stableId, viewType: .separator) } }) @@ -98,14 +124,31 @@ fileprivate func prepareEntries(from:[AppearanceWrapperEntry], } -private func createGroupEntries(_ view: MultiplePeersView, appearance: Appearance) -> [AppearanceWrapperEntry] { +private func createGroupEntries(_ view: MultiplePeersView, picture: String?, text: String, appearance: Appearance) -> [AppearanceWrapperEntry] { + + + + var entries:[CreateGroupEntry] = [] + var sectionId:Int32 = 0 + + entries.append(.section(sectionId)) + sectionId += 1 - var entries:[CreateGroupEntry] = [.info] - var index:Int = 0 - for peer in view.peers.map({$1}) { - entries.append(.peer(peer, index, view.presences[peer.id])) + entries.append(.info(sectionId, picture, text, .singleItem)) + + entries.append(.section(sectionId)) + sectionId += 1 + + var index:Int32 = 0 + let peers = view.peers.map({$1}) + for (i, peer) in peers.enumerated() { + entries.append(.peer(sectionId, peer, index, view.presences[peer.id], bestGeneralViewType(peers, for: i))) index += 1 } + + entries.append(.section(sectionId)) + sectionId += 1 + return entries.map{AppearanceWrapperEntry(entry: $0, appearance: appearance)} } @@ -113,28 +156,100 @@ private func createGroupEntries(_ view: MultiplePeersView, appearance: Appearanc class CreateGroupViewController: ComposeViewController { // Title, photo path private let entries:Atomic<[AppearanceWrapperEntry]> = Atomic(value:[]) private let disposable:MetaDisposable = MetaDisposable() + private let pictureValue = Promise(nil) + private let textValue = ValuePromise("", ignoreRepeated: true) + + private let defaultText: String + init(titles: ComposeTitles, context: AccountContext, defaultText: String = "") { + self.defaultText = defaultText + super.init(titles: titles, context: context) + self.textValue.set(self.defaultText) + } override func restart(with result: ComposeState<[PeerId]>) { super.restart(with: result) assert(isLoaded()) let initialSize = self.atomicSize let table = self.genericView + let pictureValue = self.pictureValue + let textValue = self.textValue + let context = self.context - let account: Account = self.account + if self.defaultText == "" && result.result.count < 5 { + let peers: Signal = context.account.postbox.transaction { transaction in + let main = transaction.getPeer(context.peerId) + + let rest = result.result + .map { + transaction.getPeer($0) + } + .compactMap { $0 } + .map { $0.compactDisplayTitle } + .joined(separator: ", ") + + if let main = main, !rest.isEmpty { + return main.compactDisplayTitle + " & " + rest + } else { + return "" + } + + } |> deliverOnMainQueue + + _ = peers.start(next: { [weak self] title in + self?.textValue.set(title) + delay(0.2, closure: { [weak self] in + self?.genericView.enumerateItems(with: { item in + if let item = item as? GroupNameRowItem { + let textView = item.view?.firstResponder as? NSTextView + textView?.selectAll(nil) + return false + } + return true + }) + }) + + }) + } + let entries = self.entries + let arguments = CreateGroupArguments(context: context, choicePicture: { select in + if select { + + filePanel(with: photoExts, allowMultiple: false, canChooseDirectories: false, for: mainWindow, completion: { paths in + if let path = paths?.first, let image = NSImage(contentsOfFile: path) { + _ = (putToTemp(image: image, compress: true) |> deliverOnMainQueue).start(next: { path in + let controller = EditImageModalController(URL(fileURLWithPath: path), settings: .disableSizes(dimensions: .square)) + showModal(with: controller, for: mainWindow, animationType: .scaleCenter) + pictureValue.set(controller.result |> map {Optional($0.0.path)}) + + + controller.onClose = { + removeFile(at: path) + } + }) + } + }) + + } else { + pictureValue.set(.single(nil)) + } + + }, updatedText: { text in + textValue.set(text) + }) - let signal:Signal = combineLatest(account.postbox.multiplePeersView(result.result) |> deliverOnPrepareQueue, appearanceSignal |> deliverOnPrepareQueue) |> mapToSignal { view, appearance in - let list = createGroupEntries(view, appearance: appearance) + let signal:Signal = combineLatest(context.account.postbox.multiplePeersView(result.result) |> deliverOnPrepareQueue, appearanceSignal |> deliverOnPrepareQueue, pictureValue.get() |> deliverOnPrepareQueue, textValue.get() |> deliverOnPrepareQueue) |> mapToSignal { view, appearance, picture, text in + let list = createGroupEntries(view, picture: picture, text: text, appearance: appearance) - return prepareEntries(from: entries.swap(list), to: list, account: account, initialSize: initialSize.modify({$0}), animated: true) + return prepareEntries(from: entries.swap(list), to: list, arguments: arguments, initialSize: initialSize.modify({$0}), animated: true) } |> deliverOnMainQueue disposable.set(signal.start(next: { (transition) in table.merge(with: transition) - table.reloadData() + //table.reloadData() })) } @@ -147,7 +262,7 @@ class CreateGroupViewController: ComposeViewController NSResponder? { - if let view = genericView.viewNecessary(at: 0) as? GroupNameRowView { + if let view = genericView.viewNecessary(at: 1) as? GroupNameRowView { return view.textView } return nil @@ -164,15 +279,27 @@ class CreateGroupViewController: ComposeViewController Void { - if let previousResult = previousResult, let item = self.genericView.item(at: 0) as? GroupNameRowItem { - onComplete.set(.single(CreateGroupResult(title: item.text, peerIds: previousResult.result))) + if let previousResult = previousResult { + let result = combineLatest(pictureValue.get() |> take(1), textValue.get() |> take(1)) |> map { value, text in + return CreateGroupResult(title: text, picture: value, peerIds: previousResult.result) + } + onComplete.set(result |> filter { + !$0.title.isEmpty + }) } } + override func backKeyAction() -> KeyHandlerResult { + return .invokeNext + } + } diff --git a/Telegram-Mac/Currency.swift b/Telegram-Mac/Currency.swift new file mode 100644 index 0000000000..de6e7c03d2 --- /dev/null +++ b/Telegram-Mac/Currency.swift @@ -0,0 +1,178 @@ +// +// CurrencyCode.swift +// CurrencyText +// +// Created by Felipe Lefèvre Marino on 1/26/19. +// + +import Foundation + +/// Currency wraps all availabe currencies that can represented as formatted monetary values +/// A currency code is a three-letter code that is, in most cases, +/// composed of a country’s two-character Internet country code plus an extra character +/// to denote the currency unit. For example, the currency code for the Australian +/// dollar is “AUD”. Currency codes are based on the ISO 4217 standard +public enum Currency: String { + case afghani = "AFN", + algerianDinar = "DZD", + argentinePeso = "ARS", + armenianDram = "AMD", + arubanFlorin = "AWG", + australianDollar = "AUD", + azerbaijanManat = "AZN", + bahamianDollar = "BSD", + bahrainiDinar = "BHD", + baht = "THB", + balboa = "PAB", + barbadosDollar = "BBD", + belarusianRuble = "BYN", + belizeDollar = "BZD", + bermudianDollar = "BMD", + boliviano = "BOB", + bolívar = "VEF", + brazilianReal = "BRL", + bruneiDollar = "BND", + bulgarianLev = "BGN", + burundiFranc = "BIF", + caboVerdeEscudo = "CVE", + canadianDollar = "CAD", + caymanIslandsDollar = "KYD", + chileanPeso = "CLP", + colombianPeso = "COP", + comorianFranc = "KMF", + congoleseFranc = "CDF", + convertibleMark = "BAM", + cordobaOro = "NIO", + costaRicanColon = "CRC", + cubanPeso = "CUP", + czechKoruna = "CZK", + dalasi = "GMD", + danishKrone = "DKK", + denar = "MKD", + djiboutiFranc = "DJF", + dobra = "STN", + dollar = "USD", + dominicanPeso = "DOP", + dong = "VND", + eastCaribbeanDollar = "XCD", + egyptianPound = "EGP", + elSalvadorColon = "SVC", + ethiopianBirr = "ETB", + euro = "EUR", + falklandIslandsPound = "FKP", + fijiDollar = "FJD", + forint = "HUF", + ghanaCedi = "GHS", + gibraltarPound = "GIP", + gourde = "HTG", + guarani = "PYG", + guineanFranc = "GNF", + guyanaDollar = "GYD", + hongKongDollar = "HKD", + hryvnia = "UAH", + icelandKrona = "ISK", + indianRupee = "INR", + iranianRial = "IRR", + iraqiDinar = "IQD", + jamaicanDollar = "JMD", + jordanianDinar = "JOD", + kenyanShilling = "KES", + kina = "PGK", + kuna = "HRK", + kuwaitiDinar = "KWD", + kwanza = "AOA", + kyat = "MMK", + laoKip = "LAK", + lari = "GEL", + lebanesePound = "LBP", + lek = "ALL", + lempira = "HNL", + leone = "SLL", + liberianDollar = "LRD", + libyanDinar = "LYD", + lilangeni = "SZL", + loti = "LSL", + malagasyAriary = "MGA", + malawiKwacha = "MWK", + malaysianRinggit = "MYR", + mauritiusRupee = "MUR", + mexicanPeso = "MXN", + mexicanUnidadDeInversion = "MXV", + moldovanLeu = "MDL", + moroccanDirham = "MAD", + mozambiqueMetical = "MZN", + mvdol = "BOV", + naira = "NGN", + nakfa = "ERN", + namibiaDollar = "NAD", + nepaleseRupee = "NPR", + netherlandsAntilleanGuilder = "ANG", + newIsraeliSheqel = "ILS", + newTaiwanDollar = "TWD", + newZealandDollar = "NZD", + ngultrum = "BTN", + northKoreanWon = "KPW", + norwegianKrone = "NOK", + ouguiya = "MRU", + paanga = "TOP", + pakistanRupee = "PKR", + pataca = "MOP", + pesoConvertible = "CUC", + pesoUruguayo = "UYU", + philippinePiso = "PHP", + poundSterling = "GBP", + pula = "BWP", + qatariRial = "QAR", + quetzal = "GTQ", + rand = "ZAR", + rialOmani = "OMR", + riel = "KHR", + romanianLeu = "RON", + rufiyaa = "MVR", + rupiah = "IDR", + russianRuble = "RUB", + rwandaFranc = "RWF", + saintHelenaPound = "SHP", + saudiRiyal = "SAR", + serbianDinar = "RSD", + seychellesRupee = "SCR", + singaporeDollar = "SGD", + sol = "PEN", + solomonIslandsDollar = "SBD", + som = "KGS", + somaliShilling = "SOS", + somoni = "TJS", + southSudanesePound = "SSP", + sriLankaRupee = "LKR", + sudanesePound = "SDG", + surinamDollar = "SRD", + swedishKrona = "SEK", + swissFranc = "CHF", + syrianPound = "SYP", + taka = "BDT", + tala = "WST", + tanzanianShilling = "TZS", + tenge = "KZT", + trinidadAndTobagoDollar = "TTD", + tugrik = "MNT", + tunisianDinar = "TND", + turkishLira = "TRY", + turkmenistanNewManat = "TMT", + uaeDirham = "AED", + ugandaShilling = "UGX", + unidadDeFomento = "CLF", + unidadDeValorReal = "COU", + uruguayPesoEnUnidadesIndexadas = "UYI", + uzbekistanSum = "UZS", + vatu = "VUV", + wirEuro = "CHE", + wirFranc = "CHW", + won = "KRW", + yemeniRial = "YER", + yen = "JPY", + yuanRenminbi = "CNY", + zambianKwacha = "ZMW", + zimbabweDollar = "ZWL", + zloty = "PLN", + none +} diff --git a/Telegram-Mac/CurrencyFormat.swift b/Telegram-Mac/CurrencyFormat.swift new file mode 100644 index 0000000000..a8081c26ad --- /dev/null +++ b/Telegram-Mac/CurrencyFormat.swift @@ -0,0 +1,178 @@ +// +// CurrencyFormat.swift +// Telegram +// +// Created by Mikhail Filimonov on 25.02.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation + +private final class CurrencyFormatterEntry { + let symbol: String + let thousandsSeparator: String + let decimalSeparator: String + let symbolOnLeft: Bool + let spaceBetweenAmountAndSymbol: Bool + let decimalDigits: Int + + init(symbol: String, thousandsSeparator: String, decimalSeparator: String, symbolOnLeft: Bool, spaceBetweenAmountAndSymbol: Bool, decimalDigits: Int) { + self.symbol = symbol + self.thousandsSeparator = thousandsSeparator + self.decimalSeparator = decimalSeparator + self.symbolOnLeft = symbolOnLeft + self.spaceBetweenAmountAndSymbol = spaceBetweenAmountAndSymbol + self.decimalDigits = decimalDigits + } +} + +private func loadCurrencyFormatterEntries() -> [String: CurrencyFormatterEntry] { + guard let filePath = Bundle.main.path(forResource: "currencies", ofType: "json") else { + return [:] + } + guard let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) else { + return [:] + } + + guard let object = try? JSONSerialization.jsonObject(with: data, options: []), let dict = object as? [String: AnyObject] else { + return [:] + } + + var result: [String: CurrencyFormatterEntry] = [:] + + for (code, contents) in dict { + if let contentsDict = contents as? [String: AnyObject] { + let entry = CurrencyFormatterEntry(symbol: contentsDict["symbol"] as! String, thousandsSeparator: contentsDict["thousandsSeparator"] as! String, decimalSeparator: contentsDict["decimalSeparator"] as! String, symbolOnLeft: (contentsDict["symbolOnLeft"] as! NSNumber).boolValue, spaceBetweenAmountAndSymbol: (contentsDict["spaceBetweenAmountAndSymbol"] as! NSNumber).boolValue, decimalDigits: (contentsDict["decimalDigits"] as! NSNumber).intValue) + result[code] = entry + result[code.lowercased()] = entry + } + } + + return result +} + +private let currencyFormatterEntries = loadCurrencyFormatterEntries() + +public func setupCurrencyNumberFormatter(currency: String) -> NumberFormatter { + guard let entry = currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] else { + preconditionFailure() + } + + var result = "" + if entry.symbolOnLeft { + result.append("¤") + if entry.spaceBetweenAmountAndSymbol { + result.append(" ") + } + } + + result.append("#") + + if entry.decimalDigits > 0 { + result.append(entry.decimalSeparator) + } + + for _ in 0 ..< entry.decimalDigits { + result.append("#") + } + if entry.decimalDigits != 0 { + result.append("0") + } + + if !entry.symbolOnLeft { + if entry.spaceBetweenAmountAndSymbol { + result.append(" ") + } + if entry.decimalDigits > 0 { + result.append("¤") + } + } + + let numberFormatter = NumberFormatter() + + numberFormatter.numberStyle = .currency + + numberFormatter.positiveFormat = result + numberFormatter.negativeFormat = "-\(result)" + + numberFormatter.currencySymbol = entry.symbol + numberFormatter.currencyDecimalSeparator = entry.decimalSeparator + numberFormatter.currencyGroupingSeparator = entry.thousandsSeparator + + numberFormatter.locale = Locale.current + + numberFormatter.minimumFractionDigits = entry.decimalDigits + numberFormatter.maximumFractionDigits = entry.decimalDigits + numberFormatter.minimumIntegerDigits = 1 + + return numberFormatter +} + +public func fractionalToCurrencyAmount(value: Double, currency: String) -> Int64? { + guard let entry = currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] else { + return nil + } + var factor: Double = 1.0 + for _ in 0 ..< entry.decimalDigits { + factor *= 10.0 + } + if value > Double(Int64.max) / factor { + return nil + } else { + return Int64(value * factor) + } +} + +public func currencyToFractionalAmount(value: Int64, currency: String) -> Double? { + guard let entry = currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] else { + return nil + } + var factor: Double = 1.0 + for _ in 0 ..< entry.decimalDigits { + factor *= 10.0 + } + return Double(value) / factor +} + +public func formatCurrencyAmount(_ amount: Int64, currency: String) -> String { + if let entry = currencyFormatterEntries[currency] ?? currencyFormatterEntries["USD"] { + var result = "" + if amount < 0 { + result.append("-") + } + if entry.symbolOnLeft { + result.append(entry.symbol) + if entry.spaceBetweenAmountAndSymbol { + result.append(" ") + } + } + var integerPart = abs(amount) + var fractional: [Character] = [] + for _ in 0 ..< entry.decimalDigits { + let part = integerPart % 10 + integerPart /= 10 + if let scalar = UnicodeScalar(UInt32(part + 48)) { + fractional.append(Character(scalar)) + } + } + result.append("\(integerPart)") + result.append(entry.decimalSeparator) + for i in 0 ..< fractional.count { + result.append(fractional[fractional.count - i - 1]) + } + if !entry.symbolOnLeft { + if entry.spaceBetweenAmountAndSymbol { + result.append(" ") + } + result.append(entry.symbol) + } + return CurrencyFormatter(currency: currency).formattedStringWithAdjustedDecimalSeparator(from: result) ?? result + } else { + assertionFailure() + let formatter = NumberFormatter() + formatter.numberStyle = .currency + formatter.currencyCode = currency + formatter.negativeFormat = "-¤#,##0.00" + return formatter.string(from: (Float(amount) * 0.01) as NSNumber) ?? "" + } +} diff --git a/Telegram-Mac/CurrencyFormatter.swift b/Telegram-Mac/CurrencyFormatter.swift new file mode 100644 index 0000000000..67dee45f37 --- /dev/null +++ b/Telegram-Mac/CurrencyFormatter.swift @@ -0,0 +1,344 @@ +// +// CurrencyFormatter.swift +// CurrencyText +// +// Created by Felipe Lefèvre Marino on 1/27/19. +// + +import Foundation + + +// MARK: - Currency protocols + +public protocol CurrencyFormatting { + var maxDigitsCount: Int { get } + var decimalDigits: Int { get set } + var maxValue: Double? { get set } + var minValue: Double? { get set } + var initialText: String { get } + var currencySymbol: String { get set } + + func string(from double: Double) -> String? + func unformatted(string: String) -> String? + func double(from string: String) -> Double? +} + +public protocol CurrencyAdjusting { + func formattedStringWithAdjustedDecimalSeparator(from string: String) -> String? + func formattedStringAdjustedToFitAllowedValues(from string: String) -> String? +} + +// MARK: - Currency formatter + +public class CurrencyFormatter: CurrencyFormatting { + + /// Set the locale to retrieve the currency from + /// You can pass a Swift type Locale or one of the + /// Locales enum options - that encapsulates all available locales. + public var locale: LocaleConvertible { + set { self.numberFormatter.locale = newValue.locale } + get { self.numberFormatter.locale } + } + + /// Set the desired currency type + /// * Note: The currency take effetcs above the displayed currency symbol, + /// however details such as decimal separators, grouping separators and others + /// will be set based on the defined locale. So for a precise experience, please + /// preferarbly setup both, when you are setting a currency that does not match the + /// default/current user locale. + public var currency: Currency { + set { numberFormatter.currencyCode = newValue.rawValue } + get { Currency(rawValue: numberFormatter.currencyCode) ?? .dollar } + } + + /// Define if currency symbol should be presented or not. + /// Note: when set to false the current currency symbol is removed + public var showCurrencySymbol: Bool = true { + didSet { + numberFormatter.currencySymbol = showCurrencySymbol ? numberFormatter.currencySymbol : "" + } + } + + /// The currency's symbol. + /// Can be used to read or set a custom symbol. + /// Note: showCurrencySymbol must be set to true for + /// the currencySymbol to be correctly changed. + public var currencySymbol: String { + set { + guard showCurrencySymbol else { return } + numberFormatter.currencySymbol = newValue + } + get { numberFormatter.currencySymbol } + } + + /// The lowest number allowed as input. + /// This value is initially set to the text field text + /// when defined. + public var minValue: Double? { + set { + guard let newValue = newValue else { return } + numberFormatter.minimum = NSNumber(value: newValue) + } + get { + if let minValue = numberFormatter.minimum { + return Double(truncating: minValue) + } + return nil + } + } + + /// The highest number allowed as input. + /// The text field will not allow the user to increase the input + /// value beyond it, when defined. + public var maxValue: Double? { + set { + guard let newValue = newValue else { return } + numberFormatter.maximum = NSNumber(value: newValue) + } + get { + if let maxValue = numberFormatter.maximum { + return Double(truncating: maxValue) + } + return nil + } + } + + /// The number of decimal digits shown. + /// default is set to zero. + /// * Example: With decimal digits set to 3, if the value to represent is "1", + /// the formatted text in the fractions will be ",001". + /// Other than that with the value as 1, the formatted text fractions will be ",1". + public var decimalDigits: Int { + set { + numberFormatter.minimumFractionDigits = newValue + numberFormatter.maximumFractionDigits = newValue + } + get { numberFormatter.minimumFractionDigits } + } + + /// Set decimal numbers behavior. + /// When set to true decimalDigits are automatically set to 2 (most currencies pattern), + /// and the decimal separator is presented. Otherwise decimal digits are not shown and + /// the separator gets hidden as well + /// When reading it returns the current pattern based on the setup. + /// Note: Setting decimal digits after, or alwaysShowsDecimalSeparator can overlap this definitios, + /// and should be only done if you need specific cases + public var hasDecimals: Bool { + set { + self.decimalDigits = newValue ? 2 : 0 + self.numberFormatter.alwaysShowsDecimalSeparator = newValue ? true : false + } + get { decimalDigits != 0 } + } + + /// Defines the string that is the decimal separator + /// Note: only presented when hasDecimals is true OR decimalDigits + /// is greater than 0. + public var decimalSeparator: String { + set { self.numberFormatter.currencyDecimalSeparator = newValue } + get { numberFormatter.currencyDecimalSeparator } + } + + /// Can be used to set a custom currency code string + public var currencyCode: String { + set { self.numberFormatter.currencyCode = newValue } + get { numberFormatter.currencyCode } + } + + /// Sets if decimal separator should always be presented, + /// even when decimal digits are disabled + public var alwaysShowsDecimalSeparator: Bool { + set { self.numberFormatter.alwaysShowsDecimalSeparator = newValue } + get { numberFormatter.alwaysShowsDecimalSeparator } + } + + /// The amount of grouped numbers. This definition is fixed for at least + /// the first non-decimal group of numbers, and is applied to all other + /// groups if secondaryGroupingSize does not have another value. + public var groupingSize: Int { + set { self.numberFormatter.groupingSize = newValue } + get { numberFormatter.groupingSize } + } + + /// The amount of grouped numbers after the first group. + /// Example: for the given value of 99999999999, when grouping size + /// is set to 3 and secondaryGroupingSize has 4 as value, + /// the number is represented as: (9999) (9999) [999]. + /// Beign [] grouping size and () secondary grouping size. + public var secondaryGroupingSize: Int { + set { self.numberFormatter.secondaryGroupingSize = newValue } + get { numberFormatter.secondaryGroupingSize } + } + + /// Defines the string that is shown between groups of numbers + /// * Example: a monetary value of a thousand (1000) with a grouping + /// separator == "." is represented as `1.000` *. + /// Note: It automatically sets hasGroupingSeparator to true. + public var groupingSeparator: String { + set { + self.numberFormatter.currencyGroupingSeparator = newValue + self.numberFormatter.usesGroupingSeparator = true + } + get { self.numberFormatter.currencyGroupingSeparator } + } + + /// Sets if has separator between all group of numbers. + /// * Example: when set to false, a bug number such as a million + /// is represented by tight numbers "1000000". Otherwise if set + /// to true each group is separated by the defined `groupingSeparator`. * + /// Note: When set to true only works by defining a grouping separator. + public var hasGroupingSeparator: Bool { + set { self.numberFormatter.usesGroupingSeparator = newValue } + get { self.numberFormatter.usesGroupingSeparator } + } + + /// Value that will be presented when the text field + /// text values matches zero (0) + public var zeroSymbol: String? { + set { numberFormatter.zeroSymbol = newValue } + get { numberFormatter.zeroSymbol } + } + + /// Value that will be presented when the text field + /// is empty. The default is "" - empty string + public var nilSymbol: String { + set { numberFormatter.nilSymbol = newValue } + get { return numberFormatter.nilSymbol } + } + + /// Encapsulated Number formatter + let numberFormatter: NumberFormatter + + /// Maximum allowed number of integers + public var maxIntegers: Int? { + set { + guard let maxIntegers = newValue else { return } + numberFormatter.maximumIntegerDigits = maxIntegers + } + get { return numberFormatter.maximumIntegerDigits } + } + + /// Returns the maximum allowed number of numerical characters + public var maxDigitsCount: Int { + numberFormatter.maximumIntegerDigits + numberFormatter.maximumFractionDigits + } + + /// The value zero formatted to serve as initial text. + public var initialText: String { + numberFormatter.string(from: 0) ?? "0.0" + } + + //MARK: - INIT + + /// Handler to initialize a new style. + public typealias InitHandler = ((CurrencyFormatter) -> (Void)) + + /// Initialize a new currency formatter with optional configuration handler callback. + /// + /// - Parameter handler: configuration handler callback. + + public init(currency: String, _ handler: InitHandler? = nil) { + numberFormatter = setupCurrencyNumberFormatter(currency: currency) + + numberFormatter.alwaysShowsDecimalSeparator = false + /*numberFormatter.numberStyle = .currency + + numberFormatter.minimumFractionDigits = 2 + numberFormatter.maximumFractionDigits = 2 + numberFormatter.minimumIntegerDigits = 1*/ + + handler?(self) + } +} + +// MARK: Format +extension CurrencyFormatter { + + /// Returns a currency string from a given double value. + /// + /// - Parameter double: the monetary amount. + /// - Returns: formatted currency string. + public func string(from double: Double) -> String? { + let validValue = valueAdjustedToFitAllowedValues(from: double) + return numberFormatter.string(from: NSNumber(value: validValue)) + } + + /// Returns a double from a string that represents a numerical value. + /// + /// - Parameter string: string that describes the numerical value. + /// - Returns: the value as a Double. + public func double(from string: String) -> Double? { + Double(string) + } + + /// Receives a currency formatted string and returns its + /// numerical/unformatted representation. + /// + /// - Parameter string: currency formatted string + /// - Returns: numerical representation + public func unformatted(string: String) -> String? { + string.numeralFormat() + } +} + +// MARK: - Currency adjusting conformance + +extension CurrencyFormatter: CurrencyAdjusting { + + /// Receives a currency formatted String, and returns it with its decimal separator adjusted. + /// + /// _Note_: Useful when appending values to a currency formatted String. + /// E.g. "$ 23.24" after users taps an additional number, is equal = "$ 23.247". + /// Which gets updated to "$ 232.47". + /// + /// - Parameter string: The currency formatted String + /// - Returns: The currency formatted received String with its decimal separator adjusted + public func formattedStringWithAdjustedDecimalSeparator(from string: String) -> String? { + let adjustedString = numeralStringWithAdjustedDecimalSeparator(from: string) + guard let value = double(from: adjustedString) else { return nil } + + return self.numberFormatter.string(from: NSNumber(value: value)) + } + + /// Receives a currency formatted String, and returns it to fit the formatter's min and max values, when needed. + /// + /// - Parameter string: The currency formatted String + /// - Returns: The currency formatted String, or the formatted version of its closes allowed value, min or max, depending on the closest boundary. + public func formattedStringAdjustedToFitAllowedValues(from string: String) -> String? { + let adjustedString = numeralStringWithAdjustedDecimalSeparator(from: string) + guard let originalValue = double(from: adjustedString) else { return nil } + + return self.string(from: originalValue) + } + + /// Receives a currency formatted String, and returns a numeral version of it with its decimal separator adjusted. + /// + /// E.g. "$ 23.24", after users taps an additional number, get equal as "$ 23.247". The returned value would be "232.47". + /// + /// - Parameter string: The currency formatted String + /// - Returns: The received String with numeral format and with its decimal separator adjusted + private func numeralStringWithAdjustedDecimalSeparator(from string: String) -> String { + var updatedString = string.numeralFormat() + let isNegative: Bool = string.contains(String.negativeSymbol) + + updatedString = isNegative ? .negativeSymbol + updatedString : updatedString + updatedString.updateDecimalSeparator(decimalDigits: decimalDigits) + + return updatedString + } + + /// Receives a Double value, and returns it adjusted to fit min and max allowed values, when needed. + /// If the value respect number formatter's min and max, it will be returned without changes. + /// + /// - Parameter value: The value to be adjusted if needed + /// - Returns: The value updated or not, depending on the formatter's settings + private func valueAdjustedToFitAllowedValues(from value: Double) -> Double { + if let minValue = minValue, value < minValue { + return minValue + } else if let maxValue = maxValue, value > maxValue { + return maxValue + } + + return value + } +} diff --git a/Telegram-Mac/CurrencyLocale.swift b/Telegram-Mac/CurrencyLocale.swift new file mode 100644 index 0000000000..e9af7b2f76 --- /dev/null +++ b/Telegram-Mac/CurrencyLocale.swift @@ -0,0 +1,755 @@ +// +// CurrencyLocale.swift +// CurrencyText +// +// Created by Felipe Lefèvre Marino on 1/26/19. +// + +import Foundation + +/// All locales were extracted from: +/// jacobbubu/ioslocaleidentifiers.csv - https://gist.github.com/jacobbubu/1836273 + +/// The LocaleConvertible pattern is inspired in SwiftDate by malcommac +/// https://github.com/malcommac/SwiftDate + +/// LocaleConvertible defines the behavior to convert locale info to system Locale type +public protocol LocaleConvertible { + var locale: Locale { get } +} + +extension Locale: LocaleConvertible { + public var locale: Locale { return self } +} + +/// Defines locales available in system +public enum CurrencyLocale: String, LocaleConvertible { + + case current = "current" + case autoUpdating = "currentAutoUpdating" + + case afrikaans = "af" + case afrikaansNamibia = "af_NA" + case afrikaansSouthAfrica = "af_ZA" + case aghem = "agq" + case aghemCameroon = "agq_CM" + case akan = "ak" + case akanGhana = "ak_GH" + case albanian = "sq" + case albanianAlbania = "sq_AL" + case albanianKosovo = "sq_XK" + case albanianMacedonia = "sq_MK" + case amharic = "am" + case amharicEthiopia = "am_ET" + case arabic = "ar" + case arabicAlgeria = "ar_DZ" + case arabicBahrain = "ar_BH" + case arabicChad = "ar_TD" + case arabicComoros = "ar_KM" + case arabicDjibouti = "ar_DJ" + case arabicEgypt = "ar_EG" + case arabicEritrea = "ar_ER" + case arabicIraq = "ar_IQ" + case arabicIsrael = "ar_IL" + case arabicJordan = "ar_JO" + case arabicKuwait = "ar_KW" + case arabicLebanon = "ar_LB" + case arabicLibya = "ar_LY" + case arabicMauritania = "ar_MR" + case arabicMorocco = "ar_MA" + case arabicOman = "ar_OM" + case arabicPalestinianTerritories = "ar_PS" + case arabicQatar = "ar_QA" + case arabicSaudiArabia = "ar_SA" + case arabicSomalia = "ar_SO" + case arabicSouthSudan = "ar_SS" + case arabicSudan = "ar_SD" + case arabicSyria = "ar_SY" + case arabicTunisia = "ar_TN" + case arabicUnitedArabEmirates = "ar_AE" + case arabicWesternSahara = "ar_EH" + case arabicWorld = "ar_001" + case arabicYemen = "ar_YE" + case armenian = "hy" + case armenianArmenia = "hy_AM" + case assamese = "as" + case assameseIndia = "as_IN" + case asu = "asa" + case asuTanzania = "asa_TZ" + case azerbaijani = "az_Latn" + case azerbaijaniAzerbaijan = "az_Latn_AZ" + case azerbaijaniCyrillic = "az_Cyrl" + case azerbaijaniCyrillicAzerbaijan = "az_Cyrl_AZ" + case bafia = "ksf" + case bafiaCameroon = "ksf_CM" + case bambara = "bm_Latn" + case bambaraMali = "bm_Latn_ML" + case basaa = "bas" + case basaaCameroon = "bas_CM" + case basque = "eu" + case basqueSpain = "eu_ES" + case belarusian = "be" + case belarusianBelarus = "be_BY" + case bemba = "bem" + case bembaZambia = "bem_ZM" + case bena = "bez" + case benaTanzania = "bez_TZ" + case bengali = "bn" + case bengaliBangladesh = "bn_BD" + case engaliIndia = "bn_IN" + case bodo = "brx" + case bodoIndia = "brx_IN" + case bosnian = "bs_Latn" + case bosnianBosniaHerzegovina = "bs_Latn_BA" + case bosnianCyrillic = "bs_Cyrl" + case bosnianCyrillicBosniaHerzegovina = "bs_Cyrl_BA" + case breton = "br" + case bretonFrance = "br_FR" + case bulgarian = "bg" + case bulgarianBulgaria = "bg_BG" + case burmese = "my" + case burmeseMyanmarBurma = "my_MM" + case catalan = "ca" + case catalanAndorra = "ca_AD" + case catalanFrance = "ca_FR" + case catalanItaly = "ca_IT" + case catalanSpain = "ca_ES" + case centralAtlasTamazight = "tzm_Latn" + case centralAtlasTamazightMorocco = "tzm_Latn_MA" + case centralKurdish = "ckb" + case centralKurdishIran = "ckb_IR" + case centralKurdishIraq = "ckb_IQ" + case cherokee = "chr" + case cherokeeUnitedStates = "chr_US" + case chiga = "cgg" + case chigaUganda = "cgg_UG" + case chinese = "zh" + case chineseChina = "zh_Hans_CN" + case chineseHongKongSarChina = "zh_Hant_HK" + case chineseMacauSarChina = "zh_Hant_MO" + case chineseSimplified = "zh_Hans" + case chineseSimplifiedHongKongSarChina = "zh_Hans_HK" + case chineseSimplifiedMacauSarChina = "zh_Hans_MO" + case chineseSingapore = "zh_Hans_SG" + case chineseTaiwan = "zh_Hant_TW" + case chineseTraditional = "zh_Hant" + case colognian = "ksh" + case colognianGermany = "ksh_DE" + case cornish = "kw" + case cornishUnitedKingdom = "kw_GB" + case croatian = "hr" + case croatianBosniaHerzegovina = "hr_BA" + case croatianCroatia = "hr_HR" + case czech = "cs" + case czechCzechRepublic = "cs_CZ" + case danish = "da" + case danishDenmark = "da_DK" + case danishGreenland = "da_GL" + case duala = "dua" + case dualaCameroon = "dua_CM" + case dutch = "nl" + case dutchAruba = "nl_AW" + case dutchBelgium = "nl_BE" + case dutchCaribbeanNetherlands = "nl_BQ" + case dutchCuraao = "nl_CW" + case dutchNetherlands = "nl_NL" + case dutchSintMaarten = "nl_SX" + case dutchSuriname = "nl_SR" + case dzongkha = "dz" + case dzongkhaBhutan = "dz_BT" + case embu = "ebu" + case embuKenya = "ebu_KE" + case english = "en" + case englishAlbania = "en_AL" + case englishAmericanSamoa = "en_AS" + case englishAndorra = "en_AD" + case englishAnguilla = "en_AI" + case englishAntiguaBarbuda = "en_AG" + case englishAustralia = "en_AU" + case englishAustria = "en_AT" + case englishBahamas = "en_BS" + case englishBarbados = "en_BB" + case englishBelgium = "en_BE" + case englishBelize = "en_BZ" + case englishBermuda = "en_BM" + case englishBosniaHerzegovina = "en_BA" + case englishBotswana = "en_BW" + case englishBritishIndianOceanTerritory = "en_IO" + case englishBritishVirginIslands = "en_VG" + case englishCameroon = "en_CM" + case englishCanada = "en_CA" + case englishCaymanIslands = "en_KY" + case englishChristmasIsland = "en_CX" + case englishCocosKeelingIslands = "en_CC" + case englishCookIslands = "en_CK" + case englishCroatia = "en_HR" + case englishCyprus = "en_CY" + case englishCzechRepublic = "en_CZ" + case englishDenmark = "en_DK" + case englishDiegoGarcia = "en_DG" + case englishDominica = "en_DM" + case englishEritrea = "en_ER" + case englishEstonia = "en_EE" + case englishEurope = "en_150" + case englishFalklandIslands = "en_FK" + case englishFiji = "en_FJ" + case englishFinland = "en_FI" + case englishFrance = "en_FR" + case englishGambia = "en_GM" + case englishGermany = "en_DE" + case englishGhana = "en_GH" + case englishGibraltar = "en_GI" + case englishGreece = "en_GR" + case englishGrenada = "en_GD" + case englishGuam = "en_GU" + case englishGuernsey = "en_GG" + case englishGuyana = "en_GY" + case englishHongKongSarChina = "en_HK" + case englishHungary = "en_HU" + case englishIceland = "en_IS" + case englishIndia = "en_IN" + case englishIreland = "en_IE" + case englishIsleOfMan = "en_IM" + case englishIsrael = "en_IL" + case englishItaly = "en_IT" + case englishJamaica = "en_JM" + case englishJersey = "en_JE" + case englishKenya = "en_KE" + case englishKiribati = "en_KI" + case englishLatvia = "en_LV" + case englishLesotho = "en_LS" + case englishLiberia = "en_LR" + case englishLithuania = "en_LT" + case englishLuxembourg = "en_LU" + case englishMacauSarChina = "en_MO" + case englishMadagascar = "en_MG" + case englishMalawi = "en_MW" + case englishMalaysia = "en_MY" + case englishMalta = "en_MT" + case englishMarshallIslands = "en_MH" + case englishMauritius = "en_MU" + case englishMicronesia = "en_FM" + case englishMontenegro = "en_ME" + case englishMontserrat = "en_MS" + case englishNamibia = "en_NA" + case englishNauru = "en_NR" + case englishNetherlands = "en_NL" + case englishNewZealand = "en_NZ" + case englishNigeria = "en_NG" + case englishNiue = "en_NU" + case englishNorfolkIsland = "en_NF" + case englishNorthernMarianaIslands = "en_MP" + case englishNorway = "en_NO" + case englishPakistan = "en_PK" + case englishPalau = "en_PW" + case englishPapuaNewGuinea = "en_PG" + case englishPhilippines = "en_PH" + case englishPitcairnIslands = "en_PN" + case englishPoland = "en_PL" + case englishPortugal = "en_PT" + case englishPuertoRico = "en_PR" + case englishRomania = "en_RO" + case englishRussia = "en_RU" + case englishRwanda = "en_RW" + case englishSamoa = "en_WS" + case englishSeychelles = "en_SC" + case englishSierraLeone = "en_SL" + case englishSingapore = "en_SG" + case englishSintMaarten = "en_SX" + case englishSlovakia = "en_SK" + case englishSlovenia = "en_SI" + case englishSolomonIslands = "en_SB" + case englishSouthAfrica = "en_ZA" + case englishSouthSudan = "en_SS" + case englishSpain = "en_ES" + case englishStHelena = "en_SH" + case englishStKittsNevis = "en_KN" + case englishStLucia = "en_LC" + case englishStVincentGrenadines = "en_VC" + case englishSudan = "en_SD" + case englishSwaziland = "en_SZ" + case englishSweden = "en_SE" + case englishSwitzerland = "en_CH" + case englishTanzania = "en_TZ" + case englishTokelau = "en_TK" + case englishTonga = "en_TO" + case englishTrinidadTobago = "en_TT" + case englishTurkey = "en_TR" + case englishTurksCaicosIslands = "en_TC" + case englishTuvalu = "en_TV" + case englishUSOutlyingIslands = "en_UM" + case englishUSVirginIslands = "en_VI" + case englishUganda = "en_UG" + case englishUnitedKingdom = "en_GB" + case englishUnitedStates = "en_US" + case englishUnitedStatesComputer = "en_US_POSIX" + case englishVanuatu = "en_VU" + case englishWorld = "en_001" + case englishZambia = "en_ZM" + case englishZimbabwe = "en_ZW" + case esperanto = "eo" + case estonian = "et" + case estonianEstonia = "et_EE" + case ewe = "ee" + case eweGhana = "ee_GH" + case eweTogo = "ee_TG" + case ewondo = "ewo" + case ewondoCameroon = "ewo_CM" + case faroese = "fo" + case faroeseFaroeIslands = "fo_FO" + case filipino = "fil" + case filipinoPhilippines = "fil_PH" + case finnish = "fi" + case finnishFinland = "fi_FI" + case french = "fr" + case frenchAlgeria = "fr_DZ" + case frenchBelgium = "fr_BE" + case frenchBenin = "fr_BJ" + case frenchBurkinaFaso = "fr_BF" + case frenchBurundi = "fr_BI" + case frenchCameroon = "fr_CM" + case frenchCanada = "fr_CA" + case frenchCentralAfricanRepublic = "fr_CF" + case frenchChad = "fr_TD" + case frenchComoros = "fr_KM" + case frenchCongoBrazzaville = "fr_CG" + case frenchCongoKinshasa = "fr_CD" + case frenchCteDivoire = "fr_CI" + case frenchDjibouti = "fr_DJ" + case frenchEquatorialGuinea = "fr_GQ" + case frenchFrance = "fr_FR" + case frenchFrenchGuiana = "fr_GF" + case frenchFrenchPolynesia = "fr_PF" + case frenchGabon = "fr_GA" + case frenchGuadeloupe = "fr_GP" + case frenchGuinea = "fr_GN" + case frenchHaiti = "fr_HT" + case frenchLuxembourg = "fr_LU" + case frenchMadagascar = "fr_MG" + case frenchMali = "fr_ML" + case frenchMartinique = "fr_MQ" + case frenchMauritania = "fr_MR" + case frenchMauritius = "fr_MU" + case frenchMayotte = "fr_YT" + case frenchMonaco = "fr_MC" + case frenchMorocco = "fr_MA" + case frenchNewCaledonia = "fr_NC" + case frenchNiger = "fr_NE" + case frenchRunion = "fr_RE" + case frenchRwanda = "fr_RW" + case frenchSenegal = "fr_SN" + case frenchSeychelles = "fr_SC" + case frenchStBarthlemy = "fr_BL" + case frenchStMartin = "fr_MF" + case frenchStPierreMiquelon = "fr_PM" + case frenchSwitzerland = "fr_CH" + case frenchSyria = "fr_SY" + case frenchTogo = "fr_TG" + case frenchTunisia = "fr_TN" + case frenchVanuatu = "fr_VU" + case frenchWallisFutuna = "fr_WF" + case friulian = "fur" + case friulianItaly = "fur_IT" + case fulah = "ff" + case fulahCameroon = "ff_CM" + case fulahGuinea = "ff_GN" + case fulahMauritania = "ff_MR" + case fulahSenegal = "ff_SN" + case galician = "gl" + case galicianSpain = "gl_ES" + case ganda = "lg" + case gandaUganda = "lg_UG" + case georgian = "ka" + case georgianGeorgia = "ka_GE" + case german = "de" + case germanAustria = "de_AT" + case germanBelgium = "de_BE" + case germanGermany = "de_DE" + case germanLiechtenstein = "de_LI" + case germanLuxembourg = "de_LU" + case germanSwitzerland = "de_CH" + case greek = "el" + case greekCyprus = "el_CY" + case greekGreece = "el_GR" + case gujarati = "gu" + case gujaratiIndia = "gu_IN" + case gusii = "guz" + case gusiiKenya = "guz_KE" + case hausa = "ha_Latn" + case hausaGhana = "ha_Latn_GH" + case hausaNiger = "ha_Latn_NE" + case hausaNigeria = "ha_Latn_NG" + case hawaiian = "haw" + case hawaiianUnitedStates = "haw_US" + case hebrew = "he" + case hebrewIsrael = "he_IL" + case hindi = "hi" + case hindiIndia = "hi_IN" + case hungarian = "hu" + case hungarianHungary = "hu_HU" + case icelandic = "is" + case icelandicIceland = "is_IS" + case igbo = "ig" + case igboNigeria = "ig_NG" + case inariSami = "smn" + case inariSamiFinland = "smn_FI" + case indonesian = "id" + case indonesianIndonesia = "id_ID" + case inuktitut = "iu" + case inuktitutUnifiedCanadianAboriginalSyllabics = "iu_Cans" + case inuktitutUnifiedCanadianAboriginalSyllabicsCanada = "iu_Cans_CA" + case irish = "ga" + case irishIreland = "ga_IE" + case italian = "it" + case italianItaly = "it_IT" + case italianSanMarino = "it_SM" + case italianSwitzerland = "it_CH" + case japanese = "ja" + case japaneseJapan = "ja_JP" + case jolaFonyi = "dyo" + case jolaFonyiSenegal = "dyo_SN" + case kabuverdianu = "kea" + case kabuverdianuCapeVerde = "kea_CV" + case kabyle = "kab" + case kabyleAlgeria = "kab_DZ" + case kako = "kkj" + case kakoCameroon = "kkj_CM" + case kalaallisut = "kl" + case kalaallisutGreenland = "kl_GL" + case kalenjin = "kln" + case kalenjinKenya = "kln_KE" + case kamba = "kam" + case kambaKenya = "kam_KE" + case kannada = "kn" + case kannadaIndia = "kn_IN" + case kashmiri = "ks" + case kashmiriArabic = "ks_Arab" + case kashmiriArabicIndia = "ks_Arab_IN" + case kazakh = "kk_Cyrl" + case kazakhKazakhstan = "kk_Cyrl_KZ" + case khmer = "km" + case khmerCambodia = "km_KH" + case kikuyu = "ki" + case kikuyuKenya = "ki_KE" + case kinyarwanda = "rw" + case kinyarwandaRwanda = "rw_RW" + case konkani = "kok" + case konkaniIndia = "kok_IN" + case korean = "ko" + case koreanNorthKorea = "ko_KP" + case koreanSouthKorea = "ko_KR" + case koyraChiini = "khq" + case koyraChiiniMali = "khq_ML" + case koyraboroSenni = "ses" + case koyraboroSenniMali = "ses_ML" + case kwasio = "nmg" + case kwasioCameroon = "nmg_CM" + case kyrgyz = "ky_Cyrl" + case kyrgyzKyrgyzstan = "ky_Cyrl_KG" + case lakota = "lkt" + case lakotaUnitedStates = "lkt_US" + case langi = "lag" + case langiTanzania = "lag_TZ" + case lao = "lo" + case laoLaos = "lo_LA" + case latvian = "lv" + case latvianLatvia = "lv_LV" + case lingala = "ln" + case lingalaAngola = "ln_AO" + case lingalaCentralAfricanRepublic = "ln_CF" + case lingalaCongoBrazzaville = "ln_CG" + case lingalaCongoKinshasa = "ln_CD" + case lithuanian = "lt" + case lithuanianLithuania = "lt_LT" + case lowerSorbian = "dsb" + case lowerSorbianGermany = "dsb_DE" + case lubaKatanga = "lu" + case lubaKatangaCongoKinshasa = "lu_CD" + case luo = "luo" + case luoKenya = "luo_KE" + case luxembourgish = "lb" + case luxembourgishLuxembourg = "lb_LU" + case luyia = "luy" + case luyiaKenya = "luy_KE" + case macedonian = "mk" + case macedonianMacedonia = "mk_MK" + case machame = "jmc" + case machameTanzania = "jmc_TZ" + case makhuwaMeetto = "mgh" + case makhuwaMeettoMozambique = "mgh_MZ" + case makonde = "kde" + case makondeTanzania = "kde_TZ" + case malagasy = "mg" + case malagasyMadagascar = "mg_MG" + case malay = "ms_Latn" + case malayArabic = "ms_Arab" + case malayArabicBrunei = "ms_Arab_BN" + case malayArabicMalaysia = "ms_Arab_MY" + case malayBrunei = "ms_Latn_BN" + case malayMalaysia = "ms_Latn_MY" + case malaySingapore = "ms_Latn_SG" + case malayalam = "ml" + case malayalamIndia = "ml_IN" + case maltese = "mt" + case malteseMalta = "mt_MT" + case manx = "gv" + case manxIsleOfMan = "gv_IM" + case marathi = "mr" + case marathiIndia = "mr_IN" + case masai = "mas" + case masaiKenya = "mas_KE" + case masaiTanzania = "mas_TZ" + case meru = "mer" + case meruKenya = "mer_KE" + case meta = "mgo" + case metaCameroon = "mgo_CM" + case mongolian = "mn_Cyrl" + case mongolianMongolia = "mn_Cyrl_MN" + case morisyen = "mfe" + case morisyenMauritius = "mfe_MU" + case mundang = "mua" + case mundangCameroon = "mua_CM" + case nama = "naq" + case namaNamibia = "naq_NA" + case nepali = "ne" + case nepaliIndia = "ne_IN" + case nepaliNepal = "ne_NP" + case ngiemboon = "nnh" + case ngiemboonCameroon = "nnh_CM" + case ngomba = "jgo" + case ngombaCameroon = "jgo_CM" + case northNdebele = "nd" + case northNdebeleZimbabwe = "nd_ZW" + case northernSami = "se" + case northernSamiFinland = "se_FI" + case northernSamiNorway = "se_NO" + case northernSamiSweden = "se_SE" + case norwegianBokml = "nb" + case norwegianBokmlNorway = "nb_NO" + case norwegianBokmlSvalbardJanMayen = "nb_SJ" + case norwegianNynorsk = "nn" + case norwegianNynorskNorway = "nn_NO" + case nuer = "nus" + case nuerSudan = "nus_SD" + case nyankole = "nyn" + case nyankoleUganda = "nyn_UG" + case oriya = "or" + case oriyaIndia = "or_IN" + case oromo = "om" + case oromoEthiopia = "om_ET" + case oromoKenya = "om_KE" + case ossetic = "os" + case osseticGeorgia = "os_GE" + case osseticRussia = "os_RU" + case pashto = "ps" + case pashtoAfghanistan = "ps_AF" + case persian = "fa" + case persianAfghanistan = "fa_AF" + case persianIran = "fa_IR" + case polish = "pl" + case polishPoland = "pl_PL" + case portuguese = "pt" + case portugueseAngola = "pt_AO" + case portugueseBrazil = "pt_BR" + case portugueseCapeVerde = "pt_CV" + case portugueseGuineaBissau = "pt_GW" + case portugueseMacauSarChina = "pt_MO" + case portugueseMozambique = "pt_MZ" + case portuguesePortugal = "pt_PT" + case portugueseSoTomPrncipe = "pt_ST" + case portugueseTimorLeste = "pt_TL" + case punjabi = "pa_Guru" + case punjabiArabic = "pa_Arab" + case punjabiArabicPakistan = "pa_Arab_PK" + case punjabiIndia = "pa_Guru_IN" + case quechua = "qu" + case quechuaBolivia = "qu_BO" + case quechuaEcuador = "qu_EC" + case quechuaPeru = "qu_PE" + case romanian = "ro" + case romanianMoldova = "ro_MD" + case romanianRomania = "ro_RO" + case romansh = "rm" + case romanshSwitzerland = "rm_CH" + case rombo = "rof" + case romboTanzania = "rof_TZ" + case rundi = "rn" + case rundiBurundi = "rn_BI" + case russian = "ru" + case russianBelarus = "ru_BY" + case russianKazakhstan = "ru_KZ" + case russianKyrgyzstan = "ru_KG" + case russianMoldova = "ru_MD" + case russianRussia = "ru_RU" + case russianUkraine = "ru_UA" + case rwa = "rwk" + case rwaTanzania = "rwk_TZ" + case sakha = "sah" + case sakhaRussia = "sah_RU" + case samburu = "saq" + case samburuKenya = "saq_KE" + case sango = "sg" + case sangoCentralAfricanRepublic = "sg_CF" + case sangu = "sbp" + case sanguTanzania = "sbp_TZ" + case scottishGaelic = "gd" + case scottishGaelicUnitedKingdom = "gd_GB" + case sena = "seh" + case senaMozambique = "seh_MZ" + case serbian = "sr_Cyrl" + case serbianBosniaHerzegovina = "sr_Cyrl_BA" + case serbianKosovo = "sr_Cyrl_XK" + case serbianLatin = "sr_Latn" + case serbianLatinBosniaHerzegovina = "sr_Latn_BA" + case serbianLatinKosovo = "sr_Latn_XK" + case serbianLatinMontenegro = "sr_Latn_ME" + case serbianLatinSerbia = "sr_Latn_RS" + case serbianMontenegro = "sr_Cyrl_ME" + case serbianSerbia = "sr_Cyrl_RS" + case shambala = "ksb" + case shambalaTanzania = "ksb_TZ" + case shona = "sn" + case shonaZimbabwe = "sn_ZW" + case sichuanYi = "ii" + case sichuanYiChina = "ii_CN" + case sinhala = "si" + case sinhalaSriLanka = "si_LK" + case slovak = "sk" + case slovakSlovakia = "sk_SK" + case slovenian = "sl" + case slovenianSlovenia = "sl_SI" + case soga = "xog" + case sogaUganda = "xog_UG" + case somali = "so" + case somaliDjibouti = "so_DJ" + case somaliEthiopia = "so_ET" + case somaliKenya = "so_KE" + case somaliSomalia = "so_SO" + case spanish = "es" + case spanishArgentina = "es_AR" + case spanishBolivia = "es_BO" + case spanishCanaryIslands = "es_IC" + case spanishCeutaMelilla = "es_EA" + case spanishChile = "es_CL" + case spanishColombia = "es_CO" + case spanishCostaRica = "es_CR" + case spanishCuba = "es_CU" + case spanishDominicanRepublic = "es_DO" + case spanishEcuador = "es_EC" + case spanishElSalvador = "es_SV" + case spanishEquatorialGuinea = "es_GQ" + case spanishGuatemala = "es_GT" + case spanishHonduras = "es_HN" + case spanishLatinAmerica = "es_419" + case spanishMexico = "es_MX" + case spanishNicaragua = "es_NI" + case spanishPanama = "es_PA" + case spanishParaguay = "es_PY" + case spanishPeru = "es_PE" + case spanishPhilippines = "es_PH" + case spanishPuertoRico = "es_PR" + case spanishSpain = "es_ES" + case spanishUnitedStates = "es_US" + case spanishUruguay = "es_UY" + case spanishVenezuela = "es_VE" + case standardMoroccanTamazight = "zgh" + case standardMoroccanTamazightMorocco = "zgh_MA" + case swahili = "sw" + case swahiliCongoKinshasa = "sw_CD" + case swahiliKenya = "sw_KE" + case swahiliTanzania = "sw_TZ" + case swahiliUganda = "sw_UG" + case swedish = "sv" + case swedishlandIslands = "sv_AX" + case swedishFinland = "sv_FI" + case swedishSweden = "sv_SE" + case swissGerman = "gsw" + case swissGermanFrance = "gsw_FR" + case swissGermanLiechtenstein = "gsw_LI" + case swissGermanSwitzerland = "gsw_CH" + case tachelhit = "shi_Latn" + case tachelhitMorocco = "shi_Latn_MA" + case tachelhitTifinagh = "shi_Tfng" + case tachelhitTifinaghMorocco = "shi_Tfng_MA" + case taita = "dav" + case taitaKenya = "dav_KE" + case tajik = "tg_Cyrl" + case tajikTajikistan = "tg_Cyrl_TJ" + case tamil = "ta" + case tamilIndia = "ta_IN" + case tamilMalaysia = "ta_MY" + case tamilSingapore = "ta_SG" + case tamilSriLanka = "ta_LK" + case tasawaq = "twq" + case tasawaqNiger = "twq_NE" + case telugu = "te" + case teluguIndia = "te_IN" + case teso = "teo" + case tesoKenya = "teo_KE" + case tesoUganda = "teo_UG" + case thai = "th" + case thaiThailand = "th_TH" + case tibetan = "bo" + case tibetanChina = "bo_CN" + case tibetanIndia = "bo_IN" + case tigrinya = "ti" + case tigrinyaEritrea = "ti_ER" + case tigrinyaEthiopia = "ti_ET" + case tongan = "to" + case tonganTonga = "to_TO" + case turkish = "tr" + case turkishCyprus = "tr_CY" + case turkishTurkey = "tr_TR" + case turkmen = "tk_Latn" + case turkmenTurkmenistan = "tk_Latn_TM" + case ukrainian = "uk" + case ukrainianUkraine = "uk_UA" + case upperSorbian = "hsb" + case upperSorbianGermany = "hsb_DE" + case urdu = "ur" + case urduIndia = "ur_IN" + case urduPakistan = "ur_PK" + case uyghur = "ug" + case uyghurArabic = "ug_Arab" + case uyghurArabicChina = "ug_Arab_CN" + case uzbek = "uz_Cyrl" + case uzbekArabic = "uz_Arab" + case uzbekArabicAfghanistan = "uz_Arab_AF" + case uzbekLatin = "uz_Latn" + case uzbekLatinUzbekistan = "uz_Latn_UZ" + case uzbekUzbekistan = "uz_Cyrl_UZ" + case vai = "vai_Vaii" + case vaiLatin = "vai_Latn" + case vaiLatinLiberia = "vai_Latn_LR" + case vaiLiberia = "vai_Vaii_LR" + case vietnamese = "vi" + case vietnameseVietnam = "vi_VN" + case vunjo = "vun" + case vunjoTanzania = "vun_TZ" + case walser = "wae" + case walserSwitzerland = "wae_CH" + case welsh = "cy" + case welshUnitedKingdom = "cy_GB" + case westernFrisian = "fy" + case westernFrisianNetherlands = "fy_NL" + case yangben = "yav" + case yangbenCameroon = "yav_CM" + case yiddish = "yi" + case yiddishWorld = "yi_001" + case yoruba = "yo" + case yorubaBenin = "yo_BJ" + case yorubaNigeria = "yo_NG" + case zarma = "dje" + case zarmaNiger = "dje_NE" + case zulu = "zu" + case zuluSouthAfrica = "zu_ZA" + + /// Return a valid `Locale` instance from currency locale enum + public var locale: Locale { + switch self { + case .current: return Locale.current + case .autoUpdating: return Locale.autoupdatingCurrent + default: return Locale(identifier: rawValue) + } + } +} diff --git a/Telegram-Mac/CurrencyUITextFieldDelegate.swift b/Telegram-Mac/CurrencyUITextFieldDelegate.swift new file mode 100644 index 0000000000..c383936399 --- /dev/null +++ b/Telegram-Mac/CurrencyUITextFieldDelegate.swift @@ -0,0 +1,114 @@ + + +import Cocoa + +/// Custom text field delegate, that formats user inputs based on a given currency formatter. +public class CurrencyUITextFieldDelegate: NSObject { + + public var formatter: (CurrencyFormatting & CurrencyAdjusting)! + + public var textUpdated: (() -> Void)? + + /// Text field clears its text when value value is equal to zero. + public var clearsWhenValueIsZero: Bool = false + + /// A delegate object to receive and potentially handle `UITextFieldDelegate events` that are sent to `CurrencyUITextFieldDelegate`. + /// + /// Note: Make sure the implementation of this object does not wrongly interfere with currency formatting. + /// + /// By returning `false` on`textField(textField:shouldChangeCharactersIn:replacementString:)` no currency formatting is done. + public var passthroughDelegate: NSTextViewDelegate? { + get { return _passthroughDelegate } + set { + guard newValue !== self else { return } + _passthroughDelegate = newValue + } + } + weak private(set) var _passthroughDelegate: NSTextViewDelegate? + + public init(formatter: CurrencyFormatter) { + self.formatter = formatter + } +} + +// MARK: - UITextFieldDelegate + +extension CurrencyUITextFieldDelegate: NSTextViewDelegate { + + + + + public func textView(_ textView: NSTextView, shouldChangeTextIn affectedCharRange: NSRange, replacementString : String?) -> Bool { + let lastSelectedTextRangeOffsetFromEnd = textView.selectedTextRangeOffsetFromEnd + + let string = replacementString ?? "" + let range = affectedCharRange + + defer { + textView.updateSelectedTextRange(lastOffsetFromEnd: lastSelectedTextRangeOffsetFromEnd) + textUpdated?() + } + + guard !string.isEmpty else { + handleDeletion(in: textView, at: range) + return false + } + guard string.hasNumbers else { + addNegativeSymbolIfNeeded(in: textView, at: range, replacementString: string) + return false + } + + setFormattedText(in: textView, inputString: string, range: range) + + return false + } + +} + + +extension CurrencyUITextFieldDelegate { + + private func addNegativeSymbolIfNeeded(in textField: NSTextView, at range: NSRange, replacementString string: String) { + + if string == .negativeSymbol && textField.string.isEmpty { + textField.string = .negativeSymbol + } else if range.lowerBound == 0 && string == .negativeSymbol && + textField.string.contains(String.negativeSymbol) == false { + + textField.string = .negativeSymbol + textField.string + } + } + private func handleDeletion(in textField: NSTextView, at range: NSRange) { + var text = textField.string + if let textRange = Range(range, in: text) { + text.removeSubrange(textRange) + } else { + text.removeLast() + } + + if text.isEmpty { + textField.string = text + } else { + textField.string = formatter.formattedStringWithAdjustedDecimalSeparator(from: text) ?? "" + } + } + func setFormattedText(in textField: NSTextView, inputString: String, range: NSRange) { + var updatedText = "" + + let text = textField.string + if text.isEmpty { + updatedText = formatter.initialText + inputString + } else if let range = Range(range, in: text) { + updatedText = text.replacingCharacters(in: range, with: inputString) + } else { + updatedText = text.appending(inputString) + } + + if updatedText.numeralFormat().count > formatter.maxDigitsCount { + updatedText.removeLast() + } + + textField.string = formatter.formattedStringWithAdjustedDecimalSeparator(from: updatedText) ?? "" + } + +} diff --git a/Telegram-Mac/CustomAccentColorModalController.swift b/Telegram-Mac/CustomAccentColorModalController.swift new file mode 100644 index 0000000000..8ee17c33c1 --- /dev/null +++ b/Telegram-Mac/CustomAccentColorModalController.swift @@ -0,0 +1,291 @@ +// +// CustomAccentColorModalController.swift +// Telegram +// +// Created by Mikhail Filimonov on 21/08/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + +private final class CustomAccentColorView : View { + private let tableView: TableView = TableView(frame: NSZeroRect, isFlipped: false) + weak var controller: ModalViewController? + let colorPicker = WallpaperColorPickerContainerView(frame: NSZeroRect) + private let context: AccountContext + private let backgroundView = BackgroundView(frame: .zero) + fileprivate let segmentControl = CatalinaStyledSegmentController(frame: NSMakeRect(0, 0, 290, 30)) + private let segmentContainer = View() + + required init(frame frameRect: NSRect, theme: TelegramPresentationTheme, context: AccountContext) { + self.context = context + super.init(frame: frameRect) + self.addSubview(backgroundView) + self.addSubview(tableView) + self.addSubview(colorPicker) + colorPicker.colorPicker.color = theme.colors.accent + + + + tableView.addScroll(listener: TableScrollListener(dispatchWhenVisibleRangeUpdated: false, { [weak self] position in + guard let `self` = self else { + return + } + self.tableView.enumerateVisibleViews(with: { view in + if let view = view as? ChatRowView { + view.updateBackground(animated: false, item: view.item) + } + }) + })) + + if theme.bubbled { + backgroundView.backgroundMode = theme.backgroundMode + } else { + backgroundView.backgroundMode = .color(color: theme.colors.chatBackground) + } + + segmentContainer.backgroundColor = theme.colors.background + + segmentContainer.addSubview(segmentControl.view) + self.addSubview(segmentContainer) + + + + } + + override func layout() { + super.layout() + updateLayout(size: frame.size, transition: .immediate) + } + + func updateLayout(size: NSSize, transition: ContainedViewLayoutTransition) { + + transition.updateFrame(view: backgroundView, frame: NSMakeRect(0, 50, frame.width, frame.height - 160)) + backgroundView.updateLayout(size: NSMakeSize(frame.width, frame.height - 160), transition: transition) + + transition.updateFrame(view: tableView, frame: NSMakeRect(0, 50, frame.width, frame.height - 160 - 50)) + + transition.updateFrame(view: colorPicker, frame: NSMakeRect(0, frame.height - 160, frame.width, 160)) + colorPicker.updateLayout(size: NSMakeSize(frame.width, 160), transition: transition) + + transition.updateFrame(view: segmentContainer, frame: NSMakeRect(0, 0, frame.width, 50)) + transition.updateFrame(view: segmentControl.view, frame: segmentControl.view.centerFrame()) + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + fileprivate func addTableItems(_ context: AccountContext, theme: TelegramPresentationTheme) { + + segmentContainer.backgroundColor = theme.colors.background + segmentContainer.borderColor = theme.colors.border + segmentContainer.border = [.Bottom] + segmentControl.theme = CatalinaSegmentTheme(backgroundColor: theme.colors.listBackground, foregroundColor: theme.colors.background, activeTextColor: theme.colors.text, inactiveTextColor: theme.colors.listGrayText) + + + tableView.removeAll() + self.tableView.getBackgroundColor = { + .clear + } + _ = tableView.addItem(item: GeneralRowItem(frame.size, height: 10, stableId: arc4random(), backgroundColor: .clear)) + + + let chatInteraction = ChatInteraction(chatLocation: .peer(PeerId(0)), context: context, disableSelectAbility: true) + + chatInteraction.getGradientOffsetRect = { [weak self] in + guard let `self` = self else { + return .zero + } + let offset = self.tableView.scrollPosition().current.rect.origin + return CGRect(origin: offset, size: self.tableView.frame.size) + } + + let fromUser1 = TelegramUser(id: PeerId(1), accessHash: nil, firstName: L10n.appearanceSettingsChatPreviewUserName1, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + + let fromUser2 = TelegramUser(id: PeerId(2), accessHash: nil, firstName: L10n.appearanceSettingsChatPreviewUserName2, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + + + + let firstMessage = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: fromUser1.id, namespace: 0, id: 1), globallyUniqueId: 0, groupingKey: 0, groupInfo: nil, threadId: nil, timestamp: 60 * 18 + 60*60*18, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: fromUser1, text: L10n.appearanceSettingsChatPreview1, attributes: [], media: [], peers:SimpleDictionary([fromUser2.id : fromUser2, fromUser1.id : fromUser1]) , associatedMessages: SimpleDictionary(), associatedMessageIds: []) + + let firstEntry: ChatHistoryEntry = .MessageEntry(firstMessage, MessageIndex(firstMessage), true, theme.bubbled ? .bubble : .list, .Full(rank: nil, header: .normal), nil, ChatHistoryEntryData(nil, MessageEntryAdditionalData(), AutoplayMediaPreferences.defaultSettings)) + + + let timestamp1: Int32 = 60 * 20 + 60 * 60 * 18 + + let secondMessage = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: fromUser1.id, namespace: 0, id: 0), globallyUniqueId: 0, groupingKey: 0, groupInfo: nil, threadId: nil, timestamp: timestamp1, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: fromUser2, text: tr(L10n.appearanceSettingsChatPreview2), attributes: [ReplyMessageAttribute(messageId: firstMessage.id, threadMessageId: nil)], media: [], peers:SimpleDictionary([fromUser2.id : fromUser2, fromUser1.id : fromUser1]) , associatedMessages: SimpleDictionary([firstMessage.id : firstMessage]), associatedMessageIds: []) + + let secondEntry: ChatHistoryEntry = .MessageEntry(secondMessage, MessageIndex(secondMessage), true, theme.bubbled ? .bubble : .list, .Full(rank: nil, header: .normal), nil, ChatHistoryEntryData(nil, MessageEntryAdditionalData(), AutoplayMediaPreferences.defaultSettings)) + + let timestamp2: Int32 = 60 * 22 + 60 * 60 * 18 + + let thridMessage = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: fromUser1.id, namespace: 0, id: 1), globallyUniqueId: 0, groupingKey: 0, groupInfo: nil, threadId: nil, timestamp: timestamp2, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: fromUser1, text: L10n.appearanceSettingsChatPreview3, attributes: [], media: [], peers:SimpleDictionary([fromUser2.id : fromUser2, fromUser1.id : fromUser1]) , associatedMessages: SimpleDictionary(), associatedMessageIds: []) + + let thridEntry: ChatHistoryEntry = .MessageEntry(thridMessage, MessageIndex(thridMessage), true, theme.bubbled ? .bubble : .list, .Full(rank: nil, header: .normal), nil, ChatHistoryEntryData(nil, MessageEntryAdditionalData(), AutoplayMediaPreferences.defaultSettings)) + + + let item1 = ChatRowItem.item(frame.size, from: firstEntry, interaction: chatInteraction, theme: theme) + let item2 = ChatRowItem.item(frame.size, from: secondEntry, interaction: chatInteraction, theme: theme) + let item3 = ChatRowItem.item(frame.size, from: thridEntry, interaction: chatInteraction, theme: theme) + + let items = [item1, item2, item3] + for item in items { + _ = item.makeSize(frame.width, oldWidth: 0) + _ = self.tableView.addItem(item: item) + } + + } + + func updateMode(_ mode: WallpaperColorSelectMode, newTheme: TelegramPresentationTheme) { + self.colorPicker.colorPicker.needsLayout = true + self.addTableItems(self.context, theme: newTheme) + self.tableView.updateLocalizationAndTheme(theme: newTheme) + self.controller?.updateLocalizationAndTheme(theme: newTheme) + self.colorPicker.updateLocalizationAndTheme(theme: newTheme) + self.updateLocalizationAndTheme(theme: newTheme) + self.colorPicker.updateMode(mode, animated: true) + + } + +} + + +class CustomAccentColorModalController: ModalViewController { + + enum SelectMode { + case accent + case messages + } + + private var selectMode: SelectMode = .accent + + private let context: AccountContext + private let updateColor: (PaletteAccentColor)->Void + init(context: AccountContext, updateColor: @escaping(PaletteAccentColor)->Void) { + self.context = context + self.updateColor = updateColor + super.init(frame: NSMakeRect(0, 0, 350, 380)) + self.bar = .init(height: 0) + } + private var currentTheme: TelegramPresentationTheme = theme + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + currentTheme = theme as! TelegramPresentationTheme + self.modal?.updateLocalizationAndTheme(theme: theme) + } + private func updateSelectMode(_ selectMode: SelectMode, animated: Bool) { + self.selectMode = selectMode + switch selectMode { + case .accent: + self.genericView.colorPicker.updateMode(.single(currentTheme.colors.accent), animated: animated) + case .messages: + self.genericView.colorPicker.updateMode(.gradient(currentTheme.colors.bubbleBackground_outgoing, 0, nil), animated: animated) + } + } + + override func viewDidLoad() { + super.viewDidLoad() + + genericView.controller = self + + genericView.addTableItems(self.context, theme: theme) + + self.genericView.segmentControl.add(segment: CatalinaSegmentedItem(title: L10n.appearanceThemeAccent, handler: { [weak self] in + self?.updateSelectMode(.accent, animated: true) + })) + if theme.bubbled && System.supportsTransparentFontDrawing { + self.genericView.segmentControl.add(segment: CatalinaSegmentedItem(title: L10n.appearanceThemeAccentMessages, handler: { [weak self] in + self?.updateSelectMode(.messages, animated: true) + })) + } + + + genericView.colorPicker.updateMode(.single(theme.colors.accent), animated: false) + + genericView.colorPicker.modeDidUpdate = { [weak self] mode in + guard let `self` = self else {return} + var newTheme = self.currentTheme + switch mode { + case let .single(color): + let accent = PaletteAccentColor(color, newTheme.colors.bubbleBackground_outgoing) + let colors = newTheme.colors.withoutAccentColor().withAccentColor(accent, disableTint: false) + newTheme = newTheme.withUpdatedColors(colors) + + case let .gradient(colors, _, _): + let accent = PaletteAccentColor(newTheme.colors.accent, colors) + let colors = newTheme.colors.withoutAccentColor().withAccentColor(accent, disableTint: false) + newTheme = newTheme.withUpdatedColors(colors) + } + self.currentTheme = newTheme + self.genericView.updateMode(mode, newTheme: newTheme) + } + + readyOnce() + } + + +// override var modalHeader: (left: ModalHeaderData?, center: ModalHeaderData?, right: ModalHeaderData?)? { +// return (left: ModalHeaderData(image: currentTheme.icons.modalClose, handler: { [weak self] in +// self?.close() +// }), center: ModalHeaderData(title: L10n.generalSettingsAccentColor), right: nil) +// } + + private func saveAccent() { + self.updateColor(PaletteAccentColor(currentTheme.colors.accent, currentTheme.colors.bubbleBackground_outgoing)) + + delay(0.1, closure: { [weak self] in + self?.close() + }) + } + + override var modalInteractions: ModalInteractions? { + return ModalInteractions(acceptTitle: L10n.modalSet, accept: { [weak self] in + self?.saveAccent() + }, drawBorder: true, singleButton: true, customTheme: { [weak self] in + return self?.modalTheme ?? .init() + }) + } + + override var modalTheme: ModalViewController.Theme { + return .init(text: currentTheme.colors.text, grayText: currentTheme.colors.grayText, background: currentTheme.colors.background, border: currentTheme.colors.border, accent: currentTheme.colors.accent, grayForeground: currentTheme.colors.grayForeground) + } + + override var dynamicSize: Bool { + return true + } + + override func initializer() -> NSView { + return CustomAccentColorView(frame: NSMakeRect(_frameRect.minX, _frameRect.minY, _frameRect.width, _frameRect.height - bar.height), theme: currentTheme, context: self.context) + } + + override func measure(size: NSSize) { + self.modal?.resize(with: NSMakeSize(350, 380), animated: false) + } + + private var genericView:CustomAccentColorView { + return self.view as! CustomAccentColorView + } + override func viewClass() -> AnyClass { + return CustomAccentColorView.self + } + + override var handleAllEvents: Bool { + return false + } + + override func firstResponder() -> NSResponder? { + return genericView.colorPicker.colorEditor.textView.inputView + } +} diff --git a/Telegram-Mac/DataAndStorageViewController.swift b/Telegram-Mac/DataAndStorageViewController.swift index e35400c1c1..8bf8e737d1 100644 --- a/Telegram-Mac/DataAndStorageViewController.swift +++ b/Telegram-Mac/DataAndStorageViewController.swift @@ -8,38 +8,163 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac +import Postbox +import TelegramCore +import SwiftSignalKit -private enum AutomaticDownloadCategory { + +enum DataAndStorageEntryTag : ItemListItemTag { + case automaticDownloadReset + case autoplayGifs + case autoplayVideos + + func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? DataAndStorageEntryTag, self == other { + return true + } else { + return false + } + } + var stableId: Int32 { + switch self { + case .automaticDownloadReset: + return 10 + case .autoplayGifs: + return 13 + case .autoplayVideos: + return 14 + } + } +} + + +public func autodownloadDataSizeString(_ size: Int64) -> String { + if size >= 1024 * 1024 * 1024 { + let remainder = (size % (1024 * 1024 * 1024)) / (1024 * 1024 * 102) + if remainder != 0 { + return "\(size / (1024 * 1024 * 1024)),\(remainder) GB" + } else { + return "\(size / (1024 * 1024 * 1024)) GB" + } + } else if size >= 1024 * 1024 { + let remainder = (size % (1024 * 1024)) / (1024 * 102) + if size < 10 * 1024 * 1024 { + return "\(size / (1024 * 1024)),\(remainder) MB" + } else { + return "\(size / (1024 * 1024)) MB" + } + } else if size >= 1024 { + return "\(size / 1024) KB" + } else { + return "\(size) B" + } +} + + +private struct AutomaticDownloadPeers { + let privateChats: Bool + let groups: Bool + let channels: Bool + let size: Int32? + + init(category: AutomaticMediaDownloadCategoryPeers) { + self.privateChats = category.privateChats + self.groups = category.groupChats + self.channels = category.channels + self.size = category.fileSize + } +} + + +private func stringForAutomaticDownloadPeers(peers: AutomaticDownloadPeers, category: AutomaticDownloadCategory) -> String { + var size: String? + if var peersSize = peers.size, category == .video || category == .file { + if peersSize == Int32.max { + peersSize = 1536 * 1024 * 1024 + } + size = autodownloadDataSizeString(Int64(peersSize)) + } + + if peers.privateChats && peers.groups && peers.channels { + if let size = size { + return L10n.autoDownloadSettingsUpToForAll(size) + } else { + return L10n.autoDownloadSettingsOnForAll + } + } else { + var types: [String] = [] + if peers.privateChats { + types.append(L10n.autoDownloadSettingsTypePrivateChats) + } + if peers.groups { + types.append(L10n.autoDownloadSettingsTypeGroupChats) + } + if peers.channels { + types.append(L10n.autoDownloadSettingsTypeChannels) + } + + if types.isEmpty { + return L10n.autoDownloadSettingsOffForAll + } + + var string: String = "" + for i in 0 ..< types.count { + if !string.isEmpty { + if i == types.count - 1 { + string.append(L10n.autoDownloadSettingsLastDelimeter) + } else { + string.append(L10n.autoDownloadSettingsDelimeter) + } + } + string.append(types[i]) + } + + if let size = size { + return L10n.autoDownloadSettingsUpToFor(size, string) + } else { + return L10n.autoDownloadSettingsOnFor(string) + } + } +} + + +enum AutomaticDownloadCategory { case photo - case voice - case instantVideo - case gif + case video + case file } -private enum AutomaticDownloadPeers { - case privateChats - case groupsAndChannels +private enum AutomaticDownloadPeerType { + case contact + case otherPrivate + case group + case channel } + private final class DataAndStorageControllerArguments { let openStorageUsage: () -> Void let openNetworkUsage: () -> Void - let toggleAutomaticDownload: (AutomaticDownloadCategory, AutomaticDownloadPeers, Bool) -> Void - let openVoiceUseLessData: () -> Void - let toggleSaveIncomingPhotos: (Bool) -> Void - let toggleSaveEditedPhotos: (Bool) -> Void - - init(openStorageUsage: @escaping () -> Void, openNetworkUsage: @escaping () -> Void, toggleAutomaticDownload: @escaping (AutomaticDownloadCategory, AutomaticDownloadPeers, Bool) -> Void, openVoiceUseLessData: @escaping () -> Void, toggleSaveIncomingPhotos: @escaping (Bool) -> Void, toggleSaveEditedPhotos: @escaping (Bool) -> Void) { + let openCategorySettings: (AutomaticMediaDownloadCategoryPeers, String) -> Void + let toggleAutomaticDownload:(Bool) -> Void + let resetDownloadSettings:()->Void + let selectDownloadFolder: ()->Void + let toggleAutoplayGifs:(Bool) -> Void + let toggleAutoplayVideos:(Bool) -> Void + let toggleAutoplaySoundOnHover:(Bool) -> Void + let openProxySettings:()->Void + init(openStorageUsage: @escaping () -> Void, openNetworkUsage: @escaping () -> Void, openCategorySettings: @escaping(AutomaticMediaDownloadCategoryPeers, String) -> Void, toggleAutomaticDownload:@escaping(Bool) -> Void, resetDownloadSettings:@escaping()->Void, selectDownloadFolder: @escaping() -> Void, toggleAutoplayGifs: @escaping(Bool) -> Void, toggleAutoplayVideos:@escaping(Bool) -> Void, toggleAutoplaySoundOnHover:@escaping(Bool) -> Void, openProxySettings: @escaping()->Void) { self.openStorageUsage = openStorageUsage self.openNetworkUsage = openNetworkUsage + self.openCategorySettings = openCategorySettings self.toggleAutomaticDownload = toggleAutomaticDownload - self.openVoiceUseLessData = openVoiceUseLessData - self.toggleSaveIncomingPhotos = toggleSaveIncomingPhotos - self.toggleSaveEditedPhotos = toggleSaveEditedPhotos + self.resetDownloadSettings = resetDownloadSettings + self.selectDownloadFolder = selectDownloadFolder + self.toggleAutoplayGifs = toggleAutoplayGifs + self.toggleAutoplayVideos = toggleAutoplayVideos + self.toggleAutoplaySoundOnHover = toggleAutoplaySoundOnHover + self.openProxySettings = openProxySettings } } @@ -54,22 +179,26 @@ private enum DataAndStorageSection: Int32 { private enum DataAndStorageEntry: TableItemListNodeEntry { - case storageUsage(Int32, String) - case networkUsage(Int32, String) - case automaticPhotoDownloadHeader(Int32, String) - case automaticPhotoDownloadPrivateChats(Int32, String, Bool) - case automaticPhotoDownloadGroupsAndChannels(Int32, String, Bool) - case automaticVoiceDownloadHeader(Int32, String) - case automaticVoiceDownloadPrivateChats(Int32, String, Bool) - case automaticVoiceDownloadGroupsAndChannels(Int32, String, Bool) - case automaticInstantVideoDownloadHeader(Int32, String) - case automaticInstantVideoDownloadPrivateChats(Int32, String, Bool) - case automaticInstantVideoDownloadGroupsAndChannels(Int32, String, Bool) - case voiceCallsHeader(Int32, String) - case useLessVoiceData(Int32, String, String) - case otherHeader(Int32, String) - case saveIncomingPhotos(Int32, String, Bool) - case saveEditedPhotos(Int32, String, Bool) + case storageUsage(Int32, String, viewType: GeneralViewType) + case networkUsage(Int32, String, viewType: GeneralViewType) + case automaticMediaDownloadHeader(Int32, String, viewType: GeneralViewType) + case automaticDownloadMedia(Int32, Bool, viewType: GeneralViewType) + case photos(Int32, AutomaticMediaDownloadCategoryPeers, Bool, viewType: GeneralViewType) + case videos(Int32, AutomaticMediaDownloadCategoryPeers, Bool, Int32?, viewType: GeneralViewType) + case files(Int32, AutomaticMediaDownloadCategoryPeers, Bool, Int32?, viewType: GeneralViewType) + case voice(Int32, AutomaticMediaDownloadCategoryPeers, Bool, viewType: GeneralViewType) + case instantVideo(Int32, AutomaticMediaDownloadCategoryPeers, Bool, viewType: GeneralViewType) + case gifs(Int32, AutomaticMediaDownloadCategoryPeers, Bool, viewType: GeneralViewType) + + case autoplayHeader(Int32, viewType: GeneralViewType) + case autoplayGifs(Int32, Bool, viewType: GeneralViewType) + case autoplayVideos(Int32, Bool, viewType: GeneralViewType) + case soundOnHover(Int32, Bool, viewType: GeneralViewType) + case soundOnHoverDesc(Int32, viewType: GeneralViewType) + case resetDownloadSettings(Int32, Bool, viewType: GeneralViewType) + case downloadFolder(Int32, String, viewType: GeneralViewType) + case proxyHeader(Int32) + case proxySettings(Int32, String, viewType: GeneralViewType) case sectionId(Int32) var stableId: Int32 { @@ -78,34 +207,40 @@ private enum DataAndStorageEntry: TableItemListNodeEntry { return 0 case .networkUsage: return 1 - case .automaticPhotoDownloadHeader: + case .automaticMediaDownloadHeader: return 2 - case .automaticPhotoDownloadPrivateChats: + case .automaticDownloadMedia: return 3 - case .automaticPhotoDownloadGroupsAndChannels: + case .photos: return 4 - case .automaticVoiceDownloadHeader: + case .videos: return 5 - case .automaticVoiceDownloadPrivateChats: + case .files: return 6 - case .automaticVoiceDownloadGroupsAndChannels: + case .voice: return 7 - case .automaticInstantVideoDownloadHeader: + case .instantVideo: return 8 - case .automaticInstantVideoDownloadPrivateChats: + case .gifs: return 9 - case .automaticInstantVideoDownloadGroupsAndChannels: + case .resetDownloadSettings: return 10 - case .voiceCallsHeader: + case .downloadFolder: return 11 - case .useLessVoiceData: + case .autoplayHeader: return 12 - case .otherHeader: + case .autoplayGifs: return 13 - case .saveIncomingPhotos: + case .autoplayVideos: return 14 - case .saveEditedPhotos: + case .soundOnHover: return 15 + case .soundOnHoverDesc: + return 16 + case .proxyHeader: + return 17 + case .proxySettings: + return 18 case let .sectionId(sectionId): return (sectionId + 1) * 1000 - sectionId } @@ -113,147 +248,46 @@ private enum DataAndStorageEntry: TableItemListNodeEntry { var index:Int32 { switch self { - case .storageUsage(let sectionId, _): + case .storageUsage(let sectionId, _, _): return (sectionId * 1000) + stableId - case .networkUsage(let sectionId, _): + case .networkUsage(let sectionId, _, _): return (sectionId * 1000) + stableId - case .automaticPhotoDownloadHeader(let sectionId, _): + case .automaticMediaDownloadHeader(let sectionId, _, _): return (sectionId * 1000) + stableId - case .automaticPhotoDownloadPrivateChats(let sectionId, _, _): + case .automaticDownloadMedia(let sectionId, _, _): return (sectionId * 1000) + stableId - case .automaticPhotoDownloadGroupsAndChannels(let sectionId, _, _): + case let .photos(sectionId, _, _, _): return (sectionId * 1000) + stableId - case .automaticVoiceDownloadHeader(let sectionId, _): + case let .videos(sectionId, _, _, _, _): return (sectionId * 1000) + stableId - case .automaticVoiceDownloadPrivateChats(let sectionId, _, _): + case let .files(sectionId, _, _, _, _): return (sectionId * 1000) + stableId - case .automaticVoiceDownloadGroupsAndChannels(let sectionId, _, _): + case let .voice(sectionId, _, _, _): return (sectionId * 1000) + stableId - case .automaticInstantVideoDownloadHeader(let sectionId, _): + case let .instantVideo(sectionId, _, _, _): return (sectionId * 1000) + stableId - case .automaticInstantVideoDownloadPrivateChats(let sectionId, _, _): + case let .gifs(sectionId, _, _, _): return (sectionId * 1000) + stableId - case .automaticInstantVideoDownloadGroupsAndChannels(let sectionId, _, _): + case let .resetDownloadSettings(sectionId, _, _): return (sectionId * 1000) + stableId - case .voiceCallsHeader(let sectionId, _): + case let .autoplayHeader(sectionId, _): return (sectionId * 1000) + stableId - case .useLessVoiceData(let sectionId, _, _): + case let .autoplayGifs(sectionId, _, _): return (sectionId * 1000) + stableId - case .otherHeader(let sectionId, _): + case let .autoplayVideos(sectionId, _, _): return (sectionId * 1000) + stableId - case .saveIncomingPhotos(let sectionId, _, _): + case let .soundOnHover(sectionId, _, _): return (sectionId * 1000) + stableId - case .saveEditedPhotos(let sectionId, _, _): + case let .soundOnHoverDesc(sectionId, _): return (sectionId * 1000) + stableId - case .sectionId(let sectionId): - return (sectionId + 1) * 1000 - sectionId - } - } - - static func ==(lhs: DataAndStorageEntry, rhs: DataAndStorageEntry) -> Bool { - switch lhs { - case let .storageUsage(sectionId, text): - if case .storageUsage(sectionId, text) = rhs { - return true - } else { - return false - } - case let .networkUsage(sectionId, text): - if case .networkUsage(sectionId, text) = rhs { - return true - } else { - return false - } - case let .automaticPhotoDownloadHeader(sectionId, text): - if case .automaticPhotoDownloadHeader(sectionId, text) = rhs { - return true - } else { - return false - } - case let .automaticPhotoDownloadPrivateChats(sectionId, text, value): - if case .automaticPhotoDownloadPrivateChats(sectionId, text, value) = rhs { - return true - } else { - return false - } - case let .automaticPhotoDownloadGroupsAndChannels(sectionId, text, value): - if case .automaticPhotoDownloadGroupsAndChannels(sectionId, text, value) = rhs { - return true - } else { - return false - } - case let .automaticVoiceDownloadHeader(sectionId, text): - if case .automaticVoiceDownloadHeader(sectionId, text) = rhs { - return true - } else { - return false - } - case let .automaticVoiceDownloadPrivateChats(sectionId, text, value): - if case .automaticVoiceDownloadPrivateChats(sectionId, text, value) = rhs { - return true - } else { - return false - } - case let .automaticVoiceDownloadGroupsAndChannels(sectionId, text, value): - if case .automaticVoiceDownloadGroupsAndChannels(sectionId, text, value) = rhs { - return true - } else { - return false - } - case let .automaticInstantVideoDownloadHeader(sectionId, text): - if case .automaticInstantVideoDownloadHeader(sectionId, text) = rhs { - return true - } else { - return false - } - case let .automaticInstantVideoDownloadPrivateChats(sectionId, text, value): - if case .automaticInstantVideoDownloadPrivateChats(sectionId, text, value) = rhs { - return true - } else { - return false - } - case let .automaticInstantVideoDownloadGroupsAndChannels(sectionId, text, value): - if case .automaticInstantVideoDownloadGroupsAndChannels(sectionId, text, value) = rhs { - return true - } else { - return false - } - case let .voiceCallsHeader(sectionId, text): - if case .voiceCallsHeader(sectionId, text) = rhs { - return true - } else { - return false - } - case let .useLessVoiceData(sectionId, text, value): - if case .useLessVoiceData(sectionId, text, value) = rhs { - return true - } else { - return false - } - case let .otherHeader(sectionId, text): - if case .otherHeader(sectionId, text) = rhs { - return true - } else { - return false - } - case let .saveIncomingPhotos(sectionId, text, value): - if case .saveIncomingPhotos(sectionId, text, value) = rhs { - return true - } else { - return false - } - case let .saveEditedPhotos(sectionId, text, value): - if case .saveEditedPhotos(sectionId, text, value) = rhs { - return true - } else { - return false - } + case let .downloadFolder(sectionId, _, _): + return (sectionId * 1000) + stableId + case let .proxyHeader(sectionId): + return sectionId + case let .proxySettings(sectionId, _, _): + return sectionId case let .sectionId(sectionId): - if case .sectionId(sectionId) = rhs { - return true - } else { - return false - } + return (sectionId + 1) * 1000 - sectionId } } @@ -263,68 +297,77 @@ private enum DataAndStorageEntry: TableItemListNodeEntry { func item(_ arguments: DataAndStorageControllerArguments, initialSize: NSSize) -> TableRowItem { switch self { - case let .storageUsage(_, text): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: text, type: .next, action: { + case let .storageUsage(_, text, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: text, type: .next, viewType: viewType, action: { arguments.openStorageUsage() }) - case let .networkUsage(_, text): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: text, type: .next, action: { + case let .networkUsage(_, text, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: text, type: .next, viewType: viewType, action: { arguments.openNetworkUsage() }) - case let .automaticPhotoDownloadHeader(_, text): - return GeneralTextRowItem(initialSize, stableId: stableId, text: text) - case let .automaticPhotoDownloadPrivateChats(_, text, value): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: text, type: .switchable(stateback: { - return value - }), action: { - arguments.toggleAutomaticDownload(.photo, .privateChats, value) - }) - case let .automaticPhotoDownloadGroupsAndChannels(_, text, value): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: text, type: .switchable(stateback: { - return value - }), action: { - arguments.toggleAutomaticDownload(.photo, .groupsAndChannels, value) + case let .automaticMediaDownloadHeader(_, text, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, viewType: viewType) + case let .automaticDownloadMedia(_ , value, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.dataAndStorageAutomaticDownload, type: .switchable(value), viewType: viewType, action: { + arguments.toggleAutomaticDownload(!value) }) - case let .automaticVoiceDownloadHeader(_, text): - return GeneralTextRowItem(initialSize, stableId: stableId, text: text) - case let .automaticVoiceDownloadPrivateChats(_, text, value): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: text, type: .switchable(stateback: { - return value - }), action: { - arguments.toggleAutomaticDownload(.voice, .privateChats, value) + case let .photos(_, category, enabled, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.dataAndStorageAutomaticDownloadPhoto, description: stringForAutomaticDownloadPeers(peers: AutomaticDownloadPeers(category: category), category: .photo), type: .next, viewType: viewType, action: { + arguments.openCategorySettings(category, L10n.dataAndStorageAutomaticDownloadPhoto) + }, enabled: enabled) + case let .videos(_, category, enabled, _, viewType): + + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.dataAndStorageAutomaticDownloadVideo, description: stringForAutomaticDownloadPeers(peers: AutomaticDownloadPeers(category: category), category: .video), type: .next, viewType: viewType, action: { + arguments.openCategorySettings(category, L10n.dataAndStorageAutomaticDownloadVideo) + }, enabled: enabled) + case let .files(_, category, enabled, _, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.dataAndStorageAutomaticDownloadFiles, description: stringForAutomaticDownloadPeers(peers: AutomaticDownloadPeers(category: category), category: .file), type: .next, viewType: viewType, action: { + arguments.openCategorySettings(category, L10n.dataAndStorageAutomaticDownloadFiles) + }, enabled: enabled) + case let .voice(_, category, enabled, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.dataAndStorageAutomaticDownloadVoice, type: .next, viewType: viewType, action: { + arguments.openCategorySettings(category, L10n.dataAndStorageAutomaticDownloadVoice) + }, enabled: enabled) + case let .instantVideo(_, category, enabled, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.dataAndStorageAutomaticDownloadInstantVideo, type: .next, viewType: viewType, action: { + arguments.openCategorySettings(category, L10n.dataAndStorageAutomaticDownloadInstantVideo) + }, enabled: enabled) + case let .gifs(_, category, enabled, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.dataAndStorageAutomaticDownloadGIFs, type: .next, viewType: viewType, action: { + arguments.openCategorySettings(category, L10n.dataAndStorageAutomaticDownloadGIFs) + }, enabled: enabled) + case let .resetDownloadSettings(_, enabled, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.dataAndStorageAutomaticDownloadReset, nameStyle: ControlStyle(font: .normal(.title), foregroundColor: theme.colors.accent), type: .none, viewType: viewType, action: { + arguments.resetDownloadSettings() + }, enabled: enabled) + case let .downloadFolder(_, path, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.dataAndStorageDownloadFolder, type: .context(path), viewType: viewType, action: { + arguments.selectDownloadFolder() }) - case let .automaticVoiceDownloadGroupsAndChannels(_, text, value): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: text, type: .switchable(stateback: { - return value - }), action: { - arguments.toggleAutomaticDownload(.voice, .groupsAndChannels, value) + case let .autoplayHeader(_, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.dataAndStorageAutoplayHeader, viewType: viewType) + case let .autoplayGifs(_, value, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.dataAndStorageAutoplayGIFs, type: .switchable(value), viewType: viewType, action: { + arguments.toggleAutoplayGifs(!value) }) - case let .automaticInstantVideoDownloadHeader(_, text): - return GeneralTextRowItem(initialSize, stableId: stableId, text: text) - case let .automaticInstantVideoDownloadPrivateChats(_, text, value): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: text, type: .switchable(stateback: { - return value - }), action: { - arguments.toggleAutomaticDownload(.instantVideo, .privateChats, value) + case let .autoplayVideos(_, value, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.dataAndStorageAutoplayVideos, type: .switchable(value), viewType: viewType, action: { + arguments.toggleAutoplayVideos(!value) }) - case let .automaticInstantVideoDownloadGroupsAndChannels(_, text, value): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: text, type: .switchable(stateback: { - return value - }), action: { - arguments.toggleAutomaticDownload(.instantVideo, .groupsAndChannels, value) + case let .soundOnHover(_, value, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.dataAndStorageAutoplaySoundOnHover, type: .switchable(value), viewType: viewType, action: { + arguments.toggleAutoplaySoundOnHover(!value) }) - case let .voiceCallsHeader(_, text): - return GeneralTextRowItem(initialSize, stableId: stableId, text: text) - case let .useLessVoiceData(_, text, value): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: text, type: .context(stateback: { - return value - }), action: { - arguments.openVoiceUseLessData() + case let .soundOnHoverDesc(_, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.dataAndStorageAutoplaySoundOnHoverDesc, viewType: viewType) + case .proxyHeader: + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.privacySettingsProxyHeader, viewType: .textTopItem) + case let .proxySettings(_, text, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.privacySettingsUseProxy, type: .nextContext(text), viewType: viewType, action: { + arguments.openProxySettings() }) - case let .otherHeader(_, text): - return GeneralTextRowItem(initialSize, stableId: stableId, text: text) default: - return GeneralRowItem(initialSize, height: 20, stableId: stableId) + return GeneralRowItem(initialSize, height: 30, stableId: stableId, viewType: .separator) } } } @@ -352,59 +395,67 @@ private struct DataAndStorageData: Equatable { } -private func dataAndStorageControllerEntries(state: DataAndStorageControllerState, data: DataAndStorageData) -> [DataAndStorageEntry] { +private func dataAndStorageControllerEntries(state: DataAndStorageControllerState, data: DataAndStorageData, proxy: ProxySettings, autoplayMedia: AutoplayMediaPreferences) -> [DataAndStorageEntry] { var entries: [DataAndStorageEntry] = [] var sectionId:Int32 = 1 entries.append(.sectionId(sectionId)) sectionId += 1 - entries.append(.storageUsage(sectionId, tr(.dataAndStorageStorageUsage))) - // entries.append(.networkUsage(sectionId, tr(.dataAndStorageNetworkUsage))) + entries.append(.storageUsage(sectionId, L10n.dataAndStorageStorageUsage, viewType: .firstItem)) + entries.append(.networkUsage(sectionId, L10n.dataAndStorageNetworkUsage, viewType: .lastItem)) entries.append(.sectionId(sectionId)) sectionId += 1 - entries.append(.automaticPhotoDownloadHeader(sectionId, tr(.dataAndStorageAutomaticPhotoDownloadHeader))) - // entries.append(.automaticPhotoDownloadPrivateChats(sectionId, tr(.dataAndStorageAutomaticDownloadPrivateChats), data.automaticMediaDownloadSettings.categories.photo.privateChats)) - entries.append(.automaticPhotoDownloadGroupsAndChannels(sectionId, tr(.dataAndStorageAutomaticDownloadGroupsChannels), data.automaticMediaDownloadSettings.categories.photo.groupsAndChannels)) + entries.append(.automaticMediaDownloadHeader(sectionId, L10n.dataAndStorageAutomaticDownloadHeader, viewType: .textTopItem)) + entries.append(.automaticDownloadMedia(sectionId, data.automaticMediaDownloadSettings.automaticDownload, viewType: .firstItem)) + entries.append(.photos(sectionId, data.automaticMediaDownloadSettings.categories.photo, data.automaticMediaDownloadSettings.automaticDownload, viewType: .innerItem)) + entries.append(.videos(sectionId, data.automaticMediaDownloadSettings.categories.video, data.automaticMediaDownloadSettings.automaticDownload, data.automaticMediaDownloadSettings.categories.video.fileSize, viewType: .innerItem)) + entries.append(.files(sectionId, data.automaticMediaDownloadSettings.categories.files, data.automaticMediaDownloadSettings.automaticDownload, data.automaticMediaDownloadSettings.categories.files.fileSize, viewType: .innerItem)) + entries.append(.resetDownloadSettings(sectionId, data.automaticMediaDownloadSettings != AutomaticMediaDownloadSettings.defaultSettings, viewType: .lastItem)) entries.append(.sectionId(sectionId)) sectionId += 1 - entries.append(.automaticVoiceDownloadHeader(sectionId, tr(.dataAndStorageAutomaticAudioDownloadHeader))) - // entries.append(.automaticVoiceDownloadPrivateChats(sectionId, tr(.dataAndStorageAutomaticDownloadPrivateChats), data.automaticMediaDownloadSettings.categories.voice.privateChats)) - entries.append(.automaticVoiceDownloadGroupsAndChannels(sectionId, tr(.dataAndStorageAutomaticDownloadGroupsChannels), data.automaticMediaDownloadSettings.categories.voice.groupsAndChannels)) + entries.append(.downloadFolder(sectionId, data.automaticMediaDownloadSettings.downloadFolder, viewType: .singleItem)) + entries.append(.sectionId(sectionId)) sectionId += 1 - entries.append(.automaticInstantVideoDownloadHeader(sectionId, tr(.dataAndStorageAutomaticVideoDownloadHeader))) - // entries.append(.automaticInstantVideoDownloadPrivateChats(sectionId, tr(.dataAndStorageAutomaticDownloadPrivateChats), data.automaticMediaDownloadSettings.categories.instantVideo.privateChats)) - entries.append(.automaticInstantVideoDownloadGroupsAndChannels(sectionId, tr(.dataAndStorageAutomaticDownloadGroupsChannels), data.automaticMediaDownloadSettings.categories.instantVideo.groupsAndChannels)) - // entries.append(.sectionId(sectionId)) - // sectionId += 1 + entries.append(.autoplayHeader(sectionId, viewType: .textTopItem)) + entries.append(.autoplayGifs(sectionId, autoplayMedia.gifs, viewType: .firstItem)) + entries.append(.autoplayVideos(sectionId, autoplayMedia.videos, viewType: .lastItem)) + + + entries.append(.sectionId(sectionId)) + sectionId += 1 + + + entries.append(.proxyHeader(sectionId)) + let text: String + if let active = proxy.activeServer, proxy.enabled { + switch active.connection { + case .socks5: + text = L10n.proxySettingsSocks5 + case .mtp: + text = L10n.proxySettingsMTP + } + } else { + text = L10n.proxySettingsDisabled + } + entries.append(.proxySettings(sectionId, text, viewType: .singleItem)) - // entries.append(.voiceCallsHeader(sectionId, tr(.dataAndStorageVoiceCallsHeader))) - // entries.append(.useLessVoiceData(sectionId, tr(.dataAndStorageVoiceCallsLessData), stringForUseLessDataSetting(data.voiceCallSettings))) - + entries.append(.sectionId(sectionId)) + sectionId += 1 return entries } -private func stringForUseLessDataSetting(_ settings: VoiceCallSettings) -> String { - switch settings.dataSaving { - case .never: - return "Never" - case .cellular: - return "On Mobile Network" - case .always: - return "Always" - } -} private func prepareTransition(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], initialSize: NSSize, arguments: DataAndStorageControllerArguments) -> TableUpdateTransition { let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in @@ -414,13 +465,18 @@ private func prepareTransition(left:[AppearanceWrapperEntry } class DataAndStorageViewController: TableViewController { - + private let disposable = MetaDisposable() + private var focusOnItemTag: DataAndStorageEntryTag? + init(_ context: AccountContext, focusOnItemTag: DataAndStorageEntryTag? = nil) { + self.focusOnItemTag = focusOnItemTag + super.init(context) + } + override func viewDidLoad() { super.viewDidLoad() - readyOnce() - let account = self.account + let context = self.context let initialState = DataAndStorageControllerState() let initialSize = self.atomicSize let statePromise = ValuePromise(initialState, ignoreRepeated: true) @@ -429,7 +485,7 @@ class DataAndStorageViewController: TableViewController { statePromise.set(stateValue.modify { f($0) }) } - let pushControllerImpl = { [weak self] controller in + let pushControllerImpl:(ViewController)->Void = { [weak self] controller in self?.navigationController?.push(controller) } @@ -437,91 +493,115 @@ class DataAndStorageViewController: TableViewController { let actionsDisposable = DisposableSet() let dataAndStorageDataPromise = Promise() - dataAndStorageDataPromise.set(account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.automaticMediaDownloadSettings, ApplicationSpecificPreferencesKeys.generatedMediaStoreSettings, ApplicationSpecificPreferencesKeys.voiceCallSettings]) - |> map { view -> DataAndStorageData in - let automaticMediaDownloadSettings: AutomaticMediaDownloadSettings - if let value = view.values[ApplicationSpecificPreferencesKeys.automaticMediaDownloadSettings] as? AutomaticMediaDownloadSettings { - automaticMediaDownloadSettings = value - } else { - automaticMediaDownloadSettings = AutomaticMediaDownloadSettings.defaultSettings - } + dataAndStorageDataPromise.set(combineLatest(context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.automaticMediaDownloadSettings, ApplicationSpecificPreferencesKeys.generatedMediaStoreSettings]), voiceCallSettings(context.sharedContext.accountManager)) + |> map { view, voiceCallSettings -> DataAndStorageData in + let automaticMediaDownloadSettings: AutomaticMediaDownloadSettings = view.values[ApplicationSpecificPreferencesKeys.automaticMediaDownloadSettings] as? AutomaticMediaDownloadSettings ?? AutomaticMediaDownloadSettings.defaultSettings + - let generatedMediaStoreSettings: GeneratedMediaStoreSettings - if let value = view.values[ApplicationSpecificPreferencesKeys.generatedMediaStoreSettings] as? GeneratedMediaStoreSettings { - generatedMediaStoreSettings = value - } else { - generatedMediaStoreSettings = GeneratedMediaStoreSettings.defaultSettings - } + let generatedMediaStoreSettings: GeneratedMediaStoreSettings = view.values[ApplicationSpecificPreferencesKeys.generatedMediaStoreSettings] as? GeneratedMediaStoreSettings ?? GeneratedMediaStoreSettings.defaultSettings - let voiceCallSettings: VoiceCallSettings - if let value = view.values[ApplicationSpecificPreferencesKeys.voiceCallSettings] as? VoiceCallSettings { - voiceCallSettings = value - } else { - voiceCallSettings = VoiceCallSettings.defaultSettings - } return DataAndStorageData(automaticMediaDownloadSettings: automaticMediaDownloadSettings, generatedMediaStoreSettings: generatedMediaStoreSettings, voiceCallSettings: voiceCallSettings) }) - let arguments = DataAndStorageControllerArguments(openStorageUsage: { [weak self] in - pushControllerImpl(StorageUsageController(account)) + let arguments = DataAndStorageControllerArguments(openStorageUsage: { + pushControllerImpl(StorageUsageController(context)) }, openNetworkUsage: { - // pushControllerImpl?(networkUsageStatsController(account: account)) - }, toggleAutomaticDownload: { category, peers, value in - let _ = updateMediaDownloadSettingsInteractively(postbox: account.postbox, { current in - switch category { - case .photo: - switch peers { - case .privateChats: - return current.withUpdatedCategories(current.categories.withUpdatedPhoto(current.categories.photo.withUpdatedPrivateChats(value))) - case .groupsAndChannels: - return current.withUpdatedCategories(current.categories.withUpdatedPhoto(current.categories.photo.withUpdatedGroupsAndChannels(value))) - } - case .voice: - switch peers { - case .privateChats: - return current.withUpdatedCategories(current.categories.withUpdatedVoice(current.categories.voice.withUpdatedPrivateChats(value))) - case .groupsAndChannels: - return current.withUpdatedCategories(current.categories.withUpdatedVoice(current.categories.voice.withUpdatedGroupsAndChannels(value))) - } - case .instantVideo: - switch peers { - case .privateChats: - return current.withUpdatedCategories(current.categories.withUpdatedInstantVideo(current.categories.instantVideo.withUpdatedPrivateChats(value))) - case .groupsAndChannels: - return current.withUpdatedCategories(current.categories.withUpdatedInstantVideo(current.categories.instantVideo.withUpdatedGroupsAndChannels(value))) - } - case .gif: - switch peers { - case .privateChats: - return current.withUpdatedCategories(current.categories.withUpdatedGif(current.categories.gif.withUpdatedPrivateChats(value))) - case .groupsAndChannels: - return current.withUpdatedCategories(current.categories.withUpdatedGif(current.categories.gif.withUpdatedGroupsAndChannels(value))) + pushControllerImpl(networkUsageStatsController(context: context)) + }, openCategorySettings: { category, title in + pushControllerImpl(DownloadSettingsViewController(context, category, title, updateCategory: { category in + _ = updateMediaDownloadSettingsInteractively(postbox: context.account.postbox, { current -> AutomaticMediaDownloadSettings in + switch title { + case L10n.dataAndStorageAutomaticDownloadPhoto: + return current.withUpdatedCategories(current.categories.withUpdatedPhoto(category)) + case L10n.dataAndStorageAutomaticDownloadVideo: + return current.withUpdatedCategories(current.categories.withUpdatedVideo(category)) + case L10n.dataAndStorageAutomaticDownloadFiles: + return current.withUpdatedCategories(current.categories.withUpdatedFiles(category)) + case L10n.dataAndStorageAutomaticDownloadVoice: + return current.withUpdatedCategories(current.categories.withUpdatedVoice(category)) + case L10n.dataAndStorageAutomaticDownloadInstantVideo: + return current.withUpdatedCategories(current.categories.withUpdatedInstantVideo(category)) + case L10n.dataAndStorageAutomaticDownloadGIFs: + return current.withUpdatedCategories(current.categories.withUpdatedGif(category)) + default: + return current } - } + }).start() + })) + }, toggleAutomaticDownload: { enabled in + _ = updateMediaDownloadSettingsInteractively(postbox: context.account.postbox, { current -> AutomaticMediaDownloadSettings in + return current.withUpdatedAutomaticDownload(enabled) + }).start() + }, resetDownloadSettings: { + _ = (confirmSignal(for: mainWindow, header: appName, information: L10n.dataAndStorageConfirmResetSettings, okTitle: L10n.modalOK, cancelTitle: L10n.modalCancel) |> filter {$0} |> mapToSignal { _ -> Signal in + return updateMediaDownloadSettingsInteractively(postbox: context.account.postbox, { _ -> AutomaticMediaDownloadSettings in + return AutomaticMediaDownloadSettings.defaultSettings + }) + }).start() + }, selectDownloadFolder: { + selectFolder(for: mainWindow, completion: { newPath in + _ = updateMediaDownloadSettingsInteractively(postbox: context.account.postbox, { current -> AutomaticMediaDownloadSettings in + return current.withUpdatedDownloadFolder(newPath) + }).start() + }) + + }, toggleAutoplayGifs: { enable in + _ = updateAutoplayMediaSettingsInteractively(postbox: context.account.postbox, { + return $0.withUpdatedAutoplayGifs(enable) }).start() - }, openVoiceUseLessData: { - // pushControllerImpl?(voiceCallDataSavingController(account: account)) - }, toggleSaveIncomingPhotos: { value in - let _ = updateMediaDownloadSettingsInteractively(postbox: account.postbox, { current in - return current.withUpdatedSaveIncomingPhotos(value) + }, toggleAutoplayVideos: { enable in + _ = updateAutoplayMediaSettingsInteractively(postbox: context.account.postbox, { + return $0.withUpdatedAutoplayVideos(enable) }).start() - }, toggleSaveEditedPhotos: { value in - let _ = updateGeneratedMediaStoreSettingsInteractively(postbox: account.postbox, { current in - return current.withUpdatedStoreEditedPhotos(value) + }, toggleAutoplaySoundOnHover: { enable in + _ = updateAutoplayMediaSettingsInteractively(postbox: context.account.postbox, { + return $0.withUpdatedAutoplaySoundOnHover(enable) }).start() + }, openProxySettings: { + let controller = proxyListController(accountManager: context.sharedContext.accountManager, network: context.account.network, share: { servers in + var message: String = "" + for server in servers { + message += server.link + "\n\n" + } + message = message.trimmed + + showModal(with: ShareModalController(ShareLinkObject(context, link: message)), for: mainWindow) + }, pushController: { controller in + pushControllerImpl(controller) + }) + pushControllerImpl(controller) }) - self.genericView.merge(with: combineLatest(statePromise.get(), dataAndStorageDataPromise.get(), appearanceSignal) |> deliverOnMainQueue - |> map { state, dataAndStorageData, appearance -> TableUpdateTransition in - - let entries = dataAndStorageControllerEntries(state: state, data: dataAndStorageData).map {AppearanceWrapperEntry(entry: $0, appearance: appearance)} - return prepareTransition(left: previous.swap(entries), right: entries, initialSize: initialSize.modify({$0}), arguments: arguments) + let proxy:Signal = proxySettings(accountManager: context.sharedContext.accountManager) - } |> afterDisposed { - actionsDisposable.dispose() - }) + + let signal = combineLatest(queue: .mainQueue(), statePromise.get(), dataAndStorageDataPromise.get(), appearanceSignal, proxy, autoplayMediaSettings(postbox: context.account.postbox)) + |> map { state, dataAndStorageData, appearance, proxy, autoplayMediaSettings -> TableUpdateTransition in + let entries = dataAndStorageControllerEntries(state: state, data: dataAndStorageData, proxy: proxy, autoplayMedia: autoplayMediaSettings).map {AppearanceWrapperEntry(entry: $0, appearance: appearance)} + return prepareTransition(left: previous.swap(entries), right: entries, initialSize: initialSize.modify({$0}), arguments: arguments) + } |> beforeNext { [weak self] _ in + self?.readyOnce() + } |> afterDisposed { + actionsDisposable.dispose() + } |> deliverOnMainQueue + + + + self.disposable.set(signal.start(next: { [weak self] transition in + self?.genericView.merge(with: transition) + self?.readyOnce() + if let focusOnItemTag = self?.focusOnItemTag { + self?.genericView.scroll(to: .center(id: focusOnItemTag.stableId, innerId: nil, animated: true, focus: .init(focus: true), inset: 0), inset: NSEdgeInsets()) + self?.focusOnItemTag = nil + } + })) + + } + + deinit { + disposable.dispose() } override func getRightBarViewOnce() -> BarView { diff --git a/Telegram-Mac/DataItem.swift b/Telegram-Mac/DataItem.swift new file mode 100644 index 0000000000..51498bbace --- /dev/null +++ b/Telegram-Mac/DataItem.swift @@ -0,0 +1,32 @@ +// +// DataItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 28/11/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Foundation +import OpusBinding + +final class DataItem : TGDataItem { + let path: String + init(path: String) { + self.path = path + } + override func appendData(_ data: Data!) { + do { + if !FileManager.default.fileExists(atPath: self.path) { + FileManager.default.createFile(atPath: self.path, contents: self.data(), attributes: nil) + } + let fileManager = try FileHandle(forWritingTo: URL(fileURLWithPath: self.path)) + fileManager.seekToEndOfFile() + fileManager.write(data) + fileManager.synchronizeFile() + fileManager.closeFile() + } catch { + + } + super.appendData(data) + } +} diff --git a/Telegram-Mac/DatePickerRowItem.swift b/Telegram-Mac/DatePickerRowItem.swift new file mode 100644 index 0000000000..919ac66aff --- /dev/null +++ b/Telegram-Mac/DatePickerRowItem.swift @@ -0,0 +1,190 @@ +// +// DatePickerRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 06.04.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit + + + +private final class DateSelectorView : View { + fileprivate let dayPicker: DatePicker + private let atView = TextView() + fileprivate let timePicker: TimePicker + fileprivate let containerView = View() + + var update:((Date)->Void)? + + required init(frame frameRect: NSRect) { + self.dayPicker = DatePicker(selected: DatePickerOption(name: DateSelectorUtil.formatDay(Date()), value: Date())) + self.timePicker = TimePicker(selected: TimePickerOption(hours: 0, minutes: 0, seconds: 0)) + super.init(frame: frameRect) + containerView.addSubview(dayPicker) + containerView.addSubview(timePicker) + containerView.addSubview(atView) + addSubview(containerView) + + let atLayout = TextViewLayout(.initialize(string: L10n.scheduleControllerAt, color: GroupCallTheme.customTheme.textColor, font: .normal(.title)), alwaysStaticItems: true) + atLayout.measure(width: .greatestFiniteMagnitude) + atView.update(atLayout) + + self.dayPicker.set(handler: { [weak self] control in + if let control = control as? DatePicker, let window = self?.kitWindow, !hasPopover(window) { + let calendar = CalendarController(NSMakeRect(0, 0, 250, 250), window, current: control.selected.value, onlyFuture: true, limitedBy: Date(timeIntervalSinceNow: 7 * 24 * 60 * 60), selectHandler: { [weak self] date in + self?.applyDay(date) + if let date = self?.select() { + self?.update?(date) + } + }) + showPopover(for: control, with: calendar, edge: .maxY, inset: NSMakePoint(-8, -60)) + } + }, for: .Down) + + + let date = Date() + + var t: time_t = time_t(date.timeIntervalSince1970) + var timeinfo: tm = tm() + localtime_r(&t, &timeinfo) + + + + self.timePicker.update = { [weak self] updated in + guard let `self` = self else { + return false + } + + let day = self.dayPicker.selected.value + + let date = day.startOfDay.addingTimeInterval(updated.interval) + + self.applyTime(date) + self.update?(self.select()) + return true + } + + } + + private func applyDay(_ date: Date) { + self.dayPicker.selected = DatePickerOption(name: DateSelectorUtil.formatDay(date), value: date) + let current = date.addingTimeInterval(self.timePicker.selected.interval) + + if CalendarUtils.isSameDate(Date(), date: date, checkDay: true) { + if current < Date() { + for interval in DateSelectorUtil.timeIntervals.compactMap ({$0}) { + let new = date.startOfDay.addingTimeInterval(interval) + if new > Date() { + applyTime(new) + break + } + } + } else { + if date != current { + applyTime(date.addingTimeInterval(current.timeIntervalSince1970 - current.startOfDay.timeIntervalSince1970)) + } else { + applyTime(date) + } + } + } else { + if date != current { + applyTime(date.addingTimeInterval(current.timeIntervalSince1970 - current.startOfDay.timeIntervalSince1970)) + } else { + applyTime(date) + } + } + } + + private func applyTime(_ date: Date) { + + var t: time_t = time_t(date.timeIntervalSince1970) + var timeinfo: tm = tm() + localtime_r(&t, &timeinfo) + + timePicker.selected = TimePickerOption(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, seconds: timeinfo.tm_sec) + } + + func updateDate(_ date: Date) { + self.dayPicker.selected = DatePickerOption(name: DateSelectorUtil.formatDay(date), value: date) + self.timePicker.selected = TimePickerOption(hours: 0, minutes: 0, seconds: 0) + self.applyDay(date) + } + + override func layout() { + super.layout() + + self.dayPicker.setFrameSize(NSMakeSize(124, 30)) + self.timePicker.setFrameSize(NSMakeSize(124, 30)) + let fullWidth = dayPicker.frame.width + 15 + atView.frame.width + 15 + timePicker.frame.width + self.containerView.setFrameSize(NSMakeSize(fullWidth, max(dayPicker.frame.height, timePicker.frame.height))) + self.dayPicker.centerY(x: 0) + self.atView.centerY(x: self.dayPicker.frame.maxX + 15) + self.timePicker.centerY(x: self.atView.frame.maxX + 15) + self.containerView.center() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func select() -> Date { + let day = self.dayPicker.selected.value + let date = day.startOfDay.addingTimeInterval(self.timePicker.selected.interval) + return date + } +} + + +final class DatePickerRowItem : GeneralRowItem { + fileprivate let initialDate: Date? + fileprivate let update:(Date)->Void + init(_ initialSize: NSSize, stableId: AnyHashable, viewType: GeneralViewType, initialDate: Date?, update:@escaping(Date)->Void) { + self.initialDate = initialDate + self.update = update + super.init(initialSize, height: 48, stableId: stableId, viewType: viewType) + } + + + + override func viewClass() -> AnyClass { + return DatePickerRowView.self + } +} + + +private final class DatePickerRowView : GeneralContainableRowView { + private let view = DateSelectorView(frame: .zero) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(view) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func shakeView() { + super.shakeView() + self.view.shake() + } + + + override func layout() { + super.layout() + + view.frame = containerView.focus(NSMakeSize(containerView.frame.width, 30)) + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? DatePickerRowItem else { + return + } + view.updateDate(item.initialDate ?? Date(timeIntervalSinceNow: 1 * 60 * 60)) + view.update = item.update + } +} diff --git a/Telegram-Mac/DateSelectorModalController.swift b/Telegram-Mac/DateSelectorModalController.swift new file mode 100644 index 0000000000..d4d47db094 --- /dev/null +++ b/Telegram-Mac/DateSelectorModalController.swift @@ -0,0 +1,403 @@ +// +// DateSelectorModalController.swift +// Telegram +// +// Created by Mikhail Filimonov on 07/08/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import TGUIKit +import TelegramCore +import SwiftSignalKit + +import Postbox + + +final class DateSelectorModalView : View { + fileprivate let dayPicker: DatePicker + private let atView = TextView() + fileprivate let timePicker: TimePicker + private let containerView = View() + fileprivate let sendOn = TitleButton() + fileprivate let sendWhenOnline = TitleButton() + required init(frame frameRect: NSRect) { + + self.dayPicker = DatePicker(selected: DatePickerOption(name: DateSelectorUtil.formatDay(Date()), value: Date())) + self.timePicker = TimePicker(selected: TimePickerOption(hours: 0, minutes: 0, seconds: 0)) + super.init(frame: frameRect) + containerView.addSubview(self.dayPicker) + containerView.addSubview(self.atView) + containerView.addSubview(self.timePicker) + self.addSubview(self.containerView) + self.addSubview(sendOn) + self.atView.userInteractionEnabled = false + self.atView.isSelectable = false + self.sendOn.layer?.cornerRadius = .cornerRadius + self.sendOn.disableActions() + self.addSubview(self.sendWhenOnline) + self.updateLocalizationAndTheme(theme: theme) + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + + self.sendOn.set(font: .medium(.text), for: .Normal) + self.sendOn.set(color: .white, for: .Normal) + self.sendOn.set(background: theme.colors.accent, for: .Normal) + self.sendOn.set(background: theme.colors.accent.highlighted, for: .Highlight) + + self.sendWhenOnline.set(font: .normal(.text), for: .Normal) + self.sendWhenOnline.set(color: theme.colors.accent, for: .Normal) + self.sendWhenOnline.set(text: L10n.scheduleSendWhenOnline, for: .Normal) + _ = self.sendWhenOnline.sizeToFit() + + let atLayout = TextViewLayout(.initialize(string: L10n.scheduleControllerAt, color: theme.colors.text, font: .normal(.title)), alwaysStaticItems: true) + atLayout.measure(width: .greatestFiniteMagnitude) + atView.update(atLayout) + + needsLayout = true + } + + private var mode: DateSelectorModalController.Mode? + + func updateWithMode(_ mode: DateSelectorModalController.Mode, sendWhenOnline: Bool) { + self.mode = mode + self.sendWhenOnline.isHidden = !sendWhenOnline + switch mode { + case .date: + self.atView.isHidden = true + self.sendOn.isHidden = true + self.sendWhenOnline.isHidden = true + case .schedule: + self.atView.isHidden = false + self.sendOn.isHidden = false + self.sendWhenOnline.isHidden = !sendWhenOnline + } + needsLayout = true + } + + + override func layout() { + super.layout() + + if let mode = mode { + switch mode { + case .date: + self.dayPicker.setFrameSize(NSMakeSize(130, 30)) + self.timePicker.setFrameSize(NSMakeSize(130, 30)) + + let fullWidth = dayPicker.frame.width + 15 + timePicker.frame.width + self.containerView.setFrameSize(NSMakeSize(fullWidth, max(dayPicker.frame.height, timePicker.frame.height))) + self.dayPicker.centerY(x: 0) + self.timePicker.centerY(x: dayPicker.frame.maxX + 15) + self.containerView.centerX(y: 30) + case .schedule: + self.dayPicker.setFrameSize(NSMakeSize(115, 30)) + self.timePicker.setFrameSize(NSMakeSize(115, 30)) + let fullWidth = dayPicker.frame.width + 15 + atView.frame.width + 15 + timePicker.frame.width + self.containerView.setFrameSize(NSMakeSize(fullWidth, max(dayPicker.frame.height, timePicker.frame.height))) + self.dayPicker.centerY(x: 0) + self.atView.centerY(x: self.dayPicker.frame.maxX + 15) + self.timePicker.centerY(x: self.atView.frame.maxX + 15) + self.containerView.centerX(y: 30) + _ = self.sendOn.sizeToFit(NSZeroSize, NSMakeSize(fullWidth, 30), thatFit: true) + self.sendOn.centerX(y: containerView.frame.maxY + 30) + self.sendWhenOnline.centerX(y: self.sendOn.frame.maxY + 15) + } + } + + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + + +extension TimePickerOption { + var interval: TimeInterval { + let hours = Double(self.hours) * 60.0 * 60 + let minutes = Double(self.minutes) * 60.0 + let seconds = Double(self.seconds) + return hours + minutes + seconds + } +} +class DateSelectorModalController: ModalViewController { + + enum Mode { + case schedule(PeerId) + case date(title: String, doneTitle: String) + } + + private let context: AccountContext + private let selectedAt: (Date)->Void + private let defaultDate: Date? + private var sendWhenOnline: Bool = false + fileprivate let mode: Mode + private let disposable = MetaDisposable() + init(context: AccountContext, defaultDate: Date? = nil, mode: Mode, selectedAt:@escaping(Date)->Void) { + self.context = context + self.defaultDate = defaultDate + self.selectedAt = selectedAt + self.mode = mode + switch mode { + case .schedule: + super.init(frame: NSMakeRect(0, 0, 350, 200)) + case .date: + super.init(frame: NSMakeRect(0, 0, 350, 90)) + } + self.bar = .init(height: 0) + } + + override func viewClass() -> AnyClass { + return DateSelectorModalView.self + } + + override var modalHeader: (left: ModalHeaderData?, center: ModalHeaderData?, right: ModalHeaderData?)? { + let title: String + switch mode { + case .schedule: + title = L10n.scheduleControllerTitle + case let .date(value, _): + title = value + } + return (left: ModalHeaderData(title: nil, image: theme.icons.modalClose, handler: { [weak self] in + self?.close() + }), center: ModalHeaderData(title: title, handler: { + + }), right: nil) + } + + + + override open func measure(size: NSSize) { + var height: CGFloat = 0 + switch mode { + case .date: + height = 90 + case .schedule: + height = sendWhenOnline ? 170 : 150 + } + + self.modal?.resize(with:NSMakeSize(frame.width, height), animated: false) + } + + override var dynamicSize: Bool { + return true + } + + var genericView: DateSelectorModalView { + return self.view as! DateSelectorModalView + } + + private func applyDay(_ date: Date) { + genericView.dayPicker.selected = DatePickerOption(name: DateSelectorUtil.formatDay(date), value: date) + let current = date.addingTimeInterval(self.genericView.timePicker.selected.interval) + + if CalendarUtils.isSameDate(Date(), date: date, checkDay: true) { + if current < Date() { + for interval in DateSelectorUtil.timeIntervals.compactMap ({$0}) { + let new = date.startOfDay.addingTimeInterval(interval) + if new > Date() { + applyTime(new) + break + } + } + } else { + if date != current { + applyTime(date.addingTimeInterval(current.timeIntervalSince1970 - current.startOfDay.timeIntervalSince1970)) + } else { + applyTime(date) + } + } + } else { + if date != current { + applyTime(date.addingTimeInterval(current.timeIntervalSince1970 - current.startOfDay.timeIntervalSince1970)) + } else { + applyTime(date) + } + } + } + + private func applyTime(_ date: Date) { + + + var t: time_t = time_t(date.timeIntervalSince1970) + var timeinfo: tm = tm() + localtime_r(&t, &timeinfo) + + genericView.timePicker.selected = TimePickerOption(hours: timeinfo.tm_hour, minutes: timeinfo.tm_min, seconds: timeinfo.tm_sec) + + if CalendarUtils.isSameDate(Date(), date: date, checkDay: true) { + genericView.sendOn.set(text: L10n.scheduleSendToday(DateSelectorUtil.formatTime(date)), for: .Normal) + } else { + genericView.sendOn.set(text: L10n.scheduleSendDate(DateSelectorUtil.formatDay(date), DateSelectorUtil.formatTime(date)), for: .Normal) + } + } + + override var handleAllEvents: Bool { + return true + } + + private func select() { + let day = self.genericView.dayPicker.selected.value + let date = day.startOfDay.addingTimeInterval(self.genericView.timePicker.selected.interval) + if CalendarUtils.isSameDate(Date(), date: day, checkDay: true) { + if Date() > date { + genericView.timePicker.shake() + return + } + } + self.selectedAt(date) + self.close() + } + + override func returnKeyAction() -> KeyHandlerResult { + self.select() + return .invoked + } + + override func firstResponder() -> NSResponder? { + return genericView.timePicker.firstResponder + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + window?.set(handler: { _ -> KeyHandlerResult in + + return .invokeNext + }, with: self, for: .Tab, priority: .modal) + window?.set(handler: { _ -> KeyHandlerResult in + + return .invokeNext + }, with: self, for: .LeftArrow, priority: .modal) + window?.set(handler: { _ -> KeyHandlerResult in + + return .invokeNext + }, with: self, for: .RightArrow, priority: .modal) + } + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + window?.removeAllHandlers(for: self) + } + + override func viewDidLoad() { + super.viewDidLoad() + + let context = self.context + + switch mode { + case let .schedule(peerId): + let presence = context.account.postbox.transaction { + $0.getPeerPresence(peerId: peerId) as? TelegramUserPresence + } |> deliverOnMainQueue + + disposable.set(presence.start(next: { [weak self] presence in + var sendWhenOnline: Bool = false + if let presence = presence { + switch presence.status { + case .present: + sendWhenOnline = peerId != context.peerId + default: + break + } + } + self?.sendWhenOnline = sendWhenOnline + self?.initialize() + })) + case .date: + initialize() + } + } + + override var modalInteractions: ModalInteractions? { + switch mode { + case .schedule: + return nil + case let .date(_, doneTitle): + return ModalInteractions(acceptTitle: doneTitle, accept: { [weak self] in + self?.select() + }, drawBorder: true, height: 50, singleButton: true) + } + } + + private func initialize() { + let date = self.defaultDate ?? Date() + + var t: time_t = time_t(date.timeIntervalSince1970) + var timeinfo: tm = tm() + localtime_r(&t, &timeinfo) + + self.genericView.dayPicker.selected = DatePickerOption(name: DateSelectorUtil.formatDay(date), value: date) + self.genericView.timePicker.selected = TimePickerOption(hours: 0, minutes: 0, seconds: 0) + + self.genericView.updateWithMode(self.mode, sendWhenOnline: self.sendWhenOnline) + + self.applyDay(date) + + self.genericView.timePicker.update = { [weak self] updated in + guard let `self` = self else { + return false + } + + let day = self.genericView.dayPicker.selected.value + + let date = day.startOfDay.addingTimeInterval(updated.interval) + + self.applyTime(date) + return true + } + + self.genericView.sendOn.set(handler: { [weak self] _ in + self?.select() + }, for: .Click) + + self.genericView.sendWhenOnline.set(handler: { [weak self] _ in + self?.selectedAt(Date(timeIntervalSince1970: TimeInterval(scheduleWhenOnlineTimestamp))) + self?.close() + }, for: .Click) + + self.readyOnce() + + self.genericView.dayPicker.set(handler: { [weak self] control in + if let control = control as? DatePicker, let window = self?.window, !hasPopover(window) { + let calendar = CalendarController(NSMakeRect(0, 0, 250, 250), window, current: control.selected.value, onlyFuture: true, selectHandler: { [weak self] date in + self?.applyDay(date) + }) + showPopover(for: control, with: calendar, edge: .maxY, inset: NSMakePoint(-8, -60)) + } + }, for: .Down) + + self.genericView.timePicker.set(handler: { [weak self] control in + if let control = control as? DatePicker, let `self` = self, let window = self.window, !hasPopover(window) { + var items:[SPopoverItem] = [] + + let day = self.genericView.dayPicker.selected.value + + for interval in DateSelectorUtil.timeIntervals { + if let interval = interval { + let date = day.startOfDay.addingTimeInterval(interval) + if CalendarUtils.isSameDate(Date(), date: day, checkDay: true) { + if Date() > date { + continue + } + } + items.append(SPopoverItem(DateSelectorUtil.formatTime(date), { [weak self] in + self?.applyTime(date) + }, height: 30)) + } else if !items.isEmpty { + items.append(SPopoverItem()) + } + } + showPopover(for: control, with: SPopoverViewController(items: items, visibility: 6), edge: .maxY, inset: NSMakePoint(0, -50)) + } + + }, for: .Down) + } + + deinit { + disposable.dispose() + } +} diff --git a/Telegram-Mac/DateUtils.h b/Telegram-Mac/DateUtils.h index a8c1af32f9..0f7750f881 100644 --- a/Telegram-Mac/DateUtils.h +++ b/Telegram-Mac/DateUtils.h @@ -24,6 +24,7 @@ + (void)setDateLocalizationFunc:(NSString* (^)(NSString *key))localizationF; @end +NSString * NSLocalized(NSString * key, NSString *comment); #ifdef __cplusplus diff --git a/Telegram-Mac/DateUtils.mm b/Telegram-Mac/DateUtils.mm index f581db227d..642d26dd7d 100644 --- a/Telegram-Mac/DateUtils.mm +++ b/Telegram-Mac/DateUtils.mm @@ -1,5 +1,5 @@ #include "DateUtils.h" -#include +//#include static time_t midnightOnDay(time_t t) { diff --git a/Telegram-Mac/DeclareEncodables.swift b/Telegram-Mac/DeclareEncodables.swift index b2ff670f45..7e88490705 100644 --- a/Telegram-Mac/DeclareEncodables.swift +++ b/Telegram-Mac/DeclareEncodables.swift @@ -7,17 +7,53 @@ // import Cocoa -import PostboxMac +import Postbox private var telegramUIDeclaredEncodables: Void = { - declareEncodable(ChatInterfaceState.self, f: { ChatInterfaceState(decoder: $0) }) declareEncodable(InAppNotificationSettings.self, f: { InAppNotificationSettings(decoder: $0) }) declareEncodable(BaseApplicationSettings.self, f: { BaseApplicationSettings(decoder: $0) }) - declareEncodable(ThemePalleteSettings.self, f: { ThemePalleteSettings(decoder: $0) }) + declareEncodable(ThemePaletteSettings.self, f: { ThemePaletteSettings(decoder: $0) }) declareEncodable(LocalFileGifMediaResource.self, f: { LocalFileGifMediaResource(decoder: $0) }) + declareEncodable(LottieSoundMediaResource.self, f: { LottieSoundMediaResource(decoder: $0) }) + declareEncodable(LocalFileVideoMediaResource.self, f: { LocalFileVideoMediaResource(decoder: $0) }) + declareEncodable(LocalFileArchiveMediaResource.self, f: { LocalFileArchiveMediaResource(decoder: $0) }) declareEncodable(RecentUsedEmoji.self, f: { RecentUsedEmoji(decoder: $0) }) declareEncodable(InstantViewAppearance.self, f: { InstantViewAppearance(decoder: $0) }) declareEncodable(IVReadState.self, f: { IVReadState(decoder: $0) }) + declareEncodable(AdditionalSettings.self, f: { AdditionalSettings(decoder: $0) }) + declareEncodable(AutomaticMediaDownloadCategoryPeers.self, f: { AutomaticMediaDownloadCategoryPeers(decoder: $0) }) + declareEncodable(AutomaticMediaDownloadCategories.self, f: { AutomaticMediaDownloadCategories(decoder: $0) }) + declareEncodable(AutomaticMediaDownloadSettings.self, f: { AutomaticMediaDownloadSettings(decoder: $0) }) + declareEncodable(ReadArticle.self, f: { ReadArticle(decoder: $0) }) + declareEncodable(ReadArticlesListPreferences.self, f: { ReadArticlesListPreferences(decoder: $0) }) + declareEncodable(AutoNightThemePreferences.self, f: { AutoNightThemePreferences(decoder: $0) }) + declareEncodable(StickerSettings.self, f: { StickerSettings(decoder: $0) }) + declareEncodable(EmojiSkinModifier.self, f: { AutoNightThemePreferences(decoder: $0) }) + declareEncodable(InstantPageStoredDetailsState.self, f: { InstantPageStoredDetailsState(decoder: $0) }) + declareEncodable(CachedChannelAdminRanks.self, f: { CachedChannelAdminRanks(decoder: $0) }) + declareEncodable(CachedChannelAdminRank.self, f: { CachedChannelAdminRank(decoder: $0) }) + declareEncodable(CachedChannelAdminRankType.self, f: { CachedChannelAdminRankType(decoder: $0) }) + declareEncodable(LaunchSettings.self, f: { LaunchSettings(decoder: $0)}) + declareEncodable(AutoplayMediaPreferences.self, f: { AutoplayMediaPreferences(decoder: $0)}) + declareEncodable(VoiceCallSettings.self, f: { VoiceCallSettings(decoder: $0)}) + declareEncodable(LaunchNavigation.self, f: { LaunchNavigation(decoder: $0)}) + declareEncodable(DownloadedFilesPaths.self, f: { DownloadedFilesPaths(decoder: $0)}) + declareEncodable(DownloadedPath.self, f: { DownloadedPath(decoder: $0)}) + declareEncodable(LocalBundleResource.self, f: { LocalBundleResource(decoder: $0)}) + declareEncodable(AssociatedWallpaper.self, f: { AssociatedWallpaper(decoder: $0) }) + declareEncodable(ThemeWallpaper.self, f: { ThemeWallpaper(decoder: $0) }) + declareEncodable(DefaultTheme.self, f: { DefaultTheme(decoder: $0) }) + declareEncodable(DefaultCloudTheme.self, f: { DefaultCloudTheme(decoder: $0) }) + declareEncodable(LocalWallapper.self, f: { LocalWallapper(decoder: $0) }) + declareEncodable(LocalAccentColor.self, f: { LocalAccentColor(decoder: $0) }) + // declareEncodable(WalletPasscodeTimeout.self, f: { WalletPasscodeTimeout(decoder: $0) }) + declareEncodable(PasscodeSettings.self, f: { PasscodeSettings(decoder: $0) }) + declareEncodable(CachedInstantPage.self, f: { CachedInstantPage(decoder: $0) }) + declareEncodable(RecentSettingsSearchQueryItem.self, f: { RecentSettingsSearchQueryItem(decoder: $0) }) + declareEncodable(ChatListFoldersSettings.self, f: { ChatListFoldersSettings(decoder: $0) }) + declareEncodable(PushToTalkValue.self, f: { PushToTalkValue(decoder: $0) }) + declareEncodable(PushToTalkValue.ModifierFlag.self, f: { PushToTalkValue.ModifierFlag(decoder: $0) }) + return }() diff --git a/Telegram-Mac/DeleteSupergroupMessagesModalController.swift b/Telegram-Mac/DeleteSupergroupMessagesModalController.swift index 97bea9716d..62ec72fa9a 100644 --- a/Telegram-Mac/DeleteSupergroupMessagesModalController.swift +++ b/Telegram-Mac/DeleteSupergroupMessagesModalController.swift @@ -8,9 +8,10 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit struct DeleteSupergroupMessagesSet : OptionSet { @@ -57,18 +58,18 @@ struct DeleteSupergroupMessagesSet : OptionSet { class DeleteSupergroupMessagesModalController: TableModalViewController { private let peerId:PeerId private let messageIds:[MessageId] - private let account:Account + private let context:AccountContext private let memberId:PeerId private var options:DeleteSupergroupMessagesSet = DeleteSupergroupMessagesSet(.deleteMessages) private let onComplete:()->Void private let peerViewDisposable = MetaDisposable() - init(account:Account, messageIds:[MessageId], peerId:PeerId, memberId: PeerId, onComplete: @escaping() -> Void) { - self.account = account + init(context: AccountContext, messageIds:[MessageId], peerId:PeerId, memberId: PeerId, onComplete: @escaping() -> Void) { + self.context = context self.messageIds = messageIds self.peerId = peerId self.memberId = memberId self.onComplete = onComplete - super.init(frame: NSMakeRect(0, 0, 280, 260)) + super.init(frame: NSMakeRect(0, 0, 350, 260)) bar = .init(height: 0) } @@ -81,66 +82,67 @@ class DeleteSupergroupMessagesModalController: TableModalViewController { super.viewDidLoad() let initialSize = atomicSize.modify({$0}) - peerViewDisposable.set((account.viewTracker.peerView( peerId) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peerView in + let update: Promise = Promise(Void()) + + peerViewDisposable.set(combineLatest(context.account.viewTracker.peerView( peerId) |> take(1) |> deliverOnMainQueue, update.get()).start(next: { [weak self] peerView, _ in if let strongSelf = self, let peer = peerViewMainPeer(peerView) as? TelegramChannel { + + _ = strongSelf.genericView.removeAll() + _ = strongSelf.genericView.addItem(item: GeneralRowItem(initialSize, height: 20, stableId: 0)) - _ = strongSelf.genericView.addItem(item: GeneralInteractedRowItem(initialSize, stableId: 1, name: tr(.supergroupDeleteRestrictionDeleteMessage), type: .selectable(stateback: { [weak strongSelf] () -> Bool in + _ = strongSelf.genericView.addItem(item: GeneralInteractedRowItem(initialSize, stableId: 1, name: tr(L10n.supergroupDeleteRestrictionDeleteMessage), type: .selectable(strongSelf.options.contains(.deleteMessages)), action: { [weak strongSelf] in if let strongSelf = strongSelf { - return strongSelf.options.contains(.deleteMessages) + if !strongSelf.options.isEmpty { + strongSelf.options.remove(.deleteMessages) + } + update.set(.single(Void())) } - return false - }), action: { - })) - if peer.hasAdminRights(.canBanUsers) { - _ = strongSelf.genericView.addItem(item: GeneralInteractedRowItem(initialSize, stableId: 2, name: tr(.supergroupDeleteRestrictionBanUser), type: .selectable(stateback: { [weak strongSelf] () -> Bool in - if let strongSelf = strongSelf { - return strongSelf.options.contains(.banUser) - } - return false - }), action: { [weak strongSelf] in + if peer.hasPermission(.banMembers) { + _ = strongSelf.genericView.addItem(item: GeneralInteractedRowItem(initialSize, stableId: 2, name: tr(L10n.supergroupDeleteRestrictionBanUser), type: .selectable(strongSelf.options.contains(.banUser)), action: { [weak strongSelf] in if let strongSelf = strongSelf { if strongSelf.options.contains(.banUser) { strongSelf.options.remove(.banUser) + if strongSelf.options.isEmpty { + strongSelf.options.insert(.deleteMessages) + } } else { strongSelf.options.insert(.banUser) } - strongSelf.genericView.reloadData() + update.set(.single(Void())) } })) } - _ = strongSelf.genericView.addItem(item: GeneralInteractedRowItem(initialSize, stableId: 3, name: tr(.supergroupDeleteRestrictionReportSpam), type: .selectable(stateback: { [weak strongSelf] () -> Bool in - if let strongSelf = strongSelf { - return strongSelf.options.contains(.reportSpam) - } - return false - }), action: { [weak strongSelf] in + _ = strongSelf.genericView.addItem(item: GeneralInteractedRowItem(initialSize, stableId: 3, name: tr(L10n.supergroupDeleteRestrictionReportSpam), type: .selectable(strongSelf.options.contains(.reportSpam)), action: { [weak strongSelf] in if let strongSelf = strongSelf { if strongSelf.options.contains(.reportSpam) { strongSelf.options.remove(.reportSpam) + if strongSelf.options.isEmpty { + strongSelf.options.insert(.deleteMessages) + } } else { strongSelf.options.insert(.reportSpam) } strongSelf.genericView.reloadData() + update.set(.single(Void())) } })) - _ = strongSelf.genericView.addItem(item: GeneralInteractedRowItem(initialSize, stableId: 4, name: tr(.supergroupDeleteRestrictionDeleteAllMessages), type: .selectable(stateback: { [weak strongSelf] () -> Bool in - if let strongSelf = strongSelf { - return strongSelf.options.contains(.deleteAllMessages) - } - return false - }), action: { [weak strongSelf] in + _ = strongSelf.genericView.addItem(item: GeneralInteractedRowItem(initialSize, stableId: 4, name: tr(L10n.supergroupDeleteRestrictionDeleteAllMessages), type: .selectable(strongSelf.options.contains(.deleteAllMessages)), action: { [weak strongSelf] in if let strongSelf = strongSelf { if strongSelf.options.contains(.deleteAllMessages) { strongSelf.options.remove(.deleteAllMessages) + if strongSelf.options.isEmpty { + strongSelf.options.insert(.deleteMessages) + } } else { strongSelf.options.insert(.deleteAllMessages) } strongSelf.genericView.reloadData() + update.set(.single(Void())) } })) @@ -152,25 +154,32 @@ class DeleteSupergroupMessagesModalController: TableModalViewController { } private func perform() { - var signals:[Signal] = [deleteMessagesInteractively(postbox: account.postbox, messageIds: messageIds, type: .forEveryone)] + var signals:[Signal] = [context.engine.messages.deleteMessagesInteractively(messageIds: messageIds, type: .forEveryone)] if options.contains(.banUser) { - signals.append(removePeerMember(account: account, peerId: peerId, memberId: memberId)) + + signals.append(context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(peerId: peerId, memberId: memberId, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max))) } if options.contains(.reportSpam) { - signals.append(reportSupergroupPeer(account: account, peerId: memberId, memberId: memberId, messageIds: messageIds)) + signals.append(context.engine.peers.reportPeerMessages(messageIds: messageIds, reason: .spam, message: "")) } if options.contains(.deleteAllMessages) { - signals.append(clearAuthorHistory(account: account, peerId: peerId, memberId: memberId)) + signals.append(context.engine.messages.clearAuthorHistory(peerId: peerId, memberId: memberId)) } _ = combineLatest(signals).start() onComplete() close() } + override func returnKeyAction() -> KeyHandlerResult { + perform() + close() + return .invoked + } + override var modalInteractions: ModalInteractions? { - return ModalInteractions(acceptTitle: tr(.modalOK), accept: { [weak self] in + return ModalInteractions(acceptTitle: tr(L10n.modalOK), accept: { [weak self] in self?.perform() - }, cancelTitle: tr(.modalCancel), drawBorder: true, height: 40) + }, cancelTitle: tr(L10n.modalCancel), drawBorder: true, height: 40) } diff --git a/Telegram-Mac/DesktopCaptureListUI.swift b/Telegram-Mac/DesktopCaptureListUI.swift new file mode 100644 index 0000000000..b4fb153908 --- /dev/null +++ b/Telegram-Mac/DesktopCaptureListUI.swift @@ -0,0 +1,449 @@ +// +// DesktopCaptureListController.swift +// Telegram +// +// Created by Mikhail Filimonov on 29.12.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import TgVoipWebrtc +import SwiftSignalKit + + +struct DesktopCapturerObjectWrapper : Equatable { + static func == (lhs: DesktopCapturerObjectWrapper, rhs: DesktopCapturerObjectWrapper) -> Bool { + if !lhs.source.isEqual(rhs.source) { + return false + } + if lhs.isAvailableToStream != rhs.isAvailableToStream { + return false + } + return true + } + + let source: VideoSourceMac + let isAvailableToStream: Bool +} + + +final class CameraCaptureDevice : VideoSourceMac, Equatable { + func isEqual(_ another: Any) -> Bool { + if let another = another as? VideoSourceMac { + return another.uniqueKey() == self.uniqueKey() + } else { + return false + } + } + + let device: AVCaptureDevice + init(_ device: AVCaptureDevice) { + self.device = device + } + func deviceIdKey() -> String { + return self.device.uniqueID + } + func title() -> String { + return device.localizedName + } + func uniqueKey() -> String { + return self.device.uniqueID + } + static func ==(lhs: CameraCaptureDevice, rhs: CameraCaptureDevice) -> Bool { + return lhs.device == rhs.device + } +} + +private final class DesktopCaptureListArguments { + let selectDesktop:(DesktopCaptureSourceMac, DesktopCaptureSourceManagerMac)->Void + let selectCamera:(CameraCaptureDevice)->Void + + init(selectDesktop:@escaping(DesktopCaptureSourceMac, DesktopCaptureSourceManagerMac)->Void, selectCamera:@escaping(CameraCaptureDevice)->Void) { + self.selectDesktop = selectDesktop + self.selectCamera = selectCamera + } +} + +private struct DesktopCaptureListState : Equatable { + + struct Access : Equatable { + let sharing: Bool + let camera: Bool + } + + var cameras:[CameraCaptureDevice] + var screens: [DesktopCaptureSourceMac] + var windows: [DesktopCaptureSourceMac] + var selected: VideoSourceMac? + var access:Access + init(cameras: [CameraCaptureDevice], screens: [DesktopCaptureSourceMac], windows: [DesktopCaptureSourceMac], selected: VideoSourceMac?, access: Access) { + self.cameras = cameras + self.screens = screens + self.windows = windows + self.selected = selected + self.access = access + } + static func ==(lhs: DesktopCaptureListState, rhs: DesktopCaptureListState) -> Bool { + let listEquals = lhs.cameras == rhs.cameras && lhs.screens == rhs.screens && lhs.windows == rhs.windows + + if !listEquals { + return false + } + if let lhsSelected = lhs.selected, let rhsSelected = rhs.selected { + if !lhsSelected.isEqual(rhsSelected) { + return false + } + } else if (lhs.selected != nil) != (rhs.selected != nil) { + return false + } + if lhs.access != rhs.access { + return false + } + return true + } +} + +private func entries(_ state: DesktopCaptureListState, screens: DesktopCaptureSourceManagerMac?, windows: DesktopCaptureSourceManagerMac?, excludeWindowNumber: Int = 0, arguments: DesktopCaptureListArguments) -> [InputDataEntry] { + + var entries:[InputDataEntry] = [] + + struct DesktopTuple : Equatable { + let source: DesktopCaptureSourceMac + let selected: Bool + let isAvailable: Bool + } + struct CameraTuple : Equatable { + let source: CameraCaptureDevice + let selected: Bool + let isAvailable: Bool + } + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("\(sectionId)"), equatable: nil, comparable: nil, item: { initialSize, stableId in + return GeneralRowItem(initialSize, height: 15, stableId: stableId, backgroundColor: .clear) + })) + sectionId += 1 + + + for source in state.cameras { + let id: String = source.uniqueKey() + let selected = state.selected != nil ? source.isEqual(state.selected!) : false + let tuple = CameraTuple(source: source, selected: selected, isAvailable: state.access.camera) + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier(id), equatable: InputDataEquatable(tuple), comparable: nil, item: { initialSize, stableId in + return DesktopCameraCapturerRowItem(initialSize, stableId: stableId, device: tuple.source, isAvailable: tuple.isAvailable, isSelected: tuple.selected, select: arguments.selectCamera) + })) + index += 1 + } + + for source in state.screens { + let id: String = source.uniqueKey() + let selected = state.selected != nil ? source.isEqual(state.selected!) : false + let tuple = DesktopTuple(source: source, selected: selected, isAvailable: state.access.sharing) + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier(id), equatable: InputDataEquatable(tuple), comparable: nil, item: { [weak screens] initialSize, stableId in + return DesktopCapturePreviewItem(initialSize, stableId: stableId, source: tuple.source, isAvailable: tuple.isAvailable, isSelected: tuple.selected, manager: screens, select: arguments.selectDesktop) + })) + index += 1 + } + + for source in state.windows { + let id: String = source.uniqueKey() + let selected = state.selected != nil ? source.isEqual(state.selected!) : false + let tuple = DesktopTuple(source: source, selected: selected, isAvailable: state.access.sharing) + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier(id), equatable: InputDataEquatable(tuple), comparable: nil, item: { [weak windows] initialSize, stableId in + return DesktopCapturePreviewItem(initialSize, stableId: stableId, source: tuple.source, isAvailable: tuple.isAvailable, isSelected: tuple.selected, manager: windows, select: arguments.selectDesktop) + })) + index += 1 + } + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("\(sectionId)"), equatable: nil, comparable: nil, item: { initialSize, stableId in + return GeneralRowItem(initialSize, height: 15, stableId: stableId, backgroundColor: .clear) + })) + sectionId += 1 + + return entries +} + +private final class DesktopCaptureListView : View { + fileprivate let tableView: HorizontalTableView + required init(frame frameRect: NSRect) { + tableView = HorizontalTableView(frame: frameRect.size.bounds, isFlipped: true, bottomInset: 0, drawBorder: false) + super.init(frame: frameRect) + addSubview(tableView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + tableView.frame = bounds + } +} + +final class DesktopCaptureListUI : GenericViewController { + + private var windows: DesktopCaptureSourceManagerMac! + private var screens: DesktopCaptureSourceManagerMac! + + private var updateDisposable: Disposable? + private let disposable: MetaDisposable = MetaDisposable() + private let devicesDisposable = MetaDisposable() + + private let mode: VideoSourceMacMode + private let devices: DevicesContext + + init(size: NSSize, devices: DevicesContext, mode: VideoSourceMacMode) { + self.devices = devices + self.mode = mode + super.init(frame: size.bounds) + self.bar = .init(height: 0) + } + + + + var updateDesktopSelected:((DesktopCapturerObjectWrapper, DesktopCaptureSourceManagerMac)->Void)? = nil + var updateCameraSelected:((DesktopCapturerObjectWrapper)->Void)? = nil + + + + var excludeWindowNumber: Int = 0 + + private var getCurrentlySelected: (()->VideoSourceMac?)? = nil + var selected: VideoSourceMac? { + return self.getCurrentlySelected?() + } + + override func viewDidLoad() { + super.viewDidLoad() + self.windows = DesktopCaptureSourceManagerMac(_w: ()) + self.screens = DesktopCaptureSourceManagerMac(_s: ()) + + let actionsDisposable = DisposableSet() + + var hasCameraAccess = false + var requestCamera = false + if #available(OSX 10.14, *) { + let camera = AVCaptureDevice.authorizationStatus(for: .video) + switch camera { + case .authorized: + hasCameraAccess = true + case .notDetermined: + requestCamera = true + default: + break + } + } else { + hasCameraAccess = true + } + + var sList:[DesktopCaptureSourceMac] = [] + var wList: [DesktopCaptureSourceMac] = [] + var sharingAccess: Bool = false + + switch mode { + case .screencast: + sList = screens.list() + wList = windows.list() + sharingAccess = requestScreenCaptureAccess() + case .video: + break + } + + + let initialState = DesktopCaptureListState(cameras: [], screens: sList, windows: wList, selected: nil, access: .init(sharing: sharingAccess, camera: hasCameraAccess)) + + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((DesktopCaptureListState) -> DesktopCaptureListState) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + getCurrentlySelected = { + stateValue.with { $0.selected } + } + + self.onDeinit = { + updateState { current in + var current = current + current.cameras = [] + current.screens = [] + current.selected = nil + current.windows = [] + return current + } + actionsDisposable.dispose() + } + + if requestCamera && mode == .video { + actionsDisposable.add(requestCameraPermission().start(next: { access in + updateState { state in + var state = state + state.access = DesktopCaptureListState.Access(sharing: state.access.sharing, camera: access) + return state + } + })) + } + + let windows = self.windows + let screens = self.screens + + let checkSelected = { + updateState { current in + var current = current + if let selected = current.selected { + let windowsContains = current.windows.contains(where: { + $0.isEqual(selected) + }) + let screensContains = current.screens.contains(where: { + $0.isEqual(selected) + }) + let camerasContains = current.cameras.contains(where: { + $0.isEqual(selected) + }) + if !windowsContains && !screensContains && !camerasContains { + current.selected = nil + } + } + if current.selected == nil { + current.selected = current.cameras.first ?? current.screens.first ?? current.windows.first + } + return current + } + } + + let muxedDevices = devices.signal |> deliverOnMainQueue |> map { devices in + + updateState { current in + var current = current + current.cameras = devices.camera.filter { !$0.isSuspended && $0.isConnected && $0.hasMediaType(.muxed) }.map { CameraCaptureDevice($0) } + return current + } + + } + + let updateSignal = (Signal { [weak windows, weak screens] subscriber in + + updateState { current in + var current = current + current.screens = screens?.list() ?? [] + current.windows = windows?.list() ?? [] + return current + } + checkSelected() + subscriber.putCompletion() + + return EmptyDisposable + } |> then(.complete() |> suspendAwareDelay(2, queue: .mainQueue()))) |> restart + + let updateAccess = (Signal { subscriber in + + updateState { current in + var current = current + if #available(macOS 10.14, *) { + current.access = .init(sharing: screenCaptureAvailable(), camera: AVCaptureDevice.authorizationStatus(for: .video) == .authorized) + } else { + current.access = .init(sharing: screenCaptureAvailable(), camera: true) + } + return current + } + checkSelected() + subscriber.putCompletion() + + return EmptyDisposable + } |> then(.complete() |> suspendAwareDelay(5, queue: .mainQueue()))) |> restart + + let updateSelected: Signal = statePromise.get() |> map { $0.selected } |> distinctUntilChanged(isEqual: { lhs, rhs in + if let lhs = lhs, let rhs = rhs { + return lhs.isEqual(rhs) + } else if (lhs != nil) != (rhs != nil) { + return false + } + return true + }) + actionsDisposable.add(updateSelected.start(next: { [weak self, weak screens] selected in + if let selected = selected as? DesktopCaptureSourceMac, let screens = screens { + self?.updateDesktopSelected?(DesktopCapturerObjectWrapper(source: selected, isAvailableToStream: stateValue.with { $0.access.sharing }), screens) + } else if let selected = selected as? CameraCaptureDevice { + self?.updateCameraSelected?(DesktopCapturerObjectWrapper(source: selected, isAvailableToStream: stateValue.with { $0.access.camera })) + } + })) + + switch mode { + case .screencast: + self.updateDisposable = combineLatest(updateSignal, muxedDevices).start() + case .video: + devicesDisposable.set((devices.signal |> deliverOnMainQueue).start(next: { devices in + updateState { current in + var current = current + current.cameras = devices.camera.filter { !$0.isSuspended && $0.isConnected && $0.hasMediaType(.video) }.map { CameraCaptureDevice($0) } + return current + } + checkSelected() + })) + } + + actionsDisposable.add(updateAccess.start()) + + let arguments = DesktopCaptureListArguments(selectDesktop: { source, manager in + updateState { current in + var current = current + current.selected = source + return current + } + }, selectCamera: { source in + updateState { current in + var current = current + current.selected = source + return current + } + }) + + let excludeWindowNumber = self.excludeWindowNumber + + + let signal = statePromise.get() |> map { [weak windows, weak screens] state in + return InputDataSignalValue(entries: entries(state, screens: screens, windows: windows, excludeWindowNumber: excludeWindowNumber, arguments: arguments)) + } + + let previous: Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) + + let initialSize = self.atomicSize + + let transaction: Signal = combineLatest(signal, appearanceSignal) |> mapToQueue { state, appearance in + + let entries = state.entries.map { AppearanceWrapperEntry(entry: $0, appearance: appearance) } + return prepareInputDataTransition(left: previous.swap(entries), right: entries, animated: state.animated, searchState: nil, initialSize: initialSize.with { $0 }, arguments: InputDataArguments(select: {_, _ in }, dataUpdated: {}), onMainQueue: false) + } |> deliverOnMainQueue + + genericView.needUpdateVisibleAfterScroll = true + + genericView.getBackgroundColor = { + .clear + } + + disposable.set(transaction.start(next: { [weak self] transaction in + self?.genericView.merge(with: transaction) + self?.readyOnce() + checkSelected() + })) + + } + + override func initializer() -> HorizontalTableView { + return HorizontalTableView(frame: _frameRect.size.bounds, isFlipped: true, bottomInset: 0, drawBorder: false) + } + + deinit { + disposable.dispose() + updateDisposable?.dispose() + devicesDisposable.dispose() + onDeinit?() + } + +} + diff --git a/Telegram-Mac/DesktopCapturePreviewItem.swift b/Telegram-Mac/DesktopCapturePreviewItem.swift new file mode 100644 index 0000000000..9a108dd088 --- /dev/null +++ b/Telegram-Mac/DesktopCapturePreviewItem.swift @@ -0,0 +1,377 @@ +// +// DesktopCapturerPreviewItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 29.12.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Foundation +import TgVoipWebrtc +import TGUIKit +import SwiftSignalKit + +final class DesktopCapturePreviewItem : GeneralRowItem { + fileprivate let scope: DesktopCaptureSourceScopeMac + fileprivate let selected: Bool + fileprivate let select: (DesktopCaptureSourceMac, DesktopCaptureSourceManagerMac)->Void + fileprivate private(set) weak var manager: DesktopCaptureSourceManagerMac? + fileprivate let isAvailable: Bool + init(_ initialSize: NSSize, stableId: AnyHashable, source: DesktopCaptureSourceMac, isAvailable: Bool, isSelected: Bool, manager: DesktopCaptureSourceManagerMac?, select: @escaping(DesktopCaptureSourceMac, DesktopCaptureSourceManagerMac)->Void) { + self.manager = manager + self.scope = DesktopCaptureSourceScopeMac(source: source, data: DesktopCaptureSourceDataMac(size: CGSize(width: 135, height: 90).multipliedByScreenScale(), fps: 0.5, captureMouse: false)) + self.select = select + self.isAvailable = isAvailable + self.selected = isSelected + super.init(initialSize, stableId: stableId) + } + + override var height:CGFloat { + return 145 + } + override var width: CGFloat { + return 90 + } + + override func viewClass() -> AnyClass { + return DesktopCapturePreviewView.self + } +} + + +class DesktopCameraCapturerRowItem: GeneralRowItem { + fileprivate let source: CameraCaptureDevice + fileprivate let selected: Bool + fileprivate let select:(CameraCaptureDevice)->Void + fileprivate let isAvailable: Bool + init(_ initialSize: NSSize, stableId: AnyHashable, device: CameraCaptureDevice, isAvailable: Bool, isSelected: Bool, select:@escaping(CameraCaptureDevice)->Void) { + self.source = device + self.selected = isSelected + self.select = select + self.isAvailable = isAvailable + super.init(initialSize, stableId: stableId) + + } + + override var height:CGFloat { + return 145 + } + override var width: CGFloat { + return 90 + } + + + + override func viewClass() -> AnyClass { + return DesktopCapturePreviewView.self + } +} + + + +private final class DesktopCaptureSourceMacView : Control { + + + private var contentView: View = View() + private let backgroundView: View = View() + private let textView = TextView() + + private let picture: ImageView = ImageView() + + private var callback:(()->Void)? + private var selected: Bool = false + + + private var view: NSView? = nil + + private let shadowView = ShadowView() + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(contentView) + addSubview(picture) + + shadowView.shadowBackground = .blackTransparent + + textView.userInteractionEnabled = false + textView.isSelectable = false + + + layer?.cornerRadius = 10 + self.contentView.layer?.cornerRadius = 10 + set(handler: { [weak self] control in + if self?.selected == false { + self?.picture.change(opacity: 1) + } + }, for: .Hover) + + set(handler: { [weak self] control in + if self?.selected == false { + self?.picture.change(opacity: 1) + } + }, for: .Highlight) + + set(handler: { [weak self] control in + if self?.selected == false { + self?.picture.change(opacity: 0) + } + }, for: .Normal) + + set(handler: { [weak self] _ in + self?.callback?() + }, for: .Click) + } + + var previousState: ControlState? + + + override func stateDidUpdated(_ state: ControlState) { + super.stateDidUpdated(state) + + switch controlState { + case .Normal: + if self.selected == false { + self.picture.change(opacity: 0) + } + case .Hover, .Highlight: + if self.selected == false { + self.picture.change(opacity: 1) + } + default: + break + } + previousState = state + } + + var contentRect: NSRect { + let rect: NSRect + rect = NSMakeRect(4, 4, frame.width - 8, frame.height - 8) + return rect + } + + private var source: VideoSourceMac? + func update(view: NSView, source: VideoSourceMac, selected: Bool, animated: Bool, callback:@escaping()->Void) { + self.callback = callback + self.source = source + view.frame = bounds + self.view = view + self.contentView.subviews = [self.backgroundView, view, self.shadowView, self.textView] + + + let layout = TextViewLayout(.initialize(string: source.title(), color: .white, font: .normal(.short)), maximumNumberOfLines: 1, truncationType: .middle) + layout.measure(width: frame.width - 20) + textView.update(layout) + + self.selected = selected + backgroundView.backgroundColor = NSColor.black.withAlphaComponent(0.9) + + contentView.layer?.cornerRadius = 6 + +// picture.animates = true + + picture.layer?.opacity = selected ? 1 : 0 + + picture.image = generateImage(frame.size, contextGenerator: { size, ctx in + ctx.clear(.init(origin: .zero, size: size)) + + if selected { + ctx.setStrokeColor(GroupCallTheme.accent.cgColor) + } else { + ctx.setStrokeColor(GroupCallTheme.secondary.cgColor) + } + ctx.setLineWidth(5) + let path = CGMutablePath() + path.addRoundedRect(in: .init(origin: .zero, size: size), cornerWidth: 10, cornerHeight: 10) + path.closeSubpath() + ctx.addPath(path) + ctx.strokePath() + }) + picture.sizeToFit() + + updateState() + + needsLayout = true + } + + func viewFor(_ other: VideoSourceMac) -> NSView? { + if let source = self.source { + if source.isEqual(other) { + return self.view + } + } + return nil + } + + deinit { + if let layer = self.view?.layer as? AVCaptureVideoPreviewLayer { + layer.session?.stopRunning() + layer.session = nil + } + } + + override func layout() { + super.layout() + self.contentView.frame = contentRect + self.textView.centerX(y: contentRect.height - self.textView.frame.height - 8) + self.backgroundView.frame = contentView.bounds + self.view?.frame = contentView.bounds + self.shadowView.frame = contentView.bounds + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +final class DesktopCapturePreviewView : HorizontalRowView { + + private let contentView = DesktopCaptureSourceMacView(frame: NSMakeRect(5, 0, 135, 90)) + private let disposable = MetaDisposable() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + + addSubview(contentView) + + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + updateListeners() + update() + } + + deinit { + NotificationCenter.default.removeObserver(self) + disposable.dispose() + } + + private func updateListeners() { + NotificationCenter.default.removeObserver(self) + NotificationCenter.default.addObserver(self, selector: #selector(update), name: NSView.boundsDidChangeNotification, object: enclosingScrollView?.contentView) + NotificationCenter.default.addObserver(self, selector: #selector(update), name: NSView.frameDidChangeNotification, object: enclosingScrollView) + } + @objc private func update() { + if let item = item as? DesktopCapturePreviewItem { + if let manager = item.manager { + if visibleRect != .zero, item.isAvailable, window != nil { + disposable.set(delaySignal(0.03).start(completed: { [weak manager, weak item] in + if let item = item { + manager?.start(item.scope) + } + })) + } else { + disposable.set(nil) + manager.stop(item.scope) + } + } + } + if let item = item as? DesktopCameraCapturerRowItem { + if item.isAvailable { + if let session = (contentView.viewFor(item.source)?.layer as? AVCaptureVideoPreviewLayer)?.session { + if visibleRect != .zero { + disposable.set(delaySignal(0.07).start(completed: { [weak session] in + DispatchQueue.global().async { [weak session] in + session?.startRunning() + } + })) + } else { + disposable.set(nil) + DispatchQueue.global().async { [weak session] in + session?.stopRunning() + } + } + } + } + } + + } + + override var backdorColor: NSColor { + return .clear + } + + override func updateColors() { + super.updateColors() + self.backgroundColor = backdorColor + } + + override func updateMouse() { + super.updateMouse() + contentView.updateState() + } + + + + override func set(item: TableRowItem, animated: Bool = false) { + + let previous = self.item as? DesktopCapturePreviewItem + + super.set(item: item, animated: animated) + + if let previous = previous { + if let manager = previous.manager { + manager.stop(previous.scope) + } + } + + if let item = item as? DesktopCapturePreviewItem { + if let manager = item.manager { + let view: NSView + if item.isAvailable { + view = contentView.viewFor(item.scope.source) ?? manager.create(forScope: item.scope) + } else { + view = View() + } + contentView.update(view: view, source: item.scope.source, selected: item.selected, animated: animated, callback: { [weak item] in + if let item = item, let manager = item.manager { + item.select(item.scope.source, manager) + } + }) + } + } + + if let item = item as? DesktopCameraCapturerRowItem { + if item.isAvailable { + + } + let view: View + if item.isAvailable { + if let exist = contentView.viewFor(item.source) as? View { + view = exist + } else { + let session: AVCaptureSession = AVCaptureSession() + let input = try? AVCaptureDeviceInput(device: item.source.device) + if let input = input { + session.addInput(input) + } + let captureLayer = AVCaptureVideoPreviewLayer() + captureLayer.session = session + captureLayer.connection?.automaticallyAdjustsVideoMirroring = false + captureLayer.connection?.isVideoMirrored = shouldBeMirrored(item.source.device) + captureLayer.videoGravity = .resizeAspectFill + view = View() + view.layer = captureLayer + } + } else { + view = View() + } + + + contentView.update(view: view, source: item.source, selected: item.selected, animated: animated, callback: { [weak item] in + if let item = item { + item.select(item.source) + } + }) + } + + + update() + needsLayout = true + } + +} diff --git a/Telegram-Mac/DesktopCapturerWindow.swift b/Telegram-Mac/DesktopCapturerWindow.swift new file mode 100644 index 0000000000..d293e51c67 --- /dev/null +++ b/Telegram-Mac/DesktopCapturerWindow.swift @@ -0,0 +1,440 @@ +// +// DesktopCapturerWindow.swift +// Telegram +// +// Created by Mikhail Filimonov on 06.01.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import TgVoipWebrtc +import SwiftSignalKit + + + + +private final class UnavailableToStreamView : View { + let text: TextView = TextView() + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(text) + backgroundColor = .black + self.text.isSelectable = false + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(isScreen: Bool) { + let text: String + if isScreen { + text = L10n.voiceChatScreenShareUnavailable + } else { + text = L10n.voiceChatVideoShareUnavailable + } + let attr = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: .normal(.text), textColor: GroupCallTheme.grayStatusColor), bold: MarkdownAttributeSet(font: .bold(.text), textColor: GroupCallTheme.grayStatusColor), link: MarkdownAttributeSet(font: .normal(.text), textColor: GroupCallTheme.accent), linkAttribute: { contents in + return (NSAttributedString.Key.link.rawValue, inAppLink.callback(contents, {_ in})) + })) + let layout = TextViewLayout(attr) + let executor = globalLinkExecutor + executor.processURL = { value in + if let value = value as? inAppLink { + switch value.link { + case "screen": + openSystemSettings(.sharing) + case "camera": + openSystemSettings(.camera) + default: + break + } + } + } + layout.interactions = executor + layout.measure(width: frame.width) + self.text.update(layout) + } + + override func layout() { + super.layout() + self.text.center() + } +} + + + +private final class DesktopCapturerView : View { + private let listContainer = View() + private let previewContainer = View() + private let titleView = TextView() + private let titleContainer = View() + private let controls = View() + + let cancel = TitleButton() + let share = TitleButton() + + fileprivate class Micro : Control { + + var isOn: Bool = true { + didSet { + if isOn != oldValue { + toggle(animated: true) + FastSettings.updateVCShareMicro(isOn) + } + } + } + + private let animationView = LottiePlayerView(frame: NSMakeRect(0, 0, 50, 50)) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(animationView) + + self.set(background: NSColor.black.withAlphaComponent(0.7), for: .Normal) + self.set(background: NSColor.black.withAlphaComponent(0.8), for: .Highlight) + self.scaleOnClick = true + self.layer?.cornerRadius = frame.height / 2 + + self.toggle(animated: false) + + self.set(handler: { [weak self] _ in + guard let strongSelf = self else { + return + } + strongSelf.isOn = !strongSelf.isOn + }, for: .Click) + } + + private func toggle(animated: Bool) { + let isOn = self.isOn + + let sticker: LocalAnimatedSticker + let playPolicy: LottiePlayPolicy + + if !isOn { + sticker = .voice_chat_mute + } else { + sticker = .voice_chat_unmute + } + + if !animated { + playPolicy = .toEnd(from: .max) + } else { + playPolicy = .toEnd(from: 0) + } + + if let data = sticker.data { + animationView.set(.init(compressed: data, key: .init(key: .bundle(sticker.rawValue), size: NSMakeSize(50, 50)), cachePurpose: .none, playPolicy: playPolicy, runOnQueue: .mainQueue())) + } + } + + override func layout() { + super.layout() + animationView.center() + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + } + + fileprivate let micro: Micro = Micro(frame: NSMakeRect(0, 0, 40, 40)) + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(listContainer) + addSubview(previewContainer) + + addSubview(titleContainer) + titleContainer.addSubview(titleView) + addSubview(controls) + previewContainer.layer?.cornerRadius = 10 + previewContainer.backgroundColor = .black + backgroundColor = GroupCallTheme.windowBackground + titleView.userInteractionEnabled = false + titleView.isSelectable = false + layout() + + addSubview(micro) + + let titleLayout = TextViewLayout.init(.initialize(string: L10n.voiceChatVideoVideoSource, color: GroupCallTheme.titleColor, font: .medium(.title))) + titleLayout.measure(width: frameRect.width) + titleView.update(titleLayout) + + cancel.set(text: L10n.voiceChatVideoVideoSourceCancel, for: .Normal) + cancel.set(color: .white, for: .Normal) + cancel.set(background: GroupCallTheme.speakDisabledColor, for: .Normal) + cancel.set(background: GroupCallTheme.speakDisabledColor.withAlphaComponent(0.8), for: .Highlight) + cancel.sizeToFit(.zero, NSMakeSize(100, 30), thatFit: true) + cancel.layer?.cornerRadius = .cornerRadius + + share.set(text: L10n.voiceChatVideoVideoSourceShare, for: .Normal) + share.set(color: .white, for: .Normal) + share.set(background: GroupCallTheme.accent, for: .Normal) + share.set(background: GroupCallTheme.accent.withAlphaComponent(0.8), for: .Highlight) + share.sizeToFit(.zero, NSMakeSize(100, 30), thatFit: true) + share.layer?.cornerRadius = .cornerRadius + + controls.addSubview(cancel) + controls.addSubview(share) + + cancel.scaleOnClick = true + share.scaleOnClick = true + + } + + private var previousDesktop: (DesktopCaptureSourceScopeMac, DesktopCaptureSourceManagerMac)? + + func updatePreview(_ source: DesktopCaptureSourceMac, isAvailable: Bool, manager: DesktopCaptureSourceManagerMac, animated: Bool) { + if let previous = previousDesktop { + previous.1.stop(previous.0) + } + if isAvailable { + let size = NSMakeSize(previewContainer.frame.width * 2.5, previewContainer.frame.size.height * 2.5) + let scope = DesktopCaptureSourceScopeMac(source: source, data: DesktopCaptureSourceDataMac(size: size, fps: 24, captureMouse: true)) + let view = manager.create(forScope: scope) + manager.start(scope) + self.previousDesktop = (scope, manager) + swapView(view, animated: animated) + + } else { + let view = UnavailableToStreamView(frame: previewContainer.bounds) + view.update(isScreen: true) + swapView(view, animated: animated) + } + + share.isEnabled = isAvailable + } + + private func swapView(_ view: NSView, animated: Bool) { + let previewView = previewContainer + view.frame = previewView.bounds + + for previous in previewView.subviews { + if animated { + previous.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak previous] _ in + previous?.removeFromSuperview() + }) + } else { + previous.removeFromSuperview() + } + } + previewView.addSubview(view) + if animated { + view.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + + } + + func updatePreview(_ source: CameraCaptureDevice, isAvailable: Bool, animated: Bool) { + if let previous = previousDesktop { + previous.1.stop(previous.0) + } + + if isAvailable { + let view: View = View() + let session: AVCaptureSession = AVCaptureSession() + let input = try? AVCaptureDeviceInput(device: source.device) + if let input = input { + session.addInput(input) + } + let captureLayer = AVCaptureVideoPreviewLayer(session: session) + captureLayer.connection?.automaticallyAdjustsVideoMirroring = false + captureLayer.connection?.isVideoMirrored = shouldBeMirrored(source.device) + captureLayer.videoGravity = .resizeAspect + view.layer = captureLayer + + + swapView(view, animated: animated) + + session.startRunning() + + } else { + let view = UnavailableToStreamView(frame: previewContainer.bounds) + view.update(isScreen: false) + swapView(view, animated: animated) + + } + previousDesktop = nil + share.isEnabled = isAvailable + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var listView: NSView? { + didSet { + oldValue?.removeFromSuperview() + if let value = listView { + listContainer.addSubview(value) + } + } + } + + override func layout() { + super.layout() + previewContainer.frame = .init(origin: .init(x: 20, y: 53), size: .init(width: 660, height: 360)) + listContainer.frame = .init(origin: .init(x: 0, y: frame.height - 90 - 80), size: .init(width: frame.width, height: 90)) + if let listView = listView { + listView.frame = listContainer.bounds + } + titleContainer.frame = NSMakeRect(0, 0, frame.width, 53) + titleView.center() + + controls.frame = NSMakeRect(0, frame.height - 80, frame.width, 80) + + cancel.centerY(x: frame.midX - cancel.frame.width - 5) + share.centerY(x: frame.midX + 5) + + micro.setFrameOrigin(NSMakePoint(previewContainer.frame.minX + 10, previewContainer.frame.maxY - micro.frame.height - 10)) + + } +} + +final class DesktopCapturerWindow : Window { + + private let listController: DesktopCaptureListUI + let mode: VideoSourceMacMode + fileprivate let select: (VideoSourceMac, Bool)->Void + init(mode: VideoSourceMacMode, select: @escaping(VideoSourceMac, Bool)->Void, devices: DevicesContext) { + self.mode = mode + self.select = select + let size = NSMakeSize(700, 600) + listController = DesktopCaptureListUI(size: NSMakeSize(size.width, 90), devices: devices, mode: mode) + + var rect: NSRect = .init(origin: .zero, size: size) + if let screen = NSScreen.main { + let x = floorToScreenPixels(System.backingScale, (screen.frame.width - size.width) / 2) + let y = floorToScreenPixels(System.backingScale, (screen.frame.height - size.height) / 2) + rect = .init(origin: .init(x: x, y: y), size: size) + } + + super.init(contentRect: rect, styleMask: [.fullSizeContentView, .borderless, .closable, .titled], backing: .buffered, defer: true) + self.minSize = NSMakeSize(700, 600) + self.name = "DesktopCapturerWindow" + self.titlebarAppearsTransparent = true + self.titleVisibility = .visible + self.animationBehavior = .alertPanel + self.isReleasedWhenClosed = false + self.isMovableByWindowBackground = true + self.level = .normal + self.toolbar = NSToolbar(identifier: "window") + self.toolbar?.showsBaselineSeparator = false + + + initSaver() + } + + func initGuts() { + + self.contentView = DesktopCapturerView(frame: .init(origin: .zero, size: self.frame.size)) + + var first: Bool = true + + + listController.updateDesktopSelected = { [weak self] wrap, manager in + self?.genericView.updatePreview(wrap.source as! DesktopCaptureSourceMac, isAvailable: wrap.isAvailableToStream, manager: manager, animated: !first) + first = false + } + + listController.updateCameraSelected = { [weak self] wrap in + self?.genericView.updatePreview(wrap.source as! CameraCaptureDevice, isAvailable: wrap.isAvailableToStream, animated: !first) + first = false + } + + + self.listController.excludeWindowNumber = self.windowNumber + self.genericView.listView = listController.view + + + self.genericView.cancel.set(handler: { [weak self] _ in + self?.orderOut(nil) + }, for: .Click) + + self.genericView.share.set(handler: { [weak self] _ in + self?.orderOut(nil) + if let source = self?.listController.selected { + let select = self?.select + let wantsToSpeak = self?.genericView.micro.isOn ?? false + delay(1.0, closure: { + select?(source, wantsToSpeak) + }) + } + }, for: .Click) + + } + + private var genericView:DesktopCapturerView { + return self.contentView as! DesktopCapturerView + } + + + override func layoutIfNeeded() { + super.layoutIfNeeded() + + var point: NSPoint = NSMakePoint(20, 17) + self.standardWindowButton(.closeButton)?.setFrameOrigin(point) + point.x += 20 + self.standardWindowButton(.miniaturizeButton)?.setFrameOrigin(point) + point.x += 20 + self.standardWindowButton(.zoomButton)?.setFrameOrigin(point) + } + + deinit { + var bp:Int = 0 + bp += 1 + } +} + +enum VideoSourceMacMode { + case video + case screencast + + var viceVersa: VideoSourceMacMode { + switch self { + case .video: + return .screencast + case .screencast: + return .video + } + } +} +extension VideoSourceMac { + + var mode: VideoSourceMacMode { + if self is DesktopCaptureSourceMac { + return .screencast + } else if let device = self as? CameraCaptureDevice { + if device.device.hasMediaType(.muxed) { + return .screencast + } else { + return .video + } + } else { + return .video + } + } +} + +func presentDesktopCapturerWindow(mode: VideoSourceMacMode, select: @escaping(VideoSourceMac, Bool)->Void, devices: DevicesContext) -> DesktopCapturerWindow? { + + switch mode { + case .video: + let devices = AVCaptureDevice.devices(for: .video).filter({ $0.isConnected && !$0.isSuspended }) + if devices.isEmpty { + return nil + } + case .screencast: + break + } + + let window = DesktopCapturerWindow(mode: mode, select: select, devices: devices) + window.initGuts() + window.makeKeyAndOrderFront(nil) + + return window +} diff --git a/Telegram-Mac/DeveloperViewController.swift b/Telegram-Mac/DeveloperViewController.swift new file mode 100644 index 0000000000..04945ccd6a --- /dev/null +++ b/Telegram-Mac/DeveloperViewController.swift @@ -0,0 +1,242 @@ +// +// DeveloperViewController.swift +// Telegram +// +// Created by keepcoder on 30/11/2017. +// Copyright © 2017 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit +import TelegramCore + +import MtProtoKit +import Postbox + +private final class DeveloperArguments { + let importColors:()->Void + let exportColors:()->Void + let toggleLogs:(Bool)->Void + let navigateToLogs:()->Void + let addAccount:()->Void + init(importColors:@escaping()->Void, exportColors:@escaping()->Void, toggleLogs:@escaping(Bool)->Void, navigateToLogs:@escaping()->Void, addAccount: @escaping() -> Void) { + self.importColors = importColors + self.exportColors = exportColors + self.toggleLogs = toggleLogs + self.navigateToLogs = navigateToLogs + self.addAccount = addAccount + } +} + +private enum DeveloperEntryId : Hashable { + case importColors + case exportColors + case toggleLogs + case openLogs + case accounts + case enableFilters + case section(Int32) + var hashValue: Int { + switch self { + case .importColors: + return 0 + case .exportColors: + return 1 + case .toggleLogs: + return 2 + case .openLogs: + return 3 + case .accounts: + return 4 + case .enableFilters: + return 5 + case .section(let section): + return 6 + Int(section) + } + } +} + +private enum DeveloperEntry : TableItemListNodeEntry { + + case importColors(sectionId: Int32) + case exportColors(sectionId: Int32) + case toggleLogs(sectionId: Int32, enabled: Bool) + case openLogs(sectionId: Int32) + case accounts(sectionId: Int32) + case enableFilters(sectionId: Int32, enabled: Bool) + case section(Int32) + + var stableId:DeveloperEntryId { + switch self { + case .importColors: + return .importColors + case .exportColors: + return .exportColors + case .toggleLogs: + return .toggleLogs + case .openLogs: + return .openLogs + case .accounts: + return .accounts + case .enableFilters: + return .enableFilters + case .section(let section): + return .section(section) + } + } + + var index:Int32 { + switch self { + case .importColors(let sectionId): + return (sectionId * 1000) + Int32(stableId.hashValue) + case .exportColors(let sectionId): + return (sectionId * 1000) + Int32(stableId.hashValue) + case .toggleLogs(let sectionId, _): + return (sectionId * 1000) + Int32(stableId.hashValue) + case .openLogs(let sectionId): + return (sectionId * 1000) + Int32(stableId.hashValue) + case .accounts(let sectionId): + return (sectionId * 1000) + Int32(stableId.hashValue) + case .enableFilters(let sectionId, _): + return (sectionId * 1000) + Int32(stableId.hashValue) + case .section(let sectionId): + return (sectionId + 1) * 1000 - sectionId + } + } + + static func <(lhs: DeveloperEntry, rhs: DeveloperEntry) -> Bool { + return lhs.index < rhs.index + } + + + func item(_ arguments: DeveloperArguments, initialSize: NSSize) -> TableRowItem { + switch self { + case .importColors: + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: "Import Palette", type: .next, action: { + arguments.importColors() + }) + case .exportColors: + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: "Export Palette", type: .next, action: { + arguments.exportColors() + }) + case .openLogs: + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: "Open Logs", type: .next, action: { + arguments.navigateToLogs() + }) + case .accounts: + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: "Add Account", type: .next, action: { + arguments.addAccount() + }) + case let .enableFilters(_, enabled): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: "Enable Filters", type: .switchable(enabled), action: { + }) + case let .toggleLogs(_, enabled): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: "Enable Logs", type: .switchable(enabled), action: { + arguments.toggleLogs(!enabled) + }) + case .section: + return GeneralRowItem(initialSize, height: 20, stableId: stableId) + } + } + +} + +private func developerEntries(loginSettings: LoggingSettings) -> [DeveloperEntry] { + var entries:[DeveloperEntry] = [] + + var sectionId:Int32 = 1 + + entries.append(.section(sectionId)) + sectionId += 1 + + entries.append(.accounts(sectionId: sectionId)) + + entries.append(.section(sectionId)) + sectionId += 1 + + entries.append(.toggleLogs(sectionId: sectionId, enabled: loginSettings.logToFile)) + + entries.append(.openLogs(sectionId: sectionId)) + + entries.append(.section(sectionId)) + sectionId += 1 + + + entries.append(.section(sectionId)) + sectionId += 1 + return entries +} + +fileprivate func prepareTransition(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], initialSize:NSSize, arguments:DeveloperArguments) -> TableUpdateTransition { + + let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in + return entry.entry.item(arguments, initialSize: initialSize) + } + + return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true) +} + +class DeveloperViewController: TableViewController { + + init(context: AccountContext) { + super.init(context) + } + + override func viewDidLoad() { + super.viewDidLoad() + + genericView.getBackgroundColor = { + theme.colors.background + } + + let context = self.context + let previousEntries:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) + let initialSize = self.atomicSize + let arguments = DeveloperArguments(importColors: { + filePanel(with: ["palette"], allowMultiple: false, for: mainWindow, completion: { list in + if let path = list?.first { + if let theme = importPalette(path) { + let palettesDir = ApiEnvironment.containerURL!.appendingPathComponent("Palettes").path + try? FileManager.default.createDirectory(atPath: palettesDir, withIntermediateDirectories: true, attributes: nil) + try? FileManager.default.removeItem(atPath: palettesDir + "/" + path.nsstring.lastPathComponent) + try? FileManager.default.copyItem(atPath: path, toPath: palettesDir + "/" + path.nsstring.lastPathComponent) + _ = updateThemeInteractivetly(accountManager: context.sharedContext.accountManager, f: { settings in + return settings.withUpdatedPalette(theme).withUpdatedCloudTheme(nil) + }).start() + } else { + alert(for: mainWindow, info: "Parsing Error") + } + } + }) + }, exportColors: { + exportPalette(palette: theme.colors) + }, toggleLogs: { enabled in + MTLogSetEnabled(enabled) + _ = updateLoggingSettings(accountManager: context.sharedContext.accountManager, { + $0.withUpdatedLogToFile(enabled) + }).start() + Logger.shared.logToConsole = false + Logger.shared.logToFile = enabled + }, navigateToLogs: { + NSWorkspace.shared.activateFileViewerSelecting([ApiEnvironment.containerURL!.appendingPathComponent("logs")]) + }, addAccount: { + let testingEnvironment = NSApp.currentEvent?.modifierFlags.contains(.command) == true + context.sharedContext.beginNewAuth(testingEnvironment: testingEnvironment) + }) + + let signal = combineLatest(queue: prepareQueue, context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.loggingSettings]), appearanceSignal) + + genericView.merge(with: signal |> map { preferences, appearance in + let entries = developerEntries(loginSettings: preferences.entries[SharedDataKeys.loggingSettings] as? LoggingSettings ?? LoggingSettings.defaultSettings).map{AppearanceWrapperEntry(entry: $0, appearance: appearance)} + return prepareTransition(left: previousEntries.swap(entries), right: entries, initialSize: initialSize.modify({$0}), arguments: arguments) + } |> deliverOnMainQueue) + + readyOnce() + } + + override var defaultBarTitle: String { + return "Developer" + } + +} diff --git a/Telegram-Mac/DevicesContext.swift b/Telegram-Mac/DevicesContext.swift new file mode 100644 index 0000000000..f6d74f1004 --- /dev/null +++ b/Telegram-Mac/DevicesContext.swift @@ -0,0 +1,505 @@ +// +// DevicesContext.swift +// Telegram +// +// Created by Mikhail Filimonov on 06.12.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Foundation +import SwiftSignalKit +import TelegramCore +import Postbox +import CoreMediaIO + +struct IODevices { + let camera: [AVCaptureDevice] + let audioInput: [AVCaptureDevice] + let audioOutput: [AudioDeviceID] + let loading: Bool +} + +extension AudioDeviceID { + var uniqueID: String { + return DevicesContext.Audio.getDeviceUid(deviceId: self) + } + var localizedName: String { + return DevicesContext.Audio.getDeviceName(deviceID: self) + } +} + +private func devicesList() -> Signal { + return Signal { subscriber in + + let defAudioDevice = AVCaptureDevice.default(for: .audio) + let defVideoDevice = AVCaptureDevice.default(for: .video) + + var videoDevices = DALDevices() + var audioDevices = AVCaptureDevice.devices(for: .audio) + + if !videoDevices.isEmpty, let device = defVideoDevice { + videoDevices.removeAll(where: { $0.uniqueID == device.uniqueID}) + videoDevices.insert(device, at: 0) + } + if !audioDevices.isEmpty, let device = defAudioDevice { + audioDevices.removeAll(where: { $0.uniqueID == device.uniqueID}) + audioDevices.insert(device, at: 0) + } + subscriber.putNext(.init(camera: videoDevices, audioInput: audioDevices, audioOutput: DevicesContext.Audio.getAllDevices().filter { DevicesContext.Audio.isOutputDevice(deviceID: $0) }, loading: false)) + subscriber.putCompletion() + + + return EmptyDisposable + } |> runOn(.concurrentDefaultQueue()) + +} + + +func sizeof (_ : T.Type) -> Int +{ + return (MemoryLayout.size) +} + +func sizeof (_ : T) -> Int +{ + return (MemoryLayout.size) +} + +func sizeof (_ value : [T]) -> Int +{ + return (MemoryLayout.size * value.count) +} + + + +final class DevicesContext : NSObject { + private var _signal: Promise = Promise() + var signal: Signal { + return _signal.get() + } + + + private var observeContext = 0; + + private final class UpdaterContext { +// var status: (camera: String?, input: String?, output: String?) = (camera: nil, input: nil, output: nil) + let subscribers = Bag<((camera: String?, input: String?, output: String?)) -> Void>() + } + + private let updaterContext: UpdaterContext = UpdaterContext() + + + + func updater() -> Signal<(camera: String?, input: String?, output: String?), NoError> { + return Signal { subscriber in + + let disposable = MetaDisposable() + let statusContext: UpdaterContext = self.updaterContext + + let index = statusContext.subscribers.add({ status in + subscriber.putNext(status) + }) + +// subscriber.putNext(statusContext.status) + + disposable.set(ActionDisposable { + DispatchQueue.main.async { + self.updaterContext.subscribers.remove(index) + } + }) + + return disposable + } |> runOn(.mainQueue()) + } + + + private let disposable = MetaDisposable() + private let devicesQueue = Queue(name: "devicesQueue") + + private let _currentCameraId: Atomic = Atomic(value: nil) + private let _currentMicroId: Atomic = Atomic(value: nil) + private let _currentOutputId: Atomic = Atomic(value: nil) + + var currentCameraId: String? { + return _currentCameraId.with { $0 } + } + var currentMicroId: String? { + return _currentMicroId.with { $0 } + } + var currentOutputId: String? { + return _currentOutputId.with { $0 } + } + + init(_ accountManager: AccountManager ) { + super.init() + + + var prop : CMIOObjectPropertyAddress = CMIOObjectPropertyAddress( + mSelector: CMIOObjectPropertySelector(kCMIOHardwarePropertyAllowScreenCaptureDevices), + mScope: CMIOObjectPropertyScope(kCMIOObjectPropertyScopeGlobal), + mElement: CMIOObjectPropertyElement(kCMIOObjectPropertyElementMaster)) + + var allow: UInt32 = 1 + CMIOObjectSetPropertyData(CMIOObjectID(kCMIOObjectSystemObject), + &prop, 0, nil, + UInt32(sizeof(allow)), &allow ); + + + + /* + + CMIOObjectPropertyAddress prop = { + kCMIOHardwarePropertyAllowScreenCaptureDevices, + kCMIOObjectPropertyScopeGlobal, + kCMIOObjectPropertyElementMaster + }; + UInt32 allow = 1; + CMIOObjectSetPropertyData(kCMIOObjectSystemObject, + &prop, 0, NULL, + sizeof(allow), &allow ); + */ + + NotificationCenter.default.addObserver(forName: NSNotification.Name.AVCaptureDeviceWasConnected, object: nil, queue: nil, using: { [weak self] _ in + self?.update() + }) + NotificationCenter.default.addObserver(forName: NSNotification.Name.AVCaptureDeviceWasDisconnected, object: nil, queue: nil, using: { [weak self] _ in + self?.update() + }) + AudioObjectAddPropertyListener(AudioObjectID(kAudioObjectSystemObject), &AudioAddress.outputDevice, AudioListener.output, nil) + + AudioObjectAddPropertyListener(AudioObjectID(kAudioObjectSystemObject), &AudioAddress.inputDevice, AudioListener.input, nil) + + + NotificationCenter.default.addObserver(forName: AudioNotification.audioOutputDeviceDidChange.notificationName, object: nil, queue: nil, using: { [weak self] _ in + self?.update() + }) + + NotificationCenter.default.addObserver(forName: AudioNotification.audioInputDeviceDidChange.notificationName, object: nil, queue: nil, using: { [weak self] _ in + self?.update() + }) + + let currentCameraId = self._currentCameraId + let currentMicroId = self._currentMicroId + let currentOutputId = self._currentOutputId + + let updated = combineLatest(queue: devicesQueue, voiceCallSettings(accountManager), signal) |> map { settings, devices -> (camera: String?, input: String?, output: String?) in + let inputUpdated = DevicesContext.updateMicroId(settings, devices: devices) + let cameraUpdated = DevicesContext.updateCameraId(settings, devices: devices) + let outputUpdated = DevicesContext.updateOutputId(settings, devices: devices) + + var result:(camera: String?, input: String?, output: String?) = (camera: nil, input: nil, output: nil) + + if currentMicroId.swap(inputUpdated) != inputUpdated { + result.input = inputUpdated + } + if currentCameraId.swap(cameraUpdated) != cameraUpdated { + result.camera = cameraUpdated + } + if currentOutputId.swap(outputUpdated) != outputUpdated { + result.output = outputUpdated + } + return result + } |> deliverOnMainQueue + + disposable.set(updated.start(next: { [weak self] result in + guard let `self` = self else { + return + } + //self.updaterContext.status = result + + for subscriber in self.updaterContext.subscribers.copyItems() { + subscriber(result) + } + + // self.updaterContext.status = (camera: nil, input: nil, output: nil) + })) + + update() + } + + private func update() { + _signal.set(devicesList() |> filter { !$0.loading }) + } + + @objc private func handleOutputNotification(_ notification: Notification) { + self.update() + } + + static func updateCameraId(_ settings: VoiceCallSettings, devices: IODevices) -> String? { + let cameraDevice = devices.camera.first(where: { $0.uniqueID == settings.cameraInputDeviceId }) + + let activeDevice: AVCaptureDevice? + if let cameraDevice = cameraDevice { + if cameraDevice.isConnected && !cameraDevice.isSuspended { + activeDevice = cameraDevice + } else { + activeDevice = nil + } + } else if settings.cameraInputDeviceId == nil { + activeDevice = AVCaptureDevice.default(for: .video) + } else { + activeDevice = devices.camera.first(where: { $0.isConnected && !$0.isSuspended }) + } + + return activeDevice?.uniqueID + } + static func updateMicroId(_ settings: VoiceCallSettings, devices: IODevices) -> String? { + let audiodevice = devices.audioInput.first(where: { $0.uniqueID == settings.audioInputDeviceId }) + + let activeDevice: AVCaptureDevice? + if let audiodevice = audiodevice { + if audiodevice.isConnected && !audiodevice.isSuspended { + activeDevice = audiodevice + } else { + activeDevice = nil + } + } else if settings.audioInputDeviceId == nil { + activeDevice = AVCaptureDevice.default(for: .audio) + } else { + activeDevice = devices.audioInput.first(where: { $0.isConnected && !$0.isSuspended }) + } + + return activeDevice?.uniqueID + } + + static func updateOutputId(_ settings: VoiceCallSettings, devices: IODevices) -> String? { + var deviceUid: String? = nil + var found = false + for id in devices.audioOutput { + let current = Audio.getDeviceUid(deviceId: id) + if settings.audioOutputDeviceId == current { + deviceUid = Audio.getDeviceUid(deviceId: id) + found = true + } + } + if !found { + deviceUid = Audio.getDeviceUid(deviceId: Audio.getDefaultOutputDevice()) + } + + return deviceUid + } + + deinit { + NotificationCenter.default.removeObserver(self) + AudioObjectRemovePropertyListener(AudioObjectID(kAudioObjectSystemObject), &AudioAddress.outputDevice, AudioListener.output, nil) + AudioObjectRemovePropertyListener(AudioObjectID(kAudioObjectSystemObject), &AudioAddress.inputDevice, AudioListener.input, nil) + disposable.dispose() + } +} + +private extension DevicesContext { + class Audio { + static func getOutputDevices() -> [AudioDeviceID: String]? { + var result: [AudioDeviceID: String] = [:] + let devices = getAllDevices() + + for device in devices { + if isOutputDevice(deviceID: device) { + result[device] = getDeviceName(deviceID: device) + } + } + + return result + } + static func isOutputDevice(deviceID: AudioDeviceID) -> Bool { + var propertySize: UInt32 = 256 + + var propertyAddress = AudioObjectPropertyAddress( + mSelector: AudioObjectPropertySelector(kAudioDevicePropertyStreams), + mScope: AudioObjectPropertyScope(kAudioDevicePropertyScopeOutput), + mElement: AudioObjectPropertyElement(kAudioObjectPropertyElementMaster)) + + _ = AudioObjectGetPropertyDataSize(deviceID, &propertyAddress, 0, nil, &propertySize) + + return propertySize > 0 + } + + static func getAggregateDeviceSubDeviceList(deviceID: AudioDeviceID) -> [AudioDeviceID] { + let subDevicesCount = getNumberOfSubDevices(deviceID: deviceID) + var subDevices = [AudioDeviceID](repeating: 0, count: Int(subDevicesCount)) + + var propertyAddress = AudioObjectPropertyAddress( + mSelector: AudioObjectPropertySelector(kAudioAggregateDevicePropertyActiveSubDeviceList), + mScope: AudioObjectPropertyScope(kAudioObjectPropertyScopeGlobal), + mElement: AudioObjectPropertyElement(kAudioObjectPropertyElementMaster)) + + var subDevicesSize = subDevicesCount * UInt32(MemoryLayout.size) + + AudioObjectGetPropertyData(deviceID, &propertyAddress, 0, nil, &subDevicesSize, &subDevices) + + return subDevices + } + + static func isAggregateDevice(deviceID: AudioDeviceID) -> Bool { + let deviceType = getDeviceTransportType(deviceID: deviceID) + return deviceType == kAudioDeviceTransportTypeAggregate + } + + static func setDeviceVolume(deviceID: AudioDeviceID, leftChannelLevel: Float, rightChannelLevel: Float) { + let channelsCount = 2 + var channels = [UInt32](repeating: 0, count: channelsCount) + var propertySize = UInt32(MemoryLayout.size * channelsCount) + var leftLevel = leftChannelLevel + var rigthLevel = rightChannelLevel + + var propertyAddress = AudioObjectPropertyAddress( + mSelector: AudioObjectPropertySelector(kAudioDevicePropertyPreferredChannelsForStereo), + mScope: AudioObjectPropertyScope(kAudioDevicePropertyScopeOutput), + mElement: AudioObjectPropertyElement(kAudioObjectPropertyElementMaster)) + + let status = AudioObjectGetPropertyData(deviceID, &propertyAddress, 0, nil, &propertySize, &channels) + + if status != noErr { return } + + propertyAddress.mSelector = kAudioDevicePropertyVolumeScalar + propertySize = UInt32(MemoryLayout.size) + propertyAddress.mElement = channels[0] + + AudioObjectSetPropertyData(deviceID, &propertyAddress, 0, nil, propertySize, &leftLevel) + + propertyAddress.mElement = channels[1] + + AudioObjectSetPropertyData(deviceID, &propertyAddress, 0, nil, propertySize, &rigthLevel) + } + + static func setOutputDevice(newDeviceID: AudioDeviceID) { + let propertySize = UInt32(MemoryLayout.size) + var deviceID = newDeviceID + + var propertyAddress = AudioObjectPropertyAddress( + mSelector: AudioObjectPropertySelector(kAudioHardwarePropertyDefaultOutputDevice), + mScope: AudioObjectPropertyScope(kAudioObjectPropertyScopeGlobal), + mElement: AudioObjectPropertyElement(kAudioObjectPropertyElementMaster)) + + AudioObjectSetPropertyData(AudioObjectID(kAudioObjectSystemObject), &propertyAddress, 0, nil, propertySize, &deviceID) + } + + static func getDeviceVolume(deviceID: AudioDeviceID) -> [Float] { + let channelsCount = 2 + var channels = [UInt32](repeating: 0, count: channelsCount) + var propertySize = UInt32(MemoryLayout.size * channelsCount) + var leftLevel = Float32(-1) + var rigthLevel = Float32(-1) + + var propertyAddress = AudioObjectPropertyAddress( + mSelector: AudioObjectPropertySelector(kAudioDevicePropertyPreferredChannelsForStereo), + mScope: AudioObjectPropertyScope(kAudioDevicePropertyScopeOutput), + mElement: AudioObjectPropertyElement(kAudioObjectPropertyElementMaster)) + + let status = AudioObjectGetPropertyData(deviceID, &propertyAddress, 0, nil, &propertySize, &channels) + + if status != noErr { return [-1] } + + propertyAddress.mSelector = kAudioDevicePropertyVolumeScalar + propertySize = UInt32(MemoryLayout.size) + propertyAddress.mElement = channels[0] + + AudioObjectGetPropertyData(deviceID, &propertyAddress, 0, nil, &propertySize, &leftLevel) + + propertyAddress.mElement = channels[1] + + AudioObjectGetPropertyData(deviceID, &propertyAddress, 0, nil, &propertySize, &rigthLevel) + + return [leftLevel, rigthLevel] + } + + static func getDefaultOutputDevice() -> AudioDeviceID { + var propertySize = UInt32(MemoryLayout.size) + var deviceID = kAudioDeviceUnknown + + var propertyAddress = AudioObjectPropertyAddress( + mSelector: AudioObjectPropertySelector(kAudioHardwarePropertyDefaultOutputDevice), + mScope: AudioObjectPropertyScope(kAudioObjectPropertyScopeGlobal), + mElement: AudioObjectPropertyElement(kAudioObjectPropertyElementMaster)) + + AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject), &propertyAddress, 0, nil, &propertySize, &deviceID) + + return deviceID + } + + private static func getDeviceTransportType(deviceID: AudioDeviceID) -> AudioDevicePropertyID { + var deviceTransportType = AudioDevicePropertyID() + var propertySize = UInt32(MemoryLayout.size) + + var propertyAddress = AudioObjectPropertyAddress( + mSelector: AudioObjectPropertySelector(kAudioDevicePropertyTransportType), + mScope: AudioObjectPropertyScope(kAudioObjectPropertyScopeGlobal), + mElement: AudioObjectPropertyElement(kAudioObjectPropertyElementMaster)) + + AudioObjectGetPropertyData(deviceID, &propertyAddress, 0, nil, &propertySize, &deviceTransportType) + + return deviceTransportType + } + + private static func getNumberOfDevices() -> UInt32 { + var propertySize: UInt32 = 0 + + var propertyAddress = AudioObjectPropertyAddress( + mSelector: AudioObjectPropertySelector(kAudioHardwarePropertyDevices), + mScope: AudioObjectPropertyScope(kAudioObjectPropertyScopeGlobal), + mElement: AudioObjectPropertyElement(kAudioObjectPropertyElementMaster)) + + _ = AudioObjectGetPropertyDataSize(AudioObjectID(kAudioObjectSystemObject), &propertyAddress, 0, nil, &propertySize) + + return propertySize / UInt32(MemoryLayout.size) + } + + private static func getNumberOfSubDevices(deviceID: AudioDeviceID) -> UInt32 { + var propertySize: UInt32 = 0 + + var propertyAddress = AudioObjectPropertyAddress( + mSelector: AudioObjectPropertySelector(kAudioAggregateDevicePropertyActiveSubDeviceList), + mScope: AudioObjectPropertyScope(kAudioObjectPropertyScopeGlobal), + mElement: AudioObjectPropertyElement(kAudioObjectPropertyElementMaster)) + + _ = AudioObjectGetPropertyDataSize(deviceID, &propertyAddress, 0, nil, &propertySize) + + return propertySize / UInt32(MemoryLayout.size) + } + + static func getDeviceName(deviceID: AudioDeviceID) -> String { + var propertySize = UInt32(MemoryLayout.size) + + var propertyAddress = AudioObjectPropertyAddress( + mSelector: AudioObjectPropertySelector(kAudioDevicePropertyDeviceNameCFString), + mScope: AudioObjectPropertyScope(kAudioObjectPropertyScopeGlobal), + mElement: AudioObjectPropertyElement(kAudioObjectPropertyElementMaster)) + + var result: CFString = "" as CFString + + AudioObjectGetPropertyData(deviceID, &propertyAddress, 0, nil, &propertySize, &result) + + return result as String + } + + static func getDeviceUid(deviceId: AudioDeviceID) -> String { + var propertySize = UInt32(MemoryLayout.size) + var propertyAddress = AudioObjectPropertyAddress( + mSelector: kAudioDevicePropertyDeviceUID, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster) + + var deviceUid: CFString = "" as CFString + AudioObjectGetPropertyData(deviceId, &propertyAddress, 0, nil, &propertySize, &deviceUid) + + return deviceUid as String + } + + static func getAllDevices() -> [AudioDeviceID] { + let devicesCount = getNumberOfDevices() + var devices = [AudioDeviceID](repeating: 0, count: Int(devicesCount)) + + var propertyAddress = AudioObjectPropertyAddress( + mSelector: AudioObjectPropertySelector(kAudioHardwarePropertyDevices), + mScope: AudioObjectPropertyScope(kAudioObjectPropertyScopeGlobal), + mElement: AudioObjectPropertyElement(kAudioObjectPropertyElementMaster)) + + var devicesSize = devicesCount * UInt32(MemoryLayout.size) + + AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject), &propertyAddress, 0, nil, &devicesSize, &devices) + + return devices + } + + } +} diff --git a/Telegram-Mac/DiceCache.swift b/Telegram-Mac/DiceCache.swift new file mode 100644 index 0000000000..a5c334c5e2 --- /dev/null +++ b/Telegram-Mac/DiceCache.swift @@ -0,0 +1,373 @@ +// +// DiceCache.swift +// Telegram +// +// Created by Mikhail Filimonov on 28.02.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import SwiftSignalKit +import Postbox + +struct InteractiveEmojiConfetti : Equatable { + let playAt: Int32 + let value:Int32 +} + +struct InteractiveEmojiConfiguration : Equatable { + static var defaultValue: InteractiveEmojiConfiguration { + return InteractiveEmojiConfiguration(emojis: [], confettiCompitable: [:]) + } + + let emojis: [String] + private let confettiCompitable: [String: InteractiveEmojiConfetti] + + fileprivate init(emojis: [String], confettiCompitable: [String: InteractiveEmojiConfetti]) { + self.emojis = emojis.map { $0.fixed } + self.confettiCompitable = confettiCompitable + } + + static func with(appConfiguration: AppConfiguration) -> InteractiveEmojiConfiguration { + if let data = appConfiguration.data, let value = data["emojies_send_dice"] as? [String] { + let dict:[String : Any]? = data["emojies_send_dice_success"] as? [String:Any] + + var confetti:[String: InteractiveEmojiConfetti] = [:] + if let dict = dict { + for (key, value) in dict { + if let data = value as? [String: Any], let frameStart = data["frame_start"] as? Double, let value = data["value"] as? Double { + confetti[key] = InteractiveEmojiConfetti(playAt: Int32(frameStart), value: Int32(value)) + } + } + } + return InteractiveEmojiConfiguration(emojis: value, confettiCompitable: confetti) + } else { + return .defaultValue + } + } + + func playConfetti(_ emoji: String) -> InteractiveEmojiConfetti? { + return confettiCompitable[emoji] + } +} + +struct EmojiesSoundConfiguration : Equatable { + + static var defaultValue: EmojiesSoundConfiguration { + return EmojiesSoundConfiguration(sounds: [:]) + } + + public let sounds: [String: TelegramMediaFile] + + fileprivate init(sounds: [String: TelegramMediaFile]) { + self.sounds = sounds + } + + static func with(appConfiguration: AppConfiguration) -> EmojiesSoundConfiguration { + if let data = appConfiguration.data, let values = data["emojies_sounds"] as? [String: Any] { + var sounds: [String: TelegramMediaFile] = [:] + for (key, value) in values { + if let dict = value as? [String: String], var fileReferenceString = dict["file_reference_base64"] { + fileReferenceString = fileReferenceString.replacingOccurrences(of: "-", with: "+") + fileReferenceString = fileReferenceString.replacingOccurrences(of: "_", with: "/") + while fileReferenceString.count % 4 != 0 { + fileReferenceString.append("=") + } + + if let idString = dict["id"], let id = Int64(idString), let accessHashString = dict["access_hash"], let accessHash = Int64(accessHashString), let fileReference = Data(base64Encoded: fileReferenceString) { + let resource = CloudDocumentMediaResource(datacenterId: 1, fileId: id, accessHash: accessHash, size: nil, fileReference: fileReference, fileName: nil) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: nil, attributes: []) + sounds[key] = file + } + } + } + return EmojiesSoundConfiguration(sounds: sounds) + } else { + return .defaultValue + } + } + + +} + +private final class EmojiDataContext { + var data: [(String, Data?, TelegramMediaFile)] = [] + let subscribers = Bag<([(String, Data?, TelegramMediaFile)]) -> Void>() +} + +class DiceCache { + private let postbox: Postbox + private let engine: TelegramEngine + private var dataContexts: [String : EmojiDataContext] = [:] + private var dataEffectsContexts: [String : EmojiDataContext] = [:] + + private let fetchDisposable = MetaDisposable() + private let loadDataDisposable = MetaDisposable() + private let emojiesSoundDisposable = MetaDisposable() + + init(postbox: Postbox, engine: TelegramEngine) { + self.postbox = postbox + self.engine = engine + + + let availablePacks = postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) |> map { view in + return view.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? AppConfiguration.defaultValue + } |> map { + return InteractiveEmojiConfiguration.with(appConfiguration: $0) + } |> distinctUntilChanged + + + let emojiesSound = postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) |> map { view in + return view.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? AppConfiguration.defaultValue + } |> map { value -> EmojiesSoundConfiguration in + return EmojiesSoundConfiguration.with(appConfiguration: value) + } |> distinctUntilChanged |> mapToSignal { value -> Signal in + //val + let list = value.sounds.map { fetchedMediaResource(mediaBox: postbox.mediaBox, reference: MediaResourceReference.standalone(resource: $0.value.resource )) } + let signals = combineLatest(list) + + return signals |> ignoreValues |> `catch` { _ -> Signal in return .complete() } + } + + emojiesSoundDisposable.set(emojiesSound.start()) + + let packs = availablePacks |> mapToSignal { config -> Signal<[(String, [StickerPackItem])], NoError> in + var signals: [Signal<(String, [StickerPackItem]), NoError>] = [] + for emoji in config.emojis { + signals.append(engine.stickers.loadedStickerPack(reference: .dice(emoji), forceActualized: true) + |> map { result -> (String, [StickerPackItem]) in + switch result { + case let .result(_, items, _): + var dices: [StickerPackItem] = [] + for case let item as StickerPackItem in items { + dices.append(item) + } + return (emoji, dices) + default: + return (emoji, []) + } + }) + } + return combineLatest(signals) + } + + let emojiEffects: Signal<[StickerPackItem], NoError> = engine.stickers.loadedStickerPack(reference: .animatedEmojiAnimations, forceActualized: true) + |> map { result -> [StickerPackItem] in + switch result { + case let .result(_, items, _): + var effects: [StickerPackItem] = [] + for case let item as StickerPackItem in items { + effects.append(item) + } + return effects + default: + return [] + } + } + + let fetchDices = combineLatest(packs, emojiEffects) |> map { value, effects in + return value.map { $0.1 }.reduce([], { current, value in + return current + value + }) + effects + } |> mapToSignal { dices -> Signal in + let signals = dices.map { value -> Signal in + let reference: MediaResourceReference + if let stickerReference = value.file.stickerReference { + reference = FileMediaReference.stickerPack(stickerPack: stickerReference, media: value.file).resourceReference(value.file.resource) + } else { + reference = FileMediaReference.standalone(media: value.file).resourceReference(value.file.resource) + } + return fetchedMediaResource(mediaBox: postbox.mediaBox, reference: reference) + } + return combineLatest(signals) |> map { _ in return } |> `catch` { _ in return .complete() } + } + + fetchDisposable.set(fetchDices.start()) + + let data = packs |> mapToSignal { values -> Signal<[String : [(String, Data?, TelegramMediaFile)]], NoError> in + + var signals: [Signal<(String, [(String, Data?, TelegramMediaFile)]), NoError>] = [] + + for value in values { + let dices = value.1.map { value in + return postbox.mediaBox.resourceData(value.file.resource) |> mapToSignal { resourceData -> Signal in + if resourceData.complete, let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) { + return .single(data) + } else { + return .single(nil) + } + } |> map { ((value.file.stickerText ?? value.getStringRepresentationsOfIndexKeys().first!).fixed, $0, value.file) } + } + signals.append(combineLatest(dices) |> map { (value.0, $0) }) + } + + return combineLatest(signals) |> map { values in + var dict: [String : [(String, Data?, TelegramMediaFile)]] = [:] + + for value in values { + dict[value.0.fixed] = value.1 + } + return dict + } + } |> deliverOnResourceQueue + + + let dataEffects = emojiEffects |> mapToSignal { values -> Signal<[String: [(String, Data?, TelegramMediaFile)]], NoError> in + + + let effects = values.map { value in + return postbox.mediaBox.resourceData(value.file.resource) |> mapToSignal { resourceData -> Signal in + if resourceData.complete, let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) { + return .single(data) + } else { + return .single(nil) + } + } |> map { ((value.file.stickerText ?? value.getStringRepresentationsOfIndexKeys().first!).fixed, $0, value.file) } + } + return combineLatest(effects) |> map { values in + var dict: [String : [(String, Data?, TelegramMediaFile)]] = [:] + + for value in values { + var list = dict[value.0.fixed] ?? [] + list.append(value) + dict[value.0] = list + } + return dict + } + } |> deliverOnResourceQueue + + loadDataDisposable.set(combineLatest(data, dataEffects).start(next: { [weak self] data, dataEffects in + guard let `self` = self else { + return + } + for diceData in data { + let context = self.dataContexts[diceData.key.fixed] ?? EmojiDataContext() + context.data = diceData.value + for subscriber in context.subscribers.copyItems() { + subscriber(diceData.value) + } + self.dataContexts[diceData.key.fixed] = context + } + for effect in dataEffects { + let context = self.dataEffectsContexts[effect.key] ?? EmojiDataContext() + context.data = effect.value + for subscriber in context.subscribers.copyItems() { + subscriber(effect.value) + } + self.dataEffectsContexts[effect.key] = context + } + })) + + } + + func animationEffect(for emoji: String) -> Signal<[(String, Data?, TelegramMediaFile)], NoError> { + return Signal { [weak self] subscriber in + guard let `self` = self else { + return EmptyDisposable + } + var cancelled = false + let disposable = MetaDisposable() + + let invoke = { + if !cancelled { + var dataContext: EmojiDataContext + if let dc = self.dataEffectsContexts[emoji.fixed] { + dataContext = dc + } else { + dataContext = EmojiDataContext() + } + + self.dataEffectsContexts[emoji.fixed] = dataContext + + let index = dataContext.subscribers.add({ data in + if !cancelled { + subscriber.putNext(data) + } + }) + subscriber.putNext(dataContext.data) + disposable.set(ActionDisposable { [weak self] in + resourcesQueue.async { + if let current = self?.dataEffectsContexts[emoji.fixed] { + current.subscribers.remove(index) + } + } + }) + } + + } + resourcesQueue.sync(invoke) + + return ActionDisposable { + disposable.dispose() + cancelled = true + } + } + } + + + + func interactiveSymbolData(baseSymbol: String, synchronous: Bool) -> Signal<[(String, Data?, TelegramMediaFile)], NoError> { + return Signal { [weak self] subscriber in + + guard let `self` = self else { + return EmptyDisposable + } + var cancelled = false + let disposable = MetaDisposable() + + let invoke = { + if !cancelled { + var dataContext: EmojiDataContext + if let dc = self.dataContexts[baseSymbol] { + dataContext = dc + } else { + dataContext = EmojiDataContext() + } + + self.dataContexts[baseSymbol] = dataContext + + let index = dataContext.subscribers.add({ data in + if !cancelled { + subscriber.putNext(data) + } + }) + + subscriber.putNext(dataContext.data) + + disposable.set(ActionDisposable { [weak self] in + resourcesQueue.async { + if let current = self?.dataContexts[baseSymbol] { + current.subscribers.remove(index) + } + } + }) + } + + } + + // if synchronous { + resourcesQueue.sync(invoke) + // } else { + // resourcesQueue.async(invoke) + // } + + + return ActionDisposable { + disposable.dispose() + cancelled = true + } + } + } + + func cleanup() { + fetchDisposable.dispose() + loadDataDisposable.dispose() + emojiesSoundDisposable.dispose() + } + + deinit { + cleanup() + } +} diff --git a/Telegram-Mac/DiscussionSetModalController.swift b/Telegram-Mac/DiscussionSetModalController.swift new file mode 100644 index 0000000000..3e714511b0 --- /dev/null +++ b/Telegram-Mac/DiscussionSetModalController.swift @@ -0,0 +1,129 @@ +// +// DiscussionSetModalController.swift +// Telegram +// +// Created by Mikhail Filimonov on 23/05/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + + +private final class DiscussionSetView : View { + private let channelPhoto: AvatarControl = AvatarControl(font: .avatar(22)) + private let groupPhoto: AvatarControl = AvatarControl(font: .avatar(22)) + private let photoContainer: View = View() + private let textView: TextView = TextView() + private let maskView: View = View() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(photoContainer) + channelPhoto.setFrameSize(NSMakeSize(60, 60)) + groupPhoto.setFrameSize(NSMakeSize(60, 60)) + maskView.setFrameSize(NSMakeSize(64, 64)) + maskView.layer?.cornerRadius = maskView.frame.height / 2 + maskView.layer?.borderWidth = (maskView.frame.width - groupPhoto.frame.width) / 2 + maskView.layer?.borderColor = theme.colors.background.cgColor + photoContainer.addSubview(channelPhoto) + photoContainer.addSubview(groupPhoto) + photoContainer.addSubview(maskView) + + photoContainer.setFrameSize(NSMakeSize(groupPhoto.frame.width + channelPhoto.frame.width - 10, maskView.frame.height)) + textView.isSelectable = false + textView.userInteractionEnabled = false + addSubview(textView) + } + + override func layout() { + super.layout() + + photoContainer.centerX(y: 20) + groupPhoto.centerY(x: 2) + channelPhoto.centerY(x: channelPhoto.frame.maxX - 10) + textView.centerX(y: photoContainer.frame.maxY + 20) + } + + func update(context: AccountContext, channel: Peer, group: Peer) -> NSSize { + channelPhoto.setPeer(account: context.account, peer: channel) + groupPhoto.setPeer(account: context.account, peer: group) + + let attributedString = NSMutableAttributedString() + + if channel.addressName == nil && group.addressName != nil { + _ = attributedString.append(string: L10n.discussionSetModalTextPrivateChannelPublicGroup(group.displayTitle, channel.displayTitle), color: theme.colors.text, font: .normal(.text)) + } else if group.addressName == nil { + _ = attributedString.append(string: L10n.discussionSetModalTextChannelPrivateGroup(group.displayTitle, channel.displayTitle), color: theme.colors.text, font: .normal(.text)) + } else { + _ = attributedString.append(string: L10n.discussionSetModalTextPublicChannelPublicGroup(group.displayTitle, channel.displayTitle), color: theme.colors.text, font: .normal(.text)) + } + attributedString.detectBoldColorInString(with: .medium(.text)) + + let layout = TextViewLayout(attributedString, alignment: .center) + layout.measure(width: 300 - 40) + + textView.update(layout) + + return NSMakeSize(300, textView.frame.height + photoContainer.frame.height + 20 + 20 + 20) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class DiscussionSetModalController: ModalViewController { + + private let context: AccountContext + private let channel: Peer + private let group:Peer + private let accept:()->Void + init(context: AccountContext, channel: Peer, group:Peer, accept:@escaping()->Void) { + self.context = context + self.channel = channel + self.group = group + self.accept = accept + super.init(frame: NSMakeRect(0, 0, 300, 300)) + } + + override func viewDidLoad() { + super.viewDidLoad() + let size = genericView.update(context: self.context, channel: self.channel, group: self.group) + modal?.resize(with: size, animated: false) + readyOnce() + } + + override var handleEvents: Bool { + return true + } + + override func returnKeyAction() -> KeyHandlerResult { + self.close() + self.accept() + return .invoked + } + + override var handleAllEvents: Bool { + return true + } + + + private var genericView:DiscussionSetView { + return self.view as! DiscussionSetView + } + + override func viewClass() -> AnyClass { + return DiscussionSetView.self + } + + override var modalInteractions: ModalInteractions? { + return ModalInteractions(acceptTitle: L10n.discussionSetModalOK, accept: { [weak self] in + self?.close() + self?.accept() + }, cancelTitle: L10n.modalCancel, drawBorder: false, height: 50) + } +} diff --git a/Telegram-Mac/DisplayLink.swift b/Telegram-Mac/DisplayLink.swift new file mode 100644 index 0000000000..dec68baeaa --- /dev/null +++ b/Telegram-Mac/DisplayLink.swift @@ -0,0 +1,93 @@ +// +// DisplayLink.swift +// +// Created by Jose Canepa on 8/18/16. +// Copyright © 2016 Jose Canepa. All rights reserved. +// +import AppKit + +/** + Analog to the CADisplayLink in iOS. + */ +class DisplayLink +{ + let timer : CVDisplayLink + let source : DispatchSourceUserDataAdd + + var callback : Optional<() -> ()> = nil + + var running : Bool { return CVDisplayLinkIsRunning(timer) } + + init?(onQueue queue: DispatchQueue = DispatchQueue.main) + { + source = DispatchSource.makeUserDataAddSource(queue: queue) + + var timerRef : CVDisplayLink? = nil + + var successLink = CVDisplayLinkCreateWithActiveCGDisplays(&timerRef) + + if let timer = timerRef + { + + successLink = CVDisplayLinkSetOutputCallback(timer, + { + (timer : CVDisplayLink, currentTime : UnsafePointer, outputTime : UnsafePointer, _ : CVOptionFlags, _ : UnsafeMutablePointer, sourceUnsafeRaw : UnsafeMutableRawPointer?) -> CVReturn in + + if let sourceUnsafeRaw = sourceUnsafeRaw + { + let sourceUnmanaged = Unmanaged.fromOpaque(sourceUnsafeRaw) + sourceUnmanaged.takeUnretainedValue().add(data: 1) + } + + return kCVReturnSuccess + + }, Unmanaged.passUnretained(source).toOpaque()) + + guard successLink == kCVReturnSuccess else + { + NSLog("Failed to create timer with active display") + return nil + } + + successLink = CVDisplayLinkSetCurrentCGDisplay(timer, CGMainDisplayID()) + + guard successLink == kCVReturnSuccess else + { + return nil + } + + self.timer = timer + } + else + { + return nil + } + source.setEventHandler(handler: + { + [weak self] in self?.callback?() + }) + } + + func start() { + guard !running else { return } + + CVDisplayLinkStart(timer) + source.resume() + } + + func cancel() + { + guard running else { return } + + CVDisplayLinkStop(timer) + source.cancel() + } + + deinit + { + if running + { + cancel() + } + } +} diff --git a/Telegram-Mac/DownloadSettingsViewController.swift b/Telegram-Mac/DownloadSettingsViewController.swift new file mode 100644 index 0000000000..3224b1ee23 --- /dev/null +++ b/Telegram-Mac/DownloadSettingsViewController.swift @@ -0,0 +1,218 @@ +// +// DownloadSettingsViewController.swift +// Telegram +// +// Created by Mikhail Filimonov on 30/01/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import SwiftSignalKit +import TGUIKit + +private final class DownloadSettingsArguments { + let context: AccountContext + let toggleCategory:(AutomaticMediaDownloadCategoryPeers)->Void + let togglePreloadLargeVideos:(Bool)->Void + init(_ context: AccountContext, toggleCategory: @escaping(AutomaticMediaDownloadCategoryPeers)->Void, togglePreloadLargeVideos: @escaping(Bool)->Void) { + self.context = context + self.toggleCategory = toggleCategory + self.togglePreloadLargeVideos = togglePreloadLargeVideos + } +} + +private enum DownloadSettingsEntry : TableItemListNodeEntry { + case contacts(sectionId: Int32, enabled: Bool, category: AutomaticMediaDownloadCategoryPeers, viewType: GeneralViewType) + case groupChats(sectionId: Int32, enabled: Bool, category: AutomaticMediaDownloadCategoryPeers, viewType: GeneralViewType) + case channels(sectionId: Int32, enabled: Bool, category: AutomaticMediaDownloadCategoryPeers, viewType: GeneralViewType) + case fileSizeLimitHeader(sectionId: Int32, viewType: GeneralViewType) + case fileSizeLimit(sectionId: Int32, limit: Int32, category: AutomaticMediaDownloadCategoryPeers, viewType: GeneralViewType) + case preloadLargeVideos(sectionId: Int32, Bool, Bool, viewType: GeneralViewType) + case preloadLargeVideosDesc(sectionId: Int32, String, viewType: GeneralViewType) + case sectionId(Int32) + + var stableId: Int32 { + switch self { + case .contacts: + return 0 + case .groupChats: + return 1 + case .channels: + return 2 + case .fileSizeLimitHeader: + return 3 + case .fileSizeLimit: + return 5 + case .preloadLargeVideos: + return 6 + case .preloadLargeVideosDesc: + return 7 + case .sectionId(let id): + return 1000 + id + } + } + + + func item(_ arguments: DownloadSettingsArguments, initialSize: NSSize) -> TableRowItem { + switch self { + case let .contacts(_, enabled, category, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.dataAndStorageCategorySettingsPrivateChats, type: .switchable(enabled), viewType: viewType, action: { + arguments.toggleCategory(category.withUpdatedPrivateChats(!enabled)) + }) + case let .groupChats(_, enabled, category, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.dataAndStorageCategorySettingsGroupChats, type: .switchable(enabled), viewType: viewType, action: { + arguments.toggleCategory(category.withUpdatedGroupChats(!enabled)) + }) + case let .channels(_, enabled, category, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.dataAndStorageCategorySettingsChannels, type: .switchable(enabled), viewType: viewType, action: { + arguments.toggleCategory(category.withUpdatedChannels(!enabled)) + }) + case let .fileSizeLimitHeader(_, viewType): + return GeneralTextRowItem(initialSize, text: L10n.dataAndStorageCateroryFileSizeLimitHeader, viewType: viewType) + case let .fileSizeLimit(_, limit, category, viewType): + let list:[Int32] = [Int32(1 * 1024 * 1024), Int32(5 * 1024 * 1024), Int32(10 * 1024 * 1024), Int32(50 * 1024 * 1024), Int32(100 * 1024 * 1024), Int32(300 * 1024 * 1024), Int32(500 * 1024 * 1024), Int32(2000 * 1024 * 1024)] + + var titles:[String] = [] + titles.append(String.prettySized(with: Int(limit))) + + return SelectSizeRowItem(initialSize, stableId: stableId, current: limit, sizes: list, hasMarkers: false, titles: titles, viewType: viewType, selectAction: { select in + arguments.toggleCategory(category.withUpdatedSizeLimit(list[select])) + }) + case let .preloadLargeVideos(_, enabled, value, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.dataAndStorageCategoryPreloadLargeVideos, type: .switchable(value), viewType: viewType, action: { + arguments.togglePreloadLargeVideos(!value) + }, enabled: enabled) + case let .preloadLargeVideosDesc(_, limit, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.dataAndStorageCategoryPreloadLargeVideosDesc(limit), viewType: viewType) + case .sectionId: + return GeneralRowItem(initialSize, height: 30, stableId: stableId, viewType: .separator) + } + } + + var index: Int32 { + switch self { + case let .contacts(sectionId, _, _, _): + return (sectionId * 1000) + stableId + case let .groupChats(sectionId, _, _, _): + return (sectionId * 1000) + stableId + case let .channels(sectionId, _, _, _): + return (sectionId * 1000) + stableId + case let .fileSizeLimitHeader(sectionId, _): + return (sectionId * 1000) + stableId + case let .fileSizeLimit(sectionId, _, _, _): + return (sectionId * 1000) + stableId + case let .preloadLargeVideos(sectionId, _, _, _): + return (sectionId * 1000) + stableId + case let .preloadLargeVideosDesc(sectionId, _, _): + return (sectionId * 1000) + stableId + case .sectionId(let sectionId): + return (sectionId + 1) * 1000 - sectionId + } + } +} + +private func <(lhs: DownloadSettingsEntry, rhs: DownloadSettingsEntry) -> Bool { + return lhs.index < rhs.index +} + + +private func downloadSettingsEntries(state: AutomaticMediaDownloadCategoryPeers, isVideo: Bool, autoplayMedia: AutoplayMediaPreferences) -> [DownloadSettingsEntry] { + var entries:[DownloadSettingsEntry] = [] + var sectionId:Int32 = 0 + entries.append(.sectionId(sectionId)) + sectionId += 1 + + entries.append(.contacts(sectionId: sectionId, enabled: state.privateChats, category: state, viewType: .firstItem)) + entries.append(.groupChats(sectionId: sectionId, enabled: state.groupChats, category: state, viewType: .innerItem)) + entries.append(.channels(sectionId: sectionId, enabled: state.channels, category: state, viewType: .lastItem)) + + if let fileSizeLimit = state.fileSize { + entries.append(.sectionId(sectionId)) + sectionId += 1 + + entries.append(.fileSizeLimitHeader(sectionId: sectionId, viewType: .textTopItem)) + + entries.append(.fileSizeLimit(sectionId: sectionId, limit: fileSizeLimit, category: state, viewType: isVideo ? .firstItem : .singleItem)) + + if isVideo { + let preloadEnabled = fileSizeLimit >= 5 * 1024 * 1024 + + entries.append(.preloadLargeVideos(sectionId: sectionId, preloadEnabled, autoplayMedia.preloadVideos, viewType: .lastItem)) + entries.append(.preloadLargeVideosDesc(sectionId: sectionId, "\(fileSizeLimit / 1024 / 1024)", viewType: .textBottomItem)) + } + + } + + entries.append(.sectionId(sectionId)) + sectionId += 1 + + return entries + +} + +fileprivate func prepareTransition(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], initialSize:NSSize, arguments:DownloadSettingsArguments) -> TableUpdateTransition { + + let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in + return entry.entry.item(arguments, initialSize: initialSize) + } + + return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true) +} + + + +class DownloadSettingsViewController: TableViewController { + private let disposable = MetaDisposable() + private let stateValue: ValuePromise + private let title: String + private let isVideo: Bool + private let updateCategory:(AutomaticMediaDownloadCategoryPeers)->Void + init(_ context: AccountContext, _ state: AutomaticMediaDownloadCategoryPeers, _ title: String, updateCategory:@escaping(AutomaticMediaDownloadCategoryPeers) -> Void) { + self.stateValue = ValuePromise(state, ignoreRepeated: true) + self.title = title + self.isVideo = L10n.dataAndStorageAutomaticDownloadVideo == title + self.updateCategory = updateCategory + super.init(context) + } + + override var defaultBarTitle: String { + return title + } + + override func viewDidLoad() { + super.viewDidLoad() + let context = self.context + + let arguments = DownloadSettingsArguments(context, toggleCategory: { [weak self] category in + self?.updateCategory(category) + self?.stateValue.set(category) + }, togglePreloadLargeVideos: { enabled in + _ = updateAutoplayMediaSettingsInteractively(postbox: context.account.postbox, { + $0.withUpdatedAutoplayPreloadVideos(enabled) + }).start() + }) + + let initialSize = self.atomicSize + let isVideo = self.isVideo + + let previous: Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) + + let signal = combineLatest(stateValue.get(), appearanceSignal, autoplayMediaSettings(postbox: context.account.postbox)) |> map { state, appearance, autoplayMedia -> TableUpdateTransition in + let entries = downloadSettingsEntries(state: state, isVideo: isVideo, autoplayMedia: autoplayMedia).map {AppearanceWrapperEntry(entry: $0, appearance: appearance)} + return prepareTransition(left: previous.swap(entries), right: entries, initialSize: initialSize.modify {$0}, arguments: arguments) + } |> deliverOnMainQueue + + disposable.set(signal.start(next: { [weak self] transition in + self?.genericView.merge(with: transition) + self?.readyOnce() + })) + + } + + deinit { + disposable.dispose() + } + +} diff --git a/Telegram-Mac/DownloadedFilesPaths.swift b/Telegram-Mac/DownloadedFilesPaths.swift new file mode 100644 index 0000000000..6e81d0e4eb --- /dev/null +++ b/Telegram-Mac/DownloadedFilesPaths.swift @@ -0,0 +1,104 @@ +// +// DownloadedFilesPaths.swift +// Telegram +// +// Created by Mikhail Filimonov on 13/03/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import Postbox +import SwiftSignalKit + +struct DownloadedPath : PostboxCoding, Equatable { + let id: MediaId + let downloadedPath: String + let size: Int32 + let lastModified: Int32 + init(id: MediaId, downloadedPath: String, size: Int32, lastModified: Int32) { + self.id = id + self.downloadedPath = downloadedPath + self.size = size + self.lastModified = lastModified + } + + + init(decoder: PostboxDecoder) { + self.id = decoder.decodeObjectForKey("id", decoder: { MediaId(decoder: $0) }) as! MediaId + self.downloadedPath = decoder.decodeStringForKey("dp", orElse: "") + self.size = decoder.decodeInt32ForKey("s", orElse: 0) + self.lastModified = decoder.decodeInt32ForKey("lm", orElse: 0) + } + + func encode(_ encoder: PostboxEncoder) { + encoder.encodeObject(self.id, forKey: "id") + encoder.encodeString(self.downloadedPath, forKey: "dp") + encoder.encodeInt32(self.size, forKey: "s") + encoder.encodeInt32(self.lastModified, forKey: "lm") + } +} + +struct DownloadedFilesPaths: PreferencesEntry, Equatable { + + private let paths: [DownloadedPath] + + static var defaultValue: DownloadedFilesPaths { + return DownloadedFilesPaths(paths: []) + } + + init(paths: [DownloadedPath]) { + self.paths = paths + } + + func isEqual(to: PreferencesEntry) -> Bool { + if let other = to as? DownloadedFilesPaths { + return other == self + } else { + return false + } + } + + init(decoder: PostboxDecoder) { + self.paths = decoder.decodeObjectArrayForKey("p") + } + + func encode(_ encoder: PostboxEncoder) { + encoder.encodeObjectArray(self.paths, forKey: "p") + } + + func path(for mediaId: MediaId) -> DownloadedPath? { + for path in paths { + if path.id == mediaId { + return path + } + } + return nil + } + + func withAddedPath(_ path: DownloadedPath) -> DownloadedFilesPaths { + var paths = self.paths + if let index = paths.firstIndex(where: {$0.id == path.id}) { + paths[index] = path + } else { + paths.append(path) + } + return DownloadedFilesPaths(paths: paths) + } +} + + +func downloadedFilePaths(_ postbox: Postbox) -> Signal { + return postbox.transaction { transaction in + return transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.downloadedPaths) as? DownloadedFilesPaths ?? DownloadedFilesPaths.defaultValue + } +} + +func updateDownloadedFilePaths(_ postbox: Postbox, _ f: @escaping(DownloadedFilesPaths) -> DownloadedFilesPaths) -> Signal { + return postbox.transaction { transaction in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.downloadedPaths, { entry in + let current = entry as? DownloadedFilesPaths ?? DownloadedFilesPaths.defaultValue + + return f(current) + }) + } |> ignoreValues +} diff --git a/Telegram-Mac/DynamicHeightRowItem.swift b/Telegram-Mac/DynamicHeightRowItem.swift new file mode 100644 index 0000000000..9d96e46685 --- /dev/null +++ b/Telegram-Mac/DynamicHeightRowItem.swift @@ -0,0 +1,60 @@ +// +// DynamicHeightRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 03/10/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + + +enum DynamicItemSide { + case top + case bottom +} +class DynamicHeightRowItem: GeneralRowItem { + private let side:DynamicItemSide + init(_ initialSize: NSSize, stableId: AnyHashable, side: DynamicItemSide) { + self.side = side + super.init(initialSize, stableId: stableId) + } + + override var height: CGFloat { + if let table = table { + var tableHeight: CGFloat = 0 + table.enumerateItems { item -> Bool in + if !item.reloadOnTableHeightChanged { + tableHeight += item.height + } + return true + } + + return max((table.frame.height - tableHeight) / 2, 0) + } else { + return 0 + } + } + + override var instantlyResize: Bool { + return true + } + override var reloadOnTableHeightChanged: Bool { + return true + } + + override func viewClass() -> AnyClass { + return DynamicHeightRowView.self + } +} + +private final class DynamicHeightRowView : TableRowView { + override func updateColors() { + + } + + override var firstResponder: NSResponder? { + return nil + } +} diff --git a/Telegram-Mac/EBlockItem.swift b/Telegram-Mac/EBlockItem.swift index 65b8f3f168..e448af5c33 100644 --- a/Telegram-Mac/EBlockItem.swift +++ b/Telegram-Mac/EBlockItem.swift @@ -8,7 +8,8 @@ import Cocoa import TGUIKit -import TelegramCoreMac +import TelegramCore + class EBlockItem: TableRowItem { var _stableId:Int64 = Int64(arc4random()) diff --git a/Telegram-Mac/EBlockRowView.swift b/Telegram-Mac/EBlockRowView.swift index 30a873e9a8..8d80e855fb 100644 --- a/Telegram-Mac/EBlockRowView.swift +++ b/Telegram-Mac/EBlockRowView.swift @@ -8,7 +8,7 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac +import SwiftSignalKit extension CATiledLayer { func fadeDuration() -> CFTimeInterval { @@ -16,7 +16,7 @@ extension CATiledLayer { } } -class ETiledLayer : CATiledLayer { +class ETiledLayer : CALayer { fileprivate var layoutNextRequest: Bool = true @@ -29,22 +29,23 @@ class ETiledLayer : CATiledLayer { // } } -private class EmojiSegmentView: NSView, CALayerDelegate { +private class EmojiSegmentView: View { fileprivate override var isFlipped: Bool { return true } - private let item:Atomic = Atomic(value: nil) + private var item: EBlockItem? - fileprivate func draw(_ layer: CALayer, in ctx: CGContext) { + fileprivate override func draw(_ layer: CALayer, in ctx: CGContext) { - if let item = item.modify({$0}) { + if let item = self.item { ctx.textMatrix = CGAffineTransform(scaleX: 1.0, y: -1.0) var ts:NSPoint = NSMakePoint(17, 29) for segment in item.lineAttr { for line in segment { + ctx.textPosition = ts CTLineDraw(CTLineCreateWithAttributedString(line), ctx) ts.x+=xAdd @@ -57,17 +58,9 @@ private class EmojiSegmentView: NSView, CALayerDelegate { } - var tiled:ETiledLayer = ETiledLayer() - required override init(frame frameRect: NSRect) { + required init(frame frameRect: NSRect) { super.init(frame: frameRect) - wantsLayer = true - self.layer?.addSublayer(tiled) - tiled.frame = self.bounds - tiled.levelsOfDetailBias = Int(backingScaleFactor) - self.tiled.delegate = self - - //tiled.shouldRasterize } required init?(coder: NSCoder) { @@ -76,31 +69,11 @@ private class EmojiSegmentView: NSView, CALayerDelegate { override func viewDidChangeBackingProperties() { super.viewDidChangeBackingProperties() - tiled.levelsOfDetailBias = Int(backingScaleFactor) } - override var needsDisplay: Bool { - get { - return super.needsDisplay - } - set { - super.needsDisplay = true - } - } - - override func setNeedsDisplay(_ invalidRect: NSRect) { - - } - - override func setFrameSize(_ newSize: NSSize) { - super.setFrameSize(newSize) - tiled.frame = bounds - tiled.tileSize = bounds.size - } func update(with item:EBlockItem?) -> Void { - _ = self.item.swap(item) - tiled.layoutNextRequest = true + self.item = item background = theme.colors.background self.needsDisplay = true } @@ -112,10 +85,8 @@ private let yAdd:CGFloat = 34 class EBlockRowView: TableRowView { - // var tiled:CATiledLayer = CATiledLayer() - var button:Control = Control() - private var segmentView:EmojiSegmentView = EmojiSegmentView() + private var segmentView:EmojiSegmentView = EmojiSegmentView(frame: NSZeroRect) var mouseDown:Bool = false private var popover: NSPopover? @@ -150,7 +121,7 @@ class EBlockRowView: TableRowView { func update(with location:NSPoint) -> Bool { - if self.mouse(location, in: self.visibleRect) { + if self.isMousePoint(location, in: self.visibleRect) { if let item = item as? EBlockItem { var point:NSPoint = location @@ -187,7 +158,7 @@ class EBlockRowView: TableRowView { if point != button.frame.origin { if self.button.isSelected { - button.layer?.animatePosition(from: button.frame.origin, to: point, duration: 0.1, timingFunction: kCAMediaTimingFunctionLinear) + button.layer?.animatePosition(from: button.frame.origin, to: point, duration: 0.1, timingFunction: CAMediaTimingFunctionName.linear) } button.frame = NSMakeRect(point.x, point.y, button.frame.width, button.frame.height) @@ -210,10 +181,11 @@ class EBlockRowView: TableRowView { if selectedEmoji.emojiUnmodified != selectedEmoji, let item = item as? EBlockItem { popover?.close() popover = NSPopover() - popover?.contentViewController = EmojiToleranceController(selectedEmoji.emojiUnmodified, postbox: item.account.postbox, handle: { [weak self, weak item] emoji in + popover?.contentViewController = EmojiToleranceController(selectedEmoji.emojiUnmodified, postbox: item.account.postbox, handle: { [weak self, weak item] emoji, modifier in if let item = item { - _ = modifySkinEmoji(emoji, postbox: item.account.postbox).start() + _ = modifySkinEmoji(emoji, modifier: modifier, postbox: item.account.postbox).start() } + self?.popover?.close() self?.popover = nil }) @@ -226,17 +198,21 @@ class EBlockRowView: TableRowView { self.button.isSelected = self.update(with: segmentView.convert(event.locationInWindow, from: nil)) let emoji = selectedEmoji - let lhs = emoji.emojiUnmodified.glyphCount - let rhs = ( emoji.emojiUnmodified + "🏻").glyphCount - longHandle.set((Signal.single(Void()) |> delay(0.3, queue: Queue.mainQueue())).start(next: { [weak self] in + let rhs = emoji.emojiUnmodified.emojiWithSkinModifier("🏻").glyphCount + longHandle.set((Signal.single(Void()) |> delay(0.3, queue: Queue.mainQueue())).start(next: { [weak self] in if let strongSelf = self, lhs == rhs, let item = self?.item as? EBlockItem { strongSelf.useEmoji = false strongSelf.popover?.close() strongSelf.popover = NSPopover() - strongSelf.popover?.contentViewController = EmojiToleranceController(emoji.emojiUnmodified, postbox: item.account.postbox, handle: { [weak strongSelf, weak item] emoji in + strongSelf.popover?.contentViewController = EmojiToleranceController(emoji.emojiUnmodified, postbox: item.account.postbox, handle: { [weak strongSelf, weak item] emoji, modifier in if let item = item { - _ = modifySkinEmoji(emoji, postbox: item.account.postbox).start() + _ = modifySkinEmoji(emoji, modifier: modifier, postbox: item.account.postbox).start() + if let modifier = modifier { + item.selectHandler(emoji + modifier) + } else { + item.selectHandler(emoji) + } } strongSelf?.popover?.close() strongSelf?.popover = nil @@ -296,6 +272,7 @@ class EBlockRowView: TableRowView { override func setFrameSize(_ newSize: NSSize) { super.setFrameSize(newSize) + segmentView.frame = bounds // tiled.frame = bounds // tiled.tileSize = bounds.size } diff --git a/Telegram-Mac/EDSunriseSet.h b/Telegram-Mac/EDSunriseSet.h new file mode 100644 index 0000000000..73f2714c08 --- /dev/null +++ b/Telegram-Mac/EDSunriseSet.h @@ -0,0 +1,51 @@ +// +// EDSunriseSet.h +// +// Created by Ernesto García on 20/08/11. +// Copyright 2011 Ernesto García. All rights reserved. +// + +// C/C++ sun calculations created by Paul Schlyter +// sunriset.c +// http://stjarnhimlen.se/english.html +// SUNRISET.C - computes Sun rise/set times, start/end of twilight, and +// the length of the day at any date and latitude +// Written as DAYLEN.C, 1989-08-16 +// Modified to SUNRISET.C, 1992-12-01 +// (c) Paul Schlyter, 1989, 1992 +// Released to the public domain by Paul Schlyter, December 1992 +// + +#import + +#if ! __has_feature(objc_arc) +#error This file must be compiled with ARC. Either turn on ARC for the project or use -fobjc-arc flag in this file. +#endif + +@interface EDSunriseSet : NSObject + +@property (readonly, strong) NSDate *date; +@property (readonly, strong) NSDate *sunset; +@property (readonly, strong) NSDate *sunrise; +@property (readonly, strong) NSDate *civilTwilightStart; +@property (readonly, strong) NSDate *civilTwilightEnd; +@property (readonly, strong) NSDate *nauticalTwilightStart; +@property (readonly, strong) NSDate *nauticalTwilightEnd; +@property (readonly, strong) NSDate *astronomicalTwilightStart; +@property (readonly, strong) NSDate *astronomicalTwilightEnd; + +@property (readonly, strong) NSDateComponents* localSunrise; +@property (readonly, strong) NSDateComponents* localSunset; +@property (readonly, strong) NSDateComponents* localCivilTwilightStart; +@property (readonly, strong) NSDateComponents* localCivilTwilightEnd; +@property (readonly, strong) NSDateComponents* localNauticalTwilightStart; +@property (readonly, strong) NSDateComponents* localNauticalTwilightEnd; +@property (readonly, strong) NSDateComponents* localAstronomicalTwilightStart; +@property (readonly, strong) NSDateComponents* localAstronomicalTwilightEnd; + + +-(instancetype)initWithDate:(NSDate*)date timezone:(NSTimeZone*)timezone latitude:(double)latitude longitude:(double)longitude NS_DESIGNATED_INITIALIZER; ++(instancetype)sunrisesetWithDate:(NSDate*)date timezone:(NSTimeZone*)timezone latitude:(double)latitude longitude:(double)longitude; +-(instancetype) init __attribute__((unavailable("init not available. Use initWithDate:timeZone:latitude:longitude: instead"))); + +@end diff --git a/Telegram-Mac/EDSunriseSet.m b/Telegram-Mac/EDSunriseSet.m new file mode 100644 index 0000000000..27b4632e0e --- /dev/null +++ b/Telegram-Mac/EDSunriseSet.m @@ -0,0 +1,447 @@ +// +// EDSunriseSet.m +// +// Created by Ernesto García on 20/08/11. +// Copyright 2011 Ernesto García. All rights reserved. +// + +// C/C++ sun calculations created by Paul Schlyter +// sunriset.c +// http://stjarnhimlen.se/english.html +// SUNRISET.C - computes Sun rise/set times, start/end of twilight, and +// the length of the day at any date and latitude +// Written as DAYLEN.C, 1989-08-16 +// Modified to SUNRISET.C, 1992-12-01 +// (c) Paul Schlyter, 1989, 1992 +// Released to the public domain by Paul Schlyter, December 1992 +// + +#import "EDSunriseSet.h" + +// +// Defines from sunriset.c +// +#define INV360 ( 1.0 / 360.0 ) + +#define RADEG ( 180.0 / M_PI ) +#define DEGRAD ( M_PI / 180.0 ) + +/* The trigonometric functions in degrees */ + +#define sind(x) sin((x)*DEGRAD) +#define cosd(x) cos((x)*DEGRAD) +#define tand(x) tan((x)*DEGRAD) + +#define atand(x) (RADEG*atan(x)) +#define asind(x) (RADEG*asin(x)) +#define acosd(x) (RADEG*acos(x)) +#define atan2d(y,x) (RADEG*atan2(y,x)) + +/* A macro to compute the number of days elapsed since 2000 Jan 0.0 */ +/* (which is equal to 1999 Dec 31, 0h UT) */ +#define days_since_2000_Jan_0(y,m,d) \ +(367L*(y)-((7*((y)+(((m)+9)/12)))/4)+((275*(m))/9)+(d)-730530L) + + +#if defined(__IPHONE_8_0) || defined (__MAC_10_10) +#define EDGregorianCalendar NSCalendarIdentifierGregorian +#else +#define EDGregorianCalendar NSGregorianCalendar +#endif + + +#pragma mark - Readwrite accessors only private +@interface EDSunriseSet() + +@property (nonatomic) double latitude; +@property (nonatomic) double longitude; +@property (nonatomic, strong) NSTimeZone *timezone; +@property (nonatomic, strong) NSCalendar *calendar; +@property (nonatomic, strong) NSTimeZone *utcTimeZone; + +@property (readwrite, strong) NSDate *date; +@property (readwrite, strong) NSDate *sunset; +@property (readwrite, strong) NSDate *sunrise; +@property (readwrite, strong) NSDate *civilTwilightStart; +@property (readwrite, strong) NSDate *civilTwilightEnd; +@property (readwrite, strong) NSDate *nauticalTwilightStart; +@property (readwrite, strong) NSDate *nauticalTwilightEnd; +@property (readwrite, strong) NSDate *astronomicalTwilightStart; +@property (readwrite, strong) NSDate *astronomicalTwilightEnd; + +@property (readwrite, strong) NSDateComponents* localSunrise; +@property (readwrite, strong) NSDateComponents* localSunset; +@property (readwrite, strong) NSDateComponents* localCivilTwilightStart; +@property (readwrite, strong) NSDateComponents* localCivilTwilightEnd; +@property (readwrite, strong) NSDateComponents* localNauticalTwilightStart; +@property (readwrite, strong) NSDateComponents* localNauticalTwilightEnd; +@property (readwrite, strong) NSDateComponents* localAstronomicalTwilightStart; +@property (readwrite, strong) NSDateComponents* localAstronomicalTwilightEnd; + +@end + +#pragma mark - Calculations from sunriset.c +@implementation EDSunriseSet(Calculations) + +/*****************************************/ +/* Reduce angle to within 0..360 degrees */ +/*****************************************/ +-(double) revolution:(double) x +{ + return( x - 360.0 * floor( x * INV360 ) ); +} + +/*********************************************/ +/* Reduce angle to within -180..+180 degrees */ +/*********************************************/ +-(double) rev180:(double) x +{ + return( x - 360.0 * floor( x * INV360 + 0.5 ) ); +} + +-(double) GMST0:(double) d +{ + double sidtim0; + /* Sidtime at 0h UT = L (Sun's mean longitude) + 180.0 degr */ + /* L = M + w, as defined in sunpos(). Since I'm too lazy to */ + /* add these numbers, I'll let the C compiler do it for me. */ + /* Any decent C compiler will add the constants at compile */ + /* time, imposing no runtime or code overhead. */ + sidtim0 = [self revolution: ( 180.0 + 356.0470 + 282.9404 ) + + ( 0.9856002585 + 4.70935E-5 ) * d]; + return sidtim0; +} + +/******************************************************/ +/* Computes the Sun's ecliptic longitude and distance */ +/* at an instant given in d, number of days since */ +/* 2000 Jan 0.0. The Sun's ecliptic latitude is not */ +/* computed, since it's always very near 0. */ +/******************************************************/ +-(void) sunposAtDay:(double)d longitude:(double*)lon r:(double *)r +{ + double M, /* Mean anomaly of the Sun */ + w, /* Mean longitude of perihelion */ + /* Note: Sun's mean longitude = M + w */ + e, /* Eccentricity of Earth's orbit */ + E, /* Eccentric anomaly */ + x, y, /* x, y coordinates in orbit */ + v; /* True anomaly */ + + /* Compute mean elements */ + M = [self revolution:( 356.0470 + 0.9856002585 * d )]; + w = 282.9404 + 4.70935E-5 * d; + e = 0.016709 - 1.151E-9 * d; + + /* Compute true longitude and radius vector */ + E = M + e * RADEG * sind(M) * ( 1.0 + e * cosd(M) ); + x = cosd(E) - e; + y = sqrt( 1.0 - e*e ) * sind(E); + *r = sqrt( x*x + y*y ); /* Solar distance */ + v = atan2d( y, x ); /* True anomaly */ + *lon = v + w; /* True solar longitude */ + if ( *lon >= 360.0 ) + *lon -= 360.0; /* Make it 0..360 degrees */ +} + +-(void) sun_RA_decAtDay:(double)d RA:(double*)RA decl:(double *)dec r:(double *)r +{ + double lon, obl_ecl; + double xs, ys, zs; + double xe, ye, ze; + + /* Compute Sun's ecliptical coordinates */ + //sunpos( d, &lon, r ); + [self sunposAtDay:d longitude:&lon r:r]; + + /* Compute ecliptic rectangular coordinates */ + xs = *r * cosd(lon); + ys = *r * sind(lon); + zs = 0; /* because the Sun is always in the ecliptic plane! */ + + /* Compute obliquity of ecliptic (inclination of Earth's axis) */ + obl_ecl = 23.4393 - 3.563E-7 * d; + + /* Convert to equatorial rectangular coordinates - x is unchanged */ + xe = xs; + ye = ys * cosd(obl_ecl); + ze = ys * sind(obl_ecl); + + /* Convert to spherical coordinates */ + *RA = atan2d( ye, xe ); + *dec = atan2d( ze, sqrt(xe*xe + ye*ye) ); + +} /* sun_RA_dec */ + +#define sun_rise_set(year,month,day,lon,lat,rise,set) \ +__sunriset__( year, month, day, lon, lat, -35.0/60.0, 1, rise, set ) + +-(int)sunRiseSetForYear:(int)year month:(int)month day:(int)day longitude:(double)lon latitude:(double)lat + trise:(double *)trise tset:(double *)tset +{ + + return [self sunRiseSetHelperForYear:year month:month day:day longitude:lon latitude:lat altitude:(-35.0/60.0) + upper_limb:1 trise:trise tset:tset]; + +} +/* + #define civil_twilight(year,month,day,lon,lat,start,end) \ + __sunriset__( year, month, day, lon, lat, -6.0, 0, start, end ) + */ +-(int) civilTwilightForYear:(int)year month:(int)month day:(int)day longitude:(double)lon latitude:(double)lat + trise:(double *)trise tset:(double *)tset +{ + return [self sunRiseSetHelperForYear:year month:month day:day longitude:lon latitude:lat altitude:-6.0 + upper_limb:0 trise:trise tset:tset]; +} +/* + #define nautical_twilight(year,month,day,lon,lat,start,end) \ + __sunriset__( year, month, day, lon, lat, -12.0, 0, start, end ) + */ +-(int) nauticalTwilightForYear:(int)year month:(int)month day:(int)day longitude:(double)lon latitude:(double)lat + trise:(double *)trise tset:(double *)tset +{ + return [self sunRiseSetHelperForYear:year month:month day:day longitude:lon latitude:lat altitude:-12.0 + upper_limb:0 trise:trise tset:tset]; +} +/* + #define astronomical_twilight(year,month,day,lon,lat,start,end) \ + __sunriset__( year, month, day, lon, lat, -18.0, 0, start, end ) + */ +-(int) astronomicalTwilightForYear:(int)year month:(int)month day:(int)day longitude:(double)lon latitude:(double)lat + trise:(double *)trise tset:(double *)tset +{ + return [self sunRiseSetHelperForYear:year month:month day:day longitude:lon latitude:lat altitude:-18.0 + upper_limb:0 trise:trise tset:tset]; +} + +/***************************************************************************/ +/* Note: year,month,date = calendar date, 1801-2099 only. */ +/* Eastern longitude positive, Western longitude negative */ +/* Northern latitude positive, Southern latitude negative */ +/* The longitude value IS critical in this function! */ +/* altit = the altitude which the Sun should cross */ +/* Set to -35/60 degrees for rise/set, -6 degrees */ +/* for civil, -12 degrees for nautical and -18 */ +/* degrees for astronomical twilight. */ +/* upper_limb: non-zero -> upper limb, zero -> center */ +/* Set to non-zero (e.g. 1) when computing rise/set */ +/* times, and to zero when computing start/end of */ +/* twilight. */ +/* *rise = where to store the rise time */ +/* *set = where to store the set time */ +/* Both times are relative to the specified altitude, */ +/* and thus this function can be used to comupte */ +/* various twilight times, as well as rise/set times */ +/* Return value: 0 = sun rises/sets this day, times stored at */ +/* *trise and *tset. */ +/* +1 = sun above the specified "horizon" 24 hours. */ +/* *trise set to time when the sun is at south, */ +/* minus 12 hours while *tset is set to the south */ +/* time plus 12 hours. "Day" length = 24 hours */ +/* -1 = sun is below the specified "horizon" 24 hours */ +/* "Day" length = 0 hours, *trise and *tset are */ +/* both set to the time when the sun is at south. */ +/* */ +/**********************************************************************/ +-(int)sunRiseSetHelperForYear:(int)year month:(int)month day:(int)day longitude:(double)lon latitude:(double)lat + altitude:(double)altit upper_limb:(int)upper_limb trise:(double *)trise tset:(double *)tset +{ + double d, /* Days since 2000 Jan 0.0 (negative before) */ + sr, /* Solar distance, astronomical units */ + sRA, /* Sun's Right Ascension */ + sdec, /* Sun's declination */ + sradius, /* Sun's apparent radius */ + t, /* Diurnal arc */ + tsouth, /* Time when Sun is at south */ + sidtime; /* Local sidereal time */ + + int rc = 0; /* Return cde from function - usually 0 */ + + /* Compute d of 12h local mean solar time */ + d = days_since_2000_Jan_0(year,month,day) + 0.5 - lon/360.0; + + + /* Compute local sideral time of this moment */ + //sidtime = revolution( GMST0(d) + 180.0 + lon ); + sidtime = [self revolution:[self GMST0:d] + 180.0 + lon]; + /* Compute Sun's RA + Decl at this moment */ + //sun_RA_dec( d, &sRA, &sdec, &sr ); + [self sun_RA_decAtDay:d RA: &sRA decl:&sdec r:&sr]; + + /* Compute time when Sun is at south - in hours UT */ + //tsouth = 12.0 - rev180(sidtime - sRA)/15.0; + tsouth = 12.0 - [self rev180:sidtime - sRA] / 15.0; + + /* Compute the Sun's apparent radius, degrees */ + sradius = 0.2666 / sr; + + /* Do correction to upper limb, if necessary */ + if ( upper_limb ) + altit -= sradius; + + /* Compute the diurnal arc that the Sun traverses to reach */ + /* the specified altitide altit: */ + { + double cost; + cost = ( sind(altit) - sind(lat) * sind(sdec) ) / + ( cosd(lat) * cosd(sdec) ); + if ( cost >= 1.0 ) + rc = -1, t = 0.0; /* Sun always below altit */ + else if ( cost <= -1.0 ) + rc = +1, t = 12.0; /* Sun always above altit */ + else + t = acosd(cost)/15.0; /* The diurnal arc, hours */ + } + + /* Store rise and set times - in hours UT */ + *trise = tsouth - t; + *tset = tsouth + t; + + return rc; +} /* __sunriset__ */ + + +@end + + +#pragma mark - Private Implementation + +@implementation EDSunriseSet(Private) + +static const int kSecondsInHour= 60.0*60.0; + + +-(NSDate*)utcTime:(NSDateComponents*)dateComponents withOffset:(NSTimeInterval)interval +{ + [self.calendar setTimeZone:self.utcTimeZone]; + return [[self.calendar dateFromComponents:dateComponents] dateByAddingTimeInterval:(NSTimeInterval)(interval)]; +} + +-(NSDateComponents*)localTime:(NSDate*)refDate +{ + [self.calendar setTimeZone:self.timezone]; + // Return only hour, minute, seconds + NSDateComponents *dc = [self.calendar components:( NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond) fromDate:refDate] ; + + return dc; +} + +- (instancetype) init { + [super doesNotRecognizeSelector:_cmd]; + return nil; +} + +-(NSString *)description +{ + return [NSString stringWithFormat: + @"Date: %@\nTimeZone: %@\n" + @"Local Sunrise: %@\n" + @"Local Sunset: %@\n" + @"Local Civil Twilight Start: %@\n" + @"Local Civil Twilight End: %@\n" + @"Local Nautical Twilight Start: %@\n" + @"Local Nautical Twilight End: %@\n" + @"Local Astronomical Twilight Start: %@\n" + @"Local Astronomical Twilight End: %@\n", + self.date.description, self.timezone.name, + self.localSunrise.description, self.localSunset.description, + self.localCivilTwilightStart, self.localCivilTwilightEnd, + self.localNauticalTwilightStart, self.localNauticalTwilightEnd, + self.localAstronomicalTwilightStart, self.localAstronomicalTwilightEnd + ]; +} + +#pragma mark - Calculation methods + +-(void)calculateSunriseSunset +{ + // Get date components + [self.calendar setTimeZone:self.timezone]; + NSDateComponents *dateComponents = [self.calendar components:( NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay ) fromDate:self.date]; + + // Calculate sunrise and sunset + double rise=0.0, set=0.0; + [self sunRiseSetForYear:(int)[dateComponents year] month:(int)[dateComponents month] day:(int)[dateComponents day] longitude:self.longitude latitude:self.latitude + trise:&rise tset:&set ]; + NSTimeInterval secondsRise = rise*kSecondsInHour; + NSTimeInterval secondsSet = set*kSecondsInHour; + + self.sunrise = [self utcTime:dateComponents withOffset:(NSTimeInterval)secondsRise]; + self.sunset = [self utcTime:dateComponents withOffset:(NSTimeInterval)secondsSet]; + self.localSunrise = [self localTime:self.sunrise]; + self.localSunset = [self localTime:self.sunset]; +} + +-(void)calculateTwilight +{ + // Get date components + [self.calendar setTimeZone:self.timezone]; + NSDateComponents *dateComponents = [self.calendar components:( NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay ) fromDate:self.date]; + double start=0.0, end=0.0; + + // Civil twilight + [self civilTwilightForYear:(int)[dateComponents year] month:(int)[dateComponents month] day:(int)[dateComponents day] longitude:self.longitude latitude:self.latitude + trise:&start tset:&end ]; + self.civilTwilightStart = [self utcTime:dateComponents withOffset:(NSTimeInterval)(start*kSecondsInHour)]; + self.civilTwilightEnd = [self utcTime:dateComponents withOffset:(NSTimeInterval)(end*kSecondsInHour)]; + self.localCivilTwilightStart = [self localTime:self.civilTwilightStart]; + self.localCivilTwilightEnd = [self localTime:self.civilTwilightEnd]; + + // Nautical twilight + [self nauticalTwilightForYear:(int)[dateComponents year] month:(int)[dateComponents month] day:(int)[dateComponents day] longitude:self.longitude latitude:self.latitude + trise:&start tset:&end ]; + self.nauticalTwilightStart = [self utcTime:dateComponents withOffset:(NSTimeInterval)(start*kSecondsInHour)]; + self.nauticalTwilightEnd = [self utcTime:dateComponents withOffset:(NSTimeInterval)(end*kSecondsInHour)]; + self.localNauticalTwilightStart = [self localTime:self.nauticalTwilightStart]; + self.localNauticalTwilightEnd = [self localTime:self.nauticalTwilightEnd]; + // Astronomical twilight + [self astronomicalTwilightForYear:(int)[dateComponents year] month:(int)[dateComponents month] day:(int)[dateComponents day] longitude:self.longitude latitude:self.latitude + trise:&start tset:&end ]; + self.astronomicalTwilightStart = [self utcTime:dateComponents withOffset:(NSTimeInterval)(start*kSecondsInHour)]; + self.astronomicalTwilightEnd = [self utcTime:dateComponents withOffset:(NSTimeInterval)(end*kSecondsInHour)]; + self.localAstronomicalTwilightStart = [self localTime:self.astronomicalTwilightStart]; + self.localAstronomicalTwilightEnd = [self localTime:self.astronomicalTwilightEnd]; +} + +-(void)calculate +{ + [self calculateSunriseSunset]; + [self calculateTwilight]; +} + +@end + + +#pragma mark - Public Implementation + +@implementation EDSunriseSet + +#pragma mark - Initialization + +-(EDSunriseSet*)initWithDate:(NSDate*)date timezone:(NSTimeZone*)tz latitude:(double)latitude longitude:(double)longitude { + self = [super init]; + if( self ) + { + self.latitude = latitude; + self.longitude = longitude; + self.timezone = tz; + self.date = date; + + self.calendar = [[NSCalendar alloc] initWithCalendarIdentifier:EDGregorianCalendar]; + self.utcTimeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; + + [self calculate]; + + } + return self; +} + ++(EDSunriseSet*)sunrisesetWithDate:(NSDate*)date timezone:(NSTimeZone*)tz latitude:(double)latitude longitude:(double)longitude { + return [[EDSunriseSet alloc] initWithDate:date timezone:tz latitude:latitude longitude:longitude]; +} + +@end + + + diff --git a/Telegram-Mac/EStickView.swift b/Telegram-Mac/EStickView.swift index bf1e5291a7..29a9731807 100644 --- a/Telegram-Mac/EStickView.swift +++ b/Telegram-Mac/EStickView.swift @@ -12,12 +12,22 @@ class EStickView: TableStickView { required init(frame frameRect: NSRect) { super.init(frame: frameRect) + layerContentsRedrawPolicy = .onSetNeedsDisplay } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override var backdorColor: NSColor { + return theme.colors.background + } + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + needsDisplay = true + } + override func draw(_ layer: CALayer, in ctx: CGContext) { super.draw(layer, in: ctx) @@ -30,7 +40,7 @@ class EStickView: TableStickView { var f = focus(item.layout.0.size) f.origin.x = 20 f.origin.y -= 1 - item.layout.1.draw(f, in: ctx, backingScaleFactor: backingScaleFactor) + item.layout.1.draw(f, in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backdorColor) } } diff --git a/Telegram-Mac/EStickerGridEntries.swift b/Telegram-Mac/EStickerGridEntries.swift deleted file mode 100644 index 52ab749555..0000000000 --- a/Telegram-Mac/EStickerGridEntries.swift +++ /dev/null @@ -1,279 +0,0 @@ -// -// StickerGridEntries.swift -// Telegram-Mac -// -// Created by keepcoder on 23/10/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac -import TGUIKit - -enum ChatMediaInputGridEntryStableId : Hashable { - - case sticker(ItemCollectionId, ItemCollectionItemIndex.Id) - case speficicSticker(ItemCollectionId, ItemCollectionItemIndex.Id) - case recent(TelegramMediaFile) - case saved(TelegramMediaFile) - - static func ==(lhs: ChatMediaInputGridEntryStableId, rhs: ChatMediaInputGridEntryStableId) -> Bool { - switch lhs { - case let .sticker(lhsItemCollectionId, lhsItemId): - if case let .sticker(rhsItemCollectionId, rhsItemId) = rhs { - return lhsItemCollectionId == rhsItemCollectionId && lhsItemId == rhsItemId - } else { - return false - } - case let .speficicSticker(lhsItemCollectionId, lhsItemId): - if case let .speficicSticker(rhsItemCollectionId, rhsItemId) = rhs { - return lhsItemCollectionId == rhsItemCollectionId && lhsItemId == rhsItemId - } else { - return false - } - case let .recent(lhsFile): - if case let .recent(rhsFile) = rhs { - return lhsFile.isEqual(rhsFile) - } else { - return false - } - case let .saved(lhsFile): - if case let .saved(rhsFile) = rhs { - return lhsFile.isEqual(rhsFile) - } else { - return false - } - - } - } - - var hashValue: Int { - switch self { - case let .sticker(_, itemId): - return itemId.hashValue - case let .speficicSticker(_, itemId): - return itemId.hashValue - case let .recent(file): - return file.fileId.hashValue - case let .saved(file): - return file.fileId.hashValue - } - // return self.itemId.hashValue - } -} - -enum ChatMediaGridPackHeaderInfo { - case pack(StickerPackCollectionInfo?) - case speficicPack(StickerPackCollectionInfo?) - case recent - case saved -} - -extension ChatMediaGridPackHeaderInfo { - var title:String { - switch self { - case let .pack(info): - if let info = info { - return info.title.uppercased() - } else { - return "" - } - case .recent: - return tr(.stickersRecent) - case .saved: - return "tr(.stickersFavorite)" - case .speficicPack: - return tr(.stickersGroupStickers) - } - } -} - -enum ChatMediaGridCollectionStableId : Hashable { - case pack(ItemCollectionId) - case recent - case specificPack(ItemCollectionId) - case saved - - var hashValue: Int { - switch self { - case let .pack(collectionId): - return collectionId.hashValue - case let .specificPack(collectionId): - return collectionId.hashValue - case .recent: - return 1 - case .saved: - return 2 - } - } - - var itemCollectionId:ItemCollectionId? { - switch self { - case let .pack(collectionId): - return collectionId - case let .specificPack(collectionId): - return collectionId - - default: - return nil - } - } - - - static func ==(lhs: ChatMediaGridCollectionStableId, rhs: ChatMediaGridCollectionStableId) -> Bool { - switch lhs { - case let .pack(lhsCollectionId): - if case let .pack(rhsCollectionId) = rhs { - return lhsCollectionId == rhsCollectionId - } else { - return false - } - case .recent: - if case .recent = rhs { - return true - } else { - return false - } - case .saved: - if case .saved = rhs { - return true - } else { - return false - } - case let .specificPack(collectionId): - if case .specificPack(collectionId) = rhs { - return true - } else { - return false - } - } - } -} - -enum ChatMediaInputGridIndex : Hashable, Comparable { - case sticker(ItemCollectionViewEntryIndex) - case speficicSticker(ItemCollectionItemIndex) - case recent(Int) - case saved(Int) - - var packIndex:ItemCollectionViewEntryIndex { - switch self { - case let .sticker(index): - return index - case .saved(let index), .recent(let index): - return ItemCollectionViewEntryIndex.lowerBound(collectionIndex: Int32(index), collectionId: ItemCollectionId(namespace: 0, id: 0)) - case .speficicSticker: - return ItemCollectionViewEntryIndex.lowerBound(collectionIndex: 3, collectionId: ItemCollectionId(namespace: 0, id: 0)) - } - } - - - var hashValue: Int { - switch self { - case let .sticker(index): - return Int(index.itemIndex.index) - case let .recent(index): - return index - case let .saved(index): - return index - case .speficicSticker(let index): - return index.hashValue - } - } - - static func ==(lhs: ChatMediaInputGridIndex, rhs: ChatMediaInputGridIndex) -> Bool { - switch lhs { - case let .sticker(lhsIndex): - if case let .sticker(rhsIndex) = rhs { - return lhsIndex == rhsIndex - } else { - return false - } - case let .recent(lhsIndex): - if case let .recent(rhsIndex) = rhs { - return lhsIndex == rhsIndex - } else { - return false - } - case let .speficicSticker(index): - if case .speficicSticker(index) = rhs { - return true - } else { - return false - } - case let .saved(lhsIndex): - if case let .saved(rhsIndex) = rhs { - return lhsIndex == rhsIndex - } else { - return false - } - } - } - - static func <(lhs: ChatMediaInputGridIndex, rhs: ChatMediaInputGridIndex) -> Bool { - switch lhs { - case let .recent(lhsIndex): - if case let .recent(rhsIndex) = rhs { - return lhsIndex < rhsIndex - } else { - switch rhs { - case .saved: - return false - default: - return true - } - } - case let .sticker(lhsIndex): - if case let .sticker(rhsIndex) = rhs { - return lhsIndex < rhsIndex - } else { - switch rhs { - case .recent, .saved: - return true - default: - return false - } - } - case let .saved(lhsIndex): - if case let .saved(rhsIndex) = rhs { - return lhsIndex < rhsIndex - } else { - return true - } - case let .speficicSticker(lhsIndex): - if case let .speficicSticker(rhsIndex) = rhs { - return lhsIndex < rhsIndex - } else { - return true - } - } - } -} - -struct ChatMediaInputGridEntry: Comparable, Identifiable { - - - let index: ChatMediaInputGridIndex - let file: TelegramMediaFile - let packInfo: ChatMediaGridPackHeaderInfo - let _stableId:ChatMediaInputGridEntryStableId - let collectionId:ChatMediaGridCollectionStableId - - var stableId: ChatMediaInputGridEntryStableId { - return _stableId //ChatMediaInputGridEntryStableId(collectionId: self.index.collectionId, itemId: self.stickerItem.index.id) - } - - static func ==(lhs: ChatMediaInputGridEntry, rhs: ChatMediaInputGridEntry) -> Bool { - return lhs.file.isEqual(rhs.file) && lhs.collectionId == rhs.collectionId - } - - static func <(lhs: ChatMediaInputGridEntry, rhs: ChatMediaInputGridEntry) -> Bool { - return lhs.index < rhs.index - } - - func item(account: Account, inputNodeInteraction: EStickersInteraction) -> GridItem { - return StickerGridItem(account: account, collectionId: self.collectionId, packInfo: packInfo, index: self.index, file: self.file, inputNodeInteraction: inputNodeInteraction, selected: { }) - } -} diff --git a/Telegram-Mac/EStickerGridItem.swift b/Telegram-Mac/EStickerGridItem.swift deleted file mode 100644 index 66afbe1a47..0000000000 --- a/Telegram-Mac/EStickerGridItem.swift +++ /dev/null @@ -1,243 +0,0 @@ -// -// StickerGridItem.swift -// Telegram-Mac -// -// Created by keepcoder on 23/10/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac -import TGUIKit - -final class StickerGridSection: GridSection { - let collectionId: ChatMediaGridCollectionStableId - let height: CGFloat = 30 - let reference:StickerPackReference? - let packInfo: ChatMediaGridPackHeaderInfo - let inputInteraction:EStickersInteraction - var hashValue: Int { - return self.collectionId.hashValue - } - - init(collectionId: ChatMediaGridCollectionStableId, packInfo: ChatMediaGridPackHeaderInfo, inputInteraction: EStickersInteraction, reference: StickerPackReference?) { - self.packInfo = packInfo - self.collectionId = collectionId - self.reference = reference - self.inputInteraction = inputInteraction - } - - func isEqual(to: GridSection) -> Bool { - if let to = to as? StickerGridSection { - return self.collectionId == to.collectionId - } else { - return false - } - } - - func node() -> View { - return StickerGridSectionNode(collectionInfo: self) - } -} - - -final class StickerGridSectionNode: View { - var textView:TextView = TextView() - private let collectionInfo:StickerGridSection - init(collectionInfo: StickerGridSection) { - self.collectionInfo = collectionInfo - self.textView.userInteractionEnabled = false - super.init() - addSubview(textView) - updateLocalizationAndTheme() - } - override func updateLocalizationAndTheme() { - backgroundColor = theme.colors.background - textView.backgroundColor = theme.colors.background - let textLayout = TextViewLayout(.initialize(string: collectionInfo.packInfo.title.uppercased(), color: theme.colors.grayText, font: .medium(.title)), constrainedWidth: 300, maximumNumberOfLines: 1, truncationType: .end) - textLayout.measure() - textView.update(textLayout) - needsLayout = true - } - - override func layout() { - super.layout() - textView.centerY(x:10) - } - - override func mouseUp(with event: NSEvent) { - if mouseInside() && event.clickCount == 1, let reference = collectionInfo.reference { - self.collectionInfo.inputInteraction.previewStickerSet(reference) - } - } - - required init(frame frameRect: NSRect) { - fatalError("init(frame:) has not been implemented") - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - -} - -final class StickerGridItem: GridItem { - - let account: Account - let index: ChatMediaInputGridIndex - let file: TelegramMediaFile - let selected: () -> Void - let inputNodeInteraction: EStickersInteraction - let collectionId:ChatMediaGridCollectionStableId - let section: GridSection? - - init(account: Account, collectionId: ChatMediaGridCollectionStableId, packInfo: ChatMediaGridPackHeaderInfo, index: ChatMediaInputGridIndex, file: TelegramMediaFile, inputNodeInteraction: EStickersInteraction, selected: @escaping () -> Void) { - self.account = account - self.index = index - self.file = file - self.collectionId = collectionId - self.inputNodeInteraction = inputNodeInteraction - self.selected = selected - - - let reference: StickerPackReference? - switch packInfo { - case .recent: - reference = nil - case .pack: - reference = file.stickerReference - case .saved: - reference = nil - case .speficicPack: - reference = file.stickerReference - } - if collectionId != .saved { - self.section = StickerGridSection(collectionId: collectionId, packInfo: packInfo, inputInteraction: inputNodeInteraction, reference: reference) - } else { - self.section = nil - } - } - - func node(layout: GridNodeLayout, gridNode:GridNode) -> GridItemNode { - let node = StickerGridItemView(gridNode) - node.inputNodeInteraction = self.inputNodeInteraction - node.setup(account: self.account, file: self.file, collectionId: self.collectionId) - node.selected = self.selected - return node - } - - func update(node: GridItemNode) { - guard let node = node as? StickerGridItemView else { - assertionFailure() - return - } - node.setup(account: self.account, file: self.file, collectionId: self.collectionId) - node.selected = self.selected - } -} - -let eStickerSize:NSSize = NSMakeSize(80, 80) - - - -final class StickerGridItemView: GridItemNode, StickerPreviewRowViewProtocol { - private var currentState: (Account, TelegramMediaFile, CGSize, ChatMediaGridCollectionStableId?)? - - - private let imageView: TransformImageView - - func fileAtPoint(_ point: NSPoint) -> TelegramMediaFile? { - return currentState?.1 - } - - override func menu(for event: NSEvent) -> NSMenu? { - if let currentState = currentState, let state = currentState.3 { - let menu = NSMenu() - let file = currentState.1 - if state == .recent { - if let reference = file.stickerReference, case let .id(id, _) = reference { - menu.addItem(ContextMenuItem(tr(.contextViewStickerSet), handler: { [weak self] in - self?.inputNodeInteraction?.navigateToCollectionId(.pack(ItemCollectionId.init(namespace: Namespaces.ItemCollection.CloudStickerPacks, id: id))) - })) - } - } else if state == .saved, let mediaId = file.id { - menu.addItem(ContextMenuItem(tr(.contextRemoveFaveSticker), handler: { - _ = removeSavedSticker(postbox: currentState.0.postbox, mediaId: mediaId).start() - })) - } - - return menu - } - return nil - } - - private let stickerFetchedDisposable = MetaDisposable() - - var inputNodeInteraction: EStickersInteraction? - var selected: (() -> Void)? - - override init(_ grid:GridNode) { - imageView = TransformImageView() - super.init(grid) - layer?.cornerRadius = .cornerRadius - self.autohighlight = false - - set(handler: { [weak self] _ in - if let (_, file, _, _) = self?.currentState { - self?.inputNodeInteraction?.sendSticker(file) - } - }, for: .Click) - - - set(handler: { [weak self] (control) in - if let window = self?.window as? Window, let currentState = self?.currentState, let grid = self?.grid { - _ = startStickerPreviewHandle(grid, window: window, account: currentState.0) - } - }, for: .LongMouseDown) - set(background: theme.colors.background, for: .Normal) - } - - override func setFrameSize(_ newSize: NSSize) { - super.setFrameSize(newSize) - imageView.center() - - } - - required init(frame frameRect: NSRect) { - fatalError("init(frame:) has not been implemented") - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - stickerFetchedDisposable.dispose() - } - - func setup(account: Account, file: TelegramMediaFile, collectionId: ChatMediaGridCollectionStableId? = nil) { - if let dimensions = file.dimensions { - addSubview(imageView) - - set(image: theme.icons.stickerBackgroundActive, for: .Highlight) - set(image: theme.icons.stickerBackground, for: .Normal) - set(background: theme.colors.background, for: .Normal) - set(background: theme.colors.background, for: .Hover) - - imageView.setSignal(account: account, signal: chatMessageSticker(account: account, file: file, type: .small, scale: backingScaleFactor)) - stickerFetchedDisposable.set(fileInteractiveFetched(account: account, file: file).start()) - - let imageSize = dimensions.aspectFitted(eStickerSize) - imageView.set(arguments: TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: eStickerSize, intrinsicInsets: NSEdgeInsets())) - - imageView.setFrameSize(imageSize) - currentState = (account, file, dimensions, collectionId) - return - } - imageView.removeFromSuperview() - } - - -} diff --git a/Telegram-Mac/EStickerPackEntries.swift b/Telegram-Mac/EStickerPackEntries.swift deleted file mode 100644 index 90149d3b73..0000000000 --- a/Telegram-Mac/EStickerPackEntries.swift +++ /dev/null @@ -1,106 +0,0 @@ -// -// StickerPackEntries.swift -// Telegram-Mac -// -// Created by keepcoder on 25/10/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac -import TGUIKit - - - - - -enum ChatMediaInputPanelEntry: Comparable, Identifiable { - case stickerPack(index:Int, stableId: ChatMediaGridCollectionStableId, info: StickerPackCollectionInfo, topItem: StickerPackItem?) - case recent - case saved - case specificPack(info: StickerPackCollectionInfo, peer: Peer) - var stableId: ChatMediaGridCollectionStableId { - switch self { - case let .stickerPack(data): - return data.stableId - case .recent: - return .recent - case .saved: - return .saved - case let .specificPack(info, _): - return .specificPack(info.id) - - } - } - - static func ==(lhs: ChatMediaInputPanelEntry, rhs: ChatMediaInputPanelEntry) -> Bool { - switch lhs { - case let .stickerPack(lhsIndex, lhsStableId, lhsInfo, lhsTopItem): - if case let .stickerPack(rhsIndex, rhsStableId, rhsInfo, rhsTopItem) = rhs { - return lhsIndex == rhsIndex && lhsStableId == rhsStableId && lhsInfo == rhsInfo && lhsTopItem == rhsTopItem - } else { - return false - } - case .recent: - if case .recent = rhs { - return true - } else { - return false - } - case let .specificPack(lhsInfo, lhsPeer): - if case let .specificPack(rhsInfo, rhsPeer) = rhs { - return lhsInfo == rhsInfo && lhsPeer.isEqual(rhsPeer) - } else { - return false - } - case .saved: - if case .saved = rhs { - return true - } else { - return false - } - } - } - - static func <(lhs: ChatMediaInputPanelEntry, rhs: ChatMediaInputPanelEntry) -> Bool { - switch lhs { - case let .stickerPack(lhsIndex, _, lhsInfo, _): - switch rhs { - case let .stickerPack(rhsIndex, _, rhsInfo, _): - if lhsIndex == rhsIndex { - return lhsInfo.id.id > rhsInfo.id.id - } else { - return lhsIndex > rhsIndex - } - default: - return true - } - case .recent: - switch rhs { - case .saved: - return true - default: - return false - } - case .specificPack: - switch rhs { - case .stickerPack: - return false - default: - return true - } - case .saved: - switch rhs { - case .saved: - return true - default: - return false - } - } - } - - -} diff --git a/Telegram-Mac/EStickerPackItem.swift b/Telegram-Mac/EStickerPackItem.swift deleted file mode 100644 index f194532582..0000000000 --- a/Telegram-Mac/EStickerPackItem.swift +++ /dev/null @@ -1,310 +0,0 @@ -// -// StickerPackItem.swift -// Telegram-Mac -// -// Created by keepcoder on 25/10/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac - - - -class EStickerPackRowItem: TableRowItem { - - override var height:CGFloat { - return 40.0 - } - - var info:StickerPackCollectionInfo - var topItem:StickerPackItem? - var account:Account - var interaction:EStickersInteraction - private var packIndex:Int - - let _stableId:ChatMediaGridCollectionStableId - override var stableId:AnyHashable { - return _stableId - } - - init(_ initialSize:NSSize, _ account:Account, _ index:Int, _ stableId:ChatMediaGridCollectionStableId, _ info:StickerPackCollectionInfo, _ topItem:StickerPackItem?, _ interaction:EStickersInteraction) { - self.account = account - self._stableId = stableId - self.info = info - self.topItem = topItem - self.packIndex = index - self.interaction = interaction - super.init(initialSize) - } - - override func viewClass() -> AnyClass { - return EStickerPackRowView.self - } -} - -class ERecentPackRowItem: TableRowItem { - - override var height:CGFloat { - return 40.0 - } - var interaction:EStickersInteraction - - let _stableId:ChatMediaGridCollectionStableId - override var stableId:AnyHashable { - return _stableId - } - - init(_ initialSize:NSSize, _ stableId:ChatMediaGridCollectionStableId, _ interaction:EStickersInteraction) { - self._stableId = stableId - self.interaction = interaction - super.init(initialSize) - } - - override func viewClass() -> AnyClass { - return ERecentPackRowView.self - } -} - - -class EStickerPackRowView: HorizontalRowView { - - private let boundingSize = CGSize(width: 40.0, height: 40.0) - private let imageSize = CGSize(width: 30.0, height: 30.0) - - private let stickerFetchedDisposable = MetaDisposable() - - var imageView:TransformImageView = TransformImageView() - - var overlay:OverlayControl = OverlayControl() - - required init(frame frameRect:NSRect) { - super.init(frame:frameRect) - - overlay.frame = NSMakeRect(2.0, 2.0, bounds.width - 4.0, bounds.height - 4.0) - overlay.layer?.cornerRadius = .cornerRadius - addSubview(overlay) - - - imageView.frame = self.bounds - addSubview(imageView) - - overlay.set(handler: { [weak self] _ in - - if let item = self?.item as? EStickerPackRowItem { - item.interaction.navigateToCollectionId(item._stableId) - } - - }, for: .Click) - } - - override func layout() { - super.layout() - - imageView.center() - - overlay.setFrameSize(38, 38) - overlay.layer?.cornerRadius = .cornerRadius - overlay.center() - } - - - deinit { - stickerFetchedDisposable.dispose() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func set(item:TableRowItem, animated:Bool = false) { - - var mediaUpdated = true - if let lhs = (self.item as? EStickerPackRowItem)?.topItem, let rhs = (item as? EStickerPackRowItem)?.topItem { - mediaUpdated = !lhs.file.isEqual(rhs.file) - } - - super.set(item: item, animated: animated) - overlay.set(background: theme.colors.grayBackground, for: .Highlight) - overlay.set(background: theme.colors.background, for: .Normal) - - overlay.isSelected = item.isSelected - - if let item = item as? EStickerPackRowItem, mediaUpdated { - if let topItem = item.topItem, let dimensions = topItem.file.dimensions { - imageView.setSignal(account: item.account, signal: chatMessageSticker(account: item.account, file: topItem.file, type: .thumb, scale: backingScaleFactor)) - let arguments = TransformImageArguments(corners: ImageCorners(), imageSize:dimensions.aspectFitted(imageSize), boundingSize: imageSize, intrinsicInsets: NSEdgeInsets()) - imageView.set(arguments:arguments) - imageView.setFrameSize(arguments.imageSize) - _ = fileInteractiveFetched(account: item.account, file: topItem.file).start() - } - self.needsLayout = true - } - - } - -} - - - -class ERecentPackRowView: HorizontalRowView { - - private let boundingSize = CGSize(width: 40.0, height: 40.0) - private let imageSize = CGSize(width: 30.0, height: 30.0) - - - var imageView:ImageView = ImageView() - - var overlay:OverlayControl = OverlayControl() - - required init(frame frameRect:NSRect) { - super.init(frame:frameRect) - - overlay.frame = NSMakeRect(2.0, 2.0, bounds.width - 4.0, bounds.height - 4.0) - overlay.layer?.cornerRadius = .cornerRadius - - addSubview(overlay) - - addSubview(imageView) - - overlay.set(handler: { [weak self] _ in - if let item = self?.item as? ERecentPackRowItem { - item.interaction.navigateToCollectionId(item._stableId) - } - }, for: .Click) - } - - override func layout() { - super.layout() - - imageView.center() - - overlay.setFrameSize(38, 38) - overlay.layer?.cornerRadius = .cornerRadius - overlay.center() - } - - - deinit { - - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func set(item:TableRowItem, animated:Bool = false) { - - super.set(item: item, animated: animated) - overlay.isSelected = item.isSelected - overlay.set(background: theme.colors.background, for: .Normal) - overlay.set(background: theme.colors.grayBackground, for: .Highlight) - - if let item = item as? ERecentPackRowItem { - self.needsLayout = true - switch item._stableId { - case .saved: - imageView.image = theme.icons.stickersTabFave - case .recent: - imageView.image = theme.icons.stickersTabRecent - default: - break - } - imageView.sizeToFit() - - } - - } - -} - - - -class EStickerSpecificPackItem: TableRowItem { - override var height:CGFloat { - return 40.0 - } - let interaction:EStickersInteraction - fileprivate let specificPack: (StickerPackCollectionInfo, Peer) - fileprivate let account: Account - let _stableId:ChatMediaGridCollectionStableId - override var stableId:AnyHashable { - return _stableId - } - - init(_ initialSize:NSSize, _ stableId:ChatMediaGridCollectionStableId, specificPack: (StickerPackCollectionInfo, Peer), account: Account, _ interaction:EStickersInteraction) { - self._stableId = stableId - self.interaction = interaction - self.specificPack = specificPack - self.account = account - super.init(initialSize) - } - - override func viewClass() -> AnyClass { - return EStickerSpecificPackView.self - } -} - -class EStickerSpecificPackView: HorizontalRowView { - - private let boundingSize = CGSize(width: 40.0, height: 40.0) - private let imageSize = CGSize(width: 30.0, height: 30.0) - - - var imageView:AvatarControl = AvatarControl(font: .medium(.short)) - - var overlay:OverlayControl = OverlayControl() - - required init(frame frameRect:NSRect) { - super.init(frame:frameRect) - - overlay.frame = NSMakeRect(2.0, 2.0, bounds.width - 4.0, bounds.height - 4.0) - overlay.layer?.cornerRadius = .cornerRadius - - addSubview(overlay) - - addSubview(imageView) - imageView.setFrameSize(30, 30) - imageView.set(handler: { [weak self] _ in - if let item = self?.item as? EStickerSpecificPackItem { - item.interaction.navigateToCollectionId(item._stableId) - } - }, for: .Click) - } - - override func layout() { - super.layout() - - imageView.center() - - overlay.setFrameSize(38, 38) - overlay.layer?.cornerRadius = .cornerRadius - overlay.center() - } - - - deinit { - - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func set(item:TableRowItem, animated:Bool = false) { - - super.set(item: item, animated: animated) - overlay.isSelected = item.isSelected - overlay.set(background: theme.colors.background, for: .Normal) - overlay.set(background: theme.colors.grayBackground, for: .Highlight) - if let item = item as? EStickerSpecificPackItem { - imageView.setPeer(account: item.account, peer: item.specificPack.1) - } - - } - -} - diff --git a/Telegram-Mac/EStickersViewController.swift b/Telegram-Mac/EStickersViewController.swift deleted file mode 100644 index 43b7631a6c..0000000000 --- a/Telegram-Mac/EStickersViewController.swift +++ /dev/null @@ -1,538 +0,0 @@ -// -// StickersViewController.swift -// Telegram-Mac -// -// Created by keepcoder on 17/10/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac - - -private struct ChatMediaInputGridTransition { - let deletions: [Int] - let insertions: [GridNodeInsertItem] - let updates: [GridNodeUpdateItem] - let updateFirstIndexInSectionOffset: Int? - let stationaryItems: GridNodeStationaryItems - let scrollToItem: GridNodeScrollToItem? - let animated: Bool -} - - - -private func preparedChatMediaInputGridEntryTransition(account: Account, from fromEntries: [AppearanceWrapperEntry], to toEntries: [AppearanceWrapperEntry], update: StickerPacksCollectionUpdate, inputNodeInteraction: EStickersInteraction) -> ChatMediaInputGridTransition { - var stationaryItems: GridNodeStationaryItems = .none - var scrollToItem: GridNodeScrollToItem? - var animated: Bool = false - switch update { - case .generic: - animated = true - case .scroll: - var fromStableIds = Set() - for entry in fromEntries { - fromStableIds.insert(entry.entry.stableId) - } - var index = 0 - var indices = Set() - for entry in toEntries { - if fromStableIds.contains(entry.entry.stableId) { - indices.insert(index) - } - index += 1 - } - stationaryItems = .indices(indices) - case let .navigate(index): - for i in 0 ..< toEntries.count { - if toEntries[i].entry.index >= index { - var directionHint: GridNodePreviousItemsTransitionDirectionHint = .up - if !fromEntries.isEmpty && fromEntries[0].entry.index < toEntries[i].entry.index { - directionHint = .down - } - scrollToItem = GridNodeScrollToItem(index: i, position: .top, transition: .animated(duration: 0.45, curve: .spring), directionHint: directionHint, adjustForSection: true) - break - } - } - } - - let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) - - let deletions = deleteIndices - let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.entry.item(account: account, inputNodeInteraction: inputNodeInteraction), previousIndex: $0.2) } - let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.entry.item(account: account, inputNodeInteraction: inputNodeInteraction)) } - - var firstIndexInSectionOffset = 0 - if !toEntries.isEmpty { - firstIndexInSectionOffset = Int(toEntries[0].entry.index.hashValue) - } - - return ChatMediaInputGridTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: firstIndexInSectionOffset, stationaryItems: stationaryItems, scrollToItem:scrollToItem, animated: animated) -} - -fileprivate func preparePackEntries(from:[AppearanceWrapperEntry]?, to:[AppearanceWrapperEntry], account:Account, initialSize:NSSize, stickersInteraction:EStickersInteraction) -> TableUpdateTransition { - - let (deleted,inserted,updated) = proccessEntries(from, right: to, { (entry) -> TableRowItem in - switch entry.entry { - case let .stickerPack(index, stableId, info, topItem): - return EStickerPackRowItem(initialSize, account, index, stableId, info, topItem, stickersInteraction) - case .recent: - return ERecentPackRowItem(initialSize, entry.entry.stableId, stickersInteraction) - case .saved: - return ERecentPackRowItem(initialSize, entry.entry.stableId, stickersInteraction) - case let .specificPack(info, peer): - return EStickerSpecificPackItem(initialSize, entry.entry.stableId, specificPack: (info, peer), account: account, stickersInteraction) - } - }) - - return TableUpdateTransition(deleted: deleted, inserted: inserted, updated:updated, animated:true, state: .none(nil)) - -} - -private func chatMediaInputPanelEntries(view: ItemCollectionsView, orderedItemListViews:[OrderedItemListView], specificPack:(StickerPackCollectionInfo?, Peer?)) -> [ChatMediaInputPanelEntry] { - var entries: [ChatMediaInputPanelEntry] = [] - var index = 0 -// - if !orderedItemListViews[1].items.isEmpty { - entries.append(.saved) - } - - if !orderedItemListViews[0].items.isEmpty { - entries.append(.recent) - } - - if let info = specificPack.0, let peer = specificPack.1 { - entries.append(.specificPack(info: info, peer: peer)) - } - - for (_, info, item) in view.collectionInfos { - if let info = info as? StickerPackCollectionInfo { - entries.append(.stickerPack(index: index, stableId: .pack(info.id), info: info, topItem: item as? StickerPackItem)) - index += 1 - } - } - entries.sort(by: <) - return entries -} - -private func chatMediaInputGridEntries(view: ItemCollectionsView, orderedItemListViews:[OrderedItemListView], specificPack:(StickerPackCollectionInfo, [ItemCollectionItem])?) -> [ChatMediaInputGridEntry] { - var entries: [ChatMediaInputGridEntry] = [] - - var stickerPackInfos: [ItemCollectionId: StickerPackCollectionInfo] = [:] - for (id, info, _) in view.collectionInfos { - if let info = info as? StickerPackCollectionInfo { - stickerPackInfos[id] = info - } - } - - var fileIds:[MediaId: MediaId] = [:] - - var j:Int = 0 - for item in orderedItemListViews[1].items { - if let entry = item.contents as? SavedStickerItem { - if let id = entry.file.id, fileIds[id] == nil { - fileIds[id] = id - entries.append(ChatMediaInputGridEntry(index: .saved(j), file: entry.file, packInfo: .saved, _stableId: .saved(entry.file), collectionId: .saved)) - j += 1 - } - - } - } - - var i:Int = 0 - for item in orderedItemListViews[0].items { - if let entry = item.contents as? RecentMediaItem { - if let file = entry.media as? TelegramMediaFile, let id = file.id, fileIds[id] == nil { - fileIds[id] = id - entries.append(ChatMediaInputGridEntry(index: .recent(i), file: file, packInfo: .recent, _stableId: .recent(file), collectionId: .recent)) - i += 1 - } - - } - } - - if let specificPack = specificPack { - for entry in specificPack.1 { - if let item = entry as? StickerPackItem { - entries.append(ChatMediaInputGridEntry(index: .speficicSticker(entry.index), file: item.file, packInfo: .speficicPack(specificPack.0), _stableId: .speficicSticker(specificPack.0.id, entry.index.id), collectionId: .specificPack(specificPack.0.id))) - } - } - } - - for entry in view.entries { - if let item = entry.item as? StickerPackItem { - entries.append(ChatMediaInputGridEntry(index: .sticker(entry.index), file: item.file, packInfo: .pack(stickerPackInfos[entry.index.collectionId]), _stableId: .sticker(entry.index.collectionId, entry.index.itemIndex.id), collectionId: .pack(entry.index.collectionId))) - } - } - return entries -} - -private enum StickerPacksCollectionPosition: Equatable { - case initial - case scroll(aroundIndex: ChatMediaInputGridIndex) - case navigate(index: ChatMediaInputGridIndex) - - static func ==(lhs: StickerPacksCollectionPosition, rhs: StickerPacksCollectionPosition) -> Bool { - switch lhs { - case .initial: - if case .initial = rhs { - return true - } else { - return false - } - case let .scroll(aroundIndex): - if case .scroll(aroundIndex) = rhs { - return true - } else { - return false - } - case .navigate: - return false - } - } -} - -private enum StickerPacksCollectionUpdate { - case generic - case scroll - case navigate(ChatMediaInputGridIndex) -} - -final class EStickersInteraction { - let navigateToCollectionId: (ChatMediaGridCollectionStableId) -> Void - - let sendSticker:(TelegramMediaFile) -> Void - let previewStickerSet:(StickerPackReference) -> Void - - var highlightedItemCollectionId: ChatMediaGridCollectionStableId? - - init(navigateToCollectionId: @escaping (ChatMediaGridCollectionStableId) -> Void, sendSticker: @escaping(TelegramMediaFile)-> Void, previewStickerSet: @escaping(StickerPackReference)-> Void) { - self.navigateToCollectionId = navigateToCollectionId - self.sendSticker = sendSticker - self.previewStickerSet = previewStickerSet - } -} - - -class StickersControllerView : View { - fileprivate var gridView:GridNode - fileprivate var packsTable:HorizontalTableView - private var separator:View! - fileprivate var restrictedView:RestrictionWrappedView? - required init(frame frameRect: NSRect) { - self.gridView = GridNode(frame:NSZeroRect) - self.packsTable = HorizontalTableView(frame: NSZeroRect) - separator = View(frame: NSMakeRect(0,0,frameRect.width,.borderSize)) - separator.backgroundColor = .border - - super.init(frame: frameRect) - - addSubview(gridView) - addSubview(packsTable) - addSubview(separator) - updateLocalizationAndTheme() - } - - func updateRestricion(_ peer: Peer?) { - if let peer = peer as? TelegramChannel { - if peer.stickersRestricted, let bannedRights = peer.bannedRights { - restrictedView = RestrictionWrappedView(bannedRights.untilDate != .max ? tr(.channelPersmissionDeniedSendStickersUntil(bannedRights.formattedUntilDate)) : tr(.channelPersmissionDeniedSendStickersForever)) - addSubview(restrictedView!) - } else { - restrictedView?.removeFromSuperview() - restrictedView = nil - } - } else { - restrictedView?.removeFromSuperview() - restrictedView = nil - } - setFrameSize(frame.size) - needsLayout = true - } - - override func updateLocalizationAndTheme() { - self.restrictedView?.updateLocalizationAndTheme() - self.separator.backgroundColor = theme.colors.border - } - - override func setFrameSize(_ newSize: NSSize) { - super.setFrameSize(newSize) - gridView.setFrameSize(frame.width, frame.height - 50) - packsTable.setFrameSize(frame.width - 6.0, 49) - separator.setFrameSize(frame.width, .borderSize) - restrictedView?.setFrameSize(newSize) - } - - override func layout() { - super.layout() - packsTable.setFrameOrigin(3, frame.height - 50) - separator.setFrameOrigin(0, gridView.frame.maxY) - } - - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -class StickersViewController: GenericViewController, TableViewDelegate, Notifable { - - private var interactions:EntertainmentInteractions? - private var chatInteraction:ChatInteraction? - private var account:Account - - private let peerIdPromise: ValuePromise = ValuePromise(ignoreRepeated: true) - - private let itemCollectionsViewPosition = Promise() - private var currentStickerPacksCollectionPosition: StickerPacksCollectionPosition? - private var currentView: ItemCollectionsView? - - private(set) var inputNodeInteraction: EStickersInteraction! - private let disposable = MetaDisposable() - - - func isSelectable(row: Int, item: TableRowItem) -> Bool { - return true - } -// - func selectionWillChange(row: Int, item: TableRowItem) -> Bool { - return true - } -// - func selectionDidChange(row:Int, item:TableRowItem, byClick:Bool, isNew:Bool) { - - } - - func update(with interactions:EntertainmentInteractions, chatInteraction: ChatInteraction) { - self.interactions = interactions - self.chatInteraction?.remove(observer: self) - self.chatInteraction = chatInteraction - self.peerIdPromise.set(chatInteraction.peerId) - chatInteraction.add(observer: self) - if isLoaded() { - genericView.updateRestricion(chatInteraction.presentation.peer) - } - } - - func notify(with value: Any, oldValue: Any, animated: Bool) { - if let value = value as? ChatPresentationInterfaceState, let oldValue = oldValue as? ChatPresentationInterfaceState, let peer = value.peer, let oldPeer = oldValue.peer { - if peer.stickersRestricted != oldPeer.stickersRestricted { - genericView.updateRestricion(peer) - } - } - } - - override func updateLocalizationAndTheme() { - genericView.updateLocalizationAndTheme() - } - - func isEqual(to other: Notifable) -> Bool { - return other === self - } - - - init(account:Account) { - self.account = account - super.init() - self.bar = NavigationBarStyle(height: 0) - - self.inputNodeInteraction = EStickersInteraction(navigateToCollectionId: { [weak self] collectionId in - if let strongSelf = self, let currentView = strongSelf.currentView, collectionId != strongSelf.inputNodeInteraction.highlightedItemCollectionId { - switch collectionId { - case .pack(let itemCollectionId): - var index: Int32 = 0 - for (id, _, _) in currentView.collectionInfos { - if id == itemCollectionId { - let itemIndex = ItemCollectionViewEntryIndex.lowerBound(collectionIndex: index, collectionId: id) - strongSelf.itemCollectionsViewPosition.set(.single(.navigate(index: .sticker(itemIndex)))) - return - } - index += 1 - } - case .saved: - strongSelf.itemCollectionsViewPosition.set(.single(.navigate(index: .saved(0)))) - case .recent: - strongSelf.itemCollectionsViewPosition.set(.single(.navigate(index: .recent(0)))) - case .specificPack: - strongSelf.itemCollectionsViewPosition.set(.single(.navigate(index: .speficicSticker(ItemCollectionItemIndex(index: 0, id: 0))))) - } - - - } - }, sendSticker: { [weak self] file in - self?.interactions?.sendSticker(file) - }, previewStickerSet: { [weak self] reference in - if let eInteraction = self?.interactions, let account = self?.account { - self?.account.context.entertainment.popover?.hide() - showModal(with: StickersPackPreviewModalController(account, peerId: eInteraction.peerId, reference: reference), for: mainWindow) - } - }) - - - } - - deinit { - disposable.dispose() - chatInteraction?.remove(observer: self) - } - - override func viewDidResized(_ size: NSSize) { - super.viewDidResized(size) - let layout = GridNodeLayout(size: CGSize(width: frame.width, height: frame.height - 50), insets: NSEdgeInsets(left: 10, right: 10, top: 10), preloadSize: size.height, type: .fixed(itemSize: CGSize(width: 80, height: 80), lineSpacing: 0)) - let updateLayout = GridNodeUpdateLayout(layout: layout, transition: .immediate) - - self.genericView.gridView.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: updateLayout, itemTransition: .immediate, stationaryItems: .all, updateFirstIndexInSectionOffset: nil), completion: { _ in }) - } - - override func viewDidLoad() { - super.viewDidLoad() - if let chatInteraction = chatInteraction { - genericView.updateRestricion(chatInteraction.presentation.peer) - } - let account = self.account - genericView.packsTable.delegate = self - - - let itemCollectionsView = itemCollectionsViewPosition.get() |> distinctUntilChanged - |> mapToSignal { position -> Signal<(ItemCollectionsView, StickerPacksCollectionUpdate), NoError> in - - switch position { - case .initial: - return account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudSavedStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: nil, count: 50) - |> map { view in - return (view, .generic) - } - case let .scroll(aroundIndex): - var firstTime = true - - return account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudSavedStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: aroundIndex.packIndex, count: 200) - |> map { view in - let update: StickerPacksCollectionUpdate - if firstTime { - firstTime = false - update = .scroll - } else { - update = .generic - } - return (view, update) - } - case let .navigate(index): - var firstTime = true - return account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudSavedStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: index.packIndex, count: 140) - |> map { view in - let update: StickerPacksCollectionUpdate - if firstTime { - firstTime = false - update = .navigate(index) - } else { - update = .generic - } - return (view, update) - } - } - } - - let previousEntries = Atomic<([AppearanceWrapperEntry],[AppearanceWrapperEntry])>(value: ([],[])) - - let inputNodeInteraction = self.inputNodeInteraction! - let initialSize = atomicSize - - let transitions = combineLatest(itemCollectionsView |> deliverOnPrepareQueue, appearanceSignal |> deliverOnPrepareQueue, peerIdPromise.get() |> mapToSignal { - - combineLatest(account.viewTracker.peerView($0) |> take(1) |> map {peerViewMainPeer($0)}, peerSpecificStickerPack(postbox: account.postbox, network: account.network, peerId: $0)) - - } |> deliverOnPrepareQueue) - |> map { itemsView, appearance, specificData -> (ItemCollectionsView, TableUpdateTransition, Bool, ChatMediaInputGridTransition, Bool) in - - let update: StickerPacksCollectionUpdate = itemsView.1 - - let gridEntries = chatMediaInputGridEntries(view: itemsView.0, orderedItemListViews: itemsView.0.orderedItemListsViews, specificPack: specificData.1) - let panelEntries = chatMediaInputPanelEntries(view: itemsView.0, orderedItemListViews: itemsView.0.orderedItemListsViews, specificPack: (specificData.1?.0, specificData.0)) - - let panelEntriesMapped = panelEntries.map({AppearanceWrapperEntry(entry: $0, appearance: appearance)}) - let gridEntriesMapped = gridEntries.map({AppearanceWrapperEntry(entry: $0, appearance: appearance)}) - - let (previousPanelEntries, previousGridEntries) = previousEntries.swap((panelEntriesMapped, gridEntriesMapped)) - - return (itemsView.0, preparePackEntries(from: previousPanelEntries, to: panelEntriesMapped, account: account, initialSize: initialSize.modify({$0}), stickersInteraction:inputNodeInteraction),previousPanelEntries.isEmpty, preparedChatMediaInputGridEntryTransition(account: account, from: previousGridEntries, to: gridEntriesMapped, update: update, inputNodeInteraction: inputNodeInteraction), previousGridEntries.isEmpty) - } - - self.disposable.set((transitions |> deliverOnMainQueue).start(next: { [weak self] (view, packsTransition, packsFirstTime, gridTransition, gridFirstTime) in - if let strongSelf = self { - - strongSelf.currentView = view - strongSelf.genericView.packsTable.merge(with: packsTransition) - strongSelf.enqueueGridTransition(gridTransition, firstTime: gridFirstTime) - - if packsFirstTime { - strongSelf.readyOnce() - if !strongSelf.genericView.packsTable.isEmpty { - let stableId = strongSelf.genericView.packsTable.item(at: 0).stableId - strongSelf.genericView.packsTable.changeSelection(stableId: stableId) - } - } - } - })) - - genericView.gridView.visibleItemsUpdated = { [weak self] visibleItems in - if let strongSelf = self { - if let topVisible = visibleItems.topVisible { - if let item = topVisible.1 as? StickerGridItem { - let collectionId = item.collectionId - if strongSelf.inputNodeInteraction.highlightedItemCollectionId != collectionId { - strongSelf.inputNodeInteraction.highlightedItemCollectionId = collectionId - strongSelf.genericView.packsTable.scroll(to: .center(id: collectionId, animated: true, focus: false, inset: 0)) - strongSelf.genericView.packsTable.changeSelection(stableId: collectionId) - } - } - } - - if let currentView = strongSelf.currentView, let (topIndex, _) = visibleItems.top, let (bottomIndex, _) = visibleItems.bottom { - if topIndex <= 5, let lower = currentView.lower { - let position: StickerPacksCollectionPosition = .scroll(aroundIndex: .sticker(lower.index)) - if strongSelf.currentStickerPacksCollectionPosition != position { - strongSelf.currentStickerPacksCollectionPosition = position - strongSelf.itemCollectionsViewPosition.set(.single(position)) - } - } else if bottomIndex >= visibleItems.count - 5, let higher = currentView.higher { - let position: StickerPacksCollectionPosition = .scroll(aroundIndex: .sticker(higher.index)) - if strongSelf.currentStickerPacksCollectionPosition != position { - strongSelf.currentStickerPacksCollectionPosition = position - strongSelf.itemCollectionsViewPosition.set(.single(position)) - } - } - } - } - } - - self.currentStickerPacksCollectionPosition = .initial - self.itemCollectionsViewPosition.set(.single(.initial)) - - } - - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - genericView.packsTable.clipView.scroll(to: NSZeroPoint) - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - } - - private func enqueueGridTransition(_ transition: ChatMediaInputGridTransition, firstTime: Bool) { - genericView.gridView.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: transition.scrollToItem, updateLayout: nil, itemTransition: .immediate, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: transition.updateFirstIndexInSectionOffset), completion: { _ in }) - } - -} diff --git a/Telegram-Mac/ETabRowItem.swift b/Telegram-Mac/ETabRowItem.swift index 47047c8bd9..f15eb7675e 100644 --- a/Telegram-Mac/ETabRowItem.swift +++ b/Telegram-Mac/ETabRowItem.swift @@ -29,6 +29,10 @@ class ETabRowItem: TableRowItem { return _height } + override var width: CGFloat { + return _height + } + init(_ initialSize:NSSize, icon:CGImage, iconSelected:CGImage, stableId:AnyHashable, width:CGFloat, clickHandler:@escaping(AnyHashable)->Void) { self.icon = icon self.iconSelected = iconSelected diff --git a/Telegram-Mac/ETabRowView.swift b/Telegram-Mac/ETabRowView.swift index af29278fe8..6675ab7e95 100644 --- a/Telegram-Mac/ETabRowView.swift +++ b/Telegram-Mac/ETabRowView.swift @@ -48,9 +48,10 @@ class ETabRowView: HorizontalRowView { override func set(item: TableRowItem, animated: Bool) { super.set(item: item, animated:animated) if let item = item as? ETabRowItem { - overlay.style = ControlStyle(highlightColor: theme.colors.blueIcon) + overlay.style = ControlStyle(highlightColor: theme.colors.accentIcon) overlay.set(image: item.icon, for: .Normal) - overlay.sizeToFit() + overlay.disableActions() + _ = overlay.frame = bounds overlay.isSelected = item.isSelected overlay.set(background: theme.colors.background, for: .Normal) } diff --git a/Telegram-Mac/EditAccountInfoController.swift b/Telegram-Mac/EditAccountInfoController.swift new file mode 100644 index 0000000000..6377a0cac6 --- /dev/null +++ b/Telegram-Mac/EditAccountInfoController.swift @@ -0,0 +1,521 @@ +// +// EditAccountInfoController.swift +// Telegram +// +// Created by Mikhail Filimonov on 26/04/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + + +enum EditSettingsEntryTag: ItemListItemTag { + case bio + + func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? EditSettingsEntryTag, self == other { + return true + } else { + return false + } + } + var stableId: InputDataEntryId { + switch self { + case .bio: + return .input(_id_about) + } + } +} + + +private func valuesRequiringUpdate(state: EditInfoState, view: PeerView) -> ((fn: String, ln: String)?, about: String?) { + if let peer = view.peers[view.peerId] as? TelegramUser { + var names:(String, String)? = nil + if state.firstName != peer.firstName || state.lastName != peer.lastName { + names = (state.firstName, state.lastName) + } + var about: String? = nil + + if let cachedData = view.cachedData as? CachedUserData { + if state.about != (cachedData.about ?? "") { + about = state.about + } + } + + return (names, about) + } + return (nil, nil) +} + +private final class EditInfoControllerArguments { + let context: AccountContext + let uploadNewPhoto:(Control)->Void + let logout:()->Void + let username:()->Void + let changeNumber:()->Void + let addAccount: ()->Void + init(context: AccountContext, uploadNewPhoto:@escaping(Control)->Void, logout:@escaping()->Void, username: @escaping()->Void, changeNumber:@escaping()->Void, addAccount: @escaping() -> Void) { + self.context = context + self.logout = logout + self.username = username + self.changeNumber = changeNumber + self.uploadNewPhoto = uploadNewPhoto + self.addAccount = addAccount + } +} +struct EditInfoState : Equatable { + static func == (lhs: EditInfoState, rhs: EditInfoState) -> Bool { + + if let lhsPeer = lhs.peer, let rhsPeer = rhs.peer { + if !lhsPeer.isEqual(rhsPeer) { + return false + } + } else if (lhs.peer != nil) != (rhs.peer != nil) { + return false + } + + return lhs.firstName == rhs.firstName && lhs.lastName == rhs.lastName && lhs.username == rhs.username && lhs.phone == rhs.phone && lhs.representation == rhs.representation && lhs.updatingPhotoState == rhs.updatingPhotoState && lhs.stateInited == rhs.stateInited && lhs.peerStatusSettings == rhs.peerStatusSettings + } + + let firstName: String + let lastName: String + let about: String + let username: String? + let phone: String? + let representation:TelegramMediaImageRepresentation? + let updatingPhotoState: PeerInfoUpdatingPhotoState? + let stateInited: Bool + let peer: Peer? + let peerStatusSettings: PeerStatusSettings? + let addToException: Bool + init(stateInited: Bool = false, firstName: String = "", lastName: String = "", about: String = "", username: String? = nil, phone: String? = nil, representation: TelegramMediaImageRepresentation? = nil, updatingPhotoState: PeerInfoUpdatingPhotoState? = nil, peer: Peer? = nil, peerStatusSettings: PeerStatusSettings? = nil, addToException: Bool = true) { + self.firstName = firstName + self.lastName = lastName + self.about = about + self.username = username + self.phone = phone + self.representation = representation + self.updatingPhotoState = updatingPhotoState + self.stateInited = stateInited + self.peer = peer + self.peerStatusSettings = peerStatusSettings + self.addToException = addToException + } + + init(_ peerView: PeerView) { + let peer = peerView.peers[peerView.peerId] as? TelegramUser + self.peer = peer + self.firstName = peer?.firstName ?? "" + self.lastName = peer?.lastName ?? "" + self.username = peer?.username + self.phone = peer?.phone + self.about = (peerView.cachedData as? CachedUserData)?.about ?? "" + self.representation = peer?.smallProfileImage + self.updatingPhotoState = nil + self.stateInited = true + self.peerStatusSettings = (peerView.cachedData as? CachedUserData)?.peerStatusSettings + self.addToException = true + } + + func withUpdatedInited(_ stateInited: Bool) -> EditInfoState { + return EditInfoState(stateInited: stateInited, firstName: self.firstName, lastName: self.lastName, about: self.about, username: self.username, phone: self.phone, representation: self.representation, updatingPhotoState: self.updatingPhotoState, peer: self.peer, peerStatusSettings: self.peerStatusSettings, addToException: self.addToException) + } + func withUpdatedAbout(_ about: String) -> EditInfoState { + return EditInfoState(stateInited: self.stateInited, firstName: self.firstName, lastName: self.lastName, about: about, username: self.username, phone: self.phone, representation: self.representation, updatingPhotoState: self.updatingPhotoState, peer: self.peer, peerStatusSettings: self.peerStatusSettings, addToException: self.addToException) + } + + + func withUpdatedFirstName(_ firstName: String) -> EditInfoState { + return EditInfoState(stateInited: self.stateInited, firstName: firstName, lastName: self.lastName, about: self.about, username: self.username, phone: self.phone, representation: self.representation, updatingPhotoState: self.updatingPhotoState, peer: self.peer, peerStatusSettings: self.peerStatusSettings, addToException: self.addToException) + } + func withUpdatedLastName(_ lastName: String) -> EditInfoState { + return EditInfoState(stateInited: self.stateInited, firstName: self.firstName, lastName: lastName, about: self.about, username: self.username, phone: self.phone, representation: self.representation, updatingPhotoState: self.updatingPhotoState, peer: self.peer, peerStatusSettings: self.peerStatusSettings, addToException: self.addToException) + } + + func withUpdatedPeerView(_ peerView: PeerView) -> EditInfoState { + let peer = peerView.peers[peerView.peerId] as? TelegramUser + let about = stateInited ? self.about : (peerView.cachedData as? CachedUserData)?.about ?? self.about + let peerStatusSettings = (peerView.cachedData as? CachedUserData)?.peerStatusSettings + return EditInfoState(stateInited: true, firstName: stateInited ? self.firstName : peer?.firstName ?? self.firstName, lastName: stateInited ? self.lastName : peer?.lastName ?? self.lastName, about: about, username: peer?.username, phone: peer?.phone, representation: peer?.smallProfileImage, updatingPhotoState: self.updatingPhotoState, peer: peer, peerStatusSettings: peerStatusSettings, addToException: self.addToException) + } + func withUpdatedUpdatingPhotoState(_ f: (PeerInfoUpdatingPhotoState?) -> PeerInfoUpdatingPhotoState?) -> EditInfoState { + return EditInfoState(stateInited: self.stateInited, firstName: self.firstName, lastName: self.lastName, about: self.about, username: self.username, phone: self.phone, representation: self.representation, updatingPhotoState: f(self.updatingPhotoState), peer: self.peer, peerStatusSettings: self.peerStatusSettings, addToException: self.addToException) + } + func withoutUpdatingPhotoState() -> EditInfoState { + return EditInfoState(stateInited: self.stateInited, firstName: self.firstName, lastName: self.lastName, about: self.about, username: self.username, phone: self.phone, representation: self.representation, updatingPhotoState: nil, peer:self.peer, peerStatusSettings: self.peerStatusSettings, addToException: self.addToException) + } + + func withUpdatedAddToException(_ addToException: Bool) -> EditInfoState { + return EditInfoState(stateInited: self.stateInited, firstName: self.firstName, lastName: self.lastName, about: self.about, username: self.username, phone: self.phone, representation: self.representation, updatingPhotoState: self.updatingPhotoState, peer:self.peer, peerStatusSettings: self.peerStatusSettings, addToException: addToException) + } +} + +private let _id_info = InputDataIdentifier("_id_info") +private let _id_about = InputDataIdentifier("_id_about") +private let _id_username = InputDataIdentifier("_id_username") +private let _id_phone = InputDataIdentifier("_id_phone") +private let _id_logout = InputDataIdentifier("_id_logout") +private let _id_add_account = InputDataIdentifier("_id_add_account") + +private func editInfoEntries(state: EditInfoState, arguments: EditInfoControllerArguments, activeAccounts: [AccountWithInfo], updateState:@escaping ((EditInfoState)->EditInfoState)->Void) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId: Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_info, equatable: InputDataEquatable(state), comparable: nil, item: { size, stableId -> TableRowItem in + return EditAccountInfoItem(size, stableId: stableId, account: arguments.context.account, state: state, viewType: .singleItem, updateText: { firstName, lastName in + updateState { current in + return current.withUpdatedFirstName(firstName).withUpdatedLastName(lastName).withUpdatedInited(true) + } + }, uploadNewPhoto: { control in + arguments.uploadNewPhoto(control) + }) + })) + index += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.editAccountNameDesc), data: InputDataGeneralTextData(viewType: .textBottomItem))) + index += 1 + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.bioHeader), data: InputDataGeneralTextData(viewType: .textTopItem))) + index += 1 + + + entries.append(.input(sectionId: sectionId, index: index, value: .string(state.about), error: nil, identifier: _id_about, mode: .plain, data: InputDataRowData(viewType: .singleItem), placeholder: nil, inputPlaceholder: L10n.bioPlaceholder, filter: {$0}, limit: 70)) + index += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.bioDescription), data: InputDataGeneralTextData(viewType: .textBottomItem))) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_username, data: InputDataGeneralData(name: L10n.editAccountUsername, color: theme.colors.text, icon: nil, type: .nextContext(state.username != nil ? "@\(state.username!)" : ""), viewType: .firstItem, action: nil))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_phone, data: InputDataGeneralData(name: L10n.editAccountChangeNumber, color: theme.colors.text, icon: nil, type: .nextContext(state.phone != nil ? formatPhoneNumber(state.phone!) : ""), viewType: .lastItem, action: nil))) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + if activeAccounts.count < 3 { + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_add_account, data: InputDataGeneralData(name: L10n.editAccountAddAccount, color: theme.colors.accent, icon: nil, type: .none, viewType: .firstItem, action: { + arguments.addAccount() + }))) + index += 1 + } + + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_logout, data: InputDataGeneralData(name: L10n.editAccountLogout, color: theme.colors.redUI, icon: nil, type: .none, viewType: activeAccounts.count < 3 ? .lastItem : .singleItem, action: nil))) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + + +func EditAccountInfoController(context: AccountContext, focusOnItemTag: EditSettingsEntryTag? = nil, f: @escaping((ViewController)) -> Void) -> Void { + + let state: Promise = Promise() + let stateValue: Atomic = Atomic(value: EditInfoState()) + let actionsDisposable = DisposableSet() + let photoDisposable = MetaDisposable() + let peerDisposable = MetaDisposable() + let logoutDisposable = MetaDisposable() + let updateNameDisposable = MetaDisposable() + + actionsDisposable.add(photoDisposable) + actionsDisposable.add(peerDisposable) + actionsDisposable.add(logoutDisposable) + actionsDisposable.add(updateNameDisposable) + let updateState:((EditInfoState)->EditInfoState)->Void = { f in + state.set(.single(stateValue.modify(f))) + } + + var peerView:PeerView? = nil + + peerDisposable.set((context.account.postbox.peerView(id: context.peerId) |> deliverOnMainQueue).start(next: { pv in + peerView = pv + updateState { current in + return current.withUpdatedPeerView(pv) + } + })) + + let peerId = context.peerId + + let cancel = { + photoDisposable.set(nil) + updateState { state -> EditInfoState in + return state.withoutUpdatingPhotoState() + } + } + + var close:(()->Void)? = nil + + let updatePhoto:(NSImage)->Void = { image in + + _ = (putToTemp(image: image, compress: true) |> deliverOnMainQueue).start(next: { path in + let controller = EditImageModalController(URL(fileURLWithPath: path), settings: .disableSizes(dimensions: .square)) + showModal(with: controller, for: context.window, animationType: .scaleCenter) + + let updateSignal = controller.result |> map { path, _ -> TelegramMediaResource in + return LocalFileReferenceMediaResource(localFilePath: path.path, randomId: arc4random64()) + } |> beforeNext { resource in + updateState { state -> EditInfoState in + return state.withUpdatedUpdatingPhotoState { _ in + return PeerInfoUpdatingPhotoState(progress: 0, cancel: cancel) + } + } + } |> mapError {_ in return UploadPeerPhotoError.generic } |> mapToSignal { resource -> Signal in + return context.engine.accountData.updateAccountPhoto(resource: resource, videoResource: nil, videoStartTimestamp: nil, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: context.account.postbox, resource: resource, representations: representations) + }) + } |> deliverOnMainQueue + + + + photoDisposable.set(updateSignal.start(next: { status in + updateState { state -> EditInfoState in + switch status { + case .complete: + return state.withoutUpdatingPhotoState() + case let .progress(progress): + return state.withUpdatedUpdatingPhotoState { current -> PeerInfoUpdatingPhotoState? in + return current?.withUpdatedProgress(progress) + } + } + } + }, error: { error in + updateState { state in + return state.withoutUpdatingPhotoState() + } + }, completed: { + updateState { state -> EditInfoState in + return state.withoutUpdatingPhotoState() + } + })) + + controller.onClose = { + removeFile(at: path) + } + }) + } + + let updateVideo:(Signal) -> Void = { signal in + let updateSignal: Signal = signal + |> mapError { _ in return UploadPeerPhotoError.generic } + |> mapToSignal { state in + switch state { + case .error: + return .fail(.generic) + case let .start(path): + updateState { (state) -> EditInfoState in + return state.withUpdatedUpdatingPhotoState { previous -> PeerInfoUpdatingPhotoState? in + return PeerInfoUpdatingPhotoState(progress: 0, image: NSImage(contentsOfFile: path)?._cgImage, cancel: cancel) + } + } + return .next(.progress(0)) + case let .progress(value): + return .next(.progress(value * 0.2)) + case let .complete(thumb, video, keyFrame): + let (thumbResource, videoResource) = (LocalFileReferenceMediaResource(localFilePath: thumb, randomId: arc4random64(), isUniquelyReferencedTemporaryFile: true), + LocalFileReferenceMediaResource(localFilePath: video, randomId: arc4random64(), isUniquelyReferencedTemporaryFile: true)) + return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: thumbResource), video: context.engine.peers.uploadedPeerVideo(resource: videoResource) |> map(Optional.init), videoStartTimestamp: keyFrame, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: context.account.postbox, resource: resource, representations: representations) + }) |> map { result in + switch result { + case let .progress(current): + return .progress(0.2 + (current * 0.8)) + default: + return result + } + } + } + } + photoDisposable.set(updateSignal.start(next: { status in + updateState { state -> EditInfoState in + switch status { + case .complete: + return state.withoutUpdatingPhotoState() + case let .progress(progress): + return state.withUpdatedUpdatingPhotoState { current -> PeerInfoUpdatingPhotoState? in + return current?.withUpdatedProgress(progress) + } + } + } + }, error: { error in + updateState { state in + return state.withoutUpdatingPhotoState() + } + }, completed: { + updateState { state -> EditInfoState in + return state.withoutUpdatingPhotoState() + } + })) + } + + let arguments = EditInfoControllerArguments(context: context, uploadNewPhoto: { control in + + var items:[SPopoverItem] = [] + + items.append(.init(L10n.editAvatarPhotoOrVideo, { + filePanel(with: photoExts + videoExts, allowMultiple: false, canChooseDirectories: false, for: context.window, completion: { paths in + if let path = paths?.first, let image = NSImage(contentsOfFile: path) { + updatePhoto(image) + } else if let path = paths?.first { + selectVideoAvatar(context: context, path: path, localize: L10n.videoAvatarChooseDescProfile, signal: { signal in + updateVideo(signal) + }) + } + }) + })) + + items.append(.init(L10n.editAvatarStickerOrGif, { [weak control] in + + let controller = EntertainmentViewController(size: NSMakeSize(350, 350), context: context, mode: .selectAvatar) + controller._frameRect = NSMakeRect(0, 0, 350, 400) + + let interactions = ChatInteraction(chatLocation: .peer(context.peerId), context: context) + + let runConvertor:(MediaObjectToAvatar)->Void = { [weak control] convertor in + _ = showModalProgress(signal: convertor.start(), for: context.window).start(next: { [weak control] result in + switch result { + case let .image(image): + updatePhoto(image) + case let .video(path): + selectVideoAvatar(context: context, path: path, localize: L10n.videoAvatarChooseDescProfile, quality: AVAssetExportPresetHighestQuality, signal: { signal in + updateVideo(signal) + }) + } + control?.contextObject = nil + }) + control?.contextObject = convertor + } + + interactions.sendAppFile = { file, _, _ in + let object: MediaObjectToAvatar.Object + if file.isAnimatedSticker { + object = .animated(file) + } else if file.isSticker { + object = .sticker(file) + } else { + object = .gif(file) + } + let convertor = MediaObjectToAvatar(context: context, object: object) + runConvertor(convertor) + } + interactions.sendInlineResult = { [] collection, result in + switch result { + case let .internalReference(reference): + if let file = reference.file { + let convertor = MediaObjectToAvatar(context: context, object: .gif(file)) + runConvertor(convertor) + } + case .externalReference: + break + } + } + + control?.contextObject = interactions + controller.update(with: interactions) + if let control = control { + showPopover(for: control, with: controller, edge: .maxY, inset: NSMakePoint(0, -110), static: true) + } + })) + + showPopover(for: control, with: SPopoverViewController(items: items), edge: .maxY, inset: NSMakePoint(0, -60)) + + }, logout: { + showModal(with: LogoutViewController(context: context, f: f), for: context.window) + }, username: { + f(UsernameSettingsViewController(context)) + }, changeNumber: { + f(PhoneNumberIntroController(context)) + }, addAccount: { + let testingEnvironment = NSApp.currentEvent?.modifierFlags.contains(.command) == true + context.sharedContext.beginNewAuth(testingEnvironment: testingEnvironment) + }) + + let controller = InputDataController(dataSignal: combineLatest(state.get() |> deliverOnPrepareQueue, appearanceSignal |> deliverOnPrepareQueue, context.sharedContext.activeAccountsWithInfo) |> map {editInfoEntries(state: $0.0, arguments: arguments, activeAccounts: $0.2.accounts, updateState: updateState)} |> map { InputDataSignalValue(entries: $0) }, title: L10n.editAccountTitle, validateData: { data -> InputDataValidation in + + if let _ = data[_id_logout] { + arguments.logout() + return .fail(.none) + } + if let _ = data[_id_username] { + arguments.username() + return .fail(.none) + } + if let _ = data[_id_phone] { + arguments.changeNumber() + return .fail(.none) + } + + return .fail(.doSomething { f in + let current = stateValue.modify {$0} + if current.firstName.isEmpty { + f(.fail(.fields([_id_info : .shake]))) + } + var signals:[Signal] = [] + if let peerView = peerView { + let updates = valuesRequiringUpdate(state: current, view: peerView) + if let names = updates.0 { + + signals.append(context.engine.accountData.updateAccountPeerName(firstName: names.fn, lastName: names.ln)) + } + if let about = updates.1 { + signals.append(context.engine.accountData.updateAbout(about: about) |> `catch` { _ in .complete()}) + } + updateNameDisposable.set(showModalProgress(signal: combineLatest(signals) |> deliverOnMainQueue, for: context.window).start(completed: { + updateState { $0 } + close?() + _ = showModalSuccess(for: context.window, icon: theme.icons.successModalProgress, delay: 1.5).start() + })) + } + }) + }, updateDatas: { data in + updateState { current in + return current.withUpdatedAbout(data[_id_about]?.stringValue ?? "") + } + return .fail(.none) + }, afterDisappear: { + actionsDisposable.dispose() + }, updateDoneValue: { data in + return { f in + let current = stateValue.modify {$0} + if let peerView = peerView { + let updates = valuesRequiringUpdate(state: current, view: peerView) + f((updates.0 != nil || updates.1 != nil) ? .enabled(L10n.navigationDone) : .disabled(L10n.navigationDone)) + } else { + f(.disabled(L10n.navigationDone)) + } + } + }, removeAfterDisappear: false, identifier: "account") + + controller.didLoaded = { controller, _ in + if let focusOnItemTag = focusOnItemTag { + controller.genericView.tableView.scroll(to: .center(id: focusOnItemTag.stableId, innerId: nil, animated: true, focus: .init(focus: true), inset: 0), inset: NSEdgeInsets()) + } + } + + close = { [weak controller] in + controller?.navigationController?.back() + } + + f(controller) +} diff --git a/Telegram-Mac/EditAccountInfoItem.swift b/Telegram-Mac/EditAccountInfoItem.swift new file mode 100644 index 0000000000..192313aa6d --- /dev/null +++ b/Telegram-Mac/EditAccountInfoItem.swift @@ -0,0 +1,326 @@ +// +// EditAccountInfoItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 26/04/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + +class EditAccountInfoItem: GeneralRowItem { + + fileprivate let account: Account + fileprivate let state: EditInfoState + fileprivate let photo: AvatarNodeState + fileprivate let updateText: (String, String)->Void + fileprivate let uploadNewPhoto: ((Control)->Void)? + init(_ initialSize: NSSize, stableId: AnyHashable, account: Account, state: EditInfoState, viewType: GeneralViewType = .legacy, updateText:@escaping(String, String)->Void, uploadNewPhoto: ((Control)->Void)? = nil) { + self.account = account + self.updateText = updateText + self.state = state + self.uploadNewPhoto = uploadNewPhoto + self.photo = state.peer != nil ? .PeerAvatar(state.peer!, [state.firstName.first, state.lastName.first].compactMap{$0}.map{String($0)}, state.representation, nil, nil) : .Empty + + let height: CGFloat + switch viewType { + case .legacy: + height = 90 + case let .modern(_, insets): + height = 60 + insets.top + insets.bottom + } + + super.init(initialSize, height: height, stableId: stableId, viewType: viewType) + } + + override func viewClass() -> AnyClass { + return EditAccountInfoItemView.self + } +} + +private final class EditAccountInfoItemView : TableRowView, TGModernGrowingDelegate { + private let containerView = GeneralRowContainerView(frame: NSZeroRect) + private let firstNameTextView: TGModernGrowingTextView = TGModernGrowingTextView(frame: NSZeroRect) + private let lastNameTextView: TGModernGrowingTextView = TGModernGrowingTextView(frame: NSZeroRect) + private let avatar: AvatarControl = AvatarControl(font: .avatar(22)) + private let nameSeparator: View = View() + private let secondSeparator: View = View() + private var tempImageView: ImageView? = nil + + private let updoadPhotoCap:ImageButton = ImageButton() + private let progressView:RadialProgressContainerView = RadialProgressContainerView(theme: RadialProgressTheme(backgroundColor: .clear, foregroundColor: .white, icon: nil)) + private var ignoreUpdates: Bool = false + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + avatar.setFrameSize(NSMakeSize(60, 60)) + avatar.layer?.cornerRadius = 30 + progressView.frame = avatar.bounds + firstNameTextView.delegate = self + lastNameTextView.delegate = self + + firstNameTextView.textFont = .normal(.text) + lastNameTextView.textFont = .normal(.text) + + containerView.addSubview(firstNameTextView) + containerView.addSubview(lastNameTextView) + containerView.addSubview(nameSeparator) + containerView.addSubview(secondSeparator) + containerView.addSubview(avatar) + + addSubview(containerView) + + updoadPhotoCap.backgroundColor = NSColor.black.withAlphaComponent(0.4) + updoadPhotoCap.setFrameSize(avatar.frame.size) + updoadPhotoCap.layer?.cornerRadius = updoadPhotoCap.frame.width / 2 + updoadPhotoCap.set(image: ControlStyle(highlightColor: .white).highlight(image: theme.icons.chatAttachCamera), for: .Normal) + updoadPhotoCap.set(image: ControlStyle(highlightColor: theme.colors.accentIcon).highlight(image: theme.icons.chatAttachCamera), for: .Highlight) + + + updoadPhotoCap.set(handler: { [weak self] control in + guard let item = self?.item as? EditAccountInfoItem else {return} + item.uploadNewPhoto?(control) + }, for: .Click) + + avatar.addSubview(updoadPhotoCap) + + progressView.progress.fetchControls = FetchControls(fetch: { [weak self] in + guard let item = self?.item as? EditAccountInfoItem else {return} + item.state.updatingPhotoState?.cancel() + }) + } + + override var mouseInsideField: Bool { + return lastNameTextView._mouseInside() || firstNameTextView._mouseInside() + } + + override func hitTest(_ point: NSPoint) -> NSView? { +// switch true { +// case NSPointInRect(point, firstNameTextView.frame): +// return firstNameTextView.inputView +// case NSPointInRect(point, lastNameTextView.frame): +// return lastNameTextView.inputView +// default: + return super.hitTest(point) +// } + } + + override func hasFirstResponder() -> Bool { + return true + } + + override var firstResponder: NSResponder? { + let isKeyDown = NSApp.currentEvent?.type == NSEvent.EventType.keyDown && NSApp.currentEvent?.keyCode == KeyboardKey.Tab.rawValue + switch true { + case firstNameTextView._mouseInside() && !isKeyDown: + return firstNameTextView.inputView + case lastNameTextView._mouseInside() && !isKeyDown: + return lastNameTextView.inputView + default: + switch true { + case firstNameTextView.inputView == window?.firstResponder: + return firstNameTextView.inputView + case lastNameTextView.inputView == window?.firstResponder: + return lastNameTextView.inputView + default: + return firstNameTextView.inputView + } + } + } + + override func nextResponder() -> NSResponder? { + if window?.firstResponder == firstNameTextView.inputView { + return lastNameTextView.inputView + } + + return nil + } + + override var backdorColor: NSColor { + return theme.colors.background + } + + override func set(item: TableRowItem, animated: Bool) { + + guard let item = item as? EditAccountInfoItem else {return} + + avatar.setState(account: item.account, state: item.photo) + ignoreUpdates = true + firstNameTextView.animates = false + lastNameTextView.animates = false + + firstNameTextView.placeholderAttributedString = .initialize(string: L10n.peerInfoFirstNamePlaceholder, color: theme.colors.grayText, font: .normal(.text)) + lastNameTextView.placeholderAttributedString = .initialize(string: L10n.peerInfoLastNamePlaceholder, color: theme.colors.grayText, font: .normal(.text)) + + firstNameTextView.setString(item.state.firstName) + lastNameTextView.setString(item.state.lastName) + + if let uploadState = item.state.updatingPhotoState { + if progressView.superview == nil { + avatar.addSubview(progressView) + progressView.layer?.opacity = 0 + } + progressView.change(opacity: 1, animated: animated) + progressView.progress.state = .Fetching(progress: uploadState.progress, force: false) + self.updoadPhotoCap.isHidden = true + + if let _ = uploadState.image, self.tempImageView == nil { + self.tempImageView = ImageView() + self.tempImageView?.contentGravity = .resizeAspect + self.tempImageView!.frame = avatar.bounds + self.avatar.addSubview(tempImageView!, positioned: .below, relativeTo: self.updoadPhotoCap) + if animated { + self.tempImageView?.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } + self.tempImageView?.image = uploadState.image + + } else { + if animated { + progressView.change(opacity: 0, animated: animated, removeOnCompletion: false, completion: { [weak self] complete in + if complete { + self?.progressView.removeFromSuperview() + self?.progressView.layer?.removeAllAnimations() + } + }) + } else { + progressView.removeFromSuperview() + } + updoadPhotoCap.isHidden = item.uploadNewPhoto == nil + + if let tempImageView = self.tempImageView { + self.tempImageView = nil + if animated { + tempImageView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak tempImageView] _ in + tempImageView?.removeFromSuperview() + }) + } else { + tempImageView.removeFromSuperview() + } + } + } + + secondSeparator.isHidden = item.uploadNewPhoto == nil + + super.set(item: item, animated: animated) + + + layout() + + ignoreUpdates = false + } + + override func updateColors() { + firstNameTextView.textColor = theme.colors.text + lastNameTextView.textColor = theme.colors.text + + firstNameTextView.setBackgroundColor(backdorColor) + lastNameTextView.setBackgroundColor(backdorColor) + + nameSeparator.backgroundColor = theme.colors.border + secondSeparator.backgroundColor = theme.colors.border + containerView.background = backdorColor + guard let item = item as? EditAccountInfoItem else {return} + self.background = item.viewType.rowBackground + } + + override func layout() { + super.layout() + + guard let item = item as? EditAccountInfoItem else {return} + + switch item.viewType { + case .legacy: + self.containerView.frame = bounds + self.containerView.setCorners([]) + firstNameTextView.setFrameSize(NSMakeSize(self.containerView.frame.width - item.inset.left - item.inset.right - avatar.frame.width - 10, firstNameTextView.frame.height)) + lastNameTextView.setFrameSize(NSMakeSize(self.containerView.frame.width - item.inset.left - item.inset.right - avatar.frame.width - 10, lastNameTextView.frame.height)) + avatar.setFrameOrigin(item.inset.left, 16) + firstNameTextView.setFrameOrigin(NSMakePoint(avatar.frame.maxX + 10, avatar.frame.minY - 6)) + nameSeparator.frame = NSMakeRect(avatar.frame.maxX + 14, firstNameTextView.frame.maxY + 2, self.containerView.frame.width - avatar.frame.maxX - item.inset.right - 14, .borderSize) + lastNameTextView.setFrameOrigin(NSMakePoint(avatar.frame.maxX + 10, firstNameTextView.frame.maxY + 4)) + secondSeparator.frame = NSMakeRect(item.inset.left, self.containerView.frame.height - .borderSize, self.containerView.frame.width - item.inset.right - item.inset.left, .borderSize) + secondSeparator.isHidden = false + case let .modern(position, innerInsets): + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) + self.containerView.setCorners(position.corners) + firstNameTextView.setFrameSize(NSMakeSize(self.containerView.frame.width - innerInsets.left - innerInsets.right - avatar.frame.width - 10, firstNameTextView.frame.height)) + lastNameTextView.setFrameSize(NSMakeSize(self.containerView.frame.width - innerInsets.left - innerInsets.right - avatar.frame.width - 10, lastNameTextView.frame.height)) + avatar.setFrameOrigin(innerInsets.left, innerInsets.top) + firstNameTextView.setFrameOrigin(NSMakePoint(avatar.frame.maxX + 10, avatar.frame.minY - 6)) + nameSeparator.frame = NSMakeRect(avatar.frame.maxX + 14, firstNameTextView.frame.maxY + 2, self.containerView.frame.width - avatar.frame.maxX - item.inset.right - 14, .borderSize) + lastNameTextView.setFrameOrigin(NSMakePoint(avatar.frame.maxX + 10, firstNameTextView.frame.maxY + 4)) + secondSeparator.frame = NSMakeRect(innerInsets.left, self.containerView.frame.height - .borderSize, self.containerView.frame.width - item.inset.right - item.inset.left, .borderSize) + + secondSeparator.isHidden = !position.border + + } + + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func maxCharactersLimit(_ textView: TGModernGrowingTextView!) -> Int32 { + return 64 + } + + func textViewHeightChanged(_ height: CGFloat, animated: Bool) { + + } + + func textViewSize(_ textView: TGModernGrowingTextView!) -> NSSize { + return textView.frame.size + } + + override func setFrameSize(_ newSize: NSSize) { + super.setFrameSize(newSize) + } + + func textViewEnterPressed(_ event:NSEvent) -> Bool { + if FastSettings.checkSendingAbility(for: event) { + return true + } + return false + } + + func textViewIsTypingEnabled() -> Bool { + return true + } + + func textViewNeedClose(_ textView: Any) { + + } + + func textViewTextDidChange(_ string: String) { + guard let item = item as? EditAccountInfoItem else {return} + guard !ignoreUpdates else {return} + + item.updateText(firstNameTextView.string(), lastNameTextView.string()) + } + + func textViewDidReachedLimit(_ textView: Any) { +// if let responder = nextResponder() { +// window?.makeFirstResponder(responder) +// } + } + + func controlTextDidChange(_ obj: Notification) { + + } + + func textViewTextDidChangeSelectedRange(_ range: NSRange) { + + } + + func textViewDidPaste(_ pasteboard: NSPasteboard) -> Bool { + return false + } +} diff --git a/Telegram-Mac/EditImageCanvasColorPicker.swift b/Telegram-Mac/EditImageCanvasColorPicker.swift new file mode 100644 index 0000000000..abf9b43f7d --- /dev/null +++ b/Telegram-Mac/EditImageCanvasColorPicker.swift @@ -0,0 +1,448 @@ +// +// EditImageCanvasColorPickerBackground.swift +// Telegram +// +// Created by Mikhail Filimonov on 16/04/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + + + +class EditImageCanvasColorPickerBackground: Control { + + + override func draw(_ layer: CALayer, in ctx: CGContext) { + super.draw(layer, in: ctx) + + let rect = bounds + + let radius: CGFloat = rect.size.width > rect.size.height ? rect.size.height / 2.0 : rect.size.width / 2.0 + addRoundedRectToPath(ctx, bounds, radius, radius) + ctx.clip() + + let colors = EditImageCanvasColorPickerBackground.colors + var locations = EditImageCanvasColorPickerBackground.locations + + let colorSpc = CGColorSpaceCreateDeviceRGB() + let gradient: CGGradient = CGGradient(colorsSpace: colorSpc, colors: colors as CFArray, locations: &locations)! + + if rect.size.width > rect.size.height { + ctx.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: rect.size.height / 2.0), end: CGPoint(x: rect.size.width, y: rect.size.height / 2.0), options: .drawsAfterEndLocation) + } else { + ctx.drawLinearGradient(gradient, start: CGPoint(x: rect.size.width / 2.0, y: 0.0), end: CGPoint(x: rect.size.width / 2.0, y: rect.size.height), options: .drawsAfterEndLocation) + } + + ctx.setBlendMode(.clear) + ctx.setFillColor(.clear) + + } + + private func addRoundedRectToPath(_ context: CGContext, _ rect: CGRect, _ ovalWidth: CGFloat, _ ovalHeight: CGFloat) { + var fw: CGFloat + var fh: CGFloat + if ovalWidth == 0 || ovalHeight == 0 { + context.addRect(rect) + return + } + context.saveGState() + context.translateBy(x: rect.minX, y: rect.minY) + context.scaleBy(x: ovalWidth, y: ovalHeight) + fw = rect.width / ovalWidth + fh = rect.height / ovalHeight + context.move(to: CGPoint(x: fw, y: fh / 2)) + context.addArc(tangent1End: CGPoint(x: fw, y: fh), tangent2End: CGPoint(x: fw / 2, y: fh), radius: 1) + context.addArc(tangent1End: CGPoint(x: 0, y: fh), tangent2End: CGPoint(x: 0, y: fh / 2), radius: 1) + context.addArc(tangent1End: CGPoint(x: 0, y: 0), tangent2End: CGPoint(x: fw / 2, y: 0), radius: 1) + context.addArc(tangent1End: CGPoint(x: fw, y: 0), tangent2End: CGPoint(x: fw, y: fh / 2), radius: 1) + context.closePath() + context.restoreGState() + } + + func color(for location: CGFloat) -> NSColor { + let locations = EditImageCanvasColorPickerBackground.locations + let colors = EditImageCanvasColorPickerBackground.colors + + if location < .ulpOfOne { + return NSColor(cgColor: colors[0])! + } else if location > 1 - .ulpOfOne { + return NSColor(cgColor: colors[colors.count - 1])! + } + + var leftIndex: Int = -1 + var rightIndex: Int = -1 + + for (index, value) in locations.enumerated() { + if index > 0 { + if value > location { + leftIndex = index - 1 + rightIndex = index + break + } + } + } + + let leftLocation = locations[leftIndex] + let leftColor = NSColor(cgColor: colors[leftIndex])! + + let rightLocation = locations[rightIndex] + let rightColor = NSColor(cgColor: colors[rightIndex])! + + let factor = (location - leftLocation) / (rightLocation - leftLocation) + + return self.interpolateColor(color1: leftColor, color2: rightColor, factor: factor) + } + + private func interpolateColor(color1: NSColor, color2: NSColor, factor: CGFloat) -> NSColor { + let factor = min(max(factor, 0.0), 1.0) + + var r1: CGFloat = 0 + var r2: CGFloat = 0 + var g1: CGFloat = 0 + var g2: CGFloat = 0 + var b1: CGFloat = 0 + var b2: CGFloat = 0 + + + self.colorComponentsFor(color1, red: &r1, green: &g1, blue: &b1) + self.colorComponentsFor(color2, red: &r2, green: &g2, blue: &b2) + + let r = r1 + (r2 - r1) * factor; + let g = g1 + (g2 - g1) * factor; + let b = b1 + (b2 - b1) * factor; + + return NSColor(red: r, green: g, blue: b, alpha: 1.0) + } + + private func colorComponentsFor(_ color: NSColor, red:inout CGFloat, green:inout CGFloat, blue:inout CGFloat) { + let componentsCount = color.cgColor.numberOfComponents + let components = color.cgColor.components + + var r: CGFloat = 0.0 + var g: CGFloat = 0.0 + var b: CGFloat = 0.0 + var a: CGFloat = 1.0 + + if componentsCount == 4 { + r = components?[0] ?? 0.0 + g = components?[1] ?? 0.0 + b = components?[2] ?? 0.0 + a = components?[3] ?? 0.0 + } else { + b = components?[0] ?? 0.0 + g = b + r = g + } + red = r + green = g + blue = b + } + + + static var colors: [CGColor] { + return [NSColor(0xea2739).cgColor, + NSColor(0xdb3ad2).cgColor, + NSColor(0x3051e3).cgColor, + NSColor(0x49c5ed).cgColor, + NSColor(0x80c864).cgColor, + NSColor(0xfcde65).cgColor, + NSColor(0xfc964d).cgColor, + NSColor(0x000000).cgColor, + NSColor(0xffffff).cgColor + ] + } + static var locations: [CGFloat] { + return [ 0.0, //red + 0.14, //pink + 0.24, //blue + 0.39, //cyan + 0.49, //green + 0.62, //yellow + 0.73, //orange + 0.85, //black + 1.0 + ] + } + +} + +private final class PaintColorPickerKnobCircleView : View { + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var strokeIntensity: CGFloat = 0 + + var strokesLowContrastColors: Bool = false + + + override var needsLayout: Bool { + didSet { + needsDisplay = true + } + } + + var color: NSColor = .black { + didSet { + if strokesLowContrastColors { + var strokeIntensity: CGFloat = 0.0 + var hue: CGFloat = 0 + var saturation: CGFloat = 0 + var brightness: CGFloat = 0 + var alpha: CGFloat = 0 + + color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) + if hue < CGFloat.ulpOfOne && saturation < CGFloat.ulpOfOne && brightness > 0.92 { + strokeIntensity = (brightness - 0.92) / 0.08 + } + self.strokeIntensity = strokeIntensity + } + needsDisplay = true + } + } + + override func draw(_ layer: CALayer, in context: CGContext) { + super.draw(layer, in: context) + + let rect = bounds + + context.setFillColor(color.cgColor) + context.fillEllipse(in: rect) + + if strokeIntensity > .ulpOfOne { + context.setLineWidth(1.0) + context.setStrokeColor(NSColor(white: 0.88, alpha: strokeIntensity).cgColor) + context.strokeEllipse(in: rect.insetBy(dx: 1.0, dy: 1.0)) + } + + } + + +} + +private let paintColorSmallCircle: CGFloat = 4.0 +private let paintColorLargeCircle: CGFloat = 20.0 +private let paintColorWeightGestureRange: CGFloat = 200 +private let paintVerticalThreshold: CGFloat = 5 +private let paintPreviewOffset: CGFloat = -60 +private let paintPreviewScale: CGFloat = 2.0 +private let paintDefaultBrushWeight: CGFloat = 0.22 +private let oaintDefaultColorLocation: CGFloat = 1.0 + + +private final class PaintColorPickerKnob: View { + + fileprivate var isZoomed: Bool = false + + fileprivate var weight: CGFloat = 0.5 + + fileprivate func updateWeight(_ weight: CGFloat, animated: Bool) { + self.weight = weight + var diameter = circleDiameter(forBrushWeight: weight, zoomed: self.isZoomed) + if Int(diameter) % 2 != 0 { + diameter -= 1 + } + colorView.setFrameSize(NSMakeSize(diameter, diameter)) + + backgroundView.setFrameSize(NSMakeSize(24 * (isZoomed ? paintPreviewScale : 1), 24 * (isZoomed ? paintPreviewScale : 1))) + backgroundView.center() + + + if animated { + if isZoomed { + colorView.layer?.animateScaleSpring(from: 0.5, to: 1, duration: 0.3) + backgroundView.layer?.animateScaleSpring(from: 0.5, to: 1, duration: 0.3) + } else { + colorView.layer?.animateScaleSpring(from: 2.0, to: 1, duration: 0.3) + backgroundView.layer?.animateScaleSpring(from: 2.0, to: 1, duration: 0.3) + } + } + + needsLayout = true + } + + fileprivate var color: NSColor = .random { + didSet { + colorView.color = color + } + } + fileprivate var width: CGFloat { + return circleDiameter(forBrushWeight: weight, zoomed: false) - 2 + } + + private let backgroundView = PaintColorPickerKnobCircleView(frame: .zero) + private let colorView = PaintColorPickerKnobCircleView(frame: .zero) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + + + backgroundView.color = NSColor(0xffffff) + backgroundView.isEventLess = true + colorView.isEventLess = true + colorView.color = NSColor.blue + colorView.strokesLowContrastColors = true + addSubview(backgroundView) + addSubview(colorView) + } + + + func circleDiameter(forBrushWeight size: CGFloat, zoomed: Bool) -> CGFloat { + var result = CGFloat(paintColorSmallCircle) + CGFloat((paintColorLargeCircle - paintColorSmallCircle)) * size + result = CGFloat(zoomed ? result * paintPreviewScale : floor(result)) + return floorToScreenPixels(backingScaleFactor, result) + } + + override func layout() { + super.layout() + backgroundView.setFrameSize(NSMakeSize(24 * (isZoomed ? paintPreviewScale : 1), 24 * (isZoomed ? paintPreviewScale : 1))) + backgroundView.center() + colorView.center() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +final class EditImageColorPicker: View { + + var arguments: EditImageCanvasArguments? { + didSet { + arguments?.updateColorAndWidth(knobView.color, knobView.width) + } + } + + private let knobView = PaintColorPickerKnob(frame: NSMakeRect(0, 0, 24 * paintPreviewScale, 24 * paintPreviewScale)) + let backgroundView = EditImageCanvasColorPickerBackground() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(backgroundView) + addSubview(knobView) + knobView.isEventLess = true + knobView.userInteractionEnabled = false + + backgroundView.set(handler: { [weak self] _ in + self?.updateLocation(animated: false) + }, for: .MouseDragging) + + backgroundView.set(handler: { [weak self] _ in + self?.knobView.isZoomed = true + self?.updateLocation(animated: true) + }, for: .Down) + + backgroundView.set(handler: { [weak self] _ in + self?.knobView.isZoomed = false + self?.updateLocation(animated: true) + }, for: .Up) + + let colorValue = UserDefaults.standard.value(forKey: "painterColorLocation") as? CGFloat + let weightValue = UserDefaults.standard.value(forKey: "painterBrushWeight") as? CGFloat + + + let colorLocation: CGFloat + if let colorValue = colorValue { + colorLocation = colorValue + } else { + colorLocation = CGFloat(arc4random()) / CGFloat(UInt32.max) + UserDefaults.standard.setValue(colorLocation, forKey: "painterColorLocation") + } + self.location = colorLocation + knobView.color = backgroundView.color(for: colorLocation) + + let weight = weightValue ?? paintDefaultBrushWeight + knobView.updateWeight(weight, animated: false) + + } + + private var location: CGFloat = 0 + + private func updateLocation(animated: Bool) { + + guard let window = self.window else { + return + } + + let location = backgroundView.convert(window.mouseLocationOutsideOfEventStream, from: nil) + + let colorLocation = max(0.0, min(1.0, location.x / backgroundView.frame.width)) + self.location = colorLocation + + + knobView.color = backgroundView.color(for: colorLocation) + + let threshold = min(max(frame.height - backgroundView.frame.minY, frame.height - self.convert(window.mouseLocationOutsideOfEventStream, from: nil).y), paintColorWeightGestureRange + paintPreviewOffset) + + let weight = threshold / (paintColorWeightGestureRange + paintPreviewOffset); + + + knobView.updateWeight(weight, animated: animated) + + arguments?.updateColorAndWidth(knobView.color, knobView.width) + + UserDefaults.standard.set(Double(colorLocation), forKey: "painterColorLocation") + UserDefaults.standard.set(Double(weight), forKey: "painterBrushWeight") + + + if animated { + knobView.layer?.animatePosition(from: NSMakePoint(knobView.frame.minX - knobPosition.x, knobView.frame.minY - knobPosition.y), to: .zero, duration: 0.3, timingFunction: .spring, removeOnCompletion: true, additive: true) + } + + needsLayout = true + + + } + + override var isEventLess: Bool { + get { + guard let window = self.window else { + return false + } + let point = self.convert(window.mouseLocationOutsideOfEventStream, from: nil) + + if NSPointInRect(point, backgroundView.frame) || NSPointInRect(point, knobView.frame) { + return false + } else { + return true + } + } + set { + super.isEventLess = newValue + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var knobPosition: NSPoint { + + guard let window = self.window else { + return .zero + } + + let threshold: CGFloat + if knobView.isZoomed { + threshold = min(max(0, frame.height - self.convert(window.mouseLocationOutsideOfEventStream, from: nil).y - (frame.height - backgroundView.frame.minY)), paintColorWeightGestureRange) + } else { + threshold = 0 + } + let knobY: CGFloat = max(0, (backgroundView.frame.midY - knobView.frame.height / 2) + (knobView.isZoomed ? paintPreviewOffset : 0) + -threshold) + let knobX: CGFloat = max(0, min(backgroundView.frame.width * location, self.frame.width - knobView.frame.width)) + return NSMakePoint(knobX, knobY) + } + + override func layout() { + super.layout() + backgroundView.frame = NSMakeRect(24, frame.height - 24, frame.width - 48, 20) + knobView.frame = CGRect(x: knobPosition.x, y: knobPosition.y, width: knobView.frame.size.width, height: knobView.frame.size.height) + + } +} diff --git a/Telegram-Mac/EditImageCanvasController.swift b/Telegram-Mac/EditImageCanvasController.swift new file mode 100644 index 0000000000..4df53eba5f --- /dev/null +++ b/Telegram-Mac/EditImageCanvasController.swift @@ -0,0 +1,515 @@ +// +// EditImageSticker.swift +// Telegram +// +// Created by Mikhail Filimonov on 16/04/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit + +final class EditImageCanvasArguments { + let makeNewActionAt:(NSPoint)->Void + let addToLastAction:(NSPoint)->Void + + let switchAction:(EditImageDrawTouch.Action)->Void + let undo:()->Void + let redo:()->Void + let updateColorAndWidth:(NSColor, CGFloat)->Void + + let save:()->Void + let cancel:()->Void + init(makeNewActionAt:@escaping(NSPoint)->Void, addToLastAction:@escaping(NSPoint)->Void, switchAction:@escaping(EditImageDrawTouch.Action)->Void, undo: @escaping()->Void, redo: @escaping()->Void, updateColorAndWidth: @escaping(NSColor, CGFloat)->Void, save: @escaping()->Void, cancel: @escaping()->Void) { + self.makeNewActionAt = makeNewActionAt + self.addToLastAction = addToLastAction + self.switchAction = switchAction + self.undo = undo + self.redo = redo + self.updateColorAndWidth = updateColorAndWidth + self.save = save + self.cancel = cancel + } +} + +private extension CGMutablePath { + func addArrow(start: CGPoint, end: CGPoint, pointerLineLength: CGFloat, arrowAngle: CGFloat) { + self.move(to: start) + self.addLine(to: end) + + let startEndAngle = atan((end.y - start.y) / (end.x - start.x)) + ((end.x - start.x) < 0 ? CGFloat(Double.pi) : 0) + let arrowLine1 = CGPoint(x: end.x + pointerLineLength * cos(CGFloat(Double.pi) - startEndAngle + arrowAngle), y: end.y - pointerLineLength * sin(CGFloat(Double.pi) - startEndAngle + arrowAngle)) + let arrowLine2 = CGPoint(x: end.x + pointerLineLength * cos(CGFloat(Double.pi) - startEndAngle - arrowAngle), y: end.y - pointerLineLength * sin(CGFloat(Double.pi) - startEndAngle - arrowAngle)) + + self.addLine(to: arrowLine1) + self.move(to: end) + self.addLine(to: arrowLine2) + } +} + + +func applyPaints(_ touches: [EditImageDrawTouch], for context: CGContext, imageSize: NSSize) { + context.saveGState() + for touch in touches { + context.beginPath() + + let multiplier = NSMakePoint(imageSize.width / touch.canvasSize.width, imageSize.height / touch.canvasSize.height) + let lineWidth = touch.width * ((multiplier.x + multiplier.y) / 2) + + + switch touch.action { + case .draw, .clear: + for (i, point) in touch.lines.enumerated() { + let point = NSMakePoint(point.x * multiplier.x, point.y * multiplier.y) + if i == 0 { + context.move(to: point) + } else { + context.addLine(to: point) + } + } + context.setLineWidth(lineWidth) + default: + break + } + + context.setLineCap(.round) + context.setLineJoin(.round) + context.setStrokeColor(touch.color.cgColor) + + + switch touch.action { + case .draw: + context.setBlendMode(.normal) + case .clear: + context.setBlendMode(.clear) + case .drawArrow: + context.setBlendMode(.normal) + let path = CGMutablePath() + context.setLineWidth(lineWidth * 0.7) + if touch.lines.count > 1 { + let first = touch.lines.first! + let last = touch.lines.last! + let dif = last - first + if abs(dif.x) > lineWidth * 1.5 || abs(dif.y) > lineWidth * 1.5 { + path.addArrow(start: first, end: last, pointerLineLength: lineWidth * 2.5, arrowAngle: CGFloat(Double.pi / 4)) + context.addPath(path) + } + } + } + context.strokePath() + } + context.restoreGState() + context.setBlendMode(.normal) +} + +final class EditImageDrawTouch : Equatable { + + enum Action : Hashable { + case draw + case drawArrow + case clear + } + + + static func == (lhs: EditImageDrawTouch, rhs: EditImageDrawTouch) -> Bool { + return lhs.lines == rhs.lines && + lhs.color.argb == rhs.color.argb && + lhs.width == rhs.width && + lhs.action == rhs.action && + lhs.canvasSize == rhs.canvasSize + } + private(set) var lines:[NSPoint] + let color: NSColor + let width: CGFloat + let action: Action + let canvasSize: NSSize + init(action: Action, point: NSPoint, canvasSize: NSSize, color: NSColor, width: CGFloat) { + self.action = action + self.lines = [point] + self.color = color + self.width = width + self.canvasSize = canvasSize + } + func addPoint(_ point: NSPoint) { + self.lines.append(point) + } +} + +class EditImageDrawView: Control { + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + } + fileprivate var arguments: EditImageCanvasArguments? = nil + private(set) fileprivate var state:EditImageCanvasState = EditImageCanvasState.default([]) + + func update(with state: EditImageCanvasState) { + self.state = state + needsDisplay = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + needsDisplay = true + } + + override func draw(_ layer: CALayer, in context: CGContext) { + super.draw(layer, in: context) + applyPaints(self.state.actionValues, for: context, imageSize: self.frame.size) + } + + override func mouseDown(with event: NSEvent) { + super.mouseDown(with: event) + + let point = self.convert(event.locationInWindow, from: nil) + arguments?.makeNewActionAt(point) + } + + override func mouseUp(with event: NSEvent) { + super.mouseUp(with: event) + } + + override func mouseDragged(with event: NSEvent) { + super.mouseDragged(with: event) + + let point = self.convert(event.locationInWindow, from: nil) + arguments?.addToLastAction(point) + + } +} + + +final class EditImageCanvasView : View { + + let imageContainer: View = View() + + // let magnifyView: MagnifyView + + let imageView: ImageView = ImageView() + let drawView: EditImageDrawView = EditImageDrawView(frame: .zero) + + let colorPicker: EditImageColorPicker + let shadowView: View = View() + private let controls: EditImageCanvasControlsView = EditImageCanvasControlsView(frame: NSMakeRect(0, 0, 350, 40)) + required init(frame frameRect: NSRect, image: CGImage) { + self.imageView.image = image + colorPicker = EditImageColorPicker(frame: NSMakeRect(0, 0, 348, 200)) + super.init(frame: frameRect) + + shadowView.isEventLess = true + + imageView.background = .white + + addSubview(imageContainer) + imageContainer.addSubview(imageView) + + + imageContainer.addSubview(drawView) + addSubview(colorPicker) + addSubview(controls) + + } + + fileprivate var arguments: EditImageCanvasArguments? = nil { + didSet { + controls.arguments = arguments + drawView.arguments = arguments + colorPicker.arguments = arguments + } + } + + + func update(with state: EditImageCanvasState) { + controls.update(with: state) + drawView.update(with: state) + } + + override func layout() { + super.layout() + imageContainer.setFrameSize(frame.width, frame.height - 120) + + let imageSize = self.imageView.image!.size.fitted(NSMakeSize(imageContainer.frame.width - 8, imageContainer.frame.height - 8)) + self.imageView.setFrameSize(imageSize) + + self.imageView.center() + self.drawView.frame = imageView.frame + + + controls.centerX(y: frame.height - controls.frame.height) + colorPicker.centerX(y: controls.frame.minY - colorPicker.frame.height - 20) + } + + func contentSize(maxSize: NSSize) -> NSSize { + return NSMakeSize(maxSize.width, maxSize.height) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } +} + + +struct EditImageCanvasState : Equatable { + let action: EditImageDrawTouch.Action + + let color: NSColor + let width: CGFloat + + let actionValues:[EditImageDrawTouch] + let removedActions:[EditImageDrawTouch] + + init(action: EditImageDrawTouch.Action, actionValues:[EditImageDrawTouch], removedActions:[EditImageDrawTouch], color: NSColor, width: CGFloat) { + self.action = action + self.actionValues = actionValues + self.color = color + self.width = width + self.removedActions = removedActions + } + + func withUpdatedAction(_ action: EditImageDrawTouch.Action) -> EditImageCanvasState { + return EditImageCanvasState(action: action, actionValues: self.actionValues, removedActions: self.removedActions, color: self.color, width: self.width) + } + + func withUpdatedCurrentActionValues(_ f:([EditImageDrawTouch])->[EditImageDrawTouch]) -> EditImageCanvasState { + return EditImageCanvasState(action: action, actionValues: f(self.actionValues), removedActions: self.removedActions, color: self.color, width: self.width) + } + + func withAddedRemovedAction(_ action: EditImageDrawTouch) -> EditImageCanvasState { + + var removedActions = self.removedActions + removedActions.append(action) + + return EditImageCanvasState(action: self.action, actionValues: self.actionValues, removedActions: removedActions, color: self.color, width: self.width) + } + + func withReturnedRemovedAction() -> EditImageCanvasState { + + var removedActions = self.removedActions + var actionValues = self.actionValues + if !removedActions.isEmpty { + let last = removedActions.removeLast() + actionValues.append(last) + } else { + NSSound.beep() + } + return EditImageCanvasState(action: self.action, actionValues: actionValues, removedActions: removedActions, color: self.color, width: self.width) + } + + func withClearedRemovedActions() -> EditImageCanvasState { + return EditImageCanvasState(action: self.action, actionValues: self.actionValues, removedActions: [], color: self.color, width: self.width) + } + + func withUpdateColorAndWidth(_ color: NSColor, _ width: CGFloat) -> EditImageCanvasState { + return EditImageCanvasState(action: self.action, actionValues: self.actionValues, removedActions: self.removedActions, color: color, width: width) + } + + static func `default`(_ actions: [EditImageDrawTouch]) -> EditImageCanvasState { + return EditImageCanvasState(action: .draw, actionValues: actions, removedActions: [], color: .random, width: 6) + } +} + +final class EditImageCanvasController : ModalViewController { + private let disposable = MetaDisposable() + private let image: CGImage + private let actions: [EditImageDrawTouch] + private let updatedImage: ([EditImageDrawTouch])->Void + private let closeHandler: ()->Void + private let alone: Bool + init(image: CGImage, actions: [EditImageDrawTouch], updatedImage: @escaping([EditImageDrawTouch])->Void, closeHandler: @escaping() -> Void, alone: Bool = false) { + self.stateValue = Atomic(value: EditImageCanvasState.default(actions)) + self.state = ValuePromise(EditImageCanvasState.default(actions), ignoreRepeated: false) + self.image = image + self.alone = alone + self.actions = actions + self.updatedImage = updatedImage + self.closeHandler = closeHandler + super.init() + bar = .init(height: 0) + } + + private let stateValue: Atomic + private let state: ValuePromise + + override var containerBackground: NSColor { + return .clear + } + override var isVisualEffectBackground: Bool { + if alone { + return true + } else { + return false + } + } + + override func close(animationType: ModalAnimationCloseBehaviour = .common) { + super.close(animationType: animationType) + self.closeHandler() + } + + + override var background: NSColor { + return .clear + } + + + + override func returnKeyAction() -> KeyHandlerResult { + self.updatedImage(stateValue.with { $0.actionValues} ) + close() + return .invoked + } + + override func measure(size: NSSize) { + if let contentSize = self.modal?.window.contentView?.frame.size { + self.modal?.resize(with: genericView.contentSize(maxSize: NSMakeSize(contentSize.width - 80, contentSize.height - 80)), animated: false) + } + } + + func updateSize(_ animated: Bool) { + if let contentSize = self.modal?.window.contentView?.frame.size { + self.modal?.resize(with: genericView.contentSize(maxSize: NSMakeSize(contentSize.width - 80, contentSize.height - 80)), animated: animated) + } + } + + override var dynamicSize: Bool { + return true + } + + override func viewClass() -> AnyClass { + return EditImageCanvasView.self + } + + private var genericView: EditImageCanvasView { + return self.view as! EditImageCanvasView + } + + override func viewDidLoad() { + super.viewDidLoad() + + + let updateState:((EditImageCanvasState)->EditImageCanvasState)->Void = { [weak self] f in + guard let `self` = self else { + return + } + self.state.set(self.stateValue.modify(f)) + } + + let arguments = EditImageCanvasArguments(makeNewActionAt: { [weak self] point in + let canvasSize = self?.genericView.drawView.frame.size ?? .zero + updateState { state in + return state.withUpdatedCurrentActionValues { touches in + var touches = touches + touches.append(EditImageDrawTouch(action: state.action, point: point, canvasSize: canvasSize, color: state.color, width: state.width)) + return touches + }.withClearedRemovedActions() + } + }, addToLastAction: { point in + updateState { state in + return state.withUpdatedCurrentActionValues { touches in + touches.last?.addPoint(point) + return touches + } + } + }, switchAction: { action in + updateState { state in + if action == .draw && state.action == .draw { + return state.withUpdatedAction(.drawArrow) + } else if action == .drawArrow && state.action == .drawArrow { + return state.withUpdatedAction(.draw) + } + return state.withUpdatedAction(action) + } + }, undo: { + updateState { state in + var state = state + var lastAction: EditImageDrawTouch? + state = state.withUpdatedCurrentActionValues { touches in + var touches = touches + if !touches.isEmpty { + lastAction = touches.removeLast() + } else { + NSSound.beep() + } + return touches + } + if let lastAction = lastAction { + state = state.withAddedRemovedAction(lastAction) + } + return state + } + }, redo: { + updateState { + $0.withReturnedRemovedAction() + } + }, updateColorAndWidth: { color, width in + updateState { + $0.withUpdateColorAndWidth(color, width) + } + }, save: { [weak self] in + _ = self?.returnKeyAction() + }, cancel: { [weak self] in + self?.close() + }) + + genericView.arguments = arguments + + disposable.set(state.get().start(next: { [weak self] state in + self?.genericView.update(with: state) + })) + + readyOnce() + } + override func initializer() -> NSView { + let vz = viewClass() as! EditImageCanvasView.Type + return vz.init(frame: NSMakeRect(_frameRect.minX, _frameRect.minY, _frameRect.width, _frameRect.height - bar.height), image: image); + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + + + window?.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.genericView.arguments?.undo() + return .invoked + }, with: self, for: .Z, priority: .modal, modifierFlags: [.command]) + + window?.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.genericView.arguments?.redo() + return .invoked + }, with: self, for: .Z, priority: .modal, modifierFlags: [.command, .shift]) + + + window?.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.genericView.arguments?.switchAction(.clear) + return .invoked + }, with: self, for: .E, priority: .modal) + + window?.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.genericView.arguments?.switchAction(.draw) + return .invoked + }, with: self, for: .L, priority: .modal) + + window?.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.genericView.arguments?.switchAction(.drawArrow) + return .invoked + }, with: self, for: .A, priority: .modal) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + window?.removeAllHandlers(for: self) + } + + deinit { + disposable.dispose() + } +} diff --git a/Telegram-Mac/EditImageCanvasControls.swift b/Telegram-Mac/EditImageCanvasControls.swift new file mode 100644 index 0000000000..ddb1d57032 --- /dev/null +++ b/Telegram-Mac/EditImageCanvasControls.swift @@ -0,0 +1,145 @@ +// +// EditImageCanvasControls.swift +// Telegram +// +// Created by Mikhail Filimonov on 16/04/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + + + + +final class EditImageCanvasControlsView : View { + let cancel = TitleButton() + private let success = TitleButton() + private let controlsContainer = View() + private let undo = ImageButton() + private let redo = ImageButton() + private let draw = ImageButton() + private let clear = ImageButton() + + private var currentData: EditedImageData? + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + + addSubview(cancel) + addSubview(success) + + addSubview(controlsContainer) + + controlsContainer.addSubview(undo) + controlsContainer.addSubview(redo) + controlsContainer.addSubview(draw) + controlsContainer.addSubview(clear) + + controlsContainer.border = [.Left, .Right] + controlsContainer.borderColor = NSColor.black.withAlphaComponent(0.2) + backgroundColor = NSColor(0x303030) + layer?.cornerRadius = 6 + + updateUserInterface() + } + + fileprivate func updateUserInterface() { + undo.set(image: NSImage(named: "Icon_EditImageUndo")!.precomposed(NSColor.white.withAlphaComponent(0.8)), for: .Normal) + redo.set(image: NSImage(named: "Icon_EditImageUndo")!.precomposed(NSColor.white.withAlphaComponent(0.8), flipHorizontal: true), for: .Normal) + draw.set(image: NSImage(named: "Icon_EditImageDraw")!.precomposed(NSColor.white.withAlphaComponent(0.8)), for: .Normal) + clear.set(image: NSImage(named: "Icon_EditImageEraser")!.precomposed(NSColor.white.withAlphaComponent(0.8)), for: .Normal) + + undo.appTooltip = L10n.canvasUndo + redo.appTooltip = L10n.canvasRedo + draw.appTooltip = L10n.canvasDraw + clear.appTooltip = L10n.canvasClear + + undo.set(image: NSImage(named: "Icon_EditImageUndo")!.precomposed(.white), for: .Hover) + redo.set(image: NSImage(named: "Icon_EditImageUndo")!.precomposed(NSColor.white, flipHorizontal: true), for: .Hover) + draw.set(image: NSImage(named: "Icon_EditImageDraw")!.precomposed(.white), for: .Hover) + clear.set(image: NSImage(named: "Icon_EditImageEraser")!.precomposed(.white), for: .Hover) + + + _ = undo.sizeToFit(NSZeroSize, NSMakeSize(50, frame.height), thatFit: true) + _ = redo.sizeToFit(NSZeroSize, NSMakeSize(50, frame.height), thatFit: true) + _ = draw.sizeToFit(NSZeroSize, NSMakeSize(50, frame.height), thatFit: true) + _ = clear.sizeToFit(NSZeroSize, NSMakeSize(50, frame.height), thatFit: true) + + cancel.set(font: .medium(.title), for: .Normal) + success.set(font: .medium(.title), for: .Normal) + + success.set(color: nightAccentPalette.accent, for: .Normal) + cancel.set(color: .white, for: .Normal) + + cancel.set(text: L10n.modalCancel, for: .Normal) + success.set(text: L10n.navigationDone, for: .Normal) + + _ = cancel.sizeToFit(NSZeroSize, NSMakeSize(75, frame.height), thatFit: true) + _ = success.sizeToFit(NSZeroSize, NSMakeSize(75, frame.height), thatFit: true) + + + undo.set(handler: { [weak self] _ in + self?.arguments?.undo() + }, for: .Click) + + redo.set(handler: { [weak self] _ in + self?.arguments?.redo() + }, for: .Click) + + draw.set(handler: { [weak self] _ in + self?.arguments?.switchAction(.draw) + }, for: .Click) + + clear.set(handler: { [weak self] _ in + self?.arguments?.switchAction(.clear) + }, for: .Click) + + + success.set(handler: { [weak self] _ in + self?.arguments?.save() + }, for: .Click) + + cancel.set(handler: { [weak self] _ in + self?.arguments?.cancel() + }, for: .Click) + } + + override func layout() { + super.layout() + controlsContainer.setFrameSize(draw.frame.width + undo.frame.width + redo.frame.width + clear.frame.width, draw.frame.height) + undo.setFrameOrigin(NSMakePoint(0, 0)) + redo.setFrameOrigin(NSMakePoint(undo.frame.maxX, 0)) + draw.setFrameOrigin(NSMakePoint(redo.frame.maxX, 0)) + clear.setFrameOrigin(NSMakePoint(draw.frame.maxX, 0)) + controlsContainer.center() + + cancel.centerY() + success.centerY(x: frame.width - success.frame.width) + } + var arguments: EditImageCanvasArguments? = nil + + func update(with state: EditImageCanvasState) { + + undo.isEnabled = !state.actionValues.isEmpty + redo.isEnabled = !state.removedActions.isEmpty + draw.isSelected = state.action == .draw || state.action == .drawArrow + clear.isSelected = state.action == .clear + + let drawImage: NSImage + switch state.action { + case .drawArrow: + drawImage = NSImage(named: "Icon_EditImageArrow")! + default: + drawImage = NSImage(named: "Icon_EditImageDraw")! + } + + draw.set(image: drawImage.precomposed(NSColor.white.withAlphaComponent(0.8)), for: .Normal) + draw.set(image: drawImage.precomposed(.white), for: .Hover) + + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/EditImageControls.swift b/Telegram-Mac/EditImageControls.swift new file mode 100644 index 0000000000..e8a95dc805 --- /dev/null +++ b/Telegram-Mac/EditImageControls.swift @@ -0,0 +1,317 @@ +// +// EditImageControls.swift +// Telegram +// +// Created by Mikhail Filimonov on 08/10/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit + +struct EditedImageData : Equatable { + let originalUrl: URL + let selectedRect: NSRect + let orientation: ImageOrientation? + let dimensions: SelectionRectDimensions + let isHorizontalFlipped: Bool + let paintings: [EditImageDrawTouch] + init(originalUrl: URL, selectedRect: NSRect = NSZeroRect, orientation: ImageOrientation? = nil, dimensions: SelectionRectDimensions = .none, isHorizontalFlipped: Bool = false, paintings: [EditImageDrawTouch] = []) { + self.originalUrl = originalUrl + self.dimensions = dimensions + self.selectedRect = selectedRect + self.orientation = orientation + self.isHorizontalFlipped = isHorizontalFlipped + self.paintings = paintings + } + var hasntData: Bool { + return orientation == nil && dimensions == .none && !isHorizontalFlipped && paintings.isEmpty + } + + + func withUpdatedOrientation( _ orientation: ImageOrientation?) -> EditedImageData { + return EditedImageData(originalUrl: self.originalUrl, selectedRect: selectedRect, orientation: orientation, dimensions: self.dimensions, isHorizontalFlipped: self.isHorizontalFlipped, paintings: self.paintings) + } + func withUpdatedFlip(_ isHorizontalFlipped: Bool) -> EditedImageData { + return EditedImageData(originalUrl: self.originalUrl, selectedRect: self.selectedRect, orientation: self.orientation, dimensions: self.dimensions, isHorizontalFlipped: isHorizontalFlipped, paintings: self.paintings) + } + func withUpdatedDimensions(_ dimensions: SelectionRectDimensions) -> EditedImageData { + return EditedImageData(originalUrl: self.originalUrl, selectedRect: self.selectedRect, orientation: self.orientation, dimensions: dimensions, isHorizontalFlipped: self.isHorizontalFlipped, paintings: self.paintings) + } + + func withUpdatedSelectedRect(_ selectedRect: NSRect) -> EditedImageData { + return EditedImageData(originalUrl: self.originalUrl, selectedRect: selectedRect, orientation: self.orientation, dimensions: self.dimensions, isHorizontalFlipped: self.isHorizontalFlipped, paintings: self.paintings) + } + + func withUpdatedPaintings(_ paintings: [EditImageDrawTouch]) -> EditedImageData { + return EditedImageData(originalUrl: self.originalUrl, selectedRect: self.selectedRect, orientation: self.orientation, dimensions: self.dimensions, isHorizontalFlipped: self.isHorizontalFlipped, paintings: paintings) + } + + func makeImage(_ image: CGImage) -> CGImage { + return EditedImageData.makeImage(image, data: self) + } + + func isNeedToRegenerate(_ data: EditedImageData?) -> Bool { + return data?.orientation != self.orientation || (data != nil && data!.isHorizontalFlipped != self.isHorizontalFlipped) || (data != nil && data!.paintings != self.paintings) || data == nil + } + + fileprivate static func makeImage(_ image: CGImage, data: EditedImageData) -> CGImage { + var image: CGImage = image + var orientation = data.orientation + + if !data.paintings.isEmpty { + image = generateImage(image.size, scale: 1.0, rotatedContext: { size, context in + let rect = NSMakeRect(0, 0, size.width, size.height) + context.clear(rect) + + context.saveGState() + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: 1, y: -1.0) + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + context.draw(image, in: rect) + context.restoreGState() + + let paints = generateImage(size, contextGenerator: { size, ctx in + ctx.clear(rect) + applyPaints(data.paintings, for: ctx, imageSize: size) + }, scale: 1.0)! + + context.draw(paints, in: rect) + + })! + } + + if data.isHorizontalFlipped, let temp = orientation { + switch temp { + case .left: + orientation = .leftMirrored + case .right: + orientation = .rightMirrored + case .down: + orientation = .downMirrored + default: + orientation = nil + } + } + if let orientation = orientation { + image = image.createMatchingBackingDataWithImage(orienation: orientation)! + } else if data.isHorizontalFlipped { + return generateImage(image.size, contextGenerator: { size, ctx in + ctx.translateBy(x: size.width / 2.0, y: size.height / 2.0) + ctx.scaleBy(x: -1.0, y: 1.0) + ctx.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + + ctx.draw(image, in: NSMakeRect(0, 0, size.width, size.height)) + + ctx.translateBy(x: size.width / 2.0, y: size.height / 2.0) + ctx.scaleBy(x: -1.0, y: 1.0) + ctx.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + }, scale: 1.0)! + // image = NSImage(cgImage: image, size: image.backingSize).precomposed(flipHorizontal: true) + } + + return image + } + + static func generateNewUrl(data: EditedImageData, selectedRect: NSRect) -> Signal { + return Signal { subscriber in + + if let image = NSImage(contentsOf: data.originalUrl)?.cgImage(forProposedRect: nil, context: nil, hints: nil) { + if selectedRect == NSMakeRect(0, 0, image.size.width, image.size.height) && data.hasntData { + subscriber.putNext(data.originalUrl) + subscriber.putCompletion() + } else { + if let image = self.makeImage(image, data: data).cropping(to: selectedRect) { + return putToTemp(image: NSImage(cgImage: image, size: image.size), compress: true).start(next: { url in + subscriber.putNext(URL(fileURLWithPath: url)) + }, error: { error in + subscriber.putError(error) + }, completed: { + subscriber.putCompletion() + }) + } + } + + + } + subscriber.putCompletion() + return EmptyDisposable + } |> runOn(resourcesQueue) + } + +} + +final class EditImageControlsArguments { + let cancel:()->Void + let success: () -> Void + let flip: () -> Void + let selectionDimensions: (SelectionRectDimensions) -> Void + let rotate: () -> Void + let draw: ()->Void + init(cancel:@escaping()->Void, success: @escaping()->Void, flip: @escaping()->Void, selectionDimensions: @escaping(SelectionRectDimensions)->Void, rotate: @escaping() -> Void, draw: @escaping()->Void) { + self.cancel = cancel + self.success = success + self.flip = flip + self.rotate = rotate + self.selectionDimensions = selectionDimensions + self.draw = draw + } +} + +final class EditImageControlsView : View { + let cancel = TitleButton() + private let success = TitleButton() + private let controlsContainer = View() + private let flipper = ImageButton() + private let draw = ImageButton() + private let rotate = ImageButton() + private let dimensions = ImageButton() + private var currentData: EditedImageData? + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + + addSubview(cancel) + addSubview(success) + + addSubview(controlsContainer) + + controlsContainer.addSubview(flipper) + controlsContainer.addSubview(draw) + controlsContainer.addSubview(rotate) + controlsContainer.addSubview(dimensions) + + controlsContainer.border = [.Left, .Right] + controlsContainer.borderColor = NSColor.black.withAlphaComponent(0.2) + backgroundColor = NSColor(0x303030) + layer?.cornerRadius = 6 + } + + fileprivate func updateUserInterface(_ data: EditedImageData) { + draw.set(image: NSImage(named: "Icon_EditImageDraw")!.precomposed(NSColor.white.withAlphaComponent(0.8)), for: .Normal) + flipper.set(image: NSImage(named: "Icon_EditImageFlip")!.precomposed(NSColor.white.withAlphaComponent(0.8)), for: .Normal) + rotate.set(image: NSImage(named: "Icon_EditImageRotate")!.precomposed(NSColor.white.withAlphaComponent(0.8)), for: .Normal) + dimensions.set(image: NSImage(named: "Icon_EditImageSizes")!.precomposed(NSColor.white.withAlphaComponent(0.8)), for: .Normal) + + draw.set(image: NSImage(named: "Icon_EditImageDraw")!.precomposed(.white), for: .Hover) + flipper.set(image: NSImage(named: "Icon_EditImageFlip")!.precomposed(.white), for: .Hover) + rotate.set(image: NSImage(named: "Icon_EditImageRotate")!.precomposed(.white), for: .Hover) + dimensions.set(image: NSImage(named: "Icon_EditImageSizes")!.precomposed(.white), for: .Hover) + + draw.appTooltip = "⌘D" + rotate.appTooltip = "⌘R" + + _ = draw.sizeToFit(NSZeroSize, NSMakeSize(50, frame.height), thatFit: true) + _ = flipper.sizeToFit(NSZeroSize, NSMakeSize(50, frame.height), thatFit: true) + _ = rotate.sizeToFit(NSZeroSize, NSMakeSize(50, frame.height), thatFit: true) + _ = dimensions.sizeToFit(NSZeroSize, NSMakeSize(50, frame.height), thatFit: true) + + cancel.set(font: .medium(.title), for: .Normal) + success.set(font: .medium(.title), for: .Normal) + + success.set(color: nightAccentPalette.accent, for: .Normal) + cancel.set(color: .white, for: .Normal) + + cancel.set(text: L10n.modalCancel, for: .Normal) + success.set(text: L10n.navigationDone, for: .Normal) + + _ = cancel.sizeToFit(NSZeroSize, NSMakeSize(75, frame.height), thatFit: true) + _ = success.sizeToFit(NSZeroSize, NSMakeSize(75, frame.height), thatFit: true) + + flipper.isSelected = data.isHorizontalFlipped + rotate.isSelected = data.orientation != nil + dimensions.isSelected = data.dimensions != .none + draw.isSelected = !data.paintings.isEmpty + } + + override func layout() { + super.layout() + controlsContainer.setFrameSize(rotate.frame.width + flipper.frame.width + draw.frame.width + dimensions.frame.width, rotate.frame.height) + rotate.setFrameOrigin(NSMakePoint(0, 0)) + flipper.setFrameOrigin(NSMakePoint(rotate.frame.maxX, 0)) + draw.setFrameOrigin(NSMakePoint(flipper.frame.maxX, 0)) + dimensions.setFrameOrigin(NSMakePoint(draw.frame.maxX, 0)) + controlsContainer.center() + + cancel.centerY() + success.centerY(x: frame.width - success.frame.width) + } + + override func setFrameSize(_ newSize: NSSize) { + super.setFrameSize(newSize) + } + + fileprivate func set(settings: EditControllerSettings, handlers: EditImageControlsArguments) { + + success.set(handler: { _ in + handlers.success() + }, for: .Click) + + cancel.set(handler: { _ in + handlers.cancel() + }, for: .Click) + + flipper.set(handler: { control in + handlers.flip() + }, for: .Click) + + rotate.set(handler: { _ in + handlers.rotate() + }, for: .Click) + + draw.set(handler: { _ in + handlers.draw() + }, for: .Click) + + dimensions.set(handler: { control in + switch settings { + case .disableSizes: + break + case .plain: + if control.isSelected { + handlers.selectionDimensions(.none) + } else { + let items: [SPopoverItem] = SelectionRectDimensions.all.map { value in + return SPopoverItem(value.description, { + handlers.selectionDimensions(value) + }) + } + showPopover(for: control, with: SPopoverViewController(items: items, visibility: SelectionRectDimensions.all.count, handlerDelay: 0)) + } + } + + + }, for: .Click) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class EditImageControls: GenericViewController { + let arguments: EditImageControlsArguments + private let stateDisposable = MetaDisposable() + private let stateValue: Signal + private let settings: EditControllerSettings + init(settings:EditControllerSettings, arguments: EditImageControlsArguments, stateValue: Signal) { + self.arguments = arguments + self.stateValue = stateValue + self.settings = settings + super.init(frame: NSMakeRect(0, 0, 350, 40)) + bar = .init(height: 0) + } + + override func viewDidLoad() { + super.viewDidLoad() + + genericView.set(settings: settings, handlers: arguments) + stateDisposable.set(stateValue.start(next: { [weak self] current in + self?.genericView.updateUserInterface(current) + })) + } + + deinit { + stateDisposable.dispose() + } +} diff --git a/Telegram-Mac/EditImageModalController.swift b/Telegram-Mac/EditImageModalController.swift new file mode 100644 index 0000000000..b02b065190 --- /dev/null +++ b/Telegram-Mac/EditImageModalController.swift @@ -0,0 +1,446 @@ +// +// EditImagModalController.swift +// Telegram +// +// Created by Mikhail Filimonov on 01/10/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit + + + + + +private final class EditImageView : View { + fileprivate let imageView: ImageView = ImageView() + private var image: CGImage + fileprivate let selectionRectView: SelectionRectView = SelectionRectView(frame: NSMakeRect(0, 0, 100, 100)) + private let imageContainer: View = View() + private let reset: TitleButton = TitleButton() + private var currentData: EditedImageData? + private let fakeCorners: (topLeft: ImageView, topRight: ImageView, bottomLeft: ImageView, bottomRight: ImageView) + private var canReset: Bool = false + + required init(frame frameRect: NSRect, image: CGImage) { + self.image = image + fakeCorners = (topLeft: ImageView(), topRight: ImageView(), bottomLeft: ImageView(), bottomRight: ImageView()) + let corners = generateSelectionAreaCorners(.white) + fakeCorners.topLeft.image = corners.topLeft + fakeCorners.topRight.image = corners.topRight + fakeCorners.bottomLeft.image = corners.bottomLeft + fakeCorners.bottomRight.image = corners.bottomRight + + fakeCorners.topLeft.sizeToFit() + fakeCorners.topRight.sizeToFit() + fakeCorners.bottomLeft.sizeToFit() + fakeCorners.bottomRight.sizeToFit() + + imageView.background = .white + + super.init(frame: frameRect) + + + imageContainer.addSubview(fakeCorners.topLeft) + imageContainer.addSubview(fakeCorners.topRight) + imageContainer.addSubview(fakeCorners.bottomLeft) + imageContainer.addSubview(fakeCorners.bottomRight) + + + imageView.wantsLayer = true + imageView.image = image + addSubview(imageContainer) + imageContainer.addSubview(imageView) + imageView.addSubview(selectionRectView) + addSubview(reset) + // reset.isHidden = true + autoresizesSubviews = false + + + + reset.set(font: .medium(.title), for: .Normal) + reset.set(color: .white, for: .Normal) + reset.set(text: L10n.editImageControlReset, for: .Normal) + _ = reset.sizeToFit() + + } + + var controls: View? { + didSet { + oldValue?.removeFromSuperview() + if let controls = controls { + addSubview(controls) + } + } + } + + var selectedRect: NSRect { + let multiplierX = self.imageView.image!.size.width / selectionRectView.frame.width + let multiplierY = self.imageView.image!.size.height / selectionRectView.frame.height + let rect = NSMakeRect(selectionRectView.selectedRect.minX, selectionRectView.selectedRect.minY, selectionRectView.selectedRect.width, selectionRectView.selectedRect.height) + return rect.apply(multiplier: NSMakeSize(multiplierX, multiplierY)) + } + + fileprivate func updateVisibleCorners() { + + } + + func applyEditedData(_ value: EditedImageData, canReset: Bool, reset: @escaping()->Void) { + if value.isNeedToRegenerate(currentData) { + self.imageView.image = value.makeImage(self.image) + } + self.currentData = value + + setFrameSize(frame.size) + + + if value.selectedRect != NSZeroRect { + self.selectionRectView.applyRect(value.selectedRect, force: self.selectionRectView.dimensions != value.dimensions, dimensions: value.dimensions) + } else { + selectionRectView.applyRect(imageView.bounds, dimensions: value.dimensions) + } + self.canReset = canReset + self.reset.isHidden = !canReset + self.reset.removeAllHandlers() + self.reset.set(handler: { _ in + reset() + }, for: .Click) + + needsLayout = true + + } + + override func mouseUp(with event: NSEvent) { + super.mouseUp(with: event) + + if !imageView._mouseInside() && !controls!.mouseInside() && !selectionRectView.inDragging { + if let data = self.currentData, selectionRectView.isWholeSelected && data.hasntData { + (controls as? EditImageControlsView)?.cancel.send(event: .Click) + } else { + confirm(for: mainWindow, information: L10n.editImageControlConfirmDiscard, successHandler: { [weak self] _ in + (self?.controls as? EditImageControlsView)?.cancel.send(event: .Click) + }) + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + + override func setFrameSize(_ newSize: NSSize) { + let oldSize = self.frame.size + super.setFrameSize(newSize) + + imageContainer.setFrameSize(frame.width, frame.height - 120) + + let imageSize = imageView.image!.size.fitted(NSMakeSize(imageContainer.frame.width - 8, imageContainer.frame.height - 8)) + let oldImageSize = imageView.frame.size + imageView.setFrameSize(imageSize) + selectionRectView.frame = imageView.bounds + + imageView.center() + + if oldSize != newSize, oldSize != NSZeroSize, inLiveResize { + let multiplier = NSMakeSize(imageSize.width / oldImageSize.width, imageSize.height / oldImageSize.height) + selectionRectView.applyRect(selectionRectView.selectedRect.apply(multiplier: multiplier)) + } + + + if let controls = controls { + controls.centerX(y: frame.height - controls.frame.height) + reset.centerX(y: controls.frame.minY - (80 - reset.frame.height) / 2) + } + + } + + func hideElements(_ hide: Bool) { + imageContainer.isHidden = hide + reset.isHidden = hide || !canReset + } + + func contentSize(maxSize: NSSize) -> NSSize { + return NSMakeSize(maxSize.width, maxSize.height) + } + + override func layout() { + super.layout() + fakeCorners.topLeft.setFrameOrigin(selectionRectView.convert(selectionRectView.topLeftPosition, to: fakeCorners.topLeft.superview)) + fakeCorners.topRight.setFrameOrigin(selectionRectView.convert(selectionRectView.topRightPosition, to: fakeCorners.topRight.superview)) + + fakeCorners.bottomLeft.setFrameOrigin(selectionRectView.convert(selectionRectView.bottomLeftPosition, to: fakeCorners.bottomLeft.superview)) + fakeCorners.bottomRight.setFrameOrigin(selectionRectView.convert(selectionRectView.bottomRightPosition, to: fakeCorners.bottomRight.superview)) + } + +} + +enum EditControllerSettings { + case disableSizes(dimensions: SelectionRectDimensions) + case plain +} + +class EditImageModalController: ModalViewController { + private let path: URL + private let editValue: ValuePromise = ValuePromise(ignoreRepeated: true) + private let editState: Atomic + private let updateDisposable = MetaDisposable() + private let updatedRectDisposable = MetaDisposable() + private var controls: EditImageControls! + private let image: CGImage + private let settings: EditControllerSettings + private let resultValue: Promise<(URL, EditedImageData?)> = Promise() + private var canReset: Bool + + var onClose: () -> Void = {} + + init(_ path: URL, defaultData: EditedImageData? = nil, settings: EditControllerSettings = .plain) { + self.canReset = defaultData != nil + editState = Atomic(value: defaultData ?? EditedImageData(originalUrl: path)) + + self.image = NSImage(contentsOf: path)!.cgImage(forProposedRect: nil, context: nil, hints: nil)! + self.path = path + self.settings = settings + super.init() + bar = .init(height: 0) + editValue.set(defaultData ?? EditedImageData(originalUrl: path)) + } + + override func close(animationType: ModalAnimationCloseBehaviour = .common) { + super.close(animationType: animationType) + + onClose() + } + + + var result:Signal<(URL, EditedImageData?), NoError> { + return resultValue.get() + } + + private var markAsClosed: Bool = false + + override func returnKeyAction() -> KeyHandlerResult { + + guard !markAsClosed else { return .invoked } + + let currentData = editState.modify {$0} + resultValue.set(EditedImageData.generateNewUrl(data: currentData, selectedRect: genericView.selectedRect) |> map { ($0, $0 == currentData.originalUrl ? nil : currentData)}) + + + + let signal = resultValue.get() |> take(1) |> deliverOnMainQueue |> delay(0.1, queue: .mainQueue()) + markAsClosed = true + _ = signal.start(next: { [weak self] _ in + self?.close() + }) + + return .invoked + } + + override open func measure(size: NSSize) { + if let contentSize = self.modal?.window.contentView?.frame.size { + self.modal?.resize(with:genericView.contentSize(maxSize: NSMakeSize(contentSize.width - 80, contentSize.height - 80)), animated: false) + } + } + + public func updateSize(_ animated: Bool) { + if let contentSize = self.modal?.window.contentView?.frame.size { + self.modal?.resize(with:genericView.contentSize(maxSize: NSMakeSize(contentSize.width - 80, contentSize.height - 80)), animated: animated) + } + } + + override var dynamicSize: Bool { + return true + } + + override func initializer() -> NSView { + return EditImageView.init(frame: NSMakeRect(_frameRect.minX, _frameRect.minY, _frameRect.width, _frameRect.height - bar.height), image: image) + } + + override var containerBackground: NSColor { + return .clear + } + + override func viewClass() -> AnyClass { + return EditImageView.self + } + + private var genericView: EditImageView { + return self.view as! EditImageView + } + + override var background: NSColor { + return .clear + } + override var isVisualEffectBackground: Bool { + return true + } + + + private func updateValue(_ f:@escaping(EditedImageData) -> EditedImageData) { + self.editValue.set(editState.modify(f)) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + switch settings { + case let .disableSizes(dimensions): + let imageSize = self.genericView.imageView.frame.size + let size = NSMakeSize(200, 200).aspectFitted(imageSize) + let rect = NSMakeRect((imageSize.width - size.width) / 2, (imageSize.height - size.height) / 2, size.width, size.height) + genericView.selectionRectView.isCircleCap = true + updateValue { data in + return data.withUpdatedDimensions(dimensions).withUpdatedSelectedRect(rect) + } + default: + genericView.selectionRectView.isCircleCap = false + } + } + + override var responderPriority: HandlerPriority { + return .modal + } + + override var handleAllEvents: Bool { + return true + } + + private func loadCanvas() { + guard let window = self.window else { + return + } + genericView.hideElements(true) + showModal(with: EditImageCanvasController(image: self.image, actions: editState.with { $0.paintings }, updatedImage: { [weak self] paintings in + self?.updateValue { + $0.withUpdatedPaintings(paintings) + } + }, closeHandler: { [weak self] in + self?.genericView.hideElements(false) + }), for: window, animated: false, animationType: .alpha) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + window?.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.controls.arguments.rotate() + return .invoked + }, with: self, for: .R, priority: .modal, modifierFlags: [.command]) + + window?.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.loadCanvas() + return .invoked + }, with: self, for: .D, priority: .modal, modifierFlags: [.command]) + + + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + window?.removeAllHandlers(for: self) + } + + private func rotate() { + var rect = NSZeroRect + let imageSize = genericView.imageView.frame.size + var isFlipped: Bool = false + var newRotation: ImageOrientation? + self.updateValue { current in + rect = current.selectedRect + let orientation: ImageOrientation? + if let value = current.orientation { + switch value { + case .right: + orientation = .down + case .down: + orientation = .left + default: + orientation = nil + } + } else { + orientation = .right + } + newRotation = orientation + isFlipped = current.isHorizontalFlipped + return current.withUpdatedOrientation(orientation) + } + +// if isFlipped, let newRotation = newRotation, newRotation == .right { +// rect.origin.x = imageSize.width - rect.maxX +// } else if isFlipped, newRotation == nil { +// rect.origin.x = imageSize.width - rect.maxX +// } + + let newSize = genericView.imageView.frame.size + let multiplierWidth = newSize.height / imageSize.width + let multiplierHeight = newSize.width / imageSize.height + + rect = rect.rotate90Degress(parentSize: imageSize) + rect = rect.apply(multiplier: NSMakeSize(multiplierHeight, multiplierWidth)) + + self.updateValue { current in + return current.withUpdatedSelectedRect(rect) + } + } + + private func flip() { + let imageSize = genericView.imageView.frame.size + updateValue { value in + var rect = value.selectedRect + rect.origin.x = imageSize.width - rect.maxX + return value.withUpdatedFlip(!value.isHorizontalFlipped).withUpdatedSelectedRect(rect) + } + } + + + override func viewDidLoad() { + super.viewDidLoad() + + self.controls = EditImageControls(settings: settings, arguments: EditImageControlsArguments(cancel: { [weak self] in + self?.close() + }, success: { [weak self] in + _ = self?.returnKeyAction() + }, flip: { [weak self] in + self?.flip() + }, selectionDimensions: { [weak self] dimension in + self?.updateValue { value in + return value.withUpdatedDimensions(dimension) + } + }, rotate: { [weak self] in + self?.rotate() + }, draw: { [weak self] in + self?.loadCanvas() + }), stateValue: editValue.get()) + + + + + + genericView.controls = self.controls.genericView + updateDisposable.set((editValue.get() |> deliverOnMainQueue).start(next: { [weak self] data in + guard let `self` = self else {return} + self.readyOnce() + self.updateSize(false) + self.genericView.applyEditedData(data, canReset: self.canReset, reset: { [weak self] in + self?.canReset = false + self?.updateValue {$0.withUpdatedSelectedRect(NSZeroRect).withUpdatedFlip(false).withUpdatedDimensions(.none).withUpdatedOrientation(nil).withUpdatedPaintings([])} + }) + })) + + updatedRectDisposable.set(genericView.selectionRectView.updatedRect.start(next: { [weak self] rect in + self?.updateValue { $0.withUpdatedSelectedRect(rect) } + self?.genericView.updateVisibleCorners() + })) + } + + deinit { + updateDisposable.dispose() + updatedRectDisposable.dispose() + } + +} diff --git a/Telegram-Mac/EditMessageModel.swift b/Telegram-Mac/EditMessageModel.swift index d4c570450b..23cdf83dd2 100644 --- a/Telegram-Mac/EditMessageModel.swift +++ b/Telegram-Mac/EditMessageModel.swift @@ -8,30 +8,176 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac +import TelegramCore + +import SwiftSignalKit +import Postbox class EditMessageModel: ChatAccessoryModel { private var account:Account - private(set) var editMessage:Message - - init(message:Message , account:Account) { + private(set) var state:ChatEditState + private let fetchDisposable = MetaDisposable() + private var previousMedia: Media? + init(state:ChatEditState, account:Account) { self.account = account - self.editMessage = message + self.state = state super.init() - make(with :message) + make(with: state.message) + } + + override var view: ChatAccessoryView? { + didSet { + updateImageIfNeeded() + } + } + + override var size:NSSize { + didSet { + updateImageIfNeeded() + } + } + + override var frame: NSRect { + didSet { + updateImageIfNeeded() + } } + + override var leftInset: CGFloat { + var imageDimensions: CGSize? + let message = state.message + if !message.containsSecretMedia { + for media in message.media { + if let image = media as? TelegramMediaImage { + if let representation = largestRepresentationForPhoto(image) { + imageDimensions = representation.dimensions.size + } + break + } else if let file = media as? TelegramMediaFile, file.isVideo { + if let dimensions = file.dimensions?.size { + imageDimensions = dimensions + } else if let representation = largestImageRepresentation(file.previewRepresentations), !file.isStaticSticker { + imageDimensions = representation.dimensions.size + } + break + } + } + } + + + if let _ = imageDimensions { + return 30 + super.leftInset * 2 + } + + return super.leftInset + } + func make(with message:Message) -> Void { - self.headerAttr = .initialize(string: tr(.chatInputAccessoryEditMessage), color: theme.colors.blueUI, font: .medium(.text)) + let attr = NSMutableAttributedString() + _ = attr.append(string: L10n.chatInputAccessoryEditMessage, color: theme.colors.accent, font: .medium(.text)) + + self.headerAttr = attr self.messageAttr = .initialize(string: pullText(from:message) as String, color: message.media.isEmpty ? theme.colors.text : theme.colors.grayText, font: .normal(.text)) nodeReady.set(.single(true)) + updateImageIfNeeded() self.setNeedDisplay() } + private func updateImageIfNeeded() { + if let view = self.view, view.frame != NSZeroRect { + let message = self.state.message + var updatedMedia: Media? + var imageDimensions: CGSize? + var hasRoundImage = false + if !message.containsSecretMedia { + for media in message.media { + if let image = media as? TelegramMediaImage { + updatedMedia = image + if let representation = largestRepresentationForPhoto(image) { + imageDimensions = representation.dimensions.size + } + break + } else if let file = media as? TelegramMediaFile, file.isVideo { + updatedMedia = file + + if let dimensions = file.dimensions { + imageDimensions = dimensions.size + } else if let representation = largestImageRepresentation(file.previewRepresentations), !file.isStaticSticker { + imageDimensions = representation.dimensions.size + } + if file.isInstantVideo { + hasRoundImage = true + } + break + } + } + } + + + if let imageDimensions = imageDimensions { + let boundingSize = CGSize(width: 30.0, height: 30.0) + let arguments = TransformImageArguments(corners: ImageCorners(radius: 2.0), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: NSEdgeInsets()) + + if view.imageView == nil { + view.imageView = TransformImageView() + } + view.imageView?.setFrameSize(boundingSize) + if view.imageView?.superview == nil { + view.addSubview(view.imageView!) + } + view.imageView?.setFrameOrigin(super.leftInset + (self.isSideAccessory ? 10 : 0), floorToScreenPixels(System.backingScale, self.topOffset + (self.size.height - self.topOffset - boundingSize.height)/2)) + + + let mediaUpdated = true + + + var updateImageSignal: Signal? + if mediaUpdated { + if let image = updatedMedia as? TelegramMediaImage { + updateImageSignal = chatMessagePhotoThumbnail(account: self.account, imageReference: ImageMediaReference.message(message: MessageReference(message), media: image), scale: view.backingScaleFactor) + } else if let file = updatedMedia as? TelegramMediaFile { + if file.isVideo { + updateImageSignal = chatMessageVideoThumbnail(account: self.account, fileReference: FileMediaReference.message(message: MessageReference(message), media: file), scale: view.backingScaleFactor) + } else if let iconImageRepresentation = smallestImageRepresentation(file.previewRepresentations) { + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [iconImageRepresentation], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + updateImageSignal = chatWebpageSnippetPhoto(account: self.account, imageReference: ImageMediaReference.message(message: MessageReference(message), media: tmpImage), scale: view.backingScaleFactor, small: true) + } + } + } + + if let updateImageSignal = updateImageSignal, let media = updatedMedia { + view.imageView?.setSignal(signal: cachedMedia(media: media, arguments: arguments, scale: view.backingScaleFactor)) + view.imageView?.setSignal(updateImageSignal, animate: true, cacheImage: { [weak media] result in + if let media = media { + cacheMedia(result, media: media, arguments: arguments, scale: System.backingScale) + } + }) + if let media = media as? TelegramMediaImage, !media.isLocalResource { + self.fetchDisposable.set(chatMessagePhotoInteractiveFetched(account: self.account, imageReference: ImageMediaReference.message(message: MessageReference(message), media: media)).start()) + } + + view.imageView?.set(arguments: arguments) + if hasRoundImage { + view.imageView!.layer?.cornerRadius = 15 + } else { + view.imageView?.layer?.cornerRadius = 0 + } + } + } else { + view.imageView?.removeFromSuperview() + view.imageView = nil + } + + self.previousMedia = updatedMedia + } else { + self.view?.imageView?.removeFromSuperview() + self.view?.imageView = nil + } + } - - + deinit { + fetchDisposable.dispose() + } } diff --git a/Telegram-Mac/EditThemeController.swift b/Telegram-Mac/EditThemeController.swift new file mode 100644 index 0000000000..281eb35ce5 --- /dev/null +++ b/Telegram-Mac/EditThemeController.swift @@ -0,0 +1,479 @@ +// +// EditThemeController.swift +// Telegram +// +// Created by Mikhail Filimonov on 29/08/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit +import TelegramCore + +import Postbox + +private let _id_no_preview1 = InputDataIdentifier("_id_no_preview1") +private let _id_no_preview2 = InputDataIdentifier("_id_no_preview2") +private let _id_uploadFile = InputDataIdentifier("_id_uploadFile") +private let _id_input_title = InputDataIdentifier("_id_input_title") +private let _id_input_slug = InputDataIdentifier("_id_input_slug") + +private struct EditThemeState : Equatable { + let current: TelegramTheme + let presentation: TelegramPresentationTheme + let name: String + let slug: String? + let path: String? + let errors:[InputDataIdentifier : InputDataValueError] + init(current:TelegramTheme, presentation: TelegramPresentationTheme, name: String, slug: String?, path: String?, errors:[InputDataIdentifier : InputDataValueError]) { + self.current = current + self.presentation = presentation + self.name = name + self.slug = slug + self.path = path + self.errors = errors + } + func withUpdatedError(_ error: InputDataValueError?, for key: InputDataIdentifier) -> EditThemeState { + var errors = self.errors + if let error = error { + errors[key] = error + } else { + errors.removeValue(forKey: key) + } + return EditThemeState(current: self.current, presentation: self.presentation, name: self.name, slug: self.slug, path: self.path, errors: errors) + } + func withUpdatedName(_ name: String) -> EditThemeState { + return EditThemeState(current: self.current, presentation: self.presentation, name: name, slug: self.slug, path: self.path, errors: self.errors) + } + func withUpdatedSlug(_ slug: String?) -> EditThemeState { + return EditThemeState(current: self.current, presentation: self.presentation, name: self.name, slug: slug, path: self.path, errors: self.errors) + } + func withUpdatedPath(_ path: String?) -> EditThemeState { + return EditThemeState(current: self.current, presentation: self.presentation, name: self.name, slug: self.slug, path: path, errors: self.errors) + } + func withUpdatedPresentation(_ presentation: TelegramPresentationTheme) -> EditThemeState { + return EditThemeState(current: self.current, presentation: presentation, name: self.name, slug: self.slug, path: self.path, errors: self.errors) + } +} + +private final class EditThemeArguments { + let context: AccountContext + let updateFile:(String)->Void + let updateSlug:(String)->Void + init(context: AccountContext, updateFile:@escaping(String)->Void, updateSlug:@escaping(String)->Void) { + self.context = context + self.updateFile = updateFile + self.updateSlug = updateSlug + } +} + +private func editThemeEntries(state: EditThemeState, chatInteraction: ChatInteraction, arguments: EditThemeArguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId: Int32 = 0 + var index:Int32 = 0 + + + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: .string(state.name), error: state.errors[_id_input_title], identifier: _id_input_title, mode: .plain, data: InputDataRowData(), placeholder: nil, inputPlaceholder: L10n.editThemeNamePlaceholder, filter: { $0 }, limit: 128)) + index += 1 + + + entries.append(.input(sectionId: sectionId, index: index, value: .string(state.slug), error: state.errors[_id_input_slug], identifier: _id_input_slug, mode: .plain, data: InputDataRowData(viewType: .legacy, defaultText: "https://t.me/addtheme/"), placeholder: nil, inputPlaceholder: "", filter: { $0 }, limit: 64)) + + + let slugDesc = L10n.editThemeSlugDesc + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(slugDesc), data: InputDataGeneralTextData())) + index += 1 + + let previewTheme = state.presentation + + + let timestamp1:Int32 = 60 * 22 + 60 * 60 * 18 + let timestamp2:Int32 = 60 * 20 + 60 * 60 * 18 + let timestamp3:Int32 = 60 * 22 + 60 * 60 * 18 + + + let fromUser1 = TelegramUser(id: PeerId(1), accessHash: nil, firstName: L10n.appearanceSettingsChatPreviewUserName1, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + let fromUser2 = TelegramUser(id: PeerId(2), accessHash: nil, firstName: L10n.appearanceSettingsChatPreviewUserName2, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + let replyMessage = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: fromUser1.id, namespace: 0, id: 1), globallyUniqueId: 0, groupingKey: 0, groupInfo: nil, threadId: nil, timestamp: timestamp1, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: fromUser1, text: L10n.appearanceSettingsChatPreviewZeroText, attributes: [], media: [], peers:SimpleDictionary([fromUser2.id : fromUser2, fromUser1.id : fromUser1]) , associatedMessages: SimpleDictionary(), associatedMessageIds: []) + let firstMessage = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: fromUser1.id, namespace: 0, id: 0), globallyUniqueId: 0, groupingKey: 0, groupInfo: nil, threadId: nil, timestamp: timestamp2, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: fromUser2, text: tr(L10n.appearanceSettingsChatPreviewFirstText), attributes: [ReplyMessageAttribute(messageId: replyMessage.id, threadMessageId: nil)], media: [], peers:SimpleDictionary([fromUser2.id : fromUser2, fromUser1.id : fromUser1]) , associatedMessages: SimpleDictionary([replyMessage.id : replyMessage]), associatedMessageIds: []) + let firstEntry: ChatHistoryEntry = .MessageEntry(firstMessage, MessageIndex(firstMessage), true, previewTheme.bubbled ? .bubble : .list, .Full(rank: nil, header: .normal), nil, ChatHistoryEntryData(nil, MessageEntryAdditionalData(), AutoplayMediaPreferences.defaultSettings)) + let secondMessage = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: fromUser1.id, namespace: 0, id: 1), globallyUniqueId: 0, groupingKey: 0, groupInfo: nil, threadId: nil, timestamp: timestamp3, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: fromUser1, text: L10n.appearanceSettingsChatPreviewSecondText, attributes: [], media: [], peers:SimpleDictionary([fromUser2.id : fromUser2, fromUser1.id : fromUser1]) , associatedMessages: SimpleDictionary(), associatedMessageIds: []) + let secondEntry: ChatHistoryEntry = .MessageEntry(secondMessage, MessageIndex(secondMessage), true, previewTheme.bubbled ? .bubble : .list, .Full(rank: nil, header: .normal), nil, ChatHistoryEntryData(nil, MessageEntryAdditionalData(), AutoplayMediaPreferences.defaultSettings)) + + entries.append(.sectionId(sectionId, type: .custom(10))) + sectionId += 1 + + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_no_preview1, equatable: InputDataEquatable(state.presentation), comparable: nil, item: { size, stableId in + let item = ChatRowItem.item(size, from: firstEntry, interaction: chatInteraction, theme: previewTheme) + _ = item.makeSize(size.width, oldWidth: 0) + return item + })) + index += 1 + + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_no_preview2, equatable: InputDataEquatable(state.presentation), comparable: nil, item: { size, stableId in + let item = ChatRowItem.item(size, from: secondEntry, interaction: chatInteraction, theme: previewTheme) + _ = item.makeSize(size.width, oldWidth: 0) + return item + })) + index += 1 + + entries.append(.sectionId(sectionId, type: .custom(10))) + sectionId += 1 + + let selectFileText: String + let selectFileDesc: String + + if state.current.file == nil { + selectFileText = L10n.editThemeSelectFile + selectFileDesc = L10n.editThemeSelectFileDesc + } else { + selectFileText = L10n.editThemeSelectUpdatedFile + selectFileDesc = L10n.editThemeSelectUpdatedFileDesc + } + + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_uploadFile, data: .init(name: selectFileText, color: theme.colors.accent, type: .context(state.path ?? ""), action: { + filePanel(with: ["palette"], allowMultiple: false, for: arguments.context.window, completion: { paths in + if let first = paths?.first { + arguments.updateFile(first) + } + }) + }))) + index += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(selectFileDesc), data: InputDataGeneralTextData())) + index += 1 + + entries.append(.sectionId(sectionId, type: .legacy)) + sectionId += 1 + + return entries +} + +func EditThemeController(context: AccountContext, telegramTheme: TelegramTheme, presentation: TelegramPresentationTheme) -> InputDataModalController { + let initialState = EditThemeState(current: telegramTheme, presentation: presentation, name: telegramTheme.title, slug: telegramTheme.slug, path: nil, errors: [:]) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((EditThemeState) -> EditThemeState) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let chatInteraction = ChatInteraction(chatLocation: .peer(PeerId(0)), context: context, disableSelectAbility: true) + + + + let slugDisposable = MetaDisposable() + let disposable = MetaDisposable() + let updateWallpaper = MetaDisposable() + + + func checkSlug(_ slug: String)->Void { + if slug.length >= 5 && slug != telegramTheme.slug { + let signal = getTheme(account: context.account, slug: slug) |> deliverOnMainQueue |> delay(0.2, queue: .mainQueue()) + slugDisposable.set(signal.start(next: { value in + updateState { + $0.withUpdatedError(InputDataValueError(description: L10n.editThemeSlugErrorAlreadyExists, target: .data), for: _id_input_slug) + } + }, error: { error in + switch error { + case .slugInvalid: + updateState { + $0.withUpdatedError(InputDataValueError(description: L10n.editThemeSlugErrorFormat, target: .data), for: _id_input_slug) + } + default: + updateState { + $0.withUpdatedError(nil, for: _id_input_slug) + } + } + + })) + } else { + slugDisposable.set(nil) + updateState { + $0.withUpdatedError(nil, for: _id_input_slug) + } + } + } + + let arguments = EditThemeArguments(context: context, updateFile: { path in + if let palette = importPalette(path) { + let presentation = stateValue.with { $0.presentation } + if palette.wallpaper != presentation.colors.wallpaper { + switch palette.wallpaper { + case let .url(string): + let link = inApp(for: string as NSString, context: context) + switch link { + case let .wallpaper(values): + switch values.preview { + case let .slug(slug, settings): + let signal: Signal<(Wallpaper, TelegramWallpaper?), NoError> = getWallpaper(network: context.account.network, slug: slug) + |> mapToSignal { cloud in + return moveWallpaperToCache(postbox: context.account.postbox, wallpaper: Wallpaper(cloud).withUpdatedSettings(settings)) |> map { wallpaper in + return (wallpaper, cloud) + } |> castError(GetWallpaperError.self) + } + |> `catch` { _ in + return .single((.none, nil)) + } + + updateWallpaper.set(showModalProgress(signal: signal |> deliverOnMainQueue, for: context.window).start(next: { wallpaper, cloud in + updateState { + $0.withUpdatedPresentation(presentation.withUpdatedColors(palette) + .withUpdatedWallpaper(ThemeWallpaper(wallpaper: wallpaper, associated: AssociatedWallpaper(cloud: cloud, wallpaper: wallpaper)))) + } + })) + default: + break + } + default: + break + } + default: + updateState { + $0.withUpdatedPresentation(presentation.withUpdatedColors(palette) + .withUpdatedWallpaper(ThemeWallpaper(wallpaper: palette.wallpaper.wallpaper, associated: AssociatedWallpaper(cloud: nil, wallpaper: palette.wallpaper.wallpaper)))) + } + } + } else { + updateState { + $0.withUpdatedPresentation(presentation.withUpdatedColors(palette)) + } + } + + + } else { + alert(for: context.window, info: L10n.unknownError) + } + + }, updateSlug: { slug in + let oldSlug = stateValue.with { $0.slug } + updateState { value in + var value = value.withUpdatedSlug(slug) + if oldSlug != slug { + value = value.withUpdatedError(nil, for: _id_input_slug) + } + return value + } + if oldSlug != slug { + checkSlug(slug) + } + }) + + + var close: (() -> Void)? = nil + + let signal = statePromise.get() |> map { state in + return InputDataSignalValue(entries: editThemeEntries(state: state, chatInteraction: chatInteraction, arguments: arguments)) + } + + + let save:()->InputDataValidation = { + return .fail(.doSomething(next: { f in + let state = stateValue.with { $0 } + slugDisposable.set(nil) + + let slug = state.slug ?? "" + + var failed:[InputDataIdentifier : InputDataValidationFailAction] = [:] + if !slug.isEmpty, slug.length < 5 { + failed[_id_input_slug] = .shake + } + if state.name.isEmpty { + failed[_id_input_title] = .shake + } + if !failed.isEmpty { + f(.fail(.fields(failed))) + return + } + + var mediaResource: MediaResource? = nil + var thumbnailData: Data? = nil + let newTheme: TelegramPresentationTheme = state.presentation + + if newTheme.colors != presentation.colors || state.current.file == nil { + let temp = NSTemporaryDirectory() + "\(arc4random()).palette" + try? newTheme.colors.withUpdatedName(state.name).toString.write(to: URL(fileURLWithPath: temp), atomically: true, encoding: .utf8) + mediaResource = LocalFileReferenceMediaResource(localFilePath: temp, randomId: arc4random64(), isUniquelyReferencedTemporaryFile: true, size: fs(temp)) + } + + if let _ = mediaResource { + let preview = generateThemePreview(for: newTheme.colors, wallpaper: newTheme.wallpaper.wallpaper, backgroundMode: newTheme.backgroundMode) + if let mutableData = CFDataCreateMutable(nil, 0), let destination = CGImageDestinationCreateWithData(mutableData, "public.png" as CFString, 1, nil) { + CGImageDestinationAddImage(destination, preview, nil) + if CGImageDestinationFinalize(destination) { + let data = mutableData as Data + thumbnailData = data + } + } + } + + let updateSignal = updateThemeInteractivetly(accountManager: context.sharedContext.accountManager, f: { settings in + if settings.cloudTheme?.id == telegramTheme.id { + let defaultCloud = DefaultCloudTheme(cloud: telegramTheme, palette: newTheme.colors, wallpaper: AssociatedWallpaper(cloud: newTheme.wallpaper.associated?.cloud, wallpaper: newTheme.wallpaper.wallpaper)) + + let defaultTheme = DefaultTheme(local: newTheme.colors.parent, cloud: defaultCloud) + var settings = settings.withUpdatedCloudTheme(telegramTheme).withUpdatedPalette(newTheme.colors) + if presentation.colors.isDark { + settings = settings.withUpdatedDefaultDark(defaultTheme) + } else { + settings = settings.withUpdatedDefaultDay(defaultTheme) + } + return settings.withUpdatedDefaultIsDark(presentation.colors.isDark) + } else { + return settings + } + + }) |> mapError { _ in CreateThemeError.generic } + |> mapToSignal { + updateTheme(account: context.account, accountManager: context.sharedContext.accountManager, theme: telegramTheme, title: state.name, slug: state.slug, resource: mediaResource, thumbnailData: thumbnailData, settings: nil) + |> filter { + switch $0 { + case .progress: + return false + case .result: + return true + } + } + |> take(1) + } + + disposable.set(showModalProgress(signal: updateSignal, for: context.window).start(next: { _ in + delay(0.2, closure: { + close?() + }) + }, error: { error in + switch error { + case .generic: + alert(for: context.window, info: L10n.unknownError) + case .slugOccupied: + updateState { + $0.withUpdatedError(InputDataValueError(description: L10n.editThameNameAlreadyTaken, target: .data), for: _id_input_slug) + } + f(.fail(.fields([_id_input_slug : .shake]))) + case .slugInvalid: + updateState { + $0.withUpdatedError(InputDataValueError(description: L10n.editThemeSlugErrorFormat, target: .data), for: _id_input_slug) + } + f(.fail(.fields([_id_input_slug : .shake]))) + } + })) + })) + } + + let controller = InputDataController(dataSignal: signal, title: L10n.editThemeTitle, validateData: { data in + + return save() + + }, updateDatas: { data in + var checkNext: Bool = false + updateState { value in + let oldSlug = value.slug + var value = value + value = value.withUpdatedName(data[_id_input_title]?.stringValue ?? value.name) + .withUpdatedSlug(data[_id_input_slug]?.stringValue ?? oldSlug) + .withUpdatedError(nil, for: _id_input_title) + if oldSlug != value.slug { + value = value.withUpdatedError(nil, for: _id_input_slug) + checkNext = true + } + return value + } + if checkNext { + checkSlug(stateValue.with { $0.slug } ?? "") + } + return .none + }, afterDisappear: { + disposable.dispose() + slugDisposable.dispose() + updateWallpaper.dispose() + }, afterTransaction: { controller in + let theme = stateValue.with { $0.presentation } + controller.genericView.tableView.getBackgroundColor = { + if !theme.bubbled { + return theme.colors.chatBackground + } else { + return .clear + } + } + controller.genericView.tableView.updateLocalizationAndTheme(theme: theme) + controller.genericView.backgroundMode = theme.controllerBackgroundMode + }, getBackgroundColor: { + theme.colors.background + }) + + + chatInteraction.getGradientOffsetRect = { [weak controller] in + guard let controller = controller else { + return .zero + } + let offset = controller.tableView.scrollPosition().current.rect.origin + return CGRect(origin: offset, size: controller.tableView.frame.size) + } + + let modalInteractions = ModalInteractions(acceptTitle: L10n.editThemeEdit, accept: { [weak controller] in + _ = controller?.returnKeyAction() + }, drawBorder: true, height: 50, singleButton: true) + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) + + controller.didLoaded = { controller, _ in + controller.tableView.addScroll(listener: TableScrollListener(dispatchWhenVisibleRangeUpdated: false, { [weak controller] position in + guard let controller = controller else { + return + } + controller.tableView.enumerateVisibleViews(with: { view in + if let view = view as? ChatRowView { + view.updateBackground(animated: false, item: view.item) + } + }) + })) + + controller.tableView.afterSetupItem = { [weak controller] view, item in + guard let controller = controller else { + return + } + if let view = view as? ChatRowView { + let offset = controller.tableView.scrollPosition().current.rect.origin + view.updateBackground(animated: false, item: view.item) + } + } + + } + + close = { [weak modalController] in + modalController?.modal?.close() + } + + return modalController +} + + +func showEditThemeModalController(context: AccountContext, theme telegramTheme: TelegramTheme) { + if let file = telegramTheme.file, telegramTheme != theme.cloudTheme { + let fetchDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: MediaResourceReference.standalone(resource: file.resource)).start() + + + let signal = loadCloudPaletteAndWallpaper(context: context, file: file) |> afterDisposed { + fetchDisposable.dispose() + } + _ = showModalProgress(signal: signal |> deliverOnMainQueue, for: context.window).start(next: { data in + if let (palette, wallpaper, cloudWallpaper) = data { + let newTheme = theme.withUpdatedColors(palette).withUpdatedWallpaper(ThemeWallpaper(wallpaper: wallpaper, associated: AssociatedWallpaper(cloud: cloudWallpaper, wallpaper: wallpaper))) + showModal(with: EditThemeController(context: context, telegramTheme: telegramTheme, presentation: newTheme), for: context.window) + } else { + alert(for: context.window, info: L10n.unknownError) + } + }) + } else { + showModal(with: EditThemeController(context: context, telegramTheme: telegramTheme, presentation: theme), for: context.window) + } +} + + diff --git a/Telegram-Mac/EmojiAnimationEffectView.swift b/Telegram-Mac/EmojiAnimationEffectView.swift new file mode 100644 index 0000000000..7f55efc35e --- /dev/null +++ b/Telegram-Mac/EmojiAnimationEffectView.swift @@ -0,0 +1,53 @@ +// +// EmojiAnimationEffect.swift +// Telegram +// +// Created by Mikhail Filimonov on 14.09.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit + +final class EmojiAnimationEffectView : View { + + private let player: LottiePlayerView + private let animation: LottieAnimation + let animationSize: NSSize + private var animationPoint: CGPoint + init(animation: LottieAnimation, animationSize: NSSize, animationPoint: CGPoint, frameRect: NSRect) { + self.animation = animation + self.player = LottiePlayerView(frame: .init(origin: animationPoint, size: animationSize)) + self.animationSize = animationSize + self.animationPoint = animationPoint + super.init(frame: frameRect) + addSubview(player) + player.set(animation) + isEventLess = true + + updateLayout(size: frameRect.size, transition: .immediate) + } + + override func layout() { + super.layout() + self.updateLayout(size: frame.size, transition: .immediate) + } + + func updateLayout(size: NSSize, transition: ContainedViewLayoutTransition) { + transition.updateFrame(view: self.player, frame: CGRect(origin: animationPoint, size: animationSize)) + self.player.update(size: animationSize, transition: transition) + } + + func updatePoint(_ point: NSPoint, transition: ContainedViewLayoutTransition) { + self.animationPoint = point + self.updateLayout(size: frame.size, transition: transition) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } +} diff --git a/Telegram-Mac/EmojiScreenEffect.swift b/Telegram-Mac/EmojiScreenEffect.swift new file mode 100644 index 0000000000..11d54d2dc3 --- /dev/null +++ b/Telegram-Mac/EmojiScreenEffect.swift @@ -0,0 +1,268 @@ +// +// EmojiScreenEffect.swift +// Telegram +// +// Created by Mikhail Filimonov on 14.09.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit +import TelegramCore +import Postbox + +final class EmojiScreenEffect { + fileprivate let context: AccountContext + fileprivate let takeTableItem:(MessageId)->TableRowItem? + fileprivate(set) var scrollUpdater: TableScrollListener! + private let dataDisposable: DisposableDict = DisposableDict() + + + private let limit: Int = 5 + + struct Key : Hashable { + let animationKey: LottieAnimationKey + let messageId: MessageId + let timestamp: TimeInterval + let isIncoming: Bool + } + + struct Value { + let view: WeakReference + let index: Int + let emoji: String + let mirror: Bool + let key: Key + } + + private var animations:[Key: Value] = [:] + + private var enqueuedToServer:[Value] = [] + private var enqueuedToEnjoy:[Value] = [] + + private var enjoyTimer: SwiftSignalKit.Timer? + + private var timers:[MessageId : SwiftSignalKit.Timer] = [:] + + + + init(context: AccountContext, takeTableItem:@escaping(MessageId)->TableRowItem?) { + self.context = context + self.takeTableItem = takeTableItem + + self.scrollUpdater = .init(dispatchWhenVisibleRangeUpdated: false, { [weak self] position in + self?.updateScroll(transition: .immediate) + }) + } + + private func checkItem(_ item: TableRowItem, _ messageId: MessageId, with emoji: String) -> Bool { + if let item = item as? ChatRowItem, item.message?.text == emoji { + if messageId.peerId.namespace == Namespaces.Peer.CloudUser { + return context.sharedContext.baseSettings.bigEmoji + } + } + return false + } + + private func updateScroll(transition: ContainedViewLayoutTransition) { + var outOfBounds: Set = Set() + for (key, animation) in animations { + var success: Bool = false + if let animationView = animation.view.value { + if let item = takeTableItem(key.messageId) { + if let view = item.view as? ChatMediaView { + if let contentView = view.contentNode { + + var point = contentView.convert(CGPoint.zero, to: animationView) + let subSize = animationView.animationSize - contentView.frame.size + + point.x-=(subSize.width - 20) + point.y-=subSize.height/2 + + + animationView.updatePoint(point, transition: .immediate) + + if contentView.visibleRect != .zero { + success = true + } + } + } + } + } + if !success { + outOfBounds.insert(key) + } + } + for key in outOfBounds { + self.deinitAnimation(key: key, animated: true) + } + } + + deinit { + dataDisposable.dispose() + let animations = self.animations + for animation in animations { + deinitAnimation(key: animation.key, animated: false) + } + } + private func isLimitExceed(_ messageId: MessageId) -> Bool { + let onair = animations.filter { $0.key.messageId == messageId } + let last = onair.max(by: { $0.key.timestamp < $1.key.timestamp }) + if let last = last { + if Date().timeIntervalSince1970 - last.key.timestamp < 0.2 { + return true + } + } + return onair.count >= limit + } + + + func addAnimation(_ emoji: String, index: Int?, mirror: Bool, isIncoming: Bool, messageId: MessageId, animationSize: NSSize, viewFrame: NSRect, for parentView: NSView) { + + if !isLimitExceed(messageId), let item = takeTableItem(messageId), checkItem(item, messageId, with: emoji) { + let signal: Signal = context.diceCache.animationEffect(for: emoji.emojiUnmodified) + |> map { value -> LottieAnimation? in + if let random = value.randomElement(), let data = random.1 { + return LottieAnimation(compressed: data, key: .init(key: .bundle("_effect_\(emoji)"), size: animationSize, backingScale: Int(System.backingScale)), cachePurpose: .temporaryLZ4(.effect), playPolicy: .onceEnd) + } else { + return nil + } + } + |> deliverOnMainQueue + + dataDisposable.set(signal.start(next: { [weak self, weak parentView] animation in + if let animation = animation, let parentView = parentView { + self?.initAnimation(animation, emoji: emoji, mirror: mirror, isIncoming: isIncoming, messageId: messageId, animationSize: animationSize, viewFrame: viewFrame, parentView: parentView) + } + }), forKey: emoji) + } else { + dataDisposable.set(nil, forKey: emoji) + } + } + + + private func deinitAnimation(key: Key, animated: Bool) { + let view = animations.removeValue(forKey: key)?.view.value + if let view = view { + performSubviewRemoval(view, animated: animated) + } + enqueuedToServer.removeAll(where: { $0.key == key }) + enqueuedToEnjoy.removeAll(where: { $0.key == key }) + } + + private func initAnimation(_ animation: LottieAnimation, emoji: String, mirror: Bool, isIncoming: Bool, messageId: MessageId, animationSize: NSSize, viewFrame: NSRect, parentView: NSView) { + + + let mediaView = (takeTableItem(messageId)?.view as? ChatMediaView)?.contentNode as? MediaAnimatedStickerView + mediaView?.playAgain() + + let key: Key = .init(animationKey: animation.key.key, messageId: messageId, timestamp: Date().timeIntervalSince1970, isIncoming: isIncoming) + + animation.triggerOn = (LottiePlayerTriggerFrame.last, { [weak self] in + self?.deinitAnimation(key: key, animated: true) + }, {}) + + let view = EmojiAnimationEffectView(animation: animation, animationSize: animationSize, animationPoint: .zero, frameRect: viewFrame) + + + CATransaction.begin() + if mirror { + let size = animationSize + var fr = CATransform3DIdentity + fr = CATransform3DTranslate(fr, viewFrame.width / 2, 0, 0) + fr = CATransform3DScale(fr, -1, 1, 1) + fr = CATransform3DTranslate(fr, -size.width, 0, 0) + view.layer?.sublayerTransform = fr + } else { + view.layer?.sublayerTransform = CATransform3DIdentity + } + CATransaction.commit() + + parentView.addSubview(view) + + let value: Value = .init(view: .init(value: view), index: 1, emoji: emoji, mirror: mirror, key: key) + animations[key] = value + + updateScroll(transition: .immediate) + if !isIncoming { + self.enqueuedToServer.append(value) + } else { + self.enqueuedToEnjoy.append(value) + } + self.enqueueToServer() + self.enqueueToEnjoy() + } + + private func enqueueToEnjoy() { + if enjoyTimer == nil, !enqueuedToEnjoy.isEmpty { + enjoyTimer = .init(timeout: 1.0, repeat: false, completion: { [weak self] in + self?.performEnjoyAction() + }, queue: .mainQueue()) + enjoyTimer?.start() + } + } + + private func performEnjoyAction() { + self.enjoyTimer = nil + + var exists:Set = Set() + for value in enqueuedToEnjoy { + if !exists.contains(value.key.messageId) { + context.account.updateLocalInputActivity(peerId: PeerActivitySpace(peerId: value.key.messageId.peerId, category: .global), activity: .seeingEmojiInteraction(emoticon: value.emoji), isPresent: true) + exists.insert(value.key.messageId) + } + } + self.enqueuedToEnjoy.removeAll() + } + + private func enqueueToServer() { + let outgoing = self.enqueuedToServer + let msgIds:[MessageId] = outgoing.map { $0.key.messageId }.uniqueElements + + for msgId in msgIds { + if self.timers[msgId] == nil { + self.timers[msgId] = .init(timeout: 1, repeat: false, completion: { [weak self] in + self?.performServerActions(for: msgId) + }, queue: .mainQueue()) + self.timers[msgId]?.start() + } + } + } + + private func performServerActions(for msgId: MessageId) { + let values = self.enqueuedToServer.filter { $0.key.messageId == msgId } + self.enqueuedToServer.removeAll(where: { $0.key.messageId == msgId }) + self.timers.removeValue(forKey: msgId) + if !values.isEmpty { + let value = values.min(by: { $0.key.timestamp < $1.key.timestamp })! + let animations:[EmojiInteraction.Animation] = values.map { current -> EmojiInteraction.Animation in + .init(index: current.index, timeOffset: Float((current.key.timestamp - value.key.timestamp))) + }.sorted(by: { $0.timeOffset < $1.timeOffset }) + + context.account.updateLocalInputActivity(peerId: PeerActivitySpace(peerId: msgId.peerId, category: .global), activity: .interactingWithEmoji(emoticon: value.emoji, messageId: msgId, interaction: EmojiInteraction(animations: animations)), isPresent: true) + } + } + + + func updateLayout(rect: CGRect, transition: ContainedViewLayoutTransition) { + for (_ , animation) in animations { + if let value = animation.view.value { + transition.updateFrame(view: value, frame: rect) + value.updateLayout(size: rect.size, transition: transition) + + if animation.mirror { + let size = value.animationSize + var fr = CATransform3DIdentity + fr = CATransform3DTranslate(fr, rect.width / 2, 0, 0) + fr = CATransform3DScale(fr, -1, 1, 1) + fr = CATransform3DTranslate(fr, -size.width, 0, 0) + value.layer?.sublayerTransform = fr + } else { + value.layer?.sublayerTransform = CATransform3DIdentity + } + + } + } + } +} diff --git a/Telegram-Mac/EmojiToleranceController.swift b/Telegram-Mac/EmojiToleranceController.swift index d22609a118..13df957788 100644 --- a/Telegram-Mac/EmojiToleranceController.swift +++ b/Telegram-Mac/EmojiToleranceController.swift @@ -8,36 +8,38 @@ import Cocoa import TGUIKit -import PostboxMac +import Postbox private class EmojiTolerance : View { - init(frame frameRect: NSRect, emoji:String, handle:@escaping(String)->Void) { + init(frame frameRect: NSRect, emoji:String, handle:@escaping(String, String?)->Void) { super.init(frame: frameRect) + + let modifiers = emoji.emojiSkinToneModifiers var x:CGFloat = 2 - let add:(String)->Void = { [weak self] emoji in + let add:(String, String, String?)->Void = { [weak self] emoji, notModified, modifier in let button: TitleButton = TitleButton() button.set(font: .normal(.header), for: .Normal) button.set(text: emoji, for: .Normal) button.setFrameSize(NSMakeSize(30, 30)) - button.centerY(x: x) - button.set(background: theme.colors.background, for: .Normal) + button.centerY(x: x, addition: 4) + button.set(background: .clear, for: .Normal) button.set(background: theme.colors.grayForeground, for: .Highlight) button.layer?.cornerRadius = .cornerRadius self?.addSubview(button) x += button.frame.width button.set(handler: { _ in - handle(emoji) + handle(notModified, modifier) }, for: .Click) } - add(emoji) + add(emoji, emoji, nil) for modifier in modifiers { - add("\(emoji)\(modifier)") + add(emoji.emojiWithSkinModifier(modifier), emoji, modifier) } } @@ -62,12 +64,12 @@ class EmojiToleranceController: NSViewController { private let emoji:String - init(_ emoji:String, postbox: Postbox, handle:@escaping(String)->Void) { + init(_ emoji:String, postbox: Postbox, handle:@escaping(String, String?)->Void) { self.emoji = emoji super.init(nibName: nil, bundle: nil) - self.view = EmojiTolerance(frame: NSMakeRect(0, 0, 30 * 6 + 4, 34), emoji: emoji, handle: handle) + self.view = EmojiTolerance(frame: NSMakeRect(0, 4, 30 * 6 + 4, 34), emoji: emoji, handle: handle) } required init?(coder: NSCoder) { diff --git a/Telegram-Mac/EmojiViewController.swift b/Telegram-Mac/EmojiViewController.swift index ae4aad8449..6542b6927b 100644 --- a/Telegram-Mac/EmojiViewController.swift +++ b/Telegram-Mac/EmojiViewController.swift @@ -8,24 +8,11 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac -import TelegramCoreMac +import SwiftSignalKit +import TelegramCore -var segmentNames:(Int)->String = { value in - var list:[String] = [] - list.append(tr(.emojiRecent)) - list.append(tr(.emojiSmilesAndPeople)) - list.append(tr(.emojiAnimalsAndNature)) - list.append(tr(.emojiFoodAndDrink)) - list.append(tr(.emojiActivityAndSport)) - list.append(tr(.emojiTravelAndPlaces)) - list.append(tr(.emojiObjects)) - list.append(tr(.emojiSymbols)) - list.append(tr(.emojiFlags)) - return list[value] -} enum EmojiSegment : Int64, Comparable { case Recent = 0 @@ -38,6 +25,19 @@ enum EmojiSegment : Int64, Comparable { case Symbols = 7 case Flags = 8 + var localizedString: String { + switch self { + case .Recent: return L10n.emojiRecent + case .People: return L10n.emojiSmilesAndPeople + case .AnimalsAndNature: return L10n.emojiAnimalsAndNature + case .FoodAndDrink: return L10n.emojiFoodAndDrink + case .ActivityAndSport: return L10n.emojiActivityAndSport + case .TravelAndPlaces: return L10n.emojiTravelAndPlaces + case .Objects: return L10n.emojiObjects + case .Symbols: return L10n.emojiSymbols + case .Flags: return L10n.emojiFlags + } + } var hashValue:Int { return Int(self.rawValue) @@ -52,12 +52,16 @@ func <(lhs:EmojiSegment, rhs:EmojiSegment) -> Bool { return lhs.rawValue < rhs.rawValue } -private let emoji:[EmojiSegment:[String]] = { +let emojiesInstance:[EmojiSegment:[String]] = { assertNotOnMainThread() var local:[EmojiSegment:[String]] = [EmojiSegment:[String]]() let resource:URL? - if #available(OSX 10.12, *) { + if #available(OSX 11.1, *) { + resource = Bundle.main.url(forResource:"emoji1016", withExtension:"txt") + } else if #available(OSX 10.14.1, *) { + resource = Bundle.main.url(forResource:"emoji1014-1", withExtension:"txt") + } else if #available(OSX 10.12, *) { resource = Bundle.main.url(forResource:"emoji", withExtension:"txt") } else { resource = Bundle.main.url(forResource:"emoji11", withExtension:"txt") @@ -91,7 +95,7 @@ private let emoji:[EmojiSegment:[String]] = { }() -private func segments(_ emoji: [EmojiSegment : [String]], skinModifiers: [String]) -> [EmojiSegment:[[NSAttributedString]]] { +private func segments(_ emoji: [EmojiSegment : [String]], skinModifiers: [EmojiSkinModifier]) -> [EmojiSegment:[[NSAttributedString]]] { var segments:[EmojiSegment:[[NSAttributedString]]] = [:] for (key,list) in emoji { @@ -101,16 +105,26 @@ private func segments(_ emoji: [EmojiSegment : [String]], skinModifiers: [String for emoji in list { - var e:String = emoji + var e:String = emoji.emojiUnmodified for modifier in skinModifiers { - if emoji.emojiUnmodified == modifier.emojiUnmodified { - e = modifier + if e == modifier.emoji { + if e.length == 5 { + let mutable = NSMutableString() + mutable.insert(e, at: 0) + mutable.insert(modifier.modifier, at: 2) + e = mutable as String + } else { + e = e + modifier.modifier + } } + + } + if !line.contains(where: {$0.string == String(e.first!) }), let first = e.first { + line.append(.initialize(string: String(first), font: .normal(26.0))) + i += 1 } - line.append(.initialize(string: e, font: NSFont.normal(.custom(26)))) - i += 1 if i == 8 { lines.append(line) @@ -135,27 +149,78 @@ fileprivate var isReady:Bool = false class EmojiControllerView : View { fileprivate let tableView:TableView = TableView(frame:NSZeroRect) + private let tabsContainer = View() fileprivate let tabs:HorizontalTableView = HorizontalTableView(frame:NSZeroRect) private let borderView:View = View() + private let emptyResults: ImageView = ImageView() + let searchView = SearchView(frame: .zero) + private let searchContainer = View() required init(frame frameRect: NSRect) { super.init(frame: frameRect) addSubview(tableView) - addSubview(tabs) - addSubview(borderView) - + + searchContainer.addSubview(searchView) + addSubview(searchContainer) + + addSubview(emptyResults) + + tabsContainer.addSubview(tabs) + tabsContainer.addSubview(borderView) + addSubview(tabsContainer) + updateLocalizationAndTheme(theme: theme) } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) self.backgroundColor = theme.colors.background self.borderView.backgroundColor = theme.colors.border + emptyResults.image = theme.icons.stickersEmptySearch + emptyResults.sizeToFit() + searchView.updateLocalizationAndTheme(theme: theme) } + private var searchState: SearchState? = nil + + func updateSearchState(_ searchState: SearchState, animated: Bool) { + self.searchState = searchState + switch searchState.state { + case .Focus: + tabsContainer.change(pos: NSMakePoint(0, -tabsContainer.frame.height), animated: animated) + searchContainer.change(pos: NSMakePoint(0, tabsContainer.frame.maxY), animated: animated) + case .None: + tabsContainer.change(pos: NSMakePoint(0, 0), animated: animated) + searchContainer.change(pos: NSMakePoint(0, tabsContainer.frame.maxY), animated: animated) + } + tableView.change(size: NSMakeSize(frame.width, frame.height - searchContainer.frame.maxY), animated: animated) + tableView.change(pos: NSMakePoint(0, searchContainer.frame.maxY), animated: animated) + } + + + func updateVisibility(_ isEmpty: Bool, isSearch: Bool) { + emptyResults.isHidden = !isEmpty + tableView.isHidden = isEmpty + tabs.isHidden = isSearch + borderView.isHidden = isSearch + } + + override func layout() { super.layout() - tableView.frame = NSMakeRect(0, 3.0, bounds.width , frame.height - 3.0 - 50) - tabs.frame = NSMakeRect(0, tableView.frame.maxY + 1, frame.width,49) - borderView.frame = NSMakeRect(0, frame.height - 50, frame.width, .borderSize) + + let initial: CGFloat = searchState?.state == .Focus ? -50 : 0 + + tabsContainer.frame = NSMakeRect(0, initial, frame.width, 50) + tabs.setFrameSize(NSMakeSize(frame.width - 8, 40)) + tabs.center() + + searchContainer.frame = NSMakeRect(0, tabsContainer.frame.maxY, frame.width, 50) + searchView.setFrameSize(NSMakeSize(frame.width - 20, 30)) + searchView.center() + + borderView.frame = NSMakeRect(0, tabsContainer.frame.height - .borderSize, frame.width, .borderSize) + tableView.frame = NSMakeRect(0, searchContainer.frame.maxY, frame.width , frame.height - searchContainer.frame.maxY) + emptyResults.center() } required init?(coder: NSCoder) { @@ -164,21 +229,45 @@ class EmojiControllerView : View { } class EmojiViewController: TelegramGenericViewController, TableViewDelegate { - private var disposable:MetaDisposable = MetaDisposable() - + func findGroupStableId(for stableId: AnyHashable) -> AnyHashable? { + return nil + } + private let searchValue = ValuePromise(.init(state: .None, request: nil)) + private var searchState: SearchState = .init(state: .None, request: nil) { + didSet { + self.searchValue.set(searchState) + } + } + private let disposable:MetaDisposable = MetaDisposable() + private let searchStateDisposable = MetaDisposable() + private var interactions:EntertainmentInteractions? - - override init(_ account: Account) { - super.init(account) + var makeSearchCommand:((ESearchCommand)->Void)? + private func updateSearchState(_ state: SearchState) { + self.searchState = state + if !state.request.isEmpty { + self.makeSearchCommand?(.loading) + } + if self.isLoaded() == true { + self.genericView.updateSearchState(state, animated: true) + self.genericView.tableView.scroll(to: .up(true)) + } + } + + override init(_ context: AccountContext) { + super.init(context) + + _frameRect = NSMakeRect(0, 0, 350, 300) self.bar = .init(height: 0) } + override func loadView() { super.loadView() genericView.tabs.delegate = self - updateLocalizationAndTheme() + updateLocalizationAndTheme(theme: theme) } @@ -186,7 +275,7 @@ class EmojiViewController: TelegramGenericViewController, T return true } - func selectionWillChange(row: Int, item: TableRowItem) -> Bool { + func selectionWillChange(row: Int, item: TableRowItem, byClick: Bool) -> Bool { return true } @@ -194,9 +283,9 @@ class EmojiViewController: TelegramGenericViewController, T } - func loadResource() -> Signal { + func loadResource() -> Signal { return Signal { (subscriber) -> Disposable in - _ = emoji + _ = emojiesInstance subscriber.putNext(Void()) subscriber.putCompletion() return ActionDisposable(action: { @@ -207,10 +296,12 @@ class EmojiViewController: TelegramGenericViewController, T deinit { disposable.dispose() + searchStateDisposable.dispose() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + genericView.needsLayout = true } override func viewDidAppear(_ animated: Bool) { @@ -218,80 +309,52 @@ class EmojiViewController: TelegramGenericViewController, T self.genericView.tableView.performScrollEvent() } + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + } + override func viewDidLoad() { super.viewDidLoad() + + let searchInteractions = SearchInteractions({ [weak self] state, _ in + self?.updateSearchState(state) + }, { [weak self] state in + self?.updateSearchState(state) + }) + + genericView.searchView.searchInteractions = searchInteractions + + // DO NOT WRITE CODE OUTSIZE READY BLOCK - let ready:(RecentUsedEmoji)->Void = { [weak self] recent in + let ready:(RecentUsedEmoji, [String]?)->Void = { [weak self] recent, search in if let strongSelf = self { - strongSelf.readyForDisplay(recent) + strongSelf.makeSearchCommand?(.normal) + strongSelf.readyForDisplay(recent, search) strongSelf.readyOnce() - } - } - let s:Signal = combineLatest(loadResource(), recentUsedEmoji(postbox: account.postbox), appearanceSignal) |> deliverOnMainQueue + let context = self.context - disposable.set(s.start(next: { (_, recent, _) in + let s:Signal = combineLatest(queue: resourcesQueue, loadResource(), recentUsedEmoji(postbox: context.account.postbox), appearanceSignal, self.searchValue.get() |> distinctUntilChanged(isEqual: { prev, new in + return prev.request == new.request + }) |> mapToSignal { state -> Signal<[String]?, NoError> in + if state.request.isEmpty { + return .single(nil) + } else { + return context.sharedContext.inputSource.searchEmoji(postbox: context.account.postbox, engine: context.engine, sharedContext: context.sharedContext, query: state.request, completeMatch: false, checkPrediction: false) |> map(Optional.init) |> delay(0.2, queue: .concurrentDefaultQueue()) + } + }) |> deliverOnMainQueue + + disposable.set(s.start(next: { (_, recent, _, search) in isReady = true - ready(recent) + + ready(recent, search) })) - } - - func readyForDisplay(_ recent: RecentUsedEmoji) -> Void { - - - genericView.tableView.removeAll() - genericView.tabs.removeAll() - var e = emoji - e[EmojiSegment.Recent] = recent.emojies - let seg = segments(e, skinModifiers: recent.skinModifiers) - let seglist = seg.map { (key,_) -> EmojiSegment in - return key - }.sorted(by: <) - - let w = floorToScreenPixels(frame.width / CGFloat(seg.count)) - genericView.tabs.setFrameSize(NSMakeSize(w * CGFloat(seg.count), genericView.tabs.frame.height)) - genericView.tabs.centerX() - let initialSize = atomicSize - var tabIcons:[CGImage] = [] - tabIcons.append(theme.icons.emojiRecentTab) - tabIcons.append(theme.icons.emojiSmileTab) - tabIcons.append(theme.icons.emojiNatureTab) - tabIcons.append(theme.icons.emojiFoodTab) - tabIcons.append(theme.icons.emojiSportTab) - tabIcons.append(theme.icons.emojiCarTab) - tabIcons.append(theme.icons.emojiObjectsTab) - tabIcons.append(theme.icons.emojiSymbolsTab) - tabIcons.append(theme.icons.emojiFlagsTab) - var tabIconsSelected:[CGImage] = [] - tabIconsSelected.append(theme.icons.emojiRecentTabActive) - tabIconsSelected.append(theme.icons.emojiSmileTabActive) - tabIconsSelected.append(theme.icons.emojiNatureTabActive) - tabIconsSelected.append(theme.icons.emojiFoodTabActive) - tabIconsSelected.append(theme.icons.emojiSportTabActive) - tabIconsSelected.append(theme.icons.emojiCarTabActive) - tabIconsSelected.append(theme.icons.emojiObjectsTabActive) - tabIconsSelected.append(theme.icons.emojiSymbolsTabActive) - tabIconsSelected.append(theme.icons.emojiFlagsTabActive) - for key in seglist { - if key != .Recent { - let _ = genericView.tableView.addItem(item: EStickItem(initialSize.modify({$0}), segment:key, segmentName:segmentNames(key.hashValue))) - } - let _ = genericView.tableView.addItem(item: EBlockItem(initialSize.modify({$0}), attrLines: seg[key]!, segment: key, account: account, selectHandler: { [weak self] emoji in - if let interactions = self?.interactions { - interactions.sendEmoji(emoji) - } - } )) - let _ = genericView.tabs.addItem(item: ETabRowItem(initialSize.modify({$0}), icon: tabIcons[key.hashValue], iconSelected:tabIconsSelected[key.hashValue], stableId:key.rawValue, width:w, clickHandler:{[weak self] (stableId) in - self?.scrollTo(stableId: stableId) - })) - } - //set(stickClass: TableStickItem.self, handler:(Table)) genericView.tableView.addScroll(listener: TableScrollListener(dispatchWhenVisibleRangeUpdated: false, { [weak self] _ in if let view = self?.genericView { view.tableView.enumerateVisibleItems(with: { item -> Bool in @@ -306,19 +369,91 @@ class EmojiViewController: TelegramGenericViewController, T })) } + func readyForDisplay(_ recent: RecentUsedEmoji, _ search: [String]?) -> Void { + + + let initialSize = atomicSize.modify({$0}) + genericView.tableView.beginTableUpdates() + genericView.tableView.removeAll() + genericView.tabs.removeAll() + + if let search = search { + + let lines = search.chunks(8).map({ clues -> [NSAttributedString] in + return clues.map({NSAttributedString.initialize(string: $0, font: .normal(26.0))}) + }) + if lines.count > 0 { + let _ = genericView.tableView.addItem(item: EBlockItem(initialSize, attrLines: lines, segment: .Recent, account: context.account, selectHandler: { [weak self] emoji in + self?.interactions?.sendEmoji(emoji) + })) + } + + } else { + var e = emojiesInstance + e[EmojiSegment.Recent] = recent.emojies + let seg = segments(e, skinModifiers: recent.skinModifiers) + let seglist = seg.map { (key,_) -> EmojiSegment in + return key + }.sorted(by: <) + + + + let w = floorToScreenPixels(System.backingScale, 350 / CGFloat(seg.count)) + + var tabIcons:[CGImage] = [] + tabIcons.append(theme.icons.emojiRecentTab) + tabIcons.append(theme.icons.emojiSmileTab) + tabIcons.append(theme.icons.emojiNatureTab) + tabIcons.append(theme.icons.emojiFoodTab) + tabIcons.append(theme.icons.emojiSportTab) + tabIcons.append(theme.icons.emojiCarTab) + tabIcons.append(theme.icons.emojiObjectsTab) + tabIcons.append(theme.icons.emojiSymbolsTab) + tabIcons.append(theme.icons.emojiFlagsTab) + + var tabIconsSelected:[CGImage] = [] + tabIconsSelected.append(theme.icons.emojiRecentTabActive) + tabIconsSelected.append(theme.icons.emojiSmileTabActive) + tabIconsSelected.append(theme.icons.emojiNatureTabActive) + tabIconsSelected.append(theme.icons.emojiFoodTabActive) + tabIconsSelected.append(theme.icons.emojiSportTabActive) + tabIconsSelected.append(theme.icons.emojiCarTabActive) + tabIconsSelected.append(theme.icons.emojiObjectsTabActive) + tabIconsSelected.append(theme.icons.emojiSymbolsTabActive) + tabIconsSelected.append(theme.icons.emojiFlagsTabActive) + for key in seglist { + if key != .Recent { + let _ = genericView.tableView.addItem(item: EStickItem(initialSize, segment:key, segmentName:key.localizedString)) + } + let _ = genericView.tableView.addItem(item: EBlockItem(initialSize, attrLines: seg[key]!, segment: key, account: context.account, selectHandler: { [weak self] emoji in + self?.interactions?.sendEmoji(emoji) + })) + let _ = genericView.tabs.addItem(item: ETabRowItem(initialSize, icon: tabIcons[key.hashValue], iconSelected:tabIconsSelected[key.hashValue], stableId:key.rawValue, width:w, clickHandler:{[weak self] (stableId) in + self?.scrollTo(stableId: stableId) + })) + } + } + genericView.tableView.endTableUpdates() + genericView.updateVisibility(genericView.tableView.isEmpty, isSearch: search != nil) + + // self.genericView.tableView.scroll(to: .up(true)) + } + func update(with interactions: EntertainmentInteractions) { self.interactions = interactions } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) + override var supportSwipes: Bool { + return !genericView.tabs._mouseInside() } + func scrollTo(stableId:AnyHashable) -> Void { genericView.tabs.changeSelection(stableId: stableId) - genericView.tableView.scroll(to: .top(id: stableId, animated: true, focus: false, inset: 0), inset:NSEdgeInsets(top:3)) + genericView.tableView.scroll(to: .top(id: stableId, innerId: nil, animated: true, focus: .init(focus: false), inset: 0), inset:NSEdgeInsets(top:3)) + } + override func scrollup(force: Bool = false) { + self.genericView.tableView.scroll(to: .up(true)) } - } diff --git a/Telegram-Mac/EmptyChatViewController.swift b/Telegram-Mac/EmptyChatViewController.swift index c0f968e2ff..c43d257c31 100644 --- a/Telegram-Mac/EmptyChatViewController.swift +++ b/Telegram-Mac/EmptyChatViewController.swift @@ -8,28 +8,83 @@ import Cocoa import TGUIKit -import TelegramCoreMac +import TelegramCore + +import SwiftSignalKit + + class EmptyChatView : View { private let containerView: View = View() private let label:TextView = TextView() private let imageView:ImageView = ImageView() + + let toggleTips: ImageButton = ImageButton() + + private var cards: NSView? + + func toggleTips(_ isEnabled: Bool, animated: Bool, view: NSView) { + if isEnabled { + addSubview(view) + self.cards = view + view.frame = NSMakeRect(0, 0, frame.width, 370) + view.center() + if animated { + view.layer?.animateAlpha(from: 0, to: 1, duration: 0.3, timingFunction: .spring) + view.layer?.animateScaleSpring(from: 0.1, to: 1, duration: 0.3) + } + } else { + performSubviewRemoval(view, animated: animated, duration: 0.3, timingFunction: .spring) + if animated { + view.layer?.animateScaleSpring(from: 1, to: 0.1, duration: 0.3, bounce: false) + } + + self.cards = nil + } + containerView.change(opacity: isEnabled ? 0 : 1, animated: animated) + toggleTips.set(image: isEnabled ? theme.empty_chat_hidetips : theme.empty_chat_showtips, for: .Normal) + + } + required init(frame frameRect: NSRect) { super.init(frame: frameRect) + self.layer = CAGradientLayer() + self.layer?.disableActions() + + toggleTips.set(image: theme.empty_chat_showtips, for: .Normal) + toggleTips.setFrameSize(NSMakeSize(30, 30)) + toggleTips.set(background: theme.chatServiceItemColor, for: .Normal) + toggleTips.autohighlight = false + toggleTips.scaleOnClick = true + toggleTips.layer?.cornerRadius = 15 + addSubview(containerView) containerView.addSubview(imageView) containerView.addSubview(label) - updateLocalizationAndTheme() + addSubview(toggleTips) + label.userInteractionEnabled = false + label.isSelectable = false + updateLocalizationAndTheme(theme: theme) } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - containerView.backgroundColor = theme.colors.background - self.background = theme.colors.background + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) imageView.image = theme.icons.chatEmpty + switch theme.controllerBackgroundMode { + case .plain: + imageView.isHidden = false + default: + imageView.isHidden = true + } + + toggleTips.set(image: cards != nil ? theme.empty_chat_hidetips : theme.empty_chat_showtips, for: .Normal) + toggleTips.set(background: theme.chatServiceItemColor, for: .Normal) + imageView.sizeToFit() - label.backgroundColor = theme.colors.background - label.update(TextViewLayout(.initialize(string: tr(.emptyPeerDescription), color: theme.colors.grayText, font: .normal(.header)), maximumNumberOfLines: 1)) + label.disableBackgroundDrawing = true + label.backgroundColor = imageView.isHidden ? theme.chatServiceItemColor : theme.chatBackground + label.update(TextViewLayout(.initialize(string: L10n.emptyPeerDescription, color: imageView.isHidden ? theme.chatServiceItemTextColor : theme.colors.grayText, font: .medium(imageView.isHidden ? .text : .header)), maximumNumberOfLines: 1, alignment: .center)) needsLayout = true } @@ -41,20 +96,62 @@ class EmptyChatView : View { super.layout() label.layout?.measure(width: frame.size.width - 20) label.update(label.layout) - containerView.setFrameSize(frame.size.width - 20, imageView.frame.size.height + label.frame.size.height + 30) - imageView.centerX() - containerView.center() - label.centerX(y: imageView.frame.maxY + 30) + + cards?.frame = NSMakeRect(0, 0, frame.width, 370) + cards?.center() + + + + if imageView.isHidden { + + label.setFrameSize(label.frame.width + 16, label.frame.height + 6) + + containerView.setFrameSize(label.frame.width + 20, 24) + containerView.center() + label.center() + label.layer?.cornerRadius = label.frame.height / 2 + containerView.layer?.cornerRadius = containerView.frame.height / 2 + } else { + containerView.setFrameSize(max(imageView.frame.width, label.frame.width) + 40, imageView.frame.size.height + label.frame.size.height + 70) + imageView.centerX(y: 20) + containerView.center() + label.centerX(y: imageView.frame.maxY + 30) + containerView.layer?.cornerRadius = 0 + } + + toggleTips.setFrameOrigin(NSMakePoint(frame.width - toggleTips.frame.width - 10, 10)) } } class EmptyChatViewController: TelegramGenericViewController { - override init(_ account: Account) { - super.init(account) + + + private let cards: WidgetController + override init(_ context: AccountContext) { + cards = WidgetController(context) + super.init(context) self.bar = NavigationBarStyle(height:0) } + private var temporaryTouchBar: Any? + + @available(OSX 10.12.2, *) + override func makeTouchBar() -> NSTouchBar? { + if temporaryTouchBar == nil { + temporaryTouchBar = ChatListTouchBar(context: self.context, search: { [weak self] in + self?.context.sharedContext.bindings.globalSearch("") + }, newGroup: { [weak self] in + self?.context.composeCreateGroup() + }, newSecretChat: { [weak self] in + self?.context.composeCreateSecretChat() + }, newChannel: { [weak self] in + self?.context.composeCreateChannel() + }) + } + return temporaryTouchBar as? NSTouchBar + } + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) (navigationController as? MajorNavigationController)?.closeSidebar() @@ -63,14 +160,84 @@ class EmptyChatViewController: TelegramGenericViewController { override func escapeKeyAction() -> KeyHandlerResult { return .rejected } + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) + updateBackgroundColor(theme.controllerBackgroundMode) + } + + override func updateBackgroundColor(_ backgroundMode: TableBackgroundMode) { + super.updateBackgroundColor(backgroundMode) + var containerBg = self.backgroundColor + if theme.bubbled { + switch theme.backgroundMode { + case .background, .tiled, .gradient: + containerBg = .clear + case .plain: + if theme.colors.chatBackground == theme.colors.background { + containerBg = theme.colors.border + } else { + containerBg = .clear + } + case let .color(color): + if color == theme.colors.background { + containerBg = theme.colors.border + } else { + containerBg = .clear + } + } + } else { + if theme.colors.chatBackground == theme.colors.background { + containerBg = theme.colors.border + } else { + containerBg = .clear + } + } + self.backgroundColor = containerBg + } + + override public var isOpaque: Bool { + return false + } + + override var responderPriority: HandlerPriority { + return .medium + } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - globalPeerHandler.set(.single(nil)) + context.globalPeerHandler.set(.single(nil)) + } + + override func backKeyAction() -> KeyHandlerResult { + return cards.backKeyAction() + } + override func nextKeyAction() -> KeyHandlerResult { + return cards.nextKeyAction() + } + + private let disposable = MetaDisposable() + + deinit { + disposable.dispose() } override func viewDidLoad() { super.viewDidLoad() - self.readyOnce() + +// readyOnce() + + self.ready.set(cards.ready.get()) + + + self.genericView.toggleTips(FastSettings.emptyTips, animated: false, view: cards.view) + + self.genericView.toggleTips.set(handler: { [weak self] _ in + guard let cards = self?.cards.view else { + return + } + FastSettings.updateEmptyTips(!FastSettings.emptyTips) + self?.genericView.toggleTips(FastSettings.emptyTips, animated: true, view: cards) + }, for: .Click) } } diff --git a/Telegram-Mac/EmptyComposeController.swift b/Telegram-Mac/EmptyComposeController.swift index cae9abd175..4f751120d2 100644 --- a/Telegram-Mac/EmptyComposeController.swift +++ b/Telegram-Mac/EmptyComposeController.swift @@ -8,8 +8,9 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac -import TelegramCoreMac +import SwiftSignalKit +import TelegramCore + class ComposeState { let result:T diff --git a/Telegram-Mac/EmptyGroupstickerSearchRowItem.swift b/Telegram-Mac/EmptyGroupstickerSearchRowItem.swift index 9779feae42..7841fe076c 100644 --- a/Telegram-Mac/EmptyGroupstickerSearchRowItem.swift +++ b/Telegram-Mac/EmptyGroupstickerSearchRowItem.swift @@ -11,8 +11,8 @@ import TGUIKit class EmptyGroupstickerSearchRowItem: GeneralRowItem { - init(_ initialSize: NSSize, height: CGFloat, stableId: AnyHashable, inset:NSEdgeInsets = NSEdgeInsets(left: 30.0, right: 30.0)) { - super.init(initialSize, height: height, stableId: stableId, inset: inset) + init(_ initialSize: NSSize, height: CGFloat, stableId: AnyHashable, viewType: GeneralViewType = .legacy) { + super.init(initialSize, height: height, stableId: stableId, viewType: viewType) } override func viewClass() -> AnyClass { @@ -22,43 +22,75 @@ class EmptyGroupstickerSearchRowItem: GeneralRowItem { private class EmptyGroupstickerSearchRowView : TableRowView { + private let containerView = GeneralRowContainerView(frame: NSZeroRect) private let headerView: TextView = TextView() private let descView: TextView = TextView() private let imageView: ImageView = ImageView() required init(frame frameRect: NSRect) { super.init(frame: frameRect) - addSubview(imageView) - addSubview(headerView) - addSubview(descView) + containerView.addSubview(imageView) + containerView.addSubview(headerView) + containerView.addSubview(descView) + addSubview(containerView) } override func set(item: TableRowItem, animated: Bool) { super.set(item: item, animated: animated) + + if let item = item as? GeneralRowItem { + let contentRect: NSRect + switch item.viewType { + case .legacy: + contentRect = bounds + case .modern: + contentRect = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) + } + self.containerView.change(size: contentRect.size, animated: animated, corners: item.viewType.corners) + self.containerView.change(pos: contentRect.origin, animated: animated) + } + imageView.image = theme.icons.groupStickerNotFound imageView.sizeToFit() needsLayout = true } + override var backdorColor: NSColor { + return theme.colors.background + } + override func updateColors() { - descView.backgroundColor = backdorColor - headerView.backgroundColor = backdorColor + if let item = item as? GeneralRowItem { + descView.backgroundColor = backdorColor + headerView.backgroundColor = backdorColor + containerView.backgroundColor = backdorColor + backgroundColor = item.viewType.rowBackground + } } override func layout() { super.layout() if let item = item as? EmptyGroupstickerSearchRowItem { - let headerLayout = TextViewLayout(.initialize(string: tr(.groupStickersEmptyHeader), color: theme.colors.redUI, font: .medium(.text)), maximumNumberOfLines: 1) - let descLayout = TextViewLayout(.initialize(string: tr(.groupStickersEmptyDesc), color: theme.colors.grayText, font: .normal(.text)), maximumNumberOfLines: 1) - headerLayout.measure(width: frame.width - item.inset.left - item.inset.right - 40) - descLayout.measure(width: frame.width - item.inset.left - item.inset.right - 40) - descView.update(descLayout) - headerView.update(headerLayout) - - headerView.setFrameOrigin(item.inset.left + 40, 8) - descView.setFrameOrigin(item.inset.left + 40, frame.height - descView.frame.height - 8) - - imageView.centerY(x: item.inset.left) + switch item.viewType { + case .legacy: + containerView.frame = bounds + case let .modern(_, insets): + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) + + let headerLayout = TextViewLayout(.initialize(string: L10n.groupStickersEmptyHeader, color: theme.colors.redUI, font: .medium(.text)), maximumNumberOfLines: 1) + let descLayout = TextViewLayout(.initialize(string: L10n.groupStickersEmptyDesc, color: theme.colors.grayText, font: .normal(.text)), maximumNumberOfLines: 1) + headerLayout.measure(width: item.blockWidth - insets.left - insets.right - 40) + descLayout.measure(width: item.blockWidth - insets.left - insets.right - 40) + descView.update(descLayout) + headerView.update(headerLayout) + + headerView.setFrameOrigin(insets.left + 40, 8) + descView.setFrameOrigin(insets.left + 40, containerView.frame.height - descView.frame.height - 8) + + imageView.centerY(x: insets.left) + + } + self.containerView.setCorners(item.viewType.corners) } } diff --git a/Telegram-Mac/EntertainmentViewController.swift b/Telegram-Mac/EntertainmentViewController.swift index d2fa23459d..511edd04f8 100644 --- a/Telegram-Mac/EntertainmentViewController.swift +++ b/Telegram-Mac/EntertainmentViewController.swift @@ -8,38 +8,588 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac -import TelegramCoreMac -import PostboxMac +import SwiftSignalKit +import TelegramCore + +import Postbox + + +enum ESearchCommand { + case loading + case normal + case close + case clearText + case apply(String) +} + +open class EntertainmentSearchView: OverlayControl, NSTextViewDelegate { + + public private(set) var state:SearchFieldState = .None + private(set) public var input:NSTextView = SearchTextField() + + private var lock:Bool = false + + private let clear:ImageButton = ImageButton() + private let search:ImageView = ImageView() + private let progressIndicator:ProgressIndicator = ProgressIndicator(frame: NSMakeRect(0, 0, 18, 18)) + private let placeholder:TextViewLabel = TextViewLabel() + + + public let inset:CGFloat = 6 + public let leftInset:CGFloat = 20.0 + + public var searchInteractions:SearchInteractions? + + private let _searchValue:ValuePromise = ValuePromise(SearchState(state: .None, request: nil), ignoreRepeated: true) + + public var searchValue: Signal { + return _searchValue.get() + } + public var shouldUpdateTouchBarItemIdentifiers: (()->[Any])? + + + private let inputContainer = View() + + public var isLoading:Bool = false { + didSet { + if oldValue != isLoading { + self.updateLoading() + needsLayout = true + } + } + } + + override open func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + + let theme = (theme as! TelegramPresentationTheme) + inputContainer.backgroundColor = .clear + input.textColor = presentation.search.textColor + input.backgroundColor = presentation.colors.background + placeholder.attributedString = .initialize(string: presentation.search.placeholder(), color: presentation.search.placeholderColor, font: .normal(.title)) + placeholder.backgroundColor = presentation.colors.background + self.backgroundColor = presentation.colors.background + placeholder.sizeToFit() + search.image = theme.icons.entertainment_Search + search.sizeToFit() + clear.set(image: theme.icons.entertainment_SearchCancel, for: .Normal) + _ = clear.sizeToFit() + input.insertionPointColor = presentation.search.textColor + progressIndicator.progressColor = theme.colors.grayIcon + needsLayout = true + + } + + open var startTextInset: CGFloat { + return leftInset + } + + open var placeholderTextInset: CGFloat { + return startTextInset + } + + required public init(frame frameRect: NSRect) { + super.init(frame: frameRect) + self.backgroundColor = .grayBackground + if #available(OSX 10.12.2, *) { + input.allowsCharacterPickerTouchBarItem = false + } + progressIndicator.isHidden = true + input.focusRingType = .none + input.autoresizingMask = [.width, .height] + input.backgroundColor = NSColor.clear + input.delegate = self + input.isRichText = false + + input.textContainer?.widthTracksTextView = true + input.textContainer?.heightTracksTextView = false + + input.isHorizontallyResizable = false + input.isVerticallyResizable = false + + input.font = .normal(.title) + input.textColor = .text + input.isHidden = true + input.drawsBackground = false + + input.setFrameSize(20, 18) + + placeholder.sizeToFit() + self.border = [.Bottom] + + //self.addSubview(search) + self.addSubview(placeholder) + inputContainer.addSubview(input) + addSubview(inputContainer) + inputContainer.backgroundColor = .clear + clear.backgroundColor = .clear + + + clear.set(handler: { [weak self] _ in + self?.cancelSearch() + }, for: .Click) + + addSubview(clear) + + clear.isHidden = true + + + self.set(handler: {[weak self] (event) in + if let strongSelf = self { + strongSelf.change(state: .Focus , true) + } + }, for: .Click) + + updateLocalizationAndTheme(theme: theme) + + + + progressIndicator.set(handler: { [weak self] _ in + self?.cancelSearch() + }, for: .Click) + + } + + @available(OSX 10.12.2, *) + public func textView(_ textView: NSTextView, shouldUpdateTouchBarItemIdentifiers identifiers: [NSTouchBarItem.Identifier]) -> [NSTouchBarItem.Identifier] { + return self.shouldUpdateTouchBarItemIdentifiers?() as? [NSTouchBarItem.Identifier] ?? identifiers + } + + open func cancelSearch() { + if self.query.isEmpty { + change(state: .None, true) + } else { + setString("") + } + } + + open func textView(_ textView: NSTextView, shouldChangeTextIn affectedCharRange: NSRange, replacementString: String?) -> Bool { + if let trimmed = replacementString?.trimmed, trimmed.isEmpty, affectedCharRange.min == 0 && affectedCharRange.max == 0, textView.string.isEmpty { + return false + } + if replacementString == "\n" { + return false + } + return true + } + + + + open func textDidChange(_ notification: Notification) { + + let trimmed = input.string.trimmingCharacters(in: CharacterSet(charactersIn: "\n\r")) + if trimmed != input.string { + self.setString(trimmed) + return + } + + let value = SearchState(state: state, request: trimmed, responder: self.input == window?.firstResponder) + searchInteractions?.textModified(value) + _searchValue.set(value) + + + let pHidden = !input.string.isEmpty + if placeholder.isHidden != pHidden { + placeholder.isHidden = pHidden + } + + needsLayout = true + + let iHidden = !(state == .Focus && !input.string.isEmpty) + if input.isHidden != iHidden { + // input.isHidden = iHidden + window?.makeFirstResponder(input) + } + } + + open override func mouseUp(with event: NSEvent) { + if isLoading { + let point = convert(event.locationInWindow, from: nil) + if NSPointInRect(point, progressIndicator.frame) { + setString("") + } else { + super.mouseUp(with: event) + } + } else { + super.mouseUp(with: event) + } + } + + + public func textViewDidChangeSelection(_ notification: Notification) { + if let storage = input.textStorage { + let size = storage.size() + + let inputInset = placeholderTextInset + + let defWidth = frame.width - inputInset - inset - clear.frame.width - 10 + // input.sizeToFit() + input.setFrameSize(max(size.width + 10, defWidth), size.height) + // inputContainer.setFrameSize(inputContainer.frame.width, input.frame.height) + if let layout = input.layoutManager, !input.string.isEmpty { + let index = max(0, input.selectedRange().max - 1) + let point = layout.location(forGlyphAt: layout.glyphIndexForCharacter(at: index)) + + let additionalInset: CGFloat + if index + 2 < input.string.length { + let nextPoint = layout.location(forGlyphAt: layout.glyphIndexForCharacter(at: index + 2)) + additionalInset = nextPoint.x - point.x + } else { + additionalInset = 8 + } + + if defWidth < size.width && point.x > defWidth { + input.setFrameOrigin(floorToScreenPixels(backingScaleFactor, defWidth - point.x - additionalInset), input.frame.minY) + if input.frame.maxX < inputContainer.frame.width { + input.setFrameOrigin(inputContainer.frame.width - input.frame.width + 4, input.frame.minY) + } + } else { + input.setFrameOrigin(0, input.frame.minY) + } + } else { + input.setFrameOrigin(0, input.frame.minY) + } + needsLayout = true + } + } + + open func textDidEndEditing(_ notification: Notification) { + didResignResponder() + } + + open func textDidBeginEditing(_ notification: Notification) { + didBecomeResponder() + } + + open var isEmpty: Bool { + return query.isEmpty + } + + open func didResignResponder() { + let value = SearchState(state: state, request: self.query, responder: false) + searchInteractions?.responderModified(value) + _searchValue.set(value) + if isEmpty { + change(state: .None, true) + } + + self.kitWindow?.removeAllHandlers(for: self) + self.kitWindow?.removeObserver(for: self) + } + + open func didBecomeResponder() { + let value = SearchState(state: state, request: self.query, responder: true) + searchInteractions?.responderModified(SearchState(state: state, request: self.query, responder: true)) + _searchValue.set(value) + + change(state: .Focus, true) + + self.kitWindow?.set(escape: { [weak self] _ -> KeyHandlerResult in + if let strongSelf = self { + return strongSelf.changeResponder() ? .invoked : .rejected + } + return .rejected + + }, with: self, priority: .modal) + + self.kitWindow?.set(handler: { [weak self] _ -> KeyHandlerResult in + if self?.state == .Focus { + return .invokeNext + } + return .rejected + }, with: self, for: .RightArrow, priority: .modal) + + self.kitWindow?.set(handler: { [weak self] _ -> KeyHandlerResult in + if self?.state == .Focus { + return .invokeNext + } + return .rejected + }, with: self, for: .LeftArrow, priority: .modal) + + self.kitWindow?.set(responder: {[weak self] () -> NSResponder? in + return self?.input + }, with: self, priority: .modal) + } + + + open override func draw(_ layer: CALayer, in ctx: CGContext) { + super.draw(layer, in: ctx) + } + + + + open func change(state:SearchFieldState, _ animated:Bool) -> Void { + + if state != self.state && !lock { + self.state = state + + let text = input.string.trimmingCharacters(in: CharacterSet(charactersIn: "\n\r")) + let value = SearchState(state: state, request: state == .None ? nil : text, responder: self.input == window?.firstResponder) + searchInteractions?.stateModified(value, animated) + + _searchValue.set(value) + + lock = true + + if state == .Focus { + + window?.makeFirstResponder(input) + + let inputInset = placeholderTextInset + 8 + + inputContainer.setFrameSize(frame.width - inputInset - inset - clear.frame.width - 6, input.frame.height) + inputContainer.centerY(x: inputInset) + input.frame = inputContainer.bounds + + input.isHidden = false + + self.input.isHidden = false + self.window?.makeFirstResponder(self.input) + self.lock = false + + clear.isHidden = false + clear.layer?.opacity = 1.0 + + } + + if state == .None { + + self.kitWindow?.removeAllHandlers(for: self) + self.kitWindow?.removeObserver(for: self) + + self.input.isHidden = true + self.input.string = "" + self.window?.makeFirstResponder(nil) + self.placeholder.isHidden = false + + if animated { + + clear.layer?.animate(from: 1.0 as NSNumber, to: 0.0 as NSNumber, keyPath: "opacity", timingFunction: animationStyle.function, duration: animationStyle.duration, removeOnCompletion:true, additive:false, completion: {[weak self] (complete) in + self?.clear.isHidden = true + self?.lock = false + }) + } else { + clear.isHidden = true + lock = false + } + + clear.layer?.opacity = 0.0 + } + updateLoading() + self.needsLayout = true + } + + } + + open override func viewWillMove(toWindow newWindow: NSWindow?) { + if newWindow == nil { + if isEmpty { + change(state: .None, false) + } + self.kitWindow?.removeAllHandlers(for: self) + self.kitWindow?.removeObserver(for: self) + } + } + + + func updateLoading() { + if isLoading && state == .Focus { + if progressIndicator.superview == nil { + addSubview(progressIndicator) + } + clear.isHidden = false + progressIndicator.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + clear.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak self] completed in + if completed { + self?.clear.isHidden = true + } + }) + progressIndicator.isHidden = false + progressIndicator.animates = true + } else { + progressIndicator.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak self] completed in + if completed { + self?.progressIndicator.isHidden = true + } + }) + clear.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + progressIndicator.animates = false + progressIndicator.removeFromSuperview() + clear.isHidden = self.state == .None + } + if window?.firstResponder == input { + window?.makeFirstResponder(input) + } + } + + + + + open override func layout() { + super.layout() + search.centerY(x: leftInset) + placeholder.centerY(x: placeholderTextInset + 2) + clear.centerY(x: frame.width - leftInset - clear.frame.width) + progressIndicator.frame = NSMakeRect(clear.frame.minX + 2, clear.frame.minY + 2, self.clear.frame.width - 4, self.clear.frame.height - 4) + inputContainer.centerY(x: placeholderTextInset, addition: 1) + } + + public func changeResponder(_ animated:Bool = true) -> Bool { + if state == .Focus { + cancelSearch() + } else { + change(state: .Focus, animated) + } + return true + } + + deinit { + self.kitWindow?.removeAllHandlers(for: self) + self.kitWindow?.removeObserver(for: self) + } + + public var query:String { + return self.input.string + } + + open override func change(size: NSSize, animated: Bool = true, _ save: Bool = true, removeOnCompletion: Bool = false, duration: Double = 0.2, timingFunction: CAMediaTimingFunctionName = CAMediaTimingFunctionName.easeOut, completion: ((Bool) -> Void)? = nil) { + super.change(size: size, animated: animated, save, duration: duration, timingFunction: timingFunction) + clear.change(pos: NSMakePoint(frame.width - inset - clear.frame.width, clear.frame.minY), animated: animated) + } + + + public func setString(_ string:String) { + self.input.string = string + textDidChange(Notification(name: NSText.didChangeNotification)) + needsLayout = true + } + + public func cancel(_ animated:Bool) -> Void { + change(state: .None, animated) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + + public final class EntertainmentInteractions { var current:EntertainmentState = .emoji var sendEmoji:(String) ->Void = {_ in} - var sendSticker:(TelegramMediaFile) ->Void = {_ in} - var sendGIF:(TelegramMediaFile) ->Void = {_ in} + var sendSticker:(TelegramMediaFile, Bool) ->Void = { _, _ in} + var sendGIF:(TelegramMediaFile, Bool) ->Void = { _, _ in} - var showEntertainment:(EntertainmentState,Bool)->Void = { _,_ in} + var showEntertainment:(EntertainmentState, Bool)->Void = { _,_ in} var close:()->Void = {} + var toggleSearch:()->Void = { } + let peerId:PeerId init(_ defaultState: EntertainmentState, peerId:PeerId) { current = defaultState self.peerId = peerId } - } +final class EntertainmentView : View { + fileprivate var sectionView: NSView + private let bottomView = View() + private let borderView = View() + fileprivate let emoji: ImageButton = ImageButton() + fileprivate let stickers: ImageButton = ImageButton() + fileprivate let gifs: ImageButton = ImageButton() + + + + private let sectionTabs: View = View() + init(sectionView: NSView, frame: NSRect) { + self.sectionView = sectionView + super.init(frame: frame) + self.bottomView.border = [.Top] + self.addSubview(self.sectionView) + addSubview(self.bottomView) + self.bottomView.addSubview(sectionTabs) + + self.sectionTabs.addSubview(self.emoji) + self.sectionTabs.addSubview(self.stickers) + self.sectionTabs.addSubview(self.gifs) + + self.bottomView.addSubview(self.borderView) + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) + self.borderView.background = theme.colors.border + self.emoji.set(image: theme.icons.entertainment_Emoji, for: .Normal) + self.stickers.set(image: theme.icons.entertainment_Stickers, for: .Normal) + self.gifs.set(image: theme.icons.entertainment_Gifs, for: .Normal) + _ = self.emoji.sizeToFit() + _ = self.stickers.sizeToFit() + _ = self.gifs.sizeToFit() + + } + + func toggleSearch(_ signal:ValuePromise) { + + } + + func updateSelected(_ state: EntertainmentState, mode: EntertainmentViewController.Mode) { + self.emoji.isSelected = false + self.stickers.isSelected = false + self.gifs.isSelected = false + + switch state { + case .emoji: + self.emoji.isSelected = true + case .stickers: + self.stickers.isSelected = true + case .gifs: + self.gifs.isSelected = true + } + emoji.isHidden = mode == .selectAvatar + + needsLayout = true + } + + + override func layout() { + super.layout() + self.sectionView.frame = NSMakeRect(0, 0, self.frame.width, self.frame.height - 50) + self.bottomView.frame = NSMakeRect(0, self.frame.height - 50, self.frame.width, 50) + self.borderView.frame = NSMakeRect(0, 0, self.bottomView.frame.width, .borderSize) + + let buttons:[NSView] = [self.emoji, self.stickers, self.gifs].filter { !$0.isHidden } + + self.sectionTabs.setFrameSize(NSMakeSize(buttons.reduce(0, { $0 + $1.frame.width }) + CGFloat(buttons.count - 1) * 20, 40)) + self.sectionTabs.center() + + for (i, button) in buttons.enumerated() { + button.centerY(x: (button.frame.width + 20) * CGFloat(i)) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } +} -class EntertainmentViewController: NavigationViewController { +class EntertainmentViewController: TelegramGenericViewController { private let languageDisposable:MetaDisposable = MetaDisposable() - private var account:Account - private var chatInteraction:ChatInteraction? - private var interactions:EntertainmentInteractions? + private(set) weak var chatInteraction:ChatInteraction? + private(set) var interactions:EntertainmentInteractions? private let cap:SidebarCapViewController private let section: SectionViewController @@ -47,36 +597,75 @@ class EntertainmentViewController: NavigationViewController { private var disposable:MetaDisposable = MetaDisposable() private var locked:Bool = false + enum Mode { + case common + case selectAvatar + } + + private let mode: Mode private let emoji:EmojiViewController - private let stickers:StickersViewController + private let stickers:NStickersViewController private let gifs:GIFViewController + private let searchState = ValuePromise(.init(state: .None, request: nil)) + + private var effectiveSearchView: SearchView? { + if self.gifs.view.superview != nil { + return self.gifs.genericView.searchView + } + if self.emoji.view.superview != nil { + return self.emoji.genericView.searchView + } + if self.stickers.view.superview != nil { + return self.stickers.genericView.searchView + } + return nil + } + func update(with chatInteraction:ChatInteraction) -> Void { self.chatInteraction = chatInteraction - let interactions = EntertainmentInteractions(FastSettings.entertainmentState, peerId: chatInteraction.peerId) + let state: EntertainmentState + if mode == .selectAvatar { + state = .stickers + } else { + state = FastSettings.entertainmentState + } + + let interactions = EntertainmentInteractions(state, peerId: chatInteraction.peerId) interactions.close = { [weak self] in self?.closePopover() } - interactions.sendSticker = { [weak self] file in - self?.chatInteraction?.sendAppFile(file) + interactions.sendSticker = { [weak self] file, silent in + self?.chatInteraction?.sendAppFile(file, silent, self?.effectiveSearchView?.query) self?.closePopover() } - interactions.sendGIF = { [weak self] file in - self?.chatInteraction?.sendAppFile(file) + interactions.sendGIF = { [weak self] file, silent in + self?.chatInteraction?.sendAppFile(file, silent, self?.effectiveSearchView?.query) self?.closePopover() } interactions.sendEmoji = { [weak self] emoji in - _ = self?.chatInteraction?.appendText(emoji) + if self?.mode == .selectAvatar { + _ = self?.chatInteraction?.sendPlainText(emoji) + self?.closePopover() + } else { + _ = self?.chatInteraction?.appendText(emoji) + } + + } + interactions.toggleSearch = { [weak self] in + guard let `self` = self else { + return + } + self.toggleSearch() } - self.interactions = interactions emoji.update(with: interactions) stickers.update(with: interactions, chatInteraction: chatInteraction) - gifs.update(with: interactions) + gifs.update(with: interactions, chatInteraction: chatInteraction) } @@ -84,31 +673,48 @@ class EntertainmentViewController: NavigationViewController { self.viewWillDisappear(false) } - init(size:NSSize, account:Account) { + init(size:NSSize, context:AccountContext, mode: Mode = .common) { + self.mode = mode + self.cap = SidebarCapViewController(context) + self.emoji = EmojiViewController(context) + self.stickers = NStickersViewController(context) + self.gifs = GIFViewController(context) - self.account = account - self.cap = SidebarCapViewController(account: account) - self.emoji = EmojiViewController(account) - self.stickers = StickersViewController(account:account) - self.gifs = GIFViewController(account: account) + self.stickers.mode = mode + self.gifs.mode = mode var items:[SectionControllerItem] = [] - items.append(SectionControllerItem(title: tr(.entertainmentEmoji).uppercased(), controller: emoji)) - items.append(SectionControllerItem(title: tr(.entertainmentStickers).uppercased(), controller: stickers)) - items.append(SectionControllerItem(title: tr(.entertainmentGIF ).uppercased(), controller: gifs)) - self.section = SectionViewController(sections: items, selected: Int(FastSettings.entertainmentState.rawValue)) - super.init(section) + if mode == .common { + items.append(SectionControllerItem(title:{L10n.entertainmentEmoji.uppercased()}, controller: emoji)) + } + items.append(SectionControllerItem(title: {L10n.entertainmentStickers.uppercased()}, controller: stickers)) + items.append(SectionControllerItem(title: {L10n.entertainmentGIF.uppercased()}, controller: gifs)) + + let index: Int + if mode == .selectAvatar { + index = 0 + } else { + index = Int(FastSettings.entertainmentState.rawValue) + } + self.section = SectionViewController(sections: items, selected: index, hasHeaderView: false) + super.init(context) bar = .init(height: 0) } - - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + self.section.updateLocalizationAndTheme(theme: theme) self.view.background = theme.colors.background - emoji.view.background = theme.colors.background - stickers.view.background = theme.colors.background - gifs.view.background = theme.colors.background + if emoji.isLoaded() { + emoji.view.background = theme.colors.background + } + if stickers.isLoaded() { + stickers.view.background = theme.colors.background + } + if gifs.isLoaded() { + gifs.view.background = theme.colors.background + } } deinit { @@ -120,52 +726,168 @@ class EntertainmentViewController: NavigationViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) section.viewWillAppear(animated) + updateLocalizationAndTheme(theme: theme) } + override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) section.viewWillDisappear(animated) + window?.removeAllHandlers(for: self) + } + + + private func toggleSearch() { + if let searchView = self.effectiveSearchView { + if searchView.state == .Focus { + searchView.setString("") + searchView.cancel(true) + } else { + searchView.change(state: .Focus, true) + } + } } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) section.viewDidAppear(animated) + + window?.set(handler: { [weak self] _ -> KeyHandlerResult in + guard let `self` = self else { + return .rejected + } + if self.context.sharedContext.bindings.rootNavigation().genericView.state != .single { + return .rejected + } + self.toggleSearch() + return .invoked + }, with: self, for: .F, priority: .modal, modifierFlags: .command) + } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) section.viewDidDisappear(animated) } + override func initializer() -> EntertainmentView { + let rect = NSMakeRect(self._frameRect.minX, self._frameRect.minY, self._frameRect.width, self._frameRect.height - self.bar.height) + self.section._frameRect = NSMakeRect(rect.minX, rect.minY, rect.width, rect.height - 50) + return EntertainmentView(sectionView: self.section.view, frame: rect) + } + override func firstResponder() -> NSResponder? { + if popover == nil { + return nil + } + return effectiveSearchView//genericView.searchView?.input + } override func viewDidLoad() { super.viewDidLoad() cap.loadViewIfNeeded() + let state:EntertainmentState + if mode == .selectAvatar { + state = .stickers + } else { + state = FastSettings.entertainmentState + } - let contentRect = NSMakeRect(0, 0, frame.width, frame.height) + self.genericView.updateSelected(state, mode: mode) + - emoji._frameRect = NSMakeRect(0, 0, frame.width, frame.height - 50) - stickers._frameRect = NSMakeRect(0, 0, frame.width, frame.height - 50) + let callSearchCmd:(ESearchCommand, SearchView)->Void = { command, view in + switch command { + case .clearText: + view.setString("") + case .loading: + view.isLoading = true + case .normal: + view.isLoading = false + case .close: + view.cancel(true) + case let .apply(value): + view.setString(value) + } + } + self.stickers.makeSearchCommand = { [weak self] command in + if self?.stickers.view.superview != nil, let view = self?.stickers.genericView.searchView { + callSearchCmd(command, view) + } + } + + self.gifs.makeSearchCommand = { [weak self] command in + if self?.gifs.view.superview != nil, let view = self?.gifs.genericView.searchView { + callSearchCmd(command, view) + } + } + self.emoji.makeSearchCommand = { [weak self] command in + if self?.emoji.view.superview != nil, let view = self?.emoji.genericView.searchView { + callSearchCmd(command, view) + } + } + + + + let e_index: Int = 0 + let s_index: Int = mode == .selectAvatar ? 0 : 1 + let g_index: Int = mode == .selectAvatar ? 1 : 2 + + self.genericView.emoji.set(handler: { [weak self] _ in + guard let `self` = self else { + return + } + if self.genericView.emoji.isSelected { + self.emoji.scrollup() + } + self.section.select(e_index, true, notifyApper: true) + + }, for: .Click) + + self.genericView.stickers.set(handler: { [weak self] _ in + guard let `self` = self else { + return + } + if self.genericView.stickers.isSelected { + self.stickers.scrollup() + } + self.section.select(s_index, true, notifyApper: true) + }, for: .Click) + + self.genericView.gifs.set(handler: { [weak self] _ in + guard let `self` = self else { + return + } + if self.genericView.gifs.isSelected { + self.gifs.scrollup() + } + self.section.select(g_index, true, notifyApper: true) + }, for: .Click) + + + let mode = self.mode section.selectionUpdateHandler = { [weak self] index in - FastSettings.changeEntertainmentState(EntertainmentState(rawValue: Int32(index))!) - self?.chatInteraction?.update({$0.withUpdatedIsEmojiSection(index == 0)}) + var index = index + if mode == .selectAvatar { + index += 1 + } else { + FastSettings.changeEntertainmentState(state) + } + + let state = EntertainmentState(rawValue: Int32(index))! + self?.chatInteraction?.update({ $0.withUpdatedIsEmojiSection(state == .emoji )}) + self?.genericView.updateSelected(state, mode: mode) + } - section._frameRect = contentRect - addSubview(section.view) - self.ready.set(section.ready.get()) languageDisposable.set((combineLatest(appearanceSignal, ready.get() |> filter {$0} |> take(1))).start(next: { [weak self] _ in - self?.updateLocalizationAndTheme() + self?.updateLocalizationAndTheme(theme: theme) })) } - - - } diff --git a/Telegram-Mac/ExMajorNavigationController.swift b/Telegram-Mac/ExMajorNavigationController.swift index 8bcd8fe9a5..931d1e44e0 100644 --- a/Telegram-Mac/ExMajorNavigationController.swift +++ b/Telegram-Mac/ExMajorNavigationController.swift @@ -8,18 +8,36 @@ import Cocoa import TGUIKit -import TelegramCoreMac +import TelegramCore + class ExMajorNavigationController: MajorNavigationController { - private let account:Account + private let context:AccountContext override var sidebar: ViewController? { - return account.context.entertainment + return context.sharedContext.bindings.entertainment() + } + + override var window: Window? { + return context.window + } + + open override var responderPriority: HandlerPriority { + return .medium + } + + public init(_ context: AccountContext, _ majorClass:AnyClass, _ empty:ViewController) { + self.context = context + super.init(majorClass, empty, context.window) + } + + override func push(_ controller: ViewController, _ animated: Bool, style: ViewControllerStyle?) { + super.push(controller, animated, style: style) } - public init(_ account: Account, _ majorClass:AnyClass, _ empty:ViewController) { - self.account = account - super.init(majorClass, empty) + @available(OSX 10.12.2, *) + override func makeTouchBar() -> NSTouchBar? { + return controller.makeTouchBar()//globalAudio?.makeTouchBar()// } } diff --git a/Telegram-Mac/ExportProxyModalController.swift b/Telegram-Mac/ExportProxyModalController.swift index 2c31beafd3..6bac1ec66e 100644 --- a/Telegram-Mac/ExportProxyModalController.swift +++ b/Telegram-Mac/ExportProxyModalController.swift @@ -8,7 +8,8 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit diff --git a/Telegram-Mac/ExportedInvitationController.swift b/Telegram-Mac/ExportedInvitationController.swift new file mode 100644 index 0000000000..feba3f160d --- /dev/null +++ b/Telegram-Mac/ExportedInvitationController.swift @@ -0,0 +1,284 @@ +// +// ExportedInvitationController.swift +// Telegram +// +// Created by Mikhail Filimonov on 17.01.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import SwiftSignalKit +import Postbox +import TGUIKit + +private final class ExportInvitationArguments { + let context: PeerInvitationImportersContext + let accountContext: AccountContext + let copyLink: (String)->Void + let shareLink: (String)->Void + let openProfile:(PeerId)->Void + let revokeLink: (ExportedInvitation)->Void + let editLink:(ExportedInvitation)->Void + init(context: PeerInvitationImportersContext, accountContext: AccountContext, copyLink: @escaping(String)->Void, shareLink: @escaping(String)->Void, openProfile:@escaping(PeerId)->Void, revokeLink: @escaping(ExportedInvitation)->Void, editLink: @escaping(ExportedInvitation)->Void) { + self.context = context + self.accountContext = accountContext + self.copyLink = copyLink + self.shareLink = shareLink + self.openProfile = openProfile + self.revokeLink = revokeLink + self.editLink = editLink + } +} + +private struct ExportInvitationState : Equatable { + +} + +private let _id_link = InputDataIdentifier("_id_link") +private func _id_admin(_ peerId: PeerId) -> InputDataIdentifier { + return InputDataIdentifier("_id_admin_\(peerId.toInt64())") +} +private func _id_peer(_ peerId: PeerId) -> InputDataIdentifier { + return InputDataIdentifier("_id_peer_\(peerId.toInt64())") +} +private func entries(_ state: PeerInvitationImportersState, admin: Peer?, invitation: ExportedInvitation, arguments: ExportInvitationArguments) -> [InputDataEntry] { + + var entries:[InputDataEntry] = [] + + var sectionId: Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_link, equatable: InputDataEquatable(invitation), comparable: nil, item: { initialSize, stableId in + return ExportedInvitationRowItem(initialSize, stableId: stableId, context: arguments.accountContext, exportedLink: invitation, lastPeers: [], viewType: .singleItem, mode: .short, menuItems: { + + var items:[ContextMenuItem] = [] + + items.append(ContextMenuItem(L10n.exportedInvitationContextCopy, handler: { + arguments.copyLink(invitation.link) + })) + + if !invitation.isRevoked { + if !invitation.isExpired { + items.append(ContextMenuItem(L10n.manageLinksContextShare, handler: { + arguments.shareLink(invitation.link) + })) + } + if !invitation.isPermanent { + items.append(ContextMenuItem(L10n.manageLinksContextEdit, handler: { + arguments.editLink(invitation) + })) + } + + if admin?.isBot == true { + + } else { + items.append(ContextMenuItem(L10n.manageLinksContextRevoke, handler: { + arguments.revokeLink(invitation) + })) + } + } + return .single(items) + }, share: arguments.shareLink, copyLink: arguments.copyLink) + })) + + let dateFormatter = DateFormatter() + dateFormatter.locale = appAppearance.locale + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .short + + + if let admin = admin { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.exportedInvitationLinkCreatedBy), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_admin(admin.id), equatable: InputDataEquatable(PeerEquatable(admin)), comparable: nil, item: { initialSize, stableId in + return ShortPeerRowItem(initialSize, peer: admin, account: arguments.accountContext.account, stableId: stableId, height: 48, photoSize: NSMakeSize(36, 36), status: dateFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(invitation.date))), inset: NSEdgeInsetsMake(0, 30, 0, 30), viewType: .singleItem) + })) + } + + let importers = state.importers.filter { $0.peer.peer != nil } + + if !importers.isEmpty { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.exportedInvitationPeopleJoinedCountable(Int(state.count))), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + for importer in state.importers { + struct Tuple : Equatable { + let importer: PeerInvitationImportersState.Importer + let viewType: GeneralViewType + } + + let tuple = Tuple(importer: importer, viewType: bestGeneralViewType(state.importers, for: importer)) + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_peer(importer.peer.peerId), equatable: InputDataEquatable(tuple), comparable: nil, item: { initialSize, stableId in + return ShortPeerRowItem(initialSize, peer: tuple.importer.peer.peer!, account: arguments.accountContext.account, stableId: stableId, height: 48, photoSize: NSMakeSize(36, 36), status: dateFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(importer.date))), inset: NSEdgeInsetsMake(0, 30, 0, 30), viewType: tuple.viewType, action: { + arguments.openProfile(tuple.importer.peer.peerId) + }, contextMenuItems: { + let items = [ContextMenuItem(L10n.exportedInvitationContextOpenProfile, handler: { + arguments.openProfile(tuple.importer.peer.peerId) + })] + + return .single(items) + }) + })) + } + } + + if state.count == 0, !invitation.isExpired, !invitation.isRevoked, let usageCount = invitation.usageLimit, invitation.count == nil { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("_id_join_count"), equatable: nil, comparable: nil, item: { initialSize, stableId in + return GeneralBlockTextRowItem(initialSize, stableId: stableId, viewType: .singleItem, text: L10n.inviteLinkEmptyJoinDescCountable(Int(usageCount)), font: .normal(.text)) + })) + } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func ExportedInvitationController(invitation: ExportedInvitation, peerId: PeerId, accountContext: AccountContext, manager: InviteLinkPeerManager, context: PeerInvitationImportersContext) -> InputDataModalController { + + + var getController:(()->InputDataController?)? = nil + var getModalController:(()->InputDataModalController?)? = nil + + let arguments = ExportInvitationArguments(context: context, accountContext: accountContext, copyLink: { link in + getController?()?.show(toaster: ControllerToaster(text: L10n.shareLinkCopied)) + copyToClipboard(link) + }, shareLink: { link in + showModal(with: ShareModalController(ShareLinkObject(accountContext, link: link)), for: accountContext.window) + }, openProfile: { peerId in + getModalController?()?.close() + accountContext.sharedContext.bindings.rootNavigation().push(PeerInfoController(context: accountContext, peerId: peerId)) + }, revokeLink: { [weak manager] link in + confirm(for: accountContext.window, header: L10n.channelRevokeLinkConfirmHeader, information: L10n.channelRevokeLinkConfirmText, okTitle: L10n.channelRevokeLinkConfirmOK, cancelTitle: L10n.modalCancel, successHandler: { _ in + if let manager = manager { + _ = showModalProgress(signal: manager.revokePeerExportedInvitation(link: link), for: accountContext.window).start() + getModalController?()?.close() + } + }) + }, editLink: { [weak manager] link in + getModalController?()?.close() + showModal(with: ClosureInviteLinkController(context: accountContext, peerId: peerId, mode: .edit(link), save: { [weak manager] updated in + let signal = manager?.editPeerExportedInvitation(link: link, expireDate: updated.date == .max ? 0 : updated.date + Int32(Date().timeIntervalSince1970), usageLimit: updated.count == .max ? 0 : updated.count) + if let signal = signal { + _ = showModalProgress(signal: signal, for: accountContext.window).start() + } + }), for: accountContext.window) + }) + + let dataSignal = combineLatest(queue: prepareQueue, context.state, accountContext.account.postbox.transaction { $0.getPeer(invitation.adminId) }) |> deliverOnPrepareQueue |> map { state, admin in + return entries(state, admin: admin, invitation: invitation, arguments: arguments) + } |> map { entries in + return InputDataSignalValue(entries: entries) + } + + + let controller = InputDataController(dataSignal: dataSignal, title: L10n.exportedInvitationTitle) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { + getModalController?()?.close() + }) + + let dateFormatter = DateFormatter() + dateFormatter.locale = appAppearance.locale + dateFormatter.dateStyle = .short + dateFormatter.timeStyle = .short + + + let getSubtitle:()->String? = { + var subtitle: String? = nil + if invitation.isRevoked { + subtitle = L10n.exportedInvitationStatusRevoked + } else { + if let expireDate = invitation.expireDate { + if expireDate > Int32(Date().timeIntervalSince1970) { + let left = Int(expireDate) - Int(Date().timeIntervalSince1970) + if left <= Int(Int32.secondsInDay) { + let minutes = left / 60 % 60 + let seconds = left % 60 + let hours = left / 60 / 60 + let string = String(format: "%@:%@:%@", hours < 10 ? "0\(hours)" : "\(hours)", minutes < 10 ? "0\(minutes)" : "\(minutes)", seconds < 10 ? "0\(seconds)" : "\(seconds)") + subtitle = L10n.inviteLinkStickerTimeLeft(string) + } else { + subtitle = L10n.inviteLinkStickerTimeLeft(autoremoveLocalized(left)) + } + } else { + subtitle = L10n.exportedInvitationStatusExpired + } + } + } + return subtitle + } + + + + controller.centerModalHeader = ModalHeaderData(title: L10n.exportedInvitationTitle, subtitle: getSubtitle()) + + getController = { [weak controller] in + return controller + } + + controller.updateDatas = { data in + + return .none + } + + let modalInteractions = ModalInteractions(acceptTitle: L10n.exportedInvitationDone, accept: { [weak controller] in + controller?.validateInputValues() + }, drawBorder: true, singleButton: true) + + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions, closeHandler: { f in + f() + }, size: NSMakeSize(340, 350)) + + getModalController = { [weak modalController] in + return modalController + } + + controller.validateData = { data in + return .success(.custom { + getModalController?()?.close() + }) + } + + controller.didLoaded = { [weak context] controller, _ in + controller.tableView.setScrollHandler { [weak context] position in + switch position.direction { + case .bottom: + context?.loadMore() + default: + break + } + } + } + + let timer = SwiftSignalKit.Timer.init(timeout: 1, repeat: true, completion: { [weak modalController, weak controller] in + if let modalController = modalController { + controller?.centerModalHeader = ModalHeaderData(title: L10n.exportedInvitationTitle, subtitle: getSubtitle()) + modalController.updateLocalizationAndTheme(theme: theme) + } + }, queue: .mainQueue()) + + timer.start() + + controller.contextOject = timer + + return modalController +} diff --git a/Telegram-Mac/ExportedInvitationRowItem.swift b/Telegram-Mac/ExportedInvitationRowItem.swift new file mode 100644 index 0000000000..355b804a19 --- /dev/null +++ b/Telegram-Mac/ExportedInvitationRowItem.swift @@ -0,0 +1,416 @@ +// +// ExportedInvitationRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 13.01.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +import Postbox +import TelegramCore +import SwiftSignalKit + + +private func generate(_ color: NSColor) -> CGImage { + return generateImage(NSMakeSize(40 / System.backingScale, 40 / System.backingScale), contextGenerator: { size, ctx in + let rect: NSRect = .init(origin: .zero, size: size) + ctx.clear(rect) + + ctx.setFillColor(color.cgColor) + ctx.fillEllipse(in: rect) + + let image = NSImage(named: "Icon_ChatActionsActive")!.precomposed() + + ctx.clip(to: rect, mask: image) + ctx.clear(rect) + + + }, scale: System.backingScale)! +} + +private var menuIcon: CGImage { + return generate(theme.colors.grayIcon.darker()) +} +private var menuIconActive: CGImage { + return generate(theme.colors.grayIcon.darker().highlighted) +} + +class ExportedInvitationRowItem: GeneralRowItem { + + enum Mode { + case normal + case short + } + + fileprivate let context: AccountContext + fileprivate let exportedLink: ExportedInvitation? + fileprivate let linkTextLayout: TextViewLayout + private let _menuItems: ()->Signal<[ContextMenuItem], NoError> + fileprivate let shareLink:(String)->Void + fileprivate let usageTextLayout: TextViewLayout + fileprivate let lastPeers: [RenderedPeer] + fileprivate let mode: Mode + fileprivate let open:(ExportedInvitation)->Void + fileprivate let copyLink:(String)->Void + fileprivate let publicAddress: String? + init(_ initialSize: NSSize, stableId: AnyHashable, context: AccountContext, exportedLink: ExportedInvitation?, publicAddress: String? = nil, lastPeers: [RenderedPeer], viewType: GeneralViewType, mode: Mode = .normal, menuItems: @escaping()->Signal<[ContextMenuItem], NoError>, share: @escaping(String)->Void, open: @escaping(ExportedInvitation)->Void = { _ in }, copyLink: @escaping(String)->Void = { _ in }) { + self.context = context + self.exportedLink = exportedLink + self._menuItems = menuItems + self.lastPeers = lastPeers + self.publicAddress = publicAddress + self.shareLink = share + self.open = open + self.mode = enableBetaFeatures ? mode : .short + self.copyLink = copyLink + let text: String + let color: NSColor + let usageText: String + let usageColor: NSColor + if let exportedLink = exportedLink { + text = exportedLink.link.replacingOccurrences(of: "https://", with: "") + color = theme.colors.text + if let count = exportedLink.count { + usageText = L10n.inviteLinkJoinedCountable(Int(count)) + if count > 0 { + usageColor = theme.colors.link + } else { + usageColor = theme.colors.grayText + } + } else { + usageText = L10n.inviteLinkJoinedZero + usageColor = theme.colors.grayText + } + } else if let publicAddress = publicAddress { + text = "t.me/\(publicAddress)" + color = theme.colors.text + usageText = L10n.inviteLinkJoinedZero + usageColor = theme.colors.grayText + } else { + text = L10n.channelVisibilityLoading + color = theme.colors.grayText + usageText = L10n.inviteLinkJoinedZero + usageColor = theme.colors.grayText + } + + linkTextLayout = TextViewLayout(.initialize(string: text, color: color, font: .normal(.text)), truncationType: .start, alignment: .center) + + + usageTextLayout = TextViewLayout(.initialize(string: usageText, color: usageColor, font: .normal(.text))) + + super.init(initialSize, stableId: stableId, viewType: viewType) + } + + override var height: CGFloat { + var height: CGFloat = viewType.innerInset.top + 40 + viewType.innerInset.top + + switch mode { + case .normal: + if let link = exportedLink, !link.isExpired && !link.isRevoked { + height += 40 + viewType.innerInset.top + } else if exportedLink == nil && publicAddress == nil { + height += 40 + } + height += 30 + viewType.innerInset.bottom + if exportedLink == nil { + height += viewType.innerInset.bottom + } + case .short: + break + } + + return height + } + + var copyItem: String? { + if let exportedLink = exportedLink { + if !exportedLink.isExpired && !exportedLink.isRevoked { + return exportedLink.link + } else { + return nil + } + } else if let publicAddress = publicAddress { + return publicAddress + } else { + return nil + } + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + let result = super.makeSize(width, oldWidth: oldWidth) + + linkTextLayout.measure(width: blockWidth - viewType.innerInset.left * 2 + viewType.innerInset.right * 2 - 50) + usageTextLayout.measure(width: blockWidth - viewType.innerInset.left - viewType.innerInset.right) + return result + } + + override func menuItems(in location: NSPoint) -> Signal<[ContextMenuItem], NoError> { + return _menuItems() + } + + override func viewClass() -> AnyClass { + return ExportedInvitationRowView.self + } +} + + +private final class ExportedInvitationRowView : GeneralContainableRowView { + + + struct Avatar : Comparable, Identifiable { + static func < (lhs: Avatar, rhs: Avatar) -> Bool { + return lhs.index < rhs.index + } + var stableId: PeerId { + return peer.id + } + static func == (lhs: Avatar, rhs: Avatar) -> Bool { + if lhs.index != rhs.index { + return false + } + if !lhs.peer.isEqual(rhs.peer) { + return false + } + return true + } + + let peer: Peer + let index: Int + } + + + private let linkContainer: Control = Control() + private let linkView: TextView = TextView() + private let share: TitleButton = TitleButton() + private let copy: TitleButton = TitleButton() + private let actions: ImageButton = ImageButton() + private let usageTextView = TextView() + private let usageContainer = Control() + + + private var topPeers: [Avatar] = [] + private var avatars:[AvatarContentView] = [] + private let avatarsContainer = View(frame: NSMakeRect(0, 0, 25 * 3 + 10, 38)) + + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(linkContainer) + linkContainer.layer?.cornerRadius = 10 + linkContainer.addSubview(linkView) + addSubview(share) + share.layer?.cornerRadius = 10 + share._thatFit = true + + addSubview(copy) + copy.layer?.cornerRadius = 10 + copy._thatFit = true + + linkView.userInteractionEnabled = false + linkView.isSelectable = false + + + linkContainer.addSubview(actions) + + addSubview(usageContainer) + + usageTextView.userInteractionEnabled = false + usageTextView.isSelectable = false + usageTextView.isEventLess = true + avatarsContainer.isEventLess = true + + + usageContainer.addSubview(usageTextView) + + usageContainer.addSubview(avatarsContainer) + + linkContainer.scaleOnClick = true + + linkContainer.set(handler: { [weak self] _ in + guard let item = self?.item as? ExportedInvitationRowItem else { + return + } + if let link = item.exportedLink { + item.copyLink(link.link) + } + }, for: .Click) + + actions.set(handler: { [weak self] control in + guard let event = NSApp.currentEvent else { + return + } + self?.showContextMenu(event) + }, for: .Down) + + share.set(handler: { [weak self] _ in + guard let item = self?.item as? ExportedInvitationRowItem else { + return + } + if let link = item.copyItem { + item.shareLink(link) + } + }, for: .Click) + + copy.set(handler: { [weak self] _ in + guard let item = self?.item as? ExportedInvitationRowItem else { + return + } + if let link = item.copyItem { + item.copyLink(link) + } + }, for: .Click) + + usageContainer.set(handler: { [weak self] _ in + guard let item = self?.item as? ExportedInvitationRowItem else { + return + } + if let exportedLink = item.exportedLink { + item.open(exportedLink) + } + }, for: .Click) + } + + override func layout() { + super.layout() + guard let item = item as? ExportedInvitationRowItem else { + return + } + + let innerBlockSize = item.blockWidth - item.viewType.innerInset.left - item.viewType.innerInset.right + + linkContainer.frame = NSMakeRect(item.viewType.innerInset.left, item.viewType.innerInset.top, innerBlockSize, 40) + linkView.centerY(x: item.viewType.innerInset.left) + + copy.frame = NSMakeRect(item.viewType.innerInset.left, linkContainer.frame.maxY + item.viewType.innerInset.top, innerBlockSize / 2 - 10, 40) + share.frame = NSMakeRect(copy.frame.maxX + 20, linkContainer.frame.maxY + item.viewType.innerInset.top, innerBlockSize / 2 - 10, 40) + + actions.centerY(x: linkContainer.frame.width - actions.frame.width - item.viewType.innerInset.right) + + usageContainer.frame = NSMakeRect(item.viewType.innerInset.left, share.frame.maxY + item.viewType.innerInset.top, innerBlockSize, 30) + + let avatarSize: CGFloat = avatarsContainer.subviews.map { $0.frame.maxX }.max() ?? 0 + + if avatarSize > 0 { + usageTextView.centerY(x: floorToScreenPixels(backingScaleFactor, (frame.width - usageTextView.frame.width - avatarSize - 5) / 2)) + avatarsContainer.centerY(x: usageTextView.frame.minX - avatarSize - 5) + } else { + usageTextView.center() + } + } + + override func updateColors() { + super.updateColors() + + guard let item = item as? ExportedInvitationRowItem else { + return + } + + + linkContainer.backgroundColor = theme.colors.grayBackground + linkView.backgroundColor = theme.colors.grayBackground + share.set(background: theme.colors.accent, for: .Normal) + share.set(background: theme.colors.accent.highlighted, for: .Highlight) + share.set(color: theme.colors.underSelectedColor, for: .Normal) + copy.set(background: theme.colors.accent, for: .Normal) + copy.set(background: theme.colors.accent.highlighted, for: .Highlight) + copy.set(color: theme.colors.underSelectedColor, for: .Normal) + actions.set(image: menuIcon, for: .Normal) + actions.set(image: menuIconActive, for: .Highlight) + actions.sizeToFit() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? ExportedInvitationRowItem else { + return + } + + let duration: Double = 0.4 + let timingFunction: CAMediaTimingFunctionName = .spring + + var topPeers: [Avatar] = [] + if !item.lastPeers.isEmpty { + var index:Int = 0 + for participant in item.lastPeers { + if let peer = participant.peer { + topPeers.append(Avatar(peer: peer, index: index)) + index += 1 + } + } + } + + let (removed, inserted, updated) = mergeListsStableWithUpdates(leftList: self.topPeers, rightList: topPeers) + + let avatarSize = NSMakeSize(38, 38) + + for removed in removed.reversed() { + let control = avatars.remove(at: removed) + let peer = self.topPeers[removed] + let haveNext = topPeers.contains(where: { $0.stableId == peer.stableId }) + control.updateLayout(size: avatarSize - NSMakeSize(8, 8), isClipped: false, animated: animated) + control.layer?.opacity = 0 + if animated && !haveNext { + control.layer?.animateAlpha(from: 1, to: 0, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { [weak control] _ in + control?.removeFromSuperview() + }) + control.layer?.animateScaleSpring(from: 1.0, to: 0.2, duration: duration, bounce: false) + } else { + control.removeFromSuperview() + } + } + for inserted in inserted { + let control = AvatarContentView(context: item.context, peer: inserted.1.peer, message: nil, synchronousLoad: false, size: avatarSize, inset: 6) + control.updateLayout(size: avatarSize - NSMakeSize(8, 8), isClipped: inserted.0 != 0, animated: animated) + control.userInteractionEnabled = false + control.setFrameSize(avatarSize) + control.setFrameOrigin(NSMakePoint(CGFloat(inserted.0) * (avatarSize.width - 14), 0)) + avatars.insert(control, at: inserted.0) + avatarsContainer.subviews.insert(control, at: inserted.0) + if animated { + if let index = inserted.2 { + control.layer?.animatePosition(from: NSMakePoint(CGFloat(index) * (avatarSize.width - 14), 0), to: control.frame.origin, timingFunction: timingFunction) + } else { + control.layer?.animateAlpha(from: 0, to: 1, duration: duration, timingFunction: timingFunction) + control.layer?.animateScaleSpring(from: 0.2, to: 1.0, duration: duration, bounce: false) + } + } + } + for updated in updated { + let control = avatars[updated.0] + control.updateLayout(size: avatarSize - NSMakeSize(8, 8), isClipped: updated.0 != 0, animated: animated) + let updatedPoint = NSMakePoint(CGFloat(updated.0) * (avatarSize.width - 14), 0) + if animated { + control.layer?.animatePosition(from: control.frame.origin - updatedPoint, to: .zero, duration: duration, timingFunction: timingFunction, additive: true) + } + control.setFrameOrigin(updatedPoint) + } + var index: CGFloat = 10 + for control in avatarsContainer.subviews.compactMap({ $0 as? AvatarContentView }) { + control.layer?.zPosition = index + index -= 1 + } + + share.set(font: .medium(.text), for: .Normal) + share.set(text: L10n.manageLinksContextShare, for: .Normal) + share.userInteractionEnabled = item.copyItem != nil + + + + copy.set(font: .medium(.text), for: .Normal) + copy.set(text: L10n.manageLinksContextCopy, for: .Normal) + copy.userInteractionEnabled = item.copyItem != nil + + linkView.update(item.linkTextLayout) + + usageTextView.update(item.usageTextLayout) + needsLayout = true + } +} diff --git a/Telegram-Mac/Extensions.swift b/Telegram-Mac/Extensions.swift index 91aef881a9..c5446072ee 100644 --- a/Telegram-Mac/Extensions.swift +++ b/Telegram-Mac/Extensions.swift @@ -8,10 +8,11 @@ import Foundation import TGUIKit -import SwiftSignalKitMac -import TelegramCoreMac -import PostboxMac +import SwiftSignalKit +import TelegramCore +import Postbox +import LocalAuthentication extension Message { var chatStableId:ChatHistoryEntryId { @@ -19,17 +20,16 @@ extension Message { } } -extension MessageHistoryHole { - - var chatStableId:ChatHistoryEntryId { - return ChatHistoryEntryId.hole(self) - } +func makeNewDateFormatter() -> DateFormatter { + let formatter = DateFormatter() + formatter.locale = appAppearance.locale + return formatter } extension NSMutableAttributedString { - func detectLinks(type:ParsingType, account:Account? = nil, color:NSColor = .link, openInfo:((PeerId, Bool, MessageId?, ChatInitialAction?)->Void)? = nil, hashtag:((String)->Void)? = nil, command:((String)->Void)? = nil, applyProxy:((ProxySettings)->Void)? = nil) -> Void { - let things = ObjcUtils.textCheckingResults(forText: self.string, highlightMentionsAndTags: type.contains(.Mentions) || type.contains(.Hashtags), highlightCommands: type.contains(.Commands)) + func detectLinks(type:ParsingType, onlyInApp: Bool = false, context:AccountContext? = nil, color:NSColor = theme.colors.link, openInfo:((PeerId, Bool, MessageId?, ChatInitialAction?)->Void)? = nil, hashtag:((String)->Void)? = nil, command:((String)->Void)? = nil, applyProxy:((ProxyServerSettings)->Void)? = nil, dotInMention: Bool = false) -> Void { + let things = ObjcUtils.textCheckingResults(forText: self.string, highlightMentions: type.contains(.Mentions), highlightTags: type.contains(.Hashtags), highlightCommands: type.contains(.Commands), dotInMention: dotInMention) self.beginEditing() @@ -38,15 +38,45 @@ extension NSMutableAttributedString { let range = (value as! NSValue).rangeValue + var addLinkAttrs: Bool = false + if range.location != NSNotFound { let sublink = (self.string as NSString).substring(with: range) - if let account = account { - self.addAttribute(NSAttributedStringKey.link, value: inApp(for: sublink as NSString, account: account, openInfo: openInfo, hashtag: hashtag, command: command, applyProxy: applyProxy), range: range) + if let context = context { + let link = inApp(for: sublink as NSString, context: context, openInfo: openInfo, hashtag: hashtag, command: command, applyProxy: applyProxy) + if onlyInApp { + switch link { + case let .external(link, _): + let allowed = ["telegram.org", "telegram.dog", "telegram.me", "telegra.ph", "telesco.pe"] + if let url = URL(string: link) { + if let host = url.host, allowed.contains(host) { + self.addAttribute(NSAttributedString.Key.link, value: inAppLink.external(link: sublink, false), range: range) + addLinkAttrs = true + } else if allowed.contains(link) { + self.addAttribute(NSAttributedString.Key.link, value: inAppLink.external(link: sublink, false), range: range) + addLinkAttrs = true + } + } else { + continue + } + default: + self.addAttribute(NSAttributedString.Key.link, value: link, range: range) + addLinkAttrs = true + } + } else { + self.addAttribute(NSAttributedString.Key.link, value: link, range: range) + addLinkAttrs = true + } } else { - self.addAttribute(NSAttributedStringKey.link, value: inAppLink.external(link: sublink, false), range: range) + if !onlyInApp { + self.addAttribute(NSAttributedString.Key.link, value: inAppLink.external(link: sublink, false), range: range) + addLinkAttrs = true + } + } + if addLinkAttrs { + self.addAttribute(NSAttributedString.Key.foregroundColor, value: color, range: range) + self.addAttribute(.cursor, value: NSCursor.pointingHand, range: range) } - self.addAttribute(NSAttributedStringKey.foregroundColor, value: theme.colors.link, range: range) - self.addAttribute(.cursor, value: NSCursor.pointingHand, range: range) } } @@ -64,8 +94,8 @@ extension NSMutableAttributedString { } return false } - - let symbols:[(from: String, to: String)] = [(from: "✌", to: "✌️"), (from: "☺", to: "☺️"), (from: "☝", to: "☝️"), (from: "1⃣", to: "1️⃣"), (from: "2⃣", to: "2️⃣"), (from: "3⃣", to: "3️⃣"), (from: "4⃣", to: "4️⃣"), (from: "5⃣", to: "5️⃣"), (from: "6⃣", to: "6️⃣"), (from: "7⃣", to: "7️⃣"), (from: "8⃣", to: "8️⃣"), (from: "9⃣", to: "9️⃣"), (from: "0⃣", to: "0️⃣"), (from: "❤", to: "❤️"), (from: "☁", to: "☁️"), (from: "ℹ", to: "ℹ️"), (from: "✍", to: "✍️")] + + let symbols:[(from: String, to: String)] = [(from: "✌", to: "✌️"), (from: "☺", to: "☺️"), (from: "☝", to: "☝️"), (from: "1⃣", to: "1️⃣"), (from: "2⃣", to: "2️⃣"), (from: "3⃣", to: "3️⃣"), (from: "4⃣", to: "4️⃣"), (from: "5⃣", to: "5️⃣"), (from: "6⃣", to: "6️⃣"), (from: "7⃣", to: "7️⃣"), (from: "8⃣", to: "8️⃣"), (from: "9⃣", to: "9️⃣"), (from: "0⃣", to: "0️⃣"), (from: "❤", to: "❤️"), (from: "☁", to: "☁️"), (from: "ℹ", to: "ℹ️"), (from: "✍", to: "✍️"), (from: "♥", to: "❤️"), (from: "⁉", to: "⁉️"), (from: "❣", to: "❣️"), (from: "⬅", to: "⬅️"), (from: "◻", to: "◻️"), (from: "➡", to: "➡️"), (from: "◼", to: "◼️")] for symbol in symbols { while changeSymbol(symbol.from, to: symbol.to) { @@ -73,6 +103,8 @@ extension NSMutableAttributedString { } } + // while + // 7️⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣⃣ } @@ -94,73 +126,38 @@ public extension String { str = str.replacingOccurrences(of: "8⃣", with: "8️⃣") str = str.replacingOccurrences(of: "9⃣", with: "9️⃣") str = str.replacingOccurrences(of: "0⃣", with: "0️⃣") + str = str.replacingOccurrences(of: "#⃣", with: "#️⃣") str = str.replacingOccurrences(of: "❤", with: "❤️") + str = str.replacingOccurrences(of: "♥", with: "❤️") str = str.replacingOccurrences(of: "☁", with: "☁️") str = str.replacingOccurrences(of: "✍", with: "✍️") + str = str.replacingOccurrences(of: "⁉", with: "⁉️") + str = str.replacingOccurrences(of: "❣", with: "❣️") + str = str.replacingOccurrences(of: "⬅", with: "⬅️") + str = str.replacingOccurrences(of: "◻", with: "◻️") + str = str.replacingOccurrences(of: "◼", with: "◼️") + str = str.replacingOccurrences(of: "➡", with: "➡️") + str = str.replacingOccurrences(of: "⚰", with: "⚰️") + str = str.replacingOccurrences(of: "⚡", with: "⚡️") + str = str.replacingOccurrences(of: "⛄", with: "⛄️") + + + return str } static func stringForShortCallDurationSeconds(for seconds: Int32) -> String { if seconds < 60 { - return tr(.callShortSecondsCountable(Int(seconds))) + return tr(L10n.callShortSecondsCountable(Int(seconds))) } else { let number = Int(seconds) / 60 - return tr(.callShortMinutesCountable(number)) + return tr(L10n.callShortMinutesCountable(number)) } } - var trimmed:String { - - var string:String = self - while !string.isEmpty, let index = string.rangeOfCharacter(from: NSCharacterSet.whitespacesAndNewlines), index.lowerBound == string.startIndex { - string = String(string[index.upperBound.. 2 { - copy = String(copy[.."] = "😆" dictionary[":->"] = "😆" dictionary["XD"] = "😆" + dictionary["xD"] = "😂" dictionary["O:)"] = "😇" dictionary["3-)"] = "😌" dictionary[":P"] = "😛" @@ -1678,7 +1703,7 @@ func ==(lhs: EmojiClue, rhs: EmojiClue) -> Bool { return lhs.emoji == rhs.emoji && lhs.label == rhs.label && lhs.replacement == rhs.replacement } -func searchEmojiClue(query: String, postbox: Postbox) -> Signal<[EmojiClue], Void> { +func searchEmojiClue(query: String, postbox: Postbox) -> Signal<[EmojiClue], NoError> { return recentUsedEmoji(postbox: postbox) |> deliverOn(resourcesQueue) |> map { recent in @@ -1700,6 +1725,11 @@ func searchEmojiClue(query: String, postbox: Postbox) -> Signal<[EmojiClue], Voi } } +func randomInt32() -> Int32 { + let uRandom = arc4random() + return Int32(bitPattern: uRandom) +} + func + (left: Dictionary, right: Dictionary) -> Dictionary @@ -1714,11 +1744,67 @@ func + (left: Dictionary, right: Dictionary) return map } +extension Array { + func subarray(with range: NSRange) -> Array { + return Array(self[range.min ..< range.max]) + } + mutating func move(at oldIndex: Int, to newIndex: Int) { + self.insert(self.remove(at: oldIndex), at: newIndex) + } +} +extension Array { + func chunks(_ chunkSize: Int) -> [[Element]] { + return stride(from: 0, to: self.count, by: chunkSize).map { + Array(self[$0..(with selectKey: (Element) -> Key) -> [Key:Element] { + var dict = [Key:Element]() + for element in self { + dict[selectKey(element)] = element + } + return dict + } +} + func copyToClipboard(_ string:String) { + NSPasteboard.general.clearContents() NSPasteboard.general.declareTypes([.string], owner: nil) NSPasteboard.general.setString(string, forType: .string) } +extension LAPolicy { + static var applicationPolicy: LAPolicy { + if #available(OSX 10.12.2, *) { + #if DEBUG + return .deviceOwnerAuthentication + #endif + return .deviceOwnerAuthenticationWithBiometrics + } else { + return .deviceOwnerAuthentication + } + } +} + +extension LAContext { + var canUseBiometric: Bool { + if #available(OSX 10.12.2, *) { + #if DEBUG + return true + #endif + if canEvaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, error: nil) { + return true + } else { + return false + } + } else { + #if DEBUG + return true + #endif + return false + } + } +} extension CVImageBuffer { @@ -1778,14 +1864,30 @@ extension CGImage { let thumbnailImage: CGImage = self - let thumbnailContextSize = thumbnailImage.size - let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) + let thumbnailContextSize = thumbnailImage.size.multipliedByScreenScale() + + let thumbnailContextSmallSize = thumbnailContextSize.aspectFitted(NSMakeSize(50, 50)) + + let thumbnailContext = DrawingContext(size: thumbnailContextSmallSize, scale: 1.0) + + + thumbnailContext.withContext { ctx in ctx.interpolationQuality = .none - ctx.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) } - telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + + telegramFastBlurMore(Int32(thumbnailContextSmallSize.width), Int32(thumbnailContextSmallSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + + let thumb = DrawingContext(size: thumbnailContextSize, scale: 1.0) + + + thumb.withContext { ctx in + ctx.interpolationQuality = .none + ctx.draw(thumbnailContext.generateImage()!, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) + } + // telegramFastBlurMore(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumb.bytesPerRow), thumb.bytes) + return thumbnailContext.generateImage()! } @@ -1808,3 +1910,739 @@ func synced(_ lock: Any, closure: ()->Void) { closure() objc_sync_exit(lock) } + + +extension NSData { + + var hexString: String { + let buf = bytes.assumingMemoryBound(to: UInt8.self) + let charA = UInt8(UnicodeScalar("a").value) + let char0 = UInt8(UnicodeScalar("0").value) + + func itoh(_ value: UInt8) -> UInt8 { + return (value > 9) ? (charA + value - 10) : (char0 + value) + } + + let hexLen = length * 2 + let ptr = UnsafeMutablePointer.allocate(capacity: hexLen) + + for i in 0 ..< length { + ptr[i*2] = itoh((buf[i] >> 4) & 0xF) + ptr[i*2+1] = itoh(buf[i] & 0xF) + } + + return String(bytesNoCopy: ptr, length: hexLen, encoding: .utf8, freeWhenDone: true)! + } +} + +extension NSTextView { + + var selectedRangeRect: NSRect { + + var rect: NSRect = firstRect(forCharacterRange: selectedRange(), actualRange: nil) + + + + + if let window = window { + //rect = window.convertFromScreen(rect) + + var textViewBounds: NSRect = convert(bounds, to: nil) + textViewBounds = window.convertToScreen(textViewBounds) + + rect.origin.x -= textViewBounds.origin.x; + rect.origin.y -= (textViewBounds.origin.y ); + } + +// if let superview = superview { +// rect = superview.convert(rect, from: nil) +// } + // rect.origin.y += 10 + return rect + } + + + +} + +extension CGContext { + func round(_ size:NSSize,_ cornerRadius:CGFloat = .cornerRadius, positionFlags: LayoutPositionFlags? = nil) { + let minx:CGFloat = 0, midx = size.width/2.0, maxx = size.width + let miny:CGFloat = 0, midy = size.height/2.0, maxy = size.height + + self.move(to: NSMakePoint(minx, midy)) + + var topLeftRadius: CGFloat = cornerRadius + var bottomLeftRadius: CGFloat = cornerRadius + var topRightRadius: CGFloat = cornerRadius + var bottomRightRadius: CGFloat = cornerRadius + + + if let positionFlags = positionFlags { + if positionFlags.contains(.top) && positionFlags.contains(.left) { + topLeftRadius = topLeftRadius * 3 + 2 + } + if positionFlags.contains(.top) && positionFlags.contains(.right) { + topRightRadius = topRightRadius * 3 + 2 + } + if positionFlags.contains(.bottom) && positionFlags.contains(.left) { + bottomLeftRadius = bottomLeftRadius * 3 + 2 + } + if positionFlags.contains(.bottom) && positionFlags.contains(.right) { + bottomRightRadius = bottomRightRadius * 3 + 2 + } + } + + self.addArc(tangent1End: NSMakePoint(minx, miny), tangent2End: NSMakePoint(midx, miny), radius: topLeftRadius) + self.addArc(tangent1End: NSMakePoint(maxx, miny), tangent2End: NSMakePoint(maxx, midy), radius: topRightRadius) + self.addArc(tangent1End: NSMakePoint(maxx, maxy), tangent2End: NSMakePoint(midx, maxy), radius: bottomRightRadius) + self.addArc(tangent1End: NSMakePoint(minx, maxy), tangent2End: NSMakePoint(minx, midy), radius: bottomLeftRadius) + + self.closePath() + self.clip() + + } +} + + + + + + +extension NSControl.ImagePosition { + + var touchBarDefaultSize: CGFloat { + switch self { + case .imageOnly: + return 56.0 + case .imageLeading: + return 175.0 + default: + return 150.0 + } + } + +} + +extension NSButton { + + func addTouchBarButtonWidthConstraint() { + switch self.imagePosition { + case .imageLeading: + addWidthConstraint(relation: .lessThanOrEqual, + size: self.imagePosition.touchBarDefaultSize) + default: + addWidthConstraint(relation: .equal, + size: self.imagePosition.touchBarDefaultSize) + } + } + +} + + +@available(OSX 10.12.2, *) +extension NSImage { + + // MARK: Project drawables + + + static let defaultBg = NSImage(named: "DefaultBackground")! + + static let shuffling = #imageLiteral(resourceName: "Icon_DFRShuffle").forUI() + static let repeating = #imageLiteral(resourceName: "Icon_DFRRepeat").forUI() + + static let previous = NSImage(named: NSImage.touchBarRewindTemplateName)! + static let next = NSImage(named: NSImage.touchBarFastForwardTemplateName)! + static let play = NSImage(named: NSImage.touchBarPlayTemplateName)! + static let pause = NSImage(named: NSImage.touchBarPauseTemplateName)! + + static let volumeLow = NSImage(named: NSImage.touchBarAudioOutputVolumeLowTemplateName) + static let volumeMedium = NSImage(named: NSImage.touchBarAudioOutputVolumeMediumTemplateName) + static let volumeHigh = NSImage(named: NSImage.touchBarAudioOutputVolumeHighTemplateName) + + static let playhead = NSImage(named: NSImage.touchBarPlayheadTemplateName) + static let playbar = #imageLiteral(resourceName: "Icon_Playbar") + +} + +extension NSImage { + + // MARK: Extended functions + + /** + Returns the NSImage with 'isTemplate' enabled + This lets the UI draw the appropriate color + according to background (e.g. dark in TouchBar) + */ + func forUI() -> NSImage { + self.isTemplate = true + + return self + } + + func edit(size: NSSize? = nil, + editCommand: @escaping (NSImage, NSSize) -> ()) -> NSImage { + let temp = NSImage(size: size ?? self.size) + + guard temp.size.width > 0, temp.size.height > 0 else { return self } + + temp.lockFocus() + editCommand(self, temp.size) + temp.unlockFocus() + + return temp + } + + /** + Resizes NSImage + - parameter newSize: the requested image size + - parameter squareCrop: if true + - returns: self scaled to requested size + */ + func resized(to newSize: CGSize, squareCrop: Bool = true) -> NSImage { + return self.edit(size: newSize) { + image, size in + + var fromRect = NSZeroRect + let inRect = NSMakeRect(0, 0, size.width, size.height) + + if squareCrop, image.size.width != image.size.height { + let minSize = min(image.size.width, image.size.height) + let maxSize = max(image.size.width, image.size.height) + let start = ( maxSize - minSize ) / 2 + + fromRect = NSMakeRect( + image.size.width != minSize ? start : 0, + image.size.height != minSize ? start : 0, + minSize, + minSize + ) + } + + image.draw(in: inRect, + from: fromRect, + operation: .sourceOver, + fraction: 1.0) + } + } + + /** + Changes NSImage alpha + - parameter alpha: the requested alpha value + - returns: self with requested alpha value + */ + func withAlpha(_ alpha: CGFloat) -> NSImage { + return self.edit { + image, size in + image.draw(in: NSMakeRect(0, 0, size.width, size.height), + from: NSZeroRect, + operation: .sourceOver, + fraction: alpha) + } + } + +} + + +extension Window { + var titleView: NSView? { + if let windowView = contentView?.superview { + return ObjcUtils.findElements(byClass: "NSTitlebarContainerView", in: windowView).first + } + return nil + } +} + +func quadraticEaseOut (_ x: T) -> T { + return -x * (x - 2) +} + +extension Date { + var startOfDay: Date { + var calendar = NSCalendar.current + return calendar.startOfDay(for: self) + } + + var startOfDayUTC: Date { + var calendar = NSCalendar.current + calendar.timeZone = TimeZone(abbreviation: "UTC")! + return calendar.startOfDay(for: self) + } + + var endOfDay: Date { + var components = DateComponents() + components.day = 1 + components.second = -1 + var calendar = NSCalendar.current + return calendar.date(byAdding: components, to: startOfDay)! + } + + var startOfMonth: Date { + let components = Calendar.current.dateComponents([.year, .month], from: startOfDay) + return Calendar.current.date(from: components)! + } + + var endOfMonth: Date { + var components = DateComponents() + components.month = 1 + components.second = -1 + return Calendar.current.date(byAdding: components, to: startOfMonth)! + } +} + + + + +public extension NSAttributedString { + + func applyRtf() -> (NSAttributedString, [NSTextAttachment]) { + let string = self.mutableCopy() as! NSMutableAttributedString + + let modified: NSMutableAttributedString = string.trimNewLines.mutableCopy() as! NSMutableAttributedString + + + var index: Int = 1 + while true { + let range = modified.string.nsstring.range(of: "\t0") + if range.location != NSNotFound { + modified.replaceCharacters(in: range, with: "\t\(index)") + index += 1 + } else { + break + } + } + + var attachments:[NSTextAttachment] = [] + + modified.enumerateAttributes(in: modified.range, options: [], using: { attr, range, _ in + if let url = attr[.link] { + var string: String? + if let url = url as? NSURL, let link = url.absoluteString { + string = link + } else if let link = url as? String { + string = link + } + if let string = string { + let tag = TGInputTextTag(uniqueId: arc4random64(), attachment: string, attribute: TGInputTextAttribute(name: NSAttributedString.Key.foregroundColor.rawValue, value: theme.colors.link)) + if let tag = tag { + modified.addAttribute(NSAttributedString.Key(rawValue: TGCustomLinkAttributeName), value: tag, range: range) + } + } + } else if let font = attr[.font] as? NSFont { + let newFont: NSFont + if font.fontDescriptor.symbolicTraits.contains(.bold) && font.fontDescriptor.symbolicTraits.contains(.italic) { + newFont = .boldItalic(theme.fontSize) + } else if font.fontDescriptor.symbolicTraits.contains(.bold) { + newFont = .bold(theme.fontSize) + } else if font.fontDescriptor.symbolicTraits.contains(.italic) { + newFont = .italic(theme.fontSize) + } else if font.fontDescriptor.symbolicTraits.contains(NSFontDescriptor.SymbolicTraits.monoSpace) { + newFont = .code(theme.fontSize) + } else { + newFont = .normal(theme.fontSize) + } + modified.addAttribute(.font, value: newFont, range: range) + } + for key in attr.keys { + switch key { + case .font, .link: + break + case .attachment: + modified.removeAttribute(key, range: range) + attachments.append(attr[key] as! NSTextAttachment) + default: + modified.removeAttribute(key, range: range) + } + } + }) + + return (modified, attachments) //.trimmed + } + + func appendAttributedString(_ string: NSAttributedString, selectedRange: NSRange = NSMakeRange(0, 0)) -> (NSAttributedString, NSRange) { + let inputText = self.mutableCopy() as! NSMutableAttributedString + + + var range: NSRange = NSMakeRange(selectedRange.location + string.string.length, 0); + if selectedRange.upperBound - selectedRange.lowerBound > 0 { + inputText.replaceCharacters(in: NSMakeRange(selectedRange.lowerBound, selectedRange.upperBound - selectedRange.lowerBound), with: string) + } else { + inputText.insert(string, at: selectedRange.lowerBound) + } + return (inputText, range) + } +} + + +extension Date { + + static var kernelBootTimeSecs:Int32 { + var mib = [ CTL_KERN, KERN_BOOTTIME ] + var bootTime = timeval() + var bootTimeSize = MemoryLayout.size + + if 0 != sysctl(&mib, UInt32(mib.count), &bootTime, &bootTimeSize, nil, 0) { + fatalError("Could not get boot time, errno: \(errno)") + } + + return Int32(bootTime.tv_sec) + } + var isToday: Bool { + return CalendarUtils.isSameDate(self, date: Date(), checkDay: true) + } + var isTomorrow: Bool { + return Calendar.current.isDateInTomorrow(self) + } +} + + + + + +private let colorKeyRegex = try? NSRegularExpression(pattern: "\"k\":\\[[\\d\\.]+\\,[\\d\\.]+\\,[\\d\\.]+\\,[\\d\\.]+\\]") + +func transformedWithFitzModifier(data: Data, fitzModifier: EmojiFitzModifier?) -> Data { + if let fitzModifier = fitzModifier, var string = String(data: data, encoding: .utf8) { + let color1: NSColor + let color2: NSColor + let color3: NSColor + let color4: NSColor + + var colors: [NSColor] = [0xf77e41, 0xffb139, 0xffd140, 0xffdf79].map { NSColor(rgb: $0) } + let replacementColors: [NSColor] + switch fitzModifier { + case .type12: + replacementColors = [0xca907a, 0xedc5a5, 0xf7e3c3, 0xfbefd6].map { NSColor(rgb: $0) } + case .type3: + replacementColors = [0xaa7c60, 0xc8a987, 0xddc89f, 0xe6d6b2].map { NSColor(rgb: $0) } + case .type4: + replacementColors = [0x8c6148, 0xad8562, 0xc49e76, 0xd4b188].map { NSColor(rgb: $0) } + case .type5: + replacementColors = [0x6e3c2c, 0x925a34, 0xa16e46, 0xac7a52].map { NSColor(rgb: $0) } + case .type6: + replacementColors = [0x291c12, 0x472a22, 0x573b30, 0x68493c].map { NSColor(rgb: $0) } + } + + func colorToString(_ color: NSColor) -> String { + var r: CGFloat = 0.0 + var g: CGFloat = 0.0 + var b: CGFloat = 0.0 + color.getRed(&r, green: &g, blue: &b, alpha: nil) + return "\"k\":[\(r),\(g),\(b),1]" + } + + func match(_ a: Double, _ b: Double, eps: Double) -> Bool { + return abs(a - b) < eps + } + + var replacements: [(NSTextCheckingResult, String)] = [] + + if let colorKeyRegex = colorKeyRegex { + let results = colorKeyRegex.matches(in: string, range: NSRange(string.startIndex..., in: string)) + for result in results.reversed() { + if let range = Range(result.range, in: string) { + let substring = String(string[range]) + let color = substring[substring.index(string.startIndex, offsetBy: "\"k\":[".count) ..< substring.index(before: substring.endIndex)] + let components = color.split(separator: ",") + if components.count == 4, let r = Double(components[0]), let g = Double(components[1]), let b = Double(components[2]), let a = Double(components[3]) { + if match(a, 1.0, eps: 0.01) { + for i in 0 ..< colors.count { + let color = colors[i] + var cr: CGFloat = 0.0 + var cg: CGFloat = 0.0 + var cb: CGFloat = 0.0 + color.getRed(&cr, green: &cg, blue: &cb, alpha: nil) + if match(r, Double(cr), eps: 0.01) && match(g, Double(cg), eps: 0.01) && match(b, Double(cb), eps: 0.01) { + replacements.append((result, colorToString(replacementColors[i]))) + } + } + } + } + } + } + } + + for (result, text) in replacements { + if let range = Range(result.range, in: string) { + string = string.replacingCharacters(in: range, with: text) + } + } + + return string.data(using: .utf8) ?? data + } else { + return data + } +} + +func applyLottieColor(data: Data, color: NSColor) -> Data { + if var string = String(data: data, encoding: .utf8) { + func colorToString(_ color: NSColor) -> String { + var r: CGFloat = 0.0 + var g: CGFloat = 0.0 + var b: CGFloat = 0.0 + color.getRed(&r, green: &g, blue: &b, alpha: nil) + return "\"k\":[\(r),\(g),\(b),1]" + } + var replacements: [(NSTextCheckingResult, String)] = [] + if let colorKeyRegex = colorKeyRegex { + let results = colorKeyRegex.matches(in: string, range: NSRange(string.startIndex..., in: string)) + for result in results.reversed() { + replacements.append((result, colorToString(color))) + } + } + for (result, text) in replacements { + if let range = Range(result.range, in: string) { + string = string.replacingCharacters(in: range, with: text) + } + } + return string.data(using: .utf8) ?? data + } else { + return data + } +} + + + +extension Double { + + func toString(decimal: Int = 9) -> String { + let value = decimal < 0 ? 0 : decimal + var string = String(format: "%.\(value)f", self) + + while string.last == "0" || string.last == "." { + if string.last == "." { string = String(string.dropLast()); break} + string = String(string.dropLast()) + } + return string + } +} + + +public extension String { + func rightJustified(width: Int, pad: String = " ", truncate: Bool = false) -> String { + guard width > count else { + return truncate ? String(suffix(width)) : self + } + return String(repeating: pad, count: width - count) + self + } + + func leftJustified(width: Int, pad: String = " ", truncate: Bool = false) -> String { + guard width > count else { + return truncate ? String(prefix(width)) : self + } + return self + String(repeating: pad, count: width - count) + } +} + + + +extension CGImage { + var data: Data? { + guard let mutableData = CFDataCreateMutable(nil, 0), + let destination = CGImageDestinationCreateWithData(mutableData, "public.png" as CFString, 1, nil) else { return nil } + CGImageDestinationAddImage(destination, self, nil) + guard CGImageDestinationFinalize(destination) else { return nil } + return mutableData as Data + } +} + +func localizedPsa(_ key: String, type: String, args: [CVarArg] = []) -> String { + let fullKey = key + "." + type + let cloud = translate(key: fullKey, args) + if cloud == fullKey { + return translate(key: key, args) + } else { + return cloud + } +} + +func + (left: CGPoint, right: CGPoint) -> CGPoint { + return CGPoint(x: left.x + right.x, y: left.y + right.y) +} +func - (left: CGPoint, right: CGPoint) -> CGPoint { + return CGPoint(x: left.x - right.x, y: left.y - right.y) +} +func + (left: CGSize, right: CGSize) -> CGSize { + return CGSize(width: left.width + right.width, height: left.height + right.height) +} +func - (left: CGSize, right: CGSize) -> CGSize { + return CGSize(width: left.width - right.width, height: left.height - right.height) +} + + +func freeSystemGigabytes() -> UInt64? { + let attrs = try? FileManager.default.attributesOfFileSystem(forPath: "/") + + if let freeBytes = attrs?[FileAttributeKey.systemFreeSize] as? UInt64 { + return freeBytes / 1073741824 + } + return nil +} + +func systemSizeGigabytes() -> UInt64? { + let attrs = try? FileManager.default.attributesOfFileSystem(forPath: "/") + + if let freeBytes = attrs?[FileAttributeKey.systemSize] as? UInt64 { + return freeBytes / 1000000000 + } + return nil +} + +func showOutOfMemoryWarning(_ window: Window, freeSpace: UInt64, context: AccountContext) { + let alert: NSAlert = NSAlert() + alert.addButton(withTitle: L10n.systemMemoryWarningOK) + alert.addButton(withTitle: L10n.systemMemoryWarningDataAndStorage) + // alert.addButton(withTitle: L10n.systemMemoryWarningManageSystemStorage) + + alert.messageText = L10n.systemMemoryWarningHeader + alert.informativeText = L10n.systemMemoryWarningText(freeSpace == 0 ? L10n.systemMemoryWarningLessThen1GB : L10n.systemMemoryWarningFreeSpace(Int(freeSpace))) + alert.alertStyle = .critical + + alert.beginSheetModal(for: window, completionHandler: { response in + switch response.rawValue { + case 1000: + break + case 1001: + context.sharedContext.bindings.rootNavigation().push(StorageUsageController(context)) + case 1002: + openSystemSettings(.storage) + default: + break + } + }) +} + + + +extension NSCursor { + static var set_windowResizeNorthWestSouthEastCursor: NSCursor? { + return ObjcUtils.windowResizeNorthWestSouthEastCursor() + } + static var set_windowResizeNorthEastSouthWestCursor: NSCursor? { + return ObjcUtils.windowResizeNorthEastSouthWestCursor() + } +} + +extension NSImage { + var _cgImage: CGImage? { + return self.cgImage(forProposedRect: nil, context: nil, hints: nil) + } +} + + +func truncate(double: Double, places : Int)-> Double +{ + return Double(floor(pow(10.0, Double(places)) * double)/pow(10.0, Double(places))) +} + + + + +struct DateSelectorUtil { + + static var timeIntervals:[TimeInterval?] { + var intervals:[TimeInterval?] = [] + for i in 0 ... 23 { + let current = Double(i) * 60.0 * 60 + intervals.append(current) + // #if DEBUG + for i in 1 ... 59 { + intervals.append(current + Double(i) * 60.0) + } + if i < 23 { + intervals.append(nil) + } + + } + return intervals + } + + static var dayFormatter: DateFormatter { + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: appAppearance.language.languageCode) + //dateFormatter.timeZone = TimeZone(abbreviation: "UTC")! + dateFormatter.dateFormat = "MMM d, yyyy" + return dateFormatter + } + + static var dayFormatterRelative: DateFormatter { + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: appAppearance.language.languageCode) + // dateFormatter.timeZone = TimeZone(abbreviation: "UTC")! + + dateFormatter.dateStyle = .short + dateFormatter.doesRelativeDateFormatting = true + return dateFormatter + } + + static func formatDay(_ date: Date) -> String { + if CalendarUtils.isSameDate(date, date: Date(), checkDay: true) { + return dayFormatterRelative.string(from: date) + } else { + return dayFormatter.string(from: date) + } + } + + static func formatTime(_ date: Date) -> String { + let timeFormatter = DateFormatter() + timeFormatter.timeStyle = .medium + // timeFormatter.timeZone = TimeZone(abbreviation: "UTC")! + return timeFormatter.string(from: date) + } +} + + +extension Int32 { + static var secondsInDay: Int32 { + return 60 * 60 * 24 + } + static var secondsInWeek: Int32 { + return secondsInDay * 7 + } + static var secondsInMonth: Int32 { + return secondsInDay * 31 + } +} + + +func smartTimeleftText( _ left: Int) -> String { + if left <= Int(Int32.secondsInDay) { + let minutes = left / 60 % 60 + let seconds = left % 60 + let hours = left / 60 / 60 + let string = String(format: "%@:%@:%@", hours < 10 ? "0\(hours)" : "\(hours)", minutes < 10 ? "0\(minutes)" : "\(minutes)", seconds < 10 ? "0\(seconds)" : "\(seconds)") + return string + } else { + return autoremoveLocalized(left, roundToCeil: true) + } +} + + +public typealias UIImage = NSImage + +extension NSImage { + + enum Orientation { + case up + case down + } + + convenience init(cgImage: CGImage, scale: CGFloat, orientation: UIImage.Orientation) { + self.init(cgImage: cgImage, size: cgImage.systemSize) + } +} + +public extension DataSizeStringFormatting { + + static var current: DataSizeStringFormatting { + return DataSizeStringFormatting.init(decimalSeparator: NumberFormatter().decimalSeparator, byte: { value in + return (L10n.fileSizeB(value), []) + }, kilobyte: { value in + return (L10n.fileSizeKB(value), []) + }, megabyte: { value in + return (L10n.fileSizeMB(value), []) + }, gigabyte: { value in + return (L10n.fileSizeGB(value), []) + }) + } +} diff --git a/Telegram-Mac/ExternalMusicAlbumArtResources.swift b/Telegram-Mac/ExternalMusicAlbumArtResources.swift new file mode 100644 index 0000000000..536638cda3 --- /dev/null +++ b/Telegram-Mac/ExternalMusicAlbumArtResources.swift @@ -0,0 +1,113 @@ +// +// ExternalMusicAlbumArtResources.swift +// Telegram +// +// Created by Mikhail Filimonov on 29/06/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa + +import Foundation +import TelegramCore + +import SwiftSignalKit +import Postbox + +private func urlEncodedStringFromString(_ string: String) -> String { + var nsString: NSString = string as NSString + if let value = nsString.replacingPercentEscapes(using: String.Encoding.utf8.rawValue) { + nsString = value as NSString + } + + let result = CFURLCreateStringByAddingPercentEscapes(nil, nsString as CFString, nil, "?!@#$^&%*+=,:;'\"`<>()[]{}/\\|~ " as CFString, CFStringConvertNSStringEncodingToEncoding(String.Encoding.utf8.rawValue))! + return result as String +} + +func fetchExternalMusicAlbumArtResource(account: Account, resource: ExternalMusicAlbumArtResource) -> Signal { + return Signal { subscriber in + subscriber.putNext(.reset) + + if resource.performer.isEmpty || resource.performer.lowercased().trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) == "unknown artist" || resource.title.isEmpty { + subscriber.putNext(.dataPart(resourceOffset: 0, data: Data(), range: 0 ..< 0, complete: true)) + subscriber.putCompletion() + return EmptyDisposable + } else { + let excludeWords: [String] = [ + " vs. ", + " vs ", + " versus ", + " ft. ", + " ft ", + " featuring ", + " feat. ", + " feat ", + " presents ", + " pres. ", + " pres ", + " and ", + " & ", + " . " + ] + + var performer = resource.performer + + for word in excludeWords { + performer = performer.replacingOccurrences(of: word, with: " ") + } + + let metaUrl = "https://itunes.apple.com/search?term=\(urlEncodedStringFromString("\(performer) \(resource.title)"))&entity=song&limit=4" + + let fetchDisposable = MetaDisposable() + + let disposable = fetchHttpResource(url: metaUrl).start(next: { result in + if case let .dataPart(_, data, _, complete) = result, complete { + guard let dict = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] else { + subscriber.putNext(.dataPart(resourceOffset: 0, data: Data(), range: 0 ..< 0, complete: true)) + subscriber.putCompletion() + return + } + + guard let results = dict["results"] as? [Any] else { + subscriber.putNext(.dataPart(resourceOffset: 0, data: Data(), range: 0 ..< 0, complete: true)) + subscriber.putCompletion() + return + } + + guard let result = results.first as? [String: Any] else { + subscriber.putNext(.dataPart(resourceOffset: 0, data: Data(), range: 0 ..< 0, complete: true)) + subscriber.putCompletion() + return + } + + guard var artworkUrl = result["artworkUrl100"] as? String else { + subscriber.putNext(.dataPart(resourceOffset: 0, data: Data(), range: 0 ..< 0, complete: true)) + subscriber.putCompletion() + return + } + + if !resource.isThumbnail { + artworkUrl = artworkUrl.replacingOccurrences(of: "100x100", with: "600x600") + } + + if artworkUrl.isEmpty { + subscriber.putNext(.dataPart(resourceOffset: 0, data: Data(), range: 0 ..< 0, complete: true)) + subscriber.putCompletion() + return + } else { + fetchDisposable.set(fetchHttpResource(url: artworkUrl).start(next: { next in + subscriber.putNext(next) + }, completed: { + subscriber.putCompletion() + })) + } + } + }) + + return ActionDisposable { + disposable.dispose() + fetchDisposable.dispose() + } + } + } +} diff --git a/Telegram-Mac/ExternalVideoLoader.swift b/Telegram-Mac/ExternalVideoLoader.swift index b4c3de4fbc..1e6d0e5069 100644 --- a/Telegram-Mac/ExternalVideoLoader.swift +++ b/Telegram-Mac/ExternalVideoLoader.swift @@ -7,9 +7,10 @@ // import Cocoa -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit let sharedVideoLoader:ExternalVideoLoader = { let shared = ExternalVideoLoader() @@ -17,20 +18,27 @@ let sharedVideoLoader:ExternalVideoLoader = { }() +enum ExternalVideoServiceType { + case youtube + case vimeo + case none +} final class ExternalVideo : Equatable { let dimensions:NSSize let quality:NSSize let stream:String - fileprivate init(dimensions:NSSize, stream:String, quality:NSSize) { + let date: TimeInterval + fileprivate init(dimensions:NSSize, stream:String, quality:NSSize, date: TimeInterval) { self.dimensions = dimensions self.stream = stream self.quality = quality + self.date = date } } func ==(lhs:ExternalVideo, rhs:ExternalVideo) -> Bool { - return lhs.stream == rhs.stream + return lhs.stream == rhs.stream && lhs.date == rhs.date } enum ExternalVideoStatus { @@ -63,6 +71,7 @@ private final class ExternalVideoStatusContext { private let youtubeName = "YouTube" private let vimeoName = "Vimeo" +fileprivate let twitterIcon = #imageLiteral(resourceName: "icons8-circled-play-48").precomposed() fileprivate let youtubeIcon = #imageLiteral(resourceName: "icon_YouTubePlay").precomposed() fileprivate let vimeoIcon = #imageLiteral(resourceName: "Icon_VimeoPlay").precomposed() @@ -78,10 +87,16 @@ class ExternalVideoLoader { private var cancelTokensVimeo:[WrappedExternalVideoId: Any] = [:] static func isPlayable(_ content:TelegramMediaWebpageLoadedContent) -> Bool { + return (content.websiteName == youtubeName || content.websiteName == vimeoName) && content.image != nil } static func playIcon(_ content:TelegramMediaWebpageLoadedContent) -> CGImage? { + if let embedUrl = content.embedUrl { + if embedUrl.contains("twitter.com/i/videos") && content.image != nil { + return twitterIcon + } + } if content.websiteName == vimeoName { return vimeoIcon } else if content.websiteName == youtubeName { @@ -91,6 +106,15 @@ class ExternalVideoLoader { } + static func serviceType(_ content:TelegramMediaWebpageLoadedContent) -> ExternalVideoServiceType { + if content.websiteName == vimeoName { + return .vimeo + } else if content.websiteName == youtubeName { + return .youtube + } + return .none + } + func status(for content:TelegramMediaWebpageLoadedContent) -> Signal { return Signal { subscriber in let disposable = MetaDisposable() @@ -127,10 +151,10 @@ class ExternalVideoLoader { } func fetch(for content:TelegramMediaWebpageLoadedContent) -> Signal { - if content.websiteName == youtubeName { + if content.displayUrl.lowercased().contains(youtubeName.lowercased()) { return fetchYoutubeContent(for: content.displayUrl) } - if content.websiteName == vimeoName { + if content.displayUrl.lowercased().contains(vimeoName.lowercased()) { return fetchVimeoContent(for: content.displayUrl) } return .fail(Void()) @@ -142,7 +166,7 @@ class ExternalVideoLoader { let disposable:MetaDisposable = MetaDisposable() self.statusQueue.async { - if let video = self.dataContexts[WrappedExternalVideoId(embed)] { + if let video = self.dataContexts[WrappedExternalVideoId(embed)], Date().timeIntervalSince1970 - 30 * 60 < video.date { subscriber.putNext(video) subscriber.putCompletion() } else if let statusContext = self.statusContexts[WrappedExternalVideoId(embed)], statusContext.status != nil { @@ -180,7 +204,7 @@ class ExternalVideoLoader { stream = url.absoluteString } if let quality = quality, let stream = stream { - externalVideo = ExternalVideo(dimensions: NSMakeSize(1280, 720), stream: stream, quality: quality) + externalVideo = ExternalVideo(dimensions: NSMakeSize(1280, 720), stream: stream, quality: quality, date: Date().timeIntervalSince1970) self.dataContexts[WrappedExternalVideoId(embed)] = externalVideo! status = .loaded(externalVideo!) } else { @@ -190,6 +214,10 @@ class ExternalVideoLoader { status = .fail } + if self.statusContexts[WrappedExternalVideoId(embed)] == nil { + self.statusContexts[WrappedExternalVideoId(embed)] = ExternalVideoStatusContext() + } + if let statusContext = self.statusContexts[WrappedExternalVideoId(embed)] { statusContext.status = status @@ -205,15 +233,17 @@ class ExternalVideoLoader { }) } disposable.set(ActionDisposable { - if let operation = self.cancelTokensYT[WrappedExternalVideoId(embed)] { - operation.cancel() - self.cancelTokensYT.removeValue(forKey: WrappedExternalVideoId(embed)) - - if let statusContext = self.statusContexts[WrappedExternalVideoId(embed)], let status = statusContext.status, case .fetching = status { - statusContext.status = nil + self.statusQueue.async { + if let operation = self.cancelTokensYT[WrappedExternalVideoId(embed)] { + operation.cancel() + self.cancelTokensYT.removeValue(forKey: WrappedExternalVideoId(embed)) - for subscriber in statusContext.subscribers.copyItems() { - subscriber(statusContext.status) + if let statusContext = self.statusContexts[WrappedExternalVideoId(embed)], let status = statusContext.status, case .fetching = status { + statusContext.status = nil + + for subscriber in statusContext.subscribers.copyItems() { + subscriber(statusContext.status) + } } } } @@ -234,7 +264,7 @@ class ExternalVideoLoader { var canceled:Bool = false - if let video = self.dataContexts[WrappedExternalVideoId(embed)] { + if let video = self.dataContexts[WrappedExternalVideoId(embed)], Date().timeIntervalSince1970 - 30 * 60 < video.date { subscriber.putNext(video) subscriber.putCompletion() } else if self.cancelTokensVimeo[WrappedExternalVideoId(embed)] == nil { @@ -257,11 +287,15 @@ class ExternalVideoLoader { if let video = video { let quality:NSSize = NSMakeSize(1280, 720) - let stream:String = video.highestQualityStreamURL().absoluteString + let stream:String? = video.highestQualityStreamURL()?.absoluteString + if let stream = stream { + externalVideo = ExternalVideo(dimensions: NSMakeSize(1280, 720), stream: stream, quality: quality, date: Date().timeIntervalSince1970) + self.dataContexts[WrappedExternalVideoId(embed)] = externalVideo! + status = .loaded(externalVideo!) + } else { + status = .fail + } - externalVideo = ExternalVideo(dimensions: NSMakeSize(1280, 720), stream: stream, quality: quality) - self.dataContexts[WrappedExternalVideoId(embed)] = externalVideo! - status = .loaded(externalVideo!) } else { status = .fail } @@ -283,14 +317,16 @@ class ExternalVideoLoader { } disposable.set(ActionDisposable { - if let _ = self.cancelTokensVimeo[WrappedExternalVideoId(embed)] { - self.cancelTokensVimeo.removeValue(forKey: WrappedExternalVideoId(embed)) - canceled = true - if let statusContext = self.statusContexts[WrappedExternalVideoId(embed)], let status = statusContext.status, case .fetching = status { - statusContext.status = nil - - for subscriber in statusContext.subscribers.copyItems() { - subscriber(statusContext.status) + self.statusQueue.async { + if let _ = self.cancelTokensVimeo[WrappedExternalVideoId(embed)] { + self.cancelTokensVimeo.removeValue(forKey: WrappedExternalVideoId(embed)) + canceled = true + if let statusContext = self.statusContexts[WrappedExternalVideoId(embed)], let status = statusContext.status, case .fetching = status { + statusContext.status = nil + + for subscriber in statusContext.subscribers.copyItems() { + subscriber(statusContext.status) + } } } } diff --git a/Telegram-Mac/FWDNavigationAction.swift b/Telegram-Mac/FWDNavigationAction.swift index f0e176ae69..d05f19a0e7 100644 --- a/Telegram-Mac/FWDNavigationAction.swift +++ b/Telegram-Mac/FWDNavigationAction.swift @@ -8,8 +8,9 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac +import TelegramCore + +import Postbox class FWDNavigationAction: NavigationModalAction { let messages:[Message] @@ -20,23 +21,19 @@ class FWDNavigationAction: NavigationModalAction { init(messages:[Message], displayName:String) { self.messages = messages - super.init(reason: tr(.forwardModalActionTitleCountable(messages.count)), desc: tr(.forwardModalActionDescriptionCountable(messages.count, displayName))) + super.init(reason: L10n.forwardModalActionTitleCountable(messages.count), desc: L10n.forwardModalActionDescriptionCountable(messages.count, displayName)) } override func isInvokable(for value:Any) -> Bool { if let value = value as? Peer { - for message in messages { - if !message.possibilityForwardTo(value) { - return false - } - } + return value.canSendMessage(false) } return true } override func alertError(for value:Any, with window:Window) -> Void { if let _ = value as? Peer { - alert(for: window, header: appName, info: tr(.alertForwardError)) + alert(for: window, info: tr(L10n.alertForwardError)) } } diff --git a/Telegram-Mac/FastBlur.h b/Telegram-Mac/FastBlur.h index 6a0448eef5..9cd7646dd7 100644 --- a/Telegram-Mac/FastBlur.h +++ b/Telegram-Mac/FastBlur.h @@ -12,5 +12,6 @@ #import void telegramFastBlur(int imageWidth, int imageHeight, int imageStride, void *pixels); - +void telegramFastBlurMore(int imageWidth, int imageHeight, int imageStride, void *pixels); +void stickerThumbnailAlphaBlur(int imageWidth, int imageHeight, int imageStride, void *pixels); #endif diff --git a/Telegram-Mac/FastBlur.m b/Telegram-Mac/FastBlur.m index e125ce260d..f4faf4f63c 100644 --- a/Telegram-Mac/FastBlur.m +++ b/Telegram-Mac/FastBlur.m @@ -7,6 +7,8 @@ // #import "FastBlur.h" +#import + @@ -110,3 +112,128 @@ void telegramFastBlur(int imageWidth, int imageHeight, int imageStride, void *pi free(rgb); } + + + + + +void telegramFastBlurMore(int imageWidth, int imageHeight, int imageStride, void *pixels) +{ + uint8_t *pix = (uint8_t *)pixels; + const int w = imageWidth; + const int h = imageHeight; + const int stride = imageStride; + const int radius = 7; + const int r1 = radius + 1; + const int div = radius * 2 + 1; + + if (radius > 15 || div >= w || div >= h) + { + return; + } + + uint64_t *rgb = malloc(imageStride * imageHeight * sizeof(uint64_t)); + + int x, y, i; + + int yw = 0; + const int we = w - r1; + for (y = 0; y < h; y++) { + uint64_t cur = get_colors (&pix[yw]); + uint64_t rgballsum = -radius * cur; + uint64_t rgbsum = cur * ((r1 * (r1 + 1)) >> 1); + + for (i = 1; i <= radius; i++) { + uint64_t cur = get_colors (&pix[yw + i * 4]); + rgbsum += cur * (r1 - i); + rgballsum += cur; + } + + x = 0; + +#define update(start, middle, end) \ +rgb[y * w + x] = (rgbsum >> 6) & 0x00FF00FF00FF00FF; \ +\ +rgballsum += get_colors (&pix[yw + (start) * 4]) - \ +2 * get_colors (&pix[yw + (middle) * 4]) + \ +get_colors (&pix[yw + (end) * 4]); \ +rgbsum += rgballsum; \ +x++; \ + + while (x < r1) { + update (0, x, x + r1); + } + while (x < we) { + update (x - r1, x, x + r1); + } + while (x < w) { + update (x - r1, x, w - 1); + } +#undef update + + yw += stride; + } + + const int he = h - r1; + for (x = 0; x < w; x++) { + uint64_t rgballsum = -radius * rgb[x]; + uint64_t rgbsum = rgb[x] * ((r1 * (r1 + 1)) >> 1); + for (i = 1; i <= radius; i++) { + rgbsum += rgb[i * w + x] * (r1 - i); + rgballsum += rgb[i * w + x]; + } + + y = 0; + int yi = x * 4; + +#define update(start, middle, end) \ +int64_t res = rgbsum >> 6; \ +pix[yi] = (uint8_t)res; \ +pix[yi + 1] = (uint8_t)(res >> 16); \ +pix[yi + 2] = (uint8_t)(res >> 32); \ +\ +rgballsum += rgb[x + (start) * w] - \ +2 * rgb[x + (middle) * w] + \ +rgb[x + (end) * w]; \ +rgbsum += rgballsum; \ +y++; \ +yi += stride; + + while (y < r1) { + update (0, y, y + r1); + } + while (y < he) { + update (y - r1, y, y + r1); + } + while (y < h) { + update (y - r1, y, h - 1); + } +#undef update + } + + free(rgb); +} + + + +void stickerThumbnailAlphaBlur(int imageWidth, int imageHeight, int imageStride, void *pixels) { + vImage_Buffer srcBuffer; + srcBuffer.width = imageWidth; + srcBuffer.height = imageHeight; + srcBuffer.rowBytes = imageStride; + srcBuffer.data = pixels; + + { + vImage_Buffer dstBuffer; + dstBuffer.width = imageWidth; + dstBuffer.height = imageHeight; + dstBuffer.rowBytes = imageStride; + dstBuffer.data = pixels; + + int boxSize = 2; + boxSize = boxSize - (boxSize % 2) + 1; + + vImageBoxConvolve_ARGB8888(&srcBuffer, &dstBuffer, NULL, 0, 0, boxSize, boxSize, NULL, kvImageEdgeExtend); + } + +} diff --git a/Telegram-Mac/FastSettings.swift b/Telegram-Mac/FastSettings.swift index a5f7b60e20..279214aca1 100644 --- a/Telegram-Mac/FastSettings.swift +++ b/Telegram-Mac/FastSettings.swift @@ -7,9 +7,10 @@ // import Cocoa -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac +import TelegramCore + +import SwiftSignalKit +import Postbox enum SendingType :String { case enter = "enter" @@ -31,6 +32,86 @@ enum ForceTouchAction: Int32 { case edit case reply case forward + case previewMedia +} + +enum ContextTextTooltip : Int32 { + case reply + case edit +} + +enum BotPemissionKey: String { + case contact = "PermissionInlineBotContact" +} + + +enum AppTooltip { + case voiceRecording + case videoRecording + case mediaPreview_archive + case mediaPreview_collage + case mediaPreview_media + case mediaPreview_file + fileprivate var localizedString: String { + switch self { + case .voiceRecording: + return L10n.appTooltipVoiceRecord + case .videoRecording: + return L10n.appTooltipVideoRecord + case .mediaPreview_archive: + return L10n.previewSenderArchiveTooltip + case .mediaPreview_collage: + return L10n.previewSenderCollageTooltip + case .mediaPreview_media: + return L10n.previewSenderMediaTooltip + case .mediaPreview_file: + return L10n.previewSenderFileTooltip + } + } + + private var version:Int { + return 1 + } + + fileprivate var key: String { + switch self { + case .voiceRecording: + return "app_tooltip_voice_recording_" + "\(version)" + case .videoRecording: + return "app_tooltip_video_recording_" + "\(version)" + case .mediaPreview_archive: + return "app_tooltip_mediaPreview_archive_" + "\(version)" + case .mediaPreview_collage: + return "app_tooltip_mediaPreview_collage_" + "\(version)" + case .mediaPreview_media: + return "app_tooltip_mediaPreview_media_" + "\(version)" + case .mediaPreview_file: + return "app_tooltip_mediaPreview_file_" + "\(version)" + } + } + + fileprivate var showCount: Int { + return 4 + } + +} + +func getAppTooltip(for value: AppTooltip, callback: (String) -> Void) { + let shownCount: Int = UserDefaults.standard.integer(forKey: value.key) + + var success: Bool = false + + defer { + if success { + UserDefaults.standard.set(shownCount + 1, forKey: value.key) + UserDefaults.standard.synchronize() + } + } + //shownCount == 0 || (shownCount < value.showCount && arc4random_uniform(100) > 100 / 3) + if true { + success = true + callback(value.localizedString) + } } class FastSettings { @@ -44,6 +125,28 @@ class FastSettings { private static let kIsMinimisizeType = "kIsMinimisizeType" private static let kAutomaticConvertEmojiesType = "kAutomaticConvertEmojiesType2" private static let kForceTouchAction = "kForceTouchAction" + private static let kNeedCollage = "kNeedCollage" + private static let kInstantViewScrollBySpace = "kInstantViewScrollBySpace" + private static let kAutomaticallyPlayGifs = "kAutomaticallyPlayGifs" + private static let kArchiveIsHidden = "kArchiveIsHidden" + private static let kRTFEnable = "kRTFEnable"; + private static let kNeedShowChannelIntro = "kNeedShowChannelIntro" + + private static let kNoticeAdChannel = "kNoticeAdChannel" + private static let kPlayingRate = "kPlayingRate" + + private static let kSVCShareMicro = "kSVCShareMicro" + + + private static let kVolumeRate = "kVolumeRate" + + private static let kArchiveAutohidden = "kArchiveAutohidden" + private static let kAutohideArchiveFeature = "kAutohideArchiveFeature" + + private static let kLeftColumnWidth = "kLeftColumnWidth" + + private static let kShowEmptyTips = "kShowEmptyTips" + static var sendingType:SendingType { let type = UserDefaults.standard.value(forKey: kSendingType) as? String @@ -79,6 +182,26 @@ class FastSettings { UserDefaults.standard.set(!isChannelMessagesMuted(peerId), forKey: "\(peerId)_m_muted") } + static var playingRate: Double { + return min(max(UserDefaults.standard.double(forKey: kPlayingRate), 1), 1.7) + } + + static func setPlayingRate(_ rate: Double) { + UserDefaults.standard.set(rate, forKey: kPlayingRate) + } + + static var volumeRate: Float { + if UserDefaults.standard.value(forKey: kVolumeRate) != nil { + return min(max(UserDefaults.standard.float(forKey: kVolumeRate), 0), 1) + } else { + return 0.8 + } + } + + static func setVolumeRate(_ rate: Float) { + UserDefaults.standard.set(rate, forKey: kVolumeRate) + } + static var isMinimisize: Bool { get { return UserDefaults.standard.bool(forKey: kIsMinimisizeType) @@ -100,41 +223,182 @@ class FastSettings { return RecordingStateSettings(rawValue: Int32(UserDefaults.standard.integer(forKey: kRecordingStateType))) ?? .voice } + static var isNeedCollage: Bool { + if UserDefaults.standard.value(forKey: kNeedCollage) == nil { + return true + } + return UserDefaults.standard.bool(forKey: kNeedCollage) + } + + static var enableRTF: Bool { + set { + UserDefaults.standard.set(!newValue, forKey: kRTFEnable) + UserDefaults.standard.synchronize() + } + get { + return !UserDefaults.standard.bool(forKey: kRTFEnable) + } + } + + static func toggleIsNeedCollage(_ enable: Bool) -> Void { + UserDefaults.standard.set(enable, forKey: kNeedCollage) + UserDefaults.standard.synchronize() + } + + static var vcShareMicro: Bool { + if let value = UserDefaults.standard.value(forKey: kSVCShareMicro) as? Bool { + return value + } + return true + } + static func updateVCShareMicro(_ value: Bool) { + UserDefaults.standard.setValue(value, forKey: kSVCShareMicro) + } + + static var emptyTips: Bool { + if let value = UserDefaults.standard.value(forKey: kShowEmptyTips) as? Bool { + return value + } + return true + } + static func updateEmptyTips(_ value: Bool) { + UserDefaults.standard.setValue(value, forKey: kShowEmptyTips) + } + + + + static func toggleRecordingState() { UserDefaults.standard.set((recordingState == .voice ? RecordingStateSettings.video : RecordingStateSettings.voice).rawValue, forKey: kRecordingStateType) } + static var needShowChannelIntro: Bool { + return !UserDefaults.standard.bool(forKey: kNeedShowChannelIntro) + } + + static func markChannelIntroHasSeen() { + UserDefaults.standard.set(true, forKey: kNeedShowChannelIntro) + } + static var forceTouchAction: ForceTouchAction { return ForceTouchAction(rawValue: Int32(UserDefaults.standard.integer(forKey: kForceTouchAction))) ?? .edit } static func toggleForceTouchAction(_ action: ForceTouchAction) { UserDefaults.standard.set(action.rawValue, forKey: kForceTouchAction) + UserDefaults.standard.synchronize() + } + + static func tooltipAbility(for tooltip: ContextTextTooltip) -> Bool { + let value = UserDefaults.standard.integer(forKey: "tooltip:\(tooltip.rawValue)") + UserDefaults.standard.set(value + 1, forKey: "tooltip:\(tooltip.rawValue)") + return value < 12 + } + + static func archivedTooltipCountAndIncrement() -> Int { + let value = UserDefaults.standard.integer(forKey: "archivation_tooltips") + UserDefaults.standard.set(value + 1, forKey: "archivation_tooltips") + return value + } + + static var showAdAlert: Bool { + return !UserDefaults.standard.bool(forKey: kNoticeAdChannel) + } + + static func adAlertViewed() { + UserDefaults.standard.set(true, forKey: kNoticeAdChannel) + UserDefaults.standard.synchronize() + } + + static func openInQuickLook(_ ext: String) -> Bool { + return !UserDefaults.standard.bool(forKey: "open_in_quick_look_\(ext)") + } + static func toggleOpenInQuickLook(_ ext: String) -> Void { + UserDefaults.standard.set(openInQuickLook(ext), forKey: "open_in_quick_look_\(ext)") + UserDefaults.standard.synchronize() } static func toggleSidebarShown(_ enable: Bool) { UserDefaults.standard.set(!enable, forKey: kSidebarShownType) + UserDefaults.standard.synchronize() } static func toggleSidebar(_ enable: Bool) { UserDefaults.standard.set(enable, forKey: kSidebarType) + UserDefaults.standard.synchronize() } static func toggleInAppSouds(_ enable: Bool) { UserDefaults.standard.set(!enable, forKey: kInAppSoundsType) + UserDefaults.standard.synchronize() } static var inAppSounds: Bool { return !UserDefaults.standard.bool(forKey: kInAppSoundsType) } + + static func toggleInstantViewScrollBySpace(_ enable: Bool) { + UserDefaults.standard.set(enable, forKey: kInstantViewScrollBySpace) + UserDefaults.standard.synchronize() + } static func toggleAutomaticReplaceEmojies(_ enable: Bool) { UserDefaults.standard.set(!enable, forKey: kAutomaticConvertEmojiesType) + UserDefaults.standard.synchronize() } static var isPossibleReplaceEmojies: Bool { return !UserDefaults.standard.bool(forKey: kAutomaticConvertEmojiesType) } + + static var instantViewScrollBySpace: Bool { + return UserDefaults.standard.bool(forKey: kInstantViewScrollBySpace) + } + + static func toggleAutoPlayGifs(_ enable: Bool) { + UserDefaults.standard.set(!enable, forKey: kAutomaticallyPlayGifs) + UserDefaults.standard.synchronize() + } + + static var gifsAutoPlay:Bool { + return !UserDefaults.standard.bool(forKey: kAutomaticallyPlayGifs) + } + + static var archiveStatus: HiddenArchiveStatus { + get { + let value = UserDefaults.standard.integer(forKey: kArchiveIsHidden) + return HiddenArchiveStatus(rawValue: min(value, 3))! + } + set { + UserDefaults.standard.set(newValue.rawValue, forKey: kArchiveIsHidden) + UserDefaults.standard.synchronize() + } + } + + static func showPromoTitle(for peerId: PeerId) -> Bool { + return UserDefaults.standard.value(forKey: "promo_\(peerId)_1") as? Bool ?? true + } + static func removePromoTitle(for peerId: PeerId) { + UserDefaults.standard.set(false, forKey: "promo_\(peerId)_1") + UserDefaults.standard.synchronize() + } + + static func isTestLiked(_ messageId: MessageId) -> Bool { + return UserDefaults.standard.value(forKey: "isTestLiked_\(messageId)") as? Bool ?? false + } + static func toggleTestLike(_ messageId: MessageId) { + UserDefaults.standard.set(!isTestLiked(messageId), forKey: "isTestLiked_\(messageId)") + UserDefaults.standard.synchronize() + } + + static func isSecretChatWebPreviewAvailable(for accountId: Int64) -> Bool? { + return UserDefaults.standard.value(forKey: "IsSecretChatWebPreviewAvailable_\(accountId)") as? Bool + } + + static func setSecretChatWebPreviewAvailable(for accountId: Int64, value: Bool) -> Void { + UserDefaults.standard.set(value, forKey: "IsSecretChatWebPreviewAvailable_\(accountId)") + UserDefaults.standard.synchronize() + } static var downloadsFolder:String? { let paths = NSSearchPathForDirectoriesInDomains(.downloadsDirectory, .userDomainMask, true) @@ -142,18 +406,101 @@ class FastSettings { return path } + + static func requstPermission(with permission: BotPemissionKey, for peerId: PeerId, success: @escaping()->Void) { + + let localizedHeader = _NSLocalizedString("Confirm.Header.\(permission.rawValue)") + let localizedDesc = _NSLocalizedString("Confirm.Desc.\(permission.rawValue)") + confirm(for: mainWindow, header: localizedHeader, information: localizedDesc, successHandler: { _ in + success() + }) + } + + static func diceHasAlreadyPlayed(_ message: Message) -> Bool { + return UserDefaults.standard.bool(forKey: "dice_\(message.id.id)_\(message.id.namespace)_\(message.stableId)") + } + static func markDiceAsPlayed(_ message: Message) { + UserDefaults.standard.set(true, forKey: "dice_\(message.id.id)_\(message.id.namespace)_\(message.stableId)") + UserDefaults.standard.synchronize() + } + + static func updateLeftColumnWidth(_ width: CGFloat) { + UserDefaults.standard.set(round(width), forKey: kLeftColumnWidth) + UserDefaults.standard.synchronize() + } + static var leftColumnWidth: CGFloat { + return round(UserDefaults.standard.value(forKey: kLeftColumnWidth) as? CGFloat ?? 300) + } + + /* + + +(void)requestPermissionWithKey:(NSString *)permissionKey peer_id:(int)peer_id handler:(void (^)(bool success))handler { + + static NSMutableDictionary *denied; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + denied = [NSMutableDictionary dictionary]; + }); + + + + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + + NSString *key = [NSString stringWithFormat:@"%@:%d",permissionKey,peer_id]; + + BOOL access = [defaults boolForKey:key]; + + + if(access) { + if(handler) + handler(access); + } else { + + if([denied[key] boolValue]) { + if(handler) + handler(NO); + return; + } + + NSString *localizeHeaderKey = [NSString stringWithFormat:@"Confirm.Header.%@",permissionKey]; + NSString *localizeDescKey = [NSString stringWithFormat:@"Confirm.Desc.%@",permissionKey]; + confirm(NSLocalizedString(localizeHeaderKey, nil), NSLocalizedString(localizeDescKey, nil), ^{ + if(handler) + handler(YES); + + [defaults setBool:YES forKey:key]; + [defaults synchronize]; + }, ^{ + if(handler) + handler(NO); + + [denied setValue:@(YES) forKey:key]; + + [defaults setBool:NO forKey:key]; + [defaults synchronize]; + }); + } + + } + + */ + } fileprivate let TelegramFileMediaBoxPath:String = "TelegramFileMediaBoxPathAttributeKey" func saveAs(_ file:TelegramMediaFile, account:Account) { - let name = account.postbox.mediaBox.resourceData(file.resource) |> mapToSignal { data -> Signal< (String, String), Void> in + let name = account.postbox.mediaBox.resourceData(file.resource) |> mapToSignal { data -> Signal< (String, String), NoError> in if data.complete { var ext:String = "" let fileName = file.fileName ?? data.path.nsstring.lastPathComponent + if let ext = file.fileName?.nsstring.pathExtension { + return .single((data.path, ext)) + } ext = fileName.nsstring.pathExtension - return resourceType(mimeType: file.mimeType) |> mapToSignal { _type -> Signal<(String, String), Void> in + return resourceType(mimeType: file.mimeType) |> mapToSignal { _type -> Signal<(String, String), NoError> in let ext = _type == "*" || _type == nil ? (ext.length == 0 ? "file" : ext) : _type! return .single((data.path, ext)) @@ -164,90 +511,194 @@ func saveAs(_ file:TelegramMediaFile, account:Account) { } |> deliverOnMainQueue _ = name.start(next: { path, ext in - savePanel(file: path, ext: ext, for: mainWindow) + savePanel(file: path, ext: ext, for: mainWindow, defaultName: file.fileName) }) } -func copyToDownloads(_ file: TelegramMediaFile, account:Account) -> Signal { - return downloadFilePath(file, account) |> deliverOn(resourcesQueue) |> map { (boxPath, adopted) in - var adopted = adopted - var i:Int = 1 - let deletedPathExt = adopted.nsstring.deletingPathExtension - while FileManager.default.fileExists(atPath: adopted, isDirectory: nil) { - let ext = adopted.nsstring.pathExtension - let box = FileManager.xattrStringValue(forKey: TelegramFileMediaBoxPath, at: URL(fileURLWithPath: adopted)) - if box == boxPath { - return +func copyToDownloads(_ file: TelegramMediaFile, postbox: Postbox, saveAnyway: Bool = false) -> Signal { + let path = downloadFilePath(file, postbox) + return combineLatest(queue: resourcesQueue, path, downloadedFilePaths(postbox)) |> map { (expanded, paths) in + guard let (boxPath, adopted) = expanded else { + return nil + } + if let id = file.id { + if let path = paths.path(for: id), !saveAnyway { + let lastModified = Int32(FileManager.default.modificationDateForFileAtPath(path: path.downloadedPath)?.timeIntervalSince1970 ?? 0) + if fileSize(path.downloadedPath) == Int(path.size), lastModified == path.lastModified { + return path.downloadedPath + } + } - adopted = "\(deletedPathExt) (\(i)).\(ext)" - i += 1 + var adopted = adopted + var i:Int = 1 + let deletedPathExt = adopted.nsstring.deletingPathExtension + while FileManager.default.fileExists(atPath: adopted, isDirectory: nil) { + let ext = adopted.nsstring.pathExtension + adopted = "\(deletedPathExt) (\(i)).\(ext)" + i += 1 + } + + try? FileManager.default.copyItem(atPath: boxPath, toPath: adopted) + +// let quarantineData = "doesn't matter".data(using: .utf8)! +// +// +// URL(fileURLWithPath: adopted).withUnsafeFileSystemRepresentation { fileSystemPath in +// _ = quarantineData.withUnsafeBytes { +// setxattr(fileSystemPath, "com.apple.quarantine", $0.baseAddress, quarantineData.count, 0, 0) +// } +// } + + let lastModified = FileManager.default.modificationDateForFileAtPath(path: adopted)?.timeIntervalSince1970 ?? FileManager.default.creationDateForFileAtPath(path: adopted)?.timeIntervalSince1970 ?? Date().timeIntervalSince1970 + + let fs = fileSize(boxPath) + let path = DownloadedPath(id: id, downloadedPath: adopted, size: fs != nil ? Int32(fs!) : nil ?? Int32(file.size ?? 0), lastModified: Int32(lastModified)) + + _ = updateDownloadedFilePaths(postbox, { + $0.withAddedPath(path) + }).start() + + return adopted + } else { + return adopted } - - try? FileManager.default.copyItem(atPath: boxPath, toPath: adopted) - FileManager.setXAttrStringValue(boxPath, forKey: TelegramFileMediaBoxPath, at: URL(fileURLWithPath: adopted)) } +// return downloadFilePath(file, postbox) |> deliverOn(resourcesQueue) |> map { (boxPath, adopted) in +// var adopted = adopted +// var i:Int = 1 +// let deletedPathExt = adopted.nsstring.deletingPathExtension +// while FileManager.default.fileExists(atPath: adopted, isDirectory: nil) { +// let ext = adopted.nsstring.pathExtension +// let box = FileManager.xattrStringValue(forKey: TelegramFileMediaBoxPath, at: URL(fileURLWithPath: adopted)) +// if box == boxPath { +// return +// } +// +// adopted = "\(deletedPathExt) (\(i)).\(ext)" +// i += 1 +// } +// +// try? FileManager.default.copyItem(atPath: boxPath, toPath: adopted) +// FileManager.setXAttrStringValue(boxPath, forKey: TelegramFileMediaBoxPath, at: URL(fileURLWithPath: adopted)) +// } +// } -private func downloadFilePath(_ file: TelegramMediaFile, _ account:Account) -> Signal<(String, String), Void> { - return account.postbox.mediaBox.resourceData(file.resource) |> mapToSignal { data -> Signal< (String, String), Void> in +func downloadFilePath(_ file: TelegramMediaFile, _ postbox: Postbox) -> Signal<(String, String)?, NoError> { + return combineLatest(postbox.mediaBox.resourceData(file.resource) |> take(1), automaticDownloadSettings(postbox: postbox) |> take(1)) |> mapToSignal { data, settings -> Signal< (String, String)?, NoError> in if data.complete { var ext:String = "" let fileName = file.fileName ?? data.path.nsstring.lastPathComponent ext = fileName.nsstring.pathExtension if !ext.isEmpty { - if let folder = FastSettings.downloadsFolder { - return .single((data.path, "\(folder)/\(fileName.nsstring.deletingPathExtension).\(ext)")) - } - return .complete() + return .single((data.path, "\(settings.downloadFolder)/\(fileName.nsstring.deletingPathExtension).\(ext)")) } else { - return resourceType(mimeType: file.mimeType) |> mapToSignal { (ext) -> Signal<(String, String), Void> in + return resourceType(mimeType: file.mimeType) |> mapToSignal { (ext) -> Signal<(String, String)?, NoError> in if let folder = FastSettings.downloadsFolder { let ext = ext == "*" || ext == nil ? "file" : ext! return .single((data.path, "\(folder)/\(fileName).\( ext )")) } - return .complete() + return .single(nil) } } } else { - return .complete() + return .single(nil) + } + } +} + +func fileFinderPath(_ file: TelegramMediaFile, _ postbox: Postbox) -> Signal { + return combineLatest(downloadFilePath(file, postbox), downloadedFilePaths(postbox)) |> map { (expanded, paths) in + guard let (boxPath, adopted) = expanded else { + return nil + } + if let id = file.id { + do { + + if let path = paths.path(for: id) { + let lastModified = Int32(FileManager.default.modificationDateForFileAtPath(path: path.downloadedPath)?.timeIntervalSince1970 ?? 0) + if fileSize(path.downloadedPath) == Int(path.size), lastModified == path.lastModified { + return path.downloadedPath + } + } + + return adopted + } + } else { + return nil } } } func showInFinder(_ file:TelegramMediaFile, account:Account) { - let path = downloadFilePath(file, account) |> deliverOnMainQueue + let path = downloadFilePath(file, account.postbox) |> deliverOnMainQueue - _ = path.start(next: { (boxPath, adopted) in - do { - var adopted = adopted - - var i:Int = 1 - let deletedPathExt = adopted.nsstring.deletingPathExtension - while FileManager.default.fileExists(atPath: adopted, isDirectory: nil) { - let ext = adopted.nsstring.pathExtension - let box = FileManager.xattrStringValue(forKey: TelegramFileMediaBoxPath, at: URL(fileURLWithPath: adopted)) - if box == boxPath { - NSWorkspace.shared.activateFileViewerSelecting([URL(fileURLWithPath: adopted)]) - return + _ = combineLatest(path, downloadedFilePaths(account.postbox)).start(next: { (expanded, paths) in + + guard let (boxPath, adopted) = expanded else { + return + } + if let id = file.id { + do { + + if let path = paths.path(for: id) { + let lastModified = Int32(FileManager.default.modificationDateForFileAtPath(path: path.downloadedPath)?.timeIntervalSince1970 ?? 0) + if fileSize(path.downloadedPath) == Int(path.size), lastModified == path.lastModified { + NSWorkspace.shared.activateFileViewerSelecting([URL(fileURLWithPath: path.downloadedPath)]) + return + } + } - adopted = "\(deletedPathExt) (\(i)).\(ext)" - i += 1 + var adopted = adopted + var i:Int = 1 + let deletedPathExt = adopted.nsstring.deletingPathExtension + while FileManager.default.fileExists(atPath: adopted, isDirectory: nil) { + let ext = adopted.nsstring.pathExtension + adopted = "\(deletedPathExt) (\(i)).\(ext)" + i += 1 + } + + try? FileManager.default.copyItem(atPath: boxPath, toPath: adopted) + +// let quarantineData = "doesn't matter".data(using: .utf8)! +// +// URL(fileURLWithPath: adopted).withUnsafeFileSystemRepresentation { fileSystemPath in +// _ = quarantineData.withUnsafeBytes { +// setxattr(fileSystemPath, "com.apple.quarantine", $0.baseAddress, quarantineData.count, 0, 0) +// } +// } + + //setxattr(<#T##path: UnsafePointer!##UnsafePointer!#>, <#T##name: UnsafePointer!##UnsafePointer!#>, <#T##value: UnsafeRawPointer!##UnsafeRawPointer!#>, <#T##size: Int##Int#>, <#T##position: UInt32##UInt32#>, <#T##options: Int32##Int32#>) + + // setxattr(ordinaryFileURL.path, SUAppleQuarantineIdentifier, quarantineData, quarantineDataLength, 0, XATTR_CREATE) + + let lastModified = FileManager.default.modificationDateForFileAtPath(path: adopted)?.timeIntervalSince1970 ?? FileManager.default.creationDateForFileAtPath(path: adopted)?.timeIntervalSince1970 ?? Date().timeIntervalSince1970 + + let fs = fileSize(boxPath) + let path = DownloadedPath(id: id, downloadedPath: adopted, size: fs != nil ? Int32(fs!) : nil ?? Int32(file.size ?? 0), lastModified: Int32(lastModified)) + + + NSWorkspace.shared.activateFileViewerSelecting([URL(fileURLWithPath: adopted)]) + _ = updateDownloadedFilePaths(account.postbox, { + $0.withAddedPath(path) + }).start() + } - - try? FileManager.default.copyItem(atPath: boxPath, toPath: adopted) - FileManager.setXAttrStringValue(boxPath, forKey: TelegramFileMediaBoxPath, at: URL(fileURLWithPath: adopted)) - NSWorkspace.shared.activateFileViewerSelecting([URL(fileURLWithPath: adopted)]) } }) } -func putFileToTemp(from:String, named:String) -> Signal { + + + +func putFileToTemp(from:String, named:String) -> Signal { return Signal { subscriber in let new = NSTemporaryDirectory() + named + try? FileManager.default.removeItem(atPath: new) try? FileManager.default.copyItem(atPath: from, toPath: new) subscriber.putNext(new) @@ -255,3 +706,5 @@ func putFileToTemp(from:String, named:String) -> Signal { return EmptyDisposable } } + + diff --git a/Telegram-Mac/FeaturedStickerPacksController.swift b/Telegram-Mac/FeaturedStickerPacksController.swift index 9593ccb0b4..162d5bdca6 100644 --- a/Telegram-Mac/FeaturedStickerPacksController.swift +++ b/Telegram-Mac/FeaturedStickerPacksController.swift @@ -8,18 +8,19 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac -import PostboxMac -import TelegramCoreMac +import SwiftSignalKit +import Postbox +import TelegramCore + private final class FeaturedStickerPacksControllerArguments { - let account: Account + let context: AccountContext let openStickerPack: (StickerPackCollectionInfo) -> Void let addPack: (StickerPackCollectionInfo) -> Void - init(account: Account, openStickerPack: @escaping (StickerPackCollectionInfo) -> Void, addPack: @escaping (StickerPackCollectionInfo) -> Void) { - self.account = account + init(context: AccountContext, openStickerPack: @escaping (StickerPackCollectionInfo) -> Void, addPack: @escaping (StickerPackCollectionInfo) -> Void) { + self.context = context self.openStickerPack = openStickerPack self.addPack = addPack } @@ -40,81 +41,25 @@ private enum FeaturedStickerPacksEntryId: Hashable { return id.hashValue } } - - static func ==(lhs: FeaturedStickerPacksEntryId, rhs: FeaturedStickerPacksEntryId) -> Bool { - switch lhs { - case let .pack(id): - if case .pack(id) = rhs { - return true - } else { - return false - } - case let .section(id): - if case .section(id) = rhs { - return true - } else { - return false - } - } - } } private enum FeaturedStickerPacksEntry: TableItemListNodeEntry { case section(sectionId:Int32) - case pack(sectionId:Int32, Int32, StickerPackCollectionInfo, Bool, StickerPackItem?, Int32, Bool) + case pack(sectionId:Int32, Int32, StickerPackCollectionInfo, Bool, StickerPackItem?, Int32, Bool, GeneralViewType) var stableId: FeaturedStickerPacksEntryId { switch self { - case let .pack(_, _, info, _, _, _, _): + case let .pack(_, _, info, _, _, _, _, _): return .pack(info.id) case let .section(id): return .section(id) } } - static func ==(lhs: FeaturedStickerPacksEntry, rhs: FeaturedStickerPacksEntry) -> Bool { - switch lhs { - case let .pack(lhsSectionId, lhsIndex, lhsInfo, lhsUnread, lhsTopItem, lhsCount, lhsInstalled): - if case let .pack(rhsSectionId, rhsIndex, rhsInfo, rhsUnread, rhsTopItem, rhsCount, rhsInstalled) = rhs { - - if lhsSectionId != rhsSectionId { - return false - } - if lhsIndex != rhsIndex { - return false - } - if lhsInfo != rhsInfo { - return false - } - if lhsUnread != rhsUnread { - return false - } - if lhsTopItem != rhsTopItem { - return false - } - if lhsCount != rhsCount { - return false - } - if lhsInstalled != rhsInstalled { - return false - } - return true - } else { - return false - } - case let .section(sectionId): - if case .section(sectionId) = rhs { - return true - } else { - return false - } - } - } - var stableIndex:Int32 { switch self { - case let .pack(sectionId: sectionId, index, _, _, _, _, _): + case let .pack(sectionId: sectionId, index, _, _, _, _, _, _): fatalError() case let .section(sectionId: sectionId): return (sectionId + 1) * 1000 - sectionId @@ -123,7 +68,7 @@ private enum FeaturedStickerPacksEntry: TableItemListNodeEntry { var index:Int32 { switch self { - case let .pack(sectionId: sectionId, index, _, _, _, _, _): + case let .pack(sectionId: sectionId, index, _, _, _, _, _, _): return (sectionId * 1000) + 100 + index case let .section(sectionId: sectionId): return (sectionId + 1) * 1000 - sectionId @@ -136,25 +81,16 @@ private enum FeaturedStickerPacksEntry: TableItemListNodeEntry { func item(_ arguments: FeaturedStickerPacksControllerArguments, initialSize: NSSize) -> TableRowItem { switch self { - case let .pack(_, _, info, unread, topItem, count, installed): - return StickerSetTableRowItem(initialSize, account: arguments.account, stableId: stableId, info: info, topItem: topItem, itemCount: count, unread: false, editing: ItemListStickerPackItemEditing(editable: false, editing: false), enabled: true, control: .installation(installed: installed), action: { + case let .pack(_, _, info, unread, topItem, count, installed, viewType): + return StickerSetTableRowItem(initialSize, context: arguments.context, stableId: stableId, info: info, topItem: topItem, itemCount: count, unread: false, editing: ItemListStickerPackItemEditing(editable: false, editing: false), enabled: true, control: .installation(installed: installed), viewType: viewType, action: { arguments.openStickerPack(info) }, addPack: { arguments.addPack(info) }, removePack: { }) - - /* - return ItemListStickerPackItem(account: arguments.account, packInfo: info, itemCount: count, topItem: topItem, unread: unread, control: .installation(installed: installed), editing: ItemListStickerPackItemEditing(editable: false, editing: false, revealed: false), enabled: true, sectionId: self.section, action: { _ in - arguments.openStickerPack(info) - }, addPack: { - arguments.addPack(info) - }, removePack: { _ in - }) - */ case .section: - return GeneralRowItem(initialSize, height: 20, stableId: stableId) + return GeneralRowItem(initialSize, height: 30, stableId: stableId, viewType: .separator) } } } @@ -187,11 +123,13 @@ private func featuredStickerPacksControllerEntries(state: FeaturedStickerPacksCo if let value = unreadPacks[item.info.id] { unread = value } - entries.append(.pack(sectionId: sectionId, index, item.info, unread, item.topItems.first, item.info.count, installedPacks.contains(item.info.id))) + entries.append(.pack(sectionId: sectionId, index, item.info, unread, item.topItems.first, item.info.count, installedPacks.contains(item.info.id), bestGeneralViewType(featured, for: item))) index += 1 } } } + entries.append(.section(sectionId: sectionId)) + sectionId += 1 return entries } @@ -205,7 +143,12 @@ private func prepareTransition(left:[AppearanceWrapperEntry() - stickerPacks.set(account.postbox.combinedView(keys: [.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])])) + stickerPacks.set(context.account.postbox.combinedView(keys: [.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])])) let featured = Promise<[FeaturedStickerPackItem]>() - featured.set(account.viewTracker.featuredStickerPacks()) + featured.set(context.account.viewTracker.featuredStickerPacks()) var initialUnreadPacks: [ItemCollectionId: Bool] = [:] let previousEntries:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) let initialSize = self.atomicSize - genericView.merge(with: combineLatest(statePromise.get(), stickerPacks.get(), featured.get(), appearanceSignal) |> deliverOnMainQueue + + let signal = combineLatest(queue: prepareQueue,statePromise.get(), stickerPacks.get(), featured.get(), appearanceSignal) |> map { state, view, featured, appearance -> TableUpdateTransition in - for item in featured { if initialUnreadPacks[item.info.id] == nil { initialUnreadPacks[item.info.id] = item.unread @@ -250,32 +193,22 @@ class FeaturedStickerPacksController: TableViewController { let entries = featuredStickerPacksControllerEntries(state: state, view: view, featured: featured, unreadPacks: initialUnreadPacks).map{AppearanceWrapperEntry(entry: $0, appearance: appearance)} return prepareTransition(left: previousEntries.swap(entries), right: entries, initialSize: initialSize.modify({$0}), arguments: arguments) - } |> afterDisposed { + } |> afterDisposed { actionsDisposable.dispose() - } ) + } |> deliverOnMainQueue + + self.disposable.set(signal.start(next: { [weak self] transition in + self?.genericView.merge(with: transition) + self?.readyOnce() + })) + var alreadyReadIds = Set() genericView.addScroll(listener: TableScrollListener ({ scroll in - /* - var unreadIds: [ItemCollectionId] = [] - for entry in entries { - switch entry { - case let .pack(_, info, unread, _, _, _): - if unread && !alreadyReadIds.contains(info.id) { - unreadIds.append(info.id) - } - } - } - if !unreadIds.isEmpty { - alreadyReadIds.formUnion(Set(unreadIds)) - - let _ = markFeaturedStickerPacksAsSeenInteractively(postbox: account.postbox, ids: unreadIds).start() - } - */ + })) - readyOnce() } } diff --git a/Telegram-Mac/FetchCachedRepresentations.swift b/Telegram-Mac/FetchCachedRepresentations.swift index 88476072a7..b67ebed0b0 100644 --- a/Telegram-Mac/FetchCachedRepresentations.swift +++ b/Telegram-Mac/FetchCachedRepresentations.swift @@ -7,136 +7,585 @@ // import Cocoa -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit import TGUIKit +import RLottie +import libwebp + +private let cacheThreadPool = ThreadPool(threadCount: 1, threadPriority: 0.1) -public func fetchCachedResourceRepresentation(account: Account, resource: MediaResource, resourceData: MediaResourceData, representation: CachedMediaResourceRepresentation) -> Signal { + +public func fetchCachedResourceRepresentation(account: Account, resource: MediaResource, representation: CachedMediaResourceRepresentation) -> Signal { if let representation = representation as? CachedStickerAJpegRepresentation { - return fetchCachedStickerAJpegRepresentation(account: account, resource: resource, resourceData: resourceData, representation: representation) + return fetchCachedStickerAJpegRepresentation(account: account, resource: resource, representation: representation) + } else if let representation = representation as? CachedAnimatedStickerRepresentation { + return fetchCachedAnimatedStickerRepresentation(account: account, resource: resource, representation: representation) } else if let representation = representation as? CachedScaledImageRepresentation { - return fetchCachedScaledImageRepresentation(account: account, resource: resource, resourceData: resourceData, representation: representation) - } else if let representation = representation as? CachedVideoFirstFrameRepresentation { - return fetchCachedVideoFirstFrameRepresentation(account: account, resource: resource, resourceData: resourceData, representation: representation) + return fetchCachedScaledImageRepresentation(account: account, resource: resource, representation: representation) + } else if representation is CachedVideoFirstFrameRepresentation { + return account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false)) + |> mapToSignal { data -> Signal in + if data.complete { + return fetchCachedVideoFirstFrameRepresentation(account: account, resource: resource, resourceData: data) + |> `catch` { _ -> Signal in + return .complete() + } + } else if let size = resource.size { + return videoFirstFrameData(account: account, resource: resource, chunkSize: min(size, 192 * 1024)) + } else { + return .complete() + } + } + } else if let representation = representation as? CachedScaledVideoFirstFrameRepresentation { + return fetchCachedScaledVideoFirstFrameRepresentation(account: account, resource: resource, representation: representation) + } else if let representation = representation as? CachedDiceRepresentation { + if let diceCache = account.diceCache { + return diceCache.interactiveSymbolData(baseSymbol: representation.emoji, synchronous: false) |> map { values -> (String, Data?, TelegramMediaFile)? in + for value in values { + if value.0 == representation.value { + return value + } + } + return nil + } |> filter { $0?.1 != nil } |> mapToSignal { data in + return fetchCachedDiceRepresentation(account: account, data: data!.1!, representation: representation) + } + } else { + return .complete() + } + } else if let representation = representation as? CachedSlotMachineRepresentation { + if let diceCache = account.diceCache { + return diceCache.interactiveSymbolData(baseSymbol: slotsEmoji, synchronous: false) |> map { values -> [(Data?, Int32)] in + var required: [(Data?, Int32)] = [] + required.append((values[1].1, 1)) + switch representation.value.left { + case .rolling: + required.append((values[representation.value.packIndex[0]].1, 1)) + default: + required.append((values[representation.value.packIndex[0]].1, .max)) + } + switch representation.value.center { + case .rolling: + required.append((values[representation.value.packIndex[1]].1, 1)) + default: + required.append((values[representation.value.packIndex[1]].1, .max)) + } + switch representation.value.right { + case .rolling: + required.append((values[representation.value.packIndex[2]].1, 1)) + default: + required.append((values[representation.value.packIndex[2]].1, .max)) + } + required.append((values[2].1, 1)) + return required + } |> filter { !$0.contains(where: { $0.0 == nil} ) } |> map { $0.map { ($0.0!, $0.1) }} |> mapToSignal { data in + return fetchCachedSlotRepresentation(account: account, data: data, representation: representation) + } + } else { + return .complete() + } + } else if let representation = representation as? CachedBlurredWallpaperRepresentation { + return account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false)) + |> mapToSignal { data -> Signal in + if !data.complete { + return .complete() + } + return fetchCachedBlurredWallpaperRepresentation(account: account, resource: resource, resourceData: data, representation: representation) + } + } else if let representation = representation as? CachedPatternWallpaperMaskRepresentation { + return account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false)) + |> mapToSignal { data -> Signal in + if !data.complete { + return .complete() + } + return fetchCachedPatternWallpaperMaskRepresentation(resource: resource, resourceData: data, representation: representation) + } + } else if let resource = resource as? MapSnapshotMediaResource, let _ = representation as? MapSnapshotMediaResourceRepresentation { + return fetchMapSnapshotResource(resource: resource) } + + + + return .never() } -private func accountRecordIdPathName(_ id: AccountRecordId) -> String { - return "account-\(UInt64(bitPattern: id.int64))" + +public func fetchCachedSharedResourceRepresentation(accountManager: AccountManager, resource: MediaResource, representation: CachedMediaResourceRepresentation) -> Signal { + fatalError() } -private func fetchCachedStickerAJpegRepresentation(account: Account, resource: MediaResource, resourceData: MediaResourceData, representation: CachedStickerAJpegRepresentation) -> Signal { + +private func fetchCachedPatternWallpaperMaskRepresentation(resource: MediaResource, resourceData: MediaResourceData, representation: CachedPatternWallpaperMaskRepresentation) -> Signal { return Signal({ subscriber in if let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) { - let image = convertFromWebP(data)?.takeRetainedValue() - let appGroupName = "6N38VWS5BX.ru.keepcoder.Telegram" - if let image = image, let containerUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName) { - var randomId: Int64 = 0 - arc4random_buf(&randomId, 8) - // + + var svgPath: String? + + let path = NSTemporaryDirectory() + "\(arc4random64())" + let url = URL(fileURLWithPath: path) + + if let data = TGGUnzipData(data, 8 * 1024 * 1024), data.count > 5, let string = String(data: data.subdata(in: 0 ..< 5), encoding: .utf8), string == " runOn(Queue.concurrentDefaultQueue()) +} + + +private func accountRecordIdPathName(_ id: AccountRecordId) -> String { + return "account-\(UInt64(bitPattern: id.int64))" +} + + +public enum FetchVideoFirstFrameError { + case generic +} + + +private func videoFirstFrameData(account: Account, resource: MediaResource, chunkSize: Int) -> Signal { + if let size = resource.size { + return account.postbox.mediaBox.resourceData(resource, size: size, in: 0 ..< min(size, chunkSize)) + |> mapToSignal { _ -> Signal in + return account.postbox.mediaBox.resourceData(resource, option: .incremental(waitUntilFetchStatus: false), attemptSynchronously: false) + |> mapToSignal { data -> Signal in + return fetchCachedVideoFirstFrameRepresentation(account: account, resource: resource, resourceData: data) + |> `catch` { _ -> Signal in + if chunkSize > size { + return .complete() + } else { + return videoFirstFrameData(account: account, resource: resource, chunkSize: chunkSize + chunkSize) + } + } + } + } + } else { + return .complete() + } +} + + + +private func fetchCachedVideoFirstFrameRepresentation(account: Account, resource: MediaResource, resourceData: MediaResourceData) -> Signal { + return Signal { subscriber in + let tempFilePath = NSTemporaryDirectory() + "\(arc4random()).mov" + do { + let _ = try? FileManager.default.removeItem(atPath: tempFilePath) + try FileManager.default.createSymbolicLink(atPath: tempFilePath, withDestinationPath: resourceData.path) + + let asset = AVAsset(url: URL(fileURLWithPath: tempFilePath)) + let imageGenerator = AVAssetImageGenerator(asset: asset) + imageGenerator.maximumSize = CGSize(width: 800.0, height: 800.0) + imageGenerator.appliesPreferredTrackTransform = true + + let fullSizeImage = try imageGenerator.copyCGImage(at: CMTime(seconds: 0.0, preferredTimescale: asset.duration.timescale), actualTime: nil) + + + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + let path = NSTemporaryDirectory() + "\(randomId)" + let url = URL(fileURLWithPath: path) + + if let colorDestination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypeJPEG, 1, nil) { + CGImageDestinationSetProperties(colorDestination, [:] as CFDictionary) + + let colorQuality: Float = 0.6 + let options = NSMutableDictionary() + options.setObject(colorQuality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString) - let size:CGSize - if let s = representation.size { - size = s + CGImageDestinationAddImage(colorDestination, fullSizeImage, options as CFDictionary) + if CGImageDestinationFinalize(colorDestination) { + subscriber.putNext(.temporaryPath(path)) + subscriber.putCompletion() + } + } + } catch { + subscriber.putError(.generic) + subscriber.putCompletion() + } + let _ = try? FileManager.default.removeItem(atPath: tempFilePath) + return EmptyDisposable + } |> runOn(cacheThreadPool) +} + + +private func fetchCachedAnimatedStickerRepresentation(account: Account, resource: MediaResource, representation: CachedAnimatedStickerRepresentation) -> Signal { + + let data: Signal + if let resource = resource as? LocalBundleResource { + data = Signal { subscriber in + if let path = Bundle.main.path(forResource: resource.name, ofType: resource.ext), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) { + subscriber.putNext(MediaResourceData(path: path, offset: 0, size: data.count, complete: true)) + subscriber.putCompletion() + } + return EmptyDisposable + } + } else { + data = account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false)) + } + + return data |> deliverOn(lottieThreadPool) |> map { resourceData -> (CGImage?, Data?, MediaResourceData) in + if resourceData.complete { + if let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) { + if !representation.thumb { + var dataValue: Data! = TGGUnzipData(data, 8 * 1024 * 1024) + if dataValue == nil { + dataValue = data + } + if let json = String(data: transformedWithFitzModifier(data: dataValue, fitzModifier: representation.fitzModifier), encoding: .utf8), json.length > 0 { + let rlottie = RLottieBridge(json: json, key: resourceData.path) + + let unmanaged = rlottie?.renderFrame(0, width: Int(representation.size.width * 2), height: Int(representation.size.height * 2)) + let colorImage = unmanaged?.takeRetainedValue() + return (colorImage, nil, resourceData) + } } else { - size = CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale) + return (nil, data, resourceData) } + } + } + return (nil, nil, resourceData) + } |> runOn(cacheThreadPool) |> mapToSignal { frame, data, resourceData in + if resourceData.complete { + if !representation.thumb { + let path = NSTemporaryDirectory() + "\(arc4random64())" + let url = URL(fileURLWithPath: path) - let colorImage: CGImage - if let _ = representation.size { - colorImage = generateImage(size, contextGenerator: { size, context in - context.setBlendMode(.copy) - context.draw(image, in: CGRect(origin: CGPoint(), size: size)) - })! + let colorData = NSMutableData() + if let colorImage = frame, let colorDestination = CGImageDestinationCreateWithData(colorData as CFMutableData, kUTTypePNG, 1, nil){ + CGImageDestinationSetProperties(colorDestination, [:] as CFDictionary) + + let colorQuality: Float + colorQuality = 0.4 + + let options = NSMutableDictionary() + options.setObject(colorQuality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString) + CGImageDestinationAddImage(colorDestination, colorImage, options as CFDictionary) + if CGImageDestinationFinalize(colorDestination) { + try? colorData.write(to: url, options: .atomic) + return .single(.temporaryPath(path)) + } } else { - colorImage = image + return .complete() } + } else if let data = data { + + let path = NSTemporaryDirectory() + "\(arc4random64())" + let url = URL(fileURLWithPath: path) + + let colorData = NSMutableData() + var image = convertFromWebP(data)?._cgImage ?? NSImage(data: data)?.cgImage(forProposedRect: nil, context: nil, hints: nil) - let alphaImage = generateImage(size, contextGenerator: { size, context in - context.setFillColor(NSColor.white.cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - context.clip(to: CGRect(origin: CGPoint(), size: size), mask: colorImage) - context.setFillColor(NSColor.black.cgColor) - context.fill(CGRect(origin: CGPoint(), size: size)) - }) + if image == nil, let data = TGGUnzipData(data, 8 * 1024 * 1024) { + if let json = String(data: transformedWithFitzModifier(data: data, fitzModifier: representation.fitzModifier), encoding: .utf8), json.length > 0 { + let rlottie = RLottieBridge(json: json, key: resourceData.path) + let unmanaged = rlottie?.renderFrame(0, width: Int(representation.size.width * 2), height: Int(representation.size.height * 2)) + image = unmanaged?.takeRetainedValue() + } + } else if image != nil { + let webp = WebPImageDecoder(data: data, scale: 2.0) + let fullSizeWebp = webp?.frame(at: 0, decodeForDisplay: true)?.image?._cgImage + image = fullSizeWebp ?? image + } - if let alphaImage = alphaImage, let colorDestination = CGImageDestinationCreateWithData(colorData as CFMutableData, kUTTypeJPEG, 1, nil), let alphaDestination = CGImageDestinationCreateWithData(alphaData as CFMutableData, kUTTypeJPEG, 1, nil) { + if let image = image, let colorDestination = CGImageDestinationCreateWithData(colorData as CFMutableData, kUTTypePNG, 1, nil) { CGImageDestinationSetProperties(colorDestination, [:] as CFDictionary) - CGImageDestinationSetProperties(alphaDestination, [:] as CFDictionary) let colorQuality: Float - let alphaQuality: Float - if representation.size == nil { - colorQuality = 0.6 - alphaQuality = 0.6 - } else { - colorQuality = 0.5 - alphaQuality = 0.4 - } + colorQuality = 0.4 let options = NSMutableDictionary() options.setObject(colorQuality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString) + CGImageDestinationAddImage(colorDestination, image, options as CFDictionary) + if CGImageDestinationFinalize(colorDestination) { + try? colorData.write(to: url, options: .atomic) + return .single(.temporaryPath(path)) + } + } else { + return .complete() + } + } + } + return .never() + } +} + +private func fetchCachedStickerAJpegRepresentation(account: Account, resource: MediaResource, representation: CachedStickerAJpegRepresentation) -> Signal { + + let signal: Signal + + if resource is LocalFileReferenceMediaResource { + signal = .single(nil) + } else { + signal = account.postbox.mediaBox.resourceData(resource) |> map(Optional.init) + } + + return signal |> mapToSignal { resourceData in + return Signal { subscriber in + + let complete: Bool + let resourcePath: String? + if let resourceData = resourceData { + resourcePath = resourceData.path + complete = resourceData.complete + } else if let resource = resource as? LocalFileReferenceMediaResource { + resourcePath = resource.localFilePath + complete = true + } else { + resourcePath = nil + complete = false + } + + if let resourcePath = resourcePath, let data = try? Data(contentsOf: URL(fileURLWithPath: resourcePath), options: [.mappedIfSafe]), complete { + let image = convertFromWebP(data)?._cgImage ?? NSImage(data: data)?.cgImage(forProposedRect: nil, context: nil, hints: nil) + if let image = image, let containerUrl = ApiEnvironment.containerURL { + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + let directory = "\(containerUrl.path)/\(accountRecordIdPathName(account.id))/cached/" + try? FileManager.default.createDirectory(at: URL(fileURLWithPath: directory), withIntermediateDirectories: true, attributes: nil) + let path: String = directory + "\(randomId)" + let url = URL(fileURLWithPath: path) - let optionsAlpha = NSMutableDictionary() - optionsAlpha.setObject(alphaQuality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString) + let colorData = NSMutableData() - CGImageDestinationAddImage(colorDestination, colorImage, options as CFDictionary) - CGImageDestinationAddImage(alphaDestination, alphaImage, optionsAlpha as CFDictionary) - if CGImageDestinationFinalize(colorDestination) && CGImageDestinationFinalize(alphaDestination) { - let finalData = NSMutableData() - var colorSize: Int32 = Int32(colorData.length) - finalData.append(&colorSize, length: 4) - finalData.append(colorData as Data) - var alphaSize: Int32 = Int32(alphaData.length) - finalData.append(&alphaSize, length: 4) - finalData.append(alphaData as Data) - - let _ = try? finalData.write(to: url, options: [.atomic]) - - subscriber.putNext(CachedMediaResourceRepresentationResult(temporaryPath: path)) + + let size:CGSize + if let s = representation.size { + size = s + } else { + size = CGSize(width: image.size.width * image.scale, height: image.size.height * image.scale) + } + + let colorImage: CGImage + if let _ = representation.size { + colorImage = generateImage(size, contextGenerator: { size, context in + context.setBlendMode(.copy) + context.draw(image, in: CGRect(origin: CGPoint(), size: size)) + })! + } else { + colorImage = image + } + if let colorDestination = CGImageDestinationCreateWithData(colorData as CFMutableData, kUTTypePNG, 1, nil) { + CGImageDestinationSetProperties(colorDestination, [:] as CFDictionary) + + let colorQuality: Float + if representation.size == nil { + colorQuality = 0.6 + } else { + colorQuality = 0.3 + } + + let options = NSMutableDictionary() + options.setObject(colorQuality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString) + + CGImageDestinationAddImage(colorDestination, colorImage, options as CFDictionary) + if CGImageDestinationFinalize(colorDestination) { + let _ = try? colorData.write(to: url, options: [.atomic]) + subscriber.putNext(.temporaryPath(path)) + subscriber.putCompletion() + } + } else { subscriber.putCompletion() } } else { subscriber.putCompletion() } - + } + return EmptyDisposable + } |> runOn(cacheThreadPool) + } +} + +private func fetchCachedScaledImageRepresentation(account: Account, resource: MediaResource, representation: CachedScaledImageRepresentation) -> Signal { + return account.postbox.mediaBox.resourceData(resource) |> mapToSignal { resourceData in + return Signal { subscriber in + if let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) { + if let image = NSImage(data: data) { + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + let path = NSTemporaryDirectory() + "\(randomId)" + let url = URL(fileURLWithPath: path) + + let size = representation.size + + let colorImage = generateImage(size, contextGenerator: { size, context in + context.setBlendMode(.copy) + if let image = image.cgImage(forProposedRect: nil, context: nil, hints: nil) { + context.draw(image, in: CGRect(origin: CGPoint(), size: size)) + } + })! + + if let colorDestination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypeJPEG, 1, nil) { + CGImageDestinationSetProperties(colorDestination, nil) + + let colorQuality: Float = 0.5 + + let options = NSMutableDictionary() + options.setObject(colorQuality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString) + + CGImageDestinationAddImage(colorDestination, colorImage, options as CFDictionary) + if CGImageDestinationFinalize(colorDestination) { + subscriber.putNext(.temporaryPath(path)) + subscriber.putCompletion() + } + } + } + } + return EmptyDisposable + } |> runOn(cacheThreadPool) + } +} + +private func fetchCachedVideoFirstFrameRepresentation(account: Account, resource: MediaResource, representation: CachedVideoFirstFrameRepresentation) -> Signal { + return account.postbox.mediaBox.resourceRangesStatus(resource) |> mapToSignal { _ in + return account.postbox.mediaBox.resourceData(resource) |> take(1) + } |> runOn(cacheThreadPool) |> mapToSignal { resourceData in + let tempFilePath = NSTemporaryDirectory() + "\(resourceData.path.nsstring.lastPathComponent).mp4" + let _ = try? FileManager.default.removeItem(atPath: tempFilePath) + try? FileManager.default.createSymbolicLink(atPath: resourceData.path, withDestinationPath: tempFilePath) + + let asset = AVAsset(url: URL(fileURLWithPath: tempFilePath)) + let imageGenerator = AVAssetImageGenerator(asset: asset) + imageGenerator.maximumSize = CGSize(width: 800.0, height: 800.0) + imageGenerator.appliesPreferredTrackTransform = true + let fullSizeImage = try? imageGenerator.copyCGImage(at: CMTime(seconds: 0.0, preferredTimescale: asset.duration.timescale), actualTime: nil) + + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + let path = NSTemporaryDirectory() + "\(randomId)" + let url = URL(fileURLWithPath: path) + + if let fullSizeImage = fullSizeImage, let colorDestination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypeJPEG, 1, nil) { + CGImageDestinationSetProperties(colorDestination, nil) + + let colorQuality: Float = 0.6 + + let options = NSMutableDictionary() + options.setObject(colorQuality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString) + + CGImageDestinationAddImage(colorDestination, fullSizeImage, options as CFDictionary) + if CGImageDestinationFinalize(colorDestination) { + return .single(.temporaryPath(path)) } } - return EmptyDisposable - }) |> runOn(account.graphicsThreadPool) + return .never() + } +} + + +private func fetchCachedScaledVideoFirstFrameRepresentation(account: Account, resource: MediaResource, representation: CachedScaledVideoFirstFrameRepresentation) -> Signal { + return account.postbox.mediaBox.cachedResourceRepresentation(resource, representation: CachedVideoFirstFrameRepresentation(), complete: true) |> mapToSignal { firstFrame -> Signal in + return Signal({ subscriber in + if let data = try? Data(contentsOf: URL(fileURLWithPath: firstFrame.path), options: [.mappedIfSafe]) { + if let image = NSImage(data: data)?.cgImage(forProposedRect: nil, context: nil, hints: nil) { + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + let path = NSTemporaryDirectory() + "\(randomId)" + let url = URL(fileURLWithPath: path) + + let size = representation.size + + let colorImage = generateImage(size, contextGenerator: { size, context in + context.setBlendMode(.copy) + context.draw(image, in: CGRect(origin: CGPoint(), size: size)) + })! + + if let colorDestination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypeJPEG, 1, nil) { + CGImageDestinationSetProperties(colorDestination, nil) + + let colorQuality: Float = 0.5 + + let options = NSMutableDictionary() + options.setObject(colorQuality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString) + + CGImageDestinationAddImage(colorDestination, colorImage, options as CFDictionary) + if CGImageDestinationFinalize(colorDestination) { + subscriber.putNext(.temporaryPath(path)) + subscriber.putCompletion() + } + } + } + } + return EmptyDisposable + }) |> runOn(cacheThreadPool) + } } -private func fetchCachedScaledImageRepresentation(account: Account, resource: MediaResource, resourceData: MediaResourceData, representation: CachedScaledImageRepresentation) -> Signal { + + +private func fetchCachedBlurredWallpaperRepresentation(account: Account, resource: MediaResource, resourceData: MediaResourceData, representation: CachedBlurredWallpaperRepresentation) -> Signal { return Signal({ subscriber in if let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) { - if let image = NSImage(data: data) { + if let image = NSImage(data: data)?.cgImage(forProposedRect: nil, context: nil, hints: nil) { var randomId: Int64 = 0 arc4random_buf(&randomId, 8) let path = NSTemporaryDirectory() + "\(randomId)" let url = URL(fileURLWithPath: path) - let size = representation.size - - let colorImage = generateImage(size, contextGenerator: { size, context in - context.setBlendMode(.copy) - context.draw(image.precomposed(), in: CGRect(origin: CGPoint(), size: size)) - })! - - if let colorDestination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypeJPEG, 1, nil) { - CGImageDestinationSetProperties(colorDestination, nil) + if let colorImage = blurredImage(image, radius: 70), let colorDestination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypeJPEG, 1, nil) { + CGImageDestinationSetProperties(colorDestination, [:] as CFDictionary) let colorQuality: Float = 0.5 @@ -145,57 +594,135 @@ private func fetchCachedScaledImageRepresentation(account: Account, resource: Me CGImageDestinationAddImage(colorDestination, colorImage, options as CFDictionary) if CGImageDestinationFinalize(colorDestination) { - subscriber.putNext(CachedMediaResourceRepresentationResult(temporaryPath: path)) + subscriber.putNext(.temporaryPath(path)) subscriber.putCompletion() } } } } return EmptyDisposable - }) |> runOn(account.graphicsThreadPool) + }) |> runOn(cacheThreadPool) } -private func fetchCachedVideoFirstFrameRepresentation(account: Account, resource: MediaResource, resourceData: MediaResourceData, representation: CachedVideoFirstFrameRepresentation) -> Signal { +private func fetchCachedSlotRepresentation(account: Account, data: [(Data, Int32)], representation: CachedSlotMachineRepresentation) -> Signal { return Signal { subscriber in - if resourceData.complete { - let tempFilePath = NSTemporaryDirectory() + "\(arc4random()).mov" + + var images: [CGImage] = [] + for (data, frame) in data { + var dataValue: Data! = TGGUnzipData(data, 8 * 1024 * 1024) + if dataValue == nil { + dataValue = data + } + if let json = String(data: dataValue, encoding: .utf8) { + let rlottie = RLottieBridge(json: json, key: "\(arc4random())") + if let rlottie = rlottie { + let unmanaged = rlottie.renderFrame(Int32.max == frame ? rlottie.endFrame() - 1 : frame, width: Int(representation.size.width * 2), height: Int(representation.size.height * 2)) + let colorImage = unmanaged.takeRetainedValue() + images.append(colorImage) + + } + } + } + + let colorImage = generateImage(NSMakeSize(representation.size.width * 2, representation.size.height * 2), contextGenerator: { size, ctx in + ctx.clear(CGRect(origin: .zero, size: size)) + for image in images { + ctx.draw(image, in: CGRect(origin: .zero, size: size)) + } + }, scale: 1.0)! + + + let path = NSTemporaryDirectory() + "\(arc4random64())" + let url = URL(fileURLWithPath: path) + + let colorData = NSMutableData() + if let colorDestination = CGImageDestinationCreateWithData(colorData as CFMutableData, kUTTypePNG, 1, nil){ + CGImageDestinationSetProperties(colorDestination, [:] as CFDictionary) + let colorQuality: Float + colorQuality = 0.4 + let options = NSMutableDictionary() + options.setObject(colorQuality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString) + CGImageDestinationAddImage(colorDestination, colorImage, options as CFDictionary) + if CGImageDestinationFinalize(colorDestination) { + try? colorData.write(to: url, options: .atomic) + subscriber.putNext(.temporaryPath(path)) + subscriber.putCompletion() + } + } else { + subscriber.putCompletion() + } + + + return ActionDisposable { - do { - let _ = try? FileManager.default.removeItem(atPath: tempFilePath) - try FileManager.default.linkItem(atPath: resourceData.path, toPath: tempFilePath) - - let asset = AVAsset(url: URL(fileURLWithPath: tempFilePath)) - let imageGenerator = AVAssetImageGenerator(asset: asset) - imageGenerator.maximumSize = CGSize(width: 800.0, height: 800.0) - imageGenerator.appliesPreferredTrackTransform = true - let fullSizeImage = try imageGenerator.copyCGImage(at: CMTime(seconds: 0.0, preferredTimescale: asset.duration.timescale), actualTime: nil) + } + } |> runOn(lottieThreadPool) +} + +private func fetchCachedDiceRepresentation(account: Account, data: Data, representation: CachedDiceRepresentation) -> Signal { + return Signal { subscriber in + + var dataValue: Data! = TGGUnzipData(data, 8 * 1024 * 1024) + if dataValue == nil { + dataValue = data + } + if let json = String(data: dataValue, encoding: .utf8) { + let rlottie = RLottieBridge(json: json, key: representation.emoji + representation.value) + if let rlottie = rlottie { + let unmanaged = rlottie.renderFrame(representation.value == diceIdle ? 0 : rlottie.endFrame() - 1, width: Int(representation.size.width * 2), height: Int(representation.size.height * 2)) + let colorImage = unmanaged.takeRetainedValue() - var randomId: Int64 = 0 - arc4random_buf(&randomId, 8) - let path = NSTemporaryDirectory() + "\(randomId)" + let path = NSTemporaryDirectory() + "\(arc4random64())" let url = URL(fileURLWithPath: path) - if let colorDestination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypeJPEG, 1, nil) { - CGImageDestinationSetProperties(colorDestination, nil) - - let colorQuality: Float = 0.6 - + let colorData = NSMutableData() + if let colorDestination = CGImageDestinationCreateWithData(colorData as CFMutableData, kUTTypePNG, 1, nil){ + CGImageDestinationSetProperties(colorDestination, [:] as CFDictionary) + let colorQuality: Float + colorQuality = 0.4 let options = NSMutableDictionary() options.setObject(colorQuality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString) - - CGImageDestinationAddImage(colorDestination, fullSizeImage, options as CFDictionary) - if CGImageDestinationFinalize(colorDestination) { - subscriber.putNext(CachedMediaResourceRepresentationResult(temporaryPath: path)) + CGImageDestinationAddImage(colorDestination, colorImage, options as CFDictionary) + if CGImageDestinationFinalize(colorDestination) { + try? colorData.write(to: url, options: .atomic) + subscriber.putNext(.temporaryPath(path)) subscriber.putCompletion() } + } else { + subscriber.putCompletion() } - - subscriber.putNext(CachedMediaResourceRepresentationResult(temporaryPath: path)) + } else { subscriber.putCompletion() - } catch (let e) { - print("\(e)") } + } - return EmptyDisposable - } |> runOn(account.graphicsThreadPool) + + return ActionDisposable { + + } + } |> runOn(lottieThreadPool) +} + +func getAnimatedStickerThumb(data: Data, size: NSSize = NSMakeSize(512, 512)) -> Signal { + + return .single(data) |> deliverOn(lottieThreadPool) |> map { data -> String? in + var dataValue: Data! = TGGUnzipData(data, 8 * 1024 * 1024) + if dataValue == nil { + dataValue = data + } + if let json = String(data: transformedWithFitzModifier(data: dataValue, fitzModifier: nil), encoding: .utf8), json.length > 0 { + let rlottie = RLottieBridge(json: json, key: "\(arc4random())") + let unmanaged = rlottie?.renderFrame(0, width: Int(size.width), height: Int(size.height)) + let colorImage = unmanaged?.takeRetainedValue() + + if let image = colorImage { + let rep = NSBitmapImageRep(cgImage: image) + let data = rep.representation(using: .png, properties: [:]) + let path = NSTemporaryDirectory() + "temp_as_\(arc4random64()).png" + try? data?.write(to: URL(fileURLWithPath: path)) + return path + } + } + return nil + } |> deliverOnMainQueue } diff --git a/Telegram-Mac/FetchManager.swift b/Telegram-Mac/FetchManager.swift new file mode 100644 index 0000000000..30c4592883 --- /dev/null +++ b/Telegram-Mac/FetchManager.swift @@ -0,0 +1,487 @@ +import Foundation +import Postbox +import TelegramCore + +import SwiftSignalKit + +private struct FetchManagerLocationEntryId: Hashable { + let location: FetchManagerLocation + let resourceId: MediaResourceId + let locationKey: FetchManagerLocationKey + + static func ==(lhs: FetchManagerLocationEntryId, rhs: FetchManagerLocationEntryId) -> Bool { + if lhs.location != rhs.location { + return false + } + if !lhs.resourceId.isEqual(to: rhs.resourceId) { + return false + } + if lhs.locationKey != rhs.locationKey { + return false + } + return true + } + + var hashValue: Int { + return self.resourceId.hashValue &* 31 &+ self.locationKey.hashValue + } +} + +private final class FetchManagerLocationEntry { + let id: FetchManagerLocationEntryId + let episode: Int32 + let reference: MediaResourceReference + let fetchTag: MediaResourceStatsCategory + + var referenceCount: Int32 = 0 + var elevatedPriorityReferenceCount: Int32 = 0 + var userInitiatedPriorityIndices: [Int32] = [] + let downloadRange: Range? + var priorityKey: FetchManagerPriorityKey? { + if self.referenceCount > 0 { + return FetchManagerPriorityKey(locationKey: self.id.locationKey, hasElevatedPriority: self.elevatedPriorityReferenceCount > 0, userInitiatedPriority: userInitiatedPriorityIndices.last) + } else { + return nil + } + } + + init(id: FetchManagerLocationEntryId, episode: Int32, reference: MediaResourceReference, fetchTag: MediaResourceStatsCategory?, downloadRange: Range? = nil) { + self.id = id + self.episode = episode + self.reference = reference + self.fetchTag = fetchTag ?? .generic + self.downloadRange = downloadRange + } +} + +private final class FetchManagerActiveContext { + var disposable: Disposable? +} + +private final class FetchManagerStatusContext { + var disposable: Disposable? + var originalStatus: MediaResourceStatus? + var subscribers = Bag<(MediaResourceStatus) -> Void>() + + var hasEntry = false + + var isEmpty: Bool { + return !self.hasEntry && self.subscribers.isEmpty + } + + var combinedStatus: MediaResourceStatus? { + if let originalStatus = self.originalStatus { + if originalStatus == .Remote && self.hasEntry { + return .Fetching(isActive: false, progress: 0.0) + } else { + return originalStatus + } + } else { + return nil + } + } +} + +private final class FetchManagerCategoryContext { + private let postbox: Postbox + private let entryCompleted: (FetchManagerLocationEntryId, FetchResourceSourceType) -> Void + + private var topEntryIdAndPriority: (FetchManagerLocationEntryId, FetchManagerPriorityKey)? + private var entries: [FetchManagerLocationEntryId: FetchManagerLocationEntry] = [:] + + private var activeContexts: [FetchManagerLocationEntryId: FetchManagerActiveContext] = [:] + private var statusContexts: [FetchManagerLocationEntryId: FetchManagerStatusContext] = [:] + + init(postbox: Postbox, entryCompleted: @escaping (FetchManagerLocationEntryId, FetchResourceSourceType) -> Void) { + self.postbox = postbox + self.entryCompleted = entryCompleted + } + + func withEntry(id: FetchManagerLocationEntryId, downloadRange: Range? = nil, takeNew: (() -> (MediaResourceReference, MediaResourceStatsCategory?, Int32))?, _ f: (FetchManagerLocationEntry) -> Void) { + let entry: FetchManagerLocationEntry + let previousPriorityKey: FetchManagerPriorityKey? + + if let current = self.entries[id] { + entry = current + previousPriorityKey = entry.priorityKey + } else if let takeNew = takeNew { + previousPriorityKey = nil + let (reference, fetchTag, episode) = takeNew() + entry = FetchManagerLocationEntry(id: id, episode: episode, reference: reference, fetchTag: fetchTag, downloadRange: downloadRange) + self.entries[id] = entry + } else { + return + } + + f(entry) + + var removedEntries = false + + let updatedPriorityKey = entry.priorityKey + if previousPriorityKey != updatedPriorityKey { + if let updatedPriorityKey = updatedPriorityKey { + if let (topId, topPriority) = self.topEntryIdAndPriority { + if updatedPriorityKey < topPriority { + self.topEntryIdAndPriority = (entry.id, updatedPriorityKey) + } else if updatedPriorityKey > topPriority && topId == id { + self.topEntryIdAndPriority = nil + } + } else { + self.topEntryIdAndPriority = (entry.id, updatedPriorityKey) + } + } else { + if self.topEntryIdAndPriority?.0 == id { + self.topEntryIdAndPriority = nil + } + self.entries.removeValue(forKey: id) + removedEntries = true + } + } + + self.maybeFindAndActivateNewTopEntry() + + if removedEntries { + var removedIds: [FetchManagerLocationEntryId] = [] + for (entryId, activeContext) in self.activeContexts { + if self.entries[entryId] == nil { + removedIds.append(entryId) + activeContext.disposable?.dispose() + } + } + for entryId in removedIds { + self.activeContexts.removeValue(forKey: entryId) + } + } + + if let activeContext = self.activeContexts[id] { + if activeContext.disposable == nil { + if let entry = self.entries[id] { + let entryCompleted = self.entryCompleted + let range: (Range, MediaBoxFetchPriority)? + if let downloadRange = entry.downloadRange { + range = (downloadRange, .default) + } else { + range = nil + } + activeContext.disposable = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: entry.reference, range: range, statsCategory: entry.fetchTag, reportResultStatus: true).start(next: { value in + entryCompleted(id, value) + }) + } else { + assertionFailure() + } + } + } + + if (previousPriorityKey != nil) != (updatedPriorityKey != nil) { + if let statusContext = self.statusContexts[id] { + if updatedPriorityKey != nil { + if !statusContext.hasEntry { + let previousStatus = statusContext.combinedStatus + statusContext.hasEntry = true + if let combinedStatus = statusContext.combinedStatus, combinedStatus != previousStatus { + for f in statusContext.subscribers.copyItems() { + f(combinedStatus) + } + } + } else { + assertionFailure() + } + } else { + if statusContext.hasEntry { + let previousStatus = statusContext.combinedStatus + statusContext.hasEntry = false + if let combinedStatus = statusContext.combinedStatus, combinedStatus != previousStatus { + for f in statusContext.subscribers.copyItems() { + f(combinedStatus) + } + } + } else { + assertionFailure() + } + } + } + } + } + + func maybeFindAndActivateNewTopEntry() { + if !self.entries.isEmpty { + for (id, entry) in self.entries { + if activeContexts[id] == nil { + let activeContext = FetchManagerActiveContext() + self.activeContexts[id] = activeContext + let entryCompleted = self.entryCompleted + let range: (Range, MediaBoxFetchPriority)? + if let downloadRange = entry.downloadRange { + range = (downloadRange, .default) + } else { + range = nil + } + activeContext.disposable = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: entry.reference, range: range, statsCategory: entry.fetchTag, reportResultStatus: true).start(next: { value in + entryCompleted(id, value) + }) + } + } + } + + } + + func cancelEntry(_ entryId: FetchManagerLocationEntryId) { + var id: FetchManagerLocationEntryId = entryId + if self.entries[id] == nil { + for (key, _) in self.entries { + if key.resourceId.isEqual(to: entryId.resourceId) { + id = key + break + } + } + } + + if let _ = self.entries[id] { + self.entries.removeValue(forKey: id) + + if let statusContext = self.statusContexts[id] { + if statusContext.hasEntry { + let previousStatus = statusContext.combinedStatus + statusContext.hasEntry = false + if let combinedStatus = statusContext.combinedStatus, combinedStatus != previousStatus { + for f in statusContext.subscribers.copyItems() { + f(combinedStatus) + } + } + } else { + assertionFailure() + } + } + } + + if let activeContext = self.activeContexts[id] { + activeContext.disposable?.dispose() + activeContext.disposable = nil + self.activeContexts.removeValue(forKey: id) + } + + if self.topEntryIdAndPriority?.0 == id { + self.topEntryIdAndPriority = nil + } + + self.maybeFindAndActivateNewTopEntry() + } + + func withFetchStatusContext(_ id: FetchManagerLocationEntryId, _ f: (FetchManagerStatusContext) -> Void) { + let statusContext: FetchManagerStatusContext + if let current = self.statusContexts[id] { + statusContext = current + } else { + statusContext = FetchManagerStatusContext() + self.statusContexts[id] = statusContext + if self.entries[id] != nil { + statusContext.hasEntry = true + } + } + + f(statusContext) + + if statusContext.isEmpty { + statusContext.disposable?.dispose() + self.statusContexts.removeValue(forKey: id) + } + } + + var isEmpty: Bool { + return self.entries.isEmpty && self.activeContexts.isEmpty && self.statusContexts.isEmpty + } +} + +final class FetchManager { + private let queue = Queue() + private let postbox: Postbox + private var nextEpisodeId: Int32 = 0 + private var nextUserInitiatedIndex: Int32 = 0 + + private var categoryContexts: [FetchManagerCategory: FetchManagerCategoryContext] = [:] + + init(postbox: Postbox) { + self.postbox = postbox + } + + private func takeNextEpisodeId() -> Int32 { + let value = self.nextEpisodeId + self.nextEpisodeId += 1 + return value + } + + private func takeNextUserInitiatedIndex() -> Int32 { + let value = self.nextUserInitiatedIndex + self.nextUserInitiatedIndex += 1 + return value + } + + private func withCategoryContext(_ key: FetchManagerCategory, _ f: (FetchManagerCategoryContext) -> Void) { + assert(self.queue.isCurrent()) + let context: FetchManagerCategoryContext + if let current = self.categoryContexts[key] { + context = current + } else { + let queue = self.queue + context = FetchManagerCategoryContext(postbox: self.postbox, entryCompleted: { [weak self] id, source in + queue.async { + if let strongSelf = self { + let postbox = strongSelf.postbox + switch source { + case .remote: + switch id.locationKey { + case let .messageId(messageId): + _ = (strongSelf.postbox.messageAtId(messageId) |> map { $0?.media.first as? TelegramMediaFile} |> filter {$0 != nil} |> map {$0!} |> mapToSignal { file -> Signal in + if !file.isMusic && !file.isAnimated && !file.isVideo && !file.isVoice && !file.isInstantVideo && !file.isAnimatedSticker && !file.isStaticSticker { + return copyToDownloads(file, postbox: postbox) |> map { _ in } + } + return .single(Void()) + }).start() + default: + break + } + default: + break + } + strongSelf.withCategoryContext(key, { context in + context.cancelEntry(id) + }) + } + } + }) + self.categoryContexts[key] = context + } + + f(context) + + if context.isEmpty { + self.categoryContexts.removeValue(forKey: key) + } + } + + func interactivelyFetched(category: FetchManagerCategory, location: FetchManagerLocation, locationKey: FetchManagerLocationKey, downloadRange: Range? = nil, reference: MediaResourceReference, fetchTag: MediaResourceStatsCategory?, elevatedPriority: Bool, userInitiated: Bool) -> Signal { + let queue = self.queue + return Signal { [weak self] subscriber in + if let strongSelf = self { + var assignedEpisode: Int32? + var assignedUserInitiatedIndex: Int32? + + strongSelf.withCategoryContext(category, { context in + context.withEntry(id: FetchManagerLocationEntryId(location: location, resourceId: reference.resource.id, locationKey: locationKey), downloadRange: downloadRange, takeNew: { return (reference, fetchTag, strongSelf.takeNextEpisodeId()) }, { entry in + assignedEpisode = entry.episode + entry.referenceCount += 1 + if elevatedPriority { + entry.elevatedPriorityReferenceCount += 1 + } + if userInitiated { + let userInitiatedIndex = strongSelf.takeNextUserInitiatedIndex() + assignedUserInitiatedIndex = userInitiatedIndex + entry.userInitiatedPriorityIndices.append(userInitiatedIndex) + entry.userInitiatedPriorityIndices.sort() + } + }) + }) + + return ActionDisposable { + queue.async { + if let strongSelf = self { + strongSelf.withCategoryContext(category, { context in + context.withEntry(id: FetchManagerLocationEntryId(location: location, resourceId: reference.resource.id, locationKey: locationKey), downloadRange: downloadRange, takeNew: nil, { entry in + if entry.episode == assignedEpisode { + entry.referenceCount -= 1 + assert(entry.referenceCount >= 0) + if elevatedPriority { + entry.elevatedPriorityReferenceCount -= 1 + assert(entry.elevatedPriorityReferenceCount >= 0) + } + if let userInitiatedIndex = assignedUserInitiatedIndex { + if let index = entry.userInitiatedPriorityIndices.index(of: userInitiatedIndex) { + entry.userInitiatedPriorityIndices.remove(at: index) + } else { + assertionFailure() + } + } + } + }) + }) + } + } + } + } else { + return EmptyDisposable + } + } |> runOn(self.queue) + } + + func cancelInteractiveFetches(category: FetchManagerCategory, location: FetchManagerLocation, locationKey: FetchManagerLocationKey, reference: MediaResourceReference) { + self.queue.async { + self.withCategoryContext(category, { context in + context.cancelEntry(FetchManagerLocationEntryId(location: location, resourceId: reference.resource.id, locationKey: locationKey)) + }) + } + } + + func fetchStatus(category: FetchManagerCategory, location: FetchManagerLocation, locationKey: FetchManagerLocationKey, reference: MediaResourceReference) -> Signal { + let queue = self.queue + return Signal { [weak self] subscriber in + if let strongSelf = self { + var assignedIndex: Int? + + let entryId = FetchManagerLocationEntryId(location: location, resourceId: reference.resource.id, locationKey: locationKey) + strongSelf.withCategoryContext(category, { context in + context.withFetchStatusContext(entryId, { statusContext in + assignedIndex = statusContext.subscribers.add({ status in + subscriber.putNext(status) + if case .Local = status { + subscriber.putCompletion() + } + }) + if let status = statusContext.combinedStatus { + subscriber.putNext(status) + if case .Local = status { + subscriber.putCompletion() + } + } + if statusContext.disposable == nil { + statusContext.disposable = strongSelf.postbox.mediaBox.resourceStatus(reference.resource).start(next: { status in + queue.async { + if let strongSelf = self { + strongSelf.withCategoryContext(category, { context in + context.withFetchStatusContext(entryId, { statusContext in + statusContext.originalStatus = status + + + + if let combinedStatus = statusContext.combinedStatus { + for f in statusContext.subscribers.copyItems() { + f(combinedStatus) + } + } + }) + }) + } + } + }) + } + }) + }) + + return ActionDisposable { + queue.async { + if let strongSelf = self { + strongSelf.withCategoryContext(category, { context in + context.withFetchStatusContext(entryId, { statusContext in + if let assignedIndex = assignedIndex { + statusContext.subscribers.remove(assignedIndex) + } + }) + }) + } + } + } + } else { + return EmptyDisposable + } + } |> runOn(self.queue) + } +} diff --git a/Telegram-Mac/FetchManagerLocation.swift b/Telegram-Mac/FetchManagerLocation.swift new file mode 100644 index 0000000000..4481192496 --- /dev/null +++ b/Telegram-Mac/FetchManagerLocation.swift @@ -0,0 +1,126 @@ + +import Foundation +import Postbox + +enum FetchManagerCategory: Int32 { + case image + case file +} + +enum FetchManagerLocationKey: Comparable, Hashable { + case messageId(MessageId) + case free + + static func ==(lhs: FetchManagerLocationKey, rhs: FetchManagerLocationKey) -> Bool { + switch lhs { + case let .messageId(id): + if case .messageId(id) = rhs { + return true + } else { + return false + } + case .free: + if case .free = rhs { + return true + } else { + return false + } + } + } + + static func <(lhs: FetchManagerLocationKey, rhs: FetchManagerLocationKey) -> Bool { + switch lhs { + case let .messageId(lhsId): + if case let .messageId(rhsId) = rhs { + return lhsId < rhsId + } else { + return true + } + case .free: + if case .free = rhs { + return false + } else { + return false + } + } + } + + var hashValue: Int { + switch self { + case let .messageId(id): + return id.hashValue + case .free: + return 1 + } + } +} + +struct FetchManagerPriorityKey: Comparable { + let locationKey: FetchManagerLocationKey + let hasElevatedPriority: Bool + let userInitiatedPriority: Int32? + + static func ==(lhs: FetchManagerPriorityKey, rhs: FetchManagerPriorityKey) -> Bool { + if lhs.locationKey != rhs.locationKey { + return false + } + if lhs.hasElevatedPriority != rhs.hasElevatedPriority { + return false + } + if lhs.userInitiatedPriority != rhs.userInitiatedPriority { + return false + } + return true + } + + static func <(lhs: FetchManagerPriorityKey, rhs: FetchManagerPriorityKey) -> Bool { + if let lhsUserInitiatedPriority = lhs.userInitiatedPriority, let rhsUserInitiatedPriority = rhs.userInitiatedPriority { + if lhsUserInitiatedPriority != rhsUserInitiatedPriority { + if lhsUserInitiatedPriority > rhsUserInitiatedPriority { + return false + } else { + return true + } + } + } else if (lhs.userInitiatedPriority != nil) != (rhs.userInitiatedPriority != nil) { + if lhs.userInitiatedPriority != nil { + return false + } else { + return true + } + } + + if lhs.hasElevatedPriority != rhs.hasElevatedPriority { + if lhs.hasElevatedPriority { + return false + } else { + return true + } + } + + return lhs.locationKey < rhs.locationKey + } +} + +enum FetchManagerLocation: Hashable { + case chat(PeerId) + + static func ==(lhs: FetchManagerLocation, rhs: FetchManagerLocation) -> Bool { + switch lhs { + case let .chat(peerId): + if case .chat(peerId) = rhs { + return true + } else { + return false + } + } + } + + var hashValue: Int { + switch self { + case let .chat(peerId): + return peerId.hashValue + } + } +} + diff --git a/Telegram-Mac/FetchMediaUtils.swift b/Telegram-Mac/FetchMediaUtils.swift new file mode 100644 index 0000000000..18264584ea --- /dev/null +++ b/Telegram-Mac/FetchMediaUtils.swift @@ -0,0 +1,28 @@ + +import Cocoa +import TelegramCore + +import Postbox +import SwiftSignalKit + +func freeMediaFileInteractiveFetched(context: AccountContext, fileReference: FileMediaReference, range: Range? = nil) -> Signal { + return fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: fileReference.resourceReference(fileReference.media.resource), range: range != nil ? (range!, .default) : nil, statsCategory: fileReference.media.isVideo ? .video : .file) |> `catch` { _ in return .complete() } +} + +func cancelFreeMediaFileInteractiveFetch(context: AccountContext, resource: MediaResource) { + context.account.postbox.mediaBox.cancelInteractiveResourceFetch(resource) +} + +func messageMediaFileInteractiveFetched(context: AccountContext, messageId: MessageId, fileReference: FileMediaReference, range: Range? = nil) -> Signal { + return context.fetchManager.interactivelyFetched(category: .file, location: .chat(messageId.peerId), locationKey: .messageId(messageId), downloadRange: range, reference: fileReference.resourceReference(fileReference.media.resource), fetchTag: fileReference.media.isVideo ? .video : .file, elevatedPriority: false, userInitiated: true) +} + + +func messageMediaFileCancelInteractiveFetch(context: AccountContext, messageId: MessageId, fileReference: FileMediaReference) { + context.fetchManager.cancelInteractiveFetches(category: .file, location: .chat(messageId.peerId), locationKey: .messageId(messageId), reference: fileReference.resourceReference(fileReference.media.resource)) + +} + +func messageMediaFileStatus(context: AccountContext, messageId: MessageId, fileReference: FileMediaReference) -> Signal { + return context.fetchManager.fetchStatus(category: .file, location: .chat(messageId.peerId), locationKey: .messageId(messageId), reference: fileReference.resourceReference(fileReference.media.resource)) +} diff --git a/Telegram-Mac/FetchVideoMediaResource.swift b/Telegram-Mac/FetchVideoMediaResource.swift index 2f8ea9deac..678dc2e68b 100644 --- a/Telegram-Mac/FetchVideoMediaResource.swift +++ b/Telegram-Mac/FetchVideoMediaResource.swift @@ -7,24 +7,33 @@ // import Cocoa -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore -func fetchGifMediaResource(resource: LocalFileGifMediaResource) -> Signal { +import Postbox +import SwiftSignalKit + +func fetchGifMediaResource(resource: LocalFileGifMediaResource) -> Signal { return Signal { subscriber in subscriber.putNext(.reset) + let queue: Queue = Queue() var cancelled: Bool = false let exportPath = NSTemporaryDirectory() + "\(resource.randomId).mp4" if let data = try? Data(contentsOf: URL(fileURLWithPath: resource.path)) { - resourcesQueue.async { + queue.async { TGGifConverter.convertGif(toMp4: data, exportPath: exportPath, completionHandler: { path in - subscriber.putNext(.moveLocalFile(path: path)) + let remuxedPath = NSTemporaryDirectory() + "\(arc4random()).mp4" +// let remuxed = FFMpegRemuxer.remux(path, to: remuxedPath) +// if remuxed { +// try? FileManager.default.removeItem(atPath: path) +// try? FileManager.default.moveItem(atPath: remuxedPath, toPath: path) +// } + if let path = path { + subscriber.putNext(.moveLocalFile(path: path)) + } subscriber.putCompletion() }, errorHandler: { - subscriber.putError(Void()) - subscriber.putCompletion() + subscriber.putError(.generic) }, cancelHandler: { () -> Bool in return cancelled }) @@ -36,3 +45,135 @@ func fetchGifMediaResource(resource: LocalFileGifMediaResource) -> Signal Signal { + return Signal { subscriber in + subscriber.putNext(.reset) + + + +// var cancelled: Bool = false +// let queue: Queue = Queue() +// +// let avAsset = AVURLAsset(url: URL(fileURLWithPath: resource.path)) + + let exportPath = NSTemporaryDirectory() + "\(resource.randomId).mp4" + + + if FileManager.default.fileExists(atPath: exportPath) { + removeFile(at: exportPath) + } + + try? FileManager.default.copyItem(atPath: resource.path, toPath: exportPath) + + subscriber.putNext(.moveLocalFile(path: exportPath)) + + subscriber.putCompletion() + +// var timer: SwiftSignalKit.Timer? +// +// let exportSession = AVAssetExportSession(asset: avAsset, presetName: AVAssetExportPreset1280x720) +// if let exportSession = exportSession { +// +// exportSession.outputURL = URL(fileURLWithPath: exportPath) +// exportSession.outputFileType = .mp4 +// exportSession.canPerformMultiplePassesOverSourceMediaData = true +// exportSession.shouldOptimizeForNetworkUse = true +// let start = CMTimeMakeWithSeconds(0.0, preferredTimescale: 0) +// let range = CMTimeRangeMake(start: start, duration: avAsset.duration) +// exportSession.timeRange = range +// +// exportSession.exportAsynchronously { +// if cancelled { +// subscriber.putCompletion() +// exportSession.cancelExport() +// return +// } +// switch exportSession.status { +// case .failed: +// timer?.invalidate() +// subscriber.putError(.generic) +// case .cancelled: +// timer?.invalidate() +// subscriber.putCompletion() +// case .completed: +// //let remuxedPath = NSTemporaryDirectory() + "\(arc4random()).mp4" +//// let remuxed = FFMpegRemuxer.remux(exportPath, to: remuxedPath) +//// if remuxed { +//// try? FileManager.default.removeItem(atPath: exportPath) +//// try? FileManager.default.moveItem(atPath: remuxedPath, toPath: exportPath) +//// } +// subscriber.putNext(.moveLocalFile(path: exportPath)) +// timer?.invalidate() +// subscriber.putCompletion() +// case .exporting: +// break +// default: +// break +// } +// } +// +// timer = SwiftSignalKit.Timer(timeout: 0.05, repeat: true, completion: { +// subscriber.putNext(.progressUpdated(exportSession.progress)) +// }, queue: queue) +// +// timer?.start() +// } + + + + return ActionDisposable { +// cancelled = true +// exportSession?.cancelExport() +// timer?.invalidate() + } + } +} + +func fetchArchiveMediaResource(account: Account, resource: LocalFileArchiveMediaResource) -> Signal { + return Signal { subscriber in + subscriber.putNext(.reset) + let source: ArchiveSource = .resource(resource) + let disposable = archiver.archive(source, startIfNeeded: true).start(next: { status in + switch status { + case let .done(url): + if resource.path.contains("tg_temp_archive_") { + try? FileManager.default.removeItem(atPath: resource.path) + } + subscriber.putNext(.moveLocalFile(path: url.path)) + subscriber.putCompletion() + archiver.remove(source) + case .fail: + subscriber.putError(.generic) + subscriber.putCompletion() + default: + break + } + }, error: { error in + subscriber.putError(.generic) + }, completed: { + subscriber.putCompletion() + }) + + return ActionDisposable { + disposable.dispose() + } + } +} + + +func fetchLottieSoundData(resource: LottieSoundMediaResource) -> Signal { + return Signal { subscriber in + subscriber.putNext(.reset) + let remuxedPath = NSTemporaryDirectory() + "\(arc4random()).mp4" + try? resource.data.write(to: URL(fileURLWithPath: remuxedPath)) + subscriber.putNext(.moveLocalFile(path: remuxedPath)) + subscriber.putCompletion() + + + return ActionDisposable { + + } + } +} diff --git a/Telegram-Mac/FireTimerControl.swift b/Telegram-Mac/FireTimerControl.swift new file mode 100644 index 0000000000..34c24da313 --- /dev/null +++ b/Telegram-Mac/FireTimerControl.swift @@ -0,0 +1,261 @@ +// +// FireTimerControl.swift +// Telegram +// +// Created by Mikhail Filimonov on 14.01.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + + +private enum ContentState: Equatable { + case clock(NSColor) + case timeout(NSColor, CGFloat) +} + +private struct ContentParticle { + var position: CGPoint + var direction: CGPoint + var velocity: CGFloat + var alpha: CGFloat + var lifetime: Double + var beginTime: Double + + init(position: CGPoint, direction: CGPoint, velocity: CGFloat, alpha: CGFloat, lifetime: Double, beginTime: Double) { + self.position = position + self.direction = direction + self.velocity = velocity + self.alpha = alpha + self.lifetime = lifetime + self.beginTime = beginTime + } +} + +class FireTimerControl: Control { + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + self.addSubview(self.contentView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private struct Params: Equatable { + var color: NSColor + var timeout: Int32 + var deadlineTimestamp: Int32? + } + + private var animator: ConstantDisplayLinkAnimator? + private let contentView: ImageView = ImageView() + private var currentContentState: ContentState? + private var particles: [ContentParticle] = [] + + + + private var currentParams: Params? + + var reachedTimeout: (() -> Void)? + var reachedHalf: (() -> Void)? + var updateValue: ((CGFloat) -> Void)? + + private var reachedHalfNotified: Bool = false + + deinit { + self.animator?.invalidate() + } + + func updateColor(_ color: NSColor) { + if let params = self.currentParams { + self.currentParams = Params( + color: color, + timeout: params.timeout, + deadlineTimestamp: params.deadlineTimestamp + ) + } + } + + func update(color: NSColor, timeout: Int32, deadlineTimestamp: Int32?) { + let params = Params( + color: color, + timeout: timeout, + deadlineTimestamp: deadlineTimestamp + ) + self.currentParams = params + self.reachedHalfNotified = false + self.updateValues() + } + + override func viewWillMove(toWindow newWindow: NSWindow?) { + super.viewWillMove(toWindow: newWindow) + self.animator?.isPaused = newWindow == nil + } + + private func updateValues() { + guard let params = self.currentParams else { + return + } + + let fractionalTimeout: Double + + if let deadlineTimestamp = params.deadlineTimestamp { + let fractionalTimestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + fractionalTimeout = min(Double(params.timeout), max(0.0, Double(deadlineTimestamp) + 1.0 - fractionalTimestamp)) + } else { + fractionalTimeout = Double(params.timeout) + } + + let isTimer = true + let color = params.color + + let contentState: ContentState + if isTimer { + var fraction: CGFloat = 1.0 + fraction = CGFloat(fractionalTimeout) / CGFloat(params.timeout) + fraction = max(0.0, min(0.99, fraction)) + contentState = .timeout(color, 1.0 - fraction) + + self.updateValue?(fraction) + } else { + contentState = .clock(color) + } + + if let deadlineTimestamp = params.deadlineTimestamp, Int32(Double(deadlineTimestamp) - (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)) < params.timeout / 2 { + if let reachedHalf = self.reachedHalf, !reachedHalfNotified { + reachedHalf() + reachedHalfNotified = true + } + } + + if self.currentContentState != contentState { + self.currentContentState = contentState + let image: CGImage? + + let diameter: CGFloat = 42 + let inset: CGFloat = 7 + let lineWidth: CGFloat = 2 + + switch contentState { + case let .clock(color): + image = generateImage(CGSize(width: diameter + inset, height: diameter + inset), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(color.cgColor) + context.setLineWidth(lineWidth) + context.setLineCap(.round) + + let clockFrame = CGRect(origin: CGPoint(x: (size.width - diameter) / 2.0, y: (size.height - diameter) / 2.0), size: CGSize(width: diameter, height: diameter)) + context.strokeEllipse(in: clockFrame.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0)) + + context.move(to: CGPoint(x: size.width / 2.0, y: size.height / 2.0)) + context.addLine(to: CGPoint(x: size.width / 2.0, y: clockFrame.minY + 4.0)) + context.strokePath() + + let topWidth: CGFloat = 4.0 + context.move(to: CGPoint(x: size.width / 2.0 - topWidth / 2.0, y: clockFrame.minY - 2.0)) + context.addLine(to: CGPoint(x: size.width / 2.0 + topWidth / 2.0, y: clockFrame.minY - 2.0)) + context.strokePath() + }) + case let .timeout(color, fraction): + + let timestamp = CACurrentMediaTime() + + let center = CGPoint(x: (diameter + inset) / 2.0, y: (diameter + inset) / 2.0) + let radius: CGFloat = (diameter - lineWidth / 2.0) / 2.0 + + let startAngle: CGFloat = -CGFloat.pi / 2.0 + let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * fraction + + let v = CGPoint(x: sin(endAngle), y: -cos(endAngle)) + let c = CGPoint(x: -v.y * radius + center.x, y: v.x * radius + center.y) + + let dt: CGFloat = 1.0 / 60.0 + var removeIndices: [Int] = [] + for i in 0 ..< self.particles.count { + let currentTime = timestamp - self.particles[i].beginTime + if currentTime > self.particles[i].lifetime { + removeIndices.append(i) + } else { + let input: CGFloat = CGFloat(currentTime / self.particles[i].lifetime) + let decelerated: CGFloat = (1.0 - (1.0 - input) * (1.0 - input)) + self.particles[i].alpha = 1.0 - decelerated + + var p = self.particles[i].position + let d = self.particles[i].direction + let v = self.particles[i].velocity + p = CGPoint(x: p.x + d.x * v * dt, y: p.y + d.y * v * dt) + self.particles[i].position = p + } + } + + for i in removeIndices.reversed() { + self.particles.remove(at: i) + } + + let newParticleCount = 1 + for _ in 0 ..< newParticleCount { + let degrees: CGFloat = CGFloat(arc4random_uniform(140)) - 40.0 + let angle: CGFloat = degrees * CGFloat.pi / 180.0 + + let direction = CGPoint(x: v.x * cos(angle) - v.y * sin(angle), y: v.x * sin(angle) + v.y * cos(angle)) + let velocity = (20.0 + (CGFloat(arc4random()) / CGFloat(UINT32_MAX)) * 4.0) * 0.3 + + let lifetime = Double(0.4 + CGFloat(arc4random_uniform(100)) * 0.01) + + let particle = ContentParticle(position: c, direction: direction, velocity: velocity, alpha: 1.0, lifetime: lifetime, beginTime: timestamp) + self.particles.append(particle) + } + + image = generateImage(CGSize(width: diameter + inset, height: diameter + inset), rotatedContext: { size, context in + let rect = CGRect(origin: CGPoint(), size: size) + context.clear(rect) + context.setStrokeColor(color.cgColor) + context.setFillColor(color.cgColor) + context.setLineWidth(lineWidth) + context.setLineCap(.round) + + let path = CGMutablePath() + path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true) + context.addPath(path) + context.strokePath() + + for particle in self.particles { + let size: CGFloat = 1.15 + context.setAlpha(particle.alpha) + context.fillEllipse(in: CGRect(origin: CGPoint(x: particle.position.x - size / 2.0, y: particle.position.y - size / 2.0), size: CGSize(width: size, height: size))) + } + + // let image = NSImage(named: "Icon_ExportedInvitation_Fire")!.precomposed(color, flipVertical: true) + + // context.draw(image, in: rect.focus(image.size.aspectFitted(NSMakeSize(30, 30)))) + }) + } + + self.contentView.image = image + self.contentView.sizeToFit() + self.contentView.centerY(x: frame.width - contentView.frame.width) + + } + + if let reachedTimeout = self.reachedTimeout, fractionalTimeout <= .ulpOfOne { + reachedTimeout() + } + + if fractionalTimeout <= .ulpOfOne { + self.animator?.invalidate() + self.animator = nil + } else { + if self.animator == nil { + let animator = ConstantDisplayLinkAnimator(update: { [weak self] in + self?.updateValues() + }) + animator.isPaused = self.window == nil + self.animator = animator + } + } + } + +} diff --git a/Telegram-Mac/FolderIcons.swift b/Telegram-Mac/FolderIcons.swift new file mode 100644 index 0000000000..e7397af840 --- /dev/null +++ b/Telegram-Mac/FolderIcons.swift @@ -0,0 +1,194 @@ +// +// FolderIcons.swift +// Telegram +// +// Created by Mikhail Filimonov on 06/04/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa + + +enum FolderIconState { + case sidebar + case sidebarActive + case preview + case settings + var color: NSColor { + switch self { + case .sidebar: + return NSColor.white.withAlphaComponent(0.5) + case .sidebarActive: + return .white + case .preview: + return theme.colors.grayIcon + case .settings: + return theme.colors.grayIcon + } + } +} + +let allSidebarFolderIcons: [FolderIcon] = [FolderIcon(emoticon: .emoji("🐱")), + FolderIcon(emoticon: .emoji("📕")), + FolderIcon(emoticon: .emoji("💰")), + FolderIcon(emoticon: .emoji("📸")), + FolderIcon(emoticon: .emoji("🎮")), + FolderIcon(emoticon: .emoji("🏡")), + FolderIcon(emoticon: .emoji("💡")), + FolderIcon(emoticon: .emoji("👍")), + FolderIcon(emoticon: .emoji("🔒")), + FolderIcon(emoticon: .emoji("❤️")), + FolderIcon(emoticon: .emoji("➕")), + FolderIcon(emoticon: .emoji("🎵")), + FolderIcon(emoticon: .emoji("🎨")), + FolderIcon(emoticon: .emoji("✈️")), + FolderIcon(emoticon: .emoji("⚽️")), + FolderIcon(emoticon: .emoji("⭐")), + FolderIcon(emoticon: .emoji("🎓")), + FolderIcon(emoticon: .emoji("🛫")), + FolderIcon(emoticon: .emoji("👑")), + FolderIcon(emoticon: .emoji("👨‍💼")), + FolderIcon(emoticon: .emoji("👤")), + FolderIcon(emoticon: .emoji("👥")), + //FolderIcon(emoticon: .emoji("📢")), + FolderIcon(emoticon: .emoji("💬")), + FolderIcon(emoticon: .emoji("✅")), + FolderIcon(emoticon: .emoji("☑️")), + FolderIcon(emoticon: .emoji("🤖")), + FolderIcon(emoticon: .emoji("🗂"))] + + + +enum FolderEmoticon { + case emoji(String) + case allChats + case groups + case read + case personal + case unmuted + case unread + case channels + case bots + case folder + + var emoji: String? { + switch self { + case let .emoji(emoji): + return emoji + case .allChats: return "💬" + case .personal: return "👤" + case .groups: return "👥" + case .read: return "✅" + case .unmuted: return "🔔" + case .unread: return "☑️" + case .channels: return "📢" + case .bots: return "🤖" + case .folder: return "🗂" + } + } + + var iconName: String { + switch self { + case .allChats: + return "Icon_Sidebar_AllChats" + case .groups: + return "Icon_Sidebar_Group" + case .read: + return "Icon_Sidebar_Read" + case .unread: + return "Icon_Sidebar_Unread" + case .personal: + return "Icon_Sidebar_Personal" + case .unmuted: + return "Icon_Sidebar_Unmuted" + case .channels: + return "Icon_Sidebar_Channel" + case .bots: + return "Icon_Sidebar_Bot" + case .folder: + return "Icon_Sidebar_Folder" + case let .emoji(emoji): + switch emoji { + case "👤": + return "Icon_Sidebar_Personal" + case "👥": + return "Icon_Sidebar_Group" + case "📢": + return "Icon_Sidebar_Channel" + case "💬": + return "Icon_Sidebar_AllChats" + case "✅": + return "Icon_Sidebar_Read" + case "☑️": + return "Icon_Sidebar_Unread" + case "🔔": + return "Icon_Sidebar_Unmuted" + case "🗂": + return "Icon_Sidebar_Folder" + case "🤖": + return "Icon_Sidebar_Bot" + case "🐶", "🐱": + return "Icon_Sidebar_Animal" + case "📕": + return "Icon_Sidebar_Book" + case "💰": + return "Icon_Sidebar_Coin" + case "📸": + return "Icon_Sidebar_Flash" + case "🎮": + return "Icon_Sidebar_Game" + case "🏡": + return "Icon_Sidebar_Home" + case "💡": + return "Icon_Sidebar_Lamp" + case "👍": + return "Icon_Sidebar_Like" + case "🔒": + return "Icon_Sidebar_Lock" + case "❤️": + return "Icon_Sidebar_Love" + case "➕": + return "Icon_Sidebar_Math" + case "🎵": + return "Icon_Sidebar_Music" + case "🎨": + return "Icon_Sidebar_Paint" + case "✈️": + return "Icon_Sidebar_Plane" + case "⚽️": + return "Icon_Sidebar_Sport" + case "⭐": + return "Icon_Sidebar_Star" + case "🎓": + return "Icon_Sidebar_Student" + case "🛫": + return "Icon_Sidebar_Telegram" + case "👑": + return "Icon_Sidebar_Virus" + case "👨‍💼": + return "Icon_Sidebar_Work" + case "🍷": + return "Icon_Sidebar_Wine" + case "🎭": + return "Icon_Sidebar_Mask" + default: + return "Icon_Sidebar_Folder" + } + } + } +} + +final class FolderIcon { + let emoticon: FolderEmoticon + + init(emoticon: FolderEmoticon) { + self.emoticon = emoticon + } + + func icon(for state: FolderIconState) -> CGImage { + return NSImage(named: self.emoticon.iconName)!.precomposed(state.color, flipVertical: state == .preview) + } + +} + + diff --git a/Telegram-Mac/ForgotPasswordController.swift b/Telegram-Mac/ForgotPasswordController.swift new file mode 100644 index 0000000000..abbf44352f --- /dev/null +++ b/Telegram-Mac/ForgotPasswordController.swift @@ -0,0 +1,180 @@ +// +// ForgotPasswordController.swift +// Telegram +// +// Created by Mikhail Filimonov on 25/12/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit +import TelegramCore + +import Postbox + + +private let _id_input_code = InputDataIdentifier("_id_input_code") + +private struct ForgotPasswordState : Equatable { + let code: String + let error: InputDataValueError? + let checking: Bool + init(code: String, error: InputDataValueError?, checking: Bool) { + self.code = code + self.error = error + self.checking = checking + } + func withUpdatedCode(_ code: String) -> ForgotPasswordState { + return ForgotPasswordState(code: code, error: self.error, checking: self.checking) + } + func withUpdatedError(_ error: InputDataValueError?) -> ForgotPasswordState { + return ForgotPasswordState(code: self.code, error: error, checking: self.checking) + } + func withUpdatedChecking(_ checking: Bool) -> ForgotPasswordState { + return ForgotPasswordState(code: self.code, error: self.error, checking: checking) + } +} + +private func forgotPasswordEntries(state: ForgotPasswordState, pattern: String, unavailable: @escaping()->Void) -> [InputDataEntry] { + var entries: [InputDataEntry] = [] + + var sectionId: Int32 = 0 + var index:Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + entries.append(.input(sectionId: sectionId, index: index, value: .string(state.code), error: state.error, identifier: _id_input_code, mode: .plain, data: InputDataRowData(), placeholder: nil, inputPlaceholder: L10n.twoStepAuthRecoveryCode, filter: {String($0.unicodeScalars.filter { CharacterSet.decimalDigits.contains($0)})}, limit: 6)) + index += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: GeneralRowTextType.markdown(L10n.twoStepAuthRecoveryCodeHelp + "\n\n" + L10n.twoStepAuthRecoveryEmailUnavailableNew(pattern), linkHandler: { _ in + unavailable() + }), data: InputDataGeneralTextData(detectBold: false))) + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func ForgotUnauthorizedPasswordController(accountManager: AccountManager, engine: TelegramEngineUnauthorized, emailPattern: String) -> InputDataModalController { + + + let initialState = ForgotPasswordState(code: "", error: nil, checking: false) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((ForgotPasswordState) -> ForgotPasswordState) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + + + let disposable = MetaDisposable() + + var close: (() -> Void)? = nil + + let checkCode: (String) -> InputDataValidation = { code in + return .fail(.doSomething { f in + let checking: Bool = stateValue.with { $0.checking } + if !checking { + updateState { state in + return state.withUpdatedChecking(true) + } + + if code.length == 6 { + disposable.set(showModalProgress(signal: engine.auth.performPasswordRecovery(code: code, updatedPassword: .none) |> deliverOnMainQueue, for: mainWindow).start(next: { _ in + + updateState { state in + return state.withUpdatedChecking(false) + } + + close?() + + }, error: { error in + + updateState { state in + return state.withUpdatedChecking(false) + } + + let text: String + switch error { + case .invalidCode: + text = L10n.twoStepAuthEmailCodeInvalid + case .expired: + text = L10n.twoStepAuthEmailCodeExpired + case .generic: + text = L10n.unknownError + case .limitExceeded: + text = L10n.loginFloodWait + } + + updateState { current in + return current.withUpdatedError(InputDataValueError(description: text, target: .data)) + } + + f(.fail(.fields([_id_input_code : .shake]))) + })) + } else { + updateState { current in + return current.withUpdatedError(InputDataValueError(description: L10n.twoStepAuthEmailCodeInvalid, target: .data)) + } + + f(.fail(.fields([_id_input_code : .shake]))) + } + + + } + + }) + } + let signal = statePromise.get() |> map { state in + return InputDataSignalValue(entries: forgotPasswordEntries(state: state, pattern: emailPattern, unavailable: { + alert(for: mainWindow, info: L10n.twoStepAuthRecoveryFailed) + })) + } + + let controller = InputDataController(dataSignal: signal, title: L10n.twoStepAuthRecoveryTitle, validateData: { data in + + return checkCode(stateValue.with { $0.code }) + }, updateDatas: { data in + updateState { current in + return current.withUpdatedCode(data[_id_input_code]?.stringValue ?? current.code).withUpdatedError(nil) + } + let code = stateValue.with { $0.code } + if code.length == 6 { + return checkCode(code) + } + return .none + }, afterDisappear: { + disposable.dispose() + }, updateDoneValue: { data in + return { f in + let checking = stateValue.with { $0.checking } + f(checking ? .loading : .invisible) + } + }, hasDone: true) + + controller.getBackgroundColor = { + theme.colors.background + } + + let modalInteractions = ModalInteractions(acceptTitle: L10n.modalSend, accept: { [weak controller] in + _ = controller?.returnKeyAction() + }, drawBorder: true, height: 50, singleButton: true) + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) + + close = { [weak modalController] in + modalController?.modal?.close() + } + + return modalController + +} diff --git a/Telegram-Mac/ForwardChatListController.swift b/Telegram-Mac/ForwardChatListController.swift index 378919aa70..f07b9d2e44 100644 --- a/Telegram-Mac/ForwardChatListController.swift +++ b/Telegram-Mac/ForwardChatListController.swift @@ -8,23 +8,30 @@ import Cocoa import TGUIKit -import TelegramCoreMac +import TelegramCore + class ForwardChatListController: ChatListController { override func getLeftBarViewOnce() -> BarView { - let button = TextButtonBarView(controller: self, text: tr(.chatCancel)) + let button = TextButtonBarView(controller: self, text: tr(L10n.chatCancel)) - button.button.set(handler: { [weak self] _ in + button.set(handler: { [weak self] _ in + self?.navigationController?.removeModalAction() self?.navigationController?.back() - }, for: .Click) + }, for: .Click) return button } - init(_ account: Account) { - super.init(account, modal:true) + override func getRightBarViewOnce() -> BarView { + return BarView(controller: self) + } + + init(_ context: AccountContext) { + super.init(context, modal:true) } override func escapeKeyAction() -> KeyHandlerResult { + navigationController?.removeModalAction() return .rejected } diff --git a/Telegram-Mac/ForwardPanelModel.swift b/Telegram-Mac/ForwardPanelModel.swift index db5e4a89dc..d841686ed4 100644 --- a/Telegram-Mac/ForwardPanelModel.swift +++ b/Telegram-Mac/ForwardPanelModel.swift @@ -8,37 +8,25 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac -import PostboxMac -import TelegramCoreMac +import SwiftSignalKit +import Postbox +import TelegramCore + class ForwardPanelModel: ChatAccessoryModel { - private var account:Account - private var forwardIds:[MessageId] - private var forwardMessages:[Message] = [] - - private var disposable:MetaDisposable = MetaDisposable() - - init(forwardIds:[MessageId], account:Account) { - + private let account:Account + private let forwardMessages:[Message] + private let hideNames: Bool + init(forwardMessages:[Message], hideNames: Bool, account:Account) { self.account = account - self.forwardIds = forwardIds + self.forwardMessages = forwardMessages + self.hideNames = hideNames super.init() - - - disposable.set((account.postbox.messagesAtIds(forwardIds) - |> deliverOnMainQueue).start(next: { [weak self] result in - if let strongSelf = self { - strongSelf.forwardMessages = result - strongSelf.make() - } - })) + self.make() } - deinit { - disposable.dispose() } @@ -48,22 +36,55 @@ class ForwardPanelModel: ChatAccessoryModel { var used:Set = Set() + var keys:[Int64:Int64] = [:] + var forwardMessages:[Message] = [] + for message in self.forwardMessages { + if let groupingKey = message.groupingKey { + if keys[groupingKey] == nil { + keys[groupingKey] = groupingKey + forwardMessages.append(message) + } + } else { + forwardMessages.append(message) + } + } + + for message in forwardMessages { - if let peer = messageMainPeer(message), let author = message.author { + if let author = message.chatPeer(account.peerId) { if !used.contains(author.id) { used.insert(author.id) - if peer.isChannel { - names.append(peer.displayTitle) + if author.isChannel { + names.append(author.displayTitle) } else { names.append(author.displayTitle) } } } - } - self.headerAttr = NSAttributedString.initialize(string: names.joined(separator: ", "), color: theme.colors.blueUI, font: .medium(.text)) - self.messageAttr = NSAttributedString.initialize(string: tr(.messageAccessoryPanelForwardedCountable(forwardMessages.count)), color: theme.colors.text, font: .normal(.text)) + //hideNames ? L10n.chatInputForwardHidden : names.joined(separator: ", ") + + let text = hideNames ? L10n.chatAccessoryHiddenCountable(forwardMessages.count) : L10n.chatAccessoryForwardCountable(forwardMessages.count) + self.headerAttr = NSAttributedString.initialize(string: text, color: theme.colors.accent, font: .medium(.text)) + if forwardMessages.count == 1, !forwardMessages[0].text.isEmpty, forwardMessages[0].media.isEmpty { + let text: String + let messageText = chatListText(account: account, for: forwardMessages[0]).string + if forwardMessages[0].effectiveAuthor?.id == account.peerId { + text = "\(L10n.chatAccessoryForwardYou): \(messageText)" + } else if let author = forwardMessages[0].effectiveAuthor { + text = "\(author.displayTitle): \(messageText)" + } else { + text = messageText + } + self.messageAttr = NSAttributedString.initialize(string: text, color: theme.colors.grayText, font: .normal(.text)) + } else { + let authors = uniquePeers(from: forwardMessages.compactMap { $0.effectiveAuthor }) + let messageText = authors.map { $0.compactDisplayTitle }.joined(separator: ", ") + let text = "\(L10n.chatAccessoryForwardFrom): \(messageText)" + + self.messageAttr = NSAttributedString.initialize(string: text, color: theme.colors.grayText, font: .normal(.text)) + } nodeReady.set(.single(true)) self.setNeedDisplay() diff --git a/Telegram-Mac/GIFContainerView.swift b/Telegram-Mac/GIFContainerView.swift index 435159846a..3ba3eebeac 100644 --- a/Telegram-Mac/GIFContainerView.swift +++ b/Telegram-Mac/GIFContainerView.swift @@ -7,13 +7,19 @@ // import Cocoa -import TelegramCoreMac +import TelegramCore + import TGUIKit -import PostboxMac -import SwiftSignalKitMac -class GIFContainerView: View { +import Postbox +import SwiftSignalKit + + + +class GIFContainerView: Control { - private(set) var player:GIFPlayerView = GIFPlayerView() + let player:GifPlayerBufferView = GifPlayerBufferView() + + private var progressView:RadialProgressView? var playerInset:NSEdgeInsets = NSEdgeInsets() { didSet { @@ -24,30 +30,35 @@ class GIFContainerView: View { private let fetchDisposable = MetaDisposable() private let playerDisposable = MetaDisposable() - private var resource:TelegramMediaResource? - private var account:Account? + private var context: AccountContext? private var size:NSSize = NSZeroSize + private var ignoreWindowKey: Bool = false private weak var tableView:TableView? - var timebase:CMTimebase? { - didSet { - player.reset(with: timebase, false) - } - } - private var path:String? { - didSet { - updatePlayerIfNeeded() - } - } + + var associatedMessageId: MessageId? = nil + private var fileReference: FileMediaReference? + override init() { + + super.init() addSubview(player) self.backgroundColor = .clear - self.layer?.borderWidth = 1.5 - //self.layer?.cornerRadius = 4.0 + + + player.background = .clear + player.setVideoLayerGravity(.resizeAspectFill) + set(handler: { [weak self] control in + if let `self` = self, let window = self.window as? Window, let table = self.tableView, let context = self.context { + _ = startModalPreviewHandle(table, window: window, context: context) + } + }, for: .LongMouseDown) + } + required convenience init(frame frameRect: NSRect) { self.init() } @@ -69,60 +80,49 @@ class GIFContainerView: View { } - func fetch() { - if let account = account, let resource = resource { - fetchDisposable.set(account.postbox.mediaBox.fetchedResource(resource, tag: TelegramMediaResourceFetchTag(statsCategory: .file)).start()) + func cancelFetching() { + if let reference = fileReference { + context?.account.postbox.mediaBox.cancelInteractiveResourceFetch(reference.media.resource) } } - + + func fetch() { + if let context = context, let reference = fileReference { + fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: reference.resourceReference(reference.media.resource), statsCategory: .file).start()) + } + } func removeNotificationListeners() { NotificationCenter.default.removeObserver(self) } + var accept: Bool { + let wAccept = window != nil && (window!.isKeyWindow || self.ignoreWindowKey) && !NSIsEmptyRect(visibleRect) + let accept:Bool = wAccept + return accept + } + + @objc func updatePlayerIfNeeded() { + let accept = self.accept - - let wAccept = window != nil && window!.isKeyWindow && !NSIsEmptyRect(visibleRect) - var accept:Bool = false - - if let window = window { - var points:[NSPoint] = [] - - points.append(convert(focus(NSMakeSize(1, 1)).origin, to: window.contentView)) - points.append(convert(NSMakePoint(1, 1), to: window.contentView)) - points.append(convert(NSMakePoint(frame.width - 1, frame.height - 1), to: window.contentView)) - - - for point in points { - if let hit = window.contentView?.hitTest(point) { - accept = wAccept && (hit == self.player || hit == self) - if !accept && wAccept, let hit = hit as? Control { - accept = !hit.userInteractionEnabled - } - } - if accept { - break - } + if !ignoreWindowKey { + var s:Signal = .single(Void()) + if accept { + s = s |> delay(0.05, queue: Queue.mainQueue()) } - - - + playerDisposable.set(s.start(next: {[weak self] (next) in + if let strongSelf = self { + strongSelf.player.ticking = accept + } + })) + } else { + playerDisposable.set(nil) + self.player.ticking = accept } - player.set(path: accept ? path : nil, timebase: timebase) - - - /*var s:Signal = .single() - s = s |> delay(0.01, queue: Queue.mainQueue()) - playerDisposable.set(s.start(next: {[weak self] (next) in - if let strongSelf = self { - let accept = strongSelf.window != nil && strongSelf.window!.isKeyWindow && !NSIsEmptyRect(strongSelf.visibleRect) - strongSelf.player.set(path: accept ? strongSelf.path : nil) - } - })) */ } @@ -132,12 +132,14 @@ class GIFContainerView: View { NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSWindow.didBecomeKeyNotification, object: window) NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSWindow.didResignKeyNotification, object: window) NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSView.boundsDidChangeNotification, object: tableView?.clipView) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSView.frameDidChangeNotification, object: tableView?.view) } else { removeNotificationListeners() } } deinit { playerDisposable.dispose() + removeNotificationListeners() } override func viewDidMoveToWindow() { @@ -145,65 +147,54 @@ class GIFContainerView: View { updatePlayerIfNeeded() } - func update(with resource: TelegramMediaResource, size: NSSize, viewSize:NSSize, account: Account, table: TableView?, iconSignal:Signal<(TransformImageArguments)->DrawingContext?,Void>) { + + func update(with fileReference: FileMediaReference, size: NSSize, viewSize:NSSize, context: AccountContext, table: TableView?, ignoreWindowKey: Bool = false, isPreview: Bool = false, iconSignal:Signal) { + + + let updated = self.fileReference == nil || !fileReference.media.isEqual(to: self.fileReference!.media) self.tableView = table - self.account = account - self.resource = resource + self.fileReference = fileReference + self.context = context self.size = size self.setFrameSize(size) - + self.ignoreWindowKey = ignoreWindowKey self.layer?.borderColor = theme.colors.background.cgColor - updateListeners() player.setFrameSize(viewSize) - player.center() progressView?.center() - player.setSignal(account: account, signal: iconSignal) - let imageSize = viewSize.aspectFitted(NSMakeSize(size.width, size.height - 8)) - - let arguments = TransformImageArguments(corners: ImageCorners(radius:2.0), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: NSEdgeInsets()) + player.update(fileReference, context: context) + + + let imageSize = viewSize.aspectFitted(NSMakeSize(size.width, size.height)) + let size = (fileReference.media.dimensions?.size ?? imageSize).aspectFilled(viewSize) + let arguments = TransformImageArguments(corners: ImageCorners(radius:2.0), imageSize: size, boundingSize: imageSize, intrinsicInsets: NSEdgeInsets()) + + player.setSignal(signal: cachedMedia(media: fileReference.media, arguments: arguments, scale: backingScaleFactor), clearInstantly: updated) + + if !player.isFullyLoaded { + player.setSignal(iconSignal, cacheImage: { result in + cacheMedia(result, media: fileReference.media, arguments: arguments, scale: System.backingScale) + }) + } + player.set(arguments: arguments) - let updatedStatusSignal = account.postbox.mediaBox.resourceStatus(resource) - - self.statusDisposable.set((combineLatest(updatedStatusSignal, account.postbox.mediaBox.resourceData(resource)) |> deliverOnMainQueue).start(next: { [weak self] (status,resource) in - if let strongSelf = self { - if case .Local = status { - if let progressView = strongSelf.progressView { - progressView.removeFromSuperview() - strongSelf.progressView = nil - } - strongSelf.path = resource.path - - } else { - if strongSelf.progressView == nil { - let progressView = RadialProgressView() - progressView.frame = CGRect(origin: CGPoint(), size: CGSize(width: 40.0, height: 40.0)) - strongSelf.progressView = progressView - strongSelf.addSubview(progressView) - strongSelf.progressView?.center() - } - } - - switch status { - case let .Fetching(_, progress): - strongSelf.progressView?.state = .Fetching(progress: progress, force: false) - case .Local: - strongSelf.progressView?.state = .Play - case .Remote: - strongSelf.progressView?.state = .Remote - } - } - })) - + updatePlayerIfNeeded() fetch() + needsLayout = true } + override func layout() { + super.layout() + progressView?.center() + updatePlayerIfNeeded() + } + override func copy() -> Any { let view = View() view.backgroundColor = .clear diff --git a/Telegram-Mac/GIFPlayerView.swift b/Telegram-Mac/GIFPlayerView.swift index 0390fa350d..8b61cc20f3 100644 --- a/Telegram-Mac/GIFPlayerView.swift +++ b/Telegram-Mac/GIFPlayerView.swift @@ -9,57 +9,172 @@ import Cocoa import TGUIKit import AVFoundation -import SwiftSignalKitMac +import SwiftSignalKit -let sampleBufferQueue = DispatchQueue(label: "samplebuffer") -private let veryLongTimeInterval = CFTimeInterval(256.0 * 365.0 * 24.0 * 60.0 * 60.0) +final class CIStickerContext : CIContext { + deinit { + var bp:Int = 0 + bp += 1 + } +} + +private class AlphaFrameFilter: CIFilter { + static var kernel: CIColorKernel? = { + return CIColorKernel(source: """ +kernel vec4 alphaFrame(__sample s, __sample m) { + return vec4( s.rgb, m.r ); +} +""") + }() + + var inputImage: CIImage? + var maskImage: CIImage? + + override var outputImage: CIImage? { + let kernel = AlphaFrameFilter.kernel! + guard let inputImage = inputImage, let maskImage = maskImage else { + return nil + } + let args = [inputImage as AnyObject, maskImage as AnyObject] + return kernel.apply(extent: inputImage.extent, arguments: args) + } +} +let sampleBufferQueue = DispatchQueue.init(label: "sampleBufferQueue", qos: DispatchQoS.background, attributes: []) +private let veryLongTimeInterval = CFTimeInterval(8073216000) -class GIFPlayerView: TransformImageView { +struct AVGifData : Equatable { + let asset: AVURLAsset + let track: AVAssetTrack + let animatedSticker: Bool + let swapOnComplete: Bool + private init(asset: AVURLAsset, track: AVAssetTrack, animatedSticker: Bool, swapOnComplete: Bool) { + self.asset = asset + self.track = track + self.swapOnComplete = swapOnComplete + self.animatedSticker = animatedSticker + } - private var sampleLayer:AVSampleBufferDisplayLayer = AVSampleBufferDisplayLayer() + static func dataFrom(_ path: String?, animatedSticker: Bool = false, swapOnComplete: Bool = false) -> AVGifData? { + let new = link(path: path, ext: "mp4") + if let new = new { + let avAsset = AVURLAsset(url: URL(fileURLWithPath: new)) + let t = avAsset.tracks(withMediaType: .video).first + if let t = t { + return AVGifData(asset: avAsset, track: t, animatedSticker: animatedSticker, swapOnComplete: swapOnComplete) + } + } + return nil + } + static func ==(lhs: AVGifData, rhs: AVGifData) -> Bool { + return lhs.asset.url == rhs.asset.url && lhs.animatedSticker == rhs.animatedSticker + } - private var _reader:Atomic = Atomic(value:nil) +} - - private var _asset:Atomic = Atomic(value:nil) +private final class TAVSampleBufferDisplayLayer : AVSampleBufferDisplayLayer { + deinit { + + } +} - - private let _output:Atomic = Atomic(value:nil) - - private let _track:Atomic = Atomic(value:nil) +class GIFPlayerView: TransformImageView { - private let _needReset:Atomic = Atomic(value:false) - + enum LoopActionResult { + case pause + } - private let _timer:Atomic = Atomic(value:nil) - + private let sampleLayer:TAVSampleBufferDisplayLayer = TAVSampleBufferDisplayLayer() + private var _reader:Atomic = Atomic(value:nil) + private var _asset:Atomic = Atomic(value:nil) + private let _output:Atomic = Atomic(value:nil) + private let _track:Atomic = Atomic(value:nil) + private let _needReset:Atomic = Atomic(value:false) + private let _timer:Atomic = Atomic(value:nil) + private let _loopAction:Atomic<(()->LoopActionResult)?> = Atomic(value:nil) private let _timebase:Atomic = Atomic(value:nil) - private let _stopRequesting:Atomic = Atomic(value:false) private let _swapNext:Atomic = Atomic(value:true) - private let _path:Atomic = Atomic(value:nil) + private let _data:Atomic = Atomic(value:nil) + + func setLoopAction(_ action:(()->LoopActionResult)?) { + _ = _loopAction.swap(action) + } + - public var followWindow:Bool = true + private let maskLayer = CAShapeLayer() + var positionFlags: LayoutPositionFlags? { + didSet { + if let positionFlags = positionFlags { + let path = CGMutablePath() + + let minx:CGFloat = 0, midx = frame.width/2.0, maxx = frame.width + let miny:CGFloat = 0, midy = frame.height/2.0, maxy = frame.height + + path.move(to: NSMakePoint(minx, midy)) + + var topLeftRadius: CGFloat = .cornerRadius + var bottomLeftRadius: CGFloat = .cornerRadius + var topRightRadius: CGFloat = .cornerRadius + var bottomRightRadius: CGFloat = .cornerRadius + + + if positionFlags.contains(.top) && positionFlags.contains(.left) { + bottomLeftRadius = .cornerRadius * 3 + 2 + } + if positionFlags.contains(.top) && positionFlags.contains(.right) { + bottomRightRadius = .cornerRadius * 3 + 2 + } + if positionFlags.contains(.bottom) && positionFlags.contains(.left) { + topLeftRadius = .cornerRadius * 3 + 2 + } + if positionFlags.contains(.bottom) && positionFlags.contains(.right) { + topRightRadius = .cornerRadius * 3 + 2 + } + + path.addArc(tangent1End: NSMakePoint(minx, miny), tangent2End: NSMakePoint(midx, miny), radius: bottomLeftRadius) + path.addArc(tangent1End: NSMakePoint(maxx, miny), tangent2End: NSMakePoint(maxx, midy), radius: bottomRightRadius) + path.addArc(tangent1End: NSMakePoint(maxx, maxy), tangent2End: NSMakePoint(midx, maxy), radius: topRightRadius) + path.addArc(tangent1End: NSMakePoint(minx, maxy), tangent2End: NSMakePoint(minx, midy), radius: topLeftRadius) + + maskLayer.path = path + layer?.mask = maskLayer + } else { + layer?.mask = nil + } + } + } override init() { super.init() sampleLayer.actions = ["onOrderIn":NSNull(),"sublayers":NSNull(),"bounds":NSNull(),"frame":NSNull(),"position":NSNull(),"contents":NSNull(),"opacity":NSNull(), "transform": NSNull() ] - sampleLayer.videoGravity = .resizeAspectFill + sampleLayer.videoGravity = .resizeAspect sampleLayer.backgroundColor = NSColor.clear.cgColor + + layer?.addSublayer(sampleLayer) + + } + + func setVideoLayerGravity(_ gravity: AVLayerVideoGravity) { + sampleLayer.videoGravity = gravity } - var isHasPath: Bool { - return _path.modify({$0}) != nil + + var controlTimebase: CMTimebase? { + return sampleLayer.controlTimebase + } + + var isHasData: Bool { + return _data.modify({$0}) != nil } required init?(coder: NSCoder) { @@ -76,17 +191,12 @@ class GIFPlayerView: TransformImageView { sampleLayer.frame = bounds } - func set(path:String?, timebase:CMTimebase? = nil) -> Void { + func set(data: AVGifData?, timebase:CMTimebase? = nil) -> Void { assertOnMainThread() - - - let realPath:String? = link(path:path, ext:"mov") - - - if realPath != self._path.modify({$0}) { - _ = _path.swap(realPath) + + if data != _data.swap(data) { _ = _timebase.swap(timebase) - let path = self._path + let _data = self._data let layer:AVSampleBufferDisplayLayer = self.sampleLayer let reader = self._reader let output = self._output @@ -97,18 +207,16 @@ class GIFPlayerView: TransformImageView { let asset = self._asset let timer = self._timer let timebase = self._timebase - - if let path = realPath { - let avAsset = AVURLAsset(url: URL(fileURLWithPath: path)) - let _ = asset.swap(avAsset) - let t = avAsset.tracks(withMediaType: .video).first - if let track = t { - layer.setAffineTransform(track.preferredTransform.inverted()) + let loopAction = self._loopAction + if let data = data { + let _ = track.swap(data.track) + let _ = asset.swap(data.asset) + + _ = stopRequesting.swap(false) + if data.swapOnComplete { + _ = timebase.swap(self.controlTimebase) } - - let _ = track.swap(t) - _ = stopRequesting.swap(t == nil) - _ = swapNext.swap(t != nil) + _ = swapNext.swap(true) } else { _ = asset.swap(nil) _ = track.swap(nil) @@ -118,31 +226,49 @@ class GIFPlayerView: TransformImageView { } layer.requestMediaDataWhenReady(on: sampleBufferQueue, using: { - if stopRequesting.modify({$0}) { + if stopRequesting.swap(false) { + + if let controlTimebase = layer.controlTimebase, let current = timer.swap(nil) { + CMTimebaseRemoveTimer(controlTimebase, timer: current) + _ = timebase.swap(nil) + } + layer.stopRequestingMediaData() layer.flushAndRemoveImage() - reader.modify({$0})?.cancelReading() - _ = reader.swap(nil) - _ = stopRequesting.swap(false) + var reader = reader.swap(nil) + Queue.concurrentBackgroundQueue().async { + reader?.cancelReading() + reader = nil + } + return } if swapNext.swap(false) { - _ = reader.modify({$0})?.cancelReading() - _ = reader.swap(nil) _ = output.swap(nil) + var reader = reader.swap(nil) + Queue.concurrentBackgroundQueue().async { + reader?.cancelReading() + reader = nil + } } - if let readerValue = reader.modify({$0}), let outputValue = output.modify({$0}) { + if let readerValue = reader.with({ $0 }), let outputValue = output.with({ $0 }) { + + let affineTransform = track.with { $0?.preferredTransform.inverted() } + if let affineTransform = affineTransform { + layer.setAffineTransform(affineTransform) + } + while layer.isReadyForMoreMediaData { - if !stopRequesting.modify({$0}) { - if let sampleVideo = outputValue.copyNextSampleBuffer() { - layer.enqueue(sampleVideo) - + if !stopRequesting.with({ $0 }) { + + if readerValue.status == .reading, let sampleBuffer = outputValue.copyNextSampleBuffer() { + layer.enqueue(sampleBuffer) continue } - _ = stopRequesting.modify({_ in path.modify({$0}) == nil}) + _ = stopRequesting.modify { _ in _data.with { $0 } == nil } break } else { break @@ -150,9 +276,15 @@ class GIFPlayerView: TransformImageView { } if readerValue.status == .completed || readerValue.status == .cancelled { - if reset.modify({$0}) { - _ = reset.swap(false) - + if reset.swap(false) { + let loopActionResult = loopAction.with({ $0?() }) + + if let loopActionResult = loopActionResult { + switch loopActionResult { + case .pause: + return + } + } let result = restartReading(_reader: reader, _asset: asset, _track: track, _output: output, _needReset: reset, _timer: timer, layer: layer, _timebase: timebase) if result { layer.flush() @@ -172,13 +304,12 @@ class GIFPlayerView: TransformImageView { } func reset(with timebase:CMTimebase? = nil, _ resetImage: Bool = true) { - if resetImage { + // if resetImage { sampleLayer.flushAndRemoveImage() - } else { - sampleLayer.flush() - } + // } else { + // sampleLayer.flush() + // } - clear(false) _ = _swapNext.swap(true) _ = _timebase.swap(timebase) } @@ -186,22 +317,11 @@ class GIFPlayerView: TransformImageView { deinit { - clear(true) - _ = _path.swap(nil) - sampleLayer.flushAndRemoveImage() + _ = _stopRequesting.swap(true) } - private func clear(_ stopRequesting:Bool = false) { - _ = _stopRequesting.swap(stopRequesting) - - if let timebase = sampleLayer.controlTimebase, let timer = _timer.modify({$0}) { - _ = _timer.swap(nil) - CMTimebaseRemoveTimer(timebase, timer) - } - - } - + required convenience init(frame frameRect: NSRect) { self.init() @@ -213,7 +333,7 @@ fileprivate func restartReading(_reader:Atomic, _asset:Atomic, _asset:Atomic, _asset:Atomic, _asset:Atomic Bool { - let sum:CGFloat = sizes.reduce(0, { (acc, size) -> CGFloat in - return acc + size.width - }) - if sum >= width { - return true - } else { - return false - } + +private final class GifTabsArguments { + let select:(GifTabEntryId)->Void + let context: AccountContext + init(context: AccountContext, select: @escaping(GifTabEntryId)->Void) { + self.context = context + self.select = select } } -func ==(lhs:RecentGifRow, rhs:RecentGifRow) -> Bool { - return lhs.entries == rhs.entries && lhs.results == rhs.results && lhs.sizes == rhs.sizes + +enum GifTabEntryId : Hashable { + case recent + case trending + case recommended(String) } -func makeRecentGifEnties(_ results:[TelegramMediaFile], initialSize:NSSize) -> [RecentGifRowEntry] { - var entries:[RecentGifEntry] = [] - var rows:[RecentGifRow] = [] +private enum GifTabEntry : TableItemListNodeEntry { + typealias ItemGenerationArguments = GifTabsArguments - var dimensions:[NSSize] = [] - var results = results - var index:Int = 0 - for result in results { - entries.append(.gif(index: index, file: result)) - dimensions.append(result.dimensions ?? NSZeroSize) - index += 1 - } + case recent(selected: Bool) + case trending(selected: Bool) + case recommended(selected: Bool, index: Int, value: String) - var fitted:[[NSSize]] = [] - let f:Int = Int(round(initialSize.width / initialSize.height)) - while !dimensions.isEmpty { - let row = fitPrettyDimensions(dimensions, isLastRow: f > dimensions.count, fitToHeight: false, perSize:initialSize) - fitted.append(row) - dimensions.removeSubrange(0 ..< row.count) - } - for row in fitted { - let subentries = Array(entries.prefix(row.count)) - let subresult = Array(results.prefix(row.count)) - rows.append(RecentGifRow(entries: subentries, results: subresult, sizes: row)) - - entries.removeSubrange(0 ..< row.count) - results.removeSubrange(0 ..< row.count) - - } - var idx:Int = 0 - return rows.map { row in - let entry = RecentGifRowEntry.gif(index: idx, row: row) - idx += 1 - return entry - } -} - - -enum RecentGifEntry : Equatable { - case gif(index:Int, file:TelegramMediaFile) - var index:Int { + var index: Int { switch self { - case let .gif(index, _): + case .recent: + return -2 + case .trending: + return -1 + case let .recommended(_, index, _): return index } } - var mediaId:MediaId { + var stableId: GifTabEntryId { switch self { - case let .gif(_, file): - return file.id ?? MediaId(namespace: 0, id: 0) + case .recent: + return .recent + case .trending: + return .trending + case let .recommended(_, _, value): + return .recommended(value) } } -} -func ==(lhs:RecentGifEntry, rhs: RecentGifEntry) -> Bool { - switch lhs { - case let .gif(lhsIndex, lhsFile): - if case let .gif(rhsIndex, rhsFile) = rhs { - return lhsIndex == rhsIndex && lhsFile.isEqual(rhsFile) - } else { - return false - } + + static func < (lhs: GifTabEntry, rhs: GifTabEntry) -> Bool { + return lhs.index < rhs.index } -} - -enum RecentGifRowEntry : Comparable, Identifiable { - case gif(index:Int, row: RecentGifRow) - var index:Int { + var selected: Bool { switch self { - case let .gif(index, _): - return index + case let .recent(selected): + return selected + case let .trending(selected): + return selected + case let .recommended(selected, _, _): + return selected } } - var stableId: AnyHashable { - switch self { - case let .gif(index: _, row: row): - return row.entries.reduce("", { (current, row) -> String in - return current + "index:\(row.index), id:\(row.mediaId)" - }).hashValue - } + func item(_ arguments: GifTabsArguments, initialSize: NSSize) -> TableRowItem { + return GifPanelTabRowItem(initialSize, selected: self.selected, entry: stableId, select: arguments.select) } } -func ==(lhs:RecentGifRowEntry, rhs: RecentGifRowEntry) -> Bool { - switch lhs { - case let .gif(index, row): - if case .gif(index, row) = rhs { - return true + + + +struct GIFKeyboardConfiguration : Equatable { + static var defaultValue: GIFKeyboardConfiguration { + return GIFKeyboardConfiguration(emojis: []) + } + + let emojis: [String] + + fileprivate init(emojis: [String]) { + self.emojis = emojis.map { $0.fixed } + } + + static func with(appConfiguration: AppConfiguration) -> GIFKeyboardConfiguration { + if let data = appConfiguration.data, let value = data["gif_search_emojies"] as? [String] { + return GIFKeyboardConfiguration(emojis: value.map { $0.fixed }) } else { - return false + return .defaultValue } } + } -func <(lhs:RecentGifRowEntry, rhs: RecentGifRowEntry) -> Bool { - return lhs.index < rhs.index +private func prepareEntries(left:[InputContextEntry], right:[InputContextEntry], context: AccountContext, initialSize:NSSize, arguments: RecentGifsArguments?, mode: EntertainmentViewController.Mode) -> TableUpdateTransition { + let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right, { entry -> TableRowItem in + switch entry { + case let .contextMediaResult(collection, row, index): + return ContextMediaRowItem(initialSize, row, index, context, ContextMediaArguments(sendResult: { result, view in + if let collection = collection { + arguments?.sendInlineResult(collection, result, view) + } else { + switch result { + case let .internalReference(values): + if let file = values.file { + arguments?.sendAppFile(file, view, false) + } + default: + break + } + } + }, menuItems: { file, view in + if mode == .common { + return context.account.postbox.transaction { transaction -> [ContextMenuItem] in + var items: [ContextMenuItem] = [] + if let mediaId = file.id { + let gifItems = transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs).compactMap {$0.contents as? RecentMediaItem} + if let _ = gifItems.firstIndex(where: {$0.media.id == mediaId}) { + items.append(ContextMenuItem(L10n.messageContextRemoveGif, handler: { + let _ = removeSavedGif(postbox: context.account.postbox, mediaId: mediaId).start() + })) + } else { + items.append(ContextMenuItem(L10n.messageContextSaveGif, handler: { + let _ = addSavedGif(postbox: context.account.postbox, fileReference: FileMediaReference.savedGif(media: file)).start() + })) + } + items.append(ContextMenuItem(L10n.chatSendWithoutSound, handler: { + arguments?.sendAppFile(file, view, true) + })) + } + return items + } + } else { + return .single([]) + } + })) + case let .separator(string, _, _): + return SeparatorRowItem(initialSize, entry.stableId, string: string) + case let .emoji(clues, selected, _, _): + return ContextClueRowItem(initialSize, stableId: entry.stableId, context: context, clues: clues, selected: selected, canDisablePrediction: false, callback: { emoji in + arguments?.searchBySuggestion(emoji) + }) + default: + fatalError() + } + }) + + return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true) } -private func prepareEntries(left:[RecentGifRowEntry], right:[RecentGifRowEntry], account:Account, initialSize:NSSize, arguments: RecentGifsArguments) -> TableUpdateTransition { - +private func prepareTabTransition(left:[GifTabEntry], right:[GifTabEntry], initialSize:NSSize, arguments: GifTabsArguments) -> TableUpdateTransition { let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right, { entry -> TableRowItem in - switch entry { - case .gif: - return RecentGIFRowItem(initialSize, account: account, entry: entry, arguments: arguments) - } + return entry.item(arguments, initialSize: initialSize) }) - return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated) + return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true) + } -private func recentEntries(for view:OrderedItemListView?, initialSize:NSSize) -> [RecentGifRowEntry] { +private func recentEntries(for view:OrderedItemListView?, initialSize:NSSize) -> [InputContextEntry] { if let view = view { - return makeRecentGifEnties(view.items.prefix(70).flatMap({($0.contents as? RecentMediaItem)?.media as? TelegramMediaFile}), initialSize: NSMakeSize(initialSize.width, 100)) + let result: [ChatContextResult] = view.items.compactMap({($0.contents as? RecentMediaItem)?.media as? TelegramMediaFile}).map { file in + let reference = ChatContextResult.InternalReference(queryId: 0, id: "gif-panel", type: "gif", title: nil, description: nil, image: nil, file: file, message: .auto(caption: "", entities: nil, replyMarkup: nil)) + return .internalReference(reference) + } + let values = makeMediaEnties(result, isSavedGifs: true, initialSize: NSMakeSize(initialSize.width, 100)) + var wrapped:[InputContextEntry] = [] + for value in values { + wrapped.append(InputContextEntry.contextMediaResult(nil, value, Int64(arc4random()) | ((Int64(wrapped.count) << 40)))) + } + + return wrapped } return [] } -struct RecentGifsArguments { - let sendGif:(TelegramMediaFile)->Void +private func tabsEntries(_ emojis: [String], selected: GifTabEntryId) -> [GifTabEntry] { + var entries:[GifTabEntry] = [] + + entries.append(.recent(selected: selected == .recent)) + entries.append(.trending(selected: selected == .trending)) + + for (i, emoji) in emojis.enumerated() { + entries.append(.recommended(selected: selected == .recommended(emoji), index: i, value: emoji)) + } + + return entries +} + +private func gifEntries(for collection: ChatContextResultCollection?, results: [ChatContextResult], initialSize: NSSize, mode: EntertainmentViewController.Mode) -> [InputContextEntry] { + var result: [InputContextEntry] = [] + if let collection = collection { + + result = makeMediaEnties(results, isSavedGifs: true, initialSize: NSMakeSize(initialSize.width, 100)).map({InputContextEntry.contextMediaResult(collection, $0, arc4random64())}) + + switch mode { + case .selectAvatar: + result = result.filter { entry in + switch entry { + case let .contextResult(_, result, _): + if case .externalReference = result { + return false + } else { + return true + } + default: + return true + } + } + default: + break + } + } + + return result +} + +final class RecentGifsArguments { + var sendInlineResult:(ChatContextResultCollection,ChatContextResult, NSView) -> Void = { _,_,_ in} + var sendAppFile:(TelegramMediaFile, NSView, Bool) -> Void = { _,_,_ in} + var searchBySuggestion:(String)->Void = { _ in } } final class TableContainer : View { fileprivate var tableView: TableView? + fileprivate var restrictedView:RestrictionWrappedView? + fileprivate let progressView: ProgressIndicator = ProgressIndicator(frame: NSMakeRect(0, 0, 30, 30)) + fileprivate let emptyResults: ImageView = ImageView() + + + let searchView = SearchView(frame: .zero) + private let searchContainer = View() + fileprivate let packsView:HorizontalTableView = HorizontalTableView(frame: NSZeroRect) + private let separator:View = View() + fileprivate let tabsContainer: View = View() + required init(frame frameRect: NSRect) { super.init(frame: frameRect) + emptyResults.contentGravity = .center + updateLocalizationAndTheme(theme: theme) + + + searchContainer.addSubview(searchView) + addSubview(searchContainer) + + tabsContainer.addSubview(packsView) + tabsContainer.addSubview(separator) + addSubview(tabsContainer) + + reinstall() + } + + func updateRestricion(_ peer: Peer?) { + if let peer = peer, let text = permissionText(from: peer, for: .banSendGifs) { + restrictedView?.removeFromSuperview() + restrictedView = RestrictionWrappedView(text) + addSubview(restrictedView!) + } else { + restrictedView?.removeFromSuperview() + restrictedView = nil + } + setFrameSize(frame.size) + needsLayout = true + } + + private var searchState: SearchState? = nil + + func updateSearchState(_ searchState: SearchState, animated: Bool) { + self.searchState = searchState + switch searchState.state { + case .Focus: + tabsContainer.change(pos: NSMakePoint(0, -tabsContainer.frame.height), animated: animated) + searchContainer.change(pos: NSMakePoint(0, tabsContainer.frame.maxY), animated: animated) + case .None: + tabsContainer.change(pos: NSMakePoint(0, 0), animated: animated) + searchContainer.change(pos: NSMakePoint(0, tabsContainer.frame.maxY), animated: animated) + } + if let tableView = tableView { + tableView.change(size: NSMakeSize(frame.width, frame.height - searchContainer.frame.maxY), animated: animated) + tableView.change(pos: NSMakePoint(0, searchContainer.frame.maxY), animated: animated) + } } func reinstall() { + self.packsView.removeAll() tableView?.removeFromSuperview() tableView = TableView(frame: bounds) - addSubview(tableView!) + var subviews:[NSView] = [tabsContainer, searchContainer,tableView!, emptyResults] + + restrictedView?.removeFromSuperview() + if let restrictedView = restrictedView { + subviews.append(restrictedView) + } + self.subviews = subviews } + fileprivate func merge(with transition: TableUpdateTransition, tabTransition: TableUpdateTransition, animated: Bool) { + self.tableView?.merge(with: transition) + self.packsView.merge(with: tabTransition) + if let tableView = tableView { + let emptySearchHidden: Bool = !tableView.isEmpty + + if !emptySearchHidden { + emptyResults.isHidden = false + } + emptyResults.change(opacity: emptySearchHidden ? 0 : 1, animated: animated, completion: { [weak self] completed in + if completed { + self?.emptyResults.isHidden = emptySearchHidden + } + }) + + } else { + emptyResults.isHidden = true + } + } func deinstall() { tableView?.removeFromSuperview() tableView = nil } - override func setFrameSize(_ newSize: NSSize) { - super.setFrameSize(newSize) - tableView?.setFrameSize(newSize) + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) + self.restrictedView?.updateLocalizationAndTheme(theme: theme) + emptyResults.background = theme.colors.background + emptyResults.image = theme.icons.stickersEmptySearch + searchView.updateLocalizationAndTheme(theme: theme) + separator.backgroundColor = theme.colors.border + } + + override func layout() { + super.layout() + + let initial: CGFloat = searchState?.state == .Focus ? -50 : 0 + + tabsContainer.frame = NSMakeRect(0, initial, frame.width, 50) + separator.frame = NSMakeRect(0, tabsContainer.frame.height - .borderSize, tabsContainer.frame.width, .borderSize) + packsView.frame = tabsContainer.focus(NSMakeSize(frame.width, 40)) + + + searchContainer.frame = NSMakeRect(0, tabsContainer.frame.maxY, frame.width, 50) + searchView.setFrameSize(NSMakeSize(frame.width - 20, 30)) + searchView.center() + + restrictedView?.setFrameSize(frame.size) + + if let tableView = tableView { + tableView.frame = NSMakeRect(0, searchContainer.frame.maxY, frame.width, frame.height - searchContainer.frame.maxY) + emptyResults.sizeToFit() + emptyResults.center() + } + progressView.center() } required init?(coder: NSCoder) { @@ -185,16 +369,71 @@ final class TableContainer : View { } } -class GIFViewController: TelegramGenericViewController { +class GIFViewController: TelegramGenericViewController, Notifable { + + + private var tabsState: ValuePromise = ValuePromise(.recent, ignoreRepeated: true) + + private let searchValue = ValuePromise(.init(state: .None, request: nil)) + private var searchState: SearchState = .init(state: .None, request: nil) { + didSet { + let value = searchState + if value.request.isEmpty { + self.searchValue.set(value) + } else { + self.searchValue.set(value) + } + + } + } + private var interactions:EntertainmentInteractions? + private weak var chatInteraction: ChatInteraction? private let disposable = MetaDisposable() - init(account:Account) { - super.init(account) + private let searchStateDisposable = MetaDisposable() + private let preloadDisposable = MetaDisposable() + var makeSearchCommand:((ESearchCommand)->Void)? + + var mode: EntertainmentViewController.Mode = .common + + override init(_ context: AccountContext) { + super.init(context) bar = .init(height: 0) } - func update(with interactions:EntertainmentInteractions?) { + private func updateSearchState(_ state: SearchState) { + self.searchState = state + if !state.request.isEmpty { + self.makeSearchCommand?(.loading) + } + if self.isLoaded() == true { + self.genericView.updateSearchState(state, animated: true) + self.genericView.tableView?.scroll(to: .up(true)) + } + } + + func update(with interactions:EntertainmentInteractions?, chatInteraction: ChatInteraction) { self.interactions = interactions + self.chatInteraction?.remove(observer: self) + self.chatInteraction = chatInteraction + chatInteraction.add(observer: self) + if isLoaded() { + genericView.updateRestricion(chatInteraction.presentation.peer) + } + } + + func notify(with value: Any, oldValue: Any, animated: Bool) { + if let value = value as? ChatPresentationInterfaceState, let oldValue = oldValue as? ChatPresentationInterfaceState, let peer = value.peer, let oldPeer = oldValue.peer { + if permissionText(from: peer, for: .banSendGifs) != permissionText(from: oldPeer, for: .banSendGifs) { + genericView.updateRestricion(peer) + } + } + } + + + + func isEqual(to other: Notifable) -> Bool { + return other === self } override func viewWillDisappear(_ animated: Bool) { @@ -204,42 +443,195 @@ class GIFViewController: TelegramGenericViewController { override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) - genericView.deinstall() + genericView.tableView?.removeAll() + genericView.tableView?.removeFromSuperview() + genericView.tableView = nil ready.set(.single(false)) } + + override var responderPriority: HandlerPriority { + return .modal + } + + override var canBecomeResponder: Bool { + if let view = context.sharedContext.bindings.rootNavigation().view as? SplitView { + return view.state == .single + } + return false + } + + override func becomeFirstResponder() -> Bool? { + return false + } + override func viewWillAppear(_ animated: Bool) { - super.viewDidAppear(animated) + super.viewWillAppear(animated) + + let value = GIFKeyboardConfiguration.with(appConfiguration: context.appConfiguration) genericView.reinstall() + genericView.updateRestricion(chatInteraction?.presentation.peer) + + + let searchInteractions = SearchInteractions({ [weak self] state, _ in + self?.updateSearchState(state) + }, { [weak self] state in + self?.updateSearchState(state) + }) + genericView.searchView.searchInteractions = searchInteractions _ = atomicSize.swap(_frameRect.size) - let arguments = RecentGifsArguments(sendGif: { [weak self] file in - self?.interactions?.sendGIF(file) + let arguments = RecentGifsArguments() + + arguments.sendAppFile = { [weak self] file, view, silent in + if let slowMode = self?.chatInteraction?.presentation.slowMode, slowMode.hasLocked { + showSlowModeTimeoutTooltip(slowMode, for: view) + } else { + self?.interactions?.sendGIF(file, silent) + self?.makeSearchCommand?(.close) + self?.interactions?.close() + } + } + + arguments.sendInlineResult = { [weak self] results, result, view in + if let slowMode = self?.chatInteraction?.presentation.slowMode, slowMode.hasLocked { + showSlowModeTimeoutTooltip(slowMode, for: view) + } else { + self?.chatInteraction?.sendInlineResult(results, result) + self?.makeSearchCommand?(.close) + self?.interactions?.close() + } + } + + arguments.searchBySuggestion = { [weak self] value in + self?.makeSearchCommand?(.apply(value)) + } + + let tabsArguments = GifTabsArguments(context: context, select: { [weak self] id in + self?.makeSearchCommand?(.close) + self?.tabsState.set(id) + self?.scrollup() }) - let previous:Atomic<[RecentGifRowEntry]> = Atomic(value: []) + let previous:Atomic<[InputContextEntry]> = Atomic(value: []) let initialSize = self.atomicSize - let account = self.account + let context = self.context + + struct SearchGifsState { + var request: String + var state: SearchFieldState + var values:[ChatContextResult] + var nextOffset: String + var tab: GifTabEntryId + } + + let loadNext: ValuePromise = ValuePromise(true, ignoreRepeated: false) + + let searchState:Atomic = Atomic(value: SearchGifsState(request: "", state: .None, values: [], nextOffset: "", tab: .recent)) - let signal = account.postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)]) |> deliverOn(prepareQueue) |> map { view -> TableUpdateTransition in - let postboxView = view.views[.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)] as! OrderedItemListView - let entries = recentEntries(for: postboxView, initialSize: initialSize.modify({$0})) - return prepareEntries(left: previous.swap(entries), right: entries, account: account, initialSize: initialSize.modify({$0}), arguments: arguments) + let mode = self.mode + + let signal = combineLatest(queue: prepareQueue, context.account.postbox.combinedView(keys: [.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)]), self.searchValue.get(), tabsState.get(), loadNext.get()) |> mapToSignal { view, search, selectedTab, _ -> Signal<(TableUpdateTransition, GifTabEntryId), NoError> in + + _ = searchState.modify { current -> SearchGifsState in + var current = current + if current.request != search.request || current.state != search.state || current.tab != selectedTab { + current.values = [] + current.nextOffset = "" + } + current.request = search.request + current.state = search.state + current.tab = selectedTab + return current + } + + switch search.state { + case .Focus: + let searchSignal = context.engine.stickers.searchGifs(query: search.request, nextOffset: searchState.with { $0.nextOffset }) + return searchSignal |> map { result in + _ = searchState.modify { current -> SearchGifsState in + var current = current + current.values += (result?.results ?? []) + current.nextOffset = result?.nextOffset ?? "" + return current + } + let entries = gifEntries(for: result, results: searchState.with { $0.values }, initialSize: initialSize.with { $0 }, mode: mode) + return (prepareEntries(left: previous.swap(entries), right: entries, context: context, initialSize: initialSize.with { $0 }, arguments: arguments, mode: mode), selectedTab) + } + default: + var request: String? = nil + + switch selectedTab { + case .recent: + break + case .trending: + request = "" + case let .recommended(value): + request = value + } + if let request = request { + + let searchSignal = context.engine.stickers.searchGifs(query: request, nextOffset: searchState.with { $0.nextOffset }) + return searchSignal |> map { result in + _ = searchState.modify { current -> SearchGifsState in + var current = current + current.values += (result?.results ?? []) + current.nextOffset = result?.nextOffset ?? "" + return current + } + let entries = gifEntries(for: result, results: searchState.with { $0.values }, initialSize: initialSize.with { $0 }, mode: mode) + return (prepareEntries(left: previous.swap(entries), right: entries, context: context, initialSize: initialSize.with { $0 }, arguments: arguments, mode: mode), selectedTab) + } + } else { + let postboxView = view.views[.orderedItemList(id: Namespaces.OrderedItemList.CloudRecentGifs)] as! OrderedItemListView + let entries = recentEntries(for: postboxView, initialSize: initialSize.with { $0 }).sorted(by: <) + return .single((prepareEntries(left: previous.swap(entries), right: entries, context: context, initialSize: initialSize.with { $0 }, arguments: arguments, mode: mode), selectedTab)) + } + + } + } |> deliverOnMainQueue + + var firstTime: Bool = true + + let previvousTabs: Atomic<[GifTabEntry]> = Atomic(value: []) + + let transitions: Signal<(TableUpdateTransition, TableUpdateTransition), NoError> = signal |> map { transition, id in + let entries = tabsEntries(value.emojis, selected: id) + return (transition, prepareTabTransition(left: previvousTabs.swap(entries), right: entries, initialSize: initialSize.with { $0 }, arguments: tabsArguments)) } |> deliverOnMainQueue - disposable.set(signal.start(next: { [weak self] transition in - self?.genericView.tableView?.merge(with: transition) + disposable.set(transitions.start(next: { [weak self] transition, tabTransition in + self?.genericView.merge(with: transition, tabTransition: tabTransition, animated: !firstTime) + self?.makeSearchCommand?(.normal) + firstTime = false self?.ready.set(.single(true)) })) + + + genericView.tableView?.setScrollHandler { position in + if !searchState.with({ $0.values.isEmpty && !$0.nextOffset.isEmpty }) { + switch position.direction { + case .bottom: + loadNext.set(true) + default: + break + } + } + } } + override func scrollup(force: Bool = false) { + self.genericView.tableView?.scroll(to: .up(true)) + } deinit { disposable.dispose() - NSLog("deinit gifs controller") + searchStateDisposable.dispose() + chatInteraction?.remove(observer: self) + preloadDisposable.dispose() } } diff --git a/Telegram-Mac/GalleryControls.swift b/Telegram-Mac/GalleryControls.swift deleted file mode 100644 index 4720386cb4..0000000000 --- a/Telegram-Mac/GalleryControls.swift +++ /dev/null @@ -1,209 +0,0 @@ -// -// GalleryControls.swift -// Telegram-Mac -// -// Created by keepcoder on 07/10/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import TGUIKit -import SwiftSignalKitMac -import TelegramCoreMac - -private let prevImage = #imageLiteral(resourceName: "Icon_GalleryLeft").precomposed() -private let nextImage = #imageLiteral(resourceName: "Icon_GalleryRight").precomposed() -private let moreImage = #imageLiteral(resourceName: "Icon_GalleryMore").precomposed() -private let dismissImage = #imageLiteral(resourceName: "Icon_GalleryDismiss").precomposed() - - - - - -class GalleryControls: Node { - - let index:Promise<(Int,Int)> = Promise() - private let interactions:GalleryInteractions - - - override var backgroundColor: NSColor? { - return .blackTransparent - } - - init(_ view: View? = nil, interactions:GalleryInteractions) { - self.interactions = interactions - super.init(view) - view?.layer?.opacity = 0.0 - } - - func animateIn() -> Void { - self.setNeedDisplay() - - if let view = view { - view.centerX(y: 10.0) - view.layer?.opacity = 1.0 - view.layer?.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) - view.layer?.animatePosition(from: NSMakePoint(view.frame.minX, -view.frame.height), to: NSMakePoint(view.frame.minX, 10), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) - } - - } - - func animateOut() -> Void { - self.setNeedDisplay() - - if let view = view { - - - view.layer?.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) - view.layer?.animatePosition(from: view.frame.origin, to: NSMakePoint(view.frame.minX, -view.frame.height), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion:false) - } - - } - - override func draw(_ layer: CALayer, in ctx: CGContext) { - ctx.round(layer.frame.size,.cornerRadius) - super.draw(layer, in: ctx) - } - - - -} - -class GalleryGeneralControls : GalleryControls { - - private let previous:ImageButton = ImageButton() - private let next:ImageButton = ImageButton() - private let more:ImageButton = ImageButton() - private let dismiss:ImageButton = ImageButton() - private let counter:TitleButton = TitleButton() - - - private let disposable:MetaDisposable = MetaDisposable() - - override var backgroundColor: NSColor? { - return .blackTransparent - } - - override init(_ view: View? = nil, interactions:GalleryInteractions) { - - - super.init(view, interactions: interactions) - - counter.style = galleryButtonStyle - previous.style = galleryButtonStyle - next.style = galleryButtonStyle - more.style = galleryButtonStyle - dismiss.style = galleryButtonStyle - - previous.set(image: prevImage, for: .Normal) - next.set(image: nextImage, for: .Normal) - more.set(image: moreImage, for: .Normal) - dismiss.set(image: dismissImage, for: .Normal) - - previous.set(handler: {_ in _ = interactions.previous()}, for: .Click) - next.set(handler: {_ in _ = interactions.next()}, for: .Click) - more.set(handler: { control in _ = interactions.showActions(control)}, for: .Click) - dismiss.set(handler: {_ in _ = interactions.dismiss()}, for: .Click) - - if let view = view { - - counter.sizeToFit(NSZeroSize, NSMakeSize(150, view.frame.height)) - counter.center(view) - - let bwidth = (view.frame.width - counter.frame.width) / 4.0 - - previous.frame = NSMakeRect(0, 0, bwidth, view.frame.height) - next.frame = NSMakeRect(previous.frame.maxX, 0, bwidth, view.frame.height) - more.frame = NSMakeRect(counter.frame.maxX, 0, bwidth, view.frame.height) - dismiss.frame = NSMakeRect(more.frame.maxX, 0, bwidth, view.frame.height) - - view.addSubview(previous) - view.addSubview(next) - view.addSubview(counter) - view.addSubview(more) - view.addSubview(dismiss) - - } - - disposable.set(index.get().start(next: {[weak self] (current, total) in - self?.counter.set(text: tr(.galleryCounter(current, total)), for: .Normal) - })) - - - } - - deinit { - disposable.dispose() - } -} - - -class GallerySecretControls : GalleryControls { - private let progress:TimableProgressView = TimableProgressView(TimableProgressTheme(seconds: 20)) - private let duration: TitleButton = TitleButton() - private let dismiss:ImageButton = ImageButton() - private var timer:SwiftSignalKitMac.Timer? = nil - override init(_ view: View?, interactions: GalleryInteractions) { - super.init(view, interactions: interactions) - if let view = view { - duration.set(font: .bold(.header), for: .Normal) - duration.set(color: .white, for: .Normal) - duration.set(text: "20 sec", for: .Normal) - dismiss.set(image: dismissImage, for: .Normal) - dismiss.sizeToFit() - dismiss.set(handler: {_ in _ = interactions.dismiss()}, for: .Click) - view.addSubview(progress) - view.addSubview(duration) - view.addSubview(dismiss) - - } - } - - func update(with attribute: AutoremoveTimeoutMessageAttribute, outgoing: Bool) { - timer?.invalidate() - if !outgoing { - progress.isHidden = false - duration.isHidden = false - dismiss.centerY(x: frame.width - dismiss.frame.width - 20) - progress.centerY(x: 20) - duration.center() - if let countdownBeginTime = attribute.countdownBeginTime { - - let difference:()->TimeInterval = { - return TimeInterval((countdownBeginTime + attribute.timeout)) - (CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - } - let start = difference() / Double(attribute.timeout) * 100.0 - progress.theme = TimableProgressTheme(seconds: difference(), start: start) - - let updateTitle:()->Void = { [weak self] in - self?.duration.set(text: String.stringForShortCallDurationSeconds(for: Int32(difference())), for: .Normal) - self?.duration.center() - } - updateTitle() - timer = SwiftSignalKitMac.Timer(timeout: 1, repeat: true, completion: updateTitle, queue: Queue.mainQueue()) - timer?.start() - } else { - progress.theme = TimableProgressTheme(seconds: TimeInterval(attribute.timeout)) - duration.set(text: String.stringForShortCallDurationSeconds(for: attribute.timeout), for: .Normal) - duration.center() - } - progress.progress = 0 - progress.startAnimation() - } else { - progress.isHidden = true - duration.isHidden = true - dismiss.center() - } - - } - - override func animateIn() { - super.animateIn() - } - - override func animateOut() { - super.animateOut() - progress.stopAnimation() - } - -} diff --git a/Telegram-Mac/GalleryMessageEntry.swift b/Telegram-Mac/GalleryMessageEntry.swift deleted file mode 100644 index ef66958456..0000000000 --- a/Telegram-Mac/GalleryMessageEntry.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// GalleryMessageEntry.swift -// Telegram -// -// Created by keepcoder on 22/09/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - -import Cocoa - - diff --git a/Telegram-Mac/GalleryModernControls.swift b/Telegram-Mac/GalleryModernControls.swift new file mode 100644 index 0000000000..545c400d66 --- /dev/null +++ b/Telegram-Mac/GalleryModernControls.swift @@ -0,0 +1,371 @@ +// +// GalleryModernControls.swift +// Telegram +// +// Created by Mikhail Filimonov on 28/08/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + +class GalleryModernControlsView: View { + + fileprivate let photoView: AvatarControl = AvatarControl(font: .avatar(18)) + private var nameNode: (TextNodeLayout, TextNode)? = nil + private var dateNode: (TextNodeLayout, TextNode)? = nil + private let shareControl: ImageButton = ImageButton() + private let moreControl: ImageButton = ImageButton() + + fileprivate let zoomInControl: ImageButton = ImageButton() + fileprivate let zoomOutControl: ImageButton = ImageButton() + private let rotateControl: ImageButton = ImageButton() + private let fastSaveControl: ImageButton = ImageButton() + + fileprivate var interactions: GalleryInteractions? + fileprivate var thumbs: GalleryThumbsControlView? { + didSet { + oldValue?.removeFromSuperview() + if let thumbs = thumbs { + addSubview(thumbs, positioned: .below, relativeTo: self.subviews.first) + thumbs.setFrameOrigin(NSMakePoint((self.frame.width - thumbs.frame.width) / 2 + (thumbs.frame.width - thumbs.documentSize.width) / 2, (self.frame.height - thumbs.frame.height) / 2)) + } + needsLayout = true + } + } + private var currentState:(peer: Peer?, timestamp: TimeInterval, account: Account)? { + didSet { + updateInterface() + } + } + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + // backgroundColor = .blackTransparent + photoView.setFrameSize(60, 60) + addSubview(photoView) + addSubview(shareControl) + addSubview(moreControl) + photoView.userInteractionEnabled = false + shareControl.autohighlight = false + moreControl.autohighlight = false + + let shareIcon = NSImage(cgImage: theme.icons.galleryShare, size: theme.icons.galleryShare.backingSize).precomposed(NSColor.white.withAlphaComponent(0.7)) + let moreIcon = NSImage(cgImage: theme.icons.galleryMore, size: theme.icons.galleryMore.backingSize).precomposed(NSColor.white.withAlphaComponent(0.7)) + let fastSaveIcon = NSImage(cgImage: theme.icons.galleryFastSave, size: theme.icons.galleryFastSave.backingSize).precomposed(NSColor.white.withAlphaComponent(0.7)) + + + shareControl.set(image: shareIcon, for: .Normal) + moreControl.set(image: moreIcon, for: .Normal) + fastSaveControl.set(image: fastSaveIcon, for: .Normal) + + + shareControl.set(image: theme.icons.galleryShare, for: .Hover) + moreControl.set(image: theme.icons.galleryMore, for: .Hover) + shareControl.set(image: theme.icons.galleryShare, for: .Highlight) + moreControl.set(image: theme.icons.galleryMore, for: .Highlight) + + fastSaveControl.set(image: theme.icons.galleryFastSave, for: .Hover) + fastSaveControl.set(image: theme.icons.galleryFastSave, for: .Highlight) + + _ = moreControl.sizeToFit(NSZeroSize, NSMakeSize(60, 60), thatFit: true) + _ = shareControl.sizeToFit(NSZeroSize, NSMakeSize(60, 60), thatFit: true) + + addSubview(fastSaveControl) + addSubview(zoomInControl) + addSubview(zoomOutControl) + addSubview(rotateControl) + + let zoomIn = NSImage(cgImage: theme.icons.galleryZoomIn, size: theme.icons.galleryZoomIn.backingSize).precomposed(NSColor.white.withAlphaComponent(0.7)) + let zoomOut = NSImage(cgImage: theme.icons.galleryZoomOut, size: theme.icons.galleryZoomOut.backingSize).precomposed(NSColor.white.withAlphaComponent(0.7)) + let rotate = NSImage(cgImage: theme.icons.galleryRotate, size: theme.icons.galleryRotate.backingSize).precomposed(NSColor.white.withAlphaComponent(0.7)) + + + zoomInControl.set(image: zoomIn, for: .Normal) + zoomOutControl.set(image: zoomOut, for: .Normal) + rotateControl.set(image: rotate, for: .Normal) + + zoomInControl.set(image: theme.icons.galleryZoomIn, for: .Hover) + zoomOutControl.set(image: theme.icons.galleryZoomOut, for: .Hover) + rotateControl.set(image: theme.icons.galleryRotate, for: .Hover) + + zoomInControl.set(image: theme.icons.galleryZoomIn, for: .Highlight) + zoomOutControl.set(image: theme.icons.galleryZoomOut, for: .Highlight) + rotateControl.set(image: theme.icons.galleryRotate, for: .Highlight) + + + _ = zoomInControl.sizeToFit(NSZeroSize, NSMakeSize(60, 60), thatFit: true) + _ = zoomOutControl.sizeToFit(NSZeroSize, NSMakeSize(60, 60), thatFit: true) + _ = rotateControl.sizeToFit(NSZeroSize, NSMakeSize(60, 60), thatFit: true) + _ = fastSaveControl.sizeToFit(NSZeroSize, NSMakeSize(60, 60), thatFit: true) + + shareControl.set(handler: { [weak self] control in + _ = self?.interactions?.share(control) + }, for: .Click) + + moreControl.set(handler: { [weak self] control in + _ = self?.interactions?.showActions(control) + }, for: .Click) + + rotateControl.set(handler: { [weak self] _ in + self?.interactions?.rotateLeft() + }, for: .Click) + + zoomInControl.set(handler: { [weak self] _ in + self?.interactions?.zoomIn() + }, for: .Click) + + zoomOutControl.set(handler: { [weak self] _ in + self?.interactions?.zoomOut() + }, for: .Click) + + fastSaveControl.set(handler: { [weak self] _ in + self?.interactions?.fastSave() + }, for: .Click) + } + + override func mouseUp(with event: NSEvent) { + let point = self.convert(event.locationInWindow, from: nil) + if let currentState = currentState { + if NSPointInRect(point, photoView.frame) || NSPointInRect(point, nameRect), let peerId = currentState.peer?.id { + interactions?.openInfo(peerId) + } else if NSPointInRect(point, dateRect) { + interactions?.openMessage() + } else if let thumbs = thumbs, !NSPointInRect(point, thumbs.frame) { + _ = interactions?.dismiss(event) + } + } + + } + + private var nameRect: NSRect { + if let nameNode = nameNode { + return NSMakeRect(photoView.frame.maxX + 10, photoView.frame.midY - nameNode.0.size.height - 2, nameNode.0.size.width, nameNode.0.size.height) + } + return NSZeroRect + } + private var dateRect: NSRect { + if let dateNode = dateNode { + return NSMakeRect(photoView.frame.maxX + 10, photoView.frame.midY + 2, dateNode.0.size.width, dateNode.0.size.height) + } + return NSZeroRect + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + super.draw(layer, in: ctx) + if let nameNode = nameNode { + var point = NSMakePoint(photoView.frame.maxX + 10, photoView.frame.midY - nameNode.0.size.height - 2) + if dateNode == nil { + point.y = photoView.frame.midY - floorToScreenPixels(backingScaleFactor, (nameNode.0.size.height / 2)) + } + nameNode.1.draw(NSMakeRect(point.x, point.y, nameNode.0.size.width, nameNode.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: .clear) + } + if let dateNode = dateNode { + dateNode.1.draw(NSMakeRect(photoView.frame.maxX + 10, photoView.frame.midY + 2, dateNode.0.size.width, dateNode.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: .clear) + } + } + + func updateControlsVisible(_ entry: GalleryEntry) { + switch entry { + + case let .instantMedia(media, _): + if media.media is TelegramMediaImage { + zoomInControl.isHidden = false + zoomOutControl.isHidden = false + rotateControl.isHidden = false + fastSaveControl.isHidden = false + } else if let file = media.media as? TelegramMediaFile { + if file.isVideo { + zoomInControl.isHidden = false + zoomOutControl.isHidden = false + rotateControl.isHidden = true + fastSaveControl.isHidden = false + } + } + case let .message(message): + if let message = message.message, message.containsSecretMedia { + + } + if message.message?.media.first is TelegramMediaImage { + zoomInControl.isHidden = false + zoomOutControl.isHidden = false + rotateControl.isHidden = false + fastSaveControl.isHidden = message.message?.containsSecretMedia == true + } else if let file = message.message?.media.first as? TelegramMediaFile { + if file.isVideo { + zoomInControl.isHidden = false + zoomOutControl.isHidden = false + rotateControl.isHidden = true + fastSaveControl.isHidden = message.message?.containsSecretMedia == true + } else if !file.isGraphicFile { + zoomInControl.isHidden = false + zoomOutControl.isHidden = false + rotateControl.isHidden = true + fastSaveControl.isHidden = message.message?.containsSecretMedia == true + } + } else if let webpage = message.message?.media.first as? TelegramMediaWebpage { + if case let .Loaded(content) = webpage.content { + if ExternalVideoLoader.isPlayable(content) { + zoomInControl.isHidden = false + zoomOutControl.isHidden = false + rotateControl.isHidden = true + fastSaveControl.isHidden = true + } + } + } else { + zoomInControl.isHidden = true + zoomOutControl.isHidden = true + rotateControl.isHidden = true + fastSaveControl.isHidden = true + } + case let .photo(_, _, photo, _, _, _, _): + zoomInControl.isHidden = false + zoomOutControl.isHidden = false + rotateControl.isHidden = !photo.videoRepresentations.isEmpty + fastSaveControl.isHidden = false + default: + zoomInControl.isHidden = false + zoomOutControl.isHidden = false + rotateControl.isHidden = false + fastSaveControl.isHidden = false + } + } + + func updatePeer(_ peer: Peer?, timestamp: TimeInterval, account: Account, canShare: Bool) { + currentState = (peer, timestamp, account) + shareControl.isHidden = !canShare + needsLayout = true + } + + override func viewWillMove(toWindow newWindow: NSWindow?) { + super.viewWillMove(toWindow: newWindow) + + if let window = newWindow as? Window { + window.set(mouseHandler: { [weak self] _ -> KeyHandlerResult in + self?.updateVisibility() + return .rejected + }, with: self, for: .mouseMoved) + } else { + (self.window as? Window)?.remove(object: self, for: .mouseMoved) + } + } + + private func updateInterface() { + guard let window = window else {return} + + let point = self.convert(window.mouseLocationOutsideOfEventStream, from: nil) + if let currentState = currentState { + photoView.setPeer(account: currentState.account, peer: currentState.peer) + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .short + formatter.doesRelativeDateFormatting = true + formatter.timeZone = NSTimeZone.local + nameNode = TextNode.layoutText(.initialize(string: currentState.peer?.displayTitle.prefixWithDots(30) ?? L10n.peerDeletedUser, color: NSPointInRect(point, nameRect) ? .white : .grayText, font: .medium(.huge)), nil, 1, .end, NSMakeSize(frame.width, 20), nil, false, .left) + dateNode = currentState.timestamp == 0 ? nil : TextNode.layoutText(.initialize(string: formatter.string(from: Date(timeIntervalSince1970: currentState.timestamp)), color: NSPointInRect(point, dateRect) ? .white : .grayText, font: .normal(.title)), nil, 1, .end, NSMakeSize(frame.width, 20), nil, false, .left) + } + + photoView._change(opacity: NSPointInRect(point, photoView.frame) ? 1 : 0.7, animated: false) + + needsDisplay = true + } + + fileprivate var isInside: Bool = false { + didSet { + updateInterface() + } + } + + func updateVisibility() { + if frame.minY >= 0 { + isInside = mouseInside() + } + } + + override func layout() { + super.layout() + photoView.centerY(x: 80) + + moreControl.centerY(x: frame.width - moreControl.frame.width - 80) + shareControl.centerY(x: moreControl.frame.minX - shareControl.frame.width) + + let alignControl = shareControl.isHidden ? moreControl : shareControl + fastSaveControl.centerY(x: alignControl.frame.minX - fastSaveControl.frame.width) + rotateControl.centerY(x: (fastSaveControl.isHidden ? alignControl.frame.minX : fastSaveControl.frame.minX) - rotateControl.frame.width - 60) + zoomInControl.centerY(x: (rotateControl.isHidden ? alignControl.frame.minX - 60 : rotateControl.frame.minX) - zoomInControl.frame.width) + zoomOutControl.centerY(x: zoomInControl.frame.minX - zoomOutControl.frame.width) + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + +} + + +class GalleryModernControls: GenericViewController { + private let context: AccountContext + private let interactions: GalleryInteractions + private let thumbs: GalleryThumbsControl + private let peerDisposable = MetaDisposable() + private let zoomControlsDisposable = MetaDisposable() + init(_ context: AccountContext, interactions: GalleryInteractions, frame: NSRect, thumbsControl: GalleryThumbsControl) { + self.context = context + self.interactions = interactions + thumbs = thumbsControl + super.init(frame: frame) + } + + override func viewDidLoad() { + super.viewDidLoad() + genericView.thumbs = thumbs.genericView + genericView.interactions = interactions + + thumbs.afterLayoutTransition = { [weak self] animated in + guard let `self` = self else { return } + self.thumbs.genericView.change(pos: NSMakePoint((self.frame.width - self.thumbs.frame.width) / 2 + (self.thumbs.frame.width - self.thumbs.genericView.documentSize.width) / 2, (self.frame.height - self.thumbs.frame.height) / 2), animated: animated) + } + } + + deinit { + peerDisposable.dispose() + zoomControlsDisposable.dispose() + } + + + func update(_ item: MGalleryItem?) { + if let item = item { + if let interfaceState = item.entry.interfaceState { + self.genericView.updateControlsVisible(item.entry) + peerDisposable.set((context.account.postbox.loadedPeerWithId(interfaceState.0) |> deliverOnMainQueue).start(next: { [weak self, weak item] peer in + guard let `self` = self, let item = item else {return} + self.genericView.updatePeer(peer, timestamp: interfaceState.1 == 0 ? 0 : interfaceState.1 - self.context.timeDifference, account: self.context.account, canShare: item.entry.canShare) + })) + zoomControlsDisposable.set((item.magnify.get() |> deliverOnMainQueue).start(next: { [weak self, weak item] value in + if let item = item { + self?.genericView.zoomOutControl.isEnabled = item.minMagnify < value + self?.genericView.zoomInControl.isEnabled = item.maxMagnify > value + } + })) + } + } + } + + + + + func animateIn() { + genericView.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + genericView.change(pos: NSMakePoint(0, 0), animated: true, timingFunction: CAMediaTimingFunctionName.spring) + } + + func animateOut() { + genericView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false) + } +} diff --git a/Telegram-Mac/GalleryPageController.swift b/Telegram-Mac/GalleryPageController.swift index 230f983587..dafdb71cd8 100644 --- a/Telegram-Mac/GalleryPageController.swift +++ b/Telegram-Mac/GalleryPageController.swift @@ -8,23 +8,159 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac +import SwiftSignalKit import AVFoundation +import AVKit +import TelegramCore +import Postbox -fileprivate extension MagnifyView { +fileprivate class GMagnifyView : MagnifyView { + private var progressView: RadialProgressView? + + fileprivate let statusDisposable = MetaDisposable() + var minX:CGFloat { if contentView.frame.minX > 0 { return frame.minX + contentView.frame.minX } return frame.minX } + + override var isOpaque: Bool { + return true + } + + override func scrollWheel(with event: NSEvent) { + if magnify == 1.0 { + superview?.scrollWheel(with: event) + } else { + super.scrollWheel(with: event) + } + } + + func updateStatus(_ status: Signal) { + statusDisposable.set((status |> deliverOnMainQueue).start(next: { [weak self] status in + self?.updateProgress(status) + })) + } + private func updateProgress(_ status: MediaResourceStatus) { + + switch status { + case let .Fetching(_, progress): + if self.progressView == nil { + self.progressView = RadialProgressView() + self.addSubview(self.progressView!) + self.progressView!.center() + } + progressView?.state = .ImpossibleFetching(progress: progress, force: false) + case .Local: + + if let progressView = self.progressView { + progressView.state = .ImpossibleFetching(progress: 1, force: false) + self.progressView = nil + progressView.layer?.animateAlpha(from: 1, to: 0, duration: 0.25, timingFunction: .linear, removeOnCompletion: false, completion: { [weak progressView] complete in + if complete { + progressView?.removeFromSuperview() + } + }) + } + case .Remote: + if self.progressView == nil { + self.progressView = RadialProgressView() + self.addSubview(self.progressView!) + self.progressView!.center() + } + progressView?.state = .Remote + } + + progressView?.userInteractionEnabled = status != .Local + } + + override func mouseInside() -> Bool { + return super.mouseInside() + } + + deinit { + statusDisposable.dispose() + } + + private let fillFrame:(GMagnifyView)->NSRect + private let prevAction:()->Void + private let nextAction:()->Void + private let hasPrev:()->Bool + private let hasNext:()->Bool + private let dismiss:()->Void + private let prev: Control + private let next: Control + init(_ contentView: NSView, contentSize: NSSize, prev: Control, next: Control, fillFrame:@escaping(GMagnifyView)->NSRect, prevAction: @escaping()->Void, nextAction:@escaping()->Void, hasPrev: @escaping()->Bool, hasNext:@escaping()->Bool, dismiss:@escaping()->Void) { + self.fillFrame = fillFrame + self.prevAction = prevAction + self.nextAction = nextAction + self.prev = prev + self.next = next + self.hasPrev = hasPrev + self.hasNext = hasNext + self.dismiss = dismiss + super.init(contentView, contentSize: contentSize) + prev.alphaValue = 0 + next.alphaValue = 0 + + } + + override var contentSize: NSSize { + didSet { + if frame.origin != NSZeroPoint { + self.frame = fillFrame(self) + } + } + } + + + override func mouseUp(with theEvent: NSEvent) { + let point = convert(theEvent.locationInWindow, from: nil) + + if point.x > frame.width - 80 && self.hasNext() { + nextAction() + } else if point.x < 80 && self.hasPrev() { + prevAction() + } else { + dismiss() + } + } + + func hideOrShowControls(hasPrev: Bool, hasNext: Bool, animated: Bool) { + guard let window = window as? Window else {return} + + let point = convert(window.mouseLocationOutsideOfEventStream, from: nil) + (animated ? prev.animator() : prev).alphaValue = point.x > 80 || !hasPrev ? 0 : 1 + (animated ? next.animator() : next).alphaValue = point.x < frame.width - 80 || !hasNext ? 0 : 1 + } + + override func add(magnify: CGFloat, for location: NSPoint, animated: Bool) { + super.add(magnify: magnify, for: location, animated: animated) + } + + override func setFrameSize(_ newSize: NSSize) { + super.setFrameSize(newSize) + progressView?.center() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + } class GalleryPageView : NSView { init() { super.init(frame:NSZeroRect) - //self.wantsLayer = true + self.wantsLayer = true + self.canDrawSubviewsIntoLayer = true + } + + override func setFrameSize(_ newSize: NSSize) { + super.setFrameSize(newSize) } @@ -34,10 +170,24 @@ class GalleryPageView : NSView { } +private final class PageController : NSPageController { + + + deinit { + var bp:Int = 0 + bp += 1 + } + + override func scrollWheel(with event: NSEvent) { + super.scrollWheel(with: event) + } +} + class GalleryPageController : NSObject, NSPageControllerDelegate { - private let controller:NSPageController = NSPageController() + private let controller:PageController = PageController() private let ioDisposabe:MetaDisposable = MetaDisposable() + private let smartUpdaterDisposable = MetaDisposable() private var identifiers:[NSPageController.ObjectIdentifier:MGalleryItem] = [:] private var cache:NSCache = NSCache() private var queuedTransitions:[UpdateTransition] = [] @@ -52,38 +202,149 @@ class GalleryPageController : NSObject, NSPageControllerDelegate { private var startIndex:Int = -1 let view:GalleryPageView = GalleryPageView() private let captionView: TextView = TextView() + private let captionContainer = View() + private let captionScrollView = ScrollView() private let window:Window + private let autohideCaptionDisposable = MetaDisposable() + private let magnifyDisposable = MetaDisposable() let selectedIndex:ValuePromise = ValuePromise(ignoreRepeated: false) - init(frame:NSRect, contentInset:NSEdgeInsets, interactions:GalleryInteractions, window:Window) { + let thumbsControl: GalleryThumbsControl + private let indexDisposable = MetaDisposable() + fileprivate let reversed: Bool + private let navigationDisposable = MetaDisposable() + private let _prev: ImageButton = ImageButton() + private let _next: ImageButton = ImageButton() + + private var hasInited: Bool = false + + var selectedItemChanged: ((@escaping(MGalleryItem) -> Void)->Void)! + var transition: ((@escaping(UpdateTransition, MGalleryItem?) -> Void) -> Void)! + + private var transitionCallFunc: ((UpdateTransition, MGalleryItem?) -> Void)? + private var selectedItemCallFunc: ((MGalleryItem) -> Void)? + private let interactions: GalleryInteractions + init(frame:NSRect, contentInset:NSEdgeInsets, interactions:GalleryInteractions, window:Window, reversed: Bool) { self.contentInset = contentInset self.window = window - + self.reversed = reversed + thumbsControl = GalleryThumbsControl(interactions: interactions) + self.interactions = interactions super.init() + + self.selectedItemChanged = { [weak self] selectedItemCallFunc in + self?.selectedItemCallFunc = selectedItemCallFunc + } + + self.transition = { [weak self] transitionCallFunc in + self?.transitionCallFunc = transitionCallFunc + } + + _prev.animates = true + _next.animates = true + + _prev.autohighlight = false + _next.autohighlight = false + _prev.set(image: theme.icons.galleryPrev, for: .Normal) + _next.set(image: theme.icons.galleryNext, for: .Normal) + + _prev.set(background: .clear, for: .Normal) + _next.set(background: .clear, for: .Normal) + + + _prev.frame = NSMakeRect(0, 0, 60, frame.height) + _next.frame = NSMakeRect(frame.width - 60, 0, 60, frame.height) + + _next.userInteractionEnabled = false + _prev.userInteractionEnabled = false + + captionContainer.addSubview(captionView) + captionScrollView.documentView = captionContainer + + indexDisposable.set((selectedIndex.get()).start(next: { [weak self] index in + guard let `self` = self else {return} + + let transition = self.thumbsControl.layoutItems(with: self.items, selectedIndex: index, animated: true) + self.transitionCallFunc?(transition, self.selectedItem) + })) + cache.countLimit = 10 + captionView.isSelectable = false + captionView.userInteractionEnabled = true - window.set(mouseHandler: { [weak self] (_) -> KeyHandlerResult in - if let view = self?.controller.selectedViewController?.view as? MagnifyView, let window = view.window { + var dragged: NSPoint? = nil + + window.set(mouseHandler: { [weak self] event -> KeyHandlerResult in + guard let `self` = self, !hasModals(self.window) else {return .rejected} + + if let view = self.controller.selectedViewController?.view as? GMagnifyView { + if let view = view.contentView as? SVideoView, view.insideControls { + dragged = nil + return .invokeNext + } - let point = window.mouseLocationOutsideOfEventStream - if point.x < view.minX && !view.mouseInContent && view.magnify == 1.0 { - _ = interactions.previous() - } else if view.mouseInContent && view.magnify == 1.0 { - _ = interactions.next() - } else { - let hitTestView = window.contentView?.hitTest(point) - if hitTestView is GalleryBackgroundView || view.contentView == hitTestView?.subviews.first { - _ = interactions.dismiss() - - } else { - return .invokeNext - } + } + + if let _dragged = dragged { + let difference = NSMakePoint(abs(_dragged.x - event.locationInWindow.x), abs(_dragged.y - event.locationInWindow.y)) + if difference.x >= 10 || difference.y >= 10 { + dragged = nil + return .invoked + } + } + dragged = nil + + let view = self.window.contentView?.hitTest(event.locationInWindow) + let point = self.controller.view.convert(event.locationInWindow, from: nil) + + if NSPointInRect(point, self.captionScrollView.frame), self.captionScrollView.layer?.opacity != 0, let captionLayout = self.captionView.layout, captionLayout.link(at: self.captionView.convert(event.locationInWindow, from: nil)) != nil { + self.captionView.mouseUp(with: event) + return .invoked + } else if self.captionView.mouseInside() { + return .invoked + } + if let view = self.controller.selectedViewController?.view as? GMagnifyView, let window = view.window as? Window, self.controller.view._mouseInside() { + guard event.locationInWindow.x > 80 && event.locationInWindow.x < window.frame.width - 80 else { + view.mouseUp(with: event) + return .invoked } + let hitTestView = self.window.contentView?.hitTest(event.locationInWindow) + if hitTestView is Control { + return .rejected + } + + if let view = view.contentView as? SVideoView, view.insideControls { + return .rejected + } + if hasPictureInPicture { + return .rejected + } + + _ = interactions.dismiss(event) + return .invoked + } else if view is GalleryModernControlsView { + _ = interactions.dismiss(event) + return .invoked } - return .invoked + return .invokeNext }, with: self, for: .leftMouseUp) + window.set(mouseHandler: { event -> KeyHandlerResult in + dragged = nil + return .rejected + }, with: self, for: .leftMouseDown) + + window.set(mouseHandler: { event -> KeyHandlerResult in + guard dragged == nil else {return .rejected} + dragged = event.locationInWindow + return .rejected + }, with: self, for: .leftMouseDragged) + + window.set(responder: { [weak self] () -> NSResponder? in + return self?.controller.selectedViewController?.view + }, with: self, priority: .high) + window.set(mouseHandler: { [weak self] (_) -> KeyHandlerResult in if let strongSelf = self { if strongSelf.lockedTransition { @@ -95,9 +356,20 @@ class GalleryPageController : NSObject, NSPageControllerDelegate { return .invoked }, with: self, for: .scrollWheel) + window.set(mouseHandler: { [weak self] (_) -> KeyHandlerResult in + guard let `self` = self else {return .rejected} + self.autohideCaptionDisposable.set(nil) + if self.lockedTransition == false { + self.captionScrollView.change(opacity: 1.0) + self.configureCaptionAutohide() + } + (self.controller.selectedViewController?.view as? GMagnifyView)?.hideOrShowControls(hasPrev: self.hasPrev, hasNext: self.hasNext, animated: true) + return .rejected + }, with: self, for: .mouseMoved) + window.set(mouseHandler: { [weak self] (_) -> KeyHandlerResult in - if let view = self?.controller.selectedViewController?.view as? MagnifyView, let window = view.window { + if let view = self?.controller.selectedViewController?.view as? GMagnifyView, let window = view.window { let point = window.mouseLocationOutsideOfEventStream let hitTestView = window.contentView?.hitTest(point) @@ -113,27 +385,51 @@ class GalleryPageController : NSObject, NSPageControllerDelegate { return .invoked }, with: self, for: .rightMouseUp) - // view.background = .blackTransparent controller.view = view controller.view.frame = frame controller.delegate = self controller.transitionStyle = .horizontalStrip } - func merge(with transition:UpdateTransition) -> Bool { + private func configureCaptionAutohide() { + let view = controller.selectedViewController?.view as? MagnifyView + if captionScrollView.superview != nil { + captionScrollView.removeFromSuperview() + controller.view.addSubview(captionScrollView) + } + autohideCaptionDisposable.set((Signal.single(Void()) |> delay(view?.mouseInContent == true ? 5.0 : 1.5, queue: Queue.mainQueue())).start(next: { [weak self] in + guard let `self` = self else { + return + } + self.captionScrollView.change(opacity: self.captionScrollView._mouseInside() ? 1 : 0) + })) + } + + var items: [MGalleryItem] { + return controller.arrangedObjects.map {$0 as! MGalleryItem} + } + + private var afterTransaction:(()->Void)? = nil + + func merge(with transition:UpdateTransition, afterTransaction:(()->Void)? = nil) -> Bool { queuedTransitions.append(transition) + self.afterTransaction = afterTransaction return enqueueTransitions() } var isFullScreen: Bool { if let view = controller.selectedViewController?.view as? MagnifyView { - if view.contentView.frame.size == window.frame.size { + if view.contentView.window !== window { return true } } return false } + func exitFullScreen() { + self.selectedItem?.toggleFullScreen() + } + var itemView:NSView? { return (controller.selectedViewController?.view as? MagnifyView)?.contentView } @@ -143,13 +439,14 @@ class GalleryPageController : NSObject, NSPageControllerDelegate { let wasInited = !controller.arrangedObjects.isEmpty let item: MGalleryItem? = !controller.arrangedObjects.isEmpty ? self.item(at: controller.selectedIndex) : nil - + let animated = !self.items.isEmpty var items:[MGalleryItem] = controller.arrangedObjects as! [MGalleryItem] + items = reversed ? items.reversed() : items while !queuedTransitions.isEmpty { let transition = queuedTransitions[0] - let searchItem:(AnyHashable)->MGalleryItem? = { stableId in + let searchItem:(AnyHashable, [MGalleryItem])->MGalleryItem? = { stableId, items in for item in items { if item.stableId == stableId { return item @@ -159,58 +456,109 @@ class GalleryPageController : NSObject, NSPageControllerDelegate { } for rdx in transition.deleted.reversed() { - let item = items[rdx] - identifiers.removeValue(forKey: item.identifier) - items.remove(at: rdx) + if items.count > rdx { + let item = items[rdx] + identifiers.removeValue(forKey: item.identifier) + cache.removeObject(forKey: item.identifier as AnyObject) + items.remove(at: rdx) + } } for (idx,item) in transition.inserted { - let item = searchItem(item.stableId) ?? item + let item = searchItem(item.stableId, items) ?? item identifiers[item.identifier] = item - items.insert(item, at: idx) + items.insert(item, at: min(idx, items.count)) } for (idx,item) in transition.updated { - let item = searchItem(item.stableId) ?? item + let item = searchItem(item.stableId, items) ?? item identifiers[item.identifier] = item - items[idx] = item + cache.removeObject(forKey: item.identifier as AnyObject) + if idx < items.count { + items[idx] = item + } } - queuedTransitions.removeFirst() } - if items.count > 0 { - controller.arrangedObjects = items - - if let item = item { - for i in 0 ..< items.count { - if item.identifier == items[i].identifier { - if controller.selectedIndex != i { - controller.selectedIndex = i + items = reversed ? items.reversed() : items + + if self.items != items { + + if items.count > 0 { + + let selectedItem = self.selectedItem + + controller.arrangedObjects = items + controller.completeTransition() + + if let item = item { + for i in 0 ..< items.count { + if item.identifier == items[i].identifier { + if controller.selectedIndex != i { + controller.selectedIndex = i + } + break } - break + } + } + if wasInited { + items[controller.selectedIndex].request(immediately: false) + if selectedItem != self.selectedItem { + self.selectedItem?.appear(for: controller.selectedViewController?.view) } } } if wasInited { - items[controller.selectedIndex].request(immediately: false) + transitionCallFunc?(self.thumbsControl.layoutItems(with: self.items, selectedIndex: controller.selectedIndex, animated: animated), selectedItem) } } + if let item = selectedItem { + (controller.selectedViewController?.view as? GMagnifyView)?.updateStatus(item.status) + } + afterTransaction?() + return items.isEmpty } return false } + var hasNext: Bool { + return controller.selectedIndex < controller.arrangedObjects.count - 1 + } + var hasPrev: Bool { + return controller.selectedIndex > 0 + } + func next() { if !lockedTransition { - set(index: min(controller.selectedIndex + 1, controller.arrangedObjects.count - 1), animated: false) + if let item = self.selectedItem as? MGalleryVideoItem, item.isFullscreen { + return + } + let item = self.item(at: min(controller.selectedIndex + 1, controller.arrangedObjects.count - 1)) + item.request() + + if let index = self.items.firstIndex(of: item) { + self.set(index: index, animated: false) + } } } func prev() { if !lockedTransition { - set(index: max(controller.selectedIndex - 1, 0), animated: false) + if let item = self.selectedItem as? MGalleryVideoItem, item.isFullscreen { + return + } + let item = self.item(at: max(controller.selectedIndex - 1, 0)) + item.request() + if let index = self.items.firstIndex(of: item) { + self.set(index: index, animated: false) + } } } + var currentIndex: Int { + return controller.selectedIndex + } + func zoomIn() { if let magnigy = controller.selectedViewController?.view as? MagnifyView { magnigy.zoomIn() @@ -223,6 +571,35 @@ class GalleryPageController : NSObject, NSPageControllerDelegate { } } + func decreaseSpeed() { + + } + + func increaseSpeed() { + + } + + + func rotateLeft() { + guard let item = self.selectedItem else {return} + item.disableAnimations = true + + _ = (item.rotate.get() |> take(1)).start(next: { [weak item] orientation in + if let orientation = orientation { + switch orientation { + case .right: + item?.rotate.set(.down) + case .down: + item?.rotate.set(.left) + default: + item?.rotate.set(nil) + } + } else { + item?.rotate.set(.right) + } + }) + } + func set(index:Int, animated:Bool) { _ = enqueueTransitions() @@ -240,9 +617,17 @@ class GalleryPageController : NSObject, NSPageControllerDelegate { } else { if controller.selectedIndex != index { controller.selectedIndex = index + pageControllerDidEndLiveTransition(controller, force:true) + } else if currentController == nil { + selectedIndex.set(index) } - pageControllerDidEndLiveTransition(controller, force:true) currentController = controller.selectedViewController + + } + + if items.count > 1, hasInited { + items[min(max(self.controller.selectedIndex - 1, 0), items.count - 1)].request() + items[min(max(self.controller.selectedIndex + 1, 0), items.count - 1)].request() } } } @@ -255,46 +640,88 @@ class GalleryPageController : NSObject, NSPageControllerDelegate { view.minMagnify = item.minMagnify view.maxMagnify = item.maxMagnify + + item.size.set(.single(item.sizeValue)) item.view.set(.single(view.contentView)) - item.size.set(view.smartUpdater.get()) + +// smartUpdaterDisposable.set(view.smartUpdaterValue.start(next: { size in +// item.size.set(.single(size)) +// })) + } } func pageControllerWillStartLiveTransition(_ pageController: NSPageController) { lockedTransition = true + captionScrollView.change(opacity: 0) startIndex = pageController.selectedIndex + + if items.count > 1 { + items[min(max(pageController.selectedIndex - 1, 0), items.count - 1)].request() + items[min(max(pageController.selectedIndex + 1, 0), items.count - 1)].request() + } } func pageControllerDidEndLiveTransition(_ pageController: NSPageController, force:Bool) { let previousView = currentController?.view as? MagnifyView - if startIndex != pageController.selectedIndex { - if startIndex > 0 && startIndex < pageController.arrangedObjects.count { + + captionScrollView.change(opacity: 0, animated: captionScrollView.superview != nil, completion: { [weak captionScrollView] completed in + if completed { + captionScrollView?.removeFromSuperview() + } + }) + + + if startIndex != pageController.selectedIndex || force { + if startIndex != -1, startIndex <= pageController.arrangedObjects.count - 1 { self.item(at: startIndex).disappear(for: previousView?.contentView) } startIndex = pageController.selectedIndex - -// if let caption = item.caption { -// caption.measure(width: item.sizeValue.width) -// captionView.update(caption) -// captionView.setFrameSize(captionView.frame.size.width + 10, captionView.frame.size.height + 8) -// (pageController.selectedViewController?.view as? MagnifyView)?.contentView.addSubview(captionView) -// captionView.centerX(y: 10) -// } else { -// captionView.removeFromSuperview() -// } -// + + pageController.completeTransition() - if let controllerView = pageController.selectedViewController?.view as? MagnifyView, previousView != controllerView || force { + if let controllerView = pageController.selectedViewController?.view as? GMagnifyView, previousView != controllerView || force { + controllerView.hideOrShowControls(hasPrev: hasPrev, hasNext: hasNext, animated: !force) let item = self.item(at: startIndex) - item.appear(for: controllerView.contentView) + if hasInited { + item.appear(for: controllerView.contentView) + + } controllerView.frame = view.focus(contentFrame.size, inset:contentInset) + magnifyDisposable.set(controllerView.magnifyUpdaterValue.start(next: { [weak self] value in + self?.captionScrollView.isHidden = value > 1.0 + })) + } + } + + let item = self.item(at: pageController.selectedIndex) + if let caption = item.caption { + caption.measure(width: min(item.sizeValue.width + 240, min(item.pagerSize.width - 200, 600))) + captionView.update(caption) + captionView.backgroundColor = .clear + captionView.disableBackgroundDrawing = true + + view.addSubview(captionScrollView) + captionScrollView.change(opacity: 1.0) + captionScrollView.setFrameSize(captionView.frame.size.width + 10, min(90, captionView.frame.height)) + captionScrollView.centerX(y: 100) + captionScrollView.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.9).cgColor + captionScrollView.layer?.cornerRadius = .cornerRadius + captionContainer.frame = NSMakeRect(0, 0, captionScrollView.frame.width, captionView.frame.height) + captionView.center() + + } else { + captionView.update(nil) + captionScrollView.removeFromSuperview() } + + configureCaptionAutohide() } private var currentController:NSViewController? @@ -302,9 +729,9 @@ class GalleryPageController : NSObject, NSPageControllerDelegate { func pageControllerDidEndLiveTransition(_ pageController: NSPageController) { pageControllerDidEndLiveTransition(pageController, force:false) currentController = pageController.selectedViewController - if let view = pageController.view as? MagnifyView { - window.makeFirstResponder(view.contentView) - } + // if let view = pageController.view as? MagnifyView { + // window.makeFirstResponder(view) + // } lockedTransition = false } @@ -322,15 +749,17 @@ class GalleryPageController : NSObject, NSPageControllerDelegate { var contentFrame:NSRect { - return NSMakeRect(frame.minX + contentInset.left, frame.minY + contentInset.top, frame.width - contentInset.left - contentInset.right, frame.height - contentInset.top - contentInset.bottom) + let rect = NSMakeRect(frame.minX + contentInset.left, frame.minY + contentInset.top, frame.width - contentInset.left - contentInset.right, frame.height - contentInset.top - contentInset.bottom) + + return rect } func pageController(_ pageController: NSPageController, frameFor object: Any?) -> NSRect { if let object = object { let item = self.item(for: object) let size = item.sizeValue - - return view.focus(size.fitted(contentFrame.size), inset:contentInset) + + return view.focus(size.fitted(contentFrame.size), inset:self.contentInset) } return view.bounds } @@ -343,10 +772,35 @@ class GalleryPageController : NSObject, NSPageControllerDelegate { let item = identifiers[identifier]! let view = item.singleView() view.wantsLayer = true - view.background = theme.colors.background - controller.view = MagnifyView(view, contentSize:item.sizeValue) + let magnify = GMagnifyView(view, contentSize:item.sizeValue, prev: _prev, next: _next, fillFrame: { [weak self] view in + guard let `self` = self else {return NSZeroRect} + + return self.view.focus(self.contentFrame.size, inset: self.contentInset) + }, prevAction: { [weak self] in + self?.prev() + }, nextAction: { [weak self] in + self?.next() + }, hasPrev: { [weak self] in + return self?.hasPrev ?? false + }, hasNext: { [weak self] in + return self?.hasNext ?? false + }, dismiss: { [weak self] in + if let event = NSApp.currentEvent { + _ = self?.interactions.dismiss(event) + } + }) + item.magnify.set(magnify.magnifyUpdaterValue |> deliverOnPrepareQueue) + controller.view = magnify + if hasInited { + item.request() + } + magnify.updateStatus(item.status) cache.setObject(controller, forKey: identifier as AnyObject) - item.request() + + if selectedItem == item, hasInited { + // item.view.set(.single(magnify.contentView)) + item.appear(for: magnify.contentView) + } return controller } } @@ -364,6 +818,12 @@ class GalleryPageController : NSObject, NSPageControllerDelegate { return object as! MGalleryItem } + func select(by item: MGalleryItem) -> Void { + if let index = index(for: item), !lockedTransition { + set(index: index, animated: false) + } + } + func index(for item:MGalleryItem) -> Int? { for i in 0 ..< controller.arrangedObjects.count { if let _item = controller.arrangedObjects[i] as? MGalleryItem { @@ -386,128 +846,187 @@ class GalleryPageController : NSObject, NSPageControllerDelegate { return nil } - func animateIn( from:@escaping(AnyHashable)->NSView?, completion:(()->Void)? = nil) ->Void { + func hideVideoControls() -> Bool { + if let item = self.selectedItem as? MGalleryVideoItem { + return item.hideControls() + } + return false + } + + func animateIn( from:@escaping(AnyHashable)->NSView?, completion:(()->Void)? = nil, addAccesoryOnCopiedView:(((AnyHashable?, NSView))->Void)? = nil, addVideoTimebase:(((AnyHashable, NSView))->Void)? = nil, showBackground:(()->Void)? = nil) ->Void { + + window.contentView?.addSubview(_prev) + window.contentView?.addSubview(_next) + captionScrollView.change(opacity: 0, animated: false) if let selectedView = controller.selectedViewController?.view as? MagnifyView, let item = self.selectedItem { + item.request() lockedTransition = true - if let oldView = from(item.stableId), let oldWindow = oldView.window { + if let oldView = from(item.stableId), let oldWindow = oldView.window, let oldScreen = oldWindow.screen { selectedView.isHidden = true - ioDisposabe.set((item.image.get() |> take(1)).start(next: { [weak self, weak selectedView] image in + ioDisposabe.set((item.image.get() |> map { $0.value } |> take(1) |> timeout(0.7, queue: Queue.mainQueue(), alternate: .single(.image(nil, nil)))).start(next: { [weak self, weak oldView, weak selectedView] value in - if let view = self?.view, let contentInset = self?.contentInset, let contentFrame = self?.contentFrame { - let newRect = view.focus(item.sizeValue.fitted(contentFrame.size), inset:contentInset) - let oldRect = oldWindow.convertToScreen(oldView.convert(oldView.bounds, to: nil)) - + + if let view = self?.view, let contentInset = self?.contentInset, let contentFrame = self?.contentFrame, let oldView = oldView { + let newRect = view.focus(item.sizeValue.fitted(contentFrame.size), inset: contentInset) + var oldRect = oldWindow.convertToScreen(oldView.convert(oldView.bounds, to: nil)) + oldRect.origin = oldRect.origin.offsetBy(dx: -oldScreen.frame.minX, dy: -oldScreen.frame.minY) selectedView?.contentSize = item.sizeValue.fitted(contentFrame.size) - - if let _ = image, let strongSelf = self { - self?.animate(oldRect: oldRect, newRect: newRect, newAlphaFrom: 0, newAlphaTo:1, oldAlphaFrom: 1, oldAlphaTo:0, contents: image, oldView: oldView, completion: { [weak strongSelf] in + if value.hasValue, let strongSelf = self { + self?.animate(oldRect: oldRect, newRect: newRect, newAlphaFrom: 0, newAlphaTo:1, oldAlphaFrom: 1, oldAlphaTo:0, contents: value, oldView: oldView, completion: { [weak strongSelf, weak selectedView] in selectedView?.isHidden = false + strongSelf?.captionScrollView.change(opacity: 1.0) + strongSelf?.hasInited = true + strongSelf?.selectedItem?.appear(for: selectedView?.contentView) strongSelf?.lockedTransition = false - }) + }, stableId: item.stableId, addAccesoryOnCopiedView: addAccesoryOnCopiedView) } else { selectedView?.isHidden = false + self?.hasInited = true + self?.selectedItem?.appear(for: selectedView?.contentView) self?.lockedTransition = false } + if let selectedView = selectedView { + addVideoTimebase?((item.stableId, selectedView.contentView)) + } } - + showBackground?() + completion?() })) } else { - ioDisposabe.set((item.image.get() |> take(1)).start(next: { [weak self] image in - //selectedView?.isHidden = false - self?.lockedTransition = false - if let completion = completion { - completion() + ioDisposabe.set((item.image.get() |> take(1)).start(next: { [weak self, weak selectedView] image in + if let selectedView = selectedView { + selectedView.isHidden = false + selectedView.swapView(selectedView.contentView) + self?.lockedTransition = false + self?.hasInited = true + self?.selectedItem?.appear(for: selectedView.contentView) + if let completion = completion { + completion() + self?.window.applyResponderIfNeeded() + } } + })) } } } - func animate(oldRect:NSRect, newRect:NSRect, newAlphaFrom:CGFloat, newAlphaTo:CGFloat, oldAlphaFrom:CGFloat, oldAlphaTo:CGFloat, contents:CGImage?, oldView:NSView, completion:@escaping ()->Void) { + func animate(oldRect:NSRect, newRect:NSRect, newAlphaFrom:CGFloat, newAlphaTo:CGFloat, oldAlphaFrom:CGFloat, oldAlphaTo:CGFloat, contents:GPreviewValue, oldView:NSView, completion:@escaping ()->Void, stableId: AnyHashable, addAccesoryOnCopiedView:(((AnyHashable?, NSView))->Void)? = nil) { lockedTransition = true let view = self.view - let newView:NSView = NSView(frame: oldRect) - newView.wantsLayer = true - newView.layer?.opacity = Float(newAlphaFrom) - newView.layer?.contents = contents - newView.layer?.backgroundColor = theme.colors.background.cgColor + + let newView:NSView // + + switch contents { + case let .image(contents, _): + newView = NSView(frame: newRect) + newView.wantsLayer = true + newView.layer?.contents = contents + case let .view(view): + newView = view ?? NSView(frame: newRect) + newView.frame = newRect + } + let copyView = oldView.copy() as! NSView - copyView.layer?.backgroundColor = theme.colors.background.cgColor - copyView.frame = oldRect + addAccesoryOnCopiedView?((stableId, copyView)) + + copyView.frame = NSMakeRect(oldRect.minX, oldRect.minY, oldAlphaFrom == 0 ? newRect.width : oldRect.width, oldAlphaFrom == 0 ? newRect.height : oldRect.height) copyView.wantsLayer = true - copyView.layer?.opacity = Float(oldAlphaFrom) view.addSubview(newView) view.addSubview(copyView) + + CATransaction.begin() - let duration:Double = 0.2 + let duration:Double = 0.25 + + let timingFunction: CAMediaTimingFunctionName = .spring - newView._change(pos: newRect.origin, animated: true, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) - newView._change(size: newRect.size, animated: true, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) - newView._change(opacity: newAlphaTo, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) - copyView._change(pos: newRect.origin, animated: true, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) - copyView._change(size: newRect.size, animated: true, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) - copyView._change(opacity: oldAlphaTo, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) { [weak self] _ in + + + newView.layer?.animatePosition(from: oldRect.origin, to: newRect.origin, duration: duration, timingFunction: timingFunction, removeOnCompletion: false) + newView.layer?.animateAlpha(from: newAlphaFrom, to: newAlphaTo, duration: duration / 2, timingFunction: timingFunction, removeOnCompletion: false) + + + newView.layer?.animateScaleX(from: oldRect.width / newRect.width, to: 1, duration: duration, timingFunction: timingFunction, removeOnCompletion: false) + newView.layer?.animateScaleY(from: oldRect.height / newRect.height, to: 1, duration: duration, timingFunction: timingFunction, removeOnCompletion: false) + + + copyView.layer?.animatePosition(from: oldRect.origin, to: newRect.origin, duration: duration, timingFunction: timingFunction, removeOnCompletion: false) + copyView.layer?.animateScaleX(from: oldAlphaFrom == 0 ? oldRect.width / newRect.width : 1, to: oldAlphaFrom != 0 ? newRect.width / oldRect.width : 1, duration: duration, timingFunction: timingFunction, removeOnCompletion: false) + copyView.layer?.animateScaleY(from: oldAlphaFrom == 0 ? oldRect.height / newRect.height : 1, to: oldAlphaFrom != 0 ? newRect.height / oldRect.height : 1, duration: duration, timingFunction: timingFunction, removeOnCompletion: false) + + copyView.layer?.animateAlpha(from: oldAlphaFrom , to: oldAlphaTo, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { [weak self, weak copyView, weak newView] _ in completion() self?.lockedTransition = false - if let strongSelf = self { - newView.removeFromSuperview() - copyView.removeFromSuperview() - Queue.mainQueue().after(0.1, { [weak strongSelf] in - if let view = strongSelf?.controller.selectedViewController?.view as? MagnifyView { - strongSelf?.window.makeFirstResponder(view) - } - }) - } - } + newView?.removeFromSuperview() + copyView?.removeFromSuperview() + }) CATransaction.commit() } - func animateOut( to:@escaping(AnyHashable)->NSView?, completion:(()->Void)? = nil) ->Void { + func animateOut( to:@escaping(AnyHashable)->NSView?, completion:(((Bool, AnyHashable?))->Void)? = nil, addAccesoryOnCopiedView:(((AnyHashable?, NSView))->Void)? = nil, addVideoTimebase:(((AnyHashable, NSView))->Void)? = nil) ->Void { lockedTransition = true + + + captionScrollView.change(opacity: 0, animated: true) + if let selectedView = controller.selectedViewController?.view as? MagnifyView, let item = selectedItem { selectedView.isHidden = true item.disappear(for: selectedView.contentView) - if let oldView = to(item.stableId), let window = oldView.window { - let newRect = window.convertToScreen(oldView.convert(oldView.bounds, to: nil)) + if let oldView = to(item.stableId), let window = oldView.window, let screen = window.screen { + var newRect = window.convertToScreen(oldView.convert(oldView.bounds, to: nil)) + newRect.origin = newRect.origin.offsetBy(dx: -screen.frame.minX, dy: -screen.frame.minY) let oldRect = view.focus(item.sizeValue.fitted(contentFrame.size), inset:contentInset) - ioDisposabe.set((item.image.get() |> take(1)).start(next: { [weak self] (image) in - self?.animate(oldRect: oldRect, newRect: newRect, newAlphaFrom: 1, newAlphaTo:0, oldAlphaFrom: 0, oldAlphaTo: 1, contents: image, oldView: oldView, completion: { - completion?() - }) - + ioDisposabe.set((item.image.get() |> map { $0.value } |> take(1) |> timeout(0.1, queue: Queue.mainQueue(), alternate: .single(.image(nil, nil)))).start(next: { [weak self, weak item] value in + if let item = item { + self?.animate(oldRect: oldRect, newRect: newRect, newAlphaFrom: 1, newAlphaTo:0, oldAlphaFrom: 0, oldAlphaTo: 1, contents: value, oldView: oldView, completion: { [weak item] in + if let item = item { + completion?((true, item.stableId)) + } + }, stableId: item.stableId, addAccesoryOnCopiedView: addAccesoryOnCopiedView) + } })) + + addVideoTimebase?((item.stableId, selectedView.contentView)) + } else { view._change(opacity: 0, completion: { (_) in - completion?() + completion?((false, item.stableId)) }) } } else { view._change(opacity: 0, completion: { (_) in - completion?() + completion?((false, nil)) }) } } deinit { - window.remove(object: self, for: .leftMouseUp) + window.removeAllHandlers(for: self) ioDisposabe.dispose() + navigationDisposable.dispose() + autohideCaptionDisposable.dispose() + magnifyDisposable.dispose() + indexDisposable.dispose() + smartUpdaterDisposable.dispose() + cache.removeAllObjects() } } diff --git a/Telegram-Mac/GalleryThumbsControl.swift b/Telegram-Mac/GalleryThumbsControl.swift new file mode 100644 index 0000000000..22a9e17fa5 --- /dev/null +++ b/Telegram-Mac/GalleryThumbsControl.swift @@ -0,0 +1,153 @@ +// +// GalleryThumbsControl.swift +// Telegram +// +// Created by keepcoder on 10/11/2017. +// Copyright © 2017 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit +import TelegramCore + + +class GalleryThumbsControl: ViewController { + private let interactions: GalleryInteractions + + var afterLayoutTransition:((Bool)->Void)? = nil + + init(interactions: GalleryInteractions) { + self.interactions = interactions + super.init(frame: NSMakeRect(0, 0, 400, 80)) + } + + private(set) var items:[MGalleryItem] = [] + + + func layoutItems(with items: [MGalleryItem], selectedIndex selected: Int, animated: Bool) -> UpdateTransition { + + let current: MGalleryItem? = selected > items.count - 1 || selected < 0 ? nil : items[selected] + + var newItems:[MGalleryItem] = [] + + var isForceInstant: Bool = false + for item in items { + if case .instantMedia = item.entry { + isForceInstant = true + newItems = items + break + } + } + + if !isForceInstant, let current = current { + switch current.entry { + case .message(let entry): + + if let message = entry.message { + if let groupInfo = message.groupInfo { + + newItems.append(current) + + var next: Int = selected + 1 + var prev: Int = selected - 1 + + var prevFilled: Bool = prev < 0 + var nextFilled: Bool = next >= items.count + + while !prevFilled || !nextFilled { + if !prevFilled { + prevFilled = items[prev].entry.message?.groupInfo != groupInfo + if !prevFilled { + newItems.insert(items[prev], at: 0) + } + prev -= 1 + } + if !nextFilled { + nextFilled = items[next].entry.message?.groupInfo != groupInfo + if !nextFilled { + newItems.append(items[next]) + } + next += 1 + } + + prevFilled = prevFilled || prev < 0 + nextFilled = nextFilled || next >= items.count + + } + } + } + + case .instantMedia: + newItems = items + case .photo: + newItems = items + case .secureIdDocument: + newItems = items + } + } + + if newItems.count == 1 { + newItems = [] + } + + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: self.items, rightList: newItems) + + + + for rdx in deleteIndices.reversed() { + genericView.removeItem(at: rdx, animated: animated) + self.items.remove(at: rdx) + } + + + for (idx, item, _) in indicesAndItems { + genericView.insertItem(item, at: idx, isSelected: current?.stableId == item.stableId, animated: animated, callback: { [weak self] item in + self?.interactions.select(item) + }) + self.items.insert(item, at: idx) + } + for (idx, item, _) in updateIndices { + let item = item + genericView.updateItem(item, at: idx) + self.items[idx] = item + } + + for i in 0 ..< self.items.count { + if current?.stableId == self.items[i].stableId { + genericView.layoutItems(selectedIndex: i, animated: animated) + break + } + } + + genericView.updateHighlight() + + if self.items.count <= 1 { + genericView.change(opacity: 0, animated: animated, completion: { [weak self] completed in + if completed { + self?.genericView.isHidden = true + } + }) + } else { + genericView.isHidden = false + genericView.change(opacity: 1, animated: animated) + } + + afterLayoutTransition?(animated) + + return UpdateTransition(deleted: deleteIndices, inserted: indicesAndItems.map {($0.0, $0.1)}, updated: updateIndices.map {($0.0, $0.1)}) + } + + deinit { + var bp:Int = 0 + bp += 1 + } + + var genericView:GalleryThumbsControlView { + return view as! GalleryThumbsControlView + } + + override func viewClass() -> AnyClass { + return GalleryThumbsControlView.self + } +} diff --git a/Telegram-Mac/GalleryThumbsControlView.swift b/Telegram-Mac/GalleryThumbsControlView.swift new file mode 100644 index 0000000000..758ddbbfa1 --- /dev/null +++ b/Telegram-Mac/GalleryThumbsControlView.swift @@ -0,0 +1,313 @@ +// +// GalleryThumsControllerView.swift +// Telegram +// +// Created by keepcoder on 10/11/2017. +// Copyright © 2017 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit + +private class GalleryThumb { + var signal:Signal? { + var signal:Signal? + if let item = item as? MGalleryPhotoItem { + signal = chatWebpageSnippetPhoto(account: item.context.account, imageReference: item.entry.imageReference(item.media), scale: System.backingScale, small: true, secureIdAccessContext: item.secureIdAccessContext) + } else if let item = item as? MGalleryGIFItem { + signal = chatMessageVideo(postbox: item.context.account.postbox, fileReference: item.entry.fileReference(item.media), scale: System.backingScale) + } else if let item = item as? MGalleryVideoItem { + signal = chatMessageVideo(postbox: item.context.account.postbox, fileReference: item.entry.fileReference(item.media), scale: System.backingScale) + } else if let item = item as? MGalleryPeerPhotoItem { + signal = chatMessagePhoto(account: item.context.account, imageReference: item.entry.imageReference(item.media), scale: System.backingScale) + } + return signal + } + let size: NSSize? + private weak var _view: GalleryThumbContainer? = nil + var selected: Bool = false + var isEnabled: Bool = true + private let callback:(MGalleryItem)->Void + let item: MGalleryItem + + var frame: NSRect = .zero + + init(_ item: MGalleryItem, callback:@escaping(MGalleryItem)->Void) { + self.callback = callback + self.item = item + + if let item = item as? MGalleryPhotoItem { + item.fetch() + } else if let item = item as? MGalleryPeerPhotoItem { + item.fetch() + } + + var size: NSSize? + if let item = item as? MGalleryPhotoItem { + size = item.media.representations.first?.dimensions.size + } else if let item = item as? MGalleryGIFItem { + size = item.media.videoSize + } else if let item = item as? MGalleryVideoItem { + size = item.media.videoSize + } else if let item = item as? MGalleryPeerPhotoItem { + size = item.media.representations.first?.dimensions.size + } + + self.size = size + } + + var viewSize: NSSize { + if selected { + return NSMakeSize(80, 80) + } else { + return NSMakeSize(40, 80) + } + } + + var view: GalleryThumbContainer { + if _view == nil { + let view = GalleryThumbContainer(self) + view.frame = frame + view.set(handler: { [weak self] _ in + guard let `self` = self else { + return + } + self.callback(self.item) + }, for: .Click) + _view = view + return view + } + return _view! + } + + var opt:GalleryThumbContainer? { + return _view + } + + func cleanup() { + _view?.removeFromSuperview() + _view = nil + } + +} + +class GalleryThumbContainer : Control { + + fileprivate let imageView: TransformImageView = TransformImageView() + fileprivate let overlay: View = View() + fileprivate init(_ item: GalleryThumb) { + super.init(frame: NSZeroRect) + backgroundColor = .clear + if let signal = item.signal, let size = item.size { + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize:size.aspectFilled(NSMakeSize(80, 80)), boundingSize: NSMakeSize(80, 80), intrinsicInsets: NSEdgeInsets()) + let media = item.item.entry.message?.media.first + + if let media = media { + imageView.setSignal(signal: cachedMedia(media: media, arguments: arguments, scale: System.backingScale), clearInstantly: true) + } + + imageView.setSignal(signal, cacheImage: { result in + if let media = media { + cacheMedia(result, media: media, arguments: arguments, scale: System.backingScale) + } + }) + imageView.set(arguments: arguments) + } + overlay.layer?.opacity = 0.35 + overlay.setFrameSize(80, 80) + imageView.setFrameSize(80, 80) + addSubview(imageView) + addSubview(overlay) + overlay.backgroundColor = .black + layer?.cornerRadius = .cornerRadius + } + + deinit { + var bp:Int = 0 + bp += 1 + } + + override func layout() { + imageView.center() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + +} + + + +class GalleryThumbsControlView: View { + + private let scrollView: HorizontalScrollView = HorizontalScrollView() + private let documentView: View = View() + private var selectedView: GalleryThumbContainer? + + private var items: [GalleryThumb] = [] + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + backgroundColor = .clear + addSubview(scrollView) + scrollView.documentView = documentView + scrollView.backgroundColor = .clear + scrollView.background = .clear + + documentView.backgroundColor = .clear + + + NotificationCenter.default.addObserver(self, selector: #selector(scrollDidUpdated), name: NSView.boundsDidChangeNotification, object: scrollView.contentView) + NotificationCenter.default.addObserver(self, selector: #selector(scrollDidUpdated), name: NSView.frameDidChangeNotification, object: scrollView) + + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + private var previousRange: NSRange? = nil + + @objc private func scrollDidUpdated() { + + + var range: NSRange = NSMakeRange(NSNotFound, 0) + + let distance:(min: CGFloat, max: CGFloat) = (min: scrollView.documentOffset.x - 80, max: scrollView.documentOffset.x + scrollView.frame.width + 80) + + for (i, item) in items.enumerated() { + if item.frame.minX >= distance.min && item.frame.maxX <= distance.max { + range.length += 1 + if range.location == NSNotFound { + range.location = i + } + } else if range.location != NSNotFound { + break + } + } + + if previousRange == range { + return + } + + previousRange = range + + documentView.subviews = range.location == NSNotFound ? [] : items.subarray(with: range).map { $0.view } + } + + override func layout() { + super.layout() + scrollView.frame = bounds + } + + required init?(coder: NSCoder) { + fatalError() + } + + func insertItem(_ item: MGalleryItem, at: Int, isSelected: Bool, animated: Bool, callback:@escaping(MGalleryItem)->Void) { + let view = GalleryThumb(item, callback: callback) + view.selected = isSelected + + let idx = idsExcludeDisabled(at) + + items.insert(view, at: idx) + + } + + func idsExcludeDisabled(_ at: Int) -> Int { + var idx = at + for i in 0 ..< items.count { + if !items[i].isEnabled { + if i <= at { + idx += 1 + } + } + } + + return idx + } + + func removeItem(at: Int, animated: Bool) { + + let idx = idsExcludeDisabled(at) + var subview:GalleryThumb? = items[idx] + subview?.isEnabled = false + subview?.opt?.isEnabled = false + items.remove(at: idx) + subview?.opt?.change(opacity: 0, animated: animated, completion: { completed in + subview?.cleanup() + subview = nil + }) + } + + func updateItem(_ item: MGalleryItem, at: Int) { + + } + + var documentSize: NSSize { + return NSMakeSize(min(documentView.frame.width, frame.width), documentView.frame.height) + } + + func updateHighlight() { + + } + + func layoutItems(selectedIndex: Int? = nil, animated: Bool) { + + let idx = idsExcludeDisabled(selectedIndex ?? 0) + + let minWidth: CGFloat = frame.height / 2 + let difSize = NSMakeSize(frame.height, frame.height) + + var x:CGFloat = 0 + + let duration: Double = 0.4 + var selectedView: GalleryThumbContainer? + for i in 0 ..< items.count { + let thumb = items[i] + var size = idx == i ? difSize : NSMakeSize(minWidth, frame.height) + let view = thumb.opt + + let rect = CGRect(origin: NSMakePoint(x, 0), size: size) + thumb.frame = rect + + view?.overlay.change(opacity: 0.35) + if thumb.isEnabled { + if let view = view { + view._change(size: rect.size, animated: animated, duration: duration, timingFunction: CAMediaTimingFunctionName.spring) + + let f = view.focus(view.imageView.frame.size) + view.imageView._change(pos: f.origin, animated: animated, duration: duration, timingFunction: CAMediaTimingFunctionName.spring) + + view._change(pos: rect.origin, animated: animated, duration: duration, timingFunction: CAMediaTimingFunctionName.spring) + } + } else { + size.width -= minWidth + } + x += size.width + 4 + + if idx == i { + selectedView = view + view?.overlay.change(opacity: 0.0) + } + } + + self.selectedView = selectedView + + documentView.setFrameSize(x, frame.height) + + + if let selectedView = selectedView { + scrollView.clipView.scroll(to: NSMakePoint(min(max(selectedView.frame.midX - frame.width / 2, 0), max(documentView.frame.width - frame.width, 0)), 0), animated: animated && documentView.subviews.count > 0) + } + previousRange = nil + scrollDidUpdated() + } + +} diff --git a/Telegram-Mac/GalleryTouchBar.swift b/Telegram-Mac/GalleryTouchBar.swift new file mode 100644 index 0000000000..832a7a221f --- /dev/null +++ b/Telegram-Mac/GalleryTouchBar.swift @@ -0,0 +1,327 @@ +// +// GalleryTouchBar.swift +// Telegram +// +// Created by Mikhail Filimonov on 19/09/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import SwiftSignalKit +import TGUIKit + + +@available(OSX 10.12.2, *) +private extension NSTouchBarItem.Identifier { + static let zoomControls = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.gallery.zoom") + static let rotate = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.gallery.rotate") + + static let videoPlayControl = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.gallery.videoPlayControl") + static let videoTimeControls = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.gallery.videoTimeControls") + static let scrubber = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.gallery.scrubber") +} + +@available(OSX 10.12.2, *) +private class GalleryThumbsTouchBarItem: NSCustomTouchBarItem, NSScrubberDelegate, NSScrubberDataSource, NSScrubberFlowLayoutDelegate { + + private static let itemViewIdentifier = "GalleryTouchBarThumbItemView" + + private var entries: [MGalleryItem] = [] + private var selected:(MGalleryItem)->Void + init(identifier: NSTouchBarItem.Identifier, selected:@escaping(MGalleryItem)->Void) { + self.selected = selected + super.init(identifier: identifier) + + let scrubber = TGScrubber() + scrubber.register(GalleryTouchBarThumbItemView.self, forItemIdentifier: NSUserInterfaceItemIdentifier(rawValue: GalleryThumbsTouchBarItem.itemViewIdentifier)) + + scrubber.mode = .free + scrubber.selectionBackgroundStyle = .outlineOverlay + scrubber.selectionOverlayStyle = .outlineOverlay + scrubber.delegate = self + scrubber.dataSource = self + scrubber.isContinuous = true + scrubber.floatsSelectionViews = false + scrubber.itemAlignment = .center + self.view = scrubber + } + + var scrubber: NSScrubber { + return view as! NSScrubber + } + + func insertItem(_ item: MGalleryItem, at: Int) { +// let index = min(at, entries.count) +// entries.insert(item, at: index) +// scrubber.insertItems(at: IndexSet(integer: index)) + } + func removeItem(at: Int) { +// if at < entries.count { +// entries.remove(at: at) +// scrubber.removeItems(at: IndexSet(integer: at)) +// } + } + func updateItem(_ item: MGalleryItem, at: Int) { +// if at < entries.count { +// entries[at] = item +// scrubber.reloadItems(at: IndexSet(integer: at)) +// } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var lockNotify: Bool = false + func selectAndScroll(to item: MGalleryItem?) { + Queue.mainQueue().justDispatch { + if let first = self.entries.firstIndex(where: {$0.entry.stableId == item?.stableId}) { + self.lockNotify = true + self.scrubber.selectedIndex = first + } else { + self.scrubber.selectedIndex = -1 + } + } + } + + func numberOfItems(for scrubber: NSScrubber) -> Int { + return entries.count + } + + + func scrubber(_ scrubber: NSScrubber, viewForItemAt index: Int) -> NSScrubberItemView { + let view = scrubber.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: GalleryThumbsTouchBarItem.itemViewIdentifier), owner: nil) as! GalleryTouchBarThumbItemView + view.update(entries[index]) + return view + } + + func scrubber(_ scrubber: NSScrubber, layout: NSScrubberFlowLayout, sizeForItemAt itemIndex: Int) -> NSSize { + return NSMakeSize(40, 30) + } + func scrubber(_ scrubber: NSScrubber, didHighlightItemAt highlightedIndex: Int) { + + } + + + func scrubber(_ scrubber: NSScrubber, didSelectItemAt index: Int) { + if let current = NSApp.currentEvent, current.window == nil { + selected(entries[index]) + } + } + +} + +@available(OSX 10.12.2, *) +class GalleryTouchBar: NSTouchBar, NSTouchBarDelegate { + private let interactions: GalleryInteractions + + private var selectedItem: MGalleryItem? + private let videoStatusDisposable = MetaDisposable() + init(interactions: GalleryInteractions, selectedItemChanged: @escaping(@escaping(MGalleryItem) -> Void) ->Void, transition:@escaping(@escaping(UpdateTransition, MGalleryItem?) -> Void) ->Void) { + self.interactions = interactions + super.init() + self.customizationIdentifier = .windowBar + self.delegate = self + + self.defaultItemIdentifiers = [.scrubber] + + + transition { [weak self] transition, selectedItem in + self?.applyTransition(transition, selectedItem) + } + selectedItemChanged { [weak self] selectedItem in + self?.updateSelectedItem(selectedItem) + } + + + } + + private func applyTransition(_ transition: UpdateTransition, _ selectedItem: MGalleryItem?) { + guard let item = self.item(forIdentifier: .scrubber) as? GalleryThumbsTouchBarItem else {return} + item.lockNotify = true + //item.scrubber.performSequentialBatchUpdates { [weak item] in + for rdx in transition.deleted.reversed() { + item.removeItem(at: rdx) + } + for (idx, insertItem) in transition.inserted { + item.insertItem(insertItem, at: idx) + } + for (idx, updateItem) in transition.updated { + item.updateItem(updateItem, at: idx) + } + // } + if !transition.isEmpty { + item.scrubber.reloadData() + } + updateSelectedItem(selectedItem) + } + + private func updateSelectedItem(_ item: MGalleryItem?) { + if self.selectedItem?.entry != item?.entry { + self.selectedItem = item + // + var items: [NSTouchBarItem.Identifier] = [] + if !(item is MGalleryGIFItem) && !(item is MGalleryVideoItem) { + items.append(.zoomControls) + items.append(.rotate) + } + if let item = item as? MGalleryVideoItem { + items.append(.videoPlayControl) + items.append(.videoTimeControls) + videoStatusDisposable.set((item.playerState |> deliverOnMainQueue).start(next: { [weak self] state in + self?.updateVideoControls(state) + })) + } else { + videoStatusDisposable.set(nil) + } + items.append(.fixedSpaceLarge) + items.append(.scrubber) + items.append(.fixedSpaceLarge) + + self.defaultItemIdentifiers = items + + (self.item(forIdentifier: .scrubber) as? GalleryThumbsTouchBarItem)?.selectAndScroll(to: item) + } + } + + private func updateVideoControls(_ state: AVPlayerState) { + guard let button = (self.item(forIdentifier: .videoPlayControl) as? NSCustomTouchBarItem)?.view as? NSButton else {return} + guard let segment = (self.item(forIdentifier: .videoTimeControls) as? NSCustomTouchBarItem)?.view as? NSSegmentedControl else {return} + + if let item = selectedItem as? MGalleryExternalVideoItem { + switch ExternalVideoLoader.serviceType(item.content) { + case .youtube: + button.bezelColor = .redUI + case .vimeo: + button.bezelColor = .accent + case .none: + button.bezelColor = nil + } + } else { + button.bezelColor = nil + } + switch state { + case let .playing(duration): + button.isEnabled = true + button.image = NSImage(named: NSImage.touchBarPauseTemplateName)! + segment.setEnabled(duration >= 30, forSegment: 0) + segment.setEnabled(duration >= 30, forSegment: 1) + case let .paused(duration): + button.isEnabled = true + button.image = NSImage(named: NSImage.touchBarPlayTemplateName)! + + segment.setEnabled(duration >= 30, forSegment: 0) + segment.setEnabled(duration >= 30, forSegment: 1) + default: + button.isEnabled = false + button.image = NSImage(named: NSImage.touchBarPlayTemplateName)! + segment.setEnabled(false, forSegment: 0) + segment.setEnabled(false, forSegment: 1) + } + } + + deinit { + videoStatusDisposable.dispose() + } + + + @objc private func zoom(_ sender: Any?) { + guard let segment = sender as? NSSegmentedControl else {return} + switch segment.selectedSegment { + case 0: + interactions.zoomOut() + case 1: + interactions.zoomIn() + default: + break + } + } + + @objc private func rotate() { + interactions.rotateLeft() + } + @objc private func videoTimeControlsActions(_ sender: Any?) { + guard let segment = sender as? NSSegmentedControl else {return} + if let item = selectedItem { + switch segment.selectedSegment { + case 0: + item.rewindBack() + case 1: + item.rewindForward() + default: + break + } + } + } + + @objc private func playOrPauseAction() { + if let item = selectedItem { + item.togglePlayerOrPause() + } + } + + func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? { + switch identifier { + case .zoomControls: + let item = NSCustomTouchBarItem(identifier: identifier) + + let segment = NSSegmentedControl() + segment.segmentStyle = .separated + segment.segmentCount = 2 + segment.setImage(NSImage(named: NSImage.Name("Icon_TouchBar_ZoomOut"))!, forSegment: 0) + segment.setImage(NSImage(named: NSImage.Name("Icon_TouchBar_ZoomIn"))!, forSegment: 1) + + segment.setWidth(93, forSegment: 0) + segment.setWidth(93, forSegment: 1) + + segment.trackingMode = .momentary + segment.target = self + segment.action = #selector(zoom(_:)) + item.view = segment + return item + case .rotate: + let item = NSCustomTouchBarItem(identifier: identifier) + let button = NSButton(image: NSImage(named: NSImage.touchBarRotateLeftTemplateName)!, target: self, action: #selector(rotate)) + button.addWidthConstraint(size: 93) + item.view = button + item.customizationLabel = button.title + return item + + case .videoTimeControls: + let item = NSCustomTouchBarItem(identifier: identifier) + + let segment = NSSegmentedControl() + segment.segmentStyle = .separated + segment.segmentCount = 2 + segment.setImage(NSImage(named: NSImage.touchBarSkipBack15SecondsTemplateName)!, forSegment: 0) + segment.setImage(NSImage(named: NSImage.touchBarSkipAhead15SecondsTemplateName)!, forSegment: 1) + + segment.setWidth(93, forSegment: 0) + segment.setWidth(93, forSegment: 1) + + segment.trackingMode = .momentary + segment.target = self + segment.action = #selector(videoTimeControlsActions(_:)) + item.view = segment + return item + case .videoPlayControl: + let item = NSCustomTouchBarItem(identifier: identifier) + let button = NSButton(image:NSImage(named: NSImage.touchBarPlayTemplateName)!, target: self, action: #selector(playOrPauseAction)) + button.addWidthConstraint(size: 93) + item.view = button + item.customizationLabel = button.title + return item + case .scrubber: + return GalleryThumbsTouchBarItem(identifier: identifier, selected: { [weak self] item in + self?.interactions.select(item) + }) + + default: + return nil + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/GalleryTouchBarThumbItemView.swift b/Telegram-Mac/GalleryTouchBarThumbItemView.swift new file mode 100644 index 0000000000..7a619b1602 --- /dev/null +++ b/Telegram-Mac/GalleryTouchBarThumbItemView.swift @@ -0,0 +1,63 @@ +// +// GalleryTouchBarThumbItemView.swift +// Telegram +// +// Created by Mikhail Filimonov on 20/09/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + +@available(OSX 10.12.2, *) +class GalleryTouchBarThumbItemView: NSScrubberItemView { + fileprivate let imageView: TransformImageView = TransformImageView() + + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(imageView) + } + + func update(_ item: MGalleryItem) { + var signal:Signal? + var size: NSSize? + if let item = item as? MGalleryPhotoItem { + signal = chatWebpageSnippetPhoto(account: item.context.account, imageReference: item.entry.imageReference(item.media), scale: backingScaleFactor, small: true, secureIdAccessContext: item.secureIdAccessContext) + size = item.media.representations.first?.dimensions.size + } else if let item = item as? MGalleryGIFItem { + signal = chatMessageImageFile(account: item.context.account, fileReference: item.entry.fileReference(item.media), scale: backingScaleFactor) + size = item.media.videoSize + } else if let item = item as? MGalleryExternalVideoItem { + signal = chatWebpageSnippetPhoto(account: item.context.account, imageReference: item.entry.imageReference(item.mediaImage), scale: backingScaleFactor, small: true, secureIdAccessContext: nil) + size = item.mediaImage.representations.first?.dimensions.size + } else if let item = item as? MGalleryVideoItem { + signal = chatMessageImageFile(account: item.context.account, fileReference: item.entry.fileReference(item.media), scale: backingScaleFactor) + size = item.media.videoSize + } else if let item = item as? MGalleryPeerPhotoItem { + signal = chatMessagePhoto(account: item.context.account, imageReference: item.entry.imageReference(item.media), scale: backingScaleFactor) + + size = item.media.representations.first?.dimensions.size + } + item.fetch() + + if let signal = signal, let size = size { + imageView.setSignal(signal) + let arguments = TransformImageArguments(corners: ImageCorners(radius: 4.0), imageSize:size.aspectFilled(NSMakeSize(36, 30)), boundingSize: NSMakeSize(36, 30), intrinsicInsets: NSEdgeInsets()) + imageView.set(arguments: arguments) + } + imageView.setFrameSize(36, 30) + } + + override func layout() { + super.layout() + imageView.center() + } + + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/GalleryViewer.swift b/Telegram-Mac/GalleryViewer.swift index b19558bf38..3e00e6b7f5 100644 --- a/Telegram-Mac/GalleryViewer.swift +++ b/Telegram-Mac/GalleryViewer.swift @@ -7,27 +7,34 @@ // import Cocoa -import SwiftSignalKitMac +import SwiftSignalKit import TGUIKit -import TelegramCoreMac -import PostboxMac +import TelegramCore + +import Postbox import AVFoundation + final class GalleryInteractions { - var dismiss:()->KeyHandlerResult = { return .rejected} - var next:()->KeyHandlerResult = { return .rejected} - var previous:()->KeyHandlerResult = { return .rejected} + var dismiss:(NSEvent)->KeyHandlerResult = { _ in return .rejected} + var next:(NSEvent)->KeyHandlerResult = { _ in return .rejected} + var select:(MGalleryItem)->Void = { _ in} + var previous:(NSEvent)->KeyHandlerResult = { _ in return .rejected} var showActions:(Control)->KeyHandlerResult = {_ in return .rejected} + var share:(Control)->Void = { _ in } var contextMenu:()->NSMenu? = {return nil} + var openInfo:(PeerId)->Void = {_ in} + var openMessage:()->Void = {} + var showThumbsControl:(View, Bool)->Void = {_, _ in} + var hideThumbsControl:(View, Bool)->Void = {_, _ in} + + var zoomIn:()->Void = {} + var zoomOut:()->Void = {} + var rotateLeft:()->Void = {} + + var fastSave:()->Void = {} } private(set) var viewer:GalleryViewer? -func cacheGalleryView(_ view: NSView, for item: MGalleryItem) { - viewer?.viewCache[item.stableId] = view -} - -func cachedGalleryView(for item: MGalleryItem) -> NSView? { - return viewer?.viewCache[item.stableId] -} let galleryButtonStyle = ControlStyle(font:.medium(.huge), foregroundColor:.white, backgroundColor:.clear, highlightColor:.grayIcon) @@ -38,11 +45,13 @@ private func tagsForMessage(_ message: Message) -> MessageTags? { case _ as TelegramMediaImage: return .photoOrVideo case let file as TelegramMediaFile: - if file.isVideo && !file.isAnimated { + if file.isVideo && file.isAnimated { + return nil + } else if file.isVideo && !file.isAnimated { return .photoOrVideo } else if file.isVoice { return .voiceOrInstantVideo - } else if file.isSticker || (file.isVideo && file.isAnimated) { + } else if file.isStaticSticker || (file.isVideo && file.isAnimated) { return nil } else { return .file @@ -55,19 +64,22 @@ private func tagsForMessage(_ message: Message) -> MessageTags? { } -enum GalleryAppearType { +enum GalleryAppearType : Equatable { case alone case history case profile(PeerId) case secret + case messages([Message]) } -private func mediaForMessage(message: Message) -> Media? { +private func mediaForMessage(message: Message, postbox: Postbox) -> Media? { for media in message.media { if let media = media as? TelegramMediaImage { return media } else if let file = media as? TelegramMediaFile { - if file.mimeType.hasPrefix("image/") || file.isVideo { + if file.isGraphicFile || file.isVideo || file.isAnimated { + return file + } else if file.isVideoFile, FileManager.default.fileExists(atPath: postbox.mediaBox.resourcePath(file.resource)) { return file } } else if let webpage = media as? TelegramMediaWebpage { @@ -84,45 +96,51 @@ private func mediaForMessage(message: Message) -> Media? { return nil } +fileprivate func itemFor(entry: ChatHistoryEntry, context: AccountContext, pagerSize: NSSize) -> MGalleryItem { + switch entry { + case let .MessageEntry(message, _, _, _, _, _, _): + if let media = mediaForMessage(message: message, postbox: context.account.postbox) { + if let _ = media as? TelegramMediaImage { + return MGalleryPhotoItem(context, .message(entry), pagerSize) + } else if let file = media as? TelegramMediaFile { + if (file.isVideo && !file.isAnimated) { + return MGalleryVideoItem(context, .message(entry), pagerSize) + } else { + if file.mimeType.hasPrefix("image/") { + return MGalleryPhotoItem(context, .message(entry), pagerSize) + } else if file.isVideo && file.isAnimated { + return MGalleryGIFItem(context, .message(entry), pagerSize) + } else if file.isVideoFile { + return MGalleryVideoItem(context, .message(entry), pagerSize) + } + } + } + } else if !message.media.isEmpty, let webpage = message.media[0] as? TelegramMediaWebpage { + if case let .Loaded(content) = webpage.content { + if ExternalVideoLoader.isPlayable(content) { + return MGalleryExternalVideoItem(context, .message(entry), pagerSize) + } + } + } + default: + break + } + + return MGalleryItem(context, .message(entry), pagerSize) +} -fileprivate func prepareEntries(from:[ChatHistoryEntry]?, to:[ChatHistoryEntry], account:Account, pagerSize:NSSize) -> Signal, Void> { +fileprivate func prepareEntries(from:[ChatHistoryEntry]?, to:[ChatHistoryEntry], context: AccountContext, pagerSize:NSSize) -> Signal, NoError> { return Signal { subscriber in let (removed, inserted, updated) = proccessEntriesWithoutReverse(from, right: to, { (entry) -> MGalleryItem in - switch entry { - case let .MessageEntry(message, _, _, _, _): - if let media = mediaForMessage(message: message) { - if let _ = media as? TelegramMediaImage { - return MGalleryPhotoItem(account, .message(entry), pagerSize) - } else if let file = media as? TelegramMediaFile { - if file.isVideo && !file.isAnimated { - return MGalleryVideoItem(account, .message(entry), pagerSize) - } else { - if file.mimeType.hasPrefix("image/") { - return MGalleryPhotoItem(account, .message(entry), pagerSize) - } else if file.isVideo && file.isAnimated { - return MGalleryGIFItem(account, .message(entry), pagerSize) - } - } - } - } else if !message.media.isEmpty, let webpage = message.media[0] as? TelegramMediaWebpage { - if case let .Loaded(content) = webpage.content { - if ExternalVideoLoader.isPlayable(content) { - return MGalleryExternalVideoItem(account, .message(entry), pagerSize) - } - } - } - default: - break - } - return MGalleryItem(account, .message(entry), pagerSize) + return itemFor(entry: entry, context: context, pagerSize: pagerSize) }) subscriber.putNext(UpdateTransition(deleted: removed, inserted: inserted, updated: updated)) subscriber.putCompletion() return EmptyDisposable - } |> runOn(prepareQueue) + } |> runOn(Queue.mainQueue()) } @@ -143,17 +161,51 @@ class GalleryMessagesBehavior { } } -final class GalleryBackgroundView : View { +final class GalleryBackgroundView : NSView { + + deinit { + var bp:Int = 0 + bp += 1 + } +} + + + +private final class GalleryTouchBarController : ViewController { + + private let interactions: GalleryInteractions + private let selectedItemChanged: (@escaping(MGalleryItem) -> Void)->Void + private let transition: (@escaping(UpdateTransition, MGalleryItem?) -> Void) -> Void + init(interactions: GalleryInteractions, selectedItemChanged: @escaping(@escaping(MGalleryItem) -> Void) ->Void, transition: @escaping(@escaping(UpdateTransition, MGalleryItem?) -> Void) ->Void) { + self.interactions = interactions + self.selectedItemChanged = selectedItemChanged + self.transition = transition + super.init() + } + private var temporaryTouchBar: Any? + + @available(OSX 10.12.2, *) + override func makeTouchBar() -> NSTouchBar? { + if temporaryTouchBar == nil { + temporaryTouchBar = GalleryTouchBar(interactions: interactions, selectedItemChanged: selectedItemChanged, transition: transition) + } + return temporaryTouchBar as? NSTouchBar + } + deinit { + var bp:Int = 0 + bp += 1 + } } + class GalleryViewer: NSResponder { - fileprivate var viewCache:[AnyHashable: NSView] = [:] - private var window:Window - private var controls:GalleryControls! - private let pager:GalleryPageController + let window:Window + // private var controls:GalleryControls! + private var controls: GalleryModernControls! + let pager:GalleryPageController private let backgroundView: GalleryBackgroundView = GalleryBackgroundView() private let ready = Promise() private var didSetReady = false @@ -162,49 +214,63 @@ class GalleryViewer: NSResponder { private let readyDispose = MetaDisposable() private let operationDisposable = MetaDisposable() private(set) weak var delegate:InteractionContentViewProtocol? - private let account:Account - + private let context:AccountContext + private let touchbarController: GalleryTouchBarController private let indexDisposable:MetaDisposable = MetaDisposable() + private let messagesActionDisposable = MetaDisposable() let interactions:GalleryInteractions = GalleryInteractions() - let contentInteractions:ChatMediaGalleryParameters? + let contentInteractions:ChatMediaLayoutParameters? + fileprivate var firstStableId: AnyHashable? = nil required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } let type:GalleryAppearType - - private init(account:Account, _ delegate:InteractionContentViewProtocol? = nil, _ contentInteractions:ChatMediaGalleryParameters? = nil, type: GalleryAppearType) { - self.account = account + let chatMode: ChatMode? + private let reversed: Bool + private init(context: AccountContext, _ delegate:InteractionContentViewProtocol? = nil, _ contentInteractions:ChatMediaLayoutParameters? = nil, type: GalleryAppearType, reversed:Bool = false, chatMode: ChatMode? = nil) { + self.context = context self.delegate = delegate self.type = type + self.chatMode = chatMode + self.reversed = reversed self.contentInteractions = contentInteractions if let screen = NSScreen.main { let bounds = NSMakeRect(0, 0, screen.frame.width, screen.frame.height) self.window = Window(contentRect: bounds, styleMask: [.borderless], backing: .buffered, defer: false, screen: screen) self.window.contentView?.wantsLayer = true - - self.window.level = .screenSaver + self.window.contentView?.canDrawSubviewsIntoLayer = true + + self.window.level = .popUpMenu self.window.isOpaque = false self.window.backgroundColor = .clear - - backgroundView.backgroundColor = .blackTransparent + // self.window.appearance = theme.appearance + backgroundView.wantsLayer = true + backgroundView.background = NSColor.black.withAlphaComponent(0.9) backgroundView.frame = bounds - self.pager = GalleryPageController(frame: bounds, contentInset:NSEdgeInsets(left: 0, right: 0, top: 0, bottom: 95), interactions:interactions, window:window) + self.pager = GalleryPageController(frame: bounds, contentInset:NSEdgeInsets(left: 0, right: 0, top: 0, bottom: 95), interactions:interactions, window:window, reversed: reversed) + //, selectedItemChanged: selectedItemChanged, transition: transition + self.touchbarController = GalleryTouchBarController(interactions: interactions, selectedItemChanged: pager.selectedItemChanged, transition: pager.transition) + self.window.rootViewController = touchbarController + } else { fatalError("main screen not found for MediaViewer") } + super.init() - window.set(responder: { [weak self] () -> NSResponder? in - return self - }, with: self, priority: .high) + NotificationCenter.default.addObserver(self, selector: #selector(windowDidBecomeKey), name: NSWindow.didBecomeKeyNotification, object: window) + NotificationCenter.default.addObserver(self, selector: #selector(windowDidResignKey), name: NSWindow.didResignKeyNotification, object: window) + - interactions.dismiss = { [weak self] () -> KeyHandlerResult in + interactions.dismiss = { [weak self] _ -> KeyHandlerResult in if let pager = self?.pager { + if pager.isFullScreen { - return .invokeNext + pager.exitFullScreen() + return .invoked } if !pager.lockedTransition { self?.close(true) @@ -213,46 +279,100 @@ class GalleryViewer: NSResponder { return .invoked } - interactions.next = { [weak self] () -> KeyHandlerResult in + interactions.next = { [weak self] _ -> KeyHandlerResult in self?.pager.next() return .invoked } - interactions.previous = { [weak self] () -> KeyHandlerResult in + interactions.previous = { [weak self] _ -> KeyHandlerResult in self?.pager.prev() return .invoked } + interactions.select = { [weak self] item in + self?.pager.select(by: item) + } + interactions.showActions = { [weak self] control -> KeyHandlerResult in self?.showControlsPopover(control) return .invoked } + interactions.share = { [weak self] control in + self?.share(control) + } + + interactions.openInfo = { [weak self] peerId in + self?.openInfo(peerId) + } + + interactions.openMessage = { [weak self] in + self?.showMessage() + } interactions.contextMenu = {[weak self] in return self?.contextMenu } - window.set(handler: interactions.dismiss, with:self, for: .Space) + interactions.zoomIn = { [weak self] in + self?.pager.zoomIn() + } + interactions.zoomOut = { [weak self] in + self?.pager.zoomOut() + } + interactions.rotateLeft = { [weak self] in + self?.pager.rotateLeft() + } + interactions.fastSave = { [weak self] in + self?.saveAs(true) + } + window.set(handler: { [weak self] event in + guard let `self` = self else {return .rejected} + if self.pager.selectedItem is MGalleryVideoItem || self.pager.selectedItem is MGalleryExternalVideoItem { + self.pager.selectedItem?.togglePlayerOrPause() + return .invoked + } else { + return self.interactions.dismiss(event) + } + }, with:self, for: .Space) + window.set(handler: interactions.dismiss, with:self, for: .Escape) window.closeInterceptor = { [weak self] in - _ = self?.interactions.dismiss() + if let event = NSApp.currentEvent { + _ = self?.interactions.dismiss(event) + } + return true } window.set(handler: interactions.next, with:self, for: .RightArrow) window.set(handler: interactions.previous, with:self, for: .LeftArrow) - window.set(handler: { [weak self] () -> KeyHandlerResult in + window.set(handler: { [weak self] _ -> KeyHandlerResult in self?.pager.zoomOut() return .invoked }, with: self, for: .Minus) - window.set(handler: { [weak self] () -> KeyHandlerResult in + window.set(handler: { [weak self] _ -> KeyHandlerResult in self?.pager.zoomIn() return .invoked }, with: self, for: .Equal) - window.set(handler: { [weak self] () -> KeyHandlerResult in + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.pager.decreaseSpeed() + return .invoked + }, with: self, for: .Minus, modifierFlags: [.command, .option]) + + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.pager.increaseSpeed() + return .invoked + }, with: self, for: .Equal, modifierFlags: [.command, .option]) + + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.pager.rotateLeft() + return .invoked + }, with: self, for: .R, modifierFlags: [.command]) + + window.set(handler: { [weak self] _ -> KeyHandlerResult in self?.saveAs() return .invoked }, with: self, for: .S, priority: .high, modifierFlags: [.command]) @@ -261,126 +381,159 @@ class GalleryViewer: NSResponder { self?.copy(nil) } - switch type { - case .secret: - self.controls = GallerySecretControls(View(frame:NSMakeRect(0, 10, 200, 75)), interactions:interactions) - default: - self.controls = GalleryGeneralControls(View(frame:NSMakeRect(0, 10, 400, 75)), interactions:interactions) + window.firstResponderFilter = { responder in + return responder } + + self.controls = GalleryModernControls(context, interactions: interactions, frame: NSMakeRect(0, -150, window.frame.width, 150), thumbsControl: pager.thumbsControl) + self.pager.view.addSubview(self.backgroundView) self.window.contentView?.addSubview(self.pager.view) - self.window.contentView?.addSubview(self.controls.view!) + self.window.contentView?.addSubview(self.controls.view) + + if #available(OSX 10.12.2, *) { + window.touchBar = window.makeTouchBar() + } + } + + @objc open func windowDidBecomeKey() { + + } + + + @objc open func windowDidResignKey() { + self.window.makeKeyAndOrderFront(self) + // window.makeFirstResponder(self) } var pagerSize: NSSize { return NSMakeSize(pager.frame.size.width - pager.contentInset.right - pager.contentInset.left, pager.frame.size.height - pager.contentInset.bottom - pager.contentInset.top) } - fileprivate convenience init(account:Account, peerId:PeerId, firstStableId:AnyHashable, _ delegate:InteractionContentViewProtocol? = nil, _ contentInteractions:ChatMediaGalleryParameters? = nil) { - self.init(account: account, delegate, contentInteractions, type: .profile(peerId)) + fileprivate convenience init(context: AccountContext, peerId:PeerId, firstStableId:AnyHashable, _ delegate:InteractionContentViewProtocol? = nil, _ contentInteractions:ChatMediaLayoutParameters? = nil, reversed:Bool = false, chatMode: ChatMode? = nil) { + self.init(context: context, delegate, contentInteractions, type: .profile(peerId), reversed: reversed, chatMode: chatMode) let pagerSize = self.pagerSize - ready.set(account.postbox.modify { modifier -> Peer? in - return modifier.getPeer(peerId) - } |> deliverOnMainQueue |> map { [weak self] peer -> Bool in - if let peer = peer { - var representations:[TelegramMediaImageRepresentation] = [] - if let representation = peer.smallProfileImage { - representations.append(representation) - } - if let representation = peer.largeProfileImage { - representations.append(representation) - } - let image = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.CloudImage, id: 0), representations: representations) - - _ = self?.pager.merge(with: UpdateTransition(deleted: [], inserted: [(0,MGalleryPeerPhotoItem(account, .photo(index: 0, stableId: firstStableId, photo: image, reference: .none), pagerSize))], updated: [])) - - self?.controls.index.set(.single((1,1))) - self?.pager.set(index: 0, animated: false) - - return true - } - return false - }) - - var totalCount:Int = 1 + let previous: Atomic<[GalleryEntry]> = Atomic(value: []) - self.disposable.set((requestPeerPhotos(account: account, peerId: peerId) |> map { photos -> (UpdateTransition, Int) in - - var inserted:[(Int, MGalleryItem)] = [] - var updated:[(Int, MGalleryItem)] = [] - let deleted:[Int] = [] - if !photos.isEmpty { - updated.append((0, MGalleryPeerPhotoItem(account, .photo(index: photos[0].index, stableId: firstStableId, photo: photos[0].image, reference: photos[0].reference), pagerSize))) - for i in 1 ..< photos.count { - inserted.append((i, MGalleryPeerPhotoItem(account, .photo(index: photos[i].index, stableId: photos[i].image.imageId, photo: photos[i].image, reference: photos[i].reference), pagerSize))) + let transaction: Signal<(UpdateTransition, Int), NoError> = peerPhotosGalleryEntries(context: context, peerId: peerId, firstStableId: firstStableId) |> map { (entries, selected) in + let (deleted, inserted, updated) = proccessEntriesWithoutReverse(previous.swap(entries), right: entries, { entry -> MGalleryItem in + switch entry { + case let .photo(_, _, photo, _, _, _, _): + if !photo.videoRepresentations.isEmpty { + return MGalleryGIFItem(context, entry, pagerSize) + } else { + return MGalleryPeerPhotoItem(context, entry, pagerSize) + } + default: + preconditionFailure() } - } - return (UpdateTransition(deleted: deleted, inserted: inserted, updated: updated), max(0, photos.count)) - - } |> deliverOnMainQueue).start(next: { [weak self] transition, total in - totalCount = total - self?.controls.index.set(.single((1,max(totalCount, 1)))) - _ = self?.pager.merge(with: transition) - - })) + }) + return (UpdateTransition(deleted: deleted, inserted: inserted, updated: updated), selected) + } |> deliverOnMainQueue - self.indexDisposable.set((pager.selectedIndex.get() |> deliverOnMainQueue).start(next: { [weak self] (selectedIndex) in - if let strongSelf = self { - self?.controls.index.set(.single((selectedIndex + 1, strongSelf.pager.count))) - } + + disposable.set(transaction.start(next: { [weak self] transaction, selected in + _ = self?.pager.merge(with: transaction, afterTransaction: { + self?.controls.update(self?.pager.selectedItem) + }) + self?.pager.selectedIndex.set(selected) + self?.pager.set(index: selected, animated: false) + self?.ready.set(.single(true)) + + })) + + self.indexDisposable.set((pager.selectedIndex.get() |> deliverOnMainQueue).start(next: { [weak self] selectedIndex in + guard let `self` = self else {return} + self.controls.update(self.pager.selectedItem) })) } - fileprivate convenience init(account:Account, instantMedias:[InstantPageMedia], firstIndex:Int, _ delegate:InteractionContentViewProtocol? = nil, _ contentInteractions:ChatMediaGalleryParameters? = nil) { - self.init(account: account, delegate, contentInteractions, type: .history) - + fileprivate convenience init(context: AccountContext, instantMedias:[InstantPageMedia], firstIndex:Int, firstStableId: AnyHashable? = nil, parent: Message? = nil, _ delegate:InteractionContentViewProtocol? = nil, _ contentInteractions:ChatMediaLayoutParameters? = nil, reversed: Bool = false, chatMode: ChatMode? = nil) { + self.init(context: context, delegate, contentInteractions, type: .history, reversed: reversed, chatMode: chatMode) + self.firstStableId = firstStableId let pagerSize = self.pagerSize - let totalCount:Int = instantMedias.count ready.set(.single(true) |> map { [weak self] _ -> Bool in + guard let `self` = self else {return false} + var inserted: [(Int, MGalleryItem)] = [] for i in 0 ..< instantMedias.count { let media = instantMedias[i] if media.media is TelegramMediaImage { - inserted.append((media.index, MGalleryPhotoItem(account, .instantMedia(media), pagerSize))) + inserted.append((media.index, MGalleryPhotoItem(context, .instantMedia(media, parent), pagerSize))) } else if let file = media.media as? TelegramMediaFile { if file.isVideo && file.isAnimated { - inserted.append((media.index, MGalleryGIFItem(account, .instantMedia(media), pagerSize))) + inserted.append((media.index, MGalleryGIFItem(context, .instantMedia(media, parent), pagerSize))) } else if file.isVideo { - inserted.append((media.index, MGalleryVideoItem(account, .instantMedia(media), pagerSize))) + inserted.append((media.index, MGalleryVideoItem(context, .instantMedia(media, parent), pagerSize))) } + } else if media.media is TelegramMediaWebpage { + inserted.append((media.index, MGalleryExternalVideoItem(context, .instantMedia(media, parent), pagerSize))) } } - _ = self?.pager.merge(with: UpdateTransition(deleted: [], inserted: inserted, updated: [])) - - self?.controls.index.set(.single((firstIndex + 1, totalCount))) - self?.pager.set(index: firstIndex, animated: false) + _ = self.pager.merge(with: UpdateTransition(deleted: [], inserted: inserted, updated: [])) + //self?.controls.index.set(.single((firstIndex + 1, totalCount))) + self.pager.set(index: firstIndex, animated: false) + self.controls.update(self.pager.selectedItem) return true }) - self.indexDisposable.set((pager.selectedIndex.get() |> deliverOnMainQueue).start(next: { [weak self] (selectedIndex) in - self?.controls.index.set(.single((selectedIndex + 1,totalCount))) + self.indexDisposable.set((pager.selectedIndex.get() |> deliverOnMainQueue).start(next: { [weak self] selectedIndex in + guard let `self` = self else {return} + self.controls.update(self.pager.selectedItem) })) } + + fileprivate convenience init(context: AccountContext, secureIdMedias:[SecureIdDocumentValue], firstIndex:Int, _ delegate:InteractionContentViewProtocol? = nil, reversed:Bool = false, chatMode: ChatMode? = nil) { + self.init(context: context, delegate, nil, type: .history, reversed: reversed, chatMode: chatMode) + + let pagerSize = self.pagerSize + + + + ready.set(.single(true) |> map { [weak self] _ -> Bool in + guard let `self` = self else {return false} + var inserted: [(Int, MGalleryItem)] = [] + for i in 0 ..< secureIdMedias.count { + let media = secureIdMedias[i] + inserted.append((i, MGalleryPhotoItem(context, .secureIdDocument(media, i), pagerSize))) + + } + + _ = self.pager.merge(with: UpdateTransition(deleted: [], inserted: inserted, updated: [])) + + self.pager.set(index: firstIndex, animated: false) + self.controls.update(self.pager.selectedItem) + return true + + }) + + self.indexDisposable.set((pager.selectedIndex.get() |> deliverOnMainQueue).start(next: { [weak self] selectedIndex in + guard let `self` = self else {return} + self.controls.update(self.pager.selectedItem) + })) + + + } - fileprivate convenience init(account:Account, message:Message, _ delegate:InteractionContentViewProtocol? = nil, _ contentInteractions:ChatMediaGalleryParameters? = nil, type: GalleryAppearType = .history, item: MGalleryItem? = nil) { + fileprivate convenience init(context: AccountContext, message:Message, _ delegate:InteractionContentViewProtocol? = nil, _ contentInteractions:ChatMediaLayoutParameters? = nil, type: GalleryAppearType = .history, item: MGalleryItem? = nil, reversed: Bool = false, chatMode: ChatMode? = nil, contextHolder: Atomic = Atomic(value: nil)) { - self.init(account: account, delegate, contentInteractions, type: type) + self.init(context: context, delegate, contentInteractions, type: type, reversed: reversed, chatMode: chatMode) - + let chatMode = self.chatMode let previous:Atomic<[ChatHistoryEntry]> = Atomic(value:[]) let current:Atomic<[ChatHistoryEntry]> = Atomic(value:[]) let currentIndex:Atomic = Atomic(value:nil) @@ -397,76 +550,154 @@ class GalleryViewer: NSResponder { ready.set(.single(true)) } - - let signal = request.get() |> distinctUntilChanged - |> mapToSignal { index -> Signal<(UpdateTransition, [ChatHistoryEntry], [ChatHistoryEntry]), Void> in + |> mapToSignal { index -> Signal<(UpdateTransition, [ChatHistoryEntry], [ChatHistoryEntry]), NoError> in + var type = type let tags = tagsForMessage(message) - let pullCount = tags != nil ? 50 : 1 - let view = account.viewTracker.aroundIdMessageHistoryViewForPeerId(message.id.peerId, count: pullCount, messageId: index.id, tagMask: tags, orderStatistics: [.combinedLocation]) + if tags == nil { + type = .alone + } + let mode: ChatMode = chatMode ?? .history + + let signal:Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> + switch mode { + case .history, .preview: + signal = context.account.viewTracker.aroundIdMessageHistoryViewForLocation(.peer(message.id.peerId), count: 50, ignoreRelatedChats: false, messageId: index.id, tagMask: tags, orderStatistics: [.combinedLocation], additionalData: []) + case let .replyThread(data, _): + if data.messageId == message.id { + signal = context.account.viewTracker.aroundIdMessageHistoryViewForLocation(.peer(message.id.peerId), count: 50, ignoreRelatedChats: false, messageId: index.id, tagMask: tags, orderStatistics: [.combinedLocation], additionalData: []) + } else { + signal = context.account.viewTracker.aroundIdMessageHistoryViewForLocation(context.chatLocationInput(for: .replyThread(data), contextHolder: contextHolder), count: 50, ignoreRelatedChats: false, messageId: index.id, tagMask: tags, orderStatistics: [.combinedLocation], additionalData: []) + } + case .pinned: + signal = context.account.viewTracker.aroundIdMessageHistoryViewForLocation(.peer(message.id.peerId), count: 50, ignoreRelatedChats: false, messageId: index.id, tagMask: .pinned, orderStatistics: [.combinedLocation], additionalData: []) + case .scheduled: + signal = context.account.viewTracker.scheduledMessagesViewForLocation(.peer(message.id.peerId)) + } + switch type { case .alone: - let entries:[ChatHistoryEntry] = [.MessageEntry(message, false, .Full(isAdmin: false), nil, nil)] + let entries:[ChatHistoryEntry] = [.MessageEntry(message, MessageIndex(message), false, .list, .Full(rank: nil, header: .normal), nil, ChatHistoryEntryData(nil, MessageEntryAdditionalData(), AutoplayMediaPreferences.defaultSettings))] let previous = previous.swap(entries) - return prepareEntries(from: previous, to: entries, account: account, pagerSize: pagerSize) |> map { transition in - return (transition,previous, entries) - } |> deliverOnMainQueue + + var inserted: [(Int, MGalleryItem)] = [] + + inserted.insert((0, itemFor(entry: entries[0], context: context, pagerSize: pagerSize)), at: 0) + + if let webpage = message.media.first as? TelegramMediaWebpage { + let instantMedias = instantPageMedias(for: webpage) + if instantMedias.count > 1 { + for i in 1 ..< instantMedias.count { + let media = instantMedias[i] + if media.media is TelegramMediaImage { + inserted.append((i, MGalleryPhotoItem(context, .instantMedia(media, message), pagerSize))) + } else if let file = media.media as? TelegramMediaFile { + if file.isVideo && file.isAnimated { + inserted.append((i, MGalleryGIFItem(context, .instantMedia(media, message), pagerSize))) + } else if file.isVideo || file.isVideoFile { + inserted.append((i, MGalleryVideoItem(context, .instantMedia(media, message), pagerSize))) + } + } else if media.media is TelegramMediaWebpage { + inserted.append((i, MGalleryExternalVideoItem(context, .instantMedia(media, message), pagerSize))) + } + } + } + + } + + return .single((UpdateTransition(deleted: [], inserted: inserted, updated: []), previous, entries)) |> deliverOnMainQueue + case .history: - return view |> mapToQueue { view, _, _ -> Signal<(UpdateTransition, [ChatHistoryEntry], [ChatHistoryEntry]), Void> in - let entries:[ChatHistoryEntry] = messageEntries(view.entries, includeHoles : false) - let previous = previous.swap(entries) - return prepareEntries(from: previous, to: entries, account: account, pagerSize: pagerSize) |> deliverOnMainQueue |> map { transition in + return signal |> mapToSignal { view, _, _ -> Signal<(UpdateTransition, [ChatHistoryEntry], [ChatHistoryEntry]), NoError> in + let entries:[ChatHistoryEntry] = messageEntries(view.entries, includeHoles : false).filter { entry -> Bool in + switch entry { + case let .MessageEntry(message, _, _, _, _, _, _): + return message.id.peerId.namespace == Namespaces.Peer.SecretChat || !message.containsSecretMedia && mediaForMessage(message: message, postbox: context.account.postbox) != nil + default: + return true + } + } + let previous = previous.with {$0} + return prepareEntries(from: previous, to: entries, context: context, pagerSize: pagerSize) |> deliverOnMainQueue |> map { transition in _ = indexes.swap((view.earlierId, view.laterId)) return (transition,previous, entries) } } case .secret: - return account.postbox.messageView(index.id) |> mapToQueue { view -> Signal<(UpdateTransition, [ChatHistoryEntry], [ChatHistoryEntry]), Void> in + return context.account.postbox.messageView(index.id) |> mapToSignal { view -> Signal<(UpdateTransition, [ChatHistoryEntry], [ChatHistoryEntry]), NoError> in var entries:[ChatHistoryEntry] = [] if let message = view.message, !(message.media.first is TelegramMediaExpiredContent) { - entries.append(.MessageEntry(message, false, .Full(isAdmin: false), nil, nil)) + entries.append(.MessageEntry(message, MessageIndex(message), false, .list, .Full(rank: nil, header: .normal), nil, ChatHistoryEntryData(nil, MessageEntryAdditionalData(), AutoplayMediaPreferences.defaultSettings))) } - let previous = previous.swap(entries) - return prepareEntries(from: previous, to: entries, account: account, pagerSize: pagerSize) |> map { transition in + let previous = previous.with {$0} + return prepareEntries(from: previous, to: entries, context: context, pagerSize: pagerSize) |> map { transition in return (transition,previous, entries) - } |> deliverOnMainQueue + } } case .profile: return .complete() + case let .messages(messages): + let messages = messages.map { + MessageHistoryEntry(message: $0, isRead: true, location: nil, monthLocation: nil, attributes: MutableMessageHistoryEntryAttributes(authorIsContact: false)) + } + let entries:[ChatHistoryEntry] = messageEntries(messages, includeHoles : false).filter { entry -> Bool in + switch entry { + case let .MessageEntry(message, _, _, _, _, _, _): + return message.id.peerId.namespace == Namespaces.Peer.SecretChat || !message.containsSecretMedia && mediaForMessage(message: message, postbox: context.account.postbox) != nil + default: + return true + } + } + let previous = previous.with {$0} + return prepareEntries(from: previous, to: entries, context: context, pagerSize: pagerSize) |> deliverOnMainQueue |> map { transition in + return (transition,previous, entries) + } } - } - |> map { [weak self] transition, previous, new in + } |> deliverOnMainQueue + |> map { [weak self] transition, prev, new in if let strongSelf = self { + + _ = previous.swap(new) + + let new = reversed ? new.reversed() : new + _ = current.swap(new) var id:MessageId = message.id let index = currentIndex.modify({$0}) - if let index = index { - id = previous[index].message!.id + if let index = index, prev.count > index { + id = prev[index].message!.id } - for i in 0 ..< new.count { - if let message = new[i].message { - if message.id == id { - _ = currentIndex.swap(i) + var current:Int? = currentIndex.modify({$0}) + if current == nil || !reversed { + for i in 0 ..< new.count { + if let message = new[i].message { + if message.id == id { + current = i + } } } } +// let isEmpty = strongSelf.pager.merge(with: transition) + if !isEmpty { - if let newIndex = currentIndex.modify({$0}) { + + + if let newIndex = current { strongSelf.pager.selectedIndex.set(newIndex) strongSelf.pager.set(index: newIndex, animated: false) - if let attribute = new[newIndex].message?.autoremoveAttribute { - (self?.controls as? GallerySecretControls)?.update(with: attribute, outgoing: !new[newIndex].message!.flags.contains(.Incoming)) + if !new.isEmpty && newIndex < new.count && newIndex >= 0, let attribute = new[newIndex].message?.autoremoveAttribute { + // (self?.controls as? GallerySecretControls)?.update(with: attribute, outgoing: !new[newIndex].message!.flags.contains(.Incoming)) } } } else { @@ -474,7 +705,6 @@ class GalleryViewer: NSResponder { } strongSelf.ready.set(.single(true)) } - } @@ -483,27 +713,38 @@ class GalleryViewer: NSResponder { self.indexDisposable.set(pager.selectedIndex.get().start(next: { [weak self] (selectedIndex) in let entries = current.modify({$0}) - let selectedIndex = min(entries.count - 1, selectedIndex) + guard let `self` = self else {return} + let current = entries[selectedIndex] if let location = current.location { - self?.controls.index.set(.single((location.index + 1, location.count))) - } else { - self?.controls.index.set(.single((1,1))) + let total = location.count + let current = reversed ? total - location.index : location.index + self.controls.update(self.pager.selectedItem) + } else { + self.controls.update(self.pager.selectedItem) } if let message = entries[selectedIndex].message, message.containsSecretMedia { - _ = (markMessageContentAsConsumedInteractively(postbox: account.postbox, messageId: message.id) |> delay(0.5, queue: Queue.concurrentDefaultQueue())).start() + _ = (context.engine.messages.markMessageContentAsConsumedInteractively(messageId: message.id) |> delay(0.5, queue: Queue.concurrentDefaultQueue())).start() } let indexes = indexes.modify({$0}) if let pagerIndex = currentIndex.modify({$0}) { - if selectedIndex < pagerIndex && pagerIndex < reqlimit, let earlier = indexes.earlierId { - request.set(.single(earlier)) - } else if selectedIndex > pagerIndex && pagerIndex > entries.count - reqlimit, let later = indexes.laterId { - request.set(.single(later)) + if selectedIndex < pagerIndex && pagerIndex < reqlimit { + if !reversed, let earlier = indexes.earlierId { + request.set(.single(earlier)) + } else if reversed, let later = indexes.laterId { + request.set(.single(later)) + } + } else if selectedIndex > pagerIndex && pagerIndex > entries.count - reqlimit { + if !reversed, let later = indexes.laterId { + request.set(.single(later)) + } else if reversed, let earlier = indexes.earlierId { + request.set(.single(earlier)) + } } } _ = currentIndex.swap(selectedIndex) @@ -515,34 +756,225 @@ class GalleryViewer: NSResponder { } - + func showControlsPopover(_ control:Control) { + + if let popover = control.popover { + popover.hide() + return + } + var items:[SPopoverItem] = [] - items.append(SPopoverItem(tr(.galleryContextSaveAs), {[weak self] in - self?.saveAs() - })) - if let _ = self.contentInteractions, case .history = type { - items.append(SPopoverItem(tr(.galleryContextShowMessage), {[weak self] in - self?.showMessage() + + if pager.selectedItem?.entry.message?.containsSecretMedia == true { + } else { + items.append(SPopoverItem(L10n.galleryContextSaveAs, {[weak self] in + self?.saveAs() })) } - items.append(SPopoverItem(tr(.galleryContextCopyToClipboard), {[weak self] in - self?.copy(nil) - })) + + + let context = self.context + + var chatMode: ChatMode = self.chatMode ?? .history + if let message = pager.selectedItem?.entry.message, message.isScheduledMessage { + chatMode = .scheduled + } + + if let item = pager.selectedItem as? MGalleryGIFItem, chatMode == .history { + let file = item.media + if file.isAnimated && file.isVideo { + let reference = item.entry.fileReference(file) + items.append(SPopoverItem(L10n.gallerySaveGif, { + let _ = addSavedGif(postbox: context.account.postbox, fileReference: reference).start() + })) + } + } + + + + if let _ = self.contentInteractions { + if let message = pager.selectedItem?.entry.message { + if self.type == .history { + items.append(SPopoverItem(L10n.galleryContextShowMessage, { [weak self] in + self?.showMessage() + })) + } + if chatMode == .history && message.id.peerId != repliesPeerId && self.type == .history { + items.append(SPopoverItem(L10n.galleryContextShowGallery, { [weak self] in + self?.showSharedMedia() + })) + } + if canDeleteMessage(message, account: context.account, mode: .history) { + items.append(SPopoverItem(L10n.galleryContextDeletePhoto, { [weak self] in + self?.deleteMessage(control) + })) + } + } + } + + if pager.selectedItem?.entry.message?.containsSecretMedia == true { + } else { + items.append(SPopoverItem(L10n.galleryContextCopyToClipboard, {[weak self] in + self?.copy(nil) + })) + } + switch type { case .profile(let peerId): - if peerId == account.peerId { - items.append(SPopoverItem(tr(.galleryContextDeletePhoto), {[weak self] in + if peerId == context.peerId { + items.append(SPopoverItem(L10n.galleryContextDeletePhoto, {[weak self] in self?.deletePhoto() })) + if pager.currentIndex != 0 { + items.append(SPopoverItem(L10n.galleryContextMainPhoto, { [weak self] in + self?.updateMainPhoto() + })) + } } default: break } - showPopover(for: control, with: SPopoverViewController(items: items), inset:NSMakePoint((-125 + 14),0)) + items.append(SPopoverItem(L10n.navigationClose, { [weak self] in + if let event = NSApp.currentEvent { + _ = self?.interactions.dismiss(event) + } + })) + + showPopover(for: control, with: SPopoverViewController(items: items, visibility: 6), inset:NSMakePoint((-105 + 14), 0), static: true) + } + + private func deleteMessages(_ messages:[Message]) { + if !messages.isEmpty, let peer = messageMainPeer(messages[0]) { + + let peerId = messages[0].id.peerId + let messageIds = messages.map {$0.id} + + let adminsPromise = ValuePromise<[RenderedChannelParticipant]>([]) + _ = context.peerChannelMemberCategoriesContextsManager.admins(peerId: peerId, updated: { membersState in + if case .loading = membersState.loadingState, membersState.list.isEmpty { + adminsPromise.set([]) + } else { + adminsPromise.set(membersState.list) + } + }) + + + messagesActionDisposable.set((adminsPromise.get() |> deliverOnMainQueue).start( next:{ [weak self] admins in + guard let `self` = self else {return} + + var canDelete:Bool = true + var canDeleteForEveryone = true + var otherCounter:Int32 = 0 + var _mustDeleteForEveryoneMessage: Bool = true + for message in messages { + if !canDeleteMessage(message, account: self.context.account, mode: .history) { + canDelete = false + } + if !mustDeleteForEveryoneMessage(message) { + _mustDeleteForEveryoneMessage = false + } + if !canDeleteForEveryoneMessage(message, context: self.context) { + canDeleteForEveryone = false + } else { + if message.effectiveAuthor?.id != self.context.peerId && !(self.context.limitConfiguration.canRemoveIncomingMessagesInPrivateChats && message.peers[message.id.peerId] is TelegramUser) { + if let peer = message.peers[message.id.peerId] as? TelegramGroup { + inner: switch peer.role { + case .member: + otherCounter += 1 + default: + break inner + } + } else { + otherCounter += 1 + } + } + } + } + + if otherCounter == messages.count { + canDeleteForEveryone = false + } + + if canDelete { + let thrid:String? = (canDeleteForEveryone ? peer.isUser ? L10n.chatMessageDeleteForMeAndPerson(peer.compactDisplayTitle) : L10n.chatConfirmDeleteMessagesForEveryone : nil) + + + if let thrid = thrid { + modernConfirm(for: self.window, account: self.context.account, peerId: nil, header: L10n.chatConfirmDeleteMessages1Countable(messages.count), information: nil, okTitle: L10n.confirmDelete, thridTitle: thrid, successHandler: { [weak self] result in + guard let `self` = self else {return} + + let type:InteractiveMessagesDeletionType + switch result { + case .basic: + type = .forLocalPeer + case .thrid: + type = .forEveryone + } + _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: messageIds, type: type).start() + }) + } else { + _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: messageIds, type: .forLocalPeer).start() + } + } + })) + } + } + + private func deleteMessage(_ control: Control) { + if let message = self.pager.selectedItem?.entry.message { + let messages = pager.thumbsControl.items.compactMap({$0.entry.message}) + + if messages.count > 1 { + + var items:[SPopoverItem] = [] + + let thisTitle: String + if message.media.first is TelegramMediaImage { + thisTitle = L10n.galleryContextShareThisPhoto + } else { + thisTitle = L10n.galleryContextShareThisVideo + } + items.append(SPopoverItem(thisTitle, { [weak self] in + self?.deleteMessages([message]) + })) + + + let allTitle: String + if messages.filter({$0.media.first is TelegramMediaImage}).count == messages.count { + allTitle = L10n.galleryContextShareAllPhotosCountable(messages.count) + } else if messages.filter({$0.media.first is TelegramMediaFile}).count == messages.count { + allTitle = L10n.galleryContextShareAllVideosCountable(messages.count) + } else { + allTitle = L10n.galleryContextShareAllItemsCountable(messages.count) + } + + items.append(SPopoverItem(allTitle, { [weak self] in + self?.deleteMessages(messages) + })) + showPopover(for: control, with: SPopoverViewController(items: items), inset:NSMakePoint((-90 + 14),0), static: true) + } else { + deleteMessages([message]) + } + } + } + + private func updateMainPhoto() { + if let item = self.pager.selectedItem { + if let index = self.pager.index(for: item) { + if case let .photo(_, _, _, reference, _, _, _) = item.entry { + if let reference = reference { + _ = context.engine.accountData.updatePeerPhotoExisting(reference: reference).start() + _ = pager.merge(with: UpdateTransition(deleted: [index], inserted: [(0, item)], updated: [])) + pager.selectedIndex.set(0) + } + } + } + + } } private func deletePhoto() { @@ -554,10 +986,10 @@ class GalleryViewer: NSResponder { close() } - pager.selectedIndex.set(index - 1) + pager.selectedIndex.set(index) - if case let .photo(_, _, _, reference) = item.entry { - _ = removeUserPhoto(account: account, reference: index == 0 ? .none : reference).start() + if case let .photo(_, _, _, reference, _, _, _) = item.entry { + _ = context.engine.accountData.removeAccountPhoto(reference: index == 0 ? nil : reference).start() } } @@ -570,17 +1002,17 @@ class GalleryViewer: NSResponder { if let item = self.pager.selectedItem { if !(item is MGalleryExternalVideoItem) { - menu.addItem(ContextMenuItem(tr(.galleryContextSaveAs), handler: { [weak self] in + menu.addItem(ContextMenuItem(tr(L10n.galleryContextSaveAs), handler: { [weak self] in self?.saveAs() })) } if let _ = self.contentInteractions { - menu.addItem(ContextMenuItem(tr(.galleryContextShowMessage), handler: { [weak self] in + menu.addItem(ContextMenuItem(tr(L10n.galleryContextShowMessage), handler: { [weak self] in self?.showMessage() })) } - menu.addItem(ContextMenuItem(tr(.galleryContextCopyToClipboard), handler: { [weak self] in + menu.addItem(ContextMenuItem(tr(L10n.galleryContextCopyToClipboard), handler: { [weak self] in self?.copy(nil) })) } @@ -590,14 +1022,75 @@ class GalleryViewer: NSResponder { } - func saveAs() -> Void { + func saveAs(_ fast: Bool = false) -> Void { if let item = self.pager.selectedItem { if !(item is MGalleryExternalVideoItem) { - operationDisposable.set((item.path.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak self] path in - if let strongSelf = self { - savePanel(file: path.nsstring.deletingPathExtension, ext: path.nsstring.pathExtension, for: strongSelf.window) + let isPhoto = item is MGalleryPhotoItem || item is MGalleryPeerPhotoItem + operationDisposable.set((item.realStatus |> take(1) |> deliverOnMainQueue).start(next: { [weak self] status in + guard let `self` = self else {return} + switch status { + case .Local: + self.operationDisposable.set((item.path.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak self] path in + if let strongSelf = self { + if fast { + + let text: String + if item is MGalleryVideoItem { + text = L10n.galleryViewFastSaveVideo1 + } else if item is MGalleryGIFItem { + text = L10n.galleryViewFastSaveGif1 + } else { + text = L10n.galleryViewFastSaveImage1 + } + + let dateFormatter = makeNewDateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH.mm.ss" + + + let file: TelegramMediaFile? + if let item = item as? MGalleryVideoItem { + file = item.media + } else if let item = item as? MGalleryGIFItem { + file = item.media + } else if let photo = item as? MGalleryPhotoItem { + file = photo.entry.file ?? TelegramMediaFile(fileId: MediaId(namespace: 0, id: arc4random64()), partialReference: nil, resource: photo.media.representations.last!.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/jpeg", size: nil, attributes: [.FileName(fileName: "photo_\(dateFormatter.string(from: Date())).jpeg")]) + } else if let photo = item as? MGalleryPeerPhotoItem { + file = TelegramMediaFile(fileId: MediaId(namespace: 0, id: arc4random64()), partialReference: nil, resource: photo.media.representations.last!.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/jpeg", size: nil, attributes: [.FileName(fileName: "photo_\(dateFormatter.string(from: Date())).jpeg")]) + } else { + file = nil + } + + let context = strongSelf.context + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: .bold(15), textColor: .white), bold: MarkdownAttributeSet(font: .bold(15), textColor: .white), link: MarkdownAttributeSet(font: .bold(15), textColor: .link), linkAttribute: { contents in + return (NSAttributedString.Key.link.rawValue, inAppLink.callback(contents, { _ in })) + })).mutableCopy() as! NSMutableAttributedString + + let layout = TextViewLayout(attributedText, alignment: .center, lineSpacing: 5.0, alwaysStaticItems: true) + layout.interactions = TextViewInteractions(processURL: { [weak strongSelf] url in + if let file = file { + showInFinder(file, account: context.account) + strongSelf?.close(false) + } + }) + layout.measure(width: 160) + + if let file = file { + + _ = (copyToDownloads(file, postbox: context.account.postbox, saveAnyway: true) |> map { _ in } |> deliverOnMainQueue |> take(1) |> then (showSaveModal(for: strongSelf.window, context: context, animation: LocalAnimatedSticker.success_saved, text: layout, delay: 3.0))).start() + } else { + savePanel(file: path.nsstring.deletingPathExtension, ext: path.nsstring.pathExtension, for: strongSelf.window) + } + } else { + savePanel(file: path.nsstring.deletingPathExtension, ext: path.nsstring.pathExtension, for: strongSelf.window) + } + } + })) + default: + alert(for: self.window, info: isPhoto ? L10n.galleryWaitDownloadPhoto : L10n.galleryWaitDownloadVideo) } + })) + } } @@ -610,9 +1103,68 @@ class GalleryViewer: NSResponder { } } + func showSharedMedia() { + close() + if let message = self.pager.selectedItem?.entry.message { + context.sharedContext.bindings.rootNavigation().push(PeerMediaController(context: context, peerId: message.id.peerId)) + } + } + + func openInfo(_ peerId: PeerId) { + close() + context.sharedContext.bindings.rootNavigation().push(PeerInfoController(context: context, peerId: peerId)) + } + + func share(_ control: Control) -> Void { + if let message = self.pager.selectedItem?.entry.message { + if message.groupInfo != nil { + let messages = pager.thumbsControl.items.compactMap({$0.entry.message}) + var items:[SPopoverItem] = [] + + let thisTitle: String + if message.media.first is TelegramMediaImage { + thisTitle = L10n.galleryContextShareThisPhoto + } else if message.media.first!.isVideoFile { + thisTitle = L10n.galleryContextShareThisVideo + } else if message.media.first!.isGraphicFile { + thisTitle = L10n.galleryContextShareThisPhoto + } else { + thisTitle = L10n.galleryContextShareThisFile + } + + items.append(SPopoverItem(thisTitle, { [weak self] in + guard let `self` = self else {return} + showModal(with: ShareModalController(ShareMessageObject(self.context, message)), for: self.window) + + })) + + let allTitle: String + if messages.filter({$0.media.first is TelegramMediaImage}).count == messages.count { + allTitle = L10n.galleryContextShareAllPhotosCountable(messages.count) + } else if messages.filter({ $0.media.first!.isVideoFile }).count == messages.count { + allTitle = L10n.galleryContextShareAllVideosCountable(messages.count) + } else if messages.filter({ $0.media.first!.isGraphicFile }).count == messages.count { + allTitle = L10n.galleryContextShareAllPhotosCountable(messages.count) + } else { + allTitle = L10n.galleryContextShareAllItemsCountable(messages.count) + } + + items.append(SPopoverItem(allTitle, { [weak self] in + guard let `self` = self else {return} + showModal(with: ShareModalController(ShareMessageObject(self.context, message, messages)), for: self.window) + })) + + + showPopover(for: control, with: SPopoverViewController(items: items), inset:NSMakePoint((-125 + 14),0), static: true) + } else { + showModal(with: ShareModalController(ShareMessageObject(self.context, message)), for: self.window) + } + } + } + @objc func copy(_ sender:Any? = nil) -> Void { if let item = self.pager.selectedItem { - if !(item is MGalleryExternalVideoItem) { + if !(item is MGalleryExternalVideoItem), item.entry.message?.containsSecretMedia != true { operationDisposable.set((item.path.get() |> take(1) |> deliverOnMainQueue).start(next: { path in let pb = NSPasteboard.general pb.clearContents() @@ -632,23 +1184,49 @@ class GalleryViewer: NSResponder { fileprivate func show(_ animated: Bool = true, _ ignoreStableId:AnyHashable? = nil) -> Void { viewer = self - window.makeFirstResponder(self) + mainWindow.resignFirstResponder() + self.window.makeKeyAndOrderFront(nil) + //window.makeFirstResponder(self) //closePipVideo() - backgroundView.change(opacity: 0, animated: false) - self.readyDispose.set((self.ready.get() |> take(1) |> deliverOnMainQueue).start { [weak self] in + // backgroundView.alphaValue = 0 + backgroundView._change(opacity: 0, animated: false) + self.readyDispose.set((self.ready.get() |> take(1) |> deliverOnMainQueue).start(completed: { [weak self] in if let strongSelf = self { - strongSelf.backgroundView.change(opacity: 1, animated: animated) + + if let startTime = strongSelf.contentInteractions?.timeCodeInitializer { + if let item = strongSelf.pager.selectedItem as? MGalleryVideoItem { + item.startTime = startTime + } + } + strongSelf.pager.animateIn(from: { [weak strongSelf] stableId -> NSView? in + + + if let firstStableId = strongSelf?.firstStableId, let innerIndex = stableId.base as? Int { + if let ignore = ignoreStableId?.base as? Int, ignore == innerIndex { + return nil + } + let view = strongSelf?.delegate?.contentInteractionView(for: firstStableId, animateIn: false) + return view?.subviews[innerIndex] + } if ignoreStableId != stableId { - return strongSelf?.delegate?.contentInteractionView(for: stableId) + return strongSelf?.delegate?.contentInteractionView(for: stableId, animateIn: false) } + return nil }, completion:{ [weak strongSelf] in + //strongSelf?.backgroundView.alphaValue = 1.0 strongSelf?.controls.animateIn() + strongSelf?.backgroundView._change(opacity: 1, animated: false) + }, addAccesoryOnCopiedView: { stableId, view in + if let stableId = stableId { + //self?.delegate?.addAccesoryOnCopiedView(for: stableId, view: view) + } + }, addVideoTimebase: { stableId, view in + }) - strongSelf.window.makeKeyAndOrderFront(nil) } - }); + })); } @@ -656,16 +1234,30 @@ class GalleryViewer: NSResponder { disposable.dispose() readyDispose.dispose() didSetReady = false - + NotificationCenter.default.removeObserver(self) if animated { - backgroundView.change(opacity: 0, duration: 0.15, timingFunction: kCAMediaTimingFunctionSpring) + backgroundView._change(opacity: 0, animated: false) controls.animateOut() - self.pager.animateOut(to: {[weak self] (stableId) -> NSView? in - return self?.delegate?.contentInteractionView(for: stableId) - }, completion: { [weak self] in + self.pager.animateOut(to: { [weak self] stableId in + if let firstStableId = self?.firstStableId, let innerIndex = stableId.base as? Int { + let view = self?.delegate?.contentInteractionView(for: firstStableId, animateIn: false) + return view?.subviews[innerIndex] + } + return self?.delegate?.contentInteractionView(for: stableId, animateIn: true) + }, completion: { [weak self] interactive, stableId in + + if let stableId = stableId { + self?.delegate?.interactionControllerDidFinishAnimation(interactive: interactive, for: stableId) + } self?.window.orderOut(nil) viewer = nil playPipIfNeeded() + }, addAccesoryOnCopiedView: { [weak self] stableId, view in + if let stableId = stableId { + self?.delegate?.addAccesoryOnCopiedView(for: stableId, view: view) + } + }, addVideoTimebase: { stableId, view in + }) } else { window.orderOut(nil) @@ -676,43 +1268,64 @@ class GalleryViewer: NSResponder { } deinit { + clean() + } + + func clean() { indexDisposable.dispose() disposable.dispose() operationDisposable.dispose() window.removeAllHandlers(for: self) readyDispose.dispose() + messagesActionDisposable.dispose() + self.contentInteractions?.remove_timeCodeInitializer() } + } + func closeGalleryViewer(_ animated: Bool) { viewer?.close(animated) } -func showChatGallery(account:Account, message:Message, _ delegate:InteractionContentViewProtocol? = nil, _ contentInteractions:ChatMediaGalleryParameters? = nil, type: GalleryAppearType = .history) { +func showChatGallery(context: AccountContext, message:Message, _ delegate:InteractionContentViewProtocol? = nil, _ contentInteractions:ChatMediaLayoutParameters? = nil, type: GalleryAppearType = .history, reversed: Bool = false, chatMode: ChatMode? = nil, contextHolder: Atomic = Atomic(value: nil)) { if viewer == nil { - let gallery = GalleryViewer(account: account, message: message, delegate, contentInteractions, type: type) + viewer?.clean() + let gallery = GalleryViewer(context: context, message: message, delegate, contentInteractions, type: type, reversed: reversed, chatMode: chatMode, contextHolder: contextHolder) gallery.show() } } -func showGalleryFromPip(item: MGalleryItem, delegate:InteractionContentViewProtocol? = nil, contentInteractions:ChatMediaGalleryParameters? = nil, type: GalleryAppearType = .history) { - if viewer == nil, let message = item.entry.message { - let gallery = GalleryViewer(account: item.account, message: message, delegate, contentInteractions, type: type, item: item) +func showGalleryFromPip(item: MGalleryItem, gallery: GalleryViewer, delegate:InteractionContentViewProtocol? = nil, contentInteractions:ChatMediaLayoutParameters? = nil, type: GalleryAppearType = .history) { + if viewer == nil { + viewer?.clean() gallery.show(true, item.stableId) } } -func showPhotosGallery(account:Account, peerId:PeerId, firstStableId:AnyHashable, _ delegate:InteractionContentViewProtocol? = nil, _ contentInteractions:ChatMediaGalleryParameters? = nil) { +func showPhotosGallery(context: AccountContext, peerId:PeerId, firstStableId:AnyHashable, _ delegate:InteractionContentViewProtocol? = nil, _ contentInteractions:ChatMediaLayoutParameters? = nil) { + if viewer == nil { + viewer?.clean() + let gallery = GalleryViewer(context: context, peerId: peerId, firstStableId: firstStableId, delegate, contentInteractions) + gallery.show() + } +} + +func showInstantViewGallery(context: AccountContext, medias:[InstantPageMedia], firstIndex: Int, firstStableId:AnyHashable? = nil, parent: Message? = nil, _ delegate: InteractionContentViewProtocol? = nil, _ contentInteractions:ChatMediaLayoutParameters? = nil) { if viewer == nil { - let gallery = GalleryViewer(account: account, peerId: peerId, firstStableId: firstStableId, delegate, contentInteractions) + viewer?.clean() + let gallery = GalleryViewer(context: context, instantMedias: medias, firstIndex: firstIndex, firstStableId: firstStableId, parent: parent, delegate, contentInteractions) gallery.show() } } -func showInstantViewGallery(account: Account, medias:[InstantPageMedia], firstIndex: Int, _ delegate: InteractionContentViewProtocol? = nil, _ contentInteractions:ChatMediaGalleryParameters? = nil) { + +func showSecureIdDocumentsGallery(context: AccountContext, medias:[SecureIdDocumentValue], firstIndex: Int, _ delegate: InteractionContentViewProtocol? = nil) { if viewer == nil { - let gallery = GalleryViewer(account: account, instantMedias: medias, firstIndex: firstIndex, delegate, contentInteractions) + viewer?.clean() + let gallery = GalleryViewer(context: context, secureIdMedias: medias, firstIndex: firstIndex, delegate) gallery.show() } + } diff --git a/Telegram-Mac/GeneralBlockTextRowItem.swift b/Telegram-Mac/GeneralBlockTextRowItem.swift new file mode 100644 index 0000000000..fa895a4004 --- /dev/null +++ b/Telegram-Mac/GeneralBlockTextRowItem.swift @@ -0,0 +1,152 @@ +// +// GeneralBlockTextRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 24/09/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +struct GeneralBlockTextHeader { + let text: String + let icon: CGImage? + init(text: String, icon: CGImage?) { + self.text = text + self.icon = icon + } +} + +class GeneralBlockTextRowItem: GeneralRowItem { + fileprivate let textLayout: TextViewLayout + fileprivate let header: GeneralBlockTextHeader? + fileprivate let headerLayout: TextViewLayout? + init(_ initialSize: NSSize, stableId: AnyHashable, viewType: GeneralViewType, text: String, font: NSFont, header: GeneralBlockTextHeader? = nil) { + self.textLayout = TextViewLayout(.initialize(string: text, color: theme.colors.text, font: font), alwaysStaticItems: false) + self.header = header + if let header = header { + self.headerLayout = TextViewLayout(.initialize(string: header.text, color: theme.colors.text, font: .medium(.title)), maximumNumberOfLines: 3) + } else { + self.headerLayout = nil + } + super.init(initialSize, stableId: stableId, viewType: viewType) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + + self.textLayout.measure(width: self.blockWidth - self.viewType.innerInset.left - self.viewType.innerInset.right) + self.headerLayout?.measure(width: self.blockWidth - self.viewType.innerInset.left - self.viewType.innerInset.right) + return true + } + + override func viewClass() -> AnyClass { + return GeneralBlockTextRowView.self + } + + override var height: CGFloat { + var height: CGFloat = textLayout.layoutSize.height + viewType.innerInset.bottom + viewType.innerInset.top + + if let headerLayout = self.headerLayout { + height += (headerLayout.layoutSize.height + 4) + } + + return height + } +} + + +private final class GeneralBlockTextRowView : TableRowView { + private let containerView = GeneralRowContainerView(frame: NSZeroRect) + private let textView = TextView() + private var headerView: TextView? + private var headerImageView : ImageView? + private let separator: View = View() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(containerView) + containerView.addSubview(textView) + containerView.addSubview(separator) + } + + override var backdorColor: NSColor { + return theme.colors.background + } + + override func updateColors() { + guard let item = item as? GeneralBlockTextRowItem else { + return + } + self.backgroundColor = item.viewType.rowBackground + self.containerView.backgroundColor = backdorColor + self.textView.backgroundColor = backdorColor + self.separator.backgroundColor = theme.colors.border + } + + override func layout() { + super.layout() + guard let item = item as? GeneralBlockTextRowItem else { + return + } + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) + self.containerView.setCorners(item.viewType.corners) + + if let headerView = headerView { + var inset: CGFloat = 0 + if let headerImageView = headerImageView { + headerImageView.setFrameOrigin(NSMakePoint(item.viewType.innerInset.left, item.viewType.innerInset.top + 2)) + inset += headerImageView.frame.width + 4 + } + headerView.setFrameOrigin(NSMakePoint(item.viewType.innerInset.left + inset, item.viewType.innerInset.top)) + textView.setFrameOrigin(NSMakePoint(item.viewType.innerInset.left, headerView.frame.maxY + 4)) + } else { + textView.setFrameOrigin(NSMakePoint(item.viewType.innerInset.left, item.viewType.innerInset.top)) + } + + + separator.frame = NSMakeRect(item.viewType.innerInset.left, containerView.frame.height - .borderSize, containerView.frame.width - item.viewType.innerInset.left - item.viewType.innerInset.right, .borderSize) + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? GeneralBlockTextRowItem else { + return + } + + if let headerLayout = item.headerLayout { + if headerView == nil { + self.headerView = TextView() + self.headerView?.userInteractionEnabled = false + self.headerView?.isSelectable = false + containerView.addSubview(self.headerView!) + } + headerView?.update(headerLayout) + + if let image = item.header?.icon { + if headerImageView == nil { + self.headerImageView = ImageView() + containerView.addSubview(self.headerImageView!) + } + headerImageView?.image = image + headerImageView?.sizeToFit() + } + } else { + self.headerView?.removeFromSuperview() + self.headerView = nil + } + + textView.update(item.textLayout) + self.separator.isHidden = !item.viewType.hasBorder + needsLayout = true + } + + override var firstResponder: NSResponder? { + return nil + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/GeneralInputRow.swift b/Telegram-Mac/GeneralInputRow.swift index bbc0f23665..3446b72b79 100644 --- a/Telegram-Mac/GeneralInputRow.swift +++ b/Telegram-Mac/GeneralInputRow.swift @@ -8,7 +8,7 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac +import SwiftSignalKit enum GeneralInputRowType { case plain @@ -39,32 +39,37 @@ class GeneralInputRowItem: TableRowItem { let placeholder:NSAttributedString let limit:Int32 let holdText:Bool + let automaticallyBecomeResponder: Bool fileprivate let canFastClean: Bool let _stableId:AnyHashable override var stableId: AnyHashable { return _stableId } + fileprivate let font: NSFont - - init(_ initialSize:NSSize, stableId:AnyHashable = arc4random(), placeholder:String, text:String = "", limit:Int32 = 140, insets: NSEdgeInsets = NSEdgeInsets(left:25,right:25,top:2,bottom:3), textChangeHandler:@escaping(String)->Void = {_ in}, textFilter:@escaping(String)->String = {value in return value}, holdText:Bool = false, inputType: GeneralInputRowType = .plain, pasteFilter:((String)->(Bool, String))? = nil, canFastClean: Bool = false) { + init(_ initialSize:NSSize, stableId:AnyHashable = arc4random(), placeholder:String, text:String = "", limit:Int32 = 140, insets: NSEdgeInsets = NSEdgeInsets(left:25,right:25,top:2,bottom:3), textChangeHandler:@escaping(String)->Void = {_ in}, textFilter:@escaping(String)->String = {value in return value}, holdText:Bool = false, font: NSFont = .normal(.text), inputType: GeneralInputRowType = .plain, pasteFilter:((String)->(Bool, String))? = nil, canFastClean: Bool = false, automaticallyBecomeResponder: Bool = true) { _stableId = stableId self.insets = insets + self.automaticallyBecomeResponder = automaticallyBecomeResponder self.pasteFilter = pasteFilter self.holdText = holdText self.canFastClean = canFastClean self.textChangeHandler = textChangeHandler self.limit = limit + self.font = font self.text = text self.inputType = inputType self.textFilter = textFilter self.placeholder = .initialize(string: placeholder, color: theme.colors.grayText, font: .normal(.text), coreText: false) - let textStorage = NSTextStorage(attributedString: .initialize(string: text)) - let textContainer = NSTextContainer(containerSize: NSMakeSize(initialSize.width - insets.left - insets.right, .greatestFiniteMagnitude)) + let textStorage = NSTextStorage(attributedString: .initialize(string: text, font: font, coreText: false)) + let textContainer = NSTextContainer(size: NSMakeSize(initialSize.width - insets.left - insets.right, .greatestFiniteMagnitude)) let layoutManager = NSLayoutManager(); + layoutManager.addTextContainer(textContainer) + textStorage.addLayoutManager(layoutManager) layoutManager.ensureLayout(for: textContainer) @@ -78,9 +83,11 @@ class GeneralInputRowItem: TableRowItem { var _height:CGFloat = 24 override var height: CGFloat { - return _height + insets.top + insets.bottom + return _height + insets.top + insets.bottom + 5 } + + override func viewClass() -> AnyClass { return GeneralInputRowView.self } @@ -92,18 +99,19 @@ class GeneralInputRowView: TableRowView,TGModernGrowingDelegate, NSTextFieldDele let textView:TGModernGrowingTextView - private let secureField: NSSecureTextField = NSSecureTextField(frame: NSMakeRect(0, 0, 100, 16)) private let cleanImage: ImageButton = ImageButton() + let separator: View = View() + required init(frame frameRect: NSRect) { - textView = TGModernGrowingTextView(frame: frameRect) + textView = TGModernGrowingTextView(frame: NSMakeRect(25, 0, frameRect.width - 50, frameRect.height)) super.init(frame: frameRect) textView.delegate = self textView.textFont = .normal(.text) - textView.min_height = 16 - + //textView.min_height = 16 + textView.max_height = 1500 secureField.isBordered = false secureField.isBezeled = false secureField.focusRingType = .none @@ -111,14 +119,15 @@ class GeneralInputRowView: TableRowView,TGModernGrowingDelegate, NSTextFieldDele secureField.drawsBackground = true secureField.isEditable = true secureField.isSelectable = true - // secureField.wantsLayer = true + secureField.font = .normal(.text) secureField.textView?.insertionPointColor = theme.colors.text secureField.sizeToFit() addSubview(cleanImage) - + addSubview(separator) + cleanImage.set(handler: { [weak self] _ in self?.textView.setString("") self?.secureField.stringValue = "" @@ -126,7 +135,16 @@ class GeneralInputRowView: TableRowView,TGModernGrowingDelegate, NSTextFieldDele } - override func controlTextDidChange(_ obj: Notification) { + override func shakeView() { + if !secureField.isHidden { + secureField.shake() + } + if !textView.isHidden { + textView.shake() + } + } + + func controlTextDidChange(_ obj: Notification) { if let item = item as? GeneralInputRowItem { let string = secureField.stringValue let updated = item.textFilter(string) @@ -147,19 +165,14 @@ class GeneralInputRowView: TableRowView,TGModernGrowingDelegate, NSTextFieldDele super.layout() if let item = item as? GeneralInputRowItem { textView.frame = NSMakeRect(item.insets.left, item.insets.top, frame.width - item.insets.left - item.insets.right,textView.frame.height) - secureField.frame = NSMakeRect(item.insets.left, item.insets.top, frame.width - item.insets.left - item.insets.right, secureField.frame.height) - cleanImage.centerY(x: frame.width - item.insets.right - cleanImage.frame.width) + separator.frame = NSMakeRect(item.insets.left + 2, frame.height - .borderSize, frame.width - item.insets.left - item.insets.right, .borderSize) } } override func draw(_ layer: CALayer, in ctx: CGContext) { - super.draw(layer, in: ctx) - if let item = item as? GeneralInputRowItem { - ctx.setFillColor(theme.colors.border.cgColor) - ctx.fill(NSMakeRect(item.insets.left, frame.height - .borderSize, frame.width - item.insets.left - item.insets.right, .borderSize)) - } + } override func viewDidMoveToWindow() { @@ -167,19 +180,22 @@ class GeneralInputRowView: TableRowView,TGModernGrowingDelegate, NSTextFieldDele _ = becomeFirstResponder() } + override func updateColors() { + super.updateColors() + textView.setBackgroundColor(theme.colors.background) + separator.backgroundColor = theme.colors.border + } + override func set(item: TableRowItem, animated: Bool) { super.set(item: item, animated:animated) - textView.textColor = theme.colors.text - textView.linkColor = theme.colors.link - - secureField.textColor = theme.colors.text - secureField.backgroundColor = backdorColor + textView.animates = false if let item = item as? GeneralInputRowItem { + cleanImage.set(image: theme.icons.recentDismiss, for: .Normal) - cleanImage.sizeToFit() + _ = cleanImage.sizeToFit() cleanImage.isHidden = (!item.canFastClean || (item.holdText && item.text.isEmpty)) @@ -188,16 +204,18 @@ class GeneralInputRowView: TableRowView,TGModernGrowingDelegate, NSTextFieldDele secureField.removeFromSuperview() - addSubview(textView, positioned: .below, relativeTo: cleanImage) + if textView.superview == nil { + addSubview(textView, positioned: .below, relativeTo: cleanImage) + } // secureField.isHidden = true // textView.isHidden = false if item.holdText { - textView.defaultText = item.placeholder.string - // if item.text != textView.string() { - textView.setString(item.text, animated: false) + //if item.text != textView.string().replacingOccurrences(of: item.placeholder.string, with: "") { + // textView.defaultText = item.placeholder.string + textView.setString(item.text, animated: false) // } } else { if textView.placeholderAttributedString == nil || !textView.placeholderAttributedString!.isEqual(to: item.placeholder) { @@ -221,9 +239,14 @@ class GeneralInputRowView: TableRowView,TGModernGrowingDelegate, NSTextFieldDele } + textView.textFont = item.font + textView.textColor = theme.colors.text + textView.linkColor = theme.colors.link + secureField.textColor = theme.colors.text + secureField.backgroundColor = backdorColor - } + textView.animates = true needsLayout = true } @@ -232,7 +255,7 @@ class GeneralInputRowView: TableRowView,TGModernGrowingDelegate, NSTextFieldDele window?.makeFirstResponder(firstResponder) } - public func maxCharactersLimit() -> Int32 { + public func maxCharactersLimit(_ textView: TGModernGrowingTextView!) -> Int32 { if let item = item as? GeneralInputRowItem { return item.limit } @@ -245,11 +268,14 @@ class GeneralInputRowView: TableRowView,TGModernGrowingDelegate, NSTextFieldDele item._height = height table.noteHeightOfRow(item.index,animated) + + separator.change(pos: NSMakePoint(separator.frame.minX, frame.height - .borderSize), animated: animated) + } } - func textViewSize() -> NSSize { + func textViewSize(_ textView: TGModernGrowingTextView!) -> NSSize { return textView.frame.size } @@ -284,6 +310,10 @@ class GeneralInputRowView: TableRowView,TGModernGrowingDelegate, NSTextFieldDele } + func textViewDidReachedLimit(_ textView: Any) { + self.textView.shake() + } + func textViewDidPaste(_ pasteboard: NSPasteboard) -> Bool { if let item = item as? GeneralInputRowItem, let pasteFilter = item.pasteFilter { if let string = pasteboard.string(forType: .string) { @@ -319,7 +349,9 @@ class GeneralInputRowView: TableRowView,TGModernGrowingDelegate, NSTextFieldDele } override func becomeFirstResponder() -> Bool { - window?.makeFirstResponder(firstResponder) + if let item = item as? GeneralInputRowItem, item.automaticallyBecomeResponder { + window?.makeFirstResponder(firstResponder) + } return true } diff --git a/Telegram-Mac/GeneralInteractedRowItem.swift b/Telegram-Mac/GeneralInteractedRowItem.swift index 2753a45238..0a4df5f7a6 100644 --- a/Telegram-Mac/GeneralInteractedRowItem.swift +++ b/Telegram-Mac/GeneralInteractedRowItem.swift @@ -8,12 +8,18 @@ import Cocoa import TGUIKit - +import SwiftSignalKit struct GeneralThumbAdditional { let thumb:CGImage let textInset:CGFloat? + let thumbInset: CGFloat? + init(thumb: CGImage, textInset: CGFloat? = nil, thumbInset: CGFloat? = nil) { + self.thumb = thumb + self.textInset = textInset + self.thumbInset = thumbInset + } } @@ -25,28 +31,57 @@ class GeneralInteractedRowItem: GeneralRowItem { var descLayout:TextViewLayout? var nameStyle:ControlStyle let thumb:GeneralThumbAdditional? + let activeThumb:GeneralThumbAdditional? let switchAppearance: SwitchViewAppearance + let autoswitch: Bool + + let disabledAction:()->Void + var nameWidth:CGFloat { - var width = self.size.width - (inset.left + inset.right) - switch type { - case .switchable: - width -= 40 - case .context: - width -= 40 - default: - break + switch self.viewType { + case .legacy: + var width = self.size.width - (inset.left + inset.right) + switch type { + case .switchable: + width -= 40 + case .context: + width -= 40 + case .selectable: + width -= 40 + default: + break + } + if let thumb = thumb { + width -= thumb.thumb.backingSize.width + 20 + } + return width + case let .modern(_, insets): + var width = self.blockWidth - (insets.left + insets.right) + switch type { + case .switchable: + width -= 40 + case .context: + width -= 40 + case .selectable: + width -= 40 + default: + break + } + if let thumb = thumb { + width -= thumb.thumb.backingSize.width + 20 + } + return width } - if let thumb = thumb { - width -= thumb.thumb.backingSize.width + 20 - } - return width } - - init(_ initialSize:NSSize, stableId:AnyHashable = arc4random(), name:String, icon: CGImage? = nil, nameStyle:ControlStyle = ControlStyle(font: NSFont.normal(.title), foregroundColor: theme.colors.text), description: String? = nil, type:GeneralInteractedType = .none, action:@escaping ()->Void = {}, drawCustomSeparator:Bool = true, thumb:GeneralThumbAdditional? = nil, border:BorderType = [], inset: NSEdgeInsets = NSEdgeInsets(left: 30.0, right: 30.0), enabled: Bool = true, switchAppearance: SwitchViewAppearance = switchViewAppearance) { + private let menuItems:(()->[ContextMenuItem])? + let disableBorder: Bool + init(_ initialSize:NSSize, stableId:AnyHashable = arc4random(), name:String, icon: CGImage? = nil, activeIcon: CGImage? = nil, nameStyle:ControlStyle = ControlStyle(font: .normal(.title), foregroundColor: theme.colors.text), description: String? = nil, descTextColor: NSColor = theme.colors.grayText, type:GeneralInteractedType = .none, viewType: GeneralViewType = .legacy, action:@escaping ()->Void = {}, drawCustomSeparator:Bool = true, thumb:GeneralThumbAdditional? = nil, border:BorderType = [], inset: NSEdgeInsets = NSEdgeInsets(left: 30.0, right: 30.0), enabled: Bool = true, switchAppearance: SwitchViewAppearance = switchViewAppearance, error: InputDataValueError? = nil, autoswitch: Bool = true, disabledAction: @escaping()-> Void = {}, menuItems:(()->[ContextMenuItem])? = nil, customTheme: GeneralRowItem.Theme? = nil, disableBorder: Bool = false) { self.name = name + self.menuItems = menuItems + self.disableBorder = disableBorder if let description = description { - descLayout = TextViewLayout(.initialize(string: description, color: theme.colors.grayText, font: .normal(.text))) + descLayout = TextViewLayout(.initialize(string: description, color: descTextColor, font: .normal(.text))) } else { descLayout = nil } @@ -56,24 +91,41 @@ class GeneralInteractedRowItem: GeneralRowItem { } else { self.thumb = thumb } - self.switchAppearance = switchAppearance - super.init(initialSize, stableId:stableId, type:type, action:action, drawCustomSeparator:drawCustomSeparator, border:border, inset:inset, enabled: enabled) + self.disabledAction = disabledAction + self.autoswitch = autoswitch + self.activeThumb = activeIcon != nil ? GeneralThumbAdditional(thumb: activeIcon!, textInset: nil) : self.thumb + self.switchAppearance = customTheme?.switchAppearance ?? switchAppearance + super.init(initialSize, height: 0, stableId:stableId, type:type, viewType: viewType, action:action, drawCustomSeparator:drawCustomSeparator, border:border, inset:inset, enabled: enabled, error: error, customTheme: customTheme) + _ = makeSize(initialSize.width, oldWidth: 0) } override var height: CGFloat { - if let descLayout = descLayout { - return super.height + descLayout.layoutSize.height + 5 + + switch viewType { + case .legacy: + let height: CGFloat = super.height + 40 + if let descLayout = descLayout { + return height + descLayout.layoutSize.height + } + return height + case let .modern(_, insets): + let height: CGFloat = super.height + insets.top + insets.bottom + nameLayout!.0.size.height + if let descLayout = self.descLayout { + return height + descLayout.layoutSize.height + 2 + } + return height } - return super.height } + + override func makeSize(_ width: CGFloat, oldWidth:CGFloat) -> Bool { - - nameLayout = TextNode.layoutText(maybeNode: nil, NSAttributedString.initialize(string: name, color: nameStyle.foregroundColor, font: nameStyle.font), nil, 1, .end, NSMakeSize(nameWidth, self.size.height), nil, isSelected, .left) - nameLayoutSelected = TextNode.layoutText(maybeNode: nil, NSAttributedString.initialize(string: name, color: .white, font: nameStyle.font), nil, 1, .end, NSMakeSize(nameWidth, self.size.height), nil, isSelected, .left) + let result = super.makeSize(width, oldWidth: oldWidth) + nameLayout = TextNode.layoutText(maybeNode: nil, NSAttributedString.initialize(string: name, color: enabled ? nameStyle.foregroundColor : theme.colors.grayText, font: nameStyle.font), nil, 1, .end, NSMakeSize(nameWidth, .greatestFiniteMagnitude), nil, isSelected, .left) + nameLayoutSelected = TextNode.layoutText(maybeNode: nil, NSAttributedString.initialize(string: name, color: theme.colors.underSelectedColor, font: nameStyle.font), nil, 1, .end, NSMakeSize(nameWidth, .greatestFiniteMagnitude), nil, isSelected, .left) descLayout?.measure(width: nameWidth) - return super.makeSize(width, oldWidth: oldWidth) + return result } override func prepare(_ selected: Bool) { @@ -84,4 +136,13 @@ class GeneralInteractedRowItem: GeneralRowItem { return GeneralInteractedRowView.self } + + override func menuItems(in location: NSPoint) -> Signal<[ContextMenuItem], NoError> { + + if let menuItems = self.menuItems { + return .single(menuItems()) + } else { + return super.menuItems(in: location) + } + } } diff --git a/Telegram-Mac/GeneralInteractedRowView.swift b/Telegram-Mac/GeneralInteractedRowView.swift index 219d91f990..0862b34d79 100644 --- a/Telegram-Mac/GeneralInteractedRowView.swift +++ b/Telegram-Mac/GeneralInteractedRowView.swift @@ -11,37 +11,32 @@ import TGUIKit class GeneralInteractedRowView: GeneralRowView { - + + private let containerView: GeneralRowContainerView = GeneralRowContainerView(frame: NSZeroRect) private(set) var switchView:SwitchView? + private(set) var progressView: ProgressIndicator? private(set) var textView:TextView? - private(set) var overlay:OverlayControl = OverlayControl() private(set) var descriptionView: TextView? private var nextView:ImageView = ImageView() + + + override func set(item:TableRowItem, animated:Bool = false) { - overlay.removeAllHandlers() - - nextView.image = theme.icons.generalNext - overlay.animates = false - // overlay.set(handler: { [weak self] control in - // if let strongSelf = self { - // self?.textView?.backgroundColor = strongSelf.isSelect ? strongSelf.backdorColor : .clear - // } - // }, for: .Highlight) - - // overlay.set(handler: { [weak self] control in - // if let strongSelf = self { - // self?.textView?.backgroundColor = strongSelf.backdorColor - // } - // }, for: .Normal) - // + + if let item = item as? GeneralInteractedRowItem { + nextView.image = theme.icons.generalNext + if let descLayout = item.descLayout { if descriptionView == nil { descriptionView = TextView() - addSubview(descriptionView!) + descriptionView?.userInteractionEnabled = false + descriptionView?.isSelectable = false + descriptionView?.isEventLess = true + containerView.addSubview(descriptionView!) } descriptionView?.update(descLayout) } else { @@ -53,176 +48,417 @@ class GeneralInteractedRowView: GeneralRowView { if case let .switchable(stateback) = item.type { if switchView == nil { switchView = SwitchView(frame: NSMakeRect(0, 0, 32, 20)) - addSubview(switchView!) + containerView.addSubview(switchView!) } + switchView?.autoswitch = item.autoswitch switchView?.presentation = item.switchAppearance - switchView?.setIsOn(stateback(),animated:animated) + switchView?.setIsOn(stateback,animated:animated) switchView?.stateChanged = item.action switchView?.userInteractionEnabled = item.enabled + switchView?.isEnabled = item.enabled } else { switchView?.removeFromSuperview() switchView = nil } if case let .image(stateback) = item.type { - nextView.image = stateback() + nextView.image = stateback nextView.sizeToFit() - nextView.isHidden = false + nextView.isHidden = item.isSelected } - if case let .context(value) = item.type { + switch item.type { + case let .context(value), let .nextContext(value), let .contextSelector(value, _): if textView == nil { textView = TextView() textView?.animates = false textView?.userInteractionEnabled = false - addSubview(textView!) + textView?.isEventLess = true + containerView.addSubview(textView!) } - let layout = TextViewLayout(.initialize(string: value(), color: isSelect ? .white : theme.colors.grayText, font: .normal(.title)), maximumNumberOfLines: 1) + let layout = item.isSelected ? nil : TextViewLayout(.initialize(string: value, color: isSelect ? theme.colors.underSelectedColor : theme.colors.grayText, font: .normal(.title)), maximumNumberOfLines: 1) textView?.set(layout: layout) - - nextView.isHidden = false - } else { + var nextVisible: Bool = true + if case let .contextSelector(_, items) = item.type { + nextVisible = !items.isEmpty + } + nextView.isHidden = !nextVisible + default: textView?.removeFromSuperview() textView = nil } - - textView?.backgroundColor = theme.colors.background - - if item.enabled { - overlay.set(handler:{ _ in - item.action() - }, for: .SingleClick) - } - + textView?.backgroundColor = backdorColor if case let .selectable(value) = item.type { - nextView.isHidden = !value() - nextView.image = theme.icons.generalCheck + nextView.isHidden = !value + nextView.image = #imageLiteral(resourceName: "Icon_Check").precomposed(item.customTheme?.accentColor ?? theme.colors.accent) nextView.sizeToFit() } + var needNextImage: Bool = false + if case .colorSelector = item.type { + needNextImage = true + } if case .next = item.type { + needNextImage = true + } + if case .nextContext = item.type { + needNextImage = true + } + if case let .contextSelector(_, items) = item.type { + needNextImage = !items.isEmpty + } + if needNextImage { nextView.isHidden = false - nextView.image = theme.icons.generalNext + nextView.image = item.isSelected ? nil : theme.icons.generalNext nextView.sizeToFit() - + } + switch item.viewType { + case .legacy: + containerView.setCorners([], animated: false) + case .modern: + containerView.setCorners(self.isResorting ? GeneralViewItemCorners.all : item.viewType.corners, animated: animated) } - + switch item.type { + case .loading: + if progressView == nil { + self.progressView = ProgressIndicator(frame: NSMakeRect(0, 0, 20, 20)) + containerView.addSubview(self.progressView!) + } + default: + self.progressView?.removeFromSuperview() + self.progressView = nil + } + + } super.set(item: item, animated: animated) - self.needsLayout = true + + + containerView.needsLayout = true + containerView.needsDisplay = true + } + + override func updateIsResorting() { + if let item = self.item { + self.set(item: item, animated: true) + } } override var backdorColor: NSColor { - return isSelect ? theme.colors.blueSelect : theme.colors.background + guard let item = item as? GeneralInteractedRowItem else { + return super.backdorColor + } + if let theme = item.customTheme { + return theme.backgroundColor + } + return isSelect ? theme.colors.accentSelect : theme.colors.background } - override func mouseDown(with event: NSEvent) { - + var highlightColor: NSColor { + guard let item = item as? GeneralInteractedRowItem else { + return super.backdorColor + } + if let theme = item.customTheme { + return theme.highlightColor + } + return theme.colors.grayHighlight + } + + var borderColor: NSColor { + guard let item = item as? GeneralInteractedRowItem else { + return theme.colors.border + } + if item.disableBorder { + return .clear + } + if let theme = item.customTheme { + return theme.borderColor + } + return theme.colors.border } override func updateColors() { - super.updateColors() - descriptionView?.backgroundColor = backdorColor - textView?.backgroundColor = backdorColor - - if isSelect { - // overlay.set(background: .clear, for: .Highlight) - } else { - //overlay.set(background: theme.colors.grayTransparent, for: .Highlight) - + if let item = item as? GeneralInteractedRowItem { + self.background = item.viewType.rowBackground + let highlighted = isSelect ? self.backdorColor : highlightColor + descriptionView?.backgroundColor = containerView.controlState == .Highlight && !isSelect ? .clear : self.backdorColor + textView?.backgroundColor = containerView.controlState == .Highlight && !isSelect ? .clear : self.backdorColor + containerView.set(background: self.backdorColor, for: .Normal) + containerView.set(background: highlighted, for: .Highlight) + progressView?.progressColor = item.customTheme?.secondaryColor ?? theme.colors.grayIcon } - // overlay.set(background: backdorColor, for: .Hover) - + containerView.needsDisplay = true + } + + override func shakeView() { + self.shake() + } + + private var textXAdditional: CGFloat { + var textXAdditional:CGFloat = 0 + guard let item = item as? GeneralInteractedRowItem else {return 0} + let t = item.isSelected ? item.activeThumb : item.thumb + if let thumb = t { + if let textInset = thumb.textInset { + textXAdditional = textInset + } else { + textXAdditional = thumb.thumb.backingSize.width + 10 + } + } + return textXAdditional } override func draw(_ layer: CALayer, in ctx: CGContext) { - - super.draw(layer, in: ctx) - - if let item = item as? GeneralInteractedRowItem { - + + if let item = item as? GeneralInteractedRowItem, layer == containerView.layer { - var textXAdditional:CGFloat = 0 - if let thumb = item.thumb { - let f = focus(thumb.thumb.backingSize) - let icon = isSelect ? ControlStyle(highlightColor: .white).highlight(image: thumb.thumb) : thumb.thumb - ctx.draw(icon, in: NSMakeRect(item.inset.left, f.minY, f.width, f.height)) - if let textInset = thumb.textInset { - textXAdditional = textInset - } else { - textXAdditional = thumb.thumb.backingSize.width + 10 + switch item.viewType { + case .legacy: + super.draw(layer, in: ctx) + let t = item.isSelected ? item.activeThumb : item.thumb + if let thumb = t { + var f = focus(thumb.thumb.backingSize) + if item.descLayout != nil { + f.origin.y = 11 + } + let icon = thumb.thumb //isSelect ? ControlStyle(highlightColor: .white).highlight(image: thumb.thumb) : + ctx.draw(icon, in: NSMakeRect(item.inset.left, f.minY, f.width, f.height)) } - } - - if item.drawCustomSeparator, !isSelect { - ctx.setFillColor(theme.colors.border.cgColor) - ctx.fill(NSMakeRect(textXAdditional + item.inset.left, frame.height - .borderSize, frame.width - (item.inset.left + item.inset.right + textXAdditional), .borderSize)) - } - - if let nameLayout = (item.isSelected ? item.nameLayoutSelected : item.nameLayout) { - var textRect = focus(NSMakeSize(nameLayout.0.size.width,nameLayout.0.size.height)) - textRect.origin.x = item.inset.left + textXAdditional - textRect.origin.y -= 1 - if item.descLayout != nil { - textRect.origin.y = floorToScreenPixels(frame.height/2) - nameLayout.0.size.height - 2 + + if item.drawCustomSeparator, !isSelect && !self.isResorting && containerView.controlState != .Highlight { + ctx.setFillColor(borderColor.cgColor) + ctx.fill(NSMakeRect(textXAdditional + item.inset.left, frame.height - .borderSize, frame.width - (item.inset.left + item.inset.right + textXAdditional), .borderSize)) + } + + if let nameLayout = (item.isSelected ? item.nameLayoutSelected : item.nameLayout) { + var textRect = focus(NSMakeSize(nameLayout.0.size.width,nameLayout.0.size.height)) + textRect.origin.x = item.inset.left + textXAdditional + textRect.origin.y -= 2 + if item.descLayout != nil { + textRect.origin.y = 10 + } + nameLayout.1.draw(textRect, in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) + } + + if case let .colorSelector(stateback) = item.type { + ctx.setFillColor(stateback.cgColor) + ctx.fillEllipse(in: NSMakeRect(frame.width - 14 - item.inset.right - 16, floorToScreenPixels(backingScaleFactor, (frame.height - 14) / 2), 14, 14)) + } + case let .modern(position, insets): + let t = item.isSelected ? item.activeThumb : item.thumb + if let thumb = t { + var f = focus(thumb.thumb.backingSize) + if item.descLayout != nil { + f.origin.y = insets.top + } + let icon = thumb.thumb + ctx.draw(icon, in: NSMakeRect(insets.left + (thumb.thumbInset ?? 0), f.minY, f.width, f.height)) + } + + if position.border, !isSelect && !self.isResorting { + ctx.setFillColor(borderColor.cgColor) + ctx.fill(NSMakeRect(textXAdditional + insets.left, containerView.frame.height - .borderSize, containerView.frame.width - (insets.left + insets.right + textXAdditional), .borderSize)) + } + + if let nameLayout = (item.isSelected ? item.nameLayoutSelected : item.nameLayout) { + var textRect = focus(NSMakeSize(nameLayout.0.size.width,nameLayout.0.size.height)) + textRect.origin.x = insets.left + textXAdditional + textRect.origin.y = insets.top - 1 + + nameLayout.1.draw(textRect, in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) } - nameLayout.1.draw(textRect, in: ctx, backingScaleFactor: backingScaleFactor) + if case let .colorSelector(stateback) = item.type { + ctx.setFillColor(stateback.cgColor) + ctx.fillEllipse(in: NSMakeRect(containerView.frame.width - 14 - insets.right, floorToScreenPixels(backingScaleFactor, (containerView.frame.height - 14) / 2), 14, 14)) + } } + + } } + + required init(frame frameRect: NSRect) { super.init(frame: frameRect) - addSubview(overlay) nextView.sizeToFit() - addSubview(nextView) + containerView.addSubview(nextView) + self.containerView.displayDelegate = self + self.addSubview(self.containerView) + + containerView.set(handler: { [weak self] _ in + self?.updateColors() + }, for: .Highlight) + containerView.set(handler: { [weak self] _ in + self?.updateColors() + }, for: .Normal) + containerView.set(handler: { [weak self] _ in + self?.updateColors() + }, for: .Hover) + + containerView.set(handler: { [weak self] _ in + self?.invokeIfNeededDown() + }, for: .Down) + + containerView.set(handler: { [weak self] _ in + if let event = NSApp.currentEvent { + self?.mouseDragged(with: event) + } + }, for: .MouseDragging) + + containerView.set(handler: { [weak self] _ in + self?.invokeIfNeededUp() + }, for: .Up) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + func invokeAction(_ item: GeneralInteractedRowItem) { + if item.enabled { + if let textView = self.textView { + switch item.type { + case let .contextSelector(value, items): + if let event = NSApp.currentEvent { + let menu = NSMenu() + if let customTheme = item.customTheme { + menu.appearance = customTheme.appearance + } + let items = items.map{ pItem -> ContextMenuItem in + return ContextMenuItem(pItem.title, handler: pItem.handler, dynamicTitle: nil, state: value == pItem.title ? .on : nil) + } + for item in items { + menu.addItem(item) + } + NSMenu.popUpContextMenu(menu, with: event, for: textView) + } else { + showPopover(for: textView, with: SPopoverViewController(items: items), edge: .minX, inset: NSMakePoint(0,-30)) + } + + + return + default: + break + } + } + + switch item.type { + case let .switchable(enabled): + if item.autoswitch { + item.type = .switchable(!enabled) + self.switchView?.send(event: .Click) + return + } + default: + break + } + item.action() + } else { + item.disabledAction() + } + } + private func invokeIfNeededUp() { + if let event = NSApp.currentEvent { + guard let item = item as? GeneralInteractedRowItem else { + return + } + if case .contextSelector = item.type { + + } else if let table = item.table, table.alwaysOpenRowsOnMouseUp, containerView.mouseInside() { + invokeAction(item) + } else { + super.mouseUp(with: event) + } + } + + } + private func invokeIfNeededDown() { + if let event = NSApp.currentEvent { + guard let item = item as? GeneralInteractedRowItem else { + return + } + if case .contextSelector = item.type { + invokeAction(item) + } else if let table = item.table, !table.alwaysOpenRowsOnMouseUp, containerView.mouseInside() { + invokeAction(item) + } else { + super.mouseDown(with: event) + } + } + } override func layout() { super.layout() if let item = item as? GeneralInteractedRowItem { - let inset = general?.inset ?? NSEdgeInsetsZero - self.overlay.frame = NSMakeRect(inset.left, 0, frame.width - inset.left - inset.right, frame.height) - - if let descriptionView = descriptionView { - descriptionView.setFrameOrigin(inset.left, floorToScreenPixels(frame.height / 2) + 2) - } - - let nextInset = nextView.isHidden ? 0 : nextView.frame.width + 6 + (inset.right == 0 ? 10 : 0) + let insets = item.inset - if let switchView = switchView { - switchView.centerY(x:frame.width - inset.right - switchView.frame.width - nextInset) - } - if let textView = textView { - var width:CGFloat = 100 - if let name = item.nameLayout { - width = frame.width - name.0.size.width - nextInset - inset.right - inset.left - 10 + switch item.viewType { + case .legacy: + self.containerView.frame = bounds + self.containerView.setCorners([]) + if let descriptionView = descriptionView { + descriptionView.setFrameOrigin(insets.left + textXAdditional, floorToScreenPixels(backingScaleFactor, frame.height - descriptionView.frame.height - 6)) } + let nextInset = nextView.isHidden ? 0 : nextView.frame.width + 6 + (insets.right == 0 ? 10 : 0) + if let switchView = switchView { + switchView.centerY(x:frame.width - insets.right - switchView.frame.width - nextInset, addition: -1) + } + if let textView = textView { + var width:CGFloat = 100 + if let name = item.nameLayout { + width = frame.width - name.0.size.width - nextInset - insets.right - insets.left - 10 + } + textView.layout?.measure(width: width) + textView.update(textView.layout) + textView.centerY(x:frame.width - insets.right - textView.frame.width - nextInset, addition: -1) + if !nextView.isHidden { + textView.setFrameOrigin(textView.frame.minX,textView.frame.minY - 1) + } + } + nextView.centerY(x: frame.width - (insets.right == 0 ? 10 : insets.right) - nextView.frame.width) + if let progressView = progressView { + progressView.centerY(x: frame.width - (insets.right == 0 ? 10 : insets.right) - progressView.frame.width, addition: -1) + } + case let .modern(_, innerInsets): + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), insets.top, item.blockWidth, frame.height - insets.bottom - insets.top) + self.containerView.setCorners(self.isResorting ? GeneralViewItemCorners.all : item.viewType.corners) + if let descriptionView = self.descriptionView { + descriptionView.setFrameOrigin(innerInsets.left + textXAdditional, containerView.frame.height - descriptionView.frame.height - innerInsets.bottom) + } + let nextInset = nextView.isHidden ? 0 : nextView.frame.width + 6 - textView.layout?.measure(width: width) - textView.update(textView.layout) - textView.centerY(x:frame.width - inset.right - textView.frame.width - nextInset) - if !nextView.isHidden { - textView.setFrameOrigin(textView.frame.minX,textView.frame.minY - 1) + if let switchView = switchView { + switchView.centerY(x: containerView.frame.width - innerInsets.right - switchView.frame.width - nextInset, addition: -1) + } + if let textView = textView { + var width:CGFloat = 100 + if let name = item.nameLayout { + width = containerView.frame.width - name.0.size.width - innerInsets.right - insets.left - 10 + } + textView.layout?.measure(width: width) + textView.update(textView.layout) + textView.centerY(x: containerView.frame.width - innerInsets.right - textView.frame.width - nextInset) + if !nextView.isHidden { + textView.setFrameOrigin(textView.frame.minX, textView.frame.minY - 1) + } + } + nextView.centerY(x: containerView.frame.width - innerInsets.right - nextView.frame.width, addition: -1) + if let progressView = progressView { + progressView.centerY(x: containerView.frame.width - innerInsets.right - progressView.frame.width, addition: -1) } } - nextView.centerY(x: frame.width - (inset.right == 0 ? 10 : inset.right) - nextView.frame.width) } diff --git a/Telegram-Mac/GeneralLineSeparatorRowItem.swift b/Telegram-Mac/GeneralLineSeparatorRowItem.swift new file mode 100644 index 0000000000..3973c1bec6 --- /dev/null +++ b/Telegram-Mac/GeneralLineSeparatorRowItem.swift @@ -0,0 +1,27 @@ +// +// GeneralLineSeparatorRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 10/06/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + + +class GeneralLineSeparatorRowItem: GeneralRowItem { + init(initialSize: NSSize, stableId: AnyHashable, height: CGFloat = .borderSize) { + super.init(initialSize, height: height, stableId: stableId) + } + + override func viewClass() -> AnyClass { + return GeneralLineSeparatorRowView.self + } +} + +private final class GeneralLineSeparatorRowView : TableRowView { + override var backdorColor: NSColor { + return theme.colors.border + } +} diff --git a/Telegram-Mac/GeneralLoadingRowItem.swift b/Telegram-Mac/GeneralLoadingRowItem.swift new file mode 100644 index 0000000000..ba91d2d98a --- /dev/null +++ b/Telegram-Mac/GeneralLoadingRowItem.swift @@ -0,0 +1,49 @@ +// +// GeneralLoadingRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 14.01.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + + +class GeneralLoadingRowItem: GeneralRowItem { + + init(_ initialSize: NSSize, stableId: AnyHashable, viewType: GeneralViewType) { + super.init(initialSize, height: 42, stableId: stableId, viewType: viewType) + } + + override func viewClass() -> AnyClass { + return GeneralLoadingRowView.self + } + +} + +private final class GeneralLoadingRowView: GeneralContainableRowView { + private let indicator: ProgressIndicator = ProgressIndicator(frame: NSMakeRect(0, 0, 20, 20)) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(indicator) + } + + override func layout() { + super.layout() + indicator.center() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func updateColors() { + super.updateColors() + indicator.progressColor = theme.colors.text + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + } +} diff --git a/Telegram-Mac/GeneralRowItem.swift b/Telegram-Mac/GeneralRowItem.swift index b641a239e6..4998d0b056 100644 --- a/Telegram-Mac/GeneralRowItem.swift +++ b/Telegram-Mac/GeneralRowItem.swift @@ -9,26 +9,294 @@ import Cocoa import TGUIKit +enum InputDataValueErrorTarget : Equatable { + case data + case files +} + +struct InputDataValueError : Equatable { + let description: String + let target: InputDataValueErrorTarget + init(description: String, target: InputDataValueErrorTarget) { + self.description = description + self.target = target + } +} + +func bestGeneralViewType(_ array:[T], for item: T) -> GeneralViewType where T: AnyObject { + for _ in array { + if item === array.first && item === array.last { + return .singleItem + } else if item === array.first { + return .firstItem + } else if item === array.last { + return .lastItem + } else { + return .innerItem + } + } + return .singleItem +} + +func bestGeneralViewType(_ array:[T], for item: T) -> GeneralViewType where T: Equatable { + for _ in array { + if item == array.first && item == array.last { + return .singleItem + } else if item == array.first { + return .firstItem + } else if item == array.last { + return .lastItem + } else { + return .innerItem + } + } + return .singleItem +} +func bestGeneralViewType(_ array:[T], for i: Int) -> GeneralViewType { + if array.count <= 1 { + return .singleItem + } else if i == 0 { + return .firstItem + } else if i == array.count - 1 { + return .lastItem + } else { + return .innerItem + } +} -enum GeneralInteractedType { +enum GeneralInteractedType : Equatable { case none case next - case selectable(stateback:()->Bool) - case switchable(stateback:()->Bool) - case context(stateback:()->String) - case image(stateback:()->CGImage) - case button(stateback:()->String) - case search(stateback:(String)->Bool) + case nextContext(String) + case selectable(Bool) + case switchable(Bool) + case context(String) + case loading + case image(CGImage) + case button(String) + case search(Bool) + case colorSelector(NSColor) + #if !SHARE + case contextSelector(String, [SPopoverItem]) + #endif +} + + +final class GeneralViewItemCorners : OptionSet { + public var rawValue: Int32 + + public init() { + self.rawValue = 0 + } + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public static let topLeft = GeneralViewItemCorners(rawValue: (1 << 0)) + public static let topRight = GeneralViewItemCorners(rawValue: (1 << 1)) + public static let bottomLeft = GeneralViewItemCorners(rawValue: (1 << 2)) + public static let bottomRight = GeneralViewItemCorners(rawValue: (1 << 3)) + + static var all: GeneralViewItemCorners { + return [.topLeft, .topRight, .bottomLeft, .bottomRight] + } + +} + +enum GeneralViewItemPosition : Equatable { + case first + case last + case inner + case single + var corners: GeneralViewItemCorners { + switch self { + case .first: + return [.topLeft, .topRight] + case .inner: + return [] + case .last: + return [.bottomLeft, .bottomRight] + case .single: + return [.topLeft, .topRight, .bottomRight, .bottomLeft] + } + } + + + + var border: Bool { + guard theme.colors.listBackground != theme.colors.background else { + return true + } + switch self { + case .first, .inner: + return true + default: + return false + } + } + +} + + +enum GeneralViewType : Equatable { + case legacy + case modern(position: GeneralViewItemPosition, insets: NSEdgeInsets) + + var isPlainMode: Bool { + return theme.colors.listBackground == theme.colors.background || self == .legacy + } + var innerInset: NSEdgeInsets { + switch self { + case .legacy: + return NSEdgeInsetsMake(0, 0, 0, 0) + case let .modern(_, insets): + return insets + } + } + + var rowBackground: NSColor { + switch self { + case .legacy: + return theme.colors.background + case .modern: + return .clear + } + } + + var corners:GeneralViewItemCorners { + switch self { + case .legacy: + return [] + case let .modern(position, _): + return isPlainMode ? [] : position.corners + } + } + var hasBorder: Bool { + switch self { + case .legacy: + return false + case let .modern(position, _): + return position.border + } + } + var position: GeneralViewItemPosition { + switch self { + case .legacy: + return .single + case let .modern(position, _): + return position + } + } + + func withUpdatedInsets(_ insets: NSEdgeInsets) -> GeneralViewType { + switch self { + case .legacy: + return self + case let .modern(position, _): + return .modern(position: position, insets: insets) + } + } + + static var firstItem: GeneralViewType { + return .modern(position: .first, insets: NSEdgeInsetsMake(12, 16, 12, 16)) + } + static var innerItem: GeneralViewType { + return .modern(position: .inner, insets: NSEdgeInsetsMake(12, 16, 12, 16)) + } + static var lastItem: GeneralViewType { + return .modern(position: .last, insets: NSEdgeInsetsMake(12, 16, 12, 16)) + } + static var singleItem: GeneralViewType { + return .modern(position: .single, insets: NSEdgeInsetsMake(12, 16, 12, 16)) + } + static var textTopItem: GeneralViewType { + return .modern(position: .single, insets: NSEdgeInsetsMake(0, 16, 5, 0)) + } + static var textBottomItem: GeneralViewType { + return .modern(position: .single, insets: NSEdgeInsetsMake(5, 16, 0, 0)) + } + static var separator: GeneralViewType { + return .modern(position: .single, insets: NSEdgeInsetsMake(0, 0, 0, 0)) + } + static func plain(_ position: GeneralViewItemPosition) -> GeneralViewType { + return .modern(position: position, insets: NSEdgeInsetsMake(0, 0, 0, 0)) + } } class GeneralRowItem: TableRowItem { + + struct Theme : Equatable { + let backgroundColor: NSColor + let grayBackground: NSColor + let grayForeground: NSColor + let highlightColor: NSColor + let borderColor: NSColor + let accentColor: NSColor + let secondaryColor: NSColor + let textColor: NSColor + let grayTextColor: NSColor + let underSelectedColor: NSColor + let accentSelectColor: NSColor + let redColor: NSColor + let indicatorColor: NSColor + let appearance: NSAppearance + + let switchAppearance: SwitchViewAppearance? + + let unselectedImage: CGImage + let selectedImage: CGImage + + init(backgroundColor: NSColor = theme.colors.background, + grayBackground: NSColor = theme.colors.grayBackground, + grayForeground: NSColor = theme.colors.grayForeground, + highlightColor: NSColor = theme.colors.grayHighlight, + borderColor: NSColor = theme.colors.border, + accentColor: NSColor = theme.colors.accent, + secondaryColor: NSColor = theme.colors.grayUI, + textColor: NSColor = theme.colors.text, + grayTextColor: NSColor = theme.colors.grayText, + underSelectedColor: NSColor = theme.colors.underSelectedColor, + accentSelectColor: NSColor = theme.colors.accentSelect, + redColor: NSColor = theme.colors.redUI, + indicatorColor: NSColor = theme.colors.indicatorColor, + appearance: NSAppearance = theme.colors.appearance, + switchAppearance: SwitchViewAppearance? = nil, + unselectedImage: CGImage = theme.icons.chatToggleUnselected, + selectedImage: CGImage = theme.icons.chatToggleSelected) { + + + self.backgroundColor = backgroundColor + self.grayBackground = grayBackground + self.grayForeground = grayForeground + self.highlightColor = highlightColor + self.borderColor = borderColor + self.accentColor = accentColor + self.secondaryColor = secondaryColor + self.textColor = textColor + self.grayTextColor = grayTextColor + self.underSelectedColor = underSelectedColor + self.redColor = redColor + self.accentSelectColor = accentSelectColor + self.indicatorColor = indicatorColor + self.appearance = appearance + self.switchAppearance = switchAppearance + self.unselectedImage = unselectedImage + self.selectedImage = selectedImage + } + } + let border:BorderType let enabled: Bool let _height:CGFloat override var height: CGFloat { - return _height + var height = _height + if let errorLayout = errorLayout { + height += errorLayout.layoutSize.height + } + return height } private let _stableId:AnyHashable @@ -44,29 +312,86 @@ class GeneralRowItem: TableRowItem { } } - let inset:NSEdgeInsets + private let _inset: NSEdgeInsets + var inset:NSEdgeInsets { + return _inset + } + private(set) var action:()->Void - private(set) var type:GeneralInteractedType + var type:GeneralInteractedType + + let backgroundColor: NSColor + + let error: InputDataValueError? + let errorLayout: TextViewLayout? + + private(set) var viewType: GeneralViewType + + + func updateViewType(_ viewType: GeneralViewType) { + self.viewType = viewType + } + let customTheme: Theme? - init(_ initialSize: NSSize, height:CGFloat = 40.0, stableId:AnyHashable = arc4random(),type:GeneralInteractedType = .none, action:@escaping()->Void = {}, drawCustomSeparator:Bool = true, border:BorderType = [], inset:NSEdgeInsets = NSEdgeInsets(left: 30.0, right: 30.0), enabled: Bool = true) { + init(_ initialSize: NSSize, height:CGFloat = 40.0, stableId:AnyHashable = arc4random(),type:GeneralInteractedType = .none, viewType: GeneralViewType = .legacy, action:@escaping()->Void = {}, drawCustomSeparator:Bool = true, border:BorderType = [], inset:NSEdgeInsets = NSEdgeInsets(left: 30.0, right: 30.0), enabled: Bool = true, backgroundColor: NSColor? = nil, error: InputDataValueError? = nil, customTheme: Theme? = nil) { self.type = type _height = height _stableId = stableId self.border = border - self.inset = inset + self._inset = inset + self.customTheme = customTheme + if let backgroundColor = backgroundColor { + self.backgroundColor = backgroundColor + } else { + self.backgroundColor = viewType.rowBackground + } + self.drawCustomSeparator = drawCustomSeparator self.action = action self.enabled = enabled + self.error = error + self.viewType = viewType + if let error = error { + errorLayout = TextViewLayout(.initialize(string: error.description, color: theme.colors.redUI, font: .normal(.text))) + } else { + errorLayout = nil + } + super.init(initialSize) let _ = self.makeSize(initialSize.width) } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) + errorLayout?.measure(width: width - inset.left - inset.right) + return success + } + + var hasBorder: Bool { + return viewType.hasBorder + } + override var instantlyResize: Bool { return true } + override var canBeAnchor: Bool { + return false + } + + + var blockWidth: CGFloat { + switch self.viewType { + case .legacy: + return self.width - self.inset.left - self.inset.right + case .modern: + return min(600, self.width - self.inset.left - self.inset.right) + } + } + override func viewClass() -> AnyClass { return GeneralRowView.self } diff --git a/Telegram-Mac/GeneralRowView.swift b/Telegram-Mac/GeneralRowView.swift index 5a57dd87f6..4bd12687f0 100644 --- a/Telegram-Mac/GeneralRowView.swift +++ b/Telegram-Mac/GeneralRowView.swift @@ -8,30 +8,262 @@ import Cocoa import TGUIKit -class GeneralRowView: TableRowView,ViewDisplayDelegate { + + +class GeneralContainableRowView : TableRowView { + let containerView = GeneralRowContainerView(frame: NSZeroRect) + let borderView: View = View() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + super.addSubview(self.containerView) + containerView.addSubview(borderView) + } + + deinit { + self.containerView.removeAllSubviews() + } + + override func addSubview(_ view: NSView) { + self.containerView.addSubview(view) + } + + override func addSubview(_ view: NSView, positioned place: NSWindow.OrderingMode, relativeTo otherView: NSView?) { + self.containerView.addSubview(view, positioned: place, relativeTo: otherView) + } + + override var backdorColor: NSColor { + return theme.colors.background + } + + override var mouseDownCanMoveWindow: Bool { + return false + } + + var borderColor: NSColor { + return theme.colors.border + } + + override func updateColors() { + guard let item = item as? GeneralRowItem else { + return + } + self.backgroundColor = item.viewType.rowBackground + self.containerView.backgroundColor = backdorColor + self.borderView.backgroundColor = borderColor + } + + var maxBlockWidth: CGFloat { + return 600 + } + var maxWidth: CGFloat { + return frame.width + } + var maxHeight: CGFloat { + return frame.height + } + override func layout() { + super.layout() + guard let item = item as? GeneralRowItem else { + return + } + let blockWidth = min(maxBlockWidth, frame.width - item.inset.left - item.inset.right) + + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (maxWidth - blockWidth) / 2), item.inset.top, blockWidth, maxHeight - item.inset.bottom - item.inset.top) + self.containerView.setCorners(item.viewType.corners) + + + + borderView.frame = NSMakeRect(item.viewType.innerInset.left + additionBorderInset, containerView.frame.height - .borderSize, containerView.frame.width - item.viewType.innerInset.left - item.viewType.innerInset.right - additionBorderInset, .borderSize) + } + + var additionBorderInset: CGFloat { + return 0 + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? GeneralRowItem else { + return + } + + borderView.isHidden = !item.hasBorder + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} +class GeneralRowContainerView : Control { + var cornerRadius: CGFloat = 10 + + private let maskLayer = CAShapeLayer() + private var newPath: CGPath? + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + layer?.mask = maskLayer + self.maskLayer.disableActions() + } + + private var corners: GeneralViewItemCorners? = nil + func setCorners(_ corners: GeneralViewItemCorners, animated: Bool = false, frame: NSRect? = nil) { + if animated && self.corners != nil { + let newPath = self.createMask(for: corners, frame: frame ?? self.frame) + + var oldPath: CGPath = self.maskLayer.path ?? CGMutablePath() + + if let presentation = self.maskLayer.presentation(), let _ = self.maskLayer.animation(forKey:"path") { + oldPath = presentation.path ?? oldPath + if newPath == self.newPath { + self.corners = corners + return + } + } + self.newPath = newPath + + self.maskLayer.animate(from: oldPath, to: newPath, keyPath: "path", timingFunction: .easeOut, duration: 0.18, removeOnCompletion: false, additive: false, completion: { [weak self] completed in + //if completed { + self?.maskLayer.removeAllAnimations() + // } + self?.maskLayer.path = newPath + }) + + } else { + self.maskLayer.path = createMask(for: corners, frame: frame ?? self.bounds) + } + self.corners = corners + } + private func createMask(for corners: GeneralViewItemCorners, frame: NSRect) -> CGPath { + let path = CGMutablePath() + + let minx:CGFloat = 0, midx = frame.width/2.0, maxx = frame.width + let miny:CGFloat = 0, midy = frame.height/2.0, maxy = frame.height + + path.move(to: NSMakePoint(minx, midy)) + + var topLeftRadius: CGFloat = 0 + var bottomLeftRadius: CGFloat = 0 + var topRightRadius: CGFloat = 0 + var bottomRightRadius: CGFloat = 0 + + + if corners.contains(.topLeft) { + bottomLeftRadius = cornerRadius + } + if corners.contains(.topRight) { + bottomRightRadius = cornerRadius + } + if corners.contains(.bottomLeft) { + topLeftRadius = cornerRadius + } + if corners.contains(.bottomRight) { + topRightRadius = cornerRadius + } + + path.addArc(tangent1End: NSMakePoint(minx, miny), tangent2End: NSMakePoint(midx, miny), radius: bottomLeftRadius) + path.addArc(tangent1End: NSMakePoint(maxx, miny), tangent2End: NSMakePoint(maxx, midy), radius: bottomRightRadius) + path.addArc(tangent1End: NSMakePoint(maxx, maxy), tangent2End: NSMakePoint(midx, maxy), radius: topRightRadius) + path.addArc(tangent1End: NSMakePoint(minx, maxy), tangent2End: NSMakePoint(minx, midy), radius: topLeftRadius) + + + return path + } + + override func setFrameSize(_ newSize: NSSize) { + super.setFrameSize(newSize) + } + + func change(size: NSSize, animated: Bool, corners: GeneralViewItemCorners) { + super._change(size: size, animated: animated, animated, duration: 0.18) + setCorners(corners, animated: animated, frame: NSMakeRect(0, 0, size.width, size.height)) + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + +class GeneralRowView: TableRowView,ViewDisplayDelegate { + + + private var errorTextView: TextView? = nil var general:GeneralRowItem? { return self.item as? GeneralRowItem } + + required init(frame frameRect: NSRect) { super.init(frame: frameRect) + layerContentsRedrawPolicy = .onSetNeedsDisplay } + override var firstResponder: NSResponder? { + return nil + } + + + override func set(item: TableRowItem, animated: Bool) { super.set(item: item, animated: animated) if let item = item as? GeneralRowItem { self.border = item.border + + let minX = (frame.width - item.blockWidth) / 2 + + + if let errorLayout = item.errorLayout { + let alphaAnimated = animated && errorTextView == nil + let posAnimated = animated && errorTextView != nil + if errorTextView == nil { + errorTextView = TextView() + errorTextView?.isSelectable = false + addSubview(errorTextView!) + } + errorTextView!.update(errorLayout) + switch item.viewType { + case .legacy: + errorTextView!.change(pos: NSMakePoint(item.inset.left, frame.height - 6 - errorLayout.layoutSize.height), animated: posAnimated) + case let .modern(_, insets): + errorTextView!.change(pos: NSMakePoint(minX + insets.left, frame.height - 2 - errorLayout.layoutSize.height), animated: posAnimated) + } + if alphaAnimated { + errorTextView!.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } else { + if let errorTextView = self.errorTextView { + if animated { + self.errorTextView = nil + errorTextView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, completion: { [weak errorTextView] _ in + errorTextView?.removeFromSuperview() + }) + } else { + errorTextView.removeFromSuperview() + self.errorTextView = nil + } + } + + } } self.needsDisplay = true + self.needsLayout = true } override func draw(_ layer: CALayer, in ctx: CGContext) { + if backingScaleFactor == 1.0 { + ctx.setFillColor(backdorColor.cgColor) + ctx.fill(layer.bounds) + } super.draw(layer, in: ctx) } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") @@ -44,8 +276,37 @@ class GeneralRowView: TableRowView,ViewDisplayDelegate { // overlay.frame = NSMakeRect(inset.left, 0, newSize.width - (inset.left + inset.right), newSize.height) } + override func layout() { + + guard let item = item as? GeneralRowItem else {return} + + let minX = (frame.width - item.blockWidth) / 2 + + if let errorTextView = errorTextView { + switch item.viewType { + case .legacy: + errorTextView.setFrameOrigin(item.inset.left, frame.height - 6 - errorTextView.frame.height) + case let .modern(_, insets): + errorTextView.setFrameOrigin(minX + insets.left, frame.height - 2 + - errorTextView.frame.height) + } + } + } + override var backdorColor: NSColor { - return theme.colors.background + + guard let item = self.item as? GeneralRowItem else { + return .clear + } + return item.backgroundColor + } + + override var mouseDownCanMoveWindow: Bool { + if self.className == GeneralRowView.className() { + return item?.table?._mouseDownCanMoveWindow ?? super.mouseDownCanMoveWindow + } else { + return false + } } } diff --git a/Telegram-Mac/GeneralSettingsViewController.swift b/Telegram-Mac/GeneralSettingsViewController.swift index 91adcac9c5..6de0335f82 100644 --- a/Telegram-Mac/GeneralSettingsViewController.swift +++ b/Telegram-Mac/GeneralSettingsViewController.swift @@ -8,51 +8,73 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac +import TelegramCore + +import SwiftSignalKit +import Postbox private enum GeneralSettingsEntry : Comparable, Identifiable { case section(sectionId:Int) case header(sectionId: Int, uniqueId:Int, text:String) - case handleInAppKeys(sectionId:Int, enabled:Bool) - case darkMode(sectionId:Int, enabled: Bool) - case fontSize(sectionId:Int, enabled: Bool) - case sidebar(sectionId:Int, enabled: Bool) - case inAppSounds(sectionId:Int, enabled: Bool) - case enterBehavior(sectionId:Int, enabled: Bool) - case cmdEnterBehavior(sectionId:Int, enabled: Bool) - case emojiReplacements(sectionId:Int, enabled: Bool) - case forceTouchReply(sectionId:Int, enabled: Bool) - case forceTouchEdit(sectionId:Int, enabled: Bool) - case forceTouchForward(sectionId:Int, enabled: Bool) + case sidebar(sectionId:Int, enabled: Bool, viewType: GeneralViewType) + case inAppSounds(sectionId:Int, enabled: Bool, viewType: GeneralViewType) + case shortcuts(sectionId: Int, viewType: GeneralViewType) + case enterBehavior(sectionId:Int, enabled: Bool, viewType: GeneralViewType) + case cmdEnterBehavior(sectionId:Int, enabled: Bool, viewType: GeneralViewType) + case emojiReplacements(sectionId:Int, enabled: Bool, viewType: GeneralViewType) + case predictEmoji(sectionId:Int, enabled: Bool, viewType: GeneralViewType) + case bigEmoji(sectionId:Int, enabled: Bool, viewType: GeneralViewType) + case statusBar(sectionId:Int, enabled: Bool, viewType: GeneralViewType) + case showCallsTab(sectionId:Int, enabled: Bool, viewType: GeneralViewType) + case enableRFTCopy(sectionId:Int, enabled: Bool, viewType: GeneralViewType) + case openChatAtLaunch(sectionId:Int, enabled: Bool, viewType: GeneralViewType) + case acceptSecretChats(sectionId:Int, enabled: Bool, viewType: GeneralViewType) + case forceTouchReply(sectionId:Int, enabled: Bool, viewType: GeneralViewType) + case forceTouchEdit(sectionId:Int, enabled: Bool, viewType: GeneralViewType) + case forceTouchForward(sectionId:Int, enabled: Bool, viewType: GeneralViewType) + case forceTouchPreviewMedia(sectionId:Int, enabled: Bool, viewType: GeneralViewType) + case callSettings(sectionId:Int, enabled: Bool, viewType: GeneralViewType) var stableId: Int { switch self { case let .header(_, uniqueId, _): return uniqueId - case .darkMode: - return 0 - case .fontSize: + case .sidebar: return 1 - case .enterBehavior: + case .emojiReplacements: return 2 - case .cmdEnterBehavior: + case .predictEmoji: return 3 - case .handleInAppKeys: + case .bigEmoji: return 4 - case .sidebar: + case .showCallsTab: return 5 - case .inAppSounds: + case .statusBar: return 6 - case .emojiReplacements: + case .inAppSounds: return 7 - case .forceTouchReply: + case .shortcuts: return 8 - case .forceTouchEdit: + case .enableRFTCopy: return 9 - case .forceTouchForward: + case .openChatAtLaunch: return 10 + case .acceptSecretChats: + return 11 + case .forceTouchReply: + return 12 + case .forceTouchEdit: + return 13 + case .forceTouchForward: + return 14 + case .forceTouchPreviewMedia: + return 15 + case .enterBehavior: + return 16 + case .cmdEnterBehavior: + return 17 + case .callSettings: + return 18 case let .section(id): return (id + 1) * 1000 - id } @@ -62,27 +84,41 @@ private enum GeneralSettingsEntry : Comparable, Identifiable { switch self { case let .header(sectionId, _, _): return (sectionId * 1000) + stableId - case let .fontSize(sectionId, _): + case let .showCallsTab(sectionId, _, _): + return (sectionId * 1000) + stableId + case let .enableRFTCopy(sectionId, _, _): + return (sectionId * 1000) + stableId + case let .openChatAtLaunch(sectionId, _, _): + return (sectionId * 1000) + stableId + case let .acceptSecretChats(sectionId, _, _): + return (sectionId * 1000) + stableId + case let .sidebar(sectionId, _, _): + return (sectionId * 1000) + stableId + case let .inAppSounds(sectionId, _, _): return (sectionId * 1000) + stableId - case let .darkMode(sectionId, _): + case let .shortcuts(sectionId, _): return (sectionId * 1000) + stableId - case let .sidebar(sectionId, _): + case let .emojiReplacements(sectionId, _, _): return (sectionId * 1000) + stableId - case let .inAppSounds(sectionId, _): + case let .predictEmoji(sectionId, _, _): return (sectionId * 1000) + stableId - case let .emojiReplacements(sectionId, _): + case let .bigEmoji(sectionId, _, _): return (sectionId * 1000) + stableId - case let .handleInAppKeys(sectionId, _): + case let .statusBar(sectionId, _, _): return (sectionId * 1000) + stableId - case let .enterBehavior(sectionId, _): + case let .enterBehavior(sectionId, _, _): return (sectionId * 1000) + stableId - case let .cmdEnterBehavior(sectionId, _): + case let .cmdEnterBehavior(sectionId, _, _): return (sectionId * 1000) + stableId - case let .forceTouchReply(sectionId, _): + case let .forceTouchReply(sectionId, _, _): return (sectionId * 1000) + stableId - case let .forceTouchEdit(sectionId, _): + case let .forceTouchEdit(sectionId, _, _): return (sectionId * 1000) + stableId - case let .forceTouchForward(sectionId, _): + case let .forceTouchForward(sectionId, _, _): + return (sectionId * 1000) + stableId + case let .forceTouchPreviewMedia(sectionId, _, _): + return (sectionId * 1000) + stableId + case let .callSettings(sectionId, _, _): return (sectionId * 1000) + stableId case let .section(id): return (id + 1) * 1000 - id @@ -92,243 +128,207 @@ private enum GeneralSettingsEntry : Comparable, Identifiable { func item(_ arguments:GeneralSettingsArguments, initialSize:NSSize) -> TableRowItem { switch self { case .section: - return GeneralRowItem(initialSize, height: 30, stableId: stableId) - case let .fontSize(sectionId: _, enabled: enabled): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.generalSettingsLargeFonts), description: tr(.generalSettingsFontDescription), type: .switchable(stateback: { () -> Bool in - return enabled - }), action: { - arguments.toggleFonts(!enabled) + return GeneralRowItem(initialSize, height: 30, stableId: stableId, viewType: .separator) + case let .header(sectionId: _, uniqueId: _, text: text): + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, drawCustomSeparator: true, inset: NSEdgeInsets(left: 30.0, right: 30.0), viewType: .textTopItem) + case let .showCallsTab(sectionId: _, enabled: enabled, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.generalSettingsShowCallsTab, type: .switchable(enabled), viewType: viewType, action: { + arguments.toggleCallsTab(!enabled) }) - case let .darkMode(sectionId: _, enabled: enabled): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.generalSettingsDarkMode), description: tr(.generalSettingsDarkModeDescription), type: .switchable(stateback: { () -> Bool in - return enabled - }), action: { - _ = updateThemeSettings(postbox: arguments.account.postbox, pallete: !enabled ? darkPallete : whitePallete, dark: !enabled).start() - + case let .enableRFTCopy(sectionId: _, enabled, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.generalSettingsCopyRTF, type: .switchable(enabled), viewType: viewType, action: { + arguments.toggleRTFEnabled(!enabled) + }) + case let .openChatAtLaunch(_, enabled, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.generalSettingsOpenLatestChatOnLaunch, type: .switchable(enabled), viewType: viewType, action: { + arguments.openChatAtLaunch(!enabled) }) - case let .handleInAppKeys(sectionId: _, enabled: enabled): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.generalSettingsMediaKeysForInAppPlayer), type: .switchable(stateback: { () -> Bool in - return enabled - }), action: { - arguments.toggleInAppKeys(!enabled) + case let .acceptSecretChats(_, enabled, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.generalSettingsAcceptSecretChats, type: .switchable(enabled), viewType: viewType, action: { + arguments.acceptSecretChats(!enabled) }) - case let .sidebar(sectionId: _, enabled: enabled): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.generalSettingsEnableSidebar), type: .switchable(stateback: { () -> Bool in - return enabled - }), action: { + case let .sidebar(sectionId: _, enabled, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.generalSettingsEnableSidebar, type: .switchable(enabled), viewType: viewType, action: { arguments.toggleSidebar(!enabled) }) - case let .inAppSounds(sectionId: _, enabled: enabled): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.generalSettingsInAppSounds), type: .switchable(stateback: { () -> Bool in - return enabled - }), action: { + case let .inAppSounds(sectionId: _, enabled, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.generalSettingsInAppSounds, type: .switchable(enabled), viewType: viewType, action: { arguments.toggleInAppSounds(!enabled) }) - case let .emojiReplacements(sectionId: _, enabled: enabled): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.generalSettingsEmojiReplacements), type: .switchable(stateback: { () -> Bool in - return enabled - }), action: { + case let .shortcuts(_, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.generalSettingsShortcuts, type: .nextContext("⌘ + ?"), viewType: viewType, action: { + arguments.openShortcuts() + }) + case let .emojiReplacements(sectionId: _, enabled, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.generalSettingsEmojiReplacements, type: .switchable(enabled), viewType: viewType, action: { arguments.toggleEmojiReplacements(!enabled) }) - case let .header(sectionId: _, uniqueId: _, text: text): - return GeneralTextRowItem(initialSize, stableId: stableId, text: text, drawCustomSeparator: true, inset: NSEdgeInsets(left: 30.0, right: 30.0, top:2, bottom:6)) - case let .enterBehavior(sectionId: _, enabled: enabled): - return GeneralInteractedRowItem(initialSize, name: tr(.generalSettingsSendByEnter), type: .selectable(stateback: { () -> Bool in - return enabled - }), action: { + case let .predictEmoji(sectionId: _, enabled, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.generalSettingsEmojiPrediction, type: .switchable(enabled), viewType: viewType, action: { + arguments.toggleEmojiPrediction(!enabled) + }) + case let .bigEmoji(sectionId: _, enabled, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.generalSettingsBigEmoji, type: .switchable(enabled), viewType: viewType, action: { + arguments.toggleBigEmoji(!enabled) + }) + case let .statusBar(sectionId: _, enabled, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.generalSettingsStatusBarItem, type: .switchable(enabled), viewType: viewType, action: { + arguments.toggleStatusBar(!enabled) + }) + case let .enterBehavior(sectionId: _, enabled, viewType): + return GeneralInteractedRowItem(initialSize, name: L10n.generalSettingsSendByEnter, type: .selectable(enabled), viewType: viewType, action: { arguments.toggleInput(.enter) }) - case let .cmdEnterBehavior(sectionId: _, enabled: enabled): - return GeneralInteractedRowItem(initialSize, name: tr(.generalSettingsSendByCmdEnter), type: .selectable(stateback: { () -> Bool in - return enabled - }), action: { + case let .cmdEnterBehavior(sectionId: _, enabled, viewType): + return GeneralInteractedRowItem(initialSize, name: L10n.generalSettingsSendByCmdEnter, type: .selectable(enabled), viewType: viewType, action: { arguments.toggleInput(.cmdEnter) }) - case let .forceTouchEdit(sectionId: _, enabled: enabled): - return GeneralInteractedRowItem(initialSize, name: tr(.generalSettingsForceTouchEdit), type: .selectable(stateback: { () -> Bool in - return enabled - }), action: { + case let .forceTouchEdit(sectionId: _, enabled, viewType): + return GeneralInteractedRowItem(initialSize, name: L10n.generalSettingsForceTouchEdit, type: .selectable(enabled), viewType: viewType, action: { arguments.toggleForceTouchAction(.edit) }) - case let .forceTouchReply(sectionId: _, enabled: enabled): - return GeneralInteractedRowItem(initialSize, name: tr(.generalSettingsForceTouchReply), type: .selectable(stateback: { () -> Bool in - return enabled - }), action: { + case let .forceTouchReply(sectionId: _, enabled, viewType): + return GeneralInteractedRowItem(initialSize, name: L10n.generalSettingsForceTouchReply, type: .selectable(enabled), viewType: viewType, action: { arguments.toggleForceTouchAction(.reply) }) - case let .forceTouchForward(sectionId: _, enabled: enabled): - return GeneralInteractedRowItem(initialSize, name: tr(.generalSettingsForceTouchForward), type: .selectable(stateback: { () -> Bool in - return enabled - }), action: { + case let .forceTouchForward(sectionId: _, enabled, viewType): + return GeneralInteractedRowItem(initialSize, name: L10n.generalSettingsForceTouchForward, type: .selectable(enabled), viewType: viewType, action: { arguments.toggleForceTouchAction(.forward) }) - } - } - -} - -private func ==(lhs: GeneralSettingsEntry, rhs: GeneralSettingsEntry) -> Bool { - switch lhs { - case let .header(sectionId, uniqueId, text): - if case .header(sectionId, uniqueId, text) = rhs { - return true - } else { - return false - } - case let .fontSize(sectionId, enabled): - if case .fontSize(sectionId, enabled) = rhs { - return true - } else { - return false - } - case let .darkMode(sectionId, enabled): - if case .darkMode(sectionId, enabled) = rhs { - return true - } else { - return false - } - case let .handleInAppKeys(sectionId, enabled): - if case .handleInAppKeys(sectionId, enabled) = rhs { - return true - } else { - return false - } - case let .sidebar(sectionId, enabled): - if case .sidebar(sectionId, enabled) = rhs { - return true - } else { - return false - } - case let .inAppSounds(sectionId, enabled): - if case .inAppSounds(sectionId, enabled) = rhs { - return true - } else { - return false - } - case let .emojiReplacements(sectionId, enabled): - if case .emojiReplacements(sectionId, enabled) = rhs { - return true - } else { - return false - } - case let .enterBehavior(sectionId, enabled): - if case .enterBehavior(sectionId, enabled) = rhs { - return true - } else { - return false - } - case let .cmdEnterBehavior(sectionId, enabled): - if case .cmdEnterBehavior(sectionId, enabled) = rhs { - return true - } else { - return false - } - case let .forceTouchReply(sectionId, enabled): - if case .forceTouchReply(sectionId, enabled) = rhs { - return true - } else { - return false - } - case let .forceTouchEdit(sectionId, enabled): - if case .forceTouchEdit(sectionId, enabled) = rhs { - return true - } else { - return false - } - case let .forceTouchForward(sectionId, enabled): - if case .forceTouchForward(sectionId, enabled) = rhs { - return true - } else { - return false - } - case let .section(sectionId): - if case .section(sectionId) = rhs { - return true - } else { - return false + case let .forceTouchPreviewMedia(sectionId: _, enabled, viewType): + return GeneralInteractedRowItem(initialSize, name: L10n.generalSettingsForceTouchPreviewMedia, type: .selectable(enabled), viewType: viewType, action: { + arguments.toggleForceTouchAction(.previewMedia) + }) + case let .callSettings(_, _, viewType): + return GeneralInteractedRowItem(initialSize, name: L10n.generalSettingsCallSettingsText, type: .next, viewType: viewType, action: { + arguments.callSettings() + }) } } } - private func <(lhs: GeneralSettingsEntry, rhs: GeneralSettingsEntry) -> Bool { return lhs.sortIndex < rhs.sortIndex } private final class GeneralSettingsArguments { - let account:Account - let toggleFonts:(Bool) -> Void + let context:AccountContext + let toggleCallsTab:(Bool) -> Void let toggleInAppKeys:(Bool) -> Void let toggleInput:(SendingType)-> Void let toggleSidebar:(Bool) -> Void let toggleInAppSounds:(Bool) -> Void let toggleEmojiReplacements:(Bool) -> Void let toggleForceTouchAction:(ForceTouchAction) -> Void - init(account:Account, toggleFonts:@escaping(Bool)-> Void, toggleInAppKeys: @escaping(Bool) -> Void, toggleInput: @escaping(SendingType)-> Void, toggleSidebar: @escaping (Bool) -> Void, toggleInAppSounds: @escaping (Bool) -> Void, toggleEmojiReplacements:@escaping(Bool) -> Void, toggleForceTouchAction: @escaping(ForceTouchAction)->Void) { - self.account = account - self.toggleFonts = toggleFonts + let toggleInstantViewScrollBySpace:(Bool) -> Void + let toggleAutoplayGifs:(Bool) -> Void + let toggleEmojiPrediction: (Bool)->Void + let toggleBigEmoji: (Bool) -> Void + let toggleStatusBar: (Bool) -> Void + let toggleRTFEnabled: (Bool) -> Void + let openChatAtLaunch:(Bool)->Void + let acceptSecretChats:(Bool)->Void + let toggleWorkMode:(Bool)->Void + let openShortcuts: ()->Void + let callSettings: ()->Void + init(context:AccountContext, toggleCallsTab:@escaping(Bool)-> Void, toggleInAppKeys: @escaping(Bool) -> Void, toggleInput: @escaping(SendingType)-> Void, toggleSidebar: @escaping (Bool) -> Void, toggleInAppSounds: @escaping (Bool) -> Void, toggleEmojiReplacements:@escaping(Bool) -> Void, toggleForceTouchAction: @escaping(ForceTouchAction)->Void, toggleInstantViewScrollBySpace: @escaping(Bool)->Void, toggleAutoplayGifs:@escaping(Bool) -> Void, toggleEmojiPrediction: @escaping(Bool) -> Void, toggleBigEmoji: @escaping(Bool) -> Void, toggleStatusBar: @escaping(Bool) -> Void, toggleRTFEnabled: @escaping(Bool)->Void, openChatAtLaunch:@escaping(Bool)->Void, acceptSecretChats: @escaping(Bool)->Void, toggleWorkMode:@escaping(Bool)->Void, openShortcuts: @escaping()->Void, callSettings: @escaping() ->Void) { + self.context = context + self.toggleCallsTab = toggleCallsTab self.toggleInAppKeys = toggleInAppKeys self.toggleInput = toggleInput self.toggleSidebar = toggleSidebar self.toggleInAppSounds = toggleInAppSounds self.toggleEmojiReplacements = toggleEmojiReplacements self.toggleForceTouchAction = toggleForceTouchAction + self.toggleInstantViewScrollBySpace = toggleInstantViewScrollBySpace + self.toggleAutoplayGifs = toggleAutoplayGifs + self.toggleEmojiPrediction = toggleEmojiPrediction + self.toggleBigEmoji = toggleBigEmoji + self.toggleStatusBar = toggleStatusBar + self.toggleRTFEnabled = toggleRTFEnabled + self.openChatAtLaunch = openChatAtLaunch + self.acceptSecretChats = acceptSecretChats + self.toggleWorkMode = toggleWorkMode + self.openShortcuts = openShortcuts + self.callSettings = callSettings } } -private func generalSettingsEntries(arguments:GeneralSettingsArguments, baseSettings: BaseApplicationSettings, appearance: Appearance) -> [GeneralSettingsEntry] { +private func generalSettingsEntries(arguments:GeneralSettingsArguments, baseSettings: BaseApplicationSettings, appearance: Appearance, launchSettings: LaunchSettings, secretChatSettings: SecretChatSettings) -> [GeneralSettingsEntry] { var sectionId:Int = 1 var entries:[GeneralSettingsEntry] = [] + var headerUnique:Int = -1 + entries.append(.section(sectionId: sectionId)) sectionId += 1 - var headerUnique:Int = -1 - - entries.append(.header(sectionId: sectionId, uniqueId: headerUnique, text: tr(.generalSettingsAppearanceSettings))) + entries.append(.header(sectionId: sectionId, uniqueId: headerUnique, text: L10n.generalSettingsEmojiAndStickers)) headerUnique -= 1 - entries.append(.darkMode(sectionId: sectionId, enabled: appearance.presentation.dark)) - entries.append(.fontSize(sectionId: sectionId, enabled: appearance.presentation.fontSize > 13.0)) + entries.append(.sidebar(sectionId: sectionId, enabled: FastSettings.sidebarEnabled, viewType: .firstItem)) + entries.append(.emojiReplacements(sectionId: sectionId, enabled: FastSettings.isPossibleReplaceEmojies, viewType: .innerItem)) + if !baseSettings.predictEmoji { + entries.append(.predictEmoji(sectionId: sectionId, enabled: baseSettings.predictEmoji, viewType: .innerItem)) + } + entries.append(.bigEmoji(sectionId: sectionId, enabled: baseSettings.bigEmoji, viewType: .lastItem)) entries.append(.section(sectionId: sectionId)) sectionId += 1 - entries.append(.header(sectionId: sectionId, uniqueId: headerUnique, text: tr(.generalSettingsInputSettings))) + entries.append(.header(sectionId: sectionId, uniqueId: headerUnique, text: L10n.generalSettingsInterfaceHeader)) headerUnique -= 1 - - entries.append(.enterBehavior(sectionId: sectionId, enabled: FastSettings.sendingType == .enter)) - entries.append(.cmdEnterBehavior(sectionId: sectionId, enabled: FastSettings.sendingType == .cmdEnter)) + entries.append(.showCallsTab(sectionId: sectionId, enabled: baseSettings.showCallsTab, viewType: .firstItem)) + entries.append(.statusBar(sectionId: sectionId, enabled: baseSettings.statusBar, viewType: .innerItem)) + entries.append(.inAppSounds(sectionId: sectionId, enabled: FastSettings.inAppSounds, viewType: .lastItem)) entries.append(.section(sectionId: sectionId)) sectionId += 1 + entries.append(.header(sectionId: sectionId, uniqueId: headerUnique, text: L10n.generalSettingsShortcutsHeader)) + headerUnique -= 1 + entries.append(.shortcuts(sectionId: sectionId, viewType: .singleItem)) + - entries.append(.header(sectionId: sectionId, uniqueId: headerUnique, text: tr(.generalSettingsGeneralSettings))) + + entries.append(.section(sectionId: sectionId)) + sectionId += 1 + + entries.append(.header(sectionId: sectionId, uniqueId: headerUnique, text: L10n.generalSettingsAdvancedHeader)) headerUnique -= 1 + entries.append(.enableRFTCopy(sectionId: sectionId, enabled: FastSettings.enableRTF, viewType: .firstItem)) + // entries.append(.openChatAtLaunch(sectionId: sectionId, enabled: launchSettings.openAtLaunch, viewType: .innerItem)) + entries.append(.acceptSecretChats(sectionId: sectionId, enabled: secretChatSettings.acceptOnThisDevice, viewType: .lastItem)) + + entries.append(.section(sectionId: sectionId)) + sectionId += 1 + entries.append(.header(sectionId: sectionId, uniqueId: headerUnique, text: L10n.generalSettingsForceTouchHeader)) + headerUnique -= 1 - //entries.append(.largeFonts(sectionId: sectionId, enabled: baseSettings.fontSize > 13)) - #if !APP_STORE - entries.append(.handleInAppKeys(sectionId: sectionId, enabled: baseSettings.handleInAppKeys)) - #endif - entries.append(.sidebar(sectionId: sectionId, enabled: FastSettings.sidebarEnabled)) - entries.append(.inAppSounds(sectionId: sectionId, enabled: FastSettings.inAppSounds)) - entries.append(.emojiReplacements(sectionId: sectionId, enabled: FastSettings.isPossibleReplaceEmojies)) + entries.append(.forceTouchReply(sectionId: sectionId, enabled: FastSettings.forceTouchAction == .reply, viewType: .firstItem)) + entries.append(.forceTouchEdit(sectionId: sectionId, enabled: FastSettings.forceTouchAction == .edit, viewType: .innerItem)) + entries.append(.forceTouchForward(sectionId: sectionId, enabled: FastSettings.forceTouchAction == .forward, viewType: .lastItem)) - entries.append(.section(sectionId: sectionId)) sectionId += 1 - entries.append(.header(sectionId: sectionId, uniqueId: headerUnique, text: tr(.generalSettingsForceTouchHeader))) + entries.append(.header(sectionId: sectionId, uniqueId: headerUnique, text: L10n.generalSettingsInputSettings)) headerUnique -= 1 + entries.append(.enterBehavior(sectionId: sectionId, enabled: FastSettings.sendingType == .enter, viewType: .firstItem)) + entries.append(.cmdEnterBehavior(sectionId: sectionId, enabled: FastSettings.sendingType == .cmdEnter, viewType: .lastItem)) - entries.append(.forceTouchReply(sectionId: sectionId, enabled: FastSettings.forceTouchAction == .reply)) - entries.append(.forceTouchEdit(sectionId: sectionId, enabled: FastSettings.forceTouchAction == .edit)) - entries.append(.forceTouchForward(sectionId: sectionId, enabled: FastSettings.forceTouchAction == .forward)) + entries.append(.section(sectionId: sectionId)) + sectionId += 1 + + entries.append(.header(sectionId: sectionId, uniqueId: headerUnique, text: L10n.generalSettingsCallSettingsHeader)) + headerUnique -= 1 + + entries.append(.callSettings(sectionId: sectionId, enabled: true, viewType: .singleItem)) + entries.append(.section(sectionId: sectionId)) sectionId += 1 @@ -345,24 +345,26 @@ private func prepareEntries(left: [AppearanceWrapperEntry] class GeneralSettingsViewController: TableViewController { - + private let disposable = MetaDisposable() override var removeAfterDisapper:Bool { return true } override func viewDidLoad() { super.viewDidLoad() - readyOnce() - let postbox = account.postbox + + let context = self.context let inputPromise:ValuePromise = ValuePromise(FastSettings.sendingType, ignoreRepeated: true) let forceTouchPromise:ValuePromise = ValuePromise(FastSettings.forceTouchAction, ignoreRepeated: true) - let arguments = GeneralSettingsArguments(account: account, toggleFonts: { enable in - _ = updateApplicationFontSize(postbox: postbox, fontSize: enable ? 15.0 : 13.0).start() + let arguments = GeneralSettingsArguments(context: context, toggleCallsTab: { enable in + _ = updateBaseAppSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings -> BaseApplicationSettings in + return settings.withUpdatedShowCallsTab(enable) + }).start() }, toggleInAppKeys: { enable in - _ = updateBaseAppSettingsInteractively(postbox: postbox, { settings -> BaseApplicationSettings in + _ = updateBaseAppSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings -> BaseApplicationSettings in return settings.withUpdatedInAppKeyHandle(enable) }).start() }, toggleInput: { input in @@ -377,44 +379,82 @@ class GeneralSettingsViewController: TableViewController { }, toggleForceTouchAction: { action in FastSettings.toggleForceTouchAction(action) forceTouchPromise.set(action) + }, toggleInstantViewScrollBySpace: { enable in + FastSettings.toggleInstantViewScrollBySpace(enable) + }, toggleAutoplayGifs: { enable in + FastSettings.toggleAutoPlayGifs(enable) + }, toggleEmojiPrediction: { enable in + _ = updateBaseAppSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings -> BaseApplicationSettings in + return settings.withUpdatedPredictEmoji(enable) + }).start() + }, toggleBigEmoji: { enable in + _ = updateBaseAppSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings -> BaseApplicationSettings in + return settings.withUpdatedBigEmoji(enable) + }).start() + }, toggleStatusBar: { enable in + _ = updateBaseAppSettingsInteractively(accountManager: context.sharedContext.accountManager, { settings -> BaseApplicationSettings in + return settings.withUpdatedStatusBar(enable) + }).start() + }, toggleRTFEnabled: { enable in + FastSettings.enableRTF = enable + }, openChatAtLaunch: { enable in + _ = updateLaunchSettings(context.account.postbox, { + $0.withUpdatedOpenAtLaunch(enable) + }).start() + }, acceptSecretChats: { enable in + _ = context.account.postbox.transaction({ transaction -> Void in + transaction.updatePreferencesEntry(key: PreferencesKeys.secretChatSettings, { _ in + return SecretChatSettings(acceptOnThisDevice: enable) + }) + }).start() + }, toggleWorkMode: { value in + + }, openShortcuts: { + context.sharedContext.bindings.rootNavigation().push(ShortcutListController(context: context)) + }, callSettings: { + context.sharedContext.bindings.rootNavigation().push(CallSettingsController(sharedContext: context.sharedContext)) }) let initialSize = atomicSize let previos:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) - genericView.merge(with: combineLatest(account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.baseAppSettings]) |> deliverOnMainQueue, inputPromise.get() |> deliverOnMainQueue, forceTouchPromise.get() |> deliverOnMainQueue, appearanceSignal) |> map { settings, _, _, appearance -> TableUpdateTransition in + let baseSettingsSignal: Signal = .single(context.sharedContext.baseSettings) |> then(baseAppSettings(accountManager: context.sharedContext.accountManager)) + + let signal = combineLatest(queue: prepareQueue, baseSettingsSignal, inputPromise.get(), forceTouchPromise.get(), appearanceSignal, appLaunchSettings(postbox: context.account.postbox), context.account.postbox.preferencesView(keys: [PreferencesKeys.secretChatSettings])) |> map { settings, _, _, appearance, launchSettings, preferencesView -> TableUpdateTransition in - let baseSettings: BaseApplicationSettings - if let settings = settings.values[ApplicationSpecificPreferencesKeys.baseAppSettings] as? BaseApplicationSettings { - baseSettings = settings - } else { - baseSettings = BaseApplicationSettings.defaultSettings - } + let baseSettings: BaseApplicationSettings = settings + + let secretChatSettings = preferencesView.values[PreferencesKeys.secretChatSettings] as? SecretChatSettings ?? SecretChatSettings.defaultSettings - let entries = generalSettingsEntries(arguments: arguments, baseSettings: baseSettings, appearance: appearance).map({AppearanceWrapperEntry(entry: $0, appearance: appearance)}) + let entries = generalSettingsEntries(arguments: arguments, baseSettings: baseSettings, appearance: appearance, launchSettings: launchSettings, secretChatSettings: secretChatSettings).map({AppearanceWrapperEntry(entry: $0, appearance: appearance)}) let previous = previos.swap(entries) return prepareEntries(left: previous, right: entries, arguments: arguments, initialSize: initialSize.modify({$0})) - - } |> deliverOnMainQueue ) + + } |> deliverOnMainQueue + disposable.set(signal.start(next: { [weak self] transition in + self?.genericView.merge(with: transition) + self?.readyOnce() + })) + } private var loggerClickCount = 0 private func incrementLogClick() { loggerClickCount += 1 - let account = self.account + let context = self.context if loggerClickCount == 5 { UserDefaults.standard.set(!UserDefaults.standard.bool(forKey: "enablelogs"), forKey: "enablelogs") - let logs = Logger.shared.collectLogs() |> deliverOnMainQueue |> mapToSignal { logs -> Signal in - return selectModalPeers(account: account, title: "Send Logs", limit: 1, confirmation: {_ in return confirmSignal(for: mainWindow, header: appName, information: "Are you sure you want send logs?")}) |> filter {!$0.isEmpty} |> map {$0.first!} |> mapToSignal { peerId -> Signal in + let logs = Logger.shared.collectLogs() |> deliverOnMainQueue |> mapToSignal { logs -> Signal in + return selectModalPeers(window: context.window, context: context, title: "Send Logs", limit: 1, confirmation: {_ in return confirmSignal(for: mainWindow, information: "Are you sure you want send logs?")}) |> filter {!$0.isEmpty} |> map {$0.first!} |> mapToSignal { peerId -> Signal in let messages = logs.map { (name, path) -> EnqueueMessage in let id = arc4random64() - let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], mimeType: "application/text", size: nil, attributes: [.FileName(fileName: name)]) - return .message(text: "", attributes: [], media: file, replyToMessageId: nil) + let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: id), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: nil, attributes: [.FileName(fileName: name)]) + return .message(text: "", attributes: [], mediaReference: AnyMediaReference.standalone(media: file), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil) } - return enqueueMessages(account: account, peerId: peerId, messages: messages) |> map {_ in} + return enqueueMessages(account: context.account, peerId: peerId, messages: messages) |> map {_ in} } } _ = logs.start() @@ -425,12 +465,15 @@ class GeneralSettingsViewController: TableViewController { } + deinit { + disposable.dispose() + } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - self.window?.set(handler: { [weak self] () -> KeyHandlerResult in + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in self?.incrementLogClick() return .invoked }, with: self, for: .L, modifierFlags: [.control]) diff --git a/Telegram-Mac/GeneralTextRowItem.swift b/Telegram-Mac/GeneralTextRowItem.swift index 912613170d..28bb8f0ec6 100644 --- a/Telegram-Mac/GeneralTextRowItem.swift +++ b/Telegram-Mac/GeneralTextRowItem.swift @@ -9,69 +9,140 @@ import Cocoa import TGUIKit -enum GeneralRowTextType { +enum GeneralRowTextType : Equatable { case plain(String) case markdown(String, linkHandler: (String)->Void) + case customMarkdown(String, linkColor: NSColor, linkFont: NSFont, linkHandler: (String)->Void) + + static func ==(lhs: GeneralRowTextType, rhs: GeneralRowTextType) -> Bool { + switch lhs { + case let .plain(text): + if case .plain(text) = rhs { + return true + } else { + return false + } + case let .markdown(text, _): + if case .markdown(text, _) = rhs { + return true + } else { + return false + } + case let .customMarkdown(text, color, font, _): + if case .customMarkdown(text, color, font, _) = rhs { + return true + } else { + return false + } + } + } } -class GeneralTextRowItem: GeneralRowItem { + +class GeneralTextRowItem: GeneralRowItem { + fileprivate let textColor: NSColor fileprivate var layout:TextViewLayout private let text:NSAttributedString private let alignment:NSTextAlignment fileprivate let centerViewAlignment: Bool - init(_ initialSize: NSSize, stableId: AnyHashable = arc4random(), height: CGFloat = 0, text:NSAttributedString, alignment:NSTextAlignment = .left, drawCustomSeparator:Bool = false, border:BorderType = [], inset:NSEdgeInsets = NSEdgeInsets(left: 30.0, right: 30.0, top:2, bottom:2), action: @escaping ()->Void = {}, centerViewAlignment: Bool = false) { - self.text = text + fileprivate let additionLoading: Bool + fileprivate let isTextSelectable: Bool + fileprivate let rightItem: InputDataGeneralTextRightData + init(_ initialSize: NSSize, stableId: AnyHashable = arc4random(), height: CGFloat = 0, text:NSAttributedString, alignment:NSTextAlignment = .left, drawCustomSeparator:Bool = false, border:BorderType = [], inset:NSEdgeInsets = NSEdgeInsets(left: 30.0, right: 30.0, top:4, bottom:2), action: @escaping ()->Void = {}, centerViewAlignment: Bool = false, additionLoading: Bool = false, additionRightText: String? = nil, linkExecutor: TextViewInteractions = globalLinkExecutor, isTextSelectable: Bool = false, detectLinks: Bool = true, viewType: GeneralViewType = .legacy, rightItem: InputDataGeneralTextRightData = InputDataGeneralTextRightData(isLoading: false, text: nil)) { + self.textColor = theme.colors.listGrayText + self.isTextSelectable = isTextSelectable + let mutable = text.mutableCopy() as! NSMutableAttributedString + if detectLinks { + mutable.detectLinks(type: [.Links], context: nil, openInfo: {_, _, _, _ in }, hashtag: nil, command: nil, applyProxy: nil, dotInMention: false) + } + self.rightItem = rightItem + self.text = mutable + self.additionLoading = additionLoading self.alignment = alignment self.centerViewAlignment = centerViewAlignment - layout = TextViewLayout(text, truncationType: .end, alignment: alignment) - layout.interactions = globalLinkExecutor - super.init(initialSize, height: height, stableId: stableId, type: .none, action: action, drawCustomSeparator: drawCustomSeparator, border: border, inset: inset) + layout = TextViewLayout(mutable, truncationType: .end, alignment: alignment) + layout.interactions = linkExecutor + super.init(initialSize, height: height, stableId: stableId, type: .none, viewType: viewType, action: action, drawCustomSeparator: drawCustomSeparator, border: border, inset: inset) } - init(_ initialSize: NSSize, stableId: AnyHashable = arc4random(), height: CGFloat = 0, text: GeneralRowTextType, alignment:NSTextAlignment = .left, drawCustomSeparator:Bool = false, border:BorderType = [], inset:NSEdgeInsets = NSEdgeInsets(left: 30.0, right: 30.0, top:2, bottom:2), action: @escaping ()->Void = {}, centerViewAlignment: Bool = false) { + init(_ initialSize: NSSize, stableId: AnyHashable = arc4random(), height: CGFloat = 0, text: GeneralRowTextType, detectBold: Bool = true, textColor: NSColor = theme.colors.listGrayText, alignment:NSTextAlignment = .left, drawCustomSeparator:Bool = false, border:BorderType = [], inset:NSEdgeInsets = NSEdgeInsets(left: 30.0, right: 30.0, top:4, bottom:2), action: @escaping ()->Void = {}, centerViewAlignment: Bool = false, additionLoading: Bool = false, isTextSelectable: Bool = false, viewType: GeneralViewType = .legacy, rightItem: InputDataGeneralTextRightData = InputDataGeneralTextRightData(isLoading: false, text: nil), fontSize: CGFloat? = nil) { - let attributedText: NSAttributedString - + let attributedText: NSMutableAttributedString + self.textColor = textColor switch text { case let .plain(text): - attributedText = .initialize(string: text, color: theme.colors.grayText, font: .normal(.custom(11.5))) + attributedText = NSAttributedString.initialize(string: text, color: textColor, font: .normal(fontSize ?? 11.5)).mutableCopy() as! NSMutableAttributedString case let .markdown(text, handler): - attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: .normal(.custom(11.5)), textColor: theme.colors.grayText), bold: MarkdownAttributeSet(font: .bold(.custom(11.5)), textColor: theme.colors.grayText), link: MarkdownAttributeSet(font: .normal(.custom(11.5)), textColor: theme.colors.link), linkAttribute: { contents in - return (NSAttributedStringKey.link.rawValue, inAppLink.callback(contents, handler)) - })) + attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: .normal(fontSize ?? 11.5), textColor: textColor), bold: MarkdownAttributeSet(font: .bold(fontSize ?? 11.5), textColor: textColor), link: MarkdownAttributeSet(font: .normal(fontSize ?? 11.5), textColor: theme.colors.link), linkAttribute: { contents in + return (NSAttributedString.Key.link.rawValue, inAppLink.callback(contents, handler)) + })).mutableCopy() as! NSMutableAttributedString + case let .customMarkdown(text, linkColor, linkFont, handler): + attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: .normal(fontSize ?? 11.5), textColor: textColor), bold: MarkdownAttributeSet(font: .bold(fontSize ?? 11.5), textColor: textColor), link: MarkdownAttributeSet(font: linkFont, textColor: linkColor), linkAttribute: { contents in + return (NSAttributedString.Key.link.rawValue, inAppLink.callback(contents, handler)) + })).mutableCopy() as! NSMutableAttributedString + } + if detectBold { + attributedText.detectBoldColorInString(with: .bold(fontSize ?? 11.5)) } + self.rightItem = rightItem self.text = attributedText self.alignment = alignment + self.isTextSelectable = isTextSelectable + self.additionLoading = additionLoading self.centerViewAlignment = centerViewAlignment layout = TextViewLayout(attributedText, truncationType: .end, alignment: alignment) layout.interactions = globalLinkExecutor - super.init(initialSize, height: height, stableId: stableId, type: .none, action: action, drawCustomSeparator: drawCustomSeparator, border: border, inset: inset) + super.init(initialSize, height: height, stableId: stableId, type: .none, viewType: viewType, action: action, drawCustomSeparator: drawCustomSeparator, border: border, inset: inset) } - init(_ initialSize: NSSize, stableId: AnyHashable = arc4random(), height: CGFloat = 0, text:String, alignment:NSTextAlignment = .left, drawCustomSeparator:Bool = false, border:BorderType = [], inset:NSEdgeInsets = NSEdgeInsets(left: 30.0, right: 30.0, top:2, bottom:2), action: @escaping ()->Void = {}, centerViewAlignment: Bool = false) { - let attr = NSAttributedString.initialize(string: text, color: theme.colors.grayText, font: .normal(.custom(11.5))).mutableCopy() as! NSMutableAttributedString - attr.detectBoldColorInString(with: .medium(.text)) + init(_ initialSize: NSSize, stableId: AnyHashable = arc4random(), height: CGFloat = 0, text:String, detectBold: Bool = true, textColor: NSColor = theme.colors.listGrayText, alignment:NSTextAlignment = .left, drawCustomSeparator:Bool = false, border:BorderType = [], inset:NSEdgeInsets = NSEdgeInsets(left: 30.0, right: 30.0), action: @escaping ()->Void = {}, centerViewAlignment: Bool = false, additionLoading: Bool = false, fontSize: CGFloat = 11.5, isTextSelectable: Bool = false, viewType: GeneralViewType = .legacy, rightItem: InputDataGeneralTextRightData = InputDataGeneralTextRightData(isLoading: false, text: nil)) { + let attr = NSAttributedString.initialize(string: text, color: textColor, font: .normal(fontSize)).mutableCopy() as! NSMutableAttributedString + if detectBold { + attr.detectBoldColorInString(with: .medium(fontSize)) + } + self.textColor = textColor self.text = attr self.alignment = alignment + self.isTextSelectable = isTextSelectable + self.additionLoading = additionLoading self.centerViewAlignment = centerViewAlignment layout = TextViewLayout(self.text, truncationType: .end, alignment: alignment) layout.interactions = globalLinkExecutor - super.init(initialSize, height: height, stableId: stableId, type: .none, action: action, drawCustomSeparator: drawCustomSeparator, border: border, inset: inset) + self.rightItem = rightItem + super.init(initialSize, height: height, stableId: stableId, type: .none, viewType: viewType, action: action, drawCustomSeparator: drawCustomSeparator, border: border, inset: inset) } + + override var height: CGFloat { if _height > 0 { return _height } - return layout.layoutSize.height + inset.top + inset.bottom + switch viewType { + case .legacy: + return layout.layoutSize.height + inset.top + inset.bottom + (additionLoading ? 30 : 0) + case let .modern(_, insets): + return layout.layoutSize.height + inset.top + inset.bottom + insets.top + insets.bottom + (additionLoading ? 30 : 0) + } } override func makeSize(_ width: CGFloat, oldWidth:CGFloat) -> Bool { - - layout.measure(width: width - inset.left - inset.right) + let success = super.makeSize(width, oldWidth: oldWidth) + switch viewType { + case .legacy: + layout.measure(width: width - inset.left - inset.right) + case let .modern(_, insets): + var addition: CGFloat = 0 + if let text = rightItem.text { + let layout = TextViewLayout(text) + layout.measure(width: .greatestFiniteMagnitude) + addition += layout.layoutSize.width + 20 + } + layout.measure(width: self.blockWidth - insets.left - insets.right - addition) + } - return super.makeSize(width, oldWidth: oldWidth) + return success } override func viewClass() -> AnyClass { @@ -83,29 +154,105 @@ class GeneralTextRowItem: GeneralRowItem { class GeneralTextRowView : GeneralRowView { private let textView:TextView = TextView() - + private var progressView: ProgressIndicator? + private var rightTextView: TextView? + private var animatedView: MediaAnimatedStickerView? required init(frame frameRect: NSRect) { super.init(frame: frameRect) addSubview(textView) - textView.isSelectable = false + } + + override var firstResponder: NSResponder? { + return nil } override func draw(_ layer: CALayer, in ctx: CGContext) { super.draw(layer, in: ctx) - if let item = item as? GeneralTextRowItem, item.drawCustomSeparator { - ctx.setFillColor(theme.colors.border.cgColor) - ctx.fill(NSMakeRect(item.inset.left, frame.height - .borderSize, frame.width - item.inset.left - item.inset.right, .borderSize)) + if let item = item as? GeneralTextRowItem { + switch item.viewType { + case .legacy: + if item.drawCustomSeparator { + ctx.setFillColor(theme.colors.border.cgColor) + ctx.fill(NSMakeRect(item.inset.left, frame.height - .borderSize, frame.width - item.inset.left - item.inset.right, .borderSize)) + } + case .modern: + break + } } } + override var backdorColor: NSColor { + if let item = item as? GeneralTextRowItem { + return item.viewType.rowBackground + } + return theme.colors.background + } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func set(item: TableRowItem, animated: Bool) { super.set(item: item, animated: animated) - textView.backgroundColor = theme.colors.background + textView.backgroundColor = self.backdorColor + + guard let item = item as? GeneralTextRowItem else {return} + textView.isSelectable = item.isTextSelectable + + if item.additionLoading || item.rightItem.isLoading { + let size = item.rightItem.isLoading ? NSMakeSize(15, 15) : NSMakeSize(20, 20) + if progressView == nil { + progressView = ProgressIndicator(frame: NSMakeRect(0, 0, size.width, size.height)) + } + if progressView!.superview == nil { + addSubview(progressView!) + } + } else { + progressView?.removeFromSuperview() + progressView = nil + } + if let text = item.rightItem.text { + if self.rightTextView == nil { + self.rightTextView = TextView() + addSubview(self.rightTextView!) + } + + + let textLayout = TextViewLayout(text) + textLayout.measure(width: .greatestFiniteMagnitude) + + var animatedData:InputDataTextInsertAnimatedViewData? + text.enumerateAttributes(in: text.range, options: [], using: { data, range, stop in + + if let attr = data[InputDataTextInsertAnimatedViewData.attributeKey] { + animatedData = attr as? InputDataTextInsertAnimatedViewData + } + }) + + if let attr = animatedData { + if self.animatedView == nil { + self.animatedView = MediaAnimatedStickerView(frame: NSZeroRect) + self.addSubview(self.animatedView!) + } + self.animatedView?.update(with: attr.file, size: NSMakeSize(16, 16), context: attr.context, parent: nil, table: nil, parameters: ChatAnimatedStickerMediaLayoutParameters(playPolicy: .loop, media: attr.file), animated: animated, positionFlags: nil, approximateSynchronousValue: true) + + } else { + self.animatedView?.removeFromSuperview() + self.animatedView = nil + } + + self.rightTextView?.update(textLayout) + self.rightTextView?.isSelectable = false + self.rightTextView?.userInteractionEnabled = false + } else { + self.rightTextView?.removeFromSuperview() + self.rightTextView = nil + self.animatedView?.removeFromSuperview() + self.animatedView = nil + } + + needsDisplay = true needsLayout = true } @@ -117,12 +264,51 @@ class GeneralTextRowView : GeneralRowView { } } + override func shakeView() { + textView.shake() + } + override func layout() { super.layout() if let item = item as? GeneralTextRowItem { - textView.update(item.layout, origin:NSMakePoint(item.inset.left, item.inset.top)) + if item.additionLoading, let progressView = progressView { + progressView.centerX(y: 0) + textView.update(item.layout) + textView.centerX(y: progressView.frame.maxY + 10) + } else { + switch item.viewType { + case .legacy: + textView.update(item.layout, origin: NSMakePoint(item.inset.left, item.inset.top)) + case let .modern(_, insets): + let mid = max(0, floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2)) + textView.update(item.layout, origin: NSMakePoint(mid + insets.left, item.inset.top + insets.top)) + + if item.rightItem.isLoading, let progressView = self.progressView { + progressView.setFrameOrigin(NSMakePoint(frame.width - progressView.frame.width - mid - insets.left - insets.right, item.inset.top + insets.top)) + progressView.progressColor = item.textColor + } + if let rightTextView = self.rightTextView { + rightTextView.setFrameOrigin(NSMakePoint(frame.width - rightTextView.frame.width - mid - insets.left - insets.right, frame.height - insets.bottom - rightTextView.frame.height)) + + if let layout = rightTextView.layout { + var animatedRange: NSRange? = nil + layout.attributedString.enumerateAttributes(in: layout.attributedString.range, options: [], using: { data, range, stop in + if let _ = data[InputDataTextInsertAnimatedViewData.attributeKey] { + animatedRange = range + } + }) + if let range = animatedRange, let view = self.animatedView, let offset = layout.offset(for: range.location) { + view.setFrameOrigin(NSMakePoint(rightTextView.frame.minX + offset, rightTextView.frame.minY - 1)) + } + } + } + + } + + + } if item.centerViewAlignment { - textView.center() + textView.center() } } } diff --git a/Telegram-Mac/GeneratedMediaStoreSettings.swift b/Telegram-Mac/GeneratedMediaStoreSettings.swift index d0d7333c29..a0e7724f42 100644 --- a/Telegram-Mac/GeneratedMediaStoreSettings.swift +++ b/Telegram-Mac/GeneratedMediaStoreSettings.swift @@ -7,8 +7,8 @@ // import Cocoa -import PostboxMac -import SwiftSignalKitMac +import Postbox +import SwiftSignalKit public struct GeneratedMediaStoreSettings: PreferencesEntry, Equatable { public let storeEditedPhotos: Bool @@ -47,8 +47,8 @@ public struct GeneratedMediaStoreSettings: PreferencesEntry, Equatable { } func updateGeneratedMediaStoreSettingsInteractively(postbox: Postbox, _ f: @escaping (GeneratedMediaStoreSettings) -> GeneratedMediaStoreSettings) -> Signal { - return postbox.modify { modifier -> Void in - modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.generatedMediaStoreSettings, { entry in + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.generatedMediaStoreSettings, { entry in let currentSettings: GeneratedMediaStoreSettings if let entry = entry as? GeneratedMediaStoreSettings { currentSettings = entry diff --git a/Telegram-Mac/Geocoding.swift b/Telegram-Mac/Geocoding.swift new file mode 100644 index 0000000000..9a39958f09 --- /dev/null +++ b/Telegram-Mac/Geocoding.swift @@ -0,0 +1,60 @@ +import Foundation +import CoreLocation +import SwiftSignalKit + +func geocodeLocation(dictionary: [String: String]) -> Signal<(Double, Double)?, NoError> { + return Signal { subscriber in + let geocoder = CLGeocoder() + geocoder.geocodeAddressDictionary(dictionary, completionHandler: { placemarks, _ in + if let location = placemarks?.first?.location { + subscriber.putNext((location.coordinate.latitude, location.coordinate.longitude)) + } else { + subscriber.putNext(nil) + } + subscriber.putCompletion() + }) + return ActionDisposable { + geocoder.cancelGeocode() + } + } +} + +struct ReverseGeocodedPlacemark { + let street: String? + let city: String? + let country: String? + + var fullAddress: String { + var components: [String] = [] + if let street = self.street { + components.append(street) + } + if let city = self.city { + components.append(city) + } + if let country = self.country { + components.append(country) + } + + return components.joined(separator: ", ") + } +} + +func reverseGeocodeLocation(latitude: Double, longitude: Double) -> Signal { + return Signal { subscriber in + let geocoder = CLGeocoder() + geocoder.reverseGeocodeLocation(CLLocation(latitude: latitude, longitude: longitude), completionHandler: { placemarks, _ in + if let placemarks = placemarks, let placemark = placemarks.first { + let result = ReverseGeocodedPlacemark(street: placemark.thoroughfare, city: placemark.locality, country: placemark.country) + subscriber.putNext(result) + subscriber.putCompletion() + } else { + subscriber.putNext(nil) + subscriber.putCompletion() + } + }) + return ActionDisposable { + geocoder.cancelGeocode() + } + } +} diff --git a/Telegram-Mac/GifPanelTabRowItem.swift b/Telegram-Mac/GifPanelTabRowItem.swift new file mode 100644 index 0000000000..8c9e6dc761 --- /dev/null +++ b/Telegram-Mac/GifPanelTabRowItem.swift @@ -0,0 +1,110 @@ +// +// GifPanelTabRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 05/06/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +class GifPanelTabRowItem: TableRowItem { + override var stableId: AnyHashable { + return entry + } + + let entry: GifTabEntryId + private let selected: Bool + + let select:(GifTabEntryId)->Void + + fileprivate let icon: CGImage + + init(_ initialSize: NSSize, selected: Bool, entry: GifTabEntryId, select: @escaping(GifTabEntryId)->Void) { + self.selected = selected + self.entry = entry + self.select = select + var icon: CGImage + switch entry { + case .recent: + icon = theme.icons.stickersTabRecent + case .trending: + icon = theme.icons.gif_trending + case let .recommended(value): + icon = generateTextIcon(.initialize(string: value, color: .white, font: .normal(18))) + } + + self.icon = generateImage(NSMakeSize(35, 35), contextGenerator: { size, ctx in + let rect = CGRect(origin: CGPoint(), size: size) + ctx.interpolationQuality = .high + ctx.clear(rect) + if selected { + ctx.round(size, .cornerRadius) + ctx.setFillColor(theme.colors.grayForeground.cgColor) + ctx.fill(rect) + } + ctx.draw(icon, in: rect.focus(icon.backingSize)) + })! + + + super.init(initialSize) + } + + override var height:CGFloat { + return 40.0 + } + override var width: CGFloat { + return 40.0 + } + + override func viewClass() -> AnyClass { + return GifPanelTabRowView.self + } +} + + +private final class GifPanelTabRowView: HorizontalRowView { + + private let control: ImageButton = ImageButton() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + + control.set(handler: { [weak self] control in + if let item = self?.item as? GifPanelTabRowItem { + item.select(item.entry) + } + }, for: .Click) + + + control.autohighlight = false + control.animates = false + control.frame = NSMakeRect(0, 0, 40, 40) + + addSubview(control) + + } + + + override var backdorColor: NSColor { + return .clear + } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + + if let item = item as? GifPanelTabRowItem { + control.set(image: item.icon, for: .Normal) + control.set(image: item.icon, for: .Highlight) + control.set(image: item.icon, for: .Hover) + + } + control.frame = bounds + control.center() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/Telegram-Mac/GifPlayerBufferView.swift b/Telegram-Mac/GifPlayerBufferView.swift new file mode 100644 index 0000000000..b947f73409 --- /dev/null +++ b/Telegram-Mac/GifPlayerBufferView.swift @@ -0,0 +1,179 @@ +// +// GifPlayerBufferView.swift +// Telegram +// +// Created by Mikhail Filimonov on 28/05/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import TGUIKit +import Postbox +import SwiftSignalKit + + +final class GifPlayerBufferView : TransformImageView { + var timebase: CMTimebase + + private var videoLayer: (SoftwareVideoLayerFrameManager, SampleBufferLayer)? + + override init() { + var timebase: CMTimebase? + CMTimebaseCreateWithMasterClock(allocator: nil, masterClock: CMClockGetHostTimeClock(), timebaseOut: &timebase) + CMTimebaseSetRate(timebase!, rate: 0.0) + self.timebase = timebase! + + super.init() + self.layerContentsRedrawPolicy = .duringViewResize + } + + private var fileReference: FileMediaReference? + private var resizeInChat: Bool = false + func update(_ fileReference: FileMediaReference, context: AccountContext, resizeInChat: Bool = false) -> Void { + + let updated = self.fileReference == nil || !fileReference.media.isEqual(to: self.fileReference!.media) + self.fileReference = fileReference + self.resizeInChat = resizeInChat + if updated { + self.videoLayer?.1.layer.removeFromSuperlayer() + + let layerHolder = takeSampleBufferLayer() + if let gravity = gravity { + layerHolder.layer.videoGravity = gravity + } else { + layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill + } + layerHolder.layer.backgroundColor = NSColor.clear.cgColor + self.layer?.addSublayer(layerHolder.layer) + let manager = SoftwareVideoLayerFrameManager(account: context.account, fileReference: fileReference, layerHolder: layerHolder) + self.videoLayer = (manager, layerHolder) + manager.start() + } + + + needsLayout = true + } + + override func layout() { + super.layout() + + if let file = fileReference?.media, resizeInChat { + let dimensions = file.dimensions?.size ?? frame.size + let size = dimensions.aspectFitted(frame.size) + let rect = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - size.width) / 2), floorToScreenPixels(backingScaleFactor, (frame.height - size.height) / 2), size.width, size.height) + videoLayer?.1.layer.frame = rect + } else { + videoLayer?.1.layer.frame = bounds + } + + } + + private var gravity: AVLayerVideoGravity? + + func setVideoLayerGravity(_ gravity: AVLayerVideoGravity) { + self.gravity = gravity + videoLayer?.1.layer.videoGravity = gravity + } + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required public init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + private var displayLink: ConstantDisplayLinkAnimator? + var ticking: Bool = false { + didSet { + if self.ticking != oldValue { + if self.ticking { + let displayLink = ConstantDisplayLinkAnimator(update: { [weak self] in + self?.displayLinkEvent() + }, fps: 25) + self.displayLink = displayLink + displayLink.isPaused = false + CMTimebaseSetRate(self.timebase, rate: 1.0) + } else if let displayLink = self.displayLink { + self.displayLink = nil + displayLink.isPaused = true + displayLink.invalidate() + CMTimebaseSetRate(self.timebase, rate: 0.0) + } + } + } + } + +// private var displayLink: DisplayLink? +// var ticking: Bool = false { +// didSet { +// if self.ticking != oldValue { +// if self.ticking { +// let displayLink = DisplayLink(onQueue: .main) +// self.displayLink = displayLink +// displayLink?.start() +// displayLink?.callback = { [weak self] in +// self?.displayLinkEvent() +// } +// CMTimebaseSetRate(self.timebase, rate: 1.0) +// } else if let displayLink = self.displayLink { +// self.displayLink = nil +// displayLink.cancel() +// CMTimebaseSetRate(self.timebase, rate: 0.0) +// } +// } +// } +// } + + private func displayLinkEvent() { + let timestamp = CMTimebaseGetTime(self.timebase).seconds + self.videoLayer?.0.tick(timestamp: timestamp) + } + + + private let maskLayer = CAShapeLayer() + + var positionFlags: LayoutPositionFlags? { + didSet { + if let positionFlags = positionFlags { + let path = CGMutablePath() + + let minx:CGFloat = 0, midx = frame.width/2.0, maxx = frame.width + let miny:CGFloat = 0, midy = frame.height/2.0, maxy = frame.height + + path.move(to: NSMakePoint(minx, midy)) + + var topLeftRadius: CGFloat = .cornerRadius + var bottomLeftRadius: CGFloat = .cornerRadius + var topRightRadius: CGFloat = .cornerRadius + var bottomRightRadius: CGFloat = .cornerRadius + + + if positionFlags.contains(.top) && positionFlags.contains(.left) { + bottomLeftRadius = .cornerRadius * 3 + 2 + } + if positionFlags.contains(.top) && positionFlags.contains(.right) { + bottomRightRadius = .cornerRadius * 3 + 2 + } + if positionFlags.contains(.bottom) && positionFlags.contains(.left) { + topLeftRadius = .cornerRadius * 3 + 2 + } + if positionFlags.contains(.bottom) && positionFlags.contains(.right) { + topRightRadius = .cornerRadius * 3 + 2 + } + + path.addArc(tangent1End: NSMakePoint(minx, miny), tangent2End: NSMakePoint(midx, miny), radius: bottomLeftRadius) + path.addArc(tangent1End: NSMakePoint(maxx, miny), tangent2End: NSMakePoint(maxx, midy), radius: bottomRightRadius) + path.addArc(tangent1End: NSMakePoint(maxx, maxy), tangent2End: NSMakePoint(midx, maxy), radius: topRightRadius) + path.addArc(tangent1End: NSMakePoint(minx, maxy), tangent2End: NSMakePoint(minx, midy), radius: topLeftRadius) + + maskLayer.path = path + layer?.mask = maskLayer + } else { + layer?.mask = nil + } + } + } + +} diff --git a/Telegram-Mac/GigagroupLanding.swift b/Telegram-Mac/GigagroupLanding.swift new file mode 100644 index 0000000000..45a4b99b14 --- /dev/null +++ b/Telegram-Mac/GigagroupLanding.swift @@ -0,0 +1,121 @@ +// +// GigagroupLanding.swift +// Telegram +// +// Created by Mikhail Filimonov on 15.02.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import SwiftSignalKit +import TGUIKit +import TelegramCore +import Postbox + + + + +private final class Arguments { + let context: AccountContext + init(context: AccountContext) { + self.context = context + } +} + +private struct State : Equatable { + +} + + +private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: .init("sticker"), equatable: nil, comparable: nil, item: { initialSize, stableId in + return AnimtedStickerHeaderItem(initialSize, stableId: stableId, context: arguments.context, sticker: .gigagroup, text: .init()) + })) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: .init("features"), equatable: nil, comparable: nil, item: { initialSize, stableId in + return GeneralBlockTextRowItem(initialSize, stableId: stableId, viewType: .singleItem, text: L10n.broadcastGroupsIntroText, font: .normal(.text)) + })) + index += 1 + + // entries + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func GigagroupLandingController(context: AccountContext, peerId: PeerId) -> InputDataModalController { + + var close:(()->Void)? = nil + + let actionsDisposable = DisposableSet() + + let initialState = State() + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let arguments = Arguments(context: context) + + let signal = statePromise.get() |> map { state in + return InputDataSignalValue(entries: entries(state, arguments: arguments)) + } + + let controller = InputDataController(dataSignal: signal, title: L10n.broadcastGroupsIntroTitle) + + controller.onDeinit = { + actionsDisposable.dispose() + } + + controller.validateData = { _ in + confirm(for: context.window, header: L10n.broadcastGroupsConfirmationAlertTitle, information: L10n.broadcastGroupsConfirmationAlertText, okTitle: L10n.broadcastGroupsConfirmationAlertConvert, successHandler: { _ in + _ = showModalProgress(signal: convertGroupToGigagroup(account: context.account, peerId: peerId), for: context.window).start(error: { error in + switch error { + case .generic: + alert(for: context.window, info: L10n.unknownError) + } + }, completed: { + showModalText(for: context.window, text: L10n.broadcastGroupsSuccess) + close?() + }) + + }, cancelHandler: { + + }) + return .none + } + + let modalInteractions = ModalInteractions(acceptTitle: L10n.broadcastGroupsConvert, accept: { [weak controller] in + _ = controller?.returnKeyAction() + }, drawBorder: true, height: 50, singleButton: true) + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) + + close = { [weak modalController] in + modalController?.modal?.close() + } + + + return modalController +} diff --git a/Telegram-Mac/GlobalBadgeNode.swift b/Telegram-Mac/GlobalBadgeNode.swift index 195b6fad58..4cc883f9b2 100644 --- a/Telegram-Mac/GlobalBadgeNode.swift +++ b/Telegram-Mac/GlobalBadgeNode.swift @@ -8,86 +8,199 @@ import Cocoa import TGUIKit -import PostboxMac -import SwiftSignalKitMac -import TelegramCoreMac +import Postbox +import SwiftSignalKit +import TelegramCore + + + class GlobalBadgeNode: Node { private let account:Account + private let sharedContext: SharedAccountContext private let layoutChanged:(()->Void)? private let excludePeerId:PeerId? private let disposable:MetaDisposable = MetaDisposable() private var textLayout:(TextNodeLayout, TextNode)? + var customLayout: Bool = false var xInset:CGFloat = 0 + var onUpdate:(()->Void)? private var attributedString:NSAttributedString? { didSet { if let attributedString = attributedString { textLayout = TextNode.layoutText(maybeNode: nil, attributedString, nil, 1, .middle, NSMakeSize(CGFloat.greatestFiniteMagnitude, CGFloat.greatestFiniteMagnitude), nil, false, .left) size = NSMakeSize(textLayout!.0.size.width + 8, textLayout!.0.size.height + 7) size = NSMakeSize(max(size.height,size.width), size.height) + + } else { textLayout = nil size = NSZeroSize } - if let superview = view?.superview as? View { + setNeedDisplay() + if let superview = view?.superview as? View, !self.customLayout { superview.customHandler.layout = { [weak self] view in if let strongSelf = self { if strongSelf.layoutChanged == nil { var origin:NSPoint = NSZeroPoint let center = view.focus(strongSelf.size) - origin = NSMakePoint(floorToScreenPixels(center.midX) + 4 + strongSelf.xInset, 4) + origin = NSMakePoint(floorToScreenPixels(System.backingScale, center.midX) + strongSelf.xInset, 4) + origin.x = min(view.frame.width - strongSelf.size.width - 4, origin.x) strongSelf.frame = NSMakeRect(origin.x,origin.y,strongSelf.size.width,strongSelf.size.height) } else { strongSelf.view?.setFrameSize(strongSelf.size) + strongSelf.layoutChanged?() } } } - setNeedDisplay() - superview.needsLayout = true } - + view?.superview?.needsLayout = true + onUpdate?() } } + override func update() { + let attributedString = self.attributedString + self.attributedString = attributedString + } + + override func setNeedDisplay() { + super.setNeedDisplay() + } + + var isSelected: Bool = false { + didSet { + if oldValue != self.isSelected { + self.view?.needsDisplay = true + let copy = self.attributedString?.mutableCopy() as? NSMutableAttributedString + guard let attr = copy else { + return + } + attr.addAttribute(.foregroundColor, value: getColor(!isSelected), range: attr.range) + self.attributedString = copy + } + } + } + private let getColor: (Bool) -> NSColor - init(_ account:Account, excludePeerId:PeerId? = nil, layoutChanged:(()->Void)? = nil) { + init(_ account: Account, sharedContext: SharedAccountContext, dockTile: Bool = false, collectAllAccounts: Bool = false, excludePeerId:PeerId? = nil, excludeGroupId: PeerGroupId? = nil, view: View? = nil, layoutChanged:(()->Void)? = nil, getColor: @escaping(Bool) -> NSColor = { _ in return theme.colors.redUI }, fontSize: CGFloat = .small, applyFilter: Bool = true, filter: ChatListFilter? = nil, removeWhenSidebar: Bool = false) { self.account = account self.excludePeerId = excludePeerId self.layoutChanged = layoutChanged - super.init(View()) + self.sharedContext = sharedContext + self.getColor = getColor + super.init(view) + + struct Result : Equatable { + let dockText: String? + let total:Int32 + init(dockText: String?, total: Int32) { + self.dockText = dockText + self.total = max(total, 0) + } + } + + var items:[UnreadMessageCountsItem] = [] + let peerSignal: Signal<(Peer, Bool)?, NoError> + + + - var items:[UnreadMessageCountsItem] = [.total] if let peerId = excludePeerId { items.append(.peer(peerId)) + let notificationKeyView: PostboxViewKey = .peerNotificationSettings(peerIds: Set([peerId])) + peerSignal = combineLatest(account.postbox.loadedPeerWithId(peerId), account.postbox.combinedView(keys: [notificationKeyView]) |> map { view in + return ((view.views[notificationKeyView] as? PeerNotificationSettingsView)?.notificationSettings[peerId])?.isRemovedFromTotalUnreadCount(default: false) ?? false + }) |> map {Optional($0)} + } else { + peerSignal = .single(nil) } - self.disposable.set((account.postbox.unreadMessageCountsView(items: items) |> deliverOnMainQueue).start(next: { [weak self] view in - if let strongSelf = self { - var count: Int32 = 0 - var totalCount:Int32 = 0 - if let total = view.count(for: .total) { - count = total - totalCount = total - } - if let excludePeerId = excludePeerId, let peerCount = view.count(for: .peer(excludePeerId)) { - count -= peerCount + + let signal: Signal<[(Int32, RenderedTotalUnreadCountType)], NoError> + if collectAllAccounts { + signal = sharedContext.activeAccountsWithInfo |> mapToSignal { primaryId, accounts in + return combineLatest(accounts.filter { $0.account.id != account.id }.map { renderedTotalUnreadCount(accountManager: sharedContext.accountManager, postbox: $0.account.postbox) }) + } + } else { + signal = renderedTotalUnreadCount(accountManager: sharedContext.accountManager, postbox: account.postbox) |> map { [$0] } + } + + var unreadCountItems: [UnreadMessageCountsItem] = [] + unreadCountItems.append(.total(nil)) + var keys: [PostboxViewKey] = [] + let unreadKey: PostboxViewKey + unreadKey = .unreadCounts(items: []) + + var s:Signal + + if let filter = filter { + s = chatListFilterItems(engine: TelegramEngine(account: account), accountManager: sharedContext.accountManager) |> map { value in + if let unread = value.count(for: filter) { + return Result(dockText: nil, total: Int32(unread.count)) + } else { + return Result(dockText: nil, total: 0) } + } |> deliverOnMainQueue + } else { + s = combineLatest(signal, account.postbox.unreadMessageCountsView(items: items), account.postbox.combinedView(keys: keys), appNotificationSettings(accountManager: sharedContext.accountManager), peerSignal) |> map { (counts, view, keysView, inAppSettings, peerSettings) in - count = max(0, count) - totalCount = max(0, totalCount) - - var dockTile:String? = nil - if totalCount > 0 { - dockTile = "\(totalCount)" + if !applyFilter || filter == nil { + var excludeTotal: Int32 = 0 + + var dockText: String? + let totalValue = !inAppSettings.badgeEnabled ? 0 : (collectAllAccounts && !inAppSettings.notifyAllAccounts ? 0 : max(0, counts.reduce(0, { $0 + $1.0 }))) + if totalValue > 0 { + dockText = "\(max(0, totalValue))" + } + + excludeTotal = totalValue + + if items.count == 1, let peerSettings = peerSettings { + if let count = view.count(for: items[0]), count > 0 { + var removable = false + switch inAppSettings.totalUnreadCountDisplayStyle { + case .raw: + removable = true + case .filtered: + if !peerSettings.1 { + removable = true + } + } + if removable { + switch inAppSettings.totalUnreadCountDisplayCategory { + case .chats: + excludeTotal -= 1 + case .messages: + excludeTotal -= count + } + } + } + } + return Result(dockText: dockText, total: excludeTotal) } - if count == 0 { + return Result(dockText: nil, total: 0) + } |> deliverOnMainQueue + } + + s = combineLatest(s, chatListFolderSettings(account.postbox)) |> map { + return Result(dockText: $0.dockText, total: $1.sidebar && removeWhenSidebar ? 0 : $0.total) + } |> deliverOnMainQueue + + self.disposable.set(s.start(next: { [weak self] result in + if let strongSelf = self { + + if result.total == 0 { strongSelf.attributedString = nil } else { - strongSelf.attributedString = .initialize(string: Int(count).prettyNumber, color: .white, font: .bold(.small)) + strongSelf.attributedString = .initialize(string: Int(result.total).prettyNumber, color: getColor(strongSelf.isSelected) != theme.colors.redUI ? theme.colors.underSelectedColor : .white, font: .bold(fontSize)) } strongSelf.layoutChanged?() - NSApplication.shared.dockTile.badgeLabel = dockTile + if dockTile { + NSApplication.shared.dockTile.badgeLabel = result.dockText + forceUpdateStatusBarIconByDockTile(sharedContext: sharedContext) + } } })) } @@ -95,14 +208,14 @@ class GlobalBadgeNode: Node { override public func draw(_ layer: CALayer, in ctx: CGContext) { if let view = view { - ctx.setFillColor(theme.colors.redUI.cgColor) + ctx.setFillColor(getColor(isSelected).cgColor) ctx.round(self.size, self.size.height/2.0) ctx.fill(layer.bounds) if let textLayout = textLayout { let focus = view.focus(textLayout.0.size) - textLayout.1.draw(focus, in: ctx, backingScaleFactor: view.backingScaleFactor) + textLayout.1.draw(focus, in: ctx, backingScaleFactor: view.backingScaleFactor, backgroundColor: view.backgroundColor) } } } @@ -112,3 +225,74 @@ class GlobalBadgeNode: Node { } } + +func forceUpdateStatusBarIconByDockTile(sharedContext: SharedAccountContext) { + if let count = Int(NSApplication.shared.dockTile.badgeLabel ?? "0") { + var color: NSColor = .black + if #available(OSX 10.14, *) { + if systemAppearance.name != .aqua { + color = .white + } + } + if #available(OSX 11.0, *) { + color = .white + } + resourcesQueue.async { + let icon = generateStatusBarIcon(count, color: color) + Queue.mainQueue().async { + sharedContext.updateStatusBarImage(icon) + } + } + + } +} + +private func generateStatusBarIcon(_ unreadCount: Int, color: NSColor) -> NSImage { + let icon = NSImage(named: "StatusIcon")! +// if unreadCount > 0 { +// return NSImage(cgImage: icon.precomposed(whitePalette.redUI), size: icon.size) +// } else { +// return icon +// } + + var string = "\(unreadCount)" + if string.count > 3 { + string = ".." + string.nsstring.substring(from: string.length - 2) + } + let attributedString = NSAttributedString.initialize(string: string, color: .white, font: .medium(8), coreText: true) + + let textLayout = TextNode.layoutText(maybeNode: nil, attributedString, nil, 1, .start, NSMakeSize(18, CGFloat.greatestFiniteMagnitude), nil, false, .center) + + let generated: CGImage? + if unreadCount > 0 { + generated = generateImage(NSMakeSize(max((textLayout.0.size.width + 4), (textLayout.0.size.height + 4)), (textLayout.0.size.height + 2)), scale: nil, rotatedContext: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + + ctx.setFillColor(NSColor.red.cgColor) + + + ctx.round(size, size.height/2.0) + ctx.fill(rect) + + let focus = NSMakePoint((rect.width - textLayout.0.size.width) / 2, (rect.height - textLayout.0.size.height) / 2) + textLayout.1.draw(NSMakeRect(focus.x, 2, textLayout.0.size.width, textLayout.0.size.height), in: ctx, backingScaleFactor: 2.0, backgroundColor: .white) + + })! + } else { + generated = nil + } + + let full = generateImage(NSMakeSize(24, 20), contextGenerator: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + + + ctx.draw(icon.precomposed(color), in: NSMakeRect((size.width - icon.size.width) / 2, 2, icon.size.width, icon.size.height)) + if let generated = generated { + ctx.draw(generated, in: NSMakeRect(rect.width - generated.size.width / System.backingScale, 0, generated.size.width / System.backingScale, generated.size.height / System.backingScale)) + } + })! + let image = NSImage(cgImage: full, size: full.backingSize) + return image +} diff --git a/Telegram-Mac/GlobalHandlers.swift b/Telegram-Mac/GlobalHandlers.swift index c4e98390e5..ab7e8007ea 100644 --- a/Telegram-Mac/GlobalHandlers.swift +++ b/Telegram-Mac/GlobalHandlers.swift @@ -7,8 +7,9 @@ // import Cocoa -import SwiftSignalKitMac -import PostboxMac -import TelegramCoreMac +import SwiftSignalKit +import Postbox +import TelegramCore -public let globalPeerHandler:Promise = Promise() + +let archiver: ArchiverContext = ArchiverContext() diff --git a/Telegram-Mac/GridHoleItem.swift b/Telegram-Mac/GridHoleItem.swift deleted file mode 100644 index 63aeddbf31..0000000000 --- a/Telegram-Mac/GridHoleItem.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// GridHoleItem.swift -// Telegram-Mac -// -// Created by keepcoder on 26/10/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import TGUIKit -final class GridHoleItem: GridItem { - func update(node: GridItemNode) { - - } - - let section: GridSection? = nil - - func node(layout: GridNodeLayout, gridNode:GridNode) -> GridItemNode { - return GridHoleItemNode(gridNode) - } -} - -class GridHoleItemNode: GridItemNode { -// private let activityIndicatorView: UIActivityIndicatorView -// -// override init() { -// self.activityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle: .gray) -// -// super.init() -// -// self.view.addSubview(self.activityIndicatorView) -// self.activityIndicatorView.startAnimating() -// } -// -// override func layout() { -// super.layout() -// -// let size = self.bounds.size -// let activityIndicatorSize = self.activityIndicatorView.bounds.size -// self.activityIndicatorView.frame = CGRect(origin: CGPoint(x: floor((size.width - activityIndicatorSize.width) / 2.0), y: floor((size.height - activityIndicatorSize.height) / 2.0)), size: activityIndicatorSize) -// } -} diff --git a/Telegram-Mac/GridMessageItem.swift b/Telegram-Mac/GridMessageItem.swift deleted file mode 100644 index 6d3c3ae2b9..0000000000 --- a/Telegram-Mac/GridMessageItem.swift +++ /dev/null @@ -1,261 +0,0 @@ -// -// GridMessageItem.swift -// Telegram-Mac -// -// Created by keepcoder on 26/10/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac - - -private func mediaForMessage(_ message: Message) -> Media? { - for media in message.media { - if let media = media as? TelegramMediaImage { - return media - } else if let file = media as? TelegramMediaFile { - if file.mimeType.hasPrefix("audio/") { - return nil - } else if !file.isVideo && file.mimeType.hasPrefix("video/") { - return file - } else { - return file - } - } - } - return nil -} - -final class GridMessageItem: GridItem { - private let account: Account - private let message: Message - private let chatInteraction: ChatInteraction - - let section: GridSection? = nil - fileprivate weak var grid:GridNode? - init(account: Account, message: Message, chatInteraction: ChatInteraction) { - self.account = account - self.message = message - self.chatInteraction = chatInteraction - } - - func update(node: GridItemNode) { - guard let node = node as? GridMessageItemNode else { - assertionFailure() - return - } - if let media = mediaForMessage(self.message) { - node.setup(account: self.account, media: media, message: self.message, chatInteraction: self.chatInteraction) - } - } - - func node(layout: GridNodeLayout, gridNode:GridNode) -> GridItemNode { - let node = GridMessageItemNode(gridNode) - if let media = mediaForMessage(self.message) { - node.setup(account: self.account, media: media, message: self.message, chatInteraction: self.chatInteraction) - } - return node - } -} - -final class GridMessageItemNode: GridItemNode { - private var currentState: (Account, Media, CGSize)? - private let imageView: TransformImageView - private var message: Message? - private var chatInteraction: ChatInteraction? - private var selectionView:SelectingControl? - private var progressView:RadialProgressView? - private var _status:MediaResourceStatus? - private let statusDisposable = MetaDisposable() - private let fetchingDisposable = MetaDisposable() - override var stableId: AnyHashable { - return (message?.chatStableId ?? ChatHistoryEntryId.undefined) - } - - - open override func menu(for event: NSEvent) -> NSMenu? { - if let message = message, let account = currentState?.0 { - let menu = ContextMenu() - - - if canForwardMessage(message, account: account) { - menu.addItem(ContextMenuItem(tr(.messageContextForward), handler: { [weak self] in - self?.chatInteraction?.forwardMessages([message.id]) - })) - } - - if canDeleteMessage(message, account: account) { - menu.addItem(ContextMenuItem(tr(.messageContextDelete), handler: { [weak self] in - self?.chatInteraction?.deleteMessages([message.id]) - })) - } - - menu.addItem(ContextMenuItem(tr(.messageContextGoto), handler: { [weak self] in - self?.chatInteraction?.focusMessageId(nil, message.id, .center(id: 0, animated: false, focus: true, inset: 0)) - })) - return menu - } - return nil - } - - override init(_ grid:GridNode) { - self.imageView = TransformImageView() - - super.init(grid) - - self.addSubview(self.imageView) - - self.set(handler: { [weak self] (control) in - if let strongSelf = self, let interactions = strongSelf.chatInteraction, let currentState = strongSelf.currentState, let message = strongSelf.message { - if interactions.presentation.state == .selecting { - interactions.update({$0.withToggledSelectedMessage(message.id)}) - strongSelf.updateSelectionState(animated: true) - } else { - if strongSelf._status == nil || strongSelf._status == .Local { - showChatGallery(account: currentState.0, message: message, strongSelf.grid, ChatMediaGalleryParameters(showMedia: {}, showMessage: { [weak interactions] message in - interactions?.focusMessageId(nil, message.id, .center(id: 0, animated: false, focus: true, inset: 0)) - }, isWebpage: false)) - } else if let file = message.media.first as? TelegramMediaFile { - if let status = strongSelf._status { - switch status { - case .Remote: - strongSelf.fetchingDisposable.set(chatMessageFileInteractiveFetched(account: currentState.0, file: file).start()) - case .Fetching: - fileCancelInteractiveFetch(account: currentState.0, file: file) - default: - break - } - } - } - - } - } - - - }, for: .Click) - } - - - required init(frame frameRect: NSRect) { - fatalError("init(frame:) has not been implemented") - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func copy() -> Any { - return imageView.copy() - } - - - func setup(account: Account, media: Media, message: Message, chatInteraction: ChatInteraction) { - if self.currentState == nil || self.currentState!.0 !== account || !self.currentState!.1.isEqual(media) { - var mediaDimensions: CGSize? - backgroundColor = theme.colors.background - statusDisposable.set(nil) - fetchingDisposable.set(nil) - - if let image = media as? TelegramMediaImage, let largestSize = largestImageRepresentation(image.representations)?.dimensions { - mediaDimensions = largestSize - self.imageView.setSignal(account: account, signal: mediaGridMessagePhoto(account: account, photo: image, scale: backingScaleFactor)) - progressView?.removeFromSuperview() - progressView = nil - } else if let file = media as? TelegramMediaFile { - mediaDimensions = file.previewRepresentations.last?.dimensions - self.imageView.setSignal(account: account, signal: mediaGridMessageVideo(account: account, file: file, scale: backingScaleFactor)) - - - statusDisposable.set((chatMessageFileStatus(account: account, file: file) |> deliverOnMainQueue).start(next: { [weak self] status in - - if let strongSelf = self { - if strongSelf.progressView == nil { - strongSelf.progressView = RadialProgressView(theme: RadialProgressTheme(backgroundColor: .blackTransparent, foregroundColor: .white, icon: playerPlayThumb)) - strongSelf.progressView?.userInteractionEnabled = false - strongSelf.addSubview(strongSelf.progressView!) - strongSelf.progressView?.center() - } - strongSelf._status = status - - switch status { - case let .Fetching(_, progress): - strongSelf.progressView?.state = .Fetching(progress: progress, force: false) - case .Remote: - strongSelf.progressView?.state = .Remote - case .Local: - strongSelf.progressView?.state = .Play - } - - } - })) - - } - - - self.currentState = (account, media, mediaDimensions ?? NSMakeSize(100, 100)) - } - - self.message = message - self.chatInteraction = chatInteraction - - self.updateSelectionState(animated: false) - - self.needsLayout = true - } - - override func layout() { - super.layout() - - let imageFrame = NSMakeRect(1, 1, bounds.width - 4, bounds.height - 4) - self.imageView.frame = imageFrame - - if let (_, _, mediaDimensions) = self.currentState { - let imageSize = mediaDimensions.aspectFilled(imageFrame.size) - self.imageView.set(arguments:TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageFrame.size, intrinsicInsets: NSEdgeInsets())) - } - if let selectionView = selectionView { - selectionView.setFrameOrigin(frame.width - selectionView.frame.width - 5, 5) - } - progressView?.center() - } - - func updateSelectionState(animated: Bool) { - if let messageId = self.message?.id, let interactions = self.chatInteraction { - if let selectionState = interactions.presentation.selectionState { - let selected = selectionState.selectedIds.contains(messageId) - - if let selectionView = self.selectionView { - selectionView.set(selected: selected, animated: animated) - } else { - selectionView = SelectingControl(unselectedImage: theme.icons.chatToggleUnselected, selectedImage: theme.icons.chatToggleSelected) - - addSubview(selectionView!) - selectionView?.set(selected: selected, animated: animated) - - } - } else { - if let selectionView = selectionView { - self.selectionView = nil - if animated { - selectionView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak selectionView] completion in - selectionView?.removeFromSuperview() - }) - } else { - selectionView.removeFromSuperview() - } - } - } - needsLayout = true - } - } - - deinit { - statusDisposable.dispose() - fetchingDisposable.dispose() - } - -} diff --git a/Telegram-Mac/GroupAdminsController.swift b/Telegram-Mac/GroupAdminsController.swift deleted file mode 100644 index bf80e79d8f..0000000000 --- a/Telegram-Mac/GroupAdminsController.swift +++ /dev/null @@ -1,420 +0,0 @@ -// -// GroupAdminsController.swift -// Telegram -// -// Created by keepcoder on 13/03/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - -import Cocoa -import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac - - -private final class GroupAdminsControllerArguments { - let account: Account - - let updateAllAreAdmins: (Bool) -> Void - let updatePeerIsAdmin: (PeerId, Bool) -> Void - - init(account: Account, updateAllAreAdmins: @escaping (Bool) -> Void, updatePeerIsAdmin: @escaping (PeerId, Bool) -> Void) { - self.account = account - self.updateAllAreAdmins = updateAllAreAdmins - self.updatePeerIsAdmin = updatePeerIsAdmin - } -} - -private enum GroupAdminsSection: Int32 { - case allAdmins - case peers -} - -private enum GroupAdminsEntryStableId: Hashable { - case index(Int32) - case section(Int) - case peer(PeerId) - - var hashValue: Int { - switch self { - case let .section(index): - return index.hashValue - case let .index(index): - return index.hashValue - case let .peer(peerId): - return peerId.hashValue - } - } - - static func ==(lhs: GroupAdminsEntryStableId, rhs: GroupAdminsEntryStableId) -> Bool { - switch lhs { - case let .index(index): - if case .index(index) = rhs { - return true - } else { - return false - } - case let .section(index): - if case .section(index) = rhs { - return true - } else { - return false - } - case let .peer(peerId): - if case .peer(peerId) = rhs { - return true - } else { - return false - } - } - } -} - -private enum GroupAdminsEntry: Comparable, Identifiable { - case allAdmins(sectionId:Int, Bool) - case allAdminsInfo(sectionId:Int, String) - case peerItem(sectionId:Int, Int32, Peer, PeerPresence?, Bool, Bool) - case section(sectionId:Int) - var stableId: GroupAdminsEntryStableId { - switch self { - case .allAdmins: - return .index(0) - case .allAdminsInfo: - return .index(1) - case let .section(sectionId): - return .section(sectionId) - case let .peerItem(_, _, peer, _, _, _): - return .peer(peer.id) - } - } - - var stableIndex: Int { - switch self { - case .allAdmins: - return 0 - case .allAdminsInfo: - return 1 - case let .section(sectionId): - return (sectionId + 1) * 1000 - sectionId - case .peerItem: - fatalError() - } - } - - static func ==(lhs: GroupAdminsEntry, rhs: GroupAdminsEntry) -> Bool { - switch lhs { - case let .allAdmins(sectionId, value): - if case .allAdmins(sectionId, value) = rhs { - return true - } else { - return false - } - case let .allAdminsInfo(sectionId, text): - if case .allAdminsInfo(sectionId, text) = rhs { - return true - } else { - return false - } - case let .section(section): - if case .section(section) = rhs { - return true - } else { - return false - } - case let .peerItem(lhsSectionId, lhsIndex, lhsPeer, lhsPresence, lhsToggled, lhsEnabled): - if case let .peerItem(rhsSectionId, rhsIndex, rhsPeer, rhsPresence, rhsToggled, rhsEnabled) = rhs { - if lhsIndex != rhsIndex { - return false - } - if lhsSectionId != rhsSectionId { - return false - } - if !lhsPeer.isEqual(rhsPeer) { - return false - } - if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { - if !lhsPresence.isEqual(to: rhsPresence) { - return false - } - } else if (lhsPresence != nil) != (rhsPresence != nil) { - return false - } - if lhsToggled != rhsToggled { - return false - } - if lhsEnabled != rhsEnabled { - return false - } - return true - } else { - return false - } - } - } - - var sortIndex:Int { - switch self { - case let .allAdmins(sectionId, _): - return (sectionId * 1000) + stableIndex - case let .allAdminsInfo(sectionId, _): - return (sectionId * 1000) + stableIndex - case let .peerItem(sectionId, index, _, _, _, _): - return (sectionId * 1000) + Int(index) + 100 - case let .section(sectionId): - return (sectionId + 1) * 1000 - sectionId - } - } - - static func <(lhs: GroupAdminsEntry, rhs: GroupAdminsEntry) -> Bool { - return lhs.sortIndex < rhs.sortIndex - } - - func item(_ arguments: GroupAdminsControllerArguments, initialSize: NSSize) -> TableRowItem { - switch self { - case let .allAdmins(_, value): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.groupAdminsAllMembersAdmins), type: .switchable(stateback: { () -> Bool in - return value - }), action: { - arguments.updateAllAreAdmins(!value) - }) - - case let .allAdminsInfo(_, text): - return GeneralTextRowItem(initialSize, stableId: stableId, text: text) - case .section: - return GeneralRowItem(initialSize, height: 20, stableId: stableId) - case let .peerItem(_, _, peer, presence, toggled, enabled): - - var string:String = tr(.peerStatusRecently) - var color:NSColor = theme.colors.grayText - - if let presence = presence as? TelegramUserPresence { - let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - (string, _, color) = stringAndActivityForUserPresence(presence, relativeTo: Int32(timestamp)) - } else if let peer = peer as? TelegramUser, let botInfo = peer.botInfo { - string = botInfo.flags.contains(.hasAccessToChatHistory) ? tr(.peerInfoBotStatusHasAccess) : tr(.peerInfoBotStatusHasNoAccess) - } - - - return ShortPeerRowItem(initialSize, peer: peer, account: arguments.account, stableId: stableId, enabled: enabled, height: 46, photoSize: NSMakeSize(36, 36), titleStyle: ControlStyle(font: .medium(.custom(12.5))), statusStyle: ControlStyle(font: NSFont.normal(.custom(12.5)), foregroundColor:color), status: string, drawLastSeparator: true, inset:NSEdgeInsets(left:30.0,right:30.0), generalType: .switchable(stateback: { () -> Bool in - return toggled - }), action: { - arguments.updatePeerIsAdmin(peer.id, !toggled) - }) - - } - } -} - -private struct GroupAdminsControllerState: Equatable { - let updatingAllAdminsValue: Bool? - let updatedAllAdminsValue: Bool? - - let updatingAdminValue: [PeerId: Bool] - - init() { - self.updatingAllAdminsValue = nil - self.updatedAllAdminsValue = nil - self.updatingAdminValue = [:] - } - - init(updatingAllAdminsValue: Bool?, updatedAllAdminsValue: Bool?, updatingAdminValue: [PeerId: Bool]) { - self.updatingAllAdminsValue = updatingAllAdminsValue - self.updatedAllAdminsValue = updatedAllAdminsValue - self.updatingAdminValue = updatingAdminValue - } - - static func ==(lhs: GroupAdminsControllerState, rhs: GroupAdminsControllerState) -> Bool { - if lhs.updatingAllAdminsValue != rhs.updatingAllAdminsValue { - return false - } - if lhs.updatedAllAdminsValue != rhs.updatedAllAdminsValue { - return false - } - if lhs.updatingAdminValue != rhs.updatingAdminValue { - return false - } - - return true - } - - func withUpdatedUpdatingAllAdminsValue(_ updatingAllAdminsValue: Bool?) -> GroupAdminsControllerState { - return GroupAdminsControllerState(updatingAllAdminsValue: updatingAllAdminsValue, updatedAllAdminsValue: self.updatedAllAdminsValue, updatingAdminValue: self.updatingAdminValue) - } - - func withUpdatedUpdatedAllAdminsValue(_ updatedAllAdminsValue: Bool?) -> GroupAdminsControllerState { - return GroupAdminsControllerState(updatingAllAdminsValue: self.updatingAllAdminsValue, updatedAllAdminsValue: updatedAllAdminsValue, updatingAdminValue: self.updatingAdminValue) - } - - func withUpdatedUpdatingAdminValue(_ updatingAdminValue: [PeerId: Bool]) -> GroupAdminsControllerState { - return GroupAdminsControllerState(updatingAllAdminsValue: self.updatingAllAdminsValue, updatedAllAdminsValue: self.updatedAllAdminsValue, updatingAdminValue: updatingAdminValue) - } -} - -private func groupAdminsControllerEntries(account: Account, view: PeerView, state: GroupAdminsControllerState) -> [GroupAdminsEntry] { - var entries: [GroupAdminsEntry] = [] - - if let peer = view.peers[view.peerId] as? TelegramGroup, let cachedData = view.cachedData as? CachedGroupData, let participants = cachedData.participants { - - var sectionId:Int = 1 - entries.append(.section(sectionId: sectionId)) - sectionId += 1 - - let effectiveAdminsEnabled: Bool - if let updatingAllAdminsValue = state.updatingAllAdminsValue { - effectiveAdminsEnabled = updatingAllAdminsValue - } else { - effectiveAdminsEnabled = peer.flags.contains(.adminsEnabled) - } - - entries.append(.allAdmins(sectionId: sectionId, !effectiveAdminsEnabled)) - if effectiveAdminsEnabled { - entries.append(.allAdminsInfo(sectionId: sectionId, tr(.groupAdminsDescAdminInvites))) - } else { - entries.append(.allAdminsInfo(sectionId: sectionId, tr(.groupAdminsDescAllInvites))) - } - - entries.append(.section(sectionId: sectionId)) - sectionId += 1 - - - var index: Int32 = 0 - for participant in participants.participants.sorted(by: <) { - if let peer = view.peers[participant.peerId] { - var isAdmin = false - var isEnabled = true - if !effectiveAdminsEnabled { - isAdmin = true - isEnabled = false - } else { - switch participant { - case .creator: - isAdmin = true - isEnabled = false - case .admin: - if let value = state.updatingAdminValue[peer.id] { - isAdmin = value - } else { - isAdmin = true - } - case .member: - if let value = state.updatingAdminValue[peer.id] { - isAdmin = value - } else { - isAdmin = false - } - } - } - entries.append(.peerItem(sectionId: sectionId, index, peer, view.peerPresences[participant.peerId], isAdmin, isEnabled)) - index += 1 - } - } - } - - return entries -} - -private func prepareTransition(left:[AppearanceWrapperEntry], right:[AppearanceWrapperEntry], arguments: GroupAdminsControllerArguments, initialSize: NSSize) -> TableUpdateTransition { - let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right, { entry -> TableRowItem in - return entry.entry.item(arguments, initialSize: initialSize) - }) - - return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true) -} - - -class GroupAdminsController : TableViewController { - private let peerId:PeerId - init(account:Account, peerId:PeerId) { - self.peerId = peerId - super.init(account) - } - - override func viewDidLoad() { - super.viewDidLoad() - - let account = self.account - let peerId = self.peerId - - let statePromise = ValuePromise(GroupAdminsControllerState(), ignoreRepeated: true) - let stateValue = Atomic(value: GroupAdminsControllerState()) - let updateState: ((GroupAdminsControllerState) -> GroupAdminsControllerState) -> Void = { f in - statePromise.set(stateValue.modify { f($0) }) - } - - let actionsDisposable = DisposableSet() - - let toggleAllAdminsDisposable = MetaDisposable() - actionsDisposable.add(toggleAllAdminsDisposable) - - let toggleAdminsDisposables = DisposableDict() - actionsDisposable.add(toggleAdminsDisposables) - - let arguments = GroupAdminsControllerArguments(account: account, updateAllAreAdmins: { value in - updateState { state in - return state.withUpdatedUpdatingAllAdminsValue(value) - } - toggleAllAdminsDisposable.set((updateGroupManagementType(account: account, peerId: peerId, type: value ? .unrestricted : .restrictedToAdmins) |> deliverOnMainQueue).start(error: { - updateState { state in - return state.withUpdatedUpdatingAllAdminsValue(nil) - } - }, completed: { - updateState { state in - return state.withUpdatedUpdatingAllAdminsValue(nil).withUpdatedUpdatedAllAdminsValue(value) - } - })) - }, updatePeerIsAdmin: { memberId, value in - updateState { state in - var updatingAdminValue = state.updatingAdminValue - updatingAdminValue[memberId] = value - return state.withUpdatedUpdatingAdminValue(updatingAdminValue) - } - - if value { - toggleAdminsDisposables.set((addPeerAdmin(account: account, peerId: peerId, adminId: memberId) |> deliverOnMainQueue).start(error: { _ in - updateState { state in - var updatingAdminValue = state.updatingAdminValue - updatingAdminValue.removeValue(forKey: memberId) - return state.withUpdatedUpdatingAdminValue(updatingAdminValue) - } - }, completed: { - updateState { state in - var updatingAdminValue = state.updatingAdminValue - updatingAdminValue.removeValue(forKey: memberId) - return state.withUpdatedUpdatingAdminValue(updatingAdminValue) - } - }), forKey: memberId) - } else { - toggleAdminsDisposables.set((removePeerAdmin(account: account, peerId: peerId, adminId: memberId) |> deliverOnMainQueue).start(error: { _ in - updateState { state in - var updatingAdminValue = state.updatingAdminValue - updatingAdminValue.removeValue(forKey: memberId) - return state.withUpdatedUpdatingAdminValue(updatingAdminValue) - } - }, completed: { - updateState { state in - var updatingAdminValue = state.updatingAdminValue - updatingAdminValue.removeValue(forKey: memberId) - return state.withUpdatedUpdatingAdminValue(updatingAdminValue) - } - }), forKey: memberId) - } - }) - - let peerView = account.viewTracker.peerView(peerId) - let previous:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) - let initialSize = self.atomicSize - - genericView.merge(with: combineLatest(statePromise.get(), peerView, appearanceSignal) |> deliverOnMainQueue - |> map { state, view, appearance -> TableUpdateTransition in - let entries = groupAdminsControllerEntries(account: account, view: view, state: state).map{AppearanceWrapperEntry(entry: $0, appearance: appearance)} - return prepareTransition(left: previous.swap(entries), right: entries, arguments: arguments, initialSize: initialSize.modify({$0})) - - } |> afterDisposed { - actionsDisposable.dispose() - }) - readyOnce() - - } -} - diff --git a/Telegram-Mac/GroupCallAvatarView.swift b/Telegram-Mac/GroupCallAvatarView.swift new file mode 100644 index 0000000000..d27196b369 --- /dev/null +++ b/Telegram-Mac/GroupCallAvatarView.swift @@ -0,0 +1,140 @@ +// +// GroupCallAvatarView.swift +// Telegram +// +// Created by Mikhail Filimonov on 25.05.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit + +import Postbox +import TelegramCore + + +final class GroupCallAvatarView : View { + private let playbackAudioLevelView: VoiceBlobView + private var scaleAnimator: DisplayLinkAnimator? + private let photoView: AvatarControl = AvatarControl(font: .avatar(20)) + private let audioLevelDisposable = MetaDisposable() + let photoSize: NSSize + init(frame frameRect: NSRect, photoSize: NSSize) { + playbackAudioLevelView = VoiceBlobView( + frame: frameRect.size.bounds, + maxLevel: 0.3, + smallBlobRange: (0, 0), + mediumBlobRange: (0.7, 0.8), + bigBlobRange: (0.8, 0.9) + ) + self.photoSize = photoSize + super.init(frame: frameRect) + photoView.setFrameSize(photoSize) + addSubview(playbackAudioLevelView) + addSubview(photoView) + + self.isEventLess = true + playbackAudioLevelView.isEventLess = true + photoView.userInteractionEnabled = false + } + + deinit { + audioLevelDisposable.dispose() + } + + func update(_ audioLevel:(PeerId)->Signal?, data: PeerGroupCallData, activityColor: NSColor, account: Account, animated: Bool) { + self.timestamp = nil + if let audioLevel = audioLevel(data.peer.id), data.state?.muteState == nil { + self.audioLevelDisposable.set(audioLevel.start(next: { [weak self] value in + self?.updateAudioLevel(value, data: data, animated: animated) + })) + } else { + self.audioLevelDisposable.set(nil) + self.updateAudioLevel(nil, data: data, animated: animated) + } + + playbackAudioLevelView.setColor(activityColor) + photoView.setPeer(account: account, peer: data.peer, message: nil, size: NSMakeSize(floor(photoSize.width * 1.5), floor(photoSize.height * 1.5))) + } + + private var value: Float? = nil + + private var timestamp: TimeInterval? + + private func updateAudioLevel(_ value: Float?, data: PeerGroupCallData, animated: Bool) { + if let timestamp = self.timestamp { + if CACurrentMediaTime() - timestamp < 0.100 { + return + } + } + self.timestamp = CACurrentMediaTime() + + if (value != nil || data.isSpeaking) { + playbackAudioLevelView.startAnimating() + } else { + playbackAudioLevelView.stopAnimating() + } + playbackAudioLevelView.change(opacity: (value != nil || data.isSpeaking) ? 1 : 0, animated: animated) + + + if value != self.value { + let value = value != nil ? Float(truncate(double: Double(value ?? 0), places: 2)) : nil + + self.value = value + + playbackAudioLevelView.updateLevel(CGFloat(value ?? 0)) + + let audioLevel = value ?? 0 + let level = min(1.0, max(0.0, CGFloat(audioLevel))) + let avatarScale: CGFloat + if audioLevel > 0.0 { + avatarScale = 0.9 + level * 0.07 + } else { + avatarScale = 1.0 + } + + + let valueScale = CGFloat(truncate(double: Double(avatarScale), places: 2)) + + let t = photoView.layer!.transform + let scale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) + + if animated { + self.scaleAnimator = DisplayLinkAnimator(duration: 0.1, from: scale, to: valueScale, update: { [weak self] value in + guard let `self` = self else { + return + } + let rect = self.photoView.bounds + var fr = CATransform3DIdentity + fr = CATransform3DTranslate(fr, rect.width / 2, rect.height / 2, 0) + fr = CATransform3DScale(fr, value, value, 1) + fr = CATransform3DTranslate(fr, -(rect.width / 2), -(rect.height / 2), 0) + self.photoView.layer?.transform = fr + }, completion: { + + }) + } else { + self.scaleAnimator = nil + self.photoView.layer?.transform = CATransform3DIdentity + } + } + + + } + + + override func layout() { + super.layout() + photoView.center() + playbackAudioLevelView.center() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } +} diff --git a/Telegram-Mac/GroupCallContextMenuHeader.swift b/Telegram-Mac/GroupCallContextMenuHeader.swift new file mode 100644 index 0000000000..5c72b19ad6 --- /dev/null +++ b/Telegram-Mac/GroupCallContextMenuHeader.swift @@ -0,0 +1,233 @@ +// +// GroupCallContextMenuHeader.swift +// Telegram +// +// Created by Mikhail Filimonov on 20.05.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import Postbox +import TelegramCore + +import SwiftSignalKit + +private final class PhotoOrVideoView: View { + private let imageView: TransformImageView + + + private var photoVideoView: MediaPlayerView? + private var photoVideoPlayer: MediaPlayer? + + private var videoView: NSView? + + required init(frame frameRect: NSRect) { + imageView = TransformImageView(frame: frameRect.size.bounds) + super.init(frame: frameRect) + addSubview(imageView) + } + + override func layout() { + super.layout() + imageView.frame = bounds + photoVideoView?.frame = imageView.frame + videoView?.frame = imageView.frame + } + + deinit { + } + + func setPeer(_ peer: Peer, peerPhoto: TelegramPeerPhoto?, video: NSView?, account: Account) { + + self.videoView = video + + if let video = video { + self.photoVideoPlayer = nil + self.photoVideoView?.removeFromSuperview() + self.photoVideoView = nil + addSubview(video) + } else if let first = peerPhoto, let video = first.image.videoRepresentations.last { + self.photoVideoView?.removeFromSuperview() + self.photoVideoView = nil + + self.photoVideoView = MediaPlayerView(backgroundThread: true) + + addSubview(photoVideoView!, positioned: .above, relativeTo: self.imageView) + + self.photoVideoView!.isEventLess = true + + self.photoVideoView!.frame = self.imageView.frame + + let file = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: first.image.representations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: video.resource.size, attributes: []) + + + let mediaPlayer = MediaPlayer(postbox: account.postbox, reference: MediaResourceReference.standalone(resource: file.resource), streamable: true, video: true, preferSoftwareDecoding: false, enableSound: false, fetchAutomatically: true) + + mediaPlayer.actionAtEnd = .loop(nil) + + self.photoVideoPlayer = mediaPlayer + + if let seekTo = video.startTimestamp { + mediaPlayer.seek(timestamp: seekTo) + } + mediaPlayer.attachPlayerView(self.photoVideoView!) + mediaPlayer.play() + } else { + self.photoVideoPlayer = nil + self.photoVideoView?.removeFromSuperview() + self.photoVideoView = nil + + let profileImageRepresentations:[TelegramMediaImageRepresentation] + if let peer = peer as? TelegramChannel { + profileImageRepresentations = peer.profileImageRepresentations + } else if let peer = peer as? TelegramUser { + profileImageRepresentations = peer.profileImageRepresentations + } else if let peer = peer as? TelegramGroup { + profileImageRepresentations = peer.profileImageRepresentations + } else { + profileImageRepresentations = [] + } + + let id = profileImageRepresentations.first?.resource.id.hashValue ?? Int(peer.id.toInt64()) + let media = peerPhoto?.image ?? TelegramMediaImage(imageId: MediaId(namespace: 0, id: MediaId.Id(id)), representations: profileImageRepresentations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + + + if let dimension = profileImageRepresentations.last?.dimensions.size { + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: dimension, boundingSize: frame.size, intrinsicInsets: NSEdgeInsets()) + self.imageView.setSignal(signal: cachedMedia(media: media, arguments: arguments, scale: self.backingScaleFactor), clearInstantly: false) + self.imageView.setSignal(chatMessagePhoto(account: account, imageReference: ImageMediaReference.standalone(media: media), peer: peer, scale: self.backingScaleFactor), clearInstantly: false, animate: true, cacheImage: { result in + cacheMedia(result, media: media, arguments: arguments, scale: System.backingScale) + }) + self.imageView.set(arguments: arguments) + + if let reference = PeerReference(peer) { + _ = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: .avatar(peer: reference, resource: media.representations.last!.resource)).start() + } + } else { + self.imageView.setSignal(signal: generateEmptyRoundAvatar(self.imageView.frame.size, font: .avatar(90.0), account: account, peer: peer) |> map { TransformImageResult($0, true) }) + } + } + + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + +final class GroupCallContextMenuHeaderView : View { + private let nameView = TextView() + private var descView: TextView? + + let peerPhotosDisposable = MetaDisposable() + + + private let slider: SliderView = SliderView(frame: .zero) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(slider) + addSubview(nameView) + slider.layer?.cornerRadius = 4 + nameView.isSelectable = false + nameView.userInteractionEnabled = false + } + + deinit { + peerPhotosDisposable.dispose() + } + + override func layout() { + super.layout() + slider.frame = NSMakeSize(frame.width, 190).bounds.insetBy(dx: 5, dy: 0) + nameView.setFrameOrigin(NSMakePoint(14, slider.frame.maxY + 5)) + descView?.setFrameOrigin(NSMakePoint(14, nameView.frame.maxY + 3)) + } + + func setPeer(_ peer: Peer, about: String?, context: AccountContext, videos: [NSView]) { + + + let name = TextViewLayout(.initialize(string: peer.displayTitle, color: GroupCallTheme.customTheme.textColor, font: .medium(.text)), maximumNumberOfLines: 2) + name.measure(width: frame.width - 28) + + nameView.update(name) + + if let about = about { + self.descView = TextView() + descView?.isSelectable = false + descView?.userInteractionEnabled = false + addSubview(self.descView!) + let desc = TextViewLayout(.initialize(string: about, color: GroupCallTheme.customTheme.grayTextColor, font: .normal(.text))) + desc.measure(width: frame.width - 28) + self.descView?.update(desc) + + } else { + self.descView?.removeFromSuperview() + self.descView = nil + } + + setFrameSize(NSMakeSize(frame.width, 190 + name.layoutSize.height + 10 + (descView != nil ? descView!.frame.height + 3 : 0))) + layout() + + var photos = Array(syncPeerPhotos(peerId: peer.id).prefix(10)) + let signal = peerPhotos(context: context, peerId: peer.id, force: true) |> deliverOnMainQueue + + for video in videos { + let view = PhotoOrVideoView(frame: self.slider.bounds) + view.setPeer(peer, peerPhoto: nil, video: video, account: context.account) + self.slider.addSlide(view) + } + + + let view = PhotoOrVideoView(frame: self.slider.bounds) + view.setPeer(peer, peerPhoto: nil, video: nil, account: context.account) + self.slider.addSlide(view) + + if photos.isEmpty { + + peerPhotosDisposable.set(signal.start(next: { [weak self, weak view] photos in + guard let `self` = self else { + return + } + + var photos = Array(photos.prefix(10)) + + if !photos.isEmpty { + let first = photos.removeFirst() + if !first.image.videoRepresentations.isEmpty { + photos.insert(first, at: 0) + self.slider.removeSlide(view) + } + } + for photo in photos { + let view = PhotoOrVideoView(frame: self.slider.bounds) + view.setPeer(peer, peerPhoto: photo, video: nil, account: context.account) + self.slider.addSlide(view) + } + })) + } else { + if !photos.isEmpty { + let first = photos.removeFirst() + if !first.image.videoRepresentations.isEmpty { + photos.insert(first, at: 0) + self.slider.removeSlide(view) + } + } + for photo in photos { + let view = PhotoOrVideoView(frame: self.slider.bounds) + view.setPeer(peer, peerPhoto: photo, video: nil, account: context.account) + self.slider.addSlide(view) + } + } + + + + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/GroupCallController.swift b/Telegram-Mac/GroupCallController.swift new file mode 100644 index 0000000000..3fd2e287e4 --- /dev/null +++ b/Telegram-Mac/GroupCallController.swift @@ -0,0 +1,2179 @@ +// +// GroupCallController.swift +// Telegram +// +// Created by Mikhail Filimonov on 22/11/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit +import Postbox + +import TelegramCore +import HotKey +import TgVoipWebrtc + + +final class GroupCallsConfig { + let videoLimit: Int + init(_ config: AppConfiguration) { + if let data = config.data, let value = data["groupcall_video_participants_max"] as? Double { + self.videoLimit = Int(value) + } else { + videoLimit = 30 + } + } +} + +private struct Tooltips : Equatable { + var dismissed: Set + var speachDetected: Bool + + static var initialValue: Tooltips { + return Tooltips(dismissed: Set(), speachDetected: false) + } +} + +final class GroupCallUIArguments { + let leave:()->Void + let settings:()->Void + let invite:(PeerId)->Void + let mute:(PeerId, Bool)->Void + let toggleSpeaker:()->Void + let remove:(Peer)->Void + let openInfo: (Peer)->Void + let inviteMembers:()->Void + let shareSource:(VideoSourceMacMode, Bool)->Void + let takeVideo:(PeerId, VideoSourceMacMode?, GroupCallUIState.ActiveVideo.Mode)->NSView? + let isSharingVideo:(PeerId)->Bool + let isSharingScreencast:(PeerId)->Bool + let canUnpinVideo:(PeerId, VideoSourceMacMode)->Bool + let setVolume: (PeerId, Double, Bool) -> Void + let pinVideo:(DominantVideo)->Void + let unpinVideo:()->Void + let isPinnedVideo:(PeerId, VideoSourceMacMode)->Bool + let getAccountPeerId: ()->PeerId? + let getAccount:()->Account + let cancelShareScreencast: ()->Void + let cancelShareVideo: ()->Void + let toggleRaiseHand:()->Void + let recordClick:(PresentationGroupCallState)->Void + let audioLevel:(PeerId)->Signal? + let startVoiceChat:()->Void + let toggleReminder:(Bool)->Void + let toggleScreenMode:()->Void + let switchCamera:(PeerGroupCallData)->Void + let togglePeersHidden:()->Void + let contextMenuItems:(PeerGroupCallData)->[ContextMenuItem] + let dismissTooltip:(GroupCallUIState.ControlsTooltip)->Void + let focusVideo: (String?)->Void + let takeTileView:() -> (NSSize, GroupCallTileView)? + let getSource:(VideoSourceMacMode)->VideoSourceMac? + init(leave:@escaping()->Void, + settings:@escaping()->Void, + invite:@escaping(PeerId)->Void, + mute:@escaping(PeerId, Bool)->Void, + toggleSpeaker:@escaping()->Void, + remove:@escaping(Peer)->Void, + openInfo: @escaping(Peer)->Void, + inviteMembers:@escaping()->Void, + shareSource: @escaping(VideoSourceMacMode, Bool)->Void, + takeVideo:@escaping(PeerId, VideoSourceMacMode?, GroupCallUIState.ActiveVideo.Mode)->NSView?, + isSharingVideo: @escaping(PeerId)->Bool, + isSharingScreencast: @escaping(PeerId)->Bool, + canUnpinVideo:@escaping(PeerId, VideoSourceMacMode)->Bool, + pinVideo:@escaping(DominantVideo)->Void, + unpinVideo:@escaping()->Void, + isPinnedVideo:@escaping(PeerId, VideoSourceMacMode)->Bool, + setVolume: @escaping(PeerId, Double, Bool)->Void, + getAccountPeerId: @escaping()->PeerId?, + getAccount: @escaping() -> Account, + cancelShareScreencast: @escaping()->Void, + cancelShareVideo: @escaping()->Void, + toggleRaiseHand:@escaping()->Void, + recordClick:@escaping(PresentationGroupCallState)->Void, + audioLevel:@escaping(PeerId)->Signal?, + startVoiceChat:@escaping()->Void, + toggleReminder:@escaping(Bool)->Void, + toggleScreenMode:@escaping()->Void, + switchCamera:@escaping(PeerGroupCallData)->Void, + togglePeersHidden: @escaping()->Void, + contextMenuItems:@escaping(PeerGroupCallData)->[ContextMenuItem], + dismissTooltip:@escaping(GroupCallUIState.ControlsTooltip)->Void, + focusVideo: @escaping(String?)->Void, + takeTileView:@escaping() -> (NSSize, GroupCallTileView)?, + getSource:@escaping(VideoSourceMacMode)->VideoSourceMac?) { + self.leave = leave + self.invite = invite + self.mute = mute + self.settings = settings + self.toggleSpeaker = toggleSpeaker + self.remove = remove + self.openInfo = openInfo + self.inviteMembers = inviteMembers + self.shareSource = shareSource + self.takeVideo = takeVideo + self.isSharingVideo = isSharingVideo + self.isSharingScreencast = isSharingScreencast + self.canUnpinVideo = canUnpinVideo + self.pinVideo = pinVideo + self.unpinVideo = unpinVideo + self.isPinnedVideo = isPinnedVideo + self.setVolume = setVolume + self.getAccountPeerId = getAccountPeerId + self.getAccount = getAccount + self.cancelShareVideo = cancelShareVideo + self.cancelShareScreencast = cancelShareScreencast + self.toggleRaiseHand = toggleRaiseHand + self.recordClick = recordClick + self.audioLevel = audioLevel + self.startVoiceChat = startVoiceChat + self.toggleReminder = toggleReminder + self.toggleScreenMode = toggleScreenMode + self.switchCamera = switchCamera + self.togglePeersHidden = togglePeersHidden + self.contextMenuItems = contextMenuItems + self.dismissTooltip = dismissTooltip + self.focusVideo = focusVideo + self.takeTileView = takeTileView + self.getSource = getSource + } +} + + + + +struct PeerGroupCallData : Equatable, Comparable { + + + struct AudioLevel { + let timestamp: Int32 + let value: Float + } + + struct PinnedMode : Equatable { + let mode: VideoSourceMacMode + let temporary: Bool + + var viceVersa: VideoSourceMacMode { + return mode.viceVersa + } + } + + struct ActiveVideo : Hashable { + let endpoint: String + let index: Int + func hash(into hasher: inout Hasher) { + hasher.combine(self.endpoint) + hasher.combine(self.index) + } + } + + let peer: Peer + let state: GroupCallParticipantsContext.Participant? + let isSpeaking: Bool + let isInvited: Bool + let unsyncVolume: Int32? + let accountPeerId: PeerId + let accountAbout: String? + let canManageCall: Bool + let hideWantsToSpeak: Bool + let activityTimestamp: Int + let firstTimestamp: Int32 + let videoMode: Bool + let isVertical: Bool + let hasVideo: Bool + let activeVideos:Set + let adminIds: Set + let isFullscreen: Bool + + let videoEndpointId: String? + let presentationEndpointId: String? + let visualEffects: Bool + + var isRaisedHand: Bool { + return self.state?.hasRaiseHand == true + } + var wantsToSpeak: Bool { + return isRaisedHand && !hideWantsToSpeak + } + + var videoEndpoint: String? { + return activeVideos.first(where: { $0.endpoint == videoEndpointId })?.endpoint + } + var presentationEndpoint: String? { + return activeVideos.first(where: { $0.endpoint == presentationEndpointId })?.endpoint + } + + var about: String? { + let about: String? + if self.peer.id == accountPeerId { + about = accountAbout + } else { + about = self.state?.about + } + if let about = about, about.isEmpty { + return nil + } else { + return about + } + } + + func isVideoPaused(_ endpointId: String?) -> Bool { + if self.state?.videoDescription?.endpointId == endpointId { + return self.state?.videoDescription?.isPaused == true + } + if self.state?.presentationDescription?.endpointId == endpointId { + return self.state?.presentationDescription?.isPaused == true + } + return false + } + + func videoStatus(_ mode: VideoSourceMacMode) -> String { + var string:String = L10n.voiceChatStatusListening + switch mode { + case .video: + string = self.status.0 + if string == self.about { + string = L10n.voiceChatStatusListening + } + case .screencast: + string = L10n.voiceChatStatusScreensharing + } + return string + } + + var status: (String, NSColor) { + var string:String = L10n.peerStatusRecently + var color:NSColor = GroupCallTheme.grayStatusColor + if let state = state { + if wantsToSpeak, let _ = state.muteState { + string = L10n.voiceChatStatusWantsSpeak + color = GroupCallTheme.blueStatusColor + } else if let muteState = state.muteState, muteState.mutedByYou { + string = muteState.mutedByYou ? L10n.voiceChatStatusMutedForYou : L10n.voiceChatStatusMuted + color = GroupCallTheme.speakLockedColor + } else if isSpeaking, state.muteState == nil { + string = L10n.voiceChatStatusSpeaking + color = GroupCallTheme.greenStatusColor + } else { + if let about = about { + string = about + color = GroupCallTheme.grayStatusColor + } else { + string = L10n.voiceChatStatusListening + color = GroupCallTheme.grayStatusColor + } + } + } else if peer.id == accountPeerId { + if let about = about { + string = about + color = GroupCallTheme.grayStatusColor.withAlphaComponent(0.6) + } else { + string = L10n.voiceChatStatusConnecting.lowercased() + color = GroupCallTheme.grayStatusColor.withAlphaComponent(0.6) + } + } else if isInvited { + string = L10n.voiceChatStatusInvited + } + return (string, color) + } + + static func ==(lhs: PeerGroupCallData, rhs: PeerGroupCallData) -> Bool { + if !lhs.peer.isEqual(rhs.peer) { + return false + } + if lhs.activityTimestamp != rhs.activityTimestamp { + return false + } + if lhs.state != rhs.state { + return false + } + if lhs.isSpeaking != rhs.isSpeaking { + return false + } + if lhs.isInvited != rhs.isInvited { + return false + } + if lhs.unsyncVolume != rhs.unsyncVolume { + return false + } + if lhs.firstTimestamp != rhs.firstTimestamp { + return false + } + if lhs.accountPeerId != rhs.accountPeerId { + return false + } + if lhs.accountAbout != rhs.accountAbout { + return false + } + if lhs.hideWantsToSpeak != rhs.hideWantsToSpeak { + return false + } + if lhs.videoMode != rhs.videoMode { + return false + } + if lhs.isVertical != rhs.isVertical { + return false + } + if lhs.hasVideo != rhs.hasVideo { + return false + } + if lhs.activeVideos != rhs.activeVideos { + return false + } + if lhs.adminIds != rhs.adminIds { + return false + } + + if lhs.videoEndpointId != rhs.videoEndpointId { + return false + } + if lhs.presentationEndpointId != rhs.presentationEndpointId { + return false + } + if lhs.isFullscreen != rhs.isFullscreen { + return false + } + if lhs.visualEffects != rhs.visualEffects { + return false + } + return true + } + + static func <(lhs: PeerGroupCallData, rhs: PeerGroupCallData) -> Bool { + if lhs.activityTimestamp != rhs.activityTimestamp { + return lhs.activityTimestamp > rhs.activityTimestamp + } + return lhs.firstTimestamp > rhs.firstTimestamp + } +} + + + + + +private func _id_peer_id(_ data: PeerGroupCallData, endpoint: String? = nil) -> InputDataIdentifier { + if let endpoint = endpoint { + return InputDataIdentifier("_peer_id_\(data.peer.id.toInt64())_\(endpoint)") + } else { + return InputDataIdentifier("_peer_id_\(data.peer.id.toInt64())_") + } +} + +private func makeState(previous:GroupCallUIState?, peerView: PeerView, state: PresentationGroupCallState, isMuted: Bool, invitedPeers: [Peer], peerStates: PresentationGroupCallMembers?, myAudioLevel: Float, summaryState: PresentationGroupCallSummaryState?, voiceSettings: VoiceCallSettings, isWindowVisible: Bool, accountPeer: (Peer, String?), unsyncVolumes: [PeerId: Int32], pinnedData: GroupCallUIState.PinnedData, hideWantsToSpeak: Set, isFullScreen: Bool, videoSources: GroupCallUIState.VideoSources, activeVideoViews: [GroupCallUIState.ActiveVideo], hideParticipants: Bool, tooltips: Tooltips, version: Int) -> GroupCallUIState { + + var memberDatas: [PeerGroupCallData] = [] + + let accountPeerId = accountPeer.0.id + let accountPeerAbout = accountPeer.1 + var activeParticipants: [GroupCallParticipantsContext.Participant] = [] + + activeParticipants = peerStates?.participants ?? [] + var index: Int = 0 + let startIndex: Int = (Int.max - 10000000000) + + + + var current: DominantVideo? + + let dominantSpeaker = pinnedData.permanent ?? pinnedData.focused?.id + let videoExists = activeVideoViews.contains(where: { $0.endpointId == dominantSpeaker && $0.mode == .main }) + if let dominantSpeaker = dominantSpeaker, videoExists { + let peer = activeParticipants.first(where: { $0.videoEndpointId == dominantSpeaker || $0.presentationEndpointId == dominantSpeaker }) + + if let peer = peer { + let pinMode: DominantVideo.PinMode = dominantSpeaker == pinnedData.permanent ? .permanent : .focused + current = .init(peer.peer.id, dominantSpeaker, peer.videoEndpointId == dominantSpeaker ? .video : .screencast, pinMode) + } + } + + + func hasVideo(_ peerId: PeerId) -> Bool { + return activeParticipants.first(where: { participant in + if participant.peer.id != peerId { + return false + } + if let endpointId = participant.presentationEndpointId { + if activeVideoViews.contains(where: { $0.endpointId == endpointId && $0.mode == .list }) { + return true + } + } + if let endpointId = participant.videoEndpointId { + if activeVideoViews.contains(where: { $0.endpointId == endpointId && $0.mode == .list }) { + return true + } + } + return false + }) != nil + } + + + if !activeParticipants.contains(where: { $0.peer.id == accountPeerId }) { + + memberDatas.append(PeerGroupCallData(peer: accountPeer.0, state: nil, isSpeaking: false, isInvited: false, unsyncVolume: unsyncVolumes[accountPeerId], accountPeerId: accountPeerId, accountAbout: accountPeerAbout, canManageCall: state.canManageCall, hideWantsToSpeak: hideWantsToSpeak.contains(accountPeerId), activityTimestamp: startIndex - 1 - index, firstTimestamp: 0, videoMode: !activeVideoViews.isEmpty, isVertical: false, hasVideo: hasVideo(accountPeerId), activeVideos: Set(), adminIds: state.adminIds, isFullscreen: isFullScreen, videoEndpointId: nil, presentationEndpointId: nil, visualEffects: voiceSettings.visualEffects)) + index += 1 + } + + let addMember:(GroupCallParticipantsContext.Participant, Int, Bool)->Void = { value, activityIndex, isVertical in + var isSpeaking = peerStates?.speakingParticipants.contains(value.peer.id) ?? false + if accountPeerId == value.peer.id, isMuted { + isSpeaking = false + } + let activeVideos = Set(activeVideoViews.filter({ active in + return active.endpointId == value.presentationEndpointId || active.endpointId == value.videoEndpointId + }).map { PeerGroupCallData.ActiveVideo(endpoint: $0.endpointId, index: $0.index) }) + + + + memberDatas.append(PeerGroupCallData(peer: value.peer, state: value, isSpeaking: isSpeaking, isInvited: false, unsyncVolume: unsyncVolumes[value.peer.id], accountPeerId: accountPeerId, accountAbout: accountPeerAbout, canManageCall: state.canManageCall, hideWantsToSpeak: hideWantsToSpeak.contains(value.peer.id), activityTimestamp: activityIndex, firstTimestamp: value.joinTimestamp, videoMode: !activeVideoViews.isEmpty, isVertical: isVertical, hasVideo: hasVideo(value.peer.id), activeVideos: activeVideos, adminIds: state.adminIds, isFullscreen: isFullScreen, videoEndpointId: value.videoEndpointId, presentationEndpointId: value.presentationEndpointId, visualEffects: voiceSettings.visualEffects)) + } + + var indexes:[PeerId: Int] = [:] + + for value in activeParticipants { + + var peerIndex = startIndex - 1 - index + if activeVideoViews.contains(where: { $0.endpointId == value.videoEndpointId || $0.endpointId == value.presentationEndpointId }) { + peerIndex += 1000 + } + indexes[value.peer.id] = peerIndex + index += 1 + } + + + let vertical = activeParticipants.filter { member in + var isVertical = isFullScreen && current != nil && hasVideo(member.peer.id) + if isVertical, current?.peerId == member.peer.id { + if member.videoEndpointId == nil || member.presentationEndpointId == nil { + isVertical = false + } + } + return isVertical + } + let rest = activeParticipants.filter { member in + return !vertical.contains(where: { $0.peer.id == member.peer.id }) + } + + + for value in vertical { + var activityIndex: Int = indexes[value.peer.id]! + if let activeVideo = activeVideoViews.first(where: { $0.endpointId == value.presentationEndpointId }) { + activityIndex += (10000000 + (Int.max - activeVideo.index)) + } + if let activeVideo = activeVideoViews.first(where: { $0.endpointId == value.videoEndpointId }) { + activityIndex += (1000000 + (Int.max - activeVideo.index)) + } + addMember(value, activityIndex, true) + } + for value in rest { + addMember(value, indexes[value.peer.id]!, false) + } + + + for invited in invitedPeers { + if !activeParticipants.contains(where: { $0.peer.id == invited.id}) { + memberDatas.append(PeerGroupCallData(peer: invited, state: nil, isSpeaking: false, isInvited: true, unsyncVolume: nil, accountPeerId: accountPeerId, accountAbout: accountPeerAbout, canManageCall: state.canManageCall, hideWantsToSpeak: false, activityTimestamp: startIndex - 1 - index, firstTimestamp: 0, videoMode: !activeVideoViews.isEmpty, isVertical: false, hasVideo: false, activeVideos: Set(), adminIds: state.adminIds, isFullscreen: isFullScreen, videoEndpointId: nil, presentationEndpointId: nil, visualEffects: voiceSettings.visualEffects)) + index += 1 + } + } + + + let mode: GroupCallUIState.Mode + let isVideoEnabled = summaryState?.info?.isVideoEnabled ?? false + + let main = activeParticipants.first(where: { $0.peer.id == accountPeerId }) + + if main?.joinedVideo == false { + mode = .voice + } else { + switch isVideoEnabled || !videoSources.isEmpty || !activeVideoViews.isEmpty { + case true: + mode = .video + case false: + mode = .voice + } + } + + var tooltipSpeaker: PeerGroupCallData? = nil + if !activeVideoViews.isEmpty && isFullScreen { + if current != nil { + if let previous = previous?.tooltipSpeaker { + let member = memberDatas.first(where: { $0.peer.id == previous.peer.id }) + if let member = member, member.isSpeaking, member.hasVideo { + if current?.peerId != previous.peer.id { + tooltipSpeaker = previous + } + } + } + if tooltipSpeaker == nil { + tooltipSpeaker = memberDatas.first(where: { $0.isSpeaking && $0.hasVideo && $0.peer.id != $0.accountPeerId && $0.peer.id != current?.peerId }) + } + + } + if tooltipSpeaker == nil && current == nil { + tooltipSpeaker = memberDatas.first(where: { $0.isSpeaking && $0.hasVideo && $0.peer.id != $0.accountPeerId && $0.peer.id != current?.peerId && $0.videoEndpoint == nil && $0.presentationEndpointId == nil }) + } + } + + var controlsTooltip: GroupCallUIState.ControlsTooltip? = previous?.controlsTooltip + + + if let current = controlsTooltip, tooltips.dismissed.contains(current) { + controlsTooltip = nil + } + + if controlsTooltip == nil { + if let member = memberDatas.first(where: { $0.peer.id == $0.accountPeerId }) { + if member.isSpeaking, !member.hasVideo, !activeVideoViews.isEmpty { + if !tooltips.dismissed.contains(.camera) { + controlsTooltip = .camera + } + } + } + } + + if controlsTooltip == nil { + if tooltips.speachDetected, !tooltips.dismissed.contains(.micro) { + controlsTooltip = .micro + } + } + + if state.networkState == .connecting { + controlsTooltip = nil + } + + //voiceSettings.visualEffects + + return GroupCallUIState(memberDatas: memberDatas.sorted(by: <), state: state, isMuted: isMuted, summaryState: summaryState, myAudioLevel: myAudioLevel, peer: peerViewMainPeer(peerView)!, cachedData: peerView.cachedData as? CachedChannelData, voiceSettings: voiceSettings, isWindowVisible: isWindowVisible, dominantSpeaker: current, pinnedData: pinnedData, isFullScreen: isFullScreen, mode: mode, videoSources: videoSources, version: version, activeVideoViews: activeVideoViews.sorted(by: { $0.index < $1.index }), hideParticipants: hideParticipants, isVideoEnabled: main?.joinedVideo ?? summaryState?.info?.isVideoEnabled ?? false, tooltipSpeaker: tooltipSpeaker, controlsTooltip: controlsTooltip, dismissedTooltips: tooltips.dismissed, videoJoined: main?.joinedVideo ?? isVideoEnabled, visualEffects: false) +} + + +private func peerEntries(state: GroupCallUIState, account: Account, arguments: GroupCallUIArguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + let index: Int32 = 0 + + + let members = state.memberDatas + + let canInvite: Bool = !members.contains(where: { $0.isVertical }) + + + if canInvite { + + struct Tuple : Equatable { + let viewType: GeneralViewType + let videoMode: Bool + } + + let viewType: GeneralViewType + if entries.isEmpty { + viewType = GeneralViewType.singleItem.withUpdatedInsets(NSEdgeInsetsMake(12, 16, 0, 0)) + } else { + viewType = GeneralViewType.firstItem.withUpdatedInsets(NSEdgeInsetsMake(12, 16, 0, 0)) + } + let tuple = Tuple(viewType: viewType, videoMode: false) + + entries.append(.custom(sectionId: 0, index: index, value: .none, identifier: InputDataIdentifier("invite"), equatable: InputDataEquatable(tuple), comparable: nil, item: { initialSize, stableId in + return GroupCallInviteRowItem(initialSize, height: 42, stableId: stableId, videoMode: tuple.videoMode, viewType: viewType, action: arguments.inviteMembers) + })) + } + for (i, data) in members.enumerated() { + + let drawLine = i != members.count - 1 + + var viewType: GeneralViewType = bestGeneralViewType(members, for: i) + if i == 0, canInvite { + viewType = i != members.count - 1 ? .innerItem : .lastItem + } else if !canInvite, !data.isVertical, i > 0 { + if members[i - 1].isVertical { + viewType = i != members.count - 1 ? .firstItem : .singleItem + } + } + + struct Tuple : Equatable { + let drawLine: Bool + let data: PeerGroupCallData + let canManageCall:Bool + let adminIds: Set + let viewType: GeneralViewType + let baseEndpoint: String? + } + + var duplicates:[(InputDataIdentifier, String?)] = [] + + if data.isVertical { + if let endpoint = data.presentationEndpoint { + if state.dominantSpeaker?.endpointId != endpoint { + duplicates.append((_id_peer_id(data, endpoint: endpoint), endpoint)) + } + } + if let endpoint = data.videoEndpoint { + if state.dominantSpeaker?.endpointId != endpoint { + duplicates.append((_id_peer_id(data, endpoint: endpoint), endpoint)) + } + } + } else { + duplicates.append((_id_peer_id(data), nil)) + } + + + + for (i, (stableId, baseEndpoint)) in duplicates.enumerated() { + + let tuple = Tuple(drawLine: drawLine, data: data, canManageCall: state.state.canManageCall, adminIds: state.state.adminIds, viewType: viewType, baseEndpoint: baseEndpoint) + + struct TupleIndex { + let data: PeerGroupCallData + let index: Int + } + + let index = TupleIndex(data: data, index: i) + + let comparable = InputDataComparableIndex(data: index, compare: { lhs, rhs in + let lhs = lhs as? TupleIndex + let rhs = rhs as? TupleIndex + if let lhs = lhs, let rhs = rhs { + if lhs.data == rhs.data { + return lhs.index < rhs.index + } else { + return lhs.data < rhs.data + } + } else { + return false + } + }, equatable: { lhs, rhs in + let lhs = lhs as? TupleIndex + let rhs = rhs as? TupleIndex + if let lhs = lhs, let rhs = rhs { + return lhs.data == rhs.data && lhs.index == rhs.index + } else { + return false + } + }) + + entries.append(.custom(sectionId: 0, index: 1, value: .none, identifier: stableId, equatable: InputDataEquatable(tuple), comparable: comparable, item: { initialSize, stableId in + return GroupCallParticipantRowItem(initialSize, stableId: stableId, account: account, data: tuple.data, baseEndpoint: tuple.baseEndpoint, canManageCall: tuple.canManageCall, isInvited: tuple.data.isInvited, isLastItem: false, drawLine: drawLine, viewType: viewType, action: { + arguments.openInfo(data.peer) + }, invite: arguments.invite, contextMenu: { + return .single(arguments.contextMenuItems(tuple.data)) + }, takeVideo: arguments.takeVideo, audioLevel: arguments.audioLevel, focusVideo: arguments.focusVideo) + })) + } + + + } + + return entries +} + + + +final class GroupCallUIController : ViewController { + + final class UIData { + let call: PresentationGroupCall + let peerMemberContextsManager: PeerChannelMemberCategoriesContextsManager + init(call: PresentationGroupCall, peerMemberContextsManager: PeerChannelMemberCategoriesContextsManager) { + self.call = call + self.peerMemberContextsManager = peerMemberContextsManager + } + } + private let data: UIData + private let disposable = MetaDisposable() + private let pushToTalkDisposable = MetaDisposable() + private let requestPermissionDisposable = MetaDisposable() + private let voiceSourcesDisposable = MetaDisposable() + private var pushToTalk: PushToTalk? + private let actionsDisposable = DisposableSet() + private var canManageCall: Bool = false + private let connecting = MetaDisposable() + private let isFullScreen = ValuePromise(false, ignoreRepeated: true) + private weak var sharing: DesktopCapturerWindow? + + private var requestedVideoSources = Set() + private var videoViews: [(DominantVideo, GroupCallUIState.ActiveVideo.Mode, GroupVideoView)] = [] + + private var idleTimer: SwiftSignalKit.Timer? + private var speakController: MicroListenerContext + + let size: ValuePromise = ValuePromise(.zero, ignoreRepeated: true) + + + private let tooltips:Atomic = Atomic(value: Tooltips.initialValue) + private let tooltipsValue:ValuePromise = ValuePromise(Tooltips.initialValue, ignoreRepeated: true) + private func updateTooltips(_ f: (Tooltips)->Tooltips)->Void { + tooltipsValue.set(tooltips.modify(f)) + } + + + + var disableSounds: Bool = false + init(_ data: UIData, size: NSSize) { + self.data = data + self.speakController = MicroListenerContext(devices: data.call.sharedContext.devicesContext, accountManager: data.call.sharedContext.accountManager) + super.init(frame: NSMakeRect(0, 0, size.width, size.height)) + bar = .init(height: 0) + isFullScreen.set(size.width >= GroupCallTheme.fullScreenThreshold) + self.size.set(size) + } + + + override func viewDidResized(_ size: NSSize) { + + + } + + @objc private func _viewFrameChanged(_ notification:Notification) { + let size = self.genericView.frame.size + _ = self.atomicSize.swap(genericView.peersTable.frame.size) + self.isFullScreen.set(size.width >= GroupCallTheme.fullScreenThreshold) + self.size.set(size) + } + + override func viewDidLoad() { + super.viewDidLoad() + + NotificationCenter.default.addObserver(self, selector: #selector(_viewFrameChanged(_:)), name: NSView.frameDidChangeNotification, object: genericView.peersTable) + + + + let dominantSpeakerSignal:ValuePromise = ValuePromise(GroupCallUIState.PinnedData(), ignoreRepeated: true) + let dominantSpeakerValue:Atomic = Atomic(value: GroupCallUIState.PinnedData()) + let updateDominant:((GroupCallUIState.PinnedData)->GroupCallUIState.PinnedData)->Void = { f in + dominantSpeakerSignal.set(dominantSpeakerValue.modify(f)) + } + + + let hideParticipantsValue:Atomic = Atomic(value: false) + let hideParticipants:Promise = Promise(false) + let updateHideParticipants:((Bool)->Bool)->Void = { f in + hideParticipants.set(.single(hideParticipantsValue.modify(f))) + } + + + let callState: ValuePromise = ValuePromise(ignoreRepeated: true) + + struct ActiveVideos : Equatable { + var set: [GroupCallUIState.ActiveVideo] + var index: Int + } + let activeVideoViewsValue:Atomic = Atomic(value: ActiveVideos(set: [], index: Int.max)) + let activeVideoViews = Promise(ActiveVideos(set: [], index: Int.max)) + let updateActiveVideoViews:((ActiveVideos)->ActiveVideos)->Void = { f in + let updated = activeVideoViewsValue.modify(f) + activeVideoViews.set(.single(updated) |> delay(0.15, queue: .mainQueue())) + } + + let actionsDisposable = self.actionsDisposable + + let peerId = self.data.call.peerId + let account = self.data.call.account + + let videoSources = ValuePromise(.init()) + let videoSourcesValue: Atomic = Atomic(value: .init()) + let updateVideoSources:(@escaping(GroupCallUIState.VideoSources)->GroupCallUIState.VideoSources)->Void = { f in + videoSources.set(videoSourcesValue.modify(f)) + } + + + + let displayedRaisedHandsPromise = ValuePromise>([], ignoreRepeated: true) + let displayedRaisedHands: Atomic> = Atomic(value: []) + + var raisedHandDisplayDisposables: [PeerId: Disposable] = [:] + + let updateDisplayedRaisedHands:(@escaping(Set)->Set)->Void = { f in + _ = displayedRaisedHands.modify(f) + } + + guard let window = self.navigationController?.window else { + fatalError() + } + let animate: Signal = window.takeOcclusionState |> map { + $0.contains(.visible) + } + self.pushToTalk = PushToTalk(sharedContext: data.call.sharedContext, window: window) + let sharedContext = self.data.call.sharedContext + let unsyncVolumes = ValuePromise<[PeerId: Int32]>([:]) + var askedForSpeak: Bool = false + + var contextMenuItems:((PeerGroupCallData)->[ContextMenuItem])? = nil + + let arguments = GroupCallUIArguments(leave: { [weak self] in + guard let `self` = self, let window = self.window else { + return + } + if self.canManageCall { + modernConfirm(for: window, account: account, peerId: nil, header: L10n.voiceChatEndTitle, information: L10n.voiceChatEndText, okTitle: L10n.voiceChatEndOK, thridTitle: L10n.voiceChatEndThird, thridAutoOn: false, successHandler: { + [weak self] result in + _ = self?.data.call.sharedContext.endGroupCall(terminate: result == .thrid).start() + }) + } else { + _ = self.data.call.sharedContext.endGroupCall(terminate: false).start() + } + }, settings: { [weak self] in + guard let `self` = self else { + return + } + self.navigationController?.push(GroupCallSettingsController(sharedContext: sharedContext, account: account, callState: callState.get(), call: self.data.call)) + }, invite: { [weak self] peerId in + _ = self?.data.call.invitePeer(peerId) + }, mute: { [weak self] peerId, isMuted in + _ = self?.data.call.updateMuteState(peerId: peerId, isMuted: isMuted) + }, toggleSpeaker: { [weak self] in + self?.data.call.toggleIsMuted() + }, remove: { [weak self] peer in + guard let window = self?.window, let accountContext = self?.data.call.accountContext else { + return + } + let isChannel = self?.data.call.peer?.isChannel == true + modernConfirm(for: window, account: account, peerId: peer.id, information: isChannel ? L10n.voiceChatRemovePeerConfirmChannel(peer.displayTitle) : L10n.voiceChatRemovePeerConfirm(peer.displayTitle), okTitle: L10n.voiceChatRemovePeerConfirmOK, cancelTitle: L10n.voiceChatRemovePeerConfirmCancel, successHandler: { [weak window] _ in + if peerId.namespace == Namespaces.Peer.CloudChannel { + _ = self?.data.peerMemberContextsManager.updateMemberBannedRights(peerId: peerId, memberId: peer.id, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: 0)).start() + } else if let window = window { + _ = showModalProgress(signal: accountContext.engine.peers.removePeerMember(peerId: peerId, memberId: peer.id), for: window).start() + } + }, appearance: darkPalette.appearance) + }, openInfo: { [weak self] peer in + guard let strongSelf = self else { + return + } + strongSelf.data.call.sharedContext.bindings.rootNavigation().push(PeerInfoController.init(context: strongSelf.data.call.accountContext, peerId: peer.id)) + strongSelf.data.call.accountContext.window.orderFrontRegardless() + }, inviteMembers: { [weak self] in + guard let window = self?.window, let data = self?.data else { + return + } + actionsDisposable.add(GroupCallAddmembers(data, window: window).start(next: { [weak window, weak self] peerId in + if let peerId = peerId.first, let window = window, let `self` = self { + if self.data.call.invitePeer(peerId) { + _ = showModalSuccess(for: window, icon: theme.icons.successModalProgress, delay: 2.0).start() + } + } + })) + }, shareSource: { [weak self] mode, takeFirst in + guard let state = self?.genericView.state, let window = self?.window, let data = self?.data else { + return + } + + if state.state.muteState?.canUnmute == false { + let text: String + switch mode { + case .screencast: + text = L10n.voiceChatShareVideoMutedError + case .video: + text = L10n.voiceChatShareScreenMutedError + } + alert(for: window, info: text) + return + } + + if state.state.networkState == .connecting { + NSSound.beep() + return + } + + if !state.isVideoEnabled { + let config = GroupCallsConfig(data.call.accountContext.appConfiguration) + switch mode { + case .video: + alert(for: window, info: L10n.voiceChatTooltipErrorVideoUnavailable(config.videoLimit)) + case .screencast: + alert(for: window, info: L10n.voiceChatTooltipErrorScreenUnavailable(config.videoLimit)) + } + return + } + let confirmSource:(VideoSourceMacMode, @escaping(Bool)->Void)->Void = { [weak state, weak window] source, f in + if let state = state, let window = window { + switch mode { + case .screencast: + let presentingPeer = state.videoActive(.main).first(where: { $0.presentationEndpoint != nil }) + if let peer = presentingPeer { + confirm(for: window, header: L10n.voiceChatScreencastConfirmHeader, information: L10n.voiceChatScreencastConfirmText(peer.peer.compactDisplayTitle), okTitle: L10n.voiceChatScreencastConfirmOK, successHandler: { _ in + f(true) + }, cancelHandler: { + f(false) + }) + } else { + f(true) + } + case .video: + f(true) + } + } + } + + let toggleMicro:(Bool, @escaping()->Void)->Void = { [weak self] value, f in + if let strongSelf = self { + if !value != strongSelf.genericView.state?.isMuted { + let signal = strongSelf.data.call.state + |> map { + ($0.muteState == nil && value) || ($0.muteState != nil && !value) + } |> filter { + $0 + } |> take(1) + |> deliverOnMainQueue + + _ = signal.start(completed: f) + + strongSelf.data.call.toggleIsMuted() + } else { + f() + } + } + } + + let select:(VideoSourceMac)->Void = { source in + updateVideoSources { current in + var current = current + switch source.mode { + case .screencast: + current.screencast = source + case .video: + current.video = source + } + return current + } + switch source.mode { + case .screencast: + self?.data.call.requestScreencast(deviceId: source.deviceIdKey()) + case .video: + self?.data.call.requestVideo(deviceId: source.deviceIdKey()) + } + self?.updateTooltips { current in + var current = current + current.dismissed.insert(.camera) + return current + } + } + + if takeFirst { + switch mode { + case .video: + let devicesSignal = sharedContext.devicesContext.signal + |> take(1) + |> deliverOnMainQueue + + let deviceId = sharedContext.devicesContext.currentCameraId + + actionsDisposable.add(devicesSignal.start(next: { devices in + let device = devices.camera.first(where: { deviceId == $0.uniqueID }) + if let device = device { + select(CameraCaptureDevice(device)) + } + })) + case .screencast: + let screens = DesktopCaptureSourceManagerMac(_s: ()) + if let first = screens.list().first { + select(first) + } + } + return + } + if let sharing = self?.sharing, sharing.mode == mode { + sharing.orderFront(nil) + } else { + self?.sharing?.orderOut(nil) + confirmSource(mode, { accept in + if accept { + let sharing = presentDesktopCapturerWindow(mode: mode, select: { source, wantsToSpeak in + toggleMicro(wantsToSpeak, { + select(source) + }) +// DispatchQueue.main.async { +// +// } + }, devices: sharedContext.devicesContext) + self?.sharing = sharing + sharing?.level = self?.window?.level ?? .normal + if sharing == nil, let window = self?.window { + switch mode { + case .video: + showModalText(for: window, text: L10n.voiceChatTooltipNoCameraFound) + default: + break + } + } + } + }) + } + }, takeVideo: { [weak self] peerId, mode, listMode in + let views = self?.videoViews.filter { $0.0.peerId == peerId && $0.1 == listMode } + var view: NSView? = nil + if let views = views { + if let mode = mode { + view = views.first(where: { $0.0.mode == mode })?.2 + } else { + view = views.first(where: { $0.0.mode == .video })?.2 ?? views.first?.2 + } + } + view?.layer?.removeAllAnimations() + return view + }, isSharingVideo: { [weak self] peerId in + return self?.videoViews.first(where: { $0.0.peerId == peerId && $0.0.mode == .video }) != nil + }, isSharingScreencast: { [weak self] peerId in + return self?.videoViews.first(where: { $0.0.peerId == peerId && $0.0.mode == .screencast }) != nil + }, canUnpinVideo: { [weak self] peerId, mode in + let dominant = self?.genericView.state?.dominantSpeaker + return dominant?.peerId == peerId + }, pinVideo: { [weak self] video in + updateDominant { current in + var current = current + current.permanent = video.endpointId + current.focused = .init(id: video.endpointId, time: Date().timeIntervalSince1970) + return current + } + self?.genericView.peersTable.scroll(to: .up(true)) + }, unpinVideo: { + updateDominant { current in + var current = current + if let permanent = current.permanent { + current.permanent = nil + current.excludePins.insert(permanent) + } + return current + } + }, isPinnedVideo: { [weak self] peerId, mode in + if let current = self?.genericView.state?.dominantSpeaker, current.pinMode == .permanent { + return current.peerId == peerId && current.mode == mode + } + return false + }, setVolume: { [weak self] peerId, volume, sync in + let value = Int32(volume * 10000) + self?.data.call.setVolume(peerId: peerId, volume: value, sync: sync) + if sync { + unsyncVolumes.set([:]) + } else { + unsyncVolumes.set([peerId : value]) + } + }, getAccountPeerId:{ [weak self] in + return self?.data.call.joinAsPeerId + }, getAccount: { + return account + }, cancelShareScreencast: { [weak self] in + updateVideoSources { current in + var current = current + current.screencast = nil + return current + } + self?.data.call.disableScreencast() + }, cancelShareVideo: { [weak self] in + updateVideoSources {current in + var current = current + current.video = nil + return current + } + self?.data.call.disableVideo() + }, toggleRaiseHand: { [weak self] in + if let strongSelf = self, let state = self?.genericView.state { + let call = strongSelf.data.call + if !state.state.raisedHand { + askedForSpeak = true + call.raiseHand() + } else { + askedForSpeak = false + call.lowerHand() + } + } + }, recordClick: { [weak self] state in + if let window = self?.window { + if state.canManageCall { + confirm(for: window, header: L10n.voiceChatRecordingStopTitle, information: L10n.voiceChatRecordingStopText, okTitle: L10n.voiceChatRecordingStopOK, successHandler: { [weak window] _ in + self?.data.call.setShouldBeRecording(false, title: nil, videoOrientation: nil) + if let window = window { + showModalText(for: window, text: L10n.voiceChatToastStop) + } + }) + } else { + showModalText(for: window, text: L10n.voiceChatAlertRecording) + } + } + }, audioLevel: { [weak self] peerId in + if let call = self?.data.call { + if peerId == call.joinAsPeerId { + return combineLatest(animate, call.myAudioLevel) + |> map (Optional.init) + |> map { $0?.1 == 0 || $0?.0 == false ? nil : $0?.1 } + |> mapToThrottled { value in + return .single(value) |> delay(0.1, queue: .mainQueue()) + } + |> deliverOnMainQueue + } else { + return combineLatest(animate, call.audioLevels) |> map { (visible, values) in + if visible { + for value in values { + if value.0 == peerId { + return value.2 + } + } + } + return nil + } |> deliverOnMainQueue + } + } + return nil + }, startVoiceChat: { [weak self] in + self?.data.call.startScheduled() + }, toggleReminder: { [weak self] subscribe in + if subscribe, let window = self?.window { + showModalText(for: window, text: L10n.voiceChatTooltipSubscribe) + } + self?.data.call.toggleScheduledSubscription(subscribe) + }, toggleScreenMode: { + }, switchCamera: { _ in + }, togglePeersHidden: { + updateHideParticipants { + !$0 + } + }, contextMenuItems: { data in + return contextMenuItems?(data) ?? [] + }, dismissTooltip: { [weak self] tooltip in + self?.updateTooltips { current in + var current = current + current.dismissed.insert(tooltip) + return current + } + }, focusVideo: { [weak self] endpointId in + updateDominant { current in + var current = current + if current.focused?.id == endpointId && endpointId != nil { + current.focused = nil + } else { + if let endpointId = endpointId { + current.focused = .init(id: endpointId, time: Date().timeIntervalSince1970) + } else { + current.focused = nil + } + current.permanent = nil + } + return current + } + self?.genericView.peersTable.scroll(to: .up(true)) + }, takeTileView: { [weak self] in + if let `self` = self, let view = self.genericView.tileView { + return (self.genericView.videoRect.size, view) + } + return nil + }, getSource: { mode in + return videoSourcesValue.with { value in + switch mode { + case .screencast: + return value.screencast + case .video: + return value.video + } + } + }) + + contextMenuItems = { [weak arguments, weak self] data in + + guard let arguments = arguments else { + return [] + } + + var items: [ContextMenuItem] = [] + + if let state = data.state, let accountContext = self?.data.call.accountContext { + + let headerItem: ContextMenuItem = .init("headerItem", handler: { + + }) + let headerView = GroupCallContextMenuHeaderView(frame: NSMakeRect(0, 0, 200, 200)) + + let videos = [arguments.takeVideo(state.peer.id, .video, .profile), arguments.takeVideo(state.peer.id, .screencast, .profile)].compactMap { $0} + + headerView.setPeer(state.peer, about: data.about, context: accountContext, videos: videos) + headerItem.view = headerView + + + items.append(headerItem) + items.append(ContextSeparatorItem()) + + if data.peer.id == data.accountPeerId, data.isRaisedHand { + items.append(ContextMenuItem(L10n.voiceChatDownHand, handler: arguments.toggleRaiseHand)) + } + + if data.peer.id != data.accountPeerId, state.muteState == nil || state.muteState?.canUnmute == true { + let volume: ContextMenuItem = .init("Volume", handler: { + + }) + + let volumeControl = VolumeMenuItemView(frame: NSMakeRect(0, 0, 200, 26)) + volumeControl.stateImages = (on: NSImage(named: "Icon_VolumeMenu_On")!.precomposed(.white), + off: NSImage(named: "Icon_VolumeMenu_Off")!.precomposed(.white)) + volumeControl.value = CGFloat((state.volume ?? 10000)) / 10000.0 + volumeControl.lineColor = GroupCallTheme.memberSeparatorColor.lighter() + volume.view = volumeControl + + volumeControl.didUpdateValue = { value, sync in + if value == 0 { + arguments.mute(data.peer.id, true) + } else { + arguments.setVolume(data.peer.id, Double(value), sync) + } + } + + items.append(volume) + items.append(ContextSeparatorItem()) + } + if let endpointId = data.videoEndpoint { + if !arguments.isPinnedVideo(data.peer.id, .video) { + items.append(ContextMenuItem(L10n.voiceChatPinVideo, handler: { + arguments.pinVideo(.init(data.peer.id, endpointId, .video, .permanent)) + })) + } else if arguments.canUnpinVideo(data.peer.id, .video) { + items.append(ContextMenuItem(L10n.voiceChatUnpinVideo, handler: { + arguments.unpinVideo() + })) + } + } + if let endpointId = data.presentationEndpoint { + if !arguments.isPinnedVideo(data.peer.id, .screencast) { + items.append(ContextMenuItem(L10n.voiceChatPinScreencast, handler: { + arguments.pinVideo(.init(data.peer.id, endpointId, .screencast, .permanent)) + })) + } else if arguments.canUnpinVideo(data.peer.id, .screencast) { + items.append(ContextMenuItem(L10n.voiceChatUnpinScreencast, handler: { + arguments.unpinVideo() + })) + } + } + + + + if !data.canManageCall, data.peer.id != data.accountPeerId { + if let muteState = state.muteState { + if muteState.mutedByYou { + items.append(.init(L10n.voiceChatUnmuteForMe, handler: { + arguments.mute(data.peer.id, false) + })) + } else { + items.append(.init(L10n.voiceChatMuteForMe, handler: { + arguments.mute(data.peer.id, true) + })) + } + } else { + items.append(.init(L10n.voiceChatMuteForMe, handler: { + arguments.mute(data.peer.id, true) + })) + } + items.append(ContextSeparatorItem()) + } + + if data.canManageCall, data.peer.id != data.accountPeerId { + if data.adminIds.contains(data.peer.id) { + if state.muteState == nil { + items.append(.init(L10n.voiceChatMutePeer, handler: { + arguments.mute(data.peer.id, true) + })) + } + if !data.adminIds.contains(data.peer.id), !data.peer.isChannel { + items.append(.init(L10n.voiceChatRemovePeer, handler: { + arguments.remove(data.peer) + })) + } + if !items.isEmpty { + items.append(ContextSeparatorItem()) + } + } else if let muteState = state.muteState, !muteState.canUnmute { + items.append(.init(L10n.voiceChatUnmutePeer, handler: { + arguments.mute(data.peer.id, false) + })) + } else { + items.append(.init(L10n.voiceChatMutePeer, handler: { + arguments.mute(data.peer.id, true) + })) + } + if !data.adminIds.contains(data.peer.id), !data.peer.isChannel { + items.append(.init(L10n.voiceChatRemovePeer, handler: { + arguments.remove(data.peer) + })) + } + } + if data.peer.id != data.accountPeerId { + items.append(.init(L10n.voiceChatOpenProfile, handler: { + arguments.openInfo(data.peer) + })) + } + } + return items + + } + + + let pinUpdater:(GroupCallUIState?, GroupCallUIState) -> Void = { previous, state in + + let videoMembers: [PeerGroupCallData] = state.memberDatas + .filter { member in + return member.presentationEndpoint != nil && member.peer.id != member.accountPeerId + } + + let prevPresenting = previous?.memberDatas + .filter { member in + return member.presentationEndpoint != nil && member.peer.id != member.accountPeerId + } ?? [] + let presenting = videoMembers + + var current: DominantVideo? = previous?.dominantSpeaker + + if presenting.count != prevPresenting.count { + var intersection:[PeerGroupCallData] = [] + if presenting.count > prevPresenting.count { + intersection = presenting + intersection.removeAll(where: { value in + if prevPresenting.contains(where: { $0.peer.id == value.peer.id }) { + return true + } else { + return false + } + }) + } else if current != nil { + if !presenting.contains(where: { $0.presentationEndpoint == current?.endpointId }) { + if let first = presenting.first { + intersection.append(first) + } + } + } + intersection = intersection.filter({ + return !state.pinnedData.excludePins.contains($0.presentationEndpointId!) + }) + if let first = intersection.first { + let master: DominantVideo = DominantVideo(first.peer.id, first.presentationEndpointId!, .screencast, .permanent) + current = master + } + + if let value = current, value != previous?.dominantSpeaker { + DispatchQueue.main.async { + updateDominant { current in + var current = current + current.permanent = value.endpointId + current.focused = nil + return current + } + } + } + } else if state.pinnedData.permanent == nil { + let members = state.activeVideoMembers[.main] ?? [] + if let active = members.first(where: { $0.isSpeaking && $0.accountPeerId != $0.peer.id }) { + var endpointId: String + if let endpoint = active.videoEndpoint { + endpointId = endpoint + } else if let endpoint = active.presentationEndpoint { + endpointId = endpoint + } else { + fatalError("sounds impossible, but at the end it happened.") + } + var canSwitch: Bool = false + if let current = state.pinnedData.focused { + let member = members.first(where: { $0.videoEndpoint == current.id || $0.presentationEndpoint == current.id }) + if active.peer.id != member?.peer.id { + canSwitch = current.id != endpointId && (Date().timeIntervalSince1970 - current.time) > 5.0 + } + } + if canSwitch { + DispatchQueue.main.async { + updateDominant { current in + var current = current + current.permanent = nil + current.focused = .init(id: endpointId, time: Date().timeIntervalSince1970) + return current + } + } + } + } + } + } + + let checkRaiseHand:(GroupCallUIState)->Void = { state in + for member in state.memberDatas { + if member.isRaisedHand { + let displayedRaisedHands = displayedRaisedHands.with { $0 } + let signal: Signal = Signal.complete() |> delay(3.0, queue: Queue.mainQueue()) + if !displayedRaisedHands.contains(member.peer.id) { + if raisedHandDisplayDisposables[member.peer.id] == nil { + raisedHandDisplayDisposables[member.peer.id] = signal.start(completed: { + updateDisplayedRaisedHands { current in + var current = current + current.insert(member.peer.id) + return current + } + raisedHandDisplayDisposables[member.peer.id] = nil + }) + } + } + } else { + raisedHandDisplayDisposables[member.peer.id]?.dispose() + raisedHandDisplayDisposables[member.peer.id] = nil + DispatchQueue.main.async { + updateDisplayedRaisedHands { current in + var current = current + current.remove(member.peer.id) + return current + } + } + + } + } + DispatchQueue.main.async { + displayedRaisedHandsPromise.set(displayedRaisedHands.with { $0 }) + } + } + + let checkVideo:(GroupCallUIState?, GroupCallUIState) -> Void = { [weak self] currentState, state in + if state.state.muteState?.canUnmute == false || currentState?.state.myPeerId != state.state.myPeerId { + if !state.videoSources.isEmpty { + DispatchQueue.main.async { + updateVideoSources { current in + var current = current + current.screencast = nil + current.video = nil + return current + } + self?.data.call.disableVideo() + self?.data.call.disableScreencast() + } + } + } + } + + + let applyTooltipsAndSounds:(GroupCallUIState?, GroupCallUIState) -> Void = { [weak self] currentState, state in + + guard let window = self?.window else { + return + } + + switch state.state.networkState { + case .connected: + var notifyCanSpeak: Bool = false + var notifyStartRecording: Bool = false + + if let previous = currentState { + + if previous.state.muteState != state.state.muteState { + if askedForSpeak, let muteState = state.state.muteState, muteState.canUnmute { + notifyCanSpeak = true + } + } + if previous.state.recordingStartTimestamp == nil && state.state.recordingStartTimestamp != nil { + notifyStartRecording = true + } + if !state.videoSources.failed { + if (previous.videoSources.screencast != nil) != (state.videoSources.screencast != nil) { + if let _ = state.videoSources.screencast { + showModalText(for: window, text: L10n.voiceChatTooltipShareScreen) + } else if let _ = previous.videoSources.screencast { + showModalText(for: window, text: L10n.voiceChatTooltipStopScreen) + } + } + if (previous.videoSources.video != nil) != (state.videoSources.video != nil) { + if let _ = state.videoSources.video { + showModalText(for: window, text: L10n.voiceChatTooltipShareVideo) + } else if let _ = previous.videoSources.video { + showModalText(for: window, text: L10n.voiceChatTooltipStopVideo) + } + } + } + } + if notifyCanSpeak { + askedForSpeak = false + SoundEffectPlay.play(postbox: account.postbox, name: "voip_group_unmuted") + showModalText(for: window, text: L10n.voiceChatToastYouCanSpeak) + } + if notifyStartRecording { + SoundEffectPlay.play(postbox: account.postbox, name: "voip_group_recording_started") + showModalText(for: window, text: L10n.voiceChatAlertRecording) + } + case .connecting: + break + } + } + + self.data.call.mustStopVideo = { [weak arguments, weak window] in + updateVideoSources { current in + var current = current + current.failed = true + return current + } + arguments?.cancelShareVideo() + if let window = window { + showModalText(for: window, text: L10n.voiceChatTooltipVideoFailed) + } + delay(0.2, closure: { + updateVideoSources { current in + var current = current + current.failed = false + return current + } + }) + } + self.data.call.mustStopSharing = { [weak arguments, weak window] in + updateVideoSources { current in + var current = current + current.failed = true + return current + } + arguments?.cancelShareScreencast() + if let window = window { + showModalText(for: window, text: L10n.voiceChatTooltipScreencastFailed) + } + delay(0.2, closure: { + updateVideoSources { current in + var current = current + current.failed = false + return current + } + }) + } + + genericView.arguments = arguments + let members = data.call.members + + + let videoData = combineLatest(queue: .mainQueue(), members, dominantSpeakerSignal.get(), isFullScreen.get(), self.data.call.joinAsPeerIdValue, self.data.call.stateVersion |> filter { $0 > 0 }, size.get(), videoSources.get()) + + + actionsDisposable.add(videoData.start(next: { [weak self] members, dominant, isFullScreen, accountId, stateVersion, size, videoSources in + DispatchQueue.main.async { + guard let strongSelf = self else { + return + } + let types:[GroupCallUIState.ActiveVideo.Mode] = GroupCallUIState.ActiveVideo.allModes + + let mainMember = members?.participants.first(where: { $0.peer.id == accountId }) + + let videoMembers: [GroupCallParticipantsContext.Participant] = members?.participants.filter { member in + return (member.videoEndpointId != nil || member.presentationEndpointId != nil) + } ?? [] + + let tiles = tileViews(videoMembers.count, isFullscreen: isFullScreen, frameSize: strongSelf.genericView.videoRect.size) + + let selectBest = videoMembers.filter { $0.peer.id != accountId }.count == 1 + + var items:[PresentationGroupCallRequestedVideo] = [] + + for (i, member) in videoMembers.enumerated() { + var videoQuality: PresentationGroupCallRequestedVideo.Quality = selectBest ? .full : tiles[i].bestQuality + var screencastQuality: PresentationGroupCallRequestedVideo.Quality = selectBest ? .full : tiles[i].bestQuality + + let dominant = dominant.permanent ?? dominant.focused?.id + if let dominant = dominant { + videoQuality = .thumbnail + screencastQuality = .thumbnail + if dominant == member.videoEndpointId { + videoQuality = .full + } else { + videoQuality = .thumbnail + } + if dominant == member.presentationEndpointId { + screencastQuality = .full + } else { + screencastQuality = .thumbnail + } + } + + let minVideo: PresentationGroupCallRequestedVideo.Quality = .thumbnail + let maxVideo: PresentationGroupCallRequestedVideo.Quality = videoQuality + + + if let item = member.requestedVideoChannel(minQuality: minVideo, maxQuality: maxVideo) { + items.append(item) + } + + var minScreencast: PresentationGroupCallRequestedVideo.Quality = .thumbnail + var maxScreencast: PresentationGroupCallRequestedVideo.Quality = screencastQuality + + if maxScreencast == .medium { + maxScreencast = .full + } + + if maxScreencast == .full { + minScreencast = .full + } + if let item = member.requestedPresentationVideoChannel(minQuality: minScreencast, maxQuality: maxScreencast) { + items.append(item) + } + } + + var validSources = Set() + for item in items { + let endpointId = item.endpointId + let member = members?.participants.first(where: { participant in + if participant.peer.id == accountId { + if participant.videoEndpointId == item.endpointId { + return videoSources.video != nil + } + if participant.presentationEndpointId == item.endpointId { + return videoSources.screencast != nil + } + } + return participant.videoEndpointId == item.endpointId || participant.presentationEndpointId == item.endpointId + }) + + if let member = member { + validSources.insert(endpointId) + if !strongSelf.requestedVideoSources.contains(endpointId) { + strongSelf.requestedVideoSources.insert(endpointId) + + let isScreencast = member.presentationEndpointId == endpointId + let videoMode: VideoSourceMacMode = isScreencast ? .screencast : .video + let takeVideoMode: GroupCallVideoMode = isScreencast && member.peer.id == accountId ? .screencast : .video + + + + for type in types { + strongSelf.data.call.makeVideoView(endpointId: endpointId, videoMode: takeVideoMode, completion: { videoView in + guard let videoView = videoView else { + return + } + var videoViewValue: GroupVideoView? = GroupVideoView(videoView: videoView) + + switch type { + case .main: + videoView.setVideoContentMode(.resizeAspect) + case .list: + videoView.setVideoContentMode(.resizeAspectFill) + case .profile: + videoView.setVideoContentMode(.resizeAspectFill) + } + + videoView.setOnFirstFrameReceived( { [weak self] f in + if let videoViewValue = videoViewValue { + self?.videoViews.append((DominantVideo(member.peer.id, endpointId, videoMode, nil), type, videoViewValue)) + updateActiveVideoViews { current in + var current = current + current.set.append(.init(endpointId: endpointId, mode: type, index: current.index)) + current.index -= 1 + return current + } + } + videoViewValue = nil + }) + }) + } + } + } + } + for i in (0 ..< strongSelf.videoViews.count).reversed() { + if !validSources.contains(strongSelf.videoViews[i].0.endpointId) { + let endpointId = strongSelf.videoViews[i].0.endpointId + strongSelf.videoViews.remove(at: i) + strongSelf.requestedVideoSources.remove(endpointId) + updateActiveVideoViews { current in + var current = current + current.set.removeAll(where: { + $0.endpointId == endpointId + }) + return current + } + } + } + + items = items.filter({ item in + if let mainMember = mainMember { + if mainMember.videoEndpointId == item.endpointId { + return false + } + if mainMember.presentationEndpointId == item.endpointId { + return false + } + } + return true + }) + + self?.data.call.setRequestedVideoList(items: items) + } + })) + + + let invited: Signal<[Peer], NoError> = self.data.call.invitedPeers |> mapToSignal { ids in + return account.postbox.transaction { transaction -> [Peer] in + var peers:[Peer] = [] + for id in ids { + if let peer = transaction.getPeer(id) { + peers.append(peer) + } + } + return peers + } + } + + + let joinAsPeer:Signal<(Peer, String?), NoError> = self.data.call.joinAsPeerIdValue |> mapToSignal { + return account.postbox.peerView(id: $0) |> map { view in + if let cachedData = view.cachedData as? CachedChannelData { + return (peerViewMainPeer(view)!, cachedData.about) + } else if let cachedData = view.cachedData as? CachedUserData { + return (peerViewMainPeer(view)!, cachedData.about) + } else { + return (peerViewMainPeer(view)!, nil) + } + } + } + + + let some = combineLatest(queue: .mainQueue(), self.data.call.isMuted, animate, joinAsPeer, unsyncVolumes.get(), dominantSpeakerSignal.get(), activeVideoViews.get() |> distinctUntilChanged, isFullScreen.get(), self.data.call.stateVersion, hideParticipants.get()) + + + var currentState: GroupCallUIState? + + let previousState: Atomic = Atomic(value: nil) + + let state: Signal = combineLatest(queue: .mainQueue(), self.data.call.state, members, (.single(0) |> then(data.call.myAudioLevel)) |> distinctUntilChanged, account.viewTracker.peerView(peerId), invited, self.data.call.summaryState, voiceCallSettings(data.call.sharedContext.accountManager), some, displayedRaisedHandsPromise.get(), videoSources.get(), tooltipsValue.get()) |> mapToQueue { values in + let value = previousState.modify { previous in + return makeState(previous: previous, + peerView: values.3, + state: values.0, + isMuted: values.7.0, + invitedPeers: values.4, + peerStates: values.1, + myAudioLevel: values.2, + summaryState: values.5, + voiceSettings: values.6, + isWindowVisible: values.7.1, + accountPeer: values.7.2, + unsyncVolumes: values.7.3, + pinnedData: values.7.4, + hideWantsToSpeak: values.8, + isFullScreen: values.7.6, + videoSources: values.9, + activeVideoViews: values.7.5.set, + hideParticipants: values.7.8, + tooltips: values.10, + version: values.7.7) + } + return .single(value!) + } |> distinctUntilChanged + + + + let initialSize = self.atomicSize + var previousIsFullScreen: Bool = initialSize.with { $0.width >= GroupCallTheme.fullScreenThreshold } + + let previousEntries:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) + let animated: Atomic = Atomic(value: false) + let inputArguments = InputDataArguments(select: { _, _ in }, dataUpdated: {}) + + + let transition: Signal<(GroupCallUIState, TableUpdateTransition), NoError> = combineLatest(state, appearanceSignal) |> mapToQueue { state, appAppearance in + let current = peerEntries(state: state, account: account, arguments: arguments).map { AppearanceWrapperEntry(entry: $0, appearance: appAppearance) } + let previous = previousEntries.swap(current) + let signal = prepareInputDataTransition(left: previous, right: current, animated: abs(current.count - previous.count) <= 10 && state.isWindowVisible && state.isFullScreen == previousIsFullScreen, searchState: nil, initialSize: initialSize.with { $0 }, arguments: inputArguments, onMainQueue: false, animateEverything: true) + + previousIsFullScreen = state.isFullScreen + + return combineLatest(.single(state), signal) + } |> deliverOnMainQueue + + self.disposable.set(transition.start { [weak self] value in + guard let strongSelf = self else { + return + } + let state = value.0 + + if currentState == nil { + _ = strongSelf.disableScreenSleep() + } + strongSelf.applyUpdates(state, value.1, strongSelf.data.call, animated: animated.swap(true)) + strongSelf.readyOnce() + + checkRaiseHand(state) + applyTooltipsAndSounds(currentState, state) + checkVideo(currentState, state) + pinUpdater(currentState, state) + + + currentState = state + + callState.set(state) + + }) + + + self.onDeinit = { + currentState = nil + _ = previousEntries.swap([]) + _ = previousState.swap(nil) + } + + genericView.peersTable.setScrollHandler { [weak self] position in + switch position.direction { + case .bottom: + self?.data.call.loadMore() + default: + break + } + } + + var connectedMusicPlayed: Bool = false + + let connecting = self.connecting + + pushToTalkDisposable.set(combineLatest(queue: .mainQueue(), data.call.state, data.call.isMuted, data.call.canBeRemoved).start(next: { [weak self] state, isMuted, canBeRemoved in + + let disableSounds = self?.disableSounds ?? true + + switch state.networkState { + case .connected: + if !connectedMusicPlayed && !disableSounds { + SoundEffectPlay.play(postbox: account.postbox, name: "call up") + connectedMusicPlayed = true + } + if canBeRemoved, connectedMusicPlayed && !disableSounds { + SoundEffectPlay.play(postbox: account.postbox, name: "call down") + } + connecting.set(nil) + case .connecting: + if state.scheduleTimestamp == nil { + connecting.set((Signal.single(Void()) |> delay(3.0, queue: .mainQueue()) |> restart).start(next: { + SoundEffectPlay.play(postbox: account.postbox, name: "reconnecting") + })) + } else { + connecting.set(nil) + } + } + + self?.pushToTalk?.update = { [weak self] mode in + switch state.networkState { + case .connected: + switch mode { + case .speaking: + if isMuted { + if let muteState = state.muteState { + if muteState.canUnmute { + self?.data.call.setIsMuted(action: .muted(isPushToTalkActive: true)) + self?.pushToTalkIsActive = true + } + } + } + case .waiting: + if !isMuted, self?.pushToTalkIsActive == true { + self?.data.call.setIsMuted(action: .muted(isPushToTalkActive: false)) + } + self?.pushToTalkIsActive = false + case .toggle: + if let muteState = state.muteState { + if muteState.canUnmute { + self?.data.call.setIsMuted(action: .unmuted) + } + } else { + self?.data.call.setIsMuted(action: .muted(isPushToTalkActive: false)) + } + } + case .connecting: + break + } + } + })) + + var hasMicroPermission: Bool? = nil + + let alertPermission = { [weak self] in + guard let window = self?.window else { + return + } + confirm(for: window, information: L10n.voiceChatRequestAccess, okTitle: L10n.modalOK, cancelTitle: "", thridTitle: L10n.requestAccesErrorConirmSettings, successHandler: { result in + switch result { + case .thrid: + openSystemSettings(.microphone) + default: + break + } + }, appearance: darkPalette.appearance) + } + + data.call.permissions = { action, f in + switch action { + case .unmuted, .muted(isPushToTalkActive: true): + if let permission = hasMicroPermission { + f(permission) + if !permission { + alertPermission() + } + } else { + _ = requestMicrophonePermission().start(next: { permission in + hasMicroPermission = permission + f(permission) + if !permission { + alertPermission() + } + }) + } + default: + f(true) + } + } + + let launchIdleTimer:()->Void = { [weak self] in + let timer = SwiftSignalKit.Timer(timeout: 2.0, repeat: false, completion: { [weak self] in + self?.genericView.idleHide() + }, queue: .mainQueue()) + + self?.idleTimer = timer + timer.start() + } + +// window.set(mouseHandler: { [weak self] event in +// self?.genericView.updateMouse(animated: true, isReal: true) +// launchIdleTimer() +// return .rejected +// }, with: self, for: .mouseEntered, priority: .modal) + + window.set(mouseHandler: { [weak self] event in + self?.genericView.updateMouse(animated: true, isReal: true) + launchIdleTimer() + return .rejected + }, with: self, for: .mouseMoved, priority: .modal) + + window.set(mouseHandler: { [weak self] event in + self?.genericView.updateMouse(animated: true, isReal: true) + launchIdleTimer() + return .rejected + }, with: self, for: .mouseExited, priority: .modal) + + + window.set(handler: { [weak arguments] event in + if videoSourcesValue.with ({ $0.screencast == nil }) { + arguments?.shareSource(.screencast, true) + } else { + arguments?.cancelShareScreencast() + } + return .invokeNext + }, with: self, for: .T, priority: .modal, modifierFlags: [.command]) + + window.set(handler: { [weak arguments] event in + if videoSourcesValue.with ({ $0.video == nil }) { + arguments?.shareSource(.video, true) + } else { + arguments?.cancelShareVideo() + } + return .invokeNext + }, with: self, for: .E, priority: .modal, modifierFlags: [.command]) + + window.set(handler: { [weak arguments] event in + arguments?.focusVideo(nil) + return .invokeNext + }, with: self, for: .Escape, priority: .modal) + + } + + override func readyOnce() { + let was = self.didSetReady + super.readyOnce() + if didSetReady, !was { + requestPermissionDisposable.set(requestMicrophonePermission().start()) + } + } + + private var pushToTalkIsActive: Bool = false + + + private func applyUpdates(_ state: GroupCallUIState, _ transition: TableUpdateTransition, _ call: PresentationGroupCall, animated: Bool) { + CATransaction.begin() + self.genericView.applyUpdates(state, transition, call, animated: transition.animated) + CATransaction.commit() + canManageCall = state.state.canManageCall + + + self.checkMicro(state) + } + + + private func checkMicro(_ state: GroupCallUIState) { + + + switch state.state.networkState { + case .connecting: + speakController.pause() + case .connected: + if !state.dismissedTooltips.contains(.micro), state.controlsTooltip == nil { + if state.isMuted && state.state.muteState?.canUnmute == true { + speakController.resume(onSpeaking: { [weak self] _ in + self?.updateTooltips { current in + var current = current + current.speachDetected = true + return current + } + }) + } else { + speakController.pause() + } + } else { + speakController.pause() + } + if !state.isMuted { + if !state.isMuted { + DispatchQueue.main.async { [weak self] in + self?.updateTooltips { current in + var current = current + current.dismissed.insert(.micro) + return current + } + } + } + } + } + } + + deinit { + disposable.dispose() + pushToTalkDisposable.dispose() + requestPermissionDisposable.dispose() + actionsDisposable.dispose() + connecting.dispose() + sharing?.orderOut(nil) + idleTimer?.invalidate() + _ = enableScreenSleep() + NotificationCenter.default.removeObserver(self) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + } + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + } + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + } + private var genericView: GroupCallView { + return self.view as! GroupCallView + } + + + override func viewClass() -> AnyClass { + return GroupCallView.self + } + + private var assertionID: IOPMAssertionID = 0 + private var success: IOReturn? + + private func disableScreenSleep() -> Bool? { + guard success == nil else { return nil } + success = IOPMAssertionCreateWithName( kIOPMAssertionTypeNoDisplaySleep as CFString, + IOPMAssertionLevel(kIOPMAssertionLevelOn), + "Group Call" as CFString, + &assertionID ) + return success == kIOReturnSuccess + } + + private func enableScreenSleep() -> Bool { + if success != nil { + success = IOPMAssertionRelease(assertionID) + success = nil + return true + } + return false + } + + +} + + + +/* + + let videoMembers: [GroupCallParticipantsContext.Participant] = activeParticipants + .filter { member in + return member.presentationEndpointId != nil + }.filter { member in + return activeVideoViews.contains(where: { + $0.endpointId == member.presentationEndpointId && $0.mode == .main + }) + } + + let prevPresenting = previous?.memberDatas + .compactMap { $0.state } + .filter { member in + return member.presentationEndpointId != nil + }.filter { member in + return previous?.activeVideoViews.contains(where: { + $0.endpointId == member.presentationEndpointId && $0.mode == .main + }) ?? false + } ?? [] + let presenting = videoMembers + + var current: DominantVideo? = previous?.dominantSpeaker + + if presenting.count != prevPresenting.count { + var intersection:[GroupCallParticipantsContext.Participant] = [] + if presenting.count > prevPresenting.count { + intersection = presenting + intersection.removeAll(where: { value in + if prevPresenting.contains(where: { $0.peer.id == value.peer.id }) { + return true + } else { + return false + } + }) + } else if current != nil { + if !presenting.contains(where: { $0.presentationEndpointId == current?.endpointId }) { + if let first = presenting.first { + intersection.append(first) + } + } + } + + intersection = intersection.filter({ + return !pinnedData.excludePins.contains($0.presentationEndpointId!) + }) + if let first = intersection.first { + let master: DominantVideo = DominantVideo(first.peer.id, first.presentationEndpointId!, .screencast, .permanent) + current = master + } + } + */ + + +// if currentState?.dominantSpeaker != state.dominantSpeaker { +// +// let current = state.dominantSpeaker +// let prev = currentState?.dominantSpeaker +// +// let dominantSpeaker = state.dominantSpeaker +// let dominant = dominantSpeaker ?? currentState?.dominantSpeaker +// if let dominant = dominant { +// let isPinned = dominantSpeaker != nil +// let participant = state.memberDatas.first(where: { $0.peer.id == dominant.peerId }) +// if let participant = participant { +// let text: String = participant.peer.compactDisplayTitle +// switch dominant.mode { +// case .video: +// if isPinned { +// if participant.accountPeerId == participant.peer.id { +// showModalText(for: window, text: L10n.voiceChatTooltipYourVideoPinned) +// } else { +// showModalText(for: window, text: L10n.voiceChatTooltipVideoPinned(text)) +// } +// } else { +// if participant.accountPeerId == participant.peer.id { +// showModalText(for: window, text: L10n.voiceChatTooltipYourVideoUnpinned) +// } else { +// showModalText(for: window, text: L10n.voiceChatTooltipVideoUnpinned(text)) +// } +// } +// case .screencast: +// if isPinned { +// if participant.accountPeerId == participant.peer.id { +// showModalText(for: window, text: L10n.voiceChatTooltipYourScreenPinned) +// } else { +// showModalText(for: window, text: L10n.voiceChatTooltipScreenPinned(text)) +// } +// } else { +// if participant.accountPeerId == participant.peer.id { +// showModalText(for: window, text: L10n.voiceChatTooltipYourScreenUnpinned) +// } else { +// showModalText(for: window, text: L10n.voiceChatTooltipScreenUnpinned(text)) +// } +// } +// } +// } +// } +// } diff --git a/Telegram-Mac/GroupCallControlsView.swift b/Telegram-Mac/GroupCallControlsView.swift new file mode 100644 index 0000000000..617f1a8aa6 --- /dev/null +++ b/Telegram-Mac/GroupCallControlsView.swift @@ -0,0 +1,624 @@ +// +// GroupCallControlsView.swift +// Telegram +// +// Created by Mikhail Filimonov on 06.04.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit +import TelegramCore +import Postbox + + +private final class GroupCallControlsTooltipView: Control { + private let backgroundView = View() + private let textView = TextView() + private let cornerView = ImageView() + private let closeView = ImageButton() + private(set) weak var toView: NSView? + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(backgroundView) + backgroundView.addSubview(textView) + addSubview(cornerView) + addSubview(closeView) + cornerView.isEventLess = true + backgroundView.isEventLess = true + textView.userInteractionEnabled = false + textView.isSelectable = false + textView.disableBackgroundDrawing = true + cornerView.image = generateImage(NSMakeSize(30, 12), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(GroupCallTheme.memberSeparatorColor.cgColor) + context.scaleBy(x: 0.333, y: 0.333) + let _ = try? drawSvgPath(context, path: "M85.882251,0 C79.5170552,0 73.4125613,2.52817247 68.9116882,7.02834833 L51.4264069,24.5109211 C46.7401154,29.1964866 39.1421356,29.1964866 34.4558441,24.5109211 L16.9705627,7.02834833 C12.4696897,2.52817247 6.36519576,0 0,0 L85.882251,0 ") + context.fillPath() + })! + cornerView.sizeToFit() + + closeView.autohighlight = false + closeView.scaleOnClick = true + + closeView.set(image: generateImage(NSMakeSize(20, 20), contextGenerator: { size, ctx in + ctx.clear(size.bounds) + ctx.round(size, size.height / 2) + ctx.setFillColor(GroupCallTheme.membersColor.cgColor) + ctx.fill(size.bounds) + + ctx.draw(GroupCallTheme.closeTooltip, in: size.bounds.focus(GroupCallTheme.closeTooltip.backingSize)) + })!, for: .Normal) + + closeView.set(handler: { [weak self] _ in + self?.send(event: .SingleClick) + }, for: .SingleClick) + + closeView.sizeToFit() + } + + func set(text: String, maxWidth: CGFloat, to view: NSView?) { + let layout = TextViewLayout(.initialize(string: text, color: GroupCallTheme.customTheme.textColor, font: .normal(12))) + layout.measure(width: maxWidth) + textView.update(layout) + + self.toView = view + backgroundView.background = GroupCallTheme.memberSeparatorColor + + setFrameSize(NSMakeSize(layout.layoutSize.width + 16 + 24, layout.layoutSize.height + 8 + 10)) + + backgroundView.layer?.cornerRadius = (frame.height - 10) / 2 + } + + override func layout() { + super.layout() + backgroundView.frame = focus(NSMakeSize(frame.width, frame.height - 10)) + + if let toView = toView { + cornerView.setFrameOrigin(NSMakePoint(toView.frame.minX - 12 + toView.frame.width / 2 - cornerView.frame.width / 2, frame.height - 10)) + } else { + cornerView.centerX(y: frame.height - 10) + } + + closeView.centerY(x: 1) + textView.centerY(x: 26) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +final class GroupCallControlsView : View { + + enum Mode : Equatable { + case normal + case fullscreen + } + + private(set) var mode: Mode = .normal + private(set) var callMode: GroupCallUIState.Mode = .voice + + private(set) var hasVideo: Bool = false + private(set) var hasScreencast: Bool = false + + private let speak: GroupCallSpeakButton = GroupCallSpeakButton(frame: NSMakeRect(0, 0, 140, 140)) + private let leftButton1: CallControl = CallControl(frame: .zero) + private var leftButton2: CallControl? + private var rightButton1: CallControl? + private let end: CallControl = CallControl(frame: .zero) + private var speakText: TextView? + var arguments: GroupCallUIArguments? + + private let backgroundView = VoiceChatActionButtonBackgroundView() + let fullscreenBackgroundView = NSVisualEffectView(frame: .zero) + + private var tooltipView: GroupCallControlsTooltipView? + + required init(frame frameRect: NSRect) { + + + super.init(frame: frameRect) + + self.fullscreenBackgroundView.material = .ultraDark + self.fullscreenBackgroundView.blendingMode = .withinWindow + self.fullscreenBackgroundView.state = .active + self.fullscreenBackgroundView.wantsLayer = true + self.fullscreenBackgroundView.layer?.cornerRadius = 20 + self.fullscreenBackgroundView.layer?.opacity = 0 + + addSubview(fullscreenBackgroundView) + + addSubview(backgroundView) + addSubview(speak) + + + addSubview(leftButton1) + addSubview(end) + + backgroundView.isEventLess = true + backgroundView.userInteractionEnabled = false + + self.isEventLess = true + + + end.set(handler: { [weak self] _ in + self?.arguments?.leave() + }, for: .SingleClick) + + speak.set(handler: { [weak self] _ in + if let state = self?.currentState { + if let _ = state.state.scheduleTimestamp { + if state.state.canManageCall { + self?.arguments?.startVoiceChat() + } else { + self?.arguments?.toggleReminder(!state.state.subscribedToScheduled) + } + } else if let muteState = state.state.muteState, !muteState.canUnmute { + if !state.state.raisedHand { + self?.arguments?.toggleRaiseHand() + } + self?.speak.playRaiseHand() + } else { + self?.arguments?.toggleSpeaker() + } + } + + }, for: .SingleClick) + + self.backgroundView.update(state: .connecting, animated: false) + + self.updateMode(self.mode, callMode: self.callMode, hasVideo: self.hasScreencast, hasScreencast: self.hasScreencast, animated: false, force: true) + } + + private func updateMode(_ mode: Mode, callMode: GroupCallUIState.Mode, hasVideo: Bool, hasScreencast: Bool, animated: Bool, force: Bool = false) { + let previous = self.mode + let previousCallMode = self.callMode + + if previous != mode || hasVideo != self.hasVideo || hasScreencast != self.hasScreencast || self.callMode != callMode || force { + self.speakText?.change(opacity: mode == .fullscreen || callMode == .video ? 0 : 1, animated: animated) + self.fullscreenBackgroundView._change(opacity: mode == .fullscreen ? 1 : 0, animated: animated) + let leftButton1Text: String + let leftBg: CallControlData.Mode + let hasText: Bool = mode != .fullscreen + switch callMode { + case .voice: + leftButton1Text = L10n.voiceChatSettings + leftBg = .normal(GroupCallTheme.settingsColor, GroupCallTheme.settingsIcon) + if let view = leftButton2 { + self.leftButton2 = nil + if animated { + view.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } else { + view.removeFromSuperview() + } + } + if let view = rightButton1 { + self.rightButton1 = nil + if animated { + view.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } else { + view.removeFromSuperview() + } + } + case .video: + leftButton1Text = L10n.voiceChatVideoStreamVideo + leftBg = .animated(!hasVideo ? .cameraoff : .cameraon, GroupCallTheme.settingsColor) + + let leftButton2: CallControl + let rightButton1: CallControl + + if let control = self.rightButton1 { + rightButton1 = control + } else { + rightButton1 = CallControl(frame: .zero) + self.rightButton1 = rightButton1 + rightButton1.setFrameOrigin(end.frame.origin) + addSubview(rightButton1, positioned: .below, relativeTo: end) + if animated { + rightButton1.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } + if let control = self.leftButton2 { + leftButton2 = control + } else { + leftButton2 = CallControl(frame: .zero) + self.leftButton2 = leftButton2 + leftButton2.setFrameOrigin(leftButton1.frame.origin) + addSubview(leftButton2, positioned: .below, relativeTo: leftButton1) + if animated { + leftButton2.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } + leftButton2.updateWithData(CallControlData(text: hasText ? L10n.voiceChatVideoStreamScreencast : nil, mode: .animated(!hasScreencast ? .screenoff : .screenon, GroupCallTheme.settingsColor), iconSize: NSMakeSize(48, 48)), animated: animated) + + rightButton1.updateWithData(CallControlData(text: hasText ? L10n.voiceChatVideoStreamMore : nil, mode: .normal(GroupCallTheme.settingsColor, GroupCallTheme.settingsIcon), iconSize: NSMakeSize(48, 48)), animated: animated) + } + + end.updateWithData(CallControlData(text: hasText ? L10n.voiceChatLeave : nil, mode: .normal(GroupCallTheme.declineColor, GroupCallTheme.declineIcon), iconSize: NSMakeSize(48, 48)), animated: animated) + leftButton1.updateWithData(CallControlData(text: hasText ? leftButton1Text : nil, mode: leftBg, iconSize: NSMakeSize(48, 48)), animated: animated) + + if callMode != previousCallMode { + let from: CGFloat + let to: CGFloat + switch callMode { + case .video: + from = 1.0 + to = 0.42 + case .voice: + from = 0.42 + to = 1.0 + } + + let view = self.backgroundView + + + let rect = view.bounds + var fr = CATransform3DIdentity + fr = CATransform3DTranslate(fr, rect.width / 2, rect.height / 2, 0) + fr = CATransform3DScale(fr, to, to, 1) + fr = CATransform3DTranslate(fr, -(rect.width / 2), -(rect.height / 2), 0) + + if animated { + view.layer?.transform = CATransform3DIdentity + view.layer?.animateScaleCenter(from: from, to: to, duration: 0.2, removeOnCompletion: false, completion: { [weak view] completed in + if completed { + view?.layer?.transform = fr + view?.layer?.removeAnimation(forKey: "transform") + } + }) + } else { + view.layer?.transform = fr + } + } + } + self.mode = mode + self.callMode = callMode + self.hasVideo = hasVideo + self.hasScreencast = hasScreencast + } + + + override func layout() { + super.layout() + self.updateLayout(size: frame.size, transition: .immediate) + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + + switch callMode { + case .voice: + self.speak.frame = focus(NSMakeSize(140, 140)) + speak.update(size: NSMakeSize(140, 140), transition: .immediate) + + transition.updateFrame(view: self.leftButton1, frame: leftButton1.centerFrameY(x: 30)) + transition.updateFrame(view: self.end, frame: end.centerFrameY(x: frame.width - end.frame.width - 30)) + if let speakText = self.speakText { + let speakFrame = speakText.centerFrameX(y: self.speak.frame.maxY + floorToScreenPixels(backingScaleFactor, ((frame.height - speak.frame.maxY) - speakText.frame.height) / 2 - 33)) + transition.updateFrame(view: speakText, frame: speakFrame) + } + transition.updateFrame(view: self.backgroundView, frame: focus(.init(width: 360, height: 360))) + transition.updateFrame(view: self.fullscreenBackgroundView, frame: focus(.init(width: 250, height: 80))) + case .video: + let bgRect = focus(NSMakeSize(340, 70)) + self.speak.frame = focus(NSMakeSize(80, 80)) + speak.update(size: NSMakeSize(80, 80), transition: .immediate) + + let addition: CGFloat = mode == .normal ? 10 : 0 + + transition.updateFrame(view: self.leftButton1, frame: leftButton1.centerFrameY(x: bgRect.minX + 16, addition: addition)) + if let leftButton2 = self.leftButton2 { + transition.updateFrame(view: leftButton2, frame: leftButton1.centerFrameY(x: leftButton1.frame.maxX + 16, addition: addition)) + } + transition.updateFrame(view: self.end, frame: end.centerFrameY(x: bgRect.maxX - end.frame.width - 16, addition: addition)) + if let rightButton1 = self.rightButton1 { + transition.updateFrame(view: rightButton1, frame: leftButton1.centerFrameY(x: end.frame.minX - 16 - rightButton1.frame.width, addition: addition)) + } + + transition.updateFrame(view: self.backgroundView, frame: focus(.init(width: 360, height: 360))) + transition.updateFrame(view: self.fullscreenBackgroundView, frame: bgRect) + } + + if let tooltipView = tooltipView { + + var yOffset: CGFloat = 0 + switch callMode { + case .video: + if mode == .normal { + yOffset = 10 + } + case .voice: + yOffset = -20 + } + + var rect = CGRect(origin: CGPoint(x: fullscreenBackgroundView.frame.minX, y: fullscreenBackgroundView.frame.minY - tooltipView.frame.height - 5 + yOffset), size: tooltipView.frame.size) + + if tooltipView.toView == nil { + rect.origin.x = focus(rect.size).minX + } + + transition.updateFrame(view: tooltipView, frame: rect) + } + } + + + + fileprivate private(set) var currentState: GroupCallUIState? + private var leftToken: UInt32? + func update(_ callState: GroupCallUIState, voiceSettings: VoiceCallSettings, audioLevel: Float?, animated: Bool) { + + + + let mode: Mode = callState.isFullScreen && !callState.videoActive(.main).isEmpty ? .fullscreen : .normal + + let hidden: Bool = mode == .fullscreen || callState.mode == .video + + self.updateMode(mode, callMode: callState.mode, hasVideo: callState.hasVideo, hasScreencast: callState.hasScreencast, animated: animated) + + let state = callState.state + speak.update(with: state, isMuted: callState.isMuted, animated: animated) + + if let leftToken = leftToken { + leftButton1.removeHandler(leftToken) + } + leftToken = leftButton1.set(handler: { [weak self, weak callState] _ in + if let callState = callState { + switch callState.mode { + case .video: + if !callState.hasVideo { + self?.arguments?.shareSource(.video, false) + } else { + self?.arguments?.cancelShareVideo() + } + case .voice: + self?.arguments?.settings() + } + } + }, for: .SingleClick) + + + leftButton2?.removeAllHandlers() + leftButton2?.set(handler: { [weak self, weak callState] _ in + if let callState = callState, callState.hasScreencast { + self?.arguments?.cancelShareScreencast() + } else { + self?.arguments?.shareSource(.screencast, false) + } + }, for: .SingleClick) + + rightButton1?.removeAllHandlers() + rightButton1?.set(handler: { [weak self] _ in + self?.arguments?.settings() + }, for: .SingleClick) + + var backgroundState: VoiceChatActionButtonBackgroundView.State + if state.scheduleTimestamp == nil { + switch state.networkState { + case .connected: + if callState.isMuted { + if let muteState = callState.state.muteState { + if muteState.canUnmute { + backgroundState = .blob(false) + } else { + backgroundState = .disabled + } + } else { + backgroundState = .blob(true) + } + } else { + backgroundState = .blob(true) + } + case .connecting: + backgroundState = .connecting + } + } else { + backgroundState = .disabled + } + + self.backgroundView.isDark = false + self.backgroundView.update(state: backgroundState, animated: animated) + + self.backgroundView.audioLevel = CGFloat(audioLevel ?? 0) + + + let statusText: String + var secondary: String? = nil + if state.scheduleTimestamp == nil { + switch state.networkState { + case .connected: + if callState.isMuted { + if let muteState = state.muteState { + if muteState.canUnmute { + statusText = L10n.voiceChatClickToUnmute + switch voiceSettings.mode { + case .always: + if let pushToTalk = voiceSettings.pushToTalk, !pushToTalk.isSpace { + secondary = L10n.voiceChatClickToUnmuteSecondaryPress(pushToTalk.string) + } else { + secondary = L10n.voiceChatClickToUnmuteSecondaryPressDefault + } + case .pushToTalk: + if let pushToTalk = voiceSettings.pushToTalk, !pushToTalk.isSpace { + secondary = L10n.voiceChatClickToUnmuteSecondaryHold(pushToTalk.string) + } else { + secondary = L10n.voiceChatClickToUnmuteSecondaryHoldDefault + } + case .none: + secondary = nil + } + } else { + if !state.raisedHand { + statusText = L10n.voiceChatMutedByAdmin + secondary = L10n.voiceChatClickToRaiseHand + } else { + statusText = L10n.voiceChatRaisedHandTitle + secondary = L10n.voiceChatRaisedHandText + } + + } + } else { + statusText = L10n.voiceChatYouLive + } + } else { + statusText = L10n.voiceChatYouLive + } + case .connecting: + statusText = L10n.voiceChatConnecting + } + } else if let _ = state.scheduleTimestamp { + if state.canManageCall { + statusText = L10n.voiceChatStartNow + } else if state.subscribedToScheduled { + statusText = L10n.voiceChatRemoveReminder + } else { + statusText = L10n.voiceChatSetReminder + } + } else { + statusText = "" + } + + + let string = NSMutableAttributedString() + string.append(.initialize(string: statusText, color: .white, font: .medium(.title))) + if let secondary = secondary { + string.append(.initialize(string: "\n", color: .white, font: .medium(.text))) + string.append(.initialize(string: secondary, color: .white, font: .normal(.short))) + } + + if string.string != self.speakText?.layout?.attributedString.string { + let speakText = TextView() + speakText.userInteractionEnabled = false + speakText.isSelectable = false + let layout = TextViewLayout(string, alignment: .center) + layout.measure(width: frame.width - 60) + speakText.update(layout) + + if hidden { + speakText.layer?.opacity = 0 + } else { + speakText.layer?.opacity = 1 + } + + let animated = animated && !hidden + + if let speakText = self.speakText { + self.speakText = nil + if animated { + speakText.layer?.animateAlpha(from: 1, to: 0, duration: 0.3, removeOnCompletion: false, completion: { [weak speakText] _ in + speakText?.removeFromSuperview() + }) + speakText.layer?.animateScaleSpring(from: 1, to: 0.2, duration: 0.5) + } else { + speakText.removeFromSuperview() + } + } + + + self.speakText = speakText + addSubview(speakText) + speakText.centerX(y: speak.frame.maxY + floorToScreenPixels(backingScaleFactor, ((frame.height - speak.frame.maxY) - speakText.frame.height) / 2) - 33) + if animated { + speakText.layer?.animateAlpha(from: 0, to: 1, duration: 0.3) + speakText.layer?.animateScaleSpring(from: 0.2, to: 1, duration: 0.5) + } + } + + if self.currentState?.controlsTooltip != callState.controlsTooltip { + + if let tooltip = callState.controlsTooltip { + let current: GroupCallControlsTooltipView + var presented = false + if let view = self.tooltipView { + current = view + } else { + current = GroupCallControlsTooltipView(frame: .zero) + self.tooltipView = current + self.addSubview(current) + presented = true + + current.set(handler: { [weak self] _ in + self?.arguments?.dismissTooltip(tooltip) + }, for: .SingleClick) + } + let toView: NSView? + switch tooltip { + case .camera: + toView = self.leftButton1 + case .micro: + toView = nil + } + current.set(text: tooltip.text, maxWidth: 340, to: toView) + + if presented { + if animated { + current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + + var yOffset: CGFloat = 0 + switch callMode { + case .video: + if mode == .normal { + yOffset = 10 + } + case .voice: + yOffset = -20 + } + + var point = CGPoint(x: fullscreenBackgroundView.frame.minX, y: fullscreenBackgroundView.frame.minY - current.frame.height - 5 + yOffset) + + if current.toView == nil { + point.x = focus(current.frame.size).minX + } + current.setFrameOrigin(point) + } + + } else { + if let current = self.tooltipView { + self.tooltipView = nil + + if animated { + current.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak current] _ in + current?.removeFromSuperview() + }) + } else { + current.removeFromSuperview() + } + } + } + + } + + self.currentState = callState + + let transition: ContainedViewLayoutTransition = !animated ? .immediate : .animated(duration: 0.2, curve: .spring) + + updateLayout(size: frame.size, transition: transition) + } + + override var mouseDownCanMoveWindow: Bool { + return true + } + + + private var blue:NSColor { + return GroupCallTheme.speakInactiveColor + } + + private var lightBlue: NSColor { + return NSColor(rgb: 0x59c7f8) + } + + private var green: NSColor { + return GroupCallTheme.speakActiveColor + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/GroupCallDisplayAsController.swift b/Telegram-Mac/GroupCallDisplayAsController.swift new file mode 100644 index 0000000000..7ec56b5a28 --- /dev/null +++ b/Telegram-Mac/GroupCallDisplayAsController.swift @@ -0,0 +1,361 @@ +// +// GroupCallDisplayAsController.swift +// Telegram +// +// Created by Mikhail Filimonov on 02.03.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + + +import Cocoa +import TGUIKit +import SwiftSignalKit +import TelegramCore +import Postbox + + + +private final class DisplayMeAsHeaderItem : GeneralRowItem { + fileprivate let textLayout: TextViewLayout + init(_ initialSize: NSSize, stableId: AnyHashable, isAlone: Bool, isGroup: Bool) { + textLayout = .init(.initialize(string: isAlone ? L10n.displayMeAsAlone : isGroup ? L10n.displayMeAsTextGroup : L10n.displayMeAsText, color: theme.colors.listGrayText, font: .normal(.text)), alignment: .center) + super.init(initialSize, stableId: stableId) + } + override var height: CGFloat { + return textLayout.layoutSize.height + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + textLayout.measure(width: width - 60) + return true + } + + override func viewClass() -> AnyClass { + return DisplayMeAsHeaderView.self + } +} + +private final class DisplayMeAsHeaderView : TableRowView { + private let textView: TextView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(textView) + } + + override func layout() { + super.layout() + textView.center() + } + + override var backdorColor: NSColor { + return theme.colors.listBackground + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + guard let item = item as? DisplayMeAsHeaderItem else { + return + } + textView.update(item.textLayout) + needsLayout = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private final class Arguments { + let context: AccountContext + let canBeScheduled: Bool + let select:(PeerId)->Void + let toggleSchedule:()->Void + let updateScheduleDate:(Date)->Void + + init(context: AccountContext, canBeScheduled: Bool, select:@escaping(PeerId)->Void, toggleSchedule:@escaping()->Void, updateScheduleDate:@escaping(Date)->Void) { + self.context = context + self.select = select + self.canBeScheduled = canBeScheduled + self.toggleSchedule = toggleSchedule + self.updateScheduleDate = updateScheduleDate + } +} + +private struct State : Equatable { + var peer: PeerEquatable? + var list: [FoundPeer]? + var selected: PeerId + var schedule: Bool + var scheduleDate: Date? + var next: Int +} + +private func _id_peer(_ id:PeerId) -> InputDataIdentifier { + return InputDataIdentifier("_id_peer_\(id.toInt64())") +} +private let _id_schedule = InputDataIdentifier("_id_schedule") +private let _id_schedule_time = InputDataIdentifier("_id_schedule_time") +private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + let isGroup = state.peer?.peer.isGroup == true || state.peer?.peer.isSupergroup == true + + let isEmpty = state.list?.isEmpty == true + + struct T : Equatable { + let isGroup: Bool + let isAlone: Bool + } + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: .init("header"), equatable: InputDataEquatable(T(isGroup: isGroup, isAlone: isEmpty)), comparable: nil, item: { initialSize, stableId in + return DisplayMeAsHeaderItem(initialSize, stableId: stableId, isAlone: isEmpty, isGroup: isGroup) + })) + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + struct Tuple : Equatable { + let peer: FoundPeer + let viewType: GeneralViewType + let selected: Bool + let status: String? + } + + if let peer = state.peer { + + + let tuple = Tuple(peer: FoundPeer(peer: peer.peer, subscribers: nil), viewType: state.list == nil || !isEmpty ? .firstItem : .singleItem, selected: peer.peer.id == state.selected, status: L10n.displayMeAsPersonalAccount) + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: .init("self"), equatable: InputDataEquatable(tuple), comparable: nil, item: { initialSize, stableId in + return ShortPeerRowItem(initialSize, peer: tuple.peer.peer, account: arguments.context.account, stableId: stableId, height: 50, photoSize: NSMakeSize(36, 36), status: tuple.status, inset: NSEdgeInsets(left: 30, right: 30), interactionType: .plain, generalType: .selectable(tuple.selected), viewType: tuple.viewType, action: { + arguments.select(tuple.peer.peer.id) + }) + })) + + if isEmpty { + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.displayMeAsAloneDesc), data: .init(color: theme.colors.listGrayText, viewType: .textBottomItem))) + } + index += 1 + } + + if let list = state.list { + + if !list.isEmpty { + //TODOLANG + for peer in list { + + var status: String? + if let subscribers = peer.subscribers { + if peer.peer.isChannel { + status = L10n.voiceChatJoinAsChannelCountable(Int(subscribers)) + } else if peer.peer.isSupergroup || peer.peer.isGroup { + status = L10n.voiceChatJoinAsGroupCountable(Int(subscribers)) + } + } else { + status = L10n.chatChannelBadge + } + + var viewType = bestGeneralViewType(list, for: peer) + if list.first == peer { + if list.count == 1 { + viewType = .lastItem + } else { + viewType = .innerItem + } + } + + let tuple = Tuple(peer: peer, viewType: viewType, selected: peer.peer.id == state.selected, status: status) + + + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_peer(peer.peer.id), equatable: InputDataEquatable(tuple), comparable: nil, item: { initialSize, stableId in + return ShortPeerRowItem(initialSize, peer: tuple.peer.peer, account: arguments.context.account, stableId: stableId, height: 50, photoSize: NSMakeSize(36, 36), status: tuple.status, inset: NSEdgeInsets(left: 30, right: 30), interactionType: .plain, generalType: .selectable(tuple.selected), viewType: tuple.viewType, action: { + arguments.select(tuple.peer.peer.id) + }) + + })) + } + } + + } else { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: .init("loading"), equatable: nil, comparable: nil, item: { initialSize, stableId in + return GeneralLoadingRowItem(initialSize, stableId: stableId, viewType: .lastItem) + })) + index += 1 + } + + // entries + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + if arguments.canBeScheduled { + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_schedule, data: .init(name: L10n.displayMeAsScheduled, color: theme.colors.text, type: .switchable(state.schedule), viewType: state.schedule ? .firstItem : .singleItem, action: arguments.toggleSchedule))) + index += 1 + if state.schedule, let scheduleDate = state.scheduleDate { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_schedule_time, equatable: nil, comparable: nil, item: { initialSize, stableId in + return DatePickerRowItem(initialSize, stableId: stableId, viewType: .lastItem, initialDate: scheduleDate, update: arguments.updateScheduleDate) + })) + index += 1 + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.displayMeAsScheduledDesc(timerText(Int(scheduleDate.timeIntervalSince1970) - Int(Date().timeIntervalSince1970)))), data: .init(color: theme.colors.listGrayText, viewType: .textBottomItem))) + } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + } + + return entries +} + +enum GroupCallDisplayAsMode { + case join + case create +} + +func GroupCallDisplayAsController(context: AccountContext, mode: GroupCallDisplayAsMode, peerId: PeerId, list:[FoundPeer], completion: @escaping(PeerId, Date?)->Void, canBeScheduled: Bool) -> InputDataModalController { + + let actionsDisposable = DisposableSet() + var close:(()->Void)? = nil + + + let calendar = NSCalendar.current + var components = calendar.dateComponents([.hour, .day, .year, .month], from: Date()) + + components.setValue(components.hour! + 2, for: .hour) + + let initialState = State(list: list, selected: context.peerId, schedule: false, scheduleDate: calendar.date(from: components), next: 1) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let arguments = Arguments(context: context, canBeScheduled: canBeScheduled, select: { peerId in + updateState { current in + var current = current + current.selected = peerId + return current + } + }, toggleSchedule: { + updateState { current in + var current = current + current.schedule = !current.schedule + return current + } + }, updateScheduleDate: { date in + updateState { current in + var current = current + current.scheduleDate = date + return current + } + }) + + let signal = statePromise.get() |> deliverOnPrepareQueue |> map { state in + return InputDataSignalValue(entries: entries(state, arguments: arguments)) + } + + let list: Signal<[FoundPeer]?, NoError> = context.engine.calls.cachedGroupCallDisplayAsAvailablePeers(peerId: peerId) |> map(Optional.init) + let peerSignal = context.account.postbox.loadedPeerWithId(context.peerId) + + actionsDisposable.add(combineLatest(list, peerSignal).start(next: { list, peer in + updateState { current in + var current = current + current.list = list + current.peer = PeerEquatable(peer) + return current + } + })) + + let timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { + updateState { current in + var current = current + current.next += 1 + return current + } + }, queue: .mainQueue()) + + timer.start() + + let controller = InputDataController(dataSignal: signal, title: canBeScheduled ? L10n.displayMeAsNewTitle : L10n.displayMeAsTitle) + + controller.onDeinit = { + actionsDisposable.dispose() + } + + controller.contextOject = timer + + + controller.validateData = { _ in + let value = stateValue.with { ($0.selected, $0.schedule ? $0.scheduleDate : nil) } + + if let date = value.1 { + if date.timeIntervalSince1970 - Date().timeIntervalSince1970 <= 10 { + return .fail(.fields([_id_schedule_time : .shake])) + } + } + + completion(value.0, value.1) + close?() + return .none + } + + let modalInteractions = ModalInteractions(acceptTitle: "", accept: { [weak controller] in + _ = controller?.returnKeyAction() + }, drawBorder: true, height: 50, singleButton: true) + + + controller.afterTransaction = { [weak modalInteractions] _ in + modalInteractions?.updateDone { button in + let title: String = stateValue.with { value in + let peer = value.list?.first(where: { $0.peer.id == value.selected })?.peer ?? value.peer?.peer + return peer?.compactDisplayTitle ?? "" + } + let state = stateValue.with { $0 } + if canBeScheduled { + if state.schedule { + button.set(text: L10n.displayMeAsNewScheduleAs(title), for: .Normal) + } else { + button.set(text: L10n.displayMeAsNewStartAs(title), for: .Normal) + } + } else { + button.set(text: L10n.displayMeAsContinueAs(title), for: .Normal) + } + + } + } + + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) + + close = { [weak modalController] in + modalController?.modal?.close() + } + + return modalController +} + + +func selectGroupCallJoiner(context: AccountContext, peerId: PeerId, completion: @escaping(PeerId, Date?)->Void, canBeScheduled: Bool = false) { + _ = showModalProgress(signal: context.engine.calls.cachedGroupCallDisplayAsAvailablePeers(peerId: peerId), for: context.window).start(next: { displayAsList in + showModal(with: GroupCallDisplayAsController(context: context, mode: .create, peerId: peerId, list: displayAsList, completion: completion, canBeScheduled: canBeScheduled), for: context.window) + }) +} + +/* + + */ + + + diff --git a/Telegram-Mac/GroupCallInvitation.swift b/Telegram-Mac/GroupCallInvitation.swift new file mode 100644 index 0000000000..6d2a23eb12 --- /dev/null +++ b/Telegram-Mac/GroupCallInvitation.swift @@ -0,0 +1,593 @@ +// +// GroupCallInv.swift +// Telegram +// +// Created by Mikhail Filimonov on 12.12.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit +import Postbox + +import TelegramCore + +private final class InvitationArguments { + let account: Account + let copyLink: (String)->Void + let inviteGroupMember:(PeerId)->Void + let inviteContact:(PeerId)->Void + init(account: Account, copyLink: @escaping(String)->Void, inviteGroupMember:@escaping(PeerId)->Void, inviteContact:@escaping(PeerId)->Void) { + self.account = account + self.copyLink = copyLink + self.inviteGroupMember = inviteGroupMember + self.inviteContact = inviteContact + } +} + +private struct InvitationPeer : Equatable { + let peer: Peer + let presence: PeerPresence? + let contact: Bool + let enabled: Bool + static func ==(lhs:InvitationPeer, rhs: InvitationPeer) -> Bool { + if !lhs.peer.isEqual(rhs.peer) { + return false + } + if let lhsPresence = lhs.presence, let rhsPresence = rhs.presence { + return lhsPresence.isEqual(to: rhsPresence) + } else if (lhs.presence != nil) != (rhs.presence != nil) { + return false + } + if lhs.contact != rhs.contact { + return false + } + if lhs.enabled != rhs.enabled { + return false + } + return true + } +} + + +final class GroupCallAddMembersBehaviour : SelectPeersBehavior { + fileprivate let data: GroupCallUIController.UIData + private let disposable = MetaDisposable() + private let window: Window + init(data: GroupCallUIController.UIData, window: Window) { + self.data = data + self.window = window + super.init(settings: [], excludePeerIds: [], limit: 1, customTheme: { GroupCallTheme.customTheme }) + } + + private let cachedContacts:Atomic<[PeerId]> = Atomic(value: []) + func isContact(_ peerId: PeerId) -> Bool { + return cachedContacts.with { + $0.contains(peerId) + } + } + + override func start(context: AccountContext, search: Signal, linkInvation: ((Int) -> Void)? = nil) -> Signal<([SelectPeerEntry], Bool), NoError> { + + + let peerMemberContextsManager = data.peerMemberContextsManager + let account = data.call.account + let peerId = data.call.peerId + let engine = data.call.engine + let customTheme = self.customTheme + let cachedContacts = self.cachedContacts + let members = data.call.members |> filter { $0 != nil } |> map { $0! } + let invited = data.call.invitedPeers + let peer = data.call.peer + let isUnmutedForAll: Signal = data.call.state |> take(1) |> map { value in + if let muteState = value.defaultParticipantMuteState { + switch muteState { + case .muted: + return false + case .unmuted: + return true + } + } + return false + } + return search |> mapToSignal { search in + var contacts:Signal<([Peer], [PeerId : PeerPresence]), NoError> + if search.request.isEmpty { + contacts = account.postbox.contactPeersView(accountPeerId: account.peerId, includePresences: true) |> map { + return ($0.peers, $0.peerPresences) + } + } else { + contacts = account.postbox.searchContacts(query: search.request) + } + contacts = combineLatest(account.postbox.peerView(id: peerId), contacts) |> map { peerView, contacts in + if let peer = peerViewMainPeer(peerView) { + if peer.groupAccess.canAddMembers { + return contacts + } else { + return ([], [:]) + } + } else { + return ([], [:]) + } + } + + let globalSearch: Signal<[Peer], NoError> + if search.request.isEmpty { + globalSearch = .single([]) + } else if let peer = peer, peer.groupAccess.canAddMembers { + globalSearch = engine.peers.searchPeers(query: search.request.lowercased()) |> map { + return $0.0.map { + $0.peer + } + $0.1.map { + $0.peer + } + } + } else { + globalSearch = .single([]) + } + + struct Participant { + let peer: Peer + let presence: PeerPresence? + } + + let groupMembers:Signal<[Participant], NoError> = Signal { subscriber in + let disposable: Disposable + if peerId.namespace == Namespaces.Peer.CloudChannel { + (disposable, _) = peerMemberContextsManager.recent(peerId: peerId, searchQuery: search.request.isEmpty ? nil : search.request, updated: { state in + if case .ready = state.loadingState { + subscriber.putNext(state.list.map { + return Participant(peer: $0.peer, presence: $0.presences[$0.peer.id]) + }) + subscriber.putCompletion() + } + }) + } else { + let signal: Signal<[Participant], NoError> = account.postbox.peerView(id: peerId) |> map { peerView in + let participants = (peerView.cachedData as? CachedGroupData)?.participants + let list:[Participant] = participants?.participants.compactMap { value in + if let peer = peerView.peers[value.peerId] { + return Participant(peer: peer, presence: peerView.peerPresences[value.peerId]) + } else { + return nil + } + } ?? [] + return list + } + disposable = signal.start(next: { list in + subscriber.putNext(list) + }) + } + return disposable + } + + + let allMembers: Signal<([InvitationPeer], [InvitationPeer], [InvitationPeer]), NoError> = combineLatest(groupMembers, members, contacts, globalSearch, invited) |> map { recent, participants, contacts, global, invited in + let membersList = recent.filter { value in + if participants.participants.contains(where: { $0.peer.id == value.peer.id }) { + return false + } + return !value.peer.isBot + }.map { + InvitationPeer(peer: $0.peer, presence: $0.presence, contact: false, enabled: !invited.contains($0.peer.id)) + } + var contactList:[InvitationPeer] = [] + for contact in contacts.0 { + let containsInCall = participants.participants.contains(where: { $0.peer.id == contact.id }) + let containsInMembers = membersList.contains(where: { $0.peer.id == contact.id }) + if !containsInMembers && !containsInCall { + contactList.append(InvitationPeer(peer: contact, presence: contacts.1[contact.id], contact: true, enabled: !invited.contains(contact.id))) + } + } + + var globalList:[InvitationPeer] = [] + + for peer in global { + let containsInCall = participants.participants.contains(where: { $0.peer.id == peer.id }) + let containsInMembers = membersList.contains(where: { $0.peer.id == peer.id }) + let containsInContacts = contactList.contains(where: { $0.peer.id == peer.id }) + + if !containsInMembers && !containsInCall && !containsInContacts { + if !peer.isBot && peer.isUser { + globalList.append(.init(peer: peer, presence: nil, contact: false, enabled: !invited.contains(peer.id))) + } + } + } + + _ = cachedContacts.swap(contactList.map { $0.peer.id } + globalList.map { $0.peer.id }) + return (membersList, contactList, globalList) + } + + let previousSearch: Atomic = Atomic(value: "") + return combineLatest(queue: .mainQueue(), allMembers, isUnmutedForAll) |> map { members, isUnmutedForAll in + var entries:[SelectPeerEntry] = [] + var index:Int32 = 0 + if search.request.isEmpty { + if let linkInvation = linkInvation, let peer = peer { + if peer.groupAccess.canMakeVoiceChat { + if peer.isSupergroup, isUnmutedForAll { + entries.append(.inviteLink(L10n.voiceChatInviteCopyInviteLink, GroupCallTheme.invite_link, 0, customTheme(), linkInvation)) + } else { + entries.append(.inviteLink(L10n.voiceChatInviteCopyListenersLink, GroupCallTheme.invite_listener, 0, customTheme(), linkInvation)) + entries.append(.inviteLink(L10n.voiceChatInviteCopySpeakersLink, GroupCallTheme.invite_speaker, 1, customTheme(), linkInvation)) + } + } else { + entries.append(.inviteLink(L10n.voiceChatInviteCopyInviteLink, GroupCallTheme.invite_link, 0, customTheme(), linkInvation)) + } + } + } + + if !members.0.isEmpty { + entries.append(.separator(index, customTheme(), L10n.voiceChatInviteGroupMembers)) + index += 1 + } + + + for member in members.0 { + entries.append(.peer(SelectPeerValue(peer: member.peer, presence: member.presence, subscribers: nil, customTheme: customTheme()), index, member.enabled)) + index += 1 + } + + if !members.1.isEmpty { + entries.append(.separator(index, customTheme(), L10n.voiceChatInviteContacts)) + index += 1 + } + + for member in members.1 { + entries.append(.peer(SelectPeerValue(peer: member.peer, presence: member.presence, subscribers: nil, customTheme: customTheme()), index, member.enabled)) + index += 1 + } + + if !members.2.isEmpty { + entries.append(.separator(index, customTheme(), L10n.voiceChatInviteGlobalSearch)) + index += 1 + } + + + for member in members.2 { + entries.append(.peer(SelectPeerValue(peer: member.peer, presence: member.presence, subscribers: nil, customTheme: customTheme()), index, member.enabled)) + index += 1 + } + + + + let updatedSearch = previousSearch.swap(search.request) != search.request + + if entries.isEmpty { + entries.append(.searchEmpty(customTheme(), NSImage(named: "Icon_EmptySearchResults")!.precomposed(customTheme().grayTextColor))) + } + + return (entries, updatedSearch) + } + } + } + + deinit { + var bp:Int = 0 + bp += 1 + } +} + +final class GroupCallInviteMembersBehaviour : SelectPeersBehavior { + fileprivate let data: GroupCallUIController.UIData + private let disposable = MetaDisposable() + private let window: Window + init(data: GroupCallUIController.UIData, window: Window) { + self.data = data + self.window = window + super.init(settings: [], excludePeerIds: [], limit: 100, customTheme: { GroupCallTheme.customTheme }) + } + + override var okTitle: String? { + return L10n.voiceChatInviteInvite + } + + private let cachedContacts:Atomic<[PeerId]> = Atomic(value: []) + func isContact(_ peerId: PeerId) -> Bool { + return cachedContacts.with { + $0.contains(peerId) + } + } + + override func start(context: AccountContext, search: Signal, linkInvation: ((Int) -> Void)? = nil) -> Signal<([SelectPeerEntry], Bool), NoError> { + + let account = data.call.account + let peerId = data.call.peerId + let engine = data.call.engine + let customTheme = self.customTheme + let cachedContacts = self.cachedContacts + let members = data.call.members |> filter { $0 != nil } |> map { $0! } + let invited = data.call.invitedPeers + let peer = data.call.peer + let isUnmutedForAll: Signal = data.call.state |> take(1) |> map { value in + if let muteState = value.defaultParticipantMuteState { + switch muteState { + case .muted: + return false + case .unmuted: + return true + } + } + return false + } + return search |> mapToSignal { search in + var dialogs:Signal<([Peer]), NoError> + if search.request.isEmpty { + dialogs = account.viewTracker.tailChatListView(groupId: .root, count: 100) |> map { value in + var entries:[Peer] = [] + for entry in value.0.entries.reversed() { + switch entry { + case let .MessageEntry(_, _, _, _, _, renderedPeer, _, _, _, _): + if let peer = renderedPeer.chatMainPeer, peer.canSendMessage() { + entries.append(peer) + } + default: + break + } + } + return entries + } + } else { + dialogs = .single([]) + } + + let globalSearch: Signal<[Peer], NoError> + if search.request.isEmpty { + globalSearch = .single([]) + } else { + globalSearch = engine.peers.searchPeers(query: search.request.lowercased()) |> map { + return $0.0.map { + $0.peer + } + $0.1.map { + $0.peer + } + } + } + + struct Participant { + let peer: Peer + let presence: PeerPresence? + } + + + let allMembers: Signal<([InvitationPeer], [InvitationPeer]), NoError> = combineLatest(members, dialogs, globalSearch, invited) |> map { recent, contacts, global, invited in + let membersList = recent.participants.map { + InvitationPeer(peer: $0.peer, presence: nil, contact: false, enabled: !invited.contains($0.peer.id)) + } + var contactList:[InvitationPeer] = [] + for contact in contacts { + let containsInMembers = membersList.contains(where: { $0.peer.id == contact.id }) + if !containsInMembers { + contactList.append(InvitationPeer(peer: contact, presence: nil, contact: true, enabled: !invited.contains(contact.id))) + } + } + + var globalList:[InvitationPeer] = [] + + for peer in global { + let containsInMembers = membersList.contains(where: { $0.peer.id == peer.id }) + let containsInContacts = contactList.contains(where: { $0.peer.id == peer.id }) + + if !containsInMembers && !containsInContacts { + if peer.canSendMessage() { + globalList.append(.init(peer: peer, presence: nil, contact: false, enabled: !invited.contains(peer.id))) + } + } + } + + _ = cachedContacts.swap(contactList.map { $0.peer.id } + globalList.map { $0.peer.id }) + return (contactList, globalList) + } + + let inviteLink: Signal = account.viewTracker.peerView(peerId) |> map { peerView in + if let peer = peerViewMainPeer(peerView) { + return peer.groupAccess.canMakeVoiceChat + } + return (false) + } + + let previousSearch: Atomic = Atomic(value: "") + return combineLatest(allMembers, inviteLink, isUnmutedForAll) |> map { members, inviteLink, isUnmutedForAll in + var entries:[SelectPeerEntry] = [] + var index:Int32 = 0 + if search.request.isEmpty { + if let linkInvation = linkInvation, let peer = peer { + if peer.addressName != nil { + if peer.groupAccess.canMakeVoiceChat { + if peer.isSupergroup, isUnmutedForAll { + entries.append(.inviteLink(L10n.voiceChatInviteCopyInviteLink, GroupCallTheme.invite_link, 0, customTheme(), linkInvation)) + } else { + entries.append(.inviteLink(L10n.voiceChatInviteCopyListenersLink, GroupCallTheme.invite_listener, 0, customTheme(), linkInvation)) + entries.append(.inviteLink(L10n.voiceChatInviteCopySpeakersLink, GroupCallTheme.invite_speaker, 1, customTheme(), linkInvation)) + } + } else { + entries.append(.inviteLink(L10n.voiceChatInviteCopyInviteLink, GroupCallTheme.invite_link, 0, customTheme(), linkInvation)) + } + } else { + entries.append(.inviteLink(L10n.voiceChatInviteCopyInviteLink, GroupCallTheme.invite_link, 0, customTheme(), linkInvation)) + } + } + } + + if !members.0.isEmpty { + entries.append(.separator(index, customTheme(), L10n.voiceChatInviteChats)) + index += 1 + } + + for member in members.0 { + entries.append(.peer(SelectPeerValue(peer: member.peer, presence: member.presence, subscribers: nil, customTheme: customTheme(), ignoreStatus: true), index, member.enabled)) + index += 1 + } + + if !members.1.isEmpty { + entries.append(.separator(index, customTheme(), L10n.voiceChatInviteGlobalSearch)) + index += 1 + } + + + for member in members.1 { + entries.append(.peer(SelectPeerValue(peer: member.peer, presence: nil, subscribers: nil, customTheme: customTheme(), ignoreStatus: true), index, member.enabled)) + index += 1 + } + + let updatedSearch = previousSearch.swap(search.request) != search.request + + if entries.isEmpty { + entries.append(.searchEmpty(customTheme(), NSImage(named: "Icon_EmptySearchResults")!.precomposed(customTheme().grayTextColor))) + } + + return (entries, updatedSearch) + } + } + } +} + +func GroupCallAddmembers(_ data: GroupCallUIController.UIData, window: Window) -> Signal<[PeerId], NoError> { + + let behaviour: SelectPeersBehavior + let title: String + if let peer = data.call.peer, peer.isChannel { + title = L10n.voiceChatInviteChannelsTitle + behaviour = GroupCallInviteMembersBehaviour(data: data, window: window) + } else { + title = L10n.voiceChatInviteTitle + behaviour = GroupCallAddMembersBehaviour(data: data, window: window) + } + let account = data.call.account + let context = data.call.accountContext + let callPeerId = data.call.peerId + let peerMemberContextsManager = data.peerMemberContextsManager + + let peer = data.call.peer + let links = data.call.inviteLinks + return selectModalPeers(window: window, context: data.call.accountContext, title: title, settings: [], excludePeerIds: [], limit: behaviour is GroupCallAddMembersBehaviour ? 1 : 100, behavior: behaviour, confirmation: { [weak behaviour, weak window, weak data] peerIds in + + + if let behaviour = behaviour as? GroupCallAddMembersBehaviour { + guard let peerId = peerIds.first else { + return .single(false) + } + if behaviour.isContact(peerId) { + return account.postbox.transaction { + return (user: $0.getPeer(peerId), chat: $0.getPeer(callPeerId)) + } |> mapToSignal { [weak window] values in + if let window = window { + return confirmSignal(for: window, information: L10n.voiceChatInviteMemberToGroupFirstText(values.user?.displayTitle ?? "", values.chat?.displayTitle ?? ""), okTitle: L10n.voiceChatInviteMemberToGroupFirstAdd, appearance: darkPalette.appearance) |> filter { $0 } + |> take(1) + |> mapToSignal { _ in + if peerId.namespace == Namespaces.Peer.CloudChannel { + return peerMemberContextsManager.addMember(peerId: callPeerId, memberId: peerId) |> map { _ in + return true + } + } else { + return context.engine.peers.addGroupMember(peerId: callPeerId, memberId: peerId) + |> map { + return true + } |> `catch` { _ in + return .single(false) + } + } + + } |> deliverOnMainQueue + } else { + return .single(false) + } + } + } else { + return .single(true) + } + } else if let call = data?.call { + + let isUnmutedForAll: Signal = call.state |> take(1) |> map { value in + if let muteState = value.defaultParticipantMuteState { + switch muteState { + case .muted: + return false + case .unmuted: + return true + } + } + return false + } + + return combineLatest(queue: .mainQueue(), links, isUnmutedForAll) |> mapToSignal { [weak window] links, isUnmutedForAll in + return Signal { [weak window] subscriber in + if let window = window, let links = links, let peer = peer { + let third: String? + if peer.groupAccess.canMakeVoiceChat, peer.addressName != nil { + if peer.isSupergroup && isUnmutedForAll { + third = nil + } else { + third = L10n.voiceChatInviteConfirmThird + } + } else { + third = nil + } + + if let third = third { + modernConfirm(for: window, header: L10n.voiceChatInviteConfirmHeader, information: L10n.voiceChatInviteConfirmText, okTitle: L10n.voiceChatInviteConfirmOK, cancelTitle: L10n.modalCancel, thridTitle: third, successHandler: { result in + + let link: String + switch result { + case .basic: + link = links.listenerLink + case .thrid: + link = links.speakerLink ?? links.listenerLink + } + for peerId in peerIds { + _ = enqueueMessages(account: account, peerId: peerId, messages: [EnqueueMessage.message(text: link, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)]).start() + } + + subscriber.putNext(true) + subscriber.putCompletion() + + }, appearance: GroupCallTheme.customTheme.appearance) + } else { + for peerId in peerIds { + _ = enqueueMessages(account: account, peerId: peerId, messages: [EnqueueMessage.message(text: links.listenerLink, attributes: [], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)]).start() + } + subscriber.putNext(true) + subscriber.putCompletion() + } + + } else { + subscriber.putNext(false) + subscriber.putCompletion() + } + + return EmptyDisposable + } + } + } else { + return .single(false) + } + + }, linkInvation: { [weak window] index in + + if let peer = peer { + if let window = window { + if peer.addressName != nil { + _ = showModalProgress(signal: links, for: window).start(next: { [weak window] links in + if let links = links, let window = window { + if index == 0 { + copyToClipboard(links.listenerLink) + } else if let speakerLink = links.speakerLink { + copyToClipboard(speakerLink) + } + showModalText(for: window, text: L10n.shareLinkCopied) + } + }) + } else { + _ = showModalProgress(signal: permanentExportedInvitation(context: context, peerId: callPeerId), for: window).start(next: { [weak window] link in + if let link = link, let window = window { + copyToClipboard(link.link) + showModalText(for: window, text: L10n.shareLinkCopied) + } + }) + } + } + } + }) + +} diff --git a/Telegram-Mac/GroupCallInviteRowItem.swift b/Telegram-Mac/GroupCallInviteRowItem.swift new file mode 100644 index 0000000000..22984c635b --- /dev/null +++ b/Telegram-Mac/GroupCallInviteRowItem.swift @@ -0,0 +1,129 @@ +// +// GroupCallInviteRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 30.03.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import TelegramCore +import Postbox +import SwiftSignalKit + + + +final class GroupCallInviteRowItem : GeneralRowItem { + fileprivate let videoMode: Bool + init(_ initialSize: NSSize, height: CGFloat, stableId: AnyHashable, videoMode: Bool, viewType: GeneralViewType = .legacy, action: @escaping () -> Void) { + self.videoMode = videoMode + super.init(initialSize, height: height, stableId: stableId, viewType: viewType, action: action, inset: NSEdgeInsets()) + } + + override var width: CGFloat { + if let superview = table?.superview { + return superview.frame.width + } else { + return super.width + } + } + + var isVertical: Bool { + return false + } + + override var hasBorder: Bool { + return !isVertical + } + + override var instantlyResize: Bool { + return false + } + + override func viewClass() -> AnyClass { + return GroupCallInviteRowView.self + } +} + + +private final class GroupCallInviteRowView : GeneralContainableRowView { + private let textView = TextView() + private let thumbView = ImageView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(thumbView) + addSubview(textView) + + thumbView.isEventLess = true + textView.userInteractionEnabled = false + textView.isSelectable = false + + containerView.set(handler: { [weak self] _ in + self?.updateColors() + }, for: .Hover) + + containerView.set(handler: { [weak self] _ in + self?.updateColors() + }, for: .Normal) + + containerView.set(handler: { [weak self] _ in + self?.updateColors() + }, for: .Highlight) + + containerView.set(handler: { [weak self] _ in + guard let item = self?.item as? GroupCallInviteRowItem else { + return + } + item.action() + }, for: .Click) + } + + override func updateColors() { + super.updateColors() + let color = containerView.controlState == .Highlight ? self.backdorColor.lighter() : self.backdorColor + containerView.backgroundColor = color + textView.backgroundColor = color + } + + override var borderColor: NSColor { + return GroupCallTheme.customTheme.borderColor + } + + override var backdorColor: NSColor { + return GroupCallTheme.membersColor + } + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + guard let item = item as? GroupCallInviteRowItem else { + return + } + + thumbView.image = GroupCallTheme.inviteIcon + thumbView.sizeToFit() + + textView.change(opacity: item.isVertical ? 0 : 1, animated: animated) + + let layout = TextViewLayout(.initialize(string: L10n.voiceChatInviteInviteMembers, color: GroupCallTheme.customTheme.textColor, font: .normal(.title))) + layout.measure(width: .greatestFiniteMagnitude) + textView.update(layout) + + self.layout() + + if item.isVertical { + thumbView.change(pos: NSMakePoint(floorToScreenPixels(backingScaleFactor, (160 - thumbView.frame.width) / 2), floorToScreenPixels(backingScaleFactor, (containerView.frame.height - thumbView.frame.height) / 2)), animated: animated) + } else { + thumbView.change(pos: NSMakePoint(item.viewType.innerInset.left, floorToScreenPixels(backingScaleFactor, (containerView.frame.height - thumbView.frame.height) / 2)), animated: animated) + textView.change(pos: NSMakePoint(thumbView.frame.maxX + 20, floorToScreenPixels(backingScaleFactor, (containerView.frame.height - textView.frame.height) / 2)), animated: animated) + } + + } + + override var additionBorderInset: CGFloat { + return 44 + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/GroupCallMainVideoContainer.swift b/Telegram-Mac/GroupCallMainVideoContainer.swift new file mode 100644 index 0000000000..e8006697d1 --- /dev/null +++ b/Telegram-Mac/GroupCallMainVideoContainer.swift @@ -0,0 +1,670 @@ +// +// GroupCallMainVideoContainer.swift +// Telegram +// +// Created by Mikhail Filimonov on 06.04.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit +import TelegramCore +import Postbox + + +private final class PinView : Control { + private let imageView:ImageView = ImageView() + private var textView: TextView? + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(imageView) + backgroundColor = GroupCallTheme.windowBackground.withAlphaComponent(0.9) + imageView.isEventLess = true + scaleOnClick = true + set(background: GroupCallTheme.windowBackground.withAlphaComponent(0.7), for: .Highlight) + } + + func update(_ isPinned: Bool, animated: Bool) { + if isPinned { + var isNew: Bool = false + let current: TextView + if let c = self.textView { + current = c + } else { + current = TextView() + self.textView = current + current.userInteractionEnabled = false + current.isSelectable = false + addSubview(current, positioned: .below, relativeTo: imageView) + if animated { + current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + isNew = true + } + + let textLayout = TextViewLayout(.initialize(string: L10n.voiceChatVideoShortUnpin, color: GroupCallTheme.customTheme.textColor, font: .medium(.title))) + textLayout.measure(width: .greatestFiniteMagnitude) + current.update(textLayout) + + if isNew { + textView?.centerY(x: 10, addition: -1) + } + } else { + if let textView = textView { + self.textView = nil + if animated { + textView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak textView] _ in + textView?.removeFromSuperview() + }) + } else { + textView.removeFromSuperview() + } + } + } + imageView.animates = true + imageView.image = !isPinned ? GroupCallTheme.pin_video :GroupCallTheme.unpin_video + imageView.sizeToFit() + layer?.cornerRadius = (imageView.frame.height + 10) / 2 + } + + func size(_ isPinned: Bool) -> NSSize { + if let textView = textView { + return imageView.frame.size + NSMakeSize(textView.frame.width, 0) + NSMakeSize(20, 10) + } else { + return imageView.frame.size + NSMakeSize(10, 10) + } + } + + func updateLayout(size: NSSize, transition: ContainedViewLayoutTransition) { + transition.updateFrame(view: imageView, frame: imageView.centerFrameY(x: frame.width - imageView.frame.width - 5)) + if let textView = textView { + let textFrame = textView.centerFrameY(x: 10, addition: -1) + transition.updateFrame(view: textView, frame: textFrame) + } + } + + override func layout() { + super.layout() + updateLayout(size: frame.size, transition: .immediate) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private final class BackView : Control { + private let imageView:ImageView = ImageView() + private var textView: TextView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(imageView) + + addSubview(textView) + + + backgroundColor = GroupCallTheme.windowBackground.withAlphaComponent(0.9) + imageView.isEventLess = true + scaleOnClick = true + set(background: GroupCallTheme.windowBackground.withAlphaComponent(0.7), for: .Highlight) + + let textLayout = TextViewLayout(.initialize(string: L10n.navigationBack, color: GroupCallTheme.customTheme.textColor, font: .medium(.title))) + textLayout.measure(width: .greatestFiniteMagnitude) + textView.update(textLayout) + + imageView.animates = false + imageView.image = GroupCallTheme.video_back + imageView.sizeToFit() + + setFrameSize(NSMakeSize(imageView.frame.width + textView.frame.width + 25, 30)) + + layer?.cornerRadius = frame.height / 2 + + imageView.isEventLess = true + textView.userInteractionEnabled = false + textView.isSelectable = false + + layout() + } + + func updateLayout(size: NSSize, transition: ContainedViewLayoutTransition) { + transition.updateFrame(view: imageView, frame: imageView.centerFrameY(x: 10)) + let textFrame = textView.centerFrameY(x: frame.width - textView.frame.width - 10, addition: -1) + transition.updateFrame(view: textView, frame: textFrame) + } + + override func layout() { + super.layout() + updateLayout(size: frame.size, transition: .immediate) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private final class SelfPresentationPlaceholder : View { + private let textView = TextView() + private let button = TitleButton() + private let visualEffect = NSVisualEffectView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(visualEffect) + addSubview(textView) + addSubview(button) + + button.autohighlight = false + button.scaleOnClick = true + + textView.userInteractionEnabled = false + textView.isSelectable = false + + visualEffect.wantsLayer = true + visualEffect.material = .ultraDark + visualEffect.blendingMode = .withinWindow + visualEffect.state = .active + } + + func update(stop: @escaping()->Void) { + + let textLayout = TextViewLayout(.initialize(string: L10n.voiceChatSharingPlaceholder, color: GroupCallTheme.customTheme.textColor, font: .medium(.text)), alignment: .center) + textLayout.measure(width: frame.width - 40) + textView.update(textLayout) + + button.set(text: L10n.voiceChatSharingStop, for: .Normal) + button.set(font: .medium(.text), for: .Normal) + button.set(color: GroupCallTheme.customTheme.textColor, for: .Normal) + button.sizeToFit(NSMakeSize(50, 10), .zero, thatFit: false) + button.set(background: GroupCallTheme.customTheme.accentColor, for: .Normal) + button.set(background: GroupCallTheme.customTheme.accentColor.withAlphaComponent(0.8), for: .Highlight) + button.layer?.cornerRadius = 4 + + button.removeAllHandlers() + button.set(handler: { _ in + stop() + }, for: .SingleClick) + } + + override func layout() { + super.layout() + updateLayout(size: frame.size, transition: .immediate) + } + + func updateLayout(size: NSSize, transition: ContainedViewLayoutTransition) { + textView.resize(frame.width - 40) + var textRect = size.bounds.focus(textView.frame.size) + textRect.origin.y = frame.midY - textView.frame.height - 5 + transition.updateFrame(view: textView, frame: textRect) + + var buttonRect = size.bounds.focus(button.frame.size) + buttonRect.origin.y = frame.midY + 5 + transition.updateFrame(view: button, frame: buttonRect) + + if let window = window { + visualEffect.frame = window.bounds + } + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +struct DominantVideo : Equatable { + + enum PinMode { + case permanent + case focused + } + + let peerId: PeerId + let endpointId: String + let mode: VideoSourceMacMode + let pinMode: PinMode? + init(_ peerId: PeerId, _ endpointId: String, _ mode: VideoSourceMacMode, _ pinMode: PinMode?) { + self.peerId = peerId + self.endpointId = endpointId + self.mode = mode + self.pinMode = pinMode + } +} + +final class GroupCallMainVideoContainerView: Control { + private let call: PresentationGroupCall + + private class V : NSVisualEffectView { + override var mouseDownCanMoveWindow: Bool { + return true + } + } + + private(set) var currentVideoView: GroupVideoView? + private(set) var currentPeer: DominantVideo? + + let shadowView: ShadowView = ShadowView() + + private var validLayout: CGSize? + + private let nameView: TextView = TextView() + private let statusView = ImageView() + + private let speakingView: View = View() + private let audioLevelDisposable = MetaDisposable() + + private var arguments: GroupCallUIArguments? + + private var backView: BackView? + private var pinView: PinView? + + + private var pausedTextView: TextView? + private var pausedImageView: ImageView? + + + private var selfPresentationPlaceholder: SelfPresentationPlaceholder? + + init(call: PresentationGroupCall) { + self.call = call + super.init() + + + speakingView.layer?.cornerRadius = 10 + speakingView.layer?.borderWidth = 2 + speakingView.layer?.borderColor = GroupCallTheme.speakActiveColor.cgColor + + self.backgroundColor = GroupCallTheme.membersColor + addSubview(shadowView) + + shadowView.shadowBackground = NSColor.black.withAlphaComponent(0.3) + shadowView.direction = .vertical(true) + + self.layer?.cornerRadius = 10 + + addSubview(nameView) + addSubview(statusView) + + self.forceMouseDownCanMoveWindow = true + + + +// backstage.wantsLayer = true +// backstage.material = .dark +// backstage.blendingMode = .withinWindow +// if #available(OSX 10.12, *) { +// backstage.isEmphasized = true +// } +// backstage.state = .active + + + nameView.userInteractionEnabled = false + nameView.isSelectable = false + + statusView.isEventLess = true + + addSubview(speakingView) + + self.set(handler: { [weak self] _ in + if let dominant = self?.currentPeer, self?.isPinned == false { + self?.arguments?.focusVideo(dominant.endpointId) + } + }, for: .SingleClick) + + self.set(handler: { [weak self] control in + if let data = self?.participant { + if let menuItems = self?.arguments?.contextMenuItems(data), let event = NSApp.currentEvent { + ContextMenu.show(items: menuItems, view: control, event: event) + } + } + }, for: .RightDown) + + self.set(handler: { [weak self] control in + self?.pinView?.change(opacity: self?.pinIsVisible == true ? 1 : 0, animated: true) + self?.backView?.change(opacity: self?.pinIsVisible == true ? 1 : 0, animated: true) + }, for: .Hover) + + self.set(handler: { [weak self] control in + self?.pinView?.change(opacity: self?.pinIsVisible == true ? 1 : 0, animated: true) + self?.backView?.change(opacity: self?.pinIsVisible == true ? 1 : 0, animated: true) + }, for: .Highlight) + + self.set(handler: { [weak self] control in + self?.pinView?.change(opacity: 0, animated: true) + self?.backView?.change(opacity: 0, animated: true) + }, for: .Normal) + + } + + private var pinIsVisible: Bool { + return ((self.isFocused && !isAlone) || self.isPinned) + } + + override var mouseDownCanMoveWindow: Bool { + return true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + func updateMode(controlsMode: GroupCallView.ControlsMode, controlsState: GroupCallControlsView.Mode, animated: Bool) { + shadowView.change(opacity: controlsMode == .normal ? 1 : 0, animated: animated) + + nameView.change(opacity: controlsMode == .normal ? 1 : 0, animated: animated) + statusView.change(opacity: controlsMode == .normal ? 1 : 0, animated: animated) + self.pinView?.change(opacity: self.mouseInside() && self.pinIsVisible ? 1 : 0, animated: animated) + self.backView?.change(opacity: self.mouseInside() && self.pinIsVisible ? 1 : 0, animated: animated) + } + + private var participant: PeerGroupCallData? + + private var isPinned: Bool = false + private var isFocused: Bool = false + private var isAlone: Bool = false + func updatePeer(peer: DominantVideo?, participant: PeerGroupCallData?, resizeMode: CALayerContentsGravity, transition: ContainedViewLayoutTransition, animated: Bool, controlsMode: GroupCallView.ControlsMode, isPinned: Bool, isFocused: Bool, isAlone: Bool, arguments: GroupCallUIArguments?) { + + + self.isFocused = isFocused + self.isPinned = isPinned + self.isAlone = isAlone + self.arguments = arguments + + + if self.pinIsVisible { + let currentPinView: PinView + if let current = self.pinView { + currentPinView = current + } else { + currentPinView = PinView(frame: .zero) + let pinnedSize = currentPinView.size(isPinned) + let pinRect = CGRect(origin: CGPoint(x: frame.width - pinnedSize.width - 10, y: 10), size: pinnedSize) + currentPinView.frame = pinRect + currentPinView.layer?.opacity = self.mouseInside() && pinIsVisible ? 1 : 0 + self.pinView = currentPinView + addSubview(currentPinView) + currentPinView.set(handler: { [weak self] _ in + if let strongSelf = self, let dominant = strongSelf.currentPeer { + if !strongSelf.isPinned { + self?.arguments?.pinVideo(dominant) + } else { + self?.arguments?.unpinVideo() + } + } + }, for: .SingleClick) + + if currentPinView.layer?.opacity != 0, animated { + currentPinView.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } + currentPinView.update(self.isPinned, animated: animated) + + + } else { + if let view = pinView { + self.pinView = nil + if animated { + if view.layer?.opacity != 0 { + view.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } else { + view.removeFromSuperview() + } + } else { + view.removeFromSuperview() + } + } + } + + if pinIsVisible { + let currentBackView: BackView + if let current = self.backView { + currentBackView = current + } else { + currentBackView = BackView(frame: .zero) + currentBackView.frame = NSMakeRect(10, 10, currentBackView.frame.width, currentBackView.frame.height) + currentBackView.layer?.opacity = self.mouseInside() && pinIsVisible ? 1 : 0 + self.backView = currentBackView + addSubview(currentBackView) + currentBackView.set(handler: { [weak self] _ in + self?.arguments?.focusVideo(nil) + }, for: .SingleClick) + + if currentBackView.layer?.opacity != 0, animated { + currentBackView.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } + } else { + if let view = backView { + self.backView = nil + if animated { + if view.layer?.opacity != 0 { + view.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } else { + view.removeFromSuperview() + } + } else { + view.removeFromSuperview() + } + } + } + + self.pinView?.change(opacity: self.mouseInside() && pinIsVisible ? 1 : 0, animated: animated) + self.backView?.change(opacity: self.mouseInside() && pinIsVisible ? 1 : 0, animated: animated) + + + + let showSpeakingView = participant?.isSpeaking == true && (participant?.state?.muteState?.mutedByYou == nil || participant?.state?.muteState?.mutedByYou == false) + + transition.updateAlpha(view: speakingView, alpha: showSpeakingView ? 1 : 0) + + speakingView.layer?.borderColor = participant?.state?.muteState?.mutedByYou == true ? GroupCallTheme.customTheme.redColor.cgColor : GroupCallTheme.speakActiveColor.cgColor + + transition.updateAlpha(view: shadowView, alpha: controlsMode == .normal ? 1 : 0) + transition.updateAlpha(view: nameView, alpha: controlsMode == .normal ? 1 : 0) + transition.updateAlpha(view: statusView, alpha: controlsMode == .normal ? 1 : 0) + + + if participant != self.participant, let participant = participant { + let text: String + if participant.peer.id == participant.accountPeerId { + text = L10n.voiceChatStatusYou + } else { + text = participant.peer.displayTitle + } + let nameLayout = TextViewLayout(.initialize(string: text, color: NSColor.white.withAlphaComponent(1), font: .medium(.short)), maximumNumberOfLines: 1) + nameLayout.measure(width: frame.width - 20) + self.nameView.update(nameLayout) + + self.statusView.image = participant.state?.muteState == nil ? GroupCallTheme.videoBox_unmuted : GroupCallTheme.videoBox_muted + self.statusView.sizeToFit() + } + self.currentPeer = peer + if let peer = peer { + + var selfPresentation = peer.peerId == arguments?.getAccountPeerId() && peer.mode == .screencast + + if let source = arguments?.getSource(.screencast) { + if source.deviceIdKey().hasPrefix("desktop_capturer_window") { + selfPresentation = false + } + } + + if selfPresentation { + + let current: SelfPresentationPlaceholder + if let c = self.selfPresentationPlaceholder { + current = c + } else { + current = SelfPresentationPlaceholder(frame: self.bounds) + self.selfPresentationPlaceholder = current + addSubview(current) + + if animated { + current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } + + + current.update(stop: { [weak arguments] in + arguments?.cancelShareScreencast() + }) + } else { + if let view = self.selfPresentationPlaceholder { + self.selfPresentationPlaceholder = nil + if animated { + view.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } else { + view.removeFromSuperview() + } + } + } + + + let videoView = arguments?.takeVideo(peer.peerId, peer.mode, .main) as? GroupVideoView + let isPaused = participant?.isVideoPaused(peer.endpointId) == true && !selfPresentation + + if let videoView = videoView, self.currentVideoView != videoView || videoView.superview != self { + if let currentVideoView = self.currentVideoView { + currentVideoView.removeFromSuperview() + } + self.currentVideoView = videoView + self.addSubview(videoView, positioned: .below, relativeTo: self.shadowView) + } + + if let videoView = videoView { + + videoView.change(opacity: isPaused ? 0 : 1, animated: animated) + + let prevIsPaused = self.participant?.isVideoPaused(peer.endpointId) == true + if prevIsPaused != isPaused { + if isPaused { + self.pausedTextView?.removeFromSuperview() + self.pausedImageView?.removeFromSuperview() + self.pausedTextView = TextView() + self.pausedImageView = ImageView() + self.pausedImageView?.image = GroupCallTheme.video_paused + self.pausedImageView?.sizeToFit() + let layout = TextViewLayout(.initialize(string: peer.mode == .video ? L10n.voiceChatVideoPaused : L10n.voiceChatScreencastPaused, color: GroupCallTheme.customTheme.textColor, font: .medium(.text))) + layout.measure(width: .greatestFiniteMagnitude) + self.pausedTextView?.update(layout) + addSubview(self.pausedTextView!) + addSubview(self.pausedImageView!) + + + self.pausedImageView!.frame = focus(pausedImageView!.frame.size).offsetBy(dx: 0, dy: -5) + self.pausedTextView!.frame = self.pausedTextView!.centerFrameX(y: self.pausedImageView!.frame.maxY + 5) + if animated { + pausedTextView?.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + pausedImageView?.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } else { + if let pausedTextView = pausedTextView { + self.pausedTextView = nil + if animated { + pausedTextView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak pausedTextView] _ in + pausedTextView?.removeFromSuperview() + }) + } else { + pausedTextView.removeFromSuperview() + } + } + if let pausedImageView = pausedImageView { + self.pausedImageView = nil + if animated { + pausedImageView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak pausedImageView] _ in + pausedImageView?.removeFromSuperview() + }) + } else { + pausedImageView.removeFromSuperview() + } + } + } + } + } + + + self.currentVideoView?.gravity = resizeMode + + } else { + if let view = self.selfPresentationPlaceholder { + view.removeFromSuperview() + self.selfPresentationPlaceholder = nil + } + + if let currentVideoView = self.currentVideoView { + currentVideoView.removeFromSuperview() + self.currentVideoView = nil + } + } + self.participant = participant + + self.updateLayout(size: self.frame.size, transition: transition) + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + + + self.validLayout = size + if let currentVideoView = self.currentVideoView { + transition.updateFrame(view: currentVideoView, frame: size.bounds) + currentVideoView.updateLayout(size: size, transition: transition) + } + + + transition.updateFrame(view: shadowView, frame: CGRect(origin: NSMakePoint(0, size.height - 50), size: NSMakeSize(size.width, 50))) + + + self.nameView.resize(size.width - 40) + + + transition.updateFrame(view: statusView, frame: CGRect(origin: NSMakePoint(5, size.height - 5 - self.statusView.frame.height), size: self.statusView.frame.size)) + transition.updateFrame(view: self.nameView, frame: CGRect(origin: NSMakePoint(statusView.frame.maxX + 5, size.height - 5 - self.nameView.frame.height), size: self.nameView.frame.size)) + + transition.updateFrame(view: speakingView, frame: bounds) + + if let pausedImageView = pausedImageView { + transition.updateFrame(view: pausedImageView, frame: focus(pausedImageView.frame.size).offsetBy(dx: 0, dy: -10)) + if let pausedTextView = pausedTextView { + transition.updateFrame(view: pausedTextView, frame: pausedTextView.centerFrameX(y: pausedImageView.frame.maxY + 5)) + } + } + + + if let pinView = pinView { + let pinnedSize = pinView.size(self.isPinned) + let pinRect = CGRect(origin: CGPoint(x: frame.width - pinnedSize.width - 10, y: 10), size: pinnedSize) + transition.updateFrame(view: pinView, frame: pinRect) + pinView.updateLayout(size: pinRect.size, transition: transition) + } + if let backView = self.backView { + let backRect = NSMakeRect(10, 10, backView.frame.width, backView.frame.height) + transition.updateFrame(view: backView, frame: backRect) + backView.updateLayout(size: backRect.size, transition: transition) + } + + if let view = self.selfPresentationPlaceholder { + transition.updateFrame(view: view, frame: size.bounds) + view.updateLayout(size: size, transition: transition) + } + } + + deinit { + audioLevelDisposable.dispose() + } + + + override func layout() { + super.layout() + updateLayout(size: frame.size, transition: .immediate) + } +} diff --git a/Telegram-Mac/GroupCallNavigationHeaderView.swift b/Telegram-Mac/GroupCallNavigationHeaderView.swift new file mode 100644 index 0000000000..4a9948f524 --- /dev/null +++ b/Telegram-Mac/GroupCallNavigationHeaderView.swift @@ -0,0 +1,192 @@ +// +// GroupCallNavigationHeaderView.swift +// Telegram +// +// Created by Mikhail Filimonov on 07.12.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit +import Postbox + +import TelegramCore + + + + + +class GroupCallNavigationHeaderView: CallHeaderBasicView { + + + + + private let audioLevelDisposable = MetaDisposable() + + var context: GroupCallContext? { + get { + return self.header?.contextObject as? GroupCallContext + } + } + + override func toggleMute() { + self.context?.call.toggleIsMuted() + } + + override func showInfoWindow() { + self.context?.present() + } + + override func hangUp() { + self.context?.leave() + } + + override var blueColor: NSColor { + return NSColor(rgb: 0x0078ff) + } + override var grayColor: NSColor { + return NSColor(rgb: 0x33c659) + } + + override func hide(_ animated: Bool) { + super.hide(true) + audioLevelDisposable.set(nil) + } + + override func update(with contextObject: Any) { + super.update(with: contextObject) + + + let context = contextObject as! GroupCallContext + let peerId = context.call.peerId + + + let data = context.call.summaryState + |> filter { $0 != nil } + |> map { $0! } + |> map { summary -> GroupCallPanelData in + return GroupCallPanelData( + peerId: peerId, + info: summary.info, + topParticipants: summary.topParticipants, + participantCount: summary.participantCount, + activeSpeakers: summary.activeSpeakers, + groupCall: nil + ) + } + + let account = context.call.account + + let signal = Signal.single(context.call.peer) |> then(context.call.account.postbox.loadedPeerWithId(context.call.peerId) |> map(Optional.init) |> deliverOnMainQueue) + + let accountPeer: Signal = context.call.sharedContext.activeAccounts |> mapToSignal { accounts in + if accounts.accounts.count == 1 { + return .single(nil) + } else { + return account.postbox.loadedPeerWithId(account.peerId) |> map(Optional.init) + } + } + + + + disposable.set(combineLatest(queue: .mainQueue(), context.call.state, context.call.isMuted, data, signal, accountPeer, appearanceSignal, context.call.members, context.call.summaryState).start(next: { [weak self] state, isMuted, data, peer, accountPeer, _, members, summary in + + let title: String? + if let custom = state.title, !custom.isEmpty { + title = custom + } else { + title = peer?.displayTitle + } + + if let title = title { + self?.setInfo(title) + } + self?.updateState(state, isMuted: isMuted, data: data, members: members, accountPeer: accountPeer, animated: false) + self?.needsLayout = true + self?.ready.set(.single(true)) + })) + + hideDisposable.set((context.call.canBeRemoved |> deliverOnMainQueue).start(next: { [weak self] value in + if value { + self?.hide(true) + } + })) + let isVisible = context.window.takeOcclusionState |> map { $0.contains(.visible) } + self.audioLevelDisposable.set((combineLatest(isVisible, context.call.myAudioLevel, .single([]) |> then(context.call.audioLevels), context.call.isMuted, context.call.state) + |> deliverOnMainQueue).start(next: { [weak self] isVisible, myAudioLevel, audioLevels, isMuted, state in + guard let strongSelf = self else { + return + } + var effectiveLevel: Float = 0.0 + if isVisible { + switch state.networkState { + case .connected: + effectiveLevel = audioLevels.reduce(0, { current, value in + return current + value.2 + }) + if !audioLevels.isEmpty { + effectiveLevel = effectiveLevel / Float(audioLevels.count) + } + case .connecting: + effectiveLevel = 0 + } + } + strongSelf.backgroundView.audioLevel = effectiveLevel + })) + } + + deinit { + audioLevelDisposable.dispose() + } + + + private func updateState(_ state: PresentationGroupCallState, isMuted: Bool, data: GroupCallPanelData, members: PresentationGroupCallMembers?, accountPeer: Peer?, animated: Bool) { + let isConnected: Bool + switch state.networkState { + case .connecting: + if let scheduleTimestamp = state.scheduleTimestamp { + self.status = .startsIn(Int(scheduleTimestamp)) + isConnected = true + } else { + self.status = .text(L10n.voiceChatStatusConnecting, nil) + isConnected = false + } + case .connected: + + if let first = data.topParticipants.first(where: { members?.speakingParticipants.contains($0.peer.id) ?? false }) { + self.status = .text(first.peer.compactDisplayTitle.prefixWithDots(12), nil) + } else { + self.status = .text(L10n.voiceChatStatusMembersCountable(data.participantCount), nil) + } + isConnected = true + } + if state.scheduleTimestamp != nil { + self.backgroundView.speaking = (true, true, false) + } else { + self.backgroundView.speaking = (isConnected && !isMuted, isConnected, state.muteState?.canUnmute ?? true) + } + + setMicroIcon(isMuted ? theme.icons.callInlineMuted : theme.icons.callInlineUnmuted) + needsLayout = true + + } + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func getEndText() -> String { + return L10n.voiceChatTitleEnd + } + + override init(_ header: NavigationHeader) { + super.init(header) + } + +} + diff --git a/Telegram-Mac/GroupCallParticipantRowItem.swift b/Telegram-Mac/GroupCallParticipantRowItem.swift new file mode 100644 index 0000000000..dd5323e95d --- /dev/null +++ b/Telegram-Mac/GroupCallParticipantRowItem.swift @@ -0,0 +1,1131 @@ +// +// GroupCallParticipantRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 23/11/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit + +import Postbox +import TelegramCore + +private let fakeIcon = generateFakeIconReversed(foregroundColor: GroupCallTheme.customTheme.redColor, backgroundColor: GroupCallTheme.customTheme.backgroundColor) +private let scamIcon = generateScamIconReversed(foregroundColor: GroupCallTheme.customTheme.redColor, backgroundColor: GroupCallTheme.customTheme.backgroundColor) +private let verifyIcon = NSImage(named: "Icon_VerifyDialog")!.precomposed(GroupCallTheme.customTheme.accentColor) + +final class GroupCallParticipantRowItem : GeneralRowItem { + let data: PeerGroupCallData + private let _contextMenu: ()->Signal<[ContextMenuItem], NoError> + + fileprivate private(set) var titleLayout: TextViewLayout! + fileprivate let statusLayout: TextViewLayout + fileprivate let account: Account + fileprivate let isLastItem: Bool + fileprivate let isInvited: Bool + fileprivate let drawLine: Bool + fileprivate let invite:(PeerId)->Void + fileprivate let canManageCall:Bool + fileprivate let takeVideo:(PeerId, VideoSourceMacMode?, GroupCallUIState.ActiveVideo.Mode)->NSView? + fileprivate let volume: TextViewLayout? + fileprivate let audioLevel:(PeerId)->Signal? + fileprivate private(set) var buttonImage: (CGImage, CGImage?)? = nil + fileprivate let baseEndpoint: String? + fileprivate let focusVideo:(String?)->Void + init(_ initialSize: NSSize, stableId: AnyHashable, account: Account, data: PeerGroupCallData, baseEndpoint: String?, canManageCall: Bool, isInvited: Bool, isLastItem: Bool, drawLine: Bool, viewType: GeneralViewType, action: @escaping()->Void, invite:@escaping(PeerId)->Void, contextMenu:@escaping()->Signal<[ContextMenuItem], NoError>, takeVideo:@escaping(PeerId, VideoSourceMacMode?, GroupCallUIState.ActiveVideo.Mode)->NSView?, audioLevel:@escaping(PeerId)->Signal?, focusVideo: @escaping(String?)->Void) { + self.data = data + self.audioLevel = audioLevel + self.account = account + self.canManageCall = canManageCall + self.invite = invite + self.focusVideo = focusVideo + self._contextMenu = contextMenu + self.isInvited = isInvited + self.drawLine = drawLine + self.takeVideo = takeVideo + self.baseEndpoint = baseEndpoint + self.isLastItem = isLastItem + let (string, color) = data.status + + if let volume = data.unsyncVolume ?? data.state?.volume, volume != 10000 { + if let muteState = data.state?.muteState, !muteState.canUnmute || muteState.mutedByYou { + self.volume = nil + } else { + if data.isSpeaking { + var volumeColor: NSColor + if volume == 0 { + volumeColor = GroupCallTheme.grayStatusColor + } else { + volumeColor = color + } + self.volume = TextViewLayout(.initialize(string: "\(Int(Float(volume) / 10000 * 100))%", color: volumeColor, font: .normal(.short))) + } else { + self.volume = nil + } + } + } else { + self.volume = nil + } + + + self.statusLayout = TextViewLayout(.initialize(string: string, color: color, font: .normal(.short)), maximumNumberOfLines: 1) + super.init(initialSize, height: 0, stableId: stableId, type: .none, viewType: viewType, action: action, inset: .init(), enabled: true) + + + if isActivePeer { + if data.isSpeaking { + self.buttonImage = (GroupCallTheme.small_speaking, GroupCallTheme.small_speaking_active) + } else { + if let muteState = data.state?.muteState { + if !muteState.canUnmute && data.isRaisedHand { + self.buttonImage = (GroupCallTheme.small_raised_hand, GroupCallTheme.small_raised_hand_active) + } else if muteState.canUnmute && !muteState.mutedByYou { + buttonImage = (GroupCallTheme.small_muted, GroupCallTheme.small_muted_active) + } else { + buttonImage = (GroupCallTheme.small_muted_locked, GroupCallTheme.small_muted_locked_active) + } + } else if data.state == nil { + buttonImage = (GroupCallTheme.small_muted, GroupCallTheme.small_muted_active) + } else { + buttonImage = (GroupCallTheme.small_unmuted, GroupCallTheme.small_unmuted_active) + } + } + } else { + if isInvited { + buttonImage = (GroupCallTheme.invitedIcon, nil) + } else { + buttonImage = (GroupCallTheme.inviteIcon, nil) + } + } + + } + + override var viewType: GeneralViewType { + return isVertical ? .singleItem : super.viewType + } + + override var height: CGFloat { + return isVertical ? 120 : 48 + } + + override var inset: NSEdgeInsets { + let insets: NSEdgeInsets + if isVertical { + insets = NSEdgeInsetsMake(0, 0, 5, 0) + } else { + insets = NSEdgeInsetsMake(0, 0, 0, 0) + } + return insets + } + + override var width: CGFloat { + return super.width + } + + var isVertical: Bool { + return data.isVertical + } + + + var itemInset: NSEdgeInsets { + return NSEdgeInsetsMake(0, 12, 0, 12) + } + + var isActivePeer: Bool { + return data.state != nil || data.peer.id == data.accountPeerId + } + + var peer: Peer { + return data.peer + } + + func takeCurrentVideo() -> NSView? { + var mode: VideoSourceMacMode? = nil + if let baseEndpoint = self.baseEndpoint { + if self.data.videoEndpoint == baseEndpoint { + mode = .video + } + if self.data.presentationEndpoint == baseEndpoint { + mode = .screencast + } + } + let videoView = isVertical ? self.takeVideo(peer.id, mode, .list) : nil + + return videoView + } + + var supplementIcon: (CGImage, NSSize)? { + + let isScam: Bool = peer.isScam + let isFake: Bool = peer.isFake + let verified: Bool = peer.isVerified + + + + if isScam { + return (scamIcon, .zero) + } else if isFake { + return (fakeIcon, .zero) + } else if verified { + return (verifyIcon, NSMakeSize(-4, -4)) + } else { + return nil + } + } + + override var hasBorder: Bool { + return false + } + override var instantlyResize: Bool { + return true + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + + + self.volume?.measure(width: .greatestFiniteMagnitude) + var inset: CGFloat = 0 + if let volume = self.volume { + inset = volume.layoutSize.width + 25 + } else { + for image in statusImage { + inset += image.backingSize.width + 3 + } + } + + + + if isVertical { + self.titleLayout = TextViewLayout(.initialize(string: data.peer.compactDisplayTitle, color: NSColor.white.withAlphaComponent(0.8), font: .normal(.text)), maximumNumberOfLines: 1) + } else { + self.titleLayout = TextViewLayout(.initialize(string: data.peer.displayTitle, color: (data.state != nil ? .white : GroupCallTheme.grayStatusColor), font: .medium(.text)), maximumNumberOfLines: 1) + } + + if isVertical { + titleLayout.measure(width: GroupCallTheme.smallTableWidth - 16 - 10) + } else { + let width = (data.isFullscreen && data.videoMode) ? GroupCallTheme.tileTableWidth - 20 : width - 20 + + titleLayout.measure(width: width - itemInset.left - itemInset.left - itemInset.right - (data.videoMode ? 0 : 28) - itemInset.right) + statusLayout.measure(width: width - itemInset.left - itemInset.left - itemInset.right - (data.videoMode ? 0 : 28) - itemInset.right - inset) + } + + return true + } + + override func menuItems(in location: NSPoint) -> Signal<[ContextMenuItem], NoError> { + return _contextMenu() + } + + override func viewClass() -> AnyClass { + return GroupCallParticipantRowView.self + } + + override var identifier: String { + return isVertical ? "vertical_group_call_item" : super.identifier + } + + var statusImage: [CGImage] { + let hasVideo = data.hasVideo + + var images:[CGImage] = [] + + if hasVideo || volume != nil || data.videoMode, let state = data.state { + + if data.videoMode { + if let muteState = state.muteState { + if muteState.mutedByYou { + images.append(GroupCallTheme.video_status_muted_red) + } else { + images.append(GroupCallTheme.video_status_muted_gray) + } + } else { + if data.isSpeaking { + images.append(GroupCallTheme.video_status_unmuted_green) + } else if data.wantsToSpeak { + images.append(GroupCallTheme.video_status_unmuted_accent) + } else { + images.append(GroupCallTheme.video_status_unmuted_gray) + } + } + } + + if hasVideo { + if let endpoint = data.videoEndpoint { + if baseEndpoint == nil || baseEndpoint == endpoint { + if let muteState = state.muteState, muteState.mutedByYou { + images.append(GroupCallTheme.status_video_red) + } else { + if data.isSpeaking { + images.append(GroupCallTheme.status_video_green) + } else if data.wantsToSpeak { + images.append(GroupCallTheme.status_video_accent) + } else { + images.append(GroupCallTheme.status_video_gray) + } + } + } + } + if let endpoint = data.presentationEndpoint { + if baseEndpoint == nil || baseEndpoint == endpoint { + if let muteState = state.muteState, muteState.mutedByYou { + images.append(GroupCallTheme.status_screencast_red) + } else { + if data.isSpeaking { + images.append(GroupCallTheme.status_screencast_green) + } else if data.wantsToSpeak { + images.append(GroupCallTheme.status_screencast_accent) + } else { + images.append(GroupCallTheme.status_screencast_gray) + } + } + } + } + } else { + if let muteState = state.muteState, muteState.mutedByYou { + images.append(GroupCallTheme.status_muted_red) + } else if !data.videoMode { + if data.isSpeaking { + images.append(GroupCallTheme.status_unmuted_green) + } else if data.wantsToSpeak { + images.append(GroupCallTheme.status_unmuted_accent) + } else { + images.append(GroupCallTheme.status_unmuted_gray) + } + } + } + } + return images + } + + var videoBoxImage: [CGImage] { + if isInvited { + return [GroupCallTheme.videoBox_muted_locked] + } + var images:[CGImage] = [] + + if let _ = data.state?.muteState { + images.append(GroupCallTheme.videoBox_muted) + } else if data.state == nil { + images.append(GroupCallTheme.videoBox_muted) + } else { + images.append(GroupCallTheme.videoBox_unmuted) + } + + if let endpoint = data.videoEndpoint { + if baseEndpoint == nil || baseEndpoint == endpoint { + images.append(GroupCallTheme.videoBox_video) + } + } + if let endpoint = data.presentationEndpoint { + if baseEndpoint == nil || baseEndpoint == endpoint { + images.append(GroupCallTheme.videoBox_screencast) + } + } + + return images + } + + var actionInteractionEnabled: Bool { + if data.accountPeerId == data.peer.id { + return false + } + if isActivePeer { + return canManageCall + } else { + if isInvited { + return false + } else { + return true + } + } + } + + var activityColor: NSColor { + if let muteState = data.state?.muteState, muteState.mutedByYou { + return GroupCallTheme.speakLockedColor + } else { + return data.isSpeaking ? GroupCallTheme.speakActiveColor : GroupCallTheme.speakInactiveColor + } + } + + deinit { + + } +} + +protocol GroupCallParticipantRowProtocolView : NSView { + func getPhotoView() -> NSView +} + + + + +final class VerticalContainerView : GeneralContainableRowView, GroupCallParticipantRowProtocolView { + private let photoView: GroupCallAvatarView = GroupCallAvatarView(frame: NSMakeRect(0, 0, 68, 68), photoSize: NSMakeSize(48, 48)) + + private final class VideoContainer : View { + private let shadowView = ShadowView() + var view: NSView? { + didSet { + if let view = view { + addSubview(view, positioned: .below, relativeTo: shadowView) + } + needsLayout = true + } + } + + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(shadowView) + shadowView.direction = .vertical(true) + shadowView.shadowBackground = NSColor.black.withAlphaComponent(0.3) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + view?.frame = bounds + shadowView.frame = NSMakeRect(0, frame.height - 30, frame.width, 30) + } + } + + func getPhotoView() -> NSView { + return self.photoView + } + + private var videoContainer: VideoContainer? + private let nameContainer = View() + private let titleView = TextView() + private let statusView = ImageView() + private var pinnedFrameView: View? + private let imagesView: View = View() + private let speakingView: View = View() + + private let audioLevelDisposable = MetaDisposable() + private var scaleAnimator: DisplayLinkAnimator? + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(photoView) + nameContainer.addSubview(titleView) + nameContainer.addSubview(imagesView) + addSubview(statusView) + + addSubview(nameContainer) + + addSubview(speakingView) + titleView.userInteractionEnabled = false + titleView.isSelectable = false + photoView.userInteractionEnabled = false + + speakingView.layer?.cornerRadius = 10 + speakingView.layer?.borderWidth = 2 + speakingView.layer?.borderColor = GroupCallTheme.speakActiveColor.cgColor + + containerView.set(handler: { [weak self] _ in + if let item = self?.item as? GroupCallParticipantRowItem { + item.focusVideo(item.baseEndpoint) + } + }, for: .Click) + + containerView.set(handler: { [weak self] _ in + self?.updateColors() + }, for: .Hover) + + containerView.set(handler: { [weak self] _ in + self?.updateColors() + }, for: .Normal) + + containerView.set(handler: { [weak self] _ in + self?.updateColors() + }, for: .Highlight) + + containerView.scaleOnClick = true + } + + + override var backdorColor: NSColor { + let color: NSColor + if containerView.controlState == .Highlight || contextMenu != nil { + color = GroupCallTheme.membersColor.lighter() + } else { + color = GroupCallTheme.membersColor + } + return color + } + override func updateColors() { + super.updateColors() + } + + override func showContextMenu(_ event: NSEvent) { + super.showContextMenu(event) + } + + override var maxBlockWidth: CGFloat { + return GroupCallTheme.tileTableWidth + } + + override func layout() { + super.layout() + + guard let item = item as? GroupCallParticipantRowItem else { + return + } + + let blockWidth = min(maxBlockWidth, frame.width - item.inset.left - item.inset.right) + + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (maxWidth - blockWidth) / 2), 0, blockWidth, maxHeight) + self.containerView.setCorners(item.viewType.corners) + + + photoView.center() + videoContainer?.frame = containerView.bounds + + nameContainer.frame = NSMakeRect(0, 0, containerView.frame.width, max(imagesView.frame.height, titleView.frame.height)) + nameContainer.centerX(y: containerView.frame.height - nameContainer.frame.height - 8) + + imagesView.setFrameOrigin(NSMakePoint(8, 0)) + titleView.setFrameOrigin(NSMakePoint(imagesView.frame.maxX + 8, 0)) + + + pinnedFrameView?.frame = containerView.bounds + + speakingView.frame = containerView.bounds + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? GroupCallParticipantRowItem else { + return + } + photoView.update(item.audioLevel, data: item.data, activityColor: item.activityColor, account: item.account, animated: animated) + photoView._change(opacity: item.isActivePeer ? 1.0 : 0.5, animated: animated) + + + let showSpeakingView = item.data.isSpeaking == true && (item.data.state?.muteState?.mutedByYou == nil || item.data.state?.muteState?.mutedByYou == false) + + speakingView.change(opacity: showSpeakingView ? 1 : 0, animated: animated) + + speakingView.layer?.borderColor = item.data.state?.muteState?.mutedByYou == true ? GroupCallTheme.customTheme.redColor.cgColor : GroupCallTheme.speakActiveColor.cgColor + + +// if item.data.pinnedMode != nil { +// let current: View +// if let pinnedView = self.pinnedFrameView { +// current = pinnedView +// } else { +// current = View() +// self.pinnedFrameView = current +// addSubview(current) +// current.layer?.cornerRadius = 10 +// current.layer?.borderWidth = 2 +// if animated { +// current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) +// } +// } +// current.layer?.borderColor = item.activityColor.withAlphaComponent(0.7).cgColor +// } else { +// if let pinnedView = self.pinnedFrameView { +// self.pinnedFrameView = nil +// if animated { +// pinnedView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak pinnedView] _ in +// pinnedView?.removeFromSuperview() +// }) +// } else { +// pinnedView.removeFromSuperview() +// } +// } +// } + + + let videoBoxImages = item.videoBoxImage + + while imagesView.subviews.count > videoBoxImages.count { + imagesView.subviews.removeLast() + } + while imagesView.subviews.count < videoBoxImages.count { + imagesView.addSubview(ImageView()) + } + for (i, image) in videoBoxImages.enumerated() { + let view = (imagesView.subviews[i] as? ImageView) + view?.image = image + view?.sizeToFit() + } + imagesView.setFrameSize(imagesView.subviewsWidthSize) + + var x: CGFloat = 0 + for view in imagesView.subviews { + view.centerY(x: x) + x += view.frame.width + 2 + } + + titleView.update(item.titleLayout) + + let videoView = item.takeCurrentVideo() + + + if let videoView = videoView { + + var isPresented: Bool = false + + let videoContainer: VideoContainer + if let current = self.videoContainer { + videoContainer = current + } else { + videoContainer = VideoContainer(frame: containerView.bounds) + videoContainer.isEventLess = true + self.videoContainer = videoContainer + addSubview(videoContainer, positioned: .above, relativeTo: photoView) + isPresented = true + } + videoContainer.view = videoView + + if animated && isPresented { + videoContainer.layer?.animateAlpha(from: 0, to: 1, duration: 0.3) + let from = Float(videoContainer.frame.height / 2) + videoContainer.layer?.animate(from: NSNumber(value: from), to: NSNumber(value: 0), keyPath: "cornerRadius", timingFunction: .easeInEaseOut, duration: 0.2, forKey: "cornerRadius") + + videoContainer.layer?.animateScaleCenter(from: photoView.frame.height / videoContainer.frame.width, to: 1, duration: 0.2) + + videoContainer.layer?.animatePosition(from: NSMakePoint(0, -photoView.frame.minY - 10), to: videoContainer.frame.origin, duration: 0.2) + + } + } else { + if let first = self.videoContainer { + self.videoContainer = nil + if animated { + first.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak first, weak self] _ in + if first?.superview == self?.videoContainer { + first?.removeFromSuperview() + } + }) + + let to = Float(first.frame.height / 2) + first.layer?.animate(from: NSNumber(value: 0), to: NSNumber(value: to), keyPath: "cornerRadius", timingFunction: .easeInEaseOut, duration: 0.2, removeOnCompletion: false, forKey: "cornerRadius") + + first.layer?.animateScaleCenter(from: 1, to: photoView.frame.height / first.frame.width, duration: 0.3, removeOnCompletion: false) + + first.layer?.animatePosition(from: first.frame.origin, to: NSMakePoint(0, -photoView.frame.minY - 10), duration: 0.2, removeOnCompletion: false) + + } else { + first.removeFromSuperview() + } + } + } + needsLayout = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + + } +} + +private final class HorizontalContainerView : GeneralContainableRowView, GroupCallParticipantRowProtocolView { + + private final class VideoContainer : View { + weak var view: NSView? { + didSet { + if let view = view { + addSubview(view) + } + needsLayout = true + } + } + + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + view?.frame = bounds + } + + } + + + private let photoView: GroupCallAvatarView = GroupCallAvatarView(frame: NSMakeRect(0, 0, 55, 55), photoSize: NSMakeSize(35, 35)) + private let titleView: TextView = TextView() + private var statusView: TextView? + private let button = ImageButton() + private let separator: View = View() + private let videoContainer: VideoContainer = VideoContainer(frame: .zero) + private var volumeView: TextView? + private var statusImageContainer: View = View() + private var supplementImageView: ImageView? + private let audioLevelDisposable = MetaDisposable() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(photoView) + addSubview(titleView) + addSubview(separator) + addSubview(button) + addSubview(statusImageContainer) + titleView.userInteractionEnabled = false + titleView.isSelectable = false + + photoView.userInteractionEnabled = false + + addSubview(videoContainer) + videoContainer.frame = .init(origin: .zero, size: photoView.photoSize) + videoContainer.layer?.cornerRadius = photoView.photoSize.height / 2 + + button.animates = true + + button.autohighlight = true + button.set(handler: { [weak self] _ in + guard let item = self?.item as? GroupCallParticipantRowItem else { + return + } + if item.data.state == nil { + item.invite(item.peer.id) + } + }, for: .SingleClick) + + button.set(handler: { [weak self] _ in + guard let item = self?.item as? GroupCallParticipantRowItem else { + return + } + if item.data.state != nil { + _ = item.menuItems(in: .zero).start(next: { [weak self] items in + if let event = NSApp.currentEvent, let button = self?.button { + let menu = NSMenu() + menu.appearance = darkPalette.appearance + for item in items { + menu.addItem(item) + } + NSMenu.popUpContextMenu(menu, with: event, for: button) + } + }) + } + }, for: .Down) + + containerView.set(handler: { [weak self] _ in + if let event = NSApp.currentEvent { + self?.showContextMenu(event) + } + }, for: .Click) + + containerView.set(handler: { [weak self] _ in + self?.updateColors() + }, for: .Hover) + + containerView.set(handler: { [weak self] _ in + self?.updateColors() + }, for: .Normal) + + containerView.set(handler: { [weak self] _ in + self?.updateColors() + }, for: .Highlight) + } + + override var backdorColor: NSColor { + let color: NSColor + if containerView.controlState == .Highlight || contextMenu != nil { + color = GroupCallTheme.membersColor.lighter() + } else { + color = GroupCallTheme.membersColor + } + return color + } + + + func getPhotoView() -> NSView { + return self.photoView + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + guard let item = item as? GroupCallParticipantRowItem else { + return + } + + let frame = size.bounds + + if let statusView = statusView { + transition.updateFrame(view: statusView, frame: CGRect(origin: statusViewPoint, size: statusView.frame.size)) + } + if let volumeView = volumeView { + transition.updateFrame(view: volumeView, frame: CGRect(origin: volumeViewPoint, size: volumeView.frame.size)) + } + transition.updateFrame(view: statusImageContainer, frame: CGRect(origin: statusImageViewViewPoint, size: statusImageContainer.frame.size)) + + transition.updateFrame(view: self.photoView, frame: self.photoView.centerFrameY(x: item.itemInset.left - (self.photoView.frame.width - photoView.photoSize.width) / 2)) + + transition.updateFrame(view: titleView, frame: CGRect(origin: NSMakePoint(item.itemInset.left + photoView.photoSize.width + item.itemInset.left, 6), size: titleView.frame.size)) + + if let imageView = self.supplementImageView { + transition.updateFrame(view: imageView, frame: CGRect.init(origin: NSMakePoint(titleView.frame.maxX + 3 + (item.supplementIcon?.1.width ?? 0), titleView.frame.minY + (item.supplementIcon?.1.height ?? 0)), size: imageView.frame.size)) + } + if item.drawLine { + transition.updateFrame(view: separator, frame: NSMakeRect(titleView.frame.minX, frame.height - .borderSize, frame.width - titleView.frame.minX, .borderSize)) + } else { + transition.updateFrame(view: separator, frame: .zero) + } + + transition.updateFrame(view: button, frame: button.centerFrameY(x: frame.width - 12 - button.frame.width)) + + transition.updateFrame(view: videoContainer, frame: videoContainer.centerFrameY(x: item.itemInset.left, addition: -1)) + + + } + + override func layout() { + super.layout() + updateLayout(size: frame.size, transition: .immediate) + } + + override func updateColors() { + super.updateColors() + self.titleView.backgroundColor = backdorColor + self.statusView?.backgroundColor = backdorColor + self.separator.backgroundColor = GroupCallTheme.memberSeparatorColor + } + + + override func set(item: TableRowItem, animated: Bool = false) { + let previousItem = self.item as? GroupCallParticipantRowItem + super.set(item: item, animated: animated) + + guard let item = item as? GroupCallParticipantRowItem else { + return + } + + let videoView = item.takeCurrentVideo() + + if let videoView = videoView { + let previous = self.videoContainer.view + + videoView.frame = self.videoContainer.bounds + self.videoContainer.view = videoView + + if animated && previous == nil { + videoView.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } else { + if let first = self.videoContainer.view { + self.videoContainer.view = nil + if animated { + first.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak first, weak self] _ in + if first?.superview == self?.videoContainer { + first?.removeFromSuperview() + } + }) + } else { + first.removeFromSuperview() + } + } + } + + if let icon = item.supplementIcon { + let current: ImageView + if let value = self.supplementImageView { + current = value + } else { + current = ImageView() + self.supplementImageView = current + addSubview(current) + } + current.image = icon.0 + current.sizeToFit() + } else { + self.supplementImageView?.removeFromSuperview() + self.supplementImageView = nil + } + + + if previousItem?.buttonImage?.0 != item.buttonImage?.0 { + if let image = item.buttonImage { + button.set(image: image.0, for: .Normal) + if let highlight = image.1 { + button.set(image: highlight, for: .Highlight) + } else { + button.removeImage(for: .Highlight) + } + } + button.sizeToFit(.zero, NSMakeSize(28, 28), thatFit: true) + } + button.userInteractionEnabled = item.actionInteractionEnabled + button.isHidden = item.data.videoMode + photoView.update(item.audioLevel, data: item.data, activityColor: item.activityColor, account: item.account, animated: animated) + + titleView.update(item.titleLayout) + photoView._change(opacity: item.isActivePeer ? 1.0 : 0.5, animated: animated) + + + while statusImageContainer.subviews.count > item.statusImage.count { + statusImageContainer.subviews.last?.removeFromSuperview() + } + while statusImageContainer.subviews.count < item.statusImage.count { + let statusImageView = ImageView() + statusImageContainer.addSubview(statusImageView) + } + + for (i, statusImage) in item.statusImage.enumerated() { + let statusImageView = statusImageContainer.subviews[i] as! ImageView + if statusImageView.image != statusImage { + statusImageView.image = statusImage + statusImageView.sizeToFit() + } + } + + statusImageContainer.setFrameSize(statusImageContainer.subviewsWidthSize + NSMakeSize((2 * CGFloat(statusImageContainer.subviews.count) - 1), 0)) + var x: CGFloat = 0 + for subview in statusImageContainer.subviews { + subview.setFrameOrigin(NSMakePoint(x, 0)) + x += subview.frame.width + 2 + } + + if statusView?.layout?.attributedString.string != item.statusLayout.attributedString.string { + if let statusView = statusView { + if animated { + statusView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak statusView] _ in + statusView?.removeFromSuperview() + }) + statusView.layer?.animatePosition(from: statusView.frame.origin, to: NSMakePoint(statusView.frame.minX, statusView.frame.minY + 10)) + } else { + statusView.removeFromSuperview() + } + } + + let animated = statusView?.layout != nil + + let statusView = TextView() + let hadOld = self.statusView != nil + self.statusView = statusView + statusView.userInteractionEnabled = false + statusView.isSelectable = false + statusView.update(item.statusLayout) + addSubview(statusView) + statusView.setFrameOrigin(statusViewPoint) + + if animated && hadOld { + statusView.layer?.animateAlpha(from: 0, to: 1, duration: 0.3) + statusView.layer?.animatePosition(from: NSMakePoint(statusViewPoint.x, statusViewPoint.y - 10), to: statusViewPoint) + } + } else { + statusView?.change(pos: statusViewPoint, animated: animated) + } + + statusView?.update(item.statusLayout) + + + if let volume = item.volume { + var isPresented: Bool = false + if volumeView == nil { + self.volumeView = TextView() + self.volumeView?.userInteractionEnabled = false + self.volumeView?.isSelectable = false + addSubview(volumeView!) + isPresented = true + } + guard let volumeView = volumeView else { + return + } + volumeView.update(volume) + + if isPresented { + volumeView.setFrameOrigin(volumeViewPoint) + } + if isPresented && animated { + volumeView.layer?.animateAlpha(from: 0, to: 1, duration: 0.3) + volumeView.layer?.animatePosition(from: NSMakePoint(volumeView.frame.minX - volumeView.frame.width, volumeView.frame.minY), to: volumeView.frame.origin) + + if let statusView = statusView { + statusView.change(pos: statusViewPoint, animated: animated) + } + } + } else { + if let volumeView = volumeView { + self.volumeView = nil + if animated { + volumeView.layer?.animateAlpha(from: 1, to: 0, duration: 0.3, removeOnCompletion: false, completion: { [weak volumeView] _ in + volumeView?.removeFromSuperview() + }) + volumeView.layer?.animatePosition(from: volumeView.frame.origin, to: NSMakePoint(volumeView.frame.minX - volumeView.frame.width, volumeView.frame.minY)) + } else { + volumeView.removeFromSuperview() + } + if let statusView = statusView { + statusView.change(pos: statusViewPoint, animated: animated) + } + } + } + needsLayout = true + } + + + var statusViewPoint: NSPoint { + guard let item = item as? GroupCallParticipantRowItem else { + return .zero + } + var point: NSPoint = .zero + + if let statusView = statusView { + point = NSMakePoint(item.itemInset.left + photoView.photoSize.width + item.itemInset.left, frame.height - statusView.frame.height - 6) + } + if let volume = item.volume { + point.x += volume.layoutSize.width + 3 + } + if !statusImageContainer.subviews.isEmpty { + point.x += statusImageContainer.frame.width + 3 + } + + return point + } + var volumeViewPoint: NSPoint { + guard let item = item as? GroupCallParticipantRowItem else { + return .zero + } + var point: NSPoint = .zero + + if let volumeView = volumeView { + point = NSMakePoint(item.itemInset.left + photoView.photoSize.width + item.itemInset.left, frame.height - volumeView.frame.height - 6) + } + if !statusImageContainer.subviews.isEmpty { + point.x += statusImageContainer.frame.width + 3 + } + return point + } + + var statusImageViewViewPoint: NSPoint { + guard let item = item as? GroupCallParticipantRowItem else { + return .zero + } + var point: NSPoint = .zero + + point = NSMakePoint(item.itemInset.left + photoView.photoSize.width + item.itemInset.left, containerView.frame.height - statusImageContainer.frame.height - 5) + return point + } + + deinit { + audioLevelDisposable.dispose() + } + + + override func mouseUp(with event: NSEvent) { + super.mouseUp(with: event) + showContextMenu(event) + } + + override var rowAppearance: NSAppearance? { + return darkPalette.appearance + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + + +private final class GroupCallParticipantRowView : GeneralContainableRowView, GroupCallParticipantRowProtocolView { + + private var container: (GroupCallParticipantRowProtocolView & GeneralContainableRowView)? + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + + } + + override var backdorColor: NSColor { + let color: NSColor + if containerView.controlState == .Highlight || contextMenu != nil { + color = GroupCallTheme.membersColor.lighter() + } else { + color = GroupCallTheme.membersColor + } + return color + } + + + func getPhotoView() -> NSView { + return self.container?.getPhotoView() ?? self + } + + override func layout() { + super.layout() + if let container = container as? HorizontalContainerView { + container.frame = containerView.bounds + } + } + + override func updateColors() { + super.updateColors() + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? GroupCallParticipantRowItem else { + return + } + + let current: (GeneralContainableRowView & GroupCallParticipantRowProtocolView) + + var previous: NSView? + if item.isVertical { + if self.container is VerticalContainerView { + current = self.container! + } else { + current = VerticalContainerView(frame: NSMakeRect(0, 0, GroupCallTheme.tileTableWidth, item.height - 5)) + previous = self.container + self.container = current + addSubview(current) + } + } else { + if self.container is HorizontalContainerView { + current = self.container! + } else { + current = HorizontalContainerView(frame: containerView.bounds) + previous = self.container + self.container = current + addSubview(current) + } + } + + if let previous = previous { + previous.removeFromSuperview() + if animated { + current.layer?.animateAlpha(from: 0, to: 1, duration: 0.3) + } + } + + self.container?.set(item: item, animated: animated && previous == nil) + + needsLayout = true + } + + + override func mouseUp(with event: NSEvent) { + super.mouseUp(with: event) + showContextMenu(event) + } + + override var rowAppearance: NSAppearance? { + return darkPalette.appearance + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + diff --git a/Telegram-Mac/GroupCallPeerAvatarRowItem.swift b/Telegram-Mac/GroupCallPeerAvatarRowItem.swift new file mode 100644 index 0000000000..cea16421a7 --- /dev/null +++ b/Telegram-Mac/GroupCallPeerAvatarRowItem.swift @@ -0,0 +1,126 @@ +// +// GroupCallPeerAvatarRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 09.03.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import Postbox +import TelegramCore +import SwiftSignalKit + + +final class GroupCallPeerAvatarRowItem : GeneralRowItem { + fileprivate let account: Account + fileprivate let peer: Peer + fileprivate let nameLayout: TextViewLayout + init(_ initialSize: NSSize, stableId: AnyHashable, account: Account, peer: Peer, viewType: GeneralViewType, customTheme: GeneralRowItem.Theme) { + self.account = account + self.peer = peer + self.nameLayout = TextViewLayout(.initialize(string: peer.displayTitle, color: customTheme.textColor, font: .medium(.title))) + super.init(initialSize, stableId: stableId, viewType: viewType, customTheme: customTheme) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + + nameLayout.measure(width: blockWidth - viewType.innerInset.left - viewType.innerInset.right) + + return true + } + + override var height: CGFloat { + return 180 + } + + override var hasBorder: Bool { + return false + } + + override func viewClass() -> AnyClass { + return GroupCallPeerAvatarRowView.self + } +} + + +private final class GroupCallPeerAvatarRowView: GeneralContainableRowView { + private let imageView: TransformImageView = TransformImageView() + private let nameView = TextView() + private let shadowView = ShadowView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(imageView) + + + shadowView.direction = .vertical(true) + shadowView.shadowBackground = NSColor.black.withAlphaComponent(0.4) + self.addSubview(shadowView) + + addSubview(nameView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + guard let item = item as? GeneralRowItem else { + return + } + imageView.frame = containerView.bounds + nameView.setFrameOrigin(NSMakePoint(item.viewType.innerInset.left, imageView.frame.maxY - nameView.frame.height - item.viewType.innerInset.top)) + shadowView.frame = NSMakeRect(0, containerView.frame.height - 50, containerView.frame.width, 50) + } + + override var backdorColor: NSColor { + return .clear + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? GroupCallPeerAvatarRowItem else { + return + } + + nameView.update(item.nameLayout) + + let profileImageRepresentations:[TelegramMediaImageRepresentation] + if let peer = item.peer as? TelegramChannel { + profileImageRepresentations = peer.profileImageRepresentations + } else if let peer = item.peer as? TelegramUser { + profileImageRepresentations = peer.profileImageRepresentations + } else if let peer = item.peer as? TelegramGroup { + profileImageRepresentations = peer.profileImageRepresentations + } else { + profileImageRepresentations = [] + } + + let id = profileImageRepresentations.first?.resource.id.hashValue ?? Int(item.peer.id.toInt64()) + let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: MediaId.Id(id)), representations: profileImageRepresentations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + + layout() + + + if let dimension = profileImageRepresentations.last?.dimensions.size { + + + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: dimension, boundingSize: NSMakeSize(item.blockWidth, item.height), intrinsicInsets: NSEdgeInsets()) + self.imageView.setSignal(signal: cachedMedia(media: media, arguments: arguments, scale: self.backingScaleFactor), clearInstantly: false) + self.imageView.setSignal(chatMessagePhoto(account: item.account, imageReference: ImageMediaReference.standalone(media: media), peer: item.peer, scale: self.backingScaleFactor), clearInstantly: false, animate: true, cacheImage: { result in + cacheMedia(result, media: media, arguments: arguments, scale: System.backingScale) + }) + self.imageView.set(arguments: arguments) + + if let reference = PeerReference(item.peer) { + _ = fetchedMediaResource(mediaBox: item.account.postbox.mediaBox, reference: .avatar(peer: reference, resource: media.representations.last!.resource)).start() + } + } else { + self.imageView.setSignal(signal: generateEmptyRoundAvatar(self.imageView.frame.size, font: .avatar(90.0), account: item.account, peer: item.peer) |> map { TransformImageResult($0, true) }) + } + } +} diff --git a/Telegram-Mac/GroupCallPeerController.swift b/Telegram-Mac/GroupCallPeerController.swift new file mode 100644 index 0000000000..ebe8fffa09 --- /dev/null +++ b/Telegram-Mac/GroupCallPeerController.swift @@ -0,0 +1,295 @@ +// +// GroupCallPeerController.swift +// Telegram +// +// Created by Mikhail Filimonov on 09.03.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + + +import Cocoa +import TGUIKit +import SwiftSignalKit +import TelegramCore +import Postbox + + +private final class Arguments { + let account: Account + let openInfo:(PeerId)->Void + let openChat:(PeerId)->Void + let joinChannel:(PeerId)->Void + let leaveChannel:(PeerId)->Void + init(account: Account, openInfo: @escaping(PeerId)->Void, openChat: @escaping(PeerId)->Void, joinChannel: @escaping(PeerId)->Void, leaveChannel: @escaping(PeerId)->Void) { + self.account = account + self.openInfo = openInfo + self.openChat = openChat + self.joinChannel = joinChannel + self.leaveChannel = leaveChannel + } +} + +private struct State : Equatable { + static func == (lhs: State, rhs: State) -> Bool { + if lhs.peer != rhs.peer { + return false + } + if let lhsCachedData = lhs.cachedData, let rhsCachedData = rhs.cachedData { + if !lhsCachedData.isEqual(to: rhsCachedData) { + return false + } + } else if (lhs.cachedData != nil) != (rhs.cachedData != nil) { + return false + } + return true + } + + var peer: PeerEquatable + var cachedData: CachedPeerData? +} + +private struct ActionTuple: Equatable { + static func == (lhs: ActionTuple, rhs: ActionTuple) -> Bool { + return lhs.title == rhs.title && lhs.viewType == rhs.viewType + } + + var title: String + var action:()->Void + var viewType: GeneralViewType + + var id: InputDataIdentifier { + return .init(title) + } +} + +private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + let theme = GroupCallTheme.customTheme + + + + struct InfoTuple : Equatable { + var title: String + var info: String + var viewType:GeneralViewType + + var id: InputDataIdentifier { + return .init(title) + } + } + + var info:[InfoTuple] = [] + if let user = state.peer.peer as? TelegramUser { + let phoneNumber: String + if let phone = user.phone { + phoneNumber = formatPhoneNumber(phone) + } else { + phoneNumber = L10n.newContactPhoneHidden + } + info.append(.init(title: L10n.peerInfoPhone, info: phoneNumber, viewType: .singleItem)) + } + if let addressName = state.peer.peer.addressName { + info.append(.init(title: L10n.peerInfoUsername, info: "@\(addressName)", viewType: .singleItem)) + } + if state.peer.peer.isScam { + info.append(.init(title: L10n.peerInfoScam, info: L10n.peerInfoScamWarning, viewType: .singleItem)) + } else if state.peer.peer.isFake { + info.append(.init(title: L10n.peerInfoFake, info: L10n.peerInfoFakeWarning, viewType: .singleItem)) + } else if let cachedData = state.cachedData as? CachedUserData, let about = cachedData.about, !about.isEmpty { + info.append(.init(title: L10n.peerInfoAbout, info: about, viewType: .singleItem)) + } else if let cachedData = state.cachedData as? CachedChannelData, let about = cachedData.about, !about.isEmpty { + info.append(.init(title: L10n.peerInfoAbout, info: about, viewType: .singleItem)) + } + + let viewType: GeneralViewType = info.isEmpty ? .singleItem : .firstItem + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: .init("avatar"), equatable: InputDataEquatable(state), comparable: nil, item: { initialSize, stableId in + return GroupCallPeerAvatarRowItem(initialSize, stableId: stableId, account: arguments.account, peer: state.peer.peer, viewType: viewType, customTheme: theme) + })) + index += 1 + + + for i in 0 ..< info.count { + var value = info[i] + if i == 0 { + if info.count == 1 { + value.viewType = .lastItem + } else { + value.viewType = .innerItem + } + } else { + value.viewType = bestGeneralViewType(info, for: i) + } + info[i] = value + } + + if !info.isEmpty { +// entries.append(.sectionId(sectionId, type: .normal)) +// sectionId += 1 + + for info in info { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: info.id, equatable: InputDataEquatable(info), comparable: nil, item: { initialSize, stableId in + return GroupCallTextAndLabelItem(initialSize, stableId: stableId, label: info.title, text: info.info, viewType: info.viewType, customTheme: theme) + })) + index += 1 + } + } + + var actions:[ActionTuple] = [] + + if let peer = state.peer.peer as? TelegramUser { + actions.append(.init(title: L10n.voiceChatInfoSendMessage, action: { + arguments.openChat(peer.id) + }, viewType: .singleItem)) + + actions.append(.init(title: L10n.voiceChatInfoOpenProfile, action: { + arguments.openInfo(peer.id) + }, viewType: .singleItem)) + } else if let peer = state.peer.peer as? TelegramChannel { + + switch peer.participationStatus { + case .kicked: + break + default: + actions.append(.init(title: L10n.voiceChatInfoOpenChannel, action: { + arguments.openChat(peer.id) + }, viewType: .singleItem)) + } + + + switch peer.participationStatus { + case .left: + actions.append(.init(title: L10n.voiceChatInfoJoinChannel, action: { + arguments.joinChannel(peer.id) + }, viewType: .singleItem)) + case .member: + actions.append(.init(title: L10n.voiceChatInfoLeaveChannel, action: { + arguments.leaveChannel(peer.id) + }, viewType: .singleItem)) + case .kicked: + break + } + } + + for i in 0 ..< actions.count { + var value = actions[i] + value.viewType = bestGeneralViewType(actions, for: i) + actions[i] = value + } + + if !actions.isEmpty { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + for action in actions { + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: action.id, data: .init(name: action.title, color: theme.accentColor, type: .none, viewType: action.viewType, action: action.action, theme: theme))) + index += 1 + } + } + + + // entries + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func GroupCallPeerController(context: AccountContext, peer: Peer) -> InputDataModalController { + + let actionsDisposable = DisposableSet() + + let initialState = State(peer: PeerEquatable(peer)) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + let account = context.account + + var getWindow:(()->Window?)? = nil + + actionsDisposable.add(context.account.viewTracker.peerView(peer.id, updateData: true).start(next: { peerView in + updateState { current in + var current = current + if let peer = peerViewMainPeer(peerView) { + current.peer = PeerEquatable(peer) + current.cachedData = peerView.cachedData + } + return current + } + })) + + let arguments = Arguments(account: account, openInfo: { peerId in + appDelegate?.navigateProfile(peerId, account: account) + }, openChat: { peerId in + appDelegate?.navigateChat(peerId, account: account) + }, joinChannel: { peerId in + if let window = getWindow?() { + _ = showModalProgress(signal: context.engine.peers.joinChannel(peerId: peerId, hash: nil), for: window).start(error: { [weak window] error in + let text: String + switch error { + case .generic: + text = L10n.unknownError + case .tooMuchJoined: + text = L10n.joinChannelsTooMuch + case .tooMuchUsers: + text = L10n.groupUsersTooMuchError + } + if let window = window { + alert(for: window, info: text, appearance: GroupCallTheme.customTheme.appearance) + } + }) + } + + }, leaveChannel: { peerId in + if let window = getWindow?() { + _ = showModalProgress(signal: context.engine.peers.removePeerChat(peerId: peerId, reportChatSpam: false, deleteGloballyIfPossible: false), for: window).start() + } + }) + + let signal = statePromise.get() |> deliverOnPrepareQueue |> map { state in + return InputDataSignalValue(entries: entries(state, arguments: arguments)) + } + + let controller = InputDataController(dataSignal: signal, title: L10n.peerInfoInfo) + + controller.getBackgroundColor = { + GroupCallTheme.windowBackground + } + + controller.onDeinit = { + actionsDisposable.dispose() + } + let customTheme = GroupCallTheme.customTheme + + + let modalController = InputDataModalController(controller, modalInteractions: nil, size: NSMakeSize(350, 300)) + + controller.leftModalHeader = ModalHeaderData(image: #imageLiteral(resourceName: "Icon_ChatSearchCancel").precomposed(customTheme.accentColor), handler: { [weak modalController] in + modalController?.close() + }) + + + modalController.getModalTheme = { + .init(text: customTheme.textColor, grayText: customTheme.grayTextColor, background: customTheme.backgroundColor, border: customTheme.borderColor) + } + + getWindow = { [weak modalController] in + return modalController?.modal?.window + } + + + return modalController +} + + + diff --git a/Telegram-Mac/GroupCallRecorderRowItem.swift b/Telegram-Mac/GroupCallRecorderRowItem.swift new file mode 100644 index 0000000000..e4a0537f70 --- /dev/null +++ b/Telegram-Mac/GroupCallRecorderRowItem.swift @@ -0,0 +1,270 @@ +// +// GroupCallRecorderRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 02.03.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit +import TelegramCore + + +final class GroupCallRecorderRowItem : GeneralRowItem { + + fileprivate let account: Account + fileprivate let startedRecordedTime: Int32? + fileprivate let start: ()->Void + fileprivate let stop: ()->Void + + fileprivate let titleLayout: TextViewLayout + + init(_ initialSize: NSSize, stableId: AnyHashable, viewType: GeneralViewType, account: Account, startedRecordedTime: Int32?, customTheme: GeneralRowItem.Theme? = nil, start:@escaping()->Void, stop:@escaping()->Void) { + self.startedRecordedTime = startedRecordedTime + self.start = start + self.stop = stop + self.account = account + + let text: String + if let _ = startedRecordedTime { + text = L10n.voiceChatStopRecording + } else { + text = L10n.voiceChatStartRecording + } + self.titleLayout = TextViewLayout(.initialize(string: text, color: customTheme?.textColor ?? theme.colors.text, font: .normal(.text))) + + super.init(initialSize, height: 42, stableId: stableId, viewType: viewType, customTheme: customTheme) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + + titleLayout.measure(width: blockWidth - 80) + + return true + } + + override func viewClass() -> AnyClass { + return GroupCallRecorderRowView.self + } +} + +final class GroupCallRecorderRowView : GeneralContainableRowView { + private let textView: TextView = TextView() + private var indicator:View? + private var statusTextView: TextView? + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(textView) + + textView.userInteractionEnabled = false + textView.isSelectable = false + + containerView.set(handler: { [weak self] control in + self?.updateColors() + }, for: .Highlight) + + containerView.set(handler: { [weak self] control in + self?.updateColors() + }, for: .Normal) + + containerView.set(handler: { [weak self] control in + self?.updateColors() + }, for: .Hover) + + containerView.set(handler: { [weak self] control in + if let item = self?.item as? GroupCallRecorderRowItem { + if item.startedRecordedTime != nil { + item.stop() + } else { + item.start() + } + } + }, for: .Click) + + + } + + override func updateColors() { + super.updateColors() + containerView.backgroundColor = containerView.controlState != .Highlight ? backdorColor : highlightColor + textView.backgroundColor = containerView.backgroundColor + statusTextView?.backgroundColor = containerView.backgroundColor + } + + + var highlightColor: NSColor { + if let item = item as? GeneralRowItem, let theme = item.customTheme { + return theme.highlightColor + } + return theme.colors.grayHighlight + } + override var backdorColor: NSColor { + if let item = item as? GeneralRowItem, let theme = item.customTheme { + return theme.backgroundColor + } + return super.backdorColor + } + + var textColor: NSColor { + if let item = item as? GeneralRowItem, let theme = item.customTheme { + return theme.textColor + } + return theme.colors.text + } + var secondaryColor: NSColor { + if let item = item as? GeneralRowItem, let theme = item.customTheme { + return theme.grayTextColor + } + return theme.colors.grayText + } + + override func layout() { + super.layout() + + if let statusView = statusTextView { + statusView.setFrameOrigin(statusPos) + } + if let indicator = indicator { + indicator.setFrameOrigin(indicatorPos) + } + textView.setFrameOrigin(titlePos) + } + + var titlePos: NSPoint { + guard let item = item as? GroupCallRecorderRowItem else { + return .zero + } + if let _ = statusTextView { + return NSMakePoint(item.viewType.innerInset.left, 5) + } else { + return NSMakePoint(item.viewType.innerInset.left, floorToScreenPixels(backingScaleFactor, (containerView.frame.height - textView.frame.height) / 2)) + } + } + + var indicatorPos: NSPoint { + guard let item = item as? GroupCallRecorderRowItem else { + return .zero + } + if let indicator = indicator, let statusView = statusTextView { + return NSMakePoint(item.viewType.innerInset.left, containerView.frame.height - statusView.frame.height - 5 + ((statusView.frame.height - indicator.frame.height) / 2)) + } + return .zero + } + var statusPos: NSPoint { + guard let item = item as? GroupCallRecorderRowItem else { + return .zero + } + var point: NSPoint = .zero + if let statusView = statusTextView { + point = NSMakePoint(item.viewType.innerInset.left, containerView.frame.height - statusView.frame.height - 5) + } + if let indicator = indicator { + point.x += indicator.frame.width + 4 + } + return point + } + + private var timer: SwiftSignalKit.Timer? + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? GroupCallRecorderRowItem else { + return + } + textView.update(item.titleLayout) + + if let recording = item.startedRecordedTime { + let statusView: TextView + if let current = self.statusTextView { + statusView = current + } else { + statusView = TextView() + statusView.userInteractionEnabled = false + statusView.isSelectable = false + statusView.backgroundColor = containerView.backgroundColor + self.statusTextView = statusView + addSubview(statusView) + if animated { + statusView.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } + let indicator: View + if let current = self.indicator { + indicator = current + } else { + indicator = View() + self.indicator = indicator + indicator.setFrameSize(NSMakeSize(8, 8)) + indicator.layer?.cornerRadius = indicator.frame.height / 2 + addSubview(indicator) + + let animation = CABasicAnimation(keyPath: "opacity") + animation.timingFunction = .init(name: .easeInEaseOut) + animation.fromValue = 0.5 + animation.toValue = 1.0 + animation.duration = 1.0 + animation.autoreverses = true + animation.repeatCount = .infinity + animation.isRemovedOnCompletion = false + animation.fillMode = CAMediaTimingFillMode.forwards + + indicator.layer?.add(animation, forKey: "opacity") + } + + + + indicator.backgroundColor = item.customTheme?.redColor ?? theme.colors.redUI + + let duration:Int32 = item.account.network.getApproximateRemoteTimestamp() - recording + let layout = TextViewLayout(.initialize(string: timerText(Int(duration)), color: secondaryColor, font: .normal(.short))) + + layout.measure(width: .greatestFiniteMagnitude) + statusView.update(layout) + textView.change(pos: titlePos, animated: animated) + + timer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self, weak item] in + if let item = item { + self?.set(item: item, animated: animated) + } + }, queue: .mainQueue()) + timer?.start() + + } else { + + timer?.invalidate() + timer = nil + + if let statusView = statusTextView { + self.statusTextView = nil + if animated { + statusView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false,completion: { [weak statusView] _ in + statusView?.removeFromSuperview() + }) + } else { + statusView.removeFromSuperview() + } + textView.change(pos: titlePos, animated: animated) + } + if let indicator = indicator { + self.indicator = nil + if animated { + indicator.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false,completion: { [weak indicator] _ in + indicator?.removeFromSuperview() + }) + } else { + indicator.removeFromSuperview() + } + } + } + + needsLayout = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/GroupCallSchedule.swift b/Telegram-Mac/GroupCallSchedule.swift new file mode 100644 index 0000000000..edab8a0ea3 --- /dev/null +++ b/Telegram-Mac/GroupCallSchedule.swift @@ -0,0 +1,185 @@ +// +// GroupCallSchedule.swift +// Telegram +// +// Created by Mikhail Filimonov on 06.04.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit +import TelegramCore +import Postbox + + +private final class GroupCallScheduleTimerView : View { + private let counter = DynamicCounterTextView(frame: .zero) + private var nextTimer: SwiftSignalKit.Timer? + + private let headerView = TextView() + private let descView = TextView() + + private let maskImage: CGImage + private let mask: View + required init(frame frameRect: NSRect) { + mask = View(frame: NSMakeRect(0, 0, frameRect.width, 64)) + let purple = GroupCallTheme.purple + let pink = GroupCallTheme.pink + + headerView.userInteractionEnabled = false + headerView.isSelectable = false + + descView.userInteractionEnabled = false + descView.isSelectable = false + + + + maskImage = generateImage(mask.frame.size, contextGenerator: { size, ctx in + ctx.clear(size.bounds) + let colorSpace = CGColorSpaceCreateDeviceRGB() + var locations:[CGFloat] = [0.0, 0.85, 1.0] + let gradient = CGGradient(colorsSpace: colorSpace, colors: [pink.cgColor, purple.cgColor, purple.cgColor] as CFArray, locations: &locations)! + ctx.drawLinearGradient(gradient, start: CGPoint(x: 0, y: 0.0), end: CGPoint(x: size.width, y: size.height), options: []) + })! + super.init(frame: frameRect) + addSubview(mask) + addSubview(headerView) + addSubview(descView) + + isEventLess = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(time timeValue: Int32, animated: Bool) { + let time = Int(timeValue - Int32(Date().timeIntervalSince1970)) + + let text = timerText(time, addminus: false) + let value = DynamicCounterTextView.make(for: text, count: text, font: .avatar(50), textColor: .white, width: frame.width) + + counter.update(value, animated: animated, reversed: true) + + + counter.change(size: value.size, animated: animated) + counter.change(pos: focus(value.size).origin, animated: animated) + + self.nextTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: false, completion: { [weak self] in + self?.update(time: timeValue, animated: true) + }, queue: .mainQueue()) + + + let counterSubviews = counter.effectiveSubviews + + while mask.subviews.count > counterSubviews.count { + mask.subviews.removeLast() + } + while mask.subviews.count < counterSubviews.count { + let view = ImageView() + view.image = maskImage + view.sizeToFit() + mask.addSubview(view) + } + + for (i, mask) in mask.subviews.enumerated() { + mask.layer?.mask = counterSubviews[i].layer + } + mask.setFrameSize(value.size) + mask.change(pos: NSMakePoint(round((frame.width - value.size.width) / 2), focus(mask.frame.size).minY), animated: animated) + self.nextTimer?.start() + + if time <= 5 { + if mask.layer?.animation(forKey: "opacity") == nil { + let animation: CABasicAnimation = CABasicAnimation(keyPath: "opacity") + animation.timingFunction = .init(name: .easeInEaseOut) + animation.fromValue = 1 + animation.toValue = 0.5 + animation.duration = 1.0 + animation.autoreverses = true + animation.isRemovedOnCompletion = true + animation.fillMode = CAMediaTimingFillMode.forwards + + mask.layer?.add(animation, forKey: "opacity") + } + } else { + mask.layer?.removeAnimation(forKey: "opacity") + } + + let headerText = time >= 0 ? L10n.voiceChatScheduledHeader : L10n.voiceChatScheduledHeaderLate + + let headerLayout = TextViewLayout.init(.initialize(string: headerText, color: GroupCallTheme.customTheme.textColor, font: .avatar(26))) + headerLayout.measure(width: frame.width - 60) + headerView.update(headerLayout) + headerView.centerX(y: mask.frame.minY - headerView.frame.height) + + + + let descLayout = TextViewLayout.init(.initialize(string: stringForMediumDate(timestamp: timeValue), color: GroupCallTheme.customTheme.textColor, font: .avatar(26))) + descLayout.measure(width: frame.width - 60) + descView.update(descLayout) + descView.centerX(y: mask.frame.maxY) + + } + + override func layout() { + super.layout() + mask.center() + } +} + + +final class GroupCallScheduleView : View { + private weak var currentView: View? + private var timerView: GroupCallScheduleTimerView? + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + isEventLess = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(_ state: GroupCallUIState, arguments: GroupCallUIArguments?, animated: Bool) { + if let scheduleTimestamp = state.state.scheduleTimestamp { + let current: GroupCallScheduleTimerView + if let timerView = timerView { + current = timerView + } else { + current = GroupCallScheduleTimerView(frame: NSMakeRect(0, 0, frame.width, frame.height)) + self.timerView = current + addSubview(current) + + if animated { + current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } + if current != self.currentView { + if let view = self.currentView { + if animated { + view.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } else { + view.removeFromSuperview() + } + } + } + self.timerView?.update(time: scheduleTimestamp, animated: animated) + self.currentView = current + } + } + + func updateLayout(size: NSSize, transition: ContainedViewLayoutTransition) { + if let view = currentView { + transition.updateFrame(view: view, frame: NSMakeRect(0, 0, size.width, size.height)) + } + } + + override func layout() { + super.layout() + updateLayout(size: self.frame.size, transition: .immediate) + } +} diff --git a/Telegram-Mac/GroupCallSettingsController.swift b/Telegram-Mac/GroupCallSettingsController.swift new file mode 100644 index 0000000000..2359d8ee86 --- /dev/null +++ b/Telegram-Mac/GroupCallSettingsController.swift @@ -0,0 +1,881 @@ +// +// GroupCallSettingsController.swift +// Telegram +// +// Created by Mikhail Filimonov on 25/11/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit +import TelegramCore + +import Postbox +import HotKey + +private final class Arguments { + let sharedContext: SharedAccountContext + let toggleInputAudioDevice:(String?)->Void + let toggleOutputAudioDevice:(String?)->Void + let toggleInputVideoDevice:(String?)->Void + let finishCall:()->Void + let updateDefaultParticipantsAreMuted: (Bool)->Void + let updateSettings: (@escaping(VoiceCallSettings)->VoiceCallSettings)->Void + let checkPermission:()->Void + let showTooltip:(String)->Void + let switchAccount:(PeerId)->Void + let startRecording:()->Void + let stopRecording:()->Void + let resetLink:()->Void + let setNoiseSuppression:(Bool)->Void + let reduceMotions:(Bool)->Void + let selectVideoRecordOrientation:(GroupCallSettingsState.VideoOrientation)->Void + let toggleRecordVideo: ()->Void + init(sharedContext: SharedAccountContext, + toggleInputAudioDevice: @escaping(String?)->Void, + toggleOutputAudioDevice:@escaping(String?)->Void, + toggleInputVideoDevice:@escaping(String?)->Void, + finishCall:@escaping()->Void, + updateDefaultParticipantsAreMuted: @escaping(Bool)->Void, + updateSettings: @escaping(@escaping(VoiceCallSettings)->VoiceCallSettings)->Void, + checkPermission:@escaping()->Void, + showTooltip: @escaping(String)->Void, + switchAccount: @escaping(PeerId)->Void, + startRecording: @escaping()->Void, + stopRecording: @escaping()->Void, + resetLink: @escaping()->Void, + setNoiseSuppression:@escaping(Bool)->Void, + reduceMotions:@escaping(Bool)->Void, + selectVideoRecordOrientation:@escaping(GroupCallSettingsState.VideoOrientation)->Void, + toggleRecordVideo: @escaping()->Void) { + self.sharedContext = sharedContext + self.toggleInputAudioDevice = toggleInputAudioDevice + self.toggleOutputAudioDevice = toggleOutputAudioDevice + self.toggleInputVideoDevice = toggleInputVideoDevice + self.finishCall = finishCall + self.updateDefaultParticipantsAreMuted = updateDefaultParticipantsAreMuted + self.updateSettings = updateSettings + self.checkPermission = checkPermission + self.showTooltip = showTooltip + self.switchAccount = switchAccount + self.startRecording = startRecording + self.stopRecording = stopRecording + self.resetLink = resetLink + self.setNoiseSuppression = setNoiseSuppression + self.reduceMotions = reduceMotions + self.selectVideoRecordOrientation = selectVideoRecordOrientation + self.toggleRecordVideo = toggleRecordVideo + } +} + +final class GroupCallSettingsView : View { + fileprivate let tableView:TableView = TableView() + private let titleContainer = View() + fileprivate let backButton: ImageButton = ImageButton() + private let title: TextView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(tableView) + addSubview(titleContainer) + titleContainer.addSubview(backButton) + titleContainer.addSubview(title) + let backColor = NSColor(srgbRed: 175, green: 170, blue: 172, alpha: 1.0) + + title.userInteractionEnabled = false + title.isSelectable = false + + let icon = #imageLiteral(resourceName: "Icon_NavigationBack").precomposed(backColor) + let activeIcon = #imageLiteral(resourceName: "Icon_NavigationBack").precomposed(backColor.withAlphaComponent(0.7)) + backButton.set(image: icon, for: .Normal) + backButton.set(image: activeIcon, for: .Highlight) + + _ = backButton.sizeToFit(.zero, NSMakeSize(24, 24), thatFit: true) + + let layout = TextViewLayout.init(.initialize(string: L10n.voiceChatSettingsTitle, color: GroupCallTheme.customTheme.textColor, font: .medium(.header))) + layout.measure(width: frame.width - 200) + title.update(layout) + tableView.getBackgroundColor = { + GroupCallTheme.windowBackground + } + updateLocalizationAndTheme(theme: theme) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + // super.updateLocalizationAndTheme(theme: theme) + backgroundColor = GroupCallTheme.windowBackground + titleContainer.backgroundColor = GroupCallTheme.windowBackground + title.backgroundColor = GroupCallTheme.windowBackground + } + + override func layout() { + super.layout() + titleContainer.frame = NSMakeRect(0, 0, frame.width, 54) + tableView.frame = NSMakeRect(0, titleContainer.frame.maxY, frame.width, frame.height - titleContainer.frame.height) + backButton.centerY(x: 90) + let f = titleContainer.focus(title.frame.size) + title.setFrameOrigin(NSMakePoint(max(126, f.minX), f.minY)) + } +} + + +struct GroupCallSettingsState : Equatable { + + + + enum VideoOrientation : Equatable { + case landscape + case portrait + + var rawValue: Bool { + switch self { + case .portrait: + return true + case .landscape: + return false + } + } + } + + var hasPermission: Bool? + var title: String? + var displayAsList: [FoundPeer]? + var recordName: String? + var recordVideo: Bool + var videoOrientation: VideoOrientation +} + +private let _id_leave_chat = InputDataIdentifier.init("_id_leave_chat") +private let _id_reset_link = InputDataIdentifier.init("_id_reset_link") +private let _id_input_audio = InputDataIdentifier("_id_input_audio") +private let _id_output_audio = InputDataIdentifier("_id_output_audio") +private let _id_micro = InputDataIdentifier("_id_micro") +private let _id_speak_admin_only = InputDataIdentifier("_id_speak_admin_only") +private let _id_speak_all_members = InputDataIdentifier("_id_speak_all_members") +private let _id_input_mode_always = InputDataIdentifier("_id_input_mode_always") +private let _id_input_mode_ptt = InputDataIdentifier("_id_input_mode_ptt") +private let _id_ptt = InputDataIdentifier("_id_ptt") +private let _id_input_mode_ptt_se = InputDataIdentifier("_id_input_mode_ptt_se") +private let _id_input_mode_toggle = InputDataIdentifier("_id_input_mode_toggle") +private let _id_noise_suppression = InputDataIdentifier("_id_noise_suppression") + +private let _id_input_chat_title = InputDataIdentifier("_id_input_chat_title") +private let _id_input_record_title = InputDataIdentifier("_id_input_record_title") + +private let _id_listening_link = InputDataIdentifier("_id_listening_link") +private let _id_speaking_link = InputDataIdentifier("_id_speaking_link") + +private let _id_reduce_motion = InputDataIdentifier("_id_reduce_motion") + +private let _id_record_video_toggle = InputDataIdentifier("_id_record_video_toggle") + +private func _id_peer(_ id:PeerId) -> InputDataIdentifier { + return InputDataIdentifier("_id_peer_\(id.toInt64())") +} + +private func groupCallSettingsEntries(callState: GroupCallUIState, devices: IODevices, uiState: GroupCallSettingsState, settings: VoiceCallSettings, account: Account, peer: Peer, accountPeer: Peer, joinAsPeerId: PeerId, arguments: Arguments) -> [InputDataEntry] { + + var entries:[InputDataEntry] = [] + let theme = GroupCallTheme.customTheme + + var sectionId: Int32 = 0 + var index:Int32 = 0 + + entries.append(.sectionId(sectionId, type: .customModern(10))) + sectionId += 1 + + let state = callState.state + + + + if state.canManageCall { +// entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.voiceChatSettingsTitle), data: .init(color: GroupCallTheme.grayStatusColor, viewType: .textTopItem))) +// index += 1 + + entries.append(.input(sectionId: sectionId, index: index, value: .string(uiState.title), error: nil, identifier: _id_input_chat_title, mode: .plain, data: .init(viewType: .singleItem, pasteFilter: nil, customTheme: theme), placeholder: nil, inputPlaceholder: L10n.voiceChatSettingsTitlePlaceholder, filter: { $0 }, limit: 40)) + index += 1 + + } + + + if let list = uiState.displayAsList { + + if !list.isEmpty { + + if case .sectionId = entries.last { + + } else { + entries.append(.sectionId(sectionId, type: .customModern(20))) + sectionId += 1 + } + + struct Tuple : Equatable { + let peer: FoundPeer + let viewType: GeneralViewType + let selected: Bool + let status: String? + } + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.voiceChatSettingsDisplayAsTitle), data: .init(color: GroupCallTheme.grayStatusColor, viewType: .textTopItem))) + index += 1 + + let tuple = Tuple(peer: FoundPeer(peer: accountPeer, subscribers: nil), viewType: uiState.displayAsList == nil || uiState.displayAsList?.isEmpty == false ? .firstItem : .singleItem, selected: accountPeer.id == joinAsPeerId, status: L10n.voiceChatSettingsDisplayAsPersonalAccount) + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: .init("self"), equatable: InputDataEquatable(tuple), comparable: nil, item: { initialSize, stableId in + return ShortPeerRowItem(initialSize, peer: tuple.peer.peer, account: account, stableId: stableId, height: 50, photoSize: NSMakeSize(36, 36), titleStyle: ControlStyle(font: .medium(.title), foregroundColor: theme.textColor, highlightColor: .white), statusStyle: ControlStyle(foregroundColor: theme.grayTextColor), status: tuple.status, inset: NSEdgeInsets(left: 30, right: 30), interactionType: .plain, generalType: .selectable(tuple.selected), viewType: tuple.viewType, action: { + arguments.switchAccount(tuple.peer.peer.id) + }, customTheme: theme) + })) + index += 1 + + for peer in list { + + var status: String? + if let subscribers = peer.subscribers { + if peer.peer.isChannel { + status = L10n.voiceChatJoinAsChannelCountable(Int(subscribers)) + } else if peer.peer.isSupergroup || peer.peer.isGroup { + status = L10n.voiceChatJoinAsGroupCountable(Int(subscribers)) + } + } + + var viewType = bestGeneralViewType(list, for: peer) + if list.first == peer { + if list.count == 1 { + viewType = .lastItem + } else { + viewType = .innerItem + } + } + + let tuple = Tuple(peer: peer, viewType: viewType, selected: peer.peer.id == joinAsPeerId, status: status) + + + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_peer(peer.peer.id), equatable: InputDataEquatable(tuple), comparable: nil, item: { initialSize, stableId in + return ShortPeerRowItem(initialSize, peer: tuple.peer.peer, account: account, stableId: stableId, height: 50, photoSize: NSMakeSize(36, 36), titleStyle: ControlStyle(font: .medium(.title), foregroundColor: theme.textColor, highlightColor: .white), statusStyle: ControlStyle(foregroundColor: theme.grayTextColor), status: tuple.status, inset: NSEdgeInsets(left: 30, right: 30), interactionType: .plain, generalType: .selectable(tuple.selected), viewType: tuple.viewType, action: { + arguments.switchAccount(tuple.peer.peer.id) + }, customTheme: theme) + + })) + } + } + + } else { + + if case .sectionId = entries.last { + + } else { + entries.append(.sectionId(sectionId, type: .customModern(20))) + sectionId += 1 + } + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: .init("loading"), equatable: nil, comparable: nil, item: { initialSize, stableId in + return GeneralLoadingRowItem(initialSize, stableId: stableId, viewType: .lastItem) + })) + index += 1 + } + + + if state.canManageCall && state.scheduleTimestamp == nil { + if case .sectionId = entries.last { + + } else { + entries.append(.sectionId(sectionId, type: .customModern(20))) + sectionId += 1 + } + + + let recordTitle: String + let recordPlaceholder: String = L10n.voiecChatSettingsRecordPlaceholder1 + if callState.peer.isChannel || callState.peer.isGigagroup { + recordTitle = L10n.voiecChatSettingsRecordLiveTitle + } else if !callState.videoActive(.list).isEmpty { + recordTitle = L10n.voiecChatSettingsRecordVideoTitle + } else { + recordTitle = L10n.voiecChatSettingsRecordTitle + } + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(recordTitle), data: .init(color: GroupCallTheme.grayStatusColor, viewType: .textTopItem))) + index += 1 + + + + let recordingStartTimestamp = state.recordingStartTimestamp + + if recordingStartTimestamp == nil { + entries.append(.input(sectionId: sectionId, index: index, value: .string(uiState.recordName), error: nil, identifier: _id_input_record_title, mode: .plain, data: .init(viewType: .firstItem, pasteFilter: nil, customTheme: theme), placeholder: nil, inputPlaceholder: recordPlaceholder, filter: { $0 }, limit: 40)) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_record_video_toggle, data: .init(name: L10n.voiceChatSettingsRecordIncludeVideo, color: theme.textColor, type: .switchable(uiState.recordVideo), viewType: .innerItem, action: arguments.toggleRecordVideo, theme: theme))) + index += 1 + + if uiState.recordVideo { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: .init("video_orientation"), equatable: InputDataEquatable(uiState), comparable: nil, item: { initialSize, stableId in + return GroupCallVideoOrientationRowItem(initialSize, stableId: stableId, viewType: .innerItem, account: account, customTheme: theme, selected: uiState.videoOrientation, select: arguments.selectVideoRecordOrientation) + })) + index += 1 + } + + } + struct Tuple : Equatable { + let recordingStartTimestamp: Int32? + let viewType: GeneralViewType + } + + let tuple = Tuple(recordingStartTimestamp: recordingStartTimestamp, viewType: recordingStartTimestamp == nil ? .lastItem : .singleItem) + + + + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: .init("recording"), equatable: InputDataEquatable(tuple), comparable: nil, item: { initialSize, stableId in + return GroupCallRecorderRowItem(initialSize, stableId: stableId, viewType: tuple.viewType, account: account, startedRecordedTime: tuple.recordingStartTimestamp, customTheme: theme, start: arguments.startRecording, stop: arguments.stopRecording) + })) + index += 1 + + } + +// if state.canManageCall { +// +// entries.append(.sectionId(sectionId, type: .customModern(20))) +// sectionId += 1 +// +// //TODOLANG +// entries.append(.desc(sectionId: sectionId, index: index, text: .plain("INVITE LINKS"), data: .init(color: GroupCallTheme.grayStatusColor, viewType: .textTopItem))) +// index += 1 +// +// //TODOLANG +// entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_listening_link, data: InputDataGeneralData(name: "Copy Listening Link", color: theme.accentColor, type: .none, viewType: .firstItem, enabled: true, action: { +// copyToClipboard("t.me/listeninglink") +// arguments.showTooltip("Listening link successfully copied to Clipboard") +// }, theme: theme))) +// index += 1 +// +// //TODOLANG +// entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_speaking_link, data: InputDataGeneralData(name: "Copy Speaking Link", color: theme.accentColor, type: .none, viewType: .lastItem, enabled: true, action: { +// copyToClipboard("t.me/speakinglink") +// arguments.showTooltip("Speaking link successfully copied to Clipboard") +// }, theme: theme))) +// index += 1 +// +// entries.append(.desc(sectionId: sectionId, index: index, text: .plain("Use these links to invite listeners or speakers to your voice chat."), data: .init(color: GroupCallTheme.grayStatusColor, viewType: .textBottomItem))) +// index += 1 +// +// +// } + + + if state.canManageCall, let defaultParticipantMuteState = state.defaultParticipantMuteState { + + if case .sectionId = entries.last { + + } else { + entries.append(.sectionId(sectionId, type: .customModern(20))) + sectionId += 1 + } + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.voiceChatSettingsPermissionsTitle), data: .init(color: GroupCallTheme.grayStatusColor, viewType: .textTopItem))) + index += 1 + + let isMuted = defaultParticipantMuteState == .muted + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_speak_all_members, data: InputDataGeneralData(name: L10n.voiceChatSettingsAllMembers, color: theme.textColor, type: .selectable(!isMuted), viewType: .firstItem, enabled: true, action: { + arguments.updateDefaultParticipantsAreMuted(false) + }, theme: theme))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_speak_admin_only, data: InputDataGeneralData(name: L10n.voiceChatSettingsOnlyAdmins, color: theme.textColor, type: .selectable(isMuted), viewType: .lastItem, enabled: true, action: { + arguments.updateDefaultParticipantsAreMuted(true) + }, theme: theme))) + index += 1 + + + } + + + if case .sectionId = entries.last { + + } else { + entries.append(.sectionId(sectionId, type: .customModern(20))) + sectionId += 1 + } + + + let microDevice = settings.audioInputDeviceId == nil ? devices.audioInput.first : devices.audioInput.first(where: { $0.uniqueID == settings.audioInputDeviceId }) + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.callSettingsInputTitle), data: .init(color: GroupCallTheme.grayStatusColor, viewType: .textTopItem))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_input_audio, data: .init(name: L10n.callSettingsInputText, color: theme.textColor, type: .contextSelector(settings.audioInputDeviceId == nil ? L10n.callSettingsDeviceDefault : microDevice?.localizedName ?? L10n.callSettingsDeviceDefault, [SPopoverItem(L10n.callSettingsDeviceDefault, { + arguments.toggleInputAudioDevice(nil) + })] + devices.audioInput.map { value in + return SPopoverItem(value.localizedName, { + arguments.toggleInputAudioDevice(value.uniqueID) + }) + }), viewType: microDevice == nil ? .singleItem : .firstItem, theme: theme))) + index += 1 + + if let microDevice = microDevice { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_micro, equatable: InputDataEquatable(microDevice.uniqueID), comparable: nil, item: { initialSize, stableId -> TableRowItem in + return MicrophonePreviewRowItem(initialSize, stableId: stableId, context: arguments.sharedContext, viewType: .lastItem, customTheme: theme) + })) + index += 1 + } + + + entries.append(.sectionId(sectionId, type: .customModern(20))) + sectionId += 1 + + + let outputDevice = devices.audioOutput.first(where: { $0.uniqueID == settings.audioOutputDeviceId }) + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.voiceChatSettingsOutput), data: .init(color: GroupCallTheme.grayStatusColor, viewType: .textTopItem))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_output_audio, data: .init(name: L10n.voiceChatSettingsOutputDevice, color: theme.textColor, type: .contextSelector(outputDevice?.localizedName ?? L10n.callSettingsDeviceDefault, [SPopoverItem(L10n.callSettingsDeviceDefault, { + arguments.toggleOutputAudioDevice(nil) + })] + devices.audioOutput.map { value in + return SPopoverItem(value.localizedName, { + arguments.toggleOutputAudioDevice(value.uniqueID) + }) + }), viewType: .singleItem, theme: theme))) + index += 1 + + + entries.append(.sectionId(sectionId, type: .customModern(20))) + sectionId += 1 + + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.voiceChatSettingsPushToTalkTitle), data: .init(color: GroupCallTheme.grayStatusColor, viewType: .textTopItem))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_input_mode_toggle, data: .init(name: L10n.voiceChatSettingsPushToTalkEnabled, color: theme.textColor, type: .switchable(settings.mode != .none), viewType: .singleItem, action: { + if settings.mode == .none { + arguments.checkPermission() + } + arguments.updateSettings { + $0.withUpdatedMode($0.mode == .none ? .pushToTalk : .none) + } + }, theme: theme))) + index += 1 + + switch settings.mode { + case .none: + break + default: + entries.append(.sectionId(sectionId, type: .customModern(20))) + sectionId += 1 + + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.voiceChatSettingsInputMode), data: .init(color: GroupCallTheme.grayStatusColor, viewType: .textTopItem))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_input_mode_always, data: .init(name: L10n.voiceChatSettingsInputModeAlways, color: theme.textColor, type: .selectable(settings.mode == .always), viewType: .firstItem, action: { + arguments.updateSettings { + $0.withUpdatedMode(.always) + } + }, theme: theme))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_input_mode_ptt, data: .init(name: L10n.voiceChatSettingsInputModePushToTalk, color: theme.textColor, type: .selectable(settings.mode == .pushToTalk), viewType: .lastItem, action: { + arguments.updateSettings { + $0.withUpdatedMode(.pushToTalk) + } + }, theme: theme))) + index += 1 + + + + entries.append(.sectionId(sectionId, type: .customModern(20))) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.voiceChatSettingsPushToTalk), data: .init(color: GroupCallTheme.grayStatusColor, viewType: .modern(position: .single, insets: NSEdgeInsetsMake(0, 16, 0, 0))))) + index += 1 + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_ptt, equatable: InputDataEquatable(settings.pushToTalk), comparable: nil, item: { initialSize, stableId -> TableRowItem in + return PushToTalkRowItem(initialSize, stableId: stableId, settings: settings.pushToTalk, update: { value in + arguments.updateSettings { + $0.withUpdatedPushToTalk(value) + } + }, checkPermission: arguments.checkPermission, viewType: .singleItem) + })) + index += 1 + + if let permission = uiState.hasPermission { + if !permission { + + let text: String + if #available(macOS 10.15, *) { + text = L10n.voiceChatSettingsPushToTalkAccess + } else { + text = L10n.voiceChatSettingsPushToTalkAccessOld + } + + entries.append(.desc(sectionId: sectionId, index: index, text: .customMarkdown(text, linkColor: GroupCallTheme.speakLockedColor, linkFont: .bold(11.5), linkHandler: { permission in + PermissionsManager.openInputMonitoringPrefs() + }), data: .init(color: GroupCallTheme.speakLockedColor, viewType: .modern(position: .single, insets: NSEdgeInsetsMake(0, 16, 0, 0))))) + index += 1 + } else { + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.voiceChatSettingsPushToTalkDesc), data: .init(color: GroupCallTheme.grayStatusColor, viewType: .modern(position: .single, insets: NSEdgeInsetsMake(0, 16, 0, 0))))) + index += 1 + } + } + } + + entries.append(.sectionId(sectionId, type: .customModern(20))) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.voiceChatSettingsPerformanceHeader), data: .init(color: GroupCallTheme.grayStatusColor, viewType: .textTopItem))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_noise_suppression, data: InputDataGeneralData(name: L10n.voiceChatSettingsNoiseSuppression, color: theme.textColor, type: .switchable(settings.noiseSuppression), viewType: .singleItem, enabled: true, action: { + arguments.setNoiseSuppression(!settings.noiseSuppression) + }, theme: theme))) + index += 1 + + + +// entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_reduce_motion, data: InputDataGeneralData(name: L10n.voiceChatSettingsReduceMotion, color: theme.textColor, type: .switchable(!settings.visualEffects), viewType: .lastItem, enabled: true, action: { +// arguments.reduceMotions(!settings.visualEffects) +// }, theme: theme))) +// index += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.voiceChatSettingsPerformanceDesc), data: .init(color: GroupCallTheme.grayStatusColor, viewType: .textBottomItem))) + index += 1 + + + if state.canManageCall { + entries.append(.sectionId(sectionId, type: .customModern(20))) + sectionId += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_reset_link, data: InputDataGeneralData(name: L10n.voiceChatSettingsResetLink, color: GroupCallTheme.customTheme.accentColor, type: .none, viewType: .firstItem, enabled: true, action: arguments.resetLink, theme: theme))) + index += 1 + + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_leave_chat, data: InputDataGeneralData(name: L10n.voiceChatSettingsEnd, color: GroupCallTheme.speakLockedColor, type: .none, viewType: .lastItem, enabled: true, action: arguments.finishCall, theme: theme))) + index += 1 + } + + entries.append(.sectionId(sectionId, type: .customModern(20))) + sectionId += 1 + + return entries +} + +// entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_input_mode_ptt_se, data: .init(name: "Sound Effects", color: .white, type: .switchable(settings.pushToTalkSoundEffects), viewType: .lastItem, action: { +// updateSettings { +// $0.withUpdatedSoundEffects(!$0.pushToTalkSoundEffects) +// } +// }, theme: theme))) +// index += 1 +// + +final class GroupCallSettingsController : GenericViewController { + fileprivate let sharedContext: SharedAccountContext + fileprivate let call: PresentationGroupCall + private let disposable = MetaDisposable() + private let account: Account + private let monitorPermissionDisposable = MetaDisposable() + private let actualizeTitleDisposable = MetaDisposable() + private let displayAsPeersDisposable = MetaDisposable() + + private let callState: Signal + + init(sharedContext: SharedAccountContext, account: Account, callState: Signal, call: PresentationGroupCall) { + self.sharedContext = sharedContext + self.account = account + self.call = call + self.callState = callState + super.init() + bar = .init(height: 0) + } + + private var tableView: TableView { + return genericView.tableView + } + private var firstTake: Bool = true + + override func firstResponder() -> NSResponder? { + if self.window?.firstResponder == self.window || self.window?.firstResponder == tableView.documentView { + var first: NSResponder? = nil + var isRecordingPushToTalk: Bool = false + tableView.enumerateViews { view -> Bool in + if let view = view as? PushToTalkRowView { + if view.mode == .editing { + isRecordingPushToTalk = true + return false + } + } + return true + } + if !isRecordingPushToTalk { + tableView.enumerateViews { view -> Bool in + first = view.firstResponder + if first != nil, self.firstTake { + if let item = view.item as? InputDataRowDataValue { + switch item.value { + case let .string(value): + let value = value ?? "" + if !value.isEmpty { + return true + } + default: + break + } + } + } + return first == nil + } + self.firstTake = false + return first + } else { + return window?.firstResponder + } + + } + return window?.firstResponder + } + + override func escapeKeyAction() -> KeyHandlerResult { + return .rejected + } + + override var enableBack: Bool { + return true + } + + deinit { + disposable.dispose() + monitorPermissionDisposable.dispose() + actualizeTitleDisposable.dispose() + displayAsPeersDisposable.dispose() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + _ = self.window?.makeFirstResponder(nil) + + window?.set(mouseHandler: { [weak self] event -> KeyHandlerResult in + guard let `self` = self else {return .rejected} + + let index = self.tableView.row(at: self.tableView.documentView!.convert(event.locationInWindow, from: nil)) + + if index > -1, let view = self.tableView.item(at: index).view { + if view.mouseInsideField { + if self.window?.firstResponder != view.firstResponder { + _ = self.window?.makeFirstResponder(view.firstResponder) + return .invoked + } + } + } + + return .invokeNext + }, with: self, for: .leftMouseUp, priority: self.responderPriority) + } + private func fetchData() -> [InputDataIdentifier : InputDataValue] { + var values:[InputDataIdentifier : InputDataValue] = [:] + tableView.enumerateItems { item -> Bool in + if let identifier = (item.stableId.base as? InputDataEntryId)?.identifier { + if let item = item as? InputDataRowDataValue { + values[identifier] = item.value + } + } + return true + } + return values + } + + private var getState:(()->GroupCallSettingsState?)? = nil + + override func viewDidLoad() { + super.viewDidLoad() + + + + self.genericView.tableView._mouseDownCanMoveWindow = true + + let account = self.account + + let initialState = GroupCallSettingsState(hasPermission: nil, title: nil, recordVideo: true, videoOrientation: .landscape) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((GroupCallSettingsState) -> GroupCallSettingsState) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + monitorPermissionDisposable.set((KeyboardGlobalHandler.getPermission() |> deliverOnMainQueue).start(next: { value in + updateState { current in + var current = current + current.hasPermission = value + return current + } + })) + + getState = { [weak stateValue] in + return stateValue?.with { $0 } + } + + + actualizeTitleDisposable.set(call.state.start(next: { state in + updateState { current in + var current = current + if current.title == nil { + current.title = state.title + } + return current + } + })) + + displayAsPeersDisposable.set(combineLatest(queue: prepareQueue,call.displayAsPeers, account.postbox.peerView(id: account.peerId)).start(next: { list, peerView in + updateState { current in + var current = current + current.displayAsList = list + return current + } + })) + + genericView.backButton.set(handler: { [weak self] _ in + self?.navigationController?.back() + }, for: .Click) + + let sharedContext = self.sharedContext + + + let arguments = Arguments(sharedContext: sharedContext, toggleInputAudioDevice: { value in + _ = updateVoiceCallSettingsSettingsInteractively(accountManager: sharedContext.accountManager, { + $0.withUpdatedAudioInputDeviceId(value) + }).start() + }, toggleOutputAudioDevice: { value in + _ = updateVoiceCallSettingsSettingsInteractively(accountManager: sharedContext.accountManager, { + $0.withUpdatedAudioOutputDeviceId(value) + }).start() + }, toggleInputVideoDevice: { value in + _ = updateVoiceCallSettingsSettingsInteractively(accountManager: sharedContext.accountManager, { + $0.withUpdatedCameraInputDeviceId(value) + }).start() + }, finishCall: { [weak self] in + guard let window = self?.window else { + return + } + confirm(for: window, header: L10n.voiceChatSettingsEndConfirmTitle, information: L10n.voiceChatSettingsEndConfirm, okTitle: L10n.voiceChatSettingsEndConfirmOK, successHandler: { [weak self] _ in + + guard let call = self?.call, let window = self?.window else { + return + } + _ = showModalProgress(signal: call.sharedContext.endGroupCall(terminate: true), for: window).start() + }, appearance: darkPalette.appearance) + + }, updateDefaultParticipantsAreMuted: { [weak self] value in + self?.call.updateDefaultParticipantsAreMuted(isMuted: value) + }, updateSettings: { f in + _ = updateVoiceCallSettingsSettingsInteractively(accountManager: sharedContext.accountManager, f).start() + }, checkPermission: { + updateState { current in + var current = current + current.hasPermission = KeyboardGlobalHandler.hasPermission() + return current + } + }, showTooltip: { [weak self] text in + if let window = self?.window { + showModalText(for: window, text: text) + } + }, switchAccount: { [weak self] peerId in + self?.call.reconnect(as: peerId) + }, startRecording: { [weak self] in + if let window = self?.window { + confirm(for: window, header: L10n.voiceChatRecordingStartTitle, information: L10n.voiceChatRecordingStartText1, okTitle: L10n.voiceChatRecordingStartOK, successHandler: { _ in + self?.call.setShouldBeRecording(true, title: stateValue.with { $0.recordName }, videoOrientation: stateValue.with { $0.recordVideo ? $0.videoOrientation.rawValue : nil}) + }) + } + }, stopRecording: { [weak self] in + if let window = self?.window { + confirm(for: window, header: L10n.voiceChatRecordingStopTitle, information: L10n.voiceChatRecordingStopText, okTitle: L10n.voiceChatRecordingStopOK, successHandler: { [weak window] _ in + self?.call.setShouldBeRecording(false, title: nil, videoOrientation: nil) + if let window = window { + showModalText(for: window, text: L10n.voiceChatToastStop) + } + }) + } + }, resetLink: { [weak self] in + self?.call.resetListenerLink() + if let window = self?.window { + showModalText(for: window, text: L10n.voiceChatSettingsResetLinkSuccess) + } + }, setNoiseSuppression: { value in + _ = updateVoiceCallSettingsSettingsInteractively(accountManager: sharedContext.accountManager, { + $0.withUpdatedNoiseSuppression(value) + }).start() + }, reduceMotions: { value in + _ = updateVoiceCallSettingsSettingsInteractively(accountManager: sharedContext.accountManager, { + $0.withUpdatedVisualEffects(value) + }).start() + }, selectVideoRecordOrientation: { value in + updateState { current in + var current = current + current.videoOrientation = value + return current + } + }, toggleRecordVideo: { + updateState { current in + var current = current + current.recordVideo = !current.recordVideo + return current + } + }) + + let previousEntries:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) + let inputDataArguments = InputDataArguments(select: { _, _ in }, dataUpdated: { [weak self] in + guard let `self` = self else { + return + } + let data = self.fetchData() + var previousTitle: String? = stateValue.with { $0.title } + + updateState { current in + var current = current + current.title = data[_id_input_chat_title]?.stringValue ?? current.title + current.recordName = data[_id_input_record_title]?.stringValue ?? current.title + return current + } + let title = stateValue.with({ $0.title }) + if previousTitle != title, let title = title { + self.call.updateTitle(title, force: false) + } + }) + let initialSize = self.atomicSize + let joinAsPeer: Signal = self.call.joinAsPeerIdValue + let signal: Signal = combineLatest(queue: prepareQueue, sharedContext.devicesContext.signal, voiceCallSettings(sharedContext.accountManager), appearanceSignal, self.call.account.postbox.loadedPeerWithId(self.call.peerId), self.call.account.postbox.loadedPeerWithId(account.peerId), joinAsPeer, self.callState, statePromise.get()) |> mapToQueue { devices, settings, appearance, peer, accountPeer, joinAsPeerId, state, uiState in + let entries = groupCallSettingsEntries(callState: state, devices: devices, uiState: uiState, settings: settings, account: account, peer: peer, accountPeer: accountPeer, joinAsPeerId: joinAsPeerId, arguments: arguments).map { AppearanceWrapperEntry(entry: $0, appearance: appearance) } + return prepareInputDataTransition(left: previousEntries.swap(entries), right: entries, animated: true, searchState: nil, initialSize: initialSize.with { $0 }, arguments: inputDataArguments, onMainQueue: false) + } |> deliverOnMainQueue + + disposable.set(signal.start(next: { [weak self] value in + self?.genericView.tableView.merge(with: value) + self?.readyOnce() + })) + + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + backgroundColor = GroupCallTheme.windowBackground + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + if let state = getState?(), let title = state.title { + self.call.updateTitle(title, force: true) + } + self.window?.removeObserver(for: self) + } + + override func backKeyAction() -> KeyHandlerResult { + return .invokeNext + } + + override func returnKeyAction() -> KeyHandlerResult { + self.navigationController?.back() + return super.returnKeyAction() + } + +} diff --git a/Telegram-Mac/GroupCallSpeakButton.swift b/Telegram-Mac/GroupCallSpeakButton.swift new file mode 100644 index 0000000000..585507fe46 --- /dev/null +++ b/Telegram-Mac/GroupCallSpeakButton.swift @@ -0,0 +1,218 @@ +// +// GroupCallSpeakButton.swift +// Telegram +// +// Created by Mikhail Filimonov on 23/11/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import TelegramCore + + +final class GroupCallSpeakButton : Control { + private let animationView: LottiePlayerView + required init(frame frameRect: NSRect) { + animationView = LottiePlayerView(frame: NSMakeRect(0, 0, frameRect.width - 20, frameRect.height - 20)) + super.init(frame: frameRect) + + + scaleOnClick = true + addSubview(animationView) + } + + override var mouseDownCanMoveWindow: Bool { + return false + } + + override func layout() { + super.layout() + update(size: bounds.size, transition: .immediate) + } + + func update(size: CGSize, transition: ContainedViewLayoutTransition) { + let newSize = NSMakeSize(size.width - 20, size.height - 20) + transition.updateFrame(view: animationView, frame: focus(newSize)) + animationView.update(size: newSize, transition: transition) + } + + private var previousState: PresentationGroupCallState? + private var previousIsMuted: Bool? + + func update(with state: PresentationGroupCallState, isMuted: Bool, animated: Bool) { + if state.scheduleTimestamp == nil { + switch state.networkState { + case .connecting: + userInteractionEnabled = false + case .connected: + if isMuted { + if let _ = state.muteState { + userInteractionEnabled = true + } else { + userInteractionEnabled = true + } + } else { + userInteractionEnabled = true + } + } + } else { + userInteractionEnabled = true + } + + + + + let scheduleState = state.scheduleState + let previousScheduleState = previousState?.scheduleState + + + let activeRaiseHand = state.muteState?.canUnmute == false + let previousActiveRaiseHand = previousState?.muteState?.canUnmute == false + let raiseHandUpdated = activeRaiseHand != previousActiveRaiseHand + let scheduleUpdated = scheduleState != previousScheduleState + + let previousIsMuted = self.previousIsMuted + let isMutedUpdated = (previousState?.muteState != nil) != (state.muteState != nil) || previousIsMuted != isMuted + + if previousState != nil { + if scheduleUpdated { + if scheduleState == nil, let previous = previousScheduleState { + if state.canManageCall { + playChangeState(.voice_chat_start_chat_to_mute) + } else { + if previous.subscribed { + if activeRaiseHand { + playChangeState(.voice_chat_cancel_reminder_to_raise_hand) + } else { + playChangeState(.voice_chat_cancel_reminder_to_mute) + } + } else { + if activeRaiseHand { + playChangeState(.voice_chat_set_reminder_to_raise_hand) + } else { + playChangeState(.voice_chat_set_reminder_to_mute) + } + } + } + } else { + if scheduleState?.subscribed != previousScheduleState?.subscribed { + if let subscribed = scheduleState?.subscribed { + playChangeState(subscribed ? .voice_chat_cancel_reminder : .voice_chat_set_reminder) + } + } + } + //playChangeState(state.canManageCall ? .voice_chat_start_chat_to_mute : .voice_chat_start_chat_to_mute) + } else if raiseHandUpdated { + if activeRaiseHand { + playChangeState(previousState?.muteState != nil ? .voice_chat_hand_on_muted : .voice_chat_hand_on_unmuted) + } else { + playChangeState(.voice_chat_hand_off) + } + } else if isMutedUpdated { + if isMuted { + playChangeState(.voice_chat_mute) + } else { + playChangeState(.voice_chat_unmute) + } + } + } else { + if let scheduleState = scheduleState { + if state.canManageCall { + setupScheduled(.voice_chat_start_chat_to_mute) + } else { + setupEndAnimation(scheduleState.subscribed ? .voice_chat_cancel_reminder : .voice_chat_set_reminder) + } + } else if activeRaiseHand { + setupEndAnimation(activeRaiseHand ? .voice_chat_hand_off : .voice_chat_hand_on_muted) + } else { + setupEndAnimation(isMuted ? .voice_chat_mute : .voice_chat_unmute) + } + } + + + self.previousState = state + self.previousIsMuted = isMuted + } + + private func setupScheduled(_ animation: LocalAnimatedSticker) { + if let data = animation.data { + animationView.set(LottieAnimation(compressed: data, key: .init(key: .bundle(animation.rawValue), size: renderSize), cachePurpose: .none, playPolicy: .framesCount(1), maximumFps: 60, runOnQueue: .mainQueue())) + } + } + + private func setupEndAnimation(_ animation: LocalAnimatedSticker) { + if let data = animation.data { + animationView.set(LottieAnimation(compressed: data, key: .init(key: .bundle(animation.rawValue), size: renderSize), cachePurpose: .none, playPolicy: .toEnd(from: .max), maximumFps: 60, runOnQueue: .mainQueue())) + } + } + private func playChangeState(_ animation: LocalAnimatedSticker) { + if let data = animation.data { + + let animated = allHands.contains(where: { $0.rawValue == currentAnimation?.rawValue}) + + var fromFrame: Int32 = 1 + if currentAnimation?.rawValue == animation.rawValue { + fromFrame = self.animationView.currentFrame ?? 1 + } + + animationView.set(LottieAnimation(compressed: data, key: .init(key: .bundle(animation.rawValue), size: renderSize), cachePurpose: .none, playPolicy: .toEnd(from: fromFrame), maximumFps: 60, runOnQueue: .mainQueue()), animated: animated) + + + } + } + + private var renderSize: NSSize { + return NSMakeSize(120, 120) + } + + let allHands:[LocalAnimatedSticker] = [.voice_chat_raise_hand_1, + .voice_chat_raise_hand_2, + .voice_chat_raise_hand_3, + .voice_chat_raise_hand_4, + .voice_chat_raise_hand_5, + .voice_chat_raise_hand_6, + .voice_chat_raise_hand_7] + + private var currentAnimation: LocalAnimatedSticker? + + func playRaiseHand() { + let raise_hand: LocalAnimatedSticker + + + var startFrame: Int32 = 1 + if let current = currentAnimation { + raise_hand = current + startFrame = animationView.currentFrame ?? 1 + } else { + var random = Int.random(in: 0 ... 6) + loop: while random == 5 || random == 4 { + let percent = Int.random(in: 0 ..< 100) + if percent == 1 { + break loop + } else { + random = Int.random(in: 0 ... 6) + } + } + raise_hand = allHands[random] + } + + if let data = raise_hand.data { + let animation = LottieAnimation(compressed: data, key: .init(key: .bundle("\(arc4random())"), size: renderSize), cachePurpose: .none, playPolicy: .toStart(from: startFrame), maximumFps: 60, runOnQueue: .mainQueue()) + + animation.onFinish = { [weak self] in + self?.currentAnimation = nil + self?.animationView.ignoreCachedContext() + } + self.currentAnimation = raise_hand + animationView.set(animation) + } + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + diff --git a/Telegram-Mac/GroupCallSpeakingTooltipView.swift b/Telegram-Mac/GroupCallSpeakingTooltipView.swift new file mode 100644 index 0000000000..bdbfccf4d6 --- /dev/null +++ b/Telegram-Mac/GroupCallSpeakingTooltipView.swift @@ -0,0 +1,62 @@ +// +// GroupCallSpeakingTooltipView.swift +// Telegram +// +// Created by Mikhail Filimonov on 25.05.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit + +import Postbox +import TelegramCore + +final class GroupCallSpeakingTooltipView: Control { + private let nameView: TextView = TextView() + private let avatarView = GroupCallAvatarView(frame: NSMakeRect(0, 0, 42, 42), photoSize: NSMakeSize(30, 30)) + private let backgroundView = View() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(backgroundView) + addSubview(avatarView) + addSubview(nameView) + + nameView.userInteractionEnabled = false + nameView.isSelectable = false + avatarView.isEventLess = true + backgroundView.isEventLess = true + scaleOnClick = true + } + + func setPeer(data: PeerGroupCallData, account: Account, audioLevel:(PeerId)->Signal?) { + self.avatarView.update(audioLevel, data: data, activityColor: GroupCallTheme.speakActiveColor, account: account, animated: true) + + + let attr = NSMutableAttributedString() + _ = attr.append(string: L10n.voiceChatTooltipIsSpeaking(data.peer.compactDisplayTitle.prefix(25)), color: GroupCallTheme.customTheme.textColor, font: .normal(.text)) + attr.detectBoldColorInString(with: .medium(.text)) + let layout = TextViewLayout(attr, maximumNumberOfLines: 1) + layout.measure(width: 200) + nameView.update(layout) + + + setFrameSize(NSMakeSize(30 + layout.layoutSize.width + 30, 42)) + + backgroundView.layer?.cornerRadius = 30 / 2 + backgroundView.backgroundColor = GroupCallTheme.membersColor + } + + override func layout() { + super.layout() + backgroundView.frame = focus(NSMakeSize(frame.width - 12, 30)) + avatarView.centerY(x: 0) + nameView.centerY(x: 30 + 10) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + diff --git a/Telegram-Mac/GroupCallTextAndLabelRowItem.swift b/Telegram-Mac/GroupCallTextAndLabelRowItem.swift new file mode 100644 index 0000000000..67865cd65c --- /dev/null +++ b/Telegram-Mac/GroupCallTextAndLabelRowItem.swift @@ -0,0 +1,164 @@ +// +// GroupCallTextAndLabelRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 09.03.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + + +class GroupCallTextAndLabelItem: GeneralRowItem { + + + + private var labelStyle:ControlStyle = ControlStyle() + private var textStyle:ControlStyle = ControlStyle() + + private var label:NSAttributedString + + var labelLayout:(TextNodeLayout, TextNode)? + var textLayout:TextViewLayout + + + init(_ initialSize:NSSize, stableId:AnyHashable, label:String, text: String, viewType: GeneralViewType, customTheme: GeneralRowItem.Theme) { + self.label = NSAttributedString.initialize(string: label, color: customTheme.accentColor, font: .normal(FontSize.text)) + let attr = NSMutableAttributedString() + _ = attr.append(string: text.trimmed.fullTrimmed, color: customTheme.textColor, font: .normal(.title)) + + textLayout = TextViewLayout(attr, alwaysStaticItems: true) + textLayout.interactions = globalLinkExecutor + + super.init(initialSize, stableId: stableId, viewType: viewType, customTheme: customTheme) + } + + override func viewClass() -> AnyClass { + return GroupCallTextAndLabelRowView.self + } + + var textWidth:CGFloat { + return blockWidth - viewType.innerInset.left - viewType.innerInset.right + } + + override var height: CGFloat { + return labelsHeight + viewType.innerInset.top + viewType.innerInset.bottom - 4 + } + + var labelsHeight:CGFloat { + var inset:CGFloat = 0 + if let labelLayout = labelLayout { + inset = labelLayout.0.size.height + } + return (textLayout.layoutSize.height + inset + 4) + } + + var textY:CGFloat { + var inset:CGFloat = 0 + if let labelLayout = labelLayout { + inset = labelLayout.0.size.height + } + return ((height - labelsHeight) / 2.0) + inset + 4.0 + } + + var labelY:CGFloat { + return (height - labelsHeight) / 2.0 + } + +// + override func makeSize(_ width: CGFloat, oldWidth:CGFloat) -> Bool { + let result = super.makeSize(width, oldWidth: oldWidth) + textLayout.measure(width: textWidth) + + labelLayout = TextNode.layoutText(maybeNode: nil, label, nil, 1, .end, NSMakeSize(textWidth, .greatestFiniteMagnitude), nil, false, .left) + return result + } + +} + +private class GroupCallTextAndLabelRowView: GeneralRowView { + private let containerView = GeneralRowContainerView(frame: NSZeroRect) + private var labelView:TextView = TextView() + override func draw(_ layer: CALayer, in ctx: CGContext) { + + if let item = item as? GroupCallTextAndLabelItem, let label = item.labelLayout, layer == containerView.layer { + switch item.viewType { + case .legacy: + label.1.draw(NSMakeRect(item.inset.left, item.labelY, label.0.size.width, label.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backdorColor) + if item.drawCustomSeparator { + ctx.setFillColor(theme.colors.border.cgColor) + ctx.fill(NSMakeRect(item.inset.left, frame.height - .borderSize, frame.width - item.inset.left - item.inset.right, .borderSize)) + } + case let .modern(position, insets): + label.1.draw(NSMakeRect(insets.left, item.labelY, label.0.size.width, label.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backdorColor) + if position.border { + ctx.setFillColor(theme.colors.border.cgColor) + ctx.fill(NSMakeRect(insets.left, self.containerView.frame.height - .borderSize, self.containerView.frame.width - insets.left - insets.right, .borderSize)) + } + } + } + + } + + override var backdorColor: NSColor { + guard let item = item as? GeneralRowItem, let customTheme = item.customTheme else { + return theme.colors.background + } + return customTheme.backgroundColor + } + override func updateColors() { + if let item = item as? GroupCallTextAndLabelItem { + self.labelView.backgroundColor = backdorColor + self.containerView.backgroundColor = backdorColor + self.background = item.viewType.rowBackground + } + } + + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + containerView.addSubview(labelView) + self.addSubview(self.containerView) + self.containerView.displayDelegate = self + self.containerView.userInteractionEnabled = false + + } + + override func layout() { + super.layout() + + guard let item = item as? GroupCallTextAndLabelItem else { + return + } + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) + + if let _ = item.labelLayout { + labelView.setFrameOrigin(item.viewType.innerInset.left, item.textY) + } else { + labelView.centerY(x: item.viewType.innerInset.left) + } + self.containerView.setCorners(item.viewType.corners) + } + + + override func set(item:TableRowItem, animated:Bool = false) { + super.set(item: item, animated: animated) + + if let item = item as? GroupCallTextAndLabelItem { + labelView.update(item.textLayout) + } + containerView.needsDisplay = true + needsLayout = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + +} diff --git a/Telegram-Mac/GroupCallTileRowItem.swift b/Telegram-Mac/GroupCallTileRowItem.swift new file mode 100644 index 0000000000..6247de4e79 --- /dev/null +++ b/Telegram-Mac/GroupCallTileRowItem.swift @@ -0,0 +1,65 @@ +// +// GroupCallTileRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 04.06.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit + +import Postbox +import TelegramCore + +final class GroupCallTileRowItem : GeneralRowItem { + fileprivate let takeView: ()->(NSSize, GroupCallTileView)? + init(_ initialSize: NSSize, stableId: AnyHashable, takeView: @escaping()->(NSSize, GroupCallTileView)?) { + self.takeView = takeView + super.init(initialSize, stableId: stableId) + } + + override var height: CGFloat { + let value = takeView() + if let value = value { + return value.1.getSize(value.0).height + 5 + } + return 1 + } + + override var instantlyResize: Bool { + return true + } + + override func viewClass() -> AnyClass { + return GroupCallTileRowView.self + } +} + +private final class GroupCallTileRowView: TableRowView { + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + override var backdorColor: NSColor { + return GroupCallTheme.windowBackground + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + guard let item = item as? GroupCallTileRowItem else { + return + } + if let view = item.takeView() { + addSubview(view.1) + } + } + + +} diff --git a/Telegram-Mac/GroupCallTileView.swift b/Telegram-Mac/GroupCallTileView.swift new file mode 100644 index 0000000000..ec168886f2 --- /dev/null +++ b/Telegram-Mac/GroupCallTileView.swift @@ -0,0 +1,569 @@ +// +// GroupCallTileView.swift +// Telegram +// +// Created by Mikhail Filimonov on 10.05.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import TelegramCore +import Postbox +import SwiftSignalKit + + +struct VoiceChatTile { + fileprivate(set) var rect: NSRect + fileprivate(set) var index: Int + + var bestQuality: PresentationGroupCallRequestedVideo.Quality { + let option = min(rect.width, rect.height) + if option > 500 { + return .full + } else if option > 160 { + return .medium + } else { + return .thumbnail + } + } +} + + +func tileViews(_ count: Int, isFullscreen: Bool, frameSize: NSSize, pinnedIndex: Int? = nil) -> [VoiceChatTile] { + + var tiles:[VoiceChatTile] = [] + let minSize: NSSize = NSMakeSize(240, 135) + + func optimalCellSize(_ size: NSSize, count: Int) -> (size: NSSize, cols: Int, rows: Int) { + var size: NSSize = frameSize + var cols: Int = 2 + while true { + if count == 0 { + return (size: size, cols: 0, rows: 0) + } else if count == 1 { + return (size: size, cols: 1, rows: 1) + } else if count == 2 { + if !isFullscreen { + return (size: NSMakeSize(frameSize.width / 2, frameSize.height), cols: 2, rows: 1) + } else { + return (size: NSMakeSize(frameSize.width, frameSize.height / 2), cols: 1, rows: 2) + } + } else { + if size.width / size.height > 2 { + cols += Int(floor(size.width / size.height / 3.0)) + } + + var rows: Int = Int(ceil(Float(count) / Float(cols))) + if floor(frameSize.height / CGFloat(rows)) <= minSize.height { + cols = Int(max(floor(frameSize.width / minSize.width), 2)) + rows = Int(ceil(Float(count) / Float(cols))) + var height = minSize.height + if CGFloat(rows) * minSize.height < frameSize.height { + height = frameSize.height / CGFloat(rows) + } + return (size: NSMakeSize(frameSize.width / CGFloat(cols), height), cols: cols, rows: rows) + } + if floor(CGFloat(rows) * size.height) > floor(frameSize.height) { + size = NSMakeSize(frameSize.width / CGFloat(cols), frameSize.height / CGFloat(rows)) + } else { + size = NSMakeSize(frameSize.width / CGFloat(cols), frameSize.height / CGFloat(rows)) + return (size: size, cols: cols, rows: rows) + } + } + } + } + + var data = optimalCellSize(frameSize, count: count) + data.size = NSMakeSize(floor(data.size.width), floor(data.size.height)) + + var point: CGPoint = .zero + var index: Int = 0 + let inset: CGFloat = 5 + let insetSize = NSMakeSize(floor(CGFloat((data.cols - 1) * 5) / CGFloat(data.cols)), floor(CGFloat((data.rows - 1) * 5) / CGFloat(data.rows))) + + + let firstIsSuperior = data.rows * data.cols > count && data.cols == 2 + + if data.rows * data.cols > count && data.cols == 2 { + tiles.append(.init(rect: CGRect(origin: point, size: CGSize(width: frameSize.width, height: data.size.height - insetSize.height)), index: index)) + point.y += (data.size.height - insetSize.height) + inset + index += 1 + } + + for _ in 0 ..< data.rows { + for _ in 0 ..< data.cols { + if index < count { + let size = (data.size - insetSize) + tiles.append(.init(rect: CGRect(origin: point, size: size), index: index)) + point.x += size.width + inset + index += 1 + } + } + point.x = 0 + point.y += data.size.height - insetSize.height + inset + } + + let getPos:(Int) -> (col: Int, row: Int) = { index in + + if index == 0 { + return (col: 0, row: 0) + } + + let index = index + + let row = Int(floor(Float(index) / Float(data.cols))) + + + if data.rows * data.cols > count && data.cols <= 2 { + let col = (index - 1) % data.cols + return (col: col, row: col == 0 ? row + 1 : row) + } else { + return (col: index % data.cols, row: row) + } + } + + if let pinnedIndex = pinnedIndex { + let pinnedPos = getPos(pinnedIndex) + for i in 0 ..< tiles.count { + let pos = getPos(i) + var tile = tiles[i] + + let farAway = (col: CGFloat(pos.col - pinnedPos.col), row: CGFloat(pos.row - pinnedPos.row)) + + if i == pinnedIndex { + tile.rect = frameSize.bounds + } else { + var x: CGFloat = 0 + var y: CGFloat = 0 + + if i == 0 && firstIsSuperior { + x = 0 + } else { + x += farAway.col * frameSize.width + x += max(0, farAway.col - 1) * inset + } + y += farAway.row * frameSize.height + y += max(0, farAway.row - 1) * inset + + tile.rect = CGRect(origin: CGPoint(x: x, y: y), size: frameSize) + } + tiles[i] = tile + } + } + + return tiles +} + + + +private final class LimitView : View { + + private class V : NSVisualEffectView { + override var mouseDownCanMoveWindow: Bool { + return true + } + } + + private let effectView: NSVisualEffectView = V() + private let textView = TextView() + private let imageView = TransformImageView() + private let thumbView = ImageView() + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(imageView) + addSubview(effectView) + addSubview(textView) + addSubview(thumbView) + effectView.wantsLayer = true + effectView.material = .dark + effectView.blendingMode = .withinWindow + if #available(OSX 10.12, *) { + effectView.isEmphasized = true + } + effectView.state = .active + + textView.userInteractionEnabled = false + textView.isSelectable = false + } + private var dimension: CGSize? + func update(_ peer: Peer, size: NSSize, context: AccountContext) { + let profileImageRepresentations:[TelegramMediaImageRepresentation] + if let peer = peer as? TelegramChannel { + profileImageRepresentations = peer.profileImageRepresentations + } else if let peer = peer as? TelegramUser { + profileImageRepresentations = peer.profileImageRepresentations + } else if let peer = peer as? TelegramGroup { + profileImageRepresentations = peer.profileImageRepresentations + } else { + profileImageRepresentations = [] + } + + let id = profileImageRepresentations.first?.resource.id.hashValue ?? Int(peer.id.toInt64()) + let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: MediaId.Id(id)), representations: profileImageRepresentations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + + + if let dimension = profileImageRepresentations.last?.dimensions.size { + self.dimension = dimension + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: dimension, boundingSize: size, intrinsicInsets: NSEdgeInsets()) + self.imageView.set(arguments: arguments) + self.imageView.setSignal(signal: cachedMedia(media: media, arguments: arguments, scale: self.backingScaleFactor), clearInstantly: false) + self.imageView.setSignal(chatMessagePhoto(account: context.account, imageReference: ImageMediaReference.standalone(media: media), peer: peer, scale: self.backingScaleFactor), clearInstantly: false, animate: false, cacheImage: { result in + cacheMedia(result, media: media, arguments: arguments, scale: System.backingScale) + }) + + if let reference = PeerReference(peer) { + _ = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: .avatar(peer: reference, resource: media.representations.last!.resource)).start() + } + } else { + self.imageView.setSignal(signal: generateEmptyRoundAvatar(self.imageView.frame.size, font: .avatar(90.0), account: context.account, peer: peer) |> map { TransformImageResult($0, true) }) + } + + let config = GroupCallsConfig(context.appConfiguration) + + let layout = TextViewLayout(.initialize(string: L10n.voiceChatTooltipErrorVideoUnavailable(config.videoLimit), color: GroupCallTheme.customTheme.textColor, font: .medium(.text))) + layout.measure(width: size.width - 40) + textView.update(layout) + + thumbView.image = GroupCallTheme.video_limit + thumbView.sizeToFit() + needsLayout = true + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + + if let dimension = self.dimension { + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: dimension, boundingSize: frame.size, intrinsicInsets: NSEdgeInsets()) + self.imageView.set(arguments: arguments) + } + + effectView.frame = bounds + imageView.frame = bounds + thumbView.center() + thumbView.setFrameOrigin(NSMakePoint(thumbView.frame.minX, thumbView.frame.minY - 20)) + + textView.resize(frame.width - 40) + textView.center() + textView.setFrameOrigin(NSMakePoint(textView.frame.minX, textView.frame.minY + 20)) + + } +} + + +final class GroupCallTileView: View { + + private struct TileEntry : Comparable, Identifiable { + static func < (lhs: TileEntry, rhs: TileEntry) -> Bool { + return lhs.index < rhs.index + } + var stableId: Int { + return video.endpointId.hash + } + let video: DominantVideo + let member: PeerGroupCallData + let isFullScreen: Bool + let isPinned: Bool + let isFocused: Bool + let resizeMode: CALayerContentsGravity + let index: Int + let alone: Bool + } + + struct Transition { + let size: NSSize + let prevPinnedIndex: Int? + let pinnedIndex: Int? + let prevTiles:[VoiceChatTile] + let tiles:[VoiceChatTile] + } + + + private var views: [GroupCallMainVideoContainerView] = [] + private var items:[TileEntry] = [] + private let call: PresentationGroupCall + private var controlsMode: GroupCallView.ControlsMode = .normal + private var arguments: GroupCallUIArguments? = nil + private var prevState: GroupCallUIState? + private var pinnedIndex: Int? = nil + + private var limitView: LimitView? = nil + + init(call: PresentationGroupCall, arguments: GroupCallUIArguments?, frame: NSRect) { + self.call = call + self.arguments = arguments + super.init(frame: frame) + self.layer?.cornerRadius = 10 + } + + func update(state: GroupCallUIState, context: AccountContext, transition: ContainedViewLayoutTransition, size: NSSize, animated: Bool, controlsMode: GroupCallView.ControlsMode) -> Transition { + + + self.controlsMode = controlsMode + + var items:[TileEntry] = [] + + let prevItems = self.items + + + let prevTiles = tileViews(self.items.count, isFullscreen: prevState?.isFullScreen ?? state.isFullScreen, frameSize: frame.size, pinnedIndex: self.items.firstIndex(where: { $0.isPinned || $0.isFocused })) + + let prevTilesOpaque = tileViews(self.items.count, isFullscreen: prevState?.isFullScreen ?? state.isFullScreen, frameSize: frame.size, pinnedIndex: nil) + + + let prevPinnedIndex = self.items.firstIndex(where: { $0.isPinned || $0.isFocused }) + + + let activeMembers = state.videoActive(.main) + + let activeVideos = state.activeVideoViews.filter { $0.mode == .main } + + for member in activeMembers { + let endpoints:[String] = [member.presentationEndpoint, member.videoEndpoint].compactMap { $0 } + for endpointId in endpoints { + if let activeVideo = state.activeVideoViews.first(where: { $0.mode == .main && $0.endpointId == endpointId }) { + + let source: VideoSourceMacMode? + if member.videoEndpoint == endpointId { + source = .video + } else if member.presentationEndpoint == endpointId { + source = .screencast + } else { + source = nil + } + if let source = source { + + let pinVideo = state.dominantSpeaker?.endpointId == endpointId ? state.dominantSpeaker : nil + + let resizeMode: CALayerContentsGravity = state.isFullScreen ? .resizeAspect : .resizeAspectFill + + let video = DominantVideo(member.peer.id, endpointId, source, pinVideo?.pinMode) + items.append(.init(video: video, member: member, isFullScreen: state.isFullScreen, isPinned: pinVideo?.pinMode == .permanent, isFocused: pinVideo?.pinMode == .focused || activeVideos.count == 1, resizeMode: resizeMode, index: activeVideo.index, alone: activeVideos.count == 1)) + } + } + } + } + + + + let tiles = tileViews(items.count, isFullscreen: state.isFullScreen, frameSize: frame.size, pinnedIndex: self.pinnedIndex) + + + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: self.items, rightList: items) + + var deletedViews:[Int: GroupCallMainVideoContainerView] = [:] + + for rdx in deleteIndices.reversed() { + deletedViews[rdx] = self.deleteItem(at: rdx, animated: animated) + self.items.remove(at: rdx) + } + for (idx, item, pix) in indicesAndItems { + var prevFrame: NSRect? = nil + var prevView: GroupCallMainVideoContainerView? = nil + if let pix = pix { + prevFrame = prevTiles[pix].rect + prevView = deletedViews[pix] + } + self.insertItem(item, at: idx, prevFrame: prevFrame, prevView: prevView, frame: tiles[idx].rect, animated: animated) + self.items.insert(item, at: idx) + } + + + for (idx, item, prev) in updateIndices { + let item = item + updateItem(item, at: idx, prevFrame: prev != idx ? prevTiles[prev].rect : nil, animated: animated) + self.items[idx] = item + } + self.prevState = state + self.pinnedIndex = self.items.firstIndex(where: { $0.isPinned || $0.isFocused }) + + for (i, view) in views.enumerated() { + if let pinnedIndex = pinnedIndex, i == pinnedIndex { + view.layer?.zPosition = 1000 + } else { + view.layer?.zPosition = CGFloat(i) + } + } + + let size = getSize(size) + + var update: Bool = false + + + + var prevPinnedId: String? + if let prevPinnedIndex = prevPinnedIndex { + prevPinnedId = prevItems[prevPinnedIndex].video.endpointId + } + + var pinnedId: String? + if let pinnedIndex = pinnedIndex { + pinnedId = items[pinnedIndex].video.endpointId + } + + if prevPinnedId != nil, pinnedId != nil, prevPinnedId != pinnedId { + update = true + } else if let pinnedId = pinnedId { + let contains = prevItems.contains(where: { tile in + tile.video.endpointId == pinnedId + }) + if !contains { + update = true + } + } else if pinnedId == nil, let prevPinnedId = prevPinnedId { + let contains = items.contains(where: { tile in + tile.video.endpointId == prevPinnedId + }) + if !contains { + update = true + } + } + + if update { + updateLayout(size: size, transition: .immediate) + } + + if tiles.isEmpty { + let current: LimitView + if let v = self.limitView { + current = v + } else { + current = LimitView(frame: size.bounds) + self.limitView = current + addSubview(current) + + if animated { + current.layer?.animateAlpha(from: 0, to: 2, duration: 0.2) + } + } + current.update(state.peer, size: size, context: context) + } else { + if let view = self.limitView { + self.limitView = nil + if animated { + view.layer?.animateAlpha(from: 2, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } else { + view.removeFromSuperview() + } + } + } + + return Transition(size: size, prevPinnedIndex: prevPinnedIndex, pinnedIndex: pinnedIndex, prevTiles: prevTilesOpaque, tiles: tiles) + } + + func getSize(_ size: NSSize) -> NSSize { + + let tiles = tileViews(items.count, isFullscreen: prevState?.isFullScreen ?? false, frameSize: size, pinnedIndex: pinnedIndex) + + if let tile = tiles.last, pinnedIndex == nil { + if tile.rect.maxY - size.height < 4 { + return NSMakeSize(size.width, size.height) + } else { + return NSMakeSize(size.width, tile.rect.maxY) + } + } else { + if tiles.isEmpty { + return NSMakeSize(size.width, 200) + } + return size + } + } + + private func deleteItem(at index: Int, animated: Bool) -> GroupCallMainVideoContainerView { + let view = self.views.remove(at: index) + + if animated { + view.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] completion in + if completion { + view?.removeFromSuperview() + } + }) + } else { + view.removeFromSuperview() + } + return view + } + private func insertItem(_ item: TileEntry, at index: Int, prevFrame: NSRect?, prevView: GroupCallMainVideoContainerView?, frame: NSRect, animated: Bool) { + + let view = prevView ?? GroupCallMainVideoContainerView(call: self.call) + view.frame = prevFrame ?? frame + prevView?.layer?.removeAllAnimations() + if index == 0 { + addSubview(view, positioned: .below, relativeTo: self.subviews.first) + } else { + addSubview(view, positioned: .above, relativeTo: self.subviews[index - 1]) + } + + view.updatePeer(peer: item.video, participant: item.member, resizeMode: item.resizeMode, transition: animated && prevView != nil ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, animated: animated, controlsMode: self.controlsMode, isPinned: item.isPinned, isFocused: item.isFocused, isAlone: item.alone, arguments: self.arguments) + view.updateLayout(size: view.frame.size, transition: .immediate) + + self.views.insert(view, at: index) + + if animated && prevFrame == nil { + view.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } + private func updateItem(_ item: TileEntry, at index: Int, prevFrame: NSRect?, animated: Bool) { + if let prevFrame = prevFrame { + self.views[index].frame = prevFrame + } + self.views[index].updatePeer(peer: item.video, participant: item.member, resizeMode: item.resizeMode, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate, animated: animated, controlsMode: self.controlsMode, isPinned: item.isPinned, isFocused: item.isFocused, isAlone: item.alone, arguments: self.arguments) + } + + + override func layout() { + super.layout() + self.updateLayout(size: frame.size, transition: .immediate) + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + + + let tiles:[VoiceChatTile] = tileViews(items.count, isFullscreen: prevState?.isFullScreen ?? false, frameSize: size, pinnedIndex: pinnedIndex) + + + for tile in tiles { + transition.updateFrame(view: views[tile.index], frame: tile.rect) + views[tile.index].updateLayout(size: tile.rect.size, transition: transition) + } + + limitView?.frame = bounds + } + + func makeTemporaryOffset(_ makeRect: (NSRect)->NSRect, pinnedIndex: Int?, size: NSSize) { + let tiles:[VoiceChatTile] = tileViews(items.count, isFullscreen: prevState?.isFullScreen ?? false, frameSize: size, pinnedIndex: pinnedIndex) + + for tile in tiles { + let rect = makeRect(tile.rect) + views[tile.index].frame = rect + views[tile.index].updateLayout(size: rect.size, transition: .immediate) + } + + } + + func updateMode(controlsMode: GroupCallView.ControlsMode, controlsState: GroupCallControlsView.Mode, animated: Bool) { + + for view in views { + view.updateMode(controlsMode: controlsMode, controlsState: controlsState, animated: animated) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + public override var mouseDownCanMoveWindow: Bool { + return true + } +} diff --git a/Telegram-Mac/GroupCallTitleView.swift b/Telegram-Mac/GroupCallTitleView.swift new file mode 100644 index 0000000000..5c543b7fdb --- /dev/null +++ b/Telegram-Mac/GroupCallTitleView.swift @@ -0,0 +1,438 @@ +// +// GroupCallTitleView.swift +// Telegram +// +// Created by Mikhail Filimonov on 06.04.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit +import TelegramCore +import Postbox + +private final class GroupCallSpeakingView : View { + private let animationView: View = View() + private let textView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(animationView) + addSubview(textView) + + textView.userInteractionEnabled = false + textView.isSelectable = false + + let animation = CAKeyframeAnimation(keyPath: "contents") + animationView.layer?.contents = GroupCallTheme.titleSpeakingAnimation.first + animationView.setFrameSize(GroupCallTheme.titleSpeakingAnimation.first!.backingSize) + + animation.values = GroupCallTheme.titleSpeakingAnimation + animation.duration = 0.7 + + animationView.layer?.removeAllAnimations() + animation.repeatCount = .infinity + animation.isRemovedOnCompletion = false + animationView.layer?.add(animation, forKey: "contents") + } + + func update(_ peers:[PeerGroupCallData], maxWidth: CGFloat, animated: Bool) { + let text: String = peers.map { $0.peer.compactDisplayTitle }.joined(separator: ", ") + + let layout = TextViewLayout(.initialize(string: text, color: GroupCallTheme.greenStatusColor, font: .normal(.text)), maximumNumberOfLines: 1) + + layout.measure(width: maxWidth) + textView.update(layout) + + self.change(size: NSMakeSize(animationView.frame.width + textView.frame.width, max(animationView.frame.height, textView.frame.height)), animated: animated) + + needsLayout = true + + } + + override func layout() { + super.layout() + animationView.centerY(x: 0, addition: -3) + textView.centerY(x: animationView.frame.maxX - 2) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private final class GroupCallRecordingView : Control { + private let indicator: View = View() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(indicator) + + indicator.isEventLess = true + + self.set(handler: { [weak self] control in + self?.recordClick?() + }, for: .Click) + + + indicator.backgroundColor = GroupCallTheme.customTheme.redColor + indicator.setFrameSize(NSMakeSize(8, 8)) + indicator.layer?.cornerRadius = indicator.frame.height / 2 + + let animation = CABasicAnimation(keyPath: "opacity") + animation.timingFunction = .init(name: .easeInEaseOut) + animation.fromValue = 0.5 + animation.toValue = 1.0 + animation.duration = 1.0 + animation.autoreverses = true + animation.repeatCount = .infinity + animation.isRemovedOnCompletion = false + animation.fillMode = CAMediaTimingFillMode.forwards + + indicator.layer?.add(animation, forKey: "opacity") + + } + private var recordingStartTime: Int32 = 0 + private var account: Account? + private var recordClick:(()->Void)? = nil + + var updateParentLayout:(()->Void)? = nil + + func update(recordingStartTime: Int32, account: Account, recordClick: (()->Void)?) { + self.account = account + self.recordClick = recordClick + self.recordingStartTime = recordingStartTime + self.backgroundColor = .clear + self.updateParentLayout?() + + setFrameSize(NSMakeSize(8, 8)) + } + + override func layout() { + super.layout() + indicator.center() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + + +final class GroupCallTitleView : Control { + fileprivate let titleView: TextView = TextView() + fileprivate let statusView: DynamicCounterTextView = DynamicCounterTextView() + private var recordingView: GroupCallRecordingView? + private var speakingView: GroupCallSpeakingView? + let pinWindow = ImageButton() + let hidePeers = ImageButton() + private let backgroundView: View = View() + enum Mode { + case normal + case transparent + } + + fileprivate var mode: Mode = .normal + + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(backgroundView) + backgroundView.addSubview(titleView) + backgroundView.addSubview(statusView) + backgroundView.addSubview(pinWindow) + backgroundView.addSubview(hidePeers) + titleView.isSelectable = false + titleView.userInteractionEnabled = false + statusView.userInteractionEnabled = false + titleView.disableBackgroundDrawing = true + + pinWindow.autohighlight = false + pinWindow.scaleOnClick = true + + set(handler: { [weak self] _ in + self?.window?.performZoom(nil) + }, for: .DoubleClick) + } + + override var backgroundColor: NSColor { + didSet { + titleView.backgroundColor = .clear + statusView.backgroundColor = .clear + } + } + + func updateMode(_ mode: Mode, animated: Bool) { + self.mode = mode + backgroundView.backgroundColor = mode == .transparent ? .clear : GroupCallTheme.windowBackground + if animated { + backgroundView.layer?.animateBackground() + } + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + super.draw(layer, in: ctx) + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: NSArray(array: [NSColor.black.withAlphaComponent(0.6).cgColor, NSColor.black.withAlphaComponent(0).cgColor]), locations: nil)! + + ctx.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: layer.bounds.height), options: CGGradientDrawingOptions()) + } + + + override var mouseDownCanMoveWindow: Bool { + return true + } + + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + + + transition.updateFrame(view: backgroundView, frame: NSMakeRect(0, 0, bounds.width, 54)) + + transition.updateFrame(view: statusView, frame: statusView.centerFrameX(y: backgroundView.frame.midY)) + + if let speakingView = speakingView { + transition.updateFrame(view: speakingView, frame: speakingView.centerFrameX(y: backgroundView.frame.midY)) + } + + + var add: CGFloat = 0 + if !self.pinWindow.isHidden { + add += self.pinWindow.frame.width + 5 + } + if !self.hidePeers.isHidden { + add += self.hidePeers.frame.width + 5 + } + + if let recordingView = recordingView { + + let layout = titleView.layout + + layout?.measure(width: backgroundView.frame.width - 125 - recordingView.frame.width - 10 - 30 - add) + titleView.update(layout) + + let rect = backgroundView.focus(titleView.frame.size) + + transition.updateFrame(view: titleView, frame: CGRect(origin: NSMakePoint(max(100, rect.minX), backgroundView.frame.midY - titleView.frame.height), size: titleView.frame.size)) + + transition.updateFrame(view: recordingView, frame: CGRect(origin: NSMakePoint(titleView.frame.maxX + 5, titleView.frame.minY + 6), size: recordingView.frame.size)) + + } else { + + let layout = titleView.layout + layout?.measure(width: backgroundView.frame.width - 125 - add) + titleView.update(layout) + + let rect = backgroundView.focus(titleView.frame.size) + transition.updateFrame(view: titleView, frame: CGRect(origin: NSMakePoint(max(100, rect.minX), backgroundView.frame.midY - titleView.frame.height), size: titleView.frame.size)) + } + + transition.updateFrame(view: pinWindow, frame: pinWindow.centerFrameY(x: frame.width - pinWindow.frame.width - 20)) + transition.updateFrame(view: hidePeers, frame: hidePeers.centerFrameY(x: pinWindow.frame.minX - 10 - hidePeers.frame.width)) + } + + + override func layout() { + super.layout() + updateLayout(size: frame.size, transition: .immediate) + } + + + private var currentState: GroupCallUIState? + private var currentPeer: Peer? + func update(_ peer: Peer, _ state: GroupCallUIState, _ account: Account, recordClick: @escaping()->Void, resizeClick: @escaping()->Void, hidePeersClick: @escaping()->Void, animated: Bool) { + + let oldMode = self.mode + let mode: Mode = .normal + + self.updateMode(mode, animated: animated) + + let title: String = state.title + let oldTitle: String? = currentState?.title + + let titleUpdated = title != oldTitle + + let recordingUpdated = state.state.recordingStartTimestamp != currentState?.state.recordingStartTimestamp + let participantsUpdated = state.summaryState?.participantCount != currentState?.summaryState?.participantCount || state.state.scheduleTimestamp != currentState?.state.scheduleTimestamp + + + let pinned = state.pinnedData.focused?.id ?? state.pinnedData.permanent + + let speaking = state.memberDatas.filter { member in + if state.videoActive(.list).isEmpty { + return false + } + if !state.hideParticipants && state.isFullScreen { + return false + } + if member.isSpeaking && member.peer.id != peer.id { + if pinned == nil { + return member.videoEndpoint == nil && member.presentationEndpoint == nil + } else { + return member.videoEndpoint != pinned && member.presentationEndpoint != pinned + } + } else { + return false + } + } + if (!state.isFullScreen || state.tooltipSpeaker == nil) && !speaking.isEmpty { + let current: GroupCallSpeakingView + var presented = false + if let view = self.speakingView { + current = view + } else { + current = GroupCallSpeakingView(frame: .zero) + self.speakingView = current + addSubview(current) + + presented = true + + if animated { + current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } + current.update(speaking, maxWidth: 200, animated: animated) + if presented { + current.frame = current.centerFrameX(y: backgroundView.frame.midY) + } + } else { + if let speakingView = self.speakingView { + self.speakingView = nil + if animated { + speakingView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak speakingView] _ in + speakingView?.removeFromSuperview() + }) + } else { + speakingView.removeFromSuperview() + } + } + } + + statusView.change(opacity: speakingView == nil ? 1 : 0, animated: animated) + + + let hidePeers = state.hideParticipants + let oldHidePeers = currentState?.hideParticipants == true + + + let hidePeersButtonHide = state.mode != .video || state.activeVideoViews.isEmpty || !state.isFullScreen + + let oldHidePeersButtonHide = currentState?.mode != .video || currentState?.activeVideoViews.isEmpty == true || currentState?.isFullScreen == false + + + let updated = titleUpdated || recordingUpdated || participantsUpdated || mode != oldMode || hidePeers != oldHidePeers || oldHidePeersButtonHide != hidePeersButtonHide + + + guard updated else { + self.currentState = state + self.currentPeer = peer + return + } + + + + self.hidePeers.isHidden = hidePeersButtonHide + self.hidePeers.set(image: hidePeers ? GroupCallTheme.unhide_peers : GroupCallTheme.hide_peers, for: .Normal) + self.hidePeers.sizeToFit() + self.hidePeers.autohighlight = false + self.hidePeers.scaleOnClick = true + + self.hidePeers.removeAllHandlers() + self.hidePeers.set(handler: { _ in + hidePeersClick() + }, for: .Click) + + + if titleUpdated { + let layout = TextViewLayout(.initialize(string: title, color: GroupCallTheme.titleColor, font: .medium(.title)), maximumNumberOfLines: 1) + layout.measure(width: frame.width - 125 - (recordingView != nil ? 80 : 0)) + titleView.update(layout) + } + + if recordingUpdated { + if let recordingStartTimestamp = state.state.recordingStartTimestamp { + let view: GroupCallRecordingView + if let current = self.recordingView { + view = current + } else { + view = GroupCallRecordingView(frame: .zero) + backgroundView.addSubview(view) + self.recordingView = view + updateLayout(size: frame.size, transition: .immediate) + if animated { + recordingView?.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } + view.update(recordingStartTime: recordingStartTimestamp, account: account, recordClick: recordClick) + + view.updateParentLayout = { [weak self] in + self?.needsLayout = true + } + } else { + if let recordingView = recordingView { + self.recordingView = nil + if animated { + recordingView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false,completion: { [weak recordingView] _ in + recordingView?.removeFromSuperview() + }) + } else { + recordingView.removeFromSuperview() + } + } + } + + } + if participantsUpdated || oldMode != mode { + let status: String + let count: Int + if state.state.scheduleTimestamp != nil { + status = L10n.voiceChatTitleScheduledSoon + count = 0 + } else if let summaryState = state.summaryState { + status = L10n.voiceChatStatusMembersCountable(summaryState.participantCount) + count = summaryState.participantCount + } else { + status = L10n.voiceChatStatusLoading + count = 0 + } + + let dynamicResult = DynamicCounterTextView.make(for: status, count: "\(count)", font: .normal(.text), textColor: mode == .transparent ? NSColor.white.withAlphaComponent(0.8) : GroupCallTheme.grayStatusColor, width: frame.width - 140) + + self.statusView.update(dynamicResult, animated: animated && oldMode == mode) + + self.statusView.change(size: dynamicResult.size, animated: animated) + self.statusView.change(pos: NSMakePoint(floorToScreenPixels(backingScaleFactor, (frame.width - dynamicResult.size.width) / 2), frame.midY), animated: animated) + } + self.currentState = state + self.currentPeer = peer + if updated { + needsLayout = true + } + + let windowIsPinned = window?.level == NSWindow.Level.popUpMenu + + pinWindow.set(image: !windowIsPinned ? GroupCallTheme.pin_window : GroupCallTheme.unpin_window, for: .Normal) + pinWindow.sizeToFit() + + pinWindow.removeAllHandlers() + pinWindow.set(handler: { control in + guard let window = control.window as? Window else { + return + } + let windowIsPinned = control.window?.level == NSWindow.Level.popUpMenu + control.window?.level = (windowIsPinned ? NSWindow.Level.normal : NSWindow.Level.popUpMenu) + (control as? ImageButton)?.set(image: windowIsPinned ? GroupCallTheme.pin_window : GroupCallTheme.unpin_window, for: .Normal) + + showModalText(for: window, text: !windowIsPinned ? L10n.voiceChatTooltipPinWindow : L10n.voiceChatTooltipUnpinWindow) + + }, for: .Click) + + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + diff --git a/Telegram-Mac/GroupCallUIState.swift b/Telegram-Mac/GroupCallUIState.swift new file mode 100644 index 0000000000..836beb6dc1 --- /dev/null +++ b/Telegram-Mac/GroupCallUIState.swift @@ -0,0 +1,317 @@ +// +// GroupCallUIState.swift +// Telegram +// +// Created by Mikhail Filimonov on 06.04.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit +import Postbox + +import TelegramCore +import TgVoipWebrtc + +final class GroupCallUIState : Equatable { + + struct RecentActive : Equatable { + let peerId: PeerId + let timestamp: TimeInterval + } + + enum ControlsTooltip : Equatable { + case camera + case micro + } + + enum Mode : Equatable { + case voice + case video + } + struct ActiveVideo : Hashable { + enum Mode : Int { + case main + case list + case profile + } + + static var allModes:[Mode] { + return [.list, .main] + } + + let endpointId: String + let mode: Mode + let index: Int + func hash(into hasher: inout Hasher) { + hasher.combine(endpointId) + hasher.combine(mode.hashValue) + hasher.combine(index) + } + } + + struct VideoSources : Equatable { + static func == (lhs: GroupCallUIState.VideoSources, rhs: GroupCallUIState.VideoSources) -> Bool { + if let lhsVideo = lhs.video, let rhsVideo = rhs.video { + if !lhsVideo.isEqual(rhsVideo) { + return false + } + } else if (lhs.video != nil) != (rhs.video != nil) { + return false + } + if let lhsScreencast = lhs.screencast, let rhsScreencast = rhs.screencast { + if !lhsScreencast.isEqual(rhsScreencast) { + return false + } + } else if (lhs.screencast != nil) != (rhs.screencast != nil) { + return false + } + if lhs.failed != rhs.failed { + return false + } + return true + } + + var video: VideoSourceMac? = nil + var screencast: VideoSourceMac? = nil + + var failed: Bool = false + + var isEmpty: Bool { + return video == nil && screencast == nil + } + } + + struct PinnedData : Equatable { + struct Focused: Equatable { + var id: String + var time: TimeInterval + } + var permanent: String? = nil + var focused: Focused? = nil + var excludePins: Set = Set() + var focusedTime: TimeInterval? + + var isEmpty: Bool { + return permanent != nil || focused != nil + } + } + + let memberDatas:[PeerGroupCallData] + let isMuted: Bool + let state: PresentationGroupCallState + let summaryState: PresentationGroupCallSummaryState? + let peer: Peer + let cachedData: CachedChannelData? + let myAudioLevel: Float + let voiceSettings: VoiceCallSettings + let isWindowVisible: Bool + let dominantSpeaker: DominantVideo? + let pinnedData: PinnedData + let isFullScreen: Bool + let mode: Mode + let videoSources: VideoSources + let version: Int + let activeVideoViews: [ActiveVideo] + let hideParticipants: Bool + let isVideoEnabled: Bool + + let videoJoined: Bool + + let tooltipSpeaker: PeerGroupCallData? + let activeVideoMembers: [GroupCallUIState.ActiveVideo.Mode : [PeerGroupCallData]] + + let controlsTooltip: ControlsTooltip? + let dismissedTooltips: Set + + let myPeer: PeerGroupCallData? + + let visualEffects: Bool + + init(memberDatas: [PeerGroupCallData], state: PresentationGroupCallState, isMuted: Bool, summaryState: PresentationGroupCallSummaryState?, myAudioLevel: Float, peer: Peer, cachedData: CachedChannelData?, voiceSettings: VoiceCallSettings, isWindowVisible: Bool, dominantSpeaker: DominantVideo?, pinnedData: PinnedData, isFullScreen: Bool, mode: Mode, videoSources: VideoSources, version: Int, activeVideoViews: [ActiveVideo], hideParticipants: Bool, isVideoEnabled: Bool, tooltipSpeaker: PeerGroupCallData?, controlsTooltip: ControlsTooltip?, dismissedTooltips: Set, videoJoined: Bool, visualEffects: Bool) { + self.summaryState = summaryState + self.memberDatas = memberDatas + self.peer = peer + self.isMuted = isMuted + self.cachedData = cachedData + self.state = state + self.myAudioLevel = myAudioLevel + self.voiceSettings = voiceSettings + self.isWindowVisible = isWindowVisible + self.dominantSpeaker = dominantSpeaker + self.pinnedData = pinnedData + self.isFullScreen = isFullScreen + self.mode = activeVideoViews.isEmpty ? mode : .video + self.videoSources = videoSources + self.version = version + self.activeVideoViews = activeVideoViews + self.hideParticipants = hideParticipants + self.isVideoEnabled = isVideoEnabled + self.tooltipSpeaker = tooltipSpeaker + self.controlsTooltip = controlsTooltip + self.dismissedTooltips = dismissedTooltips + self.videoJoined = videoJoined + self.myPeer = memberDatas.first(where: { $0.peer.id == $0.accountPeerId }) + self.visualEffects = visualEffects + var modeMembers:[GroupCallUIState.ActiveVideo.Mode : [PeerGroupCallData]] = [:] + + let modes:[GroupCallUIState.ActiveVideo.Mode] = [.list, .main] + + for mode in modes { + var members:[PeerGroupCallData] = [] + for activeVideo in activeVideoViews.filter({ $0.mode == mode }) { + let member = memberDatas.first(where: { peer in + + if let endpoint = peer.videoEndpoint { + if activeVideo.endpointId == endpoint { + return true + } + } + if let endpoint = peer.presentationEndpoint { + if activeVideo.endpointId == endpoint { + return true + } + } + return false + }) + if let member = member, !members.contains(where: { $0.peer.id == member.peer.id }) { + members.append(member) + } + } + modeMembers[mode] = members + } + self.activeVideoMembers = modeMembers + } + + var hasVideo: Bool { + return videoSources.video != nil + } + var hasScreencast: Bool { + return videoSources.screencast != nil + } + + deinit { + + } + + var cantRunVideo: Bool { + if isVideoEnabled { + return false + } + return (!videoJoined) && !memberDatas.filter({ $0.videoEndpointId != nil || $0.presentationEndpointId != nil }).isEmpty + } + + var title: String { + if state.scheduleTimestamp != nil { + return L10n.voiceChatTitleScheduled + } else if let custom = state.title, !custom.isEmpty { + return custom + } else { + return peer.displayTitle + } + } + + static func == (lhs: GroupCallUIState, rhs: GroupCallUIState) -> Bool { + if lhs.memberDatas != rhs.memberDatas { + return false + } + if lhs.state != rhs.state { + return false + } + if lhs.myAudioLevel != rhs.myAudioLevel { + return false + } + if lhs.summaryState != rhs.summaryState { + return false + } + if !lhs.peer.isEqual(rhs.peer) { + return false + } + if lhs.voiceSettings != rhs.voiceSettings { + return false + } + if let lhsCachedData = lhs.cachedData, let rhsCachedData = rhs.cachedData { + if !lhsCachedData.isEqual(to: rhsCachedData) { + return false + } + } else if (lhs.cachedData != nil) != (rhs.cachedData != nil) { + return false + } + if lhs.isWindowVisible != rhs.isWindowVisible { + return false + } + if lhs.dominantSpeaker != rhs.dominantSpeaker { + return false + } + if lhs.pinnedData != rhs.pinnedData { + return false + } + if lhs.isFullScreen != rhs.isFullScreen { + return false + } + if lhs.mode != rhs.mode { + return false + } + if lhs.hasVideo != rhs.hasVideo { + return false + } + if lhs.isMuted != rhs.isMuted { + return false + } + if lhs.videoSources != rhs.videoSources { + return false + } + if lhs.version != rhs.version { + return false + } + if lhs.activeVideoViews != rhs.activeVideoViews { + return false + } + if lhs.hideParticipants != rhs.hideParticipants { + return false + } + if lhs.isVideoEnabled != rhs.isVideoEnabled { + return false + } + if lhs.isMuted != rhs.isMuted { + return false + } + if lhs.activeVideoMembers != rhs.activeVideoMembers { + return false + } + if lhs.tooltipSpeaker != rhs.tooltipSpeaker { + return false + } + if lhs.controlsTooltip != rhs.controlsTooltip { + return false + } + if lhs.dismissedTooltips != rhs.dismissedTooltips { + return false + } + if lhs.videoJoined != rhs.videoJoined { + return false + } + return true + } + + func videoActive(_ mode: ActiveVideo.Mode) -> [PeerGroupCallData] { + return activeVideoMembers[mode] ?? [] + } + + func withUpdatedFullScreen(_ isFullScreen: Bool) -> GroupCallUIState { + return .init(memberDatas: self.memberDatas, state: self.state, isMuted: self.isMuted, summaryState: self.summaryState, myAudioLevel: self.myAudioLevel, peer: self.peer, cachedData: self.cachedData, voiceSettings: self.voiceSettings, isWindowVisible: self.isWindowVisible, dominantSpeaker: self.dominantSpeaker, pinnedData: self.pinnedData, isFullScreen: isFullScreen, mode: self.mode, videoSources: self.videoSources, version: self.version, activeVideoViews: self.activeVideoViews, hideParticipants: self.hideParticipants, isVideoEnabled: self.isVideoEnabled, tooltipSpeaker: self.tooltipSpeaker, controlsTooltip: self.controlsTooltip, dismissedTooltips: self.dismissedTooltips, videoJoined: self.videoJoined, visualEffects: self.visualEffects) + } +} + + +extension GroupCallUIState.ControlsTooltip { + var text: String { + switch self { + case .camera: + return L10n.voiceChatTooltipEnableCamera + case .micro: + return L10n.voiceChatTooltipEnableMicro + } + } +} diff --git a/Telegram-Mac/GroupCallVideoOrientationRowItem.swift b/Telegram-Mac/GroupCallVideoOrientationRowItem.swift new file mode 100644 index 0000000000..aa0875cc41 --- /dev/null +++ b/Telegram-Mac/GroupCallVideoOrientationRowItem.swift @@ -0,0 +1,281 @@ +// +// GroupCallVideoOrientationRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 03.09.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TelegramCore +import TGUIKit +import SwiftSignalKit + +final class GroupCallVideoOrientationRowItem : GeneralRowItem { + + fileprivate let account: Account + fileprivate let select: (GroupCallSettingsState.VideoOrientation)->Void + + fileprivate let selected:GroupCallSettingsState.VideoOrientation + + init(_ initialSize: NSSize, stableId: AnyHashable, viewType: GeneralViewType, account: Account, customTheme: GeneralRowItem.Theme? = nil, selected: GroupCallSettingsState.VideoOrientation, select:@escaping(GroupCallSettingsState.VideoOrientation)->Void) { + self.account = account + self.select = select + self.selected = selected + + super.init(initialSize, height: 143, stableId: stableId, viewType: viewType, customTheme: customTheme) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + + + return true + } + + override func viewClass() -> AnyClass { + return GroupCallVideoOrientationRowView.self + } +} + + +private final class GroupCallVideoOrientationRowView : GeneralContainableRowView { + + private final class OrientationView : Control { + + private let images: View = View() + private let isVertical: Bool + required init(frame frameRect: NSRect, isVertical: Bool) { + self.isVertical = isVertical + super.init(frame: frameRect) + addSubview(images) + images.isEventLess = true + backgroundColor = GroupCallTheme.windowBackground + + + self.layer?.cornerRadius = 4 + + set(handler: { [weak self] _ in + self?.updateColors() + }, for: .Normal) + + set(handler: { [weak self] _ in + self?.updateColors() + }, for: .Highlight) + } + + func updateColors() { + if isSelected || controlState == .Highlight { + self.layer?.borderWidth = 2 + self.layer?.borderColor = GroupCallTheme.purple.cgColor + } else { + self.layer?.borderWidth = 0 + self.layer?.borderColor = .clear + } + } + + override var isSelected: Bool { + didSet { + needsLayout = true + updateColors() + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + + + override func layout() { + super.layout() + + images.removeAllSubviews() + + let top = ImageView() + top.image = NSImage(named: "Icon_GroupCall_Record_Avatar1")?._cgImage + top.isEventLess = true + top.layer?.cornerRadius = 2 + + images.addSubview(top) + + top.sizeToFit() + + + if isVertical { + images.setFrameSize(NSMakeSize(60, 81)) + } else { + images.setFrameSize(NSMakeSize(80, 60)) + } + + + var list:[ImageView] = [] + + for i in 2 ... 4 { + let imageView = ImageView() + imageView.isEventLess = true + imageView.image = NSImage(named: "Icon_GroupCall_Record_Avatar\(i)")?._cgImage + imageView.setFrameSize(NSMakeSize(19, 19)) + imageView.layer?.cornerRadius = 2 + + images.addSubview(imageView) + list.append(imageView) + } + + top.setFrameOrigin(NSMakePoint(0, 0)) + + if isVertical { + for (i, image) in list.enumerated() { + image.setFrameOrigin(NSMakePoint(CGFloat(i) * (image.frame.width + 1), 61)) + } + } else { + for (i, image) in list.enumerated() { + image.setFrameOrigin(NSMakePoint(top.frame.maxX + 1, CGFloat(i) * (image.frame.height + 1))) + } + } + + images.center() + + } + } + + + + private let vertical = OrientationView(frame: NSMakeRect(0, 0, 120, 100), isVertical: true) + private let horizontal = OrientationView(frame: NSMakeRect(0, 0, 120, 100), isVertical: false) + + private let verticalTextView = TextView() + private let horizontalTextView = TextView() + + private let container = View() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(container) + container.addSubview(vertical) + container.addSubview(horizontal) + container.addSubview(verticalTextView) + container.addSubview(horizontalTextView) + vertical.layout() + horizontal.layout() + + containerView.set(handler: { [weak self] control in + self?.updateColors() + }, for: .Highlight) + + containerView.set(handler: { [weak self] control in + self?.updateColors() + }, for: .Normal) + + containerView.set(handler: { [weak self] control in + self?.updateColors() + }, for: .Hover) + + vertical.set(handler: { [weak self] _ in + self?.select(.portrait) + }, for: .Click) + + horizontal.set(handler: { [weak self] _ in + self?.select(.landscape) + }, for: .Click) + + } + + private func select(_ value: GroupCallSettingsState.VideoOrientation) { + guard let item = item as? GroupCallVideoOrientationRowItem else { + return + } + item.select(value) + } + + override func updateColors() { + super.updateColors() + containerView.backgroundColor = containerView.controlState != .Highlight ? backdorColor : highlightColor + borderView.backgroundColor = borderColor + } + + + var highlightColor: NSColor { + if let item = item as? GeneralRowItem, let theme = item.customTheme { + return theme.highlightColor + } + return theme.colors.grayHighlight + } + override var backdorColor: NSColor { + if let item = item as? GeneralRowItem, let theme = item.customTheme { + return theme.backgroundColor + } + return super.backdorColor + } + + var textColor: NSColor { + if let item = item as? GeneralRowItem, let theme = item.customTheme { + return theme.textColor + } + return theme.colors.text + } + var secondaryColor: NSColor { + if let item = item as? GeneralRowItem, let theme = item.customTheme { + return theme.grayTextColor + } + return theme.colors.grayText + } + + override var borderColor: NSColor { + if let item = item as? GeneralRowItem, let theme = item.customTheme { + return theme.borderColor + } + return theme.colors.border + } + + override func layout() { + super.layout() + + guard let item = item as? GroupCallVideoOrientationRowItem else { + return + } + + container.setFrameSize(NSMakeSize(vertical.frame.width + horizontal.frame.width + 20, 120)) + container.centerX(y: item.viewType.innerInset.top) + + vertical.setFrameOrigin(.zero) + horizontal.setFrameOrigin(NSMakePoint(vertical.frame.maxX + 20, 0)) + + + let verticalLayout = TextViewLayout(.initialize(string: "Portrait", color: item.selected == .portrait ? GroupCallTheme.purple : GroupCallTheme.customTheme.grayTextColor, font: .medium(.text))) + + let horizontalLayout = TextViewLayout(.initialize(string: "Landscape", color: item.selected == .landscape ? GroupCallTheme.purple : GroupCallTheme.customTheme.grayTextColor, font: .medium(.text))) + + + verticalLayout.measure(width: vertical.frame.width) + verticalTextView.update(verticalLayout) + + horizontalLayout.measure(width: horizontal.frame.width) + horizontalTextView.update(horizontalLayout) + + verticalTextView.setFrameOrigin(NSMakePoint((vertical.frame.width - verticalTextView.frame.width) / 2, vertical.frame.maxY + 5)) + + horizontalTextView.setFrameOrigin(NSMakePoint(horizontal.frame.minX + (horizontal.frame.width - horizontalTextView.frame.width) / 2, horizontal.frame.maxY + 5)) + + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? GroupCallVideoOrientationRowItem else { + return + } + + vertical.isSelected = item.selected == .portrait + horizontal.isSelected = item.selected == .landscape + + needsLayout = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/GroupCallView.swift b/Telegram-Mac/GroupCallView.swift new file mode 100644 index 0000000000..ebdada0c66 --- /dev/null +++ b/Telegram-Mac/GroupCallView.swift @@ -0,0 +1,711 @@ +// +// GroupCallView.swift +// Telegram +// +// Created by Mikhail Filimonov on 06.04.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit +import TelegramCore +import Postbox + + + +final class GroupCallView : View { + + enum ControlsMode { + case normal + case invisible + } + + private var controlsMode: ControlsMode = .normal + + let peersTable: TableView = TableView(frame: NSMakeRect(0, 0, 340, 329)) + + let titleView: GroupCallTitleView = GroupCallTitleView(frame: NSMakeRect(0, 0, 380, 54)) + private let peersTableContainer: View = View(frame: NSMakeRect(0, 0, 340, 329)) + private let controlsContainer = GroupCallControlsView(frame: .init(x: 0, y: 0, width: 360, height: 320)) + + private var scheduleView: GroupCallScheduleView? + private(set) var tileView: GroupCallTileView? + + private var scrollView = ScrollView() + + private var speakingTooltipView: GroupCallSpeakingTooltipView? + + var arguments: GroupCallUIArguments? { + didSet { + controlsContainer.arguments = arguments + } + } + + private final class Content : View { + + var state: GroupCallUIState? { + didSet { + needsDisplay = true + } + } + + override func layout() { + super.layout() + needsDisplay = true + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + ctx.setFillColor(GroupCallTheme.windowBackground.cgColor) + ctx.fill(bounds) + var rect: CGRect = .zero + if let state = self.state { + switch state.mode { + case .video: + if state.videoActive(.main).isEmpty || !state.isFullScreen { + rect = NSMakeRect(0, 54, min(frame.width - 40, 600), frame.height - 180) + rect.origin.x = focus(rect.size).minX + } else { + rect = NSMakeRect(5, 54, frame.width - 10, frame.height - 5 - 54) + } + case .voice: + rect = NSMakeRect(0, 54, min(frame.width - 40, 600), frame.height - 271) + rect.origin.x = focus(rect.size).minX + } + if rect != .zero { + let path = CGMutablePath() + path.addRoundedRect(in: rect, cornerWidth: 10, cornerHeight: 10) + ctx.addPath(path) + ctx.clip() + ctx.clear(rect) + } + } + } + } + + private let content = Content() + + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(scrollView) + + addSubview(peersTableContainer) + addSubview(peersTable) + + addSubview(content) + + addSubview(controlsContainer) + addSubview(titleView) + + content.isEventLess = true + + scrollView.clipView._mouseDownCanMoveWindow = true + scrollView._mouseDownCanMoveWindow = true + + scrollView.background = .clear + scrollView.layer?.cornerRadius = 10 + peersTableContainer.layer?.cornerRadius = 10 + updateLocalizationAndTheme(theme: theme) + + peersTable._mouseDownCanMoveWindow = true + + peersTable.getBackgroundColor = { + .clear + } + peersTable.addScroll(listener: TableScrollListener(dispatchWhenVisibleRangeUpdated: false, { [weak self] pos in + guard let `self` = self else { + return + } + self.peersTableContainer.frame = self.substrateRect() + })) + + + peersTable.applyExternalScroll = { [weak self] event in + guard let strongSelf = self, !strongSelf.isFullScreen, let state = strongSelf.state else { + return false + } + + if state.videoActive(.main).isEmpty { + return false + } + if state.pinnedData.focused != nil || state.pinnedData.permanent != nil { + return false + } + + let tileHeight = strongSelf.tileView?.frame.height ?? 0 + + if tileHeight <= 280 { + return false + } + if state.videoActive(.main).count == 1 { + return false + } + if strongSelf.peersTable.documentOffset.y > 0 { + return false + } + if strongSelf.peersTable.listHeight + strongSelf.videoRect.height < strongSelf.frame.height - 180 { + return false + } + + + let local = strongSelf.scrollTempOffset + + var scrollTempOffset = local + + + + scrollTempOffset += -event.scrollingDeltaY + + + strongSelf.scrollTempOffset = min(max(0, scrollTempOffset), strongSelf.videoRect.height + 5) + + strongSelf.updateLayout(size: strongSelf.frame.size, transition: .immediate) + + if strongSelf.tableRect.minY == strongSelf.titleView.frame.maxY { + return false + } + + return true + } + + scrollView.applyExternalScroll = { [weak self] event in + + guard let strongSelf = self, let state = strongSelf.state, let tileView = strongSelf.tileView else { + return false + } + + if !state.isFullScreen { + return strongSelf.peersTable.applyExternalScroll?(event) ?? false + } + + let local = strongSelf.scrollTempOffset + + var scrollTempOffset = local + + scrollTempOffset += -event.scrollingDeltaY + + strongSelf.scrollTempOffset = min(max(0, scrollTempOffset), -(strongSelf.frame.height - tileView.frame.height - strongSelf.titleView.frame.height - 5)) + + strongSelf.updateLayout(size: strongSelf.frame.size, transition: .immediate) + + + return true + } + + + updateLayout(size: frame.size, transition: .immediate) + + +// NotificationCenter.default.addObserver(forName: NSView.boundsDidChangeNotification, object: scrollView.contentView, queue: OperationQueue.main, using: { [weak self] notification in +// let bounds = self?.scrollView.contentView.bounds +// if bounds?.minY == 0 { +// var bp = 0 +// bp += 1 +// } +// NSLog("bounds: \(bounds)") +// }) + } + + private func substrateRect() -> NSRect { + var h = self.peersTable.listHeight + if peersTable.documentOffset.y < 0 { + h -= peersTable.documentOffset.y + } + var isVertical: Bool? = nil + var offset: CGFloat = 0 + peersTable.enumerateItems(with: { item in + + if let item = item as? GroupCallParticipantRowItem { + isVertical = item.isVertical + } + if isVertical == true { + offset += item.height + } + if let item = item as? GroupCallTileRowItem { + offset += item.height + } + return isVertical == nil || isVertical == true + }) + h = min(h, self.peersTable.frame.height) + h -= offset + if h < 0 { + offset = -h + h = 0 + } + + let point = tableRect.origin + NSMakePoint(0, offset) + return .init(origin: point, size: NSMakeSize(self.peersTable.frame.width, h)) + + } + + override var mouseDownCanMoveWindow: Bool { + return true + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + peersTableContainer.backgroundColor = GroupCallTheme.membersColor + backgroundColor = GroupCallTheme.windowBackground + titleView.backgroundColor = .clear + } + + func updateMouse(animated: Bool, isReal: Bool) { + guard let window = self.window else { + return + } + let location = self.convert(window.mouseLocationOutsideOfEventStream, from: nil) + + var mode: ControlsMode + let videoView = self.tileView + if let videoView = videoView { + if NSPointInRect(location, videoView.frame) && mouseInside() { + if isReal { + mode = .normal + } else { + mode = self.controlsMode + } + } else { + mode = .invisible + } + } else { + mode = .normal + } + + if state?.state.networkState == .connecting { + mode = .normal + } + + let previousMode = self.controlsMode + self.controlsMode = mode + + + // if previousMode != mode { + controlsContainer.change(opacity: mode == .invisible && isFullScreen && state?.controlsTooltip == nil ? 0 : 1, animated: animated) + tileView?.updateMode(controlsMode: mode, controlsState: controlsContainer.mode, animated: animated) + + /// } + } + + func idleHide() { + + guard let window = self.window else { + return + } + let location = window.mouseLocationOutsideOfEventStream + + let frame = controlsContainer.convert(controlsContainer.fullscreenBackgroundView.frame, to: nil) + + + + let hasVideo = tileView != nil + let mode: ControlsMode = hasVideo && isFullScreen && !NSPointInRect(location, frame) ? .invisible :.normal + let previousMode = self.controlsMode + self.controlsMode = mode + + controlsContainer.change(opacity: mode == .invisible && isFullScreen && state?.controlsTooltip == nil ? 0 : 1, animated: true) + + + + var videosMode: ControlsMode + if !isFullScreen { + if NSPointInRect(location, frame) && mouseInside() { + videosMode = .normal + } else { + videosMode = .invisible + } + } else { + videosMode = mode + } + + self.controlsMode = videosMode + + tileView?.updateMode(controlsMode: videosMode, controlsState: controlsContainer.mode, animated: true) + + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + + + let hasVideo = isFullScreen && (self.tileView != nil) + + let isVideo = state?.mode == .video + + + peersTableContainer.setFrameSize(NSMakeSize(substrateRect().width, peersTableContainer.frame.height)) + peersTable.setFrameSize(NSMakeSize(tableRect.width, peersTable.frame.height)) + + transition.updateFrame(view: peersTable, frame: tableRect) + + transition.updateFrame(view: peersTableContainer, frame: substrateRect()) + if hasVideo { + if isFullScreen, state?.hideParticipants == true { + transition.updateFrame(view: controlsContainer, frame: controlsContainer.centerFrameX(y: frame.height - controlsContainer.frame.height + 75)) + } else { + transition.updateFrame(view: controlsContainer, frame: controlsContainer.centerFrameX(y: frame.height - controlsContainer.frame.height + 75, addition: -peersTable.frame.width / 2)) + } + } else { + if isVideo { + transition.updateFrame(view: controlsContainer, frame: controlsContainer.centerFrameX(y: frame.height - controlsContainer.frame.height + 100)) + } else { + transition.updateFrame(view: controlsContainer, frame: controlsContainer.centerFrameX(y: frame.height - controlsContainer.frame.height + 50)) + } + } + + let titleRect = NSMakeRect(0, 0, frame.width, 54) + transition.updateFrame(view: titleView, frame: titleRect) + titleView.updateLayout(size: titleRect.size, transition: transition) + + controlsContainer.updateLayout(size: controlsContainer.frame.size, transition: transition) + + if let tileView = self.tileView { + let size = tileView.getSize(videoRect.size) + var rect = videoRect + if tileView.superview != self { + rect = size.bounds + } + transition.updateFrame(view: tileView, frame: rect) + tileView.updateLayout(size: rect.size, transition: transition) + + } + let clipRect = videoRect.size.bounds + var scrollRect = videoRect + scrollRect.size.height += 5 + transition.updateFrame(view: scrollView.contentView, frame: clipRect) + transition.updateFrame(view: scrollView, frame: scrollRect) + + transition.updateFrame(view: content, frame: bounds) + + + if let scheduleView = self.scheduleView { + let rect = tableRect + transition.updateFrame(view: scheduleView, frame: rect) + scheduleView.updateLayout(size: rect.size, transition: transition) + } + + if let current = speakingTooltipView { + let hasTable = isFullScreen && state?.hideParticipants == false + transition.updateFrame(view: current, frame: current.centerFrameX(y: 60, addition: hasTable ? (-peersTable.frame.width / 2) : 0)) + } + } + + + + private var tableRect: NSRect { + var size = peersTable.frame.size + let width = min(frame.width - 40, 600) + + if let state = state { + if !state.videoActive(.main).isEmpty { + if isFullScreen { + size = NSMakeSize(GroupCallTheme.tileTableWidth, frame.height - 54 - 5) + } else { + var videoHeight = max(160, frame.height - 180 - 200) + videoHeight -= (self.scrollTempOffset) + size = NSMakeSize(width, frame.height - 180 - max(0, videoHeight) - 5) + } + } else { + switch state.mode { + case .voice: + size = NSMakeSize(width, frame.height - 271) + case .video: + size = NSMakeSize(width, frame.height - 180) + } + } + + } + var rect = focus(size) + rect.origin.y = 54 + + if let state = state, !state.videoActive(.main).isEmpty { + if !isFullScreen { + rect.origin.y = videoRect.maxY + 5 + } else { + rect.origin.x = frame.width - size.width - 5 + rect.origin.y = 54 + + if state.hideParticipants { + rect.origin.x = (frame.width + 5) + } + } + } + + return rect + } + + override func setFrameSize(_ newSize: NSSize) { + let prevFullScreen = self.isFullScreen + super.setFrameSize(newSize) + + if prevFullScreen != self.isFullScreen, let state = self.state { + updateUIAfterFullScreenUpdated(state, reloadTable: false) + } + } + + override func layout() { + super.layout() + updateLayout(size: frame.size, transition: .immediate) + } + + var isFullScreen: Bool { + if let state = state { + return state.isFullScreen + } + if frame.width >= GroupCallTheme.fullScreenThreshold { + return true + } + return false + } + + var videoRect: NSRect { + var rect: CGRect + if isFullScreen, let state = state { + let tableWidth: CGFloat + tableWidth = (GroupCallTheme.tileTableWidth + 20) + + if state.hideParticipants, isFullScreen { + let width = frame.width - 10 + var height = frame.height - 5 - 54 + if let tileView = tileView { + height = tileView.getSize(NSMakeSize(width, height)).height + } + rect = CGRect(origin: .init(x: 5, y: 54 - scrollTempOffset), size: .init(width: width, height: height)) + } else { + let width = frame.width - tableWidth + 5 + var height = frame.height - 5 - 54 + if let tileView = tileView { + height = tileView.getSize(NSMakeSize(width, height)).height + } + rect = CGRect(origin: .init(x: 5, y: 54 - scrollTempOffset), size: .init(width: width, height: height)) + } + + } else { + let width = min(frame.width - 40, 600) + + var height = max(200, frame.height - 180 - 200) + if let tileView = tileView { + height = tileView.getSize(NSMakeSize(width, height)).height + } + rect = focus(NSMakeSize(width, height)) + rect.origin.y = 54 - scrollTempOffset + } + return rect + } + + var state: GroupCallUIState? + + var markWasScheduled: Bool? = false + + private var _scrollTempOffset: CGFloat = 0 + private var scrollTempOffset: CGFloat { + get { + if let state = state { + if state.pinnedData.isEmpty { + return 0 + } else { + return _scrollTempOffset + } + } + return 0 + } + set { + _scrollTempOffset = newValue + } + } + + private var saveScrollInset: (GroupCallTileView.Transition, CGPoint)? + + func applyUpdates(_ state: GroupCallUIState, _ tableTransition: TableUpdateTransition, _ call: PresentationGroupCall, animated: Bool) { + + let duration: Double = 0.3 + + let previousState = self.state + + if previousState?.isFullScreen != state.isFullScreen { + self.scrollTempOffset = 0 + } + + if let previousState = previousState { + if let markWasScheduled = self.markWasScheduled, !state.state.canManageCall { + if !markWasScheduled { + self.markWasScheduled = previousState.state.scheduleState != nil && state.state.scheduleState == nil + } + if self.markWasScheduled == true { + switch state.state.networkState { + case .connecting: + return + default: + self.markWasScheduled = nil + } + } + + } + } + self.state = state + titleView.update(state.peer, state, call.account, recordClick: { [weak self, weak state] in + if let state = state { + self?.arguments?.recordClick(state.state) + } + }, resizeClick: { [weak self] in + self?.arguments?.toggleScreenMode() + }, hidePeersClick: { [weak self] in + self?.arguments?.togglePeersHidden() + } , animated: animated) + controlsContainer.update(state, voiceSettings: state.voiceSettings, audioLevel: state.myAudioLevel, animated: animated) + + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: duration, curve: .easeInOut) : .immediate + + if let _ = state.state.scheduleTimestamp { + let current: GroupCallScheduleView + if let view = self.scheduleView { + current = view + } else { + current = GroupCallScheduleView(frame: tableRect) + self.scheduleView = current + addSubview(current) + } + } else { + if let view = self.scheduleView { + self.scheduleView = nil + if animated { + view.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + view.layer?.animateScaleSpring(from: 1, to: 0.1, duration: 0.3) + } else { + view.removeFromSuperview() + } + } + } + scheduleView?.update(state, arguments: arguments, animated: animated) + + if animated { + let from: CGFloat = state.state.scheduleTimestamp != nil ? 1 : 0 + let to: CGFloat = state.state.scheduleTimestamp != nil ? 0 : 1 + if previousState?.state.scheduleTimestamp != state.state.scheduleTimestamp { + let remove: Bool = state.state.scheduleTimestamp != nil + if !remove { + self.addSubview(peersTableContainer, positioned: .below, relativeTo: content) + self.addSubview(peersTable, positioned: .below, relativeTo: content) + } + self.peersTable.layer?.animateAlpha(from: from, to: to, duration: duration, removeOnCompletion: false, completion: { [weak self] _ in + if remove { + self?.peersTable.removeFromSuperview() + self?.peersTableContainer.removeFromSuperview() + } + }) + } + } else { + if state.state.scheduleState != nil { + peersTable.removeFromSuperview() + peersTableContainer.removeFromSuperview() + } else if peersTable.superview == nil { + addSubview(peersTableContainer, positioned: .below, relativeTo: content) + addSubview(peersTable, positioned: .below, relativeTo: content) + } + } + + if !state.videoActive(.main).isEmpty { + let current: GroupCallTileView + if let tileView = self.tileView { + current = tileView + } else { + current = GroupCallTileView(call: call, arguments: arguments, frame: videoRect.size.bounds) + self.tileView = current + if animated { + current.layer?.animateAlpha(from: 0, to: 1, duration: duration) + } + } + + let _ = current.update(state: state, context: call.accountContext, transition: transition, size: videoRect.size, animated: animated, controlsMode: self.controlsMode) + + self.addSubview(current, positioned: .below, relativeTo: content) + } else { + if let tileView = self.tileView { + self.tileView = nil + if animated { + tileView.layer?.animateAlpha(from: 1, to: 0, duration: duration, removeOnCompletion: false, completion: { [weak tileView] _ in + tileView?.removeFromSuperview() + }) + } else { + tileView.removeFromSuperview() + } + } + } + scrollView.isHidden = self.tileView == nil + + + + if previousState?.tooltipSpeaker != state.tooltipSpeaker { + + if let current = self.speakingTooltipView { + self.speakingTooltipView = nil + if animated { + current.layer?.animateAlpha(from: 1, to: 0, duration: duration, removeOnCompletion: false, completion: { [weak current] _ in + current?.removeFromSuperview() + }) + current.layer?.animatePosition(from: current.frame.origin, to: current.frame.origin - NSMakePoint(0, 10), removeOnCompletion: false) + } else { + current.removeFromSuperview() + } + } + if let tooltipSpeaker = state.tooltipSpeaker { + let current: GroupCallSpeakingTooltipView + var presented = false + if let speakingTooltipView = self.speakingTooltipView { + current = speakingTooltipView + } else { + current = GroupCallSpeakingTooltipView(frame: .zero) + self.speakingTooltipView = current + addSubview(current) + if animated { + current.layer?.animateAlpha(from: 0, to: 1, duration: duration) + } + presented = true + + current.set(handler: { [weak self] _ in + if tooltipSpeaker.hasVideo { + self?.arguments?.focusVideo(tooltipSpeaker.videoEndpoint ?? tooltipSpeaker.presentationEndpoint) + } + }, for: .Click) + } + current.setPeer(data: tooltipSpeaker, account: call.account, audioLevel: arguments?.audioLevel ?? { _ in return nil }) + + if presented { + let hasTable = isFullScreen && state.hideParticipants == false + current.setFrameOrigin(current.centerFrameX(y: 60, addition: hasTable ? (-peersTable.frame.width / 2) : 0).origin) + if animated { + current.layer?.animatePosition(from: current.frame.origin - NSMakePoint(0, 10), to: current.frame.origin) + } + } + } + } + + content.state = state + + CATransaction.begin() + if !tableTransition.isEmpty { + peersTable.merge(with: tableTransition) + } + CATransaction.commit() + + updateLayout(size: frame.size, transition: transition) + updateUIAfterFullScreenUpdated(state, reloadTable: false) + + } + + var isVertical: Bool { + return isFullScreen && state?.dominantSpeaker != nil + } + + private func updateUIAfterFullScreenUpdated(_ state: GroupCallUIState, reloadTable: Bool) { + + peersTable.layer?.cornerRadius = isVertical ? 0 : 10 + + updateMouse(animated: false, isReal: false) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/GroupCallWindow.swift b/Telegram-Mac/GroupCallWindow.swift new file mode 100644 index 0000000000..0484cdecaa --- /dev/null +++ b/Telegram-Mac/GroupCallWindow.swift @@ -0,0 +1,364 @@ +// +// GroupCallWindow.swift +// Telegram +// +// Created by Mikhail Filimonov on 22/11/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit + +private func generatePeerControl(_ icon: CGImage, background: NSColor) -> CGImage { + return generateImage(NSMakeSize(28, 28), contextGenerator: { size, ctx in + let rect: NSRect = .init(origin: .zero, size: size) + ctx.clear(rect) + + ctx.round(size, 4) + ctx.setFillColor(background.cgColor) + ctx.fill(rect) + + ctx.draw(icon, in: rect.focus(icon.backingSize)) + })! +} + +struct GroupCallTheme { + static let membersColor = NSColor(hexString: "#333333")! + static let windowBackground = NSColor(hexString: "#212121")! + static let grayStatusColor = NSColor(srgbRed: 133 / 255, green: 133 / 255, blue: 133 / 255, alpha: 1) + static let blueStatusColor = NSColor(srgbRed: 38 / 255, green: 122 / 255, blue: 255 / 255, alpha: 1) + static let greenStatusColor = NSColor(hexString: "#34C759")! + static let memberSeparatorColor = NSColor(srgbRed: 58 / 255, green: 58 / 255, blue: 58 / 255, alpha: 1) + static let speakActiveColor = NSColor(hexString: "#34C759")! + static let speakInactiveColor = NSColor(srgbRed: 38 / 255, green: 122 / 255, blue: 255 / 255, alpha: 1) + static let speakLockedColor = NSColor(hexString: "#FF5257")! + static let speakDisabledColor = NSColor(hexString: "#333333")! + static let titleColor = NSColor.white + static let declineColor = NSColor(hexString: "#FF3B30")!.withAlphaComponent(0.3) + static let settingsColor = NSColor(hexString: "#333333")! + + + static let purple = NSColor(rgb: 0x3252ef) + static let pink = NSColor(rgb: 0xef436c) + + + static var accent: NSColor { + return speakInactiveColor + } + static var secondary: NSColor { + return grayStatusColor + } + + static let titleSpeakingAnimation = recordVoiceActivityAnimation(GroupCallTheme.greenStatusColor) + + static let video_status_muted_red = NSImage(named: "Icon_GroupCall_VideoBox_Muted")!.precomposed(GroupCallTheme.speakLockedColor) + static let video_status_muted_accent = NSImage(named: "Icon_GroupCall_VideoBox_Muted")!.precomposed(GroupCallTheme.greenStatusColor) + static let video_status_muted_gray = NSImage(named: "Icon_GroupCall_VideoBox_Muted")!.precomposed(GroupCallTheme.grayStatusColor) + + static let video_status_unmuted_green = NSImage(named: "Icon_GroupCall_VideoBox_Unmuted")!.precomposed(GroupCallTheme.greenStatusColor) + static let video_status_unmuted_gray = NSImage(named: "Icon_GroupCall_VideoBox_Unmuted")!.precomposed(GroupCallTheme.grayStatusColor) + static let video_status_unmuted_accent = NSImage(named: "Icon_GroupCall_VideoBox_Unmuted")!.precomposed(GroupCallTheme.accent) + + static let video_back = NSImage(named: "Icon_ChatNavigationBack")!.precomposed(NSColor.white) + + static let video_paused = NSImage(named: "Icon_VoiceChat_PausedVideo")!.precomposed(NSColor.white) + + static let videoBox_muted = NSImage(named: "Icon_GroupCall_VideoBox_Muted")!.precomposed(NSColor.white.withAlphaComponent(0.8)) + static let videoBox_unmuted = NSImage(named: "Icon_GroupCall_VideoBox_Unmuted")!.precomposed(NSColor.white.withAlphaComponent(0.8)) + static let videoBox_speaking = NSImage(named: "Icon_GroupCall_VideoBox_Unmuted")!.precomposed(GroupCallTheme.greenStatusColor.withAlphaComponent(0.8)) + + static let videoBox_muted_locked = NSImage(named: "Icon_GroupCall_VideoBox_Muted")!.precomposed(GroupCallTheme.grayStatusColor) + static let videoBox_unmuted_locked = NSImage(named: "Icon_GroupCall_VideoBox_Unmuted")!.precomposed(GroupCallTheme.grayStatusColor) + + + static let videoBox_video = NSImage(named: "Icon_GroupCall_Status_Video")!.precomposed(NSColor.white.withAlphaComponent(0.8)) + static let videoBox_screencast = NSImage(named: "Icon_GroupCall_Status_Screencast")!.precomposed(NSColor.white.withAlphaComponent(0.8)) + + + static let closeTooltip = NSImage(named: "Icon_VoiceChat_Tooltip_Close")!.precomposed(.white) + + static let settingsIcon = NSImage(named: "Icon_GroupCall_Settings")!.precomposed(.white) + static let declineIcon = NSImage(named: "Icon_GroupCall_Decline")!.precomposed(.white) + static let inviteIcon = NSImage(named: "Icon_GroupCall_Invite")!.precomposed(.white) + static let invitedIcon = NSImage(named: "Icon_GroupCall_Invited")!.precomposed(GroupCallTheme.grayStatusColor) + + + + + static let small_speaking = generatePeerControl(NSImage(named: "Icon_GroupCall_Small_Unmuted")!.precomposed(GroupCallTheme.greenStatusColor), background: .clear) + static let small_unmuted = generatePeerControl(NSImage(named: "Icon_GroupCall_Small_Unmuted")!.precomposed(GroupCallTheme.grayStatusColor), background: .clear) + static let small_muted = generatePeerControl(NSImage(named: "Icon_GroupCall_Small_Muted")!.precomposed(GroupCallTheme.grayStatusColor), background: .clear) + static let small_muted_locked = generatePeerControl(NSImage(named: "Icon_GroupCall_Small_Muted")!.precomposed(GroupCallTheme.speakLockedColor), background: .clear) + + static let small_speaking_active = generatePeerControl(NSImage(named: "Icon_GroupCall_Small_Unmuted")!.precomposed(GroupCallTheme.greenStatusColor), background: GroupCallTheme.windowBackground.withAlphaComponent(0.3)) + static let small_unmuted_active = generatePeerControl(NSImage(named: "Icon_GroupCall_Small_Unmuted")!.precomposed(GroupCallTheme.grayStatusColor), background: GroupCallTheme.windowBackground.withAlphaComponent(0.3)) + static let small_muted_active = generatePeerControl(NSImage(named: "Icon_GroupCall_Small_Muted")!.precomposed(GroupCallTheme.grayStatusColor), background: GroupCallTheme.windowBackground.withAlphaComponent(0.3)) + static let small_muted_locked_active = generatePeerControl(NSImage(named: "Icon_GroupCall_Small_Muted")!.precomposed(GroupCallTheme.speakLockedColor), background: GroupCallTheme.windowBackground.withAlphaComponent(0.3)) + + + static let small_raised_hand = generatePeerControl(NSImage(named: "Icon_GroupCall_RaiseHand_Small")!.precomposed(GroupCallTheme.customTheme.accentColor), background: .clear) + static let small_raised_hand_active = generatePeerControl(NSImage(named: "Icon_GroupCall_RaiseHand_Small")!.precomposed(GroupCallTheme.customTheme.accentColor), background: GroupCallTheme.windowBackground.withAlphaComponent(0.3)) + + + + + + static let status_video_gray = NSImage(named: "Icon_GroupCall_Status_Video")!.precomposed(GroupCallTheme.grayStatusColor) + static let status_video_accent = NSImage(named: "Icon_GroupCall_Status_Video")!.precomposed(GroupCallTheme.blueStatusColor) + static let status_video_green = NSImage(named: "Icon_GroupCall_Status_Video")!.precomposed(GroupCallTheme.greenStatusColor) + static let status_video_red = NSImage(named: "Icon_GroupCall_Status_Video")!.precomposed(GroupCallTheme.speakLockedColor) + + + static let status_screencast_gray = NSImage(named: "Icon_GroupCall_Status_Screencast")!.precomposed(GroupCallTheme.grayStatusColor) + static let status_screencast_accent = NSImage(named: "Icon_GroupCall_Status_Screencast")!.precomposed(GroupCallTheme.blueStatusColor) + static let status_screencast_green = NSImage(named: "Icon_GroupCall_Status_Screencast")!.precomposed(GroupCallTheme.greenStatusColor) + static let status_screencast_red = NSImage(named: "Icon_GroupCall_Status_Screencast")!.precomposed(GroupCallTheme.speakLockedColor) + + + + static let status_muted = NSImage(named: "Icon_GroupCall_Status_Muted")!.precomposed(GroupCallTheme.grayStatusColor) + + static let status_muted_red = NSImage(named: "Icon_GroupCall_Status_Muted")!.precomposed(GroupCallTheme.speakLockedColor) + + + static let status_unmuted_accent = NSImage(named: "Icon_GroupCall_Status_Unmuted")!.precomposed(GroupCallTheme.blueStatusColor) + static let status_unmuted_green = NSImage(named: "Icon_GroupCall_Status_Unmuted")!.precomposed(GroupCallTheme.greenStatusColor) + static let status_unmuted_gray = NSImage(named: "Icon_GroupCall_Status_Unmuted")!.precomposed(GroupCallTheme.grayStatusColor) + + + static let video_limit = NSImage(named: "Icon_VoiceChat_VideoLimit")!.precomposed(.white) + + + static let video_on = NSImage(named: "Icon_GroupCall_VideoOn")!.precomposed(.white) + static let video_off = NSImage(named: "Icon_GroupCall_VideoOff")!.precomposed(.white) + + static let invite_listener = NSImage(named: "Icon_VoiceChat_InviteListener")!.precomposed(GroupCallTheme.customTheme.accentColor, flipVertical: true) + static let invite_speaker = NSImage(named: "Icon_VoiceChat_InviteSpeaker")!.precomposed(customTheme.accentColor, flipVertical: true) + static let invite_link = NSImage(named: "Icon_InviteViaLink")!.precomposed(GroupCallTheme.customTheme.accentColor, flipVertical: true) + + + static let videoZoomOut = NSImage(named: "Icon_GroupCall_Video_ZoomOut")!.precomposed(NSColor.white.withAlphaComponent(0.8)) + static let videoZoomIn = NSImage(named: "Icon_GroupCall_Video_ZoomIn")!.precomposed(NSColor.white.withAlphaComponent(0.8)) + + + static let pin_video = NSImage(named: "Icon_VoiceChat_PinVideo")!.precomposed(.white) + static let unpin_video = NSImage(named: "Icon_VoiceChat_UnpinVideo")!.precomposed(.white) + + + static let pin_window = NSImage(named: "Icon_VoiceChat_PinWindow")!.precomposed(.white) + static let unpin_window = NSImage(named: "Icon_VoiceChat_PinWindow")!.precomposed(GroupCallTheme.customTheme.accentColor) + + + static let unhide_peers = NSImage(named: "Icon_VoiceChat_HidePeers")!.precomposed(.white) + static let hide_peers = NSImage(named: "Icon_VoiceChat_HidePeers")!.precomposed(GroupCallTheme.customTheme.accentColor) + + static let smallTableWidth: CGFloat = 160 + static let fullScreenThreshold: CGFloat = 500 + + static let tileTableWidth: CGFloat = 200 + + static var minSize:NSSize { + return NSMakeSize(380, 600) + } + static var minFullScreenSize:NSSize { + return NSMakeSize(fullScreenThreshold, minSize.height) + } + + private static let switchAppearance = SwitchViewAppearance(backgroundColor: GroupCallTheme.membersColor, stateOnColor: GroupCallTheme.blueStatusColor, stateOffColor: GroupCallTheme.grayStatusColor, disabledColor: GroupCallTheme.grayStatusColor.withAlphaComponent(0.5), borderColor: GroupCallTheme.memberSeparatorColor) + + static let customTheme: GeneralRowItem.Theme = GeneralRowItem.Theme(backgroundColor: GroupCallTheme.membersColor, + grayBackground: GroupCallTheme.windowBackground, + grayForeground: GroupCallTheme.grayStatusColor, + highlightColor: GroupCallTheme.membersColor.withAlphaComponent(0.7), + borderColor: GroupCallTheme.memberSeparatorColor, + accentColor: GroupCallTheme.blueStatusColor, + secondaryColor: GroupCallTheme.grayStatusColor, + textColor: NSColor(rgb: 0xffffff), + grayTextColor: GroupCallTheme.grayStatusColor, + underSelectedColor: NSColor(rgb: 0xffffff), + accentSelectColor: GroupCallTheme.blueStatusColor.darker(), + redColor: GroupCallTheme.speakLockedColor, + indicatorColor: NSColor(rgb: 0xffffff), + appearance: darkPalette.appearance, + switchAppearance: switchAppearance, + unselectedImage: generateChatGroupToggleUnselected(foregroundColor: GroupCallTheme.grayStatusColor.withAlphaComponent(0.6), backgroundColor: NSColor.black.withAlphaComponent(0.01)), + selectedImage: generateChatGroupToggleSelected(foregroundColor: GroupCallTheme.blueStatusColor, backgroundColor: NSColor(rgb: 0xffffff))) + + +} + +final class GroupCallWindow : Window { + + + var navigation: NavigationViewController? + + init() { + let size = GroupCallTheme.minSize + var rect: NSRect = .init(origin: .init(x: 100, y: 100), size: size) + if let screen = NSScreen.main { + let x = floorToScreenPixels(System.backingScale, (screen.frame.width - size.width) / 2) + let y = floorToScreenPixels(System.backingScale, (screen.frame.height - size.height) / 2) + rect = .init(origin: .init(x: x, y: y), size: size) + } + + // + super.init(contentRect: rect, styleMask: [.fullSizeContentView, .borderless, .miniaturizable, .closable, .titled, .resizable], backing: .buffered, defer: true) + self.minSize = GroupCallTheme.minSize + self.name = "GroupCallWindow5" + self.acceptFirstMouse = false + self.titlebarAppearsTransparent = true + self.titleVisibility = .hidden + self.animationBehavior = .alertPanel + self.isReleasedWhenClosed = false + self.isMovableByWindowBackground = true + self.level = .normal + self.appearance = darkPalette.appearance +// self.toolbar = NSToolbar(identifier: "window") +// self.toolbar?.showsBaselineSeparator = false + + initSaver() + } + + + override func layoutIfNeeded() { + super.layoutIfNeeded() + + if !isFullScreen { + var point: NSPoint = NSMakePoint(20, 4) + self.standardWindowButton(.closeButton)?.setFrameOrigin(point) + point.x += 20 + self.standardWindowButton(.miniaturizeButton)?.setFrameOrigin(point) + point.x += 20 + self.standardWindowButton(.zoomButton)?.setFrameOrigin(point) + } + + } + + deinit { + var bp:Int = 0 + bp += 1 + } + + override func orderOut(_ sender: Any?) { + super.orderOut(sender) + } +} + + +final class GroupCallContext { + private let controller: GroupCallUIController + private let navigation: MajorNavigationController + + let window: GroupCallWindow + + + let call: PresentationGroupCall + let peerMemberContextsManager: PeerChannelMemberCategoriesContextsManager + private let presentDisposable = MetaDisposable() + private let removeDisposable = MetaDisposable() + init(call: PresentationGroupCall, peerMemberContextsManager: PeerChannelMemberCategoriesContextsManager) { + self.call = call + self.peerMemberContextsManager = peerMemberContextsManager + self.window = GroupCallWindow() + self.controller = GroupCallUIController(.init(call: call, peerMemberContextsManager: peerMemberContextsManager), size: window.frame.size) + self.navigation = MajorNavigationController(GroupCallUIController.self, controller, self.window) + self.navigation._frameRect = NSMakeRect(0, 0, window.frame.width, window.frame.height) + self.navigation.alwaysAnimate = true + self.navigation.cleanupAfterDeinit = true + self.navigation.viewWillAppear(false) + self.window.contentView = self.navigation.view + self.window.navigation = navigation + self.navigation.viewDidAppear(false) + removeDisposable.set((self.call.canBeRemoved |> deliverOnMainQueue).start(next: { [weak self] value in + if value { + self?.readyClose(last: value) + } + })) + + self.window.closeInterceptor = { [weak self] in + self?.readyClose() + return true + } + } + + deinit { + presentDisposable.dispose() + removeDisposable.dispose() + } + + func present() { + + presentDisposable.set((self.controller.ready.get() |> take(1)).start(completed: { [weak self] in + guard let `self` = self else { + return + } + self._readyPresent() + })) + } + + private func readyClose(last: Bool = false) { + if window.isFullScreen { + window.toggleFullScreen(nil) + window._windowDidExitFullScreen = { [weak self] in + self?.invikeClose(last: last) + } + } else { + invikeClose(last: last) + } + + } + private func invikeClose(last: Bool) { + if last { + call.sharedContext.updateCurrentGroupCallValue(nil) + } + closeAllModals(window: window) + self.navigation.viewWillDisappear(false) + var window: GroupCallWindow? = self.window + if self.window.isVisible { + NSAnimationContext.runAnimationGroup({ _ in + window?.animator().alphaValue = 0 + }, completionHandler: { + window?.orderOut(nil) + if last { + window?.contentView?.removeFromSuperview() + window?.contentView = nil + window?.navigation = nil + } + window = nil + }) + } else if last { + window?.contentView?.removeFromSuperview() + window?.contentView = nil + window?.navigation = nil + } + self.navigation.viewDidDisappear(false) + } + + func close() { + _ = call.sharedContext.endGroupCall(terminate: false).start() + self.readyClose() + } + func leave() { + _ = call.sharedContext.endGroupCall(terminate: false).start() + } + func leaveSignal() -> Signal { + self.controller.disableSounds = true + return call.sharedContext.endGroupCall(terminate: false) + } + + @objc private func _readyPresent() { + call.sharedContext.updateCurrentGroupCallValue(self) + window.alphaValue = 1 + self.window.makeKeyAndOrderFront(nil) + self.window.orderFrontRegardless() + } + +} + + +func applyGroupCallResult(_ sharedContext: SharedAccountContext, _ result:GroupCallContext) { + assertOnMainThread() + result.call.sharedContext.showGroupCall(with: result) + result.present() +} diff --git a/Telegram-Mac/GroupInfoEntries.swift b/Telegram-Mac/GroupInfoEntries.swift index 75ec9407a2..da78943c0d 100644 --- a/Telegram-Mac/GroupInfoEntries.swift +++ b/Telegram-Mac/GroupInfoEntries.swift @@ -7,21 +7,35 @@ // import Cocoa -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac +import Postbox +import TelegramCore + +import SwiftSignalKit import TGUIKit +let minumimUsersBlock: Int = 5 private func valuesRequiringUpdate(state: GroupInfoState, view: PeerView) -> (title: String?, description: String?) { if let peer = view.peers[view.peerId] as? TelegramGroup { + var titleValue: String? + var descriptionValue: String? if let editingState = state.editingState { if let title = editingState.editingName, title != peer.title { - return (title, nil) + titleValue = title + } + if let cachedData = view.cachedData as? CachedGroupData { + if let about = cachedData.about { + if about != editingState.editingDescriptionText { + descriptionValue = editingState.editingDescriptionText + } + } else if !editingState.editingDescriptionText.isEmpty { + descriptionValue = editingState.editingDescriptionText + } } } - return (nil, nil) + + return (titleValue, descriptionValue) } else if let peer = view.peers[view.peerId] as? TelegramChannel { var titleValue: String? var descriptionValue: String? @@ -58,16 +72,6 @@ struct GroupInfoEditingState: Equatable { func withUpdatedEditingDescriptionText(_ editingDescriptionText: String) -> GroupInfoEditingState { return GroupInfoEditingState(editingName: self.editingName, editingDescriptionText: editingDescriptionText) } - - static func ==(lhs: GroupInfoEditingState, rhs: GroupInfoEditingState) -> Bool { - if lhs.editingName != rhs.editingName { - return false - } - if lhs.editingDescriptionText != rhs.editingDescriptionText { - return false - } - return true - } } final class GroupInfoArguments : PeerInfoArguments { @@ -76,35 +80,63 @@ final class GroupInfoArguments : PeerInfoArguments { private let removeMemberDisposable = MetaDisposable() private let updatePeerNameDisposable = MetaDisposable() private let updatePhotoDisposable = MetaDisposable() + private let reportPeerDisposable = MetaDisposable() func updateState(_ f: (GroupInfoState) -> GroupInfoState) -> Void { updateInfoState { state -> PeerInfoState in - return f(state as! GroupInfoState) + let result = f(state as! GroupInfoState) + return result + } + } + + + var loadMore: (()->Void)? = nil + + private var _linksManager:InviteLinkPeerManager? + var linksManager: InviteLinkPeerManager { + if let _linksManager = _linksManager { + return _linksManager + } else { + _linksManager = InviteLinkPeerManager(context: context, peerId: peerId) + _linksManager!.loadNext() + return _linksManager! } } - override func updateEditable(_ editable:Bool, peerView:PeerView) { + override func updateEditable(_ editable:Bool, peerView:PeerView, controller: PeerInfoController) -> Bool { - let account = self.account + let context = self.context let peerId = self.peerId let updateState:((GroupInfoState)->GroupInfoState)->Void = { [weak self] f in self?.updateState(f) } if editable { if let peer = peerViewMainPeer(peerView) { - if peer.isGroup { + if peer.isSupergroup, let cachedData = peerView.cachedData as? CachedChannelData { updateState { state -> GroupInfoState in - return state.withUpdatedEditingState(GroupInfoEditingState(editingName: peer.displayTitle)) + return state.withUpdatedEditingState(GroupInfoEditingState(editingName: peer.displayTitle, editingDescriptionText: cachedData.about ?? "")) } - } else if peer.isSupergroup, let cachedData = peerView.cachedData as? CachedChannelData { + } else if let cachedData = peerView.cachedData as? CachedGroupData { updateState { state -> GroupInfoState in return state.withUpdatedEditingState(GroupInfoEditingState(editingName: peer.displayTitle, editingDescriptionText: cachedData.about ?? "")) } } } } else { + + + var updateValues: (title: String?, description: String?) = (nil, nil) updateState { state in updateValues = valuesRequiringUpdate(state: state, view: peerView) + return state + } + + if let titleValue = updateValues.title, titleValue.isEmpty { + controller.genericView.item(stableId: IntPeerInfoEntryStableId(value: 1).hashValue)?.view?.shakeView() + return false + } + + updateState { state in if updateValues.0 != nil || updateValues.1 != nil { return state.withUpdatedSavingData(true) } else { @@ -112,25 +144,26 @@ final class GroupInfoArguments : PeerInfoArguments { } } - let updateTitle: Signal + + let updateTitle: Signal if let titleValue = updateValues.title { - updateTitle = updatePeerTitle(account: account, peerId: peerId, title: titleValue) - |> mapError { _ in return Void() } + updateTitle = context.engine.peers.updatePeerTitle(peerId: peerId, title: titleValue) + |> `catch` {_ in return .complete()} } else { updateTitle = .complete() } - let updateDescription: Signal + let updateDescription: Signal if let descriptionValue = updateValues.description { - updateDescription = updatePeerDescription(account: account, peerId: peerId, description: descriptionValue.isEmpty ? nil : descriptionValue) - |> mapError { _ in return Void() } + updateDescription = context.engine.peers.updatePeerDescription(peerId: peerId, description: descriptionValue.isEmpty ? nil : descriptionValue) + |> `catch` {_ in return .complete()} } else { updateDescription = .complete() } let signal = combineLatest(updateTitle, updateDescription) - updatePeerNameDisposable.set(showModalProgress(signal: (signal |> deliverOnMainQueue), for: mainWindow).start(error: { _ in + updatePeerNameDisposable.set(showModalProgress(signal: (signal |> deliverOnMainQueue), for: context.window).start(error: { _ in updateState { state in return state.withUpdatedSavingData(false) } @@ -140,6 +173,8 @@ final class GroupInfoArguments : PeerInfoArguments { } })) } + + return true } override func dismissEdition() { @@ -147,7 +182,7 @@ final class GroupInfoArguments : PeerInfoArguments { return state.withUpdatedSavingData(false).withUpdatedEditingState(nil) } } - + func updateEditingName(_ name:String) -> Void { updateState { state in if let editingState = state.editingState { @@ -167,38 +202,151 @@ final class GroupInfoArguments : PeerInfoArguments { } func visibilitySetup() { - let setup = ChannelVisibilityController(account: account, peerId: peerId) - _ = (setup.onComplete.get() |> deliverOnMainQueue).start(next: { [weak self] _ in + let setup = ChannelVisibilityController(context, peerId: peerId, isChannel: false, linksManager: linksManager) + _ = (setup.onComplete.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peerId in + self?.changeControllers(peerId) self?.pullNavigation()?.back() }) pushViewController(setup) } + + func autoremoveController() { + //pushViewController(AutoremoveMessagesController(context: context, peerId: peerId)) + } - func preHistorySetup() { - let setup = PreHistorySettingsController(account, peerId: peerId) - _ = (setup.onComplete.get() |> deliverOnMainQueue).start(next: { [weak self] enabled in - if let strongSelf = self { - _ = showModalProgress(signal: updateChannelHistoryAvailabilitySettingsInteractively(postbox: strongSelf.account.postbox, network: strongSelf.account.network, peerId: strongSelf.peerId, historyAvailableForNewMembers: enabled), for: mainWindow).start() + func openInviteLinks() { + pushViewController(InviteLinksController(context: context, peerId: peerId, manager: linksManager)) + } + + private func changeControllers(_ peerId: PeerId?) { + guard let navigationController = self.pullNavigation() else { + return + } + if let peerId = peerId { + var chatController: ChatController? = ChatController(context: context, chatLocation: .peer(peerId)) + + navigationController.removeAll() + + chatController!.navigationController = navigationController + chatController!.loadViewIfNeeded(navigationController.bounds) + + var signal = chatController!.ready.get() |> filter {$0} |> take(1) |> ignoreValues + + var controller: PeerInfoController? = PeerInfoController(context: context, peerId: peerId) + + + controller!.navigationController = navigationController + controller!.loadViewIfNeeded(navigationController.bounds) + + let mainSignal = controller!.ready.get() |> filter {$0} |> take(1) |> ignoreValues + + signal = combineLatest(queue: .mainQueue(), signal, mainSignal) |> ignoreValues + + _ = signal.start(completed: { [weak navigationController] in + guard let navigationController = navigationController else { return } + + + navigationController.stackInsert(chatController!, at: 0) + navigationController.stackInsert(controller!, at: 1) + navigationController.stackInsert(navigationController.controller, at: 2) + navigationController.back() + chatController = nil + controller = nil + }) + } else { + navigationController.back() + } + + + } + + func report() -> Void { + let context = self.context + let peerId = self.peerId + let report = reportReasonSelector(context: context) |> map { value -> (ChatController?, ReportReasonValue) in + switch value.reason { + case .fake: + return (nil, value) + default: + return (ChatController(context: context, chatLocation: .peer(peerId), initialAction: .selectToReport(reason: value)), value) + } + } |> deliverOnMainQueue + + reportPeerDisposable.set(report.start(next: { [weak self] controller, value in + if let controller = controller { + self?.pullNavigation()?.push(controller) + } else { + showModal(with: ReportDetailsController(context: context, reason: value, updated: { value in + _ = showModalProgress(signal: context.engine.peers.reportPeer(peerId: peerId, reason: value.reason, message: value.comment), for: context.window).start(completed: { + showModalText(for: context.window, text: L10n.peerInfoChannelReported) + }) + }), for: context.window) + } + })) + } + + func preHistorySetup() { + let setup = PreHistorySettingsController(context, peerId: peerId) + _ = (setup.onComplete.get() |> deliverOnMainQueue).start(next: { [weak self] peerId in + self?.changeControllers(peerId) }) pushViewController(setup) } func blacklist() { - pushViewController(ChannelBlacklistViewController(account: account, peerId: peerId)) + pushViewController(ChannelPermissionsController(context, peerId: peerId)) } - - func convert() { - pushViewController(ConvertGroupViewController(account: account, peerId: peerId)) + func blocked() -> Void { + pushViewController(ChannelBlacklistViewController(context, peerId: peerId)) } func admins() { - pushViewController(ChannelAdminsViewController(account: account, peerId: peerId)) + pushViewController(ChannelAdminsViewController(context, peerId: peerId)) } func invation() { - pushViewController(LinkInvationController(account: account, peerId: peerId)) + pushViewController(InviteLinksController(context: context, peerId: peerId, manager: linksManager)) + } + + func stats(_ datacenterId: Int32) { + self.pushViewController(GroupStatsViewController(context, peerId: peerId, datacenterId: datacenterId)) + } + + func makeVoiceChat(_ current: CachedChannelData.ActiveCall?, callJoinPeerId: PeerId?) { + let context = self.context + let peerId = self.peerId + if let activeCall = current { + let join:(PeerId, Date?)->Void = { joinAs, _ in + _ = showModalProgress(signal: requestOrJoinGroupCall(context: context, peerId: peerId, joinAs: joinAs, initialCall: activeCall, initialInfo: nil, joinHash: nil), for: context.window).start(next: { result in + switch result { + case let .samePeer(callContext): + applyGroupCallResult(context.sharedContext, callContext) + case let .success(callContext): + applyGroupCallResult(context.sharedContext, callContext) + default: + alert(for: context.window, info: L10n.errorAnError) + } + }) + } + if let callJoinPeerId = callJoinPeerId { + join(callJoinPeerId, nil) + } else { + selectGroupCallJoiner(context: context, peerId: peerId, completion: join) + } + } else { + createVoiceChat(context: context, peerId: peerId, canBeScheduled: true) + } + } + + func showMore() { + let updateState:((GroupInfoState)->GroupInfoState)->Void = { [weak self] f in + self?.updateState(f) + } + updateState { + return $0.withUpdatedHasShowMoreButton(nil) + } } func updatePhoto(_ path:String) -> Void { @@ -208,42 +356,29 @@ final class GroupInfoArguments : PeerInfoArguments { } let cancel = { [weak self] in - self?.updatePhotoDisposable.dispose() + self?.updatePhotoDisposable.set(nil) updateState { state -> GroupInfoState in return state.withoutUpdatingPhotoState() } } - let account = self.account + let context = self.context let peerId = self.peerId - - -// let updateSignal = filethumb(with: URL(fileURLWithPath: path), account: account, scale: System.backingScale) |> mapToSignal { res -> Signal in -// guard let image = NSImage(contentsOf: URL(fileURLWithPath: path)) else { -// return .complete() -// } -// let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: image.size, boundingSize: NSMakeSize(640, 640), intrinsicInsets: NSEdgeInsets()) -// if let image = res(arguments)?.generateImage() { -// return putToTemp(image: NSImage(cgImage: image, size: image.backingSize)) -// } -// return .complete() -// } |> map { path -> TelegramMediaResource in -// return LocalFileReferenceMediaResource(localFilePath: path, randomId: arc4random64()) -// } - - let updateSignal = Signal.single(path) |> map { path -> TelegramMediaResource in + var updateSignal = Signal.single(path) |> map { path -> TelegramMediaResource in return LocalFileReferenceMediaResource(localFilePath: path, randomId: arc4random64()) - } |> beforeNext { resource in + } |> beforeNext { resource in updateState { (state) -> GroupInfoState in return state.withUpdatedUpdatingPhotoState { previous -> PeerInfoUpdatingPhotoState? in - return PeerInfoUpdatingPhotoState(progress: 0, cancel: cancel) + return PeerInfoUpdatingPhotoState(progress: 0, image: NSImage(contentsOfFile: path)?.cgImage(forProposedRect: nil, context: nil, hints: nil), cancel: cancel) } } } |> mapError {_ in return UploadPeerPhotoError.generic} |> mapToSignal { resource -> Signal in - return updatePeerPhoto(account: account, peerId: peerId, resource: resource) + return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: resource), mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: context.account.postbox, resource: resource, representations: representations) + }) } @@ -271,22 +406,143 @@ final class GroupInfoArguments : PeerInfoArguments { } - func addMember() -> Void { - let account = self.account + func updateVideo(_ signal:Signal) -> Void { + + let updateState:((GroupInfoState)->GroupInfoState)->Void = { [weak self] f in + self?.updateState(f) + } + + let cancel = { [weak self] in + self?.updatePhotoDisposable.set(nil) + updateState { state -> GroupInfoState in + return state.withoutUpdatingPhotoState() + } + } + + let context = self.context + let peerId = self.peerId + + + let updateSignal: Signal = signal + |> mapError { _ in return UploadPeerPhotoError.generic } + |> mapToSignal { state in + switch state { + case .error: + return .fail(.generic) + case let .start(path): + updateState { (state) -> GroupInfoState in + return state.withUpdatedUpdatingPhotoState { previous -> PeerInfoUpdatingPhotoState? in + return PeerInfoUpdatingPhotoState(progress: 0, image: NSImage(contentsOfFile: path)?._cgImage, cancel: cancel) + } + } + return .next(.progress(0)) + case let .progress(value): + return .next(.progress(value * 0.2)) + case let .complete(thumb, video, keyFrame): + let (thumbResource, videoResource) = (LocalFileReferenceMediaResource(localFilePath: thumb, randomId: arc4random64(), isUniquelyReferencedTemporaryFile: true), + LocalFileReferenceMediaResource(localFilePath: video, randomId: arc4random64(), isUniquelyReferencedTemporaryFile: true)) + + return context.engine.peers.updatePeerPhoto(peerId: peerId, photo: context.engine.peers.uploadedPeerPhoto(resource: thumbResource), video: context.engine.peers.uploadedPeerVideo(resource: videoResource) |> map(Optional.init), videoStartTimestamp: keyFrame, mapResourceToAvatarSizes: { resource, representations in + return mapResourceToAvatarSizes(postbox: context.account.postbox, resource: resource, representations: representations) + }) |> map { result in + switch result { + case let .progress(current): + return .progress(0.2 + (current * 0.8)) + default: + return result + } + } + } + } + + updatePhotoDisposable.set((updateSignal |> deliverOnMainQueue).start(next: { status in + updateState { state -> GroupInfoState in + switch status { + case .complete: + return state.withoutUpdatingPhotoState() + case let .progress(progress): + return state.withUpdatedUpdatingPhotoState { previous -> PeerInfoUpdatingPhotoState? in + return previous?.withUpdatedProgress(progress) + } + } + } + }, error: { error in + updateState { (state) -> GroupInfoState in + return state.withoutUpdatingPhotoState() + } + }, completed: { + updateState { (state) -> GroupInfoState in + return state.withoutUpdatingPhotoState() + } + })) + + + } + + func setupDiscussion() { + _ = (self.context.account.postbox.loadedPeerWithId(self.peerId) |> deliverOnMainQueue).start(next: { [weak self] peer in + if let `self` = self { + self.pushViewController(ChannelDiscussionSetupController(context: self.context, peer: peer)) + } + }) + } + + private func upgradeToSupergroup() -> (PeerId, @escaping () -> Void) -> Void { + return { [weak self] upgradedPeerId, f in + guard let `self` = self, let navigationController = self.pullNavigation() else { + return + } + let context = self.context + + var chatController: ChatController? = ChatController(context: context, chatLocation: .peer(upgradedPeerId)) + + + chatController!.navigationController = navigationController + chatController!.loadViewIfNeeded(navigationController.bounds) + + var signal = chatController!.ready.get() |> filter {$0} |> take(1) |> ignoreValues + + var controller: PeerInfoController? = PeerInfoController(context: context, peerId: upgradedPeerId) + + controller!.navigationController = navigationController + controller!.loadViewIfNeeded(navigationController.bounds) + + let mainSignal = combineLatest(controller!.ready.get(), controller!.ready.get()) |> map { $0 && $1 } |> filter {$0} |> take(1) |> ignoreValues + + signal = combineLatest(queue: .mainQueue(), signal, mainSignal) |> ignoreValues + + _ = signal.start(completed: { [weak navigationController] in + navigationController?.removeAll() + navigationController?.push(chatController!, false, style: .none) + navigationController?.push(controller!, false, style: .none) + + chatController = nil + controller = nil + }) + + } + } + + func addMember(_ canInviteByLink: Bool) -> Void { + + let upgradeToSupergroup = self.upgradeToSupergroup() + + let context = self.context let peerId = self.peerId let updateState:((GroupInfoState)->GroupInfoState)->Void = { [weak self] f in self?.updateState(f) } - let confirmationImpl:([PeerId])->Signal = { peerIds in + let confirmationImpl:([PeerId])->Signal = { peerIds in if let first = peerIds.first, peerIds.count == 1 { - return account.postbox.loadedPeerWithId(first) |> deliverOnMainQueue |> mapToSignal { peer in - return confirmSignal(for: mainWindow, header: appName, information: tr(.peerInfoConfirmAddMember(peer.displayTitle))) + return context.account.postbox.loadedPeerWithId(first) |> deliverOnMainQueue |> mapToSignal { peer in + return confirmSignal(for: context.window, information: L10n.peerInfoConfirmAddMember(peer.displayTitle), okTitle: L10n.peerInfoConfirmAdd) } } - return confirmSignal(for: mainWindow, header: appName, information: tr(.peerInfoConfirmAddMembers(peerIds.count))) + return confirmSignal(for: context.window, information: L10n.peerInfoConfirmAddMembers1Countable(peerIds.count), okTitle: L10n.peerInfoConfirmAdd) } - let addMember = account.viewTracker.peerView( peerId) |> take(1) |> deliverOnMainQueue |> mapToSignal{ view -> Signal in + + let addMember = context.account.viewTracker.peerView(peerId) |> take(1) |> deliverOnMainQueue |> mapToSignal{ view -> Signal in var excludePeerIds:[PeerId] = [] if let cachedData = view.cachedData as? CachedChannelData { @@ -295,10 +551,18 @@ final class GroupInfoArguments : PeerInfoArguments { excludePeerIds = Array(cachedData.peerIds) } - return selectModalPeers(account: account, title: tr(.peerInfoAddMember), settings: [.contacts, .remote], excludePeerIds:excludePeerIds, limit: peerId.namespace == Namespaces.Peer.CloudGroup ? 1 : 100, confirmation: confirmationImpl) + var linkInvation: ((Int)->Void)? = nil + if canInviteByLink { + linkInvation = { [weak self] _ in + self?.invation() + } + } + + + return selectModalPeers(window: context.window, context: context, title: L10n.peerInfoAddMember, settings: [.contacts, .remote], excludePeerIds:excludePeerIds, limit: peerId.namespace == Namespaces.Peer.CloudGroup ? 1 : 100, confirmation: confirmationImpl, linkInvation: linkInvation) |> deliverOnMainQueue |> mapToSignal { memberIds -> Signal in - return account.postbox.multiplePeersView(memberIds + [peerId]) + return context.account.postbox.multiplePeersView(memberIds + [peerId]) |> take(1) |> deliverOnMainQueue |> mapToSignal { view -> Signal in @@ -326,7 +590,7 @@ final class GroupInfoArguments : PeerInfoArguments { if let peer = view.peers[peerId] { if peer.isGroup, let memberId = memberIds.first { - return addPeerMember(account: account, peerId: peerId, memberId: memberId) + return context.engine.peers.addGroupMember(peerId: peerId, memberId: memberId) |> deliverOnMainQueue |> afterCompleted { updateState { state in @@ -335,7 +599,7 @@ final class GroupInfoArguments : PeerInfoArguments { return state.withUpdatedSuccessfullyAddedParticipantIds(successfullyAddedParticipantIds) } - } |> `catch` { _ -> Signal in + } |> `catch` { error -> Signal in updateState { state in var temporaryParticipants = state.temporaryParticipants for i in 0 ..< temporaryParticipants.count { @@ -349,12 +613,50 @@ final class GroupInfoArguments : PeerInfoArguments { return state.withUpdatedTemporaryParticipants(temporaryParticipants).withUpdatedSuccessfullyAddedParticipantIds(successfullyAddedParticipantIds) } - return .complete() } } else if peer.isSupergroup { - return addChannelMembers(account: account, peerId: peerId, memberIds: memberIds) + return context.peerChannelMemberCategoriesContextsManager.addMembers(peerId: peerId, memberIds: memberIds) |> deliverOnMainQueue |> `catch` { error in + let text: String + switch error { + case .notMutualContact: + text = L10n.groupInfoAddUserLeftError + case .limitExceeded: + text = L10n.channelErrorAddTooMuch + case .botDoesntSupportGroups: + text = L10n.channelBotDoesntSupportGroups + case .tooMuchBots: + text = L10n.channelTooMuchBots + case .tooMuchJoined: + text = L10n.inviteChannelsTooMuch + case .generic: + text = L10n.unknownError + case let .bot(memberId): + let _ = (context.account.postbox.transaction { transaction in + return transaction.getPeer(peerId) + } + |> deliverOnMainQueue).start(next: { peer in + guard let peer = peer as? TelegramChannel else { + alert(for: context.window, info: L10n.unknownError) + return + } + if peer.hasPermission(.addAdmins) { + confirm(for: context.window, information: L10n.channelAddBotErrorHaveRights, okTitle: L10n.channelAddBotAsAdmin, successHandler: { _ in + showModal(with: ChannelAdminController(context, peerId: peerId, adminId: memberId, initialParticipant: nil, updated: { _ in }, upgradedToSupergroup: upgradeToSupergroup), for: context.window) + }) + } else { + alert(for: context.window, info: L10n.channelAddBotErrorHaveRights) + } + }) + return .complete() + case .restricted: + text = L10n.groupErrorAddBlocked + } + alert(for: context.window, info: text) + + return .complete() + } } } @@ -366,18 +668,38 @@ final class GroupInfoArguments : PeerInfoArguments { addMemberDisposable.set(addMember.start()) } + + func restrict(_ participant: ChannelParticipant) -> Void { + + let context = self.context + let peerId = self.peerId + + showModal(with: RestrictedModalViewController(context, peerId: peerId, memberId: participant.peerId, initialParticipant: participant, updated: { updatedRights in + _ = context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(peerId: peerId, memberId: participant.peerId, bannedRights: updatedRights).start() + }), for: context.window) + } + + func promote(_ participant: ChannelParticipant) -> Void { + showModal(with: ChannelAdminController(context, peerId: peerId, adminId: participant.peerId, initialParticipant: participant, updated: { _ in }, upgradedToSupergroup: self.upgradeToSupergroup()), for: context.window) + } + func removePeer(_ memberId:PeerId) -> Void { - let account = self.account + let context = self.context let peerId = self.peerId let updateState:((GroupInfoState)->GroupInfoState)->Void = { [weak self] f in self?.updateState(f) } - let signal = account.postbox.loadedPeerWithId(memberId) + + + + let signal = context.account.postbox.loadedPeerWithId(memberId) |> deliverOnMainQueue |> mapToSignal { peer -> Signal in - return confirmSignal(for: mainWindow, header: appName, information: tr(.peerInfoConfirmRemovePeer(peer.displayTitle))) + let result = ValuePromise() + result.set(true) + return result.get() } |> mapToSignal { value -> Signal in if value { @@ -398,7 +720,21 @@ final class GroupInfoArguments : PeerInfoArguments { return state.withUpdatedTemporaryParticipants(temporaryParticipants).withUpdatedSuccessfullyAddedParticipantIds(successfullyAddedParticipantIds).withUpdatedRemovingParticipantIds(removingParticipantIds) } - return (peerId.namespace == Namespaces.Peer.CloudChannel ? updateChannelMemberBannedRights(account: account, peerId: peerId, memberId: memberId, rights: TelegramChannelBannedRights(flags: [.banReadMessages], untilDate: 0)) : removePeerMember(account: account, peerId: peerId, memberId: memberId)) + if peerId.namespace == Namespaces.Peer.CloudChannel { + return context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(peerId: peerId, memberId: memberId, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)) + |> afterDisposed { + Queue.mainQueue().async { + updateState { state in + var removingParticipantIds = state.removingParticipantIds + removingParticipantIds.remove(memberId) + + return state.withUpdatedRemovingParticipantIds(removingParticipantIds) + } + } + } + } + + return context.engine.peers.removePeerMember(peerId: peerId, memberId: memberId) |> deliverOnMainQueue |> afterDisposed { updateState { state in @@ -414,18 +750,123 @@ final class GroupInfoArguments : PeerInfoArguments { } removeMemberDisposable.set(signal.start()) } - - func setGroupAdmins() { - pullNavigation()?.push(GroupAdminsController(account: account, peerId: peerId)) - } + func setGroupStickerset() { - pullNavigation()?.push(GroupStickerSetController(account: account, peerId: peerId)) + pullNavigation()?.push(GroupStickerSetController(context, peerId: peerId)) } + func updateGroupPhoto(_ custom: NSImage?, control: Control?) { + let updatePhoto:(NSImage) -> Void = { image in + _ = (putToTemp(image: image, compress: true) |> deliverOnMainQueue).start(next: { path in + let controller = EditImageModalController(URL(fileURLWithPath: path), settings: .disableSizes(dimensions: .square)) + showModal(with: controller, for: mainWindow, animationType: .scaleCenter) + _ = controller.result.start(next: { [weak self] url, _ in + self?.updatePhoto(url.path) + }) + controller.onClose = { + removeFile(at: path) + } + }) + } + if let image = custom { + updatePhoto(image) + } else { + + let context = self.context + let updateVideo = self.updateVideo + + + var items:[SPopoverItem] = [] + + items.append(.init(L10n.editAvatarPhotoOrVideo, { + filePanel(with: photoExts + videoExts, allowMultiple: false, canChooseDirectories: false, for: context.window, completion: { paths in + if let path = paths?.first, let image = NSImage(contentsOfFile: path) { + updatePhoto(image) + } else if let path = paths?.first { + selectVideoAvatar(context: context, path: path, localize: L10n.videoAvatarChooseDescGroup, signal: { signal in + updateVideo(signal) + }) + } + }) + })) + + items.append(.init(L10n.editAvatarStickerOrGif, { [weak control] in + let controller = EntertainmentViewController(size: NSMakeSize(350, 350), context: context, mode: .selectAvatar) + controller._frameRect = NSMakeRect(0, 0, 350, 400) + + let interactions = ChatInteraction(chatLocation: .peer(context.peerId), context: context) + + let runConvertor:(MediaObjectToAvatar)->Void = { [weak control] convertor in + _ = showModalProgress(signal: convertor.start(), for: context.window).start(next: { [weak control] result in + switch result { + case let .image(image): + updatePhoto(image) + case let .video(path): + selectVideoAvatar(context: context, path: path, localize: L10n.videoAvatarChooseDescGroup, quality: AVAssetExportPresetHighestQuality, signal: { signal in + updateVideo(signal) + }) + } + control?.contextObject = nil + }) + control?.contextObject = convertor + } + + interactions.sendAppFile = { file, _, _ in + let object: MediaObjectToAvatar.Object + if file.isAnimatedSticker { + object = .animated(file) + } else if file.isSticker { + object = .sticker(file) + } else { + object = .gif(file) + } + let convertor = MediaObjectToAvatar(context: context, object: object) + runConvertor(convertor) + } + interactions.sendInlineResult = { [] collection, result in + switch result { + case let .internalReference(reference): + if let file = reference.file { + let convertor = MediaObjectToAvatar(context: context, object: .gif(file)) + runConvertor(convertor) + } + case .externalReference: + break + } + } + + control?.contextObject = interactions + controller.update(with: interactions) + if let control = control { + showPopover(for: control, with: controller, edge: .maxY, inset: NSMakePoint(0, -110), static: true) + } + })) + + if let control = control { + showPopover(for: control, with: SPopoverViewController(items: items), edge: .maxY, inset: NSMakePoint(0, -60)) + } else { + filePanel(with: photoExts + videoExts, allowMultiple: false, canChooseDirectories: false, for: context.window, completion: { paths in + if let path = paths?.first, let image = NSImage(contentsOfFile: path) { + updatePhoto(image) + } else if let path = paths?.first { + selectVideoAvatar(context: context, path: path, localize: L10n.videoAvatarChooseDescGroup, signal: { signal in + updateVideo(signal) + }) + } + }) + } + } + } + func eventLog() { - pullNavigation()?.push(ChannelEventLogController(account, peerId: peerId)) + pullNavigation()?.push(ChannelEventLogController(context, peerId: peerId)) + } + + func peerMenuItems(for peer: Peer) -> [ContextMenuItem] { + + return [] } deinit { @@ -433,6 +874,7 @@ final class GroupInfoArguments : PeerInfoArguments { addMemberDisposable.dispose() updatePeerNameDisposable.dispose() updatePhotoDisposable.dispose() + reportPeerDisposable.dispose() } } @@ -445,10 +887,12 @@ class GroupInfoState: PeerInfoState { let successfullyAddedParticipantIds: Set let removingParticipantIds: Set let updatingPhotoState:PeerInfoUpdatingPhotoState? - + let savingData: Bool - init(editingState: GroupInfoEditingState?, updatingName:String?, temporaryParticipants:[TemporaryParticipant], successfullyAddedParticipantIds:Set, removingParticipantIds:Set, savingData: Bool, updatingPhotoState:PeerInfoUpdatingPhotoState?) { + let hasShowMoreButton: Bool? + + init(editingState: GroupInfoEditingState?, updatingName:String?, temporaryParticipants:[TemporaryParticipant], successfullyAddedParticipantIds:Set, removingParticipantIds:Set, savingData: Bool, updatingPhotoState:PeerInfoUpdatingPhotoState?, hasShowMoreButton: Bool?) { self.editingState = editingState self.updatingName = updatingName self.temporaryParticipants = temporaryParticipants @@ -456,6 +900,7 @@ class GroupInfoState: PeerInfoState { self.removingParticipantIds = removingParticipantIds self.savingData = savingData self.updatingPhotoState = updatingPhotoState + self.hasShowMoreButton = hasShowMoreButton } override init() { @@ -466,6 +911,7 @@ class GroupInfoState: PeerInfoState { self.removingParticipantIds = Set() self.savingData = false self.updatingPhotoState = nil + self.hasShowMoreButton = true } func isEqual(to: PeerInfoState) -> Bool { @@ -475,71 +921,47 @@ class GroupInfoState: PeerInfoState { return false } - static func ==(lhs: GroupInfoState, rhs: GroupInfoState) -> Bool { - if lhs.editingState != rhs.editingState { - return false - } - if lhs.updatingName != rhs.updatingName { - return false - } - if lhs.temporaryParticipants != rhs.temporaryParticipants { - return false - } - if lhs.successfullyAddedParticipantIds != rhs.successfullyAddedParticipantIds { - return false - } - if lhs.removingParticipantIds != rhs.removingParticipantIds { - return false - } - if lhs.savingData != rhs.savingData { - return false - } - - if lhs.updatingPhotoState != rhs.updatingPhotoState { - return false - } - - return true - } - func withUpdatedEditingState(_ editingState: GroupInfoEditingState?) -> GroupInfoState { - return GroupInfoState(editingState: editingState, updatingName: self.updatingName, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData, updatingPhotoState: self.updatingPhotoState) + return GroupInfoState(editingState: editingState, updatingName: self.updatingName, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData, updatingPhotoState: self.updatingPhotoState, hasShowMoreButton: self.hasShowMoreButton) } func withUpdatedUpdatingName(_ updatingName: String?) -> GroupInfoState { - return GroupInfoState(editingState: self.editingState, updatingName: updatingName, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData, updatingPhotoState: self.updatingPhotoState) + return GroupInfoState(editingState: self.editingState, updatingName: updatingName, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData, updatingPhotoState: self.updatingPhotoState, hasShowMoreButton: self.hasShowMoreButton) } func withUpdatedTemporaryParticipants(_ temporaryParticipants: [TemporaryParticipant]) -> GroupInfoState { - return GroupInfoState(editingState: self.editingState, updatingName: self.updatingName, temporaryParticipants: temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData, updatingPhotoState: self.updatingPhotoState) + return GroupInfoState(editingState: self.editingState, updatingName: self.updatingName, temporaryParticipants: temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData, updatingPhotoState: self.updatingPhotoState, hasShowMoreButton: self.hasShowMoreButton) } func withUpdatedSuccessfullyAddedParticipantIds(_ successfullyAddedParticipantIds: Set) -> GroupInfoState { - return GroupInfoState(editingState: self.editingState, updatingName: self.updatingName, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData, updatingPhotoState: self.updatingPhotoState) + return GroupInfoState(editingState: self.editingState, updatingName: self.updatingName, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData, updatingPhotoState: self.updatingPhotoState, hasShowMoreButton: self.hasShowMoreButton) } func withUpdatedRemovingParticipantIds(_ removingParticipantIds: Set) -> GroupInfoState { - return GroupInfoState(editingState: self.editingState, updatingName: self.updatingName, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: removingParticipantIds, savingData: self.savingData, updatingPhotoState: self.updatingPhotoState) + return GroupInfoState(editingState: self.editingState, updatingName: self.updatingName, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: removingParticipantIds, savingData: self.savingData, updatingPhotoState: self.updatingPhotoState, hasShowMoreButton: self.hasShowMoreButton) } func withUpdatedSavingData(_ savingData: Bool) -> GroupInfoState { - return GroupInfoState(editingState: self.editingState, updatingName: self.updatingName, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: savingData, updatingPhotoState: self.updatingPhotoState) + return GroupInfoState(editingState: self.editingState, updatingName: self.updatingName, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: savingData, updatingPhotoState: self.updatingPhotoState, hasShowMoreButton: self.hasShowMoreButton) } func withUpdatedUpdatingPhotoState(_ f: (PeerInfoUpdatingPhotoState?) -> PeerInfoUpdatingPhotoState?) -> GroupInfoState { - return GroupInfoState(editingState: self.editingState, updatingName: self.updatingName, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData, updatingPhotoState: f(self.updatingPhotoState)) + return GroupInfoState(editingState: self.editingState, updatingName: self.updatingName, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData, updatingPhotoState: f(self.updatingPhotoState), hasShowMoreButton: self.hasShowMoreButton) } func withoutUpdatingPhotoState() -> GroupInfoState { - return GroupInfoState(editingState: self.editingState, updatingName: self.updatingName, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData, updatingPhotoState: nil) + return GroupInfoState(editingState: self.editingState, updatingName: self.updatingName, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData, updatingPhotoState: nil, hasShowMoreButton: self.hasShowMoreButton) + } + func withUpdatedHasShowMoreButton(_ hasShowMoreButton: Bool?) -> GroupInfoState { + return GroupInfoState(editingState: self.editingState, updatingName: self.updatingName, temporaryParticipants: self.temporaryParticipants, successfullyAddedParticipantIds: self.successfullyAddedParticipantIds, removingParticipantIds: self.removingParticipantIds, savingData: self.savingData, updatingPhotoState: self.updatingPhotoState, hasShowMoreButton: hasShowMoreButton) } } -enum GroupInfoMemberStatus { +enum GroupInfoMemberStatus : Equatable { case member - case admin + case admin(rank: String) } private struct GroupPeerEntryStableId: PeerInfoEntryStableId { @@ -561,53 +983,95 @@ private struct GroupPeerEntryStableId: PeerInfoEntryStableId { enum GroupInfoEntry: PeerInfoEntry { - case info(section:Int, view: PeerView, editable:Bool, updatingPhotoState:PeerInfoUpdatingPhotoState?) - case setGroupPhoto(section:Int) - case about(section:Int, text: String) - case addressName(section:Int, name:String) - case sharedMedia(section:Int) - case notifications(section:Int, settings: PeerNotificationSettings?) - case usersHeader(section:Int, count:Int) - case addMember(section:Int) - case inviteLink(section:Int) - case convertToSuperGroup(section:Int) - case groupTypeSetup(section:Int, isPublic: Bool) - case groupDescriptionSetup(section:Int, text: String) - case groupAboutDescription(section:Int) - case groupStickerset(section:Int, packName: String) - case preHistory(section:Int, enabled: Bool) - case groupManagementInfoLabel(section:Int, text: String) - case setAdmins(section:Int) - case membersAdmins(section:Int, count: Int) - case membersBlacklist(section:Int, count: Int) - - case member(section:Int, index: Int, peerId: PeerId, peer: Peer?, presence: PeerPresence?, memberStatus: GroupInfoMemberStatus, editing: ShortPeerDeleting?, enabled:Bool) - case leave(section:Int) + case info(section:Int, view: PeerView, editingState: Bool, updatingPhotoState:PeerInfoUpdatingPhotoState?, viewType: GeneralViewType) + case setTitle(section:Int, text: String, viewType: GeneralViewType) + case scam(section:Int, title: String, text: String, viewType: GeneralViewType) + case about(section:Int, text: String, viewType: GeneralViewType) + case addressName(section:Int, name:String, viewType: GeneralViewType) + case sharedMedia(section:Int, viewType: GeneralViewType) + case notifications(section:Int, settings: PeerNotificationSettings?, viewType: GeneralViewType) + case usersHeader(section:Int, count:Int, viewType: GeneralViewType) + case addMember(section:Int, inviteViaLink: Bool, viewType: GeneralViewType) + case groupTypeSetup(section:Int, isPublic: Bool, viewType: GeneralViewType) + case autoDeleteMessages(section:Int, timer: CachedPeerAutoremoveTimeout?, viewType: GeneralViewType) + case inviteLinks(section:Int, count: Int32, viewType: GeneralViewType) + case linkedChannel(section:Int, channel: Peer, subscribers: Int32?, viewType: GeneralViewType) + case groupDescriptionSetup(section:Int, text: String, viewType: GeneralViewType) + case groupAboutDescription(section:Int, viewType: GeneralViewType) + case groupStickerset(section:Int, packName: String, viewType: GeneralViewType) + case preHistory(section:Int, enabled: Bool, viewType: GeneralViewType) + case groupManagementInfoLabel(section:Int, text: String, viewType: GeneralViewType) + case administrators(section:Int, count: String, viewType: GeneralViewType) + case permissions(section:Int, count: String, viewType: GeneralViewType) + case blocked(section:Int, count:Int32?, viewType: GeneralViewType) + case member(section:Int, index: Int, peerId: PeerId, peer: Peer?, presence: PeerPresence?, activity: PeerInputActivity?, memberStatus: GroupInfoMemberStatus, editing: ShortPeerDeleting?, menuItems: [ContextMenuItem], enabled:Bool, viewType: GeneralViewType) + case showMore(section:Int, index: Int, viewType: GeneralViewType) + case leave(section:Int, text: String, viewType: GeneralViewType) + case media(section:Int, controller: PeerMediaController, isVisible: Bool, viewType: GeneralViewType) case section(Int) + func withUpdatedViewType(_ viewType: GeneralViewType) -> GroupInfoEntry { + switch self { + case let .info(section, view, editingState, updatingPhotoState, _): return .info(section: section, view: view, editingState: editingState, updatingPhotoState: updatingPhotoState, viewType: viewType) + case let .setTitle(section, text, _): return .setTitle(section: section, text: text, viewType: viewType) + case let .scam(section, title, text, _): return .scam(section: section, title: title, text: text, viewType: viewType) + case let .about(section, text, _): return .about(section: section, text: text, viewType: viewType) + case let .addressName(section, name, _): return .addressName(section: section, name: name, viewType: viewType) + case let .sharedMedia(section, _): return .sharedMedia(section: section, viewType: viewType) + case let .notifications(section, settings, _): return .notifications(section: section, settings: settings, viewType: viewType) + case let .usersHeader(section, count, _): return .usersHeader(section: section, count: count, viewType: viewType) + case let .addMember(section, inviteViaLink, _): return .addMember(section: section, inviteViaLink: inviteViaLink, viewType: viewType) + case let .groupTypeSetup(section, isPublic, _): return .groupTypeSetup(section: section, isPublic: isPublic, viewType: viewType) + case let .autoDeleteMessages(section, timer, _): return .autoDeleteMessages(section: section, timer: timer, viewType: viewType) + case let .inviteLinks(section, count, _): return .inviteLinks(section: section, count: count, viewType: viewType) + case let .linkedChannel(section, channel, subscriber, _): return .linkedChannel(section: section, channel: channel, subscribers: subscriber, viewType: viewType) + case let .groupDescriptionSetup(section, text, _): return .groupDescriptionSetup(section: section, text: text, viewType: viewType) + case let .groupAboutDescription(section, _): return .groupAboutDescription(section: section, viewType: viewType) + case let .groupStickerset(section, packName, _): return .groupStickerset(section: section, packName: packName, viewType: viewType) + case let .preHistory(section, enabled, _): return .preHistory(section: section, enabled: enabled, viewType: viewType) + case let .groupManagementInfoLabel(section, text, _): return .groupManagementInfoLabel(section: section, text: text, viewType: viewType) + case let .administrators(section, count, _): return .administrators(section: section, count: count, viewType: viewType) + case let .permissions(section, count, _): return .permissions(section: section, count: count, viewType: viewType) + case let .blocked(section, count, _): return .blocked(section: section, count: count, viewType: viewType) + case let .member(section, index, peerId, peer, presence, activity, memberStatus, editing, menuItems, enabled, _): return .member(section: section, index: index, peerId: peerId, peer: peer, presence: presence, activity: activity, memberStatus: memberStatus, editing: editing, menuItems: menuItems, enabled: enabled, viewType: viewType) + case let .showMore(section, index, _): return .showMore(section: section, index: index, viewType: viewType) + case let .leave(section, text, _): return .leave(section: section, text: text, viewType: viewType) + case let .media(section, controller, isVisible, _): return .media(section: section, controller: controller, isVisible: isVisible, viewType: viewType) + case .section: return self + } + } + func isEqual(to: PeerInfoEntry) -> Bool { guard let entry = to as? GroupInfoEntry else { return false } switch self { - case let .info(_, lhsPeerView, lhsEditable, lhsUpdatingPhotoState): + case let .info(lhsSection, lhsPeerView, lhsEditingState, lhsUpdatingPhotoState, lhsViewType): switch entry { - case let .info(_, rhsPeerView, rhsEditable, rhsUpdatingPhotoState): + case let .info(rhsSection, rhsPeerView, rhsEditingState, rhsUpdatingPhotoState, rhsViewType): - if lhsEditable != rhsEditable { + if lhsUpdatingPhotoState != rhsUpdatingPhotoState { return false } - if lhsUpdatingPhotoState != rhsUpdatingPhotoState { + if lhsSection != rhsSection { + return false + } + if lhsViewType != rhsViewType { + return false + } + + if lhsEditingState != rhsEditingState { return false } let lhsPeer = peerViewMainPeer(lhsPeerView) let lhsCachedData = lhsPeerView.cachedData + let lhsNotificationSettings = lhsPeerView.notificationSettings let rhsPeer = peerViewMainPeer(rhsPeerView) let rhsCachedData = rhsPeerView.cachedData - + let rhsNotificationSettings = rhsPeerView.notificationSettings if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer { if !lhsPeer.isEqual(rhsPeer) { return false @@ -615,6 +1079,14 @@ enum GroupInfoEntry: PeerInfoEntry { } else if (lhsPeer == nil) != (rhsPeer != nil) { return false } + + if let lhsNotificationSettings = lhsNotificationSettings, let rhsNotificationSettings = rhsNotificationSettings { + if !lhsNotificationSettings.isEqual(to: rhsNotificationSettings) { + return false + } + } else if (lhsNotificationSettings == nil) != (rhsNotificationSettings == nil) { + return false + } if let lhsCachedData = lhsCachedData, let rhsCachedData = rhsCachedData { if !lhsCachedData.isEqual(to: rhsCachedData) { return false @@ -626,65 +1098,70 @@ enum GroupInfoEntry: PeerInfoEntry { default: return false } - case .setGroupPhoto: - if case .setGroupPhoto = entry { + case let .setTitle(section, text, viewType): + if case .setTitle(section, text, viewType) = entry { return true } else { return false } - case let .addressName(_, addressName): - if case .addressName(_, addressName) = entry { + case let .addressName(section, addressName, viewType): + if case .addressName(section, addressName, viewType) = entry { return true } else { return false } - case let .about(_, text): - if case .about(_, text) = entry { + case let .scam(section, title, text, viewType): + if case .scam(section, title, text, viewType) = entry { return true } else { return false } - case .sharedMedia: - if case .sharedMedia = entry { + case let .about(section, text, viewType): + if case .about(section, text, viewType) = entry { return true } else { return false } - case let .preHistory(sectionId, enabled): - if case .preHistory(sectionId, enabled) = entry { + case let .sharedMedia(sectionId, viewType): + if case .sharedMedia(sectionId, viewType) = entry { return true } else { return false } - case .setAdmins: - if case .setAdmins = entry { + case let .preHistory(sectionId, enabled, viewType): + if case .preHistory(sectionId, enabled, viewType) = entry { return true } else { return false } - - case .inviteLink: - if case .inviteLink = entry { + case let .administrators(section, count, viewType): + if case .administrators(section, count, viewType) = entry { return true } else { return false } - case let .groupStickerset(sectionId, packName): - if case .groupStickerset(sectionId, packName) = entry { + case let .permissions(section, count, viewType): + if case .permissions(section, count, viewType) = entry { return true } else { return false } - case .convertToSuperGroup: - if case .convertToSuperGroup = entry { + case let .blocked(section, count, viewType): + if case .blocked(section, count, viewType) = entry { return true } else { return false } - case let .notifications(_, lhsSettings): + case let .groupStickerset(sectionId, packName, viewType): + if case .groupStickerset(sectionId, packName, viewType) = entry { + return true + } else { + return false + } + case let .notifications(section, lhsSettings, viewType): switch entry { - case let .notifications(_, rhsSettings): - + case .notifications(section, let rhsSettings, viewType): + if let lhsSettings = lhsSettings, let rhsSettings = rhsSettings { return lhsSettings.isEqual(to: rhsSettings) } else if (lhsSettings != nil) != (rhsSettings != nil) { @@ -694,59 +1171,69 @@ enum GroupInfoEntry: PeerInfoEntry { default: return false } - case let .groupTypeSetup(_, isPublic): - if case .groupTypeSetup(_, isPublic) = entry { + case let .groupTypeSetup(sectionId, isPublic, viewType): + if case .groupTypeSetup(sectionId, isPublic, viewType) = entry { return true } else { return false } - case .groupDescriptionSetup: - if case .groupDescriptionSetup = entry { + case let .autoDeleteMessages(sectionId, value, viewType): + if case .autoDeleteMessages(sectionId, value, viewType) = entry { return true } else { return false } - case .groupAboutDescription: - if case .groupAboutDescription = entry { + + case let .inviteLinks(sectionId, count, viewType): + if case .inviteLinks(sectionId, count, viewType) = entry { return true } else { return false } - case let .groupManagementInfoLabel(_, text): - if case .groupManagementInfoLabel(_, text) = entry { + case let .linkedChannel(sectionId, lhsChannel, subscribers, viewType): + if case .linkedChannel(sectionId, let rhsChannel, subscribers, viewType) = entry { + return lhsChannel.isEqual(rhsChannel) + } else { + return false + } + case let .groupDescriptionSetup(section, text, viewType): + if case .groupDescriptionSetup(section, text, viewType) = entry { return true } else { return false } - case let .membersAdmins(_, count): - if case .membersAdmins(_, count) = entry { + case let .groupAboutDescription(section, viewType): + if case .groupAboutDescription(section, viewType) = entry { return true } else { return false } - case let .membersBlacklist(_, count): - if case .membersBlacklist(_, count) = entry { + case let .groupManagementInfoLabel(section, text, viewType): + if case .groupManagementInfoLabel(section, text, viewType) = entry { return true } else { return false } - case let .usersHeader(_, count): - if case .usersHeader(_, count) = entry { + case let .usersHeader(section, count, viewType): + if case .usersHeader(section, count, viewType) = entry { return true } else { return false } - case .addMember: - if case .addMember = entry { + case let .addMember(section, inviteViaLink, viewType): + if case .addMember(section, inviteViaLink, viewType) = entry { return true } else { return false } - case let .member(_, lhsIndex, lhsPeerId, lhsPeer, lhsPresence, lhsMemberStatus, lhsEditing, lhsEnabled): - if case let .member(_, rhsIndex, rhsPeerId, rhsPeer, rhsPresence, rhsMemberStatus, rhsEditing, rhsEnabled) = entry { + case let .member(lhsSection, lhsIndex, lhsPeerId, lhsPeer, lhsPresence, lhsActivity, lhsMemberStatus, lhsEditing, lhsMenuItems, lhsEnabled, lhsViewType): + if case let .member(rhsSection, rhsIndex, rhsPeerId, rhsPeer, rhsPresence, rhsActivity, rhsMemberStatus, rhsEditing, rhsMenuItems, rhsEnabled, rhsViewType) = entry { if lhsIndex != rhsIndex { return false } + if lhsSection != rhsSection { + return false + } if lhsMemberStatus != rhsMemberStatus { return false } @@ -756,6 +1243,9 @@ enum GroupInfoEntry: PeerInfoEntry { if lhsEnabled != rhsEnabled { return false } + if lhsViewType != rhsViewType { + return false + } if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer { if !lhsPeer.isEqual(rhsPeer) { return false @@ -763,6 +1253,9 @@ enum GroupInfoEntry: PeerInfoEntry { } else if (lhsPeer != nil) != (rhsPeer != nil) { return false } + if lhsActivity != rhsActivity { + return false + } if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { if !lhsPresence.isEqual(to: rhsPresence) { return false @@ -773,13 +1266,27 @@ enum GroupInfoEntry: PeerInfoEntry { if lhsEditing != rhsEditing { return false } - + if lhsMenuItems != rhsMenuItems { + return false + } return true } else { return false } - case .leave: - if case .leave = entry { + case let .showMore(sectionId, index, viewType): + if case .showMore(sectionId, index, viewType) = entry { + return true + } else { + return false + } + case let .leave(sectionId, text, viewType): + if case .leave(sectionId, text, viewType) = entry { + return true + } else { + return false + } + case let .media(sectionId, _, isVisible, viewType): + if case .media(sectionId, _, isVisible, viewType) = entry { return true } else { return false @@ -796,7 +1303,7 @@ enum GroupInfoEntry: PeerInfoEntry { var stableId: PeerInfoEntryStableId { switch self { - case let .member(_, _, peerId, _, _, _, _, _): + case let .member(_, _, peerId, _, _, _, _, _, _, _, _): return GroupPeerEntryStableId(peerId: peerId) default: return IntPeerInfoEntryStableId(value: stableIndex) @@ -807,99 +1314,170 @@ enum GroupInfoEntry: PeerInfoEntry { switch self { case .info: return 0 - case .about: + case .setTitle: return 1 - case .addressName: + case .scam: return 2 - case .setGroupPhoto: + case .about: return 3 - case .inviteLink: + case .addressName: return 4 - case .notifications: + case .groupDescriptionSetup: return 5 - case .sharedMedia: + case .groupAboutDescription: return 6 - case .groupTypeSetup: + case .notifications: return 7 - case .preHistory: + case .sharedMedia: return 8 - case .setAdmins: + case .groupTypeSetup: return 9 - case .groupDescriptionSetup: + case .inviteLinks: return 10 - case .groupAboutDescription: + case .linkedChannel: return 11 - case .groupStickerset: + case .preHistory: return 12 - case .groupManagementInfoLabel: + case .groupStickerset: return 13 - case .membersAdmins: + case .autoDeleteMessages: return 14 - case .membersBlacklist: + case .groupManagementInfoLabel: return 15 - case .usersHeader: + case .permissions: return 16 - case .addMember: + case .blocked: return 17 + case .administrators: + return 18 + case .usersHeader: + return 19 + case .addMember: + return 20 case .member: fatalError("no stableIndex") - case .convertToSuperGroup: - return 18 + case .showMore: + return 22 case .leave: - return 19 + return 23 + case .media: + return 24 case let .section(id): - return (id + 1) * 1000 - id + return (id + 1) * 100000 - id } } - + var sectionId: Int { + switch self { + case let .info(sectionId, _, _, _, _): + return sectionId + case let .scam(sectionId, _, _, _): + return sectionId + case let .about(sectionId, _, _): + return sectionId + case let .addressName(sectionId, _, _): + return sectionId + case let .setTitle(sectionId, _, _): + return sectionId + case let .addMember(sectionId, _, _): + return sectionId + case let .notifications(sectionId, _, _): + return sectionId + case let .sharedMedia(sectionId, _): + return sectionId + case let .groupTypeSetup(sectionId, _, _): + return sectionId + case let .autoDeleteMessages(sectionId, _, _): + return sectionId + case let .inviteLinks(sectionId, _, _): + return sectionId + case let .linkedChannel(sectionId, _, _, _): + return sectionId + case let .preHistory(sectionId, _, _): + return sectionId + case let .groupStickerset(sectionId, _, _): + return sectionId + case let .administrators(sectionId, _, _): + return sectionId + case let .permissions(sectionId, _, _): + return sectionId + case let .blocked(sectionId, _, _): + return sectionId + case let .groupDescriptionSetup(sectionId, _, _): + return sectionId + case let .groupAboutDescription(sectionId, _): + return sectionId + case let .groupManagementInfoLabel(sectionId, _, _): + return sectionId + case let .usersHeader(sectionId, _, _): + return sectionId + case let .member(sectionId, _, _, _, _, _, _, _, _, _, _): + return sectionId + case let .showMore(sectionId, _, _): + return sectionId + case let .leave(sectionId, _, _): + return sectionId + case let .media(sectionId, _, _, _): + return sectionId + case let .section(sectionId): + return sectionId + } + } var sortIndex: Int { switch self { - case let .info(sectionId, _, _, _): - return (sectionId * 1000) + stableIndex - case let .about(sectionId, _): - return (sectionId * 1000) + stableIndex - case let .addressName(sectionId, _): - return (sectionId * 1000) + stableIndex - case let .setGroupPhoto(sectionId): - return (sectionId * 1000) + stableIndex - case let .inviteLink(sectionId): - return (sectionId * 1000) + stableIndex - case let .addMember(sectionId): - return (sectionId * 1000) + stableIndex - case let .notifications(sectionId, _): - return (sectionId * 1000) + stableIndex - case let .sharedMedia(sectionId): - return (sectionId * 1000) + stableIndex - case let .groupTypeSetup(sectionId, _): - return (sectionId * 1000) + stableIndex - case let .preHistory(sectionId, _): - return (sectionId * 1000) + stableIndex - case let .groupStickerset(sectionId, _): - return (sectionId * 1000) + stableIndex - case let .setAdmins(sectionId): - return (sectionId * 1000) + stableIndex - case let .groupDescriptionSetup(sectionId, _): - return (sectionId * 1000) + stableIndex - case let .groupAboutDescription(sectionId): - return (sectionId * 1000) + stableIndex - case let .groupManagementInfoLabel(sectionId, _): - return (sectionId * 1000) + stableIndex - case let .membersAdmins(sectionId, _): - return (sectionId * 1000) + stableIndex - case let .membersBlacklist(sectionId, _): - return (sectionId * 1000) + stableIndex - case let .usersHeader(sectionId, _): - return (sectionId * 1000) + stableIndex - case let .member(sectionId, index, _, _, _, _, _, _): - return (sectionId * 1000) + index + 200 - case let .leave(sectionId): - return (sectionId * 1000) + stableIndex - case let .convertToSuperGroup(sectionId): - return (sectionId * 1000) + stableIndex + case let .info(sectionId, _, _, _, _): + return (sectionId * 100000) + stableIndex + case let .scam(sectionId, _, _, _): + return (sectionId * 100000) + stableIndex + case let .about(sectionId, _, _): + return (sectionId * 100000) + stableIndex + case let .addressName(sectionId, _, _): + return (sectionId * 100000) + stableIndex + case let .setTitle(sectionId, _, _): + return (sectionId * 100000) + stableIndex + case let .addMember(sectionId, _, _): + return (sectionId * 100000) + stableIndex + case let .notifications(sectionId, _, _): + return (sectionId * 100000) + stableIndex + case let .sharedMedia(sectionId, _): + return (sectionId * 100000) + stableIndex + case let .groupTypeSetup(sectionId, _, _): + return (sectionId * 100000) + stableIndex + case let .autoDeleteMessages(sectionId, _, _): + return (sectionId * 100000) + stableIndex + case let .inviteLinks(sectionId, _, _): + return (sectionId * 100000) + stableIndex + case let .linkedChannel(sectionId, _, _, _): + return (sectionId * 100000) + stableIndex + case let .preHistory(sectionId, _, _): + return (sectionId * 100000) + stableIndex + case let .groupStickerset(sectionId, _, _): + return (sectionId * 100000) + stableIndex + case let .administrators(sectionId, _, _): + return (sectionId * 100000) + stableIndex + case let .permissions(sectionId, _, _): + return (sectionId * 100000) + stableIndex + case let .blocked(sectionId, _, _): + return (sectionId * 100000) + stableIndex + case let .groupDescriptionSetup(sectionId, _, _): + return (sectionId * 100000) + stableIndex + case let .groupAboutDescription(sectionId, _): + return (sectionId * 100000) + stableIndex + case let .groupManagementInfoLabel(sectionId, _, _): + return (sectionId * 100000) + stableIndex + case let .usersHeader(sectionId, _, _): + return (sectionId * 100000) + stableIndex + case let .member(sectionId, index, _, _, _, _, _, _, _, _, _): + return (sectionId * 100000) + index + 200 + case let .showMore(sectionId, index, _): + return (sectionId * 100000) + index + 200 + case let .leave(sectionId, _, _): + return (sectionId * 100000) + stableIndex + case let .media(sectionId, _, _, _): + return (sectionId * 100000) + stableIndex case let .section(sectionId): - return (sectionId + 1) * 1000 - sectionId + return (sectionId + 1) * 100000 - sectionId } } @@ -908,274 +1486,352 @@ enum GroupInfoEntry: PeerInfoEntry { return false } - return self.sortIndex > other.sortIndex + return self.sortIndex < other.sortIndex } func item(initialSize:NSSize, arguments:PeerInfoArguments) -> TableRowItem { let arguments = arguments as! GroupInfoArguments - let state = arguments.state as! GroupInfoState - switch self { - case let .info(_, peerView, editable, updatingPhotoState): - return PeerInfoHeaderItem(initialSize, stableId:stableId.hashValue, account: arguments.account, peerView:peerView, editable: editable, updatingPhotoState: updatingPhotoState, firstNameEditableText: state.editingState?.editingName, textChangeHandler: { name, _ in - arguments.updateEditingName(name) - }) - case let .about(_, text): - return TextAndLabelItem(initialSize, stableId: stableId.hashValue, label:tr(.peerInfoInfo), text:text, account: arguments.account, detectLinks:true, openInfo: { peerId, toChat, _, _ in + case let .info(_, peerView, editable, updatingPhotoState, viewType): + return PeerInfoHeadItem(initialSize, stableId: stableId.hashValue, context: arguments.context, arguments: arguments, peerView: peerView, viewType: viewType, editing: editable, updatingPhotoState: updatingPhotoState, updatePhoto: arguments.updateGroupPhoto) + case let .scam(_, title, text, viewType): + return TextAndLabelItem(initialSize, stableId:stableId.hashValue, label: title, copyMenuText: L10n.textCopy, labelColor: theme.colors.redUI, text: text, context: arguments.context, viewType: viewType, detectLinks:false) + case let .about(_, text, viewType): + return TextAndLabelItem(initialSize, stableId: stableId.hashValue, label: L10n.peerInfoInfo, copyMenuText: L10n.textCopyLabelAbout, text: text, context: arguments.context, viewType: viewType, detectLinks: true, openInfo: { [weak arguments] peerId, toChat, postId, _ in if toChat { - arguments.peerChat(peerId) + arguments?.peerChat(peerId, postId: postId) } else { - arguments.peerInfo(peerId) + arguments?.peerInfo(peerId) } - }, hashtag: arguments.account.context.globalSearch) - case let .addressName(_, value): + }, hashtag: arguments.context.sharedContext.bindings.globalSearch) + case let .addressName(_, value, viewType): let link = "https://t.me/\(value)" - return TextAndLabelItem(initialSize, stableId: stableId.hashValue, label:tr(.peerInfoSharelink), text: link, account: arguments.account, isTextSelectable:false, callback:{ - showModal(with: ShareModalController(ShareLinkObject(arguments.account, link: link)), for: mainWindow) + return TextAndLabelItem(initialSize, stableId: stableId.hashValue, label: L10n.peerInfoSharelink, copyMenuText: L10n.textCopyLabelShareLink, text: link, context: arguments.context, viewType: viewType, isTextSelectable:false, callback:{ + showModal(with: ShareModalController(ShareLinkObject(arguments.context, link: link)), for: mainWindow) + }, selectFullWord: true, _copyToClipboard: { + arguments.copy(link) }) - case .setGroupPhoto: - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoSetGroupPhoto), nameStyle: blueActionButton, type: .none, action: { - - pickImage(for: mainWindow, completion: { image in - if let image = image { - _ = (putToTemp(image: image) |> deliverOnMainQueue).start(next: { path in - arguments.updatePhoto(path) - }) + case let .setTitle(_, text, viewType): + return InputDataRowItem(initialSize, stableId: stableId.hashValue, mode: .plain, error: nil, viewType: viewType, currentText: text, placeholder: nil, inputPlaceholder: L10n.peerInfoGroupTitlePleceholder, filter: { $0 }, updated: arguments.updateEditingName, limit: 255) + case let .notifications(_, settings, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoNotifications, type: .switchable(!((settings as? TelegramPeerNotificationSettings)?.isMuted ?? true)), viewType: viewType, action: {}) + + case let .sharedMedia(_, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoSharedMedia, type: .next, viewType: viewType, action: arguments.sharedMedia) + case let .groupDescriptionSetup(section: _, text, viewType): + return InputDataRowItem(initialSize, stableId: stableId.hashValue, mode: .plain, error: nil, viewType: viewType, currentText: text, placeholder: nil, inputPlaceholder: L10n.peerInfoAboutPlaceholder, filter: { $0 }, updated: arguments.updateEditingDescriptionText, limit: 255) + case let .preHistory(_, enabled, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoPreHistory, icon: theme.icons.profile_group_discussion, type: .context(enabled ? L10n.peerInfoPreHistoryVisible : L10n.peerInfoPreHistoryHidden), viewType: viewType, action: arguments.preHistorySetup) + case let .groupAboutDescription(_, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId.hashValue, text: L10n.peerInfoSetAboutDescription, viewType: viewType) + case let .groupTypeSetup(section: _, isPublic: isPublic, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoGroupType, icon: theme.icons.profile_group_type, type: .nextContext(isPublic ? L10n.peerInfoGroupTypePublic : L10n.peerInfoGroupTypePrivate), viewType: viewType, action: arguments.visibilitySetup) + case let .autoDeleteMessages(section: _, timer, viewType): + + let text: String + if let timer = timer { + switch timer { + case let .known(timer): + if let timer = timer?.effectiveValue { + text = autoremoveLocalized(Int(timer)) + } else { + text = L10n.peerInfoGroupTimerNever } - }) - - }) - case let .notifications(_, settings): - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoNotifications), type: .switchable(stateback: { () -> Bool in - - if let settings = settings as? TelegramPeerNotificationSettings, case .muted = settings.muteState { - return false - } else { - return true + case .unknown: + text = "" } - - }), action: { - arguments.toggleNotifications() - }) - - case .sharedMedia: - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoSharedMedia), type: .none, action: { () in - arguments.sharedMedia() - }) - case let .groupDescriptionSetup(section: _, text: text): - return GeneralInputRowItem(initialSize, stableId: stableId.hashValue, placeholder: tr(.peerInfoAboutPlaceholder), text: text, limit: 255, insets: NSEdgeInsets(left:25,right:25,top:8,bottom:3), textChangeHandler: { updatedText in - arguments.updateEditingDescriptionText(updatedText) - }) - case let .preHistory(_, enabled): - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoPreHistory), type: .context(stateback: { () -> String in - return enabled ? tr(.peerInfoPreHistoryVisible) : tr(.peerInfoPreHistoryHidden) - }), action: { () in - arguments.preHistorySetup() - }) - case .groupAboutDescription: - return GeneralTextRowItem(initialSize, stableId: stableId.hashValue, text: tr(.peerInfoSetAboutDescription)) + } else { + text = "" + } - case let .groupTypeSetup(section: _, isPublic: isPublic): - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoGroupType), type: .context(stateback: { () -> String in - return isPublic ? tr(.peerInfoGroupTypePublic) : tr(.peerInfoGroupTypePrivate) - }), action: { () in - arguments.visibilitySetup() - }) - case .setAdmins: - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoSetAdmins), type: .none, action: { () in - arguments.setGroupAdmins() - }) - case .groupStickerset(_, let name): - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoSetGroupStickersSet), type: .context(stateback: { - return name - }), action: { () in - arguments.setGroupStickerset() - }) - case let .membersBlacklist(section: _, count: count): - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoBlackList), type: .context(stateback: { () -> String in - return "\(count)" - }), action: { () in - arguments.blacklist() - }) - case let .membersAdmins(section: _, count: count): - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoAdmins), type: .context(stateback: { () -> String in - return "\(count)" - }), action: { () in - arguments.admins() - }) - case let .usersHeader(section: _, count: count): - return GeneralTextRowItem(initialSize, stableId: stableId.hashValue, text: tr(.peerInfoMembersHeaderCountable(count))) - case .addMember: - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoAddMember), nameStyle: blueActionButton, type: .none, action: { () in - - arguments.addMember() - - }, thumb: GeneralThumbAdditional(thumb: theme.icons.peerInfoAddMember, textInset: 36), inset:NSEdgeInsets(left: 40, right: 30)) - case .inviteLink: - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoInviteLink), nameStyle: blueActionButton, type: .none, action: { () in - arguments.invation() - }) - - case let .member(_, _, _, peer, presence, memberStatus, editing, enabled): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoGroupAutoDeleteMessages, icon: theme.icons.profile_group_destruct, type: .nextContext(text), viewType: viewType, action: arguments.autoremoveController) + case let .inviteLinks(_, count, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoInviteLinks, icon: theme.icons.profile_links, type: .nextContext(count > 0 ? "\(count)" : ""), viewType: viewType, action: arguments.openInviteLinks) + case let .linkedChannel(_, channel, _, viewType): + let title: String + if let address = channel.addressName { + title = "@\(address)" + } else { + title = channel.displayTitle + } + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoLinkedChannel, icon: theme.icons.profile_group_discussion, type: .nextContext(title), viewType: viewType, action: arguments.setupDiscussion) + case let .groupStickerset(_, name, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoSetGroupStickersSet, icon: theme.icons.settingsStickers, type: .nextContext(name), viewType: viewType, action: arguments.setGroupStickerset) + case let .permissions(section: _, count, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoPermissions, icon: theme.icons.peerInfoPermissions, type: .nextContext(count), viewType: viewType, action: arguments.blacklist) + case let .blocked(section: _, count, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoBlackList, icon: theme.icons.peerInfoBanned, type: .nextContext(count != nil && count! > 0 ? "\(count!)" : ""), viewType: viewType, action: arguments.blocked) + case let .administrators(section: _, count, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoAdministrators, icon: theme.icons.peerInfoAdmins, type: .nextContext(count), viewType: viewType, action: arguments.admins) + case let .usersHeader(section: _, count, viewType): + var countValue = L10n.peerInfoMembersHeaderCountable(count) + countValue = countValue.replacingOccurrences(of: "\(count)", with: count.separatedNumber) + return GeneralTextRowItem(initialSize, stableId: stableId.hashValue, text: countValue, viewType: viewType) + case let .addMember(_, inviteViaLink, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoAddMember, nameStyle: blueActionButton, type: .none, viewType: viewType, action: { [weak arguments] in + arguments?.addMember(inviteViaLink) + }, thumb: GeneralThumbAdditional(thumb: theme.icons.peerInfoAddMember, textInset: 52, thumbInset: 5)) + case let .member(_, _, _, peer, presence, inputActivity, memberStatus, editing, menuItems, enabled, viewType): let label: String switch memberStatus { - case .admin: - label = tr(.peerInfoAdminLabel) + case let .admin(rank): + label = rank case .member: label = "" } - - var string:String = tr(.peerStatusRecently) + + var string:String = L10n.peerStatusRecently var color:NSColor = theme.colors.grayText - if let presence = presence as? TelegramUserPresence { + if let peer = peer as? TelegramUser, let botInfo = peer.botInfo { + string = botInfo.flags.contains(.hasAccessToChatHistory) ? L10n.peerInfoBotStatusHasAccess : L10n.peerInfoBotStatusHasNoAccess + } else if let presence = presence as? TelegramUserPresence { let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - (string, _, color) = stringAndActivityForUserPresence(presence, relativeTo: Int32(timestamp)) - } else if let peer = peer as? TelegramUser, let botInfo = peer.botInfo { - string = botInfo.flags.contains(.hasAccessToChatHistory) ? tr(.peerInfoBotStatusHasAccess) : tr(.peerInfoBotStatusHasNoAccess) + (string, _, color) = stringAndActivityForUserPresence(presence, timeDifference: arguments.context.timeDifference, relativeTo: Int32(timestamp)) } let interactionType:ShortPeerItemInteractionType if let editing = editing { - interactionType = .deletable(onRemove: { memberId in - arguments.removePeer(memberId) + interactionType = .deletable(onRemove: { [weak arguments] memberId in + arguments?.removePeer(memberId) }, deletable: editing.editable) } else { interactionType = .plain } - return ShortPeerRowItem(initialSize, peer: peer!, account: arguments.account, stableId: stableId.hashValue, enabled: enabled, height: 46, photoSize: NSMakeSize(36, 36), titleStyle: ControlStyle(font: .medium(.custom(12.5)), foregroundColor: theme.colors.text), statusStyle: ControlStyle(font: NSFont.normal(.custom(12.5)), foregroundColor:color), status: string, inset:NSEdgeInsets(left:30.0,right:30.0), interactionType: interactionType, generalType:.context( stateback: { - return label - }), action:{ - arguments.peerInfo(peer!.id) - }) - - case .leave: - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoDeleteAndExit), nameStyle: redActionButton, type: .none, action: { - arguments.delete() - }) - case .convertToSuperGroup: - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoConvertToSupergroup), nameStyle: blueActionButton, type: .none, action: { () in - arguments.convert() - }) - case .section(_): - return GeneralRowItem(initialSize, height:20, stableId: stableId.hashValue) + return ShortPeerRowItem(initialSize, peer: peer!, account: arguments.context.account, stableId: stableId.hashValue, enabled: enabled, height: 50, photoSize: NSMakeSize(36, 36), titleStyle: ControlStyle(font: .medium(12.5), foregroundColor: theme.colors.text), statusStyle: ControlStyle(font: NSFont.normal(12.5), foregroundColor:color), status: string, inset: NSEdgeInsets(left:30.0,right:30.0), interactionType: interactionType, generalType: .context(label), viewType: viewType, action: { [weak arguments] in + arguments?.peerInfo(peer!.id) + }, contextMenuItems: { + return .single(menuItems) + }, inputActivity: inputActivity) + case let .showMore(_, _, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoShowMore, nameStyle: blueActionButton, type: .none, viewType: viewType, action: arguments.showMore, thumb: GeneralThumbAdditional(thumb: theme.icons.chatSearchUp, textInset: 52, thumbInset: 4)) + case let .leave(_, text, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: text, nameStyle: redActionButton, type: .none, viewType: viewType, action: arguments.delete) + case let .media(_, controller, isVisible, viewType): + return PeerMediaBlockRowItem(initialSize, stableId: stableId.hashValue, controller: controller, isVisible: isVisible, viewType: viewType) + case .section: + return GeneralRowItem(initialSize, height: 30, stableId: stableId.hashValue, viewType: .separator) default: preconditionFailure() } } } +enum GroupInfoSection : Int { + case header = 1 + case info = 2 + case desc = 3 + case action = 4 + case addition = 5 + case type = 6 + case admin = 7 + case members = 8 + case destruct = 9 + case media = 10 +} - -func groupInfoEntries(view: PeerView, arguments: PeerInfoArguments) -> [PeerInfoEntry] { - var entries: [PeerInfoEntry] = [] +func groupInfoEntries(view: PeerView, arguments: PeerInfoArguments, inputActivities: [PeerId: PeerInputActivity], channelMembers: [RenderedChannelParticipant] = [], mediaTabsData: PeerMediaTabsData, inviteLinksCount: Int32) -> [PeerInfoEntry] { + var entries: [GroupInfoEntry] = [] if let group = peerViewMainPeer(view), let arguments = arguments as? GroupInfoArguments, let state = arguments.state as? GroupInfoState { - var sectionId:Int = 0 - let access = group.groupAccess - var canInviteByLink = access.isCreator + let access = group.groupAccess - var canEditInfo = state.editingState != nil - if let group = group as? TelegramChannel { - canEditInfo = state.editingState != nil && group.hasAdminRights(.canChangeInfo) - canInviteByLink = group.hasAdminRights(.canChangeInviteLink) - } - entries.append(GroupInfoEntry.info(section: sectionId, view: view, editable: canEditInfo, updatingPhotoState: state.updatingPhotoState)) + var infoBlock: [GroupInfoEntry] = [] + func applyBlock(_ block:[GroupInfoEntry]) { + var block = block + for (i, item) in block.enumerated() { + block[i] = item.withUpdatedViewType(bestGeneralViewType(block, for: i)) + } + entries.append(contentsOf: block) + } - + infoBlock.append(.info(section: GroupInfoSection.header.rawValue, view: view, editingState: state.editingState != nil, updatingPhotoState: state.updatingPhotoState, viewType: .singleItem)) if let editingState = state.editingState { - if canEditInfo { - entries.append(GroupInfoEntry.setGroupPhoto(section: sectionId)) - } - if canInviteByLink { - entries.append(GroupInfoEntry.inviteLink(section: sectionId)) + if access.canEditGroupInfo { + infoBlock.append(GroupInfoEntry.setTitle(section: GroupInfoSection.header.rawValue, text: editingState.editingName ?? group.displayTitle, viewType: .singleItem)) + + infoBlock.append(GroupInfoEntry.groupDescriptionSetup(section: GroupInfoSection.header.rawValue, text: editingState.editingDescriptionText, viewType: .singleItem)) + applyBlock(infoBlock) + + entries.append(GroupInfoEntry.groupAboutDescription(section: GroupInfoSection.header.rawValue, viewType: .textBottomItem)) + + + } else { + applyBlock(infoBlock) } - entries.append(GroupInfoEntry.section(sectionId)) - sectionId += 1 - if let cachedChannelData = view.cachedData as? CachedChannelData { - - if access.isCreator { - entries.append(GroupInfoEntry.groupTypeSetup(section: sectionId, isPublic: group.addressName != nil)) - if group.addressName == nil { - entries.append(GroupInfoEntry.preHistory(section: sectionId, enabled: cachedChannelData.flags.contains(.preHistoryEnabled))) + if let group = view.peers[view.peerId] as? TelegramGroup { + let hasAccess: Bool + switch group.role { + case .admin: + hasAccess = true + case .creator: + hasAccess = true + default: + hasAccess = false + } + if case .creator = group.role { + + } + var actionBlock:[GroupInfoEntry] = [] + + switch group.role { + case .admin, .creator: + if case .creator = group.role { + actionBlock.append(.groupTypeSetup(section: GroupInfoSection.type.rawValue, isPublic: group.addressName != nil, viewType: .singleItem)) } + + if case .creator = group.role { + actionBlock.append(.preHistory(section: GroupInfoSection.type.rawValue, enabled: false, viewType: .singleItem)) + } +// actionBlock.append(.autoDeleteMessages(section: GroupInfoSection.type.rawValue, timer: (view.cachedData as? CachedGroupData)?.autoremoveTimeout, viewType: .singleItem)) + applyBlock(actionBlock) + default: + break } - - if canEditInfo { - entries.append(GroupInfoEntry.groupDescriptionSetup(section: sectionId, text: editingState.editingDescriptionText)) - entries.append(GroupInfoEntry.groupAboutDescription(section: sectionId)) - - entries.append(GroupInfoEntry.section(sectionId)) - sectionId += 1 - - if cachedChannelData.flags.contains(.canSetStickerSet) { - entries.append(GroupInfoEntry.groupStickerset(section: sectionId, packName: cachedChannelData.stickerPack?.title ?? "")) - - entries.append(GroupInfoEntry.section(sectionId)) - sectionId += 1 + + if hasAccess { + var activePermissionCount: Int? + if let defaultBannedRights = group.defaultBannedRights { + var count = 0 + for right in allGroupPermissionList { + if !defaultBannedRights.flags.contains(right) { + count += 1 + } + } + activePermissionCount = count } + entries.append(.inviteLinks(section: GroupInfoSection.type.rawValue, count: inviteLinksCount, viewType: .firstItem)) + + entries.append(GroupInfoEntry.permissions(section: GroupInfoSection.admin.rawValue, count: activePermissionCount.flatMap({ "\($0)/\(allGroupPermissionList.count)" }) ?? "", viewType: .innerItem)) + entries.append(GroupInfoEntry.administrators(section: GroupInfoSection.admin.rawValue, count: "", viewType: .lastItem)) } + } else if let channel = view.peers[view.peerId] as? TelegramChannel, let cachedChannelData = view.cachedData as? CachedChannelData { + var actionBlock:[GroupInfoEntry] = [] - if access.canManageGroup { - let adminCount = cachedChannelData.participantsSummary.adminCount ?? 0 - entries.append(GroupInfoEntry.membersAdmins(section: sectionId, count: Int(adminCount))) - let bannedCount = (cachedChannelData.participantsSummary.bannedCount ?? 0) + (cachedChannelData.participantsSummary.kickedCount ?? 0) - entries.append(GroupInfoEntry.membersBlacklist(section: sectionId, count: Int(bannedCount))) - + if access.isCreator { + actionBlock.append(.groupTypeSetup(section: GroupInfoSection.type.rawValue, isPublic: group.addressName != nil, viewType: .singleItem)) } - } else if group.isGroup { - if access.isCreator { - entries.append(GroupInfoEntry.setAdmins(section: sectionId)) + + if (channel.adminRights != nil || channel.flags.contains(.isCreator)), let linkedDiscussionPeerId = cachedChannelData.linkedDiscussionPeerId.peerId, let peer = view.peers[linkedDiscussionPeerId] { + actionBlock.append(.linkedChannel(section: GroupInfoSection.type.rawValue, channel: peer, subscribers: cachedChannelData.participantsSummary.memberCount, viewType: .singleItem)) + } else if channel.hasPermission(.banMembers) { + if !access.isPublic { + actionBlock.append(.preHistory(section: GroupInfoSection.type.rawValue, enabled: cachedChannelData.flags.contains(.preHistoryEnabled), viewType: .singleItem)) + } + } + + if cachedChannelData.flags.contains(.canSetStickerSet) && access.canEditGroupInfo { + actionBlock.append(.groupStickerset(section: GroupInfoSection.type.rawValue, packName: cachedChannelData.stickerPack?.title ?? "", viewType: .singleItem)) + } +// if access.canEditGroupInfo { +// actionBlock.append(.autoDeleteMessages(section: GroupInfoSection.type.rawValue, timer: cachedChannelData.autoremoveTimeout, viewType: .singleItem)) +// } + applyBlock(actionBlock) + + var canViewAdminsAndBanned = false + if let channel = view.peers[view.peerId] as? TelegramChannel { + if let _ = channel.adminRights { + canViewAdminsAndBanned = true + } else if channel.flags.contains(.isCreator) { + canViewAdminsAndBanned = true + } + } + + if canViewAdminsAndBanned { + var block: [GroupInfoEntry] = [] + var activePermissionCount: Int? + if let defaultBannedRights = channel.defaultBannedRights { + var count = 0 + for right in allGroupPermissionList { + if !defaultBannedRights.flags.contains(right) { + count += 1 + } + } + activePermissionCount = count + } + + if (access.isCreator || access.canCreateInviteLink) { + block.append(.inviteLinks(section: GroupInfoSection.admin.rawValue, count: inviteLinksCount, viewType: .singleItem)) + } + + if !channel.flags.contains(.isGigagroup) { + if access.canEditMembers { + block.append(.permissions(section: GroupInfoSection.admin.rawValue, count: activePermissionCount.flatMap({ "\($0)/\(allGroupPermissionList.count)" }) ?? "", viewType: .singleItem)) + } + } else { + block.append(.blocked(section: GroupInfoSection.admin.rawValue, count: cachedChannelData.participantsSummary.kickedCount, viewType: .singleItem)) + } + block.append(.administrators(section: GroupInfoSection.admin.rawValue, count: cachedChannelData.participantsSummary.adminCount.flatMap { "\($0)" } ?? "", viewType: .lastItem)) + + applyBlock(block) + } } + } else { + applyBlock(infoBlock) + + + + var aboutBlock:[GroupInfoEntry] = [] + + if group.isScam { + aboutBlock.append(GroupInfoEntry.scam(section: GroupInfoSection.desc.rawValue, title: L10n.peerInfoScam, text: L10n.groupInfoScamWarning, viewType: .singleItem)) + } else if group.isFake { + aboutBlock.append(GroupInfoEntry.scam(section: GroupInfoSection.desc.rawValue, title: L10n.peerInfoFake, text: L10n.groupInfoFakeWarning, viewType: .singleItem)) + } + if let cachedChannelData = view.cachedData as? CachedChannelData { - if let about = cachedChannelData.about, !about.isEmpty { - entries.append(GroupInfoEntry.about(section: sectionId, text: about)) + if let about = cachedChannelData.about, !about.isEmpty, !group.isScam && !group.isFake { + aboutBlock.append(GroupInfoEntry.about(section: GroupInfoSection.desc.rawValue, text: about, viewType: .singleItem)) } } - if let addressName = group.addressName { - entries.append(GroupInfoEntry.addressName(section: sectionId, name: addressName)) + + if let cachedGroupData = view.cachedData as? CachedGroupData { + if let about = cachedGroupData.about, !about.isEmpty, !group.isScam { + aboutBlock.append(GroupInfoEntry.about(section: GroupInfoSection.desc.rawValue, text: about, viewType: .singleItem)) + } } - if entries.count > 1 { - entries.append(GroupInfoEntry.section(sectionId)) - sectionId += 1 + if let addressName = group.addressName { + aboutBlock.append(GroupInfoEntry.addressName(section: GroupInfoSection.desc.rawValue, name: addressName, viewType: .singleItem)) } - entries.append(GroupInfoEntry.sharedMedia(section: sectionId)) - } - - entries.append(GroupInfoEntry.notifications(section: sectionId, settings: view.notificationSettings)) + applyBlock(aboutBlock) + + } + - if let cachedGroupData = view.cachedData as? CachedGroupData, let participants = cachedGroupData.participants { + + if let participants = (view.cachedData as? CachedGroupData)?.participants, participants.participants.count <= minumimUsersBlock, let group = peerViewMainPeer(view) as? TelegramGroup { - entries.append(GroupInfoEntry.section(sectionId)) - sectionId = 10 + // entries.append(GroupInfoEntry.usersHeader(section: GroupInfoSection.members.rawValue, count: participants.participants.count, viewType: .textTopItem)) - entries.append(GroupInfoEntry.usersHeader(section: sectionId, count: participants.participants.count)) + var usersBlock:[GroupInfoEntry] = [] - if access.canManageMembers { - entries.append(GroupInfoEntry.addMember(section: sectionId)) - } +// if access.canAddMembers { +// usersBlock.append(.addMember(section: GroupInfoSection.members.rawValue, inviteViaLink: access.canCreateInviteLink, viewType: .singleItem)) +// } var updatedParticipants = participants.participants let existingParticipantIds = Set(updatedParticipants.map { $0.peerId }) + + var peerPresences: [PeerId: PeerPresence] = view.peerPresences var peers: [PeerId: Peer] = view.peers var disabledPeerIds = state.removingParticipantIds @@ -1183,7 +1839,7 @@ func groupInfoEntries(view: PeerView, arguments: PeerInfoArguments) -> [PeerInfo if !state.temporaryParticipants.isEmpty { for participant in state.temporaryParticipants { if !existingParticipantIds.contains(participant.peer.id) { - updatedParticipants.append(.member(id: participant.peer.id, invitedBy: arguments.account.peerId, invitedAt: participant.timestamp)) + updatedParticipants.append(.member(id: participant.peer.id, invitedBy: arguments.context.account.peerId, invitedAt: participant.timestamp)) if let presence = participant.presence, peerPresences[participant.peer.id] == nil { peerPresences[participant.peer.id] = presence } @@ -1195,9 +1851,19 @@ func groupInfoEntries(view: PeerView, arguments: PeerInfoArguments) -> [PeerInfo } } - let sortedParticipants = participants.participants.sorted(by: { lhs, rhs in + let sortedParticipants = participants.participants.filter({peers[$0.peerId]?.displayTitle != nil}).sorted(by: { lhs, rhs in let lhsPresence = view.peerPresences[lhs.peerId] as? TelegramUserPresence let rhsPresence = view.peerPresences[rhs.peerId] as? TelegramUserPresence + + let lhsActivity = inputActivities[lhs.peerId] + let rhsActivity = inputActivities[rhs.peerId] + + if lhsActivity != nil && rhsActivity == nil { + return true + } else if rhsActivity != nil && lhsActivity == nil { + return false + } + if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { return lhsPresence.status > rhsPresence.status } else if let _ = lhsPresence { @@ -1214,8 +1880,10 @@ func groupInfoEntries(view: PeerView, arguments: PeerInfoArguments) -> [PeerInfo let memberStatus: GroupInfoMemberStatus if access.highlightAdmins { switch sortedParticipants[i] { - case .admin, .creator: - memberStatus = .admin + case .admin: + memberStatus = .admin(rank: L10n.chatAdminBadge) + case .creator: + memberStatus = .admin(rank: L10n.chatOwnerBadge) case .member: memberStatus = .member } @@ -1223,36 +1891,85 @@ func groupInfoEntries(view: PeerView, arguments: PeerInfoArguments) -> [PeerInfo memberStatus = .member } + var canRestrict: Bool + if sortedParticipants[i].peerId == arguments.context.peerId { + canRestrict = false + } else { + switch group.role { + case .creator: + canRestrict = true + case .member: + switch sortedParticipants[i] { + case .creator, .admin: + canRestrict = false + case let .member(member): + if member.invitedBy == arguments.context.peerId { + canRestrict = true + } else { + canRestrict = false + } + } + case .admin: + switch sortedParticipants[i] { + case .creator, .admin: + canRestrict = false + case .member: + canRestrict = true + } + } + } + + + let editing:ShortPeerDeleting? - if state.editingState != nil, let group = group as? TelegramGroup { - let deletable:Bool = group.canRemoveParticipant(sortedParticipants[i]) + if state.editingState != nil { + let deletable:Bool = group.canRemoveParticipant(sortedParticipants[i]) || (sortedParticipants[i].invitedBy == arguments.context.peerId && sortedParticipants[i].peerId != arguments.context.peerId) editing = ShortPeerDeleting(editable: deletable) } else { editing = nil } - entries.append(GroupInfoEntry.member(section: sectionId, index: i, peerId: peer.id, peer: peer, presence: view.peerPresences[peer.id], memberStatus: memberStatus, editing: editing, enabled: !disabledPeerIds.contains(peer.id))) + var menuItems:[ContextMenuItem] = [] + + + if canRestrict { + menuItems.append(ContextMenuItem(L10n.peerInfoGroupMenuDelete, handler: { + arguments.removePeer(sortedParticipants[i].peerId) + })) + } + + + usersBlock.append(.member(section: GroupInfoSection.members.rawValue, index: i, peerId: peer.id, peer: peer, presence: view.peerPresences[peer.id], activity: inputActivities[peer.id], memberStatus: memberStatus, editing: editing, menuItems: menuItems, enabled: !disabledPeerIds.contains(peer.id), viewType: .singleItem)) } } + + if usersBlock.count <= minumimUsersBlock { + applyBlock(usersBlock) + } } - if let cachedGroupData = view.cachedData as? CachedChannelData, let participants = cachedGroupData.topParticipants, let channel = group as? TelegramChannel { + if channelMembers.count <= minumimUsersBlock, let channel = peerViewMainPeer(view) as? TelegramChannel { - var updatedParticipants = participants.participants - let existingParticipantIds = Set(updatedParticipants.map { $0.peerId }) + let participants = channelMembers + + var updatedParticipants = participants + let existingParticipantIds = Set(updatedParticipants.map { $0.peer.id }) var peerPresences: [PeerId: PeerPresence] = view.peerPresences var peers: [PeerId: Peer] = view.peers var disabledPeerIds = state.removingParticipantIds + if !state.temporaryParticipants.isEmpty { for participant in state.temporaryParticipants { if !existingParticipantIds.contains(participant.peer.id) { - //member(id: participant.peer.id, invitedAt: participant.timestamp) - updatedParticipants.append(.member(id: participant.peer.id, invitedAt: participant.timestamp, adminInfo: nil, banInfo: nil)) + updatedParticipants.append(RenderedChannelParticipant(participant: .member(id: participant.peer.id, invitedAt: participant.timestamp, adminInfo: nil, banInfo: nil, rank: nil), peer: participant.peer)) if let presence = participant.presence, peerPresences[participant.peer.id] == nil { peerPresences[participant.peer.id] = presence } + if participant.peer.id == arguments.context.account.peerId { + peerPresences[participant.peer.id] = TelegramUserPresence(status: .present(until: Int32.max), lastActivity: Int32.max) + } if peers[participant.peer.id] == nil { peers[participant.peer.id] = participant.peer } @@ -1261,20 +1978,22 @@ func groupInfoEntries(view: PeerView, arguments: PeerInfoArguments) -> [PeerInfo } } - entries.append(GroupInfoEntry.section(sectionId)) - sectionId = 10 - - if let membersCount = cachedGroupData.participantsSummary.memberCount { - entries.append(GroupInfoEntry.usersHeader(section: sectionId, count: Int(membersCount))) - } - - if channel.hasAdminRights(.canInviteUsers) { - entries.append(GroupInfoEntry.addMember(section: sectionId)) - } + var usersBlock:[GroupInfoEntry] = [] + - let sortedParticipants = participants.participants.sorted(by: { lhs, rhs in - let lhsPresence = view.peerPresences[lhs.peerId] as? TelegramUserPresence - let rhsPresence = view.peerPresences[rhs.peerId] as? TelegramUserPresence + var sortedParticipants = participants.filter({!$0.peer.rawDisplayTitle.isEmpty}).sorted(by: { lhs, rhs in + let lhsPresence = lhs.presences[lhs.peer.id] as? TelegramUserPresence + let rhsPresence = rhs.presences[rhs.peer.id] as? TelegramUserPresence + + let lhsActivity = inputActivities[lhs.peer.id] + let rhsActivity = inputActivities[rhs.peer.id] + + if lhsActivity != nil && rhsActivity == nil { + return true + } else if rhsActivity != nil && lhsActivity == nil { + return false + } + if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { return lhsPresence.status > rhsPresence.status } else if let _ = lhsPresence { @@ -1287,49 +2006,126 @@ func groupInfoEntries(view: PeerView, arguments: PeerInfoArguments) -> [PeerInfo }) for i in 0 ..< sortedParticipants.count { - if let peer = view.peers[sortedParticipants[i].peerId] { - let memberStatus: GroupInfoMemberStatus - if access.highlightAdmins { - switch sortedParticipants[i] { - case .creator: - memberStatus = .admin - case .member(_, _, let adminRights, _): - memberStatus = adminRights != nil ? .admin : .member + let memberStatus: GroupInfoMemberStatus + if access.highlightAdmins { + switch sortedParticipants[i].participant { + case let .creator(_, _, rank): + memberStatus = .admin(rank: rank ?? L10n.chatOwnerBadge) + case let .member(_, _, adminRights, _, rank): + memberStatus = adminRights != nil ? .admin(rank: rank ?? L10n.chatAdminBadge) : .member + } + } else { + memberStatus = .member + } + + var canPromote: Bool + var canRestrict: Bool + if sortedParticipants[i].peer.id == arguments.context.peerId { + canPromote = false + canRestrict = false + } else { + switch sortedParticipants[i].participant { + case .creator: + canPromote = false + canRestrict = false + case let .member(_, _, adminRights, bannedRights, _): + if channel.hasPermission(.addAdmins) { + canPromote = true + } else { + canPromote = false + } + if channel.hasPermission(.banMembers) { + canRestrict = true + } else { + canRestrict = false + } + if canPromote { + if let bannedRights = bannedRights { + if bannedRights.restrictedBy != arguments.context.peerId && !channel.flags.contains(.isCreator) { + canPromote = false + } + } + } + if canRestrict { + if let adminRights = adminRights { + if adminRights.promotedBy != arguments.context.peerId && !channel.flags.contains(.isCreator) { + canRestrict = false + } + } } - } else { - memberStatus = .member } - - let editing:ShortPeerDeleting? - - if state.editingState != nil, let group = group as? TelegramChannel { - let deletable:Bool = group.canRemoveParticipant(sortedParticipants[i], accountId: arguments.account.peerId) - editing = ShortPeerDeleting(editable: deletable) + } + + var menuItems:[ContextMenuItem] = [] + + + if canPromote { + menuItems.append(ContextMenuItem(L10n.peerInfoGroupMenuPromote, handler: { + arguments.promote(sortedParticipants[i].participant) + })) + } + if canRestrict { + if let group = group as? TelegramChannel, group.flags.contains(.isGigagroup) { } else { - editing = nil + menuItems.append(ContextMenuItem(L10n.peerInfoGroupMenuRestrict, handler: { + arguments.restrict(sortedParticipants[i].participant) + })) } - - entries.append(GroupInfoEntry.member(section: sectionId, index: i, peerId: peer.id, peer: peer, presence: view.peerPresences[peer.id], memberStatus: memberStatus, editing: editing, enabled: !disabledPeerIds.contains(peer.id))) + menuItems.append(ContextMenuItem(L10n.peerInfoGroupMenuDelete, handler: { + arguments.removePeer(sortedParticipants[i].peer.id) + })) } + + + let editing:ShortPeerDeleting? + + if state.editingState != nil, let group = group as? TelegramChannel { + let deletable:Bool = group.canRemoveParticipant(sortedParticipants[i].participant, accountId: arguments.context.account.peerId) + editing = ShortPeerDeleting(editable: deletable) + } else { + editing = nil + } + + usersBlock.append(GroupInfoEntry.member(section: GroupInfoSection.members.rawValue, index: i, peerId: sortedParticipants[i].peer.id, peer: sortedParticipants[i].peer, presence: sortedParticipants[i].presences[sortedParticipants[i].peer.id], activity: inputActivities[sortedParticipants[i].peer.id], memberStatus: memberStatus, editing: editing, menuItems: menuItems, enabled: !disabledPeerIds.contains(sortedParticipants[i].peer.id), viewType: .singleItem)) + } + if usersBlock.count <= minumimUsersBlock { + applyBlock(usersBlock) } } - entries.append(GroupInfoEntry.section(sectionId)) - sectionId += 1 - if let group = peerViewMainPeer(view) as? TelegramGroup { - if case .Member = group.membership { - if state.editingState != nil && access.isCreator { - entries.append(GroupInfoEntry.convertToSuperGroup(section: sectionId)) - } - entries.append(GroupInfoEntry.leave(section: sectionId)) - } - } else if let channel = peerViewMainPeer(view) as? TelegramChannel { + var destructBlock:[GroupInfoEntry] = [] + + if let channel = peerViewMainPeer(view) as? TelegramChannel { if case .member = channel.participationStatus { - entries.append(GroupInfoEntry.leave(section: sectionId)) + if state.editingState != nil, access.isCreator { + destructBlock.append(GroupInfoEntry.leave(section: GroupInfoSection.destruct.rawValue, text: L10n.peerInfoDeleteGroup, viewType: .singleItem)) + } } } + applyBlock(destructBlock) + + if mediaTabsData.loaded && !mediaTabsData.collections.isEmpty, let controller = arguments.mediaController() { + entries.append(.media(section: GroupInfoSection.media.rawValue, controller: controller, isVisible: state.editingState == nil, viewType: .singleItem)) + } + + var items:[GroupInfoEntry] = [] + var sectionId:Int = 0 + for entry in entries { + if entry.sectionId == GroupInfoSection.media.rawValue { + sectionId = entry.sectionId + } else if entry.sectionId != sectionId { + items.append(.section(sectionId)) + sectionId = entry.sectionId + } + items.append(entry) + } + sectionId += 1 + items.append(.section(sectionId)) + + entries = items + } diff --git a/Telegram-Mac/GroupNameRowItem.swift b/Telegram-Mac/GroupNameRowItem.swift index de6c85b9f2..8b63fe73a4 100644 --- a/Telegram-Mac/GroupNameRowItem.swift +++ b/Telegram-Mac/GroupNameRowItem.swift @@ -8,48 +8,129 @@ import Cocoa import TGUIKit -class GroupNameRowItem: GeneralInputRowItem { +import TelegramCore + +import Postbox + +class GroupNameRowItem: InputDataRowItem { + + var photo:String? + fileprivate let pickPicture: ((Bool)->Void)? + fileprivate let account: Account + init(_ initialSize: NSSize, stableId: AnyHashable, account: Account, placeholder: String, photo: String? = nil, viewType: GeneralViewType = .legacy, text:String = "", limit: Int32 = 140, textChangeHandler:@escaping(String)->Void = {_ in}, pickPicture: ((Bool)->Void)? = nil) { + self.photo = photo + self.account = account + self.pickPicture = pickPicture + super.init(initialSize, stableId: stableId, mode: .plain, error: nil, viewType: viewType, currentText: text, placeholder: nil, inputPlaceholder: placeholder, filter: { $0 }, updated: textChangeHandler, limit: limit) - init(_ initialSize: NSSize, stableId: AnyHashable, placeholder: String, text:String = "", limit: Int32 = 140, textChangeHandler:@escaping(String)->Void = {_ in}) { - super.init(initialSize, stableId: stableId, placeholder: placeholder, limit: limit, textChangeHandler:textChangeHandler) } override func viewClass() -> AnyClass { return GroupNameRowView.self } + override var textFieldLeftInset: CGFloat { + return 60 + } + override var height: CGFloat { - return 80 + switch viewType { + case .legacy: + return max(80, super.height) + case let .modern(_, insets): + return max(insets.bottom + insets.top + 50, super.height) + } } } - - -class GroupNameRowView : GeneralInputRowView { +class GroupNameRowView : InputDataRowView { private let imageView:ImageView = ImageView() - private let sepator:View = View() + private let photoView: TransformImageView = TransformImageView() + private let tranparentView: View = View() + private let circleView = View(frame: NSMakeRect(0, 0, 50, 50)) required init(frame frameRect: NSRect) { super.init(frame: frameRect) - textView.isSingleLine = true - addSubview(sepator) - addSubview(imageView) + containerView.addSubview(photoView) + containerView.addSubview(tranparentView) + containerView.addSubview(imageView) + containerView.addSubview(circleView) + photoView.setFrameSize(50, 50) + tranparentView.setFrameSize(50, 50) + photoView.animatesAlphaOnFirstTransition = true + tranparentView.layer?.cornerRadius = 25 + circleView.layer?.cornerRadius = 25 + circleView.layer?.borderWidth = .borderSize } override func set(item: TableRowItem, animated: Bool) { super.set(item: item, animated: animated) - sepator.backgroundColor = theme.colors.border + + guard let item = item as? GroupNameRowItem else {return} + + photoView.isHidden = item.photo == nil + imageView.isHidden = item.photo != nil + if let path = item.photo, let image = NSImage(contentsOf: URL(fileURLWithPath: path)) { + + let resource = LocalFileReferenceMediaResource(localFilePath: path, randomId: arc4random64()) + let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [TelegramMediaImageRepresentation(dimensions: PixelDimensions(image.size), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + photoView.setSignal(chatMessagePhoto(account: item.account, imageReference: ImageMediaReference.standalone(media: image), scale: backingScaleFactor), clearInstantly: false, animate: true) + + let arguments = TransformImageArguments(corners: ImageCorners(radius: photoView.frame.width / 2), imageSize: photoView.frame.size, boundingSize: photoView.frame.size, intrinsicInsets: NSEdgeInsets()) + photoView.set(arguments: arguments) + + } + circleView.isHidden = item.photo != nil + tranparentView.backgroundColor = NSColor.clear imageView.image = theme.icons.newChatCamera imageView.sizeToFit() } + override func updateColors() { + super.updateColors() + circleView.layer?.borderColor = theme.colors.grayIcon.cgColor + } + + override func mouseUp(with event: NSEvent) { + let point = containerView.convert(event.locationInWindow, from: nil) + if NSPointInRect(point, photoView.frame) { + if let item = item as? GroupNameRowItem { + if item.photo == nil { + item.pickPicture?(true) + } else { + ContextMenu.show(items: [ContextMenuItem(L10n.peerCreatePeerContextUpdatePhoto, handler: { + item.pickPicture?(true) + }), ContextMenuItem(L10n.peerCreatePeerContextRemovePhoto, handler: { + item.pickPicture?(false) + })], view: photoView, event: event) + } + } + } + } + override func layout() { super.layout() - textView.frame = NSMakeRect(100, 0, frame.width - 140 ,textView.frame.height) - textView.centerY() - imageView.setFrameOrigin(30 + floorToScreenPixels((50 - imageView.frame.width)/2.0), 17 + floorToScreenPixels((50 - imageView.frame.height)/2.0)) - sepator.frame = NSMakeRect(105, textView.frame.maxY - .borderSize, frame.width - 140, .borderSize) + + guard let item = item as? GroupNameRowItem else {return} + + switch item.viewType { + case .legacy: + imageView.setFrameOrigin(30 + floorToScreenPixels(backingScaleFactor, (50 - imageView.frame.width)/2.0), 17 + floorToScreenPixels(backingScaleFactor, (50 - imageView.frame.height)/2.0)) + photoView.setFrameOrigin(30 + floorToScreenPixels(backingScaleFactor, (50 - photoView.frame.width)/2.0), 17 + floorToScreenPixels(backingScaleFactor, (50 - photoView.frame.height)/2.0)) + tranparentView.setFrameOrigin(30 + floorToScreenPixels(backingScaleFactor, (50 - tranparentView.frame.width)/2.0), 17 + floorToScreenPixels(backingScaleFactor, (50 - tranparentView.frame.height)/2.0)) + case let .modern(_, insets): + circleView.setFrameOrigin(insets.left, insets.top) + imageView.setFrameOrigin(insets.left + floorToScreenPixels(backingScaleFactor, (50 - imageView.frame.width) / 2), insets.top + floorToScreenPixels(backingScaleFactor, (50 - imageView.frame.height) / 2)) + photoView.setFrameOrigin(insets.left, insets.top) + tranparentView.setFrameOrigin(insets.left, insets.top) + textView.centerY(x: insets.left + item.textFieldLeftInset - 3) + + } + + } + override var firstResponder: NSResponder? { + return self.textView.inputView } override func textViewTextDidChange(_ string: String) { @@ -57,17 +138,11 @@ class GroupNameRowView : GeneralInputRowView { } override func textViewHeightChanged(_ height: CGFloat, animated: Bool) { - textView._change(pos: NSMakePoint(100, floorToScreenPixels((frame.height - height)/2.0)), animated: animated) super.textViewHeightChanged(height, animated: animated) - sepator._change(pos: NSMakePoint(105, textView.frame.maxY - .borderSize), animated: animated) } override func draw(_ layer: CALayer, in ctx: CGContext) { - ctx.setFillColor(theme.colors.background.cgColor) - ctx.fill(bounds) - ctx.setStrokeColor(theme.colors.grayIcon.cgColor) - ctx.setLineWidth(1.0) - ctx.strokeEllipse(in: NSMakeRect(30, 17, 50, 50)) + super.draw(layer, in: ctx) } required init?(coder: NSCoder) { diff --git a/Telegram-Mac/GroupStickerSetController.swift b/Telegram-Mac/GroupStickerSetController.swift index c76dbaf4f7..499789812b 100644 --- a/Telegram-Mac/GroupStickerSetController.swift +++ b/Telegram-Mac/GroupStickerSetController.swift @@ -8,17 +8,18 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit private final class GroupStickersArguments { - let account: Account + let context: AccountContext let textChanged:(String, String)->Void let installStickerset:((StickerPackCollectionInfo, [ItemCollectionItem], Int32))->Void let openChat:(PeerId)->Void - init(account: Account, textChanged:@escaping(String, String)->Void, installStickerset:@escaping((StickerPackCollectionInfo, [ItemCollectionItem], Int32))->Void, openChat:@escaping(PeerId)->Void) { - self.account = account + init(context: AccountContext, textChanged:@escaping(String, String)->Void, installStickerset:@escaping((StickerPackCollectionInfo, [ItemCollectionItem], Int32))->Void, openChat:@escaping(PeerId)->Void) { + self.context = context self.textChanged = textChanged self.installStickerset = installStickerset self.openChat = openChat @@ -35,41 +36,6 @@ private enum GroupStickersetEntryId: Hashable { var hashValue: Int { return 0 } - - static func ==(lhs: GroupStickersetEntryId, rhs: GroupStickersetEntryId) -> Bool { - switch lhs { - case let .section(index): - if case .section(index) = rhs { - return true - } else { - return false - } - case let .pack(id): - if case .pack(id) = rhs { - return true - } else { - return false - } - case .status: - if case .status = rhs { - return true - } else { - return false - } - case .input: - if case .input = rhs { - return true - } else { - return false - } - case let .description(index): - if case .description(index) = rhs { - return true - } else { - return false - } - } - } } private enum GroupStickersetLoadingStatus : Equatable { @@ -78,44 +44,22 @@ private enum GroupStickersetLoadingStatus : Equatable { case failed } -private func ==(lhs: GroupStickersetLoadingStatus, rhs: GroupStickersetLoadingStatus) -> Bool { - switch lhs { - case .loading: - if case .loading = rhs { - return true - } else { - return false - } - case .failed: - if case .failed = rhs { - return true - } else { - return false - } - case let .loaded(lhsInfo, lhsPackItem, lhsCount): - if case let .loaded(rhsInfo, rhsPackItem, rhsCount) = rhs { - if lhsInfo != rhsInfo { - return false - } - if lhsPackItem != rhsPackItem { - return false - } - if lhsCount != rhsCount { - return false - } - return true - } else { - return false - } - } -} - private enum GroupStickersetEntry : TableItemListNodeEntry { case section(Int32) - case input(Int32, value: String) - case status(Int32, status:GroupStickersetLoadingStatus) - case description(Int32, Int32, text: String) - case pack(Int32, Int32, StickerPackCollectionInfo, StickerPackItem?, Int32, Bool) + case input(Int32, value: String, viewType: GeneralViewType) + case status(Int32, status:GroupStickersetLoadingStatus, viewType: GeneralViewType) + case description(Int32, Int32, text: String, viewType: GeneralViewType) + case pack(Int32, Int32, StickerPackCollectionInfo, StickerPackItem?, Int32, Bool, GeneralViewType) + + func withUpdatedViewType(_ viewType: GeneralViewType) -> GroupStickersetEntry { + switch self { + case .section: return self + case let .input(sectionId, value: value, _): return .input(sectionId, value: value, viewType: viewType) + case let .status(sectionId, status: status, _): return .status(sectionId, status: status, viewType: viewType) + case let .description(sectionId, index, text: text, _): return .description(sectionId, index, text: text, viewType: viewType) + case let .pack(sectionId, index, info, item, count, selected, _): return .pack(sectionId, index, info, item, count, selected, viewType) + } + } var stableId: GroupStickersetEntryId { switch self { @@ -125,9 +69,9 @@ private enum GroupStickersetEntry : TableItemListNodeEntry { return .input case .status: return .status - case .description(_, let id, _): + case .description(_, let id, _, _): return .description(id) - case .pack(_, _, let info, _, _, _): + case .pack(_, _, let info, _, _, _, _): return .pack(info.id) } } @@ -138,7 +82,7 @@ private enum GroupStickersetEntry : TableItemListNodeEntry { return 0 case .status: return 1 - case .description(_, let index, _): + case .description(_, let index, _, _): return 2 + index case .pack: fatalError("") @@ -149,13 +93,13 @@ private enum GroupStickersetEntry : TableItemListNodeEntry { var index:Int32 { switch self { - case let .input(sectionId, _): + case let .input(sectionId, _, _): return (sectionId * 1000) + stableIndex - case let .status(sectionId, _): + case let .status(sectionId, _, _): return (sectionId * 1000) + stableIndex - case let .description(sectionId, _, _): + case let .description(sectionId, _, _, _): return (sectionId * 1000) + stableIndex - case let .pack( sectionId, index, _, _, _, _): + case let .pack( sectionId, index, _, _, _, _, _): return (sectionId * 1000) + 100 + index case let .section(sectionId): return (sectionId + 1) * 1000 - sectionId @@ -168,39 +112,40 @@ private enum GroupStickersetEntry : TableItemListNodeEntry { func item(_ arguments: GroupStickersArguments, initialSize: NSSize) -> TableRowItem { switch self { case .section: - return GeneralRowItem(initialSize, height: 20, stableId: stableId) - case let .input(_, value): - return GeneralInputRowItem(initialSize, stableId: stableId, placeholder: "t.me/addstickers/", text: value, limit: 30, insets: NSEdgeInsets(left: 30, right: 30, top: 2, bottom: 3), textChangeHandler: { updatedText in - arguments.textChanged(value, updatedText) - }, textFilter: { text -> String in + return GeneralRowItem(initialSize, height: 30, stableId: stableId, viewType: .separator) + case let .input(_, value, viewType): + return InputDataRowItem(initialSize, stableId: stableId, mode: .plain, error: nil, viewType: viewType, currentText: value, placeholder: nil, inputPlaceholder: "https://t.me/addstickers/", defaultText: "https://t.me/addstickers/", rightItem: .action(theme.icons.recentDismiss, .clearText), filter: { text in var filter = NSCharacterSet.alphanumerics filter.insert(charactersIn: "_") return text.trimmingCharacters(in: filter.inverted) - }, holdText:true, pasteFilter: { value in + }, updated: { updatedText in + arguments.textChanged(value, updatedText) + }, pasteFilter: { value in if let index = value.range(of: "t.me/addstickers/") { - return (true, value.substring(from: index.upperBound)) + return (true, String(value[index.upperBound...])) } return (false, value) - }, canFastClean: true) - case let .description(_, _, text): + }, limit: 25 + 30) + + case let .description(_, _, text, viewType): let attr = NSMutableAttributedString() _ = attr.append(string: text, color: theme.colors.grayText, font: .normal(.text)) - attr.detectLinks(type: [.Mentions, .Hashtags], account: arguments.account, color: theme.colors.link, openInfo: { peerId, _, _, _ in + attr.detectLinks(type: [.Mentions, .Hashtags], context: arguments.context, color: theme.colors.link, openInfo: { peerId, _, _, _ in arguments.openChat(peerId) }) - return GeneralTextRowItem(initialSize, stableId: stableId, text: attr) - case let .status(_, status): + return GeneralTextRowItem(initialSize, stableId: stableId, text: attr, viewType: viewType) + case let .status(_, status, viewType): switch status { case let .loaded(info, topItem, count): - return StickerSetTableRowItem(initialSize, account: arguments.account, stableId: stableId, info: info, topItem: topItem, itemCount: count, unread: false, editing: ItemListStickerPackItemEditing(editable: false, editing: false), enabled: true, control: .empty, action: {}) + return StickerSetTableRowItem(initialSize, context: arguments.context, stableId: stableId, info: info, topItem: topItem, itemCount: count, unread: false, editing: ItemListStickerPackItemEditing(editable: false, editing: false), enabled: true, control: .empty, viewType: viewType, action: {}) case .loading: - return LoadingTableItem(initialSize, height: 50, stableId: stableId) + return LoadingTableItem(initialSize, height: 50, stableId: stableId, viewType: viewType) case .failed: - return EmptyGroupstickerSearchRowItem(initialSize, height: 50, stableId: stableId) + return EmptyGroupstickerSearchRowItem(initialSize, height: 50, stableId: stableId, viewType: viewType) } - case let .pack(_, _, info, topItem, count, selected): - return StickerSetTableRowItem(initialSize, account: arguments.account, stableId: stableId, info: info, topItem: topItem, itemCount: count, unread: false, editing: ItemListStickerPackItemEditing(editable: false, editing: false), enabled: true, control: selected ? .selected : .empty, action: { + case let .pack(_, _, info, topItem, count, selected, viewType): + return StickerSetTableRowItem(initialSize, context: arguments.context, stableId: stableId, info: info, topItem: topItem, itemCount: count, unread: false, editing: ItemListStickerPackItemEditing(editable: false, editing: false), enabled: true, control: selected ? .selected : .empty, viewType: viewType, action: { if let topItem = topItem { arguments.installStickerset((info, [topItem], count)) } @@ -210,62 +155,6 @@ private enum GroupStickersetEntry : TableItemListNodeEntry { } -private func ==(lhs: GroupStickersetEntry, rhs: GroupStickersetEntry) -> Bool { - switch lhs { - case .section(let id): - if case .section(id) = rhs { - return true - } else { - return false - } - case let .input(index, text): - if case .input(index, text) = rhs { - return true - } else { - return false - } - case let .status(index, status): - if case .status(index, status: status) = rhs { - return true - } else { - return false - } - - case let .description(section, id, text): - if case .description(section, id, text: text) = rhs { - return true - } else { - return false - } - case let .pack(lhsSectionId, lhsIndex, lhsInfo, lhsTopItem, lhsCount, lhsSelected): - if case let .pack(rhsSectionId, rhsIndex, rhsInfo, rhsTopItem, rhsCount, rhsSelected) = rhs { - - if lhsSectionId != rhsSectionId { - return false - } - if lhsIndex != rhsIndex { - return false - } - if lhsInfo != rhsInfo { - return false - } - if lhsTopItem != rhsTopItem { - return false - } - if lhsCount != rhsCount { - return false - } - if lhsSelected != rhsSelected { - return false - } - return true - } else { - return false - } - } - -} - private func groupStickersEntries(state: GroupStickerSetControllerState, view: CombinedView, peerId: PeerId, specificPack: (StickerPackCollectionInfo, [ItemCollectionItem])?) -> [GroupStickersetEntry] { var entries: [GroupStickersetEntry] = [] @@ -287,25 +176,35 @@ private func groupStickersEntries(state: GroupStickerSetControllerState, view: C } } - entries.append(.input(sectionId, value: value)) + var inputBlock: [GroupStickersetEntry] = [] + func applyBlock(_ block:[GroupStickersetEntry]) { + var block = block + for (i, item) in block.enumerated() { + block[i] = item.withUpdatedViewType(bestGeneralViewType(block, for: i)) + } + entries.append(contentsOf: block) + } + inputBlock.append(.input(sectionId, value: value, viewType: .singleItem)) if state.loading { - entries.append(.status(sectionId, status: .loading)) + inputBlock.append(.status(sectionId, status: .loading, viewType: .singleItem)) } else { if state.failed { - entries.append(.status(sectionId, status: .failed)) + inputBlock.append(.status(sectionId, status: .failed, viewType: .singleItem)) } else if let loadedPack = state.loadedPack { - entries.append(.status(sectionId, status: .loaded(loadedPack.0, loadedPack.1.first as? StickerPackItem, loadedPack.2))) + inputBlock.append(.status(sectionId, status: .loaded(loadedPack.0, loadedPack.1.first as? StickerPackItem, loadedPack.2), viewType: .singleItem)) } else { if let specificPack = specificPack, !value.isEmpty { - entries.append(.status(sectionId, status: .loaded(specificPack.0, specificPack.1.first as? StickerPackItem, Int32(specificPack.1.count)))) + inputBlock.append(.status(sectionId, status: .loaded(specificPack.0, specificPack.1.first as? StickerPackItem, Int32(specificPack.1.count)), viewType: .singleItem)) } } } - entries.append(.description(sectionId, descriptionId, text: tr(.groupStickersCreateDescription))) + applyBlock(inputBlock) + + entries.append(.description(sectionId, descriptionId, text: L10n.groupStickersCreateDescription, viewType: .textBottomItem)) descriptionId += 1 @@ -314,12 +213,12 @@ private func groupStickersEntries(state: GroupStickerSetControllerState, view: C - entries.append(.description(sectionId, descriptionId, text: tr(.groupStickersChooseHeader))) + entries.append(.description(sectionId, descriptionId, text: L10n.groupStickersChooseHeader, viewType: .textTopItem)) descriptionId += 1 if let stickerPacksView = view.views[.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])] as? ItemCollectionInfosView { if let packsEntries = stickerPacksView.entriesByNamespace[Namespaces.ItemCollection.CloudStickerPacks] { var index: Int32 = 0 - for entry in packsEntries { + for (i, entry) in packsEntries.enumerated() { if let info = entry.info as? StickerPackCollectionInfo { var selected: Bool if let loadedPack = state.loadedPack { @@ -327,7 +226,7 @@ private func groupStickersEntries(state: GroupStickerSetControllerState, view: C } else { selected = info == specificPack?.0 } - entries.append(.pack(sectionId, index, info, entry.firstItem as? StickerPackItem, info.count == 0 ? entry.count : info.count, selected)) + entries.append(.pack(sectionId, index, info, entry.firstItem as? StickerPackItem, info.count == 0 ? entry.count : info.count, selected, bestGeneralViewType(packsEntries, for: i))) index += 1 } } @@ -379,16 +278,37 @@ private struct GroupStickerSetControllerState: Equatable { class GroupStickerSetController: TableViewController { private let peerId: PeerId + private let disposable = MetaDisposable() private var saveGroupStickerSet:(()->Void)? = nil - init( account: Account, peerId:PeerId) { + init(_ context: AccountContext, peerId:PeerId) { self.peerId = peerId - super.init(account) + super.init(context) + } + + deinit { + disposable.dispose() + } + + override func becomeFirstResponder() -> Bool? { + return true + } + + override func firstResponder() -> NSResponder? { + var responder: NSResponder? + genericView.enumerateViews { view -> Bool in + if responder == nil, let firstResponder = view.firstResponder { + responder = firstResponder + return false + } + return true + } + return responder } override func viewDidLoad() { super.viewDidLoad() - let account = self.account + let context = self.context let peerId = self.peerId let statePromise = ValuePromise(GroupStickerSetControllerState(), ignoreRepeated: true) @@ -404,12 +324,12 @@ class GroupStickerSetController: TableViewController { let resolveDisposable = MetaDisposable() actionsDisposable.add(resolveDisposable) - let arguments = GroupStickersArguments(account: account, textChanged: { current, updated in + let arguments = GroupStickersArguments(context: context, textChanged: { current, updated in updateState({$0.withUpdatedLoadedPack(nil).withUpdatedFailed(false).withUpdatedText(updated)}) if updated.isEmpty { resolveDisposable.set(nil) } else { - resolveDisposable.set((loadedStickerPack(postbox: account.postbox, network: account.network, reference: .name(updated)) |> deliverOnMainQueue).start(next: { result in + resolveDisposable.set((context.engine.stickers.loadedStickerPack(reference: .name(updated), forceActualized: false) |> deliverOnMainQueue).start(next: { result in switch result { case .fetching: updateState({$0.withUpdatedLoadedPack(nil).withUpdatedLoading(true)}) @@ -423,14 +343,13 @@ class GroupStickerSetController: TableViewController { }, installStickerset: { info in updateState({$0.withUpdatedLoadedPack(info).withUpdatedText(info.0.shortName)}) }, openChat: { [weak self] peerId in - self?.navigationController?.push(ChatController(account: account, peerId: peerId)) + self?.navigationController?.push(ChatController(context: context, chatLocation: .peer(peerId))) }) - saveGroupStickerSet = { [weak self] in if let strongSelf = self { - actionsDisposable.add(showModalProgress(signal: updateGroupSpecificStickerset(postbox: account.postbox, network: account.network, peerId: peerId, info: stateValue.modify{$0}.loadedPack?.0), for: mainWindow).start(next: { [weak strongSelf] in + actionsDisposable.add(showModalProgress(signal: context.engine.peers.updateGroupSpecificStickerset(peerId: peerId, info: stateValue.modify{$0}.loadedPack?.0), for: context.window).start(next: { [weak strongSelf] _ in strongSelf?.navigationController?.back() - }, error: { [weak strongSelf] in + }, error: { [weak strongSelf] _ in strongSelf?.navigationController?.back() })) self?.doneButton?.isEnabled = false @@ -438,21 +357,28 @@ class GroupStickerSetController: TableViewController { } let stickerPacks = Promise() - stickerPacks.set(account.postbox.combinedView(keys: [.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])])) + stickerPacks.set(context.account.postbox.combinedView(keys: [.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])])) let featured = Promise<[FeaturedStickerPackItem]>() - featured.set(account.viewTracker.featuredStickerPacks()) + featured.set(context.account.viewTracker.featuredStickerPacks()) let previousEntries:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) let initialSize = self.atomicSize - genericView.merge(with: combineLatest(statePromise.get() |> deliverOnMainQueue, stickerPacks.get() |> deliverOnMainQueue, peerSpecificStickerPack(postbox: account.postbox, network: account.network, peerId: peerId) |> deliverOnMainQueue, appearanceSignal) + + let signal = combineLatest(queue: prepareQueue,statePromise.get(), stickerPacks.get(), context.engine.peers.peerSpecificStickerPack(peerId: peerId), appearanceSignal) |> map { state, view, specificPack, appearance -> TableUpdateTransition in - let entries = groupStickersEntries(state: state, view: view, peerId: peerId, specificPack: specificPack).map {AppearanceWrapperEntry(entry: $0, appearance: appearance)} + let entries = groupStickersEntries(state: state, view: view, peerId: peerId, specificPack: specificPack.packInfo).map {AppearanceWrapperEntry(entry: $0, appearance: appearance)} return prepareTransition(left: previousEntries.swap(entries), right: entries, initialSize: initialSize.modify({$0}), arguments: arguments) } |> afterDisposed { actionsDisposable.dispose() - } ) - readyOnce() + } |> deliverOnMainQueue + + self.disposable.set(signal.start(next: { [weak self] transition in + self?.genericView.merge(with: transition) + self?.readyOnce() + })) + + actionsDisposable.add((statePromise.get() |> deliverOnMainQueue).start(next: { [weak self] state in var enabled = !state.failed @@ -467,17 +393,14 @@ class GroupStickerSetController: TableViewController { })) } - var doneButton:Button? { - if let button = rightBarView as? TextButtonBarView { - return button.button - } - return nil + var doneButton:Control? { + return rightBarView } override func getRightBarViewOnce() -> BarView { - let button = TextButtonBarView(controller: self, text: tr(.navigationDone)) + let button = TextButtonBarView(controller: self, text: tr(L10n.navigationDone)) - button.button.set(handler: { [weak self] _ in + button.set(handler: { [weak self] _ in self?.saveGroupStickerSet?() }, for: .Click) diff --git a/Telegram-Mac/GroupVideoView.swift b/Telegram-Mac/GroupVideoView.swift new file mode 100644 index 0000000000..80ce9203f3 --- /dev/null +++ b/Telegram-Mac/GroupVideoView.swift @@ -0,0 +1,159 @@ +// +// GroupVideoView.swift +// Telegram +// +// Created by Mikhail Filimonov on 11.01.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit + + +final class GroupVideoView: View { + + + private let videoViewContainer: View + let videoView: PresentationCallVideoView + var gravity: CALayerContentsGravity = .resizeAspect + var initialGravity: CALayerContentsGravity? = nil + private var validLayout: CGSize? + + private var videoAnimator: DisplayLinkAnimator? + + private var isMirrored: Bool = false { + didSet { + CATransaction.begin() + if isMirrored { + let rect = self.videoViewContainer.bounds + var fr = CATransform3DIdentity + fr = CATransform3DTranslate(fr, rect.width / 2, 0, 0) + fr = CATransform3DScale(fr, -1, 1, 1) + fr = CATransform3DTranslate(fr, -(rect.width / 2), 0, 0) + self.videoViewContainer.layer?.sublayerTransform = fr + } else { + self.videoViewContainer.layer?.sublayerTransform = CATransform3DIdentity + } + CATransaction.commit() + } + } + + var tapped: (() -> Void)? + + init(videoView: PresentationCallVideoView) { + self.videoViewContainer = View() + self.videoView = videoView + + super.init() + + self.videoViewContainer.addSubview(self.videoView.view) + self.addSubview(self.videoViewContainer) + + + videoView.setOnOrientationUpdated({ [weak self] _, _ in + guard let strongSelf = self else { + return + } + if let size = strongSelf.validLayout { + strongSelf.updateLayout(size: size, transition: .immediate) + } + }) + + videoView.setOnIsMirroredUpdated({ [weak self] isMirrored in + self?.isMirrored = isMirrored + }) + +// videoView.setIsPaused(true); + } + + override func mouseDown(with event: NSEvent) { + super.mouseDown(with: event) + } + + override var mouseDownCanMoveWindow: Bool { + return true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + func setVideoContentMode(_ contentMode: CALayerContentsGravity, animated: Bool) { + + + self.gravity = contentMode + self.validLayout = nil + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate + self.updateLayout(size: frame.size, transition: transition) + } + + override func layout() { + super.layout() + updateLayout(size: frame.size, transition: .immediate) + } + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + guard self.validLayout != size else { + return + } + self.validLayout = size + + var videoRect: CGRect = .zero + videoRect = focus(size) + + transition.updateFrame(view: self.videoViewContainer, frame: videoRect) + + if transition.isAnimated { + let videoView = self.videoView + + videoView.renderToSize(self.videoView.view.frame.size, true) + videoView.setIsPaused(true) + + transition.updateFrame(view: videoView.view, frame: videoRect, completion: { [weak videoView] _ in + videoView?.renderToSize(videoRect.size, false) + videoView?.setIsPaused(false) + }) + } else { + transition.updateFrame(view: videoView.view, frame: videoRect) + } + + + for subview in self.videoView.view.subviews { + transition.updateFrame(view: subview, frame: videoRect.size.bounds) + } + + var fr = CATransform3DIdentity + if isMirrored { + let rect = videoRect + fr = CATransform3DTranslate(fr, rect.width / 2, 0, 0) + fr = CATransform3DScale(fr, -1, 1, 1) + fr = CATransform3DTranslate(fr, -(rect.width / 2), 0, 0) + } + + switch transition { + case .immediate: + self.videoViewContainer.layer?.sublayerTransform = fr + case let .animated(duration, curve): + let animation = CABasicAnimation(keyPath: "sublayerTransform") + animation.fromValue = self.videoViewContainer.layer?.presentation()?.sublayerTransform ?? self.videoViewContainer.layer?.sublayerTransform ?? CATransform3DIdentity + animation.toValue = fr + animation.timingFunction = .init(name: curve.timingFunction) + animation.duration = duration + self.videoViewContainer.layer?.add(animation, forKey: "sublayerTransform") + self.videoViewContainer.layer?.sublayerTransform = fr + } + + } + + override func viewDidMoveToSuperview() { + if superview == nil { + didRemoveFromSuperview?() + } + } + + var didRemoveFromSuperview: (()->Void)? = nil +} diff --git a/Telegram-Mac/GroupedLayout.swift b/Telegram-Mac/GroupedLayout.swift new file mode 100644 index 0000000000..fe27db255f --- /dev/null +++ b/Telegram-Mac/GroupedLayout.swift @@ -0,0 +1,523 @@ +// +// GroupedLayout.swift +// Telegram +// +// Created by keepcoder on 31/10/2017. +// Copyright © 2017 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import TGUIKit +import Postbox + +private final class MessagePhotoInfo { + let mid:MessageId + let imageSize: NSSize + let aspectRatio: CGFloat + fileprivate(set) var layoutFrame: NSRect = NSZeroRect + fileprivate(set) var positionFlags: LayoutPositionFlags = .none + + init(_ message: Message) { + self.mid = message.id + + self.imageSize = ChatLayoutUtils.contentSize(for: message.media[0], with: 320) + self.aspectRatio = self.imageSize.width / self.imageSize.height + + } +} + + +private final class GroupedLayoutAttempt { + let lineCounts:[Int] + let heights:[CGFloat] + init(lineCounts:[Int], heights: [CGFloat]) { + self.lineCounts = lineCounts + self.heights = heights + } +} + +enum GroupedMediaType { + case photoOrVideo + case files +} + +class GroupedLayout { + + private(set) var dimensions: NSSize = NSZeroSize + private var layouts:[MessageId: MessagePhotoInfo] = [:] + private(set) var messages:[Message] + private(set) var type: GroupedMediaType + init(_ messages: [Message], type: GroupedMediaType = .photoOrVideo) { + switch type { + case .photoOrVideo: + self.messages = messages.filter { $0.media.first!.isInteractiveMedia } + case .files: + self.messages = messages.filter { $0.media.first is TelegramMediaFile } + } + self.type = type + } + + func contentNode(for index: Int) -> ChatMediaContentView.Type { + return ChatLayoutUtils.contentNode(for: messages[index].media[0]) + } + + var count: Int { + return messages.count + } + + func message(at point: NSPoint) -> Message? { + for i in 0 ..< messages.count { + if NSPointInRect(point, frame(at: i)) { + return messages[i] + } + } + return nil + } + + func measure(_ maxSize: NSSize, spacing: CGFloat = 4.0) { + + var photos: [MessagePhotoInfo] = [] + + switch type { + case .photoOrVideo: + if messages.count == 1 { + let photo = MessagePhotoInfo(messages[0]) + photos.append(photo) + photos[0].layoutFrame = NSMakeRect(0, 0, photos[0].imageSize.width, photos[0].imageSize.height) + photos[0].positionFlags = .none + } else { + var proportions: String = "" + var averageAspectRatio: CGFloat = 1.0 + var forceCalc: Bool = false + for message in messages { + let photo = MessagePhotoInfo(message) + photos.append(photo) + + if photo.aspectRatio > 1.2 { + proportions += "w" + } else if photo.aspectRatio < 0.8 { + proportions += "n" + } else { + proportions += "q" + } + averageAspectRatio += photo.aspectRatio + + if photo.aspectRatio > 2.0 { + forceCalc = true + } + } + + let minWidth: CGFloat = 70 + let maxAspectRatio = maxSize.width / maxSize.height + if (photos.count > 0) { + averageAspectRatio = averageAspectRatio / CGFloat(photos.count) + } + + if !forceCalc { + if photos.count == 2 { + if proportions == "ww" && averageAspectRatio > 1.4 * maxAspectRatio && photos[1].aspectRatio - photos[0].aspectRatio < 0.2 { + let width: CGFloat = maxSize.width + let height:CGFloat = min(width / photos[0].aspectRatio, min(width / photos[1].aspectRatio, (maxSize.height - spacing) / 2.0)) + + photos[0].layoutFrame = NSMakeRect(0.0, 0.0, width, height) + photos[0].positionFlags = [.top, .left, .right] + + photos[1].layoutFrame = NSMakeRect(0.0, height + spacing, width, height) + photos[1].positionFlags = [.bottom, .left, .right] + } else if proportions == "ww" || proportions == "qq" { + let width: CGFloat = (maxSize.width - spacing) / 2.0 + let height: CGFloat = min(width / photos[0].aspectRatio, min(width / photos[1].aspectRatio, maxSize.height)) + + photos[0].layoutFrame = NSMakeRect(0.0, 0.0, width, height) + photos[0].positionFlags = [.top, .left, .bottom] + + photos[1].layoutFrame = NSMakeRect(width + spacing, 0.0, width, height) + photos[1].positionFlags = [.top, .right, .bottom] + } else { + let firstWidth: CGFloat = (maxSize.width - spacing) / photos[1].aspectRatio / (1.0 / photos[0].aspectRatio + 1.0 / photos[1].aspectRatio) + let secondWidth: CGFloat = maxSize.width - firstWidth - spacing + let height: CGFloat = min(maxSize.height, min(firstWidth / photos[0].aspectRatio, secondWidth / photos[1].aspectRatio)) + + photos[0].layoutFrame = NSMakeRect(0.0, 0.0, firstWidth, height) + photos[0].positionFlags = [.top, .left, .bottom] + + photos[1].layoutFrame = NSMakeRect(firstWidth + spacing, 0.0, secondWidth, height) + photos[1].positionFlags = [.top, .right, .bottom] + } + } else if photos.count == 3 { + if proportions.hasPrefix("n") { + let firstHeight = maxSize.height + + let thirdHeight = min((maxSize.height - spacing) * 0.5, round(photos[1].aspectRatio * (maxSize.width - spacing) / (photos[2].aspectRatio + photos[1].aspectRatio))) + let secondHeight = maxSize.height - thirdHeight - spacing + let rightWidth = max(minWidth, min((maxSize.width - spacing) * 0.5, round(min(thirdHeight * photos[2].aspectRatio, secondHeight * photos[1].aspectRatio)))) + + let leftWidth = round(min(firstHeight * photos[0].aspectRatio, (maxSize.width - spacing - rightWidth))) + photos[0].layoutFrame = CGRect(x: 0.0, y: 0.0, width: leftWidth, height: firstHeight) + photos[0].positionFlags = [.top, .left, .bottom] + + photos[1].layoutFrame = CGRect(x: leftWidth + spacing, y: 0.0, width: rightWidth, height: secondHeight) + photos[1].positionFlags = [.right, .top] + + photos[2].layoutFrame = CGRect(x: leftWidth + spacing, y: secondHeight + spacing, width: rightWidth, height: thirdHeight) + photos[2].positionFlags = [.right, .bottom] + } else { + var width = maxSize.width + let firstHeight = floor(min(width / photos[0].aspectRatio, (maxSize.height - spacing) * 0.66)) + photos[0].layoutFrame = CGRect(x: 0.0, y: 0.0, width: width, height: firstHeight) + photos[0].positionFlags = [.top, .left, .right] + + width = (maxSize.width - spacing) / 2.0 + let secondHeight = min(maxSize.height - firstHeight - spacing, round(min(width / photos[1].aspectRatio, width / photos[2].aspectRatio))) + photos[1].layoutFrame = CGRect(x: 0.0, y: firstHeight + spacing, width: width, height: secondHeight) + photos[1].positionFlags = [.left, .bottom] + + photos[2].layoutFrame = CGRect(x: width + spacing, y: firstHeight + spacing, width: width, height: secondHeight) + photos[2].positionFlags = [.right, .bottom] + } + + } else if photos.count == 4 { + if proportions == "www" || proportions.hasPrefix("w") { + let w: CGFloat = maxSize.width + let h0: CGFloat = min(w / photos[0].aspectRatio, (maxSize.height - spacing) * 0.66) + photos[0].layoutFrame = NSMakeRect(0.0, 0.0, w, h0) + photos[0].positionFlags = [.top, .left, .right] + + var h: CGFloat = (maxSize.width - 2 * spacing) / (photos[1].aspectRatio + photos[2].aspectRatio + photos[3].aspectRatio) + let w0: CGFloat = max((maxSize.width - 2 * spacing) * 0.33, h * photos[1].aspectRatio) + var w2: CGFloat = max((maxSize.width - 2 * spacing) * 0.33, h * photos[3].aspectRatio) + var w1: CGFloat = w - w0 - w2 - 2 * spacing + + if w1 < minWidth { + w2 -= minWidth - w1 + w1 = minWidth + } + + h = min(maxSize.height - h0 - spacing, h) + photos[1].layoutFrame = NSMakeRect(0.0, h0 + spacing, w0, h) + photos[1].positionFlags = [.left, .bottom] + + photos[2].layoutFrame = NSMakeRect(w0 + spacing, h0 + spacing, w1, h) + photos[2].positionFlags = [.bottom] + + photos[3].layoutFrame = NSMakeRect(w0 + w1 + 2 * spacing, h0 + spacing, w2, h) + photos[3].positionFlags = [.right, .bottom] + } else { + let h: CGFloat = maxSize.height + let w0: CGFloat = min(h * photos[0].aspectRatio, (maxSize.width - spacing) * 0.6) + photos[0].layoutFrame = NSMakeRect(0.0, 0.0, w0, h) + photos[0].positionFlags = [.top, .left, .bottom] + + var w: CGFloat = (maxSize.height - 2 * spacing) / (1.0 / photos[1].aspectRatio + 1.0 / photos[2].aspectRatio + 1.0 / photos[3].aspectRatio) + let h0: CGFloat = w / photos[1].aspectRatio + let h1: CGFloat = w / photos[2].aspectRatio + let h2: CGFloat = w / photos[3].aspectRatio + w = min(maxSize.width - w0 - spacing, w) + photos[1].layoutFrame = NSMakeRect(w0 + spacing, 0.0, w, h0) + photos[1].positionFlags = [.right, .top] + + photos[2].layoutFrame = NSMakeRect(w0 + spacing, h0 + spacing, w, h1) + photos[2].positionFlags = [.right] + + photos[3].layoutFrame = NSMakeRect(w0 + spacing, h0 + h1 + 2 * spacing, w, h2) + photos[3].positionFlags = [.right, .bottom] + } + } + } + + if forceCalc || photos.count >= 5 { + var croppedRatios:[CGFloat] = [] + for photo in photos { + if averageAspectRatio > 1.1 { + croppedRatios.append(max(1.0, photo.aspectRatio)) + } else { + croppedRatios.append(min(1.0, photo.aspectRatio)) + } + } + + func multiHeight(_ ratios: [CGFloat]) -> CGFloat { + var ratioSum: CGFloat = 0 + for ratio in ratios { + ratioSum += ratio + } + return (maxSize.width - (CGFloat(ratios.count) - 1) * spacing) / ratioSum + } + + var attempts: [GroupedLayoutAttempt] = [] + + func addAttempt(_ lineCounts:[Int], _ heights:[CGFloat]) { + attempts.append(GroupedLayoutAttempt(lineCounts: lineCounts, heights: heights)) + } + + + addAttempt([croppedRatios.count], [multiHeight(croppedRatios)]) + + + var secondLine:Int = 0 + var thirdLine:Int = 0 + var fourthLine:Int = 0 + + for firstLine in 1 ..< croppedRatios.count { + secondLine = croppedRatios.count - firstLine + if firstLine > 3 || secondLine > 3 { + continue + } + + addAttempt([firstLine, croppedRatios.count - firstLine], [multiHeight(croppedRatios.subarray(with: NSMakeRange(0, firstLine))), multiHeight(croppedRatios.subarray(with: NSMakeRange(firstLine, croppedRatios.count - firstLine)))]) + } + + for firstLine in 1 ..< croppedRatios.count - 1 { + for secondLine in 1.. 3 || secondLine > (averageAspectRatio < 0.85 ? 4 : 3) || thirdLine > 3 { + continue + } + addAttempt([firstLine, secondLine, thirdLine], [multiHeight(croppedRatios.subarray(with: NSMakeRange(0, firstLine))), multiHeight(croppedRatios.subarray(with: NSMakeRange(firstLine, croppedRatios.count - firstLine - thirdLine))), multiHeight(croppedRatios.subarray(with: NSMakeRange(firstLine + secondLine, croppedRatios.count - firstLine - secondLine)))]) + + } + } + if croppedRatios.count > 2 { + for firstLine in 1 ..< croppedRatios.count - 2 { + for secondLine in 1 ..< croppedRatios.count - firstLine { + for thirdLine in 1 ..< croppedRatios.count - firstLine - secondLine { + fourthLine = croppedRatios.count - firstLine - secondLine - thirdLine; + if firstLine > 3 || secondLine > 3 || thirdLine > 3 || fourthLine > 3 { + continue + } + + addAttempt([firstLine, secondLine, thirdLine, fourthLine], [multiHeight(croppedRatios.subarray(with: NSMakeRange(0, firstLine))), multiHeight(croppedRatios.subarray(with: NSMakeRange(firstLine, croppedRatios.count - firstLine - thirdLine - fourthLine))), multiHeight(croppedRatios.subarray(with: NSMakeRange(firstLine + secondLine, croppedRatios.count - firstLine - secondLine - fourthLine))), multiHeight(croppedRatios.subarray(with: NSMakeRange(firstLine + secondLine + thirdLine, croppedRatios.count - firstLine - secondLine - thirdLine)))]) + } + } + } + } + + + let maxHeight: CGFloat = maxSize.height / 3 * 4 + var optimal: GroupedLayoutAttempt? = nil + var optimalDiff: CGFloat = 0.0 + for attempt in attempts { + var totalHeight: CGFloat = spacing * (CGFloat(attempt.heights.count) - 1); + var minLineHeight: CGFloat = .greatestFiniteMagnitude; + var maxLineHeight: CGFloat = 0.0 + for lineHeight in attempt.heights { + totalHeight += lineHeight + if lineHeight < minLineHeight { + minLineHeight = lineHeight + } + if lineHeight > maxLineHeight { + maxLineHeight = lineHeight; + } + } + + var diff: CGFloat = fabs(totalHeight - maxHeight); + + if (attempt.lineCounts.count > 1) { + if (attempt.lineCounts[0] > attempt.lineCounts[1]) + || (attempt.lineCounts.count > 2 && attempt.lineCounts[1] > attempt.lineCounts[2]) + || (attempt.lineCounts.count > 3 && attempt.lineCounts[2] > attempt.lineCounts[3]) { + diff *= 1.5 + } + } + + if minLineHeight < minWidth { + diff *= 1.5 + } + + if (optimal == nil || diff < optimalDiff) + { + optimal = attempt; + optimalDiff = diff; + } + } + + var index: Int = 0 + var y: CGFloat = 0.0 + if let optimal = optimal { + for i in 0 ..< optimal.lineCounts.count { + let count: Int = optimal.lineCounts[i] + let lineHeight: CGFloat = optimal.heights[i] + var x: CGFloat = 0.0 + + var positionFlags: LayoutPositionFlags = [.none] + if i == 0 { + positionFlags.insert(.top) + } + if i == optimal.lineCounts.count - 1 { + positionFlags.insert(.bottom) + } + + for k in 0 ..< count + { + var innerPositionFlags:LayoutPositionFlags = positionFlags; + + if k == 0 { + innerPositionFlags.insert(.left); + } + if k == count - 1 { + innerPositionFlags.insert(.right) + } + + if positionFlags == .none { + innerPositionFlags.insert(.inside) + } + + let ratio: CGFloat = croppedRatios[index]; + let width: CGFloat = ratio * lineHeight; + photos[index].layoutFrame = NSMakeRect(x, y, width, lineHeight); + photos[index].positionFlags = innerPositionFlags; + + x += width + spacing; + index += 1 + } + + y += lineHeight + spacing; + } + } + } + } + + var dimensions: CGSize = NSZeroSize + var layouts: [MessageId: MessagePhotoInfo] = [:] + for photo in photos { + layouts[photo.mid] = photo + + if photo.layoutFrame.maxX > dimensions.width { + dimensions.width = photo.layoutFrame.maxX + } + if photo.layoutFrame.maxY > dimensions.height { + dimensions.height = photo.layoutFrame.maxY + } + } + + + for (_, layout) in layouts { + layout.layoutFrame = NSMakeRect(floorToScreenPixels(System.backingScale, layout.layoutFrame.minX), floorToScreenPixels(System.backingScale, layout.layoutFrame.minY), floorToScreenPixels(System.backingScale, layout.layoutFrame.width), floorToScreenPixels(System.backingScale, layout.layoutFrame.height)) + } + self.layouts = layouts + self.dimensions = NSMakeSize(floorToScreenPixels(System.backingScale, dimensions.width), floorToScreenPixels(System.backingScale, dimensions.height)) + case .files: + var layouts: [MessageId: MessagePhotoInfo] = [:] + var y: CGFloat = 0 + for (i, message) in messages.enumerated() { + let info = MessagePhotoInfo(message) + var height:CGFloat = 40 + if let file = message.media.first as? TelegramMediaFile { + if file.isMusicFile { + height = 40 + } else if file.previewRepresentations.isEmpty { + height = 40 + } else { + height = 70 + } + } + + info.layoutFrame = NSMakeRect(0, y, maxSize.width, height) + var flags: LayoutPositionFlags = [] + if i == 0 { + flags.insert(.top) + flags.insert(.right) + flags.insert(.left) + + if messages.count == 1 { + flags.insert(.bottom) + } + } else if i == messages.count - 1 { + flags.insert(.right) + flags.insert(.left) + flags.insert(.bottom) + } + layouts[message.id] = info + y += height + 8 + } + self.layouts = layouts + + self.dimensions = NSMakeSize(maxSize.width, y - 8) + } + } + + func applyCaptions(_ captions: [ChatRowItem.RowCaption]) -> [ChatRowItem.RowCaption] { + var captions = captions + switch self.type { + case .photoOrVideo: + break + case .files: + var offset: CGFloat = 0 + for message in messages { + let info = self.layouts[message.id] + let index = captions.firstIndex(where: {$0.id == message.stableId }) + + if let info = info { + info.layoutFrame = info.layoutFrame.offsetBy(dx: 0, dy: offset) + if let index = index { + let caption = captions[index] + offset += caption.layout.layoutSize.height + 6 + } + } + } + + self.dimensions.height += offset + + + for (i, caption) in captions.enumerated() { + if let message = messages.first(where: { $0.stableId == caption.id}), let info = self.layouts[message.id] { + captions[i] = caption.withUpdatedOffset(-(self.dimensions.height - info.layoutFrame.maxY)) + } + } + } + + return captions + } + + func frame(for messageId: MessageId) -> NSRect { + guard let photo = layouts[messageId] else { + return NSZeroRect + } + return photo.layoutFrame + } + + func frame(at index: Int) -> NSRect { + return frame(for: messages[index].id) + } + + func position(for messageId: MessageId) -> LayoutPositionFlags { + guard let photo = layouts[messageId] else { + return .none + } + return photo.positionFlags + } + + + func moveItemIfNeeded(at index:Int, point: NSPoint) -> Int? { + + for i in 0 ..< count { + let frame = self.frame(at: i) + if NSPointInRect(point, frame) && i != index { + let current = messages[index] + messages.remove(at: index) + messages.insert(current, at: i) + return i + } + } + + return nil + } + + func isNeedMoveItem(at index:Int, point: NSPoint) -> Bool { + + for i in 0 ..< count { + let frame = self.frame(at: i) + if NSPointInRect(point, frame) && i != index { + return true + } + } + + return false + } + + func position(at index: Int) -> LayoutPositionFlags { + return position(for: messages[index].id) + } + +} diff --git a/Telegram-Mac/GroupsInCommonViewController.swift b/Telegram-Mac/GroupsInCommonViewController.swift index 42b0d4617f..d6cb16768d 100644 --- a/Telegram-Mac/GroupsInCommonViewController.swift +++ b/Telegram-Mac/GroupsInCommonViewController.swift @@ -8,148 +8,109 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit final class GroupsInCommonArguments { - let account:Account + let context: AccountContext let open:(PeerId)->Void - init(account: Account, open: @escaping(PeerId) -> Void) { + init(context: AccountContext, open: @escaping(PeerId) -> Void) { self.open = open - self.account = account + self.context = context } } -private enum GroupsInCommonEntry : Comparable, Identifiable { - case empty(Bool) - case peer(Int, Peer) - case section - - var stableId: AnyHashable { - switch self { - case .empty: - return -1 - case .section: - return 0 - case let .peer(_, peer): - return peer.id.hashValue - } - } + +private func _id_peer_id(_ id: PeerId) -> InputDataIdentifier { + return InputDataIdentifier("_id_peer_id_\(id)") +} + +private func commonGroupsEntries(state: GroupsInCommonState, arguments: GroupsInCommonArguments) -> [InputDataEntry] { + + + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 - var index:Int { - switch self { - case .empty: - return -1 - case .section: - return 0 - case let .peer(index, _): - return index + 10 - } - } - func item(arguments: GroupsInCommonArguments, initialSize:NSSize) -> TableRowItem { - switch self { - case let .empty(loading): - return SearchEmptyRowItem(initialSize, stableId: stableId, isLoading: loading, text: tr(.groupsInCommonEmpty)) - case .section: - return GeneralRowItem(initialSize, height: 20, stableId: stableId, type: .none) - case let .peer(_, peer): - return ShortPeerRowItem(initialSize, peer: peer, account: arguments.account, stableId: stableId, inset:NSEdgeInsets(left:30.0,right:30.0), action: { + let peers = state.peers.compactMap { $0.chatMainPeer } + + for (i, peer) in peers.enumerated() { + var viewType: GeneralViewType = bestGeneralViewType(peers, for: i) + if i == 0 { + if peers.count == 1 { + viewType = .lastItem + } else { + viewType = .innerItem + } + } + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_peer_id(peer.id), equatable: InputDataEquatable(PeerEquatable(peer)), comparable: nil, item: { initialSize, stableId in + return ShortPeerRowItem(initialSize, peer: peer, account: arguments.context.account, stableId: stableId, height: 46, photoSize: NSMakeSize(32, 32), inset: NSEdgeInsetsZero, viewType: viewType, action: { arguments.open(peer.id) }) - } + })) + index += 1 } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries + } -private func ==(lhs:GroupsInCommonEntry, rhs: GroupsInCommonEntry) -> Bool { - switch lhs { - case let .empty(loading): - if case .empty(loading) = rhs { - return true - } else { - return false - } - case .section: - if case .section = rhs { - return true - } else { - return false - } - case let .peer(lhsIndex, lhsPeer): - if case let .peer(rhsIndex, rhsPeer) = rhs { - return lhsIndex == rhsIndex && lhsPeer.isEqual(rhsPeer) - } else { - return false - } - } -} +func GroupsInCommonViewController(context: AccountContext, peerId: PeerId) -> ViewController { + -private func groupsInCommonEntries(_ peers:[Peer], loading:Bool) -> [GroupsInCommonEntry] { - if peers.isEmpty { - return [.empty(loading)] - } else { - var entries:[GroupsInCommonEntry] = [] - entries.append(.section) - var index:Int = 0 - for peer in peers { - entries.append(.peer(index, peer)) - index += 1 - } - return entries + let actionsDisposable = DisposableSet() + + let arguments = GroupsInCommonArguments(context: context, open: { peerId in + context.sharedContext.bindings.rootNavigation().push(ChatAdditionController(context: context, chatLocation: .peer(peerId))) + }) + + let contextValue: Promise = Promise() + let peerId = context.account.postbox.peerView(id: peerId) |> take(1) |> map { view in + return peerViewMainPeer(view)?.id ?? peerId } -} - -private func prepareTransition(left:[AppearanceWrapperEntry], right:[AppearanceWrapperEntry], arguments: GroupsInCommonArguments, initialSize: NSSize) -> TableUpdateTransition { - let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right, { entry -> TableRowItem in - return entry.entry.item(arguments: arguments, initialSize: initialSize) + contextValue.set(peerId |> map { + GroupsInCommonContext(account: context.account, peerId: $0) }) + let state = contextValue.get() |> mapToSignal { + $0.state + } + let dataSignal = state |> map { + return InputDataSignalValue(entries: commonGroupsEntries(state: $0, arguments: arguments)) + } - return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true) -} - -private func <(lhs:GroupsInCommonEntry, rhs: GroupsInCommonEntry) -> Bool { - return lhs.index < rhs.index -} - -class GroupsInCommonViewController: TableViewController { - private let peerId:PeerId - init(account:Account, peerId:PeerId) { - self.peerId = peerId - super.init(account) + let controller = InputDataController(dataSignal: dataSignal, title: "") + controller.bar = .init(height: 0) + + controller.contextOject = contextValue + + controller.onDeinit = { + actionsDisposable.dispose() } - override func viewDidLoad() { - super.viewDidLoad() - let account = self.account - - let arguments = GroupsInCommonArguments(account: account, open: { [weak self] peerId in - if let strongSelf = self { - strongSelf.navigationController?.push(ChatController(account: strongSelf.account, peerId: peerId)) - } - }) - - let previous:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) - let initialSize = atomicSize - let signal = combineLatest(Signal<([Peer], Bool), Void>.single(([], true)), appearanceSignal |> take(1)) |> then(combineLatest(groupsInCommon(account: account, peerId: peerId) |> mapToSignal { peerIds -> Signal<([Peer], Bool), Void> in - return account.postbox.modify { modififer -> ([Peer], Bool) in - var peers:[Peer] = [] - for peerId in peerIds { - if let peer = modififer.getPeer(peerId) { - peers.append(peer) - } - } - return (peers, false) + controller.getBackgroundColor = { + theme.colors.listBackground + } + + controller.didLoaded = { controller, _ in + controller.tableView.setScrollHandler { position in + switch position.direction { + case .bottom: + _ = contextValue.get().start(next: { ctx in + ctx.loadMore() + }) + //commonContext.loadMore() + default: + break } - }, appearanceSignal)) |> map { result -> TableUpdateTransition in - let entries = groupsInCommonEntries(result.0.0, loading: result.0.1).map {AppearanceWrapperEntry(entry: $0, appearance: result.1)} - - return prepareTransition(left: previous.swap(entries), right: entries, arguments: arguments, initialSize: initialSize.modify {$0} ) - } |> deliverOnMainQueue - - self.genericView.merge(with: signal) - - readyOnce() + } } + return controller } diff --git a/Telegram-Mac/GroupsStatsController.swift b/Telegram-Mac/GroupsStatsController.swift new file mode 100644 index 0000000000..f7aca24d06 --- /dev/null +++ b/Telegram-Mac/GroupsStatsController.swift @@ -0,0 +1,447 @@ +// +// GroupsStatsController.swift +// Telegram +// +// Created by Mikhail Filimonov on 19/06/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit +import TelegramCore +import Postbox + +import GraphCore + +private func statsEntries(_ state: GroupStatsContextState, uiState: UIStatsState, peers: [PeerId : Peer]?, updateIsLoading: @escaping(InputDataIdentifier, Bool)->Void, revealSection: @escaping(UIStatsState.RevealSection)->Void, context: GroupStatsContext, accountContext: AccountContext, openPeerInfo: @escaping(PeerId)->Void, detailedDisposable: DisposableDict) -> [InputDataEntry] { + var entries: [InputDataEntry] = [] + + var sectionId: Int32 = 0 + var index: Int32 = 0 + + + if state.stats == nil { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("loading"), equatable: nil, comparable: nil, item: { initialSize, stableId in + return StatisticsLoadingRowItem(initialSize, stableId: stableId, context: accountContext, text: L10n.channelStatsLoading) + })) + } else if let stats = state.stats { + + let dates = "\(dateFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(stats.period.minDate)))) – \(dateFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(stats.period.maxDate))))" + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.statsGroupOverview), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem, rightItem: InputDataGeneralTextRightData(isLoading: false, text: .initialize(string: dates, color: theme.colors.listGrayText, font: .normal(12)))))) + index += 1 + + var overviewItems:[ChannelOverviewItem] = [] + + if stats.members.current > 0 { + overviewItems.append(ChannelOverviewItem(title: L10n.statsGroupMembers, value: stats.members.attributedString)) + } + if stats.messages.current != 0 { + overviewItems.append(ChannelOverviewItem(title: L10n.statsGroupMessages, value: stats.messages.attributedString)) + } + if stats.viewers.current > 0 { + overviewItems.append(ChannelOverviewItem(title: L10n.statsGroupViewers, value: stats.viewers.attributedString)) + } + if stats.posters.current > 0 { + overviewItems.append(ChannelOverviewItem(title: L10n.statsGroupPosters, value: stats.posters.attributedString)) + } + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("overview"), equatable: InputDataEquatable(overviewItems), comparable: nil, item: { initialSize, stableId in + return ChannelOverviewStatsRowItem(initialSize, stableId: stableId, items: overviewItems, viewType: .singleItem) + })) + index += 1 + + + struct Graph { + let graph: StatsGraph + let title: String + let identifier: InputDataIdentifier + let type: ChartItemType + let load:(InputDataIdentifier)->Void + } + + var graphs: [Graph] = [] + + if !stats.growthGraph.isEmpty { + graphs.append(Graph(graph: stats.growthGraph, title: L10n.statsGroupGrowthTitle, identifier: InputDataIdentifier("growthGraph"), type: .lines, load: { identifier in + context.loadGrowthGraph() + updateIsLoading(identifier, true) + })) + } + + if !stats.membersGraph.isEmpty { + graphs.append(Graph(graph: stats.membersGraph, title: L10n.statsGroupMembersTitle, identifier: InputDataIdentifier("membersGraph"), type: .lines, load: { identifier in + context.loadMembersGraph() + updateIsLoading(identifier, true) + })) + } + + if !stats.newMembersBySourceGraph.isEmpty { + graphs.append(Graph(graph: stats.newMembersBySourceGraph, title: L10n.statsGroupNewMembersBySourceTitle, identifier: InputDataIdentifier("newMembersBySourceGraph"), type: .bars, load: { identifier in + context.loadNewMembersBySourceGraph() + updateIsLoading(identifier, true) + })) + } + + if !stats.languagesGraph.isEmpty { + graphs.append(Graph(graph: stats.languagesGraph, title: L10n.statsGroupLanguagesTitle, identifier: InputDataIdentifier("languagesGraph"), type: .pie, load: { identifier in + context.loadLanguagesGraph() + updateIsLoading(identifier, true) + })) + } + + if !stats.messagesGraph.isEmpty { + graphs.append(Graph(graph: stats.messagesGraph, title: L10n.statsGroupMessagesTitle, identifier: InputDataIdentifier("messagesGraph"), type: .bars, load: { identifier in + context.loadMessagesGraph() + updateIsLoading(identifier, true) + })) + } + + if !stats.actionsGraph.isEmpty { + graphs.append(Graph(graph: stats.actionsGraph, title: L10n.statsGroupActionsTitle, identifier: InputDataIdentifier("actionsGraph"), type: .lines, load: { identifier in + context.loadActionsGraph() + updateIsLoading(identifier, true) + })) + } + + + if !stats.topHoursGraph.isEmpty { + graphs.append(Graph(graph: stats.topHoursGraph, title: L10n.statsGroupTopHoursTitle, identifier: InputDataIdentifier("topHoursGraph"), type: .hourlyStep, load: { identifier in + context.loadTopHoursGraph() + updateIsLoading(identifier, true) + })) + } + + if !stats.topWeekdaysGraph.isEmpty { + graphs.append(Graph(graph: stats.topWeekdaysGraph, title: L10n.statsGroupTopWeekdaysTitle, identifier: InputDataIdentifier("topWeekdaysGraph"), type: .area, load: { identifier in + context.loadTopWeekdaysGraph() + updateIsLoading(identifier, true) + })) + } + + + + for graph in graphs { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(graph.title), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + switch graph.graph { + case let .Loaded(_, string): + ChartsDataManager.readChart(data: string.data(using: .utf8)!, sync: true, success: { collection in + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: graph.identifier, equatable: InputDataEquatable(graph.graph), comparable: nil, item: { initialSize, stableId in + return StatisticRowItem(initialSize, stableId: stableId, context: accountContext, collection: collection, viewType: .singleItem, type: graph.type, getDetailsData: { date, completion in + detailedDisposable.set(context.loadDetailedGraph(graph.graph, x: Int64(date.timeIntervalSince1970) * 1000).start(next: { graph in + if let graph = graph, case let .Loaded(_, data) = graph { + completion(data) + } + }), forKey: graph.identifier) + }) + })) + }, failure: { error in + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: graph.identifier, equatable: InputDataEquatable(graph.graph), comparable: nil, item: { initialSize, stableId in + return StatisticLoadingRowItem(initialSize, stableId: stableId, error: error.localizedDescription) + })) + }) + + updateIsLoading(graph.identifier, false) + + index += 1 + case .OnDemand: + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: graph.identifier, equatable: InputDataEquatable(graph.graph), comparable: nil, item: { initialSize, stableId in + return StatisticLoadingRowItem(initialSize, stableId: stableId, error: nil) + })) + index += 1 + if !uiState.loading.contains(graph.identifier) { + graph.load(graph.identifier) + } + case let .Failed(error): + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: graph.identifier, equatable: InputDataEquatable(graph.graph), comparable: nil, item: { initialSize, stableId in + return StatisticLoadingRowItem(initialSize, stableId: stableId, error: error) + })) + index += 1 + updateIsLoading(graph.identifier, false) + case .Empty: + break + } + } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + var addNextSection: Bool = false + + + + if let peers = peers { + var topPosters = stats.topPosters.filter { $0.messageCount > 0 && peers[$0.peerId] != nil && !peers[$0.peerId]!.rawDisplayTitle.isEmpty } + if !topPosters.isEmpty { + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.statsGroupTopPostersTitle), data: .init(color: theme.colors.listGrayText, detectBold: false, viewType: .textTopItem, rightItem: InputDataGeneralTextRightData(isLoading: false, text: .initialize(string: dates, color: theme.colors.listGrayText, font: .normal(12)))))) + index += 1 + + let needReveal = !uiState.revealed.contains(.topPosters) && topPosters.count > 10 + let toRevealCount = topPosters.count - 10 + if needReveal { + topPosters = Array(topPosters.prefix(10)) + } + + for (i, topPoster) in topPosters.enumerated() { + if let peer = peers[topPoster.peerId], topPoster.messageCount > 0 { + var textComponents: [String] = [] + if topPoster.messageCount > 0 { + textComponents.append(L10n.statsGroupTopPosterMessagesCountable(Int(topPoster.messageCount))) + if topPoster.averageChars > 0 { + textComponents.append(L10n.statsGroupTopPosterCharsCountable(Int(topPoster.averageChars))) + } + } + + var viewType = bestGeneralViewType(topPosters, for: i) + + if topPoster == topPosters.last, needReveal { + viewType = .innerItem + } + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("top_posters_\(peer.id)"), equatable: nil, comparable: nil, item: { initialSize, stableId in + return ShortPeerRowItem(initialSize, peer: peer, account: accountContext.account, stableId: stableId, enabled: true, height: 56, photoSize: NSMakeSize(36, 36), status: textComponents.joined(separator: ", "), inset: NSEdgeInsets(left: 30, right: 30), viewType: viewType, action: { + openPeerInfo(peer.id) + }) + })) + index += 1 + } + } + + if needReveal { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: UIStatsState.RevealSection.topPosters.id, equatable: nil, comparable: nil, item: { initialSize, stableId in + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.statsShowMoreCountable(toRevealCount), nameStyle: blueActionButton, type: .none, viewType: .lastItem, action: { + + revealSection(UIStatsState.RevealSection.topPosters) + + }, thumb: GeneralThumbAdditional(thumb: theme.icons.chatSearchUp, textInset: 52, thumbInset: 4)) + })) + index += 1 + } + + addNextSection = true + } + + var topAdmins = stats.topAdmins.filter { + return peers[$0.peerId] != nil && ($0.deletedCount + $0.kickedCount + $0.bannedCount) > 0 && !peers[$0.peerId]!.rawDisplayTitle.isEmpty + } + + if !topAdmins.isEmpty { + if addNextSection { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + } + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.statsGroupTopAdminsTitle), data: .init(color: theme.colors.listGrayText, detectBold: false, viewType: .textTopItem, rightItem: InputDataGeneralTextRightData(isLoading: false, text: .initialize(string: dates, color: theme.colors.listGrayText, font: .normal(12)))))) + index += 1 + + let needReveal = !uiState.revealed.contains(.topAdmins) && topAdmins.count > 10 + let toRevealCount = topAdmins.count - 10 + if needReveal { + topAdmins = Array(topAdmins.prefix(10)) + } + + for (i, topAdmin) in topAdmins.enumerated() { + if let peer = peers[topAdmin.peerId] { + + var textComponents: [String] = [] + if topAdmin.deletedCount > 0 { + textComponents.append(L10n.statsGroupTopAdminDeletionsCountable(Int(topAdmin.deletedCount))) + } + if topAdmin.kickedCount > 0 { + textComponents.append(L10n.statsGroupTopAdminKicksCountable(Int(topAdmin.kickedCount))) + } + if topAdmin.bannedCount > 0 { + textComponents.append(L10n.statsGroupTopAdminBansCountable(Int(topAdmin.bannedCount))) + } + + var viewType = bestGeneralViewType(topPosters, for: i) + + if topAdmin == topAdmins.last, needReveal { + viewType = .innerItem + } + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier.init("top_admin_\(peer.id)"), equatable: nil, comparable: nil, item: { initialSize, stableId in + return ShortPeerRowItem(initialSize, peer: peer, account: accountContext.account, stableId: stableId, enabled: true, height: 56, photoSize: NSMakeSize(36, 36), status: textComponents.joined(separator: ", "), inset: NSEdgeInsets(left: 30, right: 30), viewType: viewType, action: { + openPeerInfo(peer.id) + }) + })) + index += 1 + } + } + + + if needReveal { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: UIStatsState.RevealSection.topAdmins.id, equatable: nil, comparable: nil, item: { initialSize, stableId in + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.statsShowMoreCountable(toRevealCount), nameStyle: blueActionButton, type: .none, viewType: .lastItem, action: { + + revealSection(UIStatsState.RevealSection.topAdmins) + + }, thumb: GeneralThumbAdditional(thumb: theme.icons.chatSearchUp, textInset: 52, thumbInset: 4)) + })) + index += 1 + } + + + } else { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + addNextSection = false + } + + + var topInviters = stats.topInviters.filter { + return peers[$0.peerId] != nil && $0.inviteCount > 0 && !peers[$0.peerId]!.rawDisplayTitle.isEmpty + } + + if !topInviters.isEmpty { + if addNextSection { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + } + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.statsGroupTopInvitersTitle), data: .init(color: theme.colors.listGrayText, detectBold: false, viewType: .textTopItem, rightItem: InputDataGeneralTextRightData(isLoading: false, text: .initialize(string: dates, color: theme.colors.listGrayText, font: .normal(12)))))) + index += 1 + + + let needReveal = !uiState.revealed.contains(.topInviters) && topInviters.count > 10 + let toRevealCount = topInviters.count - 10 + if needReveal { + topInviters = Array(topInviters.prefix(10)) + } + + + for (i, topInviter) in topInviters.enumerated() { + if let peer = peers[topInviter.peerId] { + + var textComponents: [String] = [] + textComponents.append(L10n.statsGroupTopInviterInvitesCountable(Int(topInviter.inviteCount))) + + var viewType = bestGeneralViewType(topPosters, for: i) + + if topInviter == topInviters.last, needReveal { + viewType = .innerItem + } + + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("top_inviter_\(peer.id)"), equatable: nil, comparable: nil, item: { initialSize, stableId in + return ShortPeerRowItem(initialSize, peer: peer, account: accountContext.account, stableId: stableId, enabled: true, height: 56, photoSize: NSMakeSize(36, 36), status: textComponents.joined(separator: ", "), inset: NSEdgeInsets(left: 30, right: 30), viewType: viewType, action: { + openPeerInfo(peer.id) + }) + })) + index += 1 + } + } + + if needReveal { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: UIStatsState.RevealSection.topInviters.id, equatable: nil, comparable: nil, item: { initialSize, stableId in + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.statsShowMoreCountable(toRevealCount), nameStyle: blueActionButton, type: .none, viewType: .lastItem, action: { + + revealSection(UIStatsState.RevealSection.topInviters) + + }, thumb: GeneralThumbAdditional(thumb: theme.icons.chatSearchUp, textInset: 52, thumbInset: 4)) + })) + index += 1 + } + + } else { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + addNextSection = false + } + } + if addNextSection { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + } + } + + + return entries +} + + +func GroupStatsViewController(_ context: AccountContext, peerId: PeerId, datacenterId: Int32) -> ViewController { + + let initialState = UIStatsState(loading: []) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((UIStatsState) -> UIStatsState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + let openPeerInfo:(PeerId)->Void = { peerId in + return context.sharedContext.bindings.rootNavigation().push(PeerInfoController(context: context, peerId: peerId)) + } + + let statsContext = GroupStatsContext(postbox: context.account.postbox, network: context.account.network, datacenterId: datacenterId, peerId: peerId) + + let peersPromise = Promise<[PeerId: Peer]?>(nil) + + peersPromise.set(.single(nil) |> then(statsContext.state |> map(Optional.init) + |> map { stats -> [PeerId]? in + guard let stats = stats?.stats else { + return nil + } + var peerIds = Set() + peerIds.formUnion(stats.topPosters.map { $0.peerId }) + peerIds.formUnion(stats.topAdmins.map { $0.peerId }) + peerIds.formUnion(stats.topInviters.map { $0.peerId }) + return Array(peerIds) + } + |> mapToSignal { peerIds -> Signal<[PeerId: Peer]?, NoError> in + return context.account.postbox.transaction { transaction -> [PeerId: Peer]? in + var peers: [PeerId: Peer] = [:] + if let peerIds = peerIds { + for peerId in peerIds { + if let peer = transaction.getPeer(peerId) { + peers[peerId] = peer + } + } + } + return peers + } + })) + + let detailedDisposable = DisposableDict() + + let signal = combineLatest(queue: prepareQueue, statePromise.get(), statsContext.state, peersPromise.get()) |> map { uiState, state, peers in + return statsEntries(state, uiState: uiState, peers: peers, updateIsLoading: { identifier, isLoading in + updateState { state in + if isLoading { + return state.withAddedLoading(identifier) + } else { + return state.withRemovedLoading(identifier) + } + } + }, revealSection: { section in + updateState { + $0.withRevealedSection(section) + } + }, context: statsContext, accountContext: context, openPeerInfo: openPeerInfo, detailedDisposable: detailedDisposable) + } |> map { + return InputDataSignalValue(entries: $0) + } + + + let controller = InputDataController(dataSignal: signal, title: L10n.groupStatsTitle, removeAfterDisappear: false, hasDone: false) + + controller.contextOject = statsContext + controller.didLoaded = { controller, _ in + controller.tableView.alwaysOpenRowsOnMouseUp = true + controller.tableView.needUpdateVisibleAfterScroll = true + } + + controller.onDeinit = { + detailedDisposable.dispose() + } + + return controller +} diff --git a/Telegram-Mac/HapticEngine.swift b/Telegram-Mac/HapticEngine.swift new file mode 100644 index 0000000000..6ef1509ef6 --- /dev/null +++ b/Telegram-Mac/HapticEngine.swift @@ -0,0 +1,15 @@ +// +// HapticEngine.swift +// Telegram +// +// Created by Mikhail Filimonov on 24/04/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa + +class HapticEngine { + + + +} diff --git a/Telegram-Mac/ImageCompression.swift b/Telegram-Mac/ImageCompression.swift new file mode 100644 index 0000000000..6468f63257 --- /dev/null +++ b/Telegram-Mac/ImageCompression.swift @@ -0,0 +1,80 @@ + +// +// ImageCompression.swift +// Telegram +// +// Created by Mikhail Filimonov on 02/01/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import Mozjpeg + +private let tinyThumbnailHeaderPattern = Data(base64Encoded: "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDACgcHiMeGSgjISMtKygwPGRBPDc3PHtYXUlkkYCZlo+AjIqgtObDoKrarYqMyP/L2u71////m8H////6/+b9//j/2wBDASstLTw1PHZBQXb4pYyl+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj/wAARCAAAAAADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwA=") +private let tinyThumbnailFooterPattern = Data(base64Encoded: "/9k=") + +public func decodeTinyThumbnail(data: Data) -> Data? { + if data.count < 3 { + return nil + } + guard let tinyThumbnailHeaderPattern = tinyThumbnailHeaderPattern, let tinyThumbnailFooterPattern = tinyThumbnailFooterPattern else { + return nil + } + var version: UInt8 = 0 + data.copyBytes(to: &version, count: 1) + if version != 1 { + return nil + } + var width: UInt8 = 0 + var height: UInt8 = 0 + data.copyBytes(to: &width, from: 1 ..< 2) + data.copyBytes(to: &height, from: 2 ..< 3) + + var resultData = Data() + resultData.append(tinyThumbnailHeaderPattern) + resultData.append(data.subdata(in: 3 ..< data.count)) + resultData.append(tinyThumbnailFooterPattern) + resultData.withUnsafeMutableBytes({ (resultBytes: UnsafeMutablePointer) -> Void in + resultBytes[164] = width + resultBytes[166] = height + }) + return resultData +} + + + + +public func extractImageExtraScans(_ data: Data) -> [Int] { + return extractJPEGDataScans(data).map { item in + return item.intValue + } +} + +public func compressImageToJPEG(_ cgImage: CGImage, quality: Float) -> Data? { + if let result = compressJPEGData(cgImage) { + return result + } + + let data = NSMutableData() + guard let destination = CGImageDestinationCreateWithData(data as CFMutableData, "public.jpeg" as CFString, 1, nil) else { + return nil + } + + let options = NSMutableDictionary() + options.setObject(quality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString) + + CGImageDestinationAddImage(destination, cgImage, options as CFDictionary) + CGImageDestinationFinalize(destination) + + if data.length == 0 { + return nil + } + + return data as Data +} + + + +public func compressImageMiniThumbnail(_ image: CGImage) -> Data? { + return compressMiniThumbnail(image) +} diff --git a/Telegram-Mac/ImageTransparency.swift b/Telegram-Mac/ImageTransparency.swift new file mode 100644 index 0000000000..5d85f1b97c --- /dev/null +++ b/Telegram-Mac/ImageTransparency.swift @@ -0,0 +1,188 @@ +// +// ImageTransparency.swift +// Telegram +// +// Created by Mikhail Filimonov on 27/12/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import Accelerate +import TGUIKit + + +private func generateHistogram(cgImage: CGImage) -> ([[vImagePixelCount]], Int)? { + var sourceBuffer = vImage_Buffer() + defer { + free(sourceBuffer.data) + } + + var cgImageFormat = vImage_CGImageFormat( + bitsPerComponent: UInt32(cgImage.bitsPerComponent), + bitsPerPixel: UInt32(cgImage.bitsPerPixel), + colorSpace: Unmanaged.passUnretained(cgImage.colorSpace!), + bitmapInfo: cgImage.bitmapInfo, + version: 0, + decode: nil, + renderingIntent: .defaultIntent + ) + + let noFlags = vImage_Flags(kvImageNoFlags) + var error = vImageBuffer_InitWithCGImage(&sourceBuffer, &cgImageFormat, nil, cgImage, noFlags) + assert(error == kvImageNoError) + + if cgImage.alphaInfo == .premultipliedLast { + error = vImageUnpremultiplyData_RGBA8888(&sourceBuffer, &sourceBuffer, noFlags) + } else if cgImage.alphaInfo == .premultipliedFirst { + error = vImageUnpremultiplyData_ARGB8888(&sourceBuffer, &sourceBuffer, noFlags) + } + assert(error == kvImageNoError) + + let histogramBins = (0...3).map { _ in + return [vImagePixelCount](repeating: 0, count: 256) + } + var mutableHistogram: [UnsafeMutablePointer?] = histogramBins.map { + return UnsafeMutablePointer(mutating: $0) + } + error = vImageHistogramCalculation_ARGB8888(&sourceBuffer, &mutableHistogram, noFlags) + assert(error == kvImageNoError) + + let alphaBinIndex = [.last, .premultipliedLast].contains(cgImage.alphaInfo) ? 3 : 0 + return (histogramBins, alphaBinIndex) +} + +func imageHasTransparency(_ cgImage: CGImage) -> Bool { + guard cgImage.bitsPerComponent == 8, cgImage.bitsPerPixel == 32 else { + return false + } + guard [.first, .last, .premultipliedFirst, .premultipliedLast].contains(cgImage.alphaInfo) else { + return false + } + if let (histogramBins, alphaBinIndex) = generateHistogram(cgImage: cgImage) { + for i in 0 ..< 255 { + if histogramBins[alphaBinIndex][i] > 0 { + return true + } + } + } + return false +} + +private func scaledDrawingContext(_ cgImage: CGImage, maxSize: CGSize) -> DrawingContext { + var size = CGSize(width: cgImage.width, height: cgImage.height) + if (size.width > maxSize.width && size.height > maxSize.height) { + size = size.aspectFilled(maxSize) + } + let context = DrawingContext(size: size, scale: 1.0, clear: true) + context.withFlippedContext { context in + context.draw(cgImage, in: CGRect(origin: CGPoint(), size: size)) + } + return context +} + +func imageRequiresInversion(_ cgImage: CGImage) -> Bool { + guard cgImage.bitsPerComponent == 8, cgImage.bitsPerPixel == 32 else { + return false + } + guard [.first, .last, .premultipliedFirst, .premultipliedLast].contains(cgImage.alphaInfo) else { + return false + } + + let context = scaledDrawingContext(cgImage, maxSize: CGSize(width: 128.0, height: 128.0)) + if let cgImage = context.generateImage(), let (histogramBins, alphaBinIndex) = generateHistogram(cgImage: cgImage) { + var hasAlpha = false + for i in 0 ..< 255 { + if histogramBins[alphaBinIndex][i] > 0 { + hasAlpha = true + break + } + } + + if hasAlpha { + var matching: Int = 0 + var total: Int = 0 + for y in 0 ..< Int(context.size.height) { + for x in 0 ..< Int(context.size.width) { + var saturation: CGFloat = 0.0 + var brightness: CGFloat = 0.0 + var alpha: CGFloat = 0.0 + context.colorAt(CGPoint(x: x, y: y)).getHue(nil, saturation: &saturation, brightness: &brightness, alpha: &alpha) + if alpha < 1.0 { + hasAlpha = true + } + + if alpha > 0.0 { + total += 1 + if saturation < 0.1 && brightness < 0.25 { + matching += 1 + } + } + } + } + return CGFloat(matching) / CGFloat(total) > 0.85 + } + } + return false +} + + +func generateTintedImage(image: CGImage?, color: NSColor, backgroundColor: NSColor? = nil, flipVertical: Bool = true) -> CGImage? { + guard let image = image else { + return nil + } + let imageSize = image.size + return generateImage(imageSize, contextGenerator: { size, context in + if let backgroundColor = backgroundColor { + context.setFillColor(backgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: imageSize)) + } + + let imageRect = CGRect(origin: CGPoint(), size: imageSize) + context.saveGState() + if flipVertical { + context.translateBy(x: imageRect.midX, y: imageRect.midY) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -imageRect.midX, y: -imageRect.midY) + } + context.clip(to: imageRect, mask: image) + context.setFillColor(color.cgColor) + context.fill(imageRect) + context.restoreGState() + }) +} + + + +private func orientationFromExif(orientation: Int) -> ImageOrientation { + switch orientation { + case 1: + return .up; + case 3: + return .down; + case 8: + return .left; + case 6: + return .right; + case 2: + return .upMirrored; + case 4: + return .downMirrored; + case 5: + return .leftMirrored; + case 7: + return .rightMirrored; + default: + return .up + } +} + +func imageOrientationFromSource(_ source: CGImageSource) -> ImageOrientation { + if let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) { + let dict = properties as NSDictionary + if let value = dict.object(forKey: kCGImagePropertyOrientation) as? NSNumber { + return orientationFromExif(orientation: value.intValue) + } + } + + return .up +} diff --git a/Telegram-Mac/ImageUtils.swift b/Telegram-Mac/ImageUtils.swift index 96e6273585..3c19bf592c 100644 --- a/Telegram-Mac/ImageUtils.swift +++ b/Telegram-Mac/ImageUtils.swift @@ -7,87 +7,130 @@ // import Cocoa -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac +import Postbox +import TelegramCore + +import SwiftSignalKit import TGUIKit -/* - @[TGColorWithHex(0xff516a), TGColorWithHex(0xff885e)], - @[TGColorWithHex(0xffa85c), TGColorWithHex(0xffcd6a)], - @[TGColorWithHex(0x54cb68), TGColorWithHex(0xa0de7e)], - @[TGColorWithHex(0x2a9ef1), TGColorWithHex(0x72d5fd)], - @[TGColorWithHex(0x665fff), TGColorWithHex(0x82b1ff)], - @[TGColorWithHex(0xd669ed), TGColorWithHex(0xe0a2f3)], - */ -private let colors: [(top: NSColor, bottom: NSColor)] = [ - (NSColor(0xff516a), NSColor(0xff885e)), - (NSColor(0xffa85c), NSColor(0xffcd6a)), - (NSColor(0x54cb68), NSColor(0xa0de7e)), - (NSColor(0x2a9ef1), NSColor(0x72d5fd)), - (NSColor(0x665fff), NSColor(0x82b1ff)), - (NSColor(0xd669ed), NSColor(0xe0a2f3)) -] +let graphicsThreadPool = ThreadPool(threadCount: 5, threadPriority: 1) -public func peerAvatarImage(account: Account, peer: Peer, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0), scale:CGFloat = 1.0, font:NSFont = .medium(.title)) -> Signal? { - if let smallProfileImage = peer.smallProfileImage { - - return cachedPeerPhoto(peer.id, representation: smallProfileImage, size: displayDimensions, scale: scale) |> mapToSignal { cached -> Signal in - if let cached = cached { - return .single(cached) - } else { - let resourceData = account.postbox.mediaBox.resourceData(smallProfileImage.resource) - let imageData = resourceData - |> take(1) - |> mapToSignal { maybeData -> Signal in - if maybeData.complete { - return .single(try? Data(contentsOf: URL(fileURLWithPath: maybeData.path))) - } else { - return Signal { subscriber in - let resourceDataDisposable = resourceData.start(next: { data in - if data.complete { - subscriber.putNext(try? Data(contentsOf: URL(fileURLWithPath: maybeData.path))) - subscriber.putCompletion() - } - }, error: { error in - subscriber.putError(error) - }, completed: { - subscriber.putCompletion() - }) - let fetchedDataDisposable = account.postbox.mediaBox.fetchedResource(smallProfileImage.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)).start() - return ActionDisposable { - resourceDataDisposable.dispose() - fetchedDataDisposable.dispose() - } +enum PeerPhoto { + case peer(Peer, TelegramMediaImageRepresentation?, [String], Message?) +} + +private var capHolder:[String : CGImage] = [:] + +private func peerImage(account: Account, peer: Peer, displayDimensions: NSSize, representation: TelegramMediaImageRepresentation?, message: Message? = nil, displayLetters: [String], font: NSFont, scale: CGFloat, genCap: Bool, synchronousLoad: Bool) -> Signal<(CGImage?, Bool), NoError> { + if let representation = representation { + return cachedPeerPhoto(peer.id, representation: representation, size: displayDimensions, scale: scale) |> mapToSignal { cached -> Signal<(CGImage?, Bool), NoError> in + return autoreleasepool { + if let cached = cached { + return cachePeerPhoto(image: cached, peerId: peer.id, representation: representation, size: displayDimensions, scale: scale) |> map { + return (cached, false) + } + } else { + let resourceData = account.postbox.mediaBox.resourceData(representation.resource, attemptSynchronously: synchronousLoad) + let imageData = resourceData + |> take(1) + |> mapToSignal { maybeData -> Signal<(Data?, Bool, Bool), NoError> in + return autoreleasepool { + if maybeData.complete { + return .single((try? Data(contentsOf: URL(fileURLWithPath: maybeData.path)), false, false)) + } else { + return Signal { subscriber in + + if let data = representation.immediateThumbnailData { + subscriber.putNext((decodeTinyThumbnail(data: data), false, true)) + } + + let resourceData = account.postbox.mediaBox.resourceData(representation.resource, attemptSynchronously: synchronousLoad) + + let resourceDataDisposable = resourceData.start(next: { data in + if data.complete { + subscriber.putNext((try? Data(contentsOf: URL(fileURLWithPath: data.path)), true, false)) + subscriber.putCompletion() + } + }, completed: { + subscriber.putCompletion() + }) + + let fetchedDataDisposable: Disposable + if let message = message { + fetchedDataDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: MediaResourceReference.messageAuthorAvatar(message: MessageReference(message), resource: representation.resource), statsCategory: .image).start() + } else if let reference = PeerReference(peer) { + fetchedDataDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: MediaResourceReference.avatar(peer: reference, resource: representation.resource), statsCategory: .image).start() + } else { + fetchedDataDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: MediaResourceReference.standalone(resource: representation.resource), statsCategory: .image).start() + } + return ActionDisposable { + resourceDataDisposable.dispose() + fetchedDataDisposable.dispose() + } + } + } } + } + + let def = deferred({ () -> Signal<(CGImage?, Bool), NoError> in + let key = NSStringFromSize(displayDimensions) + if let image = capHolder[key] { + return .single((image, false)) + } else { + let size = NSMakeSize(max(15, displayDimensions.width), max(15, displayDimensions.height)) + capHolder[key] = generateAvatarPlaceholder(foregroundColor: theme.colors.grayBackground, size: size) + return .single((capHolder[key]!, false)) } - } - return imageData - |> deliverOn(account.graphicsThreadPool) - |> mapToSignal { data -> Signal in - - let image:CGImage? + }) |> deliverOnMainQueue + + let loadDataSignal = synchronousLoad ? imageData : imageData |> deliverOn(graphicsThreadPool) + + let img = loadDataSignal |> mapToSignal { data, animated, tiny -> Signal<(CGImage?, Bool), NoError> in + + var image:CGImage? if let data = data { - image = roundImage(data, displayDimensions, scale:scale) + image = roundImage(data, displayDimensions, scale: scale) } else { image = nil } + #if !SHARE + if tiny, let img = image { + let size = img.size + let ctx = DrawingContext(size: img.size, scale: 1.0) + ctx.withContext { ctx in + ctx.clear(size.bounds) + ctx.draw(img, in: size.bounds) + } + telegramFastBlurMore(Int32(size.width), Int32(size.height), Int32(ctx.bytesPerRow), ctx.bytes) + + image = ctx.generateImage() + } + #endif if let image = image { - return cachePeerPhoto(image: image, peerId: peer.id, representation: smallProfileImage, size: displayDimensions, scale: scale) |> map { - return image + if tiny { + return .single((image, animated)) + } + return cachePeerPhoto(image: image, peerId: peer.id, representation: representation, size: displayDimensions, scale: scale) |> map { + return (image, animated) } } else { - return .single(image) + return .single((image, animated)) } - + + } + if genCap { + return def |> then(img) + } else { + return img + } } } } } else { - var letters = peer.displayLetters + var letters = displayLetters if letters.count < 2 { while letters.count != 2 { letters.append("") @@ -95,74 +138,24 @@ public func peerAvatarImage(account: Account, peer: Peer, displayDimensions: CGS } - - - - - let colorIndex: Int32 - if peer.id.namespace == Namespaces.Peer.CloudUser { - colorIndex = colorIndexForUid(peer.id.id, account.peerId.id) - } else if peer.id.namespace == Namespaces.Peer.CloudChannel { - colorIndex = colorIndexForGroupId(TGPeerIdFromChannelId(peer.id.id)) - } else { - colorIndex = colorIndexForGroupId(-Int64(peer.id.id)) - } - let color = colors[abs(Int(colorIndex % 6))] + let color = theme.colors.peerColors(Int(abs(peer.id.id._internalGetInt64Value() % 7))) let symbol = letters.reduce("", { (current, letter) -> String in return current + letter }) - return cachedEmptyPeerPhoto(peer.id, symbol: symbol, color: color.top, size: displayDimensions, scale: scale) |> mapToSignal { cached -> Signal in + return cachedEmptyPeerPhoto(peer.id, symbol: symbol, color: color.top, size: displayDimensions, scale: scale) |> mapToSignal { cached -> Signal<(CGImage?, Bool), NoError> in if let cached = cached { - return .single(cached) + return .single((cached, false)) } else { - return Signal { subscriber in - let image = generateImage(displayDimensions, contextGenerator: { (size, ctx) in - ctx.clear(NSMakeRect(0, 0, size.width, size.height)) - ctx.beginPath() - ctx.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: - size.height)) - ctx.clip() - - - var locations: [CGFloat] = [1.0, 0.2]; - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: NSArray(array: [color.bottom.cgColor, color.top.cgColor]), locations: &locations)! - - ctx.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) - - ctx.setBlendMode(.normal) - - let letters = letters - let string = letters.count == 0 ? "" : (letters[0] + (letters.count == 1 ? "" : letters[1])) - let attributedString = NSAttributedString(string: string, attributes: [NSAttributedStringKey.font: font, NSAttributedStringKey.foregroundColor: NSColor.white]) - - let line = CTLineCreateWithAttributedString(attributedString) - let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds) - - let lineOrigin = CGPoint(x: floorToScreenPixels(-lineBounds.origin.x + (size.width - lineBounds.size.width) / 2.0) , y: floorToScreenPixels(-lineBounds.origin.y + (size.height - lineBounds.size.height) / 2.0)) - - ctx.translateBy(x: size.width / 2.0, y: size.height / 2.0) - ctx.scaleBy(x: 1.0, y: 1.0) - ctx.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) - // - ctx.translateBy(x: lineOrigin.x, y: lineOrigin.y) - CTLineDraw(line, ctx) - ctx.translateBy(x: -lineOrigin.x, y: -lineOrigin.y) - }) - - subscriber.putNext(image) - subscriber.putCompletion() - return EmptyDisposable - } |> runOn(account.graphicsThreadPool) |> mapToSignal { image -> Signal in + return generateEmptyPhoto(displayDimensions, type: .peer(colors: color, letter: letters, font: font)) |> runOn(graphicsThreadPool) |> mapToSignal { image -> Signal<(CGImage?, Bool), NoError> in if let image = image { return cacheEmptyPeerPhoto(image: image, peerId: peer.id, symbol: symbol, color: color.top, size: displayDimensions, scale: scale) |> map { - return image + return (image, false) } } else { - return .single(image) + return .single((image, false)) } } } @@ -171,26 +164,107 @@ public func peerAvatarImage(account: Account, peer: Peer, displayDimensions: CGS } } -func generateEmptyRoundAvatar(_ displayDimensions:NSSize, font: NSFont, account:Account, peer:Peer) -> Signal { +func peerAvatarImage(account: Account, photo: PeerPhoto, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0), scale:CGFloat = 1.0, font:NSFont = .medium(.title), genCap: Bool = true, synchronousLoad: Bool = false) -> Signal<(CGImage?, Bool), NoError> { + + switch photo { + case let .peer(peer, representation, displayLetters, message): + return peerImage(account: account, peer: peer, displayDimensions: displayDimensions, representation: representation, message: message, displayLetters: displayLetters, font: font, scale: scale, genCap: genCap, synchronousLoad: synchronousLoad) + } +} + +enum EmptyAvatartType { + case peer(colors:(top:NSColor, bottom: NSColor), letter: [String], font: NSFont) + case icon(colors:(top:NSColor, bottom: NSColor), icon: CGImage, iconSize: NSSize, cornerRadius: CGFloat?) +} + +func generateEmptyPhoto(_ displayDimensions:NSSize, type: EmptyAvatartType) -> Signal { + return Signal { subscriber in + + let color:(top: NSColor, bottom: NSColor) + let letters: [String]? + let icon: CGImage? + let iconSize: NSSize? + let font: NSFont? + let cornerRadius: CGFloat? + switch type { + case let .icon(colors, _icon, _iconSize, _cornerRadius): + color = colors + icon = _icon + letters = nil + font = nil + iconSize = _iconSize + cornerRadius = _cornerRadius + case let .peer(colors, _letters, _font): + color = colors + icon = nil + font = _font + letters = _letters + iconSize = nil + cornerRadius = nil + } + + let image = generateImage(displayDimensions, contextGenerator: { (size, ctx) in + ctx.clear(NSMakeRect(0, 0, size.width, size.height)) + + if let cornerRadius = cornerRadius { + ctx.round(size, cornerRadius) + } else { + ctx.round(size, size.height / 2) + } + //ctx.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: + // size.height)) + // ctx.clip() + + var locations: [CGFloat] = [1.0, 0.2]; + let colorSpace = deviceColorSpace + let gradient = CGGradient(colorsSpace: colorSpace, colors: NSArray(array: [color.top.cgColor, color.bottom.cgColor]), locations: &locations)! + + ctx.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + + ctx.setBlendMode(.normal) + + if let letters = letters, let font = font { + let string = letters.count == 0 ? "" : (letters[0] + (letters.count == 1 ? "" : letters[1])) + let attributedString = NSAttributedString(string: string, attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: NSColor.white]) + + let line = CTLineCreateWithAttributedString(attributedString) + let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds) + + let lineOrigin = CGPoint(x: floorToScreenPixels(System.backingScale, -lineBounds.origin.x + (size.width - lineBounds.size.width) / 2.0) , y: floorToScreenPixels(System.backingScale, -lineBounds.origin.y + (size.height - lineBounds.size.height) / 2.0)) + + ctx.translateBy(x: size.width / 2.0, y: size.height / 2.0) + ctx.scaleBy(x: 1.0, y: 1.0) + ctx.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + // + ctx.translateBy(x: lineOrigin.x, y: lineOrigin.y) + CTLineDraw(line, ctx) + ctx.translateBy(x: -lineOrigin.x, y: -lineOrigin.y) + } + + if let icon = icon, let iconSize = iconSize { + let rect = NSMakeRect((displayDimensions.width - iconSize.width)/2, (displayDimensions.height - iconSize.height)/2, iconSize.width, iconSize.height) + ctx.draw(icon, in: rect) + } + + }) + subscriber.putNext(image) + subscriber.putCompletion() + return EmptyDisposable + } +} + +func generateEmptyRoundAvatar(_ displayDimensions:NSSize, font: NSFont, account:Account, peer:Peer) -> Signal { return Signal { subscriber in let letters = peer.displayLetters - let colorIndex: Int32 - if peer.id.namespace == Namespaces.Peer.CloudUser { - colorIndex = colorIndexForUid(peer.id.id, account.peerId.id) - } else if peer.id.namespace == Namespaces.Peer.CloudChannel { - colorIndex = colorIndexForGroupId(TGPeerIdFromChannelId(peer.id.id)) - } else { - colorIndex = colorIndexForGroupId(-Int64(peer.id.id)) - } - let color = colors[abs(Int(colorIndex % 6))] + let color = theme.colors.peerColors(Int(abs(peer.id.id._internalGetInt64Value() % 7))) let image = generateImage(displayDimensions, contextGenerator: { (size, ctx) in ctx.clear(NSMakeRect(0, 0, size.width, size.height)) var locations: [CGFloat] = [1.0, 0.2]; - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: NSArray(array: [color.bottom.cgColor, color.top.cgColor]), locations: &locations)! + let colorSpace = deviceColorSpace + let gradient = CGGradient(colorsSpace: colorSpace, colors: NSArray(array: [color.top.cgColor, color.bottom.cgColor]), locations: &locations)! ctx.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) @@ -198,12 +272,12 @@ func generateEmptyRoundAvatar(_ displayDimensions:NSSize, font: NSFont, account: let letters = letters let string = letters.count == 0 ? "" : (letters[0] + (letters.count == 1 ? "" : letters[1])) - let attributedString = NSAttributedString(string: string, attributes: [NSAttributedStringKey.font: font, NSAttributedStringKey.foregroundColor: NSColor.white]) + let attributedString = NSAttributedString(string: string, attributes: [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: NSColor.white]) let line = CTLineCreateWithAttributedString(attributedString) let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds) - let lineOrigin = CGPoint(x: floorToScreenPixels(-lineBounds.origin.x + (size.width - lineBounds.size.width) / 2.0) , y: floorToScreenPixels(-lineBounds.origin.y + (size.height - lineBounds.size.height) / 2.0)) + let lineOrigin = CGPoint(x: floorToScreenPixels(System.backingScale, -lineBounds.origin.x + (size.width - lineBounds.size.width) / 2.0) , y: floorToScreenPixels(System.backingScale, -lineBounds.origin.y + (size.height - lineBounds.size.height) / 2.0)) ctx.translateBy(x: size.width / 2.0, y: size.height / 2.0) ctx.scaleBy(x: 1.0, y: 1.0) diff --git a/Telegram-Mac/InAppLinks.swift b/Telegram-Mac/InAppLinks.swift index 6ff1004241..9135f0fc6d 100644 --- a/Telegram-Mac/InAppLinks.swift +++ b/Telegram-Mac/InAppLinks.swift @@ -7,14 +7,67 @@ // import Cocoa +import Foundation import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit +import MtProtoKit +//import WalletCore + private let inapp:String = "chat://" private let tgme:String = "tg://" + + +func resolveUsername(username: String, context: AccountContext) -> Signal { + if username.hasPrefix("_private_"), let range = username.range(of: "_private_") { + if let channelId = Int64(username[range.upperBound...]) { + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) + + let peerSignal: Signal = context.account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peerId) + } |> mapToSignal { peer in + if let peer = peer { + return .single(peer) + } else { + return context.engine.peers.findChannelById(channelId: peerId.id._internalGetInt64Value()) + } + } + + return peerSignal |> deliverOnMainQueue |> map { peer in + if let peer = peer { + if let peer = peer as? TelegramChannel { + if peer.participationStatus != .member { + return nil + } + } + } + return peer + } + } else { + return .single(nil) + } + } else { + return context.engine.peers.resolvePeerByName(name: username) |> mapToSignal { peerId -> Signal in + if let peerId = peerId { + return context.account.postbox.loadedPeerWithId(peerId._asPeer().id) |> map(Optional.init) + } + return .single(nil) + } |> deliverOnMainQueue + } + +} + +enum InAppSettingsSection : String { + case themes + case devices + case folders + case privacy +} + enum ChatInitialActionBehavior : Equatable { case none case automatic @@ -24,60 +77,246 @@ enum ChatInitialAction : Equatable { case start(parameter: String, behavior: ChatInitialActionBehavior) case inputText(text: String, behavior: ChatInitialActionBehavior) case files(list: [String], behavior: ChatInitialActionBehavior) -} - -func ==(lhs:ChatInitialAction, rhs:ChatInitialAction) -> Bool { - switch lhs { - case let .start(lhsText, lhsBehavior): - if case let .start(rhsText, rhsBehavior) = rhs, lhsText == rhsText, lhsBehavior == rhsBehavior { + case forward(messageIds: [MessageId], text: ChatTextInputState?, behavior: ChatInitialActionBehavior) + case ad(PromoChatListItem.Kind) + case source(MessageId) + case closeAfter(Int32) + case selectToReport(reason: ReportReasonValue) + case joinVoiceChat(_ joinHash: String?) + case openMedia(_ timemark: Int32?) + var selectionNeeded: Bool { + switch self { + case .selectToReport: return true - } else { + default: return false } - case let .inputText(lhsText, lhsBehavior): - if case let .inputText(rhsText, rhsBehavior) = rhs, lhsText == rhsText, lhsBehavior == rhsBehavior { - return true - } else { + } +} + + + +var globalLinkExecutor:TextViewInteractions { + get { + return TextViewInteractions(processURL:{(link) in + if let link = link as? inAppLink { + switch link { + case .requestSecureId: + break // never execute from inapp + default: + execute(inapp:link) + } + } + }, isDomainLink: { value, origin in + if let value = value as? inAppLink { + switch value { + case .external: + return true + default: + if let origin = origin { + if origin != value.link, !origin.isEmpty && origin != "‌" { + return true + } + } + return false + } + } return false - } - case let .files(lhsList, lhsBehavior): - if case let .files(rhsList, rhsBehavior) = rhs, lhsList == rhsList, lhsBehavior == rhsBehavior { - return true - } else { + }, makeLinkType: { link, url in + if let link = link as? inAppLink { + switch link { + case .botCommand: + return .command + case .hashtag: + return .hashtag + case .code: + return .code + case .followResolvedName: + if url.hasPrefix("@") { + return .username + } else { + return .plain + } + case let .external(link, _): + if isValidEmail(link) { + return .email + } else if telegram_me.first(where: {link.contains($0 + "joinchat/")}) != nil { + return .inviteLink + } else { + return .plain + } + case .stickerPack: + return .stickerPack + case .joinchat: + return .inviteLink + default: + return .plain + } + } + return .plain + }, localizeLinkCopy: { type in + return copyContextText(from: type) + }, resolveLink: { link in + return (link as? inAppLink)?.link + }, copyAttributedString: { string in + let pb = NSPasteboard.general + + if !FastSettings.enableRTF { + pb.clearContents() + pb.declareTypes([.string], owner: nil) + pb.setString(string.string, forType: .string) + return true + } + let modified: NSMutableAttributedString = string.mutableCopy() as! NSMutableAttributedString + + string.enumerateAttributes(in: string.range, options: [], using: { attr, range, _ in + if let appLink = attr[NSAttributedString.Key.link] as? inAppLink { + switch appLink { + case .code, .hashtag, .callback: + break + default: + if appLink.link != modified.string.nsstring.substring(with: range) { + modified.addAttribute(NSAttributedString.Key.link, value: appLink.link, range: range) + } + } + + } else if let appLink = attr[NSAttributedString.Key(TGCustomLinkAttributeName)] as? TGInputTextTag { + if (appLink.attachment as? String) != modified.string.nsstring.substring(with: range) { + modified.addAttribute(NSAttributedString.Key.link, value: appLink.attachment, range: range) + } + } else if attr[.foregroundColor] != nil { + modified.removeAttribute(.foregroundColor, range: range) + } else if let font = attr[.font] as? NSFont { + if let newFont = NSFont(name: font.fontName, size: 0) { + modified.setFont(font: newFont, range: range) + } + } else if attr[.paragraphStyle] != nil { + modified.removeAttribute(.paragraphStyle, range: range) + } + }) + + + if !modified.string.isEmpty { + pb.clearContents() + + let rtf = try? modified.data(from: modified.range, documentAttributes: [NSAttributedString.DocumentAttributeKey.documentType : NSAttributedString.DocumentType.rtf]) + if let rtf = rtf { + pb.declareTypes([.rtf], owner: nil) + pb.setData(rtf, forType: .rtf) + pb.setString(modified.string, forType: .string) + return true + } + } + return false - } + }) } } -let globalLinkExecutor:TextViewInteractions = TextViewInteractions(processURL:{(link) in - if let link = link as? inAppLink { - execute(inapp:link) +func copyContextText(from type: LinkType) -> String { + switch type { + case .username: + return L10n.textContextCopyUsername + case .command: + return L10n.textContextCopyCommand + case .hashtag: + return L10n.textContextCopyHashtag + case .email: + return L10n.textContextCopyEmail + case .plain: + return L10n.textContextCopyLink + case .inviteLink: + return L10n.textContextCopyInviteLink + case .stickerPack: + return L10n.textContextCopyStickerPack + case .code: + return L10n.textContextCopyCode } -}) +} -func execute(inapp:inAppLink) { +func execute(inapp:inAppLink, afterComplete: @escaping(Bool)->Void = { _ in }) { switch inapp { - case let .external(link,needConfirm): + case let .external(link, needConfirm): var url:String = link.trimmed + + + + var reversedUrl = String(url.reversed()) + while reversedUrl.components(separatedBy: "#").count > 2 { + if let index = reversedUrl.range(of: "#") { + reversedUrl.replaceSubrange(index, with: "32%") + } + } + url = String(reversedUrl.reversed()) if !url.hasPrefix("http") && !url.hasPrefix("ftp") { - if isValidEmail(link) { - url = "mailto:" + url + if let range = url.range(of: "://") { + if url.length > 10, range.lowerBound > url.index(url.startIndex, offsetBy: 10) { + url = "http://" + url + } } else { - url = "http://" + url + if isValidEmail(link) { + // url = "mailto:" + url + } else { + url = "http://" + url + } } } - if let url = URL(string: escape(with:url)) { - let success:()->Void = { - NSWorkspace.shared.open(url) - } - if needConfirm { - confirm(for: mainWindow, with: appName, and: tr(.inAppLinksConfirmOpenExternal(url.absoluteString)), successHandler: {_ in success()}) - } else { - success() + let urlValue = url + let escaped = escape(with:url) + if let urlQueryAllowed = Optional(escaped) { + if let url = URL(string: urlQueryAllowed) ?? URL(string: urlValue) { + var needConfirm = needConfirm || url.host != URL(string: urlValue)?.host + + if needConfirm { + let allowed = ["telegram.org", "telegram.dog", "telegram.me", "telesco.pe"] + if let url = URL(string: urlValue) { + if let host = url.host, allowed.contains(host) { + needConfirm = false + } + } + } + + if let withToken = appDelegate?.tryApplyAutologinToken(url.absoluteString), let url = URL(string: withToken) { + NSWorkspace.shared.open(url) + afterComplete(true) + return + } + + let removePecentEncoding = url.host == URL(string: urlValue)?.host + let success:()->Void = { + + var path = url.absoluteString + let supportSchemes:[String] = ["itunes.apple.com"] + for scheme in supportSchemes { + var url:URL? = nil + if path.contains(scheme) { + switch scheme { + case supportSchemes[0]: // itunes + path = "itms://" + path.nsstring.substring(from: path.nsstring.range(of: scheme).location) + url = URL(string: path) + default: + continue + } + } + if let url = url { + NSWorkspace.shared.open(url) + afterComplete(true) + return + } + } + afterComplete(true) + NSWorkspace.shared.open(url) + } + if needConfirm { + confirm(for: mainWindow, header: L10n.inAppLinksConfirmOpenExternalHeader, information: L10n.inAppLinksConfirmOpenExternalNew(removePecentEncoding ? (url.absoluteString.removingPercentEncoding ?? url.absoluteString) : escaped), okTitle: L10n.inAppLinksConfirmOpenExternalOK, successHandler: {_ in success()}, cancelHandler: { afterComplete(false) }) + } else { + success() + } } + } - case let .peerInfo(peerId, action, openChat, postId, callback): + case let .peerInfo(_, peerId, action, openChat, postId, callback): let messageId:MessageId? if let postId = postId { messageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: MessageId.Id(postId)) @@ -85,189 +324,739 @@ func execute(inapp:inAppLink) { messageId = nil } callback(peerId, openChat, messageId, action) - case let .followResolvedName(username, postId, account, action, callback): - let _ = showModalProgress(signal: resolvePeerByName(account: account, name: username) |> mapToSignal { peerId -> Signal in - if let peerId = peerId { - return account.postbox.loadedPeerWithId(peerId) |> map {Optional($0)} + afterComplete(true) + case let .comments(_, username, context, threadId, commentId): + + enum Error { + case doesntExists + case privateAccess + case generic + } + + var peerSignal: Signal = .fail(.doesntExists) + if username.hasPrefix("_private_"), let range = username.range(of: "_private_") { + if let channelId = Int64(username[range.upperBound...]) { + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) + peerSignal = context.account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peerId) + } |> mapToSignalPromotingError { peer in + if let peer = peer { + return .single(peer) + } else { + return context.engine.peers.findChannelById(channelId: peerId.id._internalGetInt64Value()) + |> mapToSignalPromotingError { value in + if let value = value { + return .single(value) + } else { + return .fail(.privateAccess) + } + } + + } + } } - return .single(nil) - } |> deliverOnMainQueue, for: mainWindow).start(next: { peer in - if let peer = peer { - let messageId:MessageId? - if let postId = postId { - messageId = MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: postId) - } else { - messageId = nil + } else { + peerSignal = context.engine.peers.resolvePeerByName(name: username) |> mapToSignalPromotingError { peerId -> Signal in + if let peerId = peerId { + return context.account.postbox.loadedPeerWithId(peerId._asPeer().id) |> mapError { _ in + return .doesntExists + } } - callback(peer.id, peer.isChannel || peer.isSupergroup || peer.isBot, messageId, action) - } else { - alert(for: mainWindow, header: appName, info: tr(.alertUserDoesntExists)) + return .fail(.doesntExists) + } |> mapError { _ in + return .doesntExists } + } + + let signal:Signal<(ReplyThreadInfo, Peer), Error> = peerSignal |> mapToSignal { peer in + let messageId: MessageId = MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: threadId) + return fetchAndPreloadReplyThreadInfo(context: context, subject: peer.isChannel ? .channelPost(messageId) : .groupMessage(messageId)) + |> map { + return ($0, peer) + } |> mapError { error in + switch error { + case .generic: + return .generic + } + } + } |> deliverOnMainQueue + + + _ = showModalProgress(signal: signal |> take(1), for: context.window).start(next: { values in + let (result, peer) = values + let threadMessageId: MessageId = MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: threadId) + let navigation = context.sharedContext.bindings.rootNavigation() + let current = navigation.controller as? ChatController + if let current = current, current.chatInteraction.mode.threadId == result.message.messageId { + if let commentId = commentId { + let commentMessageId = MessageId(peerId: result.message.messageId.peerId, namespace: Namespaces.Message.Cloud, id: commentId) + current.chatInteraction.focusMessageId(nil, commentMessageId, .CenterEmpty) + } + } else { + let mode: ReplyThreadMode + if peer.isChannel { + mode = .comments(origin: threadMessageId) + } else { + mode = .replies(origin: threadMessageId) + } + var commentMessageId: MessageId? = nil + if let commentId = commentId { + commentMessageId = MessageId(peerId: result.message.messageId.peerId, namespace: Namespaces.Message.Cloud, id: commentId) + } + + navigation.push(ChatAdditionController(context: context, chatLocation: .replyThread(result.message), mode: .replyThread(data: result.message, mode: mode), messageId: commentMessageId, initialAction: nil, chatLocationContextHolder: result.contextHolder)) + } + }, error: { error in + switch error { + case .doesntExists: + alert(for: context.window, info: L10n.alertUserDoesntExists) + case .privateAccess: + alert(for: context.window, info: L10n.alertPrivateChannelAccessError) + case .generic: + break + } }) - case let .inviteBotToGroup(username, account, action, callback): - let _ = (showModalProgress(signal: resolvePeerByName(account: account, name: username) |> filter {$0 != nil} |> map{$0!} |> deliverOnMainQueue, for: mainWindow) |> mapToSignal { memberId -> Signal in + afterComplete(true) + case let .followResolvedName(_, username, postId, context, action, callback): + + if username.hasPrefix("_private_"), let range = username.range(of: "_private_") { + if let channelId = Int64(username[range.upperBound...]) { + let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)) + + let peerSignal: Signal = context.account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peerId) + } |> mapToSignal { peer in + if let peer = peer { + return .single(peer) + } else { + return context.engine.peers.findChannelById(channelId: peerId.id._internalGetInt64Value()) + } + } + + _ = showModalProgress(signal: peerSignal |> deliverOnMainQueue, for: context.window).start(next: { peer in + if let peer = peer { + let messageId:MessageId? + if let postId = postId { + messageId = MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: postId) + } else { + messageId = nil + } + if let peer = peer as? TelegramChannel { + if peer.participationStatus == .kicked { + alert(for: context.window, info: L10n.alertPrivateChannelAccessError) + return + } + } + callback(peer.id, peer.isChannel || peer.isSupergroup || peer.isBot, messageId, action) + } else { + alert(for: context.window, info: L10n.alertPrivateChannelAccessError) + } + }) + } else { + alert(for: context.window, info: L10n.alertPrivateChannelAccessError) + } + } else { + let _ = showModalProgress(signal: context.engine.peers.resolvePeerByName(name: username) |> mapToSignal { peerId -> Signal in + if let peerId = peerId { + return context.account.postbox.loadedPeerWithId(peerId._asPeer().id) |> map {Optional($0)} + } + return .single(nil) + } |> deliverOnMainQueue, for: context.window).start(next: { peer in + if let peer = peer { + let messageId:MessageId? + if let postId = postId { + messageId = MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: postId) + } else { + messageId = nil + } + callback(peer.id, peer.isChannel || peer.isSupergroup || peer.isBot, messageId, action) + } else { + alert(for: context.window, info: tr(L10n.alertUserDoesntExists)) + } + + }) + } + afterComplete(true) + case let .inviteBotToGroup(_, username, context, action, callback): + let _ = showModalProgress(signal: context.engine.peers.resolvePeerByName(name: username) |> filter {$0 != nil} |> map{$0!} |> deliverOnMainQueue, for: context.window).start(next: { botPeerId in - return selectModalPeers(account: account, title: "", behavior: SelectChatsBehavior(limit: 1), confirmation: { peerIds -> Signal in + let selectedPeer = selectModalPeers(window: context.window, context: context, title: L10n.selectPeersTitleSelectChat, behavior: SelectChatsBehavior(limit: 1), confirmation: { peerIds -> Signal in if let peerId = peerIds.first { - return account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue |> mapToSignal { peer -> Signal in - return confirmSignal(for: mainWindow, header: appName, information: tr(.confirmAddBotToGroup(peer.displayTitle))) + return context.account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue |> mapToSignal { peer -> Signal in + return confirmSignal(for: context.window, information: L10n.confirmAddBotToGroup(peer.displayTitle)) } } return .single(false) - }) |> deliverOnMainQueue |> filter {$0.first != nil} |> map {$0.first!} |> mapToSignal { peerId in - return showModalProgress(signal: addPeerMember(account: account, peerId: peerId, memberId: memberId), for: mainWindow) |> mapError {_ in} |> map {peerId} - } - }).start(next: { peerId in - callback(peerId, true, nil, action) - }, error: { + }) |> deliverOnMainQueue |> filter { $0.first != nil } |> map { $0.first! } + + let signal:Signal<(StartBotInGroupResult, PeerId), NoError> = selectedPeer |> mapToSignal { peerId in + var payload: String = "" + if let action = action { + switch action { + case let .start(data, _): + payload = data + default: + break + } + } + if payload.isEmpty { + if peerId.namespace == Namespaces.Peer.CloudGroup { + return showModalProgress(signal: context.engine.peers.addGroupMember(peerId: peerId, memberId: botPeerId._asPeer().id), for: context.window) + |> map { (.none, peerId) } + |> `catch` { _ -> Signal<(StartBotInGroupResult, PeerId), NoError> in return .single((.none, peerId)) } + } else { + return showModalProgress(signal: context.peerChannelMemberCategoriesContextsManager.addMember(peerId: peerId, memberId: botPeerId._asPeer().id), for: context.window) + |> map { _ in (.none, peerId) } + |> then(.single((.none, peerId))) + } + } else { + return showModalProgress(signal: context.engine.messages.requestStartBotInGroup(botPeerId: botPeerId._asPeer().id, groupPeerId: peerId, payload: payload), for: context.window) + |> map { + ($0, peerId) + } + |> `catch` { _ -> Signal<(StartBotInGroupResult, PeerId), NoError> in return .single((.none, peerId)) } + + } + } |> deliverOnMainQueue + _ = signal.start(next: { result, peerId in + switch result { + case let .channelParticipant(participant): + context.peerChannelMemberCategoriesContextsManager.externallyAdded(peerId: peerId, participant: participant) + case .none: + break + } + callback(peerId, true, nil, nil) + }) }) + afterComplete(true) case let .botCommand(command, interaction): interaction(command) + afterComplete(true) case let .hashtag(hashtag, interaction): interaction(hashtag) - case let .joinchat(hash, account, interaction): - _ = showModalProgress(signal: joinLinkInformation(hash, account: account), for: mainWindow).start(next: { (result) in + afterComplete(true) + case let .joinchat(_, hash, context, interaction): + _ = showModalProgress(signal: context.engine.peers.joinLinkInformation(hash), for: context.window).start(next: { (result) in switch result { case let .alreadyJoined(peerId): interaction(peerId, true, nil, nil) case .invite: - showModal(with: JoinLinkPreviewModalController(account, hash: hash, join: result, interaction: { peerId in + showModal(with: JoinLinkPreviewModalController(context, hash: hash, join: result, interaction: { peerId in if let peerId = peerId { interaction(peerId, true, nil, nil) } - }), for: mainWindow) + }), for: context.window) + case let .peek(peerId, peek): + interaction(peerId, true, nil, .closeAfter(peek)) case .invalidHash: - alert(for: mainWindow, info: tr(.groupUnavailable)) + alert(for: context.window, info: L10n.linkExpired) } }) + afterComplete(true) case let .callback(param, interaction): interaction(param) + afterComplete(true) + case let .code(param, interaction): + interaction(param) + afterComplete(true) case let .logout(interaction): interaction() - case let .shareUrl(account, url): + afterComplete(true) + case let .shareUrl(_, context, url): if !url.hasPrefix("@") { - showModal(with: ShareModalController(ShareLinkObject(account, link: url)), for: mainWindow) + showModal(with: ShareModalController(ShareLinkObject(context, link: url)), for: context.window) + } + afterComplete(true) + case let .wallpaper(_, context, preview): + switch preview { + case let .gradient(id, colors, settings): + let wallpaper: TelegramWallpaper = .gradient(.init(id: id, colors: colors.map { $0.argb }, settings: settings)) + showModal(with: WallpaperPreviewController(context, wallpaper: Wallpaper(wallpaper), source: .link(wallpaper)), for: context.window) + case let .color(color): + let wallpaper: TelegramWallpaper = .color(color.argb) + showModal(with: WallpaperPreviewController(context, wallpaper: Wallpaper(wallpaper), source: .link(wallpaper)), for: context.window) + case let .slug(slug, settings): + _ = showModalProgress(signal: getWallpaper(network: context.account.network, slug: slug) |> deliverOnMainQueue, for: context.window).start(next: { wallpaper in + showModal(with: WallpaperPreviewController(context, wallpaper: Wallpaper(wallpaper).withUpdatedSettings(settings), source: .link(wallpaper)), for: context.window) + }, error: { error in + switch error { + case .generic: + alert(for: context.window, info: L10n.wallpaperPreviewDoesntExists) + } + }) } - case let .stickerPack(reference, account, peerId): - showModal(with: StickersPackPreviewModalController(account, peerId: peerId, reference: reference), for: mainWindow) - case let .socks(settings, applyProxy): + afterComplete(true) + case let .stickerPack(_, reference, context, peerId): + showModal(with: StickerPackPreviewModalController(context, peerId: peerId, reference: reference), for: context.window) + afterComplete(true) + case let .confirmPhone(_, context, phone, hash): + _ = showModalProgress(signal: context.engine.auth.requestCancelAccountResetData(hash: hash) |> deliverOnMainQueue, for: context.window).start(next: { data in + showModal(with: cancelResetAccountController(context: context, phone: phone, data: data), for: context.window) + }, error: { error in + switch error { + case .limitExceeded: + alert(for: context.window, info: L10n.loginFloodWait) + case .generic: + alert(for: context.window, info: L10n.unknownError) + } + }) + afterComplete(true) + case let .socks(_, settings, applyProxy): applyProxy(settings) + afterComplete(true) case .nothing: - break + afterComplete(true) + case let .requestSecureId(_, context, value): + if value.nonce.isEmpty { + alert(for: context.window, info: value.isModern ? "nonce is empty" : "payload is empty") + return + } + _ = showModalProgress(signal: (requestSecureIdForm(postbox: context.account.postbox, network: context.account.network, peerId: value.peerId, scope: value.scope, publicKey: value.publicKey) |> mapToSignal { form in + return context.account.postbox.loadedPeerWithId(context.peerId) |> mapError {_ in return .generic} |> map { peer in + return (form, peer) + } + } |> deliverOnMainQueue), for: context.window).start(next: { form, peer in + let passport = PassportWindowController(context: context, peer: peer, request: value, form: form) + passport.show() + }, error: { error in + switch error { + case .serverError(let text): + alert(for: context.window, info: text) + case .generic: + alert(for: context.window, info: "An error occured") + case .versionOutdated: + updateAppAsYouWish(text: L10n.secureIdAppVersionOutdated, updateApp: true) + } + }) + afterComplete(true) + case let .applyLocalization(_, context, value): + _ = showModalProgress(signal: context.engine.localization.requestLocalizationPreview(identifier: value) |> deliverOnMainQueue, for: context.window).start(next: { info in + if appAppearance.language.primaryLanguage.languageCode == info.languageCode { + alert(for: context.window, info: L10n.applyLanguageChangeLanguageAlreadyActive(info.title)) + } else if info.totalStringCount == 0 { + confirm(for: context.window, header: L10n.applyLanguageUnsufficientDataTitle, information: L10n.applyLanguageUnsufficientDataText(info.title), cancelTitle: "", thridTitle: L10n.applyLanguageUnsufficientDataOpenPlatform, successHandler: { result in + switch result { + case .basic: + break + case .thrid: + execute(inapp: inAppLink.external(link: info.platformUrl, false)) + } + }) + } else { + showModal(with: LocalizationPreviewModalController(context, info: info), for: context.window) + } + + }, error: { error in + switch error { + case .generic: + alert(for: context.window, info: L10n.localizationPreviewErrorGeneric) + } + }) + afterComplete(true) + case let .theme(_, context, name): + _ = showModalProgress(signal: getTheme(account: context.account, slug: name), for: context.window).start(next: { value in + if value.file == nil, let _ = value.settings { + showModal(with: ThemePreviewModalController(context: context, source: .cloudTheme(value)), for: context.window) + } else if value.file == nil { + showEditThemeModalController(context: context, theme: value) + } else { + showModal(with: ThemePreviewModalController(context: context, source: .cloudTheme(value)), for: context.window) + } + }, error: { error in + switch error { + case .generic: + alert(for: context.window, info: L10n.themeGetThemeError) + case .unsupported: + alert(for: context.window, info: L10n.themeGetThemeError) + case .slugInvalid: + alert(for: context.window, info: L10n.themeGetThemeError) + } + }) + afterComplete(true) + case let .unsupportedScheme(_, context, path): + _ = (context.engine.resolve.getDeepLinkInfo(path: path) |> deliverOnMainQueue).start(next: { info in + if let info = info { + updateAppAsYouWish(text: info.message, updateApp: info.updateApp) + } + }) + afterComplete(true) + case let .tonTransfer(_, context, data: data): + if #available(OSX 10.12, *) { + + } + case .instantView: + afterComplete(true) + case let .settings(_, context, section): + let controller: ViewController + switch section { + case .themes: + controller = AppAppearanceViewController(context: context) + case .devices: + controller = RecentSessionsController(context) + case .folders: + controller = ChatListFiltersListController(context: context) + case .privacy: + controller = PrivacyAndSecurityViewController(context, initialSettings: nil, focusOnItemTag: .autoArchive) + } + context.sharedContext.bindings.rootNavigation().push(controller) + afterComplete(true) + case let .joinGroupCall(_, context, peerId, callId): + selectGroupCallJoiner(context: context, peerId: peerId, completion: { peerId, schedule in + _ = showModalProgress(signal: requestOrJoinGroupCall(context: context, peerId: peerId, joinAs: context.peerId, initialCall: callId), for: context.window).start(next: { result in + switch result { + case let .success(callContext), let .samePeer(callContext): + applyGroupCallResult(context.sharedContext, callContext) + default: + alert(for: context.window, info: L10n.errorAnError) + } + }) + }) + + afterComplete(true) } } -private func escape(with link:String) -> String { - var escaped = link.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) - escaped = escaped?.replacingOccurrences(of: "%24", with: "$") - escaped = escaped?.replacingOccurrences(of: "%26", with: "&") - escaped = escaped?.replacingOccurrences(of: "%2B", with: "+") - escaped = escaped?.replacingOccurrences(of: "%2C", with: ",") - escaped = escaped?.replacingOccurrences(of: "%2F", with: "/") - escaped = escaped?.replacingOccurrences(of: "%3A", with: ":") - escaped = escaped?.replacingOccurrences(of: "%3B", with: ";") - escaped = escaped?.replacingOccurrences(of: "%3D", with: "=") - escaped = escaped?.replacingOccurrences(of: "%3F", with: "?") - escaped = escaped?.replacingOccurrences(of: "%40", with: "@") - escaped = escaped?.replacingOccurrences(of: "%20", with: " ") - escaped = escaped?.replacingOccurrences(of: "%09", with: "\t") - escaped = escaped?.replacingOccurrences(of: "%23", with: "#") - escaped = escaped?.replacingOccurrences(of: "%3C", with: "<") - escaped = escaped?.replacingOccurrences(of: "%3E", with: ">") - escaped = escaped?.replacingOccurrences(of: "%22", with: "\"") - escaped = escaped?.replacingOccurrences(of: "%0A", with: "\n") - escaped = escaped?.replacingOccurrences(of: "%25", with: "%") - escaped = escaped?.replacingOccurrences(of: "%2E", with: ".") - return escaped ?? link +private func updateAppAsYouWish(text: String, updateApp: Bool) { + // + confirm(for: mainWindow, header: appName, information: text, okTitle: updateApp ? L10n.alertButtonOKUpdateApp : L10n.modalOK, cancelTitle: updateApp ? L10n.modalCancel : "", thridTitle: nil, successHandler: { _ in + if updateApp { + #if APP_STORE + execute(inapp: inAppLink.external(link: "https://apps.apple.com/us/app/telegram/id747648890", false)) + #else + (NSApp.delegate as? AppDelegate)?.checkForUpdates(updateApp) + #endif + } + }) } -private func urlVars(with url:String) -> [String:String] { +func escape(with link:String, addPercent: Bool = true) -> String { + var escaped = addPercent ? link.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? link : link + escaped = escaped.replacingOccurrences(of: "%21", with: "!") + escaped = escaped.replacingOccurrences(of: "%24", with: "$") + escaped = escaped.replacingOccurrences(of: "%26", with: "&") + escaped = escaped.replacingOccurrences(of: "%2B", with: "+") + escaped = escaped.replacingOccurrences(of: "%2C", with: ",") + escaped = escaped.replacingOccurrences(of: "%2F", with: "/") + escaped = escaped.replacingOccurrences(of: "%3A", with: ":") + escaped = escaped.replacingOccurrences(of: "%3B", with: ";") + escaped = escaped.replacingOccurrences(of: "%3D", with: "=") + escaped = escaped.replacingOccurrences(of: "%3F", with: "?") + escaped = escaped.replacingOccurrences(of: "%40", with: "@") + escaped = escaped.replacingOccurrences(of: "%20", with: " ") + escaped = escaped.replacingOccurrences(of: "%09", with: "\t") + escaped = escaped.replacingOccurrences(of: "%23", with: "#") + escaped = escaped.replacingOccurrences(of: "%3C", with: "<") + escaped = escaped.replacingOccurrences(of: "%3E", with: ">") + escaped = escaped.replacingOccurrences(of: "%22", with: "\"") + escaped = escaped.replacingOccurrences(of: "%0A", with: "\n") + escaped = escaped.replacingOccurrences(of: "%25", with: "%") + escaped = escaped.replacingOccurrences(of: "%2E", with: ".") + escaped = escaped.replacingOccurrences(of: "%2C", with: ",") + escaped = escaped.replacingOccurrences(of: "%7D", with: "}") + escaped = escaped.replacingOccurrences(of: "%7B", with: "{") + escaped = escaped.replacingOccurrences(of: "%5B", with: "[") + escaped = escaped.replacingOccurrences(of: "%5D", with: "]") + return escaped +} + + +func urlVars(with url:String) -> ([String:String], Set) { var vars:[String:String] = [:] let range = url.nsstring.range(of: "?") let ns:NSString = range.location != NSNotFound ? url.nsstring.substring(from: range.location + 1).nsstring : url.nsstring let hashes = ns.components(separatedBy: "&") + var emptyVars:Set = Set() for hash in hashes { + let param = hash.components(separatedBy: "=") if param.count > 1 { - vars[param[0]] = param[1] + if hashes.count == 1 { + var value: String = param[1] + for (i, p) in param.enumerated() { + if i > 1 { + value += "=\(p)" + } + } + vars[param[0]] = value + } else { + vars[param[0]] = param[1] + } + } else if param.count == 1 { + emptyVars.insert(param[0]) } } - return vars + return (vars, emptyVars) } -private func isValidEmail(_ checkString:String) -> Bool { - let emailRegex = ".+@([A-Za-z0-9-]+\\.)+[A-Za-z]{2}[A-Za-z]*" - let emailTest = NSPredicate(format: "SELF MATCHES %@", emailRegex) - return emailTest.evaluate(with: checkString) + +enum SecureIdPermission : String { + case identity = "identity" + case address = "address" + case email = "email" + case phone = "phone" } +struct inAppSecureIdRequest { + let peerId: PeerId + let scope: String + let callback: String? + let publicKey: String + let nonce: Data + let isModern: Bool +} + + + +enum WallpaperPreview { + case color(NSColor) + case slug(String, WallpaperSettings) + case gradient(Int64?, [NSColor], WallpaperSettings) +} enum inAppLink { case external(link:String, Bool) // link, confirm - case peerInfo(peerId:PeerId, action:ChatInitialAction?, openChat:Bool, postId:Int32?, callback:(PeerId, Bool, MessageId?, ChatInitialAction?)->Void) - case followResolvedName(username:String, postId:Int32?, account:Account, action:ChatInitialAction?, callback:(PeerId, Bool, MessageId?, ChatInitialAction?)->Void) - case inviteBotToGroup(username:String, account:Account, action:ChatInitialAction?, callback:(PeerId, Bool, MessageId?, ChatInitialAction?)->Void) + case peerInfo(link: String, peerId:PeerId, action:ChatInitialAction?, openChat:Bool, postId:Int32?, callback:(PeerId, Bool, MessageId?, ChatInitialAction?)->Void) + case followResolvedName(link: String, username:String, postId:Int32?, context: AccountContext, action:ChatInitialAction?, callback:(PeerId, Bool, MessageId?, ChatInitialAction?)->Void) + case comments(link: String, username:String, context: AccountContext, threadId: Int32, commentId: Int32?) + case inviteBotToGroup(link: String, username:String, context: AccountContext, action:ChatInitialAction?, callback:(PeerId, Bool, MessageId?, ChatInitialAction?)->Void) case botCommand(String, (String)->Void) case callback(String, (String)->Void) + case code(String, (String)->Void) case hashtag(String, (String)->Void) - case shareUrl(Account, String) - case joinchat(String, account:Account, callback:(PeerId, Bool, MessageId?, ChatInitialAction?)->Void) + case shareUrl(link: String, AccountContext, String) + case joinchat(link: String, String, context: AccountContext, callback:(PeerId, Bool, MessageId?, ChatInitialAction?)->Void) case logout(()->Void) - case stickerPack(StickerPackReference, account:Account, peerId:PeerId?) + case stickerPack(link: String, StickerPackReference, context: AccountContext, peerId:PeerId?) + case confirmPhone(link: String, context: AccountContext, phone: String, hash: String) case nothing - case socks(ProxySettings, applyProxy:(ProxySettings)->Void) + case socks(link: String, ProxyServerSettings, applyProxy:(ProxyServerSettings)->Void) + case requestSecureId(link: String, context: AccountContext, value: inAppSecureIdRequest) + case unsupportedScheme(link: String, context: AccountContext, path: String) + case applyLocalization(link: String, context: AccountContext, value: String) + case wallpaper(link: String, context: AccountContext, preview: WallpaperPreview) + case theme(link: String, context: AccountContext, name: String) + case tonTransfer(link: String, context: AccountContext, data: ParsedWalletUrl) + case instantView(link: String, webpage: TelegramMediaWebpage, anchor: String?) + case settings(link: String, context: AccountContext, section: InAppSettingsSection) + case joinGroupCall(link: String, context: AccountContext, peerId: PeerId, call: CachedChannelData.ActiveCall) + var link: String { + switch self { + case let .external(link,_): + if link.hasPrefix("mailto:") { + return link.replacingOccurrences(of: "mailto:", with: "") + } + return link + case let .peerInfo(values): + return values.link + case let .comments(values): + return values.link + case let .followResolvedName(values): + return values.link + case let .inviteBotToGroup(values): + return values.link + case let .botCommand(link, _), let .callback(link, _), let .code(link, _), let .hashtag(link, _): + return link + case let .shareUrl(values): + return values.link + case let .joinchat(values): + return values.link + case let .stickerPack(values): + return values.link + case let .confirmPhone(values): + return values.link + case let .socks(values): + return values.link + case let .requestSecureId(values): + return values.link + case let .unsupportedScheme(values): + return values.link + case let .applyLocalization(values): + return values.link + case let .wallpaper(values): + return values.link + case let .theme(values): + return values.link + case let .tonTransfer(link, _, _): + return link + case let .instantView(link, _, _): + return link + case let .settings(link, _, _): + return link + case let .joinGroupCall(link, _, _, _): + return link + case .nothing: + return "" + case .logout: + return "" + } + } } let telegram_me:[String] = ["telegram.me/","telegram.dog/","t.me/"] -let actions_me:[String] = ["joinchat/","addstickers/","confirmphone?","socks?"] +let actions_me:[String] = ["joinchat/","addstickers/","confirmphone","socks", "proxy", "setlanguage", "bg", "addtheme/"] let telegram_scheme:String = "tg://" -let known_scheme:[String] = ["resolve?domain=","msg_url?url=","join?invite=","addstickers?set=","confirmphone", "socks?"] +let known_scheme:[String] = ["resolve","msg_url","join","addstickers","confirmphone", "socks", "proxy", "passport", "setlanguage", "bg", "privatepost", "addtheme", "settings"] + +let ton_scheme:String = "ton://" private let keyURLUsername = "domain"; private let keyURLPostId = "post"; +private let keyURLCommentId = "comment"; +private let keyURLThreadId = "thread"; private let keyURLInvite = "invite"; private let keyURLUrl = "url"; private let keyURLSet = "set"; private let keyURLText = "text"; private let keyURLStart = "start"; +private let keyURLVoiceChat = "voicechat"; private let keyURLStartGroup = "startgroup"; +private let keyURLSecret = "secret"; +private let keyURLPhone = "phone"; +private let keyURLHash = "hash"; private let keyURLHost = "server"; private let keyURLPort = "port"; private let keyURLUser = "user"; private let keyURLPass = "pass"; -func inApp(for url:NSString, account:Account, peerId:PeerId? = nil, openInfo:((PeerId, Bool, MessageId?, ChatInitialAction?)->Void)? = nil, hashtag:((String)->Void)? = nil, command:((String)->Void)? = nil, applyProxy:((ProxySettings) -> Void)? = nil, confirm: Bool = false) -> inAppLink { +let legacyPassportUsername = "telegrampassport" + +func inApp(for url:NSString, context: AccountContext? = nil, peerId:PeerId? = nil, openInfo:((PeerId, Bool, MessageId?, ChatInitialAction?)->Void)? = nil, hashtag:((String)->Void)? = nil, command:((String)->Void)? = nil, applyProxy:((ProxyServerSettings) -> Void)? = nil, confirm: Bool = false) -> inAppLink { let external = url + let urlString = external as String let url = url.lowercased.nsstring + + + for domain in telegram_me { let range = url.range(of: domain) - if range.location != NSNotFound && (range.location == 0 || url.substring(from: range.location - 1).hasPrefix("/")) { + if range.location != NSNotFound && (range.location == 0 || (range.location <= 8 && url.substring(from: range.location - 1).hasPrefix("/"))) { let string = external.substring(from: range.location + range.length) for action in actions_me { if string.hasPrefix(action) { - let value = string.substring(from: string.index(string.startIndex, offsetBy: action.length)) + let value = String(string[string.index(string.startIndex, offsetBy: action.length) ..< string.endIndex]) switch action { case actions_me[0]: - if let openInfo = openInfo { - return .joinchat(value, account: account, callback: openInfo) + if let openInfo = openInfo, let context = context { + return .joinchat(link: urlString, value, context: context, callback: openInfo) } case actions_me[1]: - return .stickerPack(StickerPackReference.name(value), account: account, peerId: peerId) + if let context = context { + return .stickerPack(link: urlString, StickerPackReference.name(value), context: context, peerId: peerId) + } + case actions_me[2]: + let (vars, _) = urlVars(with: string) + if let context = context, let phone = vars[keyURLPhone], let hash = vars[keyURLHash] { + return .confirmPhone(link: urlString, context: context, phone: phone, hash: hash) + } case actions_me[3]: - let vars = urlVars(with: string) + let (vars, _) = urlVars(with: string) if let applyProxy = applyProxy, let server = vars[keyURLHost], let maybePort = vars[keyURLPort], let port = Int32(maybePort) { let server = escape(with: server) - return .socks(ProxySettings(host: server, port: port, username: vars[keyURLUser], password: vars[keyURLPass]), applyProxy: applyProxy) + let username = vars[keyURLUser] != nil ? escape(with: vars[keyURLUser]!) : nil + let pass = vars[keyURLPass] != nil ? escape(with: vars[keyURLPass]!) : nil + return .socks(link: urlString, ProxyServerSettings(host: server, port: port, connection: .socks5(username: username, password: pass)), applyProxy: applyProxy) + } + case actions_me[4]: + let (vars, _) = urlVars(with: string) + if let applyProxy = applyProxy, let server = vars[keyURLHost], let maybePort = vars[keyURLPort], let port = Int32(maybePort), let rawSecret = vars[keyURLSecret] { + let server = escape(with: server) + if let secret = MTProxySecret.parse(rawSecret)?.serialize() { + return .socks(link: urlString, ProxyServerSettings(host: server, port: port, connection: .mtp(secret: secret)), applyProxy: applyProxy) + } } + case actions_me[5]: + if let context = context, !value.isEmpty { + return .applyLocalization(link: urlString, context: context, value: String(value[value.index(after: value.startIndex) ..< value.endIndex])) + } else { + + } + case actions_me[6]: + if !value.isEmpty { + var component = String(value[value.index(after: value.startIndex) ..< value.endIndex]) + component = component.components(separatedBy: "?")[0] + + if let context = context { + let (vars, emptyVars) = urlVars(with: value) + var rotation:Int32? = vars["rotation"] != nil ? Int32(vars["rotation"]!) : nil + + if let r = rotation { + let available:[Int32] = [0, 45, 90, 135, 180, 225, 270, 310] + if !available.contains(r) { + rotation = nil + } + } + + + var blur: Bool = false + var intensity: Int32? = 80 + var colors: [UInt32] = [] + + if let bgcolor = vars["bg_color"], !bgcolor.isEmpty { + var components = bgcolor.components(separatedBy: "~") + if components.count == 1 { + components = bgcolor.components(separatedBy: "-") + if components.count > 2 { + components = [] + } + } + colors = components.compactMap { + return NSColor(hexString: "#\($0)")?.argb + } + } else { + var components = component.components(separatedBy: "~") + if components.count == 1 { + components = component.components(separatedBy: "-") + if components.count > 2 { + components = [] + } + } + colors = components.compactMap { + return NSColor(hexString: "#\($0)")?.argb + } + } + if let intensityString = vars["intensity"] { + intensity = Int32(intensityString) + } + if let mode = vars["mode"] { + blur = mode.contains("blur") + } + + let settings: WallpaperSettings = WallpaperSettings(blur: blur, motion: false, colors: colors, intensity: intensity, rotation: rotation) + + + var slug = component + if let index = component.range(of: "?") { + slug = String(component[component.startIndex ..< index.lowerBound]) + } + if (slug.contains("~") || slug.length < 27) { + slug = "" + } + + var preview: WallpaperPreview = .slug(slug, settings) + if !colors.isEmpty, slug == "" { + preview = .gradient(nil, colors.map { NSColor(argb: $0) }, settings) + } + + return .wallpaper(link: urlString, context: context, preview: preview) + + } + } + return .external(link: urlString, false) + case actions_me[7]: + let userAndPost = string.components(separatedBy: "/") + if userAndPost.count == 2, let context = context { + return .theme(link: urlString, context: context, name: userAndPost[1]) + } + return .external(link: urlString, false) default: break } @@ -278,47 +1067,111 @@ func inApp(for url:NSString, account:Account, peerId:PeerId? = nil, openInfo:((P let username:String = userAndVariables[0] var action:ChatInitialAction? = nil if userAndVariables.count == 2 { - let vars = urlVars(with: userAndVariables[1]) + let (vars, emptyVars) = urlVars(with: userAndVariables[1]) loop: for (key,value) in vars { switch key { case keyURLStart: action = .start(parameter: value, behavior: .none) break loop; case keyURLStartGroup: - if let openInfo = openInfo { - return .inviteBotToGroup(username: username, account: account, action: .start(parameter: value, behavior: .none), callback: openInfo) + if let openInfo = openInfo, let context = context { + return .inviteBotToGroup(link: urlString, username: username, context: context, action: .start(parameter: value, behavior: .automatic), callback: openInfo) } break loop; + case keyURLVoiceChat: + action = .joinVoiceChat(value) + break loop; default: break } } + if vars.isEmpty && userAndVariables[1] == keyURLVoiceChat { + action = .joinVoiceChat(nil) + } } - + if let openInfo = openInfo { - if username == "iv" { - return .external(link: url as String, false) - } else { - return .followResolvedName(username: username, postId: nil, account: account, action: action, callback: openInfo) + + + + if username == "iv" || username.isEmpty { + return .external(link: urlString, username.isEmpty) + } else if let context = context { + + let joinKeys:[String] = ["+", "%20"] + + for joinKey in joinKeys { + if username.hasPrefix(joinKey), username.length > joinKey.length { + return .joinchat(link: urlString, username.nsstring.substring(from: joinKey.length), context: context, callback: openInfo) + } + } + return .followResolvedName(link: urlString, username: username, postId: nil, context: context, action: action, callback: openInfo) } } } else if let openInfo = openInfo { let userAndPost = string.components(separatedBy: "/") if userAndPost.count >= 2 { let name = userAndPost[0] - let post = userAndPost[1].isEmpty ? nil : userAndPost[1].nsstring.intValue - if name.hasPrefix("iv?") { - return .external(link: url as String, false) + + if name == "c" { + if let context = context { + var post = userAndPost.count >= 3 ? (userAndPost[2].isEmpty ? nil : Int32(userAndPost[2])) : nil + if userAndPost.count >= 3, let range = userAndPost[2].range(of: "?") { + post = Int32(userAndPost[2][.. 2 { + components = [] + } + } + colors = components.compactMap { + return NSColor(hexString: "#\($0)")?.argb + } + } + if let mode = vars["mode"] { + blur = mode.contains("blur") + } + if let intensityString = vars["intensity"] { + intensity = Int32(intensityString) + } + + let settings: WallpaperSettings = WallpaperSettings(blur: blur, motion: false, colors: colors, intensity: intensity, rotation: rotation) + + return .wallpaper(link: urlString, context: context, preview: .slug(value, settings)) + } + if let context = context, let value = vars["color"] { + return .wallpaper(link: urlString, context: context, preview: .slug(value, WallpaperSettings())) + } else if let context = context { + + var rotation:Int32? = vars["rotation"] != nil ? Int32(vars["rotation"]!) : nil + + if let r = rotation { + let available:[Int32] = [0, 45, 90, 135, 180, 225, 270, 310] + if !available.contains(r) { + rotation = nil + } + } + + var components = vars["bg_color"]?.components(separatedBy: "~") ?? [] + if components.count == 1 { + components = vars["bg_color"]?.components(separatedBy: "-") ?? [] + if components.count > 2 { + components = [] + } + } + let colors = components.compactMap { + return NSColor(hexString: "#\($0)") + } + if !colors.isEmpty { + return .wallpaper(link: urlString, context: context, preview: .gradient(0, colors, WallpaperSettings(rotation: rotation))) + } + } + case known_scheme[10]: + if let username = vars["channel"], let openInfo = openInfo { + let post = vars[keyURLPostId]?.nsstring.intValue + let threadId = vars[keyURLThreadId]?.nsstring.intValue + if let threadId = threadId, let post = post, let context = context { + return .comments(link: urlString, username: "_private_\(username)", context: context, threadId: threadId, commentId: post) + } else if let context = context { + return .followResolvedName(link: urlString, username: "_private_\(username)", postId: post, context: context, action:nil, callback: openInfo) + } + } + case known_scheme[11]: + if let context = context, let value = vars["slug"] { + return .theme(link: urlString, context: context, name: value) + } + case known_scheme[12]: + if let context = context, let range = action.range(of: known_scheme[12] + "/") { + let section = String(action[range.upperBound...]) + if let section = InAppSettingsSection(rawValue: section) { + return .settings(link: urlString, context: context, section: section) + } } default: break } - + return .nothing } } + if let context = context { + var path = url.substring(from: telegram_scheme.length) + let qLocation = path.nsstring.range(of: "?").location + path = path.nsstring.substring(to: qLocation != NSNotFound ? qLocation : path.length) + return .unsupportedScheme(link: urlString, context: context, path: path) + } + } else if url.hasPrefix(ton_scheme), let context = context { + return .external(link: urlString, false) +// let action = url.substring(from: ton_scheme.length) +// if action.hasPrefix("transfer/") { +// let (vars, emptyVars) = urlVars(with: url as String) +// let preAddressLength = ton_scheme.length + "transfer/".length + walletAddressLength +// let address = urlString.prefix(preAddressLength) +// if address.length == preAddressLength { +// let address = String(address.suffix(walletAddressLength)) +// var amount: Int64? = nil +// var comment: String? = nil +// if let varAmount = vars["amount"], !varAmount.isEmpty, let intAmount = Int64(varAmount) { +// amount = intAmount +// } +// if let varComment = vars["text"], !varComment.isEmpty { +// comment = escape(with: varComment, addPercent: false) +// } +// return .tonTransfer(link: urlString, context: context, data: ParsedWalletUrl(address: address, amount: amount, comment: comment)) +// } +// } + return .nothing } - return .external(link: external as String, confirm) + return .external(link: urlString as String, confirm) +} + +func addUrlParameter(value: String, to url: String) -> String { + if let _ = url.range(of: "?") { + return url + "&" + value + } else { + if url.hasSuffix("/") { + return url + value + } else { + return url + "/" + value + } + } } func makeInAppLink(with action:String, params:[String:Any]) -> String { @@ -407,20 +1443,96 @@ func makeInAppLink(with action:String, params:[String:Any]) -> String { return link } -func proxySettings(from url:String) -> (ProxySettings?, Bool) { +func proxySettings(from url:String) -> (ProxyServerSettings?, Bool) { let url = url.nsstring - if url.hasPrefix(telegram_scheme) { + if url.hasPrefix(telegram_scheme), let _ = URL(string: url as String) { let action = url.substring(from: telegram_scheme.length) - let vars = urlVars(with: url as String) + let (vars, emptyVars) = urlVars(with: url as String) if action.hasPrefix("socks") { if let server = vars[keyURLHost], let maybePort = vars[keyURLPort], let port = Int32(maybePort) { let server = escape(with: server) - return (ProxySettings(host: server, port: port, username: vars[keyURLUser], password: vars[keyURLPass]), true) + return (ProxyServerSettings(host: server, port: port, connection: .socks5(username: vars[keyURLUser], password: vars[keyURLPass])), true) } return (nil , true) + } else if action.hasPrefix("proxy") { + if let server = vars[keyURLHost], let maybePort = vars[keyURLPort], let port = Int32(maybePort), let rawSecret = vars[keyURLSecret] { + let server = escape(with: server) + if let secret = MTProxySecret.parse(rawSecret)?.serialize() { + return (ProxyServerSettings(host: server, port: port, connection: .mtp(secret: secret)), true) + } + } } + } else if let _ = URL(string: url as String) { + let link = inApp(for: url, applyProxy: {_ in}) + switch link { + case let .socks(_, settings, _): + return (settings, true) + default: + break + } } return (nil, false) } + +public struct ParsedWalletUrl { + public let address: String + public let amount: Int64? + public let comment: String? +} + +// +//public func parseWalletUrl(_ url: URL) -> ParsedWalletUrl? { +// guard url.scheme == "ton" && url.host == "transfer" else { +// return nil +// } +// var address: String? +// let path = url.path.trimmingCharacters(in: CharacterSet(charactersIn: "/")) +// if isValidAddress(path) { +// address = path +// } +// var amount: Int64? +// var comment: String? +// if let query = url.query, let components = URLComponents(string: "/?" + query), let queryItems = components.queryItems { +// for queryItem in queryItems { +// if let value = queryItem.value { +// if queryItem.name == "amount", !value.isEmpty, let amountValue = Int64(value) { +// amount = amountValue +// } else if queryItem.name == "text", !value.isEmpty { +// comment = value +// } +// } +// } +// } +// return address.flatMap { ParsedWalletUrl(address: $0, amount: amount, comment: comment) } +//} + + + +func resolveInstantViewUrl(account: Account, url: String) -> Signal { + return webpagePreview(account: account, url: url) + |> mapToSignal { webpage -> Signal in + if let webpage = webpage { + + if case let .Loaded(content) = webpage.content { + if content.instantPage != nil { + var anchorValue: String? + if let anchorRange = url.range(of: "#") { + let anchor = url[anchorRange.upperBound...] + if !anchor.isEmpty { + anchorValue = String(anchor) + } + } + return .single(.instantView(link: url, webpage: webpage, anchor: anchorValue)) + } else { + return .single(.external(link: url, false)) + } + } else { + return .complete() + } + } else { + return .single(.external(link: url, false)) + } + } +} diff --git a/Telegram-Mac/InAppNotificationSettings.swift b/Telegram-Mac/InAppNotificationSettings.swift index 5585cb2f40..f0d30a39d7 100644 --- a/Telegram-Mac/InAppNotificationSettings.swift +++ b/Telegram-Mac/InAppNotificationSettings.swift @@ -6,63 +6,210 @@ // Copyright © 2017 Telegram. All rights reserved. // -import Cocoa -import PostboxMac -import SwiftSignalKitMac + +public enum TotalUnreadCountDisplayStyle: Int32 { + case filtered = 0 + case raw = 1 + + var category: ChatListTotalUnreadStateCategory { + switch self { + case .filtered: + return .filtered + case .raw: + return .raw + } + } +} + +public enum TotalUnreadCountDisplayCategory: Int32 { + case chats = 0 + case messages = 1 + + var statsType: ChatListTotalUnreadStateStats { + switch self { + case .chats: + return .chats + case .messages: + return .messages + } + } +} + +import Postbox +import SwiftSignalKit +import TelegramCore + + +private enum PeerMessageSoundValue: Int32 { + case none + case bundledModern + case bundledClassic + case `default` +} + +extension PeerMessageSound { + fileprivate static func decodeInline(_ decoder: PostboxDecoder) -> PeerMessageSound { + switch decoder.decodeInt32ForKey("s1.v", orElse: 1) { + case PeerMessageSoundValue.none.rawValue: + return .none + case PeerMessageSoundValue.bundledModern.rawValue: + return .bundledModern(id: decoder.decodeInt32ForKey("s1.i", orElse: 0)) + case PeerMessageSoundValue.bundledClassic.rawValue: + return .bundledClassic(id: decoder.decodeInt32ForKey("s1.i", orElse: 0)) + case PeerMessageSoundValue.default.rawValue: + return .default + default: + assertionFailure() + return .bundledModern(id: 0) + } + } + + fileprivate func encodeInline(_ encoder: PostboxEncoder) { + switch self { + case .none: + encoder.encodeInt32(PeerMessageSoundValue.none.rawValue, forKey: "s1.v") + case let .bundledModern(id): + encoder.encodeInt32(PeerMessageSoundValue.bundledModern.rawValue, forKey: "s1.v") + encoder.encodeInt32(id, forKey: "s1.i") + case let .bundledClassic(id): + encoder.encodeInt32(PeerMessageSoundValue.bundledClassic.rawValue, forKey: "s1.v") + encoder.encodeInt32(id, forKey: "s1.i") + case .default: + encoder.encodeInt32(PeerMessageSoundValue.default.rawValue, forKey: "s1.v") + } + } +} + + + struct InAppNotificationSettings: PreferencesEntry, Equatable { let enabled: Bool let playSounds: Bool - let tone: String + let tone: PeerMessageSound let displayPreviews: Bool let muteUntil: Int32 + let notifyAllAccounts: Bool + let totalUnreadCountDisplayStyle: TotalUnreadCountDisplayStyle + let totalUnreadCountDisplayCategory: TotalUnreadCountDisplayCategory + let totalUnreadCountIncludeTags: PeerSummaryCounterTags + let showNotificationsOutOfFocus: Bool + let badgeEnabled: Bool + let requestUserAttention: Bool static var defaultSettings: InAppNotificationSettings { - return InAppNotificationSettings(enabled: true, playSounds: true, tone: "Default", displayPreviews: true, muteUntil: 0) + return InAppNotificationSettings(enabled: true, playSounds: true, tone: .default, displayPreviews: true, muteUntil: 0, totalUnreadCountDisplayStyle: .filtered, totalUnreadCountDisplayCategory: .chats, totalUnreadCountIncludeTags: .all, notifyAllAccounts: true, showNotificationsOutOfFocus: true, badgeEnabled: true, requestUserAttention: false) } - init(enabled:Bool, playSounds: Bool, tone: String, displayPreviews: Bool, muteUntil: Int32) { + init(enabled:Bool, playSounds: Bool, tone: PeerMessageSound, displayPreviews: Bool, muteUntil: Int32, totalUnreadCountDisplayStyle: TotalUnreadCountDisplayStyle, totalUnreadCountDisplayCategory: TotalUnreadCountDisplayCategory, totalUnreadCountIncludeTags: PeerSummaryCounterTags, notifyAllAccounts: Bool, showNotificationsOutOfFocus: Bool, badgeEnabled: Bool, requestUserAttention: Bool) { self.enabled = enabled self.playSounds = playSounds self.tone = tone self.displayPreviews = displayPreviews self.muteUntil = muteUntil + self.notifyAllAccounts = notifyAllAccounts + self.totalUnreadCountDisplayStyle = totalUnreadCountDisplayStyle + self.totalUnreadCountDisplayCategory = totalUnreadCountDisplayCategory + self.totalUnreadCountIncludeTags = totalUnreadCountIncludeTags + self.showNotificationsOutOfFocus = showNotificationsOutOfFocus + self.badgeEnabled = badgeEnabled + self.requestUserAttention = requestUserAttention } init(decoder: PostboxDecoder) { self.enabled = decoder.decodeInt32ForKey("e", orElse: 0) != 0 self.playSounds = decoder.decodeInt32ForKey("s", orElse: 0) != 0 - self.tone = decoder.decodeStringForKey("t", orElse: "") + self.tone = PeerMessageSound.decodeInline(decoder) self.displayPreviews = decoder.decodeInt32ForKey("p", orElse: 0) != 0 self.muteUntil = decoder.decodeInt32ForKey("m2", orElse: 0) + self.notifyAllAccounts = decoder.decodeBoolForKey("naa", orElse: true) + self.totalUnreadCountDisplayStyle = TotalUnreadCountDisplayStyle(rawValue: decoder.decodeInt32ForKey("tds", orElse: 1)) ?? .filtered + self.totalUnreadCountDisplayCategory = TotalUnreadCountDisplayCategory(rawValue: decoder.decodeInt32ForKey("totalUnreadCountDisplayCategory", orElse: 1)) ?? .chats + if let value = decoder.decodeOptionalInt32ForKey("totalUnreadCountIncludeTags_2") { + self.totalUnreadCountIncludeTags = PeerSummaryCounterTags(rawValue: value) + } else if let value = decoder.decodeOptionalInt32ForKey("totalUnreadCountIncludeTags") { + var resultTags: PeerSummaryCounterTags = [] + for legacyTag in LegacyPeerSummaryCounterTags(rawValue: value) { + if legacyTag == .regularChatsAndPrivateGroups { + resultTags.insert(.contact) + resultTags.insert(.nonContact) + resultTags.insert(.bot) + resultTags.insert(.group) + } else if legacyTag == .publicGroups { + resultTags.insert(.group) + } else if legacyTag == .channels { + resultTags.insert(.channel) + } + } + self.totalUnreadCountIncludeTags = resultTags + } else { + self.totalUnreadCountIncludeTags = .all + } + + self.showNotificationsOutOfFocus = decoder.decodeInt32ForKey("snoof", orElse: 1) != 0 + self.badgeEnabled = decoder.decodeBoolForKey("badge", orElse: true) + self.requestUserAttention = decoder.decodeBoolForKey("requestUserAttention", orElse: false) } func encode(_ encoder: PostboxEncoder) { encoder.encodeInt32(self.enabled ? 1 : 0, forKey: "e") encoder.encodeInt32(self.playSounds ? 1 : 0, forKey: "s") - encoder.encodeString(self.tone, forKey: "t") + self.tone.encodeInline(encoder) encoder.encodeInt32(self.displayPreviews ? 1 : 0, forKey: "p") encoder.encodeInt32(self.muteUntil, forKey: "m2") + encoder.encodeBool(self.notifyAllAccounts, forKey: "naa") + encoder.encodeInt32(self.totalUnreadCountDisplayStyle.rawValue, forKey: "tds") + encoder.encodeInt32(self.totalUnreadCountDisplayCategory.rawValue, forKey: "totalUnreadCountDisplayCategory") + encoder.encodeInt32(self.totalUnreadCountIncludeTags.rawValue, forKey: "totalUnreadCountIncludeTags_2") + encoder.encodeInt32(self.showNotificationsOutOfFocus ? 1 : 0, forKey: "snoof") + encoder.encodeBool(self.badgeEnabled, forKey: "badge") + encoder.encodeBool(self.requestUserAttention, forKey: "requestUserAttention") } func withUpdatedEnables(_ enabled: Bool) -> InAppNotificationSettings { - return InAppNotificationSettings(enabled: enabled, playSounds: self.playSounds, tone: self.tone, displayPreviews: self.displayPreviews, muteUntil: self.muteUntil) + return InAppNotificationSettings(enabled: enabled, playSounds: self.playSounds, tone: self.tone, displayPreviews: self.displayPreviews, muteUntil: self.muteUntil, totalUnreadCountDisplayStyle: self.totalUnreadCountDisplayStyle, totalUnreadCountDisplayCategory: self.totalUnreadCountDisplayCategory, totalUnreadCountIncludeTags: self.totalUnreadCountIncludeTags, notifyAllAccounts: self.notifyAllAccounts, showNotificationsOutOfFocus: self.showNotificationsOutOfFocus, badgeEnabled: self.badgeEnabled, requestUserAttention: self.requestUserAttention) } func withUpdatedPlaySounds(_ playSounds: Bool) -> InAppNotificationSettings { - return InAppNotificationSettings(enabled: self.enabled, playSounds: playSounds, tone: self.tone, displayPreviews: self.displayPreviews, muteUntil: self.muteUntil) + return InAppNotificationSettings(enabled: self.enabled, playSounds: playSounds, tone: self.tone, displayPreviews: self.displayPreviews, muteUntil: self.muteUntil, totalUnreadCountDisplayStyle: self.totalUnreadCountDisplayStyle, totalUnreadCountDisplayCategory: self.totalUnreadCountDisplayCategory, totalUnreadCountIncludeTags: self.totalUnreadCountIncludeTags, notifyAllAccounts: self.notifyAllAccounts, showNotificationsOutOfFocus: self.showNotificationsOutOfFocus, badgeEnabled: self.badgeEnabled, requestUserAttention: self.requestUserAttention) } - func withUpdatedTone(_ tone: String) -> InAppNotificationSettings { - return InAppNotificationSettings(enabled: self.enabled, playSounds: self.playSounds, tone: tone, displayPreviews: self.displayPreviews, muteUntil: self.muteUntil) + func withUpdatedTone(_ tone: PeerMessageSound) -> InAppNotificationSettings { + return InAppNotificationSettings(enabled: self.enabled, playSounds: self.playSounds, tone: tone, displayPreviews: self.displayPreviews, muteUntil: self.muteUntil, totalUnreadCountDisplayStyle: self.totalUnreadCountDisplayStyle, totalUnreadCountDisplayCategory: self.totalUnreadCountDisplayCategory, totalUnreadCountIncludeTags: self.totalUnreadCountIncludeTags, notifyAllAccounts: self.notifyAllAccounts, showNotificationsOutOfFocus: self.showNotificationsOutOfFocus, badgeEnabled: self.badgeEnabled, requestUserAttention: self.requestUserAttention) } func withUpdatedDisplayPreviews(_ displayPreviews: Bool) -> InAppNotificationSettings { - return InAppNotificationSettings(enabled: self.enabled, playSounds: self.playSounds, tone: self.tone, displayPreviews: displayPreviews, muteUntil: self.muteUntil) + return InAppNotificationSettings(enabled: self.enabled, playSounds: self.playSounds, tone: self.tone, displayPreviews: displayPreviews, muteUntil: self.muteUntil, totalUnreadCountDisplayStyle: self.totalUnreadCountDisplayStyle, totalUnreadCountDisplayCategory: self.totalUnreadCountDisplayCategory, totalUnreadCountIncludeTags: self.totalUnreadCountIncludeTags, notifyAllAccounts: self.notifyAllAccounts, showNotificationsOutOfFocus: self.showNotificationsOutOfFocus, badgeEnabled: self.badgeEnabled, requestUserAttention: self.requestUserAttention) } func withUpdatedMuteUntil(_ muteUntil: Int32) -> InAppNotificationSettings { - return InAppNotificationSettings(enabled: self.enabled, playSounds: self.playSounds, tone: self.tone, displayPreviews: self.displayPreviews, muteUntil: muteUntil) + return InAppNotificationSettings(enabled: self.enabled, playSounds: self.playSounds, tone: self.tone, displayPreviews: self.displayPreviews, muteUntil: muteUntil, totalUnreadCountDisplayStyle: self.totalUnreadCountDisplayStyle, totalUnreadCountDisplayCategory: self.totalUnreadCountDisplayCategory, totalUnreadCountIncludeTags: self.totalUnreadCountIncludeTags, notifyAllAccounts: self.notifyAllAccounts, showNotificationsOutOfFocus: self.showNotificationsOutOfFocus, badgeEnabled: self.badgeEnabled, requestUserAttention: self.requestUserAttention) } + func withUpdatedTotalUnreadCountDisplayCategory(_ totalUnreadCountDisplayCategory: TotalUnreadCountDisplayCategory) -> InAppNotificationSettings { + return InAppNotificationSettings(enabled: self.enabled, playSounds: self.playSounds, tone: self.tone, displayPreviews: self.displayPreviews, muteUntil: muteUntil, totalUnreadCountDisplayStyle: self.totalUnreadCountDisplayStyle, totalUnreadCountDisplayCategory: totalUnreadCountDisplayCategory, totalUnreadCountIncludeTags: self.totalUnreadCountIncludeTags, notifyAllAccounts: self.notifyAllAccounts, showNotificationsOutOfFocus: self.showNotificationsOutOfFocus, badgeEnabled: self.badgeEnabled, requestUserAttention: self.requestUserAttention) + } + + func withUpdatedTotalUnreadCountDisplayStyle(_ totalUnreadCountDisplayStyle: TotalUnreadCountDisplayStyle) -> InAppNotificationSettings { + return InAppNotificationSettings(enabled: self.enabled, playSounds: self.playSounds, tone: self.tone, displayPreviews: self.displayPreviews, muteUntil: muteUntil, totalUnreadCountDisplayStyle: totalUnreadCountDisplayStyle, totalUnreadCountDisplayCategory: self.totalUnreadCountDisplayCategory, totalUnreadCountIncludeTags: self.totalUnreadCountIncludeTags, notifyAllAccounts: self.notifyAllAccounts, showNotificationsOutOfFocus: self.showNotificationsOutOfFocus, badgeEnabled: self.badgeEnabled, requestUserAttention: self.requestUserAttention) + } + + func withUpdatedNotifyAllAccounts(_ notifyAllAccounts: Bool) -> InAppNotificationSettings { + return InAppNotificationSettings(enabled: self.enabled, playSounds: self.playSounds, tone: self.tone, displayPreviews: self.displayPreviews, muteUntil: muteUntil, totalUnreadCountDisplayStyle: self.totalUnreadCountDisplayStyle, totalUnreadCountDisplayCategory: self.totalUnreadCountDisplayCategory, totalUnreadCountIncludeTags: self.totalUnreadCountIncludeTags, notifyAllAccounts: notifyAllAccounts, showNotificationsOutOfFocus: self.showNotificationsOutOfFocus, badgeEnabled: self.badgeEnabled, requestUserAttention: self.requestUserAttention) + } + + func withUpdatedTotalUnreadCountIncludeTags(_ totalUnreadCountIncludeTags: PeerSummaryCounterTags) -> InAppNotificationSettings { + return InAppNotificationSettings(enabled: self.enabled, playSounds: self.playSounds, tone: self.tone, displayPreviews: self.displayPreviews, muteUntil: muteUntil, totalUnreadCountDisplayStyle: totalUnreadCountDisplayStyle, totalUnreadCountDisplayCategory: self.totalUnreadCountDisplayCategory, totalUnreadCountIncludeTags: totalUnreadCountIncludeTags, notifyAllAccounts: self.notifyAllAccounts, showNotificationsOutOfFocus: self.showNotificationsOutOfFocus, badgeEnabled: self.badgeEnabled, requestUserAttention: self.requestUserAttention) + } + + func withUpdatedSnoof(_ showNotificationsOutOfFocus: Bool) -> InAppNotificationSettings { + return InAppNotificationSettings(enabled: self.enabled, playSounds: self.playSounds, tone: self.tone, displayPreviews: self.displayPreviews, muteUntil: muteUntil, totalUnreadCountDisplayStyle: self.totalUnreadCountDisplayStyle, totalUnreadCountDisplayCategory: self.totalUnreadCountDisplayCategory, totalUnreadCountIncludeTags: totalUnreadCountIncludeTags, notifyAllAccounts: self.notifyAllAccounts, showNotificationsOutOfFocus: showNotificationsOutOfFocus, badgeEnabled: self.badgeEnabled, requestUserAttention: self.requestUserAttention) + } + + func withUpdatedBadgeEnabled(_ badgeEnabled: Bool) -> InAppNotificationSettings { + return InAppNotificationSettings(enabled: self.enabled, playSounds: self.playSounds, tone: self.tone, displayPreviews: self.displayPreviews, muteUntil: muteUntil, totalUnreadCountDisplayStyle: self.totalUnreadCountDisplayStyle, totalUnreadCountDisplayCategory: self.totalUnreadCountDisplayCategory, totalUnreadCountIncludeTags: totalUnreadCountIncludeTags, notifyAllAccounts: self.notifyAllAccounts, showNotificationsOutOfFocus: self.showNotificationsOutOfFocus, badgeEnabled: badgeEnabled, requestUserAttention: self.requestUserAttention) + } + func withUpdatedRequestUserAttention(_ requestUserAttention: Bool) -> InAppNotificationSettings { + return InAppNotificationSettings(enabled: self.enabled, playSounds: self.playSounds, tone: self.tone, displayPreviews: self.displayPreviews, muteUntil: self.muteUntil, totalUnreadCountDisplayStyle: self.totalUnreadCountDisplayStyle, totalUnreadCountDisplayCategory: self.totalUnreadCountDisplayCategory, totalUnreadCountIncludeTags: totalUnreadCountIncludeTags, notifyAllAccounts: self.notifyAllAccounts, showNotificationsOutOfFocus: self.showNotificationsOutOfFocus, badgeEnabled: self.badgeEnabled, requestUserAttention: requestUserAttention) + } func isEqual(to: PreferencesEntry) -> Bool { if let to = to as? InAppNotificationSettings { return self == to @@ -70,30 +217,12 @@ struct InAppNotificationSettings: PreferencesEntry, Equatable { return false } } - - static func ==(lhs: InAppNotificationSettings, rhs: InAppNotificationSettings) -> Bool { - if lhs.enabled != rhs.enabled { - return false - } - if lhs.playSounds != rhs.playSounds { - return false - } - if lhs.tone != rhs.tone { - return false - } - if lhs.displayPreviews != rhs.displayPreviews { - return false - } - if lhs.muteUntil != rhs.muteUntil { - return false - } - return true - } } -func updateInAppNotificationSettingsInteractively(postbox: Postbox, _ f: @escaping (InAppNotificationSettings) -> InAppNotificationSettings) -> Signal { - return postbox.modify { modifier -> Void in - modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.inAppNotificationSettings, { entry in +func updateInAppNotificationSettingsInteractively(accountManager: AccountManager, _ f: @escaping (InAppNotificationSettings) -> InAppNotificationSettings) -> Signal { + + return accountManager.transaction { transaction in + transaction.updateSharedData(ApplicationSharedPreferencesKeys.inAppNotificationSettings, { entry in let currentSettings: InAppNotificationSettings if let entry = entry as? InAppNotificationSettings { currentSettings = entry @@ -105,8 +234,19 @@ func updateInAppNotificationSettingsInteractively(postbox: Postbox, _ f: @escapi } } -func appNotificationSettings(postbox: Postbox) -> Signal { - return postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.inAppNotificationSettings]) |> map { preferences in - return (preferences.values[ApplicationSpecificPreferencesKeys.inAppNotificationSettings] as? InAppNotificationSettings) ?? InAppNotificationSettings.defaultSettings +func appNotificationSettings(accountManager: AccountManager) -> Signal { + return accountManager.sharedData(keys: [ApplicationSharedPreferencesKeys.inAppNotificationSettings]) |> map { view in + return view.entries[ApplicationSharedPreferencesKeys.inAppNotificationSettings] as? InAppNotificationSettings ?? InAppNotificationSettings.defaultSettings + } +} +func globalNotificationSettings(postbox: Postbox) -> Signal { + return postbox.preferencesView(keys: [PreferencesKeys.globalNotifications]) |> map { view in + let viewSettings: GlobalNotificationSettingsSet + if let settings = view.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings { + viewSettings = settings.effective + } else { + viewSettings = GlobalNotificationSettingsSet.defaultSettings + } + return viewSettings } } diff --git a/Telegram-Mac/InactiveChannelsController.swift b/Telegram-Mac/InactiveChannelsController.swift new file mode 100644 index 0000000000..666b6b2877 --- /dev/null +++ b/Telegram-Mac/InactiveChannelsController.swift @@ -0,0 +1,238 @@ +// +// InactiveChannelsController.swift +// Telegram +// +// Created by Mikhail Filimonov on 13/12/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit +import Postbox +import TelegramCore + + +private func localizedInactiveDate(_ timestamp: Int32) -> String { + + let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + + var t: time_t = time_t(TimeInterval(timestamp)) + var timeinfo: tm = tm() + localtime_r(&t, &timeinfo) + + var now: time_t = time_t(nowTimestamp) + var timeinfoNow: tm = tm() + localtime_r(&now, &timeinfoNow) + + let string: String + + if timeinfoNow.tm_year == timeinfo.tm_year && timeinfoNow.tm_mon == timeinfo.tm_mon { + //weeks + let dif = Int(roundf(Float(timeinfoNow.tm_mday - timeinfo.tm_mday) / 7)) + string = L10n.inactiveChannelsInactiveWeekCountable(dif) + + } else if timeinfoNow.tm_year == timeinfo.tm_year { + //month + let dif = Int(timeinfoNow.tm_mon - timeinfo.tm_mon) + string = L10n.inactiveChannelsInactiveMonthCountable(dif) + } else { + //year + var dif = Int(timeinfoNow.tm_year - timeinfo.tm_year) + + if Int(timeinfoNow.tm_mon - timeinfo.tm_mon) > 6 { + dif += 1 + } + string = L10n.inactiveChannelsInactiveYearCountable(dif) + } + return string +} + +private final class InactiveChannelsArguments { + let context: AccountContext + let select: SelectPeerInteraction + init(context: AccountContext, select: SelectPeerInteraction) { + self.context = context + self.select = select + } +} + +private struct InactiveChannelsState : Equatable { + let channels:[InactiveChannel]? + init(channels: [InactiveChannel]?) { + self.channels = channels + } + func withUpdatedChannels(_ channels: [InactiveChannel]) -> InactiveChannelsState { + return InactiveChannelsState(channels: channels) + } +} + + +private func inactiveEntries(state: InactiveChannelsState, arguments: InactiveChannelsArguments, source: InactiveSource) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId: Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("_id_text"), equatable: nil, comparable: nil, item: { initialSize, stableId in + return GeneralBlockTextRowItem(initialSize, stableId: stableId, viewType: .singleItem, text: source.localizedString, font: .normal(.text), header: GeneralBlockTextHeader(text: source.header, icon: theme.icons.sentFailed)) + })) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + +// + if let channels = state.channels { + if !channels.isEmpty { + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.inactiveChannelsHeader), data: .init(color: theme.colors.grayText, viewType: .textTopItem))) + index += 1 + } + for channel in channels { + let viewType = bestGeneralViewType(channels, for: channel) + struct _Equatable : Equatable { + let channel: InactiveChannel + let viewType: GeneralViewType + } + let equatable = _Equatable(channel: channel, viewType: viewType) + + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("_id_peer_\(channel.peer.id.toInt64())"), equatable: InputDataEquatable(equatable), comparable: nil, item: { initialSize, stableId in + return ShortPeerRowItem(initialSize, peer: channel.peer, account: arguments.context.account, stableId: stableId, enabled: true, height: 50, photoSize: NSMakeSize(36, 36), status: localizedInactiveDate(channel.lastActivityDate), inset: NSEdgeInsets(left: 30, right: 30), interactionType: .selectable(arguments.select), viewType: viewType) + })) + index += 1 + } + if !channels.isEmpty { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + } + + } else { + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.inactiveChannelsHeader), data: .init(color: theme.colors.grayText, viewType: .textTopItem))) + index += 1 + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("_id_loading"), equatable: nil, comparable: nil, item: { initialSize, stableId in + return LoadingTableItem(initialSize, height: 42, stableId: stableId, viewType: .singleItem) + })) + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + } + + return entries +} + +func InactiveChannelsController(context: AccountContext, source: InactiveSource) -> InputDataModalController { + let initialState = InactiveChannelsState(channels: nil) + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((InactiveChannelsState) -> InactiveChannelsState) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + + let disposable = MetaDisposable() + + disposable.set((context.engine.peers.inactiveChannelList() |> delay(0.5, queue: .mainQueue())).start(next: { channels in + updateState { + $0.withUpdatedChannels(channels) + } + })) + + let arguments = InactiveChannelsArguments(context: context, select: SelectPeerInteraction()) + + let signal = statePromise.get() |> map { state in + return InputDataSignalValue(entries: inactiveEntries(state: state, arguments: arguments, source: source)) + } + + let controller = InputDataController(dataSignal: signal, title: L10n.inactiveChannelsTitle) + + var close: (()->Void)? = nil + + let modalInteractions = ModalInteractions(acceptTitle: L10n.inactiveChannelsOK, accept: { + close?() + + if !arguments.select.presentation.selected.isEmpty { + let removeSignal = combineLatest(arguments.select.presentation.selected.map { context.engine.peers.removePeerChat(peerId: $0, reportChatSpam: false)}) + let peers = arguments.select.presentation.peers.map { $0.value } + let signal = context.account.postbox.transaction { transaction in + updatePeers(transaction: transaction, peers: peers, update: { _, updated in + return updated + }) + } |> mapToSignal { _ in + return removeSignal + } + + _ = showModalProgress(signal: signal, for: context.window).start() + } + + }, drawBorder: true, height: 50, singleButton: true) + + + arguments.select.singleUpdater = { [weak modalInteractions] presentation in + modalInteractions?.updateDone { button in + button.isEnabled = !presentation.selected.isEmpty + } + } + + controller.afterTransaction = { [weak modalInteractions] _ in + modalInteractions?.updateDone { button in + let state = stateValue.with { $0 } + if let channels = state.channels { + button.isEnabled = channels.isEmpty || !arguments.select.presentation.selected.isEmpty + button.set(text: channels.isEmpty ? L10n.modalOK : L10n.inactiveChannelsOK, for: .Normal) + } else { + button.isEnabled = false + } + } + } + + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { + close?() + }) + + controller.updateDatas = { data in + return .none + } + controller.onDeinit = { + disposable.dispose() + } + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions, closeHandler: { f in f() }, size: NSMakeSize(400, 300)) + + close = { [weak modalController] in + modalController?.close() + } + + return modalController + +} + +enum InactiveSource { + case join + case create + case upgrade + case invite + var localizedString: String { + switch self { + case .join: + return L10n.joinChannelsTooMuch + case .create: + return L10n.createChannelsTooMuch + case .upgrade: + return L10n.upgradeChannelsTooMuch + case .invite: + return L10n.inviteChannelsTooMuch + } + } + var header: String { + return L10n.inactiveChannelsBlockHeader + } +} + +func showInactiveChannels(context: AccountContext, source: InactiveSource) { + showModal(with: InactiveChannelsController(context: context, source: source), for: context.window) +} diff --git a/Telegram-Mac/Info.plist b/Telegram-Mac/Info.plist index e32892c1f2..547065a986 100644 --- a/Telegram-Mac/Info.plist +++ b/Telegram-Mac/Info.plist @@ -2,6 +2,8 @@ + APPCENTER_SECRET + ${APPCENTER_SECRET} ATSApplicationFontsPath fonts CFBundleDevelopmentRegion @@ -19,7 +21,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 3.5.2 + $(MARKETING_VERSION) CFBundleURLTypes @@ -29,13 +31,16 @@ tg telegram + ton CFBundleVersion - 107977 + 221856 LSApplicationCategoryType public.app-category.social-networking + LSFileQuarantineEnabled + LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSAppTransportSecurity @@ -43,15 +48,27 @@ NSAllowsArbitraryLoads + NSCameraUsageDescription + We need access to your camera so that you can record video messages. NSHumanReadableCopyright Copyright © 2016 Telegram. All rights reserved. + NSLocationAlwaysUsageDescription + We need access to your location so that you can send your current locations and setup Auto-Night theme + NSLocationWhenInUseUsageDescription + We need access to your location so that you can send your current locations. NSMainNibFile MainMenu + NSMicrophoneUsageDescription + We need access to your microphone so that you can record voice messages and make calls. NSPrincipalClass NSApplication SUFeedURL ${SFEED_URL} SUPublicDSAKeyFile ${DSA_PEM_FILE} + UIAppFonts + + SFCompactRounded-Semibold.otf + diff --git a/Telegram-Mac/InlineAudioPlayerView.swift b/Telegram-Mac/InlineAudioPlayerView.swift index 6e066f7f53..84ee76833b 100644 --- a/Telegram-Mac/InlineAudioPlayerView.swift +++ b/Telegram-Mac/InlineAudioPlayerView.swift @@ -8,35 +8,70 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac +import TelegramCore +import Postbox +import SwiftSignalKit class InlineAudioPlayerView: NavigationHeaderView, APDelegate { - let previous:ImageButton = ImageButton() - let next:ImageButton = ImageButton() - let playOrPause:ImageButton = ImageButton() - let dismiss:ImageButton = ImageButton() - let repeatControl:ImageButton = ImageButton() - let progressView:LinearProgressControl = LinearProgressControl(progressHeight: .borderSize) - let textView:TextView = TextView() - let containerView:View = View() - let separator:View = View() - private var controller:APController? + + struct ContextObject { + let controller: APController + let context: AccountContext + let tableView: TableView? + let supportTableView: TableView? + } + + var contextValue: ContextObject? { + return header?.contextObject as? ContextObject + } + var controller: APController? { + return contextValue?.controller + } + var context: AccountContext? { + return contextValue?.context + } + + private let previous:ImageButton = ImageButton() + private let next:ImageButton = ImageButton() + + private let playPause = Button() + private let playPauseView = LottiePlayerView() + + private let dismiss:ImageButton = ImageButton() + private let repeatControl:ImageButton = ImageButton() + private let volumeControl: ImageButton = ImageButton() + private let progressView:LinearProgressControl = LinearProgressControl(progressHeight: 2) + private var artistNameView:TextView? + private let trackNameView:TextView = TextView() + private let textViewContainer = Control() + private let containerView:Control + private let separator:View = View() + private let playingSpeed: ImageButton = ImageButton() + + + private var message:Message? private(set) var instantVideoPip:InstantVideoPIP? + private var ranges: (IndexSet, Int)? + + private var bufferingStatusDisposable: MetaDisposable = MetaDisposable() + + override init(_ header: NavigationHeader) { separator.backgroundColor = .border + dismiss.disableActions() + repeatControl.disableActions() - textView.isSelectable = false - - + trackNameView.isSelectable = false + trackNameView.userInteractionEnabled = false + containerView = Control(frame: NSMakeRect(0, 0, 0, header.height)) super.init(header) @@ -52,200 +87,493 @@ class InlineAudioPlayerView: NavigationHeaderView, APDelegate { self?.controller?.next() }, for: .Click) - playOrPause.set(handler: { [weak self] _ in + playPause.set(handler: { [weak self] _ in self?.controller?.playOrPause() }, for: .Click) - repeatControl.set(handler: { [weak self] control in - let control = control as! ImageButton + repeatControl.set(handler: { [weak self] _ in if let controller = self?.controller { - controller.toggleRepeat() - control.set(image: controller.needRepeat ? theme.icons.audioPlayerRepeatActive : theme.icons.audioPlayerRepeat, for: .Normal) + controller.nextRepeatState() } - }, for: .Click) + progressView.onUserChanged = { [weak self] progress in self?.controller?.set(trackProgress: progress) + self?.progressView.set(progress: CGFloat(progress), animated: false) + } + + var paused: Bool = false + + progressView.startScrobbling = { [weak self] in + guard let controller = self?.controller else { + return + } + if controller.isPlaying { + _ = self?.controller?.pause() + paused = true + } else { + paused = false + } + } + + progressView.endScrobbling = { [weak self] in + if paused { + DispatchQueue.main.async { + _ = self?.controller?.play() + } + } } progressView.set(handler: { [weak self] control in let control = control as! LinearProgressControl if let strongSelf = self { strongSelf.controller?.set(trackProgress: control.interactiveValue) + strongSelf.progressView.set(progress: CGFloat(control.interactiveValue), animated: false) } }, for: .Click) + previous.autohighlight = false + next.autohighlight = false + playPause.autohighlight = false + repeatControl.autohighlight = false + volumeControl.autohighlight = false + playingSpeed.autohighlight = false + + previous.scaleOnClick = true + next.scaleOnClick = true + playPause.scaleOnClick = true + repeatControl.scaleOnClick = true + volumeControl.scaleOnClick = true + playingSpeed.scaleOnClick = true + + + containerView.addSubview(previous) containerView.addSubview(next) - containerView.addSubview(playOrPause) + + playPause.addSubview(playPauseView) + playPause.setFrameSize(NSMakeSize(34, 34)) + playPauseView.setFrameSize(playPause.frame.size) + containerView.addSubview(playPause) + containerView.addSubview(dismiss) containerView.addSubview(repeatControl) - containerView.addSubview(textView) + containerView.addSubview(textViewContainer) + containerView.addSubview(playingSpeed) + containerView.addSubview(volumeControl) addSubview(containerView) addSubview(separator) addSubview(progressView) - textView.userInteractionEnabled = false + textViewContainer.addSubview(trackNameView) + + trackNameView.userInteractionEnabled = false + trackNameView.isEventLess = true + + + textViewContainer.set(handler: { [weak self] _ in + self?.showAudioPlayerList() + }, for: .LongOver) + + textViewContainer.set(handler: { [weak self] _ in + self?.gotoMessage() + }, for: .SingleClick) + + playingSpeed.set(handler: { [weak self] control in + FastSettings.setPlayingRate(FastSettings.playingRate == 1.7 ? 1.0 : 1.7) + self?.controller?.baseRate = FastSettings.playingRate + }, for: .Click) - updateLocalizationAndTheme() + + volumeControl.set(handler: { [weak self] control in + if control.popover == nil { + showPopover(for: control, with: VolumeControllerPopover(initialValue: CGFloat(FastSettings.volumeRate), updatedValue: { updatedVolume in + FastSettings.setVolumeRate(Float(updatedVolume)) + self?.controller?.volume = FastSettings.volumeRate + }), edge: .maxY, inset: NSMakePoint(-5, -50)) + } + }, for: .Hover) + + volumeControl.set(handler: { control in + FastSettings.setVolumeRate(FastSettings.volumeRate > 0 ? 0 : 1.0) + if let popover = control.popover?.controller as? VolumeControllerPopover { + popover.value = CGFloat(FastSettings.volumeRate) + } + }, for: .Up) + + updateLocalizationAndTheme(theme: theme) + } - private var playProgressStyle:ControlStyle { - return ControlStyle(foregroundColor: theme.colors.blueUI, backgroundColor: .clear) - } - private var fetchProgressStyle:ControlStyle { - return ControlStyle(foregroundColor: theme.colors.grayTransparent, backgroundColor: .clear) + private func showAudioPlayerList() { + guard let window = kitWindow, let context = self.context else {return} + let point = containerView.convert(window.mouseLocationOutsideOfEventStream, from: nil) + if NSPointInRect(point, textViewContainer.frame) { + if let controller = controller as? APChatMusicController, let song = controller.currentSong { + switch song.stableId { + case let .message(message): + showPopover(for: textViewContainer, with: PlayerListController(audioPlayer: self, context: controller.context, currentContext: context, messageIndex: MessageIndex(message), messages: controller.messages), edge: .minX, inset: NSMakePoint(-130, -60)) + default: + break + } + } + } } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() + func updateStatus(_ ranges: IndexSet, _ size: Int) { + self.ranges = (ranges, size) - previous.set(image: theme.icons.audioPlayerPrev, for: .Normal) - next.set(image: theme.icons.audioPlayerNext, for: .Normal) - playOrPause.set(image: theme.icons.audioPlayerPause, for: .Normal) - dismiss.set(image: theme.icons.auduiPlayerDismiss, for: .Normal) - if let controller = controller { - repeatControl.set(image: controller.needRepeat ? theme.icons.audioPlayerRepeatActive : theme.icons.audioPlayerRepeat, for: .Normal) - if let song = controller.currentSong { - songDidChanged(song: song, for: controller) - songDidChangedState(song: song, for: controller) + if let ranges = self.ranges, !ranges.0.isEmpty, ranges.1 != 0 { + for range in ranges.0.rangeView { + var progress = (CGFloat(range.count) / CGFloat(ranges.1)) + progress = progress == 1.0 ? 0 : progress + progressView.set(fetchingProgress: progress, animated: progress > 0) + + break } - } else { - repeatControl.set(image: theme.icons.audioPlayerRepeat, for: .Normal) } + } + + + private var playProgressStyle:ControlStyle { + return ControlStyle(foregroundColor: theme.colors.accent, backgroundColor: .clear, highlightColor: .clear) + } + private var fetchProgressStyle:ControlStyle { + return ControlStyle(foregroundColor: theme.colors.grayTransparent, backgroundColor: .clear, highlightColor: .clear) + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) - previous.sizeToFit() - next.sizeToFit() - playOrPause.sizeToFit() - dismiss.sizeToFit() - repeatControl.sizeToFit() + progressView.fetchingColor = theme.colors.accent.withAlphaComponent(0.5) + backgroundColor = theme.colors.background containerView.backgroundColor = theme.colors.background - textView.backgroundColor = theme.colors.background + artistNameView?.backgroundColor = theme.colors.background separator.backgroundColor = theme.colors.border + + controller?.notifyGlobalStateChanged(animated: false) + } - override func mouseUp(with event: NSEvent) { - super.mouseUp(with: event) - if let message = message, let controller = controller, let navigation = controller.account.context.mainNavigation { - if let controller = navigation.controller as? ChatController, controller.chatInteraction.peerId == message.id.peerId { - controller.chatInteraction.focusMessageId(nil, message.id, .center(id: 0, animated: true, focus: false, inset: 0)) + private func gotoMessage() { + if let message = message, let context = context, context.peerId == controller?.context.peerId { + if let controller = context.sharedContext.bindings.rootNavigation().controller as? ChatController, controller.chatInteraction.peerId == message.id.peerId { + controller.chatInteraction.focusMessageId(nil, message.id, .center(id: 0, innerId: nil, animated: true, focus: .init(focus: false), inset: 0)) } else { - navigation.push(ChatController(account: controller.account, peerId: message.id.peerId, messageId: message.id)) + context.sharedContext.bindings.rootNavigation().push(ChatController(context: context, chatLocation: .peer(message.id.peerId), messageId: message.id)) } } } - - func update(with controller:APController, tableView:TableView) { - self.controller?.remove(listener: self) - self.controller = controller - self.controller?.add(listener: self) + + override func update(with contextObject: Any) { + super.update(with: contextObject) + + let contextObject = contextObject as! ContextObject + + let controller = contextObject.controller + + self.bufferingStatusDisposable.set((controller.bufferingStatus + |> deliverOnMainQueue).start(next: { [weak self] status in + if let status = status { + self?.updateStatus(status.0, status.1) + } + })) + controller.baseRate = (controller is APChatVoiceController) ? FastSettings.playingRate : 1.0 + + self.playingSpeed.isHidden = !(controller is APChatVoiceController) + + controller.add(listener: self) self.ready.set(controller.ready.get()) - - repeatControl.isHidden = !(controller is APChatMusicController) - self.instantVideoPip = InstantVideoPIP(controller, window: mainWindow) - self.instantVideoPip?.updateTableView(tableView) + + repeatControl.isHidden = !controller.canMakeRepeat + if let tableView = contextObject.tableView { + if self.instantVideoPip == nil { + self.instantVideoPip = InstantVideoPIP(controller, context: controller.context, window: mainWindow) + } + self.instantVideoPip?.updateTableView(tableView, context: controller.context, controller: controller) + addGlobalAudioToVisible(tableView: tableView) + } + if let supportTableView = contextObject.supportTableView { + addGlobalAudioToVisible(tableView: supportTableView) + } + if let song = controller.currentSong { + songDidChanged(song: song, for: controller, animated: true) + } + } + + private func addGlobalAudioToVisible(tableView: TableView) { + if let controller = controller { + tableView.enumerateViews(with: { (view) in + var contentView: NSView? = (view as? ChatRowView)?.contentView.subviews.last ?? (view as? PeerMediaMusicRowView) + if let view = ((view as? ChatMessageView)?.webpageContent as? WPMediaContentView)?.contentNode { + contentView = view + } + + if let view = view as? ChatGroupedView { + for content in view.contents { + controller.add(listener: content) + } + } else if let view = contentView as? ChatAudioContentView { + controller.add(listener: view) + } else if let view = contentView as? ChatVideoMessageContentView { + controller.add(listener: view) + } else if let view = contentView as? WPMediaContentView { + if let contentNode = view.contentNode as? ChatAudioContentView { + controller.add(listener: contentNode) + } + } else if let view = view as? PeerMediaMusicRowView { + controller.add(listener: view) + } else if let view = view as? PeerMediaVoiceRowView { + controller.add(listener: view) + } + return true + }) + controller.notifyGlobalStateChanged(animated: false) + } } deinit { - controller?.remove(listener: self) - controller?.stop() + bufferingStatusDisposable.dispose() } - func attributedTitle(for song:APSongItem) -> NSAttributedString { - let attributed:NSMutableAttributedString = NSMutableAttributedString() - if !song.performerName.isEmpty { - _ = attributed.append(string: song.performerName, color: theme.colors.text, font: .normal(.text)) - _ = attributed.append(string: "\n") + func attributedTitle(for song:APSongItem) -> (NSAttributedString, NSAttributedString?) { + let trackName:NSAttributedString + let artistName:NSAttributedString? + + if song.songName.isEmpty { + trackName = .initialize(string: song.performerName, color: theme.colors.text, font: .normal(.text)) + artistName = nil + } else { + trackName = .initialize(string: song.songName, color: theme.colors.text, font: .normal(.text)) + if !song.performerName.isEmpty { + artistName = .initialize(string: song.performerName, color: theme.colors.grayText, font: .normal(.text)) + } else { + artistName = nil + } } - _ = attributed.append(string: song.songName, color: theme.colors.grayText, font: .normal(.text)) - return attributed + return (trackName, artistName) } - func songDidChanged(song:APSongItem, for controller:APController) { - next.set(image: controller.nextEnabled ? theme.icons.audioPlayerNext : theme.icons.audioPlayerLockedNext, for: .Normal) - previous.set(image: controller.prevEnabled ? theme.icons.audioPlayerPrev : theme.icons.audioPlayerLockedPrev, for: .Normal) - let layout = TextViewLayout(attributedTitle(for: song), maximumNumberOfLines:2, alignment: .center) - self.textView.update(layout) - self.needsLayout = true + private func update(_ song: APSongItem, controller: APController, animated: Bool) { + + dismiss.set(image: theme.icons.audioplayer_dismiss, for: .Normal) + switch song.entry { case let .song(message): self.message = message default: - break + self.message = nil } - } - - func songDidChangedState(song: APSongItem, for controller: APController) { + + next.userInteractionEnabled = controller.nextEnabled + previous.userInteractionEnabled = controller.prevEnabled + + switch controller.nextEnabled { + case true: + next.set(image: theme.icons.audioplayer_next, for: .Normal) + case false: + next.set(image: theme.icons.audioplayer_locked_next, for: .Normal) + } + + switch controller.prevEnabled { + case true: + previous.set(image: theme.icons.audioplayer_prev, for: .Normal) + case false: + previous.set(image: theme.icons.audioplayer_locked_prev, for: .Normal) + } + + let attr = attributedTitle(for: song) + + if trackNameView.layout?.attributedString != attr.0 { + let artist = TextViewLayout(attr.0, maximumNumberOfLines:1, alignment: .left) + self.trackNameView.update(artist) + } + if let attr = attr.1 { + let current: TextView + if self.artistNameView == nil { + current = TextView() + current.userInteractionEnabled = false + current.isEventLess = true + self.artistNameView = current + textViewContainer.addSubview(current) + } else { + current = self.artistNameView! + } + if current.layout?.attributedString != attr { + let artist = TextViewLayout(attr, maximumNumberOfLines:1, alignment: .left) + current.update(artist) + } + + } else { + if let view = self.artistNameView { + self.artistNameView = nil + if animated { + view.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } else { + view.removeFromSuperview() + } + } + } + + switch FastSettings.playingRate { + case 1.0: + playingSpeed.set(image: theme.icons.audioplayer_speed_x1, for: .Normal) + default: + playingSpeed.set(image: theme.icons.audioplayer_speed_x2, for: .Normal) + } + + switch FastSettings.volumeRate { + case 0: + volumeControl.set(image: theme.icons.audioplayer_volume_off, for: .Normal) + default: + volumeControl.set(image: theme.icons.audioplayer_volume, for: .Normal) + } + + + switch controller.state.repeatState { + case .circle: + repeatControl.set(image: theme.icons.audioplayer_repeat_circle, for: .Normal) + case .one: + repeatControl.set(image: theme.icons.audioplayer_repeat_one, for: .Normal) + case .none: + repeatControl.set(image: theme.icons.audioplayer_repeat_none, for: .Normal) + } + + + switch song.state { - case .waiting, .paused: + case .waiting: progressView.style = playProgressStyle - playOrPause.set(image: theme.icons.audioPlayerPlay, for: .Normal) case .stoped: - playOrPause.set(image: theme.icons.audioPlayerPlay, for: .Normal) - progressView.set(progress: 0, animated:true) - case let .playing(data): + progressView.set(progress: 0, animated: animated) + case let .playing(_, _, progress), let .paused(_, _, progress): progressView.style = playProgressStyle - progressView.set(progress: CGFloat(data.progress), animated:data.animated) - playOrPause.set(image: theme.icons.audioPlayerPause, for: .Normal) - break - case let .fetching(progress, animated): - playOrPause.set(image: theme.icons.audioPlayerLockedPlay, for: .Normal) + progressView.set(progress: CGFloat(progress == .nan ? 0 : progress), animated: animated, duration: 0.2) + case let .fetching(progress): progressView.style = fetchProgressStyle progressView.set(progress: CGFloat(progress), animated:animated) + } + + switch controller.state.status { + case .playing: + play(animated: animated, sticker: LocalAnimatedSticker.playlist_play_pause) + case .paused: + play(animated: animated, sticker: LocalAnimatedSticker.playlist_pause_play) + default: break } - } - - func songDidStartPlaying(song:APSongItem, for controller:APController) { - } - func songDidStopPlaying(song:APSongItem, for controller:APController) { + _ = previous.sizeToFit() + _ = next.sizeToFit() + _ = dismiss.sizeToFit() + _ = repeatControl.sizeToFit() + _ = playingSpeed.sizeToFit() + _ = volumeControl.sizeToFit() + needsLayout = true } - func playerDidChangedTimebase(song:APSongItem, for controller:APController) { - + + func songDidChanged(song:APSongItem, for controller:APController, animated: Bool) { + self.update(song, controller: controller, animated: animated) } - func audioDidCompleteQueue(for controller:APController) { - stopAndHide(true) + func songDidChangedState(song: APSongItem, for controller: APController, animated: Bool) { + self.update(song, controller: controller, animated: animated) } - override func setFrameSize(_ newSize: NSSize) { - super.setFrameSize(newSize) - separator.setFrameSize(newSize.width, .borderSize) + func songDidStartPlaying(song:APSongItem, for controller:APController, animated: Bool) { + self.update(song, controller: controller, animated: animated) + } + func songDidStopPlaying(song:APSongItem, for controller:APController, animated: Bool) { + self.update(song, controller: controller, animated: animated) + } + func playerDidChangedTimebase(song:APSongItem, for controller:APController, animated: Bool) { + self.update(song, controller: controller, animated: animated) + } + + func audioDidCompleteQueue(for controller:APController, animated: Bool) { + stopAndHide(true) } override func layout() { super.layout() - containerView.frame = NSMakeRect(0, 0, frame.width, frame.height) - previous.centerY(x: 20) - playOrPause.centerY(x: previous.frame.maxX + 20) - next.centerY(x: playOrPause.frame.maxX + 20) + containerView.frame = bounds + + previous.centerY(x: 17) + playPause.centerY(x: previous.frame.maxX + 5) + next.centerY(x: playPause.frame.maxX + 5) + + dismiss.centerY(x: frame.width - 20 - dismiss.frame.width) - repeatControl.centerY(x: frame.width - dismiss.frame.width - 40 - repeatControl.frame.width) - progressView.frame = NSMakeRect(0, frame.height - 10, frame.width, 10) - textView.layout?.measure(width: frame.width - (next.frame.maxX + dismiss.frame.width + repeatControl.frame.width + 20)) - textView.update(textView.layout) + repeatControl.centerY(x: dismiss.frame.minX - 10 - repeatControl.frame.width) + + + progressView.frame = NSMakeRect(0, frame.height - 6, frame.width, 6) - let w = dismiss.frame.minX - next.frame.maxX - textView.centerY(x: next.frame.maxX + floorToScreenPixels((w - textView.frame.width)/2)) - separator.setFrameOrigin(0, frame.height - .borderSize) + let textWidth = frame.width - (next.frame.maxX + dismiss.frame.width + repeatControl.frame.width + (playingSpeed.isHidden ? 0 : playingSpeed.frame.width + 10) + volumeControl.frame.width + 70) + + artistNameView?.resize(textWidth) + trackNameView.resize(textWidth) + + let effectiveWidth = [artistNameView, trackNameView].compactMap { $0?.frame.width }.max(by: { $0 < $1 }) ?? 0 + + textViewContainer.setFrameSize(NSMakeSize(effectiveWidth, 40)) + textViewContainer.centerY(x: next.frame.maxX + 20) + + playingSpeed.centerY(x: dismiss.frame.minX - playingSpeed.frame.width - 10) + + + if let artistNameView = artistNameView { + trackNameView.setFrameOrigin(NSMakePoint(0, 4)) + artistNameView.setFrameOrigin(NSMakePoint(0, textViewContainer.frame.height - artistNameView.frame.height - 4)) + } else { + trackNameView.centerY(x: 0) + } + + if repeatControl.isHidden { + volumeControl.centerY(x: playingSpeed.frame.minX - 10 - volumeControl.frame.width) + } else { + volumeControl.centerY(x: repeatControl.frame.minX - 10 - volumeControl.frame.width) + } + + + separator.frame = NSMakeRect(0, frame.height - .borderSize, frame.width, .borderSize) + } + + private func play(animated: Bool, sticker: LocalAnimatedSticker) { + let data = sticker.data + if let data = data { + + let current: Int32 + let total: Int32 + if playPauseView.animation?.key.key != LottieAnimationKey.bundle(sticker.rawValue) { + current = playPauseView.currentFrame ?? 0 + total = playPauseView.totalFrames ?? 0 + } else { + current = 0 + total = playPauseView.currentFrame ?? 0 + } + let animation = LottieAnimation(compressed: data, key: .init(key: .bundle(sticker.rawValue), size: NSMakeSize(34, 34)), cachePurpose: .none, playPolicy: .toEnd(from: animated ? total - current : .max), colors: [.init(keyPath: "", color: theme.colors.accent)], runOnQueue: .mainQueue()) + playPauseView.set(animation) + } } func stopAndHide(_ animated:Bool) -> Void { - header?.hide(true) controller?.remove(listener: self) controller?.stop() controller?.cleanup() - controller = nil instantVideoPip?.hide() instantVideoPip = nil + self.hide(animated) } required init(frame frameRect: NSRect) { diff --git a/Telegram-Mac/InlineAuthOptionRowItem.swift b/Telegram-Mac/InlineAuthOptionRowItem.swift new file mode 100644 index 0000000000..aa08863f37 --- /dev/null +++ b/Telegram-Mac/InlineAuthOptionRowItem.swift @@ -0,0 +1,91 @@ +// +// InlineAuthOptionRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 22/05/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + + +class InlineAuthOptionRowItem: GeneralRowItem { + + fileprivate let selected: Bool + fileprivate let textLayout: TextViewLayout + + init(_ initialSize: NSSize, stableId: AnyHashable, attributedString: NSAttributedString, selected: Bool, action: @escaping()->Void) { + self.selected = selected + self.textLayout = TextViewLayout(attributedString, maximumNumberOfLines: 3, alwaysStaticItems: true) + super.init(initialSize, stableId: stableId, action: action, inset: NSEdgeInsetsMake(10, 30, 10, 30)) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { + textLayout.measure(width: width - inset.left - inset.right - 50) + + return super.makeSize(width, oldWidth: oldWidth) + } + + override func viewClass() -> AnyClass { + return InlineAuthOptionRowView.self + } + + override var height: CGFloat { + return max(textLayout.layoutSize.height + inset.top + inset.bottom, 30) + } +} + + +private final class InlineAuthOptionRowView : TableRowView { + private let textView = TextView() + private let selectView: SelectingControl = SelectingControl(unselectedImage: theme.icons.chatGroupToggleUnselected, selectedImage: theme.icons.chatGroupToggleSelected) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(textView) + addSubview(selectView) + selectView.userInteractionEnabled = false + textView.userInteractionEnabled = false + textView.isSelectable = false + } + + override func mouseUp(with event: NSEvent) { + guard let item = item as? InlineAuthOptionRowItem else { + return + } + item.action() + } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + + guard let item = item as? InlineAuthOptionRowItem else { + return + } + + selectView.set(selected: item.selected, animated: animated) + } + + override func layout() { + super.layout() + + guard let item = item as? InlineAuthOptionRowItem else { + return + } + + textView.update(item.textLayout) + + if item.textLayout.layoutSize.height < selectView.frame.height { + selectView.centerY(x: item.inset.left) + textView.centerY(x: selectView.frame.maxX + 10) + } else { + selectView.setFrameOrigin(NSMakePoint(item.inset.left, item.inset.top)) + textView.setFrameOrigin(NSMakePoint(selectView.frame.maxX + 10, selectView.frame.minY)) + } + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/InlineLoginController.swift b/Telegram-Mac/InlineLoginController.swift new file mode 100644 index 0000000000..849fd7ed4f --- /dev/null +++ b/Telegram-Mac/InlineLoginController.swift @@ -0,0 +1,183 @@ +// +// InlineLoginController.swift +// Telegram +// +// Created by Mikhail Filimonov on 22/05/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import SwiftSignalKit +import Postbox + +public struct InlineLoginOption: OptionSet { + public var rawValue: Int32 + + public init() { + self.rawValue = 0 + } + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public static let login = InlineLoginOption(rawValue: 1 << 0) + public static let allowMessages = InlineLoginOption(rawValue: 1 << 1) + +} + + + +private struct InlineLoginState : Equatable { + let options:InlineLoginOption + init(options: InlineLoginOption) { + self.options = options + } + + func withUpdatedOption(_ option: InlineLoginOption) -> InlineLoginState { + var options = self.options + if options.contains(option) { + options.remove(option) + } else { + options.insert(option) + } + return InlineLoginState(options: options) + } + + func withRemovedOption(_ option: InlineLoginOption, _ dependsOn: InlineLoginOption) -> InlineLoginState { + var options = self.options + if !options.contains(dependsOn) { + options.remove(option) + } + return InlineLoginState(options: options) + } +} + +private let _id_option_login = InputDataIdentifier("_id_option_login") +private let _id_option_allow_send_messages = InputDataIdentifier("_id_option_allow_send_messages") + +private func inlineLoginEntries(_ state: InlineLoginState, url: String, accountPeer: Peer, botPeer: Peer, writeAllowed: Bool, toggleOption:@escaping(InlineLoginOption)->Void, removeOption:@escaping(InlineLoginOption, InlineLoginOption)->Void) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId: Int32 = 0 + var index: Int32 = 0 + + let host = URL(string: url)?.host ?? url + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("title"), equatable: nil, comparable: nil, item: { initialSize, stableId in + + let attributedString = NSMutableAttributedString() + let string = L10n.botInlineAuthTitle(url) + let _ = attributedString.append(string: string, color: theme.colors.text, font: .normal(.text)) + let range = string.nsstring.range(of: url) + attributedString.addAttribute(.font, value: NSFont.medium(.text), range: range) + + return GeneralTextRowItem(initialSize, stableId: stableId, text: attributedString, alignment: .center, drawCustomSeparator: false, centerViewAlignment: true, isTextSelectable: false, detectLinks: false) + })) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + let loginEnabled = state.options.contains(.login) + let allowMessagesEnabled = state.options.contains(.allowMessages) + + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_option_login, equatable: InputDataEquatable(loginEnabled), comparable: nil, item: { initialSize, stableId in + + let attributeString = NSMutableAttributedString() + let string = L10n.botInlineAuthOptionLogin(host, accountPeer.displayTitle) + let hostRange = string.nsstring.range(of: host) + _ = attributeString.append(string: string, color: theme.colors.text, font: .normal(.text)) + attributeString.addAttribute(.font, value: NSFont.medium(.text), range: hostRange) + + return InlineAuthOptionRowItem(initialSize, stableId: stableId, attributedString: attributeString, selected: loginEnabled, action: { + toggleOption(.login) + removeOption(.allowMessages, .login) + }) + })) + index += 1 + + + if writeAllowed { + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_option_allow_send_messages, equatable: InputDataEquatable(allowMessagesEnabled), comparable: nil, item: { initialSize, stableId in + + let attributeString = NSMutableAttributedString() + let string = L10n.botInlineAuthOptionAllowSendMessages(botPeer.displayTitle) + let titleRange = string.nsstring.range(of: botPeer.displayTitle) + _ = attributeString.append(string: string, color: theme.colors.text, font: .normal(.text)) + attributeString.addAttribute(.font, value: NSFont.medium(.text), range: titleRange) + + return InlineAuthOptionRowItem(initialSize, stableId: stableId, attributedString: attributeString, selected: allowMessagesEnabled, action: { + toggleOption(.allowMessages) + }) + })) + index += 1 + } + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func InlineLoginController(context: AccountContext, url: String, originalURL: String, writeAllowed: Bool, botPeer: Peer, authorize: @escaping(Bool)->Void) -> InputDataModalController { + + + let initialState = writeAllowed ? InlineLoginState(options: [.login, .allowMessages]) : InlineLoginState(options: [.login]) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((InlineLoginState) -> InlineLoginState) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let signal = combineLatest(statePromise.get(), context.account.postbox.loadedPeerWithId(context.peerId)) |> map { state, accountPeer in + return inlineLoginEntries(state, url: url, accountPeer: accountPeer, botPeer: botPeer, writeAllowed: writeAllowed, toggleOption: { option in + updateState { current in + return current.withUpdatedOption(option) + } + }, removeOption: { option, dependsOn in + updateState { current in + return current.withRemovedOption(option, dependsOn) + } + }) + } |> map { InputDataSignalValue(entries: $0) } + + var close:(()->Void)? + + let interactions = ModalInteractions(acceptTitle: L10n.botInlineAuthOpen, accept: { + let state = stateValue.with { $0 } + + if state.options.isEmpty { + execute(inapp: inAppLink.external(link: originalURL, false)) + } else { + authorize(state.options.contains(.allowMessages)) + } + close?() + }, drawBorder: true, height: 50, singleButton: true) + + let controller = InputDataController(dataSignal: signal, title: L10n.botInlineAuthHeader) + + controller.getBackgroundColor = { + theme.colors.background + } + + let modalController = InputDataModalController(controller, modalInteractions: interactions) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) + + close = { [weak modalController] in + modalController?.close() + } + + return modalController +} diff --git a/Telegram-Mac/InputContextHelper.swift b/Telegram-Mac/InputContextHelper.swift index 82a27efb46..d807187355 100644 --- a/Telegram-Mac/InputContextHelper.swift +++ b/Telegram-Mac/InputContextHelper.swift @@ -7,28 +7,35 @@ // import Cocoa -import SwiftSignalKitMac +import SwiftSignalKit import TGUIKit -import PostboxMac -import TelegramCoreMac +import Postbox +import TelegramCore + enum InputContextEntry : Comparable, Identifiable { case switchPeer(PeerId, ChatContextResultSwitchPeer) + case message(Int64, Message, String) case peer(Peer, Int, Int64) case contextResult(ChatContextResultCollection,ChatContextResult,Int64) - case contextMediaResult(ChatContextResultCollection, InputMediaContextRow, Int64) + case contextMediaResult(ChatContextResultCollection?, InputMediaContextRow, Int64) case command(PeerCommand, Int64, Int64) case sticker(InputMediaStickersRow, Int64) - case emoji(EmojiClue, Int32) - case inlineRestricted(String?) + case showPeers(Int, Int64) + case emoji([String], String?, Bool, Int32) + case hashtag(String, Int64) + case inlineRestricted(String) + case separator(String, Int64, Int64) var stableId: Int64 { switch self { case .switchPeer: return -1 - case let .peer(peer,_, stableId): + case let .message(_, message, _): + return message.id.toInt64() + case let .peer(_,_, stableId): return stableId case let .contextResult(_,_,index): return index @@ -36,12 +43,18 @@ enum InputContextEntry : Comparable, Identifiable { return index case let .command( _, _, stableId): return stableId - case let .sticker( _, stableId): + case let .sticker(_, stableId): + return stableId + case let .showPeers(_, stableId): return stableId - case let .emoji(clue, _): - return clue.hashValue + case let .hashtag(hashtag, _): + return Int64(hashtag.hashValue) + case let .emoji(clue, _, _, _): + return Int64(clue.joined().hashValue) case .inlineRestricted: return -1000 + case let .separator(_, _, stableId): + return stableId } } @@ -59,10 +72,18 @@ enum InputContextEntry : Comparable, Identifiable { return index //result.maybeId | ((Int64(index) << 40)) case let .sticker(_, index): return index //result.maybeId | ((Int64(index) << 40)) - case let .emoji(_, index): + case let .showPeers(index, _): + return Int64(index) //result.maybeId | ((Int64(index) << 40)) + case let .hashtag(_, index): + return index + case let .emoji(_, _, _, index): return Int64(index) //result.maybeId | ((Int64(index) << 40)) case .inlineRestricted: return 0 + case let .message(index, _, _): + return index + case let .separator(_, index, _): + return index } } } @@ -104,9 +125,19 @@ func ==(lhs:InputContextEntry, rhs:InputContextEntry) -> Bool { return lhsSticker == rhsSticker && lhsIndex == rhsIndex } return false - case let .emoji(lhsClue, lhsIndex): - if case let .emoji(rhsClue, rhsIndex) = rhs { - return lhsClue == rhsClue && lhsIndex == rhsIndex + case let .showPeers(index, stableId): + if case .showPeers(index, stableId) = rhs { + return true + } + return false + case let .hashtag(lhsHashtag, lhsIndex): + if case let .hashtag(rhsHashtag, rhsIndex) = rhs { + return lhsHashtag == rhsHashtag && lhsIndex == rhsIndex + } + return false + case let .emoji(lhsClue, lhsCurrent, lhsFirstWord, lhsIndex): + if case let .emoji(rhsClue, rhsCurrent, rhsFirstWord, rhsIndex) = rhs { + return lhsClue == rhsClue && lhsIndex == rhsIndex && lhsFirstWord == rhsFirstWord && lhsCurrent == rhsCurrent } return false case let .inlineRestricted(lhsText): @@ -115,41 +146,71 @@ func ==(lhs:InputContextEntry, rhs:InputContextEntry) -> Bool { } else { return false } + case let .message(index, lhsMessage, searchText): + if case .message(index, let rhsMessage, searchText) = rhs { + return isEqualMessages(lhsMessage, rhsMessage) + } else { + return false + } + case let .separator(value1, value2, value3): + if case .separator(value1, value2, value3) = rhs { + return true + } + return false } } -fileprivate func prepareEntries(left:[AppearanceWrapperEntry]?, right:[AppearanceWrapperEntry], account:Account,initialSize:NSSize, chatInteraction:ChatInteraction) -> TableUpdateTransition { +fileprivate func prepareEntries(left:[AppearanceWrapperEntry]?, right:[AppearanceWrapperEntry], context: AccountContext, initialSize:NSSize, chatInteraction:ChatInteraction) -> TableUpdateTransition { let (removed,inserted, updated) = proccessEntriesWithoutReverse(left, right: right, { entry -> TableRowItem in switch entry.entry { case let .switchPeer(peerId, switchPeer): - return ContextSwitchPeerRowItem(initialSize, peerId:peerId, switchPeer:switchPeer, account:account, callback: { + return ContextSwitchPeerRowItem(initialSize, peerId:peerId, switchPeer:switchPeer, account: context.account, callback: { chatInteraction.switchInlinePeer(peerId, .start(parameter: switchPeer.startParam, behavior: .automatic)) }) case let .peer(peer, _, _): var status:String? - if let user = peer as? TelegramUser { - status = user.username + if let user = peer as? TelegramUser, let address = user.addressName { + status = "@\(address)" } let titleStyle:ControlStyle = ControlStyle(font: .normal(.text), foregroundColor: theme.colors.text, backgroundColor: theme.colors.background, highlightColor:.white) let statusStyle:ControlStyle = ControlStyle(font: .normal(.text), foregroundColor: theme.colors.grayText, backgroundColor: theme.colors.background, highlightColor:.white) - return ShortPeerRowItem(initialSize, peer: peer, account: account, height: 40, photoSize: NSMakeSize(30, 30), titleStyle: titleStyle, statusStyle: statusStyle, status: status, borderType: [], drawCustomSeparator: true, inset: NSEdgeInsets(left:20)) + return ShortPeerRowItem(initialSize, peer: peer, account: context.account, height: 40, photoSize: NSMakeSize(30, 30), titleStyle: titleStyle, statusStyle: statusStyle, status: status, borderType: [], drawCustomSeparator: true, inset: NSEdgeInsets(left:20)) case let .contextResult(results,result,index): - return ContextListRowItem(initialSize, results, result, index, account, chatInteraction) + return ContextListRowItem(initialSize, results, result, index, context, chatInteraction) case let .contextMediaResult(results,result,index): - return ContextMediaRowItem(initialSize, results, result, index, account, chatInteraction) + return ContextMediaRowItem(initialSize, result, index, context, ContextMediaArguments(sendResult: { result, view in + if let results = results { + if let slowMode = chatInteraction.presentation.slowMode, slowMode.hasLocked { + showSlowModeTimeoutTooltip(slowMode, for: view) + } else { + chatInteraction.sendInlineResult(results, result) + } + } + })) case let .command(command,_, stableId): - return ContextCommandRowItem(initialSize, account, command, stableId) - case let .emoji(clue, _): - return ContextClueRowItem(initialSize, stableId: entry.stableId, clue: clue) + return ContextCommandRowItem(initialSize, context.account, command, stableId) + case let .emoji(clues, selected, firstWord, _): + return ContextClueRowItem(initialSize, stableId: entry.stableId, context: context, clues: clues, selected: selected, canDisablePrediction: firstWord) + case let .hashtag(hashtag, _): + return ContextHashtagRowItem(initialSize, hashtag: "#\(hashtag)") case let .sticker(result, stableId): - return ContextStickerRowItem(initialSize, account, result, stableId, chatInteraction) - case .inlineRestricted(let until): - let text = until != nil ? tr(.channelPersmissionDeniedSendInlineUntil(until!)) : tr(.channelPersmissionDeniedSendInlineForever) + return ContextStickerRowItem(initialSize, context, result, stableId, chatInteraction) + case .showPeers: + return ContextShowPeersHolderItem(initialSize, stableId: entry.stableId, action: { + + }) + case let .inlineRestricted(text): return GeneralTextRowItem(initialSize, stableId: entry.stableId, height: 40, text: text, alignment: .center, centerViewAlignment: true) + case let .message(_, message, searchText): + return ContextSearchMessageItem(initialSize, context: context, message: message, searchText: searchText, action: { + + }) + case let .separator(string, _, _): + return SeparatorRowItem(initialSize, entry.stableId, string: string) } }) @@ -159,7 +220,7 @@ fileprivate func prepareEntries(left:[AppearanceWrapperEntry] } -class InputContextView : TableView, AppearanceViewProtocol { +class InputContextView : TableView { //let tableView:TableView let separatorView:View weak var relativeView: NSView? @@ -170,21 +231,21 @@ class InputContextView : TableView, AppearanceViewProtocol { } } - public required init(frame frameRect: NSRect, isFlipped: Bool = true, bottomInset:CGFloat = 0, drawBorder: Bool = false) { + public override init(frame frameRect: NSRect) { // tableView = TableView(frame: NSMakeRect(0, 0, frameRect.width, frameRect.height)) separatorView = View(frame: NSMakeRect(0, 0, frameRect.width, .borderSize)) super.init(frame: frameRect) // addSubview(tableView) addSubview(separatorView) separatorView.autoresizingMask = [.width, .maxYMargin] - updateLocalizationAndTheme() + updateLocalizationAndTheme(theme: theme) } - func updateLocalizationAndTheme() { + override func updateLocalizationAndTheme(theme: PresentationTheme) { separatorView.backgroundColor = theme.colors.border - //backgroundColor = theme.colors.background + // backgroundColor = theme.colors.background } required init?(coder: NSCoder) { @@ -196,7 +257,7 @@ class InputContextView : TableView, AppearanceViewProtocol { case .above: separatorView.setFrameOrigin(0, 0) case .below: - separatorView.setFrameOrigin(NSMakePoint(0, frame.height - separatorView.frame.height)) + separatorView.frame = NSMakeRect(0, frame.height - separatorView.frame.height, frame.width, .borderSize) } } @@ -206,112 +267,203 @@ class InputContextView : TableView, AppearanceViewProtocol { } } +private enum OverscrollState { + case small + case intermediate + case full +} + +private final class OverscrollData { + var state:OverscrollState + init(state: OverscrollState) { + self.state = state + } +} + class InputContextViewController : GenericViewController, TableViewDelegate { - private let account:Account + func findGroupStableId(for stableId: AnyHashable) -> AnyHashable? { + return nil + } + + private let overscrollData: OverscrollData = OverscrollData(state: .small) + + fileprivate var markAsNeedShown: Bool = false + + private let context:AccountContext private let chatInteraction:ChatInteraction + private let highlightInsteadOfSelect: Bool + + private var escapeTextMarked: String? + + fileprivate var result:ChatPresentationInputQueryResult? + + fileprivate weak var superview: NSView? override func loadView() { super.loadView() genericView.delegate = self view.layer?.opacity = 0 } - init(account:Account,chatInteraction:ChatInteraction) { - self.account = account + init(context: AccountContext, chatInteraction: ChatInteraction, highlightInsteadOfSelect: Bool) { self.chatInteraction = chatInteraction + self.context = context + self.highlightInsteadOfSelect = highlightInsteadOfSelect super.init() } + + override func viewDidLoad() { + super.viewDidLoad() + } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - self.window?.set(handler: {[weak self] () -> KeyHandlerResult in - let prev = self?.deselectSelectedSticker() - self?.genericView.selectNext(true,true) - self?.selectFirstInRowIfCan(prev) + context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + guard let `self` = self else {return .rejected} + let prev = self.deselectSelectedHorizontalItem() + self.highlightInsteadOfSelect ? self.genericView.highlightNext(true,true) : self.genericView.selectNext(true,true) + self.selectFirstInRowIfCan(prev, false) return .invoked - }, with: self, for: .DownArrow, priority: .high) + }, with: self, for: .DownArrow, priority: .modal) - self.window?.set(handler: {[weak self] () -> KeyHandlerResult in - let prev = self?.deselectSelectedSticker() - self?.genericView.selectPrev(true,true) - self?.selectFirstInRowIfCan(prev) + context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + guard let `self` = self else {return .rejected} + let prev = self.deselectSelectedHorizontalItem() + self.highlightInsteadOfSelect ? self.genericView.highlightPrev(true,true) : self.genericView.selectPrev(true,true) + self.selectFirstInRowIfCan(prev, true) return .invoked - }, with: self, for: .UpArrow, priority: .high) + }, with: self, for: .UpArrow, priority: .modal) - self.window?.set(handler: { [weak self] () -> KeyHandlerResult in + context.window.set(handler: { [weak self] _ -> KeyHandlerResult in if let strongSelf = self { if case .stickers = strongSelf.chatInteraction.presentation.inputContext { strongSelf.selectPreviousSticker() - return .invoked + return strongSelf.genericView.selectedItem() != nil ? .invoked : .invokeNext + } else if case .emoji = strongSelf.chatInteraction.presentation.inputContext { + return strongSelf.selectPrevEmojiClue() } } return .invokeNext - }, with: self, for: .LeftArrow, priority: .high) + }, with: self, for: .LeftArrow, priority: .modal) + + context.window.set(handler: { _ -> KeyHandlerResult in + return .invokeNext + }, with: self, for: .RightArrow, priority: .modal, modifierFlags: [.command]) - self.window?.set(handler: { [weak self] () -> KeyHandlerResult in + context.window.set(handler: { _ -> KeyHandlerResult in + return .invokeNext + }, with: self, for: .UpArrow, priority: .modal, modifierFlags: [.command]) + + context.window.set(handler: { _ -> KeyHandlerResult in + return .invokeNext + }, with: self, for: .DownArrow, priority: .modal, modifierFlags: [.command]) + + context.window.set(handler: { _ -> KeyHandlerResult in + return .invokeNext + }, with: self, for: .LeftArrow, priority: .modal, modifierFlags: [.command]) + + context.window.set(handler: { [weak self] _ -> KeyHandlerResult in if let strongSelf = self { if case .stickers = strongSelf.chatInteraction.presentation.inputContext { strongSelf.selectNextSticker() - return .invoked + return strongSelf.genericView.selectedItem() != nil ? .invoked : .invokeNext + } else if case .emoji = strongSelf.chatInteraction.presentation.inputContext { + return strongSelf.selectNextEmojiClue() } } return .invokeNext - }, with: self, for: .RightArrow, priority: .high) + }, with: self, for: .RightArrow, priority: .modal) - self.window?.set(handler: {[weak self] () -> KeyHandlerResult in + context.window.set(handler: { [weak self] _ -> KeyHandlerResult in if let strongSelf = self { + if strongSelf.context.isInGlobalSearch { + return .rejected + } return strongSelf.invoke() } return .invokeNext - }, with: self, for: .Return, priority: .high) + }, with: self, for: .Return, priority: .modal) - self.window?.set(handler: {[weak self] () -> KeyHandlerResult in + context.window.set(handler: { [weak self] _ -> KeyHandlerResult in if let strongSelf = self { return strongSelf.invokeTab() } return .invokeNext - }, with: self, for: .Tab, priority: .high) + }, with: self, for: .Tab, priority: .modal) - self.window?.set(handler: {[weak self] () -> KeyHandlerResult in + context.window.set(handler: { [weak self] _ -> KeyHandlerResult in if self?.genericView.selectedItem() != nil { - _ = self?.deselectSelectedSticker() + _ = self?.deselectSelectedHorizontalItem() self?.genericView.cancelSelection() + self?.escapeTextMarked = self?.chatInteraction.presentation.effectiveInput.inputText return .invoked } return .rejected }, with: self, for: .Escape, priority: .modal) + + } func invoke() -> KeyHandlerResult { - if let selectedItem = genericView.selectedItem() { + if let selectedItem = genericView.highlightedItem() ?? genericView.selectedItem() { if let selectedItem = selectedItem as? ShortPeerRowItem { chatInteraction.movePeerToInput(selectedItem.peer) } else if let selectedItem = selectedItem as? ContextListRowItem { - chatInteraction.sendInlineResult(selectedItem.results,selectedItem.result) + + if let slowMode = chatInteraction.presentation.slowMode, slowMode.hasLocked { + if let view = selectedItem.view { + showSlowModeTimeoutTooltip(slowMode, for: view) + self.genericView.cancelSelection() + } + } else { + chatInteraction.sendInlineResult(selectedItem.results,selectedItem.result) + } } else if let selectedItem = selectedItem as? ContextCommandRowItem { - chatInteraction.sendCommand(selectedItem.command) + if let slowMode = chatInteraction.presentation.slowMode, slowMode.hasLocked { + if let view = selectedItem.view { + showSlowModeTimeoutTooltip(slowMode, for: view) + self.genericView.cancelSelection() + } + } else { + chatInteraction.sendCommand(selectedItem.command) + } } else if let selectedItem = selectedItem as? ContextClueRowItem { - let clue = selectedItem.clue - + if let clue = selectedItem.selectedIndex != nil ? selectedItem.clues[selectedItem.selectedIndex!] : nil { + let textInputState = chatInteraction.presentation.effectiveInput + if let (range, _, _) = textInputStateContextQueryRangeAndType(textInputState, includeContext: false) { + let inputText = textInputState.inputText + + let distance = inputText.distance(from: range.lowerBound, to: range.upperBound) + let replacementText = clue + + let atLength = range.lowerBound > inputText.startIndex && inputText[inputText.index(before: range.lowerBound)] == ":" ? 1 : 0 + _ = chatInteraction.appendText(replacementText, selectedRange: textInputState.selectionRange.lowerBound - distance - atLength ..< textInputState.selectionRange.upperBound) + } + } else { + return .rejected + } + } else if let selectedItem = selectedItem as? ContextHashtagRowItem { let textInputState = chatInteraction.presentation.effectiveInput if let (range, _, _) = textInputStateContextQueryRangeAndType(textInputState, includeContext: false) { let inputText = textInputState.inputText let distance = inputText.distance(from: range.lowerBound, to: range.upperBound) - let replacementText = clue.emoji + let replacementText = selectedItem.hashtag + " " let atLength = 1 _ = chatInteraction.appendText(replacementText, selectedRange: textInputState.selectionRange.lowerBound - distance - atLength ..< textInputState.selectionRange.upperBound) } } else if let selectedItem = selectedItem as? ContextStickerRowItem, let index = selectedItem.selectedIndex { - chatInteraction.sendAppFile(selectedItem.result.results[index].file) + chatInteraction.sendAppFile(selectedItem.result.results[index].file, false, chatInteraction.presentation.effectiveInput.inputText) chatInteraction.clearInput() + } else if let selectedItem = selectedItem as? ContextSearchMessageItem { + chatInteraction.focusMessageId(nil, selectedItem.message.id, .CenterEmpty) } return .invoked } - return .invokeNext + return .rejected } func invokeTab() -> KeyHandlerResult { @@ -322,22 +474,85 @@ class InputContextViewController : GenericViewController, Tabl let commandText = "/" + selectedItem.command.command.text + " " chatInteraction.updateInput(with: commandText) + } else if let selectedItem = selectedItem as? ContextClueRowItem { + let clue: String? + if let index = selectedItem.selectedIndex { + clue = selectedItem.clues[index] + } else { + clue = selectedItem.clues.first + } + if let clue = clue { + let textInputState = chatInteraction.presentation.effectiveInput + if let (range, _, _) = textInputStateContextQueryRangeAndType(textInputState, includeContext: false) { + let inputText = textInputState.inputText + + let distance = inputText.distance(from: range.lowerBound, to: range.upperBound) + let replacementText = clue + + let atLength = range.lowerBound > inputText.startIndex && inputText[inputText.index(before: range.lowerBound)] == ":" ? 1 : 0 + _ = chatInteraction.appendText(replacementText, selectedRange: textInputState.selectionRange.lowerBound - distance - atLength ..< textInputState.selectionRange.upperBound) + } + } + return .invoked + } else if let selectedItem = selectedItem as? ContextHashtagRowItem { + let textInputState = chatInteraction.presentation.effectiveInput + if let (range, _, _) = textInputStateContextQueryRangeAndType(textInputState, includeContext: false) { + let inputText = textInputState.inputText + + let distance = inputText.distance(from: range.lowerBound, to: range.upperBound) + let replacementText = selectedItem.hashtag + " " + + let atLength = 1 + _ = chatInteraction.appendText(replacementText, selectedRange: textInputState.selectionRange.lowerBound - distance - atLength ..< textInputState.selectionRange.upperBound) + } } return .invoked } return .invokeNext } - func deselectSelectedSticker() -> Int? { + func deselectSelectedHorizontalItem() -> Int? { var prev:Int? = nil if let selectedItem = genericView.selectedItem() as? ContextStickerRowItem { prev = selectedItem.selectedIndex selectedItem.selectedIndex = nil - selectedItem.redraw() + selectedItem.redraw(animated: true) + } + if let selectedItem = genericView.selectedItem() as? ContextClueRowItem { + prev = selectedItem.selectedIndex + selectedItem.selectedIndex = nil + selectedItem.redraw(animated: true) } return prev } + func selectNextEmojiClue() -> KeyHandlerResult { + if let selectedItem = genericView.selectedItem() as? ContextClueRowItem { + if let selectedIndex = selectedItem.selectedIndex { + var index = selectedIndex + index += 1 + selectedItem.selectedIndex = max(min(index, selectedItem.clues.count - 1), 0) + selectedItem.redraw(animated: true) + } + + return selectedItem.selectedIndex != nil ? .invoked : .rejected + } + return .rejected + + } + func selectPrevEmojiClue() -> KeyHandlerResult { + if let selectedItem = genericView.selectedItem() as? ContextClueRowItem { + if let selectedIndex = selectedItem.selectedIndex { + var index = selectedIndex + index -= 1 + selectedItem.selectedIndex = max(min(index, selectedItem.clues.count - 1), 0) + selectedItem.redraw(animated: true) + } + return selectedItem.selectedIndex != nil ? .invoked : .rejected + } + return .rejected + } + func selectPreviousSticker() { if let selectedItem = genericView.selectedItem() as? ContextStickerRowItem { if selectedItem.selectedIndex != nil { @@ -348,7 +563,7 @@ class InputContextViewController : GenericViewController, Tabl } if selectedItem.selectedIndex! < 0 { - _ = deselectSelectedSticker() + _ = deselectSelectedHorizontalItem() genericView.selectPrev(true,true) selectLastInRowIfCan() } else { @@ -367,7 +582,7 @@ class InputContextViewController : GenericViewController, Tabl } if selectedItem.selectedIndex! > selectedItem.result.entries.count - 1 { - _ = deselectSelectedSticker() + _ = deselectSelectedHorizontalItem() genericView.selectNext(true,true) selectFirstInRowIfCan() } else { @@ -376,13 +591,24 @@ class InputContextViewController : GenericViewController, Tabl } } - func selectFirstInRowIfCan(_ start:Int? = nil) { + func selectFirstInRowIfCan(_ start:Int? = nil, _ bottom: Bool = false) { if let selectedItem = genericView.selectedItem() as? ContextStickerRowItem { var index = start ?? 0 index = max(index, 0) selectedItem.selectedIndex = index selectedItem.redraw() } + if let selectedItem = genericView.selectedItem() as? ContextClueRowItem { + var index: Int + if let start = start { + index = start + (bottom ? -1 : 1) + } else { + index = bottom ? selectedItem.clues.count - 1 : 0 + } + index = min(max(index, 0), selectedItem.clues.count - 1) + selectedItem.selectedIndex = index + selectedItem.redraw(animated: true) + } } func selectLastInRowIfCan(_ start:Int? = nil) { @@ -390,7 +616,7 @@ class InputContextViewController : GenericViewController, Tabl var index = start ?? selectedItem.result.entries.count - 1 index = min(index, selectedItem.result.entries.count - 1) selectedItem.selectedIndex = index - selectedItem.redraw() + selectedItem.redraw(animated: true) } } @@ -400,7 +626,7 @@ class InputContextViewController : GenericViewController, Tabl } func cleanup() { - self.window?.removeAllHandlers(for: self) + mainWindow.removeAllHandlers(for: self) } deinit { @@ -417,41 +643,77 @@ class InputContextViewController : GenericViewController, Tabl _ = invoke() } } - func selectionWillChange(row:Int, item:TableRowItem) -> Bool { + func selectionWillChange(row:Int, item:TableRowItem, byClick: Bool) -> Bool { return true } func isSelectable(row:Int, item:TableRowItem) -> Bool { return !(item is ContextMediaRowItem) } - func make(with transition:TableUpdateTransition, animated:Bool, result: ChatPresentationInputQueryResult? = nil) { + func make(with transition:TableUpdateTransition, animated:Bool, selectIndex: Int?, result: ChatPresentationInputQueryResult? = nil) { assertOnMainThread() genericView.cancelSelection() + genericView.cancelHighlight() genericView.merge(with: transition) layout(animated) - if !genericView.isEmpty, let result = result, case .mentions = result { - _ = genericView.select(item: genericView.item(at: 0)) + if !genericView.isEmpty, let result = result { + + if let escapeTextMarked = escapeTextMarked, escapeTextMarked == self.chatInteraction.presentation.effectiveInput.inputText { + return + } else { + escapeTextMarked = nil + } + switch result { + case .mentions, .searchMessages, .commands, .hashtags: + if !highlightInsteadOfSelect { + _ = genericView.select(item: genericView.item(at: selectIndex ?? 0)) + } else { + _ = genericView.highlight(item: genericView.item(at: selectIndex ?? 0)) + } + case let .emoji(_, firstWord): + if !highlightInsteadOfSelect { + _ = genericView.select(item: genericView.item(at: selectIndex ?? 0)) + } else { + _ = genericView.highlight(item: genericView.item(at: selectIndex ?? 0)) + } + if !firstWord { + _ = selectFirstInRowIfCan() + } + default: + break + } + if let selectIndex = selectIndex { + _ = genericView.select(item: genericView.item(at: selectIndex)) + genericView.scroll(to: .center(id: genericView.item(at: selectIndex).stableId, innerId: nil, animated: false, focus: .init(focus: false), inset: 0)) + } } } func layout(_ animated:Bool) { - let future = NSMakeSize(frame.width, min(genericView.listHeight,140)) - // genericView.change(size: future, animated: animated) - // genericView.change(pos: NSMakePoint(0, 0), animated: animated) - - genericView.change(size: future, animated: animated) - - switch genericView.position { - case .above: - genericView.separatorView.change(pos: NSZeroPoint, animated: true) - case .below: - genericView.separatorView.change(pos: NSMakePoint(0, frame.height - genericView.separatorView.frame.height), animated: true) + if let superview = superview, let relativeView = genericView.relativeView { + var height = min(superview.frame.height - 50 - relativeView.frame.height, floor(superview.frame.height / 3)) + if genericView.firstItem is ContextClueRowItem { + height = min(height, 120) + } + let future = NSMakeSize(frame.width, min(genericView.listHeight, height)) + // genericView.change(size: future, animated: animated) + // genericView.change(pos: NSMakePoint(0, 0), animated: animated) + CATransaction.begin() + genericView.change(size: future, animated: animated, duration: future.height > frame.height || genericView.position == .below ? 0 : 0.5) + + switch genericView.position { + case .above: + genericView.separatorView.change(pos: NSZeroPoint, animated: animated) + case .below: + genericView.separatorView.change(pos: NSMakePoint(0, frame.height - genericView.separatorView.frame.height), animated: animated) + } + +// let y = genericView.position == .above ? relativeView.frame.minY - frame.height : relativeView.frame.maxY +// genericView.change(pos: NSMakePoint(0, y), animated: animated) + + CATransaction.commit() } - if let relativeView = genericView.relativeView { - let y = genericView.position == .above ? relativeView.frame.minY - frame.height : relativeView.frame.maxY - genericView.change(pos: NSMakePoint(0, y), animated: animated) - } } @@ -464,17 +726,17 @@ enum InputContextPosition { class InputContextHelper: NSObject { - var disposable:MetaDisposable = MetaDisposable() + private let disposable:MetaDisposable = MetaDisposable() - private var controller:InputContextViewController - private let account:Account + let controller:InputContextViewController + private let context:AccountContext private let chatInteraction:ChatInteraction private let entries:Atomic<[AppearanceWrapperEntry]?> = Atomic(value:nil) - - init(account:Account, chatInteraction:ChatInteraction) { - self.account = account + private let loadMoreDisposable = MetaDisposable() + init(chatInteraction:ChatInteraction, highlightInsteadOfSelect: Bool = false) { self.chatInteraction = chatInteraction - controller = InputContextViewController(account:account,chatInteraction:chatInteraction) + self.context = chatInteraction.context + controller = InputContextViewController(context: chatInteraction.context, chatInteraction: chatInteraction, highlightInsteadOfSelect: highlightInsteadOfSelect) } public var accessoryView:NSView? { @@ -485,34 +747,74 @@ class InputContextHelper: NSObject { self.controller.viewWillDisappear(false) } - func context(with result:ChatPresentationInputQueryResult?, for view: NSView, relativeView: NSView, position: InputContextPosition = .above, animated:Bool) { - - controller._frameRect = NSMakeRect(0, 0, view.frame.width, 140) + func context(with result:ChatPresentationInputQueryResult?, for view: NSView, relativeView: NSView, position: InputContextPosition = .above, selectIndex:Int? = nil, animated:Bool) { + controller._frameRect = NSMakeRect(0, 0, view.frame.width, floor(view.frame.height / 3)) controller.loadViewIfNeeded() - + controller.superview = view controller.genericView.relativeView = relativeView controller.genericView.position = position + controller.updateLocalizationAndTheme(theme: theme) + var currentResult = result + let initialSize = controller.atomicSize + let previosEntries = self.entries + let context = self.chatInteraction.context + let chatInteraction = self.chatInteraction - let makeSignal = combineLatest(entries(for: result, initialSize:initialSize.modify {$0}, chatInteraction: chatInteraction), appearanceSignal) |> map { [weak self] entries, appearance -> (TableUpdateTransition,Bool, Bool) in - if let strongSelf = self { - let entries = entries.map{AppearanceWrapperEntry(entry: $0, appearance: appearance)} - let previous = strongSelf.entries.swap(entries) - let previousIsEmpty:Bool = previous?.isEmpty ?? true - return (prepareEntries(left: previous, right: entries, account: strongSelf.account, initialSize: initialSize.modify({$0}), chatInteraction:strongSelf.chatInteraction),!entries.isEmpty, previousIsEmpty) + let entriesValue: Promise<[InputContextEntry]> = Promise() + + self.loadMoreDisposable.set(nil) + + controller.genericView.setScrollHandler { [weak self] position in + guard let `self` = self, let result = currentResult else {return} + switch position.direction { + case .bottom: + switch result { + case let .searchMessages(messages, _, _): + messages.2(messages.1) + case let .contextRequestResult(peer, oldCollection): + if let oldCollection = oldCollection, let nextOffset = oldCollection.nextOffset { + self.loadMoreDisposable.set((context.engine.messages.requestChatContextResults(botId: oldCollection.botId, peerId: self.chatInteraction.peerId, query: oldCollection.query, offset: nextOffset) |> delay(0.5, queue: Queue.mainQueue())).start(next: { [weak self] collection in + guard let `self` = self else {return} + + if let collection = collection { + let newResult = ChatPresentationInputQueryResult.contextRequestResult(peer, oldCollection.withAdditionalCollection(collection.results)) + currentResult = newResult + entriesValue.set(self.entries(for: newResult, initialSize: initialSize.modify {$0}, chatInteraction: chatInteraction)) + } + })) + } + default: + break + } + default: + break } - return (TableUpdateTransition(deleted: [], inserted: [], updated: []), false, false) + } + if chatInteraction.presentation.state == .normal || chatInteraction.presentation.state == .editing { + entriesValue.set(entries(for: result, initialSize: initialSize.modify {$0}, chatInteraction: chatInteraction)) + } else { + entriesValue.set(.single([])) + } + + let makeSignal = combineLatest(queue: prepareQueue, entriesValue.get(), appearanceSignal) |> map { entries, appearance -> (TableUpdateTransition,Bool, Bool) in + + let entries = entries.map{AppearanceWrapperEntry(entry: $0, appearance: appearance)} + let previous = previosEntries.swap(entries) + let previousIsEmpty:Bool = previous?.isEmpty ?? true + return (prepareEntries(left: previous, right: entries, context: context, initialSize: initialSize.modify({$0}), chatInteraction:chatInteraction),!entries.isEmpty, previousIsEmpty) } |> deliverOnMainQueue - disposable.set(makeSignal.start(next: { [weak self, weak view] transition, show, previousIsEmpty in + disposable.set((makeSignal |> map { [weak self, weak view, weak relativeView] transition, show, previousIsEmpty in - if show, let controller = self?.controller { + if show, let controller = self?.controller, let relativeView = relativeView { if previousIsEmpty { controller.genericView.removeAll() } - controller.make(with: transition, animated:animated, result: result) + controller.make(with: transition, animated:animated, selectIndex: selectIndex, result: result) if let view = view { + controller.markAsNeedShown = true controller.viewWillAppear(animated) if controller.view.superview == nil { view.addSubview(controller.view, positioned: .below, relativeTo: relativeView) @@ -520,52 +822,59 @@ class InputContextHelper: NSObject { controller.view.setFrameOrigin(0, relativeView.frame.minY) } controller.viewDidAppear(animated) - + controller.genericView.isHidden = false controller.genericView.change(opacity: 1, animated: animated) let y = position == .above ? relativeView.frame.minY - controller.frame.height : relativeView.frame.maxY - controller.genericView.change(pos: NSMakePoint(0, y), animated: animated, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) - + if y != controller.genericView.frame.minY { + controller.genericView._change(pos: NSMakePoint(0, y), animated: animated, duration: 0.4, timingFunction: CAMediaTimingFunctionName.spring, forceAnimateIfHasAnimation: true) + } } - } else if let controller = self?.controller { - controller.viewWillDisappear(animated) - + } else if let controller = self?.controller, let relativeView = relativeView { + var controller:InputContextViewController? = controller + controller?.viewWillDisappear(animated) + controller?.markAsNeedShown = false if animated { - controller.genericView.change(pos: NSMakePoint(0, relativeView.frame.minY), animated: animated, removeOnCompletion: false, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak controller] completed in - - if completed { + controller?.genericView._change(pos: NSMakePoint(0, relativeView.frame.minY), animated: animated, removeOnCompletion: false, duration: 0.4, timingFunction: CAMediaTimingFunctionName.spring, forceAnimateIfHasAnimation: true, completion: { completed in + if controller?.markAsNeedShown == false { controller?.removeFromSuperview() controller?.genericView.removeAll() controller?.viewDidDisappear(animated) + controller?.genericView.cancelSelection() } - + controller = nil }) - controller.genericView.change(opacity: 0, animated: true) + controller?.genericView.change(opacity: 0, animated: true) } else { - controller.removeFromSuperview() - controller.viewDidDisappear(animated) + controller?.removeFromSuperview() + controller?.viewDidDisappear(animated) + controller?.genericView.cancelSelection() } } - })) + }).start()) } - func entries(for result:ChatPresentationInputQueryResult?, initialSize:NSSize, chatInteraction: ChatInteraction) -> Signal<[InputContextEntry],Void> { + func entries(for result:ChatPresentationInputQueryResult?, initialSize:NSSize, chatInteraction: ChatInteraction) -> Signal<[InputContextEntry], NoError> { if let result = result { - return Signal {(subscriber) in + return Signal { subscriber in var entries:[InputContextEntry] = [] switch result { case let .mentions(peers): + var mention:[PeerId: PeerId] = [:] for i in 0 ..< peers.count { - entries.append(.peer(peers[i],entries.count, Int64(arc4random()))) + if mention[peers[i].id] == nil { + entries.append(.peer(peers[i],entries.count, Int64(arc4random()))) + mention[peers[i].id] = peers[i].id + } } case let .contextRequestResult(_, result): - if let peer = chatInteraction.presentation.peer as? TelegramChannel { - if peer.inlineRestricted, let bannedRights = peer.bannedRights { - entries.append(.inlineRestricted(bannedRights.untilDate == .max ? nil : bannedRights.formattedUntilDate)) + if let peer = chatInteraction.presentation.peer { + if let text = permissionText(from: peer, for: .banSendInline) { + entries.append(.inlineRestricted(text)) break } } @@ -583,10 +892,12 @@ class InputContextHelper: NSObject { } case .media: - let mediaRows = makeMediaEnties(result.results, initialSize:NSMakeSize(initialSize.width, 100)) + let mediaRows = makeMediaEnties(result.results, isSavedGifs: false, initialSize:NSMakeSize(initialSize.width, 100)) for i in 0 ..< mediaRows.count { - entries.append(.contextMediaResult(result, mediaRows[i], Int64(arc4random()) | ((Int64(entries.count) << 40)))) + if !mediaRows[i].results.isEmpty { + entries.append(.contextMediaResult(result, mediaRows[i], Int64(arc4random()) | ((Int64(entries.count) << 40)))) + } } } @@ -597,10 +908,17 @@ class InputContextHelper: NSObject { entries.append(.command(commands[i], index, Int64(arc4random()) | ((Int64(commands.count) << 40)))) index += 1 } + case .hashtags(let hashtags): + var index:Int64 = 2000 + for i in 0 ..< hashtags.count { + entries.append(.hashtag(hashtags[i], index)) + index += 1 + } case let .stickers(stickers): - if let peer = chatInteraction.presentation.peer as? TelegramChannel { - if peer.stickersRestricted { + if let peer = chatInteraction.presentation.peer { + if let text = permissionText(from: peer, for: .banSendStickers) { + entries.append(.inlineRestricted(text)) break } } @@ -611,15 +929,34 @@ class InputContextHelper: NSObject { entries.append(.sticker(mediaRows[i], Int64(arc4random()) | ((Int64(entries.count) << 40)))) } - break - case let .emoji(clues): + case let .emoji(clues, firstWord): var index:Int32 = 0 - for clue in clues { - entries.append(.emoji(clue, index)) + if !clues.isEmpty { + entries.append(.emoji(clues, nil, firstWord, index)) + index += 1 + } + + case let .searchMessages(messages, suggestPeers, searchText): + var index: Int64 = 0 + + + let count:Int = min(max(6 - messages.0.count, 1), suggestPeers.count) + for i in 0 ..< count { + let peer = suggestPeers[i] + entries.append(.peer(peer, Int(index), arc4random64())) + index += 1 + } + + +// if suggestPeers.count > 1, messages.0.count > 0 { +// entries.append(.showPeers(Int(index), arc4random64())) +// index += 1 +// } + + for message in messages.0 { + entries.append(.message(index, message, searchText)) index += 1 } - default: - break } entries.sort(by: <) subscriber.putNext(entries) @@ -633,6 +970,7 @@ class InputContextHelper: NSObject { deinit { disposable.dispose() + loadMoreDisposable.dispose() } } diff --git a/Telegram-Mac/InputDataController.swift b/Telegram-Mac/InputDataController.swift new file mode 100644 index 0000000000..043dc80ab5 --- /dev/null +++ b/Telegram-Mac/InputDataController.swift @@ -0,0 +1,903 @@ +// +// InputDataController.swift +// Telegram +// +// Created by Mikhail Filimonov on 21/03/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import SwiftSignalKit + + +public class InputDataModalController : ModalViewController { + private let controller: InputDataController + private let _modalInteractions: ModalInteractions? + private let closeHandler: (@escaping()-> Void) -> Void + private let themeDisposable = MetaDisposable() + init(_ controller: InputDataController, modalInteractions: ModalInteractions? = nil, closeHandler: @escaping(@escaping()-> Void) -> Void = { $0() }, size: NSSize = NSMakeSize(380, 300)) { + self.controller = controller + self._modalInteractions = modalInteractions + self.controller._frameRect = NSMakeRect(0, 0, max(size.width, 330), size.height) + self.controller.prepareAllItems = true + self.closeHandler = closeHandler + super.init(frame: controller._frameRect) + } + + + var getHeaderColor: (()->NSColor)? = nil + public override var headerBackground: NSColor { + return getHeaderColor?() ?? super.headerBackground + } + var getHeaderBorderColor: (()->NSColor)? = nil + public override var headerBorderColor: NSColor { + return getHeaderBorderColor?() ?? super.headerBackground + } + var getModalTheme: (()->ModalViewController.Theme)? = nil + public override var modalTheme: ModalViewController.Theme { + return getModalTheme?() ?? super.modalTheme + } + + var isFullScreenImpl: (()->Bool)? = nil + + public override var isFullScreen: Bool { + return self.isFullScreenImpl?() ?? super.isFullScreen + } + var closableImpl: (()->Bool)? = nil + + public override var closable: Bool { + return closableImpl?() ?? super.closable + } + + public override func close(animationType: ModalAnimationCloseBehaviour = .common) { + closeHandler({ [weak self] in + self?.closeModal() + }) + } + public override var shouldCloseAllTheSameModals: Bool { + return false + } + + public override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + modal?.updateLocalizationAndTheme(theme: theme) + } + + public override var containerBackground: NSColor { + return controller.getBackgroundColor() + } + + private func closeModal() { + super.close() + } + + public override var modalHeader: (left: ModalHeaderData?, center: ModalHeaderData?, right: ModalHeaderData?)? { + if controller.defaultBarTitle.isEmpty { + return nil + } + return (left: self.controller.leftModalHeader, center: self.controller.centerModalHeader ?? ModalHeaderData(title: controller.defaultBarTitle), right: self.controller.rightModalHeader) + } + + + public override var modalInteractions: ModalInteractions? { + return _modalInteractions + } + + public override var handleEvents: Bool { + return true + } + + public override func becomeFirstResponder() -> Bool? { + return controller.becomeFirstResponder() + } + + public override func firstResponder() -> NSResponder? { + return controller.firstResponder() + } + + public override func returnKeyAction() -> KeyHandlerResult { + return controller.returnKeyAction() + } + + public override var haveNextResponder: Bool { + return controller.haveNextResponder + } + + public override func nextResponder() -> NSResponder? { + return controller.nextResponder() + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + controller.viewWillAppear(animated) + } + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + controller.viewDidAppear(animated) + } + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + controller.viewWillDisappear(animated) + } + public override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + controller.viewDidDisappear(animated) + controller.didRemovedFromStack() + } + + + @objc private func rootControllerFrameChanged(_ notification:Notification) { + viewDidResized(frame.size) + } + + public override func viewDidResized(_ size: NSSize) { + super.viewDidResized(size) + + updateSize(true) + } + + var dynamicSizeImpl:(()->Bool)? = nil + + override open var dynamicSize: Bool { + return self.dynamicSizeImpl?() ?? true + } + + override open func measure(size: NSSize) { + self.modal?.resize(with:NSMakeSize(max(340, min(self.controller._frameRect.width, max(size.width, 350))), min(size.height - 150, controller.tableView.listHeight)), animated: false) + } + + public func updateSize(_ animated: Bool) { + if let contentSize = self.modal?.window.contentView?.frame.size { + self.modal?.resize(with:NSMakeSize(max(340, min(self.controller._frameRect.width, max(contentSize.width, 350))), min(contentSize.height - 150, controller.tableView.listHeight)), animated: animated) + } + } + + public override func viewClass() -> AnyClass { + fatalError() + } + + public override func loadView() { + viewDidLoad() + } + + public override var view: NSView { + if !controller.isLoaded() { + loadView() + } + return controller.view + } + + + public override func viewDidLoad() { + controller.loadView() + super.viewDidLoad() + ready.set(controller.ready.get()) + + themeDisposable.set(appearanceSignal.start(next: { [weak self] appearance in + self?.modal?.updateLocalizationAndTheme(theme: appearance.presentation) + self?.controller.updateLocalizationAndTheme(theme: appearance.presentation) + })) + + controller.modalTransitionHandler = { [weak self] animated in + if self?.dynamicSize == true { + self?.updateSize(animated) + } + } + } + deinit { + themeDisposable.dispose() + } +} + + + + +final class InputDataArguments { + let select:((InputDataIdentifier, InputDataValue))->Void + let dataUpdated:()->Void + init(select: @escaping((InputDataIdentifier, InputDataValue))->Void, dataUpdated:@escaping()->Void) { + self.select = select + self.dataUpdated = dataUpdated + } +} + +private let queue: Queue = Queue(name: "InputDataItemsQueue", qos: DispatchQoS.background) + +func prepareInputDataTransition(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], animated: Bool, searchState: TableSearchViewState?, initialSize:NSSize, arguments: InputDataArguments, onMainQueue: Bool, animateEverything: Bool = false) -> Signal { + return Signal { subscriber in + + func makeItem(_ entry: InputDataEntry) -> TableRowItem { + return entry.item(arguments: arguments, initialSize: initialSize) + } + + let applyQueue = onMainQueue ? .mainQueue() : prepareQueue + + let cancelled: Atomic = Atomic(value: false) + + if Thread.isMainThread && left.isEmpty { + var initialIndex:Int = 0 + var height:CGFloat = 0 + var firstInsertion:[(Int, TableRowItem)] = [] + let entries = Array(right) + + let index:Int = 0 + + for i in index ..< entries.count { + let item = makeItem(entries[i].entry) + height += item.height + firstInsertion.append((i, item)) + if initialSize.height < height { + break + } + } + initialIndex = firstInsertion.count + subscriber.putNext(TableUpdateTransition(deleted: [], inserted: firstInsertion, updated: [], state: .none(nil), searchState: searchState)) + + if !cancelled.with({ $0 }) { + + var insertions:[(Int, TableRowItem)] = [] + let updates:[(Int, TableRowItem)] = [] + + for i in initialIndex ..< entries.count { + let item:TableRowItem + item = makeItem(entries[i].entry) + insertions.append((i, item)) + if cancelled.with({ $0 }) { + break + } + } + if !cancelled.with({ $0 }) { + applyQueue.async { + subscriber.putNext(TableUpdateTransition(deleted: [], inserted: insertions, updated: updates, state: .none(nil), animateVisibleOnly: !animateEverything, searchState: searchState)) + subscriber.putCompletion() + } + } + } + } else { + let (deleted,inserted,updated) = proccessEntriesWithoutReverse(left, right: right, { entry -> TableRowItem in + if !cancelled.with({ $0 }) { + return makeItem(entry.entry) + } else { + return TableRowItem(.zero) + } + }) + if !cancelled.with({ $0 }) { + applyQueue.async { + subscriber.putNext(TableUpdateTransition(deleted: deleted, inserted: inserted, updated:updated, animated:animated, state: .none(nil), animateVisibleOnly: !animateEverything, searchState: searchState)) + subscriber.putCompletion() + } + } + } + + return ActionDisposable { + _ = cancelled.swap(true) + } + } |> runOn(onMainQueue ? .mainQueue() : prepareQueue) + +} + + +enum InputDataReturnResult { + case `default` + case nextResponder + case invokeEvent + case nothing +} + +enum InputDataDeleteResult { + case `default` + case invoked +} + +struct InputDataSignalValue { + let entries: [InputDataEntry] + let animated: Bool + let searchState: TableSearchViewState? + init(entries: [InputDataEntry], animated: Bool = true, searchState: TableSearchViewState? = nil) { + self.entries = entries + self.animated = animated + self.searchState = searchState + } +} + +final class InputDataView : BackgroundView { + let tableView = TableView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(tableView) + tableView.frame = bounds + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + tableView.updateLocalizationAndTheme(theme: theme) + } + + override func setFrameSize(_ newSize: NSSize) { + super.setFrameSize(newSize) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func layout() { + super.layout() + tableView.frame = bounds + } +} + +class InputDataController: GenericViewController { + + fileprivate var modalTransitionHandler:((Bool)->Void)? = nil + fileprivate var prepareAllItems: Bool = false + + private let values: Promise = Promise() + private let disposable = MetaDisposable() + private let appearanceDisposablet = MetaDisposable() + private let title: String + var validateData:([InputDataIdentifier : InputDataValue]) -> InputDataValidation + var afterDisappear: ()->Void + var updateDatas:([InputDataIdentifier : InputDataValue]) -> InputDataValidation + var didLoaded:(InputDataController, [InputDataIdentifier : InputDataValue]) -> Void + private let _removeAfterDisappear: Bool + private let hasDone: Bool + var updateDoneValue:([InputDataIdentifier : InputDataValue])->((InputDoneValue)->Void)->Void + var customRightButton:((ViewController)->BarView?)? + var updateRightBarView:((BarView)->Void)? + var afterTransaction: (InputDataController)->Void + var backInvocation: ([InputDataIdentifier : InputDataValue], @escaping(Bool)->Void)->Void + var returnKeyInvocation:(InputDataIdentifier?, NSEvent) -> InputDataReturnResult + var deleteKeyInvocation:(InputDataIdentifier?) -> InputDataDeleteResult + var tabKeyInvocation:(InputDataIdentifier?) -> InputDataDeleteResult + var rightModalHeader: ModalHeaderData? = nil + var leftModalHeader: ModalHeaderData? = nil + var centerModalHeader: ModalHeaderData? = nil + var keyWindowUpdate:(Bool, InputDataController) -> Void = { _, _ in } + var hasBackSwipe:()->Bool = { return true } + private let searchKeyInvocation:() -> InputDataDeleteResult + var getBackgroundColor: ()->NSColor + let identifier: String + var ignoreRightBarHandler: Bool = false + + var contextOject: Any? + var didAppear: ((InputDataController)->Void)? + + var _abolishWhenNavigationSame: Bool = false + + var getTitle:(()->String)? = nil + var getStatus:(()->String?)? = nil + + init(dataSignal:Signal, title: String, validateData:@escaping([InputDataIdentifier : InputDataValue]) -> InputDataValidation = {_ in return .fail(.none)}, updateDatas: @escaping([InputDataIdentifier : InputDataValue]) -> InputDataValidation = {_ in return .fail(.none)}, afterDisappear: @escaping() -> Void = {}, didLoaded: @escaping(InputDataController, [InputDataIdentifier : InputDataValue]) -> Void = { _, _ in}, updateDoneValue:@escaping([InputDataIdentifier : InputDataValue])->((InputDoneValue)->Void)->Void = { _ in return {_ in}}, removeAfterDisappear: Bool = true, hasDone: Bool = true, identifier: String = "", customRightButton: ((ViewController)->BarView?)? = nil, afterTransaction: @escaping(InputDataController)->Void = { _ in }, backInvocation: @escaping([InputDataIdentifier : InputDataValue], @escaping(Bool)->Void)->Void = { $1(true) }, returnKeyInvocation: @escaping(InputDataIdentifier?, NSEvent) -> InputDataReturnResult = {_, _ in return .default }, deleteKeyInvocation: @escaping(InputDataIdentifier?) -> InputDataDeleteResult = {_ in return .default }, tabKeyInvocation: @escaping(InputDataIdentifier?) -> InputDataDeleteResult = {_ in return .default }, searchKeyInvocation: @escaping() -> InputDataDeleteResult = { return .default }, getBackgroundColor: @escaping()->NSColor = { theme.colors.listBackground }) { + self.title = title + self.validateData = validateData + self.afterDisappear = afterDisappear + self.updateDatas = updateDatas + self.didLoaded = didLoaded + self.identifier = identifier + self._removeAfterDisappear = removeAfterDisappear + self.hasDone = hasDone + self.updateDoneValue = updateDoneValue + self.customRightButton = customRightButton + self.afterTransaction = afterTransaction + self.backInvocation = backInvocation + self.returnKeyInvocation = returnKeyInvocation + self.deleteKeyInvocation = deleteKeyInvocation + self.tabKeyInvocation = tabKeyInvocation + self.searchKeyInvocation = searchKeyInvocation + self.getBackgroundColor = getBackgroundColor + super.init() + values.set(dataSignal) + } + + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + + self.genericView.updateLocalizationAndTheme(theme: theme) + requestUpdateBackBar() + requestUpdateCenterBar() + requestUpdateRightBar() + } + + override func requestUpdateRightBar() { + super.requestUpdateRightBar() + self.updateRightBarView?(self.rightBarView) + } + + override var defaultBarTitle: String { + return getTitle?() ?? title + } + override var defaultBarStatus: String? { + return getStatus?() + } + + override func getRightBarViewOnce() -> BarView { + return customRightButton?(self) ?? (hasDone ? TextButtonBarView(controller: self, text: L10n.navigationDone, style: navigationButtonStyle, alignment:.Right) : super.getRightBarViewOnce()) + } + + private var doneView: TextButtonBarView { + return rightBarView as! TextButtonBarView + } + + override var responderPriority: HandlerPriority { + return .modal + } + var tableView: TableView { + return self.genericView.tableView + } + + func fetchData() -> [InputDataIdentifier : InputDataValue] { + var values:[InputDataIdentifier : InputDataValue] = [:] + tableView.enumerateItems { item -> Bool in + if let identifier = (item.stableId.base as? InputDataEntryId)?.identifier { + if let item = item as? InputDataRowDataValue { + values[identifier] = item.value + } + } + return true + } + return values + } + + private func findItem(for identifier: InputDataIdentifier) -> TableRowItem? { + var item: TableRowItem? + tableView.enumerateItems { current -> Bool in + if let stableId = current.stableId.base as? InputDataEntryId { + if stableId.identifier == identifier { + item = current + } + } + return item == nil + } + return item + } + + func makeFirstResponderIfPossible(for identifier: InputDataIdentifier, focusIdentifier: InputDataIdentifier? = nil, scrollDown: Bool = false, scrollIfNeeded: Bool = true) { + if let item = findItem(for: identifier) { + _ = window?.makeFirstResponder(findItem(for: identifier)?.view?.firstResponder) + + if let focusIdentifier = focusIdentifier { + if let item = findItem(for: focusIdentifier) { + tableView.scroll(to: .center(id: item.stableId, innerId: nil, animated: true, focus: .init(focus: false), inset: 0), inset: NSEdgeInsets(), true) + } + } else if scrollIfNeeded { + if !scrollDown { + tableView.scroll(to: .center(id: item.stableId, innerId: nil, animated: true, focus: .init(focus: false), inset: 0), inset: NSEdgeInsets(), true) + } else { + tableView.scroll(to: .down(true)) + } + } + } + } + + func proccessValidation(_ validation: InputDataValidation) { + var scrollFirstItem: TableRowItem? = nil + switch validation { + case let .fail(fail): + switch fail { + case let .alert(text): + alert(for: mainWindow, info: text) + case let .fields(fields): + for (identifier, action) in fields { + switch action { + case .shake: + let item = findItem(for: identifier) + item?.view?.shakeView() + if scrollFirstItem == nil { + var invoked: Bool = false + scrollFirstItem = item + if let item = item, !invoked { + tableView.scroll(to: .top(id: item.stableId, innerId: nil, animated: true, focus: .init(focus: false), inset: 0), inset: NSEdgeInsets(), timingFunction: .linear, true) + invoked = true + } + } + case let .shakeWithData(data): + let item = findItem(for: identifier) + item?.view?.shakeViewWithData(data) + } + } + case let .doSomething(next): + next { [weak self] validation in + self?.proccessValidation(validation) + } + default: + //TODO IF NEEDED + break + } + case let .success(behaviour): + switch behaviour { + case .navigationBack: + navigationController?.back() + case .navigationBackWithPushAnimation: + navigationController?.back(animationStyle: .push) + case let .custom(action): + action() + } + case .none: + break + } + } + + func validateInputValues() { + self.proccessValidation(self.validateData(self.fetchData())) + } + + private func validateInput(data: [InputDataIdentifier : InputDataValue]) { + proccessValidation(self.validateData(data)) + } + + override func viewDidLoad() { + super.viewDidLoad() + + genericView.tableView.getBackgroundColor = self.getBackgroundColor + + + appearanceDisposablet.set(appearanceSignal.start(next: { [weak self] _ in + self?.updateLocalizationAndTheme(theme: theme) + })) + + let arguments = InputDataArguments(select: { [weak self] (identifier, value) in + guard let `self` = self else {return} + self.validateInput(data: [identifier : value]) + }, dataUpdated: { [weak self] in + guard let `self` = self else {return} + self.proccessValidation(self.updateDatas(self.fetchData())) + }) + + self.rightBarView.set(handler:{ [weak self] _ in + guard let `self` = self else {return} + if !self.ignoreRightBarHandler { + self.validateInput(data: self.fetchData()) + } + }, for: .Click) + + let previous: Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) + let initialSize = self.atomicSize + + let onMainQueue: Atomic = Atomic(value: !prepareAllItems) + + let signal: Signal = combineLatest(queue: .mainQueue(), appearanceSignal, values.get()) |> mapToQueue { appearance, state in + let entries = state.entries.map({AppearanceWrapperEntry(entry: $0, appearance: appearance)}) + return prepareInputDataTransition(left: previous.swap(entries), right: entries, animated: state.animated, searchState: state.searchState, initialSize: initialSize.modify{$0}, arguments: arguments, onMainQueue: onMainQueue.swap(false)) + } |> deliverOnMainQueue |> afterDisposed { + previous.swap([]) + } + + disposable.set(signal.start(next: { [weak self] transition in + guard let `self` = self else {return} + self.tableView.merge(with: transition) + + + let result = self.updateDoneValue(self.fetchData()) + result { [weak self] value in + guard let `self` = self else {return} + switch value { + case let .disabled(text): + self.doneView.isHidden = false + self.doneView.isLoading = false + self.doneView.isEnabled = false + self.doneView.set(text: text, for: .Normal) + case let .enabled(text): + self.doneView.isHidden = false + self.doneView.isLoading = false + self.doneView.isEnabled = true + self.doneView.set(text: text, for: .Normal) + case .loading: + self.doneView.isHidden = false + self.doneView.isLoading = true + case .invisible: + self.doneView.isHidden = true + } + + } + + self.afterTransaction(self) + self.modalTransitionHandler?(transition.animated) + + let wasReady: Bool = self.didSetReady + self.readyOnce() + if !wasReady { + self.didLoaded(self, self.fetchData()) + } + })) + } + + override func returnKeyAction() -> KeyHandlerResult { + if let event = NSApp.currentEvent { + switch returnKeyInvocation(self.currentFirstResponderIdentifier, event) { + case .default: + self.validateInput(data: self.fetchData()) + return .invoked + case .nextResponder: + _ = window?.makeFirstResponder(self.nextResponder()) + return .invoked + case .nothing: + return .invoked + case .invokeEvent: + return .invokeNext + } + } + return .invokeNext + } + + override func becomeFirstResponder() -> Bool? { + return true + } + + override var canBecomeResponder: Bool { + return true + } + + override func didRemovedFromStack() { + super.didRemovedFromStack() + afterDisappear() + } + private var firstTake: Bool = true + + override func firstResponder() -> NSResponder? { + + let responder = window?.firstResponder as? NSView + + var responderInController: Bool = false + var superview = responder?.superview + while superview != nil { + if superview == self.genericView { + responderInController = true + break + } else { + superview = superview?.superview + } + } + + if self.window?.firstResponder == self.window || self.window?.firstResponder == tableView.documentView || !responderInController { + var first: NSResponder? = nil + tableView.enumerateViews { view -> Bool in + first = view.firstResponder + if first != nil, self.firstTake { + if let item = view.item as? InputDataRowDataValue { + switch item.value { + case let .string(value): + let value = value ?? "" + if !value.isEmpty { + return true + } + default: + break + } + } + } + return first == nil + } + self.firstTake = false + return first + } + return window?.firstResponder + } + + override func backSettings() -> (String, CGImage?) { + + return super.backSettings() + } + + override var enableBack: Bool { + return true + } + // private var canInvokeBack: Bool = false + override func invokeNavigationBack() -> Bool { + return true + } + + override func executeReturn() { + backInvocation(fetchData(), { [weak self] result in + if result { + self?.navigationController?.back() + } + }) + } + + override func getLeftBarViewOnce() -> BarView { + if let navigation = navigationController { + return navigation.empty === self ? BarView(controller: self) : super.getLeftBarViewOnce() + } + return BarView(controller: self) + } + + override var haveNextResponder: Bool { + return true + } + + override var abolishWhenNavigationSame: Bool { + return _abolishWhenNavigationSame + } + + override func backKeyAction() -> KeyHandlerResult { + return .invokeNext + } + + override func viewDidAppear(_ animated: Bool) { + _ = self.window?.makeFirstResponder(nil) + super.viewDidAppear(animated) + + didAppear?(self) + + window?.set(mouseHandler: { [weak self] event -> KeyHandlerResult in + guard let `self` = self else {return .rejected} + + let index = self.tableView.row(at: self.tableView.documentView!.convert(event.locationInWindow, from: nil)) + + if index > -1, let view = self.tableView.item(at: index).view { + if view.mouseInsideField { + if self.window?.firstResponder != view.firstResponder { + _ = self.window?.makeFirstResponder(view.firstResponder) + return .invoked + } + } + } + + return .invokeNext + }, with: self, for: .leftMouseUp, priority: self.responderPriority) + + + window?.set(handler: { [weak self] _ in + guard let `self` = self else {return .rejected} + + switch self.deleteKeyInvocation(self.currentFirstResponderIdentifier) { + case .default: + return .rejected + case .invoked: + return .invoked + } + + }, with: self, for: .Delete, priority: self.responderPriority, modifierFlags: nil) + + + window?.set(handler: { [weak self] _ in + guard let `self` = self else {return .rejected} + + switch self.searchKeyInvocation() { + case .default: + return .rejected + case .invoked: + return .invoked + } + + }, with: self, for: .F, priority: self.responderPriority, modifierFlags: nil) + + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in + let view = self?.findReponsderView as? InputDataRowView + + view?.makeBold() + return .invoked + }, with: self, for: .B, priority: self.responderPriority, modifierFlags: [.command]) + + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in + let view = self?.findReponsderView as? InputDataRowView + view?.makeUrl() + return .invoked + }, with: self, for: .U, priority: self.responderPriority, modifierFlags: [.command]) + + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in + let view = self?.findReponsderView as? InputDataRowView + view?.makeItalic() + return .invoked + }, with: self, for: .I, priority: self.responderPriority, modifierFlags: [.command]) + + + + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in + let view = self?.findReponsderView as? InputDataRowView + view?.makeMonospace() + return .invoked + }, with: self, for: .K, priority: responderPriority, modifierFlags: [.command, .shift]) + + } + + + + var findReponsderView: TableRowView? { + if let view = self.firstResponder() as? NSView { + var superview: NSView? = view + while superview != nil { + if let current = superview as? TableRowView { + return current + } else { + superview = superview?.superview + } + } + } + return nil + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + window?.removeAllHandlers(for: self) + } + + + var currentFirstResponderIdentifier: InputDataIdentifier? { + var identifier: InputDataIdentifier? = nil + + tableView.enumerateViews { view -> Bool in + if view.hasFirstResponder() { + if view.firstResponder == view.window?.firstResponder { + identifier = (view.item?.stableId.base as? InputDataEntryId)?.identifier + } + + } + return identifier == nil + } + return identifier + } + + override var supportSwipes: Bool { + let horizontal = HackUtils.findElements(byClass: "TGUIKit.HorizontalTableView", in: genericView.tableView)?.first as? HorizontalTableView + if let horizontal = horizontal { + return !horizontal._mouseInside() + } + return self.hasBackSwipe() + } + + override func nextResponder() -> NSResponder? { + var next: NSResponder? + let current = self.window?.firstResponder + + + + var selectNext: Bool = false + + var first: NSResponder? = nil + + + tableView.enumerateViews { view -> Bool in + if view.hasFirstResponder() { + first = view.firstResponder + } + return first == nil + } + + tableView.enumerateViews { view -> Bool in + if view.hasFirstResponder() { + if selectNext { + next = view.firstResponder + } else if view.firstResponder == current || view.firstResponder == (current as? NSView)?.superview?.superview { + if let nextInner = view.nextResponder() { + next = nextInner + return false + } + selectNext = true + return true + } + } + return next == nil + } + + + return next ?? first + } + + override var removeAfterDisapper: Bool { + return _removeAfterDisappear + } + + override func windowDidBecomeKey() { + self.keyWindowUpdate(true, self) + } + + override func windowDidResignKey() { + self.keyWindowUpdate(false, self) + } + + override func escapeKeyAction() -> KeyHandlerResult { + self.executeReturn() + return .invoked + } + + deinit { + disposable.dispose() + appearanceDisposablet.dispose() + onDeinit?() + } + +} diff --git a/Telegram-Mac/InputDataControllerEntries.swift b/Telegram-Mac/InputDataControllerEntries.swift new file mode 100644 index 0000000000..d8ef49b7bf --- /dev/null +++ b/Telegram-Mac/InputDataControllerEntries.swift @@ -0,0 +1,703 @@ +// +// InputDataControllerEntries.swift +// Telegram +// +// Created by Mikhail Filimonov on 21/03/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import SwiftSignalKit + +enum InputDataEntryId : Hashable { + case desc(Int32) + case input(InputDataIdentifier) + case general(InputDataIdentifier) + case selector(InputDataIdentifier) + case dataSelector(InputDataIdentifier) + case dateSelector(InputDataIdentifier) + case custom(InputDataIdentifier) + case search(InputDataIdentifier) + case loading + case sectionId(Int32) + var hashValue: Int { + return 0 + } + + var identifier: InputDataIdentifier? { + switch self { + case let .input(identifier), let .selector(identifier), let .dataSelector(identifier), let .dateSelector(identifier), let .general(identifier), let .custom(identifier), let .search(identifier): + return identifier + default: + return nil + } + } +} + + +enum InputDataInputMode : Equatable { + case plain + case secure +} + + + +public protocol _HasCustomInputDataEquatableRepresentation { + func _toCustomInputDataEquatable() -> InputDataEquatable? +} + +internal protocol _InputDataEquatableBox { + var _typeID: ObjectIdentifier { get } + func _unbox() -> T? + + func _isEqual(to: _InputDataEquatableBox) -> Bool? + + var _base: Any { get } + func _downCastConditional(into result: UnsafeMutablePointer) -> Bool +} + +internal struct _ConcreteEquatableBox : _InputDataEquatableBox { + internal var _baseEquatable: Base + + internal init(_ base: Base) { + self._baseEquatable = base + } + + + internal var _typeID: ObjectIdentifier { + return ObjectIdentifier(type(of: self)) + } + + internal func _unbox() -> T? { + return (self as _InputDataEquatableBox as? _ConcreteEquatableBox)?._baseEquatable + } + + internal func _isEqual(to rhs: _InputDataEquatableBox) -> Bool? { + if let rhs: Base = rhs._unbox() { + return _baseEquatable == rhs + } + return nil + } + + internal var _base: Any { + return _baseEquatable + } + + internal + func _downCastConditional(into result: UnsafeMutablePointer) -> Bool { + guard let value = _baseEquatable as? T else { return false } + result.initialize(to: value) + return true + } +} + + +struct InputDataComparableIndex : Comparable { + let data: Any + let compare:(Any, Any)->Bool + let equatable:(Any, Any)->Bool + + static func <(lhs: InputDataComparableIndex, rhs: InputDataComparableIndex) -> Bool { + return lhs.compare(lhs.data, rhs.data) + } + static func ==(lhs: InputDataComparableIndex, rhs: InputDataComparableIndex) -> Bool { + return lhs.equatable(lhs.data, rhs.data) + } +} + +public struct InputDataEquatable { + internal var _box: _InputDataEquatableBox + internal var _usedCustomRepresentation: Bool + + + public init(_ base: H) { + if let customRepresentation = + (base as? _HasCustomInputDataEquatableRepresentation)?._toCustomInputDataEquatable() { + self = customRepresentation + self._usedCustomRepresentation = true + return + } + + self._box = _ConcreteEquatableBox(base) + self._usedCustomRepresentation = false + } + + internal init(_usingDefaultRepresentationOf base: H) { + self._box = _ConcreteEquatableBox(base) + self._usedCustomRepresentation = false + } + + public var base: Any { + return _box._base + } + internal + func _downCastConditional(into result: UnsafeMutablePointer) -> Bool { + // Attempt the downcast. + if _box._downCastConditional(into: result) { return true } + + + + return false + } +} + +extension InputDataEquatable : Equatable { + public static func == (lhs: InputDataEquatable, rhs: InputDataEquatable) -> Bool { + if let result = lhs._box._isEqual(to: rhs._box) { return result } + + return false + } +} + +extension InputDataEquatable : CustomStringConvertible { + public var description: String { + return String(describing: base) + } +} + +extension InputDataEquatable : CustomDebugStringConvertible { + public var debugDescription: String { + return "InputDataEquatable(" + String(reflecting: base) + ")" + } +} + +extension InputDataEquatable : CustomReflectable { + public var customMirror: Mirror { + return Mirror( + self, + children: ["value": base]) + } +} + + + + +public // COMPILER_INTRINSIC +func _convertToInputDataEquatable(_ value: H) -> InputDataEquatable { + return InputDataEquatable(value) +} + +internal func _convertToInputDataEquatableIndirect( + _ value: H, + _ target: UnsafeMutablePointer + ) { + target.initialize(to: InputDataEquatable(value)) +} + +internal func _InputDataEquatableDownCastConditionalIndirect( + _ value: UnsafePointer, + _ target: UnsafeMutablePointer + ) -> Bool { + return value.pointee._downCastConditional(into: target) +} + + +struct InputDataInputPlaceholder : Equatable { + let placeholder: String? + let drawBorderAfterPlaceholder: Bool + let icon: CGImage? + let action: (()-> Void)? + let hasLimitationText: Bool + let insets: NSEdgeInsets + init(_ placeholder: String? = nil, icon: CGImage? = nil, drawBorderAfterPlaceholder: Bool = false, hasLimitationText: Bool = false, insets: NSEdgeInsets = NSEdgeInsets(), action: (()-> Void)? = nil) { + self.drawBorderAfterPlaceholder = drawBorderAfterPlaceholder + self.hasLimitationText = hasLimitationText + self.placeholder = placeholder + self.icon = icon + self.action = action + self.insets = insets + } + + static func ==(lhs: InputDataInputPlaceholder, rhs: InputDataInputPlaceholder) -> Bool { + return lhs.placeholder == rhs.placeholder && lhs.icon === rhs.icon && lhs.drawBorderAfterPlaceholder == rhs.drawBorderAfterPlaceholder && lhs.insets == rhs.insets + } +} + + +final class InputDataGeneralData : Equatable { + + + + + let name: String + let color: NSColor + let icon: CGImage? + let type: GeneralInteractedType + let viewType: GeneralViewType + let description: String? + let action: (()->Void)? + let disabledAction:(()->Void)? + let enabled: Bool + let justUpdate: Int64? + let menuItems:(()->[ContextMenuItem])? + let theme: GeneralRowItem.Theme? + let disableBorder: Bool + init(name: String, color: NSColor, icon: CGImage? = nil, type: GeneralInteractedType = .none, viewType: GeneralViewType = .legacy, enabled: Bool = true, description: String? = nil, justUpdate: Int64? = nil, action: (()->Void)? = nil, disabledAction: (()->Void)? = nil, menuItems:(()->[ContextMenuItem])? = nil, theme: GeneralRowItem.Theme? = nil, disableBorder: Bool = false) { + self.name = name + self.color = color + self.icon = icon + self.type = type + self.viewType = viewType + self.description = description + self.action = action + self.enabled = enabled + self.justUpdate = justUpdate + self.disabledAction = disabledAction + self.menuItems = menuItems + self.theme = theme + self.disableBorder = disableBorder + } + + static func ==(lhs: InputDataGeneralData, rhs: InputDataGeneralData) -> Bool { + return lhs.name == rhs.name && lhs.icon === rhs.icon && lhs.color.hexString == rhs.color.hexString && lhs.type == rhs.type && lhs.description == rhs.description && lhs.viewType == rhs.viewType && lhs.enabled == rhs.enabled && lhs.justUpdate == rhs.justUpdate && lhs.theme == rhs.theme && lhs.disableBorder == rhs.disableBorder + } +} + +final class InputDataTextInsertAnimatedViewData : NSObject { + let context: AccountContext + let file: TelegramMediaFile + init(context: AccountContext, file: TelegramMediaFile) { + self.context = context + self.file = file + } + static func == (lhs: InputDataTextInsertAnimatedViewData, rhs: InputDataTextInsertAnimatedViewData) -> Bool { + return lhs.file == rhs.file + } + static var attributeKey: NSAttributedString.Key { + return NSAttributedString.Key("InputDataTextInsertAnimatedDataKey") + } +} + +struct InputDataGeneralTextRightData : Equatable { + let isLoading: Bool + let text: NSAttributedString? + init(isLoading: Bool, text: NSAttributedString?) { + self.isLoading = isLoading + self.text = text + } +} + +final class InputDataGeneralTextData : Equatable { + let color: NSColor + let detectBold: Bool + let viewType: GeneralViewType + let rightItem: InputDataGeneralTextRightData + let fontSize: CGFloat? + init(color: NSColor = theme.colors.listGrayText, detectBold: Bool = true, viewType: GeneralViewType = .legacy, rightItem: InputDataGeneralTextRightData = InputDataGeneralTextRightData(isLoading: false, text: nil), fontSize: CGFloat? = nil) { + self.color = color + self.detectBold = detectBold + self.viewType = viewType + self.rightItem = rightItem + self.fontSize = fontSize + } + static func ==(lhs: InputDataGeneralTextData, rhs: InputDataGeneralTextData) -> Bool { + return lhs.color == rhs.color && lhs.detectBold == rhs.detectBold && lhs.viewType == rhs.viewType && lhs.rightItem == rhs.rightItem && lhs.fontSize == rhs.fontSize + } +} + +final class InputDataRowData : Equatable { + let viewType: GeneralViewType + let rightItem: InputDataRightItem? + let defaultText: String? + let pasteFilter:((String)->(Bool, String))? + let maxBlockWidth: CGFloat? + let canMakeTransformations: Bool + let customTheme: GeneralRowItem.Theme? + init(viewType: GeneralViewType = .legacy, rightItem: InputDataRightItem? = nil, defaultText: String? = nil, maxBlockWidth: CGFloat? = nil, canMakeTransformations: Bool = false, pasteFilter:((String)->(Bool, String))? = nil, customTheme: GeneralRowItem.Theme? = nil) { + self.viewType = viewType + self.rightItem = rightItem + self.defaultText = defaultText + self.pasteFilter = pasteFilter + self.maxBlockWidth = maxBlockWidth + self.canMakeTransformations = canMakeTransformations + self.customTheme = customTheme + } + static func ==(lhs: InputDataRowData, rhs: InputDataRowData) -> Bool { + return lhs.viewType == rhs.viewType && lhs.rightItem == rhs.rightItem && lhs.defaultText == rhs.defaultText && lhs.maxBlockWidth == rhs.maxBlockWidth && lhs.canMakeTransformations == rhs.canMakeTransformations && lhs.customTheme == rhs.customTheme + } +} + +enum InputDataSectionType : Equatable { + case normal + case legacy + case custom(CGFloat) + case customModern(CGFloat) + var height: CGFloat { + switch self { + case .normal: + return 30 + case .legacy: + return 20 + case let .custom(height): + return height + case let .customModern(height): + return height + } + } +} + +enum InputDataEntry : Identifiable, Comparable { + case desc(sectionId: Int32, index: Int32, text: GeneralRowTextType, data: InputDataGeneralTextData) + case input(sectionId: Int32, index: Int32, value: InputDataValue, error: InputDataValueError?, identifier: InputDataIdentifier, mode: InputDataInputMode, data: InputDataRowData, placeholder: InputDataInputPlaceholder?, inputPlaceholder: String, filter:(String)->String, limit: Int32) + case general(sectionId: Int32, index: Int32, value: InputDataValue, error: InputDataValueError?, identifier: InputDataIdentifier, data: InputDataGeneralData) + case dateSelector(sectionId: Int32, index: Int32, value: InputDataValue, error: InputDataValueError?, identifier: InputDataIdentifier, placeholder: String) + case selector(sectionId: Int32, index: Int32, value: InputDataValue, error: InputDataValueError?, identifier: InputDataIdentifier, placeholder: String, viewType: GeneralViewType, values:[ValuesSelectorValue]) + case dataSelector(sectionId: Int32, index: Int32, value: InputDataValue, error: InputDataValueError?, identifier: InputDataIdentifier, placeholder: String, description: String?, icon: CGImage?, action:()->Void) + case custom(sectionId: Int32, index: Int32, value: InputDataValue, identifier: InputDataIdentifier, equatable: InputDataEquatable?, comparable: InputDataComparableIndex?, item:(NSSize, InputDataEntryId)->TableRowItem) + case search(sectionId: Int32, index: Int32, value: InputDataValue, identifier: InputDataIdentifier, update:(SearchState)->Void) + case loading + case sectionId(Int32, type: InputDataSectionType) + + var comparable: InputDataComparableIndex? { + switch self { + case let .custom(_, _, _, _, _, comparable, _): + return comparable + default: + return nil + } + } + + var stableId: InputDataEntryId { + switch self { + case let .desc(_, index, _, _): + return .desc(index) + case let .input(_, _, _, _, identifier, _, _, _, _, _, _): + return .input(identifier) + case let .general(_, _, _, _, identifier, _): + return .general(identifier) + case let .selector(_, _, _, _, identifier, _, _, _): + return .selector(identifier) + case let .dataSelector(_, _, _, _, identifier, _, _, _, _): + return .dataSelector(identifier) + case let .dateSelector(_, _, _, _, identifier, _): + return .dateSelector(identifier) + case let .custom(_, _, _, identifier, _, _, _): + return .custom(identifier) + case let .search(_, _, _, identifier, _): + return .custom(identifier) + case let .sectionId(index, _): + return .sectionId(index) + case .loading: + return .loading + } + } + + var stableIndex: Int32 { + switch self { + case let .desc(_, index, _, _): + return index + case let .input(_, index, _, _, _, _, _, _, _, _, _): + return index + case let .general(_, index, _, _, _, _): + return index + case let .selector(_, index, _, _, _, _, _, _): + return index + case let .dateSelector(_, index, _, _, _, _): + return index + case let .dataSelector(_, index, _, _, _, _, _, _, _): + return index + case let .custom(_, index, _, _, _, _, _): + return index + case let .search(_, index, _, _, _): + return index + case .loading: + return 0 + case .sectionId: + fatalError() + } + } + + var sectionIndex: Int32 { + switch self { + case let .desc(index, _, _, _): + return index + case let .input(index, _, _, _, _, _, _, _, _, _, _): + return index + case let .selector(index, _, _, _, _, _, _, _): + return index + case let .general(index, _, _, _, _, _): + return index + case let .dateSelector(index, _, _, _, _, _): + return index + case let .dataSelector(index, _, _, _, _, _, _, _, _): + return index + case let .custom(index, _, _, _, _, _, _): + return index + case let .search(index, _, _, _, _): + return index + case .loading: + return 0 + case .sectionId: + fatalError() + } + } + + var index: Int32 { + switch self { + case let .sectionId(sectionId, _): + return (sectionId + 1) * 100000 - sectionId + default: + return (sectionIndex * 100000) + stableIndex + } + } + + func item(arguments: InputDataArguments, initialSize: NSSize) -> TableRowItem { + switch self { + case let .sectionId(_, type): + let viewType: GeneralViewType + switch type { + case .legacy, .custom: + viewType = .legacy + default: + viewType = .separator + } + return GeneralRowItem(initialSize, height: type.height, stableId: stableId, viewType: viewType) + case let .desc(_, _, text, data): + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, detectBold: data.detectBold, textColor: data.color, viewType: data.viewType, rightItem: data.rightItem, fontSize: data.fontSize) + case let .custom(_, _, _, _, _, _, item): + return item(initialSize, stableId) + case let .selector(_, _, value, error, _, placeholder, viewType, values): + return InputDataDataSelectorRowItem(initialSize, stableId: stableId, value: value, error: error, placeholder: placeholder, viewType: viewType, updated: arguments.dataUpdated, values: values) + case let .dataSelector(_, _, _, error, _, placeholder, description, icon, action): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: placeholder, icon: icon, nameStyle: ControlStyle(font: .normal(.title), foregroundColor: theme.colors.accent), description: description, type: .none, action: action, error: error) + case let .general(_, _, value, error, identifier, data): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: data.name, icon: data.icon, nameStyle: ControlStyle(font: .normal(.title), foregroundColor: data.color), description: data.description, type: data.type, viewType: data.viewType, action: { + data.action != nil ? data.action?() : arguments.select((identifier, value)) + }, enabled: data.enabled, error: error, disabledAction: data.disabledAction ?? {}, menuItems: data.menuItems, customTheme: data.theme, disableBorder: data.disableBorder) + case let .dateSelector(_, _, value, error, _, placeholder): + return InputDataDateRowItem(initialSize, stableId: stableId, value: value, error: error, updated: arguments.dataUpdated, placeholder: placeholder) + case let .input(_, _, value, error, _, mode, data, placeholder, inputPlaceholder, filter, limit: limit): + return InputDataRowItem(initialSize, stableId: stableId, mode: mode, error: error, viewType: data.viewType, currentText: value.stringValue ?? "", currentAttributedText: value.attributedString, placeholder: placeholder, inputPlaceholder: inputPlaceholder, defaultText: data.defaultText, rightItem: data.rightItem, canMakeTransformations: data.canMakeTransformations, maxBlockWidth: data.maxBlockWidth, filter: filter, updated: { _ in + arguments.dataUpdated() + }, pasteFilter: data.pasteFilter, limit: limit, customTheme: data.customTheme) + case .loading: + return SearchEmptyRowItem(initialSize, stableId: stableId, isLoading: true) + case let .search(_, _, value, _, update): + return SearchRowItem(initialSize, stableId: stableId, searchInteractions: SearchInteractions({ state, _ in + update(state) + }, { state in + update(state) + }), inset: NSEdgeInsets(left: 10,right: 10, top: 10, bottom: 10)) + } + } +} + +func <(lhs: InputDataEntry, rhs: InputDataEntry) -> Bool { + if let lhsComparable = lhs.comparable, let rhsComparable = rhs.comparable { + return lhsComparable < rhsComparable + } + return lhs.index < rhs.index +} + +func ==(lhs: InputDataEntry, rhs: InputDataEntry) -> Bool { + switch lhs { + case let .desc(sectionId, index, text, data): + if case .desc(sectionId, index, text, data) = rhs { + return true + } else { + return false + } + case let .input(sectionId, index, lhsValue, lhsError, identifier, mode, data, placeholder, inputPlaceholder, _, limit): + if case .input(sectionId, index, let rhsValue, let rhsError, identifier, mode, data, placeholder, inputPlaceholder, _, limit) = rhs { + return lhsValue == rhsValue && lhsError == rhsError + } else { + return false + } + case let .general(sectionId, index, lhsValue, lhsError, identifier, data): + if case .general(sectionId, index, let rhsValue, let rhsError, identifier, data) = rhs { + return lhsValue == rhsValue && lhsError == rhsError + } else { + return false + } + case let .selector(sectionId, index, lhsValue, lhsError, identifier, placeholder, viewType, lhsValues): + if case .selector(sectionId, index, let rhsValue, let rhsError, identifier, placeholder, viewType, let rhsValues) = rhs { + return lhsValues == rhsValues && lhsValue == rhsValue && lhsError == rhsError + } else { + return false + } + case let .dateSelector(sectionId, index, lhsValue, lhsError, identifier, placeholder): + if case .dateSelector(sectionId, index, let rhsValue, let rhsError, identifier, placeholder) = rhs { + return lhsValue == rhsValue && lhsError == rhsError + } else { + return false + } + case let .dataSelector(sectionId, index, lhsValue, lhsError, identifier, placeholder, description, lhsIcon, _): + if case .dataSelector(sectionId, index, let rhsValue, let rhsError, identifier, placeholder, description, let rhsIcon, _) = rhs { + return lhsValue == rhsValue && lhsError == rhsError && lhsIcon == rhsIcon + } else { + return false + } + case let .custom(_, _, value, identifier, lhsEquatable, comparable, _): + if case .custom(_, _, value, identifier, let rhsEquatable, comparable, _) = rhs { + return lhsEquatable == rhsEquatable + } else { + return false + } + case let .search(sectionId, index, value, identifier, _): + if case .search(sectionId, index, value, identifier, _) = rhs { + return true + } else { + return false + } + case let .sectionId(id, type): + if case .sectionId(id, type) = rhs { + return true + } else { + return false + } + case .loading: + if case .loading = rhs { + return true + } else { + return false + } + } +} + +let InputDataEmptyIdentifier = InputDataIdentifier("") + +struct InputDataIdentifier : Hashable { + let identifier: String + init(_ identifier: String) { + self.identifier = identifier + } + func hash(into hasher: inout Hasher) { + hasher.combine(identifier) + } +} + +func ==(lhs: InputDataIdentifier, rhs: InputDataIdentifier) -> Bool { + return lhs.identifier == rhs.identifier +} + +enum InputDataValue : Equatable { + case string(String?) + case attributedString(NSAttributedString?) + case date(Int32?, Int32?, Int32?) + case gender(SecureIdGender?) + case secureIdDocument(SecureIdVerificationDocument) + case none + var stringValue: String? { + switch self { + case let .string(value): + return value + default: + return nil + } + } + + var attributedString:NSAttributedString? { + switch self { + case let .attributedString(value): + return value + default: + return nil + } + } + + var secureIdDocument: SecureIdVerificationDocument? { + switch self { + case let .secureIdDocument(document): + return document + default: + return nil + } + } + + var gender: SecureIdGender? { + switch self { + case let .gender(gender): + return gender + default: + return nil + } + } +} + +func ==(lhs: InputDataValue, rhs: InputDataValue) -> Bool { + switch lhs { + case let .string(lhsValue): + if case let .string(rhsValue) = rhs { + return lhsValue == rhsValue + } else { + return false + } + case let .attributedString(lhsValue): + if case let .attributedString(rhsValue) = rhs { + return lhsValue == rhsValue + } else { + return false + } + case let .gender(lhsValue): + if case let .gender(rhsValue) = rhs { + return lhsValue == rhsValue + } else { + return false + } + case let .secureIdDocument(lhsValue): + if case let .secureIdDocument(rhsValue) = rhs { + return lhsValue.isEqual(to: rhsValue) + } else { + return false + } + case let .date(lhsDay, lhsMonth, lhsYear): + if case let .date(rhsDay, rhsMonth,rhsYear) = rhs { + return lhsDay == rhsDay && lhsMonth == rhsMonth && lhsYear == rhsYear + } else { + return false + } + case .none: + if case .none = rhs { + return true + } else { + return false + } + } +} + +enum InputDataValidationFailAction { + case shake + case shakeWithData(Any) +} + +enum InputDataValidationBehaviour { + case navigationBack + case navigationBackWithPushAnimation + case custom(()->Void) +} + +enum InputDataFailResult { + case alert(String) + case doSomething(next: (@escaping(InputDataValidation)->Void) -> Void) + case textAfter(String, InputDataIdentifier) + case fields([InputDataIdentifier: InputDataValidationFailAction]) + case none +} + +enum InputDataValidation { + case success(InputDataValidationBehaviour) + case fail(InputDataFailResult) + case none + var isSuccess: Bool { + switch self { + case .success: + return true + case .fail, .none: + return false + + } + } +} + + + + +enum InputDoneValue : Equatable { + case enabled(String) + case disabled(String) + case invisible + case loading +} diff --git a/Telegram-Mac/InputDataDataSelectorRowItem.swift b/Telegram-Mac/InputDataDataSelectorRowItem.swift new file mode 100644 index 0000000000..853a4af517 --- /dev/null +++ b/Telegram-Mac/InputDataDataSelectorRowItem.swift @@ -0,0 +1,119 @@ +// +// InputDataDataSelectorRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 21/03/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +class InputDataDataSelectorRowItem: GeneralRowItem, InputDataRowDataValue { + + private let updated:()->Void + fileprivate var _value: InputDataValue { + didSet { + if _value != oldValue { + updated() + } + } + } + fileprivate let placeholderLayout: TextViewLayout + + var value: InputDataValue { + return _value + } + + fileprivate let values: [ValuesSelectorValue] + init(_ initialSize: NSSize, stableId: AnyHashable, value: InputDataValue, error: InputDataValueError?, placeholder: String, viewType: GeneralViewType, updated: @escaping()->Void, values: [ValuesSelectorValue]) { + self._value = value + self.updated = updated + self.placeholderLayout = TextViewLayout(.initialize(string: placeholder, color: theme.colors.text, font: .normal(.text)), maximumNumberOfLines: 1) + self.values = values + super.init(initialSize, height: 42, stableId: stableId, viewType: viewType, error: error) + _ = makeSize(initialSize.width, oldWidth: oldWidth) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) + placeholderLayout.measure(width: 100) + return success + } + + override func viewClass() -> AnyClass { + return InputDataDataSelectorRowView.self + } + +} + + +final class InputDataDataSelectorRowView : GeneralContainableRowView { + private let placeholderTextView = TextView() + private let dataTextView = TextView() + private let overlay = OverlayControl() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(placeholderTextView) + addSubview(dataTextView) + addSubview(overlay) + placeholderTextView.userInteractionEnabled = false + placeholderTextView.isSelectable = false + dataTextView.userInteractionEnabled = false + dataTextView.isSelectable = false + + overlay.set(handler: { [weak self] _ in + guard let item = self?.item as? InputDataDataSelectorRowItem else {return} + showModal(with: ValuesSelectorModalController(values: item.values, selected: item.values.first(where: {$0.value == item.value}), title: item.placeholderLayout.attributedString.string, onComplete: { [weak item] newValue in + item?._value = newValue.value + item?.redraw() + }), for: mainWindow) + }, for: .Click) + } + + override func shakeView() { + dataTextView.shake() + } + + + override func layout() { + super.layout() + guard let item = item as? InputDataDataSelectorRowItem else {return} + placeholderTextView.setFrameOrigin(item.viewType.innerInset.left, 14) + + dataTextView.layout?.measure(width: frame.width - item.viewType.innerInset.left - item.viewType.innerInset.right - 104) + dataTextView.update(dataTextView.layout) + dataTextView.setFrameOrigin(item.viewType.innerInset.left + 104, 14) + + overlay.frame = containerView.bounds + } + + override func updateColors() { + super.updateColors() + placeholderTextView.backgroundColor = theme.colors.background + } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + + guard let item = item as? InputDataDataSelectorRowItem else {return} + placeholderTextView.update(item.placeholderLayout) + + var selected: ValuesSelectorValue? + let index:Int? = item.values.index(where: { entry -> Bool in + return entry.value == item.value + }) + if let index = index { + selected = item.values[index] + } + let layout = TextViewLayout(.initialize(string: selected?.localized ?? item.placeholderLayout.attributedString.string, color: selected == nil ? theme.colors.grayText : theme.colors.text, font: .normal(.text)), maximumNumberOfLines: 1) + dataTextView.update(layout) + + needsLayout = true + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/InputDataDateRowItem.swift b/Telegram-Mac/InputDataDateRowItem.swift new file mode 100644 index 0000000000..de06187e86 --- /dev/null +++ b/Telegram-Mac/InputDataDateRowItem.swift @@ -0,0 +1,422 @@ +// +// InputDataDateRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 21/03/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +class InputDataDateRowItem: GeneralRowItem, InputDataRowDataValue { + fileprivate let placeholderLayout: TextViewLayout + fileprivate let dayHolderLayout: TextViewLayout + fileprivate let monthHolderLayout: TextViewLayout + fileprivate let yearHolderLayout: TextViewLayout + + private let updated:()->Void + fileprivate var _value: InputDataValue { + didSet { + if _value != oldValue { + updated() + } + } + } + + var value: InputDataValue { + return _value + } + + init(_ initialSize: NSSize, stableId: AnyHashable, value: InputDataValue, error: InputDataValueError?, updated:@escaping()->Void, placeholder: String) { + self._value = value + self.updated = updated + placeholderLayout = TextViewLayout(.initialize(string: placeholder, color: theme.colors.text, font: .normal(.text)), maximumNumberOfLines: 1) + + dayHolderLayout = TextViewLayout(.initialize(string: L10n.inputDataDateDayPlaceholder1, color: theme.colors.text, font: .normal(.text))) + monthHolderLayout = TextViewLayout(.initialize(string: L10n.inputDataDateMonthPlaceholder1, color: theme.colors.text, font: .normal(.text))) + yearHolderLayout = TextViewLayout(.initialize(string: L10n.inputDataDateYearPlaceholder1, color: theme.colors.text, font: .normal(.text))) + + dayHolderLayout.measure(width: .greatestFiniteMagnitude) + monthHolderLayout.measure(width: .greatestFiniteMagnitude) + yearHolderLayout.measure(width: .greatestFiniteMagnitude) + + super.init(initialSize, height: 44, stableId: stableId, error: error) + _ = makeSize(initialSize.width, oldWidth: oldWidth) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) + placeholderLayout.measure(width: 100) + return success + } + + override func viewClass() -> AnyClass { + return InputDataDateRowView.self + } + +} + + +final class InputDataDateRowView : GeneralRowView, TGModernGrowingDelegate { + private let placeholderTextView = TextView() + private let dayInput = TGModernGrowingTextView(frame: NSZeroRect) + private let monthInput = TGModernGrowingTextView(frame: NSZeroRect) + private let yearInput = TGModernGrowingTextView(frame: NSZeroRect) + + private let firstSeparator = TextViewLabel() + private let secondSeparator = TextViewLabel() + private var ignoreChanges: Bool = false + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(placeholderTextView) + + placeholderTextView.userInteractionEnabled = false + placeholderTextView.isSelectable = false + + dayInput.delegate = self + monthInput.delegate = self + yearInput.delegate = self + + + + dayInput.textFont = .normal(.text) + monthInput.textFont = .normal(.text) + yearInput.textFont = .normal(.text) + + + + addSubview(dayInput) + addSubview(monthInput) + addSubview(yearInput) + + firstSeparator.autosize = true + secondSeparator.autosize = true + + addSubview(firstSeparator) + addSubview(secondSeparator) + + } + + public func maxCharactersLimit(_ textView: TGModernGrowingTextView!) -> Int32 { + switch true { + case textView === dayInput: + return 2 + case textView === monthInput: + return 2 + case textView === yearInput: + return 4 + default: + return 0 + } + } + + func textViewHeightChanged(_ height: CGFloat, animated: Bool) { + var bp:Int = 0 + bp += 1 + } + + func textViewSize(_ textView: TGModernGrowingTextView!) -> NSSize { + return textView.frame.size + } + + func textViewEnterPressed(_ event:NSEvent) -> Bool { + if FastSettings.checkSendingAbility(for: event) { + return true + } + return false + } + + func textViewIsTypingEnabled() -> Bool { + return true + } + + func textViewNeedClose(_ textView: Any) { + + } + + func textViewTextDidChange(_ string: String) { + + guard let item = item as? InputDataDateRowItem else {return} + guard !ignoreChanges else {return} + + var day = String(dayInput.string().unicodeScalars.filter { CharacterSet.decimalDigits.contains($0)}) + var month = String(monthInput.string().unicodeScalars.filter { CharacterSet.decimalDigits.contains($0)}) + var year = String(yearInput.string().unicodeScalars.filter { CharacterSet.decimalDigits.contains($0)}) + + var _month:String? + var _year: String? + var _day: String? + if year.length == 4 { + year = "\(Int(year)!)" + _year = year + } + if month.length > 0 { + let _m = min(Int(month)!, 12) + if _m == 0 { + month = "0" + } else { + month = "\(month.length == 2 && _m < 10 ? "0\(_m)" : "\(_m)")" + } + _month = month == "0" ? nil : month + } + + if day.length > 0 { + var _max:Int = 31 + if let year = _year, let month = _month { + if let date = dateFormatter.date(from: "02.\(month).\(year)") { + _max = CalendarUtils.lastDay(ofTheMonth: date) + } + } + let _d = min(Int(day)!, _max) + if _d == 0 { + day = "0" + } else { + day = "\(day.length == 2 && _d < 10 ? "0\(_d)" : "\(_d)")" + } + _day = day == "0" ? nil : day + } + + item._value = .date(_day != nil ? Int32(_day!) : nil, _month != nil ? Int32(_month!) : nil, _year != nil ? Int32(_year!) : nil) + + dayInput.setString(day) + monthInput.setString(month) + yearInput.setString(year) + + if month.length == 2, month.prefix(1) == "0" { + textViewDidReachedLimit(monthInput) + } + + if day.length == 2, window?.firstResponder == dayInput.inputView { + textViewDidReachedLimit(dayInput) + } + if month.length == 2, window?.firstResponder == monthInput.inputView { + textViewDidReachedLimit(monthInput) + } + } + + func textViewDidReachedLimit(_ textView: Any) { + if let responder = nextResponder() { + window?.makeFirstResponder(responder) + } + } + + func controlTextDidChange(_ obj: Notification) { + + } + + func textViewTextDidChangeSelectedRange(_ range: NSRange) { + + } + + func textViewDidPaste(_ pasteboard: NSPasteboard) -> Bool { + return false + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + super.draw(layer, in: ctx) + + guard let item = item as? InputDataDateRowItem else {return} + + ctx.setFillColor(theme.colors.border.cgColor) + ctx.fill(NSMakeRect(item.inset.left, frame.height - .borderSize, frame.width - item.inset.left - item.inset.right, .borderSize)) + } + + override var mouseInsideField: Bool { + return yearInput._mouseInside() || dayInput._mouseInside() || monthInput._mouseInside() + } + + override func hitTest(_ point: NSPoint) -> NSView? { + switch true { + case NSPointInRect(point, yearInput.frame): + return yearInput + case NSPointInRect(point, dayInput.frame): + return dayInput + case NSPointInRect(point, monthInput.frame): + return monthInput + default: + return super.hitTest(point) + } + } + + override func hasFirstResponder() -> Bool { + return true + } + + override var firstResponder: NSResponder? { + let isKeyDown = NSApp.currentEvent?.type == NSEvent.EventType.keyDown && NSApp.currentEvent?.keyCode == KeyboardKey.Tab.rawValue + switch true { + case yearInput._mouseInside() && !isKeyDown: + return yearInput.inputView + case dayInput._mouseInside() && !isKeyDown: + return dayInput.inputView + case monthInput._mouseInside() && !isKeyDown: + return monthInput.inputView + default: + switch true { + case yearInput.inputView == window?.firstResponder: + return yearInput.inputView + case dayInput.inputView == window?.firstResponder: + return dayInput.inputView + case monthInput.inputView == window?.firstResponder: + return monthInput.inputView + default: + return dayInput.inputView + } + } + } + + override func nextResponder() -> NSResponder? { + if window?.firstResponder == dayInput.inputView { + return monthInput.inputView + } + if window?.firstResponder == monthInput.inputView { + return yearInput.inputView + } + return nil + } + + + override func layout() { + super.layout() + guard let item = item as? InputDataDateRowItem else {return} + + placeholderTextView.setFrameOrigin(item.inset.left, 14) + + let defaultLeftInset = item.inset.left + 100 + + dayInput.setFrameOrigin(defaultLeftInset, 15) + monthInput.setFrameOrigin(dayInput.frame.maxX + 8, 15) + yearInput.setFrameOrigin(monthInput.frame.maxX + 8, 15) + + firstSeparator.setFrameOrigin(dayInput.frame.maxX - 7, 14) + secondSeparator.setFrameOrigin(monthInput.frame.maxX - 7, 14) + + +// +// dayPlaceholder.setFrameOrigin(defaultLeftInset, 14) + +// +// monthPlaceholder.setFrameOrigin(defaultLeftInset, dayPlaceholder.frame.maxY + 5) +// monthSelector.setFrameOrigin(monthPlaceholder.frame.maxX + 3, monthPlaceholder.frame.minY - 3) +// +// yearPlaceholder.setFrameOrigin(defaultLeftInset, monthPlaceholder.frame.maxY + 5) +// yearSelector.setFrameOrigin(yearPlaceholder.frame.maxX + 3, yearPlaceholder.frame.minY - 3) + } + + override func shakeView() { + guard let item = item as? InputDataDateRowItem else {return} + + switch item.value { + case let .date(day, month, year): + if day == nil { + dayInput.shake() + } + if month == nil { + monthInput.shake() + } + if year == nil { + yearInput.shake() + } + if year != nil && month != nil && day != nil { + dayInput.shake() + monthInput.shake() + yearInput.shake() + } + default: + break + } + + + } + + override func updateColors() { + placeholderTextView.backgroundColor = theme.colors.background + firstSeparator.backgroundColor = theme.colors.background + secondSeparator.backgroundColor = theme.colors.background + + dayInput.textColor = theme.colors.text + monthInput.textColor = theme.colors.text + yearInput.textColor = theme.colors.text + + dayInput.setBackgroundColor(backdorColor) + monthInput.setBackgroundColor(backdorColor) + yearInput.setBackgroundColor(backdorColor) + } + + override func set(item: TableRowItem, animated: Bool) { + + + + guard let item = item as? InputDataDateRowItem else {return} + placeholderTextView.update(item.placeholderLayout) + + + let dayLayout = TextViewLayout(.initialize(string: L10n.inputDataDateDayPlaceholder1, color: theme.colors.grayText, font: .normal(.text))) + dayLayout.measure(width: .greatestFiniteMagnitude) + + let monthLayout = TextViewLayout(.initialize(string: L10n.inputDataDateMonthPlaceholder1, color: theme.colors.grayText, font: .normal(.text))) + monthLayout.measure(width: .greatestFiniteMagnitude) + + let yearLayout = TextViewLayout(.initialize(string: L10n.inputDataDateYearPlaceholder1, color: theme.colors.grayText, font: .normal(.text))) + yearLayout.measure(width: .greatestFiniteMagnitude) + + + + dayInput.min_height = Int32(dayLayout.layoutSize.height) + dayInput.max_height = Int32(dayLayout.layoutSize.height) + dayInput.setFrameSize(NSMakeSize(dayLayout.layoutSize.width + 20, dayLayout.layoutSize.height)) + + monthInput.min_height = Int32(monthLayout.layoutSize.height) + monthInput.max_height = Int32(monthLayout.layoutSize.height) + monthInput.setFrameSize(NSMakeSize(monthLayout.layoutSize.width + 20, monthLayout.layoutSize.height)) + + yearInput.min_height = Int32(yearLayout.layoutSize.height) + yearInput.max_height = Int32(yearLayout.layoutSize.height) + yearInput.setFrameSize(NSMakeSize(yearLayout.layoutSize.width + 20, yearLayout.layoutSize.height)) + + firstSeparator.attributedString = .initialize(string: "/", color: theme.colors.text, font: .medium(.text)) + secondSeparator.attributedString = .initialize(string: "/", color: theme.colors.text, font: .medium(.text)) + firstSeparator.sizeToFit() + secondSeparator.sizeToFit() + + ignoreChanges = true + + switch item.value { + case let .date(day, month, year): + if let day = day { + dayInput.setString("\( day < 10 && day > 0 ? "\(day)" : "\(day)")", animated: false) + } else { + dayInput.setString("", animated: false) + } + if let month = month { + monthInput.setString("\( month < 10 && month > 0 ? "\(month)" : "\(month)")", animated: false) + } else { + monthInput.setString("", animated: false) + } + if let year = year { + yearInput.setString("\(year)", animated: false) + } else { + yearInput.setString("", animated: false) + } + default: + dayInput.setString("", animated: false) + monthInput.setString("", animated: false) + yearInput.setString("", animated: false) + } + ignoreChanges = false + + dayInput.placeholderAttributedString = dayLayout.attributedString + monthInput.placeholderAttributedString = monthLayout.attributedString + yearInput.placeholderAttributedString = yearLayout.attributedString + + + super.set(item: item, animated: animated) + + layout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/InputDataRowItem.swift b/Telegram-Mac/InputDataRowItem.swift new file mode 100644 index 0000000000..ce2964f9b4 --- /dev/null +++ b/Telegram-Mac/InputDataRowItem.swift @@ -0,0 +1,969 @@ +// +// InputDataRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 21/03/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +protocol InputDataRowDataValue { + var value: InputDataValue { get } +} + +enum InputDataRightItemAction : Equatable { + static func == (lhs: InputDataRightItemAction, rhs: InputDataRightItemAction) -> Bool { + switch lhs { + case .clearText: + if case .clearText = rhs { + return true + } else { + return false + } + case .resort: + if case .resort = rhs { + return true + } else { + return false + } + case .none: + if case .none = rhs { + return true + } else { + return false + } + case .custom: + if case .custom = rhs { + return true + } else { + return false + } + } + } + + case clearText + case resort + case custom((TableRowItem, Control)->Void) + case none +} + +enum InputDataRightItem : Equatable { + case action(CGImage, InputDataRightItemAction) + case loading +} + +class InputDataRowItem: GeneralRowItem, InputDataRowDataValue { + + fileprivate let placeholderLayout: TextViewLayout? + fileprivate let placeholder: InputDataInputPlaceholder? + + + fileprivate let inputPlaceholder: NSAttributedString + fileprivate let filter:(String)->String + let limit:Int32 + private let updated:(String)->Void + private var updatedText: String? + private var _currentText: NSAttributedString = NSAttributedString() { + didSet { + let newValue = currentText.string + if updatedText != newValue { + updated(newValue) + updatedText = newValue + } + } + } + fileprivate(set) var currentText: NSAttributedString { + set { + let copy = newValue.mutableCopy() as! NSMutableAttributedString + if let defaultText = self.defaultText { + copy.replaceCharacters(in: NSMakeRange(0, min(defaultText.length, copy.length)), with: "") + } + _currentText = copy + + if copy != newValue { + self.redraw() + } + } + get { + return _currentText + } + } + + + + var currentAttributed: NSAttributedString { + let attr = NSMutableAttributedString() + + if let defaultText = self.defaultText { + attr.append(.initialize(string: defaultText, color: theme.colors.text, font: .normal(.text))) + } + attr.append(currentText) + return attr + } + + var value: InputDataValue { + if canMakeTransformations { + return .attributedString(currentText) + } else { + return .string(currentText.string) + } + } + + fileprivate var inputHeight: CGFloat = 21 + fileprivate var realInputHeight: CGFloat = 21 + + override var inset: NSEdgeInsets { + var inset = super.inset + switch viewType { + case .legacy: + break + case .modern: + if let errorLayout = errorLayout { + inset.bottom += errorLayout.layoutSize.height + 4 + } + } + return inset + } + + override var height: CGFloat { + switch viewType { + case .legacy: + var height = inputHeight + 8 + if let errorLayout = errorLayout { + height += (height == 42 ? errorLayout.layoutSize.height : errorLayout.layoutSize.height / 2) + } + return height + case let .modern(_, insets): + var inputHeight = realInputHeight + switch self.mode { + case .plain: + break + case .secure: + inputHeight -= 6 + } + let height = inputHeight + insets.top + insets.bottom + inset.top + inset.bottom +// if let errorLayout = errorLayout { +// height += errorLayout.layoutSize.height + 4 +// } + return height + } + + } + fileprivate let defaultText: String? + fileprivate let mode: InputDataInputMode + fileprivate let rightItem: InputDataRightItem? + fileprivate let canMakeTransformations: Bool + fileprivate let pasteFilter:((String)->(Bool, String))? + private let maxBlockWidth: CGFloat? + init(_ initialSize: NSSize, stableId: AnyHashable, mode: InputDataInputMode, error: InputDataValueError?, viewType: GeneralViewType = .legacy, currentText: String, currentAttributedText: NSAttributedString? = nil, placeholder: InputDataInputPlaceholder?, inputPlaceholder: String, defaultText: String? = nil, rightItem: InputDataRightItem? = nil, canMakeTransformations: Bool = false, insets: NSEdgeInsets = NSEdgeInsets(left: 30.0, right: 30.0), maxBlockWidth: CGFloat? = nil, filter:@escaping(String)->String, updated:@escaping(String)->Void, pasteFilter:((String)->(Bool, String))? = nil, limit: Int32, customTheme: GeneralRowItem.Theme? = nil) { + self.filter = filter + self.limit = limit + self.updated = updated + self.placeholder = placeholder + self.pasteFilter = pasteFilter + self.defaultText = defaultText + self.maxBlockWidth = maxBlockWidth + self.canMakeTransformations = canMakeTransformations + self.rightItem = rightItem + let holder = NSMutableAttributedString() + switch mode { + case .secure: + holder.append(.initialize(string: inputPlaceholder, color: customTheme?.grayTextColor ?? theme.colors.grayText, font: .light(.text))) + case .plain: + holder.append(.initialize(string: inputPlaceholder, color: customTheme?.grayTextColor ?? theme.colors.grayText, font: .normal(.text))) + } + self.inputPlaceholder = holder + placeholderLayout = placeholder?.placeholder != nil ? TextViewLayout(.initialize(string: placeholder!.placeholder!, color: customTheme?.grayTextColor ?? theme.colors.text, font: .normal(.text)), maximumNumberOfLines: 1) : nil + + _currentText = currentAttributedText ?? NSAttributedString.initialize(string: currentText, color: theme.colors.text, font: .normal(.text), coreText: false) + self.mode = mode + + super.init(initialSize, stableId: stableId, viewType: viewType, inset: insets, error: error, customTheme: customTheme) + + + _ = makeSize(initialSize.width, oldWidth: oldWidth) + } + + override var blockWidth: CGFloat { + if let maxBlockWidth = maxBlockWidth { + return min(maxBlockWidth, super.blockWidth) + } else { + return super.blockWidth + } + } + + var textFieldLeftInset: CGFloat { + if let placeholder = placeholder { + if let _ = placeholder.placeholder { + return 102 + } else { + if let icon = placeholder.icon { + return icon.backingSize.width + 6 + placeholder.insets.left + } else { + return -2 + } + } + } else { + return -2 + } + } + + override var instantlyResize: Bool { + return true + } + + func calculateHeight() { + _ = self.makeSize(self.width, oldWidth: self.width) + } + + private(set) fileprivate var additionRightInset: CGFloat = 0 + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { + let currentAttributed: NSMutableAttributedString = NSMutableAttributedString() + _ = currentAttributed.append(string: (defaultText ?? ""), font: .normal(.text)) + currentAttributed.append(currentText) + + if mode == .secure { + currentAttributed.setAttributedString(.init(string: String(currentText.string.map { _ in return "•" }))) + currentAttributed.addAttribute(.font, value: NSFont.normal(15.0 + 3.22), range: currentAttributed.range) + } + + let textStorage = NSTextStorage(attributedString: currentAttributed) + + if let rightItem = self.rightItem { + switch rightItem { + case .loading: + self.additionRightInset = 20 + case let .action(icon, _): + self.additionRightInset = icon.backingSize.width + 2 + } + } else { + self.additionRightInset = 0 + } + + switch viewType { + case .legacy: + let textContainer = NSTextContainer(size: NSMakeSize(initialSize.width - inset.left - inset.right - textFieldLeftInset - additionRightInset, .greatestFiniteMagnitude)) + let layoutManager = NSLayoutManager() + layoutManager.addTextContainer(textContainer) + textStorage.addLayoutManager(layoutManager) + layoutManager.ensureLayout(for: textContainer) + self.realInputHeight = max(34, layoutManager.usedRect(for: textContainer).height + 6) + inputHeight = max(34, layoutManager.usedRect(for: textContainer).height + 6) + case let .modern(_, insets): + let textContainer = NSTextContainer(size: NSMakeSize(self.blockWidth - insets.left - insets.right - textFieldLeftInset - additionRightInset, .greatestFiniteMagnitude)) + let layoutManager = NSLayoutManager() + layoutManager.addTextContainer(textContainer) + textStorage.addLayoutManager(layoutManager) + layoutManager.ensureLayout(for: textContainer) + switch self.mode { + case .plain: + self.realInputHeight = max(16, layoutManager.usedRect(for: textContainer).height) + case .secure: + self.realInputHeight = max(22, layoutManager.usedRect(for: textContainer).height) + } + inputHeight = max(34, layoutManager.usedRect(for: textContainer).height + 1) + } + + let success = super.makeSize(width, oldWidth: oldWidth) + placeholderLayout?.measure(width: 100) + return success + } + + override func viewClass() -> AnyClass { + return InputDataRowView.self + } + +} + +private final class InputDataSecureField : NSSecureTextField { + override func becomeFirstResponder() -> Bool { + + let success = super.becomeFirstResponder() + if success { + let tetView = self.currentEditor() as? NSTextView + tetView?.insertionPointColor = theme.colors.indicatorColor + } + //NSTextView* textField = (NSTextView*) [self currentEditor]; + + return success + } +} + + +class InputDataRowView : GeneralRowView, TGModernGrowingDelegate, NSTextFieldDelegate { + internal let containerView = GeneralRowContainerView(frame: NSZeroRect) + private let placeholderTextView = TextView() + private let rightActionView: ImageButton = ImageButton() + private var loadingView: ProgressIndicator? = nil + private var placeholderAction: ImageButton? + internal let textView: TGModernGrowingTextView = TGModernGrowingTextView(frame: NSZeroRect, unscrollable: true) + private let secureField: InputDataSecureField = InputDataSecureField(frame: NSMakeRect(0, 0, 100, 16)) + private let textLimitation: TextViewLabel = TextViewLabel(frame: NSMakeRect(0, 0, 16, 14)) + private let separator: View = View() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + containerView.addSubview(placeholderTextView) + containerView.addSubview(textView) + containerView.addSubview(secureField) + containerView.addSubview(separator) + containerView.addSubview(textLimitation) + containerView.addSubview(rightActionView) + addSubview(containerView) + rightActionView.autohighlight = false + + containerView.userInteractionEnabled = false + + textLimitation.alignment = .right + + + + // textView.max_height = 34 + // .isSingleLine = true + textView.delegate = self + placeholderTextView.userInteractionEnabled = false + placeholderTextView.isSelectable = false + + secureField.isBordered = false + secureField.isBezeled = false + secureField.focusRingType = .none + secureField.delegate = self + secureField.drawsBackground = true + secureField.isEditable = true + secureField.isSelectable = true + + textView.max_height = 10000 + + secureField.font = .normal(.text) + secureField.textView?.insertionPointColor = theme.colors.text + secureField.sizeToFit() + + } + + override func shakeView() { + if !secureField.isHidden { + secureField.shake() + secureField.setSelectionRange(NSMakeRange(0, secureField.stringValue.length)) + } + if !textView.isHidden { + textView.shake() + textView.setSelectedRange(NSMakeRange(0, textView.string().length)) + } + self.separator.shake() + } + + override func hasFirstResponder() -> Bool { + return true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func mouseDown(with event: NSEvent) { + super.mouseDown(with: event) + } + + func canTransformInputText() -> Bool { + guard let item = item as? InputDataRowItem else { return false } + return item.canMakeTransformations + } + + func makeBold() { + self.textView.boldWord() + } + func makeUrl() { + self.makeUrl(of: textView.selectedRange()) + } + func makeItalic() { + self.textView.italicWord() + } + func makeMonospace() { + self.textView.codeWord() + } + + func makeUrl(of range: NSRange) { + guard range.min != range.max, let window = window as? Window else { + return + } + var effectiveRange:NSRange = NSMakeRange(NSNotFound, 0) + let defaultTag: TGInputTextTag? = self.textView.attributedString().attribute(NSAttributedString.Key(rawValue: TGCustomLinkAttributeName), at: range.location, effectiveRange: &effectiveRange) as? TGInputTextTag + + + let defaultUrl = defaultTag?.attachment as? String + + if effectiveRange.location == NSNotFound || defaultTag == nil { + effectiveRange = range + } + + showModal(with: InputURLFormatterModalController(string: self.textView.string().nsstring.substring(with: effectiveRange), defaultUrl: defaultUrl, completion: { [weak self] url in + self?.textView.addLink(url, range: effectiveRange) + }), for: window) + + } + + + private var separatorFrame: NSRect { + + guard let item = item as? InputDataRowItem else { return NSZeroRect } + + switch item.viewType { + case .legacy: + if let placeholder = item.placeholder { + if placeholder.drawBorderAfterPlaceholder { + return NSMakeRect(item.inset.left + item.textFieldLeftInset + 4, self.containerView.frame.height - .borderSize, self.containerView.frame.width - item.inset.left - item.inset.right - item.textFieldLeftInset - 4, .borderSize) + } else { + return NSMakeRect(item.inset.left, self.containerView.frame.height - .borderSize, self.containerView.frame.width - item.inset.left - item.inset.right, .borderSize) + } + } else { + return NSMakeRect(item.inset.left, self.containerView.frame.height - .borderSize, self.containerView.frame.width - item.inset.left - item.inset.right, .borderSize) + } + case let .modern(_, innerInsets): + if let placeholder = item.placeholder { + if placeholder.drawBorderAfterPlaceholder { + return NSMakeRect(innerInsets.left + item.textFieldLeftInset + 4, self.containerView.frame.height - .borderSize, self.containerView.frame.width - innerInsets.left - innerInsets.right - item.textFieldLeftInset - 4, .borderSize) + } else { + return NSMakeRect(innerInsets.left, self.containerView.frame.height - .borderSize, self.containerView.frame.width - innerInsets.left - innerInsets.right, .borderSize) + } + } else { + return NSMakeRect(innerInsets.left, self.containerView.frame.height - .borderSize, self.containerView.frame.width - innerInsets.left - innerInsets.right, .borderSize) + } + } + } + + override func layout() { + super.layout() + guard let item = item as? InputDataRowItem else {return} + + switch item.viewType { + case .legacy: + self.containerView.frame = bounds + placeholderTextView.setFrameOrigin(item.inset.left, 14) + placeholderAction?.setFrameOrigin(item.inset.left, 12) + + separator.frame = separatorFrame + + if let rightItem = item.rightItem { + switch rightItem { + case .action: + rightActionView.setFrameOrigin(NSMakePoint(self.containerView.frame.width - rightActionView.frame.width - item.inset.right + 4, 14)) + default: + break + } + } + + secureField.setFrameSize(NSMakeSize(self.containerView.frame.width - item.inset.left - item.inset.right - item.textFieldLeftInset - item.additionRightInset, item.inputHeight)) + secureField.setFrameOrigin(item.inset.left + item.textFieldLeftInset, 14) + + textView.setFrameSize(NSMakeSize(self.containerView.frame.width - item.inset.left - item.inset.right - item.textFieldLeftInset - item.additionRightInset, item.inputHeight)) + textView.setFrameOrigin(item.inset.left + item.textFieldLeftInset - 3, 6) + + textLimitation.setFrameOrigin(NSMakePoint(self.containerView.frame.width - item.inset.right - textLimitation.frame.width + 4, self.containerView.frame.height - textLimitation.frame.height - 4)) + case let .modern(position, innerInsets): + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) + + self.separator.isHidden = !position.border + + let textX = innerInsets.left + item.textFieldLeftInset - 3 + + placeholderTextView.setFrameOrigin(NSMakePoint(innerInsets.left, innerInsets.top)) + placeholderAction?.setFrameOrigin(NSMakePoint(innerInsets.left, innerInsets.top - 1)) + + separator.frame = separatorFrame + + + if let rightItem = item.rightItem { + switch rightItem { + case .action: + if item.realInputHeight <= 22 { + rightActionView.centerY(x: self.containerView.frame.width - rightActionView.frame.width - innerInsets.right) + } else { + rightActionView.setFrameOrigin(NSMakePoint(self.containerView.frame.width - rightActionView.frame.width - innerInsets.right, innerInsets.top)) + } + case .loading: + if let loadingView = loadingView { + if item.realInputHeight <= 16 { + loadingView.centerY(x: self.containerView.frame.width - loadingView.frame.width - innerInsets.right) + } else { + loadingView.setFrameOrigin(NSMakePoint(self.containerView.frame.width - loadingView.frame.width - innerInsets.right, innerInsets.top)) + } + } + } + } + + + secureField.setFrameSize(NSMakeSize(item.blockWidth - innerInsets.left - innerInsets.right - item.textFieldLeftInset - item.additionRightInset, item.inputHeight)) + secureField.setFrameOrigin(innerInsets.left + item.textFieldLeftInset + 1, innerInsets.top) + + textView.setFrameSize(NSMakeSize(item.blockWidth - innerInsets.left - innerInsets.right - item.textFieldLeftInset - item.additionRightInset, item.inputHeight)) + + if item.realInputHeight <= 16 { + textView.setFrameOrigin(textX, innerInsets.top - 8) + } else { + textView.setFrameOrigin(textX, innerInsets.top ) + } + + textLimitation.setFrameOrigin(NSMakePoint(item.blockWidth - innerInsets.right - textLimitation.frame.width, self.containerView.frame.height - innerInsets.bottom - textLimitation.frame.height)) + + + } + } + + public func maxCharactersLimit(_ textView: TGModernGrowingTextView!) -> Int32 { + if let item = item as? InputDataRowItem { + return item.limit + } + return 100000 + } + + func textViewDidReachedLimit(_ textView: Any) { + if let item = item as? InputDataRowItem { + switch item.mode { + case .plain: + if self.textView.selectedRange().max == self.textView.string().length { + self.textView.shake() + } + case .secure: + self.secureField.shake() + } + } + + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + + } + + func textViewDidPaste(_ pasteboard: NSPasteboard) -> Bool { + if let item = item as? InputDataRowItem, let pasteFilter = item.pasteFilter { + if let string = pasteboard.string(forType: .string)?.trimmed { + let value = pasteFilter(string) + let updatedText = item.filter(value.1) + if value.0 { + switch item.mode { + case .plain: + textView.setString(updatedText) + case .secure: + secureField.stringValue = updatedText + } + } else { + switch item.mode { + case .plain: + textView.appendText(updatedText) + case .secure: + secureField.stringValue = secureField.stringValue + updatedText + } + } + return true + } + + } + return false + } + + func textViewHeightChanged(_ height: CGFloat, animated: Bool) { + + if let item = item as? InputDataRowItem, let table = item.table { + item.inputHeight = height + + + switch item.viewType { + case .legacy: + textLimitation.change(pos: NSMakePoint(containerView.frame.width - item.inset.right - textLimitation.frame.width + 4, item.height - textLimitation.frame.height), animated: animated) + case let .modern(_, insets): + textLimitation.change(pos: NSMakePoint(item.blockWidth - insets.right - textLimitation.frame.width , item.height - textLimitation.frame.height - insets.bottom), animated: animated) + } + + item.calculateHeight() + + change(size: NSMakeSize(item.width, item.height), animated: animated) + + let containerRect: NSRect + switch item.viewType { + case .legacy: + containerRect = self.bounds + case .modern: + containerRect = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, item.height - item.inset.bottom - item.inset.top) + } + containerView.change(size: containerRect.size, animated: animated, corners: item.viewType.corners) + containerView.change(pos: containerRect.origin, animated: animated) + + if let placeholder = item.placeholder { + if placeholder.drawBorderAfterPlaceholder { + separator.change(pos: NSMakePoint(separator.frame.minX, self.containerView.frame.height - .borderSize), animated: animated) + } else { + separator.change(pos: NSMakePoint(separator.frame.minX, self.containerView.frame.height - .borderSize), animated: animated) + } + } else { + separator.change(pos: NSMakePoint(separator.frame.minX, self.containerView.frame.height - .borderSize), animated: animated) + } + + table.noteHeightOfRow(item.index, animated) + } + + } + + func textViewSize(_ textView: TGModernGrowingTextView!) -> NSSize { + return textView.frame.size + } + + func textViewEnterPressed(_ event:NSEvent) -> Bool { + if FastSettings.checkSendingAbility(for: event) { + return true + } + return false + } + + func textViewIsTypingEnabled() -> Bool { + return true + } + + func textViewNeedClose(_ textView: Any) { + + } + + func textViewTextDidChange(_ string: String) { + if let item = item as? InputDataRowItem { + let updated = item.filter(string) + if updated != string { + + textView.setString(updated, animated: true) + NSSound.beep() + } else { + item.currentText = textView.attributedString() + } + let prevInputHeight = item.realInputHeight + let prevHeight = item.inputHeight + item.calculateHeight() + if prevInputHeight != item.realInputHeight && prevHeight == item.inputHeight { + textViewHeightChanged(item.inputHeight, animated: true) + } + containerView.needsDisplay = true + } + } + + + + func controlTextDidChange(_ obj: Notification) { + if let item = item as? InputDataRowItem { + let string = secureField.stringValue + let updated = item.filter(string) + if updated != string { + secureField.stringValue = updated + } else { + item.currentText = .initialize(string: updated, color: theme.colors.text, font: .normal(.text)) + } + let prevInputHeight = item.realInputHeight + item.calculateHeight() + if prevInputHeight != item.realInputHeight { + textViewHeightChanged(item.inputHeight, animated: true) + } + containerView.needsDisplay = true + } + } + + func textViewTextDidChangeSelectedRange(_ range: NSRange) { + if let item = item as? InputDataRowItem { + if item.currentText != textView.attributedString(), !textView.inputView.hasMarkedText() { + item.currentText = textView.attributedString() + } + } + } + +// func textViewDidPaste(_ pasteboard: NSPasteboard) -> Bool { +// if let item = item as? InputDataRowItem, let string = pasteboard.string(forType: .string) { +// let updated = item.filter(string) +// if updated == string { +// return false +// } else { +// NSSound.beep() +// shakeView() +// } +// } +// return true +// } + + override var backdorColor: NSColor { + if let item = item as? GeneralRowItem, let customTheme = item.customTheme { + return customTheme.backgroundColor + } + return theme.colors.background + } + + var textColor: NSColor { + if let item = item as? GeneralRowItem, let customTheme = item.customTheme { + return customTheme.textColor + } + return theme.colors.text + } + var linkColor: NSColor { + if let item = item as? GeneralRowItem, let customTheme = item.customTheme { + return customTheme.accentColor + } + return theme.colors.accent + } + var grayText: NSColor { + if let item = item as? GeneralRowItem, let customTheme = item.customTheme { + return customTheme.grayTextColor + } + return theme.colors.grayText + } + var borderColor: NSColor { + if let item = item as? GeneralRowItem, let customTheme = item.customTheme { + return customTheme.borderColor + } + return theme.colors.border + } + + var indicatorColor: NSColor { + if let item = item as? GeneralRowItem, let customTheme = item.customTheme { + return customTheme.indicatorColor + } + return theme.colors.indicatorColor + } + + override func updateColors() { + placeholderTextView.backgroundColor = backdorColor + textView.cursorColor = theme.colors.indicatorColor + textView.textFont = .normal(.text) + textView.textColor = textColor + textView.linkColor = linkColor + + textView.setBackgroundColor(backdorColor) + secureField.font = .normal(13) + secureField.backgroundColor = backdorColor + secureField.textColor = textColor + separator.backgroundColor = borderColor + containerView.backgroundColor = backdorColor + loadingView?.progressColor = grayText + guard let item = item as? InputDataRowItem else { + return + } + self.background = item.viewType.rowBackground + } + + + override var mouseInsideField: Bool { + return secureField._mouseInside() || textView._mouseInside() + } + + override func hitTest(_ point: NSPoint) -> NSView? { +// switch true { +// case NSPointInRect(convert(point, from: superview), secureField.frame): +// return secureField +// case NSPointInRect(convert(point, from: superview), textView.frame): +// return textView +// default: + return super.hitTest(point) + //} + } + + override var firstResponder: NSResponder? { + if let item = item as? InputDataRowItem { + switch item.mode { + case .plain: + return textView.inputView + case .secure: + return secureField + } + } + return super.firstResponder + } + + func showPlaceholderActionTooltip(_ text: String) -> Void { + if let placeholderAction = placeholderAction { + tooltip(for: placeholderAction, text: text) + } + } + + override func set(item: TableRowItem, animated: Bool) { + + guard let item = item as? InputDataRowItem else {return} + + self.textView.animates = false + super.set(item: item, animated: animated) + self.textView.animates = true + + placeholderTextView.isHidden = item.placeholderLayout == nil + placeholderTextView.update(item.placeholderLayout) + + let containerRect: NSRect + switch item.viewType { + case .legacy: + containerRect = self.bounds + case .modern: + containerRect = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, item.height - item.inset.bottom - item.inset.top) + } + containerView.change(size: containerRect.size, animated: animated, corners: item.viewType.corners) + containerView.change(pos: containerRect.origin, animated: animated) + + if let rightItem = item.rightItem { + switch rightItem { + case let .action(image, action): + rightActionView.set(image: image, for: .Normal) + _ = rightActionView.sizeToFit() + rightActionView.isHidden = false + loadingView?.removeFromSuperview() + loadingView = nil + rightActionView.removeAllHandlers() + switch action { + case .none: + rightActionView.userInteractionEnabled = false + rightActionView.autohighlight = false + case .resort: + rightActionView.userInteractionEnabled = false + rightActionView.autohighlight = false + case .clearText: + rightActionView.userInteractionEnabled = true + rightActionView.autohighlight = true + rightActionView.set(handler: { [weak self] _ in + self?.secureField.stringValue = "" + self?.textView.setString("") + }, for: .Click) + case let .custom(action): + rightActionView.userInteractionEnabled = true + rightActionView.autohighlight = true + rightActionView.set(handler: { control in + action(item, control) + }, for: .Click) + } + case .loading: + if loadingView == nil { + loadingView = ProgressIndicator(frame: NSMakeRect(0, 0, 18, 18)) + loadingView?.progressColor = self.grayText + containerView.addSubview(loadingView!) + } + rightActionView.isHidden = true + } + } else { + rightActionView.isHidden = true + loadingView?.removeFromSuperview() + loadingView = nil + } + + if let placeholder = item.placeholder { + if let icon = placeholder.icon { + if placeholderAction == nil { + self.placeholderAction = ImageButton() + containerView.addSubview(self.placeholderAction!) + if animated { + placeholderAction!.layer?.animateAlpha(from: 0, to: 2, duration: 0.2) + separator.change(size: separatorFrame.size, animated: animated) + separator.change(pos: separatorFrame.origin, animated: animated) + } + + switch item.viewType { + case .legacy: + textView._change(pos: NSMakePoint(item.inset.left + item.textFieldLeftInset - 3, 6), animated: animated) + case let .modern(_, innerInsets): + let textX = innerInsets.left + item.textFieldLeftInset - 3 + if item.realInputHeight <= 16 { + textView._change(pos: NSMakePoint(textX, textView.frame.minY), animated: animated) + } else { + textView._change(pos: NSMakePoint(textX, textView.frame.minY), animated: animated) + } + } + + } + guard let placeholderAction = self.placeholderAction else { + return + } + placeholderAction.set(image: icon, for: .Normal) + placeholderAction.set(image: icon, for: .Highlight) + placeholderAction.set(image: icon, for: .Hover) + _ = placeholderAction.sizeToFit() + placeholderAction.removeAllHandlers() + placeholderAction.set(handler: { _ in + placeholder.action?() + }, for: .SingleClick) + } else { + if animated { + if let placeholderAction = placeholderAction { + self.placeholderAction = nil + placeholderAction.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak placeholderAction] _ in + placeholderAction?.removeFromSuperview() + }) + separator.change(size: separatorFrame.size, animated: animated) + separator.change(pos: separatorFrame.origin, animated: animated) + } + } else { + placeholderAction?.removeFromSuperview() + placeholderAction = nil + } + + switch item.viewType { + case .legacy: + textView._change(pos: NSMakePoint(item.inset.left + item.textFieldLeftInset - 3, 6), animated: animated) + case let .modern(_, innerInsets): + let textX = innerInsets.left + item.textFieldLeftInset - 3 + if item.realInputHeight <= 16 { + textView._change(pos: NSMakePoint(textX, textView.frame.minY), animated: animated) + } else { + textView._change(pos: NSMakePoint(textX, textView.frame.minY), animated: animated) + } + } + } + + if placeholder.hasLimitationText { + textLimitation.isHidden = item.currentText.length < item.limit / 3 * 2 + textLimitation.attributedString = .initialize(string: "\(item.limit - Int32(item.currentText.length))", color: self.grayText, font: .normal(.small)) + } else { + textLimitation.isHidden = true + } + } else { + if animated { + if let placeholderAction = placeholderAction { + self.placeholderAction = nil + placeholderAction.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak placeholderAction] _ in + placeholderAction?.removeFromSuperview() + }) + separator.change(size: separatorFrame.size, animated: animated) + separator.change(pos: separatorFrame.origin, animated: animated) + } + } else { + placeholderAction?.removeFromSuperview() + placeholderAction = nil + } + + switch item.viewType { + case .legacy: + textView._change(pos: NSMakePoint(item.inset.left + item.textFieldLeftInset - 3, 6), animated: animated) + case let .modern(_, innerInsets): + let textX = innerInsets.left + item.textFieldLeftInset - 3 + if item.realInputHeight <= 16 { + textView._change(pos: NSMakePoint(textX, textView.frame.minY), animated: animated) + } else { + textView._change(pos: NSMakePoint(textX, textView.frame.minY), animated: animated) + } + } + } + + switch item.mode { + case .plain: + secureField.isHidden = true + textView.isHidden = false + textView.animates = false + textView.setPlaceholderAttributedString(item.inputPlaceholder, update: false) + if item.currentAttributed != textView.attributedString() { + textView.setAttributedString(item.currentAttributed, animated: false) + } + textView.update(false) + textView.needsDisplay = true + textView.animates = true + case .secure: + secureField.placeholderAttributedString = item.inputPlaceholder + secureField.isHidden = false + + textView.isHidden = true + if item.currentText.string != secureField.stringValue { + secureField.stringValue = item.currentText.string + } + } + + containerView.needsDisplay = true + self.needsLayout = true + + } +} diff --git a/Telegram-Mac/InputFormatterPopover.swift b/Telegram-Mac/InputFormatterPopover.swift new file mode 100644 index 0000000000..2080752a06 --- /dev/null +++ b/Telegram-Mac/InputFormatterPopover.swift @@ -0,0 +1,214 @@ +// +// InputFormatterPopover.swift +// Telegram +// +// Created by keepcoder on 27/10/2017. +// Copyright © 2017 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + + +private enum InputFormatterViewState { + case normal + case link +} +private class InputFormatterView : NSView { + let link: TitleButton = TitleButton() + + let linkField: NSTextField = NSTextField(frame: NSMakeRect(0, 0, 30, 18)) + let dismissLink:ImageButton = ImageButton() + + fileprivate var state: InputFormatterViewState = .link + + required override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(linkField) + addSubview(link) + addSubview(dismissLink) + + dismissLink.set(image: theme.icons.recentDismiss, for: .Normal) + _ = dismissLink.sizeToFit() + + // linkField.placeholderAttributedString = NSAttributedString.initialize(string: L10n.inputFormatterSetLink, color: theme.colors.grayText, font: .normal(.text)) + linkField.font = .normal(.text) + linkField.wantsLayer = true + linkField.isEditable = true + linkField.isSelectable = true + linkField.maximumNumberOfLines = 1 + linkField.backgroundColor = .clear + linkField.drawsBackground = false + linkField.isBezeled = false + linkField.isBordered = false + linkField.focusRingType = .none + + + // linkField.delegate = self + + link.set(handler: { [weak self] _ in + self?.change(state: .link, animated: true) + }, for: .Click) + + + + + dismissLink.centerY(x: frame.width - 10 - dismissLink.frame.width) + dismissLink.isHidden = true + + change(state: .link, animated: false) + } + + func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { + + if commandSelector == #selector(insertNewline(_:)) { + + return true + } + + return false + } + + func change(state: InputFormatterViewState, animated: Bool) { + self.state = state + switch state { + case .normal: + link.isHidden = false + link._change(opacity: 1.0, animated: animated) + dismissLink._change(opacity: 0, animated: animated, completion: { [weak self] completed in + if completed { + self?.dismissLink.isHidden = true + } + }) + linkField._change(opacity: 0, animated: animated, completion: { [weak self] completed in + if completed { + self?.linkField.isHidden = true + } + }) + case .link: + linkField.isHidden = false + dismissLink.isHidden = false + linkField._change(opacity: 1.0, animated: animated) + dismissLink._change(opacity: 1.0, animated: animated) + + link._change(opacity: 0, animated: animated, completion: { [weak self] completed in + if completed { + self?.link.isHidden = true + } + }) + + } + } + + override func layout() { + super.layout() + + linkField.setFrameSize(frame.width - link.frame.width - 10, 18) + linkField.centerY(x: 10) + linkField.textView?.frame = linkField.bounds + switch state { + case .normal: + link.centerY(x: 0) + case .link: + link.centerY(x: frame.width - link.frame.width) + } + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + +final class InputFormatterArguments { + let link:(String)->Void + init(link:@escaping(String)->Void) { + self.link = link + } +} + +private final class FormatterViewController : NSViewController { + + override var acceptsFirstResponder: Bool { + return true + } +} + +class InputFormatterPopover: NSPopover { + + + + private let window: Window + init(_ arguments: InputFormatterArguments, window: Window) { + self.window = window + super.init() + let controller = FormatterViewController() + let view = InputFormatterView(frame: NSMakeRect(0, 0, 240, 40)) + + + + + controller.view = view + + view.dismissLink.set(handler: { [weak self] _ in + self?.close() + }, for: .Click) + + self.contentViewController = controller + + window.set(handler: { [weak view] _ -> KeyHandlerResult in + if let view = view { + if view.state == .link { + let attr = view.linkField.attributedStringValue.mutableCopy() as! NSMutableAttributedString + + attr.detectLinks(type: [.Links]) + + var url:String? = nil + + attr.enumerateAttribute(NSAttributedString.Key.link, in: attr.range, options: NSAttributedString.EnumerationOptions(rawValue: 0), using: { (value, range, stop) in + + if let value = value as? inAppLink { + switch value { + case let .external(link, _): + url = link + break + default: + break + } + } + + let s: ObjCBool = (url != nil) ? true : false + stop.pointee = s + + }) + + if let url = url { + arguments.link(url) + } else { + view.shake() + } + + } + return .invoked + } + return .rejected + }, with: self, for: .Return, priority: .modal) + + window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.close() + return .invoked + }, with: self, for: .Escape, priority: .modal) + + + } + + deinit { + window.removeAllHandlers(for: self) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/Telegram-Mac/InputPasswordController.swift b/Telegram-Mac/InputPasswordController.swift new file mode 100644 index 0000000000..ccf62112dd --- /dev/null +++ b/Telegram-Mac/InputPasswordController.swift @@ -0,0 +1,141 @@ +// +// InputPasswordController.swift +// Telegram +// +// Created by Mikhail Filimonov on 06/06/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import SwiftSignalKit +import TelegramCore + +import TGUIKit + +enum InputPasswordValueError { + case generic + case wrong +} + + +private struct InputPasswordState : Equatable { + let error: InputDataValueError? + let value: InputDataValue + let isLoading: Bool + init(value: InputDataValue, error: InputDataValueError?, isLoading: Bool) { + self.value = value + self.error = error + self.isLoading = isLoading + } + + func withUpdatedError(_ error: InputDataValueError?) -> InputPasswordState { + return InputPasswordState(value: self.value, error: error, isLoading: self.isLoading) + } + func withUpdatedValue(_ value: InputDataValue) -> InputPasswordState { + return InputPasswordState(value: value, error: self.error, isLoading: self.isLoading) + } + func withUpdatedLoading(_ isLoading: Bool) -> InputPasswordState { + return InputPasswordState(value: self.value, error: self.error, isLoading: isLoading) + } +} + +private let _id_input_pwd:InputDataIdentifier = InputDataIdentifier("_id_input_pwd") + +private func inputPasswordEntries(state: InputPasswordState, desc:String) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + var sectionId:Int32 = 0 + var index:Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + entries.append(.input(sectionId: sectionId, index: index, value: state.value, error: state.error, identifier: _id_input_pwd, mode: .secure, data: InputDataRowData(viewType: .singleItem), placeholder: nil, inputPlaceholder: L10n.inputPasswordControllerPlaceholder, filter: { $0 }, limit: 255)) + index += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(desc), data: InputDataGeneralTextData(detectBold: false, viewType: .textBottomItem))) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func InputPasswordController(context: AccountContext, title: String, desc: String, checker:@escaping(String)->Signal) -> InputDataModalController { + + let initialState: InputPasswordState = InputPasswordState(value: .string(nil), error: nil, isLoading: false) + let stateValue: Atomic = Atomic(value: initialState) + let statePromise:ValuePromise = ValuePromise(initialState, ignoreRepeated: true) + + let updateState:(_ f:(InputPasswordState)->InputPasswordState) -> Void = { f in + statePromise.set(stateValue.modify(f)) + } + + let dataSignal = statePromise.get() |> map { state in + return inputPasswordEntries(state: state, desc: desc) + } |> map { entries in + return InputDataSignalValue(entries: entries) + } + + let checkPassword = MetaDisposable() + + var dismiss:(()->Void)? + + let controller = InputDataController(dataSignal: dataSignal, title: title, validateData: { data in + return .fail(.doSomething { f in + if let pwd = data[_id_input_pwd]?.stringValue, !stateValue.with({$0.isLoading}) { + updateState { + return $0.withUpdatedLoading(true) + } + checkPassword.set(showModalProgress(signal: checker(pwd), for: context.window).start(error: { error in + let text: String + switch error { + case .wrong: + text = L10n.inputPasswordControllerErrorWrongPassword + case .generic: + text = L10n.unknownError + } + updateState { + return $0.withUpdatedLoading(false).withUpdatedError(InputDataValueError(description: text, target: .data)) + } + f(.fail(.fields([_id_input_pwd : .shake]))) + }, completed: { + updateState { + return $0.withUpdatedLoading(false) + } + dismiss?() + })) + } + }) + }, updateDatas: { data in + updateState { + return $0.withUpdatedValue(data[_id_input_pwd]!).withUpdatedError(nil) + } + return .fail(.none) + }, afterDisappear: { + checkPassword.dispose() + }, hasDone: true) + + let interactions = ModalInteractions(acceptTitle: L10n.navigationDone, accept: { [weak controller] in + + controller?.validateInputValues() + + }, drawBorder: true, height: 50, singleButton: true) + + controller.getBackgroundColor = { + theme.colors.listBackground + } + + let modalController = InputDataModalController(controller, modalInteractions: interactions, size: NSMakeSize(300, 300)) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) + + dismiss = { [weak modalController] in + modalController?.close() + } + + return modalController +} diff --git a/Telegram-Mac/InputPasteboardParser.swift b/Telegram-Mac/InputPasteboardParser.swift index 37859547df..c80905be4b 100644 --- a/Telegram-Mac/InputPasteboardParser.swift +++ b/Telegram-Mac/InputPasteboardParser.swift @@ -7,15 +7,76 @@ // import Cocoa -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac +import TelegramCore + +import SwiftSignalKit +import Postbox import TGUIKit + class InputPasteboardParser: NSObject { + public class func getPasteboardUrls(_ pasteboard: NSPasteboard) -> Signal<[URL], NoError> { + let items = pasteboard.pasteboardItems + + if let items = items, !items.isEmpty { + var files:[URL] = [] + + for item in items { + let path = item.string(forType: NSPasteboard.PasteboardType(rawValue: "public.file-url")) + if let path = path, let url = URL(string: path) { + files.append(url) + } +// if let type = item.availableType(from: [.html]), let data = item.data(forType: type) { +// let attributed = NSAttributedString(html: data, documentAttributes: nil) +// if let attributed = attributed, let attachment = attributed.attribute(.attachment, at: 0, effectiveRange: nil) as? NSTextAttachment { +// +// if let fileWrapper = attachment.fileWrapper, let fileName = fileWrapper.preferredFilename, fileWrapper.isRegularFile { +// if let data = fileWrapper.regularFileContents { +// let url = URL(fileURLWithPath: NSTemporaryDirectory() + "\(arc4random())_" + fileName) +// do { +// try data.write(to: url) +// files.append(url) +// } catch { +// +// } +// +// } +// } +// } +// } + } + + var image:NSImage? = nil + + if files.isEmpty { + if let images = pasteboard.readObjects(forClasses: [NSImage.self], options: nil) as? [NSImage], !images.isEmpty { + image = images[0] + } + } + + + files = files.filter { path -> Bool in + if let size = fs(path.path) { + return size <= 2000 * 1024 * 1024 + } + + return false + } + + + if !files.isEmpty { + return .single(files) + } else if let image = image { + return putToTemp(image: image, compress: false) |> map {[URL(fileURLWithPath: $0)]} |> deliverOnMainQueue + } + + } + + return .single([]) + } - public class func proccess(pasteboard:NSPasteboard, account:Account, chatInteraction:ChatInteraction, window:Window) -> Bool { + public class func canProccessPasteboard(_ pasteboard:NSPasteboard) -> Bool { let items = pasteboard.pasteboardItems if let items = items, !items.isEmpty { @@ -38,7 +99,7 @@ class InputPasteboardParser: NSObject { } - if let _ = items[0].types.index(of: NSPasteboard.PasteboardType(rawValue: "com.apple.traditional-mac-plain-text")) { + if let _ = items[0].types.firstIndex(of: NSPasteboard.PasteboardType(rawValue: "com.apple.traditional-mac-plain-text")) { return true } @@ -46,7 +107,7 @@ class InputPasteboardParser: NSObject { files = files.filter { path -> Bool in if let size = fileSize(path.path) { - return size <= 1500000000 + return size <= 2000 * 1024 * 1024 } return false @@ -55,28 +116,126 @@ class InputPasteboardParser: NSObject { let afterSizeCheck = files.count if afterSizeCheck == 0 && previous != afterSizeCheck { - alert(for: mainWindow, header: appName, info: tr(.appMaxFileSize)) return false } - if let peer = chatInteraction.presentation.peer, peer.mediaRestricted { - alertForMediaRestriction(peer) + if !files.isEmpty { + + return false + } else if let _ = image { return false } - if !files.isEmpty { - showModal(with:PreviewSenderController(urls: files, account:account, chatInteraction:chatInteraction), for:window) + } + + return true + } + + public class func proccess(pasteboard:NSPasteboard, chatInteraction:ChatInteraction, window:Window) -> Bool { + let items = pasteboard.pasteboardItems + + + if let items = items, !items.isEmpty { + var files:[URL] = [] + + for item in items { + let path = item.string(forType: NSPasteboard.PasteboardType(rawValue: "public.file-url")) + if let path = path, let url = URL(string: path) { + files.append(url) + } +// if let type = item.availableType(from: [.html]), let data = item.data(forType: type) { +// let attributed = NSAttributedString(html: data, documentAttributes: nil) +// if let attributed = attributed, let attachment = attributed.attribute(.attachment, at: 0, effectiveRange: nil) as? NSTextAttachment { +// +// if let fileWrapper = attachment.fileWrapper, let fileName = fileWrapper.preferredFilename, fileWrapper.isRegularFile { +// if let data = fileWrapper.regularFileContents { +// let url = URL(fileURLWithPath: NSTemporaryDirectory() + "\(arc4random())_" + fileName) +// do { +// try data.write(to: url) +// files.append(url) +// } catch { +// +// } +// +// } +// } +// } +// } + + } + + var image:NSImage? = nil + + if files.isEmpty { + + if let images = pasteboard.readObjects(forClasses: [NSImage.self], options: nil) as? [NSImage], !images.isEmpty { + + if let representation = images[0].representations.first as? NSPDFImageRep { + let url = URL(fileURLWithPath: NSTemporaryDirectory() + "ios_scan_\(arc4random()).pdf") + + try? representation.pdfRepresentation.write(to: url) + + files.append(url) + image = nil + } else { + image = images[0] + } + } + } + + + if let _ = items[0].types.firstIndex(of: NSPasteboard.PasteboardType(rawValue: "com.microsoft.appbundleid")) { + return true + } + + let previous = files.count + + files = files.filter { path -> Bool in + if let size = fileSize(path.path) { + return size <= 2000 * 1024 * 1024 + } + return false + } + + let afterSizeCheck = files.count + + if afterSizeCheck == 0 && previous != afterSizeCheck { + alert(for: mainWindow, info: L10n.appMaxFileSize1) + return false + } + if let peer = chatInteraction.presentation.peer, let permissionText = permissionText(from: peer, for: .banSendMedia) { + if !files.isEmpty || image != nil { + alert(for: mainWindow, info: permissionText) + return false + } + } + + if files.count == 1, let editState = chatInteraction.presentation.interfaceState.editState, editState.canEditMedia { + _ = (Sender.generateMedia(for: MediaSenderContainer(path: files[0].path, isFile: false), account: chatInteraction.context.account, isSecretRelated: chatInteraction.peerId.namespace == Namespaces.Peer.SecretChat) |> deliverOnMainQueue).start(next: { [weak chatInteraction] media, _ in + chatInteraction?.update({$0.updatedInterfaceState({$0.updatedEditState({$0?.withUpdatedMedia(media)})})}) + }) + return false + } else if let image = image, let editState = chatInteraction.presentation.interfaceState.editState, editState.canEditMedia { + _ = (putToTemp(image: image) |> mapToSignal {Sender.generateMedia(for: MediaSenderContainer(path: $0, isFile: false), account: chatInteraction.context.account, isSecretRelated: chatInteraction.peerId.namespace == Namespaces.Peer.SecretChat) } |> deliverOnMainQueue).start(next: { [weak chatInteraction] media, _ in + chatInteraction?.update({$0.updatedInterfaceState({$0.updatedEditState({$0?.withUpdatedMedia(media)})})}) + }) + return false + } + + if !files.isEmpty { + chatInteraction.showPreviewSender(files, true, nil) return false } else if let image = image { _ = (putToTemp(image: image, compress: false) |> deliverOnMainQueue).start(next: { (path) in - showModal(with:PreviewSenderController(urls: [URL(fileURLWithPath: path)], account:account, chatInteraction:chatInteraction), for:window) + chatInteraction.showPreviewSender([URL(fileURLWithPath: path)], true, nil) }) return false } } + return true } diff --git a/Telegram-Mac/InputSources.swift b/Telegram-Mac/InputSources.swift new file mode 100644 index 0000000000..cc4c775eb9 --- /dev/null +++ b/Telegram-Mac/InputSources.swift @@ -0,0 +1,64 @@ +// +// InputSources.swift +// Telegram +// +// Created by Mikhail Filimonov on 18/03/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import SwiftSignalKit +import Foundation +import TelegramCore + +import Postbox + + +final class InputSources: NSObject { + + private let _inputSource: Promise<[String]> = Promise(["en"]) + + var value: Signal<[String], NoError> { + _inputSource.set(Signal { subscriber in + subscriber.putNext(currentAppInputSource().uniqueElements) + subscriber.putCompletion() + return EmptyDisposable + } |> runOn(.mainQueue())) + return _inputSource.get() |> distinctUntilChanged(isEqual: { $0 == $1 }) + } + + func searchEmoji(postbox: Postbox, engine: TelegramEngine, sharedContext: SharedAccountContext, query: String, completeMatch: Bool, checkPrediction: Bool) -> Signal<[String], NoError> { + return combineLatest(value, baseAppSettings(accountManager: sharedContext.accountManager)) |> mapToSignal { sources, settings in + if settings.predictEmoji || !checkPrediction { + return combineLatest(sources.map({ engine.stickers.searchEmojiKeywords(inputLanguageCode: $0, query: query.lowercased(), completeMatch: completeMatch) })) |> map { results in + return results.reduce([], { $0 + $1 }).reduce([], { current, value -> [String] in + if completeMatch { + if query.lowercased() == value.keyword.lowercased() { + return current + value.emoticons + } else { + return current + } + } else { + return current + value.emoticons + } + }).uniqueElements.map { $0.fixed } + } |> distinctUntilChanged + } else { + return .single([]) + } + } + } + + + override init() { + super.init() + NotificationCenter.default.addObserver(self, selector: #selector(inputSourceChanged), name: NSTextInputContext.keyboardSelectionDidChangeNotification, object: nil) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + @objc private func inputSourceChanged() { + _inputSource.set(.single(currentAppInputSource().uniqueElements)) + } +} diff --git a/Telegram-Mac/InputURLFormatterModalController.swift b/Telegram-Mac/InputURLFormatterModalController.swift new file mode 100644 index 0000000000..6aed0b11ea --- /dev/null +++ b/Telegram-Mac/InputURLFormatterModalController.swift @@ -0,0 +1,146 @@ +// +// InputURLFormatterModalController.swift +// Telegram +// +// Created by Mikhail Filimonov on 12/03/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit + +private struct InputURLFormatterState : Equatable { + let text: String + let url: String? + init(text: String, url: String?) { + self.text = text + self.url = url + } + + func withUpdatedUrl(_ url: String?) -> InputURLFormatterState { + return InputURLFormatterState(text: self.text, url: url) + } +} + +private let _id_input_url = InputDataIdentifier("_id_input_url") + +private func inputURLFormatterEntries(state: InputURLFormatterState) -> [InputDataEntry] { + var entries: [InputDataEntry] = [] + + var sectionId: Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.inputFormatterTextHeader), data: InputDataGeneralTextData(color: theme.colors.text, viewType: .textTopItem))) + index += 1 + + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("_id_text"), equatable: nil, comparable: nil, item: { initialSize, stableId in + return GeneralBlockTextRowItem(initialSize, stableId: stableId, viewType: .singleItem, text: state.text, font: .normal(.text)) + })) + index += 1 + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.inputFormatterURLHeader), data: InputDataGeneralTextData(color: theme.colors.text, viewType: .textTopItem))) + index += 1 + + entries.append(.input(sectionId: sectionId, index: index, value: .string(state.url), error: nil, identifier: _id_input_url, mode: .plain, data: InputDataRowData( viewType: .singleItem), placeholder: nil, inputPlaceholder: L10n.inputFormatterURLHeader, filter: { $0 }, limit: 10000)) + index += 1 + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func InputURLFormatterModalController(string: String, defaultUrl: String? = nil, completion: @escaping(String?) -> Void) -> InputDataModalController { + + + let initialState = InputURLFormatterState(text: string, url: defaultUrl?.removingPercentEncoding) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((InputURLFormatterState) -> InputURLFormatterState) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let dataSignal = statePromise.get() |> map { state in + return inputURLFormatterEntries(state: state) + } + + var close: (() -> Void)? = nil + + let controller = InputDataController(dataSignal: dataSignal |> map { InputDataSignalValue(entries: $0) }, title: L10n.inputFormatterURLHeader, validateData: { data in + + if let string = data[_id_input_url]?.stringValue { + + let attr = NSMutableAttributedString(string: string) + + attr.detectLinks(type: [.Links]) + + var url:String? = nil + + + attr.enumerateAttribute(NSAttributedString.Key.link, in: attr.range, options: NSAttributedString.EnumerationOptions(rawValue: 0), using: { (value, range, stop) in + + if let value = value as? inAppLink { + switch value { + case let .external(link, _): + url = link + break + default: + break + } + } + + let s: ObjCBool = (url != nil) ? true : false + stop.pointee = s + + }) + + completion(url) + close?() + return .none + + + } + + return .fail(.fields([_id_input_url: .shake])) + + }, updateDatas: { data in + + updateState { + $0.withUpdatedUrl(data[_id_input_url]?.stringValue) + } + + return .none + + }) + + + let modalInteractions = ModalInteractions(acceptTitle: L10n.modalOK, accept: { [weak controller] in + controller?.validateInputValues() + }, drawBorder: true, singleButton: true) + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) + + close = { [weak modalController] in + modalController?.modal?.close() + } + + + + return modalController + +} diff --git a/Telegram-Mac/InstalledStickerPacksController.swift b/Telegram-Mac/InstalledStickerPacksController.swift index 1e96086a6b..10fa374484 100644 --- a/Telegram-Mac/InstalledStickerPacksController.swift +++ b/Telegram-Mac/InstalledStickerPacksController.swift @@ -8,26 +8,43 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac -import PostboxMac -import TelegramCoreMac +import SwiftSignalKit +import Postbox +import TelegramCore + + +enum InstalledStickerPacksEntryTag: ItemListItemTag { + case suggestOptions + case loopAnimatedStickers + + func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? InstalledStickerPacksEntryTag, self == other { + return true + } else { + return false + } + } +} private final class InstalledStickerPacksControllerArguments { - let account: Account + let context: AccountContext let openStickerPack: (StickerPackCollectionInfo) -> Void let removePack: (ItemCollectionId) -> Void let openStickersBot: () -> Void let openFeatured: () -> Void - let openArchived: () -> Void - - init(account: Account, openStickerPack: @escaping (StickerPackCollectionInfo) -> Void, removePack: @escaping (ItemCollectionId) -> Void, openStickersBot: @escaping () -> Void, openFeatured: @escaping () -> Void, openArchived: @escaping () -> Void) { - self.account = account + let openArchived: ([ArchivedStickerPackItem]?) -> Void + let openSuggestionOptions: () -> Void + let toggleLoopAnimated: (Bool)->Void + init(context: AccountContext, openStickerPack: @escaping (StickerPackCollectionInfo) -> Void, removePack: @escaping (ItemCollectionId) -> Void, openStickersBot: @escaping () -> Void, openFeatured: @escaping () -> Void, openArchived: @escaping ([ArchivedStickerPackItem]?) -> Void, openSuggestionOptions: @escaping() -> Void, toggleLoopAnimated: @escaping(Bool)->Void) { + self.context = context self.openStickerPack = openStickerPack self.removePack = removePack self.openStickersBot = openStickersBot self.openFeatured = openFeatured self.openArchived = openArchived + self.openSuggestionOptions = openSuggestionOptions + self.toggleLoopAnimated = toggleLoopAnimated } } @@ -78,107 +95,80 @@ private enum InstalledStickerPacksEntryId: Hashable { } } +private struct ArchivedListContainer : Equatable { + let archived: [ArchivedStickerPackItem]? + static func ==(lhs: ArchivedListContainer, rhs: ArchivedListContainer) -> Bool { + if let lhsItem = lhs.archived, let rhsItem = rhs.archived { + if lhsItem.count != rhsItem.count { + return false + } else { + for i in 0 ..< lhsItem.count { + let lhs = lhsItem[i] + let rhs = rhsItem[i] + if lhs.info != rhs.info { + return false + } + if lhs.topItems != rhs.topItems { + return false + } + } + } + } else if (lhs.archived != nil) != (rhs.archived != nil) { + return false + } + return true + } +} + private enum InstalledStickerPacksEntry: TableItemListNodeEntry { case section(sectionId:Int32) - case trending(sectionId:Int32, Int32) - case archived(sectionId:Int32) - case packsTitle(sectionId:Int32, String) - case pack(sectionId:Int32, Int32, StickerPackCollectionInfo, StickerPackItem?, Int32, Bool, ItemListStickerPackItemEditing) - case packsInfo(sectionId:Int32, String) + case suggestOptions(sectionId: Int32, String, GeneralViewType) + case trending(sectionId:Int32, Int32, GeneralViewType) + case archived(sectionId:Int32, ArchivedListContainer, GeneralViewType) + case loopAnimated(sectionId: Int32, Bool, GeneralViewType) + case packsTitle(sectionId:Int32, String, GeneralViewType) + case pack(sectionId:Int32, Int32, StickerPackCollectionInfo, StickerPackItem?, Int32, Bool, Bool, ItemListStickerPackItemEditing, GeneralViewType) + case packsInfo(sectionId:Int32, String, GeneralViewType) var stableId: InstalledStickerPacksEntryId { switch self { - case .trending: + case .suggestOptions: return .index(0) - case .archived: + case .trending: return .index(1) + case .archived: + return .index(2) + case .loopAnimated: + return .index(4) case .packsTitle: - return .index(3) - case let .pack(_, _, info, _, _, _, _): + return .index(5) + case let .pack(_, _, info, _, _, _, _, _, _): return .pack(info.id) case .packsInfo: - return .index(4) + return .index(6) case let .section(sectionId): return .index((sectionId + 1) * 1000 - sectionId) } } - static func ==(lhs: InstalledStickerPacksEntry, rhs: InstalledStickerPacksEntry) -> Bool { - switch lhs { - case let .trending(sectionId, count): - if case .trending(sectionId, count) = rhs { - return true - } else { - return false - } - case let .archived(sectionId): - if case .archived(sectionId) = rhs { - return true - } else { - return false - } - case let .packsTitle(sectionId, text): - if case .packsTitle(sectionId, text) = rhs { - return true - } else { - return false - } - case let .pack(lhsSectionId, lhsIndex, lhsInfo, lhsTopItem, lhsCount, lhsEnabled, lhsEditing): - if case let .pack(rhsSectionId, rhsIndex, rhsInfo, rhsTopItem, rhsCount, rhsEnabled, rhsEditing) = rhs { - - if lhsSectionId != rhsSectionId { - return false - } - if lhsIndex != rhsIndex { - return false - } - if lhsInfo != rhsInfo { - return false - } - if lhsTopItem != rhsTopItem { - return false - } - if lhsCount != rhsCount { - return false - } - if lhsEnabled != rhsEnabled { - return false - } - if lhsEditing != rhsEditing { - return false - } - return true - } else { - return false - } - case let .packsInfo(sectionId, text): - if case .packsInfo(sectionId, text) = rhs { - return true - } else { - return false - } - case let .section(sectionId): - if case .section(sectionId) = rhs { - return true - } else { - return false - } - } - } var stableIndex:Int32 { switch self { - case .trending: + case .suggestOptions: return 0 - case .archived: + case .trending: return 1 - case .packsTitle: + case .archived: return 2 + case .loopAnimated: + return 3 + case .packsTitle: + return 4 case .pack: fatalError("") case .packsInfo: - return 4 + return 5 case let .section(sectionId): return (sectionId + 1) * 1000 - sectionId } @@ -186,15 +176,19 @@ private enum InstalledStickerPacksEntry: TableItemListNodeEntry { var index:Int32 { switch self { - case let .trending(sectionId, _): + case let .suggestOptions(sectionId, _, _): return (sectionId * 1000) + stableIndex - case let .archived(sectionId): + case let .trending(sectionId, _, _): return (sectionId * 1000) + stableIndex - case let .packsTitle(sectionId, _): + case let .archived(sectionId, _, _): return (sectionId * 1000) + stableIndex - case let .pack( sectionId, index, _, _, _, _, _): + case let .loopAnimated(sectionId, _, _): + return (sectionId * 1000) + stableIndex + case let .packsTitle(sectionId, _, _): + return (sectionId * 1000) + stableIndex + case let .pack( sectionId, index, _, _, _, _, _, _, _): return (sectionId * 1000) + 100 + index - case let .packsInfo(sectionId, _): + case let .packsInfo(sectionId, _, _): return (sectionId * 1000) + stableIndex case let .section(sectionId): return (sectionId + 1) * 1000 - sectionId @@ -207,21 +201,26 @@ private enum InstalledStickerPacksEntry: TableItemListNodeEntry { func item(_ arguments: InstalledStickerPacksControllerArguments, initialSize:NSSize) -> TableRowItem { switch self { - case let .trending(_, count): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.installedStickersTranding), type: .context(stateback: { () -> String in - return count > 0 ? "\(count)" : "" - }), action: { + case let .suggestOptions(_, value, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.stickersSuggestStickers, type: .context(value), viewType: viewType, action: { + arguments.openSuggestionOptions() + }) + case let .trending(_, count, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.installedStickersTranding, type: .context(count > 0 ? "\(count)" : ""), viewType: viewType, action: { arguments.openFeatured() }) - - case .archived: - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.installedStickersArchived), type: .next, action: { - arguments.openArchived() + case let .archived(_, archived, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.installedStickersArchived, type: .next, viewType: viewType, action: { + arguments.openArchived(archived.archived) + }) + case let .loopAnimated(_, value, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.installedStickersLoopAnimated, type: .switchable(value), viewType: viewType, action: { + arguments.toggleLoopAnimated(!value) }) - case let .packsTitle(_, text): - return GeneralTextRowItem(initialSize, stableId: stableId, text: text) - case let .pack(_, _, info, topItem, count, enabled, editing): - return StickerSetTableRowItem(initialSize, account: arguments.account, stableId: stableId, info: info, topItem: topItem, itemCount: count, unread: false, editing: editing, enabled: enabled, control: .none, action: { + case let .packsTitle(_, text, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, viewType: viewType) + case let .pack(_, _, info, topItem, count, enabled, _, editing, viewType): + return StickerSetTableRowItem(initialSize, context: arguments.context, stableId: stableId, info: info, topItem: topItem, itemCount: count, unread: false, editing: editing, enabled: enabled, control: .none, viewType: viewType, action: { arguments.openStickerPack(info) }, addPack: { @@ -229,10 +228,10 @@ private enum InstalledStickerPacksEntry: TableItemListNodeEntry { arguments.removePack(info.id) }) - case let .packsInfo(_, text): - return GeneralTextRowItem(initialSize, stableId: stableId, height: 20, text: text) + case let .packsInfo(_, text, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, viewType: viewType) case .section: - return GeneralRowItem(initialSize, height: 20, stableId: stableId) + return GeneralRowItem(initialSize, height: 30, stableId: stableId, viewType: .separator) } } } @@ -267,7 +266,7 @@ private struct InstalledStickerPacksControllerState: Equatable { } -private func installedStickerPacksControllerEntries(state: InstalledStickerPacksControllerState, view: CombinedView, featured: [FeaturedStickerPackItem]) -> [InstalledStickerPacksEntry] { +private func installedStickerPacksControllerEntries(state: InstalledStickerPacksControllerState, autoplayMedia: AutoplayMediaPreferences, stickerSettings: StickerSettings, view: CombinedView, featured: [FeaturedStickerPackItem], archived: [ArchivedStickerPackItem]?) -> [InstalledStickerPacksEntry] { var entries: [InstalledStickerPacksEntry] = [] var sectionId:Int32 = 1 @@ -275,6 +274,17 @@ private func installedStickerPacksControllerEntries(state: InstalledStickerPacks entries.append(.section(sectionId: sectionId)) sectionId += 1 + let suggestString: String + switch stickerSettings.emojiStickerSuggestionMode { + case .none: + suggestString = L10n.stickersSuggestNone + case .all: + suggestString = L10n.stickersSuggestAll + case .installed: + suggestString = L10n.stickersSuggestAdded + } + entries.append(.suggestOptions(sectionId: sectionId, suggestString, .firstItem)) + if featured.count != 0 { var unreadCount: Int32 = 0 for item in featured { @@ -282,29 +292,30 @@ private func installedStickerPacksControllerEntries(state: InstalledStickerPacks unreadCount += 1 } } - entries.append(.trending(sectionId: sectionId, unreadCount)) + entries.append(.trending(sectionId: sectionId, unreadCount, .innerItem)) } - entries.append(.archived(sectionId: sectionId)) + entries.append(.archived(sectionId: sectionId, ArchivedListContainer(archived: archived), .innerItem)) + entries.append(.loopAnimated(sectionId: sectionId, autoplayMedia.loopAnimatedStickers, .lastItem)) entries.append(.section(sectionId: sectionId)) sectionId += 1 - entries.append(.packsTitle(sectionId: sectionId, tr(.installedStickersPacksTitle))) + entries.append(.packsTitle(sectionId: sectionId, L10n.installedStickersPacksTitle, .textTopItem)) if let stickerPacksView = view.views[.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])] as? ItemCollectionInfosView { if let packsEntries = stickerPacksView.entriesByNamespace[Namespaces.ItemCollection.CloudStickerPacks] { var index: Int32 = 0 for entry in packsEntries { if let info = entry.info as? StickerPackCollectionInfo { - entries.append(.pack(sectionId: sectionId, index, info, entry.firstItem as? StickerPackItem, info.count == 0 ? entry.count : info.count, true, ItemListStickerPackItemEditing(editable: true, editing: state.editing))) + let viewType: GeneralViewType = bestGeneralViewType(packsEntries, for: entry) + + entries.append(.pack(sectionId: sectionId, index, info, entry.firstItem as? StickerPackItem, info.count == 0 ? entry.count : info.count, true, autoplayMedia.loopAnimatedStickers, ItemListStickerPackItemEditing(editable: true, editing: state.editing), viewType)) index += 1 } } } } - entries.append(.section(sectionId: sectionId)) - sectionId += 1 - entries.append(.packsInfo(sectionId: sectionId, tr(.installedStickersDescrpiption))) + entries.append(.packsInfo(sectionId: sectionId, L10n.installedStickersDescrpiption, .textBottomItem)) entries.append(.section(sectionId: sectionId)) sectionId += 1 return entries @@ -319,63 +330,223 @@ private func prepareTransition(left:[AppearanceWrapperEntry InstalledStickerPacksControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } + let archivedPromise = Promise<[ArchivedStickerPackItem]?>() + archivedPromise.set(.single(nil) |> then(context.engine.stickers.archivedStickerPacks() |> map(Optional.init))) + + let actionsDisposable = DisposableSet() let resolveDisposable = MetaDisposable() actionsDisposable.add(resolveDisposable) - let arguments = InstalledStickerPacksControllerArguments(account: account, openStickerPack: { info in - showModal(with: StickersPackPreviewModalController(account, peerId: nil, reference: .name(info.shortName)), for: mainWindow) + let arguments = InstalledStickerPacksControllerArguments(context: context, openStickerPack: { info in + showModal(with: StickerPackPreviewModalController(context, peerId: nil, reference: .name(info.shortName)), for: context.window) }, removePack: { id in - confirm(for: mainWindow, with: appName, and: tr(.installedStickersRemoveDescription), okTitle: tr(.installedStickersRemoveDelete), successHandler: { result in + confirm(for: context.window, information: tr(L10n.installedStickersRemoveDescription), okTitle: tr(L10n.installedStickersRemoveDelete), successHandler: { result in switch result { case .basic: - _ = removeStickerPackInteractively(postbox: account.postbox, id: id).start() + _ = context.engine.stickers.removeStickerPackInteractively(id: id, option: .archive).start() case .thrid: break } }) }, openStickersBot: { - resolveDisposable.set((resolvePeerByName(account: account, name: "stickers") |> deliverOnMainQueue).start(next: { peerId in + resolveDisposable.set((context.engine.peers.resolvePeerByName(name: "stickers") |> deliverOnMainQueue).start(next: { peerId in if let peerId = peerId { // navigateToChatControllerImpl?(peerId) } })) }, openFeatured: { [weak self] in - self?.navigationController?.push(FeaturedStickerPacksController(account)) - }, openArchived: { [weak self] in - self?.navigationController?.push(ArchivedStickerPacksController(account)) + self?.navigationController?.push(FeaturedStickerPacksController(context)) + }, openArchived: { [weak self] archived in + self?.navigationController?.push(ArchivedStickerPacksController(context, archived: archived, updatedPacks: { packs in + archivedPromise.set(.single(packs)) + })) + }, openSuggestionOptions: { [weak self] in + self?.openSuggestionOptions() + }, toggleLoopAnimated: { value in + _ = updateAutoplayMediaSettingsInteractively(postbox: context.account.postbox, { + $0.withUpdatedLoopAnimatedStickers(value) + }).start() }) - let stickerPacks = Promise() - stickerPacks.set(account.postbox.combinedView(keys: [.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])])) + let stickerPacks = context.account.postbox.combinedView(keys: [.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])]) + + let featured = context.account.viewTracker.featuredStickerPacks() - let featured = Promise<[FeaturedStickerPackItem]>() - featured.set(account.viewTracker.featuredStickerPacks()) + + let stickerSettingsKey = ApplicationSpecificPreferencesKeys.stickerSettings + let autoplayKey = ApplicationSpecificPreferencesKeys.autoplayMedia + let preferencesKey: PostboxViewKey = .preferences(keys: Set([stickerSettingsKey, autoplayKey])) + let preferencesView = context.account.postbox.combinedView(keys: [preferencesKey]) let previousEntries:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) let initialSize = self.atomicSize - genericView.merge(with: combineLatest(statePromise.get() |> deliverOnMainQueue, stickerPacks.get() |> deliverOnMainQueue, featured.get() |> deliverOnMainQueue, appearanceSignal) - |> map { state, view, featured, appearance -> TableUpdateTransition in - let entries = installedStickerPacksControllerEntries(state: state, view: view, featured: featured).map {AppearanceWrapperEntry(entry: $0, appearance: appearance)} + + let signal = combineLatest(queue: prepareQueue, statePromise.get(), stickerPacks, featured, archivedPromise.get(), appearanceSignal, preferencesView) + |> map { state, view, featured, archived, appearance, preferencesView -> TableUpdateTransition in + + var stickerSettings = StickerSettings.defaultSettings + if let view = preferencesView.views[preferencesKey] as? PreferencesView { + if let value = view.values[stickerSettingsKey] as? StickerSettings { + stickerSettings = value + } + } + + var autoplayMedia = AutoplayMediaPreferences.defaultSettings + if let view = preferencesView.views[preferencesKey] as? PreferencesView { + if let value = view.values[autoplayKey] as? AutoplayMediaPreferences { + autoplayMedia = value + } + } + + let entries = installedStickerPacksControllerEntries(state: state, autoplayMedia: autoplayMedia, stickerSettings: stickerSettings, view: view, featured: featured, archived: archived).map {AppearanceWrapperEntry(entry: $0, appearance: appearance)} return prepareTransition(left: previousEntries.swap(entries), right: entries, initialSize: initialSize.modify({$0}), arguments: arguments) - } |> afterDisposed { - actionsDisposable.dispose() - } ) - readyOnce() + } |> afterDisposed { + actionsDisposable.dispose() + } |> deliverOnMainQueue + + + disposbale.set(signal.start(next: { [weak self] transition in + guard let `self` = self else {return} + + self.genericView.merge(with: transition) + + self.readyOnce() + + if !transition.isEmpty { + var start: Int? = nil + var length: Int = 0 + self.genericView.enumerateItems(with: { item -> Bool in + if item is StickerSetTableRowItem { + if start == nil { + start = item.index + } + length += 1 + } else if start != nil { + return false + } + return true + }) + if let start = start { + self.genericView.resortController = TableResortController(resortRange: NSMakeRange(start, length), startTimeout: 0.2, start: { _ in }, resort: { _ in }, complete: { fromIndex, toIndex in + + + if fromIndex == toIndex { + return + } + + let entries = previousEntries.with {$0}.map( {$0.entry }) + + + let fromEntry = entries[fromIndex] + guard case let .pack(_, _, fromPackInfo, _, _, _, _, _, _) = fromEntry else { + return + } + + var referenceId: ItemCollectionId? + var beforeAll = false + var afterAll = false + if toIndex < entries.count { + switch entries[toIndex] { + case let .pack(_, _, toPackInfo, _, _, _, _, _, _): + referenceId = toPackInfo.id + default: + if entries[toIndex] < fromEntry { + beforeAll = true + } else { + afterAll = true + } + } + } else { + afterAll = true + } + + + let _ = (context.account.postbox.transaction { transaction -> Void in + var infos = transaction.getItemCollectionsInfos(namespace: Namespaces.ItemCollection.CloudStickerPacks) + var reorderInfo: ItemCollectionInfo? + for i in 0 ..< infos.count { + if infos[i].0 == fromPackInfo.id { + reorderInfo = infos[i].1 + infos.remove(at: i) + break + } + } + if let reorderInfo = reorderInfo { + if let referenceId = referenceId { + var inserted = false + for i in 0 ..< infos.count { + if infos[i].0 == referenceId { + if fromIndex < toIndex { + infos.insert((fromPackInfo.id, reorderInfo), at: i + 1) + } else { + infos.insert((fromPackInfo.id, reorderInfo), at: i) + } + inserted = true + break + } + } + if !inserted { + infos.append((fromPackInfo.id, reorderInfo)) + } + } else if beforeAll { + infos.insert((fromPackInfo.id, reorderInfo), at: 0) + } else if afterAll { + infos.append((fromPackInfo.id, reorderInfo)) + } + addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: Namespaces.ItemCollection.CloudStickerPacks, content: .sync, noDelay: false) + transaction.replaceItemCollectionInfos(namespace: Namespaces.ItemCollection.CloudStickerPacks, itemCollectionInfos: infos) + } + }).start() + + }) + } else { + self.genericView.resortController = nil + } + } + + + + })) + + } } diff --git a/Telegram-Mac/InstantPageAnchorItem.swift b/Telegram-Mac/InstantPageAnchorItem.swift index a21b615aeb..f304a47242 100644 --- a/Telegram-Mac/InstantPageAnchorItem.swift +++ b/Telegram-Mac/InstantPageAnchorItem.swift @@ -7,17 +7,19 @@ // import Cocoa -import TelegramCoreMac +import TelegramCore + final class InstantPageAnchorItem: InstantPageItem { var frame: CGRect let medias: [InstantPageMedia] = [] - let wantsNode: Bool = false + let wantsView: Bool = false let hasLinks: Bool = false let isInteractive: Bool = false - - private let anchor: String + let separatesTiles: Bool = false + + let anchor: String init(frame: CGRect, anchor: String) { self.anchor = anchor @@ -32,11 +34,11 @@ final class InstantPageAnchorItem: InstantPageItem { return self.anchor == anchor } - func matchesNode(_ node: InstantPageView) -> Bool { + func matchesView(_ node: InstantPageView) -> Bool { return false } - func node(account: Account) -> InstantPageView? { + func view(arguments: InstantPageItemArguments, currentExpandedDetails: [Int : Bool]?) -> (InstantPageView & NSView)? { return nil } diff --git a/Telegram-Mac/InstantPageArticleItem.swift b/Telegram-Mac/InstantPageArticleItem.swift new file mode 100644 index 0000000000..e23f40442e --- /dev/null +++ b/Telegram-Mac/InstantPageArticleItem.swift @@ -0,0 +1,251 @@ +// +// InstantPageArticleItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 27/12/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import Foundation +import Postbox +import TelegramCore + +import TGUIKit +import SwiftSignalKit + + +final class InstantPageArticleItem: InstantPageItem { + + + func linkSelectionViews() -> [InstantPageLinkSelectionView] { + return [] + } + + let hasLinks: Bool = false + + + let isInteractive: Bool = false + + + + var frame: CGRect + let wantsView: Bool = true + let separatesTiles: Bool = false + let medias: [InstantPageMedia] = [] + let webPage: TelegramMediaWebpage + + let contentItems: [InstantPageItem] + let contentSize: CGSize + let cover: TelegramMediaImage? + let url: String + let webpageId: MediaId + let rtl: Bool + + init(frame: CGRect, webPage: TelegramMediaWebpage, contentItems: [InstantPageItem], contentSize: CGSize, cover: TelegramMediaImage?, url: String, webpageId: MediaId, rtl: Bool) { + self.frame = frame + self.webPage = webPage + self.contentItems = contentItems + self.contentSize = contentSize + self.cover = cover + self.url = url + self.webpageId = webpageId + self.rtl = rtl + } + + func view(arguments: InstantPageItemArguments, currentExpandedDetails: [Int : Bool]?) -> (InstantPageView & NSView)? { + return InstantPageArticleView(context: arguments.context, item: self, webPage: self.webPage, contentItems: self.contentItems, contentSize: self.contentSize, cover: self.cover, url: self.url, webpageId: self.webpageId, rtl: self.rtl, openUrl: arguments.openUrl) + } + + func matchesAnchor(_ anchor: String) -> Bool { + return false + } + + func matchesView(_ view: InstantPageView) -> Bool { + if let view = view as? InstantPageArticleView { + return self === view.item + } else { + return false + } + } + + func distanceThresholdGroup() -> Int? { + return 7 + } + + func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat { + if count > 3 { + return 1000.0 + } else { + return CGFloat.greatestFiniteMagnitude + } + } + + func drawInTile(context: CGContext) { + } + + func linkSelectionRects(at point: CGPoint) -> [CGRect] { + return [] + } +} + +func layoutArticleItem(theme: InstantPageTheme, webPage: TelegramMediaWebpage, title: NSAttributedString, description: NSAttributedString, cover: TelegramMediaImage?, url: String, webpageId: MediaId, boundingWidth: CGFloat, rtl: Bool) -> InstantPageArticleItem { + let inset: CGFloat = 17.0 + let imageSpacing: CGFloat = 10.0 + var sideInset = inset + let imageSize = CGSize(width: 44.0, height: 44.0) + if cover != nil { + sideInset += imageSize.width + imageSpacing + } + + var availableLines: Int = 3 + var contentHeight: CGFloat = 15.0 * 2.0 + + var hasRTL = false + var contentItems: [InstantPageItem] = [] + let (titleTextItem, titleItems, titleSize) = layoutTextItemWithString(title, boundingWidth: boundingWidth - inset - sideInset, offset: CGPoint(x: inset, y: 15.0), maxNumberOfLines: availableLines) + contentItems.append(contentsOf: titleItems) + contentHeight += titleSize.height + + if let textItem = titleTextItem { + availableLines -= textItem.lines.count + if textItem.containsRTL { + hasRTL = true + } + } + var descriptionInset = inset + if hasRTL && cover != nil { + descriptionInset += imageSize.width + imageSpacing + for var item in titleItems { + item.frame = item.frame.offsetBy(dx: imageSize.width + imageSpacing, dy: 0.0) + } + } + + if availableLines > 0 { + let (descriptionTextItem, descriptionItems, descriptionSize) = layoutTextItemWithString(description, boundingWidth: boundingWidth - inset - sideInset, alignment: hasRTL ? .right : .natural, offset: CGPoint(x: descriptionInset, y: 15.0 + titleSize.height + 14.0), maxNumberOfLines: availableLines) + contentItems.append(contentsOf: descriptionItems) + + if let textItem = descriptionTextItem { + if textItem.containsRTL || hasRTL { + hasRTL = true + } + } + contentHeight += descriptionSize.height + 14.0 + } + + let contentSize = CGSize(width: boundingWidth, height: contentHeight) + return InstantPageArticleItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: contentSize.height)), webPage: webPage, contentItems: contentItems, contentSize: contentSize, cover: cover, url: url, webpageId: webpageId, rtl: rtl || hasRTL) +} + + + + + + + + +final class InstantPageArticleView: Button, InstantPageView { + let item: InstantPageArticleItem + + + private let contentTile: InstantPageTile + private let contentTileView: InstantPageTileView + private var imageView: TransformImageView? + + let url: String + let webpageId: MediaId + let cover: TelegramMediaImage? + let rtl: Bool + + private let openUrl: (InstantPageUrlItem) -> Void + + private var fetchedDisposable = MetaDisposable() + + init(context: AccountContext, item: InstantPageArticleItem, webPage: TelegramMediaWebpage, contentItems: [InstantPageItem], contentSize: CGSize, cover: TelegramMediaImage?, url: String, webpageId: MediaId, rtl: Bool, openUrl: @escaping (InstantPageUrlItem) -> Void) { + self.item = item + self.url = url + self.webpageId = webpageId + self.cover = cover + self.rtl = rtl + self.openUrl = openUrl + + self.contentTile = InstantPageTile(frame: CGRect(x: 0.0, y: 0.0, width: contentSize.width, height: contentSize.height)) + self.contentTile.items.append(contentsOf: contentItems) + self.contentTileView = InstantPageTileView(tile: self.contentTile, backgroundColor: .clear) + + super.init() + + self.addSubview(self.contentTileView) + + if let image = cover { + let imageView = TransformImageView() + + let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image) + imageView.setSignal(chatMessagePhoto(account: context.account, imageReference: imageReference, scale: backingScaleFactor)) + self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(account: context.account, imageReference: imageReference).start()) + + self.imageView = imageView + self.addSubview(imageView) + } + + set(handler: { [weak self] _ in + self?.click() + }, for: .Up) + + set(background: theme.colors.grayBackground, for: .Highlight) + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + deinit { + self.fetchedDisposable.dispose() + } + + private func click() { + self.openUrl(InstantPageUrlItem(url: self.url, webpageId: self.webpageId)) + } + + override func layout() { + super.layout() + + let size = self.bounds.size + let inset: CGFloat = 17.0 + let imageSize = CGSize(width: 44.0, height: 44.0) + + self.contentTileView.frame = self.bounds + + if let imageView = self.imageView, let image = self.cover, let largest = largestImageRepresentation(image.representations) { + let size = largest.dimensions.size.aspectFilled(imageSize) + let boundingSize = imageSize + + imageView.set(arguments: TransformImageArguments(corners: ImageCorners(radius: 5.0), imageSize: size, boundingSize: boundingSize, intrinsicInsets: NSEdgeInsets())) + } + + if let imageView = self.imageView { + if self.rtl { + imageView.frame = CGRect(origin: CGPoint(x: inset, y: 11.0), size: imageSize) + } else { + imageView.frame = CGRect(origin: CGPoint(x: size.width - inset - imageSize.width, y: 11.0), size: imageSize) + } + } + } + + func updateIsVisible(_ isVisible: Bool) { + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + } + + + + func updateHiddenMedia(media: InstantPageMedia?) { + } + +} diff --git a/Telegram-Mac/InstantPageAudioItem.swift b/Telegram-Mac/InstantPageAudioItem.swift new file mode 100644 index 0000000000..f2f4bf7b7e --- /dev/null +++ b/Telegram-Mac/InstantPageAudioItem.swift @@ -0,0 +1,71 @@ +// +// InstantPageAudioItem.swift +// Telegram +// +// Created by keepcoder on 11/04/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import Postbox +import TelegramCore + + +final class InstantPageAudioItem: InstantPageItem { + let wantsView: Bool = true + let hasLinks: Bool = false + var isInteractive: Bool { + return true + } + let separatesTiles: Bool = false + + func linkSelectionViews() -> [InstantPageLinkSelectionView] { + return [] + } + + var frame: CGRect + let medias: [InstantPageMedia] + + let media: InstantPageMedia + + init(frame: CGRect, media: InstantPageMedia) { + self.frame = frame + self.media = media + self.medias = [media] + } + + func view(arguments: InstantPageItemArguments, currentExpandedDetails: [Int : Bool]?) -> (InstantPageView & NSView)? { + return InstantPageAudioView(context: arguments.context, media: media) + } + + func matchesAnchor(_ anchor: String) -> Bool { + return false + } + + func matchesView(_ node: InstantPageView) -> Bool { + if let node = node as? InstantPageAudioView { + return self.media == node.media + } else { + return false + } + } + + func distanceThresholdGroup() -> Int? { + return 4 + } + + func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat { + if count > 3 { + return 1000.0 + } else { + return CGFloat.greatestFiniteMagnitude + } + } + + func linkSelectionRects(at point: CGPoint) -> [CGRect] { + return [] + } + + func drawInTile(context: CGContext) { + } +} diff --git a/Telegram-Mac/InstantPageAudioView.swift b/Telegram-Mac/InstantPageAudioView.swift new file mode 100644 index 0000000000..d9a6c5f488 --- /dev/null +++ b/Telegram-Mac/InstantPageAudioView.swift @@ -0,0 +1,186 @@ +// +// InstantPageAudioItem.swift +// Telegram +// +// Created by keepcoder on 11/04/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import Postbox +import TelegramCore + +import SwiftSignalKit + +final class InstantPageAudioView: View, InstantPageView, APDelegate { + + + private let context: AccountContext + let media: InstantPageMedia + private var nameView: TextView? + private let statusView: RadialProgressView = RadialProgressView() + private let linearProgress: LinearProgressControl = LinearProgressControl(progressHeight: 3) + private var bufferingStatusDisposable: MetaDisposable = MetaDisposable() + private var ranges: (IndexSet, Int)? + + weak var controller:APController? { + didSet { + if let controller = controller { + self.bufferingStatusDisposable.set((controller.bufferingStatus + |> deliverOnMainQueue).start(next: { [weak self] status in + if let status = status { + self?.updateStatus(status.0, status.1) + } + })) + controller.add(listener: self) + } + } + } + + private func updateStatus(_ ranges: IndexSet, _ size: Int) { + self.ranges = (ranges, size) + + if let ranges = self.ranges, !ranges.0.isEmpty, ranges.1 != 0 { + for range in ranges.0.rangeView { + var progress = (CGFloat(range.count) / CGFloat(ranges.1)) + progress = progress == 1.0 ? 0 : progress + linearProgress.set(fetchingProgress: progress, animated: progress > 0) + + break + } + } + } + + deinit { + bufferingStatusDisposable.dispose() + } + + init(context: AccountContext, media: InstantPageMedia) { + self.context = context + self.media = media + super.init() + addSubview(statusView) + addSubview(linearProgress) + linearProgress.style = ControlStyle(foregroundColor: theme.colors.text, backgroundColor: theme.colors.border, highlightColor: theme.colors.text) + linearProgress.set(background: theme.colors.border, for: .Normal) + + let file = media.media as! TelegramMediaFile + + if file.isMusic { + nameView = TextView() + let attr = NSMutableAttributedString() + _ = attr.append(string: file.musicText.1, color: theme.colors.text, font: .medium(.title)) + _ = attr.append(string: " - ", color: theme.colors.grayText, font: .normal(.title)) + _ = attr.append(string: file.musicText.0, color: theme.colors.grayText, font: .normal(.title)) + let nameLayout = TextViewLayout(attr, maximumNumberOfLines: 1) + nameView?.update(nameLayout) + addSubview(nameView!) + } + + linearProgress.fetchingColor = theme.colors.grayText + + if let current = globalAudio?.currentSong { + if current.entry.isEqual(to: self.wrapper) { + globalAudio?.add(listener: self) + } + } + statusView.state = .Icon(image: theme.icons.ivAudioPlay, mode: .copy) + linearProgress.set(progress: 0, animated:true) + + } + + var wrapper: APSingleWrapper { + let file = self.media.media as! TelegramMediaFile + return APSingleWrapper(resource: file.resource, name: nil, performer: nil, duration: file.duration, id: file.id ?? MediaId(namespace: 0, id: 0)) + } + + override func layout() { + super.layout() + + let size = self.bounds.size + + + + let insets = NSEdgeInsets(top: 18.0, left: 17.0, bottom: 18.0, right: 17.0) + + let leftInset: CGFloat = 46.0 + 10.0 + let rightInset: CGFloat = 0.0 + + let maxTitleWidth = max(1.0, size.width - insets.left - leftInset - rightInset - insets.right) + + statusView.centerY(x: insets.left) + + let leftScrubberInset: CGFloat = insets.left + 46.0 + 10.0 + let rightScrubberInset: CGFloat = insets.right + + + if let nameView = nameView { + nameView.layout?.measure(width: maxTitleWidth) + nameView.update(nameView.layout, origin: CGPoint(x: insets.left + leftInset, y: 5)) + } + + var topOffset: CGFloat = 0.0 + if nameView == nil { + topOffset = -10.0 + } + + linearProgress.frame = CGRect(origin: CGPoint(x: leftScrubberInset, y: 26.0 + topOffset + 5), size: CGSize(width: size.width - leftScrubberInset - rightScrubberInset, height: 5)) + + } + + func updateIsVisible(_ isVisible: Bool) { + + } + + func songDidChanged(song: APSongItem, for controller: APController, animated: Bool) { + linearProgress.onUserChanged = { [weak controller, weak self] progress in + controller?.set(trackProgress: progress) + self?.linearProgress.set(progress: CGFloat(progress), animated: false) + } + } + + func songDidChangedState(song: APSongItem, for controller: APController, animated: Bool) { + switch song.state { + case .waiting, .paused: + statusView.state = .Icon(image: theme.icons.ivAudioPlay, mode: .copy) + case .stoped: + statusView.state = .Icon(image: theme.icons.ivAudioPlay, mode: .copy) + linearProgress.set(progress: 0, animated:true) + case let .playing(_, _, progress): + linearProgress.set(progress: CGFloat(progress), animated: animated) + statusView.state = .Icon(image: theme.icons.ivAudioPause, mode: .copy) + break + case .fetching: + break + } + } + + func songDidStartPlaying(song: APSongItem, for controller: APController, animated: Bool) { + + } + + func songDidStopPlaying(song: APSongItem, for controller: APController, animated: Bool) { + self.bufferingStatusDisposable.set(nil) + statusView.state = .Icon(image: theme.icons.ivAudioPlay, mode: .copy) + linearProgress.set(progress: 0) + linearProgress.set(fetchingProgress: 0) + linearProgress.onUserChanged = nil + } + + func playerDidChangedTimebase(song: APSongItem, for controller: APController, animated: Bool) { + + } + + func audioDidCompleteQueue(for controller: APController, animated: Bool) { + + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/InstantPageChannelItem.swift b/Telegram-Mac/InstantPageChannelItem.swift index d872b737a6..d9db416016 100644 --- a/Telegram-Mac/InstantPageChannelItem.swift +++ b/Telegram-Mac/InstantPageChannelItem.swift @@ -7,17 +7,20 @@ // import Cocoa -import TelegramCoreMac -import PostboxMac +import TelegramCore + +import Postbox import TGUIKit class InstantPageChannelItem: InstantPageItem { var frame: CGRect let medias: [InstantPageMedia] = [] - let wantsNode: Bool = true + let wantsView: Bool = true let hasLinks: Bool = false let isInteractive: Bool = false + let separatesTiles: Bool = false + let channel: TelegramChannel let overlay: Bool private let joinChannel:(TelegramChannel)->Void @@ -39,11 +42,11 @@ class InstantPageChannelItem: InstantPageItem { return false } - func matchesNode(_ node: InstantPageView) -> Bool { + func matchesView(_ node: InstantPageView) -> Bool { return node is InstantPageChannelView } - func node(account: Account) -> InstantPageView? { + func view(arguments: InstantPageItemArguments, currentExpandedDetails: [Int : Bool]?) -> (InstantPageView & NSView)? { return InstantPageChannelView(frameRect: frame, channel: channel, overlay: overlay, openChannel: openChannel, joinChannel: joinChannel) } diff --git a/Telegram-Mac/InstantPageChannelView.swift b/Telegram-Mac/InstantPageChannelView.swift index d2d261ac88..d75c3695e4 100644 --- a/Telegram-Mac/InstantPageChannelView.swift +++ b/Telegram-Mac/InstantPageChannelView.swift @@ -8,8 +8,9 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac +import TelegramCore + +import Postbox class InstantPageChannelView : View, InstantPageView { private let channel: TelegramChannel @@ -27,7 +28,7 @@ class InstantPageChannelView : View, InstantPageView { self.openChannel = openChannel checkView.image = theme.icons.ivChannelJoined checkView.sizeToFit() - joinLayout = TextNode.layoutText(.initialize(string: tr(.ivChannelJoin), color: .white, font: .normal(.huge)), nil, 1, .end, NSMakeSize(.greatestFiniteMagnitude, .greatestFiniteMagnitude), nil, false, .left) + joinLayout = TextNode.layoutText(.initialize(string: tr(L10n.ivChannelJoin), color: .white, font: .normal(.huge)), nil, 1, .end, NSMakeSize(.greatestFiniteMagnitude, .greatestFiniteMagnitude), nil, false, .left) nameLayout = TextNode.layoutText(.initialize(string: channel.displayTitle, color: .white, font: .normal(.huge)), nil, 1, .end, NSMakeSize(frameRect.width - 40 - joinLayout.0.size.width, .greatestFiniteMagnitude), nil, false, .left) super.init(frame: frameRect) @@ -37,7 +38,7 @@ class InstantPageChannelView : View, InstantPageView { override func layout() { super.layout() - joinLayout = TextNode.layoutText(.initialize(string: tr(.ivChannelJoin), color: overlay ? .white : theme.colors.text, font: .normal(.huge)), nil, 1, .end, NSMakeSize(.greatestFiniteMagnitude, .greatestFiniteMagnitude), nil, false, .left) + joinLayout = TextNode.layoutText(.initialize(string: tr(L10n.ivChannelJoin), color: overlay ? .white : theme.colors.text, font: .normal(.huge)), nil, 1, .end, NSMakeSize(.greatestFiniteMagnitude, .greatestFiniteMagnitude), nil, false, .left) nameLayout = TextNode.layoutText(.initialize(string: channel.displayTitle, color: overlay ? .white : theme.colors.text, font: .normal(.huge)), nil, 1, .end, NSMakeSize(frame.width - 40 - joinLayout.0.size.width, .greatestFiniteMagnitude), nil, false, .left) checkView.centerY(x: frame.width - checkView.frame.width - 20) @@ -66,7 +67,7 @@ class InstantPageChannelView : View, InstantPageView { ctx.fill(bounds) let f = focus(nameLayout.0.size) - nameLayout.1.draw(NSMakeRect(40, f.minY, f.width, f.height), in: ctx, backingScaleFactor: backingScaleFactor) + nameLayout.1.draw(NSMakeRect(40, f.minY, f.width, f.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) switch channel.participationStatus { case .member: @@ -74,7 +75,7 @@ class InstantPageChannelView : View, InstantPageView { default: checkView.isHidden = true let f = focus(joinLayout.0.size) - joinLayout.1.draw(NSMakeRect(frame.width - f.width - 40, f.minY, f.width, f.height), in: ctx, backingScaleFactor: backingScaleFactor) + joinLayout.1.draw(NSMakeRect(frame.width - f.width - 40, f.minY, f.width, f.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) } } diff --git a/Telegram-Mac/InstantPageContentView.swift b/Telegram-Mac/InstantPageContentView.swift new file mode 100644 index 0000000000..e47457087d --- /dev/null +++ b/Telegram-Mac/InstantPageContentView.swift @@ -0,0 +1,409 @@ +// +// InstantPageContentView.swift +// Telegram +// +// Created by Mikhail Filimonov on 26/12/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Foundation +import Postbox +import TelegramCore + +import SwiftSignalKit +import TGUIKit + + +final class InstantPageContentView : View { + private let arguments: InstantPageItemArguments + + + var currentLayoutTiles: [InstantPageTile] = [] + var currentLayoutItemsWithViews: [InstantPageItem] = [] + var distanceThresholdGroupCount: [Int: Int] = [:] + + var visibleTiles: [Int: InstantPageTileView] = [:] + var visibleItemsWithViews: [Int: InstantPageView] = [:] + + var currentWebEmbedHeights: [Int : CGFloat] = [:] + var currentExpandedDetails: [Int : Bool]? + var currentDetailsItems: [InstantPageDetailsItem] = [] + + var requestLayoutUpdate: ((Bool) -> Void)? + + var currentLayout: InstantPageLayout + let contentSize: CGSize + let inOverlayPanel: Bool + + private var previousVisibleBounds: CGRect? + + init(arguments: InstantPageItemArguments, items: [InstantPageItem], contentSize: CGSize, inOverlayPanel: Bool = false) { + self.arguments = arguments + + + self.currentLayout = InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) + self.contentSize = contentSize + self.inOverlayPanel = inOverlayPanel + + super.init() + + self.updateLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + func updateLayout() { + for (_, tileView) in self.visibleTiles { + tileView.removeFromSuperview() + } + self.visibleTiles.removeAll() + + let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: contentSize.width) + + var currentDetailsItems: [InstantPageDetailsItem] = [] + var currentLayoutItemsWithViews: [InstantPageItem] = [] + var distanceThresholdGroupCount: [Int : Int] = [:] + + var expandedDetails: [Int : Bool] = [:] + + var detailsIndex = -1 + for item in self.currentLayout.items { + if item.wantsView { + currentLayoutItemsWithViews.append(item) + if let group = item.distanceThresholdGroup() { + let count: Int + if let currentCount = distanceThresholdGroupCount[Int(group)] { + count = currentCount + } else { + count = 0 + } + distanceThresholdGroupCount[Int(group)] = count + 1 + } + if let detailsItem = item as? InstantPageDetailsItem { + detailsIndex += 1 + expandedDetails[detailsIndex] = detailsItem.initiallyExpanded + currentDetailsItems.append(detailsItem) + } + } + } + + if self.currentExpandedDetails == nil { + self.currentExpandedDetails = expandedDetails + } + + self.currentLayoutTiles = currentLayoutTiles + self.currentLayoutItemsWithViews = currentLayoutItemsWithViews + self.currentDetailsItems = currentDetailsItems + self.distanceThresholdGroupCount = distanceThresholdGroupCount + } + + var effectiveContentSize: CGSize { + var contentSize = self.contentSize + for item in self.currentDetailsItems { + let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded + contentSize.height += -item.frame.height + (expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight) + } + return contentSize + } + + func isExpandedItem(_ item: InstantPageDetailsItem) -> Bool { + if let index = self.currentDetailsItems.firstIndex(where: {$0 === item}) { + return self.currentExpandedDetails?[index] ?? item.initiallyExpanded + } else { + return false + } + } + + func updateVisibleItems(visibleBounds: CGRect, animated: Bool = false) { + var visibleTileIndices = Set() + var visibleItemIndices = Set() + + self.previousVisibleBounds = visibleBounds + + CATransaction.begin() + defer { + CATransaction.commit() + } + + var topView: View? + let topTileView = topView + for view in self.subviews.reversed() { + if let view = view as? InstantPageTileView { + topView = view + break + } + } + + var collapseOffset: CGFloat = 0.0 + + var itemIndex = -1 + var embedIndex = -1 + var detailsIndex = -1 + + + for item in self.currentLayoutItemsWithViews { + itemIndex += 1 + if item is InstantPageWebEmbedItem { + embedIndex += 1 + } + if item is InstantPageDetailsItem { + detailsIndex += 1 + } + + var itemThreshold: CGFloat = 0.0 + if let group = item.distanceThresholdGroup() { + var count: Int = 0 + if let currentCount = self.distanceThresholdGroupCount[group] { + count = currentCount + } + itemThreshold = item.distanceThresholdWithGroupCount(count) + } + + var itemFrame = item.frame.offsetBy(dx: 0.0, dy: -collapseOffset) + var thresholdedItemFrame = itemFrame + thresholdedItemFrame.origin.y -= itemThreshold + thresholdedItemFrame.size.height += itemThreshold * 2.0 + + if let detailsItem = item as? InstantPageDetailsItem, let expanded = self.currentExpandedDetails?[detailsIndex] { + let height = expanded ? self.effectiveSizeForDetails(detailsItem).height : detailsItem.titleHeight + collapseOffset += itemFrame.height - height + itemFrame = CGRect(origin: itemFrame.origin, size: CGSize(width: itemFrame.width, height: height)) + } + + if visibleBounds.intersects(thresholdedItemFrame) { + visibleItemIndices.insert(itemIndex) + + var itemView = self.visibleItemsWithViews[itemIndex] + if let currentItemView = itemView { + if !item.matchesView(currentItemView) { + (currentItemView as! NSView).removeFromSuperview() + self.visibleItemsWithViews.removeValue(forKey: itemIndex) + itemView = nil + } + } + + if itemView == nil { + let itemIndex = itemIndex + let detailsIndex = detailsIndex + + let arguments = InstantPageItemArguments(context: self.arguments.context, theme: self.arguments.theme, openMedia: self.arguments.openMedia, openPeer: self.arguments.openPeer, openUrl: self.arguments.openUrl, updateWebEmbedHeight: { _ in }, updateDetailsExpanded: { [weak self] expanded in + self?.updateDetailsExpanded(detailsIndex, expanded) + }, isExpandedItem: { [weak self] item in + return self?.isExpandedItem(item) ?? false + }, effectiveRectForItem: { [weak self] item in + return self?.effectiveFrameForItem(item) ?? item.frame + }) + + if let newView = item.view(arguments: arguments, currentExpandedDetails: self.currentExpandedDetails) { + newView.frame = itemFrame + self.addSubview(newView) + topView = newView as? View + self.visibleItemsWithViews[itemIndex] = newView + itemView = newView + + if let itemView = itemView as? InstantPageDetailsView { + itemView.requestLayoutUpdate = { [weak self] animated in + self?.requestLayoutUpdate?(animated) + } + } + } + } else { + if (itemView as! NSView).frame != itemFrame { + (itemView as! NSView)._change(size: itemFrame.size, animated: animated) + (itemView as! NSView)._change(pos: itemFrame.origin, animated: animated) + } else { + (itemView as! NSView).needsDisplay = true + } + } + + if let itemView = itemView as? InstantPageDetailsView { + itemView.updateVisibleItems(visibleBounds: visibleBounds.offsetBy(dx: -itemView.frame.minX, dy: -itemView.frame.minY), animated: animated) + } + } + } + + topView = topTileView + + var tileIndex = -1 + for tile in self.currentLayoutTiles { + tileIndex += 1 + + let tileFrame = effectiveFrameForTile(tile) + var tileVisibleFrame = tileFrame + tileVisibleFrame.origin.y -= 400.0 + tileVisibleFrame.size.height += 400.0 * 2.0 + if tileVisibleFrame.intersects(visibleBounds) { + visibleTileIndices.insert(tileIndex) + + if self.visibleTiles[tileIndex] == nil { + let tileView = InstantPageTileView(tile: tile, backgroundColor: .clear) + tileView.frame = tileFrame + self.addSubview(tileView) + topView = tileView + self.visibleTiles[tileIndex] = tileView + } else { + if visibleTiles[tileIndex]!.frame != tileFrame { + + let view = self.visibleTiles[tileIndex]! + view._change(pos: tileFrame.origin, animated: animated) + view._change(size: tileFrame.size, animated: animated) + } + } + } + } + + var removeTileIndices: [Int] = [] + for (index, tileView) in self.visibleTiles { + if !visibleTileIndices.contains(index) { + removeTileIndices.append(index) + tileView.removeFromSuperview() + } + } + for index in removeTileIndices { + self.visibleTiles.removeValue(forKey: index) + } + + var removeItemIndices: [Int] = [] + for (index, itemView) in self.visibleItemsWithViews { + if !visibleItemIndices.contains(index) { + removeItemIndices.append(index) + (itemView as! NSView).removeFromSuperview() + } else { + var itemFrame = (itemView as! NSView).frame + let itemThreshold: CGFloat = 200.0 + itemFrame.origin.y -= itemThreshold + itemFrame.size.height += itemThreshold * 2.0 + itemView.updateIsVisible(visibleBounds.intersects(itemFrame)) + } + } + for index in removeItemIndices { + self.visibleItemsWithViews.removeValue(forKey: index) + } + + let subviews = self.subviews.sorted(by: {$0.frame.minY < $1.frame.minY}) + self.subviews = subviews + self.needsLayout = true + } + + private func updateWebEmbedHeight(_ index: Int, _ height: CGFloat) { + // let currentHeight = self.currentWebEmbedHeights[index] + // if height != currentHeight { + // if let currentHeight = currentHeight, currentHeight > height { + // return + // } + // self.currentWebEmbedHeights[index] = height + // + // let signal: Signal = (.complete() |> delay(0.08, queue: Queue.mainQueue())) + // self.updateLayoutDisposable.set(signal.start(completed: { [weak self] in + // if let strongSelf = self { + // strongSelf.updateLayout() + // strongSelf.updateVisibleItems() + // } + // })) + // } + } + + func updateDetailsExpanded(_ index: Int, _ expanded: Bool, animated: Bool = true, requestLayout: Bool = true) { + if var currentExpandedDetails = self.currentExpandedDetails { + currentExpandedDetails[index] = expanded + self.currentExpandedDetails = currentExpandedDetails + } + self.requestLayoutUpdate?(animated) + } + + + + func scrollableContentOffset(item: InstantPageScrollableItem) -> CGPoint { + var contentOffset = CGPoint() + for (_, itemView) in self.visibleItemsWithViews { + if let itemView = itemView as? InstantPageScrollableView, itemView.item === item { + contentOffset = itemView.contentOffset + break + } + } + return contentOffset + } + + func viewForDetailsItem(_ item: InstantPageDetailsItem) -> InstantPageDetailsView? { + for (_, itemView) in self.visibleItemsWithViews { + if let detailsView = itemView as? InstantPageDetailsView, detailsView.item === item { + return detailsView + } + } + return nil + } + + private func effectiveSizeForDetails(_ item: InstantPageDetailsItem) -> CGSize { + if let view = viewForDetailsItem(item) { + return CGSize(width: item.frame.width, height: view.effectiveContentSize.height + item.titleHeight) + } else { + return item.frame.size + } + } + + private func effectiveFrameForTile(_ tile: InstantPageTile) -> CGRect { + let layoutOrigin = tile.frame.origin + var origin = layoutOrigin + for item in self.currentDetailsItems { + let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded + if layoutOrigin.y >= item.frame.maxY { + let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight + origin.y += height - item.frame.height + } + } + return CGRect(origin: origin, size: tile.frame.size) + } + + func effectiveFrameForItem(_ item: InstantPageItem) -> CGRect { + let layoutOrigin = item.frame.origin + var origin = layoutOrigin + + for item in self.currentDetailsItems { + let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded + if layoutOrigin.y >= item.frame.maxY { + let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight + origin.y += height - item.frame.height + } + } + + if let item = item as? InstantPageDetailsItem { + let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded + let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight + return CGRect(origin: origin, size: CGSize(width: item.frame.width, height: height)) + } else { + return CGRect(origin: origin, size: item.frame.size) + } + } + + func textItemAtLocation(_ location: CGPoint) -> (InstantPageTextItem, CGPoint)? { + for item in self.currentLayout.items { + let itemFrame = self.effectiveFrameForItem(item) + if itemFrame.contains(location) { + if let item = item as? InstantPageTextItem, item.selectable { + return (item, CGPoint(x: itemFrame.minX - item.frame.minX, y: itemFrame.minY - item.frame.minY)) + } else if let item = item as? InstantPageScrollableItem { + let contentOffset = scrollableContentOffset(item: item) + if let (textItem, parentOffset) = item.textItemAtLocation(location.offsetBy(dx: -itemFrame.minX + contentOffset.x, dy: -itemFrame.minY)) { + return (textItem, itemFrame.origin.offsetBy(dx: parentOffset.x - contentOffset.x, dy: parentOffset.y)) + } + } else if let item = item as? InstantPageDetailsItem { + for (_, itemView) in self.visibleItemsWithViews { + if let itemView = itemView as? InstantPageDetailsView, itemView.item === item { + if let (textItem, parentOffset) = itemView.textItemAtLocation(location.offsetBy(dx: -itemFrame.minX, dy: -itemFrame.minY)) { + return (textItem, itemFrame.origin.offsetBy(dx: parentOffset.x, dy: parentOffset.y)) + } + } + } + } + } + } + return nil + } + +} diff --git a/Telegram-Mac/InstantPageDetailsItem.swift b/Telegram-Mac/InstantPageDetailsItem.swift new file mode 100644 index 0000000000..cb35f8e0ca --- /dev/null +++ b/Telegram-Mac/InstantPageDetailsItem.swift @@ -0,0 +1,435 @@ +// +// InstantPageDetailsItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 26/12/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Foundation +import Postbox +import TelegramCore + +import TGUIKit +import SwiftSignalKit + +final class InstantPageDetailsItem: InstantPageItem { + var hasLinks: Bool = false + + var isInteractive: Bool = false + + func linkSelectionViews() -> [InstantPageLinkSelectionView] { + return [] + } + + var frame: CGRect + let wantsView: Bool = true + let separatesTiles: Bool = true + let medias: [InstantPageMedia] = [] + + let titleItems: [InstantPageItem] + let titleHeight: CGFloat + let items: [InstantPageItem] + let safeInset: CGFloat + let rtl: Bool + let initiallyExpanded: Bool + let index: Int + + var isExpanded: Bool { + return self.arguments?.isExpandedItem(self) ?? initiallyExpanded + } + + var effectiveRect: NSRect { + return self.arguments?.effectiveRectForItem(self) ?? frame + } + + private var arguments: InstantPageItemArguments? + + init(frame: CGRect, titleItems: [InstantPageItem], titleHeight: CGFloat, items: [InstantPageItem], safeInset: CGFloat, rtl: Bool, initiallyExpanded: Bool, index: Int) { + self.frame = frame + self.titleItems = titleItems + self.titleHeight = titleHeight + self.items = items + self.safeInset = safeInset + self.rtl = rtl + self.initiallyExpanded = initiallyExpanded + self.index = index + } + + func view(arguments: InstantPageItemArguments, currentExpandedDetails: [Int : Bool]?) -> (InstantPageView & NSView)? { + var expanded: Bool? + self.arguments = arguments + if let expandedDetails = currentExpandedDetails, let currentlyExpanded = expandedDetails[self.index] { + expanded = currentlyExpanded + } + return InstantPageDetailsView(arguments: arguments, item: self, currentlyExpanded: expanded) + } + + private func itemsIn( _ rect: NSRect, items: [InstantPageItem] = []) -> [InstantPageItem] { + var items: [InstantPageItem] = items + for (_, item) in self.items.enumerated() { + if item.frame.intersects(rect) { + if let item = item as? InstantPageTableItem { + return item.itemsIn(rect, items: items) + } else if let item = item as? InstantPageDetailsItem { + var rect = rect + rect.origin.y = rect.minY - item.effectiveRect.minY - titleHeight + return item.itemsIn(rect, items: items) + } else { + items.append(item) + } + } + + } + return items + } + func itemsIn( _ rect: NSRect) -> [InstantPageItem] { + return self.itemsIn(rect.offsetBy(dx: 0, dy: -titleHeight), items: []) + } + + func deepRect(_ rect: NSRect) -> NSRect { + for (_, item) in self.items.enumerated() { + if item.frame.intersects(rect) { + if let item = item as? InstantPageDetailsItem { + var rect = rect + let result = rect.minY - item.effectiveRect.minY - titleHeight + rect.origin.y = result + if result > 0 { + + return item.deepRect(rect) + + } else { + var bp:Int = 0 + bp += 1 + } + } + } + + } + return rect + } + + func deepItemsInRect(_ rect: NSRect, items: [InstantPageItem] = []) -> [InstantPageItem] { + for (_, item) in self.items.enumerated() { + if item.frame.intersects(rect) { + if let item = item as? InstantPageDetailsItem { + var rect = rect + rect.origin.y = rect.minY - item.effectiveRect.minY - titleHeight + return item.deepItemsInRect(rect, items: item.items) + } + } + + } + return items + } + + func deepItemsInRect(_ rect: NSRect) -> [InstantPageItem] { + return deepItemsInRect(rect, items: self.items) + } + + + func matchesAnchor(_ anchor: String) -> Bool { + return false + } + + func matchesView(_ node: InstantPageView) -> Bool { + if let node = node as? InstantPageDetailsView { + return self === node.item + } else { + return false + } + } + + func distanceThresholdGroup() -> Int? { + return 8 + } + + func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat { + return CGFloat.greatestFiniteMagnitude + } + + func drawInTile(context: CGContext) { + } + + +} + +func layoutDetailsItem(theme: InstantPageTheme, title: NSAttributedString, boundingWidth: CGFloat, items: [InstantPageItem], contentSize: CGSize, safeInset: CGFloat, rtl: Bool, initiallyExpanded: Bool, index: Int) -> InstantPageDetailsItem { + let detailsInset: CGFloat = 17.0 + safeInset + let titleInset: CGFloat = 22.0 + + let (_, titleItems, titleSize) = layoutTextItemWithString(title, boundingWidth: boundingWidth - detailsInset * 2.0 - titleInset, offset: CGPoint(x: detailsInset + titleInset, y: 0.0)) + let titleHeight = max(44.0, titleSize.height + 26.0) + var offset: CGFloat? + for var item in titleItems { + var itemOffset = floorToScreenPixels(System.backingScale, (titleHeight - item.frame.height) / 2.0) + if item is InstantPageTextItem { + offset = itemOffset + } else if let offset = offset { + itemOffset = offset + } + item.frame = item.frame.offsetBy(dx: 0.0, dy: itemOffset) + } + + return InstantPageDetailsItem(frame: CGRect(x: 0.0, y: 0.0, width: boundingWidth, height: contentSize.height + titleHeight), titleItems: titleItems, titleHeight: titleHeight, items: items, safeInset: safeInset, rtl: rtl, initiallyExpanded: initiallyExpanded, index: index) +} + + + + + + + + + + +private let detailsInset: CGFloat = 17.0 +private let titleInset: CGFloat = 22.0 + +final class InstantPageDetailsView: Control, InstantPageView { + private let arguments: InstantPageItemArguments + let item: InstantPageDetailsItem + + private let titleTile: InstantPageTile + private let titleTileView: InstantPageTileView + + private let highlightedBackgroundView: View + private let buttonView: Control + private let arrowView: InstantPageDetailsArrowView + let separatorView: View + let contentView: InstantPageContentView + + var expanded: Bool + + var previousView: InstantPageDetailsView? + + var requestLayoutUpdate: ((Bool) -> Void)? + + init(arguments: InstantPageItemArguments, item: InstantPageDetailsItem, currentlyExpanded: Bool?) { + self.arguments = arguments + + self.item = item + + + let frame = item.frame + + self.highlightedBackgroundView = View() + self.highlightedBackgroundView.layer?.opacity = 0.0 + + + self.titleTile = InstantPageTile(frame: CGRect(x: 0.0, y: 0.0, width: frame.width, height: item.titleHeight)) + self.titleTile.items.append(contentsOf: item.titleItems) + self.titleTileView = InstantPageTileView(tile: self.titleTile, backgroundColor: .clear) + + if let expanded = currentlyExpanded { + self.expanded = expanded + } else { + self.expanded = item.initiallyExpanded + } + + self.arrowView = InstantPageDetailsArrowView(color: theme.colors.grayText, open: self.expanded) + self.separatorView = View() + separatorView.backgroundColor = theme.colors.border + self.buttonView = Control() + + self.contentView = InstantPageContentView(arguments: arguments, items: item.items, contentSize: CGSize(width: item.frame.width, height: item.frame.height - item.titleHeight)) + + + + super.init() + + + self.addSubview(self.contentView) + self.addSubview(self.highlightedBackgroundView) + self.addSubview(self.titleTileView) + self.addSubview(self.arrowView) + self.addSubview(self.separatorView) + self.addSubview(self.buttonView) + + + + buttonView.set(handler: { [weak self] _ in + guard let `self` = self else {return} + arguments.updateDetailsExpanded(!self.expanded) + self.setExpanded(!self.expanded, animated: true) + }, for: .Click) + + self.contentView.requestLayoutUpdate = { [weak self] animated in + self?.requestLayoutUpdate?(animated) + } + self.setExpanded(self.expanded, animated: false) + + } + + + override var needsDisplay: Bool { + didSet { + for subview in self.contentView.subviews { + subview.needsDisplay = true + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + + func setExpanded(_ expanded: Bool, animated: Bool) { + self.expanded = expanded + self.arrowView.setOpen(expanded, animated: animated) + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + } + + override func layout() { + super.layout() + + let size = self.bounds.size + let inset = detailsInset + self.item.safeInset + + self.titleTileView.frame = self.titleTile.frame + self.highlightedBackgroundView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: self.item.titleHeight + .borderSize)) + self.buttonView.frame = CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: self.item.titleHeight)) + self.arrowView.frame = CGRect(x: inset, y: floorToScreenPixels(backingScaleFactor, (self.item.titleHeight - 8.0) / 2.0) + 1.0, width: 13.0, height: 8.0) + self.contentView.frame = CGRect(x: 0.0, y: self.item.titleHeight, width: size.width, height: self.item.frame.height - self.item.titleHeight) + + let lineSize = CGSize(width: self.frame.width - inset, height: .borderSize) + self.separatorView.frame = CGRect(origin: CGPoint(x: self.item.rtl ? 0.0 : inset, y: self.item.titleHeight - lineSize.height), size: lineSize) + } + + func updateIsVisible(_ isVisible: Bool) { + + } + + + func updateVisibleItems(visibleBounds: CGRect, animated: Bool) { + if self.bounds.height > self.item.titleHeight { + self.contentView.updateVisibleItems(visibleBounds: visibleBounds.offsetBy(dx: -self.contentView.frame.minX, dy: -self.contentView.frame.minY), animated: animated) + } + } + + func textItemAtLocation(_ location: CGPoint) -> (InstantPageTextItem, CGPoint)? { + if self.titleTileView.frame.contains(location) { + for case let item as InstantPageTextItem in self.item.titleItems { + if item.frame.contains(location) { + return (item, self.titleTileView.frame.origin) + } + } + } + else if let (textItem, parentOffset) = self.contentView.textItemAtLocation(location.offsetBy(dx: -self.contentView.frame.minX, dy: -self.contentView.frame.minY)) { + return (textItem, self.contentView.frame.origin.offsetBy(dx: parentOffset.x, dy: parentOffset.y)) + } + return nil + } + + + var effectiveContentSize: CGSize { + return self.contentView.effectiveContentSize + } + + func effectiveFrameForItem(_ item: InstantPageItem) -> CGRect { + return self.contentView.effectiveFrameForItem(item).offsetBy(dx: 0.0, dy: self.item.titleHeight) + } +} + + +final class InstantPageDetailsArrowView : View { + var color: NSColor { + didSet { + self.setNeedsDisplay() + } + } + private (set) var open: Bool + + private var progress: CGFloat = 0.0 + private var targetProgress: CGFloat? + private var timer: SwiftSignalKit.Timer? + + init(color: NSColor, open: Bool) { + self.color = color + self.open = open + self.progress = open ? 1.0 : 0.0 + + super.init() + + + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + deinit { + timer?.invalidate() + } + + private func stopTimer() { + timer?.invalidate() + timer = nil + } + private func startTimer() { + if timer == nil { + timer = SwiftSignalKit.Timer(timeout: 0.016, repeat: true, completion: { [weak self] in + self?.displayLinkEvent() + }, queue: Queue.mainQueue()) + timer?.start() + } + } + + func setOpen(_ open: Bool, animated: Bool) { + self.open = open + let openProgress: CGFloat = open ? 1.0 : 0.0 + if animated { + self.targetProgress = openProgress + startTimer() + } else { + self.progress = openProgress + self.targetProgress = nil + stopTimer() + } + } + + + private func displayLinkEvent() { + if let targetProgress = self.targetProgress { + let sign = CGFloat(targetProgress - self.progress > 0 ? 1 : -1) + self.progress += 0.14 * sign + if sign > 0 && self.progress > targetProgress { + self.progress = 1.0 + self.targetProgress = nil + stopTimer() + //self.displayLink?.isPaused = true + } else if sign < 0 && self.progress < targetProgress { + self.progress = 0.0 + self.targetProgress = nil + stopTimer() + } + } + + self.setNeedsDisplay() + } + + + override func draw(_ layer: CALayer, in ctx: CGContext) { + ctx.setStrokeColor(color.cgColor) + ctx.setLineCap(.round) + ctx.setLineWidth(2.0) + + ctx.move(to: CGPoint(x: 1.0, y: 1.0 + 5.0 * progress)) + ctx.addLine(to: CGPoint(x: 6.0, y: 6.0 - 5.0 * progress)) + ctx.addLine(to: CGPoint(x: 11.0, y: 1.0 + 5.0 * progress)) + ctx.strokePath() + } + +} diff --git a/Telegram-Mac/InstantPageImageItem.swift b/Telegram-Mac/InstantPageImageItem.swift new file mode 100644 index 0000000000..3675c08610 --- /dev/null +++ b/Telegram-Mac/InstantPageImageItem.swift @@ -0,0 +1,103 @@ +// +// InstantPageImageItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 12/12/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import Postbox +import TelegramCore + + +protocol InstantPageImageAttribute { +} + +struct InstantPageMapAttribute: InstantPageImageAttribute { + let zoom: Int32 + let dimensions: CGSize +} + +final class InstantPageImageItem: InstantPageItem { + let hasLinks: Bool = false + let isInteractive: Bool + let separatesTiles: Bool = false + + func linkSelectionViews() -> [InstantPageLinkSelectionView] { + return [] + } + + var frame: CGRect + + let webPage: TelegramMediaWebpage + + let media: InstantPageMedia + let attributes: [InstantPageImageAttribute] + + var medias: [InstantPageMedia] { + return [self.media] + } + + let roundCorners: Bool + let fit: Bool + + let wantsView: Bool = true + + init(frame: CGRect, webPage: TelegramMediaWebpage, media: InstantPageMedia, attributes: [InstantPageImageAttribute] = [], interactive: Bool, roundCorners: Bool, fit: Bool) { + self.frame = frame + self.webPage = webPage + self.media = media + self.isInteractive = interactive + self.attributes = attributes + self.roundCorners = roundCorners + self.fit = fit + } + + func view(arguments: InstantPageItemArguments, currentExpandedDetails: [Int : Bool]?) -> (InstantPageView & NSView)? { + + let viewArguments: InstantPageMediaArguments + if let _ = media.media as? TelegramMediaMap, let attribute = attributes.first as? InstantPageMapAttribute { + viewArguments = .map(attribute) + } else { + viewArguments = .image(interactive: self.isInteractive, roundCorners: self.roundCorners, fit: self.fit) + } + + return InstantPageMediaView(context: arguments.context, media: media, arguments: viewArguments) + } + + func matchesAnchor(_ anchor: String) -> Bool { + return false + } + + func matchesView(_ node: InstantPageView) -> Bool { + if let node = node as? InstantPageMediaView { + return node.media == self.media + } else { + return false + } + } + + func distanceThresholdGroup() -> Int? { + return 1 + } + + func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat { + if count > 3 { + return 400.0 + } else { + return CGFloat.greatestFiniteMagnitude + } + } + + func drawInTile(context: CGContext) { + } + + func linkSelectionRects(at point: CGPoint) -> [CGRect] { + return [] + } +} + + + + diff --git a/Telegram-Mac/InstantPageItem.swift b/Telegram-Mac/InstantPageItem.swift index 5035d63de8..f8b0adf089 100644 --- a/Telegram-Mac/InstantPageItem.swift +++ b/Telegram-Mac/InstantPageItem.swift @@ -7,22 +7,50 @@ // import Cocoa -import TelegramCoreMac +import TelegramCore + +import Postbox + + +final class InstantPageItemArguments { + let context: AccountContext + let theme: InstantPageTheme + let openMedia:(InstantPageMedia)->Void + let openPeer:(PeerId) -> Void + let openUrl:(InstantPageUrlItem) -> Void + let updateWebEmbedHeight:(CGFloat) -> Void + let updateDetailsExpanded: (Bool) -> Void + let isExpandedItem: (InstantPageDetailsItem) -> Bool + let effectiveRectForItem: (InstantPageItem) -> NSRect + init(context: AccountContext, theme: InstantPageTheme, openMedia: @escaping (InstantPageMedia) -> Void, openPeer: @escaping (PeerId) -> Void, openUrl: @escaping (InstantPageUrlItem) -> Void, updateWebEmbedHeight: @escaping (CGFloat) -> Void, updateDetailsExpanded: @escaping (Bool) -> Void, isExpandedItem: @escaping(InstantPageDetailsItem) -> Bool, effectiveRectForItem: @escaping(InstantPageItem) -> NSRect) { + self.context = context + self.theme = theme + self.openMedia = openMedia + self.openPeer = openPeer + self.openUrl = openUrl + self.updateWebEmbedHeight = updateWebEmbedHeight + self.updateDetailsExpanded = updateDetailsExpanded + self.isExpandedItem = isExpandedItem + self.effectiveRectForItem = effectiveRectForItem + } +} protocol InstantPageItem { var frame: CGRect { get set } var hasLinks: Bool { get } - var wantsNode: Bool { get } + var wantsView: Bool { get } var medias: [InstantPageMedia] { get } - + var separatesTiles: Bool { get } + var isInteractive: Bool { get } func matchesAnchor(_ anchor: String) -> Bool func drawInTile(context: CGContext) - func node(account: Account) -> InstantPageView? - func matchesNode(_ node: InstantPageView) -> Bool + func view(arguments: InstantPageItemArguments, currentExpandedDetails: [Int : Bool]?) -> (InstantPageView & NSView)? + func matchesView(_ node: InstantPageView) -> Bool func linkSelectionViews() -> [InstantPageLinkSelectionView] func distanceThresholdGroup() -> Int? func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat } + diff --git a/Telegram-Mac/InstantPageLayout.swift b/Telegram-Mac/InstantPageLayout.swift index a4edbc4d7b..ed5954e96b 100644 --- a/Telegram-Mac/InstantPageLayout.swift +++ b/Telegram-Mac/InstantPageLayout.swift @@ -7,10 +7,12 @@ // import Cocoa -import TelegramCoreMac -import PostboxMac +import TelegramCore + +import Postbox import TGUIKit + final class InstantPageLayout { let origin: CGPoint let contentSize: CGSize @@ -28,273 +30,500 @@ final class InstantPageLayout { func flattenedItemsWithOrigin(_ origin: CGPoint) -> [InstantPageItem] { return self.items.map({ item in - var item = item - item.frame = item.frame.offsetBy(dx: origin.x, dy: origin.y) - return item + var _item = item + _item.frame = item.frame.offsetBy(dx: origin.x, dy: origin.y) + return _item }) } + + var deepMedias: [InstantPageMedia] { + var media:[InstantPageMedia] = [] + for item in items { + media.append(contentsOf: self.deepMedia(item, medias: [])) + } + return media + } + + private func deepMedia(_ item: InstantPageItem, medias: [InstantPageMedia]) -> [InstantPageMedia] { + var medias = medias + if item.isInteractive { + return medias + item.medias//.filter({ $0.media is TelegramMediaImage || $0.media is TelegramMediaFile }) + } else if let item = item as? InstantPageDetailsItem { + var mediaDetails:[InstantPageMedia] = medias + for item in item.items { + mediaDetails.append(contentsOf: deepMedia(item, medias: [])) + } + medias += mediaDetails + } + return medias + } } -func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, isCover: Bool, fillToWidthAndHeight: Bool, horizontalInsetBetweenMaxWidth: CGFloat, presentation: InstantViewAppearance, media: [MediaId: Media], mediaIndexCounter: inout Int, overlay: Bool, openChannel:@escaping(TelegramChannel)->Void, joinChannel:@escaping(TelegramChannel)->Void) -> InstantPageLayout { +private func setupStyleStack(_ stack: InstantPageTextStyleStack, theme: InstantPageTheme, category: InstantPageTextCategoryType, link: Bool) { + let attributes = theme.textCategories.attributes(type: category, link: link) + stack.push(.textColor(attributes.color)) + stack.push(.markerColor(theme.markerColor)) + stack.push(.linkColor(theme.linkColor)) + stack.push(.linkMarkerColor(theme.linkHighlightColor)) + switch attributes.font.style { + case .sans: + stack.push(.fontSerif(false)) + case .serif: + stack.push(.fontSerif(true)) + } + stack.push(.fontSize(attributes.font.size)) + stack.push(.lineSpacingFactor(attributes.font.lineSpacingFactor)) + if attributes.underline { + stack.push(.underline) + } +} +func layoutInstantPageBlock(webpage: TelegramMediaWebpage, rtl: Bool, block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, safeInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToSize: CGSize?, media: [MediaId: Media], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, detailsIndexCounter: inout Int, theme: InstantPageTheme, webEmbedHeights: [Int : CGFloat] = [:]) -> InstantPageLayout { + + let layoutCaption: (InstantPageCaption, CGSize) -> ([InstantPageItem], CGSize) = { caption, contentSize in + var items: [InstantPageItem] = [] + var offset = contentSize.height + var contentSize = CGSize() + var rtl = rtl + if case .empty = caption.text { + } else { + contentSize.height += 14.0 + offset += 14.0 + let styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .caption, link: false) + let (textItem, captionItems, captionContentSize) = layoutTextItemWithString(attributedStringForRichText(caption.text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: offset), media: media, webpage: webpage) + contentSize.height += captionContentSize.height + offset += captionContentSize.height + items.append(contentsOf: captionItems) + + rtl = textItem?.containsRTL ?? rtl + } + + if case .empty = caption.credit { + } else { + if case .empty = caption.text { + contentSize.height += 14.0 + offset += 14.0 + } else { + contentSize.height += 10.0 + offset += 10.0 + } + let styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .credit, link: false) + let (_, captionItems, captionContentSize) = layoutTextItemWithString(attributedStringForRichText(caption.credit, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, alignment: rtl ? .right : .natural, offset: CGPoint(x: horizontalInset, y: offset), media: media, webpage: webpage) + contentSize.height += captionContentSize.height + offset += captionContentSize.height + items.append(contentsOf: captionItems) + } + if contentSize.height > 0.0 && isCover { + contentSize.height += 14.0 + } + return (items, contentSize) + } + + let stringForDate: (Int32) -> String = { date in + let dateFormatter = DateFormatter() + dateFormatter.locale = appAppearance.locale + dateFormatter.dateStyle = .long + dateFormatter.timeStyle = .none + return dateFormatter.string(from: Date(timeIntervalSince1970: Double(date))) + } switch block { case let .cover(block): - return layoutInstantPageBlock(block, boundingWidth: boundingWidth, horizontalInset: horizontalInset, isCover: true, fillToWidthAndHeight: fillToWidthAndHeight, horizontalInsetBetweenMaxWidth: horizontalInsetBetweenMaxWidth, presentation: presentation, media: media, mediaIndexCounter: &mediaIndexCounter, overlay: overlay, openChannel: openChannel, joinChannel: joinChannel) + return layoutInstantPageBlock(webpage: webpage, rtl: rtl, block: block, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: true, previousItems:previousItems, fillToSize: fillToSize, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, webEmbedHeights: webEmbedHeights) case let .title(text): let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(28.0)) - styleStack.push(.fontSerif(true)) - styleStack.push(.lineSpacingFactor(0.685)) - let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) - item.frame = item.frame.offsetBy(dx: horizontalInset, dy: 0.0) - return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item]) + setupStyleStack(styleStack, theme: theme, category: .header, link: false) + let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage) + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) case let .subtitle(text): let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(17.0)) - let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) - item.frame = item.frame.offsetBy(dx: horizontalInset, dy: 0.0) - return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item]) + setupStyleStack(styleStack, theme: theme, category: .subheader, link: false) + let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage) + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) + case let .authorDate(author: author, date: date): + let styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .caption, link: false) + var text: RichText? + if case .empty = author { + if date != 0 { + text = .plain(stringForDate(date)) + } + } else { + if date != 0 { + let dateText = RichText.plain(stringForDate(date)) + let formatString = _NSLocalizedString("InstantPage.AuthorAndDateTitle") + let authorRange = formatString.range(of: "%1$@") + let dateRange = formatString.range(of: "%2$@") + if let authorRange = authorRange, let dateRange = dateRange { + if authorRange.lowerBound < dateRange.lowerBound { + let byPart = String(formatString[formatString.startIndex ..< authorRange.lowerBound]) + let middlePart = String(formatString[authorRange.upperBound ..< dateRange.lowerBound]) + let endPart = String(formatString[dateRange.upperBound...]) + + text = .concat([.plain(byPart), author, .plain(middlePart), dateText, .plain(endPart)]) + } else { + let beforePart = String(formatString[formatString.startIndex ..< dateRange.lowerBound]) + let middlePart = String(formatString[dateRange.upperBound ..< authorRange.lowerBound]) + let endPart = String(formatString[authorRange.upperBound...]) + + text = .concat([.plain(beforePart), dateText, .plain(middlePart), author, .plain(endPart)]) + } + } + + } else { + text = author + } + } + if let text = text { + var previousItemHasRTL = false + if let previousItem = previousItems.last as? InstantPageTextItem, previousItem.containsRTL { + previousItemHasRTL = true + } + let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, alignment: rtl || previousItemHasRTL ? .right : .natural, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage) + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) + } else { + return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(), items: []) + } + case let .kicker(text): + let styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .kicker, link: false) + let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage) + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) case let .header(text): let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(24.0)) - styleStack.push(.fontSerif(true)) - styleStack.push(.lineSpacingFactor(0.685)) - let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) - item.frame = item.frame.offsetBy(dx: horizontalInset, dy: 0.0) - return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item]) + setupStyleStack(styleStack, theme: theme, category: .header, link: false) + let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage) + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) case let .subheader(text): let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(19.0)) - styleStack.push(.fontSerif(true)) - styleStack.push(.lineSpacingFactor(0.685)) - let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) - item.frame = item.frame.offsetBy(dx: horizontalInset, dy: 0.0) - return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item]) + setupStyleStack(styleStack, theme: theme, category: .subheader, link: false) + let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage) + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) case let .paragraph(text): let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(17.0)) - if presentation.fontSerif { - styleStack.push(.fontSerif(true)) - } - let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) - item.frame = item.frame.offsetBy(dx: horizontalInset, dy: 0.0) - return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item]) + setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) + let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, horizontalInset: horizontalInset, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage) + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) case let .preformatted(text): let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(16.0)) - styleStack.push(.fontFixed(true)) - - styleStack.push(.lineSpacingFactor(0.685)) + setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) let backgroundInset: CGFloat = 14.0 - let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - backgroundInset * 2.0) - item.frame = item.frame.offsetBy(dx: horizontalInset, dy: backgroundInset) - let backgroundItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: item.frame.height + backgroundInset * 2.0)), shapeFrame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: item.frame.height + backgroundInset * 2.0)), shape: .rect, color: theme.colors.grayBackground) - return InstantPageLayout(origin: CGPoint(), contentSize: backgroundItem.frame.size, items: [backgroundItem, item]) - case let .authorDate(author: author, date: date): + let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - backgroundInset * 2.0, offset: CGPoint(x: 17.0, y: backgroundInset), media: media, webpage: webpage) + let backgroundItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: contentSize.height + backgroundInset * 2.0)), shapeFrame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: contentSize.height + backgroundInset * 2.0)), shape: .rect, color: theme.codeBlockBackgroundColor) + var allItems: [InstantPageItem] = [backgroundItem] + allItems.append(contentsOf: items) + return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(width: boundingWidth, height: contentSize.height + backgroundInset * 2.0), items: allItems) + case let .footer(text): let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(15.0)) - if presentation.fontSerif { - styleStack.push(.fontSerif(true)) + setupStyleStack(styleStack, theme: theme, category: .caption, link: false) + let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage) + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) + case .divider: + let lineWidth = floor(boundingWidth / 2.0) + let shapeItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - lineWidth) / 2.0), y: 0.0), size: CGSize(width: lineWidth, height: 1.0)), shapeFrame: CGRect(origin: CGPoint(), size: CGSize(width: lineWidth, height: 1.0)), shape: .rect, color: theme.textCategories.caption.color) + return InstantPageLayout(origin: CGPoint(), contentSize: shapeItem.frame.size, items: [shapeItem]) + case let .list(contentItems, ordered): + var contentSize = CGSize(width: boundingWidth, height: 0.0) + var maxIndexWidth: CGFloat = 0.0 + var listItems: [InstantPageItem] = [] + var indexItems: [InstantPageItem] = [] + + var hasNums = false + if ordered { + for item in contentItems { + if let num = item.num, !num.isEmpty { + hasNums = true + break + } + } } - styleStack.push(.textColor(theme.colors.grayText)) - var text: RichText? - if case .empty = author { - if date != 0 { - let dateStringPlain = DateFormatter.localizedString(from: Date(timeIntervalSince1970: Double(date)), dateStyle: .long, timeStyle: .none) - text = RichText.plain(dateStringPlain) + for i in 0 ..< contentItems.count { + let item = contentItems[i] + if ordered { + let styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) + let value: String + if hasNums { + if let num = item.num { + value = "\(num)." + } else { + value = " " + } + } else { + value = "\(i + 1)." + } + let (textItem, _, _) = layoutTextItemWithString(attributedStringForRichText(.plain(value), styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint()) + if let textItem = textItem, let line = textItem.lines.first { + textItem.selectable = false + maxIndexWidth = max(maxIndexWidth, line.frame.width) + indexItems.append(textItem) + } + } else { + let shapeItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 6.0, height: 12.0)), shapeFrame: CGRect(origin: CGPoint(x: 0.0, y: 3.0), size: CGSize(width: 6.0, height: 6.0)), shape: .ellipse, color: theme.textCategories.paragraph.color) + indexItems.append(shapeItem) } - } else { - let dateStringPlain = DateFormatter.localizedString(from: Date(timeIntervalSince1970: Double(date)), dateStyle: .long, timeStyle: .none) - let dateText = RichText.plain(dateStringPlain) + } + let indexSpacing: CGFloat = ordered ? 12.0 : 20.0 + for (i, item) in contentItems.enumerated() { + if (i != 0) { + contentSize.height += 18.0 + } + let styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) - if date != 0 { - let formatString = _NSLocalizedString("InstantPage.AuthorAndDateTitle") - let authorRange = formatString.range(of: "%1$@")! - let dateRange = formatString.range(of: "%2$@")! + var effectiveItem = item + if case let .blocks(blocks, num) = effectiveItem, blocks.isEmpty { + effectiveItem = .text(.plain(" "), num) + } + switch effectiveItem { + case let .text(text, _): + let (textItem, textItems, textItemSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - indexSpacing - maxIndexWidth, offset: CGPoint(x: horizontalInset + indexSpacing + maxIndexWidth, y: contentSize.height), media: media, webpage: webpage) - if authorRange.lowerBound < dateRange.lowerBound { - let byPart = formatString.substring(to: authorRange.lowerBound) - let middlePart = formatString.substring(with: authorRange.upperBound ..< dateRange.lowerBound) - let endPart = formatString.substring(from: dateRange.upperBound) - - text = .concat([.plain(byPart), author, .plain(middlePart), dateText, .plain(endPart)]) + contentSize.height += textItemSize.height + var indexItem = indexItems[i] + var itemFrame = indexItem.frame + + var lineMidY: CGFloat = 0.0 + if let textItem = textItem { + if let line = textItem.lines.first { + lineMidY = textItem.frame.minY + line.frame.midY + } else { + lineMidY = textItem.frame.midY + } + } + + if let textIndexItem = indexItem as? InstantPageTextItem, let line = textIndexItem.lines.first { + itemFrame = itemFrame.offsetBy(dx: horizontalInset + maxIndexWidth - line.frame.width, dy: floorToScreenPixels(System.backingScale, lineMidY - (itemFrame.height / 2.0))) } else { - let beforePart = formatString.substring(to: dateRange.lowerBound) - let middlePart = formatString.substring(with: dateRange.upperBound ..< authorRange.lowerBound) - let endPart = formatString.substring(from: authorRange.upperBound) + itemFrame = itemFrame.offsetBy(dx: horizontalInset, dy: floorToScreenPixels(System.backingScale, lineMidY - itemFrame.height / 2.0)) + } + indexItems[i].frame = itemFrame + listItems.append(indexItems[i]) + listItems.append(contentsOf: textItems) + case let .blocks(blocks, _): + var previousBlock: InstantPageBlock? + var originY: CGFloat = contentSize.height + for subBlock in blocks { + let subLayout = layoutInstantPageBlock(webpage: webpage, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - indexSpacing - maxIndexWidth, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: listItems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, webEmbedHeights: webEmbedHeights) - text = .concat([.plain(beforePart), dateText, .plain(middlePart), author, .plain(endPart)]) + let spacing: CGFloat = previousBlock != nil && subLayout.contentSize.height > 0.0 ? spacingBetweenBlocks(upper: previousBlock, lower: subBlock) : 0.0 + let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: horizontalInset + indexSpacing + maxIndexWidth, y: contentSize.height + spacing)) + if previousBlock == nil { + originY += spacing + } + listItems.append(contentsOf: blockItems) + contentSize.height += subLayout.contentSize.height + spacing + previousBlock = subBlock } - } else { - text = author + var indexItem = indexItems[i] + var indexItemFrame = indexItem.frame + if let textIndexItem = indexItem as? InstantPageTextItem, let line = textIndexItem.lines.first { + indexItemFrame = indexItemFrame.offsetBy(dx: horizontalInset + maxIndexWidth - line.frame.width, dy: originY) + } else { + indexItemFrame = indexItemFrame.offsetBy(dx: horizontalInset, dy: originY) + } + indexItems[i].frame = indexItemFrame + listItems.append(indexItems[i]) + break + + default: + break } } - if let text = text { - let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) - item.frame = item.frame.offsetBy(dx: horizontalInset, dy: 0.0) - return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item]) + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: listItems) + case let .blockQuote(text, caption): + let lineInset: CGFloat = 20.0 + let verticalInset: CGFloat = 4.0 + var contentSize = CGSize(width: boundingWidth, height: verticalInset) + + var items: [InstantPageItem] = [] + + let styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) + styleStack.push(.italic) + + let (_, textItems, textContentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset, offset: CGPoint(x: horizontalInset + lineInset, y: contentSize.height), media: media, webpage: webpage) + + contentSize.height += textContentSize.height + items.append(contentsOf: textItems) + + if case .empty = caption { } else { - return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(), items: []) + contentSize.height += 14.0 + + let styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .caption, link: false) + + let (_, captionItems, captionContentSize) = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset, offset: CGPoint(x: horizontalInset + lineInset, y: contentSize.height), media: media, webpage: webpage) + + contentSize.height += captionContentSize.height + items.append(contentsOf: captionItems) } - case let .image(id, caption): + contentSize.height += verticalInset + + let shapeItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(x: horizontalInset, y: 0.0), size: CGSize(width: 3.0, height: contentSize.height)), shapeFrame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 3.0, height: contentSize.height)), shape: .roundLine, color: theme.textCategories.paragraph.color) + + items.append(shapeItem) + + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) + case let .pullQuote(text, caption): + let verticalInset: CGFloat = 4.0 + var contentSize = CGSize(width: boundingWidth, height: verticalInset) + + var items: [InstantPageItem] = [] + + let styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) + styleStack.push(.italic) + + let (textItem, textItems, textContentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, alignment: .center, offset: CGPoint(x: 0.0, y: contentSize.height), media: media, webpage: webpage) + if let textItem = textItem { + textItem.frame = textItem.frame.offsetBy(dx: floor(boundingWidth - textItem.frame.width) / 2.0, dy: contentSize.height) + } + + contentSize.height += textContentSize.height + items.append(contentsOf: textItems) + + if case .empty = caption { + } else { + contentSize.height += 14.0 + + let styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .caption, link: false) + + let (textItem, captionItems, captionContentSize) = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, alignment: .center, offset: CGPoint(x: 0.0, y: contentSize.height), media: media, webpage: webpage) + if let textItem = textItem { + textItem.frame = textItem.frame.offsetBy(dx: floor(boundingWidth - textItem.frame.width) / 2.0, dy: contentSize.height) + } + + contentSize.height += captionContentSize.height + items.append(contentsOf: captionItems) + } + contentSize.height += verticalInset + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) + case let .image(id, caption, url, webpageId): if let image = media[id] as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { - let imageSize = largest.dimensions - var filledSize = imageSize.fit(CGSize(width: boundingWidth, height: 600.0)) + let imageSize = largest.dimensions.size + var filledSize = imageSize.aspectFitted(CGSize(width: boundingWidth - safeInset * 2.0, height: 1200.0)) - if fillToWidthAndHeight { - filledSize = CGSize(width: boundingWidth - horizontalInsetBetweenMaxWidth * 2, height: boundingWidth - horizontalInsetBetweenMaxWidth * 2); + if let size = fillToSize { + filledSize = size } else if isCover { - filledSize = imageSize.aspectFilled(CGSize(width: boundingWidth - horizontalInsetBetweenMaxWidth * 2, height: 1.0)) - - if filledSize.height > .ulpOfOne { - let maxSize = CGSize(width: boundingWidth - horizontalInsetBetweenMaxWidth * 2, height: floor((boundingWidth - horizontalInsetBetweenMaxWidth * 2) * 3.0 / 5.0)) - filledSize = CGSize(width: min(filledSize.width, maxSize.width), height: min(filledSize.height, maxSize.height)) + filledSize = imageSize.aspectFilled(CGSize(width: boundingWidth - safeInset * 2.0, height: 1.0)) + if !filledSize.height.isZero { + filledSize = filledSize.cropped(CGSize(width: boundingWidth - safeInset * 2.0, height: floor((boundingWidth - safeInset * 2.0) * 3.0 / 5.0))) } } let mediaIndex = mediaIndexCounter mediaIndexCounter += 1 - var items:[InstantPageItem] = [] + var contentSize = CGSize(width: boundingWidth - safeInset * 2.0, height: 0.0) + var items: [InstantPageItem] = [] - var contentSize: NSSize = NSMakeSize(boundingWidth, 0.0) - - contentSize.height += filledSize.height + var mediaUrl: InstantPageUrlItem? + if let url = url { + mediaUrl = InstantPageUrlItem(url: url, webpageId: webpageId) + } - let mediaItem = InstantPageMediaItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), media: InstantPageMedia(index: mediaIndex, media: image, caption: richPlainText(caption)), arguments: InstantPageMediaArguments.image(interactive: true, roundCorners: false, fit: false)) + let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: mediaIndex, media: image, webpage: webpage, url: mediaUrl, caption: caption.text, credit: caption.credit), interactive: true, roundCorners: false, fit: false) items.append(mediaItem) + contentSize.height += filledSize.height - var hasCaption: Bool = true - if case .empty = caption { - hasCaption = false - } + let (captionItems, captionSize) = layoutCaption(caption, contentSize) + items.append(contentsOf: captionItems) + contentSize.height += captionSize.height - if hasCaption { - contentSize.height += 10 - - let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(15.0)) - styleStack.push(.textColor(theme.colors.grayText)) - if presentation.fontSerif { - styleStack.push(.fontSerif(true)) - } - let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) - captionItem.alignment = .center - if filledSize.width > boundingWidth - .ulpOfOne { - captionItem.frame = captionItem.frame.offsetBy(dx: horizontalInset, dy: contentSize.height) - } else { - captionItem.frame = captionItem.frame.offsetBy(dx: floorToScreenPixels((boundingWidth - captionItem.frame.size.width) / 2.0), dy: contentSize.height) + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) + } else { + return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(), items: []) + } + case let .video(id, caption, autoplay, loop): + if let file = media[id] as? TelegramMediaFile, let dimensions = file.dimensions?.size { + let imageSize = dimensions + var filledSize = imageSize.aspectFitted(CGSize(width: boundingWidth - safeInset * 2.0, height: 1200.0)) + + if let size = fillToSize { + filledSize = size + } else if isCover { + filledSize = imageSize.aspectFilled(CGSize(width: boundingWidth - safeInset * 2.0, height: 1.0)) + if !filledSize.height.isZero { + filledSize = filledSize.cropped(CGSize(width: boundingWidth - safeInset * 2.0, height: floor((boundingWidth - safeInset * 2.0) * 3.0 / 5.0))) } - contentSize.height += captionItem.frame.size.height; - items.append(captionItem) } + + let mediaIndex = mediaIndexCounter + mediaIndexCounter += 1 + + var contentSize = CGSize(width: boundingWidth - safeInset * 2.0, height: 0.0) + var items: [InstantPageItem] = [] + + let mediaItem = InstantPageMediaItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), media: InstantPageMedia(index: mediaIndex, media: file, webpage: webpage, url: nil, caption: caption.text, credit: caption.credit), arguments: InstantPageMediaArguments.video(interactive: true, autoplay: autoplay)) + items.append(mediaItem) - + contentSize.height += filledSize.height + + let (captionItems, captionSize) = layoutCaption(caption, contentSize) + items.append(contentsOf: captionItems) + contentSize.height += captionSize.height + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) } else { return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(), items: []) } - case let .video(id, caption, autoplay, loop): + case let .collage(blocks, caption): - if let video = media[id] as? TelegramMediaFile { - let imageSize = video.dimensions ?? CGSize() - if imageSize.width > .ulpOfOne && imageSize.height > .ulpOfOne { - var filledSize = imageSize.fit(CGSize(width: boundingWidth, height: 600.0)) - if fillToWidthAndHeight { - filledSize = CGSize(width: boundingWidth - horizontalInsetBetweenMaxWidth * 2, height: boundingWidth - horizontalInsetBetweenMaxWidth * 2) - } else if isCover { - filledSize = imageSize.aspectFilled(CGSize(width: boundingWidth - horizontalInsetBetweenMaxWidth * 2, height: 1)) - if filledSize.height > .ulpOfOne { - let maxSize = CGSize(width: boundingWidth - horizontalInsetBetweenMaxWidth * 2, height: floor((boundingWidth - horizontalInsetBetweenMaxWidth * 2) * 3.0 / 5.0)) - filledSize = CGSize(width: min(filledSize.width, maxSize.width), height: min(filledSize.height, maxSize.height)) - } - } - - var items:[InstantPageItem] = [] - - let mediaIndex = mediaIndexCounter - mediaIndexCounter += 1 - - var contentSize = CGSize(width: boundingWidth, height: 0) - - contentSize.height += filledSize.height - - let mediaItem = InstantPageMediaItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), media: InstantPageMedia(index: mediaIndex, media: video, caption: richPlainText(caption)), arguments: InstantPageMediaArguments.video(interactive: true, autoplay: autoplay)) - - items.append(mediaItem) - - var hasCaption: Bool = true - if case .empty = caption { - hasCaption = false - } - - if hasCaption { - contentSize.height += 10 - - let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(15.0)) - if presentation.fontSerif { - styleStack.push(.fontSerif(true)) - } - styleStack.push(.textColor(theme.colors.grayText)) - - let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) - captionItem.alignment = .center - if filledSize.width > boundingWidth - .ulpOfOne { - captionItem.frame = captionItem.frame.offsetBy(dx: horizontalInset, dy: contentSize.height) - } else { - captionItem.frame = captionItem.frame.offsetBy(dx: floorToScreenPixels((boundingWidth - captionItem.frame.size.width) / 2.0), dy: contentSize.height) - } - contentSize.height += captionItem.frame.size.height; - items.append(captionItem) - } - - - return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) - + let spacing: CGFloat = 2 + let itemsPerRow: CGFloat = min(round(boundingWidth / 150), CGFloat(blocks.count)) + let itemSize: CGFloat = floorToScreenPixels(System.backingScale, (boundingWidth - spacing * max(0, itemsPerRow - 1)) / itemsPerRow) + + var items:[InstantPageItem] = [] + var nextItemOrigin: CGPoint = CGPoint() + + var i = 0 + for subItem in blocks { + if nextItemOrigin.x + itemSize > boundingWidth { + nextItemOrigin.x = 0.0 + nextItemOrigin.y += itemSize + spacing } + let subLayout = layoutInstantPageBlock(webpage: webpage, rtl: rtl, block: subItem, boundingWidth: itemSize, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToSize: NSMakeSize(itemSize, itemSize), media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, webEmbedHeights: webEmbedHeights) + items.append(contentsOf: subLayout.flattenedItemsWithOrigin(nextItemOrigin)) + nextItemOrigin.x += itemSize + spacing; + + i += 1 } - case let .webEmbed(url, html, dimensions, caption, stretchToWidth, allowScrolling, coverId): - var embedBoundingWidth = boundingWidth - horizontalInset * 2.0 - if stretchToWidth { - embedBoundingWidth = boundingWidth - } - let size: CGSize - if dimensions.width.isLessThanOrEqualTo(0.0) { - size = CGSize(width: embedBoundingWidth, height: dimensions.height) - } else { - size = dimensions.aspectFitted(CGSize(width: embedBoundingWidth, height: embedBoundingWidth)) - } - let item = InstantPageWebEmbedItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - size.width) / 2.0), y: 0.0), size: size), url: url, html: html, enableScrolling: allowScrolling) - return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item]) - case let .postEmbed(_, _, avatarId, author, date, blocks, caption): - var contentSize = NSMakeSize(boundingWidth, 0.0) + + var contentSize = CGSize(width: boundingWidth, height: nextItemOrigin.y + itemSize) + + let (captionItems, captionSize) = layoutCaption(caption, contentSize) + items.append(contentsOf: captionItems) + contentSize.height += captionSize.height + + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) + case let .postEmbed(url, webpageId, avatarId, author, date, blocks, caption): + var contentSize = CGSize(width: boundingWidth, height: 0.0) let lineInset: CGFloat = 20.0 - let verticalInset:CGFloat = 4.0 - let itemSpacing:CGFloat = 10.0 - var avatarInset:CGFloat = 0.0 - var avatarVerticalInset:CGFloat = 0.0 + let verticalInset: CGFloat = 4.0 + let itemSpacing: CGFloat = 10.0 + var avatarInset: CGFloat = 0.0 + var avatarVerticalInset: CGFloat = 0.0 contentSize.height += verticalInset - var items:[InstantPageItem] = [] - - if author.length > 0 { - let avatar: TelegramMediaImage? - if let avatarId = avatarId { - avatar = media[avatarId] as? TelegramMediaImage - } else { - avatar = nil - } + var items: [InstantPageItem] = [] + + if !author.isEmpty { + let avatar: TelegramMediaImage? = avatarId.flatMap { media[$0] as? TelegramMediaImage } if let avatar = avatar { - let avatarItem = InstantPageMediaItem(frame: NSMakeRect(horizontalInset + lineInset + 1.0, contentSize.height - 2.0, 50.0, 50.0), media: InstantPageMedia.init(index: -1, media: avatar, caption: richPlainText(caption)), arguments: .image(interactive: false, roundCorners: true, fit: false)) - + let avatarItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: horizontalInset + lineInset + 1.0, y: contentSize.height - 2.0), size: CGSize(width: 50.0, height: 50.0)), webPage: webpage, media: InstantPageMedia(index: -1, media: avatar, webpage: webpage, url: nil, caption: nil, credit: nil), interactive: false, roundCorners: true, fit: false) items.append(avatarItem) + avatarInset += 62.0 avatarVerticalInset += 6.0 if date == 0 { @@ -303,313 +532,405 @@ func layoutInstantPageBlock(_ block: InstantPageBlock, boundingWidth: CGFloat, h } let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(17)) - if presentation.fontSerif { - styleStack.push(.fontSerif(true)) - } + setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) styleStack.push(.bold) - styleStack.push(.textColor(theme.colors.text)) - let textItem = layoutTextItemWithString(attributedStringForRichText(.plain(author), styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) - textItem.frame = textItem.frame.offsetBy(dx: horizontalInset + lineInset + avatarInset, dy: contentSize.height + avatarVerticalInset) + let (_, textItems, textContentSize) = layoutTextItemWithString(attributedStringForRichText(.plain(author), styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset - avatarInset, offset: CGPoint(x: horizontalInset + lineInset + avatarInset, y: contentSize.height + avatarVerticalInset), media: media, webpage: webpage) + items.append(contentsOf: textItems) - contentSize.height += textItem.frame.size.height + avatarVerticalInset - items.append(textItem) + contentSize.height += textContentSize.height + avatarVerticalInset } - if date != 0 { - if !items.isEmpty { + if items.count != 0 { contentSize.height += itemSpacing } - let dateString = DateFormatter.localizedString(from: Date(timeIntervalSince1970: TimeInterval(date)), dateStyle: .long, timeStyle: .none) - + + let dateString = DateFormatter.localizedString(from: Date(timeIntervalSince1970: Double(date)), dateStyle: .long, timeStyle: .none) + let styleStack = InstantPageTextStyleStack() - styleStack.push(.textColor(theme.colors.grayText)) - styleStack.push(.fontSize(15)) - if presentation.fontSerif { - styleStack.push(.fontSerif(true)) - } + setupStyleStack(styleStack, theme: theme, category: .caption, link: false) + + let (_, textItems, textContentSize) = layoutTextItemWithString(attributedStringForRichText(.plain(dateString), styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset - avatarInset, offset: CGPoint(x: horizontalInset + lineInset + avatarInset, y: contentSize.height), media: media, webpage: webpage) + items.append(contentsOf: textItems) - let textItem = layoutTextItemWithString(attributedStringForRichText(.plain(dateString), styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) - textItem.frame = textItem.frame.offsetBy(dx: horizontalInset + lineInset + avatarInset, dy: contentSize.height) - items.append(textItem) + contentSize.height += textContentSize.height } - if !items.isEmpty { - contentSize.height += itemSpacing; + if items.count != 0 { + contentSize.height += itemSpacing } - var previous: InstantPageBlock? = nil - for sub in blocks { - let subLayout = layoutInstantPageBlock(sub, boundingWidth: boundingWidth - horizontalInset * 2 - lineInset, horizontalInset: 0, isCover: false, fillToWidthAndHeight: false, horizontalInsetBetweenMaxWidth: horizontalInsetBetweenMaxWidth, presentation: presentation, media: media, mediaIndexCounter: &mediaIndexCounter, overlay: overlay, openChannel: openChannel, joinChannel: joinChannel) - let spacing = spacingBetweenBlocks(upper: previous, lower: sub) - let subItems = subLayout.flattenedItemsWithOrigin(NSMakePoint(horizontalInset + lineInset, contentSize.height + spacing)) - items.append(contentsOf: subItems) + var previousBlock: InstantPageBlock? + for subBlock in blocks { + let subLayout = layoutInstantPageBlock(webpage: webpage, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, webEmbedHeights: webEmbedHeights) + + let spacing = spacingBetweenBlocks(upper: previousBlock, lower: subBlock) + let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: horizontalInset + lineInset, y: contentSize.height + spacing)) + items.append(contentsOf: blockItems) contentSize.height += subLayout.contentSize.height + spacing - previous = sub + previousBlock = subBlock } - contentSize.height += verticalInset; + contentSize.height += verticalInset - items.append(InstantPageShapeItem(frame: NSMakeRect(horizontalInset, 0.0, 3.0, contentSize.height), shapeFrame: NSMakeRect(0.0, 0.0, 3.0, contentSize.height), shape: .roundLine, color: theme.colors.text)) + items.append(InstantPageShapeItem(frame: CGRect(origin: CGPoint(x: horizontalInset, y: 0.0), size: CGSize(width: 3.0, height: contentSize.height)), shapeFrame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 3.0, height: contentSize.height)), shape: .roundLine, color: theme.textCategories.paragraph.color)) - var hasCaption: Bool = true - if case .empty = caption { - hasCaption = false - } + let (captionItems, captionSize) = layoutCaption(caption, contentSize) + items.append(contentsOf: captionItems) + contentSize.height += captionSize.height - if hasCaption { - contentSize.height += 14.0 - - let styleStack = InstantPageTextStyleStack() - styleStack.push(.textColor(theme.colors.grayText)) - styleStack.push(.fontSize(15.0)) - if presentation.fontSerif { - styleStack.push(.fontSerif(true)) - } - let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) - captionItem.frame = captionItem.frame.offsetBy(dx: horizontalInset, dy: contentSize.height) - contentSize.height += captionItem.frame.size.height - items.append(captionItem) - } return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) - case let .collage(blocks, _): + case let .slideshow(items: subItems, caption: caption): + var contentSize = CGSize(width: boundingWidth, height: 0.0) + var items: [InstantPageItem] = [] - let spacing: CGFloat = 2 - let itemsPerRow: CGFloat = min(round(boundingWidth / 150), CGFloat(blocks.count)) - let itemSize: CGFloat = floorToScreenPixels((boundingWidth - spacing * max(0, itemsPerRow - 1)) / itemsPerRow) + var itemMedias: [InstantPageMedia] = [] - var items:[InstantPageItem] = [] - var nextItemOrigin: CGPoint = CGPoint() - for subBlock in blocks { - if nextItemOrigin.x + itemSize > boundingWidth { - nextItemOrigin.x = 0.0 - nextItemOrigin.y += itemSize + spacing + for subBlock in subItems { + switch subBlock { + case let .image(id, caption, url, webpageId): + if let image = media[id] as? TelegramMediaImage, let imageSize = largestImageRepresentation(image.representations)?.dimensions.size { + let mediaIndex = mediaIndexCounter + mediaIndexCounter += 1 + + let filledSize = imageSize.fitted(CGSize(width: boundingWidth, height: 1200.0)) + contentSize.height = max(contentSize.height, filledSize.height) + + var mediaUrl: InstantPageUrlItem? + if let url = url { + mediaUrl = InstantPageUrlItem(url: url, webpageId: webpageId) + } + itemMedias.append(InstantPageMedia(index: mediaIndex, media: image, webpage: webpage, url: mediaUrl, caption: caption.text, credit: caption.credit)) + } + break + default: + break } - let subLayout = layoutInstantPageBlock(subBlock, boundingWidth: itemSize, horizontalInset: 0, isCover: false, fillToWidthAndHeight: true, horizontalInsetBetweenMaxWidth: horizontalInsetBetweenMaxWidth, presentation: presentation, media: media, mediaIndexCounter: &mediaIndexCounter, overlay: overlay, openChannel: openChannel, joinChannel: joinChannel) - items.append(contentsOf: subLayout.flattenedItemsWithOrigin(nextItemOrigin)) - nextItemOrigin.x += itemSize + spacing; } + items.append(InstantPageSlideshowItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: contentSize.height)), webPage: webpage, medias: itemMedias)) - return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(width: boundingWidth, height: nextItemOrigin.y + itemSize), items: items) - case .anchor(let anchor): - return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(), items: [InstantPageAnchorItem(frame: CGRect(), anchor: anchor)]) - case .channelBanner(let channel): - if let channel = channel { - let width = boundingWidth - horizontalInsetBetweenMaxWidth * 2 - let item = InstantPageChannelItem(frame: CGRect(x: floorToScreenPixels((boundingWidth - width)/2), y: 0, width: width , height: InstantPageChannelView.height), channel: channel, overlay: overlay, openChannel: openChannel, joinChannel: joinChannel) - return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(width: boundingWidth, height: item.frame.height), items: [item]) - } - case .footer(let text): - let styleStack = InstantPageTextStyleStack() - styleStack.push(.textColor(theme.colors.grayText)) - styleStack.push(.fontSize(15.0)) - if presentation.fontSerif { - styleStack.push(.fontSerif(true)) - } - let item = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) - item.frame = item.frame.offsetBy(dx: horizontalInset, dy: 0.0) - return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item]) - case .divider: - let lineWidth = floorToScreenPixels(boundingWidth / 2) - let shapeItem = InstantPageShapeItem(frame: CGRect(x: floorToScreenPixels((boundingWidth - lineWidth) / 2.0), y: 0.0, width: lineWidth, height: 1.0), shapeFrame: CGRect(x: 0, y: 0, width: lineWidth, height: 1.0), shape: .rect, color: theme.colors.grayText) - return InstantPageLayout(origin: CGPoint(), contentSize: shapeItem.frame.size, items: [shapeItem]) - case let .list(items, ordered): + let (captionItems, captionSize) = layoutCaption(caption, contentSize) + items.append(contentsOf: captionItems) + contentSize.height += captionSize.height - var contentSize = CGSize(width: boundingWidth, height: 0) - var maxIndexWidth:CGFloat = 0.0 - var listItems:[InstantPageItem] = [] - var indexItems:[InstantPageItem] = [] - for i in 0 ..< items.count { - if ordered { - let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(17)) - if presentation.fontSerif { - styleStack.push(.fontSerif(true)) - } - styleStack.push(.textColor(theme.colors.text)) - let textItem = layoutTextItemWithString(attributedStringForRichText(.plain("\(i + 1)."), styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2) - textItem.frame.size.width = textItem.lines.first!.frame.width - maxIndexWidth = max(textItem.frame.width, maxIndexWidth); - indexItems.append(textItem) - } else { - let shapeItem = InstantPageShapeItem(frame: CGRect(x: 0.0, y: 0.0, width: 6.0, height: 12.0), shapeFrame: CGRect(x: 0.0, y: 3.0, width: 6.0, height: 6.0), shape: .ellipse, color: theme.colors.text) - indexItems.append(shapeItem) - } - } - var index: Int = -1 - let indexSpacing: CGFloat = ordered ? 7.0 : 20.0 - for text in items { - index += 1 - if index != 0 { - contentSize.height += 20.0 - } - let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(17)) - if presentation.fontSerif { - styleStack.push(.fontSerif(true)) - } - styleStack.push(.textColor(theme.colors.text)) - - let textItem = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2 - indexSpacing - maxIndexWidth) - textItem.frame = textItem.frame.offsetBy(dx: horizontalInset + indexSpacing + maxIndexWidth, dy: contentSize.height) - contentSize.height += textItem.frame.size.height - - var indexItem = indexItems[index] - indexItem.frame = indexItem.frame.offsetBy(dx: horizontalInset, dy: textItem.frame.minY) - - listItems.append(indexItem) - listItems.append(textItem) - + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) + case let .table(title, rows, bordered, striped): + var contentSize = CGSize(width: boundingWidth, height: 0.0) + var items: [InstantPageItem] = [] + + var styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .caption, link: false) + let backgroundInset: CGFloat = 0.0 + let (textItem, textItems, textContentSize) = layoutTextItemWithString(attributedStringForRichText(title, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - backgroundInset * 2.0, alignment: .center, offset: CGPoint(), media: media, webpage: webpage) + if let textItem = textItem { + textItem.frame = textItem.frame.offsetBy(dx: floor(boundingWidth - textItem.frame.width) / 2.0, dy: 0.0) } - return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: listItems) - case let .blockQuote(text, caption): - let lineInset: CGFloat = 20.0 - let verticalInset: CGFloat = 4 - var contentSize = CGSize(width: boundingWidth, height: verticalInset) - var items:[InstantPageItem] = [] + items.append(contentsOf: textItems) + contentSize.height += textContentSize.height + 10.0 - let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(17)) - styleStack.push(.fontSerif(true)) - styleStack.push(.italic) - styleStack.push(.textColor(theme.colors.text)) + styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .table, link: false) + let tableBoundingWidth = boundingWidth - horizontalInset * 2.0 + let tableItem = layoutTableItem(rtl: rtl, rows: rows, styleStack: styleStack, theme: theme, bordered: bordered, striped: striped, boundingWidth: tableBoundingWidth, horizontalInset: horizontalInset, media: media, webpage: webpage) + tableItem.frame = tableItem.frame.offsetBy(dx: 0.0, dy: contentSize.height) - let textItem = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2 - lineInset) - textItem.frame = textItem.frame.offsetBy(dx: horizontalInset + lineInset, dy: contentSize.height) + contentSize.height += tableItem.frame.height + items.append(tableItem) - contentSize.height += textItem.frame.size.height - items.append(textItem) + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) + case let .details(title, blocks, expanded): + var contentSize = CGSize(width: boundingWidth, height: 0.0) + var subitems: [InstantPageItem] = [] + let detailsIndex = detailsIndexCounter + detailsIndexCounter += 1 - var hasCaption: Bool = true - if case .empty = caption { - hasCaption = false + var subDetailsIndex = 0 + + var previousBlock: InstantPageBlock? + for subBlock in blocks { + + let subLayout = layoutInstantPageBlock(webpage: webpage, rtl: rtl, block: subBlock, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: false, previousItems: subitems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &subDetailsIndex, theme: theme, webEmbedHeights: webEmbedHeights) + + + let spacing = spacingBetweenBlocks(upper: previousBlock, lower: subBlock) + let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: 0.0, y: contentSize.height + spacing)) + subitems.append(contentsOf: blockItems) + contentSize.height += subLayout.contentSize.height + spacing + previousBlock = subBlock } - if hasCaption { - contentSize.height += 14.0 + if !blocks.isEmpty { + let closingSpacing = spacingBetweenBlocks(upper: previousBlock, lower: nil) + contentSize.height += closingSpacing + } + + let styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) + styleStack.push(.lineSpacingFactor(0.685)) + let detailsItem = layoutDetailsItem(theme: theme, title: attributedStringForRichText(title, styleStack: styleStack), boundingWidth: boundingWidth, items: subitems, contentSize: contentSize, safeInset: safeInset, rtl: rtl, initiallyExpanded: expanded, index: detailsIndex) + return InstantPageLayout(origin: CGPoint(), contentSize: detailsItem.frame.size, items: [detailsItem]) + case let .relatedArticles(title, articles): + var contentSize = CGSize(width: boundingWidth, height: 0.0) + var items: [InstantPageItem] = [] + + let styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) + styleStack.push(.bold) + let backgroundInset: CGFloat = 14.0 + let (_, textItems, textContentSize) = layoutTextItemWithString(attributedStringForRichText(title, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - backgroundInset * 2.0, offset: CGPoint(x: horizontalInset, y: backgroundInset), media: media, webpage: webpage) + let backgroundItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: textContentSize.height + backgroundInset * 2.0)), shapeFrame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: textContentSize.height + backgroundInset * 2.0)), shape: .rect, color: appAppearance.presentation.colors.grayBackground) + items.append(backgroundItem) + items.append(contentsOf: textItems) + contentSize.height += backgroundItem.frame.height + + for (i, article) in articles.enumerated() { + var cover: TelegramMediaImage? + if let coverId = article.photoId { + cover = media[coverId] as? TelegramMediaImage + } - let styleStack = InstantPageTextStyleStack() - styleStack.push(.textColor(theme.colors.grayText)) - styleStack.push(.fontSize(15.0)) - if presentation.fontSerif { - styleStack.push(.fontSerif(true)) + var styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .article, link: false) + let title = attributedStringForRichText(.plain(article.title ?? ""), styleStack: styleStack) + + styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .caption, link: false) + + var subtext: String? + if article.author != nil || article.date != nil { + if let author = article.author { + if let date = article.date { + subtext = L10n.instantPageRelatedArticleAuthorAndDateTitle(author, stringForDate(date)) + } else { + subtext = author + } + } else if let date = article.date { + subtext = stringForDate(date) + } + } else { + subtext = article.description + } + let description = attributedStringForRichText(.plain(subtext ?? ""), styleStack: styleStack) + + let item = layoutArticleItem(theme: theme, webPage: webpage, title: title, description: description, cover: cover, url: article.url, webpageId: article.webpageId, boundingWidth: boundingWidth, rtl: rtl) + item.frame = item.frame.offsetBy(dx: 0.0, dy: contentSize.height) + contentSize.height += item.frame.height + items.append(item) + + let inset: CGFloat = i == articles.count - 1 ? 0.0 : 17.0 + let lineSize = CGSize(width: boundingWidth - inset, height: .borderSize) + let shapeItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(x: rtl || item.rtl ? 0.0 : inset, y: contentSize.height - lineSize.height), size: lineSize), shapeFrame: CGRect(origin: CGPoint(), size: lineSize), shape: .rect, color: theme.controlColor) + items.append(shapeItem) + } + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) + case let .map(latitude, longitude, zoom, dimensions, caption): + let imageSize = dimensions.size + var filledSize = imageSize.aspectFitted(CGSize(width: boundingWidth - safeInset * 2.0, height: 1200.0)) + + if let size = fillToSize { + filledSize = size + } else if isCover { + filledSize = imageSize.aspectFilled(CGSize(width: boundingWidth - safeInset * 2.0, height: 1.0)) + if !filledSize.height.isZero { + filledSize = filledSize.cropped(CGSize(width: boundingWidth - safeInset * 2.0, height: floor((boundingWidth - safeInset * 2.0) * 3.0 / 5.0))) } - let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) - captionItem.frame = captionItem.frame.offsetBy(dx: horizontalInset + lineInset, dy: contentSize.height) - contentSize.height += captionItem.frame.size.height - items.append(captionItem) } - contentSize.height += verticalInset - items.append(InstantPageShapeItem(frame: CGRect(x: horizontalInset, y: 0, width: 3.0, height: contentSize.height), shapeFrame: CGRect(x: 0.0, y: 0.0, width: 3.0, height: contentSize.height), shape: .roundLine, color: theme.colors.text)) + let map = TelegramMediaMap(latitude: latitude, longitude: longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil) + let attributes: [InstantPageImageAttribute] = [InstantPageMapAttribute(zoom: zoom, dimensions: dimensions.size)] + var contentSize = CGSize(width: boundingWidth - safeInset * 2.0, height: 0.0) + var items: [InstantPageItem] = [] + let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: -1, media: map, webpage: webpage, url: nil, caption: caption.text, credit: caption.credit), attributes: attributes, interactive: true, roundCorners: false, fit: false) - return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) - case let .pullQuote(text, caption): + items.append(mediaItem) + contentSize.height += filledSize.height - let verticalInset: CGFloat = 4.0 - var contentSize = CGSize(width: boundingWidth, height: verticalInset) - var items:[InstantPageItem] = [] + let (captionItems, captionSize) = layoutCaption(caption, contentSize) + items.append(contentsOf: captionItems) + contentSize.height += captionSize.height - let styleStack = InstantPageTextStyleStack() - styleStack.push(.fontSize(17)) - styleStack.push(.fontSerif(true)) - styleStack.push(.italic) - styleStack.push(.textColor(theme.colors.text)) + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) + case let .webEmbed(url, html, dimensions, caption, stretchToWidth, allowScrolling, coverId): + var embedBoundingWidth = boundingWidth - horizontalInset * 2.0 + if stretchToWidth { + embedBoundingWidth = boundingWidth + } - let textItem = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2) - textItem.frame = textItem.frame.offsetBy(dx: floorToScreenPixels((boundingWidth - textItem.frame.size.width) / 2.0), dy: contentSize.height) - textItem.alignment = .center + let embedIndex = embedIndexCounter + embedIndexCounter += 1 - contentSize.height += textItem.frame.size.height - items.append(textItem) + let size: CGSize + if let dimensions = dimensions?.size { + if dimensions.width.isLessThanOrEqualTo(0.0) { + size = CGSize(width: embedBoundingWidth, height: dimensions.height) + } else { + size = dimensions.aspectFitted(CGSize(width: embedBoundingWidth, height: embedBoundingWidth)) + } + } else { + if let height = webEmbedHeights[embedIndex] { + size = CGSize(width: embedBoundingWidth, height: CGFloat(height)) + } else { + size = CGSize(width: embedBoundingWidth, height: 44.0) + } + } - if true { - contentSize.height += 14.0 - - let styleStack = InstantPageTextStyleStack() - styleStack.push(.textColor(theme.colors.grayText)) - styleStack.push(.fontSize(15.0)) - if presentation.fontSerif { - styleStack.push(.fontSerif(true)) + var items: [InstantPageItem] = [] + var contentSize: CGSize + let frame = CGRect(origin: CGPoint(x: floor((boundingWidth - size.width) / 2.0), y: 0.0), size: size) + let item: InstantPageItem + if let url = url, let coverId = coverId, let image = media[coverId] as? TelegramMediaImage { + var url = url + if url.lowercased().contains("youtube"), url.lowercased().contains("embed/") { + url = url.replacingOccurrences(of: "embed/", with: "watch?v=") } - let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) - captionItem.frame = captionItem.frame.offsetBy(dx: floorToScreenPixels((boundingWidth - captionItem.frame.size.width) / 2.0), dy: contentSize.height) - captionItem.alignment = .center - contentSize.height += captionItem.frame.size.height - items.append(captionItem) + let loadedContent = TelegramMediaWebpageLoadedContent(url: url, displayUrl: url, hash: 0, type: "video", websiteName: nil, title: nil, text: nil, embedUrl: url, embedType: "video", embedSize: PixelDimensions(size), duration: nil, author: nil, image: image, file: nil, attributes: [], instantPage: nil) + let content = TelegramMediaWebpageContent.Loaded(loadedContent) + + item = InstantPageImageItem(frame: frame, webPage: webpage, media: InstantPageMedia(index: embedIndex, media: TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.LocalWebpage, id: -1), content: content), webpage: webpage, url: nil, caption: nil, credit: nil), attributes: [], interactive: false, roundCorners: false, fit: false) + + } else { + item = InstantPageWebEmbedItem(frame: frame, url: url, html: html, enableScrolling: allowScrolling) } + items.append(item) + contentSize = item.frame.size - contentSize.height += verticalInset - return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) - case let .audio(id, caption): - break - case let .slideshow(blocks, caption): + let (captionItems, captionSize) = layoutCaption(caption, contentSize) + items.append(contentsOf: captionItems) + contentSize.height += captionSize.height - var medias:[InstantPageMedia] = [] + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) + case let .channelBanner(peer): var contentSize = CGSize(width: boundingWidth, height: 0.0) - var items:[InstantPageItem] = [] + var items: [InstantPageItem] = [] - for subBlock in blocks { - switch subBlock { - case let .image(id, _): - if let photo = media[id] as? TelegramMediaImage, let imageSize = largestImageRepresentation(photo.representations)?.dimensions { - let mediaIndex = mediaIndexCounter - mediaIndexCounter += 1 - let filledSize = imageSize.fit(CGSize(width: boundingWidth, height: 600)) - contentSize.height = min(max(contentSize.height, filledSize.height), boundingWidth) - medias.append(InstantPageMedia(index: mediaIndex, media: photo, caption: richPlainText(caption))) - } - default: - break + var offset: CGFloat = 0.0 + + var previousItemHasRTL = false + if let previousItem = previousItems.last as? InstantPageTextItem { + if previousItem.containsRTL { + previousItemHasRTL = true + } + var minY = previousItem.frame.minY + if let firstItem = previousItems.first { + minY = firstItem.frame.maxY } + offset = minY - previousItem.frame.maxY + } + if !offset.isZero { + offset -= 40.0 + 14.0 } - items.append(InstantPageSlideshowItem(frame: CGRect(x: 0, y: 0, width: boundingWidth, height: contentSize.height), medias: medias)) - - var hasCaption: Bool = true - if case .empty = caption { - hasCaption = false + if let peer = peer { + let item = InstantPagePeerReferenceItem(frame: CGRect(origin: CGPoint(x: 0.0, y: offset), size: CGSize(width: boundingWidth, height: 40.0)), initialPeer: peer, safeInset: safeInset, transparent: !offset.isZero, rtl: rtl || previousItemHasRTL) + items.append(item) + if offset.isZero { + contentSize.height += 40.0 + } } + return InstantPageLayout(origin: CGPoint(x: 0.0, y: offset), contentSize: contentSize, items: items) + case let .anchor(name): + let item = InstantPageAnchorItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: 0.0)), anchor: name) + return InstantPageLayout(origin: CGPoint(), contentSize: item.frame.size, items: [item]) + case let .audio(audioId, caption): + var contentSize = CGSize(width: boundingWidth, height: 0.0) + var items: [InstantPageItem] = [] - if hasCaption { - contentSize.height += 14.0 + if let file = media[audioId] as? TelegramMediaFile { + let mediaIndex = mediaIndexCounter + mediaIndexCounter += 1 + let item = InstantPageAudioItem(frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: boundingWidth, height: 48.0)), media: InstantPageMedia(index: mediaIndex, media: file, webpage: webpage, url: nil, caption: nil, credit: nil)) - let styleStack = InstantPageTextStyleStack() - styleStack.push(.textColor(theme.colors.grayText)) - styleStack.push(.fontSize(15.0)) - if presentation.fontSerif { - styleStack.push(.fontSerif(true)) - } - let captionItem = layoutTextItemWithString(attributedStringForRichText(caption, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0) - captionItem.frame = captionItem.frame.offsetBy(dx: floorToScreenPixels((boundingWidth - captionItem.frame.size.width) / 2.0), dy: contentSize.height) - captionItem.alignment = .center - contentSize.height += captionItem.frame.size.height - items.append(captionItem) + contentSize.height += item.frame.height + items.append(item) + let (captionItems, captionSize) = layoutCaption(caption, contentSize) + items.append(contentsOf: captionItems) + contentSize.height += captionSize.height } - return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) - case .unsupported: - break + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) + default: + return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(), items: []) } - return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(), items: []) } -func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, boundingWidth: CGFloat, presentation: InstantViewAppearance, openChannel:@escaping(TelegramChannel)->Void, joinChannel:@escaping(TelegramChannel)->Void) -> InstantPageLayout { + +func instantPageMedias(for webpage: TelegramMediaWebpage) -> [InstantPageMedia] { + + switch webpage.content { + case let .Loaded(content): + var index: Int = 0 + var detailsIndex: Int = 0 + if let instantPage = content.instantPage { + return instantPageMedias(for: instantPage.blocks, webpage: webpage, medias: instantPage.media, mediaIndexCounter: &index, detailsIndexCounter: &detailsIndex) + } else { + return [] + } + default: + return [] + } +} + +private func instantPageMedias(for blocks: [InstantPageBlock], webpage: TelegramMediaWebpage, medias: [MediaId : Media], mediaIndexCounter: inout Int, detailsIndexCounter: inout Int) -> [InstantPageMedia] { + var current: [InstantPageMedia] = [] + for block in blocks { + switch block { + case let .audio(id, _): + if let media = medias[id] { + current.append(InstantPageMedia(index: mediaIndexCounter, media: media, webpage: webpage, url: nil, caption: nil, credit: nil)) + mediaIndexCounter += 1 + } + case let .collage(blocks, _), let .slideshow(blocks, _): + current.append(contentsOf: instantPageMedias(for: blocks, webpage: webpage, medias: medias, mediaIndexCounter: &mediaIndexCounter, detailsIndexCounter: &detailsIndexCounter)) + case .details(_, blocks, _): + current.append(contentsOf: instantPageMedias(for: blocks, webpage: webpage, medias: medias, mediaIndexCounter: &mediaIndexCounter, detailsIndexCounter: &detailsIndexCounter)) + detailsIndexCounter += 1 + case let .image(id, _, _, _): + if let media = medias[id] { + current.append(InstantPageMedia(index: mediaIndexCounter, media: media, webpage: webpage, url: nil, caption: nil, credit: nil)) + mediaIndexCounter += 1 + } + case let .video(id, _, _, _): + if let media = medias[id] { + current.append(InstantPageMedia(index: mediaIndexCounter, media: media, webpage: webpage, url: nil, caption: nil, credit: nil)) + mediaIndexCounter += 1 + } +// case let .webEmbed(url, _, size, _, _, _, coverId): +// if let url = url, let coverId = coverId, let image = medias[coverId] as? TelegramMediaImage { +// var url = url +// if url.lowercased().contains("youtube"), url.lowercased().contains("embed/") { +// url = url.replacingOccurrences(of: "embed/", with: "watch?v=") +// } +// let loadedContent = TelegramMediaWebpageLoadedContent(url: url, displayUrl: url, hash: 0, type: "video", websiteName: nil, title: nil, text: nil, embedUrl: url, embedType: "video", embedSize: size, duration: nil, author: nil, image: image, file: nil, instantPage: nil) +// let content = TelegramMediaWebpageContent.Loaded(loadedContent) +// +// current.append(InstantPageMedia(index: mediaIndexCounter, media: TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.LocalWebpage, id: -1), content: content), webpage: webpage, url: nil, caption: nil, credit: nil)) +// mediaIndexCounter += 1 +// } + default: + break + } + } + return current +} + + +func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, boundingWidth: CGFloat, safeInset: CGFloat, theme: InstantPageTheme, webEmbedHeights: [Int : CGFloat] = [:]) -> InstantPageLayout { var maybeLoadedContent: TelegramMediaWebpageLoadedContent? if case let .Loaded(content) = webPage.content { maybeLoadedContent = content } + guard let loadedContent = maybeLoadedContent, let instantPage = loadedContent.instantPage else { return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(), items: []) } + let rtl = instantPage.rtl let pageBlocks = instantPage.blocks var contentSize = CGSize(width: boundingWidth, height: 0.0) var items: [InstantPageItem] = [] @@ -620,33 +941,27 @@ func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, boundingWidth: } var mediaIndexCounter: Int = 0 + var embedIndexCounter: Int = 0 + var detailsIndexCounter: Int = 0 var previousBlock: InstantPageBlock? - var previousLayout:InstantPageLayout? for block in pageBlocks { - var spacingBetween = spacingBetweenBlocks(upper: previousBlock, lower: block) - - if (spacingBetween < -.ulpOfOne) { - spacingBetween -= (previousLayout?.contentSize.height ?? 0) - (previousLayout?.items.first?.frame.height ?? 0); - } - - let horizontalInsetBetweenMaxWidth = max(0, (boundingWidth - 720)/2) - - let blockLayout = layoutInstantPageBlock(block, boundingWidth: boundingWidth, horizontalInset: 40 + horizontalInsetBetweenMaxWidth, isCover: false, fillToWidthAndHeight: false, horizontalInsetBetweenMaxWidth: horizontalInsetBetweenMaxWidth, presentation: presentation, media: media, mediaIndexCounter: &mediaIndexCounter, overlay: spacingBetween < -.ulpOfOne, openChannel: openChannel, joinChannel: joinChannel) - - let spacing = blockLayout.contentSize.height > .ulpOfOne ? spacingBetween : 0.0 - + let blockLayout = layoutInstantPageBlock(webpage: webPage, rtl: rtl, block: block, boundingWidth: boundingWidth, horizontalInset: 17.0 + safeInset, safeInset: safeInset, isCover: false, previousItems: items, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, webEmbedHeights: webEmbedHeights) + let spacing = spacingBetweenBlocks(upper: previousBlock, lower: block) let blockItems = blockLayout.flattenedItemsWithOrigin(CGPoint(x: 0.0, y: contentSize.height + spacing)) items.append(contentsOf: blockItems) if CGFloat(0.0).isLess(than: blockLayout.contentSize.height) { - contentSize.height += spacing > -.ulpOfOne ? blockLayout.contentSize.height + spacing : 0.0 + contentSize.height += blockLayout.contentSize.height + spacing previousBlock = block - previousLayout = blockLayout } } let closingSpacing = spacingBetweenBlocks(upper: previousBlock, lower: nil) contentSize.height += closingSpacing +// let feedbackItem = InstantPageFeedbackItem(frame: CGRect(x: 0.0, y: contentSize.height, width: boundingWidth, height: 40.0), webPage: webPage) +// contentSize.height += feedbackItem.frame.height +// items.append(feedbackItem) + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) } diff --git a/Telegram-Mac/InstantPageLayoutSpacings.swift b/Telegram-Mac/InstantPageLayoutSpacings.swift index a26e8a71d1..6d0505d516 100644 --- a/Telegram-Mac/InstantPageLayoutSpacings.swift +++ b/Telegram-Mac/InstantPageLayoutSpacings.swift @@ -7,46 +7,24 @@ // import Cocoa -import TelegramCoreMac +import TelegramCore + func spacingBetweenBlocks(upper: InstantPageBlock?, lower: InstantPageBlock?) -> CGFloat { if let upper = upper, let lower = lower { switch (upper, lower) { - case (_, .cover): + case (_, .cover), (_, .channelBanner), (.details, .details), (.relatedArticles, nil), (_, .anchor): return 0.0 - case (.cover(let block), .channelBanner): - var hasCaption: Bool = true - switch block { - case let .image(_, caption: caption): - if case .empty = caption { - hasCaption = false - } - case let .video(_, caption, _, _): - if case .empty = caption { - hasCaption = false - } - case let .slideshow(_, caption): - if case .empty = caption { - hasCaption = false - } - default: - hasCaption = false - } - - return hasCaption ? -40 : 0 - case (.divider, _), (_, .divider): return 25.0 case (_, .blockQuote), (.blockQuote, _), (_, .pullQuote), (.pullQuote, _): return 27.0 + case (.kicker, .title), (.cover, .title): + return 16.0 case (_, .title): return 20.0 - case (.title, .subtitle): - return 20.0 - case (.title, .authorDate): + case (.title, .authorDate), (.subtitle, .authorDate): return 18.0 - case (.subtitle, .authorDate): - return 20 case (_, .authorDate): return 20.0 case (.title, .paragraph), (.authorDate, .paragraph): @@ -67,29 +45,29 @@ func spacingBetweenBlocks(upper: InstantPageBlock?, lower: InstantPageBlock?) -> return 31.0 case (.preformatted, .list): return 19.0 - case (.paragraph, .list), (.list, .list): - return 31 case (_, .list): - return 20.0 + return 25.0 case (.paragraph, .preformatted): return 19.0 case (_, .preformatted): return 20.0 - case (_, .header): - return 32.0 - case (_, .subheader): + case (_, .header), (_, .subheader): return 32.0 default: return 20.0 } } else if let lower = lower { switch lower { - case .cover, .channelBanner: + case .cover, .channelBanner, .details, .anchor: return 0.0 default: - return 24.0 + return 25.0 } } else { - return 24.0 + if let upper = upper, case .relatedArticles = upper { + return 0.0 + } else { + return 25.0 + } } } diff --git a/Telegram-Mac/InstantPageMedia.swift b/Telegram-Mac/InstantPageMedia.swift index 39ad86a2ef..efae2676ae 100644 --- a/Telegram-Mac/InstantPageMedia.swift +++ b/Telegram-Mac/InstantPageMedia.swift @@ -7,19 +7,31 @@ // import Cocoa -import PostboxMac -import TelegramCoreMac +import Postbox +import TelegramCore + +import TGUIKit + + struct InstantPageMedia: Equatable, Identifiable { let index: Int let media: Media - let caption: String? - + let webpage:TelegramMediaWebpage + let url: InstantPageUrlItem? + let caption: RichText? + let credit: RichText? + var stableId: Int { return index } + func withUpdatedIndex(_ index: Int) -> InstantPageMedia { + return InstantPageMedia(index: index, media: self.media, webpage: webpage, url: self.url, caption: self.caption, credit: self.credit) + } + + static func ==(lhs: InstantPageMedia, rhs: InstantPageMedia) -> Bool { - return lhs.index == rhs.index && lhs.media.isEqual(rhs.media) && lhs.caption == rhs.caption + return lhs.index == rhs.index && lhs.media.isEqual(to: rhs.media) && lhs.url == rhs.url && lhs.caption == rhs.caption && lhs.credit == rhs.credit } } diff --git a/Telegram-Mac/InstantPageMediaItem.swift b/Telegram-Mac/InstantPageMediaItem.swift index 4abedc6dff..6b0dc1e4d7 100644 --- a/Telegram-Mac/InstantPageMediaItem.swift +++ b/Telegram-Mac/InstantPageMediaItem.swift @@ -8,18 +8,21 @@ import Cocoa -import TelegramCoreMac +import TelegramCore + enum InstantPageMediaArguments { case image(interactive: Bool, roundCorners: Bool, fit: Bool) case video(interactive: Bool, autoplay: Bool) - + case map(InstantPageMapAttribute) var isInteractive: Bool { switch self { case .image(let interactive, _, _): return interactive case .video(let interactive, _): return interactive + case .map: + return false } } } @@ -38,24 +41,25 @@ final class InstantPageMediaItem: InstantPageItem { let arguments: InstantPageMediaArguments - let wantsNode: Bool = true + let wantsView: Bool = true let hasLinks: Bool = false - + let separatesTiles: Bool = false + init(frame: CGRect, media: InstantPageMedia, arguments: InstantPageMediaArguments) { self.frame = frame self.media = media self.arguments = arguments } - func node(account: Account) -> InstantPageView? { - return InstantPageMediaView(account: account, media: self.media, arguments: self.arguments) + func view(arguments: InstantPageItemArguments, currentExpandedDetails: [Int : Bool]?) -> (InstantPageView & NSView)? { + return InstantPageMediaView(context: arguments.context, media: self.media, arguments: self.arguments) } func matchesAnchor(_ anchor: String) -> Bool { return false } - func matchesNode(_ node: InstantPageView) -> Bool { + func matchesView(_ node: InstantPageView) -> Bool { if let node = node as? InstantPageMediaView { return node.media == self.media } else { diff --git a/Telegram-Mac/InstantPageMediaView.swift b/Telegram-Mac/InstantPageMediaView.swift index 5ae955e9c8..aeca58876b 100644 --- a/Telegram-Mac/InstantPageMediaView.swift +++ b/Telegram-Mac/InstantPageMediaView.swift @@ -8,24 +8,26 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac +import Postbox +import TelegramCore + +import SwiftSignalKit final class InstantPageMediaView: View, InstantPageView { - private let account: Account + private let context: AccountContext let media: InstantPageMedia private let arguments: InstantPageMediaArguments - + private var iconView:ImageView? + private let imageView: TransformImageView - private let progressView = RadialProgressView() + private let progressView = RadialProgressView(theme:RadialProgressTheme(backgroundColor: .blackTransparent, foregroundColor: .white, icon: playerPlayThumb)) private var currentSize: CGSize? private let fetchedDisposable = MetaDisposable() private let statusDisposable = MetaDisposable() private let playerDisposable = MetaDisposable() private let videoDataDisposable = MetaDisposable() - private var videoPath:String? { + private var videoData:AVGifData? { didSet { updatePlayerIfNeeded() } @@ -33,12 +35,12 @@ final class InstantPageMediaView: View, InstantPageView { @objc private func updatePlayerIfNeeded() { - var s:Signal = .single(Void()) + var s:Signal = .single(Void()) s = s |> delay(0.01, queue: Queue.mainQueue()) playerDisposable.set(s.start(next: { [weak self] in if let strongSelf = self { let accept = strongSelf.window != nil && strongSelf.window!.isKeyWindow - (strongSelf.imageView as? GIFPlayerView)?.set(path: accept ? strongSelf.videoPath : nil) + (strongSelf.imageView as? GIFPlayerView)?.set(data: accept ? strongSelf.videoData : nil) } })) } @@ -59,8 +61,8 @@ final class InstantPageMediaView: View, InstantPageView { updatePlayerListenters() } - init(account: Account, media: InstantPageMedia, arguments: InstantPageMediaArguments) { - self.account = account + init(context: AccountContext, media: InstantPageMedia, arguments: InstantPageMediaArguments) { + self.context = context self.media = media self.arguments = arguments @@ -73,6 +75,8 @@ final class InstantPageMediaView: View, InstantPageView { } else { self.imageView = TransformImageView() } + case .map: + self.imageView = TransformImageView() } @@ -80,59 +84,111 @@ final class InstantPageMediaView: View, InstantPageView { progressView.isHidden = true - self.imageView.alphaTransitionOnFirstUpdate = true + self.imageView.animatesAlphaOnFirstTransition = true self.addSubview(self.imageView) addSubview(progressView) + + let updateProgressState:(MediaResourceStatus)->Void = { [weak self] status in + guard let `self` = self else {return} + + self.progressView.fetchControls = FetchControls(fetch: { [weak self] in + guard let `self` = self else {return} + + switch status { + case .Remote: + if let image = media.media as? TelegramMediaImage { + self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(account: context.account, imageReference: ImageMediaReference.webPage(webPage: WebpageReference(media.webpage), media: image)).start()) + } else if let file = media.media as? TelegramMediaFile { + self.fetchedDisposable.set(freeMediaFileInteractiveFetched(context: context, fileReference: FileMediaReference.webPage(webPage: WebpageReference(media.webpage), media: file)).start()) + } + case .Fetching: + if let image = media.media as? TelegramMediaImage { + chatMessagePhotoCancelInteractiveFetch(account: context.account, photo: image) + } else if let file = media.media as? TelegramMediaFile { + cancelFreeMediaFileInteractiveFetch(context: context, resource: file.resource) + } + default: + break + } + }) + switch status { case let .Fetching(_, progress): - self?.progressView.isHidden = false - self?.progressView.state = .Fetching(progress: progress, force: false) + self.progressView.isHidden = false + self.progressView.state = .Fetching(progress: progress, force: false) case .Local: - self?.progressView.isHidden = true - self?.progressView.state = .None + self.progressView.isHidden = media.media is TelegramMediaImage || self.imageView is GIFPlayerView + self.progressView.state = media.media is TelegramMediaImage || self.imageView is GIFPlayerView ? .None : .Play case .Remote: - self?.progressView.state = .Remote + self.progressView.state = .Remote } } if let image = media.media as? TelegramMediaImage { - let imageSize = largestImageRepresentation(image.representations)?.dimensions ?? NSZeroSize - imageView.setSignal(signal: cachedMedia(media: image, size: imageSize, scale: backingScaleFactor)) - self.imageView.setSignal(account: account, signal: chatMessagePhoto(account: account, photo: image, scale: backingScaleFactor), cacheImage: { [weak self] img in - if let strongSelf = self { - return cacheMedia(signal: img, media: image, size: imageSize, scale: strongSelf.backingScaleFactor) - } else { - return .complete() - } - }) - self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photo: image).start()) + self.imageView.setSignal( chatMessagePhoto(account: context.account, imageReference: ImageMediaReference.webPage(webPage: WebpageReference(media.webpage), media: image), scale: backingScaleFactor)) + self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(account: context.account, imageReference: ImageMediaReference.webPage(webPage: WebpageReference(media.webpage), media: image)).start()) if let largest = largestImageRepresentation(image.representations) { - statusDisposable.set((account.postbox.mediaBox.resourceStatus(largest.resource) |> deliverOnMainQueue).start()) + if arguments.isInteractive { + statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(largest.resource) |> deliverOnMainQueue).start(next: updateProgressState)) + } } } else if let file = media.media as? TelegramMediaFile { - statusDisposable.set((account.postbox.mediaBox.resourceStatus(file.resource) |> deliverOnMainQueue).start(next: updateProgressState)) - self.fetchedDisposable.set(account.postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .video)).start()) + if arguments.isInteractive { + statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(file.resource) |> deliverOnMainQueue).start(next: updateProgressState)) + } + self.fetchedDisposable.set(freeMediaFileInteractiveFetched(context: context, fileReference: FileMediaReference.webPage(webPage: WebpageReference(media.webpage), media: file)).start()) - self.imageView.setSignal(account: account, signal: chatMessageVideo(account: account, video: file, scale: backingScaleFactor)) + if file.mimeType.hasPrefix("image/") && !file.mimeType.hasSuffix("gif") { + self.imageView.setSignal(instantPageImageFile(account: context.account, fileReference: .webPage(webPage: WebpageReference(media.webpage), media: file), scale: backingScaleFactor, fetched: true)) + } else { + self.imageView.setSignal(chatMessageVideo(postbox: context.account.postbox, fileReference: .webPage(webPage: WebpageReference(media.webpage), media: file), scale: backingScaleFactor)) + } switch arguments { case let .video(_, autoplay): if autoplay { - videoDataDisposable.set((account.postbox.mediaBox.resourceData(file.resource) |> deliverOnMainQueue).start(next: { [weak self] data in - if data.complete { - self?.videoPath = data.path - } else { - self?.videoPath = nil - } + videoDataDisposable.set((context.account.postbox.mediaBox.resourceData(file.resource) |> deliverOnResourceQueue |> map { data in return data.complete ? AVGifData.dataFrom(data.path) : nil} |> deliverOnMainQueue).start(next: { [weak self] data in + self?.videoData = data })) } default: break } + } else if let map = media.media as? TelegramMediaMap { + + let iconView = ImageView() + iconView.image = theme.icons.chatMapPin + iconView.sizeToFit() + addSubview(iconView) + + self.iconView = iconView + + var zoom: Int32 = 12 + var dimensions = CGSize(width: 200.0, height: 100.0) + switch arguments { + case let .map(attribute): + zoom = attribute.zoom + dimensions = attribute.dimensions + default: + break + } + + let resource = MapSnapshotMediaResource(latitude: map.latitude, longitude: map.longitude, width: Int32(dimensions.width), height: Int32(dimensions.height), zoom: zoom) + + let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [TelegramMediaImageRepresentation(dimensions: PixelDimensions(dimensions), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(media.webpage), media: image) + let signal = chatWebpageSnippetPhoto(account: context.account, imageReference: imageReference, scale: backingScaleFactor, small: false) + self.imageView.setSignal(signal) + } else if let webPage = media.media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content, let image = content.image { + let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image) + let signal = chatWebpageSnippetPhoto(account: context.account, imageReference: imageReference, scale: backingScaleFactor, small: false) + self.imageView.setSignal(signal) + self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(account: context.account, imageReference: imageReference).start()) + statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(image.representations.last!.resource) |> deliverOnMainQueue).start(next: updateProgressState)) } } @@ -166,13 +222,15 @@ final class InstantPageMediaView: View, InstantPageView { let size = self.bounds.size + iconView?.center() + if self.currentSize != size { self.currentSize = size self.imageView.frame = CGRect(origin: CGPoint(), size: size) if let image = self.media.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { - var imageSize = largest.dimensions.aspectFilled(size) + var imageSize = largest.dimensions.size.aspectFilled(size) var boundingSize = size var radius: CGFloat = 0.0 @@ -181,10 +239,12 @@ final class InstantPageMediaView: View, InstantPageView { radius = roundCorners ? floor(min(size.width, size.height) / 2.0) : 0.0 if fit { - imageSize = largest.dimensions.fitted(size) + imageSize = largest.dimensions.size.fitted(size) boundingSize = imageSize; } + default: + break } @@ -192,11 +252,45 @@ final class InstantPageMediaView: View, InstantPageView { imageView.set(arguments: TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: NSEdgeInsets())) } else if let file = self.media.media as? TelegramMediaFile { - let imageSize = file.dimensions?.aspectFilled(size) ?? size + let imageSize = file.dimensions?.size.aspectFilled(size) ?? size let boundingSize = size imageView.set(arguments: TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: NSEdgeInsets())) + } else if let _ = self.media.media as? TelegramMediaMap { + var imageSize = size + + var boundingSize = size + switch arguments { + case let .map(attribute): + boundingSize = attribute.dimensions + imageSize = attribute.dimensions.aspectFilled(size) + default: + break + } + + + imageView.set(arguments: TransformImageArguments(corners: ImageCorners(radius: .cornerRadius), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: NSEdgeInsets())) + } else if let webPage = media.media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content, let image = content.image, let largest = largestImageRepresentation(image.representations) { + var imageSize = largest.dimensions.size.aspectFilled(size) + var boundingSize = size + var radius: CGFloat = 0.0 + + switch arguments { + case let .image(_, roundCorners, fit): + radius = roundCorners ? floor(min(size.width, size.height) / 2.0) : 0.0 + + if fit { + imageSize = largest.dimensions.size.fitted(size) + boundingSize = imageSize; + } + + default: + + break + } + imageView.set(arguments: TransformImageArguments(corners: ImageCorners(radius: radius), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: NSEdgeInsets())) } + } progressView.center() } diff --git a/Telegram-Mac/InstantPagePeerReferenceItem.swift b/Telegram-Mac/InstantPagePeerReferenceItem.swift new file mode 100644 index 0000000000..81eb77b317 --- /dev/null +++ b/Telegram-Mac/InstantPagePeerReferenceItem.swift @@ -0,0 +1,72 @@ +// +// InstantPagePeerReferenceItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 12/12/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import Postbox +import TelegramCore + + +final class InstantPagePeerReferenceItem: InstantPageItem { + + + let hasLinks: Bool = false + let isInteractive: Bool = false + + func linkSelectionViews() -> [InstantPageLinkSelectionView] { + return [] + } + + var frame: CGRect + let wantsView: Bool = true + let separatesTiles: Bool = false + let medias: [InstantPageMedia] = [] + + let initialPeer: Peer + let safeInset: CGFloat + let transparent: Bool + let rtl: Bool + + init(frame: CGRect, initialPeer: Peer, safeInset: CGFloat, transparent: Bool, rtl: Bool) { + self.frame = frame + self.initialPeer = initialPeer + self.safeInset = safeInset + self.transparent = transparent + self.rtl = rtl + } + + func view(arguments: InstantPageItemArguments, currentExpandedDetails: [Int : Bool]?) -> (InstantPageView & NSView)? { + return nil + } + + func matchesView(_ node: InstantPageView) -> Bool { + return false + } + + func matchesAnchor(_ anchor: String) -> Bool { + return false + } + + func distanceThresholdGroup() -> Int? { + return 5 + } + + func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat { + if count > 3 { + return 1000.0 + } else { + return CGFloat.greatestFiniteMagnitude + } + } + + func linkSelectionRects(at point: CGPoint) -> [CGRect] { + return [] + } + + func drawInTile(context: CGContext) { + } +} diff --git a/Telegram-Mac/InstantPageScrollableItem.swift b/Telegram-Mac/InstantPageScrollableItem.swift new file mode 100644 index 0000000000..ffeabaa6bb --- /dev/null +++ b/Telegram-Mac/InstantPageScrollableItem.swift @@ -0,0 +1,147 @@ + +import Foundation +import TelegramCore + +import Postbox +import TGUIKit + +protocol InstantPageScrollableItem: class, InstantPageItem { + var contentSize: CGSize { get } + var horizontalInset: CGFloat { get } + var isRTL: Bool { get } + + func textItemAtLocation(_ location: CGPoint) -> (InstantPageTextItem, CGPoint)? +} + +private final class InstantPageScrollableContentViewParameters: NSObject { + let item: InstantPageScrollableItem + + init(item: InstantPageScrollableItem) { + self.item = item + super.init() + } +} + +final class InstantPageScrollableContentView: View { + let item: InstantPageScrollableItem + + init(item: InstantPageScrollableItem, additionalViews: [InstantPageView]) { + self.item = item + super.init() + for case let view as NSView in additionalViews { + self.addSubview(view) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + + override func draw(_ layer: CALayer, in ctx: CGContext) { + item.drawInTile(context: ctx) + } + +} + + + + +final class InstantPageScrollableView: ScrollView, InstantPageView { + let item: InstantPageScrollableItem + let contentNode: InstantPageScrollableContentView + let containerView: View = View() + + + override var hasVerticalScroller: Bool { + get { + return false + } + set { + super.hasVerticalScroller = newValue + } + } + + + override func scrollWheel(with event: NSEvent) { + + var scrollPoint = contentView.bounds.origin + let isInverted: Bool = System.isScrollInverted + + if event.scrollingDeltaX != 0 { + if !isInverted { + scrollPoint.x += -event.scrollingDeltaX + } else { + scrollPoint.x -= event.scrollingDeltaX + } + + scrollPoint.x = max(0, min(scrollPoint.x, (documentView!.frame.width) - contentSize.width)) + + clipView.scroll(to: scrollPoint) + + + } else { + superview?.enclosingScrollView?.scrollWheel(with: event) + } + } + + override func viewDidMoveToSuperview() { + super.viewDidMoveToSuperview() + // clipView.scroll(to: NSMakePoint(0, 0)) + } + + +// @objc var contentOffset: NSPoint { +// get { +// return NSZeroPoint +// } +// } + + init(item: InstantPageScrollableItem, arguments: InstantPageItemArguments, additionalViews: [InstantPageView]) { + self.item = item + self.contentNode = InstantPageScrollableContentView(item: item, additionalViews: additionalViews) + super.init(frame: NSZeroRect) + // wantsLayer = true + + containerView.frame = CGRect(origin: CGPoint(x: 0, y: 0.0), size: NSMakeSize(item.contentSize.width + item.horizontalInset * 2, item.contentSize.height)) + containerView.backgroundColor = .clear + self.contentNode.frame = CGRect(origin: CGPoint(x: item.horizontalInset, y: 0.0), size: item.contentSize) + + + containerView.addSubview(contentNode) + self.documentView = containerView + + if item.isRTL { + self.contentView.scroll(to: CGPoint(x: containerView.frame.width - item.frame.width, y: 0.0)) + // self.contentOffset = CGPoint(x: self.contentSize.width - item.frame.width, y: 0.0) + } + + } + + override var needsDisplay: Bool { + didSet { + contentNode.needsDisplay = true + } + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updateIsVisible(_ isVisible: Bool) { + } + + func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) { + } + + + func updateHiddenMedia(media: InstantPageMedia?) { + } + + +} diff --git a/Telegram-Mac/InstantPageSelectText.swift b/Telegram-Mac/InstantPageSelectText.swift index 032fd50f00..719106fdeb 100644 --- a/Telegram-Mac/InstantPageSelectText.swift +++ b/Telegram-Mac/InstantPageSelectText.swift @@ -16,9 +16,11 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit +import AVFoundation struct InstantPageSelectContainer { let attributedString: NSAttributedString @@ -88,13 +90,28 @@ private let instantSelectManager:InstantPageSelectManager = { }() private class InstantViewContentInteractive : InteractionContentViewProtocol { + + private let callback:(AnyHashable)->NSView? init(_ callback:@escaping(AnyHashable)->NSView?) { self.callback = callback } - func contentInteractionView(for stableId: AnyHashable) -> NSView? { + func contentInteractionView(for stableId: AnyHashable, animateIn: Bool) -> NSView? { return callback(stableId) } + func interactionControllerDidFinishAnimation(interactive: Bool, for stableId: AnyHashable) { + + } + + public func addAccesoryOnCopiedView(for stableId: AnyHashable, view: NSView) { + + } + func videoTimebase(for stableId: AnyHashable) -> CMTimebase? { + return nil + } + public func applyTimebase(for stableId: AnyHashable, timebase: CMTimebase?) { + + } } class InstantPageSelectText : NSObject { @@ -111,10 +128,45 @@ class InstantPageSelectText : NSObject { self.scroll = scroll } + private func deepItemsInRect(_ rect: NSRect, itemsInRect:@escaping(NSRect)->[InstantPageItem], effectiveRectForItem:@escaping(InstantPageItem)-> NSRect) -> [(InstantPageItem, InstantPageTableItem?, InstantPageDetailsItem?)] { + return itemsInRect(rect).reduce([], { (current, item) -> [(InstantPageItem, InstantPageTableItem?, InstantPageDetailsItem?)] in + var current = current + if let item = item as? InstantPageTableItem { + var itemRect = effectiveRectForItem(item) + let view = findView(item, self.scroll.documentView) as? InstantPageScrollableView + if let view = view { + itemRect.origin.x -= view.documentOffset.x - item.horizontalInset + } + current += item.itemsIn(NSMakeRect(rect.minX - itemRect.minX, rect.minY - itemRect.minY, 1, 1)).map {($0, Optional(item), nil)} + } else if let item = item as? InstantPageDetailsItem, item.isExpanded { + let itemRect = effectiveRectForItem(item) + current += item.itemsIn(NSMakeRect(rect.minX - itemRect.minX, rect.minY - itemRect.minY, 1, 1)).map {($0, nil, Optional(item))} + } else { + current += [(item, nil, nil)] + } + return current + }) + } - func initializeHandlers(for window:Window, instantLayout: InstantPageLayout, instantPage: InstantPage, account: Account, updateLayout: @escaping()->Void, openInfo:@escaping(PeerId, Bool, MessageId?, ChatInitialAction?)->Void, openNewTab:@escaping (MediaId, String)->Void) { + private func findView(_ item: InstantPageItem, _ currentView: NSView?) -> NSView? { + if let currentView = currentView { + for (_, subview) in currentView.subviews.enumerated() { + if let subview = subview as? InstantPageView, item.matchesView(subview) { + return subview as? NSView + } else if let subview = subview as? InstantPageDetailsView { + if let view = findView(item, subview.contentView) as? InstantPageView, item.matchesView(view) { + return view as? NSView + } + } + } + } + return nil + } + + func initializeHandlers(for window:Window, instantLayout: InstantPageLayout, instantPage: InstantPage, context: AccountContext, updateLayout: @escaping()->Void, openUrl:@escaping(InstantPageUrlItem) -> Void, itemsInRect:@escaping(NSRect) -> [InstantPageItem], effectiveRectForItem:@escaping(InstantPageItem)-> NSRect) { window.removeAllHandlers(for: self) + window.set(mouseHandler: { [weak self, weak window] event -> KeyHandlerResult in @@ -122,7 +174,7 @@ class InstantPageSelectText : NSObject { let isInDocument = self?.scroll.documentView?.isInnerView(window?.contentView?.hitTest(event.locationInWindow)) ?? false self?.started = false - window?.makeFirstResponder(nil) + _ = window?.makeFirstResponder(nil) if isInDocument { if let scroll = self?.scroll, let superview = scroll.superview, let documentView = scroll.documentView, let window = window { let point = superview.convert(window.mouseLocationOutsideOfEventStream, from: nil) @@ -167,118 +219,96 @@ class InstantPageSelectText : NSObject { let isInDocument = self?.scroll.documentView?.isInnerView(window?.contentView?.hitTest(event.locationInWindow)) ?? false - guard isInDocument else { + guard isInDocument, let `self` = self else { return .rejected } let result: KeyHandlerResult - self?.beginInnerLocation = NSZeroPoint + self.beginInnerLocation = NSZeroPoint - let point = self?.scroll.documentView?.convert(event.locationInWindow, from: nil) ?? NSZeroPoint + let point = self.scroll.documentView?.convert(event.locationInWindow, from: nil) ?? NSZeroPoint + _ = window?.makeFirstResponder(instantSelectManager) - let textItem = instantLayout.items(in: NSMakeRect(point.x, point.y, 1, 1)).filter({$0 is InstantPageTextItem}).map({$0 as! InstantPageTextItem}).first - - let item = instantLayout.items(in: NSMakeRect(point.x, point.y, 1, 1)).first - - window?.makeFirstResponder(instantSelectManager) - if event.clickCount == 2, let item = textItem { - - let itemsRect = NSMakeRect(max(point.x, 0), max(point.y, 0), 1, 1) - - - instantSelectManager.removeAll() - - for line in item.lines { - - var minX:CGFloat = item.frame.minX - switch item.alignment { - case .center: - minX += floorToScreenPixels((item.frame.width - line.frame.width) / 2) - default: - break - } - - let rect = NSMakeRect(item.frame.minX, itemsRect.minY < item.frame.minY ? 0 : itemsRect.minY - item.frame.minY, itemsRect.width ,itemsRect.minY < item.frame.minY ? min(itemsRect.maxY - item.frame.minY, item.frame.height) : itemsRect.minY < item.frame.minY ? min(item.frame.maxY - itemsRect.minY, item.frame.height) : itemsRect.height) - - - let beginX = point.x - minX - - if rect.intersects(line.frame) { - instantSelectManager.add(line: line, attributedString: line.selectWord(in: NSMakePoint(beginX, 0), boundingWidth: 1, alignment: item.alignment)) + let rect = NSMakeRect(point.x, point.y, 1.0, 1.0) + + let item = self.deepItemsInRect(rect, itemsInRect: itemsInRect, effectiveRectForItem: effectiveRectForItem).last + + if let item = item { + if let textItem = item.0 as? InstantPageTextItem { + let itemRect: NSRect + if let tableItem = item.1 { + let effectiveRect = effectiveRectForItem(item.1!) + let skipCells = tableItem.itemFrameSkipCells(textItem, effectiveRect: effectiveRect) + itemRect = rect.offsetBy(dx: -skipCells.minX, dy: -skipCells.minY) + } else if let detailsItem = item.2 { + let effectiveRect = detailsItem.effectiveRect + let r = NSMakeRect(rect.minX - effectiveRect.minX, rect.minY - effectiveRect.minY, 1, 1).offsetBy(dx: 0, dy: -detailsItem.titleHeight) + itemRect = detailsItem.deepRect(r).offsetBy(dx: -item.0.frame.minX, dy: -item.0.frame.minY) + } else { + let effectiveRect = effectiveRectForItem(textItem) + itemRect = rect.offsetBy(dx: -effectiveRect.minX, dy: -effectiveRect.minY) } - } - result = .rejected - } else if event.clickCount == 3, let textItem = textItem { - instantSelectManager.removeAll() - for line in textItem.lines { - instantSelectManager.add(line: line, attributedString: line.selectText(in: NSMakeRect(0, 0, line.frame.width, 1), boundingWidth: line.frame.width, alignment: textItem.alignment)) - } - result = .rejected - } else if event.clickCount == 1 { - if let item = textItem, instantSelectManager.isEmpty { - let p = NSMakePoint(point.x - item.frame.minX, point.y - item.frame.minY) - if let link = item.linkAt(point: p) { - - switch link { - case .email(_, let email): - execute(inapp: inAppLink.external(link: email, false)) - case let .url(_ , url, webpageId): + if event.clickCount == 1, instantSelectManager.isEmpty { + if let link = textItem.linkAt(point: itemRect.origin) { + openUrl(link) + result = .rejected + } else { + result = .invokeNext + } + } else if event.clickCount == 2, item.1 == nil { + instantSelectManager.removeAll() + for line in textItem.lines { - let url = url.nsstring - let anchorRange = url.range(of: "#") - var foundAnchor = false - if anchorRange.location != NSNotFound { - let anchor = url.substring(from: anchorRange.location + anchorRange.length) - if !anchor.isEmpty { - for item in instantLayout.items { - if item.matchesAnchor(anchor) { - self?.scroll.clipView.scroll(to: item.frame.origin, animated: true) - foundAnchor = true - break - } - } - } + var minX:CGFloat = item.0.frame.minX + switch textItem.alignment { + case .center: + minX += floorToScreenPixels(System.backingScale, (item.0.frame.width - line.frame.width) / 2) + default: + break } - if !foundAnchor { - if let mediaId = webpageId { - openNewTab(mediaId, url as String) - } else { - execute(inapp: inApp(for: url, account: account, openInfo: openInfo)) - } + let beginX = point.x - minX + if line.frame.intersects(itemRect.offsetBy(dx: -itemRect.minX, dy: 0)) { + instantSelectManager.add(line: line, attributedString: line.selectWord(in: NSMakePoint(beginX, 0), boundingWidth: textItem.frame.width, alignment: textItem.alignment, rect: itemRect)) } - - break - default: - break } + result = .rejected + } else if (event.clickCount == 3 && item.1 == nil) || (event.clickCount == 2 && item.1 != nil) { + instantSelectManager.removeAll() + for line in textItem.lines { + instantSelectManager.add(line: line, attributedString: line.selectText(in: NSMakeRect(0, 0, line.frame.width, 1), boundingWidth: textItem.frame.width, alignment: textItem.alignment)) + } + result = .rejected + } else { + result = .invokeNext } - result = .rejected - - } else if let item = item, instantSelectManager.isEmpty { - let items = instantLayout.items - if item.isInteractive { - let medias = items.filter({$0.isInteractive}).reduce([], { current, item -> [InstantPageMedia] in - var current = current - current.append(contentsOf: item.medias) - return current - }) + } else { + + + if item.0.isInteractive { - self?.interactive = InstantViewContentInteractive({ stableId in - if let index = stableId.base as? Int { - for item in items { - if let _ = item.medias.filter({$0.index == index}).first { - if let subviews = self?.scroll.documentView?.subviews { - for subview in subviews { - if !NSIsEmptyRect(subview.visibleRect), let subview = subview as? InstantPageView, item.matchesNode(subview) { - return subview as? NSView - } - } - } + let items:[InstantPageItem] + if let details = item.2 { + let itemRect = effectiveRectForItem(details) + items = details.deepItemsInRect(NSMakeRect(rect.minX - itemRect.minX, rect.minY - itemRect.minY, 1, 1).offsetBy(dx: 0, dy: -details.titleHeight)).filter({$0.isInteractive}) + } else { + items = instantLayout.items.filter({$0.isInteractive}) + } + + let medias:[InstantPageMedia] = instantLayout.deepMedias + let item = item.0 + + + + self.interactive = InstantViewContentInteractive({ [weak self] stableId in + if let index = stableId.base as? Int, let `self` = self { + if let media = medias.first(where: {$0.index == index}) { + if let item = items.first(where: {$0.medias.contains(media)}) { + return self.findView(item, self.scroll.documentView) } } } @@ -287,24 +317,73 @@ class InstantPageSelectText : NSObject { var index = medias.index(of: item.medias.first!)! - let view = self?.interactive?.contentInteractionView(for: AnyHashable(index)) + let view = self.interactive?.contentInteractionView(for: AnyHashable(index), animateIn: false) if let view = view as? InstantPageSlideshowView { index += view.indexOfDisplayedSlide } - showInstantViewGallery(account: account, medias: medias, firstIndex: index, self?.interactive) + if let file = medias[index].media as? TelegramMediaFile, file.isMusic || file.isVoice { + + if view?.hitTest(point) is RadialProgressView, let view = view as? InstantPageAudioView { + if view.controller != nil { + view.controller?.playOrPause() + } else { + let audio = APSingleResourceController(context: context, wrapper: view.wrapper, streamable: true) + view.controller = audio + audio.start() + } + } + return .invokeNext + } else if let map = medias[index].media as? TelegramMediaMap { + execute(inapp: inAppLink.external(link: "https://maps.google.com/maps?q=\(String(format:"%f", map.latitude)),\(String(format:"%f", map.longitude))", false)) + return .rejected + } + + if let v = view?.hitTest(point) as? RadialProgressView { + switch v.state { + case .Fetching: + return .invokeNext + case .Remote: + return .invokeNext + default: + break + } + } + + + showInstantViewGallery(context: context, medias: medias, firstIndex: index, self.interactive) + result = .rejected + } else { - result = .invokeNext + if let media = item.0.medias.first { + if let webpage = media.media as? TelegramMediaWebpage { + switch webpage.content { + case let .Loaded(content): + execute(inapp: inAppLink.external(link: content.url, false)) + result = .rejected + default: + result = .invokeNext + } + } else { + result = .invokeNext + } + } else { + result = .invokeNext + } } - } else { - result = .invokeNext } } else { result = .invokeNext } + + + + + + if result == .invokeNext { Queue.mainQueue().justDispatch(updateLayout) } else { @@ -319,28 +398,44 @@ class InstantPageSelectText : NSObject { let isInDocument = self?.scroll.documentView?.isInnerView(window?.contentView?.hitTest(event.locationInWindow)) ?? false - guard isInDocument else { + guard isInDocument, let `self` = self else { return .rejected } - let point = self?.scroll.documentView?.convert(event.locationInWindow, from: nil) ?? NSZeroPoint - + let point = self.scroll.documentView?.convert(event.locationInWindow, from: nil) ?? NSZeroPoint - let items = instantLayout.items(in: NSMakeRect(point.x, point.y, 1, 1)).filter({$0 is InstantPageTextItem}).map({$0 as! InstantPageTextItem}) + let rect = NSMakeRect(point.x, point.y, 1.0, 1.0) - if items.isEmpty { - NSCursor.arrow.set() - } else { - if let item = items.first { - let p = NSMakePoint(point.x - item.frame.minX, point.y - item.frame.minY) - if let _ = item.linkAt(point: p) { - NSCursor.pointingHand.set() + for item in self.deepItemsInRect(rect, itemsInRect: itemsInRect, effectiveRectForItem: effectiveRectForItem).reversed() { + if let textItem = item.0 as? InstantPageTextItem { + let itemRect: NSRect + if let tableItem = item.1 { + let effectiveRect = effectiveRectForItem(item.1!) + let skipCells = tableItem.itemFrameSkipCells(textItem, effectiveRect: effectiveRect) + itemRect = rect.offsetBy(dx: -skipCells.minX, dy: -skipCells.minY) + } else if let detailsItem = item.2 { + let effectiveRect = detailsItem.effectiveRect + let r = NSMakeRect(rect.minX - effectiveRect.minX, rect.minY - effectiveRect.minY, 1, 1).offsetBy(dx: 0, dy: -detailsItem.titleHeight) + itemRect = detailsItem.deepRect(r).offsetBy(dx: -item.0.frame.minX, dy: -item.0.frame.minY) } else { + let effectiveRect = effectiveRectForItem(textItem) + itemRect = rect.offsetBy(dx: -effectiveRect.minX, dy: -effectiveRect.minY) + } + if let _ = textItem.linkAt(point: itemRect.origin) { + NSCursor.pointingHand.set() + break + } else if item.1 == nil { NSCursor.iBeam.set() + break + } else { + NSCursor.arrow.set() } + } else { + NSCursor.arrow.set() } - } + + return .invokeNext }, with: self, for: .mouseMoved, priority:.modal) @@ -353,100 +448,213 @@ class InstantPageSelectText : NSObject { self?.scroll.contentView.autoscroll(with: event) if window?.firstResponder != instantSelectManager { - window?.makeFirstResponder(instantSelectManager) + _ = window?.makeFirstResponder(instantSelectManager) } - self?.runSelector(instantLayout, updateLayout: updateLayout) + self?.runSelector(instantLayout, updateLayout: updateLayout, itemsInRect: itemsInRect, effectiveRectForItem: effectiveRectForItem) return .invoked } - return .invoked + return .invokeNext }, with: self, for: .leftMouseDragged, priority:.modal) } - private func runSelector(_ instantPage: InstantPageLayout, updateLayout: @escaping()->Void) { + private func runSelector(_ instantPage: InstantPageLayout, updateLayout: @escaping()->Void, itemsInRect:@escaping(NSRect) -> [InstantPageItem], effectiveRectForItem:@escaping(InstantPageItem)-> NSRect) { instantSelectManager.removeAll() let itemsRect = NSMakeRect(max(min(endInnerLocation.x, beginInnerLocation.x), 0), max(min(endInnerLocation.y, beginInnerLocation.y), 0), abs(endInnerLocation.x - beginInnerLocation.x), abs(endInnerLocation.y - beginInnerLocation.y)) - let items = instantPage.items(in: itemsRect).filter({$0 is InstantPageTextItem}).map({$0 as! InstantPageTextItem}) + guard itemsRect.size != NSZeroSize else { + return + } + + // let items = itemsInRect(itemsRect).compactMap { $0 as? InstantPageTextItem } let reversed = endInnerLocation.y < beginInnerLocation.y + - let multiple = items.count > 1 +// let lines = items.reduce([]) { (current, item) -> [InstantPageTextLine] in +// let itemRect = effectiveRectForItem(item) +// +// let rect = NSMakeRect(itemRect.minX, itemsRect.minY < itemRect.minY ? 0 : itemsRect.minY - itemRect.minY, itemsRect.width ,itemsRect.minY < itemRect.minY ? min(itemsRect.maxY - itemRect.minY, itemRect.height) : itemsRect.minY < itemRect.minY ? min(itemRect.maxY - itemsRect.minY, itemRect.height) : itemsRect.height) +// +// let lines = item.lines.filter { line in +// return line.frame.intersects(rect) +// } +// +// return current + lines +// } +// +// + let items = itemsInRect(itemsRect).reduce([], { (current, item) -> [(InstantPageItem, InstantPageTableItem?, InstantPageDetailsItem?)] in + var current = current + + let itemRect = effectiveRectForItem(item) + let rect = NSMakeRect(itemsRect.minX - itemRect.minX, itemsRect.minY - itemRect.minY, itemsRect.width, itemsRect.height) + if let item = item as? InstantPageTextItem { + current += [(item, nil, nil)] + } else if let item = item as? InstantPageDetailsItem, item.isExpanded { + current += item.itemsIn(rect).map {($0, nil, Optional(item))} + } + return current + }) - for i in 0 ..< items.count { - let item = items[i] - - let initiatedItem = (!multiple || (reversed ? i == items.count - 1 : i == 0)) - - for line in item.lines { - - var minX:CGFloat = item.frame.minX - switch item.alignment { - case .center: - minX += floorToScreenPixels((item.frame.width - line.frame.width) / 2) - default: - break + // let items = deepItemsInRect(itemsRect, itemsInRect: itemsInRect, effectiveRectForItem: effectiveRectForItem) + + var lines:[(InstantPageTextLine, InstantPageTextItem)] = [] + + for item in items { + if let textItem = item.0 as? InstantPageTextItem { + var itemRect: NSRect + if let detailsItem = item.2 { + let effectiveRect = detailsItem.effectiveRect + let r = NSMakeRect(itemsRect.minX - effectiveRect.minX, itemsRect.minY - effectiveRect.minY, itemsRect.width, itemsRect.height).offsetBy(dx: 0, dy: -detailsItem.titleHeight) + itemRect = detailsItem.deepRect(r).offsetBy(dx: -item.0.frame.minX, dy: -item.0.frame.minY) + } else { + let effectiveRect = effectiveRectForItem(textItem) + itemRect = itemsRect.offsetBy(dx: -effectiveRect.minX, dy: -effectiveRect.minY) } - let rect = NSMakeRect(item.frame.minX, itemsRect.minY < item.frame.minY ? 0 : itemsRect.minY - item.frame.minY, itemsRect.width ,itemsRect.minY < item.frame.minY ? min(itemsRect.maxY - item.frame.minY, item.frame.height) : itemsRect.minY < item.frame.minY ? min(item.frame.maxY - itemsRect.minY, item.frame.height) : itemsRect.height) - - let z = NSMakeRect(rect.minX, rect.minY, rect.width, 1) - let n = NSMakeRect(rect.maxX, rect.maxY, rect.width, 1) - - let start = reversed ? n : z - let end = reversed ? z : n - - let beginX = beginInnerLocation.x - minX - let endX = endInnerLocation.x - minX - - if rect.intersects(line.frame) { - - let selectedText:NSAttributedString - - if line.frame.intersects(start) && line.frame.intersects(end) { - if !initiatedItem { - selectedText = line.selectText(in: NSMakeRect(0, 0, endX, 0), boundingWidth: item.frame.width, alignment: item.alignment) - } else { - selectedText = line.selectText(in: NSMakeRect(beginX, 0, endX - beginX, 0), boundingWidth: item.frame.width, alignment: item.alignment) - } - - } else if line.frame.intersects(start) { - - if !initiatedItem { - selectedText = line.selectText(in: NSMakeRect(0, 0, line.frame.width, 0), boundingWidth: item.frame.width, alignment: item.alignment) - } else { - selectedText = line.selectText(in: NSMakeRect(reversed ? 0 : beginX, 0, reversed ? beginX : line.frame.width - beginX, 0), boundingWidth: item.frame.width, alignment: item.alignment) - } - - } else if line.frame.intersects(end) { - if !initiatedItem { - if reversed { - selectedText = line.selectText(in: NSMakeRect(endX, 0, line.frame.width - endX, 0), boundingWidth: item.frame.width, alignment: item.alignment) - } else { - selectedText = line.selectText(in: NSMakeRect(0, 0, endX, 0), boundingWidth: item.frame.width, alignment: item.alignment) - } - } else { - if multiple { - selectedText = line.selectText(in: NSMakeRect(0, 0, line.frame.width, 0), boundingWidth: item.frame.width, alignment: item.alignment) - } else { - selectedText = line.selectText(in: NSMakeRect(reversed ? endX : 0, 0, reversed ? line.frame.width - endX : endX, 0), boundingWidth: item.frame.width, alignment: item.alignment) - } - } - + for line in textItem.lines { + switch textItem.alignment { + case .center: + itemRect.origin.x -= floorToScreenPixels(System.backingScale, (textItem.frame.width - line.frame.width) / 2) + case .right: + itemRect.origin.x = textItem.frame.width - itemRect.origin.x + default: + break + } + if line.frame.intersects(itemRect.insetBy(dx: -itemRect.minX, dy: 0)) { + lines.append((line, textItem)) + } + } + } + } + + for i in 0 ..< lines.count { + let line = lines[i].0 + let item = lines[i].1 + let alignment = item.alignment + + var minX:CGFloat = item.frame.minX + switch alignment { + case .center: + minX += floorToScreenPixels(System.backingScale, (item.frame.width - line.frame.width) / 2) + case .right: + minX = item.frame.width - minX + default: + break + } + + let selectedText:NSAttributedString + + let beginX = beginInnerLocation.x - minX + let endX = endInnerLocation.x - minX + + let firstLine: InstantPageTextLine = reversed ? lines.last!.0 : lines.first!.0 + let endLine: InstantPageTextLine = !reversed ? lines.last!.0 : lines.first!.0 + let multiple: Bool = lines.count > 1 + + if firstLine === line { + if !reversed { + if multiple { + selectedText = line.selectText(in: NSMakeRect(beginX, 0, item.frame.width - beginX, 0), boundingWidth: item.frame.width, alignment: alignment) } else { - selectedText = line.selectText(in: NSMakeRect(0, 0, line.frame.width, 0), boundingWidth: item.frame.width, alignment: item.alignment) + selectedText = line.selectText(in: NSMakeRect(beginX, 0, endX - beginX, 0), boundingWidth: item.frame.width, alignment: alignment) + } + } else { + if multiple { + selectedText = line.selectText(in: NSMakeRect(0, 0, beginX, 0), boundingWidth: item.frame.width, alignment: alignment) + } else { + selectedText = line.selectText(in: NSMakeRect(endX, 0, beginX - endX, 0), boundingWidth: item.frame.width, alignment: alignment) } - - - instantSelectManager.add(line: line, attributedString: selectedText) } + + } else if endLine === line { + if !reversed { + selectedText = line.selectText(in: NSMakeRect(0, 0, endX, 0), boundingWidth: item.frame.width, alignment: alignment) + } else { + selectedText = line.selectText(in: NSMakeRect(endX, 0, item.frame.maxX - endX, 0), boundingWidth: item.frame.width, alignment: alignment) + } + } else { + selectedText = line.selectText(in: NSMakeRect(0, 0, item.frame.width, 0), boundingWidth: item.frame.width, alignment: alignment) } + + instantSelectManager.add(line: line, attributedString: selectedText) } + +// if let item = item, let textItem = item.0 as? InstantPageTextItem { +// let itemRect: NSRect +// if let tableItem = item.1 { +// let effectiveRect = effectiveRectForItem(item.1!) +// let skipCells = tableItem.itemFrameSkipCells(textItem, effectiveRect: effectiveRect) +// itemRect = rect.offsetBy(dx: -skipCells.minX, dy: -skipCells.minY) +// } else if let detailsItem = item.2 { +// let effectiveRect = detailsItem.effectiveRect +// let r = NSMakeRect(rect.minX - effectiveRect.minX, rect.minY - effectiveRect.minY, 1, 1).offsetBy(dx: 0, dy: -detailsItem.titleHeight) +// itemRect = detailsItem.deepRect(r).offsetBy(dx: -item.0.frame.minX, dy: -item.0.frame.minY) +// } else { +// let effectiveRect = effectiveRectForItem(textItem) +// itemRect = rect.offsetBy(dx: -effectiveRect.minX, dy: -effectiveRect.minY) +// } +// +// } + +// for i in 0 ..< lines.count { +// let line = lines[i] +// +// let item = items.first(where: {$0.lines.contains(where: {$0 === line})})! +// +// let itemRect = effectiveRectForItem(item) +// +// var minX:CGFloat = itemRect.minX +// switch item.alignment { +// case .center: +// minX += floorToScreenPixels(System.backingScale, (itemRect.width - line.frame.width) / 2) +// default: +// break +// } +// +// let selectedText:NSAttributedString +// +// let beginX = beginInnerLocation.x - minX +// let endX = endInnerLocation.x - minX +// +// let firstLine: InstantPageTextLine = reversed ? lines.last! : lines.first! +// let endLine: InstantPageTextLine = !reversed ? lines.last! : lines.first! +// let multiple: Bool = lines.count > 1 +// +// if firstLine === line { +// if !reversed { +// if multiple { +// selectedText = line.selectText(in: NSMakeRect(beginX, 0, itemRect.width - beginX, 0), boundingWidth: itemRect.width, alignment: item.alignment) +// } else { +// selectedText = line.selectText(in: NSMakeRect(beginX, 0, endX - beginX, 0), boundingWidth: itemRect.width, alignment: item.alignment) +// } +// } else { +// if multiple { +// selectedText = line.selectText(in: NSMakeRect(0, 0, beginX, 0), boundingWidth: itemRect.width, alignment: item.alignment) +// } else { +// selectedText = line.selectText(in: NSMakeRect(endX, 0, beginX - endX, 0), boundingWidth: itemRect.width, alignment: item.alignment) +// } +// } +// +// } else if endLine === line { +// if !reversed { +// selectedText = line.selectText(in: NSMakeRect(0, 0, endX, 0), boundingWidth: itemRect.width, alignment: item.alignment) +// } else { +// selectedText = line.selectText(in: NSMakeRect(endX, 0, itemRect.maxX - endX, 0), boundingWidth: itemRect.width, alignment: item.alignment) +// } +// } else { +// selectedText = line.selectText(in: NSMakeRect(0, 0, itemRect.width, 0), boundingWidth: itemRect.width, alignment: item.alignment) +// } +// +// instantSelectManager.add(line: line, attributedString: selectedText) +// } + updateLayout() } diff --git a/Telegram-Mac/InstantPageShapeItem.swift b/Telegram-Mac/InstantPageShapeItem.swift index 969e64e291..cedd7ec435 100644 --- a/Telegram-Mac/InstantPageShapeItem.swift +++ b/Telegram-Mac/InstantPageShapeItem.swift @@ -8,7 +8,8 @@ import Cocoa -import TelegramCoreMac +import TelegramCore + enum InstantPageShape { case rect @@ -23,9 +24,10 @@ final class InstantPageShapeItem: InstantPageItem { let color: NSColor let medias: [InstantPageMedia] = [] - let wantsNode: Bool = false + let wantsView: Bool = false let hasLinks: Bool = false - + let separatesTiles: Bool = false + let isInteractive: Bool = false init(frame: CGRect, shapeFrame: CGRect, shape: InstantPageShape, color: NSColor) { @@ -62,11 +64,11 @@ final class InstantPageShapeItem: InstantPageItem { return false } - func matchesNode(_ node: InstantPageView) -> Bool { + func matchesView(_ node: InstantPageView) -> Bool { return false } - func node(account: Account) -> InstantPageView? { + func view(arguments: InstantPageItemArguments, currentExpandedDetails: [Int : Bool]?) -> (InstantPageView & NSView)? { return nil } diff --git a/Telegram-Mac/InstantPageSlideshowItem.swift b/Telegram-Mac/InstantPageSlideshowItem.swift index 854d88c018..436cf37161 100644 --- a/Telegram-Mac/InstantPageSlideshowItem.swift +++ b/Telegram-Mac/InstantPageSlideshowItem.swift @@ -7,19 +7,24 @@ // import Cocoa -import TelegramCoreMac +import TelegramCore + import TGUIKit class InstantPageSlideshowItem: InstantPageItem { var frame: CGRect let medias: [InstantPageMedia] - let wantsNode: Bool = true + let wantsView: Bool = true let hasLinks: Bool = false let isInteractive: Bool = true + let separatesTiles: Bool = false + + let webPage: TelegramMediaWebpage - init(frame: CGRect, medias:[InstantPageMedia]) { + init(frame: CGRect, webPage: TelegramMediaWebpage, medias: [InstantPageMedia]) { self.frame = frame + self.webPage = webPage self.medias = medias } @@ -31,7 +36,7 @@ class InstantPageSlideshowItem: InstantPageItem { return false } - func matchesNode(_ node: InstantPageView) -> Bool { + func matchesView(_ node: InstantPageView) -> Bool { if let view = node as? InstantPageSlideshowView { return self.medias == view.medias } @@ -39,8 +44,8 @@ class InstantPageSlideshowItem: InstantPageItem { } - func node(account: Account) -> InstantPageView? { - return InstantPageSlideshowView(frameRect: frame, medias: medias, account: account) + func view(arguments: InstantPageItemArguments, currentExpandedDetails: [Int : Bool]?) -> (InstantPageView & NSView)? { + return InstantPageSlideshowView(frameRect: frame, medias: medias, context: arguments.context) } func linkSelectionViews() -> [InstantPageLinkSelectionView] { @@ -59,27 +64,38 @@ class InstantPageSlideshowItem: InstantPageItem { class InstantPageSlideshowView : View, InstantPageView { fileprivate let medias: [InstantPageMedia] - private let slideView: MIHSliderView - init(frameRect: NSRect, medias: [InstantPageMedia], account: Account) { + private let slideView: SliderView + init(frameRect: NSRect, medias: [InstantPageMedia], context: AccountContext) { self.medias = medias - slideView = MIHSliderView(frame: NSMakeRect(0, 0, frameRect.width, frameRect.height)) + slideView = SliderView(frame: NSMakeRect(0, 0, frameRect.width, frameRect.height)) super.init(frame: frameRect) addSubview(slideView) for media in medias { - let view = InstantPageMediaView(account: account, media: media, arguments: .image(interactive: true, roundCorners: false, fit: false)) + var arguments: InstantPageMediaArguments = .image(interactive: true, roundCorners: false, fit: false) + if let media = media.media as? TelegramMediaFile { + if media.isVideo { + arguments = .video(interactive: true, autoplay: media.isAnimated) + } + } + let view = InstantPageMediaView(context: context, media: media, arguments: arguments) slideView.addSlide(view) } } + override func layout() { + super.layout() + slideView.center() + } + var indexOfDisplayedSlide: Int { return Int(slideView.indexOfDisplayedSlide) } override func copy() -> Any { - return slideView.displayedSlide.copy() + return slideView.displayedSlide?.copy() ?? super.copy() } func updateIsVisible(_ isVisible: Bool) { diff --git a/Telegram-Mac/InstantPageStoredState.swift b/Telegram-Mac/InstantPageStoredState.swift new file mode 100644 index 0000000000..ac220a947e --- /dev/null +++ b/Telegram-Mac/InstantPageStoredState.swift @@ -0,0 +1,86 @@ +// +// InstantPageStoredState.swift +// Telegram +// +// Created by Mikhail Filimonov on 27/12/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa + +import Foundation +import SwiftSignalKit +import Postbox +import TelegramCore + + +final class InstantPageStoredDetailsState: PostboxCoding { + let index: Int32 + let expanded: Bool + let details: [InstantPageStoredDetailsState] + + init(index: Int32, expanded: Bool, details: [InstantPageStoredDetailsState]) { + self.index = index + self.expanded = expanded + self.details = details + } + + init(decoder: PostboxDecoder) { + self.index = decoder.decodeInt32ForKey("index", orElse: 0) + self.expanded = decoder.decodeBoolForKey("expanded", orElse: false) + self.details = decoder.decodeObjectArrayForKey("details") + } + + func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.index, forKey: "index") + encoder.encodeBool(self.expanded, forKey: "expanded") + encoder.encodeObjectArray(self.details, forKey: "details") + } +} + +final class InstantPageStoredState: PostboxCoding { + let contentOffset: Double + let details: [InstantPageStoredDetailsState] + + init(contentOffset: Double, details: [InstantPageStoredDetailsState]) { + self.contentOffset = contentOffset + self.details = details + } + + init(decoder: PostboxDecoder) { + self.contentOffset = decoder.decodeDoubleForKey("offset", orElse: 0.0) + self.details = decoder.decodeObjectArrayForKey("details") + } + + func encode(_ encoder: PostboxEncoder) { + encoder.encodeDouble(self.contentOffset, forKey: "offset") + encoder.encodeObjectArray(self.details, forKey: "details") + } +} + +func instantPageStoredState(postbox: Postbox, webPage: TelegramMediaWebpage) -> Signal { + return postbox.transaction { transaction -> InstantPageStoredState? in + let key = ValueBoxKey(length: 8) + key.setInt64(0, value: webPage.webpageId.id) + if let entry = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: ApplicationSpecificItemCacheCollectionId.instantPageStoredState, key: key)) as? InstantPageStoredState { + return entry + } else { + return nil + } + } +} + +private let collectionSpec = ItemCacheCollectionSpec(lowWaterItemCount: 100, highWaterItemCount: 200) + +func updateInstantPageStoredStateInteractively(postbox: Postbox, webPage: TelegramMediaWebpage, state: InstantPageStoredState?) -> Signal { + return postbox.transaction { transaction -> Void in + let key = ValueBoxKey(length: 8) + key.setInt64(0, value: webPage.webpageId.id) + let id = ItemCacheEntryId(collectionId: ApplicationSpecificItemCacheCollectionId.instantPageStoredState, key: key) + if let state = state { + transaction.putItemCacheEntry(id: id, entry: state, collectionSpec: collectionSpec) + } else { + transaction.removeItemCacheEntry(id: id) + } + } +} diff --git a/Telegram-Mac/InstantPageTableItem.swift b/Telegram-Mac/InstantPageTableItem.swift new file mode 100644 index 0000000000..1f3c49e65d --- /dev/null +++ b/Telegram-Mac/InstantPageTableItem.swift @@ -0,0 +1,677 @@ + +import Foundation +import TelegramCore + +import Postbox +import TGUIKit + + + + + +private struct TableSide: OptionSet { + var rawValue: Int32 = 0 + + static let top = TableSide(rawValue: 1 << 0) + static let left = TableSide(rawValue: 1 << 1) + static let right = TableSide(rawValue: 1 << 2) + static let bottom = TableSide(rawValue: 1 << 3) + + var uiRectCorner: NSRectCorner { + var corners: NSRectCorner = [] + if self.contains(.top) && self.contains(.left) { + corners.insert(.topLeft) + } + if self.contains(.top) && self.contains(.right) { + corners.insert(.topRight) + } + if self.contains(.bottom) && self.contains(.left) { + corners.insert(.bottomLeft) + } + if self.contains(.bottom) && self.contains(.right) { + corners.insert(.bottomRight) + } + return corners + } +} + +private extension TableHorizontalAlignment { + var textAlignment: NSTextAlignment { + switch self { + case .left: + return .left + case .center: + return .center + case .right: + return .right + } + } +} + +private struct TableCellPosition { + let row: Int + let column: Int +} + +private struct InstantPageTableCellItem { + let position: TableCellPosition + let cell: InstantPageTableCell + let frame: CGRect + let filled: Bool + let textItem: InstantPageTextItem? + let additionalItems: [InstantPageItem] + let adjacentSides: TableSide + + func withRowHeight(_ height: CGFloat) -> InstantPageTableCellItem { + var frame = self.frame + frame = CGRect(x: frame.minX, y: frame.minY, width: frame.width, height: height) + return InstantPageTableCellItem(position: position, cell: self.cell, frame: frame, filled: self.filled, textItem: self.textItem, additionalItems: self.additionalItems, adjacentSides: self.adjacentSides) + } + + func withRTL(_ totalWidth: CGFloat) -> InstantPageTableCellItem { + var frame = self.frame + frame = CGRect(x: totalWidth - frame.minX - frame.width, y: frame.minY, width: frame.width, height: frame.height) + var adjacentSides = self.adjacentSides + if adjacentSides.contains(.left) && !adjacentSides.contains(.right) { + adjacentSides.remove(.left) + adjacentSides.insert(.right) + } + else if adjacentSides.contains(.right) && !adjacentSides.contains(.left) { + adjacentSides.remove(.right) + adjacentSides.insert(.left) + } + return InstantPageTableCellItem(position: position, cell: self.cell, frame: frame, filled: self.filled, textItem: self.textItem, additionalItems: self.additionalItems, adjacentSides: adjacentSides) + } + + var verticalAlignment: TableVerticalAlignment { + return self.cell.verticalAlignment + } + + var colspan: Int { + return self.cell.colspan > 1 ? Int(clamping: self.cell.colspan) : 1 + } + + var rowspan: Int { + return self.cell.rowspan > 1 ? Int(clamping: self.cell.rowspan) : 1 + } +} + +private let tableCellInsets = NSEdgeInsetsMake(14.0, 12.0, 14.0, 12.0) +private let tableBorderWidth: CGFloat = 1.0 +private let tableCornerRadius: CGFloat = 5.0 + +final class InstantPageTableItem: InstantPageScrollableItem { + + var hasLinks: Bool = false + var isInteractive: Bool = false + + func linkSelectionViews() -> [InstantPageLinkSelectionView] { + return [] + } + + var frame: CGRect + let totalWidth: CGFloat + let horizontalInset: CGFloat + let medias: [InstantPageMedia] = [] + let wantsView: Bool = true + let separatesTiles: Bool = false + + let theme: InstantPageTheme + + let isRTL: Bool + fileprivate let cells: [InstantPageTableCellItem] + private let borderWidth: CGFloat + + let anchors: [String: (CGFloat, Bool)] + + fileprivate init(frame: CGRect, totalWidth: CGFloat, horizontalInset: CGFloat, borderWidth: CGFloat, theme: InstantPageTheme, cells: [InstantPageTableCellItem], rtl: Bool) { + self.frame = frame + self.totalWidth = totalWidth + self.horizontalInset = horizontalInset + self.borderWidth = borderWidth + self.theme = theme + self.cells = cells + self.isRTL = rtl + + var anchors: [String: (CGFloat, Bool)] = [:] + for cell in cells { + if let textItem = cell.textItem { + for (anchor, (lineIndex, empty)) in textItem.anchors { + if anchors[anchor] == nil { + let textItemFrame = textItem.frame.offsetBy(dx: cell.frame.minX, dy: cell.frame.minY) + let offset = textItemFrame.minY + textItem.lines[lineIndex].frame.minY + anchors[anchor] = (offset, empty) + } + } + } + } + self.anchors = anchors + } + + func itemsIn( _ rect: NSRect, items: [InstantPageItem] = []) -> [InstantPageItem] { + var items: [InstantPageItem] = items + for cell in cells { + if let textItem = cell.textItem, cell.frame.intersects(rect) { + items.append(textItem) + } + } + return items + } + + func itemFrameSkipCells(_ item: InstantPageTextItem, effectiveRect: NSRect) -> NSRect { + for cell in cells { + if let textItem = cell.textItem, textItem === item { + return item.frame.offsetBy(dx: cell.frame.minX, dy: cell.frame.minY).offsetBy(dx: effectiveRect.minX + horizontalInset, dy: effectiveRect.minY) + } + } + return item.frame + } + + var contentSize: CGSize { + return CGSize(width: self.totalWidth, height: self.frame.height) + } + + func drawInTile(context: CGContext) { + for cell in self.cells { + if cell.textItem == nil && cell.additionalItems.isEmpty { + continue + } + context.saveGState() + context.translateBy(x: cell.frame.minX, y: cell.frame.minY) + + let hasBorder = self.borderWidth > 0.0 + let bounds = CGRect(origin: CGPoint(), size: cell.frame.size) + var path: CGPath? + if !cell.adjacentSides.isEmpty { + + path = CGContext.round(frame: bounds, cornerRadius: tableCornerRadius, rectCorner: cell.adjacentSides.uiRectCorner) + + // path = NSBezierPath(roundedRect: bounds, byRoundingCorners: cell.adjacentSides.uiRectCorner, cornerRadius: CGSize(width: tableCornerRadius, height: tableCornerRadius)) + } + + + if cell.filled { + context.setFillColor(self.theme.tableHeaderColor.cgColor) + } + if self.borderWidth > 0.0 { + context.setStrokeColor(self.theme.tableBorderColor.cgColor) + context.setLineWidth(borderWidth) + } + if let path = path { + context.addPath(path) + var drawMode: CGPathDrawingMode? + switch (cell.filled, hasBorder) { + case (true, false): + drawMode = .fill + case (true, true): + drawMode = .fillStroke + case (false, true): + drawMode = .stroke + default: + break + } + if let drawMode = drawMode { + context.drawPath(using: drawMode) + } + + } else { + if cell.filled { + context.fill(bounds) + } + if hasBorder { + context.stroke(bounds) + } + } + + if let textItem = cell.textItem { + textItem.drawInTile(context: context) + } + context.restoreGState() + } + } + + func matchesAnchor(_ anchor: String) -> Bool { + return false + } + + func view(arguments: InstantPageItemArguments, currentExpandedDetails: [Int : Bool]?) -> (InstantPageView & NSView)? { + var additionalViews: [InstantPageView] = [] + for cell in self.cells { + for item in cell.additionalItems { + if item.wantsView { + if let view = item.view(arguments: arguments, currentExpandedDetails: nil) { + view.frame = item.frame.offsetBy(dx: cell.frame.minX, dy: cell.frame.minY) + additionalViews.append(view) + } + } + } + } + return InstantPageScrollableView(item: self, arguments: arguments, additionalViews: additionalViews) + } + + + func matchesView(_ node: InstantPageView) -> Bool { + if let node = node as? InstantPageScrollableView { + return node.item === self + } + return false + } + + func distanceThresholdGroup() -> Int? { + return nil + } + + func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat { + return 0.0 + } + +// func linkSelectionRects(at point: CGPoint) -> [CGRect] { +// for cell in self.cells { +// if let item = cell.textItem, item.selectable, item.frame.insetBy(dx: -tableCellInsets.left, dy: -tableCellInsets.top).contains(point.offsetBy(dx: -cell.frame.minX - self.horizontalInset, dy: -cell.frame.minY)) { +// let rects = item.linkSelectionRects(at: point.offsetBy(dx: -cell.frame.minX - self.horizontalInset - item.frame.minX, dy: -cell.frame.minY - item.frame.minY)) +// return rects.map { $0.offsetBy(dx: cell.frame.minX + item.frame.minX + self.horizontalInset, dy: cell.frame.minY + item.frame.minY) } +// } +// } +// return [] +// } + + func textItemAtLocation(_ location: CGPoint) -> (InstantPageTextItem, CGPoint)? { + for cell in self.cells { + if let item = cell.textItem, item.selectable, item.frame.insetBy(dx: -tableCellInsets.left, dy: -tableCellInsets.top).contains(location.offsetBy(dx: -cell.frame.minX - self.horizontalInset, dy: -cell.frame.minY)) { + return (item, cell.frame.origin.offsetBy(dx: self.horizontalInset, dy: 0.0)) + } + } + return nil + } +} + +private struct TableRow { + var minColumnWidths: [Int : CGFloat] + var maxColumnWidths: [Int : CGFloat] +} + +private func offsetForHorizontalAlignment(_ alignment: TableHorizontalAlignment, width: CGFloat, boundingWidth: CGFloat, insets: NSEdgeInsets) -> CGFloat { + switch alignment { + case .left: + return insets.left + case .center: + return (boundingWidth - width) / 2.0 + case .right: + return boundingWidth - width - insets.right + } +} + +private func offestForVerticalAlignment(_ verticalAlignment: TableVerticalAlignment, height: CGFloat, boundingHeight: CGFloat, insets: NSEdgeInsets) -> CGFloat { + switch verticalAlignment { + case .top: + return insets.top + case .middle: + return (boundingHeight - height) / 2.0 + case .bottom: + return boundingHeight - height - insets.bottom + } +} + +func layoutTableItem(rtl: Bool, rows: [InstantPageTableRow], styleStack: InstantPageTextStyleStack, theme: InstantPageTheme, bordered: Bool, striped: Bool, boundingWidth: CGFloat, horizontalInset: CGFloat, media: [MediaId: Media], webpage: TelegramMediaWebpage) -> InstantPageTableItem { + if rows.count == 0 { + return InstantPageTableItem(frame: CGRect(), totalWidth: 0.0, horizontalInset: 0.0, borderWidth: 0.0, theme: theme, cells: [], rtl: rtl) + } + + let borderWidth = bordered ? tableBorderWidth : 0.0 + let totalCellPadding = tableCellInsets.left + tableCellInsets.right + let cellWidthLimit = boundingWidth - totalCellPadding + var tableRows: [TableRow] = [] + var columnCount: Int = 0 + + var columnSpans: [Range : (CGFloat, CGFloat)] = [:] + var rowSpans: [Int : [(Int, Int)]] = [:] + + var r: Int = 0 + for row in rows { + var minColumnWidths: [Int : CGFloat] = [:] + var maxColumnWidths: [Int : CGFloat] = [:] + var i: Int = 0 + for cell in row.cells { + if let rowSpan = rowSpans[r] { + for columnAndSpan in rowSpan { + if columnAndSpan.0 == i { + i += columnAndSpan.1 + } else { + break + } + } + } + + var minCellWidth: CGFloat = 1.0 + var maxCellWidth: CGFloat = 1.0 + if let text = cell.text { + if let shortestTextItem = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack, boundingWidth: cellWidthLimit - totalCellPadding), boundingWidth: cellWidthLimit, offset: CGPoint(), media: media, webpage: webpage, minimizeWidth: true).0 { + minCellWidth = shortestTextItem.effectiveWidth() + totalCellPadding + } + + if let longestTextItem = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack, boundingWidth: cellWidthLimit - totalCellPadding), boundingWidth: cellWidthLimit, offset: CGPoint(), media: media, webpage: webpage).0 { + maxCellWidth = max(minCellWidth, longestTextItem.effectiveWidth() + totalCellPadding) + } + } + if cell.colspan > 1 { + minColumnWidths[i] = 1.0 + maxColumnWidths[i] = 1.0 + + let spanRange = i ..< i + Int(cell.colspan) + if let (minSpanWidth, maxSpanWidth) = columnSpans[spanRange] { + columnSpans[spanRange] = (max(minSpanWidth, minCellWidth), max(maxSpanWidth, maxCellWidth)) + } else { + columnSpans[spanRange] = (minCellWidth, maxCellWidth) + } + } else { + minColumnWidths[i] = minCellWidth + maxColumnWidths[i] = maxCellWidth + } + + let colspan = cell.colspan > 1 ? Int(clamping: cell.colspan) : 1 + if cell.rowspan > 1 { + for j in r ..< r + Int(cell.rowspan) { + if rowSpans[j] == nil { + rowSpans[j] = [(i, colspan)] + } else { + rowSpans[j]!.append((i, colspan)) + } + } + } + + i += colspan + } + tableRows.append(TableRow(minColumnWidths: minColumnWidths, maxColumnWidths: maxColumnWidths)) + columnCount = max(columnCount, row.cells.count) + r += 1 + } + + let maxContentWidth = boundingWidth - borderWidth + var availableWidth = maxContentWidth + var minColumnWidths: [Int : CGFloat] = [:] + var maxColumnWidths: [Int : CGFloat] = [:] + var maxTotalWidth: CGFloat = 0.0 + for i in 0 ..< columnCount { + var minWidth: CGFloat = 1.0 + var maxWidth: CGFloat = 1.0 + for row in tableRows { + if let columnWidth = row.minColumnWidths[i] { + minWidth = max(minWidth, columnWidth) + } + if let columnWidth = row.maxColumnWidths[i] { + maxWidth = max(maxWidth, columnWidth) + } + } + minColumnWidths[i] = minWidth + maxColumnWidths[i] = maxWidth + availableWidth -= minWidth + maxTotalWidth += maxWidth + } + + for (range, span) in columnSpans { + let (minSpanWidth, maxSpanWidth) = span + + var minWidth: CGFloat = 0.0 + var maxWidth: CGFloat = 0.0 + for i in range { + if let columnWidth = minColumnWidths[i] { + minWidth += columnWidth + } + if let columnWidth = maxColumnWidths[i] { + maxWidth += columnWidth + } + } + + if minWidth < minSpanWidth { + let delta = minSpanWidth - minWidth + for i in range { + if let columnWidth = minColumnWidths[i] { + let growth = floor(delta / CGFloat(range.count)) + minColumnWidths[i] = columnWidth + growth + availableWidth -= growth + } + } + } + + if maxWidth < maxSpanWidth { + let delta = maxSpanWidth - maxWidth + for i in range { + if let columnWidth = maxColumnWidths[i] { + let growth = round(delta / CGFloat(range.count)) + maxColumnWidths[i] = columnWidth + growth + maxTotalWidth += growth + } + } + } + } + + var totalWidth = maxTotalWidth + var finalColumnWidths: [Int : CGFloat] = [:] + let widthToDistribute: CGFloat + if availableWidth > 0 { + widthToDistribute = availableWidth + finalColumnWidths = minColumnWidths + } else { + widthToDistribute = maxContentWidth - maxTotalWidth + finalColumnWidths = maxColumnWidths + } + + if widthToDistribute > 0.0 { + var distributedWidth = widthToDistribute + for i in 0 ..< finalColumnWidths.count { + var width = finalColumnWidths[i]! + let maxWidth = maxColumnWidths[i]! + let growth = min(round(widthToDistribute * maxWidth / maxTotalWidth), distributedWidth) + width += growth + distributedWidth -= growth + finalColumnWidths[i] = width + } + totalWidth = boundingWidth + } else { + totalWidth += borderWidth + } + + var finalizedCells: [InstantPageTableCellItem] = [] + var origin: CGPoint = CGPoint(x: borderWidth / 2.0, y: borderWidth / 2.0) + var totalHeight: CGFloat = 0.0 + var rowHeights: [Int : CGFloat] = [:] + + var awaitingSpanCells: [Int : [(Int, InstantPageTableCellItem)]] = [:] + + for i in 0 ..< rows.count { + let row = rows[i] + var maxRowHeight: CGFloat = 0.0 + var isEmptyRow = true + origin.x = borderWidth / 2.0 + + var k: Int = 0 + var rowCells: [InstantPageTableCellItem] = [] + + for cell in row.cells { + if let cells = awaitingSpanCells[i] { + isEmptyRow = false + for colAndCell in cells { + let cell = colAndCell.1 + if cell.position.column == k { + for j in 0 ..< cell.colspan { + if let width = finalColumnWidths[k + j] { + origin.x += width + } + } + k += cell.colspan + } else { + break + } + } + } + + var cellWidth: CGFloat = 0.0 + let colspan: Int = cell.colspan > 1 ? Int(clamping: cell.colspan) : 1 + let rowspan: Int = cell.rowspan > 1 ? Int(clamping: cell.rowspan) : 1 + for j in 0 ..< colspan { + if let width = finalColumnWidths[k + j] { + cellWidth += width + } + } + + var item: InstantPageTextItem? + var additionalItems: [InstantPageItem] = [] + var cellHeight: CGFloat? + if let text = cell.text { + let (textItem, items, _) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack, boundingWidth: cellWidth - totalCellPadding), boundingWidth: cellWidth - totalCellPadding, alignment: cell.alignment.textAlignment, offset: CGPoint(), media: media, webpage: webpage) + if let textItem = textItem { + isEmptyRow = false + textItem.frame = textItem.frame.offsetBy(dx: tableCellInsets.left, dy: 0.0) + cellHeight = ceil(textItem.frame.height) + tableCellInsets.top + tableCellInsets.bottom + + item = textItem + } + for var item in items where !(item is InstantPageTextItem) { + isEmptyRow = false + if textItem == nil { + let offset = offsetForHorizontalAlignment(cell.alignment, width: item.frame.width, boundingWidth: cellWidth, insets: tableCellInsets) + item.frame = item.frame.offsetBy(dx: offset, dy: 0.0) + } else { + item.frame = item.frame.offsetBy(dx: tableCellInsets.left, dy: 0.0) + } + let height = ceil(item.frame.height) + tableCellInsets.top + tableCellInsets.bottom - 10.0 + if let currentCellHeight = cellHeight { + cellHeight = max(currentCellHeight, height) + } else { + cellHeight = height + } + + additionalItems.append(item) + } + } + var filled = cell.header + if !filled && striped { + filled = i % 2 == 0 + } + var adjacentSides: TableSide = [] + if i == 0 { + adjacentSides.insert(.top) + } + if i == rows.count - 1 { + adjacentSides.insert(.bottom) + } + if k == 0 { + adjacentSides.insert(.left) + } + if k + colspan == columnCount { + adjacentSides.insert(.right) + } + let rowCell = InstantPageTableCellItem(position: TableCellPosition(row: i, column: k), cell: cell, frame: CGRect(x: origin.x, y: origin.y, width: cellWidth, height: cellHeight ?? 20.0), filled: filled, textItem: item, additionalItems: additionalItems, adjacentSides: adjacentSides) + if rowspan == 1 { + rowCells.append(rowCell) + if let cellHeight = cellHeight { + maxRowHeight = max(maxRowHeight, cellHeight) + } + } else { + for j in i ..< i + rowspan { + if awaitingSpanCells[j] == nil { + awaitingSpanCells[j] = [(k, rowCell)] + } else { + awaitingSpanCells[j]!.append((k, rowCell)) + } + } + } + + k += colspan + origin.x += cellWidth + } + + let finalizeCell: (InstantPageTableCellItem, inout [InstantPageTableCellItem], CGFloat) -> Void = { cell, cells, height in + let updatedCell = cell.withRowHeight(height) + if let textItem = updatedCell.textItem { + let offset = offestForVerticalAlignment(cell.verticalAlignment, height: textItem.frame.height, boundingHeight: height, insets: tableCellInsets) + updatedCell.textItem!.frame = textItem.frame.offsetBy(dx: 0.0, dy: offset) + + for var item in updatedCell.additionalItems { + item.frame = item.frame.offsetBy(dx: 0.0, dy: offset) + } + } else { + for var item in updatedCell.additionalItems { + let offset = offestForVerticalAlignment(cell.verticalAlignment, height: item.frame.height, boundingHeight: height, insets: tableCellInsets) + item.frame = item.frame.offsetBy(dx: 0.0, dy: offset) + } + } + cells.append(updatedCell) + } + + if !isEmptyRow { + rowHeights[i] = maxRowHeight + } else { + rowHeights[i] = 0.0 + maxRowHeight = 0.0 + } + + var completedSpans = [Int : Set]() + if let cells = awaitingSpanCells[i] { + for colAndCell in cells { + let cell = colAndCell.1 + let utmostRow = cell.position.row + cell.rowspan - 1 + if rowHeights[utmostRow] == nil { + continue + } + + var cellHeight: CGFloat = 0.0 + for k in cell.position.row ..< utmostRow + 1 { + if let height = rowHeights[k] { + cellHeight += height + } + + if completedSpans[k] == nil { + var set = Set() + set.insert(colAndCell.0) + completedSpans[k] = set + } else { + completedSpans[k]!.insert(colAndCell.0) + } + } + + if cell.frame.height > cellHeight { + let delta = cell.frame.height - cellHeight + cellHeight = cell.frame.height + maxRowHeight += delta + rowHeights[i] = maxRowHeight + } + + finalizeCell(cell, &finalizedCells, cellHeight) + } + } + + for cell in rowCells { + finalizeCell(cell, &finalizedCells, maxRowHeight) + } + + if !completedSpans.isEmpty { + awaitingSpanCells = awaitingSpanCells.reduce([Int : [(Int, InstantPageTableCellItem)]]()) { (current, rowAndValue) in + var result = current + let cells = rowAndValue.value.filter({ column, cell in + if let completedSpansInRow = completedSpans[rowAndValue.key] { + return !completedSpansInRow.contains(column) + } else { + return true + } + }) + if !cells.isEmpty { + result[rowAndValue.key] = cells + } + return result + } + } + + if !isEmptyRow { + totalHeight += maxRowHeight + origin.y += maxRowHeight + } + } + totalHeight += borderWidth + + if rtl { + finalizedCells = finalizedCells.map { $0.withRTL(totalWidth) } + } + + return InstantPageTableItem(frame: CGRect(x: 0.0, y: 0.0, width: boundingWidth + horizontalInset * 2.0, height: totalHeight), totalWidth: totalWidth, horizontalInset: horizontalInset, borderWidth: borderWidth, theme: theme, cells: finalizedCells, rtl: rtl) +} diff --git a/Telegram-Mac/InstantPageTextItem.swift b/Telegram-Mac/InstantPageTextItem.swift index 721deea51b..1b4b4f011d 100644 --- a/Telegram-Mac/InstantPageTextItem.swift +++ b/Telegram-Mac/InstantPageTextItem.swift @@ -8,8 +8,33 @@ import Cocoa import TGUIKit +import TelegramCore -import TelegramCoreMac +import Postbox + + +extension NSAttributedString.Key { + static let URL: NSAttributedString.Key = NSAttributedString.Key("TelegramURL") +} + +final class InstantPageUrlItem: Equatable { + let url: String + let webpageId: MediaId? + + init(url: String, webpageId: MediaId?) { + self.url = url + self.webpageId = webpageId + } + + public static func ==(lhs: InstantPageUrlItem, rhs: InstantPageUrlItem) -> Bool { + return lhs.url == rhs.url && lhs.webpageId == rhs.webpageId + } +} + +struct InstantPageTextMarkedItem { + let frame: CGRect + let color: NSColor +} struct InstantPageTextUrlItem { let frame: CGRect @@ -20,53 +45,100 @@ struct InstantPageTextStrikethroughItem { let frame: CGRect } +struct InstantPageTextImageItem { + let frame: CGRect + let range: NSRange + let id: MediaId +} + +struct InstantPageTextAnchorItem { + let name: String + let empty: Bool +} + final class InstantPageTextLine { let line: CTLine + let range: NSRange let frame: CGRect - let urlItems: [InstantPageTextUrlItem] let strikethroughItems: [InstantPageTextStrikethroughItem] + let markedItems: [InstantPageTextMarkedItem] + let imageItems: [InstantPageTextImageItem] + let anchorItems: [InstantPageTextAnchorItem] + let isRTL: Bool let attributedString: NSAttributedString + + let separatesTiles: Bool = false + + var selectRect: NSRect = NSZeroRect - init(line: CTLine, attributedString: NSAttributedString, frame: CGRect, urlItems: [InstantPageTextUrlItem], strikethroughItems: [InstantPageTextStrikethroughItem]) { + + init(line: CTLine, attributedString: NSAttributedString, range: NSRange, frame: CGRect, strikethroughItems: [InstantPageTextStrikethroughItem], markedItems: [InstantPageTextMarkedItem], imageItems: [InstantPageTextImageItem], anchorItems: [InstantPageTextAnchorItem], isRTL: Bool) { self.line = line - self.frame = frame self.attributedString = attributedString - self.urlItems = urlItems + self.range = range + self.frame = frame self.strikethroughItems = strikethroughItems + self.markedItems = markedItems + self.imageItems = imageItems + self.anchorItems = anchorItems + self.isRTL = isRTL + } - func linkAt(point: NSPoint) -> RichText? { - let index: CFIndex = CTLineGetStringIndexForPosition(line, point) - if index >= 0 && index < attributedString.length { - return attributedString.attribute(NSAttributedStringKey.link, at: index, effectiveRange: nil) as? RichText + func linkAt(point: NSPoint) -> InstantPageUrlItem? { + if point.x >= 0 && point.x <= frame.width { + let index: CFIndex = CTLineGetStringIndexForPosition(line, point) + if index >= 0 && index < attributedString.length { + return attributedString.attribute(.URL, at: index, effectiveRange: nil) as? InstantPageUrlItem + } } + return nil } func selectText(in rect: NSRect, boundingWidth: CGFloat, alignment: NSTextAlignment) -> NSAttributedString { + var rect = rect + if isRTL { + rect.origin.x -= (boundingWidth - frame.width) + } let startIndex: CFIndex = CTLineGetStringIndexForPosition(line, NSMakePoint(rect.minX, 0)) let endIndex: CFIndex = CTLineGetStringIndexForPosition(line, NSMakePoint(rect.maxX, 0)) + + var startOffset = CTLineGetOffsetForStringIndex(line, startIndex, nil) var endOffset = CTLineGetOffsetForStringIndex(line, endIndex, nil) - + switch alignment { case .center: - let additional = floorToScreenPixels((boundingWidth - frame.width) / 2) + let additional = floorToScreenPixels(System.backingScale, (boundingWidth - frame.width) / 2) startOffset += additional endOffset += additional + case .right: + startOffset = boundingWidth - startOffset + endOffset = boundingWidth - endOffset default: break } - selectRect = NSMakeRect(startOffset, frame.minY - 2, endOffset - startOffset, frame.height + 6) - return attributedString.attributedSubstring(from: NSMakeRange(startIndex, endIndex - startIndex)) + var selectRect = NSMakeRect(startOffset, frame.minY - 2, endOffset - startOffset, frame.height + 6) + + if isRTL { + selectRect.origin.x += (boundingWidth - frame.width) + } + + self.selectRect = selectRect + return attributedString.attributedSubstring(from: NSMakeRange(min(startIndex, endIndex), abs(endIndex - startIndex))) } - func selectWord(in point: NSPoint, boundingWidth: CGFloat, alignment: NSTextAlignment) -> NSAttributedString { + func selectWord(in point: NSPoint, boundingWidth: CGFloat, alignment: NSTextAlignment, rect: NSRect) -> NSAttributedString { + var point = point + if isRTL { + point.x -= (boundingWidth - frame.width) + } let startIndex: CFIndex = CTLineGetStringIndexForPosition(line, point) @@ -77,7 +149,7 @@ final class InstantPageTextLine { var range = NSMakeRange(startIndex, 1) let char:NSString = attributedString.string.nsstring.substring(with: range) as NSString var effectiveRange:NSRange = NSMakeRange(NSNotFound, 0) - let check = attributedString.attribute(NSAttributedStringKey.link, at: range.location, effectiveRange: &effectiveRange) + let check = attributedString.attribute(NSAttributedString.Key.link, at: range.location, effectiveRange: &effectiveRange) if check != nil && effectiveRange.location != NSNotFound { return attributedString.attributedSubstring(from: effectiveRange) } @@ -120,14 +192,25 @@ final class InstantPageTextLine { switch alignment { case .center: - let additional = floorToScreenPixels((boundingWidth - frame.width) / 2) + let additional = floorToScreenPixels(System.backingScale, (boundingWidth - frame.width) / 2) startOffset += additional endOffset += additional + case .right: + startOffset = boundingWidth - startOffset + endOffset = boundingWidth - endOffset default: break } + selectRect = NSMakeRect(startOffset, frame.minY - 2, endOffset - startOffset, frame.height + 6) + + + + if isRTL { + selectRect.origin.x += (boundingWidth - frame.width) + } + return attributedString.attributedSubstring(from: range) } @@ -136,39 +219,71 @@ final class InstantPageTextLine { } } + +private func frameForLine(_ line: InstantPageTextLine, boundingWidth: CGFloat, alignment: NSTextAlignment) -> CGRect { + var lineFrame = line.frame + if alignment == .center { + lineFrame.origin.x = floor((boundingWidth - lineFrame.size.width) / 2.0) + } else if alignment == .right || (alignment == .natural && line.isRTL) { + lineFrame.origin.x = boundingWidth - lineFrame.size.width + } + return lineFrame +} + final class InstantPageTextItem: InstantPageItem { + var hasLinks: Bool = false + + let isInteractive: Bool = false + + let attributedString: NSAttributedString let lines: [InstantPageTextLine] - let hasLinks: Bool + let rtlLineIndices: Set var frame: CGRect - var alignment: NSTextAlignment = .left + let alignment: NSTextAlignment let medias: [InstantPageMedia] = [] - let wantsNode: Bool = false + let anchors: [String: (Int, Bool)] + let wantsView: Bool = false + let separatesTiles: Bool = false + var selectable: Bool = true - let isInteractive: Bool = false + var containsRTL: Bool { + return !self.rtlLineIndices.isEmpty + } - init(frame: CGRect, lines: [InstantPageTextLine]) { + init(frame: CGRect, attributedString: NSAttributedString, alignment: NSTextAlignment, lines: [InstantPageTextLine]) { + self.attributedString = attributedString + self.alignment = alignment self.frame = frame self.lines = lines - var hasLinks = false + var index = 0 + var rtlLineIndices = Set() + var anchors: [String: (Int, Bool)] = [:] for line in lines { - if !line.urlItems.isEmpty { - hasLinks = true + if line.isRTL { + rtlLineIndices.insert(index) + } + for anchor in line.anchorItems { + anchors[anchor.name] = (index, anchor.empty) } + index += 1 } - self.hasLinks = hasLinks + self.rtlLineIndices = rtlLineIndices + self.anchors = anchors } - func linkAt(point: NSPoint) -> RichText? { + func linkAt(point: NSPoint) -> InstantPageUrlItem? { for line in lines { - var point = NSMakePoint(min(max(point.x, 0), frame.width), point.y) + var point = NSMakePoint(point.x, point.y) switch alignment { case .center: - point.x -= floorToScreenPixels((frame.width - line.frame.width) / 2) + point.x -= floorToScreenPixels(System.backingScale, (frame.width - line.frame.width) / 2) + case .right: + point.x = frame.width - point.x default: break } - if line.frame.minY < point.y && line.frame.maxY > point.y { + if NSPointInRect(point, line.frame) { return line.linkAt(point: NSMakePoint(point.x, 0)) } } @@ -182,35 +297,52 @@ final class InstantPageTextItem: InstantPageItem { let clipRect = context.boundingBoxOfClipPath - - let upperOriginBound = clipRect.minY - 10.0 let lowerOriginBound = clipRect.maxY + 10.0 let boundsWidth = self.frame.size.width - for line in self.lines { - let lineFrame = line.frame + for i in 0 ..< self.lines.count { + let line = self.lines[i] + let lineFrame = frameForLine(line, boundingWidth: boundsWidth, alignment: self.alignment) if lineFrame.maxY < upperOriginBound || lineFrame.minY > lowerOriginBound { continue } - var lineOrigin = lineFrame.origin - if self.alignment == .center { - lineOrigin.x = floor((boundsWidth - lineFrame.size.width) / 2.0) - } + let lineOrigin = lineFrame.origin - context.setFillColor(theme.colors.selectText.cgColor) - context.fill(line.selectRect) + context.textPosition = CGPoint(x: lineOrigin.x, y: lineOrigin.y + lineFrame.size.height) + + + if !line.markedItems.isEmpty { + context.saveGState() + for item in line.markedItems { + let itemFrame = item.frame.offsetBy(dx: lineFrame.minX, dy: 0.0) + context.setFillColor(item.color.cgColor) + + let height = floor(item.frame.size.height * 2.2) + let rect = CGRect(x: itemFrame.minX - 2.0, y: floor(itemFrame.minY + (itemFrame.height - height) / 2.0), width: itemFrame.width + 4.0, height: height) + let path = CGPath(roundedRect: rect, cornerWidth: 3, cornerHeight: 3, transform: nil) //NSBezierPath(roundedRect: rect, xRadius: 3.0, yRadius: 3.0) + context.addPath(path) + context.fillPath() + } + context.restoreGState() + } + context.setFillColor(theme.colors.selectText.cgColor) + context.fill(line.selectRect) + CTLineDraw(line.line, context) if !line.strikethroughItems.isEmpty { for item in line.strikethroughItems { - context.fill(CGRect(x: item.frame.minX, y: item.frame.minY + floor((lineFrame.size.height / 2.0) + 1.0), width: item.frame.size.width, height: 1.0)) + let itemFrame = item.frame.offsetBy(dx: lineFrame.minX, dy: 0.0) + context.fill(CGRect(x: itemFrame.minX, y: itemFrame.minY + floor((lineFrame.size.height / 2.0) + 1.0), width: itemFrame.size.width, height: 1.0)) } } + + } context.restoreGState() @@ -224,11 +356,11 @@ final class InstantPageTextItem: InstantPageItem { return false } - func node(account: Account) -> InstantPageView? { + func view(arguments: InstantPageItemArguments, currentExpandedDetails: [Int : Bool]?) -> (InstantPageView & NSView)? { return nil } - func matchesNode(_ node: InstantPageView) -> Bool { + func matchesView(_ node: InstantPageView) -> Bool { return false } @@ -239,84 +371,232 @@ final class InstantPageTextItem: InstantPageItem { func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat { return 0.0 } + + func lineRects() -> [CGRect] { + let boundsWidth = self.frame.width + var rects: [CGRect] = [] + var topLeft = CGPoint(x: CGFloat.greatestFiniteMagnitude, y: 0.0) + var bottomRight = CGPoint() + + var lastLineFrame: CGRect? + for i in 0 ..< self.lines.count { + let line = self.lines[i] + + var lineFrame = line.frame + for imageItem in line.imageItems { + if imageItem.frame.minY < lineFrame.minY { + let delta = lineFrame.minY - imageItem.frame.minY - 2.0 + lineFrame = CGRect(x: lineFrame.minX, y: lineFrame.minY - delta, width: lineFrame.width, height: lineFrame.height + delta) + } + if imageItem.frame.maxY > lineFrame.maxY { + let delta = imageItem.frame.maxY - lineFrame.maxY - 2.0 + lineFrame = CGRect(x: lineFrame.minX, y: lineFrame.minY, width: lineFrame.width, height: lineFrame.height + delta) + } + } + lineFrame = lineFrame.insetBy(dx: 0.0, dy: -4.0) + if self.alignment == .center { + lineFrame.origin.x = floor((boundsWidth - lineFrame.size.width) / 2.0) + } else if self.alignment == .right { + lineFrame.origin.x = boundsWidth - lineFrame.size.width + } else if self.alignment == .natural && self.rtlLineIndices.contains(i) { + lineFrame.origin.x = boundsWidth - lineFrame.size.width + } + + if lineFrame.minX < topLeft.x { + topLeft = CGPoint(x: lineFrame.minX, y: topLeft.y) + } + if lineFrame.maxX > bottomRight.x { + bottomRight = CGPoint(x: lineFrame.maxX, y: bottomRight.y) + } + + if self.lines.count > 1 && i == self.lines.count - 1 { + lastLineFrame = lineFrame + } else { + if lineFrame.minY < topLeft.y { + topLeft = CGPoint(x: topLeft.x, y: lineFrame.minY) + } + if lineFrame.maxY > bottomRight.y { + bottomRight = CGPoint(x: bottomRight.x, y: lineFrame.maxY) + } + } + } + rects.append(CGRect(x: topLeft.x, y: topLeft.y, width: bottomRight.x - topLeft.x, height: bottomRight.y - topLeft.y)) + if self.lines.count > 1, var lastLineFrame = lastLineFrame { + let delta = lastLineFrame.minY - bottomRight.y + lastLineFrame = CGRect(x: lastLineFrame.minX, y: bottomRight.y, width: lastLineFrame.width, height: lastLineFrame.height + delta) + rects.append(lastLineFrame) + } + + return rects + } + + func effectiveWidth() -> CGFloat { + var width: CGFloat = 0.0 + for line in self.lines { + width = max(width, line.frame.width) + } + return ceil(width) + } + + func plainText() -> String { + if let first = self.lines.first, let last = self.lines.last { + return self.attributedString.attributedSubstring(from: NSMakeRange(first.range.location, last.range.location + last.range.length - first.range.location)).string + } + return "" + } + } -func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextStyleStack) -> NSAttributedString { + + + +func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextStyleStack, url: InstantPageUrlItem? = nil, boundingWidth: CGFloat? = nil) -> NSAttributedString { switch text { case .empty: return NSAttributedString(string: "", attributes: styleStack.textAttributes()) case let .plain(string): - return NSAttributedString(string: string, attributes: styleStack.textAttributes()) + var attributes = styleStack.textAttributes() + if let url = url { + attributes[.URL] = url + } + return NSAttributedString(string: string, attributes: attributes) case let .bold(text): styleStack.push(.bold) - let result = attributedStringForRichText(text, styleStack: styleStack) + let result = attributedStringForRichText(text, styleStack: styleStack, url: url) styleStack.pop() return result case let .italic(text): styleStack.push(.italic) - let result = attributedStringForRichText(text, styleStack: styleStack) + let result = attributedStringForRichText(text, styleStack: styleStack, url: url) styleStack.pop() return result case let .underline(text): styleStack.push(.underline) - let result = attributedStringForRichText(text, styleStack: styleStack) + let result = attributedStringForRichText(text, styleStack: styleStack, url: url) styleStack.pop() return result case let .strikethrough(text): styleStack.push(.strikethrough) - let result = attributedStringForRichText(text, styleStack: styleStack) + let result = attributedStringForRichText(text, styleStack: styleStack, url: url) styleStack.pop() return result case let .fixed(text): styleStack.push(.fontFixed(true)) - let result = attributedStringForRichText(text, styleStack: styleStack) + let result = attributedStringForRichText(text, styleStack: styleStack, url: url) styleStack.pop() return result case let .url(text, url, webpageId): - styleStack.push(.link(.url(text: text, url: url, webpageId: webpageId))) - styleStack.push(.underline) - let result = attributedStringForRichText(text, styleStack: styleStack) - styleStack.pop() + styleStack.push(.link(webpageId != nil)) + let result = attributedStringForRichText(text, styleStack: styleStack, url: InstantPageUrlItem(url: url, webpageId: webpageId)) styleStack.pop() return result case let .email(text, email): styleStack.push(.bold) - styleStack.push(.link(.url(text: text, url: email, webpageId: nil))) styleStack.push(.underline) - let result = attributedStringForRichText(text, styleStack: styleStack) - styleStack.pop() + let result = attributedStringForRichText(text, styleStack: styleStack, url: InstantPageUrlItem(url: "mailto:\(email)", webpageId: nil)) styleStack.pop() styleStack.pop() return result case let .concat(texts): let string = NSMutableAttributedString() for text in texts { - let substring = attributedStringForRichText(text, styleStack: styleStack) + let substring = attributedStringForRichText(text, styleStack: styleStack, url: url, boundingWidth: boundingWidth) string.append(substring) } return string + case let .subscript(text): + styleStack.push(.subscript) + let result = attributedStringForRichText(text, styleStack: styleStack, url: url) + styleStack.pop() + return result + case let .superscript(text): + styleStack.push(.superscript) + let result = attributedStringForRichText(text, styleStack: styleStack, url: url) + styleStack.pop() + return result + case let .marked(text): + styleStack.push(.marker) + let result = attributedStringForRichText(text, styleStack: styleStack, url: url) + styleStack.pop() + return result + case let .phone(text, phone): + styleStack.push(.bold) + styleStack.push(.underline) + let result = attributedStringForRichText(text, styleStack: styleStack, url: InstantPageUrlItem(url: "tel:\(phone)", webpageId: nil)) + styleStack.pop() + styleStack.pop() + return result + case let .image(id, dimensions): + struct RunStruct { + let ascent: CGFloat + let descent: CGFloat + let width: CGFloat + } + + var dimensions = dimensions.size + if let boundingWidth = boundingWidth { + dimensions = dimensions.fittedToWidthOrSmaller(boundingWidth) + } + let extentBuffer = UnsafeMutablePointer.allocate(capacity: 1) + extentBuffer.initialize(to: RunStruct(ascent: 0.0, descent: 0.0, width: dimensions.width)) + var callbacks = CTRunDelegateCallbacks(version: kCTRunDelegateVersion1, dealloc: { (pointer) in + }, getAscent: { (pointer) -> CGFloat in + let d = pointer.assumingMemoryBound(to: RunStruct.self) + return d.pointee.ascent + }, getDescent: { (pointer) -> CGFloat in + let d = pointer.assumingMemoryBound(to: RunStruct.self) + return d.pointee.descent + }, getWidth: { (pointer) -> CGFloat in + let d = pointer.assumingMemoryBound(to: RunStruct.self) + return d.pointee.width + }) + let delegate = CTRunDelegateCreate(&callbacks, extentBuffer) + let attrDictionaryDelegate = [(kCTRunDelegateAttributeName as NSAttributedString.Key): (delegate as Any), .instantPageMediaIdAttribute : id.id, .instantPageMediaDimensionsAttribute: dimensions] + return NSAttributedString(string: " ", attributes: attrDictionaryDelegate) + case let .anchor(text, name): + var empty = false + var text = text + if case .empty = text { + empty = true + text = .plain("\u{200b}") + } + styleStack.push(.anchor(name, empty)) + let result = attributedStringForRichText(text, styleStack: styleStack, url: url) + styleStack.pop() + return result } } -func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFloat) -> InstantPageTextItem { +func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFloat, horizontalInset: CGFloat = 0.0, alignment: NSTextAlignment = .natural, offset: CGPoint, media: [MediaId: Media] = [:], webpage: TelegramMediaWebpage? = nil, minimizeWidth: Bool = false, maxNumberOfLines: Int = 0) -> (InstantPageTextItem?, [InstantPageItem], CGSize) { if string.length == 0 { - return InstantPageTextItem(frame: CGRect(), lines: []) + return (nil, [], CGSize()) } var lines: [InstantPageTextLine] = [] - guard let font = string.attribute(NSAttributedStringKey.font, at: 0, effectiveRange: nil) as? NSFont else { - return InstantPageTextItem(frame: CGRect(), lines: []) + var imageItems: [InstantPageTextImageItem] = [] + var font = string.attribute(.font, at: 0, effectiveRange: nil) as? NSFont + if font == nil { + let range = NSMakeRange(0, string.length) + string.enumerateAttributes(in: range, options: []) { attributes, range, _ in + if font == nil, let furtherFont = attributes[.font] as? NSFont { + font = furtherFont + } + } + } + let image = string.attribute(.instantPageMediaIdAttribute, at: 0, effectiveRange: nil) + guard font != nil || image != nil else { + return (nil, [], CGSize()) } var lineSpacingFactor: CGFloat = 1.12 - if let lineSpacingFactorAttribute = string.attribute(.instantPageLineSpacingFactor, at: 0, effectiveRange: nil) { + if let lineSpacingFactorAttribute = string.attribute(.instantPageLineSpacingFactorAttribute, at: 0, effectiveRange: nil) { lineSpacingFactor = CGFloat((lineSpacingFactorAttribute as! NSNumber).floatValue) } let typesetter = CTTypesetterCreateWithAttributedString(string) - let fontAscent = font.ascender - let fontDescent = font.descender + let fontAscent = font?.ascender ?? 0.0 + let fontDescent = font?.descender ?? 0.0 let fontLineHeight = floor(fontAscent + fontDescent) let fontLineSpacing = floor(fontLineHeight * lineSpacingFactor) @@ -324,58 +604,221 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo var lastIndex: CFIndex = 0 var currentLineOrigin = CGPoint() + var hasAnchors = false + var maxLineWidth: CGFloat = 0.0 + var maxImageHeight: CGFloat = 0.0 + var extraDescent: CGFloat = 0.0 + let text = string.string + var indexOffset: CFIndex? while true { - let currentMaxWidth = boundingWidth - currentLineOrigin.x - let currentLineInset: CGFloat = 0.0 - let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, lastIndex, Double(currentMaxWidth)) + var workingLineOrigin = currentLineOrigin + let currentMaxWidth = boundingWidth - workingLineOrigin.x + let lineCharacterCount: CFIndex + var hadIndexOffset = false + if minimizeWidth { + var count = 0 + for ch in text.suffix(text.count - lastIndex) { + count += 1 + if ch == " " || ch == "\n" || ch == "\t" { + break + } + } + lineCharacterCount = count + } else { + let suggestedLineBreak = CTTypesetterSuggestLineBreak(typesetter, lastIndex, Double(currentMaxWidth)) + if let offset = indexOffset { + lineCharacterCount = suggestedLineBreak + offset + indexOffset = nil + hadIndexOffset = true + } else { + lineCharacterCount = suggestedLineBreak + } + } if lineCharacterCount > 0 { - let line = CTTypesetterCreateLineWithOffset(typesetter, CFRangeMake(lastIndex, lineCharacterCount), 100.0) + var line = CTTypesetterCreateLineWithOffset(typesetter, CFRangeMake(lastIndex, lineCharacterCount), 100.0) + var lineWidth = CGFloat(CTLineGetTypographicBounds(line, nil, nil, nil)) + let lineRange = NSMakeRange(lastIndex, lineCharacterCount) + let substring = string.attributedSubstring(from: lineRange).string + + var stop = false + if maxNumberOfLines > 0 && lines.count == maxNumberOfLines - 1 && lastIndex + lineCharacterCount < string.length { + let attributes = string.attributes(at: lastIndex + lineCharacterCount - 1, effectiveRange: nil) + if let truncateString = CFAttributedStringCreate(nil, "\u{2026}" as CFString, attributes as CFDictionary) { + let truncateToken = CTLineCreateWithAttributedString(truncateString) + let tokenWidth = CGFloat(CTLineGetTypographicBounds(truncateToken, nil, nil, nil) + 3.0) + if let truncatedLine = CTLineCreateTruncatedLine(line, Double(lineWidth - tokenWidth), .end, truncateToken) { + lineWidth += tokenWidth + line = truncatedLine + } + } + stop = true + } - let trailingWhitespace = CGFloat(CTLineGetTrailingWhitespaceWidth(line)) - let lineWidth = CGFloat(CTLineGetTypographicBounds(line, nil, nil, nil) + Double(currentLineInset)) + let hadExtraDescent = extraDescent > 0.0 + extraDescent = 0.0 + var lineImageItems: [InstantPageTextImageItem] = [] + var isRTL = false + if let glyphRuns = CTLineGetGlyphRuns(line) as? [CTRun], !glyphRuns.isEmpty { + if let run = glyphRuns.first, CTRunGetStatus(run).contains(CTRunStatus.rightToLeft) { + isRTL = true + } + + var appliedLineOffset: CGFloat = 0.0 + for run in glyphRuns { + let cfRunRange = CTRunGetStringRange(run) + let runRange = NSMakeRange(cfRunRange.location == kCFNotFound ? NSNotFound : cfRunRange.location, cfRunRange.length) + string.enumerateAttributes(in: runRange, options: []) { attributes, range, _ in + if let id = attributes[.instantPageMediaIdAttribute] as? Int64, let dimensions = attributes[.instantPageMediaDimensionsAttribute] as? CGSize { + var imageFrame = CGRect(origin: CGPoint(), size: dimensions) + + let xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, nil) + let yOffset = fontLineHeight.isZero ? 0.0 : floorToScreenPixels(System.backingScale, (fontLineHeight - imageFrame.size.height) / 2.0) + imageFrame.origin = imageFrame.origin.offsetBy(dx: workingLineOrigin.x + xOffset, dy: workingLineOrigin.y + yOffset) + + let minSpacing = fontLineSpacing - 4.0 + let delta = workingLineOrigin.y - minSpacing - imageFrame.minY - appliedLineOffset + if !fontAscent.isZero && delta > 0.0 { + workingLineOrigin.y += delta + appliedLineOffset += delta + imageFrame.origin = imageFrame.origin.offsetBy(dx: 0.0, dy: delta) + } + if !fontLineHeight.isZero { + extraDescent = max(extraDescent, imageFrame.maxY - (workingLineOrigin.y + fontLineHeight + minSpacing)) + } + maxImageHeight = max(maxImageHeight, imageFrame.height) + lineImageItems.append(InstantPageTextImageItem(frame: imageFrame, range: range, id: MediaId(namespace: Namespaces.Media.CloudFile, id: id))) + } + } + } + } + + if substring.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && lineImageItems.count > 0 { + extraDescent += max(6.0, fontLineSpacing / 2.0) + } + + if !minimizeWidth && !hadIndexOffset && lineCharacterCount > 1 && lineWidth > currentMaxWidth + 5.0, let imageItem = lineImageItems.last { + indexOffset = -(lastIndex + lineCharacterCount - imageItem.range.lowerBound) + continue + } - var urlItems: [InstantPageTextUrlItem] = [] var strikethroughItems: [InstantPageTextStrikethroughItem] = [] + var markedItems: [InstantPageTextMarkedItem] = [] + var anchorItems: [InstantPageTextAnchorItem] = [] - string.enumerateAttribute(.strikethroughStyle, in: NSMakeRange(lastIndex, lineCharacterCount), options: [], using: { item, range, _ in - if let _ = item { + string.enumerateAttributes(in: lineRange, options: []) { attributes, range, _ in + if let _ = attributes[.strikethroughStyle] { let lowerX = floor(CTLineGetOffsetForStringIndex(line, range.location, nil)) let upperX = ceil(CTLineGetOffsetForStringIndex(line, range.location + range.length, nil)) + let x = lowerX < upperX ? lowerX : upperX + strikethroughItems.append(InstantPageTextStrikethroughItem(frame: CGRect(x: workingLineOrigin.x + x, y: workingLineOrigin.y, width: abs(upperX - lowerX), height: fontLineHeight))) + } + if let color = attributes[.instantPageMarkerColorAttribute] as? NSColor { + var lineHeight = fontLineHeight + var delta: CGFloat = 0.0 - strikethroughItems.append(InstantPageTextStrikethroughItem(frame: CGRect(x: currentLineOrigin.x + lowerX, y: currentLineOrigin.y, width: upperX - lowerX, height: fontLineHeight))) + if let offset = attributes[.baselineOffset] as? CGFloat { + lineHeight = floorToScreenPixels(System.backingScale, lineHeight * 0.85) + delta = offset * 0.6 + } + let lowerX = floor(CTLineGetOffsetForStringIndex(line, range.location, nil)) + let upperX = ceil(CTLineGetOffsetForStringIndex(line, range.location + range.length, nil)) + let x = lowerX < upperX ? lowerX : upperX + markedItems.append(InstantPageTextMarkedItem(frame: CGRect(x: workingLineOrigin.x + x, y: workingLineOrigin.y + delta, width: abs(upperX - lowerX), height: lineHeight), color: color)) + } + if let item = attributes[.instantPageAnchorAttribute] as? Dictionary, let name = item["name"] as? String, let empty = item["empty"] as? Bool { + anchorItems.append(InstantPageTextAnchorItem(name: name, empty: empty)) } - }) + } - let textLine = InstantPageTextLine(line: line, attributedString: string, frame: CGRect(x: currentLineOrigin.x, y: currentLineOrigin.y, width: lineWidth, height: fontLineHeight), urlItems: urlItems, strikethroughItems: strikethroughItems) + if !anchorItems.isEmpty { + hasAnchors = true + } - lines.append(textLine) + if hadExtraDescent && extraDescent > 0 { + workingLineOrigin.y += fontLineSpacing + } - var rightAligned = false + let height = !fontLineHeight.isZero ? fontLineHeight : maxImageHeight + let textLine = InstantPageTextLine(line: line, attributedString: string, range: lineRange, frame: CGRect(x: workingLineOrigin.x, y: workingLineOrigin.y, width: lineWidth, height: height), strikethroughItems: strikethroughItems, markedItems: markedItems, imageItems: lineImageItems, anchorItems: anchorItems, isRTL: isRTL) - /*let glyphRuns = CTLineGetGlyphRuns(line) - if CFArrayGetCount(glyphRuns) != 0 { - if (CTRunGetStatus(CFArrayGetValueAtIndex(glyphRuns, 0) as! CTRun).rawValue & CTRunStatus.rightToLeft.rawValue) != 0 { - rightAligned = true - } - }*/ + lines.append(textLine) + imageItems.append(contentsOf: lineImageItems) - //hadRTL |= rightAligned; + if lineWidth > maxLineWidth { + maxLineWidth = lineWidth + } - currentLineOrigin.x = 0.0; - currentLineOrigin.y += fontLineHeight + fontLineSpacing + workingLineOrigin.x = 0.0 + workingLineOrigin.y += fontLineHeight + fontLineSpacing + extraDescent + currentLineOrigin = workingLineOrigin lastIndex += lineCharacterCount + if stop { + break + } } else { - break; + break } } var height: CGFloat = 0.0 - if !lines.isEmpty { - height = lines.last!.frame.maxY + if !lines.isEmpty && !(string.string == "\u{200b}" && hasAnchors) { + height = lines.last!.frame.maxY + extraDescent + } + + var textWidth = boundingWidth + var requiresScroll = false + if !imageItems.isEmpty && maxLineWidth > boundingWidth + 10.0 { + textWidth = maxLineWidth + requiresScroll = true + } + + let textItem = InstantPageTextItem(frame: CGRect(x: 0.0, y: 0.0, width: textWidth, height: height), attributedString: string, alignment: alignment, lines: lines) + if !requiresScroll { + textItem.frame = textItem.frame.offsetBy(dx: offset.x, dy: offset.y) + } + var items: [InstantPageItem] = [] + if !requiresScroll && (imageItems.isEmpty || string.length > 1) { + items.append(textItem) + } + + var topInset: CGFloat = 0.0 + var bottomInset: CGFloat = 0.0 + var additionalItems: [InstantPageItem] = [] + if let webpage = webpage { + let offset = requiresScroll ? CGPoint() : offset + for line in textItem.lines { + let lineFrame = frameForLine(line, boundingWidth: boundingWidth, alignment: alignment) + for imageItem in line.imageItems { + if let image = media[imageItem.id] as? TelegramMediaFile { + let item = InstantPageImageItem(frame: imageItem.frame.offsetBy(dx: lineFrame.minX + offset.x, dy: offset.y), webPage: webpage, media: InstantPageMedia(index: -1, media: image, webpage: webpage, url: nil, caption: nil, credit: nil), interactive: false, roundCorners: false, fit: false) + additionalItems.append(item) + + if item.frame.minY < topInset { + topInset = item.frame.minY + } + if item.frame.maxY > height { + bottomInset = max(bottomInset, item.frame.maxY - height) + } + } + } + } } - return InstantPageTextItem(frame: CGRect(x: 0.0, y: 0.0, width: boundingWidth, height: height), lines: lines) +// if requiresScroll { +// textItem.frame = textItem.frame.offsetBy(dx: 0.0, dy: fabs(topInset)) +// for var item in additionalItems { +// item.frame = item.frame.offsetBy(dx: 0.0, dy: fabs(topInset)) +// } +// +// let scrollableItem = InstantPageScrollableTextItem(frame: CGRect(x: 0.0, y: 0.0, width: boundingWidth + horizontalInset * 2.0, height: height + fabs(topInset) + bottomInset), item: textItem, additionalItems: additionalItems, totalWidth: textWidth, horizontalInset: horizontalInset, rtl: textItem.containsRTL) +// items.append(scrollableItem) +// } else { + items.append(contentsOf: additionalItems) +// } + + return (requiresScroll ? nil : textItem, items, textItem.frame.size) } + diff --git a/Telegram-Mac/InstantPageTextStyleStack.swift b/Telegram-Mac/InstantPageTextStyleStack.swift index 9a8cebe5f3..cb010913d1 100644 --- a/Telegram-Mac/InstantPageTextStyleStack.swift +++ b/Telegram-Mac/InstantPageTextStyleStack.swift @@ -8,30 +8,9 @@ import Cocoa -import TelegramCoreMac -import TGUIKit - +import TelegramCore -func richPlainText(_ text:RichText) -> String { - switch text { - case .plain(let plain): - return plain - case .bold(let rich), .italic(let rich), .fixed(let rich), .strikethrough(let rich), .underline(let rich): - return richPlainText(rich) - case .email(let rich, _): - return richPlainText(rich) - case .url(let rich, _, _): - return richPlainText(rich) - case .concat(let richs): - var string:String = "" - for rich in richs { - string += richPlainText(rich) - } - return string - case .empty: - return"" - } -} +import TGUIKit enum InstantPageTextStyle { case fontSize(CGFloat) @@ -43,15 +22,25 @@ enum InstantPageTextStyle { case underline case strikethrough case textColor(NSColor) - case link(RichText) + case `subscript` + case superscript + case markerColor(NSColor) + case marker + case anchor(String, Bool) + case linkColor(NSColor) + case linkMarkerColor(NSColor) + case link(Bool) } -extension NSAttributedStringKey { - static var instantPageLineSpacingFactor: NSAttributedStringKey { - return NSAttributedStringKey.init(rawValue: "LineSpacingFactorAttribute") - } +extension NSAttributedString.Key { + static let instantPageLineSpacingFactorAttribute = NSAttributedString.Key("InstantPageLineSpacingFactorAttribute") + static let instantPageMarkerColorAttribute = NSAttributedString.Key("InstantPageMarkerColorAttribute") + static let instantPageMediaIdAttribute = NSAttributedString.Key("InstantPageMediaIdAttribute") + static let instantPageMediaDimensionsAttribute = NSAttributedString.Key("InstantPageMediaDimensionsAttribute") + static let instantPageAnchorAttribute = NSAttributedString.Key("InstantPageAnchorAttribute") } + final class InstantPageTextStyleStack { private var items: [InstantPageTextStyle] = [] @@ -65,7 +54,7 @@ final class InstantPageTextStyleStack { } } - func textAttributes() -> [NSAttributedStringKey: Any] { + func textAttributes() -> [NSAttributedString.Key: Any] { var fontSize: CGFloat? var fontSerif: Bool? var fontFixed: Bool? @@ -75,7 +64,14 @@ final class InstantPageTextStyleStack { var underline: Bool? var color: NSColor? var lineSpacingFactor: CGFloat? - var link:RichText? + var baselineOffset: CGFloat? + var markerColor: NSColor? + var marker: Bool? + var anchor: Dictionary? + var linkColor: NSColor? + var linkMarkerColor: NSColor? + var link: Bool? + for item in self.items.reversed() { switch item { case let .fontSize(value): @@ -114,12 +110,43 @@ final class InstantPageTextStyleStack { if lineSpacingFactor == nil { lineSpacingFactor = value } - case .link(let value): - link = value + case .subscript: + if baselineOffset == nil { + baselineOffset = 0.35 + underline = false + } + case .superscript: + if baselineOffset == nil { + baselineOffset = -0.35 + } + case let .markerColor(color): + if markerColor == nil { + markerColor = color + } + case .marker: + if marker == nil { + marker = true + } + case let .anchor(name, empty): + if anchor == nil { + anchor = ["name": name, "empty": empty] + } + case let .linkColor(color): + if linkColor == nil { + linkColor = color + } + case let .linkMarkerColor(color): + if linkMarkerColor == nil { + linkMarkerColor = color + } + case let .link(instant): + if link == nil { + link = instant + } } } - var attributes: [NSAttributedStringKey: Any] = [:] + var attributes: [NSAttributedString.Key: Any] = [:] var parsedFontSize: CGFloat if let fontSize = fontSize { @@ -128,61 +155,76 @@ final class InstantPageTextStyleStack { parsedFontSize = 16.0 } + if let baselineOffset = baselineOffset { + attributes[.baselineOffset] = round(parsedFontSize * baselineOffset); + parsedFontSize = round(parsedFontSize * 0.85) + } + if (bold != nil && bold!) && (italic != nil && italic!) { if fontSerif != nil && fontSerif! { - attributes[NSAttributedStringKey.font] = NSFont(name: "Georgia-BoldItalic", size: parsedFontSize) + attributes[.font] = NSFont(name: "Georgia-BoldItalic", size: parsedFontSize) } else if fontFixed != nil && fontFixed! { - attributes[NSAttributedStringKey.font] = NSFont(name: "Menlo-BoldItalic", size: parsedFontSize) + attributes[.font] = NSFont(name: "Menlo-BoldItalic", size: parsedFontSize) } else { - attributes[NSAttributedStringKey.font] = NSFont.bold(.custom(parsedFontSize)) + attributes[.font] = systemMediumFont(parsedFontSize) } } else if bold != nil && bold! { if fontSerif != nil && fontSerif! { - attributes[NSAttributedStringKey.font] = NSFont(name: "Georgia-Bold", size: parsedFontSize) + attributes[.font] = NSFont(name: "Georgia-Bold", size: parsedFontSize) } else if fontFixed != nil && fontFixed! { - attributes[NSAttributedStringKey.font] = NSFont(name: "Menlo-Bold", size: parsedFontSize) + attributes[.font] = NSFont(name: "Menlo-Bold", size: parsedFontSize) } else { - attributes[NSAttributedStringKey.font] = NSFont.bold(.custom(parsedFontSize)) + attributes[.font] = NSFont.bold(parsedFontSize) } } else if italic != nil && italic! { if fontSerif != nil && fontSerif! { - attributes[NSAttributedStringKey.font] = NSFont(name: "Georgia-Italic", size: parsedFontSize) + attributes[.font] = NSFont(name: "Georgia-Italic", size: parsedFontSize) } else if fontFixed != nil && fontFixed! { - attributes[NSAttributedStringKey.font] = NSFont(name: "Menlo-Italic", size: parsedFontSize) + attributes[.font] = NSFont(name: "Menlo-Italic", size: parsedFontSize) } else { - attributes[NSAttributedStringKey.font] = NSFont.italic(.custom(parsedFontSize)) + attributes[.font] = NSFont.italic(parsedFontSize) } } else { if fontSerif != nil && fontSerif! { - attributes[NSAttributedStringKey.font] = NSFont(name: "Georgia", size: parsedFontSize) + attributes[.font] = NSFont(name: "Georgia", size: parsedFontSize) } else if fontFixed != nil && fontFixed! { - attributes[NSAttributedStringKey.font] = NSFont(name: "Menlo", size: parsedFontSize) + attributes[.font] = NSFont(name: "Menlo", size: parsedFontSize) } else { - attributes[NSAttributedStringKey.font] = NSFont.normal(.custom(parsedFontSize)) + attributes[.font] = NSFont.normal(parsedFontSize) } } if strikethrough != nil && strikethrough! { - attributes[NSAttributedStringKey.strikethroughStyle] = (NSUnderlineStyle.styleSingle.rawValue | NSUnderlineStyle.patternSolid.rawValue) as NSNumber + attributes[.strikethroughStyle] = (NSUnderlineStyle.single.rawValue | NSUnderlineStyle.patternDash.rawValue) as NSNumber } if underline != nil && underline! { - attributes[NSAttributedStringKey.underlineStyle] = NSUnderlineStyle.styleSingle.rawValue as NSNumber + attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue as NSNumber } - if let color = color { - attributes[NSAttributedStringKey.foregroundColor] = color + if let link = link, let linkColor = linkColor { + attributes[.foregroundColor] = linkColor + if link, let linkMarkerColor = linkMarkerColor { + attributes[.instantPageMarkerColorAttribute] = linkMarkerColor + } } else { - attributes[NSAttributedStringKey.foregroundColor] = theme.colors.text + if let color = color { + attributes[.foregroundColor] = color + } else { + attributes[.foregroundColor] = NSColor.black + } } + if let lineSpacingFactor = lineSpacingFactor { + attributes[.instantPageLineSpacingFactorAttribute] = lineSpacingFactor as NSNumber + } - if let link = link { - attributes[NSAttributedStringKey.link] = link + if marker != nil && marker!, let markerColor = markerColor { + attributes[.instantPageMarkerColorAttribute] = markerColor } - if let lineSpacingFactor = lineSpacingFactor { - attributes[.instantPageLineSpacingFactor] = lineSpacingFactor as NSNumber + if let anchor = anchor { + attributes[.instantPageAnchorAttribute] = anchor } return attributes diff --git a/Telegram-Mac/InstantPageTheme.swift b/Telegram-Mac/InstantPageTheme.swift new file mode 100644 index 0000000000..09ecc9df36 --- /dev/null +++ b/Telegram-Mac/InstantPageTheme.swift @@ -0,0 +1,339 @@ +// +// InstantPageTheme.swift +// Telegram +// +// Created by Mikhail Filimonov on 12/12/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa + +import Foundation +import Postbox + +enum InstantPageFontStyle { + case sans + case serif +} + +struct InstantPageFont { + let style: InstantPageFontStyle + let size: CGFloat + let lineSpacingFactor: CGFloat +} + +struct InstantPageTextAttributes { + let font: InstantPageFont + let color: NSColor + let underline: Bool + + init(font: InstantPageFont, color: NSColor, underline: Bool = false) { + self.font = font + self.color = color + self.underline = underline + } + + func withUnderline(_ underline: Bool) -> InstantPageTextAttributes { + return InstantPageTextAttributes(font: self.font, color: self.color, underline: underline) + } + + func withUpdatedFontStyles(sizeMultiplier: CGFloat, forceSerif: Bool) -> InstantPageTextAttributes { + return InstantPageTextAttributes(font: InstantPageFont(style: forceSerif ? .serif : self.font.style, size: floor(self.font.size * sizeMultiplier), lineSpacingFactor: self.font.lineSpacingFactor), color: self.color, underline: self.underline) + } +} + +enum InstantPageTextCategoryType { + case kicker + case header + case subheader + case paragraph + case caption + case credit + case table + case article +} + +struct InstantPageTextCategories { + let kicker: InstantPageTextAttributes + let header: InstantPageTextAttributes + let subheader: InstantPageTextAttributes + let paragraph: InstantPageTextAttributes + let caption: InstantPageTextAttributes + let credit: InstantPageTextAttributes + let table: InstantPageTextAttributes + let article: InstantPageTextAttributes + + func attributes(type: InstantPageTextCategoryType, link: Bool) -> InstantPageTextAttributes { + switch type { + case .kicker: + return self.kicker.withUnderline(link) + case .header: + return self.header.withUnderline(link) + case .subheader: + return self.subheader.withUnderline(link) + case .paragraph: + return self.paragraph.withUnderline(link) + case .caption: + return self.caption.withUnderline(link) + case .credit: + return self.credit.withUnderline(link) + case .table: + return self.table.withUnderline(link) + case .article: + return self.article.withUnderline(link) + } + } + + func withUpdatedFontStyles(sizeMultiplier: CGFloat, forceSerif: Bool) -> InstantPageTextCategories { + return InstantPageTextCategories(kicker: self.kicker.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), header: self.header.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), subheader: self.subheader.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), paragraph: self.paragraph.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), caption: self.caption.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), credit: self.credit.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), table: self.table.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), article: self.article.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif)) + } +} + +final class InstantPageTheme { + let type: InstantPageThemeType + let pageBackgroundColor: NSColor + + let textCategories: InstantPageTextCategories + let serif: Bool + + let codeBlockBackgroundColor: NSColor + + let linkColor: NSColor + let textHighlightColor: NSColor + let linkHighlightColor: NSColor + let markerColor: NSColor + + let panelBackgroundColor: NSColor + let panelHighlightedBackgroundColor: NSColor + let panelPrimaryColor: NSColor + let panelSecondaryColor: NSColor + let panelAccentColor: NSColor + + let tableBorderColor: NSColor + let tableHeaderColor: NSColor + let controlColor: NSColor + + let imageTintColor: NSColor? + + let overlayPanelColor: NSColor + + init(type: InstantPageThemeType, pageBackgroundColor: NSColor, textCategories: InstantPageTextCategories, serif: Bool, codeBlockBackgroundColor: NSColor, linkColor: NSColor, textHighlightColor: NSColor, linkHighlightColor: NSColor, markerColor: NSColor, panelBackgroundColor: NSColor, panelHighlightedBackgroundColor: NSColor, panelPrimaryColor: NSColor, panelSecondaryColor: NSColor, panelAccentColor: NSColor, tableBorderColor: NSColor, tableHeaderColor: NSColor, controlColor: NSColor, imageTintColor: NSColor?, overlayPanelColor: NSColor) { + self.type = type + self.pageBackgroundColor = pageBackgroundColor + self.textCategories = textCategories + self.serif = serif + self.codeBlockBackgroundColor = codeBlockBackgroundColor + self.linkColor = linkColor + self.textHighlightColor = textHighlightColor + self.linkHighlightColor = linkHighlightColor + self.markerColor = markerColor + self.panelBackgroundColor = panelBackgroundColor + self.panelHighlightedBackgroundColor = panelHighlightedBackgroundColor + self.panelPrimaryColor = panelPrimaryColor + self.panelSecondaryColor = panelSecondaryColor + self.panelAccentColor = panelAccentColor + self.tableBorderColor = tableBorderColor + self.tableHeaderColor = tableHeaderColor + self.controlColor = controlColor + self.imageTintColor = imageTintColor + self.overlayPanelColor = overlayPanelColor + } + + func withUpdatedFontStyles(sizeMultiplier: CGFloat, forceSerif: Bool) -> InstantPageTheme { + return InstantPageTheme(type: type, pageBackgroundColor: pageBackgroundColor, textCategories: self.textCategories.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), serif: forceSerif, codeBlockBackgroundColor: codeBlockBackgroundColor, linkColor: linkColor, textHighlightColor: textHighlightColor, linkHighlightColor: linkHighlightColor, markerColor: markerColor, panelBackgroundColor: panelBackgroundColor, panelHighlightedBackgroundColor: panelHighlightedBackgroundColor, panelPrimaryColor: panelPrimaryColor, panelSecondaryColor: panelSecondaryColor, panelAccentColor: panelAccentColor, tableBorderColor: tableBorderColor, tableHeaderColor: tableHeaderColor, controlColor: controlColor, imageTintColor: imageTintColor, overlayPanelColor: overlayPanelColor) + } +} + +private let lightTheme = InstantPageTheme( + type: .light, + pageBackgroundColor: .white, + textCategories: InstantPageTextCategories( + kicker: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 0.685), color: .black), + header: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 24.0, lineSpacingFactor: 0.685), color: .black), + subheader: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 19.0, lineSpacingFactor: 0.685), color: .black), + paragraph: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: .black), + caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: NSColor(rgb: 0x79828b)), + credit: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 13.0, lineSpacingFactor: 1.0), color: NSColor(rgb: 0x79828b)), + table: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: .black), + article: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 18.0, lineSpacingFactor: 1.0), color: .black) + ), + serif: false, + codeBlockBackgroundColor: NSColor(rgb: 0xf5f8fc), + linkColor: NSColor(rgb: 0x007ee5), + textHighlightColor: NSColor(rgb: 0, alpha: 0.12), + linkHighlightColor: NSColor(rgb: 0x007ee5, alpha: 0.07), + markerColor: NSColor(rgb: 0xfef3bc), + panelBackgroundColor: NSColor(rgb: 0xf3f4f5), + panelHighlightedBackgroundColor: NSColor(rgb: 0xe7e7e7), + panelPrimaryColor: .black, + panelSecondaryColor: NSColor(rgb: 0x79828b), + panelAccentColor: NSColor(rgb: 0x007ee5), + tableBorderColor: NSColor(rgb: 0xe2e2e2), + tableHeaderColor: NSColor(rgb: 0xf4f4f4), + controlColor: NSColor(rgb: 0xc7c7cd), + imageTintColor: nil, + overlayPanelColor: .white +) + +private let sepiaTheme = InstantPageTheme( + type: .sepia, + pageBackgroundColor: NSColor(rgb: 0xf8f1e2), + textCategories: InstantPageTextCategories( + kicker: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 0.685), color: NSColor(rgb: 0x4f321d)), + header: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 24.0, lineSpacingFactor: 0.685), color: NSColor(rgb: 0x4f321d)), + subheader: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 19.0, lineSpacingFactor: 0.685), color: NSColor(rgb: 0x4f321d)), + paragraph: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: NSColor(rgb: 0x4f321d)), + caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: NSColor(rgb: 0x927e6b)), + credit: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 13.0, lineSpacingFactor: 1.0), color: NSColor(rgb: 0x927e6b)), + table: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: NSColor(rgb: 0x4f321d)), + article: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 18.0, lineSpacingFactor: 1.0), color: NSColor(rgb: 0x4f321d)) + ), + serif: false, + codeBlockBackgroundColor: NSColor(rgb: 0xefe7d6), + linkColor: NSColor(rgb: 0xd19600), + textHighlightColor: NSColor(rgb: 0, alpha: 0.1), + linkHighlightColor: NSColor(rgb: 0xd19600, alpha: 0.1), + markerColor: NSColor(rgb: 0xe5ddcd), + panelBackgroundColor: NSColor(rgb: 0xefe7d6), + panelHighlightedBackgroundColor: NSColor(rgb: 0xe3dccb), + panelPrimaryColor: .black, + panelSecondaryColor: NSColor(rgb: 0x927e6b), + panelAccentColor: NSColor(rgb: 0xd19601), + tableBorderColor: NSColor(rgb: 0xddd1b8), + tableHeaderColor: NSColor(rgb: 0xf0e7d4), + controlColor: NSColor(rgb: 0xddd1b8), + imageTintColor: nil, + overlayPanelColor: NSColor(rgb: 0xf8f1e2) +) + +private let grayTheme = InstantPageTheme( + type: .gray, + pageBackgroundColor: NSColor(rgb: 0x5a5a5c), + textCategories: InstantPageTextCategories( + kicker: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 0.685), color: NSColor(rgb: 0xcecece)), + header: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 24.0, lineSpacingFactor: 0.685), color: NSColor(rgb: 0xcecece)), + subheader: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 19.0, lineSpacingFactor: 0.685), color: NSColor(rgb: 0xcecece)), + paragraph: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: NSColor(rgb: 0xcecece)), + caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: NSColor(rgb: 0xa0a0a0)), + credit: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 13.0, lineSpacingFactor: 1.0), color: NSColor(rgb: 0xa0a0a0)), + table: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: NSColor(rgb: 0xcecece)), + article: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 18.0, lineSpacingFactor: 1.0), color: NSColor(rgb: 0xcecece)) + ), + serif: false, + codeBlockBackgroundColor: NSColor(rgb: 0x555556), + linkColor: NSColor(rgb: 0x5ac8fa), + textHighlightColor: NSColor(rgb: 0, alpha: 0.16), + linkHighlightColor: NSColor(rgb: 0x5ac8fa, alpha: 0.13), + markerColor: NSColor(rgb: 0x4b4b4b), + panelBackgroundColor: NSColor(rgb: 0x555556), + panelHighlightedBackgroundColor: NSColor(rgb: 0x505051), + panelPrimaryColor: NSColor(rgb: 0xcecece), + panelSecondaryColor: NSColor(rgb: 0xa0a0a0), + panelAccentColor: NSColor(rgb: 0x54b9f8), + tableBorderColor: NSColor(rgb: 0x484848), + tableHeaderColor: NSColor(rgb: 0x555556), + controlColor: NSColor(rgb: 0x484848), + imageTintColor: NSColor(rgb: 0xcecece), + overlayPanelColor: NSColor(rgb: 0x5a5a5c) +) + +private let darkTheme = InstantPageTheme( + type: .dark, + pageBackgroundColor: NSColor(rgb: 0x000000), + textCategories: InstantPageTextCategories( + kicker: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 0.685), color: NSColor(rgb: 0xb0b0b0)), + header: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 24.0, lineSpacingFactor: 0.685), color: NSColor(rgb: 0xb0b0b0)), + subheader: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 19.0, lineSpacingFactor: 0.685), color: NSColor(rgb: 0xb0b0b0)), + paragraph: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 17.0, lineSpacingFactor: 1.0), color: NSColor(rgb: 0xb0b0b0)), + caption: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: NSColor(rgb: 0x6a6a6a)), + credit: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 13.0, lineSpacingFactor: 1.0), color: NSColor(rgb: 0x6a6a6a)), + table: InstantPageTextAttributes(font: InstantPageFont(style: .sans, size: 15.0, lineSpacingFactor: 1.0), color: NSColor(rgb: 0xb0b0b0)), + article: InstantPageTextAttributes(font: InstantPageFont(style: .serif, size: 18.0, lineSpacingFactor: 1.0), color: NSColor(rgb: 0xb0b0b0)) + ), + serif: false, + codeBlockBackgroundColor: NSColor(rgb: 0x131313), + linkColor: NSColor(rgb: 0x5ac8fa), + textHighlightColor: NSColor(rgb: 0xffffff, alpha: 0.1), + linkHighlightColor: NSColor(rgb: 0x5ac8fa, alpha: 0.2), + markerColor: NSColor(rgb: 0x313131), + panelBackgroundColor: NSColor(rgb: 0x131313), + panelHighlightedBackgroundColor: NSColor(rgb: 0x1f1f1f), + panelPrimaryColor: NSColor(rgb: 0xb0b0b0), + panelSecondaryColor: NSColor(rgb: 0x6a6a6a), + panelAccentColor: NSColor(rgb: 0x50b6f3), + tableBorderColor: NSColor(rgb: 0x303030), + tableHeaderColor: NSColor(rgb: 0x131313), + controlColor: NSColor(rgb: 0x303030), + imageTintColor: NSColor(rgb: 0xb0b0b0), + overlayPanelColor: NSColor(rgb: 0x232323) +) + +private func fontSizeMultiplierForVariant(_ variant: InstantPagePresentationFontSize) -> CGFloat { + switch variant { + case .small: + return 0.85 + case .standard: + return 1.0 + case .large: + return 1.15 + case .xlarge: + return 1.3 + case .xxlarge: + return 1.5 + } +} + +func instantPageThemeTypeForSettingsAndTime(settings: InstantViewAppearance, time: Date?) -> InstantPageThemeType { + + return .dark + +// switch theme.colors.name { +// +// } + +// if settings.autoNightMode { +// switch settings.themeType { +// case .light, .sepia, .gray: +// var useDarkTheme = false +// /*switch presentationTheme.name { +// case let .builtin(name): +// switch name { +// case .nightAccent, .nightGrayscale: +// useDarkTheme = true +// default: +// break +// } +// default: +// break +// }*/ +// if let time = time { +// let calendar = Calendar.current +// let hour = calendar.component(.hour, from: time) +// if hour <= 8 || hour >= 22 { +// useDarkTheme = true +// } +// } +// if useDarkTheme { +// return .dark +// } +// case .dark: +// break +// } +// } +// +// return settings.themeType +} + +func instantPageThemeForType(_ type: InstantPageThemeType, settings: InstantViewAppearance) -> InstantPageTheme { + switch type { + case .light: + return lightTheme.withUpdatedFontStyles(sizeMultiplier: fontSizeMultiplierForVariant(.standard), forceSerif: settings.fontSerif) + case .sepia: + return sepiaTheme.withUpdatedFontStyles(sizeMultiplier: fontSizeMultiplierForVariant(.standard), forceSerif: settings.fontSerif) + case .gray: + return grayTheme.withUpdatedFontStyles(sizeMultiplier: fontSizeMultiplierForVariant(.standard), forceSerif: settings.fontSerif) + case .dark: + return darkTheme.withUpdatedFontStyles(sizeMultiplier: fontSizeMultiplierForVariant(.standard), forceSerif: settings.fontSerif) + } +} + diff --git a/Telegram-Mac/InstantPageTile.swift b/Telegram-Mac/InstantPageTile.swift index 3fb408b587..262660a982 100644 --- a/Telegram-Mac/InstantPageTile.swift +++ b/Telegram-Mac/InstantPageTile.swift @@ -34,11 +34,12 @@ final class InstantPageTile { } func instantPageTilesFromLayout(_ layout: InstantPageLayout, boundingWidth: CGFloat) -> [InstantPageTile] { - var tileByOrigin: [Int: InstantPageTile] = [:] + var tileByOrigin: [Int : InstantPageTile] = [:] let tileHeight: CGFloat = 256.0 + var tileHoles: [CGRect] = [] for item in layout.items { - if !item.wantsNode { + if !item.wantsView { let topTileIndex = max(0, Int(floor(item.frame.minY - 10.0) / tileHeight)) let bottomTileIndex = max(topTileIndex, Int(floor(item.frame.maxY + 10.0) / tileHeight)) for i in topTileIndex ... bottomTileIndex { @@ -51,10 +52,45 @@ func instantPageTilesFromLayout(_ layout: InstantPageLayout, boundingWidth: CGFl } tile.items.append(item) } + } else if item.separatesTiles { + tileHoles.append(item.frame) } } - return tileByOrigin.values.sorted(by: { lhs, rhs in + var finalTiles: [InstantPageTile] = [] + var usedTiles = Set() + + for hole in tileHoles { + let topTileIndex = max(0, Int(floor(hole.minY - 10.0) / tileHeight)) + let bottomTileIndex = max(topTileIndex, Int(floor(hole.maxY + 10.0) / tileHeight)) + for i in topTileIndex ... bottomTileIndex { + if let tile = tileByOrigin[i] { + if tile.frame.minY > hole.minY && tile.frame.minY < hole.maxY { + let delta = hole.maxY - tile.frame.minY + let updatedTile = InstantPageTile(frame: CGRect(origin: tile.frame.origin.offsetBy(dx: 0.0, dy: delta), size: CGSize(width: tile.frame.width, height: tile.frame.height - delta))) + updatedTile.items.append(contentsOf: tile.items) + finalTiles.append(updatedTile) + usedTiles.insert(i) + } else if tile.frame.maxY > hole.minY && tile.frame.minY < hole.minY { + let delta = tile.frame.maxY - hole.minY + let updatedTile = InstantPageTile(frame: CGRect(origin: tile.frame.origin, size: CGSize(width: tile.frame.width, height: tile.frame.height - delta))) + updatedTile.items.append(contentsOf: tile.items) + finalTiles.append(updatedTile) + usedTiles.insert(i) + } + } + } + //let holeTile = InstantPageTile(frame: hole) + //finalTiles.append(holeTile) + } + + for (index, tile) in tileByOrigin { + if !usedTiles.contains(index) { + finalTiles.append(tile) + } + } + + return finalTiles.sorted(by: { lhs, rhs in return lhs.frame.minY < rhs.frame.minY }) } diff --git a/Telegram-Mac/InstantPageTileView.swift b/Telegram-Mac/InstantPageTileView.swift index 3ef7a54860..fdc4ca6c6b 100644 --- a/Telegram-Mac/InstantPageTileView.swift +++ b/Telegram-Mac/InstantPageTileView.swift @@ -14,12 +14,18 @@ import TGUIKit final class InstantPageTileView: View { private let tile: InstantPageTile - init(tile: InstantPageTile) { + init(tile: InstantPageTile, backgroundColor: NSColor) { self.tile = tile super.init() - super.backgroundColor = theme.colors.background + super.backgroundColor = backgroundColor + // layerContentsRedrawPolicy = .never } + func redrawTile() { +// layerContentsRedrawPolicy = .onSetNeedsDisplay +// display() +// layerContentsRedrawPolicy = .never + } required init?(coder: NSCoder) { diff --git a/Telegram-Mac/InstantPageViewController.swift b/Telegram-Mac/InstantPageViewController.swift index 0151359855..5361b5dd26 100644 --- a/Telegram-Mac/InstantPageViewController.swift +++ b/Telegram-Mac/InstantPageViewController.swift @@ -8,64 +8,15 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac +import Postbox +import TelegramCore -class InstantPageModalBrowser : ModalViewController { - - private let navigation: NavigationViewController - - init(_ page: InstantPageViewController) { - self.navigation = NavigationViewController(page) - page._frameRect = NSMakeRect(0, 0, 400, 365) - navigation._frameRect = NSMakeRect(0, 0, 400, 400) - super.init(frame: page._frameRect) - - } - - override func escapeKeyAction() -> KeyHandlerResult { - if navigation.controller == navigation.empty { - return .rejected - } - navigation.back() - return .invoked - } - - var currentInstantController:InstantPageViewController { - return navigation.controller as! InstantPageViewController - } - - override func viewDidLoad() { - super.viewDidLoad() - self.ready.set(currentInstantController.ready.get()) - } - - - override var handleEvents: Bool { - return true - } - - override var dynamicSize: Bool { - return true - } - - override func measure(size: NSSize) { - updateSize(size) - } - - private func updateSize(_ size: NSSize) { - self.modal?.resize(with:NSMakeSize(min(size.width - 120, 600), min(size.height - 40, currentInstantController.genericView.documentSize.height)), animated: false) - } - - override func initializer() -> NSView { - return navigation.view - } -} +import SwiftSignalKit class InstantPageViewController: TelegramGenericViewController { + var pageDidScrolled:(((documentSize: NSSize, position: ScrollPosition))->Void)? var currentLayout: InstantPageLayout? @@ -85,11 +36,27 @@ class InstantPageViewController: TelegramGenericViewController { private var selectManager: InstantPageSelectText? private let joinDisposable = MetaDisposable() - private let openPeerInfoDisposable = MetaDisposable() private let mediaDisposable = MetaDisposable() - private var appearance: InstantViewAppearance = InstantViewAppearance.defaultSettings private let actualizeDisposable = MetaDisposable() + private let loadStoredStateDisposable = MetaDisposable() + private let saveProgressDisposable = MetaDisposable() + private let loadWebpageDisposable = MetaDisposable() + private let updateLayoutDisposable = MetaDisposable() + private let appearanceDisposable = MetaDisposable() + private var initialAnchor: String? + private var pendingAnchor: String? + private var initialState: InstantPageStoredState? + + private let loadProgress = ValuePromise(0.00, ignoreRepeated: true) + var progressSignal: Signal { + return loadProgress.get() + } + + + var currentWebEmbedHeights: [Int : CGFloat] = [:] + var currentExpandedDetails: [Int : Bool]? + var currentDetailsItems: [InstantPageDetailsItem] = [] var webPage: TelegramMediaWebpage { didSet { @@ -102,16 +69,17 @@ class InstantPageViewController: TelegramGenericViewController { } } let message: String? - init(_ account: Account, webPage: TelegramMediaWebpage, message: String?) { + init(_ context: AccountContext, webPage: TelegramMediaWebpage, message: String?, messageId: MessageId? = nil, anchor: String? = nil, saveToRecent: Bool = true) { self.webPage = webPage self.message = message + self.pendingAnchor = anchor switch webPage.content { case .Loaded(let content): self.instantPage = content.instantPage default: break } - super.init(account) + super.init(context) bar = .init(height: 0) noticeResizeWhenLoaded = false } @@ -119,36 +87,68 @@ class InstantPageViewController: TelegramGenericViewController { override var defaultBarTitle: String { switch webPage.content { case .Loaded(let content): - return content.title ?? super.defaultBarTitle + return content.websiteName ?? super.defaultBarTitle default: return super.defaultBarTitle } } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - reloadData() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) } - private func updateLayout() { - - let currentLayout = instantPageLayoutForWebPage(webPage, boundingWidth: frame.width, presentation: appearance, openChannel: { [weak self] channel in - if let account = self?.account { - self?.account.context.mainNavigation?.push(ChatController(account: account, peerId: channel.id)) - self?.closeModal() + var currentState: InstantPageStoredState { + var details: [InstantPageStoredDetailsState] = [] + if let currentExpandedDetails = self.currentExpandedDetails { + for (index, expanded) in currentExpandedDetails { + details.append(InstantPageStoredDetailsState(index: Int32(clamping: index), expanded: expanded, details: [])) } - }, joinChannel: { [weak self] channel in - if let strongSelf = self, let window = self?.window { - strongSelf.joinDisposable.set(showModalProgress(signal: joinChannel(account: strongSelf.account, peerId: channel.id), for: window).start(next: { [weak strongSelf] in - strongSelf?.updateLayout() - strongSelf?.containerLayoutUpdated(transition: .immediate) - })) + } + return InstantPageStoredState(contentOffset: Double(self.genericView.contentOffset.y), details: details) + } + + + func updateWebPage(_ webPage: TelegramMediaWebpage, anchor: String?, state: InstantPageStoredState? = nil) { + if self.webPage != webPage { + + self.webPage = webPage + if let anchor = anchor { + self.initialAnchor = anchor.removingPercentEncoding + } else if let state = state { + self.initialState = state + if !state.details.isEmpty { + var storedExpandedDetails: [Int: Bool] = [:] + for state in state.details { + storedExpandedDetails[Int(clamping: state.index)] = state.expanded + } + self.currentExpandedDetails = storedExpandedDetails + } } - }) + self.currentLayout = nil + self.updateLayout() + + if case let .Loaded(content) = webPage.content, let instantPage = content.instantPage, instantPage.isComplete { + self.loadProgress.set(1.0) + + if let anchor = self.pendingAnchor { + self.pendingAnchor = nil + self.scrollToAnchor(anchor) + } + } + reloadData() + } + self.readyOnce() + } + + + private func updateLayout() { + let currentLayout = instantPageLayoutForWebPage(webPage, boundingWidth: max(500, frame.width), safeInset: 0, theme: instantPageThemeForType(theme.insantPageThemeType, settings: appearance), webEmbedHeights: self.currentWebEmbedHeights) - for (_, tileNode) in self.visibleTiles { - tileNode.removeFromSuperview() + updateInteractions() + + for (_, tileView) in self.visibleTiles { + tileView.removeFromSuperview() } self.visibleTiles.removeAll() @@ -159,12 +159,18 @@ class InstantPageViewController: TelegramGenericViewController { let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: frame.width) + + var currentDetailsItems: [InstantPageDetailsItem] = [] var currentLayoutItemsWithViews: [InstantPageItem] = [] var currentLayoutItemsWithLinks: [InstantPageItem] = [] var distanceThresholdGroupCount: [Int: Int] = [:] + var expandedDetails: [Int : Bool] = [:] + var detailsIndex = -1 + + for item in currentLayout.items { - if item.wantsNode { + if item.wantsView { currentLayoutItemsWithViews.append(item) if let group = item.distanceThresholdGroup() { let count: Int @@ -175,14 +181,32 @@ class InstantPageViewController: TelegramGenericViewController { } distanceThresholdGroupCount[Int(group)] = count + 1 } + if item.hasLinks { + currentLayoutItemsWithLinks.append(item) + } + if let detailsItem = item as? InstantPageDetailsItem { + detailsIndex += 1 + expandedDetails[detailsIndex] = detailsItem.initiallyExpanded + currentDetailsItems.append(detailsItem) + } } - if item.hasLinks { - currentLayoutItemsWithLinks.append(item) + + } + + if var currentExpandedDetails = self.currentExpandedDetails { + for (index, expanded) in expandedDetails { + if currentExpandedDetails[index] == nil { + currentExpandedDetails[index] = expanded + } } + self.currentExpandedDetails = currentExpandedDetails + } else { + self.currentExpandedDetails = expandedDetails } self.currentLayout = currentLayout self.currentLayoutTiles = currentLayoutTiles + self.currentDetailsItems = currentDetailsItems self.currentLayoutItemsWithViews = currentLayoutItemsWithViews self.currentLayoutItemsWithLinks = currentLayoutItemsWithLinks self.distanceThresholdGroupCount = distanceThresholdGroupCount @@ -193,28 +217,260 @@ class InstantPageViewController: TelegramGenericViewController { override func viewDidResized(_ size: NSSize) { super.viewDidResized(size) + reloadData() } private func reloadData() { updateLayout() - self.containerLayoutUpdated(transition: .immediate) + self.containerLayoutUpdated(animated: false) updateInteractions() } + func isExpandedItem(_ item: InstantPageDetailsItem) -> Bool { + if let index = self.currentDetailsItems.firstIndex(where: {$0 === item}) { + return self.currentExpandedDetails?[index] ?? item.initiallyExpanded + } else { + return false + } + } + + override var supportSwipes: Bool { + return false + } + + override var window: Window? { + if isLoaded() { + return self.view.window as? Window + } else { + return nil + } + } + func updateInteractions() { if let window = window, let layout = currentLayout, let instantPage = instantPage { - selectManager?.initializeHandlers(for: window, instantLayout: layout, instantPage: instantPage, account: account, updateLayout: { [weak self] in - self?.updateVisibleItems() - }, openInfo: { [weak self] peerId, openChat, postId, action in - self?.openInfo(peerId, openChat, postId, action) - self?.modal?.close() - }, openNewTab: { [weak self] mediaId, url in - self?.openNewTab(mediaId, url) + selectManager?.initializeHandlers(for: window, instantLayout: layout, instantPage: instantPage, context: context, updateLayout: { [weak self] in + guard let `self` = self else {return} + + self.updateVisibleItems(visibleBounds: self.genericView.contentView.bounds, animated: false) + }, openUrl: { [weak self] url in + self?.openUrl(url) + }, itemsInRect: { [weak self] rect in + guard let `self` = self, let currentLayout = self.currentLayout else { return [] } + return currentLayout.items.filter{rect.intersects(self.effectiveFrameForItem($0))} + }, effectiveRectForItem: { [weak self] item in + guard let `self` = self else { return NSZeroRect } + return self.effectiveFrameForItem(item) }) } } + private func updateWebEmbedHeight(_ index: Int, _ height: CGFloat) { + + + let currentHeight = self.currentWebEmbedHeights[index] + if height != currentHeight { + if let currentHeight = currentHeight, currentHeight > height { + return + } + self.currentWebEmbedHeights[index] = height + + let signal: Signal = (.complete() |> delay(0.08, queue: Queue.mainQueue())) + self.updateLayoutDisposable.set(signal.start(completed: { [weak self] in + if let strongSelf = self { + NSLog("\(strongSelf.currentWebEmbedHeights)") + + strongSelf.reloadData() + strongSelf.updateVisibleItems(visibleBounds: strongSelf.genericView.contentView.bounds, animated: false) + } + })) + } + } + + private func openUrl(_ url: InstantPageUrlItem) { + var baseUrl = url.url + var anchor: String? + if let anchorRange = url.url.range(of: "#") { + anchor = String(baseUrl[anchorRange.upperBound...]).removingPercentEncoding + baseUrl = String(baseUrl[.. deliverOnMainQueue).start(next: { [weak self] result in + guard let `self` = self else {return} + + switch result { + case let .result(webpage): + if let webpage = webpage, case .Loaded = webpage.content { + self.loadProgress.set(1.0) + showInstantPage(InstantPageViewController(self.context, webPage: webpage, message: nil, anchor: anchor)) + } + break + case let .progress(progress): + self.loadProgress.set(CGFloat(0.07 + progress * (1.0 - 0.07))) + } + })) + } else { + loadProgress.set(1.0) + execute(inapp: result) + } + default: + self.loadProgress.set(1.0) + execute(inapp: result) + } + } + + private func effectiveFrameForTile(_ tile: InstantPageTile) -> CGRect { + let layoutOrigin = tile.frame.origin + var origin = layoutOrigin + for item in self.currentDetailsItems { + let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded + if layoutOrigin.y >= item.frame.maxY { + let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight + origin.y += height - item.frame.height + } + } + return CGRect(origin: origin, size: tile.frame.size) + } + + private func effectiveFrameForItem(_ item: InstantPageItem) -> CGRect { + let layoutOrigin = item.frame.origin + var origin = layoutOrigin + + for item in self.currentDetailsItems { + let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded + if layoutOrigin.y >= item.frame.maxY { + let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight + origin.y += height - item.frame.height + } + } + + if let item = item as? InstantPageDetailsItem { + let expanded = self.currentExpandedDetails?[item.index] ?? item.initiallyExpanded + let height = expanded ? self.effectiveSizeForDetails(item).height : item.titleHeight + return CGRect(origin: origin, size: CGSize(width: item.frame.width, height: height)) + } else { + return CGRect(origin: origin, size: item.frame.size) + } + } + + private func viewForDetailsItem(_ item: InstantPageDetailsItem) -> InstantPageDetailsView? { + for (_, itemView) in self.visibleItemsWithViews { + if let detailsView = itemView as? InstantPageDetailsView, detailsView.item === item { + return detailsView + } + } + return nil + } + + private func effectiveSizeForDetails(_ item: InstantPageDetailsItem) -> CGSize { + if let view = viewForDetailsItem(item) { + return CGSize(width: item.frame.width, height: view.effectiveContentSize.height + item.titleHeight) + } else { + return item.frame.size + } + } + + private func findAnchorItem(_ anchor: String, items: [InstantPageItem]) -> (InstantPageItem, CGFloat, Bool, [InstantPageDetailsItem])? { + for item in items { + if let item = item as? InstantPageAnchorItem, item.anchor == anchor { + return (item, -10.0, false, []) + } else if let item = item as? InstantPageTextItem { + if let (lineIndex, empty) = item.anchors[anchor] { + return (item, item.lines[lineIndex].frame.minY - 10.0, !empty, []) + } + } + else if let item = item as? InstantPageTableItem { + if let (offset, empty) = item.anchors[anchor] { + return (item, offset - 10.0, !empty, []) + } + } + else if let item = item as? InstantPageDetailsItem { + if let (foundItem, offset, reference, detailsItems) = self.findAnchorItem(anchor, items: item.items) { + var detailsItems = detailsItems + detailsItems.insert(item, at: 0) + return (foundItem, offset, reference, detailsItems) + } + } + } + return nil + } + + + private func scrollToAnchor(_ anchor: String, animated: Bool = true) { + guard let items = self.currentLayout?.items else { + return + } + + if !anchor.isEmpty { + if let (item, lineOffset, reference, detailsItems) = findAnchorItem(String(anchor), items: items) { + var previousDetailsView: InstantPageDetailsView? + var containerOffset: CGFloat = 0.0 + for detailsItem in detailsItems { + if let previousView = previousDetailsView { + previousView.contentView.updateDetailsExpanded(detailsItem.index, true, animated: false) + let frame = previousView.effectiveFrameForItem(detailsItem) + containerOffset += frame.minY + + previousDetailsView = previousView.contentView.viewForDetailsItem(detailsItem) + previousDetailsView?.setExpanded(true, animated: false) + } else { + self.updateDetailsExpanded(detailsItem.index, true, animated: false) + let frame = self.effectiveFrameForItem(detailsItem) + containerOffset += frame.minY + + previousDetailsView = self.viewForDetailsItem(detailsItem) + previousDetailsView?.setExpanded(true, animated: false) + } + } + + let frame: CGRect + if let previousDetailsView = previousDetailsView { + frame = previousDetailsView.effectiveFrameForItem(item) + } else { + frame = self.effectiveFrameForItem(item) + } + + let targetY = min(containerOffset + frame.minY + (reference ? -5 : lineOffset), self.documentView.frame.height - self.genericView.frame.height) + genericView.clipView.scroll(to: CGPoint(x: 0.0, y: targetY), animated: animated) + } else if case let .Loaded(content) = webPage.content, let instantPage = content.instantPage, !instantPage.isComplete { + // self.loadProgress.set(0.5) + self.pendingAnchor = anchor + } + } else { + genericView.clipView.scroll(to: NSZeroPoint, animated: animated) + } + + + } + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) updateInteractions() @@ -226,43 +482,61 @@ class InstantPageViewController: TelegramGenericViewController { func openInfo(_ peerId:PeerId, _ openChat: Bool, _ postId:MessageId?, _ action:ChatInitialAction?) { if openChat { - account.context.mainNavigation?.push(ChatController(account: account, peerId: peerId, messageId: postId, initialAction: action)) + context.sharedContext.bindings.rootNavigation().push(ChatController(context: context, chatLocation: .peer(peerId), messageId: postId, initialAction: action)) closeModal() } else { - openPeerInfoDisposable.set((account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue).start(next: { [weak self] peer in - if let strongSelf = self { - strongSelf.account.context.mainNavigation?.push(PeerInfoController(account: strongSelf.account, peer: peer)) - strongSelf.closeModal() - } - })) + context.sharedContext.bindings.rootNavigation().push(PeerInfoController(context: context, peerId: peerId)) + closeModal() } } - func openNewTab(_ mediaId: MediaId, _ url: String) { - let getMedia = account.postbox.modify { modifier -> Media? in - return modifier.getMedia(mediaId) - } |> deliverOnMainQueue - mediaDisposable.set(getMedia.start(next: { [weak self] media in - if let media = media as? TelegramMediaWebpage, let strongSelf = self { - strongSelf.navigationController?.push(InstantPageViewController(strongSelf.account, webPage: media, message: nil)) - } else { - execute(inapp: .external(link: url, false)) - } - })) - } - +// func openNewTab(_) { +// let getMedia = account.postbox.transaction { transaction -> Media? in +// return transaction.getMedia(mediaId) +// } |> deliverOnMainQueue +// mediaDisposable.set(getMedia.start(next: { [weak self] media in +// if let media = media as? TelegramMediaWebpage, let strongSelf = self, case let .Loaded(content) = media.content, let page = content.instantPage { +// strongSelf.navigationController?.push(InstantPageViewController(strongSelf.account, webPage: media, message: nil)) +// } else if let window = self?.window, let account = self?.account { +// self.loadProgress.set(0.02) +// _ = (webpagePreviewWithProgress(account: account, url: url, webpageId: mediaId) |> deliverOnMainQueue).start(next: { result in +// +// if let page = page { +// switch page.content { +// case let .Loaded(content): +// if let _ = content.instantPage { +// showInstantPage(InstantPageViewController(account, webPage: page, message: nil)) +// } else { +// execute(inapp: .external(link: url, false)) +// } +// default: +// execute(inapp: .external(link: url, false)) +// } +// } else { +// execute(inapp: .external(link: url, false)) +// } +// }) +// } else { +// execute(inapp: .external(link: url, false)) +// } +// })) +// } +// + override func viewDidLoad() { - view.background = theme.colors.background + + + genericView.deltaCorner = -1 genericView.documentView = View(frame: genericView.bounds) NotificationCenter.default.addObserver(forName: NSView.boundsDidChangeNotification, object: genericView.contentView, queue: nil, using: { [weak self] _ in - if let strongSelf = self { - strongSelf.updateVisibleItems() - strongSelf.pageDidScrolled?((documentSize: strongSelf.genericView.frame.size, position: strongSelf.genericView.scrollPosition().current)) - } + guard let `self` = self else { return } + self.updateVisibleItems(visibleBounds: self.genericView.contentView.bounds, animated: false) + self.pageDidScrolled?((documentSize: self.genericView.frame.size, position: self.genericView.scrollPosition().current)) + self.saveArticleProgress() }) genericView.hasVerticalScroller = true selectManager = InstantPageSelectText(genericView) @@ -270,16 +544,12 @@ class InstantPageViewController: TelegramGenericViewController { var firstLoad: Bool = true - ready.set((ivAppearance(postbox: account.postbox) |> deliverOnMainQueue |> map { [weak self] appearance -> Bool in + appearanceDisposable.set((ivAppearance(postbox: context.account.postbox) |> deliverOnMainQueue).start(next: { [weak self] appearance in self?.appearance = appearance self?.reloadData() - - if firstLoad, let currentLayout = self?.currentLayout, let webPage = self?.webPage, let message = self?.message, let scrollView = self?.genericView { + if firstLoad, let currentLayout = self?.currentLayout, let webPage = self?.webPage, let scrollView = self?.genericView { firstLoad = false - - if let mediaId = webPage.id, let state = appearance.state[mediaId] { - self?.applyScrollState(state) - } else { + if let message = self?.message { switch webPage.content { case .Loaded(let content): @@ -289,7 +559,7 @@ class InstantPageViewController: TelegramGenericViewController { let range = message.nsstring.range(of: content.url) if range.location != NSNotFound { - if let link = attr.attribute(NSAttributedStringKey.link, at: range.location, effectiveRange: nil) as? inAppLink { + if let link = attr.attribute(NSAttributedString.Key.link, at: range.location, effectiveRange: nil) as? inAppLink { switch link { case let .external(url, _): let anchorRange = url.nsstring.range(of: "#") @@ -300,7 +570,6 @@ class InstantPageViewController: TelegramGenericViewController { if item.matchesAnchor(anchor) { scrollView.clipView.scroll(to: item.frame.origin, animated: false) scrollView.reflectScrolledClipView(scrollView.clipView) - break } } } @@ -315,30 +584,41 @@ class InstantPageViewController: TelegramGenericViewController { break } } + if let mediaId = webPage.id, let state = appearance.state[mediaId] { + self?.applyScrollState(state) + } } - - return true })) - actualizeDisposable.set((actualizedWebpage(postbox: account.postbox, network: account.network, webpage: webPage) |> delay(1.0, queue: Queue.mainQueue()) |> deliverOnMainQueue).start(next: { [weak self] webpage in - switch webpage.content { - case .Loaded(let content): - if content.instantPage != nil { - self?.webPage = webpage - self?.reloadData() - } - default: - break + + loadStoredStateDisposable.set((instantPageStoredState(postbox: context.account.postbox, webPage: webPage) |> deliverOnMainQueue).start(next: { [weak self] state in + guard let `self` = self else { + return } - + self.updateWebPage(self.webPage, anchor: self.pendingAnchor, state: state) + if let anchor = self.pendingAnchor { + self.pendingAnchor = nil + self.scrollToAnchor(anchor, animated: false) + } + })) + + actualizeDisposable.set((actualizedWebpage(postbox: context.account.postbox, network: context.account.network, webpage: webPage) |> deliverOnMainQueue).start(next: { [weak self] webpage in + self?.updateWebPage(webpage, anchor: self?.pendingAnchor) })) } + + override func readyOnce() { + if !didSetReady { + loadProgress.set(1.0) + } + super.readyOnce() + } - func containerLayoutUpdated(transition: ContainedViewLayoutTransition) { + func containerLayoutUpdated(animated: Bool) { if visibleItemsWithViews.isEmpty && visibleTiles.isEmpty { genericView.contentView.bounds = NSZeroRect } - self.updateVisibleItems() + self.updateVisibleItems(visibleBounds: self.genericView.contentView.bounds, animated: animated) } override var enableBack: Bool { @@ -348,40 +628,65 @@ class InstantPageViewController: TelegramGenericViewController { return false } - private var documentView: View { - return genericView.documentView as! View + private var documentView: NSView { + return genericView.documentView! + } + + + private func updateDetailsExpanded(_ index: Int, _ expanded: Bool, animated: Bool = true) { + if var currentExpandedDetails = self.currentExpandedDetails { + currentExpandedDetails[index] = expanded + self.currentExpandedDetails = currentExpandedDetails + } + self.updateVisibleItems(visibleBounds: self.genericView.contentView.bounds, animated: animated) } - func updateVisibleItems() { + func updateVisibleItems(visibleBounds: CGRect, animated: Bool = false) { + + + CATransaction.begin() + + defer { + CATransaction.commit() + } + var visibleTileIndices = Set() var visibleItemIndices = Set() - let visibleBounds = genericView.contentView.bounds - var tileIndex = -1 - for tile in self.currentLayoutTiles { - tileIndex += 1 - var tileVisibleFrame = tile.frame - tileVisibleFrame.origin.y -= 400.0 - tileVisibleFrame.size.height += 400.0 * 2.0 - if tileVisibleFrame.intersects(visibleBounds) { - visibleTileIndices.insert(tileIndex) - - if let tile = visibleTiles[tileIndex] { - tile.needsDisplay = true - } else { - let tileNode = InstantPageTileView(tile: tile) - tileNode.frame = tile.frame - documentView.addSubview(tileNode) - self.visibleTiles[tileIndex] = tileNode - } - + var topView: NSView? + let topTileView = topView + for view in documentView.subviews.reversed() { + if let view = view as? InstantPageTileView { + topView = view + break } } + let visibleBounds = genericView.contentView.bounds + + + + var collapseOffset: CGFloat = 0.0 + + var itemIndex = -1 + var embedIndex = -1 + var detailsIndex = -1 + + var previousDetailsView: InstantPageDetailsView? + + for item in self.currentLayoutItemsWithViews { itemIndex += 1 + + if item is InstantPageWebEmbedItem { + embedIndex += 1 + } + if item is InstantPageDetailsItem { + detailsIndex += 1 + } + var itemThreshold: CGFloat = 0.0 if let group = item.distanceThresholdGroup() { var count: Int = 0 @@ -390,42 +695,135 @@ class InstantPageViewController: TelegramGenericViewController { } itemThreshold = item.distanceThresholdWithGroupCount(count) } - var itemFrame = item.frame - itemFrame.origin.y -= itemThreshold - itemFrame.size.height += itemThreshold * 2.0 - if visibleBounds.intersects(itemFrame) { + + + var itemFrame = item.frame.offsetBy(dx: 0.0, dy: -collapseOffset) + var thresholdedItemFrame = itemFrame + thresholdedItemFrame.origin.y -= itemThreshold + thresholdedItemFrame.size.height += itemThreshold * 2.0 + + if let detailsItem = item as? InstantPageDetailsItem, let expanded = self.currentExpandedDetails?[detailsIndex] { + let height = expanded ? self.effectiveSizeForDetails(detailsItem).height : detailsItem.titleHeight + collapseOffset += itemFrame.height - height + itemFrame = CGRect(origin: itemFrame.origin, size: CGSize(width: itemFrame.width, height: height)) + } + + if visibleBounds.intersects(thresholdedItemFrame) { visibleItemIndices.insert(itemIndex) - var itemNode = self.visibleItemsWithViews[itemIndex] - if let currentItemNode = itemNode { - if !item.matchesNode(currentItemNode) { - (currentItemNode as! View).removeFromSuperview() + var itemView = self.visibleItemsWithViews[itemIndex] + if let currentItemView = itemView { + if !item.matchesView(currentItemView) { + (currentItemView as! NSView).removeFromSuperview() self.visibleItemsWithViews.removeValue(forKey: itemIndex) - itemNode = nil + itemView = nil } } - if itemNode == nil { - if let itemNode = item.node(account: self.account) { - (itemNode as! View).frame = item.frame - documentView.addSubview(itemNode as! View) - self.visibleItemsWithViews[itemIndex] = itemNode + if itemView == nil { + let embedIndex = embedIndex + let detailsIndex = detailsIndex + + let arguments = InstantPageItemArguments(context: context, theme: instantPageThemeForType(theme.insantPageThemeType, settings: appearance), openMedia: { media in + + }, openPeer: { peerId in + + }, openUrl: { [weak self] url in + self?.openUrl(url) + }, updateWebEmbedHeight: { [weak self] height in + self?.updateWebEmbedHeight(embedIndex, height) + }, updateDetailsExpanded: { [weak self] expanded in + self?.updateDetailsExpanded(detailsIndex, expanded) + }, isExpandedItem: { [weak self] item in + return self?.isExpandedItem(item) ?? false + }, effectiveRectForItem: { [weak self] item in + return self?.effectiveFrameForItem(item) ?? item.frame + }) + + if let newView = item.view(arguments: arguments, currentExpandedDetails: self.currentExpandedDetails) { + newView.frame = itemFrame + documentView.addSubview(newView) + topView = newView + self.visibleItemsWithViews[itemIndex] = newView + itemView = newView + + if let itemView = itemView as? InstantPageDetailsView { + itemView.requestLayoutUpdate = { [weak self] animated in + if let strongSelf = self { + strongSelf.updateVisibleItems(visibleBounds: strongSelf.genericView.contentView.bounds, animated: animated) + } + } + + if let previousDetailsView = previousDetailsView { + if itemView.frame.minY - previousDetailsView.frame.maxY < 1.0 { + itemView.previousView = previousDetailsView + } + } + previousDetailsView = itemView + } + } } else { - (itemNode as! View).removeFromSuperview() - documentView.addSubview((itemNode as! View)) - if (itemNode as! View).frame != item.frame { - (itemNode as! View).frame = item.frame + if (itemView as! NSView).frame != itemFrame { + (itemView as! NSView)._change(size: itemFrame.size, animated: animated) + (itemView as! NSView)._change(pos: itemFrame.origin, animated: animated) + } else { + (itemView as! NSView).needsDisplay = true } } + + if let itemView = itemView as? InstantPageDetailsView { + itemView.updateVisibleItems(visibleBounds: visibleBounds.offsetBy(dx: -itemView.frame.minX, dy: -itemView.frame.minY), animated: animated) + } } } + topView = topTileView + + var tileIndex = -1 + for tile in self.currentLayoutTiles { + tileIndex += 1 + + let tileFrame = effectiveFrameForTile(tile) + var tileVisibleFrame = tileFrame + tileVisibleFrame.origin.y -= 800.0 + tileVisibleFrame.size.height += 800.0 * 2.0 + if tileVisibleFrame.intersects(visibleBounds) { + visibleTileIndices.insert(tileIndex) + + if self.visibleTiles[tileIndex] == nil { + let tileView = InstantPageTileView(tile: tile, backgroundColor: .clear) + tileView.frame = tileFrame + documentView.addSubview(tileView) + topView = tileView + self.visibleTiles[tileIndex] = tileView + } else { + if visibleTiles[tileIndex]!.frame != tileFrame { + let view = self.visibleTiles[tileIndex]! + view._change(pos: tileFrame.origin, animated: animated) + view._change(size: tileFrame.size, animated: animated) + } else { + visibleTiles[tileIndex]!.needsDisplay = true + } + } + } else { + var bp:Int = 0 + bp += 1 + } + } + + if let currentLayout = self.currentLayout { + let effectiveContentHeight = currentLayout.contentSize.height - collapseOffset + if effectiveContentHeight != self.genericView.contentSize.height { + documentView.setFrameSize(CGSize(width: currentLayout.contentSize.width, height: effectiveContentHeight)) + } + } + var removeTileIndices: [Int] = [] - for (index, tileNode) in self.visibleTiles { + for (index, tileView) in self.visibleTiles { if !visibleTileIndices.contains(index) { removeTileIndices.append(index) - tileNode.removeFromSuperview() + tileView.removeFromSuperview() } } for index in removeTileIndices { @@ -433,24 +831,29 @@ class InstantPageViewController: TelegramGenericViewController { } var removeItemIndices: [Int] = [] - for (index, itemNode) in self.visibleItemsWithViews { + for (index, itemView) in self.visibleItemsWithViews { if !visibleItemIndices.contains(index) { removeItemIndices.append(index) - (itemNode as! View).removeFromSuperview() + (itemView as! NSView).removeFromSuperview() } else { - var itemFrame = (itemNode as! View).frame + var itemFrame = (itemView as! NSView).frame let itemThreshold: CGFloat = 200.0 itemFrame.origin.y -= itemThreshold itemFrame.size.height += itemThreshold * 2.0 - itemNode.updateIsVisible(visibleBounds.intersects(itemFrame)) + itemView.updateIsVisible(visibleBounds.intersects(itemFrame)) } } + let subviews = documentView.subviews.sorted(by: {$0.frame.minY < $1.frame.minY}) + documentView.subviews = subviews + documentView.needsLayout = true + for index in removeItemIndices { self.visibleItemsWithViews.removeValue(forKey: index) } } + override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) if let window = window { @@ -491,27 +894,68 @@ class InstantPageViewController: TelegramGenericViewController { private func applyScrollState(_ state: IVReadState) { if let currentLayout = currentLayout, Int32(currentLayout.items.count) > state.blockId, let scrollState = scrollState { let item = currentLayout.items[Int(state.blockId)] - let offset = CGPoint(x: 0, y: genericView.contentInsets.top + item.frame.origin.y + CGFloat(scrollState.blockOffset) - 8) + let offset = CGPoint(x: 0, y: genericView.contentInsets.top + item.frame.origin.y + CGFloat(scrollState.blockOffset)) genericView.clipView.scroll(to: offset, animated: false) genericView.reflectScrolledClipView(genericView.clipView) } } - + // Called when space button is enabled to trigger scrolling + enum Direction { + case up + case down + } + func scrollPage(direction: Direction) { + self.updateVisibleItems(visibleBounds: self.genericView.contentView.bounds, animated: false) + + var newOrigin = genericView.clipView.bounds.origin + switch direction { + case .up: + // without *2 we will stay at current position + newOrigin.y -= 50 + if newOrigin.y < 0 { + newOrigin.y = 0 + } + case .down: + newOrigin.y += 50 + let maxY = genericView.documentSize.height - genericView.clipView.frame.height + if newOrigin.y > maxY { + newOrigin.y = maxY + } + } + + genericView.clipView.scroll(to: newOrigin, animated: true) + genericView.reflectScrolledClipView(genericView.clipView) + pageDidScrolled?((documentSize: genericView.frame.size, position: genericView.scrollPosition().current)) + } + + private func saveArticleProgress() { + let point = CGPoint(x: genericView.frame.size.width / 2.0, y: genericView.contentOffset.y + genericView.contentInsets.top) + + let id = self.webPage.webpageId + + let percent = Int32((point.y + frame.height) / genericView.documentSize.height * 100.0) + } + deinit { NotificationCenter.default.removeObserver(self) selectManager?.removeHandlers(for: mainWindow) joinDisposable.dispose() actualizeDisposable.dispose() - openPeerInfoDisposable.dispose() + saveProgressDisposable.dispose() mediaDisposable.dispose() + updateLayoutDisposable.dispose() + loadWebpageDisposable.dispose() + appearanceDisposable.dispose() + loadStoredStateDisposable.dispose() if let window = window { selectManager?.removeHandlers(for: window) } if let state = scrollState, let mediaId = webPage.id { - _ = updateInstantViewAppearanceSettingsInteractively(postbox: account.postbox, {$0.withUpdatedIVState(state, for: mediaId)}).start() + // _ = updateInstantViewAppearanceSettingsInteractively(postbox: account.postbox, {$0.withUpdatedIVState(state, for: mediaId)}).start() } + } } diff --git a/Telegram-Mac/InstantPageWebEmbedItem.swift b/Telegram-Mac/InstantPageWebEmbedItem.swift index 4cf21bd544..8431af73ed 100644 --- a/Telegram-Mac/InstantPageWebEmbedItem.swift +++ b/Telegram-Mac/InstantPageWebEmbedItem.swift @@ -7,18 +7,20 @@ // import Cocoa -import TelegramCoreMac +import TelegramCore + final class InstantPageWebEmbedItem: InstantPageItem { var frame: CGRect let hasLinks: Bool = false - let wantsNode: Bool = true + let wantsView: Bool = true let medias: [InstantPageMedia] = [] let url: String? let html: String? let enableScrolling: Bool - + let separatesTiles: Bool = false + let isInteractive: Bool = false init(frame: CGRect, url: String?, html: String?, enableScrolling: Bool) { @@ -28,16 +30,19 @@ final class InstantPageWebEmbedItem: InstantPageItem { self.enableScrolling = enableScrolling } - func node(account: Account) -> InstantPageView? { - return instantPageWebEmbedView(frame: self.frame, url: self.url, html: self.html, enableScrolling: self.enableScrolling) + func view(arguments: InstantPageItemArguments, currentExpandedDetails: [Int : Bool]?) -> (InstantPageView & NSView)? { + return InstantPageWebEmbedView(frame: self.frame, url: self.url, html: self.html, enableScrolling: self.enableScrolling, updateWebEmbedHeight: { height in + arguments.updateWebEmbedHeight(height) + + }) } func matchesAnchor(_ anchor: String) -> Bool { return false } - func matchesNode(_ node: InstantPageView) -> Bool { - if let node = node as? instantPageWebEmbedView { + func matchesView(_ node: InstantPageView) -> Bool { + if let node = node as? InstantPageWebEmbedView { return self.url == node.url && self.html == node.html } else { return false diff --git a/Telegram-Mac/InstantVideoPIP.swift b/Telegram-Mac/InstantVideoPIP.swift index 86f6ae7aa9..10c94e72a1 100644 --- a/Telegram-Mac/InstantVideoPIP.swift +++ b/Telegram-Mac/InstantVideoPIP.swift @@ -8,9 +8,10 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit enum InstantVideoPIPCornerAlignment { case topLeft @@ -19,7 +20,7 @@ enum InstantVideoPIPCornerAlignment { case bottomRight } -class InstantVideoPIPView : GIFContainerView { +class InstantVideoPIPView : GIFPlayerView { let playingProgressView: RadialProgressView = RadialProgressView(theme:RadialProgressTheme(backgroundColor: .clear, foregroundColor: NSColor.white.withAlphaComponent(0.8), lineWidth: 3), twist: false) override init() { @@ -28,6 +29,7 @@ class InstantVideoPIPView : GIFContainerView { required init(frame frameRect: NSRect) { super.init() + setFrameSize(NSMakeSize(200, 200)) playingProgressView.userInteractionEnabled = false } @@ -51,23 +53,34 @@ class InstantVideoPIPView : GIFContainerView { } class InstantVideoPIP: GenericViewController, APDelegate { - private let controller:APController + private var controller:APController + private var context: AccountContext private weak var tableView:TableView? private var listener:TableScrollListener! + private let dataDisposable = MetaDisposable() + private let fetchDisposable = MetaDisposable() + private var currentMessage:Message? = nil private var scrollTime: TimeInterval = CFAbsoluteTimeGetCurrent() private var alignment:InstantVideoPIPCornerAlignment = .topRight private var isShown:Bool = false - init(_ controller:APController, window:Window) { + + private var timebase: CMTimebase? = nil { + didSet { + genericView.reset(with: timebase) + } + } + + init(_ controller:APController, context: AccountContext, window:Window) { self.controller = controller - + self.context = context super.init() listener = TableScrollListener({ [weak self] _ in self?.updateScrolled() }) controller.add(listener: self) - (controller.account.context.mainNavigation as? MajorNavigationController)?.add(listener: WeakReference(value: self)) + context.sharedContext.bindings.rootNavigation().add(listener: WeakReference(value: self)) } override var window:Window? { @@ -82,10 +95,10 @@ class InstantVideoPIP: GenericViewController, APDelegate { } override func navigationWillChangeController() { - if let controller = controller.account.context.mainNavigation?.controller as? ChatController { - updateTableView(controller.genericView.tableView) + if let controller = context.sharedContext.bindings.rootNavigation().controller as? ChatController { + updateTableView(controller.genericView.tableView, context: context, controller: self.controller) } else { - updateTableView(nil) + updateTableView(nil, context: context, controller: self.controller) } } @@ -128,15 +141,35 @@ class InstantVideoPIP: GenericViewController, APDelegate { if isShown { hide() } + dataDisposable.dispose() + fetchDisposable.dispose() } func showIfNeeded(animated: Bool = true) { loadViewIfNeeded() isShown = true - if let media = currentMessage?.media.first as? TelegramMediaFile { - let signal:Signal<(TransformImageArguments) -> DrawingContext?, NoError> - signal = chatWebpageSnippetPhoto(account: controller.account, photo: TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: media.previewRepresentations), scale: view.backingScaleFactor, small:true) - genericView.update(with: media.resource, size: NSMakeSize(150, 150), viewSize: NSMakeSize(150, 150), account: controller.account, table: nil, iconSignal: signal) + genericView.animatesAlphaOnFirstTransition = false + if let message = currentMessage, let media = message.media.first as? TelegramMediaFile { + let signal:Signal = chatMessageVideo(postbox: context.account.postbox, fileReference: FileMediaReference.message(message: MessageReference(message), media: media), scale: view.backingScaleFactor) + + let resource = FileMediaReference.message(message: MessageReference(message), media: media) + + let data: Signal = context.account.postbox.mediaBox.resourceData(resource.media.resource) |> map { resource in + if resource.complete { + return AVGifData.dataFrom(resource.path) + } else if let resource = media.resource as? LocalFileReferenceMediaResource { + return AVGifData.dataFrom(resource.localFilePath) + } else { + return nil + } + } |> deliverOnMainQueue + + genericView.setSignal(signal) + + dataDisposable.set(data.start(next: { [weak self] data in + self?.genericView.set(data: data, timebase: self?.timebase) + })) + } if let contentView = window?.contentView, genericView.superview == nil { @@ -171,7 +204,7 @@ class InstantVideoPIP: GenericViewController, APDelegate { } startDragPosition = nil if let opacity = strongSelf.view.layer?.opacity, opacity < 0.5 { - globalAudio?.notifyCompleteQueue() + globalAudio?.notifyCompleteQueue(animated: true) globalAudio?.cleanup() } else { strongSelf.findCorner() @@ -230,7 +263,7 @@ class InstantVideoPIP: GenericViewController, APDelegate { point = NSMakePoint(-view.frame.width, 100) } - genericView.change(pos: point, animated: animated, completion: { [weak view] completed in + genericView._change(pos: point, animated: animated, completion: { [weak view] completed in view?.removeFromSuperview() }) } @@ -242,13 +275,13 @@ class InstantVideoPIP: GenericViewController, APDelegate { if let contentView = window?.contentView { switch corner { case .topRight: - genericView.change(pos: NSMakePoint(contentView.frame.width - view.frame.width - 20, contentView.frame.height - view.frame.height - 130), animated: animated) + genericView._change(pos: NSMakePoint(contentView.frame.width - view.frame.width - 20, contentView.frame.height - view.frame.height - 130), animated: animated) case .topLeft: - genericView.change(pos: NSMakePoint(20, contentView.frame.height - view.frame.height - 130), animated: animated) + genericView._change(pos: NSMakePoint(20, contentView.frame.height - view.frame.height - 130), animated: animated) case .bottomRight: - genericView.change(pos: NSMakePoint(contentView.frame.width - view.frame.width - 20, 100), animated: animated) + genericView._change(pos: NSMakePoint(contentView.frame.width - view.frame.width - 20, 100), animated: animated) case .bottomLeft: - genericView.change(pos: NSMakePoint(20, 100), animated: animated) + genericView._change(pos: NSMakePoint(20, 100), animated: animated) } } @@ -275,16 +308,22 @@ class InstantVideoPIP: GenericViewController, APDelegate { } } - func updateTableView(_ tableView:TableView?) { + func updateTableView(_ tableView:TableView?, context: AccountContext, controller: APController) { self.tableView?.removeScroll(listener: listener) self.tableView = tableView + self.context = context self.tableView?.addScroll(listener: listener) + if controller != self.controller { + self.controller = controller + controller.add(listener: self) + } + updateScrolled() } - func songDidChanged(song:APSongItem, for controller:APController) { + func songDidChanged(song:APSongItem, for controller:APController, animated: Bool) { var msg:Message? = nil switch song.entry { case let .song(message): @@ -298,44 +337,44 @@ class InstantVideoPIP: GenericViewController, APDelegate { if let msg = msg { if let currentMessage = currentMessage, !isShown && currentMessage.id != msg.id, CFAbsoluteTimeGetCurrent() - scrollTime > 1.0 { if let item = tableView?.item(stableId: msg.chatStableId) { - tableView?.scroll(to: .center(id: item.stableId, animated: true, focus: false, inset: 0)) + tableView?.scroll(to: .center(id: item.stableId, innerId: nil, animated: true, focus: .init(focus: false), inset: 0)) } } currentMessage = msg - genericView.timebase = controller.timebase +// genericView.reset(with: controller.timebase, false) } else { currentMessage = nil - genericView.player.reset(with: nil) + self.timebase = nil } updateScrolled() } - func songDidChangedState(song: APSongItem, for controller: APController) { + func songDidChangedState(song: APSongItem, for controller: APController, animated: Bool) { switch song.state { - case let .playing(data): - genericView.playingProgressView.state = .ImpossibleFetching(progress: Float(data.progress), force: false) + case let .playing(_, _, progress): + genericView.playingProgressView.state = .ImpossibleFetching(progress: Float(progress), force: false) case .stoped, .waiting, .fetching: genericView.playingProgressView.state = .None - case let .paused(data): - genericView.playingProgressView.state = .ImpossibleFetching(progress: Float(data.progress), force: true) + case let .paused(_, _, progress): + genericView.playingProgressView.state = .ImpossibleFetching(progress: Float(progress), force: true) } } - func songDidStartPlaying(song:APSongItem, for controller:APController) { + func songDidStartPlaying(song:APSongItem, for controller:APController, animated: Bool) { } - func songDidStopPlaying(song:APSongItem, for controller:APController) { + func songDidStopPlaying(song:APSongItem, for controller:APController, animated: Bool) { if song.stableId == currentMessage?.chatStableId { - genericView.timebase = nil + //self.timebase = nil } } - func playerDidChangedTimebase(song:APSongItem, for controller:APController) { + func playerDidChangedTimebase(song:APSongItem, for controller:APController, animated: Bool) { if song.stableId == currentMessage?.chatStableId { - genericView.timebase = controller.timebase + self.timebase = controller.timebase } } - func audioDidCompleteQueue(for controller:APController) { + func audioDidCompleteQueue(for controller:APController, animated: Bool) { hide() } diff --git a/Telegram-Mac/InstantViewAppearance.swift b/Telegram-Mac/InstantViewAppearance.swift index 2d1f9aabf3..68a63a6e7d 100644 --- a/Telegram-Mac/InstantViewAppearance.swift +++ b/Telegram-Mac/InstantViewAppearance.swift @@ -8,10 +8,28 @@ import Cocoa -import PostboxMac -import SwiftSignalKitMac +import Postbox +import SwiftSignalKit -public struct IVReadState : PostboxCoding { +public enum InstantPageThemeType: Int32 { + case light = 0 + case dark = 1 + case sepia = 2 + case gray = 3 +} + +public enum InstantPagePresentationFontSize: Int32 { + case small = 0 + case standard = 1 + case large = 2 + case xlarge = 3 + case xxlarge = 4 +} + + + + +public struct IVReadState : PostboxCoding, Equatable { let blockId:Int32 let blockOffset: Int32 public init(blockId:Int32, blockOffset: Int32) { @@ -27,6 +45,7 @@ public struct IVReadState : PostboxCoding { encoder.encodeInt32(blockId, forKey: "bi") encoder.encodeInt32(blockOffset, forKey: "bo") } + } @@ -61,7 +80,7 @@ public struct InstantViewAppearance: PreferencesEntry, Equatable { } public static func ==(lhs: InstantViewAppearance, rhs: InstantViewAppearance) -> Bool { - return lhs.fontSerif == rhs.fontSerif + return lhs.fontSerif == rhs.fontSerif && lhs.state == rhs.state } func withUpdatedFontSerif(_ fontSerif: Bool) -> InstantViewAppearance { @@ -76,8 +95,8 @@ public struct InstantViewAppearance: PreferencesEntry, Equatable { } func updateInstantViewAppearanceSettingsInteractively(postbox: Postbox, _ f: @escaping (InstantViewAppearance) -> InstantViewAppearance) -> Signal { - return postbox.modify { modifier -> Void in - modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.instantViewAppearance, { entry in + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.instantViewAppearance, { entry in let currentSettings: InstantViewAppearance if let entry = entry as? InstantViewAppearance { currentSettings = entry @@ -89,7 +108,7 @@ func updateInstantViewAppearanceSettingsInteractively(postbox: Postbox, _ f: @es } } -func ivAppearance(postbox: Postbox) -> Signal { +func ivAppearance(postbox: Postbox) -> Signal { return postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.instantViewAppearance]) |> map { preferences in return (preferences.values[ApplicationSpecificPreferencesKeys.instantViewAppearance] as? InstantViewAppearance) ?? InstantViewAppearance.defaultSettings } diff --git a/Telegram-Mac/InstantViewWindow.swift b/Telegram-Mac/InstantViewWindow.swift index cdff00701e..7f6cfc7131 100644 --- a/Telegram-Mac/InstantViewWindow.swift +++ b/Telegram-Mac/InstantViewWindow.swift @@ -8,17 +8,18 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit private final class InstantViewArguments { - let account: Account + let context: AccountContext let share:()->Void let back:()->Void let openInSafari:()->Void let enableSansSerif:(Bool)->Void - init(account: Account, share: @escaping()->Void, back: @escaping()->Void, openInSafari: @escaping()->Void, enableSansSerif:@escaping(Bool)->Void) { - self.account = account + init(context: AccountContext, share: @escaping()->Void, back: @escaping()->Void, openInSafari: @escaping()->Void, enableSansSerif:@escaping(Bool)->Void) { + self.context = context self.share = share self.back = back self.enableSansSerif = enableSansSerif @@ -60,7 +61,7 @@ private class HeaderView : View { } private func initialize() { - updateLocalizationAndTheme() + updateLocalizationAndTheme(theme: theme) addSubview(borderView) addSubview(share) addSubview(actions) @@ -103,13 +104,13 @@ private class HeaderView : View { view.setFrameOrigin(view.frame.minX, view.frame.minY - 1) if animated { addSubview(view) - view.layer?.animateAlpha(from: 0, to: 1, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self, weak view] completed in + view.layer?.animateAlpha(from: 0, to: 1, duration: 0.2, timingFunction: CAMediaTimingFunctionName.spring, completion: { [weak self, weak view] completed in if completed { self?.titleView?.removeFromSuperview() self?.titleView = view } }) - titleView?.change(opacity: 0, timingFunction: kCAMediaTimingFunctionSpring) + titleView?.change(opacity: 0, timingFunction: CAMediaTimingFunctionName.spring) } else { titleView?.removeFromSuperview() titleView = view @@ -143,8 +144,9 @@ private class HeaderView : View { back.centerY(x: 86) } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) borderView.backgroundColor = theme.colors.border backgroundColor = theme.colors.background titleView?.backgroundColor = theme.colors.background @@ -157,10 +159,10 @@ private class HeaderView : View { safari.set(image: theme.icons.instantViewSafari, for: .Normal) back.set(image: theme.icons.instantViewBack, for: .Normal) - share.sizeToFit() - actions.sizeToFit() - safari.sizeToFit() - back.sizeToFit() + _ = share.sizeToFit() + _ = actions.sizeToFit() + _ = safari.sizeToFit() + _ = back.sizeToFit() } required init?(coder: NSCoder) { @@ -171,6 +173,8 @@ private class HeaderView : View { class InstantWindowContentView : View { private let headerView: HeaderView = HeaderView(frame: NSZeroRect) private let contentView: View = View() + fileprivate let loadingIndicatorView: LinearProgressControl = LinearProgressControl(progressHeight: 2) + fileprivate var arguments: InstantViewArguments? { didSet { headerView.arguments = arguments @@ -186,14 +190,18 @@ class InstantWindowContentView : View { super.init(frame: frameRect) addSubview(headerView) addSubview(contentView) + addSubview(loadingIndicatorView) + loadingIndicatorView.layer?.opacity = 0 + flip = false contentView.autoresizesSubviews = false layout() } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) contentView.backgroundColor = theme.colors.background backgroundColor = theme.colors.background + loadingIndicatorView.style = ControlStyle(foregroundColor: theme.colors.text, backgroundColor: backgroundColor) } func updateTitle(_ title:String, animated: Bool) { @@ -219,9 +227,11 @@ class InstantWindowContentView : View { override func layout() { super.layout() - headerView.frame = NSMakeRect(0, 0, frame.width, barHeight) - contentView.frame = NSMakeRect(0, headerView.frame.maxY, frame.width, frame.height - headerView.frame.height) + headerView.frame = NSMakeRect(0, frame.height - barHeight, frame.width, barHeight) + contentView.frame = NSMakeRect(0, 0, min(frame.width, 720), frame.height - headerView.frame.height) + contentView.centerX() contentView.subviews.first?.frame = contentView.bounds + loadingIndicatorView.frame = NSMakeRect(0, frame.height - barHeight, frame.width, loadingIndicatorView.frame.height) } } @@ -237,33 +247,33 @@ class InstantViewController : TelegramGenericViewController 0, animated: true) } - let arguments = InstantViewArguments(account: account, share: { [weak self] in + let arguments = InstantViewArguments(context: context, share: { [weak self] in self?.share() }, back: { [weak self] in self?.navigation.back() @@ -281,33 +291,80 @@ class InstantViewController : TelegramGenericViewController deliverOnMainQueue).start(next: { [weak self] appearance in + appearanceDisposable.set((ivAppearance(postbox: context.account.postbox) |> deliverOnMainQueue).start(next: { [weak self] appearance in self?.genericView.presenation = appearance })) _window.closeInterceptor = { [weak self] in - self?._window.orderOut(nil) - instantController = nil + if let window = self?._window, !window.styleMask.contains(.fullScreen) { + self?._window.orderOut(nil) + instantController = nil + } else { + self?._window.toggleFullScreen(nil) + delay(0.8, closure: { + self?._window.orderOut(self) + instantController = nil + }) + } + return true } - let closeKeyboardHandler:()->KeyHandlerResult = { [weak self] in + let closeKeyboardHandler:(NSEvent)->KeyHandlerResult = { [weak self] _ in if let window = self?._window { if !window.styleMask.contains(.fullScreen) { self?._window.orderOut(nil) instantController = nil + } else { + window.toggleFullScreen(nil) } } - + return .invoked } + _window.set(handler: { [weak page] _ in + page?.scrollPage(direction: .up) + return .invoked + }, with: self, for: .UpArrow, priority: .medium) + + _window.set(handler: { [weak page] _ in + page?.scrollPage(direction: .down) + return .invoked + }, with: self, for: .DownArrow, priority: .medium) + + _window.set(handler: closeKeyboardHandler, with: self, for: .Escape) - _window.set(handler: closeKeyboardHandler, with: self, for: .Space) + if FastSettings.instantViewScrollBySpace { + let spaceScrollDownKeyboardHandler:(NSEvent)->KeyHandlerResult = { [weak self, weak page] _ in + if let window = self?._window { + if !window.styleMask.contains(.fullScreen) { + page?.scrollPage(direction: .down) + } + } + + return .invoked + } + _window.set(handler: spaceScrollDownKeyboardHandler, with: self, for: .Space, priority: .low) + + let spaceScrollUpKeyboardHandler:(NSEvent)->KeyHandlerResult = { [weak self, weak page] _ in + if let window = self?._window { + if !window.styleMask.contains(.fullScreen) { + page?.scrollPage(direction: .up) + } + } + + return .invoked + } + + _window.set(handler: spaceScrollUpKeyboardHandler, with: self, for: .Space, priority: .medium, modifierFlags: [.shift]) + } else { + _window.set(handler: closeKeyboardHandler, with: self, for: .Space) + } if let titleView = titleView { @@ -321,6 +378,22 @@ class InstantViewController : TelegramGenericViewController, animated: Bool = true) { + loadProgressDisposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] value in + guard let `self` = self else {return} + self.genericView.loadingIndicatorView.set(progress: value, animated: animated, duration: 0.2) + if value == 1 || value == 0 { + self.genericView.loadingIndicatorView.change(opacity: 0, animated: animated, completion: { [weak self] completed in + if completed { + self?.genericView.loadingIndicatorView.set(progress: 0, animated: false) + } + }) + } else if value > 0 { + self.genericView.loadingIndicatorView.change(opacity: 1, animated: animated) + } + })) + } private func openInSafari() { @@ -350,7 +423,7 @@ class InstantViewController : TelegramGenericViewController take(1) |> filter {$0} |> deliverOnMainQueue |> map { [weak self] value in - self?._window.makeKeyAndOrderFront(nil) - + guard let `self` = self else {return value} + + // let toRect = self._window.frame + // let fromRect = NSMakeRect(toRect.minX + (toRect.width - 100) / 2, toRect.minY + (toRect.height - 100) / 2, 100, 100) + //self._window.setFrame(fromRect, display: true) + self._window.makeKeyAndOrderFront(nil) + + // NSLog("\(self._window.contentView?.superview?.layer)") + // self._window.contentView?.superview?.layer?.animateScaleCenter(from: 0, to: 1, duration: 0.4) + + //self._window.setFrame(toRect, display: false, animate: true) + // self._window.contentView?.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + return value }) } @@ -422,12 +509,19 @@ class InstantViewController : TelegramGenericViewController CGImage { + return generateImage(NSMakeSize(50 / System.backingScale, 50 / System.backingScale), contextGenerator: { size, ctx in + let rect: NSRect = .init(origin: .zero, size: size) + ctx.clear(rect) + + ctx.setFillColor(color.cgColor) + ctx.fillEllipse(in: rect) + + let image = NSImage(named: "Icon_ChatActionsActive")!.precomposed() + + ctx.clip(to: rect, mask: image) + ctx.clear(rect) + + + }, scale: System.backingScale)! +} + +private let linkIcon: CGImage = NSImage(named: "Icon_ExportedInvitation_Link")!.precomposed(.white) + + + + +class InviteLinkRowItem: GeneralRowItem { + private let _menuItems:(ExportedInvitation)->Signal<[ContextMenuItem], NoError> + + private(set) fileprivate var frames:[NSRect] = [] + let link:ExportedInvitation + fileprivate let _action:(ExportedInvitation)->Void + init(_ initialSize: NSSize, stableId: AnyHashable, viewType: GeneralViewType, link:ExportedInvitation, action: @escaping(ExportedInvitation)->Void, menuItems:@escaping(ExportedInvitation)->Signal<[ContextMenuItem], NoError>) { + self._menuItems = menuItems + self.link = link + self._action = action + super.init(initialSize, height: 54, stableId: stableId, viewType: viewType) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + + + return true + } + + var innerBlockSize: NSSize { + return NSMakeSize(blockWidth - viewType.innerInset.left - viewType.innerInset.right, height - viewType.innerInset.bottom - viewType.innerInset.top) + } + + override func menuItems(in location: NSPoint) -> Signal<[ContextMenuItem], NoError> { + return _menuItems(link) + } + override func viewClass() -> AnyClass { + return InviteLinkRowView.self + } +} +private final class ProgressView : View { + + private let circle: View = View(frame: NSMakeRect(0, 0, 35, 35)) + private let progressView: FireTimerControl = FireTimerControl(frame: NSMakeRect(0, 0, 50, 50)) + private let imageView = ImageView() + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(circle) + addSubview(progressView) + addSubview(imageView) + + circle.layer?.cornerRadius = circle.frame.height / 2 + + circle.isEventLess = true + imageView.isEventLess = true + progressView.isEventLess = true + } + + override func layout() { + super.layout() + progressView.center() + circle.center() + imageView.center() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(link: ExportedInvitation) { + self.imageView.image = linkIcon + self.imageView.sizeToFit() + + let color:(NSColor, NSColor, CGFloat) -> NSColor = { from, to, progress in + let newRed = (1.0 - progress) * from.redComponent + progress * to.redComponent + let newGreen = (1.0 - progress) * from.greenComponent + progress * to.greenComponent + let newBlue = (1.0 - progress) * from.blueComponent + progress * to.blueComponent + let newAlpha = (1.0 - progress) * from.alphaComponent + progress * to.alphaComponent + + return NSColor(deviceRed: newRed, green: newGreen, blue: newBlue, alpha: newAlpha) + } + + let updateBackgroundColor: ()->NSColor = { [weak self] in + guard let `self` = self else { + return .white + } + let backgroundColor: NSColor + if link.isRevoked { + backgroundColor = theme.colors.grayIcon.darker() + } else if let expireDate = link.expireDate { + + let timeout = expireDate - (link.startDate ?? link.date) + + let current = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + + let from: NSColor? + let to: NSColor + + var progress: CGFloat = 1 + + if link.isExpired || link.isLimitReached { + to = theme.colors.redUI.darker() + from = nil + } else { + let dif = expireDate - current + progress = 1 - (CGFloat(dif) / CGFloat(timeout)) + + if progress <= 0.5 { + progress /= 0.5 + to = theme.colors.peerAvatarOrangeBottom + from = theme.colors.greenUI.darker() + } else { + progress = (progress - 0.5) / 0.5 + to = theme.colors.redUI.darker() + from = theme.colors.peerAvatarOrangeBottom + } + } + if let from = from { + backgroundColor = color(from, to, progress) + } else { + backgroundColor = to + } + + } else { + backgroundColor = theme.colors.accent.lighter() + } + self.circle.backgroundColor = backgroundColor + self.progressView.updateColor(backgroundColor) + + return backgroundColor + } + + if let expiryDate = link.expireDate, !link.isExpired && !link.isRevoked { + let startDate = link.startDate ?? link.date + let timeout = expiryDate - startDate + progressView.update(color: updateBackgroundColor(), timeout: timeout, deadlineTimestamp: expiryDate) + + progressView.reachedTimeout = { + _ = updateBackgroundColor() + } + progressView.reachedHalf = { + _ = updateBackgroundColor() + } + + progressView.updateValue = { value in + _ = updateBackgroundColor() + } + progressView.isHidden = false + } else { + progressView.isHidden = true + } + _ = updateBackgroundColor() + } +} + +private final class InviteLinkTokenView : Control { + private let actions = ImageButton() + private let titleView = TextView() + private let countView = TextView() + private let progressView = ProgressView(frame: NSMakeRect(0, 0, 50, 50)) + private var action:(()->Void)? + private var timer: SwiftSignalKit.Timer? + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(actions) + addSubview(titleView) + addSubview(countView) + addSubview(progressView) + layer?.cornerRadius = 10 + titleView.userInteractionEnabled = false + titleView.isSelectable = false + countView.userInteractionEnabled = false + countView.isSelectable = false + progressView.isEventLess = true + scaleOnClick = true + progressView.userInteractionEnabled = false + progressView.isEventLess = true + set(handler: { [weak self] _ in + self?.action?() + }, for: .Click) + } + + private var actionsPoint: NSPoint { + return NSMakePoint(frame.width - actions.frame.width - 15, focus(actions.frame.size).minY) + } + private var progressPoint: NSPoint { + return NSMakePoint(7, focus(progressView.frame.size).minY) + } + private var titlePoint: NSPoint { + return NSMakePoint(65, 8) + } + private var countPoint: NSPoint { + return NSMakePoint(65, frame.height - 8 - countView.frame.height) + } + + + override func layout() { + super.layout() + actions.setFrameOrigin(actionsPoint) + progressView.setFrameOrigin(progressPoint) + titleView.setFrameOrigin(titlePoint) + countView.setFrameOrigin(countPoint) + } + + func update(with link: ExportedInvitation, frame: NSRect, animated: Bool, showContextMenu:@escaping()->Void, action: @escaping()->Void) { + + self.action = action + + actions.set(image: generate(theme.colors.grayIcon), for: .Normal) + actions.style = ControlStyle(highlightColor: theme.colors.grayIcon.highlighted) + actions.sizeToFit() + + + actions.change(pos: actionsPoint, animated: animated) + progressView.change(pos: progressPoint, animated: animated) + + + + let updateText:()->Void = { [weak self] in + guard let `self` = self else { + return + } + + self.progressView.update(link: link) + + let titleText = link.link.replacingOccurrences(of: "https://", with: "") + + let titleAttr = NSMutableAttributedString() + _ = titleAttr.append(string: titleText, color: theme.colors.text, font: .medium(.text)) + let titleLayout = TextViewLayout(titleAttr, maximumNumberOfLines: 1) + titleLayout.measure(width: frame.width - 110) + + self.titleView.update(titleLayout) + + + var text: String = "" + if link.isRevoked, link.count == nil || link.count == 0 { + text = L10n.inviteLinkJoinedRevoked + } else { + if let count = link.count { + text = L10n.inviteLinkJoinedCountable(Int(count)) + text = text.replacingOccurrences(of: "\(count)", with: Int(count).prettyNumber) + } else if let usageLimit = link.usageLimit { + if link.isExpired { + text = L10n.inviteLinkJoinedRevoked + } else { + text = L10n.inviteLinkCanJoinCountable(Int(usageLimit)) + } + } else { + text = L10n.inviteLinkJoinedZero + } + } + + var countText = text + + if link.isRevoked { + countText += " " + L10n.inviteLinkStickerRevoked + } else { + if let usageLink = link.usageLimit, let count = link.count { + if !link.isLimitReached { + countText += " " + L10n.inviteLinkRemainingFew(Int(usageLink - count)) + } else if link.isLimitReached { + countText += " " + L10n.inviteLinkRemainingFew(Int(usageLink - count)) + } + } + + if link.isExpired { + countText += " " + L10n.inviteLinkStickerExpired + } else if let expireDate = link.expireDate { + let left = Int(expireDate) - Int(Date().timeIntervalSince1970) + if left <= Int(Int32.secondsInDay) { + let minutes = left / 60 % 60 + let seconds = left % 60 + let hours = left / 60 / 60 + let string = String(format: "%@:%@:%@", hours < 10 ? "0\(hours)" : "\(hours)", minutes < 10 ? "0\(minutes)" : "\(minutes)", seconds < 10 ? "0\(seconds)" : "\(seconds)") + countText += " • " + L10n.inviteLinkStickerTimeLeft(string) + } else { + countText += " • " + L10n.inviteLinkStickerTimeLeft(autoremoveLocalized(left, roundToCeil: true)) + } + } + } + + let countLayout = TextViewLayout(.initialize(string: countText, color: theme.colors.text, font: .normal(.short)), maximumNumberOfLines: 2) + countLayout.measure(width: frame.width - 20) + + self.countView.update(countLayout) + + self.titleView.change(pos: self.titlePoint, animated: animated) + self.countView.change(pos: self.countPoint, animated: animated) + } + + if !link.isRevoked && !link.isExpired { + self.timer = SwiftSignalKit.Timer.init(timeout: 0.5, repeat: true, completion: updateText, queue: .mainQueue()) + self.timer?.start() + } else { + self.timer?.invalidate() + self.timer = nil + } + + updateText() + + actions.removeAllHandlers() + actions.set(handler: { _ in + showContextMenu() + }, for: .Click) + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private final class InviteLinkRowView : GeneralContainableRowView { + private let contentView: InviteLinkTokenView = InviteLinkTokenView(frame: .zero) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(contentView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + contentView.frame = containerView.bounds + } + + override func updateColors() { + super.updateColors() + } + + + override func onShowContextMenu() { + super.onShowContextMenu() + } + override func onCloseContextMenu() { + super.onCloseContextMenu() + } + + override func convertWindowPointToContent(_ point: NSPoint) -> NSPoint { + return self.contentView.convert(point, from: nil) + } + + override var additionBorderInset: CGFloat { + return 42 + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? InviteLinkRowItem else { + return + } + + layout() + + + + contentView.update(with: item.link, frame: containerView.bounds, animated: animated, showContextMenu: { [weak self] in + if let event = NSApp.currentEvent { + self?.showContextMenu(event) + } + }, action: { [weak item] in + if let link = item?.link { + item?._action(link) + } + }) + } +} diff --git a/Telegram-Mac/InviteLinksController.swift b/Telegram-Mac/InviteLinksController.swift new file mode 100644 index 0000000000..c0f570ae3b --- /dev/null +++ b/Telegram-Mac/InviteLinksController.swift @@ -0,0 +1,727 @@ +// +// InviteLinksController.swift +// Telegram +// +// Created by Mikhail Filimonov on 14.01.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit + +import TelegramCore +import Postbox +import TelegramApi + + + + + +extension ExportedInvitation { + var isExpired: Bool { + if let expiryDate = expireDate { + if expiryDate < Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { + return true + } + } + return false + } + var isLimitReached: Bool { + if let usageLimit = usageLimit, let count = count { + if usageLimit == count { + return true + } + } + return false + } + + func withUpdatedIsRevoked(_ isRevoked: Bool) -> ExportedInvitation { + return ExportedInvitation(link: self.link, isPermanent: self.isPermanent, isRevoked: isRevoked, adminId: self.adminId, date: self.date, startDate: self.startDate, expireDate: self.expireDate, usageLimit: self.usageLimit, count: self.count) + } +} + +final class InviteLinkPeerManager { + + struct State : Equatable { + + var list: [ExportedInvitation]? + var next: ExportedInvitation? + var creators:[ExportedInvitationCreator]? + var totalCount: Int32 + var activeLoaded: Bool + var revokedList: [ExportedInvitation]? + var nextRevoked: ExportedInvitation? + var totalRevokedCount: Int32 + var revokedLoaded: Bool + var effectiveCount: Int32 { + return totalCount + (self.creators?.reduce(0, { current, value in + return current + value.count + }) ?? 0) + } + + static var `default`: State { + return State(list: nil, next: nil, creators: nil, totalCount: 0, activeLoaded: false, revokedList: nil, nextRevoked: nil, totalRevokedCount: 0, revokedLoaded: false) + } + } + + let context: AccountContext + let peerId: PeerId + let adminId: PeerId? + private let listDisposable = DisposableDict() + private let loadCreatorsDisposable = MetaDisposable() + private let stateValue: Atomic = Atomic(value: State.default) + private let statePromise = ValuePromise(State.default, ignoreRepeated: true) + private func updateState(_ f: (State) -> State) { + statePromise.set(stateValue.modify (f)) + } + + var state: Signal { + return self.statePromise.get() + } + + deinit { + listDisposable.dispose() + loadCreatorsDisposable.dispose() + updateAdminsDisposable.dispose() + } + + private let updateAdminsDisposable = MetaDisposable() + init(context: AccountContext, peerId: PeerId, adminId: PeerId? = nil) { + self.context = context + self.peerId = peerId + self.adminId = adminId + self.loadNext() + self.loadNext(true) + if adminId == nil { + self.loadCreators() + let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.admins(peerId: peerId, updated: { [weak self] _ in + self?.loadCreators() + }) + updateAdminsDisposable.set(disposable) + } + + } + + func createPeerExportedInvitation(expireDate: Int32?, usageLimit: Int32?) -> Signal { + let context = self.context + let peerId = self.peerId + return Signal { [weak self] subscriber in + let signal = context.engine.peers.createPeerExportedInvitation(peerId: peerId, expireDate: expireDate, usageLimit: usageLimit) |> deliverOnMainQueue + let disposable = signal.start(next: { [weak self] value in + self?.updateState { state in + var state = state + state.list = state.list ?? [] + if let value = value { + state.list?.insert(value, at: 0) + state.totalCount += 1 + } + return state + } + subscriber.putCompletion() + }) + return disposable + } + } + + func editPeerExportedInvitation(link: ExportedInvitation, expireDate: Int32?, usageLimit: Int32?) -> Signal { + let context = self.context + let peerId = self.peerId + return Signal { [weak self] subscriber in + let signal = context.engine.peers.editPeerExportedInvitation(peerId: peerId, link: link.link, expireDate: expireDate, usageLimit: usageLimit) + let disposable = signal.start(next: { [weak self] value in + self?.updateState { state in + var state = state + state.list = state.list ?? [] + if let value = value, let index = state.list?.firstIndex(where: { $0.link == value.link }) { + state.list?[index] = value + } + return state + } + subscriber.putCompletion() + }, error: { error in + subscriber.putError(error) + }) + return disposable + } + } + + func revokePeerExportedInvitation(link: ExportedInvitation) -> Signal { + let context = self.context + let peerId = self.peerId + return Signal { [weak self] subscriber in + + let signal: Signal + signal = context.engine.peers.revokePeerExportedInvitation(peerId: peerId, link: link.link) + let disposable = signal.start(next: { [weak self] value in + self?.updateState { state in + var state = state + state.list = state.list ?? [] + if let value = value { + switch value { + case let .update(link): + state.revokedList = state.revokedList ?? [] + state.list!.removeAll(where: { $0.link == link.link}) + state.revokedList?.append(link) + state.revokedList?.sort(by: { $0.date < $1.date }) + state.totalCount -= 1 + case let .replace(link, new): + let link = link.withUpdatedIsRevoked(true) + state.revokedList = state.revokedList ?? [] + state.list!.removeAll(where: { $0.link == link.link}) + state.list!.insert(new, at: 0) + state.revokedList?.insert(link, at: 0) + state.revokedList?.sort(by: { $0.date > $1.date }) + } + + } + + return state + } + subscriber.putCompletion() + }, error: { error in + subscriber.putError(error) + }) + return disposable + } + } + + func deletePeerExportedInvitation(link: ExportedInvitation) -> Signal { + let context = self.context + let peerId = self.peerId + return Signal { [weak self] subscriber in + let signal = context.engine.peers.deletePeerExportedInvitation(peerId: peerId, link: link.link) + let disposable = signal.start(error: { error in + subscriber.putError(error) + }, completed: { [weak self] in + self?.updateState { state in + var state = state + state.revokedList = state.revokedList ?? [] + state.revokedList?.removeAll(where: { $0.link == link.link }) + state.totalRevokedCount -= 1 + return state + } + subscriber.putCompletion() + }) + return disposable + } + } + + func deleteAllRevokedPeerExportedInvitations() -> Signal { + let context = self.context + let peerId = self.peerId + return Signal { [weak self] subscriber in + let signal = context.engine.peers.deleteAllRevokedPeerExportedInvitations(peerId: peerId, adminId: self?.adminId ?? context.peerId) + let disposable = signal.start(completed: { + self?.updateState { state in + var state = state + state.revokedList?.removeAll() + state.totalRevokedCount = 0 + state.nextRevoked = nil + state.revokedLoaded = true + return state + } + subscriber.putCompletion() + }) + return disposable + } + } + + func loadCreators() { + let signal = context.engine.peers.peerExportedInvitationsCreators(peerId: peerId) |> deliverOnMainQueue + loadCreatorsDisposable.set(signal.start(next: { [weak self] creators in + self?.updateState { state in + var state = state + state.creators = creators + return state + } + })) + } + + + func loadNext(_ forceLoadRevoked: Bool = false) { + + let revoked = forceLoadRevoked ? true : stateValue.with { $0.activeLoaded } + + if stateValue.with({ revoked ? !$0.revokedLoaded : !$0.activeLoaded }) { + let offsetLink: ExportedInvitation? = stateValue.with { state in + if revoked { + return state.nextRevoked + } else { + return state.next + } + } + + let signal = context.engine.peers.direct_peerExportedInvitations(peerId: peerId, revoked: revoked, adminId: self.adminId, offsetLink: offsetLink) |> deliverOnMainQueue + self.listDisposable.set(signal.start(next: { [weak self] list in + self?.updateState { state in + var state = state + if revoked { + state.revokedList = (state.revokedList ?? []) + (list?.list ?? []) + state.totalRevokedCount = list?.totalCount ?? 0 + state.revokedLoaded = state.revokedList?.count == Int(state.totalRevokedCount) + state.nextRevoked = state.revokedList?.last + } else { + state.list = (state.list ?? []) + (list?.list ?? []) + state.totalCount = list?.totalCount ?? 0 + state.activeLoaded = state.list?.count == Int(state.totalCount) + state.next = state.list?.last + } + return state + } + }), forKey: revoked) + } + + } + + private var cachedImporters:[String : PeerInvitationImportersContext] = [:] + + func importer(for link: ExportedInvitation) -> PeerInvitationImportersContext { + let cached = self.cachedImporters[link.link] + + if let cached = cached { + return cached + } else { + let value = context.engine.peers.peerInvitationImporters(peerId: peerId, invite: link) + cachedImporters[link.link] = value + return value + } + } +} + + +private final class InviteLinksArguments { + let context: AccountContext + let shareLink: (String)->Void + let copyLink: (String)->Void + let revokeLink: (ExportedInvitation)->Void + let editLink:(ExportedInvitation)->Void + let deleteLink:(ExportedInvitation)->Void + let deleteAll:()->Void + let newLink:()->Void + let open:(ExportedInvitation)->Void + let openAdminLinks:(ExportedInvitationCreator)->Void + init(context: AccountContext, shareLink: @escaping(String)->Void, copyLink: @escaping(String)->Void, revokeLink: @escaping(ExportedInvitation)->Void, editLink:@escaping(ExportedInvitation)->Void, newLink:@escaping()->Void, deleteLink:@escaping(ExportedInvitation)->Void, deleteAll:@escaping()->Void, open:@escaping(ExportedInvitation)->Void, openAdminLinks: @escaping(ExportedInvitationCreator)->Void) { + self.context = context + self.shareLink = shareLink + self.copyLink = copyLink + self.revokeLink = revokeLink + self.editLink = editLink + self.newLink = newLink + self.deleteLink = deleteLink + self.deleteAll = deleteAll + self.open = open + self.openAdminLinks = openAdminLinks + } +} + +private struct InviteLinksState : Equatable { + var permanent: ExportedInvitation? + var permanentImporterState: PeerInvitationImportersState? + var list: [ExportedInvitation]? + var revokedList: [ExportedInvitation]? + var creators:[ExportedInvitationCreator]? + var isAdmin: Bool + var totalCount: Int + var peer: PeerEquatable? + var adminPeer: PeerEquatable? +} + +private let _id_header = InputDataIdentifier("_id_header") +private let _id_permanent = InputDataIdentifier("_id_permanent") +private let _id_add_link = InputDataIdentifier("_id_add_link") +private let _id_loading = InputDataIdentifier("_id_loading") +private let _id_delete_all = InputDataIdentifier("_id_delete_all") +private func _id_links(_ links:[ExportedInvitation]) -> InputDataIdentifier { + return InputDataIdentifier("active_" + links.reduce("", { current, value in + return current + value.link + })) +} +private func _id_links_revoked(_ links:[ExportedInvitation]) -> InputDataIdentifier { + return InputDataIdentifier("revoked_" + links.reduce("", { current, value in + return current + value.link + })) +} +private func _id_creator(_ peerId: PeerId) -> InputDataIdentifier { + return InputDataIdentifier("_id_creator_\(peerId.toInt64())") +} + +private func entries(_ state: InviteLinksState, arguments: InviteLinksArguments) -> [InputDataEntry] { + + var entries: [InputDataEntry] = [] + + var sectionId: Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + if !state.isAdmin { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_header, equatable: nil, comparable: nil, item: { initialSize, stableId in + let text:String = state.peer?.peer.isChannel == true ? L10n.manageLinksHeaderChannelDesc : L10n.manageLinksHeaderGroupDesc + return AnimtedStickerHeaderItem(initialSize, stableId: stableId, context: arguments.context, sticker: LocalAnimatedSticker.invitations, text: .initialize(string: text, color: theme.colors.listGrayText, font: .normal(.text))) + })) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + } + + // if !state.isAdmin || state.peer?.peer.addressName == nil { + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.manageLinksInviteLink), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + var peers = state.permanentImporterState?.importers.map { $0.peer } ?? [] + peers = Array(peers.prefix(3)) + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_permanent, equatable: InputDataEquatable(state), comparable: nil, item: { initialSize, stableId in + return ExportedInvitationRowItem(initialSize, stableId: stableId, context: arguments.context, exportedLink: state.permanent, publicAddress: state.isAdmin ? nil : state.peer?.peer.addressName, lastPeers: peers, viewType: .singleItem, menuItems: { + + var items:[ContextMenuItem] = [] + if let permanent = state.permanent { + items.append(ContextMenuItem(L10n.manageLinksContextCopy, handler: { + arguments.copyLink(permanent.link) + })) + if state.adminPeer?.peer.isBot == true { + + } else { + items.append(ContextMenuItem(L10n.manageLinksContextRevoke, handler: { + arguments.revokeLink(permanent) + })) + } + + } else if let addressName = state.peer?.peer.addressName { + items.append(ContextMenuItem(L10n.manageLinksContextCopy, handler: { + arguments.copyLink(addressName) + })) + } + + return .single(items) + }, share: arguments.shareLink, open: arguments.open, copyLink: arguments.copyLink) + })) + + if state.isAdmin, let peer = state.peer, let adminPeer = state.adminPeer { + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.manageLinksAdminPermanentDesc(adminPeer.peer.displayTitle, peer.peer.displayTitle)), data: .init(color: theme.colors.listGrayText, viewType: .textBottomItem))) + index += 1 + } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + // } + + + + if !state.isAdmin || (state.list != nil && !state.list!.isEmpty) { + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.manageLinksAdditionLinks), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + } + + struct Tuple : Equatable { + let link:ExportedInvitation + let viewType: GeneralViewType + } + + let viewType: GeneralViewType = state.list == nil || !state.list!.isEmpty ? .firstItem : .singleItem + if !state.isAdmin { + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_add_link, equatable: InputDataEquatable(viewType), comparable: nil, item: { initialSize, stableId in + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.manageLinksCreateNew, nameStyle: blueActionButton, type: .none, viewType: viewType, action: arguments.newLink, drawCustomSeparator: true, thumb: GeneralThumbAdditional(thumb: theme.icons.proxyAddProxy, textInset: 43, thumbInset: 0)) + })) + index += 1 + + +// entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_add_link, data: .init(name: L10n.manageLinksCreateNew, color: theme.colors.accent, icon: theme.icons.proxyAddProxy, type: .none, viewType: viewType, enabled: true, action: arguments.newLink))) +// index += 1 + } + if let list = state.list { + if !list.isEmpty { + for (i, link) in list.enumerated() { + var viewType: GeneralViewType = bestGeneralViewType(list, for: i) + if i == 0, !state.isAdmin { + if list.count == 1 { + viewType = .lastItem + } else { + viewType = .innerItem + } + } + + let tuple = Tuple(link: link, viewType: viewType) + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_links([link]), equatable: InputDataEquatable(tuple), comparable: nil, item: { initialSize, stableId in + return InviteLinkRowItem(initialSize, stableId: stableId, viewType: tuple.viewType, link: tuple.link, action: arguments.open, menuItems: { link in + + var items:[ContextMenuItem] = [] + items.append(ContextMenuItem(L10n.manageLinksContextCopy, handler: { + arguments.copyLink(link.link) + })) + if !link.isRevoked { + if !link.isExpired { + items.append(ContextMenuItem(L10n.manageLinksContextShare, handler: { + arguments.shareLink(link.link) + })) + } + if !link.isPermanent { + items.append(ContextMenuItem(L10n.manageLinksContextEdit, handler: { + arguments.editLink(link) + })) + } + + if state.adminPeer?.peer.isBot == true { + + } else { + items.append(ContextMenuItem(L10n.manageLinksContextRevoke, handler: { + arguments.revokeLink(link) + })) + } + } + + return .single(items) + }) + })) + index += 1 + } + } else { + if !state.isAdmin { + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.manageLinksEmptyDesc), data: .init(color: theme.colors.listGrayText, viewType: .textBottomItem))) + index += 1 + } + } + + + if let list = state.revokedList, list.count > 0 { + + if state.list?.isEmpty == false || !state.isAdmin { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + } + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.manageLinksRevokedLinks), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + // if !state.isAdmin { + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_delete_all, data: .init(name: L10n.manageLinksDeleteAll, color: theme.colors.redUI, icon: nil, type: .none, viewType: .firstItem, enabled: true, action: arguments.deleteAll))) + index += 1 + // } + + + for (i, link) in list.enumerated() { + + var viewType: GeneralViewType = bestGeneralViewType(list, for: i) + if i == 0 { + if list.count == 1 { + viewType = .lastItem + } else { + viewType = .innerItem + } + } + + let tuple = Tuple(link: link, viewType: viewType) + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_links_revoked([link]), equatable: InputDataEquatable(tuple), comparable: nil, item: { initialSize, stableId in + return InviteLinkRowItem(initialSize, stableId: stableId, viewType: tuple.viewType, link: tuple.link, action: arguments.open, menuItems: { link in + var items:[ContextMenuItem] = [] + items.append(ContextMenuItem(L10n.manageLinksDelete, handler: { + arguments.deleteLink(link) + })) + return .single(items) + }) + })) + index += 1 + } + + } + + } else { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_loading, equatable: nil, comparable: nil, item: { initialSize, stableId in + return GeneralLoadingRowItem(initialSize, stableId: stableId, viewType: !state.isAdmin ? .lastItem : .singleItem) + })) + index += 1 + } + + + + if let creators = state.creators, !creators.isEmpty { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.manageLinksOtherAdmins), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + let creators = creators.filter { $0.peer.peer != nil } + for (i, creator) in creators.enumerated() { + + let viewType = bestGeneralViewType(creators, for: i) + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_creator(creator.peer.peerId), equatable: InputDataEquatable(creator), comparable: nil, item: { initialSize, stableId in + return ShortPeerRowItem(initialSize, peer: creator.peer.peer!, account: arguments.context.account, stableId: stableId, height: 50, photoSize: NSMakeSize(36, 36), status: L10n.manageLinksTitleCountCountable(Int(creator.count)), inset: NSEdgeInsets(left: 30, right: 30), viewType: viewType, action: { + arguments.openAdminLinks(creator) + }) + })) + } + } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func InviteLinksController(context: AccountContext, peerId: PeerId, manager: InviteLinkPeerManager?) -> InputDataController { + + + let initialState = InviteLinksState(permanent: nil, permanentImporterState: nil, list: nil, creators: nil, isAdmin: manager?.adminId != nil, totalCount: 0) + + let statePromise = ValuePromise(ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((InviteLinksState) -> InviteLinksState) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + let manager = manager ?? InviteLinkPeerManager(context: context, peerId: peerId) + var getController:(()->ViewController?)? = nil + + let arguments = InviteLinksArguments(context: context, shareLink: { link in + showModal(with: ShareModalController(ShareLinkObject(context, link: link)), for: context.window) + }, copyLink: { link in + getController?()?.show(toaster: ControllerToaster(text: L10n.shareLinkCopied)) + copyToClipboard(link) + }, revokeLink: { [weak manager] link in + confirm(for: context.window, header: L10n.channelRevokeLinkConfirmHeader, information: L10n.channelRevokeLinkConfirmText, okTitle: L10n.channelRevokeLinkConfirmOK, cancelTitle: L10n.modalCancel, successHandler: { _ in + if let manager = manager { + _ = showModalProgress(signal: manager.revokePeerExportedInvitation(link: link), for: context.window).start() + } + }) + }, editLink: { [weak manager] link in + showModal(with: ClosureInviteLinkController(context: context, peerId: peerId, mode: .edit(link), save: { [weak manager] updated in + let signal = manager?.editPeerExportedInvitation(link: link, expireDate: updated.date == .max ? nil : updated.date + Int32(Date().timeIntervalSince1970), usageLimit: updated.count == .max ? nil : updated.count) + if let signal = signal { + _ = showModalProgress(signal: signal, for: context.window).start() + } + }), for: context.window) + }, newLink: { [weak manager] in + showModal(with: ClosureInviteLinkController(context: context, peerId: peerId, mode: .new, save: { [weak manager] link in + let signal = manager?.createPeerExportedInvitation(expireDate: link.date == .max ? nil : link.date + Int32(Date().timeIntervalSince1970), usageLimit: link.count == .max ? nil : link.count) + if let signal = signal { + _ = showModalProgress(signal: signal, for: context.window).start() + } + }), for: context.window) + }, deleteLink: { [weak manager] link in + if let manager = manager { + _ = showModalProgress(signal: manager.deletePeerExportedInvitation(link: link), for: context.window).start() + } + }, deleteAll: { [weak manager] in + confirm(for: context.window, header: L10n.manageLinksDeleteAll, information: L10n.manageLinksDeleteAllConfirm, okTitle: L10n.manageLinksDeleteAll, cancelTitle: L10n.modalCancel, successHandler: { [weak manager] _ in + if let manager = manager { + _ = showModalProgress(signal: manager.deleteAllRevokedPeerExportedInvitations(), for: context.window).start() + } + }) + + }, open: { [weak manager] invitation in + if let manager = manager { + showModal(with: ExportedInvitationController(invitation: invitation, peerId: peerId, accountContext: context, manager: manager, context: manager.importer(for: invitation)), for: context.window) + } + }, openAdminLinks: { creator in + let manager = InviteLinkPeerManager(context: context, peerId: peerId, adminId: creator.peer.peerId) + getController?()?.navigationController?.push(InviteLinksController(context: context, peerId: peerId, manager: manager)) + }) + + let actionsDisposable = DisposableSet() + + + let importers: Signal<(PeerInvitationImportersState?, InviteLinkPeerManager.State), NoError> = manager.state |> deliverOnMainQueue |> mapToSignal { [weak manager] state in + if let permanent = state.list?.first(where: { $0.isPermanent }) { + if let importer = manager?.importer(for: permanent).state { + return importer |> map(Optional.init) |> map { ($0, state) } + } else { + return .single((nil, state)) + } + } else { + return .single((nil, state)) + } + } + + var peers: [Signal] = [] + + peers.append(context.account.postbox.loadedPeerWithId(peerId) |> map { PeerEquatable($0) }) + + if let adminId = manager.adminId { + peers.append(context.account.postbox.loadedPeerWithId(adminId) |> map { PeerEquatable($0) }) + } + + actionsDisposable.add(combineLatest(manager.state, importers, combineLatest(peers)).start(next: { state, permanentImporterState, peers in + updateState { current in + var current = current + current.peer = peers.first + if peers.count == 2 { + current.adminPeer = peers.last + } + if current.peer?.peer.addressName != nil && !current.isAdmin { + current.permanent = nil + } else { + current.permanent = state.list?.first(where: { $0.isPermanent }) + } + current.permanentImporterState = permanentImporterState.0 + current.list = state.list?.filter({ $0.link != current.permanent?.link }) + current.revokedList = state.revokedList + current.creators = state.creators + current.totalCount = Int(state.totalCount) + + + + return current + } + })) + + let signal = statePromise.get() |> map { + return InputDataSignalValue(entries: entries($0, arguments: arguments), animated: true) + } + + let controller = InputDataController(dataSignal: signal, title: L10n.manageLinksTitleNew, removeAfterDisappear: false, hasDone: false) + + controller.onDeinit = { + actionsDisposable.dispose() + } + + + controller.getTitle = { + let peer = stateValue.with { $0.adminPeer?.peer } + if let peer = peer { + return peer.displayTitle + } else { + return L10n.manageLinksTitleNew + } + } + controller.getStatus = { + let isAdmin = stateValue.with { $0.isAdmin } + if isAdmin { + return L10n.manageLinksTitleCountCountable(stateValue.with { $0.totalCount }) + } else { + return nil + } + } + + controller.didLoaded = { [weak manager] controller, _ in + controller.tableView.setScrollHandler { position in + switch position.direction { + case .bottom: + manager?.loadNext() + default: + break + } + } + } + + controller.afterTransaction = { controller in + controller.requestUpdateCenterBar() + } + + controller.contextOject = manager + + + getController = { [weak controller] in + return controller + } + + return controller + +} diff --git a/Telegram-Mac/IsEqualMessages.swift b/Telegram-Mac/IsEqualMessages.swift new file mode 100644 index 0000000000..e999684738 --- /dev/null +++ b/Telegram-Mac/IsEqualMessages.swift @@ -0,0 +1,98 @@ +// +// IsEqualMessages.swift +// Telegram +// +// Created by Mikhail Filimonov on 02.11.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import TelegramCore + +import Postbox + + +func isEqualMessages(_ lhsMessage: Message, _ rhsMessage: Message) -> Bool { + + + if MessageIndex(id: lhsMessage.id, timestamp: lhsMessage.timestamp) != MessageIndex(id: rhsMessage.id, timestamp: rhsMessage.timestamp) || lhsMessage.stableVersion != rhsMessage.stableVersion { + return false + } + if lhsMessage.flags != rhsMessage.flags { + return false + } + + if lhsMessage.media.count != rhsMessage.media.count { + return false + } + for i in 0 ..< lhsMessage.media.count { + if !lhsMessage.media[i].isEqual(to: rhsMessage.media[i]) { + return false + } + } + + if lhsMessage.attributes.count != rhsMessage.attributes.count { + return false + } + + for (_, lhsAttr) in lhsMessage.attributes.enumerated() { + if let lhsAttr = lhsAttr as? ReplyThreadMessageAttribute { + let rhsAttr = rhsMessage.attributes.compactMap { $0 as? ReplyThreadMessageAttribute }.first + if let rhsAttr = rhsAttr { + if lhsAttr.count != rhsAttr.count { + return false + } + if lhsAttr.latestUsers != rhsAttr.latestUsers { + return false + } + if lhsAttr.maxMessageId != rhsAttr.maxMessageId { + return false + } + if lhsAttr.maxReadMessageId != rhsAttr.maxReadMessageId { + return false + } + } else { + return false + } + } + if let lhsAttr = lhsAttr as? ViewCountMessageAttribute { + let rhsAttr = rhsMessage.attributes.compactMap { $0 as? ViewCountMessageAttribute }.first + if let rhsAttr = rhsAttr { + if lhsAttr.count != rhsAttr.count { + return false + } + } else { + return false + } + } + } + + if lhsMessage.associatedMessages.count != rhsMessage.associatedMessages.count { + return false + } else { + for (messageId, lhsAssociatedMessage) in lhsMessage.associatedMessages { + if let rhsAssociatedMessage = rhsMessage.associatedMessages[messageId] { + if lhsAssociatedMessage.stableVersion != rhsAssociatedMessage.stableVersion { + return false + } + } else { + return false + } + } + } + + if lhsMessage.peers.count != rhsMessage.peers.count { + return false + } else { + for (lhsPeerId, lhsPeer) in lhsMessage.peers { + if let rhsPeer = rhsMessage.peers[lhsPeerId] { + if rhsPeer.displayTitle != lhsPeer.displayTitle { + return false + } + } else { + return false + } + } + } + + return true +} diff --git a/Telegram-Mac/JoinLinkPreviewModalController.swift b/Telegram-Mac/JoinLinkPreviewModalController.swift index f5b901fdfc..ea0df5ad53 100644 --- a/Telegram-Mac/JoinLinkPreviewModalController.swift +++ b/Telegram-Mac/JoinLinkPreviewModalController.swift @@ -8,14 +8,16 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit private class JoinLinkPreviewView : View { private let imageView:AvatarControl = AvatarControl(font: .avatar(.huge)) private let titleView:TextView = TextView() private let basicContainer:View = View() + private let usersContainer: View = View() required init(frame frameRect: NSRect) { super.init(frame: frameRect) self.backgroundColor = theme.colors.background @@ -25,29 +27,88 @@ private class JoinLinkPreviewView : View { titleView.backgroundColor = theme.colors.background basicContainer.addSubview(imageView) basicContainer.addSubview(titleView) + addSubview(usersContainer) } - func update(with peer:TelegramGroup, account:Account, participants:[Peer] = []) -> Void { + func update(with peer:TelegramGroup, account:Account, participants:[Peer]? = nil, groupUserCount: Int32 = 0) -> Void { imageView.setPeer(account: account, peer: peer) let attr = NSMutableAttributedString() _ = attr.append(string: peer.displayTitle, color: theme.colors.text, font: .normal(.title)) _ = attr.append(string: "\n") - _ = attr.append(string: tr(.peerStatusMemberCountable(peer.participantCount)), color: theme.colors.grayText, font: .normal(.text)) + _ = attr.append(string: L10n.peerStatusMemberCountable(peer.participantCount).replacingOccurrences(of: "\(peer.participantCount)", with: peer.participantCount.formattedWithSeparator), color: theme.colors.grayText, font: .normal(.text)) let titleLayout = TextViewLayout(attr, alignment: .center) titleLayout.measure(width: frame.width - 40) titleView.update(titleLayout) basicContainer.setFrameSize(frame.width, imageView.frame.height + titleView.frame.height + 10) + usersContainer.removeAllSubviews() + + if let participants = participants { + for participant in participants { + if usersContainer.subviews.count < 3 { + let avatar = AvatarControl(font: .avatar(20)) + avatar.setFrameSize(50, 50) + avatar.setPeer(account: account, peer: participant) + usersContainer.addSubview(avatar) + } else { + let additionCount = Int(groupUserCount) - usersContainer.subviews.count + if additionCount > 0 { + let avatar = AvatarControl(font: .avatar(20)) + avatar.setFrameSize(50, 50) + avatar.setState(account: account, state: .Empty) + let icon = generateImage(NSMakeSize(46, 46), contextGenerator: { size, ctx in + ctx.clear(NSMakeRect(0, 0, size.width, size.height)) + var fontSize: CGFloat = 13 + + if additionCount.prettyNumber.length == 1 { + fontSize = 18 + } else if additionCount.prettyNumber.length == 2 { + fontSize = 15 + } + let layout = TextViewLayout(.initialize(string: "+\(additionCount.prettyNumber)", color: .white, font: .medium(fontSize)), maximumNumberOfLines: 1, truncationType: .middle) + layout.measure(width: size.width - 4) + if !layout.lines.isEmpty { + let line = layout.lines[0] + // ctx.textMatrix = CGAffineTransform(scaleX: 1.0, y: -1.0) + ctx.textPosition = NSMakePoint(floorToScreenPixels(System.backingScale, (size.width - line.frame.width)/2.0) - 1, floorToScreenPixels(System.backingScale, (size.height - line.frame.height)/2.0) + 4) + + CTLineDraw(line.line, ctx) + } + })! + avatar.setSignal(generateEmptyPhoto(avatar.frame.size, type: .icon(colors: theme.colors.peerColors(5), icon: icon, iconSize: icon.backingSize, cornerRadius: nil)) |> map {($0, false)}) + usersContainer.addSubview(avatar) + } + break + } + + } + } + needsLayout = true + + } override func layout() { super.layout() - basicContainer.center() imageView.centerX(y: 0) titleView.centerX(y: 80) + + if !usersContainer.subviews.isEmpty { + basicContainer.centerX(y: 20) + var x:CGFloat = 0 + for avatar in usersContainer.subviews { + avatar.setFrameOrigin(NSMakePoint(x, 0)) + x += avatar.frame.width + 10 + } + usersContainer.setFrameSize(x - 10, usersContainer.subviews[0].frame.height) + } else { + basicContainer.center() + } + + usersContainer.centerX(y: basicContainer.frame.maxY + 20) } required init?(coder: NSCoder) { @@ -57,17 +118,16 @@ private class JoinLinkPreviewView : View { class JoinLinkPreviewModalController: ModalViewController { - private let account:Account + private let context:AccountContext private let join:ExternalJoiningChatState private let joinhash:String private let interaction:(PeerId?)->Void override func viewDidLoad() { super.viewDidLoad() switch join { - case let .invite(data): - let peer = TelegramGroup(id: PeerId(namespace: 0, id: 0), title: data.title, photo: data.photoRepresentation != nil ? [data.photoRepresentation!] : [], participantCount: Int(data.participantsCount), role: .member, membership: .Left, flags: [], migrationReference: nil, creationDate: 0, version: 0) - - genericView.update(with: peer, account: account) + case let .invite(title: title, image, memberCount, participants): + let peer = TelegramGroup(id: PeerId(namespace: Namespaces.Peer.CloudGroup, id: PeerId.Id._internalFromInt64Value(0)), title: title, photo: image.flatMap { [$0] } ?? [], participantCount: Int(memberCount), role: .member, membership: .Left, flags: [], defaultBannedRights: nil, migrationReference: nil, creationDate: 0, version: 0) + genericView.update(with: peer, account: context.account, participants: participants, groupUserCount: memberCount) default: break } @@ -82,23 +142,47 @@ class JoinLinkPreviewModalController: ModalViewController { return JoinLinkPreviewView.self } - init(_ account:Account, hash:String, join:ExternalJoiningChatState, interaction:@escaping(PeerId?)->Void) { - self.account = account + init(_ context: AccountContext, hash:String, join:ExternalJoiningChatState, interaction:@escaping(PeerId?)->Void) { + self.context = context self.join = join self.joinhash = hash self.interaction = interaction - super.init(frame: NSMakeRect(0, 0, 250, 200)) + + var rect = NSMakeRect(0, 0, 270, 180) + switch join { + case let .invite(_, _, _, participants): + if let participants = participants, participants.count > 0 { + rect.size.height = 230 + } + default: + break + } + super.init(frame: rect) + bar = .init(height: 0) } override var modalInteractions: ModalInteractions? { - return ModalInteractions(acceptTitle: tr(.joinLinkJoin), accept: { [weak self] in + let context = self.context + return ModalInteractions(acceptTitle: L10n.joinLinkJoin, accept: { [weak self] in if let strongSelf = self, let window = strongSelf.window { - _ = showModalProgress(signal: joinChatInteractively(with: strongSelf.joinhash, account: strongSelf.account), for: window).start(next: { [weak strongSelf] (peerId) in + _ = showModalProgress(signal: context.engine.peers.joinChatInteractively(with: strongSelf.joinhash), for: window).start(next: { [weak strongSelf] peerId in strongSelf?.interaction(peerId) self?.close() + }, error: { error in + let text: String + switch error { + case .generic: + text = L10n.unknownError + case .tooMuchJoined: + showInactiveChannels(context: context, source: .join) + return + case .tooMuchUsers: + text = L10n.groupUsersTooMuchError + } + alert(for: context.window, info: text) }) } - }, cancelTitle: tr(.modalCancel)) + }, cancelTitle: tr(L10n.modalCancel)) } diff --git a/Telegram-Mac/JoinVoiceChatAlertController.swift b/Telegram-Mac/JoinVoiceChatAlertController.swift new file mode 100644 index 0000000000..5246768c4e --- /dev/null +++ b/Telegram-Mac/JoinVoiceChatAlertController.swift @@ -0,0 +1,101 @@ +// +// JoinVoiceChatAlertController.swift +// Telegram +// +// Created by Mikhail Filimonov on 11.03.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation + +import Cocoa +import TGUIKit +import SwiftSignalKit +import TelegramCore +import Postbox + +private final class Arguments { + let context: AccountContext + init(context: AccountContext) { + self.context = context + } +} + +private struct State : Equatable { + var title: String + var peer: PeerEquatable + var participantsCount: Int +} + + +private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: .init("header"), equatable: InputDataEquatable(state), comparable: nil, item: { initialSize, stableId in + return JoinVoiceChatAlertRowItem(initialSize, stableId: stableId, account: arguments.context.account, peer: state.peer.peer, title: state.title, participantsCount: state.participantsCount) + })) + + // entries + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func JoinVoiceChatAlertController(context: AccountContext, groupCall: GroupCallPanelData, peer: Peer, join: @escaping()->Void) -> InputDataModalController { + + let actionsDisposable = DisposableSet() + + var close:(()->Void)? = nil + + let initialState = State(title: groupCall.info?.title ?? peer.displayTitle, peer: PeerEquatable(peer), participantsCount: groupCall.participantCount) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let arguments = Arguments(context: context) + + let signal = statePromise.get() |> deliverOnPrepareQueue |> map { state in + return InputDataSignalValue(entries: entries(state, arguments: arguments)) + } + + let controller = InputDataController(dataSignal: signal, title: L10n.chatVoiceChatJoinLinkTitle) + + controller.onDeinit = { + actionsDisposable.dispose() + } + + controller.validateData = { _ in + join() + close?() + return .none + } + + let modalInteractions = ModalInteractions(acceptTitle: L10n.chatVoiceChatJoinLinkOK, accept: { [weak controller] in + _ = controller?.returnKeyAction() + }, drawBorder: true, height: 50, singleButton: true) + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions, size: NSMakeSize(250, 250)) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) + + close = { [weak modalController] in + modalController?.modal?.close() + } + + return modalController +} + + diff --git a/Telegram-Mac/JoinVoiceChatAlertRowItem.swift b/Telegram-Mac/JoinVoiceChatAlertRowItem.swift new file mode 100644 index 0000000000..3b84afcf4b --- /dev/null +++ b/Telegram-Mac/JoinVoiceChatAlertRowItem.swift @@ -0,0 +1,91 @@ +// +// JoinVoiceChatAlertRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 11.03.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TelegramCore +import Postbox +import SwiftSignalKit +import TGUIKit + +final class JoinVoiceChatAlertRowItem : GeneralRowItem { + fileprivate let account: Account + fileprivate let peer: Peer + fileprivate let titleLayout: TextViewLayout + init(_ initialSize: NSSize, stableId: AnyHashable, account: Account, peer: Peer, title: String, participantsCount: Int) { + self.account = account + self.peer = peer + + let attr = NSMutableAttributedString() + + _ = attr.append(string: title, color: theme.colors.text, font: .medium(.title)) + _ = attr.append(string: "\n") + _ = attr.append(string: L10n.chatVoiceChatJoinLinkParticipantsCountable(participantsCount), color: theme.colors.grayText, font: .normal(.text)) + + + self.titleLayout = TextViewLayout(attr, alignment: .center) + + super.init(initialSize, stableId: stableId) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + + self.titleLayout.measure(width: width - 60) + return true + } + + override var height: CGFloat { + return 70 + 10 + self.titleLayout.layoutSize.height + } + + override func viewClass() -> AnyClass { + return JoinVoiceChatAlertRowView.self + } +} + +private final class JoinVoiceChatAlertRowView : TableRowView { + private let avatar: AvatarControl = AvatarControl(font: .avatar(20)) + private let title: TextView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + avatar.setFrameSize(NSMakeSize(70, 70)) + addSubview(avatar) + addSubview(title) + title.userInteractionEnabled = false + title.isSelectable = false + } + + override func updateColors() { + super.updateColors() + self.title.backgroundColor = backdorColor + } + + override func layout() { + super.layout() + avatar.centerX(y: 0) + title.centerX(y: avatar.frame.maxY + 10) + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? JoinVoiceChatAlertRowItem else { + return + } + title.update(item.titleLayout) + avatar.setPeer(account: item.account, peer: item.peer) + } + + override var backdorColor: NSColor { + return theme.colors.listBackground + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/LAnimationButton.swift b/Telegram-Mac/LAnimationButton.swift new file mode 100644 index 0000000000..db3dc777f1 --- /dev/null +++ b/Telegram-Mac/LAnimationButton.swift @@ -0,0 +1,135 @@ +// +// LAnimationButton.swift +// Telegram +// +// Created by Mikhail Filimonov on 29/04/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import TGUIKit + + +enum LButtonAutoplaySide { + case left + case right +} + +class LAnimationButton: Button { + private let animationView: LottiePlayerView = LottiePlayerView(frame: NSZeroRect) + var speed: CGFloat = 1.0 { + didSet { + //animationView.animationSpeed = speed + } + } + private let offset: NSSize + var updateIfWindowChanged: Bool = true + var played = false + var completion: (() -> Void)? + + var autoplayOnVisibleSide: LButtonAutoplaySide? = nil + private var animation: LottieAnimation? + private var firstFrame: LottieAnimation? + init(animation: String, size: NSSize, keysToColor: [String]? = nil, color: NSColor = .black, offset: NSSize = NSMakeSize(0, 0), autoplaySide: LButtonAutoplaySide? = nil, rotated: Bool = false) { + self.offset = offset + self.autoplayOnVisibleSide = autoplaySide + + animationView.setFrameSize(size) + super.init(frame: NSMakeRect(0, 0, size.width, size.height)) + addSubview(animationView) + + setAnimationName(animation, keysToColor: keysToColor, color: color) + + if rotated { + // animationView.rotate(byDegrees: 180) + } + } + + func setAnimationName(_ animation: String, keysToColor: [String]? = nil, color: NSColor = .black) { + if let file = Bundle.main.path(forResource: animation, ofType: "json"), let data = try? Data(contentsOf: URL(fileURLWithPath: file)) { + self.animation = LottieAnimation(compressed: data, key: .init(key: .bundle(animation), size: frame.size), cachePurpose: .none, playPolicy: .toEnd(from: 1), maximumFps: 60, runOnQueue: .mainQueue()) + self.firstFrame = LottieAnimation(compressed: data, key: .init(key: .bundle(animation), size: frame.size), cachePurpose: .none, playPolicy: .framesCount(1), maximumFps: 60, runOnQueue: .mainQueue()) + } else { + self.animation = nil + self.firstFrame = nil + } + set(keysToColor: keysToColor, color: color) + } + + func set(keysToColor: [String]? = nil, color: NSColor = .black) { + let newColor = color.usingColorSpace(.deviceRGB)! + + var colors: [LottieColor] = [] + if let keysToColor = keysToColor { + for keyToColor in keysToColor { + colors.append(LottieColor(keyPath: keyToColor, color: newColor)) + } + } + + self.animation = self.animation?.withUpdatedColors(colors) + self.firstFrame = self.firstFrame?.withUpdatedColors(colors) + animationView.set(self.firstFrame) + + } + + + override func viewDidMoveToWindow() { + if updateIfWindowChanged { + if window == nil { + animationView.set(nil) + } else { + animationView.set(self.firstFrame) + } + } + } + + private var prevVisibleRect: NSRect = NSZeroRect + + override func updateTrackingAreas() { + super.updateTrackingAreas() + if let audoplaySide = self.autoplayOnVisibleSide { + let point: NSPoint + switch audoplaySide { + case .right: + point = NSMakePoint(frame.width - frame.width / 3, frame.height / 2) + case .left: + point = NSMakePoint(frame.width / 3, frame.height / 2) + } + let locationInWindow = self.convert(point, to: nil) + if window?.contentView?.hitTest(locationInWindow) == self.animationView { + if !self.played { + self.played = true + animationView.set(self.animation) + } + } else if self.played { + self.played = false + animationView.set(self.firstFrame) + } + } + + + self.prevVisibleRect = visibleRect + } + + + + func loop() { + self.animationView.set(self.animation, reset: true) + } + + + + + override func layout() { + super.layout() + animationView.center() + animationView.setFrameOrigin(animationView.frame.minX - offset.width, animationView.frame.minY - offset.height) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } +} diff --git a/Telegram-Mac/LanguageRowItem.swift b/Telegram-Mac/LanguageRowItem.swift index 14422afd9f..90bc39eb2f 100644 --- a/Telegram-Mac/LanguageRowItem.swift +++ b/Telegram-Mac/LanguageRowItem.swift @@ -8,24 +8,28 @@ import Cocoa import TGUIKit -import TelegramCoreMac -class LanguageRowItem: TableRowItem { +import TelegramCore + +class LanguageRowItem: GeneralRowItem { fileprivate let selected:Bool fileprivate let locale:TextViewLayout fileprivate let title:TextViewLayout - fileprivate let action:()->Void + fileprivate let deleteAction: ()->Void + fileprivate let deletable: Bool + fileprivate let _stableId:AnyHashable override var stableId: AnyHashable { return _stableId } - init(initialSize: NSSize, stableId: AnyHashable, selected: Bool, value:LocalizationInfo, action:@escaping()->Void, reversed: Bool = false) { + init(initialSize: NSSize, stableId: AnyHashable, selected: Bool, deletable: Bool, value:LocalizationInfo, viewType: GeneralViewType = .legacy, action:@escaping()->Void, deleteAction: @escaping()->Void = {}, reversed: Bool = false) { self._stableId = stableId self.selected = selected self.title = TextViewLayout(.initialize(string: reversed ? value.localizedTitle : value.title, color: theme.colors.text, font: .normal(.title)), maximumNumberOfLines: 1) self.locale = TextViewLayout(.initialize(string: reversed ? value.title : value.localizedTitle, color: reversed ? theme.colors.grayText : theme.colors.grayText, font: .normal(.text)), maximumNumberOfLines: 1) - self.action = action - super.init(initialSize) + self.deletable = deletable + self.deleteAction = deleteAction + super.init(initialSize, viewType: viewType, action: action) _ = makeSize(initialSize.width, oldWidth: initialSize.width) } @@ -51,22 +55,30 @@ class LanguageRowView : TableRowView { private let titleTextView:TextView = TextView() private let selectedImage:ImageView = ImageView() private let overalay:OverlayControl = OverlayControl() + private let deleteButton = ImageButton() required init(frame frameRect: NSRect) { super.init(frame: frameRect) addSubview(titleTextView) addSubview(localeTextView) addSubview(selectedImage) + selectedImage.sizeToFit() localeTextView.isSelectable = false titleTextView.isSelectable = false addSubview(overalay) overalay.set(background: .grayTransparent, for: .Highlight) - + addSubview(deleteButton) overalay.set(handler: { [weak self] _ in if let item = self?.item as? LanguageRowItem { item.action() } }, for: .Click) + + deleteButton.set(handler: { [weak self] _ in + if let item = self?.item as? LanguageRowItem { + item.deleteAction() + } + }, for: .Click) } override var backdorColor: NSColor { @@ -85,11 +97,18 @@ class LanguageRowView : TableRowView { titleTextView.update(item.title) localeTextView.update(item.locale) + deleteButton.set(image: theme.icons.customLocalizationDelete, for: .Normal) + _ = deleteButton.sizeToFit() + selectedImage.image = theme.icons.generalSelect selectedImage.sizeToFit() titleTextView.backgroundColor = theme.colors.background localeTextView.backgroundColor = theme.colors.background selectedImage.isHidden = !item.selected + + deleteButton.isHidden = !item.deletable || item.selected + + needsLayout = true } } @@ -97,6 +116,7 @@ class LanguageRowView : TableRowView { override func layout() { super.layout() selectedImage.centerY(x: frame.width - 25 - selectedImage.frame.width) + deleteButton.centerY(x: frame.width - 18 - deleteButton.frame.width) if let item = item as? LanguageRowItem { titleTextView.update(item.title, origin: NSMakePoint(25, 5)) localeTextView.update(item.locale, origin: NSMakePoint(25, frame.height - titleTextView.frame.height - 5)) diff --git a/Telegram-Mac/LanguageViewController.swift b/Telegram-Mac/LanguageViewController.swift index 7e1f5aef18..8aa506adac 100644 --- a/Telegram-Mac/LanguageViewController.swift +++ b/Telegram-Mac/LanguageViewController.swift @@ -8,24 +8,20 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac +import TelegramCore + +import SwiftSignalKit + -extension LocalizationInfo : Equatable { - -} -public func ==(lhs:LocalizationInfo, rhs:LocalizationInfo) -> Bool { - return lhs.title == rhs.title && lhs.languageCode == rhs.languageCode && lhs.localizedTitle == rhs.localizedTitle -} final class LanguageControllerArguments { - let account:Account + let context:AccountContext let change:(LocalizationInfo)->Void - let searchInteractions:SearchInteractions - init(account:Account, change:@escaping(LocalizationInfo)->Void, searchInteractions: SearchInteractions) { - self.account = account + let delete:(LocalizationInfo)->Void + init(context: AccountContext, change:@escaping(LocalizationInfo)->Void, delete:@escaping(LocalizationInfo)->Void) { + self.context = context self.change = change - self.searchInteractions = searchInteractions + self.delete = delete } } @@ -33,6 +29,8 @@ enum LanguageTableEntryId : Hashable { case search case language(String) case loading + case sectionId(Int32) + case headerId(Int32) var hashValue: Int { switch self { case .search: @@ -41,54 +39,41 @@ enum LanguageTableEntryId : Hashable { return id.hashValue case .loading: return 1 + case .sectionId: + return 2 + case .headerId: + return 3 } } - static func ==(lhs:LanguageTableEntryId, rhs:LanguageTableEntryId) -> Bool { - switch lhs { - case .search: - if case .search = rhs { - return true - } else { - return false - } - case .loading: - if case .loading = rhs { - return true - } else { - return false - } - case .language(let id): - if case .language(id) = rhs { - return true - } else { - return false - } - } - } - } enum LanguageTableEntry : TableItemListNodeEntry { - case search - case language(index:Int32, selected: Bool, value: LocalizationInfo) + case language(sectionId: Int32, index:Int32, selected: Bool, deletable: Bool, value: LocalizationInfo, viewType: GeneralViewType) + case section(Int32, Bool) + case header(sectionId: Int32, index:Int32, descId: Int32, value: String, viewType: GeneralViewType) case loading var stableId: LanguageTableEntryId { switch self { - case .search: - return .search - case .language(_, _, let value): + case .language(_, _, _, _, let value, _): return .language(value.languageCode) + case let .section(sectionId, _): + return .sectionId(sectionId) + case let .header(_, _, id, _, _): + return .headerId(id) case .loading: return .loading } } + var index:Int32 { switch self { - case .search: - return 0 - case .language(let index, _, _): - return 1 + index + case let .language(sectionId, index, _, _, _, _): + return (sectionId * 1000) + index + case let .header(sectionId, index, _, _, _): + return (sectionId * 1000) + index + case let .section(sectionId, _): + return (sectionId + 1) * 1000 - sectionId case .loading: return -1 } @@ -96,65 +81,123 @@ enum LanguageTableEntry : TableItemListNodeEntry { func item(_ arguments: LanguageControllerArguments, initialSize: NSSize) -> TableRowItem { switch self { - case .search: - return SearchRowItem(initialSize, stableId: stableId, searchInteractions: arguments.searchInteractions, inset: NSEdgeInsets(left: 25, right: 25, top: 10, bottom: 10)) - case let .language(_, selected, value): - return LanguageRowItem(initialSize: initialSize, stableId: stableId, selected: selected, value: value, action: { + case let .language(_, _, selected, deletable, value, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: value.title, description: value.localizedTitle, descTextColor: theme.colors.grayText, type: .selectable(selected), viewType: viewType, action: { arguments.change(value) + }, menuItems: { + if deletable { + return [ContextMenuItem(L10n.messageContextDelete, handler: { + arguments.delete(value) + })] + } + return [] }) + case let .section(_, hasSearch): + return GeneralRowItem(initialSize, height: hasSearch ? 80 : 30, stableId: stableId, viewType: .separator) + case let .header(_, _, _, value, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: value, viewType: viewType) case .loading: return SearchEmptyRowItem(initialSize, stableId: stableId, isLoading: true) } } } -func ==(lhs:LanguageTableEntry, rhs:LanguageTableEntry) -> Bool { - switch lhs { - case .search: - if case .search = rhs { - return true - } else { - return false - } - case .loading: - if case .loading = rhs { - return true - } else { - return false - } - case let .language(index, selected, value): - if case .language(index, selected, value) = rhs { - return true - } else { - return false - } - } -} - func <(lhs:LanguageTableEntry, rhs:LanguageTableEntry) -> Bool { return lhs.index < rhs.index } -private func languageControllerEntries(infos: [LocalizationInfo]?, language: Language, state:SearchState) -> [LanguageTableEntry] { +private func languageControllerEntries(listState: LocalizationListState?, language: TelegramLocalization, state:SearchState, searchViewState: TableSearchViewState) -> [LanguageTableEntry] { + var sectionId: Int32 = 0 + var index: Int32 = 0 + var entries: [LanguageTableEntry] = [] - if let infos = infos { - entries.append(.search) - var index:Int32 = 1 + if let listState = listState, !listState.availableSavedLocalizations.isEmpty || !listState.availableOfficialLocalizations.isEmpty { + + + switch searchViewState { + case .visible: + entries.append(.section(sectionId, true)) + sectionId += 1 + default: + entries.append(.section(sectionId, false)) + sectionId += 1 + } + + let availableSavedLocalizations = listState.availableSavedLocalizations.filter({ info in !listState.availableOfficialLocalizations.contains(where: { $0.languageCode == info.languageCode }) }).filter { value in + if state.request.isEmpty { + return true + } else { + return (value.title.lowercased().range(of: state.request.lowercased()) != nil) || (value.localizedTitle.lowercased().range(of: state.request.lowercased()) != nil) + } + } - for value in infos { + let availableOfficialLocalizations = listState.availableOfficialLocalizations.filter { value in + if state.request.isEmpty { + return true + } else { + return (value.title.lowercased().range(of: state.request.lowercased()) != nil) || (value.localizedTitle.lowercased().range(of: state.request.lowercased()) != nil) + } + } + + var existingIds:Set = Set() + + + let saved = availableSavedLocalizations.filter { value in + + if existingIds.contains(value.languageCode) { + return false + } + var accept: Bool = true if !state.request.isEmpty { accept = (value.title.lowercased().range(of: state.request.lowercased()) != nil) || (value.localizedTitle.lowercased().range(of: state.request.lowercased()) != nil) } - if accept { - entries.append(.language(index: index, selected: value.languageCode == language.languageCode, value: value)) - index += 1 + return accept + } + + for value in saved { + let viewType: GeneralViewType = bestGeneralViewType(saved, for: value) + + existingIds.insert(value.languageCode) + entries.append(.language(sectionId: sectionId, index: index, selected: value.languageCode == language.primaryLanguage.languageCode, deletable: true, value: value, viewType: viewType)) + index += 1 + } + + + + if !availableOfficialLocalizations.isEmpty { + + if !availableSavedLocalizations.isEmpty { + entries.append(.section(sectionId, false)) + sectionId += 1 } + + let list = listState.availableOfficialLocalizations.filter { value in + if existingIds.contains(value.languageCode) { + return false + } + var accept: Bool = true + if !state.request.isEmpty { + accept = (value.title.lowercased().range(of: state.request.lowercased()) != nil) || (value.localizedTitle.lowercased().range(of: state.request.lowercased()) != nil) + } + return accept + } + + for value in list { + let viewType: GeneralViewType = bestGeneralViewType(list, for: value) + existingIds.insert(value.languageCode) + entries.append(.language(sectionId: sectionId, index: index, selected: value.languageCode == language.primaryLanguage.languageCode, deletable: false, value: value, viewType: viewType)) + index += 1 + } } + + entries.append(.section(sectionId, false)) + sectionId += 1 + } else { entries.append(.loading) } @@ -163,13 +206,63 @@ private func languageControllerEntries(infos: [LocalizationInfo]?, language: Lan return entries } -fileprivate func prepareTransition(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], initialSize:NSSize, arguments:LanguageControllerArguments) -> TableUpdateTransition { +fileprivate func prepareTransition(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], initialSize:NSSize, animated: Bool, arguments:LanguageControllerArguments, searchViewState: TableSearchViewState) -> Signal { + - let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in - return entry.entry.item(arguments, initialSize: initialSize) + return Signal { subscriber in + var cancelled = false + + if Thread.isMainThread { + var initialIndex:Int = 0 + var height:CGFloat = 0 + var firstInsertion:[(Int, TableRowItem)] = [] + let entries = Array(right) + + let index:Int = 0 + + for i in index ..< entries.count { + let item = entries[i].entry.item(arguments, initialSize: initialSize) + height += item.height + firstInsertion.append((i, item)) + if initialSize.height < height { + break + } + } + + initialIndex = firstInsertion.count + subscriber.putNext(TableUpdateTransition(deleted: [], inserted: firstInsertion, updated: [], state: .none(nil), searchState: searchViewState)) + + prepareQueue.async { + if !cancelled { + + var insertions:[(Int, TableRowItem)] = [] + let updates:[(Int, TableRowItem)] = [] + + for i in initialIndex ..< entries.count { + let item:TableRowItem + item = entries[i].entry.item(arguments, initialSize: initialSize) + insertions.append((i, item)) + } + + + subscriber.putNext(TableUpdateTransition(deleted: [], inserted: insertions, updated: updates, state: .none(nil), searchState: searchViewState)) + subscriber.putCompletion() + } + } + } else { + let (deleted,inserted,updated) = proccessEntriesWithoutReverse(left, right: right, { entry -> TableRowItem in + return entry.entry.item(arguments, initialSize: initialSize) + }) + + subscriber.putNext(TableUpdateTransition(deleted: deleted, inserted: inserted, updated:updated, animated: animated, state: .none(nil), searchState: searchViewState)) + subscriber.putCompletion() + } + + return ActionDisposable { + cancelled = true + } } - return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true) } @@ -177,61 +270,137 @@ fileprivate func prepareTransition(left:[AppearanceWrapperEntryVoid)? = nil override var enableBack: Bool { return true } + override init(_ context: AccountContext) { + super.init(context) + } + deinit { applyDisposable.dispose() languageDisposable.dispose() + disposable.dispose() + } + + override func getRightBarViewOnce() -> BarView { + let view = ImageBarView(controller: self, theme.icons.chatSearch) + + view.button.set(handler: { [weak self] _ in + self?.toggleSearch?() + }, for: .Click) + view.set(image: theme.icons.chatSearch, highlightImage: nil) + return view + } + + + override func requestUpdateRightBar() { + super.requestUpdateRightBar() + (self.rightBarView as? ImageBarView)?.set(image: theme.icons.chatSearch, highlightImage: nil) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + window?.removeAllHandlers(for: self) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + window?.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.toggleSearch?() + return .invoked + }, with: self, for: .F, modifierFlags: [.command]) } override func viewDidLoad() { super.viewDidLoad() - let searchPromise = ValuePromise() + let context = self.context + + + let stateValue: Atomic = Atomic(value: SearchState(state: .None, request: nil)) + let statePromise:ValuePromise = ValuePromise(SearchState(state: .None, request: nil), ignoreRepeated: true) + + let updateState:((SearchState)->SearchState)->Void = { f in + statePromise.set(stateValue.modify(f)) + } + + + let searchValue:Atomic = Atomic(value: .none) + let searchState: ValuePromise = ValuePromise(.none, ignoreRepeated: true) + let updateSearchValue:((TableSearchViewState)->TableSearchViewState)->Void = { f in + searchState.set(searchValue.modify(f)) + } - let searchInteractions = SearchInteractions({ state in - searchPromise.set(state) - }, { state in - searchPromise.set(state) + let searchData = TableSearchVisibleData(cancelImage: theme.icons.chatSearchCancel, cancel: { + updateSearchValue { _ in + return .none + } + }, updateState: { searchState in + updateState { _ in + return searchState + } }) - searchPromise.set(SearchState(state: .None, request: nil)) - let account = self.account - let arguments = LanguageControllerArguments(account: account, change: { [weak self] value in - if value.languageCode != appCurrentLanguage.languageCode { - self?.applyDisposable.set(showModalProgress(signal: downoadAndApplyLocalization(postbox: account.postbox, network: account.network, languageCode: value.languageCode), for: mainWindow).start()) + self.toggleSearch = { + updateSearchValue { current in + switch current { + case .none: + return .visible(searchData) + case .visible: + return .none + } + } + } + + let arguments = LanguageControllerArguments(context: context, change: { [weak self] value in + if value.languageCode != appCurrentLanguage.primaryLanguage.languageCode { + + self?.applyDisposable.set(showModalProgress(signal: context.engine.localization.downloadAndApplyLocalization(accountManager: context.sharedContext.accountManager, languageCode: value.languageCode), for: context.window).start()) } - }, searchInteractions: searchInteractions) + }, delete: { info in + confirm(for: context.window, information: L10n.languageRemovePack, successHandler: { _ in + let _ = (context.account.postbox.transaction { transaction in + removeSavedLocalization(transaction: transaction, languageCode: info.languageCode) + }).start() + }) + }) let previous:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) let initialSize = atomicSize + let signal = context.account.postbox.preferencesView(keys: [PreferencesKeys.localizationListState]) |> map { value -> LocalizationListState? in + return value.values[PreferencesKeys.localizationListState] as? LocalizationListState + } |> deliverOnPrepareQueue - genericView.merge(with: combineLatest(Signal<[LocalizationInfo]?, Void>.single(nil) |> then(availableLocalizations(network: account.network) |> map {Optional($0)} |> deliverOnMainQueue), appearanceSignal) - |> mapToSignal { infos, appearance in - return searchPromise.get() |> map { state in - return (infos, appearance, state) - } - } |> map { infos, appearance, state in - let entries = languageControllerEntries(infos: infos, language: appearance.language - , state: state).map({AppearanceWrapperEntry(entry: $0, appearance: appearance)}) - return prepareTransition(left: previous.swap(entries), right: entries, initialSize: initialSize.modify({$0}), arguments: arguments) - }) - + let first: Atomic = Atomic(value: true) + let prevSearch: Atomic = Atomic(value: nil) + + let transition: Signal = combineLatest(signal, appearanceSignal, statePromise.get(), searchState.get()) |> mapToSignal { listState, appearance, state, searchViewState in + let entries = languageControllerEntries(listState: listState, language: appearance.language, state: state, searchViewState: searchViewState) + .map { AppearanceWrapperEntry(entry: $0, appearance: appearance) } + + return prepareTransition(left: previous.swap(entries), right: entries, initialSize: initialSize.with { $0 }, animated: prevSearch.swap(state.request) == state.request, arguments: arguments, searchViewState: searchViewState) + |> runOn(first.swap(false) ? .mainQueue() : prepareQueue) + } |> deliverOnMainQueue - readyOnce() + disposable.set(transition.start(next: { [weak self] transition in + self?.genericView.merge(with: transition) + self?.readyOnce() + })) } - - + } diff --git a/Telegram-Mac/LeftSidebarController.swift b/Telegram-Mac/LeftSidebarController.swift new file mode 100644 index 0000000000..9c06b99fb0 --- /dev/null +++ b/Telegram-Mac/LeftSidebarController.swift @@ -0,0 +1,246 @@ +// +// FoldersSidebarController.swift +// Telegram +// +// Created by Mikhail Filimonov on 06/04/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import Postbox +import SwiftSignalKit +import TelegramCore + +func filterContextMenuItems(_ filter: ChatListFilter?, context: AccountContext) -> [ContextMenuItem] { + var items:[ContextMenuItem] = [] + if var filter = filter { + items.append(.init(L10n.chatListFilterEdit, handler: { + context.sharedContext.bindings.rootNavigation().push(ChatListFilterController(context: context, filter: filter)) + })) + items.append(.init(L10n.chatListFilterAddChats, handler: { + showModal(with: ShareModalController(SelectCallbackObject(context, defaultSelectedIds: Set(filter.data.includePeers.peers), additionTopItems: nil, limit: 100, limitReachedText: L10n.chatListFilterIncludeLimitReached, callback: { peerIds in + return context.engine.peers.updateChatListFiltersInteractively({ filters in + var filters = filters + filter.data.includePeers.setPeers(Array(peerIds.uniqueElements.prefix(100))) + if let index = filters.firstIndex(where: {$0.id == filter.id }) { + filters[index] = filter + } + return filters + }) |> ignoreValues + + })), for: context.window) + })) + items.append(.init(L10n.chatListFilterDelete, handler: { + confirm(for: context.window, header: L10n.chatListFilterConfirmRemoveHeader, information: L10n.chatListFilterConfirmRemoveText, okTitle: L10n.chatListFilterConfirmRemoveOK, successHandler: { _ in + _ = context.engine.peers.updateChatListFiltersInteractively({ filters in + var filters = filters + filters.removeAll(where: { $0.id == filter.id }) + return filters + }).start() + }) + + })) + } else { + items.append(.init(L10n.chatListFilterEditFilters, handler: { + context.sharedContext.bindings.rootNavigation().push(ChatListFiltersListController(context: context)) + })) + } + + return items +} + +private final class LeftSidebarArguments { + let context: AccountContext + let callback:(ChatListFilter?)->Void + let menuItems:(ChatListFilter?)->[ContextMenuItem] + init(context: AccountContext, callback: @escaping(ChatListFilter?)->Void, menuItems: @escaping(ChatListFilter?)->[ContextMenuItem]) { + self.context = context + self.callback = callback + self.menuItems = menuItems + } +} + + +final class LeftSidebarView: View { + fileprivate let tableView = TableView() + private let visualEffectView: NSVisualEffectView + private let borderView = View() + required init(frame frameRect: NSRect) { + self.visualEffectView = NSVisualEffectView(frame: NSMakeRect(0, 0, frameRect.width, frameRect.height)) + super.init(frame: frameRect) + + addSubview(self.visualEffectView) + addSubview(self.borderView) + + addSubview(self.tableView) + tableView.getBackgroundColor = { + return .clear + } + + visualEffectView.blendingMode = .behindWindow + visualEffectView.material = .ultraDark + + updateLocalizationAndTheme(theme: theme) + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + + borderView.backgroundColor = theme.colors.border + self.backgroundColor = theme.colors.listBackground + self.borderView.isHidden = !theme.colors.isDark + self.visualEffectView.isHidden = theme.colors.isDark + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + self.visualEffectView.frame = bounds + self.tableView.frame = bounds + self.borderView.frame = NSMakeRect(frame.width - .borderSize, 0, .borderSize, frame.height) + } +} + +private enum LeftSibarBarEntry : Comparable, Identifiable { + static func < (lhs: LeftSibarBarEntry, rhs: LeftSibarBarEntry) -> Bool { + return lhs.index < rhs.index + } + case topOffset + case allChats(selected: Bool, unreadCount: Int, hasUnmutedUnread: Bool) + case folder(index: Int, selected: Bool, filter: ChatListFilter, unreadCount: Int, hasUnmutedUnread: Bool) + + var stableId: Int32 { + switch self { + case .topOffset: + return -2 + case .allChats: + return -1 + case let .folder(_, _, filter, _, _): + return filter.id + } + } + + var index: Int { + switch self { + case .topOffset: + return -1 + case .allChats: + return 0 + case let .folder(index, _, _, _, _): + return index + } + } + + func item(_ arguments: LeftSidebarArguments, initialSize: NSSize) -> TableRowItem { + switch self { + case let .allChats(selected, unreadCount, hasUnmutedUnread): + return LeftSidebarFolderItem(initialSize, folder: nil, selected: selected, unreadCount: unreadCount, hasUnmutedUnread: hasUnmutedUnread, callback: arguments.callback, menuItems: arguments.menuItems) + case let .folder(_, selected, filter, unreadCount, hasUnmutedUnread): + return LeftSidebarFolderItem(initialSize, folder: filter, selected: selected, unreadCount: unreadCount, hasUnmutedUnread: hasUnmutedUnread, callback: arguments.callback, menuItems: arguments.menuItems) + case .topOffset: + return GeneralRowItem(initialSize, height: 16, stableId: stableId, backgroundColor: .clear) + } + } +} + +private func leftSidebarEntries(_ filterData: FilterData, _ badges: ChatListFilterBadges) -> [LeftSibarBarEntry] { + var index: Int = 1 + + var entries:[LeftSibarBarEntry] = [] + entries.append(.topOffset) + + entries.append(.allChats(selected: filterData.filter == nil, unreadCount: badges.total, hasUnmutedUnread: true)) + + for filter in filterData.tabs { + let badge = badges.count(for: filter) + entries.append(.folder(index: index, selected: filter.id == filterData.filter?.id, filter: filter, unreadCount: badge?.count ?? 0, hasUnmutedUnread: badge?.hasUnmutedUnread ?? false)) + index += 1 + } + + return entries +} + +fileprivate func prepareTransition(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], initialSize:NSSize, arguments:LeftSidebarArguments) -> TableUpdateTransition { + + let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in + return entry.entry.item(arguments, initialSize: initialSize) + } + + return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true) +} + +class LeftSidebarController: TelegramGenericViewController { + + let filterData: Signal + let updateFilter: (_ f:(FilterData)->FilterData)->Void + + private let disposable = MetaDisposable() + + init(_ context: AccountContext, filterData: Signal, updateFilter: @escaping(_ f:(FilterData)->FilterData)->Void) { + self.filterData = filterData + self.updateFilter = updateFilter + super.init(context) + self.bar = .init(height: 0) + } + + deinit { + disposable.dispose() + } + + override func viewDidLoad() { + super.viewDidLoad() + let context = self.context + + let arguments = LeftSidebarArguments(context: context, callback: { [weak self] filter in + self?.updateFilter { state in + return state.withUpdatedFilter(filter) + } + + let rootNavigation = context.sharedContext.bindings.rootNavigation() + + let leftController = context.sharedContext.bindings.mainController() + leftController.chatListNavigation.close(animated: context.sharedContext.layout != .single || rootNavigation.stackCount == 1) + + if context.sharedContext.layout == .single { + rootNavigation.close(animated: true) + } + leftController.showChatList() + + }, menuItems: { filter in + return filterContextMenuItems(filter, context: context) + }) + let initialSize = self.atomicSize + + let previous: Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) + + let signal: Signal = combineLatest(queue: prepareQueue, filterData, chatListFilterItems(engine: context.engine, accountManager: context.sharedContext.accountManager), appearanceSignal) |> map { filterData, badges, appearance in + let entries = leftSidebarEntries(filterData, badges).map { AppearanceWrapperEntry.init(entry: $0, appearance: appearance) } + return prepareTransition(left: previous.swap(entries), right: entries, initialSize: initialSize.with { $0 }, arguments: arguments) + } |> deliverOnMainQueue + + self.genericView.tableView.alwaysOpenRowsOnMouseUp = true + + disposable.set(signal.start(next: { [weak self] transition in + + guard let `self` = self else { + return + } + self.genericView.tableView.merge(with: transition) + self.readyOnce() + + let range = NSMakeRange(2, self.genericView.tableView.count - 2) + + self.genericView.tableView.resortController = TableResortController(resortRange: range, start: { _ in }, resort: { _ in }, complete: { from, to in + _ = context.engine.peers.updateChatListFiltersInteractively({ filters in + var filters = filters + filters.move(at: from - range.location, to: to - range.location) + return filters + }).start() + }) + })) + } +} diff --git a/Telegram-Mac/LeftSidebarFolderItem.swift b/Telegram-Mac/LeftSidebarFolderItem.swift new file mode 100644 index 0000000000..4a8d2de1f8 --- /dev/null +++ b/Telegram-Mac/LeftSidebarFolderItem.swift @@ -0,0 +1,249 @@ +// +// LeftSidebarFolderItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 06/04/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore +import SwiftSignalKit + +extension FolderIcon { + convenience init(_ filter: ChatListFilter?) { + if let filter = filter { + if let emoticon = filter.emoticon { + self.init(emoticon: .emoji(emoticon)) + } else { + switch chatListFilterType(filter) { + case .bots: + self.init(emoticon: .bots) + case .channels: + self.init(emoticon: .channels) + case .contacts: + self.init(emoticon: .personal) + case .groups: + self.init(emoticon: .groups) + case .nonContacts: + self.init(emoticon: .personal) + case .unmuted: + self.init(emoticon: .unmuted) + case .unread: + self.init(emoticon: .unread) + case .generic: + self.init(emoticon: .folder) + } + } + } else { + self.init(emoticon: .allChats) + } + } +} + +class LeftSidebarFolderItem: TableRowItem { + + fileprivate let folder: ChatListFilter? + fileprivate let selected: Bool + fileprivate let callback: (ChatListFilter?)->Void + fileprivate let menuItems: (ChatListFilter?)-> [ContextMenuItem] + + let icon: CGImage + let badge: CGImage? + let nameLayout: TextViewLayout + + + init(_ initialSize: NSSize, folder: ChatListFilter?, selected: Bool, unreadCount: Int, hasUnmutedUnread: Bool, callback: @escaping(ChatListFilter?)->Void, menuItems: @escaping(ChatListFilter?) -> [ContextMenuItem]) { + self.folder = folder + self.selected = selected + self.callback = callback + self.menuItems = menuItems + var folderIcon = FolderIcon(folder).icon(for: selected ? .sidebarActive : .sidebar) + nameLayout = TextViewLayout(.initialize(string: folder != nil ? folder!.title : L10n.chatListFilterAllChats, color: !selected ? NSColor.white.withAlphaComponent(0.5) : .white, font: .medium(10)), alignment: .center) + nameLayout.measure(width: initialSize.width - 10) + + + + let generateIcon:()->CGImage? = { + let icon: CGImage? + if unreadCount > 0 { + + let textColor: NSColor + if selected { + textColor = .black + } else { + textColor = .white + } + + let attributedString = NSAttributedString.initialize(string: "\(unreadCount.prettyNumber)", color: textColor, font: .medium(.short), coreText: true) + let textLayout = TextNode.layoutText(maybeNode: nil, attributedString, nil, 1, .start, NSMakeSize(CGFloat.greatestFiniteMagnitude, CGFloat.greatestFiniteMagnitude), nil, false, .center) + var size = NSMakeSize(textLayout.0.size.width + 8, textLayout.0.size.height + 5) + size = NSMakeSize(max(size.height,size.width), size.height) + + let badge = generateImage(size, rotatedContext: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + if selected { + ctx.setFillColor(.white) + } else if hasUnmutedUnread { + ctx.setFillColor(NSColor.accentIcon.cgColor) + } else { + ctx.setFillColor(NSColor.grayIcon.cgColor) + } + ctx.round(size, size.height/2.0) + ctx.fill(rect) + +// ctx.setBlendMode(.clear) + + let focus = rect.focus(textLayout.0.size) + textLayout.1.draw(focus.offsetBy(dx: 0, dy: -1), in: ctx, backingScaleFactor: 2.0, backgroundColor: .white) + + })! + + folderIcon = generateImage(folderIcon.systemSize, contextGenerator: { size, ctx in + let rect = NSMakeRect(0, 0, size.width, size.height) + ctx.clear(rect) + + ctx.draw(folderIcon, in: rect.focus(folderIcon.systemSize)) + + ctx.clip(to: NSMakeRect(rect.width - badge.systemSize.width / 2 - 11 / System.backingScale, rect.height - badge.systemSize.height + 5 / System.backingScale, badge.systemSize.width + 4, badge.systemSize.height + 4), mask: badge) + + ctx.clear(rect) + + // ctx.draw(badge, in: rect) + })! + + icon = badge + + } else { + icon = nil + } + return icon + } + self.badge = generateIcon() + self.icon = folderIcon + super.init(initialSize) + } + + override var stableId: AnyHashable { + return folder?.id ?? -1 + } + + override func menuItems(in location: NSPoint) -> Signal<[ContextMenuItem], NoError> { + return .single(self.menuItems(folder)) + } + + override var height: CGFloat { + return 32 + 8 + 8 + nameLayout.layoutSize.height + 4 + } + + override func viewClass() -> AnyClass { + return LeftSidebarFolderView.self + } + +} + + +private final class LeftSidebarFolderView : TableRowView { + private let imageView = ImageView(frame: NSMakeRect(0, 0, 32, 32)) + private let badgeView = ImageView() + private let textView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(imageView) + addSubview(textView) + addSubview(badgeView) + textView.userInteractionEnabled = false + textView.isSelectable = false + textView.isEventLess = true + badgeView.isEventLess = true + imageView.isEventLess = true + } + + override func updateIsResorting() { + updateHighlight(animated: true) + } + + func updateHighlight(animated: Bool = true) { + guard let item = item as? LeftSidebarFolderItem else { + return + } + if !item.selected, mouseInside(), (NSEvent.pressedMouseButtons & (1 << 0)) != 0 { + self.imageView.change(opacity: 0.8, animated: animated) + self.textView.change(opacity: 0.8, animated: animated) + } else { + self.imageView.change(opacity: 1.0, animated: animated) + self.textView.change(opacity: 1.0, animated: animated) + } + } + + + override func mouseMoved(with event: NSEvent) { + super.mouseMoved(with: event) + updateHighlight() + } + + override func mouseEntered(with event: NSEvent) { + super.mouseEntered(with: event) + updateHighlight() + } + override func mouseExited(with event: NSEvent) { + super.mouseExited(with: event) + updateHighlight() + } + + override func mouseDown(with event: NSEvent) { + super.mouseDown(with: event) + updateHighlight() + } + + override func mouseUp(with event: NSEvent) { + super.mouseUp(with: event) + updateHighlight() + + if mouseInside() { + guard let item = item as? LeftSidebarFolderItem else { + return + } + item.callback(item.folder) + } + } + + override var backdorColor: NSColor { + return .clear + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? LeftSidebarFolderItem else { + return + } + // imageView.animates = animated + imageView.image = item.icon + + textView.update(item.nameLayout) + + + // badgeView.animates = animated + badgeView.image = item.badge + badgeView.sizeToFit() + + updateHighlight(animated: animated) + + needsLayout = true + } + + override func layout() { + super.layout() + + imageView.centerX(y: 8) + textView.centerX(y: imageView.frame.maxY + 4) + badgeView.setFrameOrigin(NSMakePoint(imageView.frame.maxX - badgeView.frame.width / 2 - 4, imageView.frame.minY - 4)) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/LegacyImportAuthorization.swift b/Telegram-Mac/LegacyImportAuthorization.swift deleted file mode 100644 index 535384211e..0000000000 --- a/Telegram-Mac/LegacyImportAuthorization.swift +++ /dev/null @@ -1,411 +0,0 @@ -// -// LegacyImportAuthorization.swift -// Telegram -// -// Created by keepcoder on 05/03/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - - -import SwiftSignalKitMac -import PostboxMac -import TelegramCoreMac -import MtProtoKitMac - -enum AuthorizationLegacyData { - case data(masterDatacenterId: Int32, userId:Int32, groups:[String: [String: Data]], peers:[Peer], secretMessages:[StoreMessage], resources: [(MediaResource, Data)], secretState:[PeerId: SecretChatStateBridge], passcode:String?) - case passcodeRequired - case none -} - -private enum LegacyGroupResult { - case data(NSDictionary) - case passcodeRequired - case empty -} - -private func keychainData(from dictionary: NSDictionary) -> [String : Data] { - var data:[String: Data] = [:] - for (key, value) in dictionary { - data[key as! String] = NSKeyedArchiver.archivedData(withRootObject: value) - } - return data -} - -private let keychainGroups:NSDictionary? = { - if let keychainData = SSKeychain.passwordData(forService: "Telegram", account: "authkeys") { - return NSKeyedUnarchiver.unarchiveObject(with: keychainData) as? NSDictionary - } else { - return nil - } -} () - -private func legacyGroupData (_ groupName: String, passcode:Data) -> LegacyGroupResult { - assert(passcode.count == 64) - - let groupData:NSData? - - #if APP_STORE - if let data = keychainGroups?[groupName] as? NSData { - groupData = data - } else { - groupData = nil - } - #else - let applicationSupportPath = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)[0] - let applicationName = "Telegram" - let dataDirectory = applicationSupportPath + "/\(applicationName)/encrypt-mtkeychain" - let path = dataDirectory + "/ru.keepcoder.Telegram_\(groupName).bin" - NSLog("\(path)") - - groupData = NSData(contentsOf: URL(fileURLWithPath: path)) - #endif - - - - if let groupData = groupData, groupData.length >= 8 { - let decrypt = NSMutableData(data: groupData.subdata(with: NSMakeRange(4, groupData.length - 8))) - let modifiedIv = NSMutableData(data: passcode.subdata(in: 32 ..< 64)) - MTAesDecryptInplaceAndModifyIv(decrypt, passcode.subdata(in: 0 ..< 32), modifiedIv) - - var hash:Int32 = 0 - var length:Int32 = 0 - groupData.getBytes(&hash, range: NSMakeRange(groupData.length - 4, 4)) - groupData.getBytes(&length, range: NSMakeRange(0, 4)) - - let decryptedHash = MTMurMurHash32(decrypt.bytes, length) - - if hash != decryptedHash { - return .passcodeRequired - } - - let object = NSKeyedUnarchiver.unarchiveObject(with: decrypt.subdata(with: NSMakeRange(0, Int(length)))) - - if let object = object as? NSDictionary { - return .data(object) - } - - } - return .empty -} - -func legacyAuthData(passcode:Data, textPasscode:String? = nil) -> AuthorizationLegacyData { - - var groupsData:[String : [String: Data]] = [:] - - let groups:[String] = ["persistent", "primes", "temp"] - var masterDatacenterId:Int32? - var userId:Int32? - var sqlKey:String? - for group in groups { - switch legacyGroupData(group, passcode: passcode) { - case .passcodeRequired: - return .passcodeRequired - case let .data(data): - if let dcId = data["dc_id"] as? NSNumber, let id = data["user_id"] as? NSNumber { - masterDatacenterId = dcId.int32Value - userId = id.int32Value - } - if let ekey = data["e_key"] as? String { - sqlKey = ekey - } - groupsData[group] = keychainData(from : data) - - default: - break - } - } - - - - if let masterDatacenterId = masterDatacenterId, let userId = userId { - - var peers:[Peer] = [] - var messages:[StoreMessage] = [] - var participantPeerIds:Set = Set() - var resources: [(MediaResource, Data)] = [] - var secretState:[PeerId: SecretChatStateBridge] = [:] - - - if let sSize = fileSize(legacySqlPath), sSize > 0, let ySize = fileSize(legacyYapPath), ySize > 0, let sqlKey = sqlKey, let keyData = sqlKey.data(using: .utf8), FileManager.default.fileExists(atPath: legacySqlPath), FileManager.default.fileExists(atPath: legacyYapPath), let sql = SqliteInterface(databasePath: legacySqlPath), let yap = SqliteInterface(databasePath: legacyYapPath) { - - - if sql.unlock(password: keyData) { - - sql.select("select serialized from encrypted_chats", { value -> Bool in - if let chat = MacosLegacy.parse(Buffer(data: value.getData(at: 0))) as? MacosLegacy.EncryptedChat { - switch chat { - case let .encryptedChat(chat): - let secret = TelegramSecretChat(id: PeerId(namespace: Namespaces.Peer.SecretChat, id: chat.id), creationDate: chat.date, regularPeerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: chat.adminId == userId ? chat.participantId : chat.adminId), accessHash: chat.accessHash, role: chat.adminId == userId ? .creator : .participant, embeddedState: .terminated, messageAutoremoveTimeout: nil) - peers.append(secret) - participantPeerIds.insert(secret.regularPeerId) - secretState[secret.id] = SecretChatStateBridge(role: chat.adminId == userId ? .creator : .participant) - } - } - - return true - }) - - var secretKeys:[Int64: SecretFileEncryptionKey] = [:] - - yap.select("select data, key from database2 where collection = \"encrypted_image_collection\"", { keyIvValue -> Bool in - if let dict = NSKeyedUnarchiver.unarchiveObject(with: keyIvValue.getData(at: 0)) as? NSDictionary { - if let aesKey = dict["key"] as? Data, let aesIv = dict["iv"] as? Data { - secretKeys[keyIvValue.getInt64(at: 1)] = SecretFileEncryptionKey(aesKey: aesKey, aesIv: aesIv) - } - } - return true - }) - - - - for peer in peers { - sql.select("select serialized, message_text from messages where peer_id in (\(peer.id.id))", { value -> Bool in - - var text = value.getString(at: 1) - - let message = MacosLegacy.parse(Buffer(data: value.getData(at: 0))) - if let message = message as? MacosLegacy.Message { - switch message { - case let .destructMessage(message): - - guard message.dstate == 2 else { - break - } - - var media:[Media] = [] - switch message.media { - case let .messageMediaGeo(geo: geo): - switch geo { - case let .geoPoint(long, lat): - media.append(TelegramMediaMap(latitude: lat, longitude: long, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil)) - default: - media.append(TelegramMediaMap(latitude: 0, longitude: 0, geoPlace: nil, venue: nil, liveBroadcastingTimeout: nil)) - } - case let .messageMediaContact(phoneNumber, firstName, lastName, userId): - let peerId:PeerId? = userId != 0 ? PeerId(namespace: Namespaces.Peer.CloudUser, id: userId) : nil - media.append(TelegramMediaContact(firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, peerId: peerId)) - case let .messageMediaPhoto(photo, caption): - switch photo { - case let .photo(_, id, accessHash, _, sizes): - - if let key = secretKeys[id] { - text = caption - var representations: [TelegramMediaImageRepresentation] = [] - - for size in sizes { - switch size { - case let .photoCachedSize(_, _, w, h, bytes): - let resource = LocalFileMediaResource(fileId: arc4random64()) - representations.append(TelegramMediaImageRepresentation(dimensions: CGSize(width: CGFloat(w), height: CGFloat(h)), resource: resource)) - resources.append((resource, bytes.makeData())) - - - case let .photoSize(_, location, w, h, size): - switch location { - case let .fileLocation(dcId, _, _, _): - let resource = SecretFileMediaResource(fileId: id, accessHash: accessHash, size: nil, decryptedSize: size, datacenterId: Int(dcId), key: key) - - representations.append(TelegramMediaImageRepresentation(dimensions: NSMakeSize(CGFloat(w), CGFloat(h)), resource: resource)) - - let image = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.CloudSecretImage, id: id), representations: representations) - - media.append(image) - default: - break - } - - case .photoSizeEmpty: - break - } - } - - } - - - break - default: - break - } - break - case let .messageMediaDocument(document, caption): - - switch document { - case let .document(id, accessHash, _, mimeType, size, thumb, dcId, _, attributes): - if let key = secretKeys[id] { - text = caption - var parsedAttributes: [TelegramMediaFileAttribute] = [] - for attribute in attributes { - switch attribute { - case let .documentAttributeFilename(fileName): - parsedAttributes.append(.FileName(fileName: fileName)) - case let .documentAttributeSticker(_, alt, _, _): - parsedAttributes.append(.Sticker(displayText: alt, packReference: nil, maskData: nil)) - case .documentAttributeHasStickers: - parsedAttributes.append(.HasLinkedStickers) - case let .documentAttributeImageSize(w, h): - parsedAttributes.append(.ImageSize(size: CGSize(width: CGFloat(w), height: CGFloat(h)))) - case .documentAttributeAnimated: - parsedAttributes.append(.Animated) - case let .documentAttributeVideo(duration, w, h): - parsedAttributes.append(.Video(duration: Int(duration), size: CGSize(width: CGFloat(w), height: CGFloat(h)), flags: [])) - case let .documentAttributeAudio(flags, duration, title, performer, waveform): - let isVoice = (flags & (1 << 10)) != 0 - var waveformBuffer: MemoryBuffer? - if let waveform = waveform { - waveformBuffer = MemoryBuffer(waveform) - } - parsedAttributes.append(.Audio(isVoice: isVoice, duration: Int(duration), title: title, performer: performer, waveform: waveformBuffer)) - default: - break - } - } - - var previewRepresentations: [TelegramMediaImageRepresentation] = [] - - switch thumb { - case let .photoCachedSize(_, _, w, h, bytes): - - let resource = LocalFileMediaResource(fileId: arc4random64()) - previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: NSMakeSize(CGFloat(w), CGFloat(h)), resource: resource)) - resources.append((resource, bytes.makeData())) - default: - break - } - - let resource = SecretFileMediaResource(fileId: id, accessHash: accessHash, size: nil, decryptedSize: size, datacenterId: Int(dcId), key: key) - - - let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: id), resource: resource, previewRepresentations: previewRepresentations, mimeType: mimeType, size: Int(size), attributes: parsedAttributes) - media.append(fileMedia) - - } - default: - break - } - default: - break - } - - var attributes:[MessageAttribute] = [] - - if message.ttlSeconds > 0 { - var countdownBeginTime:Int32? = nil - if message.destructionTime > 0 { - countdownBeginTime = message.destructionTime - message.ttlSeconds - } - attributes.append(AutoremoveTimeoutMessageAttribute(timeout: message.ttlSeconds, countdownBeginTime: countdownBeginTime)) - } - - var flags:StoreMessageFlags = StoreMessageFlags() - - if message.fromId != userId { - flags.insert(.Incoming) - } - let tags = tagsForStoreMessage(incoming: message.fromId == peer.id.id, attributes: [], media: media, textEntities: nil) - messages.append(StoreMessage(id: MessageId(peerId: peer.id, namespace: Namespaces.Message.SecretIncoming, id: message.id), globallyUniqueId: message.random, timestamp: message.date, flags: flags, tags: tags.0, globalTags: tags.1, forwardInfo: nil, authorId: PeerId(namespace: Namespaces.Peer.CloudUser, id: message.fromId), text: text, attributes: attributes, media: media)) - } - } - return true - }) - } - - let participantIds = participantPeerIds.map({String($0.id)}).joined(separator: ",") - sql.select("select serialized from users where n_id in (\(participantIds))", { value -> Bool in - - if let user = MacosLegacy.parse(Buffer(data: value.getData(at: 0))) as? MacosLegacy.User { - switch user { - case let .user(user): - var representations:[TelegramMediaImageRepresentation] = [] - if let photo = user.photo { - switch photo { - case let .userProfilePhoto(photo): - if let small = mediaResourceFromApiFileLocation(photo.photoSmall, size: nil) { - representations.append(TelegramMediaImageRepresentation(dimensions: CGSize(width: 100, height: 100), resource: small)) - } - if let big = mediaResourceFromApiFileLocation(photo.photoBig, size: nil) { - representations.append(TelegramMediaImageRepresentation(dimensions: CGSize(width: 100, height: 100), resource: big)) - } - default: - break - } - } - peers.append(TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: user.id), accessHash: user.accessHash, firstName: user.firstName, lastName: user.lastName, username: user.username, phone: user.phone, photo: representations, botInfo: nil, flags: [])) - default: - break - } - } - return true - }) - - - } - - } - - return .data(masterDatacenterId: masterDatacenterId, userId: userId, groups: groupsData, peers: peers, secretMessages: messages, resources: resources, secretState: secretState, passcode: textPasscode) - } - - return .none -} - -private func legacyMediaImageRepresentationsFromApiSizes(_ sizes: [MacosLegacy.PhotoSize]) -> [TelegramMediaImageRepresentation] { - var representations: [TelegramMediaImageRepresentation] = [] - for size in sizes { - switch size { - case let .photoCachedSize(_, location, w, h, bytes): - if let resource = mediaResourceFromApiFileLocation(location, size: bytes.size) { - representations.append(TelegramMediaImageRepresentation(dimensions: CGSize(width: CGFloat(w), height: CGFloat(h)), resource: resource)) - } - case let .photoSize(_, location, w, h, size): - if let resource = mediaResourceFromApiFileLocation(location, size: Int(size)) { - representations.append(TelegramMediaImageRepresentation(dimensions: CGSize(width: CGFloat(w), height: CGFloat(h)), resource: resource)) - } - case .photoSizeEmpty: - break - } - } - return representations -} - -private func mediaResourceFromApiFileLocation(_ fileLocation: MacosLegacy.FileLocation, size: Int?) -> TelegramMediaResource? { - switch fileLocation { - case let .fileLocation(dcId, volumeId, localId, secret): - return CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: size) - case .fileLocationUnavailable: - return nil - } -} - -private var legacySqlPath: String { - - let applicationSupportPath = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)[0] - let path = applicationSupportPath + "/" + "Telegram" + "/database/" - #if APP_STORE - return path + "encrypted.sqlite" - #else - return path + "encrypted6.sqlite" - #endif -} - -private var legacyYapPath: String { - let applicationSupportPath = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)[0] - return applicationSupportPath + "/" + "Telegram" + "/database/" + "yap_store-t143.sqlite" -} - -func clearLegacyData() { - let applicationSupportPath = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)[0] - let applicationName = "Telegram" - let dataDirectory = applicationSupportPath + "/\(applicationName)/encrypt-mtkeychain" - try? FileManager.default.removeItem(atPath: dataDirectory) -} - -func emptyPasscodeData() -> Data { - - var data:Data = Data() - var zero:UInt8 = 0 - for _ in 0 ..< 64 { - data.append(&zero, count: 1) - } - return data -} diff --git a/Telegram-Mac/LinkHoverController.swift b/Telegram-Mac/LinkHoverController.swift new file mode 100644 index 0000000000..ca09637c37 --- /dev/null +++ b/Telegram-Mac/LinkHoverController.swift @@ -0,0 +1,14 @@ +// +// LinkHoverController.swift +// Telegram +// +// Created by Mikhail Filimonov on 13.11.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa + +class LinkHoverController: NSObject { + + +} diff --git a/Telegram-Mac/LinkInvationController.swift b/Telegram-Mac/LinkInvationController.swift index 0f5df59748..09a4e999fd 100644 --- a/Telegram-Mac/LinkInvationController.swift +++ b/Telegram-Mac/LinkInvationController.swift @@ -9,9 +9,10 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit private enum GroupLinkInvationEntryStableId : Hashable { case section(Int) @@ -28,29 +29,6 @@ private enum GroupLinkInvationEntryStableId : Hashable { return 1000000 } } - - static func ==(lhs:GroupLinkInvationEntryStableId, rhs:GroupLinkInvationEntryStableId) -> Bool { - switch lhs { - case let .section(id): - if case .section(id) = rhs { - return true - } else { - return false - } - case let .index(id): - if case .index(id) = rhs { - return true - } else { - return false - } - case .loading: - if case .loading = rhs { - return true - } else { - return false - } - } - } } private enum GroupLinkInvationEntry : Identifiable, Comparable { @@ -136,13 +114,13 @@ private func <(lhs:GroupLinkInvationEntry, rhs:GroupLinkInvationEntry) -> Bool { } final class GroupLinkInvationArguments { - let account:Account + let context: AccountContext let copy:()->Void let share:()->Void let revoke:()->Void - init(account:Account, copy:@escaping()->Void, share:@escaping()->Void, revoke:@escaping()->Void) { - self.account = account + init(context: AccountContext, copy:@escaping()->Void, share:@escaping()->Void, revoke:@escaping()->Void) { + self.context = context self.copy = copy self.share = share self.revoke = revoke @@ -178,21 +156,21 @@ private func groupInvationEntries(view:PeerView, arguments:GroupLinkInvationArgu entries.append(.link(sectionId: sectionId, uniqueIdx: uniqueId, text: link)) uniqueId += 1 - entries.append(.text(sectionId: sectionId, uniqueIdx: uniqueId, text: isGroup ? tr(.groupInvationGroupDescription) : tr(.groupInvationChannelDescription))) + entries.append(.text(sectionId: sectionId, uniqueIdx: uniqueId, text: isGroup ? L10n.groupInvationGroupDescription : L10n.groupInvationChannelDescription)) uniqueId += 1 entries.append(.section(sectionId: sectionId)) sectionId += 1 - entries.append(.action(sectionId: sectionId, uniqueIdx: uniqueId, text: tr(.groupInvationCopyLink), callback: { + entries.append(.action(sectionId: sectionId, uniqueIdx: uniqueId, text: L10n.groupInvationCopyLink, callback: { arguments.copy() })) uniqueId += 1 - entries.append(.action(sectionId: sectionId, uniqueIdx: uniqueId, text: tr(.groupInvationRevoke), callback: { + entries.append(.action(sectionId: sectionId, uniqueIdx: uniqueId, text: L10n.groupInvationRevoke, callback: { arguments.revoke() })) uniqueId += 1 - entries.append(.action(sectionId: sectionId, uniqueIdx: uniqueId, text: tr(.groupInvationShare), callback: { + entries.append(.action(sectionId: sectionId, uniqueIdx: uniqueId, text: L10n.groupInvationShare, callback: { arguments.share() })) uniqueId += 1 @@ -231,36 +209,37 @@ class LinkInvationController: TableViewController { private let revokeLinkDisposable = MetaDisposable() private let disposable:MetaDisposable = MetaDisposable() - init(account:Account, peerId:PeerId) { + init(_ context: AccountContext, peerId:PeerId) { self.peerId = peerId - super.init(account) + super.init(context) } override func viewDidLoad() { super.viewDidLoad() - let account = self.account + let context = self.context let peerId = self.peerId let link:Atomic = Atomic(value: nil) let peer:Atomic = Atomic(value: nil) - let arguments = GroupLinkInvationArguments(account: account, copy: { [weak self] in + let arguments = GroupLinkInvationArguments(context: context, copy: { [weak self] in if let link = link.modify({$0}) { copyToClipboard(link) - self?.show(toaster: ControllerToaster(text: tr(.shareLinkCopied))) + self?.show(toaster: ControllerToaster(text: tr(L10n.shareLinkCopied))) } }, share: { if let link = link.modify({$0}) { - showModal(with: ShareModalController(ShareLinkObject(account, link: link)), for: mainWindow) + showModal(with: ShareModalController(ShareLinkObject(context, link: link)), for: mainWindow) } }, revoke: { [weak self] in - if let peer = peer.modify({$0}), let account = self?.account { - let info = peer.isChannel ? tr(.linkInvationChannelConfirmRevoke) : tr(.linkInvationGroupConfirmRevoke) - let signal = confirmSignal(for: mainWindow, header: appName, information: info, okTitle: tr(.linkInvationConfirmOk)) + if let peer = peer.modify({$0}), let context = self?.context { + let info = peer.isChannel ? L10n.linkInvationChannelConfirmRevoke : L10n.linkInvationGroupConfirmRevoke + let signal = confirmSignal(for: mainWindow, information: info, okTitle: L10n.linkInvationConfirmOk) |> filter {$0} - |> mapToSignal { _ -> Signal in - return ensuredExistingPeerExportedInvitation(account: account, peerId: peer.id, revokeExisted: true) + |> mapToSignal { _ -> Signal in + + return context.engine.peers.revokePersistentPeerExportedInvitation(peerId: peer.id) |> map { _ in return } } self?.revokeLinkDisposable.set(signal.start()) } @@ -268,7 +247,7 @@ class LinkInvationController: TableViewController { let previous:Atomic<[GroupLinkInvationEntry]> = Atomic(value: []) let atomicSize = self.atomicSize - let apply = account.viewTracker.peerView( peerId) |> deliverOn(prepareQueue) |> map { view -> TableUpdateTransition in + let apply = context.account.viewTracker.peerView( peerId) |> deliverOn(prepareQueue) |> map { view -> TableUpdateTransition in let exportLink:String? if let cachedData = view.cachedData as? CachedChannelData { @@ -290,8 +269,6 @@ class LinkInvationController: TableViewController { self?.genericView.merge(with: transition) self?.readyOnce() })) - - revokeLinkDisposable.set(ensuredExistingPeerExportedInvitation(account: account, peerId: peerId).start()) } deinit { diff --git a/Telegram-Mac/ListViewIntermediateState.swift b/Telegram-Mac/ListViewIntermediateState.swift index e7d1a2d357..10c1978591 100644 --- a/Telegram-Mac/ListViewIntermediateState.swift +++ b/Telegram-Mac/ListViewIntermediateState.swift @@ -8,7 +8,7 @@ import Cocoa -import SwiftSignalKitMac +import SwiftSignalKit public enum ListViewCenterScrollPositionOverflow { case Top diff --git a/Telegram-Mac/LiveLocationViewController.swift b/Telegram-Mac/LiveLocationViewController.swift new file mode 100644 index 0000000000..9a92108851 --- /dev/null +++ b/Telegram-Mac/LiveLocationViewController.swift @@ -0,0 +1,133 @@ +// +// LiveLocationViewController.swift +// Telegram +// +// Created by Mikhail Filimonov on 16/10/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import MapKit +import TelegramCore + +import SwiftSignalKit +import Postbox + +private final class LocationPreviewArguments { + let context:AccountContext + init(context: AccountContext) { + self.context = context + } +} + +extension TelegramMediaMap : Equatable { + public static func == (lhs: TelegramMediaMap, rhs: TelegramMediaMap) -> Bool { + return lhs.heading == rhs.heading && lhs.longitude == rhs.longitude && lhs.latitude == rhs.latitude + } +} + +private struct LocationPreviewState : Equatable { + static func == (lhs: LocationPreviewState, rhs: LocationPreviewState) -> Bool { + if let lhsPeer = lhs.peer, let rhsPeer = rhs.peer { + if !lhsPeer.isEqual(rhsPeer) { + return false + } + } else if (lhs.peer != nil) != (rhs.peer != nil) { + return false + } + return lhs.map == rhs.map + } + + let map: TelegramMediaMap + let peer: Peer? + init(map: TelegramMediaMap, peer: Peer?) { + self.map = map + self.peer = peer + } + func withUpdatedMap(_ map: TelegramMediaMap) -> LocationPreviewState { + return LocationPreviewState(map: map, peer: self.peer) + } + func withUpdatedPeer(_ peer: Peer?) -> LocationPreviewState { + return LocationPreviewState(map: self.map, peer: peer) + } +} + +private let _id_map = InputDataIdentifier("_id_map") +@available(macOS 10.13, *) +private func entries(_ state:LocationPreviewState, arguments: LocationPreviewArguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId: Int32 = 0 + var index: Int32 = 0 + + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_map, equatable: InputDataEquatable(state.map), comparable: nil, item: { initialSize, stableId in + return LocationPreviewMapRowItem(initialSize, height: 330, stableId: stableId, context: arguments.context, map: state.map, peer: state.peer, viewType: .legacy) + })) + index += 1 + + return entries +} +@available(macOS 10.13, *) +func LocationModalPreview(_ context: AccountContext, map mapValue: TelegramMediaMap, peer: Peer?, messageId: MessageId) -> InputDataModalController { + + let initialState = LocationPreviewState(map: mapValue, peer: peer) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((LocationPreviewState) -> LocationPreviewState) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let arguments = LocationPreviewArguments(context: context) + + let messageView = context.account.postbox.messageView(messageId) |> map { + $0.message + } + + let disposable = messageView.start(next: { message in + updateState { value in + var value = value.withUpdatedPeer(message?.effectiveAuthor) + if let map = message?.media.first as? TelegramMediaMap { + value = value.withUpdatedMap(map) + } + return value + } + }) + + let signal = statePromise.get() |> map { state in + return InputDataSignalValue(entries: entries(state, arguments: arguments)) + } + + let controller = InputDataController(dataSignal: signal, title: "Location Preview") + + controller.afterDisappear = { + disposable.dispose() + } + + var close: (()->Void)? = nil + + let modalInteractions = ModalInteractions(acceptTitle: "Open in Google Maps", accept: { + close?() + execute(inapp: .external(link: "https://maps.google.com/maps?q=\(String(format:"%f", stateValue.with { $0.map.latitude })),\(String(format:"%f", stateValue.with { $0.map.longitude }))", false)) + }, height: 50, singleButton: true) + + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { + close?() + }) + + controller.updateDatas = { data in + + return .none + } + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions, closeHandler: { f in f() }, size: NSMakeSize(300, 330)) + + close = { [weak modalController] in + modalController?.close() + } + + return modalController +} diff --git a/Telegram-Mac/LoadingTableItem.swift b/Telegram-Mac/LoadingTableItem.swift index 9a802cc59a..78af24e790 100644 --- a/Telegram-Mac/LoadingTableItem.swift +++ b/Telegram-Mac/LoadingTableItem.swift @@ -11,11 +11,10 @@ import TGUIKit class LoadingTableItem: GeneralRowItem { - init(_ initialSize: NSSize, height: CGFloat, stableId: AnyHashable) { - super.init(initialSize, height: height, stableId: stableId) + init(_ initialSize: NSSize, height: CGFloat, stableId: AnyHashable, viewType: GeneralViewType = .legacy) { + super.init(initialSize, height: height, stableId: stableId, viewType: viewType) } - override func viewClass() -> AnyClass { return LoadingTableRowView.self } @@ -23,11 +22,13 @@ class LoadingTableItem: GeneralRowItem { class LoadingTableRowView : TableRowView { + private let containerView = GeneralRowContainerView(frame: NSZeroRect) private let progress: ProgressIndicator = ProgressIndicator(frame: NSMakeRect(0, 0, 20, 20)) required init(frame frameRect: NSRect) { super.init(frame: frameRect) - addSubview(progress) + containerView.addSubview(progress) + addSubview(containerView) } override func viewDidMoveToWindow() { @@ -38,9 +39,30 @@ class LoadingTableRowView : TableRowView { } } + override var backdorColor: NSColor { + return theme.colors.background + } + + override func updateColors() { + if let item = item as? GeneralRowItem { + containerView.background = backdorColor + backgroundColor = item.viewType.rowBackground + } + } + override func layout() { super.layout() - progress.center() + + if let item = item as? GeneralRowItem { + switch item.viewType { + case .legacy: + containerView.frame = bounds + case .modern: + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) + } + self.containerView.setCorners(item.viewType.corners) + progress.center() + } } deinit { @@ -49,6 +71,19 @@ class LoadingTableRowView : TableRowView { override func set(item: TableRowItem, animated: Bool) { super.set(item: item, animated: animated) + + if let item = item as? GeneralRowItem { + let contentRect: NSRect + switch item.viewType { + case .legacy: + contentRect = bounds + case .modern: + contentRect = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) + } + self.containerView.change(size: contentRect.size, animated: animated, corners: item.viewType.corners) + self.containerView.change(pos: contentRect.origin, animated: animated) + } + } required init?(coder: NSCoder) { diff --git a/Telegram-Mac/Localizable.swift b/Telegram-Mac/Localizable.swift index f93dfb2807..8b8278662d 100644 --- a/Telegram-Mac/Localizable.swift +++ b/Telegram-Mac/Localizable.swift @@ -1,4246 +1,10137 @@ -// Generated using SwiftGen, by O.Halligon — https://github.com/AliSoftware/SwiftGen +// Generated using SwiftGen, by O.Halligon — https://github.com/SwiftGen/SwiftGen import Foundation +// swiftlint:disable superfluous_disable_command // swiftlint:disable file_length -// swiftlint:disable line_length -// swiftlint:disable type_body_length -enum L10n { +// swiftlint:disable identifier_name line_length type_body_length +internal final class L10n { + /// %1$@ sent an invoice for %3$@ to the group %2$@ + internal static func chatMessageInvoice(_ p1: String, _ p2: String, _ p3: String) -> String { + return L10n.tr("Localizable", "CHAT_MESSAGE_INVOICE", p1, p2, p3) + } /// Default - case defaultSoundName + internal static var defaultSoundName: String { return L10n.tr("Localizable", "DefaultSoundName") } + /// %1$@ sent you an invoice for %2$@ + internal static func messageInvoice(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "MESSAGE_INVOICE", p1, p2) + } /// None - case notificationSettingsToneNone - /// incorrect password - case passwordHashInvalid - /// code expired - case phoneCodeExpired - /// phone code invalid - case phoneCodeInvalid - /// invalid phone number - case phoneNumberInvalid + internal static var notificationSettingsToneNone: String { return L10n.tr("Localizable", "NotificationSettingsToneNone") } + /// Incorrect password + internal static var passwordHashInvalid: String { return L10n.tr("Localizable", "PASSWORD_HASH_INVALID") } + /// Code expired + internal static var phoneCodeExpired: String { return L10n.tr("Localizable", "PHONE_CODE_EXPIRED") } + /// Invalid code + internal static var phoneCodeInvalid: String { return L10n.tr("Localizable", "PHONE_CODE_INVALID") } + /// Invalid phone number + internal static var phoneNumberInvalid: String { return L10n.tr("Localizable", "PHONE_NUMBER_INVALID") } + /// %1$@ pinned an invoice + internal static func pinnedInvoice(_ p1: String) -> String { + return L10n.tr("Localizable", "PINNED_INVOICE", p1) + } + /// Share Call Logs + internal static var shareCallLogs: String { return L10n.tr("Localizable", "ShareCallLogs") } + /// An error occurred. Please try again later + internal static var unknownError: String { return L10n.tr("Localizable", "UnknownError") } /// You - case you + internal static var you: String { return L10n.tr("Localizable", "You") } + /// Your card has expired. + internal static var yourCardHasExpired: String { return L10n.tr("Localizable", "Your_card_has_expired") } + /// Your card was declined. + internal static var yourCardWasDeclined: String { return L10n.tr("Localizable", "Your_card_was_declined") } + /// You've entered an invalid expiration month. + internal static var yourCardsExpirationMonthIsInvalid: String { return L10n.tr("Localizable", "Your_cards_expiration_month_is_invalid") } + /// You've entered an invalid expiration year. + internal static var yourCardsExpirationYearIsInvalid: String { return L10n.tr("Localizable", "Your_cards_expiration_year_is_invalid") } + /// You've entered an invalid card number. + internal static var yourCardsNumberIsInvalid: String { return L10n.tr("Localizable", "Your_cards_number_is_invalid") } + /// You've entered an invalid security code. + internal static var yourCardsSecurityCodeIsInvalid: String { return L10n.tr("Localizable", "Your_cards_security_code_is_invalid") } + /// You've entered an invalid zip code. + internal static var yourCardsZipCodeIsInvalid: String { return L10n.tr("Localizable", "Your_cards_zip_code_is_invalid") } /// Check for Updates - case _1000Title + internal static var _1000Title: String { return L10n.tr("Localizable", "1000.title") } /// Telegram - case _1XtHYUBwTitle + internal static var _1XtHYUBwTitle: String { return L10n.tr("Localizable", "1Xt-HY-uBw.title") } /// Transformations - case _2oIRnZJCTitle + internal static var _2oIRnZJCTitle: String { return L10n.tr("Localizable", "2oI-Rn-ZJC.title") } /// Enter Full Screen - case _4J7DPTxaTitle + internal static var _4J7DPTxaTitle: String { return L10n.tr("Localizable", "4J7-dP-txa.title") } /// Quit Telegram - case _4sb4sVLiTitle - /// About Telegram - case _5kVVbQxSTitle + internal static var _4sb4sVLiTitle: String { return L10n.tr("Localizable", "4sb-4s-VLi.title") } /// Edit - case _5QFOaP0TTitle + internal static var _5QFOaP0TTitle: String { return L10n.tr("Localizable", "5QF-Oa-p0T.title") } + /// About Telegram + internal static var _5kVVbQxSTitle: String { return L10n.tr("Localizable", "5kV-Vb-QxS.title") } /// Redo - case _6dhZSVamTitle + internal static var _6dhZSVamTitle: String { return L10n.tr("Localizable", "6dh-zS-Vam.title") } /// Correct Spelling Automatically - case _78YHA62vTitle + internal static var _78YHA62vTitle: String { return L10n.tr("Localizable", "78Y-hA-62v.title") } /// Substitutions - case _9icFLObxTitle + internal static var _9icFLObxTitle: String { return L10n.tr("Localizable", "9ic-FL-obx.title") } /// Smart Copy/Paste - case _9yt4BNSMTitle + internal static var _9yt4BNSMTitle: String { return L10n.tr("Localizable", "9yt-4B-nSM.title") } /// Free messaging app for macOS based on MTProto for speed and security. - case aboutDescription - /// 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. - case accountConfirmAskQuestion + internal static var aboutDescription: String { return L10n.tr("Localizable", "About.Description") } + /// Tinted + internal static var accentColorsTinted: String { return L10n.tr("Localizable", "AccentColors.Tinted") } + /// Please note that Telegram Support is run 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. + internal static var accountConfirmAskQuestion: String { return L10n.tr("Localizable", "Account.Confirm.AskQuestion") } /// Open FAQ - case accountConfirmGoToFaq + internal static var accountConfirmGoToFaq: String { return L10n.tr("Localizable", "Account.Confirm.GoToFaq") } /// Log out? - case accountConfirmLogout + internal static var accountConfirmLogout: String { return L10n.tr("Localizable", "Account.Confirm.Logout") } /// Remember, logging out cancels all your Secret Chats. - case accountConfirmLogoutText - /// New Account - case accountsControllerNewAccount + internal static var accountConfirmLogoutText: String { return L10n.tr("Localizable", "Account.Confirm.LogoutText") } /// About - case accountSettingsAbout - /// Appearance - case accountSettingsAppearance + internal static var accountSettingsAbout: String { return L10n.tr("Localizable", "AccountSettings.About") } + /// Add Account + internal static var accountSettingsAddAccount: String { return L10n.tr("Localizable", "AccountSettings.AddAccount") } /// Ask a Question - case accountSettingsAskQuestion + internal static var accountSettingsAskQuestion: String { return L10n.tr("Localizable", "AccountSettings.AskQuestion") } /// Bio - case accountSettingsBio + internal static var accountSettingsBio: String { return L10n.tr("Localizable", "AccountSettings.Bio") } /// English - case accountSettingsCurrentLanguage + internal static var accountSettingsCurrentLanguage: String { return L10n.tr("Localizable", "AccountSettings.CurrentLanguage") } + /// Data and Storage + internal static var accountSettingsDataAndStorage: String { return L10n.tr("Localizable", "AccountSettings.DataAndStorage") } + /// Delete Account + internal static var accountSettingsDeleteAccount: String { return L10n.tr("Localizable", "AccountSettings.DeleteAccount") } /// Telegram FAQ - case accountSettingsFAQ + internal static var accountSettingsFAQ: String { return L10n.tr("Localizable", "AccountSettings.FAQ") } + /// Chat Folders + internal static var accountSettingsFilters: String { return L10n.tr("Localizable", "AccountSettings.Filters") } /// General - case accountSettingsGeneral + internal static var accountSettingsGeneral: String { return L10n.tr("Localizable", "AccountSettings.General") } /// Language - case accountSettingsLanguage + internal static var accountSettingsLanguage: String { return L10n.tr("Localizable", "AccountSettings.Language") } /// Logout - case accountSettingsLogout + internal static var accountSettingsLogout: String { return L10n.tr("Localizable", "AccountSettings.Logout") } /// Notifications - case accountSettingsNotifications + internal static var accountSettingsNotifications: String { return L10n.tr("Localizable", "AccountSettings.Notifications") } + /// Telegram Passport + internal static var accountSettingsPassport: String { return L10n.tr("Localizable", "AccountSettings.Passport") } /// Privacy and Security - case accountSettingsPrivacyAndSecurity + internal static var accountSettingsPrivacyAndSecurity: String { return L10n.tr("Localizable", "AccountSettings.PrivacyAndSecurity") } + /// Proxy + internal static var accountSettingsProxy: String { return L10n.tr("Localizable", "AccountSettings.Proxy") } + /// Read Articles + internal static var accountSettingsReadArticles: String { return L10n.tr("Localizable", "AccountSettings.ReadArticles") } /// Set a Bio - case accountSettingsSetBio + internal static var accountSettingsSetBio: String { return L10n.tr("Localizable", "AccountSettings.SetBio") } /// Set Profile Photo - case accountSettingsSetProfilePhoto + internal static var accountSettingsSetProfilePhoto: String { return L10n.tr("Localizable", "AccountSettings.SetProfilePhoto") } /// Set a Username - case accountSettingsSetUsername + internal static var accountSettingsSetUsername: String { return L10n.tr("Localizable", "AccountSettings.SetUsername") } /// Stickers - case accountSettingsStickers - /// Storage - case accountSettingsStorage + internal static var accountSettingsStickers: String { return L10n.tr("Localizable", "AccountSettings.Stickers") } + /// Appearance + internal static var accountSettingsTheme: String { return L10n.tr("Localizable", "AccountSettings.Theme") } /// Username - case accountSettingsUsername + internal static var accountSettingsUsername: String { return L10n.tr("Localizable", "AccountSettings.Username") } + /// Gram Wallet + internal static var accountSettingsWallet: String { return L10n.tr("Localizable", "AccountSettings.Wallet") } + /// Connected + internal static var accountSettingsProxyConnected: String { return L10n.tr("Localizable", "AccountSettings.Proxy.Connected") } + /// Connecting + internal static var accountSettingsProxyConnecting: String { return L10n.tr("Localizable", "AccountSettings.Proxy.Connecting") } + /// Disabled + internal static var accountSettingsProxyDisabled: String { return L10n.tr("Localizable", "AccountSettings.Proxy.Disabled") } + /// Update + internal static var accountViewControllerUpdate: String { return L10n.tr("Localizable", "AccountViewController.Update") } + /// failed + internal static var accountViewControllerDescFailed: String { return L10n.tr("Localizable", "AccountViewController.Desc.Failed") } + /// updated + internal static var accountViewControllerDescUpdated: String { return L10n.tr("Localizable", "AccountViewController.Desc.Updated") } + /// New Account + internal static var accountsControllerNewAccount: String { return L10n.tr("Localizable", "AccountsController.NewAccount") } /// Add Admin - case adminsAddAdmin + internal static var adminsAddAdmin: String { return L10n.tr("Localizable", "Admins.AddAdmin") } /// Admin - case adminsAdmin + internal static var adminsAdmin: String { return L10n.tr("Localizable", "Admins.Admin") } /// CHANNEL ADMINS - case adminsChannelAdmins + internal static var adminsChannelAdmins: String { return L10n.tr("Localizable", "Admins.ChannelAdmins") } /// You can add admins to help you manage your channel - case adminsChannelDescription - /// Creator - case adminsCreator - /// Everybody can add new members - case adminsEverbodyCanAddMembers + internal static var adminsChannelDescription: String { return L10n.tr("Localizable", "Admins.ChannelDescription") } + /// Any member can add new members + internal static var adminsEverbodyCanAddMembers: String { return L10n.tr("Localizable", "Admins.EverbodyCanAddMembers") } /// GROUP ADMINS - case adminsGroupAdmins - /// You can add admins to help you manage your group - case adminsGroupDescription - /// Only Admins can add new members - case adminsOnlyAdminsCanAddMembers + internal static var adminsGroupAdmins: String { return L10n.tr("Localizable", "Admins.GroupAdmins") } + /// You can add admins to help you manage your group. + internal static var adminsGroupDescription: String { return L10n.tr("Localizable", "Admins.GroupDescription") } + /// Only admins can add new members. + internal static var adminsOnlyAdminsCanAddMembers: String { return L10n.tr("Localizable", "Admins.OnlyAdminsCanAddMembers") } + /// Owner + internal static var adminsOwner: String { return L10n.tr("Localizable", "Admins.Owner") } /// Contacts - case adminsSelectNewAdminTitle + internal static var adminsSelectNewAdminTitle: String { return L10n.tr("Localizable", "Admins.SelectNewAdminTitle") } /// Only Admins - case adminsWhoCanInviteAdmins + internal static var adminsWhoCanInviteAdmins: String { return L10n.tr("Localizable", "Admins.WhoCanInvite.Admins") } /// All Members - case adminsWhoCanInviteEveryone + internal static var adminsWhoCanInviteEveryone: String { return L10n.tr("Localizable", "Admins.WhoCanInvite.Everyone") } /// Who can add members - case adminsWhoCanInviteText + internal static var adminsWhoCanInviteText: String { return L10n.tr("Localizable", "Admins.WhoCanInvite.Text") } /// Cancel - case alertCancel + internal static var alertCancel: String { return L10n.tr("Localizable", "Alert.Cancel") } + /// Discard + internal static var alertDiscard: String { return L10n.tr("Localizable", "Alert.Discard") } + /// No + internal static var alertNO: String { return L10n.tr("Localizable", "Alert.NO") } /// OK - case alertOK + internal static var alertOK: String { return L10n.tr("Localizable", "Alert.OK") } /// Sorry, this user doesn't seem to exist. - case alertUserDoesntExists - /// Can't forward messages to this conversation. - case alertForwardError + internal static var alertUserDoesntExists: String { return L10n.tr("Localizable", "Alert.UserDoesntExists") } + /// Yes + internal static var alertYes: String { return L10n.tr("Localizable", "Alert.Yes") } + /// Update App + internal static var alertButtonOKUpdateApp: String { return L10n.tr("Localizable", "Alert.ButtonOK.UpdateApp") } + /// Discard + internal static var alertConfirmDiscard: String { return L10n.tr("Localizable", "Alert.Confirm.Discard") } + /// Stop + internal static var alertConfirmStop: String { return L10n.tr("Localizable", "Alert.Confirm.Stop") } + /// Sorry, you can't forward messages to this conversation. + internal static var alertForwardError: String { return L10n.tr("Localizable", "Alert.Forward.Error") } + /// Cancel + internal static var alertHideNewChatsCancel: String { return L10n.tr("Localizable", "Alert.HideNewChats.Cancel") } + /// Hide new chats? + internal static var alertHideNewChatsHeader: String { return L10n.tr("Localizable", "Alert.HideNewChats.Header") } + /// Go to Settings + internal static var alertHideNewChatsOK: String { return L10n.tr("Localizable", "Alert.HideNewChats.OK") } + /// You are receiving lots of new chats from users who are not in your Contact List. Do you want to have such chats automatically muted and archived? + internal static var alertHideNewChatsText: String { return L10n.tr("Localizable", "Alert.HideNewChats.Text") } + /// Unfortunately, you can't access this message. You are not a member of the chat where it was posted. + internal static var alertPrivateChannelAccessError: String { return L10n.tr("Localizable", "Alert.PrivateChannel.AccessError") } /// Delete - case alertSendErrorDelete + internal static var alertSendErrorDelete: String { return L10n.tr("Localizable", "Alert.SendError.Delete") } /// Your message could not be sent - case alertSendErrorHeader + internal static var alertSendErrorHeader: String { return L10n.tr("Localizable", "Alert.SendError.Header") } /// Ignore - case alertSendErrorIgnore + internal static var alertSendErrorIgnore: String { return L10n.tr("Localizable", "Alert.SendError.Ignore") } /// Resend - case alertSendErrorResend - /// An error occurred while sending the previous message. Would you like to resend it ? - case alertSendErrorText - /// Maximum file size is 1.5 GB - case appMaxFileSize + internal static var alertSendErrorResend: String { return L10n.tr("Localizable", "Alert.SendError.Resend") } + /// %d + internal static func alertSendErrorResendItemsCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Alert.SendError.ResendItems_countable", p1) + } + /// Resend %d messages + internal static func alertSendErrorResendItemsFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Alert.SendError.ResendItems_few", p1) + } + /// Resend %d messages + internal static func alertSendErrorResendItemsMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Alert.SendError.ResendItems_many", p1) + } + /// Resend %d message + internal static func alertSendErrorResendItemsOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Alert.SendError.ResendItems_one", p1) + } + /// Resend %d messages + internal static func alertSendErrorResendItemsOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Alert.SendError.ResendItems_other", p1) + } + /// Resend %d messages + internal static func alertSendErrorResendItemsTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Alert.SendError.ResendItems_two", p1) + } + /// Resend %d messages + internal static func alertSendErrorResendItemsZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Alert.SendError.ResendItems_zero", p1) + } + /// An error occurred while sending the previous message. Would you like to try resending it? + internal static var alertSendErrorText: String { return L10n.tr("Localizable", "Alert.SendError.Text") } + /// Maximum file size is 2.0 GB + internal static var appMaxFileSize1: String { return L10n.tr("Localizable", "App.MaxFileSize1") } + /// Hold to record video. Click to switch to audio + internal static var appTooltipVideoRecord: String { return L10n.tr("Localizable", "App.Tooltip.VideoRecord") } + /// Hold to record audio. Click to switch to video + internal static var appTooltipVoiceRecord: String { return L10n.tr("Localizable", "App.Tooltip.VoiceRecord") } + /// Check for Updates + internal static var appUpdateCheckForUpdates: String { return L10n.tr("Localizable", "AppUpdate.CheckForUpdates") } + /// Downloading... + internal static var appUpdateDownloading: String { return L10n.tr("Localizable", "AppUpdate.Downloading") } + /// Download Update + internal static var appUpdateDownloadUpdate: String { return L10n.tr("Localizable", "AppUpdate.DownloadUpdate") } + /// Please update the app to get the latest features and improvements. + internal static var appUpdateNewestAvailable: String { return L10n.tr("Localizable", "AppUpdate.NewestAvailable") } + /// Retrieving Information... + internal static var appUpdateRetrievingInfo: String { return L10n.tr("Localizable", "AppUpdate.RetrievingInfo") } + /// Updates + internal static var appUpdateTitle: String { return L10n.tr("Localizable", "AppUpdate.Title") } + /// Unarchiving... + internal static var appUpdateUnarchiving: String { return L10n.tr("Localizable", "AppUpdate.Unarchiving") } + /// You have the latest version of Telegram. + internal static var appUpdateUptodate: String { return L10n.tr("Localizable", "AppUpdate.Uptodate") } + /// NEW VERSION (your version: %@) + internal static func appUpdateTitleNew(_ p1: String) -> String { + return L10n.tr("Localizable", "AppUpdate.Title.New", p1) + } + /// PREVIOUS VERSIONS + internal static var appUpdateTitlePrevious: String { return L10n.tr("Localizable", "AppUpdate.Title.Previous") } + /// CLOUD THEMES + internal static var appearanceCloudThemes: String { return L10n.tr("Localizable", "Appearance.CloudThemes") } + /// Custom Background + internal static var appearanceCustomBackground: String { return L10n.tr("Localizable", "Appearance.CustomBackground") } + /// Export Theme + internal static var appearanceExportTheme: String { return L10n.tr("Localizable", "Appearance.ExportTheme") } + /// New Theme + internal static var appearanceNewTheme: String { return L10n.tr("Localizable", "Appearance.NewTheme") } + /// Reset to Defaults + internal static var appearanceReset: String { return L10n.tr("Localizable", "Appearance.Reset") } + /// Incompatible with macOS, click to edit + internal static var appearanceCloudThemeUnsupported: String { return L10n.tr("Localizable", "Appearance.CloudTheme.Unsupported") } + /// Remove + internal static var appearanceConfirmRemoveOK: String { return L10n.tr("Localizable", "Appearance.Confirm.RemoveOK") } + /// Are you sure you want to delete this theme? + internal static var appearanceConfirmRemoveText: String { return L10n.tr("Localizable", "Appearance.Confirm.RemoveText") } + /// Theme + internal static var appearanceConfirmRemoveTitle: String { return L10n.tr("Localizable", "Appearance.Confirm.RemoveTitle") } + /// The file size must not exceed 2MB and the image dimensions must not exceed 500x500px. + internal static var appearanceCustomBackgroundFileError: String { return L10n.tr("Localizable", "Appearance.CustomBackground.FileError") } + /// Auto-Night Mode + internal static var appearanceSettingsAutoNight: String { return L10n.tr("Localizable", "Appearance.Settings.AutoNight") } + /// AUTO-NIGHT MODE + internal static var appearanceSettingsAutoNightHeader: String { return L10n.tr("Localizable", "Appearance.Settings.AutoNightHeader") } + /// Bubbles Mode + internal static var appearanceSettingsBubblesMode: String { return L10n.tr("Localizable", "Appearance.Settings.BubblesMode") } + /// Accent + internal static var appearanceThemeAccent: String { return L10n.tr("Localizable", "Appearance.Theme.Accent") } + /// Edit + internal static var appearanceThemeEdit: String { return L10n.tr("Localizable", "Appearance.Theme.Edit") } + /// Remove + internal static var appearanceThemeRemove: String { return L10n.tr("Localizable", "Appearance.Theme.Remove") } + /// Share + internal static var appearanceThemeShare: String { return L10n.tr("Localizable", "Appearance.Theme.Share") } + /// Messages + internal static var appearanceThemeAccentMessages: String { return L10n.tr("Localizable", "Appearance.Theme.Accent.Messages") } + /// Follow System Appearance + internal static var appearanceSettingsFollowSystemAppearance: String { return L10n.tr("Localizable", "AppearanceSettings.FollowSystemAppearance") } + /// Good morning! 👋 + internal static var appearanceSettingsChatPreview1: String { return L10n.tr("Localizable", "AppearanceSettings.ChatPreview.1") } + /// Do you know what time it is? + internal static var appearanceSettingsChatPreview2: String { return L10n.tr("Localizable", "AppearanceSettings.ChatPreview.2") } + /// It's morning in Tokyo 😎 + internal static var appearanceSettingsChatPreview3: String { return L10n.tr("Localizable", "AppearanceSettings.ChatPreview.3") } + /// Ah, you kids today with techno music! You should enjoy the classics, like Hasselhoff! + internal static var appearanceSettingsChatPreviewFirstText: String { return L10n.tr("Localizable", "AppearanceSettings.ChatPreview.FirstText") } + /// CHAT PREVIEW + internal static var appearanceSettingsChatPreviewHeader: String { return L10n.tr("Localizable", "AppearanceSettings.ChatPreview.Header") } + /// I can't even take you seriously right now. + internal static var appearanceSettingsChatPreviewSecondText: String { return L10n.tr("Localizable", "AppearanceSettings.ChatPreview.SecondText") } + /// Lucio + internal static var appearanceSettingsChatPreviewUserName1: String { return L10n.tr("Localizable", "AppearanceSettings.ChatPreview.UserName1") } + /// Reinhardt + internal static var appearanceSettingsChatPreviewUserName2: String { return L10n.tr("Localizable", "AppearanceSettings.ChatPreview.UserName2") } + /// Reinhardt, we need to find you some new tunes 🎶. + internal static var appearanceSettingsChatPreviewZeroText: String { return L10n.tr("Localizable", "AppearanceSettings.ChatPreview.ZeroText") } + /// Bubbles + internal static var appearanceSettingsChatViewBubbles: String { return L10n.tr("Localizable", "AppearanceSettings.ChatView.Bubbles") } + /// Minimalist + internal static var appearanceSettingsChatViewClassic: String { return L10n.tr("Localizable", "AppearanceSettings.ChatView.Classic") } + /// CHAT VIEW + internal static var appearanceSettingsChatViewHeader: String { return L10n.tr("Localizable", "AppearanceSettings.ChatView.Header") } + /// Day + internal static var appearanceSettingsColorThemeDay: String { return L10n.tr("Localizable", "AppearanceSettings.ColorTheme.day") } + /// Day Classic + internal static var appearanceSettingsColorThemeDayClassic: String { return L10n.tr("Localizable", "AppearanceSettings.ColorTheme.dayClassic") } + /// COLOR THEME + internal static var appearanceSettingsColorThemeHeader: String { return L10n.tr("Localizable", "AppearanceSettings.ColorTheme.Header") } + /// Night Accent + internal static var appearanceSettingsColorThemeNightAccent: String { return L10n.tr("Localizable", "AppearanceSettings.ColorTheme.nightAccent") } + /// System + internal static var appearanceSettingsColorThemeSystem: String { return L10n.tr("Localizable", "AppearanceSettings.ColorTheme.system") } + /// Select default dark palette which one will be used in dark system appearance mode. + internal static var appearanceSettingsFollowSystemAppearanceDefaultDark: String { return L10n.tr("Localizable", "AppearanceSettings.FollowSystemAppearance.DefaultDark") } + /// Select default day palette which one will be used in light system appearance mode. + internal static var appearanceSettingsFollowSystemAppearanceDefaultDay: String { return L10n.tr("Localizable", "AppearanceSettings.FollowSystemAppearance.DefaultDay") } + /// DEFAULT PALETTES + internal static var appearanceSettingsFollowSystemAppearanceDefaultHeader: String { return L10n.tr("Localizable", "AppearanceSettings.FollowSystemAppearance.DefaultHeader") } + /// TEXT SIZE + internal static var appearanceSettingsTextSizeHeader: String { return L10n.tr("Localizable", "AppearanceSettings.TextSize.Header") } + /// Change + internal static var applyLanguageApplyLanguageAction: String { return L10n.tr("Localizable", "ApplyLanguage.ApplyLanguageAction") } + /// Change + internal static var applyLanguageChangeLanguageAction: String { return L10n.tr("Localizable", "ApplyLanguage.ChangeLanguageAction") } + /// The language %@ is already active. + internal static func applyLanguageChangeLanguageAlreadyActive(_ p1: String) -> String { + return L10n.tr("Localizable", "ApplyLanguage.ChangeLanguageAlreadyActive", p1) + } + /// You are about to apply a language pack **%@**.\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. + internal static func applyLanguageChangeLanguageOfficialText(_ p1: String) -> String { + return L10n.tr("Localizable", "ApplyLanguage.ChangeLanguageOfficialText", p1) + } + /// Change Language? + internal static var applyLanguageChangeLanguageTitle: String { return L10n.tr("Localizable", "ApplyLanguage.ChangeLanguageTitle") } + /// 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. + internal static func applyLanguageChangeLanguageUnofficialText1(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "ApplyLanguage.ChangeLanguageUnofficialText1", p1, p2) + } + /// Translation Platform + internal static var applyLanguageUnsufficientDataOpenPlatform: String { return L10n.tr("Localizable", "ApplyLanguage.UnsufficientDataOpenPlatform") } + /// Unfortunately, this custom language pack %@ doesn't contain data for Telegram macos. You can contribute to this language pack using the translations platform. + internal static func applyLanguageUnsufficientDataText(_ p1: String) -> String { + return L10n.tr("Localizable", "ApplyLanguage.UnsufficientDataText", p1) + } + /// Insufficient Data + internal static var applyLanguageUnsufficientDataTitle: String { return L10n.tr("Localizable", "ApplyLanguage.UnsufficientDataTitle") } + /// unmuted chats will get unarchived if new messages arrive. + internal static var archiveTooltipFirstText: String { return L10n.tr("Localizable", "Archive.Tooltip.First.Text") } + /// Chat Archived + internal static var archiveTooltipFirstTitle: String { return L10n.tr("Localizable", "Archive.Tooltip.First.Title") } + /// Chat Archived + internal static var archiveTooltipJustArchiveTitle: String { return L10n.tr("Localizable", "Archive.Tooltip.JustArchive.Title") } + /// muted chats will stay archivated after new messages arrive. + internal static var archiveTooltipSecondText: String { return L10n.tr("Localizable", "Archive.Tooltip.Second.Text") } + /// Chat Archived + internal static var archiveTooltipSecondTitle: String { return L10n.tr("Localizable", "Archive.Tooltip.Second.Title") } + /// you can pin an unlimited number of archived chats on the top. + internal static var archiveTooltipThirdText: String { return L10n.tr("Localizable", "Archive.Tooltip.Third.Text") } + /// Chat Archived + internal static var archiveTooltipThirdTitle: String { return L10n.tr("Localizable", "Archive.Tooltip.Third.Title") } /// You can have up to 200 sticker sets installed. Unused stickers are archived when you add more. - case archivedStickersDescription + internal static var archivedStickersDescription: String { return L10n.tr("Localizable", "ArchivedStickers.Description") } + /// Your archived sticker packs will appear here + internal static var archivedStickersEmpty: String { return L10n.tr("Localizable", "ArchivedStickers.Empty") } + /// Mark As Read + internal static var articleMarkAsRead: String { return L10n.tr("Localizable", "Article.MarkAsRead") } + /// Mark As Unread + internal static var articleMarkAsUnread: String { return L10n.tr("Localizable", "Article.MarkAsUnread") } + /// READ + internal static var articleRead: String { return L10n.tr("Localizable", "Article.Read") } + /// Read All + internal static var articleReadAll: String { return L10n.tr("Localizable", "Article.ReadAll") } + /// Remove + internal static var articleRemove: String { return L10n.tr("Localizable", "Article.Remove") } + /// Remove All + internal static var articleRemoveAll: String { return L10n.tr("Localizable", "Article.RemoveAll") } /// Unknown Artist - case audioUnknownArtist + internal static var audioUnknownArtist: String { return L10n.tr("Localizable", "Audio.UnknownArtist") } /// Untitled - case audioUntitledSong + internal static var audioUntitledSong: String { return L10n.tr("Localizable", "Audio.UntitledSong") } /// video message - case audioControllerVideoMessage + internal static var audioControllerVideoMessage: String { return L10n.tr("Localizable", "AudioController.videoMessage") } /// voice message - case audioControllerVoiceMessage - /// Release outside of this field to cancel - case audioRecordReleaseOut - /// Window - case aufd15bRTitle - /// Any details such as age, occupation of city. Example: 23 y.o. designer from San Francisco - case bioDescription + internal static var audioControllerVoiceMessage: String { return L10n.tr("Localizable", "AudioController.voiceMessage") } + /// Click outside of circle to cancel + internal static var audioRecordHelpFixed: String { return L10n.tr("Localizable", "AudioRecord.Help.Fixed") } + /// Release outside of circle to cancel + internal static var audioRecordHelpPlain: String { return L10n.tr("Localizable", "AudioRecord.Help.Plain") } + /// , + internal static var autoDownloadSettingsDelimeter: String { return L10n.tr("Localizable", "AutoDownloadSettings.Delimeter") } + /// and + internal static var autoDownloadSettingsLastDelimeter: String { return L10n.tr("Localizable", "AutoDownloadSettings.LastDelimeter") } + /// Off for all chats + internal static var autoDownloadSettingsOffForAll: String { return L10n.tr("Localizable", "AutoDownloadSettings.OffForAll") } + /// On for %@ + internal static func autoDownloadSettingsOnFor(_ p1: String) -> String { + return L10n.tr("Localizable", "AutoDownloadSettings.OnFor", p1) + } + /// On for all chats + internal static var autoDownloadSettingsOnForAll: String { return L10n.tr("Localizable", "AutoDownloadSettings.OnForAll") } + /// Channels + internal static var autoDownloadSettingsTypeChannels: String { return L10n.tr("Localizable", "AutoDownloadSettings.TypeChannels") } + /// Groups + internal static var autoDownloadSettingsTypeGroupChats: String { return L10n.tr("Localizable", "AutoDownloadSettings.TypeGroupChats") } + /// Private Chats + internal static var autoDownloadSettingsTypePrivateChats: String { return L10n.tr("Localizable", "AutoDownloadSettings.TypePrivateChats") } + /// Up to %@ for %@ + internal static func autoDownloadSettingsUpToFor(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "AutoDownloadSettings.UpToFor", p1, p2) + } + /// Up to %@ for all chats + internal static func autoDownloadSettingsUpToForAll(_ p1: String) -> String { + return L10n.tr("Localizable", "AutoDownloadSettings.UpToForAll", p1) + } + /// Disabled + internal static var autoNightSettingsDisabled: String { return L10n.tr("Localizable", "AutoNight.Settings.Disabled") } + /// From + internal static var autoNightSettingsFrom: String { return L10n.tr("Localizable", "AutoNight.Settings.From") } + /// PREFERRED NIGHT THEME + internal static var autoNightSettingsPreferredTheme: String { return L10n.tr("Localizable", "AutoNight.Settings.PreferredTheme") } + /// Scheduled + internal static var autoNightSettingsScheduled: String { return L10n.tr("Localizable", "AutoNight.Settings.Scheduled") } + /// Use Local Sunset & Sunrise + internal static var autoNightSettingsSunsetAndSunrise: String { return L10n.tr("Localizable", "AutoNight.Settings.SunsetAndSunrise") } + /// System + internal static var autoNightSettingsSystemBased: String { return L10n.tr("Localizable", "AutoNight.Settings.SystemBased") } + /// App interfaces will match the system appearance settings. + internal static var autoNightSettingsSystemBasedDesc: String { return L10n.tr("Localizable", "AutoNight.Settings.SystemBasedDesc") } + /// Auto-Night Theme + internal static var autoNightSettingsTitle: String { return L10n.tr("Localizable", "AutoNight.Settings.Title") } + /// To + internal static var autoNightSettingsTo: String { return L10n.tr("Localizable", "AutoNight.Settings.To") } + /// Update Location + internal static var autoNightSettingsUpdateLocation: String { return L10n.tr("Localizable", "AutoNight.Settings.UpdateLocation") } + /// Calculating sunset & sunrise times requires a one-time check of your approximate location. Note that this location is only stored locally on your device.\n\nSunset: %@\nSunrise: %@ + internal static func autoNightSettingsSunriseDesc(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "AutoNight.Settings.Sunrise.Desc", p1, p2) + } + /// Calculating sunset & sunrise times requires a one-time check of your approximate location. Note that this location is only stored locally on your device.\n\nSunset: N/A\nSunrise: N/A + internal static var autoNightSettingsSunriseDescNA: String { return L10n.tr("Localizable", "AutoNight.Settings.Sunrise.Desc.NA") } + /// Can't determine your location. Please check your system settings and try again. + internal static var autoNightSettingsUpdateLocationError: String { return L10n.tr("Localizable", "AutoNight.Settings.UpdateLocation.Error") } + /// 1 Day + internal static var autoremoveMessagesDay1: String { return L10n.tr("Localizable", "AutoremoveMessages.Day1") } + /// Automatically delete messages sent in this chat after a certain period of time. + internal static var autoremoveMessagesDesc: String { return L10n.tr("Localizable", "AutoremoveMessages.Desc") } + /// AUTO-DELETE MESSAGES + internal static var autoremoveMessagesHeader: String { return L10n.tr("Localizable", "AutoremoveMessages.Header") } + /// 1 Month + internal static var autoremoveMessagesMonth1: String { return L10n.tr("Localizable", "AutoremoveMessages.Month1") } + /// Never + internal static var autoremoveMessagesNever: String { return L10n.tr("Localizable", "AutoremoveMessages.Never") } + /// Clear Chat History + internal static var autoremoveMessagesTitle: String { return L10n.tr("Localizable", "AutoremoveMessages.Title") } + /// 1 Week + internal static var autoremoveMessagesWeek1: String { return L10n.tr("Localizable", "AutoremoveMessages.Week1") } + /// Auto-Deletion + internal static var autoremoveMessagesTitleDeleteOnly: String { return L10n.tr("Localizable", "AutoremoveMessages.Title.DeleteOnly") } + /// Preferences… + internal static var bofnm1cWTitle: String { return L10n.tr("Localizable", "BOF-NM-1cW.title") } + /// Any details such as age, occupation or city.\nExample: 23 y.o. designer from San Francisco + internal static var bioDescription: String { return L10n.tr("Localizable", "Bio.Description") } + /// BIO + internal static var bioHeader: String { return L10n.tr("Localizable", "Bio.Header") } /// A few words about you - case bioPlaceholder + internal static var bioPlaceholder: String { return L10n.tr("Localizable", "Bio.Placeholder") } /// Save - case bioSave + internal static var bioSave: String { return L10n.tr("Localizable", "Bio.Save") } + /// Do you want to block %@ from messaging and calling you on Telegram? + internal static func blockContactTitle(_ p1: String) -> String { + return L10n.tr("Localizable", "BlockContact.Title", p1) + } + /// Block %@ + internal static func blockContactOptionsAction(_ p1: String) -> String { + return L10n.tr("Localizable", "BlockContact.Options.Action", p1) + } + /// Delete this Chat + internal static var blockContactOptionsDeleteChat: String { return L10n.tr("Localizable", "BlockContact.Options.DeleteChat") } + /// Report Spam + internal static var blockContactOptionsReport: String { return L10n.tr("Localizable", "BlockContact.Options.Report") } + /// Manage User + internal static var blockContactOptionsTitle: String { return L10n.tr("Localizable", "BlockContact.Options.Title") } /// Blocked users can't send you messages or add you to groups. They will not see your profile pictures, online and last seen status. - case blockedPeersEmptyDescrpition - /// Preferences… - case bofnm1cWTitle - /// Transformations - case c8aY6VQdTitle + internal static var blockedPeersEmptyDescrpition: String { return L10n.tr("Localizable", "BlockedPeers.EmptyDescrpition") } + /// Open Link + internal static var botInlineAuthHeader: String { return L10n.tr("Localizable", "Bot.InlineAuth.Header") } + /// Open + internal static var botInlineAuthOpen: String { return L10n.tr("Localizable", "Bot.InlineAuth.Open") } + /// Do you want to open \n%@? + internal static func botInlineAuthTitle(_ p1: String) -> String { + return L10n.tr("Localizable", "Bot.InlineAuth.Title", p1) + } + /// Allow %@ to send me messages + internal static func botInlineAuthOptionAllowSendMessages(_ p1: String) -> String { + return L10n.tr("Localizable", "Bot.InlineAuth.Option.AllowSendMessages", p1) + } + /// Log in to %@ as %@ + internal static func botInlineAuthOptionLogin(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Bot.InlineAuth.Option.Login", p1, p2) + } + /// Enable 2-Step Verification. + internal static var botTransferOwnerErrorEnable2FA: String { return L10n.tr("Localizable", "Bot.TransferOwner.Error.Enable2FA") } + /// Ownership transfers are only available if:\n\n• 2-Step Verification was enabled for your account more than 7 days ago.\n\n• You have logged in on this device more than 24 hours ago.\n\nPlease come back later. + internal static var botTransferOwnerErrorText: String { return L10n.tr("Localizable", "Bot.TransferOwner.Error.Text") } + /// Security Check + internal static var botTransferOwnerErrorTitle: String { return L10n.tr("Localizable", "Bot.TransferOwner.Error.Title") } + /// Please enter your 2-Step verification password to complete the transfer. + internal static var botTransferOwnershipPasswordDesc: String { return L10n.tr("Localizable", "Bot.TransferOwnership.Password.Desc") } + /// Two-Step Verification + internal static var botTransferOwnershipPasswordTitle: String { return L10n.tr("Localizable", "Bot.TransferOwnership.Password.Title") } + /// Leave as regular group + internal static var broadcastGroupsCancel: String { return L10n.tr("Localizable", "BroadcastGroups.Cancel") } + /// Convert to Broadcast Group + internal static var broadcastGroupsConvert: String { return L10n.tr("Localizable", "BroadcastGroups.Convert") } + /// • No limit on the number of members.\n\n• Only admins can post.\n\n• Can't be turned back into a regular group. + internal static var broadcastGroupsIntroText: String { return L10n.tr("Localizable", "BroadcastGroups.IntroText") } + /// Broadcast Groups + internal static var broadcastGroupsIntroTitle: String { return L10n.tr("Localizable", "BroadcastGroups.IntroTitle") } + /// Success! Now your group have not limits. + internal static var broadcastGroupsSuccess: String { return L10n.tr("Localizable", "BroadcastGroups.Success") } + /// Convert + internal static var broadcastGroupsConfirmationAlertConvert: String { return L10n.tr("Localizable", "BroadcastGroups.ConfirmationAlert.Convert") } + /// Regular members of the group (non-admins) will irrevocably lose their right to post messages in the group.\n\nThis action cannot be undone. + internal static var broadcastGroupsConfirmationAlertText: String { return L10n.tr("Localizable", "BroadcastGroups.ConfirmationAlert.Text") } + /// Are you sure? + internal static var broadcastGroupsConfirmationAlertTitle: String { return L10n.tr("Localizable", "BroadcastGroups.ConfirmationAlert.Title") } + /// Learn More + internal static var broadcastGroupsLimitAlertLearnMore: String { return L10n.tr("Localizable", "BroadcastGroups.LimitAlert.LearnMore") } + /// If you change your mind, go to the permission settings of your group. + internal static var broadcastGroupsLimitAlertSettingsTip: String { return L10n.tr("Localizable", "BroadcastGroups.LimitAlert.SettingsTip") } + /// Your group has reached a limit of %@ members.\n\nYou can increase this limit by converting the group to a broadcast group where only admins can post. Interested? + internal static func broadcastGroupsLimitAlertText(_ p1: String) -> String { + return L10n.tr("Localizable", "BroadcastGroups.LimitAlert.Text", p1) + } + /// Limit Reached + internal static var broadcastGroupsLimitAlertTitle: String { return L10n.tr("Localizable", "BroadcastGroups.LimitAlert.Title") } /// Hide Telegram - case cagYXWT6Title + internal static var cagYXWT6Title: String { return L10n.tr("Localizable", "Cag-YX-WT6.title") } + /// Accept + internal static var callAccept: String { return L10n.tr("Localizable", "Call.Accept") } + /// Camera + internal static var callCamera: String { return L10n.tr("Localizable", "Call.Camera") } + /// Camera is unavailable\n[settings]() + internal static var callCameraUnavailable: String { return L10n.tr("Localizable", "Call.CameraUnavailable") } + /// Close + internal static var callClose: String { return L10n.tr("Localizable", "Call.Close") } + /// Decline + internal static var callDecline: String { return L10n.tr("Localizable", "Call.Decline") } + /// End + internal static var callEnd: String { return L10n.tr("Localizable", "Call.End") } + /// Mute + internal static var callMute: String { return L10n.tr("Localizable", "Call.Mute") } /// %@'s app does not support calls. They need to update their app before you can call them. - case callParticipantVersionOutdatedError(String) + internal static func callParticipantVersionOutdatedError(_ p1: String) -> String { + return L10n.tr("Localizable", "Call.ParticipantVersionOutdatedError", p1) + } /// Sorry, %@ doesn't accept calls. - case callPrivacyErrorMessage(String) + internal static func callPrivacyErrorMessage(_ p1: String) -> String { + return L10n.tr("Localizable", "Call.PrivacyErrorMessage", p1) + } + /// Redial + internal static var callRecall: String { return L10n.tr("Localizable", "Call.Recall") } + /// Screen + internal static var callScreen: String { return L10n.tr("Localizable", "Call.Screen") } /// %d - case callShortMinutesCountable(Int) + internal static func callShortMinutesCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Call.ShortMinutes_countable", p1) + } /// %d min - case callShortMinutesFew(Int) + internal static func callShortMinutesFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Call.ShortMinutes_few", p1) + } /// %d min - case callShortMinutesMany(Int) + internal static func callShortMinutesMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Call.ShortMinutes_many", p1) + } /// %d min - case callShortMinutesOne(Int) + internal static func callShortMinutesOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Call.ShortMinutes_one", p1) + } /// %d min - case callShortMinutesOther(Int) + internal static func callShortMinutesOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Call.ShortMinutes_other", p1) + } /// %d min - case callShortMinutesTwo(Int) + internal static func callShortMinutesTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Call.ShortMinutes_two", p1) + } /// %d min - case callShortMinutesZero(Int) + internal static func callShortMinutesZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Call.ShortMinutes_zero", p1) + } /// %d - case callShortSecondsCountable(Int) + internal static func callShortSecondsCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Call.ShortSeconds_countable", p1) + } /// %d sec - case callShortSecondsFew(Int) + internal static func callShortSecondsFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Call.ShortSeconds_few", p1) + } /// %d sec - case callShortSecondsMany(Int) + internal static func callShortSecondsMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Call.ShortSeconds_many", p1) + } /// %d sec - case callShortSecondsOne(Int) + internal static func callShortSecondsOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Call.ShortSeconds_one", p1) + } /// %d sec - case callShortSecondsOther(Int) + internal static func callShortSecondsOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Call.ShortSeconds_other", p1) + } /// %d sec - case callShortSecondsTwo(Int) + internal static func callShortSecondsTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Call.ShortSeconds_two", p1) + } /// %d sec - case callShortSecondsZero(Int) + internal static func callShortSecondsZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Call.ShortSeconds_zero", p1) + } /// Busy - case callStatusBusy + internal static var callStatusBusy: String { return L10n.tr("Localizable", "Call.StatusBusy") } /// is calling you... - case callStatusCalling + internal static var callStatusCalling: String { return L10n.tr("Localizable", "Call.StatusCalling") } + /// is calling → %@... + internal static func callStatusCallingAccount(_ p1: String) -> String { + return L10n.tr("Localizable", "Call.StatusCallingAccount", p1) + } /// Connecting... - case callStatusConnecting + internal static var callStatusConnecting: String { return L10n.tr("Localizable", "Call.StatusConnecting") } /// Call Ended - case callStatusEnded + internal static var callStatusEnded: String { return L10n.tr("Localizable", "Call.StatusEnded") } /// Call Failed - case callStatusFailed + internal static var callStatusFailed: String { return L10n.tr("Localizable", "Call.StatusFailed") } /// Contacting... - case callStatusRequesting + internal static var callStatusRequesting: String { return L10n.tr("Localizable", "Call.StatusRequesting") } /// Ringing... - case callStatusRinging - /// Undefined Error, please try later. - case callUndefinedError - /// Finish call with %@ and start a new one with %@? - case callConfirmDiscardCurrentDescription(String, String) + internal static var callStatusRinging: String { return L10n.tr("Localizable", "Call.StatusRinging") } + /// Undefined error, please try later. + internal static var callUndefinedError: String { return L10n.tr("Localizable", "Call.UndefinedError") } + /// %@'s paused video + internal static func callVideoPaused(_ p1: String) -> String { + return L10n.tr("Localizable", "Call.VideoPaused", p1) + } + /// Telegram needs access to camera for Video Call. + internal static var callCameraError: String { return L10n.tr("Localizable", "Call.Camera.Error") } /// Call in Progress - case callConfirmDiscardCurrentHeader + internal static var callConfirmDiscardCallHeader: String { return L10n.tr("Localizable", "Call.Confirm.Discard.Call.Header") } + /// Finish call with "%1$@" and start a new one with "%2$@"? + internal static func callConfirmDiscardCallToCallText(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Call.Confirm.Discard.Call.ToCall.Text", p1, p2) + } + /// Finish call with "%1$@" and start a voice chat with "%2$@"? + internal static func callConfirmDiscardCallToVoiceText(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Call.Confirm.Discard.Call.ToVoice.Text", p1, p2) + } + /// Voice Chat in Progress + internal static var callConfirmDiscardVoiceHeader: String { return L10n.tr("Localizable", "Call.Confirm.Discard.Voice.Header") } + /// Leave voice chat in "%1$@" and start a call with "%2$@?" + internal static func callConfirmDiscardVoiceToCallText(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Call.Confirm.Discard.Voice.ToCall.Text", p1, p2) + } + /// Leave voice chat in "%1$@" and start a new one with "%2$@" + internal static func callConfirmDiscardVoiceToVoiceText(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Call.Confirm.Discard.Voice.ToVoice.Text", p1, p2) + } + /// Rate This Call + internal static var callContextRate: String { return L10n.tr("Localizable", "Call.Context.Rate") } + /// Not Now + internal static var callRatingModalNotNow: String { return L10n.tr("Localizable", "Call.RatingModal.NotNow") } /// Leave comment... - case callRatingModalPlaceholder + internal static var callRatingModalPlaceholder: String { return L10n.tr("Localizable", "Call.RatingModal.Placeholder") } + /// Please rate the quality of your Telegram call + internal static var callRatingModalText: String { return L10n.tr("Localizable", "Call.RatingModal.Text") } /// Incoming - case callRecentIncoming + internal static var callRecentIncoming: String { return L10n.tr("Localizable", "Call.Recent.Incoming") } /// Missed - case callRecentMissed + internal static var callRecentMissed: String { return L10n.tr("Localizable", "Call.Recent.Missed") } /// Outgoing - case callRecentOutgoing + internal static var callRecentOutgoing: String { return L10n.tr("Localizable", "Call.Recent.Outgoing") } + /// Sorry, you can’t make a call between two accounts on the same device. + internal static var callSameDeviceError: String { return L10n.tr("Localizable", "Call.SameDevice.Error") } + /// Telegram needs access for Screen Sharing. + internal static var callScreenError: String { return L10n.tr("Localizable", "Call.Screen.Error") } + /// %@'s camera is off + internal static func callToastCameraOff(_ p1: String) -> String { + return L10n.tr("Localizable", "Call.Toast.CameraOff", p1) + } + /// %@'s battery is low + internal static func callToastLowBattery(_ p1: String) -> String { + return L10n.tr("Localizable", "Call.Toast.LowBattery", p1) + } + /// %@'s microphone is off + internal static func callToastMicroOff(_ p1: String) -> String { + return L10n.tr("Localizable", "Call.Toast.MicroOff", p1) + } + /// Add an optional comment + internal static var callFeedbackAddComment: String { return L10n.tr("Localizable", "CallFeedback.AddComment") } + /// Include technical information + internal static var callFeedbackIncludeLogs: String { return L10n.tr("Localizable", "CallFeedback.IncludeLogs") } + /// This won't reveal the contents of your conversation, but will help us fix the issue sooner. + internal static var callFeedbackIncludeLogsInfo: String { return L10n.tr("Localizable", "CallFeedback.IncludeLogsInfo") } + /// Speech was distorted + internal static var callFeedbackReasonDistortedSpeech: String { return L10n.tr("Localizable", "CallFeedback.ReasonDistortedSpeech") } + /// Call ended unexpectedly + internal static var callFeedbackReasonDropped: String { return L10n.tr("Localizable", "CallFeedback.ReasonDropped") } + /// I heard my own voice + internal static var callFeedbackReasonEcho: String { return L10n.tr("Localizable", "CallFeedback.ReasonEcho") } + /// The other side kept disappearing + internal static var callFeedbackReasonInterruption: String { return L10n.tr("Localizable", "CallFeedback.ReasonInterruption") } + /// I heard background noise + internal static var callFeedbackReasonNoise: String { return L10n.tr("Localizable", "CallFeedback.ReasonNoise") } + /// I couldn't hear the other side + internal static var callFeedbackReasonSilentLocal: String { return L10n.tr("Localizable", "CallFeedback.ReasonSilentLocal") } + /// The other side couldn't hear me + internal static var callFeedbackReasonSilentRemote: String { return L10n.tr("Localizable", "CallFeedback.ReasonSilentRemote") } + /// Send + internal static var callFeedbackSend: String { return L10n.tr("Localizable", "CallFeedback.Send") } + /// Thanks for\nyour feedback + internal static var callFeedbackSuccess: String { return L10n.tr("Localizable", "CallFeedback.Success") } + /// Call Feedback + internal static var callFeedbackTitle: String { return L10n.tr("Localizable", "CallFeedback.Title") } + /// Video was distorted + internal static var callFeedbackVideoReasonDistorted: String { return L10n.tr("Localizable", "CallFeedback.VideoReasonDistorted") } + /// Video was pixelated + internal static var callFeedbackVideoReasonLowQuality: String { return L10n.tr("Localizable", "CallFeedback.VideoReasonLowQuality") } + /// WHAT WENT WRONG? + internal static var callFeedbackWhatWentWrong: String { return L10n.tr("Localizable", "CallFeedback.WhatWentWrong") } /// End Call - case callHeaderEndCall + internal static var callHeaderEndCall: String { return L10n.tr("Localizable", "CallHeader.EndCall") } + /// Input Level + internal static var callSettingsInputLevel: String { return L10n.tr("Localizable", "CallSettings.InputLevel") } + /// Call Settings + internal static var callSettingsTitle: String { return L10n.tr("Localizable", "CallSettings.Title") } + /// CAMERA + internal static var callSettingsCameraTitle: String { return L10n.tr("Localizable", "CallSettings.Camera.Title") } + /// Default + internal static var callSettingsDeviceDefault: String { return L10n.tr("Localizable", "CallSettings.Device.Default") } + /// Input Device + internal static var callSettingsInputText: String { return L10n.tr("Localizable", "CallSettings.Input.Text") } + /// MICROPHONE + internal static var callSettingsInputTitle: String { return L10n.tr("Localizable", "CallSettings.Input.Title") } + /// The deletion process was cancelled for your account %@. + internal static func cancelResetAccountSuccess(_ p1: String) -> String { + return L10n.tr("Localizable", "CancelResetAccount.Success", p1) + } + /// 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. + internal static func cancelResetAccountTextSMS(_ p1: String) -> String { + return L10n.tr("Localizable", "CancelResetAccount.TextSMS", p1) + } + /// Cancel Account Reset + internal static var cancelResetAccountTitle: String { return L10n.tr("Localizable", "CancelResetAccount.Title") } + /// E + internal static var canvasClear: String { return L10n.tr("Localizable", "Canvas.Clear") } + /// L - Line\nA - Arrow + internal static var canvasDraw: String { return L10n.tr("Localizable", "Canvas.Draw") } + /// ⌘⇧Z + internal static var canvasRedo: String { return L10n.tr("Localizable", "Canvas.Redo") } + /// ⌘Z + internal static var canvasUndo: String { return L10n.tr("Localizable", "Canvas.Undo") } + /// All Admins + internal static var chanelEventFilterAllAdmins: String { return L10n.tr("Localizable", "Chanel.EventFilter.AllAdmins") } + /// All Events + internal static var chanelEventFilterAllEvents: String { return L10n.tr("Localizable", "Chanel.EventFilter.AllEvents") } /// You have changed your phone number to %@. - case changeNumberConfirmCodeSuccess(String) + internal static func changeNumberConfirmCodeSuccess(_ p1: String) -> String { + return L10n.tr("Localizable", "ChangeNumber.ConfirmCode.Success", p1) + } /// Code expired. - case changeNumberConfirmCodeErrorCodeExpired + internal static var changeNumberConfirmCodeErrorCodeExpired: String { return L10n.tr("Localizable", "ChangeNumber.ConfirmCode.Error.codeExpired") } /// An error occurred. - case changeNumberConfirmCodeErrorGeneric + internal static var changeNumberConfirmCodeErrorGeneric: String { return L10n.tr("Localizable", "ChangeNumber.ConfirmCode.Error.Generic") } /// Invalid code. Please try again. - case changeNumberConfirmCodeErrorInvalidCode + internal static var changeNumberConfirmCodeErrorInvalidCode: String { return L10n.tr("Localizable", "ChangeNumber.ConfirmCode.Error.invalidCode") } /// You have entered invalid code too many times. Please try again later. - case changeNumberConfirmCodeErrorLimitExceeded + internal static var changeNumberConfirmCodeErrorLimitExceeded: String { return L10n.tr("Localizable", "ChangeNumber.ConfirmCode.Error.limitExceeded") } /// An error occurred. Please try again later. - case changeNumberSendDataErrorGeneric + internal static var changeNumberSendDataErrorGeneric: String { return L10n.tr("Localizable", "ChangeNumber.SendData.Error.Generic") } /// The phone number you entered is not valid. Please enter the correct number along with your area code. - case changeNumberSendDataErrorInvalidPhoneNumber - /// You have requested authorization code too many times. Please try again later. - case changeNumberSendDataErrorLimitExceeded + internal static var changeNumberSendDataErrorInvalidPhoneNumber: String { return L10n.tr("Localizable", "ChangeNumber.SendData.Error.InvalidPhoneNumber") } + /// You have requested for an authorization code too many times. Please try again later. + internal static var changeNumberSendDataErrorLimitExceeded: String { return L10n.tr("Localizable", "ChangeNumber.SendData.Error.LimitExceeded") } /// The number %@ is already connected to a Telegram account. Please delete that account before migrating to the new number. - case changeNumberSendDataErrorPhoneNumberOccupied(String) + internal static func changeNumberSendDataErrorPhoneNumberOccupied(_ p1: String) -> String { + return L10n.tr("Localizable", "ChangeNumber.SendData.Error.PhoneNumberOccupied", p1) + } /// 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. - case changePhoneNumberIntroAlert + internal static var changePhoneNumberIntroAlert: String { return L10n.tr("Localizable", "ChangePhoneNumber.Intro.Alert") } /// 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. - case changePhoneNumberIntroDescription + internal static var changePhoneNumberIntroDescription: String { return L10n.tr("Localizable", "ChangePhoneNumber.Intro.Description") } + /// Make Admin + internal static var channelAddBotAsAdmin: String { return L10n.tr("Localizable", "Channel.AddBotAsAdmin") } + /// Bots can only be added as administrators. + internal static var channelAddBotErrorHaveRights: String { return L10n.tr("Localizable", "Channel.AddBotErrorHaveRights") } + /// Sorry, bots can only be added to channels as administrators. + internal static var channelAddBotErrorNoRights: String { return L10n.tr("Localizable", "Channel.AddBotErrorNoRights") } /// Forever - case channelBanForever + internal static var channelBanForever: String { return L10n.tr("Localizable", "Channel.BanForever") } + /// Sorry, this bot is telling us it doesn't want to be added to groups. You can't add this bot unless its developers change their mind. + internal static var channelBotDoesntSupportGroups: String { return L10n.tr("Localizable", "Channel.BotDoesntSupportGroups") } /// Channel Name - case channelChannelNameHolder + internal static var channelChannelNameHolder: String { return L10n.tr("Localizable", "Channel.ChannelNameHolder") } /// Create - case channelCreate + internal static var channelCreate: String { return L10n.tr("Localizable", "Channel.Create") } + /// DESCRIPTION + internal static var channelDescHeader: String { return L10n.tr("Localizable", "Channel.DescHeader") } /// Description - case channelDescriptionHolder + internal static var channelDescriptionHolder: String { return L10n.tr("Localizable", "Channel.DescriptionHolder") } /// You can provide an optional description for your channel. - case channelDescriptionHolderDescrpiton + internal static var channelDescriptionHolderDescrpiton: String { return L10n.tr("Localizable", "Channel.DescriptionHolderDescrpiton") } + /// Sorry, you can't add this user to channels. + internal static var channelErrorAddBlocked: String { return L10n.tr("Localizable", "Channel.ErrorAddBlocked") } + /// 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. + internal static var channelErrorAddTooMuch: String { return L10n.tr("Localizable", "Channel.ErrorAddTooMuch") } /// People can join your channel by following this link. You can revoke the link at any time. - case channelExportLinkAboutChannel + internal static var channelExportLinkAboutChannel: String { return L10n.tr("Localizable", "Channel.ExportLinkAboutChannel") } /// People can join your group by following this link. You can revoke the link at any time. - case channelExportLinkAboutGroup - /// Channels are a tool for broadcasting your messages to large audiences. - case channelIntroDescription + internal static var channelExportLinkAboutGroup: String { return L10n.tr("Localizable", "Channel.ExportLinkAboutGroup") } + /// Channels are a tool for broadcasting your messages to unlimited audiences. + internal static var channelIntroDescription: String { return L10n.tr("Localizable", "Channel.IntroDescription") } /// What is a Channel? - case channelIntroDescriptionHeader + internal static var channelIntroDescriptionHeader: String { return L10n.tr("Localizable", "Channel.IntroDescriptionHeader") } + /// CHANNEL NAME + internal static var channelNameHeader: String { return L10n.tr("Localizable", "Channel.NameHeader") } /// New Channel - case channelNewChannel + internal static var channelNewChannel: String { return L10n.tr("Localizable", "Channel.NewChannel") } /// Private - case channelPrivate + internal static var channelPrivate: String { return L10n.tr("Localizable", "Channel.Private") } /// Private channels can only be joined via an invite link. - case channelPrivateAboutChannel - /// Private groups can only be joined if you were invited or by invite link. - case channelPrivateAboutGroup + internal static var channelPrivateAboutChannel: String { return L10n.tr("Localizable", "Channel.PrivateAboutChannel") } + /// Private groups can only be joined if you were invited or have an invite link. + internal static var channelPrivateAboutGroup: String { return L10n.tr("Localizable", "Channel.PrivateAboutGroup") } /// Public - case channelPublic - /// Public channels can be found via search, anyone can join them. - case channelPublicAboutChannel - /// Public groups can be found via search, chat history is available to everyone and anyone can join. - case channelPublicAboutGroup + internal static var channelPublic: String { return L10n.tr("Localizable", "Channel.Public") } + /// Public channels can be found in search, anyone can join them. + internal static var channelPublicAboutChannel: String { return L10n.tr("Localizable", "Channel.PublicAboutChannel") } + /// Public groups can be found in search, their chat history is available to everyone and anyone can join. + internal static var channelPublicAboutGroup: String { return L10n.tr("Localizable", "Channel.PublicAboutGroup") } /// 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 - case channelPublicNamesLimitError + internal static var channelPublicNamesLimitError: String { return L10n.tr("Localizable", "Channel.PublicNamesLimitError") } + /// Revoke Link + internal static var channelRevokeLink: String { return L10n.tr("Localizable", "Channel.RevokeLink") } + /// Sorry, there are already too many bots in this group. Please remove some of the bots you're not using first. + internal static var channelTooMuchBots: String { return L10n.tr("Localizable", "Channel.TooMuchBots") } /// CHANNEL TYPE - case channelTypeHeaderChannel + internal static var channelTypeHeaderChannel: String { return L10n.tr("Localizable", "Channel.TypeHeaderChannel") } /// GROUP TYPE - case channelTypeHeaderGroup - /// People can share this link with others and find your channel using Telegram search. - case channelUsernameAboutChannel + internal static var channelTypeHeaderGroup: String { return L10n.tr("Localizable", "Channel.TypeHeaderGroup") } + /// People can share this link with others and can find your channel using Telegram search. + internal static var channelUsernameAboutChannel: String { return L10n.tr("Localizable", "Channel.UsernameAboutChannel") } /// People can share this link with others and find your group using Telegram search. - case channelUsernameAboutGroup + internal static var channelUsernameAboutGroup: String { return L10n.tr("Localizable", "Channel.UsernameAboutGroup") } /// USER RESTRICTIONS - case channelUserRestriction + internal static var channelUserRestriction: String { return L10n.tr("Localizable", "Channel.UserRestriction") } /// This Admin will be able to add new admins with the same (or more limited) permissions than he/she has. - case channelAdminAdminAccess + internal static var channelAdminAdminAccess: String { return L10n.tr("Localizable", "Channel.Admin.AdminAccess") } /// This admin will not be able to add new admins. - case channelAdminAdminRestricted + internal static var channelAdminAdminRestricted: String { return L10n.tr("Localizable", "Channel.Admin.AdminRestricted") } + /// You are not allowed to edit the rights of this admin. + internal static var channelAdminCantEdit: String { return L10n.tr("Localizable", "Channel.Admin.CantEdit") } /// You cannot edit the rights of this admin. - case channelAdminCantEditRights + internal static var channelAdminCantEditRights: String { return L10n.tr("Localizable", "Channel.Admin.CantEditRights") } /// Dismiss Admin - case channelAdminDismiss + internal static var channelAdminDismiss: String { return L10n.tr("Localizable", "Channel.Admin.Dismiss") } /// WHAT CAN THIS ADMIN DO? - case channelAdminWhatCanAdminDo - /// Sorry you can't promote this user to admin - case channelAdminsAddAdminError + internal static var channelAdminWhatCanAdminDo: String { return L10n.tr("Localizable", "Channel.Admin.WhatCanAdminDo") } + /// CUSTOM TITLE + internal static var channelAdminRoleHeader: String { return L10n.tr("Localizable", "Channel.Admin.Role.Header") } + /// A title that will be shown instead of 'admin'. + internal static var channelAdminRoleAdminDesc: String { return L10n.tr("Localizable", "Channel.Admin.Role.Admin.Desc") } + /// A title that will be shown instead of 'owner'. + internal static var channelAdminRoleOwnerDesc: String { return L10n.tr("Localizable", "Channel.Admin.Role.Owner.Desc") } + /// admin + internal static var channelAdminRolePlaceholderAdmin: String { return L10n.tr("Localizable", "Channel.Admin.Role.Placeholder.Admin") } + /// owner + internal static var channelAdminRolePlaceholderOwner: String { return L10n.tr("Localizable", "Channel.Admin.Role.Placeholder.Owner") } + /// Transfer Channel Ownership + internal static var channelAdminTransferOwnershipChannel: String { return L10n.tr("Localizable", "Channel.Admin.TransferOwnership.Channel") } + /// Transfer Group Ownership + internal static var channelAdminTransferOwnershipGroup: String { return L10n.tr("Localizable", "Channel.Admin.TransferOwnership.Group") } + /// Change Owner + internal static var channelAdminTransferOwnershipConfirmOK: String { return L10n.tr("Localizable", "Channel.Admin.TransferOwnership.Confirm.OK") } + /// This will transfer the full owner rights for %@ to %@.\n\nYou will no longer be considered the creator of the channel. The new owner will be free to remove any of your admin privileges or even ban you. + internal static func channelAdminTransferOwnershipConfirmChannelText(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Channel.Admin.TransferOwnership.Confirm.Channel.Text", p1, p2) + } + /// Transfer Channel Ownership + internal static var channelAdminTransferOwnershipConfirmChannelTitle: String { return L10n.tr("Localizable", "Channel.Admin.TransferOwnership.Confirm.Channel.Title") } + /// This will transfer the full owner rights for %@ to %@.\n\nYou will no longer be considered the creator of the group. The new owner will be free to remove any of your admin privileges or even ban you. + internal static func channelAdminTransferOwnershipConfirmGroupText(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Channel.Admin.TransferOwnership.Confirm.Group.Text", p1, p2) + } + /// Transfer Group Ownership + internal static var channelAdminTransferOwnershipConfirmGroupTitle: String { return L10n.tr("Localizable", "Channel.Admin.TransferOwnership.Confirm.Group.Title") } + /// Please enter your 2-Step verification password to complete the transfer. + internal static var channelAdminTransferOwnershipPasswordDesc: String { return L10n.tr("Localizable", "Channel.Admin.TransferOwnership.Password.Desc") } + /// Two-Step Verification + internal static var channelAdminTransferOwnershipPasswordTitle: String { return L10n.tr("Localizable", "Channel.Admin.TransferOwnership.Password.Title") } + /// %1$@ allowed new members to speak + internal static func channelAdminLogAllowedNewMembersToSpeak(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.AdminLog.AllowedNewMembersToSpeak", p1) + } + /// Invite Users via Link + internal static var channelAdminLogCanInviteUsersViaLink: String { return L10n.tr("Localizable", "Channel.AdminLog.CanInviteUsersViaLink") } + /// Manage Voice Chats + internal static var channelAdminLogCanManageCalls: String { return L10n.tr("Localizable", "Channel.AdminLog.CanManageCalls") } + /// %1$@ created invite link %2$@ + internal static func channelAdminLogCreatedInviteLink(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Channel.AdminLog.CreatedInviteLink", p1, p2) + } + /// %1$@ deleted invite link %2$@ + internal static func channelAdminLogDeletedInviteLink(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Channel.AdminLog.DeletedInviteLink", p1, p2) + } + /// %1$@ edited invite link %2$@ + internal static func channelAdminLogEditedInviteLink(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Channel.AdminLog.EditedInviteLink", p1, p2) + } + /// %1$@ ended voice chat + internal static func channelAdminLogEndedVoiceChat(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.AdminLog.EndedVoiceChat", p1) + } + /// %1$@ joined via invite link %2$@ + internal static func channelAdminLogJoinedViaInviteLink(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Channel.AdminLog.JoinedViaInviteLink", p1, p2) + } + /// %1$@ disabled auto-remove timer + internal static func channelAdminLogMessageChangedAutoremoveTimeoutRemove(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.AdminLog.MessageChangedAutoremoveTimeoutRemove", p1) + } + /// %1$@ set auto-remove timer to %2$@ + internal static func channelAdminLogMessageChangedAutoremoveTimeoutSet(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Channel.AdminLog.MessageChangedAutoremoveTimeoutSet", p1, p2) + } + /// %1$@ muted new members + internal static func channelAdminLogMutedNewMembers(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.AdminLog.MutedNewMembers", p1) + } + /// %1$@ muted %2$@ + internal static func channelAdminLogMutedParticipant(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Channel.AdminLog.MutedParticipant", p1, p2) + } + /// %1$@ revoked invite link %2$@ + internal static func channelAdminLogRevokedInviteLink(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Channel.AdminLog.RevokedInviteLink", p1, p2) + } + /// %1$@ started voice chat + internal static func channelAdminLogStartedVoiceChat(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.AdminLog.StartedVoiceChat", p1) + } + /// %1$@ unmuted %2$@ + internal static func channelAdminLogUnmutedMutedParticipant(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Channel.AdminLog.UnmutedMutedParticipant", p1, p2) + } + /// %1$@ changed %2$@ volume to %3$@ + internal static func channelAdminLogUpdatedParticipantVolume(_ p1: String, _ p2: String, _ p3: String) -> String { + return L10n.tr("Localizable", "Channel.AdminLog.UpdatedParticipantVolume", p1, p2, p3) + } + /// Sorry, you're not allowed to promote this user to become an admin. + internal static var channelAdminsAddAdminError: String { return L10n.tr("Localizable", "Channel.Admins.AddAdminError") } /// promoted by %@ - case channelAdminsPromotedBy(String) + internal static func channelAdminsPromotedBy(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.Admins.PromotedBy", p1) + } /// Sorry, you can't add this user as an admin because they are in the blacklist and you can't unban them. - case channelAdminsPromoteBannedAdminError + internal static var channelAdminsPromoteBannedAdminError: String { return L10n.tr("Localizable", "Channel.Admins.Promote.BannedAdminError") } /// 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. - case channelAdminsPromoteUnmemberAdminError + internal static var channelAdminsPromoteUnmemberAdminError: String { return L10n.tr("Localizable", "Channel.Admins.Promote.UnmemberAdminError") } + /// Add Members + internal static var channelBanUserPermissionAddMembers: String { return L10n.tr("Localizable", "Channel.BanUser.PermissionAddMembers") } + /// Change Group Info + internal static var channelBanUserPermissionChangeGroupInfo: String { return L10n.tr("Localizable", "Channel.BanUser.PermissionChangeGroupInfo") } + /// Can Embed Links + internal static var channelBanUserPermissionEmbedLinks: String { return L10n.tr("Localizable", "Channel.BanUser.PermissionEmbedLinks") } + /// Can Read Messages + internal static var channelBanUserPermissionReadMessages: String { return L10n.tr("Localizable", "Channel.BanUser.PermissionReadMessages") } + /// Can Send Media + internal static var channelBanUserPermissionSendMedia: String { return L10n.tr("Localizable", "Channel.BanUser.PermissionSendMedia") } + /// Can Send Messages + internal static var channelBanUserPermissionSendMessages: String { return L10n.tr("Localizable", "Channel.BanUser.PermissionSendMessages") } + /// Send Polls + internal static var channelBanUserPermissionSendPolls: String { return L10n.tr("Localizable", "Channel.BanUser.PermissionSendPolls") } + /// Can Send Stickers & GIFs + internal static var channelBanUserPermissionSendStickersAndGifs: String { return L10n.tr("Localizable", "Channel.BanUser.PermissionSendStickersAndGifs") } + /// User Restrictions + internal static var channelBanUserPermissionsHeader: String { return L10n.tr("Localizable", "Channel.BanUser.PermissionsHeader") } + /// Ban User + internal static var channelBanUserTitle: String { return L10n.tr("Localizable", "Channel.BanUser.Title") } + /// Unban + internal static var channelBanUserUnban: String { return L10n.tr("Localizable", "Channel.BanUser.Unban") } /// blocked by %@ - case channelBlacklistBlockedBy(String) - /// Sorry, you can't ban this user because they are an admin in this group and you are not allowed to demote them. - case channelBlacklistDemoteAdminError + internal static func channelBlacklistBlockedBy(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.Blacklist.BlockedBy", p1) + } + /// Sorry, you can't ban this user because they are an admin of this group and you are not allowed to demote them. + internal static var channelBlacklistDemoteAdminError: String { return L10n.tr("Localizable", "Channel.Blacklist.DemoteAdminError") } + /// Users removed from the channel by admins cannot rejoin via invite links. + internal static var channelBlacklistDescChannel: String { return L10n.tr("Localizable", "Channel.Blacklist.DescChannel") } + /// Users removed from the group by admins cannot rejoin via invite links. + internal static var channelBlacklistDescGroup: String { return L10n.tr("Localizable", "Channel.Blacklist.DescGroup") } + /// Remove User + internal static var channelBlacklistRemoveUser: String { return L10n.tr("Localizable", "Channel.Blacklist.RemoveUser") } /// restricted by %@ - case channelBlacklistRestrictedBy(String) + internal static func channelBlacklistRestrictedBy(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.Blacklist.RestrictedBy", p1) + } /// Members - case channelBlacklistSelectNewUserTitle + internal static var channelBlacklistSelectNewUserTitle: String { return L10n.tr("Localizable", "Channel.Blacklist.SelectNewUserTitle") } /// Unban - case channelBlacklistUnban + internal static var channelBlacklistUnban: String { return L10n.tr("Localizable", "Channel.Blacklist.Unban") } + /// Add To Group + internal static var channelBlacklistContextAddToGroup: String { return L10n.tr("Localizable", "Channel.Blacklist.Context.AddToGroup") } + /// Remove + internal static var channelBlacklistContextRemove: String { return L10n.tr("Localizable", "Channel.Blacklist.Context.Remove") } /// Block For - case channelBlockUserBlockFor + internal static var channelBlockUserBlockFor: String { return L10n.tr("Localizable", "Channel.BlockUser.BlockFor") } /// Can Embed Links - case channelBlockUserCanEmbedLinks + internal static var channelBlockUserCanEmbedLinks: String { return L10n.tr("Localizable", "Channel.BlockUser.CanEmbedLinks") } /// Can Read Messages - case channelBlockUserCanReadMessages + internal static var channelBlockUserCanReadMessages: String { return L10n.tr("Localizable", "Channel.BlockUser.CanReadMessages") } /// Can Send Media - case channelBlockUserCanSendMedia + internal static var channelBlockUserCanSendMedia: String { return L10n.tr("Localizable", "Channel.BlockUser.CanSendMedia") } /// Can Send Messages - case channelBlockUserCanSendMessages + internal static var channelBlockUserCanSendMessages: String { return L10n.tr("Localizable", "Channel.BlockUser.CanSendMessages") } /// Can Send Stickers & GIFs - case channelBlockUserCanSendStickers + internal static var channelBlockUserCanSendStickers: String { return L10n.tr("Localizable", "Channel.BlockUser.CanSendStickers") } + /// %d + internal static func channelCommentsCountCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Channel.Comments.Count_countable", p1) + } + /// %d Comments + internal static func channelCommentsCountFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Channel.Comments.Count_few", p1) + } + /// %d Comments + internal static func channelCommentsCountMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Channel.Comments.Count_many", p1) + } + /// %d Comment + internal static func channelCommentsCountOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Channel.Comments.Count_one", p1) + } + /// %d Comments + internal static func channelCommentsCountOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Channel.Comments.Count_other", p1) + } + /// %d Comments + internal static func channelCommentsCountTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Channel.Comments.Count_two", p1) + } + /// %d Comments + internal static func channelCommentsCountZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Channel.Comments.Count_zero", p1) + } + /// Leave a Comment + internal static var channelCommentsLeaveComment: String { return L10n.tr("Localizable", "Channel.Comments.LeaveComment") } + /// %d + internal static func channelCommentsShortCountCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Channel.Comments.Short.Count_countable", p1) + } + /// %d + internal static func channelCommentsShortCountFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Channel.Comments.Short.Count_few", p1) + } + /// %d + internal static func channelCommentsShortCountMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Channel.Comments.Short.Count_many", p1) + } + /// %d + internal static func channelCommentsShortCountOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Channel.Comments.Short.Count_one", p1) + } + /// %d + internal static func channelCommentsShortCountOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Channel.Comments.Short.Count_other", p1) + } + /// %d + internal static func channelCommentsShortCountTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Channel.Comments.Short.Count_two", p1) + } + /// %d + internal static func channelCommentsShortCountZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Channel.Comments.Short.Count_zero", p1) + } + /// Comment + internal static var channelCommentsShortLeaveComment: String { return L10n.tr("Localizable", "Channel.Comments.Short.LeaveComment") } + /// Manage Voice Chats + internal static var channelEditAdminManageCalls: String { return L10n.tr("Localizable", "Channel.EditAdmin.ManageCalls") } + /// Remain Anonymous + internal static var channelEditAdminPermissionAnonymous: String { return L10n.tr("Localizable", "Channel.EditAdmin.PermissionAnonymous") } + /// Add Members + internal static var channelEditAdminPermissionInviteMembers: String { return L10n.tr("Localizable", "Channel.EditAdmin.PermissionInviteMembers") } + /// Add Subscribers + internal static var channelEditAdminPermissionInviteSubscribers: String { return L10n.tr("Localizable", "Channel.EditAdmin.PermissionInviteSubscribers") } + /// Invite Users via Link + internal static var channelEditAdminPermissionInviteViaLink: String { return L10n.tr("Localizable", "Channel.EditAdmin.PermissionInviteViaLink") } /// Add New Admins - case channelEditAdminPermissionAddNewAdmins + internal static var channelEditAdminPermissionAddNewAdmins: String { return L10n.tr("Localizable", "Channel.EditAdmin.Permission.AddNewAdmins") } /// Ban Users - case channelEditAdminPermissionBanUsers + internal static var channelEditAdminPermissionBanUsers: String { return L10n.tr("Localizable", "Channel.EditAdmin.Permission.BanUsers") } /// Change Channel Info - case channelEditAdminPermissionChangeInfo + internal static var channelEditAdminPermissionChangeInfo: String { return L10n.tr("Localizable", "Channel.EditAdmin.Permission.ChangeInfo") } /// Delete Messages - case channelEditAdminPermissionDeleteMessages + internal static var channelEditAdminPermissionDeleteMessages: String { return L10n.tr("Localizable", "Channel.EditAdmin.Permission.DeleteMessages") } /// Edit Messages - case channelEditAdminPermissionEditMessages - /// Invite Users - case channelEditAdminPermissionInviteUsers + internal static var channelEditAdminPermissionEditMessages: String { return L10n.tr("Localizable", "Channel.EditAdmin.Permission.EditMessages") } /// Pin Messages - case channelEditAdminPermissionPinMessages + internal static var channelEditAdminPermissionPinMessages: String { return L10n.tr("Localizable", "Channel.EditAdmin.Permission.PinMessages") } /// Post Messages - case channelEditAdminPermissionPostMessages + internal static var channelEditAdminPermissionPostMessages: String { return L10n.tr("Localizable", "Channel.EditAdmin.Permission.PostMessages") } + /// Sorry, you don't have the necessary permissions for this action. + internal static var channelErrorDontHavePermissions: String { return L10n.tr("Localizable", "Channel.Error.DontHavePermissions") } /// ADMINS - case channelEventFilterAdminsHeader + internal static var channelEventFilterAdminsHeader: String { return L10n.tr("Localizable", "Channel.EventFilter.AdminsHeader") } /// EVENTS - case channelEventFilterEventsHeader + internal static var channelEventFilterEventsHeader: String { return L10n.tr("Localizable", "Channel.EventFilter.EventsHeader") } /// Empty - case channelEventLogEmpty + internal static var channelEventLogEmpty: String { return L10n.tr("Localizable", "Channel.EventLog.Empty") } /// ** No events found**\n\nNo recent events that match your query have been found. - case channelEventLogEmptySearch + internal static var channelEventLogEmptySearch: String { return L10n.tr("Localizable", "Channel.EventLog.EmptySearch") } /// **No events here yet**\n\nThere were no service actions taken by the channel's members and admins for the last 48 hours. - case channelEventLogEmptyText + internal static var channelEventLogEmptyText: String { return L10n.tr("Localizable", "Channel.EventLog.EmptyText") } + /// %@ linked this group to %@ + internal static func channelEventLogMessageChangedLinkedChannel(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Channel.EventLog.MessageChangedLinkedChannel", p1, p2) + } + /// %@ linked %@ as the discussion group + internal static func channelEventLogMessageChangedLinkedGroup(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Channel.EventLog.MessageChangedLinkedGroup", p1, p2) + } + /// %@ unlinked this group from %@ + internal static func channelEventLogMessageChangedUnlinkedChannel(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Channel.EventLog.MessageChangedUnlinkedChannel", p1, p2) + } + /// %@ removed discussion group + internal static func channelEventLogMessageChangedUnlinkedGroup(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.EventLog.MessageChangedUnlinkedGroup", p1) + } + /// changed custom title for %@: %@ + internal static func channelEventLogMessageRankName(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Channel.EventLog.MessageRankName", p1, p2) + } + /// transferred ownership + internal static var channelEventLogMessageTransfered: String { return L10n.tr("Localizable", "Channel.EventLog.MessageTransfered") } + /// transferred ownership to %1$@ %2$@ + internal static func channelEventLogMessageTransferedName1(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Channel.EventLog.MessageTransferedName1", p1, p2) + } /// Original message - case channelEventLogOriginalMessage + internal static var channelEventLogOriginalMessage: String { return L10n.tr("Localizable", "Channel.EventLog.OriginalMessage") } /// What Is This? - case channelEventLogWhat - /// What is an event log? - case channelEventLogAlertHeader + internal static var channelEventLogWhat: String { return L10n.tr("Localizable", "Channel.EventLog.What") } + /// What is the event log? + internal static var channelEventLogAlertHeader: String { return L10n.tr("Localizable", "Channel.EventLog.Alert.Header") } /// This is a list of all service actions taken by the group's members and admins in the last 48 hours. - case channelEventLogAlertInfo - /// %@ removed channel description: - case channelEventLogServiceAboutRemoved(String) - /// %@ edited channel description: - case channelEventLogServiceAboutUpdated(String) + internal static var channelEventLogAlertInfo: String { return L10n.tr("Localizable", "Channel.EventLog.Alert.Info") } + /// %@ removed this channel's description: + internal static func channelEventLogServiceAboutRemoved(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.EventLog.Service.AboutRemoved", p1) + } + /// %@ edited this channel's description: + internal static func channelEventLogServiceAboutUpdated(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.EventLog.Service.AboutUpdated", p1) + } + /// %@ disabled slowmode + internal static func channelEventLogServiceDisabledSlowMode(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.EventLog.Service.DisabledSlowMode", p1) + } /// %@ disabled channel signatures - case channelEventLogServiceDisableSignatures(String) + internal static func channelEventLogServiceDisableSignatures(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.EventLog.Service.DisableSignatures", p1) + } /// %@ enabled channel signatures - case channelEventLogServiceEnableSignatures(String) + internal static func channelEventLogServiceEnableSignatures(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.EventLog.Service.EnableSignatures", p1) + } /// %@ removed channel link: - case channelEventLogServiceLinkRemoved(String) - /// %@ edited channel link: - case channelEventLogServiceLinkUpdated(String) + internal static func channelEventLogServiceLinkRemoved(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.EventLog.Service.LinkRemoved", p1) + } + /// %@ edited this channel's link: + internal static func channelEventLogServiceLinkUpdated(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.EventLog.Service.LinkUpdated", p1) + } + /// - Title + internal static var channelEventLogServiceMinusTitle: String { return L10n.tr("Localizable", "Channel.EventLog.Service.MinusTitle") } /// %@ removed channel photo - case channelEventLogServicePhotoRemoved(String) - /// %@ updated channel photo - case channelEventLogServicePhotoUpdated(String) - /// %@ edited channel title: - case channelEventLogServiceTitleUpdated(String) + internal static func channelEventLogServicePhotoRemoved(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.EventLog.Service.PhotoRemoved", p1) + } + /// %@ updated this channel's photo + internal static func channelEventLogServicePhotoUpdated(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.EventLog.Service.PhotoUpdated", p1) + } + /// + Title: %@ + internal static func channelEventLogServicePlusTitle(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.EventLog.Service.PlusTitle", p1) + } + /// %1$@ set slowmode to %2$@ + internal static func channelEventLogServiceSetSlowMode1(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Channel.EventLog.Service.SetSlowMode1", p1, p2) + } + /// %@ edited this channel's title: + internal static func channelEventLogServiceTitleUpdated(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.EventLog.Service.TitleUpdated", p1) + } /// %@ joined the channel - case channelEventLogServiceUpdateJoin(String) + internal static func channelEventLogServiceUpdateJoin(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.EventLog.Service.UpdateJoin", p1) + } /// %@ left the channel - case channelEventLogServiceUpdateLeft(String) - /// The admins of this group have restricted you from posting inline content here - case channelPersmissionDeniedSendInlineForever - /// The admins of this group have restricted you from posting inline content here until %@ - case channelPersmissionDeniedSendInlineUntil(String) - /// The admins of this group have restricted you from sending media here - case channelPersmissionDeniedSendMediaForever - /// The admins of this group have restricted you from sending media here until %@ - case channelPersmissionDeniedSendMediaUntil(String) + internal static func channelEventLogServiceUpdateLeft(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.EventLog.Service.UpdateLeft", p1) + } + /// This option is disabled in channel Permissions for all members. + internal static var channelExceptionDisabledOptionChannel: String { return L10n.tr("Localizable", "Channel.Exception.DisabledOption.Channel") } + /// This option is disabled in group's Permissions for all members. + internal static var channelExceptionDisabledOptionGroup: String { return L10n.tr("Localizable", "Channel.Exception.DisabledOption.Group") } + /// Create Channel + internal static var channelIntroCreateChannel: String { return L10n.tr("Localizable", "Channel.Intro.CreateChannel") } + /// SLOW MODE + internal static var channelPermissionsSlowModeHeader: String { return L10n.tr("Localizable", "Channel.Permissions.SlowMode.Header") } + /// Members will be able to send only one message per this interval. + internal static var channelPermissionsSlowModeTextOff: String { return L10n.tr("Localizable", "Channel.Permissions.SlowMode.Text.Off") } + /// Members will be able to send only one message every %@ + internal static func channelPermissionsSlowModeTextSelected(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.Permissions.SlowMode.Text.Selected", p1) + } + /// 10s + internal static var channelPermissionsSlowModeTimeout10s: String { return L10n.tr("Localizable", "Channel.Permissions.SlowMode.Timeout.10s") } + /// 15m + internal static var channelPermissionsSlowModeTimeout15m: String { return L10n.tr("Localizable", "Channel.Permissions.SlowMode.Timeout.15m") } + /// 1h + internal static var channelPermissionsSlowModeTimeout1h: String { return L10n.tr("Localizable", "Channel.Permissions.SlowMode.Timeout.1h") } + /// 1m + internal static var channelPermissionsSlowModeTimeout1m: String { return L10n.tr("Localizable", "Channel.Permissions.SlowMode.Timeout.1m") } + /// 30s + internal static var channelPermissionsSlowModeTimeout30s: String { return L10n.tr("Localizable", "Channel.Permissions.SlowMode.Timeout.30s") } + /// 5m + internal static var channelPermissionsSlowModeTimeout5m: String { return L10n.tr("Localizable", "Channel.Permissions.SlowMode.Timeout.5m") } + /// Off + internal static var channelPermissionsSlowModeTimeoutOff: String { return L10n.tr("Localizable", "Channel.Permissions.SlowMode.Timeout.Off") } + /// Sending GIFs isn't allowed in this group. + internal static var channelPersmissionDeniedSendGifsDefaultRestrictedText: String { return L10n.tr("Localizable", "Channel.Persmission.Denied.SendGifs.DefaultRestrictedText") } + /// The admins of this group have restricted you from sending GIFs here. + internal static var channelPersmissionDeniedSendGifsForever: String { return L10n.tr("Localizable", "Channel.Persmission.Denied.SendGifs.Forever") } + /// The admins of this group have restricted you from sending GIFs here until %@. + internal static func channelPersmissionDeniedSendGifsUntil(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.Persmission.Denied.SendGifs.Until", p1) + } + /// Posting inline content isn't allowed in this group. + internal static var channelPersmissionDeniedSendInlineDefaultRestrictedText: String { return L10n.tr("Localizable", "Channel.Persmission.Denied.SendInline.DefaultRestrictedText") } + /// The admins of this group have restricted you from posting inline content here. + internal static var channelPersmissionDeniedSendInlineForever: String { return L10n.tr("Localizable", "Channel.Persmission.Denied.SendInline.Forever") } + /// The admins of this group have restricted you from posting inline content here until %@. + internal static func channelPersmissionDeniedSendInlineUntil(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.Persmission.Denied.SendInline.Until", p1) + } + /// Sending media isn't allowed in this group. + internal static var channelPersmissionDeniedSendMediaDefaultRestrictedText: String { return L10n.tr("Localizable", "Channel.Persmission.Denied.SendMedia.DefaultRestrictedText") } + /// The admins of this group have restricted you from sending media here. + internal static var channelPersmissionDeniedSendMediaForever: String { return L10n.tr("Localizable", "Channel.Persmission.Denied.SendMedia.Forever") } + /// The admins of this group have restricted you from sending media here until %@. + internal static func channelPersmissionDeniedSendMediaUntil(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.Persmission.Denied.SendMedia.Until", p1) + } + /// Writing messages isn’t allowed in this group. + internal static var channelPersmissionDeniedSendMessagesDefaultRestrictedText: String { return L10n.tr("Localizable", "Channel.Persmission.Denied.SendMessages.DefaultRestrictedText") } /// The admins of this group have restricted you from writing here - case channelPersmissionDeniedSendMessagesForever - /// The admins of this group have restricted you from writing here until %@ - case channelPersmissionDeniedSendMessagesUntil(String) - /// The admins of this group have restricted you from sending stickers here - case channelPersmissionDeniedSendStickersForever - /// The admins of this group have restricted you from sending stickers here until %@ - case channelPersmissionDeniedSendStickersUntil(String) + internal static var channelPersmissionDeniedSendMessagesForever: String { return L10n.tr("Localizable", "Channel.Persmission.Denied.SendMessages.Forever") } + /// The admins of this group have restricted you from writing here until %@. + internal static func channelPersmissionDeniedSendMessagesUntil(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.Persmission.Denied.SendMessages.Until", p1) + } + /// Posting polls isn't allowed in this group. + internal static var channelPersmissionDeniedSendPollDefaultRestrictedText: String { return L10n.tr("Localizable", "Channel.Persmission.Denied.SendPoll.DefaultRestrictedText") } + /// The admins of this group have restricted you from posting polls here. + internal static var channelPersmissionDeniedSendPollForever: String { return L10n.tr("Localizable", "Channel.Persmission.Denied.SendPoll.Forever") } + /// The admins of this group have restricted you from posting polls here until %@. + internal static func channelPersmissionDeniedSendPollUntil(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.Persmission.Denied.SendPoll.Until", p1) + } + /// Sending stickers isn't allowed in this group. + internal static var channelPersmissionDeniedSendStickersDefaultRestrictedText: String { return L10n.tr("Localizable", "Channel.Persmission.Denied.SendStickers.DefaultRestrictedText") } + /// The admins of this group have restricted you from sending stickers here. + internal static var channelPersmissionDeniedSendStickersForever: String { return L10n.tr("Localizable", "Channel.Persmission.Denied.SendStickers.Forever") } + /// The admins of this group have restricted you from sending stickers here until %@. + internal static func channelPersmissionDeniedSendStickersUntil(_ p1: String) -> String { + return L10n.tr("Localizable", "Channel.Persmission.Denied.SendStickers.Until", p1) + } + /// Revoke Link + internal static var channelRevokeLinkConfirmHeader: String { return L10n.tr("Localizable", "Channel.RevokeLink.Confirm.Header") } + /// Revoke + internal static var channelRevokeLinkConfirmOK: String { return L10n.tr("Localizable", "Channel.RevokeLink.Confirm.OK") } + /// Are you sure you want to revoke this link? Once you do, no one will be able to join the group using it. + internal static var channelRevokeLinkConfirmText: String { return L10n.tr("Localizable", "Channel.RevokeLink.Confirm.Text") } /// contacts - case channelSelectPeersContacts + internal static var channelSelectPeersContacts: String { return L10n.tr("Localizable", "Channel.SelectPeers.Contacts") } /// global - case channelSelectPeersGlobal + internal static var channelSelectPeersGlobal: String { return L10n.tr("Localizable", "Channel.SelectPeers.Global") } + /// Off + internal static var channelSlowModeOff: String { return L10n.tr("Localizable", "Channel.SlowMode.Off") } + /// Slowmode is enabled.\nYou can send your next message in %@:%@ + internal static func channelSlowModeToolTip(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Channel.SlowMode.ToolTip", p1, p2) + } + /// **Preparing stats**\nPlease wait a few moments while we generate your stats + internal static var channelStatsLoading: String { return L10n.tr("Localizable", "Channel.Stats.Loading") } + /// Sorry, this channel has too many admins and the new owner can't be added. Please remove one of the existing admins first. + internal static var channelTransferOwnerErrorAdminsTooMuch: String { return L10n.tr("Localizable", "Channel.TransferOwner.ErrorAdminsTooMuch") } + /// Sorry, this user is not a member of this channel and their privacy settings prevent you from adding them manually. + internal static var channelTransferOwnerErrorPrivacyRestricted: String { return L10n.tr("Localizable", "Channel.TransferOwner.ErrorPrivacyRestricted") } + /// Sorry, the target user has too many public groups or channels already. Please ask them to make one of their existing groups or channels private first. + internal static var channelTransferOwnerErrorPublicChannelsTooMuch: String { return L10n.tr("Localizable", "Channel.TransferOwner.ErrorPublicChannelsTooMuch") } + /// Enable 2-Step Verification. + internal static var channelTransferOwnerErrorEnable2FA: String { return L10n.tr("Localizable", "Channel.TransferOwner.Error.Enable2FA") } + /// Ownership transfers are only available if:\n\n• 2-Step Verification was enabled for your account more than 7 days ago.\n\n• You have logged in on this device more than 24 hours ago.\n\nPlease come back later. + internal static var channelTransferOwnerErrorText: String { return L10n.tr("Localizable", "Channel.TransferOwner.Error.Text") } + /// Security Check + internal static var channelTransferOwnerErrorTitle: String { return L10n.tr("Localizable", "Channel.TransferOwner.Error.Title") } /// Recent Actions - case channelAdminsRecentActions - /// Add Member - case channelBlacklistAddMember + internal static var channelAdminsRecentActions: String { return L10n.tr("Localizable", "ChannelAdmins.RecentActions") } /// BLOCKED - case channelBlacklistBlocked - /// Blacklisted users are removed from the group and can only come back if invited by an admin. Invite links don't work for them. - case channelBlacklistEmptyDescrpition + internal static var channelBlacklistBlocked: String { return L10n.tr("Localizable", "ChannelBlacklist.Blocked") } + /// Blacklisted users are removed from the group and can only come back if they are invited back by an admin. Invite links won't work for blacklisted users. + internal static var channelBlacklistEmptyDescrpition: String { return L10n.tr("Localizable", "ChannelBlacklist.EmptyDescrpition") } /// RESTRICTED - case channelBlacklistRestricted + internal static var channelBlacklistRestricted: String { return L10n.tr("Localizable", "ChannelBlacklist.Restricted") } /// Channel Info - case channelEventFilterChannelInfo + internal static var channelEventFilterChannelInfo: String { return L10n.tr("Localizable", "ChannelEventFilter.ChannelInfo") } /// Deleted Messages - case channelEventFilterDeletedMessages + internal static var channelEventFilterDeletedMessages: String { return L10n.tr("Localizable", "ChannelEventFilter.DeletedMessages") } /// Edited Messages - case channelEventFilterEditedMessages + internal static var channelEventFilterEditedMessages: String { return L10n.tr("Localizable", "ChannelEventFilter.EditedMessages") } /// Group Info - case channelEventFilterGroupInfo + internal static var channelEventFilterGroupInfo: String { return L10n.tr("Localizable", "ChannelEventFilter.GroupInfo") } + /// Invite Links + internal static var channelEventFilterInvites: String { return L10n.tr("Localizable", "ChannelEventFilter.Invites") } /// Members Removed - case channelEventFilterLeavingMembers + internal static var channelEventFilterLeavingMembers: String { return L10n.tr("Localizable", "ChannelEventFilter.LeavingMembers") } /// New Admins - case channelEventFilterNewAdmins + internal static var channelEventFilterNewAdmins: String { return L10n.tr("Localizable", "ChannelEventFilter.NewAdmins") } /// New Members - case channelEventFilterNewMembers + internal static var channelEventFilterNewMembers: String { return L10n.tr("Localizable", "ChannelEventFilter.NewMembers") } /// New Restrictions - case channelEventFilterNewRestrictions + internal static var channelEventFilterNewRestrictions: String { return L10n.tr("Localizable", "ChannelEventFilter.NewRestrictions") } /// Pinned Messages - case channelEventFilterPinnedMessages + internal static var channelEventFilterPinnedMessages: String { return L10n.tr("Localizable", "ChannelEventFilter.PinnedMessages") } + /// Voice Chats + internal static var channelEventFilterVoiceChats: String { return L10n.tr("Localizable", "ChannelEventFilter.VoiceChats") } + /// Sorry, if a person left a channel, only a mutual contact can bring them back (they need to have your phone number, and you need theirs). + internal static var channelInfoAddUserLeftError: String { return L10n.tr("Localizable", "ChannelInfo.AddUserLeftError") } + /// ⚠️ Warning: Many users reported that this channel impersonates a famous person or organization. + internal static var channelInfoFakeWarning: String { return L10n.tr("Localizable", "ChannelInfo.FakeWarning") } + /// ⚠️ Warning: Many users reported this channel as a scam. Please be careful, especially if it asks you for money. + internal static var channelInfoScamWarning: String { return L10n.tr("Localizable", "ChannelInfo.ScamWarning") } /// Add Members - case channelMembersAddMembers + internal static var channelMembersAddMembers: String { return L10n.tr("Localizable", "ChannelMembers.AddMembers") } + /// Add Subscribers + internal static var channelMembersAddSubscribers: String { return L10n.tr("Localizable", "ChannelMembers.AddSubscribers") } /// Invite via Link - case channelMembersInviteLink + internal static var channelMembersInviteLink: String { return L10n.tr("Localizable", "ChannelMembers.InviteLink") } /// Only channel admins can see this list. - case channelMembersMembersListDesc + internal static var channelMembersMembersListDesc: String { return L10n.tr("Localizable", "ChannelMembers.MembersListDesc") } /// Add Members - case channelMembersSelectTitle - /// Checking - case channelVisibilityChecking + internal static var channelMembersSelectTitle: String { return L10n.tr("Localizable", "ChannelMembers.Select.Title") } + /// OVERVIEW + internal static var channelStatsOverview: String { return L10n.tr("Localizable", "ChannelStats.Overview") } + /// %d + internal static func channelStatsSharesCountCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelStats.SharesCount_countable", p1) + } + /// %d shares + internal static func channelStatsSharesCountFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelStats.SharesCount_few", p1) + } + /// %d shares + internal static func channelStatsSharesCountMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelStats.SharesCount_many", p1) + } + /// %d shares + internal static func channelStatsSharesCountOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelStats.SharesCount_one", p1) + } + /// %d shares + internal static func channelStatsSharesCountOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelStats.SharesCount_other", p1) + } + /// %d shares + internal static func channelStatsSharesCountTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelStats.SharesCount_two", p1) + } + /// No shares + internal static var channelStatsSharesCountZero: String { return L10n.tr("Localizable", "ChannelStats.SharesCount_zero") } + /// Channel Statistics + internal static var channelStatsTitle: String { return L10n.tr("Localizable", "ChannelStats.Title") } + /// %d + internal static func channelStatsViewsCountCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelStats.ViewsCount_countable", p1) + } + /// %d views + internal static func channelStatsViewsCountFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelStats.ViewsCount_few", p1) + } + /// %d views + internal static func channelStatsViewsCountMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelStats.ViewsCount_many", p1) + } + /// %d views + internal static func channelStatsViewsCountOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelStats.ViewsCount_one", p1) + } + /// %d views + internal static func channelStatsViewsCountOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelStats.ViewsCount_other", p1) + } + /// %d views + internal static func channelStatsViewsCountTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChannelStats.ViewsCount_two", p1) + } + /// No views + internal static var channelStatsViewsCountZero: String { return L10n.tr("Localizable", "ChannelStats.ViewsCount_zero") } + /// FOLLOWERS + internal static var channelStatsGraphFollowers: String { return L10n.tr("Localizable", "ChannelStats.Graph.Followers") } + /// GROWTH + internal static var channelStatsGraphGrowth: String { return L10n.tr("Localizable", "ChannelStats.Graph.Growth") } + /// INTERACTIONS + internal static var channelStatsGraphInteractions: String { return L10n.tr("Localizable", "ChannelStats.Graph.Interactions") } + /// LANGUAGE + internal static var channelStatsGraphLanguage: String { return L10n.tr("Localizable", "ChannelStats.Graph.Language") } + /// FOLLOWERS BY SOURCE + internal static var channelStatsGraphNewFollowersBySource: String { return L10n.tr("Localizable", "ChannelStats.Graph.NewFollowersBySource") } + /// NOTIFICATIONS + internal static var channelStatsGraphNotifications: String { return L10n.tr("Localizable", "ChannelStats.Graph.Notifications") } + /// VIEWS BY HOURS (UTC) + internal static var channelStatsGraphViewsByHours: String { return L10n.tr("Localizable", "ChannelStats.Graph.ViewsByHours") } + /// VIEWS BY SOURCE + internal static var channelStatsGraphViewsBySource: String { return L10n.tr("Localizable", "ChannelStats.Graph.ViewsBySource") } + /// Enabled Notifications + internal static var channelStatsOverviewEnabledNotifications: String { return L10n.tr("Localizable", "ChannelStats.Overview.EnabledNotifications") } + /// Followers + internal static var channelStatsOverviewFollowers: String { return L10n.tr("Localizable", "ChannelStats.Overview.Followers") } + /// Shares Per Post + internal static var channelStatsOverviewSharesPerPost: String { return L10n.tr("Localizable", "ChannelStats.Overview.SharesPerPost") } + /// Views Per Post + internal static var channelStatsOverviewViewsPerPost: String { return L10n.tr("Localizable", "ChannelStats.Overview.ViewsPerPost") } + /// RECENT POSTS + internal static var channelStatsRecentHeader: String { return L10n.tr("Localizable", "ChannelStats.Recent.Header") } + /// Checking... + internal static var channelVisibilityChecking: String { return L10n.tr("Localizable", "ChannelVisibility.Checking") } /// Loading... - case channelVisibilityLoading + internal static var channelVisibilityLoading: String { return L10n.tr("Localizable", "ChannelVisibility.Loading") } + /// Are you sure you want to make this channel private and remove its username? + internal static var channelVisibilityConfirmRevoke: String { return L10n.tr("Localizable", "ChannelVisibility.Confirm.Revoke") } + /// If you make this channel private, the name @%@ will be removed. Anyone else will be able to take it for their public groups or channels. + internal static func channelVisibilityConfirmMakePrivateChannel(_ p1: String) -> String { + return L10n.tr("Localizable", "ChannelVisibility.Confirm.MakePrivate.Channel", p1) + } + /// If you make this group private, the name @%@ will be removed. Anyone else will be able to take it for their public groups or channels. + internal static func channelVisibilityConfirmMakePrivateGroup(_ p1: String) -> String { + return L10n.tr("Localizable", "ChannelVisibility.Confirm.MakePrivate.Group", p1) + } + /// Manage Links + internal static var channelVisibiltiyManageLinks: String { return L10n.tr("Localizable", "ChannelVisibiltiy.ManageLinks") } + /// PERMANENT LINK + internal static var channelVisibiltiyPermanentLink: String { return L10n.tr("Localizable", "ChannelVisibiltiy.PermanentLink") } + /// Copy + internal static var channelVisibiltiyContextCopy: String { return L10n.tr("Localizable", "ChannelVisibiltiy.Context.Copy") } + /// Revoke + internal static var channelVisibiltiyContextRevoke: String { return L10n.tr("Localizable", "ChannelVisibiltiy.Context.Revoke") } /// admin - case chatAdminBadge + internal static var chatAdminBadge: String { return L10n.tr("Localizable", "Chat.AdminBadge") } + /// ADD PROXY + internal static var chatApplyProxy: String { return L10n.tr("Localizable", "Chat.ApplyProxy") } /// Cancel - case chatCancel + internal static var chatCancel: String { return L10n.tr("Localizable", "Chat.Cancel") } + /// channel + internal static var chatChannelBadge: String { return L10n.tr("Localizable", "Chat.ChannelBadge") } + /// Copy Selected Text + internal static var chatCopySelectedText: String { return L10n.tr("Localizable", "Chat.CopySelectedText") } /// without compression - case chatDropAsFilesDesc + internal static var chatDropAsFilesDesc: String { return L10n.tr("Localizable", "Chat.DropAsFilesDesc") } + /// Edit Media + internal static var chatDropEditDesc: String { return L10n.tr("Localizable", "Chat.DropEditDesc") } + /// Drop file there to edit media + internal static var chatDropEditTitle: String { return L10n.tr("Localizable", "Chat.DropEditTitle") } /// in a quick way - case chatDropQuickDesc + internal static var chatDropQuickDesc: String { return L10n.tr("Localizable", "Chat.DropQuickDesc") } /// Drop files here to send them - case chatDropTitle + internal static var chatDropTitle: String { return L10n.tr("Localizable", "Chat.DropTitle") } /// No messages here yet - case chatEmptyChat + internal static var chatEmptyChat: String { return L10n.tr("Localizable", "Chat.EmptyChat") } /// Forward Messages - case chatForwardActionHeader + internal static var chatForwardActionHeader: String { return L10n.tr("Localizable", "Chat.ForwardActionHeader") } /// INSTANT VIEW - case chatInstantView + internal static var chatInstantView: String { return L10n.tr("Localizable", "Chat.InstantView") } + /// Live Location + internal static var chatLiveLocation: String { return L10n.tr("Localizable", "Chat.LiveLocation") } + /// owner + internal static var chatOwnerBadge: String { return L10n.tr("Localizable", "Chat.OwnerBadge") } /// %d of %d - case chatSearchCount(Int, Int) + internal static func chatSearchCount(_ p1: Int, _ p2: Int) -> String { + return L10n.tr("Localizable", "Chat.SearchCount", p1, p2) + } /// from: - case chatSearchFrom + internal static var chatSearchFrom: String { return L10n.tr("Localizable", "Chat.SearchFrom") } + /// Sorry, you can only send messages to mutual contacts at the moment. + internal static var chatSendMessageErrorFlood: String { return L10n.tr("Localizable", "Chat.SendMessageErrorFlood") } + /// Sorry, you are currently restricted from posting to public groups. + internal static var chatSendMessageErrorGroupRestricted: String { return L10n.tr("Localizable", "Chat.SendMessageErrorGroupRestricted") } + /// Slowmode is enabled. + internal static var chatSendMessageSlowmodeError: String { return L10n.tr("Localizable", "Chat.SendMessageSlowmodeError") } /// Share - case chatShareInlineResultActionHeader + internal static var chatShareInlineResultActionHeader: String { return L10n.tr("Localizable", "Chat.ShareInlineResultActionHeader") } + /// Feed + internal static var chatTitleFeed: String { return L10n.tr("Localizable", "Chat.TitleFeed") } + /// %d + internal static func chatUnpinAllMessagesConfirmationCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UnpinAllMessagesConfirmation_countable", p1) + } + /// Do you want to unpin all %d messages in this chat? + internal static func chatUnpinAllMessagesConfirmationFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UnpinAllMessagesConfirmation_few", p1) + } + /// Do you want to unpin all %d messages in this chat? + internal static func chatUnpinAllMessagesConfirmationMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UnpinAllMessagesConfirmation_many", p1) + } + /// Do you want to unpin all %d message in this chat? + internal static func chatUnpinAllMessagesConfirmationOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UnpinAllMessagesConfirmation_one", p1) + } + /// Do you want to unpin all %d messages in this chat? + internal static func chatUnpinAllMessagesConfirmationOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UnpinAllMessagesConfirmation_other", p1) + } + /// Do you want to unpin all %d messages in this chat? + internal static func chatUnpinAllMessagesConfirmationTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UnpinAllMessagesConfirmation_two", p1) + } + /// Do you want to unpin all %d messages in this chat? + internal static func chatUnpinAllMessagesConfirmationZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UnpinAllMessagesConfirmation_zero", p1) + } + /// VIEW BACKGROUND + internal static var chatViewBackground: String { return L10n.tr("Localizable", "Chat.ViewBackground") } + /// VIEW CONTACT + internal static var chatViewContact: String { return L10n.tr("Localizable", "Chat.ViewContact") } + /// %d + internal static func chatAccessoryForwardCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Accessory.Forward_countable", p1) + } + /// Forward %d Messages + internal static func chatAccessoryForwardFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Accessory.Forward_few", p1) + } + /// Forward %d Messages + internal static func chatAccessoryForwardMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Accessory.Forward_many", p1) + } + /// Forward Message + internal static var chatAccessoryForwardOne: String { return L10n.tr("Localizable", "Chat.Accessory.Forward_one") } + /// Forward %d Messages + internal static func chatAccessoryForwardOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Accessory.Forward_other", p1) + } + /// Forward %d Messages + internal static func chatAccessoryForwardTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Accessory.Forward_two", p1) + } + /// Forward Messages + internal static var chatAccessoryForwardZero: String { return L10n.tr("Localizable", "Chat.Accessory.Forward_zero") } + /// %d + internal static func chatAccessoryHiddenCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Accessory.Hidden_countable", p1) + } + /// Forward %d Messages (sender's names hidden) + internal static func chatAccessoryHiddenFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Accessory.Hidden_few", p1) + } + /// Forward %d Messages (sender's names hidden) + internal static func chatAccessoryHiddenMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Accessory.Hidden_many", p1) + } + /// Forward Message (sender's names hidden) + internal static var chatAccessoryHiddenOne: String { return L10n.tr("Localizable", "Chat.Accessory.Hidden_one") } + /// Forward %d Messages (sender's names hidden) + internal static func chatAccessoryHiddenOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Accessory.Hidden_other", p1) + } + /// Forward %d Messages (sender's names hidden) + internal static func chatAccessoryHiddenTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Accessory.Hidden_two", p1) + } + /// Forward Messages (sender's names hidden) + internal static var chatAccessoryHiddenZero: String { return L10n.tr("Localizable", "Chat.Accessory.Hidden_zero") } + /// From + internal static var chatAccessoryForwardFrom: String { return L10n.tr("Localizable", "Chat.Accessory.Forward.From") } + /// You + internal static var chatAccessoryForwardYou: String { return L10n.tr("Localizable", "Chat.Accessory.Forward.You") } + /// VIEW THEME + internal static var chatActionViewTheme: String { return L10n.tr("Localizable", "Chat.Action.ViewTheme") } + /// %d + internal static func chatAlertForwardHeaderCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Alert.Forward.Header_countable", p1) + } + /// %d Messages + internal static func chatAlertForwardHeaderFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Alert.Forward.Header_few", p1) + } + /// %d Messages + internal static func chatAlertForwardHeaderMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Alert.Forward.Header_many", p1) + } + /// %d Message + internal static func chatAlertForwardHeaderOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Alert.Forward.Header_one", p1) + } + /// %d Messages + internal static func chatAlertForwardHeaderOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Alert.Forward.Header_other", p1) + } + /// %d Messages + internal static func chatAlertForwardHeaderTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Alert.Forward.Header_two", p1) + } + /// %d Messages + internal static func chatAlertForwardHeaderZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Alert.Forward.Header_zero", p1) + } + /// What would you like to do with %1$@ from %2$@? + internal static func chatAlertForwardText(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Chat.Alert.Forward.Text", p1, p2) + } + /// Forward to Another Chat + internal static var chatAlertForwardActionAnother: String { return L10n.tr("Localizable", "Chat.Alert.Forward.Action.Another") } + /// Cancel Forwarding + internal static var chatAlertForwardActionCancel: String { return L10n.tr("Localizable", "Chat.Alert.Forward.Action.Cancel") } + /// Hide Sender's Names + internal static var chatAlertForwardActionHide: String { return L10n.tr("Localizable", "Chat.Alert.Forward.Action.Hide") } + /// %d + internal static func chatAlertForwardActionHide1Countable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Alert.Forward.Action.Hide1_countable", p1) + } + /// Hide Sender's Names + internal static var chatAlertForwardActionHide1Few: String { return L10n.tr("Localizable", "Chat.Alert.Forward.Action.Hide1_few") } + /// Hide Sender's Names + internal static var chatAlertForwardActionHide1Many: String { return L10n.tr("Localizable", "Chat.Alert.Forward.Action.Hide1_many") } + /// Hide Sender Name + internal static var chatAlertForwardActionHide1One: String { return L10n.tr("Localizable", "Chat.Alert.Forward.Action.Hide1_one") } + /// Hide Sender's Names + internal static var chatAlertForwardActionHide1Other: String { return L10n.tr("Localizable", "Chat.Alert.Forward.Action.Hide1_other") } + /// Hide Sender's Names + internal static var chatAlertForwardActionHide1Two: String { return L10n.tr("Localizable", "Chat.Alert.Forward.Action.Hide1_two") } + /// Hide Sender's Names + internal static var chatAlertForwardActionHide1Zero: String { return L10n.tr("Localizable", "Chat.Alert.Forward.Action.Hide1_zero") } + /// Show Sender's Names + internal static var chatAlertForwardActionShow: String { return L10n.tr("Localizable", "Chat.Alert.Forward.Action.Show") } + /// %d + internal static func chatAlertForwardActionShow1Countable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Alert.Forward.Action.Show1_countable", p1) + } + /// Show Sender's Names + internal static var chatAlertForwardActionShow1Few: String { return L10n.tr("Localizable", "Chat.Alert.Forward.Action.Show1_few") } + /// Show Sender's Names + internal static var chatAlertForwardActionShow1Many: String { return L10n.tr("Localizable", "Chat.Alert.Forward.Action.Show1_many") } + /// Show Sender Name + internal static var chatAlertForwardActionShow1One: String { return L10n.tr("Localizable", "Chat.Alert.Forward.Action.Show1_one") } + /// Show Sender's Names + internal static var chatAlertForwardActionShow1Other: String { return L10n.tr("Localizable", "Chat.Alert.Forward.Action.Show1_other") } + /// Show Sender's Names + internal static var chatAlertForwardActionShow1Two: String { return L10n.tr("Localizable", "Chat.Alert.Forward.Action.Show1_two") } + /// Show Sender's Names + internal static var chatAlertForwardActionShow1Zero: String { return L10n.tr("Localizable", "Chat.Alert.Forward.Action.Show1_zero") } + /// %d + internal static func chatAlertForwardTextInnerCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Alert.Forward.Text.Inner_countable", p1) + } + /// %d messages + internal static func chatAlertForwardTextInnerFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Alert.Forward.Text.Inner_few", p1) + } + /// %d messages + internal static func chatAlertForwardTextInnerMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Alert.Forward.Text.Inner_many", p1) + } + /// %d message + internal static func chatAlertForwardTextInnerOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Alert.Forward.Text.Inner_one", p1) + } + /// %d messages + internal static func chatAlertForwardTextInnerOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Alert.Forward.Text.Inner_other", p1) + } + /// %d messages + internal static func chatAlertForwardTextInnerTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Alert.Forward.Text.Inner_two", p1) + } + /// %d messages + internal static func chatAlertForwardTextInnerZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Alert.Forward.Text.Inner_zero", p1) + } + /// Forwarded from: [%@]() + internal static func chatBubblesForwardedFrom(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Bubbles.ForwardedFrom", p1) + } /// Incoming Call - case chatCallIncoming + internal static var chatCallIncoming: String { return L10n.tr("Localizable", "Chat.Call.Incoming") } /// Outgoing Call - case chatCallOutgoing + internal static var chatCallOutgoing: String { return L10n.tr("Localizable", "Chat.Call.Outgoing") } + /// Sorry, this channel is not accessible. + internal static var chatChannelUnaccessible: String { return L10n.tr("Localizable", "Chat.Channel.Unaccessible") } + /// Apply Theme + internal static var chatChatThemeApplyTheme: String { return L10n.tr("Localizable", "Chat.ChatTheme.ApplyTheme") } + /// Cancel + internal static var chatChatThemeCancel: String { return L10n.tr("Localizable", "Chat.ChatTheme.Cancel") } + /// No\nTheme + internal static var chatChatThemeNoTheme: String { return L10n.tr("Localizable", "Chat.ChatTheme.NoTheme") } + /// You have been blocked to posting comments. + internal static var chatCommentsKicked: String { return L10n.tr("Localizable", "Chat.Comments.Kicked") } + /// No comments here yet... + internal static var chatCommentsHeaderEmpty: String { return L10n.tr("Localizable", "Chat.CommentsHeader.Empty") } + /// Discussion started + internal static var chatCommentsHeaderFull: String { return L10n.tr("Localizable", "Chat.CommentsHeader.Full") } /// This action can't be undone - case chatConfirmActionUndonable + internal static var chatConfirmActionUndonable: String { return L10n.tr("Localizable", "Chat.Confirm.ActionUndonable") } + /// %d + internal static func chatConfirmDeleteForEveryoneCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Confirm.DeleteForEveryone_countable", p1) + } + /// Are you sure you want to delete this messages for everyone? + internal static var chatConfirmDeleteForEveryoneFew: String { return L10n.tr("Localizable", "Chat.Confirm.DeleteForEveryone_few") } + /// Are you sure you want to delete this messages for everyone? + internal static var chatConfirmDeleteForEveryoneMany: String { return L10n.tr("Localizable", "Chat.Confirm.DeleteForEveryone_many") } + /// Are you sure you want to delete this message for everyone? + internal static var chatConfirmDeleteForEveryoneOne: String { return L10n.tr("Localizable", "Chat.Confirm.DeleteForEveryone_one") } + /// Are you sure you want to delete this messages for everyone? + internal static var chatConfirmDeleteForEveryoneOther: String { return L10n.tr("Localizable", "Chat.Confirm.DeleteForEveryone_other") } + /// Are you sure you want to delete this messages for everyone? + internal static var chatConfirmDeleteForEveryoneTwo: String { return L10n.tr("Localizable", "Chat.Confirm.DeleteForEveryone_two") } + /// Are you sure you want to delete this messages for everyone? + internal static var chatConfirmDeleteForEveryoneZero: String { return L10n.tr("Localizable", "Chat.Confirm.DeleteForEveryone_zero") } + /// %d + internal static func chatConfirmDeleteMessages1Countable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Confirm.DeleteMessages1_countable", p1) + } /// Delete selected messages? - case chatConfirmDeleteMessages - /// Delete for All - case chatConfirmDeleteMessagesForEveryone + internal static var chatConfirmDeleteMessages1Few: String { return L10n.tr("Localizable", "Chat.Confirm.DeleteMessages1_few") } + /// Delete selected messages? + internal static var chatConfirmDeleteMessages1Many: String { return L10n.tr("Localizable", "Chat.Confirm.DeleteMessages1_many") } + /// Delete selected message? + internal static var chatConfirmDeleteMessages1One: String { return L10n.tr("Localizable", "Chat.Confirm.DeleteMessages1_one") } + /// Delete selected messages? + internal static var chatConfirmDeleteMessages1Other: String { return L10n.tr("Localizable", "Chat.Confirm.DeleteMessages1_other") } + /// Delete selected messages? + internal static var chatConfirmDeleteMessages1Two: String { return L10n.tr("Localizable", "Chat.Confirm.DeleteMessages1_two") } + /// Delete selected messages? + internal static var chatConfirmDeleteMessages1Zero: String { return L10n.tr("Localizable", "Chat.Confirm.DeleteMessages1_zero") } + /// Delete for Everyone + internal static var chatConfirmDeleteMessagesForEveryone: String { return L10n.tr("Localizable", "Chat.Confirm.DeleteMessagesForEveryone") } + /// Pin for me and %@ + internal static func chatConfirmPinFor(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Confirm.PinFor", p1) + } + /// Do you want to pin an older message while leaving a more recent one pinned? + internal static var chatConfirmPinOld: String { return L10n.tr("Localizable", "Chat.Confirm.PinOld") } + /// Report Spam? + internal static var chatConfirmReportSpam: String { return L10n.tr("Localizable", "Chat.Confirm.ReportSpam") } + /// Are you sure you want to report spam from this user? + internal static var chatConfirmReportSpamUser: String { return L10n.tr("Localizable", "Chat.Confirm.ReportSpamUser") } /// Would you like to unpin this message? - case chatConfirmUnpin + internal static var chatConfirmUnpin: String { return L10n.tr("Localizable", "Chat.Confirm.Unpin") } + /// Report Spam and leave channel? + internal static var chatConfirmReportSpamChannel: String { return L10n.tr("Localizable", "Chat.Confirm.ReportSpam.Channel") } + /// Report Spam and leave group? + internal static var chatConfirmReportSpamGroup: String { return L10n.tr("Localizable", "Chat.Confirm.ReportSpam.Group") } + /// Report Spam + internal static var chatConfirmReportSpamHeader: String { return L10n.tr("Localizable", "Chat.Confirm.ReportSpam.Header") } + /// Unpin message + internal static var chatConfirmUnpinHeader: String { return L10n.tr("Localizable", "Chat.Confirm.Unpin.Header") } + /// Unpin + internal static var chatConfirmUnpinOK: String { return L10n.tr("Localizable", "Chat.Confirm.Unpin.OK") } /// Connecting - case chatConnectingStatusConnecting + internal static var chatConnectingStatusConnecting: String { return L10n.tr("Localizable", "Chat.ConnectingStatus.connecting") } /// Connecting to proxy - case chatConnectingStatusConnectingToProxy + internal static var chatConnectingStatusConnectingToProxy: String { return L10n.tr("Localizable", "Chat.ConnectingStatus.connectingToProxy") } /// Updating - case chatConnectingStatusUpdating + internal static var chatConnectingStatusUpdating: String { return L10n.tr("Localizable", "Chat.ConnectingStatus.updating") } /// Waiting for network - case chatConnectingStatusWaitingNetwork + internal static var chatConnectingStatusWaitingNetwork: String { return L10n.tr("Localizable", "Chat.ConnectingStatus.waitingNetwork") } /// Add to Favorites - case chatContextAddFavoriteSticker - /// Clear History - case chatContextClearHistory + internal static var chatContextAddFavoriteSticker: String { return L10n.tr("Localizable", "Chat.Context.AddFavoriteSticker") } + /// Archive + internal static var chatContextArchive: String { return L10n.tr("Localizable", "Chat.Context.Archive") } + /// Auto-Delete Messages + internal static var chatContextAutoDelete: String { return L10n.tr("Localizable", "Chat.Context.AutoDelete") } + /// Block Group + internal static var chatContextBlockGroup: String { return L10n.tr("Localizable", "Chat.Context.BlockGroup") } + /// Block User + internal static var chatContextBlockUser: String { return L10n.tr("Localizable", "Chat.Context.BlockUser") } + /// Cancel Editing + internal static var chatContextCancelEditing: String { return L10n.tr("Localizable", "Chat.Context.CancelEditing") } + /// Clear Chat History + internal static var chatContextClearHistory: String { return L10n.tr("Localizable", "Chat.Context.ClearHistory") } + /// Clear All + internal static var chatContextClearScheduled: String { return L10n.tr("Localizable", "Chat.Context.ClearScheduled") } /// Copy Preformatted Block - case chatContextCopyBlock + internal static var chatContextCopyBlock: String { return L10n.tr("Localizable", "Chat.Context.CopyBlock") } + /// Create Group + internal static var chatContextCreateGroup: String { return L10n.tr("Localizable", "Chat.Context.CreateGroup") } /// Unmute - case chatContextDisableNotifications - /// Edit (click on date) - case chatContextEdit + internal static var chatContextDisableNotifications: String { return L10n.tr("Localizable", "Chat.Context.DisableNotifications") } + /// Edit + internal static var chatContextEdit1: String { return L10n.tr("Localizable", "Chat.Context.Edit1") } + /// click on date + internal static var chatContextEditHelp: String { return L10n.tr("Localizable", "Chat.Context.EditHelp") } /// Mute - case chatContextEnableNotifications + internal static var chatContextEnableNotifications: String { return L10n.tr("Localizable", "Chat.Context.EnableNotifications") } + /// Channels Info + internal static var chatContextFeedInfo: String { return L10n.tr("Localizable", "Chat.Context.FeedInfo") } /// Info - case chatContextInfo + internal static var chatContextInfo: String { return L10n.tr("Localizable", "Chat.Context.Info") } /// Remove from Favorites - case chatContextRemoveFavoriteSticker - /// Pinned message - case chatHeaderPinnedMessage + internal static var chatContextRemoveFavoriteSticker: String { return L10n.tr("Localizable", "Chat.Context.RemoveFavoriteSticker") } + /// Restrict + internal static var chatContextRestrict: String { return L10n.tr("Localizable", "Chat.Context.Restrict") } + /// Shared Media + internal static var chatContextSharedMedia: String { return L10n.tr("Localizable", "Chat.Context.SharedMedia") } + /// Unarchive + internal static var chatContextUnarchive: String { return L10n.tr("Localizable", "Chat.Context.Unarchive") } + /// Cancel + internal static var chatContextBlockGroupCancel: String { return L10n.tr("Localizable", "Chat.Context.BlockGroup.Cancel") } + /// Block Group + internal static var chatContextBlockGroupHeader: String { return L10n.tr("Localizable", "Chat.Context.BlockGroup.Header") } + /// Do you want to block messages from %@ + internal static func chatContextBlockGroupInfo(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Context.BlockGroup.Info", p1) + } + /// Block + internal static var chatContextBlockGroupOK: String { return L10n.tr("Localizable", "Chat.Context.BlockGroup.OK") } /// Report Spam - case chatHeaderReportSpam - /// Delete and exit - case chatInputDelete - /// Join - case chatInputJoin + internal static var chatContextBlockGroupThird: String { return L10n.tr("Localizable", "Chat.Context.BlockGroup.Third") } + /// Cancel + internal static var chatContextBlockUserCancel: String { return L10n.tr("Localizable", "Chat.Context.BlockUser.Cancel") } + /// Block User + internal static var chatContextBlockUserHeader: String { return L10n.tr("Localizable", "Chat.Context.BlockUser.Header") } + /// Do you want to block messages from %@ + internal static func chatContextBlockUserInfo(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Context.BlockUser.Info", p1) + } + /// Block + internal static var chatContextBlockUserOK: String { return L10n.tr("Localizable", "Chat.Context.BlockUser.OK") } + /// Report Spam + internal static var chatContextBlockUserThird: String { return L10n.tr("Localizable", "Chat.Context.BlockUser.Third") } + /// Scheduled Messages + internal static var chatContextClearScheduledConfirmHeader: String { return L10n.tr("Localizable", "Chat.Context.ClearScheduled.Confirm.Header") } + /// Are you sure you want to delete all scheduled messages? + internal static var chatContextClearScheduledConfirmInfo: String { return L10n.tr("Localizable", "Chat.Context.ClearScheduled.Confirm.Info") } + /// Clear All + internal static var chatContextClearScheduledConfirmOK: String { return L10n.tr("Localizable", "Chat.Context.ClearScheduled.Confirm.OK") } + /// More... + internal static var chatContextForwardMore: String { return L10n.tr("Localizable", "Chat.Context.Forward.More") } + /// Reschedule + internal static var chatContextScheduledReschedule: String { return L10n.tr("Localizable", "Chat.Context.Scheduled.Reschedule") } + /// Send Now + internal static var chatContextScheduledSendNow: String { return L10n.tr("Localizable", "Chat.Context.Scheduled.SendNow") } + /// Auto-Delete in %@ + internal static func chatContextMenuAutoDelete(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.ContextMenu.AutoDelete", p1) + } + /// Copy Link to Proxy + internal static var chatCopyProxyConfiguration: String { return L10n.tr("Localizable", "Chat.Copy.ProxyConfiguration") } + /// Scheduled for %@ + internal static func chatDateScheduledFor(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Date.ScheduledFor", p1) + } + /// Scheduled for today + internal static var chatDateScheduledForToday: String { return L10n.tr("Localizable", "Chat.Date.ScheduledForToday") } + /// Scheduled until online + internal static var chatDateScheduledUntilOnline: String { return L10n.tr("Localizable", "Chat.Date.ScheduledUntilOnline") } + /// Sorry, this post has been removed from the discussion group. + internal static var chatDiscussionMessageDeleted: String { return L10n.tr("Localizable", "Chat.Discussion.MessageDeleted") } + /// as archive + internal static var chatDropFolderDesc: String { return L10n.tr("Localizable", "Chat.DropFolder.Desc") } + /// Drop the folder here to send + internal static var chatDropFolderTitle: String { return L10n.tr("Localizable", "Chat.DropFolder.Title") } + /// Sorry, you can't attach new media while editing a message. + internal static var chatEditAttachError: String { return L10n.tr("Localizable", "Chat.Edit.Attach.Error") } + /// Are you sure you want to discard all changes? + internal static var chatEditCancelText: String { return L10n.tr("Localizable", "Chat.Edit.Cancel.Text") } + /// Click to edit Media + internal static var chatEditMessageMedia: String { return L10n.tr("Localizable", "Chat.EditMessage.Media") } + /// Send + internal static var chatEmojiSend: String { return L10n.tr("Localizable", "Chat.Emoji.Send") } + /// Send a dart emoji to try your luck. + internal static var chatEmojiDartResultNew: String { return L10n.tr("Localizable", "Chat.Emoji.Dart.ResultNew") } + /// Send a %@ emoji to try your luck. + internal static func chatEmojiDefResultNew(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Emoji.Def.ResultNew", p1) + } + /// Send a dice emoji to roll a die. + internal static var chatEmojiDiceResultNew: String { return L10n.tr("Localizable", "Chat.Emoji.Dice.ResultNew") } + /// No comments here yet + internal static var chatEmptyComments: String { return L10n.tr("Localizable", "Chat.Empty.Comments") } + /// Link Preview + internal static var chatEmptyLinkPreview: String { return L10n.tr("Localizable", "Chat.Empty.LinkPreview") } + /// No replies here yet + internal static var chatEmptyReplies: String { return L10n.tr("Localizable", "Chat.Empty.Replies") } + /// Previewing this file can potentially expose your IP address to its sender. + internal static var chatFileQuickLookSvg: String { return L10n.tr("Localizable", "Chat.File.QuickLook.Svg") } + /// Only admins can send messages in this group. + internal static var chatGigagroupHelp: String { return L10n.tr("Localizable", "Chat.Gigagroup.Help") } + /// Sorry, this group is not accessible. + internal static var chatGroupUnaccessible: String { return L10n.tr("Localizable", "Chat.Group.Unaccessible") } + /// JOIN + internal static var chatGroupCallJoin: String { return L10n.tr("Localizable", "Chat.GroupCall.Join") } + /// %d + internal static func chatGroupCallMembersCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.GroupCall.Members_countable", p1) + } + /// %d participants + internal static func chatGroupCallMembersFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.GroupCall.Members_few", p1) + } + /// %d participants + internal static func chatGroupCallMembersMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.GroupCall.Members_many", p1) + } + /// %d participant + internal static func chatGroupCallMembersOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.GroupCall.Members_one", p1) + } + /// %d participants + internal static func chatGroupCallMembersOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.GroupCall.Members_other", p1) + } + /// %d participants + internal static func chatGroupCallMembersTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.GroupCall.Members_two", p1) + } + /// Click to join + internal static var chatGroupCallMembersZero: String { return L10n.tr("Localizable", "Chat.GroupCall.Members_zero") } + /// %d + internal static func chatGroupCallSpeakersCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.GroupCall.Speakers_countable", p1) + } + /// %d participants speaking + internal static func chatGroupCallSpeakersFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.GroupCall.Speakers_few", p1) + } + /// %d participants speaking + internal static func chatGroupCallSpeakersMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.GroupCall.Speakers_many", p1) + } + /// %d participant speaking + internal static func chatGroupCallSpeakersOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.GroupCall.Speakers_one", p1) + } + /// %d participants speaking + internal static func chatGroupCallSpeakersOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.GroupCall.Speakers_other", p1) + } + /// %d participants speaking + internal static func chatGroupCallSpeakersTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.GroupCall.Speakers_two", p1) + } + /// no one speaking + internal static var chatGroupCallSpeakersZero: String { return L10n.tr("Localizable", "Chat.GroupCall.Speakers_zero") } + /// Voice Chat + internal static var chatGroupCallTitle: String { return L10n.tr("Localizable", "Chat.GroupCall.Title") } + /// Live Stream + internal static var chatGroupCallLiveTitle: String { return L10n.tr("Localizable", "Chat.GroupCall.Live.Title") } + /// Starts %@ + internal static func chatGroupCallScheduledStatus(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.GroupCall.Scheduled.Status", p1) + } + /// Scheduled Voice Chat + internal static var chatGroupCallScheduledTitle: String { return L10n.tr("Localizable", "Chat.GroupCall.Scheduled.Title") } + /// Pinned message + internal static var chatHeaderPinnedMessage: String { return L10n.tr("Localizable", "Chat.Header.PinnedMessage") } + /// Pinned message #%d + internal static func chatHeaderPinnedMessageNumer(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Header.PinnedMessage_Numer", p1) + } + /// Previous message + internal static var chatHeaderPinnedPrevious: String { return L10n.tr("Localizable", "Chat.Header.PinnedPrevious") } + /// Report Spam + internal static var chatHeaderReportSpam: String { return L10n.tr("Localizable", "Chat.Header.ReportSpam") } + /// Starts in %@ + internal static func chatHeaderVoiceChatStartsIn(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Header.VoiceChat.StartsIn", p1) + } + /// Loading... + internal static var chatInlineRequestLoading: String { return L10n.tr("Localizable", "Chat.InlineRequest.Loading") } + /// Close + internal static var chatInputClose: String { return L10n.tr("Localizable", "Chat.Input.Close") } + /// Delete and exit + internal static var chatInputDelete: String { return L10n.tr("Localizable", "Chat.Input.Delete") } + /// Discuss + internal static var chatInputDiscuss: String { return L10n.tr("Localizable", "Chat.Input.Discuss") } + /// Join + internal static var chatInputJoin: String { return L10n.tr("Localizable", "Chat.Input.Join") } /// Mute - case chatInputMute - /// Return to group - case chatInputReturn + internal static var chatInputMute: String { return L10n.tr("Localizable", "Chat.Input.Mute") } + /// Restart + internal static var chatInputRestart: String { return L10n.tr("Localizable", "Chat.Input.Restart") } + /// Return to the group + internal static var chatInputReturn: String { return L10n.tr("Localizable", "Chat.Input.Return") } /// Start - case chatInputStartBot + internal static var chatInputStartBot: String { return L10n.tr("Localizable", "Chat.Input.StartBot") } /// Unblock - case chatInputUnblock + internal static var chatInputUnblock: String { return L10n.tr("Localizable", "Chat.Input.Unblock") } /// Unmute - case chatInputUnmute + internal static var chatInputUnmute: String { return L10n.tr("Localizable", "Chat.Input.Unmute") } /// Edit Message - case chatInputAccessoryEditMessage - /// Waiting for the user to come online... - case chatInputSecretChatWaitingToOnline + internal static var chatInputAccessoryEditMessage: String { return L10n.tr("Localizable", "Chat.Input.Accessory.EditMessage") } + /// Messages in this chat are automatically deleted 1 day after they have been sent. + internal static var chatInputAutoDelete1Day: String { return L10n.tr("Localizable", "Chat.Input.AutoDelete.1Day") } + /// Messages in this chat are automatically deleted 1 week after they have been sent. + internal static var chatInputAutoDelete7Days: String { return L10n.tr("Localizable", "Chat.Input.AutoDelete.7Days") } + /// %d + internal static func chatInputErrorMessageTooLongCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Input.Error.MessageTooLong_countable", p1) + } + /// Your message is too long to be saved. Please remove %d characters. + internal static func chatInputErrorMessageTooLongFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Input.Error.MessageTooLong_few", p1) + } + /// Your message is too long to be saved. Please remove %d characters. + internal static func chatInputErrorMessageTooLongMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Input.Error.MessageTooLong_many", p1) + } + /// Your message is too long to be saved. Please remove %d character. + internal static func chatInputErrorMessageTooLongOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Input.Error.MessageTooLong_one", p1) + } + /// Your message is too long to be saved. Please remove %d characters. + internal static func chatInputErrorMessageTooLongOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Input.Error.MessageTooLong_other", p1) + } + /// Your message is too long to be saved. Please remove %d characters. + internal static func chatInputErrorMessageTooLongTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Input.Error.MessageTooLong_two", p1) + } + /// Your message is too long to be saved. Please remove %d characters. + internal static func chatInputErrorMessageTooLongZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Input.Error.MessageTooLong_zero", p1) + } + /// You (sender's names hidden) + internal static var chatInputForwardHidden: String { return L10n.tr("Localizable", "Chat.Input.Forward.Hidden") } + /// Waiting for the %@ to get online... + internal static func chatInputSecretChatWaitingToUserOnline(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Input.SecretChat.WaitingToUserOnline", p1) + } /// Contact - case chatListContact + internal static var chatListContact: String { return L10n.tr("Localizable", "Chat.List.Contact") } /// GIF - case chatListGIF + internal static var chatListGIF: String { return L10n.tr("Localizable", "Chat.List.GIF") } /// Video message - case chatListInstantVideo + internal static var chatListInstantVideo: String { return L10n.tr("Localizable", "Chat.List.InstantVideo") } /// Location - case chatListMap + internal static var chatListMap: String { return L10n.tr("Localizable", "Chat.List.Map") } + /// Photo + internal static var chatListPhoto: String { return L10n.tr("Localizable", "Chat.List.Photo") } + /// %d + internal static func chatListPhoto1Countable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.List.Photo1_countable", p1) + } + /// %d Photos + internal static func chatListPhoto1Few(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.List.Photo1_few", p1) + } + /// %d Photos + internal static func chatListPhoto1Many(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.List.Photo1_many", p1) + } /// Photo - case chatListPhoto + internal static var chatListPhoto1One: String { return L10n.tr("Localizable", "Chat.List.Photo1_one") } + /// %d Photos + internal static func chatListPhoto1Other(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.List.Photo1_other", p1) + } + /// %d Photos + internal static func chatListPhoto1Two(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.List.Photo1_two", p1) + } + /// %d Photos + internal static func chatListPhoto1Zero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.List.Photo1_zero", p1) + } /// %@ Sticker - case chatListSticker(String) + internal static func chatListSticker(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.List.Sticker", p1) + } + /// Video + internal static var chatListVideo: String { return L10n.tr("Localizable", "Chat.List.Video") } + /// %d + internal static func chatListVideo1Countable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.List.Video1_countable", p1) + } + /// %d Videos + internal static func chatListVideo1Few(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.List.Video1_few", p1) + } + /// %d Videos + internal static func chatListVideo1Many(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.List.Video1_many", p1) + } /// Video - case chatListVideo + internal static var chatListVideo1One: String { return L10n.tr("Localizable", "Chat.List.Video1_one") } + /// %d Videos + internal static func chatListVideo1Other(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.List.Video1_other", p1) + } + /// %d Videos + internal static func chatListVideo1Two(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.List.Video1_two", p1) + } + /// %d Videos + internal static func chatListVideo1Zero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.List.Video1_zero", p1) + } /// Voice message - case chatListVoice + internal static var chatListVoice: String { return L10n.tr("Localizable", "Chat.List.Voice") } /// Payment: %@ - case chatListServicePaymentSent(String) + internal static func chatListServicePaymentSent(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.List.Service.PaymentSent", p1) + } + /// %d + internal static func chatLiveLocationUpdatedCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.LiveLocation.Updated_countable", p1) + } + /// Updated %d minutes ago + internal static func chatLiveLocationUpdatedFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.LiveLocation.Updated_few", p1) + } + /// Updated %d minutes ago + internal static func chatLiveLocationUpdatedMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.LiveLocation.Updated_many", p1) + } + /// Updated %d minute ago + internal static func chatLiveLocationUpdatedOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.LiveLocation.Updated_one", p1) + } + /// Updated %d minutes ago + internal static func chatLiveLocationUpdatedOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.LiveLocation.Updated_other", p1) + } + /// Updated %d minutes ago + internal static func chatLiveLocationUpdatedTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.LiveLocation.Updated_two", p1) + } + /// Updated %d minutes ago + internal static func chatLiveLocationUpdatedZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.LiveLocation.Updated_zero", p1) + } + /// Updated just now + internal static var chatLiveLocationUpdatedNow: String { return L10n.tr("Localizable", "Chat.LiveLocation.UpdatedNow") } + /// Delete for everyone + internal static var chatMessageDeleteForEveryone: String { return L10n.tr("Localizable", "Chat.Message.DeleteForEveryone") } + /// Delete for me + internal static var chatMessageDeleteForMe: String { return L10n.tr("Localizable", "Chat.Message.DeleteForMe") } + /// Delete for me and %@ + internal static func chatMessageDeleteForMeAndPerson(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Message.DeleteForMeAndPerson", p1) + } /// edited - case chatMessageEdited + internal static var chatMessageEdited: String { return L10n.tr("Localizable", "Chat.Message.edited") } + /// %@ imported + internal static func chatMessageImported(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Message.Imported", p1) + } + /// imported + internal static var chatMessageImportedShort: String { return L10n.tr("Localizable", "Chat.Message.ImportedShort") } + /// sponsored + internal static var chatMessageSponsored: String { return L10n.tr("Localizable", "Chat.Message.Sponsored") } + /// %d + internal static func chatMessageUnsendMessagesCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Message.UnsendMessages_countable", p1) + } + /// Unsend my messages + internal static var chatMessageUnsendMessagesFew: String { return L10n.tr("Localizable", "Chat.Message.UnsendMessages_few") } + /// Unsend my messages + internal static var chatMessageUnsendMessagesMany: String { return L10n.tr("Localizable", "Chat.Message.UnsendMessages_many") } + /// Unsend my message + internal static var chatMessageUnsendMessagesOne: String { return L10n.tr("Localizable", "Chat.Message.UnsendMessages_one") } + /// Unsend my messages + internal static var chatMessageUnsendMessagesOther: String { return L10n.tr("Localizable", "Chat.Message.UnsendMessages_other") } + /// Unsend my messages + internal static var chatMessageUnsendMessagesTwo: String { return L10n.tr("Localizable", "Chat.Message.UnsendMessages_two") } + /// Unsend my messages + internal static var chatMessageUnsendMessagesZero: String { return L10n.tr("Localizable", "Chat.Message.UnsendMessages_zero") } /// This message is not supported by your version of Telegram. Please update to the latest version from the AppStore or install it from https://macos.telegram.org - case chatMessageUnsupported + internal static var chatMessageUnsupported: String { return L10n.tr("Localizable", "Chat.Message.Unsupported") } + /// This message is not supported by your version Telegram. Please update to the latest version. + internal static var chatMessageUnsupportedNew: String { return L10n.tr("Localizable", "Chat.Message.UnsupportedNew") } /// via - case chatMessageVia - /// - Use end-to-end encryption - case chatSecretChat1Feature - /// - Leave no trace on our servers - case chatSecretChat2Feature - /// - Have a self-destruct timer - case chatSecretChat3Feature - /// - Do not allow forwarding - case chatSecretChat4Feature + internal static var chatMessageVia: String { return L10n.tr("Localizable", "Chat.Message.Via") } + /// VIEW BOT + internal static var chatMessageViewBot: String { return L10n.tr("Localizable", "Chat.Message.ViewBot") } + /// VIEW CHANNEL + internal static var chatMessageViewChannel: String { return L10n.tr("Localizable", "Chat.Message.ViewChannel") } + /// VIEW GROUP + internal static var chatMessageViewGroup: String { return L10n.tr("Localizable", "Chat.Message.ViewGroup") } + /// This message was imported from another app. We can't guarantee it's real. + internal static var chatMessageImportedText: String { return L10n.tr("Localizable", "Chat.Message.Imported.Text") } + /// JOIN AS LISTENER + internal static var chatMessageJoinVoiceChatAsListener: String { return L10n.tr("Localizable", "Chat.Message.JoinVoiceChat.AsListener") } + /// JOIN AS SPEAKER + internal static var chatMessageJoinVoiceChatAsSpeaker: String { return L10n.tr("Localizable", "Chat.Message.JoinVoiceChat.AsSpeaker") } + /// MTProxy Configuration + internal static var chatMessageMTProxyConfig: String { return L10n.tr("Localizable", "Chat.Message.MTProxy.Config") } + /// Nobody Listened + internal static var chatMessageReadStatsEmptyListens: String { return L10n.tr("Localizable", "Chat.Message.ReadStats.EmptyListens") } + /// Nobody Viewed + internal static var chatMessageReadStatsEmptyViews: String { return L10n.tr("Localizable", "Chat.Message.ReadStats.EmptyViews") } + /// Nobody Viewed + internal static var chatMessageReadStatsEmptyWatches: String { return L10n.tr("Localizable", "Chat.Message.ReadStats.EmptyWatches") } + /// %d + internal static func chatMessageReadStatsListenedCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Message.ReadStats.Listened_countable", p1) + } + /// %d Listened + internal static func chatMessageReadStatsListenedFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Message.ReadStats.Listened_few", p1) + } + /// %d Listened + internal static func chatMessageReadStatsListenedMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Message.ReadStats.Listened_many", p1) + } + /// %d Listened + internal static func chatMessageReadStatsListenedOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Message.ReadStats.Listened_one", p1) + } + /// %d Listened + internal static func chatMessageReadStatsListenedOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Message.ReadStats.Listened_other", p1) + } + /// %d Listened + internal static func chatMessageReadStatsListenedTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Message.ReadStats.Listened_two", p1) + } + /// %d Listened + internal static func chatMessageReadStatsListenedZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Message.ReadStats.Listened_zero", p1) + } + /// %d + internal static func chatMessageReadStatsSeenCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Message.ReadStats.Seen_countable", p1) + } + /// %d Seen + internal static func chatMessageReadStatsSeenFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Message.ReadStats.Seen_few", p1) + } + /// %d Seen + internal static func chatMessageReadStatsSeenMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Message.ReadStats.Seen_many", p1) + } + /// %d Seen + internal static func chatMessageReadStatsSeenOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Message.ReadStats.Seen_one", p1) + } + /// %d Seen + internal static func chatMessageReadStatsSeenOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Message.ReadStats.Seen_other", p1) + } + /// %d Seen + internal static func chatMessageReadStatsSeenTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Message.ReadStats.Seen_two", p1) + } + /// %d Seen + internal static func chatMessageReadStatsSeenZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Message.ReadStats.Seen_zero", p1) + } + /// %d + internal static func chatMessageReadStatsWatchedCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Message.ReadStats.Watched_countable", p1) + } + /// %d Viewed + internal static func chatMessageReadStatsWatchedFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Message.ReadStats.Watched_few", p1) + } + /// %d Viewed + internal static func chatMessageReadStatsWatchedMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Message.ReadStats.Watched_many", p1) + } + /// %d Viewed + internal static func chatMessageReadStatsWatchedOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Message.ReadStats.Watched_one", p1) + } + /// %d Viewed + internal static func chatMessageReadStatsWatchedOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Message.ReadStats.Watched_other", p1) + } + /// %d Viewed + internal static func chatMessageReadStatsWatchedTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Message.ReadStats.Watched_two", p1) + } + /// %d Viewed + internal static func chatMessageReadStatsWatchedZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Message.ReadStats.Watched_zero", p1) + } + /// SOCKS5 Configuration + internal static var chatMessageSocks5Config: String { return L10n.tr("Localizable", "Chat.Message.Socks5.Config") } + /// https://telegram.org + internal static var chatMessageSponsoredLink: String { return L10n.tr("Localizable", "Chat.Message.Sponsored.Link") } + /// What are sponsored messages? + internal static var chatMessageSponsoredWhat: String { return L10n.tr("Localizable", "Chat.Message.Sponsored.What") } + /// SHOW MESSAGE + internal static var chatMessageActionShowMessage: String { return L10n.tr("Localizable", "Chat.MessageAction.ShowMessage") } + /// Don't Show Pinned Messages + internal static var chatPinnedDontShow: String { return L10n.tr("Localizable", "Chat.Pinned.DontShow") } + /// %d + internal static func chatPinnedUnpinAllCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Pinned.UnpinAll_countable", p1) + } + /// Unpin All %d Messages + internal static func chatPinnedUnpinAllFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Pinned.UnpinAll_few", p1) + } + /// Unpin All %d Messages + internal static func chatPinnedUnpinAllMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Pinned.UnpinAll_many", p1) + } + /// Unpin %d Message + internal static func chatPinnedUnpinAllOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Pinned.UnpinAll_one", p1) + } + /// Unpin All %d Messages + internal static func chatPinnedUnpinAllOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Pinned.UnpinAll_other", p1) + } + /// Unpin All %d Messages + internal static func chatPinnedUnpinAllTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Pinned.UnpinAll_two", p1) + } + /// Unpin All %d Messages + internal static func chatPinnedUnpinAllZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Pinned.UnpinAll_zero", p1) + } + /// %@%% + internal static func chatPollResult(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Poll.Result", p1) + } + /// Stop Poll + internal static var chatPollStop: String { return L10n.tr("Localizable", "Chat.Poll.Stop") } + /// Vote + internal static var chatPollSubmitVote: String { return L10n.tr("Localizable", "Chat.Poll.SubmitVote") } + /// %d + internal static func chatPollTotalVotes1Countable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Poll.TotalVotes1_countable", p1) + } + /// %d votes + internal static func chatPollTotalVotes1Few(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Poll.TotalVotes1_few", p1) + } + /// %d votes + internal static func chatPollTotalVotes1Many(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Poll.TotalVotes1_many", p1) + } + /// %d vote + internal static func chatPollTotalVotes1One(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Poll.TotalVotes1_one", p1) + } + /// %d votes + internal static func chatPollTotalVotes1Other(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Poll.TotalVotes1_other", p1) + } + /// %d votes + internal static func chatPollTotalVotes1Two(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Poll.TotalVotes1_two", p1) + } + /// %d vote + internal static func chatPollTotalVotes1Zero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Poll.TotalVotes1_zero", p1) + } + /// No votes yet + internal static var chatPollTotalVotesEmpty: String { return L10n.tr("Localizable", "Chat.Poll.TotalVotesEmpty") } + /// No votes + internal static var chatPollTotalVotesResultEmpty: String { return L10n.tr("Localizable", "Chat.Poll.TotalVotesResultEmpty") } + /// Retract Vote + internal static var chatPollUnvote: String { return L10n.tr("Localizable", "Chat.Poll.Unvote") } + /// View Results + internal static var chatPollViewResults: String { return L10n.tr("Localizable", "Chat.Poll.ViewResults") } + /// Stop Poll? + internal static var chatPollStopConfirmHeader: String { return L10n.tr("Localizable", "Chat.Poll.Stop.Confirm.Header") } + /// If you stop this poll now, nobody will be able to vote in it anymore. This action cannot be undone. + internal static var chatPollStopConfirmText: String { return L10n.tr("Localizable", "Chat.Poll.Stop.Confirm.Text") } + /// no votes + internal static var chatPollTooltipNoVotes: String { return L10n.tr("Localizable", "Chat.Poll.Tooltip.NoVotes") } + /// %d + internal static func chatPollTooltipVotesCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Poll.Tooltip.Votes_countable", p1) + } + /// %d votes + internal static func chatPollTooltipVotesFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Poll.Tooltip.Votes_few", p1) + } + /// %d votes + internal static func chatPollTooltipVotesMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Poll.Tooltip.Votes_many", p1) + } + /// %d vote + internal static func chatPollTooltipVotesOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Poll.Tooltip.Votes_one", p1) + } + /// %d votes + internal static func chatPollTooltipVotesOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Poll.Tooltip.Votes_other", p1) + } + /// %d votes + internal static func chatPollTooltipVotesTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Poll.Tooltip.Votes_two", p1) + } + /// %d votes + internal static func chatPollTooltipVotesZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Poll.Tooltip.Votes_zero", p1) + } + /// Anonymous Poll + internal static var chatPollTypeAnonymous: String { return L10n.tr("Localizable", "Chat.Poll.Type.Anonymous") } + /// Anonymous Quiz + internal static var chatPollTypeAnonymousQuiz: String { return L10n.tr("Localizable", "Chat.Poll.Type.AnonymousQuiz") } + /// Final Results + internal static var chatPollTypeClosed: String { return L10n.tr("Localizable", "Chat.Poll.Type.Closed") } + /// Poll + internal static var chatPollTypePublic: String { return L10n.tr("Localizable", "Chat.Poll.Type.Public") } + /// Quiz + internal static var chatPollTypeQuiz: String { return L10n.tr("Localizable", "Chat.Poll.Type.Quiz") } + /// Proxy Sponsor + internal static var chatProxySponsoredAlertHeader: String { return L10n.tr("Localizable", "Chat.ProxySponsored.AlertHeader") } + /// Settings + internal static var chatProxySponsoredAlertSettings: String { return L10n.tr("Localizable", "Chat.ProxySponsored.AlertSettings") } + /// This channel is shown by your proxy server. To remove this channel from your chats list, disable the proxy in Telegram Settings. + internal static var chatProxySponsoredAlertText: String { return L10n.tr("Localizable", "Chat.ProxySponsored.AlertText") } + /// This channel is shown by your proxy server + internal static var chatProxySponsoredCapDesc: String { return L10n.tr("Localizable", "Chat.ProxySponsored.CapDesc") } + /// Proxy Sponsor + internal static var chatProxySponsoredCapTitle: String { return L10n.tr("Localizable", "Chat.ProxySponsored.CapTitle") } + /// Stop Quiz + internal static var chatQuizStop: String { return L10n.tr("Localizable", "Chat.Quiz.Stop") } + /// Quiz + internal static var chatQuizTextType: String { return L10n.tr("Localizable", "Chat.Quiz.TextType") } + /// %d + internal static func chatQuizTotalVotesCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Quiz.TotalVotes_countable", p1) + } + /// %d answers + internal static func chatQuizTotalVotesFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Quiz.TotalVotes_few", p1) + } + /// %d answers + internal static func chatQuizTotalVotesMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Quiz.TotalVotes_many", p1) + } + /// %d answer + internal static func chatQuizTotalVotesOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Quiz.TotalVotes_one", p1) + } + /// %d answers + internal static func chatQuizTotalVotesOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Quiz.TotalVotes_other", p1) + } + /// %d answers + internal static func chatQuizTotalVotesTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Quiz.TotalVotes_two", p1) + } + /// %d answer + internal static func chatQuizTotalVotesZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Quiz.TotalVotes_zero", p1) + } + /// No answers yet + internal static var chatQuizTotalVotesEmpty: String { return L10n.tr("Localizable", "Chat.Quiz.TotalVotesEmpty") } + /// No answers + internal static var chatQuizTotalVotesResultEmpty: String { return L10n.tr("Localizable", "Chat.Quiz.TotalVotesResultEmpty") } + /// Stop Quiz? + internal static var chatQuizStopConfirmHeader: String { return L10n.tr("Localizable", "Chat.Quiz.Stop.Confirm.Header") } + /// If you stop this quiz now, nobody will be able to answer in it anymore. This action cannot be undone. + internal static var chatQuizStopConfirmText: String { return L10n.tr("Localizable", "Chat.Quiz.Stop.Confirm.Text") } + /// no answers + internal static var chatQuizTooltipNoVotes: String { return L10n.tr("Localizable", "Chat.Quiz.Tooltip.NoVotes") } + /// %d + internal static func chatQuizTooltipVotesCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Quiz.Tooltip.Votes_countable", p1) + } + /// %d answers + internal static func chatQuizTooltipVotesFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Quiz.Tooltip.Votes_few", p1) + } + /// %d answers + internal static func chatQuizTooltipVotesMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Quiz.Tooltip.Votes_many", p1) + } + /// %d answer + internal static func chatQuizTooltipVotesOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Quiz.Tooltip.Votes_one", p1) + } + /// %d answers + internal static func chatQuizTooltipVotesOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Quiz.Tooltip.Votes_other", p1) + } + /// %d answers + internal static func chatQuizTooltipVotesTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Quiz.Tooltip.Votes_two", p1) + } + /// %d answers + internal static func chatQuizTooltipVotesZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Quiz.Tooltip.Votes_zero", p1) + } + /// Are you sure you want to cancel recording? + internal static var chatRecordingCancel: String { return L10n.tr("Localizable", "Chat.Recording.Cancel") } + /// This chat helps you keep track of replies to your comments in Channels. + internal static var chatRepliesDesc: String { return L10n.tr("Localizable", "Chat.Replies.Desc") } + /// Reminder + internal static var chatRightContextReminder: String { return L10n.tr("Localizable", "Chat.Right.Context.Reminder") } + /// Scheduled Messages + internal static var chatRightContextScheduledMessages: String { return L10n.tr("Localizable", "Chat.Right.Context.ScheduledMessages") } + /// The buttons will become active as soon as the message is sent. + internal static var chatScheduledInlineButtonError: String { return L10n.tr("Localizable", "Chat.Scheduled.InlineButton.Error") } + /// • Use end-to-end encryption + internal static var chatSecretChat1Feature: String { return L10n.tr("Localizable", "Chat.SecretChat.1Feature") } + /// • Leave no trace on our servers + internal static var chatSecretChat2Feature: String { return L10n.tr("Localizable", "Chat.SecretChat.2Feature") } + /// • Have a self-destruct timer + internal static var chatSecretChat3Feature: String { return L10n.tr("Localizable", "Chat.SecretChat.3Feature") } + /// • Do not allow forwarding + internal static var chatSecretChat4Feature: String { return L10n.tr("Localizable", "Chat.SecretChat.4Feature") } /// Secret chats: - case chatSecretChatEmptyHeader - /// You have just successfully transferred **%@** to **%@** for **%@** - case chatServicePaymentSent(String, String, String) + internal static var chatSecretChatEmptyHeader: String { return L10n.tr("Localizable", "Chat.SecretChat.EmptyHeader") } + /// Secret Chat + internal static var chatSecretChatPreviewHeader: String { return L10n.tr("Localizable", "Chat.SecretChat.Preview.Header") } + /// NO + internal static var chatSecretChatPreviewNO: String { return L10n.tr("Localizable", "Chat.SecretChat.Preview.NO") } + /// YES + internal static var chatSecretChatPreviewOK: String { return L10n.tr("Localizable", "Chat.SecretChat.Preview.OK") } + /// Would you like to enable extended link previews in Secret Chat? Note that link previews are generated on Telegram Servers. + internal static var chatSecretChatPreviewText: String { return L10n.tr("Localizable", "Chat.SecretChat.Preview.Text") } + /// Schedule a Message + internal static var chatSendScheduledMessage: String { return L10n.tr("Localizable", "Chat.Send.ScheduledMessage") } + /// Set a Reminder + internal static var chatSendSetReminder: String { return L10n.tr("Localizable", "Chat.Send.SetReminder") } + /// Send Without Sound + internal static var chatSendWithoutSound: String { return L10n.tr("Localizable", "Chat.Send.WithoutSound") } + /// Sorry, you can only send only 100 scheduled messages. + internal static var chatSendMessageErrorTooMuchScheduled: String { return L10n.tr("Localizable", "Chat.SendMessageError.TooMuchScheduled") } + /// You allowed this bot to message you when you logged in on %@ + internal static func chatServiceBotPermissionAllowed(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Service.BotPermissionAllowed", p1) + } + /// %1$@ disabled the chat theme + internal static func chatServiceDisabledTheme(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Service.DisabledTheme", p1) + } + /// You have successfully transferred **%1$@** to **%2$@** for **%3$@** + internal static func chatServicePaymentSent1(_ p1: String, _ p2: String, _ p3: String) -> String { + return L10n.tr("Localizable", "Chat.Service.PaymentSent1", p1, p2, p3) + } + /// %@ joined Telegram + internal static func chatServicePeerJoinedTelegram(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Service.PeerJoinedTelegram", p1) + } /// pinned message - case chatServicePinnedMessage + internal static var chatServicePinnedMessage: String { return L10n.tr("Localizable", "Chat.Service.PinnedMessage") } + /// Search messages by %@ + internal static func chatServiceSearchAllMessages(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Service.SearchAllMessages", p1) + } + /// %1$@ changed chat theme to %2$@ + internal static func chatServiceUpdateTheme(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Chat.Service.UpdateTheme", p1, p2) + } + /// %1$@ finished voice chat (%2$@) + internal static func chatServiceVoiceChatFinished(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Chat.Service.VoiceChatFinished", p1, p2) + } + /// You finished voice chat (%@) + internal static func chatServiceVoiceChatFinishedYou(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Service.VoiceChatFinishedYou", p1) + } + /// %1$@ invited %2$@ to the [voice chat](open) + internal static func chatServiceVoiceChatInvitation(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Chat.Service.VoiceChatInvitation", p1, p2) + } + /// You invited %1$@ to the [voice chat](open) + internal static func chatServiceVoiceChatInvitationByYou(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Service.VoiceChatInvitationByYou", p1) + } + /// %1$@ invited you to the [voice chat](open) + internal static func chatServiceVoiceChatInvitationForYou(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Service.VoiceChatInvitationForYou", p1) + } + /// %1$@ scheduled a [voice chat](open) for %2$@ + internal static func chatServiceVoiceChatScheduled(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Chat.Service.VoiceChatScheduled", p1, p2) + } + /// You scheduled a [voice chat](open) for %1$@ + internal static func chatServiceVoiceChatScheduledYou(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Service.VoiceChatScheduledYou", p1) + } + /// %1$@ started a [voice chat](open) + internal static func chatServiceVoiceChatStarted(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Service.VoiceChatStarted", p1) + } + /// You started a [voice chat](open) + internal static var chatServiceVoiceChatStartedYou: String { return L10n.tr("Localizable", "Chat.Service.VoiceChatStartedYou") } /// You - case chatServiceYou + internal static var chatServiceYou: String { return L10n.tr("Localizable", "Chat.Service.You") } + /// Cancelled + internal static var chatServiceCallCancelled: String { return L10n.tr("Localizable", "Chat.Service.Call.Cancelled") } + /// Missed + internal static var chatServiceCallMissed: String { return L10n.tr("Localizable", "Chat.Service.Call.Missed") } + /// a group admin disabled the auto-delete timer + internal static var chatServiceChannelDisabledTimer: String { return L10n.tr("Localizable", "Chat.Service.Channel.DisabledTimer") } /// channel photo removed - case chatServiceChannelRemovedPhoto + internal static var chatServiceChannelRemovedPhoto: String { return L10n.tr("Localizable", "Chat.Service.Channel.RemovedPhoto") } + /// a group admin set the messages to automatically delete after %@ + internal static func chatServiceChannelSetTimer(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Service.Channel.SetTimer", p1) + } /// channel photo updated - case chatServiceChannelUpdatedPhoto + internal static var chatServiceChannelUpdatedPhoto: String { return L10n.tr("Localizable", "Chat.Service.Channel.UpdatedPhoto") } /// channel renamed to "%@" - case chatServiceChannelUpdatedTitle(String) - /// %@ invited %@ - case chatServiceGroupAddedMembers(String, String) - /// %@ joined group - case chatServiceGroupAddedSelf(String) - /// %@ created the group "%@" - case chatServiceGroupCreated(String, String) + internal static func chatServiceChannelUpdatedTitle(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Service.Channel.UpdatedTitle", p1) + } + /// channel video updated + internal static var chatServiceChannelUpdatedVideo: String { return L10n.tr("Localizable", "Chat.Service.Channel.UpdatedVideo") } + /// You disabled the chat theme + internal static var chatServiceDisabledThemeYou: String { return L10n.tr("Localizable", "Chat.Service.DisabledTheme.You") } + /// %1$@ invited %2$@ + internal static func chatServiceGroupAddedMembers1(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Chat.Service.Group.AddedMembers1", p1, p2) + } + /// %@ joined the group + internal static func chatServiceGroupAddedSelf(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Service.Group.AddedSelf", p1) + } + /// %1$@ created the group "%2$@" + internal static func chatServiceGroupCreated1(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Chat.Service.Group.Created1", p1, p2) + } + /// a group admin disabled the auto-delete timer + internal static var chatServiceGroupDisabledTimer: String { return L10n.tr("Localizable", "Chat.Service.Group.DisabledTimer") } /// %@ joined group via invite link - case chatServiceGroupJoinedByLink(String) + internal static func chatServiceGroupJoinedByLink(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Service.Group.JoinedByLink", p1) + } /// This group was upgraded to a supergroup - case chatServiceGroupMigratedToSupergroup - /// %@ kicked %@ - case chatServiceGroupRemovedMembers(String, String) + internal static var chatServiceGroupMigratedToSupergroup: String { return L10n.tr("Localizable", "Chat.Service.Group.MigratedToSupergroup") } + /// %1$@ removed %2$@ + internal static func chatServiceGroupRemovedMembers1(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Chat.Service.Group.RemovedMembers1", p1, p2) + } /// %@ removed group photo - case chatServiceGroupRemovedPhoto(String) - /// %@ left group - case chatServiceGroupRemovedSelf(String) + internal static func chatServiceGroupRemovedPhoto(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Service.Group.RemovedPhoto", p1) + } + /// %@ left the group + internal static func chatServiceGroupRemovedSelf(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Service.Group.RemovedSelf", p1) + } + /// a group admin set the messages to automatically delete after %@ + internal static func chatServiceGroupSetTimer(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Service.Group.SetTimer", p1) + } /// %@ took a screenshot - case chatServiceGroupTookScreenshot(String) + internal static func chatServiceGroupTookScreenshot(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Service.Group.TookScreenshot", p1) + } /// %@ updated group photo - case chatServiceGroupUpdatedPhoto(String) - /// %@ pinned "%@" - case chatServiceGroupUpdatedPinnedMessage(String, String) - /// %@ changed group name to "%@" - case chatServiceGroupUpdatedTitle(String, String) - /// %@ disabled the self-destruct timer - case chatServiceSecretChatDisabledTimer(String) - /// %@ set the self-destruct timer to %@ - case chatServiceSecretChatSetTimer(String, String) - /// You disabled the self-destruct timer - case chatServiceSecretChatDisabledTimerSelf - /// You set the self-destruct timer to %@ - case chatServiceSecretChatSetTimerSelf(String) - /// Your Cloud storage - case chatTitleSelf + internal static func chatServiceGroupUpdatedPhoto(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Service.Group.UpdatedPhoto", p1) + } + /// %1$@ pinned "%2$@" + internal static func chatServiceGroupUpdatedPinnedMessage1(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Chat.Service.Group.UpdatedPinnedMessage1", p1, p2) + } + /// %1$@ changed the group name to "%2$@" + internal static func chatServiceGroupUpdatedTitle1(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Chat.Service.Group.UpdatedTitle1", p1, p2) + } + /// %@ updated group video + internal static func chatServiceGroupUpdatedVideo(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Service.Group.UpdatedVideo", p1) + } + /// %@ disabled the auto-delete timer + internal static func chatServiceSecretChatDisabledTimer1(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Service.SecretChat.DisabledTimer1", p1) + } + /// %@ set the messages to automatically delete after %@ + internal static func chatServiceSecretChatSetTimer1(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Chat.Service.SecretChat.SetTimer1", p1, p2) + } + /// You disabled the auto-delete timer + internal static var chatServiceSecretChatDisabledTimerSelf1: String { return L10n.tr("Localizable", "Chat.Service.SecretChat.DisabledTimer.Self1") } + /// You set messages to automatically delete after %@ + internal static func chatServiceSecretChatSetTimerSelf1(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Service.SecretChat.SetTimer.Self1", p1) + } + /// %@ received the following documents: %@ + internal static func chatServiceSecureIdAccessGranted(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Chat.Service.SecureId.AccessGranted", p1, p2) + } + /// You changed chat theme to %@ + internal static func chatServiceUpdateThemeYou(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Service.UpdateTheme.You", p1) + } + /// Voice chat ended (%1$@) + internal static func chatServiceVoiceChatFinishedChannel(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Service.VoiceChatFinished.Channel", p1) + } + /// [Voice Chat](open) scheduled for %@ + internal static func chatServiceVoiceChatScheduledChannel(_ p1: String) -> String { + return L10n.tr("Localizable", "Chat.Service.VoiceChatScheduled.Channel", p1) + } + /// [Voice Chat](open) started + internal static var chatServiceVoiceChatStartedChannel: String { return L10n.tr("Localizable", "Chat.Service.VoiceChatStarted.Channel") } + /// %d + internal static func chatTitleCommentsCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Title.Comments_countable", p1) + } + /// %d Comments + internal static func chatTitleCommentsFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Title.Comments_few", p1) + } + /// %d Comments + internal static func chatTitleCommentsMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Title.Comments_many", p1) + } + /// %d Comment + internal static func chatTitleCommentsOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Title.Comments_one", p1) + } + /// %d Comments + internal static func chatTitleCommentsOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Title.Comments_other", p1) + } + /// %d Comments + internal static func chatTitleCommentsTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Title.Comments_two", p1) + } + /// Comments + internal static var chatTitleCommentsZero: String { return L10n.tr("Localizable", "Chat.Title.Comments_zero") } + /// Discussion + internal static var chatTitleDiscussion: String { return L10n.tr("Localizable", "Chat.Title.Discussion") } + /// %d + internal static func chatTitlePinnedMessagesCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Title.PinnedMessages_countable", p1) + } + /// %d Pinned Messages + internal static func chatTitlePinnedMessagesFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Title.PinnedMessages_few", p1) + } + /// %d Pinned Messages + internal static func chatTitlePinnedMessagesMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Title.PinnedMessages_many", p1) + } + /// %d Pinned Message + internal static func chatTitlePinnedMessagesOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Title.PinnedMessages_one", p1) + } + /// %d Pinned Messages + internal static func chatTitlePinnedMessagesOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Title.PinnedMessages_other", p1) + } + /// %d Pinned Messages + internal static func chatTitlePinnedMessagesTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Title.PinnedMessages_two", p1) + } + /// %d Pinned Messages + internal static func chatTitlePinnedMessagesZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Title.PinnedMessages_zero", p1) + } + /// Reminder + internal static var chatTitleReminder: String { return L10n.tr("Localizable", "Chat.Title.Reminder") } + /// %d + internal static func chatTitleRepliesCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Title.Replies_countable", p1) + } + /// %d Replies + internal static func chatTitleRepliesFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Title.Replies_few", p1) + } + /// %d Replies + internal static func chatTitleRepliesMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Title.Replies_many", p1) + } + /// %d Reply + internal static func chatTitleRepliesOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Title.Replies_one", p1) + } + /// %d Replies + internal static func chatTitleRepliesOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Title.Replies_other", p1) + } + /// %d Replies + internal static func chatTitleRepliesTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.Title.Replies_two", p1) + } + /// Replies + internal static var chatTitleRepliesZero: String { return L10n.tr("Localizable", "Chat.Title.Replies_zero") } + /// Scheduled Messages + internal static var chatTitleScheduledMessages: String { return L10n.tr("Localizable", "Chat.Title.ScheduledMessages") } + /// Your cloud storage + internal static var chatTitleSelf: String { return L10n.tr("Localizable", "Chat.Title.self") } + /// Telegram moderators will study your report. Thank You. + internal static var chatToastReportSuccess: String { return L10n.tr("Localizable", "Chat.Toast.ReportSuccess") } + /// The account was hidden by the user + internal static var chatTooltipHiddenForwardName: String { return L10n.tr("Localizable", "Chat.Tooltip.HiddenForwardName") } + /// %d + internal static func chatUndoManagerChannelDeletedCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChannelDeleted_countable", p1) + } + /// %d Channels Deleted + internal static func chatUndoManagerChannelDeletedFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChannelDeleted_few", p1) + } + /// %d Channels Deleted + internal static func chatUndoManagerChannelDeletedMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChannelDeleted_many", p1) + } + /// Channel Deleted + internal static var chatUndoManagerChannelDeletedOne: String { return L10n.tr("Localizable", "Chat.UndoManager.ChannelDeleted_one") } + /// %d Channels Deleted + internal static func chatUndoManagerChannelDeletedOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChannelDeleted_other", p1) + } + /// %d Channels Deleted + internal static func chatUndoManagerChannelDeletedTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChannelDeleted_two", p1) + } + /// %d Channels Deleted + internal static func chatUndoManagerChannelDeletedZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChannelDeleted_zero", p1) + } + /// %d + internal static func chatUndoManagerChannelLeftCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChannelLeft_countable", p1) + } + /// %d Channels Left + internal static func chatUndoManagerChannelLeftFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChannelLeft_few", p1) + } + /// %d Channels Left + internal static func chatUndoManagerChannelLeftMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChannelLeft_many", p1) + } + /// Channel Left + internal static var chatUndoManagerChannelLeftOne: String { return L10n.tr("Localizable", "Chat.UndoManager.ChannelLeft_one") } + /// %d Channels Left + internal static func chatUndoManagerChannelLeftOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChannelLeft_other", p1) + } + /// %d Channels Left + internal static func chatUndoManagerChannelLeftTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChannelLeft_two", p1) + } + /// %d Channels Left + internal static func chatUndoManagerChannelLeftZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChannelLeft_zero", p1) + } + /// %d + internal static func chatUndoManagerChatLeftCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChatLeft_countable", p1) + } + /// %d Chats Left + internal static func chatUndoManagerChatLeftFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChatLeft_few", p1) + } + /// %d Chats Left + internal static func chatUndoManagerChatLeftMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChatLeft_many", p1) + } + /// Chat Left + internal static var chatUndoManagerChatLeftOne: String { return L10n.tr("Localizable", "Chat.UndoManager.ChatLeft_one") } + /// %d Chats Left + internal static func chatUndoManagerChatLeftOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChatLeft_other", p1) + } + /// %d Chats Left + internal static func chatUndoManagerChatLeftTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChatLeft_two", p1) + } + /// %d Chats Left + internal static func chatUndoManagerChatLeftZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChatLeft_zero", p1) + } + /// %d + internal static func chatUndoManagerChatsArchivedCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChatsArchived_countable", p1) + } + /// %d Chats Archived + internal static func chatUndoManagerChatsArchivedFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChatsArchived_few", p1) + } + /// %d Chats Archived + internal static func chatUndoManagerChatsArchivedMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChatsArchived_many", p1) + } + /// Chat Archived + internal static var chatUndoManagerChatsArchivedOne: String { return L10n.tr("Localizable", "Chat.UndoManager.ChatsArchived_one") } + /// %d Chats Archived + internal static func chatUndoManagerChatsArchivedOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChatsArchived_other", p1) + } + /// %d Chats Archived + internal static func chatUndoManagerChatsArchivedTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChatsArchived_two", p1) + } + /// %d Chat Archived + internal static func chatUndoManagerChatsArchivedZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChatsArchived_zero", p1) + } + /// %d + internal static func chatUndoManagerChatsDeletedCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChatsDeleted_countable", p1) + } + /// %d Chats Deleted + internal static func chatUndoManagerChatsDeletedFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChatsDeleted_few", p1) + } + /// %d Chats Deleted + internal static func chatUndoManagerChatsDeletedMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChatsDeleted_many", p1) + } + /// Chat Deleted + internal static var chatUndoManagerChatsDeletedOne: String { return L10n.tr("Localizable", "Chat.UndoManager.ChatsDeleted_one") } + /// %d Chats Deleted + internal static func chatUndoManagerChatsDeletedOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChatsDeleted_other", p1) + } + /// %d Chats Deleted + internal static func chatUndoManagerChatsDeletedTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChatsDeleted_two", p1) + } + /// %d Chat Deleted + internal static func chatUndoManagerChatsDeletedZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChatsDeleted_zero", p1) + } + /// %d + internal static func chatUndoManagerChatsHistoryClearedCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChatsHistoryCleared_countable", p1) + } + /// %d Chat History Cleared + internal static func chatUndoManagerChatsHistoryClearedFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChatsHistoryCleared_few", p1) + } + /// %d Chat History Cleared + internal static func chatUndoManagerChatsHistoryClearedMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChatsHistoryCleared_many", p1) + } + /// Chat History Cleared + internal static var chatUndoManagerChatsHistoryClearedOne: String { return L10n.tr("Localizable", "Chat.UndoManager.ChatsHistoryCleared_one") } + /// %d Chat History Cleared + internal static func chatUndoManagerChatsHistoryClearedOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChatsHistoryCleared_other", p1) + } + /// %d Chat History Cleared + internal static func chatUndoManagerChatsHistoryClearedTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChatsHistoryCleared_two", p1) + } + /// %d Chat History Cleared + internal static func chatUndoManagerChatsHistoryClearedZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.UndoManager.ChatsHistoryCleared_zero", p1) + } + /// Undo + internal static var chatUndoManagerUndo: String { return L10n.tr("Localizable", "Chat.UndoManager.Undo") } + /// UPDATE + internal static var chatUnsupportedUpdatedApp: String { return L10n.tr("Localizable", "Chat.Unsupported.UpdatedApp") } + /// processing... + internal static var chatVideoProcessing: String { return L10n.tr("Localizable", "Chat.Video.Processing") } + /// Incoming Video Call + internal static var chatVideoCallIncoming: String { return L10n.tr("Localizable", "Chat.VideoCall.Incoming") } + /// Outgoing Video Call + internal static var chatVideoCallOutgoing: String { return L10n.tr("Localizable", "Chat.VideoCall.Outgoing") } + /// Join + internal static var chatVoiceChatJoinLinkOK: String { return L10n.tr("Localizable", "Chat.VoiceChat.JoinLink.OK") } + /// %d + internal static func chatVoiceChatJoinLinkParticipantsCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.VoiceChat.JoinLink.Participants_countable", p1) + } + /// %d participants + internal static func chatVoiceChatJoinLinkParticipantsFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.VoiceChat.JoinLink.Participants_few", p1) + } + /// %d participants + internal static func chatVoiceChatJoinLinkParticipantsMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.VoiceChat.JoinLink.Participants_many", p1) + } + /// %d participant + internal static func chatVoiceChatJoinLinkParticipantsOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.VoiceChat.JoinLink.Participants_one", p1) + } + /// %d participants + internal static func chatVoiceChatJoinLinkParticipantsOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.VoiceChat.JoinLink.Participants_other", p1) + } + /// %d participants + internal static func chatVoiceChatJoinLinkParticipantsTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Chat.VoiceChat.JoinLink.Participants_two", p1) + } + /// no one joined yet + internal static var chatVoiceChatJoinLinkParticipantsZero: String { return L10n.tr("Localizable", "Chat.VoiceChat.JoinLink.Participants_zero") } + /// Are you sure you want to join voice chat? + internal static var chatVoiceChatJoinLinkText: String { return L10n.tr("Localizable", "Chat.VoiceChat.JoinLink.Text") } + /// Voice Chat + internal static var chatVoiceChatJoinLinkTitle: String { return L10n.tr("Localizable", "Chat.VoiceChat.JoinLink.Title") } + /// Voice Chat no longer available + internal static var chatVoiceChatJoinLinkUnavailable: String { return L10n.tr("Localizable", "Chat.VoiceChat.JoinLink.Unavailable") } + /// Chat Background + internal static var chatWPBackgroundTitle: String { return L10n.tr("Localizable", "Chat.WP.BackgroundTitle") } + /// Color + internal static var chatWPColor: String { return L10n.tr("Localizable", "Chat.WP.Color") } + /// Pinch, swipe or double tap to select a custom area for the background. + internal static var chatWPFirstMessage: String { return L10n.tr("Localizable", "Chat.WP.FirstMessage") } + /// Pattern Intensity + internal static var chatWPIntensity: String { return L10n.tr("Localizable", "Chat.WP.Intensity") } + /// Pattern + internal static var chatWPPattern: String { return L10n.tr("Localizable", "Chat.WP.Pattern") } + /// Pinch me, I'm dreaming! + internal static var chatWPSecondMessage: String { return L10n.tr("Localizable", "Chat.WP.SecondMessage") } + /// Select From File + internal static var chatWPSelectFromFile: String { return L10n.tr("Localizable", "Chat.WP.SelectFromFile") } + /// Voice Chat + internal static var chatWPVoiceChatTitle: String { return L10n.tr("Localizable", "Chat.WP.VoiceChatTitle") } + /// Press Apply to set the background + internal static var chatWPColorFirstMessage: String { return L10n.tr("Localizable", "Chat.WP.Color.FirstMessage") } + /// Enjoy the view + internal static var chatWPColorSecondMessage: String { return L10n.tr("Localizable", "Chat.WP.Color.SecondMessage") } + /// None + internal static var chatWPPatternNone: String { return L10n.tr("Localizable", "Chat.WP.Pattern.None") } + /// %1$d of %2$d + internal static func chatWebpageMediaCount1(_ p1: Int, _ p2: Int) -> String { + return L10n.tr("Localizable", "Chat.Webpage.MediaCount1", p1, p2) + } + /// Menu + internal static var chatInputBotMenu: String { return L10n.tr("Localizable", "ChatInput.BotMenu") } + /// Show Next + internal static var chatInputShowNext: String { return L10n.tr("Localizable", "ChatInput.ShowNext") } + /// Archived Chats + internal static var chatListArchivedChats: String { return L10n.tr("Localizable", "ChatList.ArchivedChats") } + /// Show All + internal static var chatListCloseFilter: String { return L10n.tr("Localizable", "ChatList.CloseFilter") } + /// All + internal static var chatListCloseFilterShort: String { return L10n.tr("Localizable", "ChatList.CloseFilterShort") } /// Draft: - case chatListDraft + internal static var chatListDraft: String { return L10n.tr("Localizable", "ChatList.Draft") } + /// **You have no conversations yet**\nStart messaging by tapping the pencil button in the top right corner or got to the Contacts section. + internal static var chatListEmptyText: String { return L10n.tr("Localizable", "ChatList.EmptyText") } + /// Channels + internal static var chatListFeeds: String { return L10n.tr("Localizable", "ChatList.Feeds") } + /// Group Channel + internal static var chatListGroupChannel: String { return L10n.tr("Localizable", "ChatList.GroupChannel") } + /// Hide Muted + internal static var chatListHideMuted: String { return L10n.tr("Localizable", "ChatList.HideMuted") } + /// Proxy Sponsor + internal static var chatListSponsoredChannel: String { return L10n.tr("Localizable", "ChatList.SponsoredChannel") } + /// Feed + internal static var chatListTitleFeed: String { return L10n.tr("Localizable", "ChatList.TitleFeed") } + /// Unhide Muted + internal static var chatListUnhideMuted: String { return L10n.tr("Localizable", "ChatList.UnhideMuted") } /// Message is not supported - case chatListUnsupportedMessage + internal static var chatListUnsupportedMessage: String { return L10n.tr("Localizable", "ChatList.UnsupportedMessage") } /// You - case chatListYou + internal static var chatListYou: String { return L10n.tr("Localizable", "ChatList.You") } + /// CHATS + internal static var chatListAddBottomSeparator: String { return L10n.tr("Localizable", "ChatList.Add.BottomSeparator") } + /// Select chats... + internal static var chatListAddPlaceholder: String { return L10n.tr("Localizable", "ChatList.Add.Placeholder") } + /// Add + internal static var chatListAddSave: String { return L10n.tr("Localizable", "ChatList.Add.Save") } + /// CHAT TYPES + internal static var chatListAddTopSeparator: String { return L10n.tr("Localizable", "ChatList.Add.TopSeparator") } + /// Chats + internal static var chatListArchiveBack: String { return L10n.tr("Localizable", "ChatList.Archive.Back") } /// Call - case chatListContextCall + internal static var chatListContextCall: String { return L10n.tr("Localizable", "ChatList.Context.Call") } /// Clear History - case chatListContextClearHistory + internal static var chatListContextClearHistory: String { return L10n.tr("Localizable", "ChatList.Context.ClearHistory") } /// Delete And Exit - case chatListContextDeleteAndExit + internal static var chatListContextDeleteAndExit: String { return L10n.tr("Localizable", "ChatList.Context.DeleteAndExit") } /// Delete Chat - case chatListContextDeleteChat + internal static var chatListContextDeleteChat: String { return L10n.tr("Localizable", "ChatList.Context.DeleteChat") } + /// Hide + internal static var chatListContextHidePromo: String { return L10n.tr("Localizable", "ChatList.Context.HidePromo") } /// Leave Channel - case chatListContextLeaveChannel + internal static var chatListContextLeaveChannel: String { return L10n.tr("Localizable", "ChatList.Context.LeaveChannel") } /// Leave Group - case chatListContextLeaveGroup + internal static var chatListContextLeaveGroup: String { return L10n.tr("Localizable", "ChatList.Context.LeaveGroup") } + /// Mark As Read + internal static var chatListContextMaskAsRead: String { return L10n.tr("Localizable", "ChatList.Context.MaskAsRead") } + /// Mark As Unread + internal static var chatListContextMaskAsUnread: String { return L10n.tr("Localizable", "ChatList.Context.MaskAsUnread") } /// Mute - case chatListContextMute + internal static var chatListContextMute: String { return L10n.tr("Localizable", "ChatList.Context.Mute") } + /// Pin + internal static var chatListContextPin: String { return L10n.tr("Localizable", "ChatList.Context.Pin") } + /// Sorry, you can pin no more than 5 chats to the top. + internal static var chatListContextPinError: String { return L10n.tr("Localizable", "ChatList.Context.PinError") } + /// Sorry, you can only pin 5 chats to the top in the main list. More chats can be pinned in Chat Folders and your Archive. + internal static var chatListContextPinErrorNew2: String { return L10n.tr("Localizable", "ChatList.Context.PinErrorNew2") } + /// Preview + internal static var chatListContextPreview: String { return L10n.tr("Localizable", "ChatList.Context.Preview") } + /// Return to Group + internal static var chatListContextReturnGroup: String { return L10n.tr("Localizable", "ChatList.Context.ReturnGroup") } + /// Unmute + internal static var chatListContextUnmute: String { return L10n.tr("Localizable", "ChatList.Context.Unmute") } + /// Unpin + internal static var chatListContextUnpin: String { return L10n.tr("Localizable", "ChatList.Context.Unpin") } + /// Set Up Folders + internal static var chatListContextPinErrorNewSetupFolders: String { return L10n.tr("Localizable", "ChatList.Context.PinErrorNew.SetupFolders") } + /// Add Chats + internal static var chatListFilterAddChats: String { return L10n.tr("Localizable", "ChatList.Filter.AddChats") } + /// Add to folder... + internal static var chatListFilterAddToFolder: String { return L10n.tr("Localizable", "ChatList.Filter.AddToFolder") } + /// All + internal static var chatListFilterAll: String { return L10n.tr("Localizable", "ChatList.Filter.All") } + /// All Chats + internal static var chatListFilterAllChats: String { return L10n.tr("Localizable", "ChatList.Filter.AllChats") } + /// Archive + internal static var chatListFilterArchive: String { return L10n.tr("Localizable", "ChatList.Filter.Archive") } + /// Chats + internal static var chatListFilterBack: String { return L10n.tr("Localizable", "ChatList.Filter.Back") } + /// Bots + internal static var chatListFilterBots: String { return L10n.tr("Localizable", "ChatList.Filter.Bots") } + /// Channels + internal static var chatListFilterChannels: String { return L10n.tr("Localizable", "ChatList.Filter.Channels") } + /// Contacts + internal static var chatListFilterContacts: String { return L10n.tr("Localizable", "ChatList.Filter.Contacts") } + /// Delete + internal static var chatListFilterDelete: String { return L10n.tr("Localizable", "ChatList.Filter.Delete") } + /// Create + internal static var chatListFilterDone: String { return L10n.tr("Localizable", "ChatList.Filter.Done") } + /// Edit + internal static var chatListFilterEdit: String { return L10n.tr("Localizable", "ChatList.Filter.Edit") } + /// Edit Folders + internal static var chatListFilterEditFilters: String { return L10n.tr("Localizable", "ChatList.Filter.EditFilters") } + /// **No chats currently match this folder.**\n\n[Edit Folder](filter) + internal static var chatListFilterEmpty: String { return L10n.tr("Localizable", "ChatList.Filter.Empty") } + /// Exclude Muted + internal static var chatListFilterExcludeMuted: String { return L10n.tr("Localizable", "ChatList.Filter.ExcludeMuted") } + /// Exclude Read + internal static var chatListFilterExcludeRead: String { return L10n.tr("Localizable", "ChatList.Filter.ExcludeRead") } + /// Groups + internal static var chatListFilterGroups: String { return L10n.tr("Localizable", "ChatList.Filter.Groups") } + /// Create folders for different groups of chats and quickly switch between them. + internal static var chatListFilterHeader: String { return L10n.tr("Localizable", "ChatList.Filter.Header") } + /// %d + internal static func chatListFilterHideCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChatList.Filter.Hide_countable", p1) + } + /// Hide %d Chats + internal static func chatListFilterHideFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChatList.Filter.Hide_few", p1) + } + /// Hide %d Chats + internal static func chatListFilterHideMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChatList.Filter.Hide_many", p1) + } + /// Hide %d Chat + internal static func chatListFilterHideOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChatList.Filter.Hide_one", p1) + } + /// Hide %d Chats + internal static func chatListFilterHideOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChatList.Filter.Hide_other", p1) + } + /// Hide %d Chats + internal static func chatListFilterHideTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChatList.Filter.Hide_two", p1) + } + /// Hide %d Chats + internal static func chatListFilterHideZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChatList.Filter.Hide_zero", p1) + } + /// **Adding Chats**\nPlease wait a few moments while we fill this folder for you... + internal static var chatListFilterLoading: String { return L10n.tr("Localizable", "ChatList.Filter.Loading") } + /// Muted + internal static var chatListFilterMutedChats: String { return L10n.tr("Localizable", "ChatList.Filter.MutedChats") } + /// Create Folder + internal static var chatListFilterNewTitle: String { return L10n.tr("Localizable", "ChatList.Filter.NewTitle") } + /// Non-Contacts + internal static var chatListFilterNonContacts: String { return L10n.tr("Localizable", "ChatList.Filter.NonContacts") } + /// Read + internal static var chatListFilterReadChats: String { return L10n.tr("Localizable", "ChatList.Filter.ReadChats") } + /// Remove From Folder + internal static var chatListFilterRemoveFromFolder: String { return L10n.tr("Localizable", "ChatList.Filter.RemoveFromFolder") } + /// Secret Chats + internal static var chatListFilterSecretChat: String { return L10n.tr("Localizable", "ChatList.Filter.SecretChat") } + /// Edit Folders + internal static var chatListFilterSetup: String { return L10n.tr("Localizable", "ChatList.Filter.Setup") } + /// Add Folder + internal static var chatListFilterSetupEmpty: String { return L10n.tr("Localizable", "ChatList.Filter.SetupEmpty") } + /// %d + internal static func chatListFilterShowMoreCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChatList.Filter.ShowMore_countable", p1) + } + /// Show %d More Chats + internal static func chatListFilterShowMoreFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChatList.Filter.ShowMore_few", p1) + } + /// Show %d More Chats + internal static func chatListFilterShowMoreMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChatList.Filter.ShowMore_many", p1) + } + /// Show %d More Chat + internal static func chatListFilterShowMoreOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChatList.Filter.ShowMore_one", p1) + } + /// Show %d More Chats + internal static func chatListFilterShowMoreOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChatList.Filter.ShowMore_other", p1) + } + /// Show %d More Chats + internal static func chatListFilterShowMoreTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChatList.Filter.ShowMore_two", p1) + } + /// Show %d More Chats + internal static func chatListFilterShowMoreZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "ChatList.Filter.ShowMore_zero", p1) + } + /// Small Groups + internal static var chatListFilterSmallGroups: String { return L10n.tr("Localizable", "ChatList.Filter.SmallGroups") } + /// Folder + internal static var chatListFilterTitle: String { return L10n.tr("Localizable", "ChatList.Filter.Title") } + /// You can organize your chats by right click. + internal static var chatListFilterTooltip: String { return L10n.tr("Localizable", "ChatList.Filter.Tooltip") } + /// Done + internal static var chatListFilterAddDone: String { return L10n.tr("Localizable", "ChatList.Filter.Add.Done") } + /// INCLUDE CHAT TYPES + internal static var chatListFilterCategoriesHeader: String { return L10n.tr("Localizable", "ChatList.Filter.Categories.Header") } + /// Delete Folder + internal static var chatListFilterConfirmRemoveHeader: String { return L10n.tr("Localizable", "ChatList.Filter.Confirm.Remove.Header") } + /// Delete + internal static var chatListFilterConfirmRemoveOK: String { return L10n.tr("Localizable", "ChatList.Filter.Confirm.Remove.OK") } + /// Are you sure you want to delete folder? + internal static var chatListFilterConfirmRemoveText: String { return L10n.tr("Localizable", "ChatList.Filter.Confirm.Remove.Text") } + /// Cancel + internal static var chatListFilterDiscardCancel: String { return L10n.tr("Localizable", "ChatList.Filter.Discard.Cancel") } + /// Discard Changes + internal static var chatListFilterDiscardHeader: String { return L10n.tr("Localizable", "ChatList.Filter.Discard.Header") } + /// Discard + internal static var chatListFilterDiscardOK: String { return L10n.tr("Localizable", "ChatList.Filter.Discard.OK") } + /// Are you sure you want to discard all changes? + internal static var chatListFilterDiscardText: String { return L10n.tr("Localizable", "ChatList.Filter.Discard.Text") } + /// Please add some chats or chat types to the folder. + internal static var chatListFilterErrorEmpty: String { return L10n.tr("Localizable", "ChatList.Filter.Error.Empty") } + /// Can’t create a folder that includes all your chats. + internal static var chatListFilterErrorLikeChats: String { return L10n.tr("Localizable", "ChatList.Filter.Error.LikeChats") } + /// Add Chats + internal static var chatListFilterExcludeAddChat: String { return L10n.tr("Localizable", "ChatList.Filter.Exclude.AddChat") } + /// Choose chats and types of chats that will never appear in this folder + internal static var chatListFilterExcludeDesc: String { return L10n.tr("Localizable", "ChatList.Filter.Exclude.Desc") } + /// EXCLUDED CHATS + internal static var chatListFilterExcludeHeader: String { return L10n.tr("Localizable", "ChatList.Filter.Exclude.Header") } + /// Sorry, you can only add up to 100 chats. + internal static var chatListFilterExcludeLimitReached: String { return L10n.tr("Localizable", "ChatList.Filter.Exclude.LimitReached") } + /// Remove + internal static var chatListFilterExcludeRemoveChat: String { return L10n.tr("Localizable", "ChatList.Filter.Exclude.RemoveChat") } + /// Add Chats + internal static var chatListFilterIncludeAddChat: String { return L10n.tr("Localizable", "ChatList.Filter.Include.AddChat") } + /// Choose chats and types of chats that will appear in this folder + internal static var chatListFilterIncludeDesc: String { return L10n.tr("Localizable", "ChatList.Filter.Include.Desc") } + /// INCLUDED CHATS + internal static var chatListFilterIncludeHeader: String { return L10n.tr("Localizable", "ChatList.Filter.Include.Header") } + /// Sorry, you can only add up to 100 chats. + internal static var chatListFilterIncludeLimitReached: String { return L10n.tr("Localizable", "ChatList.Filter.Include.LimitReached") } + /// Remove + internal static var chatListFilterIncludeRemoveChat: String { return L10n.tr("Localizable", "ChatList.Filter.Include.RemoveChat") } + /// Add a Custom Folder + internal static var chatListFilterListAddNew: String { return L10n.tr("Localizable", "ChatList.Filter.List.AddNew") } + /// Drag and drop folders to change order. Right click to remove. + internal static var chatListFilterListDesc: String { return L10n.tr("Localizable", "ChatList.Filter.List.Desc") } + /// FOLDERS + internal static var chatListFilterListHeader: String { return L10n.tr("Localizable", "ChatList.Filter.List.Header") } + /// Remove + internal static var chatListFilterListRemove: String { return L10n.tr("Localizable", "ChatList.Filter.List.Remove") } + /// Chat Folders + internal static var chatListFilterListTitle: String { return L10n.tr("Localizable", "ChatList.Filter.List.Title") } + /// FOLDER NAME + internal static var chatListFilterNameHeader: String { return L10n.tr("Localizable", "ChatList.Filter.Name.Header") } + /// Folder Name + internal static var chatListFilterNamePlaceholder: String { return L10n.tr("Localizable", "ChatList.Filter.Name.Placeholder") } + /// Add + internal static var chatListFilterRecommendedAdd: String { return L10n.tr("Localizable", "ChatList.Filter.Recommended.Add") } + /// RECOMMENDED + internal static var chatListFilterRecommendedHeader: String { return L10n.tr("Localizable", "ChatList.Filter.Recommended.Header") } + /// If you have many folders, try moving tabs to the left. + internal static var chatListFilterTabBarDesc: String { return L10n.tr("Localizable", "ChatList.Filter.TabBar.Desc") } + /// TABS VIEW + internal static var chatListFilterTabBarHeader: String { return L10n.tr("Localizable", "ChatList.Filter.TabBar.Header") } + /// Tabs on the left + internal static var chatListFilterTabBarOnTheLeft: String { return L10n.tr("Localizable", "ChatList.Filter.TabBar.OnTheLeft") } + /// Tabs at the top + internal static var chatListFilterTabBarOnTheTop: String { return L10n.tr("Localizable", "ChatList.Filter.TabBar.OnTheTop") } + /// Bots + internal static var chatListFilterTilteDefaultBots: String { return L10n.tr("Localizable", "ChatList.Filter.Tilte.Default.Bots") } + /// Channels + internal static var chatListFilterTilteDefaultChannels: String { return L10n.tr("Localizable", "ChatList.Filter.Tilte.Default.Channels") } + /// Contacts + internal static var chatListFilterTilteDefaultContacts: String { return L10n.tr("Localizable", "ChatList.Filter.Tilte.Default.Contacts") } + /// Groups + internal static var chatListFilterTilteDefaultGroups: String { return L10n.tr("Localizable", "ChatList.Filter.Tilte.Default.Groups") } + /// Non-Contacts + internal static var chatListFilterTilteDefaultNonContacts: String { return L10n.tr("Localizable", "ChatList.Filter.Tilte.Default.NonContacts") } + /// Unmuted + internal static var chatListFilterTilteDefaultUnmuted: String { return L10n.tr("Localizable", "ChatList.Filter.Tilte.Default.Unmuted") } + /// Unread + internal static var chatListFilterTilteDefaultUnread: String { return L10n.tr("Localizable", "ChatList.Filter.Tilte.Default.Unread") } + /// For 1 Day + internal static var chatListMute1Day: String { return L10n.tr("Localizable", "ChatList.Mute.1Day") } + /// For 1 Hour + internal static var chatListMute1Hour: String { return L10n.tr("Localizable", "ChatList.Mute.1Hour") } + /// For 3 Days + internal static var chatListMute3Days: String { return L10n.tr("Localizable", "ChatList.Mute.3Days") } + /// For 4 Hours + internal static var chatListMute4Hours: String { return L10n.tr("Localizable", "ChatList.Mute.4Hours") } + /// For 8 Hours + internal static var chatListMute8Hours: String { return L10n.tr("Localizable", "ChatList.Mute.8Hours") } + /// Forever + internal static var chatListMuteForever: String { return L10n.tr("Localizable", "ChatList.Mute.Forever") } + /// Are you sure you want to read all chats? + internal static var chatListPopoverConfirm: String { return L10n.tr("Localizable", "ChatList.Popover.Confirm") } + /// Read All + internal static var chatListPopoverReadAll: String { return L10n.tr("Localizable", "ChatList.Popover.ReadAll") } + /// Collapse + internal static var chatListRevealActionCollapse: String { return L10n.tr("Localizable", "ChatList.RevealAction.Collapse") } + /// Expand + internal static var chatListRevealActionExpand: String { return L10n.tr("Localizable", "ChatList.RevealAction.Expand") } + /// Hide + internal static var chatListRevealActionHide: String { return L10n.tr("Localizable", "ChatList.RevealAction.Hide") } /// Pin - case chatListContextPin - /// Return Group - case chatListContextReturnGroup - /// Unmute - case chatListContextUnmute - /// Unpin - case chatListContextUnpin + internal static var chatListRevealActionPin: String { return L10n.tr("Localizable", "ChatList.RevealAction.Pin") } /// %@ created a secret chat. - case chatListSecretChatCreated(String) + internal static func chatListSecretChatCreated(_ p1: String) -> String { + return L10n.tr("Localizable", "ChatList.SecretChat.Created", p1) + } /// Waiting to come online - case chatListSecretChatExKeys + internal static var chatListSecretChatExKeys: String { return L10n.tr("Localizable", "ChatList.SecretChat.ExKeys") } /// %@ joined your secret chat. - case chatListSecretChatJoined(String) + internal static func chatListSecretChatJoined(_ p1: String) -> String { + return L10n.tr("Localizable", "ChatList.SecretChat.Joined", p1) + } /// Secret chat cancelled - case chatListSecretChatTerminated + internal static var chatListSecretChatTerminated: String { return L10n.tr("Localizable", "ChatList.SecretChat.Terminated") } /// self-destructing photo - case chatListServiceDestructingPhoto + internal static var chatListServiceDestructingPhoto: String { return L10n.tr("Localizable", "ChatList.Service.DestructingPhoto") } /// self-destructing video - case chatListServiceDestructingVideo + internal static var chatListServiceDestructingVideo: String { return L10n.tr("Localizable", "ChatList.Service.DestructingVideo") } + /// %d %@ + internal static func chatListServiceGameScored1Countable(_ p1: Int, _ p2: String) -> String { + return L10n.tr("Localizable", "ChatList.Service.GameScored1_countable", p1, p2) + } + /// scored %d in %@ + internal static func chatListServiceGameScored1Few(_ p1: Int, _ p2: String) -> String { + return L10n.tr("Localizable", "ChatList.Service.GameScored1_few", p1, p2) + } + /// scored %d in %@ + internal static func chatListServiceGameScored1Many(_ p1: Int, _ p2: String) -> String { + return L10n.tr("Localizable", "ChatList.Service.GameScored1_many", p1, p2) + } + /// scored %d in %@ + internal static func chatListServiceGameScored1One(_ p1: Int, _ p2: String) -> String { + return L10n.tr("Localizable", "ChatList.Service.GameScored1_one", p1, p2) + } + /// scored %d in %@ + internal static func chatListServiceGameScored1Other(_ p1: Int, _ p2: String) -> String { + return L10n.tr("Localizable", "ChatList.Service.GameScored1_other", p1, p2) + } + /// scored %d in %@ + internal static func chatListServiceGameScored1Two(_ p1: Int, _ p2: String) -> String { + return L10n.tr("Localizable", "ChatList.Service.GameScored1_two", p1, p2) + } /// scored %d in %@ - case chatListServiceGameScored(Int, String) + internal static func chatListServiceGameScored1Zero(_ p1: Int, _ p2: String) -> String { + return L10n.tr("Localizable", "ChatList.Service.GameScored1_zero", p1, p2) + } + /// %1$@ invited %2$@ to the voice chat + internal static func chatListServiceVoiceChatInvitation(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "ChatList.Service.VoiceChatInvitation", p1, p2) + } + /// You invited %1$@ to the voice chat + internal static func chatListServiceVoiceChatInvitationByYou(_ p1: String) -> String { + return L10n.tr("Localizable", "ChatList.Service.VoiceChatInvitationByYou", p1) + } + /// %1$@ invited you to the voice chat + internal static func chatListServiceVoiceChatInvitationForYou(_ p1: String) -> String { + return L10n.tr("Localizable", "ChatList.Service.VoiceChatInvitationForYou", p1) + } + /// %1$@ scheduled a voice chat for %2$@ + internal static func chatListServiceVoiceChatScheduled(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "ChatList.Service.VoiceChatScheduled", p1, p2) + } + /// You scheduled a voice chat for %2$@ + internal static func chatListServiceVoiceChatScheduledYou(_ p1: String) -> String { + return L10n.tr("Localizable", "ChatList.Service.VoiceChatScheduledYou", p1) + } + /// %1$@ started a voice chat + internal static func chatListServiceVoiceChatStarted(_ p1: String) -> String { + return L10n.tr("Localizable", "ChatList.Service.VoiceChatStarted", p1) + } + /// You started a voice chat + internal static var chatListServiceVoiceChatStartedYou: String { return L10n.tr("Localizable", "ChatList.Service.VoiceChatStartedYou") } /// Cancelled Call - case chatListServiceCallCancelled - /// Disconnected - case chatListServiceCallDisconnected + internal static var chatListServiceCallCancelled: String { return L10n.tr("Localizable", "ChatList.Service.Call.Cancelled") } /// Incoming Call (%@) - case chatListServiceCallIncoming(String) + internal static func chatListServiceCallIncoming(_ p1: String) -> String { + return L10n.tr("Localizable", "ChatList.Service.Call.incoming", p1) + } /// Missed Call - case chatListServiceCallMissed + internal static var chatListServiceCallMissed: String { return L10n.tr("Localizable", "ChatList.Service.Call.Missed") } /// Outgoing Call (%@) - case chatListServiceCallOutgoing(String) + internal static func chatListServiceCallOutgoing(_ p1: String) -> String { + return L10n.tr("Localizable", "ChatList.Service.Call.outgoing", p1) + } + /// Cancelled Video Call + internal static var chatListServiceVideoCallCancelled: String { return L10n.tr("Localizable", "ChatList.Service.VideoCall.Cancelled") } + /// Incoming Video Call (%@) + internal static func chatListServiceVideoCallIncoming(_ p1: String) -> String { + return L10n.tr("Localizable", "ChatList.Service.VideoCall.incoming", p1) + } + /// Missed Video Call + internal static var chatListServiceVideoCallMissed: String { return L10n.tr("Localizable", "ChatList.Service.VideoCall.Missed") } + /// Outgoing Video Call (%@) + internal static func chatListServiceVideoCallOutgoing(_ p1: String) -> String { + return L10n.tr("Localizable", "ChatList.Service.VideoCall.outgoing", p1) + } + /// voice chat ended (%1$@) + internal static func chatListServiceVoiceChatFinishedChannel(_ p1: String) -> String { + return L10n.tr("Localizable", "ChatList.Service.VoiceChatFinished.Channel", p1) + } + /// voice chat scheduled for %@ + internal static func chatListServiceVoiceChatScheduledChannel(_ p1: String) -> String { + return L10n.tr("Localizable", "ChatList.Service.VoiceChatScheduled.Channel", p1) + } + /// voice chat started + internal static var chatListServiceVoiceChatStartedChannel: String { return L10n.tr("Localizable", "ChatList.Service.VoiceChatStarted.Channel") } + /// Archive + internal static var chatListSwipingArchive: String { return L10n.tr("Localizable", "ChatList.Swiping.Archive") } + /// Delete + internal static var chatListSwipingDelete: String { return L10n.tr("Localizable", "ChatList.Swiping.Delete") } + /// Mute + internal static var chatListSwipingMute: String { return L10n.tr("Localizable", "ChatList.Swiping.Mute") } + /// Pin + internal static var chatListSwipingPin: String { return L10n.tr("Localizable", "ChatList.Swiping.Pin") } + /// Read + internal static var chatListSwipingRead: String { return L10n.tr("Localizable", "ChatList.Swiping.Read") } + /// Unarchive + internal static var chatListSwipingUnarchive: String { return L10n.tr("Localizable", "ChatList.Swiping.Unarchive") } + /// Unmute + internal static var chatListSwipingUnmute: String { return L10n.tr("Localizable", "ChatList.Swiping.Unmute") } + /// Unpin + internal static var chatListSwipingUnpin: String { return L10n.tr("Localizable", "ChatList.Swiping.Unpin") } + /// Unread + internal static var chatListSwipingUnread: String { return L10n.tr("Localizable", "ChatList.Swiping.Unread") } /// views - case chatMessageTooltipViews + internal static var chatMessageTooltipViews: String { return L10n.tr("Localizable", "ChatMessage.Tooltip.Views") } /// channel created - case chatServiceChannelCreated + internal static var chatServiceChannelCreated: String { return L10n.tr("Localizable", "ChatService.ChannelCreated") } + /// Report Messages + internal static var chatTitleReportMessages: String { return L10n.tr("Localizable", "ChatTitle.ReportMessages") } + /// Default + internal static var chatWallpaperEmpty: String { return L10n.tr("Localizable", "ChatWallpaper.Empty") } + /// E-Mail + internal static var checkoutEmail: String { return L10n.tr("Localizable", "Checkout.Email") } + /// Enter Password + internal static var checkoutEnterPassword: String { return L10n.tr("Localizable", "Checkout.EnterPassword") } + /// An error occurred while processing your payment. Your card has not been billed. + internal static var checkoutErrorGeneric: String { return L10n.tr("Localizable", "Checkout.ErrorGeneric") } + /// You have already paid for this item. + internal static var checkoutErrorInvoiceAlreadyPaid: String { return L10n.tr("Localizable", "Checkout.ErrorInvoiceAlreadyPaid") } + /// Payment failed. Your card has not been billed. + internal static var checkoutErrorPaymentFailed: String { return L10n.tr("Localizable", "Checkout.ErrorPaymentFailed") } + /// The bot couldn't process your payment. Your card has not been billed. + internal static var checkoutErrorPrecheckoutFailed: String { return L10n.tr("Localizable", "Checkout.ErrorPrecheckoutFailed") } + /// This bot can't accept payments at the moment. Please try again later. + internal static var checkoutErrorProviderAccountInvalid: String { return L10n.tr("Localizable", "Checkout.ErrorProviderAccountInvalid") } + /// This bot can't process payments at the moment. Please try again later. + internal static var checkoutErrorProviderAccountTimeout: String { return L10n.tr("Localizable", "Checkout.ErrorProviderAccountTimeout") } + /// Name + internal static var checkoutName: String { return L10n.tr("Localizable", "Checkout.Name") } + /// Payment Method + internal static var checkoutPaymentMethod: String { return L10n.tr("Localizable", "Checkout.PaymentMethod") } + /// Pay + internal static var checkoutPayNone: String { return L10n.tr("Localizable", "Checkout.PayNone") } + /// Pay %@ + internal static func checkoutPayPrice(_ p1: String) -> String { + return L10n.tr("Localizable", "Checkout.PayPrice", p1) + } + /// Phone + internal static var checkoutPhone: String { return L10n.tr("Localizable", "Checkout.Phone") } + /// PRICE + internal static var checkoutPriceHeader: String { return L10n.tr("Localizable", "Checkout.PriceHeader") } + /// Would you like to save your password for %@? + internal static func checkoutSavePasswordTimeout(_ p1: String) -> String { + return L10n.tr("Localizable", "Checkout.SavePasswordTimeout", p1) + } + /// Shipping Information + internal static var checkoutShippingAddress: String { return L10n.tr("Localizable", "Checkout.ShippingAddress") } + /// Shipping Method + internal static var checkoutShippingMethod: String { return L10n.tr("Localizable", "Checkout.ShippingMethod") } + /// Your payment have successfully proceeded! + internal static var checkoutSuccess: String { return L10n.tr("Localizable", "Checkout.Success") } + /// Checkout + internal static var checkoutTitle: String { return L10n.tr("Localizable", "Checkout.Title") } + /// Total + internal static var checkoutTotalAmount: String { return L10n.tr("Localizable", "Checkout.TotalAmount") } + /// Total Paid + internal static var checkoutTotalPaidAmount: String { return L10n.tr("Localizable", "Checkout.TotalPaidAmount") } + /// Saving payments details are only available with 2-Step Verification. + internal static var checkout2FAText: String { return L10n.tr("Localizable", "Checkout.2FA.Text") } + /// Cardholder Name + internal static var checkoutNewCardCardholderNamePlaceholder: String { return L10n.tr("Localizable", "Checkout.NewCard.CardholderNamePlaceholder") } + /// CARDHOLDER + internal static var checkoutNewCardCardholderNameTitle: String { return L10n.tr("Localizable", "Checkout.NewCard.CardholderNameTitle") } + /// PAYMENT CARD + internal static var checkoutNewCardPaymentCard: String { return L10n.tr("Localizable", "Checkout.NewCard.PaymentCard") } + /// Zip Code + internal static var checkoutNewCardPostcodePlaceholder: String { return L10n.tr("Localizable", "Checkout.NewCard.PostcodePlaceholder") } + /// BILLING ADDRESS + internal static var checkoutNewCardPostcodeTitle: String { return L10n.tr("Localizable", "Checkout.NewCard.PostcodeTitle") } + /// Save Payment Information + internal static var checkoutNewCardSaveInfo: String { return L10n.tr("Localizable", "Checkout.NewCard.SaveInfo") } + /// You can save your payment information for future use.\nPlease [turn on Two-Step Verification] to enable this. + internal static var checkoutNewCardSaveInfoEnableHelp: String { return L10n.tr("Localizable", "Checkout.NewCard.SaveInfoEnableHelp") } + /// You can save your payment information for future use. + internal static var checkoutNewCardSaveInfoHelp: String { return L10n.tr("Localizable", "Checkout.NewCard.SaveInfoHelp") } + /// New Card + internal static var checkoutNewCardTitle: String { return L10n.tr("Localizable", "Checkout.NewCard.Title") } + /// Pay + internal static var checkoutPasswordEntryPay: String { return L10n.tr("Localizable", "Checkout.PasswordEntry.Pay") } + /// Your card %@ is on file. To pay with this card, please enter your 2-Step-Verification password. + internal static func checkoutPasswordEntryText(_ p1: String) -> String { + return L10n.tr("Localizable", "Checkout.PasswordEntry.Text", p1) + } + /// Payment Confirmation + internal static var checkoutPasswordEntryTitle: String { return L10n.tr("Localizable", "Checkout.PasswordEntry.Title") } + /// New Card... + internal static var checkoutPaymentMethodNew: String { return L10n.tr("Localizable", "Checkout.PaymentMethod.New") } + /// Payment Method + internal static var checkoutPaymentMethodTitle: String { return L10n.tr("Localizable", "Checkout.PaymentMethod.Title") } + /// Receipt + internal static var checkoutReceiptTitle: String { return L10n.tr("Localizable", "Checkout.Receipt.Title") } + /// Shipping Method + internal static var checkoutShippingOptionTitle: String { return L10n.tr("Localizable", "Checkout.ShippingOption.Title") } + /// Complete Payment + internal static var checkoutWebConfirmationTitle: String { return L10n.tr("Localizable", "Checkout.WebConfirmation.Title") } + /// Please enter a valid city. + internal static var checkoutInfoErrorCityInvalid: String { return L10n.tr("Localizable", "CheckoutInfo.ErrorCityInvalid") } + /// Please enter a valid e-mail address. + internal static var checkoutInfoErrorEmailInvalid: String { return L10n.tr("Localizable", "CheckoutInfo.ErrorEmailInvalid") } + /// Please enter a valid name. + internal static var checkoutInfoErrorNameInvalid: String { return L10n.tr("Localizable", "CheckoutInfo.ErrorNameInvalid") } + /// Please enter a valid phone number. + internal static var checkoutInfoErrorPhoneInvalid: String { return L10n.tr("Localizable", "CheckoutInfo.ErrorPhoneInvalid") } + /// Please enter a valid postcode. + internal static var checkoutInfoErrorPostcodeInvalid: String { return L10n.tr("Localizable", "CheckoutInfo.ErrorPostcodeInvalid") } + /// Shipping to the selected country is not available. + internal static var checkoutInfoErrorShippingNotAvailable: String { return L10n.tr("Localizable", "CheckoutInfo.ErrorShippingNotAvailable") } + /// Please enter a valid state. + internal static var checkoutInfoErrorStateInvalid: String { return L10n.tr("Localizable", "CheckoutInfo.ErrorStateInvalid") } + /// Pay + internal static var checkoutInfoPay: String { return L10n.tr("Localizable", "CheckoutInfo.Pay") } + /// Email + internal static var checkoutInfoReceiverInfoEmail: String { return L10n.tr("Localizable", "CheckoutInfo.ReceiverInfoEmail") } + /// Email + internal static var checkoutInfoReceiverInfoEmailPlaceholder: String { return L10n.tr("Localizable", "CheckoutInfo.ReceiverInfoEmailPlaceholder") } + /// Name + internal static var checkoutInfoReceiverInfoName: String { return L10n.tr("Localizable", "CheckoutInfo.ReceiverInfoName") } + /// Name Surname + internal static var checkoutInfoReceiverInfoNamePlaceholder: String { return L10n.tr("Localizable", "CheckoutInfo.ReceiverInfoNamePlaceholder") } + /// Phone + internal static var checkoutInfoReceiverInfoPhone: String { return L10n.tr("Localizable", "CheckoutInfo.ReceiverInfoPhone") } + /// RECEIVER + internal static var checkoutInfoReceiverInfoTitle: String { return L10n.tr("Localizable", "CheckoutInfo.ReceiverInfoTitle") } + /// Save Info + internal static var checkoutInfoSaveInfo: String { return L10n.tr("Localizable", "CheckoutInfo.SaveInfo") } + /// You can save your shipping information for future use. + internal static var checkoutInfoSaveInfoHelp: String { return L10n.tr("Localizable", "CheckoutInfo.SaveInfoHelp") } + /// Address 1 + internal static var checkoutInfoShippingInfoAddress1: String { return L10n.tr("Localizable", "CheckoutInfo.ShippingInfoAddress1") } + /// Address + internal static var checkoutInfoShippingInfoAddress1Placeholder: String { return L10n.tr("Localizable", "CheckoutInfo.ShippingInfoAddress1Placeholder") } + /// Address 2 + internal static var checkoutInfoShippingInfoAddress2: String { return L10n.tr("Localizable", "CheckoutInfo.ShippingInfoAddress2") } + /// Address + internal static var checkoutInfoShippingInfoAddress2Placeholder: String { return L10n.tr("Localizable", "CheckoutInfo.ShippingInfoAddress2Placeholder") } + /// City + internal static var checkoutInfoShippingInfoCity: String { return L10n.tr("Localizable", "CheckoutInfo.ShippingInfoCity") } + /// City + internal static var checkoutInfoShippingInfoCityPlaceholder: String { return L10n.tr("Localizable", "CheckoutInfo.ShippingInfoCityPlaceholder") } + /// Country + internal static var checkoutInfoShippingInfoCountry: String { return L10n.tr("Localizable", "CheckoutInfo.ShippingInfoCountry") } + /// Country + internal static var checkoutInfoShippingInfoCountryPlaceholder: String { return L10n.tr("Localizable", "CheckoutInfo.ShippingInfoCountryPlaceholder") } + /// Postcode + internal static var checkoutInfoShippingInfoPostcode: String { return L10n.tr("Localizable", "CheckoutInfo.ShippingInfoPostcode") } + /// Postcode + internal static var checkoutInfoShippingInfoPostcodePlaceholder: String { return L10n.tr("Localizable", "CheckoutInfo.ShippingInfoPostcodePlaceholder") } + /// State + internal static var checkoutInfoShippingInfoState: String { return L10n.tr("Localizable", "CheckoutInfo.ShippingInfoState") } + /// State + internal static var checkoutInfoShippingInfoStatePlaceholder: String { return L10n.tr("Localizable", "CheckoutInfo.ShippingInfoStatePlaceholder") } + /// SHIPPING ADDRESS + internal static var checkoutInfoShippingInfoTitle: String { return L10n.tr("Localizable", "CheckoutInfo.ShippingInfoTitle") } + /// Shipping Information + internal static var checkoutInfoTitle: String { return L10n.tr("Localizable", "CheckoutInfo.Title") } /// Create - case composeCreate + internal static var composeCreate: String { return L10n.tr("Localizable", "Compose.Create") } /// Next - case composeNext + internal static var composeNext: String { return L10n.tr("Localizable", "Compose.Next") } /// Select users - case composeSelectUsers - /// Create secret chat with "%@" - case composeConfirmStartSecretChat(String) + internal static var composeSelectUsers: String { return L10n.tr("Localizable", "Compose.SelectUsers") } + /// Start a secret chat with "%@" + internal static func composeConfirmStartSecretChat(_ p1: String) -> String { + return L10n.tr("Localizable", "Compose.Confirm.StartSecretChat", p1) + } + /// You will be able to add more users after you finish creating the group and convert it to supergroup. + internal static var composeCreateGroupLimitError: String { return L10n.tr("Localizable", "Compose.CreateGroup.LimitError") } /// New Channel - case composePopoverNewChannel + internal static var composePopoverNewChannel: String { return L10n.tr("Localizable", "Compose.Popover.NewChannel") } /// New Group - case composePopoverNewGroup + internal static var composePopoverNewGroup: String { return L10n.tr("Localizable", "Compose.Popover.NewGroup") } /// New Secret Chat - case composePopoverNewSecretChat + internal static var composePopoverNewSecretChat: String { return L10n.tr("Localizable", "Compose.Popover.NewSecretChat") } /// Secret Chat - case composeSelectSecretChat + internal static var composeSelectSecretChat: String { return L10n.tr("Localizable", "Compose.Select.SecretChat") } /// Whom would you like to message? - case composeSelectGroupUsersPlaceholder + internal static var composeSelectGroupUsersPlaceholder: String { return L10n.tr("Localizable", "Compose.SelectGroupUsers.Placeholder") } /// Add the bot to "%@"? - case confirmAddBotToGroup(String) - /// Wait! Deleting this channel will remove all members and all messages will be lost. Delete the channel anyway? - case confirmDeleteAdminedChannel - /// Are you sure you want to delete all message history?\n\nThis action cannot be undone. - case confirmDeleteChatUser + internal static func confirmAddBotToGroup(_ p1: String) -> String { + return L10n.tr("Localizable", "Confirm.AddBotToGroup", p1) + } + /// Delete + internal static var confirmDelete: String { return L10n.tr("Localizable", "Confirm.Delete") } + /// Wait! Deleting this channel will remove all of its members and all of its messages will be lost forever.\n\nAre you sure you want to continue? + internal static var confirmDeleteAdminedChannel: String { return L10n.tr("Localizable", "Confirm.DeleteAdminedChannel") } + /// Are you sure you want to delete all message history? + internal static var confirmDeleteChatUser: String { return L10n.tr("Localizable", "Confirm.DeleteChatUser") } /// Are you sure you want to leave this group? - case confirmLeaveGroup + internal static var confirmLeaveGroup: String { return L10n.tr("Localizable", "Confirm.LeaveGroup") } + /// The bot will know your phone number. This can be useful for integration with other services. + internal static var confirmDescPermissionInlineBotContact: String { return L10n.tr("Localizable", "Confirm.Desc.PermissionInlineBotContact") } + /// Share Your Phone Number? + internal static var confirmHeaderPermissionInlineBotContact: String { return L10n.tr("Localizable", "Confirm.Header.PermissionInlineBotContact") } /// connecting - case connectingStatusConnecting + internal static var connectingStatusConnecting: String { return L10n.tr("Localizable", "ConnectingStatus.connecting") } /// connecting to proxy - case connectingStatusConnectingToProxy + internal static var connectingStatusConnectingToProxy: String { return L10n.tr("Localizable", "ConnectingStatus.connectingToProxy") } /// click here to disable proxy - case connectingStatusDisableProxy + internal static var connectingStatusDisableProxy: String { return L10n.tr("Localizable", "ConnectingStatus.DisableProxy") } /// online - case connectingStatusOnline + internal static var connectingStatusOnline: String { return L10n.tr("Localizable", "ConnectingStatus.online") } /// updating - case connectingStatusUpdating + internal static var connectingStatusUpdating: String { return L10n.tr("Localizable", "ConnectingStatus.updating") } /// waiting for network - case connectingStatusWaitingNetwork + internal static var connectingStatusWaitingNetwork: String { return L10n.tr("Localizable", "ConnectingStatus.waitingNetwork") } + /// birthday + internal static var contactInfoBirthdayLabel: String { return L10n.tr("Localizable", "ContactInfo.BirthdayLabel") } + /// Contact Info + internal static var contactInfoContactInfo: String { return L10n.tr("Localizable", "ContactInfo.ContactInfo") } + /// job + internal static var contactInfoJob: String { return L10n.tr("Localizable", "ContactInfo.Job") } + /// home + internal static var contactInfoPhoneLabelHome: String { return L10n.tr("Localizable", "ContactInfo.PhoneLabelHome") } + /// home fax + internal static var contactInfoPhoneLabelHomeFax: String { return L10n.tr("Localizable", "ContactInfo.PhoneLabelHomeFax") } + /// main + internal static var contactInfoPhoneLabelMain: String { return L10n.tr("Localizable", "ContactInfo.PhoneLabelMain") } + /// mobile + internal static var contactInfoPhoneLabelMobile: String { return L10n.tr("Localizable", "ContactInfo.PhoneLabelMobile") } + /// other + internal static var contactInfoPhoneLabelOther: String { return L10n.tr("Localizable", "ContactInfo.PhoneLabelOther") } + /// pager + internal static var contactInfoPhoneLabelPager: String { return L10n.tr("Localizable", "ContactInfo.PhoneLabelPager") } + /// work + internal static var contactInfoPhoneLabelWork: String { return L10n.tr("Localizable", "ContactInfo.PhoneLabelWork") } + /// work fax + internal static var contactInfoPhoneLabelWorkFax: String { return L10n.tr("Localizable", "ContactInfo.PhoneLabelWorkFax") } + /// homepage + internal static var contactInfoURLLabelHomepage: String { return L10n.tr("Localizable", "ContactInfo.URLLabelHomepage") } /// Add Contact - case contactsAddContact + internal static var contactsAddContact: String { return L10n.tr("Localizable", "Contacts.AddContact") } /// Contacts - case contactsContacsSeparator - /// This contact is not registered in Telegram yet. You will be able to send them a Telegram message as soon as they sign up. - case contactsNotRegistredDescription - /// Not Telegram Contact - case contactsNotRegistredTitle + internal static var contactsContacsSeparator: String { return L10n.tr("Localizable", "Contacts.ContacsSeparator") } + /// This person is not registered on Telegram yet.\n\nYou will be able to send them a Telegram message as soon as they sign up. + internal static var contactsNotRegistredDescription: String { return L10n.tr("Localizable", "Contacts.NotRegistredDescription") } + /// Not a Telegram User + internal static var contactsNotRegistredTitle: String { return L10n.tr("Localizable", "Contacts.NotRegistredTitle") } /// First Name - case contactsFirstNamePlaceholder + internal static var contactsFirstNamePlaceholder: String { return L10n.tr("Localizable", "Contacts.FirstName.Placeholder") } /// Last Name - case contactsLastNamePlaceholder + internal static var contactsLastNamePlaceholder: String { return L10n.tr("Localizable", "Contacts.LastName.Placeholder") } + /// phone number can't be empty + internal static var contactsPhoneNumberInvalid: String { return L10n.tr("Localizable", "Contacts.PhoneNumber.Invalid") } + /// the person with this phone number is not registered on Telegram yet. + internal static var contactsPhoneNumberNotRegistred: String { return L10n.tr("Localizable", "Contacts.PhoneNumber.NotRegistred") } /// Phone Number - case contactsPhoneNumberPlaceholder + internal static var contactsPhoneNumberPlaceholder: String { return L10n.tr("Localizable", "Contacts.PhoneNumber.Placeholder") } /// Save as... - case contextCopyMedia + internal static var contextCopyMedia: String { return L10n.tr("Localizable", "Context.CopyMedia") } + /// Open in Quick Look + internal static var contextOpenInQuickLook: String { return L10n.tr("Localizable", "Context.OpenInQuickLook") } /// Remove - case contextRecentGifRemove + internal static var contextRecentGifRemove: String { return L10n.tr("Localizable", "Context.RecentGifRemove") } /// Remove - case contextRemoveFaveSticker + internal static var contextRemoveFaveSticker: String { return L10n.tr("Localizable", "Context.RemoveFaveSticker") } /// Show In Finder - case contextShowInFinder + internal static var contextShowInFinder: String { return L10n.tr("Localizable", "Context.ShowInFinder") } /// View Sticker Set - case contextViewStickerSet + internal static var contextViewStickerSet: String { return L10n.tr("Localizable", "Context.ViewStickerSet") } /// Are you sure? This action cannot be undone. - case convertToSuperGroupConfirm - /// Something is wrong, please try again later. - case convertToSupergroupAlertError + internal static var convertToSuperGroupConfirm: String { return L10n.tr("Localizable", "ConvertToSuperGroup.Confirm") } + /// Something went wrong, sorry. Please try again later. + internal static var convertToSupergroupAlertError: String { return L10n.tr("Localizable", "ConvertToSupergroup.Alert.Error") } + /// Cancel + internal static var crashOnLaunchCancel: String { return L10n.tr("Localizable", "CrashOnLaunch.Cancel") } + /// If Telegram keeps crashing immediately after you open it, click OK to log out of the app. This should solve this issue. + internal static var crashOnLaunchInformation: String { return L10n.tr("Localizable", "CrashOnLaunch.Information") } + /// Something’s not right. + internal static var crashOnLaunchMessage: String { return L10n.tr("Localizable", "CrashOnLaunch.Message") } + /// Log out + internal static var crashOnLaunchOK: String { return L10n.tr("Localizable", "CrashOnLaunch.OK") } + /// Sorry, you are a member of too many groups and channels. Please leave some before creating a new one. + internal static var createChannelsTooMuch: String { return L10n.tr("Localizable", "Create.ChannelsTooMuch") } /// Group Name - case createGroupNameHolder - /// Smart Links - case cwLP1JidTitle - /// Make Lower Case - case d9MCDAMdTitle + internal static var createGroupNameHolder: String { return L10n.tr("Localizable", "CreateGroup.NameHolder") } + /// Night Mode + internal static var darkModeConfirmNightModeHeader: String { return L10n.tr("Localizable", "DarkMode.Confirm.NightMode.Header") } + /// Disable + internal static var darkModeConfirmNightModeOK: String { return L10n.tr("Localizable", "DarkMode.Confirm.NightMode.OK") } + /// You have enabled auto night mode. If you want to change dark mode you have to disable it. + internal static var darkModeConfirmNightModeText: String { return L10n.tr("Localizable", "DarkMode.Confirm.NightMode.Text") } + /// Auto-Download Media + internal static var dataAndStorageAutomaticDownload: String { return L10n.tr("Localizable", "DataAndStorage.AutomaticDownload") } + /// Download Folder + internal static var dataAndStorageDownloadFolder: String { return L10n.tr("Localizable", "DataAndStorage.DownloadFolder") } /// Network Usage - case dataAndStorageNetworkUsage + internal static var dataAndStorageNetworkUsage: String { return L10n.tr("Localizable", "DataAndStorage.NetworkUsage") } /// Storage Usage - case dataAndStorageStorageUsage + internal static var dataAndStorageStorageUsage: String { return L10n.tr("Localizable", "DataAndStorage.StorageUsage") } /// AUTOMATIC AUDIO DOWNLOAD - case dataAndStorageAutomaticAudioDownloadHeader + internal static var dataAndStorageAutomaticAudioDownloadHeader: String { return L10n.tr("Localizable", "DataAndStorage.AutomaticAudioDownload.Header") } + /// Files + internal static var dataAndStorageAutomaticDownloadFiles: String { return L10n.tr("Localizable", "DataAndStorage.AutomaticDownload.Files") } + /// GIFs + internal static var dataAndStorageAutomaticDownloadGIFs: String { return L10n.tr("Localizable", "DataAndStorage.AutomaticDownload.GIFs") } /// Groups and Channels - case dataAndStorageAutomaticDownloadGroupsChannels + internal static var dataAndStorageAutomaticDownloadGroupsChannels: String { return L10n.tr("Localizable", "DataAndStorage.AutomaticDownload.GroupsChannels") } + /// AUTOMATIC MEDIA DOWNLOAD + internal static var dataAndStorageAutomaticDownloadHeader: String { return L10n.tr("Localizable", "DataAndStorage.AutomaticDownload.Header") } + /// Video Messages + internal static var dataAndStorageAutomaticDownloadInstantVideo: String { return L10n.tr("Localizable", "DataAndStorage.AutomaticDownload.InstantVideo") } + /// Photos + internal static var dataAndStorageAutomaticDownloadPhoto: String { return L10n.tr("Localizable", "DataAndStorage.AutomaticDownload.Photo") } + /// Reset Auto-Download Settings + internal static var dataAndStorageAutomaticDownloadReset: String { return L10n.tr("Localizable", "DataAndStorage.AutomaticDownload.Reset") } + /// Videos + internal static var dataAndStorageAutomaticDownloadVideo: String { return L10n.tr("Localizable", "DataAndStorage.AutomaticDownload.Video") } + /// Voice Messages + internal static var dataAndStorageAutomaticDownloadVoice: String { return L10n.tr("Localizable", "DataAndStorage.AutomaticDownload.Voice") } /// AUTOMATIC PHOTO DOWNLOAD - case dataAndStorageAutomaticPhotoDownloadHeader + internal static var dataAndStorageAutomaticPhotoDownloadHeader: String { return L10n.tr("Localizable", "DataAndStorage.AutomaticPhotoDownload.Header") } /// AUTOMATIC VIDEO DOWNLOAD - case dataAndStorageAutomaticVideoDownloadHeader + internal static var dataAndStorageAutomaticVideoDownloadHeader: String { return L10n.tr("Localizable", "DataAndStorage.AutomaticVideoDownload.Header") } + /// GIFs + internal static var dataAndStorageAutoplayGIFs: String { return L10n.tr("Localizable", "DataAndStorage.Autoplay.GIFs") } + /// AUTO-PLAY MEDIA + internal static var dataAndStorageAutoplayHeader: String { return L10n.tr("Localizable", "DataAndStorage.Autoplay.Header") } + /// Sound on Hover + internal static var dataAndStorageAutoplaySoundOnHover: String { return L10n.tr("Localizable", "DataAndStorage.Autoplay.SoundOnHover") } + /// Videos + internal static var dataAndStorageAutoplayVideos: String { return L10n.tr("Localizable", "DataAndStorage.Autoplay.Videos") } + /// Sound will start playing when you move your cursor over a video. + internal static var dataAndStorageAutoplaySoundOnHoverDesc: String { return L10n.tr("Localizable", "DataAndStorage.Autoplay.SoundOnHover.Desc") } + /// Preload Larger Videos + internal static var dataAndStorageCategoryPreloadLargeVideos: String { return L10n.tr("Localizable", "DataAndStorage.Category.PreloadLargeVideos") } + /// Preload first few seconds (1-2 MB) of videos large than %@ MB for instant playback. + internal static func dataAndStorageCategoryPreloadLargeVideosDesc(_ p1: String) -> String { + return L10n.tr("Localizable", "DataAndStorage.Category.PreloadLargeVideosDesc", p1) + } + /// Channels + internal static var dataAndStorageCategorySettingsChannels: String { return L10n.tr("Localizable", "DataAndStorage.CategorySettings.Channels") } + /// Group Chats + internal static var dataAndStorageCategorySettingsGroupChats: String { return L10n.tr("Localizable", "DataAndStorage.CategorySettings.GroupChats") } + /// Private Chats + internal static var dataAndStorageCategorySettingsPrivateChats: String { return L10n.tr("Localizable", "DataAndStorage.CategorySettings.PrivateChats") } + /// Unlimited + internal static var dataAndStorageCateroryFileSizeUnlimited: String { return L10n.tr("Localizable", "DataAndStorage.CateroryFileSize.Unlimited") } + /// LIMIT BY SIZE + internal static var dataAndStorageCateroryFileSizeLimitHeader: String { return L10n.tr("Localizable", "DataAndStorage.CateroryFileSizeLimit.Header") } + /// Undo all custom auto-download settings. + internal static var dataAndStorageConfirmResetSettings: String { return L10n.tr("Localizable", "DataAndStorage.Confirm.ResetSettings") } /// Today - case dateToday - /// Undo - case drj4nYzgTitle + internal static var dateToday: String { return L10n.tr("Localizable", "Date.Today") } + /// Delete for all members + internal static var deleteChatDeleteGroupForAll: String { return L10n.tr("Localizable", "DeleteChat.DeleteGroupForAll") } + /// Link Group + internal static var discussionSetModalOK: String { return L10n.tr("Localizable", "Discussion.Set.Modal.OK") } + /// Do you want make **%@** the discussion board for **%@**?\n\nAny member of this group will be able to see messages in the channel. + internal static func discussionSetModalTextChannelPrivateGroup(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Discussion.Set.Modal.Text.ChannelPrivateGroup", p1, p2) + } + /// Do you want make **%@** the discussion board for **%@**?\n\nAny member of this group will able to see all messages in the channel. + internal static func discussionSetModalTextPrivateChannelPublicGroup(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Discussion.Set.Modal.Text.PrivateChannelPublicGroup", p1, p2) + } + /// Do you want make **%@** the discussion board for **%@**? + internal static func discussionSetModalTextPublicChannelPublicGroup(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Discussion.Set.Modal.Text.PublicChannelPublicGroup", p1, p2) + } + /// Discuss + internal static var discussionControllerIconText: String { return L10n.tr("Localizable", "DiscussionController.IconText") } + /// private channel + internal static var discussionControllerPrivateChannel: String { return L10n.tr("Localizable", "DiscussionController.PrivateChannel") } + /// private group + internal static var discussionControllerPrivateGroup: String { return L10n.tr("Localizable", "DiscussionController.PrivateGroup") } + /// Discussion Group + internal static var discussionControllerChannelTitle: String { return L10n.tr("Localizable", "DiscussionController.Channel.Title") } + /// Create a New Group + internal static var discussionControllerChannelEmptyCreateGroup: String { return L10n.tr("Localizable", "DiscussionController.Channel.Empty.CreateGroup") } + /// Everything you post in channel will be forwarded to this group. + internal static var discussionControllerChannelEmptyDescription: String { return L10n.tr("Localizable", "DiscussionController.Channel.Empty.Description") } + /// Select a group chat that will be used to host comments from your channel. + internal static var discussionControllerChannelEmptyHeader1: String { return L10n.tr("Localizable", "DiscussionController.Channel.Empty.Header1") } + /// Everything you post in channel is forwarded to this group. + internal static var discussionControllerChannelSetDescription: String { return L10n.tr("Localizable", "DiscussionController.Channel.Set.Description") } + /// **%@** is selected as the group that will be used to host comments for your channel. + internal static func discussionControllerChannelSetHeader1(_ p1: String) -> String { + return L10n.tr("Localizable", "DiscussionController.Channel.Set.Header1", p1) + } + /// Unlink Group + internal static var discussionControllerChannelSetUnlinkGroup: String { return L10n.tr("Localizable", "DiscussionController.Channel.Set.UnlinkGroup") } + /// Are you sure you want to unlink channel from this group? + internal static var discussionControllerConfrimUnlinkChannel: String { return L10n.tr("Localizable", "DiscussionController.Confrim.UnlinkChannel") } + /// Are you sure you want to unlink group from this channel? + internal static var discussionControllerConfrimUnlinkGroup: String { return L10n.tr("Localizable", "DiscussionController.Confrim.UnlinkGroup") } + /// Proceed + internal static var discussionControllerErrorOK: String { return L10n.tr("Localizable", "DiscussionController.Error.OK") } + /// Warning: If you set this private group as the disccussion group for your channel, all channel subscribers will be able to access the group. "Chat history for new members" will be switched to Visible + internal static var discussionControllerErrorPreHistory: String { return L10n.tr("Localizable", "DiscussionController.Error.PreHistory") } + /// Linked Channel + internal static var discussionControllerGroupTitle: String { return L10n.tr("Localizable", "DiscussionController.Group.Title") } + /// All new messages posted in this channel are forwarded to this group. + internal static var discussionControllerGroupSetDescription: String { return L10n.tr("Localizable", "DiscussionController.Group.Set.Description") } + /// **%@** is linking the group as its discussion board. + internal static func discussionControllerGroupSetHeader(_ p1: String) -> String { + return L10n.tr("Localizable", "DiscussionController.Group.Set.Header", p1) + } + /// Unlink Channel + internal static var discussionControllerGroupSetUnlinkChannel: String { return L10n.tr("Localizable", "DiscussionController.Group.Set.UnlinkChannel") } + /// The channel successfully unlinked. + internal static var discussionControllerGroupUnsetDescription: String { return L10n.tr("Localizable", "DiscussionController.Group.Unset.Description") } + /// You will be displayed as your personal account. + internal static var displayMeAsAlone: String { return L10n.tr("Localizable", "DisplayMeAs.Alone") } + /// Continue as %@ + internal static func displayMeAsContinueAs(_ p1: String) -> String { + return L10n.tr("Localizable", "DisplayMeAs.ContinueAs", p1) + } + /// personal account + internal static var displayMeAsPersonalAccount: String { return L10n.tr("Localizable", "DisplayMeAs.PersonalAccount") } + /// Scheduled Voice Chat + internal static var displayMeAsScheduled: String { return L10n.tr("Localizable", "DisplayMeAs.Scheduled") } + /// Choose whether you want to be displayed as your personal account or as your channel. + internal static var displayMeAsText: String { return L10n.tr("Localizable", "DisplayMeAs.Text") } + /// Display Me As + internal static var displayMeAsTitle: String { return L10n.tr("Localizable", "DisplayMeAs.Title") } + /// You can also create a public channel to participate in voice chats as a channel. + internal static var displayMeAsAloneDesc: String { return L10n.tr("Localizable", "DisplayMeAs.Alone.Desc") } + /// Schedule Voice Chat as %@ + internal static func displayMeAsNewScheduleAs(_ p1: String) -> String { + return L10n.tr("Localizable", "DisplayMeAs.New.ScheduleAs", p1) + } + /// Start Voice Chat as %@ + internal static func displayMeAsNewStartAs(_ p1: String) -> String { + return L10n.tr("Localizable", "DisplayMeAs.New.StartAs", p1) + } + /// New Voice Chat + internal static var displayMeAsNewTitle: String { return L10n.tr("Localizable", "DisplayMeAs.New.Title") } + /// Subscribers will be notified that the voice chat start in %@ + internal static func displayMeAsScheduledDesc(_ p1: String) -> String { + return L10n.tr("Localizable", "DisplayMeAs.Scheduled.Desc", p1) + } + /// Choose whether you want to be displayed as your personal account or as group. + internal static var displayMeAsTextGroup: String { return L10n.tr("Localizable", "DisplayMeAs.Text.Group") } /// Spelling and Grammar - case dv1IoYv7Title + internal static var dv1IoYv7Title: String { return L10n.tr("Localizable", "Dv1-io-Yv7.title") } + /// Edit + internal static var editMessageEditCurrentPhoto: String { return L10n.tr("Localizable", "Edit.Message.EditCurrentPhoto") } + /// Add Account + internal static var editAccountAddAccount: String { return L10n.tr("Localizable", "EditAccount.AddAccount") } + /// Change Number + internal static var editAccountChangeNumber: String { return L10n.tr("Localizable", "EditAccount.ChangeNumber") } + /// Log Out + internal static var editAccountLogout: String { return L10n.tr("Localizable", "EditAccount.Logout") } + /// Enter your name and add a profile photo. + internal static var editAccountNameDesc: String { return L10n.tr("Localizable", "EditAccount.NameDesc") } + /// Edit Profile + internal static var editAccountTitle: String { return L10n.tr("Localizable", "EditAccount.Title") } + /// Username + internal static var editAccountUsername: String { return L10n.tr("Localizable", "EditAccount.Username") } + /// Photo or Video + internal static var editAvatarPhotoOrVideo: String { return L10n.tr("Localizable", "EditAvatar.PhotoOrVideo") } + /// Sticker or GIF + internal static var editAvatarStickerOrGif: String { return L10n.tr("Localizable", "EditAvatar.StickerOrGif") } + /// RESET + internal static var editImageControlReset: String { return L10n.tr("Localizable", "EditImageControl.Reset") } + /// Are you sure you want to close and discard all changes? + internal static var editImageControlConfirmDiscard: String { return L10n.tr("Localizable", "EditImageControl.Confirm.Discard") } + /// Edit Link + internal static var editInvitationEditTitle: String { return L10n.tr("Localizable", "EditInvitation.EditTitle") } + /// Enter Number + internal static var editInvitationEnterNumber: String { return L10n.tr("Localizable", "EditInvitation.EnterNumber") } + /// Expiry Date + internal static var editInvitationExpiryDate: String { return L10n.tr("Localizable", "EditInvitation.ExpiryDate") } + /// you can make the link expire after a certain time. + internal static var editInvitationExpiryDesc: String { return L10n.tr("Localizable", "EditInvitation.ExpiryDesc") } + /// you can make the link expire after it has been used for a certain number of times. + internal static var editInvitationLimitDesc: String { return L10n.tr("Localizable", "EditInvitation.LimitDesc") } + /// LIMITED BY NUMBER OF USERS + internal static var editInvitationLimitedByCount: String { return L10n.tr("Localizable", "EditInvitation.LimitedByCount") } + /// LIMITED BY PERIOD + internal static var editInvitationLimitedByPeriod: String { return L10n.tr("Localizable", "EditInvitation.LimitedByPeriod") } + /// Never + internal static var editInvitationNever: String { return L10n.tr("Localizable", "EditInvitation.Never") } + /// New Link + internal static var editInvitationNewTitle: String { return L10n.tr("Localizable", "EditInvitation.NewTitle") } + /// Number of Users + internal static var editInvitationNumberOfUsers: String { return L10n.tr("Localizable", "EditInvitation.NumberOfUsers") } + /// Save + internal static var editInvitationSave: String { return L10n.tr("Localizable", "EditInvitation.Save") } + /// Unlimited + internal static var editInvitationUnlimited: String { return L10n.tr("Localizable", "EditInvitation.Unlimited") } + /// Create + internal static var editInvitationOKCreate: String { return L10n.tr("Localizable", "EditInvitation.OK.Create") } + /// Save + internal static var editInvitationOKSave: String { return L10n.tr("Localizable", "EditInvitation.OK.Save") } + /// This name is already taken. + internal static var editThameNameAlreadyTaken: String { return L10n.tr("Localizable", "EditThame.Name.AlreadyTaken") } + /// Save + internal static var editThemeEdit: String { return L10n.tr("Localizable", "EditTheme.Edit") } + /// Theme Name + internal static var editThemeNamePlaceholder: String { return L10n.tr("Localizable", "EditTheme.NamePlaceholder") } + /// Create from File... + internal static var editThemeSelectFile: String { return L10n.tr("Localizable", "EditTheme.SelectFile") } + /// This theme will be based on your current theme and wallpaper. Otherwise, you can use a custom theme file if you already have one. + internal static var editThemeSelectFileDesc: String { return L10n.tr("Localizable", "EditTheme.SelectFileDesc") } + /// Update from File... + internal static var editThemeSelectUpdatedFile: String { return L10n.tr("Localizable", "EditTheme.SelectUpdatedFile") } + /// You can update your theme for all users by uploading manual changes from a file. + internal static var editThemeSelectUpdatedFileDesc: String { return L10n.tr("Localizable", "EditTheme.SelectUpdatedFileDesc") } + /// Your theme will be updated for all users each time you change it. Anyone can install it using this link.\n\nTheme links must be longer than 5 characters and can use a-z, 0-9 and underscores. + internal static var editThemeSlugDesc: String { return L10n.tr("Localizable", "EditTheme.SlugDesc") } + /// short link + internal static var editThemeSlugPlaceholder: String { return L10n.tr("Localizable", "EditTheme.SlugPlaceholder") } + /// Edit Theme + internal static var editThemeTitle: String { return L10n.tr("Localizable", "EditTheme.Title") } + /// This link is already taken. Please try a different one. + internal static var editThemeSlugErrorAlreadyExists: String { return L10n.tr("Localizable", "EditTheme.SlugError.AlreadyExists") } + /// invalid format. + internal static var editThemeSlugErrorFormat: String { return L10n.tr("Localizable", "EditTheme.SlugError.Format") } /// Activity & Sport - case emojiActivityAndSport + internal static var emojiActivityAndSport: String { return L10n.tr("Localizable", "Emoji.ActivityAndSport") } /// Animals & Nature - case emojiAnimalsAndNature + internal static var emojiAnimalsAndNature: String { return L10n.tr("Localizable", "Emoji.AnimalsAndNature") } /// Flags - case emojiFlags + internal static var emojiFlags: String { return L10n.tr("Localizable", "Emoji.Flags") } /// Food & Drink - case emojiFoodAndDrink + internal static var emojiFoodAndDrink: String { return L10n.tr("Localizable", "Emoji.FoodAndDrink") } /// Objects - case emojiObjects + internal static var emojiObjects: String { return L10n.tr("Localizable", "Emoji.Objects") } /// Frequently Used - case emojiRecent + internal static var emojiRecent: String { return L10n.tr("Localizable", "Emoji.Recent") } /// Smileys & People - case emojiSmilesAndPeople + internal static var emojiSmilesAndPeople: String { return L10n.tr("Localizable", "Emoji.SmilesAndPeople") } /// Symbols - case emojiSymbols + internal static var emojiSymbols: String { return L10n.tr("Localizable", "Emoji.Symbols") } /// Travel & Places - case emojiTravelAndPlaces + internal static var emojiTravelAndPlaces: String { return L10n.tr("Localizable", "Emoji.TravelAndPlaces") } + /// Appearance + internal static var emptyChatAppearance: String { return L10n.tr("Localizable", "EmptyChat.Appearance") } + /// Suggest Stickers By Emoji + internal static var emptyChatStickers: String { return L10n.tr("Localizable", "EmptyChat.Stickers") } + /// Storage Usage + internal static var emptyChatStorageUsage: String { return L10n.tr("Localizable", "EmptyChat.StorageUsage") } + /// Chat Mode + internal static var emptyChatAppearanceChatMode: String { return L10n.tr("Localizable", "EmptyChat.Appearance.ChatMode") } + /// Colorful + internal static var emptyChatAppearanceColorful: String { return L10n.tr("Localizable", "EmptyChat.Appearance.Colorful") } + /// Dark + internal static var emptyChatAppearanceDark: String { return L10n.tr("Localizable", "EmptyChat.Appearance.Dark") } + /// You can change these parameters and many others in Settings ⟶ [Appearance](appearance). + internal static var emptyChatAppearanceDesc: String { return L10n.tr("Localizable", "EmptyChat.Appearance.Desc") } + /// Light + internal static var emptyChatAppearanceLight: String { return L10n.tr("Localizable", "EmptyChat.Appearance.Light") } + /// Minimalism + internal static var emptyChatAppearanceMin: String { return L10n.tr("Localizable", "EmptyChat.Appearance.Min") } + /// System + internal static var emptyChatAppearanceSystem: String { return L10n.tr("Localizable", "EmptyChat.Appearance.System") } + /// Next Tip + internal static var emptyChatNavigationNext: String { return L10n.tr("Localizable", "EmptyChat.Navigation.Next") } + /// Previous Tip + internal static var emptyChatNavigationPrev: String { return L10n.tr("Localizable", "EmptyChat.Navigation.Prev") } + /// All Sets + internal static var emptyChatStickersAllSets: String { return L10n.tr("Localizable", "EmptyChat.Stickers.AllSets") } + /// More trending stickers are available in\nSettings ⟶ Stickers ⟶ [Trending Stickers](trending). + internal static var emptyChatStickersDesc: String { return L10n.tr("Localizable", "EmptyChat.Stickers.Desc") } + /// My Sets + internal static var emptyChatStickersMySets: String { return L10n.tr("Localizable", "EmptyChat.Stickers.MySets") } + /// None + internal static var emptyChatStickersNone: String { return L10n.tr("Localizable", "EmptyChat.Stickers.None") } + /// Trending Stickers + internal static var emptyChatStickersTrending: String { return L10n.tr("Localizable", "EmptyChat.Stickers.Trending") } + /// Telegram uses **%@** of your storage. + internal static func emptyChatStorageUsageCacheDesc(_ p1: String) -> String { + return L10n.tr("Localizable", "EmptyChat.StorageUsage.CacheDesc", p1) + } + /// Telegram cache is empty. + internal static var emptyChatStorageUsageCacheDescEmpty: String { return L10n.tr("Localizable", "EmptyChat.StorageUsage.CacheDescEmpty") } + /// Clear Cache + internal static var emptyChatStorageUsageClear: String { return L10n.tr("Localizable", "EmptyChat.StorageUsage.Clear") } + /// Clearing... + internal static var emptyChatStorageUsageClearing: String { return L10n.tr("Localizable", "EmptyChat.StorageUsage.Clearing") } + /// Maximum Cache Size + internal static var emptyChatStorageUsageData: String { return L10n.tr("Localizable", "EmptyChat.StorageUsage.Data") } + /// More data and storage settings are available in\nSettings ⟶ [Data And Storage](storage). + internal static var emptyChatStorageUsageDesc: String { return L10n.tr("Localizable", "EmptyChat.StorageUsage.Desc") } + /// Calculating... + internal static var emptyChatStorageUsageLoading: String { return L10n.tr("Localizable", "EmptyChat.StorageUsage.Loading") } + /// 5 GB + internal static var emptyChatStorageUsageLow: String { return L10n.tr("Localizable", "EmptyChat.StorageUsage.Low") } + /// 32 GB + internal static var emptyChatStorageUsageMedium: String { return L10n.tr("Localizable", "EmptyChat.StorageUsage.Medium") } + /// No Limit + internal static var emptyChatStorageUsageNoLimit: String { return L10n.tr("Localizable", "EmptyChat.StorageUsage.NoLimit") } + /// Telegram\n%@ + internal static func emptyChatStorageUsageTooltipApp(_ p1: String) -> String { + return L10n.tr("Localizable", "EmptyChat.StorageUsage.Tooltip.App", p1) + } + /// System\n%@ + internal static func emptyChatStorageUsageTooltipSystem(_ p1: String) -> String { + return L10n.tr("Localizable", "EmptyChat.StorageUsage.Tooltip.System", p1) + } + /// • Up to %@ members + internal static func emptyGroupInfoLine1(_ p1: String) -> String { + return L10n.tr("Localizable", "EmptyGroupInfo.Line1", p1) + } + /// • Persistent chat history + internal static var emptyGroupInfoLine2: String { return L10n.tr("Localizable", "EmptyGroupInfo.Line2") } + /// • Public links such as t.me/title + internal static var emptyGroupInfoLine3: String { return L10n.tr("Localizable", "EmptyGroupInfo.Line3") } + /// • Admins with different rights + internal static var emptyGroupInfoLine4: String { return L10n.tr("Localizable", "EmptyGroupInfo.Line4") } + /// Groups can have: + internal static var emptyGroupInfoSubtitle: String { return L10n.tr("Localizable", "EmptyGroupInfo.Subtitle") } + /// You have created a group + internal static var emptyGroupInfoTitle: String { return L10n.tr("Localizable", "EmptyGroupInfo.Title") } /// Select a chat to start messaging - case emptyPeerDescription + internal static var emptyPeerDescription: String { return L10n.tr("Localizable", "EmptyPeer.Description") } /// This image and text were derived from the encryption key for this secret chat with **%@**.\n\nIf they look the same on **%@**'s device, end-to-end encryption is guaranteed. - case encryptionKeyDescription(String, String) + internal static func encryptionKeyDescription(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "EncryptionKey.Description", p1, p2) + } /// EMOJI - case entertainmentEmoji + internal static var entertainmentEmoji: String { return L10n.tr("Localizable", "Entertainment.Emoji") } /// GIFs - case entertainmentGIF + internal static var entertainmentGIF: String { return L10n.tr("Localizable", "Entertainment.GIF") } /// STICKERS - case entertainmentStickers + internal static var entertainmentStickers: String { return L10n.tr("Localizable", "Entertainment.Stickers") } /// Emoji - case entertainmentSwitchEmoji + internal static var entertainmentSwitchEmoji: String { return L10n.tr("Localizable", "Entertainment.Switch.Emoji") } /// Stickers & GIFs - case entertainmentSwitchGifAndStickers + internal static var entertainmentSwitchGifAndStickers: String { return L10n.tr("Localizable", "Entertainment.Switch.GifAndStickers") } + /// An error occured. Please try again later. + internal static var errorAnError: String { return L10n.tr("Localizable", "Error.AnError") } /// This username is already taken. - case errorUsernameAlreadyTaken + internal static var errorUsernameAlreadyTaken: String { return L10n.tr("Localizable", "Error.Username.AlreadyTaken") } /// This username is invalid. - case errorUsernameInvalid + internal static var errorUsernameInvalid: String { return L10n.tr("Localizable", "Error.Username.Invalid") } /// A username must have at least 5 characters. - case errorUsernameMinimumLength + internal static var errorUsernameMinimumLength: String { return L10n.tr("Localizable", "Error.Username.MinimumLength") } /// A username can't start with a number. - case errorUsernameNumberStart + internal static var errorUsernameNumberStart: String { return L10n.tr("Localizable", "Error.Username.NumberStart") } /// A username can't end with an underscore. - case errorUsernameUnderscopeEnd + internal static var errorUsernameUnderscopeEnd: String { return L10n.tr("Localizable", "Error.Username.UnderscopeEnd") } /// A username can't start with an underscore. - case errorUsernameUnderscopeStart - /// Banned %@ %@ - case eventLogServiceBanned(String, String) + internal static var errorUsernameUnderscopeStart: String { return L10n.tr("Localizable", "Error.Username.UnderscopeStart") } + /// Banned %1$@ %2$@ + internal static func eventLogServiceBanned1(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "EventLog.Service.Banned1", p1, p2) + } + /// changed defaults rights + internal static var eventLogServiceChangedDefaultsRights: String { return L10n.tr("Localizable", "EventLog.Service.ChangedDefaultsRights") } /// %@ changed group sticker set - case eventLogServiceChangedStickerSet(String) + internal static func eventLogServiceChangedStickerSet(_ p1: String) -> String { + return L10n.tr("Localizable", "EventLog.Service.ChangedStickerSet", p1) + } /// %@ deleted message: - case eventLogServiceDeletedMessage(String) - /// restricted %@ %@ indefinitely - case eventLogServiceDemoted(String, String) + internal static func eventLogServiceDeletedMessage(_ p1: String) -> String { + return L10n.tr("Localizable", "EventLog.Service.DeletedMessage", p1) + } + /// restricted %1$@ %2$@ indefinitely + internal static func eventLogServiceDemoted1(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "EventLog.Service.Demoted1", p1, p2) + } + /// %@ edited caption: + internal static func eventLogServiceEditedCaption(_ p1: String) -> String { + return L10n.tr("Localizable", "EventLog.Service.EditedCaption", p1) + } + /// %@ edited media: + internal static func eventLogServiceEditedMedia(_ p1: String) -> String { + return L10n.tr("Localizable", "EventLog.Service.EditedMedia", p1) + } /// %@ edited message: - case eventLogServiceEditedMessage(String) + internal static func eventLogServiceEditedMessage(_ p1: String) -> String { + return L10n.tr("Localizable", "EventLog.Service.EditedMessage", p1) + } /// Previous Description - case eventLogServicePreviousDesc + internal static var eventLogServicePreviousDesc: String { return L10n.tr("Localizable", "EventLog.Service.PreviousDesc") } /// Previous Link - case eventLogServicePreviousLink + internal static var eventLogServicePreviousLink: String { return L10n.tr("Localizable", "EventLog.Service.PreviousLink") } /// Previous Title - case eventLogServicePreviousTitle - /// promoted %@ %@: - case eventLogServicePromoted(String, String) + internal static var eventLogServicePreviousTitle: String { return L10n.tr("Localizable", "EventLog.Service.PreviousTitle") } + /// promoted %1$@ %2$@: + internal static func eventLogServicePromoted1(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "EventLog.Service.Promoted1", p1, p2) + } /// %@ removed group sticker set - case eventLogServiceRemovedStickerSet(String) + internal static func eventLogServiceRemovedStickerSet(_ p1: String) -> String { + return L10n.tr("Localizable", "EventLog.Service.RemovedStickerSet", p1) + } /// %@ unpinned message - case eventLogServiceRemovePinned(String) + internal static func eventLogServiceRemovePinned(_ p1: String) -> String { + return L10n.tr("Localizable", "EventLog.Service.RemovePinned", p1) + } /// %@ pinned message: - case eventLogServiceUpdatePinned(String) + internal static func eventLogServiceUpdatePinned(_ p1: String) -> String { + return L10n.tr("Localizable", "EventLog.Service.UpdatePinned", p1) + } + /// Add Members + internal static var eventLogServiceDemoteAddMembers: String { return L10n.tr("Localizable", "EventLog.Service.Demote.AddMembers") } + /// Change Info + internal static var eventLogServiceDemoteChangeInfo: String { return L10n.tr("Localizable", "EventLog.Service.Demote.ChangeInfo") } /// Embed Links - case eventLogServiceDemoteEmbedLinks + internal static var eventLogServiceDemoteEmbedLinks: String { return L10n.tr("Localizable", "EventLog.Service.Demote.EmbedLinks") } + /// Pin Messages + internal static var eventLogServiceDemotePinMessages: String { return L10n.tr("Localizable", "EventLog.Service.Demote.PinMessages") } + /// Post Polls + internal static var eventLogServiceDemotePostPolls: String { return L10n.tr("Localizable", "EventLog.Service.Demote.PostPolls") } + /// Send GIFs + internal static var eventLogServiceDemoteSendGifs: String { return L10n.tr("Localizable", "EventLog.Service.Demote.SendGifs") } /// Send Inline - case eventLogServiceDemoteSendInline + internal static var eventLogServiceDemoteSendInline: String { return L10n.tr("Localizable", "EventLog.Service.Demote.SendInline") } /// Send Media - case eventLogServiceDemoteSendMedia + internal static var eventLogServiceDemoteSendMedia: String { return L10n.tr("Localizable", "EventLog.Service.Demote.SendMedia") } /// Send Messages - case eventLogServiceDemoteSendMessages + internal static var eventLogServiceDemoteSendMessages: String { return L10n.tr("Localizable", "EventLog.Service.Demote.SendMessages") } /// Send Stickers - case eventLogServiceDemoteSendStickers - /// changed restrictions for %@ %@ indefinitely - case eventLogServiceDemotedChanged(String, String) - /// restricted %@ %@ until %@ - case eventLogServiceDemotedUntil(String, String, String) - /// changed restrictions for %@ %@ until %@ - case eventLogServiceDemotedChangedUntil(String, String, String) + internal static var eventLogServiceDemoteSendStickers: String { return L10n.tr("Localizable", "EventLog.Service.Demote.SendStickers") } + /// changed the restrictions for %1$@ %2$@ indefinitely + internal static func eventLogServiceDemotedChanged1(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "EventLog.Service.Demoted.Changed1", p1, p2) + } + /// restricted %1$@ %2$@ until %3$@ + internal static func eventLogServiceDemotedUntil1(_ p1: String, _ p2: String, _ p3: String) -> String { + return L10n.tr("Localizable", "EventLog.Service.Demoted.Until1", p1, p2, p3) + } + /// changed restrictions for %1$@ %2$@ until %3$@ + internal static func eventLogServiceDemotedChangedUntil1(_ p1: String, _ p2: String, _ p3: String) -> String { + return L10n.tr("Localizable", "EventLog.Service.Demoted.Changed.Until1", p1, p2, p3) + } /// Add New Admins - case eventLogServicePromoteAddNewAdmins + internal static var eventLogServicePromoteAddNewAdmins: String { return L10n.tr("Localizable", "EventLog.Service.Promote.AddNewAdmins") } /// Add Users - case eventLogServicePromoteAddUsers + internal static var eventLogServicePromoteAddUsers: String { return L10n.tr("Localizable", "EventLog.Service.Promote.AddUsers") } /// Ban Users - case eventLogServicePromoteBanUsers + internal static var eventLogServicePromoteBanUsers: String { return L10n.tr("Localizable", "EventLog.Service.Promote.BanUsers") } /// Change Info - case eventLogServicePromoteChangeInfo + internal static var eventLogServicePromoteChangeInfo: String { return L10n.tr("Localizable", "EventLog.Service.Promote.ChangeInfo") } /// Delete Messages - case eventLogServicePromoteDeleteMessages + internal static var eventLogServicePromoteDeleteMessages: String { return L10n.tr("Localizable", "EventLog.Service.Promote.DeleteMessages") } /// Edit Messages - case eventLogServicePromoteEditMessages + internal static var eventLogServicePromoteEditMessages: String { return L10n.tr("Localizable", "EventLog.Service.Promote.EditMessages") } /// Invite Users Via Link - case eventLogServicePromoteInviteViaLink + internal static var eventLogServicePromoteInviteViaLink: String { return L10n.tr("Localizable", "EventLog.Service.Promote.InviteViaLink") } /// Pin Messages - case eventLogServicePromotePinMessages + internal static var eventLogServicePromotePinMessages: String { return L10n.tr("Localizable", "EventLog.Service.Promote.PinMessages") } /// Post Messages - case eventLogServicePromotePostMessages - /// changed privileges for %@ %@: - case eventLogServicePromotedChanged(String, String) + internal static var eventLogServicePromotePostMessages: String { return L10n.tr("Localizable", "EventLog.Service.Promote.PostMessages") } + /// Remain Anonymous + internal static var eventLogServicePromoteRemainAnonymous: String { return L10n.tr("Localizable", "EventLog.Service.Promote.RemainAnonymous") } + /// changed privileges for %1$@ %2$@: + internal static func eventLogServicePromotedChanged1(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "EventLog.Service.Promoted.Changed1", p1, p2) + } + /// Done + internal static var exportedInvitationDone: String { return L10n.tr("Localizable", "ExportedInvitation.Done") } + /// LINK CREATED BY + internal static var exportedInvitationLinkCreatedBy: String { return L10n.tr("Localizable", "ExportedInvitation.LinkCreatedBy") } + /// %d + internal static func exportedInvitationPeopleJoinedCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "ExportedInvitation.PeopleJoined_countable", p1) + } + /// %d PEOPLE JOINED + internal static func exportedInvitationPeopleJoinedFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "ExportedInvitation.PeopleJoined_few", p1) + } + /// %d PEOPLE JOINED + internal static func exportedInvitationPeopleJoinedMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "ExportedInvitation.PeopleJoined_many", p1) + } + /// %d PEOPLE JOINED + internal static func exportedInvitationPeopleJoinedOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "ExportedInvitation.PeopleJoined_one", p1) + } + /// %d PEOPLE JOINED + internal static func exportedInvitationPeopleJoinedOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "ExportedInvitation.PeopleJoined_other", p1) + } + /// %d PEOPLE JOINED + internal static func exportedInvitationPeopleJoinedTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "ExportedInvitation.PeopleJoined_two", p1) + } + /// + internal static var exportedInvitationPeopleJoinedZero: String { return L10n.tr("Localizable", "ExportedInvitation.PeopleJoined_zero") } + /// Invite Link + internal static var exportedInvitationTitle: String { return L10n.tr("Localizable", "ExportedInvitation.Title") } + /// Copy + internal static var exportedInvitationContextCopy: String { return L10n.tr("Localizable", "ExportedInvitation.Context.Copy") } + /// Open Profile + internal static var exportedInvitationContextOpenProfile: String { return L10n.tr("Localizable", "ExportedInvitation.Context.OpenProfile") } + /// expired + internal static var exportedInvitationStatusExpired: String { return L10n.tr("Localizable", "ExportedInvitation.Status.Expired") } + /// expires in %@ + internal static func exportedInvitationStatusExpiresIn(_ p1: String) -> String { + return L10n.tr("Localizable", "ExportedInvitation.Status.ExpiresIn", p1) + } + /// revoked + internal static var exportedInvitationStatusRevoked: String { return L10n.tr("Localizable", "ExportedInvitation.Status.Revoked") } /// Disable Dark Mode - case fastSettingsDisableDarkMode + internal static var fastSettingsDisableDarkMode: String { return L10n.tr("Localizable", "FastSettings.DisableDarkMode") } /// Enable Dark Mode - case fastSettingsEnableDarkMode + internal static var fastSettingsEnableDarkMode: String { return L10n.tr("Localizable", "FastSettings.EnableDarkMode") } /// Lock Telegram - case fastSettingsLockTelegram + internal static var fastSettingsLockTelegram: String { return L10n.tr("Localizable", "FastSettings.LockTelegram") } /// Mute For 2 Hours - case fastSettingsMute2Hours + internal static var fastSettingsMute2Hours: String { return L10n.tr("Localizable", "FastSettings.Mute2Hours") } /// Set a Passcode - case fastSettingsSetPasscode + internal static var fastSettingsSetPasscode: String { return L10n.tr("Localizable", "FastSettings.SetPasscode") } /// Unmute - case fastSettingsUnmute - /// Substitutions - case feMD8WVrTitle + internal static var fastSettingsUnmute: String { return L10n.tr("Localizable", "FastSettings.Unmute") } + /// %@ B + internal static func fileSizeB(_ p1: String) -> String { + return L10n.tr("Localizable", "FileSize.B", p1) + } + /// %@ GB + internal static func fileSizeGB(_ p1: String) -> String { + return L10n.tr("Localizable", "FileSize.GB", p1) + } + /// %@ KB + internal static func fileSizeKB(_ p1: String) -> String { + return L10n.tr("Localizable", "FileSize.KB", p1) + } + /// %@ MB + internal static func fileSizeMB(_ p1: String) -> String { + return L10n.tr("Localizable", "FileSize.MB", p1) + } + /// forward messages here for quick access + internal static var forwardToSavedMessages: String { return L10n.tr("Localizable", "Forward.ToSavedMessages") } /// %d %@ - case forwardModalActionDescriptionCountable(Int, String) + internal static func forwardModalActionDescriptionCountable(_ p1: Int, _ p2: String) -> String { + return L10n.tr("Localizable", "ForwardModalAction.description_countable", p1, p2) + } /// Select a user or chat to forward messages from %@ - case forwardModalActionDescriptionFew(String) + internal static func forwardModalActionDescriptionFew(_ p1: String) -> String { + return L10n.tr("Localizable", "ForwardModalAction.description_few", p1) + } /// Select a user or chat to forward messages from %@ - case forwardModalActionDescriptionMany(String) + internal static func forwardModalActionDescriptionMany(_ p1: String) -> String { + return L10n.tr("Localizable", "ForwardModalAction.description_many", p1) + } /// Select a user or chat to forward message from %@ - case forwardModalActionDescriptionOne(String) + internal static func forwardModalActionDescriptionOne(_ p1: String) -> String { + return L10n.tr("Localizable", "ForwardModalAction.description_one", p1) + } /// Select a user or chat to forward messages from %@ - case forwardModalActionDescriptionOther(String) + internal static func forwardModalActionDescriptionOther(_ p1: String) -> String { + return L10n.tr("Localizable", "ForwardModalAction.description_other", p1) + } /// Select a user or chat to forward messages from %@ - case forwardModalActionDescriptionTwo(String) + internal static func forwardModalActionDescriptionTwo(_ p1: String) -> String { + return L10n.tr("Localizable", "ForwardModalAction.description_two", p1) + } /// Select a user or chat to forward messages from %@ - case forwardModalActionDescriptionZero(String) + internal static func forwardModalActionDescriptionZero(_ p1: String) -> String { + return L10n.tr("Localizable", "ForwardModalAction.description_zero", p1) + } /// %d - case forwardModalActionTitleCountable(Int) + internal static func forwardModalActionTitleCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "ForwardModalAction.Title_countable", p1) + } /// Forwarding messages - case forwardModalActionTitleFew + internal static var forwardModalActionTitleFew: String { return L10n.tr("Localizable", "ForwardModalAction.Title_few") } /// Forwarding messages - case forwardModalActionTitleMany + internal static var forwardModalActionTitleMany: String { return L10n.tr("Localizable", "ForwardModalAction.Title_many") } /// Forwarding message - case forwardModalActionTitleOne + internal static var forwardModalActionTitleOne: String { return L10n.tr("Localizable", "ForwardModalAction.Title_one") } /// Forwarding messages - case forwardModalActionTitleOther + internal static var forwardModalActionTitleOther: String { return L10n.tr("Localizable", "ForwardModalAction.Title_other") } /// Forwarding messages - case forwardModalActionTitleTwo + internal static var forwardModalActionTitleTwo: String { return L10n.tr("Localizable", "ForwardModalAction.Title_two") } /// Forwarding messages - case forwardModalActionTitleZero + internal static var forwardModalActionTitleZero: String { return L10n.tr("Localizable", "ForwardModalAction.Title_zero") } /// Delete - case galleryContextDeletePhoto - /// %d of %d - case galleryCounter(Int, Int) + internal static var galleryContextDeletePhoto: String { return L10n.tr("Localizable", "Gallery.ContextDeletePhoto") } + /// Save GIF + internal static var gallerySaveGif: String { return L10n.tr("Localizable", "Gallery.SaveGif") } /// Copy to Clipboard - case galleryContextCopyToClipboard + internal static var galleryContextCopyToClipboard: String { return L10n.tr("Localizable", "Gallery.Context.CopyToClipboard") } + /// Set As Main Photo + internal static var galleryContextMainPhoto: String { return L10n.tr("Localizable", "Gallery.Context.MainPhoto") } /// Save As... - case galleryContextSaveAs + internal static var galleryContextSaveAs: String { return L10n.tr("Localizable", "Gallery.Context.SaveAs") } + /// Shared Media + internal static var galleryContextShowGallery: String { return L10n.tr("Localizable", "Gallery.Context.ShowGallery") } /// Show Message - case galleryContextShowMessage + internal static var galleryContextShowMessage: String { return L10n.tr("Localizable", "Gallery.Context.ShowMessage") } + /// %d + internal static func galleryContextShareAllItemsCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Gallery.Context.Share.AllItems_countable", p1) + } + /// All %d Items + internal static func galleryContextShareAllItemsFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Gallery.Context.Share.AllItems_few", p1) + } + /// All %d Items + internal static func galleryContextShareAllItemsMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Gallery.Context.Share.AllItems_many", p1) + } + /// All %d Items + internal static func galleryContextShareAllItemsOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Gallery.Context.Share.AllItems_one", p1) + } + /// All %d Items + internal static func galleryContextShareAllItemsOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Gallery.Context.Share.AllItems_other", p1) + } + /// All %d Items + internal static func galleryContextShareAllItemsTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Gallery.Context.Share.AllItems_two", p1) + } + /// All %d Items + internal static func galleryContextShareAllItemsZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Gallery.Context.Share.AllItems_zero", p1) + } + /// %d + internal static func galleryContextShareAllPhotosCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Gallery.Context.Share.AllPhotos_countable", p1) + } + /// All %d Photos + internal static func galleryContextShareAllPhotosFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Gallery.Context.Share.AllPhotos_few", p1) + } + /// All %d Photos + internal static func galleryContextShareAllPhotosMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Gallery.Context.Share.AllPhotos_many", p1) + } + /// All %d Photo + internal static func galleryContextShareAllPhotosOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Gallery.Context.Share.AllPhotos_one", p1) + } + /// All %d Photos + internal static func galleryContextShareAllPhotosOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Gallery.Context.Share.AllPhotos_other", p1) + } + /// All %d Photos + internal static func galleryContextShareAllPhotosTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Gallery.Context.Share.AllPhotos_two", p1) + } + /// All %d Photos + internal static func galleryContextShareAllPhotosZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Gallery.Context.Share.AllPhotos_zero", p1) + } + /// %d + internal static func galleryContextShareAllVideosCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Gallery.Context.Share.AllVideos_countable", p1) + } + /// All %d Videos + internal static func galleryContextShareAllVideosFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Gallery.Context.Share.AllVideos_few", p1) + } + /// All %d Videos + internal static func galleryContextShareAllVideosMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Gallery.Context.Share.AllVideos_many", p1) + } + /// All %d Videos + internal static func galleryContextShareAllVideosOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Gallery.Context.Share.AllVideos_one", p1) + } + /// All %d Videos + internal static func galleryContextShareAllVideosOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Gallery.Context.Share.AllVideos_other", p1) + } + /// All %d Videos + internal static func galleryContextShareAllVideosTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Gallery.Context.Share.AllVideos_two", p1) + } + /// All %d Videos + internal static func galleryContextShareAllVideosZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Gallery.Context.Share.AllVideos_zero", p1) + } + /// This File + internal static var galleryContextShareThisFile: String { return L10n.tr("Localizable", "Gallery.Context.Share.ThisFile") } + /// This Photo + internal static var galleryContextShareThisPhoto: String { return L10n.tr("Localizable", "Gallery.Context.Share.ThisPhoto") } + /// This Video + internal static var galleryContextShareThisVideo: String { return L10n.tr("Localizable", "Gallery.Context.Share.ThisVideo") } + /// Please wait for the photo to be fully downloaded. + internal static var galleryWaitDownloadPhoto: String { return L10n.tr("Localizable", "Gallery.WaitDownload.Photo") } + /// Please wait for the video to be fully downloaded. + internal static var galleryWaitDownloadVideo: String { return L10n.tr("Localizable", "Gallery.WaitDownload.Video") } + /// GIF saved to\n[Downloads]() + internal static var galleryViewFastSaveGif1: String { return L10n.tr("Localizable", "GalleryView.FastSave.Gif1") } + /// Image saved to\n[Downloads]() + internal static var galleryViewFastSaveImage1: String { return L10n.tr("Localizable", "GalleryView.FastSave.Image1") } + /// Video saved to\n[Downloads]() + internal static var galleryViewFastSaveVideo1: String { return L10n.tr("Localizable", "GalleryView.FastSave.Video1") } + /// Accent Color + internal static var generalSettingsAccentColor: String { return L10n.tr("Localizable", "GeneralSettings.AccentColor") } + /// Accept Secret Chats + internal static var generalSettingsAcceptSecretChats: String { return L10n.tr("Localizable", "GeneralSettings.AcceptSecretChats") } + /// ADVANCED + internal static var generalSettingsAdvancedHeader: String { return L10n.tr("Localizable", "GeneralSettings.AdvancedHeader") } /// APPEARANCE SETTINGS - case generalSettingsAppearanceSettings + internal static var generalSettingsAppearanceSettings: String { return L10n.tr("Localizable", "GeneralSettings.AppearanceSettings") } + /// Autoplay GIFs + internal static var generalSettingsAutoplayGifs: String { return L10n.tr("Localizable", "GeneralSettings.AutoplayGifs") } + /// Big Emoji + internal static var generalSettingsBigEmoji: String { return L10n.tr("Localizable", "GeneralSettings.BigEmoji") } + /// Chat Background + internal static var generalSettingsChatBackground: String { return L10n.tr("Localizable", "GeneralSettings.ChatBackground") } + /// Copy Text Formatting + internal static var generalSettingsCopyRTF: String { return L10n.tr("Localizable", "GeneralSettings.CopyRTF") } /// Dark Mode - case generalSettingsDarkMode - /// Automatic replace emojis - case generalSettingsEmojiReplacements + internal static var generalSettingsDarkMode: String { return L10n.tr("Localizable", "GeneralSettings.DarkMode") } + /// EMOJI & STICKERS + internal static var generalSettingsEmojiAndStickers: String { return L10n.tr("Localizable", "GeneralSettings.EmojiAndStickers") } + /// Suggest Emoji + internal static var generalSettingsEmojiPrediction: String { return L10n.tr("Localizable", "GeneralSettings.EmojiPrediction") } + /// Automatically replace emojis + internal static var generalSettingsEmojiReplacements: String { return L10n.tr("Localizable", "GeneralSettings.EmojiReplacements") } /// Sidebar - case generalSettingsEnableSidebar + internal static var generalSettingsEnableSidebar: String { return L10n.tr("Localizable", "GeneralSettings.EnableSidebar") } /// FORCE TOUCH ACTION - case generalSettingsForceTouchHeader + internal static var generalSettingsForceTouchHeader: String { return L10n.tr("Localizable", "GeneralSettings.ForceTouchHeader") } /// GENERAL SETTINGS - case generalSettingsGeneralSettings + internal static var generalSettingsGeneralSettings: String { return L10n.tr("Localizable", "GeneralSettings.GeneralSettings") } /// In-App Sounds - case generalSettingsInAppSounds + internal static var generalSettingsInAppSounds: String { return L10n.tr("Localizable", "GeneralSettings.InAppSounds") } /// INPUT SETTINGS - case generalSettingsInputSettings - /// Large Message Font - case generalSettingsLargeFonts + internal static var generalSettingsInputSettings: String { return L10n.tr("Localizable", "GeneralSettings.InputSettings") } + /// INSTANT VIEW + internal static var generalSettingsInstantViewHeader: String { return L10n.tr("Localizable", "GeneralSettings.InstantViewHeader") } + /// INTERFACE + internal static var generalSettingsInterfaceHeader: String { return L10n.tr("Localizable", "GeneralSettings.InterfaceHeader") } /// Handle media keys for in-app player - case generalSettingsMediaKeysForInAppPlayer + internal static var generalSettingsMediaKeysForInAppPlayer: String { return L10n.tr("Localizable", "GeneralSettings.MediaKeysForInAppPlayer") } + /// Reopen Last Chat On Launch + internal static var generalSettingsOpenLatestChatOnLaunch: String { return L10n.tr("Localizable", "GeneralSettings.OpenLatestChatOnLaunch") } /// Use ⌘ + Enter to send - case generalSettingsSendByCmdEnter + internal static var generalSettingsSendByCmdEnter: String { return L10n.tr("Localizable", "GeneralSettings.SendByCmdEnter") } /// Use Enter to send - case generalSettingsSendByEnter + internal static var generalSettingsSendByEnter: String { return L10n.tr("Localizable", "GeneralSettings.SendByEnter") } + /// Keyboard Shortcuts + internal static var generalSettingsShortcuts: String { return L10n.tr("Localizable", "GeneralSettings.Shortcuts") } + /// SHORTCUTS + internal static var generalSettingsShortcutsHeader: String { return L10n.tr("Localizable", "GeneralSettings.ShortcutsHeader") } + /// Suggest Articles in Search + internal static var generalSettingsShowArticlesInSearch: String { return L10n.tr("Localizable", "GeneralSettings.ShowArticlesInSearch") } + /// Show Calls Tab + internal static var generalSettingsShowCallsTab: String { return L10n.tr("Localizable", "GeneralSettings.ShowCallsTab") } + /// Menu Bar Item + internal static var generalSettingsStatusBarItem: String { return L10n.tr("Localizable", "GeneralSettings.StatusBarItem") } + /// CALL SETTINGS + internal static var generalSettingsCallSettingsHeader: String { return L10n.tr("Localizable", "GeneralSettings.CallSettings.Header") } + /// Call Settings + internal static var generalSettingsCallSettingsText: String { return L10n.tr("Localizable", "GeneralSettings.CallSettings.Text") } /// A color scheme for nighttime and dark desktops - case generalSettingsDarkModeDescription + internal static var generalSettingsDarkModeDescription: String { return L10n.tr("Localizable", "GeneralSettings.DarkMode.Description") } + /// Disable + internal static var generalSettingsEmojiPredictionDisable: String { return L10n.tr("Localizable", "GeneralSettings.EmojiPrediction.Disable") } + /// Disable emoji suggestions? You can re-enable them in Settings at any time. + internal static var generalSettingsEmojiPredictionDisableText: String { return L10n.tr("Localizable", "GeneralSettings.EmojiPrediction.DisableText") } /// Use large font for messages - case generalSettingsFontDescription + internal static var generalSettingsFontDescription: String { return L10n.tr("Localizable", "GeneralSettings.Font.Description") } /// Edit Message - case generalSettingsForceTouchEdit + internal static var generalSettingsForceTouchEdit: String { return L10n.tr("Localizable", "GeneralSettings.ForceTouch.Edit") } /// Forward Message - case generalSettingsForceTouchForward + internal static var generalSettingsForceTouchForward: String { return L10n.tr("Localizable", "GeneralSettings.ForceTouch.Forward") } + /// Preview Media + internal static var generalSettingsForceTouchPreviewMedia: String { return L10n.tr("Localizable", "GeneralSettings.ForceTouch.PreviewMedia") } /// Reply to Message - case generalSettingsForceTouchReply + internal static var generalSettingsForceTouchReply: String { return L10n.tr("Localizable", "GeneralSettings.ForceTouch.Reply") } + /// Scroll With Spacebar + internal static var generalSettingsInstantViewScrollBySpace: String { return L10n.tr("Localizable", "GeneralSettings.InstantView.ScrollBySpace") } + /// More Info + internal static var genericErrorMoreInfo: String { return L10n.tr("Localizable", "Generic.ErrorMoreInfo") } + /// REACTIONS + internal static var gifsPaneReactions: String { return L10n.tr("Localizable", "GifsPane.Reactions") } + /// TRENDING GIFS + internal static var gifsPaneTrending: String { return L10n.tr("Localizable", "GifsPane.Trending") } + /// Total + internal static var graphTotal: String { return L10n.tr("Localizable", "Graph.Total") } + /// Zoom Out + internal static var graphZoomOut: String { return L10n.tr("Localizable", "Graph.ZoomOut") } /// New Group - case groupCreateGroup + internal static var groupCreateGroup: String { return L10n.tr("Localizable", "Group.CreateGroup") } + /// Sorry, you can't add this user to group. + internal static var groupErrorAddBlocked: String { return L10n.tr("Localizable", "Group.ErrorAddBlocked") } /// New Group - case groupNewGroup - /// Sorry, this group does not seem to exist. - case groupUnavailable + internal static var groupNewGroup: String { return L10n.tr("Localizable", "Group.NewGroup") } + /// Sorry, this group doesn't seem to exist. + internal static var groupUnavailable: String { return L10n.tr("Localizable", "Group.Unavailable") } + /// Sorry, this group is full. You cannot add any more members here. + internal static var groupUsersTooMuchError: String { return L10n.tr("Localizable", "Group.UsersTooMuchError") } /// Change Group Info - case groupEditAdminPermissionChangeInfo + internal static var groupEditAdminPermissionChangeInfo: String { return L10n.tr("Localizable", "Group.EditAdmin.Permission.ChangeInfo") } /// **No events here yet**\n\nThere were no service actions taken by the group's members and admins for the last 48 hours. - case groupEventLogEmptyText - /// %@ removed group description: - case groupEventLogServiceAboutRemoved(String) - /// %@ edited group description: - case groupEventLogServiceAboutUpdated(String) + internal static var groupEventLogEmptyText: String { return L10n.tr("Localizable", "Group.EventLog.EmptyText") } + /// %@ removed the group's description: + internal static func groupEventLogServiceAboutRemoved(_ p1: String) -> String { + return L10n.tr("Localizable", "Group.EventLog.Service.AboutRemoved", p1) + } + /// %@ edited the group's description: + internal static func groupEventLogServiceAboutUpdated(_ p1: String) -> String { + return L10n.tr("Localizable", "Group.EventLog.Service.AboutUpdated", p1) + } /// %@ disabled group invites - case groupEventLogServiceDisableInvites(String) + internal static func groupEventLogServiceDisableInvites(_ p1: String) -> String { + return L10n.tr("Localizable", "Group.EventLog.Service.DisableInvites", p1) + } /// %@ enabled group invites - case groupEventLogServiceEnableInvites(String) - /// %@ removed group link: - case groupEventLogServiceLinkRemoved(String) - /// %@ edited group link: - case groupEventLogServiceLinkUpdated(String) + internal static func groupEventLogServiceEnableInvites(_ p1: String) -> String { + return L10n.tr("Localizable", "Group.EventLog.Service.EnableInvites", p1) + } + /// %@ removed the group's link: + internal static func groupEventLogServiceLinkRemoved(_ p1: String) -> String { + return L10n.tr("Localizable", "Group.EventLog.Service.LinkRemoved", p1) + } + /// %@ edited the group's link: + internal static func groupEventLogServiceLinkUpdated(_ p1: String) -> String { + return L10n.tr("Localizable", "Group.EventLog.Service.LinkUpdated", p1) + } /// %@ removed group photo - case groupEventLogServicePhotoRemoved(String) - /// %@ updated group photo - case groupEventLogServicePhotoUpdated(String) - /// %@ edited group title: - case groupEventLogServiceTitleUpdated(String) + internal static func groupEventLogServicePhotoRemoved(_ p1: String) -> String { + return L10n.tr("Localizable", "Group.EventLog.Service.PhotoRemoved", p1) + } + /// %@ updated the group's photo + internal static func groupEventLogServicePhotoUpdated(_ p1: String) -> String { + return L10n.tr("Localizable", "Group.EventLog.Service.PhotoUpdated", p1) + } + /// %@ edited the group's title: + internal static func groupEventLogServiceTitleUpdated(_ p1: String) -> String { + return L10n.tr("Localizable", "Group.EventLog.Service.TitleUpdated", p1) + } /// %@ joined the group - case groupEventLogServiceUpdateJoin(String) + internal static func groupEventLogServiceUpdateJoin(_ p1: String) -> String { + return L10n.tr("Localizable", "Group.EventLog.Service.UpdateJoin", p1) + } /// %@ left the group - case groupEventLogServiceUpdateLeft(String) + internal static func groupEventLogServiceUpdateLeft(_ p1: String) -> String { + return L10n.tr("Localizable", "Group.EventLog.Service.UpdateLeft", p1) + } + /// Sorry, the target user has too many location-based groups already. Please ask them to delete or transfer one of their existing ones first. + internal static var groupOwnershipTransferErrorLocatedGroupsTooMuch: String { return L10n.tr("Localizable", "Group.OwnershipTransfer.ErrorLocatedGroupsTooMuch") } + /// Sorry, this group has too many admins and the new owner can't be added. Please remove one of the existing admins first. + internal static var groupTransferOwnerErrorAdminsTooMuch: String { return L10n.tr("Localizable", "Group.TransferOwner.ErrorAdminsTooMuch") } + /// Sorry, this user is not a member of this group and their privacy settings prevent you from adding them manually. + internal static var groupTransferOwnerErrorPrivacyRestricted: String { return L10n.tr("Localizable", "Group.TransferOwner.ErrorPrivacyRestricted") } /// All Members Are Admins - case groupAdminsAllMembersAdmins - /// Only admins can add and remove members, edit name and photo of this group. - case groupAdminsDescAdminInvites - /// Group members can add new members, edit name and photo of this group. - case groupAdminsDescAllInvites + internal static var groupAdminsAllMembersAdmins: String { return L10n.tr("Localizable", "GroupAdmins.AllMembersAdmins") } + /// Only admins can add and remove members, and can edit the group's name and photo. + internal static var groupAdminsDescAdminInvites: String { return L10n.tr("Localizable", "GroupAdmins.Desc.AdminInvites") } + /// Group members can add new members, and can edit the name or photo of the group. + internal static var groupAdminsDescAllInvites: String { return L10n.tr("Localizable", "GroupAdmins.Desc.AllInvites") } + /// 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). + internal static var groupInfoAddUserLeftError: String { return L10n.tr("Localizable", "GroupInfo.AddUserLeftError") } + /// Administrators + internal static var groupInfoAdministrators: String { return L10n.tr("Localizable", "GroupInfo.Administrators") } + /// ⚠️ Warning: Many users reported that this group impersonates a famous person or organization. + internal static var groupInfoFakeWarning: String { return L10n.tr("Localizable", "GroupInfo.FakeWarning") } + /// ⚠️ Warning: Many users reported this group as a scam. Please be careful, especially if it asks you for money. + internal static var groupInfoScamWarning: String { return L10n.tr("Localizable", "GroupInfo.ScamWarning") } + /// Administrators + internal static var groupInfoAdministratorsTitle: String { return L10n.tr("Localizable", "GroupInfo.Administrators.Title") } + /// Add Exception + internal static var groupInfoPermissionsAddException: String { return L10n.tr("Localizable", "GroupInfo.Permissions.AddException") } + /// Convert to Broadcast Group + internal static var groupInfoPermissionsBroadcastConvert: String { return L10n.tr("Localizable", "GroupInfo.Permissions.BroadcastConvert") } + /// Broadcast groups can have over %@ members, but only admins can send messages in them. + internal static func groupInfoPermissionsBroadcastConvertInfo(_ p1: String) -> String { + return L10n.tr("Localizable", "GroupInfo.Permissions.BroadcastConvertInfo", p1) + } + /// Broadcast Group + internal static var groupInfoPermissionsBroadcastTitle: String { return L10n.tr("Localizable", "GroupInfo.Permissions.BroadcastTitle") } + /// EXCEPTIONS + internal static var groupInfoPermissionsExceptions: String { return L10n.tr("Localizable", "GroupInfo.Permissions.Exceptions") } + /// Removed Users + internal static var groupInfoPermissionsRemoved: String { return L10n.tr("Localizable", "GroupInfo.Permissions.Removed") } + /// Search Exceptions + internal static var groupInfoPermissionsSearchPlaceholder: String { return L10n.tr("Localizable", "GroupInfo.Permissions.SearchPlaceholder") } + /// WHAT CAN MEMBERS OF THIS GROUP DO? + internal static var groupInfoPermissionsSectionTitle: String { return L10n.tr("Localizable", "GroupInfo.Permissions.SectionTitle") } /// Anyone who has Telegram installed will be able to join your channel by following this link - case groupInvationChannelDescription + internal static var groupInvationChannelDescription: String { return L10n.tr("Localizable", "GroupInvation.ChannelDescription") } /// Copy Link - case groupInvationCopyLink - /// Anyone who has Telegram installed will be able to join your group by following this link - case groupInvationGroupDescription + internal static var groupInvationCopyLink: String { return L10n.tr("Localizable", "GroupInvation.CopyLink") } + /// Anyone who has Telegram installed will be able to join your group by opening this link. + internal static var groupInvationGroupDescription: String { return L10n.tr("Localizable", "GroupInvation.GroupDescription") } /// Revoke - case groupInvationRevoke + internal static var groupInvationRevoke: String { return L10n.tr("Localizable", "GroupInvation.Revoke") } /// Share Link - case groupInvationShare - /// No groups in common - case groupsInCommonEmpty + internal static var groupInvationShare: String { return L10n.tr("Localizable", "GroupInvation.Share") } + /// Exception added by %@ %@ + internal static func groupPermissionAddedInfo(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "GroupPermission.AddedInfo", p1, p2) + } + /// Exception Added + internal static var groupPermissionAddSuccess: String { return L10n.tr("Localizable", "GroupPermission.AddSuccess") } + /// Apply + internal static var groupPermissionApplyAlertAction: String { return L10n.tr("Localizable", "GroupPermission.ApplyAlertAction") } + /// You have changed this user's rights in %@.\nApply Changes? + internal static func groupPermissionApplyAlertText(_ p1: String) -> String { + return L10n.tr("Localizable", "GroupPermission.ApplyAlertText", p1) + } + /// Delete Exception + internal static var groupPermissionDelete: String { return L10n.tr("Localizable", "GroupPermission.Delete") } + /// Duration + internal static var groupPermissionDuration: String { return L10n.tr("Localizable", "GroupPermission.Duration") } + /// New Exception + internal static var groupPermissionNewTitle: String { return L10n.tr("Localizable", "GroupPermission.NewTitle") } + /// no add + internal static var groupPermissionNoAddMembers: String { return L10n.tr("Localizable", "GroupPermission.NoAddMembers") } + /// no info + internal static var groupPermissionNoChangeInfo: String { return L10n.tr("Localizable", "GroupPermission.NoChangeInfo") } + /// no pin + internal static var groupPermissionNoPinMessages: String { return L10n.tr("Localizable", "GroupPermission.NoPinMessages") } + /// no GIFs + internal static var groupPermissionNoSendGifs: String { return L10n.tr("Localizable", "GroupPermission.NoSendGifs") } + /// no links + internal static var groupPermissionNoSendLinks: String { return L10n.tr("Localizable", "GroupPermission.NoSendLinks") } + /// no media + internal static var groupPermissionNoSendMedia: String { return L10n.tr("Localizable", "GroupPermission.NoSendMedia") } + /// no messages + internal static var groupPermissionNoSendMessages: String { return L10n.tr("Localizable", "GroupPermission.NoSendMessages") } + /// no polls + internal static var groupPermissionNoSendPolls: String { return L10n.tr("Localizable", "GroupPermission.NoSendPolls") } + /// This permission is not available in public groups. + internal static var groupPermissionNotAvailableInPublicGroups: String { return L10n.tr("Localizable", "GroupPermission.NotAvailableInPublicGroups") } + /// WHAT CAN THIS MEMBER DO? + internal static var groupPermissionSectionTitle: String { return L10n.tr("Localizable", "GroupPermission.SectionTitle") } + /// Exception + internal static var groupPermissionTitle: String { return L10n.tr("Localizable", "GroupPermission.Title") } + /// Group Statistics + internal static var groupStatsTitle: String { return L10n.tr("Localizable", "GroupStats.Title") } /// CHOOSE FROM YOUR STICKERS - case groupStickersChooseHeader - /// You can create your own custom sticker set using @stickers bot. - case groupStickersCreateDescription - /// Try again or choose from list below - case groupStickersEmptyDesc + internal static var groupStickersChooseHeader: String { return L10n.tr("Localizable", "GroupStickers.ChooseHeader") } + /// You can create your own custom sticker set using the @stickers bot. + internal static var groupStickersCreateDescription: String { return L10n.tr("Localizable", "GroupStickers.CreateDescription") } + /// Try again or choose from the list below + internal static var groupStickersEmptyDesc: String { return L10n.tr("Localizable", "GroupStickers.EmptyDesc") } /// No such sticker set found - case groupStickersEmptyHeader - /// Paste - case gvau4SdLTitle + internal static var groupStickersEmptyHeader: String { return L10n.tr("Localizable", "GroupStickers.EmptyHeader") } + /// No groups in common + internal static var groupsInCommonEmpty: String { return L10n.tr("Localizable", "GroupsInCommon.Empty") } /// View - case h8h7bM4vTitle - /// Show Spelling and Grammar - case hFoCyZxITitle + internal static var h8h7bM4vTitle: String { return L10n.tr("Localizable", "H8h-7b-M4v.title") } /// Text Replacement - case hfqgknfaTitle - /// Smart Quotes - case hQb2vFYvTitle + internal static var hfqgknfaTitle: String { return L10n.tr("Localizable", "HFQ-gK-NFA.title") } + /// Show Spelling and Grammar + internal static var hFoCyZxITitle: String { return L10n.tr("Localizable", "HFo-cy-zxI.title") } /// View - case hyVFhRgOTitle - /// Check Document Now - case hz2CUCR7Title - /// open %@? - case inAppLinksConfirmOpenExternal(String) + internal static var hyVFhRgOTitle: String { return L10n.tr("Localizable", "HyV-fh-RgO.title") } + /// Join + internal static var ivChannelJoin: String { return L10n.tr("Localizable", "IV.Channel.Join") } + /// Do you want to open "%@"? + internal static func inAppLinksConfirmOpenExternalNew(_ p1: String) -> String { + return L10n.tr("Localizable", "InAppLinks.Confirm.OpenExternalNew", p1) + } + /// Open Link + internal static var inAppLinksConfirmOpenExternalHeader: String { return L10n.tr("Localizable", "InAppLinks.Confirm.OpenExternal.Header") } + /// Open + internal static var inAppLinksConfirmOpenExternalOK: String { return L10n.tr("Localizable", "InAppLinks.Confirm.OpenExternal.OK") } + /// Too many groups and channels + internal static var inactiveChannelsBlockHeader: String { return L10n.tr("Localizable", "InactiveChannels.BlockHeader") } + /// LEAST ACTIVE + internal static var inactiveChannelsHeader: String { return L10n.tr("Localizable", "InactiveChannels.Header") } + /// %d + internal static func inactiveChannelsInactiveMonthCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "InactiveChannels.InactiveMonth_countable", p1) + } + /// inactive %d months + internal static func inactiveChannelsInactiveMonthFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "InactiveChannels.InactiveMonth_few", p1) + } + /// inactive %d months + internal static func inactiveChannelsInactiveMonthMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "InactiveChannels.InactiveMonth_many", p1) + } + /// inactive %d month + internal static func inactiveChannelsInactiveMonthOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "InactiveChannels.InactiveMonth_one", p1) + } + /// inactive %d months + internal static func inactiveChannelsInactiveMonthOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "InactiveChannels.InactiveMonth_other", p1) + } + /// inactive %d months + internal static func inactiveChannelsInactiveMonthTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "InactiveChannels.InactiveMonth_two", p1) + } + /// inactive %d month + internal static func inactiveChannelsInactiveMonthZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "InactiveChannels.InactiveMonth_zero", p1) + } + /// %d + internal static func inactiveChannelsInactiveWeekCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "InactiveChannels.InactiveWeek_countable", p1) + } + /// inactive %d weeks + internal static func inactiveChannelsInactiveWeekFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "InactiveChannels.InactiveWeek_few", p1) + } + /// inactive %d weeks + internal static func inactiveChannelsInactiveWeekMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "InactiveChannels.InactiveWeek_many", p1) + } + /// inactive %d week + internal static func inactiveChannelsInactiveWeekOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "InactiveChannels.InactiveWeek_one", p1) + } + /// inactive %d weeks + internal static func inactiveChannelsInactiveWeekOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "InactiveChannels.InactiveWeek_other", p1) + } + /// inactive %d weeks + internal static func inactiveChannelsInactiveWeekTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "InactiveChannels.InactiveWeek_two", p1) + } + /// inactive %d week + internal static func inactiveChannelsInactiveWeekZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "InactiveChannels.InactiveWeek_zero", p1) + } + /// %d + internal static func inactiveChannelsInactiveYearCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "InactiveChannels.InactiveYear_countable", p1) + } + /// inactive %d years + internal static func inactiveChannelsInactiveYearFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "InactiveChannels.InactiveYear_few", p1) + } + /// inactive %d years + internal static func inactiveChannelsInactiveYearMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "InactiveChannels.InactiveYear_many", p1) + } + /// inactive %d year + internal static func inactiveChannelsInactiveYearOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "InactiveChannels.InactiveYear_one", p1) + } + /// inactive %d years + internal static func inactiveChannelsInactiveYearOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "InactiveChannels.InactiveYear_other", p1) + } + /// inactive %d years + internal static func inactiveChannelsInactiveYearTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "InactiveChannels.InactiveYear_two", p1) + } + /// inactive %d year + internal static func inactiveChannelsInactiveYearZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "InactiveChannels.InactiveYear_zero", p1) + } + /// Leave + internal static var inactiveChannelsOK: String { return L10n.tr("Localizable", "InactiveChannels.OK") } + /// Limit Reached + internal static var inactiveChannelsTitle: String { return L10n.tr("Localizable", "InactiveChannels.Title") } /// Select a user or chat to share content via %@ - case inlineModalActionDesc(String) + internal static func inlineModalActionDesc(_ p1: String) -> String { + return L10n.tr("Localizable", "InlineModalAction.Desc", p1) + } /// Share bot content - case inlineModalActionTitle + internal static var inlineModalActionTitle: String { return L10n.tr("Localizable", "InlineModalAction.Title") } /// File - case inputAttachPopoverFile + internal static var inputAttachPopoverFile: String { return L10n.tr("Localizable", "InputAttach.Popover.File") } + /// Location + internal static var inputAttachPopoverLocation: String { return L10n.tr("Localizable", "InputAttach.Popover.Location") } + /// Audio File + internal static var inputAttachPopoverMusic: String { return L10n.tr("Localizable", "InputAttach.Popover.Music") } /// Photo Or Video - case inputAttachPopoverPhotoOrVideo + internal static var inputAttachPopoverPhotoOrVideo: String { return L10n.tr("Localizable", "InputAttach.Popover.PhotoOrVideo") } /// Camera - case inputAttachPopoverPicture + internal static var inputAttachPopoverPicture: String { return L10n.tr("Localizable", "InputAttach.Popover.Picture") } + /// Poll + internal static var inputAttachPopoverPoll: String { return L10n.tr("Localizable", "InputAttach.Popover.Poll") } + /// Day: + internal static var inputDataDateDayPlaceholder: String { return L10n.tr("Localizable", "InputData.Date.Day.Placeholder") } + /// Day + internal static var inputDataDateDayPlaceholder1: String { return L10n.tr("Localizable", "InputData.Date.Day.Placeholder1") } + /// Month: + internal static var inputDataDateMonthPlaceholder: String { return L10n.tr("Localizable", "InputData.Date.Month.Placeholder") } + /// Month + internal static var inputDataDateMonthPlaceholder1: String { return L10n.tr("Localizable", "InputData.Date.Month.Placeholder1") } + /// Year: + internal static var inputDataDateYearPlaceholder: String { return L10n.tr("Localizable", "InputData.Date.Year.Placeholder") } + /// Year + internal static var inputDataDateYearPlaceholder1: String { return L10n.tr("Localizable", "InputData.Date.Year.Placeholder1") } + /// TEXT + internal static var inputFormatterTextHeader: String { return L10n.tr("Localizable", "InputFormatter.Text.Header") } + /// URL + internal static var inputFormatterURLHeader: String { return L10n.tr("Localizable", "InputFormatter.URL.Header") } + /// URL + internal static var inputFormatterURLPlaceholder: String { return L10n.tr("Localizable", "InputFormatter.URL.Placeholder") } + /// Password + internal static var inputPasswordControllerPlaceholder: String { return L10n.tr("Localizable", "InputPasswordController.Placeholder") } + /// Invalid password. Please try again + internal static var inputPasswordControllerErrorWrongPassword: String { return L10n.tr("Localizable", "InputPasswordController.Error.WrongPassword") } /// Archived Stickers - case installedStickersArchived + internal static var installedStickersArchived: String { return L10n.tr("Localizable", "InstalledStickers.Archived") } /// 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. - case installedStickersDescrpiption + internal static var installedStickersDescrpiption: String { return L10n.tr("Localizable", "InstalledStickers.Descrpiption") } + /// Loop Animated Stickers + internal static var installedStickersLoopAnimated: String { return L10n.tr("Localizable", "InstalledStickers.LoopAnimated") } /// STICKER SETS - case installedStickersPacksTitle + internal static var installedStickersPacksTitle: String { return L10n.tr("Localizable", "InstalledStickers.PacksTitle") } /// Trending Stickers - case installedStickersTranding + internal static var installedStickersTranding: String { return L10n.tr("Localizable", "InstalledStickers.Tranding") } /// Delete - case installedStickersRemoveDelete + internal static var installedStickersRemoveDelete: String { return L10n.tr("Localizable", "InstalledStickers.Remove.Delete") } /// Stickers will be archived, you can quickly restore it later from the Archived Stickers section. - case installedStickersRemoveDescription + internal static var installedStickersRemoveDescription: String { return L10n.tr("Localizable", "InstalledStickers.Remove.Description") } /// By %1$@ • %2$@ - case instantPageAuthorAndDateTitle(String, String) - /// Join - case ivChannelJoin + internal static func instantPageAuthorAndDateTitle(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "InstantPage.AuthorAndDateTitle", p1, p2) + } + /// %@ • %@ + internal static func instantPageRelatedArticleAuthorAndDateTitle(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "InstantPage.RelatedArticleAuthorAndDateTitle", p1, p2) + } + /// Sorry, the target user is a member of too many groups and channels. Please ask them to leave some first. + internal static var inviteChannelsTooMuch: String { return L10n.tr("Localizable", "Invite.ChannelsTooMuch") } + /// %d + internal static func inviteLinkCanJoinCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "InviteLink.CanJoin_countable", p1) + } + /// %d can join + internal static func inviteLinkCanJoinFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "InviteLink.CanJoin_few", p1) + } + /// %d can join + internal static func inviteLinkCanJoinMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "InviteLink.CanJoin_many", p1) + } + /// %d can join + internal static func inviteLinkCanJoinOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "InviteLink.CanJoin_one", p1) + } + /// %d can join + internal static func inviteLinkCanJoinOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "InviteLink.CanJoin_other", p1) + } + /// %d can join + internal static func inviteLinkCanJoinTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "InviteLink.CanJoin_two", p1) + } + /// %d can join + internal static func inviteLinkCanJoinZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "InviteLink.CanJoin_zero", p1) + } + /// %d + internal static func inviteLinkEmptyJoinDescCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "InviteLink.EmptyJoinDesc_countable", p1) + } + /// %d people can join via this link + internal static func inviteLinkEmptyJoinDescFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "InviteLink.EmptyJoinDesc_few", p1) + } + /// %d people can join via this link + internal static func inviteLinkEmptyJoinDescMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "InviteLink.EmptyJoinDesc_many", p1) + } + /// %d people can join via this link + internal static func inviteLinkEmptyJoinDescOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "InviteLink.EmptyJoinDesc_one", p1) + } + /// %d people can join via this link + internal static func inviteLinkEmptyJoinDescOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "InviteLink.EmptyJoinDesc_other", p1) + } + /// %d people can join via this link + internal static func inviteLinkEmptyJoinDescTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "InviteLink.EmptyJoinDesc_two", p1) + } + /// %d people can join via this link + internal static func inviteLinkEmptyJoinDescZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "InviteLink.EmptyJoinDesc_zero", p1) + } + /// %d + internal static func inviteLinkJoinedCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "InviteLink.Joined_countable", p1) + } + /// %d joined + internal static func inviteLinkJoinedFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "InviteLink.Joined_few", p1) + } + /// %d joined + internal static func inviteLinkJoinedMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "InviteLink.Joined_many", p1) + } + /// %d joined + internal static func inviteLinkJoinedOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "InviteLink.Joined_one", p1) + } + /// %d joined + internal static func inviteLinkJoinedOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "InviteLink.Joined_other", p1) + } + /// %d joined + internal static func inviteLinkJoinedTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "InviteLink.Joined_two", p1) + } + /// no one joined yet + internal static var inviteLinkJoinedZero: String { return L10n.tr("Localizable", "InviteLink.Joined_zero") } + /// no one joined + internal static var inviteLinkJoinedRevoked: String { return L10n.tr("Localizable", "InviteLink.JoinedRevoked") } + /// %d + internal static func inviteLinkRemainingCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "InviteLink.Remaining_countable", p1) + } + /// • %d remaining + internal static func inviteLinkRemainingFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "InviteLink.Remaining_few", p1) + } + /// • %d remaining + internal static func inviteLinkRemainingMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "InviteLink.Remaining_many", p1) + } + /// • %d remaining + internal static func inviteLinkRemainingOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "InviteLink.Remaining_one", p1) + } + /// • %d remaining + internal static func inviteLinkRemainingOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "InviteLink.Remaining_other", p1) + } + /// • %d remaining + internal static func inviteLinkRemainingTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "InviteLink.Remaining_two", p1) + } + /// • %d remaining + internal static func inviteLinkRemainingZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "InviteLink.Remaining_zero", p1) + } + /// Share Link + internal static var inviteLinkShareLink: String { return L10n.tr("Localizable", "InviteLink.ShareLink") } + /// • expired + internal static var inviteLinkStickerExpired: String { return L10n.tr("Localizable", "InviteLink.Sticker.Expired") } + /// • limit reached + internal static var inviteLinkStickerLimit: String { return L10n.tr("Localizable", "InviteLink.Sticker.Limit") } + /// • revoked + internal static var inviteLinkStickerRevoked: String { return L10n.tr("Localizable", "InviteLink.Sticker.Revoked") } + /// expires in %@ + internal static func inviteLinkStickerTimeLeft(_ p1: String) -> String { + return L10n.tr("Localizable", "InviteLink.Sticker.TimeLeft", p1) + } + /// Sorry, you are a member of too many groups and channels. Please leave some before joining one. + internal static var joinChannelsTooMuch: String { return L10n.tr("Localizable", "Join.ChannelsTooMuch") } + /// Inactive Chats + internal static var joinInactiveChannels: String { return L10n.tr("Localizable", "Join.InactiveChannels") } /// Join - case joinLinkJoin + internal static var joinLinkJoin: String { return L10n.tr("Localizable", "JoinLink.Join") } /// Show All - case kd2MpPUSTitle + internal static var kd2MpPUSTitle: String { return L10n.tr("Localizable", "Kd2-mp-pUS.title") } /// Bring All to Front - case le2AR0XJTitle + internal static var le2AR0XJTitle: String { return L10n.tr("Localizable", "LE2-aR-0XJ.title") } + /// OFFICIAL TRANSLATIONS + internal static var languageOfficialTransationsHeader: String { return L10n.tr("Localizable", "Language.OfficialTransationsHeader") } + /// Are you sure you want to remove this lang-pack? + internal static var languageRemovePack: String { return L10n.tr("Localizable", "Language.RemovePack") } + /// %d + internal static func lastSeenHoursAgoCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "LastSeen.HoursAgo_countable", p1) + } + /// last seen %d hours ago + internal static func lastSeenHoursAgoFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "LastSeen.HoursAgo_few", p1) + } + /// last seen %d hours ago + internal static func lastSeenHoursAgoMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "LastSeen.HoursAgo_many", p1) + } + /// last seen %d hour ago + internal static func lastSeenHoursAgoOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "LastSeen.HoursAgo_one", p1) + } + /// last seen %d hours ago + internal static func lastSeenHoursAgoOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "LastSeen.HoursAgo_other", p1) + } + /// last seen %d hours ago + internal static func lastSeenHoursAgoTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "LastSeen.HoursAgo_two", p1) + } + /// last seen %d hour ago + internal static func lastSeenHoursAgoZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "LastSeen.HoursAgo_zero", p1) + } /// Welcome to the new super-fast and stable Telegram for macOS, fully rewritten in Swift 3.0. - case legacyIntroDescription1 + internal static var legacyIntroDescription1: String { return L10n.tr("Localizable", "Legacy.Intro.Description1") } /// Please note that your existing secret chats will be available in read-only mode. You can of course create new ones to continue chatting. - case legacyIntroDescription2 + internal static var legacyIntroDescription2: String { return L10n.tr("Localizable", "Legacy.Intro.Description2") } /// Start Messaging - case legacyIntroNext + internal static var legacyIntroNext: String { return L10n.tr("Localizable", "Legacy.Intro.Next") } + /// Sorry, this link has expired. + internal static var linkExpired: String { return L10n.tr("Localizable", "Link.Expired") } /// Are you sure you want to revoke this link? Once you do, no one will be able to join the channel using it. - case linkInvationChannelConfirmRevoke + internal static var linkInvationChannelConfirmRevoke: String { return L10n.tr("Localizable", "LinkInvation.Channel.Confirm.Revoke") } /// Revoke - case linkInvationConfirmOk + internal static var linkInvationConfirmOk: String { return L10n.tr("Localizable", "LinkInvation.Confirm.Ok") } /// Are you sure you want to revoke this link? Once you do, no one will be able to join the group using it. - case linkInvationGroupConfirmRevoke + internal static var linkInvationGroupConfirmRevoke: String { return L10n.tr("Localizable", "LinkInvation.Group.Confirm.Revoke") } + /// Sorry, this language doesn't seem to exist. + internal static var localizationPreviewErrorGeneric: String { return L10n.tr("Localizable", "Localization.Preview.Error.Generic") } + /// Accurate to %@ + internal static func locationSendAccurateTo(_ p1: String) -> String { + return L10n.tr("Localizable", "Location.Send.AccurateTo", p1) + } + /// Hide nearby places + internal static var locationSendHideNearby: String { return L10n.tr("Localizable", "Location.Send.HideNearby") } + /// Locating... + internal static var locationSendLocating: String { return L10n.tr("Localizable", "Location.Send.Locating") } + /// Send My Current Location + internal static var locationSendMyLocation: String { return L10n.tr("Localizable", "Location.Send.MyLocation") } + /// Show nearby places + internal static var locationSendShowNearby: String { return L10n.tr("Localizable", "Location.Send.ShowNearby") } + /// Send This Location + internal static var locationSendThisLocation: String { return L10n.tr("Localizable", "Location.Send.ThisLocation") } + /// Location + internal static var locationSendTitle: String { return L10n.tr("Localizable", "Location.Send.Title") } + /// Unknown Location + internal static var locationSendThisLocationUnknown: String { return L10n.tr("Localizable", "Location.Send.ThisLocation.Unknown") } /// code - case loginCodePlaceholder - /// Continue on English - case loginContinueOnLanguage + internal static var loginCodePlaceholder: String { return L10n.tr("Localizable", "Login.codePlaceholder") } + /// Continue in English + internal static var loginContinueOnLanguage: String { return L10n.tr("Localizable", "Login.ContinueOnLanguage") } /// country - case loginCountryLabel + internal static var loginCountryLabel: String { return L10n.tr("Localizable", "Login.countryLabel") } /// Please enter the code you've just received in Telegram on your other device. - case loginEnterCodeFromApp + internal static var loginEnterCodeFromApp: String { return L10n.tr("Localizable", "Login.EnterCodeFromApp") } /// You have enabled Two-Step Verification, your account is now protected with an additional password. - case loginEnterPasswordDescription - /// too many attempts, please try later. - case loginFloodWait + internal static var loginEnterPasswordDescription: String { return L10n.tr("Localizable", "Login.EnterPasswordDescription") } + /// Too many attempts, please try again later. + internal static var loginFloodWait: String { return L10n.tr("Localizable", "Login.FloodWait") } /// Invalid Country Code - case loginInvalidCountryCode + internal static var loginInvalidCountryCode: String { return L10n.tr("Localizable", "Login.InvalidCountryCode") } + /// Invalid first name. Please try again. + internal static var loginInvalidFirstNameError: String { return L10n.tr("Localizable", "Login.InvalidFirstNameError") } + /// Invalid last name. Please try again. + internal static var loginInvalidLastNameError: String { return L10n.tr("Localizable", "Login.InvalidLastNameError") } /// We have sent you a code via SMS. Please enter it above. - case loginJustSentSms + internal static var loginJustSentSms: String { return L10n.tr("Localizable", "Login.JustSentSms") } /// Next - case loginNext + internal static var loginNext: String { return L10n.tr("Localizable", "Login.Next") } + /// Forgot password? + internal static var loginPasswordForgot: String { return L10n.tr("Localizable", "Login.PasswordForgot") } /// password - case loginPasswordPlaceholder + internal static var loginPasswordPlaceholder: String { return L10n.tr("Localizable", "Login.passwordPlaceholder") } /// We’ve just called your number. Please enter the code above. - case loginPhoneCalledCode + internal static var loginPhoneCalledCode: String { return L10n.tr("Localizable", "Login.PhoneCalledCode") } /// Telegram dialed your number - case loginPhoneDialed + internal static var loginPhoneDialed: String { return L10n.tr("Localizable", "Login.PhoneDialed") } /// phone number - case loginPhoneFieldPlaceholder - /// Phone number not registered. If you don't have a Telegram account yet, please sign up with your mobile device. - case loginPhoneNumberNotRegistred - /// Since you haven't provided a recovery e-mail during the setup of your password, your remaining options are either to remember your password or to reset your account. - case loginRecoveryMailFailed - /// RESET MY ACCOUNT - case loginResetAccount - /// All your chats and messages, along with any media and files you shared will be lost if you proceed with resetting your account. - case loginResetAccountDescription + internal static var loginPhoneFieldPlaceholder: String { return L10n.tr("Localizable", "Login.phoneFieldPlaceholder") } + /// This account is already logged in from this app. + internal static var loginPhoneNumberAlreadyAuthorized: String { return L10n.tr("Localizable", "Login.PhoneNumberAlreadyAuthorized") } + /// This phone number isn't registered. If you don't have a Telegram account yet, please sign up with your mobile device. + internal static var loginPhoneNumberNotRegistred: String { return L10n.tr("Localizable", "Login.PhoneNumberNotRegistred") } + /// 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. + internal static var loginRecoveryMailFailed: String { return L10n.tr("Localizable", "Login.RecoveryMailFailed") } + /// RESET + internal static var loginResetAccount: String { return L10n.tr("Localizable", "Login.ResetAccount") } + /// If you proceed with resetting your account, all of your chats and messages along with any media and files you shared, will be lost. + internal static var loginResetAccountDescription: String { return L10n.tr("Localizable", "Login.ResetAccountDescription") } + /// Reset Account + internal static var loginResetAccountText: String { return L10n.tr("Localizable", "Login.ResetAccountText") } /// Haven't received the code? - case loginSendSmsIfNotReceivedAppCode + internal static var loginSendSmsIfNotReceivedAppCode: String { return L10n.tr("Localizable", "Login.SendSmsIfNotReceivedAppCode") } /// Welcome to the macOS application - case loginWelcomeDescription + internal static var loginWelcomeDescription: String { return L10n.tr("Localizable", "Login.WelcomeDescription") } /// Telegram will call you in %d:%@ - case loginWillCall(Int, String) + internal static func loginWillCall(_ p1: Int, _ p2: String) -> String { + return L10n.tr("Localizable", "Login.willCall", p1, p2) + } /// Telegram will send you an SMS in %d:%@ - case loginWillSendSms(Int, String) + internal static func loginWillSendSms(_ p1: Int, _ p2: String) -> String { + return L10n.tr("Localizable", "Login.willSendSms", p1, p2) + } /// your code - case loginYourCodeLabel + internal static var loginYourCodeLabel: String { return L10n.tr("Localizable", "Login.YourCodeLabel") } /// your password - case loginYourPasswordLabel + internal static var loginYourPasswordLabel: String { return L10n.tr("Localizable", "Login.YourPasswordLabel") } /// your phone - case loginYourPhoneLabel + internal static var loginYourPhoneLabel: String { return L10n.tr("Localizable", "Login.YourPhoneLabel") } + /// Can't reach server + internal static var loginConnectionErrorHeader: String { return L10n.tr("Localizable", "Login.ConnectionError.Header") } + /// Please check your internet connection and try again. + internal static var loginConnectionErrorInfo: String { return L10n.tr("Localizable", "Login.ConnectionError.Info") } + /// Try Again + internal static var loginConnectionErrorTryAgain: String { return L10n.tr("Localizable", "Login.ConnectionError.TryAgain") } + /// Use Proxy + internal static var loginConnectionErrorUseProxy: String { return L10n.tr("Localizable", "Login.ConnectionError.UseProxy") } /// Enter Code - case loginHeaderCode + internal static var loginHeaderCode: String { return L10n.tr("Localizable", "Login.Header.Code") } /// Enter Password - case loginHeaderPassword + internal static var loginHeaderPassword: String { return L10n.tr("Localizable", "Login.Header.Password") } /// Sign Up - case loginHeaderSignUp + internal static var loginHeaderSignUp: String { return L10n.tr("Localizable", "Login.Header.SignUp") } + /// Switch + internal static var loginPhoneNumberAlreadyAuthorizedSwitch: String { return L10n.tr("Localizable", "Login.PhoneNumberAlreadyAuthorized.Switch") } + /// Log in by phone Number + internal static var loginQRCancel: String { return L10n.tr("Localizable", "Login.QR.Cancel") } + /// Open Telegram on your phone + internal static var loginQRHelp1: String { return L10n.tr("Localizable", "Login.QR.Help1") } + /// Go to **Settings** > **Devices** > **Scan QR** + internal static var loginQRHelp2: String { return L10n.tr("Localizable", "Login.QR.Help2") } + /// Point your phone at this screen to confirm login + internal static var loginQRHelp3: String { return L10n.tr("Localizable", "Login.QR.Help3") } + /// Log in by QR Code + internal static var loginQRLogin: String { return L10n.tr("Localizable", "Login.QR.Login") } + /// Log in to Telegram by QR Code + internal static var loginQRTitle: String { return L10n.tr("Localizable", "Login.QR.Title") } + /// Enter your name and add a profile picture. + internal static var loginRegisterDesc: String { return L10n.tr("Localizable", "Login.Register.Desc") } + /// add\nphoto + internal static var loginRegisterAddPhotoPlaceholder: String { return L10n.tr("Localizable", "Login.Register.AddPhoto.Placeholder") } + /// First Name + internal static var loginRegisterFirstNamePlaceholder: String { return L10n.tr("Localizable", "Login.Register.FirstName.Placeholder") } + /// Last Name + internal static var loginRegisterLastNamePlaceholder: String { return L10n.tr("Localizable", "Login.Register.LastName.Placeholder") } + /// If you already signed up for Telegram, please enter the code which was sent to your mobile app via Telegram.\n\nIf you haven’t signed up yet, please register from your phone or tablet first. + internal static var loginSmsAppErr: String { return L10n.tr("Localizable", "Login.Sms.AppErr") } + /// Open Site + internal static var loginSmsAppErrGotoSite: String { return L10n.tr("Localizable", "Login.Sms.AppErr.GotoSite") } + /// Set up multiple phone numbers and easily switch between them. + internal static var logoutOptionsAddAccountText: String { return L10n.tr("Localizable", "LogoutOptions.AddAccountText") } + /// Add another account + internal static var logoutOptionsAddAccountTitle: String { return L10n.tr("Localizable", "LogoutOptions.AddAccountTitle") } + /// ALTERNATIVE OPTIONS + internal static var logoutOptionsAlternativeOptionsSection: String { return L10n.tr("Localizable", "LogoutOptions.AlternativeOptionsSection") } + /// Move your contacts, groups, messages and media to a new number. + internal static var logoutOptionsChangePhoneNumberText: String { return L10n.tr("Localizable", "LogoutOptions.ChangePhoneNumberText") } + /// Change Phone Number + internal static var logoutOptionsChangePhoneNumberTitle: String { return L10n.tr("Localizable", "LogoutOptions.ChangePhoneNumberTitle") } + /// Free up disk space on your device; your media will stay in the cloud. + internal static var logoutOptionsClearCacheText: String { return L10n.tr("Localizable", "LogoutOptions.ClearCacheText") } + /// Clear Cache + internal static var logoutOptionsClearCacheTitle: String { return L10n.tr("Localizable", "LogoutOptions.ClearCacheTitle") } + /// Tell us about any issues; logging out doesn't usually help. + internal static var logoutOptionsContactSupportText: String { return L10n.tr("Localizable", "LogoutOptions.ContactSupportText") } + /// Contact Support + internal static var logoutOptionsContactSupportTitle: String { return L10n.tr("Localizable", "LogoutOptions.ContactSupportTitle") } + /// Log Out + internal static var logoutOptionsLogOut: String { return L10n.tr("Localizable", "LogoutOptions.LogOut") } + /// Remember, logging out kills all your Secret Chats. + internal static var logoutOptionsLogOutInfo: String { return L10n.tr("Localizable", "LogoutOptions.LogOutInfo") } + /// Lock the app with a passcode so that others can't open it. + internal static var logoutOptionsSetPasscodeText: String { return L10n.tr("Localizable", "LogoutOptions.SetPasscodeText") } + /// Set a Passcode + internal static var logoutOptionsSetPasscodeTitle: String { return L10n.tr("Localizable", "LogoutOptions.SetPasscodeTitle") } + /// Log out + internal static var logoutOptionsTitle: String { return L10n.tr("Localizable", "LogoutOptions.Title") } + /// ADDITION LINKS + internal static var manageLinksAdditionLinks: String { return L10n.tr("Localizable", "ManageLinks.AdditionLinks") } + /// Create a New Link + internal static var manageLinksCreateNew: String { return L10n.tr("Localizable", "ManageLinks.CreateNew") } + /// Delete + internal static var manageLinksDelete: String { return L10n.tr("Localizable", "ManageLinks.Delete") } + /// Delete All Revoked Links + internal static var manageLinksDeleteAll: String { return L10n.tr("Localizable", "ManageLinks.DeleteAll") } + /// You can create addition invite links that have limited time or numbers of usage. + internal static var manageLinksEmptyDesc: String { return L10n.tr("Localizable", "ManageLinks.EmptyDesc") } + /// INVITE LINK + internal static var manageLinksInviteLink: String { return L10n.tr("Localizable", "ManageLinks.InviteLink") } + /// INVITE LINKS CREATED BY OTHER ADMINS + internal static var manageLinksOtherAdmins: String { return L10n.tr("Localizable", "ManageLinks.OtherAdmins") } + /// PERMANENT LINK + internal static var manageLinksPermanent: String { return L10n.tr("Localizable", "ManageLinks.Permanent") } + /// REVOKED LINKS + internal static var manageLinksRevokedLinks: String { return L10n.tr("Localizable", "ManageLinks.RevokedLinks") } + /// %d + internal static func manageLinksTitleCountCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "ManageLinks.TitleCount_countable", p1) + } + /// %d invite links + internal static func manageLinksTitleCountFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "ManageLinks.TitleCount_few", p1) + } + /// %d invite links + internal static func manageLinksTitleCountMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "ManageLinks.TitleCount_many", p1) + } + /// %d invite link + internal static func manageLinksTitleCountOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "ManageLinks.TitleCount_one", p1) + } + /// %d invite links + internal static func manageLinksTitleCountOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "ManageLinks.TitleCount_other", p1) + } + /// %d invite links + internal static func manageLinksTitleCountTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "ManageLinks.TitleCount_two", p1) + } + /// %d invite links + internal static func manageLinksTitleCountZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "ManageLinks.TitleCount_zero", p1) + } + /// Invite Links + internal static var manageLinksTitleNew: String { return L10n.tr("Localizable", "ManageLinks.TitleNew") } + /// **%1$@** can see this link and use it to invite new members to **%2$@** + internal static func manageLinksAdminPermanentDesc(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "ManageLinks.Admin.Permanent.Desc", p1, p2) + } + /// Copy Link + internal static var manageLinksContextCopy: String { return L10n.tr("Localizable", "ManageLinks.Context.Copy") } + /// Edit Link + internal static var manageLinksContextEdit: String { return L10n.tr("Localizable", "ManageLinks.Context.Edit") } + /// Revoke Link + internal static var manageLinksContextRevoke: String { return L10n.tr("Localizable", "ManageLinks.Context.Revoke") } + /// Share Link + internal static var manageLinksContextShare: String { return L10n.tr("Localizable", "ManageLinks.Context.Share") } + /// Are you sure you want to delete all revoked links? + internal static var manageLinksDeleteAllConfirm: String { return L10n.tr("Localizable", "ManageLinks.DeleteAll.Confirm") } + /// Anyone who has Telegram installed will be able to join your channel by following this group + internal static var manageLinksHeaderChannelDesc: String { return L10n.tr("Localizable", "ManageLinks.Header.Channel.Desc") } + /// Anyone who has Telegram installed will be able to join your group by following this group + internal static var manageLinksHeaderGroupDesc: String { return L10n.tr("Localizable", "ManageLinks.Header.Group.Desc") } + /// FAKE + internal static var markFake: String { return L10n.tr("Localizable", "Mark.Fake") } + /// SCAM + internal static var markScam: String { return L10n.tr("Localizable", "Mark.Scam") } + /// Discard Changes + internal static var mediaSenderDiscardChangesHeader: String { return L10n.tr("Localizable", "MediaSender.DiscardChanges.Header") } + /// Discard + internal static var mediaSenderDiscardChangesOK: String { return L10n.tr("Localizable", "MediaSender.DiscardChanges.OK") } + /// Are you sure you want to discard all changes? + internal static var mediaSenderDiscardChangesText: String { return L10n.tr("Localizable", "MediaSender.DiscardChanges.Text") } + /// INVOICE + internal static var messageInvoiceLabel: String { return L10n.tr("Localizable", "Message.InvoiceLabel") } + /// Payment: %@ + internal static func messagePaymentSent(_ p1: String) -> String { + return L10n.tr("Localizable", "Message.PaymentSent", p1) + } + /// pinned an invoice + internal static var messagePinnedInvoice: String { return L10n.tr("Localizable", "Message.PinnedInvoice") } + /// Show Receipt + internal static var messageReplyActionButtonShowReceipt: String { return L10n.tr("Localizable", "Message.ReplyActionButtonShowReceipt") } /// %d - case messageAccessoryPanelForwardedCountable(Int) + internal static func messageAccessoryPanelForwardedCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.AccessoryPanel.Forwarded_countable", p1) + } /// %d forwarded messages - case messageAccessoryPanelForwardedFew(Int) + internal static func messageAccessoryPanelForwardedFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.AccessoryPanel.Forwarded_few", p1) + } /// %d forwarded messages - case messageAccessoryPanelForwardedMany(Int) + internal static func messageAccessoryPanelForwardedMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.AccessoryPanel.Forwarded_many", p1) + } /// %d forwarded message - case messageAccessoryPanelForwardedOne(Int) + internal static func messageAccessoryPanelForwardedOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.AccessoryPanel.Forwarded_one", p1) + } /// %d forwarded messages - case messageAccessoryPanelForwardedOther(Int) + internal static func messageAccessoryPanelForwardedOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.AccessoryPanel.Forwarded_other", p1) + } /// %d forwarded messages - case messageAccessoryPanelForwardedTwo(Int) + internal static func messageAccessoryPanelForwardedTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.AccessoryPanel.Forwarded_two", p1) + } /// %d forwarded messages - case messageAccessoryPanelForwardedZero(Int) + internal static func messageAccessoryPanelForwardedZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.AccessoryPanel.Forwarded_zero", p1) + } /// Delete - case messageActionsPanelDelete + internal static var messageActionsPanelDelete: String { return L10n.tr("Localizable", "Message.ActionsPanel.Delete") } /// Select messages - case messageActionsPanelEmptySelected + internal static var messageActionsPanelEmptySelected: String { return L10n.tr("Localizable", "Message.ActionsPanel.EmptySelected") } /// Forward - case messageActionsPanelForward + internal static var messageActionsPanelForward: String { return L10n.tr("Localizable", "Message.ActionsPanel.Forward") } /// %d - case messageActionsPanelSelectedCountCountable(Int) + internal static func messageActionsPanelSelectedCountCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.ActionsPanel.SelectedCount_countable", p1) + } /// %d messages selected - case messageActionsPanelSelectedCountFew(Int) + internal static func messageActionsPanelSelectedCountFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.ActionsPanel.SelectedCount_few", p1) + } /// %d messages selected - case messageActionsPanelSelectedCountMany(Int) + internal static func messageActionsPanelSelectedCountMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.ActionsPanel.SelectedCount_many", p1) + } /// %d message selected - case messageActionsPanelSelectedCountOne(Int) + internal static func messageActionsPanelSelectedCountOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.ActionsPanel.SelectedCount_one", p1) + } /// %d messages selected - case messageActionsPanelSelectedCountOther(Int) + internal static func messageActionsPanelSelectedCountOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.ActionsPanel.SelectedCount_other", p1) + } /// %d messages selected - case messageActionsPanelSelectedCountTwo(Int) + internal static func messageActionsPanelSelectedCountTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.ActionsPanel.SelectedCount_two", p1) + } /// %d messages selected - case messageActionsPanelSelectedCountZero(Int) + internal static func messageActionsPanelSelectedCountZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.ActionsPanel.SelectedCount_zero", p1) + } /// Delete - case messageContextDelete + internal static var messageContextDelete: String { return L10n.tr("Localizable", "Message.Context.Delete") } /// Edit - case messageContextEdit + internal static var messageContextEdit: String { return L10n.tr("Localizable", "Message.Context.Edit") } /// Forward - case messageContextForward - /// Save to Cloud Storage - case messageContextForwardToCloud + internal static var messageContextForward: String { return L10n.tr("Localizable", "Message.Context.Forward") } + /// Forward to Saved Messages + internal static var messageContextForwardToCloud: String { return L10n.tr("Localizable", "Message.Context.ForwardToCloud") } /// Show Message - case messageContextGoto + internal static var messageContextGoto: String { return L10n.tr("Localizable", "Message.Context.Goto") } + /// Open With... + internal static var messageContextOpenWith: String { return L10n.tr("Localizable", "Message.Context.OpenWith") } /// Pin - case messageContextPin - /// Reply (double click) - case messageContextReply + internal static var messageContextPin: String { return L10n.tr("Localizable", "Message.Context.Pin") } + /// Remove GIF + internal static var messageContextRemoveGif: String { return L10n.tr("Localizable", "Message.Context.RemoveGif") } + /// Reply + internal static var messageContextReply1: String { return L10n.tr("Localizable", "Message.Context.Reply1") } + /// double click + internal static var messageContextReplyHelp: String { return L10n.tr("Localizable", "Message.Context.ReplyHelp") } + /// Report + internal static var messageContextReport: String { return L10n.tr("Localizable", "Message.Context.Report") } /// Add GIF - case messageContextSaveGif + internal static var messageContextSaveGif: String { return L10n.tr("Localizable", "Message.Context.SaveGif") } /// Select - case messageContextSelect - /// Pin only - case messageContextConfirmOnlyPin - /// Pin this message and notify all members of the group? - case messageContextConfirmPin - /// Copy Link - case messageContextCopyMessageLink + internal static var messageContextSelect: String { return L10n.tr("Localizable", "Message.Context.Select") } + /// Share + internal static var messageContextShare: String { return L10n.tr("Localizable", "Message.Context.Share") } + /// Unpin + internal static var messageContextUnpin: String { return L10n.tr("Localizable", "Message.Context.Unpin") } + /// %d + internal static func messageContextViewCommentsCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.Context.ViewComments_countable", p1) + } + /// View %d Comments + internal static func messageContextViewCommentsFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.Context.ViewComments_few", p1) + } + /// View %d Comments + internal static func messageContextViewCommentsMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.Context.ViewComments_many", p1) + } + /// View %d Comment + internal static func messageContextViewCommentsOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.Context.ViewComments_one", p1) + } + /// View %d Comments + internal static func messageContextViewCommentsOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.Context.ViewComments_other", p1) + } + /// View %d Comments + internal static func messageContextViewCommentsTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.Context.ViewComments_two", p1) + } + /// View %d Comments + internal static func messageContextViewCommentsZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.Context.ViewComments_zero", p1) + } + /// %d + internal static func messageContextViewRepliesCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.Context.ViewReplies_countable", p1) + } + /// View %d Replies + internal static func messageContextViewRepliesFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.Context.ViewReplies_few", p1) + } + /// View %d Replies + internal static func messageContextViewRepliesMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.Context.ViewReplies_many", p1) + } + /// View %d Reply + internal static func messageContextViewRepliesOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.Context.ViewReplies_one", p1) + } + /// View %d Replies + internal static func messageContextViewRepliesOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.Context.ViewReplies_other", p1) + } + /// View %d Replies + internal static func messageContextViewRepliesTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.Context.ViewReplies_two", p1) + } + /// View %d Replies + internal static func messageContextViewRepliesZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.Context.ViewReplies_zero", p1) + } + /// View Thread + internal static var messageContextViewThread: String { return L10n.tr("Localizable", "Message.Context.ViewThread") } + /// Notify all members + internal static var messageContextConfirmNotifyPin: String { return L10n.tr("Localizable", "Message.Context.Confirm.NotifyPin") } + /// Would you like to pin this message? + internal static var messageContextConfirmPin1: String { return L10n.tr("Localizable", "Message.Context.Confirm.Pin1") } + /// Thank you! Your report will be reviewed by our team very soon. + internal static var messageContextReportAlertOK: String { return L10n.tr("Localizable", "Message.Context.Report.AlertOK") } + /// archived folder + internal static var messageStatusArchived: String { return L10n.tr("Localizable", "Message.Status.Archived") } + /// preparing archive + internal static var messageStatusArchivePreparing: String { return L10n.tr("Localizable", "Message.Status.ArchivePreparing") } + /// %d%% archiving + internal static func messageStatusArchiving(_ p1: Int) -> String { + return L10n.tr("Localizable", "Message.Status.Archiving", p1) + } + /// archivation failed + internal static var messageStatusArchiveFailed: String { return L10n.tr("Localizable", "Message.Status.Archive.Failed") } + /// file size limit exceeded + internal static var messageStatusArchiveFailedSizeLimit: String { return L10n.tr("Localizable", "Message.Status.Archive.FailedSizeLimit") } + /// Copy Music Name + internal static var messageTextCopyMusicTitle: String { return L10n.tr("Localizable", "Message.Text.CopyMusicTitle") } + /// Copy Message Link + internal static var messageContextCopyMessageLink1: String { return L10n.tr("Localizable", "MessageContext.CopyMessageLink1") } + /// %@d + internal static func messageTimerShortDays(_ p1: String) -> String { + return L10n.tr("Localizable", "MessageTimer.ShortDays", p1) + } + /// %@h + internal static func messageTimerShortHours(_ p1: String) -> String { + return L10n.tr("Localizable", "MessageTimer.ShortHours", p1) + } + /// %@m + internal static func messageTimerShortMinutes(_ p1: String) -> String { + return L10n.tr("Localizable", "MessageTimer.ShortMinutes", p1) + } + /// %@M + internal static func messageTimerShortMonths(_ p1: String) -> String { + return L10n.tr("Localizable", "MessageTimer.ShortMonths", p1) + } + /// %@s + internal static func messageTimerShortSeconds(_ p1: String) -> String { + return L10n.tr("Localizable", "MessageTimer.ShortSeconds", p1) + } + /// %@w + internal static func messageTimerShortWeeks(_ p1: String) -> String { + return L10n.tr("Localizable", "MessageTimer.ShortWeeks", p1) + } /// Deleted message - case messagesDeletedMessage + internal static var messagesDeletedMessage: String { return L10n.tr("Localizable", "Messages.DeletedMessage") } /// Forwarded messages - case messagesForwardHeader + internal static var messagesForwardHeader: String { return L10n.tr("Localizable", "Messages.ForwardHeader") } /// Unread messages - case messagesUnreadMark - /// %d% downloaded - case messagesFileStateFetchingIn1(Int) - /// %d% uploaded - case messagesFileStateFetchingOut1(Int) + internal static var messagesUnreadMark: String { return L10n.tr("Localizable", "Messages.UnreadMark") } + /// %d%% downloaded + internal static func messagesFileStateFetchingIn1(_ p1: Int) -> String { + return L10n.tr("Localizable", "Messages.File.State.FetchingIn_1", p1) + } + /// %d%% uploaded + internal static func messagesFileStateFetchingOut1(_ p1: Int) -> String { + return L10n.tr("Localizable", "Messages.File.State.FetchingOut_1", p1) + } /// Show in Finder - case messagesFileStateLocal + internal static var messagesFileStateLocal: String { return L10n.tr("Localizable", "Messages.File.State.Local") } /// Download - case messagesFileStateRemote + internal static var messagesFileStateRemote: String { return L10n.tr("Localizable", "Messages.File.State.Remote") } + /// Send Anonymously... + internal static var messagesPlaceholderAnonymous: String { return L10n.tr("Localizable", "Messages.Placeholder.Anonymous") } /// Broadcast... - case messagesPlaceholderBroadcast + internal static var messagesPlaceholderBroadcast: String { return L10n.tr("Localizable", "Messages.Placeholder.Broadcast") } + /// Comment... + internal static var messagesPlaceholderComment: String { return L10n.tr("Localizable", "Messages.Placeholder.Comment") } + /// Reply... + internal static var messagesPlaceholderReply: String { return L10n.tr("Localizable", "Messages.Placeholder.Reply") } /// Write a message... - case messagesPlaceholderSentMessage + internal static var messagesPlaceholderSentMessage: String { return L10n.tr("Localizable", "Messages.Placeholder.SentMessage") } + /// Silent Broadcast... + internal static var messagesPlaceholderSilentBroadcast: String { return L10n.tr("Localizable", "Messages.Placeholder.SilentBroadcast") } + /// Broadcast... + internal static var messagesPlaceholderBroadcastSmall: String { return L10n.tr("Localizable", "Messages.Placeholder.Broadcast.Small") } + /// Message... + internal static var messagesPlaceholderSentMessageSmall: String { return L10n.tr("Localizable", "Messages.Placeholder.SentMessage.Small") } /// Reply - case messagesReplyLoadingHeader + internal static var messagesReplyLoadingHeader: String { return L10n.tr("Localizable", "Messages.ReplyLoading.Header") } /// Loading... - case messagesReplyLoadingLoading - /// Check Grammar With Spelling - case mk62p4JGTitle + internal static var messagesReplyLoadingLoading: String { return L10n.tr("Localizable", "Messages.ReplyLoading.Loading") } + /// Apply + internal static var modalApply: String { return L10n.tr("Localizable", "Modal.Apply") } /// Cancel - case modalCancel + internal static var modalCancel: String { return L10n.tr("Localizable", "Modal.Cancel") } /// Copy Link - case modalCopyLink + internal static var modalCopyLink: String { return L10n.tr("Localizable", "Modal.CopyLink") } + /// Done + internal static var modalDone: String { return L10n.tr("Localizable", "Modal.Done") } + /// Not Now + internal static var modalNotNow: String { return L10n.tr("Localizable", "Modal.NotNow") } /// OK - case modalOK + internal static var modalOK: String { return L10n.tr("Localizable", "Modal.OK") } + /// Report + internal static var modalReport: String { return L10n.tr("Localizable", "Modal.Report") } + /// Save + internal static var modalSave: String { return L10n.tr("Localizable", "Modal.Save") } /// Send - case modalSend + internal static var modalSend: String { return L10n.tr("Localizable", "Modal.Send") } + /// Set + internal static var modalSet: String { return L10n.tr("Localizable", "Modal.Set") } /// Share - case modalShare + internal static var modalShare: String { return L10n.tr("Localizable", "Modal.Share") } + /// YES + internal static var modalYes: String { return L10n.tr("Localizable", "Modal.Yes") } + /// Add + internal static var navigationAdd: String { return L10n.tr("Localizable", "Navigation.Add") } /// Back - case navigationBack + internal static var navigationBack: String { return L10n.tr("Localizable", "Navigation.back") } /// Cancel - case navigationCancel + internal static var navigationCancel: String { return L10n.tr("Localizable", "Navigation.Cancel") } /// Close - case navigationClose + internal static var navigationClose: String { return L10n.tr("Localizable", "Navigation.Close") } /// Done - case navigationDone + internal static var navigationDone: String { return L10n.tr("Localizable", "Navigation.Done") } /// Edit - case navigationEdit - /// You have new message - case notificationLockedPreview + internal static var navigationEdit: String { return L10n.tr("Localizable", "Navigation.Edit") } + /// Next + internal static var navigationNext: String { return L10n.tr("Localizable", "Navigation.Next") } + /// Bytes Received + internal static var networkUsageBytesReceived: String { return L10n.tr("Localizable", "NetworkUsage.BytesReceived") } + /// Bytes Sent + internal static var networkUsageBytesSent: String { return L10n.tr("Localizable", "NetworkUsage.BytesSent") } + /// Network Usage + internal static var networkUsageNetworkUsage: String { return L10n.tr("Localizable", "NetworkUsage.NetworkUsage") } + /// Network usage since %@ + internal static func networkUsageNetworkUsageSince(_ p1: String) -> String { + return L10n.tr("Localizable", "NetworkUsage.NetworkUsageSince", p1) + } + /// Reset Statistics + internal static var networkUsageReset: String { return L10n.tr("Localizable", "NetworkUsage.Reset") } + /// AUDIO + internal static var networkUsageHeaderAudio: String { return L10n.tr("Localizable", "NetworkUsage.Header.Audio") } + /// FILES + internal static var networkUsageHeaderFiles: String { return L10n.tr("Localizable", "NetworkUsage.Header.Files") } + /// MESSAGES + internal static var networkUsageHeaderGeneric: String { return L10n.tr("Localizable", "NetworkUsage.Header.Generic") } + /// PHOTOS + internal static var networkUsageHeaderImages: String { return L10n.tr("Localizable", "NetworkUsage.Header.Images") } + /// VIDEOS + internal static var networkUsageHeaderVideos: String { return L10n.tr("Localizable", "NetworkUsage.Header.Videos") } + /// phone number + internal static var newContactPhone: String { return L10n.tr("Localizable", "NewContact.Phone") } + /// New Contact + internal static var newContactTitle: String { return L10n.tr("Localizable", "NewContact.Title") } + /// Share My Phone Number + internal static var newContactExceptionShareMyPhoneNumber: String { return L10n.tr("Localizable", "NewContact.Exception.ShareMyPhoneNumber") } + /// You can make your phone visible to %@. + internal static func newContactExceptionShareMyPhoneNumberDesc(_ p1: String) -> String { + return L10n.tr("Localizable", "NewContact.Exception.ShareMyPhoneNumber.Desc", p1) + } + /// Hidden + internal static var newContactPhoneHidden: String { return L10n.tr("Localizable", "NewContact.Phone.Hidden") } + /// Phone number will be **visible** once %@ adds you as a contact. + internal static func newContactPhoneHiddenText(_ p1: String) -> String { + return L10n.tr("Localizable", "NewContact.Phone.Hidden.Text", p1) + } + /// Anonymous Voting + internal static var newPollAnonymous: String { return L10n.tr("Localizable", "NewPoll.Anonymous") } + /// Are you sure you want to discard this poll? + internal static var newPollDisacardConfirm: String { return L10n.tr("Localizable", "NewPoll.DisacardConfirm") } + /// Poll + internal static var newPollDisacardConfirmHeader: String { return L10n.tr("Localizable", "NewPoll.DisacardConfirmHeader") } + /// Multiple Choice + internal static var newPollMultipleChoice: String { return L10n.tr("Localizable", "NewPoll.MultipleChoice") } + /// Add an Option + internal static var newPollOptionsAddOption: String { return L10n.tr("Localizable", "NewPoll.OptionsAddOption") } + /// %d + internal static func newPollOptionsDescriptionCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "NewPoll.OptionsDescription_countable", p1) + } + /// You can add %d more options + internal static func newPollOptionsDescriptionFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "NewPoll.OptionsDescription_few", p1) + } + /// You can add %d more options + internal static func newPollOptionsDescriptionMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "NewPoll.OptionsDescription_many", p1) + } + /// You can add %d more options + internal static func newPollOptionsDescriptionOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "NewPoll.OptionsDescription_one", p1) + } + /// You can add %d more options + internal static func newPollOptionsDescriptionOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "NewPoll.OptionsDescription_other", p1) + } + /// You can add %d more options + internal static func newPollOptionsDescriptionTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "NewPoll.OptionsDescription_two", p1) + } + /// You can add %d more options + internal static func newPollOptionsDescriptionZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "NewPoll.OptionsDescription_zero", p1) + } + /// You have added the maximum number of options. + internal static var newPollOptionsDescriptionLimitReached: String { return L10n.tr("Localizable", "NewPoll.OptionsDescriptionLimitReached") } + /// %d + internal static func newPollOptionsDescriptionMinimumCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "NewPoll.OptionsDescriptionMinimum_countable", p1) + } + /// Minimum %d options + internal static func newPollOptionsDescriptionMinimumFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "NewPoll.OptionsDescriptionMinimum_few", p1) + } + /// Minimum %d options + internal static func newPollOptionsDescriptionMinimumMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "NewPoll.OptionsDescriptionMinimum_many", p1) + } + /// Minimum %d options + internal static func newPollOptionsDescriptionMinimumOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "NewPoll.OptionsDescriptionMinimum_one", p1) + } + /// Minimum %d options + internal static func newPollOptionsDescriptionMinimumOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "NewPoll.OptionsDescriptionMinimum_other", p1) + } + /// Minimum %d options + internal static func newPollOptionsDescriptionMinimumTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "NewPoll.OptionsDescriptionMinimum_two", p1) + } + /// Minimum %d options + internal static func newPollOptionsDescriptionMinimumZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "NewPoll.OptionsDescriptionMinimum_zero", p1) + } + /// POLL OPTIONS + internal static var newPollOptionsHeader: String { return L10n.tr("Localizable", "NewPoll.OptionsHeader") } + /// Option + internal static var newPollOptionsPlaceholder: String { return L10n.tr("Localizable", "NewPoll.OptionsPlaceholder") } + /// QUESTION + internal static var newPollQuestionHeader: String { return L10n.tr("Localizable", "NewPoll.QuestionHeader") } + /// QUESTION (%d) + internal static func newPollQuestionHeaderLimit(_ p1: Int) -> String { + return L10n.tr("Localizable", "NewPoll.QuestionHeaderLimit", p1) + } + /// Ask a question + internal static var newPollQuestionPlaceholder: String { return L10n.tr("Localizable", "NewPoll.QuestionPlaceholder") } + /// Quiz Mode + internal static var newPollQuiz: String { return L10n.tr("Localizable", "NewPoll.Quiz") } + /// Quiz has only one right answer. You can't revoke their votes. + internal static var newPollQuizDesc: String { return L10n.tr("Localizable", "NewPoll.QuizDesc") } + /// Select the correct option + internal static var newPollQuizTooltip: String { return L10n.tr("Localizable", "NewPoll.QuizTooltip") } + /// New Poll + internal static var newPollTitle: String { return L10n.tr("Localizable", "NewPoll.Title") } + /// No + internal static var newPollDisacardConfirmNo: String { return L10n.tr("Localizable", "NewPoll.DisacardConfirm.No") } + /// Discard + internal static var newPollDisacardConfirmYes: String { return L10n.tr("Localizable", "NewPoll.DisacardConfirm.Yes") } + /// Users will see this comment after choosing a wrong answer, good for educational purposes. + internal static var newPollExplanationDesc: String { return L10n.tr("Localizable", "NewPoll.Explanation.Desc") } + /// EXPLANATION + internal static var newPollExplanationHeader: String { return L10n.tr("Localizable", "NewPoll.Explanation.Header") } + /// Add a Comment (Optional) + internal static var newPollExplanationPlaceholder: String { return L10n.tr("Localizable", "NewPoll.Explanation.Placeholder") } + /// A quiz has one correct answer. + internal static var newPollQuizMultipleError: String { return L10n.tr("Localizable", "NewPoll.QuizMultiple.Error") } + /// New Quiz + internal static var newPollTitleQuiz: String { return L10n.tr("Localizable", "NewPoll.Title.Quiz") } + /// Create + internal static var newThemeCreate: String { return L10n.tr("Localizable", "NewTheme.Create") } + /// This theme will be based on your current theme. + internal static var newThemeDesc: String { return L10n.tr("Localizable", "NewTheme.Desc") } + /// name can't be empty. + internal static var newThemeEmptyTextError: String { return L10n.tr("Localizable", "NewTheme.EmptyTextError") } + /// Theme name + internal static var newThemePlaceholder: String { return L10n.tr("Localizable", "NewTheme.Placeholder") } + /// New Theme + internal static var newThemeTitle: String { return L10n.tr("Localizable", "NewTheme.Title") } + /// You have a new message + internal static var notificationLockedPreview: String { return L10n.tr("Localizable", "Notification.LockedPreview") } + /// Mark as Read + internal static var notificationMarkAsRead: String { return L10n.tr("Localizable", "Notification.MarkAsRead") } + /// %1$@ is now within %2$@ from %3$@ + internal static func notificationProximityReached1(_ p1: String, _ p2: String, _ p3: String) -> String { + return L10n.tr("Localizable", "Notification.ProximityReached_1", p1, p2, p3) + } + /// %1$@ is now within %2$@ from you + internal static func notificationProximityReachedYou1(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Notification.ProximityReachedYou_1", p1, p2) + } + /// You are now within %1$@ from %2$@ + internal static func notificationProximityYouReached1(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Notification.ProximityYouReached_1", p1, p2) + } + /// 📆 Reminder + internal static var notificationReminder: String { return L10n.tr("Localizable", "Notification.Reminder") } + /// Reply + internal static var notificationReply: String { return L10n.tr("Localizable", "Notification.Reply") } + /// Type message... + internal static var notificationInputReply: String { return L10n.tr("Localizable", "Notification.Input.Reply") } + /// Reply + internal static var notificationTitleReply: String { return L10n.tr("Localizable", "Notification.Title.Reply") } + /// All Accounts + internal static var notificationSettingsAllAccounts: String { return L10n.tr("Localizable", "NotificationSettings.AllAccounts") } + /// Switch off to show the number of unread chats instead of messages. + internal static var notificationSettingsBadgeDesc: String { return L10n.tr("Localizable", "NotificationSettings.BadgeDesc") } + /// Enabled + internal static var notificationSettingsBadgeEnabled: String { return L10n.tr("Localizable", "NotificationSettings.BadgeEnabled") } + /// BADGE COUNTER + internal static var notificationSettingsBadgeHeader: String { return L10n.tr("Localizable", "NotificationSettings.BadgeHeader") } + /// Bounce Dock Icon + internal static var notificationSettingsBounceDockIcon: String { return L10n.tr("Localizable", "NotificationSettings.BounceDockIcon") } + /// New Contacts + internal static var notificationSettingsContactJoined: String { return L10n.tr("Localizable", "NotificationSettings.ContactJoined") } + /// Receive notifications when one of your contacts becomes available on Telegram. + internal static var notificationSettingsContactJoinedInfo: String { return L10n.tr("Localizable", "NotificationSettings.ContactJoinedInfo") } + /// Count Unread Messages + internal static var notificationSettingsCountUnreadMessages: String { return L10n.tr("Localizable", "NotificationSettings.CountUnreadMessages") } + /// Include Channels + internal static var notificationSettingsIncludeChannels: String { return L10n.tr("Localizable", "NotificationSettings.IncludeChannels") } + /// Include Groups + internal static var notificationSettingsIncludeGroups: String { return L10n.tr("Localizable", "NotificationSettings.IncludeGroups") } + /// Include Muted Chats + internal static var notificationSettingsIncludeMutedChats: String { return L10n.tr("Localizable", "NotificationSettings.IncludeMutedChats") } /// Message Preview - case notificationSettingsMessagesPreview + internal static var notificationSettingsMessagesPreview: String { return L10n.tr("Localizable", "NotificationSettings.MessagesPreview") } /// Notification Tone - case notificationSettingsNotificationTone + internal static var notificationSettingsNotificationTone: String { return L10n.tr("Localizable", "NotificationSettings.NotificationTone") } /// Reset Notifications - case notificationSettingsResetNotifications + internal static var notificationSettingsResetNotifications: String { return L10n.tr("Localizable", "NotificationSettings.ResetNotifications") } /// You can set custom notifications for specific chats below. - case notificationSettingsResetNotificationsText + internal static var notificationSettingsResetNotificationsText: String { return L10n.tr("Localizable", "NotificationSettings.ResetNotificationsText") } + /// Sent Message + internal static var notificationSettingsSendMessageEffect: String { return L10n.tr("Localizable", "NotificationSettings.SendMessageEffect") } + /// SHOW NOTIFICATIONS FROM + internal static var notificationSettingsShowNotificationsFrom: String { return L10n.tr("Localizable", "NotificationSettings.ShowNotificationsFrom") } + /// App is in Focus + internal static var notificationSettingsSnoof: String { return L10n.tr("Localizable", "NotificationSettings.Snoof") } + /// SHOW NOTIFICATIONS WHEN + internal static var notificationSettingsSnoofHeader: String { return L10n.tr("Localizable", "NotificationSettings.SnoofHeader") } + /// SOUND EFFECTS + internal static var notificationSettingsSoundEffects: String { return L10n.tr("Localizable", "NotificationSettings.SoundEffects") } /// Notifications - case notificationSettingsToggleNotifications + internal static var notificationSettingsToggleNotifications: String { return L10n.tr("Localizable", "NotificationSettings.ToggleNotifications") } + /// Allow in System Settings + internal static var notificationSettingsTurnOn: String { return L10n.tr("Localizable", "NotificationSettings.TurnOn") } /// Reset notifications - case notificationSettingsConfirmReset + internal static var notificationSettingsConfirmReset: String { return L10n.tr("Localizable", "NotificationSettings.Confirm.Reset") } + /// Turn this on if you want to receive notifications from all your accounts. + internal static var notificationSettingsShowNotificationsFromOff: String { return L10n.tr("Localizable", "NotificationSettings.ShowNotificationsFrom.Off") } + /// Turn this off if you want to receive notifications only from your active account. + internal static var notificationSettingsShowNotificationsFromOn: String { return L10n.tr("Localizable", "NotificationSettings.ShowNotificationsFrom.On") } + /// Turn this on if you want to always receive notifications. + internal static var notificationSettingsSnoofOff: String { return L10n.tr("Localizable", "NotificationSettings.Snoof.Off") } + /// Turn this off if you want to receive notifications only when application is not in focus. + internal static var notificationSettingsSnoofOn: String { return L10n.tr("Localizable", "NotificationSettings.Snoof.On") } + /// NOTIFICATIONS + internal static var notificationSettingsToggleNotificationsHeader: String { return L10n.tr("Localizable", "NotificationSettings.ToggleNotifications.Header") } /// Default - case notificationSettingsToneDefault - /// Hide - case olwNPBQNTitle + internal static var notificationSettingsToneDefault: String { return L10n.tr("Localizable", "NotificationSettings.Tone.Default") } + /// Don't miss important messages from your family and friends. + internal static var notificationSettingsTurnOnTextText: String { return L10n.tr("Localizable", "NotificationSettings.TurnOn.Text.Text") } + /// Allow Notifications + internal static var notificationSettingsTurnOnTextTitle: String { return L10n.tr("Localizable", "NotificationSettings.TurnOn.Text.Title") } + /// Mute + internal static var notificationsSnooze: String { return L10n.tr("Localizable", "Notifications.Snooze") } + /// Alert + internal static var notificationsSoundAlert: String { return L10n.tr("Localizable", "NotificationsSound.Alert") } + /// Aurora + internal static var notificationsSoundAurora: String { return L10n.tr("Localizable", "NotificationsSound.Aurora") } + /// Bamboo + internal static var notificationsSoundBamboo: String { return L10n.tr("Localizable", "NotificationsSound.Bamboo") } + /// Bell + internal static var notificationsSoundBell: String { return L10n.tr("Localizable", "NotificationsSound.Bell") } + /// Calypso + internal static var notificationsSoundCalypso: String { return L10n.tr("Localizable", "NotificationsSound.Calypso") } + /// Chime + internal static var notificationsSoundChime: String { return L10n.tr("Localizable", "NotificationsSound.Chime") } + /// Chord + internal static var notificationsSoundChord: String { return L10n.tr("Localizable", "NotificationsSound.Chord") } + /// Circles + internal static var notificationsSoundCircles: String { return L10n.tr("Localizable", "NotificationsSound.Circles") } + /// Complete + internal static var notificationsSoundComplete: String { return L10n.tr("Localizable", "NotificationsSound.Complete") } + /// Glass + internal static var notificationsSoundGlass: String { return L10n.tr("Localizable", "NotificationsSound.Glass") } + /// Hello + internal static var notificationsSoundHello: String { return L10n.tr("Localizable", "NotificationsSound.Hello") } + /// Input + internal static var notificationsSoundInput: String { return L10n.tr("Localizable", "NotificationsSound.Input") } + /// Keys + internal static var notificationsSoundKeys: String { return L10n.tr("Localizable", "NotificationsSound.Keys") } + /// None + internal static var notificationsSoundNone: String { return L10n.tr("Localizable", "NotificationsSound.None") } + /// Note + internal static var notificationsSoundNote: String { return L10n.tr("Localizable", "NotificationsSound.Note") } + /// Popcorn + internal static var notificationsSoundPopcorn: String { return L10n.tr("Localizable", "NotificationsSound.Popcorn") } + /// Pulse + internal static var notificationsSoundPulse: String { return L10n.tr("Localizable", "NotificationsSound.Pulse") } + /// Synth + internal static var notificationsSoundSynth: String { return L10n.tr("Localizable", "NotificationsSound.Synth") } + /// Telegraph + internal static var notificationsSoundTelegraph: String { return L10n.tr("Localizable", "NotificationsSound.Telegraph") } + /// Tremolo + internal static var notificationsSoundTremolo: String { return L10n.tr("Localizable", "NotificationsSound.Tremolo") } + /// Tri-tone + internal static var notificationsSoundTritone: String { return L10n.tr("Localizable", "NotificationsSound.Tritone") } /// Minimize - case oy7WFPoVTitle - /// Delete - case pa3QIU2kTitle + internal static var oy7WFPoVTitle: String { return L10n.tr("Localizable", "OY7-WF-poV.title") } + /// Hide + internal static var olwNPBQNTitle: String { return L10n.tr("Localizable", "Olw-nP-bQN.title") } /// Auto-Lock - case passcodeAutolock + internal static var passcodeAutolock: String { return L10n.tr("Localizable", "Passcode.Autolock") } /// Change passcode - case passcodeChange - /// Enter Current Passcode - case passcodeEnterCurrentPlaceholder - /// Enter New Passcode - case passcodeEnterNewPlaceholder - /// Enter a passcode - case passcodeEnterPasscodePlaceholder - /// If you don't remember your passcode, you can - case passcodeLogoutDescription - /// logout - case passcodeLogoutLinkText + internal static var passcodeChange: String { return L10n.tr("Localizable", "Passcode.Change") } + /// Enter your current passcode + internal static var passcodeEnterCurrentPlaceholder: String { return L10n.tr("Localizable", "Passcode.EnterCurrentPlaceholder") } + /// Enter the new passcode + internal static var passcodeEnterNewPlaceholder: String { return L10n.tr("Localizable", "Passcode.EnterNewPlaceholder") } + /// Enter your passcode + internal static var passcodeEnterPasscodePlaceholder: String { return L10n.tr("Localizable", "Passcode.EnterPasscodePlaceholder") } /// Next - case passcodeNext - /// Re-enter a passcode - case passcodeReEnterPlaceholder + internal static var passcodeNext: String { return L10n.tr("Localizable", "Passcode.Next") } + /// or + internal static var passcodeOr: String { return L10n.tr("Localizable", "Passcode.Or") } + /// Re-enter the passcode + internal static var passcodeReEnterPlaceholder: String { return L10n.tr("Localizable", "Passcode.ReEnterPlaceholder") } /// Turn Passcode Off - case passcodeTurnOff + internal static var passcodeTurnOff: String { return L10n.tr("Localizable", "Passcode.TurnOff") } /// Turn Passcode On - case passcodeTurnOn + internal static var passcodeTurnOn: String { return L10n.tr("Localizable", "Passcode.TurnOn") } /// When you set up an additional passcode, you can use ⌘ + L for lock.\n\nNote: if you forget the passcode, you'll need to delete and reinstall the app. All secret chats will be lost. - case passcodeTurnOnDescription + internal static var passcodeTurnOnDescription: String { return L10n.tr("Localizable", "Passcode.TurnOnDescription") } + /// unlock itself + internal static var passcodeUnlockTouchIdReason: String { return L10n.tr("Localizable", "Passcode.UnlockTouchIdReason") } + /// Unlock with Touch ID + internal static var passcodeUseTouchId: String { return L10n.tr("Localizable", "Passcode.UseTouchId") } /// Disabled - case passcodeAutoLockDisabled + internal static var passcodeAutoLockDisabled: String { return L10n.tr("Localizable", "Passcode.AutoLock.Disabled") } /// If away for %@ - case passcodeAutoLockIfAway(String) - /// Sorry, Telegram Mac doesn't support payments yet. Please use one of our mobile apps to do this. - case paymentsUnsupported - /// Deleted User - case peerDeletedUser + internal static func passcodeAutoLockIfAway(_ p1: String) -> String { + return L10n.tr("Localizable", "Passcode.AutoLock.IfAway", p1) + } + /// If you don't remember your passcode, you can [log out]() + internal static var passcodeLostDescription: String { return L10n.tr("Localizable", "Passcode.Lost.Description") } + /// When a local passcode is set, a lock button is appears in quick settings menu. Just hover settings icon in tab bar or use ⌘ + L.\n\nNote: if you forget your local passcode you'll need to log out of Telegram Macos and log in again. + internal static var passcodeControllerText: String { return L10n.tr("Localizable", "PasscodeController.Text") } + /// Change Passcode + internal static var passcodeControllerChangeTitle: String { return L10n.tr("Localizable", "PasscodeController.Change.Title") } + /// Enter current passcode + internal static var passcodeControllerCurrentPlaceholder: String { return L10n.tr("Localizable", "PasscodeController.Current.Placeholder") } + /// Disable Passcode + internal static var passcodeControllerDisableTitle: String { return L10n.tr("Localizable", "PasscodeController.Disable.Title") } + /// Enter a passcode + internal static var passcodeControllerEnterPasscodePlaceholder: String { return L10n.tr("Localizable", "PasscodeController.EnterPasscode.Placeholder") } + /// invalid passcode + internal static var passcodeControllerErrorCurrent: String { return L10n.tr("Localizable", "PasscodeController.Error.Current") } + /// passcodes are different + internal static var passcodeControllerErrorDifferent: String { return L10n.tr("Localizable", "PasscodeController.Error.Different") } + /// CURRENT PASSCODE + internal static var passcodeControllerHeaderCurrent: String { return L10n.tr("Localizable", "PasscodeController.Header.Current") } + /// NEW PASSCODE + internal static var passcodeControllerHeaderNew: String { return L10n.tr("Localizable", "PasscodeController.Header.New") } + /// Passcode + internal static var passcodeControllerInstallTitle: String { return L10n.tr("Localizable", "PasscodeController.Install.Title") } + /// Re-enter new passcode + internal static var passcodeControllerReEnterPasscodePlaceholder: String { return L10n.tr("Localizable", "PasscodeController.ReEnterPasscode.Placeholder") } + /// Enter Your Passcode + internal static var passlockEnterYourPasscode: String { return L10n.tr("Localizable", "Passlock.EnterYourPasscode") } + /// Arabic + internal static var passportLanguageAr: String { return L10n.tr("Localizable", "Passport.Language.ar") } + /// Azerbaijani + internal static var passportLanguageAz: String { return L10n.tr("Localizable", "Passport.Language.az") } + /// Bulgarian + internal static var passportLanguageBg: String { return L10n.tr("Localizable", "Passport.Language.bg") } + /// Bangla + internal static var passportLanguageBn: String { return L10n.tr("Localizable", "Passport.Language.bn") } + /// Czech + internal static var passportLanguageCs: String { return L10n.tr("Localizable", "Passport.Language.cs") } + /// Danish + internal static var passportLanguageDa: String { return L10n.tr("Localizable", "Passport.Language.da") } + /// German + internal static var passportLanguageDe: String { return L10n.tr("Localizable", "Passport.Language.de") } + /// Divehi + internal static var passportLanguageDv: String { return L10n.tr("Localizable", "Passport.Language.dv") } + /// Dzongkha + internal static var passportLanguageDz: String { return L10n.tr("Localizable", "Passport.Language.dz") } + /// Greek + internal static var passportLanguageEl: String { return L10n.tr("Localizable", "Passport.Language.el") } + /// English + internal static var passportLanguageEn: String { return L10n.tr("Localizable", "Passport.Language.en") } + /// Spanish + internal static var passportLanguageEs: String { return L10n.tr("Localizable", "Passport.Language.es") } + /// Estonian + internal static var passportLanguageEt: String { return L10n.tr("Localizable", "Passport.Language.et") } + /// Persian + internal static var passportLanguageFa: String { return L10n.tr("Localizable", "Passport.Language.fa") } + /// French + internal static var passportLanguageFr: String { return L10n.tr("Localizable", "Passport.Language.fr") } + /// Hebrew + internal static var passportLanguageHe: String { return L10n.tr("Localizable", "Passport.Language.he") } + /// Croatian + internal static var passportLanguageHr: String { return L10n.tr("Localizable", "Passport.Language.hr") } + /// Hungarian + internal static var passportLanguageHu: String { return L10n.tr("Localizable", "Passport.Language.hu") } + /// Armenian + internal static var passportLanguageHy: String { return L10n.tr("Localizable", "Passport.Language.hy") } + /// Indonesian + internal static var passportLanguageId: String { return L10n.tr("Localizable", "Passport.Language.id") } + /// Icelandic + internal static var passportLanguageIs: String { return L10n.tr("Localizable", "Passport.Language.is") } + /// Italian + internal static var passportLanguageIt: String { return L10n.tr("Localizable", "Passport.Language.it") } + /// Japanese + internal static var passportLanguageJa: String { return L10n.tr("Localizable", "Passport.Language.ja") } + /// Georgian + internal static var passportLanguageKa: String { return L10n.tr("Localizable", "Passport.Language.ka") } + /// Khmer + internal static var passportLanguageKm: String { return L10n.tr("Localizable", "Passport.Language.km") } + /// Korean + internal static var passportLanguageKo: String { return L10n.tr("Localizable", "Passport.Language.ko") } + /// Lao + internal static var passportLanguageLo: String { return L10n.tr("Localizable", "Passport.Language.lo") } + /// Lithuanian + internal static var passportLanguageLt: String { return L10n.tr("Localizable", "Passport.Language.lt") } + /// Latvian + internal static var passportLanguageLv: String { return L10n.tr("Localizable", "Passport.Language.lv") } + /// Macedonian + internal static var passportLanguageMk: String { return L10n.tr("Localizable", "Passport.Language.mk") } + /// Mongolian + internal static var passportLanguageMn: String { return L10n.tr("Localizable", "Passport.Language.mn") } + /// Malay + internal static var passportLanguageMs: String { return L10n.tr("Localizable", "Passport.Language.ms") } + /// Burmese + internal static var passportLanguageMy: String { return L10n.tr("Localizable", "Passport.Language.my") } + /// Nepali + internal static var passportLanguageNe: String { return L10n.tr("Localizable", "Passport.Language.ne") } + /// Dutch + internal static var passportLanguageNl: String { return L10n.tr("Localizable", "Passport.Language.nl") } + /// Polish + internal static var passportLanguagePl: String { return L10n.tr("Localizable", "Passport.Language.pl") } + /// Portuguese + internal static var passportLanguagePt: String { return L10n.tr("Localizable", "Passport.Language.pt") } + /// Romanian + internal static var passportLanguageRo: String { return L10n.tr("Localizable", "Passport.Language.ro") } + /// Russian + internal static var passportLanguageRu: String { return L10n.tr("Localizable", "Passport.Language.ru") } + /// Slovak + internal static var passportLanguageSk: String { return L10n.tr("Localizable", "Passport.Language.sk") } + /// Slovenian + internal static var passportLanguageSl: String { return L10n.tr("Localizable", "Passport.Language.sl") } + /// Thai + internal static var passportLanguageTh: String { return L10n.tr("Localizable", "Passport.Language.th") } + /// Turkmen + internal static var passportLanguageTk: String { return L10n.tr("Localizable", "Passport.Language.tk") } + /// Turkish + internal static var passportLanguageTr: String { return L10n.tr("Localizable", "Passport.Language.tr") } + /// Ukrainian + internal static var passportLanguageUk: String { return L10n.tr("Localizable", "Passport.Language.uk") } + /// Uzbek + internal static var passportLanguageUz: String { return L10n.tr("Localizable", "Passport.Language.uz") } + /// Vietnamese + internal static var passportLanguageVi: String { return L10n.tr("Localizable", "Passport.Language.vi") } + /// Forgotten Password + internal static var passportResetPasswordConfirmHeader: String { return L10n.tr("Localizable", "Passport.ResetPassword.Confirm.Header") } + /// Reset + internal static var passportResetPasswordConfirmOK: String { return L10n.tr("Localizable", "Passport.ResetPassword.Confirm.OK") } + /// All documents uploaded to your Telegram Passport will be lost. You will be able to upload new documents. + internal static var passportResetPasswordConfirmText: String { return L10n.tr("Localizable", "Passport.ResetPassword.Confirm.Text") } + /// You paid %1$@ to %2$@ + internal static func paymentsPaid(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Payments.Paid", p1, p2) + } + /// Tip (Optional) + internal static var paymentsTipLabel: String { return L10n.tr("Localizable", "Payments.TipLabel") } + /// Sorry, Telegram for macOS doesn't support payments yet. Please use one of our mobile apps to do this. + internal static var paymentsUnsupported: String { return L10n.tr("Localizable", "Payments.Unsupported") } + /// Neither Telegram nor %1$@ will have access to your credit card information. Credit card details will be handled only by the payments system, %2$@.\n\n Payments will go directly to the developer of %3$@. Telegram cannot provide any guarantees, so proceed at your own risk. In case of problems, please contact the developer of %4$@ or your bank. + internal static func paymentsWarningText(_ p1: String, _ p2: String, _ p3: String, _ p4: String) -> String { + return L10n.tr("Localizable", "Payments.WarningText", p1, p2, p3, p4) + } + /// Warning + internal static var paymentsWarninTitle: String { return L10n.tr("Localizable", "Payments.WarninTitle") } + /// Tip + internal static var paymentsReceiptTip: String { return L10n.tr("Localizable", "Payments.Receipt.Tip") } + /// Deleted Account + internal static var peerDeletedUser: String { return L10n.tr("Localizable", "Peer.DeletedUser") } + /// Replies Notifications + internal static var peerRepliesNotifications: String { return L10n.tr("Localizable", "Peer.RepliesNotifications") } + /// Saved Messages + internal static var peerSavedMessages: String { return L10n.tr("Localizable", "Peer.SavedMessages") } /// Service Notifications - case peerServiceNotifications - /// %d are recording voice - case peerActivityChatMultiRecordingAudio(Int) - /// %d are recording video - case peerActivityChatMultiRecordingVideo(Int) - /// %d are sending audio - case peerActivityChatMultiSendingAudio(Int) - /// %d are sending file - case peerActivityChatMultiSendingFile(Int) - /// %d are sending photo - case peerActivityChatMultiSendingPhoto(Int) - /// %d are sending video - case peerActivityChatMultiSendingVideo(Int) - /// %d are typing - case peerActivityChatMultiTypingText(Int) + internal static var peerServiceNotifications: String { return L10n.tr("Localizable", "Peer.ServiceNotifications") } + /// %@ is choosing sticker + internal static func peerActivityChatChoosingSticker(_ p1: String) -> String { + return L10n.tr("Localizable", "Peer.Activity.Chat.ChoosingSticker", p1) + } + /// %@ is enjoying %@ animations + internal static func peerActivityChatEnjoyingAnimations(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Peer.Activity.Chat.EnjoyingAnimations", p1, p2) + } + /// %@ is playing a game + internal static func peerActivityChatPlayingGame(_ p1: String) -> String { + return L10n.tr("Localizable", "Peer.Activity.Chat.PlayingGame", p1) + } + /// %@ is recording voice + internal static func peerActivityChatRecordingAudio(_ p1: String) -> String { + return L10n.tr("Localizable", "Peer.Activity.Chat.RecordingAudio", p1) + } + /// %@ is recording video + internal static func peerActivityChatRecordingVideo(_ p1: String) -> String { + return L10n.tr("Localizable", "Peer.Activity.Chat.RecordingVideo", p1) + } + /// %@ is sending file + internal static func peerActivityChatSendingFile(_ p1: String) -> String { + return L10n.tr("Localizable", "Peer.Activity.Chat.SendingFile", p1) + } + /// %@ is sending photo + internal static func peerActivityChatSendingPhoto(_ p1: String) -> String { + return L10n.tr("Localizable", "Peer.Activity.Chat.SendingPhoto", p1) + } + /// %@ is sending video + internal static func peerActivityChatSendingVideo(_ p1: String) -> String { + return L10n.tr("Localizable", "Peer.Activity.Chat.SendingVideo", p1) + } + /// %@ is typing + internal static func peerActivityChatTypingText(_ p1: String) -> String { + return L10n.tr("Localizable", "Peer.Activity.Chat.TypingText", p1) + } + /// %@ and %d others are choosing stickers + internal static func peerActivityChatMultiChoosingSticker1(_ p1: String, _ p2: Int) -> String { + return L10n.tr("Localizable", "Peer.Activity.Chat.Multi.ChoosingSticker1", p1, p2) + } + /// %@ and %d others are playing a games + internal static func peerActivityChatMultiPlayingGame1(_ p1: String, _ p2: Int) -> String { + return L10n.tr("Localizable", "Peer.Activity.Chat.Multi.PlayingGame1", p1, p2) + } + /// %@ and %d others are recording voice + internal static func peerActivityChatMultiRecordingAudio1(_ p1: String, _ p2: Int) -> String { + return L10n.tr("Localizable", "Peer.Activity.Chat.Multi.RecordingAudio1", p1, p2) + } + /// %@ and %d others are recording video + internal static func peerActivityChatMultiRecordingVideo1(_ p1: String, _ p2: Int) -> String { + return L10n.tr("Localizable", "Peer.Activity.Chat.Multi.RecordingVideo1", p1, p2) + } + /// %@ and %d others are sending audio + internal static func peerActivityChatMultiSendingAudio1(_ p1: String, _ p2: Int) -> String { + return L10n.tr("Localizable", "Peer.Activity.Chat.Multi.SendingAudio1", p1, p2) + } + /// %@ and %d others are sending files + internal static func peerActivityChatMultiSendingFile1(_ p1: String, _ p2: Int) -> String { + return L10n.tr("Localizable", "Peer.Activity.Chat.Multi.SendingFile1", p1, p2) + } + /// %@ and %d others are sending photos + internal static func peerActivityChatMultiSendingPhoto1(_ p1: String, _ p2: Int) -> String { + return L10n.tr("Localizable", "Peer.Activity.Chat.Multi.SendingPhoto1", p1, p2) + } + /// %@ and %d others are sending videos + internal static func peerActivityChatMultiSendingVideo1(_ p1: String, _ p2: Int) -> String { + return L10n.tr("Localizable", "Peer.Activity.Chat.Multi.SendingVideo1", p1, p2) + } + /// %@ and %d others are typing + internal static func peerActivityChatMultiTypingText1(_ p1: String, _ p2: Int) -> String { + return L10n.tr("Localizable", "Peer.Activity.Chat.Multi.TypingText1", p1, p2) + } + /// choosing sticker + internal static var peerActivityUserChoosingSticker: String { return L10n.tr("Localizable", "Peer.Activity.User.ChoosingSticker") } + /// enjoying %@ animations + internal static func peerActivityUserEnjoyingAnimations(_ p1: String) -> String { + return L10n.tr("Localizable", "Peer.Activity.User.EnjoyingAnimations", p1) + } + /// playing a game + internal static var peerActivityUserPlayingGame: String { return L10n.tr("Localizable", "Peer.Activity.User.PlayingGame") } /// recording voice - case peerActivityUserRecordingAudio + internal static var peerActivityUserRecordingAudio: String { return L10n.tr("Localizable", "Peer.Activity.User.RecordingAudio") } /// recording video - case peerActivityUserRecordingVideo + internal static var peerActivityUserRecordingVideo: String { return L10n.tr("Localizable", "Peer.Activity.User.RecordingVideo") } /// sending file - case peerActivityUserSendingFile - /// sending photo - case peerActivityUserSendingPhoto - /// sending video - case peerActivityUserSendingVideo + internal static var peerActivityUserSendingFile: String { return L10n.tr("Localizable", "Peer.Activity.User.SendingFile") } + /// sending a photo + internal static var peerActivityUserSendingPhoto: String { return L10n.tr("Localizable", "Peer.Activity.User.SendingPhoto") } + /// sending a video + internal static var peerActivityUserSendingVideo: String { return L10n.tr("Localizable", "Peer.Activity.User.SendingVideo") } /// typing - case peerActivityUserTypingText - /// You can send and receive files of any type up to 1.5 GB each and access them anywhere. - case peerMediaSharedFilesEmptyList + internal static var peerActivityUserTypingText: String { return L10n.tr("Localizable", "Peer.Activity.User.TypingText") } + /// Remove photo + internal static var peerCreatePeerContextRemovePhoto: String { return L10n.tr("Localizable", "Peer.CreatePeer.Context.RemovePhoto") } + /// Update photo + internal static var peerCreatePeerContextUpdatePhoto: String { return L10n.tr("Localizable", "Peer.CreatePeer.Context.UpdatePhoto") } + /// You can send and receive files of any type up to 2.0 GB each and access them anywhere. + internal static var peerMediaSharedFilesEmptyList1: String { return L10n.tr("Localizable", "Peer.Media.SharedFilesEmptyList1") } /// All links shared in this chat will appear here. - case peerMediaSharedLinksEmptyList + internal static var peerMediaSharedLinksEmptyList: String { return L10n.tr("Localizable", "Peer.Media.SharedLinksEmptyList") } /// Share photos and videos in this chat - or this paperclip stays unhappy. - case peerMediaSharedMediaEmptyList + internal static var peerMediaSharedMediaEmptyList: String { return L10n.tr("Localizable", "Peer.Media.SharedMediaEmptyList") } /// All music shared in this chat will appear here. - case peerMediaSharedMusicEmptyList + internal static var peerMediaSharedMusicEmptyList: String { return L10n.tr("Localizable", "Peer.Media.SharedMusicEmptyList") } + /// All voice and video messages shared in this chat will appear here. + internal static var peerMediaSharedVoiceEmptyList: String { return L10n.tr("Localizable", "Peer.Media.SharedVoiceEmptyList") } /// channel - case peerStatusChannel + internal static var peerStatusChannel: String { return L10n.tr("Localizable", "Peer.Status.channel") } /// group - case peerStatusGroup + internal static var peerStatusGroup: String { return L10n.tr("Localizable", "Peer.Status.group") } /// last seen just now - case peerStatusJustNow + internal static var peerStatusJustNow: String { return L10n.tr("Localizable", "Peer.Status.justNow") } /// last seen within a month - case peerStatusLastMonth + internal static var peerStatusLastMonth: String { return L10n.tr("Localizable", "Peer.Status.lastMonth") } /// last seen %@ at %@ - case peerStatusLastSeenAt(String, String) + internal static func peerStatusLastSeenAt(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Peer.Status.LastSeenAt", p1, p2) + } /// last seen within a week - case peerStatusLastWeek + internal static var peerStatusLastWeek: String { return L10n.tr("Localizable", "Peer.Status.lastWeek") } + /// last seen a long time ago + internal static var peerStatusLongTimeAgo: String { return L10n.tr("Localizable", "Peer.Status.longTimeAgo") } /// %d - case peerStatusMemberCountable(Int) + internal static func peerStatusMemberCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.Member_countable", p1) + } /// %d members - case peerStatusMemberFew(Int) + internal static func peerStatusMemberFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.Member_few", p1) + } /// %d members - case peerStatusMemberMany(Int) + internal static func peerStatusMemberMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.Member_many", p1) + } /// %d member - case peerStatusMemberOne(Int) + internal static func peerStatusMemberOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.Member_one", p1) + } /// %d members - case peerStatusMemberOther(Int) + internal static func peerStatusMemberOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.Member_other", p1) + } /// %d members - case peerStatusMemberTwo(Int) + internal static func peerStatusMemberTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.Member_two", p1) + } /// %d members - case peerStatusMemberZero(Int) + internal static func peerStatusMemberZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.Member_zero", p1) + } /// %d - case peerStatusMinAgoCountable(Int) + internal static func peerStatusMinAgoCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.minAgo_countable", p1) + } /// last seen %d minutes ago - case peerStatusMinAgoFew(Int) + internal static func peerStatusMinAgoFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.minAgo_few", p1) + } /// last seen %d minutes ago - case peerStatusMinAgoMany(Int) + internal static func peerStatusMinAgoMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.minAgo_many", p1) + } /// last seen %d minute ago - case peerStatusMinAgoOne(Int) + internal static func peerStatusMinAgoOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.minAgo_one", p1) + } /// last seen %d minutes ago - case peerStatusMinAgoOther(Int) + internal static func peerStatusMinAgoOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.minAgo_other", p1) + } /// last seen %d minutes ago - case peerStatusMinAgoTwo(Int) + internal static func peerStatusMinAgoTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.minAgo_two", p1) + } /// last seen %d minutes ago - case peerStatusMinAgoZero(Int) + internal static func peerStatusMinAgoZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.minAgo_zero", p1) + } /// online - case peerStatusOnline + internal static var peerStatusOnline: String { return L10n.tr("Localizable", "Peer.Status.online") } /// last seen recently - case peerStatusRecently + internal static var peerStatusRecently: String { return L10n.tr("Localizable", "Peer.Status.recently") } + /// %d + internal static func peerStatusSubscribersCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.Subscribers_countable", p1) + } + /// %d subscribers + internal static func peerStatusSubscribersFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.Subscribers_few", p1) + } + /// %d subscribers + internal static func peerStatusSubscribersMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.Subscribers_many", p1) + } + /// %d subscriber + internal static func peerStatusSubscribersOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.Subscribers_one", p1) + } + /// %d subscribers + internal static func peerStatusSubscribersOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.Subscribers_other", p1) + } + /// %d subscribers + internal static func peerStatusSubscribersTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.Subscribers_two", p1) + } + /// %d subscribers + internal static func peerStatusSubscribersZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.Subscribers_zero", p1) + } /// today - case peerStatusToday + internal static var peerStatusToday: String { return L10n.tr("Localizable", "Peer.Status.Today") } /// yesterday - case peerStatusYesterday + internal static var peerStatusYesterday: String { return L10n.tr("Localizable", "Peer.Status.Yesterday") } /// %d - case peerStatusMemberOnlineCountable(Int) + internal static func peerStatusMemberOnlineCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.Member.Online_countable", p1) + } /// %d online - case peerStatusMemberOnlineFew(Int) + internal static func peerStatusMemberOnlineFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.Member.Online_few", p1) + } /// %d online - case peerStatusMemberOnlineMany(Int) + internal static func peerStatusMemberOnlineMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.Member.Online_many", p1) + } /// %d online - case peerStatusMemberOnlineOne(Int) + internal static func peerStatusMemberOnlineOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.Member.Online_one", p1) + } /// %d online - case peerStatusMemberOnlineOther(Int) + internal static func peerStatusMemberOnlineOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.Member.Online_other", p1) + } /// %d online - case peerStatusMemberOnlineTwo(Int) + internal static func peerStatusMemberOnlineTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.Member.Online_two", p1) + } /// %d online - case peerStatusMemberOnlineZero(Int) + internal static func peerStatusMemberOnlineZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Peer.Status.Member.Online_zero", p1) + } /// about - case peerInfoAbout + internal static var peerInfoAbout: String { return L10n.tr("Localizable", "PeerInfo.about") } /// Add Contact - case peerInfoAddContact - /// Add member - case peerInfoAddMember + internal static var peerInfoAddContact: String { return L10n.tr("Localizable", "PeerInfo.AddContact") } + /// Add Members + internal static var peerInfoAddMember: String { return L10n.tr("Localizable", "PeerInfo.AddMember") } + /// Add %@ to Contacts + internal static func peerInfoAddUserToContact(_ p1: String) -> String { + return L10n.tr("Localizable", "PeerInfo.AddUserToContact", p1) + } + /// Administrators + internal static var peerInfoAdministrators: String { return L10n.tr("Localizable", "PeerInfo.Administrators") } /// admin - case peerInfoAdminLabel - /// Admins - case peerInfoAdmins + internal static var peerInfoAdminLabel: String { return L10n.tr("Localizable", "PeerInfo.AdminLabel") } /// bio - case peerInfoBio - /// Blacklist - case peerInfoBlackList + internal static var peerInfoBio: String { return L10n.tr("Localizable", "PeerInfo.bio") } + /// Removed Users + internal static var peerInfoBlackList: String { return L10n.tr("Localizable", "PeerInfo.BlackList") } /// Block User - case peerInfoBlockUser - /// Thank You! Your report will be reviewed by our team very soon. - case peerInfoChannelReported + internal static var peerInfoBlockUser: String { return L10n.tr("Localizable", "PeerInfo.BlockUser") } + /// Thank you! Your report will be reviewed by our team soon. + internal static var peerInfoChannelReported: String { return L10n.tr("Localizable", "PeerInfo.ChannelReported") } /// Channel Type - case peerInfoChannelType + internal static var peerInfoChannelType: String { return L10n.tr("Localizable", "PeerInfo.ChannelType") } + /// Change Colors + internal static var peerInfoChatColors: String { return L10n.tr("Localizable", "PeerInfo.ChatColors") } /// Convert To Supergroup - case peerInfoConvertToSupergroup + internal static var peerInfoConvertToSupergroup: String { return L10n.tr("Localizable", "PeerInfo.ConvertToSupergroup") } /// Delete and Exit - case peerInfoDeleteAndExit + internal static var peerInfoDeleteAndExit: String { return L10n.tr("Localizable", "PeerInfo.DeleteAndExit") } /// Delete Channel - case peerInfoDeleteChannel + internal static var peerInfoDeleteChannel: String { return L10n.tr("Localizable", "PeerInfo.DeleteChannel") } /// Delete Contact - case peerInfoDeleteContact + internal static var peerInfoDeleteContact: String { return L10n.tr("Localizable", "PeerInfo.DeleteContact") } + /// Delete Group + internal static var peerInfoDeleteGroup: String { return L10n.tr("Localizable", "PeerInfo.DeleteGroup") } /// Delete Secret Chat - case peerInfoDeleteSecretChat + internal static var peerInfoDeleteSecretChat: String { return L10n.tr("Localizable", "PeerInfo.DeleteSecretChat") } + /// Discussion + internal static var peerInfoDiscussion: String { return L10n.tr("Localizable", "PeerInfo.Discussion") } /// Encryption Key - case peerInfoEncryptionKey + internal static var peerInfoEncryptionKey: String { return L10n.tr("Localizable", "PeerInfo.EncryptionKey") } + /// fake + internal static var peerInfoFake: String { return L10n.tr("Localizable", "PeerInfo.fake") } + /// ⚠️ Warning: Many users reported that this account impersonates a famous person or organization. + internal static var peerInfoFakeWarning: String { return L10n.tr("Localizable", "PeerInfo.FakeWarning") } /// Groups In Common - case peerInfoGroupsInCommon + internal static var peerInfoGroupsInCommon: String { return L10n.tr("Localizable", "PeerInfo.GroupsInCommon") } /// Group Type - case peerInfoGroupType + internal static var peerInfoGroupType: String { return L10n.tr("Localizable", "PeerInfo.GroupType") } /// info - case peerInfoInfo + internal static var peerInfoInfo: String { return L10n.tr("Localizable", "PeerInfo.info") } /// Invite Link - case peerInfoInviteLink + internal static var peerInfoInviteLink: String { return L10n.tr("Localizable", "PeerInfo.InviteLink") } + /// Invite Links + internal static var peerInfoInviteLinks: String { return L10n.tr("Localizable", "PeerInfo.InviteLinks") } /// Leave Channel - case peerInfoLeaveChannel + internal static var peerInfoLeaveChannel: String { return L10n.tr("Localizable", "PeerInfo.LeaveChannel") } + /// Leave Group + internal static var peerInfoLeaveGroup: String { return L10n.tr("Localizable", "PeerInfo.LeaveGroup") } + /// Linked Channel + internal static var peerInfoLinkedChannel: String { return L10n.tr("Localizable", "PeerInfo.LinkedChannel") } /// Members - case peerInfoMembers + internal static var peerInfoMembers: String { return L10n.tr("Localizable", "PeerInfo.Members") } /// %d - case peerInfoMembersHeaderCountable(Int) + internal static func peerInfoMembersHeaderCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerInfo.MembersHeader_countable", p1) + } /// %d MEMBERS - case peerInfoMembersHeaderFew(Int) + internal static func peerInfoMembersHeaderFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerInfo.MembersHeader_few", p1) + } /// %d MEMBERS - case peerInfoMembersHeaderMany(Int) + internal static func peerInfoMembersHeaderMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerInfo.MembersHeader_many", p1) + } /// %d MEMBER - case peerInfoMembersHeaderOne(Int) + internal static func peerInfoMembersHeaderOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerInfo.MembersHeader_one", p1) + } /// %d MEMBERS - case peerInfoMembersHeaderOther(Int) + internal static func peerInfoMembersHeaderOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerInfo.MembersHeader_other", p1) + } /// %d MEMBERS - case peerInfoMembersHeaderTwo(Int) + internal static func peerInfoMembersHeaderTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerInfo.MembersHeader_two", p1) + } /// %d MEMBERS - case peerInfoMembersHeaderZero(Int) + internal static func peerInfoMembersHeaderZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerInfo.MembersHeader_zero", p1) + } /// Notifications - case peerInfoNotifications + internal static var peerInfoNotifications: String { return L10n.tr("Localizable", "PeerInfo.Notifications") } + /// Default + internal static var peerInfoNotificationsDefault: String { return L10n.tr("Localizable", "PeerInfo.NotificationsDefault") } + /// Default (%@) + internal static func peerInfoNotificationsDefaultSound(_ p1: String) -> String { + return L10n.tr("Localizable", "PeerInfo.NotificationsDefaultSound", p1) + } + /// Permissions + internal static var peerInfoPermissions: String { return L10n.tr("Localizable", "PeerInfo.Permissions") } /// phone - case peerInfoPhone + internal static var peerInfoPhone: String { return L10n.tr("Localizable", "PeerInfo.Phone") } /// Chat History For New Members - case peerInfoPreHistory + internal static var peerInfoPreHistory: String { return L10n.tr("Localizable", "PeerInfo.PreHistory") } + /// Removed Users + internal static var peerInfoRemovedUsers: String { return L10n.tr("Localizable", "PeerInfo.RemovedUsers") } /// Report - case peerInfoReport + internal static var peerInfoReport: String { return L10n.tr("Localizable", "PeerInfo.Report") } + /// Restart Bot + internal static var peerInfoRestartBot: String { return L10n.tr("Localizable", "PeerInfo.RestartBot") } + /// scam + internal static var peerInfoScam: String { return L10n.tr("Localizable", "PeerInfo.scam") } + /// ⚠️ Warning: Many users reported this account as a scam. Please be careful, especially if it asks you for money. + internal static var peerInfoScamWarning: String { return L10n.tr("Localizable", "PeerInfo.ScamWarning") } /// Send Message - case peerInfoSendMessage + internal static var peerInfoSendMessage: String { return L10n.tr("Localizable", "PeerInfo.SendMessage") } /// You can provide an optional description for your group. - case peerInfoSetAboutDescription - /// Set Admins - case peerInfoSetAdmins + internal static var peerInfoSetAboutDescription: String { return L10n.tr("Localizable", "PeerInfo.SetAboutDescription") } /// Set Channel Photo - case peerInfoSetChannelPhoto + internal static var peerInfoSetChannelPhoto: String { return L10n.tr("Localizable", "PeerInfo.SetChannelPhoto") } /// Set Group Photo - case peerInfoSetGroupPhoto + internal static var peerInfoSetGroupPhoto: String { return L10n.tr("Localizable", "PeerInfo.SetGroupPhoto") } /// Group Sticker Set - case peerInfoSetGroupStickersSet + internal static var peerInfoSetGroupStickersSet: String { return L10n.tr("Localizable", "PeerInfo.SetGroupStickersSet") } /// Share Contact - case peerInfoShareContact + internal static var peerInfoShareContact: String { return L10n.tr("Localizable", "PeerInfo.ShareContact") } /// Shared Media - case peerInfoSharedMedia + internal static var peerInfoSharedMedia: String { return L10n.tr("Localizable", "PeerInfo.SharedMedia") } /// share link - case peerInfoSharelink + internal static var peerInfoSharelink: String { return L10n.tr("Localizable", "PeerInfo.sharelink") } + /// Share My Contact Info + internal static var peerInfoShareMyInfo: String { return L10n.tr("Localizable", "PeerInfo.ShareMyInfo") } + /// Show More + internal static var peerInfoShowMore: String { return L10n.tr("Localizable", "PeerInfo.ShowMore") } + /// [more]() + internal static var peerInfoShowMoreText: String { return L10n.tr("Localizable", "PeerInfo.ShowMoreText") } /// Sign Messages - case peerInfoSignMessages + internal static var peerInfoSignMessages: String { return L10n.tr("Localizable", "PeerInfo.SignMessages") } /// Start Secret Chat - case peerInfoStartSecretChat + internal static var peerInfoStartSecretChat: String { return L10n.tr("Localizable", "PeerInfo.StartSecretChat") } + /// Stop Bot + internal static var peerInfoStopBot: String { return L10n.tr("Localizable", "PeerInfo.StopBot") } + /// Subscribers + internal static var peerInfoSubscribers: String { return L10n.tr("Localizable", "PeerInfo.Subscribers") } + /// Unarchive + internal static var peerInfoUnarchive: String { return L10n.tr("Localizable", "PeerInfo.Unarchive") } /// Unblock User - case peerInfoUnblockUser + internal static var peerInfoUnblockUser: String { return L10n.tr("Localizable", "PeerInfo.UnblockUser") } /// username - case peerInfoUsername + internal static var peerInfoUsername: String { return L10n.tr("Localizable", "PeerInfo.username") } /// Description - case peerInfoAboutPlaceholder + internal static var peerInfoAboutPlaceholder: String { return L10n.tr("Localizable", "PeerInfo.About.Placeholder") } + /// Add + internal static var peerInfoActionAddMembers: String { return L10n.tr("Localizable", "PeerInfo.Action.AddMembers") } + /// Call + internal static var peerInfoActionCall: String { return L10n.tr("Localizable", "PeerInfo.Action.Call") } + /// Discuss + internal static var peerInfoActionDiscussion: String { return L10n.tr("Localizable", "PeerInfo.Action.Discussion") } + /// Leave + internal static var peerInfoActionLeave: String { return L10n.tr("Localizable", "PeerInfo.Action.Leave") } + /// Live Stream + internal static var peerInfoActionLiveStream: String { return L10n.tr("Localizable", "PeerInfo.Action.LiveStream") } + /// Message + internal static var peerInfoActionMessage: String { return L10n.tr("Localizable", "PeerInfo.Action.Message") } + /// More + internal static var peerInfoActionMore: String { return L10n.tr("Localizable", "PeerInfo.Action.More") } + /// Mute + internal static var peerInfoActionMute: String { return L10n.tr("Localizable", "PeerInfo.Action.Mute") } + /// Report + internal static var peerInfoActionReport: String { return L10n.tr("Localizable", "PeerInfo.Action.Report") } + /// Secret + internal static var peerInfoActionSecretChat: String { return L10n.tr("Localizable", "PeerInfo.Action.SecretChat") } + /// Share + internal static var peerInfoActionShare: String { return L10n.tr("Localizable", "PeerInfo.Action.Share") } + /// Statistics + internal static var peerInfoActionStatistics: String { return L10n.tr("Localizable", "PeerInfo.Action.Statistics") } + /// Unmute + internal static var peerInfoActionUnmute: String { return L10n.tr("Localizable", "PeerInfo.Action.Unmute") } + /// Video + internal static var peerInfoActionVideoCall: String { return L10n.tr("Localizable", "PeerInfo.Action.VideoCall") } + /// Voice Chat + internal static var peerInfoActionVoiceChat: String { return L10n.tr("Localizable", "PeerInfo.Action.VoiceChat") } + /// Block User + internal static var peerInfoBlockHeader: String { return L10n.tr("Localizable", "PeerInfo.Block.Header") } + /// Block + internal static var peerInfoBlockOK: String { return L10n.tr("Localizable", "PeerInfo.Block.OK") } + /// Do you want to block %@ from messaging and calling you on Telegram? + internal static func peerInfoBlockText(_ p1: String) -> String { + return L10n.tr("Localizable", "PeerInfo.Block.Text", p1) + } + /// Add To Group + internal static var peerInfoBotAddToGroup: String { return L10n.tr("Localizable", "PeerInfo.Bot.AddToGroup") } + /// Help + internal static var peerInfoBotHelp: String { return L10n.tr("Localizable", "PeerInfo.Bot.Help") } + /// Privacy + internal static var peerInfoBotPrivacy: String { return L10n.tr("Localizable", "PeerInfo.Bot.Privacy") } + /// Settings + internal static var peerInfoBotSettings: String { return L10n.tr("Localizable", "PeerInfo.Bot.Settings") } + /// Share + internal static var peerInfoBotShare: String { return L10n.tr("Localizable", "PeerInfo.Bot.Share") } /// has access to messages - case peerInfoBotStatusHasAccess + internal static var peerInfoBotStatusHasAccess: String { return L10n.tr("Localizable", "PeerInfo.BotStatus.HasAccess") } /// has no access to messages - case peerInfoBotStatusHasNoAccess + internal static var peerInfoBotStatusHasNoAccess: String { return L10n.tr("Localizable", "PeerInfo.BotStatus.HasNoAccess") } + /// Channel Name + internal static var peerInfoChannelNamePlaceholder: String { return L10n.tr("Localizable", "PeerInfo.ChannelName.Placeholder") } /// Channel Name - case peerInfoChannelNamePlaceholder - /// Add "%@" to group? - case peerInfoConfirmAddMember(String) - /// Add %d users to group? - case peerInfoConfirmAddMembers(Int) - /// Are you sure you want to delete all message history and leave "%@"?\n\nThis action cannot be undone. - case peerInfoConfirmDeleteChat(String) - /// Delete Contact? - case peerInfoConfirmDeleteContact + internal static var peerInfoChannelTitlePleceholder: String { return L10n.tr("Localizable", "PeerInfo.ChannelTitle.Pleceholder") } + /// Add + internal static var peerInfoConfirmAdd: String { return L10n.tr("Localizable", "PeerInfo.Confirm.Add") } + /// Add "%@" to the group? + internal static func peerInfoConfirmAddMember(_ p1: String) -> String { + return L10n.tr("Localizable", "PeerInfo.Confirm.AddMember", p1) + } + /// %d + internal static func peerInfoConfirmAddMembers1Countable(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerInfo.Confirm.AddMembers1_countable", p1) + } + /// Add %d users to the group? + internal static func peerInfoConfirmAddMembers1Few(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerInfo.Confirm.AddMembers1_few", p1) + } + /// Add %d users to the group? + internal static func peerInfoConfirmAddMembers1Many(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerInfo.Confirm.AddMembers1_many", p1) + } + /// Add %d user to the group? + internal static func peerInfoConfirmAddMembers1One(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerInfo.Confirm.AddMembers1_one", p1) + } + /// Add %d users to the group? + internal static func peerInfoConfirmAddMembers1Other(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerInfo.Confirm.AddMembers1_other", p1) + } + /// Add %d users to the group? + internal static func peerInfoConfirmAddMembers1Two(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerInfo.Confirm.AddMembers1_two", p1) + } + /// Add %d users to the group? + internal static func peerInfoConfirmAddMembers1Zero(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerInfo.Confirm.AddMembers1_zero", p1) + } + /// Clear + internal static var peerInfoConfirmClear: String { return L10n.tr("Localizable", "PeerInfo.Confirm.Clear") } + /// Are you sure you want to delete all message history and leave "%@"? + internal static func peerInfoConfirmDeleteChat(_ p1: String) -> String { + return L10n.tr("Localizable", "PeerInfo.Confirm.DeleteChat", p1) + } + /// Are you sure you want to delete this contact? + internal static var peerInfoConfirmDeleteContact: String { return L10n.tr("Localizable", "PeerInfo.Confirm.DeleteContact") } + /// Wait! Deleting this group will remove all members and all messages will be lost. Delete the group anyway? + internal static var peerInfoConfirmDeleteGroupConfirmation: String { return L10n.tr("Localizable", "PeerInfo.Confirm.DeleteGroupConfirmation") } + /// Are you sure you want to delete chat? + internal static var peerInfoConfirmDeleteUserChat: String { return L10n.tr("Localizable", "PeerInfo.Confirm.DeleteUserChat") } /// Are you sure you want to leave this channel? - case peerInfoConfirmLeaveChannel - /// Are you sure you want to leave this group?\n\nThis action cannot be undone. - case peerInfoConfirmLeaveGroup + internal static var peerInfoConfirmLeaveChannel: String { return L10n.tr("Localizable", "PeerInfo.Confirm.LeaveChannel") } + /// Are you sure you want to leave this group? + internal static var peerInfoConfirmLeaveGroup: String { return L10n.tr("Localizable", "PeerInfo.Confirm.LeaveGroup") } /// Remove "%@" from group? - case peerInfoConfirmRemovePeer(String) + internal static func peerInfoConfirmRemovePeer(_ p1: String) -> String { + return L10n.tr("Localizable", "PeerInfo.Confirm.RemovePeer", p1) + } + /// Are you sure you want to share your phone number with "%@"? + internal static func peerInfoConfirmShareInfo(_ p1: String) -> String { + return L10n.tr("Localizable", "PeerInfo.Confirm.ShareInfo", p1) + } /// Are you sure you want to start a secret chat with "%@"? - case peerInfoConfirmStartSecretChat(String) + internal static func peerInfoConfirmStartSecretChat(_ p1: String) -> String { + return L10n.tr("Localizable", "PeerInfo.Confirm.StartSecretChat", p1) + } + /// Secret Chat + internal static var peerInfoConfirmSecretChatHeader: String { return L10n.tr("Localizable", "PeerInfo.Confirm.SecretChat.Header") } + /// Start + internal static var peerInfoConfirmSecretChatOK: String { return L10n.tr("Localizable", "PeerInfo.Confirm.SecretChat.OK") } + /// Add + internal static var peerInfoDiscussionAdd: String { return L10n.tr("Localizable", "PeerInfo.Discussion.Add") } + /// Add group chat for comments. + internal static var peerInfoDiscussionDesc: String { return L10n.tr("Localizable", "PeerInfo.Discussion.Desc") } /// First Name - case peerInfoFirstNamePlaceholder + internal static var peerInfoFirstNamePlaceholder: String { return L10n.tr("Localizable", "PeerInfo.FirstName.Placeholder") } + /// Auto-Delete Messages + internal static var peerInfoGroupAutoDeleteMessages: String { return L10n.tr("Localizable", "PeerInfo.Group.AutoDeleteMessages") } + /// Delete + internal static var peerInfoGroupMenuDelete: String { return L10n.tr("Localizable", "PeerInfo.Group.Menu.Delete") } + /// Promote + internal static var peerInfoGroupMenuPromote: String { return L10n.tr("Localizable", "PeerInfo.Group.Menu.Promote") } + /// Restrict + internal static var peerInfoGroupMenuRestrict: String { return L10n.tr("Localizable", "PeerInfo.Group.Menu.Restrict") } + /// Never + internal static var peerInfoGroupTimerNever: String { return L10n.tr("Localizable", "PeerInfo.Group.Timer.Never") } + /// Group Name + internal static var peerInfoGroupNamePlaceholder: String { return L10n.tr("Localizable", "PeerInfo.GroupName.Placeholder") } /// Group Name - case peerInfoGroupNamePlaceholder + internal static var peerInfoGroupTitlePleceholder: String { return L10n.tr("Localizable", "PeerInfo.GroupTitle.Pleceholder") } /// Private - case peerInfoGroupTypePrivate + internal static var peerInfoGroupTypePrivate: String { return L10n.tr("Localizable", "PeerInfo.GroupType.Private") } /// Public - case peerInfoGroupTypePublic + internal static var peerInfoGroupTypePublic: String { return L10n.tr("Localizable", "PeerInfo.GroupType.Public") } + /// Sorry, you must be in this user's Telegram contacts to add them to this group.\n\nThey can also join on their own if you send them an invite link. + internal static var peerInfoInviteErrorContactNeeded: String { return L10n.tr("Localizable", "PeerInfo.InviteError.ContactNeeded") } /// Last Name - case peerInfoLastNamePlaceholder + internal static var peerInfoLastNamePlaceholder: String { return L10n.tr("Localizable", "PeerInfo.LastName.Placeholder") } /// Hidden - case peerInfoPreHistoryHidden + internal static var peerInfoPreHistoryHidden: String { return L10n.tr("Localizable", "PeerInfo.PreHistory.Hidden") } /// Visible - case peerInfoPreHistoryVisible - /// Add names of the admins to the messages they post. - case peerInfoSignMessagesDesc + internal static var peerInfoPreHistoryVisible: String { return L10n.tr("Localizable", "PeerInfo.PreHistory.Visible") } + /// Select Messages + internal static var peerInfoReportSelectMessages: String { return L10n.tr("Localizable", "PeerInfo.Report.SelectMessages") } + /// Append names of the admins to the messages they post. + internal static var peerInfoSignMessagesDesc: String { return L10n.tr("Localizable", "PeerInfo.SignMessages.Desc") } + /// Audio + internal static var peerMediaAudio: String { return L10n.tr("Localizable", "PeerMedia.Audio") } + /// Groups + internal static var peerMediaCommonGroups: String { return L10n.tr("Localizable", "PeerMedia.CommonGroups") } + /// Docs + internal static var peerMediaFiles: String { return L10n.tr("Localizable", "PeerMedia.Files") } + /// GIFs + internal static var peerMediaGifs: String { return L10n.tr("Localizable", "PeerMedia.Gifs") } + /// Links + internal static var peerMediaLinks: String { return L10n.tr("Localizable", "PeerMedia.Links") } + /// Media + internal static var peerMediaMedia: String { return L10n.tr("Localizable", "PeerMedia.Media") } + /// Members + internal static var peerMediaMembers: String { return L10n.tr("Localizable", "PeerMedia.Members") } + /// Music + internal static var peerMediaMusic: String { return L10n.tr("Localizable", "PeerMedia.Music") } /// Shared Media - case peerMediaSharedMedia + internal static var peerMediaSharedMedia: String { return L10n.tr("Localizable", "PeerMedia.SharedMedia") } + /// Voicemessages + internal static var peerMediaVoice: String { return L10n.tr("Localizable", "PeerMedia.Voice") } /// Shared Audio - case peerMediaPopoverSharedAudio + internal static var peerMediaPopoverSharedAudio: String { return L10n.tr("Localizable", "PeerMedia.Popover.SharedAudio") } /// Shared Files - case peerMediaPopoverSharedFiles + internal static var peerMediaPopoverSharedFiles: String { return L10n.tr("Localizable", "PeerMedia.Popover.SharedFiles") } /// Shared Links - case peerMediaPopoverSharedLinks + internal static var peerMediaPopoverSharedLinks: String { return L10n.tr("Localizable", "PeerMedia.Popover.SharedLinks") } /// Shared Media - case peerMediaPopoverSharedMedia + internal static var peerMediaPopoverSharedMedia: String { return L10n.tr("Localizable", "PeerMedia.Popover.SharedMedia") } + /// %d + internal static func peerMediaTitleSearchFilesCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Files_countable", p1) + } + /// %d Files + internal static func peerMediaTitleSearchFilesFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Files_few", p1) + } + /// %d Files + internal static func peerMediaTitleSearchFilesMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Files_many", p1) + } + /// %d File + internal static func peerMediaTitleSearchFilesOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Files_one", p1) + } + /// %d Files + internal static func peerMediaTitleSearchFilesOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Files_other", p1) + } + /// %d Files + internal static func peerMediaTitleSearchFilesTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Files_two", p1) + } + /// %d Files + internal static func peerMediaTitleSearchFilesZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Files_zero", p1) + } + /// %d + internal static func peerMediaTitleSearchGIFsCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.GIFs_countable", p1) + } + /// %d GIFs + internal static func peerMediaTitleSearchGIFsFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.GIFs_few", p1) + } + /// %d GIFs + internal static func peerMediaTitleSearchGIFsMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.GIFs_many", p1) + } + /// %d GIF + internal static func peerMediaTitleSearchGIFsOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.GIFs_one", p1) + } + /// %d GIFs + internal static func peerMediaTitleSearchGIFsOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.GIFs_other", p1) + } + /// %d GIFs + internal static func peerMediaTitleSearchGIFsTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.GIFs_two", p1) + } + /// %d GIFs + internal static func peerMediaTitleSearchGIFsZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.GIFs_zero", p1) + } + /// %d + internal static func peerMediaTitleSearchLinksCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Links_countable", p1) + } + /// %d Links + internal static func peerMediaTitleSearchLinksFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Links_few", p1) + } + /// %d Links + internal static func peerMediaTitleSearchLinksMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Links_many", p1) + } + /// %d Link + internal static func peerMediaTitleSearchLinksOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Links_one", p1) + } + /// %d Links + internal static func peerMediaTitleSearchLinksOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Links_other", p1) + } + /// %d Links + internal static func peerMediaTitleSearchLinksTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Links_two", p1) + } + /// %d Links + internal static func peerMediaTitleSearchLinksZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Links_zero", p1) + } + /// %d + internal static func peerMediaTitleSearchMediaCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Media_countable", p1) + } + /// %d Medias + internal static func peerMediaTitleSearchMediaFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Media_few", p1) + } + /// %d Medias + internal static func peerMediaTitleSearchMediaMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Media_many", p1) + } + /// %d Media + internal static func peerMediaTitleSearchMediaOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Media_one", p1) + } + /// %d Medias + internal static func peerMediaTitleSearchMediaOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Media_other", p1) + } + /// %d Medias + internal static func peerMediaTitleSearchMediaTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Media_two", p1) + } + /// %d Media + internal static func peerMediaTitleSearchMediaZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Media_zero", p1) + } + /// %d + internal static func peerMediaTitleSearchMusicCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Music_countable", p1) + } + /// %d Audios + internal static func peerMediaTitleSearchMusicFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Music_few", p1) + } + /// %d Audios + internal static func peerMediaTitleSearchMusicMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Music_many", p1) + } + /// %d Audio + internal static func peerMediaTitleSearchMusicOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Music_one", p1) + } + /// %d Audios + internal static func peerMediaTitleSearchMusicOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Music_other", p1) + } + /// %d Audios + internal static func peerMediaTitleSearchMusicTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Music_two", p1) + } + /// %d Audios + internal static func peerMediaTitleSearchMusicZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Music_zero", p1) + } + /// %d + internal static func peerMediaTitleSearchPhotosCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Photos_countable", p1) + } + /// %d Photos + internal static func peerMediaTitleSearchPhotosFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Photos_few", p1) + } + /// %d Photos + internal static func peerMediaTitleSearchPhotosMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Photos_many", p1) + } + /// %d Photo + internal static func peerMediaTitleSearchPhotosOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Photos_one", p1) + } + /// %d Photos + internal static func peerMediaTitleSearchPhotosOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Photos_other", p1) + } + /// %d Photos + internal static func peerMediaTitleSearchPhotosTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Photos_two", p1) + } + /// %d Photos + internal static func peerMediaTitleSearchPhotosZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Photos_zero", p1) + } + /// %d + internal static func peerMediaTitleSearchVideosCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Videos_countable", p1) + } + /// %d Videos + internal static func peerMediaTitleSearchVideosFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Videos_few", p1) + } + /// %d Videos + internal static func peerMediaTitleSearchVideosMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Videos_many", p1) + } + /// %d Video + internal static func peerMediaTitleSearchVideosOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Videos_one", p1) + } + /// %d Videos + internal static func peerMediaTitleSearchVideosOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Videos_other", p1) + } + /// %d Videos + internal static func peerMediaTitleSearchVideosTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Videos_two", p1) + } + /// %d Videos + internal static func peerMediaTitleSearchVideosZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "PeerMedia.Title.Search.Videos_zero", p1) + } + /// Invite to Group via Link + internal static var peerSelectInviteViaLink: String { return L10n.tr("Localizable", "PeerSelect.InviteViaLink") } + /// Sorry, public polls can’t be forwarded to channels. + internal static var pollForwardError: String { return L10n.tr("Localizable", "Poll.Forward.Error") } + /// [Collapse]() + internal static var pollResultsCollapse: String { return L10n.tr("Localizable", "PollResults.Collapse") } + /// %d + internal static func pollResultsLoadMoreCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "PollResults.LoadMore_countable", p1) + } + /// Show More (%d) + internal static func pollResultsLoadMoreFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "PollResults.LoadMore_few", p1) + } + /// Show More (%d) + internal static func pollResultsLoadMoreMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "PollResults.LoadMore_many", p1) + } + /// Show More (%d) + internal static func pollResultsLoadMoreOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "PollResults.LoadMore_one", p1) + } + /// Show More (%d) + internal static func pollResultsLoadMoreOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "PollResults.LoadMore_other", p1) + } + /// Show More (%d) + internal static func pollResultsLoadMoreTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "PollResults.LoadMore_two", p1) + } + /// Show More (%d) + internal static func pollResultsLoadMoreZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "PollResults.LoadMore_zero", p1) + } + /// Poll Results + internal static var pollResultsTitlePoll: String { return L10n.tr("Localizable", "PollResults.Title.Poll") } + /// Quiz Results + internal static var pollResultsTitleQuiz: String { return L10n.tr("Localizable", "PollResults.Title.Quiz") } + /// Warning, this will unlink the group from "%@" + internal static func preHistoryConfirmUnlink(_ p1: String) -> String { + return L10n.tr("Localizable", "PreHistory.Confirm.Unlink", p1) + } /// CHAT HISTORY FOR NEW MEMBERS - case preHistorySettingsHeader + internal static var preHistorySettingsHeader: String { return L10n.tr("Localizable", "PreHistorySettings.Header") } /// New members won't see earlier messages. - case preHistorySettingsDescriptionHidden + internal static var preHistorySettingsDescriptionHidden: String { return L10n.tr("Localizable", "PreHistorySettings.Description.Hidden") } /// New Members will see messages that were sent before they joined. - case preHistorySettingsDescriptionVisible + internal static var preHistorySettingsDescriptionVisible: String { return L10n.tr("Localizable", "PreHistorySettings.Description.Visible") } + /// New members won't see more than 100 previous messages. + internal static var preHistorySettingsDescriptionGroupHidden: String { return L10n.tr("Localizable", "PreHistorySettings.Description.Group.Hidden") } /// bot - case presenceBot - /// Caption... - case previderSenderCaptionPlaceholder - /// Send as compressed - case previewSenderCompressFile + internal static var presenceBot: String { return L10n.tr("Localizable", "Presence.bot") } + /// support + internal static var presenceSupport: String { return L10n.tr("Localizable", "Presence.Support") } + /// %d + internal static func previewDraggingAddItemsCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Preview.Dragging.AddItems_countable", p1) + } + /// Add Items + internal static var previewDraggingAddItemsFew: String { return L10n.tr("Localizable", "Preview.Dragging.AddItems_few") } + /// Add Items + internal static var previewDraggingAddItemsMany: String { return L10n.tr("Localizable", "Preview.Dragging.AddItems_many") } + /// Add Item + internal static var previewDraggingAddItemsOne: String { return L10n.tr("Localizable", "Preview.Dragging.AddItems_one") } + /// Add Items + internal static var previewDraggingAddItemsOther: String { return L10n.tr("Localizable", "Preview.Dragging.AddItems_other") } + /// Add Items + internal static var previewDraggingAddItemsTwo: String { return L10n.tr("Localizable", "Preview.Dragging.AddItems_two") } + /// Add Items + internal static var previewDraggingAddItemsZero: String { return L10n.tr("Localizable", "Preview.Dragging.AddItems_zero") } + /// Archive all media in one zip file + internal static var previewSenderArchiveTooltip: String { return L10n.tr("Localizable", "PreviewSender.ArchiveTooltip") } + /// Add a caption... + internal static var previewSenderCaptionPlaceholder: String { return L10n.tr("Localizable", "PreviewSender.CaptionPlaceholder") } + /// Group all media into one message + internal static var previewSenderCollageTooltip: String { return L10n.tr("Localizable", "PreviewSender.CollageTooltip") } + /// Add a comment... + internal static var previewSenderCommentPlaceholder: String { return L10n.tr("Localizable", "PreviewSender.CommentPlaceholder") } + /// Send compressed + internal static var previewSenderCompressFile: String { return L10n.tr("Localizable", "PreviewSender.CompressFile") } + /// Send without compression + internal static var previewSenderFileTooltip: String { return L10n.tr("Localizable", "PreviewSender.FileTooltip") } + /// Send in a quick way + internal static var previewSenderMediaTooltip: String { return L10n.tr("Localizable", "PreviewSender.MediaTooltip") } + /// %d + internal static func previewSenderSendAudioCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendAudio_countable", p1) + } + /// Send %d Audios + internal static func previewSenderSendAudioFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendAudio_few", p1) + } + /// Send %d Audios + internal static func previewSenderSendAudioMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendAudio_many", p1) + } + /// Send Audio + internal static var previewSenderSendAudioOne: String { return L10n.tr("Localizable", "PreviewSender.SendAudio_one") } + /// Send %d Audios + internal static func previewSenderSendAudioOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendAudio_other", p1) + } + /// Send %d Audios + internal static func previewSenderSendAudioTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendAudio_two", p1) + } + /// Send %d Audios + internal static func previewSenderSendAudioZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendAudio_zero", p1) + } + /// %d + internal static func previewSenderSendFileCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendFile_countable", p1) + } + /// Send %d Files + internal static func previewSenderSendFileFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendFile_few", p1) + } + /// Send %d Files + internal static func previewSenderSendFileMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendFile_many", p1) + } + /// Send File + internal static var previewSenderSendFileOne: String { return L10n.tr("Localizable", "PreviewSender.SendFile_one") } + /// Send %d Files + internal static func previewSenderSendFileOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendFile_other", p1) + } + /// Send %d Files + internal static func previewSenderSendFileTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendFile_two", p1) + } + /// Send %d Files + internal static func previewSenderSendFileZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendFile_zero", p1) + } + /// %d + internal static func previewSenderSendGifCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendGif_countable", p1) + } + /// Send %d GIFs + internal static func previewSenderSendGifFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendGif_few", p1) + } + /// Send %d GIFs + internal static func previewSenderSendGifMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendGif_many", p1) + } + /// Send GIF + internal static var previewSenderSendGifOne: String { return L10n.tr("Localizable", "PreviewSender.SendGif_one") } + /// Send %d GIFs + internal static func previewSenderSendGifOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendGif_other", p1) + } + /// Send %d GIFs + internal static func previewSenderSendGifTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendGif_two", p1) + } + /// Send %d GIFs + internal static func previewSenderSendGifZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendGif_zero", p1) + } + /// %d + internal static func previewSenderSendMediaCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendMedia_countable", p1) + } + /// Send %d Media + internal static func previewSenderSendMediaFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendMedia_few", p1) + } + /// Send %d Media + internal static func previewSenderSendMediaMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendMedia_many", p1) + } + /// Send Media + internal static var previewSenderSendMediaOne: String { return L10n.tr("Localizable", "PreviewSender.SendMedia_one") } + /// Send %d Media + internal static func previewSenderSendMediaOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendMedia_other", p1) + } + /// Send %d Media + internal static func previewSenderSendMediaTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendMedia_two", p1) + } + /// Send %d Media + internal static func previewSenderSendMediaZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendMedia_zero", p1) + } + /// %d + internal static func previewSenderSendPhotoCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendPhoto_countable", p1) + } + /// Send %d Photos + internal static func previewSenderSendPhotoFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendPhoto_few", p1) + } + /// Send %d Photos + internal static func previewSenderSendPhotoMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendPhoto_many", p1) + } + /// Send Photo + internal static var previewSenderSendPhotoOne: String { return L10n.tr("Localizable", "PreviewSender.SendPhoto_one") } + /// Send %d Photos + internal static func previewSenderSendPhotoOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendPhoto_other", p1) + } + /// Send %d Photos + internal static func previewSenderSendPhotoTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendPhoto_two", p1) + } + /// Send %d Photos + internal static func previewSenderSendPhotoZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendPhoto_zero", p1) + } + /// %d + internal static func previewSenderSendVideoCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendVideo_countable", p1) + } + /// Send %d Videos + internal static func previewSenderSendVideoFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendVideo_few", p1) + } + /// Send %d Videos + internal static func previewSenderSendVideoMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendVideo_many", p1) + } + /// Send Video + internal static var previewSenderSendVideoOne: String { return L10n.tr("Localizable", "PreviewSender.SendVideo_one") } + /// Send %d Videos + internal static func previewSenderSendVideoOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendVideo_other", p1) + } + /// Send %d Videos + internal static func previewSenderSendVideoTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendVideo_two", p1) + } + /// Send %d Videos + internal static func previewSenderSendVideoZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "PreviewSender.SendVideo_zero", p1) + } + /// Sorry, you can't create a group with these users due to their privacy settings. + internal static var privacyGroupsAndChannelsInviteToChannelMultipleError: String { return L10n.tr("Localizable", "Privacy.GroupsAndChannels.InviteToChannelMultipleError") } + /// Automatically archive and mute new chats, groups and channels from non-contacts. + internal static var privacyAndSecurityAutoArchiveDesc: String { return L10n.tr("Localizable", "PrivacyAndSecurity.AutoArchiveDesc") } + /// NEW CHATS FROM UNKNOWN USERS + internal static var privacyAndSecurityAutoArchiveHeader: String { return L10n.tr("Localizable", "PrivacyAndSecurity.AutoArchiveHeader") } + /// Archive and Mute + internal static var privacyAndSecurityAutoArchiveText: String { return L10n.tr("Localizable", "PrivacyAndSecurity.AutoArchiveText") } + /// %@ users + internal static func privacyAndSecurityBlockedUsers(_ p1: String) -> String { + return L10n.tr("Localizable", "PrivacyAndSecurity.BlockedUsers", p1) + } + /// Clear Cloud Drafts + internal static var privacyAndSecurityClearCloudDrafts: String { return L10n.tr("Localizable", "PrivacyAndSecurity.ClearCloudDrafts") } + /// CHATS + internal static var privacyAndSecurityClearCloudDraftsHeader: String { return L10n.tr("Localizable", "PrivacyAndSecurity.ClearCloudDraftsHeader") } + /// Display sensitive media in public channels on all your Telegram devices. + internal static var privacyAndSecuritySensitiveDesc: String { return L10n.tr("Localizable", "PrivacyAndSecurity.SensitiveDesc") } + /// SENSITIVE CONTENT + internal static var privacyAndSecuritySensitiveHeader: String { return L10n.tr("Localizable", "PrivacyAndSecurity.SensitiveHeader") } + /// Disable filtering + internal static var privacyAndSecuritySensitiveText: String { return L10n.tr("Localizable", "PrivacyAndSecurity.SensitiveText") } + /// CONNECTED WEBSITES + internal static var privacyAndSecurityWebAuthorizationHeader: String { return L10n.tr("Localizable", "PrivacyAndSecurity.WebAuthorizationHeader") } + /// Are you sure you want to clear all cloud drafts? + internal static var privacyAndSecurityConfirmClearCloudDrafts: String { return L10n.tr("Localizable", "PrivacyAndSecurity.Confirm.ClearCloudDrafts") } + /// Off + internal static var privacyAndSecurityItemOff: String { return L10n.tr("Localizable", "PrivacyAndSecurity.Item.Off") } + /// On + internal static var privacyAndSecurityItemOn: String { return L10n.tr("Localizable", "PrivacyAndSecurity.Item.On") } + /// Link previews will be generated on Telegram servers. We do not store data about the links you send. + internal static var privacyAndSecuritySecretChatWebPreviewDesc: String { return L10n.tr("Localizable", "PrivacyAndSecurity.SecretChatWebPreview.Desc") } + /// SECRET CHAT + internal static var privacyAndSecuritySecretChatWebPreviewHeader: String { return L10n.tr("Localizable", "PrivacyAndSecurity.SecretChatWebPreview.Header") } + /// Link Previews + internal static var privacyAndSecuritySecretChatWebPreviewText: String { return L10n.tr("Localizable", "PrivacyAndSecurity.SecretChatWebPreview.Text") } + /// Users who add your number to their contacts will see it on Telegram only if they are your contacts. + internal static var privacyPhoneNumberSettingsCustomDisabledHelp: String { return L10n.tr("Localizable", "PrivacyPhoneNumberSettings.CustomDisabledHelp") } + /// WHO CAN FIND ME BY MY NUMBER + internal static var privacyPhoneNumberSettingsDiscoveryHeader: String { return L10n.tr("Localizable", "PrivacyPhoneNumberSettings.DiscoveryHeader") } /// Active Sessions - case privacySettingsActiveSessions + internal static var privacySettingsActiveSessions: String { return L10n.tr("Localizable", "PrivacySettings.ActiveSessions") } /// Blocked Users - case privacySettingsBlockedUsers - /// Groups - case privacySettingsGroups + internal static var privacySettingsBlockedUsers: String { return L10n.tr("Localizable", "PrivacySettings.BlockedUsers") } + /// If Away For + internal static var privacySettingsDeleteAccount: String { return L10n.tr("Localizable", "PrivacySettings.DeleteAccount") } + /// If you do not come online at least once within this period, your account will be deleted along with all messages and contacts. + internal static var privacySettingsDeleteAccountDescription: String { return L10n.tr("Localizable", "PrivacySettings.DeleteAccountDescription") } + /// DELETE MY ACCOUNT + internal static var privacySettingsDeleteAccountHeader: String { return L10n.tr("Localizable", "PrivacySettings.DeleteAccountHeader") } + /// Forwarded Messages + internal static var privacySettingsForwards: String { return L10n.tr("Localizable", "PrivacySettings.Forwards") } + /// %d + internal static func privacySettingsGroupMembersCountCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "PrivacySettings.GroupMembersCount_countable", p1) + } + /// %d members + internal static func privacySettingsGroupMembersCountFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "PrivacySettings.GroupMembersCount_few", p1) + } + /// %d members + internal static func privacySettingsGroupMembersCountMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "PrivacySettings.GroupMembersCount_many", p1) + } + /// %d member + internal static func privacySettingsGroupMembersCountOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "PrivacySettings.GroupMembersCount_one", p1) + } + /// %d members + internal static func privacySettingsGroupMembersCountOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "PrivacySettings.GroupMembersCount_other", p1) + } + /// %d members + internal static func privacySettingsGroupMembersCountTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "PrivacySettings.GroupMembersCount_two", p1) + } + /// %d members + internal static func privacySettingsGroupMembersCountZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "PrivacySettings.GroupMembersCount_zero", p1) + } + /// Groups and Channels + internal static var privacySettingsGroups: String { return L10n.tr("Localizable", "PrivacySettings.Groups") } /// Last Seen - case privacySettingsLastSeen + internal static var privacySettingsLastSeen: String { return L10n.tr("Localizable", "PrivacySettings.LastSeen") } + /// My Contacts (-%@) + internal static func privacySettingsLastSeenContactsMinus(_ p1: String) -> String { + return L10n.tr("Localizable", "PrivacySettings.LastSeenContactsMinus", p1) + } + /// My Contacts (-%@, +%@) + internal static func privacySettingsLastSeenContactsMinusPlus(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "PrivacySettings.LastSeenContactsMinusPlus", p1, p2) + } + /// My Contacts (+%@) + internal static func privacySettingsLastSeenContactsPlus(_ p1: String) -> String { + return L10n.tr("Localizable", "PrivacySettings.LastSeenContactsPlus", p1) + } + /// Everybody (-%@) + internal static func privacySettingsLastSeenEverybodyMinus(_ p1: String) -> String { + return L10n.tr("Localizable", "PrivacySettings.LastSeenEverybodyMinus", p1) + } + /// Nobody (+%@) + internal static func privacySettingsLastSeenNobodyPlus(_ p1: String) -> String { + return L10n.tr("Localizable", "PrivacySettings.LastSeenNobodyPlus", p1) + } /// Passcode - case privacySettingsPasscode + internal static var privacySettingsPasscode: String { return L10n.tr("Localizable", "PrivacySettings.Passcode") } + /// Phone Number + internal static var privacySettingsPhoneNumber: String { return L10n.tr("Localizable", "PrivacySettings.PhoneNumber") } /// PRIVACY - case privacySettingsPrivacyHeader + internal static var privacySettingsPrivacyHeader: String { return L10n.tr("Localizable", "PrivacySettings.PrivacyHeader") } + /// Profile Photo + internal static var privacySettingsProfilePhoto: String { return L10n.tr("Localizable", "PrivacySettings.ProfilePhoto") } /// CONNECTION TYPE - case privacySettingsProxyHeader + internal static var privacySettingsProxyHeader: String { return L10n.tr("Localizable", "PrivacySettings.ProxyHeader") } /// SECURITY - case privacySettingsSecurityHeader + internal static var privacySettingsSecurityHeader: String { return L10n.tr("Localizable", "PrivacySettings.SecurityHeader") } /// Two-Step Verification - case privacySettingsTwoStepVerification + internal static var privacySettingsTwoStepVerification: String { return L10n.tr("Localizable", "PrivacySettings.TwoStepVerification") } /// Use Proxy - case privacySettingsUseProxy + internal static var privacySettingsUseProxy: String { return L10n.tr("Localizable", "PrivacySettings.UseProxy") } /// Voice Calls - case privacySettingsVoiceCalls + internal static var privacySettingsVoiceCalls: String { return L10n.tr("Localizable", "PrivacySettings.VoiceCalls") } /// Add New - case privacySettingsPeerSelectAddNew + internal static var privacySettingsPeerSelectAddNew: String { return L10n.tr("Localizable", "PrivacySettings.PeerSelect.AddNew") } + /// Add Users or Groups + internal static var privacySettingsPeerSelectAddUserOrGroup: String { return L10n.tr("Localizable", "PrivacySettings.PeerSelect.AddUserOrGroup") } /// Add Users - case privacySettingsControllerAddUsers + internal static var privacySettingsControllerAddUsers: String { return L10n.tr("Localizable", "PrivacySettingsController.AddUsers") } /// Always Allow - case privacySettingsControllerAlwaysAllow + internal static var privacySettingsControllerAlwaysAllow: String { return L10n.tr("Localizable", "PrivacySettingsController.AlwaysAllow") } /// Always Share - case privacySettingsControllerAlwaysShare + internal static var privacySettingsControllerAlwaysShare: String { return L10n.tr("Localizable", "PrivacySettingsController.AlwaysShare") } /// Always Share With - case privacySettingsControllerAlwaysShareWith + internal static var privacySettingsControllerAlwaysShareWith: String { return L10n.tr("Localizable", "PrivacySettingsController.AlwaysShareWith") } /// Everybody - case privacySettingsControllerEverbody + internal static var privacySettingsControllerEverbody: String { return L10n.tr("Localizable", "PrivacySettingsController.Everbody") } /// You can restrict who can add you to groups and channels with granular precision. - case privacySettingsControllerGroupDescription + internal static var privacySettingsControllerGroupDescription: String { return L10n.tr("Localizable", "PrivacySettingsController.GroupDescription") } /// WHO CAN ADD ME TO GROUP CHATS - case privacySettingsControllerGroupHeader + internal static var privacySettingsControllerGroupHeader: String { return L10n.tr("Localizable", "PrivacySettingsController.GroupHeader") } /// Last Seen - case privacySettingsControllerHeader + internal static var privacySettingsControllerHeader: String { return L10n.tr("Localizable", "PrivacySettingsController.Header") } /// 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). - case privacySettingsControllerLastSeenDescription + internal static var privacySettingsControllerLastSeenDescription: String { return L10n.tr("Localizable", "PrivacySettingsController.LastSeenDescription") } /// WHO CAN SEE MY TIMESTAMP - case privacySettingsControllerLastSeenHeader + internal static var privacySettingsControllerLastSeenHeader: String { return L10n.tr("Localizable", "PrivacySettingsController.LastSeenHeader") } /// My Contacts - case privacySettingsControllerMyContacts + internal static var privacySettingsControllerMyContacts: String { return L10n.tr("Localizable", "PrivacySettingsController.MyContacts") } /// Never Allow - case privacySettingsControllerNeverAllow + internal static var privacySettingsControllerNeverAllow: String { return L10n.tr("Localizable", "PrivacySettingsController.NeverAllow") } /// Never Share - case privacySettingsControllerNeverShare + internal static var privacySettingsControllerNeverShare: String { return L10n.tr("Localizable", "PrivacySettingsController.NeverShare") } /// Never Share With - case privacySettingsControllerNeverShareWith + internal static var privacySettingsControllerNeverShareWith: String { return L10n.tr("Localizable", "PrivacySettingsController.NeverShareWith") } /// Nobody - case privacySettingsControllerNobody + internal static var privacySettingsControllerNobody: String { return L10n.tr("Localizable", "PrivacySettingsController.Nobody") } /// These settings will override the values above. - case privacySettingsControllerPeerInfo + internal static var privacySettingsControllerPeerInfo: String { return L10n.tr("Localizable", "PrivacySettingsController.PeerInfo") } /// You can restrict who can call you with granular precision. - case privacySettingsControllerPhoneCallDescription + internal static var privacySettingsControllerPhoneCallDescription: String { return L10n.tr("Localizable", "PrivacySettingsController.PhoneCallDescription") } /// WHO CAN CALL ME - case privacySettingsControllerPhoneCallHeader + internal static var privacySettingsControllerPhoneCallHeader: String { return L10n.tr("Localizable", "PrivacySettingsController.PhoneCallHeader") } /// %d - case privacySettingsControllerUserCountCountable(Int) + internal static func privacySettingsControllerUserCountCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "PrivacySettingsController.UserCount_countable", p1) + } /// %d users - case privacySettingsControllerUserCountFew(Int) + internal static func privacySettingsControllerUserCountFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "PrivacySettingsController.UserCount_few", p1) + } /// %d users - case privacySettingsControllerUserCountMany(Int) + internal static func privacySettingsControllerUserCountMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "PrivacySettingsController.UserCount_many", p1) + } /// %d user - case privacySettingsControllerUserCountOne(Int) + internal static func privacySettingsControllerUserCountOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "PrivacySettingsController.UserCount_one", p1) + } /// %d users - case privacySettingsControllerUserCountOther(Int) + internal static func privacySettingsControllerUserCountOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "PrivacySettingsController.UserCount_other", p1) + } /// %d users - case privacySettingsControllerUserCountTwo(Int) - /// %d user - case privacySettingsControllerUserCountZero(Int) + internal static func privacySettingsControllerUserCountTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "PrivacySettingsController.UserCount_two", p1) + } + /// %d users + internal static func privacySettingsControllerUserCountZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "PrivacySettingsController.UserCount_zero", p1) + } + /// When forwarded to other chats, messages you send will not link back to your account. + internal static var privacySettingsControllerForwardsCustomHelp: String { return L10n.tr("Localizable", "PrivacySettingsController.Forwards.CustomHelp") } + /// WHO CAN FORWARD MY MESSAGES + internal static var privacySettingsControllerForwardsWhoCanForward: String { return L10n.tr("Localizable", "PrivacySettingsController.Forwards.WhoCanForward") } + /// Always Allow + internal static var privacySettingsControllerForwardsAlwaysAllowTitle: String { return L10n.tr("Localizable", "PrivacySettingsController.Forwards.AlwaysAllow.Title") } + /// Never Allow + internal static var privacySettingsControllerForwardsNeverAllowTitle: String { return L10n.tr("Localizable", "PrivacySettingsController.Forwards.NeverAllow.Title") } + /// Always + internal static var privacySettingsControllerP2pAlways: String { return L10n.tr("Localizable", "PrivacySettingsController.P2p.Always") } + /// My Contacts + internal static var privacySettingsControllerP2pContacts: String { return L10n.tr("Localizable", "PrivacySettingsController.P2p.Contacts") } + /// Disabling peer-to-peer will relay all calls through Telegram servers to avoid revealing your IP address, but will slighly decrease audio quality + internal static var privacySettingsControllerP2pDesc: String { return L10n.tr("Localizable", "PrivacySettingsController.P2p.Desc") } + /// PEER TO PEER + internal static var privacySettingsControllerP2pHeader: String { return L10n.tr("Localizable", "PrivacySettingsController.P2p.Header") } + /// Never + internal static var privacySettingsControllerP2pNever: String { return L10n.tr("Localizable", "PrivacySettingsController.P2p.Never") } + /// Users who already have your number saved in the contacts will also see it on Telegram. + internal static var privacySettingsControllerPhoneNumberCustomHelp: String { return L10n.tr("Localizable", "PrivacySettingsController.PhoneNumber.CustomHelp") } + /// WHO CAN SEE MY PHONE NUMBER + internal static var privacySettingsControllerPhoneNumberWhoCanSeePhoneNumber: String { return L10n.tr("Localizable", "PrivacySettingsController.PhoneNumber.WhoCanSeePhoneNumber") } + /// Always Share With + internal static var privacySettingsControllerPhoneNumberAlwaysAllowTitle: String { return L10n.tr("Localizable", "PrivacySettingsController.PhoneNumber.AlwaysAllow.Title") } + /// Never Share + internal static var privacySettingsControllerPhoneNumberNeverAllowTitle: String { return L10n.tr("Localizable", "PrivacySettingsController.PhoneNumber.NeverAllow.Title") } + /// You can restrict who can see your profile photo with granular precision. + internal static var privacySettingsControllerProfilePhotoCustomHelp: String { return L10n.tr("Localizable", "PrivacySettingsController.ProfilePhoto.CustomHelp") } + /// WHO CAN SEE MY PROFILE PHOTO + internal static var privacySettingsControllerProfilePhotoWhoCanSeeMyPhoto: String { return L10n.tr("Localizable", "PrivacySettingsController.ProfilePhoto.WhoCanSeeMyPhoto") } + /// Always Share With + internal static var privacySettingsControllerProfilePhotoAlwaysShareWithTitle: String { return L10n.tr("Localizable", "PrivacySettingsController.ProfilePhoto.AlwaysShareWith.Title") } + /// Never Share With + internal static var privacySettingsControllerProfilePhotoNeverShareWithTitle: String { return L10n.tr("Localizable", "PrivacySettingsController.ProfilePhoto.NeverShareWith.Title") } + /// Cancel + internal static var privateChannelPeekCancel: String { return L10n.tr("Localizable", "PrivateChannel.Peek.Cancel") } + /// Join Channel + internal static var privateChannelPeekHeader: String { return L10n.tr("Localizable", "PrivateChannel.Peek.Header") } + /// Join Channel + internal static var privateChannelPeekOK: String { return L10n.tr("Localizable", "PrivateChannel.Peek.OK") } + /// This channel is private. Please join it to continue viewing its content. + internal static var privateChannelPeekText: String { return L10n.tr("Localizable", "PrivateChannel.Peek.Text") } /// Are you sure you want to disable proxy server %@? - case proxyForceDisable(String) - /// Are you sure you want to enable this proxy? - case proxyForceEnableHeader - /// You can change your proxy server later in the Settings (Privacy and Security). - case proxyForceEnableText + internal static func proxyForceDisable(_ p1: String) -> String { + return L10n.tr("Localizable", "Proxy.ForceDisable", p1) + } + /// Connect + internal static var proxyForceEnableConnect: String { return L10n.tr("Localizable", "Proxy.ForceEnable.Connect") } + /// Enable Proxy + internal static var proxyForceEnableEnable: String { return L10n.tr("Localizable", "Proxy.ForceEnable.Enable") } + /// Do you want to add this proxy? + internal static var proxyForceEnableHeader1: String { return L10n.tr("Localizable", "Proxy.ForceEnable.Header1") } + /// This proxy may display a sponsored channel in your chat list. This doesn't reveal any of your Telegram traffic. + internal static var proxyForceEnableMTPDesc: String { return L10n.tr("Localizable", "Proxy.ForceEnable.MTPDesc") } + /// Add Proxy + internal static var proxyForceEnableOK: String { return L10n.tr("Localizable", "Proxy.ForceEnable.OK") } + /// You can change your proxy server later in Settings > Privacy and Security. + internal static var proxyForceEnableText: String { return L10n.tr("Localizable", "Proxy.ForceEnable.Text") } /// Server: %@ - case proxyForceEnableTextIP(String) + internal static func proxyForceEnableTextIP(_ p1: String) -> String { + return L10n.tr("Localizable", "Proxy.ForceEnable.Text.IP", p1) + } /// Password: %@ - case proxyForceEnableTextPassword(String) + internal static func proxyForceEnableTextPassword(_ p1: String) -> String { + return L10n.tr("Localizable", "Proxy.ForceEnable.Text.Password", p1) + } /// Port: %d - case proxyForceEnableTextPort(Int) + internal static func proxyForceEnableTextPort(_ p1: Int) -> String { + return L10n.tr("Localizable", "Proxy.ForceEnable.Text.Port", p1) + } + /// Secret: %@ + internal static func proxyForceEnableTextSecret(_ p1: String) -> String { + return L10n.tr("Localizable", "Proxy.ForceEnable.Text.Secret", p1) + } /// Username: %@ - case proxyForceEnableTextUsername(String) + internal static func proxyForceEnableTextUsername(_ p1: String) -> String { + return L10n.tr("Localizable", "Proxy.ForceEnable.Text.Username", p1) + } + /// Add Proxy + internal static var proxySettingsAddProxy: String { return L10n.tr("Localizable", "ProxySettings.AddProxy") } /// Connection - case proxySettingsConnectionHeader + internal static var proxySettingsConnectionHeader: String { return L10n.tr("Localizable", "ProxySettings.ConnectionHeader") } + /// Share proxy link + internal static var proxySettingsCopyLink: String { return L10n.tr("Localizable", "ProxySettings.CopyLink") } /// CREDENTIALS (OPTIONAL) - case proxySettingsCredentialsHeader + internal static var proxySettingsCredentialsHeader: String { return L10n.tr("Localizable", "ProxySettings.CredentialsHeader") } /// Disabled - case proxySettingsDisabled + internal static var proxySettingsDisabled: String { return L10n.tr("Localizable", "ProxySettings.Disabled") } + /// Proxy + internal static var proxySettingsEnable: String { return L10n.tr("Localizable", "ProxySettings.Enable") } /// If your clipboard contains socks5-link (**t.me/socks?server=127.0.0.1&port=80**) it will apply immediately - case proxySettingsExportDescription + internal static var proxySettingsExportDescription: String { return L10n.tr("Localizable", "ProxySettings.ExportDescription") } /// Export link from clipboard - case proxySettingsExportLink + internal static var proxySettingsExportLink: String { return L10n.tr("Localizable", "ProxySettings.ExportLink") } + /// Incorrect secret. Please try again. + internal static var proxySettingsIncorrectSecret: String { return L10n.tr("Localizable", "ProxySettings.IncorrectSecret") } + /// MTPROTO + internal static var proxySettingsMTP: String { return L10n.tr("Localizable", "ProxySettings.MTP") } /// Password - case proxySettingsPassword + internal static var proxySettingsPassword: String { return L10n.tr("Localizable", "ProxySettings.Password") } /// Port - case proxySettingsPort + internal static var proxySettingsPort: String { return L10n.tr("Localizable", "ProxySettings.Port") } /// Proxy settings not found in clipboard. - case proxySettingsProxyNotFound + internal static var proxySettingsProxyNotFound: String { return L10n.tr("Localizable", "ProxySettings.ProxyNotFound") } /// Save - case proxySettingsSave + internal static var proxySettingsSave: String { return L10n.tr("Localizable", "ProxySettings.Save") } + /// Secret + internal static var proxySettingsSecret: String { return L10n.tr("Localizable", "ProxySettings.Secret") } /// Server - case proxySettingsServer + internal static var proxySettingsServer: String { return L10n.tr("Localizable", "ProxySettings.Server") } /// Share this link with friends to circumvent censorship in your country - case proxySettingsShare + internal static var proxySettingsShare: String { return L10n.tr("Localizable", "ProxySettings.Share") } + /// Share Proxy List + internal static var proxySettingsShareProxyList: String { return L10n.tr("Localizable", "ProxySettings.ShareProxyList") } /// SOCKS5 - case proxySettingsSocks5 + internal static var proxySettingsSocks5: String { return L10n.tr("Localizable", "ProxySettings.Socks5") } + /// Proxy Settings + internal static var proxySettingsTitle: String { return L10n.tr("Localizable", "ProxySettings.Title") } + /// Proxy Type + internal static var proxySettingsType: String { return L10n.tr("Localizable", "ProxySettings.Type") } + /// Use for Calls + internal static var proxySettingsUseForCalls: String { return L10n.tr("Localizable", "ProxySettings.UseForCalls") } /// Username - case proxySettingsUsername + internal static var proxySettingsUsername: String { return L10n.tr("Localizable", "ProxySettings.Username") } + /// available (ping: %@ ms) + internal static func proxySettingsItemAvailable(_ p1: String) -> String { + return L10n.tr("Localizable", "ProxySettings.Item.Available", p1) + } + /// checking + internal static var proxySettingsItemChecking: String { return L10n.tr("Localizable", "ProxySettings.Item.Checking") } + /// connected + internal static var proxySettingsItemConnected: String { return L10n.tr("Localizable", "ProxySettings.Item.Connected") } + /// connected (ping: %@ ms) + internal static func proxySettingsItemConnectedPing(_ p1: String) -> String { + return L10n.tr("Localizable", "ProxySettings.Item.ConnectedPing", p1) + } + /// last connection %@ + internal static func proxySettingsItemLastConnection(_ p1: String) -> String { + return L10n.tr("Localizable", "ProxySettings.Item.LastConnection", p1) + } + /// unavailable + internal static var proxySettingsItemNeverConnected: String { return L10n.tr("Localizable", "ProxySettings.Item.NeverConnected") } + /// The proxy may display a sponsored channel in your chat list. This doesn't reveal any of your Telegram traffic. + internal static var proxySettingsMtpSponsor: String { return L10n.tr("Localizable", "ProxySettings.Mtp.Sponsor") } + /// You or your friends can add this proxy by scanning this code with phone or in-app camera. + internal static var proxySettingsQRText: String { return L10n.tr("Localizable", "ProxySettings.QR.Text") } /// Preview - case quickLookPreview - /// **tab** or **↑ ↓** to navigate, **⮐** to select, **esc** to dismiss - case quickSwitcherDescription + internal static var quickLookPreview: String { return L10n.tr("Localizable", "QuickLook.Preview") } + /// **TAB** or **↑ ↓** to navigate, **⮐** to select, **ESC** to dismiss + internal static var quickSwitcherDescription: String { return L10n.tr("Localizable", "QuickSwitcher.Description") } /// Popular - case quickSwitcherPopular + internal static var quickSwitcherPopular: String { return L10n.tr("Localizable", "QuickSwitcher.Popular") } /// Recent - case quickSwitcherRecently - /// Telegram - case qvCM9Y7gTitle + internal static var quickSwitcherRecently: String { return L10n.tr("Localizable", "QuickSwitcher.Recently") } /// Zoom - case r4oN2Eq4Title - /// Check Spelling While Typing - case rbDRhWINTitle + internal static var r4oN2Eq4Title: String { return L10n.tr("Localizable", "R4o-n2-Eq4.title") } + /// Delete + internal static var recentCallsDelete: String { return L10n.tr("Localizable", "RecentCalls.Delete") } + /// Are you sure you want to delete call? + internal static var recentCallsDeleteCalls: String { return L10n.tr("Localizable", "RecentCalls.DeleteCalls") } + /// Delete for me and %@ + internal static func recentCallsDeleteForMeAnd(_ p1: String) -> String { + return L10n.tr("Localizable", "RecentCalls.DeleteForMeAnd", p1) + } + /// Delete + internal static var recentCallsDeleteHeader: String { return L10n.tr("Localizable", "RecentCalls.DeleteHeader") } /// Your recent calls will appear here - case recentCallsEmpty + internal static var recentCallsEmpty: String { return L10n.tr("Localizable", "RecentCalls.Empty") } + /// These devices have no access to your account. The code was entered correctly, but no correct password was given. + internal static var recentSessionsIncompleteAttemptDesc: String { return L10n.tr("Localizable", "RecentSessions.IncompleteAttemptDesc") } + /// INCOMPLETE LOGIN ATTEMPTS + internal static var recentSessionsIncompleteAttemptHeader: String { return L10n.tr("Localizable", "RecentSessions.IncompleteAttemptHeader") } /// Revoke - case recentSessionsRevoke + internal static var recentSessionsRevoke: String { return L10n.tr("Localizable", "RecentSessions.Revoke") } /// Do you want to terminate this session? - case recentSessionsConfirmRevoke + internal static var recentSessionsConfirmRevoke: String { return L10n.tr("Localizable", "RecentSessions.Confirm.Revoke") } /// Are you sure you want to terminate all other sessions? - case recentSessionsConfirmTerminateOthers + internal static var recentSessionsConfirmTerminateOthers: String { return L10n.tr("Localizable", "RecentSessions.Confirm.TerminateOthers") } + /// For security reasons, you can't terminate older sessions from a device that you've just connected. Please use an earlier connection or wait for a few hours. + internal static var recentSessionsErrorFreshReset: String { return L10n.tr("Localizable", "RecentSessions.Error.FreshReset") } + /// Please enter any additional details relevant for your report. + internal static var reportAdditionText: String { return L10n.tr("Localizable", "Report.AdditionText") } + /// Report + internal static var reportAdditionTextButton: String { return L10n.tr("Localizable", "Report.AdditionText.Button") } + /// Additional details... + internal static var reportAdditionTextPlaceholder: String { return L10n.tr("Localizable", "Report.AdditionText.Placeholder") } + /// Child Abuse + internal static var reportReasonChildAbuse: String { return L10n.tr("Localizable", "ReportReason.ChildAbuse") } + /// Copyright + internal static var reportReasonCopyright: String { return L10n.tr("Localizable", "ReportReason.Copyright") } + /// Fake + internal static var reportReasonFake: String { return L10n.tr("Localizable", "ReportReason.Fake") } + /// Other + internal static var reportReasonOther: String { return L10n.tr("Localizable", "ReportReason.Other") } /// Pornography - case reportReasonPorno + internal static var reportReasonPorno: String { return L10n.tr("Localizable", "ReportReason.Porno") } + /// Report + internal static var reportReasonReport: String { return L10n.tr("Localizable", "ReportReason.Report") } /// Spam - case reportReasonSpam + internal static var reportReasonSpam: String { return L10n.tr("Localizable", "ReportReason.Spam") } /// Violence - case reportReasonViolence - /// Smart Dashes - case rgMF4YcnTitle + internal static var reportReasonViolence: String { return L10n.tr("Localizable", "ReportReason.Violence") } + /// Description + internal static var reportReasonOtherPlaceholder: String { return L10n.tr("Localizable", "ReportReason.Other.Placeholder") } + /// Settings + internal static var requestAccesErrorConirmSettings: String { return L10n.tr("Localizable", "RequestAcces.Error.Conirm.Settings") } + /// Telegram needs access to your microphone to make calls + internal static var requestAccesErrorHaveNotAccessCall: String { return L10n.tr("Localizable", "RequestAcces.Error.HaveNotAccess.Call") } + /// Telegram needs access to your microphone and camera to record video messages. + internal static var requestAccesErrorHaveNotAccessVideoMessages: String { return L10n.tr("Localizable", "RequestAcces.Error.HaveNotAccess.VideoMessages") } + /// Telegram needs access to your microphone to record voice messages. + internal static var requestAccesErrorHaveNotAccessVoiceMessages: String { return L10n.tr("Localizable", "RequestAcces.Error.HaveNotAccess.VoiceMessages") } /// Select All - case ruw6mB2mTitle + internal static var ruw6mB2mTitle: String { return L10n.tr("Localizable", "Ruw-6m-B2m.title") } + /// Send on %@ at %@ + internal static func scheduleSendDate(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Schedule.SendDate", p1, p2) + } + /// Send today at %@ + internal static func scheduleSendToday(_ p1: String) -> String { + return L10n.tr("Localizable", "Schedule.SendToday", p1) + } + /// Send When Online + internal static var scheduleSendWhenOnline: String { return L10n.tr("Localizable", "Schedule.SendWhenOnline") } + /// at + internal static var scheduleControllerAt: String { return L10n.tr("Localizable", "ScheduleController.at") } + /// Schedule Message + internal static var scheduleControllerTitle: String { return L10n.tr("Localizable", "ScheduleController.Title") } + /// Are you sure you want to clear your search history? + internal static var searchConfirmClearHistory: String { return L10n.tr("Localizable", "Search.Confirm.ClearHistory") } + /// Clear Filter + internal static var searchFilterClearFilter: String { return L10n.tr("Localizable", "Search.Filter.ClearFilter") } + /// Files + internal static var searchFilterFiles: String { return L10n.tr("Localizable", "Search.Filter.Files") } + /// GIFs + internal static var searchFilterGIFs: String { return L10n.tr("Localizable", "Search.Filter.GIFs") } + /// Links + internal static var searchFilterLinks: String { return L10n.tr("Localizable", "Search.Filter.Links") } + /// Music + internal static var searchFilterMusic: String { return L10n.tr("Localizable", "Search.Filter.Music") } + /// Photos + internal static var searchFilterPhotos: String { return L10n.tr("Localizable", "Search.Filter.Photos") } + /// Videos + internal static var searchFilterVideos: String { return L10n.tr("Localizable", "Search.Filter.Videos") } + /// Voice + internal static var searchFilterVoice: String { return L10n.tr("Localizable", "Search.Filter.Voice") } + /// %@ %d + internal static func searchGlobalChannel1Countable(_ p1: String, _ p2: Int) -> String { + return L10n.tr("Localizable", "Search.Global.Channel1_countable", p1, p2) + } + /// %@, %d subscribers + internal static func searchGlobalChannel1Few(_ p1: String, _ p2: Int) -> String { + return L10n.tr("Localizable", "Search.Global.Channel1_few", p1, p2) + } + /// %@, %d subscribers + internal static func searchGlobalChannel1Many(_ p1: String, _ p2: Int) -> String { + return L10n.tr("Localizable", "Search.Global.Channel1_many", p1, p2) + } + /// %@, %d subscriber + internal static func searchGlobalChannel1One(_ p1: String, _ p2: Int) -> String { + return L10n.tr("Localizable", "Search.Global.Channel1_one", p1, p2) + } + /// %@, %d subscribers + internal static func searchGlobalChannel1Other(_ p1: String, _ p2: Int) -> String { + return L10n.tr("Localizable", "Search.Global.Channel1_other", p1, p2) + } + /// %@, %d subscribers + internal static func searchGlobalChannel1Two(_ p1: String, _ p2: Int) -> String { + return L10n.tr("Localizable", "Search.Global.Channel1_two", p1, p2) + } + /// %@, %d subscribers + internal static func searchGlobalChannel1Zero(_ p1: String, _ p2: Int) -> String { + return L10n.tr("Localizable", "Search.Global.Channel1_zero", p1, p2) + } + /// %@ %d + internal static func searchGlobalGroup1Countable(_ p1: String, _ p2: Int) -> String { + return L10n.tr("Localizable", "Search.Global.Group1_countable", p1, p2) + } + /// %@, %d members + internal static func searchGlobalGroup1Few(_ p1: String, _ p2: Int) -> String { + return L10n.tr("Localizable", "Search.Global.Group1_few", p1, p2) + } + /// %@, %d members + internal static func searchGlobalGroup1Many(_ p1: String, _ p2: Int) -> String { + return L10n.tr("Localizable", "Search.Global.Group1_many", p1, p2) + } + /// %@, %d member + internal static func searchGlobalGroup1One(_ p1: String, _ p2: Int) -> String { + return L10n.tr("Localizable", "Search.Global.Group1_one", p1, p2) + } + /// %@, %d members + internal static func searchGlobalGroup1Other(_ p1: String, _ p2: Int) -> String { + return L10n.tr("Localizable", "Search.Global.Group1_other", p1, p2) + } + /// %@, %d members + internal static func searchGlobalGroup1Two(_ p1: String, _ p2: Int) -> String { + return L10n.tr("Localizable", "Search.Global.Group1_two", p1, p2) + } + /// %@, %d members + internal static func searchGlobalGroup1Zero(_ p1: String, _ p2: Int) -> String { + return L10n.tr("Localizable", "Search.Global.Group1_zero", p1, p2) + } + /// Articles + internal static var searchPopularArticles: String { return L10n.tr("Localizable", "Search.Popular.Articles") } + /// Delete + internal static var searchPopularDelete: String { return L10n.tr("Localizable", "Search.Popular.Delete") } + /// Saved + internal static var searchPopularSavedMessages: String { return L10n.tr("Localizable", "Search.Popular.SavedMessages") } /// contacts and chats - case searchSeparatorChatsAndContacts + internal static var searchSeparatorChatsAndContacts: String { return L10n.tr("Localizable", "Search.Separator.ChatsAndContacts") } /// global search - case searchSeparatorGlobalPeers + internal static var searchSeparatorGlobalPeers: String { return L10n.tr("Localizable", "Search.Separator.GlobalPeers") } /// messages - case searchSeparatorMessages + internal static var searchSeparatorMessages: String { return L10n.tr("Localizable", "Search.Separator.Messages") } /// People - case searchSeparatorPopular + internal static var searchSeparatorPopular: String { return L10n.tr("Localizable", "Search.Separator.Popular") } /// Recent - case searchSeparatorRecent + internal static var searchSeparatorRecent: String { return L10n.tr("Localizable", "Search.Separator.Recent") } /// Search - case searchFieldSearch + internal static var searchFieldSearch: String { return L10n.tr("Localizable", "SearchField.Search") } /// Off - case secretTimerOff + internal static var secretTimerOff: String { return L10n.tr("Localizable", "SecretTimer.Off") } + /// Sorry, your Telegram app is out of date and can’t handle this request. Please update Telegram. + internal static var secureIdAppVersionOutdated: String { return L10n.tr("Localizable", "SecureId.AppVersionOutdated") } + /// Please correct errors + internal static var secureIdCorrectErrors: String { return L10n.tr("Localizable", "SecureId.CorrectErrors") } + /// Delete Address + internal static var secureIdDeleteAddress: String { return L10n.tr("Localizable", "SecureId.DeleteAddress") } + /// Delete Document + internal static var secureIdDeleteIdentity: String { return L10n.tr("Localizable", "SecureId.DeleteIdentity") } + /// Delete Telegram Passport + internal static var secureIdDeletePassport: String { return L10n.tr("Localizable", "SecureId.DeletePassport") } + /// Email Address + internal static var secureIdEmail: String { return L10n.tr("Localizable", "SecureId.Email") } + /// Identity Document + internal static var secureIdIdentityDocument: String { return L10n.tr("Localizable", "SecureId.IdentityDocument") } + /// With Telegram Passport you can easily sign up for websites and services that require identity veritification.\n\nYour information, personal data, and documents are protected by end-to-end encryption. Nobody including Telegram, can access them without your permission. + internal static var secureIdInfo: String { return L10n.tr("Localizable", "SecureId.Info") } + /// Please log in to your account to use Telegram Passport + internal static var secureIdLoginText: String { return L10n.tr("Localizable", "SecureId.LoginText") } + /// Phone Number + internal static var secureIdPhoneNumber: String { return L10n.tr("Localizable", "SecureId.PhoneNumber") } + /// Password Recovery + internal static var secureIdRecoverPassword: String { return L10n.tr("Localizable", "SecureId.RecoverPassword") } + /// Delete Email Address? + internal static var secureIdRemoveEmail: String { return L10n.tr("Localizable", "SecureId.RemoveEmail") } + /// Delete Phone Number? + internal static var secureIdRemovePhoneNumber: String { return L10n.tr("Localizable", "SecureId.RemovePhoneNumber") } + /// Residential Address + internal static var secureIdResidentialAddress: String { return L10n.tr("Localizable", "SecureId.ResidentialAddress") } + /// Scan %d + internal static func secureIdScanNumber(_ p1: Int) -> String { + return L10n.tr("Localizable", "SecureId.ScanNumber", p1) + } + /// Upload Additional Scan + internal static var secureIdUploadAdditionalScan: String { return L10n.tr("Localizable", "SecureId.UploadAdditionalScan") } + /// Upload Scan + internal static var secureIdUploadScan: String { return L10n.tr("Localizable", "SecureId.UploadScan") } + /// You are sending your documents directly to **%@** and allowing their **%@** to send you messages. + internal static func secureIdAcceptHelp(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "SecureId.Accept.Help", p1, p2) + } + /// You accept the [Login Widget Example Privacy Policy](_applyPolicy_) and allow their **%@** to send you messages. + internal static func secureIdAcceptPolicy(_ p1: String) -> String { + return L10n.tr("Localizable", "SecureId.Accept.Policy", p1) + } + /// Add Bank Statement + internal static var secureIdAddBankStatement: String { return L10n.tr("Localizable", "SecureId.Add.BankStatement") } + /// Add Driver's License + internal static var secureIdAddDriverLicense: String { return L10n.tr("Localizable", "SecureId.Add.DriverLicense") } + /// Add Identity Card + internal static var secureIdAddID: String { return L10n.tr("Localizable", "SecureId.Add.ID") } + /// Add Internal Passport + internal static var secureIdAddInternalPassport: String { return L10n.tr("Localizable", "SecureId.Add.InternalPassport") } + /// Add Passport + internal static var secureIdAddPassport: String { return L10n.tr("Localizable", "SecureId.Add.Passport") } + /// Add Passport Registration + internal static var secureIdAddPassportRegistration: String { return L10n.tr("Localizable", "SecureId.Add.PassportRegistration") } + /// Add Personal Details + internal static var secureIdAddPersonalDetails: String { return L10n.tr("Localizable", "SecureId.Add.PersonalDetails") } + /// Add Rental Agreement + internal static var secureIdAddRentalAgreement: String { return L10n.tr("Localizable", "SecureId.Add.RentalAgreement") } + /// Add Residential Address + internal static var secureIdAddResidentialAddress: String { return L10n.tr("Localizable", "SecureId.Add.ResidentialAddress") } + /// Add Temporary Registration + internal static var secureIdAddTemporaryRegistration: String { return L10n.tr("Localizable", "SecureId.Add.TemporaryRegistration") } + /// Add Tenancy Agreement + internal static var secureIdAddTenancyAgreement: String { return L10n.tr("Localizable", "SecureId.Add.TenancyAgreement") } + /// Add Utility Bill + internal static var secureIdAddUtilityBill: String { return L10n.tr("Localizable", "SecureId.Add.UtilityBill") } + /// ADDRESS + internal static var secureIdAddressHeader: String { return L10n.tr("Localizable", "SecureId.Address.Header") } + /// %d + internal static func secureIdAddressScansCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "SecureId.Address.Scans_countable", p1) + } + /// %d scans + internal static func secureIdAddressScansFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "SecureId.Address.Scans_few", p1) + } + /// %d scans + internal static func secureIdAddressScansMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "SecureId.Address.Scans_many", p1) + } + /// %d scan + internal static func secureIdAddressScansOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "SecureId.Address.Scans_one", p1) + } + /// %d scans + internal static func secureIdAddressScansOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "SecureId.Address.Scans_other", p1) + } + /// %d scans + internal static func secureIdAddressScansTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "SecureId.Address.Scans_two", p1) + } + /// %d scans + internal static func secureIdAddressScansZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "SecureId.Address.Scans_zero", p1) + } + /// City + internal static var secureIdAddressCityInputPlaceholder: String { return L10n.tr("Localizable", "SecureId.Address.City.InputPlaceholder") } + /// City + internal static var secureIdAddressCityPlaceholder: String { return L10n.tr("Localizable", "SecureId.Address.City.Placeholder") } + /// Country + internal static var secureIdAddressCountryPlaceholder: String { return L10n.tr("Localizable", "SecureId.Address.Country.Placeholder") } + /// Postcode + internal static var secureIdAddressPostcodeInputPlaceholder: String { return L10n.tr("Localizable", "SecureId.Address.Postcode.InputPlaceholder") } + /// Postcode + internal static var secureIdAddressPostcodePlaceholder: String { return L10n.tr("Localizable", "SecureId.Address.Postcode.Placeholder") } + /// State/Republic/Region + internal static var secureIdAddressRegionInputPlaceholder: String { return L10n.tr("Localizable", "SecureId.Address.Region.InputPlaceholder") } + /// Region + internal static var secureIdAddressRegionPlaceholder: String { return L10n.tr("Localizable", "SecureId.Address.Region.Placeholder") } + /// Street and Number, PO Box + internal static var secureIdAddressStreetInputPlaceholder: String { return L10n.tr("Localizable", "SecureId.Address.Street.InputPlaceholder") } + /// Street + internal static var secureIdAddressStreetPlaceholder: String { return L10n.tr("Localizable", "SecureId.Address.Street.Placeholder") } + /// Apt, suite, unit, building, floor + internal static var secureIdAddressStreet1InputPlaceholder: String { return L10n.tr("Localizable", "SecureId.Address.Street1.InputPlaceholder") } + /// Are you sure you want to stop the authorization process? + internal static var secureIdConfirmCancel: String { return L10n.tr("Localizable", "SecureId.Confirm.Cancel") } + /// Delete Address + internal static var secureIdConfirmDeleteAddress: String { return L10n.tr("Localizable", "SecureId.Confirm.DeleteAddress") } + /// Are you sure you want to delete this document? + internal static var secureIdConfirmDeleteDocument: String { return L10n.tr("Localizable", "SecureId.Confirm.DeleteDocument") } + /// Please create a password to protect your passport info. You will also be asked to enter it when you log in to Telegram. + internal static var secureIdCreatePasswordDescription: String { return L10n.tr("Localizable", "SecureId.CreatePassword.Description") } + /// PASSWORD + internal static var secureIdCreatePasswordHeader: String { return L10n.tr("Localizable", "SecureId.CreatePassword.Header") } + /// Please create a password which will be used to encrypt your personal data.\n\nThis password will also be required whenever you log in to Telegram on a new device. + internal static var secureIdCreatePasswordIntro: String { return L10n.tr("Localizable", "SecureId.CreatePassword.Intro") } + /// Enter your password + internal static var secureIdCreatePasswordPasswordInputPlaceholder: String { return L10n.tr("Localizable", "SecureId.CreatePassword.PasswordInputPlaceholder") } + /// Password + internal static var secureIdCreatePasswordPasswordPlaceholder: String { return L10n.tr("Localizable", "SecureId.CreatePassword.PasswordPlaceholder") } + /// Re-Enter your password + internal static var secureIdCreatePasswordRePasswordInputPlaceholder: String { return L10n.tr("Localizable", "SecureId.CreatePassword.RePasswordInputPlaceholder") } + /// Password & E-Mail + internal static var secureIdCreatePasswordTitle: String { return L10n.tr("Localizable", "SecureId.CreatePassword.Title") } + /// Please add your valid e-mail. It is the only way to recover a forgotten password. + internal static var secureIdCreatePasswordEmailDescription: String { return L10n.tr("Localizable", "SecureId.CreatePassword.Email.Description") } + /// RECOVERY E-MAIL + internal static var secureIdCreatePasswordEmailHeader: String { return L10n.tr("Localizable", "SecureId.CreatePassword.Email.Header") } + /// Your E-Mail + internal static var secureIdCreatePasswordEmailInputPlaceholder: String { return L10n.tr("Localizable", "SecureId.CreatePassword.Email.InputPlaceholder") } + /// E-Mail + internal static var secureIdCreatePasswordEmailPlaceholder: String { return L10n.tr("Localizable", "SecureId.CreatePassword.Email.Placeholder") } + /// HINT + internal static var secureIdCreatePasswordHintHeader: String { return L10n.tr("Localizable", "SecureId.CreatePassword.Hint.Header") } + /// Hint for your password + internal static var secureIdCreatePasswordHintInputPlaceholder: String { return L10n.tr("Localizable", "SecureId.CreatePassword.Hint.InputPlaceholder") } + /// Hint + internal static var secureIdCreatePasswordHintPlaceholder: String { return L10n.tr("Localizable", "SecureId.CreatePassword.Hint.Placeholder") } + /// **%@ requests access to your personal data**\nto sign you up for their services + internal static func secureIdCreatePasswordIntroHeader(_ p1: String) -> String { + return L10n.tr("Localizable", "SecureId.CreatePassword.Intro.Header", p1) + } + /// Delete Personal Details + internal static var secureIdDeletePersonalDetails: String { return L10n.tr("Localizable", "SecureId.Delete.PersonalDetails") } + /// Are you sure you want to delete personal details? + internal static var secureIdDeleteConfirmPersonalDetails: String { return L10n.tr("Localizable", "SecureId.Delete.Confirm.PersonalDetails") } + /// Discard Changes + internal static var secureIdDiscardChangesHeader: String { return L10n.tr("Localizable", "SecureId.DiscardChanges.Header") } + /// Are you sure you want to discard all changes? + internal static var secureIdDiscardChangesText: String { return L10n.tr("Localizable", "SecureId.DiscardChanges.Text") } + /// Edit Bank Statement + internal static var secureIdEditBankStatement: String { return L10n.tr("Localizable", "SecureId.Edit.BankStatement") } + /// Edit Driver's License + internal static var secureIdEditDriverLicense: String { return L10n.tr("Localizable", "SecureId.Edit.DriverLicense") } + /// Edit Identity Card + internal static var secureIdEditID: String { return L10n.tr("Localizable", "SecureId.Edit.ID") } + /// Edit Internal Passport + internal static var secureIdEditInternalPassport: String { return L10n.tr("Localizable", "SecureId.Edit.InternalPassport") } + /// Edit Passport + internal static var secureIdEditPassport: String { return L10n.tr("Localizable", "SecureId.Edit.Passport") } + /// Edit Passport Registration + internal static var secureIdEditPassportRegistration: String { return L10n.tr("Localizable", "SecureId.Edit.PassportRegistration") } + /// Edit Personal Details + internal static var secureIdEditPersonalDetails: String { return L10n.tr("Localizable", "SecureId.Edit.PersonalDetails") } + /// Edit Rental Agreement + internal static var secureIdEditRentalAgreement: String { return L10n.tr("Localizable", "SecureId.Edit.RentalAgreement") } + /// Edit Residential Address + internal static var secureIdEditResidentialAddress: String { return L10n.tr("Localizable", "SecureId.Edit.ResidentialAddress") } + /// Edit Temporary Registration + internal static var secureIdEditTemporaryRegistration: String { return L10n.tr("Localizable", "SecureId.Edit.TemporaryRegistration") } + /// Edit Tenancy Agreement + internal static var secureIdEditTenancyAgreement: String { return L10n.tr("Localizable", "SecureId.Edit.TenancyAgreement") } + /// Edit Utility Bill + internal static var secureIdEditUtilityBill: String { return L10n.tr("Localizable", "SecureId.Edit.UtilityBill") } + /// Use %@ + internal static func secureIdEmailUseSame(_ p1: String) -> String { + return L10n.tr("Localizable", "SecureId.Email.UseSame", p1) + } + /// Enter your e-mail + internal static var secureIdEmailEmailInputPlaceholder: String { return L10n.tr("Localizable", "SecureId.Email.Email.InputPlaceholder") } + /// E-Mail + internal static var secureIdEmailEmailPlaceholder: String { return L10n.tr("Localizable", "SecureId.Email.Email.Placeholder") } + /// Note: You will receive a confirmation code to the e-mail address you provide. + internal static var secureIdEmailUseSameDesc: String { return L10n.tr("Localizable", "SecureId.Email.UseSame.Desc") } + /// Please enter the confirmation code we've just sent to %@. + internal static func secureIdEmailActivateDescription(_ p1: String) -> String { + return L10n.tr("Localizable", "SecureId.EmailActivate.Description", p1) + } + /// Enter code + internal static var secureIdEmailActivateCodeInputPlaceholder: String { return L10n.tr("Localizable", "SecureId.EmailActivate.Code.InputPlaceholder") } + /// Code + internal static var secureIdEmailActivateCodePlaceholder: String { return L10n.tr("Localizable", "SecureId.EmailActivate.Code.Placeholder") } + /// Provide your address + internal static var secureIdEmptyDescriptionAddress: String { return L10n.tr("Localizable", "SecureId.EmptyDescription.Address") } + /// Upload a scan of your bank statement + internal static var secureIdEmptyDescriptionBankStatement: String { return L10n.tr("Localizable", "SecureId.EmptyDescription.BankStatement") } + /// Upload a scan of your driver's license + internal static var secureIdEmptyDescriptionDriversLicense: String { return L10n.tr("Localizable", "SecureId.EmptyDescription.DriversLicense") } + /// Upload a scan of your identity card + internal static var secureIdEmptyDescriptionIdentityCard: String { return L10n.tr("Localizable", "SecureId.EmptyDescription.IdentityCard") } + /// Upload a scan of your internal passport + internal static var secureIdEmptyDescriptionInternalPassport: String { return L10n.tr("Localizable", "SecureId.EmptyDescription.InternalPassport") } + /// Upload a scan of your passport + internal static var secureIdEmptyDescriptionPassport: String { return L10n.tr("Localizable", "SecureId.EmptyDescription.Passport") } + /// Upload a scan of your passport registration + internal static var secureIdEmptyDescriptionPassportRegistration: String { return L10n.tr("Localizable", "SecureId.EmptyDescription.PassportRegistration") } + /// Fill in your personal details + internal static var secureIdEmptyDescriptionPersonalDetails: String { return L10n.tr("Localizable", "SecureId.EmptyDescription.PersonalDetails") } + /// Upload a scan of your temporary registration + internal static var secureIdEmptyDescriptionTemporaryRegistration: String { return L10n.tr("Localizable", "SecureId.EmptyDescription.TemporaryRegistration") } + /// Upload a scan of your tenancy agreement + internal static var secureIdEmptyDescriptionTenancyAgreement: String { return L10n.tr("Localizable", "SecureId.EmptyDescription.TenancyAgreement") } + /// Upload a scan of your utility bill + internal static var secureIdEmptyDescriptionUtilityBill: String { return L10n.tr("Localizable", "SecureId.EmptyDescription.UtilityBill") } + /// You can't upload more than 20 files + internal static var secureIdErrorScansLimit: String { return L10n.tr("Localizable", "SecureId.Error.ScansLimit") } + /// %@%% Uploaded + internal static func secureIdFileUploadProgress(_ p1: String) -> String { + return L10n.tr("Localizable", "SecureId.FileUpload.Progress", p1) + } + /// Female + internal static var secureIdGenderFemale: String { return L10n.tr("Localizable", "SecureId.Gender.Female") } + /// Male + internal static var secureIdGenderMale: String { return L10n.tr("Localizable", "SecureId.Gender.Male") } + /// Bank Statement + internal static var secureIdIdentityBankStatement: String { return L10n.tr("Localizable", "SecureId.Identity.BankStatement") } + /// DOCUMENT DETAILS + internal static var secureIdIdentityDocumentDetailsHeader: String { return L10n.tr("Localizable", "SecureId.Identity.DocumentDetailsHeader") } + /// Driver's License + internal static var secureIdIdentityDriverLicense: String { return L10n.tr("Localizable", "SecureId.Identity.DriverLicense") } + /// Identity Card + internal static var secureIdIdentityId: String { return L10n.tr("Localizable", "SecureId.Identity.Id") } + /// Enter your name using the Latin alphabet + internal static var secureIdIdentityNameInLatine: String { return L10n.tr("Localizable", "SecureId.Identity.NameInLatine") } + /// Passport + internal static var secureIdIdentityPassport: String { return L10n.tr("Localizable", "SecureId.Identity.Passport") } + /// Passport Registration + internal static var secureIdIdentityPassportRegistration: String { return L10n.tr("Localizable", "SecureId.Identity.PassportRegistration") } + /// Selfie + internal static var secureIdIdentitySelfie: String { return L10n.tr("Localizable", "SecureId.Identity.Selfie") } + /// Upload a photo of yourself holding your document. Make sure the ID and your face are clearly visible. + internal static var secureIdIdentitySelfieHelp: String { return L10n.tr("Localizable", "SecureId.Identity.SelfieHelp") } + /// SELFIE VERIFICATION + internal static var secureIdIdentitySelfieTitle: String { return L10n.tr("Localizable", "SecureId.Identity.SelfieTitle") } + /// Add Selfie + internal static var secureIdIdentitySelfieUpload: String { return L10n.tr("Localizable", "SecureId.Identity.SelfieUpload") } + /// Retake Selfie + internal static var secureIdIdentitySelfieUploadNew: String { return L10n.tr("Localizable", "SecureId.Identity.SelfieUploadNew") } + /// Tenancy Agreement + internal static var secureIdIdentityTenancyAgreement: String { return L10n.tr("Localizable", "SecureId.Identity.TenancyAgreement") } + /// Utility Bill + internal static var secureIdIdentityUtilityBill: String { return L10n.tr("Localizable", "SecureId.Identity.UtilityBill") } + /// Card ID + internal static var secureIdIdentityCardIdInputPlaceholder: String { return L10n.tr("Localizable", "SecureId.Identity.CardId.InputPlaceholder") } + /// Card ID + internal static var secureIdIdentityCardIdPlaceholder: String { return L10n.tr("Localizable", "SecureId.Identity.CardId.Placeholder") } + /// Name + internal static var secureIdIdentityInputPlaceholderFirstName: String { return L10n.tr("Localizable", "SecureId.Identity.InputPlaceholder.FirstName") } + /// Surname + internal static var secureIdIdentityInputPlaceholderLastName: String { return L10n.tr("Localizable", "SecureId.Identity.InputPlaceholder.LastName") } + /// Middle Name + internal static var secureIdIdentityInputPlaceholderMiddleName: String { return L10n.tr("Localizable", "SecureId.Identity.InputPlaceholder.MiddleName") } + /// License ID + internal static var secureIdIdentityLicenseInputPlaceholder: String { return L10n.tr("Localizable", "SecureId.Identity.License.InputPlaceholder") } + /// License ID + internal static var secureIdIdentityLicensePlaceholder: String { return L10n.tr("Localizable", "SecureId.Identity.License.Placeholder") } + /// Document № + internal static var secureIdIdentityPassportInputPlaceholder: String { return L10n.tr("Localizable", "SecureId.Identity.Passport.InputPlaceholder") } + /// Document № + internal static var secureIdIdentityPassportPlaceholder: String { return L10n.tr("Localizable", "SecureId.Identity.Passport.Placeholder") } + /// Birthday + internal static var secureIdIdentityPlaceholderBirthday: String { return L10n.tr("Localizable", "SecureId.Identity.Placeholder.Birthday") } + /// Citizenship + internal static var secureIdIdentityPlaceholderCitizenship: String { return L10n.tr("Localizable", "SecureId.Identity.Placeholder.Citizenship") } + /// Country + internal static var secureIdIdentityPlaceholderCountry: String { return L10n.tr("Localizable", "SecureId.Identity.Placeholder.Country") } + /// Expiry Date + internal static var secureIdIdentityPlaceholderExpiryDate: String { return L10n.tr("Localizable", "SecureId.Identity.Placeholder.ExpiryDate") } + /// Name + internal static var secureIdIdentityPlaceholderFirstName: String { return L10n.tr("Localizable", "SecureId.Identity.Placeholder.FirstName") } + /// Gender + internal static var secureIdIdentityPlaceholderGender: String { return L10n.tr("Localizable", "SecureId.Identity.Placeholder.Gender") } + /// Issue Date + internal static var secureIdIdentityPlaceholderIssuedDate: String { return L10n.tr("Localizable", "SecureId.Identity.Placeholder.IssuedDate") } + /// Surname + internal static var secureIdIdentityPlaceholderLastName: String { return L10n.tr("Localizable", "SecureId.Identity.Placeholder.LastName") } + /// Middle Name + internal static var secureIdIdentityPlaceholderMiddleName: String { return L10n.tr("Localizable", "SecureId.Identity.Placeholder.MiddleName") } + /// Residence + internal static var secureIdIdentityPlaceholderResidence: String { return L10n.tr("Localizable", "SecureId.Identity.Placeholder.Residence") } + /// 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 month ago. + internal static var secureIdIdentityScanDescription: String { return L10n.tr("Localizable", "SecureId.IdentityScan.Description") } + /// Are you sure you want to delete your Telegram Passport? All details will be lost. + internal static var secureIdInfoDeletePassport: String { return L10n.tr("Localizable", "SecureId.Info.DeletePassport") } + /// More Info + internal static var secureIdInfoMore: String { return L10n.tr("Localizable", "SecureId.Info.More") } + /// What is Telegram Passport? + internal static var secureIdInfoTitle: String { return L10n.tr("Localizable", "SecureId.Info.Title") } + /// Please use latin characters only + internal static var secureIdInputErrorLatinOnly: String { return L10n.tr("Localizable", "SecureId.InputError.LatinOnly") } + /// Please enter your password to access your personal data + internal static var secureIdInsertPasswordDescription: String { return L10n.tr("Localizable", "SecureId.InsertPassword.Description") } + /// Next + internal static var secureIdInsertPasswordNext: String { return L10n.tr("Localizable", "SecureId.InsertPassword.Next") } + /// Enter your password + internal static var secureIdInsertPasswordPassword: String { return L10n.tr("Localizable", "SecureId.InsertPassword.Password") } + /// Please enter your Telegram password to decrypt your data + internal static var secureIdInsertPasswordSettingsDescription: String { return L10n.tr("Localizable", "SecureId.InsertPassword.Settings.Description") } + /// E-Mail + internal static var secureIdInstallEmailTitle: String { return L10n.tr("Localizable", "SecureId.InstallEmail.Title") } + /// Phone Number + internal static var secureIdInstallPhoneTitle: String { return L10n.tr("Localizable", "SecureId.InstallPhone.Title") } + /// YOUR NAME IN %@ + internal static func secureIdNameNativeHeader(_ p1: String) -> String { + return L10n.tr("Localizable", "SecureId.NameNative.Header", p1) + } + /// NAME IN COUNTRY OF RESIDENCE + internal static var secureIdNameNativeHeaderEmpty: String { return L10n.tr("Localizable", "SecureId.NameNative.HeaderEmpty") } + /// Your name in the language of your country of residence + internal static var secureIdNameNativeDescEmpty: String { return L10n.tr("Localizable", "SecureId.NameNative.Desc.Empty") } + /// Your name in the language of your country of residence (%@). + internal static func secureIdNameNativeDescLanguage(_ p1: String) -> String { + return L10n.tr("Localizable", "SecureId.NameNative.Desc.Language", p1) + } + /// Invalid password. Please try again + internal static var secureIdPasswordErrorInvalid: String { return L10n.tr("Localizable", "SecureId.Password.Error.Invalid") } + /// Limit exceeded. Please try again later + internal static var secureIdPasswordErrorLimit: String { return L10n.tr("Localizable", "SecureId.Password.Error.Limit") } + /// OR ENTER ANOTHER PHONE NUMBER + internal static var secureIdPhoneNumberHeader: String { return L10n.tr("Localizable", "SecureId.PhoneNumber.Header") } + /// Note: You will receive a confirmation code on the phone number you provide. + internal static var secureIdPhoneNumberNote: String { return L10n.tr("Localizable", "SecureId.PhoneNumber.Note") } + /// Use %@ + internal static func secureIdPhoneNumberUseSame(_ p1: String) -> String { + return L10n.tr("Localizable", "SecureId.PhoneNumber.UseSame", p1) + } + /// Please enter the confirmation code we've just sent to %@ via SMS + internal static func secureIdPhoneNumberConfirmCodeDesc(_ p1: String) -> String { + return L10n.tr("Localizable", "SecureId.PhoneNumber.ConfirmCode.Desc", p1) + } + /// Enter the code + internal static var secureIdPhoneNumberConfirmCodeInputPlaceholder: String { return L10n.tr("Localizable", "SecureId.PhoneNumber.ConfirmCode.InputPlaceholder") } + /// Code + internal static var secureIdPhoneNumberConfirmCodePlaceholder: String { return L10n.tr("Localizable", "SecureId.PhoneNumber.ConfirmCode.Placeholder") } + /// Use the phone number you use for Telegram + internal static var secureIdPhoneNumberUseSameDesc: String { return L10n.tr("Localizable", "SecureId.PhoneNumber.UseSame.Desc") } + /// Code was sent to %@ + internal static func secureIdRecoverPasswordSentEmailCode(_ p1: String) -> String { + return L10n.tr("Localizable", "SecureId.RecoverPassword.SentEmailCode", p1) + } + /// Authorize + internal static var secureIdRequestAccept: String { return L10n.tr("Localizable", "SecureId.Request.Accept") } + /// Create a Password + internal static var secureIdRequestCreatePassword: String { return L10n.tr("Localizable", "SecureId.Request.CreatePassword") } + /// **%@** requests access to your personal data to sign you up for their services. + internal static func secureIdRequestHeader1(_ p1: String) -> String { + return L10n.tr("Localizable", "SecureId.Request.Header1", p1) + } + /// Bank Statement + internal static var secureIdRequestPermissionBankStatement: String { return L10n.tr("Localizable", "SecureId.Request.Permission.BankStatement") } + /// Driver's License + internal static var secureIdRequestPermissionDriversLicense: String { return L10n.tr("Localizable", "SecureId.Request.Permission.DriversLicense") } + /// E-Mail + internal static var secureIdRequestPermissionEmail: String { return L10n.tr("Localizable", "SecureId.Request.Permission.Email") } + /// Identity Card + internal static var secureIdRequestPermissionIDCard: String { return L10n.tr("Localizable", "SecureId.Request.Permission.IDCard") } + /// Identity Document + internal static var secureIdRequestPermissionIdentityDocument: String { return L10n.tr("Localizable", "SecureId.Request.Permission.IdentityDocument") } + /// Internal Passport + internal static var secureIdRequestPermissionInternalPassport: String { return L10n.tr("Localizable", "SecureId.Request.Permission.InternalPassport") } + /// Passport + internal static var secureIdRequestPermissionPassport: String { return L10n.tr("Localizable", "SecureId.Request.Permission.Passport") } + /// Passport Registration + internal static var secureIdRequestPermissionPassportRegistration: String { return L10n.tr("Localizable", "SecureId.Request.Permission.PassportRegistration") } + /// Personal Details + internal static var secureIdRequestPermissionPersonalDetails: String { return L10n.tr("Localizable", "SecureId.Request.Permission.PersonalDetails") } + /// Phone Number + internal static var secureIdRequestPermissionPhone: String { return L10n.tr("Localizable", "SecureId.Request.Permission.Phone") } + /// Residential Address + internal static var secureIdRequestPermissionResidentialAddress: String { return L10n.tr("Localizable", "SecureId.Request.Permission.ResidentialAddress") } + /// Temporary Registration + internal static var secureIdRequestPermissionTemporaryRegistration: String { return L10n.tr("Localizable", "SecureId.Request.Permission.TemporaryRegistration") } + /// Tenancy Agreement + internal static var secureIdRequestPermissionTenancyAgreement: String { return L10n.tr("Localizable", "SecureId.Request.Permission.TenancyAgreement") } + /// Utility Bill + internal static var secureIdRequestPermissionUtilityBill: String { return L10n.tr("Localizable", "SecureId.Request.Permission.UtilityBill") } + /// Upload proof of your address + internal static var secureIdRequestPermissionAddressEmpty: String { return L10n.tr("Localizable", "SecureId.Request.Permission.Address.Empty") } + /// Provide your contact email address + internal static var secureIdRequestPermissionEmailEmpty: String { return L10n.tr("Localizable", "SecureId.Request.Permission.Email.Empty") } + /// Upload a scan of your passport or other ID + internal static var secureIdRequestPermissionIdentityEmpty: String { return L10n.tr("Localizable", "SecureId.Request.Permission.Identity.Empty") } + /// Provide your contact phone number + internal static var secureIdRequestPermissionPhoneEmpty: String { return L10n.tr("Localizable", "SecureId.Request.Permission.Phone.Empty") } + /// %@ or %@ + internal static func secureIdRequestTwoDocumentsTitle(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "SecureId.Request.TwoDocuments.Title", p1, p2) + } + /// Upload a selfie with your document + internal static var secureIdRequestUploadSelfie: String { return L10n.tr("Localizable", "SecureId.Request.Upload.Selfie") } + /// Upload a translation of your document + internal static var secureIdRequestUploadTranslation: String { return L10n.tr("Localizable", "SecureId.Request.Upload.Translation") } + /// REQUESTED INFORMATION + internal static var secureIdRequestedInformationHeader: String { return L10n.tr("Localizable", "SecureId.RequestedInformation.Header") } + /// SCANS + internal static var secureIdScansHeader: String { return L10n.tr("Localizable", "SecureId.Scans.Header") } + /// Upload scans of a certified English translation of the document. + internal static var secureIdTranslationDesc: String { return L10n.tr("Localizable", "SecureId.Translation.Desc") } + /// TRANSLATION + internal static var secureIdTranslationHeader: String { return L10n.tr("Localizable", "SecureId.Translation.Header") } + /// Upload a photo of the front side of the document + internal static var secureIdUploadFront: String { return L10n.tr("Localizable", "SecureId.Upload.Front") } + /// Upload the main page of the document + internal static var secureIdUploadMain: String { return L10n.tr("Localizable", "SecureId.Upload.Main") } + /// Upload a photo of the reverse side of the document + internal static var secureIdUploadReverse: String { return L10n.tr("Localizable", "SecureId.Upload.Reverse") } + /// Upload a selfie of yourself holding the document + internal static var secureIdUploadSelfie: String { return L10n.tr("Localizable", "SecureId.Upload.Selfie") } + /// Front Side + internal static var secureIdUploadTitleFrontSide: String { return L10n.tr("Localizable", "SecureId.Upload.Title.FrontSide") } + /// Main Page + internal static var secureIdUploadTitleMainPage: String { return L10n.tr("Localizable", "SecureId.Upload.Title.MainPage") } + /// Reverse Side + internal static var secureIdUploadTitleReverseSide: String { return L10n.tr("Localizable", "SecureId.Upload.Title.ReverseSide") } + /// Upload a scan of %@ or %@ + internal static func secureIdUploadScanMulti(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "SecureId.UploadScan.Multi", p1, p2) + } + /// Upload a scan of %@ + internal static func secureIdUploadScanSingle(_ p1: String) -> String { + return L10n.tr("Localizable", "SecureId.UploadScan.Single", p1) + } + /// Warning! All data saved in your Telegram passport will be lost! + internal static var secureIdWarningDataLost: String { return L10n.tr("Localizable", "SecureId.Warning.DataLost") } + /// Since you didn't provide a recovery email when setting up your password, your remaining options are either to remember your password or to reset your account. + internal static var secureIdForgotPasswordNoEmail: String { return L10n.tr("Localizable", "SecureId.forgotPassword.NoEmail") } + /// None + internal static var selectAreaControlDimensionNone: String { return L10n.tr("Localizable", "SelectAreaControl.Dimension.None") } + /// Original + internal static var selectAreaControlDimensionOriginal: String { return L10n.tr("Localizable", "SelectAreaControl.Dimension.Original") } + /// Square + internal static var selectAreaControlDimensionSquare: String { return L10n.tr("Localizable", "SelectAreaControl.Dimension.Square") } + /// Search Members + internal static var selectPeersTitleSearchMembers: String { return L10n.tr("Localizable", "SelectPeers.Title.SearchMembers") } + /// Select Chat + internal static var selectPeersTitleSelectChat: String { return L10n.tr("Localizable", "SelectPeers.Title.SelectChat") } /// clear - case separatorClear + internal static var separatorClear: String { return L10n.tr("Localizable", "Separator.Clear") } /// show less - case separatorShowLess + internal static var separatorShowLess: String { return L10n.tr("Localizable", "Separator.ShowLess") } /// show more - case separatorShowMore + internal static var separatorShowMore: String { return L10n.tr("Localizable", "Separator.ShowMore") } /// %@ sent you a self-destructing photo. Please view it on your mobile. - case serviceMessageDesturctingPhoto(String) - /// %@ sent you a self-destructing video. Please view it on your mobile. - case serviceMessageDesturctingVideo(String) + internal static func serviceMessageDesturctingPhoto(_ p1: String) -> String { + return L10n.tr("Localizable", "ServiceMessage.DesturctingPhoto", p1) + } + /// %@ sent you a self-destructing video. Please view it on your mobile device. + internal static func serviceMessageDesturctingVideo(_ p1: String) -> String { + return L10n.tr("Localizable", "ServiceMessage.DesturctingVideo", p1) + } /// file has expired - case serviceMessageExpiredFile + internal static var serviceMessageExpiredFile: String { return L10n.tr("Localizable", "ServiceMessage.ExpiredFile") } /// photo has expired - case serviceMessageExpiredPhoto + internal static var serviceMessageExpiredPhoto: String { return L10n.tr("Localizable", "ServiceMessage.ExpiredPhoto") } /// video has expired - case serviceMessageExpiredVideo + internal static var serviceMessageExpiredVideo: String { return L10n.tr("Localizable", "ServiceMessage.ExpiredVideo") } /// %@ sent a self-destructing photo. - case serviceMessageDesturctingPhotoYou(String) + internal static func serviceMessageDesturctingPhotoYou(_ p1: String) -> String { + return L10n.tr("Localizable", "ServiceMessage.DesturctingPhoto.You", p1) + } /// %@ sent a self-destructing video. - case serviceMessageDesturctingVideoYou(String) + internal static func serviceMessageDesturctingVideoYou(_ p1: String) -> String { + return L10n.tr("Localizable", "ServiceMessage.DesturctingVideo.You", p1) + } /// ACTIVE SESSIONS - case sessionsActiveSessionsHeader + internal static var sessionsActiveSessionsHeader: String { return L10n.tr("Localizable", "Sessions.ActiveSessionsHeader") } /// CURRENT SESSION - case sessionsCurrentSessionHeader + internal static var sessionsCurrentSessionHeader: String { return L10n.tr("Localizable", "Sessions.CurrentSessionHeader") } /// Logs out all devices except for this one. - case sessionsTerminateDescription + internal static var sessionsTerminateDescription: String { return L10n.tr("Localizable", "Sessions.TerminateDescription") } /// Terminate all other sessions - case sessionsTerminateOthers + internal static var sessionsTerminateOthers: String { return L10n.tr("Localizable", "Sessions.TerminateOthers") } + /// Search results from Settings and the Telegram FAQ will appear here. + internal static var settingsSearchEmptyItem: String { return L10n.tr("Localizable", "SettingsSearch.EmptyItem") } + /// RECENT + internal static var settingsSearchRecent: String { return L10n.tr("Localizable", "SettingsSearch.Recent") } + /// clear + internal static var settingsSearchRecentClear: String { return L10n.tr("Localizable", "SettingsSearch.Recent.Clear") } + /// + internal static var settingsSearchSynonymsAppLanguage: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.AppLanguage") } + /// + internal static var settingsSearchSynonymsFAQ: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.FAQ") } + /// + internal static var settingsSearchSynonymsPassport: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Passport") } + /// + internal static var settingsSearchSynonymsSavedMessages: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.SavedMessages") } + /// Support + internal static var settingsSearchSynonymsSupport: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Support") } + /// Apple Watch + internal static var settingsSearchSynonymsWatch: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Watch") } + /// + internal static var settingsSearchSynonymsAppearanceAutoNightTheme: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Appearance.AutoNightTheme") } + /// Wallpaper + internal static var settingsSearchSynonymsAppearanceChatBackground: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Appearance.ChatBackground") } + /// bubbles + internal static var settingsSearchSynonymsAppearanceChatMode: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Appearance.ChatMode") } + /// + internal static var settingsSearchSynonymsAppearanceColorTheme: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Appearance.ColorTheme") } + /// font + internal static var settingsSearchSynonymsAppearanceTextSize: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Appearance.TextSize") } + /// + internal static var settingsSearchSynonymsAppearanceTitle: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Appearance.Title") } + /// + internal static var settingsSearchSynonymsAppearanceChatBackgroundCustom: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Appearance.ChatBackground.Custom") } + /// + internal static var settingsSearchSynonymsAppearanceChatBackgroundSetColor: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Appearance.ChatBackground.SetColor") } + /// + internal static var settingsSearchSynonymsCallsCallTab: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Calls.CallTab") } + /// + internal static var settingsSearchSynonymsCallsTitle: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Calls.Title") } + /// + internal static var settingsSearchSynonymsDataAutoDownloadReset: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Data.AutoDownloadReset") } + /// + internal static var settingsSearchSynonymsDataAutoDownloadUsingCellular: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Data.AutoDownloadUsingCellular") } + /// + internal static var settingsSearchSynonymsDataAutoDownloadUsingWifi: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Data.AutoDownloadUsingWifi") } + /// + internal static var settingsSearchSynonymsDataAutoplayGifs: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Data.AutoplayGifs") } + /// + internal static var settingsSearchSynonymsDataAutoplayVideos: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Data.AutoplayVideos") } + /// + internal static var settingsSearchSynonymsDataCallsUseLessData: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Data.CallsUseLessData") } + /// + internal static var settingsSearchSynonymsDataDownloadInBackground: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Data.DownloadInBackground") } + /// + internal static var settingsSearchSynonymsDataNetworkUsage: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Data.NetworkUsage") } + /// + internal static var settingsSearchSynonymsDataSaveEditedPhotos: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Data.SaveEditedPhotos") } + /// + internal static var settingsSearchSynonymsDataSaveIncomingPhotos: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Data.SaveIncomingPhotos") } + /// + internal static var settingsSearchSynonymsDataTitle: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Data.Title") } + /// + internal static var settingsSearchSynonymsDataStorageClearCache: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Data.Storage.ClearCache") } + /// + internal static var settingsSearchSynonymsDataStorageKeepMedia: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Data.Storage.KeepMedia") } + /// Cache + internal static var settingsSearchSynonymsDataStorageTitle: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Data.Storage.Title") } + /// + internal static var settingsSearchSynonymsEditProfileAddAccount: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.EditProfile.AddAccount") } + /// + internal static var settingsSearchSynonymsEditProfileBio: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.EditProfile.Bio") } + /// + internal static var settingsSearchSynonymsEditProfileLogout: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.EditProfile.Logout") } + /// + internal static var settingsSearchSynonymsEditProfilePhoneNumber: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.EditProfile.PhoneNumber") } + /// + internal static var settingsSearchSynonymsEditProfileTitle: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.EditProfile.Title") } + /// nickname + internal static var settingsSearchSynonymsEditProfileUsername: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.EditProfile.Username") } + /// + internal static var settingsSearchSynonymsNotificationsBadgeCountUnreadMessages: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Notifications.BadgeCountUnreadMessages") } + /// + internal static var settingsSearchSynonymsNotificationsBadgeIncludeMutedChannels: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Notifications.BadgeIncludeMutedChannels") } + /// + internal static var settingsSearchSynonymsNotificationsBadgeIncludeMutedChats: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Notifications.BadgeIncludeMutedChats") } + /// + internal static var settingsSearchSynonymsNotificationsBadgeIncludeMutedPublicGroups: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Notifications.BadgeIncludeMutedPublicGroups") } + /// + internal static var settingsSearchSynonymsNotificationsChannelNotificationsAlert: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Notifications.ChannelNotificationsAlert") } + /// + internal static var settingsSearchSynonymsNotificationsChannelNotificationsExceptions: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Notifications.ChannelNotificationsExceptions") } + /// + internal static var settingsSearchSynonymsNotificationsChannelNotificationsPreview: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Notifications.ChannelNotificationsPreview") } + /// + internal static var settingsSearchSynonymsNotificationsChannelNotificationsSound: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Notifications.ChannelNotificationsSound") } + /// + internal static var settingsSearchSynonymsNotificationsContactJoined: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Notifications.ContactJoined") } + /// + internal static var settingsSearchSynonymsNotificationsDisplayNamesOnLockScreen: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Notifications.DisplayNamesOnLockScreen") } + /// + internal static var settingsSearchSynonymsNotificationsGroupNotificationsAlert: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Notifications.GroupNotificationsAlert") } + /// + internal static var settingsSearchSynonymsNotificationsGroupNotificationsExceptions: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Notifications.GroupNotificationsExceptions") } + /// + internal static var settingsSearchSynonymsNotificationsGroupNotificationsPreview: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Notifications.GroupNotificationsPreview") } + /// + internal static var settingsSearchSynonymsNotificationsGroupNotificationsSound: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Notifications.GroupNotificationsSound") } + /// + internal static var settingsSearchSynonymsNotificationsInAppNotificationsPreview: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Notifications.InAppNotificationsPreview") } + /// + internal static var settingsSearchSynonymsNotificationsInAppNotificationsSound: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Notifications.InAppNotificationsSound") } + /// + internal static var settingsSearchSynonymsNotificationsInAppNotificationsVibrate: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Notifications.InAppNotificationsVibrate") } + /// + internal static var settingsSearchSynonymsNotificationsMessageNotificationsAlert: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Notifications.MessageNotificationsAlert") } + /// + internal static var settingsSearchSynonymsNotificationsMessageNotificationsExceptions: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Notifications.MessageNotificationsExceptions") } + /// + internal static var settingsSearchSynonymsNotificationsMessageNotificationsPreview: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Notifications.MessageNotificationsPreview") } + /// + internal static var settingsSearchSynonymsNotificationsMessageNotificationsSound: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Notifications.MessageNotificationsSound") } + /// + internal static var settingsSearchSynonymsNotificationsResetAllNotifications: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Notifications.ResetAllNotifications") } + /// + internal static var settingsSearchSynonymsNotificationsTitle: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Notifications.Title") } + /// + internal static var settingsSearchSynonymsPrivacyAuthSessions: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Privacy.AuthSessions") } + /// + internal static var settingsSearchSynonymsPrivacyBlockedUsers: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Privacy.BlockedUsers") } + /// + internal static var settingsSearchSynonymsPrivacyCalls: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Privacy.Calls") } + /// + internal static var settingsSearchSynonymsPrivacyDeleteAccountIfAwayFor: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Privacy.DeleteAccountIfAwayFor") } + /// + internal static var settingsSearchSynonymsPrivacyForwards: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Privacy.Forwards") } + /// + internal static var settingsSearchSynonymsPrivacyGroupsAndChannels: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Privacy.GroupsAndChannels") } + /// + internal static var settingsSearchSynonymsPrivacyLastSeen: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Privacy.LastSeen") } + /// + internal static var settingsSearchSynonymsPrivacyPasscode: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Privacy.Passcode") } + /// + internal static var settingsSearchSynonymsPrivacyPasscodeAndFaceId: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Privacy.PasscodeAndFaceId") } + /// + internal static var settingsSearchSynonymsPrivacyPasscodeAndTouchId: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Privacy.PasscodeAndTouchId") } + /// + internal static var settingsSearchSynonymsPrivacyProfilePhoto: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Privacy.ProfilePhoto") } + /// + internal static var settingsSearchSynonymsPrivacyTitle: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Privacy.Title") } + /// Password + internal static var settingsSearchSynonymsPrivacyTwoStepAuth: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Privacy.TwoStepAuth") } + /// + internal static var settingsSearchSynonymsPrivacyDataClearPaymentsInfo: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Privacy.Data.ClearPaymentsInfo") } + /// + internal static var settingsSearchSynonymsPrivacyDataContactsReset: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Privacy.Data.ContactsReset") } + /// + internal static var settingsSearchSynonymsPrivacyDataContactsSync: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Privacy.Data.ContactsSync") } + /// + internal static var settingsSearchSynonymsPrivacyDataDeleteDrafts: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Privacy.Data.DeleteDrafts") } + /// + internal static var settingsSearchSynonymsPrivacyDataSecretChatLinkPreview: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Privacy.Data.SecretChatLinkPreview") } + /// + internal static var settingsSearchSynonymsPrivacyDataTitle: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Privacy.Data.Title") } + /// + internal static var settingsSearchSynonymsPrivacyDataTopPeers: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Privacy.Data.TopPeers") } + /// + internal static var settingsSearchSynonymsProxyAddProxy: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Proxy.AddProxy") } + /// SOCKS5\nMTProto + internal static var settingsSearchSynonymsProxyTitle: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Proxy.Title") } + /// + internal static var settingsSearchSynonymsProxyUseForCalls: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Proxy.UseForCalls") } + /// + internal static var settingsSearchSynonymsStickersArchivedPacks: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Stickers.ArchivedPacks") } + /// + internal static var settingsSearchSynonymsStickersFeaturedPacks: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Stickers.FeaturedPacks") } + /// + internal static var settingsSearchSynonymsStickersSuggestStickers: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Stickers.SuggestStickers") } + /// + internal static var settingsSearchSynonymsStickersTitle: String { return L10n.tr("Localizable", "SettingsSearch.Synonyms.Stickers.Title") } /// Copied to Clipboard - case shareLinkCopied + internal static var shareLinkCopied: String { return L10n.tr("Localizable", "Share.Link.Copied") } /// Cancel - case shareExtensionCancel + internal static var shareExtensionCancel: String { return L10n.tr("Localizable", "ShareExtension.Cancel") } /// Search - case shareExtensionSearch + internal static var shareExtensionSearch: String { return L10n.tr("Localizable", "ShareExtension.Search") } /// Share - case shareExtensionShare + internal static var shareExtensionShare: String { return L10n.tr("Localizable", "ShareExtension.Share") } /// Next - case shareExtensionPasscodeNext + internal static var shareExtensionPasscodeNext: String { return L10n.tr("Localizable", "ShareExtension.Passcode.Next") } /// passcode - case shareExtensionPasscodePlaceholder + internal static var shareExtensionPasscodePlaceholder: String { return L10n.tr("Localizable", "ShareExtension.Passcode.Placeholder") } /// To share via Telegram, please open the Telegam app and log in. - case shareExtensionUnauthorizedDescription + internal static var shareExtensionUnauthorizedDescription: String { return L10n.tr("Localizable", "ShareExtension.Unauthorized.Description") } /// OK - case shareExtensionUnauthorizedOK + internal static var shareExtensionUnauthorizedOK: String { return L10n.tr("Localizable", "ShareExtension.Unauthorized.OK") } + /// Forward to... + internal static var shareModalSearchForwardPlaceholder: String { return L10n.tr("Localizable", "ShareModal.Search.ForwardPlaceholder") } /// Share to... - case shareModalSearchPlaceholder - /// Sidebar available in chat - case sidebarAvalability + internal static var shareModalSearchPlaceholder: String { return L10n.tr("Localizable", "ShareModal.Search.Placeholder") } + /// CHAT + internal static var shortcutsControllerChat: String { return L10n.tr("Localizable", "ShortcutsController.Chat") } + /// GESTURES + internal static var shortcutsControllerGestures: String { return L10n.tr("Localizable", "ShortcutsController.Gestures") } + /// MARKDOWN + internal static var shortcutsControllerMarkdown: String { return L10n.tr("Localizable", "ShortcutsController.Markdown") } + /// MOUSE + internal static var shortcutsControllerMouse: String { return L10n.tr("Localizable", "ShortcutsController.Mouse") } + /// OTHERS + internal static var shortcutsControllerOthers: String { return L10n.tr("Localizable", "ShortcutsController.Others") } + /// SEARCH + internal static var shortcutsControllerSearch: String { return L10n.tr("Localizable", "ShortcutsController.Search") } + /// Shortcuts + internal static var shortcutsControllerTitle: String { return L10n.tr("Localizable", "ShortcutsController.Title") } + /// VIDEO CHAT + internal static var shortcutsControllerVideoChat: String { return L10n.tr("Localizable", "ShortcutsController.VideoChat") } + /// Edit Last Message + internal static var shortcutsControllerChatEditLastMessage: String { return L10n.tr("Localizable", "ShortcutsController.Chat.EditLastMessage") } + /// Open Info + internal static var shortcutsControllerChatOpenInfo: String { return L10n.tr("Localizable", "ShortcutsController.Chat.OpenInfo") } + /// Record Voice/Video Message + internal static var shortcutsControllerChatRecordVoiceMessage: String { return L10n.tr("Localizable", "ShortcutsController.Chat.RecordVoiceMessage") } + /// Search Messages + internal static var shortcutsControllerChatSearchMessages: String { return L10n.tr("Localizable", "ShortcutsController.Chat.SearchMessages") } + /// Select Message To Reply + internal static var shortcutsControllerChatSelectMessageToReply: String { return L10n.tr("Localizable", "ShortcutsController.Chat.SelectMessageToReply") } + /// Chat Actions + internal static var shortcutsControllerGesturesChatAction: String { return L10n.tr("Localizable", "ShortcutsController.Gestures.ChatAction") } + /// Navigation Back + internal static var shortcutsControllerGesturesNavigation: String { return L10n.tr("Localizable", "ShortcutsController.Gestures.Navigation") } + /// Reply + internal static var shortcutsControllerGesturesReply: String { return L10n.tr("Localizable", "ShortcutsController.Gestures.Reply") } + /// Stickers/Emoji/GIFs Panel + internal static var shortcutsControllerGesturesStickers: String { return L10n.tr("Localizable", "ShortcutsController.Gestures.Stickers") } + /// Swipe both sides + internal static var shortcutsControllerGesturesChatActionValue: String { return L10n.tr("Localizable", "ShortcutsController.Gestures.ChatAction.Value") } + /// Swipe From Left To Right + internal static var shortcutsControllerGesturesNavigationsValue: String { return L10n.tr("Localizable", "ShortcutsController.Gestures.Navigations.Value") } + /// Swipe From Right To Left + internal static var shortcutsControllerGesturesReplyValue: String { return L10n.tr("Localizable", "ShortcutsController.Gestures.Reply.Value") } + /// Swipe both sides + internal static var shortcutsControllerGesturesStickersValue: String { return L10n.tr("Localizable", "ShortcutsController.Gestures.Stickers.Value") } + /// Bold + internal static var shortcutsControllerMarkdownBold: String { return L10n.tr("Localizable", "ShortcutsController.Markdown.Bold") } + /// Hyperlink + internal static var shortcutsControllerMarkdownHyperlink: String { return L10n.tr("Localizable", "ShortcutsController.Markdown.Hyperlink") } + /// Italic + internal static var shortcutsControllerMarkdownItalic: String { return L10n.tr("Localizable", "ShortcutsController.Markdown.Italic") } + /// Monospace + internal static var shortcutsControllerMarkdownMonospace: String { return L10n.tr("Localizable", "ShortcutsController.Markdown.Monospace") } + /// Strikethrough + internal static var shortcutsControllerMarkdownStrikethrough: String { return L10n.tr("Localizable", "ShortcutsController.Markdown.Strikethrough") } + /// Fast Reply + internal static var shortcutsControllerMouseFastReply: String { return L10n.tr("Localizable", "ShortcutsController.Mouse.FastReply") } + /// Schedule a message + internal static var shortcutsControllerMouseScheduleMessage: String { return L10n.tr("Localizable", "ShortcutsController.Mouse.ScheduleMessage") } + /// Double Click + internal static var shortcutsControllerMouseFastReplyValue: String { return L10n.tr("Localizable", "ShortcutsController.Mouse.FastReply.Value") } + /// Option click on 'Send Message' + internal static var shortcutsControllerMouseScheduleMessageValue: String { return L10n.tr("Localizable", "ShortcutsController.Mouse.ScheduleMessage.Value") } + /// Lock by Passcode + internal static var shortcutsControllerOthersLockByPasscode: String { return L10n.tr("Localizable", "ShortcutsController.Others.LockByPasscode") } + /// Global Search + internal static var shortcutsControllerSearchGlobalSearch: String { return L10n.tr("Localizable", "ShortcutsController.Search.GlobalSearch") } + /// Quick Search + internal static var shortcutsControllerSearchQuickSearch: String { return L10n.tr("Localizable", "ShortcutsController.Search.QuickSearch") } + /// Toggle Camera + internal static var shortcutsControllerVideoChatToggleCamera: String { return L10n.tr("Localizable", "ShortcutsController.VideoChat.ToggleCamera") } + /// Toggle Screen Share + internal static var shortcutsControllerVideoChatToggleScreencast: String { return L10n.tr("Localizable", "ShortcutsController.VideoChat.ToggleScreencast") } + /// The sidebar is only available while chatting + internal static var sidebarAvalability: String { return L10n.tr("Localizable", "Sidebar.Avalability") } + /// Hide Panel + internal static var sidebarHide: String { return L10n.tr("Localizable", "Sidebar.Hide") } + /// Sidebar is not available in this chat + internal static var sidebarPeerRestricted: String { return L10n.tr("Localizable", "Sidebar.Peer.Restricted") } + /// Slow mode is enabled. You can't forward a message with a comment + internal static var slowModeForwardCommentError: String { return L10n.tr("Localizable", "SlowMode.ForwardComment.Error") } + /// Slow mode is enabled. You can't send more than one message at a time. + internal static var slowModeMultipleError: String { return L10n.tr("Localizable", "SlowMode.Multiple.Error") } + /// Slowmode is Enabled.\nYou can't add comment as addition message. + internal static var slowModePreviewSenderComment: String { return L10n.tr("Localizable", "SlowMode.PreviewSender.Comment") } + /// Slowmode is Enabled.\nThere is no way to send multiple files at once. + internal static var slowModePreviewSenderFileTooltip: String { return L10n.tr("Localizable", "SlowMode.PreviewSender.FileTooltip") } + /// Slowmode is Enabled.\nThere is no way to send multiple media at once. + internal static var slowModePreviewSenderMediaTooltip: String { return L10n.tr("Localizable", "SlowMode.PreviewSender.MediaTooltip") } + /// Slow mode is enabled. This text is too long to send as one message. + internal static var slowModeTooLongError: String { return L10n.tr("Localizable", "SlowMode.TooLong.Error") } + /// ACTIONS + internal static var statsGroupActionsTitle: String { return L10n.tr("Localizable", "Stats.GroupActionsTitle") } + /// GROWTH + internal static var statsGroupGrowthTitle: String { return L10n.tr("Localizable", "Stats.GroupGrowthTitle") } + /// MEMBERS' PRIMARY LANGUAGE + internal static var statsGroupLanguagesTitle: String { return L10n.tr("Localizable", "Stats.GroupLanguagesTitle") } + /// Members + internal static var statsGroupMembers: String { return L10n.tr("Localizable", "Stats.GroupMembers") } + /// GROUP MEMBERS + internal static var statsGroupMembersTitle: String { return L10n.tr("Localizable", "Stats.GroupMembersTitle") } + /// Messages + internal static var statsGroupMessages: String { return L10n.tr("Localizable", "Stats.GroupMessages") } + /// MESSAGES + internal static var statsGroupMessagesTitle: String { return L10n.tr("Localizable", "Stats.GroupMessagesTitle") } + /// NEW MEMBERS BY SOURCE + internal static var statsGroupNewMembersBySourceTitle: String { return L10n.tr("Localizable", "Stats.GroupNewMembersBySourceTitle") } + /// OVERVIEW + internal static var statsGroupOverview: String { return L10n.tr("Localizable", "Stats.GroupOverview") } + /// Posting Members + internal static var statsGroupPosters: String { return L10n.tr("Localizable", "Stats.GroupPosters") } + /// %d + internal static func statsGroupTopAdminBansCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopAdminBans_countable", p1) + } + /// %d bans + internal static func statsGroupTopAdminBansFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopAdminBans_few", p1) + } + /// %d bans + internal static func statsGroupTopAdminBansMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopAdminBans_many", p1) + } + /// %d ban + internal static func statsGroupTopAdminBansOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopAdminBans_one", p1) + } + /// %d bans + internal static func statsGroupTopAdminBansOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopAdminBans_other", p1) + } + /// %d bans + internal static func statsGroupTopAdminBansTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopAdminBans_two", p1) + } + /// %d bans + internal static func statsGroupTopAdminBansZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopAdminBans_zero", p1) + } + /// %d + internal static func statsGroupTopAdminDeletionsCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopAdminDeletions_countable", p1) + } + /// %d deletions + internal static func statsGroupTopAdminDeletionsFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopAdminDeletions_few", p1) + } + /// %d deletions + internal static func statsGroupTopAdminDeletionsMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopAdminDeletions_many", p1) + } + /// %d deletion + internal static func statsGroupTopAdminDeletionsOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopAdminDeletions_one", p1) + } + /// %d deletions + internal static func statsGroupTopAdminDeletionsOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopAdminDeletions_other", p1) + } + /// %d deletions + internal static func statsGroupTopAdminDeletionsTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopAdminDeletions_two", p1) + } + /// %d deletions + internal static func statsGroupTopAdminDeletionsZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopAdminDeletions_zero", p1) + } + /// %d + internal static func statsGroupTopAdminKicksCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopAdminKicks_countable", p1) + } + /// %d kicks + internal static func statsGroupTopAdminKicksFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopAdminKicks_few", p1) + } + /// %d kicks + internal static func statsGroupTopAdminKicksMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopAdminKicks_many", p1) + } + /// %d kick + internal static func statsGroupTopAdminKicksOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopAdminKicks_one", p1) + } + /// %d kicks + internal static func statsGroupTopAdminKicksOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopAdminKicks_other", p1) + } + /// %d kicks + internal static func statsGroupTopAdminKicksTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopAdminKicks_two", p1) + } + /// %d kicks + internal static func statsGroupTopAdminKicksZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopAdminKicks_zero", p1) + } + /// TOP ADMINS + internal static var statsGroupTopAdminsTitle: String { return L10n.tr("Localizable", "Stats.GroupTopAdminsTitle") } + /// TOP HOURS + internal static var statsGroupTopHoursTitle: String { return L10n.tr("Localizable", "Stats.GroupTopHoursTitle") } + /// %d + internal static func statsGroupTopInviterInvitesCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopInviterInvites_countable", p1) + } + /// %d invitations + internal static func statsGroupTopInviterInvitesFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopInviterInvites_few", p1) + } + /// %d invitations + internal static func statsGroupTopInviterInvitesMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopInviterInvites_many", p1) + } + /// %d invitation + internal static func statsGroupTopInviterInvitesOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopInviterInvites_one", p1) + } + /// %d invitations + internal static func statsGroupTopInviterInvitesOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopInviterInvites_other", p1) + } + /// %d invitations + internal static func statsGroupTopInviterInvitesTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopInviterInvites_two", p1) + } + /// %d invitations + internal static func statsGroupTopInviterInvitesZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopInviterInvites_zero", p1) + } + /// TOP INVITERS + internal static var statsGroupTopInvitersTitle: String { return L10n.tr("Localizable", "Stats.GroupTopInvitersTitle") } + /// %d + internal static func statsGroupTopPosterCharsCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopPosterChars_countable", p1) + } + /// %d symbols per message + internal static func statsGroupTopPosterCharsFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopPosterChars_few", p1) + } + /// %d symbols per message + internal static func statsGroupTopPosterCharsMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopPosterChars_many", p1) + } + /// %d symbol per message + internal static func statsGroupTopPosterCharsOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopPosterChars_one", p1) + } + /// %d symbols per message + internal static func statsGroupTopPosterCharsOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopPosterChars_other", p1) + } + /// %d symbols per message + internal static func statsGroupTopPosterCharsTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopPosterChars_two", p1) + } + /// %d symbols per message + internal static func statsGroupTopPosterCharsZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopPosterChars_zero", p1) + } + /// %d + internal static func statsGroupTopPosterMessagesCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopPosterMessages_countable", p1) + } + /// %d messages + internal static func statsGroupTopPosterMessagesFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopPosterMessages_few", p1) + } + /// %d messages + internal static func statsGroupTopPosterMessagesMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopPosterMessages_many", p1) + } + /// %d message + internal static func statsGroupTopPosterMessagesOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopPosterMessages_one", p1) + } + /// %d messages + internal static func statsGroupTopPosterMessagesOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopPosterMessages_other", p1) + } + /// %d messages + internal static func statsGroupTopPosterMessagesTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopPosterMessages_two", p1) + } + /// %d messages + internal static func statsGroupTopPosterMessagesZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.GroupTopPosterMessages_zero", p1) + } + /// TOP MEMBERS + internal static var statsGroupTopPostersTitle: String { return L10n.tr("Localizable", "Stats.GroupTopPostersTitle") } + /// TOP DAYS OF WEEK + internal static var statsGroupTopWeekdaysTitle: String { return L10n.tr("Localizable", "Stats.GroupTopWeekdaysTitle") } + /// Viewing Members + internal static var statsGroupViewers: String { return L10n.tr("Localizable", "Stats.GroupViewers") } + /// INTERACTIONS + internal static var statsMessageInteractionsTitle: String { return L10n.tr("Localizable", "Stats.MessageInteractionsTitle") } + /// OVERVIEW + internal static var statsMessageOverview: String { return L10n.tr("Localizable", "Stats.MessageOverview") } + /// Private Shares + internal static var statsMessagePrivateForwardsTitle: String { return L10n.tr("Localizable", "Stats.MessagePrivateForwardsTitle") } + /// Public Shares + internal static var statsMessagePublicForwardsTitle: String { return L10n.tr("Localizable", "Stats.MessagePublicForwardsTitle") } + /// Message Statistics + internal static var statsMessageTitle: String { return L10n.tr("Localizable", "Stats.MessageTitle") } + /// %d + internal static func statsShowMoreCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.ShowMore_countable", p1) + } + /// Show %d More + internal static func statsShowMoreFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.ShowMore_few", p1) + } + /// Show %d More + internal static func statsShowMoreMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.ShowMore_many", p1) + } + /// Show %d More + internal static func statsShowMoreOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.ShowMore_one", p1) + } + /// Show %d More + internal static func statsShowMoreOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.ShowMore_other", p1) + } + /// Show %d More + internal static func statsShowMoreTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.ShowMore_two", p1) + } + /// Show %d More + internal static func statsShowMoreZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stats.ShowMore_zero", p1) + } + /// Actions + internal static var statsGroupTopAdminActions: String { return L10n.tr("Localizable", "Stats.GroupTopAdmin.Actions") } + /// Promote + internal static var statsGroupTopAdminPromote: String { return L10n.tr("Localizable", "Stats.GroupTopAdmin.Promote") } + /// History + internal static var statsGroupTopInviterHistory: String { return L10n.tr("Localizable", "Stats.GroupTopInviter.History") } + /// Promote + internal static var statsGroupTopInviterPromote: String { return L10n.tr("Localizable", "Stats.GroupTopInviter.Promote") } + /// History + internal static var statsGroupTopPosterHistory: String { return L10n.tr("Localizable", "Stats.GroupTopPoster.History") } + /// Promote + internal static var statsGroupTopPosterPromote: String { return L10n.tr("Localizable", "Stats.GroupTopPoster.Promote") } + /// PUBLIC SHARES + internal static var statsMessagePublicForwardsTitleHeader: String { return L10n.tr("Localizable", "Stats.MessagePublicForwardsTitle.Header") } + /// Activate + internal static var statusBarActivate: String { return L10n.tr("Localizable", "StatusBar.Activate") } + /// Hide + internal static var statusBarHide: String { return L10n.tr("Localizable", "StatusBar.Hide") } + /// Quit + internal static var statusBarQuit: String { return L10n.tr("Localizable", "StatusBar.Quit") } + /// %d + internal static func stickerPackAdd1Countable(_ p1: Int) -> String { + return L10n.tr("Localizable", "StickerPack.Add1_countable", p1) + } + /// Add %d Stickers + internal static func stickerPackAdd1Few(_ p1: Int) -> String { + return L10n.tr("Localizable", "StickerPack.Add1_few", p1) + } + /// Add %d Stickers + internal static func stickerPackAdd1Many(_ p1: Int) -> String { + return L10n.tr("Localizable", "StickerPack.Add1_many", p1) + } + /// Add %d Sticker + internal static func stickerPackAdd1One(_ p1: Int) -> String { + return L10n.tr("Localizable", "StickerPack.Add1_one", p1) + } + /// Add %d Stickers + internal static func stickerPackAdd1Other(_ p1: Int) -> String { + return L10n.tr("Localizable", "StickerPack.Add1_other", p1) + } + /// Add %d Stickers + internal static func stickerPackAdd1Two(_ p1: Int) -> String { + return L10n.tr("Localizable", "StickerPack.Add1_two", p1) + } /// Add %d Stickers - case stickerPackAdd(Int) - /// Remove %d stickers - case stickerPackRemove(Int) + internal static func stickerPackAdd1Zero(_ p1: Int) -> String { + return L10n.tr("Localizable", "StickerPack.Add1_zero", p1) + } + /// Sorry, this sticker set doesn't seem to exist. + internal static var stickerSetDontExist: String { return L10n.tr("Localizable", "StickerSet.DontExist") } + /// Remove + internal static var stickerSetRemove: String { return L10n.tr("Localizable", "StickerSet.Remove") } + /// %d + internal static func stickersCountCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stickers.Count_countable", p1) + } + /// %d stickers + internal static func stickersCountFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stickers.Count_few", p1) + } + /// %d stickers + internal static func stickersCountMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stickers.Count_many", p1) + } + /// %d sticker + internal static func stickersCountOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stickers.Count_one", p1) + } + /// %d stickers + internal static func stickersCountOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stickers.Count_other", p1) + } + /// %d stickers + internal static func stickersCountTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stickers.Count_two", p1) + } + /// %d stickers + internal static func stickersCountZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stickers.Count_zero", p1) + } + /// Favorite + internal static var stickersFavorite: String { return L10n.tr("Localizable", "Stickers.Favorite") } /// GROUP STICKERS - case stickersGroupStickers + internal static var stickersGroupStickers: String { return L10n.tr("Localizable", "Stickers.GroupStickers") } /// Recent - case stickersRecent + internal static var stickersRecent: String { return L10n.tr("Localizable", "Stickers.Recent") } + /// Add + internal static var stickersSearchAdd: String { return L10n.tr("Localizable", "Stickers.SearchAdd") } + /// Added + internal static var stickersSearchAdded: String { return L10n.tr("Localizable", "Stickers.SearchAdded") } + /// My Sets + internal static var stickersSuggestAdded: String { return L10n.tr("Localizable", "Stickers.SuggestAdded") } + /// All Sets + internal static var stickersSuggestAll: String { return L10n.tr("Localizable", "Stickers.SuggestAll") } + /// None + internal static var stickersSuggestNone: String { return L10n.tr("Localizable", "Stickers.SuggestNone") } + /// Suggest Stickers by Emoji + internal static var stickersSuggestStickers: String { return L10n.tr("Localizable", "Stickers.SuggestStickers") } + /// Trending Stickers + internal static var stickersTrending: String { return L10n.tr("Localizable", "Stickers.Trending") } + /// Clear Recent Stickers + internal static var stickersConfirmClearRecentHeader: String { return L10n.tr("Localizable", "Stickers.Confirm.ClearRecentHeader") } + /// Clear + internal static var stickersConfirmClearRecentOK: String { return L10n.tr("Localizable", "Stickers.Confirm.ClearRecentOK") } + /// Are you sure you want to clear recent stickers? + internal static var stickersConfirmClearRecentText: String { return L10n.tr("Localizable", "Stickers.Confirm.ClearRecentText") } + /// Archive + internal static var stickersContextArchive: String { return L10n.tr("Localizable", "Stickers.Context.Archive") } + /// %d + internal static func stickersSetCount1Countable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stickers.Set.Count1_countable", p1) + } /// %d stickers - case stickersSetCount(Int) - /// Remove - case stickerSetRemove + internal static func stickersSetCount1Few(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stickers.Set.Count1_few", p1) + } + /// %d stickers + internal static func stickersSetCount1Many(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stickers.Set.Count1_many", p1) + } + /// %d sticker + internal static func stickersSetCount1One(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stickers.Set.Count1_one", p1) + } + /// %d stickers + internal static func stickersSetCount1Other(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stickers.Set.Count1_other", p1) + } + /// %d stickers + internal static func stickersSetCount1Two(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stickers.Set.Count1_two", p1) + } + /// %d stickers + internal static func stickersSetCount1Zero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Stickers.Set.Count1_zero", p1) + } /// Clear %@ - case storageClear(String) + internal static func storageClear(_ p1: String) -> String { + return L10n.tr("Localizable", "Storage.Clear", p1) + } + /// Clear All + internal static var storageClearAll: String { return L10n.tr("Localizable", "Storage.ClearAll") } /// Audio - case storageClearAudio + internal static var storageClearAudio: String { return L10n.tr("Localizable", "Storage.Clear.Audio") } /// Documents - case storageClearDocuments + internal static var storageClearDocuments: String { return L10n.tr("Localizable", "Storage.Clear.Documents") } /// Photos - case storageClearPhotos + internal static var storageClearPhotos: String { return L10n.tr("Localizable", "Storage.Clear.Photos") } /// Videos - case storageClearVideos - /// Calculating current cache size... - case storageUsageCalculating + internal static var storageClearVideos: String { return L10n.tr("Localizable", "Storage.Clear.Videos") } + /// Are you sure you want to clear all cached data? + internal static var storageClearAllConfirmDescription: String { return L10n.tr("Localizable", "Storage.ClearAll.Confirm.Description") } + /// Telegram is calculating the current cache size.\nThis can take a few minutes. + internal static var storageUsageCalculating: String { return L10n.tr("Localizable", "StorageUsage.Calculating") } /// CHATS - case storageUsageChatsHeader + internal static var storageUsageChatsHeader: String { return L10n.tr("Localizable", "StorageUsage.ChatsHeader") } + /// Your local cache is being cleaned... + internal static var storageUsageCleaningProcess: String { return L10n.tr("Localizable", "StorageUsage.CleaningProcess") } + /// Clear + internal static var storageUsageClear: String { return L10n.tr("Localizable", "StorageUsage.Clear") } /// Keep Media - case storageUsageKeepMedia + internal static var storageUsageKeepMedia: String { return L10n.tr("Localizable", "StorageUsage.KeepMedia") } /// 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. - case storageUsageKeepMediaDescription + internal static var storageUsageKeepMediaDescription: String { return L10n.tr("Localizable", "StorageUsage.KeepMedia.Description") } + /// 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. + internal static var storageUsageKeepMediaDescription1: String { return L10n.tr("Localizable", "StorageUsage.KeepMedia.Description1") } + /// If your cache size exceeds this limit, the oldest media will be deleted.\n\nAll media will stay in the Telegram cloud and can be re-downloaded if you need it again. + internal static var storageUsageLimitDesc: String { return L10n.tr("Localizable", "StorageUsage.Limit.Desc") } + /// MAXIMUM CACHE SIZE + internal static var storageUsageLimitHeader: String { return L10n.tr("Localizable", "StorageUsage.Limit.Header") } + /// No Limit + internal static var storageUsageLimitNoLimit: String { return L10n.tr("Localizable", "StorageUsage.Limit.NoLimit") } + /// Suggest Frequent Contacts + internal static var suggestFrequentContacts: String { return L10n.tr("Localizable", "Suggest.Frequent.Contacts") } + /// This will delete all data about the people you message frequently as well as the inline bots you are likely to use. + internal static var suggestFrequentContactsAlert: String { return L10n.tr("Localizable", "Suggest.Frequent.Contacts.Alert") } + /// Display people you message frequently at the top of the search section for quick access. + internal static var suggestFrequentContactsDesc: String { return L10n.tr("Localizable", "Suggest.Frequent.Contacts.Desc") } /// Choose your language - case suggestLocalizationHeader + internal static var suggestLocalizationHeader: String { return L10n.tr("Localizable", "Suggest.Localization.Header") } /// Other - case suggestLocalizationOther + internal static var suggestLocalizationOther: String { return L10n.tr("Localizable", "Suggest.Localization.Other") } /// Convert to Supergroup - case supergroupConvertButton + internal static var supergroupConvertButton: String { return L10n.tr("Localizable", "Supergroup.Convert.Button") } /// **In supergroups:**\n\n• 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 - case supergroupConvertDescription + internal static var supergroupConvertDescription: String { return L10n.tr("Localizable", "Supergroup.Convert.Description") } /// **Note**: This action cannot be undone. - case supergroupConvertUndone + internal static var supergroupConvertUndone: String { return L10n.tr("Localizable", "Supergroup.Convert.Undone") } /// Ban User - case supergroupDeleteRestrictionBanUser + internal static var supergroupDeleteRestrictionBanUser: String { return L10n.tr("Localizable", "Supergroup.DeleteRestriction.BanUser") } /// Delete All Messages - case supergroupDeleteRestrictionDeleteAllMessages + internal static var supergroupDeleteRestrictionDeleteAllMessages: String { return L10n.tr("Localizable", "Supergroup.DeleteRestriction.DeleteAllMessages") } /// Delete Message - case supergroupDeleteRestrictionDeleteMessage + internal static var supergroupDeleteRestrictionDeleteMessage: String { return L10n.tr("Localizable", "Supergroup.DeleteRestriction.DeleteMessage") } /// Report Spam - case supergroupDeleteRestrictionReportSpam - /// Quick Search - case sZhCtGQSTitle + internal static var supergroupDeleteRestrictionReportSpam: String { return L10n.tr("Localizable", "Supergroup.DeleteRestriction.ReportSpam") } + /// Manage Messages + internal static var supergroupDeleteRestrictionTitle: String { return L10n.tr("Localizable", "Supergroup.DeleteRestriction.Title") } + /// App Data Storage + internal static var systemMemoryWarningDataAndStorage: String { return L10n.tr("Localizable", "System.MemoryWarning.DataAndStorage") } + /// %d GB + internal static func systemMemoryWarningFreeSpace(_ p1: Int) -> String { + return L10n.tr("Localizable", "System.MemoryWarning.FreeSpace", p1) + } + /// Warning! + internal static var systemMemoryWarningHeader: String { return L10n.tr("Localizable", "System.MemoryWarning.Header") } + /// Less then 1GB + internal static var systemMemoryWarningLessThen1GB: String { return L10n.tr("Localizable", "System.MemoryWarning.LessThen1GB") } + /// OK + internal static var systemMemoryWarningOK: String { return L10n.tr("Localizable", "System.MemoryWarning.OK") } + /// Your Mac is running low on disk space. Please free up some space by removing unnecessary files or changing your cache settings.\n\nFree space available: ~%@ + internal static func systemMemoryWarningText(_ p1: String) -> String { + return L10n.tr("Localizable", "System.MemoryWarning.Text", p1) + } /// Window - case td7AD5loTitle + internal static var td7AD5loTitle: String { return L10n.tr("Localizable", "Td7-aD-5lo.title") } /// Appearance - case telegramAppearanceViewController + internal static var telegramAppearanceViewController: String { return L10n.tr("Localizable", "Telegram.AppearanceViewController") } /// Archived Stickers - case telegramArchivedStickerPacksController + internal static var telegramArchivedStickerPacksController: String { return L10n.tr("Localizable", "Telegram.ArchivedStickerPacksController") } /// Bio - case telegramBioViewController + internal static var telegramBioViewController: String { return L10n.tr("Localizable", "Telegram.BioViewController") } /// Blocked Users - case telegramBlockedPeersViewController + internal static var telegramBlockedPeersViewController: String { return L10n.tr("Localizable", "Telegram.BlockedPeersViewController") } /// Admins - case telegramChannelAdminsViewController - /// Blacklist - case telegramChannelBlacklistViewController + internal static var telegramChannelAdminsViewController: String { return L10n.tr("Localizable", "Telegram.ChannelAdminsViewController") } + /// Removed Users + internal static var telegramChannelBlacklistViewController: String { return L10n.tr("Localizable", "Telegram.ChannelBlacklistViewController") } /// All Actions - case telegramChannelEventLogController + internal static var telegramChannelEventLogController: String { return L10n.tr("Localizable", "Telegram.ChannelEventLogController") } /// Channel - case telegramChannelIntroViewController + internal static var telegramChannelIntroViewController: String { return L10n.tr("Localizable", "Telegram.ChannelIntroViewController") } /// Channel Members - case telegramChannelMembersViewController - /// Group - case telegramChannelVisibilityController + internal static var telegramChannelMembersViewController: String { return L10n.tr("Localizable", "Telegram.ChannelMembersViewController") } + /// Permissions + internal static var telegramChannelPermissionsController: String { return L10n.tr("Localizable", "Telegram.ChannelPermissionsController") } + /// Channel Stats + internal static var telegramChannelStatisticsController: String { return L10n.tr("Localizable", "Telegram.ChannelStatisticsController") } /// Supergroup - case telegramConvertGroupViewController - /// - case telegramEmptyChatViewController + internal static var telegramConvertGroupViewController: String { return L10n.tr("Localizable", "Telegram.ConvertGroupViewController") } + /// Data and Storage + internal static var telegramDataAndStorageViewController: String { return L10n.tr("Localizable", "Telegram.DataAndStorageViewController") } /// Trending Stickers - case telegramFeaturedStickerPacksController + internal static var telegramFeaturedStickerPacksController: String { return L10n.tr("Localizable", "Telegram.FeaturedStickerPacksController") } + /// Forward Messages + internal static var telegramForwardChatListController: String { return L10n.tr("Localizable", "Telegram.ForwardChatListController") } /// General Settings - case telegramGeneralSettingsViewController + internal static var telegramGeneralSettingsViewController: String { return L10n.tr("Localizable", "Telegram.GeneralSettingsViewController") } /// Admins - case telegramGroupAdminsController + internal static var telegramGroupAdminsController: String { return L10n.tr("Localizable", "Telegram.GroupAdminsController") } /// Groups In Common - case telegramGroupsInCommonViewController + internal static var telegramGroupsInCommonViewController: String { return L10n.tr("Localizable", "Telegram.GroupsInCommonViewController") } /// Group Sticker Set - case telegramGroupStickerSetController + internal static var telegramGroupStickerSetController: String { return L10n.tr("Localizable", "Telegram.GroupStickerSetController") } /// Stickers - case telegramInstalledStickerPacksController + internal static var telegramInstalledStickerPacksController: String { return L10n.tr("Localizable", "Telegram.InstalledStickerPacksController") } /// Language - case telegramLanguageViewController + internal static var telegramLanguageViewController: String { return L10n.tr("Localizable", "Telegram.LanguageViewController") } /// Settings - case telegramLayoutAccountController + internal static var telegramLayoutAccountController: String { return L10n.tr("Localizable", "Telegram.LayoutAccountController") } /// Recent Calls - case telegramLayoutRecentCallsViewController + internal static var telegramLayoutRecentCallsViewController: String { return L10n.tr("Localizable", "Telegram.LayoutRecentCallsViewController") } /// Invite Link - case telegramLinkInvationController - /// - case telegramMainViewController + internal static var telegramLinkInvationController: String { return L10n.tr("Localizable", "Telegram.LinkInvationController") } /// Notifications - case telegramNotificationSettingsViewController + internal static var telegramNotificationSettingsViewController: String { return L10n.tr("Localizable", "Telegram.NotificationSettingsViewController") } /// Passcode - case telegramPasscodeSettingsViewController + internal static var telegramPasscodeSettingsViewController: String { return L10n.tr("Localizable", "Telegram.PasscodeSettingsViewController") } + /// Passport + internal static var telegramPassportController: String { return L10n.tr("Localizable", "Telegram.PassportController") } /// Info - case telegramPeerInfoController + internal static var telegramPeerInfoController: String { return L10n.tr("Localizable", "Telegram.PeerInfoController") } + /// Shared Media + internal static var telegramPeerMediaController: String { return L10n.tr("Localizable", "Telegram.PeerMediaController") } /// Change Number - case telegramPhoneNumberConfirmController - /// Change Phone Number - case telegramPhoneNumberIntroController + internal static var telegramPhoneNumberConfirmController: String { return L10n.tr("Localizable", "Telegram.PhoneNumberConfirmController") } /// Chat History Settings - case telegramPreHistorySettingsController + internal static var telegramPreHistorySettingsController: String { return L10n.tr("Localizable", "Telegram.PreHistorySettingsController") } /// Privacy and Security - case telegramPrivacyAndSecurityViewController + internal static var telegramPrivacyAndSecurityViewController: String { return L10n.tr("Localizable", "Telegram.PrivacyAndSecurityViewController") } /// Proxy - case telegramProxySettingsViewController + internal static var telegramProxySettingsViewController: String { return L10n.tr("Localizable", "Telegram.ProxySettingsViewController") } /// Active Sessions - case telegramRecentSessionsController + internal static var telegramRecentSessionsController: String { return L10n.tr("Localizable", "Telegram.RecentSessionsController") } /// Encryption Key - case telegramSecretChatKeyViewController + internal static var telegramSecretChatKeyViewController: String { return L10n.tr("Localizable", "Telegram.SecretChatKeyViewController") } /// Select Users - case telegramSelectPeersController + internal static var telegramSelectPeersController: String { return L10n.tr("Localizable", "Telegram.SelectPeersController") } /// Storage Usage - case telegramStorageUsageController + internal static var telegramStorageUsageController: String { return L10n.tr("Localizable", "Telegram.StorageUsageController") } /// Two-Step Verification - case telegramTwoStepVerificationUnlockController + internal static var telegramTwoStepVerificationUnlockController: String { return L10n.tr("Localizable", "Telegram.TwoStepVerificationUnlockController") } /// Username - case telegramUsernameSettingsViewController - /// Copy - case textCopy - /// Make Bold - case textViewTransformBold - /// Make Monospace - case textViewTransformCode - /// Make Italic - case textViewTransformItalic + internal static var telegramUsernameSettingsViewController: String { return L10n.tr("Localizable", "Telegram.UsernameSettingsViewController") } + /// Logged in with Telegram + internal static var telegramWebSessionsController: String { return L10n.tr("Localizable", "Telegram.WebSessionsController") } + /// Channel + internal static var telegramChannelVisibilityControllerChannel: String { return L10n.tr("Localizable", "Telegram.ChannelVisibilityController.Channel") } + /// Group + internal static var telegramChannelVisibilityControllerGroup: String { return L10n.tr("Localizable", "Telegram.ChannelVisibilityController.Group") } + /// Telegram needs to optimize its database after this update. This may take a few minutes, sorry for the inconvenience. + internal static var telegramUpgradeDatabaseText: String { return L10n.tr("Localizable", "Telegram.UpgradeDatabase.Text") } + /// Optimizing Database + internal static var telegramUpgradeDatabaseTitle: String { return L10n.tr("Localizable", "Telegram.UpgradeDatabase.Title") } + /// Agree & Continue + internal static var termsOfServiceAccept: String { return L10n.tr("Localizable", "TermsOfService.Accept") } + /// I confirm that I am %@ or over. + internal static func termsOfServiceConfirmAge(_ p1: String) -> String { + return L10n.tr("Localizable", "TermsOfService.ConfirmAge", p1) + } + /// Decline + internal static var termsOfServiceDisagree: String { return L10n.tr("Localizable", "TermsOfService.Disagree") } + /// Please agree and proceed to %@. + internal static func termsOfServiceProceedBot(_ p1: String) -> String { + return L10n.tr("Localizable", "TermsOfService.ProceedBot", p1) + } + /// Terms of Service + internal static var termsOfServiceTitle: String { return L10n.tr("Localizable", "TermsOfService.Title") } + /// Confirm + internal static var termsOfServiceAcceptConfirmAge: String { return L10n.tr("Localizable", "TermsOfService.Accept.ConfirmAge") } + /// Decline & Deactivate + internal static var termsOfServiceDisagreeOK: String { return L10n.tr("Localizable", "TermsOfService.Disagree.OK") } + /// 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 requirements, it won't be possible for us to provide you with this service. + internal static var termsOfServiceDisagreeText: String { return L10n.tr("Localizable", "TermsOfService.Disagree.Text") } + /// Warning, this will irreversibly delete your Telegram account along with all the data you store in the Telegram cloud.\n\nImportant: You can Cancel now and export your data before deleting your account instead of losing it all. (To do this, open the latest version of Telegram Desktop and go to Settings > Export Telegram Data.) + internal static var termsOfServiceDisagreeTextLast: String { return L10n.tr("Localizable", "TermsOfService.Disagree.Text.Last") } + /// Delete Now + internal static var termsOfServiceDisagreeTextLastOK: String { return L10n.tr("Localizable", "TermsOfService.Disagree.Text.Last.OK") } + /// Copy Selected Text + internal static var textCopy: String { return L10n.tr("Localizable", "Text.Copy") } + /// Copy About + internal static var textCopyLabelAbout: String { return L10n.tr("Localizable", "Text.CopyLabel_About") } + /// Copy Bio + internal static var textCopyLabelBio: String { return L10n.tr("Localizable", "Text.CopyLabel_Bio") } + /// Copy Phone Number + internal static var textCopyLabelPhoneNumber: String { return L10n.tr("Localizable", "Text.CopyLabel_PhoneNumber") } + /// Copy Share Link + internal static var textCopyLabelShareLink: String { return L10n.tr("Localizable", "Text.CopyLabel_ShareLink") } + /// Copy Username + internal static var textCopyLabelUsername: String { return L10n.tr("Localizable", "Text.CopyLabel_Username") } + /// Copy Text + internal static var textCopyText: String { return L10n.tr("Localizable", "Text.CopyText") } + /// Copy Code + internal static var textContextCopyCode: String { return L10n.tr("Localizable", "Text.Context.Copy.Code") } + /// Copy Command + internal static var textContextCopyCommand: String { return L10n.tr("Localizable", "Text.Context.Copy.Command") } + /// Copy Email + internal static var textContextCopyEmail: String { return L10n.tr("Localizable", "Text.Context.Copy.Email") } + /// Copy Hashtag + internal static var textContextCopyHashtag: String { return L10n.tr("Localizable", "Text.Context.Copy.Hashtag") } + /// Copy Invite Link + internal static var textContextCopyInviteLink: String { return L10n.tr("Localizable", "Text.Context.Copy.InviteLink") } + /// Copy Link + internal static var textContextCopyLink: String { return L10n.tr("Localizable", "Text.Context.Copy.Link") } + /// Copy Sticker Pack + internal static var textContextCopyStickerPack: String { return L10n.tr("Localizable", "Text.Context.Copy.StickerPack") } + /// Copy Username + internal static var textContextCopyUsername: String { return L10n.tr("Localizable", "Text.Context.Copy.Username") } + /// Transformations + internal static var textViewTransformations: String { return L10n.tr("Localizable", "Text.View.Transformations") } + /// Bold + internal static var textViewTransformBold: String { return L10n.tr("Localizable", "TextView.Transform.Bold") } + /// Monospace + internal static var textViewTransformCode: String { return L10n.tr("Localizable", "TextView.Transform.Code") } + /// Italic + internal static var textViewTransformItalic: String { return L10n.tr("Localizable", "TextView.Transform.Italic") } + /// Clear Transformations + internal static var textViewTransformRemoveAll: String { return L10n.tr("Localizable", "TextView.Transform.RemoveAll") } + /// Make URL + internal static var textViewTransformURL: String { return L10n.tr("Localizable", "TextView.Transform.URL") } + /// Sorry, this theme doesn't seem to exist for macOS. + internal static var themeGetThemeError: String { return L10n.tr("Localizable", "Theme.GetTheme.Error") } + /// Theme Preview + internal static var themePreviewTitle: String { return L10n.tr("Localizable", "ThemePreview.Title") } + /// %d + internal static func themePreviewUsesCountCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "ThemePreview.UsesCount_countable", p1) + } + /// %d people are using this theme + internal static func themePreviewUsesCountFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "ThemePreview.UsesCount_few", p1) + } + /// %d people are using this theme + internal static func themePreviewUsesCountMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "ThemePreview.UsesCount_many", p1) + } + /// %d person is using this theme + internal static func themePreviewUsesCountOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "ThemePreview.UsesCount_one", p1) + } + /// %d people are using this theme + internal static func themePreviewUsesCountOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "ThemePreview.UsesCount_other", p1) + } + /// %d people are using this theme + internal static func themePreviewUsesCountTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "ThemePreview.UsesCount_two", p1) + } + /// %d person is using this theme + internal static func themePreviewUsesCountZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "ThemePreview.UsesCount_zero", p1) + } /// at - case timeAt + internal static var timeAt: String { return L10n.tr("Localizable", "Time.at") } /// last seen - case timeLastSeen + internal static var timeLastSeen: String { return L10n.tr("Localizable", "Time.last_seen") } + /// Jan %@, %@ at %@ + internal static func timePreciseDateM1(_ p1: String, _ p2: String, _ p3: String) -> String { + return L10n.tr("Localizable", "Time.PreciseDate_m1", p1, p2, p3) + } + /// Oct %@, %@ at %@ + internal static func timePreciseDateM10(_ p1: String, _ p2: String, _ p3: String) -> String { + return L10n.tr("Localizable", "Time.PreciseDate_m10", p1, p2, p3) + } + /// Nov %@, %@ at %@ + internal static func timePreciseDateM11(_ p1: String, _ p2: String, _ p3: String) -> String { + return L10n.tr("Localizable", "Time.PreciseDate_m11", p1, p2, p3) + } + /// Dec %@, %@ at %@ + internal static func timePreciseDateM12(_ p1: String, _ p2: String, _ p3: String) -> String { + return L10n.tr("Localizable", "Time.PreciseDate_m12", p1, p2, p3) + } + /// Feb %@, %@ at %@ + internal static func timePreciseDateM2(_ p1: String, _ p2: String, _ p3: String) -> String { + return L10n.tr("Localizable", "Time.PreciseDate_m2", p1, p2, p3) + } + /// Mar %@, %@ at %@ + internal static func timePreciseDateM3(_ p1: String, _ p2: String, _ p3: String) -> String { + return L10n.tr("Localizable", "Time.PreciseDate_m3", p1, p2, p3) + } + /// Apr %@, %@ at %@ + internal static func timePreciseDateM4(_ p1: String, _ p2: String, _ p3: String) -> String { + return L10n.tr("Localizable", "Time.PreciseDate_m4", p1, p2, p3) + } + /// May %@, %@ at %@ + internal static func timePreciseDateM5(_ p1: String, _ p2: String, _ p3: String) -> String { + return L10n.tr("Localizable", "Time.PreciseDate_m5", p1, p2, p3) + } + /// Jun %@, %@ at %@ + internal static func timePreciseDateM6(_ p1: String, _ p2: String, _ p3: String) -> String { + return L10n.tr("Localizable", "Time.PreciseDate_m6", p1, p2, p3) + } + /// Jul %@, %@ at %@ + internal static func timePreciseDateM7(_ p1: String, _ p2: String, _ p3: String) -> String { + return L10n.tr("Localizable", "Time.PreciseDate_m7", p1, p2, p3) + } + /// Aug %@, %@ at %@ + internal static func timePreciseDateM8(_ p1: String, _ p2: String, _ p3: String) -> String { + return L10n.tr("Localizable", "Time.PreciseDate_m8", p1, p2, p3) + } + /// Sep %@, %@ at %@ + internal static func timePreciseDateM9(_ p1: String, _ p2: String, _ p3: String) -> String { + return L10n.tr("Localizable", "Time.PreciseDate_m9", p1, p2, p3) + } + /// Jan %@ at %@ + internal static func timePreciseMediumDateM1(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Time.PreciseMediumDate_m1", p1, p2) + } + /// Oct %@ at %@ + internal static func timePreciseMediumDateM10(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Time.PreciseMediumDate_m10", p1, p2) + } + /// Nov %@ at %@ + internal static func timePreciseMediumDateM11(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Time.PreciseMediumDate_m11", p1, p2) + } + /// Dec %@ at %@ + internal static func timePreciseMediumDateM12(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Time.PreciseMediumDate_m12", p1, p2) + } + /// Feb %@ at %@ + internal static func timePreciseMediumDateM2(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Time.PreciseMediumDate_m2", p1, p2) + } + /// Mar %@ at %@ + internal static func timePreciseMediumDateM3(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Time.PreciseMediumDate_m3", p1, p2) + } + /// Apr %@ at %@ + internal static func timePreciseMediumDateM4(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Time.PreciseMediumDate_m4", p1, p2) + } + /// May %@ at %@ + internal static func timePreciseMediumDateM5(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Time.PreciseMediumDate_m5", p1, p2) + } + /// Jun %@ at %@ + internal static func timePreciseMediumDateM6(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Time.PreciseMediumDate_m6", p1, p2) + } + /// Jul %@ at %@ + internal static func timePreciseMediumDateM7(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Time.PreciseMediumDate_m7", p1, p2) + } + /// Aug %@ at %@ + internal static func timePreciseMediumDateM8(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Time.PreciseMediumDate_m8", p1, p2) + } + /// Sep %@, at %@ + internal static func timePreciseMediumDateM9(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "Time.PreciseMediumDate_m9", p1, p2) + } /// today - case timeToday + internal static var timeToday: String { return L10n.tr("Localizable", "Time.today") } + /// today at %@ + internal static func timeTodayAt(_ p1: String) -> String { + return L10n.tr("Localizable", "Time.TodayAt", p1) + } + /// tomorrow at %@ + internal static func timeTomorrow(_ p1: String) -> String { + return L10n.tr("Localizable", "Time.tomorrow", p1) + } + /// tomorrow at %@ + internal static func timeTomorrowAt(_ p1: String) -> String { + return L10n.tr("Localizable", "Time.TomorrowAt", p1) + } /// yesterday - case timeYesterday + internal static var timeYesterday: String { return L10n.tr("Localizable", "Time.yesterday") } + /// yesterday at %@ + internal static func timeYesterdayAt(_ p1: String) -> String { + return L10n.tr("Localizable", "Time.YesterdayAt", p1) + } /// %d - case timerDaysCountable(Int) + internal static func timerDaysCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Days_countable", p1) + } /// %d days - case timerDaysFew(Int) + internal static func timerDaysFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Days_few", p1) + } /// %d days - case timerDaysMany(Int) + internal static func timerDaysMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Days_many", p1) + } /// %d day - case timerDaysOne(Int) + internal static func timerDaysOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Days_one", p1) + } /// %d days - case timerDaysOther(Int) + internal static func timerDaysOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Days_other", p1) + } /// %d days - case timerDaysTwo(Int) + internal static func timerDaysTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Days_two", p1) + } /// %d days - case timerDaysZero(Int) + internal static func timerDaysZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Days_zero", p1) + } /// Forever - case timerForever + internal static var timerForever: String { return L10n.tr("Localizable", "Timer.Forever") } /// %d - case timerHoursCountable(Int) + internal static func timerHoursCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Hours_countable", p1) + } /// %d hours - case timerHoursFew(Int) + internal static func timerHoursFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Hours_few", p1) + } /// %d hours - case timerHoursMany(Int) + internal static func timerHoursMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Hours_many", p1) + } /// %d hour - case timerHoursOne(Int) + internal static func timerHoursOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Hours_one", p1) + } /// %d hours - case timerHoursOther(Int) + internal static func timerHoursOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Hours_other", p1) + } /// %d hours - case timerHoursTwo(Int) + internal static func timerHoursTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Hours_two", p1) + } /// %d hours - case timerHoursZero(Int) + internal static func timerHoursZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Hours_zero", p1) + } /// %d - case timerMinutesCountable(Int) + internal static func timerMinutesCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Minutes_countable", p1) + } /// %d minutes - case timerMinutesFew(Int) + internal static func timerMinutesFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Minutes_few", p1) + } /// %d minutes - case timerMinutesMany(Int) + internal static func timerMinutesMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Minutes_many", p1) + } /// %d minute - case timerMinutesOne(Int) + internal static func timerMinutesOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Minutes_one", p1) + } /// %d minutes - case timerMinutesOther(Int) + internal static func timerMinutesOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Minutes_other", p1) + } /// %d minutes - case timerMinutesTwo(Int) + internal static func timerMinutesTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Minutes_two", p1) + } /// %d minutes - case timerMinutesZero(Int) + internal static func timerMinutesZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Minutes_zero", p1) + } /// %d - case timerMonthsCountable(Int) + internal static func timerMonthsCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Months_countable", p1) + } /// %d months - case timerMonthsFew(Int) + internal static func timerMonthsFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Months_few", p1) + } /// %d months - case timerMonthsMany(Int) + internal static func timerMonthsMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Months_many", p1) + } /// %d month - case timerMonthsOne(Int) + internal static func timerMonthsOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Months_one", p1) + } /// %d months - case timerMonthsOther(Int) + internal static func timerMonthsOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Months_other", p1) + } /// %d months - case timerMonthsTwo(Int) + internal static func timerMonthsTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Months_two", p1) + } /// %d months - case timerMonthsZero(Int) + internal static func timerMonthsZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Months_zero", p1) + } /// %d - case timerSecondsCountable(Int) + internal static func timerSecondsCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Seconds_countable", p1) + } /// %d seconds - case timerSecondsFew(Int) + internal static func timerSecondsFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Seconds_few", p1) + } /// %d seconds - case timerSecondsMany(Int) + internal static func timerSecondsMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Seconds_many", p1) + } /// %d second - case timerSecondsOne(Int) + internal static func timerSecondsOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Seconds_one", p1) + } /// %d seconds - case timerSecondsOther(Int) + internal static func timerSecondsOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Seconds_other", p1) + } /// %d seconds - case timerSecondsTwo(Int) + internal static func timerSecondsTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Seconds_two", p1) + } /// %d seconds - case timerSecondsZero(Int) + internal static func timerSecondsZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Seconds_zero", p1) + } /// %d - case timerWeeksCountable(Int) + internal static func timerWeeksCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Weeks_countable", p1) + } /// %d weeks - case timerWeeksFew(Int) + internal static func timerWeeksFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Weeks_few", p1) + } /// %d weeks - case timerWeeksMany(Int) + internal static func timerWeeksMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Weeks_many", p1) + } /// %d week - case timerWeeksOne(Int) + internal static func timerWeeksOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Weeks_one", p1) + } /// %d weeks - case timerWeeksOther(Int) + internal static func timerWeeksOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Weeks_other", p1) + } /// %d weeks - case timerWeeksTwo(Int) + internal static func timerWeeksTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Weeks_two", p1) + } /// %d weeks - case timerWeeksZero(Int) + internal static func timerWeeksZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Weeks_zero", p1) + } /// %d - case timerYearsCountable(Int) + internal static func timerYearsCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Years_countable", p1) + } /// %d years - case timerYearsFew(Int) + internal static func timerYearsFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Years_few", p1) + } /// %d years - case timerYearsMany(Int) + internal static func timerYearsMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Years_many", p1) + } /// %d year - case timerYearsOne(Int) + internal static func timerYearsOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Years_one", p1) + } /// %d years - case timerYearsOther(Int) + internal static func timerYearsOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Years_other", p1) + } /// %d years - case timerYearsTwo(Int) + internal static func timerYearsTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Years_two", p1) + } /// %d years - case timerYearsZero(Int) - /// Data Detectors - case tRrPd1PSTitle + internal static func timerYearsZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "Timer.Years_zero", p1) + } + /// Auto-delete timer set to 1 day. + internal static var tipAutoDeleteTimerSetForDay: String { return L10n.tr("Localizable", "Tip.AutoDelete.TimerSetForDay") } + /// Auto-delete timer set to 1 week. + internal static var tipAutoDeleteTimerSetForWeek: String { return L10n.tr("Localizable", "Tip.AutoDelete.TimerSetForWeek") } + /// Auto-delete timer is now disabled. + internal static var tipAutoDeleteTimerSetOff: String { return L10n.tr("Localizable", "Tip.AutoDelete.TimerSetOff") } + /// Muted + internal static var toastMuted: String { return L10n.tr("Localizable", "Toast.Muted") } + /// Unmuted + internal static var toastUnmuted: String { return L10n.tr("Localizable", "Toast.Unmuted") } + /// Attach + internal static var touchBarAttach: String { return L10n.tr("Localizable", "TouchBar.Attach") } + /// Call + internal static var touchBarCall: String { return L10n.tr("Localizable", "TouchBar.Call") } + /// Favorite + internal static var touchBarFavorite: String { return L10n.tr("Localizable", "TouchBar.Favorite") } + /// Recent + internal static var touchBarRecent: String { return L10n.tr("Localizable", "TouchBar.Recent") } + /// Recently Used + internal static var touchBarRecentlyUsed: String { return L10n.tr("Localizable", "TouchBar.RecentlyUsed") } + /// Search for messages or users + internal static var touchBarSearchUsersOrMessages: String { return L10n.tr("Localizable", "TouchBar.SearchUsersOrMessages") } + /// Start Secret Chat + internal static var touchBarStartSecretChat: String { return L10n.tr("Localizable", "TouchBar.StartSecretChat") } + /// Replace with File + internal static var touchBarEditMessageReplaceWithFile: String { return L10n.tr("Localizable", "TouchBar.EditMessage.ReplaceWithFile") } + /// Replace with Media + internal static var touchBarEditMessageReplaceWithMedia: String { return L10n.tr("Localizable", "TouchBar.EditMessage.ReplaceWithMedia") } + /// Chat Actions + internal static var touchBarLabelChatActions: String { return L10n.tr("Localizable", "TouchBarLabel.ChatActions") } + /// Emoji & Stickers + internal static var touchBarLabelEmojiAndStickers: String { return L10n.tr("Localizable", "TouchBarLabel.EmojiAndStickers") } + /// New Chat + internal static var touchBarLabelNewChat: String { return L10n.tr("Localizable", "TouchBarLabel.NewChat") } /// Skip - case twoStepAuthEmailSkip + internal static var twoStepAuthEmailSkip: String { return L10n.tr("Localizable", "TwoStep.AuthEmailSkip") } /// An error occured. Please try again later. - case twoStepAuthAnError + internal static var twoStepAuthAnError: String { return L10n.tr("Localizable", "TwoStepAuth.AnError") } + /// Cancel Reset + internal static var twoStepAuthCancelReset: String { return L10n.tr("Localizable", "TwoStepAuth.CancelReset") } /// Change Recovery E-Mail - case twoStepAuthChangeEmail + internal static var twoStepAuthChangeEmail: String { return L10n.tr("Localizable", "TwoStepAuth.ChangeEmail") } /// Change Password - case twoStepAuthChangePassword + internal static var twoStepAuthChangePassword: String { return L10n.tr("Localizable", "TwoStepAuth.ChangePassword") } + /// Please enter a new password which will be used to protect your data. + internal static var twoStepAuthChangePasswordDesc: String { return L10n.tr("Localizable", "TwoStepAuth.ChangePasswordDesc") } /// Abort Two-Step Verification Setup - case twoStepAuthConfirmationAbort - /// 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. - case twoStepAuthConfirmationText + internal static var twoStepAuthConfirmationAbort: String { return L10n.tr("Localizable", "TwoStepAuth.ConfirmationAbort") } + /// Please check your e-mail and enter the confirmation code to complete Two-Step Verification setup. Be sure to check the spam folder as well. + internal static var twoStepAuthConfirmationTextNew: String { return L10n.tr("Localizable", "TwoStepAuth.ConfirmationTextNew") } + /// Please enter the code we've just emailed at %@. + internal static func twoStepAuthConfirmEmailCodeDesc(_ p1: String) -> String { + return L10n.tr("Localizable", "TwoStepAuth.ConfirmEmailCodeDesc", p1) + } /// E-Mail - case twoStepAuthEmail + internal static var twoStepAuthEmail: String { return L10n.tr("Localizable", "TwoStepAuth.Email") } + /// This confirmation code has expired. Please try again. + internal static var twoStepAuthEmailCodeExpired: String { return L10n.tr("Localizable", "TwoStepAuth.EmailCodeExpired") } + /// You have entered an invalid code. Please try again. + internal static var twoStepAuthEmailCodeInvalid: String { return L10n.tr("Localizable", "TwoStepAuth.EmailCodeInvalid") } /// Please add your valid e-mail. It is the only way to recover a forgotten password. - case twoStepAuthEmailHelp + internal static var twoStepAuthEmailHelp: String { return L10n.tr("Localizable", "TwoStepAuth.EmailHelp") } + /// Please enter your new recovery email. It is the only way to recover a forgotten password. + internal static var twoStepAuthEmailHelpChange: String { return L10n.tr("Localizable", "TwoStepAuth.EmailHelpChange") } /// Invalid e-mail address. Please try again. - case twoStepAuthEmailInvalid + internal static var twoStepAuthEmailInvalid: String { return L10n.tr("Localizable", "TwoStepAuth.EmailInvalid") } /// We have sent you an e-mail to confirm your address. - case twoStepAuthEmailSent + internal static var twoStepAuthEmailSent: String { return L10n.tr("Localizable", "TwoStepAuth.EmailSent") } /// No, seriously.\n\nIf you forget your password, you will lose access to your Telegram account. There will be no way to restore it. - case twoStepAuthEmailSkipAlert + internal static var twoStepAuthEmailSkipAlert: String { return L10n.tr("Localizable", "TwoStepAuth.EmailSkipAlert") } + /// Enter Code + internal static var twoStepAuthEnterEmailCode: String { return L10n.tr("Localizable", "TwoStepAuth.EnterEmailCode") } /// Forgot password? - case twoStepAuthEnterPasswordForgot + internal static var twoStepAuthEnterPasswordForgot: String { return L10n.tr("Localizable", "TwoStepAuth.EnterPasswordForgot") } /// You have enabled Two-Step Verification, so your account is protected with an additional password. - case twoStepAuthEnterPasswordHelp + internal static var twoStepAuthEnterPasswordHelp: String { return L10n.tr("Localizable", "TwoStepAuth.EnterPasswordHelp") } /// Hint: %@ - case twoStepAuthEnterPasswordHint(String) + internal static func twoStepAuthEnterPasswordHint(_ p1: String) -> String { + return L10n.tr("Localizable", "TwoStepAuth.EnterPasswordHint", p1) + } /// Password - case twoStepAuthEnterPasswordPassword + internal static var twoStepAuthEnterPasswordPassword: String { return L10n.tr("Localizable", "TwoStepAuth.EnterPasswordPassword") } /// Limit exceeded. Please try again later. - case twoStepAuthFloodError + internal static var twoStepAuthFloodError: String { return L10n.tr("Localizable", "TwoStepAuth.FloodError") } /// An error occurred. Please try again later. - case twoStepAuthGenericError + internal static var twoStepAuthGenericError: String { return L10n.tr("Localizable", "TwoStepAuth.GenericError") } /// You have enabled Two-Step verification.\nYou'll need the password you set up here to log in to your Telegram account. - case twoStepAuthGenericHelp + internal static var twoStepAuthGenericHelp: String { return L10n.tr("Localizable", "TwoStepAuth.GenericHelp") } + /// Invalid password. Please try again. + internal static var twoStepAuthInvalidPasswordError: String { return L10n.tr("Localizable", "TwoStepAuth.InvalidPasswordError") } /// Password - case twoStepAuthPasswordTitle + internal static var twoStepAuthPasswordTitle: String { return L10n.tr("Localizable", "TwoStepAuth.PasswordTitle") } /// Your recovery e-mail %@ is not yet active and pending confirmation. - case twoStepAuthPendingEmailHelp(String) + internal static func twoStepAuthPendingEmailHelp(_ p1: String) -> String { + return L10n.tr("Localizable", "TwoStepAuth.PendingEmailHelp", p1) + } /// Code - case twoStepAuthRecoveryCode + internal static var twoStepAuthRecoveryCode: String { return L10n.tr("Localizable", "TwoStepAuth.RecoveryCode") } /// Code Expired - case twoStepAuthRecoveryCodeExpired + internal static var twoStepAuthRecoveryCodeExpired: String { return L10n.tr("Localizable", "TwoStepAuth.RecoveryCodeExpired") } /// Please check your e-mail and enter the 6-digit code we've sent there to deactivate your cloud password. - case twoStepAuthRecoveryCodeHelp + internal static var twoStepAuthRecoveryCodeHelp: String { return L10n.tr("Localizable", "TwoStepAuth.RecoveryCodeHelp") } /// Invalid code. Please try again. - case twoStepAuthRecoveryCodeInvalid - /// Having trouble accessing your e-mail %@? - case twoStepAuthRecoveryEmailUnavailable(String) + internal static var twoStepAuthRecoveryCodeInvalid: String { return L10n.tr("Localizable", "TwoStepAuth.RecoveryCodeInvalid") } + /// Having trouble accessing your e-mail\n[%@]()? + internal static func twoStepAuthRecoveryEmailUnavailableNew(_ p1: String) -> String { + return L10n.tr("Localizable", "TwoStepAuth.RecoveryEmailUnavailableNew", p1) + } /// Your remaining options are either to remember your password or to reset your account. - case twoStepAuthRecoveryFailed + internal static var twoStepAuthRecoveryFailed: String { return L10n.tr("Localizable", "TwoStepAuth.RecoveryFailed") } /// We have sent a recovery code to the e-mail you provided:\n\n%@ - case twoStepAuthRecoverySent(String) + internal static func twoStepAuthRecoverySent(_ p1: String) -> String { + return L10n.tr("Localizable", "TwoStepAuth.RecoverySent", p1) + } /// E-Mail Code - case twoStepAuthRecoveryTitle + internal static var twoStepAuthRecoveryTitle: String { return L10n.tr("Localizable", "TwoStepAuth.RecoveryTitle") } /// 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. - case twoStepAuthRecoveryUnavailable + internal static var twoStepAuthRecoveryUnavailable: String { return L10n.tr("Localizable", "TwoStepAuth.RecoveryUnavailable") } /// Turn Password Off - case twoStepAuthRemovePassword + internal static var twoStepAuthRemovePassword: String { return L10n.tr("Localizable", "TwoStepAuth.RemovePassword") } + /// Reset Password + internal static var twoStepAuthReset: String { return L10n.tr("Localizable", "TwoStepAuth.Reset") } + /// 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.\n\nYou'll be able to reset your account in:\n%@ + internal static func twoStepAuthResetDescription(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "TwoStepAuth.ResetDescription", p1, p2) + } + /// You can reset your password in %@. + internal static func twoStepAuthResetPending(_ p1: String) -> String { + return L10n.tr("Localizable", "TwoStepAuth.ResetPending", p1) + } + /// You have successfully reset your password. Do you want to create a new one? + internal static var twoStepAuthResetSuccess: String { return L10n.tr("Localizable", "TwoStepAuth.ResetSuccess") } /// Set Additional Password - case twoStepAuthSetPassword + internal static var twoStepAuthSetPassword: String { return L10n.tr("Localizable", "TwoStepAuth.SetPassword") } /// 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. - case twoStepAuthSetPasswordHelp + internal static var twoStepAuthSetPasswordHelp: String { return L10n.tr("Localizable", "TwoStepAuth.SetPasswordHelp") } /// Set Recovery E-Mail - case twoStepAuthSetupEmail + internal static var twoStepAuthSetupEmail: String { return L10n.tr("Localizable", "TwoStepAuth.SetupEmail") } /// Recovery E-Mail - case twoStepAuthSetupEmailTitle - /// Please create a hint for your password - case twoStepAuthSetupHint + internal static var twoStepAuthSetupEmailTitle: String { return L10n.tr("Localizable", "TwoStepAuth.SetupEmailTitle") } + /// You can create an optional hint for your password. + internal static var twoStepAuthSetupHintDesc: String { return L10n.tr("Localizable", "TwoStepAuth.SetupHintDesc") } + /// Hint + internal static var twoStepAuthSetupHintPlaceholder: String { return L10n.tr("Localizable", "TwoStepAuth.SetupHintPlaceholder") } /// Password Hint - case twoStepAuthSetupHintTitle + internal static var twoStepAuthSetupHintTitle: String { return L10n.tr("Localizable", "TwoStepAuth.SetupHintTitle") } /// Passwords don't match. Please try again. - case twoStepAuthSetupPasswordConfirmFailed - /// Please re-enter your password - case twoStepAuthSetupPasswordConfirmPassword - /// Enter a password - case twoStepAuthSetupPasswordEnterPassword - /// Please enter your new password - case twoStepAuthSetupPasswordEnterPasswordNew + internal static var twoStepAuthSetupPasswordConfirmFailed: String { return L10n.tr("Localizable", "TwoStepAuth.SetupPasswordConfirmFailed") } + /// Re-enter your password + internal static var twoStepAuthSetupPasswordConfirmPassword: String { return L10n.tr("Localizable", "TwoStepAuth.SetupPasswordConfirmPassword") } + /// Please confirm your password. + internal static var twoStepAuthSetupPasswordConfirmPasswordDesc: String { return L10n.tr("Localizable", "TwoStepAuth.SetupPasswordConfirmPasswordDesc") } + /// Please create a password which will be used to protect your data. + internal static var twoStepAuthSetupPasswordDesc: String { return L10n.tr("Localizable", "TwoStepAuth.SetupPasswordDesc") } + /// Enter your cloud password + internal static var twoStepAuthSetupPasswordEnterPassword: String { return L10n.tr("Localizable", "TwoStepAuth.SetupPasswordEnterPassword") } + /// Enter your new password + internal static var twoStepAuthSetupPasswordEnterPasswordNew: String { return L10n.tr("Localizable", "TwoStepAuth.SetupPasswordEnterPasswordNew") } /// Your Password - case twoStepAuthSetupPasswordTitle + internal static var twoStepAuthSetupPasswordTitle: String { return L10n.tr("Localizable", "TwoStepAuth.SetupPasswordTitle") } + /// Unable to reset password, please try again at %@ + internal static func twoStepAuthUnableToReset(_ p1: String) -> String { + return L10n.tr("Localizable", "TwoStepAuth.UnableToReset", p1) + } + /// Cancel Reset + internal static var twoStepAuthCancelResetConfirm: String { return L10n.tr("Localizable", "TwoStepAuth.CancelReset.Confirm") } + /// Cancel the password resetting process? If you proceed, the expired part of the 7-day delay will be lost. + internal static var twoStepAuthCancelResetText: String { return L10n.tr("Localizable", "TwoStepAuth.CancelReset.Text") } /// Are you sure you want to disable your password? - case twoStepAuthConfirmDisablePassword + internal static var twoStepAuthConfirmDisablePassword: String { return L10n.tr("Localizable", "TwoStepAuth.Confirm.DisablePassword") } /// An error occured. Please try again later. - case twoStepAuthErrorGeneric + internal static var twoStepAuthErrorGeneric: String { return L10n.tr("Localizable", "TwoStepAuth.Error.Generic") } /// 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. - case twoStepAuthErrorHaventEmail + internal static var twoStepAuthErrorHaventEmail: String { return L10n.tr("Localizable", "TwoStepAuth.Error.HaventEmail") } /// Please enter valid e-mail address. - case twoStepAuthErrorInvalidEmail - /// You have entered invalid password too many times. Please try again later. - case twoStepAuthErrorLimitExceeded + internal static var twoStepAuthErrorInvalidEmail: String { return L10n.tr("Localizable", "TwoStepAuth.Error.InvalidEmail") } + /// You have entered an invalid password too many times. Please try again later. + internal static var twoStepAuthErrorLimitExceeded: String { return L10n.tr("Localizable", "TwoStepAuth.Error.LimitExceeded") } /// Passwords don't match.\nPlease try again. - case twoStepAuthErrorPasswordsDontMatch + internal static var twoStepAuthErrorPasswordsDontMatch: String { return L10n.tr("Localizable", "TwoStepAuth.Error.PasswordsDontMatch") } + /// Reset + internal static var twoStepAuthErrorHaventEmailReset: String { return L10n.tr("Localizable", "TwoStepAuth.Error.HaventEmail.Reset") } + /// Reset Password + internal static var twoStepAuthErrorHaventEmailResetHeader: String { return L10n.tr("Localizable", "TwoStepAuth.Error.HaventEmail.ResetHeader") } + /// Reset Password + internal static var twoStepAuthResetSuccessHeader: String { return L10n.tr("Localizable", "TwoStepAuth.ResetSuccess.Header") } /// Capitalize - case uezBsLqGTitle - /// Telegram - case uQyDDJDrTitle - /// Cut - case uRlIYUnGTitle + internal static var uezBsLqGTitle: String { return L10n.tr("Localizable", "UEZ-Bs-lqG.title") } + /// Update Telegram + internal static var updateUpdateTelegram: String { return L10n.tr("Localizable", "Update.UpdateTelegram") } + /// Telegram Update + internal static var updateAppTelegramUpdate: String { return L10n.tr("Localizable", "UpdateApp.TelegramUpdate") } + /// Update Telegram + internal static var updateAppUpdateTelegram: String { return L10n.tr("Localizable", "UpdateApp.UpdateTelegram") } + /// Sorry, you are a member of too many groups and channels. For technical reasons, you need to leave some first before changing this setting in your groups. + internal static var upgradeChannelsTooMuch: String { return L10n.tr("Localizable", "Upgrade.ChannelsTooMuch") } /// %@ is available - case usernameSettingsAvailable(String) + internal static func usernameSettingsAvailable(_ p1: String) -> String { + return L10n.tr("Localizable", "UsernameSettings.available", p1) + } /// 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\n\nYou can use a-z, 0-9 and underscores. Minimum length is 5 characters. - case usernameSettingsChangeDescription + internal static var usernameSettingsChangeDescription: String { return L10n.tr("Localizable", "UsernameSettings.ChangeDescription") } /// Done - case usernameSettingsDone + internal static var usernameSettingsDone: String { return L10n.tr("Localizable", "UsernameSettings.Done") } /// Enter your username - case usernameSettingsInputPlaceholder + internal static var usernameSettingsInputPlaceholder: String { return L10n.tr("Localizable", "UsernameSettings.InputPlaceholder") } /// Hide Others - case vdrFpXzOTitle - /// Make Upper Case - case vmV6d7jITitle + internal static var vdrFpXzOTitle: String { return L10n.tr("Localizable", "Vdr-fp-XzO.title") } + /// Cancel + internal static var videoAvatarButtonCancel: String { return L10n.tr("Localizable", "VideoAvatar.Button.Cancel") } + /// Set + internal static var videoAvatarButtonSet: String { return L10n.tr("Localizable", "VideoAvatar.Button.Set") } + /// Choose a cover for channel video + internal static var videoAvatarChooseDescChannel: String { return L10n.tr("Localizable", "VideoAvatar.ChooseDesc.Channel") } + /// Choose a cover for group video + internal static var videoAvatarChooseDescGroup: String { return L10n.tr("Localizable", "VideoAvatar.ChooseDesc.Group") } + /// Choose a cover for your profile video + internal static var videoAvatarChooseDescProfile: String { return L10n.tr("Localizable", "VideoAvatar.ChooseDesc.Profile") } + /// Sorry, you can't join voice chat as an anonymous admin. + internal static var voiceChatAnonymousDisabledAlertText: String { return L10n.tr("Localizable", "VoiceChat.AnonymousDisabledAlertText") } + /// Sorry, this voice chat has too many participants at the moment. + internal static var voiceChatChatFullAlertText: String { return L10n.tr("Localizable", "VoiceChat.ChatFullAlertText") } + /// click if you want to speak + internal static var voiceChatClickToRaiseHand: String { return L10n.tr("Localizable", "VoiceChat.ClickToRaiseHand") } + /// Click to Unmute + internal static var voiceChatClickToUnmute: String { return L10n.tr("Localizable", "VoiceChat.ClickToUnmute") } + /// Connecting... + internal static var voiceChatConnecting: String { return L10n.tr("Localizable", "VoiceChat.Connecting") } + /// Cancel request to speak + internal static var voiceChatDownHand: String { return L10n.tr("Localizable", "VoiceChat.DownHand") } + /// group members + internal static var voiceChatGroupMembers: String { return L10n.tr("Localizable", "VoiceChat.GroupMembers") } + /// Add + internal static var voiceChatInviteMemberToGroupFirstAdd: String { return L10n.tr("Localizable", "VoiceChat.InviteMemberToGroupFirstAdd") } + /// %1$@ isn't a member of "%2$@" yet. Add them to the group? + internal static func voiceChatInviteMemberToGroupFirstText(_ p1: String, _ p2: String) -> String { + return L10n.tr("Localizable", "VoiceChat.InviteMemberToGroupFirstText", p1, p2) + } + /// Leave + internal static var voiceChatLeave: String { return L10n.tr("Localizable", "VoiceChat.Leave") } + /// Leave + internal static var voiceChatLeaveCall: String { return L10n.tr("Localizable", "VoiceChat.LeaveCall") } + /// You are in Listen Mode Only + internal static var voiceChatListenMode: String { return L10n.tr("Localizable", "VoiceChat.ListenMode") } + /// Muted By Admin + internal static var voiceChatMutedByAdmin: String { return L10n.tr("Localizable", "VoiceChat.MutedByAdmin") } + /// Mute For Me + internal static var voiceChatMuteForMe: String { return L10n.tr("Localizable", "VoiceChat.MuteForMe") } + /// Mute + internal static var voiceChatMutePeer: String { return L10n.tr("Localizable", "VoiceChat.MutePeer") } + /// Open Profile + internal static var voiceChatOpenProfile: String { return L10n.tr("Localizable", "VoiceChat.OpenProfile") } + /// Pin Screencast + internal static var voiceChatPinScreencast: String { return L10n.tr("Localizable", "VoiceChat.PinScreencast") } + /// Pin Video + internal static var voiceChatPinVideo: String { return L10n.tr("Localizable", "VoiceChat.PinVideo") } + /// Remove + internal static var voiceChatRemovePeer: String { return L10n.tr("Localizable", "VoiceChat.RemovePeer") } + /// Remove + internal static var voiceChatRemovePeerRemove: String { return L10n.tr("Localizable", "VoiceChat.RemovePeerRemove") } + /// Cancel Reminder + internal static var voiceChatRemoveReminder: String { return L10n.tr("Localizable", "VoiceChat.RemoveReminder") } + /// Telegram needs access to your microphone to speak + internal static var voiceChatRequestAccess: String { return L10n.tr("Localizable", "VoiceChat.RequestAccess") } + /// Set Reminder + internal static var voiceChatSetReminder: String { return L10n.tr("Localizable", "VoiceChat.SetReminder") } + /// Settings + internal static var voiceChatSettings: String { return L10n.tr("Localizable", "VoiceChat.Settings") } + /// Show Info + internal static var voiceChatShowInfo: String { return L10n.tr("Localizable", "VoiceChat.ShowInfo") } + /// Start Now + internal static var voiceChatStartNow: String { return L10n.tr("Localizable", "VoiceChat.StartNow") } + /// Start Recording + internal static var voiceChatStartRecording: String { return L10n.tr("Localizable", "VoiceChat.StartRecording") } + /// Stop Recording + internal static var voiceChatStopRecording: String { return L10n.tr("Localizable", "VoiceChat.StopRecording") } + /// Unmute For Me + internal static var voiceChatUnmuteForMe: String { return L10n.tr("Localizable", "VoiceChat.UnmuteForMe") } + /// Allow To Speak + internal static var voiceChatUnmutePeer: String { return L10n.tr("Localizable", "VoiceChat.UnmutePeer") } + /// Unpin Screencast + internal static var voiceChatUnpinScreencast: String { return L10n.tr("Localizable", "VoiceChat.UnpinScreencast") } + /// Unpin Video + internal static var voiceChatUnpinVideo: String { return L10n.tr("Localizable", "VoiceChat.UnpinVideo") } + /// You invited **%@** to the voice chat + internal static func voiceChatUserInvited(_ p1: String) -> String { + return L10n.tr("Localizable", "VoiceChat.UserInvited", p1) + } + /// You're Live + internal static var voiceChatYouLive: String { return L10n.tr("Localizable", "VoiceChat.YouLive") } + /// Voice chat is being recorded. + internal static var voiceChatAlertRecording: String { return L10n.tr("Localizable", "VoiceChat.Alert.Recording") } + /// listening + internal static var voiceChatBlockListening: String { return L10n.tr("Localizable", "VoiceChat.Block.Listening") } + /// recent active + internal static var voiceChatBlockRecentActive: String { return L10n.tr("Localizable", "VoiceChat.Block.RecentActive") } + /// Voice chat ended. + internal static var voiceChatChatEnded: String { return L10n.tr("Localizable", "VoiceChat.Chat.Ended") } + /// Voice chat ended. Start a new one? + internal static var voiceChatChatStartNew: String { return L10n.tr("Localizable", "VoiceChat.Chat.StartNew") } + /// Start + internal static var voiceChatChatStartNewOK: String { return L10n.tr("Localizable", "VoiceChat.Chat.StartNew.OK") } + /// hold ⎵ or %@ + internal static func voiceChatClickToUnmuteSecondaryHold(_ p1: String) -> String { + return L10n.tr("Localizable", "VoiceChat.ClickToUnmute.Secondary.Hold", p1) + } + /// hold ⎵ + internal static var voiceChatClickToUnmuteSecondaryHoldDefault: String { return L10n.tr("Localizable", "VoiceChat.ClickToUnmute.Secondary.HoldDefault") } + /// press ⎵ or %@ + internal static func voiceChatClickToUnmuteSecondaryPress(_ p1: String) -> String { + return L10n.tr("Localizable", "VoiceChat.ClickToUnmute.Secondary.Press", p1) + } + /// press ⎵ + internal static var voiceChatClickToUnmuteSecondaryPressDefault: String { return L10n.tr("Localizable", "VoiceChat.ClickToUnmute.Secondary.PressDefault") } + /// Leave + internal static var voiceChatEndOK: String { return L10n.tr("Localizable", "VoiceChat.End.OK") } + /// Are you sure you want to leave this voice chat? + internal static var voiceChatEndText: String { return L10n.tr("Localizable", "VoiceChat.End.Text") } + /// End Voice Chat + internal static var voiceChatEndThird: String { return L10n.tr("Localizable", "VoiceChat.End.Third") } + /// Leave voice chat + internal static var voiceChatEndTitle: String { return L10n.tr("Localizable", "VoiceChat.End.Title") } + /// Join Channel + internal static var voiceChatInfoJoinChannel: String { return L10n.tr("Localizable", "VoiceChat.Info.JoinChannel") } + /// Leave Channel + internal static var voiceChatInfoLeaveChannel: String { return L10n.tr("Localizable", "VoiceChat.Info.LeaveChannel") } + /// Open Channel + internal static var voiceChatInfoOpenChannel: String { return L10n.tr("Localizable", "VoiceChat.Info.OpenChannel") } + /// Open Profile + internal static var voiceChatInfoOpenProfile: String { return L10n.tr("Localizable", "VoiceChat.Info.OpenProfile") } + /// Send Message + internal static var voiceChatInfoSendMessage: String { return L10n.tr("Localizable", "VoiceChat.Info.SendMessage") } + /// CHATS + internal static var voiceChatInviteChats: String { return L10n.tr("Localizable", "VoiceChat.Invite.Chats") } + /// contacts + internal static var voiceChatInviteContacts: String { return L10n.tr("Localizable", "VoiceChat.Invite.Contacts") } + /// Copy Invite Link + internal static var voiceChatInviteCopyInviteLink: String { return L10n.tr("Localizable", "VoiceChat.Invite.CopyInviteLink") } + /// Copy Listener Link + internal static var voiceChatInviteCopyListenersLink: String { return L10n.tr("Localizable", "VoiceChat.Invite.CopyListenersLink") } + /// Copy Speaker Link + internal static var voiceChatInviteCopySpeakersLink: String { return L10n.tr("Localizable", "VoiceChat.Invite.CopySpeakersLink") } + /// global search + internal static var voiceChatInviteGlobalSearch: String { return L10n.tr("Localizable", "VoiceChat.Invite.GlobalSearch") } + /// group members + internal static var voiceChatInviteGroupMembers: String { return L10n.tr("Localizable", "VoiceChat.Invite.GroupMembers") } + /// Send + internal static var voiceChatInviteInvite: String { return L10n.tr("Localizable", "VoiceChat.Invite.Invite") } + /// Invite members + internal static var voiceChatInviteInviteMembers: String { return L10n.tr("Localizable", "VoiceChat.Invite.InviteMembers") } + /// Add Members + internal static var voiceChatInviteTitle: String { return L10n.tr("Localizable", "VoiceChat.Invite.Title") } + /// Invite Members + internal static var voiceChatInviteChannelsTitle: String { return L10n.tr("Localizable", "VoiceChat.Invite.Channels.Title") } + /// Voice Chat + internal static var voiceChatInviteConfirmHeader: String { return L10n.tr("Localizable", "VoiceChat.Invite.Confirm.Header") } + /// Send + internal static var voiceChatInviteConfirmOK: String { return L10n.tr("Localizable", "VoiceChat.Invite.Confirm.OK") } + /// Send Invite Link to selected chats? + internal static var voiceChatInviteConfirmText: String { return L10n.tr("Localizable", "VoiceChat.Invite.Confirm.Text") } + /// Send Speaker Link + internal static var voiceChatInviteConfirmThird: String { return L10n.tr("Localizable", "VoiceChat.Invite.Confirm.Third") } + /// Sorry, there are too many members in this voice chat. Please try again later. + internal static var voiceChatJoinErrorTooMany: String { return L10n.tr("Localizable", "VoiceChat.Join.Error.TooMany") } + /// %d + internal static func voiceChatJoinAsChannelCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "VoiceChat.JoinAs.Channel_countable", p1) + } + /// %d subscribers + internal static func voiceChatJoinAsChannelFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "VoiceChat.JoinAs.Channel_few", p1) + } + /// %d subscribers + internal static func voiceChatJoinAsChannelMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "VoiceChat.JoinAs.Channel_many", p1) + } + /// %d subscriber + internal static func voiceChatJoinAsChannelOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "VoiceChat.JoinAs.Channel_one", p1) + } + /// %d subscribers + internal static func voiceChatJoinAsChannelOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "VoiceChat.JoinAs.Channel_other", p1) + } + /// %d subscribers + internal static func voiceChatJoinAsChannelTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "VoiceChat.JoinAs.Channel_two", p1) + } + /// %d subscribers + internal static func voiceChatJoinAsChannelZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "VoiceChat.JoinAs.Channel_zero", p1) + } + /// %d + internal static func voiceChatJoinAsGroupCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "VoiceChat.JoinAs.Group_countable", p1) + } + /// %d members + internal static func voiceChatJoinAsGroupFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "VoiceChat.JoinAs.Group_few", p1) + } + /// %d members + internal static func voiceChatJoinAsGroupMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "VoiceChat.JoinAs.Group_many", p1) + } + /// %d member + internal static func voiceChatJoinAsGroupOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "VoiceChat.JoinAs.Group_one", p1) + } + /// %d members + internal static func voiceChatJoinAsGroupOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "VoiceChat.JoinAs.Group_other", p1) + } + /// %d members + internal static func voiceChatJoinAsGroupTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "VoiceChat.JoinAs.Group_two", p1) + } + /// %d members + internal static func voiceChatJoinAsGroupZero(_ p1: Int) -> String { + return L10n.tr("Localizable", "VoiceChat.JoinAs.Group_zero", p1) + } + /// we let the speakers know + internal static var voiceChatRaisedHandText: String { return L10n.tr("Localizable", "VoiceChat.RaisedHand.Text") } + /// You asked to speak + internal static var voiceChatRaisedHandTitle: String { return L10n.tr("Localizable", "VoiceChat.RaisedHand.Title") } + /// Start + internal static var voiceChatRecordingStartOK: String { return L10n.tr("Localizable", "VoiceChat.Recording.Start.OK") } + /// Do you want to start recording this chat and save the result into an file?\n\nOther members will see that the chat is being recorded. + internal static var voiceChatRecordingStartText1: String { return L10n.tr("Localizable", "VoiceChat.Recording.Start.Text1") } + /// Start Recording + internal static var voiceChatRecordingStartTitle: String { return L10n.tr("Localizable", "VoiceChat.Recording.Start.Title") } + /// Stop + internal static var voiceChatRecordingStopOK: String { return L10n.tr("Localizable", "VoiceChat.Recording.Stop.OK") } + /// Are you sure to want to stop recording? + internal static var voiceChatRecordingStopText: String { return L10n.tr("Localizable", "VoiceChat.Recording.Stop.Text") } + /// Stop Recording + internal static var voiceChatRecordingStopTitle: String { return L10n.tr("Localizable", "VoiceChat.Recording.Stop.Title") } + /// Are you sure you want to remove %1$@ from the group chat? + internal static func voiceChatRemovePeerConfirm(_ p1: String) -> String { + return L10n.tr("Localizable", "VoiceChat.RemovePeer.Confirm", p1) + } + /// Cancel + internal static var voiceChatRemovePeerConfirmCancel: String { return L10n.tr("Localizable", "VoiceChat.RemovePeer.Confirm.Cancel") } + /// Are you sure you want to remove %1$@ from the channel? + internal static func voiceChatRemovePeerConfirmChannel(_ p1: String) -> String { + return L10n.tr("Localizable", "VoiceChat.RemovePeer.Confirm.Channel", p1) + } + /// Remove + internal static var voiceChatRemovePeerConfirmOK: String { return L10n.tr("Localizable", "VoiceChat.RemovePeer.Confirm.OK") } + /// Starts In + internal static var voiceChatScheduledHeader: String { return L10n.tr("Localizable", "VoiceChat.Scheduled.Header") } + /// Late For + internal static var voiceChatScheduledHeaderLate: String { return L10n.tr("Localizable", "VoiceChat.Scheduled.HeaderLate") } + /// Unavailable to share your screen, please grant access is [System Settings](screen). + internal static var voiceChatScreenShareUnavailable: String { return L10n.tr("Localizable", "VoiceChat.ScreenShare.Unavailable") } + /// Screencast is Paused + internal static var voiceChatScreencastPaused: String { return L10n.tr("Localizable", "VoiceChat.Screencast.Paused") } + /// Voice Chat + internal static var voiceChatScreencastConfirmHeader: String { return L10n.tr("Localizable", "VoiceChat.Screencast.Confirm.Header") } + /// Continue + internal static var voiceChatScreencastConfirmOK: String { return L10n.tr("Localizable", "VoiceChat.Screencast.Confirm.OK") } + /// %@ is screensharing. This action will make your screencast pinned for all participants. + internal static func voiceChatScreencastConfirmText(_ p1: String) -> String { + return L10n.tr("Localizable", "VoiceChat.Screencast.Confirm.Text", p1) + } + /// New participants can speak + internal static var voiceChatSettingsAllMembers: String { return L10n.tr("Localizable", "VoiceChat.Settings.AllMembers") } + /// End Voice Chat + internal static var voiceChatSettingsEnd: String { return L10n.tr("Localizable", "VoiceChat.Settings.End") } + /// MODE + internal static var voiceChatSettingsInputMode: String { return L10n.tr("Localizable", "VoiceChat.Settings.InputMode") } + /// Noise Suppression + internal static var voiceChatSettingsNoiseSuppression: String { return L10n.tr("Localizable", "VoiceChat.Settings.NoiseSuppression") } + /// New participants are muted + internal static var voiceChatSettingsOnlyAdmins: String { return L10n.tr("Localizable", "VoiceChat.Settings.OnlyAdmins") } + /// OUTPUT + internal static var voiceChatSettingsOutput: String { return L10n.tr("Localizable", "VoiceChat.Settings.Output") } + /// SHORTCUT + internal static var voiceChatSettingsPushToTalk: String { return L10n.tr("Localizable", "VoiceChat.Settings.PushToTalk") } + /// Reduce Motion + internal static var voiceChatSettingsReduceMotion: String { return L10n.tr("Localizable", "VoiceChat.Settings.ReduceMotion") } + /// Revoke Speakers Link + internal static var voiceChatSettingsResetLink: String { return L10n.tr("Localizable", "VoiceChat.Settings.ResetLink") } + /// VOICE CHAT TITLE + internal static var voiceChatSettingsTitle: String { return L10n.tr("Localizable", "VoiceChat.Settings.Title") } + /// personal account + internal static var voiceChatSettingsDisplayAsPersonalAccount: String { return L10n.tr("Localizable", "VoiceChat.Settings.DisplayAs.PersonalAccount") } + /// DISPLAY ME AS + internal static var voiceChatSettingsDisplayAsTitle: String { return L10n.tr("Localizable", "VoiceChat.Settings.DisplayAs.Title") } + /// Are you sure you want to end this voice chat? + internal static var voiceChatSettingsEndConfirm: String { return L10n.tr("Localizable", "VoiceChat.Settings.End.Confirm") } + /// End + internal static var voiceChatSettingsEndConfirmOK: String { return L10n.tr("Localizable", "VoiceChat.Settings.End.Confirm.OK") } + /// End voice chat + internal static var voiceChatSettingsEndConfirmTitle: String { return L10n.tr("Localizable", "VoiceChat.Settings.End.Confirm.Title") } + /// Press and Release + internal static var voiceChatSettingsInputModeAlways: String { return L10n.tr("Localizable", "VoiceChat.Settings.InputMode.Always") } + /// Press and Hold + internal static var voiceChatSettingsInputModePushToTalk: String { return L10n.tr("Localizable", "VoiceChat.Settings.InputMode.PushToTalk") } + /// Sound Effects + internal static var voiceChatSettingsInputModeSoundEffects: String { return L10n.tr("Localizable", "VoiceChat.Settings.InputMode.SoundEffects") } + /// Output Device + internal static var voiceChatSettingsOutputDevice: String { return L10n.tr("Localizable", "VoiceChat.Settings.Output.Device") } + /// Disabling noise suppression can increase performance. + internal static var voiceChatSettingsPerformanceDesc: String { return L10n.tr("Localizable", "VoiceChat.Settings.Performance.Desc") } + /// PERFORMANCE + internal static var voiceChatSettingsPerformanceHeader: String { return L10n.tr("Localizable", "VoiceChat.Settings.Performance.Header") } + /// PERMISSIONS + internal static var voiceChatSettingsPermissionsTitle: String { return L10n.tr("Localizable", "VoiceChat.Settings.Permissions.Title") } + /// If you want this shortcut to work even when Telegram is not in focus\nPlease grant Telegram access to [Input Monitor](input) + internal static var voiceChatSettingsPushToTalkAccess: String { return L10n.tr("Localizable", "VoiceChat.Settings.PushToTalk.Access") } + /// When the Voice Chat window is in focus, you can also use ⎵ regardless of this setting. + internal static var voiceChatSettingsPushToTalkDesc: String { return L10n.tr("Localizable", "VoiceChat.Settings.PushToTalk.Desc") } + /// Change Key + internal static var voiceChatSettingsPushToTalkEditKeybind: String { return L10n.tr("Localizable", "VoiceChat.Settings.PushToTalk.EditKeybind") } + /// Enabled + internal static var voiceChatSettingsPushToTalkEnabled: String { return L10n.tr("Localizable", "VoiceChat.Settings.PushToTalk.Enabled") } + /// Cancel + internal static var voiceChatSettingsPushToTalkStopRecording: String { return L10n.tr("Localizable", "VoiceChat.Settings.PushToTalk.StopRecording") } + /// PUSH TO TALK + internal static var voiceChatSettingsPushToTalkTitle: String { return L10n.tr("Localizable", "VoiceChat.Settings.PushToTalk.Title") } + /// Undefined + internal static var voiceChatSettingsPushToTalkUndefined: String { return L10n.tr("Localizable", "VoiceChat.Settings.PushToTalk.Undefined") } + /// Please allow Accessibility for Telegram in [Privacy Settings.](access)\n\nApp restart may be required. + internal static var voiceChatSettingsPushToTalkAccessOld: String { return L10n.tr("Localizable", "VoiceChat.Settings.PushToTalk.Access.Old") } + /// Include Video + internal static var voiceChatSettingsRecordIncludeVideo: String { return L10n.tr("Localizable", "VoiceChat.Settings.Record.IncludeVideo") } + /// Landscape + internal static var voiceChatSettingsRecordOrientationLandscape: String { return L10n.tr("Localizable", "VoiceChat.Settings.Record.Orientation.Landscape") } + /// Portrait + internal static var voiceChatSettingsRecordOrientationPortrait: String { return L10n.tr("Localizable", "VoiceChat.Settings.Record.Orientation.Portrait") } + /// Speaker Link has been revoked. + internal static var voiceChatSettingsResetLinkSuccess: String { return L10n.tr("Localizable", "VoiceChat.Settings.ResetLink.Success") } + /// Title... + internal static var voiceChatSettingsTitlePlaceholder: String { return L10n.tr("Localizable", "VoiceChat.Settings.Title.Placeholder") } + /// You can't share screencast right now. Please ask to speak. + internal static var voiceChatShareScreenMutedError: String { return L10n.tr("Localizable", "VoiceChat.ShareScreen.MutedError") } + /// You can't share video right now. Please ask to speak. + internal static var voiceChatShareVideoMutedError: String { return L10n.tr("Localizable", "VoiceChat.ShareVideo.MutedError") } + /// You are sharing your screen + internal static var voiceChatSharingPlaceholder: String { return L10n.tr("Localizable", "VoiceChat.Sharing.Placeholder") } + /// Stop + internal static var voiceChatSharingStop: String { return L10n.tr("Localizable", "VoiceChat.Sharing.Stop") } + /// Connecting... + internal static var voiceChatStatusConnecting: String { return L10n.tr("Localizable", "VoiceChat.Status.Connecting") } + /// invited + internal static var voiceChatStatusInvited: String { return L10n.tr("Localizable", "VoiceChat.Status.Invited") } + /// listening + internal static var voiceChatStatusListening: String { return L10n.tr("Localizable", "VoiceChat.Status.Listening") } + /// connecting... + internal static var voiceChatStatusLoading: String { return L10n.tr("Localizable", "VoiceChat.Status.Loading") } + /// %d + internal static func voiceChatStatusMembersCountable(_ p1: Int) -> String { + return L10n.tr("Localizable", "VoiceChat.Status.Members_countable", p1) + } + /// %d participants + internal static func voiceChatStatusMembersFew(_ p1: Int) -> String { + return L10n.tr("Localizable", "VoiceChat.Status.Members_few", p1) + } + /// %d participants + internal static func voiceChatStatusMembersMany(_ p1: Int) -> String { + return L10n.tr("Localizable", "VoiceChat.Status.Members_many", p1) + } + /// %d participant + internal static func voiceChatStatusMembersOne(_ p1: Int) -> String { + return L10n.tr("Localizable", "VoiceChat.Status.Members_one", p1) + } + /// %d participants + internal static func voiceChatStatusMembersOther(_ p1: Int) -> String { + return L10n.tr("Localizable", "VoiceChat.Status.Members_other", p1) + } + /// %d participants + internal static func voiceChatStatusMembersTwo(_ p1: Int) -> String { + return L10n.tr("Localizable", "VoiceChat.Status.Members_two", p1) + } + /// no participants + internal static var voiceChatStatusMembersZero: String { return L10n.tr("Localizable", "VoiceChat.Status.Members_zero") } + /// muted + internal static var voiceChatStatusMuted: String { return L10n.tr("Localizable", "VoiceChat.Status.Muted") } + /// muted for you + internal static var voiceChatStatusMutedForYou: String { return L10n.tr("Localizable", "VoiceChat.Status.MutedForYou") } + /// sharing screen + internal static var voiceChatStatusScreensharing: String { return L10n.tr("Localizable", "VoiceChat.Status.Screensharing") } + /// speaking + internal static var voiceChatStatusSpeaking: String { return L10n.tr("Localizable", "VoiceChat.Status.Speaking") } + /// wants to speak + internal static var voiceChatStatusWantsSpeak: String { return L10n.tr("Localizable", "VoiceChat.Status.WantsSpeak") } + /// This is you + internal static var voiceChatStatusYou: String { return L10n.tr("Localizable", "VoiceChat.Status.You") } + /// Leave + internal static var voiceChatTitleEnd: String { return L10n.tr("Localizable", "VoiceChat.Title.End") } + /// invited + internal static var voiceChatTitleInvited: String { return L10n.tr("Localizable", "VoiceChat.Title.Invited") } + /// Invite Members + internal static var voiceChatTitleInviteMembers: String { return L10n.tr("Localizable", "VoiceChat.Title.InviteMembers") } + /// Voice Chat + internal static var voiceChatTitleScheduled: String { return L10n.tr("Localizable", "VoiceChat.Title.Scheduled") } + /// scheduled + internal static var voiceChatTitleScheduledSoon: String { return L10n.tr("Localizable", "VoiceChat.Title.Scheduled.Soon") } + /// Audio saved to Saved Messsages. + internal static var voiceChatToastStop: String { return L10n.tr("Localizable", "VoiceChat.Toast.Stop") } + /// Now you can speak in the voice chat + internal static var voiceChatToastYouCanSpeak: String { return L10n.tr("Localizable", "VoiceChat.Toast.YouCanSpeak") } + /// Your camera is off. Click here to enable camera. + internal static var voiceChatTooltipEnableCamera: String { return L10n.tr("Localizable", "VoiceChat.Tooltip.EnableCamera") } + /// You are on mute. Click here to speak. + internal static var voiceChatTooltipEnableMicro: String { return L10n.tr("Localizable", "VoiceChat.Tooltip.EnableMicro") } + /// **%@** is speaking + internal static func voiceChatTooltipIsSpeaking(_ p1: String) -> String { + return L10n.tr("Localizable", "VoiceChat.Tooltip.IsSpeaking", p1) + } + /// No active and connected camera was found. + internal static var voiceChatTooltipNoCameraFound: String { return L10n.tr("Localizable", "VoiceChat.Tooltip.NoCameraFound") } + /// Window is pinned. + internal static var voiceChatTooltipPinWindow: String { return L10n.tr("Localizable", "VoiceChat.Tooltip.PinWindow") } + /// An error occured. Screencast has stopped. + internal static var voiceChatTooltipScreencastFailed: String { return L10n.tr("Localizable", "VoiceChat.Tooltip.ScreencastFailed") } + /// %@'s screencast is pinned + internal static func voiceChatTooltipScreenPinned(_ p1: String) -> String { + return L10n.tr("Localizable", "VoiceChat.Tooltip.ScreenPinned", p1) + } + /// %@'s screencast is unpinned + internal static func voiceChatTooltipScreenUnpinned(_ p1: String) -> String { + return L10n.tr("Localizable", "VoiceChat.Tooltip.ScreenUnpinned", p1) + } + /// Your screen is being broadcast. + internal static var voiceChatTooltipShareScreen: String { return L10n.tr("Localizable", "VoiceChat.Tooltip.ShareScreen") } + /// Your video is being broadcast. + internal static var voiceChatTooltipShareVideo: String { return L10n.tr("Localizable", "VoiceChat.Tooltip.ShareVideo") } + /// You have stopped broadcasting screen. + internal static var voiceChatTooltipStopScreen: String { return L10n.tr("Localizable", "VoiceChat.Tooltip.StopScreen") } + /// You have stopped broadcasting video. + internal static var voiceChatTooltipStopVideo: String { return L10n.tr("Localizable", "VoiceChat.Tooltip.StopVideo") } + /// We will notify you when it starts + internal static var voiceChatTooltipSubscribe: String { return L10n.tr("Localizable", "VoiceChat.Tooltip.Subscribe") } + /// Window is unpinned. + internal static var voiceChatTooltipUnpinWindow: String { return L10n.tr("Localizable", "VoiceChat.Tooltip.UnpinWindow") } + /// An error occured. Video stream has stopped. + internal static var voiceChatTooltipVideoFailed: String { return L10n.tr("Localizable", "VoiceChat.Tooltip.VideoFailed") } + /// %@'s video is pinned + internal static func voiceChatTooltipVideoPinned(_ p1: String) -> String { + return L10n.tr("Localizable", "VoiceChat.Tooltip.VideoPinned", p1) + } + /// %@'s video is unpinned + internal static func voiceChatTooltipVideoUnpinned(_ p1: String) -> String { + return L10n.tr("Localizable", "VoiceChat.Tooltip.VideoUnpinned", p1) + } + /// Your screencast is pinned + internal static var voiceChatTooltipYourScreenPinned: String { return L10n.tr("Localizable", "VoiceChat.Tooltip.YourScreenPinned") } + /// Your screencast is unpinned + internal static var voiceChatTooltipYourScreenUnpinned: String { return L10n.tr("Localizable", "VoiceChat.Tooltip.YourScreenUnpinned") } + /// Your video is pinned + internal static var voiceChatTooltipYourVideoPinned: String { return L10n.tr("Localizable", "VoiceChat.Tooltip.YourVideoPinned") } + /// Your video is unpinned + internal static var voiceChatTooltipYourVideoUnpinned: String { return L10n.tr("Localizable", "VoiceChat.Tooltip.YourVideoUnpinned") } + /// Screencast is only available for the first %d members. + internal static func voiceChatTooltipErrorScreenUnavailable(_ p1: Int) -> String { + return L10n.tr("Localizable", "VoiceChat.Tooltip.Error.ScreenUnavailable", p1) + } + /// Video is only available for the first %d members + internal static func voiceChatTooltipErrorVideoUnavailable(_ p1: Int) -> String { + return L10n.tr("Localizable", "VoiceChat.Tooltip.Error.VideoUnavailable", p1) + } + /// Video is Paused + internal static var voiceChatVideoPaused: String { return L10n.tr("Localizable", "VoiceChat.Video.Paused") } + /// Pin + internal static var voiceChatVideoShortPin: String { return L10n.tr("Localizable", "VoiceChat.Video.ShortPin") } + /// Unpin + internal static var voiceChatVideoShortUnpin: String { return L10n.tr("Localizable", "VoiceChat.Video.ShortUnpin") } + /// Video Source + internal static var voiceChatVideoVideoSource: String { return L10n.tr("Localizable", "VoiceChat.Video.VideoSource") } + /// more + internal static var voiceChatVideoStreamMore: String { return L10n.tr("Localizable", "VoiceChat.Video.Stream.More") } + /// screen + internal static var voiceChatVideoStreamScreencast: String { return L10n.tr("Localizable", "VoiceChat.Video.Stream.Screencast") } + /// video + internal static var voiceChatVideoStreamVideo: String { return L10n.tr("Localizable", "VoiceChat.Video.Stream.Video") } + /// Cancel + internal static var voiceChatVideoVideoSourceCancel: String { return L10n.tr("Localizable", "VoiceChat.Video.VideoSource.Cancel") } + /// Share + internal static var voiceChatVideoVideoSourceShare: String { return L10n.tr("Localizable", "VoiceChat.Video.VideoSource.Share") } + /// Unavailable to share your camera, please grant access is [System Settings](camera). + internal static var voiceChatVideoShareUnavailable: String { return L10n.tr("Localizable", "VoiceChat.VideoShare.Unavailable") } + /// Title (Optional) + internal static var voiecChatSettingsRecordPlaceholder1: String { return L10n.tr("Localizable", "VoiecChat.Settings.Record.Placeholder1") } + /// RECORD VOICE CHAT + internal static var voiecChatSettingsRecordTitle: String { return L10n.tr("Localizable", "VoiecChat.Settings.Record.Title") } + /// RECORD LIVE STREAM + internal static var voiecChatSettingsRecordLiveTitle: String { return L10n.tr("Localizable", "VoiecChat.Settings.Record.Live.Title") } + /// RECORD VIDEO CHAT + internal static var voiecChatSettingsRecordVideoTitle: String { return L10n.tr("Localizable", "VoiecChat.Settings.Record.Video.Title") } /// Edit - case w486f4DlTitle + internal static var w486f4DlTitle: String { return L10n.tr("Localizable", "W48-6f-4Dl.title") } + /// Apply + internal static var wallpaperPreviewApply: String { return L10n.tr("Localizable", "WallpaperPreview.Apply") } + /// Blurred + internal static var wallpaperPreviewBlurred: String { return L10n.tr("Localizable", "WallpaperPreview.Blurred") } + /// Sorry, this background doesn't seem to exist. + internal static var wallpaperPreviewDoesntExists: String { return L10n.tr("Localizable", "WallpaperPreview.DoesntExists") } + /// Background Preview + internal static var wallpaperPreviewHeader: String { return L10n.tr("Localizable", "WallpaperPreview.Header") } + /// Paste and Match Style + internal static var weT3VZwkTitle: String { return L10n.tr("Localizable", "WeT-3V-zwk.title") } + /// Disconnect + internal static var webAuthorizationsLogout: String { return L10n.tr("Localizable", "WebAuthorizations.Logout") } + /// Disconnect All Websites + internal static var webAuthorizationsLogoutAll: String { return L10n.tr("Localizable", "WebAuthorizations.LogoutAll") } + /// Do you want to disconnect this website? + internal static var webAuthorizationsConfirmRevoke: String { return L10n.tr("Localizable", "WebAuthorizations.Confirm.Revoke") } + /// Are you sure you want to disconnect all websites? + internal static var webAuthorizationsConfirmRevokeAll: String { return L10n.tr("Localizable", "WebAuthorizations.Confirm.RevokeAll") } + /// CONNECTED WEBSITES + internal static var webAuthorizationsLoggedInDescrpiption: String { return L10n.tr("Localizable", "WebAuthorizations.LoggedIn.Descrpiption") } + /// You can log in on websites that support signing in with Telegram. + internal static var webAuthorizationsLogoutAllDescription: String { return L10n.tr("Localizable", "WebAuthorizations.LogoutAll.Description") } /// Fri - case weekdayShortFriday + internal static var weekdayShortFriday: String { return L10n.tr("Localizable", "Weekday.ShortFriday") } /// Mon - case weekdayShortMonday + internal static var weekdayShortMonday: String { return L10n.tr("Localizable", "Weekday.ShortMonday") } /// Sat - case weekdayShortSaturday + internal static var weekdayShortSaturday: String { return L10n.tr("Localizable", "Weekday.ShortSaturday") } /// Sun - case weekdayShortSunday + internal static var weekdayShortSunday: String { return L10n.tr("Localizable", "Weekday.ShortSunday") } /// Thu - case weekdayShortThursday + internal static var weekdayShortThursday: String { return L10n.tr("Localizable", "Weekday.ShortThursday") } /// Tue - case weekdayShortTuesday + internal static var weekdayShortTuesday: String { return L10n.tr("Localizable", "Weekday.ShortTuesday") } /// Wed - case weekdayShortWednesday - /// Paste and Match Style - case weT3VZwkTitle - /// Copy - case x3vGGIWUTitle - /// Show Substitutions - case z6FFW3nzTitle + internal static var weekdayShortWednesday: String { return L10n.tr("Localizable", "Weekday.ShortWednesday") } + /// Use ⌘+K or ESC to enter [search](search) mode. + internal static var widgetRecentDesc: String { return L10n.tr("Localizable", "Widget.Recent.Desc") } + /// Both + internal static var widgetRecentMixed: String { return L10n.tr("Localizable", "Widget.Recent.Mixed") } + /// Popular + internal static var widgetRecentPopular: String { return L10n.tr("Localizable", "Widget.Recent.Popular") } + /// Recent + internal static var widgetRecentRecent: String { return L10n.tr("Localizable", "Widget.Recent.Recent") } + /// Chats + internal static var widgetRecentTitle: String { return L10n.tr("Localizable", "Widget.Recent.Title") } + /// Edit + internal static var ns103Title: String { return L10n.tr("Localizable", "_NS103.title") } /// Window - case _NS138Title + internal static var ns167Title: String { return L10n.tr("Localizable", "_NS167.title") } + /// View + internal static var ns70Title: String { return L10n.tr("Localizable", "_NS70.title") } /// View - case _NS70Title + internal static var ns81Title: String { return L10n.tr("Localizable", "_NS81.title") } /// Edit - case _NS88Title -} -// swiftlint:enable type_body_length - -extension L10n: CustomStringConvertible { - var description: String { return self.string } - - var string: String { - switch self { - case .defaultSoundName: - return L10n.tr(key: "DefaultSoundName") - case .notificationSettingsToneNone: - return L10n.tr(key: "NotificationSettingsToneNone") - case .passwordHashInvalid: - return L10n.tr(key: "PASSWORD_HASH_INVALID") - case .phoneCodeExpired: - return L10n.tr(key: "PHONE_CODE_EXPIRED") - case .phoneCodeInvalid: - return L10n.tr(key: "PHONE_CODE_INVALID") - case .phoneNumberInvalid: - return L10n.tr(key: "PHONE_NUMBER_INVALID") - case .you: - return L10n.tr(key: "You") - case ._1000Title: - return L10n.tr(key: "1000.title") - case ._1XtHYUBwTitle: - return L10n.tr(key: "1Xt-HY-uBw.title") - case ._2oIRnZJCTitle: - return L10n.tr(key: "2oI-Rn-ZJC.title") - case ._4J7DPTxaTitle: - return L10n.tr(key: "4J7-dP-txa.title") - case ._4sb4sVLiTitle: - return L10n.tr(key: "4sb-4s-VLi.title") - case ._5kVVbQxSTitle: - return L10n.tr(key: "5kV-Vb-QxS.title") - case ._5QFOaP0TTitle: - return L10n.tr(key: "5QF-Oa-p0T.title") - case ._6dhZSVamTitle: - return L10n.tr(key: "6dh-zS-Vam.title") - case ._78YHA62vTitle: - return L10n.tr(key: "78Y-hA-62v.title") - case ._9icFLObxTitle: - return L10n.tr(key: "9ic-FL-obx.title") - case ._9yt4BNSMTitle: - return L10n.tr(key: "9yt-4B-nSM.title") - case .aboutDescription: - return L10n.tr(key: "About.Description") - case .accountConfirmAskQuestion: - return L10n.tr(key: "Account.Confirm.AskQuestion") - case .accountConfirmGoToFaq: - return L10n.tr(key: "Account.Confirm.GoToFaq") - case .accountConfirmLogout: - return L10n.tr(key: "Account.Confirm.Logout") - case .accountConfirmLogoutText: - return L10n.tr(key: "Account.Confirm.LogoutText") - case .accountsControllerNewAccount: - return L10n.tr(key: "AccountsController.NewAccount") - case .accountSettingsAbout: - return L10n.tr(key: "AccountSettings.About") - case .accountSettingsAppearance: - return L10n.tr(key: "AccountSettings.Appearance") - case .accountSettingsAskQuestion: - return L10n.tr(key: "AccountSettings.AskQuestion") - case .accountSettingsBio: - return L10n.tr(key: "AccountSettings.Bio") - case .accountSettingsCurrentLanguage: - return L10n.tr(key: "AccountSettings.CurrentLanguage") - case .accountSettingsFAQ: - return L10n.tr(key: "AccountSettings.FAQ") - case .accountSettingsGeneral: - return L10n.tr(key: "AccountSettings.General") - case .accountSettingsLanguage: - return L10n.tr(key: "AccountSettings.Language") - case .accountSettingsLogout: - return L10n.tr(key: "AccountSettings.Logout") - case .accountSettingsNotifications: - return L10n.tr(key: "AccountSettings.Notifications") - case .accountSettingsPrivacyAndSecurity: - return L10n.tr(key: "AccountSettings.PrivacyAndSecurity") - case .accountSettingsSetBio: - return L10n.tr(key: "AccountSettings.SetBio") - case .accountSettingsSetProfilePhoto: - return L10n.tr(key: "AccountSettings.SetProfilePhoto") - case .accountSettingsSetUsername: - return L10n.tr(key: "AccountSettings.SetUsername") - case .accountSettingsStickers: - return L10n.tr(key: "AccountSettings.Stickers") - case .accountSettingsStorage: - return L10n.tr(key: "AccountSettings.Storage") - case .accountSettingsUsername: - return L10n.tr(key: "AccountSettings.Username") - case .adminsAddAdmin: - return L10n.tr(key: "Admins.AddAdmin") - case .adminsAdmin: - return L10n.tr(key: "Admins.Admin") - case .adminsChannelAdmins: - return L10n.tr(key: "Admins.ChannelAdmins") - case .adminsChannelDescription: - return L10n.tr(key: "Admins.ChannelDescription") - case .adminsCreator: - return L10n.tr(key: "Admins.Creator") - case .adminsEverbodyCanAddMembers: - return L10n.tr(key: "Admins.EverbodyCanAddMembers") - case .adminsGroupAdmins: - return L10n.tr(key: "Admins.GroupAdmins") - case .adminsGroupDescription: - return L10n.tr(key: "Admins.GroupDescription") - case .adminsOnlyAdminsCanAddMembers: - return L10n.tr(key: "Admins.OnlyAdminsCanAddMembers") - case .adminsSelectNewAdminTitle: - return L10n.tr(key: "Admins.SelectNewAdminTitle") - case .adminsWhoCanInviteAdmins: - return L10n.tr(key: "Admins.WhoCanInvite.Admins") - case .adminsWhoCanInviteEveryone: - return L10n.tr(key: "Admins.WhoCanInvite.Everyone") - case .adminsWhoCanInviteText: - return L10n.tr(key: "Admins.WhoCanInvite.Text") - case .alertCancel: - return L10n.tr(key: "Alert.Cancel") - case .alertOK: - return L10n.tr(key: "Alert.OK") - case .alertUserDoesntExists: - return L10n.tr(key: "Alert.UserDoesntExists") - case .alertForwardError: - return L10n.tr(key: "Alert.Forward.Error") - case .alertSendErrorDelete: - return L10n.tr(key: "Alert.SendError.Delete") - case .alertSendErrorHeader: - return L10n.tr(key: "Alert.SendError.Header") - case .alertSendErrorIgnore: - return L10n.tr(key: "Alert.SendError.Ignore") - case .alertSendErrorResend: - return L10n.tr(key: "Alert.SendError.Resend") - case .alertSendErrorText: - return L10n.tr(key: "Alert.SendError.Text") - case .appMaxFileSize: - return L10n.tr(key: "App.MaxFileSize") - case .archivedStickersDescription: - return L10n.tr(key: "ArchivedStickers.Description") - case .audioUnknownArtist: - return L10n.tr(key: "Audio.UnknownArtist") - case .audioUntitledSong: - return L10n.tr(key: "Audio.UntitledSong") - case .audioControllerVideoMessage: - return L10n.tr(key: "AudioController.videoMessage") - case .audioControllerVoiceMessage: - return L10n.tr(key: "AudioController.voiceMessage") - case .audioRecordReleaseOut: - return L10n.tr(key: "AudioRecord.ReleaseOut") - case .aufd15bRTitle: - return L10n.tr(key: "aUF-d1-5bR.title") - case .bioDescription: - return L10n.tr(key: "Bio.Description") - case .bioPlaceholder: - return L10n.tr(key: "Bio.Placeholder") - case .bioSave: - return L10n.tr(key: "Bio.Save") - case .blockedPeersEmptyDescrpition: - return L10n.tr(key: "BlockedPeers.EmptyDescrpition") - case .bofnm1cWTitle: - return L10n.tr(key: "BOF-NM-1cW.title") - case .c8aY6VQdTitle: - return L10n.tr(key: "c8a-y6-VQd.title") - case .cagYXWT6Title: - return L10n.tr(key: "Cag-YX-WT6.title") - case .callParticipantVersionOutdatedError(let p1): - return L10n.tr(key: "Call.ParticipantVersionOutdatedError", p1) - case .callPrivacyErrorMessage(let p1): - return L10n.tr(key: "Call.PrivacyErrorMessage", p1) - case .callShortMinutesCountable(let p1): - return L10n.tr(key: "Call.ShortMinutes_countable", p1) - case .callShortMinutesFew(let p1): - return L10n.tr(key: "Call.ShortMinutes_few", p1) - case .callShortMinutesMany(let p1): - return L10n.tr(key: "Call.ShortMinutes_many", p1) - case .callShortMinutesOne(let p1): - return L10n.tr(key: "Call.ShortMinutes_one", p1) - case .callShortMinutesOther(let p1): - return L10n.tr(key: "Call.ShortMinutes_other", p1) - case .callShortMinutesTwo(let p1): - return L10n.tr(key: "Call.ShortMinutes_two", p1) - case .callShortMinutesZero(let p1): - return L10n.tr(key: "Call.ShortMinutes_zero", p1) - case .callShortSecondsCountable(let p1): - return L10n.tr(key: "Call.ShortSeconds_countable", p1) - case .callShortSecondsFew(let p1): - return L10n.tr(key: "Call.ShortSeconds_few", p1) - case .callShortSecondsMany(let p1): - return L10n.tr(key: "Call.ShortSeconds_many", p1) - case .callShortSecondsOne(let p1): - return L10n.tr(key: "Call.ShortSeconds_one", p1) - case .callShortSecondsOther(let p1): - return L10n.tr(key: "Call.ShortSeconds_other", p1) - case .callShortSecondsTwo(let p1): - return L10n.tr(key: "Call.ShortSeconds_two", p1) - case .callShortSecondsZero(let p1): - return L10n.tr(key: "Call.ShortSeconds_zero", p1) - case .callStatusBusy: - return L10n.tr(key: "Call.StatusBusy") - case .callStatusCalling: - return L10n.tr(key: "Call.StatusCalling") - case .callStatusConnecting: - return L10n.tr(key: "Call.StatusConnecting") - case .callStatusEnded: - return L10n.tr(key: "Call.StatusEnded") - case .callStatusFailed: - return L10n.tr(key: "Call.StatusFailed") - case .callStatusRequesting: - return L10n.tr(key: "Call.StatusRequesting") - case .callStatusRinging: - return L10n.tr(key: "Call.StatusRinging") - case .callUndefinedError: - return L10n.tr(key: "Call.UndefinedError") - case .callConfirmDiscardCurrentDescription(let p1, let p2): - return L10n.tr(key: "Call.Confirm.DiscardCurrent.Description", p1, p2) - case .callConfirmDiscardCurrentHeader: - return L10n.tr(key: "Call.Confirm.DiscardCurrent.Header") - case .callRatingModalPlaceholder: - return L10n.tr(key: "Call.RatingModal.Placeholder") - case .callRecentIncoming: - return L10n.tr(key: "Call.Recent.Incoming") - case .callRecentMissed: - return L10n.tr(key: "Call.Recent.Missed") - case .callRecentOutgoing: - return L10n.tr(key: "Call.Recent.Outgoing") - case .callHeaderEndCall: - return L10n.tr(key: "CallHeader.EndCall") - case .changeNumberConfirmCodeSuccess(let p1): - return L10n.tr(key: "ChangeNumber.ConfirmCode.Success", p1) - case .changeNumberConfirmCodeErrorCodeExpired: - return L10n.tr(key: "ChangeNumber.ConfirmCode.Error.codeExpired") - case .changeNumberConfirmCodeErrorGeneric: - return L10n.tr(key: "ChangeNumber.ConfirmCode.Error.Generic") - case .changeNumberConfirmCodeErrorInvalidCode: - return L10n.tr(key: "ChangeNumber.ConfirmCode.Error.invalidCode") - case .changeNumberConfirmCodeErrorLimitExceeded: - return L10n.tr(key: "ChangeNumber.ConfirmCode.Error.limitExceeded") - case .changeNumberSendDataErrorGeneric: - return L10n.tr(key: "ChangeNumber.SendData.Error.Generic") - case .changeNumberSendDataErrorInvalidPhoneNumber: - return L10n.tr(key: "ChangeNumber.SendData.Error.InvalidPhoneNumber") - case .changeNumberSendDataErrorLimitExceeded: - return L10n.tr(key: "ChangeNumber.SendData.Error.LimitExceeded") - case .changeNumberSendDataErrorPhoneNumberOccupied(let p1): - return L10n.tr(key: "ChangeNumber.SendData.Error.PhoneNumberOccupied", p1) - case .changePhoneNumberIntroAlert: - return L10n.tr(key: "ChangePhoneNumber.Intro.Alert") - case .changePhoneNumberIntroDescription: - return L10n.tr(key: "ChangePhoneNumber.Intro.Description") - case .channelBanForever: - return L10n.tr(key: "Channel.BanForever") - case .channelChannelNameHolder: - return L10n.tr(key: "Channel.ChannelNameHolder") - case .channelCreate: - return L10n.tr(key: "Channel.Create") - case .channelDescriptionHolder: - return L10n.tr(key: "Channel.DescriptionHolder") - case .channelDescriptionHolderDescrpiton: - return L10n.tr(key: "Channel.DescriptionHolderDescrpiton") - case .channelExportLinkAboutChannel: - return L10n.tr(key: "Channel.ExportLinkAboutChannel") - case .channelExportLinkAboutGroup: - return L10n.tr(key: "Channel.ExportLinkAboutGroup") - case .channelIntroDescription: - return L10n.tr(key: "Channel.IntroDescription") - case .channelIntroDescriptionHeader: - return L10n.tr(key: "Channel.IntroDescriptionHeader") - case .channelNewChannel: - return L10n.tr(key: "Channel.NewChannel") - case .channelPrivate: - return L10n.tr(key: "Channel.Private") - case .channelPrivateAboutChannel: - return L10n.tr(key: "Channel.PrivateAboutChannel") - case .channelPrivateAboutGroup: - return L10n.tr(key: "Channel.PrivateAboutGroup") - case .channelPublic: - return L10n.tr(key: "Channel.Public") - case .channelPublicAboutChannel: - return L10n.tr(key: "Channel.PublicAboutChannel") - case .channelPublicAboutGroup: - return L10n.tr(key: "Channel.PublicAboutGroup") - case .channelPublicNamesLimitError: - return L10n.tr(key: "Channel.PublicNamesLimitError") - case .channelTypeHeaderChannel: - return L10n.tr(key: "Channel.TypeHeaderChannel") - case .channelTypeHeaderGroup: - return L10n.tr(key: "Channel.TypeHeaderGroup") - case .channelUsernameAboutChannel: - return L10n.tr(key: "Channel.UsernameAboutChannel") - case .channelUsernameAboutGroup: - return L10n.tr(key: "Channel.UsernameAboutGroup") - case .channelUserRestriction: - return L10n.tr(key: "Channel.UserRestriction") - case .channelAdminAdminAccess: - return L10n.tr(key: "Channel.Admin.AdminAccess") - case .channelAdminAdminRestricted: - return L10n.tr(key: "Channel.Admin.AdminRestricted") - case .channelAdminCantEditRights: - return L10n.tr(key: "Channel.Admin.CantEditRights") - case .channelAdminDismiss: - return L10n.tr(key: "Channel.Admin.Dismiss") - case .channelAdminWhatCanAdminDo: - return L10n.tr(key: "Channel.Admin.WhatCanAdminDo") - case .channelAdminsAddAdminError: - return L10n.tr(key: "Channel.Admins.AddAdminError") - case .channelAdminsPromotedBy(let p1): - return L10n.tr(key: "Channel.Admins.PromotedBy", p1) - case .channelAdminsPromoteBannedAdminError: - return L10n.tr(key: "Channel.Admins.Promote.BannedAdminError") - case .channelAdminsPromoteUnmemberAdminError: - return L10n.tr(key: "Channel.Admins.Promote.UnmemberAdminError") - case .channelBlacklistBlockedBy(let p1): - return L10n.tr(key: "Channel.Blacklist.BlockedBy", p1) - case .channelBlacklistDemoteAdminError: - return L10n.tr(key: "Channel.Blacklist.DemoteAdminError") - case .channelBlacklistRestrictedBy(let p1): - return L10n.tr(key: "Channel.Blacklist.RestrictedBy", p1) - case .channelBlacklistSelectNewUserTitle: - return L10n.tr(key: "Channel.Blacklist.SelectNewUserTitle") - case .channelBlacklistUnban: - return L10n.tr(key: "Channel.Blacklist.Unban") - case .channelBlockUserBlockFor: - return L10n.tr(key: "Channel.BlockUser.BlockFor") - case .channelBlockUserCanEmbedLinks: - return L10n.tr(key: "Channel.BlockUser.CanEmbedLinks") - case .channelBlockUserCanReadMessages: - return L10n.tr(key: "Channel.BlockUser.CanReadMessages") - case .channelBlockUserCanSendMedia: - return L10n.tr(key: "Channel.BlockUser.CanSendMedia") - case .channelBlockUserCanSendMessages: - return L10n.tr(key: "Channel.BlockUser.CanSendMessages") - case .channelBlockUserCanSendStickers: - return L10n.tr(key: "Channel.BlockUser.CanSendStickers") - case .channelEditAdminPermissionAddNewAdmins: - return L10n.tr(key: "Channel.EditAdmin.Permission.AddNewAdmins") - case .channelEditAdminPermissionBanUsers: - return L10n.tr(key: "Channel.EditAdmin.Permission.BanUsers") - case .channelEditAdminPermissionChangeInfo: - return L10n.tr(key: "Channel.EditAdmin.Permission.ChangeInfo") - case .channelEditAdminPermissionDeleteMessages: - return L10n.tr(key: "Channel.EditAdmin.Permission.DeleteMessages") - case .channelEditAdminPermissionEditMessages: - return L10n.tr(key: "Channel.EditAdmin.Permission.EditMessages") - case .channelEditAdminPermissionInviteUsers: - return L10n.tr(key: "Channel.EditAdmin.Permission.InviteUsers") - case .channelEditAdminPermissionPinMessages: - return L10n.tr(key: "Channel.EditAdmin.Permission.PinMessages") - case .channelEditAdminPermissionPostMessages: - return L10n.tr(key: "Channel.EditAdmin.Permission.PostMessages") - case .channelEventFilterAdminsHeader: - return L10n.tr(key: "Channel.EventFilter.AdminsHeader") - case .channelEventFilterEventsHeader: - return L10n.tr(key: "Channel.EventFilter.EventsHeader") - case .channelEventLogEmpty: - return L10n.tr(key: "Channel.EventLog.Empty") - case .channelEventLogEmptySearch: - return L10n.tr(key: "Channel.EventLog.EmptySearch") - case .channelEventLogEmptyText: - return L10n.tr(key: "Channel.EventLog.EmptyText") - case .channelEventLogOriginalMessage: - return L10n.tr(key: "Channel.EventLog.OriginalMessage") - case .channelEventLogWhat: - return L10n.tr(key: "Channel.EventLog.What") - case .channelEventLogAlertHeader: - return L10n.tr(key: "Channel.EventLog.Alert.Header") - case .channelEventLogAlertInfo: - return L10n.tr(key: "Channel.EventLog.Alert.Info") - case .channelEventLogServiceAboutRemoved(let p1): - return L10n.tr(key: "Channel.EventLog.Service.AboutRemoved", p1) - case .channelEventLogServiceAboutUpdated(let p1): - return L10n.tr(key: "Channel.EventLog.Service.AboutUpdated", p1) - case .channelEventLogServiceDisableSignatures(let p1): - return L10n.tr(key: "Channel.EventLog.Service.DisableSignatures", p1) - case .channelEventLogServiceEnableSignatures(let p1): - return L10n.tr(key: "Channel.EventLog.Service.EnableSignatures", p1) - case .channelEventLogServiceLinkRemoved(let p1): - return L10n.tr(key: "Channel.EventLog.Service.LinkRemoved", p1) - case .channelEventLogServiceLinkUpdated(let p1): - return L10n.tr(key: "Channel.EventLog.Service.LinkUpdated", p1) - case .channelEventLogServicePhotoRemoved(let p1): - return L10n.tr(key: "Channel.EventLog.Service.PhotoRemoved", p1) - case .channelEventLogServicePhotoUpdated(let p1): - return L10n.tr(key: "Channel.EventLog.Service.PhotoUpdated", p1) - case .channelEventLogServiceTitleUpdated(let p1): - return L10n.tr(key: "Channel.EventLog.Service.TitleUpdated", p1) - case .channelEventLogServiceUpdateJoin(let p1): - return L10n.tr(key: "Channel.EventLog.Service.UpdateJoin", p1) - case .channelEventLogServiceUpdateLeft(let p1): - return L10n.tr(key: "Channel.EventLog.Service.UpdateLeft", p1) - case .channelPersmissionDeniedSendInlineForever: - return L10n.tr(key: "Channel.Persmission.Denied.SendInline.Forever") - case .channelPersmissionDeniedSendInlineUntil(let p1): - return L10n.tr(key: "Channel.Persmission.Denied.SendInline.Until", p1) - case .channelPersmissionDeniedSendMediaForever: - return L10n.tr(key: "Channel.Persmission.Denied.SendMedia.Forever") - case .channelPersmissionDeniedSendMediaUntil(let p1): - return L10n.tr(key: "Channel.Persmission.Denied.SendMedia.Until", p1) - case .channelPersmissionDeniedSendMessagesForever: - return L10n.tr(key: "Channel.Persmission.Denied.SendMessages.Forever") - case .channelPersmissionDeniedSendMessagesUntil(let p1): - return L10n.tr(key: "Channel.Persmission.Denied.SendMessages.Until", p1) - case .channelPersmissionDeniedSendStickersForever: - return L10n.tr(key: "Channel.Persmission.Denied.SendStickers.Forever") - case .channelPersmissionDeniedSendStickersUntil(let p1): - return L10n.tr(key: "Channel.Persmission.Denied.SendStickers.Until", p1) - case .channelSelectPeersContacts: - return L10n.tr(key: "Channel.SelectPeers.Contacts") - case .channelSelectPeersGlobal: - return L10n.tr(key: "Channel.SelectPeers.Global") - case .channelAdminsRecentActions: - return L10n.tr(key: "ChannelAdmins.RecentActions") - case .channelBlacklistAddMember: - return L10n.tr(key: "ChannelBlacklist.AddMember") - case .channelBlacklistBlocked: - return L10n.tr(key: "ChannelBlacklist.Blocked") - case .channelBlacklistEmptyDescrpition: - return L10n.tr(key: "ChannelBlacklist.EmptyDescrpition") - case .channelBlacklistRestricted: - return L10n.tr(key: "ChannelBlacklist.Restricted") - case .channelEventFilterChannelInfo: - return L10n.tr(key: "ChannelEventFilter.ChannelInfo") - case .channelEventFilterDeletedMessages: - return L10n.tr(key: "ChannelEventFilter.DeletedMessages") - case .channelEventFilterEditedMessages: - return L10n.tr(key: "ChannelEventFilter.EditedMessages") - case .channelEventFilterGroupInfo: - return L10n.tr(key: "ChannelEventFilter.GroupInfo") - case .channelEventFilterLeavingMembers: - return L10n.tr(key: "ChannelEventFilter.LeavingMembers") - case .channelEventFilterNewAdmins: - return L10n.tr(key: "ChannelEventFilter.NewAdmins") - case .channelEventFilterNewMembers: - return L10n.tr(key: "ChannelEventFilter.NewMembers") - case .channelEventFilterNewRestrictions: - return L10n.tr(key: "ChannelEventFilter.NewRestrictions") - case .channelEventFilterPinnedMessages: - return L10n.tr(key: "ChannelEventFilter.PinnedMessages") - case .channelMembersAddMembers: - return L10n.tr(key: "ChannelMembers.AddMembers") - case .channelMembersInviteLink: - return L10n.tr(key: "ChannelMembers.InviteLink") - case .channelMembersMembersListDesc: - return L10n.tr(key: "ChannelMembers.MembersListDesc") - case .channelMembersSelectTitle: - return L10n.tr(key: "ChannelMembers.Select.Title") - case .channelVisibilityChecking: - return L10n.tr(key: "ChannelVisibility.Checking") - case .channelVisibilityLoading: - return L10n.tr(key: "ChannelVisibility.Loading") - case .chatAdminBadge: - return L10n.tr(key: "Chat.AdminBadge") - case .chatCancel: - return L10n.tr(key: "Chat.Cancel") - case .chatDropAsFilesDesc: - return L10n.tr(key: "Chat.DropAsFilesDesc") - case .chatDropQuickDesc: - return L10n.tr(key: "Chat.DropQuickDesc") - case .chatDropTitle: - return L10n.tr(key: "Chat.DropTitle") - case .chatEmptyChat: - return L10n.tr(key: "Chat.EmptyChat") - case .chatForwardActionHeader: - return L10n.tr(key: "Chat.ForwardActionHeader") - case .chatInstantView: - return L10n.tr(key: "Chat.InstantView") - case .chatSearchCount(let p1, let p2): - return L10n.tr(key: "Chat.SearchCount", p1, p2) - case .chatSearchFrom: - return L10n.tr(key: "Chat.SearchFrom") - case .chatShareInlineResultActionHeader: - return L10n.tr(key: "Chat.ShareInlineResultActionHeader") - case .chatCallIncoming: - return L10n.tr(key: "Chat.Call.Incoming") - case .chatCallOutgoing: - return L10n.tr(key: "Chat.Call.Outgoing") - case .chatConfirmActionUndonable: - return L10n.tr(key: "Chat.Confirm.ActionUndonable") - case .chatConfirmDeleteMessages: - return L10n.tr(key: "Chat.Confirm.DeleteMessages") - case .chatConfirmDeleteMessagesForEveryone: - return L10n.tr(key: "Chat.Confirm.DeleteMessagesForEveryone") - case .chatConfirmUnpin: - return L10n.tr(key: "Chat.Confirm.Unpin") - case .chatConnectingStatusConnecting: - return L10n.tr(key: "Chat.ConnectingStatus.connecting") - case .chatConnectingStatusConnectingToProxy: - return L10n.tr(key: "Chat.ConnectingStatus.connectingToProxy") - case .chatConnectingStatusUpdating: - return L10n.tr(key: "Chat.ConnectingStatus.updating") - case .chatConnectingStatusWaitingNetwork: - return L10n.tr(key: "Chat.ConnectingStatus.waitingNetwork") - case .chatContextAddFavoriteSticker: - return L10n.tr(key: "Chat.Context.AddFavoriteSticker") - case .chatContextClearHistory: - return L10n.tr(key: "Chat.Context.ClearHistory") - case .chatContextCopyBlock: - return L10n.tr(key: "Chat.Context.CopyBlock") - case .chatContextDisableNotifications: - return L10n.tr(key: "Chat.Context.DisableNotifications") - case .chatContextEdit: - return L10n.tr(key: "Chat.Context.Edit") - case .chatContextEnableNotifications: - return L10n.tr(key: "Chat.Context.EnableNotifications") - case .chatContextInfo: - return L10n.tr(key: "Chat.Context.Info") - case .chatContextRemoveFavoriteSticker: - return L10n.tr(key: "Chat.Context.RemoveFavoriteSticker") - case .chatHeaderPinnedMessage: - return L10n.tr(key: "Chat.Header.PinnedMessage") - case .chatHeaderReportSpam: - return L10n.tr(key: "Chat.Header.ReportSpam") - case .chatInputDelete: - return L10n.tr(key: "Chat.Input.Delete") - case .chatInputJoin: - return L10n.tr(key: "Chat.Input.Join") - case .chatInputMute: - return L10n.tr(key: "Chat.Input.Mute") - case .chatInputReturn: - return L10n.tr(key: "Chat.Input.Return") - case .chatInputStartBot: - return L10n.tr(key: "Chat.Input.StartBot") - case .chatInputUnblock: - return L10n.tr(key: "Chat.Input.Unblock") - case .chatInputUnmute: - return L10n.tr(key: "Chat.Input.Unmute") - case .chatInputAccessoryEditMessage: - return L10n.tr(key: "Chat.Input.Accessory.EditMessage") - case .chatInputSecretChatWaitingToOnline: - return L10n.tr(key: "Chat.Input.SecretChat.WaitingToOnline") - case .chatListContact: - return L10n.tr(key: "Chat.List.Contact") - case .chatListGIF: - return L10n.tr(key: "Chat.List.GIF") - case .chatListInstantVideo: - return L10n.tr(key: "Chat.List.InstantVideo") - case .chatListMap: - return L10n.tr(key: "Chat.List.Map") - case .chatListPhoto: - return L10n.tr(key: "Chat.List.Photo") - case .chatListSticker(let p1): - return L10n.tr(key: "Chat.List.Sticker", p1) - case .chatListVideo: - return L10n.tr(key: "Chat.List.Video") - case .chatListVoice: - return L10n.tr(key: "Chat.List.Voice") - case .chatListServicePaymentSent(let p1): - return L10n.tr(key: "Chat.List.Service.PaymentSent", p1) - case .chatMessageEdited: - return L10n.tr(key: "Chat.Message.edited") - case .chatMessageUnsupported: - return L10n.tr(key: "Chat.Message.Unsupported") - case .chatMessageVia: - return L10n.tr(key: "Chat.Message.Via") - case .chatSecretChat1Feature: - return L10n.tr(key: "Chat.SecretChat.1Feature") - case .chatSecretChat2Feature: - return L10n.tr(key: "Chat.SecretChat.2Feature") - case .chatSecretChat3Feature: - return L10n.tr(key: "Chat.SecretChat.3Feature") - case .chatSecretChat4Feature: - return L10n.tr(key: "Chat.SecretChat.4Feature") - case .chatSecretChatEmptyHeader: - return L10n.tr(key: "Chat.SecretChat.EmptyHeader") - case .chatServicePaymentSent(let p1, let p2, let p3): - return L10n.tr(key: "Chat.Service.PaymentSent", p1, p2, p3) - case .chatServicePinnedMessage: - return L10n.tr(key: "Chat.Service.PinnedMessage") - case .chatServiceYou: - return L10n.tr(key: "Chat.Service.You") - case .chatServiceChannelRemovedPhoto: - return L10n.tr(key: "Chat.Service.Channel.RemovedPhoto") - case .chatServiceChannelUpdatedPhoto: - return L10n.tr(key: "Chat.Service.Channel.UpdatedPhoto") - case .chatServiceChannelUpdatedTitle(let p1): - return L10n.tr(key: "Chat.Service.Channel.UpdatedTitle", p1) - case .chatServiceGroupAddedMembers(let p1, let p2): - return L10n.tr(key: "Chat.Service.Group.AddedMembers", p1, p2) - case .chatServiceGroupAddedSelf(let p1): - return L10n.tr(key: "Chat.Service.Group.AddedSelf", p1) - case .chatServiceGroupCreated(let p1, let p2): - return L10n.tr(key: "Chat.Service.Group.Created", p1, p2) - case .chatServiceGroupJoinedByLink(let p1): - return L10n.tr(key: "Chat.Service.Group.JoinedByLink", p1) - case .chatServiceGroupMigratedToSupergroup: - return L10n.tr(key: "Chat.Service.Group.MigratedToSupergroup") - case .chatServiceGroupRemovedMembers(let p1, let p2): - return L10n.tr(key: "Chat.Service.Group.RemovedMembers", p1, p2) - case .chatServiceGroupRemovedPhoto(let p1): - return L10n.tr(key: "Chat.Service.Group.RemovedPhoto", p1) - case .chatServiceGroupRemovedSelf(let p1): - return L10n.tr(key: "Chat.Service.Group.RemovedSelf", p1) - case .chatServiceGroupTookScreenshot(let p1): - return L10n.tr(key: "Chat.Service.Group.TookScreenshot", p1) - case .chatServiceGroupUpdatedPhoto(let p1): - return L10n.tr(key: "Chat.Service.Group.UpdatedPhoto", p1) - case .chatServiceGroupUpdatedPinnedMessage(let p1, let p2): - return L10n.tr(key: "Chat.Service.Group.UpdatedPinnedMessage", p1, p2) - case .chatServiceGroupUpdatedTitle(let p1, let p2): - return L10n.tr(key: "Chat.Service.Group.UpdatedTitle", p1, p2) - case .chatServiceSecretChatDisabledTimer(let p1): - return L10n.tr(key: "Chat.Service.SecretChat.DisabledTimer", p1) - case .chatServiceSecretChatSetTimer(let p1, let p2): - return L10n.tr(key: "Chat.Service.SecretChat.SetTimer", p1, p2) - case .chatServiceSecretChatDisabledTimerSelf: - return L10n.tr(key: "Chat.Service.SecretChat.DisabledTimer.Self") - case .chatServiceSecretChatSetTimerSelf(let p1): - return L10n.tr(key: "Chat.Service.SecretChat.SetTimer.Self", p1) - case .chatTitleSelf: - return L10n.tr(key: "Chat.Title.self") - case .chatListDraft: - return L10n.tr(key: "ChatList.Draft") - case .chatListUnsupportedMessage: - return L10n.tr(key: "ChatList.UnsupportedMessage") - case .chatListYou: - return L10n.tr(key: "ChatList.You") - case .chatListContextCall: - return L10n.tr(key: "ChatList.Context.Call") - case .chatListContextClearHistory: - return L10n.tr(key: "ChatList.Context.ClearHistory") - case .chatListContextDeleteAndExit: - return L10n.tr(key: "ChatList.Context.DeleteAndExit") - case .chatListContextDeleteChat: - return L10n.tr(key: "ChatList.Context.DeleteChat") - case .chatListContextLeaveChannel: - return L10n.tr(key: "ChatList.Context.LeaveChannel") - case .chatListContextLeaveGroup: - return L10n.tr(key: "ChatList.Context.LeaveGroup") - case .chatListContextMute: - return L10n.tr(key: "ChatList.Context.Mute") - case .chatListContextPin: - return L10n.tr(key: "ChatList.Context.Pin") - case .chatListContextReturnGroup: - return L10n.tr(key: "ChatList.Context.ReturnGroup") - case .chatListContextUnmute: - return L10n.tr(key: "ChatList.Context.Unmute") - case .chatListContextUnpin: - return L10n.tr(key: "ChatList.Context.Unpin") - case .chatListSecretChatCreated(let p1): - return L10n.tr(key: "ChatList.SecretChat.Created", p1) - case .chatListSecretChatExKeys: - return L10n.tr(key: "ChatList.SecretChat.ExKeys") - case .chatListSecretChatJoined(let p1): - return L10n.tr(key: "ChatList.SecretChat.Joined", p1) - case .chatListSecretChatTerminated: - return L10n.tr(key: "ChatList.SecretChat.Terminated") - case .chatListServiceDestructingPhoto: - return L10n.tr(key: "ChatList.Service.DestructingPhoto") - case .chatListServiceDestructingVideo: - return L10n.tr(key: "ChatList.Service.DestructingVideo") - case .chatListServiceGameScored(let p1, let p2): - return L10n.tr(key: "ChatList.Service.GameScored", p1, p2) - case .chatListServiceCallCancelled: - return L10n.tr(key: "ChatList.Service.Call.Cancelled") - case .chatListServiceCallDisconnected: - return L10n.tr(key: "ChatList.Service.Call.Disconnected") - case .chatListServiceCallIncoming(let p1): - return L10n.tr(key: "ChatList.Service.Call.incoming", p1) - case .chatListServiceCallMissed: - return L10n.tr(key: "ChatList.Service.Call.Missed") - case .chatListServiceCallOutgoing(let p1): - return L10n.tr(key: "ChatList.Service.Call.outgoing", p1) - case .chatMessageTooltipViews: - return L10n.tr(key: "ChatMessage.Tooltip.Views") - case .chatServiceChannelCreated: - return L10n.tr(key: "ChatService.ChannelCreated") - case .composeCreate: - return L10n.tr(key: "Compose.Create") - case .composeNext: - return L10n.tr(key: "Compose.Next") - case .composeSelectUsers: - return L10n.tr(key: "Compose.SelectUsers") - case .composeConfirmStartSecretChat(let p1): - return L10n.tr(key: "Compose.Confirm.StartSecretChat", p1) - case .composePopoverNewChannel: - return L10n.tr(key: "Compose.Popover.NewChannel") - case .composePopoverNewGroup: - return L10n.tr(key: "Compose.Popover.NewGroup") - case .composePopoverNewSecretChat: - return L10n.tr(key: "Compose.Popover.NewSecretChat") - case .composeSelectSecretChat: - return L10n.tr(key: "Compose.Select.SecretChat") - case .composeSelectGroupUsersPlaceholder: - return L10n.tr(key: "Compose.SelectGroupUsers.Placeholder") - case .confirmAddBotToGroup(let p1): - return L10n.tr(key: "Confirm.AddBotToGroup", p1) - case .confirmDeleteAdminedChannel: - return L10n.tr(key: "Confirm.DeleteAdminedChannel") - case .confirmDeleteChatUser: - return L10n.tr(key: "Confirm.DeleteChatUser") - case .confirmLeaveGroup: - return L10n.tr(key: "Confirm.LeaveGroup") - case .connectingStatusConnecting: - return L10n.tr(key: "ConnectingStatus.connecting") - case .connectingStatusConnectingToProxy: - return L10n.tr(key: "ConnectingStatus.connectingToProxy") - case .connectingStatusDisableProxy: - return L10n.tr(key: "ConnectingStatus.DisableProxy") - case .connectingStatusOnline: - return L10n.tr(key: "ConnectingStatus.online") - case .connectingStatusUpdating: - return L10n.tr(key: "ConnectingStatus.updating") - case .connectingStatusWaitingNetwork: - return L10n.tr(key: "ConnectingStatus.waitingNetwork") - case .contactsAddContact: - return L10n.tr(key: "Contacts.AddContact") - case .contactsContacsSeparator: - return L10n.tr(key: "Contacts.ContacsSeparator") - case .contactsNotRegistredDescription: - return L10n.tr(key: "Contacts.NotRegistredDescription") - case .contactsNotRegistredTitle: - return L10n.tr(key: "Contacts.NotRegistredTitle") - case .contactsFirstNamePlaceholder: - return L10n.tr(key: "Contacts.FirstName.Placeholder") - case .contactsLastNamePlaceholder: - return L10n.tr(key: "Contacts.LastName.Placeholder") - case .contactsPhoneNumberPlaceholder: - return L10n.tr(key: "Contacts.PhoneNumber.Placeholder") - case .contextCopyMedia: - return L10n.tr(key: "Context.CopyMedia") - case .contextRecentGifRemove: - return L10n.tr(key: "Context.RecentGifRemove") - case .contextRemoveFaveSticker: - return L10n.tr(key: "Context.RemoveFaveSticker") - case .contextShowInFinder: - return L10n.tr(key: "Context.ShowInFinder") - case .contextViewStickerSet: - return L10n.tr(key: "Context.ViewStickerSet") - case .convertToSuperGroupConfirm: - return L10n.tr(key: "ConvertToSuperGroup.Confirm") - case .convertToSupergroupAlertError: - return L10n.tr(key: "ConvertToSupergroup.Alert.Error") - case .createGroupNameHolder: - return L10n.tr(key: "CreateGroup.NameHolder") - case .cwLP1JidTitle: - return L10n.tr(key: "cwL-P1-jid.title") - case .d9MCDAMdTitle: - return L10n.tr(key: "d9M-CD-aMd.title") - case .dataAndStorageNetworkUsage: - return L10n.tr(key: "DataAndStorage.NetworkUsage") - case .dataAndStorageStorageUsage: - return L10n.tr(key: "DataAndStorage.StorageUsage") - case .dataAndStorageAutomaticAudioDownloadHeader: - return L10n.tr(key: "DataAndStorage.AutomaticAudioDownload.Header") - case .dataAndStorageAutomaticDownloadGroupsChannels: - return L10n.tr(key: "DataAndStorage.AutomaticDownload.GroupsChannels") - case .dataAndStorageAutomaticPhotoDownloadHeader: - return L10n.tr(key: "DataAndStorage.AutomaticPhotoDownload.Header") - case .dataAndStorageAutomaticVideoDownloadHeader: - return L10n.tr(key: "DataAndStorage.AutomaticVideoDownload.Header") - case .dateToday: - return L10n.tr(key: "Date.Today") - case .drj4nYzgTitle: - return L10n.tr(key: "dRJ-4n-Yzg.title") - case .dv1IoYv7Title: - return L10n.tr(key: "Dv1-io-Yv7.title") - case .emojiActivityAndSport: - return L10n.tr(key: "Emoji.ActivityAndSport") - case .emojiAnimalsAndNature: - return L10n.tr(key: "Emoji.AnimalsAndNature") - case .emojiFlags: - return L10n.tr(key: "Emoji.Flags") - case .emojiFoodAndDrink: - return L10n.tr(key: "Emoji.FoodAndDrink") - case .emojiObjects: - return L10n.tr(key: "Emoji.Objects") - case .emojiRecent: - return L10n.tr(key: "Emoji.Recent") - case .emojiSmilesAndPeople: - return L10n.tr(key: "Emoji.SmilesAndPeople") - case .emojiSymbols: - return L10n.tr(key: "Emoji.Symbols") - case .emojiTravelAndPlaces: - return L10n.tr(key: "Emoji.TravelAndPlaces") - case .emptyPeerDescription: - return L10n.tr(key: "EmptyPeer.Description") - case .encryptionKeyDescription(let p1, let p2): - return L10n.tr(key: "EncryptionKey.Description", p1, p2) - case .entertainmentEmoji: - return L10n.tr(key: "Entertainment.Emoji") - case .entertainmentGIF: - return L10n.tr(key: "Entertainment.GIF") - case .entertainmentStickers: - return L10n.tr(key: "Entertainment.Stickers") - case .entertainmentSwitchEmoji: - return L10n.tr(key: "Entertainment.Switch.Emoji") - case .entertainmentSwitchGifAndStickers: - return L10n.tr(key: "Entertainment.Switch.GifAndStickers") - case .errorUsernameAlreadyTaken: - return L10n.tr(key: "Error.Username.AlreadyTaken") - case .errorUsernameInvalid: - return L10n.tr(key: "Error.Username.Invalid") - case .errorUsernameMinimumLength: - return L10n.tr(key: "Error.Username.MinimumLength") - case .errorUsernameNumberStart: - return L10n.tr(key: "Error.Username.NumberStart") - case .errorUsernameUnderscopeEnd: - return L10n.tr(key: "Error.Username.UnderscopeEnd") - case .errorUsernameUnderscopeStart: - return L10n.tr(key: "Error.Username.UnderscopeStart") - case .eventLogServiceBanned(let p1, let p2): - return L10n.tr(key: "EventLog.Service.Banned", p1, p2) - case .eventLogServiceChangedStickerSet(let p1): - return L10n.tr(key: "EventLog.Service.ChangedStickerSet", p1) - case .eventLogServiceDeletedMessage(let p1): - return L10n.tr(key: "EventLog.Service.DeletedMessage", p1) - case .eventLogServiceDemoted(let p1, let p2): - return L10n.tr(key: "EventLog.Service.Demoted", p1, p2) - case .eventLogServiceEditedMessage(let p1): - return L10n.tr(key: "EventLog.Service.EditedMessage", p1) - case .eventLogServicePreviousDesc: - return L10n.tr(key: "EventLog.Service.PreviousDesc") - case .eventLogServicePreviousLink: - return L10n.tr(key: "EventLog.Service.PreviousLink") - case .eventLogServicePreviousTitle: - return L10n.tr(key: "EventLog.Service.PreviousTitle") - case .eventLogServicePromoted(let p1, let p2): - return L10n.tr(key: "EventLog.Service.Promoted", p1, p2) - case .eventLogServiceRemovedStickerSet(let p1): - return L10n.tr(key: "EventLog.Service.RemovedStickerSet", p1) - case .eventLogServiceRemovePinned(let p1): - return L10n.tr(key: "EventLog.Service.RemovePinned", p1) - case .eventLogServiceUpdatePinned(let p1): - return L10n.tr(key: "EventLog.Service.UpdatePinned", p1) - case .eventLogServiceDemoteEmbedLinks: - return L10n.tr(key: "EventLog.Service.Demote.EmbedLinks") - case .eventLogServiceDemoteSendInline: - return L10n.tr(key: "EventLog.Service.Demote.SendInline") - case .eventLogServiceDemoteSendMedia: - return L10n.tr(key: "EventLog.Service.Demote.SendMedia") - case .eventLogServiceDemoteSendMessages: - return L10n.tr(key: "EventLog.Service.Demote.SendMessages") - case .eventLogServiceDemoteSendStickers: - return L10n.tr(key: "EventLog.Service.Demote.SendStickers") - case .eventLogServiceDemotedChanged(let p1, let p2): - return L10n.tr(key: "EventLog.Service.Demoted.Changed", p1, p2) - case .eventLogServiceDemotedUntil(let p1, let p2, let p3): - return L10n.tr(key: "EventLog.Service.Demoted.Until", p1, p2, p3) - case .eventLogServiceDemotedChangedUntil(let p1, let p2, let p3): - return L10n.tr(key: "EventLog.Service.Demoted.Changed.Until", p1, p2, p3) - case .eventLogServicePromoteAddNewAdmins: - return L10n.tr(key: "EventLog.Service.Promote.AddNewAdmins") - case .eventLogServicePromoteAddUsers: - return L10n.tr(key: "EventLog.Service.Promote.AddUsers") - case .eventLogServicePromoteBanUsers: - return L10n.tr(key: "EventLog.Service.Promote.BanUsers") - case .eventLogServicePromoteChangeInfo: - return L10n.tr(key: "EventLog.Service.Promote.ChangeInfo") - case .eventLogServicePromoteDeleteMessages: - return L10n.tr(key: "EventLog.Service.Promote.DeleteMessages") - case .eventLogServicePromoteEditMessages: - return L10n.tr(key: "EventLog.Service.Promote.EditMessages") - case .eventLogServicePromoteInviteViaLink: - return L10n.tr(key: "EventLog.Service.Promote.InviteViaLink") - case .eventLogServicePromotePinMessages: - return L10n.tr(key: "EventLog.Service.Promote.PinMessages") - case .eventLogServicePromotePostMessages: - return L10n.tr(key: "EventLog.Service.Promote.PostMessages") - case .eventLogServicePromotedChanged(let p1, let p2): - return L10n.tr(key: "EventLog.Service.Promoted.Changed", p1, p2) - case .fastSettingsDisableDarkMode: - return L10n.tr(key: "FastSettings.DisableDarkMode") - case .fastSettingsEnableDarkMode: - return L10n.tr(key: "FastSettings.EnableDarkMode") - case .fastSettingsLockTelegram: - return L10n.tr(key: "FastSettings.LockTelegram") - case .fastSettingsMute2Hours: - return L10n.tr(key: "FastSettings.Mute2Hours") - case .fastSettingsSetPasscode: - return L10n.tr(key: "FastSettings.SetPasscode") - case .fastSettingsUnmute: - return L10n.tr(key: "FastSettings.Unmute") - case .feMD8WVrTitle: - return L10n.tr(key: "FeM-D8-WVr.title") - case .forwardModalActionDescriptionCountable(let p1, let p2): - return L10n.tr(key: "ForwardModalAction.description_countable", p1, p2) - case .forwardModalActionDescriptionFew(let p1): - return L10n.tr(key: "ForwardModalAction.description_few", p1) - case .forwardModalActionDescriptionMany(let p1): - return L10n.tr(key: "ForwardModalAction.description_many", p1) - case .forwardModalActionDescriptionOne(let p1): - return L10n.tr(key: "ForwardModalAction.description_one", p1) - case .forwardModalActionDescriptionOther(let p1): - return L10n.tr(key: "ForwardModalAction.description_other", p1) - case .forwardModalActionDescriptionTwo(let p1): - return L10n.tr(key: "ForwardModalAction.description_two", p1) - case .forwardModalActionDescriptionZero(let p1): - return L10n.tr(key: "ForwardModalAction.description_zero", p1) - case .forwardModalActionTitleCountable(let p1): - return L10n.tr(key: "ForwardModalAction.Title_countable", p1) - case .forwardModalActionTitleFew: - return L10n.tr(key: "ForwardModalAction.Title_few") - case .forwardModalActionTitleMany: - return L10n.tr(key: "ForwardModalAction.Title_many") - case .forwardModalActionTitleOne: - return L10n.tr(key: "ForwardModalAction.Title_one") - case .forwardModalActionTitleOther: - return L10n.tr(key: "ForwardModalAction.Title_other") - case .forwardModalActionTitleTwo: - return L10n.tr(key: "ForwardModalAction.Title_two") - case .forwardModalActionTitleZero: - return L10n.tr(key: "ForwardModalAction.Title_zero") - case .galleryContextDeletePhoto: - return L10n.tr(key: "Gallery.ContextDeletePhoto") - case .galleryCounter(let p1, let p2): - return L10n.tr(key: "Gallery.Counter", p1, p2) - case .galleryContextCopyToClipboard: - return L10n.tr(key: "Gallery.Context.CopyToClipboard") - case .galleryContextSaveAs: - return L10n.tr(key: "Gallery.Context.SaveAs") - case .galleryContextShowMessage: - return L10n.tr(key: "Gallery.Context.ShowMessage") - case .generalSettingsAppearanceSettings: - return L10n.tr(key: "GeneralSettings.AppearanceSettings") - case .generalSettingsDarkMode: - return L10n.tr(key: "GeneralSettings.DarkMode") - case .generalSettingsEmojiReplacements: - return L10n.tr(key: "GeneralSettings.EmojiReplacements") - case .generalSettingsEnableSidebar: - return L10n.tr(key: "GeneralSettings.EnableSidebar") - case .generalSettingsForceTouchHeader: - return L10n.tr(key: "GeneralSettings.ForceTouchHeader") - case .generalSettingsGeneralSettings: - return L10n.tr(key: "GeneralSettings.GeneralSettings") - case .generalSettingsInAppSounds: - return L10n.tr(key: "GeneralSettings.InAppSounds") - case .generalSettingsInputSettings: - return L10n.tr(key: "GeneralSettings.InputSettings") - case .generalSettingsLargeFonts: - return L10n.tr(key: "GeneralSettings.LargeFonts") - case .generalSettingsMediaKeysForInAppPlayer: - return L10n.tr(key: "GeneralSettings.MediaKeysForInAppPlayer") - case .generalSettingsSendByCmdEnter: - return L10n.tr(key: "GeneralSettings.SendByCmdEnter") - case .generalSettingsSendByEnter: - return L10n.tr(key: "GeneralSettings.SendByEnter") - case .generalSettingsDarkModeDescription: - return L10n.tr(key: "GeneralSettings.DarkMode.Description") - case .generalSettingsFontDescription: - return L10n.tr(key: "GeneralSettings.Font.Description") - case .generalSettingsForceTouchEdit: - return L10n.tr(key: "GeneralSettings.ForceTouch.Edit") - case .generalSettingsForceTouchForward: - return L10n.tr(key: "GeneralSettings.ForceTouch.Forward") - case .generalSettingsForceTouchReply: - return L10n.tr(key: "GeneralSettings.ForceTouch.Reply") - case .groupCreateGroup: - return L10n.tr(key: "Group.CreateGroup") - case .groupNewGroup: - return L10n.tr(key: "Group.NewGroup") - case .groupUnavailable: - return L10n.tr(key: "Group.Unavailable") - case .groupEditAdminPermissionChangeInfo: - return L10n.tr(key: "Group.EditAdmin.Permission.ChangeInfo") - case .groupEventLogEmptyText: - return L10n.tr(key: "Group.EventLog.EmptyText") - case .groupEventLogServiceAboutRemoved(let p1): - return L10n.tr(key: "Group.EventLog.Service.AboutRemoved", p1) - case .groupEventLogServiceAboutUpdated(let p1): - return L10n.tr(key: "Group.EventLog.Service.AboutUpdated", p1) - case .groupEventLogServiceDisableInvites(let p1): - return L10n.tr(key: "Group.EventLog.Service.DisableInvites", p1) - case .groupEventLogServiceEnableInvites(let p1): - return L10n.tr(key: "Group.EventLog.Service.EnableInvites", p1) - case .groupEventLogServiceLinkRemoved(let p1): - return L10n.tr(key: "Group.EventLog.Service.LinkRemoved", p1) - case .groupEventLogServiceLinkUpdated(let p1): - return L10n.tr(key: "Group.EventLog.Service.LinkUpdated", p1) - case .groupEventLogServicePhotoRemoved(let p1): - return L10n.tr(key: "Group.EventLog.Service.PhotoRemoved", p1) - case .groupEventLogServicePhotoUpdated(let p1): - return L10n.tr(key: "Group.EventLog.Service.PhotoUpdated", p1) - case .groupEventLogServiceTitleUpdated(let p1): - return L10n.tr(key: "Group.EventLog.Service.TitleUpdated", p1) - case .groupEventLogServiceUpdateJoin(let p1): - return L10n.tr(key: "Group.EventLog.Service.UpdateJoin", p1) - case .groupEventLogServiceUpdateLeft(let p1): - return L10n.tr(key: "Group.EventLog.Service.UpdateLeft", p1) - case .groupAdminsAllMembersAdmins: - return L10n.tr(key: "GroupAdmins.AllMembersAdmins") - case .groupAdminsDescAdminInvites: - return L10n.tr(key: "GroupAdmins.Desc.AdminInvites") - case .groupAdminsDescAllInvites: - return L10n.tr(key: "GroupAdmins.Desc.AllInvites") - case .groupInvationChannelDescription: - return L10n.tr(key: "GroupInvation.ChannelDescription") - case .groupInvationCopyLink: - return L10n.tr(key: "GroupInvation.CopyLink") - case .groupInvationGroupDescription: - return L10n.tr(key: "GroupInvation.GroupDescription") - case .groupInvationRevoke: - return L10n.tr(key: "GroupInvation.Revoke") - case .groupInvationShare: - return L10n.tr(key: "GroupInvation.Share") - case .groupsInCommonEmpty: - return L10n.tr(key: "GroupsInCommon.Empty") - case .groupStickersChooseHeader: - return L10n.tr(key: "GroupStickers.ChooseHeader") - case .groupStickersCreateDescription: - return L10n.tr(key: "GroupStickers.CreateDescription") - case .groupStickersEmptyDesc: - return L10n.tr(key: "GroupStickers.EmptyDesc") - case .groupStickersEmptyHeader: - return L10n.tr(key: "GroupStickers.EmptyHeader") - case .gvau4SdLTitle: - return L10n.tr(key: "gVA-U4-sdL.title") - case .h8h7bM4vTitle: - return L10n.tr(key: "H8h-7b-M4v.title") - case .hFoCyZxITitle: - return L10n.tr(key: "HFo-cy-zxI.title") - case .hfqgknfaTitle: - return L10n.tr(key: "HFQ-gK-NFA.title") - case .hQb2vFYvTitle: - return L10n.tr(key: "hQb-2v-fYv.title") - case .hyVFhRgOTitle: - return L10n.tr(key: "HyV-fh-RgO.title") - case .hz2CUCR7Title: - return L10n.tr(key: "hz2-CU-CR7.title") - case .inAppLinksConfirmOpenExternal(let p1): - return L10n.tr(key: "InAppLinks.Confirm.OpenExternal", p1) - case .inlineModalActionDesc(let p1): - return L10n.tr(key: "InlineModalAction.Desc", p1) - case .inlineModalActionTitle: - return L10n.tr(key: "InlineModalAction.Title") - case .inputAttachPopoverFile: - return L10n.tr(key: "InputAttach.Popover.File") - case .inputAttachPopoverPhotoOrVideo: - return L10n.tr(key: "InputAttach.Popover.PhotoOrVideo") - case .inputAttachPopoverPicture: - return L10n.tr(key: "InputAttach.Popover.Picture") - case .installedStickersArchived: - return L10n.tr(key: "InstalledStickers.Archived") - case .installedStickersDescrpiption: - return L10n.tr(key: "InstalledStickers.Descrpiption") - case .installedStickersPacksTitle: - return L10n.tr(key: "InstalledStickers.PacksTitle") - case .installedStickersTranding: - return L10n.tr(key: "InstalledStickers.Tranding") - case .installedStickersRemoveDelete: - return L10n.tr(key: "InstalledStickers.Remove.Delete") - case .installedStickersRemoveDescription: - return L10n.tr(key: "InstalledStickers.Remove.Description") - case .instantPageAuthorAndDateTitle(let p1, let p2): - return L10n.tr(key: "InstantPage.AuthorAndDateTitle", p1, p2) - case .ivChannelJoin: - return L10n.tr(key: "IV.Channel.Join") - case .joinLinkJoin: - return L10n.tr(key: "JoinLink.Join") - case .kd2MpPUSTitle: - return L10n.tr(key: "Kd2-mp-pUS.title") - case .le2AR0XJTitle: - return L10n.tr(key: "LE2-aR-0XJ.title") - case .legacyIntroDescription1: - return L10n.tr(key: "Legacy.Intro.Description1") - case .legacyIntroDescription2: - return L10n.tr(key: "Legacy.Intro.Description2") - case .legacyIntroNext: - return L10n.tr(key: "Legacy.Intro.Next") - case .linkInvationChannelConfirmRevoke: - return L10n.tr(key: "LinkInvation.Channel.Confirm.Revoke") - case .linkInvationConfirmOk: - return L10n.tr(key: "LinkInvation.Confirm.Ok") - case .linkInvationGroupConfirmRevoke: - return L10n.tr(key: "LinkInvation.Group.Confirm.Revoke") - case .loginCodePlaceholder: - return L10n.tr(key: "Login.codePlaceholder") - case .loginContinueOnLanguage: - return L10n.tr(key: "Login.ContinueOnLanguage") - case .loginCountryLabel: - return L10n.tr(key: "Login.countryLabel") - case .loginEnterCodeFromApp: - return L10n.tr(key: "Login.EnterCodeFromApp") - case .loginEnterPasswordDescription: - return L10n.tr(key: "Login.EnterPasswordDescription") - case .loginFloodWait: - return L10n.tr(key: "Login.FloodWait") - case .loginInvalidCountryCode: - return L10n.tr(key: "Login.InvalidCountryCode") - case .loginJustSentSms: - return L10n.tr(key: "Login.JustSentSms") - case .loginNext: - return L10n.tr(key: "Login.Next") - case .loginPasswordPlaceholder: - return L10n.tr(key: "Login.passwordPlaceholder") - case .loginPhoneCalledCode: - return L10n.tr(key: "Login.PhoneCalledCode") - case .loginPhoneDialed: - return L10n.tr(key: "Login.PhoneDialed") - case .loginPhoneFieldPlaceholder: - return L10n.tr(key: "Login.phoneFieldPlaceholder") - case .loginPhoneNumberNotRegistred: - return L10n.tr(key: "Login.PhoneNumberNotRegistred") - case .loginRecoveryMailFailed: - return L10n.tr(key: "Login.RecoveryMailFailed") - case .loginResetAccount: - return L10n.tr(key: "Login.ResetAccount") - case .loginResetAccountDescription: - return L10n.tr(key: "Login.ResetAccountDescription") - case .loginSendSmsIfNotReceivedAppCode: - return L10n.tr(key: "Login.SendSmsIfNotReceivedAppCode") - case .loginWelcomeDescription: - return L10n.tr(key: "Login.WelcomeDescription") - case .loginWillCall(let p1, let p2): - return L10n.tr(key: "Login.willCall", p1, p2) - case .loginWillSendSms(let p1, let p2): - return L10n.tr(key: "Login.willSendSms", p1, p2) - case .loginYourCodeLabel: - return L10n.tr(key: "Login.YourCodeLabel") - case .loginYourPasswordLabel: - return L10n.tr(key: "Login.YourPasswordLabel") - case .loginYourPhoneLabel: - return L10n.tr(key: "Login.YourPhoneLabel") - case .loginHeaderCode: - return L10n.tr(key: "Login.Header.Code") - case .loginHeaderPassword: - return L10n.tr(key: "Login.Header.Password") - case .loginHeaderSignUp: - return L10n.tr(key: "Login.Header.SignUp") - case .messageAccessoryPanelForwardedCountable(let p1): - return L10n.tr(key: "Message.AccessoryPanel.Forwarded_countable", p1) - case .messageAccessoryPanelForwardedFew(let p1): - return L10n.tr(key: "Message.AccessoryPanel.Forwarded_few", p1) - case .messageAccessoryPanelForwardedMany(let p1): - return L10n.tr(key: "Message.AccessoryPanel.Forwarded_many", p1) - case .messageAccessoryPanelForwardedOne(let p1): - return L10n.tr(key: "Message.AccessoryPanel.Forwarded_one", p1) - case .messageAccessoryPanelForwardedOther(let p1): - return L10n.tr(key: "Message.AccessoryPanel.Forwarded_other", p1) - case .messageAccessoryPanelForwardedTwo(let p1): - return L10n.tr(key: "Message.AccessoryPanel.Forwarded_two", p1) - case .messageAccessoryPanelForwardedZero(let p1): - return L10n.tr(key: "Message.AccessoryPanel.Forwarded_zero", p1) - case .messageActionsPanelDelete: - return L10n.tr(key: "Message.ActionsPanel.Delete") - case .messageActionsPanelEmptySelected: - return L10n.tr(key: "Message.ActionsPanel.EmptySelected") - case .messageActionsPanelForward: - return L10n.tr(key: "Message.ActionsPanel.Forward") - case .messageActionsPanelSelectedCountCountable(let p1): - return L10n.tr(key: "Message.ActionsPanel.SelectedCount_countable", p1) - case .messageActionsPanelSelectedCountFew(let p1): - return L10n.tr(key: "Message.ActionsPanel.SelectedCount_few", p1) - case .messageActionsPanelSelectedCountMany(let p1): - return L10n.tr(key: "Message.ActionsPanel.SelectedCount_many", p1) - case .messageActionsPanelSelectedCountOne(let p1): - return L10n.tr(key: "Message.ActionsPanel.SelectedCount_one", p1) - case .messageActionsPanelSelectedCountOther(let p1): - return L10n.tr(key: "Message.ActionsPanel.SelectedCount_other", p1) - case .messageActionsPanelSelectedCountTwo(let p1): - return L10n.tr(key: "Message.ActionsPanel.SelectedCount_two", p1) - case .messageActionsPanelSelectedCountZero(let p1): - return L10n.tr(key: "Message.ActionsPanel.SelectedCount_zero", p1) - case .messageContextDelete: - return L10n.tr(key: "Message.Context.Delete") - case .messageContextEdit: - return L10n.tr(key: "Message.Context.Edit") - case .messageContextForward: - return L10n.tr(key: "Message.Context.Forward") - case .messageContextForwardToCloud: - return L10n.tr(key: "Message.Context.ForwardToCloud") - case .messageContextGoto: - return L10n.tr(key: "Message.Context.Goto") - case .messageContextPin: - return L10n.tr(key: "Message.Context.Pin") - case .messageContextReply: - return L10n.tr(key: "Message.Context.Reply") - case .messageContextSaveGif: - return L10n.tr(key: "Message.Context.SaveGif") - case .messageContextSelect: - return L10n.tr(key: "Message.Context.Select") - case .messageContextConfirmOnlyPin: - return L10n.tr(key: "Message.Context.Confirm.OnlyPin") - case .messageContextConfirmPin: - return L10n.tr(key: "Message.Context.Confirm.Pin") - case .messageContextCopyMessageLink: - return L10n.tr(key: "MessageContext.CopyMessageLink") - case .messagesDeletedMessage: - return L10n.tr(key: "Messages.DeletedMessage") - case .messagesForwardHeader: - return L10n.tr(key: "Messages.ForwardHeader") - case .messagesUnreadMark: - return L10n.tr(key: "Messages.UnreadMark") - case .messagesFileStateFetchingIn1(let p1): - return L10n.tr(key: "Messages.File.State.FetchingIn_1", p1) - case .messagesFileStateFetchingOut1(let p1): - return L10n.tr(key: "Messages.File.State.FetchingOut_1", p1) - case .messagesFileStateLocal: - return L10n.tr(key: "Messages.File.State.Local") - case .messagesFileStateRemote: - return L10n.tr(key: "Messages.File.State.Remote") - case .messagesPlaceholderBroadcast: - return L10n.tr(key: "Messages.Placeholder.Broadcast") - case .messagesPlaceholderSentMessage: - return L10n.tr(key: "Messages.Placeholder.SentMessage") - case .messagesReplyLoadingHeader: - return L10n.tr(key: "Messages.ReplyLoading.Header") - case .messagesReplyLoadingLoading: - return L10n.tr(key: "Messages.ReplyLoading.Loading") - case .mk62p4JGTitle: - return L10n.tr(key: "mK6-2p-4JG.title") - case .modalCancel: - return L10n.tr(key: "Modal.Cancel") - case .modalCopyLink: - return L10n.tr(key: "Modal.CopyLink") - case .modalOK: - return L10n.tr(key: "Modal.OK") - case .modalSend: - return L10n.tr(key: "Modal.Send") - case .modalShare: - return L10n.tr(key: "Modal.Share") - case .navigationBack: - return L10n.tr(key: "Navigation.back") - case .navigationCancel: - return L10n.tr(key: "Navigation.Cancel") - case .navigationClose: - return L10n.tr(key: "Navigation.Close") - case .navigationDone: - return L10n.tr(key: "Navigation.Done") - case .navigationEdit: - return L10n.tr(key: "Navigation.Edit") - case .notificationLockedPreview: - return L10n.tr(key: "Notification.LockedPreview") - case .notificationSettingsMessagesPreview: - return L10n.tr(key: "NotificationSettings.MessagesPreview") - case .notificationSettingsNotificationTone: - return L10n.tr(key: "NotificationSettings.NotificationTone") - case .notificationSettingsResetNotifications: - return L10n.tr(key: "NotificationSettings.ResetNotifications") - case .notificationSettingsResetNotificationsText: - return L10n.tr(key: "NotificationSettings.ResetNotificationsText") - case .notificationSettingsToggleNotifications: - return L10n.tr(key: "NotificationSettings.ToggleNotifications") - case .notificationSettingsConfirmReset: - return L10n.tr(key: "NotificationSettings.Confirm.Reset") - case .notificationSettingsToneDefault: - return L10n.tr(key: "NotificationSettings.Tone.Default") - case .olwNPBQNTitle: - return L10n.tr(key: "Olw-nP-bQN.title") - case .oy7WFPoVTitle: - return L10n.tr(key: "OY7-WF-poV.title") - case .pa3QIU2kTitle: - return L10n.tr(key: "pa3-QI-u2k.title") - case .passcodeAutolock: - return L10n.tr(key: "Passcode.Autolock") - case .passcodeChange: - return L10n.tr(key: "Passcode.Change") - case .passcodeEnterCurrentPlaceholder: - return L10n.tr(key: "Passcode.EnterCurrentPlaceholder") - case .passcodeEnterNewPlaceholder: - return L10n.tr(key: "Passcode.EnterNewPlaceholder") - case .passcodeEnterPasscodePlaceholder: - return L10n.tr(key: "Passcode.EnterPasscodePlaceholder") - case .passcodeLogoutDescription: - return L10n.tr(key: "Passcode.LogoutDescription") - case .passcodeLogoutLinkText: - return L10n.tr(key: "Passcode.LogoutLinkText") - case .passcodeNext: - return L10n.tr(key: "Passcode.Next") - case .passcodeReEnterPlaceholder: - return L10n.tr(key: "Passcode.ReEnterPlaceholder") - case .passcodeTurnOff: - return L10n.tr(key: "Passcode.TurnOff") - case .passcodeTurnOn: - return L10n.tr(key: "Passcode.TurnOn") - case .passcodeTurnOnDescription: - return L10n.tr(key: "Passcode.TurnOnDescription") - case .passcodeAutoLockDisabled: - return L10n.tr(key: "Passcode.AutoLock.Disabled") - case .passcodeAutoLockIfAway(let p1): - return L10n.tr(key: "Passcode.AutoLock.IfAway", p1) - case .paymentsUnsupported: - return L10n.tr(key: "Payments.Unsupported") - case .peerDeletedUser: - return L10n.tr(key: "Peer.DeletedUser") - case .peerServiceNotifications: - return L10n.tr(key: "Peer.ServiceNotifications") - case .peerActivityChatMultiRecordingAudio(let p1): - return L10n.tr(key: "Peer.Activity.Chat.Multi.RecordingAudio", p1) - case .peerActivityChatMultiRecordingVideo(let p1): - return L10n.tr(key: "Peer.Activity.Chat.Multi.RecordingVideo", p1) - case .peerActivityChatMultiSendingAudio(let p1): - return L10n.tr(key: "Peer.Activity.Chat.Multi.SendingAudio", p1) - case .peerActivityChatMultiSendingFile(let p1): - return L10n.tr(key: "Peer.Activity.Chat.Multi.SendingFile", p1) - case .peerActivityChatMultiSendingPhoto(let p1): - return L10n.tr(key: "Peer.Activity.Chat.Multi.SendingPhoto", p1) - case .peerActivityChatMultiSendingVideo(let p1): - return L10n.tr(key: "Peer.Activity.Chat.Multi.SendingVideo", p1) - case .peerActivityChatMultiTypingText(let p1): - return L10n.tr(key: "Peer.Activity.Chat.Multi.TypingText", p1) - case .peerActivityUserRecordingAudio: - return L10n.tr(key: "Peer.Activity.User.RecordingAudio") - case .peerActivityUserRecordingVideo: - return L10n.tr(key: "Peer.Activity.User.RecordingVideo") - case .peerActivityUserSendingFile: - return L10n.tr(key: "Peer.Activity.User.SendingFile") - case .peerActivityUserSendingPhoto: - return L10n.tr(key: "Peer.Activity.User.SendingPhoto") - case .peerActivityUserSendingVideo: - return L10n.tr(key: "Peer.Activity.User.SendingVideo") - case .peerActivityUserTypingText: - return L10n.tr(key: "Peer.Activity.User.TypingText") - case .peerMediaSharedFilesEmptyList: - return L10n.tr(key: "Peer.Media.SharedFilesEmptyList") - case .peerMediaSharedLinksEmptyList: - return L10n.tr(key: "Peer.Media.SharedLinksEmptyList") - case .peerMediaSharedMediaEmptyList: - return L10n.tr(key: "Peer.Media.SharedMediaEmptyList") - case .peerMediaSharedMusicEmptyList: - return L10n.tr(key: "Peer.Media.SharedMusicEmptyList") - case .peerStatusChannel: - return L10n.tr(key: "Peer.Status.channel") - case .peerStatusGroup: - return L10n.tr(key: "Peer.Status.group") - case .peerStatusJustNow: - return L10n.tr(key: "Peer.Status.justNow") - case .peerStatusLastMonth: - return L10n.tr(key: "Peer.Status.lastMonth") - case .peerStatusLastSeenAt(let p1, let p2): - return L10n.tr(key: "Peer.Status.LastSeenAt", p1, p2) - case .peerStatusLastWeek: - return L10n.tr(key: "Peer.Status.lastWeek") - case .peerStatusMemberCountable(let p1): - return L10n.tr(key: "Peer.Status.Member_countable", p1) - case .peerStatusMemberFew(let p1): - return L10n.tr(key: "Peer.Status.Member_few", p1) - case .peerStatusMemberMany(let p1): - return L10n.tr(key: "Peer.Status.Member_many", p1) - case .peerStatusMemberOne(let p1): - return L10n.tr(key: "Peer.Status.Member_one", p1) - case .peerStatusMemberOther(let p1): - return L10n.tr(key: "Peer.Status.Member_other", p1) - case .peerStatusMemberTwo(let p1): - return L10n.tr(key: "Peer.Status.Member_two", p1) - case .peerStatusMemberZero(let p1): - return L10n.tr(key: "Peer.Status.Member_zero", p1) - case .peerStatusMinAgoCountable(let p1): - return L10n.tr(key: "Peer.Status.minAgo_countable", p1) - case .peerStatusMinAgoFew(let p1): - return L10n.tr(key: "Peer.Status.minAgo_few", p1) - case .peerStatusMinAgoMany(let p1): - return L10n.tr(key: "Peer.Status.minAgo_many", p1) - case .peerStatusMinAgoOne(let p1): - return L10n.tr(key: "Peer.Status.minAgo_one", p1) - case .peerStatusMinAgoOther(let p1): - return L10n.tr(key: "Peer.Status.minAgo_other", p1) - case .peerStatusMinAgoTwo(let p1): - return L10n.tr(key: "Peer.Status.minAgo_two", p1) - case .peerStatusMinAgoZero(let p1): - return L10n.tr(key: "Peer.Status.minAgo_zero", p1) - case .peerStatusOnline: - return L10n.tr(key: "Peer.Status.online") - case .peerStatusRecently: - return L10n.tr(key: "Peer.Status.recently") - case .peerStatusToday: - return L10n.tr(key: "Peer.Status.Today") - case .peerStatusYesterday: - return L10n.tr(key: "Peer.Status.Yesterday") - case .peerStatusMemberOnlineCountable(let p1): - return L10n.tr(key: "Peer.Status.Member.Online_countable", p1) - case .peerStatusMemberOnlineFew(let p1): - return L10n.tr(key: "Peer.Status.Member.Online_few", p1) - case .peerStatusMemberOnlineMany(let p1): - return L10n.tr(key: "Peer.Status.Member.Online_many", p1) - case .peerStatusMemberOnlineOne(let p1): - return L10n.tr(key: "Peer.Status.Member.Online_one", p1) - case .peerStatusMemberOnlineOther(let p1): - return L10n.tr(key: "Peer.Status.Member.Online_other", p1) - case .peerStatusMemberOnlineTwo(let p1): - return L10n.tr(key: "Peer.Status.Member.Online_two", p1) - case .peerStatusMemberOnlineZero(let p1): - return L10n.tr(key: "Peer.Status.Member.Online_zero", p1) - case .peerInfoAbout: - return L10n.tr(key: "PeerInfo.about") - case .peerInfoAddContact: - return L10n.tr(key: "PeerInfo.AddContact") - case .peerInfoAddMember: - return L10n.tr(key: "PeerInfo.AddMember") - case .peerInfoAdminLabel: - return L10n.tr(key: "PeerInfo.AdminLabel") - case .peerInfoAdmins: - return L10n.tr(key: "PeerInfo.Admins") - case .peerInfoBio: - return L10n.tr(key: "PeerInfo.bio") - case .peerInfoBlackList: - return L10n.tr(key: "PeerInfo.BlackList") - case .peerInfoBlockUser: - return L10n.tr(key: "PeerInfo.BlockUser") - case .peerInfoChannelReported: - return L10n.tr(key: "PeerInfo.ChannelReported") - case .peerInfoChannelType: - return L10n.tr(key: "PeerInfo.ChannelType") - case .peerInfoConvertToSupergroup: - return L10n.tr(key: "PeerInfo.ConvertToSupergroup") - case .peerInfoDeleteAndExit: - return L10n.tr(key: "PeerInfo.DeleteAndExit") - case .peerInfoDeleteChannel: - return L10n.tr(key: "PeerInfo.DeleteChannel") - case .peerInfoDeleteContact: - return L10n.tr(key: "PeerInfo.DeleteContact") - case .peerInfoDeleteSecretChat: - return L10n.tr(key: "PeerInfo.DeleteSecretChat") - case .peerInfoEncryptionKey: - return L10n.tr(key: "PeerInfo.EncryptionKey") - case .peerInfoGroupsInCommon: - return L10n.tr(key: "PeerInfo.GroupsInCommon") - case .peerInfoGroupType: - return L10n.tr(key: "PeerInfo.GroupType") - case .peerInfoInfo: - return L10n.tr(key: "PeerInfo.info") - case .peerInfoInviteLink: - return L10n.tr(key: "PeerInfo.InviteLink") - case .peerInfoLeaveChannel: - return L10n.tr(key: "PeerInfo.LeaveChannel") - case .peerInfoMembers: - return L10n.tr(key: "PeerInfo.Members") - case .peerInfoMembersHeaderCountable(let p1): - return L10n.tr(key: "PeerInfo.MembersHeader_countable", p1) - case .peerInfoMembersHeaderFew(let p1): - return L10n.tr(key: "PeerInfo.MembersHeader_few", p1) - case .peerInfoMembersHeaderMany(let p1): - return L10n.tr(key: "PeerInfo.MembersHeader_many", p1) - case .peerInfoMembersHeaderOne(let p1): - return L10n.tr(key: "PeerInfo.MembersHeader_one", p1) - case .peerInfoMembersHeaderOther(let p1): - return L10n.tr(key: "PeerInfo.MembersHeader_other", p1) - case .peerInfoMembersHeaderTwo(let p1): - return L10n.tr(key: "PeerInfo.MembersHeader_two", p1) - case .peerInfoMembersHeaderZero(let p1): - return L10n.tr(key: "PeerInfo.MembersHeader_zero", p1) - case .peerInfoNotifications: - return L10n.tr(key: "PeerInfo.Notifications") - case .peerInfoPhone: - return L10n.tr(key: "PeerInfo.Phone") - case .peerInfoPreHistory: - return L10n.tr(key: "PeerInfo.PreHistory") - case .peerInfoReport: - return L10n.tr(key: "PeerInfo.Report") - case .peerInfoSendMessage: - return L10n.tr(key: "PeerInfo.SendMessage") - case .peerInfoSetAboutDescription: - return L10n.tr(key: "PeerInfo.SetAboutDescription") - case .peerInfoSetAdmins: - return L10n.tr(key: "PeerInfo.SetAdmins") - case .peerInfoSetChannelPhoto: - return L10n.tr(key: "PeerInfo.SetChannelPhoto") - case .peerInfoSetGroupPhoto: - return L10n.tr(key: "PeerInfo.SetGroupPhoto") - case .peerInfoSetGroupStickersSet: - return L10n.tr(key: "PeerInfo.SetGroupStickersSet") - case .peerInfoShareContact: - return L10n.tr(key: "PeerInfo.ShareContact") - case .peerInfoSharedMedia: - return L10n.tr(key: "PeerInfo.SharedMedia") - case .peerInfoSharelink: - return L10n.tr(key: "PeerInfo.sharelink") - case .peerInfoSignMessages: - return L10n.tr(key: "PeerInfo.SignMessages") - case .peerInfoStartSecretChat: - return L10n.tr(key: "PeerInfo.StartSecretChat") - case .peerInfoUnblockUser: - return L10n.tr(key: "PeerInfo.UnblockUser") - case .peerInfoUsername: - return L10n.tr(key: "PeerInfo.username") - case .peerInfoAboutPlaceholder: - return L10n.tr(key: "PeerInfo.About.Placeholder") - case .peerInfoBotStatusHasAccess: - return L10n.tr(key: "PeerInfo.BotStatus.HasAccess") - case .peerInfoBotStatusHasNoAccess: - return L10n.tr(key: "PeerInfo.BotStatus.HasNoAccess") - case .peerInfoChannelNamePlaceholder: - return L10n.tr(key: "PeerInfo.ChannelName.Placeholder") - case .peerInfoConfirmAddMember(let p1): - return L10n.tr(key: "PeerInfo.Confirm.AddMember", p1) - case .peerInfoConfirmAddMembers(let p1): - return L10n.tr(key: "PeerInfo.Confirm.AddMembers", p1) - case .peerInfoConfirmDeleteChat(let p1): - return L10n.tr(key: "PeerInfo.Confirm.DeleteChat", p1) - case .peerInfoConfirmDeleteContact: - return L10n.tr(key: "PeerInfo.Confirm.DeleteContact") - case .peerInfoConfirmLeaveChannel: - return L10n.tr(key: "PeerInfo.Confirm.LeaveChannel") - case .peerInfoConfirmLeaveGroup: - return L10n.tr(key: "PeerInfo.Confirm.LeaveGroup") - case .peerInfoConfirmRemovePeer(let p1): - return L10n.tr(key: "PeerInfo.Confirm.RemovePeer", p1) - case .peerInfoConfirmStartSecretChat(let p1): - return L10n.tr(key: "PeerInfo.Confirm.StartSecretChat", p1) - case .peerInfoFirstNamePlaceholder: - return L10n.tr(key: "PeerInfo.FirstName.Placeholder") - case .peerInfoGroupNamePlaceholder: - return L10n.tr(key: "PeerInfo.GroupName.Placeholder") - case .peerInfoGroupTypePrivate: - return L10n.tr(key: "PeerInfo.GroupType.Private") - case .peerInfoGroupTypePublic: - return L10n.tr(key: "PeerInfo.GroupType.Public") - case .peerInfoLastNamePlaceholder: - return L10n.tr(key: "PeerInfo.LastName.Placeholder") - case .peerInfoPreHistoryHidden: - return L10n.tr(key: "PeerInfo.PreHistory.Hidden") - case .peerInfoPreHistoryVisible: - return L10n.tr(key: "PeerInfo.PreHistory.Visible") - case .peerInfoSignMessagesDesc: - return L10n.tr(key: "PeerInfo.SignMessages.Desc") - case .peerMediaSharedMedia: - return L10n.tr(key: "PeerMedia.SharedMedia") - case .peerMediaPopoverSharedAudio: - return L10n.tr(key: "PeerMedia.Popover.SharedAudio") - case .peerMediaPopoverSharedFiles: - return L10n.tr(key: "PeerMedia.Popover.SharedFiles") - case .peerMediaPopoverSharedLinks: - return L10n.tr(key: "PeerMedia.Popover.SharedLinks") - case .peerMediaPopoverSharedMedia: - return L10n.tr(key: "PeerMedia.Popover.SharedMedia") - case .preHistorySettingsHeader: - return L10n.tr(key: "PreHistorySettings.Header") - case .preHistorySettingsDescriptionHidden: - return L10n.tr(key: "PreHistorySettings.Description.Hidden") - case .preHistorySettingsDescriptionVisible: - return L10n.tr(key: "PreHistorySettings.Description.Visible") - case .presenceBot: - return L10n.tr(key: "Presence.bot") - case .previderSenderCaptionPlaceholder: - return L10n.tr(key: "PreviderSender.CaptionPlaceholder") - case .previewSenderCompressFile: - return L10n.tr(key: "PreviewSender.CompressFile") - case .privacySettingsActiveSessions: - return L10n.tr(key: "PrivacySettings.ActiveSessions") - case .privacySettingsBlockedUsers: - return L10n.tr(key: "PrivacySettings.BlockedUsers") - case .privacySettingsGroups: - return L10n.tr(key: "PrivacySettings.Groups") - case .privacySettingsLastSeen: - return L10n.tr(key: "PrivacySettings.LastSeen") - case .privacySettingsPasscode: - return L10n.tr(key: "PrivacySettings.Passcode") - case .privacySettingsPrivacyHeader: - return L10n.tr(key: "PrivacySettings.PrivacyHeader") - case .privacySettingsProxyHeader: - return L10n.tr(key: "PrivacySettings.ProxyHeader") - case .privacySettingsSecurityHeader: - return L10n.tr(key: "PrivacySettings.SecurityHeader") - case .privacySettingsTwoStepVerification: - return L10n.tr(key: "PrivacySettings.TwoStepVerification") - case .privacySettingsUseProxy: - return L10n.tr(key: "PrivacySettings.UseProxy") - case .privacySettingsVoiceCalls: - return L10n.tr(key: "PrivacySettings.VoiceCalls") - case .privacySettingsPeerSelectAddNew: - return L10n.tr(key: "PrivacySettings.PeerSelect.AddNew") - case .privacySettingsControllerAddUsers: - return L10n.tr(key: "PrivacySettingsController.AddUsers") - case .privacySettingsControllerAlwaysAllow: - return L10n.tr(key: "PrivacySettingsController.AlwaysAllow") - case .privacySettingsControllerAlwaysShare: - return L10n.tr(key: "PrivacySettingsController.AlwaysShare") - case .privacySettingsControllerAlwaysShareWith: - return L10n.tr(key: "PrivacySettingsController.AlwaysShareWith") - case .privacySettingsControllerEverbody: - return L10n.tr(key: "PrivacySettingsController.Everbody") - case .privacySettingsControllerGroupDescription: - return L10n.tr(key: "PrivacySettingsController.GroupDescription") - case .privacySettingsControllerGroupHeader: - return L10n.tr(key: "PrivacySettingsController.GroupHeader") - case .privacySettingsControllerHeader: - return L10n.tr(key: "PrivacySettingsController.Header") - case .privacySettingsControllerLastSeenDescription: - return L10n.tr(key: "PrivacySettingsController.LastSeenDescription") - case .privacySettingsControllerLastSeenHeader: - return L10n.tr(key: "PrivacySettingsController.LastSeenHeader") - case .privacySettingsControllerMyContacts: - return L10n.tr(key: "PrivacySettingsController.MyContacts") - case .privacySettingsControllerNeverAllow: - return L10n.tr(key: "PrivacySettingsController.NeverAllow") - case .privacySettingsControllerNeverShare: - return L10n.tr(key: "PrivacySettingsController.NeverShare") - case .privacySettingsControllerNeverShareWith: - return L10n.tr(key: "PrivacySettingsController.NeverShareWith") - case .privacySettingsControllerNobody: - return L10n.tr(key: "PrivacySettingsController.Nobody") - case .privacySettingsControllerPeerInfo: - return L10n.tr(key: "PrivacySettingsController.PeerInfo") - case .privacySettingsControllerPhoneCallDescription: - return L10n.tr(key: "PrivacySettingsController.PhoneCallDescription") - case .privacySettingsControllerPhoneCallHeader: - return L10n.tr(key: "PrivacySettingsController.PhoneCallHeader") - case .privacySettingsControllerUserCountCountable(let p1): - return L10n.tr(key: "PrivacySettingsController.UserCount_countable", p1) - case .privacySettingsControllerUserCountFew(let p1): - return L10n.tr(key: "PrivacySettingsController.UserCount_few", p1) - case .privacySettingsControllerUserCountMany(let p1): - return L10n.tr(key: "PrivacySettingsController.UserCount_many", p1) - case .privacySettingsControllerUserCountOne(let p1): - return L10n.tr(key: "PrivacySettingsController.UserCount_one", p1) - case .privacySettingsControllerUserCountOther(let p1): - return L10n.tr(key: "PrivacySettingsController.UserCount_other", p1) - case .privacySettingsControllerUserCountTwo(let p1): - return L10n.tr(key: "PrivacySettingsController.UserCount_two", p1) - case .privacySettingsControllerUserCountZero(let p1): - return L10n.tr(key: "PrivacySettingsController.UserCount_zero", p1) - case .proxyForceDisable(let p1): - return L10n.tr(key: "Proxy.ForceDisable", p1) - case .proxyForceEnableHeader: - return L10n.tr(key: "Proxy.ForceEnable.Header") - case .proxyForceEnableText: - return L10n.tr(key: "Proxy.ForceEnable.Text") - case .proxyForceEnableTextIP(let p1): - return L10n.tr(key: "Proxy.ForceEnable.Text.IP", p1) - case .proxyForceEnableTextPassword(let p1): - return L10n.tr(key: "Proxy.ForceEnable.Text.Password", p1) - case .proxyForceEnableTextPort(let p1): - return L10n.tr(key: "Proxy.ForceEnable.Text.Port", p1) - case .proxyForceEnableTextUsername(let p1): - return L10n.tr(key: "Proxy.ForceEnable.Text.Username", p1) - case .proxySettingsConnectionHeader: - return L10n.tr(key: "ProxySettings.ConnectionHeader") - case .proxySettingsCredentialsHeader: - return L10n.tr(key: "ProxySettings.CredentialsHeader") - case .proxySettingsDisabled: - return L10n.tr(key: "ProxySettings.Disabled") - case .proxySettingsExportDescription: - return L10n.tr(key: "ProxySettings.ExportDescription") - case .proxySettingsExportLink: - return L10n.tr(key: "ProxySettings.ExportLink") - case .proxySettingsPassword: - return L10n.tr(key: "ProxySettings.Password") - case .proxySettingsPort: - return L10n.tr(key: "ProxySettings.Port") - case .proxySettingsProxyNotFound: - return L10n.tr(key: "ProxySettings.ProxyNotFound") - case .proxySettingsSave: - return L10n.tr(key: "ProxySettings.Save") - case .proxySettingsServer: - return L10n.tr(key: "ProxySettings.Server") - case .proxySettingsShare: - return L10n.tr(key: "ProxySettings.Share") - case .proxySettingsSocks5: - return L10n.tr(key: "ProxySettings.Socks5") - case .proxySettingsUsername: - return L10n.tr(key: "ProxySettings.Username") - case .quickLookPreview: - return L10n.tr(key: "QuickLook.Preview") - case .quickSwitcherDescription: - return L10n.tr(key: "QuickSwitcher.Description") - case .quickSwitcherPopular: - return L10n.tr(key: "QuickSwitcher.Popular") - case .quickSwitcherRecently: - return L10n.tr(key: "QuickSwitcher.Recently") - case .qvCM9Y7gTitle: - return L10n.tr(key: "QvC-M9-y7g.title") - case .r4oN2Eq4Title: - return L10n.tr(key: "R4o-n2-Eq4.title") - case .rbDRhWINTitle: - return L10n.tr(key: "rbD-Rh-wIN.title") - case .recentCallsEmpty: - return L10n.tr(key: "RecentCalls.Empty") - case .recentSessionsRevoke: - return L10n.tr(key: "RecentSessions.Revoke") - case .recentSessionsConfirmRevoke: - return L10n.tr(key: "RecentSessions.Confirm.Revoke") - case .recentSessionsConfirmTerminateOthers: - return L10n.tr(key: "RecentSessions.Confirm.TerminateOthers") - case .reportReasonPorno: - return L10n.tr(key: "ReportReason.Porno") - case .reportReasonSpam: - return L10n.tr(key: "ReportReason.Spam") - case .reportReasonViolence: - return L10n.tr(key: "ReportReason.Violence") - case .rgMF4YcnTitle: - return L10n.tr(key: "rgM-f4-ycn.title") - case .ruw6mB2mTitle: - return L10n.tr(key: "Ruw-6m-B2m.title") - case .searchSeparatorChatsAndContacts: - return L10n.tr(key: "Search.Separator.ChatsAndContacts") - case .searchSeparatorGlobalPeers: - return L10n.tr(key: "Search.Separator.GlobalPeers") - case .searchSeparatorMessages: - return L10n.tr(key: "Search.Separator.Messages") - case .searchSeparatorPopular: - return L10n.tr(key: "Search.Separator.Popular") - case .searchSeparatorRecent: - return L10n.tr(key: "Search.Separator.Recent") - case .searchFieldSearch: - return L10n.tr(key: "SearchField.Search") - case .secretTimerOff: - return L10n.tr(key: "SecretTimer.Off") - case .separatorClear: - return L10n.tr(key: "Separator.Clear") - case .separatorShowLess: - return L10n.tr(key: "Separator.ShowLess") - case .separatorShowMore: - return L10n.tr(key: "Separator.ShowMore") - case .serviceMessageDesturctingPhoto(let p1): - return L10n.tr(key: "ServiceMessage.DesturctingPhoto", p1) - case .serviceMessageDesturctingVideo(let p1): - return L10n.tr(key: "ServiceMessage.DesturctingVideo", p1) - case .serviceMessageExpiredFile: - return L10n.tr(key: "ServiceMessage.ExpiredFile") - case .serviceMessageExpiredPhoto: - return L10n.tr(key: "ServiceMessage.ExpiredPhoto") - case .serviceMessageExpiredVideo: - return L10n.tr(key: "ServiceMessage.ExpiredVideo") - case .serviceMessageDesturctingPhotoYou(let p1): - return L10n.tr(key: "ServiceMessage.DesturctingPhoto.You", p1) - case .serviceMessageDesturctingVideoYou(let p1): - return L10n.tr(key: "ServiceMessage.DesturctingVideo.You", p1) - case .sessionsActiveSessionsHeader: - return L10n.tr(key: "Sessions.ActiveSessionsHeader") - case .sessionsCurrentSessionHeader: - return L10n.tr(key: "Sessions.CurrentSessionHeader") - case .sessionsTerminateDescription: - return L10n.tr(key: "Sessions.TerminateDescription") - case .sessionsTerminateOthers: - return L10n.tr(key: "Sessions.TerminateOthers") - case .shareLinkCopied: - return L10n.tr(key: "Share.Link.Copied") - case .shareExtensionCancel: - return L10n.tr(key: "ShareExtension.Cancel") - case .shareExtensionSearch: - return L10n.tr(key: "ShareExtension.Search") - case .shareExtensionShare: - return L10n.tr(key: "ShareExtension.Share") - case .shareExtensionPasscodeNext: - return L10n.tr(key: "ShareExtension.Passcode.Next") - case .shareExtensionPasscodePlaceholder: - return L10n.tr(key: "ShareExtension.Passcode.Placeholder") - case .shareExtensionUnauthorizedDescription: - return L10n.tr(key: "ShareExtension.Unauthorized.Description") - case .shareExtensionUnauthorizedOK: - return L10n.tr(key: "ShareExtension.Unauthorized.OK") - case .shareModalSearchPlaceholder: - return L10n.tr(key: "ShareModal.Search.Placeholder") - case .sidebarAvalability: - return L10n.tr(key: "Sidebar.Avalability") - case .stickerPackAdd(let p1): - return L10n.tr(key: "StickerPack.Add", p1) - case .stickerPackRemove(let p1): - return L10n.tr(key: "StickerPack.Remove", p1) - case .stickersGroupStickers: - return L10n.tr(key: "Stickers.GroupStickers") - case .stickersRecent: - return L10n.tr(key: "Stickers.Recent") - case .stickersSetCount(let p1): - return L10n.tr(key: "Stickers.Set.Count", p1) - case .stickerSetRemove: - return L10n.tr(key: "StickerSet.Remove") - case .storageClear(let p1): - return L10n.tr(key: "Storage.Clear", p1) - case .storageClearAudio: - return L10n.tr(key: "Storage.Clear.Audio") - case .storageClearDocuments: - return L10n.tr(key: "Storage.Clear.Documents") - case .storageClearPhotos: - return L10n.tr(key: "Storage.Clear.Photos") - case .storageClearVideos: - return L10n.tr(key: "Storage.Clear.Videos") - case .storageUsageCalculating: - return L10n.tr(key: "StorageUsage.Calculating") - case .storageUsageChatsHeader: - return L10n.tr(key: "StorageUsage.ChatsHeader") - case .storageUsageKeepMedia: - return L10n.tr(key: "StorageUsage.KeepMedia") - case .storageUsageKeepMediaDescription: - return L10n.tr(key: "StorageUsage.KeepMedia.Description") - case .suggestLocalizationHeader: - return L10n.tr(key: "Suggest.Localization.Header") - case .suggestLocalizationOther: - return L10n.tr(key: "Suggest.Localization.Other") - case .supergroupConvertButton: - return L10n.tr(key: "Supergroup.Convert.Button") - case .supergroupConvertDescription: - return L10n.tr(key: "Supergroup.Convert.Description") - case .supergroupConvertUndone: - return L10n.tr(key: "Supergroup.Convert.Undone") - case .supergroupDeleteRestrictionBanUser: - return L10n.tr(key: "Supergroup.DeleteRestriction.BanUser") - case .supergroupDeleteRestrictionDeleteAllMessages: - return L10n.tr(key: "Supergroup.DeleteRestriction.DeleteAllMessages") - case .supergroupDeleteRestrictionDeleteMessage: - return L10n.tr(key: "Supergroup.DeleteRestriction.DeleteMessage") - case .supergroupDeleteRestrictionReportSpam: - return L10n.tr(key: "Supergroup.DeleteRestriction.ReportSpam") - case .sZhCtGQSTitle: - return L10n.tr(key: "sZh-ct-GQS.title") - case .td7AD5loTitle: - return L10n.tr(key: "Td7-aD-5lo.title") - case .telegramAppearanceViewController: - return L10n.tr(key: "Telegram.AppearanceViewController") - case .telegramArchivedStickerPacksController: - return L10n.tr(key: "Telegram.ArchivedStickerPacksController") - case .telegramBioViewController: - return L10n.tr(key: "Telegram.BioViewController") - case .telegramBlockedPeersViewController: - return L10n.tr(key: "Telegram.BlockedPeersViewController") - case .telegramChannelAdminsViewController: - return L10n.tr(key: "Telegram.ChannelAdminsViewController") - case .telegramChannelBlacklistViewController: - return L10n.tr(key: "Telegram.ChannelBlacklistViewController") - case .telegramChannelEventLogController: - return L10n.tr(key: "Telegram.ChannelEventLogController") - case .telegramChannelIntroViewController: - return L10n.tr(key: "Telegram.ChannelIntroViewController") - case .telegramChannelMembersViewController: - return L10n.tr(key: "Telegram.ChannelMembersViewController") - case .telegramChannelVisibilityController: - return L10n.tr(key: "Telegram.ChannelVisibilityController") - case .telegramConvertGroupViewController: - return L10n.tr(key: "Telegram.ConvertGroupViewController") - case .telegramEmptyChatViewController: - return L10n.tr(key: "Telegram.EmptyChatViewController") - case .telegramFeaturedStickerPacksController: - return L10n.tr(key: "Telegram.FeaturedStickerPacksController") - case .telegramGeneralSettingsViewController: - return L10n.tr(key: "Telegram.GeneralSettingsViewController") - case .telegramGroupAdminsController: - return L10n.tr(key: "Telegram.GroupAdminsController") - case .telegramGroupsInCommonViewController: - return L10n.tr(key: "Telegram.GroupsInCommonViewController") - case .telegramGroupStickerSetController: - return L10n.tr(key: "Telegram.GroupStickerSetController") - case .telegramInstalledStickerPacksController: - return L10n.tr(key: "Telegram.InstalledStickerPacksController") - case .telegramLanguageViewController: - return L10n.tr(key: "Telegram.LanguageViewController") - case .telegramLayoutAccountController: - return L10n.tr(key: "Telegram.LayoutAccountController") - case .telegramLayoutRecentCallsViewController: - return L10n.tr(key: "Telegram.LayoutRecentCallsViewController") - case .telegramLinkInvationController: - return L10n.tr(key: "Telegram.LinkInvationController") - case .telegramMainViewController: - return L10n.tr(key: "Telegram.MainViewController") - case .telegramNotificationSettingsViewController: - return L10n.tr(key: "Telegram.NotificationSettingsViewController") - case .telegramPasscodeSettingsViewController: - return L10n.tr(key: "Telegram.PasscodeSettingsViewController") - case .telegramPeerInfoController: - return L10n.tr(key: "Telegram.PeerInfoController") - case .telegramPhoneNumberConfirmController: - return L10n.tr(key: "Telegram.PhoneNumberConfirmController") - case .telegramPhoneNumberIntroController: - return L10n.tr(key: "Telegram.PhoneNumberIntroController") - case .telegramPreHistorySettingsController: - return L10n.tr(key: "Telegram.PreHistorySettingsController") - case .telegramPrivacyAndSecurityViewController: - return L10n.tr(key: "Telegram.PrivacyAndSecurityViewController") - case .telegramProxySettingsViewController: - return L10n.tr(key: "Telegram.ProxySettingsViewController") - case .telegramRecentSessionsController: - return L10n.tr(key: "Telegram.RecentSessionsController") - case .telegramSecretChatKeyViewController: - return L10n.tr(key: "Telegram.SecretChatKeyViewController") - case .telegramSelectPeersController: - return L10n.tr(key: "Telegram.SelectPeersController") - case .telegramStorageUsageController: - return L10n.tr(key: "Telegram.StorageUsageController") - case .telegramTwoStepVerificationUnlockController: - return L10n.tr(key: "Telegram.TwoStepVerificationUnlockController") - case .telegramUsernameSettingsViewController: - return L10n.tr(key: "Telegram.UsernameSettingsViewController") - case .textCopy: - return L10n.tr(key: "Text.Copy") - case .textViewTransformBold: - return L10n.tr(key: "TextView.Transform.Bold") - case .textViewTransformCode: - return L10n.tr(key: "TextView.Transform.Code") - case .textViewTransformItalic: - return L10n.tr(key: "TextView.Transform.Italic") - case .timeAt: - return L10n.tr(key: "Time.at") - case .timeLastSeen: - return L10n.tr(key: "Time.last_seen") - case .timeToday: - return L10n.tr(key: "Time.today") - case .timeYesterday: - return L10n.tr(key: "Time.yesterday") - case .timerDaysCountable(let p1): - return L10n.tr(key: "Timer.Days_countable", p1) - case .timerDaysFew(let p1): - return L10n.tr(key: "Timer.Days_few", p1) - case .timerDaysMany(let p1): - return L10n.tr(key: "Timer.Days_many", p1) - case .timerDaysOne(let p1): - return L10n.tr(key: "Timer.Days_one", p1) - case .timerDaysOther(let p1): - return L10n.tr(key: "Timer.Days_other", p1) - case .timerDaysTwo(let p1): - return L10n.tr(key: "Timer.Days_two", p1) - case .timerDaysZero(let p1): - return L10n.tr(key: "Timer.Days_zero", p1) - case .timerForever: - return L10n.tr(key: "Timer.Forever") - case .timerHoursCountable(let p1): - return L10n.tr(key: "Timer.Hours_countable", p1) - case .timerHoursFew(let p1): - return L10n.tr(key: "Timer.Hours_few", p1) - case .timerHoursMany(let p1): - return L10n.tr(key: "Timer.Hours_many", p1) - case .timerHoursOne(let p1): - return L10n.tr(key: "Timer.Hours_one", p1) - case .timerHoursOther(let p1): - return L10n.tr(key: "Timer.Hours_other", p1) - case .timerHoursTwo(let p1): - return L10n.tr(key: "Timer.Hours_two", p1) - case .timerHoursZero(let p1): - return L10n.tr(key: "Timer.Hours_zero", p1) - case .timerMinutesCountable(let p1): - return L10n.tr(key: "Timer.Minutes_countable", p1) - case .timerMinutesFew(let p1): - return L10n.tr(key: "Timer.Minutes_few", p1) - case .timerMinutesMany(let p1): - return L10n.tr(key: "Timer.Minutes_many", p1) - case .timerMinutesOne(let p1): - return L10n.tr(key: "Timer.Minutes_one", p1) - case .timerMinutesOther(let p1): - return L10n.tr(key: "Timer.Minutes_other", p1) - case .timerMinutesTwo(let p1): - return L10n.tr(key: "Timer.Minutes_two", p1) - case .timerMinutesZero(let p1): - return L10n.tr(key: "Timer.Minutes_zero", p1) - case .timerMonthsCountable(let p1): - return L10n.tr(key: "Timer.Months_countable", p1) - case .timerMonthsFew(let p1): - return L10n.tr(key: "Timer.Months_few", p1) - case .timerMonthsMany(let p1): - return L10n.tr(key: "Timer.Months_many", p1) - case .timerMonthsOne(let p1): - return L10n.tr(key: "Timer.Months_one", p1) - case .timerMonthsOther(let p1): - return L10n.tr(key: "Timer.Months_other", p1) - case .timerMonthsTwo(let p1): - return L10n.tr(key: "Timer.Months_two", p1) - case .timerMonthsZero(let p1): - return L10n.tr(key: "Timer.Months_zero", p1) - case .timerSecondsCountable(let p1): - return L10n.tr(key: "Timer.Seconds_countable", p1) - case .timerSecondsFew(let p1): - return L10n.tr(key: "Timer.Seconds_few", p1) - case .timerSecondsMany(let p1): - return L10n.tr(key: "Timer.Seconds_many", p1) - case .timerSecondsOne(let p1): - return L10n.tr(key: "Timer.Seconds_one", p1) - case .timerSecondsOther(let p1): - return L10n.tr(key: "Timer.Seconds_other", p1) - case .timerSecondsTwo(let p1): - return L10n.tr(key: "Timer.Seconds_two", p1) - case .timerSecondsZero(let p1): - return L10n.tr(key: "Timer.Seconds_zero", p1) - case .timerWeeksCountable(let p1): - return L10n.tr(key: "Timer.Weeks_countable", p1) - case .timerWeeksFew(let p1): - return L10n.tr(key: "Timer.Weeks_few", p1) - case .timerWeeksMany(let p1): - return L10n.tr(key: "Timer.Weeks_many", p1) - case .timerWeeksOne(let p1): - return L10n.tr(key: "Timer.Weeks_one", p1) - case .timerWeeksOther(let p1): - return L10n.tr(key: "Timer.Weeks_other", p1) - case .timerWeeksTwo(let p1): - return L10n.tr(key: "Timer.Weeks_two", p1) - case .timerWeeksZero(let p1): - return L10n.tr(key: "Timer.Weeks_zero", p1) - case .timerYearsCountable(let p1): - return L10n.tr(key: "Timer.Years_countable", p1) - case .timerYearsFew(let p1): - return L10n.tr(key: "Timer.Years_few", p1) - case .timerYearsMany(let p1): - return L10n.tr(key: "Timer.Years_many", p1) - case .timerYearsOne(let p1): - return L10n.tr(key: "Timer.Years_one", p1) - case .timerYearsOther(let p1): - return L10n.tr(key: "Timer.Years_other", p1) - case .timerYearsTwo(let p1): - return L10n.tr(key: "Timer.Years_two", p1) - case .timerYearsZero(let p1): - return L10n.tr(key: "Timer.Years_zero", p1) - case .tRrPd1PSTitle: - return L10n.tr(key: "tRr-pd-1PS.title") - case .twoStepAuthEmailSkip: - return L10n.tr(key: "TwoStep.AuthEmailSkip") - case .twoStepAuthAnError: - return L10n.tr(key: "TwoStepAuth.AnError") - case .twoStepAuthChangeEmail: - return L10n.tr(key: "TwoStepAuth.ChangeEmail") - case .twoStepAuthChangePassword: - return L10n.tr(key: "TwoStepAuth.ChangePassword") - case .twoStepAuthConfirmationAbort: - return L10n.tr(key: "TwoStepAuth.ConfirmationAbort") - case .twoStepAuthConfirmationText: - return L10n.tr(key: "TwoStepAuth.ConfirmationText") - case .twoStepAuthEmail: - return L10n.tr(key: "TwoStepAuth.Email") - case .twoStepAuthEmailHelp: - return L10n.tr(key: "TwoStepAuth.EmailHelp") - case .twoStepAuthEmailInvalid: - return L10n.tr(key: "TwoStepAuth.EmailInvalid") - case .twoStepAuthEmailSent: - return L10n.tr(key: "TwoStepAuth.EmailSent") - case .twoStepAuthEmailSkipAlert: - return L10n.tr(key: "TwoStepAuth.EmailSkipAlert") - case .twoStepAuthEnterPasswordForgot: - return L10n.tr(key: "TwoStepAuth.EnterPasswordForgot") - case .twoStepAuthEnterPasswordHelp: - return L10n.tr(key: "TwoStepAuth.EnterPasswordHelp") - case .twoStepAuthEnterPasswordHint(let p1): - return L10n.tr(key: "TwoStepAuth.EnterPasswordHint", p1) - case .twoStepAuthEnterPasswordPassword: - return L10n.tr(key: "TwoStepAuth.EnterPasswordPassword") - case .twoStepAuthFloodError: - return L10n.tr(key: "TwoStepAuth.FloodError") - case .twoStepAuthGenericError: - return L10n.tr(key: "TwoStepAuth.GenericError") - case .twoStepAuthGenericHelp: - return L10n.tr(key: "TwoStepAuth.GenericHelp") - case .twoStepAuthPasswordTitle: - return L10n.tr(key: "TwoStepAuth.PasswordTitle") - case .twoStepAuthPendingEmailHelp(let p1): - return L10n.tr(key: "TwoStepAuth.PendingEmailHelp", p1) - case .twoStepAuthRecoveryCode: - return L10n.tr(key: "TwoStepAuth.RecoveryCode") - case .twoStepAuthRecoveryCodeExpired: - return L10n.tr(key: "TwoStepAuth.RecoveryCodeExpired") - case .twoStepAuthRecoveryCodeHelp: - return L10n.tr(key: "TwoStepAuth.RecoveryCodeHelp") - case .twoStepAuthRecoveryCodeInvalid: - return L10n.tr(key: "TwoStepAuth.RecoveryCodeInvalid") - case .twoStepAuthRecoveryEmailUnavailable(let p1): - return L10n.tr(key: "TwoStepAuth.RecoveryEmailUnavailable", p1) - case .twoStepAuthRecoveryFailed: - return L10n.tr(key: "TwoStepAuth.RecoveryFailed") - case .twoStepAuthRecoverySent(let p1): - return L10n.tr(key: "TwoStepAuth.RecoverySent", p1) - case .twoStepAuthRecoveryTitle: - return L10n.tr(key: "TwoStepAuth.RecoveryTitle") - case .twoStepAuthRecoveryUnavailable: - return L10n.tr(key: "TwoStepAuth.RecoveryUnavailable") - case .twoStepAuthRemovePassword: - return L10n.tr(key: "TwoStepAuth.RemovePassword") - case .twoStepAuthSetPassword: - return L10n.tr(key: "TwoStepAuth.SetPassword") - case .twoStepAuthSetPasswordHelp: - return L10n.tr(key: "TwoStepAuth.SetPasswordHelp") - case .twoStepAuthSetupEmail: - return L10n.tr(key: "TwoStepAuth.SetupEmail") - case .twoStepAuthSetupEmailTitle: - return L10n.tr(key: "TwoStepAuth.SetupEmailTitle") - case .twoStepAuthSetupHint: - return L10n.tr(key: "TwoStepAuth.SetupHint") - case .twoStepAuthSetupHintTitle: - return L10n.tr(key: "TwoStepAuth.SetupHintTitle") - case .twoStepAuthSetupPasswordConfirmFailed: - return L10n.tr(key: "TwoStepAuth.SetupPasswordConfirmFailed") - case .twoStepAuthSetupPasswordConfirmPassword: - return L10n.tr(key: "TwoStepAuth.SetupPasswordConfirmPassword") - case .twoStepAuthSetupPasswordEnterPassword: - return L10n.tr(key: "TwoStepAuth.SetupPasswordEnterPassword") - case .twoStepAuthSetupPasswordEnterPasswordNew: - return L10n.tr(key: "TwoStepAuth.SetupPasswordEnterPasswordNew") - case .twoStepAuthSetupPasswordTitle: - return L10n.tr(key: "TwoStepAuth.SetupPasswordTitle") - case .twoStepAuthConfirmDisablePassword: - return L10n.tr(key: "TwoStepAuth.Confirm.DisablePassword") - case .twoStepAuthErrorGeneric: - return L10n.tr(key: "TwoStepAuth.Error.Generic") - case .twoStepAuthErrorHaventEmail: - return L10n.tr(key: "TwoStepAuth.Error.HaventEmail") - case .twoStepAuthErrorInvalidEmail: - return L10n.tr(key: "TwoStepAuth.Error.InvalidEmail") - case .twoStepAuthErrorLimitExceeded: - return L10n.tr(key: "TwoStepAuth.Error.LimitExceeded") - case .twoStepAuthErrorPasswordsDontMatch: - return L10n.tr(key: "TwoStepAuth.Error.PasswordsDontMatch") - case .uezBsLqGTitle: - return L10n.tr(key: "UEZ-Bs-lqG.title") - case .uQyDDJDrTitle: - return L10n.tr(key: "uQy-DD-JDr.title") - case .uRlIYUnGTitle: - return L10n.tr(key: "uRl-iY-unG.title") - case .usernameSettingsAvailable(let p1): - return L10n.tr(key: "UsernameSettings.available", p1) - case .usernameSettingsChangeDescription: - return L10n.tr(key: "UsernameSettings.ChangeDescription") - case .usernameSettingsDone: - return L10n.tr(key: "UsernameSettings.Done") - case .usernameSettingsInputPlaceholder: - return L10n.tr(key: "UsernameSettings.InputPlaceholder") - case .vdrFpXzOTitle: - return L10n.tr(key: "Vdr-fp-XzO.title") - case .vmV6d7jITitle: - return L10n.tr(key: "vmV-6d-7jI.title") - case .w486f4DlTitle: - return L10n.tr(key: "W48-6f-4Dl.title") - case .weekdayShortFriday: - return L10n.tr(key: "Weekday.ShortFriday") - case .weekdayShortMonday: - return L10n.tr(key: "Weekday.ShortMonday") - case .weekdayShortSaturday: - return L10n.tr(key: "Weekday.ShortSaturday") - case .weekdayShortSunday: - return L10n.tr(key: "Weekday.ShortSunday") - case .weekdayShortThursday: - return L10n.tr(key: "Weekday.ShortThursday") - case .weekdayShortTuesday: - return L10n.tr(key: "Weekday.ShortTuesday") - case .weekdayShortWednesday: - return L10n.tr(key: "Weekday.ShortWednesday") - case .weT3VZwkTitle: - return L10n.tr(key: "WeT-3V-zwk.title") - case .x3vGGIWUTitle: - return L10n.tr(key: "x3v-GG-iWU.title") - case .z6FFW3nzTitle: - return L10n.tr(key: "z6F-FW-3nz.title") - case ._NS138Title: - return L10n.tr(key: "_NS:138.title") - case ._NS70Title: - return L10n.tr(key: "_NS:70.title") - case ._NS88Title: - return L10n.tr(key: "_NS:88.title") - } + internal static var ns88Title: String { return L10n.tr("Localizable", "_NS88.title") } + /// Edit + internal static var ns104Title: String { return L10n.tr("Localizable", "_NS:104.title") } + /// Window + internal static var ns163Title: String { return L10n.tr("Localizable", "_NS:163.title") } + /// Window + internal static var ns168Title: String { return L10n.tr("Localizable", "_NS:168.title") } + /// View + internal static var ns77Title: String { return L10n.tr("Localizable", "_NS:77.title") } + /// View + internal static var ns82Title: String { return L10n.tr("Localizable", "_NS:82.title") } + /// Edit + internal static var ns99Title: String { return L10n.tr("Localizable", "_NS:99.title") } + /// Global Search + internal static var aMaRbKjVTitle: String { return L10n.tr("Localizable", "aMa-rb-kjV.title") } + /// Window + internal static var aufd15bRTitle: String { return L10n.tr("Localizable", "aUF-d1-5bR.title") } + /// Transformations + internal static var c8aY6VQdTitle: String { return L10n.tr("Localizable", "c8a-y6-VQd.title") } + /// Smart Links + internal static var cwLP1JidTitle: String { return L10n.tr("Localizable", "cwL-P1-jid.title") } + /// Make Lower Case + internal static var d9MCDAMdTitle: String { return L10n.tr("Localizable", "d9M-CD-aMd.title") } + /// Undo + internal static var drj4nYzgTitle: String { return L10n.tr("Localizable", "dRJ-4n-Yzg.title") } + /// Paste + internal static var gvau4SdLTitle: String { return L10n.tr("Localizable", "gVA-U4-sdL.title") } + /// Smart Quotes + internal static var hQb2vFYvTitle: String { return L10n.tr("Localizable", "hQb-2v-fYv.title") } + /// Check Document Now + internal static var hz2CUCR7Title: String { return L10n.tr("Localizable", "hz2-CU-CR7.title") } + /// Check Grammar With Spelling + internal static var mk62p4JGTitle: String { return L10n.tr("Localizable", "mK6-2p-4JG.title") } + /// Delete + internal static var pa3QIU2kTitle: String { return L10n.tr("Localizable", "pa3-QI-u2k.title") } + /// Leave + internal static var peerInfoConfirmLeave: String { return L10n.tr("Localizable", "peerInfo.Confirm.Leave") } + /// This will delete all messages and media in this chat from your Telegram cloud. Other members of the group will still have them. + internal static var peerInfoConfirmClearHistoryGroup: String { return L10n.tr("Localizable", "peerInfo.Confirm.ClearHistory.Group") } + /// This will delete all messages and media in this chat from your Telegram cloud. + internal static var peerInfoConfirmClearHistorySavedMesssages: String { return L10n.tr("Localizable", "peerInfo.Confirm.ClearHistory.SavedMesssages") } + /// This will delete all messages and media in this chat from your Telegram cloud. Your chat partner will still have them. + internal static var peerInfoConfirmClearHistoryUser: String { return L10n.tr("Localizable", "peerInfo.Confirm.ClearHistory.User") } + /// Are you sure you want to delete all messages in the chat? + internal static var peerInfoConfirmClearHistoryUserBothSides: String { return L10n.tr("Localizable", "peerInfo.Confirm.ClearHistory.UserBothSides") } + /// PSA + internal static var psaChatlist: String { return L10n.tr("Localizable", "psa.chatlist") } + /// This message provides you with a public service announcement in your chat list + internal static var psaText: String { return L10n.tr("Localizable", "psa.text") } + /// PSA Notification + internal static var psaTitle: String { return L10n.tr("Localizable", "psa.title") } + /// Public Service Announcement + internal static var psaChatTitle: String { return L10n.tr("Localizable", "psa.chat.title") } + /// This message provides you with a public service announcement. + internal static var psaChatTextCovid: String { return L10n.tr("Localizable", "psa.chat.text.covid") } + /// PSA Notification\nfrom: [%@]() + internal static func psaTitleBubbles(_ p1: String) -> String { + return L10n.tr("Localizable", "psa.title.bubbles", p1) } - - private static func tr(key: String, _ args: CVarArg...) -> String { - return translate(key: key, args) - } + /// Check Spelling While Typing + internal static var rbDRhWINTitle: String { return L10n.tr("Localizable", "rbD-Rh-wIN.title") } + /// Smart Dashes + internal static var rgMF4YcnTitle: String { return L10n.tr("Localizable", "rgM-f4-ycn.title") } + /// Quick Search + internal static var sZhCtGQSTitle: String { return L10n.tr("Localizable", "sZh-ct-GQS.title") } + /// Data Detectors + internal static var tRrPd1PSTitle: String { return L10n.tr("Localizable", "tRr-pd-1PS.title") } + /// Telegram + internal static var uQyDDJDrTitle: String { return L10n.tr("Localizable", "uQy-DD-JDr.title") } + /// Cut + internal static var uRlIYUnGTitle: String { return L10n.tr("Localizable", "uRl-iY-unG.title") } + /// Make Upper Case + internal static var vmV6d7jITitle: String { return L10n.tr("Localizable", "vmV-6d-7jI.title") } + /// Copy + internal static var x3vGGIWUTitle: String { return L10n.tr("Localizable", "x3v-GG-iWU.title") } + /// Show Substitutions + internal static var z6FFW3nzTitle: String { return L10n.tr("Localizable", "z6F-FW-3nz.title") } } +// swiftlint:enable identifier_name line_length type_body_length -func tr(_ key: L10n) -> String { - return key.string +extension L10n { + private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String { + return translate(key: key, args) + } } private final class BundleToken {} diff --git a/Telegram-Mac/LocalizableExtension.swift b/Telegram-Mac/LocalizableExtension.swift index dc554a2ed2..afd41b7680 100644 --- a/Telegram-Mac/LocalizableExtension.swift +++ b/Telegram-Mac/LocalizableExtension.swift @@ -5,8 +5,9 @@ // Created by keepcoder on 25/05/2017. // Copyright © 2017 Telegram. All rights reserved. // -import SwiftSignalKitMac -import TelegramCoreMac +import SwiftSignalKit +import TelegramCore + import TGUIKit #if !APP_STORE @@ -36,6 +37,10 @@ extension Double { } + func tr(_ string: String) -> String { + return string +} + private func dictFromLocalization(_ value: Localization) -> [String: String] { var dict: [String: String] = [:] for entry in value.entries { @@ -65,31 +70,36 @@ private func dictFromLocalization(_ value: Localization) -> [String: String] { } func applyUILocalization(_ settings: LocalizationSettings) { - let language = Language(languageCode: settings.languageCode, strings: dictFromLocalization(settings.localization)) - #if !APP_STORE - // SULocalizationWrapper.setLanguageCode(settings.languageCode) + #if !SHARE + UNUserNotifications.current?.registerCategories() #endif + + let primaryLanguage = Language(languageCode: settings.primaryComponent.languageCode, customPluralizationCode: settings.primaryComponent.customPluralizationCode, strings: dictFromLocalization(settings.primaryComponent.localization)) + let secondaryLanguage = settings.secondaryComponent != nil ? Language.init(languageCode: settings.secondaryComponent!.languageCode, customPluralizationCode: settings.secondaryComponent!.customPluralizationCode, strings: dictFromLocalization(settings.secondaryComponent!.localization)) : nil + + let language = TelegramLocalization(primaryLanguage: primaryLanguage, secondaryLanguage: secondaryLanguage, localizedName: settings.primaryComponent.localizedName) _ = _appCurrentLanguage.swap(language) languagePromise.set(.single(language)) applyMainMenuLocalization(mainWindow) } func applyShareUILocalization(_ settings: LocalizationSettings) { - let language = Language(languageCode: settings.languageCode, strings: dictFromLocalization(settings.localization)) + let primaryLanguage = Language(languageCode: settings.primaryComponent.languageCode, customPluralizationCode: settings.primaryComponent.customPluralizationCode, strings: dictFromLocalization(settings.primaryComponent.localization)) + let secondaryLanguage = settings.secondaryComponent != nil ? Language.init(languageCode: settings.secondaryComponent!.languageCode, customPluralizationCode: settings.secondaryComponent!.customPluralizationCode, strings: dictFromLocalization(settings.secondaryComponent!.localization)) : nil + + let language = TelegramLocalization(primaryLanguage: primaryLanguage, secondaryLanguage: secondaryLanguage, localizedName: settings.primaryComponent.localizedName) _ = _appCurrentLanguage.swap(language) languagePromise.set(.single(language)) } func dropShareLocalization() { - let language = Language(languageCode: "en", strings: [:]) + let language = TelegramLocalization(primaryLanguage: Language(languageCode: "en", customPluralizationCode: nil, strings: [:]), secondaryLanguage: nil, localizedName: "English") _ = _appCurrentLanguage.swap(language) languagePromise.set(.single(language)) } func dropLocalization() { - #if !APP_STORE - // SULocalizationWrapper.setLanguageCode("en") - #endif - let language = Language(languageCode: "en", strings: [:]) + + let language = TelegramLocalization(primaryLanguage: Language(languageCode: "en", customPluralizationCode: nil, strings: [:]), secondaryLanguage: nil, localizedName: "English") _ = _appCurrentLanguage.swap(language) languagePromise.set(.single(language)) applyMainMenuLocalization(mainWindow) @@ -117,9 +127,11 @@ private func localizeMainMenuItem(_ item:NSMenuItem) { class Language : Equatable { let languageCode:String + let customPluralizationCode: String? let strings:[String: String] - init (languageCode:String, strings:[String: String]) { + init (languageCode:String, customPluralizationCode: String?, strings:[String: String]) { self.languageCode = languageCode + self.customPluralizationCode = customPluralizationCode self.strings = strings } } @@ -129,84 +141,130 @@ func ==(lhs:Language, rhs:Language) -> Bool { } func translate(key: String, _ args: [CVarArg]) -> String { - let format:String + var format:String? var args = args if key.hasSuffix("_countable") { - if let count = args.first as? Int { - - let code = languageCodehash(appCurrentLanguage.languageCode) - - if let index = key.range(of: "_")?.lowerBound { - var string = String(key[.. 1 { - args.removeFirst() - } + + for i in 0 ..< args.count { + if let count = args[i] as? Int { + let code = languageCodehash(appCurrentLanguage.pluralizationCode) - } else { - format = _NSLocalizedString(key) + if let index = key.range(of: "_")?.lowerBound { + var string = String(key[.. 1 { + //args.remove(at: i) + //} + } else { + format = _NSLocalizedString(key) + } + break } - } else { + } + if format == nil { format = _NSLocalizedString(key) } + } else { format = _NSLocalizedString(key) } - let ranges = extractArgumentRanges(format) - var formatted = format - for range in ranges.reversed() { - if range.0 < args.count { - let value = "\(args[range.0])" - formatted = formatted.nsstring.replacingCharacters(in: range.1, with: value) - } else { - formatted = formatted.nsstring.replacingCharacters(in: range.1, with: "") + if let format = format { + let ranges = extractArgumentRanges(format) + var formatted = format + while ranges.count != args.count { + args.removeFirst() + } + let argIndexes = ranges.sorted(by: { lhs, rhs -> Bool in + return lhs.2 < rhs.2 + }) + + var argValues:[String] = args.map { "\($0)" } + + for index in argIndexes.map ({ $0.0 }) { + if !args.isEmpty { + argValues[index] = "\(args.removeFirst())" + } } + for range in ranges.reversed() { + if !argValues.isEmpty { + let value = argValues.removeLast() + formatted = formatted.nsstring.replacingCharacters(in: range.1, with: value) + } else { + formatted = formatted.nsstring.replacingCharacters(in: range.1, with: "") + } + } + return formatted } - return formatted + return "UndefinedKey" } -private let argumentRegex = try! NSRegularExpression(pattern: "%(((\\\\d+)\\\\$)?)([@df])", options: []) -func extractArgumentRanges(_ value: String) -> [(Int, NSRange)] { - var result: [(Int, NSRange)] = [] +private let argumentRegex = try! NSRegularExpression(pattern: "(%(((\\d+)\\$)?)([0-9])%(((\\d+)\\$)?)([@df]))|(%(((\\d+)\\$)?)([@df]))", options: []) +func extractArgumentRanges(_ value: String) -> [(Int, NSRange, Int)] { + var result: [(Int, NSRange, Int)] = [] 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 + let range = match.range(at: 0) + var valueIndex = index + if range.length >= 4, let index = Int(string.substring(with: NSMakeRange(range.location + 1, range.length - 3))) { + valueIndex = index } - result.append((currentIndex, match.range(at: 0))) + result.append((index, range, valueIndex)) index += 1 } result.sort(by: { $0.1.location < $1.1.location }) return result } -func t(_ key: L10n) -> String { - return key.string +final class TelegramLocalization : Equatable { + + + let primaryLanguage: Language + let secondaryLanguage: Language? + let baseLanguageCode: String + let localizedName: String + init(primaryLanguage: Language, secondaryLanguage: Language?, localizedName: String) { + self.primaryLanguage = primaryLanguage + self.secondaryLanguage = secondaryLanguage + self.localizedName = localizedName + self.baseLanguageCode = secondaryLanguage?.languageCode ?? primaryLanguage.languageCode + } + + var languageCode: String { + return baseLanguageCode + } + + static func == (lhs: TelegramLocalization, rhs: TelegramLocalization) -> Bool { + return lhs.primaryLanguage == rhs.primaryLanguage && lhs.secondaryLanguage == rhs.secondaryLanguage && lhs.baseLanguageCode == rhs.baseLanguageCode + } + + var pluralizationCode: String { + return primaryLanguage.customPluralizationCode ?? secondaryLanguage?.customPluralizationCode ?? secondaryLanguage?.languageCode ?? primaryLanguage.languageCode + } + } - -let _appCurrentLanguage:Atomic = Atomic(value: Language(languageCode: "en", strings: [:])) -var appCurrentLanguage:Language { - return _appCurrentLanguage.modify({$0}) +let _appCurrentLanguage:Atomic = Atomic(value: TelegramLocalization(primaryLanguage: Language(languageCode: "en", customPluralizationCode: nil, strings: [:]), secondaryLanguage: nil, localizedName: "English")) +var appCurrentLanguage:TelegramLocalization { + return _appCurrentLanguage.modify {$0} } -let languagePromise:Promise = Promise(Language(languageCode: "en", strings: [:])) +private let languagePromise:Promise = Promise(appCurrentLanguage) -var languageSignal:Signal { +var languageSignal:Signal { return languagePromise.get() |> distinctUntilChanged |> deliverOnMainQueue } public func _NSLocalizedString(_ key: String) -> String { - let language = appCurrentLanguage - - if let value = language.strings[key], !value.isEmpty { + let primary = appCurrentLanguage.primaryLanguage + let secondary = appCurrentLanguage.secondaryLanguage + + if let value = (primary.strings[key] ?? secondary?.strings[key]), !value.isEmpty { return value } else { let path = Bundle.main.path(forResource: "en", ofType: "lproj") diff --git a/Telegram-Mac/LocalizationPreviewModalController.swift b/Telegram-Mac/LocalizationPreviewModalController.swift new file mode 100644 index 0000000000..cf4c8bb014 --- /dev/null +++ b/Telegram-Mac/LocalizationPreviewModalController.swift @@ -0,0 +1,113 @@ +// +// LocalizationPreviewModalController.swift +// Telegram +// +// Created by Mikhail Filimonov on 11/12/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + + +private final class LocalizationPreviewView : Control { + private let titleView: TextView = TextView() + private let titleContainer: View = View() + + private let textView: TextView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + + titleView.isSelectable = false + titleView.userInteractionEnabled = false + + textView.isSelectable = false + + titleContainer.addSubview(titleView) + addSubview(titleContainer) + addSubview(textView) + titleContainer.border = [.Bottom] + } + + func update(with info: LocalizationInfo, width: CGFloat) -> CGFloat { + let titleLayout = TextViewLayout(.initialize(string: L10n.applyLanguageChangeLanguageTitle, color: theme.colors.text, font: .medium(.title)), alwaysStaticItems: true) + titleLayout.measure(width: width) + titleView.update(titleLayout) + + + let text: String + if info.isOfficial { + text = L10n.applyLanguageChangeLanguageOfficialText(info.title) + } else { + text = L10n.applyLanguageChangeLanguageUnofficialText1(info.title, "\(Int(Float(info.translatedStringCount) / Float(info.totalStringCount) * 100.0))") + } + + let attributedText = parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: .normal(.text), textColor: theme.colors.text), bold: MarkdownAttributeSet(font: .bold(.text), textColor: theme.colors.text), link: MarkdownAttributeSet(font: .normal(.text), textColor: theme.colors.link), linkAttribute: { contents in + return (NSAttributedString.Key.link.rawValue, inAppLink.callback(contents, { _ in + execute(inapp: .external(link: info.platformUrl, false)) + })) + })).mutableCopy() as! NSMutableAttributedString + attributedText.detectBoldColorInString(with: .bold(.text)) + + let textLayout = TextViewLayout(attributedText, alignment: .center, alwaysStaticItems: true) + textLayout.measure(width: width - 40) + + textLayout.interactions = globalLinkExecutor + + textView.update(textLayout) + + return 50 + 40 + textLayout.layoutSize.height + } + + override func layout() { + super.layout() + titleContainer.frame = NSMakeRect(0, 0, frame.width, 50) + titleView.center() + + textView.centerX(y: titleContainer.frame.maxY + 20) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class LocalizationPreviewModalController: ModalViewController { + private let context: AccountContext + private let info: LocalizationInfo + init(_ context: AccountContext, info: LocalizationInfo) { + self.info = info + self.context = context + super.init(frame: NSMakeRect(0, 0, 320, 200)) + bar = .init(height: 0) + } + private var genericView:LocalizationPreviewView { + return self.view as! LocalizationPreviewView + } + + private func applyLocalization() { + close() + _ = showModalProgress(signal: context.engine.localization.downloadAndApplyLocalization(accountManager: context.sharedContext.accountManager, languageCode: info.languageCode), for: context.window).start() + } + + override var modalInteractions: ModalInteractions? { + return ModalInteractions(acceptTitle: L10n.applyLanguageApplyLanguageAction, accept: { [weak self] in + self?.applyLocalization() + }, cancelTitle: L10n.modalCancel, height: 50) + } + + override func viewClass() -> AnyClass { + return LocalizationPreviewView.self + } + + override func viewDidLoad() { + super.viewDidLoad() + + let value = genericView.update(with: info, width: frame.width) + self.modal?.resize(with:NSMakeSize(genericView.frame.width, value), animated: false) + + readyOnce() + + } +} diff --git a/Telegram-Mac/LocationModalController.swift b/Telegram-Mac/LocationModalController.swift new file mode 100644 index 0000000000..d9d6997746 --- /dev/null +++ b/Telegram-Mac/LocationModalController.swift @@ -0,0 +1,690 @@ +// +// LocationModalController.swift +// Telegram +// +// Created by Mikhail Filimonov on 23/05/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import MapKit +import TelegramCore +import SwiftSignalKit +import Postbox + + + +private enum PickLocationState : Equatable { + case user(CLLocation?) + case custom(CLLocation?, named: String?) + var location: CLLocation? { + switch self { + case let .user(location): + return location + case let .custom(location, _): + return location + } + } +} + +private enum LocationViewState : Equatable { + case normal(PickLocationState) + case expanded(CLLocation?) +} + + +private final class LocationPinView : View { + private let locationPin: ImageView = ImageView() + private let dotView: View = View(frame: NSMakeRect(0, 0, 4, 4)) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(locationPin) + addSubview(dotView) + dotView.layer?.cornerRadius = 2 + updateLocalizationAndTheme(theme: theme) + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) + locationPin.image = theme.icons.locationMapPin + locationPin.sizeToFit() + dotView.backgroundColor = theme.colors.accentIcon + } + + override func layout() { + super.layout() + dotView.centerX(y: frame.height - dotView.frame.height) + } + + func updateState(_ state: PickLocationState, animated: Bool) -> Void { + + switch state { + case .user: + dotView.change(opacity: 0, animated: animated) + locationPin.change(pos: NSMakePoint(locationPin.frame.minX, frame.height - dotView.frame.height - locationPin.frame.height - 6), animated: animated, duration: 0.3, timingFunction: CAMediaTimingFunctionName.spring) + case let .custom(location, _): + dotView.change(opacity: 1, animated: animated) + locationPin.change(pos: NSMakePoint(locationPin.frame.minX, location == nil ? 0 : frame.height - dotView.frame.height - locationPin.frame.height - 6), animated: animated, duration: 0.3, timingFunction: CAMediaTimingFunctionName.spring) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private final class LocationMapView : View { + fileprivate let mapView: MKMapView = MKMapView() + private let headerTextView: TextView = TextView() + private let header: View = View() + private let expandContainer: Control = Control(frame: NSMakeRect(0, 0, 0, 50)) + private let expandButton: TitleButton = TitleButton() + private var state: LocationViewState = .normal(.user(nil)) + private var hasExpand: Bool = true + private let loadingView: ProgressIndicator = ProgressIndicator(frame: NSMakeRect(0, 0, 20, 20)) + fileprivate let dismiss: ImageButton = ImageButton() + fileprivate let tableView: TableView = TableView(frame: NSZeroRect) + fileprivate let locateButton: ImageButton = ImageButton() + + private let locationPinView = LocationPinView(frame: NSMakeRect(0, 0, 40, 70)) + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(mapView) + + mapView.mapType = .standard + mapView.isZoomEnabled = true + mapView.isScrollEnabled = true + mapView.showsZoomControls = true + mapView.wantsLayer = true + header.addSubview(headerTextView) + header.addSubview(dismiss) + header.addSubview(locateButton) + addSubview(header) + addSubview(tableView) + updateLocalizationAndTheme(theme: theme) + + expandButton.isEventLess = true + expandButton.userInteractionEnabled = false + + expandContainer.addSubview(loadingView) + expandContainer.addSubview(expandButton) + addSubview(expandContainer) + locateButton.autohighlight = false + mapView.addSubview(locationPinView) + } + + fileprivate func getSelectedLocation() -> CLLocation? { + let windowLocation = locationPinView.convert(NSMakePoint(locationPinView.frame.width / 2, locationPinView.frame.height - 2), to: nil) + let coordinate = mapView.convert(windowLocation, toCoordinateFrom: nil) + return CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude) + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) + locationPinView.updateLocalizationAndTheme(theme: theme) + expandButton.set(font: .medium(.title), for: .Normal) + expandButton.set(color: theme.colors.accent, for: .Normal) + dismiss.set(image: theme.icons.modalClose, for: .Normal) + _ = locateButton.sizeToFit() + _ = dismiss.sizeToFit() + header.backgroundColor = theme.colors.background + header.border = [.Bottom] + loadingView.progressColor = theme.colors.accent + expandContainer.border = [.Top] + expandContainer.backgroundColor = theme.colors.background + let title = TextViewLayout(.initialize(string: L10n.locationSendTitle, color: theme.colors.text, font: .medium(.title)), maximumNumberOfLines: 1) + title.measure(width: frame.width - 20) + + headerTextView.update(title) + headerTextView.center() + } + + fileprivate func updateExpandState(_ state: LocationViewState, loading: Bool, hasVenues: Bool, animated: Bool, toggleExpand:@escaping(LocationViewState)->Void) { + loadingView.isHidden = !loading && hasVenues + expandButton.isHidden = loading || !hasVenues + hasExpand = (loading || hasVenues) + self.state = state + + let duration: Double = 0.3 + let timingFunction: CAMediaTimingFunctionName = CAMediaTimingFunctionName.spring + + CATransaction.begin() + let mapY: CGFloat + switch state { + case let .normal(pickState): + switch pickState { + case .custom: + hasExpand = false + locateButton.set(image: theme.icons.locationMapLocate, for: .Normal) + case let .user(location): + locateButton.set(image: theme.icons.locationMapLocated, for: .Normal) + locateButton.isHidden = location == nil + } + locationPinView.change(opacity: loading ? 0 : 1, animated: animated) + locationPinView.updateState(pickState, animated: animated) + expandButton.set(text: L10n.locationSendShowNearby, for: .Normal) + tableView.change(size: NSMakeSize(frame.width, 60), animated: animated, timingFunction: CAMediaTimingFunctionName.spring) + tableView.change(pos: NSMakePoint(0, frame.height - 60 - (hasExpand ? expandContainer.frame.height : 0)), animated: animated, duration: duration, timingFunction: timingFunction) + mapY = header.frame.height + locateButton.userInteractionEnabled = true + case .expanded: + locateButton.userInteractionEnabled = false + locateButton.set(image: theme.icons.locationMapLocate, for: .Normal) + locationPinView.change(opacity: 0, animated: animated) + expandButton.set(text: L10n.locationSendHideNearby, for: .Normal) + let tableHeight = min(tableView.listHeight, frame.height - (hasExpand ? expandContainer.frame.height : 0) - header.frame.height - 50) + tableView.change(size: NSMakeSize(frame.width, tableHeight), animated: animated, duration: duration, timingFunction: timingFunction) + tableView.change(pos: NSMakePoint(0, frame.height - (hasExpand ? expandContainer.frame.height : 0) - tableHeight), animated: animated, duration: duration, timingFunction: timingFunction) + mapY = -(mapView.frame.height / 2) + header.frame.height + 50 / 2 + } + expandContainer.change(pos: NSMakePoint(0, hasExpand ? frame.height - expandContainer.frame.height : frame.height), animated: animated, duration: duration, timingFunction: timingFunction) + _ = locateButton.sizeToFit() + + + mapView._change(pos: NSMakePoint(0, mapY), animated: animated, duration: duration, timingFunction: timingFunction) + let pinPoint = mapView.focus(NSMakeSize(locationPinView.frame.width, 4)) + locationPinView.change(pos: NSMakePoint(pinPoint.minX, pinPoint.midY - locationPinView.frame.height), animated: animated, duration: 0.2, timingFunction: CAMediaTimingFunctionName.linear) + + + CATransaction.commit() + + expandContainer.removeAllHandlers() + if !loading { + expandContainer.set(handler: { [weak self] _ in + guard let `self` = self else {return} + switch state { + case .normal: + toggleExpand(.expanded(self.mapView.userLocation.location)) + case .expanded: + toggleExpand(.normal(.user(self.mapView.userLocation.location))) + } + }, for: .Click) + } + needsLayout = true + } + + override func layout() { + super.layout() + header.frame = NSMakeRect(0, 0, frame.width, 50) + headerTextView.center() + dismiss.centerY(x: 10) + locateButton.centerY(x: frame.width - 10 - locateButton.frame.width) + expandContainer.frame = NSMakeRect(0, hasExpand ? frame.height - expandContainer.frame.height : frame.height, frame.width, expandContainer.frame.height) + let mapY: CGFloat + let mapHeight: CGFloat = frame.height - 50 - header.frame.height + switch state { + case .normal: + tableView.frame = NSMakeRect(0, frame.height - 60 - (hasExpand ? expandContainer.frame.height : 0), frame.width, 60) + mapY = header.frame.height + case .expanded: + let tableHeight = min(tableView.listHeight, frame.height - (hasExpand ? expandContainer.frame.height : 0) - header.frame.height - 50) + tableView.frame = NSMakeRect(0, frame.height - (hasExpand ? expandContainer.frame.height : 0) - tableHeight, frame.width, tableHeight) + mapY = -(mapHeight / 2) + header.frame.height + 50 / 2 + } + let delegate = mapView.delegate + mapView.delegate = nil + mapView.frame = NSMakeRect(0, mapY, frame.width, mapHeight) + mapView.delegate = delegate + + let pinPoint = mapView.focus(NSMakeSize(4, 4)) + locationPinView.centerX(y: pinPoint.midY - locationPinView.frame.height) + + + expandButton.center() + loadingView.center() + + let zoomControls = HackUtils.findElements(byClass: "MKZoomSegmentedControl", in: mapView).first as? NSView + if let zoomControls = zoomControls { + zoomControls.setFrameOrigin(20, 20) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + + +private final class MapItemsArguments { + let context: AccountContext + let sendCurrent:()->Void + let sendVenue:(TelegramMediaMap)->Void + let searchVenues:(String)->Void + init(context: AccountContext, sendCurrent:@escaping()->Void, sendVenue:@escaping(TelegramMediaMap)->Void, searchVenues: @escaping(String)->Void) { + self.context = context + self.sendCurrent = sendCurrent + self.sendVenue = sendVenue + self.searchVenues = searchVenues + } +} + +private enum MapItemEntryId : Hashable { + case currentLocation + case expandNearby + case nearby(Int32) + case search + case searchEmptyId + var hashValue: Int { + return 0 + } +} + +private enum MapItemEntry : TableItemListNodeEntry { + case currentLocation(index:Int32, state: LocationSelectCurrentState) + case expandNearby(index: Int32, expand: Bool, loading: Bool) + case nearby(index: Int32, result: ChatContextResult) + case search(index: Int32) + case searchEmpty(index: Int32, loading: Bool) + var index: Int32 { + switch self { + case let .currentLocation(index, _): + return index + case let .expandNearby(index, _, _): + return index + case let .nearby(index, _): + return index + case let .search(index): + return index + case let .searchEmpty(index, _): + return index + } + } + + var stableId: MapItemEntryId { + switch self { + case .currentLocation: + return .currentLocation + case .expandNearby: + return .expandNearby + case .search: + return .search + case .searchEmpty: + return .searchEmptyId + case let .nearby(index, _): + return .nearby(index) + } + } + + func item(_ arguments: MapItemsArguments, initialSize: NSSize) -> TableRowItem { + switch self { + case let .nearby(_, result): + return LocationPlaceSuggestionRowItem(initialSize, stableId: stableId, account: arguments.context.account, result: result, action: { + switch result.message { + case let .mapLocation(media, _): + arguments.sendVenue(media) + default: + break + } + }) + case .search: + return SearchRowItem(initialSize, stableId: stableId, searchInteractions: SearchInteractions({ state, _ in + arguments.searchVenues(state.request) + }, { state in + arguments.searchVenues(state.request) + }), inset: NSEdgeInsets(left: 10,right: 10, top: 10, bottom: 10)) + case let .currentLocation(_, state): + return LocationSendCurrentItem(initialSize, stableId: stableId, state: state, action: { + arguments.sendCurrent() + }) + case let .searchEmpty(_, loading): + return SearchEmptyRowItem(initialSize, stableId: stableId, isLoading: loading) + default: + return GeneralRowItem(initialSize, height: 20, stableId: stableId) + } + } +} + +private func < (lhs: MapItemEntry, rhs: MapItemEntry) -> Bool { + return lhs.index < rhs.index +} + +private func mapEntries(result: [ChatContextResult], loading: Bool, location: CLLocation?, state: LocationViewState) -> [MapItemEntry] { + var entries: [MapItemEntry] = [] + + + + var index: Int32 = 0 + + let selectState: LocationSelectCurrentState + switch state { + case .expanded: + selectState = .accurate(location: location, expanded: true) + case let .normal(pickState): + switch pickState { + case .user: + selectState = .accurate(location: location, expanded: false) + case let .custom(_, name): + let text: String + if let name = name { + text = name.isEmpty ? L10n.locationSendThisLocationUnknown : name + } else { + text = L10n.locationSendLocating + } + selectState = .selected(location: text) + } + } + + entries.append(.currentLocation(index: index, state: selectState)) + index += 1 + switch state { + case .expanded: + entries.append(.search(index: index)) + index += 1 + if !result.isEmpty { + for value in result { + entries.append(.nearby(index: index, result: value)) + index += 1 + } + } else { + entries.append(MapItemEntry.searchEmpty(index: index, loading: false)) + index += 1 + } + + case .normal: + break + } + + return entries +} + +fileprivate func prepareTransition(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], initialSize:NSSize, arguments:MapItemsArguments) -> TableUpdateTransition { + + let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in + return entry.entry.item(arguments, initialSize: initialSize) + } + + return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true) +} + +private class MapDelegate : NSObject, MKMapViewDelegate { + + fileprivate var isPinRaised: Bool = false + private var animated: Bool = false + let location:Promise = Promise() + fileprivate var willChangeRegion:()->Void = {} + fileprivate var didChangeRegion:()->Void = {} + + func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) { + self.location.set(.single(userLocation)) + + guard !isPinRaised else {return} + focusUserLocation(mapView) + } + + func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) { + willChangeRegion() + + } + + func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { + didChangeRegion() + } + + func mapView(_ mapView: MKMapView, didFailToLocateUserWithError error: Error) { + if "\(error)".contains("Code=1") { + cancelRequestLocation() + } + } + + func cancelRequestLocation() { + self.location.set(.single(nil)) + isPinRaised = true + didChangeRegion() + } + + + fileprivate func focusUserLocation(_ mapView: MKMapView) { + let userLocation = mapView.userLocation + + var region = MKCoordinateRegion() + var span = MKCoordinateSpan() + span.latitudeDelta = CLLocationDegrees(0.005) + span.longitudeDelta = CLLocationDegrees(0.005) + var location = CLLocationCoordinate2D() + location.latitude = userLocation.coordinate.latitude + location.longitude = userLocation.coordinate.longitude + region.span = span + region.center = location + mapView.setRegion(region, animated: animated) + animated = true + } +} + +class LocationModalController: ModalViewController { + + private let chatInteraction: ChatInteraction + private let delegate: MapDelegate = MapDelegate() + private let disposable = MetaDisposable() + private let sendDisposable = MetaDisposable() + private let requestDisposable = MetaDisposable() + private let statePromise:Promise = Promise() + init(_ chatInteraction: ChatInteraction) { + self.chatInteraction = chatInteraction + super.init(frame: NSMakeRect(0, 0, 360, 380)) + } + + override var dynamicSize: Bool { + return true + } + + override open func measure(size: NSSize) { + self.modal?.resize(with:NSMakeSize(360, size.height - 70), animated: false) + } + + public func updateSize(_ animated: Bool) { + if let contentSize = self.modal?.window.contentView?.frame.size { + self.modal?.resize(with:NSMakeSize(360, contentSize.height - 70), animated: animated) + } + } + + override func viewClass() -> AnyClass { + return LocationMapView.self + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + genericView.mapView.showsUserLocation = false + window?.removeAllHandlers(for: self) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + window?.set(mouseHandler: { [weak self] _ -> KeyHandlerResult in + self?.delegate.isPinRaised = true + return .rejected + }, with: self, for: .leftMouseDragged, priority: .modal) + } + + override func close(animationType: ModalAnimationCloseBehaviour = .common) { + super.close(animationType: animationType) + } + + private func sendLocation(_ media: TelegramMediaMap? = nil) { + sendDisposable.set((statePromise.get() |> deliverOnMainQueue).start(next: { [weak self] state in + switch state { + case let .normal(picked): + if let location = picked.location { + self?.chatInteraction.sendLocation(location.coordinate, nil) + self?.close() + } + case let .expanded(location): + if let media = media { + let coordinate = CLLocationCoordinate2D(latitude: media.latitude, longitude: media.longitude) + self?.chatInteraction.sendLocation(coordinate, media.venue) + self?.close() + } else if let location = location { + self?.chatInteraction.sendLocation(location.coordinate, nil) + self?.close() + } + } + })) + } + + override func returnKeyAction() -> KeyHandlerResult { + sendLocation() + return .invoked + } + + override func viewDidLoad() { + super.viewDidLoad() + + genericView.mapView.delegate = delegate + genericView.dismiss.set(handler: { [weak self] _ in + self?.close() + }, for: .Click) + + let state: ValuePromise = ValuePromise(.normal(.user(genericView.mapView.userLocation.location)), ignoreRepeated: true) + statePromise.set(state.get()) + + genericView.locateButton.set(handler: { [weak self] _ in + guard let `self` = self else {return} + self.delegate.focusUserLocation(self.genericView.mapView) + self.delegate.isPinRaised = false + }, for: .Click) + + if let _ = genericView.mapView.userLocation.location { + delegate.location.set(.single(genericView.mapView.userLocation)) + delegate.focusUserLocation(genericView.mapView) + } + + var handleRegion: Bool = true + + delegate.willChangeRegion = { [weak self] in + guard let `self` = self, handleRegion else {return} + if self.delegate.isPinRaised { + state.set(.normal(.custom(nil, named: nil))) + } else { + state.set(.normal(.user(self.genericView.mapView.userLocation.location))) + } + } + delegate.didChangeRegion = { [weak self] in + guard let `self` = self, handleRegion else {return} + if self.delegate.isPinRaised { + state.set(.normal(.custom(self.genericView.getSelectedLocation(), named: nil))) + } else { + state.set(.normal(.user(self.genericView.mapView.userLocation.location))) + } + } + + let peerId = chatInteraction.peerId + let context = self.chatInteraction.context + + let search:Promise = Promise("") + + var cachedData:[String : ChatContextResultCollection] = [:] + let previousResult:Atomic = Atomic(value: nil) + + + + let peerSignal: Signal = .single(nil) |> then(context.engine.peers.resolvePeerByName(name: "foursquare") |> map { $0?._asPeer().id }) + let requestSignal = combineLatest(peerSignal |> deliverOnPrepareQueue, delegate.location.get() |> take(1) |> deliverOnPrepareQueue, search.get() |> distinctUntilChanged |> deliverOnPrepareQueue) + |> mapToSignal { botId, location, query -> Signal<(ChatContextResultCollection?, CLLocation?, Bool, Bool), NoError> in + if let botId = botId, let location = location { + let first = Signal<(ChatContextResultCollection?, CLLocation?, Bool, Bool), NoError>.single((cachedData[query] ?? previousResult.modify {$0}, location.location, cachedData[query] == nil, !query.isEmpty)) + if cachedData[query] == nil { + return first |> then(context.engine.messages.requestChatContextResults(botId: botId, peerId: peerId, query: query, location: .single((location.coordinate.latitude, location.coordinate.longitude)), offset: "") + |> `catch` { _ in return .complete() } + |> deliverOnPrepareQueue |> map { result in + var value = result?.results + if let result = result { + cachedData[query] = result.results + } + value = previousResult.modify { _ in result?.results } + + return (value, location.location, false, !query.isEmpty) + }) + } else { + return first + } + + } else { + return .single((nil, location?.location, botId == nil, false)) + } + } + + let signal: Signal<(ChatContextResultCollection?, CLLocation?, Bool, Bool), NoError> = .single((nil, nil, true, false)) |> then(requestSignal) + + let previous: Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) + + let initialSize = self.atomicSize + let arguments = MapItemsArguments(context: context, sendCurrent: { [weak self] in + self?.sendLocation() + }, sendVenue: { [weak self] venue in + self?.sendLocation(venue) + }, searchVenues: { query in + prepareQueue.async { + if cachedData[query] != nil { + search.set(.single(query)) + } else { + search.set(.single(query) |> delay(0.2, queue: prepareQueue)) + } + } + }) + + let stateModified = state.get() |> mapToSignal { state -> Signal in + switch state { + case let .normal(pick): + switch pick { + case let .custom(location, _): + if let location = location { + return .single(state) |> then(reverseGeocodeLocation(latitude: location.coordinate.latitude, longitude: location.coordinate.longitude) |> map { value in + return .normal(.custom(location, named: value?.fullAddress)) + }) + } + default: + break + } + default: + break + } + return .single(state) + } |> distinctUntilChanged + + let transition:Signal<(TableUpdateTransition, Bool, LocationViewState, Bool), NoError> = combineLatest(queue: prepareQueue, signal, appearanceSignal, stateModified) |> map { data, appearance, state in + let results:[ChatContextResult] = data.0?.results ?? [] + let entries = mapEntries(result: results, loading: data.2, location: data.1, state: state).map{AppearanceWrapperEntry(entry: $0, appearance: appearance)} + return (prepareTransition(left: previous.swap(entries), right: entries, initialSize: initialSize.modify{$0}, arguments: arguments), data.2, state, !results.isEmpty || data.3) + } |> deliverOnMainQueue + + let animated:Atomic = Atomic(value: false) + + disposable.set(transition.start(next: { [weak self] transition, loading, expanded, hasVenues in + guard let `self` = self else {return} + self.genericView.tableView.merge(with: transition) + switch expanded { + case .expanded: + handleRegion = false + default: + handleRegion = true + } + self.genericView.updateExpandState(expanded, loading: loading, hasVenues: hasVenues, animated: animated.swap(true), toggleExpand: { [weak self] viewState in + self?.genericView.tableView.clipView.scroll(to: NSMakePoint(0, 0), animated: false) + search.set(.single("")) + state.set(viewState) + }) + self.readyOnce() + })) + + let request = requestUserLocation() |> deliverOnMainQueue + requestDisposable.set(request.start(next: { [weak self] result in + self?.genericView.mapView.showsUserLocation = true + }, error: { [weak self] error in + self?.delegate.cancelRequestLocation() + })) + } + + deinit { + disposable.dispose() + sendDisposable.dispose() + requestDisposable.dispose() + } + + private var genericView: LocationMapView { + return view as! LocationMapView + } +} diff --git a/Telegram-Mac/LocationPlaceSuggestionRowItem.swift b/Telegram-Mac/LocationPlaceSuggestionRowItem.swift new file mode 100644 index 0000000000..02fb45e463 --- /dev/null +++ b/Telegram-Mac/LocationPlaceSuggestionRowItem.swift @@ -0,0 +1,171 @@ +// +// LocationPlaceSuggestionRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 24/05/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit +import TelegramCore + +import Postbox + +private let randomColors = [NSColor(rgb: 0xe56cd5), NSColor(rgb: 0xf89440), NSColor(rgb: 0x9986ff), NSColor(rgb: 0x44b3f5), NSColor(rgb: 0x6dc139), NSColor(rgb: 0xff5d5a), NSColor(rgb: 0xf87aad), NSColor(rgb: 0x6e82b3), NSColor(rgb: 0xf5ba21)] + +private let venueColors: [String: NSColor] = [ + "building/medical": NSColor(rgb: 0x43b3f4), + "building/gym": NSColor(rgb: 0x43b3f4), + "arts_entertainment": NSColor(rgb: 0xe56dd6), + "travel/bedandbreakfast": NSColor(rgb: 0x9987ff), + "travel/hotel": NSColor(rgb: 0x9987ff), + "travel/hostel": NSColor(rgb: 0x9987ff), + "travel/resort": NSColor(rgb: 0x9987ff), + "building": NSColor(rgb: 0x6e81b2), + "education": NSColor(rgb: 0xa57348), + "event": NSColor(rgb: 0x959595), + "food": NSColor(rgb: 0xf7943f), + "education/cafeteria": NSColor(rgb: 0xf7943f), + "nightlife": NSColor(rgb: 0xe56dd6), + "travel/hotel_bar": NSColor(rgb: 0xe56dd6), + "parks_outdoors": NSColor(rgb: 0x6cc039), + "shops": NSColor(rgb: 0xffb300), + "travel": NSColor(rgb: 0x1c9fff), + "work": NSColor(rgb: 0xad7854), + "home": NSColor(rgb: 0x00aeef) +] + +func venueIconColor(type: String) -> NSColor { + if type.isEmpty { + return NSColor(rgb: 0x008df2) + } + if let color = venueColors[type] { + return color + } + let generalType = type.components(separatedBy: "/").first ?? type + if let color = venueColors[generalType] { + return color + } + + let index = Int(abs(persistentHash32(type)) % Int32(randomColors.count)) + return randomColors[index] +} + + + +class LocationPlaceSuggestionRowItem: GeneralRowItem { + private let result: ChatContextResult + fileprivate let textLayout: TextViewLayout + fileprivate let image: TelegramMediaImage? + fileprivate let account: Account + fileprivate let color: NSColor + init(_ initialSize: NSSize, stableId: AnyHashable, account: Account, result: ChatContextResult, action: @escaping()->Void) { + self.result = result + self.account = account + let attr = NSMutableAttributedString() + var image: TelegramMediaImage? = nil + switch result { + case let .externalReference(values): + + switch values.message { + case let .mapLocation(media, _): + if let venue = media.venue { + _ = attr.append(string: venue.title, color: theme.colors.text, font: .medium(.text)) + _ = attr.append(string: "\n") + _ = attr.append(string: venue.address, color: theme.colors.grayText, font: .normal(.text)) + if let type = venue.type { + let resource = HttpReferenceMediaResource(url: "https://ss3.4sqi.net/img/categories_v2/\(type)_88.png", size: nil) + let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 60, height: 60), resource: resource, progressiveSizes: [], immediateThumbnailData: nil) + image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [representation], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + } + } + self.color = venueIconColor(type: media.venue?.type ?? "") + default: + self.color = venueIconColor(type: "") + } + textLayout = TextViewLayout(attr, maximumNumberOfLines: 2) + default: + fatalError() + } + + self.image = image + + super.init(initialSize, height: 60, stableId: stableId, action: action, inset: NSEdgeInsetsMake(0, 10, 0, 10)) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) + textLayout.measure(width: width - inset.left - inset.right - 50 - 10) + return success + } + + override func viewClass() -> AnyClass { + return LocationPlaceSuggestionRowView.self + } + +} + +private final class LocationPlaceSuggestionRowView : TableRowView { + private let thumbHolder: View = View() + private let textView: TextView = TextView() + private let imageView: TransformImageView = TransformImageView(frame: NSMakeRect(0, 0, 50, 50)) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(thumbHolder) + addSubview(textView) + textView.userInteractionEnabled = false + textView.isSelectable = false + thumbHolder.setFrameSize(50, 50) + thumbHolder.layer?.cornerRadius = 25 + thumbHolder.addSubview(imageView) + textView.isEventLess = true + } + + override func mouseUp(with event: NSEvent) { + if mouseInside() { + guard let item = item as? GeneralRowItem else {return} + item.action() + } else { + super.mouseUp(with: event) + } + } + + override func updateColors() { + super.updateColors() + + guard let item = item as? LocationPlaceSuggestionRowItem else {return} + + + textView.backgroundColor = theme.colors.background + thumbHolder.backgroundColor = item.color + } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + + guard let item = item as? LocationPlaceSuggestionRowItem else {return} + + textView.update(item.textLayout) + imageView.isHidden = item.image == nil + if let image = item.image { + imageView.setSignal(chatWebpageSnippetPhoto(account: item.account, imageReference: ImageMediaReference.standalone(media: image), scale: backingScaleFactor, small: true)) + imageView.set(arguments: TransformImageArguments(corners: ImageCorners(radius: 25), imageSize: NSMakeSize(50, 50), boundingSize: NSMakeSize(50, 50), intrinsicInsets: NSEdgeInsetsZero)) + } + } + + override func layout() { + super.layout() + guard let item = item as? GeneralRowItem else {return} + + thumbHolder.centerY(x: item.inset.left) + textView.centerY(x: thumbHolder.frame.maxX + 10) + imageView.center() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/Telegram-Mac/LocationPreviewMapRowItem.swift b/Telegram-Mac/LocationPreviewMapRowItem.swift new file mode 100644 index 0000000000..e0529aaf22 --- /dev/null +++ b/Telegram-Mac/LocationPreviewMapRowItem.swift @@ -0,0 +1,222 @@ +// +// LocationPreviewMapRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 19/10/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit +import MapKit +import TelegramCore + +import Postbox + + +private final class MapPin : NSObject, MKAnnotation +{ + let coordinate: CLLocationCoordinate2D + let peer: Peer? + let account: Account + + var focusRegion: (()->Void)? + + init (coordinate: CLLocationCoordinate2D, account: Account, peer: Peer?) + { + self.coordinate = coordinate + self.peer = peer + self.account = account + } + + +} + +private final class MapPinView: View { + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + background = .random + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private final class LocationAnnotationView : MKAnnotationView { + private let avatar = AvatarControl(font: .avatar(14)) + private let border = View() + fileprivate let control = OverlayControl() + override init(annotation: MKAnnotation?, reuseIdentifier: String?) { + super.init(annotation: annotation, reuseIdentifier: reuseIdentifier) + + + frame = CGRect(x: 0, y: 0, width: 60, height: 60) + wantsLayer = true + + + border.setFrameSize(NSMakeSize(42, 42)) + border.layer?.cornerRadius = border.frame.width / 2 + border.background = .white + addSubview(border) + + let shadow = NSShadow() + shadow.shadowBlurRadius = 5 + shadow.shadowColor = NSColor.black.withAlphaComponent(0.3) + shadow.shadowOffset = NSMakeSize(0, 0) + self.border.shadow = shadow + + + avatar.setFrameSize(NSMakeSize(40, 40)) + avatar.layer?.cornerRadius = avatar.frame.width / 2 + addSubview(avatar) + + control.frame = bounds + + addSubview(control) + + control.set(handler: { [weak self] _ in + (self?.annotation as? MapPin)?.focusRegion?() + }, for: .Click) + + update() + } + override var annotation: MKAnnotation? { + didSet { + update() + } + } + + override func layout() { + avatar.center() + border.center() + } + + private func update() { + + if let annotation = self.annotation as? MapPin { + self.avatar.setPeer(account: annotation.account, peer: annotation.peer) + } + } + + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + static var reuseIdentifier: String { + return "peer" + } + +} +@available(macOS 10.13, *) +class LocationPreviewMapRowItem: GeneralRowItem { + let map: TelegramMediaMap + let peer: Peer? + let context: AccountContext + fileprivate let pin: MapPin + init(_ initialSize: NSSize, height: CGFloat, stableId: AnyHashable, context: AccountContext, map: TelegramMediaMap, peer: Peer?, viewType: GeneralViewType) { + self.map = map + self.peer = peer + self.context = context + self.pin = MapPin(coordinate: CLLocationCoordinate2D(latitude: map.latitude, longitude: map.longitude), account: context.account, peer: peer) + super.init(initialSize, height: height, stableId: stableId, viewType: viewType) + } + + deinit { + + } + + override func viewClass() -> AnyClass { + return LocationPreviewMapRowView.self + } + +} + +@available(macOS 10.13, *) +private final class LocationPreviewMapRowView : TableRowView, MKMapViewDelegate { + private let mapView: MKMapView = MKMapView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(mapView) + mapView.register(LocationAnnotationView.self, forAnnotationViewWithReuseIdentifier: LocationAnnotationView.reuseIdentifier) + mapView.delegate = self + + + mapView.showsZoomControls = true + // mapView.showsUserLocation = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + mapView.delegate = nil + } + + override func layout() { + super.layout() + mapView.frame = bounds + } + + func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { + switch annotation { + case is MapPin: + return mapView.dequeueReusableAnnotationView(withIdentifier: LocationAnnotationView.reuseIdentifier, for: annotation) + default: + return nil + } + } + + private var doNotUpdateRegion: Bool = false + + func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { + if location != nil { + doNotUpdateRegion = true + } + } + + private var location: NSPoint? = nil + override func mouseUp(with event: NSEvent) { + super.mouseUp(with: event) + location = nil + + } + override func mouseDown(with event: NSEvent) { + super.mouseDown(with: event) + location = event.locationInWindow + } + + override func set(item: TableRowItem, animated: Bool = false) { + let previousItem = self.item + super.set(item: item, animated: animated) + + + guard let item = item as? LocationPreviewMapRowItem else { + return + } + + let focus:(Bool)->Void = { [weak self, unowned item] animated in + let center = CLLocationCoordinate2D(latitude: item.map.latitude, longitude: item.map.longitude) + let region = MKCoordinateRegion(center: center, span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)) + self?.mapView.setRegion(region, animated: animated) + self?.doNotUpdateRegion = false + } + + if !doNotUpdateRegion { + focus(animated) + } + + if let previousItem = previousItem as? LocationPreviewMapRowItem { + mapView.removeAnnotation(previousItem.pin) + } + item.pin.focusRegion = { + focus(true) + } + + mapView.addAnnotation(item.pin) + } +} diff --git a/Telegram-Mac/LocationRequest.swift b/Telegram-Mac/LocationRequest.swift new file mode 100644 index 0000000000..3422abdd53 --- /dev/null +++ b/Telegram-Mac/LocationRequest.swift @@ -0,0 +1,110 @@ +// +// LocationRequest.swift +// Telegram +// +// Created by Mikhail Filimonov on 24/08/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import CoreLocation +import SwiftSignalKit +import AVKit +enum UserLocationResult : Equatable { + case success(CLLocation) +} +enum UserLocationError : Equatable { + case restricted + case notDetermined + case denied + case wifiRequired + case disabled +} +private let manager: CLLocationManager = CLLocationManager() + +private class UserLocationRequest : NSObject, CLLocationManagerDelegate { + fileprivate let result: ValuePromise = ValuePromise(ignoreRepeated: true) + fileprivate let error: ValuePromise = ValuePromise(ignoreRepeated: true) + + override init() { + super.init() + + manager.desiredAccuracy = kCLLocationAccuracyThreeKilometers + + manager.delegate = self + + if CLLocationManager.locationServicesEnabled() { + self.locationManager(manager, didChangeAuthorization: CLLocationManager.authorizationStatus()) + } else { + error.set(.disabled) + } + } + + @objc func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { + switch status { + case .authorizedAlways, .authorizedWhenInUse: + manager.startUpdatingLocation() + case .denied: + error.set(.denied) + case .restricted: + error.set(.restricted) + case .notDetermined: + if #available(macOS 10.15, *) { + manager.requestWhenInUseAuthorization() + } else { + manager.startUpdatingLocation() + } + @unknown default: + error.set(.denied) + } + } + + @objc func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + self.error.set(.wifiRequired) + } + + @objc func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + + if let location = locations.last { + manager.stopUpdatingLocation() + manager.delegate = nil; + result.set(.success(location)) + } + } + + deinit { + var bp:Int = 0 + bp += 1 + } + + + func stop() { + manager.stopUpdatingLocation() + manager.delegate = nil; + } +} + +func requestUserLocation() -> Signal { + + return Signal { subscriber -> Disposable in + let disposable = DisposableSet() + var manager: UserLocationRequest! + Queue.mainQueue().async { + manager = UserLocationRequest() + + disposable.add(manager.result.get().start(next: { result in + subscriber.putNext(result) + })) + disposable.add(manager.error.get().start(next: { result in + subscriber.putError(result) + })) + } +// + return ActionDisposable { + disposable.dispose() + Queue.mainQueue().async { + manager.stop() + } + } + } +} diff --git a/Telegram-Mac/LocationSendCurrentItem.swift b/Telegram-Mac/LocationSendCurrentItem.swift new file mode 100644 index 0000000000..b5a5a5ca5d --- /dev/null +++ b/Telegram-Mac/LocationSendCurrentItem.swift @@ -0,0 +1,134 @@ +// +// LocationSendCurrent.swift +// Telegram +// +// Created by Mikhail Filimonov on 24/05/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import MapKit +enum LocationSelectCurrentState : Equatable { + case accurate(location: CLLocation?, expanded: Bool) + case selected(location: String) +} + +class LocationSendCurrentItem: GeneralRowItem { + fileprivate let statusLayout: TextViewLayout + fileprivate let state: LocationSelectCurrentState + init(_ initialSize: NSSize, stableId: AnyHashable, state: LocationSelectCurrentState, action:@escaping()->Void) { + self.state = state + let text: String + switch state { + case let .accurate(location, _): + if let location = location { + let formatter = MKDistanceFormatter() + formatter.unitStyle = .full + formatter.locale = Locale(identifier: appAppearance.language.languageCode) + let formatted = formatter.string(fromDistance: location.horizontalAccuracy) + text = L10n.locationSendAccurateTo("\(formatted)") + } else { + text = L10n.locationSendLocating + } + + case let .selected(location): + text = location + } + statusLayout = TextViewLayout.init(.initialize(string: text, color: theme.colors.grayText, font: .normal(.text)), maximumNumberOfLines: 1) + super.init(initialSize, height: 60, stableId: stableId, action: action, inset: NSEdgeInsetsMake(0, 10, 0, 10)) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) + statusLayout.measure(width: width - inset.left - inset.right - theme.icons.locationPin.backingSize.width - 10) + return success + } + + override func viewClass() -> AnyClass { + return LocationSendCurrentView.self + } +} + + +private final class LocationSendCurrentView : TableRowView { + private let iconView: ImageView = ImageView() + private let statusView = TextView() + private let button: TitleButton = TitleButton() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(button) + addSubview(iconView) + addSubview(statusView) + statusView.userInteractionEnabled = false + statusView.isSelectable = false + button.userInteractionEnabled = false + button.isEventLess = true + statusView.isEventLess = true + } + + override func mouseUp(with event: NSEvent) { + if mouseInside() { + guard let item = item as? GeneralRowItem else {return} + item.action() + } else { + super.mouseUp(with: event) + } + } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + + guard let item = item as? LocationSendCurrentItem else {return} + + + button.set(font: .medium(.title), for: .Normal) + button.set(color: theme.colors.accent, for: .Normal) + let text: String + switch item.state { + case .accurate: + text = L10n.locationSendMyLocation + case .selected: + text = L10n.locationSendThisLocation + } + button.set(text: text, for: .Normal) + _ = button.sizeToFit() + + statusView.update(item.statusLayout) + + iconView.image = theme.icons.locationPin + _ = iconView.sizeToFit() + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + super.draw(layer, in: ctx) + + guard let item = item as? LocationSendCurrentItem else {return} + + switch item.state { + case let .accurate(_, expanded): + if expanded { + ctx.setFillColor(theme.colors.border.cgColor) + ctx.fill(NSMakeRect(statusView.frame.minX, frame.height - .borderSize, frame.width - statusView.frame.minX, .borderSize)) + } + default: + break + } + + } + + override func layout() { + guard let item = item as? GeneralRowItem else {return} + + iconView.centerY(x: item.inset.left) + + button.setFrameOrigin(iconView.frame.maxX + 3, frame.height / 2 - button.frame.height) + statusView.setFrameOrigin(iconView.frame.maxX + 10, frame.height / 2) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + +} diff --git a/Telegram-Mac/LoginErrorStateView.swift b/Telegram-Mac/LoginErrorStateView.swift index a354f6c1ed..419fd6b02d 100644 --- a/Telegram-Mac/LoginErrorStateView.swift +++ b/Telegram-Mac/LoginErrorStateView.swift @@ -7,7 +7,7 @@ // import Cocoa -import SwiftSignalKitMac +import SwiftSignalKit import TGUIKit enum LoginAuthErrorState : Equatable { case normal diff --git a/Telegram-Mac/LoginViewController.swift b/Telegram-Mac/LoginViewController.swift index eb8bd112c0..38abc2f44e 100644 --- a/Telegram-Mac/LoginViewController.swift +++ b/Telegram-Mac/LoginViewController.swift @@ -8,57 +8,193 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac -import MtProtoKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit + private let manager = CountryManager() final class LoginAuthViewArguments { let sendCode:(String)->Void + let updatePhoneNumberField:(String)->Void let resendCode:()->Void let editPhone:()->Void let checkCode:(String)->Void let checkPassword:(String)->Void - init(sendCode:@escaping(String)->Void, resendCode:@escaping()->Void, editPhone:@escaping()->Void, checkCode:@escaping(String)->Void, checkPassword:@escaping(String)->Void) { + let requestPasswordRecovery: (@escaping(String)-> Void)->Void + let resetAccount: ()->Void + let signUp:(String, String, URL?) -> Void + let cancelQrAuth:()->Void + init(sendCode:@escaping(String)->Void, resendCode:@escaping()->Void, editPhone:@escaping()->Void, checkCode:@escaping(String)->Void, checkPassword:@escaping(String)->Void, requestPasswordRecovery: @escaping(@escaping(String)-> Void)->Void, resetAccount: @escaping()->Void, signUp:@escaping(String, String, URL?) -> Void, cancelQrAuth: @escaping()->Void, updatePhoneNumberField:@escaping(String)->Void) { self.sendCode = sendCode self.resendCode = resendCode self.editPhone = editPhone self.checkCode = checkCode self.checkPassword = checkPassword + self.requestPasswordRecovery = requestPasswordRecovery + self.resetAccount = resetAccount + self.signUp = signUp + self.cancelQrAuth = cancelQrAuth + self.updatePhoneNumberField = updatePhoneNumberField } } -private class SignupView : View { - let textView:TextView = TextView() - let button:TitleButton = TitleButton() +private class SignupView : View, NSTextFieldDelegate { + + let firstName:NSTextField = NSTextField() + let lastName:NSTextField = NSTextField() + + private var photoUrl: URL? + + private let firstNameSeparator: View = View() + private let lastNameSeparator: View = View() + + private let photoView = ImageView() + private let descView: TextView = TextView() + private let addPhotoView: TextView = TextView() var arguments: LoginAuthViewArguments? required init(frame frameRect: NSRect) { super.init(frame: frameRect) - addSubview(textView) - addSubview(button) + addSubview(firstName) + addSubview(lastName) + addSubview(firstNameSeparator) + addSubview(lastNameSeparator) + addSubview(addPhotoView) + addSubview(photoView) + addSubview(descView) + descView.userInteractionEnabled = false + descView.isSelectable = false + addPhotoView.userInteractionEnabled = false + addPhotoView.isSelectable = false + - button.set(font: .medium(.title), for: .Normal) - button.set(color: .blueUI, for: .Normal) - button.set(text: tr(.alertOK), for: .Normal) + firstName.isBordered = false + firstName.usesSingleLineMode = true + firstName.isBezeled = false + firstName.focusRingType = .none + firstName.drawsBackground = false - button.sizeToFit() + firstName.delegate = self + lastName.delegate = self - button.set(handler: { [weak self] _ in - self?.arguments?.editPhone() - }, for: .Click) - let layout = TextViewLayout(.initialize(string: tr(.loginPhoneNumberNotRegistred), color: .text, font: .normal(.title)), alignment: .center) - layout.measure(width: frameRect.width - 20) - textView.update(layout) + lastName.isBordered = false + lastName.usesSingleLineMode = true + lastName.isBezeled = false + lastName.focusRingType = .none + lastName.drawsBackground = false + + firstName.cell?.wraps = false + firstName.cell?.isScrollable = true + + firstName.nextKeyView = lastName + firstName.nextResponder = lastName.textView + // lastName.nextKeyView = firstName + + lastName.cell?.wraps = false + lastName.cell?.isScrollable = true + + lastName.font = .medium(14) + firstName.font = .medium(14) + + photoView.layer?.cornerRadius = 50 + updateLocalizationAndTheme(theme: theme) + } + + func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { + + if commandSelector == #selector(insertNewline(_:)) { + trySignUp() + return true + } + + return false + } + + func trySignUp() { + if firstName.stringValue.isEmpty { + firstName.shake() + + if firstName.textView != window?.firstResponder { + window?.makeFirstResponder(firstName.textView) + } + } else { + arguments?.signUp(firstName.stringValue, lastName.stringValue, self.photoUrl) + } + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + + firstNameSeparator.backgroundColor = theme.colors.border + lastNameSeparator.backgroundColor = theme.colors.border + + lastName.placeholderAttributedString = .initialize(string: L10n.loginRegisterLastNamePlaceholder, color: theme.colors.grayText, font: .medium(14)) + firstName.placeholderAttributedString = .initialize(string: L10n.loginRegisterFirstNamePlaceholder, color: theme.colors.grayText, font: .medium(14)) + + lastName.textColor = theme.colors.text + firstName.textColor = theme.colors.text + + let descLayout = TextViewLayout(.initialize(string: L10n.loginRegisterDesc, color: theme.colors.grayText, font: .normal(.text))) + descLayout.measure(width: frame.width) + descView.update(descLayout) + + let addPhotoLayout = TextViewLayout(.initialize(string: L10n.loginRegisterAddPhotoPlaceholder, color: theme.colors.grayText, font: .normal(.text)), alignment: .center) + addPhotoLayout.measure(width: 90) + addPhotoView.update(addPhotoLayout) + + photoView.layer?.borderColor = theme.colors.border.cgColor + photoView.layer?.borderWidth = 1.0 + + + needsLayout = true + } + + override func mouseDown(with event: NSEvent) { + if photoView._mouseInside() { + + let updatePhoto:(URL) -> Void = { [weak self] url in + self?.photoView.image = NSImage.init(contentsOf: url)?.cgImage(forProposedRect: nil, context: nil, hints: nil) + self?.photoUrl = url + } + + filePanel(with: photoExts, allowMultiple: false, canChooseDirectories: false, for: mainWindow, completion: { paths in + if let path = paths?.first, let image = NSImage(contentsOfFile: path) { + _ = (putToTemp(image: image, compress: true) |> deliverOnMainQueue).start(next: { path in + let controller = EditImageModalController(URL(fileURLWithPath: path), settings: .disableSizes(dimensions: .square)) + showModal(with: controller, for: mainWindow, animationType: .scaleCenter) + _ = (controller.result |> deliverOnMainQueue).start(next: { url, _ in + updatePhoto(url) + //arguments.updatePhoto(url.path) + }) + + controller.onClose = { + removeFile(at: path) + } + }) + } + }) + } else { + super.mouseDown(with: event) + } } override func layout() { super.layout() - textView.layout?.measure(width: frame.width - 20) - textView.update(textView.layout) - textView.centerX(y: 30) - button.centerX(y: textView.frame.maxY + 35) + + photoView.frame = NSMakeRect(0, 0, 100, 100) + + addPhotoView.setFrameOrigin(NSMakePoint( floorToScreenPixels(backingScaleFactor, (photoView.frame.width - addPhotoView.frame.width) / 2), floorToScreenPixels(backingScaleFactor, (photoView.frame.height - addPhotoView.frame.height) / 2))) + + firstName.frame = NSMakeRect(photoView.frame.maxX + 10, 20, frame.width - (photoView.frame.maxX + 10), 20) + lastName.frame = NSMakeRect(photoView.frame.maxX + 10, 70, frame.width - (photoView.frame.maxX + 10), 20) + + + firstNameSeparator.frame = NSMakeRect(photoView.frame.maxX + 10, 50, frame.width, .borderSize) + lastNameSeparator.frame = NSMakeRect(photoView.frame.maxX + 10, 100, frame.width, .borderSize) + + descView.centerX(y: photoView.frame.maxY + 50) } required init?(coder: NSCoder) { @@ -74,12 +210,12 @@ private class InputPasswordContainerView : View { required init(frame frameRect: NSRect) { super.init(frame: frameRect) - + input.stringValue = "" input.isBordered = false input.isBezeled = false input.focusRingType = .none - updateLocalizationAndTheme() + updateLocalizationAndTheme(theme: theme) addSubview(input) @@ -88,20 +224,23 @@ private class InputPasswordContainerView : View { input.action = #selector(action) input.target = self - addSubview(passwordLabel) + // addSubview(passwordLabel) } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) let attr = NSMutableAttributedString() - _ = attr.append(string: tr(.loginPasswordPlaceholder), color: .grayText, font: .normal(.title)) + _ = attr.append(string: L10n.loginPasswordPlaceholder, color: theme.colors.grayText, font: .normal(.title)) + input.backgroundColor = theme.colors.background input.placeholderAttributedString = attr - input.font = NSFont.normal(FontSize.text) - input.textColor = .text + input.font = .normal(.text) + input.textColor = theme.colors.text input.sizeToFit() - passwordLabel.attributedString = .initialize(string: tr(.loginYourPasswordLabel), color: .grayText, font: .normal(FontSize.title)) - passwordLabel.sizeToFit() + + needsDisplay = true + + } @objc func action() { @@ -114,7 +253,7 @@ private class InputPasswordContainerView : View { override func draw(_ layer: CALayer, in ctx: CGContext) { super.draw(layer, in: ctx) ctx.setFillColor(theme.colors.border.cgColor) - ctx.fill(NSMakeRect(passwordLabel.frame.maxX + 20, frame.height - .borderSize, frame.width, .borderSize)) + ctx.fill(NSMakeRect(0, frame.height - .borderSize, frame.width, .borderSize)) } required init?(coder: NSCoder) { @@ -140,6 +279,9 @@ private class InputCodeContainerView : View, NSTextFieldDelegate { let textView:TextView = TextView() let delayView:TextView = TextView() + private let forgotPasswordView = TitleButton() + private let resetAccountView = TitleButton() + fileprivate var selectedItem:CountryItem? fileprivate var undo:[String] = [] @@ -161,9 +303,9 @@ private class InputCodeContainerView : View, NSTextFieldDelegate { delayView.isSelectable = false editControl.set(font: .medium(.title), for: .Normal) - editControl.set(color: .blueUI, for: .Normal) - editControl.set(text: tr(.navigationEdit), for: .Normal) - editControl.sizeToFit() + editControl.set(color: theme.colors.accent, for: .Normal) + editControl.set(text: tr(L10n.navigationEdit), for: .Normal) + _ = editControl.sizeToFit() editControl.set(handler: { [weak self] _ in self?.arguments?.editPhone() @@ -172,21 +314,22 @@ private class InputCodeContainerView : View, NSTextFieldDelegate { - addSubview(yourPhoneLabel) - addSubview(codeLabel) + // addSubview(yourPhoneLabel) + // addSubview(codeLabel) addSubview(editControl) - + + - codeText.textColor = .text codeText.font = NSFont.normal(.title) + codeText.textColor = theme.colors.text + numberText.textColor = theme.colors.grayText - numberText.textColor = .grayText numberText.font = NSFont.normal(.title) numberText.isSelectable = false numberText.isEditable = false @@ -214,26 +357,76 @@ private class InputCodeContainerView : View, NSTextFieldDelegate { addSubview(delayView) addSubview(errorLabel) + + addSubview(forgotPasswordView) + addSubview(resetAccountView) + + forgotPasswordView.isHidden = true + resetAccountView.isHidden = true + + forgotPasswordView.set(font: .normal(.title), for: .Normal) + resetAccountView.set(font: .normal(.title), for: .Normal) + + + forgotPasswordView.set(handler: { [weak self] _ in + self?.arguments?.requestPasswordRecovery({ [weak self] option in + self?.resetAccountView.isHidden = false + }) + }, for: .Click) + + resetAccountView.set(handler: { [weak self] _ in + self?.arguments?.resetAccount() + }, for: .Click) } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + + editControl.set(color: theme.colors.accent, for: .Normal) + + + codeText.textColor = theme.colors.text + numberText.textColor = theme.colors.grayText + + yourPhoneLabel.backgroundColor = theme.colors.background + numberText.backgroundColor = theme.colors.background + codeText.backgroundColor = theme.colors.background + - yourPhoneLabel.attributedString = .initialize(string: tr(.loginYourPhoneLabel), color: .grayText, font: NSFont.normal(FontSize.title)) + errorLabel.backgroundColor = theme.colors.background + delayView.backgroundColor = theme.colors.background + textView.backgroundColor = theme.colors.background + + yourPhoneLabel.attributedString = .initialize(string: L10n.loginYourPhoneLabel, color: theme.colors.grayText, font: .normal(.title)) yourPhoneLabel.sizeToFit() - codeLabel.attributedString = .initialize(string: tr(.loginYourCodeLabel), color: .grayText, font: NSFont.normal(FontSize.title)) + codeLabel.attributedString = .initialize(string: L10n.loginYourCodeLabel, color: theme.colors.grayText, font: .normal(.title)) codeLabel.sizeToFit() - numberText.placeholderAttributedString = NSAttributedString.initialize(string: tr(.loginPhoneFieldPlaceholder), color: .grayText, font: NSFont.normal(.header), coreText: false) - codeText.placeholderAttributedString = NSAttributedString.initialize(string: tr(.loginCodePlaceholder), color: .grayText, font: NSFont.normal(.header), coreText: false) + numberText.placeholderAttributedString = .initialize(string: L10n.loginPhoneFieldPlaceholder, color: theme.colors.grayText, font: .normal(.header), coreText: false) + codeText.placeholderAttributedString = .initialize(string: L10n.loginCodePlaceholder, color: theme.colors.grayText, font: .normal(.header), coreText: false) + + + forgotPasswordView.set(color: theme.colors.accent, for: .Normal) + resetAccountView.set(color: theme.colors.redUI, for: .Normal) + + forgotPasswordView.set(text: L10n.loginPasswordForgot, for: .Normal) + resetAccountView.set(text: L10n.loginResetAccountText, for: .Normal) + + + _ = forgotPasswordView.sizeToFit() + _ = resetAccountView.sizeToFit() + + + + needsLayout = true + needsDisplay = true } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - let defaultInset:CGFloat = 30 fileprivate override func layout() { @@ -242,28 +435,38 @@ private class InputCodeContainerView : View, NSTextFieldDelegate { numberText.sizeToFit() - let maxInset = max(yourPhoneLabel.frame.width, codeLabel.frame.width) - let contentInset = maxInset + 20 + 5 + defaultInset - yourPhoneLabel.setFrameOrigin(maxInset - yourPhoneLabel.frame.width + defaultInset, floorToScreenPixels(25 - yourPhoneLabel.frame.height/2)) - codeLabel.setFrameOrigin(maxInset - codeLabel.frame.width + defaultInset, floorToScreenPixels(75 - codeLabel.frame.height/2)) - codeText.setFrameOrigin(contentInset, floorToScreenPixels(75 - codeText.frame.height/2)) + codeText.setFrameOrigin(0, floorToScreenPixels(backingScaleFactor, 75 - codeText.frame.height/2)) + + numberText.setFrameOrigin(0, floorToScreenPixels(backingScaleFactor, 25 - yourPhoneLabel.frame.height/2)) + editControl.setFrameOrigin(frame.width - editControl.frame.width, floorToScreenPixels(backingScaleFactor, 25 - yourPhoneLabel.frame.height/2)) + + + var topOffset: CGFloat = codeText.frame.minY - numberText.setFrameOrigin(contentInset, floorToScreenPixels(25 - yourPhoneLabel.frame.height/2)) - editControl.setFrameOrigin(frame.width - editControl.frame.width, floorToScreenPixels(25 - yourPhoneLabel.frame.height/2)) + if !codeText.isHidden { + topOffset += 50 + } + if numberText.isHidden { + topOffset -= 50 + } + - textView.centerX(y: codeText.frame.maxY + 60 + (passwordEnabled ? inputPassword.frame.height : 0)) + textView.centerX(y: topOffset + 20 + (passwordEnabled ? inputPassword.frame.height : 0)) delayView.centerX(y: textView.frame.maxY + 20) - errorLabel.centerX(y: codeText.frame.maxY + 30 + (passwordEnabled ? inputPassword.frame.height : 0)) + errorLabel.centerX(y: codeText.frame.maxY + 25 + (passwordEnabled ? inputPassword.frame.height : 0)) + + forgotPasswordView.centerX(y: textView.frame.maxY + 10) + resetAccountView.centerX(y: forgotPasswordView.frame.maxY + 5) - inputPassword.passwordLabel.centerY() - inputPassword.passwordLabel.setFrameOrigin(contentInset - inputPassword.passwordLabel.frame.width - 25, inputPassword.passwordLabel.frame.minY) inputPassword.input.setFrameSize(inputPassword.frame.width - inputPassword.passwordLabel.frame.minX, inputPassword.input.frame.height) - inputPassword.input.centerY(x:inputPassword.passwordLabel.frame.maxX + 25) + inputPassword.input.centerY() - inputPassword.setFrameOrigin(0, 101) + + + inputPassword.setFrameOrigin(0, topOffset - 15) } fileprivate func update(with type:SentAuthorizationCodeType, nextType:AuthorizationCodeNextType? = nil, timeout:Int32?) { @@ -304,14 +507,14 @@ private class InputCodeContainerView : View, NSTextFieldDelegate { switch type { case let .otherSession(length: length): codeLength = Int(length) - basic = tr(.loginEnterCodeFromApp) - nextText = tr(.loginSendSmsIfNotReceivedAppCode) + basic = L10n.loginEnterCodeFromApp + nextText = L10n.loginSendSmsIfNotReceivedAppCode case let .sms(length: length): codeLength = Int(length) - basic = tr(.loginJustSentSms) + basic = L10n.loginJustSentSms case let .call(length: length): codeLength = Int(length) - basic = tr(.loginPhoneCalledCode) + basic = L10n.loginPhoneCalledCode default: break } @@ -327,10 +530,10 @@ private class InputCodeContainerView : View, NSTextFieldDelegate { if timeout > 0 { switch nextType { case .call: - nextText = tr(.loginWillCall(minutes, secValue)) + nextText = L10n.loginWillCall(minutes, secValue) break case .sms: - nextText = tr(.loginWillSendSms(minutes, secValue)) + nextText = L10n.loginWillSendSms(minutes, secValue) break default: break @@ -338,8 +541,8 @@ private class InputCodeContainerView : View, NSTextFieldDelegate { } else { switch nextType { case .call: - basic = tr(.loginPhoneCalledCode) - nextText = tr(.loginPhoneDialed) + basic = L10n.loginPhoneCalledCode + nextText = L10n.loginPhoneDialed break default: break @@ -347,11 +550,11 @@ private class InputCodeContainerView : View, NSTextFieldDelegate { } } else { - nextText = tr(.loginSendSmsIfNotReceivedAppCode) + nextText = tr(L10n.loginSendSmsIfNotReceivedAppCode) } } - _ = attr.append(string: basic, color: .grayText, font: .normal(.title)) + _ = attr.append(string: basic, color: theme.colors.grayText, font: .normal(.title)) let textLayout = TextViewLayout(attr, alignment: .center) textLayout.measure(width: 300) textView.update(textLayout) @@ -361,17 +564,17 @@ private class InputCodeContainerView : View, NSTextFieldDelegate { let attr = NSMutableAttributedString() if case .otherSession = type { - _ = attr.append(string: nextText, color: .link , font: .normal(.title)) + _ = attr.append(string: nextText, color: theme.colors.link , font: .normal(.title)) attr.add(link: inAppLink.callback("resend", { [weak self] link in self?.arguments?.resendCode() }), for: attr.range) if timeout == nil { - attr.addAttribute(NSAttributedStringKey.foregroundColor, value: theme.colors.link, range: attr.range) + attr.addAttribute(NSAttributedString.Key.foregroundColor, value: theme.colors.link, range: attr.range) } else if let timeout = timeout { - attr.addAttribute(NSAttributedStringKey.foregroundColor, value: timeout <= 0 ? theme.colors.link : theme.colors.grayText, range: attr.range) + attr.addAttribute(NSAttributedString.Key.foregroundColor, value: timeout <= 0 ? theme.colors.link : theme.colors.grayText, range: attr.range) } } else { - _ = attr.append(string: nextText, color: .grayText, font: .normal(.title)) + _ = attr.append(string: nextText, color: theme.colors.grayText, font: .normal(.title)) } let layout = TextViewLayout(attr) layout.interactions = globalLinkExecutor @@ -385,12 +588,14 @@ private class InputCodeContainerView : View, NSTextFieldDelegate { func update(number: String, type: SentAuthorizationCodeType, hash: String, timeout: Int32?, nextType: AuthorizationCodeNextType?, animated: Bool) { self.passwordEnabled = false - self.numberText.stringValue = number - self.codeText.textColor = .text + self.numberText.stringValue = formatPhoneNumber(number) self.codeText.stringValue = "" self.codeText.isEditable = true self.codeText.isSelectable = true inputPassword.isHidden = true + forgotPasswordView.isHidden = true + resetAccountView.isHidden = true + clearError() self.update(with: type, nextType: nextType, timeout: timeout) } @@ -398,11 +603,13 @@ private class InputCodeContainerView : View, NSTextFieldDelegate { let textError:String switch error { case .limitExceeded: - textError = tr(.loginFloodWait) + textError = L10n.loginFloodWait case .invalidCode: - textError = tr(.phoneCodeInvalid) + textError = L10n.phoneCodeInvalid case .generic: - textError = tr(.phoneCodeExpired) + textError = L10n.phoneCodeExpired + case .codeExpired: + textError = L10n.phoneCodeExpired } errorLabel.state.set(.single(.error(textError))) codeText.shake() @@ -412,9 +619,9 @@ private class InputCodeContainerView : View, NSTextFieldDelegate { let text:String switch error { case .invalidPassword: - text = tr(.passwordHashInvalid) + text = L10n.passwordHashInvalid case .limitExceeded: - text = tr(.loginFloodWait) + text = L10n.loginFloodWait case .generic: text = "undefined error" } @@ -431,22 +638,29 @@ private class InputCodeContainerView : View, NSTextFieldDelegate { func showPasswordInput(_ hint:String, _ number:String, _ code:String, animated: Bool) { errorLabel.state.set(.single(.normal)) self.passwordEnabled = true + + self.codeText.isHidden = code.isEmpty + self.numberText.isHidden = number.isEmpty + self.editControl.isHidden = number.isEmpty + self.numberText.stringValue = number self.codeText.stringValue = code if !hint.isEmpty { - self.inputPassword.input.placeholderAttributedString = NSAttributedString.initialize(string: hint, color: .grayText, font: .normal(.title)) + self.inputPassword.input.placeholderAttributedString = .initialize(string: hint, color: theme.colors.grayText, font: .normal(.title)) } else { - self.inputPassword.input.placeholderAttributedString = NSAttributedString.initialize(string: tr(.loginPasswordPlaceholder), color: .grayText, font: .normal(.title)) + self.inputPassword.input.placeholderAttributedString = .initialize(string: L10n.loginPasswordPlaceholder, color: theme.colors.grayText, font: .normal(.title)) } self.inputPassword.isHidden = false - self.codeText.textColor = .grayText + self.codeText.textColor = theme.colors.grayText self.codeText.isEditable = false self.codeText.isSelectable = false - let textLayout = TextViewLayout(.initialize(string: tr(.loginEnterPasswordDescription), color: .grayText, font: .normal(.title)), alignment: .center) + let textLayout = TextViewLayout(.initialize(string: L10n.loginEnterPasswordDescription, color: theme.colors.grayText, font: .normal(.title)), alignment: .center) textLayout.measure(width: 300) textView.update(textLayout) + + disposable.set(nil) delayView.isHidden = true @@ -456,10 +670,13 @@ private class InputCodeContainerView : View, NSTextFieldDelegate { textView.centerX() textView.change(pos: NSMakePoint(textView.frame.minX, textView.frame.minY + inputPassword.frame.height), animated: animated) + forgotPasswordView.isHidden = false + needsLayout = true + needsDisplay = true } - override func controlTextDidChange(_ obj: Notification) { + func controlTextDidChange(_ obj: Notification) { let code = codeText.stringValue.components(separatedBy: CharacterSet.decimalDigits.inverted).joined().prefix(codeLength) codeText.stringValue = code codeText.sizeToFit() @@ -481,10 +698,13 @@ private class InputCodeContainerView : View, NSTextFieldDelegate { super.draw(layer, in: ctx) - let maxInset = max(yourPhoneLabel.frame.width, codeLabel.frame.width) + 20 ctx.setFillColor(theme.colors.border.cgColor) - ctx.fill(NSMakeRect(defaultInset + maxInset, 50, frame.width - maxInset, .borderSize)) - ctx.fill(NSMakeRect(defaultInset + maxInset, 100, frame.width - maxInset, .borderSize)) + if !self.numberText.isHidden { + ctx.fill(NSMakeRect(0, 50, frame.width, .borderSize)) + } + if !codeText.isHidden { + ctx.fill(NSMakeRect(0, 100, frame.width, .borderSize)) + } } override func setFrameSize(_ newSize: NSSize) { @@ -518,30 +738,29 @@ private class PhoneNumberContainerView : View, NSTextFieldDelegate { super.init(frame: frameRect) - countrySelector.style = ControlStyle(font: NSFont.medium(.title), foregroundColor: NSColor(0x007ee5), backgroundColor:.white) + countrySelector.style = ControlStyle(font: .medium(.title), foregroundColor: theme.colors.accent, backgroundColor: theme.colors.background) countrySelector.set(text: "France", for: .Normal) - countrySelector.sizeToFit() + _ = countrySelector.sizeToFit() addSubview(countrySelector) - - addSubview(countryLabel) - addSubview(numberLabel) + // addSubview(countryLabel) + // addSubview(numberLabel) countrySelector.set(handler: { [weak self] _ in self?.showCountrySelector() }, for: .Click) - updateLocalizationAndTheme() + updateLocalizationAndTheme(theme: theme) codeText.stringValue = "+" - codeText.textColor = .text - codeText.font = NSFont.normal(.title) + - numberText.textColor = .text - numberText.font = NSFont.normal(.title) + codeText.font = .normal(.title) + + numberText.font = .normal(.title) numberText.isBordered = false numberText.isBezeled = false @@ -569,29 +788,42 @@ private class PhoneNumberContainerView : View, NSTextFieldDelegate { } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) - countryLabel.attributedString = .initialize(string: tr(.loginCountryLabel), color: .grayText, font: NSFont.normal(FontSize.title)) + countrySelector.style = ControlStyle(font: .medium(.title), foregroundColor: theme.colors.accent, backgroundColor: theme.colors.background) + + + codeText.backgroundColor = theme.colors.background + numberText.backgroundColor = theme.colors.background + + countryLabel.attributedString = .initialize(string: L10n.loginCountryLabel, color: theme.colors.grayText, font: .normal(.title)) countryLabel.sizeToFit() - numberLabel.attributedString = .initialize(string: tr(.loginYourPhoneLabel), color: .grayText, font: NSFont.normal(FontSize.title)) + numberLabel.attributedString = .initialize(string: L10n.loginYourPhoneLabel, color: theme.colors.grayText, font: .normal(.title)) numberLabel.sizeToFit() - numberText.placeholderAttributedString = NSAttributedString.initialize(string: tr(.loginPhoneFieldPlaceholder), color: .grayText, font: NSFont.normal(.header), coreText: false) + numberText.placeholderAttributedString = .initialize(string: L10n.loginPhoneFieldPlaceholder, color: theme.colors.grayText, font: .normal(.header), coreText: false) needsLayout = true + needsDisplay = true } func setPhoneError(_ error: AuthorizationCodeRequestError) { let text:String switch error { case .invalidPhoneNumber: - text = tr(.phoneNumberInvalid) + text = tr(L10n.phoneNumberInvalid) case .limitExceeded: - text = tr(.loginFloodWait) + text = tr(L10n.loginFloodWait) case .generic: text = "undefined error" + case .phoneLimitExceeded: + text = "undefined error" + case .phoneBanned: + text = "PHONE BANNED" + case .timeout: + text = "timeout" } errorLabel.state.set(.single(.error(text))) } @@ -610,26 +842,26 @@ private class PhoneNumberContainerView : View, NSTextFieldDelegate { codeText.sizeToFit() numberText.sizeToFit() - let maxInset = max(countryLabel.frame.width,numberLabel.frame.width) - let contentInset = maxInset + 20 + 5 - countrySelector.setFrameOrigin(contentInset, floorToScreenPixels(25 - countrySelector.frame.height/2)) + // let maxInset: CGFloat = max(countryLabel.frame.width,numberLabel.frame.width) + // let contentInset = maxInset + 20 + 5 + countrySelector.setFrameOrigin(0, floorToScreenPixels(backingScaleFactor, 25 - countrySelector.frame.height/2)) - countryLabel.setFrameOrigin(maxInset - countryLabel.frame.width, floorToScreenPixels(25 - countryLabel.frame.height/2)) - numberLabel.setFrameOrigin(maxInset - numberLabel.frame.width, floorToScreenPixels(75 - numberLabel.frame.height/2)) + // countryLabel.setFrameOrigin(maxInset - countryLabel.frame.width, floorToScreenPixels(backingScaleFactor, 25 - countryLabel.frame.height/2)) + // numberLabel.setFrameOrigin(maxInset - numberLabel.frame.width, floorToScreenPixels(backingScaleFactor, 75 - numberLabel.frame.height/2)) - codeText.setFrameOrigin(contentInset, floorToScreenPixels(75 - codeText.frame.height/2)) - numberText.setFrameOrigin(contentInset + separatorInset, floorToScreenPixels(75 - codeText.frame.height/2)) - errorLabel.centerX(y: 120) + codeText.setFrameOrigin(0, floorToScreenPixels(backingScaleFactor, 75 - codeText.frame.height/2)) + numberText.setFrameOrigin(separatorInset, floorToScreenPixels(backingScaleFactor, 75 - codeText.frame.height/2)) + errorLabel.centerX(y: 110) } override func draw(_ layer: CALayer, in ctx: CGContext) { super.draw(layer, in: ctx) - let maxInset = max(countryLabel.frame.width,numberLabel.frame.width) + 20 + // let maxInset = max(countryLabel.frame.width,numberLabel.frame.width) + 20 ctx.setFillColor(theme.colors.border.cgColor) - ctx.fill(NSMakeRect(maxInset, 50, frame.width - maxInset, .borderSize)) - ctx.fill(NSMakeRect(maxInset, 100, frame.width - maxInset, .borderSize)) + ctx.fill(NSMakeRect(0, 50, frame.width, .borderSize)) + ctx.fill(NSMakeRect(0, 100, frame.width, .borderSize)) // ctx.fill(NSMakeRect(maxInset + separatorInset, 50, .borderSize, 50)) } @@ -651,9 +883,10 @@ private class PhoneNumberContainerView : View, NSTextFieldDelegate { } - override func controlTextDidChange(_ obj: Notification) { - + func controlTextDidChange(_ obj: Notification) { if let field = obj.object as? NSTextField { + hasChanges = true + let code = codeText.stringValue.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() let dec = code.prefix(4) @@ -661,7 +894,7 @@ private class PhoneNumberContainerView : View, NSTextFieldDelegate { if code.length > 4 { - let list = Array(code.characters).map {String($0)} + let list = code.map {String($0)} let reduced = list.reduce([], { current, value -> [String] in var current = current current.append((current.last ?? "") + value) @@ -704,12 +937,18 @@ private class PhoneNumberContainerView : View, NSTextFieldDelegate { } else if field == numberText { - var formated = formatPhoneNumber(dec + numberText.stringValue.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()) + let current = dec + numberText.stringValue.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() + var formated: String = current + if !current.hasPrefix("99288") { + formated = formatPhoneNumber(current) + } if formated.hasPrefix("+") { formated = formated.fromSuffix(2) } formated = formated.substring(from: dec.endIndex).prefix(17) numberText.stringValue = formated + + self.arguments?.updatePhoneNumberField(formated) } } @@ -744,10 +983,12 @@ private class PhoneNumberContainerView : View, NSTextFieldDelegate { return false } + fileprivate var hasChanges: Bool = false + func update(selectedItem:CountryItem?, update:Bool, updateCode:Bool = true) -> Void { self.selectedItem = selectedItem if update { - countrySelector.set(text: selectedItem?.shortName ?? tr(.loginInvalidCountryCode), for: .Normal) + countrySelector.set(text: selectedItem?.shortName ?? tr(L10n.loginInvalidCountryCode), for: .Normal) countrySelector.sizeToFit() if updateCode { codeText.stringValue = selectedItem != nil ? "+\(selectedItem!.code)" : "+" @@ -766,11 +1007,132 @@ private class PhoneNumberContainerView : View, NSTextFieldDelegate { } + +private func timerValueString(days: Int32, hours: Int32, minutes: Int32) -> String { + var string = NSMutableAttributedString() + + var daysString = "" + if days > 0 { + daysString = "**" + L10n.timerDaysCountable(Int(days)) + "** " + } + + var hoursString = "" + if hours > 0 || days > 0 { + hoursString = "**" + L10n.timerHoursCountable(Int(hours)) + "** " + } + + let minutesString = "**" + L10n.timerMinutesCountable(Int(minutes)) + "**" + + return daysString + hoursString + minutesString +} + +private final class AwaitingResetConfirmationView : View { + private let textView: TextView = TextView() + private let reset: TitleButton = TitleButton() + private var phoneNumber: String = "" + private var protectedUntil: Int32 = 0 + private var timer: SwiftSignalKit.Timer? + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + textView.isSelectable = false + addSubview(textView) + addSubview(reset) + + reset.set(font: .bold(.title), for: .Normal) + } + + func update(with phoneNumber: String, until:Int32, reset: @escaping()-> Void) -> Void { + self.phoneNumber = phoneNumber + self.protectedUntil = until + updateLocalizationAndTheme(theme: theme) + + self.reset.removeAllHandlers() + self.reset.set(handler: { _ in + reset() + }, for: .Click) + + if self.timer == nil { + let timer = SwiftSignalKit.Timer(timeout: 1.0, repeat: true, completion: { [weak self] in + self?.updateTimerValue() + }, queue: Queue.mainQueue()) + self.timer = timer + timer.start() + } + } + + deinit { + timer?.invalidate() + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + + reset.set(color: theme.colors.redUI, for: .Normal) + reset.set(text: L10n.loginResetAccount, for: .Normal) + _ = reset.sizeToFit() + updateTimerValue() + } + + private func updateTimerValue() { + let timerSeconds = max(0, self.protectedUntil - Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)) + + let secondsInAMinute: Int32 = 60 + let secondsInAnHour: Int32 = 60 * secondsInAMinute + let secondsInADay: Int32 = 24 * secondsInAnHour + + let days = timerSeconds / secondsInADay + + let hourSeconds = timerSeconds % secondsInADay + let hours = hourSeconds / secondsInAnHour + + let minuteSeconds = hourSeconds % secondsInAnHour + var minutes = minuteSeconds / secondsInAMinute + + if days == 0 && hours == 0 && minutes == 0 && timerSeconds > 0 { + minutes = 1 + } + + + let attr = NSMutableAttributedString() + + + _ = attr.append(string: L10n.twoStepAuthResetDescription(self.phoneNumber, timerValueString(days: days, hours: hours, minutes: minutes)), color: theme.colors.grayText, font: .normal(.text)) + attr.detectBoldColorInString(with: .bold(.text)) + + let layout = TextViewLayout(attr, alignment: .left, alwaysStaticItems: true) + layout.measure(width: frame.width) + + textView.update(layout) + needsLayout = true + + self.reset.isEnabled = timerSeconds <= 0 + + if timerSeconds <= 0 { + timer?.invalidate() + timer = nil + } + + } + + override func layout() { + super.layout() + textView.centerX() + reset.setFrameOrigin(0, textView.frame.maxY + 20) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + class LoginAuthInfoView : View { fileprivate var state: UnauthorizedAccountStateContents = .empty private let phoneNumberContainer:PhoneNumberContainerView + private let resetAccountContainer:AwaitingResetConfirmationView + private let codeInputContainer:InputCodeContainerView private let signupView:SignupView var arguments:LoginAuthViewArguments? { @@ -785,6 +1147,9 @@ class LoginAuthInfoView : View { var phoneNumber:String { return phoneNumberContainer.codeText.stringValue + phoneNumberContainer.numberText.stringValue } + func trySignUp() { + signupView.trySignUp() + } var code:String { return codeInputContainer.codeText.stringValue @@ -809,12 +1174,20 @@ class LoginAuthInfoView : View { required init(frame frameRect: NSRect) { codeInputContainer = InputCodeContainerView(frame: frameRect) phoneNumberContainer = PhoneNumberContainerView(frame: frameRect) + resetAccountContainer = AwaitingResetConfirmationView(frame: frameRect) signupView = SignupView(frame: frameRect) super.init(frame:frameRect) addSubview(codeInputContainer) addSubview(phoneNumberContainer) addSubview(signupView) + addSubview(resetAccountContainer) + } + + func updateCountryCode(_ code: String) { + if !phoneNumberContainer.hasChanges { + phoneNumberContainer.update(selectedItem: manager.item(bySmallCountryName: code), update: true) + } } @@ -836,6 +1209,11 @@ class LoginAuthInfoView : View { return codeInputContainer.inputPassword.input } return window?.firstResponder + case .signUp: + if window?.firstResponder != signupView.firstName.textView || window?.firstResponder != signupView.lastName.textView { + return signupView.firstName + } + return window?.firstResponder default: return nil } @@ -846,7 +1224,7 @@ class LoginAuthInfoView : View { switch state { case let .phoneEntry(countryCode, phoneNumber): - phoneNumberContainer.updateLocalizationAndTheme() + phoneNumberContainer.updateLocalizationAndTheme(theme: theme) phoneNumberContainer.errorLabel.state.set(.single(.normal)) phoneNumberContainer.update(countryCode: countryCode, number: phoneNumber) phoneNumberContainer.isHidden = false @@ -855,58 +1233,93 @@ class LoginAuthInfoView : View { if completed { self?.codeInputContainer.isHidden = true self?.signupView.isHidden = true + self?.resetAccountContainer.isHidden = true } }) codeInputContainer.change(opacity: 0, animated: animated) + resetAccountContainer.change(opacity: 0, animated: animated) + signupView.change(opacity: 0, animated: animated) case .empty: - phoneNumberContainer.updateLocalizationAndTheme() + phoneNumberContainer.updateLocalizationAndTheme(theme: theme) phoneNumberContainer.isHidden = false phoneNumberContainer.change(opacity: 1, animated: animated, completion: { [weak self] completed in if completed { self?.codeInputContainer.isHidden = true self?.signupView.isHidden = true + self?.resetAccountContainer.isHidden = true } }) - case let .confirmationCodeEntry(number, type, hash, timeout, nextType): - codeInputContainer.updateLocalizationAndTheme() + codeInputContainer.change(opacity: 0, animated: animated) + resetAccountContainer.change(opacity: 0, animated: animated) + signupView.change(opacity: 0, animated: animated) + + case let .confirmationCodeEntry(number, type, hash, timeout, nextType, _): + codeInputContainer.updateLocalizationAndTheme(theme: theme) codeInputContainer.isHidden = false codeInputContainer.undo = [] codeInputContainer.update(number: number, type: type, hash: hash, timeout: timeout, nextType: nextType, animated: animated) phoneNumberContainer.change(opacity: 0, animated: animated) signupView.change(opacity: 0, animated: animated) + resetAccountContainer.change(opacity: 0, animated: animated) + + codeInputContainer.change(opacity: 1, animated: animated, completion: { [weak self] completed in if completed { self?.phoneNumberContainer.isHidden = true self?.signupView.isHidden = true + self?.resetAccountContainer.isHidden = true } }) - case let .passwordEntry(hint, number, code): - codeInputContainer.updateLocalizationAndTheme() + case let .passwordEntry(hint, number, code, _, _): + codeInputContainer.updateLocalizationAndTheme(theme: theme) codeInputContainer.isHidden = false codeInputContainer.showPasswordInput(hint, number ?? "", code ?? "", animated: animated) phoneNumberContainer.change(opacity: 0, animated: animated) signupView.change(opacity: 0, animated: animated) + resetAccountContainer.change(opacity: 0, animated: animated) codeInputContainer.change(opacity: 1, animated: animated, completion: { [weak self] completed in if completed { self?.phoneNumberContainer.isHidden = true self?.signupView.isHidden = true + self?.resetAccountContainer.isHidden = true } }) case .signUp: - signupView.updateLocalizationAndTheme() + signupView.updateLocalizationAndTheme(theme: theme) signupView.isHidden = false phoneNumberContainer.change(opacity: 0, animated: animated) codeInputContainer.change(opacity: 0, animated: animated) - + resetAccountContainer.change(opacity: 0, animated: animated) + signupView.change(opacity: 1, animated: animated, completion: { [weak self] completed in if completed { self?.phoneNumberContainer.isHidden = true self?.codeInputContainer.isHidden = true + self?.resetAccountContainer.isHidden = true } }) + case .passwordRecovery: + //TODO + break + case .awaitingAccountReset(let protectedUntil, let number, _): + resetAccountContainer.isHidden = false + + resetAccountContainer.update(with: number ?? "", until: protectedUntil, reset: { [weak self] in + self?.arguments?.resetAccount() + }) + resetAccountContainer.change(opacity: 1, animated: animated, completion: { [weak self] completed in + if completed { + self?.signupView.isHidden = true + self?.phoneNumberContainer.isHidden = true + self?.codeInputContainer.isHidden = true + } + }) + phoneNumberContainer.change(opacity: 0, animated: animated) + codeInputContainer.change(opacity: 0, animated: animated) + signupView.change(opacity: 0, animated: animated) } window?.makeFirstResponder(firstResponder()) } @@ -917,6 +1330,12 @@ class LoginAuthInfoView : View { phoneNumberContainer.setFrameSize(newSize) codeInputContainer.setFrameSize(newSize) signupView.setFrameSize(newSize) + resetAccountContainer.setFrameSize(newSize) + + phoneNumberContainer.centerX() + codeInputContainer.centerX() + signupView.centerX() + resetAccountContainer.centerX() } diff --git a/Telegram-Mac/LogoutViewController.swift b/Telegram-Mac/LogoutViewController.swift new file mode 100644 index 0000000000..52cec67e1e --- /dev/null +++ b/Telegram-Mac/LogoutViewController.swift @@ -0,0 +1,160 @@ +// +// LogoutViewController.swift +// Telegram +// +// Created by Mikhail Filimonov on 19/02/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + +private struct LogoutControllerState : Equatable { + +} + +private final class LogoutControllerArguments { + let addAccount: ()->Void + let setPasscode: ()->Void + let clearCache: ()->Void + let changePhoneNumber: ()->Void + let contactSupport: ()->Void + let logout: ()->Void + init(addAccount: @escaping()-> Void, setPasscode: @escaping()->Void, clearCache: @escaping()->Void, changePhoneNumber: @escaping()->Void, contactSupport: @escaping()->Void, logout: @escaping()->Void) { + self.addAccount = addAccount + self.setPasscode = setPasscode + self.clearCache = clearCache + self.changePhoneNumber = changePhoneNumber + self.contactSupport = contactSupport + self.logout = logout + } +} + +private let _id_add_account: InputDataIdentifier = InputDataIdentifier("_id_add_account") +private let _id_set_a_passcode: InputDataIdentifier = InputDataIdentifier("_id_set_a_passcode") +private let _id_clear_cache: InputDataIdentifier = InputDataIdentifier("_id_clear_cache") +private let _id_change_phone_number: InputDataIdentifier = InputDataIdentifier("_id_change_phone_number") +private let _id_contact_support: InputDataIdentifier = InputDataIdentifier("_id_contact_support") + +private let _id_log_out: InputDataIdentifier = InputDataIdentifier("_id_log_out") + + +private func logoutEntries(state: LogoutControllerState, activeAccounts: [AccountWithInfo], arguments: LogoutControllerArguments) -> [InputDataEntry] { + var entries: [InputDataEntry] = [] + + var sectionId: Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.logoutOptionsAlternativeOptionsSection), data: InputDataGeneralTextData(viewType: .textTopItem))) + index += 1 + + if activeAccounts.count < 3 { + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_add_account, data: InputDataGeneralData(name: L10n.logoutOptionsAddAccountTitle, color: theme.colors.text, icon: theme.icons.logoutOptionAddAccount, type: .next, viewType: .firstItem, description: L10n.logoutOptionsAddAccountText, action: arguments.addAccount))) + index += 1 + } + + + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_set_a_passcode, data: InputDataGeneralData(name: L10n.logoutOptionsSetPasscodeTitle, color: theme.colors.text, icon: theme.icons.logoutOptionSetPasscode, type: .next, viewType: .innerItem, description: L10n.logoutOptionsSetPasscodeText, action: arguments.setPasscode))) + index += 1 + + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_clear_cache, data: InputDataGeneralData(name: L10n.logoutOptionsClearCacheTitle, color: theme.colors.text, icon: theme.icons.logoutOptionClearCache, type: .next, viewType: .innerItem, description: L10n.logoutOptionsClearCacheText, action: arguments.clearCache))) + index += 1 + + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_change_phone_number, data: InputDataGeneralData(name: L10n.logoutOptionsChangePhoneNumberTitle, color: theme.colors.text, icon: theme.icons.logoutOptionChangePhoneNumber, type: .next, viewType: .innerItem, description: L10n.logoutOptionsChangePhoneNumberText, action: arguments.changePhoneNumber))) + index += 1 + + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_contact_support, data: InputDataGeneralData(name: L10n.logoutOptionsContactSupportTitle, color: theme.colors.text, icon: theme.icons.logoutOptionContactSupport, type: .next, viewType: .lastItem, description: L10n.logoutOptionsContactSupportText, action: arguments.contactSupport))) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + +// entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_log_out, data: InputDataGeneralData(name: L10n.logoutOptionsLogOut, color: theme.colors.redUI, viewType: .singleItem, action: arguments.logout))) +// index += 1 +// +// entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.logoutOptionsLogOutInfo), data: InputDataGeneralTextData(viewType: .textBottomItem))) +// index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + return entries +} + +func LogoutViewController(context: AccountContext, f: @escaping((ViewController)) -> Void) -> InputDataModalController { + + let state: ValuePromise = ValuePromise(LogoutControllerState()) + let stateValue: Atomic = Atomic(value: LogoutControllerState()) + + let updateState:((LogoutControllerState)->LogoutControllerState) -> Void = { f in + state.set(stateValue.modify(f)) + } + + + + let arguments = LogoutControllerArguments(addAccount: { + let testingEnvironment = NSApp.currentEvent?.modifierFlags.contains(.command) == true + context.sharedContext.beginNewAuth(testingEnvironment: testingEnvironment) + }, setPasscode: { + closeAllModals() + f(PasscodeSettingsViewController(context)) + }, clearCache: { + closeAllModals() + f(StorageUsageController(context)) + }, changePhoneNumber: { + closeAllModals() + f(PhoneNumberIntroController(context)) + }, contactSupport: { + confirm(for: mainWindow, information: L10n.accountConfirmAskQuestion, thridTitle: L10n.accountConfirmGoToFaq, successHandler: { result in + closeAllModals() + switch result { + case .basic: + + _ = showModalProgress(signal: context.engine.peers.supportPeerId(), for: mainWindow).start(next: { peerId in + if let peerId = peerId { + f(ChatController(context: context, chatLocation: .peer(peerId))) + } + }) + case .thrid: + openFaq(context: context) + } + }) + }, logout: { + confirm(for: mainWindow, header: L10n.accountConfirmLogout, information: L10n.accountConfirmLogoutText, successHandler: { _ in + closeAllModals() + _ = logoutFromAccount(id: context.account.id, accountManager: context.sharedContext.accountManager, alreadyLoggedOutRemotely: false).start() + }) + }) + + let signal = combineLatest(state.get() |> distinctUntilChanged, context.sharedContext.activeAccountsWithInfo |> map {$0.accounts}) |> map { state, activeAccounts in + return logoutEntries(state: state, activeAccounts: activeAccounts, arguments: arguments) + } + + let controller = InputDataController(dataSignal: signal |> map { InputDataSignalValue(entries: $0) }, title: L10n.logoutOptionsTitle, hasDone: false) + + + let modalController = InputDataModalController(controller, modalInteractions: ModalInteractions(acceptTitle: L10n.logoutOptionsLogOut, accept: { + arguments.logout() + }, drawBorder: true, height: 50, singleButton: true)) + + controller.afterTransaction = { [weak modalController] controller in + modalController?.modalInteractions?.updateDone { button in + button.set(color: theme.colors.redUI, for: .Normal) + } + } + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) + + return modalController +} diff --git a/Telegram-Mac/LottieBufferCompressor.swift b/Telegram-Mac/LottieBufferCompressor.swift new file mode 100644 index 0000000000..48dc4bd6d1 --- /dev/null +++ b/Telegram-Mac/LottieBufferCompressor.swift @@ -0,0 +1,366 @@ +// +// BufferCompressor.swift +// Telegram +// +// Created by Mikhail Filimonov on 19/06/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import Compression +import Accelerate +import Postbox +import SwiftSignalKit +import TGUIKit + +private enum WriteResult { + case success + case failed +} +private enum ReadResult { + case success(Data) + case cached(Data, ()->Void) + case failed +} + +private struct FrameDst : Codable { + let offset: Int + let length: Int + init(offset: Int, length: Int) { + self.offset = offset + self.length = length + } +} + +private var sharedData:Atomic<[LottieAnimationEntryKey:WeakReference]> = Atomic(value: [:]) + + +final class TRLotData { + + + fileprivate var map:[Int : FrameDst] + fileprivate let bufferSize: Int + + private let mapPath: String + private let dataPath: String + + private var readHandle: FileHandle? + private var writeHandle: FileHandle? + private let key: LottieAnimationEntryKey + fileprivate let queue: Queue + + fileprivate func hasAlreadyFrame(_ frame: Int) -> Bool { + assert(queue.isCurrent()) + return self.map[frame] != nil + } + fileprivate func readFrame(frame: Int) -> ReadResult { + + self.writeHandle?.closeFile() + self.writeHandle = nil + assert(queue.isCurrent()) + + if let dest = map[frame] { + let readHande: FileHandle? + if let handle = self.readHandle { + readHande = handle + } else { + readHande = FileHandle(forReadingAtPath: self.dataPath) + self.readHandle = readHande + } + + guard let dataHandle = readHande else { + self.map.removeAll() + return .failed + } + + dataHandle.seek(toFileOffset: UInt64(dest.offset)) + let data = dataHandle.readData(ofLength: dest.length) + if data.count == dest.length { + return .success(data) + } else { + self.map.removeValue(forKey: frame) + return .failed + } + } + + return .failed + } + + deinit { + queue.sync { + self.readHandle?.closeFile() + self.writeHandle?.closeFile() + let data = try? PropertyListEncoder().encode(self.map) + if let data = data { + _ = NSKeyedArchiver.archiveRootObject(data, toFile: self.mapPath) + } + } + _ = sharedData.modify { value in + var value = value + value.removeValue(forKey: self.key) + return value + } + } + + fileprivate func writeFrame(frame: Int, data:Data, endFrame: Int) -> WriteResult { + self.readHandle?.closeFile() + self.readHandle = nil + assert(queue.isCurrent()) + if map[frame] == nil { + let writeHandle: FileHandle? + if let handle = self.writeHandle { + writeHandle = handle + } else { + writeHandle = FileHandle(forWritingAtPath: self.dataPath) + self.writeHandle = writeHandle + } + + guard let dataHandle = writeHandle else { + return .failed + } + let length = dataHandle.seekToEndOfFile() + dataHandle.write(data) + self.map[frame] = FrameDst(offset: Int(length), length: data.count) + } + + return .success + } + + + fileprivate static var directory: String { + let groupPath = ApiEnvironment.containerURL!.path + + let path = groupPath + "/trlottie-animations/" + try? FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) + return path + } + + + static func mapPath(_ animation: LottieAnimation, bufferSize: Int) -> String { + + let path = TRLotData.directory + animation.cacheKey + + return path + "-v7-lzfse-bs\(bufferSize)-lt\(animation.liveTime)-map" + } + + static func dataPath(_ animation: LottieAnimation, bufferSize: Int) -> String { + let path = TRLotData.directory + animation.cacheKey + + return path + "-v7-lzfse-bs\(bufferSize)-lt\(animation.liveTime)-data" + } + + init(_ animation: LottieAnimation, endFrame: Int, bufferSize: Int, queue: Queue) { + self.queue = queue + self.mapPath = TRLotData.mapPath(animation, bufferSize: bufferSize) + self.dataPath = TRLotData.dataPath(animation, bufferSize: bufferSize) + self.key = animation.key + var mapHandle:FileHandle? + + let deferr:(TRLotData)->Void = { data in + if !FileManager.default.fileExists(atPath: data.mapPath) { + FileManager.default.createFile(atPath: data.mapPath, contents: nil, attributes: nil) + } + if !FileManager.default.fileExists(atPath: data.dataPath) { + FileManager.default.createFile(atPath: data.dataPath, contents: nil, attributes: nil) + } + try? FileManager.default.setAttributes([.modificationDate : Date()], ofItemAtPath: data.mapPath) + try? FileManager.default.setAttributes([.modificationDate : Date()], ofItemAtPath: data.dataPath) + + _ = sharedData.modify { value in + var value = value + value[data.key] = WeakReference(value: data) + return value + } + mapHandle?.closeFile() + } + + guard let handle = FileHandle(forReadingAtPath: self.mapPath) else { + self.map = [:] + self.bufferSize = bufferSize + deferr(self) + return + } + mapHandle = handle + + guard let data = NSKeyedUnarchiver.unarchiveObject(withFile: self.mapPath) as? Data else { + self.map = [:] + self.bufferSize = bufferSize + deferr(self) + return + } + do { + self.map = try PropertyListDecoder().decode([Int: FrameDst].self, from: data) + self.bufferSize = bufferSize + deferr(self) + } catch { + self.map = [:] + self.bufferSize = bufferSize + deferr(self) + } + + + } + +} + +private let lzfseQueue = Queue(name: "LZFSE BUFFER Queue", qos: DispatchQoS.default) + + +final class TRLotFileSupplyment { + fileprivate let bufferSize: Int + fileprivate let data:TRLotData + fileprivate let queue: Queue + fileprivate var shouldWaitToRead: [Int:Int] = [:] + + init(_ animation:LottieAnimation, bufferSize: Int, frames: Int, queue: Queue) { + let cached = sharedData.with { $0[animation.key]?.value } + let queue = cached?.queue ?? queue + self.data = cached ?? TRLotData(animation, endFrame: frames, bufferSize: bufferSize, queue: queue) + self.queue = queue + for value in self.data.map { + shouldWaitToRead[value.key] = value.key + } + self.bufferSize = bufferSize + } + func addFrame(_ previous: RenderedFrame?, _ current: RenderedFrame, endFrame: Int) { + if shouldWaitToRead[Int(current.frame)] == nil { + shouldWaitToRead[Int(current.frame)] = Int(current.frame) + queue.async { + if !self.data.hasAlreadyFrame(Int(current.frame)) { + let address = current.data!.assumingMemoryBound(to: UInt8.self) + + let dst: UnsafeMutablePointer = malloc(self.bufferSize)!.assumingMemoryBound(to: UInt8.self) + var length:Int = self.bufferSize + if let previous = previous { + let uint64Bs = self.bufferSize / 8 + let dstDelta: UnsafeMutablePointer = malloc(self.bufferSize)!.assumingMemoryBound(to: UInt8.self) + memcpy(dstDelta, previous.data!.assumingMemoryBound(to: UInt8.self), self.bufferSize) + + + let ui64Dst = dstDelta.withMemoryRebound(to: UInt64.self, capacity: uint64Bs, { previousBytes in + return previousBytes + }) + + let ui64Address = address.withMemoryRebound(to: UInt64.self, capacity: uint64Bs, { address in + return address + }) + + + var i: Int = 0 + while i < uint64Bs { + ui64Dst[i] = ui64Dst[i] ^ ui64Address[i] + i &+= 1 + } + + let ui8 = ui64Dst.withMemoryRebound(to: UInt8.self, capacity: self.bufferSize, { body in + return body + }) + + length = compression_encode_buffer(dst, self.bufferSize, ui8, self.bufferSize, nil, COMPRESSION_LZ4) + dstDelta.deallocate() + } else { + length = compression_encode_buffer(dst, self.bufferSize, address, self.bufferSize, nil, COMPRESSION_LZ4) + } + let _ = self.data.writeFrame(frame: Int(current.frame), data: Data(bytesNoCopy: dst, count: length, deallocator: .none), endFrame: endFrame) + dst.deallocate() + } + } + } + } + + func readFrame(previous: RenderedFrame?, frame: Int) -> UnsafeRawPointer? { + var rendered: UnsafeRawPointer? = nil + if shouldWaitToRead[frame] != nil { + queue.sync { + switch self.data.readFrame(frame: frame) { + case let .success(data): + let address = malloc(bufferSize)!.assumingMemoryBound(to: UInt8.self) + + + rendered = data.withUnsafeBytes { dataBytes -> UnsafeRawPointer in + + let unsafeBufferPointer = dataBytes.bindMemory(to: UInt8.self) + let unsafePointer = unsafeBufferPointer.baseAddress! + + let _ = compression_decode_buffer(address, bufferSize, unsafePointer, data.count, nil, COMPRESSION_LZ4) + + if let previous = previous { + + let previousBytes = previous.data!.assumingMemoryBound(to: UInt64.self) + + let uint64Bs = self.bufferSize / 8 + + address.withMemoryRebound(to: UInt64.self, capacity: uint64Bs, { address in + var i = 0 + while i < uint64Bs { + address[i] = previousBytes[i] ^ address[i] + i &+= 1 + } + }) + + } + return UnsafeRawPointer(address) + } + + default: + rendered = nil + } + } + } + + return rendered + } + + +} + + + + +private final class CacheRemovable { + init() { + + } + + fileprivate func start() { + let signal = Signal.single(Void()) |> deliverOn(lzfseQueue) |> then (Signal.single(Void()) |> delay(30 * 60, queue: lzfseQueue) |> restart) + + + _ = signal.start(next: { + self.clean() + }) + } + + private func clean() { + + let fileURLs = try? FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: TRLotData.directory), includingPropertiesForKeys: nil, options: .skipsHiddenFiles ) + if let fileURLs = fileURLs { + for url in fileURLs { + let path = url.path + let name = path.nsstring.lastPathComponent + if let index = name.range(of: "lt") { + let tail = String(name[index.upperBound...]) + if let until = tail.range(of: "-") { + if let liveTime = TimeInterval(tail[.. { + private var lock: pthread_mutex_t + private var value: T + + public init(value: T) { + self.lock = pthread_mutex_t() + self.value = value + pthread_mutex_init(&self.lock, nil) + } + + deinit { + pthread_mutex_destroy(&self.lock) + } + + public func with(_ f: (T) -> R) -> R { + pthread_mutex_lock(&self.lock) + let result = f(self.value) + pthread_mutex_unlock(&self.lock) + + return result + } + + public func modify(_ f: (T) -> T) -> T { + pthread_mutex_lock(&self.lock) + let result = f(self.value) + self.value = result + pthread_mutex_unlock(&self.lock) + + return result + } + + public func swap(_ value: T) -> T { + pthread_mutex_lock(&self.lock) + let previous = self.value + self.value = value + pthread_mutex_unlock(&self.lock) + + return previous + } +} + + +let lottieThreadPool: ThreadPool = ThreadPool(threadCount: 1, threadPriority: 0.1) +private let stateQueue = Queue() + + + +enum LottiePlayerState : Equatable { + case initializing + case failed + case playing + case stoped +} + +protocol RenderedFrame { + var duration: TimeInterval { get } + var data: UnsafeRawPointer? { get } + var image: CGImage? { get } + var backingScale: Int { get } + var size: NSSize { get } + var key: LottieAnimationEntryKey { get } + var frame: Int32 { get } +} + +final class RenderedWebpFrame : RenderedFrame, Equatable { + + let frame: Int32 + let size: NSSize + let backingScale: Int + let key: LottieAnimationEntryKey + private let webpData: WebPImageFrame + init(key: LottieAnimationEntryKey, frame: Int32, size: NSSize, webpData: WebPImageFrame, backingScale: Int) { + self.key = key + self.backingScale = backingScale + self.size = size + self.frame = frame + self.webpData = webpData + } + var image: CGImage? { + return webpData.image?._cgImage + } + var duration: TimeInterval { + return webpData.duration + } + var data: UnsafeRawPointer? { + return nil + } + static func == (lhs: RenderedWebpFrame, rhs: RenderedWebpFrame) -> Bool { + return lhs.key == rhs.key + } +} + +final class RenderedLottieFrame : RenderedFrame, Equatable { + let frame: Int32 + let data: UnsafeRawPointer? + let size: NSSize + let backingScale: Int + let key: LottieAnimationEntryKey + let fps: Int + init(key: LottieAnimationEntryKey, fps: Int, frame: Int32, size: NSSize, data: UnsafeRawPointer, backingScale: Int) { + self.key = key + self.frame = frame + self.size = size + self.data = data + self.backingScale = backingScale + self.fps = fps + } + static func ==(lhs: RenderedLottieFrame, rhs: RenderedLottieFrame) -> Bool { + return lhs.frame == rhs.frame + } + + var bufferSize: Int { + return Int(size.width * CGFloat(backingScale) * size.height * CGFloat(backingScale) * 4) + } + + var duration: TimeInterval { + return 1.0 / Double(self.fps) + } + var image: CGImage? { + if let data = data { + return generateImagePixel(size, scale: CGFloat(backingScale), pixelGenerator: { (_, pixelData) in + memcpy(pixelData, data, bufferSize) + }) + } + return nil + } + + + deinit { + data?.deallocate() + +// _ = sharedFrames.modify { value in +// var value = value +// if var shared = value[key] { +// shared.removeValue(forKey: frame) +// if shared.isEmpty { +// value.removeValue(forKey: key) +// } else { +// value[key] = shared +// } +// } +// return value +// } + + } +} + +//private var sharedFrames:RenderAtomic<[LottieAnimationEntryKey : [Int32: WeakReference]]> = RenderAtomic(value: [:]) + + + + +private final class RendererState { + fileprivate let animation: LottieAnimation + private(set) var frames: [RenderedFrame] + private(set) var previousFrame:RenderedFrame? + private(set) var cachedFrames:[Int32 : RenderedFrame] + private(set) var currentFrame: Int32 + private(set) var startFrame:Int32 + private(set) var endFrame: Int32 + private(set) var cancelled: Bool + private(set) weak var container: RenderContainer? + private(set) var renderIndex: Int32? + init(cancelled: Bool, animation: LottieAnimation, container: RenderContainer?, frames: [RenderedLottieFrame], cachedFrames: [Int32 : RenderedLottieFrame], currentFrame: Int32, startFrame: Int32, endFrame: Int32) { + self.animation = animation + self.cancelled = cancelled + self.container = container + self.frames = frames + self.cachedFrames = cachedFrames + self.currentFrame = currentFrame + self.startFrame = startFrame + self.endFrame = endFrame + } + func withUpdatedFrames(_ frames: [RenderedFrame]) -> RendererState { + self.frames = frames + return self + } + func withAddedFrame(_ frame: RenderedFrame) { + + let prev = frame.frame == 0 ? nil : self.frames.last ?? previousFrame + self.container?.cacheFrame(prev, frame) +// _ = sharedFrames.modify { value in +// var value = value +// if value[self.animation.key] == nil { +// value[self.animation.key] = [:] +// } +// value[self.animation.key]?[frame.frame] = WeakReference(value: frame) +// return value +// } + self.frames = self.frames + [frame] + } + + func withUpdatedCurrentFrame(_ currentFrame: Int32) -> RendererState { + self.currentFrame = currentFrame + return self + } + + func takeFirst() -> RenderedFrame { + var frames = self.frames + if frames.first?.frame == endFrame { + self.previousFrame = nil + } else { + self.previousFrame = frames.last + } + let prev = frames.removeFirst() + self.renderIndex = prev.frame + self.frames = frames + return prev + } + + func renderFrame(at frame: Int32) -> RenderedFrame? { + return container?.render(at: frame, frames: frames, previousFrame: previousFrame) + } + + deinit { + + } + + func cancel() -> RendererState { + self.cancelled = true + + return self + } +} + +final class LottieSoundEffect { + private let player: MediaPlayer + let triggerOn: Int32? + + private(set) var isPlayable: Bool = false + + init(file: TelegramMediaFile, postbox: Postbox, triggerOn: Int32?) { + self.player = MediaPlayer(postbox: postbox, reference: MediaResourceReference.standalone(resource: file.resource), streamable: false, video: false, preferSoftwareDecoding: false, enableSound: true, baseRate: 1.0, fetchAutomatically: true) + self.triggerOn = triggerOn + } + func play() { + if isPlayable { + self.player.play() + isPlayable = false + } + } + + func markAsPlayable() -> Void { + isPlayable = true + } +} + +protocol Renderer { + func render(at frame: Int32) -> RenderedFrame +} + +private let maximum_rendered_frames: Int = 4 +private final class PlayerRenderer { + + private var soundEffect: LottieSoundEffect? + + private(set) var finished: Bool = false + private var animation: LottieAnimation + private var layer: Atomic = Atomic(value: nil) + private let updateState:(LottiePlayerState)->Void + private let displayFrame: (RenderedFrame, LottieRunLoop)->Void + private var timer: SwiftSignalKit.Timer? + private let release:()->Void + private var maxRefreshRate: Int = 60 + init(animation: LottieAnimation, displayFrame: @escaping(RenderedFrame, LottieRunLoop)->Void, release:@escaping()->Void, updateState:@escaping(LottiePlayerState)->Void) { + self.animation = animation + self.displayFrame = displayFrame + self.updateState = updateState + self.release = release + self.soundEffect = animation.soundEffect + } + + private var onDispose: (()->Void)? + deinit { + self.timer?.invalidate() + self.onDispose?() + _ = self.layer.swap(nil) + self.release() + self.updateState(.stoped) + } + + + func initializeAndPlay(maxRefreshRate: Int) { + self.maxRefreshRate = maxRefreshRate + self.updateState(.initializing) + assert(animation.runOnQueue.isCurrent()) + + let container = self.animation.initialize() + + if let container = container { + self.play(self.layer.modify({_ in container })!) + } else { + self.updateState(.failed) + } + } + + func playAgain() { + self.layer.with { container -> Void in + if let container = container { + self.play(container) + } + } + } + + func playSoundEffect() { + self.soundEffect?.markAsPlayable() + } + + func updateSize(_ size: NSSize) { + self.animation = self.animation.withUpdatedSize(size) + } + + func setColors(_ colors: [LottieColor]) { + self.layer.with { container -> Void in + for color in colors { + container?.setColor(color.color, keyPath: color.keyPath) + } + } + } + + private var getCurrentFrame:()->Int32? = { return nil } + var currentFrame: Int32? { + return self.getCurrentFrame() + } + private var getTotalFrames:()->Int32? = { return nil } + var totalFrames: Int32? { + return self.getTotalFrames() + } + private func play(_ player: RenderContainer) { + + self.finished = false + + let runOnQueue = animation.runOnQueue + + let maximum_renderer_frames: Int = Thread.isMainThread ? 2 : maximum_rendered_frames + + let fps: Int = min(player.fps, max(30, maxRefreshRate)) + let mainFps: Int = player.mainFps + + let maxFrames:Int32 = 180 + var currentFrame: Int32 = 0 + var startFrame: Int32 = min(min(player.startFrame, maxFrames), min(player.endFrame, maxFrames)) + var endFrame: Int32 = min(player.endFrame, maxFrames) + switch self.animation.playPolicy { + case let .loopAt(firstStart, range): + startFrame = range.lowerBound + endFrame = range.upperBound + if let firstStart = firstStart { + currentFrame = firstStart + } + case let .toEnd(from): + startFrame = max(min(from, endFrame - 1), startFrame) + currentFrame = max(min(from, endFrame - 1), startFrame) + case let .toStart(from): + startFrame = 1 + + currentFrame = max(min(from, endFrame - 1), startFrame) + default: + break + } + + let initialState = RendererState(cancelled: false, animation: self.animation, container: player, frames: [], cachedFrames: [:], currentFrame: currentFrame, startFrame: startFrame, endFrame: endFrame) + + let stateValue:RenderAtomic = RenderAtomic(value: initialState) + let updateState:(_ f:(RendererState?)->RendererState?)->Void = { f in + _ = stateValue.modify(f) + } + + self.getCurrentFrame = { [weak stateValue] in + return stateValue?.with { $0?.renderIndex } + } + self.getTotalFrames = { [weak stateValue] in + return stateValue?.with { $0?.endFrame } + } + + var framesTask: ThreadPoolTask? = nil + + let isRendering: Atomic = Atomic(value: false) + + self.onDispose = { + updateState { + $0?.cancel() + } + framesTask?.cancel() + framesTask = nil + _ = stateValue.swap(nil) + } + + let currentState:(_ state: RenderAtomic) -> RendererState? = { state in + return state.with { $0 } + } + + var renderNext:(()->Void)? = nil + + var add_frames_impl:(()->Void)? = nil + var askedRender: Bool = false + var playedCount: Int32 = 0 + let render:()->Void = { [weak self] in + var hungry: Bool = false + var cancelled: Bool = false + if let renderer = self { + var current: RenderedFrame? + updateState { stateValue in + guard let state = stateValue, !state.frames.isEmpty else { + return stateValue + } + current = state.takeFirst() + hungry = state.frames.count < maximum_renderer_frames - 1 + cancelled = state.cancelled + return state + } + + if !cancelled { + if let current = current { + let displayFrame = renderer.displayFrame + let updateState = renderer.updateState + displayFrame(current, .init(fps: fps)) + playedCount += 1 + if current.frame > 0 { + updateState(.playing) + } + if let soundEffect = renderer.soundEffect { + if let triggerOn = soundEffect.triggerOn { + let triggers:[Int32] = [triggerOn - 1, triggerOn, triggerOn + 1] + if triggers.contains(current.frame) { + soundEffect.play() + } + } else { + if current.frame == 0 { + soundEffect.play() + } + } + } + if let triggerOn = renderer.animation.triggerOn { + switch triggerOn.0 { + case .first: + if currentState(stateValue)?.startFrame == current.frame { + DispatchQueue.main.async(execute: triggerOn.1) + } + case .last: + if endFrame - 1 == current.frame { + DispatchQueue.main.async(execute: triggerOn.1) + } + case let .custom(index): + if index == current.frame { + DispatchQueue.main.async(execute: triggerOn.1) + } + } + + } + + let finish:()->Void = { + renderer.finished = true + cancelled = true + updateState(.stoped) + renderer.timer?.invalidate() + framesTask?.cancel() + let onFinish = renderer.animation.onFinish ?? {} + DispatchQueue.main.async(execute: onFinish) + } + + switch renderer.animation.playPolicy { + case .loop, .loopAt: + break + case .once: + if current.frame + 1 == currentState(stateValue)?.endFrame { + finish() + } + case .onceEnd, .toEnd: + if let state = currentState(stateValue), state.endFrame - current.frame <= 1 { + finish() + } + case .toStart: + if current.frame <= 1, playedCount > 1 { + finish() + } + case let .framesCount(limit): + if limit <= playedCount { + finish() + } + case let .onceToFrame(frame): + if frame <= current.frame { + finish() + } + } + + } + if !renderer.finished { + let duration = current?.duration ?? (1.0 / TimeInterval(fps)) + if duration > 0, (renderer.totalFrames ?? 0) > 1 { + renderer.timer = SwiftSignalKit.Timer(timeout: duration, repeat: false, completion: { + renderNext?() + }, queue: runOnQueue) + + renderer.timer?.start() + } + + } + } + let isRendering = isRendering.with { $0 } + if hungry && !isRendering && !cancelled && !askedRender { + askedRender = true + add_frames_impl?() + } + } + + } + + renderNext = { + render() + } + + var firstTimeRendered: Bool = true + + let maximum = Int(initialState.endFrame - initialState.startFrame) + framesTask = ThreadPoolTask { state in + _ = isRendering.swap(true) + while !state.cancelled.with({$0}) && (currentState(stateValue)?.frames.count ?? Int.max) < min(maximum_renderer_frames, maximum) { + + let currentFrame = stateValue.with { $0?.currentFrame ?? 0 } + + let value = stateValue.with { $0 } + + let frame: RenderedFrame? + if let value = value { + frame = value.renderFrame(at: currentFrame) + } else { + frame = nil + } + + _ = stateValue.modify { stateValue -> RendererState? in + guard let state = stateValue else { + return stateValue + } + var currentFrame = state.currentFrame + + if mainFps >= fps { + if currentFrame % Int32(round(Float(mainFps) / Float(fps))) != 0 { + currentFrame += 1 + } + } else { + currentFrame += 1 + } + + if currentFrame >= state.endFrame - 1 { + currentFrame = state.startFrame - 1 + } + if let frame = frame { + state.withAddedFrame(frame) + } + return state.withUpdatedCurrentFrame(currentFrame + 1) + } + if frame == nil { + break + } + } + _ = isRendering.swap(false) + runOnQueue.async { + askedRender = false + if firstTimeRendered { + firstTimeRendered = false + render() + } + } + } + + let add_frames:()->Void = { + if let framesTask = framesTask { + if Thread.isMainThread { + framesTask.execute() + } else { + lottieThreadPool.addTask(framesTask) + } + } + } + + add_frames_impl = { + add_frames() + } + add_frames() + + } + +} + +private final class PlayerContext { + private let rendererRef: QueueLocalObject + fileprivate let animation: LottieAnimation + init(_ animation: LottieAnimation, maxRefreshRate: Int = 60, displayFrame: @escaping(RenderedFrame, LottieRunLoop)->Void, release:@escaping()->Void, updateState: @escaping(LottiePlayerState)->Void) { + self.animation = animation + self.rendererRef = QueueLocalObject.init(queue: animation.runOnQueue, generate: { + return PlayerRenderer(animation: animation, displayFrame: displayFrame, release: release, updateState: { state in + Queue.mainQueue().async { + updateState(state) + } + }) + }) + + self.rendererRef.with { renderer in + renderer.initializeAndPlay(maxRefreshRate: maxRefreshRate) + } + } + + func playAgain() { + self.rendererRef.with { renderer in + if renderer.finished { + renderer.playAgain() + } + } + } + + func setColors(_ colors: [LottieColor]) { + self.rendererRef.with { renderer in + renderer.setColors(colors) + } + } + + func playSoundEffect() { + self.rendererRef.with { renderer in + renderer.playSoundEffect() + } + } + func updateSize(_ size: NSSize) { + self.rendererRef.syncWith { renderer in + renderer.updateSize(size) + } + } + var currentFrame:Int32? { + var currentFrame:Int32? = nil + self.rendererRef.syncWith { renderer in + currentFrame = renderer.currentFrame + } + return currentFrame + } + var totalFrames:Int32? { + var totalFrames:Int32? = nil + self.rendererRef.syncWith { renderer in + totalFrames = renderer.totalFrames + } + return totalFrames + } +} + + +enum ASLiveTime : Int { + case chat = 3_600 + case thumb = 259200 + case effect = 241_920 // 7 days +} + +enum ASCachePurpose { + case none + case temporaryLZ4(ASLiveTime) +} + +struct LottieAnimationEntryKey : Hashable { + let size: CGSize + let backingScale: Int + let key:LottieAnimationKey + let fitzModifier: EmojiFitzModifier? + let colors: [LottieColor] + init(key: LottieAnimationKey, size: CGSize, backingScale: Int = Int(System.backingScale), fitzModifier: EmojiFitzModifier? = nil, colors: [LottieColor] = []) { + self.key = key + self.size = size + self.backingScale = backingScale + self.fitzModifier = fitzModifier + self.colors = colors + } + + func withUpdatedColors(_ colors: [LottieColor]) -> LottieAnimationEntryKey { + return LottieAnimationEntryKey(key: key, size: size, backingScale: backingScale, fitzModifier: fitzModifier, colors: colors) + } + func withUpdatedBackingScale(_ backingScale: Int) -> LottieAnimationEntryKey { + return LottieAnimationEntryKey(key: key, size: size, backingScale: backingScale, fitzModifier: fitzModifier, colors: colors) + } + func withUpdatedSize(_ size: CGSize) -> LottieAnimationEntryKey { + return LottieAnimationEntryKey(key: key, size: size, backingScale: backingScale, fitzModifier: fitzModifier, colors: colors) + } + + func hash(into hasher: inout Hasher) { + + } +} + +enum LottieAnimationKey : Hashable { + case media(MediaId?) + case bundle(String) +} + +enum LottiePlayPolicy : Equatable { + case loop + case loopAt(firstStart:Int32?, range: ClosedRange) + case once + case onceEnd + case toEnd(from: Int32) + case toStart(from: Int32) + case framesCount(Int32) + case onceToFrame(Int32) + + static func ==(lhs: LottiePlayPolicy, rhs: LottiePlayPolicy) -> Bool { + switch lhs { + case .loop: + if case .loop = rhs { + return true + } + case let .loopAt(firstStart, range): + if case .loopAt(firstStart, range) = rhs { + return true + } + case .once: + if case .once = rhs { + return true + } + case .onceEnd: + if case .onceEnd = rhs { + return true + } + case .toEnd: + if case .toEnd = rhs { + return true + } + case .toStart: + if case .toStart = rhs { + return true + } + case let .framesCount(count): + if case .framesCount(count) = rhs { + return true + } + case let .onceToFrame(count): + if case .onceToFrame(count) = rhs { + return true + } + } + return false + } +} + +struct LottieColor : Equatable { + let keyPath: String + let color: NSColor +} + +enum LottiePlayerTriggerFrame : Equatable { + case first + case last + case custom(Int32) +} + +private protocol RenderContainer : class { + func render(at frame: Int32, frames: [RenderedFrame], previousFrame: RenderedFrame?) -> RenderedFrame? + func cacheFrame(_ previous: RenderedFrame?, _ current: RenderedFrame) + func setColor(_ color: NSColor, keyPath: String) + + var endFrame: Int32 { get } + var startFrame: Int32 { get } + + var fps: Int { get } + var mainFps: Int { get } + +} + +private final class WebPRenderer : RenderContainer { + + private let animation: LottieAnimation + private let decoder: WebPImageDecoder + + init(animation: LottieAnimation, decoder: WebPImageDecoder) { + self.animation = animation + self.decoder = decoder + } + + func render(at frame: Int32, frames: [RenderedFrame], previousFrame: RenderedFrame?) -> RenderedFrame? { + if let webpFrame = self.decoder.frame(at: UInt(frame), decodeForDisplay: true) { + return RenderedWebpFrame(key: animation.key, frame: frame, size: animation.size, webpData: webpFrame, backingScale: animation.backingScale) + } else { + return nil + } + } + func cacheFrame(_ previous: RenderedFrame?, _ current: RenderedFrame) { + + } + func setColor(_ color: NSColor, keyPath: String) { + + } + var endFrame: Int32 { + return Int32(decoder.frameCount) + } + var startFrame: Int32 { + return 0 + } + var fps: Int { + return 1 + } + var mainFps: Int { + return 1 + } +} + +private final class LottieRenderer : RenderContainer { + + private let animation: LottieAnimation + private let bridge: RLottieBridge + private let fileSupplyment: TRLotFileSupplyment? + + init(animation: LottieAnimation, bridge: RLottieBridge, fileSupplyment: TRLotFileSupplyment?) { + self.animation = animation + self.bridge = bridge + self.fileSupplyment = fileSupplyment + } + var fps: Int { + return max(min(Int(bridge.fps()), self.animation.maximumFps), 24) + } + var mainFps: Int { + return Int(bridge.fps()) + } + var endFrame: Int32 { + return bridge.endFrame() + } + var startFrame: Int32 { + return bridge.startFrame() + } + + func setColor(_ color: NSColor, keyPath: String) { + self.bridge.setColor(color, forKeyPath: keyPath) + } + + func cacheFrame(_ previous: RenderedFrame?, _ current: RenderedFrame) { + if let fileSupplyment = fileSupplyment { + fileSupplyment.addFrame(previous, current, endFrame: Int(endFrame)) + } + } + + func render(at frame: Int32, frames: [RenderedFrame], previousFrame: RenderedFrame?) -> RenderedFrame? { + let s:(w: Int, h: Int) = (w: Int(animation.size.width) * animation.backingScale, h: Int(animation.size.height) * animation.backingScale) + + var data: UnsafeRawPointer? + +// let sharedFrame = sharedFrames.with { value -> RenderedLottieFrame? in +// return value[animation.key]?[frame]?.value +// } +// +// if let sharedFrame = sharedFrame { +// return sharedFrame +// } +// + if let fileSupplyment = fileSupplyment { + let previous = frame == startFrame ? nil : frames.last ?? previousFrame + if let frame = fileSupplyment.readFrame(previous: previous, frame: Int(frame)) { + data = frame + } + } + if data == nil { + let bufferSize = s.w * s.h * 4 + let memoryData = malloc(bufferSize)! + let frameData = memoryData.assumingMemoryBound(to: UInt8.self) + bridge.renderFrame(with: frame, into: frameData, width: Int32(s.w), height: Int32(s.h)) + data = UnsafeRawPointer(frameData) + } + if let data = data { + return RenderedLottieFrame(key: animation.key, fps: fps, frame: frame, size: animation.size, data: data, backingScale: self.animation.backingScale) + } + + return nil + } + + deinit { + var bp:Int = 0 + bp += 1 + } +} + +enum LottieAnimationType { + case lottie + case webp +} + +final class LottieAnimation : Equatable { + static func == (lhs: LottieAnimation, rhs: LottieAnimation) -> Bool { + return lhs.key == rhs.key && lhs.playPolicy == rhs.playPolicy && lhs.colors == rhs.colors + } + + let type: LottieAnimationType + + var liveTime: Int { + switch cache { + case .none: + return 0 + case let .temporaryLZ4(liveTime): + return liveTime.rawValue + } + } + + var supportsMetal: Bool { + switch type { + case .lottie: + return true + default: + return false + } + } + + let compressed: Data + let key: LottieAnimationEntryKey + let cache: ASCachePurpose + let maximumFps: Int + let playPolicy: LottiePlayPolicy + let colors:[LottieColor] + let soundEffect: LottieSoundEffect? + let postbox: Postbox? + let runOnQueue: Queue + var onFinish:(()->Void)? + + var triggerOn:(LottiePlayerTriggerFrame, ()->Void, ()->Void)? + + + init(compressed: Data, key: LottieAnimationEntryKey, type: LottieAnimationType = .lottie, cachePurpose: ASCachePurpose = .temporaryLZ4(.thumb), playPolicy: LottiePlayPolicy = .loop, maximumFps: Int = 60, colors: [LottieColor] = [], soundEffect: LottieSoundEffect? = nil, postbox: Postbox? = nil, runOnQueue: Queue = stateQueue) { + self.compressed = compressed + self.key = key.withUpdatedColors(colors) + self.cache = cachePurpose + self.maximumFps = maximumFps + self.playPolicy = playPolicy + self.colors = colors + self.postbox = postbox + self.soundEffect = soundEffect + self.runOnQueue = runOnQueue + self.type = type + } + + var size: NSSize { + let size = key.size + return size + } + var viewSize: NSSize { + return key.size + } + var backingScale: Int { + return key.backingScale + } + + func withUpdatedBackingScale(_ scale: Int) -> LottieAnimation { + return LottieAnimation(compressed: self.compressed, key: self.key.withUpdatedBackingScale(scale), cachePurpose: self.cache, playPolicy: self.playPolicy, maximumFps: self.maximumFps, colors: self.colors, postbox: self.postbox) + } + func withUpdatedColors(_ colors: [LottieColor]) -> LottieAnimation { + return LottieAnimation(compressed: self.compressed, key: self.key, cachePurpose: self.cache, playPolicy: self.playPolicy, maximumFps: self.maximumFps, colors: colors, postbox: self.postbox) + } + func withUpdatedSize(_ size: CGSize) -> LottieAnimation { + return LottieAnimation(compressed: self.compressed, key: self.key.withUpdatedSize(size), cachePurpose: self.cache, playPolicy: self.playPolicy, maximumFps: self.maximumFps, colors: colors, postbox: self.postbox) + } + + var cacheKey: String { + switch key.key { + case let .media(id): + if let id = id { + if let fitzModifier = key.fitzModifier { + return "animation-\(id.namespace)-\(id.id)-fitz\(fitzModifier.rawValue)" + self.colors.map { $0.keyPath + $0.color.hexString }.joined(separator: " ") + } else { + return "animation-\(id.namespace)-\(id.id)" + self.colors.map { $0.keyPath + $0.color.hexString }.joined(separator: " ") + } + } else { + return "\(arc4random())" + } + case let .bundle(string): + return string + self.colors.map { $0.keyPath + $0.color.hexString }.joined(separator: " ") + } + } + + fileprivate var bufferSize: Int { + return Int(size.width * CGFloat(backingScale) * size.height * CGFloat(backingScale) * 4) + } + + + fileprivate func initialize() -> RenderContainer? { + switch type { + case .lottie: + let decompressed = TGGUnzipData(self.compressed, 8 * 1024 * 1024) + let data: Data? + if let decompressed = decompressed { + data = decompressed + } else { + data = self.compressed + } + if let data = data, !data.isEmpty { + let modified: Data + if let color = self.colors.first(where: { $0.keyPath == "" }) { + modified = applyLottieColor(data: data, color: color.color) + } else { + modified = transformedWithFitzModifier(data: data, fitzModifier: self.key.fitzModifier) + } + if let json = String(data: modified, encoding: .utf8) { + if let bridge = RLottieBridge(json: json, key: self.cacheKey) { + for color in self.colors { + bridge.setColor(color.color, forKeyPath: color.keyPath) + } + let fileSupplyment: TRLotFileSupplyment? + switch self.cache { + case .temporaryLZ4: + fileSupplyment = TRLotFileSupplyment(self, bufferSize: bufferSize, frames: Int(bridge.endFrame()), queue: Queue()) + case .none: + fileSupplyment = nil + } + return LottieRenderer(animation: self, bridge: bridge, fileSupplyment: fileSupplyment) + } + } + } + case .webp: + let decompressed = TGGUnzipData(self.compressed, 8 * 1024 * 1024) + let data: Data? + if let decompressed = decompressed { + data = decompressed + } else { + data = self.compressed + } + if let data = data, !data.isEmpty { + if let decoder = WebPImageDecoder(data: data, scale: CGFloat(backingScale)) { + return WebPRenderer(animation: self, decoder: decoder) + } + } + } + return nil + } +} + +private struct RenderLoopItem { + weak private(set) var view: MetalRenderer? + let frame: RenderedFrame + + func render(_ commandBuffer: MTLCommandBuffer) -> MTLDrawable? { + return view?.draw(frame: frame, commandBuffer: commandBuffer) + } +} + + +private class Loops { + + + var data: [LottieRunLoop : Loop] = [:] + + func add(_ view: MetalRenderer, frame: RenderedFrame, runLoop: LottieRunLoop, commandQueue: MTLCommandQueue) { + let loop = getLoop(runLoop, commandQueue: commandQueue) + loop.append(.init(view: view, frame: frame)) + } + func clean() { + data.removeAll() + } + private func getLoop(_ runLoop: LottieRunLoop, commandQueue: MTLCommandQueue) -> Loop { + var loop: Loop + if let c = data[runLoop] { + loop = c + } else { + loop = Loop(runLoop, commandQueue: commandQueue) + data[runLoop] = loop + } + return loop + } +} + +private final class Loop { + var list:[RenderLoopItem] = [] + + private let commandQueue: MTLCommandQueue + + private var timer: SwiftSignalKit.Timer? + init(_ runLoop: LottieRunLoop, commandQueue: MTLCommandQueue) { + self.commandQueue = commandQueue + self.timer = SwiftSignalKit.Timer(timeout: 1 / TimeInterval(runLoop.fps), repeat: true, completion: { [weak self] in + self?.renderItems() + }, queue: stateQueue) + + self.timer?.start() + } + + private func renderItems() { + let commandBuffer = self.commandQueue.makeCommandBuffer() + if let commandBuffer = commandBuffer { + var drawables: [MTLDrawable] = [] + while !self.list.isEmpty { + let item = self.list.removeLast() + let drawable = item.render(commandBuffer) + if let drawable = drawable { + drawables.append(drawable) + } + } + + if drawables.isEmpty { + return + } + + commandBuffer.addScheduledHandler { _ in + for drawable in drawables { + drawable.present() + } + } + commandBuffer.commit() + } else { + self.list.removeAll() + } + } + + func append(_ item: RenderLoopItem) { + self.list.append(item) + } + +} + + +final class MetalContext { + + + + let device: MTLDevice + let pipelineState: MTLRenderPipelineState + let vertexBuffer: MTLBuffer + let sampler: MTLSamplerState + let commandQueue: MTLCommandQueue? + let displayId: CGDirectDisplayID + let refreshRate: Int + private var loops: QueueLocalObject + + init?() { + self.loops = QueueLocalObject(queue: stateQueue, generate: { + return Loops() + }) + self.displayId = CGMainDisplayID() + var refreshRate = CGDisplayCopyDisplayMode(displayId)?.refreshRate ?? 60 + if refreshRate == 0 { + refreshRate = 60 + } + self.refreshRate = Int(min(refreshRate, 60)) + if let device = CGDirectDisplayCopyCurrentMetalDevice(displayId) { + self.device = device + self.commandQueue = device.makeCommandQueue() + } else { + return nil + } + do { + let library = device.makeDefaultLibrary() + + let fragmentProgram = library?.makeFunction(name: "basic_fragment") + let vertexProgram = library?.makeFunction(name: "basic_vertex") + + let pipelineStateDescriptor = MTLRenderPipelineDescriptor() + pipelineStateDescriptor.vertexFunction = vertexProgram + pipelineStateDescriptor.fragmentFunction = fragmentProgram + pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + + self.pipelineState = try device.makeRenderPipelineState(descriptor: pipelineStateDescriptor) + + + let vertexData: [Float] = [ + -1.0, -1.0, 0.0, 0.0, 1.0, + -1.0, 1.0, 0.0, 0.0, 0.0, + 1.0, -1.0, 0.0, 1.0, 1.0, + 1.0, -1.0, 0.0, 1.0, 1.0, + -1.0, 1.0, 0.0, 0.0, 0.0, + 1.0, 1.0, 0.0, 1.0, 0.0 + ] + + let dataSize = vertexData.count * MemoryLayout.size(ofValue: vertexData[0]) + self.vertexBuffer = device.makeBuffer(bytes: vertexData, length: dataSize, options: [])! + + let sampler = MTLSamplerDescriptor() + sampler.minFilter = MTLSamplerMinMagFilter.nearest + sampler.magFilter = MTLSamplerMinMagFilter.nearest + sampler.mipFilter = MTLSamplerMipFilter.nearest + sampler.maxAnisotropy = 1 + sampler.sAddressMode = MTLSamplerAddressMode.clampToZero + sampler.tAddressMode = MTLSamplerAddressMode.clampToZero + sampler.rAddressMode = MTLSamplerAddressMode.clampToZero + sampler.normalizedCoordinates = true + sampler.lodMinClamp = 0.0 + sampler.lodMaxClamp = .greatestFiniteMagnitude + self.sampler = device.makeSamplerState(descriptor: sampler)! + + } catch { + return nil + } + } + + func cleanLoops() { + self.loops.with { loops in + loops.clean() + } + } + + fileprivate func add(_ view: MetalRenderer, frame: RenderedFrame, runLoop: LottieRunLoop, commandQueue: MTLCommandQueue) { + self.loops.with { loops in + loops.add(view, frame: frame, runLoop: runLoop, commandQueue: commandQueue) + } + } +} + +private var metalContext: MetalContext? + + +private final class ContextHolder { + private var useCount: Int = 0 + + let context: MetalContext + init?() { + + if metalContext == nil { + metalContext = MetalContext() + } else if metalContext?.displayId != CGMainDisplayID() { + metalContext = MetalContext() + } + + guard let context = metalContext else { + return nil + } + self.context = context + } + func incrementUseCount() { + assert(Queue.mainQueue().isCurrent()) + useCount += 1 + } + func decrementUseCount() { + assert(Queue.mainQueue().isCurrent()) + useCount -= 1 + assert(useCount >= 0) + + if shouldRelease() { + holder = nil + metalContext?.cleanLoops() + } + } + func shouldRelease() -> Bool { + return useCount == 0 + } + + deinit { + assert(Queue.mainQueue().isCurrent()) + } +} + +private var holder: ContextHolder? + + +struct LottieRunLoop : Hashable { + let fps: Int + + func hash(into hasher: inout Hasher) { + hasher.combine(fps) + } +} + +private final class MetalRenderer: View { + private let texture: MTLTexture + private let metalLayer: CAMetalLayer = CAMetalLayer() + private let context: MetalContext + init(animation: LottieAnimation, context: MetalContext) { + self.context = context + let textureDesc: MTLTextureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .bgra8Unorm, width: Int(animation.size.width) * animation.backingScale, height: Int(animation.size.height) * animation.backingScale, mipmapped: false) + textureDesc.sampleCount = 1 + textureDesc.textureType = .type2D + + self.texture = context.device.makeTexture(descriptor: textureDesc)! + + super.init(frame: NSMakeRect(0, 0, animation.viewSize.width, animation.viewSize.height)) + + self.metalLayer.device = context.device + self.metalLayer.framebufferOnly = true + self.metalLayer.isOpaque = false + self.metalLayer.contentsScale = backingScaleFactor + self.wantsLayer = true + self.layer?.addSublayer(metalLayer) + metalLayer.frame = CGRect(origin: CGPoint(), size: animation.viewSize) + holder?.incrementUseCount() + } + + override func layout() { + super.layout() + metalLayer.frame = self.bounds + } + + override func hitTest(_ point: NSPoint) -> NSView? { + return nil + } + + deinit { + holder?.decrementUseCount() + } + + override func viewDidChangeBackingProperties() { + super.viewDidChangeBackingProperties() + self.metalLayer.contentsScale = backingScaleFactor + } + + override func removeFromSuperview() { + super.removeFromSuperview() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + func draw(frame: RenderedFrame, commandBuffer: MTLCommandBuffer) -> MTLDrawable? { + + guard let drawable = metalLayer.nextDrawable(), let bytes = frame.data else { + return nil + } + + let size: NSSize = frame.size + let backingScale: Int = frame.backingScale + + let region = MTLRegionMake2D(0, 0, Int(size.width) * backingScale, Int(size.height) * backingScale) + + self.texture.replace(region: region, mipmapLevel: 0, withBytes: bytes, bytesPerRow: Int(size.width) * backingScale * 4) + + let renderPassDescriptor = MTLRenderPassDescriptor() + renderPassDescriptor.colorAttachments[0].texture = drawable.texture + renderPassDescriptor.colorAttachments[0].loadAction = .clear + renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) + + let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)! + + renderEncoder.setRenderPipelineState(self.context.pipelineState) + renderEncoder.setVertexBuffer(self.context.vertexBuffer, offset: 0, index: 0) + renderEncoder.setFragmentTexture(self.texture, index: 0) + renderEncoder.setFragmentSamplerState(self.context.sampler, index: 0) + renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6, instanceCount: 1) + renderEncoder.endEncoding() + + return drawable + } + + + func render(frame: RenderedFrame, runLoop: LottieRunLoop) { + guard let commandQueue = self.context.commandQueue else { + return + } + self.context.add(self, frame: frame, runLoop: runLoop, commandQueue: commandQueue) + } +} + +private final class LottieFallbackView: NSView { + override func hitTest(_ point: NSPoint) -> NSView? { + return nil + } +} + +class LottiePlayerView : NSView { + private var context: PlayerContext? + private var _ignoreCachedContext: Bool = false + private let _currentState: Atomic = Atomic(value: .initializing) + var currentState: LottiePlayerState { + return _currentState.with { $0 } + } + + private let stateValue: ValuePromise = ValuePromise(.initializing, ignoreRepeated: true) + var state: Signal { + return stateValue.get() + } + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + + } + + var animation: LottieAnimation? { + return context?.animation + } + + override var isFlipped: Bool { + return true + } + + override func setFrameSize(_ newSize: NSSize) { + super.setFrameSize(newSize) + update(size: newSize, transition: .immediate) + } + + func update(size: NSSize, transition: ContainedViewLayoutTransition) { + for subview in subviews { + transition.updateFrame(view: subview, frame: size.bounds) + } + } + + deinit { + var bp:Int = 0 + bp += 1 + } + + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidChangeBackingProperties() { + if let context = context { + self.set(context.animation.withUpdatedBackingScale(Int(backingScaleFactor))) + } + } + + func playIfNeeded(_ playSound: Bool = false) { + if let context = self.context, context.animation.playPolicy == .once { + context.playAgain() + if playSound { + context.playSoundEffect() + } + } else { + context?.playSoundEffect() + } + } + + var currentFrame: Int32? { + if _ignoreCachedContext { + return nil + } + if let context = self.context { + return context.currentFrame + } else { + return nil + } + } + + func ignoreCachedContext() { + _ignoreCachedContext = true + } + + var totalFrames: Int32? { + if _ignoreCachedContext { + return nil + } + if let context = self.context { + return context.totalFrames + } else { + return nil + } + } + + func setColors(_ colors: [LottieColor]) { + context?.setColors(colors) + } + + func set(_ animation: LottieAnimation?, reset: Bool = false, saveContext: Bool = false, animated: Bool = false) { + assertOnMainThread() + _ignoreCachedContext = false + if let animation = animation { + self.stateValue.set(self._currentState.modify { _ in .initializing }) + if self.context?.animation != animation || reset { + if !animation.runOnQueue.isCurrent() && animation.supportsMetal { + if holder == nil { + holder = ContextHolder() + } + } else { + holder = nil + } + + if let holder = holder { + let metal = MetalRenderer(animation: animation, context: holder.context) + self.addSubview(metal) + let layer = Unmanaged.passRetained(metal) + + + var cachedContext:Unmanaged? + if let context = self.context, saveContext { + cachedContext = Unmanaged.passRetained(context) + } else { + cachedContext = nil + } + + self.context = PlayerContext(animation, maxRefreshRate: holder.context.refreshRate, displayFrame: { frame, runLoop in + layer.takeUnretainedValue().render(frame: frame, runLoop: runLoop) + }, release: { + Queue.mainQueue().async { + layer.takeRetainedValue().removeFromSuperview() + _ = cachedContext?.takeRetainedValue() + cachedContext = nil + } + + }, updateState: { [weak self] state in + guard let `self` = self else { + return + } + switch state { + case .playing, .failed, .stoped: + _ = cachedContext?.takeRetainedValue() + cachedContext = nil + default: + break + } + self.stateValue.set(self._currentState.modify { _ in state } ) + }) + } else { + let fallback = LottieFallbackView() + fallback.wantsLayer = true + fallback.frame = CGRect(origin: CGPoint(), size: self.frame.size) + fallback.layer?.contentsGravity = .resize + self.addSubview(fallback) + if animated { + fallback.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + let layer = Unmanaged.passRetained(fallback) + + self.context = PlayerContext(animation, displayFrame: { frame, _ in + + let image = frame.image + Queue.mainQueue().async { + layer.takeUnretainedValue().layer?.contents = image + } + }, release: { + Queue.mainQueue().async { + let view = layer.takeRetainedValue() + if animated { + view.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak view] _ in + view?.removeFromSuperview() + }) + } else { + view.removeFromSuperview() + } + } + }, updateState: { [weak self] state in + guard let `self` = self else { + return + } + self.stateValue.set(self._currentState.modify { _ in state } ) + }) + } + } + } else { + self.context = nil + self.stateValue.set(self._currentState.modify { _ in .stoped }) + } + } +} + diff --git a/Telegram-Mac/MGalleryExternalVideoItem.swift b/Telegram-Mac/MGalleryExternalVideoItem.swift index 66d77d5356..7ac0a3595e 100644 --- a/Telegram-Mac/MGalleryExternalVideoItem.swift +++ b/Telegram-Mac/MGalleryExternalVideoItem.swift @@ -8,28 +8,364 @@ import Cocoa -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit import TGUIKit import AVFoundation import AVKit +enum AVPlayerState : Equatable { + case playing(duration: Float64) + case paused(duration: Float64) + case waiting + + @available(OSX 10.12, *) + init(_ player: AVPlayer) { + let duration: Float64 + if let item = player.currentItem { + duration = CMTimeGetSeconds(item.duration) + } else { + duration = 0 + } + switch player.timeControlStatus { + case .playing: + self = .playing(duration: duration) + case .paused: + self = .paused(duration: duration) + case .waitingToPlayAtSpecifiedRate: + self = .waiting + @unknown default: + self = .waiting + } + } +} + +private final class GAVPlayer : AVPlayer { + private var playerStatusContext = 0 + private let _playerState: ValuePromise = ValuePromise(.waiting, ignoreRepeated: true) + var playerState: Signal { + return _playerState.get() |> deliverOnMainQueue + } + + var bufferingValue: ValuePromise = ValuePromise(true, ignoreRepeated: true) + + override func pause() { + super.pause() + } + override init(url: URL) { + super.init(url: url) + } + override init(playerItem item: AVPlayerItem?) { + super.init(playerItem: item) + if #available(OSX 10.12, *) { + addObserver(self, forKeyPath: "timeControlStatus", options: [.new, .initial], context: &playerStatusContext) + } + NotificationCenter.default.addObserver(self, selector: #selector(playerDidEnd(_:)), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: item) + + item?.addObserver(self, forKeyPath: "playbackBufferEmpty", options: [.new], context: nil) + item?.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: [.new], context: nil) + item?.addObserver(self, forKeyPath: "playbackBufferFull", options: [.new], context: nil) + } + + override init() { + super.init() + } + + @objc private func playerDidEnd(_ notification: Notification) { + seek(to: CMTime(seconds: 0, preferredTimescale: 1000000000)); + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) + { + // Check status + if keyPath == "timeControlStatus" && context == &playerStatusContext && change != nil + { + if #available(OSX 10.12, *) { + _playerState.set(AVPlayerState(self)) + } + } + + if let item = object as? AVPlayerItem { + bufferingValue.set(!item.isPlaybackLikelyToKeepUp) + } + } + + deinit { + if #available(OSX 10.12, *) { + removeObserver(self, forKeyPath: "timeControlStatus") + } + self.currentItem?.removeObserver(self, forKeyPath: "playbackBufferEmpty") + self.currentItem?.removeObserver(self, forKeyPath: "playbackLikelyToKeepUp") + self.currentItem?.removeObserver(self, forKeyPath: "playbackBufferFull") + + + NotificationCenter.default.removeObserver(self) + } +} + +class VideoPlayerView : AVPlayerView { + + override func enterFullScreenMode(_ screen: NSScreen, withOptions options: [NSView.FullScreenModeOptionKey : Any]? = nil) -> Bool { + return super.enterFullScreenMode(screen, withOptions: options) + } + + var isPip: Bool = false + + override func mouseMoved(with event: NSEvent) { + super.mouseMoved(with: event) + updateLayout() + } + + override func mouseExited(with event: NSEvent) { + super.mouseExited(with: event) + updateLayout() + } + + override func mouseEntered(with event: NSEvent) { + super.mouseEntered(with: event) + updateLayout() + } + + override func setFrameSize(_ newSize: NSSize) { + super.setFrameSize(newSize) + updateLayout() + } + + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + updateLayout() + } + override func viewDidMoveToSuperview() { + super.viewDidMoveToSuperview() + updateLayout() + } + + + func rewindForward(_ seekDuration: Float64 = 15) { + guard let player = player, let duration = player.currentItem?.duration else { return } + let playerCurrentTime = CMTimeGetSeconds(player.currentTime()) + let newTime = min(playerCurrentTime + seekDuration, CMTimeGetSeconds(duration)) + + let time2: CMTime = CMTimeMake(value: Int64(newTime * 1000 as Float64), timescale: 1000) + player.seek(to: time2, toleranceBefore: CMTime.zero, toleranceAfter: CMTime.zero) + } + func rewindBack(_ seekDuration: Float64 = 15) { + guard let player = player else { return } + + let playerCurrentTime = CMTimeGetSeconds(player.currentTime()) + var newTime = playerCurrentTime - seekDuration + + if newTime < 0 { + newTime = 0 + } + let time2: CMTime = CMTimeMake(value: Int64(newTime * 1000 as Float64), timescale: 1000) + player.seek(to: time2, toleranceBefore: CMTime.zero, toleranceAfter: CMTime.zero) + + } + + private func updateLayout() { + let controls = HackUtils.findElements(byClass: "AVMovableView", in: self)?.first as? NSView + if let controls = controls { + if let pip = controls.subviews.last as? ImageButton { + pip.setFrameOrigin(controls.frame.width - pip.frame.width - 80, controls.frame.height - pip.frame.height - 16) + } + controls._change(opacity: _mouseInside() ? 1 : 0, animated: true) + + } + + } + + override func setFrameOrigin(_ newOrigin: NSPoint) { + super.setFrameOrigin(newOrigin) + updateLayout() + } + + override func layout() { + super.layout() + updateLayout() + } +} + -class MGalleryExternalVideoItem: MGalleryVideoItem { +class MGalleryExternalVideoItem: MGalleryItem { let content:TelegramMediaWebpageLoadedContent - private let media:TelegramMediaImage - override init(_ account: Account, _ entry:GalleryEntry, _ pagerSize: NSSize) { - let webpage = entry.message!.media[0] as! TelegramMediaWebpage + private let _media:TelegramMediaImage + + var mediaImage: TelegramMediaImage { + return _media + } + + private(set) var startTime: TimeInterval = 0 + private var playAfter:Bool = true + private let _playerItem: Promise = Promise() + + var playerState: Signal { + return _playerItem.get() |> mapToSignal { $0.playerState } + } + override init(_ context: AccountContext, _ entry: GalleryEntry, _ pagerSize: NSSize) { + + + + let webpage = entry.webpage! + + var startTime:TimeInterval = 0 if case let .Loaded(content) = webpage.content { self.content = content - self.media = content.image! + + + _ = ObjcUtils._youtubeVideoId(fromText: content.embedUrl, originalUrl: content.url, startTime: &startTime) + + self._media = content.image! } else { fatalError("content for external video not found") } - super.init(account, entry, pagerSize) + super.init(context, entry, pagerSize) + self.startTime = startTime + + _playerItem.set((path.get() |> distinctUntilChanged |> deliverOnMainQueue) |> map { path -> GAVPlayer in + let url = URL(string: path) ?? URL(fileURLWithPath: path) + return GAVPlayer(url: url) + }) + + disposable.set(combineLatest(_playerItem.get() |> deliverOnMainQueue, view.get() |> distinctUntilChanged |> deliverOnMainQueue |> map { $0 as! AVPlayerView }).start(next: { [weak self] player, view in + if let strongSelf = self { + view.player = player + if strongSelf.playAfter { + strongSelf.playAfter = false + + player.play() + if strongSelf.startTime > 0 { + player.seek(to: CMTimeMake(value: Int64(strongSelf.startTime * 1000.0), timescale: 1000)) + } + } + let controls = HackUtils.findElements(byClass: "AVMovableView", in: view)?.first as? NSView + if let controls = controls, let pip = strongSelf.pipButton { + controls.addSubview(pip) + view.needsLayout = true + } + } + })) + + } + + private var _cachedView: VideoPlayerView? + private var pipButton: ImageButton? + + override func toggleFullScreen() { + if let view = _cachedView { + let controls = HackUtils.findElements(byClass: "AVMovableView", in: view)?.first as? NSView + if let controls = controls { + if let view = controls.subviews.first?.subviews.first?.subviews.first?.subviews.last?.subviews.first?.subviews.last { + if let view = view as? NSButton { + view.performClick(self) + } + } + } + } + } + + override func togglePlayerOrPause() { + if let view = _cachedView, let player = view.player { + switch player.rate { + case 0: + player.play() + default: + player.pause() + } + } + } + override func rewindBack() { + _cachedView?.rewindBack() + } + override func rewindForward() { + _cachedView?.rewindForward() + } + + override func singleView() -> NSView { + let view: VideoPlayerView + if let _cachedView = _cachedView { + view = _cachedView + } else { + view = VideoPlayerView() + } + view.showsFullScreenToggleButton = true + view.showsFrameSteppingButtons = true + view.controlsStyle = .floating + view.autoresizingMask = [] + view.autoresizesSubviews = false + + let pip:ImageButton = ImageButton() + pip.style = ControlStyle(highlightColor: .grayIcon) + pip.set(image: #imageLiteral(resourceName: "Icon_PIPVideoEnable").precomposed(NSColor.white.withAlphaComponent(0.9)), for: .Normal) + + pip.set(handler: { [weak view, weak self] _ in + if let view = view, let strongSelf = self, let viewer = viewer { + let frame = view.window!.convertToScreen(view.convert(view.bounds, to: nil)) + if !viewer.pager.isFullScreen { + closeGalleryViewer(false) + showLegacyPipVideo(view, viewer: viewer, item: strongSelf, origin: frame.origin, delegate: viewer.delegate, contentInteractions: viewer.contentInteractions, type: viewer.type) + } + } + }, for: .Down) + + _ = pip.sizeToFit() + + pipButton = pip + + + + _cachedView = view + return view + + } + private var isPausedGlobalPlayer: Bool = false + + override func appear(for view: NSView?) { + super.appear(for: view) + + pausepip() + + if let pauseMusic = globalAudio?.pause() { + isPausedGlobalPlayer = pauseMusic + } + + if let view = view as? AVPlayerView { + if let player = view.player { + player.play() + playAfter = false + } else { + playAfter = true + } + } else { + playAfter = true + } + } + + override var maxMagnify: CGFloat { + return min(pagerSize.width / sizeValue.width, pagerSize.height / sizeValue.height) + } + + override func disappear(for view: NSView?) { + super.disappear(for: view) + if isPausedGlobalPlayer { + _ = globalAudio?.play() + } + if let view = view as? VideoPlayerView, !view.isPip { + view.player?.pause() + } + playAfter = false + } + + override var status: Signal { + return _playerItem.get() |> mapToSignal { value in + return value.bufferingValue.get() |> map { buffering in + return buffering ? .Fetching(isActive: true, progress: 0.8) : .Local + } + } } override var sizeValue: NSSize { @@ -37,21 +373,31 @@ class MGalleryExternalVideoItem: MGalleryVideoItem { } override func request(immediately: Bool) { + super.request(immediately: immediately) + let webpage = entry.webpage! + - let signal:Signal<(TransformImageArguments) -> DrawingContext?,NoError> = chatMessagePhoto(account: account, photo: media, scale: System.backingScale) + let signal:Signal = chatMessagePhoto(account: context.account, imageReference: ImageMediaReference.webPage(webPage: WebpageReference(webpage), media: _media), scale: System.backingScale) let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: sizeValue, boundingSize: sizeValue, intrinsicInsets: NSEdgeInsets()) - let result = signal |> deliverOn(account.graphicsThreadPool) |> mapToThrottled { transform -> Signal in - return .single(transform(arguments)?.generateImage()) + let result = signal |> deliverOn(graphicsThreadPool) |> mapToThrottled { data -> Signal in + return .single(data.execute(arguments, data.data)?.generateImage()) + } + + switch webpage.content { + case let .Loaded(content): + _ = sharedVideoLoader.fetch(for: content).start() + default: + break } - self.path.set(sharedVideoLoader.status(for: content) |> mapToSignal { (status) -> Signal in + self.path.set(sharedVideoLoader.status(for: content) |> `catch` {_ in return .complete()} |> mapToSignal { (status) -> Signal in if let status = status, case let .loaded(video) = status { return .single(video.stream) } return .complete() } |> deliverOnMainQueue) - self.image.set(result |> deliverOnMainQueue) + self.image.set(result |> map { GPreviewValueClass(.image($0 != nil ? NSImage(cgImage: $0!, size: $0!.backingSize) : nil, nil)) } |> deliverOnMainQueue) fetch() } diff --git a/Telegram-Mac/MGalleryGIFItem.swift b/Telegram-Mac/MGalleryGIFItem.swift index 17d3878c1a..80466fafeb 100644 --- a/Telegram-Mac/MGalleryGIFItem.swift +++ b/Telegram-Mac/MGalleryGIFItem.swift @@ -7,28 +7,50 @@ // import Cocoa -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit import TGUIKit class MGalleryGIFItem: MGalleryItem { - override init(_ account: Account, _ entry: GalleryEntry, _ pagerSize: NSSize) { - super.init(account, entry, pagerSize) + private var mediaPlayer: MediaPlayer! + + override init(_ context: AccountContext, _ entry: GalleryEntry, _ pagerSize: NSSize) { + super.init(context, entry, pagerSize) let view = self.view - let pathSignal = path.get() |> distinctUntilChanged |> deliverOnMainQueue |> mapToSignal { path -> Signal<(String?,GIFPlayerView), Void> in - return view.get() |> distinctUntilChanged |> map { view in - return (path,view as! GIFPlayerView) + + let fileReference = entry.fileReference(media) + + self.mediaPlayer = MediaPlayer(postbox: context.account.postbox, reference: fileReference.resourceReference(media.resource), streamable: media.isStreamable, video: true, preferSoftwareDecoding: false, enableSound: false, fetchAutomatically: false) + mediaPlayer.actionAtEnd = .loop(nil) + + + disposable.set(view.get().start(next: { [weak self] view in + if let view = (view as? MediaPlayerView) { + self?.mediaPlayer.attachPlayerView(view) } - } - disposable.set(pathSignal.start(next: { (path, view) in - view.set(path: path) })) } - private var media:TelegramMediaFile { + override func appear(for view: NSView?) { + super.appear(for: view) + self.mediaPlayer.play() + } + + override func disappear(for view: NSView?) { + super.disappear(for: view) + + self.mediaPlayer.pause() + } + + override var status:Signal { + return chatMessageFileStatus(account: context.account, file: media) + } + + var media:TelegramMediaFile { switch entry { case .message(let entry): if let media = entry.message!.media[0] as? TelegramMediaFile { @@ -41,8 +63,13 @@ class MGalleryGIFItem: MGalleryItem { fatalError("") } } - case .instantMedia(let media): + case .instantMedia(let media, _): return media.media as! TelegramMediaFile + case let .photo(_, _, photo, _, _, _, _): + let video = photo.videoRepresentations.last! + let file = TelegramMediaFile(fileId: photo.imageId, partialReference: nil, resource: video.resource, previewRepresentations: photo.representations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: video.resource.size, attributes: [.Video(duration:0, size: PixelDimensions(640, 640), flags: [])]) + + return file default: fatalError() } @@ -50,41 +77,57 @@ class MGalleryGIFItem: MGalleryItem { fatalError("") } - override var maxMagnify:CGFloat { - return 1.0 - } +// override var maxMagnify:CGFloat { +// return 1.0 +// } override func singleView() -> NSView { - let player = GIFPlayerView() - player.followWindow = false + let player = MediaPlayerView(backgroundThread: true) + player.layerContentsRedrawPolicy = .duringViewResize return player } override var sizeValue: NSSize { - if let size = media.dimensions { + if let size = media.dimensions?.size { + + var size = size + + if size.width == 0 || size.height == 0 { + size = NSMakeSize(300, 300) + } + + let aspectRatio = size.width / size.height + let addition = max(300 - size.width, 300 - size.height) + + if addition > 0 { + size.width += addition * aspectRatio + size.height += addition + } + return size.fitted(pagerSize) } return pagerSize } override func request(immediately: Bool) { - let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: media.previewRepresentations) + super.request(immediately: immediately) + let size = media.dimensions?.size.fitted(pagerSize) ?? sizeValue - let signal:Signal<(TransformImageArguments) -> DrawingContext?,NoError> = chatMessagePhoto(account: account, photo: image, scale: System.backingScale) - let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: sizeValue, boundingSize: sizeValue, intrinsicInsets: NSEdgeInsets()) - let result = signal |> deliverOn(account.graphicsThreadPool) |> mapToThrottled { transform -> Signal in - return .single(transform(arguments)?.generateImage()) + let signal:Signal = chatMessageVideo(postbox: context.account.postbox, fileReference: entry.fileReference(media), scale: System.backingScale) + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: NSEdgeInsets()) + let result = signal |> deliverOn(graphicsThreadPool) |> mapToThrottled { generator -> Signal in + return .single(generator.execute(arguments, generator.data)?.generateImage()) } - path.set(account.postbox.mediaBox.resourceData(media.resource) |> mapToSignal { (resource) -> Signal in + path.set(context.account.postbox.mediaBox.resourceData(media.resource) |> mapToSignal { (resource) -> Signal in if resource.complete { return .single(link(path:resource.path, ext:kMediaGifExt)!) } return .never() }) - self.image.set(result |> deliverOnMainQueue) + self.image.set(result |> map { GPreviewValueClass(.image($0 != nil ? NSImage(cgImage: $0!, size: $0!.backingSize) : nil, nil)) } |> deliverOnMainQueue) fetch() } @@ -96,7 +139,7 @@ class MGalleryGIFItem: MGalleryItem { } override func fetch() -> Void { - fetching.set(chatMessageFileInteractiveFetched(account: account, file: media).start()) + fetching.set(chatMessageFileInteractiveFetched(account: context.account, fileReference: entry.fileReference(media)).start()) } diff --git a/Telegram-Mac/MGalleryItem.swift b/Telegram-Mac/MGalleryItem.swift index b86d519ce5..7df4a25f87 100644 --- a/Telegram-Mac/MGalleryItem.swift +++ b/Telegram-Mac/MGalleryItem.swift @@ -7,35 +7,257 @@ // import Cocoa -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac +import Postbox +import TelegramCore + +import SwiftSignalKit import TGUIKit +final class GPreviewValueClass { + let value: GPreviewValue + init(_ value: GPreviewValue) { + self.value = value + } +} -enum GalleryEntry : Identifiable { + +enum GPreviewValue { + case image(NSImage?, ImageOrientation?) + case view(NSView?) + + var hasValue: Bool { + switch self { + case let .image(img, _): + return img != nil + case let .view(view): + return view != nil + } + } + var size: NSSize? { + switch self { + case let .image(img, _): + return img?.size + case let .view(view): + return view?.frame.size + } + } + + var rotation: ImageOrientation? { + switch self { + case let .image(_, rotation): + return rotation + case .view: + return nil + } + } + + var image: NSImage? { + switch self { + case let .image(img, _): + return img + case .view: + return nil + } + } + +} + +func <(lhs: GalleryEntry, rhs: GalleryEntry) -> Bool { + switch lhs { + case .message(let lhsEntry): + if case let .message(rhsEntry) = rhs { + return lhsEntry < rhsEntry + } else { + return false + } + case let .secureIdDocument(_, lhsIndex): + if case let .secureIdDocument(_, rhsIndex) = rhs { + return lhsIndex < rhsIndex + } else { + return false + } + case let .photo(lhsIndex, _, _, _, _, _, _): + if case let .photo(rhsIndex, _, _, _, _, _, _) = rhs { + return lhsIndex < rhsIndex + } else { + return false + } + case let .instantMedia(lhsMedia, _): + if case let .instantMedia(rhsMedia, _) = rhs { + return lhsMedia.index < rhsMedia.index + } else { + return false + } + } +} + +func ==(lhs: GalleryEntry, rhs: GalleryEntry) -> Bool { + switch lhs { + case .message(let lhsEntry): + if case let .message(rhsEntry) = rhs { + return lhsEntry.stableId == rhsEntry.stableId + } else { + return false + } + case let .secureIdDocument(lhsEntry, lhsIndex): + if case let .secureIdDocument(rhsEntry, rhsIndex) = rhs { + return lhsEntry.document.isEqual(to: rhsEntry.document) && lhsIndex == rhsIndex + } else { + return false + } + case let .photo(lhsIndex, lhsStableId, lhsPhoto, lhsReference, lhsPeer, _, lhsDate): + if case let .photo(rhsIndex, rhsStableId, rhsPhoto, rhsReference, rhsPeer, _, rhsDate) = rhs { + return lhsIndex == rhsIndex && lhsStableId == rhsStableId && lhsPhoto.isEqual(to: rhsPhoto) && lhsReference == rhsReference && lhsPeer.isEqual(rhsPeer) && lhsDate == rhsDate + } else { + return false + } + case let .instantMedia(lhsMedia, _): + if case let .instantMedia(rhsMedia, _) = rhs { + return lhsMedia == rhsMedia + } else { + return false + } + } +} +enum GalleryEntry : Comparable, Identifiable { case message(ChatHistoryEntry) - case photo(index:Int, stableId:AnyHashable, photo:TelegramMediaImage, reference: TelegramMediaRemoteImageReference) - case instantMedia(InstantPageMedia) + case photo(index:Int, stableId:AnyHashable, photo:TelegramMediaImage, reference: TelegramMediaImageReference?, peer: Peer, message: Message?, date: TimeInterval) + case instantMedia(InstantPageMedia, Message?) + case secureIdDocument(SecureIdDocumentValue, Int) var stableId: AnyHashable { switch self { case let .message(entry): return entry.stableId - case let .photo(_, stableId, _, _): + case let .photo(_, stableId, _, _, _, _, _): return stableId - case let .instantMedia(media): + case let .instantMedia(media, _): return media.index + case let .secureIdDocument(document, _): + return document.stableId + } + } + + var canShare: Bool { + return message != nil && !message!.isScheduledMessage && !message!.containsSecretMedia + } + + var interfaceState:(PeerId, TimeInterval)? { + switch self { + case let .message(entry): + if let peerId = entry.message!.effectiveAuthor?.id { + return (peerId, TimeInterval(entry.message!.timestamp)) + } + case let .instantMedia(_, message): + if let message = message, let peerId = message.effectiveAuthor?.id { + return (peerId, TimeInterval(message.timestamp)) + } + case let .photo(_, _, _, _, peer, _, date): + return (peer.id, date) + default: + return nil + } + return nil + } + + var file:TelegramMediaFile? { + switch self { + case .message(let entry): + if let media = entry.message!.media[0] as? TelegramMediaFile { + return media + } else if let media = entry.message!.media[0] as? TelegramMediaWebpage { + switch media.content { + case let .Loaded(content): + return content.file + default: + return nil + } + } + case .instantMedia(let media, _): + return media.media as? TelegramMediaFile + default: + return nil + } + + return nil + } + + var webpage: TelegramMediaWebpage? { + switch self { + case let .message(entry): + return entry.message!.media[0] as? TelegramMediaWebpage + case let .instantMedia(media, _): + return media.media as? TelegramMediaWebpage + default: + return nil + } + } + + func imageReference( _ image: TelegramMediaImage) -> ImageMediaReference { + switch self { + case let .message(entry): + return ImageMediaReference.message(message: MessageReference(entry.message!), media: image) + case let .instantMedia(media, _): + return ImageMediaReference.webPage(webPage: WebpageReference(media.webpage), media: image) + case .secureIdDocument: + return ImageMediaReference.standalone(media: image) + case .photo: + return ImageMediaReference.standalone(media: image) + } + } + + var peer: Peer? { + switch self { + case let .photo(_, _, _, _, peer, _, _): + return peer + default: + return nil } } + func peerPhotoResource() -> MediaResourceReference { + switch self { + case let .photo(_, _, image, _, peer, message, _): + if let representation = image.representationForDisplayAtSize(PixelDimensions(1280, 1280)) { + if let message = message { + return .media(media: .message(message: MessageReference(message), media: image), resource: representation.resource) + } + if let peerReference = PeerReference(peer) { + return .avatar(peer: peerReference, resource: representation.resource) + } else { + return .standalone(resource: representation.resource) + } + } else { + preconditionFailure() + } + default: + preconditionFailure() + } + } + + func fileReference( _ file: TelegramMediaFile) -> FileMediaReference { + switch self { + case let .message(entry): + return FileMediaReference.message(message: MessageReference(entry.message!), media: file) + case let .instantMedia(media, _): + return FileMediaReference.webPage(webPage: WebpageReference(media.webpage), media: file) + case .secureIdDocument: + return FileMediaReference.standalone(media: file) + case .photo: + return FileMediaReference.standalone(media: file) + } + } + + var identifier: String { switch self { case let .message(entry): return "\(entry.message?.stableId ?? 0)" - case .photo(_, let stableId, _, _): + case .photo(_, let stableId, _, _, _, _, _): return "\(stableId)" case .instantMedia: return "\(stableId)" + case let .secureIdDocument(document, _): + return "secureId: \(document.document.id.hashValue)" } } @@ -52,6 +274,8 @@ enum GalleryEntry : Identifiable { switch self { case let .message(entry): return entry.message + case let .instantMedia(_, message): + return message default: return nil } @@ -60,18 +284,18 @@ enum GalleryEntry : Identifiable { switch self { case .message: return nil - case let .photo(_, _, photo, _): + case let .photo(_, _, photo, _, _, _, _): return photo default: return nil } } - var photoReference:TelegramMediaRemoteImageReference? { + var photoReference:TelegramMediaImageReference? { switch self { case .message: return nil - case let .photo(_, _, _, reference): + case let .photo(_, _, _, reference, _, _, _): return reference default: return nil @@ -79,12 +303,39 @@ enum GalleryEntry : Identifiable { } } +func ==(lhs: MGalleryItem, rhs: MGalleryItem) -> Bool { + return lhs.entry == rhs.entry +} +func <(lhs: MGalleryItem, rhs: MGalleryItem) -> Bool { + return lhs.entry < rhs.entry +} + +private final class MGalleryItemView : NSView { + init() { + super.init(frame: NSZeroRect) + self.wantsLayer = true + self.layerContentsRedrawPolicy = .never + } + + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override var isOpaque: Bool { + return true + } + + deinit { + var bp:Int = 0 + bp += 1 + } +} -class MGalleryItem: NSObject { - let image:Promise = Promise() +class MGalleryItem: NSObject, Comparable, Identifiable { + let image:Promise = Promise() let view:Promise = Promise() let size:Promise = Promise() - let magnify:Promise = Promise() + let magnify:Promise = Promise(1) + let rotate: ValuePromise = ValuePromise(nil, ignoreRepeated: true) let disposable:MetaDisposable = MetaDisposable() let fetching:MetaDisposable = MetaDisposable() @@ -92,11 +343,25 @@ class MGalleryItem: NSObject { private let viewDisposable:MetaDisposable = MetaDisposable() let path:Promise = Promise() let entry:GalleryEntry - let account:Account - let pagerSize:NSSize + let context: AccountContext + private var _pagerSize: NSSize + private var captionSeized: Bool = false + var pagerSize:NSSize { + var pagerSize = _pagerSize + if let caption = caption, !captionSeized { + caption.measure(width: pagerSize.width - 300) + captionSeized = true + } + if let caption = caption { + pagerSize.height -= min(140, caption.layoutSize.height + 60) + } + return pagerSize + } let caption: TextViewLayout? - private(set) var modifiedSize: NSSize? = nil + var disableAnimations: Bool = false + + var modifiedSize: NSSize? = nil private(set) var magnifyValue:CGFloat = 1.0 var stableId: AnyHashable { @@ -104,7 +369,7 @@ class MGalleryItem: NSObject { } var identifier:NSPageController.ObjectIdentifier { - return NSPageController.ObjectIdentifier(rawValue: entry.identifier) + return entry.identifier + self.className } var sizeValue:NSSize { @@ -112,24 +377,74 @@ class MGalleryItem: NSObject { } var minMagnify:CGFloat { - return 1.0 + return 0.25 } var maxMagnify:CGFloat { return 8.0 } - func smallestValue(for size: NSSize) -> Signal { - return .single(pagerSize) + func smallestValue(for size: NSSize) -> NSSize { + return pagerSize + } + + var status:Signal { + return .single(.Local) } - init(_ account:Account, _ entry:GalleryEntry, _ pagerSize:NSSize) { + var realStatus:Signal { + return self.status + } + + func toggleFullScreen() { + + } + func togglePlayerOrPause() { + + } + func rewindBack() { + + } + func rewindForward() { + + } + + init(_ context: AccountContext, _ entry:GalleryEntry, _ pagerSize:NSSize) { self.entry = entry - self.account = account - self.pagerSize = pagerSize - if let caption = entry.message?.text { - self.caption = TextViewLayout(.initialize(string: caption, color: .white, font: .normal(.text)), alignment: .center) - self.caption?.measure(width: .greatestFiniteMagnitude) + self.context = context + self._pagerSize = pagerSize + if let caption = entry.message?.text, !caption.isEmpty, !(entry.message?.media.first is TelegramMediaWebpage) { + let attr = NSMutableAttributedString() + _ = attr.append(string: caption.trimmed.fullTrimmed, color: .white, font: .normal(.text)) + + attr.detectLinks(type: [.Links, .Mentions], context: context, color: .linkColor, openInfo: { peerId, toChat, postId, action in + let navigation = context.sharedContext.bindings.rootNavigation() + let controller = navigation.controller + if toChat { + if peerId == (controller as? ChatController)?.chatInteraction.peerId { + if let postId = postId { + (controller as? ChatController)?.chatInteraction.focusMessageId(nil, postId, TableScrollState.CenterEmpty) + } + } else { + navigation.push(ChatAdditionController(context: context, chatLocation: .peer(peerId), messageId: postId, initialAction: action)) + } + } else { + navigation.push(PeerInfoController(context: context, peerId: peerId)) + } + viewer?.close() + }, hashtag: { _ in }, command: {_ in }, applyProxy: { _ in }) + + self.caption = TextViewLayout(attr, alignment: .left) + self.caption?.interactions = TextViewInteractions(processURL: { link in + if let link = link as? inAppLink { + execute(inapp: link, afterComplete: { value in + if value { + viewer?.close() + } + }) + + } + }) } else { self.caption = nil } @@ -138,20 +453,27 @@ class MGalleryItem: NSObject { var first:Bool = true - let image = combineLatest(self.image.get(), view.get()) |> map { [weak self] image, view in - view.layer?.contents = image - view.layer?.backgroundColor = theme.colors.background.cgColor - - if first, let slf = self, let magnify = view.superview?.superview as? MagnifyView { - self?.modifiedSize = image?.size - if magnify.contentSize != slf.sizeValue { - magnify.contentSize = slf.sizeValue - } - } + let image = combineLatest(self.image.get() |> map { $0.value }, view.get()) |> map { [weak self] value, view in + guard let `self` = self else {return} + view.layer?.contents = value.image - if !first { + if !first && !self.disableAnimations { view.layer?.animateContents() } + self.disableAnimations = false + view.layer?.backgroundColor = self.backgroundColor.cgColor + + if let magnify = view.superview?.superview as? MagnifyView { + var size = magnify.contentSize + if self is MGalleryPhotoItem || self is MGalleryPeerPhotoItem { + if value.rotation == nil { + size = value.size?.aspectFitted(size) ?? size + } else { + size = value.size ?? size + } + } + magnify.contentSize = size + } first = false } viewDisposable.set(image.start()) @@ -161,18 +483,34 @@ class MGalleryItem: NSObject { })) } + var backgroundColor: NSColor { + return .black + } + + var isGraphicFile: Bool { + if self.entry.message?.media.first is TelegramMediaFile { + return true + } else { + return false + } + } + func singleView() -> NSView { - return NSView() + return MGalleryItemView() } func request(immediately:Bool = true) { - + // self.caption?.measure(width: sizeValue.width) } func fetch() { } + var notFittedSize: NSSize { + return sizeValue + } + func cancel() { fetching.set(nil) } @@ -194,7 +532,5 @@ class MGalleryItem: NSObject { } -func ==(lhs:MGalleryItem, rhs:MGalleryItem) -> Bool { - return lhs.stableId == rhs.stableId -} + diff --git a/Telegram-Mac/MGalleryPeerPhotoItem.swift b/Telegram-Mac/MGalleryPeerPhotoItem.swift index 9274cee528..b8241a7c99 100644 --- a/Telegram-Mac/MGalleryPeerPhotoItem.swift +++ b/Telegram-Mac/MGalleryPeerPhotoItem.swift @@ -7,76 +7,113 @@ // import Cocoa -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit import TGUIKit class MGalleryPeerPhotoItem: MGalleryItem { - private let media:TelegramMediaImage - override init(_ account: Account, _ entry: GalleryEntry, _ pagerSize: NSSize) { + let media:TelegramMediaImage + override init(_ context: AccountContext, _ entry: GalleryEntry, _ pagerSize: NSSize) { self.media = entry.photo! - super.init(account, entry, pagerSize) + super.init(context, entry, pagerSize) } override var sizeValue: NSSize { - if let largest = media.representations.last { - return largest.dimensions.fitted(pagerSize) + if let largest = media.representationForDisplayAtSize(PixelDimensions(1280, 1280)) { + return largest.dimensions.size.fitted(pagerSize) } return NSZeroSize } - override func smallestValue(for size: NSSize) -> Signal { - if let largest = media.representations.last { - return .single(largest.dimensions.fitted(size)) + override func smallestValue(for size: NSSize) -> NSSize { + if let largest = media.representationForDisplayAtSize(PixelDimensions(1280, 1280)) { + return largest.dimensions.size.fitted(size) + } + return pagerSize + } + + override var status:Signal { + if let largestRepresentation = media.representationForDisplayAtSize(PixelDimensions(1280, 1280)) { + return context.account.postbox.mediaBox.resourceStatus(largestRepresentation.resource) + } else { + return .never() } - return .single(pagerSize) } override func request(immediately: Bool) { + super.request(immediately: immediately) - - let account = self.account + let context = self.context let media = self.media + let entry = self.entry + + let magnify = self.magnify.get() - let result = size.get() |> mapToSignal { [weak self] size -> Signal in - if let strongSelf = self { - return strongSelf.smallestValue(for: size) + let sizeValue: Signal = size.get() |> mapToSignal { size in + return magnify |> take(1) |> map { magnify in + return NSMakeSize(floorToScreenPixels(System.backingScale, size.width / magnify), floorToScreenPixels(System.backingScale, size.height / magnify)) } - return .complete() - } |> distinctUntilChanged |> mapToSignal { size -> Signal<((TransformImageArguments) -> DrawingContext?, TransformImageArguments), Void> in - return chatMessagePhoto(account: account, photo: media, scale: System.backingScale) |> deliverOn(account.graphicsThreadPool) |> map { transform in - return (transform, TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: NSEdgeInsets())) + } |> distinctUntilChanged(isEqual: { lhs, rhs -> Bool in + return lhs == rhs + }) + + + let result = combineLatest(sizeValue, rotate.get()) |> mapToSignal { [weak self] size, orientation -> Signal<(NSSize, ImageOrientation?), NoError> in + guard let `self` = self else {return .complete()} + var newSize = self.smallestValue(for: size) + if let orientation = orientation { + if orientation == .right || orientation == .left { + newSize = NSMakeSize(newSize.height, newSize.width) + } + } + return .single((newSize, orientation)) + } |> mapToSignal { size, orientation -> Signal<(NSImage?, ImageOrientation?), NoError> in + return chatGalleryPhoto(account: context.account, imageReference: entry.imageReference(media), toRepresentationSize: NSMakeSize(1280, 1280), peer: entry.peer, scale: System.backingScale, synchronousLoad: true) + |> map { transform in + let image = transform(TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: NSEdgeInsets())) + if let orientation = orientation { + let transformed = image?.createMatchingBackingDataWithImage(orienation: orientation) + if let transformed = transformed { + return (NSImage(cgImage: transformed, size: transformed.size), orientation) + } + } + if let image = image { + return (NSImage(cgImage: image, size: image.size), orientation) + } else { + return (nil, nil) + } } - } |> mapToThrottled { (transform, arguments) -> Signal in - return .single(transform(arguments)?.generateImage()) } - - if let representation = media.representations.last { - path.set(account.postbox.mediaBox.resourceData(representation.resource) |> mapToSignal { (resource) -> Signal in + if let representation = media.representationForDisplayAtSize(PixelDimensions(1280, 1280)) { + path.set(context.account.postbox.mediaBox.resourceData(representation.resource) |> mapToSignal { (resource) -> Signal in if resource.complete { return .single(link(path:resource.path, ext:kMediaImageExt)!) } return .never() }) - } + } - self.image.set(result |> deliverOnMainQueue) + self.image.set(result |> map { GPreviewValueClass(.image($0.0, $0.1)) } |> deliverOnMainQueue) fetch() } override func fetch() -> Void { - fetching.set(chatMessagePhotoInteractiveFetched(account: account, photo: media).start()) + fetching.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: self.entry.peerPhotoResource()).start()) } override func cancel() -> Void { super.cancel() - chatMessagePhotoCancelInteractiveFetch(account: account, photo: media) + if let representation = media.representationForDisplayAtSize(PixelDimensions(1280, 1280)) { + cancelFreeMediaFileInteractiveFetch(context: context, resource: representation.resource) + } + fetching.set(nil) } } diff --git a/Telegram-Mac/MGalleryPhotoItem.swift b/Telegram-Mac/MGalleryPhotoItem.swift index b88b73ac4e..de4d07aca6 100644 --- a/Telegram-Mac/MGalleryPhotoItem.swift +++ b/Telegram-Mac/MGalleryPhotoItem.swift @@ -7,113 +7,203 @@ // import Cocoa -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit import TGUIKit class MGalleryPhotoItem: MGalleryItem { - private let media:TelegramMediaImage + let media:TelegramMediaImage + let secureIdAccessContext: SecureIdAccessContext? private let representation:TelegramMediaImageRepresentation - override init(_ account: Account, _ entry: GalleryEntry, _ pagerSize: NSSize) { + override init(_ context: AccountContext, _ entry: GalleryEntry, _ pagerSize: NSSize) { switch entry { case .message(let entry): if let webpage = entry.message!.media[0] as? TelegramMediaWebpage { if case let .Loaded(content) = webpage.content, let image = content.image { self.media = image + } else if case let .Loaded(content) = webpage.content, let media = content.file { + let represenatation = TelegramMediaImageRepresentation(dimensions: media.dimensions ?? PixelDimensions(0, 0), resource: media.resource, progressiveSizes: [], immediateThumbnailData: nil) + var representations = media.previewRepresentations + representations.append(represenatation) + self.media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + } else { fatalError("image for webpage not found") } } else { if let media = entry.message!.media[0] as? TelegramMediaFile { - let represenatation = TelegramMediaImageRepresentation(dimensions: media.dimensions ?? NSZeroSize, resource: media.resource) + let represenatation = TelegramMediaImageRepresentation(dimensions: media.dimensions ?? PixelDimensions(0, 0), resource: media.resource, progressiveSizes: [], immediateThumbnailData: nil) var representations = media.previewRepresentations representations.append(represenatation) - self.media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations) + self.media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) } else { self.media = entry.message!.media[0] as! TelegramMediaImage } } - case .instantMedia(let media): + secureIdAccessContext = nil + case .instantMedia(let media, _): self.media = media.media as! TelegramMediaImage + secureIdAccessContext = nil + case let .secureIdDocument(document, _): + self.media = document.image + self.secureIdAccessContext = document.context default: fatalError("photo item not supported entry type") } - self.representation = media.representations.last! - super.init(account, entry, pagerSize) + self.representation = media.representationForDisplayAtSize(PixelDimensions(NSMakeSize(1280, 1280)))! + super.init(context, entry, pagerSize) } override var sizeValue: NSSize { if let largest = media.representations.last { - if let modifiedSize = modifiedSize, largest.dimensions.width == 0 { - let lhsProportion = modifiedSize.width/modifiedSize.height - let rhsProportion = largest.dimensions.width/largest.dimensions.height - - if lhsProportion != rhsProportion { - return modifiedSize.fitted(pagerSize) - } - + if let modifiedSize = modifiedSize { + return modifiedSize.fitted(pagerSize) } - return largest.dimensions.fitted(pagerSize) + return largest.dimensions.size.fitted(pagerSize) } return NSZeroSize } - override func smallestValue(for size: NSSize) -> Signal { + override func smallestValue(for size: NSSize) -> NSSize { if let largest = media.representations.last { if let modifiedSize = modifiedSize { let lhsProportion = modifiedSize.width/modifiedSize.height - let rhsProportion = largest.dimensions.width/largest.dimensions.height + let rhsProportion = largest.dimensions.size.width/largest.dimensions.size.height if lhsProportion != rhsProportion { - return .single(modifiedSize.fitted(size)) + return modifiedSize.fitted(size) } } - return .single(largest.dimensions.fitted(size)) + return largest.dimensions.size.fitted(size) } - return .single(pagerSize) + return pagerSize + } + + override var status:Signal { + return chatMessagePhotoStatus(account: context.account, photo: media) } + private var hasRequested: Bool = false + override func request(immediately: Bool) { + super.request(immediately: immediately) - let account = self.account - let media = self.media - - let result = size.get() |> mapToSignal { [weak self] size -> Signal in - if let strongSelf = self { - return strongSelf.smallestValue(for: size) + if !hasRequested { + let context = self.context + let entry = self.entry + let media = self.media + let secureIdAccessContext = self.secureIdAccessContext + + let sizeValue: Signal = size.get() |> map { size in + return NSMakeSize(floorToScreenPixels(System.backingScale, size.width), floorToScreenPixels(System.backingScale, size.height)) + } |> distinctUntilChanged(isEqual: { lhs, rhs -> Bool in + return lhs == rhs + }) + + let rotateValue = rotate.get() |> distinctUntilChanged(isEqual: { lhs, rhs -> Bool in + return lhs == rhs + }) + + + let signal = combineLatest(sizeValue, rotateValue) |> mapToSignal { [weak self] size, orientation -> Signal<(NSSize, ImageOrientation?), NoError> in + guard let `self` = self else {return .complete()} + + var size = size + if self.sizeValue.width > self.sizeValue.height && size.width < size.height + || self.sizeValue.width < self.sizeValue.height && size.width > size.height { + size = NSMakeSize(size.height, size.width) + } + + var newSize = self.smallestValue(for: size) + if let orientation = orientation { + if orientation == .right || orientation == .left { + newSize = NSMakeSize(newSize.height, newSize.width) + } + } + return .single((newSize, orientation)) + } - return .complete() - } |> distinctUntilChanged |> mapToSignal { size -> Signal in - return chatGalleryPhoto(account: account, photo: media, scale: System.backingScale) |> deliverOn(account.graphicsThreadPool) |> map { transform in - return transform(TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: NSEdgeInsets())) + + class Data { + let image: NSImage? + let orientation: ImageOrientation? + init(_ image: NSImage?, _ orientation: ImageOrientation?) { + self.image = image + self.orientation = orientation + } } - } - - path.set(account.postbox.mediaBox.resourceData(representation.resource) |> mapToSignal { resource -> Signal in - if resource.complete { - return .single(link(path:resource.path, ext:kMediaImageExt)!) + + let result = combineLatest(signal, self.magnify.get() |> distinctUntilChanged) |> mapToSignal { [weak self] data, magnify -> Signal in + + let (size, orientation) = data + return chatGalleryPhoto(account: context.account, imageReference: entry.imageReference(media), scale: System.backingScale, secureIdAccessContext: secureIdAccessContext, synchronousLoad: true) + |> map { [weak self] transform in + + var size = NSMakeSize(ceil(size.width * magnify), ceil(size.height * magnify)) + let image = transform(TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: NSEdgeInsets())) + + if let image = image, orientation == nil { + let newSize = image.size.aspectFitted(size) + if newSize != size { + size = newSize + self?.modifiedSize = image.size + } + } + + if let orientation = orientation { + let transformed = image?.createMatchingBackingDataWithImage(orienation: orientation) + if let transformed = transformed { + return Data(NSImage(cgImage: transformed, size: size), orientation) + } + } + if let image = image { + return Data(NSImage(cgImage: image, size: size), orientation) + } else { + return Data(nil, orientation) + } + } + } - return .never() - }) - - self.image.set(result |> deliverOnMainQueue) - + + path.set(context.account.postbox.mediaBox.resourceData(representation.resource) |> mapToSignal { resource -> Signal in + if resource.complete { + return .single(link(path:resource.path, ext:kMediaImageExt)!) + } + return .never() + }) + + self.image.set(result |> map { GPreviewValueClass(.image($0.image, $0.orientation)) } |> deliverOnMainQueue) + + + fetch() + + hasRequested = true + } - fetch() + } + + override var backgroundColor: NSColor { + return theme.colors.transparentBackground } override func fetch() -> Void { - fetching.set(chatMessagePhotoInteractiveFetched(account: account, photo: media).start()) + fetching.set(chatMessagePhotoInteractiveFetched(account: context.account, imageReference: entry.imageReference(media)).start()) } override func cancel() -> Void { super.cancel() - chatMessagePhotoCancelInteractiveFetch(account: account, photo: media) + chatMessagePhotoCancelInteractiveFetch(account: context.account, photo: media) } + deinit { + var bp:Int = 0 + bp += 1 + } + } diff --git a/Telegram-Mac/MGalleryVideoItem.swift b/Telegram-Mac/MGalleryVideoItem.swift index ebcf51f382..c0b76f701c 100644 --- a/Telegram-Mac/MGalleryVideoItem.swift +++ b/Telegram-Mac/MGalleryVideoItem.swift @@ -8,105 +8,62 @@ import Cocoa -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit import TGUIKit import AVFoundation import AVKit -private class VideoPlayerView : AVPlayerView { - - deinit { - var bp:Int = 0 - bp += 1 - } - - override func layout() { - super.layout() - let controls = subviews.last?.subviews.last - if let controls = controls, let pip = controls.subviews.last as? ImageButton { - pip.setFrameOrigin(controls.frame.width - pip.frame.width - 80, 34) - controls.centerX(y: 50) - } - } -} - class MGalleryVideoItem: MGalleryItem { - + var startTime: TimeInterval = 0 private var playAfter:Bool = false - override init(_ account: Account, _ entry: GalleryEntry, _ pagerSize: NSSize) { - super.init(account, entry, pagerSize) - - let pathSignal = combineLatest(path.get() |> distinctUntilChanged |> deliverOnMainQueue, view.get() |> distinctUntilChanged) |> map { path, view -> (AVPlayer?,AVPlayerView) in - let url = URL(string: path) ?? URL(fileURLWithPath: path) - let player = AVPlayer(url: url) - player.seek(to: CMTime()) - return (player, view as! AVPlayerView) - } - disposable.set(pathSignal.start(next: { [weak self] player, view in - if let strongSelf = self { - view.player = player - if strongSelf.playAfter { - strongSelf.playAfter = false - player?.play() - - let controls = view.subviews.last?.subviews.last - if let controls = controls, let pip = strongSelf.pipButton { - controls.addSubview(pip) - view.needsLayout = true - } - + private let controller: SVideoController + var playerState: Signal { + return controller.status |> map { value in + switch value.status { + case .playing: + return .playing(duration: value.duration) + case .paused: + return .paused(duration: value.duration) + case let .buffering(initial, whilePlaying): + if whilePlaying { + return .playing(duration: value.duration) + } else if !whilePlaying && !initial { + return .paused(duration: value.duration) + } else { + return .waiting } } - })) - + } |> deliverOnMainQueue + } + override init(_ context: AccountContext, _ entry: GalleryEntry, _ pagerSize: NSSize) { + controller = SVideoController(postbox: context.account.postbox, reference: entry.fileReference(entry.file!)) + super.init(context, entry, pagerSize) + + controller.togglePictureInPictureImpl = { [weak self] enter, control in + guard let `self` = self else {return} + let frame = control.view.window!.convertToScreen(control.view.convert(control.view.bounds, to: nil)) + if enter, let viewer = viewer { + closeGalleryViewer(false) + showPipVideo(control: control, viewer: viewer, item: self, origin: frame.origin, delegate: viewer.delegate, contentInteractions: viewer.contentInteractions, type: viewer.type) + } else if !enter { + exitPictureInPicture() + } + } } deinit { - var bp:Int = 0 - bp += 1 + updateMagnifyDisposable.dispose() } - - private var _cachedView: VideoPlayerView? - private var pipButton: ImageButton? - - override func singleView() -> NSView { - let view: VideoPlayerView - if let _cachedView = _cachedView { - view = _cachedView - } else { - view = VideoPlayerView() - } - view.showsFullScreenToggleButton = true - view.showsFrameSteppingButtons = true - view.controlsStyle = .floating - view.autoresizingMask = [] - view.autoresizesSubviews = false - - let pip:ImageButton = ImageButton() - pip.style = ControlStyle(highlightColor: .grayIcon) - pip.set(image: #imageLiteral(resourceName: "Icon_PIPVideoEnable").precomposed(NSColor.white.withAlphaComponent(0.9)), for: .Normal) - - pip.set(handler: { [weak view, weak self] _ in - if let view = view, let strongSelf = self, let viewer = viewer { - let frame = view.window!.convertToScreen(view.convert(view.bounds, to: nil)) - closeGalleryViewer(false) - showPipVideo(view, item: strongSelf, origin: frame.origin, delegate: viewer.delegate, contentInteractions: viewer.contentInteractions, type: viewer.type) - } - }, for: .Down) - pip.sizeToFit() - - pipButton = pip - - - - _cachedView = view - return view + override func singleView() -> NSView { + return controller.genericView } private var isPausedGlobalPlayer: Bool = false + private let updateMagnifyDisposable = MetaDisposable() override func appear(for view: NSView?) { super.appear(for: view) @@ -117,15 +74,18 @@ class MGalleryVideoItem: MGalleryItem { isPausedGlobalPlayer = pauseMusic } - if let view = view as? AVPlayerView { - if let player = view.player { - player.play() + controller.play(startTime) + controller.viewDidAppear(false) + self.startTime = 0 + + + updateMagnifyDisposable.set((magnify.get() |> deliverOnMainQueue).start(next: { [weak self] value in + if value < 1.0 { + _ = self?.hideControls(forceHidden: true) } else { - playAfter = true + _ = self?.unhideControls(forceUnhidden: true) } - } else { - playAfter = true - } + })) } override var maxMagnify: CGFloat { @@ -137,73 +97,156 @@ class MGalleryVideoItem: MGalleryItem { if isPausedGlobalPlayer { _ = globalAudio?.play() } - if let view = view as? AVPlayerView { - view.player?.pause() + if controller.style != .pictureInPicture { + controller.pause() } + controller.viewDidDisappear(false) + updateMagnifyDisposable.set(nil) playAfter = false } - private var media:TelegramMediaFile { - switch entry { - case .message(let entry): - if let media = entry.message!.media[0] as? TelegramMediaFile { - return media - } else if let media = entry.message!.media[0] as? TelegramMediaWebpage { - switch media.content { - case let .Loaded(content): - return content.file! - default: - fatalError("") - } - } - case .instantMedia(let media): - return media.media as! TelegramMediaFile - default: - fatalError() + override var status:Signal { + if media.isStreamable { + return .single(.Local) + } else { + return chatMessageFileStatus(account: context.account, file: media) + } + } + + override var realStatus:Signal { + return chatMessageFileStatus(account: context.account, file: media) + } + + var media:TelegramMediaFile { + return entry.file! + } + private var examinatedSize: CGSize? + var dimensions: CGSize? { + + if let examinatedSize = examinatedSize { + return examinatedSize } + if let dimensions = media.dimensions { + return dimensions.size + } + let linked = link(path: context.account.postbox.mediaBox.resourcePath(media.resource), ext: "mp4") + guard let path = linked else { + return media.dimensions?.size + } + + let url = URL(fileURLWithPath: path) + guard let track = AVURLAsset(url: url).tracks(withMediaType: .video).first else { + return media.dimensions?.size + } + try? FileManager.default.removeItem(at: url) + let size = track.naturalSize.applying(track.preferredTransform) + self.examinatedSize = NSMakeSize(abs(size.width), abs(size.height)) + + return examinatedSize - fatalError("") + } + + override var notFittedSize: NSSize { + if let size = dimensions { + return size.fitted(pagerSize) + } + return pagerSize } override var sizeValue: NSSize { - if let size = media.dimensions { - let size = size.fitted(pagerSize) - return NSMakeSize(max(size.width, 320), max(size.height, 240)) + if let size = dimensions { + + var pagerSize = self.pagerSize + + + var size = size + + if size.width == 0 || size.height == 0 { + size = NSMakeSize(300, 300) + } + + let aspectRatio = size.width / size.height + let addition = max(300 - size.width, 300 - size.height) + + if addition > 0 { + size.width += addition * aspectRatio + size.height += addition + } + +// let addition = max(400 - size.width, 400 - size.height) +// if addition > 0 { +// size.width += addition +// size.height += addition +// } + + size = size.fitted(pagerSize) + + return size } return pagerSize } + func hideControls(forceHidden: Bool = false) -> Bool { + return controller.hideControlsIfNeeded(forceHidden) + } + func unhideControls(forceUnhidden: Bool = true) -> Bool { + return controller.unhideControlsIfNeeded(forceUnhidden) + } + + override func toggleFullScreen() { + controller.toggleFullScreen() + } + + override func togglePlayerOrPause() { + controller.togglePlayerOrPause() + } + + override func rewindBack() { + controller.rewindBackward() + } + override func rewindForward() { + controller.rewindForward() + } + + var isFullscreen: Bool { + return controller.isFullscreen + } + + override func request(immediately: Bool) { - let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: media.previewRepresentations) + super.request(immediately: immediately) - let signal:Signal<(TransformImageArguments) -> DrawingContext?,NoError> = chatMessagePhoto(account: account, photo: image, scale: System.backingScale) + let signal:Signal = chatMessageVideo(postbox: context.account.postbox, fileReference: entry.fileReference(media), scale: System.backingScale, synchronousLoad: true) + let size = sizeValue - let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: sizeValue, boundingSize: sizeValue, intrinsicInsets: NSEdgeInsets()) - let result = signal |> deliverOn(account.graphicsThreadPool) |> mapToThrottled { transform -> Signal in - return .single(transform(arguments)?.generateImage()) + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: NSEdgeInsets(), resizeMode: .none) + let result = signal |> mapToThrottled { data -> Signal in + return .single(data.execute(arguments, data.data)?.generateImage()) } - - path.set(account.postbox.mediaBox.resourceData(media.resource) |> mapToSignal { (resource) -> Signal in + path.set(context.account.postbox.mediaBox.resourceData(media.resource) |> mapToSignal { (resource) -> Signal in if resource.complete { return .single(link(path:resource.path, ext:kMediaVideoExt)!) } return .never() }) - self.image.set(media.previewRepresentations.isEmpty ? .single(nil) |> deliverOnMainQueue : result |> deliverOnMainQueue) + self.image.set(media.previewRepresentations.isEmpty ? .single(GPreviewValueClass(.image(nil, nil))) |> deliverOnMainQueue : result |> map { GPreviewValueClass(.image($0 != nil ? NSImage(cgImage: $0!, size: $0!.backingSize) : nil, nil)) } |> deliverOnMainQueue) fetch() } - - override func fetch() -> Void { - - fetching.set(chatMessageFileInteractiveFetched(account: account, file: media).start()) + if !media.isStreamable { + if let parent = entry.message { + _ = messageMediaFileInteractiveFetched(context: context, messageId: parent.id, fileReference: FileMediaReference.message(message: MessageReference(parent), media: media)).start() + } else { + _ = freeMediaFileInteractiveFetched(context: context, fileReference: FileMediaReference.standalone(media: media)).start() + } + } } } diff --git a/Telegram-Mac/MainViewController.swift b/Telegram-Mac/MainViewController.swift index 3bc8f888e7..561903fde6 100644 --- a/Telegram-Mac/MainViewController.swift +++ b/Telegram-Mac/MainViewController.swift @@ -8,20 +8,271 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore +import Postbox +import SwiftSignalKit + +#if !APP_STORE +import Sparkle +enum UpdateButtonState { + case common + case important + case critical +} + +final class UpdateTabView : Control { + let textView: TextView = TextView() + let imageView: ImageView = ImageView() + let progressView: ProgressIndicator = ProgressIndicator(frame: NSMakeRect(0, 0, 24, 24)) + + var isChatList: Bool = false + + var isInstalling: Bool = false { + didSet { + textView.isHidden = isInstalling || layoutState == .minimisize + progressView.isHidden = !isInstalling + imageView.isHidden = isInstalling || layoutState != .minimisize + + if layoutState != .minimisize, isInstalling, let superview = self.superview { + self.layer?.cornerRadius = frame.height / 2 + change(size: NSMakeSize(60, frame.height), animated: true, timingFunction: .spring) + change(pos: NSMakePoint(superview.bounds.focus(self.frame.size).minX, self.frame.minY), animated: true, timingFunction: .spring) + progressView.change(pos: self.bounds.focus(progressView.frame.size).origin, animated: true, timingFunction: .spring) + } else { + if let superview = self.superview, isChatList { + change(size: NSMakeSize(self.textView.frame.width + 40, frame.height), animated: true, timingFunction: .spring) + if layoutState != .minimisize { + change(pos: NSMakePoint(superview.bounds.focus(self.frame.size).minX, superview.frame.height - self.frame.height - 60), animated: true, timingFunction: .spring) + } else { + change(pos: NSMakePoint(superview.bounds.focus(self.frame.size).minX, superview.frame.height - self.frame.height), animated: true, timingFunction: .spring) + } + imageView.change(pos: self.bounds.focus(imageView.frame.size).origin, animated: true, timingFunction: .spring) + } + } + + } + } + + var layoutState: SplitViewState = .dual { + didSet { + let installing = self.isInstalling + self.isInstalling = installing + } + } + + + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + textView.userInteractionEnabled = false + textView.isSelectable = false + addSubview(textView) + addSubview(progressView) + addSubview(imageView) + progressView.progressColor = .white + isInstalling = false + + let layout = TextViewLayout(.initialize(string: L10n.updateUpdateTelegram, color: theme.colors.underSelectedColor, font: .medium(.title))) + layout.measure(width: max(280, frame.width)) + textView.update(layout) + + let shadow = NSShadow() + shadow.shadowBlurRadius = 5 + shadow.shadowColor = NSColor.black.withAlphaComponent(0.3) + shadow.shadowOffset = NSMakeSize(0, 2) + self.shadow = shadow + + } + + override func cursorUpdate(with event: NSEvent) { + NSCursor.pointingHand.set() + } + + override var backgroundColor: NSColor { + didSet { + textView.backgroundColor = backgroundColor + } + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + imageView.image = (theme as! TelegramPresentationTheme).icons.appUpdate + imageView.sizeToFit() + needsLayout = true + } + + override func setFrameOrigin(_ newOrigin: NSPoint) { + super.setFrameOrigin(newOrigin) + } + + + override func layout() { + super.layout() + + + + + textView.center() + progressView.center() + imageView.center() + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +final class UpdateTabController: GenericViewController { + private let disposable = MetaDisposable() + private let shakeDisposable = MetaDisposable() + private let context: SharedAccountContext + private var state: UpdateButtonState = .common { + didSet { + switch state { + case .common: + genericView.backgroundColor = theme.colors.accent + case .important: + genericView.backgroundColor = theme.colors.greenUI + case .critical: + genericView.backgroundColor = theme.colors.redUI + } + } + } + private var parentSize: NSSize = .zero + private let stateDisposable = MetaDisposable() + private var appcastItem: SUAppcastItem? { + didSet { + + genericView.isHidden = appcastItem == nil + + + var state = self.state + + if appcastItem != oldValue { + if let appcastItem = appcastItem { + state = appcastItem.isCritical ? .critical : .common + + if state != .critical { + + let importantDelay: Double = 60 * 60 * 24 + let criticalDelay: Double = 60 * 60 * 24 + let updateSignal = Signal.single(.important) |> delay(importantDelay, queue: .mainQueue()) |> then(.single(.critical) |> delay(criticalDelay, queue: .mainQueue())) + + stateDisposable.set(updateSignal.start(next: { [weak self] newState in + self?.state = newState + })) + + } + + } else { + stateDisposable.set(nil) + } + } + self.state = state +// self.updateLayout(self.context.layout, parentSize: parentSize, isChatList: true) + } + } + + init(_ context: SharedAccountContext) { + self.context = context + super.init() + self.bar = NavigationBarStyle(height: 0) + } + + override func viewDidLoad() { + super.viewDidLoad() + let context = self.context + + + genericView.set(background: theme.colors.grayForeground, for: .Normal) + genericView.isHidden = true + genericView.hideAnimated = true + + disposable.set((appUpdateStateSignal |> deliverOnMainQueue).start(next: { [weak self] state in + switch state.loadingState { + case let .readyToInstall(item): + self?.appcastItem = item + self?.genericView.isInstalling = false + case .installing: + self?.genericView.isInstalling = true + default: + self?.appcastItem = nil + self?.genericView.isInstalling = false + } + })) + + genericView.set(handler: { _ in + let authrorized = (NSApp.delegate as? AppDelegate)?.hasAuthorized ?? false + if authrorized, let controller = context.bindings.rootNavigation().controller as? ChatController { + controller.chatInteraction.saveState(true) + } + updateApplication(sharedContext: context) + }, for: .Click) + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let item = self.appcastItem + self.appcastItem = item + } + + func updateLayout(_ layout: SplitViewState, parentSize: NSSize, isChatList: Bool) { + genericView.layoutState = layout + self.parentSize = parentSize + let bottom = parentSize.height - genericView.frame.height + genericView.isChatList = isChatList + if isChatList && layout != .minimisize { + genericView.setFrameSize(NSMakeSize(genericView.textView.frame.width + 40, 40)) + genericView.layer?.cornerRadius = genericView.frame.height / 2 + genericView.centerX(y: layout == .minimisize ? bottom - 10 : bottom - 60) + + var shakeDelay: Double = 60 * 60 + + + let signal = Signal.single(Void()) |> delay(shakeDelay, queue: .mainQueue()) |> then(.single(Void()) |> delay(shakeDelay, queue: .mainQueue()) |> restart) + self.shakeDisposable.set(signal.start(next: { [weak self] in + self?.genericView.shake(beep: false) + })) + } else { + genericView.setFrameSize(NSMakeSize(parentSize.width, 60)) + genericView.setFrameOrigin(NSMakePoint(0, layout == .minimisize ? bottom : bottom - 60)) + genericView.layer?.cornerRadius = 0 + shakeDisposable.set(nil) + } + } + + deinit { + disposable.dispose() + stateDisposable.dispose() + shakeDisposable.dispose() + } +} + +#endif class MainViewController: TelegramViewController { - private var tabController:TabBarController = TabBarController() - private let accountManager:AccountManager - private var contacts:ContactsController - private var chatList:ChatListController - private var settings:AccountViewController + let tabController:TabBarController = TabBarController() + let contacts:ContactsController + let chatListNavigation:NavigationViewController + let settings:AccountViewController private let phoneCalls:RecentCallsViewController private let layoutDisposable:MetaDisposable = MetaDisposable() + private let badgeCountDisposable: MetaDisposable = MetaDisposable() + private let tooltipDisposable = MetaDisposable() + #if !APP_STORE + private let updateController: UpdateTabController + #endif + var isUpChatList: Bool = false { + didSet { + if isUpChatList != oldValue { + tabController.replace(tab: tabController.tab(at: chatIndex).withUpdatedImages(theme.icons.tab_chats, isUpChatList ? theme.icons.tab_chats_active_filters : theme.icons.tab_chats_active), at: chatIndex) + } + } + } + override var navigationController: NavigationViewController? { didSet { @@ -32,33 +283,66 @@ class MainViewController: TelegramViewController { override func viewDidResized(_ size: NSSize) { super.viewDidResized(size) tabController.view.frame = bounds + #if !APP_STORE + updateController.updateLayout(context.sharedContext.layout, parentSize: size, isChatList: true) + #endif } override func loadView() { super.loadView() + + let context = self.context + tabController._frameRect = self._frameRect self.bar = NavigationBarStyle(height: 0) backgroundColor = theme.colors.background addSubview(tabController.view) + #if !APP_STORE + addSubview(updateController.view) + #endif + - tabController.add(tab: TabItem(image: theme.tabBar.icon(key: 0, image: #imageLiteral(resourceName: "Icon_TabContacts"), selected: false), selectedImage: theme.tabBar.icon(key: 0, image: #imageLiteral(resourceName: "Icon_TabContacts_Highlighted"), selected: true), controller: contacts)) + tabController.add(tab: TabItem(image: theme.icons.tab_contacts, selectedImage: theme.icons.tab_contacts_active, controller: contacts)) - tabController.add(tab: TabItem(image: theme.tabBar.icon(key: 1, image: #imageLiteral(resourceName: "Icon_TabRecentCalls"), selected: false), selectedImage: theme.tabBar.icon(key: 1, image: #imageLiteral(resourceName: "Icon_TabRecentCallsHighlighted"), selected: true), controller: phoneCalls)) + tabController.add(tab: TabItem(image: theme.icons.tab_calls, selectedImage: theme.icons.tab_calls_active, controller: phoneCalls)) - tabController.add(tab: TabBadgeItem(account, controller: chatList, image: theme.tabBar.icon(key: 2, image: #imageLiteral(resourceName: "Icon_TabChatList"), selected: false), selectedImage: theme.tabBar.icon(key: 2, image: #imageLiteral(resourceName: "Icon_TabChatList_Highlighted"), selected: true))) + tabController.add(tab: TabBadgeItem(context, controller: chatListNavigation, image: theme.icons.tab_chats, selectedImage: isUpChatList ? theme.icons.tab_chats_active_filters : theme.icons.tab_chats_active, longHoverHandler: { [weak self] control in + self?.showFastChatSettings(control) + })) - tabController.add(tab: TabItem(image: theme.tabBar.icon(key: 3, image: #imageLiteral(resourceName: "Icon_TabSettings"), selected: false), selectedImage: theme.tabBar.icon(key: 3, image: #imageLiteral(resourceName: "Icon_TabSettings_Highlighted"), selected: true), controller: settings, longHoverHandler: { [weak self] control in + tabController.add(tab: TabAllBadgeItem(context, image: theme.icons.tab_settings, selectedImage: theme.icons.tab_settings_active, controller: settings, longHoverHandler: { [weak self] control in self?.showFastSettings(control) - })) - tabController.updateLocalizationAndTheme() + tabController.updateLocalizationAndTheme(theme: theme) +// let s:Signal = Signal.single(arc4random() % 2 == 5) |> then(deferred { +// return Signal.single(arc4random() % 2 == 5) +// } |> delay(10 * 10, queue: .mainQueue()) |> restart) +// |> filter { $0 } +// |> deliverOnMainQueue +// +// tooltipDisposable.set(s.start(next: { [weak self] show in +// +// self?.showFilterTooltip() +// +// })) - self.ready.set(.single(true)) +// account.postbox.transaction ({ transaction -> Void in +// +// +// }).start() - layoutDisposable.set(account.context.layoutHandler.get().start(next: { [weak self] state in - self?.tabController.hideTabView(state == .minimisize) + self.ready.set(combineLatest(queue: prepareQueue, self.chatList.ready.get(), self.settings.ready.get()) |> map { $0 && $1 }) + + layoutDisposable.set(context.sharedContext.layoutHandler.get().start(next: { [weak self] state in + guard let `self` = self else { + return + } + self.tabController.hideTabView(state == .minimisize) + #if !APP_STORE + self.updateController.updateLayout(state, parentSize: self.frame.size, isChatList: true) + #endif })) tabController.didChangedIndex = { [weak self] index in @@ -66,109 +350,285 @@ class MainViewController: TelegramViewController { } } - private let settingsDisposable = MetaDisposable() + func prepareControllers() { + chatList.loadViewIfNeeded(bounds) + settings.loadViewIfNeeded(bounds) + } - private func showFastSettings(_ control:Control) { + private func showCallsTab() { + tabController.insert(tab: TabItem(image: theme.icons.tab_calls, selectedImage: theme.icons.tab_calls_active, controller: phoneCalls), at: 1) + } + private func hideCallsTab() { + tabController.remove(at: 1) + } + + private func showFilterTooltip() { + tabController.showTooltip(text: L10n.chatListFilterTooltip, for: showCallTabs ? 2 : 1) + } + + private var showCallTabs: Bool = true + + override func viewDidLoad() { + super.viewDidLoad() - let passcodeData = account.postbox.modify { modifier -> PostboxAccessChallengeData in - return modifier.getAccessChallengeData() - } |> deliverOnMainQueue - let applicationSettings = appNotificationSettings(postbox: account.postbox) |> take(1) |> deliverOnMainQueue + chatListNavigation.hasBarRightBorder = true - - settingsDisposable.set(combineLatest(passcodeData, applicationSettings).start(next: { [weak self] passcode, notifications in - self?._showFast(control: control, passcodeData: passcode, notifications: notifications) + prefDisposable.set((baseAppSettings(accountManager: context.sharedContext.accountManager) |> deliverOnMainQueue).start(next: { [weak self] settings in + guard let `self` = self else {return} + if settings.showCallsTab != self.showCallTabs { + self.showCallTabs = settings.showCallsTab + if self.showCallTabs { + self.showCallsTab() + } else { + self.hideCallsTab() + } + } + })) + } + + private func _showFastChatSettings(_ control: Control, unreadCount: Int32) { + var items: [SPopoverItem] = [] + let context = self.context + + if unreadCount > 0 { + items.append(SPopoverItem(L10n.chatListPopoverReadAll, { + confirm(for: context.window, information: L10n.chatListPopoverConfirm, successHandler: { _ in + _ = context.account.postbox.transaction ({ transaction -> Void in + markAllChatsAsReadInteractively(transaction: transaction, viewTracker: context.account.viewTracker, groupId: .root, filterPredicate: nil) + markAllChatsAsReadInteractively(transaction: transaction, viewTracker: context.account.viewTracker, groupId: Namespaces.PeerGroup.archive, filterPredicate: nil) + }).start() + }) + })) + } + + if self.tabController.current == chatListNavigation, !items.isEmpty { + showPopover(for: control, with: SPopoverViewController(items: items), edge: .maxX, inset: NSMakePoint(control.frame.width + 12, 0)) + } + } + + func showFastChatSettings() { + self.showFastChatSettings(tabController.control(for: self.chatIndex)) + } + + private func showFastChatSettings(_ control: Control) { + + let context = self.context + let unreadCountsKey = PostboxViewKey.unreadCounts(items: [.total(nil)]) + + _ = (context.account.postbox.combinedView(keys: [unreadCountsKey]) |> take(1) |> deliverOnMainQueue).start(next: { [weak self, weak control] view in + let totalUnreadState: ChatListTotalUnreadState + if let value = view.views[unreadCountsKey] as? UnreadMessageCountsView, let (_, total) = value.total() { + totalUnreadState = total + } else { + totalUnreadState = ChatListTotalUnreadState(absoluteCounters: [:], filteredCounters: [:]) + } + let total = totalUnreadState.absoluteCounters.reduce(0, { current, value in + return current + value.value.messageCount + }) + if let control = control { + self?._showFastChatSettings(control, unreadCount: total) + } + }) + } + private let filterMenuDisposable = MetaDisposable() + private let settingsDisposable = MetaDisposable() + private let prefDisposable = MetaDisposable() + private weak var quickController: ViewController? + private func showFastSettings(_ control:Control) { + + let passcodeData = context.sharedContext.accountManager.transaction { transaction -> PostboxAccessChallengeData in + return transaction.getAccessChallengeData() + } |> deliverOnMainQueue + + let applicationSettings = appNotificationSettings(accountManager: context.sharedContext.accountManager) |> take(1) |> deliverOnMainQueue + + + settingsDisposable.set(combineLatest(passcodeData, applicationSettings, context.sharedContext.activeAccountsWithInfo |> take(1) |> map {$0.accounts} |> deliverOnMainQueue).start(next: { [weak self] passcode, notifications, accounts in + self?._showFast(control: control, accounts: accounts, passcodeData: passcode, notifications: notifications) })) } - private func _showFast( control: Control, passcodeData: PostboxAccessChallengeData, notifications: InAppNotificationSettings) { + private func _showFast( control: Control, accounts: [AccountWithInfo], passcodeData: PostboxAccessChallengeData, notifications: InAppNotificationSettings) { + + if let popover = control.popover { + popover.hide() + return + } + var items:[SPopoverItem] = [] - + let context = self.context + var headerItems: [TableRowItem] = [] + for account in accounts { + if account.account.id != context.account.id { + + let item = ShortPeerRowItem(NSZeroSize, peer: account.peer, account: account.account, height: 40, photoSize: NSMakeSize(25, 25), titleStyle: ControlStyle(font: .normal(.title), foregroundColor: theme.colors.text, highlightColor: .white), drawCustomSeparator: false, inset: NSEdgeInsets(left: 10), action: { + context.sharedContext.switchToAccount(id: account.account.id, action: nil) + }, highlightOnHover: true, badgeNode: GlobalBadgeNode(account.account, sharedContext: context.sharedContext, getColor: { selected in + if selected { + return theme.colors.underSelectedColor + } else { + return theme.colors.accent + } + }), compactText: true) + + headerItems.append(item) +// items.append(SPopoverItem(account.peer.displayTitle, { +// context.sharedContext.switchToAccount(id: account.account.id) +// })) + } + + } + switch passcodeData { case .none: - items.append(SPopoverItem(tr(.fastSettingsSetPasscode), { [weak self] in - if let account = self?.account { - self?.tabController.select(index: 3) - account.context.mainNavigation?.push(PasscodeSettingsViewController(account)) - } + items.append(SPopoverItem(tr(L10n.fastSettingsSetPasscode), { [weak self] in + guard let `self` = self else {return} + self.tabController.select(index: self.tabController.count - 1) + self.context.sharedContext.bindings.rootNavigation().push(PasscodeSettingsViewController(self.context)) }, theme.icons.fastSettingsLock)) default: - items.append(SPopoverItem(tr(.fastSettingsLockTelegram), { - if let event = NSEvent.keyEvent(with: .keyDown, location: NSZeroPoint, modifierFlags: [.command], timestamp: Date().timeIntervalSince1970, windowNumber: mainWindow.windowNumber, context: nil, characters: "", charactersIgnoringModifiers: "", isARepeat: false, keyCode: KeyboardKey.L.rawValue) { - mainWindow.sendEvent(event) - } + items.append(SPopoverItem(tr(L10n.fastSettingsLockTelegram), { + context.window.sendKeyEvent(KeyboardKey.L, modifierFlags: [.command]) }, theme.icons.fastSettingsLock)) } - - items.append(SPopoverItem(theme.dark ? tr(.fastSettingsDisableDarkMode) : tr(.fastSettingsEnableDarkMode), { [weak self] in - if let strongSelf = self { - _ = updateThemeSettings(postbox: strongSelf.account.postbox, pallete: !theme.dark ? darkPallete : whitePallete, dark: !theme.dark).start() - } - }, theme.dark ? theme.icons.fastSettingsSunny : theme.icons.fastSettingsDark)) + items.append(SPopoverItem(theme.colors.isDark ? L10n.fastSettingsDisableDarkMode : L10n.fastSettingsEnableDarkMode, { + let nightSettings = autoNightSettings(accountManager: context.sharedContext.accountManager) |> take(1) |> deliverOnMainQueue + + _ = nightSettings.start(next: { settings in + if settings.systemBased || settings.schedule != nil { + confirm(for: context.window, header: L10n.darkModeConfirmNightModeHeader, information: L10n.darkModeConfirmNightModeText, okTitle: L10n.darkModeConfirmNightModeOK, successHandler: { _ in + + _ = context.sharedContext.accountManager.transaction { transaction -> Void in + transaction.updateSharedData(ApplicationSharedPreferencesKeys.autoNight, { entry in + let settings: AutoNightThemePreferences = entry as? AutoNightThemePreferences ?? AutoNightThemePreferences.defaultSettings + return settings.withUpdatedSystemBased(false).withUpdatedSchedule(nil) + }) + transaction.updateSharedData(ApplicationSharedPreferencesKeys.themeSettings, { entry in + let settings = entry as? ThemePaletteSettings ?? ThemePaletteSettings.defaultTheme + return settings.withUpdatedToDefault(dark: !theme.colors.isDark).withUpdatedDefaultIsDark(!theme.colors.isDark) + }) + }.start() + }) + } else { + _ = updateThemeInteractivetly(accountManager: context.sharedContext.accountManager, f: { settings -> ThemePaletteSettings in + return settings.withUpdatedToDefault(dark: !theme.colors.isDark).withUpdatedDefaultIsDark(!theme.colors.isDark) + }).start() + } + }) + }, theme.colors.isDark ? theme.icons.fastSettingsSunny : theme.icons.fastSettingsDark)) + let time = Int32(Date().timeIntervalSince1970) let unmuted = notifications.muteUntil < time - items.append(SPopoverItem(unmuted ? tr(.fastSettingsMute2Hours) : tr(.fastSettingsUnmute), { [weak self] in - if let account = self?.account { + items.append(SPopoverItem(unmuted ? tr(L10n.fastSettingsMute2Hours) : tr(L10n.fastSettingsUnmute), { [weak self] in + if let context = self?.context { let time = Int32(Date().timeIntervalSince1970 + 2 * 60 * 60) - _ = updateInAppNotificationSettingsInteractively(postbox: account.postbox, {$0.withUpdatedMuteUntil(unmuted ? time : 0)}).start() + _ = updateInAppNotificationSettingsInteractively(accountManager: context.sharedContext.accountManager, {$0.withUpdatedMuteUntil(unmuted ? time : 0)}).start() } }, notifications.muteUntil < time ? theme.icons.fastSettingsMute : theme.icons.fastSettingsUnmute)) - let controller = SPopoverViewController(items: items) - if self.tabController.current != settings { - showPopover(for: control, with: controller, edge: .maxX, inset: NSMakePoint(control.frame.width - 12, 0)) - } + let controller = SPopoverViewController(items: items, visibility: 10, headerItems: headerItems) + showPopover(for: control, with: controller, edge: .maxX, inset: NSMakePoint(control.frame.width - 12, 0)) + self.quickController = controller } - - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - tabController.updateLocalizationAndTheme() - + private var previousTheme:TelegramPresentationTheme? + private var previousIconColor:NSColor? + private var previousIsUpChatList: Bool? + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + tabController.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) + #if !APP_STORE + updateController.updateLocalizationAndTheme(theme: theme) + #endif - if !tabController.isEmpty { - tabController.replace(tab: tabController.tab(at: 0).withUpdatedImages(theme.tabBar.icon(key: 0, image: #imageLiteral(resourceName: "Icon_TabContacts"), selected: false), theme.tabBar.icon(key: 0, image: #imageLiteral(resourceName: "Icon_TabContacts_Highlighted"), selected: true)), at: 0) - - tabController.replace(tab: tabController.tab(at: 1).withUpdatedImages(theme.tabBar.icon(key: 1, image: #imageLiteral(resourceName: "Icon_TabRecentCalls"), selected: false), theme.tabBar.icon(key: 1, image: #imageLiteral(resourceName: "Icon_TabRecentCallsHighlighted"), selected: true)), at: 1) - - tabController.replace(tab: tabController.tab(at: 2).withUpdatedImages(theme.tabBar.icon(key: 2, image: #imageLiteral(resourceName: "Icon_TabChatList"), selected: false), theme.tabBar.icon(key: 2, image: #imageLiteral(resourceName: "Icon_TabChatList_Highlighted"), selected: true)), at: 2) + updateTabsIfNeeded() + self.tabController.view.needsLayout = true + } + + private func updateTabsIfNeeded() { + if !tabController.isEmpty && (previousTheme?.colors != theme.colors || previousIconColor != theme.colors.accentIcon || self.isUpChatList != self.previousIsUpChatList) { + var index: Int = 0 + tabController.replace(tab: tabController.tab(at: index).withUpdatedImages(theme.icons.tab_contacts, theme.icons.tab_contacts_active), at: index) + index += 1 + if showCallTabs { + tabController.replace(tab: tabController.tab(at: index).withUpdatedImages(theme.icons.tab_calls, theme.icons.tab_calls_active), at: index) + index += 1 + } - tabController.replace(tab: tabController.tab(at: 3).withUpdatedImages(theme.tabBar.icon(key: 3, image: #imageLiteral(resourceName: "Icon_TabSettings"), selected: false), theme.tabBar.icon(key: 3, image: #imageLiteral(resourceName: "Icon_TabSettings_Highlighted"), selected: true)), at: 3) + tabController.replace(tab: tabController.tab(at: index).withUpdatedImages(theme.icons.tab_chats, isUpChatList ? theme.icons.tab_chats_active_filters : theme.icons.tab_chats_active), at: index) + index += 1 + tabController.replace(tab: tabController.tab(at: index).withUpdatedImages(theme.icons.tab_settings, theme.icons.tab_settings_active), at: index) } + self.previousTheme = theme + self.previousIconColor = theme.colors.accentIcon + self.previousIsUpChatList = self.isUpChatList } + private var previousIndex: Int? = nil + func checkSettings(_ index:Int) { + let isSettings = tabController.tab(at: index).controller is AccountViewController - if index == 3 && account.context.layout != .single { - account.context.mainNavigation?.push(GeneralSettingsViewController(account), false) + let navigation = context.sharedContext.bindings.rootNavigation() + + if let controller = navigation.controller as? InputDataController, controller.identifier == "wallet-create" { + self.previousIndex = index + quickController?.popover?.hide() } else { - - account.context.mainNavigation?.enumerateControllers( { controller, index in - if (controller is ChatController) || (controller is PeerInfoController) || (controller is GroupAdminsController) || (controller is GroupAdminsController) || (controller is ChannelAdminsViewController) || (controller is ChannelAdminsViewController) || (controller is EmptyChatViewController) { - self.backFromSettings(index) - return true + if previousIndex == tabController.count - 1 || isSettings { + if isSettings && context.sharedContext.layout != .single { + navigation.push(GeneralSettingsViewController(context), false) + } else { + navigation.enumerateControllers( { controller, index in + if (controller is ChatController) || (controller is PeerInfoController) || (controller is ChannelAdminsViewController) || (controller is ChannelAdminsViewController) || (controller is EmptyChatViewController) { + self.backFromSettings(index) + return true + } + return false + }) } - return false - }) + } + self.previousIndex = index + quickController?.popover?.hide() } } private func backFromSettings(_ index:Int) { - account.context.mainNavigation?.to(index: index) + context.sharedContext.bindings.rootNavigation().to(index: index) + } + + override func focusSearch(animated: Bool, text: String? = nil) { + if context.sharedContext.layout == .minimisize { + return + } + let animated = animated && (context.sharedContext.layout != .single || context.sharedContext.bindings.rootNavigation().stackCount == 1) + if context.sharedContext.layout == .single { + context.sharedContext.bindings.rootNavigation().close() + } + if let current = tabController.current { + if current is AccountViewController { + tabController.select(index: chatIndex) + } + tabController.current?.focusSearch(animated: animated, text: text) + } } override func getCenterBarViewOnce() -> TitledBarView { return TitledBarView(controller: self) } + private var firstTime: Bool = true override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - if !animated { - self.tabController.select(index:2) + if firstTime { + // self.tabController.select(index: chatIndex) + firstTime = false } + self.tabController.current?.viewDidAppear(animated) } override func viewWillAppear(_ animated: Bool) { @@ -182,28 +642,84 @@ class MainViewController: TelegramViewController { self.tabController.current?.viewWillDisappear(animated) } + var chatIndex: Int { + if showCallTabs { + return 2 + } else { + return 1 + } + } + + var settingsIndex: Int { + if showCallTabs { + return 3 + } else { + return 2 + } + } + + override func navigationUndoHeaderDidNoticeAnimation(_ current: CGFloat, _ previous: CGFloat, _ animated: Bool) -> ()->Void { + if let controller = self.tabController.current { + return controller.navigationUndoHeaderDidNoticeAnimation(current, previous, animated) + } + return {} + } + + func openChat(_ index: Int, force: Bool = false) { + if self.tabController.current == chatListNavigation { + chatList.openChat(index, force: force) + } + } + + var chatList: ChatListController { + return chatListNavigation.controller as! ChatListController + } + func showPreferences() { - if self.account.context.layout != .minimisize { - self.tabController.select(index:3) + context.sharedContext.bindings.switchSplitLayout(.dual) + if self.context.sharedContext.layout != .minimisize { + if self.context.sharedContext.layout == .single { + self.navigationController?.close() + } + self.tabController.select(index:settingsIndex) } } + func showChatList() { + self.tabController.select(index: self.chatIndex) + } + + override var responderPriority: HandlerPriority { + return context.sharedContext.layout == .single ? .medium : .low + } + func isCanMinimisize() -> Bool{ - return self.tabController.current == chatList + return self.tabController.current == chatListNavigation || self.tabController.current == contacts || self.tabController.current == self.phoneCalls + } + + override func updateFrame(_ frame: NSRect, animated: Bool) { + super.updateFrame(frame, animated: animated) + self.tabController.updateFrame(frame.size.bounds, animated: animated) } - init(_ account:Account, accountManager:AccountManager) { + override init(_ context: AccountContext) { - self.accountManager = accountManager - chatList = ChatListController(account) - contacts = ContactsController(account) - settings = AccountViewController(account, accountManager: accountManager) - phoneCalls = RecentCallsViewController(account) - super.init(account) + chatListNavigation = NavigationViewController(ChatListController(context), context.window) + contacts = ContactsController(context) + settings = AccountViewController(context) + phoneCalls = RecentCallsViewController(context) + #if !APP_STORE + updateController = UpdateTabController(context.sharedContext) + #endif + super.init(context) bar = NavigationBarStyle(height: 0) + // chatListNavigation.alwaysAnimate = true } deinit { layoutDisposable.dispose() + prefDisposable.dispose() + settingsDisposable.dispose() + filterMenuDisposable.dispose() } } diff --git a/Telegram-Mac/MajorBackNavigationBar.swift b/Telegram-Mac/MajorBackNavigationBar.swift index 33044e41a0..700bebfc6a 100644 --- a/Telegram-Mac/MajorBackNavigationBar.swift +++ b/Telegram-Mac/MajorBackNavigationBar.swift @@ -8,33 +8,42 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac -import TelegramCoreMac -import PostboxMac +import SwiftSignalKit +import TelegramCore + +import Postbox class MajorBackNavigationBar: BackNavigationBar { private let disposable:MetaDisposable = MetaDisposable() - private let account:Account + private let context:AccountContext private let peerId:PeerId private let badgeNode:GlobalBadgeNode - init(_ controller: ViewController, account:Account, excludePeerId:PeerId) { - self.account = account + init(_ controller: ViewController, context: AccountContext, excludePeerId:PeerId) { + self.context = context self.peerId = excludePeerId - badgeNode = GlobalBadgeNode(account, excludePeerId: excludePeerId) - badgeNode.xInset = -22 + + var layoutChanged:(()->Void)? = nil + badgeNode = GlobalBadgeNode(context.account, sharedContext: context.sharedContext, excludeGroupId: Namespaces.PeerGroup.archive, view: View(), layoutChanged: { + layoutChanged?() + }) + badgeNode.xInset = 0 + + super.init(controller) - disposable.set((account.applicationContext as? TelegramApplicationContext)?.layoutHandler.get().start(next: { [weak self] state in - if let strongSelf = self { - switch state { - case .single: - strongSelf.badgeNode.view?.isHidden = false - default: - strongSelf.badgeNode.view?.isHidden = true - } - } - })) addSubview(badgeNode.view!) + + layoutChanged = { [weak self] in + self?.needsLayout = true + } + + } + + override func layout() { + super.layout() + + self.badgeNode.view!.setFrameOrigin(NSMakePoint(min(frame.width == minWidth ? 30 : 22, frame.width - self.badgeNode.view!.frame.width - 4), 4)) + } deinit { diff --git a/Telegram-Mac/ManageSharedAccountInfo.swift b/Telegram-Mac/ManageSharedAccountInfo.swift new file mode 100644 index 0000000000..9f6fd10482 --- /dev/null +++ b/Telegram-Mac/ManageSharedAccountInfo.swift @@ -0,0 +1,86 @@ +// +// ManageSharedAccountInfo.swift +// Telegram +// +// Created by Mikhail Filimonov on 28/02/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Foundation +import SwiftSignalKit +import TelegramCore + +import Postbox + +private func accountInfo(account: Account) -> Signal { + let peerName = account.postbox.transaction { transaction -> String in + guard let peer = transaction.getPeer(account.peerId) else { + return "" + } + if let addressName = peer.addressName { + return "\(addressName)" + } + return peer.displayTitle + } + + let primaryDatacenterId = Int32(account.network.datacenterId) + let context = account.network.context + + var datacenters: [Int32: AccountDatacenterInfo] = [:] + for nId in context.knownDatacenterIds() { + if let id = nId as? Int { + if let authInfo = context.authInfoForDatacenter(withId: id, selector: .persistent), let authKey = authInfo.authKey { + let transportScheme = context.chooseTransportSchemeForConnection(toDatacenterId: id, schemes: context.transportSchemesForDatacenter(withId: id, media: true, enforceMedia: false, isProxy: false)) + var addressList: [AccountDatacenterAddress] = [] + if let transportScheme = transportScheme, let host = transportScheme.address.host { + let secret: Data? = transportScheme.address.secret + addressList.append(AccountDatacenterAddress(host: host, port: Int32(transportScheme.address.port), isMedia: transportScheme.address.preferForMedia, secret: secret)) + } + datacenters[Int32(id)] = AccountDatacenterInfo(masterKey: AccountDatacenterKey(id: authInfo.authKeyId, data: authKey), addressList: addressList) + } + } + } + + let notificationKey = masterNotificationsKey(account: account, ignoreDisabled: false) + + return combineLatest(peerName, notificationKey) + |> map { peerName, notificationKey -> StoredAccountInfo in + return StoredAccountInfo(id: account.id.int64, primaryId: primaryDatacenterId, isTestingEnvironment: account.testingEnvironment, peerName: peerName, datacenters: datacenters, notificationKey: AccountNotificationKey(id: notificationKey.id, data: notificationKey.data)) + } +} + +func sharedAccountInfos(accountManager: AccountManager, accounts: Signal<[Account], NoError>) -> Signal { + return combineLatest(accountManager.sharedData(keys: [SharedDataKeys.proxySettings]), accounts) + |> mapToSignal { sharedData, accounts -> Signal in + let proxySettings = sharedData.entries[SharedDataKeys.proxySettings] as? ProxySettings + let proxy = proxySettings?.effectiveActiveServer.flatMap { proxyServer -> AccountProxyConnection? in + var username: String? + var password: String? + var secret: Data? + switch proxyServer.connection { + case let .socks5(usernameValue, passwordValue): + username = usernameValue + password = passwordValue + case let .mtp(secretValue): + secret = secretValue + } + return AccountProxyConnection(host: proxyServer.host, port: proxyServer.port, username: username, password: password, secret: secret) + } + + return combineLatest(accounts.map(accountInfo)) + |> map { infos -> StoredAccountInfos in + return StoredAccountInfos(proxy: proxy, accounts: infos) + } + } +} + +func storeAccountsData(rootPath: String, accounts: StoredAccountInfos) { + guard let data = try? JSONEncoder().encode(accounts) else { + Logger.shared.log("storeAccountsData", "Error encoding data") + return + } + guard let _ = try? data.write(to: URL(fileURLWithPath: rootPath + "/accounts-shared-data")) else { + Logger.shared.log("storeAccountsData", "Error saving data") + return + } +} diff --git a/Telegram-Mac/ManagedAudioSession.swift b/Telegram-Mac/ManagedAudioSession.swift new file mode 100755 index 0000000000..07792f52a3 --- /dev/null +++ b/Telegram-Mac/ManagedAudioSession.swift @@ -0,0 +1,487 @@ +import Foundation +import SwiftSignalKit +import AVFoundation + +enum ManagedAudioSessionType { + case play + case playAndRecord + case voiceCall +} + +private func nativeCategoryForType(_ type: ManagedAudioSessionType) -> String { + switch type { + case .play: + return "AVAudioSessionCategoryPlayback" + case .playAndRecord, .voiceCall: + return "AVAudioSessionCategoryPlayAndRecord" + } +} + +private func allowBluetoothForType(_ type: ManagedAudioSessionType) -> Bool { + switch type { + case .play: + return false + case .playAndRecord, .voiceCall: + return true + } +} + +public enum AudioSessionOutput { + case speaker +} + +public enum AudioSessionOutputMode: Equatable { + case system + case speakerIfNoHeadphones + case custom(AudioSessionOutput) + + public static func ==(lhs: AudioSessionOutputMode, rhs: AudioSessionOutputMode) -> Bool { + switch lhs { + case .system: + if case .system = rhs { + return true + } else { + return false + } + case .speakerIfNoHeadphones: + if case .speakerIfNoHeadphones = rhs { + return true + } else { + return false + } + case let .custom(output): + if case .custom(output) = rhs { + return true + } else { + return false + } + } + } +} + +private final class HolderRecord { + let id: Int32 + let audioSessionType: ManagedAudioSessionType + let control: ManagedAudioSessionControl + let activate: (ManagedAudioSessionControl) -> Void + let deactivate: () -> Signal + let headsetConnectionStatusChanged: (Bool) -> Void + let once: Bool + var outputMode: AudioSessionOutputMode + var active: Bool = false + var deactivatingDisposable: Disposable? = nil + + init(id: Int32, audioSessionType: ManagedAudioSessionType, control: ManagedAudioSessionControl, activate: @escaping (ManagedAudioSessionControl) -> Void, deactivate: @escaping () -> Signal, headsetConnectionStatusChanged: @escaping (Bool) -> Void, once: Bool, outputMode: AudioSessionOutputMode) { + self.id = id + self.audioSessionType = audioSessionType + self.control = control + self.activate = activate + self.deactivate = deactivate + self.headsetConnectionStatusChanged = headsetConnectionStatusChanged + self.once = once + self.outputMode = outputMode + } +} + +private final class ManagedAudioSessionControlActivate { + let f: (AudioSessionActivationState) -> Void + + init(_ f: @escaping (AudioSessionActivationState) -> Void) { + self.f = f + } +} + +public struct AudioSessionActivationState { + public let isHeadsetConnected: Bool +} + +public class ManagedAudioSessionControl { + private let setupImpl: (Bool) -> Void + private let activateImpl: (ManagedAudioSessionControlActivate) -> Void + private let setOutputModeImpl: (AudioSessionOutputMode) -> Void + + fileprivate init(setupImpl: @escaping (Bool) -> Void, activateImpl: @escaping (ManagedAudioSessionControlActivate) -> Void, setOutputModeImpl: @escaping (AudioSessionOutputMode) -> Void) { + self.setupImpl = setupImpl + self.activateImpl = activateImpl + self.setOutputModeImpl = setOutputModeImpl + } + + public func setup(synchronous: Bool = false) { + self.setupImpl(synchronous) + } + + public func activate(_ completion: @escaping (AudioSessionActivationState) -> Void) { + self.activateImpl(ManagedAudioSessionControlActivate(completion)) + } + + public func setOutputMode(_ mode: AudioSessionOutputMode) { + self.setOutputModeImpl(mode) + } +} + +public final class ManagedAudioSession { + private var nextId: Int32 = 0 + private let queue = Queue() + private var holders: [HolderRecord] = [] + private var currentTypeAndOutputMode: (ManagedAudioSessionType, AudioSessionOutputMode)? + private var deactivateTimer: SwiftSignalKit.Timer? + + private var isHeadsetPluggedInValue = false + private let outputsToHeadphonesSubscribers = Bag<(Bool) -> Void>() + private let isActiveSubscribers = Bag<(Bool) -> Void>() + + init() { + let queue = self.queue + + + queue.async { + self.isHeadsetPluggedInValue = self.isHeadsetPluggedIn() + } + } + + deinit { + self.deactivateTimer?.invalidate() + } + + func headsetConnected() -> Signal { + let queue = self.queue + return Signal { [weak self] subscriber in + if let strongSelf = self { + subscriber.putNext(strongSelf.isHeadsetPluggedInValue) + + let index = strongSelf.outputsToHeadphonesSubscribers.add({ value in + subscriber.putNext(value) + }) + + return ActionDisposable { + queue.async { + if let strongSelf = self { + strongSelf.outputsToHeadphonesSubscribers.remove(index) + } + } + } + } else { + return EmptyDisposable + } + } |> runOn(queue) + } + + public func isActive() -> Signal { + let queue = self.queue + return Signal { [weak self] subscriber in + if let strongSelf = self { + subscriber.putNext(strongSelf.currentTypeAndOutputMode != nil) + + let index = strongSelf.isActiveSubscribers.add({ value in + subscriber.putNext(value) + }) + + return ActionDisposable { + queue.async { + if let strongSelf = self { + strongSelf.isActiveSubscribers.remove(index) + } + } + } + } else { + return EmptyDisposable + } + } |> runOn(queue) + } + + func push(audioSessionType: ManagedAudioSessionType, outputMode: AudioSessionOutputMode = .system, once: Bool = false, activate: @escaping (AudioSessionActivationState) -> Void, deactivate: @escaping () -> Signal) -> Disposable { + return self.push(audioSessionType: audioSessionType, once: once, manualActivate: { control in + control.setup() + control.activate({ state in + activate(state) + }) + }, deactivate: deactivate) + } + + func push(audioSessionType: ManagedAudioSessionType, outputMode: AudioSessionOutputMode = .system, once: Bool = false, manualActivate: @escaping (ManagedAudioSessionControl) -> Void, deactivate: @escaping () -> Signal, headsetConnectionStatusChanged: @escaping (Bool) -> Void = { _ in }) -> Disposable { + let id = OSAtomicIncrement32(&self.nextId) + self.queue.async { + self.holders.append(HolderRecord(id: id, audioSessionType: audioSessionType, control: ManagedAudioSessionControl(setupImpl: { [weak self] synchronous in + if let strongSelf = self { + let f: () -> Void = { + for holder in strongSelf.holders { + if holder.id == id && holder.active { + strongSelf.setup(type: audioSessionType, outputMode: holder.outputMode) + break + } + } + } + + if synchronous { + strongSelf.queue.sync(f) + } else { + strongSelf.queue.async(f) + } + } + }, activateImpl: { [weak self] completion in + if let strongSelf = self { + strongSelf.queue.async { + for holder in strongSelf.holders { + if holder.id == id && holder.active { + strongSelf.activate() + completion.f(AudioSessionActivationState(isHeadsetConnected: strongSelf.isHeadsetPluggedInValue)) + break + } + } + } + } + }, setOutputModeImpl: { [weak self] value in + if let strongSelf = self { + strongSelf.queue.async { + for holder in strongSelf.holders { + if holder.id == id { + if holder.outputMode != value { + holder.outputMode = value + } + + if holder.active { + strongSelf.updateOutputMode(value) + } + } + } + } + } + }), activate: manualActivate, deactivate: deactivate, headsetConnectionStatusChanged: headsetConnectionStatusChanged, once: once, outputMode: outputMode)) + self.updateHolders() + } + return ActionDisposable { [weak self] in + if let strongSelf = self { + strongSelf.queue.async { + strongSelf.removeDeactivatedHolder(id: id) + } + } + } + } + + func dropAll() { + self.queue.async { + self.updateHolders(interruption: true) + } + } + + private func removeDeactivatedHolder(id: Int32) { + assert(self.queue.isCurrent()) + + for i in 0 ..< self.holders.count { + if self.holders[i].id == id { + self.holders[i].deactivatingDisposable?.dispose() + self.holders.remove(at: i) + self.updateHolders() + break + } + } + } + + private func updateHolders(interruption: Bool = false) { + assert(self.queue.isCurrent()) + + print("holder count \(self.holders.count)") + + if !self.holders.isEmpty { + var activeIndex: Int? + var deactivating = false + var index = 0 + for record in self.holders { + if record.active { + activeIndex = index + break + } + else if record.deactivatingDisposable != nil { + deactivating = true + } + index += 1 + } + if !deactivating { + if let activeIndex = activeIndex { + var deactivate = false + + if interruption { + if self.holders[activeIndex].audioSessionType != .voiceCall { + deactivate = true + } + } else { + if activeIndex != self.holders.count - 1 { + if self.holders[activeIndex].audioSessionType == .voiceCall { + deactivate = false + } else { + deactivate = true + } + } + } + + if deactivate { + self.holders[activeIndex].active = false + let id = self.holders[activeIndex].id + self.holders[activeIndex].deactivatingDisposable = (self.holders[activeIndex].deactivate() |> deliverOn(self.queue)).start(completed: { [weak self] in + if let strongSelf = self { + var index = 0 + for currentRecord in strongSelf.holders { + if currentRecord.id == id { + currentRecord.deactivatingDisposable = nil + if currentRecord.once { + strongSelf.holders.remove(at: index) + } + break + } + index += 1 + } + strongSelf.updateHolders() + } + }) + } + } else if activeIndex == nil { + let lastIndex = self.holders.count - 1 + + self.deactivateTimer?.invalidate() + self.deactivateTimer = nil + + self.holders[lastIndex].active = true + self.holders[lastIndex].activate(self.holders[lastIndex].control) + } + } + } else { + self.applyNoneDelayed() + } + } + + private func applyNoneDelayed() { + self.deactivateTimer?.invalidate() + + if self.currentTypeAndOutputMode?.0 == .voiceCall { + self.applyNone() + } else { + let deactivateTimer = SwiftSignalKit.Timer(timeout: 1.0, repeat: false, completion: { [weak self] in + if let strongSelf = self { + strongSelf.applyNone() + } + }, queue: self.queue) + self.deactivateTimer = deactivateTimer + deactivateTimer.start() + } + } + + private func isHeadsetPluggedIn() -> Bool { + assert(self.queue.isCurrent()) + +// let route = AVAudioSession.sharedInstance().currentRoute +// //print("\(route)") +// for desc in route.outputs { +// if desc.portType == AVAudioSessionPortHeadphones || desc.portType == AVAudioSessionPortBluetoothA2DP || desc.portType == AVAudioSessionPortBluetoothHFP { +// return true +// } +// } + + return false + } + + private func applyNone() { + self.deactivateTimer?.invalidate() + self.deactivateTimer = nil + + let wasActive = self.currentTypeAndOutputMode != nil + self.currentTypeAndOutputMode = nil + +// print("ManagedAudioSession setting active false") +// do { +// try AVAudioSession.sharedInstance().setActive(false, with: [.notifyOthersOnDeactivation]) +// } catch let error { +// print("ManagedAudioSession applyNone error \(error)") +// } + + if wasActive { + for subscriber in self.isActiveSubscribers.copyItems() { + subscriber(false) + } + } + } + + private func setup(type: ManagedAudioSessionType, outputMode: AudioSessionOutputMode) { + self.deactivateTimer?.invalidate() + self.deactivateTimer = nil + + let wasActive = self.currentTypeAndOutputMode != nil + + if self.currentTypeAndOutputMode == nil || self.currentTypeAndOutputMode! != (type, outputMode) { + self.currentTypeAndOutputMode = (type, outputMode) +// +// do { +// print("ManagedAudioSession setting category for \(type)") +// try AVAudioSession.sharedInstance().setCategory(nativeCategoryForType(type), with: AVAudioSessionCategoryOptions(rawValue: allowBluetoothForType(type) ? AVAudioSessionCategoryOptions.allowBluetooth.rawValue : 0)) +// print("ManagedAudioSession setting active \(type != .none)") +// try AVAudioSession.sharedInstance().setMode(type == .voiceCall ? AVAudioSessionModeVoiceChat : AVAudioSessionModeDefault) +// } catch let error { +// print("ManagedAudioSession setup error \(error)") +// } + } + + if !wasActive { + for subscriber in self.isActiveSubscribers.copyItems() { + subscriber(true) + } + } + } + + private func setupOutputMode(_ outputMode: AudioSessionOutputMode) throws { +// switch outputMode { +// case .system: +// try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none) +// case let .custom(output): +// switch output { +// case .speaker: +// try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker) +// } +// case .speakerIfNoHeadphones: +// if !self.isHeadsetPluggedInValue { +// try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker) +// } +// } + } + + private func activate() { + if let (type, outputMode) = self.currentTypeAndOutputMode { +// do { +// try AVAudioSession.sharedInstance().setActive(true) +// +// try self.setupOutputMode(outputMode) +// +// if case .voiceCall = type { +// try AVAudioSession.sharedInstance().setPreferredIOBufferDuration(0.005) +// } +// } catch let error { +// print("ManagedAudioSession activate error \(error)") +// } + } + } + + private func updateOutputMode(_ outputMode: AudioSessionOutputMode) { + if let (type, currentOutputMode) = self.currentTypeAndOutputMode, currentOutputMode != outputMode { + self.currentTypeAndOutputMode = (type, outputMode) + do { + try self.setupOutputMode(outputMode) + } catch let error { + print("ManagedAudioSession overrideOutputAudioPort error \(error)") + } + } + } + + func callKitActivatedAudioSession() { + /*self.queue.async { + print("ManagedAudioSession callKitDeactivatedAudioSession") + self.callKitAudioSessionIsActive = true + self.updateHolders() + }*/ + } + + func callKitDeactivatedAudioSession() { + /*self.queue.async { + print("ManagedAudioSession callKitDeactivatedAudioSession") + self.callKitAudioSessionIsActive = false + self.updateHolders() + }*/ + } +} diff --git a/Telegram-Mac/MapResources.swift b/Telegram-Mac/MapResources.swift new file mode 100644 index 0000000000..f3cb1f9c6a --- /dev/null +++ b/Telegram-Mac/MapResources.swift @@ -0,0 +1,149 @@ +import Foundation +import Postbox +import TelegramCore + +import MapKit +import SwiftSignalKit +import TGUIKit + +public struct MapSnapshotMediaResourceId: MediaResourceId { + public let latitude: Double + public let longitude: Double + public let width: Int32 + public let height: Int32 + public let zoom: Int32 + public var uniqueId: String { + return "map-\(latitude)-\(longitude)-\(width)x\(height)-\(zoom)" + } + + public var hashValue: Int { + return self.uniqueId.hashValue + } + + public func isEqual(to: MediaResourceId) -> Bool { + if let to = to as? MapSnapshotMediaResourceId { + return self.latitude == to.latitude && self.longitude == to.longitude && self.width == to.width && self.height == to.height && self.zoom == to.zoom + } else { + return false + } + } +} + +public class MapSnapshotMediaResource: TelegramMediaResource { + public let latitude: Double + public let longitude: Double + public let width: Int32 + public let height: Int32 + public let zoom: Int32 + public init(latitude: Double, longitude: Double, width: Int32, height: Int32, zoom: Int32) { + self.latitude = latitude + self.longitude = longitude + self.width = width + self.height = height + self.zoom = zoom + } + + public required init(decoder: PostboxDecoder) { + self.latitude = decoder.decodeDoubleForKey("lt", orElse: 0.0) + self.longitude = decoder.decodeDoubleForKey("ln", orElse: 0.0) + self.width = decoder.decodeInt32ForKey("w", orElse: 0) + self.height = decoder.decodeInt32ForKey("h", orElse: 0) + self.zoom = decoder.decodeInt32ForKey("z", orElse: 15) + + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeDouble(self.latitude, forKey: "lt") + encoder.encodeDouble(self.longitude, forKey: "ln") + encoder.encodeInt32(self.width, forKey: "w") + encoder.encodeInt32(self.height, forKey: "h") + encoder.encodeInt32(self.zoom, forKey: "z") + } + + public var id: MediaResourceId { + return MapSnapshotMediaResourceId(latitude: self.latitude, longitude: self.longitude, width: self.width, height: self.height, zoom: self.zoom) + } + + public func isEqual(to: MediaResource) -> Bool { + if let to = to as? MapSnapshotMediaResource { + return self.latitude == to.latitude && self.longitude == to.longitude && self.width == to.width && self.height == to.height && self.zoom == to.zoom + } else { + return false + } + } +} + +final class MapSnapshotMediaResourceRepresentation: CachedMediaResourceRepresentation { + public let keepDuration: CachedMediaRepresentationKeepDuration = .shortLived + + public var uniqueId: String { + return "cached" + } + + public init() { + } + + public func isEqual(to: CachedMediaResourceRepresentation) -> Bool { + if let _ = to as? MapSnapshotMediaResourceRepresentation { + return true + } else { + return false + } + } +} + + +let TGGoogleMapsOffset: Int = 268435456 +let TGGoogleMapsRadius = Double(TGGoogleMapsOffset) / Double.pi + +private func yToLatitude(_ y: Int) -> Double { + return ((Double.pi / 2.0) - 2 * atan(exp((Double(y - TGGoogleMapsOffset)) / TGGoogleMapsRadius))) * 180.0 / Double.pi; +} + +private func latitudeToY(_ latitude: Double) -> Int { + return Int(round(Double(TGGoogleMapsOffset) - TGGoogleMapsRadius * log((1.0 + sin(latitude * Double.pi / 180.0)) / (1.0 - sin(latitude * Double.pi / 180.0))) / 2.0)) +} + +private func adjustGMapLatitude(_ latitude: Double, offset: Int, zoom: Int) -> Double { + let t: Int = (offset << (21 - zoom)) + return yToLatitude(latitudeToY(latitude) + t) +} + + + +func fetchMapSnapshotResource(resource: MapSnapshotMediaResource) -> Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + + Queue.concurrentDefaultQueue().async { + let options = MKMapSnapshotter.Options() + let latitude = adjustGMapLatitude(resource.latitude, offset: -10, zoom: Int(resource.zoom)) + options.region = MKCoordinateRegion(center: CLLocationCoordinate2DMake(latitude, resource.longitude), span: MKCoordinateSpan(latitudeDelta: 0.003, longitudeDelta: 0.003)) + options.mapType = .standard + options.showsPointsOfInterest = false + options.showsBuildings = true + options.size = CGSize(width: CGFloat(resource.width + 1), height: CGFloat(resource.height + 24)) + // options.scale = 2.0 + let snapshotter = MKMapSnapshotter(options: options) + snapshotter.start(with: DispatchQueue.global(), completionHandler: { result, error in + if let image = result?.image, let data = image.tiffRepresentation(using: .jpeg, factor: 0.6) { + let imageRep = NSBitmapImageRep(data: data) + let compressedData: Data? = imageRep?.representation(using: NSBitmapImageRep.FileType.jpeg, properties: [:]) + if let data = compressedData { + + let tempFile = TempBox.shared.tempFile(fileName: "image.jpg") + if let _ = try? data.write(to: URL(fileURLWithPath: tempFile.path), options: .atomic) { + subscriber.putNext(.tempFile(tempFile)) + subscriber.putCompletion() + } + } + } + }) + disposable.set(ActionDisposable { + snapshotter.cancel() + }) + } + return disposable + } +} + diff --git a/Telegram-Mac/Markdown.swift b/Telegram-Mac/Markdown.swift index 4ddb354228..dc3799f8cf 100644 --- a/Telegram-Mac/Markdown.swift +++ b/Telegram-Mac/Markdown.swift @@ -21,7 +21,7 @@ final class MarkdownAttributes { let link: MarkdownAttributeSet let linkAttribute: (String) -> (String, Any)? - init(body: MarkdownAttributeSet, bold: MarkdownAttributeSet, link: MarkdownAttributeSet, linkAttribute: @escaping (String) -> (String, Any)?) { + init(body: MarkdownAttributeSet, bold: MarkdownAttributeSet = MarkdownAttributeSet(font: .bold(.text), textColor: theme.colors.grayText), link: MarkdownAttributeSet, linkAttribute: @escaping (String) -> (String, Any)?) { self.body = body self.link = link self.bold = bold @@ -58,10 +58,10 @@ func parseMarkdownIntoAttributedString(_ string: String, attributes: MarkdownAtt let result = NSMutableAttributedString() var remainingRange = NSMakeRange(0, nsString.length) - var bodyAttributes: [NSAttributedStringKey: Any] = [NSAttributedStringKey.font: attributes.body.font, NSAttributedStringKey.foregroundColor: attributes.body.textColor, NSAttributedStringKey.paragraphStyle: paragraphStyleWithAlignment(textAlignment)] + var bodyAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: attributes.body.font, NSAttributedString.Key.foregroundColor: attributes.body.textColor, NSAttributedString.Key.paragraphStyle: paragraphStyleWithAlignment(textAlignment)] if !attributes.body.additionalAttributes.isEmpty { for (key, value) in attributes.body.additionalAttributes { - bodyAttributes[NSAttributedStringKey(rawValue: key)] = value + bodyAttributes[NSAttributedString.Key(rawValue: key)] = value } } @@ -77,14 +77,14 @@ func parseMarkdownIntoAttributedString(_ string: String, attributes: MarkdownAtt if character == UInt16(("[" as UnicodeScalar).value) { remainingRange = NSMakeRange(range.location + range.length, remainingRange.location + remainingRange.length - (range.location + range.length)) if let (parsedLinkText, parsedLinkContents) = parseLink(string: nsString, remainingRange: &remainingRange) { - var linkAttributes: [NSAttributedStringKey: Any] = [NSAttributedStringKey.font: attributes.link.font, NSAttributedStringKey.foregroundColor: attributes.link.textColor, NSAttributedStringKey.paragraphStyle: paragraphStyleWithAlignment(textAlignment)] + var linkAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: attributes.link.font, NSAttributedString.Key.foregroundColor: attributes.link.textColor, NSAttributedString.Key.paragraphStyle: paragraphStyleWithAlignment(textAlignment)] if !attributes.body.additionalAttributes.isEmpty { for (key, value) in attributes.link.additionalAttributes { - linkAttributes[NSAttributedStringKey(rawValue: key)] = value + linkAttributes[NSAttributedString.Key(rawValue: key)] = value } } if let (attributeName, attributeValue) = attributes.linkAttribute(parsedLinkContents) { - linkAttributes[NSAttributedStringKey(rawValue: attributeName)] = attributeValue + linkAttributes[NSAttributedString.Key(rawValue: attributeName)] = attributeValue } result.append(NSAttributedString(string: parsedLinkText, attributes: linkAttributes)) } @@ -95,10 +95,10 @@ func parseMarkdownIntoAttributedString(_ string: String, attributes: MarkdownAtt remainingRange = NSMakeRange(range.location + range.length + 1, remainingRange.location + remainingRange.length - (range.location + range.length + 1)) if let bold = parseBold(string: nsString, remainingRange: &remainingRange) { - var boldAttributes: [NSAttributedStringKey: Any] = [NSAttributedStringKey.font: attributes.bold.font, NSAttributedStringKey.foregroundColor: attributes.bold.textColor, NSAttributedStringKey.paragraphStyle: paragraphStyleWithAlignment(textAlignment)] + var boldAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: attributes.bold.font, NSAttributedString.Key.foregroundColor: attributes.bold.textColor, NSAttributedString.Key.paragraphStyle: paragraphStyleWithAlignment(textAlignment)] if !attributes.body.additionalAttributes.isEmpty { for (key, value) in attributes.bold.additionalAttributes { - boldAttributes[NSAttributedStringKey(rawValue: key)] = value + boldAttributes[NSAttributedString.Key(rawValue: key)] = value } } result.append(NSAttributedString(string: bold, attributes: boldAttributes)) diff --git a/Telegram-Mac/MediaAnimatedStickerView.swift b/Telegram-Mac/MediaAnimatedStickerView.swift new file mode 100644 index 0000000000..c49b4056aa --- /dev/null +++ b/Telegram-Mac/MediaAnimatedStickerView.swift @@ -0,0 +1,386 @@ +// +// ChatMediaAnimatedSticker.swift +// Telegram +// +// Created by Mikhail Filimonov on 13/05/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import Postbox +import TelegramCore + +import TGUIKit +import SwiftSignalKit + + + +class MediaAnimatedStickerView: ChatMediaContentView { + + private let loadResourceDisposable = MetaDisposable() + private let stateDisposable = MetaDisposable() + private let fetchDisposable = MetaDisposable() + private let playThrottleDisposable = MetaDisposable() + private let playerView: LottiePlayerView = LottiePlayerView(frame: NSMakeRect(0, 0, 240, 240)) + private var placeholderView: StickerShimmerEffectView? + + private let thumbView = TransformImageView() + private var sticker:LottieAnimation? = nil { + didSet { + if oldValue != sticker { + self.previousAccept = false + } + updatePlayerIfNeeded() + } + } + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(self.playerView) + addSubview(self.thumbView) + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func clean() { + stateDisposable.set(nil) + loadResourceDisposable.set(nil) + playThrottleDisposable.set(nil) + fetchDisposable.set(nil) + } + + deinit { + loadResourceDisposable.dispose() + stateDisposable.dispose() + playThrottleDisposable.dispose() + fetchDisposable.dispose() + } + + func removeNotificationListeners() { + NotificationCenter.default.removeObserver(self) + } + + override func viewDidUpdatedDynamicContent() { + super.viewDidUpdatedDynamicContent() + updatePlayerIfNeeded() + } + + private var previousAccept: Bool = false + + var overridePlayValue: Bool? = nil { + didSet { + updatePlayerIfNeeded() + } + } + + @objc func updatePlayerIfNeeded() { + var accept = ((self.window != nil && self.window!.isKeyWindow) || (self.window != nil && !(self.window is Window))) && !NSIsEmptyRect(self.visibleRect) && !self.isDynamicContentLocked && self.sticker != nil + + let parameters = self.parameters as? ChatAnimatedStickerMediaLayoutParameters + + accept = parameters?.alwaysAccept ?? accept + + + if NSIsEmptyRect(self.visibleRect) || self.window == nil { + accept = false + } + + if let value = overridePlayValue { + accept = value + } + + var signal = Signal.single(Void()) + if accept && !nextForceAccept && self.sticker != nil { + signal = signal |> delay(accept ? 0.1 : 0, queue: .mainQueue()) + } + if accept && self.sticker != nil { + nextForceAccept = false + } + + if let sticker = self.sticker, previousAccept { + switch sticker.playPolicy { + case .once, .onceEnd: + return + default: + break + } + } + if previousAccept != accept { + self.playThrottleDisposable.set(signal.start(next: { [weak self] in + guard let `self` = self else { + return + } + self.playerView.set(accept ? self.sticker : nil) + self.previousAccept = accept + })) + } + previousAccept = accept + + + } + + func setColors(_ colors: [LottieColor]) { + self.playerView.setColors(colors) + } + + private var nextForceAccept: Bool = false + + + override func previewMediaIfPossible() -> Bool { + if let table = table, let context = context, let window = window as? Window { + _ = startModalPreviewHandle(table, window: window, context: context) + } + return true + } + override func executeInteraction(_ isControl: Bool) { + if let window = window as? Window { + if let context = context, let peerId = parent?.id.peerId, let media = media as? TelegramMediaFile, !media.isEmojiAnimatedSticker, let reference = media.stickerReference { + showModal(with:StickerPackPreviewModalController(context, peerId: peerId, reference: reference), for:window) + } else if let media = media as? TelegramMediaFile, let sticker = media.stickerText { + self.playerView.playIfNeeded(true) + + parameters?.runEmojiScreenEffect(sticker) + } + } + } + + func playAgain() { + self.playerView.playIfNeeded(true) + } + + var chatLoopAnimated: Bool { + if let context = self.context { + return context.autoplayMedia.loopAnimatedStickers + } + return true + } + + func updateListeners() { + if let window = window { + + NotificationCenter.default.removeObserver(self) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSWindow.didBecomeKeyNotification, object: window) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSWindow.didResignKeyNotification, object: window) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSView.boundsDidChangeNotification, object: self.enclosingScrollView?.contentView) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSView.frameDidChangeNotification, object: self.enclosingScrollView?.documentView) + + } else { + removeNotificationListeners() + } + } + + override func viewWillDraw() { + super.viewWillDraw() + updatePlayerIfNeeded() + } + + override func willRemove() { + super.willRemove() + updateListeners() + updatePlayerIfNeeded() + } + + override func viewDidMoveToSuperview() { + updateListeners() + updatePlayerIfNeeded() + } + + override func viewDidMoveToWindow() { + updateListeners() + updatePlayerIfNeeded() + } + + override func update(with media: Media, size: NSSize, context: AccountContext, parent: Message? = nil, table: TableView?, parameters: ChatMediaLayoutParameters? = nil, animated: Bool, positionFlags: LayoutPositionFlags? = nil, approximateSynchronousValue: Bool = false) { + + + guard let file = media as? TelegramMediaFile else { return } + + let updated = self.media != nil ? !file.isSemanticallyEqual(to: self.media!) : true + + + if parent?.stableId != self.parent?.stableId { + self.sticker = nil + } else if parent == nil, updated { + self.sticker = nil + } + + + self.nextForceAccept = approximateSynchronousValue || parent?.id.namespace == Namespaces.Message.Local + + super.update(with: media, size: size, context: context, parent: parent, table: table, parameters: parameters, animated: animated, positionFlags: positionFlags, approximateSynchronousValue: approximateSynchronousValue) + + let reference: FileMediaReference + let mediaResource: MediaResourceReference + if let message = parent { + reference = FileMediaReference.message(message: MessageReference(message), media: file) + mediaResource = reference.resourceReference(file.resource) + } else if let stickerReference = file.stickerReference { + if file.resource is CloudStickerPackThumbnailMediaResource { + reference = FileMediaReference.stickerPack(stickerPack: stickerReference, media: file) + mediaResource = MediaResourceReference.stickerPackThumbnail(stickerPack: stickerReference, resource: file.resource) + } else { + reference = FileMediaReference.stickerPack(stickerPack: stickerReference, media: file) + mediaResource = reference.resourceReference(file.resource) + } + } else { + reference = FileMediaReference.standalone(media: file) + mediaResource = reference.resourceReference(file.resource) + } + + let data: Signal + if let resource = file.resource as? LocalBundleResource { + data = Signal { subscriber in + if let path = Bundle.main.path(forResource: resource.name, ofType: resource.ext), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) { + subscriber.putNext(MediaResourceData(path: path, offset: 0, size: data.count, complete: true)) + subscriber.putCompletion() + } + return EmptyDisposable + } |> runOn(resourcesQueue) + } else { + data = context.account.postbox.mediaBox.resourceData(file.resource, attemptSynchronously: approximateSynchronousValue) + } + + self.loadResourceDisposable.set((data |> map { resourceData -> Data? in + if resourceData.complete, let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) { + return data + } + return nil + } |> deliverOnMainQueue).start(next: { [weak file, weak self] data in + if let data = data, let file = file, let `self` = self { + let parameters = parameters as? ChatAnimatedStickerMediaLayoutParameters + let playPolicy: LottiePlayPolicy = parameters?.playPolicy ?? (file.isEmojiAnimatedSticker || !self.chatLoopAnimated ? (self.parameters == nil ? .framesCount(1) : .once) : .loop) + var soundEffect: LottieSoundEffect? = nil + if file.isEmojiAnimatedSticker, let emoji = file.stickerText { + let emojies = EmojiesSoundConfiguration.with(appConfiguration: context.appConfiguration) + if let file = emojies.sounds[emoji] { + soundEffect = LottieSoundEffect(file: file, postbox: context.account.postbox, triggerOn: 1) + } + } + let maximumFps: Int = size.width < 200 && !file.isEmojiAnimatedSticker ? size.width <= 30 ? 24 : 30 : 60 + let cache: ASCachePurpose = parameters?.cache ?? (size.width < 200 && size.width > 30 ? .temporaryLZ4(.thumb) : self.parent != nil ? .temporaryLZ4(.chat) : .none) + let fitzModifier = file.animatedEmojiFitzModifier + + let type: LottieAnimationType + if file.mimeType == "image/webp" { + type = .webp + } else { + type = .lottie + } + self.sticker = LottieAnimation(compressed: data, key: LottieAnimationEntryKey(key: .media(file.id), size: size, fitzModifier: fitzModifier), type: type, cachePurpose: cache, playPolicy: playPolicy, maximumFps: maximumFps, colors: parameters?.colors ?? [], soundEffect: soundEffect, postbox: self.context?.account.postbox) + + self.fetchStatus = .Local + } else { + self?.sticker = nil + self?.fetchStatus = .Remote + } + })) + + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: NSEdgeInsets()) + + + + + + self.thumbView.setSignal(signal: cachedMedia(media: file, arguments: arguments, scale: backingScaleFactor), clearInstantly: updated) + + let hasPlaceholder = (parent == nil || file.immediateThumbnailData != nil) && self.thumbView.image == nil && size.height >= 40 + if updated { + if hasPlaceholder { + let current: StickerShimmerEffectView + if let local = self.placeholderView { + current = local + } else { + current = StickerShimmerEffectView() + current.frame = bounds + self.placeholderView = current + addSubview(current, positioned: .below, relativeTo: playerView) + if animated { + current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } + current.update(backgroundColor: nil, foregroundColor: NSColor(rgb: 0x748391, alpha: 0.2), shimmeringColor: NSColor(rgb: 0x748391, alpha: 0.35), data: file.immediateThumbnailData, size: size) + current.updateAbsoluteRect(bounds, within: size) + } else { + self.removePlaceholder(animated: animated) + } + } + + self.thumbView.imageUpdated = { [weak self] value in + if value != nil { + self?.removePlaceholder(animated: animated) + } + } + + + + if !self.thumbView.isFullyLoaded { + + let signal: Signal + + switch file.mimeType { + case "image/webp": + signal = chatMessageSticker(postbox: context.account.postbox, file: reference, small: size.width < 120, scale: backingScaleFactor, fetched: true) + default: + signal = chatMessageAnimatedSticker(postbox: context.account.postbox, file: reference, small: false, scale: backingScaleFactor, size: size, fetched: true) + } + self.thumbView.setSignal(signal, cacheImage: { [weak file, weak self] result in + if let file = file { + cacheMedia(result, media: file, arguments: arguments, scale: System.backingScale) + } + self?.removePlaceholder(animated: false) + }) + } + self.thumbView.set(arguments: arguments) + if updated { + self.playerView.removeFromSuperview() + addSubview(self.thumbView) + } + + + fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: mediaResource).start()) + if updated { + stateDisposable.set((self.playerView.state |> deliverOnMainQueue).start(next: { [weak self] state in + guard let `self` = self else { return } + switch state { + case .playing: + self.addSubview(self.playerView) + self.thumbView.removeFromSuperview() + self.removePlaceholder(animated: false) + case .stoped: + self.playerView.removeFromSuperview() + self.addSubview(self.thumbView) + default: + break + } + })) + } + + } + + private func removePlaceholder(animated: Bool) { + if let placeholderView = self.placeholderView { + if animated { + placeholderView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak placeholderView] _ in + placeholderView?.removeFromSuperview() + }) + } else { + placeholderView.removeFromSuperview() + } + self.placeholderView = nil + } + } + + override var contents: Any? { + return self.thumbView.image + } + + override func layout() { + super.layout() + self.playerView.frame = bounds + self.thumbView.frame = bounds + self.placeholderView?.frame = bounds + } + +} diff --git a/Telegram-Mac/MediaGroupPreviewRowItem.swift b/Telegram-Mac/MediaGroupPreviewRowItem.swift new file mode 100644 index 0000000000..5d822a6858 --- /dev/null +++ b/Telegram-Mac/MediaGroupPreviewRowItem.swift @@ -0,0 +1,367 @@ +// +// MediaGroupPreviewRowItem.swift +// Telegram +// +// Created by keepcoder on 02/11/2017. +// Copyright © 2017 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + +class MediaGroupPreviewRowItem: TableRowItem { + fileprivate let context: AccountContext + private let _stableId: UInt32 = arc4random() + fileprivate let layout: GroupedLayout + fileprivate let reorder:(Int, Int)->Void + fileprivate let urls: [URL] + fileprivate let editedData: [URL: EditedImageData] + fileprivate let edit:(URL)->Void + fileprivate let paint:(URL)->Void + fileprivate let delete:(URL)->Void + init(_ initialSize: NSSize, messages: [Message], urls: [URL], editedData: [URL : EditedImageData], edit: @escaping(URL)->Void, paint: @escaping(URL)->Void, delete:@escaping(URL)->Void, context: AccountContext, reorder:@escaping(Int, Int)->Void) { + layout = GroupedLayout(messages) + self.editedData = editedData + self.edit = edit + self.paint = paint + self.delete = delete + self.urls = urls + self.reorder = reorder + self.context = context + super.init(initialSize) + _ = makeSize(initialSize.width, oldWidth: 0) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) + layout.measure(NSMakeSize(width - 20, width - 20)) + return success + } + + override var height: CGFloat { + return layout.dimensions.height + 12 + } + + override var stableId: AnyHashable { + return _stableId + } + + + + override func viewClass() -> AnyClass { + return MediaGroupPreviewRowView.self + } + +} + +class MediaGroupPreviewRowView : TableRowView, ModalPreviewRowViewProtocol { + private var contents: [ChatMediaContentView] = [] + private var startPoint: NSPoint = NSZeroPoint + private(set) var draggingIndex: Int? = nil + + + func fileAtPoint(_ point: NSPoint) -> (QuickPreviewMedia, NSView?)? { + + guard let item = item as? MediaGroupPreviewRowItem else { return nil } + + for i in 0 ..< item.layout.count { + if NSPointInRect(point, item.layout.frame(at: i).offsetBy(dx: offset.x, dy: offset.y)) { + let contentNode = contents[i] + if contentNode is ChatGIFContentView { + if let file = contentNode.media as? TelegramMediaFile { + let reference = contentNode.parent != nil ? FileMediaReference.message(message: MessageReference(contentNode.parent!), media: file) : FileMediaReference.standalone(media: file) + return (.file(reference, GifPreviewModalView.self), contentNode) + } + } else if contentNode is ChatInteractiveContentView { + if let image = contentNode.media as? TelegramMediaImage { + let reference = contentNode.parent != nil ? ImageMediaReference.message(message: MessageReference(contentNode.parent!), media: image) : ImageMediaReference.standalone(media: image) + return (.image(reference, ImagePreviewModalView.self), contentNode) + } + } + } + } + return nil + } + + override func draw(_ dirtyRect: NSRect) { + + } + + override func updateColors() { + super.updateColors() + for content in contents { + content.backgroundColor = .clear + } + } + + private var offset: NSPoint { + guard let item = item as? MediaGroupPreviewRowItem else { return NSZeroPoint } + return NSMakePoint((frame.width - item.layout.dimensions.width) / 2, 6) + } + + override func set(item: TableRowItem, animated: Bool) { + + guard let item = item as? MediaGroupPreviewRowItem else {return} + + if contents.count > item.layout.count { + let contentCount = contents.count + let layoutCount = item.layout.count + + for i in layoutCount ..< contentCount { + contents[i].removeFromSuperview() + } + contents = contents.subarray(with: NSMakeRange(0, layoutCount)) + } else if contents.count < item.layout.count { + let contentCount = contents.count + for _ in contentCount ..< item.layout.count { + contents.append(ChatInteractiveContentView(frame: NSZeroRect)) + contents.last?.userInteractionEnabled = false + } + } + + + + for i in 0 ..< contents.count { + let content = contents[i] + addSubview(content) + let control: MediaPreviewEditControl + if let editControl = content.subviews.last as? MediaPreviewEditControl { + control = editControl + } else { + let editControl = MediaPreviewEditControl() + content.addSubview(editControl) + control = editControl + } + + control.canEdit = item.layout.messages[i].media[0] is TelegramMediaImage + control.set(edit: { [weak item] in + guard let item = item else {return} + item.edit(item.urls[i]) + }, paint: { [weak item] in + guard let item = item else {return} + item.paint(item.urls[i]) + }, delete: { [weak item] in + guard let item = item else {return} + item.delete(item.urls[i]) + }, editedData: item.editedData[item.urls[i]]) + + } + + assert(contents.count == item.layout.count) + + for i in 0 ..< item.layout.count { + contents[i].update(with: item.layout.messages[i].media[0], size: item.layout.frame(at: i).size, context: item.context, parent: nil, table: item.table, positionFlags: item.layout.position(at: i)) + } + super.set(item: item, animated: animated) + + needsLayout = true + + updateMouse() + } + + override func forceClick(in location: NSPoint) { + guard let item = item as? MediaGroupPreviewRowItem else {return} + + for i in 0 ..< item.layout.count { + if NSPointInRect(location, item.layout.frame(at: i).offsetBy(dx: offset.x, dy: offset.y)) { + _ = contents[i].previewMediaIfPossible() + break + } + } + } + + override func updateMouse() { + guard let window = window, let table = item?.table else { + for node in self.contents { + if let control = node.subviews.last as? MediaPreviewEditControl { + control.isHidden = true + } + } + return + } + + let row = table.row(at: table.documentView!.convert(window.mouseLocationOutsideOfEventStream, from: nil)) + + if row == item?.index { + let point = convert(window.mouseLocationOutsideOfEventStream, from: nil) + for node in self.contents { + if let control = node.subviews.last as? MediaPreviewEditControl { + if NSPointInRect(point, node.frame) { + control.isHidden = false + } else { + control.isHidden = true + } + } + } + } else { + for node in self.contents { + if let control = node.subviews.last as? MediaPreviewEditControl { + control.isHidden = true + } + } + } + } + + override func mouseDown(with event: NSEvent) { + guard let item = item as? MediaGroupPreviewRowItem else {return} + let point = convert(event.locationInWindow, from: nil) + draggingIndex = nil + previous = point + for i in 0 ..< item.layout.count { + if NSPointInRect(point, item.layout.frame(at: i).offsetBy(dx: offset.x, dy: offset.y)) { + self.startPoint = point + self.draggingIndex = i + //contents[i].removeFromSuperview() + addSubview(contents[i]) + break + } + } + super.mouseDown(with: event) + } + + override func mouseUp(with event: NSEvent) { + + guard let item = item as? MediaGroupPreviewRowItem else {return} + + var point = convert(event.locationInWindow, from: nil) + point = NSMakePoint(min(max(0, point.x), frame.width - 10), min(max(0, point.y), frame.height - 10)) + if let index = draggingIndex, let newIndex = item.layout.moveItemIfNeeded(at: index, point: point) { + + let current = contents[index] + contents.remove(at: index) + contents.insert(current, at: newIndex) + + _ = item.makeSize(frame.width, oldWidth: 0) + item.table?.noteHeightOfRow(item.index, true) + + item.reorder(index, newIndex) + + set(item: item, animated: true) + for i in 0 ..< item.layout.count { + let rect = item.layout.frame(at: i).offsetBy(dx: offset.x, dy: offset.y) + contents[i].change(pos: rect.origin, animated: true) + contents[i].change(size: rect.size, animated: true) + } + } else { + for i in 0 ..< item.layout.count { + let rect = item.layout.frame(at: i).offsetBy(dx: offset.x, dy: offset.y) + contents[i].change(pos: rect.origin, animated: true) + contents[i].change(size: rect.size, animated: true) + } + } + + draggingIndex = nil + startPoint = NSZeroPoint + } + private var previous: NSPoint = NSZeroPoint + override func mouseDragged(with event: NSEvent) { + guard let item = item as? MediaGroupPreviewRowItem else {return} + + let point = convert(event.locationInWindow, from: nil) + + if let index = draggingIndex { + let past = contents[index].frame + + var current = contents[index].frame.origin + current.x += (point.x - previous.x) + current.y += (point.y - previous.y) + + + let size = contents[index].frame.size.fitted(NSMakeSize(100, 100)) + current.x -= (size.width - past.width) * ((point.x - past.minX) / past.width) + current.y -= (size.height - past.height) * ((point.y - past.minY) / past.height) + + + if size != contents[index].frame.size { + contents[index].change(size: size, animated: true, timingFunction: .spring) + contents[index].layer?.animatePosition(from: contents[index].frame.origin - current, to: .zero, timingFunction: .spring, additive: true) + } + contents[index].setFrameOrigin(current) + + + + previous = point + + + + + let layout = GroupedLayout(item.layout.messages) + layout.measure(NSMakeSize(frame.width - 20, frame.width - 20)) + + if let new = layout.moveItemIfNeeded(at: index, point: point) { + + for i in 0 ..< layout.count { + let current = item.layout.frame(at: i).offsetBy(dx: offset.x, dy: offset.y).origin + + if i != index { + contents[i].setFrameOrigin(current) + } + + } + } else { + for i in 0 ..< item.layout.count { + if i != index { + contents[i].setFrameOrigin(item.layout.frame(at: i).origin.offsetBy(dx: offset.x, dy: offset.y)) + } + } + } + } + } + + override var needsDisplay: Bool { + get { + return super.needsDisplay + } + set { + super.needsDisplay = newValue + for content in contents { + content.needsDisplay = newValue + } + } + } + override var backgroundColor: NSColor { + didSet { + for content in contents { + content.backgroundColor = backdorColor + } + } + } + + + override func viewWillMove(toSuperview newSuperview: NSView?) { + if newSuperview == nil { + for content in contents { + content.willRemove() + } + } + } + + + override var backdorColor: NSColor { + return theme.colors.background + } + + + override func layout() { + super.layout() + guard let item = item as? MediaGroupPreviewRowItem else {return} + + assert(contents.count == item.layout.count) + + if let _ = draggingIndex { + return + } + + for i in 0 ..< item.layout.count { + contents[i].setFrameOrigin(item.layout.frame(at: i).origin.offsetBy(dx: offset.x, dy: offset.y)) + if let control = contents[i].subviews.first(where: { $0 is MediaPreviewEditControl }) { + control.setFrameOrigin(NSMakePoint(contents[i].frame.width - control.frame.width - 10, contents[i].frame.height - control.frame.height - 10)) + } + } + + } +} diff --git a/Telegram-Mac/MediaObjectToAvatar.swift b/Telegram-Mac/MediaObjectToAvatar.swift new file mode 100644 index 0000000000..87c454b936 --- /dev/null +++ b/Telegram-Mac/MediaObjectToAvatar.swift @@ -0,0 +1,430 @@ +// +// StickerToMp4.swift +// Telegram +// +// Created by Mikhail Filimonov on 20.08.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TelegramCore +import SwiftSignalKit +import TGUIKit +import RLottie +import CoreMedia +import libwebp + +private func buffer(from image: CGImage) -> CVPixelBuffer? { + let attrs = [kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue, kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue] as CFDictionary + var pixelBuffer : CVPixelBuffer? + let status = CVPixelBufferCreate(kCFAllocatorDefault, Int(image.size.width), Int(image.size.height), kCVPixelFormatType_32ARGB, attrs, &pixelBuffer) + guard (status == kCVReturnSuccess) else { + return nil + } + + CVPixelBufferLockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0)) + let pixelData = CVPixelBufferGetBaseAddress(pixelBuffer!) + + let rect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height) + + let rgbColorSpace = CGColorSpaceCreateDeviceRGB() + let context = CGContext(data: pixelData, width: Int(image.size.width), height: Int(image.size.height), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer!), space: rgbColorSpace, bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue) + + context?.clear(rect) + context?.setFillColor(.white) + context?.fill(rect) + context?.draw(image, in: rect) + CVPixelBufferUnlockBaseAddress(pixelBuffer!, CVPixelBufferLockFlags(rawValue: 0)) + return pixelBuffer +} + + + + +private final class StickerToMp4Context { + private let statusPromise: ValuePromise = ValuePromise(ignoreRepeated: true) + + var statusValue: Signal { + return statusPromise.get() + } + + static let queue: Queue = Queue(name: "org.telegram.sticker-to-mp4") + + private var status: StickerToMp4.Status = .initializing("") { + didSet { + statusPromise.set(status) + } + } + + + final class Export { + private let writter: AVAssetWriter + private let writerInput: AVAssetWriterInput + private let path: String + private let adaptor: AVAssetWriterInputPixelBufferAdaptor + + init() throws { + self.path = NSTemporaryDirectory() + "tgs_\(arc4random()).mp4" + self.writter = try .init(url: URL.init(fileURLWithPath: path), fileType: .mov) + let settings:[String: Any] = [AVVideoWidthKey: NSNumber(value: 640), AVVideoHeightKey: NSNumber(value: 640), AVVideoCodecKey: AVVideoCodecH264]; + self.writerInput = AVAssetWriterInput(mediaType: .video, outputSettings: settings) + self.adaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: writerInput, sourcePixelBufferAttributes: nil) + self.writter.add(self.writerInput) + } + + func start() { + writter.startWriting() + writter.startSession(atSourceTime: CMTime.zero) + } + + func append(_ pixelBuffer: CVPixelBuffer, time: CMTime) { + while !writerInput.isReadyForMoreMediaData { + + } + self.adaptor.append(pixelBuffer, withPresentationTime: time) + + } + + func finish(_ complete:@escaping(String)->Void) { + writerInput.markAsFinished() + let path = self.path + writter.finishWriting { + complete(path) + } + } + } + + private let export: Export? + + private let dataDisposable = MetaDisposable() + private let fetchDisposable = MetaDisposable() + private let fileReference: FileMediaReference + private let context: AccountContext + init(context: AccountContext, fileReference: FileMediaReference) { + self.export = try? Export() + self.context = context + self.fileReference = fileReference + } + + deinit { + dataDisposable.dispose() + fetchDisposable.dispose() + } + + func start() { + let signal = context.account.postbox.mediaBox.resourceData(fileReference.media.resource) + |> deliverOn(StickerToMp4Context.queue) + |> filter { $0.complete } + |> map { + $0.path + } + + dataDisposable.set(signal.start(next: { [weak self] path in + if let data = try? Data(contentsOf: URL.init(fileURLWithPath: path)) { + if let data = TGGUnzipData(data, 8 * 1024 * 1024) { + if let json = String(data: data, encoding: .utf8) { + if let bridge = RLottieBridge(json: json, key: "\(arc4random())") { + self?.process(bridge) + } + } + } + } + })) + fetchDisposable.set(freeMediaFileInteractiveFetched(context: context, fileReference: fileReference).start()) + } + + private func process(_ rlottie: RLottieBridge) -> Void { + + let image = rlottie.renderFrame(rlottie.startFrame(), width: 640, height: 640).takeRetainedValue() + + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + let thumbPath = NSTemporaryDirectory() + "\(randomId)" + let url = URL(fileURLWithPath: thumbPath) + + if let colorDestination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypeJPEG, 1, nil) { + CGImageDestinationSetProperties(colorDestination, [:] as CFDictionary) + + let colorQuality: Float = 0.6 + + let options = NSMutableDictionary() + options.setObject(colorQuality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString) + + CGImageDestinationAddImage(colorDestination, image, options as CFDictionary) + CGImageDestinationFinalize(colorDestination) + } + + self.status = .initializing(thumbPath) + export?.start() + + let fps = rlottie.fps() + let effectiveFps = min(30, fps) + + let framesCount = rlottie.endFrame() - rlottie.startFrame() + var frame: Int32 = rlottie.startFrame() + var index: Int32 = 0 + while true { + let image = rlottie.renderFrame(frame, width: 640, height: 640).takeRetainedValue() + +// func pixellated(image: CGImage) -> CGImage? { +// let ciImage = CIImage(cgImage: image) +// guard let filter = CIFilter(name: "CIPixellate") else { return nil } +// filter.setValue(ciImage, forKey: "inputImage") +// filter.setValue(12, forKey: kCIInputScaleKey) +// +// guard let output = filter.outputImage else { return nil } +// +// return generateImage(image.size, contextGenerator: { size, ctx in +// ctx.clear(size.bounds) +// let ciContext = CIContext(cgContext: ctx, options: [CIContextOption.useSoftwareRenderer : NSNumber(value: true)]) +// ciContext.draw(output, in: CGRect(x: 0, y: 0, width: size.width, height: size.height), from: output.extent) +// })! +// } + + let pixelBuffer = buffer(from: image)! + + let frameTime: CMTime = CMTimeMake(value: 20, timescale: 600); + let lastTime: CMTime = CMTimeMake(value: Int64(index) * 20, timescale: 600); + var presentTime: CMTime = CMTimeAdd(lastTime, frameTime); + if frame == rlottie.startFrame() { + presentTime = CMTimeMake(value: 0, timescale: 600); + } + + export?.append(pixelBuffer, time: presentTime) + + if frame % Int32(round(Float(fps) / Float(effectiveFps))) != 0 { + frame += 1 + } + frame += 1 + index += 1 + if frame > framesCount { + break + } + self.status = .converting(min((Float(frame) / Float(framesCount)), 1)) + } + + export?.finish({ [weak self] path in + self?.status = .done(path, thumbPath) + }) + } + + func cancel() { + + } + +} + +private final class StickerToMp4 { + + enum Status : Equatable { + case initializing(String) + case converting(Float) + case done(String, String) + case failed + } + + private let context:QueueLocalObject + init(context _context: AccountContext, fileReference: FileMediaReference) { + self.context = .init(queue: StickerToMp4Context.queue, generate: { + return StickerToMp4Context(context: _context, fileReference: fileReference) + }) + } + + + func start() { + self.context.with { + $0.start() + } + } + + func cancel() { + self.context.with { + $0.cancel() + } + } + + var status:Signal { + return self.context.signalWith { context, subscriber in + return context.statusValue.start(next: { next in + subscriber.putNext(next) + }, completed: { + subscriber.putCompletion() + }) + } + } +} + +private final class FetchVideoToFile { + + private let statusValue: ValuePromise = ValuePromise() + var status:Signal { + return statusValue.get() + } + private let context: AccountContext + private let file: TelegramMediaFile + + private let disposable = MetaDisposable() + private let dataDisposable = MetaDisposable() + init(context: AccountContext, file: TelegramMediaFile) { + self.context = context + self.file = file + } + + deinit { + disposable.dispose() + dataDisposable.dispose() + } + + func start() { + disposable.set(freeMediaFileInteractiveFetched(context: context, fileReference: FileMediaReference.standalone(media: file)).start()) + + let signal = context.account.postbox.mediaBox.resourceData(file.resource) |> filter { + $0.complete + } |> map { + return $0.path + } + + dataDisposable.set(signal.start(next: { [weak self] path in + let temp = NSTemporaryDirectory() + "tgs_\(arc4random()).mp4" + try? FileManager.default.copyItem(atPath: path, toPath: temp) + self?.statusValue.set(temp) + })) + } +} + +private final class FetchStickerToImage { + + private let statusValue: ValuePromise = ValuePromise() + var status:Signal { + return statusValue.get() + } + private let context: AccountContext + private let file: TelegramMediaFile + + private let disposable = MetaDisposable() + private let dataDisposable = MetaDisposable() + init(context: AccountContext, file: TelegramMediaFile) { + self.context = context + self.file = file + } + + deinit { + disposable.dispose() + dataDisposable.dispose() + } + + func start() { + disposable.set(freeMediaFileInteractiveFetched(context: context, fileReference: FileMediaReference.standalone(media: file)).start()) + + let signal = context.account.postbox.mediaBox.resourceData(file.resource) |> filter { + $0.complete + } |> map { + return $0.path + } + + dataDisposable.set(signal.start(next: { [weak self] path in + if let data = try? Data.init(contentsOf: URL(fileURLWithPath: path)) { + let webp = convertFromWebP(data)?._cgImage + if let webp = webp { + let image = generateImage(NSMakeSize(640, 640), contextGenerator: { size, ctx in + ctx.clear(size.bounds) + ctx.setFillColor(.white) + ctx.fill(size.bounds) + ctx.draw(webp, in: size.bounds.focus(size)) + }, scale: 1.0)! + self?.statusValue.set(NSImage(cgImage: image, size: image.size)) + } + } + })) + } +} + +final class MediaObjectToAvatar { + enum Object { + case emoji(String) + case sticker(TelegramMediaFile) + case animated(TelegramMediaFile) + case gif(TelegramMediaFile) + } + + enum Result { + case image(NSImage) + case video(String) + } + + private var animated_c:StickerToMp4? + private var fetch_v: FetchVideoToFile? + private var fetch_i: FetchStickerToImage? + + private let object: Object + private let context: AccountContext + init(context: AccountContext, object: Object) { + self.object = object + self.context = context + } + + deinit { + var bp = 0 + bp += 1 + } + + func start() -> Signal { + + let signal: Signal + switch object { + case let .animated(file): + let stickerToMp4: StickerToMp4 = .init(context: context, fileReference: .standalone(media: file)) + self.animated_c = stickerToMp4 + + signal = stickerToMp4.status |> map { value -> String? in + switch value { + case let .done(path, _): + return path + default: + return nil + } + } |> filter { + $0 != nil + } |> map { value -> Result in + return .video(value!) + } + + stickerToMp4.start() + + case let .emoji(text): + signal = Signal { subscriber in + let emoji = generateImage(NSMakeSize(640, 640), scale: 1.0, rotatedContext: { size, ctx in + ctx.clear(size.bounds) + + ctx.setFillColor(.white) + ctx.fill(size.bounds) + + let textNode = TextNode.layoutText(.initialize(string: text, color: .black, font: .normal(300)), nil, 1, .end, NSMakeSize(.greatestFiniteMagnitude, .greatestFiniteMagnitude), nil, false, .center) + + textNode.1.draw(size.bounds.focus(textNode.0.size), in: ctx, backingScaleFactor: 1.0, backgroundColor: .white) + + })! + subscriber.putNext(.image(NSImage(cgImage: emoji, size: emoji.size))) + subscriber.putCompletion() + + return EmptyDisposable + } |> runOn(.concurrentDefaultQueue()) + + case let .gif(file): + let fetch_v = FetchVideoToFile(context: context, file: file) + self.fetch_v = fetch_v + signal = fetch_v.status |> map { + .video($0) + } + fetch_v.start() + case let .sticker(file): + let fetch_i = FetchStickerToImage(context: context, file: file) + self.fetch_i = fetch_i + signal = fetch_i.status |> map { + .image($0) + } + fetch_i.start() + } + return signal |> take(1) + } +} diff --git a/Telegram-Mac/MediaPlayer.swift b/Telegram-Mac/MediaPlayer.swift new file mode 100755 index 0000000000..1463a0e60c --- /dev/null +++ b/Telegram-Mac/MediaPlayer.swift @@ -0,0 +1,1013 @@ +import Foundation +import SwiftSignalKit +import Postbox +import CoreMedia +import TelegramCore + +import Postbox + +private let traceEvents = false + +private struct MediaPlayerControlTimebase { + let timebase: CMTimebase + let isAudio: Bool + init(timebase: CMTimebase, isAudio: Bool) { + self.timebase = timebase + self.isAudio = isAudio + } +} + +private enum MediaPlayerPlaybackAction { + case play + case pause +} + +private final class MediaPlayerLoadedState { + let frameSource: MediaFrameSource + let mediaBuffers: MediaPlaybackBuffers + let controlTimebase: MediaPlayerControlTimebase + var lostAudioSession: Bool = false + var extraVideoFrames: ([MediaTrackFrame], CMTime)? + init(frameSource: MediaFrameSource, mediaBuffers: MediaPlaybackBuffers, controlTimebase: MediaPlayerControlTimebase) { + self.frameSource = frameSource + self.mediaBuffers = mediaBuffers + self.controlTimebase = controlTimebase + } + deinit { + var bp:Int = 0 + bp += 1 + } +} + +private enum MediaPlayerState { + case empty + case seeking(frameSource: MediaFrameSource, timestamp: Double, disposable: Disposable, action: MediaPlayerPlaybackAction, enableSound: Bool) + case paused(MediaPlayerLoadedState) + case playing(MediaPlayerLoadedState) +} + +enum MediaPlayerActionAtEnd { + case loop((() -> Void)?) + case action(() -> Void) + case loopDisablingSound(() -> Void) + case stop +} + +private final class MediaPlayerAudioRendererContext { + let renderer: MediaPlayerAudioRenderer + var requestedFrames = false + + init(renderer: MediaPlayerAudioRenderer) { + self.renderer = renderer + } +} + +private final class MediaPlayerContext { + private let queue: Queue + + private let postbox: Postbox + private let resourceReference: MediaResourceReference + private let streamable: Bool + private let video: Bool + private let preferSoftwareDecoding: Bool + private var enableSound: Bool + private var baseRate: Double + private var volume: Float + private let fetchAutomatically: Bool + private var playAndRecord: Bool + private var keepAudioSessionWhilePaused: Bool + private var initialTimebase: CMTimebase? + private var seekId: Int = 0 + + private var state: MediaPlayerState = .empty { + didSet { + assert(queue.isCurrent()) + } + } + private var audioRenderer: MediaPlayerAudioRendererContext? + private var forceAudioToSpeaker = false + fileprivate let videoRenderer: VideoPlayerProxy + + private var tickTimer: SwiftSignalKit.Timer? + + private var lastStatusUpdateTimestamp: Double? + private let playerStatus: ValuePromise + + fileprivate var actionAtEnd: MediaPlayerActionAtEnd = .stop + + fileprivate var timebasePromise: Promise = Promise() + + private var stoppedAtEnd = false + + init(queue: Queue, playerStatus: ValuePromise, postbox: Postbox, resourceReference: MediaResourceReference, streamable: Bool, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool, enableSound: Bool, baseRate: Double, volume: Float, fetchAutomatically: Bool, playAndRecord: Bool, keepAudioSessionWhilePaused: Bool, initialTimebase: CMTimebase?) { + assert(queue.isCurrent()) + + self.queue = queue + self.initialTimebase = initialTimebase + self.playerStatus = playerStatus + self.postbox = postbox + self.resourceReference = resourceReference + self.streamable = streamable + self.video = video + self.preferSoftwareDecoding = preferSoftwareDecoding + self.enableSound = enableSound + self.baseRate = baseRate + self.volume = volume + self.fetchAutomatically = fetchAutomatically + self.playAndRecord = playAndRecord + self.keepAudioSessionWhilePaused = keepAudioSessionWhilePaused + + self.videoRenderer = VideoPlayerProxy(queue: queue) + + self.videoRenderer.visibilityUpdated = { [weak self] value in + assert(queue.isCurrent()) + + if let strongSelf = self, !strongSelf.enableSound { + switch strongSelf.state { + case .empty: + if value && playAutomatically { + strongSelf.play() + } + case .paused: + if value { + strongSelf.play() + } + case .playing: + if !value { + strongSelf.pause(lostAudioSession: false) + } + case let .seeking(_, _, _, action, _): + switch action { + case .pause: + if value { + strongSelf.play() + } + case .play: + if !value { + strongSelf.pause(lostAudioSession: false) + } + } + } + } + } + + self.videoRenderer.takeFrameAndQueue = (queue, { [weak self] in + assert(queue.isCurrent()) + + if let strongSelf = self { + var maybeLoadedState: MediaPlayerLoadedState? + + switch strongSelf.state { + case .empty: + return .noFrames + case let .paused(state): + maybeLoadedState = state + case let .playing(state): + maybeLoadedState = state + case .seeking: + return .noFrames + } + + if let loadedState = maybeLoadedState, let videoBuffer = loadedState.mediaBuffers.videoBuffer { + if let (extraVideoFrames, atTime) = loadedState.extraVideoFrames { + loadedState.extraVideoFrames = nil + return .restoreState(extraVideoFrames, atTime) + } else { + return videoBuffer.takeFrame() + } + } else { + return .noFrames + } + } else { + return .noFrames + } + + }) + } + + deinit { + assert(self.queue.isCurrent()) + + self.tickTimer?.invalidate() + + if case let .seeking(_, _, disposable, _, _) = self.state { + disposable.dispose() + } + } + + fileprivate func seek(timestamp: Double) { + assert(self.queue.isCurrent()) + + let action: MediaPlayerPlaybackAction + switch self.state { + case .empty, .paused: + action = .pause + case .playing: + action = .play + case let .seeking(_, _, _, currentAction, _): + action = currentAction + } + self.seek(timestamp: timestamp, action: action) + } + + fileprivate func seek(timestamp: Double, action: MediaPlayerPlaybackAction) { + assert(self.queue.isCurrent()) + + var loadedState: MediaPlayerLoadedState? + switch self.state { + case .empty: + break + case let .playing(currentLoadedState): + loadedState = currentLoadedState + case let .paused(currentLoadedState): + loadedState = currentLoadedState + case let .seeking(previousFrameSource, previousTimestamp, previousDisposable, _, previousEnableSound): + if previousTimestamp.isEqual(to: timestamp) && self.enableSound == previousEnableSound { + self.state = .seeking(frameSource: previousFrameSource, timestamp: previousTimestamp, disposable: previousDisposable, action: action, enableSound: self.enableSound) + } else { + previousDisposable.dispose() + } + } + + self.tickTimer?.invalidate() + if let loadedState = loadedState { + self.seekId += 1 + + if loadedState.controlTimebase.isAudio { + self.audioRenderer?.renderer.setRate(0.0) + } else { + if !CMTimebaseGetRate(loadedState.controlTimebase.timebase).isEqual(to: 0.0) { + CMTimebaseSetRate(loadedState.controlTimebase.timebase, rate: 0.0) + } + } + let currentTimestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase)) + var duration: Double = 0.0 + var videoStatus: MediaTrackFrameBufferStatus? + if let videoTrackFrameBuffer = loadedState.mediaBuffers.videoBuffer { + videoStatus = videoTrackFrameBuffer.status(at: currentTimestamp) + duration = max(duration, CMTimeGetSeconds(videoTrackFrameBuffer.duration)) + } + + var audioStatus: MediaTrackFrameBufferStatus? + if let audioTrackFrameBuffer = loadedState.mediaBuffers.audioBuffer { + audioStatus = audioTrackFrameBuffer.status(at: currentTimestamp) + duration = max(duration, CMTimeGetSeconds(audioTrackFrameBuffer.duration)) + } + let status = MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: duration, dimensions: CGSize(), timestamp: min(max(timestamp, 0.0), duration), baseRate: self.baseRate, volume: self.volume, seekId: self.seekId, status: .buffering(initial: false, whilePlaying: action == .play)) + self.playerStatus.set(status) + } else { + let status = MediaPlayerStatus(generationTimestamp: CACurrentMediaTime(), duration: 0.0, dimensions: CGSize(), timestamp: timestamp, baseRate: self.baseRate, volume: self.volume, seekId: self.seekId, status: .buffering(initial: false, whilePlaying: action == .play)) + self.playerStatus.set(status) + } + + let frameSource = FFMpegMediaFrameSource(queue: self.queue, postbox: self.postbox, resourceReference: self.resourceReference, tempFilePath: nil, streamable: self.streamable, video: self.video, preferSoftwareDecoding: self.preferSoftwareDecoding, fetchAutomatically: self.fetchAutomatically) + let disposable = MetaDisposable() + self.state = .seeking(frameSource: frameSource, timestamp: timestamp, disposable: disposable, action: action, enableSound: self.enableSound) + + self.lastStatusUpdateTimestamp = nil + + let seekResult = frameSource.seek(timestamp: timestamp) |> deliverOn(self.queue) + + disposable.set(seekResult.start(next: { [weak self] seekResult in + if let strongSelf = self { + var result: MediaFrameSourceSeekResult? + seekResult.with { object in + assert(strongSelf.queue.isCurrent()) + result = object + } + if let result = result { + strongSelf.seekingCompleted(seekResult: result) + } else { + assertionFailure() + } + } + }, error: { _ in + })) + } + + fileprivate func seekingCompleted(seekResult: MediaFrameSourceSeekResult) { + if traceEvents { + print("seekingCompleted at \(CMTimeGetSeconds(seekResult.timestamp))") + } + + assert(self.queue.isCurrent()) + + guard case let .seeking(frameSource, _, _, action, _) = self.state else { + assertionFailure() + return + } + + var buffers = seekResult.buffers + if !self.enableSound { + buffers = MediaPlaybackBuffers(audioBuffer: nil, videoBuffer: buffers.videoBuffer) + } + + buffers.audioBuffer?.statusUpdated = { [weak self] in + self?.tick() + } + buffers.videoBuffer?.statusUpdated = { [weak self] in + self?.tick() + } + let controlTimebase: MediaPlayerControlTimebase + + if let _ = buffers.audioBuffer { + let renderer: MediaPlayerAudioRenderer + if let currentRenderer = self.audioRenderer, !currentRenderer.requestedFrames { + renderer = currentRenderer.renderer + } else { + self.audioRenderer?.renderer.stop() + self.audioRenderer = nil + + let queue = self.queue + renderer = MediaPlayerAudioRenderer(playAndRecord: self.playAndRecord, forceAudioToSpeaker: self.forceAudioToSpeaker, baseRate: self.baseRate, volume: self.volume, updatedRate: { [weak self] in + queue.async { + if let strongSelf = self { + strongSelf.tick() + } + } + }, audioPaused: { [weak self] in + queue.async { + if let strongSelf = self { + if strongSelf.enableSound { + strongSelf.pause(lostAudioSession: true) + } else { + strongSelf.seek(timestamp: 0.0, action: .play) + } + } + } + }) + self.audioRenderer = MediaPlayerAudioRendererContext(renderer: renderer) + renderer.start() + } + + controlTimebase = MediaPlayerControlTimebase(timebase: renderer.audioTimebase, isAudio: true) + } else { + self.audioRenderer?.renderer.stop() + self.audioRenderer = nil + + var timebase: CMTimebase? + CMTimebaseCreateWithMasterClock(allocator: kCFAllocatorDefault, masterClock: CMClockGetHostTimeClock(), timebaseOut: &timebase) + CMTimebaseSetRate(timebase!, rate: self.baseRate) + CMTimebaseSetTime(timebase!, time: seekResult.timestamp) + controlTimebase = MediaPlayerControlTimebase(timebase: timebase!, isAudio: false) + } + + var loadedState: MediaPlayerLoadedState? = MediaPlayerLoadedState(frameSource: frameSource, mediaBuffers: buffers, controlTimebase: controlTimebase) + loadedState!.extraVideoFrames = (seekResult.extraDecodedVideoFrames, seekResult.timestamp) + self.timebasePromise.set(.single(loadedState!.controlTimebase.timebase)) + + + if let audioRenderer = self.audioRenderer?.renderer { + let queue = self.queue + + + + audioRenderer.flushBuffers(at: seekResult.timestamp, completion: { [weak self] in + queue.async { [weak self] in + if let strongSelf = self { + + if let loadedState = loadedState { + switch action { + case .play: + strongSelf.state = .playing(loadedState) + strongSelf.audioRenderer?.renderer.start() + case .pause: + strongSelf.state = .paused(loadedState) + } + } + + + strongSelf.lastStatusUpdateTimestamp = nil + strongSelf.tick() + } else { + loadedState = nil + } + } + }) + } else { + if let loadedState = loadedState { + switch action { + case .play: + self.state = .playing(loadedState) + case .pause: + self.state = .paused(loadedState) + } + } + + + self.lastStatusUpdateTimestamp = nil + self.tick() + } + } + + fileprivate func play() { + assert(self.queue.isCurrent()) + + switch self.state { + case .empty: + self.lastStatusUpdateTimestamp = nil + if self.enableSound { + let queue = self.queue + let renderer = MediaPlayerAudioRenderer( playAndRecord: self.playAndRecord, forceAudioToSpeaker: self.forceAudioToSpeaker, baseRate: self.baseRate, volume: self.volume, updatedRate: { [weak self] in + queue.async { + if let strongSelf = self { + strongSelf.tick() + } + } + }, audioPaused: { [weak self] in + queue.async { + if let strongSelf = self { + if strongSelf.enableSound { + strongSelf.pause(lostAudioSession: true) + } else { + strongSelf.seek(timestamp: 0.0, action: .play) + } + } + } + }) + self.audioRenderer = MediaPlayerAudioRendererContext(renderer: renderer) + renderer.start() + } + self.seek(timestamp: 0.0, action: .play) + case let .seeking(frameSource, timestamp, disposable, _, enableSound): + self.state = .seeking(frameSource: frameSource, timestamp: timestamp, disposable: disposable, action: .play, enableSound: enableSound) + self.lastStatusUpdateTimestamp = nil + case let .paused(loadedState): + if loadedState.lostAudioSession { + let timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase)) + self.seek(timestamp: timestamp, action: .play) + } else { + self.lastStatusUpdateTimestamp = nil + if self.stoppedAtEnd { + self.seek(timestamp: 0.0, action: .play) + } else { + self.state = .playing(loadedState) + self.tick() + } + } + case .playing: + break + } + } + + fileprivate func playOnceWithSound(playAndRecord: Bool) { + assert(self.queue.isCurrent()) + + if !self.enableSound { + self.lastStatusUpdateTimestamp = nil + self.enableSound = true + self.playAndRecord = playAndRecord + self.seek(timestamp: 0.0, action: .play) + } + } + + fileprivate func toggleSoundEnabled() { + assert(self.queue.isCurrent()) + + var loadedState: MediaPlayerLoadedState? + switch self.state { + case .empty: + break + case let .playing(currentLoadedState): + loadedState = currentLoadedState + case let .paused(currentLoadedState): + loadedState = currentLoadedState + case let .seeking(_, timestamp, disposable, action, _): + self.state = .empty + disposable.dispose() + self.enableSound = !self.enableSound + self.seek(timestamp: timestamp, action: action) + } + + if let loadedState = loadedState { + self.enableSound = !self.enableSound + let timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase)) + self.lastStatusUpdateTimestamp = timestamp + self.seek(timestamp: timestamp) + } + + } + + fileprivate func continuePlayingWithoutSound() { + if self.enableSound { + self.lastStatusUpdateTimestamp = nil + + var loadedState: MediaPlayerLoadedState? + switch self.state { + case .empty: + break + case let .playing(currentLoadedState): + loadedState = currentLoadedState + case let .paused(currentLoadedState): + loadedState = currentLoadedState + case let .seeking(_, timestamp, disposable, action, _): + if self.enableSound { + self.state = .empty + disposable.dispose() + self.enableSound = false + self.seek(timestamp: timestamp, action: action) + } + } + + if let loadedState = loadedState { + self.enableSound = false + self.playAndRecord = false + let timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase)) + self.seek(timestamp: timestamp, action: .play) + } + } + } + + fileprivate func setBaseRate(_ baseRate: Double) { + self.baseRate = baseRate + self.lastStatusUpdateTimestamp = nil + self.tick() + self.audioRenderer?.renderer.setBaseRate(baseRate) + } + + fileprivate func setForceAudioToSpeaker(_ value: Bool) { + if self.forceAudioToSpeaker != value { + self.forceAudioToSpeaker = value + + self.audioRenderer?.renderer.setForceAudioToSpeaker(value) + } + } + + fileprivate func setKeepAudioSessionWhilePaused(_ value: Bool) { + if self.keepAudioSessionWhilePaused != value { + self.keepAudioSessionWhilePaused = value + + var isPlaying = false + switch self.state { + case .playing: + isPlaying = true + case let .seeking(_, _, _, action, _): + switch action { + case .play: + isPlaying = true + default: + break + } + default: + break + } + if value && !isPlaying { + self.audioRenderer?.renderer.stop() + } else { + self.audioRenderer?.renderer.start() + } + } + } + + fileprivate func pause(lostAudioSession: Bool) { + assert(self.queue.isCurrent()) + + switch self.state { + case .empty: + break + case let .seeking(frameSource, timestamp, disposable, _, enableSound): + self.state = .seeking(frameSource: frameSource, timestamp: timestamp, disposable: disposable, action: .pause, enableSound: enableSound) + self.lastStatusUpdateTimestamp = nil + case let .paused(loadedState): + if lostAudioSession { + loadedState.lostAudioSession = true + } + case let .playing(loadedState): + if lostAudioSession { + loadedState.lostAudioSession = true + } + self.state = .paused(loadedState) + self.lastStatusUpdateTimestamp = nil + self.tick() + } + } + + + fileprivate func togglePlayPause() { + assert(self.queue.isCurrent()) + + switch self.state { + case .empty: + self.play() + case let .seeking(_, _, _, action, _): + switch action { + case .play: + self.pause(lostAudioSession: false) + case .pause: + self.play() + } + case .paused: + self.play() + case .playing: + self.pause(lostAudioSession: false) + } + } + + fileprivate func setVolume(_ volume: Float) { + assert(self.queue.isCurrent()) + self.volume = volume + audioRenderer?.renderer.setVolume(volume) + } + fileprivate func toggleVolumeOnOff() { + assert(self.queue.isCurrent()) + if self.volume > 0 { + self.volume = 0 + } else { + self.volume = 1.0 + } + audioRenderer?.renderer.setVolume(volume) + } + + + fileprivate func getVolume(_ completion: @escaping(Float) -> Void) { + assert(self.queue.isCurrent()) + completion(volume) + } + + + private func tick() { + self.tickTimer?.invalidate() + + var maybeLoadedState: MediaPlayerLoadedState? + + switch self.state { + case .empty: + return + case let .paused(state): + maybeLoadedState = state + case let .playing(state): + maybeLoadedState = state + case .seeking: + return + } + + guard let loadedState = maybeLoadedState else { + return + } + + let timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase)) + if traceEvents { + print("tick at \(timestamp)") + } + + var duration: Double = 0.0 + var videoStatus: MediaTrackFrameBufferStatus? + if let videoTrackFrameBuffer = loadedState.mediaBuffers.videoBuffer { + videoStatus = videoTrackFrameBuffer.status(at: timestamp) + duration = max(duration, CMTimeGetSeconds(videoTrackFrameBuffer.duration)) + } + + var audioStatus: MediaTrackFrameBufferStatus? + if let audioTrackFrameBuffer = loadedState.mediaBuffers.audioBuffer { + audioStatus = audioTrackFrameBuffer.status(at: timestamp) + duration = max(duration, CMTimeGetSeconds(audioTrackFrameBuffer.duration)) + } + + var performActionAtEndNow = false + + var worstStatus: MediaTrackFrameBufferStatus? + for status in [videoStatus, audioStatus] { + if let status = status { + if let worst = worstStatus { + switch status { + case .buffering: + worstStatus = status + case let .full(currentFullUntil): + switch worst { + case .buffering: + worstStatus = worst + case let .full(worstFullUntil): + if currentFullUntil < worstFullUntil { + worstStatus = status + } else { + worstStatus = worst + } + case .finished: + worstStatus = status + } + case let .finished(currentFinishedAt): + switch worst { + case .buffering, .full: + worstStatus = worst + case let .finished(worstFinishedAt): + if currentFinishedAt < worstFinishedAt { + worstStatus = worst + } else { + worstStatus = status + } + } + } + } else { + worstStatus = status + } + } + } + + var rate: Double + var buffering = false + + if let worstStatus = worstStatus, case let .full(fullUntil) = worstStatus, fullUntil.isFinite { + if case .playing = self.state { + rate = self.baseRate + + let nextTickDelay = max(0.0, fullUntil - timestamp) / self.baseRate + let tickTimer = SwiftSignalKit.Timer(timeout: nextTickDelay, repeat: false, completion: { [weak self] in + self?.tick() + }, queue: self.queue) + self.tickTimer = tickTimer + tickTimer.start() + } else { + rate = 0.0 + } + } else if let worstStatus = worstStatus, case let .finished(finishedAt) = worstStatus, finishedAt.isFinite { + let nextTickDelay = max(0.0, finishedAt - timestamp) / self.baseRate + if nextTickDelay.isLessThanOrEqualTo(0.0) { + rate = 0.0 + performActionAtEndNow = true + } else { + if case .playing = self.state { + rate = self.baseRate + + let tickTimer = SwiftSignalKit.Timer(timeout: nextTickDelay, repeat: false, completion: { [weak self] in + self?.tick() + }, queue: self.queue) + self.tickTimer = tickTimer + tickTimer.start() + } else { + rate = 0.0 + } + } + } else { + buffering = true + rate = 0.0 + } + + var reportRate = rate + + if loadedState.controlTimebase.isAudio { + if rate.isEqual(to: 1.0) { + self.audioRenderer?.renderer.start() + } + self.audioRenderer?.renderer.setRate(rate) + if rate.isEqual(to: 1.0), let audioRenderer = self.audioRenderer { + let timebaseRate = CMTimebaseGetRate(audioRenderer.renderer.audioTimebase) + if !timebaseRate.isEqual(to: rate) { + reportRate = timebaseRate + } + } + } else { + if !CMTimebaseGetRate(loadedState.controlTimebase.timebase).isEqual(to: rate) { + CMTimebaseSetRate(loadedState.controlTimebase.timebase, rate: rate) + } + } + + if let videoTrackFrameBuffer = loadedState.mediaBuffers.videoBuffer, videoTrackFrameBuffer.hasFrames { + self.videoRenderer.state = (loadedState.controlTimebase.timebase, true, videoTrackFrameBuffer.rotationAngle, videoTrackFrameBuffer.aspect) + } + + if let audioRenderer = self.audioRenderer, let audioTrackFrameBuffer = loadedState.mediaBuffers.audioBuffer, audioTrackFrameBuffer.hasFrames { + let queue = self.queue + audioRenderer.requestedFrames = true + audioRenderer.renderer.beginRequestingFrames(queue: queue.queue, takeFrame: { [weak audioTrackFrameBuffer] in + assert(queue.isCurrent()) + if let audioTrackFrameBuffer = audioTrackFrameBuffer { + return audioTrackFrameBuffer.takeFrame() + } else { + return .noFrames + } + }) + } + + var statusTimestamp = CACurrentMediaTime() + let playbackStatus: MediaPlayerPlaybackStatus + if buffering { + var whilePlaying = false + if case .playing = self.state { + whilePlaying = true + } + playbackStatus = .buffering(initial: false, whilePlaying: whilePlaying) + } else if !rate.isZero { + if reportRate.isZero { + //playbackStatus = .buffering(initial: false, whilePlaying: true) + playbackStatus = .playing + statusTimestamp = 0.0 + } else { + playbackStatus = .playing + } + } else { + playbackStatus = .paused + } + if self.lastStatusUpdateTimestamp == nil || self.lastStatusUpdateTimestamp! < statusTimestamp + 500 { + lastStatusUpdateTimestamp = statusTimestamp + var reportTimestamp = timestamp + if case .seeking(_, timestamp, _, _, _) = self.state { + reportTimestamp = timestamp + } + let status = MediaPlayerStatus(generationTimestamp: statusTimestamp, duration: duration, dimensions: CGSize(), timestamp: min(max(reportTimestamp, 0.0), duration), baseRate: self.baseRate, volume: self.volume, seekId: self.seekId, status: playbackStatus) + self.playerStatus.set(status) + } + + + if performActionAtEndNow { + switch self.actionAtEnd { + case let .loop(f): + self.stoppedAtEnd = false + self.seek(timestamp: 0.0, action: .play) + f?() + case .stop: + self.stoppedAtEnd = true + self.pause(lostAudioSession: false) + case let .action(f): + self.stoppedAtEnd = true + // self.pause(lostAudioSession: false) + f() + case let .loopDisablingSound(f): + self.stoppedAtEnd = false + self.enableSound = false + self.seek(timestamp: 0.0, action: .play) + f() + } + } else { + self.stoppedAtEnd = false + } + } +} + +enum MediaPlayerPlaybackStatus: Equatable { + case playing + case paused + case buffering(initial: Bool, whilePlaying: Bool) + + static func ==(lhs: MediaPlayerPlaybackStatus, rhs: MediaPlayerPlaybackStatus) -> Bool { + switch lhs { + case .playing: + if case .playing = rhs { + return true + } else { + return false + } + case .paused: + if case .paused = rhs { + return true + } else { + return false + } + case let .buffering(initial, whilePlaying): + if case .buffering(initial, whilePlaying) = rhs { + return true + } else { + return false + } + } + } +} + +struct MediaPlayerStatus: Equatable { + let generationTimestamp: Double + let duration: Double + let dimensions: CGSize + let timestamp: Double + let baseRate: Double + let volume: Float + let seekId: Int + let status: MediaPlayerPlaybackStatus +} + +let playerQueue = Queue() + + +final class MediaPlayer { + + private var contextRef: QueueLocalObject + + private let timebasePromise:Promise = Promise() + + var timebase: Signal { + return timebasePromise.get() + } + + private let statusValue = ValuePromise(ignoreRepeated: true) + + var status: Signal { + return self.statusValue.get() + } + + var actionAtEnd: MediaPlayerActionAtEnd = .stop { + didSet { + let value = self.actionAtEnd + contextRef.with { context in + context.actionAtEnd = value + } + } + } + + init(postbox: Postbox, reference: MediaResourceReference, streamable: Bool, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool = false, enableSound: Bool, baseRate: Double = 1.0, volume: Float = 0.8, fetchAutomatically: Bool, playAndRecord: Bool = false, keepAudioSessionWhilePaused: Bool = true, initialTimebase: CMTimebase? = nil) { + + self.statusValue.set(MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: baseRate, volume: volume, seekId: 0, status: .paused)) + + let statusValue = self.statusValue + + self.contextRef = QueueLocalObject(queue: playerQueue, generate: { + return MediaPlayerContext(queue: playerQueue, playerStatus: statusValue, postbox: postbox, resourceReference: reference, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, playAutomatically: playAutomatically, enableSound: enableSound, baseRate: baseRate, volume: volume, fetchAutomatically: fetchAutomatically, playAndRecord: playAndRecord, keepAudioSessionWhilePaused: keepAudioSessionWhilePaused, initialTimebase: initialTimebase) + + }) + + let timebasePromise = self.timebasePromise + self.contextRef.with({ context in + timebasePromise.set(context.timebasePromise.get()) + }) + + } + + + + deinit { + + } + + func play() { + contextRef.with { + $0.play() + } + } + + func playOnceWithSound(playAndRecord: Bool) { + contextRef.with { + $0.playOnceWithSound(playAndRecord: playAndRecord) + } + } + + func toggleSoundEnabled() { + contextRef.with { + $0.toggleSoundEnabled() + } + } + + func continuePlayingWithoutSound() { + contextRef.with { + $0.continuePlayingWithoutSound() + } + } + + func setForceAudioToSpeaker(_ value: Bool) { + contextRef.with { context in + context.setForceAudioToSpeaker(value) + } + } + + func setKeepAudioSessionWhilePaused(_ value: Bool) { + contextRef.with { context in + context.setKeepAudioSessionWhilePaused(value) + } + } + + func pause() { + contextRef.with { context in + context.pause(lostAudioSession: false) + } + } + + func togglePlayPause() { + contextRef.with { context in + context.togglePlayPause() + } + } + + func setVolume(_ volume: Float) { + contextRef.with { context in + context.setVolume(volume) + } + } + + func toggleVolumeOnOff() { + contextRef.with { context in + context.toggleVolumeOnOff() + } + } + + func getVolume(_ completion: @escaping(Float) -> Void) { + contextRef.with { context in + context.getVolume(completion) + } + } + + func seek(timestamp: Double) { + contextRef.with { context in + context.seek(timestamp: timestamp) + } + } + + + func setBaseRate(_ baseRate: Double) { + contextRef.with { context in + context.setBaseRate(baseRate) + } + } + + func attachPlayerView(_ node: MediaPlayerView) { + let nodeRef: Unmanaged = Unmanaged.passRetained(node) + contextRef.with { context in + context.videoRenderer.attachNodeAndRelease(nodeRef) + } + } +} diff --git a/Telegram-Mac/MediaPlayerAudioRenderer.swift b/Telegram-Mac/MediaPlayerAudioRenderer.swift new file mode 100755 index 0000000000..c267b99ab7 --- /dev/null +++ b/Telegram-Mac/MediaPlayerAudioRenderer.swift @@ -0,0 +1,833 @@ +import Foundation +import SwiftSignalKit +import CoreMedia +import AVFoundation +import TelegramCore + + +private enum AudioPlayerRendererState { + case paused + case playing(rate: Double, didSetRate: Bool) +} + +private final class AudioPlayerRendererBufferContext { + var state: AudioPlayerRendererState = .paused + let timebase: CMTimebase + let buffer: RingByteBuffer + var bufferMaxChannelSampleIndex: Int64 = 0 + var lowWaterSize: Int + var notifyLowWater: () -> Void + var updatedRate: () -> Void + var notifiedLowWater = false + var overflowData = Data() + var overflowDataMaxChannelSampleIndex: Int64 = 0 + var renderTimestampTick: Int64 = 0 + + init(timebase: CMTimebase, buffer: RingByteBuffer, lowWaterSize: Int, notifyLowWater: @escaping () -> Void, updatedRate: @escaping () -> Void) { + self.timebase = timebase + self.buffer = buffer + self.lowWaterSize = lowWaterSize + self.notifyLowWater = notifyLowWater + self.updatedRate = updatedRate + } +} + +private let audioPlayerRendererBufferContextMap = Atomic<[Int32: Atomic]>(value: [:]) +private let audioPlayerRendererQueue = Queue() + +private var _nextPlayerRendererBufferContextId: Int32 = 1 +private func registerPlayerRendererBufferContext(_ context: Atomic) -> Int32 { + var id: Int32 = 0 + + let _ = audioPlayerRendererBufferContextMap.modify { contextMap in + id = _nextPlayerRendererBufferContextId + _nextPlayerRendererBufferContextId += 1 + + var contextMap = contextMap + contextMap[id] = context + return contextMap + } + return id +} + +private func unregisterPlayerRendererBufferContext(_ id: Int32) { + let _ = audioPlayerRendererBufferContextMap.modify { contextMap in + var contextMap = contextMap + let _ = contextMap.removeValue(forKey: id) + return contextMap + } +} + +private func withPlayerRendererBuffer(_ id: Int32, _ f: (Atomic) -> Void) { + audioPlayerRendererBufferContextMap.with { contextMap in + if let context = contextMap[id] { + f(context) + } + } +} + +private let kOutputBus: UInt32 = 0 +private let kInputBus: UInt32 = 1 + +private func rendererInputProcPlayer(refCon: UnsafeMutableRawPointer, ioActionFlags: UnsafeMutablePointer, inTimeStamp: UnsafePointer, inBusNumber: UInt32, inNumberFrames: UInt32, ioData: UnsafeMutablePointer?) -> OSStatus { + guard let ioData = ioData else { + return noErr + } + + let bufferList = UnsafeMutableAudioBufferListPointer(ioData) + + var rendererFillOffset = (0, 0) + var notifyLowWater: (() -> Void)? + var updatedRate: (() -> Void)? + + withPlayerRendererBuffer(Int32(intptr_t(bitPattern: refCon)), { context in + context.with { context in + switch context.state { + case let .playing(rate, didSetRate): + if context.buffer.availableBytes != 0 { + let sampleIndex = context.bufferMaxChannelSampleIndex - Int64(context.buffer.availableBytes / (2 * + 2)) + + if !didSetRate { + context.state = .playing(rate: rate, didSetRate: true) + let masterClock: CMClockOrTimebase = CMTimebaseGetMaster(context.timebase)! + CMTimebaseSetRateAndAnchorTime(context.timebase, rate: rate, anchorTime: CMTimeMake(value: sampleIndex, timescale: 44100), immediateMasterTime: CMSyncGetTime(masterClock)) + updatedRate = context.updatedRate + } else { + context.renderTimestampTick += 1 + if context.renderTimestampTick % 1000 == 0 { + let delta = (Double(sampleIndex) / 44100.0) - CMTimeGetSeconds(CMTimebaseGetTime(context.timebase)) + if delta > 0.01 { + CMTimebaseSetTime(context.timebase, time: CMTimeMake(value: sampleIndex, timescale: 44100)) + updatedRate = context.updatedRate + } + } + } + + let rendererBuffer = context.buffer + + while rendererFillOffset.0 < bufferList.count { + if let bufferData = bufferList[rendererFillOffset.0].mData { + let bufferDataSize = Int(bufferList[rendererFillOffset.0].mDataByteSize) + + let dataOffset = rendererFillOffset.1 + if dataOffset == bufferDataSize { + rendererFillOffset = (rendererFillOffset.0 + 1, 0) + continue + } + + let consumeCount = bufferDataSize - dataOffset + + let actualConsumedCount = rendererBuffer.dequeue(bufferData.advanced(by: dataOffset), count: consumeCount) + rendererFillOffset.1 += actualConsumedCount + + if actualConsumedCount == 0 { + break + } + } else { + break + } + } + } + + if !context.notifiedLowWater { + let availableBytes = context.buffer.availableBytes + if availableBytes <= context.lowWaterSize { + context.notifiedLowWater = true + notifyLowWater = context.notifyLowWater + } + } + case .paused: + break + } + } + }) + + for i in rendererFillOffset.0 ..< bufferList.count { + var dataOffset = 0 + if i == rendererFillOffset.0 { + dataOffset = rendererFillOffset.1 + } + if let data = bufferList[i].mData { + memset(data.advanced(by: dataOffset), 0, Int(bufferList[i].mDataByteSize) - dataOffset) + } + } + + if let notifyLowWater = notifyLowWater { + notifyLowWater() + } + + if let updatedRate = updatedRate { + updatedRate() + } + + return noErr +} + +private struct RequestingFramesContext { + let queue: DispatchQueue + let takeFrame: () -> MediaTrackFrameResult +} + +private final class AudioPlayerRendererContext { + let audioStreamDescription: AudioStreamBasicDescription + let bufferSizeInSeconds: Int = 5 + let lowWaterSizeInSeconds: Int = 2 + + let controlTimebase: CMTimebase + let updatedRate: () -> Void + let audioPaused: () -> Void + + var paused = true + var baseRate: Double + var volume: Float + + var audioGraph: AUGraph? + var timePitchAudioUnit: AudioComponentInstance? + var outputAudioUnit: AudioComponentInstance? + + var bufferContextId: Int32! + let bufferContext: Atomic + + var requestingFramesContext: RequestingFramesContext? + + let audioSessionDisposable = MetaDisposable() + var audioSessionControl: ManagedAudioSessionControl? + let playAndRecord: Bool + var forceAudioToSpeaker: Bool { + didSet { + if self.forceAudioToSpeaker != oldValue { + if let audioSessionControl = self.audioSessionControl { + audioSessionControl.setOutputMode(self.forceAudioToSpeaker ? .speakerIfNoHeadphones : .system) + } + } + } + } + + init(controlTimebase: CMTimebase, playAndRecord: Bool, forceAudioToSpeaker: Bool, baseRate: Double, volume: Float, updatedRate: @escaping () -> Void, audioPaused: @escaping () -> Void) { + assert(audioPlayerRendererQueue.isCurrent()) + + self.forceAudioToSpeaker = forceAudioToSpeaker + self.baseRate = baseRate + + self.controlTimebase = controlTimebase + self.updatedRate = updatedRate + self.audioPaused = audioPaused + self.volume = volume + self.playAndRecord = playAndRecord + + self.audioStreamDescription = audioRendererNativeStreamDescription() + + let bufferSize = Int(self.audioStreamDescription.mSampleRate) * self.bufferSizeInSeconds * Int(self.audioStreamDescription.mBytesPerFrame) + let lowWaterSize = Int(self.audioStreamDescription.mSampleRate) * self.lowWaterSizeInSeconds * Int(self.audioStreamDescription.mBytesPerFrame) + + var notifyLowWater: () -> Void = { } + + self.bufferContext = Atomic(value: AudioPlayerRendererBufferContext(timebase: controlTimebase, buffer: RingByteBuffer(size: bufferSize), lowWaterSize: lowWaterSize, notifyLowWater: { + notifyLowWater() + }, updatedRate: { + updatedRate() + })) + self.bufferContextId = registerPlayerRendererBufferContext(self.bufferContext) + + notifyLowWater = { [weak self] in + audioPlayerRendererQueue.async { + if let strongSelf = self { + strongSelf.checkBuffer() + } + } + } + } + + deinit { + // assert(audioPlayerRendererQueue.isCurrent()) + + self.audioSessionDisposable.dispose() + + unregisterPlayerRendererBufferContext(self.bufferContextId) + + self.closeAudioUnit() + } + + fileprivate func setBaseRate(_ baseRate: Double) { + if let timePitchAudioUnit = self.timePitchAudioUnit, !self.baseRate.isEqual(to: baseRate) { + self.baseRate = baseRate + AudioUnitSetParameter(timePitchAudioUnit, kTimePitchParam_Rate, kAudioUnitScope_Global, 0, Float32(baseRate), 0) + self.bufferContext.with { context in + if case .playing = context.state { + context.state = .playing(rate: baseRate, didSetRate: false) + } + } + } + } + + fileprivate func setRate(_ rate: Double) { + assert(audioPlayerRendererQueue.isCurrent()) + + if !rate.isZero && self.paused { + self.start() + } + + let baseRate = self.baseRate + + self.bufferContext.with { context in + if !rate.isZero { + if case .playing = context.state { + } else { + context.state = .playing(rate: baseRate, didSetRate: false) + } + } else { + context.state = .paused + CMTimebaseSetRate(context.timebase, rate: 0.0) + } + } + } + + fileprivate func flushBuffers(at timestamp: CMTime, completion: () -> Void) { + assert(audioPlayerRendererQueue.isCurrent()) + + self.bufferContext.with { context in + context.buffer.clear() + context.bufferMaxChannelSampleIndex = 0 + context.notifiedLowWater = false + context.overflowData = Data() + context.overflowDataMaxChannelSampleIndex = 0 + CMTimebaseSetTime(context.timebase, time: timestamp) + + switch context.state { + case let .playing(rate, _): + context.state = .playing(rate: rate, didSetRate: false) + case .paused: + break + } + } + + completion() + } + + fileprivate func start() { + assert(audioPlayerRendererQueue.isCurrent()) + + if self.paused { + self.paused = false + self.startAudioUnit() + } + } + + fileprivate func stop() { + assert(audioPlayerRendererQueue.isCurrent()) + + if !self.paused { + self.paused = true + self.setRate(0.0) + self.closeAudioUnit() + } + } + + private func startAudioUnit() { + assert(audioPlayerRendererQueue.isCurrent()) + + if self.audioGraph == nil { + var maybeAudioGraph: AUGraph? + guard NewAUGraph(&maybeAudioGraph) == noErr, let audioGraph = maybeAudioGraph else { + return + } + + var converterNode: AUNode = 0 + var converterDescription = AudioComponentDescription() + converterDescription.componentType = kAudioUnitType_FormatConverter + converterDescription.componentSubType = kAudioUnitSubType_AUConverter + converterDescription.componentManufacturer = kAudioUnitManufacturer_Apple + guard AUGraphAddNode(audioGraph, &converterDescription, &converterNode) == noErr else { + return + } + + var timePitchNode: AUNode = 0 + var timePitchDescription = AudioComponentDescription() + timePitchDescription.componentType = kAudioUnitType_FormatConverter + timePitchDescription.componentSubType = kAudioUnitSubType_AUiPodTimeOther + timePitchDescription.componentManufacturer = kAudioUnitManufacturer_Apple + guard AUGraphAddNode(audioGraph, &timePitchDescription, &timePitchNode) == noErr else { + return + } + + var outputNode: AUNode = 0 + var outputDesc = AudioComponentDescription() + outputDesc.componentType = kAudioUnitType_Output + outputDesc.componentSubType = kAudioUnitSubType_HALOutput + outputDesc.componentFlags = 0 + outputDesc.componentFlagsMask = 0 + outputDesc.componentManufacturer = kAudioUnitManufacturer_Apple + guard AUGraphAddNode(audioGraph, &outputDesc, &outputNode) == noErr else { + return + } + + guard AUGraphOpen(audioGraph) == noErr else { + return + } + + guard AUGraphConnectNodeInput(audioGraph, converterNode, 0, timePitchNode, 0) == noErr else { + return + } + + guard AUGraphConnectNodeInput(audioGraph, timePitchNode, 0, outputNode, 0) == noErr else { + return + } + + var maybeConverterAudioUnit: AudioComponentInstance? + guard AUGraphNodeInfo(audioGraph, converterNode, &converterDescription, &maybeConverterAudioUnit) == noErr, let converterAudioUnit = maybeConverterAudioUnit else { + return + } + + var maybeTimePitchAudioUnit: AudioComponentInstance? + guard AUGraphNodeInfo(audioGraph, timePitchNode, &timePitchDescription, &maybeTimePitchAudioUnit) == noErr, let timePitchAudioUnit = maybeTimePitchAudioUnit else { + return + } + AudioUnitSetParameter(timePitchAudioUnit, kTimePitchParam_Rate, kAudioUnitScope_Global, 0, Float32(self.baseRate), 0) + + var maybeOutputAudioUnit: AudioComponentInstance? + guard AUGraphNodeInfo(audioGraph, outputNode, &outputDesc, &maybeOutputAudioUnit) == noErr, let outputAudioUnit = maybeOutputAudioUnit else { + return + } + + var outputAudioFormat = audioRendererNativeStreamDescription() + + AudioUnitSetProperty(converterAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &outputAudioFormat, UInt32(MemoryLayout.size)) + + var streamFormat = AudioStreamBasicDescription() + AudioUnitSetProperty(converterAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &streamFormat, UInt32(MemoryLayout.size)) + AudioUnitSetProperty(timePitchAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &streamFormat, UInt32(MemoryLayout.size)) + AudioUnitSetProperty(converterAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &streamFormat, UInt32(MemoryLayout.size)) + + var callbackStruct = AURenderCallbackStruct() + callbackStruct.inputProc = rendererInputProcPlayer + callbackStruct.inputProcRefCon = UnsafeMutableRawPointer(bitPattern: intptr_t(self.bufferContextId)) + + guard AUGraphSetNodeInputCallback(audioGraph, converterNode, 0, &callbackStruct) == noErr else { + return + } + + var one: UInt32 = 1 + guard AudioUnitSetProperty(outputAudioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, kOutputBus, &one, 4) == noErr else { + return + } + + AudioUnitSetParameter(outputAudioUnit, kHALOutputParam_Volume, kAudioUnitScope_Output, kOutputBus, max(min(1, volume), 0), 0) + +// guard AudioUnitSetParameter(outputAudioUnit, kHALOutputParam_Volume, kAudioUnitScope_Output, kOutputBus, 0.1, 0) == noErr else { +// return +// } + + var maximumFramesPerSlice: UInt32 = 4096 + AudioUnitSetProperty(converterAudioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maximumFramesPerSlice, 4) + AudioUnitSetProperty(timePitchAudioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maximumFramesPerSlice, 4) + AudioUnitSetProperty(outputAudioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maximumFramesPerSlice, 4) + + guard AUGraphInitialize(audioGraph) == noErr else { + return + } + + self.audioGraph = audioGraph + self.timePitchAudioUnit = timePitchAudioUnit + self.outputAudioUnit = outputAudioUnit + } + audioSessionAcquired() + } + + + + func setVolume(_ volume: Float) { + assert(audioPlayerRendererQueue.isCurrent()) + self.volume = max(min(1, volume), 0) + if let outputAudioUnit = outputAudioUnit { + AudioUnitSetParameter(outputAudioUnit, kHALOutputParam_Volume, kAudioUnitScope_Output, kOutputBus, self.volume, 0) + + } + } + + private func audioSessionAcquired() { + assert(audioPlayerRendererQueue.isCurrent()) + + if let audioGraph = self.audioGraph { + guard AUGraphStart(audioGraph) == noErr else { + self.closeAudioUnit() + return + } + } + } + + private func closeAudioUnit() { + assert(audioPlayerRendererQueue.isCurrent()) + + if let audioGraph = self.audioGraph { + var status = noErr + + self.bufferContext.with { context in + context.buffer.clear() + } + + status = AUGraphStop(audioGraph) + if status != noErr { + Logger.shared.log("AudioPlayerRenderer", "AUGraphStop error \(status)") + } + + status = AUGraphUninitialize(audioGraph) + if status != noErr { + Logger.shared.log("AudioPlayerRenderer", "AUGraphUninitialize error \(status)") + } + + status = AUGraphClose(audioGraph) + if status != noErr { + Logger.shared.log("AudioPlayerRenderer", "AUGraphClose error \(status)") + } + + status = DisposeAUGraph(audioGraph) + if status != noErr { + Logger.shared.log("AudioPlayerRenderer", "DisposeAUGraph error \(status)") + } + + self.audioGraph = nil + self.outputAudioUnit = nil + self.timePitchAudioUnit = nil + } + } + + func checkBuffer() { + assert(audioPlayerRendererQueue.isCurrent()) + + while true { + let bytesToRequest = self.bufferContext.with { context -> Int in + let availableBytes = context.buffer.availableBytes + if availableBytes <= context.lowWaterSize { + return context.buffer.size - availableBytes + } else { + return 0 + } + } + + if bytesToRequest == 0 { + self.bufferContext.with { context in + context.notifiedLowWater = false + } + break + } + + let overflowTakenLength = self.bufferContext.with { context -> Int in + let takeLength = min(context.overflowData.count, bytesToRequest) + if takeLength != 0 { + if takeLength == context.overflowData.count { + let data = context.overflowData + context.overflowData = Data() + self.enqueueSamples(data, sampleIndex: context.overflowDataMaxChannelSampleIndex - Int64(data.count / (2 * 2))) + } else { + let data = context.overflowData.subdata(in: 0 ..< takeLength) + self.enqueueSamples(data, sampleIndex: context.overflowDataMaxChannelSampleIndex - Int64(context.overflowData.count / (2 * 2))) + context.overflowData.replaceSubrange(0 ..< takeLength, with: Data()) + } + } + return takeLength + } + + if overflowTakenLength != 0 { + continue + } + + if let requestingFramesContext = self.requestingFramesContext { + requestingFramesContext.queue.async { + let takenFrame = requestingFramesContext.takeFrame() + audioPlayerRendererQueue.async { + switch takenFrame { + case let .frame(frame): + if let dataBuffer = CMSampleBufferGetDataBuffer(frame.sampleBuffer) { + let dataLength = CMBlockBufferGetDataLength(dataBuffer) + let takeLength = min(dataLength, bytesToRequest) + + let pts = CMSampleBufferGetPresentationTimeStamp(frame.sampleBuffer) + let bufferSampleIndex = CMTimeConvertScale(pts, timescale: 44100, method: .roundAwayFromZero).value + + let bytes = malloc(takeLength)! + CMBlockBufferCopyDataBytes(dataBuffer, atOffset: 0, dataLength: takeLength, destination: bytes) + self.enqueueSamples(Data(bytesNoCopy: bytes.assumingMemoryBound(to: UInt8.self), count: takeLength, deallocator: .free), sampleIndex: bufferSampleIndex) + + if takeLength < dataLength { + self.bufferContext.with { context in + let copyOffset = context.overflowData.count + context.overflowData.count += dataLength - takeLength + context.overflowData.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) -> Void in + CMBlockBufferCopyDataBytes(dataBuffer, atOffset: takeLength, dataLength: dataLength - takeLength, destination: bytes.advanced(by: copyOffset)) + } + } + } + + self.checkBuffer() + } else { + assertionFailure() + } + case .restoreState: + assertionFailure() + self.checkBuffer() + break + case .skipFrame: + self.checkBuffer() + break + case .noFrames, .finished: + self.requestingFramesContext = nil + } + } + } + } else { + self.bufferContext.with { context in + context.notifiedLowWater = false + } + } + + break + } + } + + private func enqueueSamples(_ data: Data, sampleIndex: Int64) { + assert(audioPlayerRendererQueue.isCurrent()) + + self.bufferContext.with { context in + let bytesToCopy = min(context.buffer.size - context.buffer.availableBytes, data.count) + data.withUnsafeBytes { (bytes: UnsafePointer) -> Void in + let _ = context.buffer.enqueue(UnsafeRawPointer(bytes), count: bytesToCopy) + context.bufferMaxChannelSampleIndex = sampleIndex + Int64(data.count / (2 * 2)) + } + } + } + + fileprivate func beginRequestingFrames(queue: DispatchQueue, takeFrame: @escaping () -> MediaTrackFrameResult) { + assert(audioPlayerRendererQueue.isCurrent()) + + if let _ = self.requestingFramesContext { + return + } + + self.requestingFramesContext = RequestingFramesContext(queue: queue, takeFrame: takeFrame) + + self.checkBuffer() + } + + func endRequestingFrames() { + assert(audioPlayerRendererQueue.isCurrent()) + + self.requestingFramesContext = nil + } +} + +private func audioRendererNativeStreamDescription() -> AudioStreamBasicDescription { + var canonicalBasicStreamDescription = AudioStreamBasicDescription() + canonicalBasicStreamDescription.mSampleRate = 44100.00 + canonicalBasicStreamDescription.mFormatID = kAudioFormatLinearPCM + canonicalBasicStreamDescription.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked + canonicalBasicStreamDescription.mFramesPerPacket = 1 + canonicalBasicStreamDescription.mChannelsPerFrame = 2 + canonicalBasicStreamDescription.mBytesPerFrame = 2 * 2 + canonicalBasicStreamDescription.mBitsPerChannel = 8 * 2 + canonicalBasicStreamDescription.mBytesPerPacket = 2 * 2 + return canonicalBasicStreamDescription +} + +final class MediaPlayerAudioSessionCustomControl { + let activate: () -> Void + let deactivate: () -> Void + + init(activate: @escaping () -> Void, deactivate: @escaping () -> Void) { + self.activate = activate + self.deactivate = deactivate + } +} + +enum MediaPlayerAudioSessionControl { + case manager(ManagedAudioSession) + case custom((MediaPlayerAudioSessionCustomControl) -> Disposable) +} + +struct AudioAddress { + static var outputDevice = AudioObjectPropertyAddress(mSelector: kAudioHardwarePropertyDefaultOutputDevice, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster) + + static var inputDevice = AudioObjectPropertyAddress(mSelector: kAudioHardwarePropertyDefaultInputDevice, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster) + +} + +enum AudioNotification: String { + case audioDevicesDidChange + case audioInputDeviceDidChange + case audioOutputDeviceDidChange + + var stringValue: String { + return "Audio" + rawValue + } + + var notificationName: NSNotification.Name { + return NSNotification.Name(stringValue) + } +} + +struct AudioListener { + static var output: AudioObjectPropertyListenerProc = { _, _, _, _ in + NotificationCenter.default.post(name: AudioNotification.audioOutputDeviceDidChange.notificationName, object: nil) + return 0 + } + static var input: AudioObjectPropertyListenerProc = { _, _, _, _ in + NotificationCenter.default.post(name: AudioNotification.audioInputDeviceDidChange.notificationName, object: nil) + return 0 + } +} + + +final class MediaPlayerAudioRenderer { + private var contextRef: Unmanaged? + + let audioTimebase: CMTimebase + + init(playAndRecord: Bool, forceAudioToSpeaker: Bool, baseRate: Double, volume: Float, updatedRate: @escaping () -> Void, audioPaused: @escaping () -> Void) { + var audioClock: CMClock? + + var deviceId:AudioDeviceID = AudioDeviceID() + var deviceIdRequest:AudioObjectPropertyAddress = AudioObjectPropertyAddress(mSelector: kAudioHardwarePropertyDefaultOutputDevice, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMaster) + var deviceIdSize:UInt32 = UInt32(MemoryLayout.size) + + _ = AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject), &deviceIdRequest, 0, nil, &deviceIdSize, &deviceId) + + CMAudioDeviceClockCreateFromAudioDeviceID(allocator: kCFAllocatorDefault, deviceID: deviceId, clockOut: &audioClock) + + if audioClock == nil { + CMAudioDeviceClockCreate(allocator: nil, deviceUID: nil, clockOut: &audioClock) + } + + + var audioTimebase: CMTimebase? + if let audioClock = audioClock { + CMTimebaseCreateWithMasterClock(allocator: nil, masterClock: audioClock, timebaseOut: &audioTimebase) + } + + + if audioTimebase == nil { + CMTimebaseCreateWithMasterClock(allocator: nil, masterClock: CMClockGetHostTimeClock(), timebaseOut: &audioTimebase) + } + + let timebase = audioTimebase! + + + // AudioAddress.outputDevice + + self.audioTimebase = timebase + CMTimebaseSetRate(self.audioTimebase, rate: baseRate) + audioPlayerRendererQueue.async { + let context = AudioPlayerRendererContext(controlTimebase: timebase, playAndRecord: playAndRecord, forceAudioToSpeaker: forceAudioToSpeaker, baseRate: baseRate, volume: volume, updatedRate: updatedRate, audioPaused: audioPaused) + self.contextRef = Unmanaged.passRetained(context) + context.setVolume(volume) + } + + AudioObjectAddPropertyListener(AudioObjectID(kAudioObjectSystemObject), &AudioAddress.outputDevice, AudioListener.output, nil) + NotificationCenter.default.addObserver(self, selector: #selector(handleNotification(_:)), name: AudioNotification.audioOutputDeviceDidChange.notificationName, object: nil) + } + + @objc private func handleNotification(_ notification: Notification) { + + audioPlayerRendererQueue.async { + + let context = self.contextRef!.takeRetainedValue() + + let newContext = AudioPlayerRendererContext(controlTimebase: context.controlTimebase, playAndRecord: false, forceAudioToSpeaker: false, baseRate: context.baseRate, volume: context.volume, updatedRate: context.updatedRate, audioPaused: context.audioPaused) + + self.contextRef = Unmanaged.passRetained(newContext) + } + } + + deinit { + NotificationCenter.default.removeObserver(self) + AudioObjectRemovePropertyListener(AudioObjectID(kAudioObjectSystemObject), &AudioAddress.outputDevice, AudioListener.output, nil) + let contextRef = self.contextRef + audioPlayerRendererQueue.async { + contextRef?.release() + } + } + + func start() { + audioPlayerRendererQueue.async { + if let contextRef = self.contextRef { + let context = contextRef.takeUnretainedValue() + context.start() + } + } + } + + func stop() { + audioPlayerRendererQueue.async { + if let contextRef = self.contextRef { + let context = contextRef.takeUnretainedValue() + context.stop() + } + } + } + + func volume(_ completion: @escaping (Float) -> Void) { + audioPlayerRendererQueue.async { + if let contextRef = self.contextRef { + let context = contextRef.takeUnretainedValue() + completion(context.volume) + } + } + } + + func setVolume(_ volume: Float) { + audioPlayerRendererQueue.async { + if let contextRef = self.contextRef { + let context = contextRef.takeUnretainedValue() + context.setVolume(volume) + } + } + } + + func setRate(_ rate: Double) { + audioPlayerRendererQueue.async { + if let contextRef = self.contextRef { + let context = contextRef.takeUnretainedValue() + context.setRate(rate) + } + } + } + + func setBaseRate(_ baseRate: Double) { + audioPlayerRendererQueue.async { + if let contextRef = self.contextRef { + let context = contextRef.takeUnretainedValue() + context.setBaseRate(baseRate) + } + } + } + + func beginRequestingFrames(queue: DispatchQueue, takeFrame: @escaping () -> MediaTrackFrameResult) { + audioPlayerRendererQueue.async { + if let contextRef = self.contextRef { + let context = contextRef.takeUnretainedValue() + context.beginRequestingFrames(queue: queue, takeFrame: takeFrame) + } + } + } + + func flushBuffers(at timestamp: CMTime, completion: @escaping () -> Void) { + audioPlayerRendererQueue.async { + if let contextRef = self.contextRef { + let context = contextRef.takeUnretainedValue() + context.flushBuffers(at: timestamp, completion: completion) + } + } + } + + func setForceAudioToSpeaker(_ value: Bool) { + audioPlayerRendererQueue.async { + if let contextRef = self.contextRef { + let context = contextRef.takeUnretainedValue() + context.forceAudioToSpeaker = value + } + } + } +} diff --git a/Telegram-Mac/MediaPlayerFramePreview.swift b/Telegram-Mac/MediaPlayerFramePreview.swift new file mode 100644 index 0000000000..413dfda176 --- /dev/null +++ b/Telegram-Mac/MediaPlayerFramePreview.swift @@ -0,0 +1,144 @@ +import Foundation +import SwiftSignalKit +import Postbox +import TelegramCore + + + +private final class FramePreviewContext { + let source: UniversalSoftwareVideoSource + + init(source: UniversalSoftwareVideoSource) { + self.source = source + } +} + +private func initializedPreviewContext(queue: Queue, postbox: Postbox, fileReference: FileMediaReference) -> Signal, NoError> { + return Signal { subscriber in + let source = UniversalSoftwareVideoSource(mediaBox: postbox.mediaBox, fileReference: fileReference) + let readyDisposable = (source.ready + |> filter { $0 }).start(next: { _ in + subscriber.putNext(QueueLocalObject(queue: queue, generate: { + return FramePreviewContext(source: source) + })) + }) + + return ActionDisposable { + readyDisposable.dispose() + } + } +} + +public enum MediaPlayerFramePreviewResult { + case image(CGImage) + case waitingForData +} + +private final class MediaPlayerFramePreviewImpl { + private let queue: Queue + private let context: Promise> + private let currentFrameDisposable = MetaDisposable() + private var currentFrameTimestamp: Double? + private var nextFrameTimestamp: Double? + fileprivate let framePipe = ValuePipe() + + init(queue: Queue, postbox: Postbox, fileReference: FileMediaReference) { + self.queue = queue + self.context = Promise() + self.context.set(initializedPreviewContext(queue: queue, postbox: postbox, fileReference: fileReference)) + } + + deinit { + assert(self.queue.isCurrent()) + self.currentFrameDisposable.dispose() + } + + func generateFrame(at timestamp: Double) { + if self.currentFrameTimestamp != nil { + self.nextFrameTimestamp = timestamp + return + } + self.currentFrameTimestamp = timestamp + let queue = self.queue + let takeDisposable = MetaDisposable() + let disposable = (self.context.get() + |> take(1)).start(next: { [weak self] context in + queue.justDispatch { + guard context.queue === queue else { + return + } + context.with { context in + let disposable = (context.source.takeFrame(at: timestamp)).start(next: { result in + queue.async { + guard let strongSelf = self else { + return + } + switch result { + case .waitingForData: + strongSelf.framePipe.putNext(.waitingForData) + case let .image(image): + if let image = image { + strongSelf.framePipe.putNext(.image(image)) + } + strongSelf.currentFrameTimestamp = nil + if let nextFrameTimestamp = strongSelf.nextFrameTimestamp { + strongSelf.nextFrameTimestamp = nil + strongSelf.generateFrame(at: nextFrameTimestamp) + } + } + } + + }) + takeDisposable.set(disposable) + } + } + }) + self.currentFrameDisposable.set(ActionDisposable { + takeDisposable.dispose() + disposable.dispose() + }) + } + + func cancelPendingFrames() { + self.nextFrameTimestamp = nil + self.currentFrameTimestamp = nil + self.currentFrameDisposable.set(nil) + } +} + +public final class MediaPlayerFramePreview { + private let queue: Queue + private let impl: QueueLocalObject + + public var generatedFrames: Signal { + return Signal { subscriber in + let disposable = MetaDisposable() + self.impl.with { impl in + disposable.set(impl.framePipe.signal().start(next: { result in + subscriber.putNext(result) + })) + } + return disposable + } + } + + public init(postbox: Postbox, fileReference: FileMediaReference) { + let queue = Queue() + self.queue = queue + self.impl = QueueLocalObject(queue: queue, generate: { + return MediaPlayerFramePreviewImpl(queue: queue, postbox: postbox, fileReference: fileReference) + }) + } + + public func generateFrame(at timestamp: Double) { + self.impl.with { impl in + impl.generateFrame(at: timestamp) + } + } + + public func cancelPendingFrames() { + self.impl.with { impl in + impl.cancelPendingFrames() + } + } +} diff --git a/Telegram-Mac/MediaPlayerView.swift b/Telegram-Mac/MediaPlayerView.swift new file mode 100644 index 0000000000..dbfef3bcde --- /dev/null +++ b/Telegram-Mac/MediaPlayerView.swift @@ -0,0 +1,389 @@ +// +// MediaPlayerView.swift +// Telegram +// +// Created by Mikhail Filimonov on 12/11/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit +import AVFoundation + +private func findContentsLayer(_ sublayers: [CALayer]) -> CALayer? { + for sublayer in sublayers { + if let _ = sublayer.contents { + return sublayer + } else if let sublayers = sublayer.sublayers, !sublayers.isEmpty { + return findContentsLayer(sublayers) + } + } + return nil +} + +private final class MediaPlayerViewLayer: AVSampleBufferDisplayLayer { + override func action(forKey event: String) -> CAAction? { + return NSNull() + } +} + +private final class MediaPlayerViewDisplayView: View { + var updateInHierarchy: ((Bool) -> Void)? + + + override func viewDidMoveToSuperview() { + super.viewDidMoveToSuperview() + self.updateInHierarchy?(superview != nil) + } +} + +private enum PollStatus: CustomStringConvertible { + case delay(Double) + case finished + + var description: String { + switch self { + case let .delay(value): + return "delay(\(value))" + case .finished: + return "finished" + } + } +} + +final class MediaPlayerView: View { + var videoInHierarchy: Bool = false + var updateVideoInHierarchy: ((Bool) -> Void)? + + private var videoNode: MediaPlayerViewDisplayView + + private var videoLayer: AVSampleBufferDisplayLayer? + + private let videoQueue: Queue + + + func setVideoLayerGravity(_ gravity: AVLayerVideoGravity) { + videoLayer?.videoGravity = gravity + } + + var takeFrameAndQueue: (Queue, () -> MediaTrackFrameResult)? + var timer: SwiftSignalKit.Timer? + var polling = false + + var currentRotationAngle = 0.0 + var currentAspect = 1.0 + + var state: (timebase: CMTimebase, requestFrames: Bool, rotationAngle: Double, aspect: Double)? { + didSet { + self.updateState() + } + } + + private let maskLayer = CAShapeLayer() + + var positionFlags: LayoutPositionFlags? { + didSet { + if let positionFlags = positionFlags { + let path = CGMutablePath() + + let minx:CGFloat = 0, midx = frame.width/2.0, maxx = frame.width + let miny:CGFloat = 0, midy = frame.height/2.0, maxy = frame.height + + path.move(to: NSMakePoint(minx, midy)) + + var topLeftRadius: CGFloat = .cornerRadius + var bottomLeftRadius: CGFloat = .cornerRadius + var topRightRadius: CGFloat = .cornerRadius + var bottomRightRadius: CGFloat = .cornerRadius + + + if positionFlags.contains(.bottom) && positionFlags.contains(.left) { + topLeftRadius = topLeftRadius * 3 + 2 + } + if positionFlags.contains(.bottom) && positionFlags.contains(.right) { + topRightRadius = topRightRadius * 3 + 2 + } + if positionFlags.contains(.top) && positionFlags.contains(.left) { + bottomLeftRadius = bottomLeftRadius * 3 + 2 + } + if positionFlags.contains(.top) && positionFlags.contains(.right) { + bottomRightRadius = bottomRightRadius * 3 + 2 + } + + path.addArc(tangent1End: NSMakePoint(minx, miny), tangent2End: NSMakePoint(midx, miny), radius: bottomLeftRadius) + path.addArc(tangent1End: NSMakePoint(maxx, miny), tangent2End: NSMakePoint(maxx, midy), radius: bottomRightRadius) + path.addArc(tangent1End: NSMakePoint(maxx, maxy), tangent2End: NSMakePoint(midx, maxy), radius: topRightRadius) + path.addArc(tangent1End: NSMakePoint(minx, maxy), tangent2End: NSMakePoint(minx, midy), radius: topLeftRadius) + + maskLayer.path = path + layer?.mask = maskLayer + } else { + layer?.mask = nil + } + } + } + + private func updateState() { + if let (timebase, requestFrames, rotationAngle, aspect) = self.state { + if let videoLayer = self.videoLayer { + videoQueue.async { + if videoLayer.controlTimebase !== timebase || videoLayer.status == .failed { + videoLayer.flush() + videoLayer.controlTimebase = timebase + } + } + + if !self.currentRotationAngle.isEqual(to: rotationAngle) || !self.currentAspect.isEqual(to: aspect) { + self.currentRotationAngle = rotationAngle + self.currentAspect = aspect + var transform = CGAffineTransform(rotationAngle: CGFloat(rotationAngle)) + if !rotationAngle.isZero { + transform = transform.scaledBy(x: CGFloat(aspect), y: CGFloat(1.0 / aspect)) + } + videoLayer.setAffineTransform(transform) + } + + if self.videoInHierarchy { + if requestFrames { + self.startPolling() + } + } + } + } + } + + private func startPolling() { + if !self.polling { + self.polling = true + self.poll(completion: { [weak self] status in + self?.polling = false + + if let strongSelf = self, let (_, requestFrames, _, _) = strongSelf.state, requestFrames { + strongSelf.timer?.invalidate() + switch status { + case let .delay(delay): + strongSelf.timer = SwiftSignalKit.Timer(timeout: delay, repeat: true, completion: { + if let strongSelf = self, let videoLayer = strongSelf.videoLayer, let (_, requestFrames, _, _) = strongSelf.state, requestFrames, strongSelf.videoInHierarchy { + if videoLayer.isReadyForMoreMediaData { + strongSelf.timer?.invalidate() + strongSelf.timer = nil + strongSelf.startPolling() + } + } + }, queue: Queue.mainQueue()) + strongSelf.timer?.start() + case .finished: + break + } + } + }) + } + } + + private func poll(completion: @escaping (PollStatus) -> Void) { + if let (takeFrameQueue, takeFrame) = self.takeFrameAndQueue, let videoLayer = self.videoLayer, let (timebase, _, _, _) = self.state { + let layerRef = Unmanaged.passRetained(videoLayer) + takeFrameQueue.async { + let status: PollStatus + do { + var numFrames = 0 + let layer = layerRef.takeUnretainedValue() + let layerTime = CMTimeGetSeconds(CMTimebaseGetTime(timebase)) + var maxTakenTime = layerTime + 0.1 + var finised = false + loop: while true { + let isReady = layer.isReadyForMoreMediaData + + if isReady { + switch takeFrame() { + case let .restoreState(frames, atTime): + layer.flush() + for i in 0 ..< frames.count { + let frame = frames[i] + let frameTime = CMTimeGetSeconds(frame.position) + maxTakenTime = frameTime + let attachments = CMSampleBufferGetSampleAttachmentsArray(frame.sampleBuffer, createIfNecessary: true)! as NSArray + let dict = attachments[0] as! NSMutableDictionary + if i == 0 { + CMSetAttachment(frame.sampleBuffer, key: kCMSampleBufferAttachmentKey_ResetDecoderBeforeDecoding as NSString, value: kCFBooleanTrue as AnyObject, attachmentMode: kCMAttachmentMode_ShouldPropagate) + CMSetAttachment(frame.sampleBuffer, key: kCMSampleBufferAttachmentKey_EndsPreviousSampleDuration as NSString, value: kCFBooleanTrue as AnyObject, attachmentMode: kCMAttachmentMode_ShouldPropagate) + } + if CMTimeCompare(frame.position, atTime) < 0 { + dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleAttachmentKey_DoNotDisplay as NSString as String) + } else if CMTimeCompare(frame.position, atTime) == 0 { + dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleAttachmentKey_DisplayImmediately as NSString as String) + dict.setValue(kCFBooleanTrue as AnyObject, forKey: kCMSampleBufferAttachmentKey_EndsPreviousSampleDuration as NSString as String) + //print("restore state to \(frame.position) -> \(frameTime) at \(layerTime) (\(i + 1) of \(frames.count))") + } + layer.enqueue(frame.sampleBuffer) + } + + case let .frame(frame): + numFrames += 1 + let frameTime = CMTimeGetSeconds(frame.position) + if frame.resetDecoder { + layer.flush() + } + + if frame.decoded && frameTime < layerTime { + continue loop + } + + //print("took frame at \(frameTime) current \(layerTime)") + maxTakenTime = frameTime + layer.enqueue(frame.sampleBuffer) + + + case .skipFrame: + break + case .noFrames: + finised = true + break loop + case .finished: + finised = true + break loop + } + } else { + break loop + } + } + if finised { + status = .finished + } else { + status = .delay(max(1.0 / 30.0, maxTakenTime - layerTime)) + } + //print("took \(numFrames) frames, status \(status)") + } + DispatchQueue.main.async { + layerRef.release() + + completion(status) + } + } + } + } + + var transformArguments: TransformImageArguments? { + didSet { + self.updateLayout() + } + } + + init(backgroundThread: Bool = false) { + self.videoNode = MediaPlayerViewDisplayView() + + if false && backgroundThread { + self.videoQueue = Queue() + } else { + self.videoQueue = Queue.mainQueue() + } + + super.init() + + self.videoNode.updateInHierarchy = { [weak self] value in + if let strongSelf = self { + if strongSelf.videoInHierarchy != value { + strongSelf.videoInHierarchy = value + if value { + strongSelf.updateState() + } + } + strongSelf.updateVideoInHierarchy?(value) + } + } + self.addSubview(self.videoNode) + + self.videoQueue.async { [weak self] in + let videoLayer = MediaPlayerViewLayer() + videoLayer.videoGravity = .resize + + #if arch(x86_64) + if let sublayers = videoLayer.sublayers { + findContentsLayer(sublayers)?.minificationFilter = .trilinear + } + #endif + + + //videoLayer.sublayers?.first?.magnificationFilter = .nearest + Queue.mainQueue().async { + if let strongSelf = self { + strongSelf.videoLayer = videoLayer + strongSelf.updateLayout() + strongSelf.layer?.addSublayer(videoLayer) + strongSelf.updateState() + } + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + deinit { + assert(Queue.mainQueue().isCurrent()) + self.videoLayer?.removeFromSuperlayer() + + if let (takeFrameQueue, _) = self.takeFrameAndQueue { + if let videoLayer = self.videoLayer { + takeFrameQueue.async { + videoLayer.flushAndRemoveImage() + + takeFrameQueue.after(1.0, { + videoLayer.flushAndRemoveImage() + }) + } + } + } + } + + override var frame: CGRect { + didSet { + if !oldValue.size.equalTo(self.frame.size) { + self.updateLayout() + } + } + } + + override func setFrameSize(_ newSize: NSSize) { + let oldValue = self.frame + super.setFrameSize(newSize) + if !oldValue.size.equalTo(self.frame.size) { + self.updateLayout() + } + } + + func updateLayout() { + let bounds = self.bounds + + let fittedRect: CGRect + if let arguments = self.transformArguments { + let drawingRect = bounds + var fittedSize = arguments.imageSize + if abs(fittedSize.width - bounds.size.width).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.width = bounds.size.width + } + if abs(fittedSize.height - bounds.size.height).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.height = bounds.size.height + } + + fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + } else { + fittedRect = bounds + } + + if let videoLayer = self.videoLayer { + videoLayer.position = CGPoint(x: fittedRect.midX, y: fittedRect.midY) + videoLayer.bounds = CGRect(origin: CGPoint(), size: fittedRect.size) + } + } + + func reset() { + self.videoLayer?.flush() + } +} diff --git a/Telegram-Mac/MediaPreviewEditControl.swift b/Telegram-Mac/MediaPreviewEditControl.swift new file mode 100644 index 0000000000..d114ba61dd --- /dev/null +++ b/Telegram-Mac/MediaPreviewEditControl.swift @@ -0,0 +1,108 @@ +// +// MediaPreviewEditControl.swift +// Telegram +// +// Created by Mikhail Filimonov on 09/10/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + + + +class MediaPreviewEditControl: View { + private let crop = ImageButton() + private let paint = ImageButton() + private let delete = ImageButton() + override init() { + super.init(frame: NSMakeRect(0, 0, 90, 30)) + addSubview(crop) + addSubview(paint) + addSubview(delete) + crop.autohighlight = false + paint.autohighlight = false + delete.autohighlight = false + crop.scaleOnClick = true + paint.scaleOnClick = true + delete.scaleOnClick = true + crop.set(image: theme.icons.editor_crop, for: .Normal) + paint.set(image: theme.icons.editor_draw, for: .Normal) + delete.set(image: theme.icons.editor_delete, for: .Normal) + _ = crop.sizeToFit(NSZeroSize, NSMakeSize(27, frame.height), thatFit: true) + _ = delete.sizeToFit(NSZeroSize, NSMakeSize(27, frame.height), thatFit: true) + _ = paint.sizeToFit(NSZeroSize, NSMakeSize(27, frame.height), thatFit: true) + + backgroundColor = .blackTransparent + layer?.cornerRadius = frame.height / 2 + } + + func set(edit:@escaping()->Void, paint: @escaping()->Void, delete:@escaping()->Void, editedData: EditedImageData?) { + self.crop.removeAllHandlers() + self.delete.removeAllHandlers() + self.paint.removeAllHandlers() + + + +// self.crop.isSelected = editedData != nil +// self.paint.isSelected = !(editedData?.paintings.isEmpty ?? true) + + self.crop.set(handler: { _ in + edit() + }, for: .Click) + + self.paint.set(handler: { _ in + paint() + }, for: .Click) + + self.delete.set(handler: { _ in + delete() + }, for: .Click) + } + + var canEdit: Bool = true { + didSet { + crop.isHidden = !canEdit + paint.isHidden = !canEdit + self.setFrameSize(canEdit ? 90 : 30, frame.height) + } + } + var canDelete: Bool = true { + didSet { + delete.isHidden = !canDelete + } + } + + var isInteractiveMedia: Bool = true { + didSet { + backgroundColor = isInteractiveMedia ? .blackTransparent : .clear + delete.set(image: isInteractiveMedia ? theme.icons.editor_delete : theme.icons.previewSenderDeleteFile, for: .Normal) + } + } + + override func layout() { + super.layout() + if canEdit { + crop.centerY(x: 3) + paint.centerY(x: crop.frame.maxX) + delete.centerY(x: paint.frame.maxX) + } else { + delete.center() + } + } + + override var isHidden: Bool { + didSet { + var bp:Int = 0 + bp += 1 + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } +} diff --git a/Telegram-Mac/MediaPreviewRowItem.swift b/Telegram-Mac/MediaPreviewRowItem.swift new file mode 100644 index 0000000000..ac00d912df --- /dev/null +++ b/Telegram-Mac/MediaPreviewRowItem.swift @@ -0,0 +1,243 @@ +// +// MediaPreviewRowItem.swift +// Telegram +// +// Created by keepcoder on 19/10/2017. +// Copyright © 2017 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import TGUIKit +import SwiftSignalKit +import Postbox + +class MediaPreviewRowItem: TableRowItem { + + + + + let media: Media + fileprivate let context: AccountContext + private let _stableId = arc4random() + fileprivate let parameters: ChatMediaLayoutParameters? + fileprivate let chatInteraction: ChatInteraction + fileprivate let edit:()->Void + fileprivate let paint:()->Void + fileprivate let delete: (()->Void)? + fileprivate let editedData: EditedImageData? + init(_ initialSize: NSSize, media: Media, context: AccountContext, editedData: EditedImageData? = nil, edit:@escaping()->Void = {}, paint:@escaping()->Void = {}, delete: (()->Void)? = nil) { + self.edit = edit + self.paint = paint + self.delete = delete + self.media = media + self.context = context + self.editedData = editedData + self.chatInteraction = ChatInteraction(chatLocation: .peer(PeerId(0)), context: context) + if let media = media as? TelegramMediaFile { + parameters = ChatMediaLayoutParameters.layout(for: media, isWebpage: false, chatInteraction: chatInteraction, presentation: .Empty, automaticDownload: true, isIncoming: false, autoplayMedia: AutoplayMediaPreferences.defaultSettings) + } else { + parameters = nil + } + super.init(initialSize) + _ = makeSize(initialSize.width, oldWidth: 0) + } + + + private var overSize: CGFloat? = nil + override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { + let result = super.makeSize(width, oldWidth: oldWidth) + parameters?.makeLabelsForWidth(width - (media.isInteractiveMedia ? 20 : 120)) + + if let table = table, table.count == 1 { + if contentSize.height > table.frame.height && table.frame.height > 0 { + overSize = table.frame.height - 12 + } else { + overSize = nil + } + } + + return result + } + + override var stableId: AnyHashable { + return _stableId + } + + override var identifier: String { + return "\(ChatLayoutUtils.contentNode(for: media))" + } + + override var height: CGFloat { + return contentSize.height + 12 + } + + var contentSize: NSSize { + let contentSize = layoutSize + + let width = self.width - (media.isInteractiveMedia ? 20 : 40) + + var height: CGFloat = overSize ?? contentSize.height + if let media = media as? TelegramMediaFile { + if media.isSticker || media.isAnimatedSticker { + height = width + } + } + return NSMakeSize(width, height) + } + + override var layoutSize: NSSize { + return ChatLayoutUtils.contentSize(for: media, with: initialSize.width - (media.isInteractiveMedia ? 20 : 80)) + } + + public func contentNode() -> ChatMediaContentView.Type { + return ChatLayoutUtils.contentNode(for: media) + } + + override func viewClass() -> AnyClass { + return MediaPreviewRowView.self + } +} + +fileprivate class MediaPreviewRowView : TableRowView, ModalPreviewRowViewProtocol { + + func fileAtPoint(_ point: NSPoint) -> (QuickPreviewMedia, NSView?)? { + if let contentNode = contentNode { + if contentNode is ChatGIFContentView { + if let file = contentNode.media as? TelegramMediaFile { + let reference = contentNode.parent != nil ? FileMediaReference.message(message: MessageReference(contentNode.parent!), media: file) : FileMediaReference.standalone(media: file) + return (.file(reference, GifPreviewModalView.self), contentNode) + } + } else if contentNode is ChatInteractiveContentView { + if let image = contentNode.media as? TelegramMediaImage { + let reference = contentNode.parent != nil ? ImageMediaReference.message(message: MessageReference(contentNode.parent!), media: image) : ImageMediaReference.standalone(media: image) + return (.image(reference, ImagePreviewModalView.self), contentNode) + } + } else if contentNode is MediaAnimatedStickerView { + if let file = contentNode.media as? TelegramMediaFile { + let reference = contentNode.parent != nil ? FileMediaReference.message(message: MessageReference(contentNode.parent!), media: file) : FileMediaReference.standalone(media: file) + return (.file(reference, AnimatedStickerPreviewModalView.self), contentNode) + } + } else if contentNode is ChatFileContentView { + if let file = contentNode.media as? TelegramMediaFile, file.isGraphicFile, let mediaId = file.id, let dimension = file.dimensions { + var representations: [TelegramMediaImageRepresentation] = [] + representations.append(contentsOf: file.previewRepresentations) + representations.append(TelegramMediaImageRepresentation(dimensions: dimension, resource: file.resource, progressiveSizes: [], immediateThumbnailData: nil)) + let image = TelegramMediaImage(imageId: mediaId, representations: representations, immediateThumbnailData: file.immediateThumbnailData, reference: nil, partialReference: file.partialReference, flags: []) + let reference = contentNode.parent != nil ? ImageMediaReference.message(message: MessageReference(contentNode.parent!), media: image) : ImageMediaReference.standalone(media: image) + return (.image(reference, ImagePreviewModalView.self), contentNode) + } + } + } + + return nil + } + + var contentNode:ChatMediaContentView? + let editControl: MediaPreviewEditControl = MediaPreviewEditControl() + override var needsDisplay: Bool { + get { + return super.needsDisplay + } + set { + super.needsDisplay = true + contentNode?.needsDisplay = true + } + } + + override func forceClick(in location: NSPoint) { + _ = contentNode?.previewMediaIfPossible() + } + + override func draw(_ dirtyRect: NSRect) { + + } + + override func updateMouse() { + guard let window = window, let table = item?.table else { + editControl.isHidden = true + return + } + + let row = table.row(at: table.documentView!.convert(window.mouseLocationOutsideOfEventStream, from: nil)) + + if row == item?.index { + editControl.isHidden = false + } else { + editControl.isHidden = true + } + } + + override func shakeView() { + contentNode?.shake() + } + + + override func set(item:TableRowItem, animated:Bool = false) { + super.set(item: item, animated: animated) + guard let item = item as? MediaPreviewRowItem else { return } + + if contentNode == nil || !contentNode!.isKind(of: item.contentNode()) { + self.contentNode?.removeFromSuperview() + let node = item.contentNode() + self.contentNode = node.init(frame:NSZeroRect) + self.addSubview(self.contentNode!) + addSubview(editControl) + updateMouse() + } + + + editControl.canEdit = (item.media is TelegramMediaImage) + editControl.isInteractiveMedia = item.media.isInteractiveMedia + editControl.canDelete = item.delete != nil + editControl.set(edit: { [weak item] in + item?.edit() + }, paint: { [weak item] in + item?.paint() + }, delete: { [weak item] in + item?.delete?() + }, editedData: item.editedData) + + self.contentNode?.update(with: item.media, size: item.contentSize, context: item.context, parent: nil, table: item.table, parameters: item.parameters, animated: animated) + + } + + override func layout() { + super.layout() + guard let contentNode = contentNode else {return} + contentNode.setFrameOrigin(12, 6) + if editControl.isInteractiveMedia { + editControl.setFrameOrigin(NSMakePoint(frame.width - editControl.frame.width - 20, frame.height - editControl.frame.height - 20)) + } else { + editControl.centerY(x: frame.width - editControl.frame.width - 10) + } + } + + open override func interactionContentView(for innerId: AnyHashable, animateIn: Bool ) -> NSView { + if let content = self.contentNode?.interactionContentView(for: innerId, animateIn: animateIn) { + return content + } + return self + } + + + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + } + + override var backgroundColor: NSColor { + didSet { + contentNode?.backgroundColor = backdorColor + } + } + + override func viewWillMove(toSuperview newSuperview: NSView?) { + if newSuperview == nil { + self.contentNode?.willRemove() + } + } + +} + + diff --git a/Telegram-Mac/MediaResources.swift b/Telegram-Mac/MediaResources.swift index 59eb958d5a..f329f759e0 100644 --- a/Telegram-Mac/MediaResources.swift +++ b/Telegram-Mac/MediaResources.swift @@ -8,8 +8,9 @@ import Cocoa -import PostboxMac -import TelegramCoreMac +import Postbox +import TelegramCore + public final class VideoMediaResourceAdjustments: PostboxCoding, Equatable { let data: MemoryBuffer @@ -91,7 +92,7 @@ public final class VideoLibraryMediaResource: TelegramMediaResource { return VideoLibraryMediaResourceId(localIdentifier: self.localIdentifier, adjustmentsDigest: self.adjustments?.digest) } - public func isEqual(to: TelegramMediaResource) -> Bool { + public func isEqual(to: MediaResource) -> Bool { if let to = to as? VideoLibraryMediaResource { return self.localIdentifier == to.localIdentifier && self.adjustments == to.adjustments } else { @@ -120,7 +121,18 @@ public struct LocalFileGifMediaResourceId: MediaResourceId { } } + + public final class LocalFileGifMediaResource: TelegramMediaResource { + + public func isEqual(to: MediaResource) -> Bool { + if let to = to as? LocalFileGifMediaResource { + return self.randomId == to.randomId && self.path == to.path + } else { + return false + } + } + public let randomId: Int64 public let path: String @@ -143,11 +155,284 @@ public final class LocalFileGifMediaResource: TelegramMediaResource { return LocalFileGifMediaResourceId(randomId: self.randomId) } - public func isEqual(to: TelegramMediaResource) -> Bool { - if let to = to as? LocalFileGifMediaResource { +} + +public struct LocalFileVideoMediaResourceId: MediaResourceId { + public let randomId: Int64 + + public var uniqueId: String { + return "lmov-\(self.randomId)" + } + + public var hashValue: Int { + return self.randomId.hashValue + } + + public func isEqual(to: MediaResourceId) -> Bool { + if let to = to as? LocalFileVideoMediaResourceId { + return self.randomId == to.randomId + } else { + return false + } + } +} + +public final class LocalFileVideoMediaResource: TelegramMediaResource { + + public func isEqual(to: MediaResource) -> Bool { + if let to = to as? LocalFileVideoMediaResource { + return self.randomId == to.randomId && self.path == to.path + } else { + return false + } + } + + public let randomId: Int64 + public let path: String + + public init(randomId: Int64, path: String) { + self.randomId = randomId + self.path = path + } + + public required init(decoder: PostboxDecoder) { + self.randomId = decoder.decodeInt64ForKey("i", orElse: 0) + self.path = decoder.decodeStringForKey("p", orElse: "") + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt64(self.randomId, forKey: "i") + encoder.encodeString(self.path, forKey: "p") + } + + public var id: MediaResourceId { + return LocalFileVideoMediaResourceId(randomId: self.randomId) + } + +} + +public struct LottieSoundMediaResourceId: MediaResourceId { + public let randomId: Int64 + + public var uniqueId: String { + return "lottie-sound-\(self.randomId)" + } + + public var hashValue: Int { + return self.randomId.hashValue + } + + public func isEqual(to: MediaResourceId) -> Bool { + if let to = to as? LottieSoundMediaResourceId { + return self.randomId == to.randomId + } else { + return false + } + } +} + +public final class LottieSoundMediaResource: TelegramMediaResource { + + public func isEqual(to: MediaResource) -> Bool { + if let to = to as? LottieSoundMediaResource { + return self.randomId == to.randomId && self.data == to.data + } else { + return false + } + } + + public let randomId: Int64 + public let data: Data + + public init(randomId: Int64, data: Data) { + self.randomId = randomId + self.data = data + } + + public required init(decoder: PostboxDecoder) { + self.randomId = decoder.decodeInt64ForKey("i", orElse: 0) + self.data = decoder.decodeDataForKey("d") ?? Data() + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt64(self.randomId, forKey: "i") + encoder.encodeData(self.data, forKey: "d") + } + + public var id: MediaResourceId { + return LottieSoundMediaResourceId(randomId: self.randomId) + } + +} + + + +public struct LocalFileArchiveMediaResourceId: MediaResourceId { + public let randomId: Int64 + + public var uniqueId: String { + return "larchive-\(self.randomId)" + } + + public var hashValue: Int { + return self.randomId.hashValue + } + + public func isEqual(to: MediaResourceId) -> Bool { + if let to = to as? LocalFileArchiveMediaResourceId { + return self.randomId == to.randomId + } else { + return false + } + } +} + +public final class LocalFileArchiveMediaResource: TelegramMediaResource { + public let randomId: Int64 + public let path: String + + public var headerSize: Int32 { + return 32 * 1024 + } + + public init(randomId: Int64, path: String) { + self.randomId = randomId + self.path = path + } + + public required init(decoder: PostboxDecoder) { + self.randomId = decoder.decodeInt64ForKey("i", orElse: 0) + self.path = decoder.decodeStringForKey("p", orElse: "") + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt64(self.randomId, forKey: "i") + encoder.encodeString(self.path, forKey: "p") + } + + public var id: MediaResourceId { + return LocalFileArchiveMediaResourceId(randomId: self.randomId) + } + + public func isEqual(to: MediaResource) -> Bool { + if let to = to as? LocalFileArchiveMediaResource { return self.randomId == to.randomId && self.path == to.path } else { return false } } } + + +public struct ExternalMusicAlbumArtResourceId: MediaResourceId { + public let title: String + public let performer: String + public let isThumbnail: Bool + + public var uniqueId: String { + return "ext-album-art-\(isThumbnail ? "thump" : "full")-\(self.title.replacingOccurrences(of: "/", with: "_"))-\(self.performer.replacingOccurrences(of: "/", with: "_"))" + } + + public var hashValue: Int { + return self.title.hashValue &* 31 &+ self.performer.hashValue + } + + public func isEqual(to: MediaResourceId) -> Bool { + if let to = to as? ExternalMusicAlbumArtResourceId { + return self.title == to.title && self.performer == to.performer && self.isThumbnail == to.isThumbnail + } else { + return false + } + } +} + + +public class ExternalMusicAlbumArtResource: TelegramMediaResource { + public let title: String + public let performer: String + public let isThumbnail: Bool + + public init(title: String, performer: String, isThumbnail: Bool) { + self.title = title + self.performer = performer + self.isThumbnail = isThumbnail + } + + public required init(decoder: PostboxDecoder) { + self.title = decoder.decodeStringForKey("t", orElse: "") + self.performer = decoder.decodeStringForKey("p", orElse: "") + self.isThumbnail = decoder.decodeInt32ForKey("th", orElse: 1) != 0 + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeString(self.title, forKey: "t") + encoder.encodeString(self.performer, forKey: "p") + encoder.encodeInt32(self.isThumbnail ? 1 : 0, forKey: "th") + } + + public var id: MediaResourceId { + return ExternalMusicAlbumArtResourceId(title: self.title, performer: self.performer, isThumbnail: self.isThumbnail) + } + + public func isEqual(to: MediaResource) -> Bool { + if let to = to as? ExternalMusicAlbumArtResource { + return self.title == to.title && self.performer == to.performer && self.isThumbnail == to.isThumbnail + } else { + return false + } + } +} + + +public struct LocalBundleResourceId: MediaResourceId { + public let name: String + public let ext: String + + public var uniqueId: String { + return "local-bundle-\(self.name)-\(self.ext)" + } + + public var hashValue: Int { + return self.name.hashValue + } + + public func isEqual(to: MediaResourceId) -> Bool { + if let to = to as? LocalBundleResourceId { + return self.name == to.name && self.ext == to.ext + } else { + return false + } + } +} + +public class LocalBundleResource: TelegramMediaResource { + public let name: String + public let ext: String + + public init(name: String, ext: String) { + self.name = name + self.ext = ext + } + + public required init(decoder: PostboxDecoder) { + self.name = decoder.decodeStringForKey("n", orElse: "") + self.ext = decoder.decodeStringForKey("e", orElse: "") + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeString(self.name, forKey: "n") + encoder.encodeString(self.ext, forKey: "e") + } + + public var id: MediaResourceId { + return LocalBundleResourceId(name: self.name, ext: self.ext) + } + + public func isEqual(to: MediaResource) -> Bool { + if let to = to as? LocalBundleResource { + return self.name == to.name && self.ext == to.ext + } else { + return false + } + } +} diff --git a/Telegram-Mac/MediaTitleBarView.swift b/Telegram-Mac/MediaTitleBarView.swift deleted file mode 100644 index 2a8c969567..0000000000 --- a/Telegram-Mac/MediaTitleBarView.swift +++ /dev/null @@ -1,114 +0,0 @@ -// -// MediaTitleBarView.swift -// Telegram-Mac -// -// Created by keepcoder on 14/10/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import TGUIKit - - -final class PeerMediaTypeInteraction { - var media:() -> Void = {} - var files:() -> Void = {} - var links:() -> Void = {} - var audio:() -> Void = {} - - init(media:@escaping() -> Void, files:@escaping() -> Void, links:@escaping() -> Void, audio:@escaping() -> Void) { - self.media = media - self.files = files - self.links = links - self.audio = audio - } -} - - - -class MediaTitleBarView: TitledBarView { - - private let button:TitleButton = TitleButton() - private let dropdownImage = ImageButton() - - public init(controller: ViewController, interactions:PeerMediaTypeInteraction) { - super.init(controller: controller) - - button.set(font: .medium(.title), for: .Normal) - - - button.highlightHovered = true - dropdownImage.highlightHovered = true - let showDropDown:(Control) -> Void = { [weak self] _ in - - if let strongSelf = self, !hasPopover(mainWindow) { - var items:[SPopoverItem] = [] - items.append(SPopoverItem(tr(.peerMediaPopoverSharedMedia), { [weak strongSelf] in - interactions.media() - strongSelf?.button.set(text: tr(.peerMediaSharedMedia), for: .Normal) - })) - items.append(SPopoverItem(tr(.peerMediaPopoverSharedFiles), { [weak strongSelf] in - interactions.files() - strongSelf?.button.set(text: tr(.peerMediaPopoverSharedFiles), for: .Normal) - })) - items.append(SPopoverItem(tr(.peerMediaPopoverSharedLinks), { [weak strongSelf] in - interactions.links() - strongSelf?.button.set(text: tr(.peerMediaPopoverSharedLinks), for: .Normal) - })) - items.append(SPopoverItem(tr(.peerMediaPopoverSharedAudio), { [weak strongSelf] in - interactions.audio() - strongSelf?.button.set(text: tr(.peerMediaPopoverSharedAudio), for: .Normal) - })) - - let controller = SPopoverViewController(items: items) - showPopover(for: strongSelf, with: controller, edge: .maxY, inset: NSMakePoint( floorToScreenPixels(strongSelf.frame.width / 2) - floorToScreenPixels(controller.frame.width/2),-50)) - - } - } - - self.set(handler: showDropDown, for: .Click) - - dropdownImage.userInteractionEnabled = false - button.userInteractionEnabled = false - - //dropdownImage.set(handler: showDropDown, for: .Click) - - set(handler: { [weak self] control in - - //self?.dropdownImage.layer?.animateRotateCenter(from: 0, to: 180, duration: 0.2, removeOnCompletion: false) - - }, for: .Highlight) - - button.sizeToFit() - addSubview(button) - - - addSubview(dropdownImage) - } - - override func updateLocalizationAndTheme() { - button.set(text: tr(.peerMediaSharedMedia), for: .Normal) - button.set(color: theme.colors.blueUI, for: .Normal) - dropdownImage.set(image: theme.icons.mediaDropdown, for: .Normal) - dropdownImage.sizeToFit() - needsLayout = true - } - - - override func layout() { - super.layout() - button.center() - dropdownImage.centerY(x: button.frame.maxX + 4) - dropdownImage.setFrameOrigin(dropdownImage.frame.minX, dropdownImage.frame.minY + 1) - - } - - required init(frame frameRect: NSRect) { - fatalError("init(frame:) has not been implemented") - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - -} diff --git a/Telegram-Mac/MediaUtils.swift b/Telegram-Mac/MediaUtils.swift index f9f1331c99..748cfc8645 100644 --- a/Telegram-Mac/MediaUtils.swift +++ b/Telegram-Mac/MediaUtils.swift @@ -7,19 +7,62 @@ // import Cocoa -import SwiftSignalKitMac -import TelegramCoreMac -import PostboxMac +import SwiftSignalKit +import TelegramCore + +import Postbox import TGUIKit import AVFoundation +import Accelerate + +public final class ImageDataTransformation { + let data: ImageRenderData + let execute:(TransformImageArguments, ImageRenderData)->DrawingContext? + init(data: ImageRenderData = ImageRenderData(nil, nil, false), execute:@escaping(TransformImageArguments, ImageRenderData)->DrawingContext? = { _, _ in return nil}) { + self.data = data + self.execute = execute + } +} +final class ImageRenderData { + let thumbnailData: Data? + let fullSizeData:Data? + let fullSizeComplete:Bool + init(_ thumbnailData: Data?, _ fullSizeData: Data?, _ fullSizeComplete: Bool) { + self.thumbnailData = thumbnailData + self.fullSizeData = fullSizeData + self.fullSizeComplete = fullSizeComplete + } +} -func chatMessageFileStatus(account: Account, file: TelegramMediaFile) -> Signal { - return account.postbox.mediaBox.resourceStatus(file.resource) +private let progressiveRangeMap: [(Int, [Int])] = [ + (100, [0]), + (400, [1]), + (400, [3]), + (600, [4]), + (Int(Int32.max), [2, 3, 4]) +] + +func chatMessageFileStatus(account: Account, file: TelegramMediaFile, approximateSynchronousValue: Bool = false, useVideoThumb: Bool = false) -> Signal { + if let _ = file.resource as? LocalFileReferenceMediaResource { + return .single(.Local) + } + if useVideoThumb, let videoThumb = file.videoThumbnails.first { + return combineLatest(account.postbox.mediaBox.resourceStatus(file.resource, approximateSynchronousValue: approximateSynchronousValue), account.postbox.mediaBox.resourceStatus(videoThumb.resource, approximateSynchronousValue: approximateSynchronousValue)) |> map { file, thumb in + switch thumb { + case .Local, .Fetching: + return thumb + default: + return file + } + } + // return account.postbox.mediaBox.resourceStatus(videoThumb.resource, approximateSynchronousValue: approximateSynchronousValue) + } + return account.postbox.mediaBox.resourceStatus(file.resource, approximateSynchronousValue: approximateSynchronousValue) } -func chatMessageFileInteractiveFetched(account: Account, file: TelegramMediaFile) -> Signal { - return account.postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .file), implNext: true) +func chatMessageFileInteractiveFetched(account: Account, fileReference: FileMediaReference) -> Signal { + return fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: fileReference.resourceReference(fileReference.media.resource), statsCategory: .file, reportResultStatus: true) |> `catch` { _ in return .complete() } //account.postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .file), implNext: true) } func chatMessageFileCancelInteractiveFetch(account: Account, file: TelegramMediaFile) { @@ -27,119 +70,287 @@ func chatMessageFileCancelInteractiveFetch(account: Account, file: TelegramMedia } func largestRepresentationForPhoto(_ photo: TelegramMediaImage) -> TelegramMediaImageRepresentation? { - return photo.representationForDisplayAtSize(NSMakeSize(1280.0, 1280.0)) + return photo.representationForDisplayAtSize(PixelDimensions(1280, 1280)) } func smallestImageRepresentation(_ representation:[TelegramMediaImageRepresentation]) -> TelegramMediaImageRepresentation? { return representation.first } +public func representationFetchRangeForDisplayAtSize(representation: TelegramMediaImageRepresentation, dimension: Int?) -> Range? { + if representation.progressiveSizes.count > 1, let dimension = dimension { + var largestByteSize = Int(representation.progressiveSizes[0]) + for (maxDimension, byteSizes) in progressiveRangeMap { + largestByteSize = Int(representation.progressiveSizes[min(representation.progressiveSizes.count - 1, byteSizes.last!)]) + if maxDimension >= dimension { + break + } + } + return 0 ..< largestByteSize + } + return nil +} -private func chatMessagePhotoDatas(account: Account, photo: TelegramMediaImage, fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false) -> Signal<(Data?, Data?, Bool), NoError> { - if let smallestRepresentation = smallestImageRepresentation(photo.representations), let largestRepresentation = photo.representationForDisplayAtSize(fullRepresentationSize) { - let maybeFullSize = account.postbox.mediaBox.resourceData(largestRepresentation.resource) +func chatMessagePhotoDatas(postbox: Postbox, imageReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false, tryAdditionalRepresentations: Bool = false, synchronousLoad: Bool = false, secureIdAccessContext: SecureIdAccessContext? = nil, peer: Peer? = nil, useMiniThumbnailIfAvailable: Bool = false) -> Signal { + + if let progressiveRepresentation = progressiveImageRepresentation(imageReference.media.representations), progressiveRepresentation.progressiveSizes.count > 1 { + enum SizeSource { + case miniThumbnail(data: Data) + case image(size: Int) + } - let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, Data?, Bool), NoError> in - if maybeData.complete { - let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) - - return .single((nil, loadedData, true)) - } else { - let fetchedThumbnail = account.postbox.mediaBox.fetchedResource(smallestRepresentation.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)) - let fetchedFullSize = account.postbox.mediaBox.fetchedResource(largestRepresentation.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)) - - let thumbnail = Signal { subscriber in - let fetchedDisposable = fetchedThumbnail.start() - let thumbnailDisposable = account.postbox.mediaBox.resourceData(smallestRepresentation.resource).start(next: { next in - subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) - }, error: subscriber.putError, completed: subscriber.putCompletion) - - return ActionDisposable { - fetchedDisposable.dispose() - thumbnailDisposable.dispose() + var sources: [SizeSource] = [] + if let miniThumbnail = imageReference.media.immediateThumbnailData.flatMap(decodeTinyThumbnail) { + sources.append(.miniThumbnail(data: miniThumbnail)) + } + let thumbnailByteSize = Int(progressiveRepresentation.progressiveSizes[0]) + var largestByteSize = Int(progressiveRepresentation.progressiveSizes[0]) + for (maxDimension, byteSizes) in progressiveRangeMap { + if Int(fullRepresentationSize.width) > 100 && maxDimension <= 100 { + continue + } + sources.append(contentsOf: byteSizes.compactMap { sizeIndex -> SizeSource? in + if progressiveRepresentation.progressiveSizes.count - 1 < sizeIndex { + return nil + } + return .image(size: Int(progressiveRepresentation.progressiveSizes[sizeIndex])) + }) + largestByteSize = Int(progressiveRepresentation.progressiveSizes[min(progressiveRepresentation.progressiveSizes.count - 1, byteSizes.last!)]) + if maxDimension >= Int(fullRepresentationSize.width) { + break + } + } + + + + return Signal { subscriber in + let signals: [Signal<(SizeSource, Data?), NoError>] = sources.map { source -> Signal<(SizeSource, Data?), NoError> in + switch source { + case let .miniThumbnail(data): + return .single((source, data)) + case let .image(size): + return postbox.mediaBox.resourceData(progressiveRepresentation.resource, size: Int(progressiveRepresentation.progressiveSizes.last!), in: 0 ..< size, mode: .incremental, notifyAboutIncomplete: true, attemptSynchronously: synchronousLoad) + |> map { (data, _) -> (SizeSource, Data?) in + return (source, data) } } - - let fullSizeData: Signal<(Data?, Bool), NoError> - - if autoFetchFullSize { - fullSizeData = Signal<(Data?, Bool), NoError> { subscriber in - let fetchedFullSizeDisposable = fetchedFullSize.start() - let fullSizeDisposable = account.postbox.mediaBox.resourceData(largestRepresentation.resource).start(next: { next in - subscriber.putNext((next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)) - }, error: subscriber.putError, completed: subscriber.putCompletion) - - return ActionDisposable { - fetchedFullSizeDisposable.dispose() - fullSizeDisposable.dispose() + } + + let dataDisposable = combineLatest(signals).start(next: { results in + var foundData = false + loop: for i in (0 ..< results.count).reversed() { + let isLastSize = i == results.count - 1 + switch results[i].0 { + case .image: + if let data = results[i].1, data.count != 0 { + subscriber.putNext(ImageRenderData(nil, data, isLastSize)) + foundData = true + if isLastSize { + subscriber.putCompletion() + } + break loop } - } - } else { - fullSizeData = account.postbox.mediaBox.resourceData(largestRepresentation.resource) - |> map { next -> (Data?, Bool) in - return (next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete) + case let .miniThumbnail(thumbnailData): + subscriber.putNext(ImageRenderData(thumbnailData, nil, false)) + foundData = true + break loop } } - - - return thumbnail |> mapToSignal { thumbnailData in - return fullSizeData |> map { (fullSizeData, complete) in - return (thumbnailData, fullSizeData, complete) - } + if !foundData { + subscriber.putNext(ImageRenderData(nil, nil, false)) } + }) + var fetchDisposable: Disposable? + if autoFetchFullSize { + fetchDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: imageReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< largestByteSize, .default), statsCategory: .image).start() + } else if useMiniThumbnailIfAvailable { + fetchDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: imageReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< thumbnailByteSize, .default), statsCategory: .image).start() } + + return ActionDisposable { + dataDisposable.dispose() + fetchDisposable?.dispose() } + } + } + + + if let smallestRepresentation = smallestImageRepresentation(imageReference.media.representations), let largestRepresentation = imageReference.media.representationForDisplayAtSize(PixelDimensions(fullRepresentationSize)) { + + let maybeFullSize = postbox.mediaBox.resourceData(largestRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) + + let signal = maybeFullSize + |> take(1) + |> mapToSignal { maybeData -> Signal in + if maybeData.complete { + let loadedData: Data? + if largestRepresentation.resource is EncryptedMediaResource, let secureIdAccessContext = secureIdAccessContext { + loadedData = decryptedResourceData(data: maybeData, resource: largestRepresentation.resource, params: secureIdAccessContext) + } else { + loadedData = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) + } + return .single(ImageRenderData(nil, loadedData, true)) + } else { + + + let decodedThumbnailData = imageReference.media.immediateThumbnailData.flatMap(decodeTinyThumbnail) + let fetchedThumbnail: Signal + if let _ = decodedThumbnailData { + fetchedThumbnail = .complete() + } else { + let reference: MediaResourceReference + if let peer = peer, let peerReference = PeerReference(peer) { + reference = MediaResourceReference.avatar(peer: peerReference, resource: smallestRepresentation.resource) + } else { + reference = imageReference.resourceReference(smallestRepresentation.resource) + } + fetchedThumbnail = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: reference, statsCategory: .image) |> `catch` { _ in return .complete() } + } + let reference: MediaResourceReference + if let peer = peer, let peerReference = PeerReference(peer) { + reference = MediaResourceReference.avatar(peer: peerReference, resource: largestRepresentation.resource) + } else { + reference = imageReference.resourceReference(largestRepresentation.resource) + } + let fetchedFullSize = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: reference, statsCategory: .image) + + let anyThumbnail: [Signal] + if tryAdditionalRepresentations { + anyThumbnail = imageReference.media.representations.filter({ representation in + return representation != largestRepresentation + }).map({ representation -> Signal in + return postbox.mediaBox.resourceData(representation.resource) + |> take(1) + }) + } else { + anyThumbnail = [] + } + + let mainThumbnail = Signal { subscriber in + if let decodedThumbnailData = decodedThumbnailData { + subscriber.putNext(decodedThumbnailData) + subscriber.putCompletion() + return EmptyDisposable + } else { + let fetchedDisposable = fetchedThumbnail.start() + let thumbnailDisposable = postbox.mediaBox.resourceData(smallestRepresentation.resource, attemptSynchronously: synchronousLoad).start(next: { next in + if next.complete { + subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) + } + }, error: subscriber.putError, completed: subscriber.putCompletion) + + return ActionDisposable { + fetchedDisposable.dispose() + thumbnailDisposable.dispose() + } + } + } + + let thumbnail = combineLatest(anyThumbnail) + |> mapToSignal { thumbnails -> Signal in + for thumbnail in thumbnails { + if thumbnail.size != 0, let data = try? Data(contentsOf: URL(fileURLWithPath: thumbnail.path), options: []) { + return .single(data) + } + } + return mainThumbnail + } + + let fullSizeData: Signal + + if autoFetchFullSize { + fullSizeData = Signal { subscriber in + let fetchedFullSizeDisposable = fetchedFullSize.start() + let fullSizeDisposable = postbox.mediaBox.resourceData(largestRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad).start(next: { next in + subscriber.putNext(ImageRenderData(nil, next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)) + }, error: subscriber.putError, completed: subscriber.putCompletion) + + return ActionDisposable { + fetchedFullSizeDisposable.dispose() + fullSizeDisposable.dispose() + } + } + } else { + fullSizeData = postbox.mediaBox.resourceData(largestRepresentation.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) + |> map { next -> ImageRenderData in + return ImageRenderData(nil, next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete) + } + } + + return thumbnail + |> mapToSignal { thumbnailData in + if let _ = thumbnailData { + return fullSizeData + |> map { fullSizeData in + return ImageRenderData(fullSizeData.fullSizeComplete ? nil : thumbnailData, fullSizeData.fullSizeData, fullSizeData.fullSizeComplete) + } + } else { + return .single(ImageRenderData(nil, nil, false)) + } + } +// +// return combineLatest(thumbnail, fullSizeData) +// |> map { thumbnailData, fullSizeData -> ImageRenderData in +// return ImageRenderData(fullSizeData.fullSizeComplete ? nil : thumbnailData, fullSizeData.fullSizeData, fullSizeData.fullSizeComplete) +// } + } + } + |> distinctUntilChanged(isEqual: { lhs, rhs in + if (lhs.thumbnailData == nil && lhs.fullSizeData == nil) && (rhs.thumbnailData == nil && rhs.fullSizeData == nil) { + return true + } else { + return false + } + }) + return signal } else { return .never() } } -private func chatMessageWebFilePhotoDatas(account: Account, photo: TelegramMediaWebFile) -> Signal<(Data?, Bool), NoError> { - let maybeFullSize = account.postbox.mediaBox.resourceData(photo.resource) + + +private func chatMessageWebFilePhotoDatas(account: Account, photo: TelegramMediaWebFile, synchronousLoad: Bool = false) -> Signal { + let maybeFullSize = account.postbox.mediaBox.resourceData(photo.resource, attemptSynchronously: synchronousLoad) - let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, Bool), NoError> in + let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal in if maybeData.complete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) - return .single((loadedData, true)) + return .single(ImageRenderData(nil, loadedData, true)) } else { - let fullSizeData: Signal<(Data?, Bool), NoError> - - fullSizeData = account.postbox.mediaBox.resourceData(photo.resource) - |> map { next -> (Data?, Bool) in - return (next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete) - } - - return fullSizeData |> map { resource in - return (resource.0, resource.1) + return account.postbox.mediaBox.resourceData(photo.resource, attemptSynchronously: synchronousLoad) + |> map { next -> ImageRenderData in + return ImageRenderData(nil, next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete) } } - - } |> filter({ $0.0 != nil }) + } |> filter({ $0.fullSizeData != nil }) return signal } -private func chatMessageFileDatas(account: Account, file: TelegramMediaFile, pathExtension: String? = nil, progressive: Bool = false, justThumbail: Bool = false) -> Signal<(Data?, String?, Bool), NoError> { - if let smallestRepresentation = smallestImageRepresentation(file.previewRepresentations) { +private func chatMessageFileDatas(account: Account, fileReference: FileMediaReference, pathExtension: String? = nil, progressive: Bool = false, justThumbail: Bool = false, synchronousLoad: Bool = false) -> Signal { + if let smallestRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { let thumbnailResource = smallestRepresentation.resource - let fullSizeResource = file.resource + let fullSizeResource = largestImageRepresentation(fileReference.media.previewRepresentations)?.resource ?? smallestRepresentation.resource - let maybeFullSize = account.postbox.mediaBox.resourceData(fullSizeResource, pathExtension: pathExtension) + let maybeFullSize = account.postbox.mediaBox.resourceData(fullSizeResource, pathExtension: pathExtension, attemptSynchronously: synchronousLoad) - let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, String?, Bool), NoError> in - if maybeData.complete && !justThumbail { - return .single((nil, maybeData.path, true)) - } else { - let fetchedThumbnail = account.postbox.mediaBox.fetchedResource(thumbnailResource, tag: TelegramMediaResourceFetchTag(statsCategory: .file)) + let signal = maybeFullSize |> mapToSignal { maybeData -> Signal in + + if maybeData.complete && !justThumbail { + let fullData = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path)) + return .single(ImageRenderData(nil, fullData, fullData != nil)) + } else { + let fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: fileReference.resourceReference(thumbnailResource)) let thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() - let thumbnailDisposable = account.postbox.mediaBox.resourceData(thumbnailResource, pathExtension: pathExtension).start(next: { next in + let thumbnailDisposable = account.postbox.mediaBox.resourceData(thumbnailResource, pathExtension: pathExtension, attemptSynchronously: synchronousLoad).start(next: { next in subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) }, error: subscriber.putError, completed: subscriber.putCompletion) @@ -150,17 +361,26 @@ private func chatMessageFileDatas(account: Account, file: TelegramMediaFile, pat } - let fullSizeDataAndPath = account.postbox.mediaBox.resourceData(fullSizeResource, option: !progressive ? .complete(waitUntilFetchStatus: false) : .incremental(waitUntilFetchStatus: false)) |> map { next -> (String?, Bool) in - return (next.size == 0 ? nil : next.path, next.complete) + let fullSizeDataAndPath = account.postbox.mediaBox.resourceData(fullSizeResource, option: !progressive ? .complete(waitUntilFetchStatus: false) : .incremental(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) |> map { next -> Data? in + return next.size == 0 || !next.complete ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path)) } - + + //TODO return thumbnail |> mapToSignal { thumbnailData in - return fullSizeDataAndPath |> map { (dataPath, complete) in - return (thumbnailData, dataPath, complete) + return fullSizeDataAndPath |> take(1) |> map { fullSizeData in + return ImageRenderData(thumbnailData, fullSizeData, fullSizeData != nil) } - } + } |> then(Signal({ subscriber -> Disposable in + if !maybeData.complete, let fullSizeResource = fullSizeResource as? LocalFileReferenceMediaResource { + let thumbnailData = justThumbail ? try? Data(contentsOf: URL(fileURLWithPath: fullSizeResource.localFilePath)) : nil + let fulleSizeData = try? Data(contentsOf: URL(fileURLWithPath: fullSizeResource.localFilePath)) + subscriber.putNext(ImageRenderData(thumbnailData, fulleSizeData, true)) + } + subscriber.putCompletion() + return EmptyDisposable + })) } - } |> filter({ $0.0 != nil || $0.1 != nil }) + } |> filter({ $0.thumbnailData != nil || $0.fullSizeData != nil }) return signal } else { @@ -169,41 +389,45 @@ private func chatMessageFileDatas(account: Account, file: TelegramMediaFile, pat } -func chatGalleryPhoto(account: Account, photo: TelegramMediaImage, toRepresentationSize:NSSize = NSMakeSize(1280, 1280), scale:CGFloat) -> Signal<(TransformImageArguments) -> CGImage?, NoError> { - let signal = chatMessagePhotoDatas(account: account, photo: photo, fullRepresentationSize:toRepresentationSize) +func chatGalleryPhoto(account: Account, imageReference: ImageMediaReference, toRepresentationSize:NSSize = NSMakeSize(1280, 1280), peer: Peer? = nil, scale:CGFloat, secureIdAccessContext: SecureIdAccessContext? = nil, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> CGImage?, NoError> { + let signal = chatMessagePhotoDatas(postbox: account.postbox, imageReference: imageReference, fullRepresentationSize:toRepresentationSize, synchronousLoad: synchronousLoad, secureIdAccessContext: secureIdAccessContext, peer: peer) - return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in + return signal |> map { data in return { arguments in - assertNotOnMainThread() let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) + let fullSizeData = data.fullSizeData + let thumbnailData = data.thumbnailData + + let options = NSMutableDictionary() + + options.setValue(max(fittedSize.width * scale, fittedSize.height * scale) as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) + options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) + options.setValue(false as NSNumber, forKey: kCGImageSourceShouldCache as String) + options.setValue(false as NSNumber, forKey: kCGImageSourceShouldCacheImmediately as String) + options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailWithTransform as String) + + + if let fullSizeData = fullSizeData { - if fullSizeComplete { - let options = NSMutableDictionary() - - // options.setValue(max(fittedSize.width * scale, fittedSize.height * scale) as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) - options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) - options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailWithTransform as String) + if data.fullSizeData != nil { if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, options), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) { - return image + return generateImage(image.size, contextGenerator: { (size, ctx) in + ctx.setFillColor(theme.colors.transparentBackground.cgColor) + ctx.fill(NSMakeRect(0, 0, size.width, size.height)) + ctx.draw(image, in: NSMakeRect(0, 0, size.width, size.height)) + }) + } - } else { - let imageSource = CGImageSourceCreateIncremental(nil) - CGImageSourceUpdateData(imageSource, fullSizeData as CFData, fullSizeComplete) - - let options = NSMutableDictionary() - options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailWithTransform as String) - options[kCGImageSourceShouldCache as NSString] = false as NSNumber - if let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { - return image - } } } + options.setValue(150 as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) + var thumbnailImage: CGImage? - if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { + if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, options), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) { thumbnailImage = image } @@ -215,12 +439,15 @@ func chatGalleryPhoto(account: Account, photo: TelegramMediaImage, toRepresentat c.interpolationQuality = .none c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) } - telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + telegramFastBlurMore(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + + return generateImage(fittedSize, contextGenerator: { size, ctx in + ctx.draw(thumbnailContext.generateImage()!, in: NSMakeRect(0, 0, size.width, size.height)) + }) - return thumbnailContext.generateImage() } return generateImage(fittedSize, contextGenerator: { (size, ctx) in - ctx.setFillColor(theme.colors.background.cgColor) + ctx.setFillColor(theme.colors.transparentBackground.cgColor) ctx.fill(NSMakeRect(0, 0, size.width, size.height)) }) @@ -228,31 +455,41 @@ func chatGalleryPhoto(account: Account, photo: TelegramMediaImage, toRepresentat } } -func chatMessagePhoto(account: Account, photo: TelegramMediaImage, toRepresentationSize:NSSize = NSMakeSize(1280, 1280), scale:CGFloat) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatMessagePhotoDatas(account: account, photo: photo, fullRepresentationSize:toRepresentationSize) +func chatMessagePhoto(account: Account, imageReference: ImageMediaReference, toRepresentationSize:NSSize = NSMakeSize(1280, 1280), peer: Peer? = nil, scale:CGFloat, synchronousLoad: Bool = false, autoFetchFullSize: Bool = false) -> Signal { + let signal = chatMessagePhotoDatas(postbox: account.postbox, imageReference: imageReference, fullRepresentationSize: toRepresentationSize, autoFetchFullSize: autoFetchFullSize, synchronousLoad: synchronousLoad, peer: peer) - return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in - return { arguments in - assertNotOnMainThread() + return signal |> map { data in + return ImageDataTransformation(data: data, execute: { arguments, data in let context = DrawingContext(size: arguments.drawingSize, scale:scale, clear: true) + let fullSizeData = data.fullSizeData + let thumbnailData = data.thumbnailData + let drawingRect = arguments.drawingRect - let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) - let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + var fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize) + switch arguments.resizeMode { + case .none: + break + default: + fittedSize = fittedSize.fitted(arguments.imageSize) + } + let fittedRect = CGRect(origin: CGPoint(x: floorToScreenPixels(System.backingScale, drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0), y: floorToScreenPixels(System.backingScale, drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0)), size: fittedSize) var fullSizeImage: CGImage? if let fullSizeData = fullSizeData { - if fullSizeComplete { + if data.fullSizeComplete { let options = NSMutableDictionary() options.setValue(max(fittedSize.width * context.scale, fittedSize.height * context.scale) as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) - options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) + options.setValue(false as NSNumber, forKey: kCGImageSourceShouldCache as String) + + options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) { fullSizeImage = image } } else { let imageSource = CGImageSourceCreateIncremental(nil) - CGImageSourceUpdateData(imageSource, fullSizeData as CFData, fullSizeComplete) + CGImageSourceUpdateData(imageSource, fullSizeData as CFData, data.fullSizeComplete) let options = NSMutableDictionary() options[kCGImageSourceShouldCache as NSString] = false as NSNumber @@ -262,32 +499,92 @@ func chatMessagePhoto(account: Account, photo: TelegramMediaImage, toRepresentat } } + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) var thumbnailImage: CGImage? - if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { + if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, options), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options) { thumbnailImage = image } var blurredThumbnailImage: CGImage? if let thumbnailImage = thumbnailImage { let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) - let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 150.0, height: 150.0)) + + let initialThumbnailContextFittingSize = fittedSize.fitted(CGSize(width: 90.0, height: 90.0)) + + let thumbnailContextSize = thumbnailSize.aspectFitted(initialThumbnailContextFittingSize) let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) - thumbnailContext.withContext { c in - c.interpolationQuality = .none + thumbnailContext.withFlippedContext { c in c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) } - telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + telegramFastBlurMore(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) - blurredThumbnailImage = thumbnailContext.generateImage() + var thumbnailContextFittingSize = CGSize(width: floor(arguments.drawingSize.width * 0.5), height: floor(arguments.drawingSize.width * 0.5)) + if thumbnailContextFittingSize.width < 150.0 || thumbnailContextFittingSize.height < 150.0 { + thumbnailContextFittingSize = thumbnailContextFittingSize.aspectFilled(CGSize(width: 150.0, height: 150.0)) + } + + if thumbnailContextFittingSize.width > thumbnailContextSize.width { + let additionalContextSize = thumbnailContextFittingSize + let additionalBlurContext = DrawingContext(size: additionalContextSize, scale: 1.0) + additionalBlurContext.withFlippedContext { c in + c.interpolationQuality = .default + if let image = thumbnailContext.generateImage() { + c.draw(image, in: CGRect(origin: CGPoint(), size: additionalContextSize)) + } + } + telegramFastBlur(Int32(additionalContextSize.width), Int32(additionalContextSize.height), Int32(additionalBlurContext.bytesPerRow), additionalBlurContext.bytes) + blurredThumbnailImage = additionalBlurContext.generateImage() + } else { + blurredThumbnailImage = thumbnailContext.generateImage() + } } - context.withContext { c in + + context.withContext(isHighQuality: data.fullSizeComplete, { c in c.setBlendMode(.copy) if arguments.boundingSize != arguments.imageSize { - c.fill(arguments.drawingRect) + switch arguments.resizeMode { + case .blurBackground: + let blurSourceImage = thumbnailImage ?? fullSizeImage + + if let fullSizeImage = blurSourceImage { + let thumbnailSize = CGSize(width: fullSizeImage.width, height: fullSizeImage.height) + let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 74.0, height: 74.0)) + let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) + thumbnailContext.withFlippedContext { c in + c.interpolationQuality = .none + c.draw(fullSizeImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) + } + // telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + telegramFastBlurMore(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + + if let blurredImage = thumbnailContext.generateImage() { + let filledSize = thumbnailSize.aspectFilled(arguments.drawingRect.size) + c.interpolationQuality = .medium + c.draw(blurredImage, in: CGRect(origin: CGPoint(x: arguments.drawingRect.minX + (arguments.drawingRect.width - filledSize.width) / 2.0, y: arguments.drawingRect.minY + (arguments.drawingRect.height - filledSize.height) / 2.0), size: filledSize)) + c.setBlendMode(.normal) + c.setFillColor(theme.colors.background.withAlphaComponent(0.5).cgColor) + c.fill(arguments.drawingRect) + c.setBlendMode(.copy) + } + } else { + c.fill(arguments.drawingRect) + } + case let .fill(color): + c.setFillColor(color.cgColor) + c.fill(arguments.drawingRect) + case .fillTransparent: + c.setFillColor(theme.colors.transparentBackground.cgColor) + c.fill(arguments.drawingRect) + case .none: + break + case .imageColor: + break + } } - c.setBlendMode(.copy) if let blurredThumbnailImage = blurredThumbnailImage { c.interpolationQuality = .low c.draw(blurredThumbnailImage, in: fittedRect) @@ -299,23 +596,22 @@ func chatMessagePhoto(account: Account, photo: TelegramMediaImage, toRepresentat c.interpolationQuality = .medium c.draw(fullSizeImage, in: fittedRect) } - - } + + }) addCorners(context, arguments: arguments, scale:scale) return context - } + }) } } -func chatMessageWebFilePhoto(account: Account, photo: TelegramMediaWebFile, toRepresentationSize:NSSize = NSMakeSize(1280, 1280), scale:CGFloat) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatMessageWebFilePhotoDatas(account: account, photo: photo) +func chatMessageWebFilePhoto(account: Account, photo: TelegramMediaWebFile, toRepresentationSize:NSSize = NSMakeSize(1280, 1280), scale:CGFloat, synchronousLoad: Bool = false) -> Signal { + let signal = chatMessageWebFilePhotoDatas(account: account, photo: photo, synchronousLoad: synchronousLoad) - return signal |> map { (fullSizeData, fullSizeComplete) in - return { arguments in - assertNotOnMainThread() + return signal |> map { data in + return ImageDataTransformation(data: data, execute: { arguments, data in let context = DrawingContext(size: arguments.drawingSize, scale:scale, clear: true) let drawingRect = arguments.drawingRect @@ -323,18 +619,20 @@ func chatMessageWebFilePhoto(account: Account, photo: TelegramMediaWebFile, toRe let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) var fullSizeImage: CGImage? - if let fullSizeData = fullSizeData { - if fullSizeComplete { + if let fullSizeData = data.fullSizeData { + if data.fullSizeComplete { let options = NSMutableDictionary() options.setValue(max(fittedSize.width * context.scale, fittedSize.height * context.scale) as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) - if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) { + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + + if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, options), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) { fullSizeImage = image } } else { let imageSource = CGImageSourceCreateIncremental(nil) - CGImageSourceUpdateData(imageSource, fullSizeData as CFData, fullSizeComplete) + CGImageSourceUpdateData(imageSource, fullSizeData as CFData, data.fullSizeComplete) let options = NSMutableDictionary() options[kCGImageSourceShouldCache as NSString] = false as NSNumber @@ -344,11 +642,12 @@ func chatMessageWebFilePhoto(account: Account, photo: TelegramMediaWebFile, toRe } } - - context.withContext { c in + + context.withContext(isHighQuality: fullSizeImage != nil, { c in c.setBlendMode(.copy) if arguments.boundingSize != arguments.imageSize { + c.setFillColor(theme.colors.grayBackground.cgColor) c.fill(arguments.drawingRect) } @@ -358,13 +657,13 @@ func chatMessageWebFilePhoto(account: Account, photo: TelegramMediaWebFile, toRe c.draw(fullSizeImage, in: fittedRect) } - } + }) addCorners(context, arguments: arguments, scale:scale) return context - } + }) } } @@ -376,433 +675,1136 @@ enum StickerDatasType { case full } +func chatMessageStickerResource(file: TelegramMediaFile, small: Bool) -> MediaResource { + let resource: MediaResource + if small, let smallest = largestImageRepresentation(file.previewRepresentations) { + resource = smallest.resource + } else { + resource = file.resource + } + return resource +} + + + -private func chatMessageStickerDatas(account: Account, file: TelegramMediaFile, type: StickerDatasType) -> Signal<(Data?, Data?, Bool), NoError> { - // let maybeFetched = account.postbox.mediaBox.resourceData(file.resource, complete: true) + +private func chatMessageStickerDatas(postbox: Postbox, file: FileMediaReference, small: Bool, fetched: Bool, onlyFullSize: Bool, synchronousLoad: Bool) -> Signal { - let dimensions:NSSize? - switch type { - case .thumb: - dimensions = CGSize(width: 30, height: 30) - case .small: - dimensions = CGSize(width: 120, height: 120) - case .chatMessage: - dimensions = CGSize(width: 300, height: 300) - case .full: - dimensions = nil - } + let thumbnailResource = chatMessageStickerResource(file: file.media, small: true) + let resource = chatMessageStickerResource(file: file.media, small: small) + + let maybeFetched = postbox.mediaBox.cachedResourceRepresentation(resource, representation: CachedStickerAJpegRepresentation(size: small ? CGSize(width: 120.0, height: 120.0) : nil), complete: false, fetch: false, attemptSynchronously: synchronousLoad) |> runOn(synchronousLoad ? .mainQueue() : resourcesQueue) - let maybeFetched = account.postbox.mediaBox.cachedResourceRepresentation(file.resource, representation: CachedStickerAJpegRepresentation(size: dimensions), complete: true) - return maybeFetched |> take(1) |> mapToSignal { maybeData in - var size:Int = 0 - if let s = file.size { - size = s - } - if maybeData.size >= size { - let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) - - return .single((nil, loadedData, true)) - } else { - //let fullSizeData = account.postbox.mediaBox.resourceData(file.resource, complete: true) - - let fullSizeData = account.postbox.mediaBox.cachedResourceRepresentation(file.resource, representation: CachedStickerAJpegRepresentation(size: dimensions), complete: true) |> map { next in - return (next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe), next.complete) - } - - return fullSizeData |> map { (data, complete) -> (Data?, Data?, Bool) in - return (nil, data, complete) + return maybeFetched + |> take(1) + |> mapToSignal { maybeData in + if maybeData.complete { + let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) + + return .single(ImageRenderData(nil, loadedData, true)) + } else { + var thumbnailData = postbox.mediaBox.cachedResourceRepresentation(thumbnailResource, representation: CachedStickerAJpegRepresentation(size: nil), complete: true) |> map(Optional.init) + if resource is LocalFileReferenceMediaResource { + thumbnailData = .single(nil) + } + let fullSizeData:Signal = .single(ImageRenderData(nil, nil, false)) |> then(postbox.mediaBox.cachedResourceRepresentation(resource, representation: CachedStickerAJpegRepresentation(size: small ? CGSize(width: 120.0, height: 120.0) : nil), complete: true) + |> map { next in + return ImageRenderData(nil, !next.complete ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe), next.complete) + }) + + + return Signal { subscriber in + var fetch: Disposable? + if fetched { + fetch = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: file.resourceReference(resource)).start() + } + + var fetchThumbnail: Disposable? + if !thumbnailResource.id.isEqual(to: resource.id) { + fetchThumbnail = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: file.resourceReference(thumbnailResource)).start() + } + let disposable = (combineLatest(thumbnailData, fullSizeData) + |> map { thumbnailData, fullSizeData -> ImageRenderData in + if let thumbnailData = thumbnailData { + return ImageRenderData(thumbnailData.complete && !fullSizeData.fullSizeComplete ? try? Data(contentsOf: URL(fileURLWithPath: thumbnailData.path)) : nil, fullSizeData.fullSizeData, fullSizeData.fullSizeComplete) + } else { + return fullSizeData + } + }).start(next: { next in + subscriber.putNext(next) + }, error: { error in + subscriber.putError(error) + }, completed: { + subscriber.putCompletion() + }) + + return ActionDisposable { + fetch?.dispose() + fetchThumbnail?.dispose() + disposable.dispose() + } + } } - } } } -func chatMessageSticker(account: Account, file: TelegramMediaFile, type: StickerDatasType, scale:CGFloat) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + + +private func chatMessageStickerThumbnailData(postbox: Postbox, file: TelegramMediaFile, synchronousLoad: Bool) -> Signal { + let thumbnailResource = chatMessageStickerResource(file: file, small: true) - let signal = chatMessageStickerDatas(account: account, file: file, type: type) + let maybeFetched = postbox.mediaBox.cachedResourceRepresentation(thumbnailResource, representation: CachedStickerAJpegRepresentation(size: nil), complete: false, fetch: false, attemptSynchronously: synchronousLoad) - return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in - return { arguments in - assertNotOnMainThread() + return maybeFetched + |> take(1) + |> mapToSignal { maybeData in + if maybeData.complete { + let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) + return .single(loadedData) + } else { + let thumbnailData = postbox.mediaBox.cachedResourceRepresentation(thumbnailResource, representation: CachedStickerAJpegRepresentation(size: nil), complete: true) + + return Signal { subscriber in + let fetchThumbnail = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: stickerPackFileReference(file).resourceReference(thumbnailResource)).start() + + let disposable = (thumbnailData + |> map { thumbnailData -> Data? in + return thumbnailData.complete ? try? Data(contentsOf: URL(fileURLWithPath: thumbnailData.path)) : nil + }).start(next: { next in + subscriber.putNext(next) + }, error: { error in + subscriber.putError(error) + }, completed: { + subscriber.putCompletion() + }) + + return ActionDisposable { + fetchThumbnail.dispose() + disposable.dispose() + } + } + } + } +} + + +public func chatMessageSticker(postbox: Postbox, file: FileMediaReference, small: Bool, scale: CGFloat, fetched: Bool = false, onlyFullSize: Bool = false, thumbnail: Bool = false, synchronousLoad: Bool = false) -> Signal { + let signal: Signal + signal = chatMessageStickerDatas(postbox: postbox, file: file, small: small, fetched: fetched, onlyFullSize: onlyFullSize, synchronousLoad: synchronousLoad) + return signal |> map { data in + return ImageDataTransformation(data: data, execute: { arguments, data in + let context = DrawingContext(size: arguments.drawingSize, scale: scale, clear: arguments.emptyColor == nil) - let context = DrawingContext(size: arguments.drawingSize, scale:scale, clear: true) + let drawingRect = arguments.drawingRect + let fittedSize = arguments.imageSize + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + //let fittedRect = arguments.drawingRect - var fullSizeImage: (CGImage, CGImage)? - if let fullSizeData = fullSizeData, fullSizeComplete { - if let image = imageFromAJpeg(data: fullSizeData) { + var fullSizeImage: CGImage? + if let fullSizeData = data.fullSizeData, data.fullSizeComplete { + if let image = NSImage(data: fullSizeData)?.cgImage(forProposedRect: nil, context: nil, hints: nil) { fullSizeImage = image } } - let thumbnailImage: CGImage? = nil + var thumbnailImage: CGImage? + if fullSizeImage == nil, let thumbnailData = data.thumbnailData { + if let image = NSImage(data: thumbnailData)?.cgImage(forProposedRect: nil, context: nil, hints: nil) { + thumbnailImage = image + } + } var blurredThumbnailImage: CGImage? + let thumbnailInset: CGFloat = 10.0 if let thumbnailImage = thumbnailImage { - let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) - let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 150.0, height: 150.0)) - let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) - thumbnailContext.withContext { c in - c.interpolationQuality = .none - c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) - } - telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + let thumbnailSize = thumbnailImage.size + var thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 150.0, height: 150.0)) + let thumbnailDrawingSize = thumbnailContextSize + thumbnailContextSize.width += thumbnailInset * 2.0 + thumbnailContextSize.height += thumbnailInset * 2.0 + let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0, clear: true) + thumbnailContext.withFlippedContext(isHighQuality: false, { c in + let cgImage = thumbnailImage + c.setBlendMode(.normal) + c.interpolationQuality = .medium + c.draw(cgImage, in: CGRect(origin: CGPoint(x: thumbnailInset, y: thumbnailInset), size: thumbnailDrawingSize)) + }) + stickerThumbnailAlphaBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) blurredThumbnailImage = thumbnailContext.generateImage() } - context.withContext { c in - c.setBlendMode(.copy) + context.withFlippedContext(isHighQuality: data.fullSizeData != nil, { c in + if let color = arguments.emptyColor { + c.setBlendMode(.normal) + switch color { + case let .color(color): + c.setFillColor(color.cgColor) + default: + break + } + c.fill(drawingRect) + } else { + c.setBlendMode(.copy) + } + if let blurredThumbnailImage = blurredThumbnailImage { c.interpolationQuality = .low - c.draw(blurredThumbnailImage, in: arguments.drawingRect) + let thumbnailScaledInset = thumbnailInset * (fittedRect.width / blurredThumbnailImage.size.width) + c.draw(blurredThumbnailImage, in: fittedRect.insetBy(dx: -thumbnailScaledInset, dy: -thumbnailScaledInset)) } if let fullSizeImage = fullSizeImage { - let cgImage = fullSizeImage.0 - let cgImageAlpha = fullSizeImage.1 - + let cgImage = fullSizeImage c.setBlendMode(.normal) c.interpolationQuality = .medium - - - let mask = CGImage(maskWidth: cgImageAlpha.width, height: cgImageAlpha.height, bitsPerComponent: cgImageAlpha.bitsPerComponent, bitsPerPixel: cgImageAlpha.bitsPerPixel, bytesPerRow: cgImageAlpha.bytesPerRow, provider: cgImageAlpha.dataProvider!, decode: nil, shouldInterpolate: true) - - c.draw(cgImage.masking(mask!)!, in: arguments.drawingRect) + c.draw(cgImage, in: fittedRect) } - } + }) return context - } + }) } } -func chatWebpageSnippetPhotoData(account: Account, photo: TelegramMediaImage, small:Bool) -> Signal { - if let closestRepresentation = (small ? photo.representationForDisplayAtSize(CGSize(width: 120.0, height: 120.0)) : largestImageRepresentation(photo.representations)) { - let resourceData = account.postbox.mediaBox.resourceData(closestRepresentation.resource) |> map { next in - return next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe) - } - - return Signal { subscriber in - let disposable = DisposableSet() - disposable.add(resourceData.start(next: { data in - subscriber.putNext(data) - }, error: { error in - subscriber.putError(error) - }, completed: { - subscriber.putCompletion() - })) - disposable.add(account.postbox.mediaBox.fetchedResource(closestRepresentation.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .file)).start()) - return disposable - } - } else { - return .never() - } -} -func chatWebpageSnippetPhoto(account: Account, photo: TelegramMediaImage, scale:CGFloat, small:Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatWebpageSnippetPhotoData(account: account, photo: photo, small:small) + + + + +private func chatMessageAnimatedStickerDatas(postbox: Postbox, file: FileMediaReference, small: Bool, fetched: Bool, onlyFullSize: Bool, synchronousLoad: Bool, size: NSSize) -> Signal { + let thumbnailResource = chatMessageStickerResource(file: file.media, small: true) + let resource = chatMessageStickerResource(file: file.media, small: small) - return signal |> map { fullSizeData in - return { arguments in - var fullSizeImage: CGImage? - if let fullSizeData = fullSizeData { - let options = NSMutableDictionary() - if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { - fullSizeImage = image - } - } - - if let fullSizeImage = fullSizeImage { - let context = DrawingContext(size: arguments.drawingSize, scale:scale, clear: true) + let maybeFetched = postbox.mediaBox.cachedResourceRepresentation(resource, representation: CachedAnimatedStickerRepresentation(thumb: small, size: size, fitzModifier: file.media.animatedEmojiFitzModifier), complete: false, fetch: false, attemptSynchronously: synchronousLoad) |> runOn(synchronousLoad ? .mainQueue() : resourcesQueue) + + return maybeFetched + |> take(1) + |> mapToSignal { maybeData in + let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: [.mappedIfSafe]) + if maybeData.complete, let loadedData = loadedData, loadedData.count > 0 { + return .single(ImageRenderData(nil, loadedData, true)) + } else { + let thumbnailData = postbox.mediaBox.cachedResourceRepresentation(thumbnailResource, representation: CachedAnimatedStickerRepresentation(thumb: true, size: size.aspectFitted(NSMakeSize(60, 60)), fitzModifier: file.media.animatedEmojiFitzModifier), complete: true) - let fittedSize = CGSize(width: fullSizeImage.width, height: fullSizeImage.height).aspectFilled(arguments.boundingSize) - let drawingRect = arguments.drawingRect - let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + let fullSizeData:Signal = .single(ImageRenderData(nil, nil, false)) |> then(postbox.mediaBox.cachedResourceRepresentation(resource, representation: CachedAnimatedStickerRepresentation(thumb: false, size: size, fitzModifier: file.media.animatedEmojiFitzModifier), complete: true) + |> map { next in + return ImageRenderData(nil, !next.complete ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe), next.complete) + }) - context.withContext { c in - c.setBlendMode(.copy) - if arguments.boundingSize.width > arguments.imageSize.width || arguments.boundingSize.height > arguments.imageSize.height { - c.fill(arguments.drawingRect) + return Signal { subscriber in + var fetch: Disposable? + if fetched { + fetch = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: file.resourceReference(resource)).start() } - c.interpolationQuality = .medium - c.draw(fullSizeImage, in: fittedRect) + var fetchThumbnail: Disposable? + if !thumbnailResource.id.isEqual(to: resource.id) { + fetchThumbnail = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: file.resourceReference(thumbnailResource)).start() + } + let disposable = (combineLatest(thumbnailData, fullSizeData) + |> map { thumbnailData, fullSizeData -> ImageRenderData in + return ImageRenderData(thumbnailData.complete && !fullSizeData.fullSizeComplete ? try? Data(contentsOf: URL(fileURLWithPath: thumbnailData.path)) : nil, fullSizeData.fullSizeData, fullSizeData.fullSizeComplete) + }).start(next: { next in + subscriber.putNext(next) + }, error: { error in + subscriber.putError(error) + }, completed: { + subscriber.putCompletion() + }) + + return ActionDisposable { + fetch?.dispose() + fetchThumbnail?.dispose() + disposable.dispose() + } } - - addCorners(context, arguments: arguments, scale:scale) - - return context - } else { - return nil } - } } } -func chatMessagePhotoStatus(account: Account, photo: TelegramMediaImage) -> Signal { - if let largestRepresentation = largestRepresentationForPhoto(photo) { - return account.postbox.mediaBox.resourceStatus(largestRepresentation.resource) - } else { - return .never() - } -} -func chatMessagePhotoInteractiveFetched(account: Account, photo: TelegramMediaImage) -> Signal { - if let largestRepresentation = largestRepresentationForPhoto(photo) { - return account.postbox.mediaBox.fetchedResource(largestRepresentation.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)) |> map {_ in} - } else { - return .never() - } -} -func chatMessagePhotoCancelInteractiveFetch(account: Account, photo: TelegramMediaImage) { - if let largestRepresentation = largestRepresentationForPhoto(photo) { - return account.postbox.mediaBox.cancelInteractiveResourceFetch(largestRepresentation.resource) +private func chatMessageAnimatedStickerThumbnailData(postbox: Postbox, file: FileMediaReference, synchronousLoad: Bool) -> Signal { + let thumbnailResource = chatMessageStickerResource(file: file.media, small: true) + var size: NSSize = NSMakeSize(60, 60) + size = file.media.dimensions?.size.aspectFitted(size) ?? size + let maybeFetched = postbox.mediaBox.cachedResourceRepresentation(thumbnailResource, representation: CachedAnimatedStickerRepresentation(thumb: true, size: size), complete: false, fetch: false, attemptSynchronously: synchronousLoad) + + return maybeFetched + |> take(1) + |> mapToSignal { maybeData in + if maybeData.complete { + let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) + return .single(loadedData) + } else { + let thumbnailData = postbox.mediaBox.cachedResourceRepresentation(thumbnailResource, representation: CachedAnimatedStickerRepresentation(thumb: true, size: size), complete: false) + + return Signal { subscriber in + let fetchThumbnail = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: file.resourceReference(thumbnailResource)).start() + + let disposable = (thumbnailData + |> map { thumbnailData -> Data? in + return thumbnailData.complete ? try? Data(contentsOf: URL(fileURLWithPath: thumbnailData.path)) : nil + }).start(next: { next in + subscriber.putNext(next) + }, error: { error in + subscriber.putError(error) + }, completed: { + subscriber.putCompletion() + }) + + return ActionDisposable { + fetchThumbnail.dispose() + disposable.dispose() + } + } + } } } -func fileInteractiveFetched(account: Account, file: TelegramMediaFile) -> Signal { - return account.postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .file)) |> map {_ in} -} - -func fileCancelInteractiveFetch(account: Account, file: TelegramMediaFile) { - account.postbox.mediaBox.cancelInteractiveResourceFetch(file.resource) -} - - -public func blurImage(_ data:Data?, _ s:NSSize, cornerRadius:CGFloat = 0) -> CGImage? { - - var thumbnailImage: CGImage? - if let idata = data, let imageSource = CGImageSourceCreateWithData(idata as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { - thumbnailImage = image - } - var blurredThumbnailImage: CGImage? - if let thumbnailImage = thumbnailImage { - let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) - let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 300.0, height: 300.0)) - let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) - thumbnailContext.withContext { ctx in - ctx.interpolationQuality = .none +public func chatMessageAnimatedSticker(postbox: Postbox, file: FileMediaReference, small: Bool, scale: CGFloat, size: NSSize, fetched: Bool = false, onlyFullSize: Bool = false, synchronousLoad: Bool = false) -> Signal { + let signal: Signal = chatMessageAnimatedStickerDatas(postbox: postbox, file: file, small: small, fetched: fetched, onlyFullSize: onlyFullSize, synchronousLoad: synchronousLoad, size: size) + return signal |> map { data in + return ImageDataTransformation(data: data, execute: { arguments, data in + let context = DrawingContext(size: arguments.drawingSize, scale: scale, clear: arguments.emptyColor == nil) - ctx.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) - } - telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) - - blurredThumbnailImage = thumbnailContext.generateImage() - - if cornerRadius > 0 { + let drawingRect = arguments.drawingRect + let fittedSize = arguments.imageSize + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + //let fittedRect = arguments.drawingRect - let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 2.0) - - thumbnailContext.withContext({ (ctx) in - let minx:CGFloat = 0, midx = thumbnailContextSize.width/2.0, maxx = thumbnailContextSize.width - let miny:CGFloat = 0, midy = thumbnailContextSize.height/2.0, maxy = thumbnailContextSize.height + var fullSizeImage: CGImage? + if let fullSizeData = data.fullSizeData, data.fullSizeComplete { + if let image = NSImage(data: fullSizeData)?._cgImage { + fullSizeImage = image + } + } + + var thumbnailImage: CGImage? + if fullSizeImage == nil, let thumbnailData = data.thumbnailData { + if let image = NSImage(data: thumbnailData)?.cgImage(forProposedRect: nil, context: nil, hints: nil) { + thumbnailImage = image + } + } + + var blurredThumbnailImage: CGImage? + let thumbnailInset: CGFloat = 10.0 + if let thumbnailImage = thumbnailImage { + let thumbnailSize = thumbnailImage.size + var thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 150.0, height: 150.0)) + let thumbnailDrawingSize = thumbnailContextSize + thumbnailContextSize.width += thumbnailInset * 2.0 + thumbnailContextSize.height += thumbnailInset * 2.0 + let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0, clear: true) + thumbnailContext.withFlippedContext(isHighQuality: false, { c in + let cgImage = thumbnailImage + c.setBlendMode(.normal) + c.interpolationQuality = .medium + + c.draw(cgImage, in: CGRect(origin: CGPoint(x: thumbnailInset, y: thumbnailInset), size: thumbnailDrawingSize)) + }) + stickerThumbnailAlphaBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) - ctx.move(to: NSMakePoint(minx, midy)) - ctx.addArc(tangent1End: NSMakePoint(minx, miny), tangent2End: NSMakePoint(midx, miny), radius: cornerRadius) - ctx.addArc(tangent1End: NSMakePoint(maxx, miny), tangent2End: NSMakePoint(maxx, midy), radius: cornerRadius) - ctx.addArc(tangent1End: NSMakePoint(maxx, maxy), tangent2End: NSMakePoint(midx, maxy), radius: cornerRadius) - ctx.addArc(tangent1End: NSMakePoint(minx, maxy), tangent2End: NSMakePoint(minx, midy), radius: cornerRadius) + blurredThumbnailImage = thumbnailContext.generateImage() + } + + context.withFlippedContext(isHighQuality: data.fullSizeData != nil, { c in + if let color = arguments.emptyColor { + c.setBlendMode(.normal) + switch color { + case let .color(color): + c.setFillColor(color.cgColor) + default: + break + } + c.fill(drawingRect) + } else { + c.setBlendMode(.copy) + } - ctx.closePath() - ctx.clip() - - ctx.draw(blurredThumbnailImage!, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) + if let blurredThumbnailImage = blurredThumbnailImage, fullSizeImage == nil { + c.interpolationQuality = .low + let thumbnailScaledInset = thumbnailInset * (fittedRect.width / blurredThumbnailImage.size.width) + c.draw(blurredThumbnailImage, in: fittedRect.insetBy(dx: -thumbnailScaledInset, dy: -thumbnailScaledInset)) + } + if let fullSizeImage = fullSizeImage { + let cgImage = fullSizeImage + c.setBlendMode(.normal) + c.interpolationQuality = .medium + + c.draw(cgImage, in: fittedRect) + + // c.setFillColor(NSColor.random.cgColor) + // c.fill(fittedRect) + } }) - blurredThumbnailImage = thumbnailContext.generateImage() - } + return context + }) } - - return blurredThumbnailImage } - -func chatMessageVideo(account: Account, video: TelegramMediaFile, scale:CGFloat) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatMessageFileDatas(account: account, file: video) +func chatMessageSlotSticker(postbox: Postbox, value: SlotMachineValue, scale: CGFloat, size: NSSize, synchronousLoad: Bool = false) -> Signal { - return signal |> map { (thumbnailData, fullSizeDataAndPath, fullSizeComplete) in - return { arguments in - assertNotOnMainThread() - let context = DrawingContext(size: arguments.drawingSize, scale:scale, clear: true) - if arguments.drawingSize.width.isLessThanOrEqualTo(0.0) || arguments.drawingSize.height.isLessThanOrEqualTo(0.0) { - return context - } + let signal: Signal = postbox.mediaBox.cachedResourceRepresentation(LocalFileReferenceMediaResource(localFilePath: "", randomId: 0), representation: CachedSlotMachineRepresentation(value: value, size: size), complete: true, fetch: true, attemptSynchronously: synchronousLoad) |> map { data in + if data.complete { + return ImageRenderData(nil, try? Data(contentsOf: URL(fileURLWithPath: data.path)), true) + } else { + return ImageRenderData(nil, nil, false) + } + } |> runOn(synchronousLoad ? .mainQueue() : resourcesQueue) + + return signal |> map { data in + return ImageDataTransformation(data: data, execute: { arguments, data in + let context = DrawingContext(size: arguments.drawingSize, scale: scale, clear: arguments.emptyColor == nil) let drawingRect = arguments.drawingRect - let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) + let fittedSize = arguments.imageSize let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + //let fittedRect = arguments.drawingRect var fullSizeImage: CGImage? - if let fullSizeDataAndPath = fullSizeDataAndPath { - if fullSizeComplete { - if video.mimeType.hasPrefix("video/") { - let tempFilePath = NSTemporaryDirectory() + "\(fullSizeDataAndPath.nsstring.lastPathComponent).mov" - - _ = try? FileManager.default.removeItem(atPath: tempFilePath) - _ = try? FileManager.default.linkItem(atPath: fullSizeDataAndPath, toPath: tempFilePath) - - let asset = AVAsset(url: URL(fileURLWithPath: tempFilePath)) - let imageGenerator = AVAssetImageGenerator(asset: asset) - imageGenerator.maximumSize = CGSize(width: 800.0, height: 800.0) - imageGenerator.appliesPreferredTrackTransform = true - - - if let image = try? imageGenerator.copyCGImage(at: CMTime(seconds: 0.0, preferredTimescale: asset.duration.timescale), actualTime: nil) { - fullSizeImage = image - } - - } - /*let options: [NSString: NSObject] = [ - kCGImageSourceThumbnailMaxPixelSize: max(fittedSize.width * context.scale, fittedSize.height * context.scale), - kCGImageSourceCreateThumbnailFromImageAlways: true - ] - if let imageSource = CGImageSourceCreateWithData(fullSizeData, nil), image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options) { - fullSizeImage = image - }*/ - } else { - /*let imageSource = CGImageSourceCreateIncremental(nil) - CGImageSourceUpdateData(imageSource, fullSizeData as CFDataRef, fullSizeData.length >= fullTotalSize) - - var options: [NSString : NSObject!] = [:] - options[kCGImageSourceShouldCache as NSString] = false as NSNumber - if let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionaryRef) { - fullSizeImage = image - }*/ + if let fullSizeData = data.fullSizeData, data.fullSizeComplete { + if let image = NSImage(data: fullSizeData)?.cgImage(forProposedRect: nil, context: nil, hints: nil) { + fullSizeImage = image } } var thumbnailImage: CGImage? - if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { - thumbnailImage = image + if fullSizeImage == nil, let thumbnailData = data.thumbnailData { + if let image = NSImage(data: thumbnailData)?.cgImage(forProposedRect: nil, context: nil, hints: nil) { + thumbnailImage = image + } } var blurredThumbnailImage: CGImage? + let thumbnailInset: CGFloat = 10.0 if let thumbnailImage = thumbnailImage { - let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) - let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 150.0, height: 150.0)) - let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) - thumbnailContext.withContext { c in - c.interpolationQuality = .none - c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) - } - telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + let thumbnailSize = thumbnailImage.size + var thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 150.0, height: 150.0)) + let thumbnailDrawingSize = thumbnailContextSize + thumbnailContextSize.width += thumbnailInset * 2.0 + thumbnailContextSize.height += thumbnailInset * 2.0 + let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0, clear: true) + thumbnailContext.withFlippedContext(isHighQuality: false, { c in + let cgImage = thumbnailImage + c.setBlendMode(.normal) + c.interpolationQuality = .medium + + c.draw(cgImage, in: CGRect(origin: CGPoint(x: thumbnailInset, y: thumbnailInset), size: thumbnailDrawingSize)) + }) + stickerThumbnailAlphaBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) blurredThumbnailImage = thumbnailContext.generateImage() } - context.withContext { c in - c.setBlendMode(.copy) - if arguments.boundingSize != arguments.imageSize { - c.fill(arguments.drawingRect) + context.withFlippedContext(isHighQuality: data.fullSizeData != nil, { c in + if let color = arguments.emptyColor { + c.setBlendMode(.normal) + switch color { + case let .color(color): + c.setFillColor(color.cgColor) + default: + break + } + c.fill(drawingRect) + } else { + c.setBlendMode(.copy) } - c.setBlendMode(.copy) - if let blurredThumbnailImage = blurredThumbnailImage { + if let blurredThumbnailImage = blurredThumbnailImage, fullSizeImage == nil { c.interpolationQuality = .low - c.draw(blurredThumbnailImage, in: fittedRect) + let thumbnailScaledInset = thumbnailInset * (fittedRect.width / blurredThumbnailImage.size.width) + c.draw(blurredThumbnailImage, in: fittedRect.insetBy(dx: -thumbnailScaledInset, dy: -thumbnailScaledInset)) } if let fullSizeImage = fullSizeImage { + let cgImage = fullSizeImage c.setBlendMode(.normal) c.interpolationQuality = .medium - c.draw(fullSizeImage, in: fittedRect) + + c.draw(cgImage, in: fittedRect) } - } - - addCorners(context, arguments: arguments, scale:scale) + }) return context - } + }) } } - -private func chatSecretMessageVideoData(account: Account, file: TelegramMediaFile) -> Signal { - if let smallestRepresentation = smallestImageRepresentation(file.previewRepresentations) { - let thumbnailResource = smallestRepresentation.resource - - let fetchedThumbnail = account.postbox.mediaBox.fetchedResource(thumbnailResource, tag: TelegramMediaResourceFetchTag(statsCategory: .video)) - - let thumbnail = Signal { subscriber in - let fetchedDisposable = fetchedThumbnail.start() - let thumbnailDisposable = account.postbox.mediaBox.resourceData(thumbnailResource).start(next: { next in - subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) - }, error: subscriber.putError, completed: subscriber.putCompletion) - - return ActionDisposable { - fetchedDisposable.dispose() - thumbnailDisposable.dispose() - } +public func chatMessageDiceSticker(postbox: Postbox, file: TelegramMediaFile, emoji: String, value: String, scale: CGFloat, size: NSSize, synchronousLoad: Bool = false) -> Signal { + + + let signal: Signal = postbox.mediaBox.cachedResourceRepresentation(file.resource, representation: CachedDiceRepresentation(emoji: emoji, value: value, size: size), complete: true, fetch: true, attemptSynchronously: synchronousLoad) |> map { data in + if data.complete { + return ImageRenderData(nil, try? Data(contentsOf: URL(fileURLWithPath: data.path)), true) + } else { + return ImageRenderData(nil, nil, false) } - return thumbnail - } else { - return .single(nil) - } -} - -func chatSecretMessageVideo(account: Account, video: TelegramMediaFile, scale:CGFloat) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatSecretMessageVideoData(account: account, file: video) + } |> runOn(synchronousLoad ? .mainQueue() : resourcesQueue) - return signal |> map { thumbnailData in - return { arguments in - let context = DrawingContext(size: arguments.drawingSize, scale: scale, clear: true) - if arguments.drawingSize.width.isLessThanOrEqualTo(0.0) || arguments.drawingSize.height.isLessThanOrEqualTo(0.0) { - return context + return signal |> map { data in + return ImageDataTransformation(data: data, execute: { arguments, data in + let context = DrawingContext(size: arguments.drawingSize, scale: scale, clear: arguments.emptyColor == nil) + + let drawingRect = arguments.drawingRect + let fittedSize = arguments.imageSize + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + //let fittedRect = arguments.drawingRect + + var fullSizeImage: CGImage? + if let fullSizeData = data.fullSizeData, data.fullSizeComplete { + if let image = NSImage(data: fullSizeData)?.cgImage(forProposedRect: nil, context: nil, hints: nil) { + fullSizeImage = image + } + } + + var thumbnailImage: CGImage? + if fullSizeImage == nil, let thumbnailData = data.thumbnailData { + if let image = NSImage(data: thumbnailData)?.cgImage(forProposedRect: nil, context: nil, hints: nil) { + thumbnailImage = image + } + } + + var blurredThumbnailImage: CGImage? + let thumbnailInset: CGFloat = 10.0 + if let thumbnailImage = thumbnailImage { + let thumbnailSize = thumbnailImage.size + var thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 150.0, height: 150.0)) + let thumbnailDrawingSize = thumbnailContextSize + thumbnailContextSize.width += thumbnailInset * 2.0 + thumbnailContextSize.height += thumbnailInset * 2.0 + let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0, clear: true) + thumbnailContext.withFlippedContext(isHighQuality: false, { c in + let cgImage = thumbnailImage + c.setBlendMode(.normal) + c.interpolationQuality = .medium + + c.draw(cgImage, in: CGRect(origin: CGPoint(x: thumbnailInset, y: thumbnailInset), size: thumbnailDrawingSize)) + }) + stickerThumbnailAlphaBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + + blurredThumbnailImage = thumbnailContext.generateImage() + } + + context.withFlippedContext(isHighQuality: data.fullSizeData != nil, { c in + if let color = arguments.emptyColor { + c.setBlendMode(.normal) + switch color { + case let .color(color): + c.setFillColor(color.cgColor) + default: + break + } + c.fill(drawingRect) + } else { + c.setBlendMode(.copy) + } + + if let blurredThumbnailImage = blurredThumbnailImage, fullSizeImage == nil { + c.interpolationQuality = .low + let thumbnailScaledInset = thumbnailInset * (fittedRect.width / blurredThumbnailImage.size.width) + c.draw(blurredThumbnailImage, in: fittedRect.insetBy(dx: -thumbnailScaledInset, dy: -thumbnailScaledInset)) + } + + if let fullSizeImage = fullSizeImage { + let cgImage = fullSizeImage + c.setBlendMode(.normal) + c.interpolationQuality = .medium + + c.draw(cgImage, in: fittedRect) + } + }) + + return context + }) + } +} + + +private func chatMessageStickerPackThumbnailData(postbox: Postbox, representation: TelegramMediaImageRepresentation, synchronousLoad: Bool) -> Signal { + let resource = representation.resource + + let maybeFetched = postbox.mediaBox.cachedResourceRepresentation(resource, representation: CachedStickerAJpegRepresentation(size: CGSize(width: 120.0, height: 120.0)), complete: false, fetch: false, attemptSynchronously: synchronousLoad) + + return maybeFetched + |> take(1) + |> mapToSignal { maybeData in + if maybeData.complete { + let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) + return .single(loadedData) + } else { + let fullSizeData = postbox.mediaBox.cachedResourceRepresentation(resource, representation: CachedStickerAJpegRepresentation(size: CGSize(width: 120.0, height: 120.0)), complete: false) + |> map { next in + return (!next.complete ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe), next.complete) + } + + return Signal { subscriber in + let fetch: Disposable? = nil + let disposable = fullSizeData.start(next: { next in + subscriber.putNext(next.0) + }, error: { error in + subscriber.putError(error) + }, completed: { + subscriber.putCompletion() + }) + + return ActionDisposable { + fetch?.dispose() + disposable.dispose() + } + } + } + } +} + + + + +public func chatMessageStickerPackThumbnail(postbox: Postbox, representation: TelegramMediaImageRepresentation, scale: CGFloat, synchronousLoad: Bool = false) -> Signal { + let signal = chatMessageStickerPackThumbnailData(postbox: postbox, representation: representation, synchronousLoad: synchronousLoad) + + return signal + |> map { fullSizeData in + return ImageDataTransformation(data: ImageRenderData.init(nil, fullSizeData, fullSizeData != nil), execute: { arguments, data in + let context = DrawingContext(size: arguments.drawingSize, scale: scale, clear: arguments.emptyColor == nil) + + let drawingRect = arguments.drawingRect + let fittedSize = arguments.imageSize + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + + var fullSizeImage: CGImage? + if let fullSizeData = fullSizeData { + if let image = NSImage(data: fullSizeData)?.cgImage(forProposedRect: nil, context: nil, hints: nil) { + fullSizeImage = image + } + } + + context.withFlippedContext(isHighQuality: fullSizeImage != nil, { c in + if let color = arguments.emptyColor { + c.setBlendMode(.normal) + switch color { + case let .color(color): + c.setFillColor(color.cgColor) + default: + break + } + c.fill(drawingRect) + } else { + c.setBlendMode(.copy) + } + + if let fullSizeImage = fullSizeImage { + let cgImage = fullSizeImage + c.setBlendMode(.normal) + c.interpolationQuality = .medium + c.draw(cgImage, in: fittedRect) + } + }) + + return context + }) + } +} + + + +func chatWebpageSnippetPhotoData(account: Account, imageRefence: ImageMediaReference, small:Bool) -> Signal { + if let closestRepresentation = (small ? imageRefence.media.representationForDisplayAtSize(PixelDimensions(120, 120)) : largestImageRepresentation(imageRefence.media.representations)) { + let resourceData = account.postbox.mediaBox.resourceData(closestRepresentation.resource) |> map { next in + return !next.complete ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe) + } + + return Signal { subscriber in + let disposable = DisposableSet() + disposable.add(resourceData.start(next: { data in + subscriber.putNext(data) + }, error: { error in + subscriber.putError(error) + }, completed: { + subscriber.putCompletion() + })) + //account.postbox.mediaBox.fetchedResource(closestRepresentation.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .file) + + disposable.add(fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: imageRefence.resourceReference(closestRepresentation.resource), statsCategory: .file).start()) + return disposable + } + } else { + return .never() + } +} + +func chatWebpageSnippetPhoto(account: Account, imageReference: ImageMediaReference, scale:CGFloat, small:Bool, synchronousLoad: Bool = false, secureIdAccessContext: SecureIdAccessContext? = nil) -> Signal { + let signal = chatMessagePhotoDatas(postbox: account.postbox, imageReference: imageReference, synchronousLoad: synchronousLoad, secureIdAccessContext: secureIdAccessContext) + return signal |> map { data in + return ImageDataTransformation(data: data, execute: { arguments, data in + let context = DrawingContext(size: arguments.drawingSize, scale:scale, clear: true) + + let fullSizeData = data.fullSizeData + let thumbnailData = data.thumbnailData + + let drawingRect = arguments.drawingRect + var fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize) + switch arguments.resizeMode { + case .none: + break + default: + fittedSize = fittedSize.fitted(arguments.imageSize) + } + var fittedRect = CGRect(origin: CGPoint(x: floorToScreenPixels(System.backingScale, drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0), y: floorToScreenPixels(System.backingScale, drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0)), size: fittedSize) + // + // let drawingRect = arguments.drawingRect + // var fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) + // var fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + + var fullSizeImage: CGImage? + if let fullSizeData = fullSizeData { + if data.fullSizeComplete { + let options = NSMutableDictionary() + // options.setValue(max(fittedSize.width * context.scale, fittedSize.height * context.scale) as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) + if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, options), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) { + fullSizeImage = image + switch arguments.resizeMode { + case .none: + fittedSize = image.backingSize.aspectFilled(arguments.boundingSize)//.fitted(image.backingSize) + fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + default: + break + } + } + } else { + let imageSource = CGImageSourceCreateIncremental(nil) + CGImageSourceUpdateData(imageSource, fullSizeData as CFData, data.fullSizeComplete) + + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + if let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { + fullSizeImage = image + switch arguments.resizeMode { + case .none: + fittedSize = image.backingSize.aspectFilled(arguments.boundingSize)//.fitted(image.backingSize) + fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + default: + break + } + } + } + } + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + + + var thumbnailImage: CGImage? + if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, options), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options) { + thumbnailImage = image + } + + var blurredThumbnailImage: CGImage? + if let thumbnailImage = thumbnailImage { + let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) + let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 150.0, height: 150.0)) + let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) + thumbnailContext.withContext { c in + c.interpolationQuality = .none + c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) + } + telegramFastBlurMore(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + + blurredThumbnailImage = thumbnailContext.generateImage() + } + + context.withFlippedContext(isHighQuality: data.fullSizeData != nil, { c in + c.setBlendMode(.copy) + + if arguments.boundingSize != arguments.imageSize { + switch arguments.resizeMode { + case .blurBackground: + let blurSourceImage = thumbnailImage ?? fullSizeImage + + if let fullSizeImage = blurSourceImage { + let thumbnailSize = CGSize(width: fullSizeImage.width, height: fullSizeImage.height) + let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 74.0, height: 74.0)) + let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) + thumbnailContext.withFlippedContext { c in + c.interpolationQuality = .none + c.draw(fullSizeImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) + } + telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + + if let blurredImage = thumbnailContext.generateImage() { + let filledSize = thumbnailSize.aspectFilled(arguments.drawingRect.size) + c.interpolationQuality = .low + c.draw(blurredImage, in: CGRect(origin: CGPoint(x: arguments.drawingRect.minX + (arguments.drawingRect.width - filledSize.width) / 2.0, y: arguments.drawingRect.minY + (arguments.drawingRect.height - filledSize.height) / 2.0), size: filledSize)) + c.setBlendMode(.normal) + c.setFillColor(theme.colors.background.withAlphaComponent(0.5).cgColor) + c.fill(arguments.drawingRect) + c.setBlendMode(.copy) + } + } else { + c.setBlendMode(.normal) + c.setFillColor(theme.colors.grayForeground.cgColor) + c.fill(arguments.drawingRect) + } + case let .fill(color): + c.setBlendMode(.normal) + c.setFillColor(color.cgColor) + c.fill(arguments.drawingRect) + case .fillTransparent: + c.setBlendMode(.normal) + c.setFillColor(theme.colors.transparentBackground.cgColor) + c.fill(arguments.drawingRect) + case .none: + c.setBlendMode(.normal) + c.setFillColor(theme.colors.grayForeground.cgColor) + c.fill(arguments.drawingRect) + case .imageColor: + break + } + } + + c.setBlendMode(.copy) + if let cgImage = blurredThumbnailImage { + c.interpolationQuality = .low + c.draw(cgImage, in: fittedRect) + c.setBlendMode(.normal) + } + + if let fullSizeImage = fullSizeImage { + c.interpolationQuality = .medium + c.draw(fullSizeImage, in: fittedRect) + } + + if blurredThumbnailImage == nil && fullSizeImage == nil && arguments.boundingSize == arguments.imageSize { + c.setBlendMode(.normal) + c.setFillColor(theme.colors.grayForeground.cgColor) + c.fill(arguments.drawingRect) + } + }) + + addCorners(context, arguments: arguments, scale:scale) + + return context + }) + } +} + + + +func chatMessagePhotoStatus(account: Account, photo: TelegramMediaImage, approximateSynchronousValue: Bool = false) -> Signal { + if let largestRepresentation = photo.representationForDisplayAtSize(PixelDimensions(1280, 1280)) { + if largestRepresentation.resource is LocalFileReferenceMediaResource { + return .single(.Local) + } else { + return account.postbox.mediaBox.resourceStatus(largestRepresentation.resource, approximateSynchronousValue: approximateSynchronousValue) + } + } else { + return .never() + } +} + +func chatMessagePhotoInteractiveFetched(account: Account, imageReference: ImageMediaReference, toRepresentationSize: NSSize = NSMakeSize(1280, 1280)) -> Signal { + if let largestRepresentation = imageReference.media.representationForDisplayAtSize(PixelDimensions(toRepresentationSize)) { + return fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: imageReference.resourceReference(largestRepresentation.resource), statsCategory: .image) |> `catch` { _ in return .complete() } |> map {_ in} + } else { + return .never() + } +} + +func chatMessagePhotoCancelInteractiveFetch(account: Account, photo: TelegramMediaImage) { + if let largestRepresentation = largestRepresentationForPhoto(photo) { + return account.postbox.mediaBox.cancelInteractiveResourceFetch(largestRepresentation.resource) + } +} + +func fileInteractiveFetched(account: Account, fileReference: FileMediaReference) -> Signal { + return fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: fileReference.resourceReference(fileReference.media.resource), statsCategory: .file) |> `catch` { _ in return .complete() } |> map {_ in} //account.postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .file)) |> map {_ in} +} + +func fileCancelInteractiveFetch(account: Account, file: TelegramMediaFile) { + account.postbox.mediaBox.cancelInteractiveResourceFetch(file.resource) +} + + +public func blurImage(_ data:Data?, _ s:NSSize, cornerRadius:CGFloat = 0) -> CGImage? { + + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + + + var thumbnailImage: CGImage? + if let idata = data, let imageSource = CGImageSourceCreateWithData(idata as CFData, options), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options) { + thumbnailImage = image + } + var blurredThumbnailImage: CGImage? + + if let thumbnailImage = thumbnailImage { + let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) + let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 300.0, height: 300.0)) + let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) + thumbnailContext.withContext { ctx in + ctx.interpolationQuality = .none + + ctx.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) + } + telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + + blurredThumbnailImage = thumbnailContext.generateImage() + + if cornerRadius > 0 { + + let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 2.0) + + thumbnailContext.withContext({ (ctx) in + let minx:CGFloat = 0, midx = thumbnailContextSize.width/2.0, maxx = thumbnailContextSize.width + let miny:CGFloat = 0, midy = thumbnailContextSize.height/2.0, maxy = thumbnailContextSize.height + + ctx.move(to: NSMakePoint(minx, midy)) + ctx.addArc(tangent1End: NSMakePoint(minx, miny), tangent2End: NSMakePoint(midx, miny), radius: cornerRadius) + ctx.addArc(tangent1End: NSMakePoint(maxx, miny), tangent2End: NSMakePoint(maxx, midy), radius: cornerRadius) + ctx.addArc(tangent1End: NSMakePoint(maxx, maxy), tangent2End: NSMakePoint(midx, maxy), radius: cornerRadius) + ctx.addArc(tangent1End: NSMakePoint(minx, maxy), tangent2End: NSMakePoint(minx, midy), radius: cornerRadius) + + ctx.closePath() + ctx.clip() + + ctx.draw(blurredThumbnailImage!, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) + + }) + + blurredThumbnailImage = thumbnailContext.generateImage() + } + } + + return blurredThumbnailImage +} + + + +private func chatMessageVideoDatas(postbox: Postbox, fileReference: FileMediaReference, thumbnailSize: Bool = false, onlyFullSize: Bool = false, synchronousLoad: Bool = false) -> Signal { + + let fetchedFullSize = postbox.mediaBox.cachedResourceRepresentation(fileReference.media.resource, representation: thumbnailSize ? CachedScaledVideoFirstFrameRepresentation(size: CGSize(width: 160.0, height: 160.0)) : CachedVideoFirstFrameRepresentation(), complete: false, fetch: true, attemptSynchronously: synchronousLoad) + + let maybeFullSize = postbox.mediaBox.cachedResourceRepresentation(fileReference.media.resource, representation: thumbnailSize ? CachedScaledVideoFirstFrameRepresentation(size: CGSize(width: 160.0, height: 160.0)) : CachedVideoFirstFrameRepresentation(), complete: false, fetch: false, attemptSynchronously: synchronousLoad) + + let signal = maybeFullSize + |> take(1) + |> mapToSignal { maybeData -> Signal in + if maybeData.complete { + let loadedData: Data? + loadedData = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) + return .single(ImageRenderData(nil, loadedData, true)) + } else { + let decodedThumbnailData = fileReference.media.immediateThumbnailData.flatMap(decodeTinyThumbnail) + let fetchedThumbnail: Signal + if let smallestRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { + fetchedThumbnail = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: fileReference.resourceReference(smallestRepresentation.resource), statsCategory: .image) |> `catch` { _ in return .complete() } + } else { + fetchedThumbnail = .complete() + } + + let mainThumbnail = Signal { subscriber in + if let decodedThumbnailData = decodedThumbnailData { + subscriber.putNext(ImageRenderData(decodedThumbnailData, nil, true)) + } + + let fetchedDisposable = fetchedThumbnail.start() + var thumbnailDisposable: Disposable? = nil + if let smallestRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { + thumbnailDisposable = postbox.mediaBox.resourceData(smallestRepresentation.resource, attemptSynchronously: synchronousLoad).start(next: { next in + subscriber.putNext(ImageRenderData(!next.complete ? decodedThumbnailData : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), nil, !next.complete)) + }, error: subscriber.putError, completed: subscriber.putCompletion) + } else if decodedThumbnailData == nil { + subscriber.putNext(ImageRenderData(nil, nil, true)) + } + + + return ActionDisposable { + fetchedDisposable.dispose() + thumbnailDisposable?.dispose() + } + } + + let thumbnail = mainThumbnail + + let fullSizeData: Signal + + fullSizeData = fetchedFullSize + |> map { next -> ImageRenderData in + return ImageRenderData(nil, next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete) + } + + return thumbnail + |> mapToSignal { thumbnailData in + let isThumb = thumbnailData.fullSizeComplete + return fullSizeData + |> map { fullSizeData in + if !isThumb && !fullSizeData.fullSizeComplete { + return ImageRenderData(nil, thumbnailData.thumbnailData, false) + } else { + return ImageRenderData(fullSizeData.fullSizeComplete ? nil : thumbnailData.thumbnailData, fullSizeData.fullSizeData, fullSizeData.fullSizeComplete) + } + } + } + } + } + return signal + + +// let image = TelegramMediaImage(imageId: fileReference.media.id ?? MediaId(namespace: 0, id: 0), representations: fileReference.media.previewRepresentations, immediateThumbnailData: fileReference.media.immediateThumbnailData, reference: nil, partialReference: fileReference.media.partialReference) +// let imageReference: ImageMediaReference +// switch fileReference { +// case let .message(message, _): +// imageReference = ImageMediaReference.message(message: message, media: image) +// case .savedGif: +// imageReference = ImageMediaReference.savedGif(media: image) +// case .standalone: +// imageReference = ImageMediaReference.standalone(media: image) +// case let .stickerPack(stickerPack, _): +// imageReference = ImageMediaReference.stickerPack(stickerPack: stickerPack, media: image) +// case let .webPage(webPage, _): +// imageReference = ImageMediaReference.webPage(webPage: webPage, media: image) +// } +// +// return chatMessagePhotoDatas(postbox: postbox, imageReference: imageReference, autoFetchFullSize: true, synchronousLoad: synchronousLoad) +// +// let fullSizeResource = fileReference.media.resource +// +// let thumbnailResource = smallestImageRepresentation(fileReference.media.previewRepresentations)?.resource +// +// let maybeFullSize = postbox.mediaBox.cachedResourceRepresentation(fullSizeResource, representation: thumbnailSize ? CachedScaledVideoFirstFrameRepresentation(size: CGSize(width: 160.0, height: 160.0)) : CachedVideoFirstFrameRepresentation(), complete: false, fetch: false, attemptSynchronously: synchronousLoad) +// let fetchedFullSize = postbox.mediaBox.cachedResourceRepresentation(fullSizeResource, representation: thumbnailSize ? CachedScaledVideoFirstFrameRepresentation(size: CGSize(width: 160.0, height: 160.0)) : CachedVideoFirstFrameRepresentation(), complete: false, fetch: true, attemptSynchronously: synchronousLoad) +// +// let signal = maybeFullSize +// |> take(1) +// |> mapToSignal { maybeData -> Signal<(Atomic, (Atomic, String)?, Bool), NoError> in +// if maybeData.complete { +// let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) +// +// return .single((Atomic(value: nil), loadedData == nil ? nil : (Atomic(value: loadedData!), maybeData.path), true)) +// } else { +// let thumbnail: Signal, NoError> +// if onlyFullSize { +// thumbnail = .single(Atomic(value: nil)) +// } else if let decodedThumbnailData = fileReference.media.immediateThumbnailData.flatMap(decodeTinyThumbnail) { +// thumbnail = .single(Atomic(value: decodedThumbnailData)) +// } else if let thumbnailResource = thumbnailResource { +// thumbnail = Signal { subscriber in +// let fetchedDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: fileReference.resourceReference(thumbnailResource), statsCategory: .video).start() +// let thumbnailDisposable = postbox.mediaBox.resourceData(thumbnailResource, attemptSynchronously: synchronousLoad).start(next: { next in +// subscriber.putNext(Atomic(value: next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []))) +// }, error: subscriber.putError, completed: subscriber.putCompletion) +// +// return ActionDisposable { +// fetchedDisposable.dispose() +// thumbnailDisposable.dispose() +// } +// } +// } else { +// thumbnail = .single(Atomic(value: nil)) +// } +// +// let fullSizeDataAndPath = Signal { subscriber in +// let dataDisposable = fetchedFullSize.start(next: { next in +// subscriber.putNext(next) +// }, completed: { +// subscriber.putCompletion() +// }) +// //let fetchedDisposable = fetchedPartialVideoThumbnailData(postbox: postbox, fileReference: fileReference).start() +// return ActionDisposable { +// dataDisposable.dispose() +// //fetchedDisposable.dispose() +// } +// } |> map { next -> ((Atomic, String)?, Bool) in +// let data = next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe) +// return (data == nil ? nil : (Atomic(value: data!), next.path), next.complete) +// } +// +// return thumbnail +// |> mapToSignal { thumbnailData in +// return fullSizeDataAndPath +// |> map { (dataAndPath, complete) in +// return (thumbnailData, dataAndPath, complete) +// } +// } +// } +// } |> filter({ +// if onlyFullSize { +// return $0.1 != nil || $0.2 +// } else { +// return true//$0.0 != nil || $0.1 != nil || $0.2 +// } +// }) +// +// return signal +} + + + +func chatMessageVideo(postbox: Postbox, fileReference: FileMediaReference, scale: CGFloat, synchronousLoad: Bool = false) -> Signal { + return mediaGridMessageVideo(postbox: postbox, fileReference: fileReference, scale: scale, synchronousLoad: synchronousLoad) +} + + +private func chatSecretMessageVideoData(account: Account, fileReference: FileMediaReference, synchronousLoad: Bool = false) -> Signal { + if let smallestRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) { + let thumbnailResource = smallestRepresentation.resource + + let fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: fileReference.resourceReference(thumbnailResource), statsCategory: .video) + + let thumbnail = Signal { subscriber in + let fetchedDisposable = fetchedThumbnail.start() + let thumbnailDisposable = account.postbox.mediaBox.resourceData(thumbnailResource, attemptSynchronously: synchronousLoad).start(next: { next in + subscriber.putNext(ImageRenderData(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path)), nil, true)) + }, error: subscriber.putError, completed: subscriber.putCompletion) + + return ActionDisposable { + fetchedDisposable.dispose() + thumbnailDisposable.dispose() + } + } + return thumbnail + } else { + return .single(ImageRenderData(nil, nil, false)) + } +} + +func chatSecretMessageVideo(account: Account, fileReference: FileMediaReference, scale:CGFloat, synchronousLoad: Bool = false) -> Signal { + let signal = chatSecretMessageVideoData(account: account, fileReference: fileReference, synchronousLoad: synchronousLoad) + + return signal |> map { data in + return ImageDataTransformation(data: data, execute: { arguments, data in + let context = DrawingContext(size: arguments.drawingSize, scale: scale, clear: true) + if arguments.drawingSize.width.isLessThanOrEqualTo(0.0) || arguments.drawingSize.height.isLessThanOrEqualTo(0.0) { + return context } + let thumbnailData = data.thumbnailData + let drawingRect = arguments.drawingRect let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) - /*var fullSizeImage: CGImage? - if let fullSizeDataAndPath = fullSizeDataAndPath { - if fullSizeComplete { - if video.mimeType.hasPrefix("video/") { - let tempFilePath = NSTemporaryDirectory() + "\(arc4random()).mov" - - _ = try? FileManager.default.removeItem(atPath: tempFilePath) - _ = try? FileManager.default.linkItem(atPath: fullSizeDataAndPath.1, toPath: tempFilePath) - - let asset = AVAsset(url: URL(fileURLWithPath: tempFilePath)) - let imageGenerator = AVAssetImageGenerator(asset: asset) - imageGenerator.maximumSize = CGSize(width: 800.0, height: 800.0) - imageGenerator.appliesPreferredTrackTransform = true - if let image = try? imageGenerator.copyCGImage(at: CMTime(seconds: 0.0, preferredTimescale: asset.duration.timescale), actualTime: nil) { - fullSizeImage = image - } - } - /*let options: [NSString: NSObject] = [ - kCGImageSourceThumbnailMaxPixelSize: max(fittedSize.width * context.scale, fittedSize.height * context.scale), - kCGImageSourceCreateThumbnailFromImageAlways: true - ] - if let imageSource = CGImageSourceCreateWithData(fullSizeData, nil), image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options) { - fullSizeImage = image - }*/ - } else { - /*let imageSource = CGImageSourceCreateIncremental(nil) - CGImageSourceUpdateData(imageSource, fullSizeData as CFDataRef, fullSizeData.length >= fullTotalSize) - - var options: [NSString : NSObject!] = [:] - options[kCGImageSourceShouldCache as NSString] = false as NSNumber - if let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionaryRef) { - fullSizeImage = image - }*/ - } - }*/ var blurredImage: CGImage? if blurredImage == nil { - if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + + if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, options), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options) { let thumbnailSize = CGSize(width: image.width, height: image.height) let thumbnailContextSize = thumbnailSize.aspectFilled(CGSize(width: 20.0, height: 20.0)) let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) @@ -826,10 +1828,10 @@ func chatSecretMessageVideo(account: Account, video: TelegramMediaFile, scale:CG } } - context.withContext { c in + context.withContext({ c in c.setBlendMode(.copy) if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { - //c.setFillColor(UIColor(white: 0.0, alpha: 0.4).cgColor) + //c.setFillColor(NSColor(white: 0.0, alpha: 0.4).cgColor) c.fill(arguments.drawingRect) } @@ -845,12 +1847,12 @@ func chatSecretMessageVideo(account: Account, video: TelegramMediaFile, scale:CG if !arguments.insets.right.isEqual(to: 0.0) { c.clear(CGRect(origin: CGPoint(x: context.size.width - arguments.insets.right, y: 0.0), size: CGSize(width: arguments.insets.right, height: context.size.height))) } - } + }) addCorners(context, arguments: arguments, scale: scale) return context - } + }) } } @@ -886,38 +1888,7 @@ private enum Corner: Hashable { } } -private func ==(lhs: Corner, rhs: Corner) -> Bool { - switch lhs { - case let .TopLeft(lhsRadius): - switch rhs { - case let .TopLeft(rhsRadius) where rhsRadius == lhsRadius: - return true - default: - return false - } - case let .TopRight(lhsRadius): - switch rhs { - case let .TopRight(rhsRadius) where rhsRadius == lhsRadius: - return true - default: - return false - } - case let .BottomLeft(lhsRadius): - switch rhs { - case let .BottomLeft(rhsRadius) where rhsRadius == lhsRadius: - return true - default: - return false - } - case let .BottomRight(lhsRadius): - switch rhs { - case let .BottomRight(rhsRadius) where rhsRadius == lhsRadius: - return true - default: - return false - } - } -} + private enum Tail: Hashable { case BottomLeft(Int) @@ -961,55 +1932,35 @@ private func ==(lhs: Tail, rhs: Tail) -> Bool { } } -private var cachedCorners: [CGFloat: [Corner: DrawingContext]] = [:] -private let cachedCornersLock = SwiftSignalKitMac.Lock() -private var cachedTails: [Tail: DrawingContext] = [:] -private let cachedTailsLock = SwiftSignalKitMac.Lock() - private func cornerContext(_ corner: Corner, scale:CGFloat) -> DrawingContext { var cached: DrawingContext? - cachedCornersLock.locked { - cached = cachedCorners[scale]?[corner] - } + - if let cached = cached { - return cached - } else { - let context = DrawingContext(size: CGSize(width: CGFloat(corner.radius), height: CGFloat(corner.radius)), scale: scale, clear: true) - - context.withContext { c in - c.setBlendMode(.copy) - c.setFillColor(NSColor.black.cgColor) - let rect: CGRect - switch corner { - case let .TopLeft(radius): - rect = CGRect(origin: CGPoint(x: 0.0, y: -CGFloat(radius)), size: CGSize(width: CGFloat(radius << 1), height: CGFloat(radius << 1))) - case let .TopRight(radius): - rect = CGRect(origin: CGPoint(x: -CGFloat(radius), y: -CGFloat(radius)), size: CGSize(width: CGFloat(radius << 1), height: CGFloat(radius << 1))) - case let .BottomLeft(radius): - rect = CGRect(origin: CGPoint(), size: CGSize(width: CGFloat(radius << 1), height: CGFloat(radius << 1))) - case let .BottomRight(radius): - rect = CGRect(origin: CGPoint(x: -CGFloat(radius), y: 0.0), size: CGSize(width: CGFloat(radius << 1), height: CGFloat(radius << 1))) - } - c.fillEllipse(in: rect) - } - - cachedCornersLock.locked { - if cachedCorners[scale] == nil { - cachedCorners[scale] = [:] - } - cachedCorners[scale]?[corner] = context + let context = DrawingContext(size: CGSize(width: CGFloat(corner.radius), height: CGFloat(corner.radius)), scale: scale, clear: true) + + context.withContext { c in + c.setBlendMode(.copy) + c.setFillColor(NSColor.black.cgColor) + let rect: CGRect + switch corner { + case let .TopLeft(radius): + rect = CGRect(origin: CGPoint(x: 0.0, y: -CGFloat(radius)), size: CGSize(width: CGFloat(radius << 1), height: CGFloat(radius << 1))) + case let .TopRight(radius): + rect = CGRect(origin: CGPoint(x: -CGFloat(radius), y: -CGFloat(radius)), size: CGSize(width: CGFloat(radius << 1), height: CGFloat(radius << 1))) + case let .BottomLeft(radius): + rect = CGRect(origin: CGPoint(), size: CGSize(width: CGFloat(radius << 1), height: CGFloat(radius << 1))) + case let .BottomRight(radius): + rect = CGRect(origin: CGPoint(x: -CGFloat(radius), y: 0.0), size: CGSize(width: CGFloat(radius << 1), height: CGFloat(radius << 1))) } - return context + c.fillEllipse(in: rect) } + return context } private func tailContext(_ tail: Tail, scale:CGFloat) -> DrawingContext { var cached: DrawingContext? - cachedTailsLock.locked { - cached = cachedTails[tail] - } + if let cached = cached { return cached @@ -1053,9 +2004,6 @@ private func tailContext(_ tail: Tail, scale:CGFloat) -> DrawingContext { c.fillEllipse(in: rect) } - cachedCornersLock.locked { - cachedTails[tail] = context - } return context } } @@ -1066,24 +2014,24 @@ private func addCorners(_ context: DrawingContext, arguments: TransformImageArgu let corners = arguments.corners let drawingRect = arguments.drawingRect - if case let .Corner(radius) = corners.topLeft, radius > CGFloat(FLT_EPSILON) { + if case let .Corner(radius) = corners.topLeft, radius > CGFloat.ulpOfOne { let corner = cornerContext(.TopLeft(Int(radius)), scale:scale) context.blt(corner, at: CGPoint(x: drawingRect.minX, y: drawingRect.minY)) } - if case let .Corner(radius) = corners.topRight, radius > CGFloat(FLT_EPSILON) { + if case let .Corner(radius) = corners.topRight, radius > CGFloat.ulpOfOne { let corner = cornerContext(.TopRight(Int(radius)), scale:scale) context.blt(corner, at: CGPoint(x: drawingRect.maxX - radius, y: drawingRect.minY)) } switch corners.bottomLeft { case let .Corner(radius): - if radius > CGFloat(FLT_EPSILON) { + if radius > CGFloat.ulpOfOne { let corner = cornerContext(.BottomLeft(Int(radius)), scale:scale) context.blt(corner, at: CGPoint(x: drawingRect.minX, y: drawingRect.maxY - radius)) } case let .Tail(radius): - if radius > CGFloat(FLT_EPSILON) { + if radius > CGFloat.ulpOfOne { let tail = tailContext(.BottomLeft(Int(radius)), scale:scale) let color = context.colorAt(CGPoint(x: drawingRect.minX, y: drawingRect.maxY - 1.0)) context.withContext { c in @@ -1097,12 +2045,12 @@ private func addCorners(_ context: DrawingContext, arguments: TransformImageArgu switch corners.bottomRight { case let .Corner(radius): - if radius > CGFloat(FLT_EPSILON) { + if radius > CGFloat.ulpOfOne { let corner = cornerContext(.BottomRight(Int(radius)), scale:scale) context.blt(corner, at: CGPoint(x: drawingRect.maxX - radius, y: drawingRect.maxY - radius)) } case let .Tail(radius): - if radius > CGFloat(FLT_EPSILON) { + if radius > CGFloat.ulpOfOne { let tail = tailContext(.BottomRight(Int(radius)), scale:scale) context.blt(tail, at: CGPoint(x: drawingRect.maxX - radius - 3.0, y: drawingRect.maxY - radius)) } @@ -1110,14 +2058,18 @@ private func addCorners(_ context: DrawingContext, arguments: TransformImageArgu } -func mediaGridMessagePhoto(account: Account, photo: TelegramMediaImage, scale:CGFloat) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatMessagePhotoDatas(account: account, photo: photo, fullRepresentationSize: CGSize(width: 127.0, height: 127.0), autoFetchFullSize: true) +func mediaGridMessagePhoto(account: Account, imageReference: ImageMediaReference, scale:CGFloat) -> Signal { + let signal = chatMessagePhotoDatas(postbox: account.postbox, imageReference: imageReference, fullRepresentationSize: CGSize(width: 240, height: 240), autoFetchFullSize: true) - return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in - return { arguments in + return signal |> map { data in + return ImageDataTransformation(data: data, execute: { arguments, data in assertNotOnMainThread() let context = DrawingContext(size: arguments.drawingSize, scale:scale, clear: true) + let thumbnailData = data.thumbnailData + let fullSizeData = data.fullSizeData + let fullSizeComplete = data.fullSizeComplete + let drawingRect = arguments.drawingRect let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) @@ -1128,7 +2080,9 @@ func mediaGridMessagePhoto(account: Account, photo: TelegramMediaImage, scale:CG let options = NSMutableDictionary() options.setValue(max(fittedSize.width * context.scale, fittedSize.height * context.scale) as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) - if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) { + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + + if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, options), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) { fullSizeImage = image } } else { @@ -1142,9 +2096,11 @@ func mediaGridMessagePhoto(account: Account, photo: TelegramMediaImage, scale:CG } } } - + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + var thumbnailImage: CGImage? - if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { + if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, options), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options) { thumbnailImage = image } @@ -1157,13 +2113,14 @@ func mediaGridMessagePhoto(account: Account, photo: TelegramMediaImage, scale:CG c.interpolationQuality = .none c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) } - telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + telegramFastBlurMore(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) blurredThumbnailImage = thumbnailContext.generateImage() } - context.withContext { c in + context.withContext(isHighQuality: fullSizeImage != nil, { c in c.setBlendMode(.copy) + c.setFillColor(theme.colors.grayBackground.cgColor) if arguments.boundingSize != arguments.imageSize { c.fill(arguments.drawingRect) } @@ -1179,70 +2136,69 @@ func mediaGridMessagePhoto(account: Account, photo: TelegramMediaImage, scale:CG c.interpolationQuality = .medium c.draw(fullSizeImage, in: fittedRect) } - } + + if arguments.boundingSize == arguments.imageSize && fullSizeImage == nil && blurredThumbnailImage == nil { + c.setBlendMode(.normal) + c.fill(arguments.drawingRect) + } + }) addCorners(context, arguments: arguments, scale:scale) return context - } + }) } } -func mediaGridMessageVideo(account: Account, file: TelegramMediaFile, scale:CGFloat) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatMessageFileDatas(account: account, file: file) + + +func chatMessageVideoThumbnail(account: Account, fileReference: FileMediaReference, scale: CGFloat, synchronousLoad: Bool = false) -> Signal { + let signal = chatMessageVideoDatas(postbox: account.postbox, fileReference: fileReference, thumbnailSize: true, synchronousLoad: synchronousLoad) - return signal |> map { (thumbnailData, fullSizeDataAndPath, fullSizeComplete) in - return { arguments in - assertNotOnMainThread() - let context = DrawingContext(size: arguments.drawingSize, scale:scale, clear: true) - if arguments.drawingSize.width.isLessThanOrEqualTo(0.0) || arguments.drawingSize.height.isLessThanOrEqualTo(0.0) { - return context - } + return signal |> map { data in + return ImageDataTransformation(data: data, execute: { arguments, data in + let context = DrawingContext(size: arguments.drawingSize, scale: scale, clear: true) + + let thumbnailData = data.thumbnailData + let fullSizeData = data.fullSizeData + let fullSizeComplete = data.fullSizeComplete + let drawingRect = arguments.drawingRect - let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) + var fittedSize = arguments.imageSize + if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.width = arguments.boundingSize.width + } + if abs(fittedSize.height - arguments.boundingSize.height).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.height = arguments.boundingSize.height + } + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) var fullSizeImage: CGImage? - if let fullSizeDataAndPath = fullSizeDataAndPath { + if let fullSizeData = fullSizeData { if fullSizeComplete { - if file.mimeType.hasPrefix("video/") { - let tempFilePath = NSTemporaryDirectory() + "\(fullSizeDataAndPath.nsstring.lastPathComponent).mov" - - _ = try? FileManager.default.removeItem(atPath: tempFilePath) - _ = try? FileManager.default.linkItem(atPath: fullSizeDataAndPath, toPath: tempFilePath) - - let asset = AVAsset(url: URL(fileURLWithPath: tempFilePath)) - let imageGenerator = AVAssetImageGenerator(asset: asset) - imageGenerator.maximumSize = CGSize(width: 200, height: 200) - imageGenerator.appliesPreferredTrackTransform = true - - - if let image = try? imageGenerator.copyCGImage(at: CMTime(seconds: 0.0, preferredTimescale: asset.duration.timescale), actualTime: nil) { - fullSizeImage = image - } + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, options), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { + fullSizeImage = image } - /*let options: [NSString: NSObject] = [ - kCGImageSourceThumbnailMaxPixelSize: max(fittedSize.width * context.scale, fittedSize.height * context.scale), - kCGImageSourceCreateThumbnailFromImageAlways: true - ] - if let imageSource = CGImageSourceCreateWithData(fullSizeData, nil), image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options) { - fullSizeImage = image - }*/ } else { - /*let imageSource = CGImageSourceCreateIncremental(nil) - CGImageSourceUpdateData(imageSource, fullSizeData as CFDataRef, fullSizeData.length >= fullTotalSize) - - var options: [NSString : NSObject!] = [:] - options[kCGImageSourceShouldCache as NSString] = false as NSNumber - if let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionaryRef) { - fullSizeImage = image - }*/ + let imageSource = CGImageSourceCreateIncremental(nil) + CGImageSourceUpdateData(imageSource, fullSizeData as CFData, fullSizeComplete) + + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + if let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { + fullSizeImage = image + } } } - + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + var thumbnailImage: CGImage? - if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { + if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, options), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options) { thumbnailImage = image } @@ -1251,7 +2207,7 @@ func mediaGridMessageVideo(account: Account, file: TelegramMediaFile, scale:CGFl let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 150.0, height: 150.0)) let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) - thumbnailContext.withContext { c in + thumbnailContext.withFlippedContext { c in c.interpolationQuality = .none c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) } @@ -1260,33 +2216,178 @@ func mediaGridMessageVideo(account: Account, file: TelegramMediaFile, scale:CGFl blurredThumbnailImage = thumbnailContext.generateImage() } - context.withContext { c in + context.withFlippedContext(isHighQuality: data.fullSizeData != nil, { c in c.setBlendMode(.copy) - if arguments.boundingSize != arguments.imageSize { + if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { + //c.setFillColor(NSColor(white: 0.0, alpha: 0.4).cgColor) c.fill(arguments.drawingRect) } c.setBlendMode(.copy) - if let blurredThumbnailImage = blurredThumbnailImage { + if let cgImage = blurredThumbnailImage { c.interpolationQuality = .low - c.draw(blurredThumbnailImage, in: fittedRect) + c.draw(cgImage, in: fittedRect) + c.setBlendMode(.normal) } if let fullSizeImage = fullSizeImage { - c.setBlendMode(.normal) c.interpolationQuality = .medium c.draw(fullSizeImage, in: fittedRect) } - } + }) - addCorners(context, arguments: arguments, scale:scale) + addCorners(context, arguments: arguments, scale: scale) return context - } + }) } } +func mediaGridMessageVideo(postbox: Postbox, fileReference: FileMediaReference, scale: CGFloat, synchronousLoad: Bool = false) -> Signal { + let signal = chatMessageVideoDatas(postbox: postbox, fileReference: fileReference, synchronousLoad: synchronousLoad) + + return signal |> map { data in + return ImageDataTransformation(data: data, execute: { arguments, data in + let context = DrawingContext(size: arguments.drawingSize, scale: scale, clear: true) + + let thumbnailData = data.thumbnailData + let fullSizeData = data.fullSizeData + let fullSizeComplete = data.fullSizeComplete + + let drawingRect = arguments.drawingRect + var fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) + if fittedSize.width < drawingRect.size.width && fittedSize.width >= drawingRect.size.width - 2.0 { + fittedSize.width = drawingRect.size.width + } + if fittedSize.height < drawingRect.size.height && fittedSize.height >= drawingRect.size.height - 2.0 { + fittedSize.height = drawingRect.size.height + } + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + + var fullSizeImage: CGImage? + if let fullSizeData = fullSizeData { + if fullSizeComplete { + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, options), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { + fullSizeImage = image + } + } else { + let imageSource = CGImageSourceCreateIncremental(nil) + CGImageSourceUpdateData(imageSource, fullSizeData as CFData, fullSizeComplete) + + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + if let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { + fullSizeImage = image + } + } + } + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + + var thumbnailImage: CGImage? + if fullSizeImage == nil, let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, options), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options) { + thumbnailImage = image + } + + var blurredThumbnailImage: CGImage? + if let thumbnailImage = thumbnailImage { + let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) + let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 150.0, height: 150.0)) + let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) + thumbnailContext.withFlippedContext { c in + c.interpolationQuality = .none + c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) + } + telegramFastBlurMore(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + + blurredThumbnailImage = thumbnailContext.generateImage() + } + + context.withFlippedContext(isHighQuality: fullSizeImage != nil, { c in + c.setBlendMode(.copy) + if arguments.boundingSize != arguments.imageSize { + switch arguments.resizeMode { + case .blurBackground: + let blurSourceImage = thumbnailImage ?? fullSizeImage + + if let image = blurSourceImage { + let thumbnailSize = CGSize(width: image.width, height: image.height) + let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 74.0, height: 74.0)) + let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) + thumbnailContext.withFlippedContext { c in + c.interpolationQuality = .none + c.draw(image, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) + } + // telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + telegramFastBlurMore(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + + if let blurredImage = thumbnailContext.generateImage() { + let filledSize = thumbnailSize.aspectFilled(arguments.drawingRect.size) + c.interpolationQuality = .medium + c.draw(blurredImage, in: CGRect(origin: CGPoint(x: arguments.drawingRect.minX + (arguments.drawingRect.width - filledSize.width) / 2.0, y: arguments.drawingRect.minY + (arguments.drawingRect.height - filledSize.height) / 2.0), size: filledSize)) + c.setBlendMode(.normal) + c.setFillColor(theme.colors.background.withAlphaComponent(0.5).cgColor) + c.fill(arguments.drawingRect) + c.setBlendMode(.copy) + } + + let value = arguments.imageSize.aspectFitted(arguments.boundingSize) + + if let fullSizeImage = fullSizeImage { + c.interpolationQuality = .medium + c.draw(fullSizeImage, in: NSMakeRect((arguments.boundingSize.width - value.width) / 2, (arguments.boundingSize.height - value.height) / 2, value.width, value.height)) + } + + + } else { + c.fill(arguments.drawingRect) + } + case let .fill(color): + c.setFillColor(color.cgColor) + c.fill(arguments.drawingRect) + case .fillTransparent: + c.setFillColor(theme.colors.transparentBackground.cgColor) + c.fill(arguments.drawingRect) + case .none: + c.setBlendMode(.copy) + if let cgImage = blurredThumbnailImage { + c.interpolationQuality = .low + c.draw(cgImage, in: fittedRect) + c.setBlendMode(.normal) + } + + if let fullSizeImage = fullSizeImage { + c.interpolationQuality = .medium + c.draw(fullSizeImage, in: fittedRect) + } + case .imageColor: + break + } + } else { + c.setBlendMode(.copy) + if let cgImage = blurredThumbnailImage { + c.interpolationQuality = .low + c.draw(cgImage, in: fittedRect) + c.setBlendMode(.normal) + } + + if let fullSizeImage = fullSizeImage { + c.interpolationQuality = .medium + c.draw(fullSizeImage, in: fittedRect) + } + } + }) + + addCorners(context, arguments: arguments, scale: scale) + + return context + }) + } +} + private func imageFromAJpeg(data: Data) -> (CGImage, CGImage)? { if let (colorData, alphaData) = data.withUnsafeBytes({ (bytes: UnsafePointer) -> (Data, Data)? in @@ -1306,14 +2407,16 @@ private func imageFromAJpeg(data: Data) -> (CGImage, CGImage)? { let alphaData = data.subdata(in: (4 + Int(colorSize) + 4) ..< (4 + Int(colorSize) + 4 + Int(alphaSize))) return (colorData, alphaData) }) { - - let sourceColor:CGImageSource? = CGImageSourceCreateWithData(colorData as CFData, nil); - let sourceAlpha:CGImageSource? = CGImageSourceCreateWithData(alphaData as CFData, nil); + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + + let sourceColor:CGImageSource? = CGImageSourceCreateWithData(colorData as CFData, options); + let sourceAlpha:CGImageSource? = CGImageSourceCreateWithData(alphaData as CFData, options); if let sourceColor = sourceColor, let sourceAlpha = sourceAlpha { - let colorImage = CGImageSourceCreateImageAtIndex(sourceColor, 0, nil); - let alphaImage = CGImageSourceCreateImageAtIndex(sourceAlpha, 0, nil); + let colorImage = CGImageSourceCreateImageAtIndex(sourceColor, 0, options); + let alphaImage = CGImageSourceCreateImageAtIndex(sourceAlpha, 0, options); if let colorImage = colorImage, let alphaImage = alphaImage { return (colorImage, alphaImage) } @@ -1323,22 +2426,36 @@ private func imageFromAJpeg(data: Data) -> (CGImage, CGImage)? { } -public func putToTemp(image:NSImage, compress: Bool = true) -> Signal { +public func putToTemp(image:NSImage, compress: Bool = true) -> Signal { return Signal { (subscriber) in - let data:Data? = image.tiffRepresentation(using: .jpeg, factor: compress ? 0.83 : 1) - if let data = data { - let imageRep = NSBitmapImageRep(data: data) - let repData = imageRep?.representation(using: .jpeg, properties: [NSBitmapImageRep.PropertyKey.compressionFactor: compress ? 0.83 : 1]) - let path = NSTemporaryDirectory() + "tg_image_\(arc4random()).jpeg" - try? repData?.write(to: URL(fileURLWithPath: path)) - subscriber.putNext(path) + let path = NSTemporaryDirectory() + "tg_image_\(arc4random()).jpeg" + if compress { + if let data = compressImageToJPEG(image.cgImage(forProposedRect: nil, context: nil, hints: nil)!, quality: compress ? 0.83 : 1.0) { + let path = NSTemporaryDirectory() + "tg_image_\(arc4random()).jpeg" + try? data.write(to: URL(fileURLWithPath: path)) + subscriber.putNext(path) + } + } else { + let options = NSMutableDictionary() + let mutableData: CFMutableData = NSMutableData() as CFMutableData + if let colorDestination = CGImageDestinationCreateWithData(mutableData, kUTTypeJPEG, 1, nil) { + CGImageDestinationAddImage(colorDestination, image.cgImage(forProposedRect: nil, context: nil, hints: nil)!, options as CFDictionary) + if CGImageDestinationFinalize(colorDestination) { + try? (mutableData as Data).write(to: URL(fileURLWithPath: path)) + subscriber.putNext(path) + } + } } - + + + + + subscriber.putCompletion() return EmptyDisposable @@ -1346,31 +2463,30 @@ public func putToTemp(image:NSImage, compress: Bool = true) -> Signal Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - return Signal { (subscriber) in +public func filethumb(with url:URL, account:Account, scale:CGFloat) -> Signal { + return Signal { (subscriber) in let data = try? Data(contentsOf: url) - subscriber.putNext(data) subscriber.putCompletion() return EmptyDisposable - } |> map({ (data) in - - return { arguments in - + } |> map { data in + return ImageDataTransformation(data: ImageRenderData(data, nil, true), execute: { arguments, data in let context = DrawingContext(size: arguments.drawingSize, scale:scale, clear: true) let drawingRect = arguments.drawingRect let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) - + var thumb: CGImage? - if let data = data { + if let thumbnailData = data.thumbnailData { let options = NSMutableDictionary() options.setValue(max(fittedSize.width * context.scale, fittedSize.height * context.scale) as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) - if let imageSource = CGImageSourceCreateWithData(data as CFData, nil), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) { + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + + if let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) { thumb = image } } @@ -1385,20 +2501,24 @@ public func filethumb(with url:URL, account:Account, scale:CGFloat) -> Signal<(T addCorners(context, arguments: arguments, scale:scale) return context - } - }) - |> runOn(account.graphicsThreadPool) + }) + } + } -func chatSecretPhoto(account: Account, photo: TelegramMediaImage, scale:CGFloat) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatMessagePhotoDatas(account: account, photo: photo) +func chatSecretPhoto(account: Account, imageReference: ImageMediaReference, scale:CGFloat, synchronousLoad: Bool = false) -> Signal { + let signal = chatMessagePhotoDatas(postbox: account.postbox, imageReference: imageReference, synchronousLoad: synchronousLoad) - return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in - return { arguments in + return signal |> map { data in + return ImageDataTransformation(data: data, execute: { arguments, data in let context = DrawingContext(size: arguments.drawingSize, scale: scale, clear: true) + let fullSizeData = data.fullSizeData + let thumbnailData = data.thumbnailData + let fullSizeComplete = data.fullSizeComplete + let drawingRect = arguments.drawingRect var fittedSize = arguments.imageSize if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { @@ -1416,7 +2536,7 @@ func chatSecretPhoto(account: Account, photo: TelegramMediaImage, scale:CGFloat) if fullSizeComplete { let options = NSMutableDictionary() options[kCGImageSourceShouldCache as NSString] = false as NSNumber - if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { + if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, options), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { let thumbnailSize = CGSize(width: image.width, height: image.height) let thumbnailContextSize = thumbnailSize.aspectFilled(CGSize(width: 20.0, height: 20.0)) let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) @@ -1451,7 +2571,10 @@ func chatSecretPhoto(account: Account, photo: TelegramMediaImage, scale:CGFloat) } if blurredImage == nil { - if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + + if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, options), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options) { let thumbnailSize = CGSize(width: image.width, height: image.height) let thumbnailContextSize = thumbnailSize.aspectFilled(CGSize(width: 20.0, height: 20.0)) let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) @@ -1478,7 +2601,7 @@ func chatSecretPhoto(account: Account, photo: TelegramMediaImage, scale:CGFloat) context.withContext { c in c.setBlendMode(.copy) if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { - //c.setFillColor(UIColor(white: 0.0, alpha: 0.4).cgColor) + //c.setFillColor(NSColor(white: 0.0, alpha: 0.4).cgColor) c.fill(arguments.drawingRect) } @@ -1499,26 +2622,213 @@ func chatSecretPhoto(account: Account, photo: TelegramMediaImage, scale:CGFloat) addCorners(context, arguments: arguments, scale:scale) return context - } + }) } } -func chatMessageImageFile(account: Account, file: TelegramMediaFile, progressive: Bool = false, scale: CGFloat) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatMessageFileDatas(account: account, file: file, progressive: progressive, justThumbail: true) +func chatMessageImageFile(account: Account, fileReference: FileMediaReference, progressive: Bool = false, scale: CGFloat, synchronousLoad: Bool = false) -> Signal { + let signal = chatMessageFileDatas(account: account, fileReference: fileReference, progressive: progressive, justThumbail: true, synchronousLoad: synchronousLoad) - return signal |> map { (thumbnailData, fullSizeDataAndPath, fullSizeComplete) in - return { arguments in - assertNotOnMainThread() + return signal |> map { data in + return ImageDataTransformation(data: data, execute: { arguments, data in let context = DrawingContext(size: arguments.drawingSize, scale: scale, clear: true) let drawingRect = arguments.drawingRect - let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) + let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize)//.fitted(arguments.imageSize) + let fittedRect = CGRect(origin: CGPoint(x: floorToScreenPixels(System.backingScale, drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0), y: floorToScreenPixels(System.backingScale, drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0)), size: fittedSize) + + + + var fullSizeImage: CGImage? + + if let fullSizeData = data.fullSizeData { + let options = NSMutableDictionary() + // options.setValue(max(fittedSize.width * 3, fittedSize.height * 3) as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) + options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) + options.setValue(false as NSNumber, forKey: kCGImageSourceShouldCache as String) + options.setValue(false as NSNumber, forKey: kCGImageSourceShouldCacheImmediately as String) + options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailWithTransform as String) + + if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, options) { + if let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) { + fullSizeImage = image + } else if let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options) { + fullSizeImage = image + } + } + } + + let options = NSMutableDictionary() + // options.setValue(max(fittedSize.width * 3, fittedSize.height * 3) as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) + options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) + options.setValue(false as NSNumber, forKey: kCGImageSourceShouldCache as String) + options.setValue(false as NSNumber, forKey: kCGImageSourceShouldCacheImmediately as String) + options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailWithTransform as String) + + + var thumbnailImage: CGImage? + if let thumbnailData = data.thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, options), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options) { + thumbnailImage = image + } + + var blurredThumbnailImage: CGImage? + if let thumbnailImage = thumbnailImage, fullSizeImage == nil { + let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) + let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 150.0, height: 150.0)) + let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) + thumbnailContext.withFlippedContext { c in + c.interpolationQuality = .none + c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) + } + telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + + blurredThumbnailImage = thumbnailContext.generateImage() + } + + context.withFlippedContext(isHighQuality: data.fullSizeData != nil, { c in + //c.setBlendMode(.copy) + if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { + c.setFillColor(theme.colors.transparentBackground.cgColor) + c.fill(arguments.drawingRect) + } + + // c.setBlendMode(.copy) + if let cgImage = blurredThumbnailImage { + c.interpolationQuality = .low + c.draw(cgImage, in: fittedRect) + c.setBlendMode(.normal) + } + + if let fullSizeImage = fullSizeImage { + c.interpolationQuality = .medium + c.setFillColor(theme.colors.transparentBackground.cgColor) + c.fill(fittedRect) + c.draw(fullSizeImage, in: fittedRect) + } + }) + + addCorners(context, arguments: arguments, scale: scale) + + return context + }) + } +} + + +private func chatMessagePhotoThumbnailDatas(account: Account, imageReference: ImageMediaReference, synchronousLoad: Bool = false, secureIdAccessContext: SecureIdAccessContext? = nil) -> Signal { + let fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0) + if let smallestRepresentation = smallestImageRepresentation(imageReference.media.representations), let largestRepresentation = imageReference.media.representationForDisplayAtSize(PixelDimensions(fullRepresentationSize)) { + + let size = CGSize(width: 160.0, height: 160.0) + let maybeFullSize: Signal + + if largestRepresentation.resource is EncryptedMediaResource { + maybeFullSize = account.postbox.mediaBox.resourceData(largestRepresentation.resource, attemptSynchronously: synchronousLoad) + } else { + maybeFullSize = account.postbox.mediaBox.cachedResourceRepresentation(largestRepresentation.resource, representation: CachedScaledImageRepresentation(size: size), complete: false, attemptSynchronously: synchronousLoad) + } + + let signal = maybeFullSize + |> take(1) + |> mapToSignal { maybeData -> Signal in + if maybeData.complete { + if largestRepresentation.resource is EncryptedMediaResource, let secureIdAccessContext = secureIdAccessContext { + let loadedData: Data? = decryptedResourceData(data: maybeData, resource: largestRepresentation.resource, params: secureIdAccessContext) + return .single(ImageRenderData(nil, loadedData, true)) + } else { + let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) + return .single(ImageRenderData(nil, loadedData, true)) + } + + } else { + + let fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: imageReference.resourceReference(smallestRepresentation.resource), statsCategory: .image)//account.postbox.mediaBox.fetchedResource(smallestRepresentation.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)) + + let thumbnail = Signal { subscriber in + let fetchedDisposable = fetchedThumbnail.start() + let thumbnailDisposable = account.postbox.mediaBox.resourceData(smallestRepresentation.resource, attemptSynchronously: synchronousLoad).start(next: { next in + subscriber.putNext(!next.complete ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) + }, error: subscriber.putError, completed: subscriber.putCompletion) + + return ActionDisposable { + fetchedDisposable.dispose() + thumbnailDisposable.dispose() + } + } + + let maybeFullData: Signal + + if largestRepresentation.resource is EncryptedMediaResource { + maybeFullData = account.postbox.mediaBox.resourceData(largestRepresentation.resource, attemptSynchronously: synchronousLoad) + } else { + maybeFullData = account.postbox.mediaBox.cachedResourceRepresentation(largestRepresentation.resource, representation: CachedScaledImageRepresentation(size: size), complete: false, attemptSynchronously: synchronousLoad) + } + + let fullSizeData: Signal = maybeFullData + |> map { next -> ImageRenderData in + return ImageRenderData(nil, !next.complete ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete) + } + + return thumbnail |> mapToSignal { thumbnailData in + return fullSizeData |> map { fullData in + return ImageRenderData(thumbnailData, fullData.fullSizeData, fullData.fullSizeComplete) + } + } + } + } |> filter({ $0.thumbnailData != nil || $0.fullSizeData != nil }) + + return signal + } else { + return .never() + } +} + +func chatMessagePhotoThumbnail(account: Account, imageReference: ImageMediaReference, scale: CGFloat = System.backingScale, synchronousLoad: Bool = false, secureIdAccessContext: SecureIdAccessContext? = nil) -> Signal { + let signal = chatMessagePhotoThumbnailDatas(account: account, imageReference: imageReference, synchronousLoad: synchronousLoad, secureIdAccessContext: secureIdAccessContext) + + return signal |> map { data in + return ImageDataTransformation(data: data, execute: { arguments, data in + let context = DrawingContext(size: arguments.drawingSize, scale: scale, clear: true) + + let fullSizeData = data.fullSizeData + let thumbnailData = data.thumbnailData + let fullSizeComplete = data.fullSizeComplete + + let drawingRect = arguments.drawingRect + var fittedSize = arguments.imageSize + if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.width = arguments.boundingSize.width + } + if abs(fittedSize.height - arguments.boundingSize.height).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.height = arguments.boundingSize.height + } + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + var fullSizeImage: CGImage? + if let fullSizeData = fullSizeData { + if fullSizeComplete { + /*let options = NSMutableDictionary() + options.setValue(max(fittedSize.width * context.scale, fittedSize.height * context.scale) as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) + options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) + if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) { + fullSizeImage = image + }*/ + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + + if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, options), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { + fullSizeImage = image + } + } + } var thumbnailImage: CGImage? - if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + + if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, options), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options) { thumbnailImage = image } @@ -1527,52 +2837,130 @@ func chatMessageImageFile(account: Account, file: TelegramMediaFile, progressive let thumbnailSize = CGSize(width: thumbnailImage.width, height: thumbnailImage.height) let thumbnailContextSize = thumbnailSize.aspectFitted(CGSize(width: 150.0, height: 150.0)) let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) - thumbnailContext.withContext { c in + thumbnailContext.withFlippedContext { c in c.interpolationQuality = .none c.draw(thumbnailImage, in: CGRect(origin: CGPoint(), size: thumbnailContextSize)) } + telegramFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) blurredThumbnailImage = thumbnailContext.generateImage() } - context.withContext { c in + context.withFlippedContext(isHighQuality: fullSizeImage != nil, { c in c.setBlendMode(.copy) - if arguments.boundingSize != arguments.imageSize { + if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { + //c.setFillColor(NSColor(white: 0.0, alpha: 0.4).cgColor) c.fill(arguments.drawingRect) } + c.setBlendMode(.copy) - if let blurredThumbnailImage = blurredThumbnailImage { + if let cgImage = blurredThumbnailImage { c.interpolationQuality = .low - c.draw(blurredThumbnailImage, in: fittedRect) + c.draw(cgImage, in: fittedRect) + c.setBlendMode(.normal) } - } + if let fullSizeImage = fullSizeImage { + c.interpolationQuality = .medium + c.draw(fullSizeImage, in: fittedRect) + } + }) addCorners(context, arguments: arguments, scale: scale) return context - } + }) } } - -private func chatMessagePhotoThumbnailDatas(account: Account, photo: TelegramMediaImage) -> Signal<(Data?, Data?, Bool), NoError> { - let fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0) - if let smallestRepresentation = smallestImageRepresentation(photo.representations), let largestRepresentation = photo.representationForDisplayAtSize(fullRepresentationSize) { +private func builtinWallpaperData() -> Signal { + return Signal { subscriber in + if let filePath = Bundle.main.path(forResource: "builtin-wallpaper-svg", ofType: nil), let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) { + if let data = TGGUnzipData(data, 8 * 1024 * 1024) { + subscriber.putNext(ImageRenderData(nil, data, true)) + } + } + subscriber.putCompletion() - let maybeFullSize = account.postbox.mediaBox.cachedResourceRepresentation(largestRepresentation.resource, representation: CachedScaledImageRepresentation(size: CGSize(width: 160.0, height: 160.0)), complete: false) + return EmptyDisposable + } |> runOn(Queue.concurrentDefaultQueue()) +} + +func settingsBuiltinWallpaperImage(account: Account, scale: CGFloat = 2.0) -> Signal { + return builtinWallpaperData() |> map { data in + return ImageDataTransformation(data: data, execute: { arguments, data in + let context = DrawingContext(size: arguments.drawingSize, scale: scale, clear: true) + + var fullSizeImage: CGImage? + if let data = data.fullSizeData, let image = drawSvgImageNano(data, arguments.drawingSize) { + fullSizeImage = image._cgImage + } + if let fullSizeImage = fullSizeImage { + let drawingRect = arguments.drawingRect + var fittedSize = fullSizeImage.size.aspectFilled(drawingRect.size) + if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.width = arguments.boundingSize.width + } + if abs(fittedSize.height - arguments.boundingSize.height).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.height = arguments.boundingSize.height + } + + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + + + context.withFlippedContext { c in + + + let preview = AnimatedGradientBackgroundView.generatePreview(size: arguments.drawingSize.fitted(.init(width: 30, height: 30)), colors: [0xdbddbb, 0x6ba587, 0xd5d88d, 0x88b884].map { .init(argb: $0) }) + + c.setBlendMode(.normal) + c.draw(preview, in: fittedRect) + + c.interpolationQuality = .medium + c.setBlendMode(.softLight) + c.setAlpha(0.25) + c.draw(fullSizeImage, in: fittedRect) + } + } + + addCorners(context, arguments: arguments, scale: scale) + + return context + }) + } +} + +private func chatWallpaperDatas(account: Account, representations: [TelegramMediaImageRepresentation], file: TelegramMediaFile? = nil, webpage: TelegramMediaWebpage? = nil, slug: String? = nil, autoFetchFullSize: Bool = false, isBlurred: Bool = false, synchronousLoad: Bool = false) -> Signal { + if let smallestRepresentation = smallestImageRepresentation(representations), let largestRepresentation = largestImageRepresentation(representations) { + let maybeFullSize: Signal + if isBlurred { + maybeFullSize = account.postbox.mediaBox.cachedResourceRepresentation(largestRepresentation.resource, representation: CachedBlurredWallpaperRepresentation(), complete: false, attemptSynchronously: synchronousLoad) + } else { + maybeFullSize = account.postbox.mediaBox.resourceData(largestRepresentation.resource, attemptSynchronously: synchronousLoad) + } - let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal<(Data?, Data?, Bool), NoError> in + let signal = maybeFullSize |> take(1) |> mapToSignal { maybeData -> Signal in if maybeData.complete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) - return .single((nil, loadedData, true)) + return .single(ImageRenderData(nil, loadedData, true)) } else { - let fetchedThumbnail = account.postbox.mediaBox.fetchedResource(smallestRepresentation.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .image)) + let smallReference: MediaResourceReference + let fullReference: MediaResourceReference + if let webpage = webpage, let file = file { + smallReference = MediaResourceReference.media(media: AnyMediaReference.webPage(webPage: WebpageReference(webpage), media: file), resource: smallestRepresentation.resource) + fullReference = MediaResourceReference.media(media: AnyMediaReference.webPage(webPage: WebpageReference(webpage), media: file), resource: largestRepresentation.resource) + } else { + smallReference = MediaResourceReference.wallpaper(wallpaper: slug != nil ? .slug(slug!) : nil, resource: smallestRepresentation.resource) + fullReference = MediaResourceReference.wallpaper(wallpaper: slug != nil ? .slug(slug!) : nil, resource: largestRepresentation.resource) + } + + let fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: smallReference, statsCategory: .image) + let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: fullReference, statsCategory: .image) let thumbnail = Signal { subscriber in let fetchedDisposable = fetchedThumbnail.start() - let thumbnailDisposable = account.postbox.mediaBox.resourceData(smallestRepresentation.resource).start(next: { next in + let thumbnailDisposable = account.postbox.mediaBox.resourceData(smallestRepresentation.resource, attemptSynchronously: synchronousLoad).start(next: { next in subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) }, error: subscriber.putError, completed: subscriber.putCompletion) @@ -1582,19 +2970,131 @@ private func chatMessagePhotoThumbnailDatas(account: Account, photo: TelegramMed } } - let fullSizeData: Signal<(Data?, Bool), NoError> = maybeFullSize - |> map { next -> (Data?, Bool) in - return (next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete) + let fullSizeData: Signal + + if autoFetchFullSize { + fullSizeData = Signal { subscriber in + let fetchedFullSizeDisposable = fetchedFullSize.start() + + let fetchData: Signal + if isBlurred { + fetchData = account.postbox.mediaBox.cachedResourceRepresentation(largestRepresentation.resource, representation: CachedBlurredWallpaperRepresentation(), complete: false, attemptSynchronously: synchronousLoad) + } else { + fetchData = account.postbox.mediaBox.resourceData(largestRepresentation.resource, attemptSynchronously: synchronousLoad) + } + + let fullSizeDisposable = fetchData.start(next: { next in + subscriber.putNext(ImageRenderData(nil, next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)) + }, error: subscriber.putError, completed: subscriber.putCompletion) + + return ActionDisposable { + fetchedFullSizeDisposable.dispose() + fullSizeDisposable.dispose() + } + } + } else { + fullSizeData = account.postbox.mediaBox.resourceData(largestRepresentation.resource) + |> map { next -> ImageRenderData in + return ImageRenderData(nil, next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete) + } } return thumbnail |> mapToSignal { thumbnailData in - return fullSizeData |> map { (fullSizeData, complete) in - return (thumbnailData, fullSizeData, complete) + return fullSizeData |> map { fullData in + return ImageRenderData(thumbnailData, fullData.fullSizeData, fullData.fullSizeComplete) + } + } + } + } |> filter({ $0.thumbnailData != nil || $0.fullSizeData != nil }) + + return signal + } else { + return .never() + } +} + +enum PatternWallpaperDrawMode { + case thumbnail + case fastScreen + case screen +} + + +func crossplatformPreview(account: Account, palette: ColorPalette, wallpaper: Wallpaper, webpage: TelegramMediaWebpage? = nil, mode: PatternWallpaperDrawMode, autoFetchFullSize: Bool = false, scale: CGFloat = 2.0, isBlurred: Bool = false, synchronousLoad: Bool = false) -> Signal { + let signal: Signal = moveWallpaperToCache(postbox: account.postbox, wallpaper: wallpaper) + + return signal |> map { wallpaper in + return ImageDataTransformation(data: ImageRenderData(nil, nil, false), execute: { arguments, data in + + let context = DrawingContext(size: arguments.drawingSize, scale: scale, clear: true) + let preview = generateThemePreview(for: palette, wallpaper: wallpaper, backgroundMode: generateBackgroundMode(wallpaper, palette: palette, maxSize: WallpaperDimensions.aspectFilled(NSMakeSize(600, 600)))) + + context.withContext { ctx in + ctx.draw(preview, in: arguments.drawingRect) + } + + addCorners(context, arguments: arguments, scale: arguments.scale) + + return context + }) + } +} + + +private func patternWallpaperDatas(account: Account, representations: [ImageRepresentationWithReference], mode: PatternWallpaperDrawMode, autoFetchFullSize: Bool = false) -> Signal { + if let smallestRepresentation = smallestImageRepresentation(representations.map({ $0.representation })), let largestRepresentation = largestImageRepresentation(representations.map({ $0.representation })), let smallestIndex = representations.firstIndex(where: { $0.representation == smallestRepresentation }), let largestIndex = representations.firstIndex(where: { $0.representation == largestRepresentation }) { + + let size: CGSize? + switch mode { + case .thumbnail: + size = largestRepresentation.dimensions.size.fitted(CGSize(width: 640.0, height: 640.0)) + default: + size = nil + } + let maybeFullSize = account.postbox.mediaBox.cachedResourceRepresentation(largestRepresentation.resource, representation: CachedPatternWallpaperMaskRepresentation(size: size), complete: false, fetch: false) + + let signal = maybeFullSize + |> take(1) + |> mapToSignal { maybeData -> Signal in + if maybeData.complete { + let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) + return .single(ImageRenderData(nil, loadedData, true)) + } else { + let fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: representations[smallestIndex].reference) + let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: representations[largestIndex].reference) + + let thumbnailData = Signal { subscriber in + let fetchedDisposable = fetchedThumbnail.start() + let thumbnailDisposable = account.postbox.mediaBox.cachedResourceRepresentation(representations[smallestIndex].representation.resource, representation: CachedPatternWallpaperMaskRepresentation(size: size), complete: false, fetch: true).start(next: { next in + subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) + }, error: subscriber.putError, completed: subscriber.putCompletion) + + return ActionDisposable { + fetchedDisposable.dispose() + thumbnailDisposable.dispose() + } + } + + let fullSizeData = Signal<(Data?, Bool), NoError> { subscriber in + let fetchedFullSizeDisposable = fetchedFullSize.start() + let fullSizeDisposable = account.postbox.mediaBox.cachedResourceRepresentation(representations[largestIndex].representation.resource, representation: CachedPatternWallpaperMaskRepresentation(size: size), complete: false, fetch: true).start(next: { next in + subscriber.putNext((next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: []), next.complete)) + }, error: subscriber.putError, completed: subscriber.putCompletion) + + return ActionDisposable { + fetchedFullSizeDisposable.dispose() + fullSizeDisposable.dispose() + } + } + + return thumbnailData |> mapToSignal { thumbnailData in + return fullSizeData |> map { (fullSizeData, complete) in + return ImageRenderData(thumbnailData, fullSizeData, complete) + } } } - } - } |> filter({ $0.0 != nil || $0.1 != nil }) + } return signal } else { @@ -1602,13 +3102,38 @@ private func chatMessagePhotoThumbnailDatas(account: Account, photo: TelegramMed } } -func chatMessagePhotoThumbnail(account: Account, photo: TelegramMediaImage, scale: CGFloat = System.backingScale) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let signal = chatMessagePhotoThumbnailDatas(account: account, photo: photo) - - return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in - return { arguments in + + + +private func chatWallpaperInternal(_ signal: Signal, prominent: Bool, scale: CGFloat, drawPatternOnly: Bool, palette: ColorPalette) -> Signal { + + return signal |> map { data in + + var fullSizeImage: CGImage? = nil + if let fullSizeData = data.fullSizeData, data.fullSizeComplete { + + if prominent { + let options = NSMutableDictionary() + options.setValue(960 as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) + options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) + if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options) { + fullSizeImage = image + } + } else { + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { + fullSizeImage = image + } + } + } + + + return ImageDataTransformation(data: data, execute: { arguments, data in let context = DrawingContext(size: arguments.drawingSize, scale: scale, clear: true) + let thumbnailData = data.thumbnailData + let drawingRect = arguments.drawingRect var fittedSize = arguments.imageSize if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { @@ -1619,35 +3144,13 @@ func chatMessagePhotoThumbnail(account: Account, photo: TelegramMediaImage, scal } let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) - - var fullSizeImage: CGImage? - if let fullSizeData = fullSizeData { - if fullSizeComplete { - /*let options = NSMutableDictionary() - options.setValue(max(fittedSize.width * context.scale, fittedSize.height * context.scale) as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) - options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) - if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) { - fullSizeImage = image - }*/ - let options = NSMutableDictionary() - options[kCGImageSourceShouldCache as NSString] = false as NSNumber - if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { - fullSizeImage = image - } - } else { - let imageSource = CGImageSourceCreateIncremental(nil) - CGImageSourceUpdateData(imageSource, fullSizeData as CFData, fullSizeComplete) - - let options = NSMutableDictionary() - options[kCGImageSourceShouldCache as NSString] = false as NSNumber - if let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { - fullSizeImage = image - } - } - } + var thumbnailImage: CGImage? - if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + + if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, options), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options) { thumbnailImage = image } @@ -1665,29 +3168,515 @@ func chatMessagePhotoThumbnail(account: Account, photo: TelegramMediaImage, scal blurredThumbnailImage = thumbnailContext.generateImage() } - context.withFlippedContext { c in - c.setBlendMode(.copy) - if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { - //c.setFillColor(UIColor(white: 0.0, alpha: 0.4).cgColor) - c.fill(arguments.drawingRect) + + if let combinedColor = arguments.emptyColor { + + let colors:[NSColor] + let intensity: CGFloat + let rotation: Int32? + switch combinedColor { + case let .color(combinedColor): + let color = combinedColor.withAlphaComponent(1.0) + intensity = combinedColor.alpha + colors = [color] + rotation = nil + case let .gradient(_colors, _intensity, _rotation): + intensity = _intensity + colors = _colors.reversed().map { $0.withAlphaComponent(1.0) } + rotation = _rotation } - c.setBlendMode(.copy) - if let cgImage = blurredThumbnailImage { - c.interpolationQuality = .low - c.draw(cgImage, in: fittedRect) + let context = DrawingContext(size: arguments.drawingSize, scale: scale, clear: true) + context.withFlippedContext { c in c.setBlendMode(.normal) + + if !drawPatternOnly { + if colors.count == 1, let color = colors.first { + c.setFillColor(color.cgColor) + c.fill(arguments.drawingRect) + } else { + + if palette.isDark, let image = fullSizeImage { + let size = image.size.aspectFilled(arguments.drawingRect.size) + let rect = arguments.drawingRect.focus(size) + + c.clear(rect) + c.setFillColor(NSColor.black.cgColor) + c.fill(rect) + c.clip(to: rect, mask: image) + + c.clear(rect) + c.setFillColor(NSColor.black.withAlphaComponent(0.3).cgColor) + c.fill(rect) + } + + if colors.count <= 2 { + let gradientColors = colors.map { $0.cgColor } as CFArray + let delta: CGFloat = 1.0 / (CGFloat(colors.count) - 1.0) + + var locations: [CGFloat] = [] + for i in 0 ..< colors.count { + locations.append(delta * CGFloat(i)) + } + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + + c.saveGState() + c.translateBy(x: arguments.drawingSize.width / 2.0, y: arguments.drawingSize.height / 2.0) + c.rotate(by: CGFloat(rotation ?? 0) * CGFloat.pi / -180.0) + c.translateBy(x: -arguments.drawingSize.width / 2.0, y: -arguments.drawingSize.height / 2.0) + + c.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: arguments.drawingSize.height), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]) + c.restoreGState() + } else { + let preview = AnimatedGradientBackgroundView.generatePreview(size: arguments.drawingSize.fitted(.init(width: 30, height: 30)), colors: colors) + + c.saveGState() + c.translateBy(x: arguments.drawingSize.width / 2.0, y: arguments.drawingSize.height / 2.0) + c.scaleBy(x: 1.0, y: -1.0) + c.translateBy(x: -arguments.drawingSize.width / 2.0, y: -arguments.drawingSize.height / 2.0) + c.draw(preview, in: arguments.drawingSize.bounds) + c.restoreGState() + } + } + } + + if let fullSizeImage = fullSizeImage, !palette.isDark { + if !drawPatternOnly { + c.setBlendMode(.softLight) + c.setAlpha(intensity) + } + c.draw(fullSizeImage, in: fittedRect) + } + } + addCorners(context, arguments: arguments, scale: scale) + return context + + } else { + context.withFlippedContext(isHighQuality: fullSizeImage != nil, { c in + c.setBlendMode(.copy) + if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { + //c.setFillColor(NSColor(white: 0.0, alpha: 0.4).cgColor) + c.fill(arguments.drawingRect) + } + + c.setBlendMode(.copy) + if let blurredThumbnailImage = blurredThumbnailImage { + c.interpolationQuality = .low + c.draw(blurredThumbnailImage, in: fittedRect) + c.setBlendMode(.normal) + } + + if let fullSizeImage = fullSizeImage { + c.interpolationQuality = .medium + c.draw(fullSizeImage, in: fittedRect) + } + }) - if let fullSizeImage = fullSizeImage { - c.interpolationQuality = .medium - c.draw(fullSizeImage, in: fittedRect) + addCorners(context, arguments: arguments, scale: scale) + + return context + } + }) + } +} + + +func patternWallpaperImage(account: Account, representations: [ImageRepresentationWithReference], mode: PatternWallpaperDrawMode, scale: CGFloat = 2.0, autoFetchFullSize: Bool = false, drawPatternOnly: Bool, palette: ColorPalette) -> Signal { + var prominent = false + if case .thumbnail = mode { + prominent = false + } + return chatWallpaperInternal(patternWallpaperDatas(account: account, representations: representations, mode: mode, autoFetchFullSize: autoFetchFullSize), prominent: prominent, scale: scale, drawPatternOnly: drawPatternOnly, palette: palette) +} + + +func chatWallpaper(account: Account, representations: [TelegramMediaImageRepresentation], file: TelegramMediaFile? = nil, webpage: TelegramMediaWebpage? = nil, slug: String? = nil, mode: PatternWallpaperDrawMode, isPattern: Bool, autoFetchFullSize: Bool = false, scale: CGFloat = 2.0, isBlurred: Bool = false, synchronousLoad: Bool = false, drawPatternOnly: Bool = false, palette: ColorPalette = theme.colors) -> Signal { + var prominent = false + if case .thumbnail = mode { + prominent = false + } + let signal: Signal + if isPattern { + let reps = representations.map { rep -> ImageRepresentationWithReference in + if let webpage = webpage, let file = file { + return ImageRepresentationWithReference(representation: rep, reference: MediaResourceReference.media(media: AnyMediaReference.webPage(webPage: WebpageReference(webpage), media: file), resource: rep.resource)) + } else { + return ImageRepresentationWithReference(representation: rep, reference: MediaResourceReference.wallpaper(wallpaper: slug != nil ? .slug(slug!) : nil, resource: rep.resource)) + } + } + signal = patternWallpaperDatas(account: account, representations: reps, mode: mode, autoFetchFullSize: autoFetchFullSize) + } else { + signal = chatWallpaperDatas(account: account, representations: representations, file: file, webpage: webpage, autoFetchFullSize: autoFetchFullSize, isBlurred: isBlurred, synchronousLoad: synchronousLoad) + } + return chatWallpaperInternal(signal, prominent: prominent, scale: scale, drawPatternOnly: drawPatternOnly, palette: palette) +} + + +func instantPageImageFile(account: Account, fileReference: FileMediaReference, scale: CGFloat, fetched: Bool = false) -> Signal { + return chatMessageFileDatas(account: account, fileReference: fileReference, progressive: false) + |> map { data in + return ImageDataTransformation(data: data, execute: { arguments, data in + assertNotOnMainThread() + let context = DrawingContext(size: arguments.drawingSize, scale: scale, clear: true) + + let drawingRect = arguments.drawingRect + let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) + + var fullSizeImage: CGImage? + var imageOrientation: ImageOrientation = .up + if let fullSizeData = data.fullSizeData { + let options = NSMutableDictionary() + options.setValue(max(fittedSize.width * context.scale, fittedSize.height * context.scale) as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) + options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + + if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, options), let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options) { + imageOrientation = imageOrientationFromSource(imageSource) + fullSizeImage = image + } + } + + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + + context.withFlippedContext { c in + if var fullSizeImage = fullSizeImage { + if let color = arguments.emptyColor { + switch color { + case let .color(color): + if imageRequiresInversion(fullSizeImage), let tintedImage = generateTintedImage(image: fullSizeImage, color: color) { + fullSizeImage = tintedImage + } + default: + break + } + } + + c.setBlendMode(.normal) + c.interpolationQuality = .medium + + drawImage(context: c, image: fullSizeImage, orientation: imageOrientation, in: fittedRect) + } + } + + addCorners(context, arguments: arguments, scale: scale) + + return context + }) + } +} + +private func rotationFor(_ orientation: ImageOrientation) -> CGFloat { + switch orientation { + case .left: + return CGFloat.pi / 2.0 + case .right: + return -CGFloat.pi / 2.0 + case .down: + return -CGFloat.pi + default: + return 0.0 + } +} + +func drawImage(context: CGContext, image: CGImage, orientation: ImageOrientation, in rect: CGRect) { + var restore = true + var drawRect = rect + switch orientation { + case .left: + fallthrough + case .right: + fallthrough + case .down: + let angle = rotationFor(orientation) + context.saveGState() + context.translateBy(x: rect.midX, y: rect.midY) + context.rotate(by: angle) + context.translateBy(x: -rect.midX, y: -rect.midY) + var t = CGAffineTransform(translationX: rect.midX, y: rect.midY) + t = t.rotated(by: angle) + t = t.translatedBy(x: -rect.midX, y: -rect.midY) + + drawRect = rect.applying(t) + case .leftMirrored: + context.saveGState() + context.translateBy(x: rect.midX, y: rect.midY) + context.rotate(by: -CGFloat.pi / 2.0) + context.translateBy(x: -rect.midX, y: -rect.midY) + var t = CGAffineTransform(translationX: rect.midX, y: rect.midY) + t = t.rotated(by: -CGFloat.pi / 2.0) + t = t.translatedBy(x: -rect.midX, y: -rect.midY) + + drawRect = rect.applying(t) + default: + restore = false + } + context.draw(image, in: drawRect) + if restore { + context.restoreGState() + } +} + + + + + +func mapResourceToAvatarSizes(postbox: Postbox, resource: MediaResource, representations: [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError> { + return postbox.mediaBox.resourceData(resource) + |> take(1) + |> map { data -> [Int: Data] in + guard data.complete, let image = NSImage(contentsOfFile: data.path)?.cgImage(forProposedRect: nil, context: nil, hints: nil) else { + return [:] + } + + let options = NSMutableDictionary() + options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) + + let colorQuality: Float = 0.6 + options.setObject(colorQuality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString) + + var result: [Int: Data] = [:] + for i in 0 ..< representations.count { + if let scaledImage = generateScaledImage(image: image, size: representations[i].dimensions.size, scale: 1.0) { + + let mutableData: CFMutableData = NSMutableData() as CFMutableData + if let colorDestination = CGImageDestinationCreateWithData(mutableData, kUTTypeJPEG, 1, options) { + CGImageDestinationSetProperties(colorDestination, nil) + + CGImageDestinationAddImage(colorDestination, scaledImage, options as CFDictionary) + if CGImageDestinationFinalize(colorDestination) { + + } + } + + result[i] = mutableData as Data + } + } + return result + } +} + + +public func generateScaledImage(image: CGImage?, size: CGSize, scale: CGFloat? = nil) -> CGImage? { + guard let image = image else { + return nil + } + + return generateImage(size, contextGenerator: { size, context in + context.draw(image, in: CGRect(origin: CGPoint(), size: size)) + }, opaque: true) +} + + +private func imageBuffer(from data: UnsafeMutableRawPointer!, width: vImagePixelCount, height: vImagePixelCount, rowBytes: Int) -> vImage_Buffer { + return vImage_Buffer(data: data, height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: rowBytes) +} + +func blurredImage(_ image: CGImage, radius: CGFloat, iterations: Int = 3) -> CGImage? { + guard let providerData = image.dataProvider?.data else { + return nil + } + + if image.size.width <= 0.0 || image.size.height <= 0 || radius <= 0 { + return image + } + + var boxSize = UInt32(radius) + if boxSize % 2 == 0 { + boxSize += 1 + } + + let bytes = image.bytesPerRow * image.height + let inData = malloc(bytes) + var inBuffer = imageBuffer(from: inData, width: vImagePixelCount(image.width), height: vImagePixelCount(image.height), rowBytes: image.bytesPerRow) + + let outData = malloc(bytes) + var outBuffer = imageBuffer(from: outData, width: vImagePixelCount(image.width), height: vImagePixelCount(image.height), rowBytes: image.bytesPerRow) + + let tempSize = vImageBoxConvolve_ARGB8888(&inBuffer, &outBuffer, nil, 0, 0, boxSize, boxSize, nil, vImage_Flags(kvImageEdgeExtend + kvImageGetTempBufferSize)) + let tempData = malloc(tempSize) + + defer { + free(inData) + free(outData) + free(tempData) + } + + let source = CFDataGetBytePtr(providerData) + memcpy(inBuffer.data, source, bytes) + + for _ in 0 ..< iterations { + vImageBoxConvolve_ARGB8888(&inBuffer, &outBuffer, tempData, 0, 0, boxSize, boxSize, nil, vImage_Flags(kvImageEdgeExtend)) + + let temp = inBuffer.data + inBuffer.data = outBuffer.data + outBuffer.data = temp + } + + let context = image.colorSpace.flatMap { + CGContext(data: inBuffer.data, width: image.width, height: image.height, bitsPerComponent: image.bitsPerComponent, bytesPerRow: image.bytesPerRow, space: $0, bitmapInfo: image.bitmapInfo.rawValue) + } + + let blurredCGImage = context?.makeImage() + if let blurredCGImage = blurredCGImage { + return blurredCGImage + } else { + return nil + } +} + + + + + +func patternColor(for color: NSColor, intensity: CGFloat, prominent: Bool = false) -> NSColor { + var hue: CGFloat = 0.0 + var saturation: CGFloat = 0.0 + var brightness: CGFloat = 0.0 + var alpha: CGFloat = 0.0 + color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) + if brightness > 0.5 { + brightness = max(0.0, brightness * 0.65) + } else { + brightness = max(0.0, min(1.0, 1.0 - brightness * 0.65)) + } + saturation = min(1.0, saturation + 0.05 + 0.1 * (1.0 - saturation)) + alpha = (prominent ? 0.5 : 0.4) * intensity + return NSColor(hue: hue, saturation: saturation, brightness: brightness, alpha: alpha) +} + + + + +func prepareTextAttachments(_ attachments: [NSTextAttachment]) -> Signal<[URL], NoError> { + return Signal { subscriber in + + var cancelled: Bool = false + + resourcesQueue.async { + var urls:[URL] = [] + + for attachment in attachments { + if cancelled { + for url in urls { + try? FileManager.default.removeItem(at: url) + } + subscriber.putCompletion() + return + } + if let fileWrapper = attachment.fileWrapper, fileWrapper.isRegularFile { + if let data = fileWrapper.regularFileContents { + if let fileName = fileWrapper.filename { + let path = NSTemporaryDirectory() + fileName + var newPath = path + var i:Int = 0 + if FileManager.default.fileExists(atPath: newPath) { + newPath = path.nsstring.deletingPathExtension + "\(i)." + path.nsstring.pathExtension + i += 1 + } + let url = URL(fileURLWithPath: newPath) + do { + try data.write(to: url) + urls.append(url) + } catch {} + } + } } } + subscriber.putNext(urls) + subscriber.putCompletion() + } + + return ActionDisposable { + cancelled = true + } + } |> runOn(prepareQueue) +} + + + + +func chatMapSnapshotData(account: Account, resource: MapSnapshotMediaResource) -> Signal { + return Signal { subscriber in + let dataDisposable = account.postbox.mediaBox.cachedResourceRepresentation(resource, representation: MapSnapshotMediaResourceRepresentation(), complete: true).start(next: { next in + if next.size != 0 { + subscriber.putNext(next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: [])) + } + }, error: subscriber.putError, completed: subscriber.putCompletion) + + return ActionDisposable { + dataDisposable.dispose() + } + } +} + + +public func chatMapSnapshotImage(account: Account, resource: MapSnapshotMediaResource) -> Signal { + let signal = chatMapSnapshotData(account: account, resource: resource) + + return signal |> map { data in + return ImageDataTransformation(data: ImageRenderData(nil, data, data != nil), execute: { arguments, data in - addCorners(context, arguments: arguments, scale: scale) + let fullSizeData = data.fullSizeData + + let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale, clear: true) + + var fullSizeImage: CGImage? + if let fullSizeData = fullSizeData { + let options = NSMutableDictionary() + options[kCGImageSourceShouldCache as NSString] = false as NSNumber + if let imageSource = CGImageSourceCreateWithData(fullSizeData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, options as CFDictionary) { + fullSizeImage = image + } + + if let fullSizeImage = fullSizeImage { + let drawingRect = arguments.drawingRect + var fittedSize = CGSize(width: CGFloat(fullSizeImage.width), height: CGFloat(fullSizeImage.height)).aspectFilled(drawingRect.size) + if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.width = arguments.boundingSize.width + } + if abs(fittedSize.height - arguments.boundingSize.height).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.height = arguments.boundingSize.height + } + + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + + context.withFlippedContext { c in + c.setBlendMode(.copy) + if arguments.imageSize.width < arguments.boundingSize.width || arguments.imageSize.height < arguments.boundingSize.height { + c.fill(arguments.drawingRect) + } + + c.setBlendMode(.copy) + + c.interpolationQuality = .medium + c.draw(fullSizeImage, in: fittedRect) + + c.setBlendMode(.normal) + } + } else { + context.withFlippedContext { c in + c.setBlendMode(.copy) + if let empty = arguments.emptyColor { + switch empty { + case let .color(color): + c.setFillColor(color.cgColor) + default: + c.setFillColor(.white) + } + } else { + c.setFillColor(.white) + } + c.fill(arguments.drawingRect) + + c.setBlendMode(.normal) + } + } + } + addCorners(context, arguments: arguments, scale: arguments.scale) return context - } + }) } } diff --git a/Telegram-Mac/MergedAvatarsView.swift b/Telegram-Mac/MergedAvatarsView.swift new file mode 100644 index 0000000000..af66171f9d --- /dev/null +++ b/Telegram-Mac/MergedAvatarsView.swift @@ -0,0 +1,169 @@ +// +// MergedAvatarsView.swift +// Telegram +// +// Created by Mikhail Filimonov on 03/09/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore +import SwiftSignalKit +import Postbox + + +private enum PeerAvatarReference : Equatable { + static func == (lhs: PeerAvatarReference, rhs: PeerAvatarReference) -> Bool { + switch lhs { + case let .image(lhsPeer, rep): + if case .image(let rhsPeer, rep) = rhs { + return lhsPeer.isEqual(rhsPeer) + } else { + return false + } + } + } + + case image(Peer, TelegramMediaImageRepresentation?) + + var peerId: PeerId { + switch self { + case let .image(value, _): + return value.id + } + } +} + +private extension PeerAvatarReference { + init(peer: Peer) { + self = .image(peer, peer.smallProfileImage) + } +} + + + +final class MergedAvatarsView: Control { + + init(mergedImageSize: CGFloat = 16.0, mergedImageSpacing: CGFloat = 15.0, avatarFont: NSFont = NSFont.avatar(8.0)) { + self.mergedImageSize = mergedImageSize + self.mergedImageSpacing = mergedImageSpacing + self.avatarFont = avatarFont + super.init() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required convenience init(frame frameRect: NSRect) { + self.init() + } + + let mergedImageSize: CGFloat + let mergedImageSpacing: CGFloat + + let avatarFont: NSFont + + private var peers: [PeerAvatarReference] = [] + private var images: [PeerId: CGImage] = [:] + private var disposables: [PeerId: Disposable] = [:] + + + deinit { + for (_, disposable) in self.disposables { + disposable.dispose() + } + } + + func update(context: AccountContext, peers: [Peer], message: Message?, synchronousLoad: Bool) { + let filteredPeers = Array(peers.map(PeerAvatarReference.init).prefix(3)) + + if filteredPeers != self.peers { + self.peers = filteredPeers + + var validImageIds: [PeerId] = [] + for peer in filteredPeers { + if case .image = peer { + validImageIds.append(peer.peerId) + } + } + + var removedImageIds: [PeerId] = [] + for (id, _) in self.images { + if !validImageIds.contains(id) { + removedImageIds.append(id) + } + } + var removedDisposableIds: [PeerId] = [] + for (id, disposable) in self.disposables { + if !validImageIds.contains(id) { + disposable.dispose() + removedDisposableIds.append(id) + } + } + for id in removedImageIds { + self.images.removeValue(forKey: id) + } + for id in removedDisposableIds { + self.disposables.removeValue(forKey: id) + } + for peer in filteredPeers { + switch peer { + case let .image(peer, representation): + if self.disposables[peer.id] == nil { + let signal = peerAvatarImage(account: context.account, photo: PeerPhoto.peer(peer, representation, peer.displayLetters, message), displayDimensions: NSMakeSize(mergedImageSize, mergedImageSize), scale: backingScaleFactor, font: avatarFont, synchronousLoad: synchronousLoad) + let disposable = (signal + |> deliverOnMainQueue).start(next: { [weak self] image in + guard let strongSelf = self else { + return + } + if let image = image.0 { + strongSelf.images[peer.id] = image + strongSelf.setNeedsDisplay() + } + }) + self.disposables[peer.id] = disposable + } + } + } + self.setNeedsDisplay() + } + } + + override func draw(_ layer: CALayer, in context: CGContext) { + super.draw(layer, in: context) + + + context.setBlendMode(.copy) + context.setFillColor(NSColor.clear.cgColor) + context.fill(bounds) + + + context.setBlendMode(.copy) + + var currentX = mergedImageSize + mergedImageSpacing * CGFloat(self.peers.count - 1) - mergedImageSize + for i in (0 ..< self.peers.count).reversed() { + context.saveGState() + + context.translateBy(x: frame.width / 2.0, y: frame.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -frame.width / 2.0, y: -frame.height / 2.0) + + let imageRect = CGRect(origin: CGPoint(x: currentX, y: 0.0), size: CGSize(width: mergedImageSize, height: mergedImageSize)) + context.setFillColor(NSColor.clear.cgColor) + context.fillEllipse(in: imageRect.insetBy(dx: -1.0, dy: -1.0)) + + if let image = self.images[self.peers[i].peerId] { + context.draw(image, in: imageRect) + } else { + context.setFillColor(NSColor.gray.cgColor) + context.fillEllipse(in: imageRect) + } + + currentX -= mergedImageSpacing + context.restoreGState() + } + } +} + diff --git a/Telegram-Mac/MessageActionsPanelView.swift b/Telegram-Mac/MessageActionsPanelView.swift index 3a9bd8bf61..d07bf6aa1d 100644 --- a/Telegram-Mac/MessageActionsPanelView.swift +++ b/Telegram-Mac/MessageActionsPanelView.swift @@ -8,9 +8,10 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac -import TelegramCoreMac -import PostboxMac +import SwiftSignalKit +import TelegramCore + +import Postbox @@ -19,9 +20,7 @@ class MessageActionsPanelView: Control, Notifable { private var deleteButton:TitleButton = TitleButton() private var forwardButton:TitleButton = TitleButton() private var countTitle:TitleButton = TitleButton() - - private let loadMessagesDisposable:MetaDisposable = MetaDisposable() - + required init(frame frameRect: NSRect) { super.init(frame: frameRect) @@ -39,11 +38,11 @@ class MessageActionsPanelView: Control, Notifable { addSubview(forwardButton) addSubview(countTitle) - updateLocalizationAndTheme() + updateLocalizationAndTheme(theme: theme) } private var buttonActiveStyle:ControlStyle { - return ControlStyle(font:.normal(.header), foregroundColor: theme.colors.grayText, backgroundColor: theme.colors.background, highlightColor: theme.colors.blueIcon) + return ControlStyle(font:.normal(.header), foregroundColor: theme.colors.grayText, backgroundColor: theme.colors.background, highlightColor: theme.colors.accentIcon) } private var deleteButtonActiveStyle:ControlStyle { return ControlStyle(font:.normal(.header), foregroundColor: theme.colors.grayText, backgroundColor: theme.colors.background, highlightColor: theme.colors.redUI) @@ -82,40 +81,46 @@ class MessageActionsPanelView: Control, Notifable { forwardButton.userInteractionEnabled = canForward deleteButton.set(color: !canDelete ? theme.colors.grayText : theme.colors.redUI, for: .Normal) - forwardButton.set(color: !canForward ? theme.colors.grayText : theme.colors.blueUI, for: .Normal) - - deleteButton.set(image: !deleteButton.userInteractionEnabled ? theme.icons.chatDeleteMessagesInactive : theme.icons.chatDeleteMessagesActive, for: .Normal) - forwardButton.set(image: !forwardButton.userInteractionEnabled ? theme.icons.chatForwardMessagesInactive : theme.icons.chatForwardMessagesActive, for: .Normal) + forwardButton.set(color: !canForward ? theme.colors.grayText : theme.colors.accent, for: .Normal) + + deleteButton.set(text: leftText, for: .Normal) + forwardButton.set(text: rightText, for: .Normal) + + if let leftIcon = leftIcon { + deleteButton.set(image: leftIcon, for: .Normal) + } else { + deleteButton.removeImage(for: .Normal) + } + if let rightIcon = rightIcon { + forwardButton.set(image: rightIcon, for: .Normal) + } else { + forwardButton.removeImage(for: .Normal) + } - countTitle.set(text: count == 0 ? tr(.messageActionsPanelEmptySelected) : tr(.messageActionsPanelSelectedCountCountable(count)), for: .Normal) + deleteButton.scaleOnClick = true + forwardButton.scaleOnClick = true + + deleteButton.set(color: !deleteButton.userInteractionEnabled ? theme.colors.grayIcon : leftColor, for: .Normal) + forwardButton.set(color: !forwardButton.userInteractionEnabled ? theme.colors.grayIcon : rightColor, for: .Normal) + + + countTitle.set(text: count == 0 ? L10n.messageActionsPanelEmptySelected : L10n.messageActionsPanelSelectedCountCountable(count), for: .Normal) countTitle.set(color: (!canForward && !canDelete) || count == 0 ? theme.colors.grayText : theme.colors.text, for: .Normal) countTitle.sizeToFit(NSZeroSize, NSMakeSize(frame.width - deleteButton.frame.width - forwardButton.frame.width - 80, frame.height)) countTitle.center() } func notify(with value: Any, oldValue: Any, animated:Bool) { - if let selectingState = (value as? ChatPresentationInterfaceState)?.selectionState, let account = chatInteraction?.account { - let ids = Array(selectingState.selectedIds) - loadMessagesDisposable.set((account.postbox.messagesAtIds(ids) |> deliverOnMainQueue).start( next:{ [weak self] messages in - var canDelete:Bool = !ids.isEmpty - var canForward:Bool = !ids.isEmpty - for message in messages { - if !canDeleteMessage(message, account: account) { - canDelete = false - } - if !canForwardMessage(message, account: account) { - canForward = false - } - } - self?.updateUI(canDelete, canForward, ids.count) - - })) - + if let value = value as? ChatPresentationInterfaceState, let selectionState = value.selectionState { + if value.reportMode != nil { + updateUI(true, selectionState.selectedIds.count > 0, selectionState.selectedIds.count) + } else { + updateUI(value.canInvokeBasicActions.delete, value.canInvokeBasicActions.forward, selectionState.selectedIds.count) + } } } deinit { - loadMessagesDisposable.dispose() } func isEqual(to other: Notifable) -> Bool { @@ -133,31 +138,76 @@ class MessageActionsPanelView: Control, Notifable { self.chatInteraction = chatInteraction self.chatInteraction?.add(observer: self) - forwardButton.set(handler: {_ in - chatInteraction.forwardSelectedMessages() + forwardButton.set(handler: { [weak chatInteraction] _ in + chatInteraction?.forwardSelectedMessages() }, for: .Click) - deleteButton.set(handler: {_ in - chatInteraction.deleteSelectedMessages() + deleteButton.set(handler: { [weak chatInteraction] _ in + chatInteraction?.deleteSelectedMessages() }, for: .Click) self.notify(with: chatInteraction.presentation, oldValue: chatInteraction.presentation, animated: false) } + + private var leftColor: NSColor { + if chatInteraction?.presentation.reportMode != nil { + return theme.colors.accent + } + return theme.colors.redUI + } + private var rightColor: NSColor { + if chatInteraction?.presentation.reportMode != nil { + return theme.colors.redUI + } + return theme.colors.accent + } + + private var leftText: String { + if chatInteraction?.presentation.reportMode != nil { + return L10n.modalCancel + } + return L10n.messageActionsPanelDelete + } + private var rightText: String { + if chatInteraction?.presentation.reportMode != nil { + return L10n.modalReport + } + return L10n.messageActionsPanelForward + } + private var leftIcon: CGImage? { + if chatInteraction?.presentation.reportMode != nil { + return nil + } + return !deleteButton.userInteractionEnabled ? theme.icons.chatDeleteMessagesInactive : theme.icons.chatDeleteMessagesActive + } + private var rightIcon: CGImage? { + if chatInteraction?.presentation.reportMode != nil { + return nil + } + return !forwardButton.userInteractionEnabled ? theme.icons.chatForwardMessagesInactive : theme.icons.chatForwardMessagesActive + } - - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - - deleteButton.set(text: tr(.messageActionsPanelDelete), for: .Normal) - forwardButton.set(text: tr(.messageActionsPanelForward), for: .Normal) - - deleteButton.set(image: !deleteButton.userInteractionEnabled ? theme.icons.chatDeleteMessagesInactive : theme.icons.chatDeleteMessagesActive, for: .Normal) - forwardButton.set(image: !forwardButton.userInteractionEnabled ? theme.icons.chatForwardMessagesInactive : theme.icons.chatForwardMessagesActive, for: .Normal) - - deleteButton.set(color: !deleteButton.userInteractionEnabled ? theme.colors.grayText : theme.colors.redUI, for: .Normal) - forwardButton.set(color: !forwardButton.userInteractionEnabled ? theme.colors.grayText : theme.colors.blueUI, for: .Normal) + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) + deleteButton.set(text: leftText, for: .Normal) + forwardButton.set(text: rightText, for: .Normal) + + if let leftIcon = leftIcon { + deleteButton.set(image: leftIcon, for: .Normal) + } else { + deleteButton.removeImage(for: .Normal) + } + if let rightIcon = rightIcon { + forwardButton.set(image: rightIcon, for: .Normal) + } else { + forwardButton.removeImage(for: .Normal) + } + + deleteButton.set(color: !deleteButton.userInteractionEnabled ? theme.colors.grayIcon : leftColor, for: .Normal) + forwardButton.set(color: !forwardButton.userInteractionEnabled ? theme.colors.grayIcon : rightColor, for: .Normal) - deleteButton.sizeToFit(NSZeroSize, NSMakeSize(0, frame.height)) - forwardButton.sizeToFit(NSZeroSize, NSMakeSize(0, frame.height)) + _ = deleteButton.sizeToFit(NSZeroSize, NSMakeSize(0, frame.height)) + _ = forwardButton.sizeToFit(NSZeroSize, NSMakeSize(0, frame.height)) deleteButton.style = deleteButtonActiveStyle forwardButton.style = buttonActiveStyle diff --git a/Telegram-Mac/MessageReadMenuItem.swift b/Telegram-Mac/MessageReadMenuItem.swift new file mode 100644 index 0000000000..49165fe2ae --- /dev/null +++ b/Telegram-Mac/MessageReadMenuItem.swift @@ -0,0 +1,472 @@ +// +// MessageViewsMenuItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 05.09.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TelegramCore +import SwiftSignalKit +import TGUIKit +import Postbox + + + + +private final class MessageViewsMenuItemView : Control { + + final class AvatarContentView: View { + private var disposable: Disposable? + private var images:[CGImage] = [] + init(context: AccountContext, message: Message, peers:[Peer]?, size: NSSize) { + + let count: CGFloat = peers != nil ? CGFloat(peers!.count) : 3 + let viewSize = NSMakeSize(size.width * count - (count - 1) * 1, size.height) + + super.init(frame: CGRect(origin: .zero, size: viewSize)) + + if let peers = peers { + let signal:Signal<[(CGImage?, Bool)], NoError> = combineLatest(peers.map { peer in + return peerAvatarImage(account: context.account, photo: .peer(peer, peer.smallProfileImage, peer.displayLetters, nil), displayDimensions: size, scale: System.backingScale, font: .avatar(size.height / 3 + 3), genCap: true, synchronousLoad: false) + }) + + + let disposable = (signal + |> deliverOnMainQueue).start(next: { [weak self] values in + guard let strongSelf = self else { + return + } + let images = values.compactMap { $0.0 } + strongSelf.updateImages(images) + }) + self.disposable = disposable + } else { + let image = generateImage(size, rotatedContext: { size, ctx in + ctx.clear(size.bounds) + ctx.setFillColor(theme.colors.grayUI.withAlphaComponent(0.8).cgColor) + ctx.fillEllipse(in: size.bounds) + })! + self.images = [image, image, image] + } + + } + + override func draw(_ layer: CALayer, in context: CGContext) { + super.draw(layer, in: context) + + + let mergedImageSize: CGFloat = 15.0 + let mergedImageSpacing: CGFloat = 13.0 + + context.setBlendMode(.copy) + context.setFillColor(NSColor.clear.cgColor) + context.fill(bounds) + + context.setBlendMode(.copy) + + + var currentX = mergedImageSize + mergedImageSpacing * CGFloat(images.count - 1) - mergedImageSize + for i in 0 ..< self.images.count { + + let image = self.images[i] + + context.saveGState() + + context.translateBy(x: frame.width / 2.0, y: frame.height / 2.0) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -frame.width / 2.0, y: -frame.height / 2.0) + + let imageRect = CGRect(origin: CGPoint(x: currentX, y: 0.0), size: CGSize(width: mergedImageSize, height: mergedImageSize)) + context.setFillColor(NSColor.clear.cgColor) + context.fillEllipse(in: imageRect.insetBy(dx: -1.0, dy: -1.0)) + + context.draw(image, in: imageRect) + + currentX -= mergedImageSpacing + context.restoreGState() + } + } + + private func updateImages(_ images: [CGImage]) { + self.images = images + needsDisplay = true + } + + deinit { + disposable?.dispose() + } + + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + } + + + private let selectedView = View() + private let textView = NSTextField() + + private var contentView: AvatarContentView? + private var loadingView: View? + + private var state: MessageReadMenuItem.State? + private var context: AccountContext? + private var message: Message? + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + selectedView.layer?.cornerRadius = 3 + addSubview(selectedView) + textView.isBordered = false + textView.isBezeled = false + textView.isSelectable = false + textView.isEditable = false + textView.drawsBackground = false + textView.backgroundColor = .clear + textView.wantsLayer = true + addSubview(textView) + } + + private let disposableSet: DisposableDict = DisposableDict() + + func updateState(_ state: MessageReadMenuItem.State, message: Message, context: AccountContext, animated: Bool) -> Void { + self.state = state + self.context = context + self.message = message + + if let item = self.enclosingMenuItem { + + switch state { + case let .stats(peers): + if peers.count > 1 { + let menu = ContextMenu() + var items:[ContextMenuItem] = [] + + for peer in peers { + let item = ContextMenuItem(peer.displayTitle.prefixWithDots(25), handler: { + context.sharedContext.bindings.rootNavigation().push(PeerInfoController(context: context, peerId: peer.id)) + }) + let avatar = peerAvatarImage(account: context.account, photo: .peer(peer, peer.smallProfileImage, peer.displayLetters, nil), displayDimensions: NSMakeSize(30, 30), scale: 1, font: .avatar(5), genCap: true, synchronousLoad: false) |> deliverOnMainQueue + + disposableSet.set(avatar.start(next: { [weak item] image, _ in + DispatchQueue.main.async { + item?.image = image?._NSImage + } + }), forKey: peer.id) + + items.append(item) + } + for item in items { + menu.addItem(item) + } + item.submenu = menu + } + + default: + break + } + } + + if let item = self.enclosingMenuItem { + updateIsSelected(item.isHighlighted && !state.isEmpty, message: message, context: context, state: state, animated: animated) + } + } + + override func draw(_ dirtyRect: NSRect) { + super.draw(dirtyRect) + } + + deinit { + disposableSet.dispose() + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + super.draw(layer, in: ctx) + if let item = self.enclosingMenuItem, let state = self.state, let context = self.context, let message = self.message { + updateIsSelected(item.isHighlighted && !state.isEmpty, message: message, context: context, state: state, animated: false) + } + needsLayout = true + + } + + var isDark: Bool { + let isDark:Bool + + if #available(macOS 10.14, *) { + isDark = effectiveAppearance.name == .darkAqua || effectiveAppearance.name == .vibrantDark + } else { + isDark = effectiveAppearance.name == .vibrantDark + } + return isDark + } + + func updateIsSelected(_ isSelected: Bool, message: Message, context: AccountContext, state: MessageReadMenuItem.State, animated: Bool) { + selectedView.isHidden = !isSelected + selectedView.backgroundColor = theme.colors.accent //NSColor.selectedMenuItemColor + + + + let textColor: NSColor = isSelected ? .white : (isDark ? .white : .black) + let textLayot: TextViewLayout? + let contentView: AvatarContentView? + let loadingView: View? + switch state { + case .empty: + let text: String + if let media = message.media.first as? TelegramMediaFile { + if media.isInstantVideo { + text = L10n.chatMessageReadStatsEmptyWatches + } else if media.isVoice { + text = L10n.chatMessageReadStatsEmptyListens + } else { + text = L10n.chatMessageReadStatsEmptyViews + } + } else { + text = L10n.chatMessageReadStatsEmptyViews + } + textLayot = TextViewLayout(.initialize(string: text, color: textColor, font: .normal(.text))) + contentView = nil + loadingView = nil + case .loading: + textLayot = nil + contentView = .init(context: context, message: message, peers: nil, size: NSMakeSize(15, 15)) + loadingView = View(frame: NSMakeRect(0, 0, 20, 6)) + loadingView?.layer?.cornerRadius = 3 + loadingView?.backgroundColor = (isDark ? .white : .black) + case let .stats(peers): + if peers.count == 1 { + let text: String = peers[0].displayTitle + textLayot = TextViewLayout(.initialize(string: text, color: textColor, font: .normal(.text))) + loadingView = nil + contentView = .init(context: context, message: message, peers: peers, size: NSMakeSize(15, 15)) + } else { + let text: String + if let media = message.media.first as? TelegramMediaFile { + if media.isInstantVideo { + text = L10n.chatMessageReadStatsWatchedCountable(peers.count) + } else if media.isVoice { + text = L10n.chatMessageReadStatsListenedCountable(peers.count) + } else { + text = L10n.chatMessageReadStatsSeenCountable(peers.count) + } + } else { + text = L10n.chatMessageReadStatsSeenCountable(peers.count) + } + textLayot = TextViewLayout(.initialize(string: text, color: textColor, font: .normal(.text))) + loadingView = nil + contentView = .init(context: context, message: message, peers: Array(peers.prefix(3)), size: NSMakeSize(15, 15)) + } + + } + textLayot?.measure(width: frame.width - 40) + + if let contentView = self.contentView { + performSubviewRemoval(contentView, animated: animated) + } + self.contentView = contentView + if let contentView = contentView { + addSubview(contentView) + if animated { + contentView.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } + + if let loadingView = self.loadingView { + performSubviewRemoval(loadingView, animated: false) + } + self.loadingView = loadingView + if let loadingView = loadingView { + addSubview(loadingView) + if animated { + loadingView.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } + textView._change(opacity: textLayot != nil ? 1 : 0, animated: false) + if let textLayot = textLayot { + textView.attributedStringValue = textLayot.attributedString + textView.sizeToFit() + } + + self.removeAllHandlers() + self.set(handler: { control in + switch state { + case let .stats(peers): + if peers.count == 1 { + let peer = peers[0] + context.sharedContext.bindings.rootNavigation().push(PeerInfoController(context: context, peerId: peer.id)) + control.enclosingMenuItem?.menu?.cancelTracking() + } + default: + break + } + }, for: .Click) + + needsLayout = true + } + + private var frameSetted: Bool = false + override func layout() { + if let view = superview, self.frame != view.bounds, !frameSetted { + frameSetted = true + self.frame = view.bounds + } + + let minx: CGFloat = 6 + + selectedView.frame = frame.insetBy(dx: minx, dy: 0) + textView.centerY(x: minx * 2, addition: -1) + + if let contentView = contentView { + contentView.centerY(x: frame.width - contentView.frame.width - minx * 2) + } + if let loadingView = loadingView { + loadingView.centerY(x: minx * 2) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + + + +final class MessageReadMenuItem { + + + enum State { + case loading + case empty + case stats([Peer]) + + var isEmpty: Bool { + switch self { + case let .stats(peers): + return peers.isEmpty + default: + return true + } + } + } + + fileprivate let context: AccountContext + fileprivate let message: Message + fileprivate let disposable = MetaDisposable() + + private let state: Promise = Promise(.loading) + + init(context: AccountContext, message: Message) { + self.context = context + self.message = message + DispatchQueue.main.async { [weak self] in + self?.load() + } + } + + func load() { + let readStats: Signal = context.engine.messages.messageReadStats(id: message.id) + |> deliverOnMainQueue + |> map { value in + if let value = value, !value.peers.isEmpty { + return .stats(value.peers.map { $0._asPeer() }) + } else { + return .empty + } + } + self.state.set(readStats |> deliverOnMainQueue) + + + let context = self.context + let message = self.message + + disposable.set(self.state.get().start(next: { [weak self] state in + self?._cachedView?.updateState(state, message: message, context: context, animated: true) + })) + } + + deinit { + disposable.dispose() + } + + private var _cachedView: MessageViewsMenuItemView? = nil + var view: View { + if let _cachedView = _cachedView { + return _cachedView + } + _cachedView = MessageViewsMenuItemView(frame: NSMakeRect(0, 0, 180, 20)) + _cachedView?.updateState(.loading, message: message, context: context, animated: false) + return _cachedView! + } + + + static func canViewReadStats(message: Message, chatInteraction: ChatInteraction, appConfig: AppConfiguration) -> Bool { + + guard let peer = message.peers[message.id.peerId] else { + return false + } + + if message.flags.contains(.Incoming) { + return false + } + for media in message.media { + if let _ = media as? TelegramMediaAction { + return false + } + } + + for attr in message.attributes { + if let attr = attr as? ConsumableContentMessageAttribute { + if !attr.consumed { + return false + } + } + } + var maxParticipantCount = 50 + var maxTimeout = 7 * 86400 + if let data = appConfig.data { + if let value = data["chat_read_mark_size_threshold"] as? Double { + maxParticipantCount = Int(value) + } + if let value = data["chat_read_mark_expire_period"] as? Double { + maxTimeout = Int(value) + } + } + + switch peer { + case let channel as TelegramChannel: + if case .broadcast = channel.info { + return false + } else { + if let cachedData = chatInteraction.getCachedData() as? CachedChannelData { + let members = cachedData.participantsSummary.memberCount ?? 0 + if members > maxParticipantCount { + return false + } + } else { + return false + } + } + + case let group as TelegramGroup: + if group.participantCount > maxParticipantCount { + return false + } + default: + return false + } + + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + if Int64(message.timestamp) + Int64(maxTimeout) < Int64(timestamp) { + return false + } + + return true + } + +} diff --git a/Telegram-Mac/MessageSharedRowItem.swift b/Telegram-Mac/MessageSharedRowItem.swift new file mode 100644 index 0000000000..9adc078a35 --- /dev/null +++ b/Telegram-Mac/MessageSharedRowItem.swift @@ -0,0 +1,141 @@ +// +// MessageSharedRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 28/10/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import Postbox +import TelegramCore + +import SwiftSignalKit + +class MessageSharedRowItem: GeneralRowItem { + fileprivate let viewsCountLayout: TextViewLayout + fileprivate let titleLayout: TextViewLayout + fileprivate let message: Message + fileprivate let context: AccountContext + init(_ initialSize: NSSize, stableId: AnyHashable, context: AccountContext, message: Message, viewType: GeneralViewType, action: @escaping()->Void) { + self.context = context + self.message = message + + self.titleLayout = TextViewLayout(.initialize(string: message.effectiveAuthor?.displayTitle ?? "", color: theme.colors.text, font: .medium(.text)), maximumNumberOfLines: 1) + + let views = Int(message.channelViewsCount ?? 0) + + let viewsString = L10n.channelStatsViewsCountCountable(views).replacingOccurrences(of: "\(views)", with: views.formattedWithSeparator) + + viewsCountLayout = TextViewLayout(.initialize(string: viewsString, color: theme.colors.grayText, font: .normal(.short)),maximumNumberOfLines: 1) + + super.init(initialSize, height: 46, stableId: stableId, viewType: viewType, action: action) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + + viewsCountLayout.measure(width: .greatestFiniteMagnitude) + let titleAndDateWidth: CGFloat = blockWidth - viewType.innerInset.left - viewType.innerInset.right + + titleLayout.measure(width: titleAndDateWidth) + + return true + } + + override func viewClass() -> AnyClass { + return MessageSharedRowView.self + } +} + + +private final class MessageSharedRowView : GeneralContainableRowView { + private let viewCountView = TextView() + private let titleView = TextView() + private var imageView: AvatarControl = AvatarControl(font: .avatar(15)) + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(viewCountView) + addSubview(titleView) + addSubview(imageView) + + titleView.userInteractionEnabled = false + titleView.isSelectable = false + titleView.isEventLess = true + + viewCountView.userInteractionEnabled = false + viewCountView.isSelectable = false + viewCountView.isEventLess = true + + imageView.setFrameSize(NSMakeSize(30, 30)) + + containerView.set(handler: { [weak self] _ in + self?.updateColors() + }, for: .Highlight) + containerView.set(handler: { [weak self] _ in + self?.updateColors() + }, for: .Normal) + containerView.set(handler: { [weak self] _ in + self?.updateColors() + }, for: .Hover) + + containerView.set(handler: { [weak self] _ in + guard let item = self?.item as? MessageSharedRowItem else { + return + } + item.action() + }, for: .Click) + } + + override func layout() { + super.layout() + + guard let item = item as? MessageSharedRowItem else { + return + } + + let leftOffset: CGFloat = 34 + 10 + item.viewType.innerInset.left + + titleView.setFrameOrigin(NSMakePoint(leftOffset, 7)) + viewCountView.setFrameOrigin(NSMakePoint(leftOffset, containerView.frame.height - viewCountView.frame.height - 7)) + + imageView.centerY(x: item.viewType.innerInset.left) + } + + override var backdorColor: NSColor { + return isSelect ? theme.colors.accentSelect : theme.colors.background + } + + override func updateColors() { + super.updateColors() + if let item = item as? GeneralRowItem { + self.background = item.viewType.rowBackground + let highlighted = isSelect ? self.backdorColor : theme.colors.grayHighlight + titleView.backgroundColor = containerView.controlState == .Highlight && !isSelect ? .clear : self.backdorColor + viewCountView.backgroundColor = containerView.controlState == .Highlight && !isSelect ? .clear : self.backdorColor + containerView.set(background: self.backdorColor, for: .Normal) + containerView.set(background: highlighted, for: .Highlight) + } + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? MessageSharedRowItem else { + return + } + + viewCountView.update(item.viewsCountLayout) + titleView.update(item.titleLayout) + imageView.setPeer(account: item.context.account, peer: item.message.effectiveAuthor, message: item.message) + } + + deinit { + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/MessageStatsController.swift b/Telegram-Mac/MessageStatsController.swift new file mode 100644 index 0000000000..a144c0f481 --- /dev/null +++ b/Telegram-Mac/MessageStatsController.swift @@ -0,0 +1,232 @@ +// +// MessageStatsController.swift +// Telegram +// +// Created by Mikhail Filimonov on 28/10/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit +import TelegramCore +import Postbox + +import GraphCore + + + +private func _id_message(_ messageId: MessageId) -> InputDataIdentifier { + return InputDataIdentifier("_id_message_\(messageId)") +} + +private func statsEntries(_ stats: MessageStats?, _ search: (SearchMessagesResult, SearchMessagesState)?, _ uiState: UIStatsState, openMessage: @escaping(MessageId) -> Void, updateIsLoading: @escaping(InputDataIdentifier, Bool)->Void, context: MessageStatsContext, accountContext: AccountContext) -> [InputDataEntry] { + var entries: [InputDataEntry] = [] + + var sectionId: Int32 = 0 + var index: Int32 = 0 + + + if let stats = stats { + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.channelStatsOverview), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + var overviewItems:[ChannelOverviewItem] = [] + + overviewItems.append(ChannelOverviewItem(title: "Views", value: .initialize(string: stats.views.formattedWithSeparator, color: theme.colors.text, font: .medium(.text)))) + + if let search = search, search.0.totalCount > 0 { + overviewItems.append(ChannelOverviewItem(title: L10n.statsMessagePublicForwardsTitle, value: .initialize(string: Int(search.0.totalCount).formattedWithSeparator, color: theme.colors.text, font: .medium(.text)))) + } + + if stats.forwards > 0 { + overviewItems.append(ChannelOverviewItem(title: L10n.statsMessagePrivateForwardsTitle, value: .initialize(string: "≈" + stats.forwards.formattedWithSeparator, color: theme.colors.text, font: .medium(.text)))) + } + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("overview"), equatable: InputDataEquatable(overviewItems), comparable: nil, item: { initialSize, stableId in + return ChannelOverviewStatsRowItem(initialSize, stableId: stableId, items: overviewItems, viewType: .singleItem) + })) + index += 1 + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + struct Graph { + let graph: StatsGraph + let title: String + let identifier: InputDataIdentifier + let type: ChartItemType + let load:(InputDataIdentifier)->Void + } + + var graphs: [Graph] = [] + + var chartType: ChartItemType + if stats.interactionsGraphDelta == 3600 { + chartType = .twoAxisHourlyStep + } else if stats.interactionsGraphDelta == 300 { + chartType = .twoAxis5MinStep + } else { + chartType = .twoAxisStep + } + + if !stats.interactionsGraph.isEmpty { + graphs.append(Graph(graph: stats.interactionsGraph, title: L10n.statsMessageInteractionsTitle, identifier: InputDataIdentifier("interactionsGraph"), type: chartType, load: { identifier in + // context.loadDetailedGraph(<#T##graph: StatsGraph##StatsGraph#>, x: <#T##Int64#>) + updateIsLoading(identifier, true) + })) + } + + for graph in graphs { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(graph.title), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + switch graph.graph { + case let .Loaded(_, string): + ChartsDataManager.readChart(data: string.data(using: .utf8)!, sync: true, success: { collection in + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: graph.identifier, equatable: InputDataEquatable(graph.graph), comparable: nil, item: { initialSize, stableId in + return StatisticRowItem(initialSize, stableId: stableId, context: accountContext, collection: collection, viewType: .singleItem, type: graph.type, getDetailsData: { date, completion in + + }) + })) + }, failure: { error in + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: graph.identifier, equatable: InputDataEquatable(graph.graph), comparable: nil, item: { initialSize, stableId in + return StatisticLoadingRowItem(initialSize, stableId: stableId, error: error.localizedDescription) + })) + }) + + updateIsLoading(graph.identifier, false) + + index += 1 + case .OnDemand: + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: graph.identifier, equatable: InputDataEquatable(graph.graph), comparable: nil, item: { initialSize, stableId in + return StatisticLoadingRowItem(initialSize, stableId: stableId, error: nil) + })) + index += 1 + if !uiState.loading.contains(graph.identifier) { + graph.load(graph.identifier) + } + case let .Failed(error): + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: graph.identifier, equatable: InputDataEquatable(graph.graph), comparable: nil, item: { initialSize, stableId in + return StatisticLoadingRowItem(initialSize, stableId: stableId, error: error) + })) + index += 1 + updateIsLoading(graph.identifier, false) + case .Empty: + break + } + } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + if let messages = search?.0, !messages.messages.isEmpty { + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.statsMessagePublicForwardsTitleHeader), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + for (i, message) in messages.messages.enumerated() { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_message(message.id), equatable: InputDataEquatable(message), comparable: nil, item: { initialSize, stableId in + return MessageSharedRowItem(initialSize, stableId: stableId, context: accountContext, message: message, viewType: bestGeneralViewType(messages.messages, for: i), action: { + openMessage(message.id) + }) + })) + index += 1 + } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + } + } else { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("loading"), equatable: nil, comparable: nil, item: { initialSize, stableId in + return StatisticsLoadingRowItem(initialSize, stableId: stableId, context: accountContext, text: L10n.channelStatsLoading) + })) + } + + + return entries +} + + +func MessageStatsController(_ context: AccountContext, messageId: MessageId, datacenterId: Int32) -> ViewController { + + let initialState = UIStatsState(loading: []) + + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((UIStatsState) -> UIStatsState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + + let actionsDisposable = DisposableSet() + let dataPromise = Promise(nil) + let messagesPromise = Promise<(SearchMessagesResult, SearchMessagesState)?>(nil) + + + let statsContext = MessageStatsContext(postbox: context.account.postbox, network: context.account.network, datacenterId: datacenterId, messageId: messageId) + let dataSignal: Signal = statsContext.state + |> map { state in + return state.stats + } + dataPromise.set(.single(nil) |> then(dataSignal)) + + let openMessage: (MessageId)->Void = { messageId in + context.sharedContext.bindings.rootNavigation().push(ChatAdditionController(context: context, chatLocation: .peer(messageId.peerId), messageId: messageId)) + } + + let searchSignal = context.engine.messages.searchMessages(location: .publicForwards(messageId: messageId, datacenterId: Int(datacenterId)), query: "", state: nil) + |> map(Optional.init) + |> afterNext { result in + if let result = result { + for message in result.0.messages { + if let peer = message.peers[message.id.peerId], let peerReference = PeerReference(peer) { + let _ = context.engine.peers.updatedRemotePeer(peer: peerReference).start() + } + } + } + } + messagesPromise.set(.single(nil) |> then(searchSignal)) + + + + let signal = combineLatest(dataPromise.get(), messagesPromise.get(), statePromise.get()) + |> deliverOnMainQueue + |> map { data, search, state -> [InputDataEntry] in + return statsEntries(data, search, state, openMessage: openMessage, updateIsLoading: { identifier, isLoading in + updateState { state in + if isLoading { + return state.withAddedLoading(identifier) + } else { + return state.withRemovedLoading(identifier) + } + } + }, context: statsContext, accountContext: context) + } |> map { + return InputDataSignalValue(entries: $0) + } + |> afterDisposed { + actionsDisposable.dispose() + } + + + let controller = InputDataController(dataSignal: signal, title: L10n.statsMessageTitle, removeAfterDisappear: false, hasDone: false) + + controller.contextOject = statsContext + controller.didLoaded = { controller, _ in + controller.tableView.alwaysOpenRowsOnMouseUp = true + controller.tableView.needUpdateVisibleAfterScroll = true + } + + controller.onDeinit = { + } + + return controller +} diff --git a/Telegram-Mac/MessageTimecode.swift b/Telegram-Mac/MessageTimecode.swift new file mode 100644 index 0000000000..dcf4c48381 --- /dev/null +++ b/Telegram-Mac/MessageTimecode.swift @@ -0,0 +1,355 @@ + +import Foundation +import Cocoa +import TelegramCore + + + + +private let dataDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType([.link]).rawValue) +private let dataAndPhoneNumberDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType([.link, .phoneNumber]).rawValue) +private let phoneNumberDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType([.phoneNumber]).rawValue) +private let validHashtagSet: CharacterSet = { + var set = CharacterSet.alphanumerics + set.insert("_") + return set +}() +private let validIdentifierSet: CharacterSet = { + var set = CharacterSet(charactersIn: "a".unicodeScalars.first! ... "z".unicodeScalars.first!) + set.insert(charactersIn: "A".unicodeScalars.first! ... "Z".unicodeScalars.first!) + set.insert(charactersIn: "0".unicodeScalars.first! ... "9".unicodeScalars.first!) + set.insert("_") + return set +}() +private let identifierDelimiterSet: CharacterSet = { + var set = CharacterSet.punctuationCharacters + set.formUnion(CharacterSet.whitespacesAndNewlines) + return set +}() +private let externalIdentifierDelimiterSet: CharacterSet = { + var set = CharacterSet.punctuationCharacters + set.formUnion(CharacterSet.whitespacesAndNewlines) + set.remove(".") + return set +}() +private let timecodeDelimiterSet: CharacterSet = { + var set = CharacterSet.punctuationCharacters + set.formUnion(CharacterSet.whitespacesAndNewlines) + set.remove(":") + return set +}() +private let validTimecodeSet: CharacterSet = { + var set = CharacterSet(charactersIn: "0".unicodeScalars.first! ... "9".unicodeScalars.first!) + set.insert(":") + return set +}() + +public struct ApplicationSpecificEntityType { + public static let Timecode: Int32 = 1 +} + +private enum CurrentEntityType { + case command + case mention + case hashtag + case phoneNumber + case timecode + + var type: EnabledEntityTypes { + switch self { + case .command: + return .command + case .mention: + return .mention + case .hashtag: + return .hashtag + case .phoneNumber: + return .phoneNumber + case .timecode: + return .timecode + } + } +} + +public struct EnabledEntityTypes: OptionSet { + public var rawValue: Int32 + + public init(rawValue: Int32) { + self.rawValue = rawValue + } + + public static let command = EnabledEntityTypes(rawValue: 1 << 0) + public static let mention = EnabledEntityTypes(rawValue: 1 << 1) + public static let hashtag = EnabledEntityTypes(rawValue: 1 << 2) + public static let url = EnabledEntityTypes(rawValue: 1 << 3) + public static let phoneNumber = EnabledEntityTypes(rawValue: 1 << 4) + public static let timecode = EnabledEntityTypes(rawValue: 1 << 5) + public static let external = EnabledEntityTypes(rawValue: 1 << 6) + + public static let all: EnabledEntityTypes = [.command, .mention, .hashtag, .url, .phoneNumber] +} + +private func commitEntity(_ utf16: String.UTF16View, _ type: CurrentEntityType, _ range: Range, _ enabledTypes: EnabledEntityTypes, _ entities: inout [MessageTextEntity], mediaDuration: Double? = nil) { + if !enabledTypes.contains(type.type) { + return + } + let indexRange: Range = utf16.distance(from: utf16.startIndex, to: range.lowerBound) ..< utf16.distance(from: utf16.startIndex, to: range.upperBound) + var overlaps = false + for entity in entities { + if entity.range.overlaps(indexRange) { + overlaps = true + break + } + } + if !overlaps { + let entityType: MessageTextEntityType + switch type { + case .command: + entityType = .BotCommand + case .mention: + entityType = .Mention + case .hashtag: + entityType = .Hashtag + case .phoneNumber: + entityType = .PhoneNumber + case .timecode: + entityType = .Custom(type: ApplicationSpecificEntityType.Timecode) + } + + if case .timecode = type { + if let mediaDuration = mediaDuration, let timecode = parseTimecodeString(String(utf16[range])), timecode <= mediaDuration { + entities.append(MessageTextEntity(range: indexRange, type: entityType)) + } + } else { + entities.append(MessageTextEntity(range: indexRange, type: entityType)) + } + } +} + + +public func generateTextEntities(_ text: String, enabledTypes: EnabledEntityTypes, currentEntities: [MessageTextEntity] = []) -> [MessageTextEntity] { + var entities: [MessageTextEntity] = currentEntities + + let utf16 = text.utf16 + + var detector: NSDataDetector? + if enabledTypes.contains(.phoneNumber) && enabledTypes.contains(.url) { + detector = dataAndPhoneNumberDetector + } else if enabledTypes.contains(.phoneNumber) { + detector = phoneNumberDetector + } else if enabledTypes.contains(.url) { + detector = dataDetector + } + + let delimiterSet = enabledTypes.contains(.external) ? externalIdentifierDelimiterSet : identifierDelimiterSet + + if let detector = detector { + detector.enumerateMatches(in: text, options: [], range: NSMakeRange(0, utf16.count), using: { result, _, _ in + if let result = result { + if result.resultType == NSTextCheckingResult.CheckingType.link || result.resultType == NSTextCheckingResult.CheckingType.phoneNumber { + let lowerBound = utf16.index(utf16.startIndex, offsetBy: result.range.location).samePosition(in: text) + let upperBound = utf16.index(utf16.startIndex, offsetBy: result.range.location + result.range.length).samePosition(in: text) + if let lowerBound = lowerBound, let upperBound = upperBound { + let type: MessageTextEntityType + if result.resultType == NSTextCheckingResult.CheckingType.link { + type = .Url + } else { + type = .PhoneNumber + } + entities.append(MessageTextEntity(range: utf16.distance(from: text.startIndex, to: lowerBound) ..< utf16.distance(from: text.startIndex, to: upperBound), type: type)) + } + } + } + }) + } + + var index = utf16.startIndex + var currentEntity: (CurrentEntityType, Range)? + + var previousScalar: UnicodeScalar? + while index != utf16.endIndex { + let c = utf16[index] + let scalar = UnicodeScalar(c) + var notFound = true + if let scalar = scalar { + if scalar == "/" { + notFound = false + if previousScalar != nil && !delimiterSet.contains(previousScalar!) { + currentEntity = nil + } else { + if let (type, range) = currentEntity { + commitEntity(utf16, type, range, enabledTypes, &entities) + } + currentEntity = (.command, index ..< index) + } + } else if scalar == "@" { + notFound = false + if let (type, range) = currentEntity { + if case .command = type { + currentEntity = (type, range.lowerBound ..< utf16.index(after: index)) + } else { + commitEntity(utf16, type, range, enabledTypes, &entities) + currentEntity = (.mention, index ..< index) + } + } else { + currentEntity = (.mention, index ..< index) + } + } else if scalar == "#" { + notFound = false + if let (type, range) = currentEntity { + commitEntity(utf16, type, range, enabledTypes, &entities) + } + currentEntity = (.hashtag, index ..< index) + } + + if notFound { + if let (type, range) = currentEntity { + switch type { + case .command, .mention: + if validIdentifierSet.contains(scalar) { + currentEntity = (type, range.lowerBound ..< utf16.index(after: index)) + } else if delimiterSet.contains(scalar) { + if let (type, range) = currentEntity { + commitEntity(utf16, type, range, enabledTypes, &entities) + } + currentEntity = nil + } + case .hashtag: + if validHashtagSet.contains(scalar) { + currentEntity = (type, range.lowerBound ..< utf16.index(after: index)) + } else if delimiterSet.contains(scalar) { + if let (type, range) = currentEntity { + commitEntity(utf16, type, range, enabledTypes, &entities) + } + currentEntity = nil + } + default: + break + } + } + } + } + index = utf16.index(after: index) + previousScalar = scalar + } + if let (type, range) = currentEntity { + commitEntity(utf16, type, range, enabledTypes, &entities) + } + + return entities +} + +public func addLocallyGeneratedEntities(_ text: String, enabledTypes: EnabledEntityTypes, entities: [MessageTextEntity], mediaDuration: Double? = nil) -> [MessageTextEntity]? { + var resultEntities = entities + + var hasDigits = false + var hasColons = false + + let detectPhoneNumbers = enabledTypes.contains(.phoneNumber) + let detectTimecodes = enabledTypes.contains(.timecode) + if detectPhoneNumbers || detectTimecodes { + loop: for c in text.utf16 { + if let scalar = UnicodeScalar(c) { + if scalar >= "0" && scalar <= "9" { + hasDigits = true + if !detectTimecodes || hasColons { + break loop + } + } else if scalar == ":" { + hasColons = true + if hasDigits { + break loop + } + } + } + } + } + + if hasDigits { + if let phoneNumberDetector = phoneNumberDetector, detectPhoneNumbers { + let utf16 = text.utf16 + phoneNumberDetector.enumerateMatches(in: text, options: [], range: NSMakeRange(0, utf16.count), using: { result, _, _ in + if let result = result { + if result.resultType == NSTextCheckingResult.CheckingType.phoneNumber { + let lowerBound = utf16.index(utf16.startIndex, offsetBy: result.range.location).samePosition(in: text) + let upperBound = utf16.index(utf16.startIndex, offsetBy: result.range.location + result.range.length).samePosition(in: text) + if let lowerBound = lowerBound, let upperBound = upperBound { + commitEntity(utf16, .phoneNumber, lowerBound ..< upperBound, enabledTypes, &resultEntities) + } + } + } + }) + } + if hasColons && detectTimecodes { + let utf16 = text.utf16 + let delimiterSet = timecodeDelimiterSet + + var index = utf16.startIndex + var currentEntity: (CurrentEntityType, Range)? + + var previousScalar: UnicodeScalar? + while index != utf16.endIndex { + let c = utf16[index] + let scalar = UnicodeScalar(c) + var notFound = true + if let scalar = scalar { + if validTimecodeSet.contains(scalar) { + notFound = false + if let (type, range) = currentEntity, type == .timecode { + currentEntity = (.timecode, range.lowerBound ..< utf16.index(after: index)) + } else if previousScalar == nil || CharacterSet.whitespacesAndNewlines.contains(previousScalar!) { + currentEntity = (.timecode, index ..< index) + } + } + + if notFound { + if let (type, range) = currentEntity { + switch type { + case .timecode: + if delimiterSet.contains(scalar) { + commitEntity(utf16, type, range, enabledTypes, &resultEntities, mediaDuration: mediaDuration) + currentEntity = nil + } + default: + break + } + } + } + } + index = utf16.index(after: index) + previousScalar = scalar + } + if let (type, range) = currentEntity { + commitEntity(utf16, type, range, enabledTypes, &resultEntities, mediaDuration: mediaDuration) + } + } + } + + if resultEntities.count != entities.count { + return resultEntities + } else { + return nil + } +} + +public func parseTimecodeString(_ string: String?) -> Double? { + if let string = string, string.rangeOfCharacter(from: validTimecodeSet.inverted) == nil { + let components = string.components(separatedBy: ":") + if components.count > 1 && components.count <= 3 { + if components.count == 3 { + if let hours = Int(components[0]), let minutes = Int(components[1]), let seconds = Int(components[2]) { + if hours >= 0 && hours < 48 && minutes >= 0 && minutes < 60 && seconds >= 0 && seconds < 60 { + return Double(seconds) + Double(minutes) * 60.0 + Double(hours) * 60.0 * 60.0 + } + } + } else if components.count == 2 { + if let minutes = Int(components[0]), let seconds = Int(components[1]) { + if minutes >= 0 && minutes < 60 && seconds >= 0 && seconds < 60 { + return Double(seconds) + Double(minutes) * 60.0 + } + } + } + } + } + return nil +} diff --git a/Telegram-Mac/MetalFunctions.metal b/Telegram-Mac/MetalFunctions.metal new file mode 100644 index 0000000000..9451fa1c71 --- /dev/null +++ b/Telegram-Mac/MetalFunctions.metal @@ -0,0 +1,262 @@ +// +// TwoInputFilter.metal +// TgVoipWebrtc +// +// Created by kolechqa on 25.06.2021. +// Copyright © 2021 Mikhail Filimonov. All rights reserved. +// + + +#include +using namespace metal; + +typedef struct { + packed_float2 position; + packed_float2 texcoord; +} Vertex; + +typedef struct { + float4 position[[position]]; + float2 texcoord; +} Varyings; + +vertex Varyings vertexPassthrough(constant Vertex *verticies[[buffer(0)]], + unsigned int vid[[vertex_id]]) { + Varyings out; + constant Vertex &v = verticies[vid]; + out.position = float4(float2(v.position), 0.0, 1.0); + out.texcoord = v.texcoord; + + return out; +} + +fragment float4 fragmentColorConversion( + Varyings in[[stage_in]], + sampler sourceSampler [[sampler(0)]], + texture2d textureY[[texture(0)]], + texture2d textureU[[texture(1)]], + texture2d textureV[[texture(2)]]) { + float y; + float u; + float v; + float r; + float g; + float b; + // Conversion for YUV to rgb from http://www.fourcc.org/fccyvrgb.php + + constexpr sampler quadSampler; + + y = textureY.sample(quadSampler, in.texcoord).r; + u = textureU.sample(quadSampler, in.texcoord).r; + v = textureV.sample(quadSampler, in.texcoord).r; + + y = y - 0.0625; + u = u - 0.5; + v = v - 0.5; + + r = 1.164 * y + 1.596 * v; + g = 1.164 * y - 0.392 * u - 0.813 * v; + b = 1.164 * y + 2.17 * u; + + float4 out = float4(r, g, b, 1.0); + + return float4(out); +} + + +float4 ditherNoise(texture2d texture, sampler sampler, float2 uv, float4 color) { + + float textureSize = 256; + float noiseGrain = 0.002; + + float width = texture.get_width(); + float height = texture.get_width(); + + float xPixel = (1 / width); + float yPixel = (1 / height); + + + float2 noiseTextureCoord = float2(uv.x + textureSize * xPixel, uv.y + textureSize * yPixel); + float noiseClamped = texture.sample(sampler, noiseTextureCoord).r; + float noiseIntensity = (noiseClamped * 4.) - 2.; + float3 lumcoeff = float3(0.299, 0.587, 0.114); + float luminance = dot(color.rgb, lumcoeff); + float lum = smoothstep(0.2, 0.0, luminance) + luminance; + float3 noiseColor = mix(float3(noiseIntensity), float3(0.0), pow(lum, 4.0)); + + color.rgb = color.rgb + noiseColor * noiseGrain; + return color; +} + +/* + + [[nodiscard]] ShaderPart FragmentDitherNoise() { + const auto size = QString::number(kNoiseTextureSize); + return { + .header = R"( + uniform sampler2D n_texture; + )", + .body = R"( + vec2 noiseTextureCoord = gl_FragCoord.xy / )" + size + R"(.; + float noiseClamped = texture2D(n_texture, noiseTextureCoord).r; + float noiseIntensity = (noiseClamped * 4.) - 2.; + vec3 lumcoeff = vec3(0.299, 0.587, 0.114); + float luminance = dot(result.rgb, lumcoeff); + float lum = smoothstep(0.2, 0.0, luminance) + luminance; + vec3 noiseColor = mix(vec3(noiseIntensity), vec3(0.0), pow(lum, 4.0)); + result.rgb = result.rgb + noiseColor * noiseGrain; + )", + }; + } + */ + +float2 doScale(float2 uv, float2 scale) { + uv -= float2(0.5); // relative center logic + uv = float2x2(float2(scale.x, 0.0), float2(0.0, scale.y)) * uv; // scale + uv += float2(0.5); // relative center logic + + return uv; +} +float4 applyBoxBlur(texture2d texture, sampler sampler, float2 uv, bool vertical) { + + int radius = 30; + int diameter = 2 * radius + 1; + + const float3 satLuminanceWeighting = float3(0.2126, 0.7152, 0.0722); + + float width = texture.get_width(); + float height = texture.get_width(); + + float xPixel = (1 / width); + float yPixel = (1 / height); + + float2 offsets = vertical ? float2(0, 1) : float2(1, 0); + + float4 accumulated = float4(0.); + for (int i = 0; i != diameter; i++) { + float stepOffset = float(i - radius); + float fradius = float(radius); + float2 offset = float2(stepOffset) * offsets; + float2 px = float2(uv.x + offset.x*xPixel, uv.y + offset.y*yPixel); + float4 sampled = float4(texture.sample(sampler, px)); + float boxWeight = fradius + 1.0 - abs(float(i) - fradius); + accumulated += sampled * boxWeight; + } + + + float3 blurred = accumulated.rgb / accumulated.a; + float satLuminance = dot(blurred, satLuminanceWeighting); + float3 mixinColor = float3(satLuminance); + + return float4(clamp(mix(mixinColor, blurred, 1.1), 0.0, 1.0) * 0.65, 1.0); +} + + +typedef struct { + float4 position [[position]]; + float2 textureCoordinate [[user(texturecoord)]]; + float2 textureCoordinate2 [[user(texturecoord2)]]; +} TwoInputVertexIO; + +vertex TwoInputVertexIO twoInputVertex(const device packed_float2 *position [[buffer(0)]], + const device packed_float2 *texturecoord [[buffer(1)]], + const device packed_float2 *texturecoord2 [[buffer(2)]], + unsigned int vid [[vertex_id]]) +{ + TwoInputVertexIO outputVertices; + + outputVertices.position = float4(position[vid], 0.0, 1.0); + outputVertices.textureCoordinate = texturecoord[vid]; + outputVertices.textureCoordinate2 = texturecoord2[vid]; + + return outputVertices; +} + + +fragment float4 scaleAndBlur(Varyings in[[stage_in]], + sampler sampler [[sampler(0)]], + constant float2 &scale [[buffer(0)]], + constant bool &vertical [[buffer(1)]], + texture2d inputTexture[[texture(0)]]) { + + float2 uv = doScale(in.texcoord, scale); + + return float4(applyBoxBlur(inputTexture, sampler, uv, vertical)); +} + + +fragment float4 transformAndBlend(TwoInputVertexIO fragmentInput [[stage_in]], + sampler sourceSampler1 [[sampler(0)]], + sampler sourceSampler2 [[sampler(1)]], + texture2d foregroundTexture [[texture(0)]], + texture2d backgroundTexture [[texture(1)]], + constant float2 &scale1 [[buffer(0)]], + constant float2 &scale2 [[buffer(1)]]) +{ + + float2 uv1 = doScale(fragmentInput.textureCoordinate, scale1); + float2 uv2 = doScale(fragmentInput.textureCoordinate2, scale2); + + float4 out1 = foregroundTexture.sample(sourceSampler1, uv1); + + float4 out2 = backgroundTexture.sample(sourceSampler2, uv2); + + + if (out1.a == 0) { + out2 = float4(applyBoxBlur(backgroundTexture, sourceSampler2, uv2, false)); +// out2 = ditherNoise(backgroundTexture, sourceSampler2, uv2, out2); + } + + float4 outputColor; + + float a = out1.a + out2.a * (1.0h - out1.a); + float alphaDivisor = a; // Protect against a divide-by-zero blacking out things in the output + + outputColor.r = (out1.r * out1.a + out2.r * out2.a * (1.0h - out1.a))/alphaDivisor; + outputColor.g = (out1.g * out1.a + out2.g * out2.a * (1.0h - out1.a))/alphaDivisor; + outputColor.b = (out1.b * out1.a + out2.b * out2.a * (1.0h - out1.a))/alphaDivisor; + outputColor.a = a; + + return outputColor; +} + +fragment float4 fragmentPlain(Varyings in[[stage_in]], + sampler sampler [[sampler(0)]], + texture2d inputTexture[[texture(0)]]) { + return inputTexture.sample(sampler, in.texcoord); +} + + + + +struct VertexIn { + packed_float3 position; + packed_float2 texCoord; +}; + +struct VertexOut { + float4 position [[position]]; + float2 texCoord; +}; + +vertex VertexOut basic_vertex( + const device VertexIn* vertex_array [[ buffer(0) ]], + unsigned int vid [[ vertex_id ]]) { + VertexIn in = vertex_array[vid]; + + VertexOut out; + out.position = float4(in.position, 1.0); + out.texCoord = in.texCoord; + return out; +} + +fragment float4 basic_fragment( + VertexOut interpolated [[stage_in]], + texture2d tex2D [[ texture(0) ]], + sampler sampler2D [[ sampler(0) ]]) { + + float2 p = interpolated.texCoord; + + float4 color = tex2D.sample(sampler2D, p); + return float4(color.r, color.g, color.b, color.a); +} diff --git a/Telegram-Mac/MicroListenerController.swift b/Telegram-Mac/MicroListenerController.swift new file mode 100644 index 0000000000..a5b2731cca --- /dev/null +++ b/Telegram-Mac/MicroListenerController.swift @@ -0,0 +1,364 @@ +// +// MicroListenerController.swift +// Telegram +// +// Created by Mikhail Filimonov on 25.05.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import SwiftSignalKit +import TelegramCore + +import Postbox + +private let kOutputBus: UInt32 = 0 +private let kInputBus: UInt32 = 1 + +private let queue = Queue(name: "micro-listen", qos: .background) + +private final class MicroListenerContextObject : RecoderContextRenderer { + private let devicesDisposable = MetaDisposable() + private let devices: DevicesContext + private let accountManager: AccountManager + + + private var device: AVCaptureDevice? + private var sampleRate: Int32 = 0 + + + private var onSpeaking:((Float)->Void)? + private var always: Bool = false + + private var paused: Bool = true + + private var stack:[Float] = [] { + didSet { + if stack.count >= 70, onSpeaking != nil { + onSpeaking?(stack.last!) + onSpeaking = nil + pause() + } + } + } + private let id: Int32 + private let audioUnit = Atomic(value: nil) + + private var audioBuffer = Data() + + private var micLevelPeak: Int16 = 0 + private var micLevelPeakCount: Int = 0 + + private let queue: Queue + + init(queue: Queue, devices:DevicesContext, accountManager: AccountManager) { + self.devices = devices + self.queue = queue + self.accountManager = accountManager + self.id = getNextRecorderContextId() + + addAudioRecorderContext(self.id, self) + addAudioUnitHolder(self.id, queue, self.audioUnit) + } + + deinit { + removeAudioRecorderContext(self.id) + removeAudioUnitHolder(self.id) + stop() + } + + func pause() { + if !paused { + paused = true + devicesDisposable.set(nil) + self.stop() + } + } + func resume(onSpeaking: @escaping(Float)->Void, always: Bool) { + if paused { + paused = false + self.always = always + self.onSpeaking = onSpeaking + let signal = combineLatest(devices.signal, voiceCallSettings(accountManager), requestMicrophonePermission()) |> deliverOn(queue) + + devicesDisposable.set(signal.start(next: { [weak self] devices, settings, permission in + let device = settings.audioInputDeviceId == nil ? devices.audioInput.first : devices.audioInput.first(where: { $0.uniqueID == settings.audioInputDeviceId }) + + if let device = device, permission { + self?.start(device) + } else { + self?.stop() + } + })) + } + } + + + private func start(_ device: AVCaptureDevice) { + if self.device != device { + self.device = device + + + if let audioUnit = self.audioUnit.swap(nil) { + var status = noErr + status = AudioOutputUnitStop(audioUnit) + status = AudioUnitUninitialize(audioUnit) + status = AudioComponentInstanceDispose(audioUnit) + } + + var desc = AudioComponentDescription() + desc.componentType = kAudioUnitType_Output + desc.componentSubType = kAudioUnitSubType_HALOutput + desc.componentManufacturer = kAudioUnitManufacturer_Apple + guard let inputComponent = AudioComponentFindNext(nil, &desc) else { + return + } + var maybeAudioUnit: AudioUnit? = nil + AudioComponentInstanceNew(inputComponent, &maybeAudioUnit) + + guard let audioUnit = maybeAudioUnit else { + return + } + + var o: UInt32 = 1 + guard AudioUnitSetProperty(audioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, 1, &o, 4) == noErr else { + AudioComponentInstanceDispose(audioUnit) + return + } + + var z: UInt32 = 0 + guard AudioUnitSetProperty(audioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, 0, &z, 4) == noErr else { + AudioComponentInstanceDispose(audioUnit) + return + } + + var deviceId:AudioDeviceID = AudioDeviceID() + var deviceIdRequest:AudioObjectPropertyAddress = AudioObjectPropertyAddress(mSelector: kAudioHardwarePropertyDefaultInputDevice, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMaster) + var deviceIdSize:UInt32 = UInt32(MemoryLayout.size) + + guard AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject), &deviceIdRequest, 0, nil, &deviceIdSize, &deviceId) == noErr else { + AudioComponentInstanceDispose(audioUnit) + return + } + + guard AudioUnitSetProperty(audioUnit, kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, kOutputBus, &deviceId, UInt32(MemoryLayout.size)) == noErr else { + return + } + // + var deviceDataRequest:AudioObjectPropertyAddress = AudioObjectPropertyAddress(mSelector: kAudioDevicePropertyAvailableNominalSampleRates, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMaster) + var deviceDataSize:UInt32 = 0 + guard AudioObjectGetPropertyDataSize(deviceId, &deviceDataRequest, 0, nil, &deviceDataSize) == noErr else { + AudioComponentInstanceDispose(audioUnit) + return + } + let audioValueCount = deviceDataSize / UInt32(MemoryLayout.size) + var table:[AudioValueRange] = Array(repeating: AudioValueRange(), count: Int(audioValueCount)) + + guard AudioObjectGetPropertyData(deviceId, &deviceDataRequest, 0, nil, &deviceDataSize, &table) == noErr else { + AudioComponentInstanceDispose(audioUnit) + return + } + + + var inputSampleRate:AudioValueRange = table[0] + for i in 0 ..< Int(audioValueCount) { + if table[i].mMinimum == 48000 { + inputSampleRate = table[i] + break + } + } + deviceDataRequest.mSelector = kAudioDevicePropertyNominalSampleRate + guard AudioObjectSetPropertyData(deviceId, &deviceDataRequest, 0, nil, UInt32(MemoryLayout.size), &inputSampleRate) == noErr else { + return + } + + var audioStreamDescription = audioRecorderNativeStreamDescription(inputSampleRate.mMinimum) + sampleRate = Int32(inputSampleRate.mMinimum) + guard AudioUnitSetProperty(audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &audioStreamDescription, UInt32(MemoryLayout.size)) == noErr else { + AudioComponentInstanceDispose(audioUnit) + return + } + + guard AudioUnitSetProperty(audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &audioStreamDescription, UInt32(MemoryLayout.size)) == noErr else { + AudioComponentInstanceDispose(audioUnit) + return + } + + var callbackStruct = AURenderCallbackStruct() + callbackStruct.inputProc = rendererInputProc + callbackStruct.inputProcRefCon = UnsafeMutableRawPointer(bitPattern: intptr_t(self.id)) + guard AudioUnitSetProperty(audioUnit, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, 0, &callbackStruct, UInt32(MemoryLayout.size)) == noErr else { + AudioComponentInstanceDispose(audioUnit) + return + } + + var zero: UInt32 = 0 + guard AudioUnitSetProperty(audioUnit, kAudioUnitProperty_ShouldAllocateBuffer, kAudioUnitScope_Output, 0, &zero, 4) == noErr else { + AudioComponentInstanceDispose(audioUnit) + return + } + + guard AudioUnitInitialize(audioUnit) == noErr else { + AudioComponentInstanceDispose(audioUnit) + return + } + + _ = self.audioUnit.swap(audioUnit) + + + self.audioUnit.with { audioUnit -> Void in + if let audioUnit = audioUnit { + guard AudioOutputUnitStart(audioUnit) == noErr else { + self.stop() + return + } + } + } + } + } + + private func stop() { + device = nil + assert(queue.isCurrent()) + + self.paused = true + + if let audioUnit = self.audioUnit.swap(nil) { + var status = noErr + status = AudioOutputUnitStop(audioUnit) + status = AudioUnitUninitialize(audioUnit) + status = AudioComponentInstanceDispose(audioUnit) + } + + } + + func processAndDisposeAudioBuffer(_ buffer: AudioBuffer) { + assert(queue.isCurrent()) + + var buffer = buffer + + if(sampleRate==16000){ + let initialBuffer=malloc(Int(buffer.mDataByteSize+2)); + memcpy(initialBuffer, buffer.mData, Int(buffer.mDataByteSize)); + buffer.mData=realloc(buffer.mData, Int(buffer.mDataByteSize*3)) + let values = initialBuffer!.assumingMemoryBound(to: Int16.self) + let resampled = buffer.mData!.assumingMemoryBound(to: Int16.self) + values[Int(buffer.mDataByteSize/2)]=values[Int(buffer.mDataByteSize/2)-1] + for i: Int in 0 ..< Int(buffer.mDataByteSize/2) { + resampled[i*3]=values[i] + resampled[i*3+1]=values[i]/3+values[i+1]/3*2 + resampled[i*3+2]=values[i]/3*2+values[i+1]/3 + } + free(initialBuffer) + buffer.mDataByteSize*=3 + } + + defer { + free(buffer.mData) + } + + let millisecondsPerPacket = 60 + let encoderPacketSizeInBytes = 16000 / 1000 * millisecondsPerPacket * 2 + + let currentEncoderPacket = malloc(encoderPacketSizeInBytes)! + defer { + free(currentEncoderPacket) + } + + var bufferOffset = 0 + + while true { + var currentEncoderPacketSize = 0 + + while currentEncoderPacketSize < encoderPacketSizeInBytes { + if audioBuffer.count != 0 { + let takenBytes = min(self.audioBuffer.count, encoderPacketSizeInBytes - currentEncoderPacketSize) + if takenBytes != 0 { + self.audioBuffer.withUnsafeBytes { (bytes: UnsafePointer) -> Void in + memcpy(currentEncoderPacket.advanced(by: currentEncoderPacketSize), bytes, takenBytes) + } + self.audioBuffer.replaceSubrange(0 ..< takenBytes, with: Data()) + currentEncoderPacketSize += takenBytes + } + } else if bufferOffset < Int(buffer.mDataByteSize) { + let takenBytes = min(Int(buffer.mDataByteSize) - bufferOffset, encoderPacketSizeInBytes - currentEncoderPacketSize) + if takenBytes != 0 { + self.audioBuffer.withUnsafeBytes { (bytes: UnsafePointer) -> Void in + memcpy(currentEncoderPacket.advanced(by: currentEncoderPacketSize), buffer.mData?.advanced(by: bufferOffset), takenBytes) + } + bufferOffset += takenBytes + currentEncoderPacketSize += takenBytes + } + } else { + break + } + } + + if currentEncoderPacketSize < encoderPacketSizeInBytes { + self.audioBuffer.append(currentEncoderPacket.assumingMemoryBound(to: UInt8.self), count: currentEncoderPacketSize) + break + } else { + self.processWaveformPreview(samples: currentEncoderPacket.assumingMemoryBound(to: Int16.self), count: currentEncoderPacketSize / 2) + } + } + } + + private func processWaveformPreview(samples: UnsafePointer, count: Int) { + for i in 0 ..< count { + var sample = samples.advanced(by: i).pointee + if sample < 0 { + if sample == Int16.min { + sample = Int16.max + } else { + sample = -sample + } + } + + if self.micLevelPeak < sample { + self.micLevelPeak = sample + } + self.micLevelPeakCount += 1 + + if self.micLevelPeakCount >= 1200 { + let level = Float(self.micLevelPeak) / 4000.0 + if always { + self.onSpeaking?(level) + } else { + if level >= 0.4 { + self.stack.append(level) + } + } + + self.micLevelPeak = 0 + self.micLevelPeakCount = 0 + } + } + } + +} + + +final class MicroListenerContext { + private let contextRef: QueueLocalObject + init(devices:DevicesContext, accountManager: AccountManager) { + contextRef = .init(queue: queue, generate: { + return MicroListenerContextObject(queue: queue, devices: devices, accountManager: accountManager) + }) + } + + func pause() { + contextRef.syncWith { + $0.pause() + } + } + func resume(onSpeaking: @escaping(Float)->Void, always: Bool = false) { + contextRef.syncWith { + $0.resume(onSpeaking: { value in + DispatchQueue.main.async { + onSpeaking(value) + } + }, always: always) + } + } + +} diff --git a/Telegram-Mac/MicrophonePreviewRowItem.swift b/Telegram-Mac/MicrophonePreviewRowItem.swift new file mode 100644 index 0000000000..365df83db5 --- /dev/null +++ b/Telegram-Mac/MicrophonePreviewRowItem.swift @@ -0,0 +1,145 @@ +// +// MicrophonePreviewRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 06/10/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa + +import Cocoa +import TGUIKit +import SwiftSignalKit +import TelegramCore + +private func generateValueImage(_ color: NSColor, height: CGFloat) -> CGImage { + return generateImage(NSMakeSize(4, height), rotatedContext: { size, ctx in + ctx.clear(CGRect(origin: .zero, size: size)) + ctx.round(size, 2) + ctx.setFillColor(color.cgColor) + ctx.fill(CGRect(origin: .zero, size: size)) + })! +} + +class MicrophonePreviewRowItem: GeneralRowItem { + fileprivate let controller: MicroListenerContext + fileprivate var powerLevel: Int = 0 { + didSet { + if powerLevel != oldValue { + self.redraw(animated: true, presentAsNew: false) + } + } + } + init(_ initialSize: NSSize, stableId: AnyHashable, context: SharedAccountContext, viewType: GeneralViewType, customTheme: GeneralRowItem.Theme? = nil) { + controller = MicroListenerContext(devices: context.devicesContext, accountManager: context.accountManager) + + super.init(initialSize, height: 40, stableId: stableId, viewType: viewType, customTheme: customTheme) + + controller.resume (onSpeaking: { [weak self] value in + self?.powerLevel = max(min(Int(36 * value), 36), 0) + }, always: true) + } + + + override func viewClass() -> AnyClass { + return MicrophonePreviewRowView.self + } +} + +private final class PreviewView : View { + + fileprivate var customTheme: GeneralRowItem.Theme? + + override func draw(_ layer: CALayer, in ctx: CGContext) { + super.draw(layer, in: ctx) + + let onsize = NSMakeSize(4, frame.height - 8) + + let count = Int(ceil(frame.width / (onsize.width * 2))) + var pos: NSPoint = NSMakePoint(0, 4) + + + + let active: CGImage + let passive: CGImage + if let theme = self.customTheme { + active = generateValueImage(theme.accentColor, height: onsize.height) + passive = generateValueImage(theme.secondaryColor, height: onsize.height) + } else { + active = generateValueImage(theme.colors.accentIcon, height: onsize.height) + passive = generateValueImage(theme.colors.grayIcon, height: onsize.height) + } + + let percent = Float(powerLevel) / Float(36) + let value = Int(floor(percent * Float(count))) + for i in 0 ..< count { + if value > i { + ctx.draw(active, in: CGRect(origin: pos, size: onsize)) + } else { + ctx.draw(passive, in: CGRect(origin: pos, size: onsize)) + } + pos.x += onsize.width * 2 + } + } + + + var powerLevel: Int = 0 { + didSet { + needsDisplay = true + } + } +} + +private final class MicrophonePreviewRowView : GeneralContainableRowView { + private let view = PreviewView(frame: .zero) + private let title: TextView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(view) + addSubview(title) + title.userInteractionEnabled = false + title.isSelectable = false + } + override var backdorColor: NSColor { + guard let item = item as? MicrophonePreviewRowItem else { + return super.backdorColor + } + if let theme = item.customTheme { + return theme.backgroundColor + } + return super.backdorColor + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? MicrophonePreviewRowItem else { + return + } + view.customTheme = item.customTheme + view.powerLevel = item.powerLevel + needsLayout = true + + let layout = TextViewLayout(.initialize(string: L10n.callSettingsInputLevel, color: item.customTheme?.textColor ?? theme.colors.text, font: .normal(.title))) + layout.measure(width: 200) + title.update(layout) + } + + override func layout() { + super.layout() + + guard let item = item as? MicrophonePreviewRowItem else { + return + } + view.setFrameSize(NSMakeSize(160, 20)) + view.centerY(x: containerView.frame.width - view.frame.width - item.viewType.innerInset.right) + + title.centerY(x: item.viewType.innerInset.left) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + diff --git a/Telegram-Mac/MimeTypes.swift b/Telegram-Mac/MimeTypes.swift index 81cd4aa0b1..df8f2dd840 100644 --- a/Telegram-Mac/MimeTypes.swift +++ b/Telegram-Mac/MimeTypes.swift @@ -7,40 +7,40 @@ // import Cocoa -import SwiftSignalKitMac - +import SwiftSignalKit +import TGUIKit fileprivate var mimestore:[String:String] = [:] fileprivate var extensionstore:[String:String] = [:] -private func initializeMimeStore() { - do { - if mimestore.isEmpty && extensionstore.isEmpty { - let path = Bundle.main.path(forResource: "mime-types", ofType: "txt") - let content = try? String(contentsOfFile: path ?? "") - let mimes = content?.components(separatedBy: CharacterSet.newlines) - - if let mimes = mimes { - for mime in mimes { - let single = mime.components(separatedBy: ":") - if single.count == 2 { - extensionstore[single[0]] = single[1] - mimestore[single[1]] = single[0] - } +func initializeMimeStore() { + assertOnMainThread() + if mimestore.isEmpty && extensionstore.isEmpty { + let path = Bundle.main.path(forResource: "mime-types", ofType: "txt") + let content = try? String(contentsOfFile: path ?? "") + let mimes = content?.components(separatedBy: CharacterSet.newlines) + + if let mimes = mimes { + for mime in mimes { + let single = mime.components(separatedBy: ":") + if single.count == 2 { + extensionstore[single[0]] = single[1] + mimestore[single[1]] = single[0] } } } } } -func resourceType(mimeType:String? = nil, orExt:String? = nil) -> Signal { +func resourceType(mimeType:String? = nil, orExt:String? = nil) -> Signal { - initializeMimeStore() assert(mimeType != nil || orExt != nil) assert((mimeType != nil && orExt == nil) || (mimeType == nil && orExt != nil)) - return Signal { (subscriber) -> Disposable in + return Signal { (subscriber) -> Disposable in + + initializeMimeStore() var result:String? @@ -55,33 +55,41 @@ func resourceType(mimeType:String? = nil, orExt:String? = nil) -> Signal runOn(resourcesQueue) + } |> runOn(Queue.mainQueue()) } -func MIMEType(_ fileExtension: String) -> String { - - initializeMimeStore() - - if let ext = extensionstore[fileExtension] { - return ext - } else { - if !fileExtension.isEmpty { - let UTIRef = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, fileExtension as CFString, nil) - let UTI = UTIRef?.takeRetainedValue() - if let UTI = UTI { - let MIMETypeRef = UTTypeCopyPreferredTagWithClass(UTI, kUTTagClassMIMEType) - if MIMETypeRef != nil - { - let MIMEType = MIMETypeRef?.takeRetainedValue() - return MIMEType as String? ?? "application/octet-stream" +func MIMEType(_ path: String, isExt: Bool = false) -> String { + let fileExtension = isExt ? path.lowercased() : path.nsstring.pathExtension.lowercased() + if !fileExtension.isEmpty { + if let ext = extensionstore[fileExtension] { + return ext + } else { + if !fileExtension.isEmpty { + let UTIRef = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, fileExtension as CFString, nil) + let UTI = UTIRef?.takeRetainedValue() + if let UTI = UTI { + let MIMETypeRef = UTTypeCopyPreferredTagWithClass(UTI, kUTTagClassMIMEType) + if MIMETypeRef != nil + { + let MIMEType = MIMETypeRef?.takeRetainedValue() + return MIMEType as String? ?? "application/octet-stream" + } } + } - + return "application/octet-stream" } - return "application/octet-stream" + } else { + return !isExt && path.isDirectory ? "application/zip" : "application/octet-stream" } - +} + +func fileExt(_ mimeType: String) -> String? { + if let ext = mimestore[mimeType] { + return ext + } + return nil } let voiceMime = "audio/ogg" diff --git a/Telegram-Mac/ModalOptionSetController.swift b/Telegram-Mac/ModalOptionSetController.swift new file mode 100644 index 0000000000..5c17db5a90 --- /dev/null +++ b/Telegram-Mac/ModalOptionSetController.swift @@ -0,0 +1,174 @@ +// +// ModalOptionSetController.swift +// Telegram +// +// Created by Mikhail Filimonov on 10/06/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import SwiftSignalKit +import TGUIKit + +private final class ModalOptionsArguments { + let context: AccountContext + let toggleOption: (Int)->Void + init(context: AccountContext, toggleOption:@escaping(Int)->Void) { + self.context = context + self.toggleOption = toggleOption + } +} + +struct ModalOptionSet : Equatable { + let title: String + let selected: Bool + let editable: Bool + init(title: String, selected: Bool, editable: Bool) { + self.title = title + self.selected = selected + self.editable = editable + } + func withUpdatedSelected(_ selected: Bool) -> ModalOptionSet { + return ModalOptionSet(title: self.title, selected: selected, editable: self.editable) + } +} +enum ModalOptionSetResult { + case selected + case none +} + +private struct ModalOptionsState: Equatable { + let options: [ModalOptionSet] + let selectOne: Bool + init(options:[ModalOptionSet], selectOne: Bool) { + self.options = options + self.selectOne = selectOne + } + + func withToggledOptionAt(_ index: Int) -> ModalOptionsState { + var options = self.options + options[index] = options[index].withUpdatedSelected(!options[index].selected) + if selectOne { + for i in 0 ..< options.count { + options[i] = options[i].withUpdatedSelected(false) + } + options[index] = options[index].withUpdatedSelected(true) + } + + return ModalOptionsState(options: options, selectOne: self.selectOne) + } +} + +private let _id_title: InputDataIdentifier = InputDataIdentifier("_id_title") +private let _id_border: InputDataIdentifier = InputDataIdentifier("_id_border") +private func _id_option(_ index: Int)->InputDataIdentifier { + return InputDataIdentifier("_id_option_\(index)") +} +private func modalOptionsSetEntries(state: ModalOptionsState, desc: String?, arguments: ModalOptionsArguments) -> [InputDataEntry] { + var entries: [InputDataEntry] = [] + var sectionId: Int32 = 0 + var index: Int32 = 0 + + if let desc = desc { + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_title, equatable: InputDataEquatable(desc), comparable: nil, item: { initialSize, stableId in + return GeneralTextRowItem(initialSize, stableId: stableId, text: .plain(desc), textColor: theme.colors.grayText, alignment: .center, drawCustomSeparator: false, inset: NSEdgeInsets(left: 30.0, right: 30.0, top: 10, bottom: 10)) + })) + index += 1 + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_border, equatable: nil, comparable: nil, item: { initialSize, stableId in + return GeneralLineSeparatorRowItem.init(initialSize: initialSize, stableId: stableId) + })) + index += 1 + } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + + for (i, option) in state.options.enumerated() { + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_option(i), equatable: InputDataEquatable(option), comparable: nil, item: { initialSize, stableId in + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: option.title, type: .selectable(option.selected), viewType: bestGeneralViewType(state.options, for: i), action: { + arguments.toggleOption(i) + }, enabled: option.editable, disabledAction: { + + }) + })) + index += 1 + } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func ModalOptionSetController(context: AccountContext, options: [ModalOptionSet], selectOne: Bool = false, actionText: (String, NSColor), desc: String? = nil, title: String, result: @escaping ([ModalOptionSetResult])->Void) -> InputDataModalController { + + let initialState: ModalOptionsState = ModalOptionsState(options: options, selectOne: selectOne) + let stateValue: Atomic = Atomic(value: initialState) + let statePromise: ValuePromise = ValuePromise(initialState, ignoreRepeated: true) + + let updateState: (_ f:(ModalOptionsState)->ModalOptionsState)->Void = { f in + statePromise.set(stateValue.modify(f)) + } + + let arguments = ModalOptionsArguments(context: context, toggleOption: { index in + updateState { + $0.withToggledOptionAt(index) + } + }) + + let actionsDisposable = DisposableSet() + + let dataSignal = statePromise.get() |> mapToSignal { state in + return .single(modalOptionsSetEntries(state: state, desc: desc, arguments: arguments)) + } |> map { entries in + return InputDataSignalValue(entries: entries) + } + + + var dismiss:(()->Void)? + + + let controller = InputDataController(dataSignal: dataSignal, title: title, validateData: { data in + + result(stateValue.with { state in + return state.options.map { option in + if option.selected { + return .selected + } else { + return .none + } + } + }) + + dismiss?() + + return .fail(.none) + }, afterDisappear: { + actionsDisposable.dispose() + }) + + let modalInteractions: ModalInteractions = ModalInteractions(acceptTitle: actionText.0, accept: { [weak controller] in + controller?.validateInputValues() + }, drawBorder: true, height: 50, singleButton: true) + + + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions, size: NSMakeSize(300, 300)) + + dismiss = { [weak modalController] in + modalController?.close() + } + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: dismiss) + + Queue.mainQueue().justDispatch { + modalInteractions.updateDone { title in + title.set(color: actionText.1, for: .Normal) + } + } + + + return modalController +} diff --git a/Telegram-Mac/ModalPreviewViews.swift b/Telegram-Mac/ModalPreviewViews.swift new file mode 100644 index 0000000000..8b2be8a1bd --- /dev/null +++ b/Telegram-Mac/ModalPreviewViews.swift @@ -0,0 +1,349 @@ +// +// StickerPreviewModalController.swift +// Telegram +// +// Created by keepcoder on 02/02/2017. +// Copyright © 2017 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + +class StickerPreviewModalView : View, ModalPreviewControllerView { + fileprivate let imageView:TransformImageView = TransformImageView() + fileprivate let textView:TextView = TextView() + private let fetchDisposable = MetaDisposable() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(imageView) + addSubview(textView) + textView.backgroundColor = .clear + imageView.setFrameSize(100,100) + self.background = .clear + } + + deinit { + fetchDisposable.dispose() + } + + override func layout() { + super.layout() + imageView.center() + } + + func update(with reference: QuickPreviewMedia, context: AccountContext, animated: Bool) -> Void { + if let reference = reference.fileReference { + + let size = reference.media.dimensions?.size.aspectFitted(NSMakeSize(min(300, frame.size.width), min(300, frame.size.height))) ?? frame.size + imageView.set(arguments: TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: NSEdgeInsets())) + imageView.frame = NSMakeRect(0, frame.height - size.height, size.width, size.height) + if animated { + imageView.layer?.animateScaleSpring(from: 0.5, to: 1.0, duration: 0.2) + } + + imageView.setSignal(chatMessageSticker(postbox: context.account.postbox, file: reference, small: false, scale: backingScaleFactor, fetched: true), clearInstantly: true, animate:true) + + let layout = TextViewLayout(.initialize(string: reference.media.stickerText?.fixed, color: nil, font: .normal(30.0))) + layout.measure(width: .greatestFiniteMagnitude) + textView.update(layout) + textView.centerX() + if animated { + textView.layer?.animateScaleSpring(from: 0.5, to: 1.0, duration: 0.2) + } + + needsLayout = true + } + } + + func getContentView() -> NSView { + return imageView + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + + +class GifPreviewModalView : View, ModalPreviewControllerView { + fileprivate var player:GIFContainerView = GIFContainerView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(player) + player.setFrameSize(100,100) + self.background = .clear + } + + override func layout() { + super.layout() + player.center() + + } + + func getContentView() -> NSView { + return player + } + + func update(with reference: QuickPreviewMedia, context: AccountContext, animated: Bool) -> Void { + if let reference = reference.fileReference { + if animated { + let current = self.player + current.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak current] completed in + if completed { + current?.removeFromSuperview() + } + }) + } else { + self.player.removeFromSuperview() + } + + self.player = GIFContainerView() + self.player.layer?.borderWidth = 0 + self.player.layer?.cornerRadius = .cornerRadius + addSubview(self.player) + let size = reference.media.dimensions?.size.aspectFitted(NSMakeSize(frame.size.width, frame.size.height - 40)) ?? frame.size + + + let iconSignal: Signal + iconSignal = chatMessageVideo(postbox: context.account.postbox, fileReference: reference, scale: backingScaleFactor) + + player.update(with: reference, size: size, viewSize: size, context: context, table: nil, iconSignal: iconSignal) + player.frame = NSMakeRect(0, frame.height - size.height, size.width, size.height) + if animated { + player.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + needsLayout = true + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class ImagePreviewModalView : View, ModalPreviewControllerView { + fileprivate var imageView:TransformImageView = TransformImageView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(imageView) + self.background = .clear + } + + override func layout() { + super.layout() + imageView.center() + } + + func getContentView() -> NSView { + return imageView + } + + func update(with reference: QuickPreviewMedia, context: AccountContext, animated: Bool) -> Void { + if let reference = reference.imageReference { + let current = self.imageView + if animated { + current.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak current] completed in + if completed { + current?.removeFromSuperview() + } + }) + } else { + current.removeFromSuperview() + } + + self.imageView = TransformImageView() + self.imageView.layer?.borderWidth = 0 + addSubview(self.imageView) + + let size = frame.size + + let dimensions = largestImageRepresentation(reference.media.representations)?.dimensions.size ?? size + + let arguments = TransformImageArguments(corners: ImageCorners(radius: .cornerRadius), imageSize: dimensions.fitted(size), boundingSize: dimensions.fitted(size), intrinsicInsets: NSEdgeInsets(), resizeMode: .none) + + self.imageView.setSignal(signal: cachedMedia(media: reference.media, arguments: arguments, scale: backingScaleFactor, positionFlags: nil), clearInstantly: false) + + let updateImageSignal = chatMessagePhoto(account: context.account, imageReference: reference, scale: backingScaleFactor, synchronousLoad: true) + self.imageView.setSignal(updateImageSignal, animate: false) + self.imageView.set(arguments: arguments) + + imageView.setFrameSize(arguments.imageSize) + if animated { + imageView.layer?.animateScaleSpring(from: 0.5, to: 1.0, duration: 0.2) + } + needsLayout = true + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + +class VideoPreviewModalView : View, ModalPreviewControllerView { + fileprivate var playerView:ChatVideoAutoplayView? + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + self.background = .clear + } + + override func layout() { + super.layout() + playerView?.view.center() + } + + func getContentView() -> NSView { + return playerView?.view ?? self + } + + func update(with reference: QuickPreviewMedia, context: AccountContext, animated: Bool) -> Void { + if let reference = reference.fileReference { + let currentView = self.playerView?.view + if animated { + currentView?.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak currentView] completed in + if completed { + currentView?.removeFromSuperview() + } + }) + } else { + currentView?.removeFromSuperview() + } + + self.playerView = ChatVideoAutoplayView(mediaPlayer: MediaPlayer(postbox: context.account.postbox, reference: reference.resourceReference(reference.media.resource), streamable: reference.media.isStreamable, video: true, preferSoftwareDecoding: false, enableSound: true, volume: 1.0, fetchAutomatically: true), view: MediaPlayerView(backgroundThread: true)) + + guard let playerView = self.playerView else { + return + } + + addSubview(playerView.view) + + let size = frame.size + + let dimensions = reference.media.dimensions?.size ?? size + + playerView.view.setFrameSize(dimensions.fitted(size)) + playerView.mediaPlayer.attachPlayerView(playerView.view) + + playerView.mediaPlayer.play() + + needsLayout = true + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + + +class AnimatedStickerPreviewModalView : View, ModalPreviewControllerView { + private let loadResourceDisposable = MetaDisposable() + fileprivate let textView:TextView = TextView() + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + self.background = .clear + + addSubview(textView) + textView.backgroundColor = .clear + } + private var player: LottiePlayerView? + + override func layout() { + super.layout() + //player.center() + } + + func getContentView() -> NSView { + return player ?? self + } + + override func viewDidMoveToWindow() { + + } + + deinit { + self.loadResourceDisposable.dispose() + var bp:Int = 0 + bp += 1 + } + + func update(with reference: QuickPreviewMedia, context: AccountContext, animated: Bool) -> Void { + + if let reference = reference.fileReference { + self.player?.removeFromSuperview() + self.player = nil + + let size = NSMakeSize(frame.width - 80, frame.height - 80) + + + self.player = LottiePlayerView(frame: NSMakeRect(0, 0, size.width, size.height)) + addSubview(self.player!) + self.player?.center() + + let mediaId = reference.media.id + + let data: Signal + if let resource = reference.media.resource as? LocalBundleResource { + data = Signal { subscriber in + if let path = Bundle.main.path(forResource: resource.name, ofType: resource.ext), let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) { + subscriber.putNext(MediaResourceData(path: path, offset: 0, size: data.count, complete: true)) + subscriber.putCompletion() + } + return EmptyDisposable + } + } else { + data = context.account.postbox.mediaBox.resourceData(reference.media.resource, attemptSynchronously: true) + } + + self.loadResourceDisposable.set((data |> map { resourceData -> Data? in + + if resourceData.complete, let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path), options: [.mappedIfSafe]) { + return data + } + return nil + } |> deliverOnMainQueue).start(next: { [weak self] data in + if let data = data { + + let type: LottieAnimationType + if reference.media.mimeType == "image/webp" { + type = .webp + } else { + type = .lottie + } + + self?.player?.set(LottieAnimation(compressed: data, key: LottieAnimationEntryKey(key: .media(mediaId), size: size), type: type, cachePurpose: .none)) + } else { + self?.player?.set(nil) + } + })) + + if animated { + player!.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + + let layout = TextViewLayout(.initialize(string: reference.media.stickerText?.fixed, color: nil, font: .normal(30.0))) + layout.measure(width: .greatestFiniteMagnitude) + textView.update(layout) + textView.centerX() + if animated { + textView.layer?.animateScaleSpring(from: 0.5, to: 1.0, duration: 0.2) + } + + } else { + var bp:Int = 0 + bp += 1 + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/ModalPreviews.swift b/Telegram-Mac/ModalPreviews.swift new file mode 100644 index 0000000000..7e0d4924b4 --- /dev/null +++ b/Telegram-Mac/ModalPreviews.swift @@ -0,0 +1,27 @@ +// +// ChatModalPreviewController.swift +// Telegram +// +// Created by Mikhail Filimonov on 30/10/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + + +func ChatModalPreviewController(location: ChatLocation, context: AccountContext) -> NavigationViewController { + let navigation = MajorNavigationController(ChatController.self, ChatController(context: context, chatLocation: location, mode: .preview), context.window) + navigation.backgroundMode = theme.controllerBackgroundMode + navigation._frameRect = NSMakeRect(0, 0, 350, context.window.frame.height - 60) + navigation.canAddControllers = false + return navigation +} + + +func ChatListModalPreviewController(context: AccountContext) -> NavigationViewController { + let navigation = MajorNavigationController(ChatListController.self, ChatListController(context, modal: true, groupId: nil, filterId: nil), context.window) + navigation._frameRect = NSMakeRect(0, 0, 350, context.window.frame.height - 60) + navigation.canAddControllers = false + return navigation +} diff --git a/Telegram-Mac/NativeAudioPlayer.swift b/Telegram-Mac/NativeAudioPlayer.swift deleted file mode 100644 index 651d560885..0000000000 --- a/Telegram-Mac/NativeAudioPlayer.swift +++ /dev/null @@ -1,146 +0,0 @@ -// -// NativeAudioPlayer.swift -// TelegramMac -// -// Created by keepcoder on 22/11/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa - -import AVFoundation - -//kCMTimebaseNotification_EffectiveRateChanged -class NativeAudioPlayer: AudioPlayer { - private var _player:AVPlayer - private let item:AVPlayerItem - - private var observerContext = 0 - - override init(_ path:String) { - item = AVPlayerItem(url: URL(fileURLWithPath: path)) - _player = AVPlayer(playerItem: item) - super.init(path) - - NotificationCenter.default.addObserver(self, selector: #selector(audioPlayerDidFinishPlaying(_:)), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: item) - - - var deviceId:AudioDeviceID = AudioDeviceID() - var deviceIdRequest:AudioObjectPropertyAddress = AudioObjectPropertyAddress(mSelector: kAudioHardwarePropertyDefaultInputDevice, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMaster) - var deviceIdSize:UInt32 = UInt32(MemoryLayout.size) - - if AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject), &deviceIdRequest, 0, nil, &deviceIdSize, &deviceId) == noErr { - var masterClock:CMClock? - CMAudioDeviceClockCreateFromAudioDeviceID(kCFAllocatorDefault, deviceId, &masterClock) - _player.masterClock = masterClock - } - - item.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.status), options: [.initial, .new], context: &observerContext) - item.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.timebase), options: [.initial, .new], context: &observerContext) - } - - override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { - - guard context == &observerContext else { - super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) - return - } - - guard let keyPath = keyPath else { - return - } - - switch keyPath { - case #keyPath(AVPlayerItem.status): - guard item.status == .readyToPlay else { return } - delegate?.audioPlayerDidStartPlaying(self) - case #keyPath(AVPlayerItem.timebase): - delegate?.audioPlayerDidChangedTimebase(self) - default: - super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) - } - } - - - override var timebase:CMTimebase? { - return _player.currentItem?.timebase - } - - deinit { - NotificationCenter.default.removeObserver(self) - item.removeObserver(self, forKeyPath: #keyPath(AVPlayerItem.status)) - item.removeObserver(self, forKeyPath: #keyPath(AVPlayerItem.timebase)) - - } - - @objc func audioPlayerDidFinishPlaying(_ player: AVPlayer) { - delegate?.audioPlayerDidFinishPlaying(self) - } - - override func cleanup() { - queue.sync { - self._player.pause() - } - } - - override func playFrom(position: TimeInterval) { - queue.async { - if position > 0 { - self._player.seek(to: CMTimeMakeWithSeconds(position, 10000), toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero) - } - self._player.play() - self.delegate?.audioPlayerDidChangedTimebase(self) - - } - } - - override func set(position:TimeInterval) { - queue.async { - self._player.seek(to: CMTimeMakeWithSeconds(position, 10000), toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero) - self.delegate?.audioPlayerDidChangedTimebase(self) - } - } - - override func play() { - queue.async { - self._player.play() - } - if let _ = timebase { - delegate?.audioPlayerDidStartPlaying(self) - } - } - - override func pause() { - queue.sync { - self._player.pause() - } - delegate?.audioPlayerDidPaused(self) - delegate?.audioPlayerDidChangedTimebase(self) - } - - override func stop() { - queue.async { - self._player.seek(to: CMTimeMake(0, self._player.currentTime().timescale)) - self._player.pause() - } - } - - override var currentTime: TimeInterval { - var time:TimeInterval = 0 - - queue.sync { - time = CMTimeGetSeconds(self._player.currentTime()) - } - - return time - } - - override var duration: TimeInterval { - var time:Float64? - queue.sync { - time = CMTimeGetSeconds(self.item.asset.duration) - } - - return time ?? 0 - } -} diff --git a/Telegram-Mac/NativeCallSettingsViewController.swift b/Telegram-Mac/NativeCallSettingsViewController.swift deleted file mode 100644 index 78418e32d9..0000000000 --- a/Telegram-Mac/NativeCallSettingsViewController.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// NativeCallSettingsViewController.swift -// Telegram -// -// Created by keepcoder on 17/05/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - -import Cocoa - -class NativeCallSettingsViewController: NSViewController { - @IBOutlet weak var inputDeviceTitle: NSTextField! - @IBOutlet weak var outputDeviceTitle: NSTextField! - @IBOutlet weak var inputDeviceButton: NSPopUpButton! - @IBOutlet weak var outputDeviceButton: NSPopUpButton! - @IBOutlet weak var okButton: NSButton! - @IBOutlet weak var cancelButton: NSButton! - - @IBAction func saveAction(_ sender: Any) { - onSave(inputDevices[inputDeviceButton.index(of: inputDeviceButton.selectedItem!)], outputDevices[outputDeviceButton.index(of: outputDeviceButton.selectedItem!)]) - if let window = view.window { - window.sheetParent?.endSheet(window) - } - } - @IBAction func cancelAction(_ sender: Any) { - onCancel() - if let window = view.window { - window.sheetParent?.endSheet(window) - } - } - - private let inputDevices:[AudioDevice] - private let outputDevices:[AudioDevice] - private let currentInputDeviceId:String - private let currentOutputDeviceId:String - private let onSave:(AudioDevice, AudioDevice)->Void - private let onCancel:()->Void - - init(inputDevices:[AudioDevice], outputDevices:[AudioDevice], currentInputDeviceId:String, currentOutputDeviceId:String, onSave:@escaping(AudioDevice, AudioDevice)->Void, onCancel:@escaping()->Void) { - self.inputDevices = inputDevices - self.outputDevices = outputDevices - self.currentInputDeviceId = currentInputDeviceId - self.currentOutputDeviceId = currentOutputDeviceId - self.onSave = onSave - self.onCancel = onCancel - super.init(nibName: NSNib.Name(rawValue: "NativeCallSettingsViewController"), bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() -// for device in inputDevices { -// let index = inputDeviceButton.itemArray.count -// -// inputDeviceButton.addItem(withTitle: device.deviceId == "default" ? tr(.callDeviceSettingsDefault) : device.deviceName) -// if inputDevices[index].deviceId == currentInputDeviceId { -// inputDeviceButton.select(inputDeviceButton.lastItem) -// } -// } -// for device in outputDevices { -// let index = outputDeviceButton.itemArray.count -// outputDeviceButton.addItem(withTitle: device.deviceId == "default" ? tr(.callDeviceSettingsDefault) : device.deviceName) -// if outputDevices[index].deviceId == currentOutputDeviceId { -// outputDeviceButton.select(outputDeviceButton.lastItem) -// } -// } -// -// inputDeviceTitle.stringValue = tr(.callDeviceSettingsInputLabel) -// outputDeviceTitle.stringValue = tr(.callDeviceSettingsOutputLabel) - - okButton.title = tr(.modalOK) - cancelButton.title = tr(.modalCancel) - } - -} diff --git a/Telegram-Mac/NativeCallSettingsViewController.xib b/Telegram-Mac/NativeCallSettingsViewController.xib deleted file mode 100644 index be7f3a60a5..0000000000 --- a/Telegram-Mac/NativeCallSettingsViewController.xib +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Telegram-Mac/NetworkUsageStatsController.swift b/Telegram-Mac/NetworkUsageStatsController.swift new file mode 100644 index 0000000000..07b884c07f --- /dev/null +++ b/Telegram-Mac/NetworkUsageStatsController.swift @@ -0,0 +1,106 @@ +// +// NetworkUsageStatsController.swift +// Telegram +// +// Created by Mikhail Filimonov on 11/05/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import SwiftSignalKit +import Postbox +import TGUIKit + + +private func networkUsageStatsControllerEntries(stats: NetworkUsageStats) -> [InputDataEntry] { + var entries: [InputDataEntry] = [] + + var sectionId: Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.networkUsageHeaderGeneric), data: InputDataGeneralTextData(viewType: .textTopItem))) + index += 1 + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: .init("messagesSent"), data: InputDataGeneralData(name: L10n.networkUsageBytesSent, color: theme.colors.text, icon: nil, type: .context(.prettySized(with: Int(stats.generic.wifi.outgoing + stats.generic.cellular.outgoing))), viewType: .firstItem, action: nil))) + index += 1 + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: .init("messagesReceived"), data: InputDataGeneralData(name: L10n.networkUsageBytesReceived, color: theme.colors.text, icon: nil, type: .context(.prettySized(with: Int(stats.generic.wifi.incoming + stats.generic.cellular.incoming))), viewType: .lastItem, action: nil))) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.networkUsageHeaderImages), data: InputDataGeneralTextData(viewType: .textTopItem))) + index += 1 + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: .init("imagesSent"), data: InputDataGeneralData(name: L10n.networkUsageBytesSent, color: theme.colors.text, icon: nil, type: .context(.prettySized(with: Int(stats.image.wifi.outgoing + stats.image.cellular.outgoing))), viewType: .firstItem, action: nil))) + index += 1 + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: .init("imagesReceived"), data: InputDataGeneralData(name: L10n.networkUsageBytesReceived, color: theme.colors.text, icon: nil, type: .context(.prettySized(with: Int(stats.image.wifi.incoming + stats.image.cellular.incoming))), viewType: .lastItem, action: nil))) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.networkUsageHeaderVideos), data: InputDataGeneralTextData(viewType: .textTopItem))) + index += 1 + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: .init("videosSent"), data: InputDataGeneralData(name: L10n.networkUsageBytesSent, color: theme.colors.text, icon: nil, type: .context(.prettySized(with: Int(stats.video.wifi.outgoing + stats.video.cellular.outgoing))), viewType: .firstItem, action: nil))) + index += 1 + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: .init("videosReceived"), data: InputDataGeneralData(name: L10n.networkUsageBytesReceived, color: theme.colors.text, icon: nil, type: .context(.prettySized(with: Int(stats.video.wifi.incoming + stats.video.cellular.incoming))), viewType: .lastItem, action: nil))) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.networkUsageHeaderAudio), data: InputDataGeneralTextData(viewType: .textTopItem))) + index += 1 + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: .init("audioSent"), data: InputDataGeneralData(name: L10n.networkUsageBytesSent, color: theme.colors.text, icon: nil, type: .context(.prettySized(with: Int(stats.audio.wifi.outgoing + stats.audio.cellular.outgoing))), viewType: .firstItem, action: nil))) + index += 1 + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: .init("audioReceived"), data: InputDataGeneralData(name: L10n.networkUsageBytesReceived, color: theme.colors.text, icon: nil, type: .context(.prettySized(with: Int(stats.audio.wifi.incoming + stats.audio.cellular.incoming))), viewType: .lastItem, action: nil))) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.networkUsageHeaderFiles), data: InputDataGeneralTextData(viewType: .textTopItem))) + index += 1 + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: .init("filesSent"), data: InputDataGeneralData(name: L10n.networkUsageBytesSent, color: theme.colors.text, icon: nil, type: .context(.prettySized(with: Int(stats.file.wifi.outgoing + stats.file.cellular.outgoing))), viewType: .firstItem, action: nil))) + index += 1 + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: .init("filesReceived"), data: InputDataGeneralData(name: L10n.networkUsageBytesReceived, color: theme.colors.text, icon: nil, type: .context(.prettySized(with: Int(stats.file.wifi.incoming + stats.file.cellular.incoming))), viewType: .lastItem, action: nil))) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: .init("reset"), data: InputDataGeneralData(name: L10n.networkUsageReset, color: theme.colors.accent, icon: nil, type: .none, viewType: .singleItem, action: nil))) + index += 1 + + if stats.resetWifiTimestamp != 0 { + let formatter = DateFormatter() + formatter.dateFormat = "E, d MMM yyyy HH:mm" + let dateStringPlain = formatter.string(from: Date(timeIntervalSince1970: Double(stats.resetWifiTimestamp))) + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.networkUsageNetworkUsageSince(dateStringPlain)), data: InputDataGeneralTextData(viewType: .textTopItem))) + } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + return entries +} + +func networkUsageStatsController(context: AccountContext) -> ViewController { + + let promise: Promise = Promise() + promise.set(combineLatest(accountNetworkUsageStats(account: context.account, reset: []) |> deliverOnPrepareQueue, appearanceSignal |> deliverOnPrepareQueue) |> map {$0.0}) + + return InputDataController(dataSignal: promise.get() |> deliverOnPrepareQueue |> map {networkUsageStatsControllerEntries(stats: $0)} |> map { InputDataSignalValue(entries: $0) }, title: L10n.networkUsageNetworkUsage, validateData: { data in + if data[.init("reset")] != nil { + let reset: ResetNetworkUsageStats = [.wifi, .cellular] + promise.set(accountNetworkUsageStats(account: context.account, reset: reset)) + } + return .fail(.none) + }, removeAfterDisappear: true, hasDone: false, identifier: "networkUsage") +} diff --git a/Telegram-Mac/NewContactController.swift b/Telegram-Mac/NewContactController.swift new file mode 100644 index 0000000000..45a21aff9e --- /dev/null +++ b/Telegram-Mac/NewContactController.swift @@ -0,0 +1,161 @@ +// +// AddContactController.swift +// Telegram +// +// Created by Mikhail Filimonov on 07/06/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import Postbox +import SwiftSignalKit +import TGUIKit + +private final class NewContactArguments { + let context: AccountContext + let updateText:(String, String)->Void + let toggleAddToException:(Bool)->Void + init(context: AccountContext, updateText:@escaping(String, String)->Void, toggleAddToException:@escaping(Bool)->Void) { + self.context = context + self.updateText = updateText + self.toggleAddToException = toggleAddToException + } +} + +private struct NewContactState : Equatable { + +} +private let _id_contact_info = InputDataIdentifier("_id_contact_info") +private let _id_phone_number = InputDataIdentifier("_id_phone_number") +private let _id_add_exception = InputDataIdentifier("_id_add_exception") + +private func newContactEntries(state: EditInfoState, arguments: NewContactArguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_contact_info, equatable: InputDataEquatable(state), comparable: nil, item: { initialSize, stableId in + return EditAccountInfoItem(initialSize, stableId: stableId, account: arguments.context.account, state: state, viewType: .singleItem, updateText: arguments.updateText) + })) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_phone_number, equatable: InputDataEquatable(state.phone), comparable: nil, item: { initialSize, stableId in + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.newContactPhone, description: state.phone == nil ? L10n.newContactPhoneHidden : formatPhoneNumber(state.phone!), descTextColor: theme.colors.accent, type: .none, viewType: .singleItem, action: { + + }) + })) + index += 1 + + if state.phone == nil { + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.newContactPhoneHiddenText(state.firstName)), data: InputDataGeneralTextData(viewType: .textBottomItem))) + index += 1 + } + + + if let peerStatusSettings = state.peerStatusSettings, peerStatusSettings.contains(.addExceptionWhenAddingContact) { + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_add_exception, data: InputDataGeneralData(name: L10n.newContactExceptionShareMyPhoneNumber, color: theme.colors.text, type: .switchable(state.addToException), viewType: .singleItem, action: { + arguments.toggleAddToException(!state.addToException) + }))) + index += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.newContactExceptionShareMyPhoneNumberDesc(state.firstName)), data: InputDataGeneralTextData(viewType: .textBottomItem))) + index += 1 + + } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + return entries +} + +func NewContactController(context: AccountContext, peerId: PeerId) -> InputDataModalController { + + let initialState: EditInfoState = EditInfoState() + let stateValue: Atomic = Atomic(value: initialState) + let statePromise: ValuePromise = ValuePromise(initialState, ignoreRepeated: true) + + let updateState: (_ f:(EditInfoState)->EditInfoState)->Void = { f in + statePromise.set(stateValue.modify(f)) + } + + let arguments = NewContactArguments(context: context, updateText: { firstName, lastName in + updateState { + return $0.withUpdatedFirstName(firstName).withUpdatedLastName(lastName) + } + }, toggleAddToException: { value in + updateState { + return $0.withUpdatedAddToException(value) + } + }) + + let actionsDisposable = DisposableSet() + + let dataSignal = statePromise.get() |> mapToSignal { state in + if state.peer == nil { + return .never() + } else { + return .single(newContactEntries(state: state, arguments: arguments)) + } + } |> map { entries in + return InputDataSignalValue(entries: entries) + } + + actionsDisposable.add((context.account.postbox.peerView(id: peerId) |> deliverOnMainQueue).start(next: { pv in + updateState { current in + return current.withUpdatedPeerView(pv) + } + })) + + var dismiss:(()->Void)? + + let addContact:()->Void = { + let state = stateValue.with { $0 } + _ = showModalProgress(signal: context.engine.contacts.addContactInteractively(peerId: peerId, firstName: state.firstName, lastName: state.lastName, phoneNumber: state.phone ?? "", addToPrivacyExceptions: state.addToException), for: context.window).start(completed: { + _ = showModalSuccess(for: context.window, icon: theme.icons.successModalProgress, delay: 2.0).start() + }) + dismiss?() + } + + let controller = InputDataController(dataSignal: dataSignal, title: L10n.newContactTitle, validateData: { data in + + let firstName = stateValue.with { $0.firstName } + if firstName.isEmpty { + return .fail(.fields([_id_contact_info : .shake])) + } + addContact() + + return .fail(.none) + }, afterDisappear: { + actionsDisposable.dispose() + }) + + let modalInteractions: ModalInteractions = ModalInteractions(acceptTitle: L10n.navigationDone, accept: { [weak controller] in + controller?.validateInputValues() + }, drawBorder: true, height: 50, singleButton: true) + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions, size: NSMakeSize(300, 300)) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) + + dismiss = { [weak modalController] in + modalController?.close() + } + + return modalController +} diff --git a/Telegram-Mac/NewPollController.swift b/Telegram-Mac/NewPollController.swift new file mode 100644 index 0000000000..7064235c1c --- /dev/null +++ b/Telegram-Mac/NewPollController.swift @@ -0,0 +1,786 @@ +// +// NewPollController.swift +// Telegram +// +// Created by Mikhail Filimonov on 19/12/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import Postbox +import SwiftSignalKit +import TGUIKit + +private let optionsLimit: Int = 10 +private let maxTextLength:Int32 = 255 +private let maxOptionLength: Int32 = 100 + + +private func _id_input_option() -> InputDataIdentifier { + return InputDataIdentifier("_id_input_option_\(arc4random())") +} +private let _id_input_title = InputDataIdentifier("_id_input_title") +private let _id_input_add_option = InputDataIdentifier("_id_input_add_option") + +private let _id_anonymous = InputDataIdentifier("_id_anonymous") +private let _id_multiple_choice = InputDataIdentifier("_id_multiple_choice") +private let _id_quiz = InputDataIdentifier("_id_quiz") +private let _id_explanation = InputDataIdentifier("_id_explanation") + + +private struct NewPollOption : Equatable { + let identifier: InputDataIdentifier + let text: String + let selected: Bool + init(identifier: InputDataIdentifier, text: String, selected: Bool) { + self.identifier = identifier + self.text = text + self.selected = selected + } + func withUpdatedText(_ text: String) -> NewPollOption { + return NewPollOption(identifier: self.identifier, text: text, selected: self.selected) + } + func withUpdatedSelected(_ selected: Bool) -> NewPollOption { + return NewPollOption(identifier: self.identifier, text: self.text, selected: selected) + } +} + +private enum NewPollMode : Equatable { + case normal(anonymous: Bool) + case quiz(anonymous: Bool) + case multiple(anonymous: Bool) + + var isAnonymous: Bool { + switch self { + case let .normal(anonymous): + return anonymous + case let .quiz(anonymous): + return anonymous + case let .multiple(anonymous): + return anonymous + } + } + func withUpdatedIsAnonymous(_ anonymous: Bool) -> NewPollMode { + switch self { + case .normal: + return .normal(anonymous: anonymous) + case .quiz: + return .quiz(anonymous: anonymous) + case .multiple: + return .multiple(anonymous: anonymous) + } + } + + var isQuiz: Bool { + switch self { + case .quiz: + return true + default: + return false + } + } + var isMultiple: Bool { + switch self { + case .multiple: + return true + default: + return false + } + } + + func isModeEqual(to mode: NewPollMode) -> Bool { + switch self { + case .normal: + return !mode.isQuiz && !mode.isMultiple + case .quiz: + return mode.isQuiz + case .multiple: + return mode.isMultiple + } + } + + var publicity: TelegramMediaPollPublicity { + if isAnonymous { + return .anonymous + } else { + return .public + } + } + var kind: TelegramMediaPollKind { + switch self { + case .normal: + return .poll(multipleAnswers: false) + case .multiple: + return .poll(multipleAnswers: true) + case .quiz: + return .quiz + } + } +} + +private struct NewPollState : Equatable { + let title: String + let options: [NewPollOption] + private let random: UInt32 + let mode: NewPollMode + let isQuiz: Bool? + let quizExplanation: NSAttributedString + init(title: String, options: [NewPollOption], random: UInt32, mode: NewPollMode, isQuiz: Bool?, quizExplanation: NSAttributedString) { + self.title = title + self.options = options + self.random = random + self.mode = mode + self.isQuiz = isQuiz + self.quizExplanation = quizExplanation + } + + func withUpdatedTitle(_ title: String) -> NewPollState { + return NewPollState(title: title, options: self.options, random: self.random, mode: self.mode, isQuiz: self.isQuiz, quizExplanation: self.quizExplanation) + } + func withDeleteOption(_ identifier: InputDataIdentifier) -> NewPollState { + var options = self.options + options.removeAll(where: {$0.identifier == identifier}) + return NewPollState(title: title, options: options, random: self.random, mode: self.mode, isQuiz: self.isQuiz, quizExplanation: self.quizExplanation) + } + + func withUnselectItems() -> NewPollState { + return NewPollState(title: self.title, options: self.options.map { $0.withUpdatedSelected(false) }, random: self.random, mode: self.mode, isQuiz: self.isQuiz, quizExplanation: self.quizExplanation) + } + + func withUpdatedOption(_ f:(NewPollOption) -> NewPollOption, forKey identifier: InputDataIdentifier) -> NewPollState { + var options = self.options + if let index = options.firstIndex(where: {$0.identifier == identifier}) { + options[index] = f(options[index]) + } + return NewPollState(title: self.title, options: options, random: self.random, mode: self.mode, isQuiz: self.isQuiz, quizExplanation: self.quizExplanation) + } + + func withUpdatedOptions(_ data:[InputDataIdentifier : InputDataValue]) -> NewPollState { + var options = self.options + for (key, value) in data { + if let index = self.indexOf(key) { + options[index] = options[index].withUpdatedText(value.stringValue ?? options[index].text) + } + } + return NewPollState(title: self.title, options: options, random: self.random, mode: self.mode, isQuiz: self.isQuiz, quizExplanation: self.quizExplanation) + } + + func withAddedOption(_ option: NewPollOption) -> NewPollState { + var options = self.options + options.append(option) + return NewPollState(title: self.title, options: options, random: self.random, mode: self.mode, isQuiz: self.isQuiz, quizExplanation: self.quizExplanation) + } + func withUpdatedPos(_ previous: Int, _ current: Int) -> NewPollState { + var options = self.options + options.move(at: previous, to: current) + return NewPollState(title: self.title, options: options, random: self.random, mode: self.mode, isQuiz: self.isQuiz, quizExplanation: self.quizExplanation) + } + + func indexOf(_ identifier: InputDataIdentifier) -> Int? { + return options.firstIndex(where: { $0.identifier == identifier }) + } + + func withUpdatedState() -> NewPollState { + return NewPollState(title: self.title, options: self.options, random: arc4random(), mode: self.mode, isQuiz: self.isQuiz, quizExplanation: self.quizExplanation) + } + func withUpdatedMode(_ mode: NewPollMode) -> NewPollState { + return NewPollState(title: self.title, options: self.options, random: self.random, mode: mode, isQuiz: self.isQuiz, quizExplanation: self.quizExplanation) + } + func withUpdatedQuizExplanation(_ quizExplanation: NSAttributedString) -> NewPollState { + return NewPollState(title: self.title, options: self.options, random: self.random, mode: self.mode, isQuiz: self.isQuiz, quizExplanation: quizExplanation) + } + + var isEnabled: Bool { + let isEnabled = !title.trimmed.isEmpty && options.filter({!$0.text.trimmed.isEmpty}).count >= 2 + switch self.mode { + case .quiz: + if let option = self.options.first(where: {$0.selected }) { + if option.text.trimmed.isEmpty { + return false + } + } + return isEnabled + default: + return isEnabled + } + } + + var shouldShowTooltipForQuiz: Bool { + return self.mode.isQuiz && !self.options.contains(where: { $0.selected }) + } + + var media: TelegramMediaPoll { + var options: [TelegramMediaPollOption] = [] + var answers: [Data]? + for (i, option) in self.options.enumerated() { + if !option.text.trimmed.isEmpty { + options.append(TelegramMediaPollOption(text: option.text.trimmed, opaqueIdentifier: "\(i)".data(using: .utf8)!)) + if option.selected { + answers = [options.last!.opaqueIdentifier] + } + } + } + + let solution: TelegramMediaPollResults.Solution? + if !self.quizExplanation.string.isEmpty { + let entities = ChatTextInputState(inputText: self.quizExplanation.string, selectionRange: 0..<0, attributes: chatTextAttributes(from: self.quizExplanation)) + solution = TelegramMediaPollResults.Solution(text: self.quizExplanation.string, entities: entities.messageTextEntities()) + } else { + solution = nil + } + + return TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.LocalPoll, id: arc4random64()), publicity: mode.publicity, kind: mode.kind, text: title.trimmed, options: options, correctAnswers: answers, results: TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [], solution: solution), isClosed: false, deadlineTimeout: nil) + } +} + +private func newPollEntries(_ state: NewPollState, context: AccountContext, canBePublic: Bool, deleteOption:@escaping(InputDataIdentifier) -> Void, updateQuizSelected:@escaping(InputDataIdentifier) -> Void, updateMode: @escaping(NewPollMode)->Void) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId: Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(state.title.length > maxTextLength / 3 * 2 ? L10n.newPollQuestionHeaderLimit(Int(maxTextLength) - state.title.length) : L10n.newPollQuestionHeader), data: InputDataGeneralTextData(detectBold: false, viewType: .textTopItem))) + index += 1 + + entries.append(.input(sectionId: sectionId, index: index, value: .string(state.title), error: nil, identifier: _id_input_title, mode: .plain, data: InputDataRowData(viewType: .singleItem), placeholder: nil, inputPlaceholder: L10n.newPollQuestionPlaceholder, filter: { text in + + var text = text + while text.contains("\n\n\n") { + text = text.replacingOccurrences(of: "\n\n\n", with: "\n\n") + } + + if !text.isEmpty { + while text.range(of: "\n")?.lowerBound == text.startIndex { + text = String(text[text.index(after: text.startIndex)...]) + } + } + + return text + + }, limit: maxTextLength)) + index += 1 + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.newPollOptionsHeader), data: InputDataGeneralTextData(detectBold: false, viewType: .textTopItem))) + index += 1 + + let sorted = state.options + + + + for (i, option) in sorted.enumerated() { + + var viewType: GeneralViewType = bestGeneralViewType(sorted, for: i) + if i == sorted.count - 1, state.options.count < optionsLimit { + if i == 0 { + viewType = .firstItem + } else { + viewType = .innerItem + } + } + let placeholder: InputDataInputPlaceholder? + switch state.mode { + case .multiple: + placeholder = InputDataInputPlaceholder(hasLimitationText: true) + case .normal: + placeholder = InputDataInputPlaceholder(hasLimitationText: true) + case .quiz: + + placeholder = InputDataInputPlaceholder(nil, icon: option.selected ? theme.icons.chatToggleSelected : theme.icons.poll_quiz_unselected, drawBorderAfterPlaceholder: true, hasLimitationText: true, action: { + updateQuizSelected(option.identifier) + //deleteOption(option.identifier) + }) + } + + + + entries.append(.input(sectionId: sectionId, index: index, value: .string(option.text), error: nil, identifier: option.identifier, mode: .plain, data: InputDataRowData(viewType: viewType, rightItem: .action(theme.icons.recentDismiss, .custom({ _, _ in + deleteOption(option.identifier) + }))), placeholder: placeholder, inputPlaceholder: L10n.newPollOptionsPlaceholder, filter: { text in + return text.trimmingCharacters(in: CharacterSet.newlines) + }, limit: maxOptionLength)) + index += 1 + } + if state.options.count < optionsLimit { + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .string(nil), error: nil, identifier: _id_input_add_option, data: InputDataGeneralData(name: L10n.newPollOptionsAddOption, color: theme.colors.accent, icon: theme.icons.pollAddOption, type: .none, viewType: state.options.isEmpty ? .singleItem : .lastItem, action: nil))) + index += 1 + } + + + index = 50 + + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(state.options.count < 2 ? L10n.newPollOptionsDescriptionMinimumCountable(2) : optionsLimit == state.options.count ? L10n.newPollOptionsDescriptionLimitReached : L10n.newPollOptionsDescriptionCountable(optionsLimit - state.options.count)), data: InputDataGeneralTextData(detectBold: false, viewType: .textBottomItem))) + index += 1 + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + + var hideMultiple: Bool = false + var hideQuiz: Bool = false + if let isQuiz = state.isQuiz { + hideMultiple = isQuiz + hideQuiz = true + } + + if canBePublic { + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .string(nil), error: nil, identifier: _id_anonymous, data: InputDataGeneralData(name: L10n.newPollAnonymous, color: theme.colors.text, type: .switchable(state.mode.isAnonymous), viewType: hideQuiz && hideMultiple ? .singleItem : .firstItem, justUpdate: arc4random64(), action: { + updateMode(state.mode.withUpdatedIsAnonymous(!state.mode.isAnonymous)) + }))) + index += 1 + } + + + + if !hideMultiple { + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .string(nil), error: nil, identifier: _id_multiple_choice, data: InputDataGeneralData(name: L10n.newPollMultipleChoice, color: theme.colors.text, type: .switchable(state.mode.isMultiple), viewType: canBePublic ? hideQuiz ? .lastItem : .innerItem : hideQuiz ? .lastItem : .firstItem, enabled: !state.mode.isQuiz, justUpdate: arc4random64(), action: { + if state.mode.isMultiple { + updateMode(.normal(anonymous: state.mode.isAnonymous)) + } else { + updateMode(.multiple(anonymous: state.mode.isAnonymous)) + } + }, disabledAction: { + alert(for: context.window, info: L10n.newPollQuizMultipleError) + }))) + index += 1 + } + + + + if !hideQuiz { + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .string(nil), error: nil, identifier: _id_quiz, data: InputDataGeneralData(name: L10n.newPollQuiz, color: theme.colors.text, type: .switchable(state.mode.isQuiz), viewType: .lastItem, enabled: !state.mode.isMultiple, justUpdate: arc4random64(), action: { + if state.mode.isQuiz { + updateMode(.normal(anonymous: state.mode.isAnonymous)) + } else { + updateMode(.quiz(anonymous: state.mode.isAnonymous)) + } + }))) + index += 1 + + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.newPollQuizDesc), data: InputDataGeneralTextData(color: theme.colors.listGrayText, viewType: .textBottomItem))) + index += 1 + + + + } else if state.isQuiz == true { + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.newPollQuizDesc), data: InputDataGeneralTextData(color: theme.colors.listGrayText, viewType: .textBottomItem))) + index += 1 + } + + switch state.mode { + case .quiz: + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.newPollExplanationHeader), data: InputDataGeneralTextData(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + + entries.append(.input(sectionId: sectionId, index: index, value: .attributedString(state.quizExplanation), error: nil, identifier: _id_explanation, mode: .plain, data: InputDataRowData(viewType: .singleItem, canMakeTransformations: true), placeholder: nil, inputPlaceholder: L10n.newPollExplanationPlaceholder, filter: { text in + return text.trimmingCharacters(in: CharacterSet.newlines) + }, limit: 200)) + index += 1 + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.newPollExplanationDesc), data: InputDataGeneralTextData(color: theme.colors.listGrayText, viewType: .textBottomItem))) + index += 1 + default: + break + } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func NewPollController(chatInteraction: ChatInteraction, isQuiz: Bool? = nil) -> InputDataModalController { + + + let mode: NewPollMode + if let isQuiz = isQuiz, isQuiz { + mode = .quiz(anonymous: true) + } else { + mode = .normal(anonymous: true) + } + + let initialState = NewPollState(title: "", options: [NewPollOption(identifier: _id_input_option(), text: "", selected: false), NewPollOption(identifier: _id_input_option(), text: "", selected: false)], random: arc4random(), mode: mode, isQuiz: isQuiz, quizExplanation: NSAttributedString()) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((NewPollState) -> NewPollState) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + var shouldMakeNextResponderAfterTransition: (InputDataIdentifier, Bool, InputDataIdentifier?, Bool)? = nil + var shouldMakeNearResponderAfterTransition: (InputDataIdentifier, Int)? = nil + + let animated: Atomic = Atomic(value: false) + + let deleteOption:(InputDataIdentifier)-> Void = { identifier in + let state = stateValue.with { $0 } + updateState { state in + return state.withDeleteOption(identifier) + } + shouldMakeNearResponderAfterTransition = (identifier, state.indexOf(identifier)!) + } + let updateQuizSelected:(InputDataIdentifier)-> Void = { identifier in + updateState { state in + return state.withUnselectItems().withUpdatedOption({ option -> NewPollOption in + return option.withUpdatedSelected(true) + }, forKey: identifier) + } + } + + + var showTooltipForQuiz:(()->Void)? = nil + + + let updateMode:(NewPollMode)->Void = { mode in + let oldMode = stateValue.with { $0.mode } + + updateState { state in + if state.mode.isModeEqual(to: mode) { + return state.withUpdatedMode(mode) + } else { + return state.withUnselectItems().withUpdatedMode(mode) + } + } + if mode.isQuiz && !oldMode.isQuiz { + showTooltipForQuiz?() + } + } + + let addOption:(Bool)-> InputDataValidation = { byClick in + let option = NewPollOption(identifier: _id_input_option(), text: "", selected: false) + updateState { state in + if state.options.count < optionsLimit { + return state.withAddedOption(option) + } else { + return state + } + } + shouldMakeNextResponderAfterTransition = (option.identifier, false, byClick ? _id_input_add_option : nil, true) + + return .none + } + + var close: (() -> Void)? = nil + + + + let checkAndSend:() -> Void = { + let state = stateValue.with { $0 } + + if state.isEnabled && !state.shouldShowTooltipForQuiz { + chatInteraction.sendMedias([state.media], ChatTextInputState(), false, nil, false, nil) + close?() + } else if state.shouldShowTooltipForQuiz { + showTooltipForQuiz?() + } + } + + + let interactions = ModalInteractions(acceptTitle: L10n.modalSend, accept: { + checkAndSend() + }, drawBorder: true, height: 50, singleButton: true) + + + let canBePublic: Bool + if let peer = chatInteraction.presentation.mainPeer { + canBePublic = !peer.isChannel + } else { + canBePublic = true + } + + let context = chatInteraction.context + + let signal: Signal = statePromise.get() |> deliverOnPrepareQueue |> map { value in + return InputDataSignalValue(entries: newPollEntries(value, context: context, canBePublic: canBePublic, deleteOption: deleteOption, updateQuizSelected: updateQuizSelected, updateMode: updateMode), animated: animated.swap(true)) + } + + + let controller = InputDataController(dataSignal: signal, title: isQuiz == true ? L10n.newPollTitleQuiz : L10n.newPollTitle, validateData: { data -> InputDataValidation in + + if let _ = data[_id_input_add_option] { + return addOption(true) + } + + var fails: [InputDataIdentifier : InputDataValidationFailAction] = [:] + for (key, value) in data { + if let string = value.stringValue, string.trimmed.isEmpty { + fails[key] = .shake + } + } + if !fails.isEmpty { + + let state = stateValue.with { $0 } + + if fails.contains(where: {$0.key == _id_input_title}) { + shouldMakeNextResponderAfterTransition = (_id_input_title, true, nil, true) + } else { + for option in state.options { + if fails.contains(where: {$0.key == option.identifier}) { + shouldMakeNextResponderAfterTransition = (option.identifier, true, nil, true) + break + } + } + } + } + + return .fail(.doSomething { f in + + f(.fail(.fields(fails))) + + var addedOptions: Int? = nil + updateState { state in + var state = state + if fails.isEmpty { + if state.options.count < 2 { + state = state.withAddedOption(NewPollOption(identifier: _id_input_option(), text: "", selected: false)) + if addedOptions == nil { + addedOptions = state.options.count - 1 + } + } + } + return state.withUpdatedState() + } + + if let addedOptions = addedOptions { + let state = stateValue.with { $0 } + shouldMakeNextResponderAfterTransition = (state.options[addedOptions].identifier, false, nil, true) + } + }) + + }, updateDatas: { data in + updateState { state in + return state.withUpdatedTitle(data[_id_input_title]?.stringValue ?? state.title) + .withUpdatedOptions(data) + .withUpdatedQuizExplanation(data[_id_explanation]?.attributedString ?? state.quizExplanation) + } + return .none + }, afterDisappear: { + + }, updateDoneValue: { data in + return { f in + f(.disabled(L10n.navigationDone)) + } + }, removeAfterDisappear: true, hasDone: true, identifier: "new-poll", afterTransaction: { controller in + + if let (identifier, checkEmptyCurrent, focusIdentifier, scrollIfNeeded) = shouldMakeNextResponderAfterTransition { + var markResponder: Bool = true + let state = stateValue.with { $0 } + if let current = controller.currentFirstResponderIdentifier, checkEmptyCurrent { + if current == _id_input_title, state.title.trimmed.isEmpty { + markResponder = false + } + if let option = state.options.first(where: {$0.identifier == current}), option.text.trimmed.isEmpty { + markResponder = false + } + } + if markResponder { + controller.makeFirstResponderIfPossible(for: identifier, focusIdentifier: focusIdentifier, scrollIfNeeded: scrollIfNeeded) + } + shouldMakeNextResponderAfterTransition = nil + } + + if let (_, index) = shouldMakeNearResponderAfterTransition { + + let state = stateValue.with { $0 } + if !state.options.isEmpty { + if controller.currentFirstResponderIdentifier == nil { + if index == 0 { + if !state.options.isEmpty { + controller.makeFirstResponderIfPossible(for: state.options[0].identifier) + } + } else { + controller.makeFirstResponderIfPossible(for: state.options[index - 1].identifier) + } + } + } else { + controller.makeFirstResponderIfPossible(for: _id_input_title) + } + + + + shouldMakeNearResponderAfterTransition = nil + } + + var range: NSRange = NSMakeRange(NSNotFound, 0) + let state = stateValue.with { $0 } + + controller.tableView.enumerateItems(with: { item -> Bool in + if let identifier = (item.stableId.base as? InputDataEntryId)?.identifier { + if let _ = state.indexOf(identifier) { + if range.location == NSNotFound { + range.location = item.index + } + range.length += 1 + } + } + + return true + }) + +// +// let resort = TableResortController(resortRange: range, startTimeout: 0.1, start: { _ in +// +// }, resort: { _ in +// +// }, complete: { [weak controller] previous, current in +// if previous != current { +// _ = animated.swap(false) +// +// updateState { state in +// return state.withUpdatedPos(previous - range.location, current - range.location) +// } +// } else { +// updateState { state in +// return state.withUpdatedState() +// } +// } +// if let identifier = controller?.currentFirstResponderIdentifier { +// shouldMakeNextResponderAfterTransition = (identifier, false, nil, false) +// } +// +// }, updateItems: { currentView, items in +// +//// let items = items.compactMap { $0 as? GeneralRowItem }.filter({ $0.view?.identifier != NSUserInterfaceItemIdentifier("-1")}) +//// for (i, item) in items.enumerated() { +//// NSLog("\(item.view)") +//// item.updateViewType(bestGeneralViewType(items, for: i)) +//// item.view?.set(item: item, animated: true) +//// } +//// if let item = currentView?.item as? GeneralRowItem { +//// //item.updateViewType(.singleItem) +//// currentView?.set(item: item, animated: true) +//// } +// +// }) +// controller.tableView.resortController = resort +// + interactions.updateDone { done in + done.isEnabled = state.isEnabled + } + + }, returnKeyInvocation: { identifier, event in + + if FastSettings.checkSendingAbility(for: event) { + checkAndSend() + return .default + } else { + let state = stateValue.with { $0 } + + + if identifier == _id_input_title { + if state.options.isEmpty { + _ = addOption(false) + return .nothing + } + return .invokeEvent + } + + if let identifier = identifier { + + let index: Int? = state.indexOf(identifier) + let isLast = index == state.options.count - 1 + + if isLast { + if state.options.count == optionsLimit { + return .nothing + } else { + _ = addOption(false) + return .nothing + } + } else { + return .nextResponder + } + } + } + + + return .nothing + }, deleteKeyInvocation: { identifier in + + let state = stateValue.with { $0 } + if let index = state.options.firstIndex(where: { $0.identifier == identifier}) { + if state.options[index].text.isEmpty { + deleteOption(state.options[index].identifier) + return .invoked + } + } + + return .default + }) + + + let modalController = InputDataModalController(controller, modalInteractions: interactions, closeHandler: { f in + let state = stateValue.with { $0 } + + if !state.title.isEmpty || !state.options.filter({!$0.text.isEmpty}).isEmpty { + confirm(for: mainWindow, header: L10n.newPollDisacardConfirmHeader, information: L10n.newPollDisacardConfirm, okTitle: L10n.newPollDisacardConfirmYes, cancelTitle: L10n.newPollDisacardConfirmNo, successHandler: { _ in + f() + }) + } else { + f() + } + }) + + close = { [weak modalController] in + modalController?.modal?.close() + } + + showTooltipForQuiz = { [weak controller] in + let options = stateValue.with({ $0.options }) + for option in options { + let view = controller?.tableView.item(stableId: InputDataEntryId.input(option.identifier))?.view as? InputDataRowView + if view?.visibleRect.height == view?.frame.height { + delay(0.2, closure: { [weak view] in + view?.showPlaceholderActionTooltip(L10n.newPollQuizTooltip) + }) + break + } + + } + + } + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: close) + + + chatInteraction.context.window.set(handler: { [weak controller] _ -> KeyHandlerResult in + if let controller = controller { + let state = stateValue.with {$0} + + let id = controller.currentFirstResponderIdentifier + + if let id = id, let index = state.indexOf(id) { + let option = state.options[index] + if state.options.count - 1 == index, state.options.count < 10 { + if !option.text.isEmpty { + _ = addOption(false) + return .invoked + } + } + } + + } + return .rejected + }, with: controller, for: .Tab, priority: .supreme) + + return modalController + +} diff --git a/Telegram-Mac/NewThemeController.swift b/Telegram-Mac/NewThemeController.swift new file mode 100644 index 0000000000..f357a7afc1 --- /dev/null +++ b/Telegram-Mac/NewThemeController.swift @@ -0,0 +1,186 @@ +// +// NewThemeController.swift +// Telegram +// +// Created by Mikhail Filimonov on 28/08/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit +import TelegramCore + + + + +private let _id_input_name = InputDataIdentifier("_id_input_name") + +private struct NewThemeState : Equatable { + let name: String + let error: InputDataValueError? + init(name: String, error: InputDataValueError?) { + self.name = name + self.error = error + } + func withUpdatedCode(_ name: String) -> NewThemeState { + return NewThemeState(name: name, error: self.error) + } + func withUpdatedError(_ error: InputDataValueError?) -> NewThemeState { + return NewThemeState(name: self.name, error: error) + } +} + +private func newThemeEntries(state: NewThemeState) -> [InputDataEntry] { + var entries: [InputDataEntry] = [] + + var sectionId: Int32 = 0 + var index:Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + entries.append(.input(sectionId: sectionId, index: index, value: .string(state.name), error: state.error, identifier: _id_input_name, mode: .plain, data: InputDataRowData(), placeholder: nil, inputPlaceholder: L10n.newThemePlaceholder, filter: { $0 }, limit: 100)) + index += 1 + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.newThemeDesc), data: InputDataGeneralTextData())) + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func NewThemeController(context: AccountContext, palette: ColorPalette) -> InputDataModalController { + var palette = palette + let initialState = NewThemeState(name: findBestNameForPalette(palette), error: nil) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((NewThemeState) -> NewThemeState) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let disposable = MetaDisposable() + + var close: (() -> Void)? = nil + + let signal = statePromise.get() |> map { state in + return InputDataSignalValue(entries: newThemeEntries(state: state)) + } + + func create() -> InputDataValidation { + return .fail(.doSomething(next: { f in + + let name = stateValue.with { $0.name } + + if name.isEmpty { + f(.fail(.fields([_id_input_name : .shake]))) + return + } + + let temp = NSTemporaryDirectory() + "\(arc4random()).palette" + try? palette.toString.write(to: URL(fileURLWithPath: temp), atomically: true, encoding: .utf8) + let resource = LocalFileReferenceMediaResource(localFilePath: temp, randomId: arc4random64(), isUniquelyReferencedTemporaryFile: true, size: fs(temp)) + var thumbnailData: Data? = nil + let preview = generateThemePreview(for: palette, wallpaper: theme.wallpaper.wallpaper, backgroundMode: theme.backgroundMode) + if let mutableData = CFDataCreateMutable(nil, 0), let destination = CGImageDestinationCreateWithData(mutableData, "public.png" as CFString, 1, nil) { + CGImageDestinationAddImage(destination, preview, nil) + if CGImageDestinationFinalize(destination) { + let data = mutableData as Data + thumbnailData = data + } + } +// let baseTheme: TelegramBaseTheme? +// switch palette.parent { +// case .day: +// baseTheme = .day +// case .dayClassic: +// baseTheme = .classic +// case .nightAccent: +// baseTheme = .night +// default: +// baseTheme = nil +// } +// let settings: TelegramThemeSettings? +// if let baseTheme = baseTheme { +// settings = .init(baseTheme: baseTheme, accentColor: Int32(palette.accent.rgb), messageColors: (top: Int32(palette.bubbleBackground_outgoing.rgb), Int32(palette.bubbleBackground_outgoing.rgb)), wallpaper: nil) +// } else { +// settings = nil +// } +// + disposable.set(showModalProgress(signal: createTheme(account: context.account, title: name, resource: resource, thumbnailData: thumbnailData, settings: nil) + |> filter { value in + switch value { + case .result: + return true + default: + return false + } + } |> take(1), for: context.window).start(next: { result in + switch result { + case let .result(theme): + _ = updateThemeInteractivetly(accountManager: context.sharedContext.accountManager, f: { + $0.withUpdatedCloudTheme(theme) + }).start() + default: + break + } + exportPalette(palette: palette.withUpdatedName(name), completion: { result in + if let result = result { + NSWorkspace.shared.activateFileViewerSelecting([URL(fileURLWithPath: result)]) + } + }) + f(.success(.custom { + delay(0.2, closure: { + close?() + }) + })) + + }, error: { _ in + alert(for: context.window, info: L10n.unknownError) + })) + })) + } + + let controller = InputDataController(dataSignal: signal, title: L10n.newThemeTitle, validateData: { data in + + let name = stateValue.with { $0.name } + + if name.isEmpty { + updateState { current in + return current.withUpdatedError(.init(description: L10n.newThemeEmptyTextError, target: .data)) + } + return .fail(.fields([_id_input_name: .shake])) + } else { + return create() + } + + }, updateDatas: { data in + updateState { current in + return current.withUpdatedCode(data[_id_input_name]?.stringValue ?? current.name).withUpdatedError(nil) + } + return .none + }, afterDisappear: { + disposable.dispose() + }, getBackgroundColor: { + theme.colors.background + }) + + let modalInteractions = ModalInteractions(acceptTitle: L10n.newThemeCreate, accept: { [weak controller] in + _ = controller?.returnKeyAction() + }, drawBorder: true, height: 50, singleButton: true) + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) + + close = { [weak modalController] in + modalController?.modal?.close() + } + + return modalController + +} diff --git a/Telegram-Mac/Notices.swift b/Telegram-Mac/Notices.swift new file mode 100644 index 0000000000..9ae50efe3d --- /dev/null +++ b/Telegram-Mac/Notices.swift @@ -0,0 +1,155 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramCore + +public final class ApplicationSpecificBoolNotice: NoticeEntry { + public init() { + } + + public init(decoder: PostboxDecoder) { + } + + public func encode(_ encoder: PostboxEncoder) { + } + + public func isEqual(to: NoticeEntry) -> Bool { + if let _ = to as? ApplicationSpecificBoolNotice { + return true + } else { + return false + } + } +} + +public final class ApplicationSpecificVariantNotice: NoticeEntry { + public let value: Bool + + public init(value: Bool) { + self.value = value + } + + public init(decoder: PostboxDecoder) { + self.value = decoder.decodeInt32ForKey("v", orElse: 0) != 0 + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.value ? 1 : 0, forKey: "v") + } + + public func isEqual(to: NoticeEntry) -> Bool { + if let to = to as? ApplicationSpecificVariantNotice { + if self.value != to.value { + return false + } + return true + } else { + return false + } + } +} + +public final class ApplicationSpecificCounterNotice: NoticeEntry { + public let value: Int32 + + public init(value: Int32) { + self.value = value + } + + public init(decoder: PostboxDecoder) { + self.value = decoder.decodeInt32ForKey("v", orElse: 0) + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.value, forKey: "v") + } + + public func isEqual(to: NoticeEntry) -> Bool { + if let to = to as? ApplicationSpecificCounterNotice { + if self.value != to.value { + return false + } + return true + } else { + return false + } + } +} + +public final class ApplicationSpecificTimestampNotice: NoticeEntry { + public let value: Int32 + + public init(value: Int32) { + self.value = value + } + + public init(decoder: PostboxDecoder) { + self.value = decoder.decodeInt32ForKey("v", orElse: 0) + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.value, forKey: "v") + } + + public func isEqual(to: NoticeEntry) -> Bool { + if let to = to as? ApplicationSpecificTimestampNotice { + if self.value != to.value { + return false + } + return true + } else { + return false + } + } +} + +private func noticeNamespace(namespace: Int32) -> ValueBoxKey { + let key = ValueBoxKey(length: 4) + key.setInt32(0, value: namespace) + return key +} + +private func noticeKey(peerId: PeerId, key: Int32) -> ValueBoxKey { + let v = ValueBoxKey(length: 8 + 4) + v.setInt64(0, value: peerId.toInt64()) + v.setInt32(8, value: key) + return v +} + +private enum ApplicationSpecificGlobalNotice: Int32 { + case value = 0 + + var key: ValueBoxKey { + let v = ValueBoxKey(length: 4) + v.setInt32(0, value: self.rawValue) + return v + } +} + + +private struct ApplicationSpecificNoticeKeys { + private static let botPaymentLiabilityNamespace: Int32 = 1 + + + static func botPaymentLiabilityNotice(peerId: PeerId) -> NoticeEntryKey { + return NoticeEntryKey(namespace: noticeNamespace(namespace: botPaymentLiabilityNamespace), key: noticeKey(peerId: peerId, key: 0)) + } +} + +public struct ApplicationSpecificNotice { + public static func getBotPaymentLiability(accountManager: AccountManager, peerId: PeerId) -> Signal { + return accountManager.transaction { transaction -> Bool in + if let _ = transaction.getNotice(ApplicationSpecificNoticeKeys.botPaymentLiabilityNotice(peerId: peerId)) as? ApplicationSpecificBoolNotice { + return true + } else { + return false + } + } + } + + public static func setBotPaymentLiability(accountManager: AccountManager, peerId: PeerId) -> Signal { + return accountManager.transaction { transaction -> Void in + transaction.setNotice(ApplicationSpecificNoticeKeys.botPaymentLiabilityNotice(peerId: peerId), ApplicationSpecificBoolNotice()) + } + } +} diff --git a/Telegram-Mac/NotificationPreferencesController.swift b/Telegram-Mac/NotificationPreferencesController.swift new file mode 100644 index 0000000000..78277689cf --- /dev/null +++ b/Telegram-Mac/NotificationPreferencesController.swift @@ -0,0 +1,454 @@ +// +// NotificationPreferencesController.swift +// Telegram +// +// Created by Mikhail Filimonov on 03/06/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit +import Postbox +import TelegramCore + +private let modernSoundsNamePaths: [String] = [ + L10n.notificationsSoundNote, + L10n.notificationsSoundAurora, + L10n.notificationsSoundBamboo, + L10n.notificationsSoundChord, + L10n.notificationsSoundCircles, + L10n.notificationsSoundComplete, + L10n.notificationsSoundHello, + L10n.notificationsSoundInput, + L10n.notificationsSoundKeys, + L10n.notificationsSoundPopcorn, + L10n.notificationsSoundPulse, + L10n.notificationsSoundSynth +] + +private let classicSoundNamePaths: [String] = [ + L10n.notificationsSoundTritone, + L10n.notificationsSoundTremolo, + L10n.notificationsSoundAlert, + L10n.notificationsSoundBell, + L10n.notificationsSoundCalypso, + L10n.notificationsSoundChime, + L10n.notificationsSoundGlass, + L10n.notificationsSoundTelegraph +] + +private func soundName(sound: PeerMessageSound) -> String { + switch sound { + case .none: + return L10n.notificationsSoundNone + case .default: + return "" + case let .bundledModern(id): + if id >= 0 && Int(id) < modernSoundsNamePaths.count { + return modernSoundsNamePaths[Int(id)] + } + return "Sound \(id)" + case let .bundledClassic(id): + if id >= 0 && Int(id) < classicSoundNamePaths.count { + return classicSoundNamePaths[Int(id)] + } + return "Sound \(id)" + } +} + +public func localizedPeerNotificationSoundString(sound: PeerMessageSound, default: PeerMessageSound? = nil) -> String { + switch sound { + case .default: + if let defaultSound = `default` { + let name = soundName(sound: defaultSound) + let actualName: String + if name.isEmpty { + actualName = soundName(sound: .bundledModern(id: 0)) + } else { + actualName = name + } + return L10n.peerInfoNotificationsDefaultSound(actualName) + } else { + return L10n.peerInfoNotificationsDefault + } + default: + return soundName(sound: sound) + } +} + +func fileNameForNotificationSound(_ sound: PeerMessageSound, defaultSound: PeerMessageSound?) -> String { + switch sound { + case .none: + return "" + case .default: + if let defaultSound = defaultSound { + if case .default = defaultSound { + return "\(100)" + } else { + return fileNameForNotificationSound(defaultSound, defaultSound: nil) + } + } else { + return "default" + } + case let .bundledModern(id): + return "\(id + 100)" + case let .bundledClassic(id): + return "\(id + 2)" + } +} + + + + + +enum NotificationsAndSoundsEntryTag: ItemListItemTag { + case allAccounts + case messagePreviews + case includeChannels + case unreadCountCategory + case joinedNotifications + case reset + + var stableId: InputDataEntryId { + switch self { + case .allAccounts: + return .general(_id_all_accounts) + case .messagePreviews: + return .general(_id_message_preview) + case .includeChannels: + return .general(_id_include_channels) + case .unreadCountCategory: + return .general(_id_count_unred_messages) + case .joinedNotifications: + return .general(_id_new_contacts) + case .reset: + return .general(_id_reset) + } + } + + func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? NotificationsAndSoundsEntryTag, self == other { + return true + } else { + return false + } + } +} + +private final class NotificationArguments { + let resetAllNotifications:() -> Void + let toggleMessagesPreview:() -> Void + let toggleNotifications:() -> Void + let notificationTone:(PeerMessageSound) -> Void + let toggleIncludeUnreadChats:(Bool) -> Void + let toggleCountUnreadMessages:(Bool) -> Void + let toggleIncludeGroups:(Bool) -> Void + let toggleIncludeChannels:(Bool) -> Void + let allAcounts: ()-> Void + let snoof: ()-> Void + let updateJoinedNotifications: (Bool) -> Void + let toggleBadge: (Bool)->Void + let toggleRequestUserAttention: ()->Void + let toggleInAppSounds:(Bool)->Void + init(resetAllNotifications: @escaping() -> Void, toggleMessagesPreview:@escaping() -> Void, toggleNotifications:@escaping() -> Void, notificationTone:@escaping(PeerMessageSound) -> Void, toggleIncludeUnreadChats:@escaping(Bool) -> Void, toggleCountUnreadMessages:@escaping(Bool) -> Void, toggleIncludeGroups:@escaping(Bool) -> Void, toggleIncludeChannels:@escaping(Bool) -> Void, allAcounts: @escaping()-> Void, snoof: @escaping()-> Void, updateJoinedNotifications: @escaping(Bool) -> Void, toggleBadge: @escaping(Bool)->Void, toggleRequestUserAttention: @escaping ()->Void, toggleInAppSounds: @escaping(Bool)->Void) { + self.resetAllNotifications = resetAllNotifications + self.toggleMessagesPreview = toggleMessagesPreview + self.toggleNotifications = toggleNotifications + self.notificationTone = notificationTone + self.toggleIncludeUnreadChats = toggleIncludeUnreadChats + self.toggleCountUnreadMessages = toggleCountUnreadMessages + self.toggleIncludeGroups = toggleIncludeGroups + self.toggleIncludeChannels = toggleIncludeChannels + self.allAcounts = allAcounts + self.snoof = snoof + self.updateJoinedNotifications = updateJoinedNotifications + self.toggleBadge = toggleBadge + self.toggleRequestUserAttention = toggleRequestUserAttention + self.toggleInAppSounds = toggleInAppSounds + } +} + +private let _id_all_accounts = InputDataIdentifier("_id_all_accounts") +private let _id_notifications = InputDataIdentifier("_id_notifications") +private let _id_message_preview = InputDataIdentifier("_id_message_preview") +private let _id_reset = InputDataIdentifier("_id_reset") + +private let _id_badge_enabled = InputDataIdentifier("_badge_enabled") +private let _id_include_muted_chats = InputDataIdentifier("_id_include_muted_chats") +private let _id_include_public_group = InputDataIdentifier("_id_include_public_group") +private let _id_include_channels = InputDataIdentifier("_id_include_channels") +private let _id_count_unred_messages = InputDataIdentifier("_id_count_unred_messages") +private let _id_new_contacts = InputDataIdentifier("_id_new_contacts") +private let _id_snoof = InputDataIdentifier("_id_snoof") +private let _id_tone = InputDataIdentifier("_id_tone") +private let _id_bounce = InputDataIdentifier("_id_bounce") + +private let _id_turnon_notifications = InputDataIdentifier("_id_turnon_notifications") +private let _id_turnon_notifications_title = InputDataIdentifier("_id_turnon_notifications_title") + +private let _id_message_effect = InputDataIdentifier("_id_message_effect") + +private func notificationEntries(settings:InAppNotificationSettings, globalSettings: GlobalNotificationSettingsSet, accounts: [AccountWithInfo], unAuthStatus: UNUserNotifications.AuthorizationStatus, arguments: NotificationArguments) -> [InputDataEntry] { + + var entries:[InputDataEntry] = [] + + var sectionId: Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + switch unAuthStatus { + case .denied: + + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_turnon_notifications_title, equatable: nil, comparable: nil, item: { initialSize, stableId in + return TurnOnNotificationsRowItem(initialSize, stableId: stableId, viewType: .firstItem) + })) + index += 1 + + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_turnon_notifications, data: InputDataGeneralData(name: L10n.notificationSettingsTurnOn, color: theme.colors.text, type: .none, viewType: .lastItem, action: { + openSystemSettings(.notifications) + }))) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + default: + break + } + + if accounts.count > 1 { + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.notificationSettingsShowNotificationsFrom), data: InputDataGeneralTextData(viewType: .textTopItem))) + index += 1 + + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_all_accounts, data: InputDataGeneralData(name: L10n.notificationSettingsAllAccounts, color: theme.colors.text, type: .switchable(settings.notifyAllAccounts), viewType: .singleItem, action: { + arguments.allAcounts() + }))) + index += 1 + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(settings.notifyAllAccounts ? L10n.notificationSettingsShowNotificationsFromOn : L10n.notificationSettingsShowNotificationsFromOff), data: InputDataGeneralTextData(viewType: .textBottomItem))) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + } + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.notificationSettingsToggleNotificationsHeader), data: InputDataGeneralTextData(viewType: .textTopItem))) + index += 1 + + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_notifications, data: InputDataGeneralData(name: L10n.notificationSettingsToggleNotifications, color: theme.colors.text, type: .switchable(settings.enabled), viewType: .firstItem, action: { + arguments.toggleNotifications() + }))) + index += 1 + + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_message_preview, data: InputDataGeneralData(name: L10n.notificationSettingsMessagesPreview, color: theme.colors.text, type: .switchable(settings.displayPreviews), viewType: .innerItem, action: { + arguments.toggleMessagesPreview() + }))) + index += 1 + + var tonesItems:[SPopoverItem] = [] + + tonesItems.append(SPopoverItem(localizedPeerNotificationSoundString(sound: .default), { + arguments.notificationTone(.default) + })) + + tonesItems.append(SPopoverItem(localizedPeerNotificationSoundString(sound: .none), { + arguments.notificationTone(.none) + })) + + + for i in 0 ..< 12 { + let sound: PeerMessageSound = .bundledModern(id: Int32(i)) + tonesItems.append(SPopoverItem(localizedPeerNotificationSoundString(sound: sound), { + arguments.notificationTone(sound) + })) + } + for i in 0 ..< 8 { + let sound: PeerMessageSound = .bundledClassic(id: Int32(i)) + tonesItems.append(SPopoverItem(localizedPeerNotificationSoundString(sound: sound), { + arguments.notificationTone(sound) + })) + } + + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_tone, data: InputDataGeneralData(name: L10n.notificationSettingsNotificationTone, color: theme.colors.text, type: .contextSelector(localizedPeerNotificationSoundString(sound: settings.tone), tonesItems), viewType: .innerItem))) + index += 1 + + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_bounce, data: InputDataGeneralData(name: L10n.notificationSettingsBounceDockIcon, color: theme.colors.text, type: .switchable(settings.requestUserAttention), viewType: .innerItem, action: { + arguments.toggleRequestUserAttention() + }))) + index += 1 + + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_reset, data: InputDataGeneralData(name: L10n.notificationSettingsResetNotifications, color: theme.colors.text, type: .none, viewType: .lastItem, action: { + arguments.resetAllNotifications() + }))) + index += 1 + + + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.notificationSettingsSoundEffects), data: InputDataGeneralTextData(viewType: .textTopItem))) + index += 1 + + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_message_effect, data: InputDataGeneralData(name: L10n.notificationSettingsSendMessageEffect, color: theme.colors.text, type: .switchable(FastSettings.inAppSounds), viewType: .singleItem, action: { + arguments.toggleInAppSounds(!FastSettings.inAppSounds) + }))) + index += 1 + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.notificationSettingsBadgeHeader), data: InputDataGeneralTextData(viewType: .textTopItem))) + index += 1 + + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_badge_enabled, data: InputDataGeneralData(name: L10n.notificationSettingsBadgeEnabled, color: theme.colors.text, type: .switchable(settings.badgeEnabled), viewType: .firstItem, action: { + arguments.toggleBadge(!settings.badgeEnabled) + }))) + index += 1 + + + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_include_public_group, data: InputDataGeneralData(name: L10n.notificationSettingsIncludeGroups, color: theme.colors.text, type: .switchable(settings.totalUnreadCountIncludeTags.contains(.group)), viewType: .innerItem, enabled: settings.badgeEnabled, action: { + arguments.toggleIncludeGroups(!settings.totalUnreadCountIncludeTags.contains(.group)) + }))) + index += 1 + + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_include_channels, data: InputDataGeneralData(name: L10n.notificationSettingsIncludeChannels, color: theme.colors.text, type: .switchable(settings.totalUnreadCountIncludeTags.contains(.channel)), viewType: .innerItem, enabled: settings.badgeEnabled, action: { + arguments.toggleIncludeChannels(!settings.totalUnreadCountIncludeTags.contains(.channel)) + }))) + index += 1 + + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_count_unred_messages, data: InputDataGeneralData(name: L10n.notificationSettingsCountUnreadMessages, color: theme.colors.text, type: .switchable(settings.totalUnreadCountDisplayCategory == .messages), viewType: .lastItem, enabled: settings.badgeEnabled, action: { + arguments.toggleCountUnreadMessages(settings.totalUnreadCountDisplayCategory != .messages) + }))) + index += 1 + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.notificationSettingsBadgeDesc), data: InputDataGeneralTextData(viewType: .textBottomItem))) + index += 1 + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_new_contacts, data: InputDataGeneralData(name: L10n.notificationSettingsContactJoined, color: theme.colors.text, type: .switchable(globalSettings.contactsJoined), viewType: .singleItem, action: { + arguments.updateJoinedNotifications(!globalSettings.contactsJoined) + }))) + index += 1 + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.notificationSettingsContactJoinedInfo), data: InputDataGeneralTextData(viewType: .textBottomItem))) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.notificationSettingsSnoofHeader), data: InputDataGeneralTextData(viewType: .textTopItem))) + index += 1 + + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_snoof, data: InputDataGeneralData(name: L10n.notificationSettingsSnoof, color: theme.colors.text, type: .switchable(!settings.showNotificationsOutOfFocus), viewType: .singleItem, action: { + arguments.snoof() + }))) + index += 1 + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(!settings.showNotificationsOutOfFocus ? L10n.notificationSettingsSnoofOn : L10n.notificationSettingsSnoofOff), data: InputDataGeneralTextData(viewType: .textBottomItem))) + index += 1 + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + return entries +} + +func NotificationPreferencesController(_ context: AccountContext, focusOnItemTag: NotificationsAndSoundsEntryTag? = nil) -> ViewController { + let arguments = NotificationArguments(resetAllNotifications: { + confirm(for: context.window, header: L10n.notificationSettingsConfirmReset, information: tr(L10n.chatConfirmActionUndonable), successHandler: { _ in + _ = resetPeerNotificationSettings(network: context.account.network).start() + }) + }, toggleMessagesPreview: { + _ = updateInAppNotificationSettingsInteractively(accountManager: context.sharedContext.accountManager, {$0.withUpdatedDisplayPreviews(!$0.displayPreviews)}).start() + }, toggleNotifications: { + _ = updateInAppNotificationSettingsInteractively(accountManager: context.sharedContext.accountManager, {$0.withUpdatedEnables(!$0.enabled)}).start() + }, notificationTone: { tone in + if tone == .default { + + } else if tone != .none { + let name = fileNameForNotificationSound(tone, defaultSound: nil) + SoundEffectPlay.play(postbox: context.account.postbox, name: name, type: "m4a") + } + _ = updateInAppNotificationSettingsInteractively(accountManager: context.sharedContext.accountManager, {$0.withUpdatedTone(tone)}).start() + }, toggleIncludeUnreadChats: { enable in + _ = updateInAppNotificationSettingsInteractively(accountManager: context.sharedContext.accountManager, {$0.withUpdatedTotalUnreadCountDisplayStyle(enable ? .raw : .filtered)}).start() + }, toggleCountUnreadMessages: { enable in + _ = updateInAppNotificationSettingsInteractively(accountManager: context.sharedContext.accountManager, {$0.withUpdatedTotalUnreadCountDisplayCategory(enable ? .messages : .chats)}).start() + }, toggleIncludeGroups: { enable in + _ = updateInAppNotificationSettingsInteractively(accountManager: context.sharedContext.accountManager, { value in + var tags: PeerSummaryCounterTags = value.totalUnreadCountIncludeTags + if enable { + tags.insert(.group) + } else { + tags.remove(.group) + } + return value.withUpdatedTotalUnreadCountIncludeTags(tags) + }).start() + }, toggleIncludeChannels: { enable in + _ = updateInAppNotificationSettingsInteractively(accountManager: context.sharedContext.accountManager, { value in + var tags: PeerSummaryCounterTags = value.totalUnreadCountIncludeTags + if enable { + tags.insert(.channel) + } else { + tags.remove(.channel) + } + return value.withUpdatedTotalUnreadCountIncludeTags(tags) + }).start() + }, allAcounts: { + _ = updateInAppNotificationSettingsInteractively(accountManager: context.sharedContext.accountManager, { value in + return value.withUpdatedNotifyAllAccounts(!value.notifyAllAccounts) + }).start() + }, snoof: { + _ = updateInAppNotificationSettingsInteractively(accountManager: context.sharedContext.accountManager, { value in + return value.withUpdatedSnoof(!value.showNotificationsOutOfFocus) + }).start() + }, updateJoinedNotifications: { value in + _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in + var settings = settings + settings.contactsJoined = value + return settings + }).start() + }, toggleBadge: { enabled in + _ = updateInAppNotificationSettingsInteractively(accountManager: context.sharedContext.accountManager, { value in + return value.withUpdatedBadgeEnabled(enabled) + }).start() + }, toggleRequestUserAttention: { + _ = updateInAppNotificationSettingsInteractively(accountManager: context.sharedContext.accountManager, { value in + return value.withUpdatedRequestUserAttention(!value.requestUserAttention) + }).start() + }, toggleInAppSounds: { value in + FastSettings.toggleInAppSouds(value) + }) + + + + let entriesSignal = combineLatest(queue: prepareQueue, appNotificationSettings(accountManager: context.sharedContext.accountManager), globalNotificationSettings(postbox: context.account.postbox), context.sharedContext.activeAccountsWithInfo |> map { $0.accounts }, UNUserNotifications.recurrentAuthorizationStatus(context)) |> map { inAppSettings, globalSettings, accounts, unAuthStatus -> [InputDataEntry] in + return notificationEntries(settings: inAppSettings, globalSettings: globalSettings, accounts: accounts, unAuthStatus: unAuthStatus, arguments: arguments) + } + + + let controller = InputDataController(dataSignal: entriesSignal |> map { InputDataSignalValue(entries: $0) }, title: L10n.telegramNotificationSettingsViewController, hasDone: false, identifier: "notification-settings") + + + controller.didLoaded = { controller, _ in + if let focusOnItemTag = focusOnItemTag { + controller.genericView.tableView.scroll(to: .center(id: focusOnItemTag.stableId, innerId: nil, animated: true, focus: .init(focus: true), inset: 0), inset: NSEdgeInsets()) + } + } + + return controller +} diff --git a/Telegram-Mac/NotificationSettingsViewController.swift b/Telegram-Mac/NotificationSettingsViewController.swift deleted file mode 100644 index d32714fbbe..0000000000 --- a/Telegram-Mac/NotificationSettingsViewController.swift +++ /dev/null @@ -1,409 +0,0 @@ -// -// NotificationSettingsViewController.swift -// TelegramMac -// -// Created by keepcoder on 15/11/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import TGUIKit -import SwiftSignalKitMac -import PostboxMac -import TelegramCoreMac - -fileprivate enum NotificationSettingsEntry : Comparable, Identifiable { - case notifications - case messagePreview - case notificationTone(String) - case resetNotifications - case resetText - case whiteSpace(Int32,CGFloat) - case searchField - case peer(ChatListEntry) - case searchPeer(Peer, Int32, PeerNotificationSettings?) - var index:Int32 { - switch self { - case .notifications: - return 1000 - case .messagePreview: - return 2000 - case .notificationTone: - return 3000 - case .resetNotifications: - return 4000 - case .resetText: - return 5000 - case let .whiteSpace(index,_): - return index - case .searchField: - return 6000 - case let .peer(entry): - return INT32_MAX - entry.index.messageIndex.timestamp - case let .searchPeer(_, index, _): - return 7000 + index - } - } - - var stableId:AnyHashable { - switch self { - case let .peer(entry): - switch entry { - case let .HoleEntry(hole): - return hole - default: - return entry.index - } - case let .searchPeer(peer,_,_): - return peer.id - default: - return Int64(index) - } - } -} - -fileprivate func <(lhs:NotificationSettingsEntry, rhs:NotificationSettingsEntry) -> Bool { - return lhs.index < rhs.index -} - -fileprivate func ==(lhs:NotificationSettingsEntry, rhs:NotificationSettingsEntry) -> Bool { - switch lhs { - case let .peer(lhsEntry): - if case let .peer(rhsEntry) = rhs { - return lhsEntry == rhsEntry - } - case let .searchPeer(lhsPeer, lhsIndex, lhsNotificationSettings): - if case let .searchPeer(rhsPeer, rhsIndex, rhsNotificationSettings) = rhs { - - if let lhsNotificationSettings = lhsNotificationSettings, let rhsNotificationSettings = rhsNotificationSettings { - if !lhsNotificationSettings.isEqual(to: rhsNotificationSettings) { - return false - } - } else if (lhsNotificationSettings != nil) != (rhsNotificationSettings != nil) { - return false - } - - return lhsPeer.isEqual(rhsPeer) && lhsIndex == rhsIndex - } - case let .notificationTone(lhsTone): - if case let .notificationTone(rhsTone) = rhs { - return lhsTone == rhsTone - } - return false - default: - return lhs.stableId == rhs.stableId - } - return lhs.stableId == rhs.stableId -} - -private func simpleEntries(_ settings:InAppNotificationSettings) -> [NotificationSettingsEntry] { - var simpleEntries:[NotificationSettingsEntry] = [] - simpleEntries.append(.whiteSpace(1,15)) - simpleEntries.append(.notifications) - simpleEntries.append(.messagePreview) - simpleEntries.append(.notificationTone(settings.tone)) - simpleEntries.append(.resetNotifications) - simpleEntries.append(.resetText) - simpleEntries.append(.whiteSpace(5001,40)) - simpleEntries.append(.searchField) - return simpleEntries.reversed() -} - -fileprivate struct NotificationsSettingsList { - let list:[AppearanceWrapperEntry] - let settings:InAppNotificationSettings -} - -struct NotificationSettingsInteractions { - let resetAllNotifications:() -> Void - let toggleMessagesPreview:() -> Void - let toggleNotifications:() -> Void - let notificationTone:(String) -> Void -} - - -class NotificationSettingsViewController: TableViewController { - private let request = Promise() - private let disposable:MetaDisposable = MetaDisposable() - private let notificationsDisposable:MetaDisposable = MetaDisposable() - private var tones:[SPopoverItem] = [] - - private let search:ValuePromise = ValuePromise(ignoreRepeated: true) - - override var removeAfterDisapper:Bool { - return true - } - - - - override func viewDidLoad() { - super.viewDidLoad() - - let previous:Atomic = Atomic(value: nil) - let initialSize = self.atomicSize - let account = self.account - - - search.set(SearchState(state: .None, request: nil)) - - - let searchInteractions = SearchInteractions({ [weak self] state in - self?.search.set(state) - }, { [weak self] state in - self?.search.set(state) - }) - - let interactions = NotificationSettingsInteractions(resetAllNotifications: { [weak self] in - if let window = self?.window , let account = self?.account { - confirm(for: window, with: tr(.notificationSettingsConfirmReset), and: tr(.chatConfirmActionUndonable), successHandler: { _ in - _ = resetPeerNotificationSettings(network: account.network).start() - }) - } - }, toggleMessagesPreview: { - _ = updateInAppNotificationSettingsInteractively(postbox: account.postbox, {$0.withUpdatedDisplayPreviews(!$0.displayPreviews)}).start() - }, toggleNotifications: { - _ = updateInAppNotificationSettingsInteractively(postbox: account.postbox, {$0.withUpdatedEnables(!$0.enabled)}).start() - }, notificationTone: { (tone) in - - }); - - let tones = ObjcUtils.notificationTones("Default") - for tone in tones { - self.tones.append(SPopoverItem(localizedString(tone), { - _ = NSSound(named: NSSound.Name(rawValue: tone))?.play() - _ = updateInAppNotificationSettingsInteractively(postbox: account.postbox, {$0.withUpdatedTone(tone)}).start() - })) - } - - let first = Atomic(value:true) - - let list:Signal,Void> = (combineLatest(request.get() |> distinctUntilChanged, search.get() |> distinctUntilChanged) |> mapToSignal { [weak self] (location, search) -> Signal,Void> in - - var signal:Signal - - - let mappedEntries:Signal<[NotificationSettingsEntry],Void> - - if search.request.isEmpty || search.state == .None { - - switch(location) { - case let .Initial(count, _): - signal = account.viewTracker.tailChatListView(count: count) |> map {$0.0} - case let .Index(index): - signal = account.viewTracker.aroundChatListView(index: index, count: 100) |> map {$0.0} - } - - mappedEntries = signal |> map { value -> [NotificationSettingsEntry] in - var ids:[PeerId:PeerId] = [:] - - return value.entries.filter({ index -> Bool in - switch index { - case .HoleEntry: - return false - case let .MessageEntry(_, _, _, _,_, renderedPeer, _): - let first = ids[renderedPeer.peerId] == nil && renderedPeer.peerId.namespace != Namespaces.Peer.SecretChat - ids[renderedPeer.peerId] = renderedPeer.peerId - return first - } - - }).map { peer -> NotificationSettingsEntry in - return .peer(peer) - } - } |> mapToQueue { list in return .single(list)} - - - } else { - - - var ids:[PeerId:Peer] = [:] - let foundLocalPeers = combineLatest(account.postbox.searchPeers(query: search.request.lowercased()) |> map {$0.flatMap({$0.chatMainPeer}).filter({!($0 is TelegramSecretChat)})},account.postbox.searchContacts(query: search.request.lowercased())) - |> map { (peers, contacts) -> [Peer] in - return (peers + contacts).filter({ (peer) -> Bool in - let first = ids[peer.id] == nil - ids[peer.id] = peer - return first - }) - } - - mappedEntries = foundLocalPeers |> mapToSignal { peers -> Signal<[NotificationSettingsEntry], Void> in - - return combineLatest(peers.map { peer -> Signal in - - return account.postbox.modify { (modifier) -> TelegramPeerNotificationSettings? in - return modifier.getPeerNotificationSettings(peer.id) as? TelegramPeerNotificationSettings - } - - }) |> map { (settings) -> [NotificationSettingsEntry] in - var entries:[NotificationSettingsEntry] = [] - for i in 0 ..< peers.count { - entries.append(.searchPeer(peers[i], Int32(i) + 1, settings[i])) - } - return entries - } - } - } - - return combineLatest(mappedEntries, account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.inAppNotificationSettings]), appearanceSignal) |> map { value, settings, appearance -> NotificationsSettingsList in - - let inAppSettings: InAppNotificationSettings - if let settings = settings.values[ApplicationSpecificPreferencesKeys.inAppNotificationSettings] as? InAppNotificationSettings { - inAppSettings = settings - } else { - inAppSettings = InAppNotificationSettings.defaultSettings - } - return NotificationsSettingsList(list: (simpleEntries(inAppSettings) + value).map {AppearanceWrapperEntry(entry: $0, appearance: appearance)}.sorted(by: <), settings: inAppSettings) - } |> mapToQueue { [weak self] value -> Signal, Void> in - if let strongSelf = self { - return strongSelf.prepareEntries(from: previous.modify {$0}, to: value, account: account, interactions:interactions, searchInteractions: searchInteractions, initialSize: initialSize.modify({$0}), animated: !first.swap(false)) - } - return .never() - } - - }) - |> deliverOnMainQueue - - - - - let apply = list |> mapToSignal { [weak self] transition -> Signal in - - self?.readyOnce() - - self?.genericView.resetScrollNotifies() - _ = previous.swap(transition.entries) - self?.genericView.merge(with: transition) - self?.searchView?.searchInteractions = searchInteractions - return .complete() - - } - - disposable.set(apply.start()) - - request.set(.single(.Initial(100, nil))) - - } - - fileprivate func prepareEntries(from:NotificationsSettingsList?, to:NotificationsSettingsList, account:Account, interactions:NotificationSettingsInteractions, searchInteractions:SearchInteractions, initialSize:NSSize, animated:Bool) -> Signal,Void> { - - return Signal { [weak self] subscriber in - - let (deleted,inserted, updated) = proccessEntriesWithoutReverse(from?.list, right: to.list, { (entry) -> TableRowItem in - - switch entry.entry { - case .notifications: - return GeneralInteractedRowItem(initialSize, stableId: entry.stableId, name: tr(.notificationSettingsToggleNotifications), type: .switchable(stateback: { () -> Bool in - return to.settings.enabled - }), action: { - interactions.toggleNotifications() - }) - case .messagePreview: - return GeneralInteractedRowItem(initialSize, stableId: entry.stableId, name: tr(.notificationSettingsMessagesPreview), type: .switchable(stateback: { () -> Bool in - return to.settings.displayPreviews - }), action: { - interactions.toggleMessagesPreview() - }) - case let .whiteSpace(_, height): - return GeneralRowItem(initialSize, height: height, stableId: entry.stableId) - case .notificationTone: - return GeneralInteractedRowItem(initialSize, stableId: entry.stableId, name: tr(.notificationSettingsNotificationTone), type: .context(stateback: { () -> String in - return to.settings.tone.isEmpty ? tr(.notificationSettingsToneDefault) : localizedString(to.settings.tone) - }), action: { [weak self] in - self?.showToneOptions() - }) - case .resetNotifications: - return GeneralInteractedRowItem(initialSize, stableId: entry.stableId, name: tr(.notificationSettingsResetNotifications), type: .next, action: { - interactions.resetAllNotifications() - }) - case .resetText: - return GeneralTextRowItem(initialSize, stableId: entry.stableId, text: tr(.notificationSettingsResetNotificationsText)) - case .searchField: - return SearchRowItem(initialSize, stableId: entry.stableId, searchInteractions:searchInteractions) - case let .peer(peerEntry): - switch peerEntry { - case .HoleEntry: - return GeneralRowItem(initialSize, stableId:entry.stableId) - case let .MessageEntry(_, _, _, notifySettings,_, renderedPeer, _): - if let peer = renderedPeer.chatMainPeer { - return ShortPeerRowItem(initialSize, peer: peer, account: account, height: 40, photoSize: NSMakeSize(30,30), inset: NSEdgeInsets(left:30,right:30), generalType:.switchable(stateback: { - if let notifySettings = notifySettings as? TelegramPeerNotificationSettings { - if case .muted(_) = notifySettings.muteState { - return false - } - } - return true - }), action:{ [weak self] in - self?.notificationsDisposable.set(togglePeerMuted(account: account, peerId: peer.id).start()) - }) - } - return GeneralRowItem(initialSize, stableId:entry.stableId) - } - case let .searchPeer(peer, _, notifySettings): - return ShortPeerRowItem(initialSize, peer: peer, account: account, height: 40, photoSize: NSMakeSize(30,30), inset: NSEdgeInsets(left:30,right:30), generalType:.switchable(stateback: { - if let notifySettings = notifySettings as? TelegramPeerNotificationSettings { - if case .muted(_) = notifySettings.muteState { - return false - } - } - return true - }), action:{ [weak self] in - self?.notificationsDisposable.set(togglePeerMuted(account: account, peerId: peer.id).start()) - }) - - } - - }) - - let transition = TableEntriesTransition(deleted: deleted, inserted: inserted, updated:updated, entries: to, animated:animated, state: animated ? .none(nil) : .saveVisible(.lower)) - subscriber.putNext(transition) - subscriber.putCompletion() - - return EmptyDisposable - } |> runOn(prepareQueue) - - } - - func showToneOptions() { - if let view = (genericView.viewNecessary(at: 3) as? GeneralInteractedRowView)?.textView { - showPopover(for: view, with: SPopoverViewController(items: tones), edge: .minX, inset: NSMakePoint(0,-30)) - } - - } - - override var canBecomeResponder: Bool { - return true - } - - override func becomeFirstResponder() -> Bool? { - return false - } - - override func firstResponder() -> NSResponder? { - if let item = genericView.item(stableId: NotificationSettingsEntry.searchField.stableId), let view = genericView.viewNecessary(at: item.index) as? SearchRowView { - return view.searchView.input - } - return nil - } - - - var searchView:SearchView? { - if let item = genericView.item(stableId: NotificationSettingsEntry.searchField.stableId), let view = genericView.viewNecessary(at: item.index) as? SearchRowView { - return view.searchView - } - return nil - } - - override func escapeKeyAction() -> KeyHandlerResult { - if let item = genericView.item(stableId: NotificationSettingsEntry.searchField.stableId), let view = genericView.viewNecessary(at: item.index) as? SearchRowView { - if view.searchView.state == .Focus { - return view.searchView.changeResponder() ? .invoked : .rejected - } - } - return .rejected - } - - deinit { - disposable.dispose() - notificationsDisposable.dispose() - } - -} diff --git a/Telegram-Mac/NumberSelectorController.swift b/Telegram-Mac/NumberSelectorController.swift new file mode 100644 index 0000000000..f00412c700 --- /dev/null +++ b/Telegram-Mac/NumberSelectorController.swift @@ -0,0 +1,117 @@ +// +// NumberSelectorController.swift +// Telegram +// +// Created by Mikhail Filimonov on 14.01.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit + +private struct NumberValueState : Equatable { + var value: Int? +} + +private let _id_input = InputDataIdentifier("_id_input") + +private func entries(_ state: NumberValueState, placeholder: String) -> [InputDataEntry] { + + var entries: [InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + //4 294 967 295 + let formatter = Formatter.withSeparator + + let formatted: String + if let value = state.value { + formatted = formatter.string(from: NSNumber(value: value)) ?? "\(value)" + } else { + formatted = "" + } + entries.append(.input(sectionId: sectionId, index: index, value: .string(formatted), error: nil, identifier: _id_input, mode: .plain, data: InputDataRowData(viewType: .singleItem), placeholder: nil, inputPlaceholder: placeholder, filter: { value in + return value.trimmingCharacters(in: CharacterSet(charactersIn: "1234567890, .").inverted) + }, limit: 5)) + index += 1 + + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func NumberSelectorController(base: Int?, title: String, placeholder: String, okTitle: String, updated: @escaping(Int?)->Void) -> InputDataModalController { + + let initialState = NumberValueState(value: base) + let statePromise = ValuePromise(initialState, ignoreRepeated: false) + let stateValue = Atomic(value: initialState) + let updateState: ((NumberValueState) -> NumberValueState) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let disposable = MetaDisposable() + + var close: (() -> Void)? = nil + + let signal = statePromise.get() |> map { state in + return InputDataSignalValue(entries: entries(state, placeholder: placeholder)) + } + + let controller = InputDataController(dataSignal: signal, title: title, validateData: { data in + return .none + }, updateDatas: { data in + updateState { current in + var current = current + if let value = data[_id_input]?.stringValue { + let value = value + .replacingOccurrences(of: " ", with: "") + .replacingOccurrences(of: ",", with: "") + .replacingOccurrences(of: ".", with: "") + if let intValue = Int(value) { + current.value = max(1, intValue) + } else { + current.value = nil + } + } else { + current.value = nil + } + return current + } + return .none + }, afterDisappear: { + disposable.dispose() + }, hasDone: true) + + controller.validateData = { data in + updated(stateValue.with { $0.value }) + close?() + return .none + } + + controller.getBackgroundColor = { + theme.colors.listBackground + } + + let modalInteractions = ModalInteractions(acceptTitle: okTitle, accept: { [weak controller] in + _ = controller?.returnKeyAction() + }, drawBorder: true, height: 50, singleButton: true) + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) + close = { [weak modalController] in + modalController?.modal?.close() + } + return modalController + +} diff --git a/Telegram-Mac/ObjcUtils.h b/Telegram-Mac/ObjcUtils.h index 965a2508ad..5cc5b3e7d8 100644 --- a/Telegram-Mac/ObjcUtils.h +++ b/Telegram-Mac/ObjcUtils.h @@ -9,39 +9,60 @@ #import #import #import + + + +@interface OpenWithObject : NSObject +@property (nonatomic, strong,readonly) NSString *fullname; +@property (nonatomic, strong,readonly) NSURL *app; +@property (nonatomic, strong,readonly) NSImage *icon; + + +-(id)initWithFullname:(NSString *)fullname app:(NSURL *)app icon:(NSImage *)icon; + +@end + + + @interface ObjcUtils : NSObject -+ (NSArray *)textCheckingResultsForText:(NSString *)text highlightMentionsAndTags:(bool)highlightMentionsAndTags highlightCommands:(bool)highlightCommands; -+(NSString *) md5:(NSString *)string; -+ (NSArray *)findElementsByClass:(NSString *)className inView:(NSView *)view; -+ (NSString *)stringForEmojiHashOfData:(NSData *)data count:(NSInteger)count positionExtractor:(int32_t (^)(uint8_t *, int32_t, int32_t))positionExtractor; ++ (NSData *)dataFromHexString:(NSString *)string; ++ (NSArray *)textCheckingResultsForText:(NSString *)text highlightMentions:(bool)highlightMentions highlightTags:(bool)highlightTags highlightCommands:(bool)highlightCommands dotInMention:(bool)dotInMention; ++(NSString * __nonnull) md5:(NSString *__nonnull)string; ++(NSArray *__nonnull)findElementsByClass:(NSString *__nonnull)className inView:(NSView *__nonnull)view; ++(NSString * __nonnull)stringForEmojiHashOfData:(NSData *__nonnull)data count:(NSInteger)count positionExtractor:(int32_t (^__nonnull)(uint8_t *__nonnull, int32_t, int32_t))positionExtractor; +(NSArray *)bufferList:(CMSampleBufferRef)sampleBuffer; -+(NSString *)callEmojies:(NSData *)keySha256; -+ (NSArray *)getEmojiFromString:(NSString *)string; -+(NSOpenPanel *)openPanel; -+(NSSavePanel *)savePanel; -+(NSEvent *)scrollEvent:(NSEvent *)from; -+(NSSize)gifDimensionSize:(NSString *)path; ++(NSString * __nonnull)callEmojies:(NSData *__nonnull)keySha256; ++ (NSArray * __nonnull)getEmojiFromString:(NSString * __nonnull)string; ++(NSOpenPanel * __nonnull)openPanel; ++(NSSavePanel * __nonnull)savePanel; ++(NSEvent * __nonnull)scrollEvent:(NSEvent *__nonnull)from; ++(NSSize)gifDimensionSize:(NSString * __nonnull)path; +(int)colorMask:(int)idValue mainId:(int)mainId; -+(NSArray *)notificationTones:(NSString *)def; -+(NSString *)youtubeIdentifier:(NSString *)url; -@end ++(NSArray * __nonnull)notificationTones:(NSString * __nonnull)def; ++(NSString * __nullable)youtubeIdentifier:(NSString * __nonnull)url;; ++ (NSString * __nullable)_youtubeVideoIdFromText:(NSString * __nullable)text originalUrl:(NSString * __nullable)originalUrl startTime:(NSTimeInterval *)startTime; ++(NSArray *)appsForFileUrl:(NSString *)fileUrl; -@interface NSFileManager (Extension) -+ (NSString *)xattrStringValueForKey:(NSString *)key atURL:(NSURL *)URL; -+ (BOOL)setXAttrStringValue:(NSString *)value forKey:(NSString *)key atURL:(NSURL *)URL; + ++(NSCursor * __nullable)windowResizeNorthWestSouthEastCursor; ++(NSCursor * __nullable)windowResizeNorthEastSouthWestCursor; + @end + + + @interface NSMutableAttributedString(Extension) -(void)detectBoldColorInStringWithFont:(NSFont *)font; @end -NSArray *cut_long_message(NSString *message, int max_length); +NSArray * __nonnull cut_long_message(NSString *message, int max_length); int64_t SystemIdleTime(void); -NSDictionary *audioTags(AVURLAsset *asset); -NSImage *TGIdenticonImage(NSData *data, NSData *additionalData, CGSize size); +NSDictionary * __nonnull audioTags(AVURLAsset *asset); +NSImage * __nonnull TGIdenticonImage(NSData *data, NSData *additionalData, CGSize size); @interface NSData (TG) -- (NSString *)stringByEncodingInHex; +- (NSString * __nonnull)stringByEncodingInHex; @end BOOL isEnterAccessObjc(NSEvent *theEvent, BOOL byCmdEnter); @@ -50,3 +71,8 @@ BOOL isEnterEventObjc(NSEvent *theEvent); int colorIndexForGroupId(int64_t groupId); int64_t TGPeerIdFromChannelId(int32_t channelId); int colorIndexForUid(int32_t uid, int32_t myUserId); + + +NSArray * __nonnull currentAppInputSource(); +NSEvent * __nullable createScrollWheelEvent(); +double mappingRange(double x, double in_min, double in_max, double out_min, double out_max); diff --git a/Telegram-Mac/ObjcUtils.m b/Telegram-Mac/ObjcUtils.m index 7b55267ae4..d26c174562 100644 --- a/Telegram-Mac/ObjcUtils.m +++ b/Telegram-Mac/ObjcUtils.m @@ -9,10 +9,57 @@ #import "ObjcUtils.h" #import #import +#import +@implementation OpenWithObject + +-(id)initWithFullname:(NSString *)fullname app:(NSURL *)app icon:(NSImage *)icon { + if(self = [super init]) { + _fullname = fullname; + _app = app; + _icon = icon; + } + + return self; +} + + +@end @implementation ObjcUtils -+ (NSArray *)textCheckingResultsForText:(NSString *)text highlightMentionsAndTags:(bool)highlightMentionsAndTags highlightCommands:(bool)highlightCommands + + ++(NSCursor * __nullable)windowResizeNorthWestSouthEastCursor { + NSCursor *cursor = [[NSCursor class] performSelector:@selector(_windowResizeNorthWestSouthEastCursor)]; + return cursor; +} + ++(NSCursor * __nullable)windowResizeNorthEastSouthWestCursor { + NSCursor *cursor = [[NSCursor class] performSelector:@selector(_windowResizeNorthEastSouthWestCursor)]; + return cursor; +} + ++ (NSData *)dataFromHexString:(NSString *)string +{ + string = [string lowercaseString]; + NSMutableData *data= [NSMutableData new]; + unsigned char whole_byte; + char byte_chars[3] = {'\0','\0','\0'}; + int i = 0; + int length = string.length; + while (i < length-1) { + char c = [string characterAtIndex:i++]; + if (c < '0' || (c > '9' && c < 'a') || c > 'f') + continue; + byte_chars[0] = c; + byte_chars[1] = [string characterAtIndex:i++]; + whole_byte = strtol(byte_chars, NULL, 16); + [data appendBytes:&whole_byte length:1]; + } + return data; +} + ++ (NSArray *)textCheckingResultsForText:(NSString *)text highlightMentions:(bool)highlightMentions highlightTags:(bool)highlightTags highlightCommands:(bool)highlightCommands dotInMention:(bool)dotInMention { bool containsSomething = false; @@ -31,7 +78,12 @@ + (NSArray *)textCheckingResultsForText:(NSString *)text highlightMentionsAndTag { unichar c = characterAtIndexImp(text, sel, i); - if (highlightMentionsAndTags && (c == '@' || c == '#')) + if (highlightMentions && (c == '@')) + { + containsSomething = true; + break; + } + if (highlightTags && (c == '#')) { containsSomething = true; break; @@ -111,7 +163,8 @@ + (NSArray *)textCheckingResultsForText:(NSString *)text highlightMentionsAndTag { @try { NSTextCheckingType type = [match resultType]; - if (type == NSTextCheckingTypeLink || type == NSTextCheckingTypePhoneNumber) + NSString *scheme = [[[match URL] scheme] lowercaseString]; + if ((type == NSTextCheckingTypeLink || type == NSTextCheckingTypePhoneNumber) && ([scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"] || [scheme isEqualToString:@"ftp"] || [scheme isEqualToString:@"tg"] || [scheme isEqualToString:@"ton"] || scheme == nil)) { [results addObject:[NSValue valueWithRange:match.range]]; } @@ -132,7 +185,7 @@ + (NSArray *)textCheckingResultsForText:(NSString *)text highlightMentionsAndTag characterSet = [NSCharacterSet alphanumericCharacterSet]; }); - if (containsSomething && (highlightMentionsAndTags || highlightCommands)) + if (containsSomething && (highlightMentions || highlightTags || highlightCommands)) { int mentionStart = -1; int hashtagStart = -1; @@ -142,11 +195,11 @@ + (NSArray *)textCheckingResultsForText:(NSString *)text highlightMentionsAndTag for (int i = 0; i < length; i++) { unichar c = characterAtIndexImp(text, sel, i); - if (highlightMentionsAndTags && commandStart == -1) + if ((highlightMentions || highlightTags) && commandStart == -1) { if (mentionStart != -1) { - if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_')) + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c == '_' || (dotInMention && c == '.')))) { if (i > mentionStart + 1) { @@ -238,6 +291,218 @@ + (NSArray *)textCheckingResultsForText:(NSString *)text highlightMentionsAndTag return nil; } ++ (NSString *)_youtubeVideoIdFromText:(NSString *)text originalUrl:(NSString *)originalUrl startTime:(NSTimeInterval *)startTime { + if ([text hasPrefix:@"http://www.youtube.com/watch?v="] || [text hasPrefix:@"https://www.youtube.com/watch?v="] || [text hasPrefix:@"http://m.youtube.com/watch?v="] || [text hasPrefix:@"https://m.youtube.com/watch?v="]) + { + NSRange range1 = [text rangeOfString:@"?v="]; + bool match = true; + for (NSInteger i = range1.location + range1.length; i < (NSInteger)text.length; i++) + { + unichar c = [text characterAtIndex:i]; + if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '=' || c == '&' || c == '#')) + { + match = false; + break; + } + } + + if (match) + { + NSString *videoId = nil; + NSRange ampRange = [text rangeOfString:@"&"]; + NSRange hashRange = [text rangeOfString:@"#"]; + if (ampRange.location != NSNotFound || hashRange.location != NSNotFound) + { + NSInteger location = MIN(ampRange.location, hashRange.location); + videoId = [text substringWithRange:NSMakeRange(range1.location + range1.length, location - range1.location - range1.length)]; + } + else + videoId = [text substringFromIndex:range1.location + range1.length]; + + if (videoId.length != 0) + return videoId; + } + } + else if ([text hasPrefix:@"http://youtu.be/"] || [text hasPrefix:@"https://youtu.be/"] || [text hasPrefix:@"http://www.youtube.com/embed/"] || [text hasPrefix:@"https://www.youtube.com/embed/"]) + { + NSString *suffix = @""; + + NSMutableArray *prefixes = [NSMutableArray arrayWithArray:@ + [ + @"http://youtu.be/", + @"https://youtu.be/", + @"http://www.youtube.com/embed/", + @"https://www.youtube.com/embed/" + ]]; + + while (suffix.length == 0 && prefixes.count > 0) + { + NSString *prefix = prefixes.firstObject; + if ([text hasPrefix:prefix]) + { + suffix = [text substringFromIndex:prefix.length]; + break; + } + else + { + [prefixes removeObjectAtIndex:0]; + } + } + + NSString *queryString = nil; + for (int i = 0; i < (int)suffix.length; i++) + { + unichar c = [suffix characterAtIndex:i]; + if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '=' || c == '&' || c == '#')) + { + if (c == '?') + { + queryString = [suffix substringFromIndex:i + 1]; + suffix = [suffix substringToIndex:i]; + break; + } + else + { + return nil; + } + } + } + + if (startTime != NULL) + { + NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + NSString *queryString = [NSURL URLWithString:originalUrl].query; + for (NSString *param in [queryString componentsSeparatedByString:@"&"]) + { + NSArray *components = [param componentsSeparatedByString:@"="]; + if (components.count < 2) + continue; + [params setObject:components.lastObject forKey:components.firstObject]; + } + + NSString *timeParam = params[@"t"]; + if (timeParam != nil) + { + NSTimeInterval position = 0.0; + if ([timeParam rangeOfString:@"s"].location != NSNotFound) + { + NSString *value; + NSUInteger location = 0; + for (NSUInteger i = 0; i < timeParam.length; i++) + { + unichar c = [timeParam characterAtIndex:i]; + if ((c < '0' || c > '9')) + { + value = [timeParam substringWithRange:NSMakeRange(location, i - location)]; + location = i + 1; + switch (c) + { + case 's': + position += value.doubleValue; + break; + + case 'm': + position += value.doubleValue * 60.0; + break; + + case 'h': + position += value.doubleValue * 3600.0; + break; + + default: + break; + } + } + } + } + else + { + position = timeParam.doubleValue; + } + + *startTime = position; + } + } + + return suffix; + } + + return nil; +} + ++ (void) fillAppByUrl:(NSURL*)url bundle:(NSString**)bundle name:(NSString**)name version:(NSString**)version icon:(NSImage**)icon { + NSBundle *b = [NSBundle bundleWithURL:url]; + if (b) { + NSString *path = [url path]; + *name = [[NSFileManager defaultManager] displayNameAtPath: path]; + if (!*name) *name = (NSString*)[b objectForInfoDictionaryKey:@"CFBundleDisplayName"]; + if (!*name) *name = (NSString*)[b objectForInfoDictionaryKey:@"CFBundleName"]; + if (*name) { + *bundle = [b bundleIdentifier]; + if (bundle) { + *version = (NSString*)[b objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; + *icon = [[NSWorkspace sharedWorkspace] iconForFile: path]; + if (*icon && [*icon isValid]) [*icon setSize: CGSizeMake(16., 16.)]; + return; + } + } + } + *bundle = *name = *version = nil; + *icon = nil; +} + ++(NSArray *)appsForFileUrl:(NSString *)fileUrl { + + NSArray *appsList = (__bridge NSArray*)LSCopyApplicationURLsForURL((__bridge CFURLRef)[NSURL fileURLWithPath:fileUrl], kLSRolesAll); + NSMutableDictionary *data = [NSMutableDictionary dictionaryWithCapacity:16]; + int fullcount = 0; + for (id app in appsList) { + if (fullcount > 15) break; + + NSString *bundle = nil, *name = nil, *version = nil; + NSImage *icon = nil; + [ObjcUtils fillAppByUrl:(NSURL*)app bundle:&bundle name:&name version:&version icon:&icon]; + if (bundle && name) { + NSString *key = [[NSArray arrayWithObjects:bundle, name, nil] componentsJoinedByString:@"|"]; + if (!version) version = @""; + + NSMutableDictionary *versions = (NSMutableDictionary*)[data objectForKey:key]; + if (!versions) { + versions = [NSMutableDictionary dictionaryWithCapacity:2]; + [data setValue:versions forKey:key]; + } + if (![versions objectForKey:version]) { + [versions setValue:[NSArray arrayWithObjects:name, icon, app, nil] forKey:version]; + ++fullcount; + } + } + } + + + NSMutableArray *apps = [NSMutableArray arrayWithCapacity:fullcount]; + for (id key in data) { + NSMutableDictionary *val = (NSMutableDictionary*)[data objectForKey:key]; + for (id ver in val) { + NSArray *app = (NSArray*)[val objectForKey:ver]; + + NSString *fullname = (NSString*)[app objectAtIndex:0], *version = (NSString*)ver; + BOOL showVersion = ([val count] > 1); + if (!showVersion) { + NSError *error = NULL; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^\\d+\\.\\d+\\.\\d+(\\.\\d+)?$" options:NSRegularExpressionCaseInsensitive error:&error]; + showVersion = ![regex numberOfMatchesInString:version options:NSMatchingWithoutAnchoringBounds range:NSMakeRange(0,[version length])]; + } + if (showVersion) fullname = [[NSArray arrayWithObjects:fullname, @" (", version, @")", nil] componentsJoinedByString:@""]; + OpenWithObject *a = [[OpenWithObject alloc] initWithFullname:fullname app:app[2] icon:app[1]]; + + [apps addObject:a]; + } + } + + + return apps; +} + + (NSArray *)getEmojiFromString:(NSString *)string { __block NSMutableDictionary *temp = [NSMutableDictionary dictionary]; @@ -333,7 +598,7 @@ + (NSArray *)textCheckingResultsForText:(NSString *)text highlightMentionsAndTag // MTLog(@"viewC.className %@ %@", viewC.className, className); - if([viewC.className isEqualToString:className]) { + if([viewC respondsToSelector:@selector(className)] && [viewC.className isEqualToString:className]) { [array addObject:viewC]; } @@ -343,6 +608,8 @@ + (NSArray *)textCheckingResultsForText:(NSString *)text highlightMentionsAndTag } return array; } + + +(NSString *) md5:(NSString *)string { @@ -693,7 +960,8 @@ static int32_t get_bits(uint8_t const *bytes, unsigned int bitOffset, unsigned i [list addObject:@"NotificationSettingsToneNone"]; - NSArray *dirContents = [fm contentsOfDirectoryAtPath:@"~/Library/Sounds" error:nil]; + NSString *homeSoundsPath = [NSHomeDirectory() stringByAppendingString:@"/Library/Sounds"]; + NSArray *dirContents = [fm contentsOfDirectoryAtPath:homeSoundsPath error:nil]; [list addObjectsFromArray:[dirContents filteredArrayUsingPredicate:fltr]]; dirContents = [fm contentsOfDirectoryAtPath:@"/Library/Sounds" error:nil]; @@ -865,20 +1133,44 @@ -(void)detectBoldColorInStringWithFont:(NSFont *)font string:(NSString *)string return index; }; - NSArray *csets = @[[NSCharacterSet newlineCharacterSet],[NSCharacterSet characterSetWithCharactersInString:@"."],[NSCharacterSet whitespaceCharacterSet]]; + + NSUInteger (^giveupString)(NSString *symbol) = ^NSUInteger(NSString *symbol) { + + NSUInteger index = NSNotFound; + + for (int j = (int)substring.length ; j > 0; j --) { + + if([[substring substringWithRange:NSMakeRange(MAX(0, j-symbol.length), symbol.length)] rangeOfString:symbol].location != NSNotFound) { + index = j; + break; + } + } + + return index; + }; + + + NSArray *csets = @[[NSCharacterSet newlineCharacterSet],[NSCharacterSet whitespaceCharacterSet]]; NSUInteger index = substring.length; if(index + inc > message.length) { - for (NSCharacterSet *set in csets) { - NSUInteger idx = giveup(set); - if(idx != NSNotFound) { - index = idx; - break; + NSUInteger idx = giveupString(@"\n\n"); + if (idx != NSNotFound) { + index = idx; + } else { + for (NSCharacterSet *set in csets) { + NSUInteger idx = giveup(set); + if(idx != NSNotFound) { + index = idx; + break; + } } } + + } substring = [substring substringWithRange:NSMakeRange(0, index)]; @@ -947,7 +1239,6 @@ int64_t SystemIdleTime(void) { NSArray *metadata = [asset metadataForFormat:obj]; for (AVMutableMetadataItem *metaItem in metadata) { - NSLog(@"%@ : %@", metaItem.identifier, (NSString *)metaItem.value); if([metaItem.identifier isEqualToString:AVMetadataIdentifierID3MetadataLeadPerformer]) { artistName = (NSString *) metaItem.value; } else if([metaItem.identifier isEqualToString:AVMetadataIdentifierID3MetadataTitleDescription]) { @@ -1015,7 +1306,7 @@ BOOL isEnterEventObjc(NSEvent *theEvent) { BOOL isEnterAccessObjc(NSEvent *theEvent, BOOL byCmdEnter) { if(isEnterEventObjc(theEvent)) { NSUInteger flags = (theEvent.modifierFlags & NSDeviceIndependentModifierFlagsMask); - return !byCmdEnter ? flags == 0 || flags == 65536 : (theEvent.modifierFlags & NSCommandKeyMask) > 0; + return !byCmdEnter ? flags == 0 || flags == 65536 || flags == 2097152 : (theEvent.modifierFlags & NSCommandKeyMask) > 0; } return NO; } @@ -1054,3 +1345,36 @@ inline int colorIndexForGroupId(int64_t groupId) return colorIndex; } +/* + Sorry guys there was a code which caused a crash on text input + */ + +NSArray * __nonnull currentAppInputSource() +{ + + CFArrayRef inputSourcesList = TISCreateInputSourceList(NULL, false); + + CFIndex inputSourcesCount = CFArrayGetCount(inputSourcesList); + + NSMutableArray *inputs = [[NSMutableArray alloc] init]; + + for (int i = 0; i < inputSourcesCount; i++) { + NSArray* list = (__bridge NSArray *)(TISGetInputSourceProperty(CFArrayGetValueAtIndex(inputSourcesList, i), kTISPropertyInputSourceLanguages)); + if ([list count] > 0 && list[0] != nil) { + [inputs addObject:list[0]]; + } + + } + + return inputs; +} + +NSEvent * __nullable createScrollWheelEvent() { + CGEventRef upEvent = CGEventCreateScrollWheelEvent( + NULL, + kCGScrollEventUnitPixel, + 1, 0, 0 ); + NSEvent *event = [NSEvent eventWithCGEvent:upEvent]; + CFRelease(upEvent); + return event; +} diff --git a/Telegram-Mac/OngoingCallConnectionDescription.h b/Telegram-Mac/OngoingCallConnectionDescription.h new file mode 100644 index 0000000000..e44f7eedb7 --- /dev/null +++ b/Telegram-Mac/OngoingCallConnectionDescription.h @@ -0,0 +1,13 @@ +// +// OngoingCallConnectionDescription.h +// Telegram +// +// Created by keepcoder on 03/05/2017. +// Copyright © 2017 Telegram. All rights reserved. +// + +#import + + + + diff --git a/Telegram-Mac/OngoingCallConnectionDescription.m b/Telegram-Mac/OngoingCallConnectionDescription.m new file mode 100644 index 0000000000..2df95a874e --- /dev/null +++ b/Telegram-Mac/OngoingCallConnectionDescription.m @@ -0,0 +1,10 @@ +// +// OngoingCallConnectionDescription.m +// Telegram +// +// Created by keepcoder on 03/05/2017. +// Copyright © 2017 Telegram. All rights reserved. +// + +#import "OngoingCallConnectionDescription.h" + diff --git a/Telegram-Mac/OngoingCallContext.swift b/Telegram-Mac/OngoingCallContext.swift new file mode 100644 index 0000000000..e323b9f7b5 --- /dev/null +++ b/Telegram-Mac/OngoingCallContext.swift @@ -0,0 +1,860 @@ +// +// OngoingCallContext.swift +// Telegram +// +// Created by Mikhail Filimonov on 23/06/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import Foundation +import SwiftSignalKit +import TelegramCore + +import Postbox +import TgVoipWebrtc +import TGUIKit +import TelegramVoip + +private func callConnectionDescription(_ connection: CallSessionConnection) -> OngoingCallConnectionDescription? { + switch connection { + case let .reflector(reflector): + return OngoingCallConnectionDescription(connectionId: reflector.id, ip: reflector.ip, ipv6: reflector.ipv6, port: reflector.port, peerTag: reflector.peerTag) + case .webRtcReflector: + return nil + } +} + +private func callConnectionDescriptionWebrtc(_ connection: CallSessionConnection) -> OngoingCallConnectionDescriptionWebrtc? { + switch connection { + case .reflector: + return nil + case let .webRtcReflector(reflector): + return OngoingCallConnectionDescriptionWebrtc(connectionId: reflector.id, hasStun: reflector.hasStun, hasTurn: reflector.hasTurn, ip: reflector.ip.isEmpty ? reflector.ipv6 : reflector.ip, port: reflector.port, username: reflector.username, password: reflector.password) + } +} + + +/*private func callConnectionDescriptionWebrtcCustom(_ connection: CallSessionConnection) -> OngoingCallConnectionDescriptionWebrtcCustom { + return OngoingCallConnectionDescriptionWebrtcCustom(connectionId: connection.id, ip: connection.ip, ipv6: connection.ipv6, port: connection.port, peerTag: connection.peerTag) + }*/ + +private let callLogsLimit = 20 + +func callLogNameForId(id: Int64, account: Account) -> String? { + let path = callLogsPath(account: account) + let namePrefix = "\(id)_" + + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: path), includingPropertiesForKeys: [], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { + for url in enumerator { + if let url = url as? URL { + if url.lastPathComponent.hasPrefix(namePrefix) { + return url.lastPathComponent + } + } + } + } + return nil +} + +func callLogsPath(account: Account) -> String { + return account.basePath + "/calls" +} + +private func cleanupCallLogs(account: Account) { + let path = callLogsPath(account: account) + let fileManager = FileManager.default + if !fileManager.fileExists(atPath: path, isDirectory: nil) { + try? fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) + } + + var oldest: (URL, Date)? = nil + var count = 0 + if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: path), includingPropertiesForKeys: [.contentModificationDateKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { + for url in enumerator { + if let url = url as? URL { + if let date = (try? url.resourceValues(forKeys: Set([.contentModificationDateKey])))?.contentModificationDate { + if let currentOldest = oldest { + if date < currentOldest.1 { + oldest = (url, date) + } + } else { + oldest = (url, date) + } + count += 1 + } + } + } + } + if count > callLogsLimit, let oldest = oldest { + try? fileManager.removeItem(atPath: oldest.0.path) + } +} + +private let setupLogs: Bool = { + OngoingCallThreadLocalContext.setupLoggingFunction({ value in + if let value = value { + Logger.shared.log("TGVOIP", value) + } + }) + OngoingCallThreadLocalContextWebrtc.setupLoggingFunction({ value in + if let value = value { + Logger.shared.log("TGVOIP", value) + } + }) + /*OngoingCallThreadLocalContextWebrtcCustom.setupLoggingFunction({ value in + if let value = value { + Logger.shared.log("TGVOIP", value) + } + })*/ + return true +}() + +public struct OngoingCallContextState: Equatable { + public enum State { + case initializing + case connected + case reconnecting + case failed + } + + public enum VideoState: Equatable { + case notAvailable + case inactive + case active + case paused + } + + public enum RemoteVideoState: Equatable { + case inactive + case active + case paused + } + + public enum RemoteAudioState: Equatable { + case active + case muted + } + + public enum RemoteBatteryLevel: Equatable { + case normal + case low + } + + public let state: State + public let videoState: VideoState + public let remoteVideoState: RemoteVideoState + public let remoteAudioState: RemoteAudioState + public let remoteBatteryLevel: RemoteBatteryLevel + public let remoteAspectRatio: Float +} + + +private final class OngoingCallThreadLocalContextQueueImpl: NSObject, OngoingCallThreadLocalContextQueue, OngoingCallThreadLocalContextQueueWebrtc /*, OngoingCallThreadLocalContextQueueWebrtcCustom*/ { + private let queue: Queue + + init(queue: Queue) { + self.queue = queue + + super.init() + } + + func dispatch(_ f: @escaping () -> Void) { + self.queue.async { + f() + } + } + + func dispatch(after seconds: Double, block f: @escaping () -> Void) { + self.queue.after(seconds, f) + } + + func isCurrent() -> Bool { + return self.queue.isCurrent() + } +} + +private func ongoingNetworkTypeForType(_ type: NetworkType) -> OngoingCallNetworkType { + switch type { + case .none: + return .wifi + case .wifi: + return .wifi + } +} + +private func ongoingNetworkTypeForTypeWebrtc(_ type: NetworkType) -> OngoingCallNetworkTypeWebrtc { + switch type { + case .none: + return .wifi + case .wifi: + return .wifi + } +} + +/*private func ongoingNetworkTypeForTypeWebrtcCustom(_ type: NetworkType) -> OngoingCallNetworkTypeWebrtcCustom { + switch type { + case .none: + return .wifi + case .wifi: + return .wifi + case let .cellular(cellular): + switch cellular { + case .edge: + return .cellularEdge + case .gprs: + return .cellularGprs + case .thirdG, .unknown: + return .cellular3g + case .lte: + return .cellularLte + } + } + }*/ + +private func ongoingDataSavingForType(_ type: VoiceCallDataSaving) -> OngoingCallDataSaving { + switch type { + case .never: + return .never + case .cellular: + return .cellular + case .always: + return .always + default: + return .never + } +} + +private func ongoingDataSavingForTypeWebrtc(_ type: VoiceCallDataSaving) -> OngoingCallDataSavingWebrtc { + switch type { + case .never: + return .never + case .cellular: + return .cellular + case .always: + return .always + default: + return .never + } +} + + +private protocol OngoingCallThreadLocalContextProtocol: class { + func nativeSetNetworkType(_ type: NetworkType) + func nativeSetIsMuted(_ value: Bool) + func nativeSetIsLowBatteryLevel(_ value: Bool) + func nativeRequestVideo(_ capturer: OngoingCallVideoCapturer) + func nativeSetRequestedVideoAspect(_ aspect: Float) + func nativeDisableVideo() + func nativeStop(_ completion: @escaping (String?, Int64, Int64, Int64, Int64) -> Void) + func nativeBeginTermination() + func nativeDebugInfo() -> String + func nativeVersion() -> String + func nativeGetDerivedState() -> Data + func nativeSwitchAudioOutput(_ deviceId: String) + func nativeSwitchAudioInput(_ deviceId: String) +} + + +private final class OngoingCallThreadLocalContextHolder { + let context: OngoingCallThreadLocalContextProtocol + + init(_ context: OngoingCallThreadLocalContextProtocol) { + self.context = context + } +} + +extension OngoingCallThreadLocalContext: OngoingCallThreadLocalContextProtocol { + + + + + func nativeSetNetworkType(_ type: NetworkType) { + self.setNetworkType(ongoingNetworkTypeForType(type)) + } + + func nativeStop(_ completion: @escaping (String?, Int64, Int64, Int64, Int64) -> Void) { + self.stop(completion) + } + + func nativeBeginTermination() { + } + + func nativeSetIsMuted(_ value: Bool) { + self.setIsMuted(value) + } + + func nativeRequestVideo(_ capturer: OngoingCallVideoCapturer) { + } + func nativeSwitchAudioOutput(_ deviceId: String) { + + } + func nativeSwitchAudioInput(_ deviceId: String) { + + } + func nativeAcceptVideo(_ capturer: OngoingCallVideoCapturer) { + } + func nativeSetRequestedVideoAspect(_ aspect: Float) { + + } + + func nativeSetIsLowBatteryLevel(_ value: Bool) { + } + + func nativeDisableVideo() { + } + + func nativeSwitchVideoCamera() { + } + + func nativeswitchAudioOutput() { + + } + + func nativeDebugInfo() -> String { + return self.debugInfo() ?? "" + } + + func nativeVersion() -> String { + return self.version() ?? "" + } + + func nativeGetDerivedState() -> Data { + return self.getDerivedState() + } +} + +extension OngoingCallThreadLocalContextWebrtc: OngoingCallThreadLocalContextProtocol { + func nativeSetNetworkType(_ type: NetworkType) { + self.setNetworkType(ongoingNetworkTypeForTypeWebrtc(type)) + } + + func nativeStop(_ completion: @escaping (String?, Int64, Int64, Int64, Int64) -> Void) { + self.stop(completion) + } + + func nativeBeginTermination() { + self.beginTermination() + } + + func nativeSetIsMuted(_ value: Bool) { + self.setIsMuted(value) + } + + func nativeSetIsLowBatteryLevel(_ value: Bool) { + self.setIsLowBatteryLevel(value) + } + + func nativeSwitchAudioOutput(_ deviceId: String) { + self.switchAudioOutput(deviceId) + } + func nativeSwitchAudioInput(_ deviceId: String) { + self.switchAudioInput(deviceId) + } + func nativeRequestVideo(_ capturer: OngoingCallVideoCapturer) { + self.requestVideo(capturer.impl) + } + func nativeSetRequestedVideoAspect(_ aspect: Float) { + self.setRequestedVideoAspect(aspect) + } + + func nativeDisableVideo() { + self.disableVideo() + } + + func nativeDebugInfo() -> String { + return self.debugInfo() ?? "" + } + + func nativeVersion() -> String { + return self.version() ?? "" + } + + func nativeGetDerivedState() -> Data { + return self.getDerivedState() + } +} + + +private extension OngoingCallContextState.State { + init(_ state: OngoingCallState) { + switch state { + case .initializing: + self = .initializing + case .connected: + self = .connected + case .failed: + self = .failed + case .reconnecting: + self = .reconnecting + default: + self = .failed + } + } +} + +private extension OngoingCallContextState.State { + init(_ state: OngoingCallStateWebrtc) { + switch state { + case .initializing: + self = .initializing + case .connected: + self = .connected + case .failed: + self = .failed + case .reconnecting: + self = .reconnecting + default: + self = .failed + } + } +} + +/*private extension OngoingCallContextState { + init(_ state: OngoingCallStateWebrtcCustom) { + switch state { + case .initializing: + self = .initializing + case .connected: + self = .connected + case .failed: + self = .failed + case .reconnecting: + self = .reconnecting + default: + self = .failed + } + } + }*/ + +final class OngoingCallContext { + struct AuxiliaryServer { + enum Connection { + case stun + case turn(username: String, password: String) + } + + let host: String + let port: Int + let connection: Connection + + init( + host: String, + port: Int, + connection: Connection + ) { + self.host = host + self.port = port + self.connection = connection + } + } + + let internalId: CallSessionInternalId + + private let queue = Queue() + private let account: Account + private let callSessionManager: CallSessionManager + private let logPath: String + + private var contextRef: Unmanaged? + + private let contextState = Promise(nil) + var state: Signal { + return self.contextState.get() + } + + private var didReportCallAsVideo: Bool = false + + + private var signalingDataDisposable: Disposable? + + private let receptionPromise = Promise(nil) + var reception: Signal { + return self.receptionPromise.get() + } + + private let audioLevelPromise = Promise(0.0) + public var audioLevel: Signal { + return self.audioLevelPromise.get() + } + + + private let audioSessionDisposable = MetaDisposable() + private var networkTypeDisposable: Disposable? + + private let tempLogFile: TempBoxFile + private let tempStatsLogFile: TempBoxFile + + static var maxLayer: Int32 { + return max(OngoingCallThreadLocalContext.maxLayer(), OngoingCallThreadLocalContextWebrtc.maxLayer()) + } + + static func versions(includeExperimental: Bool, includeReference: Bool) -> [(version: String, supportsVideo: Bool)] { + var result: [(version: String, supportsVideo: Bool)] = [(OngoingCallThreadLocalContext.version(), false)] + if includeExperimental { + result.append(contentsOf: OngoingCallThreadLocalContextWebrtc.versions(withIncludeReference: includeReference).map { version -> (version: String, supportsVideo: Bool) in + return (version, true) + }) + } + return result + } + + + init(account: Account, callSessionManager: CallSessionManager, internalId: CallSessionInternalId, proxyServer: ProxyServerSettings?, initialNetworkType: NetworkType, updatedNetworkType: Signal, serializedData: String?, dataSaving: VoiceCallDataSaving, derivedState: VoipDerivedState, key: Data, isOutgoing: Bool, video: OngoingCallVideoCapturer?, connections: CallSessionConnectionSet, maxLayer: Int32, version: String, allowP2P: Bool, enableTCP: Bool, enableStunMarking: Bool, logName: String, preferredVideoCodec: String?, audioInputDeviceId: String?) { + let _ = setupLogs + OngoingCallThreadLocalContext.applyServerConfig(serializedData) + OngoingCallThreadLocalContextWebrtc.applyServerConfig(serializedData) + + self.internalId = internalId + self.account = account + self.callSessionManager = callSessionManager + self.logPath = logName.isEmpty ? "" : callLogsPath(account: self.account) + "/" + logName + ".log" + let logPath = self.logPath + self.tempLogFile = TempBox.shared.tempFile(fileName: "CallLog.txt") + let tempLogPath = self.tempLogFile.path + + + self.tempStatsLogFile = TempBox.shared.tempFile(fileName: "CallStats.json") + let tempStatsLogPath = self.tempStatsLogFile.path + + + let queue = self.queue + + cleanupCallLogs(account: account) + queue.sync { + if OngoingCallThreadLocalContextWebrtc.versions(withIncludeReference: true).contains(version) { + var voipProxyServer: VoipProxyServerWebrtc? + if let proxyServer = proxyServer { + switch proxyServer.connection { + case let .socks5(username, password): + voipProxyServer = VoipProxyServerWebrtc(host: proxyServer.host, port: proxyServer.port, username: username, password: password) + case .mtp: + break + } + } + + + let unfilteredConnections = [connections.primary] + connections.alternatives + var processedConnections: [CallSessionConnection] = [] + var filteredConnections: [OngoingCallConnectionDescriptionWebrtc] = [] + for connection in unfilteredConnections { + if processedConnections.contains(connection) { + continue + } + processedConnections.append(connection) + if let mapped = callConnectionDescriptionWebrtc(connection) { + if mapped.ip.isEmpty { + continue + } + filteredConnections.append(mapped) + } + } + + let context = OngoingCallThreadLocalContextWebrtc(version: version, queue: OngoingCallThreadLocalContextQueueImpl(queue: queue), proxy: voipProxyServer, networkType: ongoingNetworkTypeForTypeWebrtc(initialNetworkType), dataSaving: ongoingDataSavingForTypeWebrtc(dataSaving), derivedState: derivedState.data, key: key, isOutgoing: isOutgoing, connections: filteredConnections, maxLayer: maxLayer, allowP2P: allowP2P, allowTCP: enableTCP, enableStunMarking: enableStunMarking, logPath: tempLogPath, statsLogPath: tempStatsLogPath, sendSignalingData: { [weak callSessionManager] data in + callSessionManager?.sendSignalingData(internalId: internalId, data: data) + }, videoCapturer: video?.impl, preferredVideoCodec: preferredVideoCodec, audioInputDeviceId: audioInputDeviceId ?? "") + + + self.contextRef = Unmanaged.passRetained(OngoingCallThreadLocalContextHolder(context)) + context.stateChanged = { [weak self, weak callSessionManager] state, videoState, remoteVideoState, remoteAudioState, remoteBatteryLevel, remotePreferredAspectRatio in + queue.async { + guard let strongSelf = self else { + return + } + + let mappedState = OngoingCallContextState.State(state) + let mappedVideoState: OngoingCallContextState.VideoState + switch videoState { + case .inactive: + mappedVideoState = .inactive + case .active: + mappedVideoState = .active + case .paused: + mappedVideoState = .paused + @unknown default: + mappedVideoState = .notAvailable + } + let mappedRemoteVideoState: OngoingCallContextState.RemoteVideoState + switch remoteVideoState { + case .inactive: + mappedRemoteVideoState = .inactive + case .active: + mappedRemoteVideoState = .active + case .paused: + mappedRemoteVideoState = .paused + @unknown default: + mappedRemoteVideoState = .inactive + } + let mappedRemoteAudioState: OngoingCallContextState.RemoteAudioState + switch remoteAudioState { + case .active: + mappedRemoteAudioState = .active + case .muted: + mappedRemoteAudioState = .muted + @unknown default: + mappedRemoteAudioState = .active + } + let mappedRemoteBatteryLevel: OngoingCallContextState.RemoteBatteryLevel + switch remoteBatteryLevel { + case .normal: + mappedRemoteBatteryLevel = .normal + case .low: + mappedRemoteBatteryLevel = .low + @unknown default: + mappedRemoteBatteryLevel = .normal + } + if case .active = mappedVideoState, !strongSelf.didReportCallAsVideo { + strongSelf.didReportCallAsVideo = true + callSessionManager?.updateCallType(internalId: internalId, type: .video) + } + strongSelf.contextState.set(.single(OngoingCallContextState(state: mappedState, videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, remoteAudioState: mappedRemoteAudioState, remoteBatteryLevel: mappedRemoteBatteryLevel, remoteAspectRatio: remotePreferredAspectRatio))) + + } + } + self.receptionPromise.set(.single(4)) + context.signalBarsChanged = { [weak self] signalBars in + self?.receptionPromise.set(.single(signalBars)) + } + context.audioLevelUpdated = { [weak self] level in + self?.audioLevelPromise.set(.single(level)) + } + + + self.networkTypeDisposable = (updatedNetworkType + |> deliverOn(queue)).start(next: { [weak self] networkType in + self?.withContext { context in + context.nativeSetNetworkType(networkType) + } + }) + } else { + var voipProxyServer: VoipProxyServer? + if let proxyServer = proxyServer { + switch proxyServer.connection { + case let .socks5(username, password): + voipProxyServer = VoipProxyServer(host: proxyServer.host, port: proxyServer.port, username: username, password: password) + case .mtp: + break + } + } + let context = OngoingCallThreadLocalContext(queue: OngoingCallThreadLocalContextQueueImpl(queue: queue), proxy: voipProxyServer, networkType: ongoingNetworkTypeForType(initialNetworkType), dataSaving: ongoingDataSavingForType(dataSaving), derivedState: derivedState.data, key: key, isOutgoing: isOutgoing, primaryConnection: callConnectionDescription(connections.primary)!, alternativeConnections: connections.alternatives.compactMap(callConnectionDescription), maxLayer: maxLayer, allowP2P: allowP2P, logPath: logPath) + + + self.contextRef = Unmanaged.passRetained(OngoingCallThreadLocalContextHolder(context)) + context.stateChanged = { [weak self] state in + self?.contextState.set(.single(OngoingCallContextState(state: OngoingCallContextState.State(state), videoState: .notAvailable, remoteVideoState: .inactive, remoteAudioState: .active, remoteBatteryLevel: .normal, remoteAspectRatio: 0))) + } + context.signalBarsChanged = { [weak self] signalBars in + self?.receptionPromise.set(.single(signalBars)) + } + + self.networkTypeDisposable = (updatedNetworkType + |> deliverOn(queue)).start(next: { [weak self] networkType in + self?.withContext { context in + context.nativeSetNetworkType(networkType) + } + }) + } + } + + + + self.signalingDataDisposable = callSessionManager.beginReceivingCallSignalingData(internalId: internalId, { [weak self] dataList in + print("data received") + queue.async { + self?.withContext { context in + if let context = context as? OngoingCallThreadLocalContextWebrtc { + for data in dataList { + context.addSignaling(data) + } + } + } + } + }) + } + + deinit { + let contextRef = self.contextRef + self.queue.async { + contextRef?.release() + } + + self.audioSessionDisposable.dispose() + self.networkTypeDisposable?.dispose() + } + + private func withContext(_ f: @escaping (OngoingCallThreadLocalContextProtocol) -> Void) { + self.queue.async { + if let contextRef = self.contextRef { + let context = contextRef.takeUnretainedValue() + f(context.context) + } + } + } + + private func withContextThenDeallocate(_ f: @escaping (OngoingCallThreadLocalContextProtocol) -> Void) { + self.queue.async { + if let contextRef = self.contextRef { + let context = contextRef.takeUnretainedValue() + f(context.context) + + self.contextRef?.release() + self.contextRef = nil + } + } + } + + + func stop(callId: CallId? = nil, sendDebugLogs: Bool = false, debugLogValue: Promise) { + let account = self.account + let logPath = self.logPath + var statsLogPath = "" + if !logPath.isEmpty { + statsLogPath = logPath + ".json" + } + let tempLogPath = self.tempLogFile.path + let tempStatsLogPath = self.tempStatsLogFile.path + + self.withContextThenDeallocate { context in + context.nativeStop { debugLog, bytesSentWifi, bytesReceivedWifi, bytesSentMobile, bytesReceivedMobile in + let delta = NetworkUsageStatsConnectionsEntry( + cellular: NetworkUsageStatsDirectionsEntry( + incoming: bytesReceivedMobile, + outgoing: bytesSentMobile), + wifi: NetworkUsageStatsDirectionsEntry( + incoming: bytesReceivedWifi, + outgoing: bytesSentWifi)) + updateAccountNetworkUsageStats(account: self.account, category: .call, delta: delta) + + if !logPath.isEmpty { + let logsPath = callLogsPath(account: account) + let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil) + let _ = try? FileManager.default.moveItem(atPath: tempLogPath, toPath: logPath) + } + + if !statsLogPath.isEmpty { + let logsPath = callLogsPath(account: account) + let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil) + let _ = try? FileManager.default.moveItem(atPath: tempStatsLogPath, toPath: statsLogPath) + } + + if let callId = callId, !statsLogPath.isEmpty, let data = try? Data(contentsOf: URL(fileURLWithPath: statsLogPath)), let dataString = String(data: data, encoding: .utf8) { + debugLogValue.set(.single(dataString)) + if sendDebugLogs { +// let _ = saveCallDebugLog(network: self.account.network, callId: callId, log: dataString).start() + } + } + } + let derivedState = context.nativeGetDerivedState() + let _ = updateVoipDerivedStateInteractively(postbox: self.account.postbox, { _ in + return VoipDerivedState(data: derivedState) + }).start() + } + + } + + func setIsMuted(_ value: Bool) { + self.withContext { context in + context.nativeSetIsMuted(value) + } + } + + public func setIsLowBatteryLevel(_ value: Bool) { + self.withContext { context in + context.nativeSetIsLowBatteryLevel(value) + } + } + + public func requestVideo(_ capturer: OngoingCallVideoCapturer) { + self.withContext { context in + context.nativeRequestVideo(capturer) + } + } + + public func setRequestedVideoAspect(_ aspect: Float) { + self.withContext { context in + context.nativeSetRequestedVideoAspect(aspect) + } + } + + public func disableVideo() { + self.withContext { context in + context.nativeDisableVideo() + } + } + + public func switchAudioOutput(_ deviceId: String) { + self.withContext { context in + context.nativeSwitchAudioOutput(deviceId) + } + } + public func switchAudioInput(_ deviceId: String) { + self.withContext { context in + context.nativeSwitchAudioInput(deviceId) + } + } + func debugInfo() -> Signal<(String, String), NoError> { + let poll = Signal<(String, String), NoError> { subscriber in + self.withContext { context in + let version = context.nativeVersion() + let debugInfo = context.nativeDebugInfo() + subscriber.putNext((version, debugInfo)) + subscriber.putCompletion() + } + + return EmptyDisposable + } + return (poll |> then(.complete() |> delay(0.5, queue: Queue.concurrentDefaultQueue()))) |> restart + } + + func makeIncomingVideoView(completion: @escaping (OngoingCallContextPresentationCallVideoView?) -> Void) { + self.withContext { context in + if let context = context as? OngoingCallThreadLocalContextWebrtc { + context.makeIncomingVideoView { view in + if let view = view { + completion(OngoingCallContextPresentationCallVideoView( + view: view, + setOnFirstFrameReceived: { [weak view] f in + view?.setOnFirstFrameReceived(f) + }, + getOrientation: { [weak view] in + if let view = view { + return OngoingCallVideoOrientation(view.orientation) + } else { + return .rotation0 + } + }, + getAspect: { [weak view] in + if let view = view { + return view.aspect + } else { + return 0.0 + } + }, + setOnOrientationUpdated: { [weak view] f in + view?.setOnOrientationUpdated { value, aspect in + f?(OngoingCallVideoOrientation(value), aspect) + } + }, setVideoContentMode: { [weak view] mode in + view?.setVideoContentMode(mode) + }, setOnIsMirroredUpdated: { [weak view] f in + view?.setOnIsMirroredUpdated { value in + f?(value) + } + }, + setIsPaused: { [weak view] paused in + view?.setIsPaused(paused) + }, renderToSize: { [weak view] size, animated in + view?.render(to: size, animated: animated) + } + )) + } else { + completion(nil) + } + } + } else { + completion(nil) + } + } + } + +} diff --git a/Telegram-Mac/OngoingCallThreadLocalContext.h b/Telegram-Mac/OngoingCallThreadLocalContext.h new file mode 100644 index 0000000000..d350c12deb --- /dev/null +++ b/Telegram-Mac/OngoingCallThreadLocalContext.h @@ -0,0 +1,94 @@ +// +// CallsBridge.h +// Telegram +// +// Created by keepcoder on 03/05/2017. +// Copyright © 2017 Telegram. All rights reserved. +// + +#import +#import "OngoingCallConnectionDescription.h" + +@protocol OngoingCallThreadLocalContextQueue + +- (void)dispatch:(void (^ _Nonnull)())f; +- (bool)isCurrent; + +@end + + +@interface OngoingCallConnectionDescription : NSObject + +@property (nonatomic, readonly) int64_t connectionId; +@property (nonatomic, strong, readonly) NSString * _Nonnull ip; +@property (nonatomic, strong, readonly) NSString * _Nonnull ipv6; +@property (nonatomic, readonly) int32_t port; +@property (nonatomic, strong, readonly) NSData * _Nonnull peerTag; + +- (instancetype _Nonnull)initWithConnectionId:(int64_t)connectionId ip:(NSString * _Nonnull)ip ipv6:(NSString * _Nonnull)ipv6 port:(int32_t)port peerTag:(NSData * _Nonnull)peerTag; + +@end + +typedef NS_ENUM(int32_t, OngoingCallState) { + OngoingCallStateInitializing, + OngoingCallStateConnected, + OngoingCallStateFailed, + OngoingCallStateReconnecting +}; + +typedef NS_ENUM(int32_t, OngoingCallNetworkType) { + OngoingCallNetworkTypeWifi, + OngoingCallNetworkTypeCellularGprs, + OngoingCallNetworkTypeCellularEdge, + OngoingCallNetworkTypeCellular3g, + OngoingCallNetworkTypeCellularLte +}; + +typedef NS_ENUM(int32_t, OngoingCallDataSaving) { + OngoingCallDataSavingNever, + OngoingCallDataSavingCellular, + OngoingCallDataSavingAlways +}; + + + + +@interface VoipProxyServer : NSObject +@property(nonatomic, strong, readonly) NSString *host; +@property(nonatomic, assign, readonly) int32_t port; +@property(nonatomic, strong, readonly) NSString *username; +@property(nonatomic, strong, readonly) NSString *password; +-(id)initWithHost:(NSString*)host port:(int32_t)port username:(NSString *)username password:(NSString *)password; +@end + +@interface AudioDevice : NSObject +@property(nonatomic, strong, readonly) NSString * deviceId; +@property(nonatomic, strong, readonly) NSString * deviceName; +-(id)initWithDeviceId:(NSString*)deviceId deviceName:(NSString *)deviceName; +@end + +@interface OngoingCallThreadLocalContext : NSObject + ++ (void)setupLoggingFunction:(void (* _Nullable)(NSString * _Nullable))loggingFunction; ++ (void)applyServerConfig:(NSString * _Nullable)data; ++ (int32_t)maxLayer; ++ (NSString * _Nonnull)version; + +@property (nonatomic, copy) void (^ _Nullable stateChanged)(OngoingCallState); +@property (nonatomic, copy) void (^ _Nullable signalBarsChanged)(int32_t); + +- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue proxy:(VoipProxyServer * _Nullable)proxy networkType:(OngoingCallNetworkType)networkType dataSaving:(OngoingCallDataSaving)dataSaving derivedState:(NSData * _Nonnull)derivedState key:(NSData * _Nonnull)key isOutgoing:(bool)isOutgoing primaryConnection:(OngoingCallConnectionDescription * _Nonnull)primaryConnection alternativeConnections:(NSArray * _Nonnull)alternativeConnections maxLayer:(int32_t)maxLayer allowP2P:(BOOL)allowP2P logPath:(NSString * _Nonnull)logPath; +- (void)stop:(void (^_Nonnull)(NSString * _Nullable debugLog, int64_t bytesSentWifi, int64_t bytesReceivedWifi, int64_t bytesSentMobile, int64_t bytesReceivedMobile))completion; + +- (bool)needRate; + +- (NSString * _Nullable)debugInfo; +- (NSString * _Nullable)version; +- (NSData * _Nonnull)getDerivedState; + +- (void)setIsMuted:(bool)isMuted; +- (void)setNetworkType:(OngoingCallNetworkType)networkType; + + + +@end diff --git a/Telegram-Mac/OngoingCallThreadLocalContext.mm b/Telegram-Mac/OngoingCallThreadLocalContext.mm new file mode 100644 index 0000000000..2deb6c22ff --- /dev/null +++ b/Telegram-Mac/OngoingCallThreadLocalContext.mm @@ -0,0 +1,456 @@ +// +// CallsBridge.m +// Telegram +// +// Created by keepcoder on 03/05/2017. +// Copyright © 2017 Telegram. All rights reserved. +// + +#import "OngoingCallThreadLocalContext.h" +#import "VoIPController.h" +#import "VoIPServerConfig.h" +#import "TGCallUtils.h" +#import "TgVoip.h" +#import "OngoingCallConnectionDescription.h" +#define CVoIPController tgvoip::VoIPController + +#import +#import + + +@implementation AudioDevice + +-(id)initWithDeviceId:(NSString*)deviceId deviceName:(NSString *)deviceName { + if (self = [super init]) { + _deviceId = deviceId; + _deviceName = deviceName; + } + return self; +} +@end + + +void TGCallAesIgeEncrypt(uint8_t *inBytes, uint8_t *outBytes, size_t length, uint8_t *key, uint8_t *iv) { + MTAesEncryptRaw(inBytes, outBytes, length, key, iv); +} + +void TGCallAesIgeDecrypt(uint8_t *inBytes, uint8_t *outBytes, size_t length, uint8_t *key, uint8_t *iv) { + MTAesDecryptRaw(inBytes, outBytes, length, key, iv); +} + +void TGCallSha1(uint8_t *msg, size_t length, uint8_t *output) { + MTRawSha1(msg, length, output); +} + +void TGCallSha256(uint8_t *msg, size_t length, uint8_t *output) { + MTRawSha256(msg, length, output); +} + +void TGCallAesCtrEncrypt(uint8_t *inOut, size_t length, uint8_t *key, uint8_t *iv, uint8_t *ecount, uint32_t *num) { + uint8_t *outData = (uint8_t *)malloc(length); + MTAesCtr *aesCtr = [[MTAesCtr alloc] initWithKey:key keyLength:32 iv:iv ecount:ecount num:*num]; + [aesCtr encryptIn:inOut out:outData len:length]; + memcpy(inOut, outData, length); + free(outData); + + [aesCtr getIv:iv]; + + memcpy(ecount, [aesCtr ecount], 16); + *num = [aesCtr num]; +} + +void TGCallRandomBytes(uint8_t *buffer, size_t length) { + arc4random_buf(buffer, length); +} + +static TgVoipNetworkType callControllerNetworkTypeForType(OngoingCallNetworkType type) { + switch (type) { + case OngoingCallNetworkTypeWifi: + return TgVoipNetworkType::WiFi; + case OngoingCallNetworkTypeCellularGprs: + return TgVoipNetworkType::Gprs; + case OngoingCallNetworkTypeCellular3g: + return TgVoipNetworkType::ThirdGeneration; + case OngoingCallNetworkTypeCellularLte: + return TgVoipNetworkType::Lte; + default: + return TgVoipNetworkType::ThirdGeneration; + } +} + +static TgVoipDataSaving callControllerDataSavingForType(OngoingCallDataSaving type) { + switch (type) { + case OngoingCallDataSavingNever: + return TgVoipDataSaving::Never; + case OngoingCallDataSavingCellular: + return TgVoipDataSaving::Mobile; + case OngoingCallDataSavingAlways: + return TgVoipDataSaving::Always; + default: + return TgVoipDataSaving::Never; + } +} + + + +@implementation VoipProxyServer +-(id)initWithHost:(NSString*)host port:(int32_t)port username:(NSString *)username password:(NSString *)password { + self = [super init]; + _host = host; + _port = port; + _username = username; + _password = password; + return self; +} +@end + +@implementation OngoingCallConnectionDescription + +- (instancetype _Nonnull)initWithConnectionId:(int64_t)connectionId ip:(NSString * _Nonnull)ip ipv6:(NSString * _Nonnull)ipv6 port:(int32_t)port peerTag:(NSData * _Nonnull)peerTag { + self = [super init]; + if (self != nil) { + _connectionId = connectionId; + _ip = ip; + _ipv6 = ipv6; + _port = port; + _peerTag = peerTag; + } + return self; +} + +@end + +static MTAtomic *callContexts() { + static MTAtomic *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[MTAtomic alloc] initWithValue:[[NSMutableDictionary alloc] init]]; + }); + return instance; +} + +@interface OngoingCallThreadLocalContextReference : NSObject + +@property (nonatomic, weak) OngoingCallThreadLocalContext *context; +@property (nonatomic, strong, readonly) id queue; + +@end + +@implementation OngoingCallThreadLocalContextReference + +- (instancetype)initWithContext:(OngoingCallThreadLocalContext *)context queue:(id)queue { + self = [super init]; + if (self != nil) { + self.context = context; + _queue = queue; + } + return self; +} + +@end + +static int32_t nextId = 1; + +static int32_t addContext(OngoingCallThreadLocalContext *context, id queue) { + int32_t contextId = OSAtomicIncrement32(&nextId); + [callContexts() with:^id(NSMutableDictionary *dict) { + dict[@(contextId)] = [[OngoingCallThreadLocalContextReference alloc] initWithContext:context queue:queue]; + return nil; + }]; + return contextId; +} + +static void removeContext(int32_t contextId) { + [callContexts() with:^id(NSMutableDictionary *dict) { + [dict removeObjectForKey:@(contextId)]; + return nil; + }]; +} + +static void withContext(int32_t contextId, void (^f)(OngoingCallThreadLocalContext *)) { + __block OngoingCallThreadLocalContextReference *reference = nil; + [callContexts() with:^id(NSMutableDictionary *dict) { + reference = dict[@(contextId)]; + return nil; + }]; + if (reference != nil) { + [reference.queue dispatch:^{ + __strong OngoingCallThreadLocalContext *context = reference.context; + if (context != nil) { + f(context); + } + }]; + } +} + +@interface OngoingCallThreadLocalContext () { + id _queue; + int32_t _contextId; + + OngoingCallNetworkType _networkType; + NSTimeInterval _callReceiveTimeout; + NSTimeInterval _callRingTimeout; + NSTimeInterval _callConnectTimeout; + NSTimeInterval _callPacketTimeout; + + std::unique_ptr _tgVoip; + + + OngoingCallState _state; + int32_t _signalBars; + NSData *_lastDerivedState; +} + +- (void)controllerStateChanged:(TgVoipState)state; +- (void)signalBarsChanged:(int32_t)signalBars; + +@end + +@implementation OngoingCallThreadLocalContext + +static void (*InternalVoipLoggingFunction)(NSString *) = NULL; + ++ (void)setupLoggingFunction:(void (*)(NSString *))loggingFunction { + InternalVoipLoggingFunction = loggingFunction; + TgVoip::setLoggingFunction([](std::string const &string) { + if (InternalVoipLoggingFunction) { + InternalVoipLoggingFunction([[NSString alloc] initWithUTF8String:string.c_str()]); + } + }); +} + ++ (void)applyServerConfig:(NSString *)string { + if (string.length != 0) { + TgVoip::setGlobalServerConfig(std::string(string.UTF8String)); + } +} + ++ (int32_t)maxLayer { + return 92; +} + ++ (NSString *)version { + return [NSString stringWithUTF8String:TgVoip::getVersion().c_str()]; +} + +- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue proxy:(VoipProxyServer * _Nullable)proxy networkType:(OngoingCallNetworkType)networkType dataSaving:(OngoingCallDataSaving)dataSaving derivedState:(NSData * _Nonnull)derivedState key:(NSData * _Nonnull)key isOutgoing:(bool)isOutgoing primaryConnection:(OngoingCallConnectionDescription * _Nonnull)primaryConnection alternativeConnections:(NSArray * _Nonnull)alternativeConnections maxLayer:(int32_t)maxLayer allowP2P:(BOOL)allowP2P logPath:(NSString * _Nonnull)logPath { + self = [super init]; + if (self != nil) { + _queue = queue; + assert([queue isCurrent]); + _contextId = addContext(self, queue); + + _callReceiveTimeout = 20.0; + _callRingTimeout = 90.0; + _callConnectTimeout = 30.0; + _callPacketTimeout = 10.0; + _networkType = networkType; + + std::vector derivedStateValue; + derivedStateValue.resize(derivedState.length); + [derivedState getBytes:derivedStateValue.data() length:derivedState.length]; + + TgVoipProxy* proxyValue = nullptr; + if (proxy != nil) { + TgVoipProxy *proxyObject = new TgVoipProxy(); + proxyObject->host = proxy.host.UTF8String; + proxyObject->port = (uint16_t)proxy.port; + proxyObject->login = proxy.username.UTF8String ?: ""; + proxyObject->password = proxy.password.UTF8String ?: ""; + proxyValue = proxyObject; + } + + TgVoipCrypto crypto; + crypto.sha1 = &TGCallSha1; + crypto.sha256 = &TGCallSha256; + crypto.rand_bytes = &TGCallRandomBytes; + crypto.aes_ige_encrypt = &TGCallAesIgeEncrypt; + crypto.aes_ige_decrypt = &TGCallAesIgeDecrypt; + crypto.aes_ctr_encrypt = &TGCallAesCtrEncrypt; + + std::vector endpoints; + NSArray *connections = [@[primaryConnection] arrayByAddingObjectsFromArray:alternativeConnections]; + for (OngoingCallConnectionDescription *connection in connections) { + unsigned char peerTag[16]; + [connection.peerTag getBytes:peerTag length:16]; + + TgVoipEndpoint endpoint; + endpoint.endpointId = connection.connectionId; + endpoint.host = { + .ipv4 = std::string(connection.ip.UTF8String), + .ipv6 = std::string(connection.ipv6.UTF8String) + }; + endpoint.port = (uint16_t)connection.port; + endpoint.type = TgVoipEndpointType::UdpRelay; + memcpy(endpoint.peerTag, peerTag, 16); + endpoints.push_back(endpoint); + } + + TgVoipConfig config = { + .initializationTimeout = _callConnectTimeout, + .receiveTimeout = _callPacketTimeout, + .dataSaving = callControllerDataSavingForType(dataSaving), + .enableP2P = static_cast(allowP2P), + .enableAEC = false, + .enableNS = true, + .enableAGC = true, + .enableCallUpgrade = false, + .logPath = logPath.length == 0 ? "" : std::string(logPath.UTF8String), + .maxApiLayer = [OngoingCallThreadLocalContext maxLayer] + }; + + std::vector encryptionKeyValue; + encryptionKeyValue.resize(key.length); + memcpy(encryptionKeyValue.data(), key.bytes, key.length); + + TgVoipEncryptionKey encryptionKey = { + .value = encryptionKeyValue, + .isOutgoing = isOutgoing, + }; + + /* + TgVoipConfig const &config, + TgVoipPersistentState const &persistentState, + std::vector const &endpoints, + std::unique_ptr const &proxy, + TgVoipNetworkType initialNetworkType, + TgVoipEncryptionKey const &encryptionKey + #ifdef TGVOIP_USE_CUSTOM_CRYPTO + , + TgVoipCrypto const &crypto + */ + + + + _tgVoip = TgVoip::makeInstance( + config, + { derivedStateValue }, + endpoints, + proxyValue, + callControllerNetworkTypeForType(networkType), + encryptionKey, + crypto + ); + + _state = OngoingCallStateInitializing; + _signalBars = -1; + + __weak OngoingCallThreadLocalContext *weakSelf = self; + _tgVoip->setOnStateUpdated([weakSelf](TgVoipState state) { + __strong OngoingCallThreadLocalContext *strongSelf = weakSelf; + if (strongSelf) { + [strongSelf controllerStateChanged:state]; + } + }); + _tgVoip->setOnSignalBarsUpdated([weakSelf](int signalBars) { + __strong OngoingCallThreadLocalContext *strongSelf = weakSelf; + if (strongSelf) { + [strongSelf signalBarsChanged:signalBars]; + } + }); + } + return self; +} + +- (void)dealloc { + assert([_queue isCurrent]); + removeContext(_contextId); + if (_tgVoip != NULL) { + [self stop:nil]; + } +} + +- (void)stop:(void (^)(NSString *, int64_t, int64_t, int64_t, int64_t))completion { + if (_tgVoip) { + TgVoipFinalState finalState = _tgVoip->stop(); + + NSString *debugLog = [NSString stringWithUTF8String:finalState.debugLog.c_str()]; + _lastDerivedState = [[NSData alloc] initWithBytes:finalState.persistentState.value.data() length:finalState.persistentState.value.size()]; + + _tgVoip = NULL; + + if (completion) { + completion(debugLog, finalState.trafficStats.bytesSentWifi, finalState.trafficStats.bytesReceivedWifi, finalState.trafficStats.bytesSentMobile, finalState.trafficStats.bytesReceivedMobile); + } + } +} + +- (NSString *)debugInfo { + if (_tgVoip != nil) { + auto rawDebugString = _tgVoip->getDebugInfo(); + return [NSString stringWithUTF8String:rawDebugString.c_str()]; + } else { + return nil; + } +} + +- (NSString *)version { + if (_tgVoip != nil) { + return [NSString stringWithUTF8String:_tgVoip->getVersion().c_str()]; + } else { + return nil; + } +} + +- (NSData * _Nonnull)getDerivedState { + if (_tgVoip) { + auto persistentState = _tgVoip->getPersistentState(); + return [[NSData alloc] initWithBytes:persistentState.value.data() length:persistentState.value.size()]; + } else if (_lastDerivedState != nil) { + return _lastDerivedState; + } else { + return [NSData data]; + } +} + +- (void)controllerStateChanged:(TgVoipState)state { + OngoingCallState callState = OngoingCallStateInitializing; + switch (state) { + case TgVoipState::Established: + callState = OngoingCallStateConnected; + break; + case TgVoipState::Failed: + callState = OngoingCallStateFailed; + break; + case TgVoipState::Reconnecting: + callState = OngoingCallStateReconnecting; + break; + default: + break; + } + + if (callState != _state) { + _state = callState; + + if (_stateChanged) { + _stateChanged(callState); + } + } +} + +- (void)signalBarsChanged:(int32_t)signalBars { + if (signalBars != _signalBars) { + _signalBars = signalBars; + + if (_signalBarsChanged) { + _signalBarsChanged(signalBars); + } + } +} + +- (void)setIsMuted:(bool)isMuted { + if (_tgVoip) { + _tgVoip->setMuteMicrophone(isMuted); + } +} + +- (void)setNetworkType:(OngoingCallNetworkType)networkType { + if (_networkType != networkType) { + _networkType = networkType; + if (_tgVoip) { + _tgVoip->setNetworkType(callControllerNetworkTypeForType(networkType)); + } + } +} + +@end diff --git a/Telegram-Mac/OpmizeDatabaseView.swift b/Telegram-Mac/OpmizeDatabaseView.swift new file mode 100644 index 0000000000..963eafc146 --- /dev/null +++ b/Telegram-Mac/OpmizeDatabaseView.swift @@ -0,0 +1,52 @@ +// +// OpmizeDatabaseView.swift +// Telegram +// +// Created by Mikhail Filimonov on 22/04/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +class OpmizeDatabaseView: Control { + private let textView = TextView() + + private let progressView = RadialProgressView.init(theme: RadialProgressTheme.init(backgroundColor: .clear, foregroundColor: theme.colors.text), twist: true, size: NSMakeSize(40, 40)) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + autoresizingMask = [.width, .height] + addSubview(textView) + addSubview(progressView) + self.backgroundColor = theme.colors.background + let attributedString = NSMutableAttributedString() + _ = attributedString.append(string: L10n.telegramUpgradeDatabaseTitle, color: theme.colors.text, font: .medium(20)) + _ = attributedString.append(string: "\n\n", color: theme.colors.text, font: .medium(13)) + _ = attributedString.append(string: L10n.telegramUpgradeDatabaseText, color: theme.colors.text, font: .normal(14)) + + let layout = TextViewLayout(attributedString, alignment: .center, alwaysStaticItems: true) + layout.measure(width: 300) + + textView.update(layout) + + textView.userInteractionEnabled = false + textView.isSelectable = false + + progressView.state = .ImpossibleFetching(progress: 0.2, force: true) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func layout() { + super.layout() + progressView.centerX(y: frame.midY - progressView.frame.height - 10) + textView.centerX(y: frame.midY + 10) + + } + + func setProgress(_ progress: Float) { + progressView.state = .ImpossibleFetching(progress: max(0.2, progress), force: true) + } + +} diff --git a/Telegram-Mac/OpusAudioBuffer.h b/Telegram-Mac/OpusAudioBuffer.h deleted file mode 100644 index 88c91903c1..0000000000 --- a/Telegram-Mac/OpusAudioBuffer.h +++ /dev/null @@ -1,28 +0,0 @@ -#import - -struct OpusAudioBuffer -{ - NSUInteger capacity; - uint8_t *data; - NSUInteger size; - int64_t pcmOffset; -}; - -inline OpusAudioBuffer *OpusAudioBufferWithCapacity(NSUInteger capacity) -{ - OpusAudioBuffer *audioBuffer = (OpusAudioBuffer *)malloc(sizeof(OpusAudioBuffer)); - audioBuffer->capacity = capacity; - audioBuffer->data = (uint8_t *)malloc(capacity); - audioBuffer->size = 0; - audioBuffer->pcmOffset = 0; - return audioBuffer; -} - -inline void OpusAudioBufferDispose(OpusAudioBuffer *audioBuffer) -{ - if (audioBuffer != NULL) - { - free(audioBuffer->data); - free(audioBuffer); - } -} diff --git a/Telegram-Mac/OpusAudioPlayer.swift b/Telegram-Mac/OpusAudioPlayer.swift deleted file mode 100644 index d383202a69..0000000000 --- a/Telegram-Mac/OpusAudioPlayer.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// OpusAudioPlayer.swift -// TelegramMac -// -// Created by keepcoder on 25/11/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import AudioUnit -import SwiftSignalKitMac -class OpusAudioPlayer: AudioPlayer, OpusBridgeDelegate { - - let bridge:OpusObjcBridge - - override init(_ path: String) { - bridge = OpusObjcBridge(path: path) - super.init(path) - bridge.delegate = self - } - - override func play() { - bridge.play() - } - - override func playFrom(position: TimeInterval) { - bridge.play(fromPosition: position) - } - - override func set(position: TimeInterval) { - bridge.setCurrentPosition(position) - } - - override func pause() { - bridge.pause() - } - - override func stop() { - bridge.stop() - } - - override var duration: TimeInterval { - return bridge.duration() - } - - override var currentTime: TimeInterval { - return bridge.currentPositionSync(true) - } - - func audioPlayerDidStartPlaying(_ audioPlayer: OpusObjcBridge!) { - Queue.mainQueue().async { - self.delegate?.audioPlayerDidStartPlaying(self) - } - } - - func audioPlayerDidFinishPlaying(_ audioPlayer: OpusObjcBridge!) { - Queue.mainQueue().async { - self.delegate?.audioPlayerDidFinishPlaying(self) - } - } - - func audioPlayerDidPause(_ audioPlayer: OpusObjcBridge!) { - Queue.mainQueue().async { - self.delegate?.audioPlayerDidPaused(self) - } - } - -} diff --git a/Telegram-Mac/OpusObjcBridge.h b/Telegram-Mac/OpusObjcBridge.h deleted file mode 100644 index abd7435373..0000000000 --- a/Telegram-Mac/OpusObjcBridge.h +++ /dev/null @@ -1,39 +0,0 @@ -// -// OpusObjcBridge.h -// TelegramMac -// -// Created by keepcoder on 25/11/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -#import - -@interface OpusObjcBridge : NSObject - -@end - -@protocol OpusBridgeDelegate -- (void)audioPlayerDidFinishPlaying:(OpusObjcBridge *)audioPlayer; -- (void)audioPlayerDidStartPlaying:(OpusObjcBridge *)audioPlayer; -- (void)audioPlayerDidPause:(OpusObjcBridge *)audioPlayer; - -@end - -@interface OpusObjcBridge () - -@property (nonatomic, weak) id delegate; - -- (instancetype)initWithPath:(NSString *)path; -+ (bool)canPlayFile:(NSString *)path; -+ (NSTimeInterval)durationFile:(NSString *)path; -- (void)play; -- (void)playFromPosition:(NSTimeInterval)position; -- (void)pause; -- (void)stop; -- (void)reset; -- (NSTimeInterval)currentPositionSync:(bool)sync; -- (NSTimeInterval)duration; --(void)setCurrentPosition:(NSTimeInterval)position; -- (BOOL)isPaused; -- (BOOL)isEqualToPath:(NSString *)path; -@end diff --git a/Telegram-Mac/OpusObjcBridge.mm b/Telegram-Mac/OpusObjcBridge.mm deleted file mode 100644 index 6f4be027fe..0000000000 --- a/Telegram-Mac/OpusObjcBridge.mm +++ /dev/null @@ -1,591 +0,0 @@ -// -// OpusObjcBridge.m -// TelegramMac -// -// Created by keepcoder on 25/11/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -#import "OpusObjcBridge.h" -#import "opusfile.h" -#import -#import -#import -#import "NSObject+TGLock.h" -#import "ATQueue.h" -#import "OpusAudioBuffer.h" -#define kOutputBus 0 -#define kInputBus 1 - -static const int OpusAudioPlayerBufferCount = 3; -static const int OpusAudioPlayerSampleRate = 48000; // libopusfile is bound to use 48 kHz - -static TG_SYNCHRONIZED_DEFINE(filledBuffersLock) = PTHREAD_MUTEX_INITIALIZER; -static volatile OSSpinLock audioPositionLock = OS_SPINLOCK_INIT; - -static std::map activeAudioPlayers; - - -@interface OpusObjcBridge () -{ -@public - int _playerId; - - NSString *_filePath; - NSInteger _fileSize; - - bool _isSeekable; - int64_t _totalPcmDuration; - - bool _isPaused; - - OggOpusFile *_opusFile; - AudioComponentInstance _audioUnit; - - OpusAudioBuffer *_filledAudioBuffers[OpusAudioPlayerBufferCount]; - int _filledAudioBufferCount; - int _filledAudioBufferPosition; - - int64_t _currentPcmOffset; - bool _finished; -} - -@end - -@implementation OpusObjcBridge - -static ATQueue * queue; -+(void)initialize { - queue = [[ATQueue alloc] initWithName:@"OpusPlayerQueue"]; -} - -+ (bool)canPlayFile:(NSString *)path -{ - int error = OPUS_OK; - OggOpusFile *file = op_test_file([path UTF8String], &error); - if (file != NULL) - { - error = op_test_open(file); - op_free(file); - return error == OPUS_OK; - } - return false; -} - -+ (NSTimeInterval)durationFile:(NSString *)path { - int error = OPUS_OK; - OggOpusFile *file = op_test_file([path UTF8String], &error); - if (file != NULL) - { - float duration = 0; - error = op_test_open(file); - duration = op_pcm_total(file, -1); - op_free(file); - return duration / (NSTimeInterval)OpusAudioPlayerSampleRate; - } - return 0; -} - --(BOOL)isPaused { - return _isPaused; -} - --(BOOL)isEqualToPath:(NSString *)path { - return [_filePath isEqualToString:path]; -} - -- (instancetype)initWithPath:(NSString *)path -{ - self = [super init]; - if (self != nil) - { - _filePath = path; - - static int nextPlayerId = 1; - _playerId = nextPlayerId++; - - _isPaused = true; - - [queue dispatch:^{ - _fileSize = [[[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil][NSFileSize] integerValue]; - if (_fileSize == 0) - { - NSLog(@"[TGOpusAudioPlayer#%p invalid file]", self); - [self cleanupAndReportError]; - } - }]; - - } - return self; -} - -- (void)dealloc -{ - [self cleanup]; -} - -- (void)cleanupAndReportError -{ - [self cleanup]; -} - -- (void)cleanup -{ - TG_SYNCHRONIZED_BEGIN(filledBuffersLock); - - activeAudioPlayers.erase(_playerId); - - for (int i = 0; i < OpusAudioPlayerBufferCount; i++) - { - if (_filledAudioBuffers[i] != NULL) - { - OpusAudioBufferDispose(_filledAudioBuffers[i]); - _filledAudioBuffers[i] = NULL; - } - } - _filledAudioBufferCount = 0; - _filledAudioBufferPosition = 0; - - TG_SYNCHRONIZED_END(filledBuffersLock); - - OggOpusFile *opusFile = _opusFile; - _opusFile = NULL; - - AudioUnit audioUnit = _audioUnit; - _audioUnit = NULL; - - intptr_t objectId = (intptr_t)self; - - [queue dispatch:^{ - if (audioUnit != NULL) - { - OSStatus status = noErr; - status = AudioOutputUnitStop(audioUnit); - if (status != noErr) - NSLog(@"[TGOpusAudioPlayer#%lx AudioOutputUnitStop failed: %d]", objectId, (int)status); - - status = AudioComponentInstanceDispose(audioUnit); - if (status != noErr) - NSLog(@"[TGOpusAudioRecorder#%lx AudioComponentInstanceDispose failed: %d]", objectId, (int)status); - } - - if (opusFile != NULL) - op_free(opusFile); - }]; - -} - -static OSStatus TGOpusAudioPlayerCallback(void *inRefCon, __unused AudioUnitRenderActionFlags *ioActionFlags, __unused const AudioTimeStamp *inTimeStamp, __unused UInt32 inBusNumber, __unused UInt32 inNumberFrames, AudioBufferList *ioData) -{ - int playerId = (int)(NSInteger)inRefCon; - - TG_SYNCHRONIZED_BEGIN(filledBuffersLock); - - OpusObjcBridge *self = nil; - auto it = activeAudioPlayers.find(playerId); - if (it != activeAudioPlayers.end()) - self = it->second; - - if (self != nil) - { - OpusAudioBuffer **freedAudioBuffers = NULL; - int freedAudioBufferCount = 0; - - for (int i = 0; i < (int)ioData->mNumberBuffers; i++) - { - AudioBuffer *buffer = &ioData->mBuffers[i]; - - buffer->mNumberChannels = 1; - - int requiredBytes = buffer->mDataByteSize; - int writtenBytes = 0; - - while (self->_filledAudioBufferCount > 0 && writtenBytes < requiredBytes) - { - OSSpinLockLock(&audioPositionLock); - self->_currentPcmOffset = self->_filledAudioBuffers[0]->pcmOffset + self->_filledAudioBufferPosition / 2; - OSSpinLockUnlock(&audioPositionLock); - - int takenBytes = MIN((int)self->_filledAudioBuffers[0]->size - self->_filledAudioBufferPosition, requiredBytes - writtenBytes); - - if (takenBytes != 0) - { - memcpy(((uint8_t *)buffer->mData) + writtenBytes, self->_filledAudioBuffers[0]->data + self->_filledAudioBufferPosition, takenBytes); - writtenBytes += takenBytes; - } - - if (self->_filledAudioBufferPosition + takenBytes >= (int)self->_filledAudioBuffers[0]->size) - { - if (freedAudioBuffers == NULL) - freedAudioBuffers = (OpusAudioBuffer **)malloc(sizeof(OpusAudioBuffer *) * OpusAudioPlayerBufferCount); - freedAudioBuffers[freedAudioBufferCount] = self->_filledAudioBuffers[0]; - freedAudioBufferCount++; - - for (int i = 0; i < OpusAudioPlayerBufferCount - 1; i++) - { - self->_filledAudioBuffers[i] = self->_filledAudioBuffers[i + 1]; - } - self->_filledAudioBuffers[OpusAudioPlayerBufferCount - 1] = NULL; - - self->_filledAudioBufferCount--; - self->_filledAudioBufferPosition = 0; - } - else - self->_filledAudioBufferPosition += takenBytes; - } - - if (writtenBytes < requiredBytes) - memset(((uint8_t *)buffer->mData) + writtenBytes, 0, requiredBytes - writtenBytes); - } - - if (freedAudioBufferCount != 0) - { - [queue dispatch:^{ - for (int i = 0; i < freedAudioBufferCount; i++) - { - [self fillBuffer:freedAudioBuffers[i]]; - } - - free(freedAudioBuffers); - }]; - } - } - else - { - for (int i = 0; i < (int)ioData->mNumberBuffers; i++) - { - AudioBuffer *buffer = &ioData->mBuffers[i]; - memset(buffer->mData, 0, buffer->mDataByteSize); - } - } - - TG_SYNCHRONIZED_END(filledBuffersLock); - - return noErr; -} - -- (void)play { - [self playFromPosition:[self currentPositionSync:true]]; -} - -- (void)playFromPosition:(NSTimeInterval)position -{ - [queue dispatch:^{ - if (!_isPaused) - return; - - if (_audioUnit == NULL) - { - - _isPaused = false; - - int openError = OPUS_OK; - _opusFile = op_open_file([_filePath UTF8String], &openError); - if (_opusFile == NULL || openError != OPUS_OK) - { - NSLog(@"[TGOpusAudioPlayer#%p op_open_file failed: %d]", self, openError); - [self cleanupAndReportError]; - - return; - } - - _isSeekable = op_seekable(_opusFile); - _totalPcmDuration = op_pcm_total(_opusFile, -1); - - AudioComponentDescription desc; - desc.componentType = kAudioUnitType_Output; - desc.componentSubType = kAudioUnitSubType_HALOutput; - desc.componentFlags = 0; - desc.componentFlagsMask = 0; - desc.componentManufacturer = kAudioUnitManufacturer_Apple; - AudioComponent inputComponent = AudioComponentFindNext(NULL, &desc); - AudioComponentInstanceNew(inputComponent, &_audioUnit); - - OSStatus status = noErr; - - static const UInt32 one = 1; - status = AudioUnitSetProperty(_audioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, kOutputBus, &one, sizeof(one)); - if (status != noErr) - { - NSLog(@"[TGOpusAudioPlayer#%@ AudioUnitSetProperty kAudioOutputUnitProperty_EnableIO failed: %d]", self, (int)status); - [self cleanupAndReportError]; - - return; - } - - AudioStreamBasicDescription outputAudioFormat; - outputAudioFormat.mSampleRate = OpusAudioPlayerSampleRate; - outputAudioFormat.mFormatID = kAudioFormatLinearPCM; - outputAudioFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked; - outputAudioFormat.mFramesPerPacket = 1; - outputAudioFormat.mChannelsPerFrame = 1; - outputAudioFormat.mBitsPerChannel = 16; - outputAudioFormat.mBytesPerPacket = 2; - outputAudioFormat.mBytesPerFrame = 2; - status = AudioUnitSetProperty(_audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, kOutputBus, &outputAudioFormat, sizeof(outputAudioFormat)); - if (status != noErr) - { - NSLog(@"[TGOpusAudioPlayer#%@ AudioUnitSetProperty kAudioUnitProperty_StreamFormat failed: %d]", self, (int)status); - [self cleanupAndReportError]; - - return; - } - - AURenderCallbackStruct callbackStruct; - callbackStruct.inputProc = &TGOpusAudioPlayerCallback; - callbackStruct.inputProcRefCon = (void *)(NSInteger)_playerId; - if (AudioUnitSetProperty(_audioUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Global, kOutputBus, &callbackStruct, sizeof(callbackStruct)) != noErr) - { - NSLog(@"[TGOpusAudioPlayer#%@ AudioUnitSetProperty kAudioUnitProperty_SetRenderCallback failed]", self); - [self cleanupAndReportError]; - - return; - } - - status = AudioUnitInitialize(_audioUnit); - if (status != noErr) - { - NSLog(@"[TGOpusAudioRecorder#%@ AudioUnitInitialize failed: %d]", self, (int)status); - [self cleanup]; - - return; - } - - TG_SYNCHRONIZED_BEGIN(filledBuffersLock); - activeAudioPlayers[_playerId] = self; - TG_SYNCHRONIZED_END(filledBuffersLock); - - NSUInteger bufferByteSize = [self bufferByteSize]; - for (int i = 0; i < OpusAudioPlayerBufferCount; i++) - { - _filledAudioBuffers[i] = OpusAudioBufferWithCapacity(bufferByteSize); - } - _filledAudioBufferCount = OpusAudioPlayerBufferCount; - _filledAudioBufferPosition = 0; - - _finished = false; - - if (_isSeekable && position >= 0.0) - op_pcm_seek(_opusFile, (ogg_int64_t)(position * OpusAudioPlayerSampleRate)); - - status = AudioOutputUnitStart(_audioUnit); - if (status != noErr) - { - NSLog(@"[TGOpusAudioRecorder#%@ AudioOutputUnitStart failed: %d]", self, (int)status); - [self cleanupAndReportError]; - } - - } - else - { - - if (_isSeekable && position >= 0.0) - { - int result = op_pcm_seek(_opusFile, (ogg_int64_t)(position * OpusAudioPlayerSampleRate)); - if (result != OPUS_OK) - NSLog(@"[TGOpusAudioPlayer#%p op_pcm_seek failed: %d]", self, result); - - ogg_int64_t pcmPosition = op_pcm_tell(_opusFile); - _currentPcmOffset = pcmPosition; - - _isPaused = false; - } - else - _isPaused = false; - - _finished = false; - - TG_SYNCHRONIZED_BEGIN(filledBuffersLock); - for (int i = 0; i < _filledAudioBufferCount; i++) - { - _filledAudioBuffers[i]->size = 0; - } - self->_filledAudioBufferPosition = 0; - TG_SYNCHRONIZED_END(filledBuffersLock); - } - [self _notifyStart]; - - }]; -} - -- (void)fillBuffer:(OpusAudioBuffer *)audioBuffer -{ - if (_opusFile != NULL) - { - audioBuffer->pcmOffset = MAX(0, op_pcm_tell(_opusFile)); - - if (!_isPaused) - { - if (_finished) - { - bool notifyFinished = false; - TG_SYNCHRONIZED_BEGIN(filledBuffersLock); - if (_filledAudioBufferCount == 0) - notifyFinished = true; - TG_SYNCHRONIZED_END(filledBuffersLock); - - if (notifyFinished) - [self _notifyFinished]; - - return; - } - else - { - int availableOutputBytes = (int)audioBuffer->capacity; - int writtenOutputBytes = 0; - - bool endOfFileReached = false; - - bool bufferPcmOffsetSet = false; - - while (writtenOutputBytes < availableOutputBytes) - { - if (!bufferPcmOffsetSet) - { - bufferPcmOffsetSet = true; - audioBuffer->pcmOffset = MAX(0, op_pcm_tell(_opusFile)); - } - - int readSamples = op_read(_opusFile, (opus_int16 *)(audioBuffer->data + writtenOutputBytes), (availableOutputBytes - writtenOutputBytes) / 2, NULL); - - if (readSamples > 0) - writtenOutputBytes += readSamples * 2; - else - { - if (readSamples < 0) - NSLog(@"[TGOpusAudioPlayer#%p op_read failed: %d]", self, readSamples); - - endOfFileReached = true; - - break; - } - } - - audioBuffer->size = writtenOutputBytes; - - if (endOfFileReached) - _finished = true; - } - } - else - { - memset(audioBuffer->data, 0, audioBuffer->capacity); - audioBuffer->size = audioBuffer->capacity; - audioBuffer->pcmOffset = _currentPcmOffset; - } - } - else - { - memset(audioBuffer->data, 0, audioBuffer->capacity); - audioBuffer->size = audioBuffer->capacity; - audioBuffer->pcmOffset = _totalPcmDuration; - } - - TG_SYNCHRONIZED_BEGIN(filledBuffersLock); - _filledAudioBufferCount++; - _filledAudioBuffers[_filledAudioBufferCount - 1] = audioBuffer; - TG_SYNCHRONIZED_END(filledBuffersLock); -} - -- (NSUInteger)bufferByteSize -{ - static const NSUInteger maxBufferSize = 0x50000; - static const NSUInteger minBufferSize = 0x4000; - - Float64 seconds = 0.4; - - Float64 numPacketsForTime = OpusAudioPlayerSampleRate * seconds; - NSUInteger result = (NSUInteger)(numPacketsForTime * 2); - - return MAX(minBufferSize, MIN(maxBufferSize, result)); -} - -- (void)pause { - [self pause:true]; -} - -- (void)pause:(bool)notify -{ - [queue dispatch:^{ - _isPaused = true; - - TG_SYNCHRONIZED_BEGIN(filledBuffersLock); - for (int i = 0; i < _filledAudioBufferCount; i++) - { - if (_filledAudioBuffers[i]->size != 0) - memset(_filledAudioBuffers[i]->data, 0, _filledAudioBuffers[i]->size); - _filledAudioBuffers[i]->pcmOffset = _currentPcmOffset; - } - TG_SYNCHRONIZED_END(filledBuffersLock); - }]; - if (notify) - [self _notifyPause]; -} - -- (void)stop -{ - [queue dispatch:^{ - [self cleanup]; - }]; - -} - -- (NSTimeInterval)currentPositionSync:(bool)sync -{ - __block NSTimeInterval result = 0.0; - - dispatch_block_t block = ^ - { - OSSpinLockLock(&audioPositionLock); - result = (float)_currentPcmOffset / (float)OpusAudioPlayerSampleRate; - OSSpinLockUnlock(&audioPositionLock); - }; - - if (sync) - [queue dispatch:block synchronous:true]; - else - block(); - - return result; -} - --(void)setCurrentPosition:(NSTimeInterval)position { - [queue dispatch:^{ - if (_isPaused) { - [self playFromPosition:position]; - [self pause]; - } else { - [self pause:false]; - [self playFromPosition:position]; - } - }]; -} - -- (NSTimeInterval)duration -{ - return _totalPcmDuration / (NSTimeInterval)OpusAudioPlayerSampleRate; -} - - -- (void)_notifyFinished -{ - id delegate = _delegate; - if ([delegate respondsToSelector:@selector(audioPlayerDidFinishPlaying:)]) - [delegate audioPlayerDidFinishPlaying:self]; -} - -- (void)_notifyStart -{ - id delegate = _delegate; - if ([delegate respondsToSelector:@selector(audioPlayerDidStartPlaying:)]) - [delegate audioPlayerDidStartPlaying:self]; -} - -- (void)_notifyPause -{ - id delegate = _delegate; - if ([delegate respondsToSelector:@selector(audioPlayerDidPause:)]) - [delegate audioPlayerDidPause:self]; -} - -@end diff --git a/Telegram-Mac/PCallSession.swift b/Telegram-Mac/PCallSession.swift index 09c7579f70..1db1d7c07b 100644 --- a/Telegram-Mac/PCallSession.swift +++ b/Telegram-Mac/PCallSession.swift @@ -7,202 +7,485 @@ // import Cocoa -import SwiftSignalKitMac -import TelegramCoreMac -import PostboxMac +import SwiftSignalKit +import TelegramCore + +import Postbox import TGUIKit +import TgVoipWebrtc +import CoreGraphics +import TelegramVoip enum CallTone { - case callToneUndefined - case callToneRingback - case callToneBusy - case callToneConnecting - case callToneFailed - case callToneEnded + case undefined + case ringback + case busy + case connecting + case failed + case ended + case ringing +} + +enum ScreenCaptureLaunchError { + case permission } +extension CallState.State { + func statusText(_ accountPeer: Peer?, _ videoState: CallState.VideoState) -> CallControllerStatusValue { + let statusValue: CallControllerStatusValue + switch self { + case .waiting, .connecting: + statusValue = .text(L10n.callStatusConnecting, nil) + case let .requesting(ringing): + if ringing { + statusValue = .text(L10n.callStatusRinging, nil) + } else { + statusValue = .text(L10n.callStatusRequesting, nil) + } + case .terminating: + statusValue = .text(L10n.callStatusEnded, nil) + case let .terminated(_, reason, _): + if let reason = reason { + switch reason { + case let .ended(type): + switch type { + case .busy: + statusValue = .text(L10n.callStatusBusy, nil) + case .hungUp, .missed: + statusValue = .text(L10n.callStatusEnded, nil) + } + case .error: + statusValue = .text(L10n.callStatusFailed, nil) + } + } else { + statusValue = .text(L10n.callStatusEnded, nil) + } + case .ringing: + if let accountPeer = accountPeer { + statusValue = .text(L10n.callStatusCallingAccount(accountPeer.addressName ?? accountPeer.compactDisplayTitle), nil) + } else { + statusValue = .text(L10n.callStatusCalling, nil) + } + case .active(let timestamp, let reception, _), .reconnecting(let timestamp, let reception, _): + if case .reconnecting = self { + statusValue = .text(L10n.callStatusConnecting, reception) + } else { + statusValue = .timer(timestamp, reception) + } + } + return statusValue + } +} + + + + + + +public struct CallAuxiliaryServer { + public enum Connection { + case stun + case turn(username: String, password: String) + } + + public let host: String + public let port: Int + public let connection: Connection + + public init( + host: String, + port: Int, + connection: Connection + ) { + self.host = host + self.port = port + self.connection = connection + } +} + struct CallState: Equatable { + enum State: Equatable { + case waiting + case ringing + case requesting(Bool) + case connecting(Data?) + case active(Double, Int32?, Data) + case reconnecting(Double, Int32?, Data) + case terminating + case terminated(CallId?, CallSessionTerminationReason?, Bool) + } + + + enum VideoState: Equatable { + case notAvailable + case inactive(Bool) + case active(Bool) + case paused(Bool) + + } + + + public enum RemoteAudioState: Equatable { + case active + case muted + } + + public enum RemoteBatteryLevel: Equatable { + case normal + case low + } + + + enum RemoteVideoState: Equatable { + case inactive + case active + case paused + } + + + let state: State + let videoState: VideoState + let remoteVideoState: RemoteVideoState + let isMuted: Bool + let isOutgoingVideoPaused: Bool + let remoteAspectRatio: Float + let remoteAudioState: RemoteAudioState + let remoteBatteryLevel: RemoteBatteryLevel + let isScreenCapture: Bool + init(state: State, videoState: VideoState, remoteVideoState: RemoteVideoState, isMuted: Bool, isOutgoingVideoPaused: Bool, remoteAspectRatio: Float, remoteAudioState: RemoteAudioState, remoteBatteryLevel: RemoteBatteryLevel, isScreenCapture: Bool) { + self.state = state + self.videoState = videoState + self.remoteVideoState = remoteVideoState + self.isMuted = isMuted + self.isOutgoingVideoPaused = isOutgoingVideoPaused + self.remoteAspectRatio = remoteAspectRatio + self.remoteAudioState = remoteAudioState + self.remoteBatteryLevel = remoteBatteryLevel + self.isScreenCapture = isScreenCapture + } +} -enum VoIPState : Int { - case waitInit = 1 - case waitInitAck = 2 - case established = 3 - case failed = 4 +private final class OngoingCallThreadLocalContextQueueImpl: NSObject, OngoingCallThreadLocalContextQueue, OngoingCallThreadLocalContextQueueWebrtc { + private let queue: Queue + + init(queue: Queue) { + self.queue = queue + + super.init() + } + + func dispatch(_ f: @escaping () -> Void) { + self.queue.async { + f() + } + } + + func dispatch(after seconds: Double, block f: @escaping () -> Void) { + self.queue.after(seconds, f) + } + + func isCurrent() -> Bool { + return self.queue.isCurrent() + } } + let callQueue = Queue(name: "VoIPQueue") -private var callSession:PCallSession? = nil -func pullCurrentSession(_ f:@escaping (PCallSession?)->Void) { - callQueue.async { - f(callSession) + +private func getAuxiliaryServers(appConfiguration: AppConfiguration) -> [CallAuxiliaryServer] { + guard let data = appConfiguration.data else { + return [] + } + guard let servers = data["rtc_servers"] as? [[String: Any]] else { + return [] } + var result: [CallAuxiliaryServer] = [] + for server in servers { + guard let host = server["host"] as? String else { + continue + } + guard let portString = server["port"] as? String else { + continue + } + guard let username = server["username"] as? String else { + continue + } + guard let password = server["password"] as? String else { + continue + } + guard let port = Int(portString) else { + continue + } + result.append(CallAuxiliaryServer( + host: host, + port: port, + connection: .stun + )) + result.append(CallAuxiliaryServer( + host: host, + port: port, + connection: .turn( + username: username, + password: password + ) + )) + } + return result } class PCallSession { let peerId:PeerId - let account:Account - let id:CallSessionInternalId + let account: Account + let sharedContext: SharedAccountContext + let internalId:CallSessionInternalId - private var contextRef: Unmanaged? + private(set) var peer: Peer? + private let peerDisposable = MetaDisposable() + private var sessionState: CallSession? + + private var ongoingContext: OngoingCallContext? + private var callContextState: OngoingCallContextState? + private var ongoingContextStateDisposable: Disposable? + private var reception: Int32? + private var requestedVideoAspect: Float? + private var receptionDisposable: Disposable? + + + private let serializedData: String? + private let dataSaving: VoiceCallDataSaving + private let derivedState: VoipDerivedState + private let proxyServer: ProxyServerSettings? + private let auxiliaryServers: [OngoingCallContext.AuxiliaryServer] + private let currentNetworkType: NetworkType + private let updatedNetworkType: Signal + + private let stateDisposable = MetaDisposable() private let timeoutDisposable = MetaDisposable() + private let devicesDisposable = MetaDisposable() + + private let sessionStateDisposable = MetaDisposable() + + private let statePromise:ValuePromise = ValuePromise() + private var presentationState: CallState? = nil + var state:Signal { + return statePromise.get() + } + private let audioLevelPromise: Promise = Promise(0) + var audioLevel:Signal { + return audioLevelPromise.get() + } + + private let canBeRemovedPromise = Promise(false) + private var didSetCanBeRemoved = false + public var canBeRemoved: Signal { + return self.canBeRemovedPromise.get() + } + + private let hungUpPromise = ValuePromise() + + private var activeTimestamp: Double? + - let state:Promise = Promise() - private(set) var isMute:Bool = false private var player:CallAudioPlayer? = nil private var playingRingtone:Bool = false private var startTime:Double = 0 private var callAcceptedTime:Double = 0 - private var tranmissionState:VoIPState = .waitInit private var completed: Bool = false - let durationPromise:Promise = Promise() - private var callSessionValue:CallSession? = nil - init(account:Account, peerId:PeerId, id: CallSessionInternalId) { + private let requestMicroAccessDisposable = MetaDisposable() + + + private let callSessionManager: CallSessionManager + + private var videoCapturer: OngoingCallVideoCapturer? + + let isOutgoing: Bool + private(set) var isVideo: Bool + private(set) var isVideoPossible: Bool + private let isVideoAvailable: Bool + private var videoIsForceDisabled: Bool + private(set) var isScreenCapture: Bool + private let enableStunMarking: Bool + private let enableTCP: Bool + public let preferredVideoCodec: String? + + + + private var callWasActive = false + private var videoWasActive = false + + private var previousVideoState: CallState.VideoState? + private var previousRemoteVideoState: CallState.RemoteVideoState? + private var previousRemoteAudioState: CallState.RemoteAudioState? + private var previousRemoteBatteryLevel: CallState.RemoteBatteryLevel? + + + private var delayMuteState: Bool? = nil + + private var droppedCall = false + private var dropCallKitCallTimer: SwiftSignalKit.Timer? + + private var remoteAspectRatio: Float = 0 + private var remoteBatteryLevel: CallState.RemoteBatteryLevel = .normal + private var remoteAudioState: CallState.RemoteAudioState = .active + + private var settingsDisposable: Disposable? + private var devicesContext: DevicesContext + + init(account: Account, sharedContext: SharedAccountContext, isOutgoing: Bool, peerId:PeerId, id: CallSessionInternalId, initialState:CallSession?, startWithVideo: Bool, isVideoPossible: Bool) { Queue.mainQueue().async { _ = globalAudio?.pause() } - - assert(callQueue.isCurrent()) - + self.account = account + self.sharedContext = sharedContext self.peerId = peerId - self.id = id + self.internalId = id + self.callSessionManager = account.callSessionManager + self.updatedNetworkType = account.networkType + self.isOutgoing = isOutgoing - let signal = account.callSessionManager.callState(internalId: id) |> deliverOnMainQueue |> beforeNext { [weak self] session in - self?.proccessState(session) - } + self.isScreenCapture = false + self.isVideo = initialState?.type == .video + self.isVideo = self.isVideo || startWithVideo - state.set(signal |> map {$0.state}) + let devices = AVCaptureDevice.devices(for: .video).filter({ $0.isConnected && !$0.isSuspended }) - callQueue.async { - callSession = self - let bridge = CallBridge() - - if let inputDeviceId = UserDefaults.standard.object(forKey: "call_inputDevice") as? String { - bridge.setCurrentInputDeviceId(inputDeviceId) - } - if let outputDeviceId = UserDefaults.standard.object(forKey: "call_outputDevice") as? String { - bridge.setCurrentOutputDeviceId(outputDeviceId) + self.isVideoPossible = isVideoPossible && !devices.isEmpty + + self.videoIsForceDisabled = !isVideoPossible + + let isVideoAvailable: Bool + if #available(OSX 10.14, *) { + let status = AVCaptureDevice.authorizationStatus(for: .video) + switch status { + case .notDetermined: + isVideoAvailable = true + case .authorized: + isVideoAvailable = true + case .denied: + isVideoAvailable = false + case .restricted: + isVideoAvailable = false + @unknown default: + isVideoAvailable = false } + } else { + isVideoAvailable = true + } + + self.isVideoAvailable = isVideoAvailable + + + + + let semaphore = DispatchSemaphore(value: 0) + var data: (PreferencesView, Peer?, VoiceCallSettings, ProxyServerSettings?, NetworkType)! + let _ = combineLatest( + account.postbox.preferencesView(keys: [PreferencesKeys.voipConfiguration, ApplicationSpecificPreferencesKeys.voipDerivedState, PreferencesKeys.appConfiguration]) + |> take(1), + account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peerId) + }, + voiceCallSettings(sharedContext.accountManager), + proxySettings(accountManager: sharedContext.accountManager) |> take(1), + account.networkType |> take(1) + ).start(next: { preferences, peer, voiceSettings, proxy, networkType in + data = (preferences, peer, voiceSettings, proxy.effectiveActiveServer, networkType) + semaphore.signal() + }) + semaphore.wait() + - - self.contextRef = Unmanaged.passRetained(bridge) - bridge.stateChangeHandler = { value in - callQueue.async { - if let state = VoIPState(rawValue: Int(value)) { - self.voipStateChanged(state) - } - } - } + let configuration = data.0.values[PreferencesKeys.voipConfiguration] as? VoipConfiguration ?? VoipConfiguration.defaultValue + let appConfiguration = data.0.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? AppConfiguration.defaultValue + let derivedState = data.0.values[ApplicationSpecificPreferencesKeys.voipDerivedState] as? VoipDerivedState ?? VoipDerivedState.default + + self.serializedData = configuration.serializedData + self.dataSaving = .never + self.derivedState = derivedState + self.proxyServer = data.3 + self.peer = data.1 + self.currentNetworkType = data.4 + self.devicesContext = sharedContext.devicesContext + self.enableStunMarking = false + self.enableTCP = false + self.preferredVideoCodec = nil + + if self.isVideo { + self.videoCapturer = OngoingCallVideoCapturer(devicesContext.currentCameraId ?? "") + self.statePromise.set(CallState(state: isOutgoing ? .waiting : .ringing, videoState: self.isVideoPossible ? .active(self.isVideoAvailable) : .notAvailable, remoteVideoState: .inactive, isMuted: self.isMuted, isOutgoingVideoPaused: self.isOutgoingVideoPaused, remoteAspectRatio: self.remoteAspectRatio, remoteAudioState: self.remoteAudioState, remoteBatteryLevel: self.remoteBatteryLevel, isScreenCapture: self.isScreenCapture)) + } else { + self.statePromise.set(CallState(state: isOutgoing ? .waiting : .ringing, videoState: .notAvailable, remoteVideoState: .inactive, isMuted: self.isMuted, isOutgoingVideoPaused: self.isOutgoingVideoPaused, remoteAspectRatio: self.remoteAspectRatio, remoteAudioState: self.remoteAudioState, remoteBatteryLevel: self.remoteBatteryLevel, isScreenCapture: self.isScreenCapture)) } - } - - private func voipStateChanged(_ state:VoIPState) { - switch state { - case .established: - if (startTime < Double.ulpOfOne) { - stopAudio() - startTime = CFAbsoluteTimeGetCurrent(); - durationPromise.set((.single(duration) |> deliverOnMainQueue) |> then (Signal<()->TimeInterval, Void>.single({[weak self] in return self?.duration ?? 0}) |> map {$0()} |> delay(1.01, queue: Queue.mainQueue()) |> restart)) + + self.auxiliaryServers = getAuxiliaryServers(appConfiguration: appConfiguration).map { server -> OngoingCallContext.AuxiliaryServer in + let mappedConnection: OngoingCallContext.AuxiliaryServer.Connection + switch server.connection { + case .stun: + mappedConnection = .stun + case let .turn(username, password): + mappedConnection = .turn(username: username, password: password) } - case .failed: - playTone(.callToneFailed) - discardCurrentCallWithReason(.error(.disconnected)) - default: - break + return OngoingCallContext.AuxiliaryServer( + host: server.host, + port: server.port, + connection: mappedConnection + ) } - } - - private func startTimeout(_ duration:TimeInterval, discardReason: CallSessionTerminationReason) { - timeoutDisposable.set((Signal.complete() |> delay(duration, queue: Queue.mainQueue())).start(completed: { [weak self] in - self?.discardCurrentCallWithReason(discardReason) - })) - } - - func inputDevices() -> Signal<[AudioDevice], Void> { - return Signal { [weak self] subscriber in - var cancel = false - self?.withContext { context in - if !cancel { - subscriber.putNext(context.inputDevices()) - subscriber.putCompletion() - } + + devicesDisposable.set(self.devicesContext.updater().start(next: { [weak self] values in + guard let `self` = self else { + return } - return ActionDisposable { - cancel = true + if isVideoAvailable, self.isVideo, !self.isOutgoingVideoPaused, let id = values.camera { + self.videoCapturer?.switchVideoInput(id) } - } - } - - func currentInputDeviceId()-> Signal { - return Signal { [weak self] subscriber in - var cancel = false - self?.withContext { context in - if !cancel { - subscriber.putNext(context.currentInputDeviceId()) - subscriber.putCompletion() - } + if let id = values.input { + self.ongoingContext?.switchAudioInput(id) } - return ActionDisposable { - cancel = true + if let id = values.output { + self.ongoingContext?.switchAudioOutput(id) } + })) + + + var callSessionState: Signal = .complete() + if let initialState = initialState { + callSessionState = .single(initialState) } - } - func currentOutputDeviceId()-> Signal { - return Signal { [weak self] subscriber in - var cancel = false - self?.withContext { context in - if !cancel { - subscriber.putNext(context.currentOutputDeviceId()) - subscriber.putCompletion() - } - } - return ActionDisposable { - cancel = true + callSessionState = callSessionState + |> then(callSessionManager.callState(internalId: id)) + + let signal = callSessionState |> deliverOn(callQueue) + + self.sessionStateDisposable.set(signal.start(next: { [weak self] sessionState in + if let strongSelf = self { + strongSelf.updateSessionState(sessionState: sessionState, callContextState: strongSelf.callContextState, reception: strongSelf.reception) } - } - } - func setCurrentInputDevice(_ device:AudioDevice) { - UserDefaults.standard.set(device.deviceId, forKey: "call_inputDevice") - UserDefaults.standard.synchronize() - withContext { context in - context.setCurrentInputDeviceId(device.deviceId); - } + })) + + } - func setCurrentOutputDevice(_ device:AudioDevice) { - UserDefaults.standard.set(device.deviceId, forKey: "call_outputDevice") - UserDefaults.standard.synchronize() - withContext { context in - context.setCurrentOutputDeviceId(device.deviceId); - } - } - func outputDevices() -> Signal<[AudioDevice], Void> { - return Signal { [weak self] subscriber in - var cancel = false - self?.withContext { context in - if !cancel { - subscriber.putNext(context.outputDevices()) - subscriber.putCompletion() - } - } - return ActionDisposable { - cancel = true - } - } + private func startTimeout(_ duration:TimeInterval, discardReason: CallSessionTerminationReason) { + timeoutDisposable.set((Signal.complete() |> delay(duration, queue: Queue.mainQueue())).start(completed: { [weak self] in + self?.discardCurrentCallWithReason(discardReason) + })) } + private func invalidateTimeout() { timeoutDisposable.set(nil) } @@ -234,102 +517,363 @@ class PCallSession { return 0.0; } - func stopTransmission() { - durationPromise.set(.complete()) - callQueue.async { - callSession = nil - self.contextRef?.release() - self.contextRef = nil - } + func stopTransmission(_ id: CallId?) { + ongoingContext?.stop(callId: id, sendDebugLogs: false, debugLogValue: Promise()) } func drop(_ reason:DropCallReason) { - account.callSessionManager.drop(internalId: id, reason: reason) + account.callSessionManager.drop(internalId: internalId, reason: reason, debugLog: .single(nil)) + } + private func acceptAfterAccess() { + callAcceptedTime = CFAbsoluteTimeGetCurrent() + account.callSessionManager.accept(internalId: internalId) } func acceptCallSession() { - callAcceptedTime = CFAbsoluteTimeGetCurrent() - account.callSessionManager.accept(internalId: id) + requestMicroAccessDisposable.set((requestMicrophonePermission() |> deliverOnMainQueue).start(next: { [weak self] access in + if access { + self?.acceptAfterAccess() + } else { + confirm(for: mainWindow, information: L10n.requestAccesErrorHaveNotAccessCall, okTitle: L10n.modalOK, cancelTitle: "", thridTitle: L10n.requestAccesErrorConirmSettings, successHandler: { [weak self] result in + switch result { + case .thrid: + openSystemSettings(.microphone) + default: + break + } + self?.drop(.hangUp) + }) + } + })) + } + + + private var isOutgoingVideoPaused: Bool = false + private var isMuted: Bool = false + func mute() { - isMute = true - withContext { context in - context.mute() + self.isMuted = true + ongoingContext?.setIsMuted(self.isMuted) + if ongoingContext == nil { + delayMuteState = true } } func unmute() { - isMute = false - withContext { context in - context.unmute() + self.isMuted = false + ongoingContext?.setIsMuted(self.isMuted) + if ongoingContext == nil { + delayMuteState = nil } } func toggleMute() { - self.isMute = !self.isMute - withContext { context in - if context.isMuted() { - context.unmute() - } else { - context.mute() - } + self.isMuted = !self.isMuted + ongoingContext?.setIsMuted(self.isMuted) + if let state = self.sessionState { + self.updateSessionState(sessionState: state, callContextState: self.callContextState, reception: self.reception) + } + if ongoingContext == nil { + delayMuteState = self.isMuted ? true : nil } } - private func proccessState(_ session: CallSession) { - self.callSessionValue = session + private func updateSessionState(sessionState: CallSession, callContextState: OngoingCallContextState?, reception: Int32?) { + if case .video = sessionState.type { + self.isVideo = true + } + let previous = self.sessionState + self.sessionState = sessionState + self.callContextState = callContextState + self.reception = reception - switch session.state { - case .active(let key, _, let connection): - playTone(.callToneConnecting) - - let cdata = TGCallConnection(key: key, keyHash: key, defaultConnection: TGCallConnectionDescription(identifier: connection.primary.id, ipv4: connection.primary.ip, ipv6: connection.primary.ipv6, port: connection.primary.port, peerTag: connection.primary.peerTag), alternativeConnections: connection.alternatives.map {TGCallConnectionDescription(identifier: $0.id, ipv4: $0.ip, ipv6: $0.ipv6, port: $0.port, peerTag: $0.peerTag)}) - - withContext { context in - context.startTransmissionIfNeeded(session.isOutgoing, connection: cdata) + + let presentationState: CallState? + + var wasActive = false + var wasTerminated = false + if let previous = previous { + switch previous.state { + case .active: + wasActive = true + case .terminated: + wasTerminated = true + default: + break } - invalidateTimeout() - case .ringing: - playRingtone() - case .requesting(let ringing): - if ringing { - playTone(.callToneRingback) - startTimeout(callReceiveTimeout, discardReason: .ended(.busy)) + } + + + self.remoteAspectRatio = callContextState?.remoteAspectRatio ?? 0 + + let mappedVideoState: CallState.VideoState + let mappedRemoteVideoState: CallState.RemoteVideoState + let mappedRemoteAudioState: CallState.RemoteAudioState + let mappedRemoteBatteryLevel: CallState.RemoteBatteryLevel + if let callContextState = callContextState { + switch callContextState.videoState { + case .notAvailable: + mappedVideoState = .notAvailable + case .active: + mappedVideoState = .active(self.isVideoAvailable) + case .inactive: + mappedVideoState = .inactive(self.isVideoAvailable) + case .paused: + mappedVideoState = .paused(self.isVideoAvailable) } - + switch callContextState.remoteVideoState { + case .inactive: + mappedRemoteVideoState = .inactive + case .active: + mappedRemoteVideoState = .active + case .paused: + mappedRemoteVideoState = .paused + } + switch callContextState.remoteAudioState { + case .active: + mappedRemoteAudioState = .active + case .muted: + mappedRemoteAudioState = .muted + } + switch callContextState.remoteBatteryLevel { + case .normal: + mappedRemoteBatteryLevel = .normal + case .low: + mappedRemoteBatteryLevel = .low + } + self.previousVideoState = mappedVideoState + self.previousRemoteVideoState = mappedRemoteVideoState + self.previousRemoteAudioState = mappedRemoteAudioState + self.previousRemoteBatteryLevel = mappedRemoteBatteryLevel + } else { + if let previousVideoState = self.previousVideoState { + mappedVideoState = previousVideoState + } else { + if self.videoIsForceDisabled { + mappedVideoState = .inactive(self.isVideoAvailable) + } else if self.isVideo { + mappedVideoState = .active(self.isVideoAvailable) + } else if self.isVideoPossible { + mappedVideoState = .inactive(self.isVideoAvailable) + } else { + mappedVideoState = .notAvailable + } + } + mappedRemoteVideoState = .inactive + if let previousRemoteAudioState = self.previousRemoteAudioState { + mappedRemoteAudioState = previousRemoteAudioState + } else { + mappedRemoteAudioState = .active + } + if let previousRemoteBatteryLevel = self.previousRemoteBatteryLevel { + mappedRemoteBatteryLevel = previousRemoteBatteryLevel + } else { + mappedRemoteBatteryLevel = .normal + } + } + + self.remoteAudioState = mappedRemoteAudioState + self.remoteBatteryLevel = mappedRemoteBatteryLevel + + switch sessionState.state { + case .ringing: + presentationState = CallState(state: .ringing, videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, isMuted: self.isMuted, isOutgoingVideoPaused: self.isOutgoingVideoPaused, remoteAspectRatio: remoteAspectRatio, remoteAudioState: self.remoteAudioState, remoteBatteryLevel: self.remoteBatteryLevel, isScreenCapture: self.isScreenCapture) + case .accepting: + self.callWasActive = true + presentationState = CallState(state: .connecting(nil), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, isMuted: self.isMuted, isOutgoingVideoPaused: self.isOutgoingVideoPaused, remoteAspectRatio: self.remoteAspectRatio, remoteAudioState: self.remoteAudioState, remoteBatteryLevel: self.remoteBatteryLevel, isScreenCapture: self.isScreenCapture) case .dropping: - invalidateTimeout() - stopAudio() + presentationState = CallState(state: .terminating, videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, isMuted: self.isMuted, isOutgoingVideoPaused: self.isOutgoingVideoPaused, remoteAspectRatio: self.remoteAspectRatio, remoteAudioState: self.remoteAudioState, remoteBatteryLevel: self.remoteBatteryLevel, isScreenCapture: self.isScreenCapture) + case let .terminated(id, reason, options): + presentationState = CallState(state: .terminated(id, reason, options.contains(.reportRating)), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, isMuted: self.isMuted, isOutgoingVideoPaused: self.isOutgoingVideoPaused, remoteAspectRatio: self.remoteAspectRatio, remoteAudioState: self.remoteAudioState, remoteBatteryLevel: self.remoteBatteryLevel, isScreenCapture: self.isScreenCapture) + case let .requesting(ringing): + presentationState = CallState(state: .requesting(ringing), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, isMuted: self.isMuted, isOutgoingVideoPaused: self.isOutgoingVideoPaused, remoteAspectRatio: self.remoteAspectRatio, remoteAudioState: self.remoteAudioState, remoteBatteryLevel: self.remoteBatteryLevel, isScreenCapture: self.isScreenCapture) + case let .active(_, _, keyVisualHash, _, _, _, _): + self.callWasActive = true + if let callContextState = callContextState { + switch callContextState.state { + case .initializing: + presentationState = CallState(state: .connecting(keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, isMuted: self.isMuted, isOutgoingVideoPaused: self.isOutgoingVideoPaused, remoteAspectRatio: self.remoteAspectRatio, remoteAudioState: self.remoteAudioState, remoteBatteryLevel: self.remoteBatteryLevel, isScreenCapture: self.isScreenCapture) + case .failed: + presentationState = nil + self.callSessionManager.drop(internalId: self.internalId, reason: .disconnect, debugLog: .single(nil)) + case .connected: + let timestamp: Double + if let activeTimestamp = self.activeTimestamp { + timestamp = activeTimestamp + } else { + timestamp = CFAbsoluteTimeGetCurrent() + self.activeTimestamp = timestamp + } + presentationState = CallState(state: .active(timestamp, reception, keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, isMuted: self.isMuted, isOutgoingVideoPaused: self.isOutgoingVideoPaused, remoteAspectRatio: self.remoteAspectRatio, remoteAudioState: self.remoteAudioState, remoteBatteryLevel: self.remoteBatteryLevel, isScreenCapture: self.isScreenCapture) + case .reconnecting: + let timestamp: Double + if let activeTimestamp = self.activeTimestamp { + timestamp = activeTimestamp + } else { + timestamp = CFAbsoluteTimeGetCurrent() + self.activeTimestamp = timestamp + } + presentationState = CallState(state: .reconnecting(timestamp, reception, keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, isMuted: self.isMuted, isOutgoingVideoPaused: self.isOutgoingVideoPaused, remoteAspectRatio: self.remoteAspectRatio, remoteAudioState: self.remoteAudioState, remoteBatteryLevel: self.remoteBatteryLevel, isScreenCapture: self.isScreenCapture) + } + } else { + presentationState = CallState(state: .connecting(keyVisualHash), videoState: mappedVideoState, remoteVideoState: mappedRemoteVideoState, isMuted: self.isMuted, isOutgoingVideoPaused: self.isOutgoingVideoPaused, remoteAspectRatio: self.remoteAspectRatio, remoteAudioState: self.remoteAudioState, remoteBatteryLevel: self.remoteBatteryLevel, isScreenCapture: self.isScreenCapture) + } + } + + switch sessionState.state { + case .requesting: break - case .terminated(let reason, let report): - stopTransmission() - invalidateTimeout() - switch reason { - case .error: - playTone(.callToneFailed) - default: - stopAudio() - } -// if let report = report { -// let account = self.account -// Queue.mainQueue().async { -// showModal(with: CallRatingModalViewController(account, report: report), for: mainWindow) -// } -// } - + case let .active(id, key, _, connections, maxLayer, version, allowsP2P): + if !wasActive { + let logName = "\(id.id)_\(id.accessHash)" + + let ongoingContext = OngoingCallContext(account: account, callSessionManager: self.callSessionManager, internalId: self.internalId, proxyServer: proxyServer, initialNetworkType: self.currentNetworkType, updatedNetworkType: self.updatedNetworkType, serializedData: self.serializedData, dataSaving: dataSaving, derivedState: self.derivedState, key: key, isOutgoing: sessionState.isOutgoing, video: self.videoCapturer, connections: connections, maxLayer: maxLayer, version: version, allowP2P: allowsP2P, enableTCP: self.enableTCP, enableStunMarking: self.enableStunMarking, logName: logName, preferredVideoCodec: self.preferredVideoCodec, audioInputDeviceId: self.devicesContext.currentMicroId) + self.ongoingContext = ongoingContext + + self.audioLevelPromise.set(ongoingContext.audioLevel) + + if let requestedVideoAspect = self.requestedVideoAspect { + ongoingContext.setRequestedVideoAspect(requestedVideoAspect) + } + + if let delayMuteState = self.delayMuteState { + self.delayMuteState = nil + if delayMuteState { + self.mute() + } else { + self.unmute() + } + } + + self.ongoingContextStateDisposable = (ongoingContext.state + |> deliverOnMainQueue).start(next: { [weak self] contextState in + if let strongSelf = self { + if let sessionState = strongSelf.sessionState { + strongSelf.updateSessionState(sessionState: sessionState, callContextState: contextState, reception: strongSelf.reception) + } else { + strongSelf.callContextState = contextState + } + } + }) + + self.receptionDisposable = (ongoingContext.reception + |> deliverOnMainQueue).start(next: { [weak self] reception in + if let strongSelf = self { + if let sessionState = strongSelf.sessionState { + strongSelf.updateSessionState(sessionState: sessionState, callContextState: strongSelf.callContextState, reception: reception) + } else { + strongSelf.reception = reception + } + } + }) + + } + case let .terminated(id, _, options): + if wasActive { + let debugLogValue = Promise() + self.ongoingContext?.stop(callId: id, sendDebugLogs: options.contains(.sendDebugLogs), debugLogValue: debugLogValue) + } default: - break + if wasActive { + let debugLogValue = Promise() + self.ongoingContext?.stop(debugLogValue: debugLogValue) + } + } + if case let .terminated(_, reason, _) = sessionState.state, !wasTerminated { + if !self.didSetCanBeRemoved { + if reason.recall { + + } else { + self.didSetCanBeRemoved = true + self.canBeRemovedPromise.set(.single(true) |> delay(1.6, queue: Queue.mainQueue())) + } + } + self.hungUpPromise.set(true) + if sessionState.isOutgoing { + if !self.droppedCall { + let dropCallKitCallTimer = SwiftSignalKit.Timer(timeout: 1.6, repeat: false, completion: { [weak self] in + if let strongSelf = self { + strongSelf.dropCallKitCallTimer = nil + if !strongSelf.droppedCall { + strongSelf.droppedCall = true + } + } + }, queue: Queue.mainQueue()) + self.dropCallKitCallTimer = dropCallKitCallTimer + dropCallKitCallTimer.start() + } + } + } + if let presentationState = presentationState { + self.statePromise.set(presentationState) + self.presentationState = presentationState + self.updateTone(presentationState, callContextState: callContextState, previous: previous) + } + + } + + private func updateTone(_ state: CallState, callContextState: OngoingCallContextState?, previous: CallSession?) { + var tone: CallTone? + if let callContextState = callContextState, case .reconnecting = callContextState.state { + tone = .connecting + } else if let previous = previous { + switch previous.state { + case .accepting, .active, .dropping, .requesting: + switch state.state { + case .connecting: + if case .requesting = previous.state { + tone = .ringback + } else { + tone = .connecting + } + case .requesting(true): + tone = .ringback + case let .terminated(_, reason, _): + if let reason = reason { + switch reason { + case let .ended(type): + switch type { + case .busy: + tone = .busy + case .hungUp, .missed: + tone = .ended + } + case .error: + tone = .failed + } + } + case .ringing: + tone = .ringing + default: + break + } + default: + break + } + } else if callContextState == nil && !isOutgoing { + tone = .ringing + } else if callContextState == nil && isOutgoing { + tone = .ringback + } + if let tone = tone { + playTone(tone) + } else { + stopTone() } } + + deinit { + peerDisposable.dispose() stateDisposable.dispose() drop(.disconnect) - let contextRef = self.contextRef - callQueue.async { - contextRef?.release() - } + sessionStateDisposable.dispose() + ongoingContextStateDisposable?.dispose() + settingsDisposable?.dispose() + devicesDisposable.dispose() } private func playRingtone() { @@ -341,28 +885,132 @@ class PCallSession { } - private func withContext(_ f: @escaping (CallBridge) -> Void) { - callQueue.async { - if let contextRef = self.contextRef { - let context = contextRef.takeUnretainedValue() - f(context) + public func requestVideo() { + if isVideoAvailable { + let requestVideo: Bool = self.videoCapturer == nil + if self.videoCapturer == nil { + let videoCapturer = OngoingCallVideoCapturer(devicesContext.currentCameraId ?? "") + self.videoCapturer = videoCapturer + self.videoIsForceDisabled = false + } + if self.isScreenCapture { + self.videoCapturer?.switchVideoInput(devicesContext.currentCameraId ?? "") + } + self.isScreenCapture = false + self.isOutgoingVideoPaused = false + + if let videoCapturer = self.videoCapturer, requestVideo { + self.ongoingContext?.requestVideo(videoCapturer) + } + if let state = self.sessionState { + self.updateSessionState(sessionState: state, callContextState: self.callContextState, reception: self.reception) + } +// setRequestedVideoAspect(Float(System.cameraAspectRatio)) + } + } + + public func enableScreenCapture(_ id: String) { + + let requestVideo: Bool = self.videoCapturer == nil + if self.videoCapturer == nil { + let videoCapturer = OngoingCallVideoCapturer(id) + self.videoCapturer = videoCapturer + self.videoIsForceDisabled = false + } + + self.isOutgoingVideoPaused = true + self.isScreenCapture = true + + if let videoCapturer = self.videoCapturer, requestVideo { + self.ongoingContext?.requestVideo(videoCapturer) + } + if !requestVideo { + self.videoCapturer?.switchVideoInput(id) + } +// setRequestedVideoAspect(Float(System.aspectRatio)) + + if let state = self.sessionState { + self.updateSessionState(sessionState: state, callContextState: self.callContextState, reception: self.reception) + } + } + public func disableScreenCapture() { + self.videoCapturer?.switchVideoInput(devicesContext.currentCameraId ?? "") + if let _ = self.videoCapturer { + self.videoCapturer = nil + self.ongoingContext?.disableVideo() + self.videoIsForceDisabled = true + } + self.isOutgoingVideoPaused = true + self.isScreenCapture = false + + if let state = self.sessionState { + self.updateSessionState(sessionState: state, callContextState: self.callContextState, reception: self.reception) + } + } + + private weak var captureSelectWindow: DesktopCapturerWindow? + + public func toggleScreenCapture() -> ScreenCaptureLaunchError? { + if !self.isScreenCapture { + if let captureSelectWindow = captureSelectWindow { + captureSelectWindow.orderFrontRegardless() + } else { + self.captureSelectWindow = presentDesktopCapturerWindow(mode: .screencast, select: { [weak self] source, value in + self?.enableScreenCapture(source.deviceIdKey()) + }, devices: sharedContext.devicesContext) } + } else { + self.disableScreenCapture() + } + return nil + } + + public func disableVideo() { + if let _ = self.videoCapturer { + self.videoCapturer = nil + self.ongoingContext?.disableVideo() + self.videoIsForceDisabled = true + } + self.isScreenCapture = false + self.isOutgoingVideoPaused = true + if let state = self.sessionState { + self.updateSessionState(sessionState: state, callContextState: self.callContextState, reception: self.reception) } } + + + public func setRequestedVideoAspect(_ aspect: Float) { + self.requestedVideoAspect = aspect + self.ongoingContext?.setRequestedVideoAspect(aspect) + } + - func hangUpCurrentCall() { - hangUpCurrentCall(false) + @discardableResult func hangUpCurrentCall() -> Signal { + return hangUpCurrentCall(false) } - func hangUpCurrentCall(_ external: Bool) { + func hangUpCurrentCall(_ external: Bool) -> Signal { completed = external var reason:CallSessionTerminationReason = .ended(.hungUp) - if let session = callSessionValue { + if let session = sessionState { if case .terminated = session.state { reason = session.isOutgoing ? .ended(.missed) : .ended(.busy) } } discardCurrentCallWithReason(reason) + + if callContextState == nil, let session = sessionState, session.isOutgoing { + self.didSetCanBeRemoved = true + self.canBeRemovedPromise.set(.single(true)) + } + return canBeRemovedPromise.get() + } + + func setToRemovableState() { + if !self.didSetCanBeRemoved { + self.didSetCanBeRemoved = true + } + self.canBeRemovedPromise.set(.single(true)) } private func discardCurrentCallWithReason(_ reason: CallSessionTerminationReason) { @@ -390,16 +1038,18 @@ class PCallSession { let path:String? switch tone { - case .callToneBusy: + case .busy: path = Bundle.main.path(forResource: "voip_busy", ofType:"caf") - case .callToneRingback: + case .ringback: path = Bundle.main.path(forResource: "voip_ringback", ofType:"caf") - case .callToneConnecting: + case .connecting: path = Bundle.main.path(forResource: "voip_connecting", ofType:"mp3") - case .callToneFailed: + case .failed: path = Bundle.main.path(forResource: "voip_fail", ofType:"caf") - case .callToneEnded: + case .ended: path = Bundle.main.path(forResource: "voip_end", ofType:"caf") + case .ringing: + path = Bundle.main.path(forResource: "opening", ofType:"m4a") default: path = nil; } @@ -410,33 +1060,30 @@ class PCallSession { } } - private func loopsForTone(_ tone:CallTone) -> Int - { - switch tone - { - case .callToneBusy: + private func loopsForTone(_ tone:CallTone) -> Int { + switch tone { + case .busy: return 3; - - case .callToneRingback: - return -1; - - case .callToneConnecting: - return -1; - - case .callToneFailed: - return 1; - - case .callToneEnded: - return 1; - + case .ringback: + return -1 + case .connecting: + return -1 + case .failed: + return 1 + case .ended: + return 1 + case .ringing: + return -1 default: - return 0; + return 0 } } private func playTone(_ tone:URL, loops:Int, completion:(()->Void)? = nil) { - self.player = CallAudioPlayer(tone, loops: loops, completion: completion) - self.player?.play() + if self.player?.tone.path != tone.path { + self.player = CallAudioPlayer(tone, loops: loops, completion: completion) + self.player?.play() + } } private func playTone(_ tone:CallTone) { @@ -445,12 +1092,20 @@ class PCallSession { } } - private func stopAudio() { + private func stopTone() { playingRingtone = false player?.stop() player = nil } + func makeIncomingVideoView(completion: @escaping (OngoingCallContextPresentationCallVideoView?) -> Void) { + self.ongoingContext?.makeIncomingVideoView(completion: completion) + } + + func makeOutgoingVideoView(completion: @escaping (OngoingCallContextPresentationCallVideoView?) -> Void) { + self.videoCapturer?.makeOutgoingVideoView(completion: completion) + } + } enum PCallResult { @@ -459,51 +1114,142 @@ enum PCallResult { case samePeer(PCallSession) } -func phoneCall(_ account:Account, peerId:PeerId, ignoreSame:Bool = false) -> Signal { - return Signal { subscriber in +func phoneCall(account: Account, sharedContext: SharedAccountContext, peerId:PeerId, ignoreSame:Bool = false, isVideo: Bool = false) -> Signal { + + let signal: Signal<(Bool, Bool?), NoError> + if isVideo { + signal = combineLatest(queue: .mainQueue(), requestMicrophonePermission(), requestCameraPermission() |> map(Optional.init)) + } else { + signal = combineLatest(queue: .mainQueue(), requestMicrophonePermission(), .single(nil)) + } + + + var isVideoPossible = account.postbox.transaction { transaction -> VideoCallsConfiguration in + let appConfiguration: AppConfiguration = transaction.getPreferencesEntry(key: PreferencesKeys.appConfiguration) as? AppConfiguration ?? AppConfiguration.defaultValue + return VideoCallsConfiguration(appConfiguration: appConfiguration) + } + |> map { callsConfiguration -> Bool in + let isVideoPossible: Bool + switch callsConfiguration.videoCallsSupport { + case .disabled: + isVideoPossible = isVideo + case .full: + isVideoPossible = true + case .onlyVideo: + isVideoPossible = isVideo + } + return isVideoPossible + } + + isVideoPossible = combineLatest(isVideoPossible, account.postbox.transaction { + ($0.getPeerCachedData(peerId: peerId) as? CachedUserData)?.videoCallsAvailable ?? true + }) |> map { + $0.0 && $0.1 + } + + let accounts = sharedContext.activeAccounts |> take(1) + + + return combineLatest(queue: .mainQueue(), signal, isVideoPossible, accounts) |> mapToSignal { values -> Signal in - assert(callQueue.isCurrent()) + let (microAccess, _) = values.0 + let isVideoPossible = values.1 + let activeAccounts = values.2 - if let session = callSession, session.peerId == peerId, !ignoreSame { - subscriber.putNext(.samePeer(session)) - subscriber.putCompletion() - } else { - let confirmation:Signal - if let session = callSession { - confirmation = account.postbox.loadedPeerWithId(peerId) |> mapToSignal { peer -> Signal<(new:Peer, previous:Peer), Void> in - return account.postbox.loadedPeerWithId(session.peerId) |> map {(new:peer, previous:$0)} - } |> mapToSignal { value in - return confirmSignal(for: mainWindow, header: tr(.callConfirmDiscardCurrentHeader), information: tr(.callConfirmDiscardCurrentDescription(value.previous.compactDisplayTitle, value.new.displayTitle))) + for account in activeAccounts.accounts { + if account.1.peerId == peerId { + alert(for: mainWindow, info: L10n.callSameDeviceError) + return .complete() + } + } + if microAccess { + return makeNewCallConfirmation(account: account, sharedContext: sharedContext, newPeerId: peerId, newCallType: .call, ignoreSame: ignoreSame) |> mapToSignal { value -> Signal in + if ignoreSame { + return .single(value) + } else { + return sharedContext.endCurrentCall() } - - } else { - confirmation = .single(true) + } |> mapToSignal { _ in + return account.callSessionManager.request(peerId: peerId, isVideo: isVideo, enableVideo: isVideoPossible) } - - return (confirmation |> filter {$0} |> map { _ in - callSession?.hangUpCurrentCall() - } |> mapToSignal { _ in return account.callSessionManager.request(peerId: peerId) } |> deliverOn(callQueue) ).start(next: { id in - subscriber.putNext(.success(PCallSession(account: account, peerId: peerId, id: id))) - subscriber.putCompletion() + |> deliverOn(callQueue) + |> map { id in + return .success(PCallSession(account: account, sharedContext: sharedContext, isOutgoing: true, peerId: peerId, id: id, initialState: nil, startWithVideo: isVideo, isVideoPossible: isVideoPossible)) + } + } else { + confirm(for: mainWindow, information: L10n.requestAccesErrorHaveNotAccessCall, okTitle: L10n.modalOK, cancelTitle: "", thridTitle: L10n.requestAccesErrorConirmSettings, successHandler: { result in + switch result { + case .thrid: + openSystemSettings(.microphone) + default: + break + } }) + return .complete() } - - return EmptyDisposable - - } |> runOn(callQueue) + } } -func _callSession() -> Signal { - return Signal { subscriber in - var cancel: Bool = false - pullCurrentSession({ session in - if !cancel { - subscriber.putNext(session) - subscriber.putCompletion() +enum CallConfirmationType { + case call + case voiceChat +} + +func makeNewCallConfirmation(account: Account, sharedContext: SharedAccountContext, newPeerId: PeerId, newCallType: CallConfirmationType, ignoreSame: Bool = false) -> Signal { + if sharedContext.hasActiveCall { + let currentCallType: CallConfirmationType + let currentPeerId: PeerId + let currentAccount: Account + if let session = sharedContext.bindings.callSession() { + currentPeerId = session.peerId + currentAccount = session.account + currentCallType = .call + } else if let groupCall = sharedContext.bindings.groupCall()?.call { + currentPeerId = groupCall.peerId + currentAccount = groupCall.account + currentCallType = .voiceChat + } else { + fatalError("wtf") + } + if ignoreSame, newPeerId == currentPeerId { + return .single(true) + } + let from = currentAccount.postbox.transaction { + return $0.getPeer(currentPeerId) + } + let to = account.postbox.transaction { + return $0.getPeer(newPeerId) + } + return combineLatest(from, to) |> map { (from: $0, to: $1) } + |> deliverOnMainQueue + |> mapToSignal { values in + let header: String + let text: String + switch currentCallType { + case .call: + header = L10n.callConfirmDiscardCallHeader + case .voiceChat: + header = L10n.callConfirmDiscardVoiceHeader + } + switch newCallType { + case .call: + switch currentCallType { + case .call: + text = L10n.callConfirmDiscardCallToCallText(values.from?.displayTitle ?? "", values.to?.displayTitle ?? "") + case .voiceChat: + text = L10n.callConfirmDiscardVoiceToCallText(values.from?.displayTitle ?? "", values.to?.displayTitle ?? "") + } + case .voiceChat: + switch currentCallType { + case .call: + text = L10n.callConfirmDiscardCallToVoiceText(values.from?.displayTitle ?? "", values.to?.displayTitle ?? "") + case .voiceChat: + text = L10n.callConfirmDiscardVoiceToVoiceText(values.from?.displayTitle ?? "", values.to?.displayTitle ?? "") + } } - }) - return ActionDisposable { - cancel = true + return confirmSignal(for: mainWindow, header: header, information: text, okTitle: L10n.modalYes, cancelTitle: L10n.modalCancel) |> filter { $0 } } + } else { + return .single(true) } } diff --git a/Telegram-Mac/PIPVideoWindow.swift b/Telegram-Mac/PIPVideoWindow.swift index 643b058ba4..79d094cd7f 100644 --- a/Telegram-Mac/PIPVideoWindow.swift +++ b/Telegram-Mac/PIPVideoWindow.swift @@ -9,28 +9,32 @@ import Cocoa import TGUIKit import AVKit -import SwiftSignalKitMac +import SwiftSignalKit + +private let pipFrameKey: String = "kPipFrameKey3" fileprivate class PIPVideoWindow: NSPanel { - fileprivate let playerView:AVPlayerView + fileprivate let playerView: VideoPlayerView private let rect:NSRect private let close:ImageButton = ImageButton() - private let openGallery:ImageButton = ImageButton() + private let gallery:ImageButton = ImageButton() fileprivate var forcePaused: Bool = false - fileprivate let item: MGalleryVideoItem + fileprivate let item: MGalleryItem fileprivate weak var _delegate: InteractionContentViewProtocol? - fileprivate let _contentInteractions:ChatMediaGalleryParameters? + fileprivate let _contentInteractions:ChatMediaLayoutParameters? fileprivate let _type: GalleryAppearType - init(_ player:AVPlayerView, item: MGalleryVideoItem, origin:NSPoint, delegate:InteractionContentViewProtocol? = nil, contentInteractions:ChatMediaGalleryParameters? = nil, type: GalleryAppearType) { - + fileprivate let viewer: GalleryViewer + private var hideAnimated: Bool = true + init(_ player: VideoPlayerView, item: MGalleryItem, viewer: GalleryViewer, origin:NSPoint, delegate:InteractionContentViewProtocol? = nil, contentInteractions:ChatMediaLayoutParameters? = nil, type: GalleryAppearType) { + self.viewer = viewer self._delegate = delegate self._contentInteractions = contentInteractions self._type = type - + player.isPip = true self.playerView = player self.rect = NSMakeRect(origin.x, origin.y, player.frame.width, player.frame.height) self.item = item - super.init(contentRect: rect, styleMask: [.closable, .borderless, .resizable, .nonactivatingPanel], backing: .buffered, defer: true) + super.init(contentRect: rect, styleMask: [.closable, .resizable, .nonactivatingPanel], backing: .buffered, defer: true) close.autohighlight = false @@ -47,60 +51,86 @@ fileprivate class PIPVideoWindow: NSPanel { close.layer?.opacity = 0.8 - openGallery.autohighlight = false - openGallery.set(image: #imageLiteral(resourceName: "Icon_PipOff").precomposed(NSColor.white.withAlphaComponent(0.9)), for: .Normal) + gallery.autohighlight = false + gallery.set(image: #imageLiteral(resourceName: "Icon_PipOff").precomposed(NSColor.white.withAlphaComponent(0.9)), for: .Normal) - openGallery.set(handler: { [weak self] _ in - self?._openGallery() + gallery.set(handler: { [weak self] _ in + self?.openGallery() }, for: .Click) - openGallery.setFrameSize(40,40) - - openGallery.layer?.cornerRadius = 20 - openGallery.style = ControlStyle(backgroundColor: .blackTransparent, highlightColor: .grayIcon) - openGallery.layer?.opacity = 0.8 - + gallery.setFrameSize(40,40) + gallery.layer?.cornerRadius = 20 + gallery.style = ControlStyle(backgroundColor: .blackTransparent, highlightColor: .grayIcon) + gallery.layer?.opacity = 0.8 + + self.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]; self.contentView?.wantsLayer = true; self.contentView?.layer?.cornerRadius = 4; - self.contentView?.layer?.backgroundColor = NSColor.clear.cgColor; + self.contentView?.layer?.backgroundColor = NSColor.random.cgColor; self.backgroundColor = .clear; player.autoresizingMask = [.width, .height]; player.setFrameOrigin(0,0) player.controlsStyle = .minimal - player.removeFromSuperview() self.contentView?.addSubview(player) self.contentView?.addSubview(close) - self.contentView?.addSubview(openGallery) + self.contentView?.addSubview(gallery) - + + self.level = .screenSaver self.isMovableByWindowBackground = true + + NotificationCenter.default.addObserver(self, selector: #selector(windowDidResized(_:)), name: NSWindow.didResizeNotification, object: self) + + } - + func hide() { + playerView.isPip = false + + if hideAnimated { + contentView?._change(opacity: 0, animated: true, duration: 0.1, timingFunction: .linear) + setFrame(NSMakeRect(frame.minX + (frame.width - 0) / 2, frame.minY + (frame.height - 0) / 2, 0, 0), display: true, animate: true) + } orderOut(nil) - playerView.player?.pause() window = nil } - func _openGallery() { + override func orderOut(_ sender: Any?) { + super.orderOut(sender) + window = nil + if playerView.controlsStyle != .floating { + playerView.player?.pause() + } + } + + func openGallery() { close.change(opacity: 0, removeOnCompletion: false) { [weak close] completed in close?.removeFromSuperview() } - openGallery.change(opacity: 0, removeOnCompletion: false) { [weak openGallery] completed in - openGallery?.removeFromSuperview() + gallery.change(opacity: 0, removeOnCompletion: false) { [weak gallery] completed in + gallery?.removeFromSuperview() } + playerView.controlsStyle = .floating setFrame(rect, display: true, animate: true) + hideAnimated = false hide() - showGalleryFromPip(item: item, delegate: _delegate, contentInteractions: _contentInteractions, type: _type) + showGalleryFromPip(item: item, gallery: self.viewer, delegate: _delegate, contentInteractions: _contentInteractions, type: _type) + } + + deinit { + if playerView.controlsStyle != .floating { + playerView.player?.pause() + } + NotificationCenter.default.removeObserver(self) } override func animationResizeTime(_ newFrame: NSRect) -> TimeInterval { @@ -108,76 +138,405 @@ fileprivate class PIPVideoWindow: NSPanel { } override func setFrame(_ frameRect: NSRect, display displayFlag: Bool, animate animateFlag: Bool) { - //let closePoint = NSMakePoint(10, frameRect.height - 50) - // let openPoint = NSMakePoint(closePoint.x + close.frame.width + 10, frameRect.height - 50) - - super.setFrame(frameRect, display: displayFlag, animate: animateFlag) } - - override func mouseMoved(with event: NSEvent) { - super.mouseMoved(with: event) - } + override func mouseEntered(with event: NSEvent) { super.mouseEntered(with: event) close.change(opacity: 1, animated: true) - openGallery.change(opacity: 1, animated: true) + gallery.change(opacity: 1, animated: true) } override func mouseExited(with event: NSEvent) { super.mouseExited(with: event) close.change(opacity: 0, animated: true) - openGallery.change(opacity: 0, animated: true) + gallery.change(opacity: 0, animated: true) + } + + @objc func windowDidResized(_ notification: Notification) { + let closePoint = NSMakePoint(10, frame.height - 50) + let openPoint = NSMakePoint(closePoint.x + close.frame.width + 10, frame.height - 50) + self.close.setFrameOrigin(closePoint) + self.gallery.setFrameOrigin(openPoint) + + } + + override var isResizable: Bool { + return true } override func makeKeyAndOrderFront(_ sender: Any?) { super.makeKeyAndOrderFront(sender) + Queue.mainQueue().justDispatch { if let screen = NSScreen.main { - let convert_s = self.playerView.frame.size.fitted(NSMakeSize(300, 300)) - self.minSize = convert_s - self.aspectRatio = convert_s + let savedRect: NSRect = NSMakeRect(0, 0, screen.frame.width * 0.3, screen.frame.width * 0.3) + let convert_s = self.playerView.frame.size.fitted(NSMakeSize(savedRect.width, savedRect.height)) + self.aspectRatio = convert_s + self.minSize = convert_s.aspectFilled(NSMakeSize(100, 100)) let closePoint = NSMakePoint(10, convert_s.height - 50) let openPoint = NSMakePoint(closePoint.x + self.close.frame.width + 10, convert_s.height - 50) self.close.change(pos: closePoint, animated: false) - self.openGallery.change(pos: openPoint, animated: false) - + self.gallery.change(pos: openPoint, animated: false) self.setFrame(NSMakeRect(screen.frame.maxX - convert_s.width - 30, screen.frame.maxY - convert_s.height - 50, convert_s.width, convert_s.height), display: true, animate: true) + } + } + } + + +} + +protocol PictureInPictureControl { + func pause() + func play() + func didEnter() + func didExit() + var view: NSView { get } + var isPictureInPicture: Bool { get } +} + + +private class PictureInpictureView : Control { + private let _window: Window + init(frame: NSRect, window: Window) { + _window = window + super.init(frame: frame) + autoresizesSubviews = true + } + + override func mouseEntered(with event: NSEvent) { + super.mouseEntered(with: event) + } + + override func mouseExited(with event: NSEvent) { + super.mouseEntered(with: event) + } + + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + override var window: NSWindow? { + set { + + } + get { + return _window + } + } +} +fileprivate class ModernPictureInPictureVideoWindow: NSPanel { + fileprivate let _window: Window + fileprivate let control: PictureInPictureControl + private let rect:NSRect + private let restoreRect: NSRect + fileprivate var forcePaused: Bool = false + fileprivate let item: MGalleryItem + fileprivate weak var _delegate: InteractionContentViewProtocol? + fileprivate let _contentInteractions:ChatMediaLayoutParameters? + fileprivate let _type: GalleryAppearType + fileprivate let viewer: GalleryViewer + fileprivate var eventLocalMonitor: Any? + fileprivate var eventGlobalMonitor: Any? + private var hideAnimated: Bool = true + private let lookAtMessageDisposable = MetaDisposable() + init(_ control: PictureInPictureControl, item: MGalleryItem, viewer: GalleryViewer, origin:NSPoint, delegate:InteractionContentViewProtocol? = nil, contentInteractions:ChatMediaLayoutParameters? = nil, type: GalleryAppearType) { + self.viewer = viewer + self._delegate = delegate + self._contentInteractions = contentInteractions + self._type = type + self.control = control + + let minSize = control.view.frame.size.aspectFilled(NSMakeSize(300, 300)) + // let difference = NSMakeSize(item.notFittedSize.width - item.sizeValue.width, item.notFittedSize.height - item.sizeValue.height) + let size = item.notFittedSize.aspectFilled(NSMakeSize(300, 300)).aspectFilled(minSize) + let newRect = NSMakeRect(origin.x, origin.y, size.width, size.height) + self.rect = newRect //NSMakeRect(origin.x, origin.y, control.view.frame.width, control.view.frame.height) + self.restoreRect = NSMakeRect(origin.x, origin.y, control.view.frame.width, control.view.frame.height) + self.item = item + _window = Window(contentRect: control.view.bounds, styleMask: [.resizable], backing: .buffered, defer: true) + super.init(contentRect: newRect, styleMask: [.resizable, .nonactivatingPanel], backing: .buffered, defer: true) + + //self.isOpaque = false + self.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]; + + + let view = PictureInpictureView(frame: bounds, window: _window) + self.contentView = view + + view.forceMouseDownCanMoveWindow = true + + // self.contentView?.wantsLayer = true; + self.contentView?.layer?.cornerRadius = 4; + + self.backgroundColor = .clear; + control.view.frame = NSMakeRect(0, 0, newRect.width, newRect.height) + // control.view.autoresizingMask = [.width, .height]; + + + control.view.setFrameOrigin(0, 0) + // contentView?.autoresizingMask = [.width, .height] + contentView?.addSubview(control.view) + + + _window.set(mouseHandler: { event -> KeyHandlerResult in + + return .invoked + }, with: self, for: .mouseMoved, priority: .low) + + _window.set(mouseHandler: { event -> KeyHandlerResult in + return .invoked + }, with: self, for: .mouseEntered, priority: .low) + + _window.set(mouseHandler: { event -> KeyHandlerResult in + return .invoked + }, with: self, for: .mouseExited, priority: .low) + + + _window.set(mouseHandler: { [weak self] event -> KeyHandlerResult in + if event.clickCount == 2, let strongSelf = self { + let inner = strongSelf.control.view.convert(event.locationInWindow, from: nil) + if NSWindow.windowNumber(at: NSEvent.mouseLocation, belowWindowWithWindowNumber: 0) == strongSelf.windowNumber, strongSelf.control.view.hitTest(inner) is MediaPlayerView { + strongSelf.hide() + } } + return .invoked + }, with: self, for: .leftMouseDown, priority: .low) + + + _window.set(mouseHandler: { [weak self] event -> KeyHandlerResult in + self?.findAndMoveToCorner() + return .rejected + }, with: self, for: .leftMouseUp, priority: .low) + + + self.level = .modalPanel + self.isMovableByWindowBackground = true + + NotificationCenter.default.addObserver(self, selector: #selector(windowDidResized(_:)), name: NSWindow.didResizeNotification, object: self) + + + + eventLocalMonitor = NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved, .mouseEntered, .mouseExited, .leftMouseDown, .leftMouseUp], handler: { [weak self] event in + guard let `self` = self else {return event} + self._window.sendEvent(event) + return event + }) + + eventGlobalMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved, .mouseEntered, .mouseExited, .leftMouseDown, .leftMouseUp], handler: { [weak self] event in + guard let `self` = self else {return} + self._window.sendEvent(event) + }) + + + if let message = item.entry.message { + let messageView = item.context.account.postbox.messageView(message.id) |> deliverOnMainQueue + lookAtMessageDisposable.set(messageView.start(next: { [weak self] view in + if view.message == nil { + self?.hideAnimated = true + self?.hide() + } + })) } } + + + func hide() { + if hideAnimated { + contentView?._change(opacity: 0, animated: true, duration: 0.1, timingFunction: .linear) + setFrame(NSMakeRect(frame.minX + (frame.width - 0) / 2, frame.minY + (frame.height - 0) / 2, 0, 0), display: true, animate: true) + } + + orderOut(nil) + window = nil + _window.removeAllHandlers(for: self) + if let monitor = eventLocalMonitor { + NSEvent.removeMonitor(monitor) + } + if let monitor = eventGlobalMonitor { + NSEvent.removeMonitor(monitor) + } + } + override func orderOut(_ sender: Any?) { + super.orderOut(sender) + window = nil + if control.isPictureInPicture { + control.pause() + } + } + + func openGallery() { + setFrame(restoreRect, display: true, animate: true) + hideAnimated = false + hide() + showGalleryFromPip(item: item, gallery: self.viewer, delegate: _delegate, contentInteractions: _contentInteractions, type: _type) + } + + deinit { + if control.isPictureInPicture { + control.pause() + } + NotificationCenter.default.removeObserver(self) + lookAtMessageDisposable.dispose() + } + + override func animationResizeTime(_ newFrame: NSRect) -> TimeInterval { + return 0.2 + } + + @objc func windowDidResized(_ notification: Notification) { + + } + + private func findAndMoveToCorner() { + if let screen = self.screen { + let rect = screen.frame.offsetBy(dx: -screen.visibleFrame.minX, dy: -screen.visibleFrame.minY) + + let point = self.frame.offsetBy(dx: -screen.visibleFrame.minX, dy: -screen.visibleFrame.minY) + + var options:BorderType = [] + + if point.maxX > rect.width && point.minX < rect.width { + options.insert(.Right) + } + + if point.minX < 0 { + options.insert(.Left) + } + + if point.minY < 0 { + options.insert(.Bottom) + } + + + var newFrame = self.frame + + if options.contains(.Right) { + newFrame.origin.x = screen.visibleFrame.maxX - newFrame.width - 30 + } + if options.contains(.Bottom) { + newFrame.origin.y = screen.visibleFrame.minY + 30 + } + if options.contains(.Left) { + newFrame.origin.x = screen.visibleFrame.minX + 30 + } + setFrame(newFrame, display: true, animate: true) + + +// switch alignment { +// case .topLeft: +// setFrame(NSMakeRect(30, 30, self.frame.width, self.frame.height), display: true, animate: true) +// case .topRight: +// setFrame(NSMakeRect(frame.width - self.frame.width - 30, 30, self.frame.width, self.frame.height), display: true, animate: true) +// case .bottomLeft: +// setFrame(NSMakeRect(30, frame.height - self.frame.height - 30, self.frame.width, self.frame.height), display: true, animate: true) +// case .bottomRight: +// setFrame(NSMakeRect(frame.width - self.frame.width - 30, frame.height - self.frame.height - 30, self.frame.width, self.frame.height), display: true, animate: true) +// } + } + } + + + override var isResizable: Bool { + return true + } + override func setFrame(_ frameRect: NSRect, display flag: Bool, animate animateFlag: Bool) { + super.setFrame(frameRect, display: flag, animate: animateFlag) + } + + override func makeKeyAndOrderFront(_ sender: Any?) { + super.makeKeyAndOrderFront(sender) + if let screen = NSScreen.main { + let savedRect: NSRect = NSMakeRect(0, 0, screen.frame.width * 0.3, screen.frame.width * 0.3) + let convert_s = self.rect.size.aspectFilled(NSMakeSize(min(savedRect.width, 250), savedRect.height)) + self.aspectRatio = self.rect.size.fitted(NSMakeSize(savedRect.width, savedRect.height)) + self.minSize = self.rect.size.aspectFitted(NSMakeSize(savedRect.width, savedRect.height)).aspectFilled(NSMakeSize(250, 250)) + + let frame = NSScreen.main?.frame ?? NSMakeRect(0, 0, 1920, 1080) + + self.maxSize = self.rect.size.fitted(NSMakeSize(savedRect.width, savedRect.height)).aspectFilled(NSMakeSize(frame.width / 3, frame.height / 3)) + + + self.setFrame(NSMakeRect(screen.frame.maxX - convert_s.width - 30, screen.frame.maxY - convert_s.height - 50, convert_s.width, convert_s.height), display: true, animate: true) + + } + } + + +} + + + +private var window: NSWindow? + + +var hasPictureInPicture: Bool { + return window != nil } -private var window: PIPVideoWindow? +func showLegacyPipVideo(_ playerView:VideoPlayerView, viewer: GalleryViewer, item: MGalleryItem, origin: NSPoint, delegate:InteractionContentViewProtocol? = nil, contentInteractions:ChatMediaLayoutParameters? = nil, type: GalleryAppearType) { + closePipVideo() + window = PIPVideoWindow(playerView, item: item, viewer: viewer, origin: origin, delegate: delegate, contentInteractions: contentInteractions, type: type) + window?.makeKeyAndOrderFront(nil) +} -func showPipVideo(_ player:AVPlayerView, item: MGalleryVideoItem, origin: NSPoint, delegate:InteractionContentViewProtocol? = nil, contentInteractions:ChatMediaGalleryParameters? = nil, type: GalleryAppearType) { - window = PIPVideoWindow(player, item: item, origin: origin, delegate: delegate, contentInteractions: contentInteractions, type: type) +func showPipVideo(control: PictureInPictureControl, viewer: GalleryViewer, item: MGalleryItem, origin: NSPoint, delegate:InteractionContentViewProtocol? = nil, contentInteractions:ChatMediaLayoutParameters? = nil, type: GalleryAppearType) { + closePipVideo() + window = ModernPictureInPictureVideoWindow(control, item: item, viewer: viewer, origin: origin, delegate: delegate, contentInteractions: contentInteractions, type: type) window?.makeKeyAndOrderFront(nil) } + +func exitPictureInPicture() { + if let window = window as? PIPVideoWindow { + window.openGallery() + } else if let window = window as? ModernPictureInPictureVideoWindow { + window.openGallery() + } +} + func pausepip() { - window?.playerView.player?.pause() - window?.forcePaused = true + if let window = window as? PIPVideoWindow { + window.playerView.player?.pause() + window.forcePaused = true + } else if let window = window as? ModernPictureInPictureVideoWindow { + window.control.pause() + window.forcePaused = true + } + } func playPipIfNeeded() { - if let forcePaused = window?.forcePaused, forcePaused { - window?.playerView.player?.play() + if let window = window as? PIPVideoWindow, window.forcePaused { + window.playerView.player?.play() + } else if let window = window as? ModernPictureInPictureVideoWindow, window.forcePaused { + window.control.play() } } func closePipVideo() { - window?.hide() + if let window = window as? PIPVideoWindow { + window.hide() + window.playerView.player?.pause() + } else if let window = window as? ModernPictureInPictureVideoWindow { + window.hide() + window.control.pause() + } window = nil + } diff --git a/Telegram-Mac/PamentsSelectMethodController.swift b/Telegram-Mac/PamentsSelectMethodController.swift new file mode 100644 index 0000000000..a337ffea58 --- /dev/null +++ b/Telegram-Mac/PamentsSelectMethodController.swift @@ -0,0 +1,119 @@ +// +// PamentsSelectMethodController.swift +// Telegram +// +// Created by Mikhail Filimonov on 01.03.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation + +import Cocoa +import TGUIKit +import SwiftSignalKit +import TelegramCore +import Postbox + + +private final class Arguments { + let context: AccountContext + let select:(BotPaymentSavedCredentials)->Void + let addNew:()->Void + init(context: AccountContext, select: @escaping(BotPaymentSavedCredentials)->Void, addNew:@escaping()->Void) { + self.context = context + self.select = select + self.addNew = addNew + } +} + +private struct State : Equatable { + var cards:[BotPaymentSavedCredentials] + var form: BotPaymentForm +} + +private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + for option in state.cards { + switch option { + case let .card(id: id, title: title): + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: InputDataIdentifier(id), data: .init(name: title, color: theme.colors.text, type: .context(""), viewType: bestGeneralViewType(state.cards, for: option), action: { + arguments.select(option) + }))) + index += 1 + } + } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: InputDataIdentifier("add_new"), data: .init(name: L10n.checkoutPaymentMethodNew, color: theme.colors.accent, type: .context(""), viewType: .singleItem, action: { + arguments.addNew() + }))) + index += 1 + + // entries + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func PamentsSelectMethodController(context: AccountContext, cards:[BotPaymentSavedCredentials], form: BotPaymentForm, select:@escaping(BotPaymentSavedCredentials)->Void, addNew: @escaping()->Void) -> InputDataModalController { + + var close:(()->Void)? = nil + + let actionsDisposable = DisposableSet() + + let initialState = State(cards: cards, form: form) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let arguments = Arguments(context: context, select: { option in + close?() + select(option) + }, addNew: { + close?() + addNew() + }) + + let signal = statePromise.get() |> deliverOnPrepareQueue |> map { state in + return InputDataSignalValue(entries: entries(state, arguments: arguments)) + } + + let controller = InputDataController(dataSignal: signal, title: L10n.checkoutPaymentMethodTitle) + + controller.onDeinit = { + actionsDisposable.dispose() + } + + let modalInteractions = ModalInteractions(acceptTitle: L10n.modalCancel, accept: { + close?() + }, drawBorder: true, height: 50, singleButton: true) + + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions) + + + close = { [weak modalController] in + modalController?.modal?.close() + } + + return modalController +} + + + + + diff --git a/Telegram-Mac/PanelUtils.swift b/Telegram-Mac/PanelUtils.swift index cd7181f23d..16e6787619 100644 --- a/Telegram-Mac/PanelUtils.swift +++ b/Telegram-Mac/PanelUtils.swift @@ -8,32 +8,33 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac +import SwiftSignalKit import Foundation -let mediaExts:[String] = ["png","jpg","jpeg","tiff","mp4","mov","avi", "gif"] -let photoExts:[String] = ["png","jpg","jpeg","tiff"] -let videoExts:[String] = ["mp4","mov","avi"] +import Postbox +import TelegramCore -func filePanel(with exts:[String]? = nil, allowMultiple:Bool = true, for window:Window, completion:@escaping ([String]?)->Void) { +let mediaExts:[String] = ["png","jpg","jpeg","tiff", "heic","mp4","mov","avi", "gif", "m4v"] +let photoExts:[String] = ["png","jpg","jpeg","tiff", "heic"] +let videoExts:[String] = ["mp4","mov","avi", "m4v"] +let audioExts:[String] = ["mp3","wav", "m4a"] + +func filePanel(with exts:[String]? = nil, allowMultiple:Bool = true, canChooseDirectories: Bool = false, for window:Window, completion:@escaping ([String]?)->Void) { var result:[String] = [] let panel:NSOpenPanel = NSOpenPanel() panel.canChooseFiles = true - - if let window = NSApp.window(withWindowNumber: panel.windowNumber) { - //window.appearance = theme.appearance - } + panel.canChooseDirectories = canChooseDirectories panel.canCreateDirectories = true panel.allowedFileTypes = exts - panel.allowsMultipleSelection = true + panel.allowsMultipleSelection = allowMultiple panel.beginSheetModal(for: window) { (response) in if response.rawValue == NSFileHandlingPanelOKButton { for url in panel.urls { let path:String = url.path if let exts = exts { let ext:String = path.nsstring.pathExtension.lowercased() - if exts.contains(ext) { + if exts.contains(ext) || (canChooseDirectories && path.isDirectory) { result.append(path) } } else { @@ -47,18 +48,73 @@ func filePanel(with exts:[String]? = nil, allowMultiple:Bool = true, for window: } } -func savePanel(file:String, ext:String, for window:Window) { +func selectFolder(for window:Window, completion:@escaping (String)->Void) { + var result:[String] = [] + let panel:NSOpenPanel = NSOpenPanel() + panel.canChooseFiles = false + panel.canChooseDirectories = true + panel.canCreateDirectories = true + panel.allowsMultipleSelection = false + panel.beginSheetModal(for: window) { (response) in + if response.rawValue == NSFileHandlingPanelOKButton { + for url in panel.urls { + let path:String = url.path + result.append(path) + } + if let first = result.first { + completion(first) + } + } + } +} + +func savePanel(file:String, ext:String, for window:Window, defaultName: String? = nil, completion:((String?)->Void)? = nil) { let savePanel:NSSavePanel = NSSavePanel() let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd HH.mm.ss" - savePanel.nameFieldStringValue = "\(dateFormatter.string(from: Date())).\(ext)" - savePanel.beginSheetModal(for: window, completionHandler: {(result) in + savePanel.nameFieldStringValue = defaultName ?? "\(dateFormatter.string(from: Date())).\(ext)" + let wLevel = window.level + // if wLevel == .screenSaver { + window.level = .normal + //} + + savePanel.begin { (result) in if result == NSApplication.ModalResponse.OK, let saveUrl = savePanel.url { + try? FileManager.default.removeItem(atPath: saveUrl.path) try? FileManager.default.copyItem(atPath: file, toPath: saveUrl.path) + completion?(saveUrl.path) + } else { + completion?(nil) } - }) + window.level = wLevel + } + +// savePanel.beginSheetModal(for: window, completionHandler: { [weak window] result in +// if result == NSApplication.ModalResponse.OK, let saveUrl = savePanel.url { +// try? FileManager.default.removeItem(atPath: saveUrl.path) +// try? FileManager.default.copyItem(atPath: file, toPath: saveUrl.path) +// completion?(saveUrl.path) +// } else { +// completion?(nil) +// } +// window?.level = wLevel +// }) + +// +// +// DispatchQueue.main.async { +// if let editor = savePanel.fieldEditor(false, for: nil) { +// let exportFilename = savePanel.nameFieldStringValue +// let ext = exportFilename.nsstring.pathExtension +// if !ext.isEmpty { +// let extensionLength = exportFilename.length - ext.length - 1 +// editor.selectedRange = NSMakeRange(0, extensionLength) +// } +// } +// } + } func savePanel(file:String, named:String, for window:Window) { @@ -73,17 +129,50 @@ func savePanel(file:String, named:String, for window:Window) { try? FileManager.default.copyItem(atPath: file, toPath: saveUrl.path) } }) + +// if let editor = savePanel.fieldEditor(false, for: nil) { +// let exportFilename = savePanel.nameFieldStringValue +// let ext = exportFilename.nsstring.pathExtension +// if !ext.isEmpty { +// let extensionLength = exportFilename.length - ext.length - 1 +// editor.selectedRange = NSMakeRange(0, extensionLength) +// } +// } + } -func alert(for window:Window, header:String = appName, info:String?) { +func alert(for window:Window, header:String = appName, info:String?, runModal: Bool = false, completion: (()->Void)? = nil, appearance: NSAppearance? = nil) { +// +// let alert = AlertController(window, header: header, text: info ?? "") +// alert.show(completionHandler: { response in +// completion?() +// }) + let alert:NSAlert = NSAlert() - alert.window.appearance = theme.appearance + alert.window.appearance = appearance ?? theme.appearance alert.alertStyle = .informational alert.messageText = header alert.informativeText = info ?? "" - alert.beginSheetModal(for: window, completionHandler: { (_) in}) + alert.addButton(withTitle: L10n.alertOK) + + +// alert.addButton(withTitle: L10n.alertCancel) +// alert.buttons.last?.wantsLayer = true +// alert.buttons.last?.layer?.opacity = 0 +// alert.buttons.last?.keyEquivalent = "\u{1b}" +// alert.buttons.last?.removeAllSubviews() +// alert.buttons.last?.focusRingType = .none + if runModal { + alert.runModal() + } else { + alert.beginSheetModal(for: window, completionHandler: { (_) in + completion?() + }) + } + + } func notSupported() { @@ -95,42 +184,188 @@ enum ConfirmResult { case basic } -func confirm(for window:Window, with header:String, and information:String?, okTitle:String? = nil, cancelTitle:String? = nil, thridTitle:String? = nil, successHandler:@escaping(ConfirmResult)->Void) { +func confirm(for window:Window, header: String? = nil, information:String?, okTitle:String? = nil, cancelTitle:String = L10n.alertCancel, thridTitle:String? = nil, fourTitle: String? = nil, successHandler:@escaping (ConfirmResult)->Void, cancelHandler: (()->Void)? = nil, appearance: NSAppearance? = nil) { + + let alert:NSAlert = NSAlert() - alert.window.appearance = theme.appearance + alert.window.appearance = appearance ?? theme.appearance alert.alertStyle = .informational - alert.messageText = header + alert.messageText = header ?? appName alert.informativeText = information ?? "" - alert.addButton(withTitle: okTitle ?? tr(.alertOK)) - alert.addButton(withTitle: cancelTitle ?? tr(.alertCancel)) + alert.addButton(withTitle: okTitle ?? L10n.alertOK) + if !cancelTitle.isEmpty { + alert.addButton(withTitle: cancelTitle) + alert.buttons.last?.keyEquivalent = "\u{1b}" + } + + if let thridTitle = thridTitle { alert.addButton(withTitle: thridTitle) } + if let fourTitle = fourTitle { + alert.addButton(withTitle: fourTitle) + } + + + + alert.beginSheetModal(for: window, completionHandler: { response in + Queue.mainQueue().justDispatch { + if response.rawValue == 1000 { + successHandler(.basic) + } else if response.rawValue == 1002 { + successHandler(.thrid) + } else if response.rawValue == 1001, cancelTitle == "" { + successHandler(.thrid) + } else if response.rawValue == 1001 { + cancelHandler?() + } + } + }) +} + +func modernConfirm(for window:Window, account: Account? = nil, peerId: PeerId? = nil, header: String = appName, information:String? = nil, okTitle:String = L10n.alertOK, cancelTitle:String = L10n.alertCancel, thridTitle:String? = nil, thridAutoOn: Bool = true, successHandler:@escaping(ConfirmResult)->Void, appearance: NSAppearance? = nil) { + // + + let alert:NSAlert = NSAlert() + alert.window.appearance = appearance ?? theme.appearance + alert.alertStyle = .informational + alert.messageText = header + alert.informativeText = information ?? "" + alert.addButton(withTitle: okTitle) + alert.addButton(withTitle: cancelTitle) + + + + if let thridTitle = thridTitle { + alert.showsSuppressionButton = true + alert.suppressionButton?.title = thridTitle + alert.suppressionButton?.state = thridAutoOn ? .on : .off + // alert.addButton(withTitle: thridTitle) + } - alert.beginSheetModal(for: window, completionHandler: { (response) in + let signal: Signal + if let peerId = peerId, let account = account { + signal = account.postbox.loadedPeerWithId(peerId) |> map(Optional.init) |> deliverOnMainQueue + } else { + signal = .single(nil) + } + + var disposable: Disposable? + + var shown: Bool = false + + let readyToShow:() -> Void = { + if !shown { + shown = true + alert.beginSheetModal(for: window, completionHandler: { [weak alert] response in + disposable?.dispose() + if let alert = alert { + if alert.showsSuppressionButton, let button = alert.suppressionButton, response.rawValue != 1001 { + switch button.state { + case .off: + successHandler(.basic) + case .on: + successHandler(.thrid) + default: + break + } + } else { + if response.rawValue == 1000 { + successHandler(.basic) + } else if response.rawValue == 1002 { + successHandler(.thrid) + } + } + } + }) + } - if response.rawValue == 1000 { - successHandler(.basic) - } else if response.rawValue == 1002 { - successHandler(.thrid) + } + + _ = signal.start(next: { peer in + if let peer = peer, let account = account { + alert.messageText = header.isEmpty || header == appName ? (account.peerId == peer.id ? L10n.peerSavedMessages : peer.displayTitle) : header + alert.icon = nil + if peerId == account.peerId { + let icon = theme.icons.searchSaved + let signal = generateEmptyPhoto(NSMakeSize(70, 70), type: .icon(colors: theme.colors.peerColors(5), icon: icon, iconSize: icon.backingSize.aspectFitted(NSMakeSize(50, 50)), cornerRadius: nil)) |> deliverOnMainQueue + disposable = signal.start(next: { image in + if let image = image { + alert.icon = NSImage(cgImage: image, size: NSMakeSize(70, 70)) + delay(0.2, closure: { + readyToShow() + }) + } + }) + + } else { + disposable = (peerAvatarImage(account: account, photo: PeerPhoto.peer(peer, peer.smallProfileImage, peer.displayLetters, nil), displayDimensions: NSMakeSize(70, 70), scale: System.backingScale, font: .avatar(30), genCap: true) |> deliverOnMainQueue).start(next: { image, _ in + if let image = image { + alert.icon = NSImage(cgImage: image, size: NSMakeSize(70, 70)) + delay(0.2, closure: { + readyToShow() + }) + } + }) + } + + } else { + readyToShow() } }) + + + +// let alert = AlertController(window, account: account, peerId: peerId, header: header, text: information, okTitle: okTitle, cancelTitle: cancelTitle, thridTitle: thridTitle, accessory: accessory) +// +// alert.show(completionHandler: { response in +// switch response { +// case .OK: +// successHandler(.basic) +// case .alertThirdButtonReturn: +// successHandler(.thrid) +// default: +// break +// } +// }) + +} +func modernConfirmSignal(for window:Window, account: Account?, peerId: PeerId?, header: String = appName, information:String? = nil, okTitle:String = L10n.alertOK, cancelTitle:String = L10n.alertCancel, thridTitle: String? = nil, thridAutoOn: Bool = true) -> Signal { + let value:ValuePromise = ValuePromise(ignoreRepeated: true) + + Queue.mainQueue().async { + modernConfirm(for: window, account: account, peerId: peerId, header: header, information: information, okTitle: okTitle, cancelTitle: cancelTitle, thridTitle: thridTitle, thridAutoOn: thridAutoOn, successHandler: { response in + value.set(response) + }) + } + return value.get() |> take(1) + } -func confirmSignal(for window:Window, header:String, information:String?, okTitle:String? = nil, cancelTitle:String? = nil) -> Signal { +func confirmSignal(for window:Window, header: String? = nil, information:String?, okTitle:String? = nil, cancelTitle:String? = nil, appearance: NSAppearance? = nil) -> Signal { +// let value:ValuePromise = ValuePromise(ignoreRepeated: true) +// +// Queue.mainQueue().async { +// let alert = AlertController(window, header: header ?? appName, text: information ?? "", okTitle: okTitle, cancelTitle: cancelTitle ?? tr(L10n.alertCancel), swapColors: swapColors) +// alert.show(completionHandler: { response in +// value.set(response == .OK) +// }) +// } +// return value.get() |> take(1) + let value:ValuePromise = ValuePromise(ignoreRepeated: true) Queue.mainQueue().async { let alert:NSAlert = NSAlert() alert.alertStyle = .informational - alert.messageText = header - alert.window.appearance = theme.appearance + alert.messageText = header ?? appName + alert.window.appearance = appearance ?? theme.appearance alert.informativeText = information ?? "" - alert.addButton(withTitle: okTitle ?? tr(.alertOK)) - alert.addButton(withTitle: cancelTitle ?? tr(.alertCancel)) + alert.addButton(withTitle: okTitle ?? tr(L10n.alertOK)) + alert.addButton(withTitle: cancelTitle ?? tr(L10n.alertCancel)) alert.beginSheetModal(for: window, completionHandler: { response in value.set(response.rawValue == 1000) diff --git a/Telegram-Mac/ParseAppearanceColors.swift b/Telegram-Mac/ParseAppearanceColors.swift new file mode 100644 index 0000000000..2e97ba20d5 --- /dev/null +++ b/Telegram-Mac/ParseAppearanceColors.swift @@ -0,0 +1,614 @@ +// +// ParseAppearanceColors.swift +// Telegram +// +// Created by keepcoder on 01/12/2017. +// Copyright © 2017 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + + +private let colors: [UInt32: String] = [ + 0x8e0000: "Berry", + 0xdec196: "Brandy", + 0x800b47: "Cherry", + 0xff7f50: "Coral", + 0xdb5079: "Cranberry", + 0xdc143c: "Crimson", + 0xe0b0ff: "Mauve", + 0xffc0cb: "Pink", + 0xff0000: "Red", + 0xff007f: "Rose", + 0x80461b: "Russet", + 0xff2400: "Scarlet", + 0xf1f1f1: "Seashell", + 0xff3399: "Strawberry", + 0xffbf00: "Amber", + 0xeb9373: "Apricot", + 0xfbe7b2: "Banana", + 0xa1c50a: "Citrus", + 0xb06500: "Ginger", + 0xffd700: "Gold", + 0xfde910: "Lemon", + 0xffa500: "Orange", + 0xffe5b4: "Peach", + 0xff6b53: "Persimmon", + 0xe4d422: "Sunflower", + 0xf28500: "Tangerine", + 0xffc87c: "Topaz", + 0xffff00: "Yellow", + 0x384910: "Clover", + 0x83aa5d: "Cucumber", + 0x50c878: "Emerald", + 0xb5b35c: "Olive", + 0x00ff00: "Green", + 0x00a86b: "Jade", + 0x29ab87: "Jungle", + 0xbfff00: "Lime", + 0x0bda51: "Malachite", + 0x98ff98: "Mint", + 0xaddfad: "Moss", + 0x315ba1: "Azure", + 0x0000ff: "Blue", + 0x0047ab: "Cobalt", + 0x4f69c6: "Indigo", + 0x017987: "Lagoon", + 0x71d9e2: "Aquamarine", + 0x120a8f: "Ultramarine", + 0x000080: "Navy", + 0x2f519e: "Sapphire", + 0x76d7ea: "Sky", + 0x008080: "Teal", + 0x40e0d0: "Turquoise", + 0x9966cc: "Amethyst", + 0x4d0135: "Blackberry", + 0x614051: "Eggplant", + 0xc8a2c8: "Lilac", + 0xb57edc: "Lavender", + 0xccccff: "Periwinkle", + 0x843179: "Plum", + 0x660099: "Purple", + 0xd8bfd8: "Thistle", + 0xda70d6: "Orchid", + 0x240a40: "Violet", + 0x3f2109: "Bronze", + 0x370202: "Chocolate", + 0x7b3f00: "Cinnamon", + 0x301f1e: "Cocoa", + 0x706555: "Coffee", + 0x796989: "Rum", + 0x4e0606: "Mahogany", + 0x782d19: "Mocha", + 0xc2b280: "Sand", + 0x882d17: "Sienna", + 0x780109: "Maple", + 0xf0e68c: "Khaki", + 0xb87333: "Copper", + 0xb94e48: "Chestnut", + 0xeed9c4: "Almond", + 0xfffdd0: "Cream", + 0xb9f2ff: "Diamond", + 0xa98307: "Honey", + 0xfffff0: "Ivory", + 0xeae0c8: "Pearl", + 0xeff2f3: "Porcelain", + 0xd1bea8: "Vanilla", + 0xffffff: "White", + 0x808080: "Gray", + 0x000000: "Black", + 0xe8f1d4: "Chrome", + 0x36454f: "Charcoal", + 0x0c0b1d: "Ebony", + 0xc0c0c0: "Silver", + 0xf5f5f5: "Smoke", + 0x262335: "Steel", + 0x4fa83d: "Apple", + 0x80b3c4: "Glacier", + 0xfebaad: "Melon", + 0xc54b8c: "Mulberry", + 0xa9c6c2: "Opal", + 0x54a5f8: "Blue" +] + +private let adjectives = [ + "Ancient", + "Antique", + "Autumn", + "Baby", + "Barely", + "Baroque", + "Blazing", + "Blushing", + "Bohemian", + "Bubbly", + "Burning", + "Buttered", + "Classic", + "Clear", + "Cool", + "Cosmic", + "Cotton", + "Cozy", + "Crystal", + "Dark", + "Daring", + "Darling", + "Dawn", + "Dazzling", + "Deep", + "Deepest", + "Delicate", + "Delightful", + "Divine", + "Double", + "Downtown", + "Dreamy", + "Dusky", + "Dusty", + "Electric", + "Enchanted", + "Endless", + "Evening", + "Fantastic", + "Flirty", + "Forever", + "Frigid", + "Frosty", + "Frozen", + "Gentle", + "Heavenly", + "Hyper", + "Icy", + "Infinite", + "Innocent", + "Instant", + "Luscious", + "Lunar", + "Lustrous", + "Magic", + "Majestic", + "Mambo", + "Midnight", + "Millenium", + "Morning", + "Mystic", + "Natural", + "Neon", + "Night", + "Opaque", + "Paradise", + "Perfect", + "Perky", + "Polished", + "Powerful", + "Rich", + "Royal", + "Sheer", + "Simply", + "Sizzling", + "Solar", + "Sparkling", + "Splendid", + "Spicy", + "Spring", + "Stellar", + "Sugared", + "Summer", + "Sunny", + "Super", + "Sweet", + "Tender", + "Tenacious", + "Tidal", + "Toasted", + "Totally", + "Tranquil", + "Tropical", + "True", + "Twilight", + "Twinkling", + "Ultimate", + "Ultra", + "Velvety", + "Vibrant", + "Vintage", + "Virtual", + "Warm", + "Warmest", + "Whipped", + "Wild", + "Winsome" +] + +private let subjectives = [ + "Ambrosia", + "Attack", + "Avalanche", + "Blast", + "Bliss", + "Blossom", + "Blush", + "Burst", + "Butter", + "Candy", + "Carnival", + "Charm", + "Chiffon", + "Cloud", + "Comet", + "Delight", + "Dream", + "Dust", + "Fantasy", + "Flame", + "Flash", + "Fire", + "Freeze", + "Frost", + "Glade", + "Glaze", + "Gleam", + "Glimmer", + "Glitter", + "Glow", + "Grande", + "Haze", + "Highlight", + "Ice", + "Illusion", + "Intrigue", + "Jewel", + "Jubilee", + "Kiss", + "Lights", + "Lollypop", + "Love", + "Luster", + "Madness", + "Matte", + "Mirage", + "Mist", + "Moon", + "Muse", + "Myth", + "Nectar", + "Nova", + "Parfait", + "Passion", + "Pop", + "Rain", + "Reflection", + "Rhapsody", + "Romance", + "Satin", + "Sensation", + "Silk", + "Shine", + "Shadow", + "Shimmer", + "Sky", + "Spice", + "Star", + "Sugar", + "Sunrise", + "Sunset", + "Sun", + "Twist", + "Unbound", + "Velvet", + "Vibrant", + "Waters", + "Wine", + "Wink", + "Wonder", + "Zone" +] + +private extension NSColor { + var colorComponents: (r: Int32, g: Int32, b: Int32) { + var r: CGFloat = 0.0 + var g: CGFloat = 0.0 + var b: CGFloat = 0.0 + self.getRed(&r, green: &g, blue: &b, alpha: nil) + return (Int32(max(0.0, r) * 255.0), Int32(max(0.0, g) * 255.0), Int32(max(0.0, b) * 255.0)) + } + + func distance(to other: NSColor) -> Int32 { + let e1 = self.colorComponents + let e2 = other.colorComponents + let rMean = (e1.r + e2.r) / 2 + let r = e1.r - e2.r + let g = e1.g - e2.g + let b = e1.b - e2.b + return ((512 + rMean) * r * r) >> 8 + 4 * g * g + ((767 - rMean) * b * b) >> 8 + } +} + + +private func generateThemeName(_ accentColor: NSColor) -> String { + var nearest: (color: UInt32, distance: Int32)? + for (color, _) in colors { + let distance = accentColor.distance(to: NSColor(rgb: color)) + if let currentNearest = nearest { + if distance < currentNearest.distance { + nearest = (color, distance) + } + } else { + nearest = (color, distance) + } + } + + if let color = nearest?.color, let colorName = colors[color]?.capitalized { + return "\(adjectives[Int(arc4random()) % adjectives.count].capitalized) \(colorName)" + } else { + return "" + } +} + + +func importPalette(_ path: String) -> ColorPalette? { + if let fs = fs(path), fs <= 30 * 1014 * 1024, let data = try? String(contentsOf: URL(fileURLWithPath: path)) { + let lines = data.components(separatedBy: "\n").filter({!$0.isEmpty}) + + var isDark: Bool = false + var tinted: Bool = false + var paletteName: String? = nil + var copyright: String = "Telegram" + var wallpaper: PaletteWallpaper? + var parent: TelegramBuiltinTheme = .dayClassic + var accentList:[PaletteAccentColor] = [] + var colors:[String: NSColor] = [:] + var bubbleBackground_outgoing:[NSColor] = [] + /* + else if name == "accentList" { + accentList = value.components(separatedBy: ",").compactMap { NSColor(hexString: $0) } + } + */ + + for line in lines { + if !line.trimmed.hasPrefix("//") { + var components = line.components(separatedBy: "=") + if components.count > 2 { + components[1] = components[1..Void)? = nil) -> Void { + let string = palette.toString + let temp = NSTemporaryDirectory() + "tmac.palette" + try? string.write(to: URL(fileURLWithPath: temp), atomically: true, encoding: .utf8) + savePanel(file: temp, ext: "palette", for: mainWindow, defaultName: "\(palette.name).palette", completion: completion) +} + + + +func findBestNameForPalette(_ palette: ColorPalette) -> String { + return generateThemeName(palette.accent) +} + + +// +// let readList = Bundle.main.path(forResource: "theme-names-tree", ofType: nil) +// +// if let readList = readList, let string = try? String(contentsOfFile: readList) { +// let lines = string.components(separatedBy: "\n") +// +// var list:[(String, NSColor)] = [] +// +// for line in lines { +// let value = line.components(separatedBy: "=") +// if value.count == 2 { +// if let color = NSColor(hexString: value[1]) { +// let name = value[0].components(separatedBy: " ").map({ $0.capitalizingFirstLetter() }).joined(separator: " ") +// list.append((name, color)) +// } +// } +// } +// +// if list.count > 0 { +// +// let first = pow(palette.accent.hsv.0 - list[0].1.hsv.0, 2) + pow(palette.accent.hsv.1 - list[0].1.hsv.1, 2) + pow(palette.accent.hsv.2 - list[0].1.hsv.2, 2) +// var closest: (Int, CGFloat) = (0, first) +// +// +// for i in 0 ..< list.count { +// let distance = pow(palette.accent.hsv.0 - list[i].1.hsv.0, 2) + pow(palette.accent.hsv.1 - list[i].1.hsv.1, 2) + pow(palette.accent.hsv.2 - list[i].1.hsv.2, 2) +// if distance < closest.1 { +// closest = (i, distance) +// } +// } +// +// if let animalsPath = Bundle.main.path(forResource: "animals", ofType: nil), let string = try? String(contentsOfFile: animalsPath) { +// let animals = string.components(separatedBy: "\n").filter { !$0.isEmpty } +// let animal = animals[Int(arc4random()) % animals.count].capitalizingFirstLetter() +// return list[closest.0].0 + " " + animal +// } +// return list[closest.0].0 +// } +// +// } +// +// return palette.name + diff --git a/Telegram-Mac/PasscodeControllers.swift b/Telegram-Mac/PasscodeControllers.swift new file mode 100644 index 0000000000..66f18c8a9a --- /dev/null +++ b/Telegram-Mac/PasscodeControllers.swift @@ -0,0 +1,297 @@ +// +// PasscodeControllers.swift +// Telegram +// +// Created by Mikhail Filimonov on 06/03/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import Postbox +import TelegramCore + +import SwiftSignalKit + + +enum PasscodeMode : Equatable { + case install + case change + case disable +} + +private struct PasscodeState : Equatable { + let mode: PasscodeMode + let data: [InputDataIdentifier : InputDataValue] + let errors: [InputDataIdentifier : InputDataValueError] + init(mode: PasscodeMode, data: [InputDataIdentifier : InputDataValue], errors: [InputDataIdentifier : InputDataValueError]) { + self.mode = mode + self.data = data + self.errors = errors + } + + func withUpdatedError(_ error: InputDataValueError?, for key: InputDataIdentifier) -> PasscodeState { + var errors = self.errors + if let error = error { + errors[key] = error + } else { + errors.removeValue(forKey: key) + } + return PasscodeState(mode: self.mode, data: self.data, errors: errors) + } + + func withUpdatedValue(_ value: InputDataValue, for key: InputDataIdentifier) -> PasscodeState { + var data = self.data + data[key] = value + return PasscodeState(mode: self.mode, data: data, errors: self.errors) + } +} + +private let _id_input_new_passcode = InputDataIdentifier("_id_input_new_passcode") +private let _id_input_re_new_passcode = InputDataIdentifier("_id_input_re_new_passcode") + +private let _id_input_current = InputDataIdentifier("_id_input_current") + +private func passcodeEntries(_ state: PasscodeState) -> [InputDataEntry] { + var entries: [InputDataEntry] = [] + + var sectionId: Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + switch state.mode { + case .install: + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.passcodeControllerHeaderNew), data: InputDataGeneralTextData(viewType: .textTopItem))) + index += 1 + + + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: .string(state.data[_id_input_new_passcode]?.stringValue), error: state.errors[_id_input_new_passcode], identifier: _id_input_new_passcode, mode: .secure, data: InputDataRowData(viewType: .firstItem), placeholder: nil, inputPlaceholder: L10n.passcodeControllerEnterPasscodePlaceholder, filter: { $0 }, limit: 255)) + index += 1 + + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: .string(state.data[_id_input_re_new_passcode]?.stringValue), error: state.errors[_id_input_re_new_passcode], identifier: _id_input_re_new_passcode, mode: .secure, data: InputDataRowData(viewType: .lastItem), placeholder: nil, inputPlaceholder: L10n.passcodeControllerReEnterPasscodePlaceholder, filter: { $0 }, limit: 255)) + index += 1 + + + case .change: + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.passcodeControllerHeaderCurrent), data: InputDataGeneralTextData(viewType: .textTopItem))) + index += 1 + + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: .string(state.data[_id_input_current]?.stringValue), error: state.errors[_id_input_current], identifier: _id_input_current, mode: .secure, data: InputDataRowData(viewType: .singleItem), placeholder: nil, inputPlaceholder: L10n.passcodeControllerCurrentPlaceholder, filter: { $0 }, limit: 255)) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.passcodeControllerHeaderNew), data: InputDataGeneralTextData(viewType: .textTopItem))) + index += 1 + + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: .string(state.data[_id_input_new_passcode]?.stringValue), error: state.errors[_id_input_new_passcode], identifier: _id_input_new_passcode, mode: .secure, data: InputDataRowData(viewType: .firstItem), placeholder: nil, inputPlaceholder: L10n.passcodeControllerEnterPasscodePlaceholder, filter: { $0 }, limit: 255)) + index += 1 + + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: .string(state.data[_id_input_re_new_passcode]?.stringValue), error: state.errors[_id_input_re_new_passcode], identifier: _id_input_re_new_passcode, mode: .secure, data: InputDataRowData(viewType: .lastItem), placeholder: nil, inputPlaceholder: L10n.passcodeControllerReEnterPasscodePlaceholder, filter: { $0 }, limit: 255)) + index += 1 + + + + case .disable: + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.passcodeControllerHeaderCurrent), data: InputDataGeneralTextData(viewType: .textTopItem))) + index += 1 + + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: .string(state.data[_id_input_current]?.stringValue), error: state.errors[_id_input_current], identifier: _id_input_current, mode: .secure, data: InputDataRowData(viewType: .singleItem), placeholder: nil, inputPlaceholder: L10n.passcodeControllerCurrentPlaceholder, filter: { $0 }, limit: 255)) + index += 1 + } + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.passcodeControllerText), data: InputDataGeneralTextData(detectBold: false, viewType: .textBottomItem))) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func PasscodeController(sharedContext: SharedAccountContext, mode: PasscodeMode) -> ViewController { + + + let initialState = PasscodeState(mode: mode, data: [:], errors: [:]) + + let statePromise = ValuePromise(initialState, ignoreRepeated: false) + let stateValue = Atomic(value: initialState) + let updateState: ((PasscodeState) -> PasscodeState) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let dataSignal = statePromise.get() |> map { + return passcodeEntries($0) + } + + let title: String + switch mode { + case .install: + title = L10n.passcodeControllerInstallTitle + case .change: + title = L10n.passcodeControllerChangeTitle + case .disable: + title = L10n.passcodeControllerDisableTitle + } + + var shouldMakeNextResponderAfterTransition: InputDataIdentifier? = nil + + let actionsDisposable = DisposableSet() + + return InputDataController(dataSignal: dataSignal |> map { InputDataSignalValue(entries: $0) }, title: title, validateData: { data in + + + return .fail(.doSomething { f in + let state = stateValue.with { $0 } + + switch state.mode { + case .install: + let passcode = state.data[_id_input_new_passcode]?.stringValue ?? "" + let confirm = state.data[_id_input_re_new_passcode]?.stringValue ?? "" + + var fields:[InputDataIdentifier : InputDataValidationFailAction] = [:] + + if !passcode.isEmpty, !confirm.isEmpty, passcode != confirm { + fields[_id_input_new_passcode] = .shake + fields[_id_input_re_new_passcode] = .shake + + updateState { + $0.withUpdatedError(InputDataValueError(description: L10n.passcodeControllerErrorDifferent, target: .data), for: _id_input_re_new_passcode) + } + } + if passcode.isEmpty { + fields[_id_input_new_passcode] = .shake + shouldMakeNextResponderAfterTransition = _id_input_new_passcode + } + if confirm.isEmpty { + fields[_id_input_re_new_passcode] = .shake + if shouldMakeNextResponderAfterTransition == nil { + shouldMakeNextResponderAfterTransition = _id_input_re_new_passcode + } + } + if !fields.isEmpty { + f(.fail(.fields(fields))) + } else { + actionsDisposable.add((sharedContext.accountManager.transaction { transaction in + transaction.setAccessChallengeData(.plaintextPassword(value: "")) + } |> deliverOnMainQueue).start(completed: { + sharedContext.appEncryptionValue.change(passcode) + f(.success(.navigationBackWithPushAnimation)) + })) + } + + case .change: + let current = state.data[_id_input_current]?.stringValue ?? "" + + let appEncryption = AppEncryptionParameters(path: sharedContext.accountManager.basePath.nsstring.deletingLastPathComponent) + appEncryption.applyPasscode(current) + + let passcode = state.data[_id_input_new_passcode]?.stringValue ?? "" + let confirm = state.data[_id_input_re_new_passcode]?.stringValue ?? "" + + var fields:[InputDataIdentifier : InputDataValidationFailAction] = [:] + + if appEncryption.decrypt() == nil { + fields[_id_input_current] = .shake + if shouldMakeNextResponderAfterTransition == nil { + shouldMakeNextResponderAfterTransition = _id_input_current + } + updateState { + $0.withUpdatedError(InputDataValueError(description: L10n.passcodeControllerErrorCurrent, target: .data), for: _id_input_current) + } + f(.fail(.fields(fields))) + return + } + + if !passcode.isEmpty, !confirm.isEmpty, passcode != confirm { + fields[_id_input_new_passcode] = .shake + fields[_id_input_re_new_passcode] = .shake + + updateState { + $0.withUpdatedError(InputDataValueError(description: L10n.passcodeControllerErrorDifferent, target: .data), for: _id_input_re_new_passcode) + } + } + if passcode.isEmpty { + fields[_id_input_new_passcode] = .shake + if shouldMakeNextResponderAfterTransition == nil { + shouldMakeNextResponderAfterTransition = _id_input_new_passcode + } + } + if confirm.isEmpty { + fields[_id_input_re_new_passcode] = .shake + if shouldMakeNextResponderAfterTransition == nil { + shouldMakeNextResponderAfterTransition = _id_input_re_new_passcode + } + } + if !fields.isEmpty { + f(.fail(.fields(fields))) + } else { + + + actionsDisposable.add((sharedContext.accountManager.transaction { transaction in + transaction.setAccessChallengeData(.plaintextPassword(value: "")) + } |> deliverOnMainQueue).start(completed: { + sharedContext.appEncryptionValue.change(passcode) + f(.success(.navigationBackWithPushAnimation)) + })) + } + case .disable: + let current = state.data[_id_input_current]?.stringValue ?? "" + + let appEncryption = AppEncryptionParameters(path: sharedContext.accountManager.basePath.nsstring.deletingLastPathComponent) + appEncryption.applyPasscode(current) + + if appEncryption.decrypt() == nil { + updateState { + $0.withUpdatedError(InputDataValueError(description: L10n.passcodeControllerErrorCurrent, target: .data), for: _id_input_current) + } + f(.fail(.fields([_id_input_current : .shake]))) + + } else { + actionsDisposable.add((sharedContext.accountManager.transaction { transaction in + transaction.setAccessChallengeData(.none) + } |> deliverOnMainQueue).start(completed: { + sharedContext.appEncryptionValue.remove() + f(.success(.navigationBackWithPushAnimation)) + })) + } + + } + + updateState { + $0 + } + }) + }, updateDatas: { data in + updateState { state in + var state = state + if let value = data[_id_input_new_passcode] { + state = state.withUpdatedValue(value, for: _id_input_new_passcode).withUpdatedError(nil, for: _id_input_new_passcode) + } + if let value = data[_id_input_re_new_passcode] { + state = state.withUpdatedValue(value, for: _id_input_re_new_passcode).withUpdatedError(nil, for: _id_input_re_new_passcode) + } + if let value = data[_id_input_current] { + state = state.withUpdatedValue(value, for: _id_input_current).withUpdatedError(nil, for: _id_input_current) + } + return state + } + + + return .none + }, afterDisappear: { + actionsDisposable.dispose() + }, afterTransaction: { controller in + if let identifier = shouldMakeNextResponderAfterTransition { + controller.makeFirstResponderIfPossible(for: identifier) + } + shouldMakeNextResponderAfterTransition = nil + }) + +} diff --git a/Telegram-Mac/PasscodeLockController.swift b/Telegram-Mac/PasscodeLockController.swift index 45e5642ee8..676d5047ce 100644 --- a/Telegram-Mac/PasscodeLockController.swift +++ b/Telegram-Mac/PasscodeLockController.swift @@ -8,82 +8,148 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore -enum PasscodeInnterState { - case old - case new - case confirm +import Postbox +import SwiftSignalKit +import LocalAuthentication + + +private class TouchIdContainerView : View { + fileprivate let button: TitleButton = TitleButton() + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(button) + + button.scaleOnClick = true + button.autohighlight = false + button.style = ControlStyle(font: .medium(.title), foregroundColor: .white, backgroundColor: theme.colors.accent, highlightColor: theme.colors.accent) + button.set(font: .medium(.title), for: .Normal) + button.set(color: .white, for: .Normal) + + button.set(text: L10n.passcodeUseTouchId, for: .Normal) + button.set(image: theme.icons.passcodeTouchId, for: .Normal) + button.layer?.cornerRadius = 18 + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + _ = button.sizeToFit(NSMakeSize(16, 0), NSMakeSize(0, 36), thatFit: true) + button.centerX(y: frame.height - button.frame.height) + + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + super.draw(layer, in: ctx) + + let (text, layout) = TextNode.layoutText(NSAttributedString.initialize(string: L10n.passcodeOr, color: theme.chatServiceItemTextColor, font: .normal(.title)), theme.colors.background, 1, .end, NSMakeSize(.greatestFiniteMagnitude, .greatestFiniteMagnitude), nil, false, .center) + + let f = focus(text.size) + layout.draw(NSMakeRect(f.minX, 0, f.width, f.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) + + ctx.setFillColor(theme.chatServiceItemTextColor.cgColor) + ctx.fill(NSMakeRect(0, floorToScreenPixels(backingScaleFactor, f.height / 2), f.minX - 10, .borderSize)) + + ctx.setFillColor(theme.chatServiceItemTextColor.cgColor) + ctx.fill(NSMakeRect(f.maxX + 10, floorToScreenPixels(backingScaleFactor, f.height / 2), f.minX - 10, .borderSize)) + } } -enum PasscodeViewState { - case login - case change(PasscodeInnterState) - case enable(PasscodeInnterState) - case disable(PasscodeInnterState) +final class PasscodeField : NSSecureTextField { + + override func resignFirstResponder() -> Bool { + (self.delegate as? PasscodeLockView)?.controlTextDidBeginEditing(Notification(name: NSControl.textDidChangeNotification)) + return super.resignFirstResponder() + } + + override func becomeFirstResponder() -> Bool { + (self.delegate as? PasscodeLockView)?.controlTextDidEndEditing(Notification(name: NSControl.textDidChangeNotification)) + return super.becomeFirstResponder() + } } -private class PasscodeLockView : Control, NSTextFieldDelegate { - fileprivate let photoView:AvatarControl = AvatarControl(font: .avatar(.custom(23))) - fileprivate let nameView:TextView = TextView() - fileprivate let input:NSSecureTextField - private let nextButton:TitleButton = TitleButton() - private var state:PasscodeViewState? + +class PasscodeLockView : Control, NSTextFieldDelegate { + private let backgroundView: BackgroundView = BackgroundView(frame: .zero) + private let visualEffect: NSVisualEffectView = NSVisualEffectView(frame: .zero) + + fileprivate let nameView:TextView = TextView() + let input:PasscodeField + private let nextButton:ImageButton = ImageButton() + private var hasTouchId:Bool = false + private let touchIdContainer:TouchIdContainerView = TouchIdContainerView(frame: NSMakeRect(0, 0, 240, 76)) fileprivate let logoutTextView:TextView = TextView() - fileprivate let value:ValuePromise = ValuePromise(ignoreRepeated: false) - fileprivate var logoutImpl:() -> Void = {} + let value:ValuePromise = ValuePromise(ignoreRepeated: false) + var logoutImpl:() -> Void = {} + fileprivate var useTouchIdImpl:() -> Void = {} + private let inputContainer: View = View() + private var fieldState: SearchFieldState = .Focus + required init(frame frameRect: NSRect) { - input = NSSecureTextField(frame: NSZeroRect) + input = PasscodeField(frame: NSZeroRect) + input.stringValue = "" + backgroundView.useSharedAnimationPhase = false super.init(frame: frameRect) - photoView.setFrameSize(NSMakeSize(80, 80)) - self.backgroundColor = .white - nextButton.set(color: theme.colors.blueUI, for: .Normal) - nextButton.set(font: .medium(.title), for: .Normal) - nextButton.set(text: tr(.passcodeNext), for: .Normal) - nextButton.sizeToFit() + addSubview(backgroundView) + // addSubview(visualEffect) - addSubview(photoView) - addSubview(nameView) - addSubview(input) - addSubview(logoutTextView) - addSubview(nextButton) + visualEffect.state = .active + visualEffect.blendingMode = .withinWindow + + autoresizingMask = [.width, .height] + self.backgroundColor = .clear + + nextButton.autohighlight = false + nextButton.set(background: theme.colors.accentIcon, for: .Normal) + nextButton.set(image: theme.icons.passcodeLogin, for: .Normal) + nextButton.setFrameSize(26, 26) + nextButton.scaleOnClick = true + nextButton.layer?.cornerRadius = nextButton.frame.height / 2 + nameView.userInteractionEnabled = false + nameView.isSelectable = false + addSubview(nextButton) + +// addSubview(nameView) + addSubview(inputContainer) + addSubview(logoutTextView) + addSubview(touchIdContainer) input.isBordered = false + input.usesSingleLineMode = true input.isBezeled = false input.focusRingType = .none - input.alignment = .center input.delegate = self + input.drawsBackground = false + + nameView.disableBackgroundDrawing = true + logoutTextView.disableBackgroundDrawing = true + + + inputContainer.backgroundColor = theme.colors.grayBackground + inputContainer.layer?.cornerRadius = .cornerRadius + inputContainer.addSubview(input) let attr = NSMutableAttributedString() - _ = attr.append(string: tr(.passcodeEnterPasscodePlaceholder), color: theme.colors.grayText, font: NSFont.normal(FontSize.text)) - attr.setAlignment(.center, range: attr.range) + _ = attr.append(string: L10n.passcodeEnterPasscodePlaceholder, color: theme.colors.grayText, font: .medium(17)) + //attr.setAlignment(.center, range: attr.range) input.placeholderAttributedString = attr - input.font = .normal(.text) - input.textColor = theme.colors.text - input.textView?.insertionPointColor = theme.colors.text - input.sizeToFit() - - let logoutAttr = NSMutableAttributedString() - _ = logoutAttr.append(string: tr(.passcodeLogoutDescription), color: theme.colors.grayText, font: .normal(.text)) - _ = logoutAttr.append(string: " ") - let range = logoutAttr.append(string: tr(.passcodeLogoutLinkText), color: theme.colors.link, font: .normal(.text)) - - logoutTextView.isSelectable = false - - logoutAttr.add(link: inAppLink.logout( { [weak self] in - self?.logoutImpl() - } ), for: range) - - let logoutLayout = TextViewLayout(logoutAttr) - logoutLayout.interactions = globalLinkExecutor - logoutTextView.set(layout: logoutLayout) - + input.cell?.usesSingleLineMode = true + input.cell?.wraps = false + input.cell?.isScrollable = true + input.font = .normal(.title) + input.textView?.insertionPointColor = theme.colors.grayText + input.sizeToFit() + + input.target = self input.action = #selector(checkPasscode) @@ -91,92 +157,192 @@ private class PasscodeLockView : Control, NSTextFieldDelegate { self?.checkPasscode() }, for: .SingleClick) - updateLocalizationAndTheme() + touchIdContainer.button.set(handler: { [weak self] _ in + self?.useTouchIdImpl() + }, for: .SingleClick) + + updateLocalizationAndTheme(theme: theme) + layout() + } + + var containerBgColor: NSColor { + switch theme.controllerBackgroundMode { + case .gradient: + return theme.chatServiceItemColor + case .background: + return theme.chatServiceItemColor + default: + if theme.chatBackground == theme.chatServiceItemColor { + return theme.colors.grayBackground + } + } + return theme.chatServiceItemColor + } + + var secondaryColor: NSColor { + switch theme.controllerBackgroundMode { + case .gradient: + return theme.chatServiceItemTextColor + case .background: + return theme.chatServiceItemTextColor + default: + if theme.chatBackground == theme.chatServiceItemColor { + return theme.colors.text + } + } + return theme.chatServiceItemTextColor } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = theme as! TelegramPresentationTheme backgroundColor = theme.colors.background - logoutTextView.backgroundColor = theme.colors.background - input.backgroundColor = theme.colors.background - nameView.backgroundColor = theme.colors.background + input.backgroundColor = .clear + input.textView?.insertionPointColor = secondaryColor; + input.textColor = secondaryColor + inputContainer.background = containerBgColor + backgroundView.backgroundMode = theme.controllerBackgroundMode + + + let placeholder = NSMutableAttributedString() + _ = placeholder.append(string: L10n.passcodeEnterPasscodePlaceholder, color: theme.chatServiceItemTextColor, font: .normal(.title)) + input.placeholderAttributedString = placeholder + + if #available(macOS 10.14, *) { + visualEffect.material = .underWindowBackground + } else { + visualEffect.material = theme.colors.isDark ? .ultraDark : .light + } + + let logoutAttr = parseMarkdownIntoAttributedString(L10n.passcodeLostDescription, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: .normal(.text), textColor: theme.chatServiceItemTextColor), bold: MarkdownAttributeSet(font: .bold(.text), textColor: theme.chatServiceItemTextColor), link: MarkdownAttributeSet(font: .bold(.text), textColor: secondaryColor == theme.chatServiceItemTextColor ? theme.chatServiceItemTextColor : theme.colors.link), linkAttribute: { contents in + return (NSAttributedString.Key.link.rawValue, inAppLink.callback(contents, {_ in})) + })) + + logoutTextView.isSelectable = false + + let logoutLayout = TextViewLayout(logoutAttr, alignment: .center) + + logoutLayout.interactions = TextViewInteractions(processURL:{ [weak self] _ in + self?.logoutImpl() + }) + logoutLayout.measure(width: frame.width - 40) + if theme.bubbled { + logoutLayout.generateAutoBlock(backgroundColor: theme.chatServiceItemColor) + } + logoutTextView.set(layout: logoutLayout) + + let layout = TextViewLayout(.initialize(string: L10n.passlockEnterYourPasscode, color: theme.chatServiceItemTextColor, font: .medium(17)), alignment: .center) + + layout.measure(width: frame.width - 40) + if theme.bubbled { + layout.generateAutoBlock(backgroundColor: theme.chatServiceItemColor) + } + nameView.update(layout) + + } override func mouseMoved(with event: NSEvent) { } + func controlTextDidChange(_ obj: Notification) { + change(state: fieldState, animated: true) + backgroundView.doAction() + } + + + func controlTextDidBeginEditing(_ obj: Notification) { + change(state: .Focus, animated: true) + input.textView?.insertionPointColor = secondaryColor + } + + func controlTextDidEndEditing(_ obj: Notification) { + window?.makeFirstResponder(input) + input.textView?.insertionPointColor = secondaryColor + } + + private func change(state: SearchFieldState, animated: Bool) { + self.fieldState = state + switch state { + case .Focus: + input._change(size: NSMakeSize(inputContainer.frame.width - 20, input.frame.height), animated: animated) + input._change(pos: NSMakePoint(10, input.frame.minY), animated: animated) + nextButton.change(opacity: 1, animated: animated) + nextButton._change(pos: NSMakePoint(inputContainer.frame.maxX + 10, nextButton.frame.minY), animated: animated) + case .None: + input.sizeToFit() + let f = inputContainer.focus(input.frame.size) + input._change(pos: NSMakePoint(f.minX, input.frame.minY), animated: animated) + nextButton.change(opacity: 0, animated: animated) + nextButton._change(pos: NSMakePoint(inputContainer.frame.maxX - nextButton.frame.width, nextButton.frame.minY), animated: animated) + } + } @objc func checkPasscode() { value.set(input.stringValue) } - func update(with state:PasscodeViewState, account:Account, peer:Peer) { - self.state = state + func update(hasTouchId: Bool) { + self.hasTouchId = hasTouchId - photoView.setPeer(account: account, peer: peer) - let layout = TextViewLayout(.initialize(string:peer.displayTitle, color: theme.colors.text, font:.normal(.title))) - layout.measure(width: frame.width - 40) - nameView.update(layout) - - switch state { - case .login: - self.logoutTextView.isHidden = false - default: - self.logoutTextView.isHidden = true + if hasTouchId { + showTouchIdUI() + } else { + hideTouchIdUI() } + + input.stringValue = "" + + needsLayout = true - changeInput(state) } + func updateAndSetValue() { + self.value.set(self.input.stringValue) + self.backgroundView.doAction() + } - fileprivate func changeInput(_ state:PasscodeViewState) { - let placeholder = NSMutableAttributedString() - let text:String - - switch state { - case .login: - text = tr(.passcodeEnterPasscodePlaceholder) - case let .change(inner), let .enable(inner), let .disable(inner): - switch inner { - case .old: - text = tr(.passcodeEnterCurrentPlaceholder) - case .new: - text = tr(.passcodeEnterNewPlaceholder) - case .confirm: - text = tr(.passcodeReEnterPlaceholder) - } - } - input.stringValue = "" - _ = placeholder.append(string: text, color: theme.colors.grayText, font: NSFont.normal(FontSize.text)) - placeholder.setAlignment(.center, range: placeholder.range) - input.placeholderAttributedString = placeholder + private func showTouchIdUI() { + touchIdContainer.isHidden = false } - override func draw(_ layer: CALayer, in ctx: CGContext) { - super.draw(layer, in: ctx) - ctx.setFillColor(theme.colors.border.cgColor) - ctx.fill(NSMakeRect(input.frame.minX, input.frame.maxY + 10, input.frame.width, .borderSize)) + private func hideTouchIdUI() { + touchIdContainer.isHidden = true } + override func layout() { super.layout() + backgroundView.frame = bounds + backgroundView.updateLayout(size: frame.size, transition: .immediate) - logoutTextView.layout?.measure(width: frame.width - 40) - logoutTextView.update(logoutTextView.layout) + visualEffect.frame = bounds + - photoView.center() - photoView.setFrameOrigin(photoView.frame.minX, photoView.frame.minY - floorToScreenPixels((20 + input.frame.height + 60)/2.0) - 20) - input.setFrameSize(200, input.frame.height) - nameView.centerX(y: photoView.frame.maxY + 20) - input.centerX(y: nameView.frame.minY + 30 + 20) - input.setFrameOrigin(input.frame.minX, input.frame.minY) - logoutTextView.centerX(y:frame.height - logoutTextView.frame.height - 20) - setNeedsDisplayLayer() + inputContainer.setFrameSize(200, 36) + input.setFrameSize(inputContainer.frame.width - 20, input.frame.height) + input.center() - nextButton.centerX(y: input.frame.maxY + 30) + inputContainer.layer?.cornerRadius = inputContainer.frame.height / 2 + + logoutTextView.resize(frame.width - 40, blockColor: theme.chatServiceItemColor) + nameView.resize(frame.width - 40, blockColor: theme.chatServiceItemColor) + + + inputContainer.center() + inputContainer.setFrameOrigin(NSMakePoint(inputContainer.frame.minX - (nextButton.frame.width + 10) / 2, inputContainer.frame.minY)) + + touchIdContainer.centerX(y: inputContainer.frame.maxY + 10) + nextButton.setFrameOrigin(inputContainer.frame.maxX + 10, inputContainer.frame.minY + (inputContainer.frame.height - nextButton.frame.height) / 2) + + nameView.centerX(y: inputContainer.frame.minY - nameView.frame.height - 10) + logoutTextView.centerX(y: frame.height - logoutTextView.frame.height - 10) + + + change(state: fieldState, animated: false) } required init?(coder: NSCoder) { @@ -185,140 +351,142 @@ private class PasscodeLockView : Control, NSTextFieldDelegate { } class PasscodeLockController: ModalViewController { - private let account:Account - private var state: PasscodeViewState { - didSet { - self.genericView.changeInput(state) - } - } + let accountManager: AccountManager + private let useTouchId: Bool + private let appearanceDisposable = MetaDisposable() private let disposable:MetaDisposable = MetaDisposable() private let valueDisposable = MetaDisposable() private let logoutDisposable = MetaDisposable() - private var passcodeValues:[String] = [] private let _doneValue:Promise = Promise() - - var doneValue:Signal { + private let laContext = LAContext() + var doneValue:Signal { return _doneValue.get() } - private let logoutImpl:() -> Void - init(_ account:Account, _ state: PasscodeViewState, logoutImpl:@escaping()->Void = {}) { - self.account = account - self.state = state + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + } + private let updateCurrectController: ()->Void + private let logoutImpl:() -> Signal + init(_ accountManager: AccountManager, useTouchId: Bool, logoutImpl:@escaping()->Signal = { .complete() }, updateCurrectController: @escaping()->Void) { + self.accountManager = accountManager self.logoutImpl = logoutImpl - super.init(frame: NSMakeRect(0, 0, 340, 310)) + self.useTouchId = useTouchId + self.updateCurrectController = updateCurrectController + super.init(frame: NSMakeRect(0, 0, 350, 350)) self.bar = .init(height: 0) } + override var isVisualEffectBackground: Bool { + return false + } + override var isFullScreen: Bool { - switch state { - case .login: - return true - default: - return false - } + return true + } + + + + override var background: NSColor { + return self.containerBackground } private var genericView:PasscodeLockView { return self.view as! PasscodeLockView } - private func checkNextValue(_ passcode: String, _ current:String?) { - switch state { - case .login: - if current == passcode { - _doneValue.set(.single(true)) - close() - } else { - genericView.input.shake() - } - case let .enable(inner): - switch inner { - case .new: - passcodeValues.append(passcode) - self.state = .enable(.confirm) - case .confirm: - if passcodeValues[0] == passcode { - _doneValue.set(account.postbox.modify { modifier -> Bool in - modifier.setAccessChallengeData(.plaintextPassword(value: passcode, timeout: 60*60, attempts: nil)) - return true - }) - close() - } else { - genericView.input.shake() - } - default: - break - } - case let .disable(inner): - switch inner { - case .old: - if current == passcode { - _doneValue.set(account.postbox.modify { modifier -> Bool in - modifier.setAccessChallengeData(.none) - return true - }) - close() - } else { - genericView.input.shake() - } - default: - break - } - case let .change(inner): - switch inner { - case .new: - passcodeValues.append(passcode) - self.state = .change(.confirm) - case .confirm: - if passcodeValues[0] == passcode { - _doneValue.set(account.postbox.modify { modifier -> Bool in - modifier.setAccessChallengeData(.plaintextPassword(value: passcode, timeout: modifier.getAccessChallengeData().timeout, attempts: nil)) - return true - }) - close() - } else { - genericView.input.shake() - } - case .old: - if current != passcode { - genericView.input.shake() - } else { - self.state = .change(.new) + private func checkNextValue(_ passcode: String) { + let appEncryption = AppEncryptionParameters(path: accountManager.basePath.nsstring.deletingLastPathComponent) + appEncryption.applyPasscode(passcode) + if appEncryption.decrypt() != nil { + self._doneValue.set(.single(true)) + self.close() + + } else { + genericView.input.shake() + } + } + + override func windowDidBecomeKey() { + super.windowDidBecomeKey() + + if NSApp.isActive { + // callTouchId() + } + } + + override func windowDidResignKey() { + super.windowDidResignKey() + if !NSApp.isActive { + // invalidateTouchId() + } + } + + func callTouchId() { + if laContext.canUseBiometric { + laContext.evaluatePolicy(.applicationPolicy, localizedReason: tr(L10n.passcodeUnlockTouchIdReason)) { (success, evaluateError) in + if (success) { + Queue.mainQueue().async { + self._doneValue.set(.single(true)) + self.close() + } } } } } + override var cornerRadius: CGFloat { + return 0 + } + + func invalidateTouchId() { + laContext.invalidate() + } + + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.updateCurrectController() + } + override func viewDidLoad() { super.viewDidLoad() - genericView.logoutImpl = logoutImpl - - valueDisposable.set((genericView.value.get() |> mapToSignal { [weak self] value in - if let strongSelf = self { - return strongSelf.account.postbox.modify { modifier -> (String, String?) in - switch modifier.getAccessChallengeData() { - case .none: - return (value, nil) - case let .plaintextPassword(passcode, _, _), let .numericalPassword(passcode, _, _): - return (value, passcode) - } - } - } - return .single(("", nil)) - } |> deliverOnMainQueue).start(next: { [weak self] value, current in - self?.checkNextValue(value, current) + appearanceDisposable.set(appearanceSignal.start(next: { [weak self] appearance in + self?.updateLocalizationAndTheme(theme: appearance.presentation) })) - disposable.set((account.postbox.loadedPeerWithId(account.peerId) |> deliverOnMainQueue).start(next: { [weak self] peer in - if let strongSelf = self { - strongSelf.genericView.update(with: strongSelf.state, account: strongSelf.account, peer: peer) - strongSelf.readyOnce() - } + genericView.logoutImpl = { [weak self] in + guard let window = self?.window else { return } + + confirm(for: window, information: L10n.accountConfirmLogoutText, successHandler: { [weak self] _ in + guard let `self` = self else { return } + _ = showModalProgress(signal: self.logoutImpl(), for: window).start(completed: { [weak self] in + delay(0.2, closure: { [weak self] in + self?.close() + }) + }) + }) + + } + + genericView.useTouchIdImpl = { [weak self] in + self?.callTouchId() + } + + valueDisposable.set((genericView.value.get() |> deliverOnMainQueue).start(next: { [weak self] value in + self?.checkNextValue(value) })) + genericView.update(hasTouchId: useTouchId) + readyOnce() + + + } + + override var closable: Bool { + return false } @@ -326,6 +494,11 @@ class PasscodeLockController: ModalViewController { return .invoked } + override func returnKeyAction() -> KeyHandlerResult { + self.genericView.updateAndSetValue() + return .invoked + } + override func firstResponder() -> NSResponder? { if !(window?.firstResponder is NSText) { return genericView.input @@ -338,14 +511,27 @@ class PasscodeLockController: ModalViewController { } + override var containerBackground: NSColor { + return theme.colors.background.withAlphaComponent(1.0) + } + override var handleEvents: Bool { + return true + } + + override var handleAllEvents: Bool { + return true + } + + override var responderPriority: HandlerPriority { - return .modal + return .supreme } deinit { disposable.dispose() logoutDisposable.dispose() valueDisposable.dispose() + appearanceDisposable.dispose() self.window?.removeAllHandlers(for: self) } diff --git a/Telegram-Mac/PasscodeSettings.swift b/Telegram-Mac/PasscodeSettings.swift new file mode 100644 index 0000000000..c981c892a6 --- /dev/null +++ b/Telegram-Mac/PasscodeSettings.swift @@ -0,0 +1,73 @@ +// +// PasscodeSettings.swift +// Telegram +// +// Created by Mikhail Filimonov on 02.11.2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import Postbox +import SwiftSignalKit +import TelegramCore + + + +struct PasscodeSettings: PreferencesEntry, Equatable { + + let timeout: Int32? + + static var defaultValue: PasscodeSettings { + return PasscodeSettings(timeout: 60 * 5) + } + + init(timeout: Int32?) { + self.timeout = timeout + } + + func isEqual(to: PreferencesEntry) -> Bool { + if let other = to as? PasscodeSettings { + return other == self + } else { + return false + } + } + + init(decoder: PostboxDecoder) { + self.timeout = decoder.decodeOptionalInt32ForKey("t") + } + + func encode(_ encoder: PostboxEncoder) { + if let timeout = self.timeout { + encoder.encodeInt32(timeout, forKey: "t") + } else { + encoder.encodeNil(forKey: "t") + } + } + + + func withUpdatedTimeout(_ timeout: Int32?) -> PasscodeSettings { + return PasscodeSettings(timeout: timeout) + } +} + + +func passcodeSettings(_ transaction: AccountManagerModifier) -> PasscodeSettings { + return transaction.getSharedData(ApplicationSharedPreferencesKeys.passcodeSettings) as? PasscodeSettings ?? PasscodeSettings.defaultValue +} + +func passcodeSettingsView(_ accountManager: AccountManager) -> Signal { + return accountManager.sharedData(keys: [ApplicationSharedPreferencesKeys.passcodeSettings]) |> map { view in + return view.entries[ApplicationSharedPreferencesKeys.passcodeSettings] as? PasscodeSettings ?? PasscodeSettings.defaultValue + } +} + +func updatePasscodeSettings(_ accountManager: AccountManager, _ f: @escaping(PasscodeSettings) -> PasscodeSettings) -> Signal { + return accountManager.transaction { transaction in + transaction.updateSharedData(ApplicationSharedPreferencesKeys.passcodeSettings, { entry in + let current = entry as? PasscodeSettings ?? PasscodeSettings.defaultValue + + return f(current) + }) + } |> ignoreValues +} diff --git a/Telegram-Mac/PasscodeSettingsViewController.swift b/Telegram-Mac/PasscodeSettingsViewController.swift index 4ac8c9e77b..4803338c0e 100644 --- a/Telegram-Mac/PasscodeSettingsViewController.swift +++ b/Telegram-Mac/PasscodeSettingsViewController.swift @@ -8,17 +8,20 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac +import TelegramCore + +import SwiftSignalKit +import Postbox +import LocalAuthentication private enum PasscodeEntry : Comparable, Identifiable { - case turnOn(sectionId:Int) - case turnOff(sectionId:Int) - case turnOnDescription(sectionId:Int) - case turnOffDescription(sectionId:Int) - case change(sectionId:Int) - case autoLock(sectionId:Int, time:Int32?) + case turnOn(sectionId:Int, viewType: GeneralViewType) + case turnOff(sectionId:Int, viewType: GeneralViewType) + case turnOnDescription(sectionId:Int, viewType: GeneralViewType) + case turnOffDescription(sectionId:Int, viewType: GeneralViewType) + case change(sectionId:Int, viewType: GeneralViewType) + case autoLock(sectionId:Int, time:Int32?, viewType: GeneralViewType) + case turnTouchId(sectionId:Int, enabled: Bool, viewType: GeneralViewType) case section(sectionId:Int) var stableId:Int { @@ -35,6 +38,8 @@ private enum PasscodeEntry : Comparable, Identifiable { return 4 case .autoLock: return 5 + case .turnTouchId: + return 6 case let .section(sectionId): return (sectionId + 1) * 1000 - sectionId } @@ -42,17 +47,19 @@ private enum PasscodeEntry : Comparable, Identifiable { var stableIndex:Int { switch self { - case let .turnOn(sectionId): + case let .turnOn(sectionId, _): + return (sectionId * 1000) + stableId + case let .turnOff(sectionId, _): return (sectionId * 1000) + stableId - case let .turnOff(sectionId): + case let .turnOnDescription(sectionId, _): return (sectionId * 1000) + stableId - case let .turnOnDescription(sectionId): + case let .turnOffDescription(sectionId, _): return (sectionId * 1000) + stableId - case let .turnOffDescription(sectionId): + case let .change(sectionId, _): return (sectionId * 1000) + stableId - case let .change(sectionId): + case let .autoLock(sectionId, _, _): return (sectionId * 1000) + stableId - case let .autoLock(sectionId, _): + case let .turnTouchId(sectionId, _, _): return (sectionId * 1000) + stableId case let .section(sectionId): return (sectionId + 1) * 1000 - sectionId @@ -64,54 +71,9 @@ private func <(lhs:PasscodeEntry, rhs:PasscodeEntry) -> Bool { return lhs.stableIndex < rhs.stableIndex } -private func ==(lhs:PasscodeEntry, rhs:PasscodeEntry) -> Bool { - switch lhs { - case let .turnOn(sectionId): - if case .turnOn(sectionId) = rhs { - return true - } else { - return false - } - case let .turnOff(sectionId): - if case .turnOff(sectionId) = rhs { - return true - } else { - return false - } - case let .turnOnDescription(sectionId): - if case .turnOnDescription(sectionId) = rhs { - return true - } else { - return false - } - case let .turnOffDescription(sectionId): - if case .turnOffDescription(sectionId) = rhs { - return true - } else { - return false - } - case let .change(sectionId): - if case .change(sectionId) = rhs { - return true - } else { - return false - } - case let .autoLock(lhsSectionId, lhsTime): - if case let .autoLock(rhsSectionId, rhsTime) = rhs { - return lhsSectionId == rhsSectionId && lhsTime == rhsTime - } else { - return false - } - case let .section(sectionId): - if case .section(sectionId) = rhs { - return true - } else { - return false - } - } -} -private func passcodeSettinsEntry(_ passcode: PostboxAccessChallengeData) -> [PasscodeEntry] { + +private func passcodeSettinsEntry(_ passcode: PostboxAccessChallengeData, passcodeSettings: PasscodeSettings, _ additional: AdditionalSettings) -> [PasscodeEntry] { var entries:[PasscodeEntry] = [] var sectionId:Int = 1 @@ -120,19 +82,27 @@ private func passcodeSettinsEntry(_ passcode: PostboxAccessChallengeData) -> [Pa switch passcode { case .none: - entries.append(.turnOn(sectionId: sectionId)) - entries.append(.turnOnDescription(sectionId: sectionId)) - case .plaintextPassword, .numericalPassword: - entries.append(.turnOff(sectionId: sectionId)) - entries.append(.change(sectionId: sectionId)) - entries.append(.turnOffDescription(sectionId: sectionId)) + entries.append(.turnOn(sectionId: sectionId, viewType: .singleItem)) + entries.append(.turnOnDescription(sectionId: sectionId, viewType: .textBottomItem)) + case .plaintextPassword, let .numericalPassword: + entries.append(.turnOff(sectionId: sectionId, viewType: .firstItem)) + entries.append(.change(sectionId: sectionId, viewType: .lastItem)) + entries.append(.turnOffDescription(sectionId: sectionId, viewType: .textBottomItem)) entries.append(.section(sectionId: sectionId)) sectionId += 1 - entries.append(.autoLock(sectionId: sectionId, time: passcode.timeout)) + let context = LAContext() + entries.append(.autoLock(sectionId: sectionId, time: passcodeSettings.timeout, viewType: context.canUseBiometric ? .firstItem : .singleItem)) + if context.canUseBiometric { + entries.append(.turnTouchId(sectionId: sectionId, enabled: additional.useTouchId, viewType: .lastItem)) + } + } + entries.append(.section(sectionId: sectionId)) + sectionId += 1 + return entries } @@ -144,41 +114,43 @@ fileprivate func prepareTransition(left:[AppearanceWrapperEntry], let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in switch entry.entry { case .section: - return GeneralRowItem(initialSize, height: 20, stableId: entry.stableId) - case .turnOn: - return GeneralInteractedRowItem(initialSize, stableId: entry.stableId, name: tr(.passcodeTurnOn), nameStyle: actionStyle, type: .none, action: { + return GeneralRowItem(initialSize, height: 30, stableId: entry.stableId, viewType: .separator) + case let .turnOn(_, viewType): + return GeneralInteractedRowItem(initialSize, stableId: entry.stableId, name: tr(L10n.passcodeTurnOn), nameStyle: actionStyle, type: .none, viewType: viewType, action: { arguments.turnOn() }) - case .turnOff: - return GeneralInteractedRowItem(initialSize, stableId: entry.stableId, name: tr(.passcodeTurnOff), nameStyle: actionStyle, type: .none, action: { + case let .turnOff(_, viewType): + return GeneralInteractedRowItem(initialSize, stableId: entry.stableId, name: tr(L10n.passcodeTurnOff), nameStyle: actionStyle, type: .none, viewType: viewType, action: { arguments.turnOff() }) - case .change: - return GeneralInteractedRowItem(initialSize, stableId: entry.stableId, name: tr(.passcodeChange), nameStyle: actionStyle, type: .none, action: { + case let .change(_, viewType): + return GeneralInteractedRowItem(initialSize, stableId: entry.stableId, name: tr(L10n.passcodeChange), nameStyle: actionStyle, type: .none, viewType: viewType, action: { arguments.change() }) - case .turnOnDescription, .turnOffDescription: - return GeneralTextRowItem(initialSize, stableId: entry.stableId, text: tr(.passcodeTurnOnDescription)) - case let .autoLock(sectionId: _, time: time): + case let .turnOnDescription(_, viewType), let .turnOffDescription(_, viewType): + return GeneralTextRowItem(initialSize, stableId: entry.stableId, text: L10n.passcodeControllerText, viewType: viewType) + case let .turnTouchId(_, enabled, viewType): + return GeneralInteractedRowItem(initialSize, stableId: entry.stableId, name: tr(L10n.passcodeUseTouchId), type: .switchable(enabled), viewType: viewType, action: { + arguments.toggleTouchId(!enabled) + }) + case let .autoLock(sectionId: _, time, viewType): var text:String if let time = time { if time < 60 { - text = tr(.timerSecondsCountable(Int(time))) - } else if time <= 60 * 60 { - text = tr(.timerMinutesCountable(Int(time / 60))) - } else if time <= 60 * 60 * 24 { - text = tr(.timerHoursCountable(Int(time / 60) / 60)) + text = tr(L10n.timerSecondsCountable(Int(time))) + } else if time < 60 * 60 { + text = tr(L10n.timerMinutesCountable(Int(time / 60))) + } else if time < 60 * 60 * 24 { + text = tr(L10n.timerHoursCountable(Int(time / 60) / 60)) } else { - text = tr(.timerDaysCountable(Int(time / 60) / 60 / 24)) + text = tr(L10n.timerDaysCountable(Int(time / 60) / 60 / 24)) } - text = tr(.passcodeAutoLockIfAway(text)) + text = tr(L10n.passcodeAutoLockIfAway(text)) } else { - text = tr(.passcodeAutoLockDisabled) + text = tr(L10n.passcodeAutoLockDisabled) } - return GeneralInteractedRowItem(initialSize, stableId: entry.stableId, name: tr(.passcodeAutolock), type: .context { - return text - }, action: { + return GeneralInteractedRowItem(initialSize, stableId: entry.stableId, name: L10n.passcodeAutolock, type: .context(text), viewType: viewType, action: { arguments.ifAway() }) } @@ -189,43 +161,37 @@ fileprivate func prepareTransition(left:[AppearanceWrapperEntry], private final class PasscodeSettingsArguments { - let account:Account + let context: AccountContext let turnOn:()->Void let turnOff:()->Void let change:()->Void let ifAway:()->Void - init(_ account:Account, turnOn: @escaping()->Void, turnOff: @escaping()->Void, change:@escaping()->Void, ifAway: @escaping()-> Void) { - self.account = account + let toggleTouchId:(Bool)->Void + init(_ context: AccountContext, turnOn: @escaping()->Void, turnOff: @escaping()->Void, change:@escaping()->Void, ifAway: @escaping()-> Void, toggleTouchId:@escaping(Bool)->Void) { + self.context = context self.turnOn = turnOn self.turnOff = turnOff self.change = change self.ifAway = ifAway + self.toggleTouchId = toggleTouchId } } class PasscodeSettingsViewController: TableViewController { - private let actionUpdate:Promise = Promise(false) + private let disposable = MetaDisposable() + private func show(mode: PasscodeMode) { + self.navigationController?.push(PasscodeController(sharedContext: context.sharedContext, mode: mode)) + } - private func show(with state: PasscodeViewState) { - let controller = PasscodeLockController(account, state) - actionUpdate.set(controller.doneValue) - showModal(with: controller, for: mainWindow) + deinit { + disposable.dispose() } func updateAwayTimeout(_ timeout:Int32?) { - self.actionUpdate.set(account.postbox.modify { modifier -> Bool in - - switch modifier.getAccessChallengeData() { - case .none: - break - case let .numericalPassword(passcode, _, attempts): - modifier.setAccessChallengeData(.numericalPassword(value: passcode, timeout: timeout, attempts: attempts)) - case let .plaintextPassword(passcode, _, attempts): - modifier.setAccessChallengeData(.plaintextPassword(value: passcode, timeout: timeout, attempts: attempts)) - } - return true - }) + disposable.set(updatePasscodeSettings(context.sharedContext.accountManager, { + $0.withUpdatedTimeout(timeout) + }).start()) } func showIfAwayOptions() { @@ -233,49 +199,46 @@ class PasscodeSettingsViewController: TableViewController { var items:[SPopoverItem] = [] - items.append(SPopoverItem(tr(.passcodeAutoLockDisabled), { [weak self] in + items.append(SPopoverItem(tr(L10n.passcodeAutoLockDisabled), { [weak self] in self?.updateAwayTimeout(nil) })) - if isDebug { - // - items.append(SPopoverItem(tr(.passcodeAutoLockIfAway(tr(.timerSecondsCountable(5)))), { [weak self] in - self?.updateAwayTimeout(5) - })) - } - - items.append(SPopoverItem(tr(.passcodeAutoLockIfAway(tr(.timerMinutesCountable(1)))), { [weak self] in + items.append(SPopoverItem(tr(L10n.passcodeAutoLockIfAway(tr(L10n.timerMinutesCountable(1)))), { [weak self] in self?.updateAwayTimeout(60) })) - items.append(SPopoverItem(tr(.passcodeAutoLockIfAway(tr(.timerMinutesCountable(5)))), { [weak self] in + items.append(SPopoverItem(tr(L10n.passcodeAutoLockIfAway(tr(L10n.timerMinutesCountable(5)))), { [weak self] in self?.updateAwayTimeout(60 * 5) })) - items.append(SPopoverItem(tr(.passcodeAutoLockIfAway(tr(.timerHoursCountable(1)))), { [weak self] in + items.append(SPopoverItem(tr(L10n.passcodeAutoLockIfAway(tr(L10n.timerHoursCountable(1)))), { [weak self] in self?.updateAwayTimeout(60 * 60) })) - items.append(SPopoverItem(tr(.passcodeAutoLockIfAway(tr(.timerHoursCountable(5)))), { [weak self] in + items.append(SPopoverItem(tr(L10n.passcodeAutoLockIfAway(tr(L10n.timerHoursCountable(5)))), { [weak self] in self?.updateAwayTimeout(60 * 60 * 5) })) - showPopover(for: view, with: SPopoverViewController(items: items), edge: .minX, inset: NSMakePoint(0, -25)) + showPopover(for: view, with: SPopoverViewController(items: items, visibility: items.count), edge: .minX, inset: NSMakePoint(0, -25)) } } override func viewDidLoad() { super.viewDidLoad() - let account = self.account - let arguments = PasscodeSettingsArguments(account, turnOn: { [weak self] in - self?.show(with: .enable(.new)) + let context = self.context + let arguments = PasscodeSettingsArguments(context, turnOn: { [weak self] in + self?.show(mode: .install) }, turnOff: { [weak self] in - self?.show(with: .disable(.old)) + self?.show(mode: .disable) }, change: { [weak self] in - self?.show(with: .change(.old)) + self?.show(mode: .change) }, ifAway: { [weak self] in self?.showIfAwayOptions() + }, toggleTouchId: { enabled in + _ = updateAdditionalSettingsInteractively(accountManager: context.sharedContext.accountManager, { current -> AdditionalSettings in + return current.withUpdatedTouchId(enabled) + }).start() }) let initialSize = self.atomicSize.modify({$0}) @@ -283,12 +246,8 @@ class PasscodeSettingsViewController: TableViewController { let previous:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) - genericView.merge(with: combineLatest(actionUpdate.get() |> mapToSignal { _ in - return account.postbox.modify { modifier -> PostboxAccessChallengeData in - return modifier.getAccessChallengeData() - } - } |> deliverOn(prepareQueue), appearanceSignal |> deliverOn(prepareQueue)) |> map { passcode, appearance in - let entries = passcodeSettinsEntry(passcode).map{AppearanceWrapperEntry(entry: $0, appearance: appearance)} + genericView.merge(with: combineLatest(queue: prepareQueue, context.sharedContext.accountManager.accessChallengeData(), passcodeSettingsView(context.sharedContext.accountManager), appearanceSignal, additionalSettings(accountManager: context.sharedContext.accountManager)) |> map { passcode, passcodeSettings, appearance, additional in + let entries = passcodeSettinsEntry(passcode.data, passcodeSettings: passcodeSettings, additional).map{AppearanceWrapperEntry(entry: $0, appearance: appearance)} return prepareTransition(left: previous.swap(entries), right: entries, initialSize: initialSize, arguments: arguments) } |> deliverOnMainQueue) diff --git a/Telegram-Mac/PassportAcceptRowItem.swift b/Telegram-Mac/PassportAcceptRowItem.swift new file mode 100644 index 0000000000..f96e4724d0 --- /dev/null +++ b/Telegram-Mac/PassportAcceptRowItem.swift @@ -0,0 +1,67 @@ +// +// PassportAcceptRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 21/03/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +class PassportAcceptRowItem: GeneralRowItem { + + init(_ initialSize: NSSize, stableId: AnyHashable, enabled: Bool, action:@escaping()->Void) { + super.init(initialSize, height: 50, stableId: stableId, action: action, enabled: enabled) + } + + + override func viewClass() -> AnyClass { + return PassportAcceptRowView.self + } +} + +final class PassportAcceptRowView : TableRowView { + private let button: TitleButton = TitleButton() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(button) + button.set(font: .medium(.header), for: .Normal) + + button.set(handler: { [weak self] _ in + guard let item = self?.item as? GeneralRowItem else {return} + item.action() + }, for: .Click) + } + + override func layout() { + super.layout() + guard let item = item as? GeneralRowItem else {return} + + _ = button.sizeToFit(NSZeroSize, NSMakeSize(170, 40), thatFit: true)//.setFrameSize(NSMakeSize(frame.width - item.inset.left - item.inset.right, 40)) + button.center() + button.layer?.cornerRadius = 20 + } + + override func updateColors() { + super.updateColors() + button.set(text: L10n.secureIdRequestAccept, for: .Normal) + button.set(color: .white, for: .Normal) + } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + + guard let item = item as? GeneralRowItem else {return} + + // button.userInteractionEnabled = item.enabled + button.autohighlight = false + button.set(background: item.enabled ? theme.colors.accent : theme.colors.grayForeground, for: .Normal) + button.set(image: theme.icons.secureIdAuth, for: .Normal) + needsLayout = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/PassportController.swift b/Telegram-Mac/PassportController.swift new file mode 100644 index 0000000000..62b7d4cba8 --- /dev/null +++ b/Telegram-Mac/PassportController.swift @@ -0,0 +1,4088 @@ + // +// PassportController.swift +// Telegram +// +// Created by Mikhail Filimonov on 20/03/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + + + + +private let _id_street1 = InputDataIdentifier("street_line1") +private let _id_street2 = InputDataIdentifier("street_line2") +private let _id_postcode = InputDataIdentifier("post_code") +private let _id_city = InputDataIdentifier("city") +private let _id_state = InputDataIdentifier("state") + +private let _id_delete = InputDataIdentifier("delete") + +private let _id_first_name = InputDataIdentifier("first_name") +private let _id_middle_name = InputDataIdentifier("middle_name") +private let _id_last_name = InputDataIdentifier("last_name") + + private let _id_first_name_native = InputDataIdentifier("first_name_native") + private let _id_middle_name_native = InputDataIdentifier("middle_name_native") + private let _id_last_name_native = InputDataIdentifier("last_name_native") + +private let _id_birthday = InputDataIdentifier("birth_date") +private let _id_issue_date = InputDataIdentifier("issue_date") +private let _id_expire_date = InputDataIdentifier("expiry_date") +private let _id_identifier = InputDataIdentifier("document_no") + +private let _id_country = InputDataIdentifier("country_code") +private let _id_residence = InputDataIdentifier("residence_country_code") +private let _id_gender = InputDataIdentifier("gender") + +private let _id_email_code = InputDataIdentifier("email_code") +private let _id_email_new = InputDataIdentifier("email_new") +private let _id_email_def = InputDataIdentifier("email_default") + +private let _id_phone_code = InputDataIdentifier("_id_phone_code") +private let _id_phone_new = InputDataIdentifier("_id_phone_new") +private let _id_phone_def = InputDataIdentifier("_id_phone_def") + +private let _id_c_password = InputDataIdentifier("create_password") +private let _id_c_repassword = InputDataIdentifier("create_re_password") +private let _id_c_email = InputDataIdentifier("create_email") +private let _id_c_hint = InputDataIdentifier("hint") + +private let _id_selfie = InputDataIdentifier("selfie") +private let _id_selfie_scan = InputDataIdentifier("selfie_scan") + +private let _id_scan = InputDataIdentifier("scan") +private let _id_translation = InputDataIdentifier("translation") + +private let _id_frontside = InputDataIdentifier("front_side") +private let _id_backside = InputDataIdentifier("reverse_side") + +private extension SecureIdVerificationDocument { + var errorIdentifier: InputDataIdentifier { + switch self { + case let .remote(file): + let hash = file.fileHash.base64EncodedString() + return InputDataIdentifier("file_\(hash)") + default: + return InputDataIdentifier("\(arc4random())") + } + } +} + + +private let cManager: CountryManager = CountryManager() + +private struct EditSettingsValues { + let values:[SecureIdValue] + func hasValue(_ relative: SecureIdRequestedFormFieldValue) -> Bool { + return values.contains(where: {$0.key == relative.valueKey}) + } +} + +private func updateFrontMrz(file: String, relative: SecureIdRequestedFormFieldValue, updateState: @escaping ((PassportState)->PassportState)->Void) { + if let image = NSImage(contentsOfFile: file) { + let string = recognizeMRZ(image.precomposed(), nil) + let mrz = TGPassportMRZ.parseLines(string?.components(separatedBy: "\n")) + let localFile:SecureIdVerificationDocument = .local(SecureIdVerificationLocalDocument(id: arc4random64(), resource: LocalFileReferenceMediaResource(localFilePath: file, randomId: arc4random64()), state: .uploading(0))) + + updateState { current in + var current = current + if let mrz = mrz { + if relative.isEqualToMRZ(mrz) { + + let filedata = try! String(contentsOfFile: Bundle.main.path(forResource: "countries", ofType: nil)!).components(separatedBy: "\n") + + var citizenship: InputDataValue? = current.detailsIntermediateState?.citizenship + var residence: InputDataValue? = current.detailsIntermediateState?.residence + + for line in filedata { + let country = line.components(separatedBy: ";") + if let symbols = country.last?.components(separatedBy: ",") { + if symbols.contains(mrz.nationality) && citizenship == nil { + citizenship = .string(country[1]) + } + if symbols.contains(mrz.issuingCountry) && residence == nil { + residence = .string(country[1]) + } + } + } + + + let expiryDate = dateFormatter.string(from: mrz.expiryDate).components(separatedBy: ".").map({Int32($0)}) + let birthDate = dateFormatter.string(from: mrz.birthDate).components(separatedBy: ".").map({Int32($0)}) + let details = DetailsIntermediateState(firstName: .string(mrz.firstName.lowercased().capitalizingFirstLetter()), middleName: nil, lastName: .string(mrz.lastName.lowercased().capitalizingFirstLetter()), firstNameNative: nil, middleNameNative: nil, lastNameNative: nil, birthday: .date(birthDate[0], birthDate[1], birthDate[2]), citizenship: citizenship, residence: residence, gender: .gender(SecureIdGender.gender(from: mrz)), expiryDate: .date(expiryDate[0], expiryDate[1], expiryDate[2]), identifier: .string(mrz.documentNumber)) + current = current.withUpdatedDetailsState(details) + } + } + return current.withUpdatedFrontSide(localFile, for: relative.valueKey) + } + } +} + + private struct FieldRequest : Hashable { + let primary: SecureIdRequestedFormFieldValue + let secondary: SecureIdRequestedFormFieldValue? + let fillPrimary: Bool + init(_ primary: SecureIdRequestedFormFieldValue, _ secondary: SecureIdRequestedFormFieldValue? = nil, _ fillPrimary: Bool = true) { + self.primary = primary + self.secondary = secondary + self.fillPrimary = fillPrimary + } + + var hashValue: Int { + return 0 + } + + static func ==(lhs: FieldRequest, rhs: FieldRequest) -> Bool { + return lhs.primary == rhs.primary && lhs.secondary == rhs.secondary + } + } + +private final class PassportArguments { + let context: AccountContext + let checkPassword:((String, ()->Void))->Void + let requestField:(FieldRequest, SecureIdValue?, SecureIdValue?, [SecureIdRequestedFormFieldValue], EditSettingsValues?)->Void + let createPassword: ()->Void + let abortVerification: ()-> Void + let authorize:(Bool)->Void + let botPrivacy:()->Void + let forgotPassword:()->Void + let deletePassport:()->Void + let updateEmailCode: ()-> Void + init(context: AccountContext, checkPassword:@escaping((String, ()->Void))->Void, requestField:@escaping(FieldRequest, SecureIdValue?, SecureIdValue?, [SecureIdRequestedFormFieldValue], EditSettingsValues?)->Void, createPassword: @escaping()->Void, abortVerification: @escaping()->Void, authorize: @escaping(Bool)->Void, botPrivacy:@escaping()->Void, forgotPassword: @escaping()->Void, deletePassport:@escaping()->Void, updateEmailCode: @escaping()-> Void) { + self.context = context + self.checkPassword = checkPassword + self.requestField = requestField + self.createPassword = createPassword + self.abortVerification = abortVerification + self.botPrivacy = botPrivacy + self.authorize = authorize + self.forgotPassword = forgotPassword + self.deletePassport = deletePassport + self.updateEmailCode = updateEmailCode + } +} + + + +private enum PassportEntryId : Hashable { + case header + case loading + case sectionId(Int32) + case emptyFieldId(FieldRequest) + case filledFieldId(FieldRequest) + case savedFieldId(SecureIdValueKey) + case description(Int32) + case accept + case requestPassword + case createPassword + case deletePassport + case settingsHeader + case inputEmailCode + var hashValue: Int { + return 0 + } +} + +private let scansLimit: Int = 20 + +private struct FieldDescription : Equatable { + let text: String + let isError: Bool + init(text: String, isError: Bool = false) { + self.text = text + self.isError = isError + } +} + +private enum PassportEntry : TableItemListNodeEntry { + case header(sectionId: Int32, index: Int32, requestedFields: [SecureIdRequestedFormField], peer: Peer) + case accept(sectionId: Int32, index: Int32, enabled: Bool) + case emptyField(sectionId: Int32, index: Int32, fieldType: FieldRequest, value: SecureIdValue?, relativeValue: SecureIdValue?, relative: [SecureIdRequestedFormFieldValue], title: String, desc: String, isError: Bool) + case filledField(sectionId: Int32, index: Int32, fieldType: FieldRequest, relative: [SecureIdRequestedFormFieldValue], title: String, description: FieldDescription, value: SecureIdValue?, relativeValue: SecureIdValue?) + case savedField(sectionId: Int32, index: Int32, valueType: SecureIdValueKey, relative: [SecureIdValueKey], relativeValues: [SecureIdValue], title: String, description: String) + case description(sectionId: Int32, index: Int32, text: String, detectBold: Bool) + case inputEmailCode(sectionId: Int32, index: Int32, text: String, limit: Int32?, error: InputDataValueError?) + case settingsHeader(sectionId: Int32, index: Int32) + case requestPassword(sectionId: Int32, index: Int32, hasRecoveryEmail: Bool, isSettings: Bool, error: String?) + case createPassword(sectionId: Int32, index: Int32, peer: Peer) + case loading + case deletePassport(sectionId: Int32, index: Int32) + case sectionId(Int32) + + var stableId: PassportEntryId { + switch self { + case .header: + return .header + case let .emptyField(_, _, fieldType, _, _, _, _, _, _): + return .emptyFieldId(fieldType) + case let .filledField(_, _, fieldType, _, _, _, _, _): + return .filledFieldId(fieldType) + case let .savedField(_, _, type, _, _, _, _): + return .savedFieldId(type) + case let .description(_, index, _, _): + return .description(index) + case .accept: + return .accept + case .loading: + return .loading + case .requestPassword: + return .requestPassword + case .createPassword: + return .createPassword + case .inputEmailCode: + return .inputEmailCode + case .deletePassport: + return .deletePassport + case .settingsHeader: + return .settingsHeader + case .sectionId(let id): + return .sectionId(id) + } + } + + var index: Int32 { + switch self { + case let .header(section, index, _, _): + return (section * 1000) + index + case let .emptyField(section, index, _, _, _, _, _, _, _): + return (section * 1000) + index + case let .accept(section, index, _): + return (section * 1000) + index + case let .filledField(section, index, _, _, _, _, _, _): + return (section * 1000) + index + case let .savedField(section, index, _, _, _, _, _): + return (section * 1000) + index + case let .description(section, index, _, _): + return (section * 1000) + index + case let .requestPassword(section, index, _, _, _): + return (section * 1000) + index + case let .createPassword(section, index, _): + return (section * 1000) + index + case let .inputEmailCode(section, index, _, _, _): + return (section * 1000) + index + case let .deletePassport(section, index): + return (section * 1000) + index + case let .settingsHeader(section, index): + return (section * 1000) + index + case .loading: + return 0 + case let .sectionId(section): + return (section + 1) * 1000 - section + } + } + + func item(_ arguments: PassportArguments, initialSize: NSSize) -> TableRowItem { + switch self { + case let .header(_, _, requestedFields, peer): + return PassportHeaderItem(initialSize, account: arguments.context.account, stableId: stableId, requestedFields: requestedFields, peer: peer) + case let .emptyField(_, _, fieldType, value, relativeValue, relative, title, desc, isError): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: title, description: desc, descTextColor: isError ? theme.colors.redUI : theme.colors.grayText, type: .none, action: { + arguments.requestField(fieldType, value, relativeValue, relative, nil) + }) + case let .accept(_, _, enabled): + return PassportAcceptRowItem(initialSize, stableId: stableId, enabled: enabled, action: { + arguments.authorize(enabled) + }) + case let .filledField(_, _, fieldType, relative, title, description, value, relativeValue): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: title, description: description.text, descTextColor: description.isError ? theme.colors.redUI : theme.colors.grayText, type: .selectable(!description.isError), action: { + arguments.requestField(fieldType, value, relativeValue, relative, nil) + }) + case let .savedField(_, _, fieldType, relative, relativeValues, title, description): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: title, description: description, type: .next, action: { + arguments.requestField(FieldRequest(fieldType.requestFieldType), relativeValues.first(where: {$0.key == fieldType}), nil, relative.map({$0.requestFieldType}), EditSettingsValues(values: relativeValues)) + }) + case .requestPassword(_, _, let hasRecoveryEmail, let isSettings, let error): + return PassportInsertPasswordItem(initialSize, stableId: stableId, checkPasswordAction: arguments.checkPassword, forgotPassword: arguments.forgotPassword, hasRecoveryEmail: hasRecoveryEmail, isSettings: isSettings, error: error) + case .createPassword(_, _, let peer): + return PassportTwoStepVerificationIntroItem(initialSize, stableId: stableId, peer: peer, action: arguments.createPassword) + case let .inputEmailCode(_, _, text, limit, error): + return InputDataRowItem(initialSize, stableId: stableId, mode: .plain, error: error, currentText: text, placeholder: nil, inputPlaceholder: L10n.twoStepAuthRecoveryCode, filter: {String($0.unicodeScalars.filter { CharacterSet.decimalDigits.contains($0)})}, updated: { _ in + arguments.updateEmailCode() + }, limit: limit ?? 255) + case let .description(_, _, text, detectBold): + return GeneralTextRowItem(initialSize, stableId: stableId, text: .markdown(text, linkHandler: { link in + switch link { + case "abortVerification": + arguments.abortVerification() + case "_applyPolicy_": + arguments.botPrivacy() + default: + break + } + }), detectBold: detectBold, inset: NSEdgeInsets(left: 30.0, right: 30.0, top: 10, bottom:2)) + case .deletePassport: + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.secureIdDeletePassport, nameStyle: ControlStyle(font: .normal(.title), foregroundColor: theme.colors.redUI), type: .none, action: { + arguments.deletePassport() + }) + case .settingsHeader: + return PassportSettingsHeaderItem(initialSize, stableId: stableId) + case .loading: + return SearchEmptyRowItem(initialSize, stableId: stableId, isLoading: true) + case .sectionId: + return GeneralRowItem(initialSize, height: 20, stableId: stableId) + } + } +} + +private func <(lhs: PassportEntry, rhs: PassportEntry) -> Bool { + return lhs.index < rhs.index +} + +private func ==(lhs: PassportEntry, rhs: PassportEntry) -> Bool { + switch lhs { + case let .header(lhsSectionId, lhsIndex, lhsRequestedFields, lhsPeer): + if case let .header(rhsSectionId, rhsIndex, rhsRequestedFields, rhsPeer) = rhs { + return lhsSectionId == rhsSectionId && lhsIndex == rhsIndex && lhsRequestedFields == rhsRequestedFields && lhsPeer.isEqual(rhsPeer) + } else { + return false + } + case let .accept(sectionId, index, enabled): + if case .accept(sectionId, index, enabled) = rhs { + return true + } else { + return false + } + case let .emptyField(sectionId, index, fieldType, lhsValue, lhsRelativeValue, lhsRelative, title, desc, isError): + if case .emptyField(sectionId, index, fieldType, let rhsValue, let rhsRelativeValue, let rhsRelative, title, desc, isError) = rhs { + return lhsRelative == rhsRelative && lhsValue == rhsValue && lhsRelativeValue == rhsRelativeValue + } else { + return false + } + case let .filledField(sectionId, index, fieldType, lhsRelative, title, description, lhsValue, lhsRelativeValue): + if case .filledField(sectionId, index, fieldType, let rhsRelative, title, description, let rhsValue, let rhsRelativeValue) = rhs { + return lhsValue == rhsValue && lhsRelative == rhsRelative && lhsRelativeValue == rhsRelativeValue + } else { + return false + } + case let .savedField(sectionId, index, fieldType, relative, relativeValues, title, description): + if case .savedField(sectionId, index, fieldType, relative, relativeValues, title, description) = rhs { + return true + } else { + return false + } + case let .description(sectionId, index, text, detectBold): + if case .description(sectionId, index, text, detectBold) = rhs { + return true + } else { + return false + } + case let .inputEmailCode(sectionId, index, text, limit, error): + if case .inputEmailCode(sectionId, index, text, limit, error) = rhs { + return true + } else { + return false + } + case let .requestPassword(sectionId, index, hasRecoveryEmail, isSettings, lhsError): + if case .requestPassword(sectionId, index, hasRecoveryEmail, isSettings, let rhsError) = rhs { + return lhsError == rhsError + } else { + return false + } + case let .createPassword(sectionId, index, lhsPeer): + if case .createPassword(sectionId, index, let rhsPeer) = rhs { + return lhsPeer.isEqual(rhsPeer) + } else { + return false + } + case let .deletePassport(sectionId, index): + if case .deletePassport(sectionId, index) = rhs { + return true + } else { + return false + } + case let .settingsHeader(sectionId, index): + if case .settingsHeader(sectionId, index) = rhs { + return true + } else { + return false + } + case .loading: + if case .loading = rhs { + return true + } else { + return false + } + case let .sectionId(id): + if case .sectionId(id) = rhs { + return true + } else { + return false + } + } +} + +private func passportEntries(encryptedForm: EncryptedSecureIdForm?, form: SecureIdForm?, peer: Peer?, passwordData: TwoStepVerificationConfiguration?, state: PassportState) -> ([PassportEntry], Bool) { + var entries:[PassportEntry] = [] + + var enabled: Bool = false + + + if let _ = passwordData { + var sectionId: Int32 = 0 + var index: Int32 = 0 + + if state.viewState == .settings { + entries.append(.sectionId(sectionId)) + sectionId += 1 + + let pdKeys:[SecureIdValueKey] = [.personalDetails, .idCard, .passport, .driversLicense, .internalPassport] + let pdValues:[SecureIdValue] = state.values.map{$0.value}.filter{pdKeys.contains($0.key)} + let pdDesc = pdValues.map({$0.key.requestFieldType.rawValue}).joined(separator: ", ") + entries.append(.savedField(sectionId: sectionId, index: index, valueType: .personalDetails, relative: pdKeys, relativeValues: pdValues, title: L10n.secureIdIdentityDocument, description: pdDesc.isEmpty ? L10n.secureIdRequestPermissionIdentityEmpty : pdDesc)) + index += 1 + + let aKeys:[SecureIdValueKey] = [.address, .rentalAgreement, .utilityBill, .bankStatement, .passportRegistration, .temporaryRegistration] + // + let aValues:[SecureIdValue] = state.values.map{$0.value}.filter{aKeys.contains($0.key)} + let aDesc = aValues.map({$0.key.requestFieldType.rawValue}).joined(separator: ", ") + entries.append(.savedField(sectionId: sectionId, index: index, valueType: .address, relative: aKeys, relativeValues: aValues, title: L10n.secureIdResidentialAddress, description: aDesc.isEmpty ? L10n.secureIdRequestPermissionAddressEmpty : aDesc)) + index += 1 + + var pValue: String? = nil + var eValue: String? = nil + + if let value = state.values.filter({$0.value.key == .phone}).first { + switch value.value { + case let .phone(value): + pValue = formatPhoneNumber(value.phone) + default: + break + } + } + if let value = state.values.filter({$0.value.key == .email}).first { + switch value.value { + case let .email(value): + eValue = value.email + default: + break + } + } + + entries.append(.savedField(sectionId: sectionId, index: index, valueType: .phone, relative: [], relativeValues: state.values.filter{$0.value.key == .phone}.map {$0.value}, title: L10n.secureIdPhoneNumber, description: pValue ?? L10n.secureIdRequestPermissionPhoneEmpty)) + index += 1 + + + entries.append(.savedField(sectionId: sectionId, index: index, valueType: .email, relative: [], relativeValues: state.values.filter{$0.value.key == .email}.map {$0.value}, title: L10n.secureIdEmail, description: eValue ?? L10n.secureIdRequestPermissionEmailEmpty)) + index += 1 + + entries.append(.sectionId(sectionId)) + sectionId += 1 + if !state.values.isEmpty { + entries.append(.deletePassport(sectionId: sectionId, index: index)) + index += 1 + } + + } else if state.passwordSettings == nil { + if let passwordData = passwordData { + switch passwordData { + case let .notSet(pendingEmail): + + entries.append(.sectionId(sectionId)) + sectionId += 1 + + if let pendingEmail = pendingEmail { + + entries.append(.inputEmailCode(sectionId: sectionId, index: index, text: state.emailCode, limit: pendingEmail.codeLength, error: state.emailCodeError)) + + + let emailText = L10n.twoStepAuthConfirmationTextNew + "\n\n\(pendingEmail.pattern)\n\n[" + L10n.twoStepAuthConfirmationAbort + "](abortVerification)" + entries.append(.description(sectionId: sectionId, index: index, text: emailText, detectBold: false)) + index += 1 + } else { + if let peer = peer { + entries.append(.createPassword(sectionId: sectionId, index: index, peer: peer)) + index += 1 + } + } + case let .set(_, hasRecoveryEmail, _, _, _): + + if state.tmpPwd == nil { + if let peer = peer, let form = encryptedForm { + entries.append(.sectionId(sectionId)) + sectionId += 1 + + entries.append(.sectionId(sectionId)) + sectionId += 1 + entries.append(.sectionId(sectionId)) + sectionId += 1 + + entries.append(.header(sectionId: sectionId, index: index, requestedFields: form.requestedFields, peer: peer)) + index += 1 + } + + + entries.append(.sectionId(sectionId)) + sectionId += 1 + + if encryptedForm == nil { + entries.append(.sectionId(sectionId)) + sectionId += 1 + entries.append(.settingsHeader(sectionId: sectionId, index: index)) + index += 1 + + entries.append(.sectionId(sectionId)) + sectionId += 1 + entries.append(.sectionId(sectionId)) + sectionId += 1 + } + + entries.append(.sectionId(sectionId)) + sectionId += 1 + entries.append(.sectionId(sectionId)) + sectionId += 1 + + + + entries.append(.requestPassword(sectionId: sectionId, index: index, hasRecoveryEmail: hasRecoveryEmail, isSettings: encryptedForm == nil, error: state.passwordError)) + index += 1 + } else { + entries.append(.loading) + } + + } + } + + } else if let form = form, let peer = peer { + + entries.append(.sectionId(sectionId)) + sectionId += 1 + + entries.append(.header(sectionId: sectionId, index: index, requestedFields: form.requestedFields, peer: peer)) + index += 1 + + entries.append(.sectionId(sectionId)) + sectionId += 1 + + entries.append(.description(sectionId: sectionId, index: index, text: L10n.secureIdRequestedInformationHeader, detectBold: true)) + index += 1 + + var filledCount: Int32 = 0 + + + let requestedFields = form.requestedFields.map { value -> SecureIdRequestedFormField in + switch value { + case let .just(key): + switch key { + case .email, .phone, .personalDetails, .address: + return value + default: + return .oneOf([key]) + } + default: + return value + } + } + + let idCount = requestedFields.filter { $0.isIdentityField }.count + let adCount = requestedFields.filter { $0.isAddressField }.count + + let isDetailsIndepend: Bool = idCount > 1 || idCount == 0 + let isAddressIndepend: Bool = adCount > 1 || adCount == 0 + let hasAddress = requestedFields.filter { value in + switch value { + case let .just(value): + switch value { + case .address: + return true + default: + return false + } + default: + return false + } + }.count == 1 + + let hasDetails = requestedFields.filter { value in + switch value { + case let .just(value): + switch value { + case .personalDetails: + return true + default: + return false + } + default: + return false + } + }.count == 1 + + var hasNativeName = requestedFields.filter { value in + switch value { + case let .just(value): + switch value { + case let .personalDetails(nativeName): + return nativeName + default: + return false + } + default: + return false + } + }.count == 1 + + if hasNativeName, let value = state.searchValue(.personalDetails)?.personalDetails { + if state.configuration?.nativeLanguageByCountry[value.residenceCountryCode] == "en" { + hasNativeName = false + } + } + + for field in requestedFields { + switch field { + case let .just(field): + switch field { + case .address: + if isAddressIndepend { + var isFilled: Bool = false + let desc: FieldDescription + let errors = state.errors[.address] ?? [:] + if let value = state.searchValue(field.valueKey), case let .address(address) = value { + + let errorText = errors[InputDataEmptyIdentifier]?.description ?? (errors.isEmpty ? "" : errors.first!.value.description) + if errorText.isEmpty { + let text = [address.street1, address.city, address.state, cManager.item(bySmallCountryName: address.countryCode)?.shortName ?? address.countryCode].compactMap {$0}.filter {!$0.isEmpty}.joined(separator: ", ") + desc = FieldDescription(text: text) + } else { + desc = FieldDescription(text: errorText, isError: true) + } + isFilled = true + + entries.append(.filledField(sectionId: sectionId, index: index, fieldType: FieldRequest(field), relative: [], title: L10n.secureIdRequestPermissionResidentialAddress, description: desc, value: value, relativeValue: nil)) + filledCount += 1 + + } + if !isFilled { + entries.append(.emptyField(sectionId: sectionId, index: index, fieldType: FieldRequest(field), value: nil, relativeValue: nil, relative: [], title: L10n.secureIdRequestPermissionResidentialAddress, desc: field.emptyDescription, isError: state.emptyErrors)) + } + index += 1 + } + case .personalDetails: + if isDetailsIndepend { + var isFilled: Bool = false + let desc: FieldDescription + let errors = state.errors[.personalDetails] ?? [:] + let errorText = errors[InputDataEmptyIdentifier]?.description ?? (errors.isEmpty ? "" : errors.first!.value.description) + + if let value = state.searchValue(field.valueKey), case let .personalDetails(details) = value, (!hasNativeName || (hasNativeName && details.nativeName != nil)) { + if errorText.isEmpty { + let text = [details.latinName.firstName + " " + details.latinName.lastName, details.gender.stringValue, details.birthdate.stringValue, cManager.item(bySmallCountryName: details.countryCode)?.shortName ?? details.countryCode].compactMap {$0}.filter {!$0.isEmpty}.joined(separator: ", ") + desc = FieldDescription(text: text) + } else { + desc = FieldDescription(text: errorText, isError: true) + } + + isFilled = true + entries.append(.filledField(sectionId: sectionId, index: index, fieldType: FieldRequest(field), relative: [], title: L10n.secureIdRequestPermissionPersonalDetails, description: desc, value: value, relativeValue: nil)) + filledCount += 1 + + } + if !isFilled { + entries.append(.emptyField(sectionId: sectionId, index: index, fieldType: FieldRequest(field), value: nil, relativeValue: nil, relative: [], title: L10n.secureIdRequestPermissionPersonalDetails, desc: field.emptyDescription, isError: state.emptyErrors)) + } + index += 1 + } + case .email: + if let value = state.searchValue(field.valueKey), case let .email(email) = value { + entries.append(.filledField(sectionId: sectionId, index: index, fieldType: FieldRequest(field), relative: [], title: field.rawValue, description: FieldDescription(text: email.email), value: value, relativeValue: nil)) + filledCount += 1 + + } else { + entries.append(.emptyField(sectionId: sectionId, index: index, fieldType: FieldRequest(field), value: nil, relativeValue: nil, relative: [], title: field.rawValue, desc: field.emptyDescription, isError: state.emptyErrors)) + } + index += 1 + case .phone: + if let value = state.searchValue(field.valueKey), case let .phone(phone) = value { + entries.append(.filledField(sectionId: sectionId, index: index, fieldType: FieldRequest(field), relative: [], title: field.rawValue, description: FieldDescription(text: formatPhoneNumber(phone.phone)), value: value, relativeValue: nil)) + filledCount += 1 + } else { + entries.append(.emptyField(sectionId: sectionId, index: index, fieldType: FieldRequest(field), value: nil, relativeValue: nil, relative: [], title: field.rawValue, desc: field.emptyDescription, isError: state.emptyErrors)) + } + index += 1 + default: + fatalError() +// entries.append(.emptyField(sectionId: sectionId, index: index, fieldType: FieldRequest(field.primary, field, hasDetails && !isDetailsIndepend), relative: [field], title: field.rawValue, desc: field.emptyDescription, isError: state.emptyErrors)) +// index += 1 + } + case let .oneOf(fields): + + var descText: String + if fields.count == 1 { + descText = L10n.secureIdUploadScanSingle(fields[0].rawValue.lowercased()) + } else { + let all = fields.prefix(fields.count - 1).reduce("", { current, value in + if current.isEmpty { + return value.rawValue.lowercased() + } + return current + ", " + value.rawValue.lowercased() + }) + descText = L10n.secureIdUploadScanMulti(all, fields[fields.count - 1].rawValue.lowercased()) + } + + switch fields[0] { + case .bankStatement, .rentalAgreement, .utilityBill, .passportRegistration, .temporaryRegistration: + + let secondary:SecureIdValue? = fields.compactMap {state.searchValue($0.valueKey)}.first + let address = hasAddress ? state.searchValue(.address) : nil + + let title: String + switch fields.count { + case 1: + title = fields[0].rawValue + case 2: + title = L10n.secureIdRequestTwoDocumentsTitle(fields[0].rawValue, fields[1].rawValue) + default: + title = L10n.secureIdRequestPermissionResidentialAddress + } + + if let secondary = secondary?.verificationDocuments { + descText = L10n.secureIdAddressScansCountable(secondary.count) + } + + if let value = address, case let .address(address) = value, !isAddressIndepend { + descText = (secondary?.verificationDocuments != nil ? descText + ", " : "") + [address.street1, address.city, address.state, cManager.item(bySmallCountryName: address.countryCode)?.shortName ?? address.countryCode].compactMap {$0}.filter {!$0.isEmpty}.joined(separator: ", ") + } + + + if let secondary = secondary?.verificationDocuments, address == nil { + descText = L10n.secureIdAddressScansCountable(secondary.count) + } else if let value = address, case let .address(address) = value, hasAddress && !isAddressIndepend { + descText = [address.street1, address.city, address.state, cManager.item(bySmallCountryName: address.countryCode)?.shortName ?? address.countryCode].compactMap {$0}.filter {!$0.isEmpty}.joined(separator: ", ") + } + + if fields.count > 1, let secondary = secondary { + descText = secondary.requestFieldType.rawValue + ", " + descText + } + + var isUnfilled: Bool = false + + if let secondary = secondary { + if let result = fields.filter({secondary.requestFieldType.valueKey == $0.valueKey}).first { + if result.hasTranslation && (secondary.translations == nil || secondary.translations!.isEmpty) { + isUnfilled = true + descText = L10n.secureIdRequestUploadTranslation + } + } + } + + + + var relative: [SecureIdRequestedFormFieldValue] = fields + if let secondary = secondary { + relative = [fields.filter({secondary.requestFieldType.valueKey == $0.valueKey}).first!] + } + + var errors:[InputDataIdentifier : InputDataValueError] = [:] + + + if hasAddress && !isAddressIndepend { + let primary = (state.errors[.address] ?? [:]) + for (key, value) in primary { + errors[key] = value + } + } + for field in fields { + let secondary = (state.errors[field.valueKey] ?? [:]) + for (key, value) in secondary { + errors[key] = value + } + } + + let errorText = errors[InputDataEmptyIdentifier]?.description ?? (errors.isEmpty ? "" : errors.first!.value.description) + + if !errorText.isEmpty { + descText = errorText + } + let addressField:SecureIdRequestedFormFieldValue = requestedFields.filter({$0.valueKey == .address}).first?.fieldValue ?? .address + + if let secondary = secondary, (!hasAddress || isAddressIndepend || (hasAddress && address != nil)), !isUnfilled { + let desc: FieldDescription = FieldDescription(text: descText, isError: !errorText.isEmpty) + entries.append(.filledField(sectionId: sectionId, index: index, fieldType: FieldRequest(addressField, relative[0], hasAddress && !isAddressIndepend), relative: relative, title: title, description: desc, value: address, relativeValue: secondary)) + if !desc.isError { + filledCount += 1 + } + } else { + entries.append(.emptyField(sectionId: sectionId, index: index, fieldType: FieldRequest(addressField, relative[0], hasAddress && !isAddressIndepend), value: address, relativeValue: secondary, relative: relative, title: title, desc: descText, isError: state.emptyErrors)) + } + + index += 1 + case .passport, .idCard, .driversLicense, .internalPassport: + + let secondary:SecureIdValue? = fields.compactMap {state.searchValue($0.valueKey)}.first + let details = hasDetails ? state.searchValue(.personalDetails) : nil + + let title: String + switch fields.count { + case 1: + title = fields[0].rawValue + case 2: + title = L10n.secureIdRequestTwoDocumentsTitle(fields[0].rawValue, fields[1].rawValue) + default: + title = L10n.secureIdRequestPermissionIdentityDocument + } + + + if let secondary = secondary { + descText = secondary.identifier ?? "" + } + + + if let value = details, case let .personalDetails(details) = value, hasDetails && !isDetailsIndepend { + descText = (secondary != nil ? descText + ", " : "") + [details.latinName.firstName + " " + details.latinName.lastName, details.gender.stringValue, details.birthdate.stringValue, cManager.item(bySmallCountryName: details.countryCode)?.shortName ?? details.countryCode].compactMap {$0}.filter {!$0.isEmpty}.joined(separator: ", ") + } + + if fields.count > 1, let secondary = secondary { + descText = secondary.requestFieldType.rawValue + ", " + descText + } + + var isUnfilled: Bool = false + + if let secondary = secondary { + if let result = fields.filter({secondary.requestFieldType.valueKey == $0.valueKey}).first { + if result.hasSelfie && secondary.selfieVerificationDocument == nil { + isUnfilled = true + descText = L10n.secureIdRequestUploadSelfie + } + if result.hasTranslation && (secondary.translations == nil || secondary.translations!.isEmpty) { + isUnfilled = true + descText = L10n.secureIdRequestUploadTranslation + } + } + } + + var relative: [SecureIdRequestedFormFieldValue] = fields + + + if let secondary = secondary { + relative = [fields.filter({secondary.requestFieldType.valueKey == $0.valueKey}).first!] + } + + var errors:[InputDataIdentifier : InputDataValueError] = [:] + + + if hasDetails && !isDetailsIndepend { + let primary = (state.errors[.personalDetails] ?? [:]) + for (key, value) in primary { + errors[key] = value + } + } + for field in fields { + let secondary = (state.errors[field.valueKey] ?? [:]) + for (key, value) in secondary { + errors[key] = value + } + } + + let errorText = errors[InputDataEmptyIdentifier]?.description ?? (errors.isEmpty ? "" : errors.first!.value.description) + + if !errorText.isEmpty { + descText = errorText + } + + let personalField:SecureIdRequestedFormFieldValue = requestedFields.filter({$0.valueKey == .personalDetails}).first?.fieldValue ?? .personalDetails(nativeName: true) + + if let secondary = secondary, (!hasDetails || isDetailsIndepend || (hasDetails && details != nil)), !isUnfilled, (!hasNativeName || (hasNativeName && details?.personalDetails?.nativeName != nil)) { + let desc: FieldDescription = FieldDescription(text: descText, isError: !errorText.isEmpty) + entries.append(.filledField(sectionId: sectionId, index: index, fieldType: FieldRequest(personalField, relative[0], hasDetails && !isDetailsIndepend), relative: relative, title: title, description: desc, value: details, relativeValue: secondary)) + if !desc.isError { + filledCount += 1 + } + + } else { + entries.append(.emptyField(sectionId: sectionId, index: index, fieldType: FieldRequest(personalField, relative[0], hasDetails && !isDetailsIndepend), value: details, relativeValue: secondary, relative: relative, title: title, desc: descText, isError: state.emptyErrors)) + } + index += 1 + default: + break + } + } + + } + let policyText = encryptedForm?.termsUrl != nil ? L10n.secureIdAcceptPolicy("@\(peer.addressName ?? "")") : L10n.secureIdAcceptHelp("@\(peer.addressName ?? "")", "@\(peer.addressName ?? "")") + entries.append(.description(sectionId: sectionId, index: index, text: policyText, detectBold: true)) + index += 1 + + entries.append(.sectionId(sectionId)) + sectionId += 1 + + enabled = filledCount == (requestedFields.count - (hasDetails && !isDetailsIndepend ? 1 : 0) - (hasAddress && !isAddressIndepend ? 1 : 0)) + + // entries.append(.accept(sectionId: sectionId, index: index, enabled: filledCount == form.requestedFields.count - relativeAddress.count - relativeIdentity.count)) + // index += 1 + } + + } else { + entries.append(.loading) + } + + return (entries, enabled) +} + +fileprivate func prepareTransition(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], initialSize:NSSize, arguments: PassportArguments) -> TableUpdateTransition { + let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in + return entry.entry.item(arguments, initialSize: initialSize) + } + return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true) +} + +private final class EmailIntermediateState : Equatable { + let email: String + let length: Int32 + init(email: String, length: Int32) { + self.email = email + self.length = length + } +} +private func ==(lhs: EmailIntermediateState, rhs: EmailIntermediateState) -> Bool { + return lhs.email == rhs.email && lhs.length == rhs.length +} + +private final class DetailsIntermediateState : Equatable { + let firstName: InputDataValue? + let middleName: InputDataValue? + let lastName: InputDataValue? + let firstNameNative: InputDataValue? + let middleNameNative: InputDataValue? + let lastNameNative: InputDataValue? + + let birthday: InputDataValue? + let citizenship: InputDataValue? + let residence: InputDataValue? + let gender: InputDataValue? + let expiryDate: InputDataValue? + let identifier: InputDataValue? + init(firstName: InputDataValue?, middleName: InputDataValue?, lastName: InputDataValue?, firstNameNative: InputDataValue?, middleNameNative: InputDataValue?, lastNameNative: InputDataValue?, birthday: InputDataValue?, citizenship: InputDataValue?, residence: InputDataValue?, gender: InputDataValue?, expiryDate: InputDataValue?, identifier: InputDataValue?) { + self.firstName = firstName + self.middleName = middleName + self.lastName = lastName + self.firstNameNative = firstNameNative + self.lastNameNative = lastNameNative + self.middleNameNative = middleNameNative + self.birthday = birthday + self.citizenship = citizenship + self.residence = residence + self.gender = gender + self.expiryDate = expiryDate + self.identifier = identifier + } + + convenience init(_ data: [InputDataIdentifier : InputDataValue]) { + self.init(firstName: data[_id_first_name], middleName: data[_id_middle_name], lastName: data[_id_last_name], firstNameNative: data[_id_first_name_native], middleNameNative: data[_id_middle_name_native], lastNameNative: data[_id_last_name_native], birthday: data[_id_birthday], citizenship: data[_id_country], residence: data[_id_residence], gender: data[_id_gender], expiryDate: data[_id_expire_date], identifier: data[_id_identifier]) + } + + fileprivate func validateErrors(currentState: DetailsIntermediateState, errors:[InputDataIdentifier : InputDataValueError]?, relativeErrors: [InputDataIdentifier : InputDataValueError]?) -> [InputDataIdentifier : InputDataValidationFailAction] { + var fails:[InputDataIdentifier : InputDataValidationFailAction] = [:] + + if errors?[_id_first_name] != nil, currentState.firstName == firstName { + fails[_id_first_name] = .shake + } + if errors?[_id_middle_name] != nil, currentState.middleName == middleName { + fails[_id_middle_name] = .shake + } + if errors?[_id_last_name] != nil, currentState.lastName == lastName { + fails[_id_last_name] = .shake + } + + if errors?[_id_first_name_native] != nil, currentState.firstNameNative == firstNameNative { + fails[_id_first_name_native] = .shake + } + if errors?[_id_middle_name_native] != nil, currentState.middleNameNative == middleNameNative { + fails[_id_middle_name_native] = .shake + } + if errors?[_id_last_name_native] != nil, currentState.lastNameNative == lastNameNative { + fails[_id_last_name_native] = .shake + } + + if errors?[_id_birthday] != nil, currentState.birthday == birthday { + fails[_id_birthday] = .shake + } + if errors?[_id_country] != nil, currentState.citizenship == citizenship { + fails[_id_country] = .shake + } + if errors?[_id_residence] != nil, currentState.residence == residence { + fails[_id_residence] = .shake + } + if errors?[_id_gender] != nil, currentState.gender == gender { + fails[_id_gender] = .shake + } + + if relativeErrors?[_id_expire_date] != nil, currentState.expiryDate == expiryDate { + fails[_id_expire_date] = .shake + } + if relativeErrors?[_id_identifier] != nil, currentState.identifier == identifier { + fails[_id_identifier] = .shake + } + return fails + } + + fileprivate func removeErrors(currentState: DetailsIntermediateState, errors:[InputDataIdentifier : InputDataValueError]?, relativeErrors: [InputDataIdentifier : InputDataValueError]?) -> (errors: [InputDataIdentifier : InputDataValueError], relativeErrors: [InputDataIdentifier : InputDataValueError]) { + + var errors = errors ?? [:] + var relativeErrors = relativeErrors ?? [:] + + if errors[_id_first_name] != nil, currentState.firstName != firstName { + errors.removeValue(forKey: _id_first_name) + } + if errors[_id_middle_name] != nil, currentState.middleName != middleName { + errors.removeValue(forKey: _id_middle_name) + } + if errors[_id_last_name] != nil, currentState.lastName != lastName { + errors.removeValue(forKey: _id_last_name) + } + + if errors[_id_first_name_native] != nil, currentState.firstNameNative != firstNameNative { + errors.removeValue(forKey: _id_first_name_native) + } + if errors[_id_middle_name_native] != nil, currentState.middleNameNative != middleNameNative { + errors.removeValue(forKey: _id_middle_name_native) + } + if errors[_id_last_name_native] != nil, currentState.lastNameNative != lastNameNative { + errors.removeValue(forKey: _id_last_name_native) + } + + if errors[_id_birthday] != nil, currentState.birthday != birthday { + errors.removeValue(forKey: _id_birthday) + } + if errors[_id_country] != nil, currentState.citizenship != citizenship { + errors.removeValue(forKey: _id_country) + } + if errors[_id_residence] != nil, currentState.residence != residence { + errors.removeValue(forKey: _id_residence) + } + if errors[_id_gender] != nil, currentState.gender != gender { + errors.removeValue(forKey: _id_gender) + } + + if relativeErrors[_id_expire_date] != nil, currentState.expiryDate != expiryDate { + relativeErrors.removeValue(forKey: _id_expire_date) + } + if relativeErrors[_id_identifier] != nil, currentState.identifier != identifier { + relativeErrors.removeValue(forKey: _id_identifier) + } + return (errors: errors, relativeErrors: relativeErrors) + } +} + +private func ==(lhs: DetailsIntermediateState, rhs: DetailsIntermediateState) -> Bool { + return lhs.firstName == rhs.firstName && lhs.lastName == rhs.lastName && lhs.birthday == rhs.birthday && lhs.residence == rhs.residence && lhs.citizenship == rhs.citizenship && lhs.gender == rhs.gender && lhs.expiryDate == rhs.expiryDate && lhs.identifier == rhs.identifier +} + +private final class AddressIntermediateState : Equatable { + let street1: InputDataValue? + let street2: InputDataValue? + let city: InputDataValue? + let state: InputDataValue? + let countryCode: InputDataValue? + let postcode: InputDataValue? + + init(street1: InputDataValue?, street2: InputDataValue?, city: InputDataValue?, state: InputDataValue?, countryCode: InputDataValue?, postcode: InputDataValue?) { + self.street1 = street1 + self.street2 = street2 + self.city = city + self.state = state + self.countryCode = countryCode + self.postcode = postcode + } + + convenience init(_ data: [InputDataIdentifier : InputDataValue]) { + self.init(street1: data[_id_street1], street2: data[_id_street2], city: data[_id_city], state: data[_id_state], countryCode: data[_id_country], postcode: data[_id_postcode]) + } + + fileprivate func validateErrors(currentState: AddressIntermediateState, errors:[InputDataIdentifier : InputDataValueError]?) -> [InputDataIdentifier : InputDataValidationFailAction] { + var fails:[InputDataIdentifier : InputDataValidationFailAction] = [:] + + if errors?[_id_street1] != nil, currentState.street1 == street1 { + fails[_id_street1] = .shake + } + if errors?[_id_street2] != nil, currentState.street2 == street2 { + fails[_id_street2] = .shake + } + if errors?[_id_state] != nil, currentState.state == state { + fails[_id_state] = .shake + } + if errors?[_id_city] != nil, currentState.city == city { + fails[_id_city] = .shake + } + if errors?[_id_country] != nil, currentState.countryCode == countryCode { + fails[_id_country] = .shake + } + if errors?[_id_postcode] != nil, currentState.postcode == postcode { + fails[_id_postcode] = .shake + } + return fails + } + + fileprivate func removeErrors(currentState: AddressIntermediateState, errors:[InputDataIdentifier : InputDataValueError]?) -> [InputDataIdentifier : InputDataValueError] { + + var errors = errors ?? [:] + + if errors[_id_street1] != nil, currentState.street1 != street1 { + errors.removeValue(forKey: _id_street1) + } + if errors[_id_street2] != nil, currentState.street2 != street2 { + errors.removeValue(forKey: _id_street2) + } + if errors[_id_state] != nil, currentState.state != state { + errors.removeValue(forKey: _id_state) + } + if errors[_id_city] != nil, currentState.city != city { + errors.removeValue(forKey: _id_city) + } + if errors[_id_country] != nil, currentState.countryCode != countryCode { + errors.removeValue(forKey: _id_country) + } + if errors[_id_postcode] != nil, currentState.postcode != postcode { + errors.removeValue(forKey: _id_postcode) + } + + return errors + } +} + +private func ==(lhs: AddressIntermediateState, rhs: AddressIntermediateState) -> Bool { + return lhs.street1 == rhs.street1 && lhs.street2 == rhs.street2 && lhs.city == rhs.city && lhs.state == rhs.state && lhs.countryCode == rhs.countryCode && lhs.postcode == rhs.postcode +} + +private enum PassportViewState { + case plain + case settings +} + +private final class PassportState : Equatable { + let context: AccountContext + let peer: Peer + let values:[SecureIdValueWithContext] + let accessContext: SecureIdAccessContext? + let password: UpdateTwoStepVerificationPasswordResult? + let passwordSettings: TwoStepVerificationSettings? + let verifyDocumentContext: SecureIdVerificationDocumentsContext? + let files: [SecureIdValueKey : [SecureIdVerificationDocument]] + + let emailIntermediateState: EmailIntermediateState? + + let detailsIntermediateState: DetailsIntermediateState? + let addressIntermediateState: AddressIntermediateState? + + let errors:[SecureIdValueKey: [InputDataIdentifier : InputDataValueError]] + + let selfies:[SecureIdValueKey : SecureIdVerificationDocument] + let translations: [SecureIdValueKey : [SecureIdVerificationDocument]] + let frontSideFile: [SecureIdValueKey : SecureIdVerificationDocument] + let backSideFile: [SecureIdValueKey : SecureIdVerificationDocument] + + let viewState: PassportViewState + + let emptyErrors:Bool + + let tmpPwd: String? + + let configuration: SecureIdConfiguration? + + let passwordError: String? + + let emailCode: String + let emailCodeError: InputDataValueError? + + init(context: AccountContext, peer: Peer, tmpPwd: String?, viewState: PassportViewState, errors: [SecureIdValueKey: [InputDataIdentifier : InputDataValueError]] = [:], passwordSettings:TwoStepVerificationSettings? = nil, password: UpdateTwoStepVerificationPasswordResult? = nil, values: [SecureIdValueWithContext] = [], accessContext: SecureIdAccessContext? = nil, verifyDocumentContext: SecureIdVerificationDocumentsContext? = nil, files: [SecureIdValueKey : [SecureIdVerificationDocument]] = [:], emailIntermediateState: EmailIntermediateState? = nil, detailsIntermediateState: DetailsIntermediateState? = nil, addressIntermediateState: AddressIntermediateState? = nil, selfies: [SecureIdValueKey : SecureIdVerificationDocument] = [:], translations: [SecureIdValueKey : [SecureIdVerificationDocument]] = [:], frontSideFile: [SecureIdValueKey : SecureIdVerificationDocument] = [:], backSideFile: [SecureIdValueKey : SecureIdVerificationDocument] = [:], emptyErrors: Bool = false, configuration: SecureIdConfiguration? = nil, passwordError: String? = nil, emailCode: String = "", emailCodeError: InputDataValueError? = nil) { + self.context = context + self.peer = peer + self.errors = errors + self.passwordSettings = passwordSettings + self.password = password + self.values = values + self.viewState = viewState + self.tmpPwd = tmpPwd + self.accessContext = accessContext + self.verifyDocumentContext = verifyDocumentContext + self.files = files + self.emailIntermediateState = emailIntermediateState + self.detailsIntermediateState = detailsIntermediateState + self.addressIntermediateState = addressIntermediateState + self.selfies = selfies + self.translations = translations + self.frontSideFile = frontSideFile + self.backSideFile = backSideFile + self.emptyErrors = emptyErrors + self.configuration = configuration + self.passwordError = passwordError + self.emailCode = emailCode + self.emailCodeError = emailCodeError + let translations:[SecureIdVerificationDocument] = translations.reduce([], { current, value in + return current + value.value + }) + + self.verifyDocumentContext?.stateUpdated(files.reduce(Array(selfies.values) + Array(frontSideFile.values) + Array(backSideFile.values) + translations, { (current, value) -> [SecureIdVerificationDocument] in + return current + value.value + })) + } + + func searchValue(_ valueKey: SecureIdValueKey) -> SecureIdValue? { + let index = values.index { value -> Bool in + return value.value.isSame(of: valueKey) + } + if let index = index { + return values[index].value + } + return nil + } + + + func withUpdatedTmpPwd(_ tmpPwd: String?) -> PassportState { + return PassportState(context: self.context, peer: self.peer, tmpPwd: tmpPwd, viewState: self.viewState, errors: self.errors, passwordSettings: self.passwordSettings, password: self.password, values: self.values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: self.files, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: addressIntermediateState, selfies: self.selfies, translations: self.translations, frontSideFile: self.frontSideFile, backSideFile: self.backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + + func withUpdatedPassword(_ password: UpdateTwoStepVerificationPasswordResult?) -> PassportState { + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: self.errors, passwordSettings: self.passwordSettings, password: password, values: self.values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: self.files, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: addressIntermediateState, selfies: self.selfies, translations: self.translations, frontSideFile: self.frontSideFile, backSideFile: self.backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + + func withUpdatedPasswordSettings(_ settings: TwoStepVerificationSettings?) -> PassportState { + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: self.errors, passwordSettings: settings, password: self.password, values: self.values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: self.files, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: addressIntermediateState, selfies: self.selfies, translations: self.translations, frontSideFile: self.frontSideFile, backSideFile: self.backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + + + func withUpdatedValues(_ values:[SecureIdValueWithContext]) -> PassportState { + var current = self + for value in values { + current = current.withUpdatedValue(value) + } + return current + } + + func withUpdatedValue(_ value: SecureIdValueWithContext) -> PassportState { + var values = self.values + let index = values.index { v -> Bool in + return value.value.isSame(of: v.value) + } + if let index = index { + values[index] = value + } else { + values.append(value) + } + + var files = self.files + if let verificationDocuments = value.value.verificationDocuments { + files[value.value.key] = verificationDocuments.compactMap { reference in + switch reference { + case let .remote(file): + return .remote(file) + default: + return nil + } + } + } + + for (key, _) in self.files { + let index = values.index { v -> Bool in + return v.value.key == key + } + if index == nil { + files[key] = [] + } + } + + var selfies = self.selfies + if let selfie = value.value.selfieVerificationDocument { + switch selfie { + case let .remote(file): + selfies[value.value.key] = .remote(file) + default: + selfies[value.value.key] = nil + } + } + + var frontSideFile = self.frontSideFile + if let frontSide = value.value.frontSideVerificationDocument { + switch frontSide { + case let .remote(file): + frontSideFile[value.value.key] = .remote(file) + default: + frontSideFile[value.value.key] = nil + } + } + + var backSideFile = self.backSideFile + if let backSide = value.value.backSideVerificationDocument { + switch backSide { + case let .remote(file): + backSideFile[value.value.key] = .remote(file) + default: + backSideFile[value.value.key] = nil + } + } + + var translations = self.translations + if let translationsDocuments = value.value.translations { + translations[value.value.key] = translationsDocuments.compactMap { reference in + switch reference { + case let .remote(file): + return .remote(file) + default: + return nil + } + } + } + + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: self.errors, passwordSettings: self.passwordSettings, password: self.password, values: values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: files, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: addressIntermediateState, selfies: selfies, translations: translations, frontSideFile: frontSideFile, backSideFile: backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + + func withRemovedValue(_ key: SecureIdValueKey) -> PassportState { + var values = self.values + let index = values.index { v -> Bool in + return v.value.key == key + } + if let index = index { + values.remove(at: index) + } + var files = self.files + files.removeValue(forKey: key) + + var translations = self.translations + translations.removeValue(forKey: key) + + var selfies = self.selfies + selfies.removeValue(forKey: key) + + var frontSideFile = self.frontSideFile + frontSideFile.removeValue(forKey: key) + + var backSideFile = self.backSideFile + backSideFile.removeValue(forKey: key) + + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: self.errors, passwordSettings: self.passwordSettings, password: self.password, values: values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: files, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: addressIntermediateState, selfies: selfies, translations: translations, frontSideFile: frontSideFile, backSideFile: backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + + func withUpdatedAccessContext(_ accessContext: SecureIdAccessContext) -> PassportState { + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: self.errors, passwordSettings: self.passwordSettings, password: self.password, values: self.values, accessContext: accessContext, verifyDocumentContext: self.verifyDocumentContext, files: self.files, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: addressIntermediateState, selfies: self.selfies, translations: self.translations, frontSideFile: self.frontSideFile, backSideFile: self.backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + + func withUpdatedVerifyDocumentContext(_ verifyDocumentContext: SecureIdVerificationDocumentsContext) -> PassportState { + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: self.errors, passwordSettings: self.passwordSettings, password: self.password, values: self.values, accessContext: self.accessContext, verifyDocumentContext: verifyDocumentContext, files: self.files, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: addressIntermediateState, selfies: self.selfies, translations: self.translations, frontSideFile: self.frontSideFile, backSideFile: self.backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + + func withUpdatedFileState(id: Int64, state: SecureIdVerificationLocalDocumentState) -> PassportState { + var files = self.files + for (key, documents) in files { + loop: for i in 0 ..< documents.count { + let file = documents[i] + if file.id.hashValue == id { + switch file { + case var .local(document): + document.state = state + var documents = documents + documents[i] = .local(document) + files[key] = documents + break loop + default: + break + } + } + } + } + var selfies = self.selfies + loop: for (key, value) in self.selfies { + if value.id.hashValue == id { + switch value { + case var .local(document): + document.state = state + selfies[key] = .local(document) + break loop + default: + break + } + } + } + var frontSideFile = self.frontSideFile + loop: for (key, value) in self.frontSideFile { + if value.id.hashValue == id { + switch value { + case var .local(document): + document.state = state + frontSideFile[key] = .local(document) + break loop + default: + break + } + } + } + var backSideFile = self.backSideFile + loop: for (key, value) in self.backSideFile { + if value.id.hashValue == id { + switch value { + case var .local(document): + document.state = state + backSideFile[key] = .local(document) + break loop + default: + break + } + } + } + + var translations = self.translations + + loop: for (key, _values) in self.translations { + var values = _values + for i in 0 ..< _values.count { + let value = values[i] + if value.id.hashValue == id { + switch value { + case var .local(document): + document.state = state + values[i] = .local(document) + translations[key] = values + break loop + default: + break + } + } + } + } + + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: self.errors, passwordSettings: self.passwordSettings, password: self.password, values: self.values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: files, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: addressIntermediateState, selfies: selfies, translations: translations, frontSideFile: frontSideFile, backSideFile: backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + + func withAppendTranslations(_ translations: [SecureIdVerificationDocument], for valueKey: SecureIdValueKey) -> PassportState { + var current = self.translations[valueKey] ?? [] + current.append(contentsOf: translations) + current = Array(current.prefix(scansLimit)) + var dictionary = self.translations + dictionary[valueKey] = current + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: self.errors, passwordSettings: self.passwordSettings, password: self.password, values: self.values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: self.files, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: addressIntermediateState, selfies: self.selfies, translations: dictionary, frontSideFile: self.frontSideFile, backSideFile: self.backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + + func withUpdatedTranslations(_ translations: [SecureIdVerificationDocument], for valueKey: SecureIdValueKey) -> PassportState { + var dictionary = self.translations + dictionary[valueKey] = translations + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: self.errors, passwordSettings: self.passwordSettings, password: self.password, values: self.values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: dictionary, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: addressIntermediateState, selfies: self.selfies, translations: dictionary, frontSideFile: self.frontSideFile, backSideFile: self.backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + + func withRemovedTranslation(_ translation: SecureIdVerificationDocument, for valueKey: SecureIdValueKey) -> PassportState { + var translations = self.translations[valueKey] ?? [] + for i in 0 ..< translations.count { + if translations[i].id == translation.id { + translations.remove(at: i) + break + } + } + var dictionary = self.translations + dictionary[valueKey] = translations + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: self.errors, passwordSettings: self.passwordSettings, password: self.password, values: self.values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: self.files, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: addressIntermediateState, selfies: self.selfies, translations: dictionary, frontSideFile: self.frontSideFile, backSideFile: self.backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + + + func withAppendFiles(_ files: [SecureIdVerificationDocument], for valueKey: SecureIdValueKey) -> PassportState { + var current = self.files[valueKey] ?? [] + current.append(contentsOf: files) + current = Array(current.prefix(scansLimit)) + var dictionary = self.files + dictionary[valueKey] = current + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: self.errors, passwordSettings: self.passwordSettings, password: self.password, values: self.values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: dictionary, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: addressIntermediateState, selfies: self.selfies, translations: self.translations, frontSideFile: self.frontSideFile, backSideFile: self.backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + + func withUpdatedFiles(_ files: [SecureIdVerificationDocument], for valueKey: SecureIdValueKey) -> PassportState { + var dictionary = self.files + dictionary[valueKey] = files + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: self.errors, passwordSettings: self.passwordSettings, password: self.password, values: self.values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: dictionary, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: addressIntermediateState, selfies: self.selfies, translations: self.translations, frontSideFile: self.frontSideFile, backSideFile: self.backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + + func withRemovedFile(_ file: SecureIdVerificationDocument, for valueKey: SecureIdValueKey) -> PassportState { + var files = self.files[valueKey] + if let _ = files { + for i in 0 ..< files!.count { + if files![i].id == file.id { + files!.remove(at: i) + break + } + } + } + var dictionary = self.files + dictionary[valueKey] = files + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: self.errors, passwordSettings: self.passwordSettings, password: self.password, values: self.values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: dictionary, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: addressIntermediateState, selfies: self.selfies, translations: self.translations, frontSideFile: self.frontSideFile, backSideFile: self.backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + + func withRemovedValues() -> PassportState { + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: self.errors, passwordSettings: self.passwordSettings, password: self.password, values: [], accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: [:], emailIntermediateState: emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: addressIntermediateState, selfies: [:], translations: [:], frontSideFile: [:], backSideFile: [:], emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + + func withUpdatedIntermediateEmailState(_ emailIntermediateState: EmailIntermediateState?) -> PassportState { + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: self.errors, passwordSettings: self.passwordSettings, password: self.password, values: self.values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: self.files, emailIntermediateState: emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: addressIntermediateState, selfies: self.selfies, translations: self.translations, frontSideFile: self.frontSideFile, backSideFile: self.backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + + func withUpdatedDetailsState(_ detailsIntermediateState: DetailsIntermediateState?) -> PassportState { + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: self.errors, passwordSettings: self.passwordSettings, password: self.password, values: self.values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: self.files, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: detailsIntermediateState, addressIntermediateState: addressIntermediateState, selfies: self.selfies, translations: self.translations, frontSideFile: self.frontSideFile, backSideFile: self.backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + func withUpdatedAddressState(_ addressState: AddressIntermediateState?) -> PassportState { + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: self.errors, passwordSettings: self.passwordSettings, password: self.password, values: self.values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: self.files, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: addressState, selfies: self.selfies, translations: self.translations, frontSideFile: self.frontSideFile, backSideFile: self.backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + func withUpdatedViewState(_ viewState: PassportViewState) -> PassportState { + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: viewState, errors: self.errors, passwordSettings: self.passwordSettings, password: self.password, values: self.values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: self.files, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: self.addressIntermediateState, selfies: self.selfies, translations: self.translations, frontSideFile: self.frontSideFile, backSideFile: self.backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + func withUpdatedSelfie(_ value: SecureIdVerificationDocument?, for key: SecureIdValueKey) -> PassportState { + var selfies = self.selfies + if let value = value { + selfies[key] = value + } else { + selfies.removeValue(forKey: key) + } + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: self.errors, passwordSettings: self.passwordSettings, password: self.password, values: self.values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: self.files, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: self.addressIntermediateState, selfies: selfies, translations: self.translations, frontSideFile: self.frontSideFile, backSideFile: self.backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + + func withUpdatedFrontSide(_ value: SecureIdVerificationDocument?, for key: SecureIdValueKey) -> PassportState { + var frontSideFile = self.frontSideFile + if let value = value { + frontSideFile[key] = value + } else { + frontSideFile.removeValue(forKey: key) + } + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: self.errors, passwordSettings: self.passwordSettings, password: self.password, values: self.values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: self.files, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: self.addressIntermediateState, selfies: self.selfies, translations: self.translations, frontSideFile: frontSideFile, backSideFile: self.backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + + func withUpdatedBackSide(_ value: SecureIdVerificationDocument?, for key: SecureIdValueKey) -> PassportState { + var backSideFile = self.backSideFile + if let value = value { + backSideFile[key] = value + } else { + backSideFile.removeValue(forKey: key) + } + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: self.errors, passwordSettings: self.passwordSettings, password: self.password, values: self.values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: self.files, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: self.addressIntermediateState, selfies: self.selfies, translations: self.translations, frontSideFile: self.frontSideFile, backSideFile: backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + + func withUpdatedEmptyErrors(_ emptyErrors: Bool) -> PassportState { + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: self.errors, passwordSettings: self.passwordSettings, password: self.password, values: self.values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: self.files, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: self.addressIntermediateState, selfies: self.selfies, translations: self.translations, frontSideFile: self.frontSideFile, backSideFile: self.backSideFile, emptyErrors: emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + + func withUpdatedPasswordError(_ passwordError: String?) -> PassportState { + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: self.errors, passwordSettings: self.passwordSettings, password: self.password, values: self.values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: self.files, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: self.addressIntermediateState, selfies: self.selfies, translations: self.translations, frontSideFile: self.frontSideFile, backSideFile: self.backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + + func withRemovedInputErrors() -> PassportState { + var errors = self.errors + for (key, value) in self.errors { + errors[key] = value.filter({$0.value != latinError}) + } + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: errors, passwordSettings: self.passwordSettings, password: self.password, values: self.values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: self.files, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: self.addressIntermediateState, selfies: self.selfies, translations: self.translations, frontSideFile: self.frontSideFile, backSideFile: self.backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + + func withRemovedError(for valueKey:SecureIdValueKey, field: InputDataIdentifier) -> PassportState { + var valyeErrors = self.errors[valueKey] ?? [:] + valyeErrors = valyeErrors.filter({$0.key != field}) + var errors = self.errors + errors[valueKey] = valyeErrors + + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: errors, passwordSettings: self.passwordSettings, password: self.password, values: self.values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: self.files, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: self.addressIntermediateState, selfies: self.selfies, translations: self.translations, frontSideFile: self.frontSideFile, backSideFile: self.backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + + func errors(for key: SecureIdValueKey) -> [InputDataIdentifier : InputDataValueError] { + switch key { + case .address: + var aErrors = errors[key] ?? [:] + let rKeys:[SecureIdValueKey] = [.rentalAgreement, .utilityBill, .bankStatement] + for rKey in rKeys { + if let rErrors = errors[rKey] { + for(key, value) in rErrors { + aErrors[key] = value + } + } + } + return aErrors + case .personalDetails: + var aErrors = errors[key] ?? [:] + let rKeys:[SecureIdValueKey] = [.passport, .driversLicense, .idCard] + for rKey in rKeys { + if let rErrors = errors[rKey] { + for(key, value) in rErrors { + aErrors[key] = value + } + } + } + return aErrors + default: + return errors[key] ?? [:] + } + } + + func withRemovedErrors(for key: SecureIdValueKey) -> PassportState { + var errors = self.errors + switch key { + case .address: + errors.removeValue(forKey: key) + let rKeys:[SecureIdValueKey] = [.rentalAgreement, .utilityBill, .bankStatement] + for rKey in rKeys { + errors.removeValue(forKey: rKey) + } + case .personalDetails: + errors.removeValue(forKey: key) + let rKeys:[SecureIdValueKey] = [.passport, .driversLicense, .idCard] + for rKey in rKeys { + errors.removeValue(forKey: rKey) + } + default: + errors.removeValue(forKey: key) + } + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: errors, passwordSettings: self.passwordSettings, password: self.password, values: self.values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: self.files, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: self.addressIntermediateState, selfies: self.selfies, translations: self.translations, frontSideFile: self.frontSideFile, backSideFile: self.backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + func withUpdatedErrors(_ errors: [SecureIdValueKey: [InputDataIdentifier : InputDataValueError]]) -> PassportState { + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: errors, passwordSettings: self.passwordSettings, password: self.password, values: self.values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: self.files, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: self.addressIntermediateState, selfies: self.selfies, translations: self.translations, frontSideFile: self.frontSideFile, backSideFile: self.backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + + func withUpdatedConfiguration(_ configuration: SecureIdConfiguration?) -> PassportState { + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: self.errors, passwordSettings: self.passwordSettings, password: self.password, values: self.values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: self.files, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: self.addressIntermediateState, selfies: self.selfies, translations: self.translations, frontSideFile: self.frontSideFile, backSideFile: self.backSideFile, emptyErrors: self.emptyErrors, configuration: configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: self.emailCodeError) + } + + func withUpdatedEmailCode(_ emailCode: String) -> PassportState { + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: self.errors, passwordSettings: self.passwordSettings, password: self.password, values: self.values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: self.files, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: self.addressIntermediateState, selfies: self.selfies, translations: self.translations, frontSideFile: self.frontSideFile, backSideFile: self.backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: emailCode, emailCodeError: self.emailCodeError) + } + + func withUpdatedEmailCodeError(_ emailCodeError: InputDataValueError?) -> PassportState { + return PassportState(context: self.context, peer: self.peer, tmpPwd: self.tmpPwd, viewState: self.viewState, errors: self.errors, passwordSettings: self.passwordSettings, password: self.password, values: self.values, accessContext: self.accessContext, verifyDocumentContext: self.verifyDocumentContext, files: self.files, emailIntermediateState: self.emailIntermediateState, detailsIntermediateState: self.detailsIntermediateState, addressIntermediateState: self.addressIntermediateState, selfies: self.selfies, translations: self.translations, frontSideFile: self.frontSideFile, backSideFile: self.backSideFile, emptyErrors: self.emptyErrors, configuration: self.configuration, passwordError: self.passwordError, emailCode: self.emailCode, emailCodeError: emailCodeError) + } +} +private func ==(lhs: PassportState, rhs: PassportState) -> Bool { + + if lhs.files.count != rhs.files.count { + return false + } else { + for (lhsKey, lhsValue) in lhs.files { + let rhsValue = rhs.files[lhsKey] + if let rhsValue = rhsValue { + if lhsValue.count != rhsValue.count { + return false + } else { + for i in 0 ..< lhsValue.count { + if !lhsValue[i].isEqual(to: rhsValue[i]) { + return false + } + } + } + + } else { + return false + } + } + } + + if lhs.translations.count != rhs.translations.count { + return false + } else { + for (lhsKey, lhsValue) in lhs.translations { + let rhsValue = rhs.translations[lhsKey] + if let rhsValue = rhsValue { + if lhsValue.count != rhsValue.count { + return false + } else { + for i in 0 ..< lhsValue.count { + if !lhsValue[i].isEqual(to: rhsValue[i]) { + return false + } + } + } + + } else { + return false + } + } + } + + if lhs.selfies.count != rhs.selfies.count { + return false + } else { + for (lhsKey, lhsValue) in lhs.selfies { + let rhsValue = rhs.selfies[lhsKey] + if let rhsValue = rhsValue { + if !lhsValue.isEqual(to: rhsValue) { + return false + } + + } else { + return false + } + } + } + + + return lhs.passwordSettings?.email == rhs.passwordSettings?.email && lhs.password == rhs.password && lhs.values == rhs.values && (lhs.accessContext == nil && rhs.accessContext == nil) && lhs.emailIntermediateState == rhs.emailIntermediateState && lhs.detailsIntermediateState == rhs.detailsIntermediateState && lhs.addressIntermediateState == rhs.addressIntermediateState && lhs.errors == rhs.errors && lhs.viewState == rhs.viewState && lhs.tmpPwd == rhs.tmpPwd && lhs.emptyErrors == rhs.emptyErrors && lhs.passwordError == rhs.passwordError && lhs.emailCode == rhs.emailCode && lhs.emailCodeError == rhs.emailCodeError +} + + + +private func createPasswordEntries( _ state: PassportState) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + var sectionId:Int32 = 0 + var index: Int32 = 0 + entries.append(.sectionId(sectionId, type: .legacy)) + sectionId += 1 + + let nonFilter:(String)->String = { value in + return value + } + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.secureIdCreatePasswordHeader), data: InputDataGeneralTextData())) + index += 1 + + + + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: .string(""), error: nil, identifier: _id_c_password, mode: .secure, data: InputDataRowData(), placeholder: InputDataInputPlaceholder(L10n.secureIdCreatePasswordPasswordPlaceholder), inputPlaceholder: L10n.secureIdCreatePasswordPasswordInputPlaceholder, filter: nonFilter, limit: 255)) + index += 1 + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: .string(""), error: nil, identifier: _id_c_repassword, mode: .secure, data: InputDataRowData(), placeholder: InputDataInputPlaceholder(""), inputPlaceholder: L10n.secureIdCreatePasswordRePasswordInputPlaceholder, filter: nonFilter, limit: 255)) + index += 1 + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.secureIdCreatePasswordDescription), data: InputDataGeneralTextData())) + index += 1 + + + entries.append(.sectionId(sectionId, type: .legacy)) + sectionId += 1 + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.secureIdCreatePasswordHintHeader), data: InputDataGeneralTextData())) + index += 1 + + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: .string(""), error: nil, identifier: _id_c_hint, mode: .plain, data: InputDataRowData(), placeholder: InputDataInputPlaceholder(L10n.secureIdCreatePasswordHintPlaceholder), inputPlaceholder: L10n.secureIdCreatePasswordHintInputPlaceholder, filter: nonFilter, limit: 255)) + index += 1 + + entries.append(.sectionId(sectionId, type: .legacy)) + sectionId += 1 + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.secureIdCreatePasswordEmailHeader), data: InputDataGeneralTextData())) + index += 1 + + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: .string(""), error: nil, identifier: _id_c_email, mode: .plain, data: InputDataRowData(), placeholder: InputDataInputPlaceholder(L10n.secureIdCreatePasswordEmailPlaceholder), inputPlaceholder: L10n.secureIdCreatePasswordEmailInputPlaceholder, filter: nonFilter, limit: 255)) + index += 1 + + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.secureIdCreatePasswordEmailDescription), data: InputDataGeneralTextData())) + index += 1 + + return entries + +} + + +private func emailEntries( _ state: PassportState, updateState: @escaping ((PassportState)->PassportState)->Void) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + var sectionId:Int32 = 0 + var index: Int32 = 0 + + + entries.append(.sectionId(sectionId, type: .legacy)) + sectionId += 1 + + var placeholder = state.searchValue(.email)?.emailValue?.email ?? "" + + + if let email = state.emailIntermediateState?.email, !email.isEmpty { + + if placeholder == email { + placeholder = "" + } + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: .string(""), error: nil, identifier: _id_email_code, mode: .plain, data: InputDataRowData(), placeholder: InputDataInputPlaceholder(L10n.secureIdEmailActivateCodePlaceholder), inputPlaceholder: L10n.secureIdEmailActivateCodeInputPlaceholder, filter: {String($0.unicodeScalars.filter { CharacterSet.decimalDigits.contains($0)})}, limit: Int32(email.length))) + index += 1 + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.secureIdEmailActivateDescription(email)), data: InputDataGeneralTextData())) + index += 1 + + return entries + + } else if let email = state.passwordSettings?.email, !email.isEmpty { + + if placeholder == email { + placeholder = "" + } + + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .string(email), error: nil, identifier: _id_email_def, data: InputDataGeneralData(name: L10n.secureIdEmailUseSame(email), color: theme.colors.accent, icon: nil, type: .next, action: nil))) + entries.append(.sectionId(sectionId, type: .legacy)) + sectionId += 1 + } + + + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: .string(placeholder), error: nil, identifier: _id_email_new, mode: .plain, data: InputDataRowData(), placeholder: InputDataInputPlaceholder(L10n.secureIdEmailEmailPlaceholder), inputPlaceholder: L10n.secureIdEmailEmailInputPlaceholder, filter: {$0}, limit: 254)) + index += 1 + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.secureIdEmailUseSameDesc), data: InputDataGeneralTextData())) + index += 1 + + + return entries +} + + +private func phoneNumberEntries( _ state: PassportState, updateState: @escaping ((PassportState)->PassportState)->Void) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + var sectionId:Int32 = 0 + var index: Int32 = 0 + + + entries.append(.sectionId(sectionId, type: .legacy)) + sectionId += 1 + // + if let phone = (state.peer as? TelegramUser)?.phone, !phone.isEmpty { + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .string(phone), error: nil, identifier: _id_phone_def, data: InputDataGeneralData(name: L10n.secureIdPhoneNumberUseSame(formatPhoneNumber(phone)), color: theme.colors.accent, icon: nil, type: .next, action: nil))) + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.secureIdPhoneNumberUseSameDesc), data: InputDataGeneralTextData())) + index += 1 + + entries.append(.sectionId(sectionId, type: .legacy)) + sectionId += 1 + } + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.secureIdPhoneNumberHeader), data: InputDataGeneralTextData())) + index += 1 + + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .string(""), identifier: _id_phone_new, equatable: nil, comparable: nil, item: { initialSize, stableId -> TableRowItem in + return PassportNewPhoneNumberRowItem(initialSize, stableId: stableId, action: { + + }) + })) + index += 1 + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.secureIdPhoneNumberNote), data: InputDataGeneralTextData())) + index += 1 + + + return entries +} + +private func confirmPhoneNumberEntries( _ state: PassportState, phoneNumber: String, updateState: @escaping ((PassportState)->PassportState)->Void) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + var sectionId:Int32 = 0 + var index: Int32 = 0 + + + entries.append(.sectionId(sectionId, type: .legacy)) + sectionId += 1 + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.secureIdPhoneNumberHeader), data: InputDataGeneralTextData())) + index += 1 + + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: .string(nil), error: nil, identifier: _id_phone_code, mode: .plain, data: InputDataRowData(), placeholder: InputDataInputPlaceholder(L10n.secureIdPhoneNumberConfirmCodePlaceholder), inputPlaceholder: L10n.secureIdPhoneNumberConfirmCodeInputPlaceholder, filter: {String($0.unicodeScalars.filter { CharacterSet.decimalDigits.contains($0)})}, limit: 6)) + + index += 1 + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.secureIdPhoneNumberConfirmCodeDesc(formatPhoneNumber(phoneNumber))), data: InputDataGeneralTextData())) + index += 1 + + + return entries +} + +private func addressEntries( _ state: PassportState, hasMainField: Bool, relative: SecureIdRequestedFormFieldValue?, updateState: @escaping ((PassportState)->PassportState)->Void)->[InputDataEntry] { + var entries:[InputDataEntry] = [] + var sectionId:Int32 = 0 + var index: Int32 = 0 + entries.append(.sectionId(sectionId, type: .legacy)) + sectionId += 1 + + let nonFilter:(String)->String = { value in + return value + } + + + let address: SecureIdAddressValue? = hasMainField ? state.searchValue(.address)?.addressValue : nil + let relativeValue: SecureIdValue? = relative == nil ? nil : state.searchValue(relative!.valueKey) + + + let aErrors: [InputDataIdentifier : InputDataValueError]? = state.errors[.address] + + + if hasMainField { + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.secureIdAddressHeader), data: InputDataGeneralTextData())) + index += 1 + + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: state.addressIntermediateState?.street1 ?? .string(address?.street1), error: aErrors?[_id_street1], identifier: _id_street1, mode: .plain, data: InputDataRowData(), placeholder: InputDataInputPlaceholder(L10n.secureIdAddressStreetPlaceholder), inputPlaceholder: L10n.secureIdAddressStreetInputPlaceholder, filter: nonFilter, limit: 255)) + index += 1 + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: state.addressIntermediateState?.street2 ?? .string(address?.street2), error: aErrors?[_id_street2], identifier: _id_street2, mode: .plain, data: InputDataRowData(), placeholder: InputDataInputPlaceholder(""), inputPlaceholder: L10n.secureIdAddressStreet1InputPlaceholder, filter: nonFilter, limit: 255)) + index += 1 + + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: state.addressIntermediateState?.city ?? .string(address?.city), error: aErrors?[_id_city], identifier: _id_city, mode: .plain, data: InputDataRowData(), placeholder: InputDataInputPlaceholder(L10n.secureIdAddressCityPlaceholder), inputPlaceholder: L10n.secureIdAddressCityInputPlaceholder, filter: nonFilter, limit: 255)) + index += 1 + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: state.addressIntermediateState?.state ?? .string(address?.state), error: aErrors?[_id_state], identifier: _id_state, mode: .plain, data: InputDataRowData(), placeholder: InputDataInputPlaceholder(L10n.secureIdAddressRegionPlaceholder), inputPlaceholder: L10n.secureIdAddressRegionInputPlaceholder, filter: nonFilter, limit: 255)) + index += 1 + + let filedata = try! String(contentsOfFile: Bundle.main.path(forResource: "countries", ofType: nil)!) + + let countries: [ValuesSelectorValue] = filedata.components(separatedBy: "\n").compactMap { country in + let entry = country.components(separatedBy: ";") + if entry.count >= 3 { + return ValuesSelectorValue(localized: entry[2], value: .string(entry[1])) + } else { + return nil + } + }.sorted(by: { $0.localized < $1.localized}) + + entries.append(InputDataEntry.selector(sectionId: sectionId, index: index, value: state.addressIntermediateState?.countryCode ?? .string(address?.countryCode), error: aErrors?[_id_country], identifier: _id_country, placeholder: L10n.secureIdAddressCountryPlaceholder, viewType: .legacy, values: countries)) + index += 1 + + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: state.addressIntermediateState?.postcode ?? .string(address?.postcode), error: aErrors?[_id_postcode], identifier: _id_postcode, mode: .plain, data: InputDataRowData(), placeholder: InputDataInputPlaceholder(L10n.secureIdAddressPostcodePlaceholder), inputPlaceholder: L10n.secureIdAddressPostcodeInputPlaceholder, filter: { text in + return latinFilter(text, .address, _id_postcode, true, updateState) + }, limit: 10)) + index += 1 + + + entries.append(.sectionId(sectionId, type: .legacy)) + sectionId += 1 + } + + + + if let relative = relative { + let rErrors = state.errors[relative.valueKey] + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.secureIdScansHeader), data: InputDataGeneralTextData())) + index += 1 + + if let scanError = rErrors?[_id_scan] { + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(scanError.description), data: InputDataGeneralTextData(color: theme.colors.redUI))) + index += 1 + } + + let files = state.files[relative.valueKey] ?? [] + + var fileIndex: Int32 = 0 + + if let accessContext = state.accessContext { + for file in files { + let header = L10n.secureIdScanNumber(Int(fileIndex + 1)) + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .secureIdDocument(file), identifier: InputDataIdentifier("_file_\(fileIndex)"), equatable: InputDataEquatable(file), comparable: nil, item: { initialSize, stableId -> TableRowItem in + return PassportDocumentRowItem(initialSize, context: state.context, document: SecureIdDocumentValue(document: file, context: accessContext, stableId: stableId), error: rErrors?[file.errorIdentifier], header: header, removeAction: { value in + updateState { current in + return current.withRemovedFile(value, for: relative.valueKey) + } + }) + })) + fileIndex += 1 + index += 1 + } + } + + if files.count < scansLimit { + entries.append(InputDataEntry.dataSelector(sectionId: sectionId, index: index, value: .string(""), error: nil, identifier: _id_scan, placeholder: files.count > 0 ? L10n.secureIdUploadAdditionalScan : L10n.secureIdUploadScan, description: nil, icon: nil, action: { + filePanel(with: photoExts, allowMultiple: true, for: mainWindow, completion: { files in + if let files = files { + let localFiles:[SecureIdVerificationDocument] = files.map({.local(SecureIdVerificationLocalDocument(id: arc4random64(), resource: LocalFileReferenceMediaResource(localFilePath: $0, randomId: arc4random64()), state: .uploading(0)))}) + + updateState { current in + if localFiles.count + (current.files[relative.valueKey] ?? []).count > scansLimit { + alert(for: mainWindow, info: L10n.secureIdErrorScansLimit) + } + return current.withAppendFiles(localFiles, for: relative.valueKey).withRemovedError(for: relative.valueKey, field: _id_scan) + } + } + }) + })) + index += 1 + } + + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.secureIdIdentityScanDescription), data: InputDataGeneralTextData())) + index += 1 + + + if relative.hasTranslation { + + entries.append(.sectionId(sectionId, type: .legacy)) + sectionId += 1 + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.secureIdTranslationHeader), data: InputDataGeneralTextData())) + index += 1 + + if let translationError = rErrors?[_id_translation] { + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(translationError.description), data: InputDataGeneralTextData(color: theme.colors.redUI))) + index += 1 + } + + let translations = state.translations[relative.valueKey] ?? [] + + var fileIndex = 0 + + if let accessContext = state.accessContext { + for translation in translations { + let header = L10n.secureIdScanNumber(Int(fileIndex + 1)) + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .secureIdDocument(translation), identifier: InputDataIdentifier("_translation_\(fileIndex)"), equatable: InputDataEquatable(translation), comparable: nil, item: { initialSize, stableId -> TableRowItem in + return PassportDocumentRowItem(initialSize, context: state.context, document: SecureIdDocumentValue(document: translation, context: accessContext, stableId: stableId), error: rErrors?[translation.errorIdentifier], header: header, removeAction: { value in + updateState { current in + return current.withRemovedTranslation(value, for: relative.valueKey) + } + }) + })) + fileIndex += 1 + index += 1 + } + } + + if translations.count < scansLimit { + entries.append(InputDataEntry.dataSelector(sectionId: sectionId, index: index, value: .string(""), error: nil, identifier: _id_translation, placeholder: translations.count > 0 ? L10n.secureIdUploadAdditionalScan : L10n.secureIdUploadScan, description: nil, icon: nil, action: { + filePanel(with: photoExts, allowMultiple: true, for: mainWindow, completion: { files in + if let files = files { + let localFiles:[SecureIdVerificationDocument] = files.map({.local(SecureIdVerificationLocalDocument(id: arc4random64(), resource: LocalFileReferenceMediaResource(localFilePath: $0, randomId: arc4random64()), state: .uploading(0)))}) + + updateState { current in + if localFiles.count + (current.translations[relative.valueKey] ?? []).count > scansLimit { + alert(for: mainWindow, info: L10n.secureIdErrorScansLimit) + } + return current.withAppendTranslations(localFiles, for: relative.valueKey).withRemovedError(for: relative.valueKey, field: _id_translation) + } + } + }) + })) + index += 1 + } + + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.secureIdTranslationDesc), data: InputDataGeneralTextData())) + index += 1 + } + + } + + entries.append(.sectionId(sectionId, type: .legacy)) + sectionId += 1 + + if address != nil || relativeValue != nil { + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .string(nil), error: nil, identifier: _id_delete, data: InputDataGeneralData(name: relativeValue != nil ? L10n.secureIdDeleteIdentity : L10n.secureIdDeleteAddress, color: theme.colors.redUI, icon: nil, type: .none, action: nil))) + entries.append(.sectionId(sectionId, type: .legacy)) + sectionId += 1 + } + + return entries + +} + fileprivate let latinError = InputDataValueError(description: L10n.secureIdInputErrorLatinOnly, target: .data) + + private func latinFilter(_ text: String, _ valueKey: SecureIdValueKey, _ identifier: InputDataIdentifier, _ includeNumbers: Bool, _ updateState: @escaping((PassportState)->PassportState)->Void) -> String { + + let upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + let lower = "abcdefghijklmnopqrstuvwxyz" + let updated = text.trimmingCharacters(in: CharacterSet(charactersIn: upper + lower + (includeNumbers ? "0987654321-" : "")).inverted) + if updated != text { + updateState { current in + var errors = current.errors + var rErrors = errors[valueKey] ?? [:] + rErrors[identifier] = latinError + errors[valueKey] = rErrors + return current.withUpdatedErrors(errors) + } + } else { + updateState { current in + var errors = current.errors + var rErrors = errors[valueKey] ?? [:] + if rErrors[identifier] == latinError { + rErrors.removeValue(forKey: identifier) + } + errors[valueKey] = rErrors + return current.withUpdatedErrors(errors) + } + } + return updated + } + +private func identityEntries( _ state: PassportState, primary: SecureIdRequestedFormFieldValue?, relative: SecureIdRequestedFormFieldValue?, updateState: @escaping ((PassportState)->PassportState)->Void)->[InputDataEntry] { + var entries:[InputDataEntry] = [] + var sectionId:Int32 = 0 + var index: Int32 = 0 + entries.append(.sectionId(sectionId, type: .legacy)) + sectionId += 1 + + + let personalDetails: SecureIdPersonalDetailsValue? = primary != nil ? state.searchValue(primary!.valueKey)?.personalDetails : nil + let relativeValue: SecureIdValue? = relative == nil ? nil : state.searchValue(relative!.valueKey) + + let pdErrors = state.errors[.personalDetails] + + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.secureIdIdentityDocumentDetailsHeader), data: InputDataGeneralTextData())) + index += 1 + + + + + let addRelativeIdentifier:()->Void = { + if let relative = relative { + let rErrors = state.errors[relative.valueKey] + + var title: String = "" + var subtitle: String = "" + switch relative { + case .passport: + title = L10n.secureIdIdentityPassportPlaceholder + subtitle = L10n.secureIdIdentityPassportInputPlaceholder + case .internalPassport: + title = L10n.secureIdIdentityPassportPlaceholder + subtitle = L10n.secureIdIdentityPassportInputPlaceholder + case .idCard: + title = L10n.secureIdIdentityCardIdPlaceholder + subtitle = L10n.secureIdIdentityCardIdInputPlaceholder + case .driversLicense: + title = L10n.secureIdIdentityLicensePlaceholder + subtitle = L10n.secureIdIdentityLicenseInputPlaceholder + default: + break + } + + + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: state.detailsIntermediateState?.identifier ?? .string(relativeValue?.identifier), error: rErrors?[_id_identifier], identifier: _id_identifier, mode: .plain, data: InputDataRowData(), placeholder: InputDataInputPlaceholder(title), inputPlaceholder: subtitle, filter: { text in + return latinFilter(text, relative.valueKey, _id_identifier, true, updateState) + }, limit: 20)) + index += 1 + + entries.append(InputDataEntry.dateSelector(sectionId: sectionId, index: index, value: state.detailsIntermediateState?.expiryDate ?? relativeValue?.expiryDate?.inputDataValue ?? .date(nil, nil, nil), error: rErrors?[_id_expire_date], identifier: _id_expire_date, placeholder: L10n.secureIdIdentityPlaceholderExpiryDate)) + index += 1 + } + } + + if let primary = primary { + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.secureIdIdentityNameInLatine), data: InputDataGeneralTextData())) + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: state.detailsIntermediateState?.firstName ?? .string(personalDetails?.latinName.firstName ?? ""), error: pdErrors?[_id_first_name], identifier: _id_first_name, mode: .plain, data: InputDataRowData(), placeholder: InputDataInputPlaceholder(L10n.secureIdIdentityPlaceholderFirstName), inputPlaceholder: L10n.secureIdIdentityInputPlaceholderFirstName, filter: { text in + return latinFilter(text, primary.valueKey, _id_first_name, false, updateState) + }, limit: 255)) + index += 1 + + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: state.detailsIntermediateState?.middleName ?? .string(personalDetails?.latinName.middleName ?? ""), error: pdErrors?[_id_middle_name], identifier: _id_middle_name, mode: .plain, data: InputDataRowData(), placeholder: InputDataInputPlaceholder(L10n.secureIdIdentityPlaceholderMiddleName), inputPlaceholder: L10n.secureIdIdentityInputPlaceholderMiddleName, filter: { text in + return latinFilter(text, primary.valueKey, _id_middle_name, false, updateState) + }, limit: 255)) + index += 1 + + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: state.detailsIntermediateState?.lastName ?? .string(personalDetails?.latinName.lastName ?? ""), error: pdErrors?[_id_last_name], identifier: _id_last_name, mode: .plain, data: InputDataRowData(), placeholder: InputDataInputPlaceholder(L10n.secureIdIdentityPlaceholderLastName), inputPlaceholder: L10n.secureIdIdentityInputPlaceholderLastName, filter: { text in + return latinFilter(text, primary.valueKey, _id_last_name, false, updateState) + }, limit: 255)) + index += 1 + + let genders:[ValuesSelectorValue] = [ValuesSelectorValue(localized: L10n.secureIdGenderMale, value: .gender(.male)), ValuesSelectorValue(localized: L10n.secureIdGenderFemale, value: .gender(.female))] + + entries.append(InputDataEntry.selector(sectionId: sectionId, index: index, value: state.detailsIntermediateState?.gender ?? .gender(personalDetails?.gender), error: pdErrors?[_id_gender], identifier: _id_gender, placeholder: L10n.secureIdIdentityPlaceholderGender, viewType: .legacy, values: genders)) + index += 1 + + entries.append(InputDataEntry.dateSelector(sectionId: sectionId, index: index, value: state.detailsIntermediateState?.birthday ?? personalDetails?.birthdate.inputDataValue ?? .date(nil, nil, nil), error: pdErrors?[_id_birthday], identifier: _id_birthday, placeholder: L10n.secureIdIdentityPlaceholderBirthday)) + index += 1 + + let filedata = try! String(contentsOfFile: Bundle.main.path(forResource: "countries", ofType: nil)!) + + let countries: [ValuesSelectorValue] = filedata.components(separatedBy: "\n").compactMap { country in + let entry = country.components(separatedBy: ";") + if entry.count >= 3 { + return ValuesSelectorValue(localized: entry[2], value: .string(entry[1])) + } else { + return nil + } + }.sorted(by: { $0.localized < $1.localized}) + + entries.append(InputDataEntry.selector(sectionId: sectionId, index: index, value: state.detailsIntermediateState?.citizenship ?? .string(personalDetails?.countryCode), error: pdErrors?[_id_country], identifier: _id_country, placeholder: L10n.secureIdIdentityPlaceholderCitizenship, viewType: .legacy, values: countries)) + index += 1 + + let residence = state.detailsIntermediateState?.residence ?? .string(personalDetails?.residenceCountryCode) + + entries.append(InputDataEntry.selector(sectionId: sectionId, index: index, value: residence, error: pdErrors?[_id_residence], identifier: _id_residence, placeholder: L10n.secureIdIdentityPlaceholderResidence, viewType: .legacy, values: countries)) + index += 1 + + if let _ = relative { + addRelativeIdentifier() + } + + if case let .personalDetails(nativeName) = primary, nativeName { + if let residence = residence.stringValue { + entries.append(.sectionId(sectionId, type: .legacy)) + sectionId += 1 + + if state.configuration?.nativeLanguageByCountry[residence] != "en" { + + let country = countries.filter({$0.value.stringValue == residence}).first?.localized ?? residence + + var localizedDesc: String = "" + var localizedTitle: String = L10n.secureIdNameNativeHeaderEmpty + if let language = state.configuration?.nativeLanguageByCountry[residence] { + let key = "Passport.Language.\(language)" + let localizedKey = localizedString(key) + if localizedKey == key { + localizedDesc = L10n.secureIdNameNativeDescLanguage(country) + } else { + localizedTitle = L10n.secureIdNameNativeHeader(localizedKey.uppercased()) + localizedDesc = L10n.secureIdNameNativeDescEmpty + } + } else { + localizedDesc = L10n.secureIdNameNativeDescLanguage(country) + } + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(localizedTitle), data: InputDataGeneralTextData())) + index += 1 + + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: state.detailsIntermediateState?.firstNameNative ?? .string(personalDetails?.nativeName?.firstName ?? ""), error: pdErrors?[_id_first_name_native], identifier: _id_first_name_native, mode: .plain, data: InputDataRowData(), placeholder: InputDataInputPlaceholder(L10n.secureIdIdentityPlaceholderFirstName), inputPlaceholder: L10n.secureIdIdentityInputPlaceholderFirstName, filter: {$0}, limit: 255)) + index += 1 + + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: state.detailsIntermediateState?.middleNameNative ?? .string(personalDetails?.nativeName?.middleName ?? ""), error: pdErrors?[_id_middle_name_native], identifier: _id_middle_name_native, mode: .plain, data: InputDataRowData(), placeholder: InputDataInputPlaceholder(L10n.secureIdIdentityPlaceholderMiddleName), inputPlaceholder: L10n.secureIdIdentityInputPlaceholderMiddleName, filter: {$0}, limit: 255)) + index += 1 + + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: state.detailsIntermediateState?.lastNameNative ?? .string(personalDetails?.nativeName?.lastName ?? ""), error: pdErrors?[_id_last_name_native], identifier: _id_last_name_native, mode: .plain, data: InputDataRowData(), placeholder: InputDataInputPlaceholder(L10n.secureIdIdentityPlaceholderLastName), inputPlaceholder: L10n.secureIdIdentityInputPlaceholderLastName, filter: {$0}, limit: 255)) + index += 1 + + + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(localizedDesc), data: InputDataGeneralTextData())) + index += 1 + + } + } + } + + } + + + + + + + + + + if let relative = relative { + + if primary == nil { + addRelativeIdentifier() + } + + entries.append(.sectionId(sectionId, type: .legacy)) + sectionId += 1 + + let rErrors = state.errors[relative.valueKey] + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.secureIdScansHeader), data: InputDataGeneralTextData())) + index += 1 + + if let scanError = rErrors?[_id_scan] { + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(scanError.description), data: InputDataGeneralTextData(color: theme.colors.redUI))) + index += 1 + } + if let accessContext = state.accessContext { + let isMainNotFront: Bool = !relative.hasBacksideDocument + if let file = state.frontSideFile[relative.valueKey] { + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .secureIdDocument(file), identifier: _id_frontside, equatable: InputDataEquatable(file), comparable: nil, item: { initialSize, stableId -> TableRowItem in + return PassportDocumentRowItem(initialSize, context: state.context, document: SecureIdDocumentValue(document: file, context: accessContext, stableId: stableId), error: rErrors?[_id_frontside], header: isMainNotFront ? L10n.secureIdUploadTitleMainPage : L10n.secureIdUploadTitleFrontSide, removeAction: { value in + modernConfirm(for: mainWindow, account: state.context.account, peerId: nil, information: L10n.secureIdConfirmDeleteDocument, successHandler: { _ in + updateState { current in + return current.withUpdatedFrontSide(nil, for: relative.valueKey) + } + }) + }) + })) + index += 1 + } else { + entries.append(InputDataEntry.dataSelector(sectionId: sectionId, index: index, value: .string(""), error: nil, identifier: _id_frontside, placeholder: isMainNotFront ? L10n.secureIdUploadTitleMainPage : L10n.secureIdUploadTitleFrontSide, description: relative.uploadFrontTitleText, icon: isMainNotFront ? theme.icons.passportPassport : (relative.valueKey == .driversLicense ? theme.icons.passportDriverLicense : theme.icons.passportIdCard), action: { + filePanel(with: photoExts, allowMultiple: false, for: mainWindow, completion: { files in + if let file = files?.first { + updateFrontMrz(file: file, relative: relative, updateState: updateState) + } + }) + })) + index += 1 + } + + if relative.hasBacksideDocument { + if let file = state.backSideFile[relative.valueKey] { + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .secureIdDocument(file), identifier: _id_backside, equatable: InputDataEquatable(file), comparable: nil, item: { initialSize, stableId -> TableRowItem in + return PassportDocumentRowItem(initialSize, context: state.context, document: SecureIdDocumentValue(document: file, context: accessContext, stableId: stableId), error: rErrors?[_id_backside], header: L10n.secureIdUploadTitleReverseSide, removeAction: { value in + updateState { current in + return current.withUpdatedBackSide(nil, for: relative.valueKey) + } + }) + })) + index += 1 + } else { + entries.append(InputDataEntry.dataSelector(sectionId: sectionId, index: index, value: .string(""), error: nil, identifier: _id_backside, placeholder: isMainNotFront ? L10n.secureIdUploadTitleMainPage : L10n.secureIdUploadTitleReverseSide, description: relative.uploadBackTitleText, icon: theme.icons.passportIdCardReverse, action: { + filePanel(with: photoExts, allowMultiple: false, for: mainWindow, completion: { files in + if let file = files?.first { + if let image = NSImage(contentsOfFile: file) { + let string = recognizeMRZ(image.precomposed(), nil) + let mrz = TGPassportMRZ.parseLines(string?.components(separatedBy: "\n")) + let localFile:SecureIdVerificationDocument = .local(SecureIdVerificationLocalDocument(id: arc4random64(), resource: LocalFileReferenceMediaResource(localFilePath: file, randomId: arc4random64()), state: .uploading(0))) + + updateState { current in + var current = current + if let mrz = mrz { + if relative.isEqualToMRZ(mrz) { + let expiryDate = dateFormatter.string(from: mrz.expiryDate).components(separatedBy: ".").map({Int32($0)}) + let birthDate = dateFormatter.string(from: mrz.birthDate).components(separatedBy: ".").map({Int32($0)}) + let details = DetailsIntermediateState(firstName: .string(mrz.firstName), middleName: nil, lastName: .string(mrz.lastName), firstNameNative: nil, middleNameNative: nil, lastNameNative: nil, birthday: .date(birthDate[0], birthDate[1], birthDate[2]), citizenship: .string(mrz.issuingCountry), residence: current.detailsIntermediateState?.residence, gender: .gender(SecureIdGender.gender(from: mrz)), expiryDate: .date(expiryDate[0], expiryDate[1], expiryDate[2]), identifier: .string(mrz.documentNumber)) + current = current.withUpdatedDetailsState(details) + } + } + return current.withUpdatedBackSide(localFile, for: relative.valueKey) + } + } + } + }) + })) + index += 1 + } + } + } + + + if relative.hasSelfie, let accessContext = state.accessContext { + let rErrors = state.errors[relative.valueKey] + if let selfie = state.selfies[relative.valueKey] { + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .secureIdDocument(selfie), identifier: _id_selfie, equatable: InputDataEquatable(selfie), comparable: nil, item: { initialSize, stableId -> TableRowItem in + return PassportDocumentRowItem(initialSize, context: state.context, document: SecureIdDocumentValue(document: selfie, context: accessContext, stableId: stableId), error: rErrors?[_id_selfie], header: L10n.secureIdIdentitySelfie, removeAction: { value in + updateState { current in + return current.withUpdatedSelfie(nil, for: relative.valueKey) + } + }) + })) + index += 1 + + } else { + entries.append(InputDataEntry.dataSelector(sectionId: sectionId, index: index, value: .string(""), error: nil, identifier: _id_selfie_scan, placeholder: L10n.secureIdIdentitySelfie, description: L10n.secureIdUploadSelfie, icon: theme.icons.passportSelfie, action: { + filePanel(with: photoExts, allowMultiple: false, for: mainWindow, completion: { paths in + if let path = paths?.first, let image = NSImage(contentsOfFile: path) { + _ = putToTemp(image: image).start(next: { path in + let localFile:SecureIdVerificationDocument = .local(SecureIdVerificationLocalDocument(id: arc4random64(), resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: arc4random64()), state: .uploading(0))) + + updateState { current in + return current.withUpdatedSelfie(localFile, for: relative.valueKey) + } + }) + } + }) + })) + index += 1 + } + } + + if relative.hasTranslation { + + entries.append(.sectionId(sectionId, type: .legacy)) + sectionId += 1 + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.secureIdTranslationHeader), data: InputDataGeneralTextData())) + index += 1 + + if let translationError = rErrors?[_id_translation] { + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(translationError.description), data: InputDataGeneralTextData(color: theme.colors.redUI))) + index += 1 + } + + let translations = state.translations[relative.valueKey] ?? [] + + var fileIndex: Int32 = 0 + + if let accessContext = state.accessContext { + for translation in translations { + let header = L10n.secureIdScanNumber(Int(fileIndex + 1)) + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .secureIdDocument(translation), identifier: InputDataIdentifier("_translation_\(fileIndex)"), equatable: InputDataEquatable(translation), comparable: nil, item: { initialSize, stableId -> TableRowItem in + return PassportDocumentRowItem(initialSize, context: state.context, document: SecureIdDocumentValue(document: translation, context: accessContext, stableId: stableId), error: rErrors?[translation.errorIdentifier], header: header, removeAction: { value in + updateState { current in + return current.withRemovedTranslation(value, for: relative.valueKey) + } + }) + })) + fileIndex += 1 + index += 1 + } + } + + if translations.count < scansLimit { + entries.append(InputDataEntry.dataSelector(sectionId: sectionId, index: index, value: .string(""), error: nil, identifier: _id_translation, placeholder: translations.count > 0 ? L10n.secureIdUploadAdditionalScan : L10n.secureIdUploadScan, description: nil, icon: nil, action: { + filePanel(with: photoExts, allowMultiple: true, for: mainWindow, completion: { files in + if let files = files { + let localFiles:[SecureIdVerificationDocument] = files.map({.local(SecureIdVerificationLocalDocument(id: arc4random64(), resource: LocalFileReferenceMediaResource(localFilePath: $0, randomId: arc4random64()), state: .uploading(0)))}) + + updateState { current in + if localFiles.count + (current.translations[relative.valueKey] ?? []).count > scansLimit { + alert(for: mainWindow, info: L10n.secureIdErrorScansLimit) + } + return current.withAppendTranslations(localFiles, for: relative.valueKey).withRemovedError(for: relative.valueKey, field: _id_translation) + } + } + }) + })) + index += 1 + } + + + entries.append(InputDataEntry.desc(sectionId: sectionId, index: index, text: .plain(L10n.secureIdTranslationDesc), data: InputDataGeneralTextData())) + index += 1 + } + + +// entries.append(.desc(sectionId: sectionId, index: index, text: L10n.secureIdIdentityScanDescription, data: InputDataGeneralTextData())) +// index += 1 + + } + + + + entries.append(.sectionId(sectionId, type: .legacy)) + sectionId += 1 + + if personalDetails != nil || relativeValue != nil { + entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .string(nil), error: nil, identifier: _id_delete, data: InputDataGeneralData(name: relativeValue != nil ? L10n.secureIdDeleteIdentity : L10n.secureIdDeletePersonalDetails, color: theme.colors.redUI, icon: nil, type: .none, action: nil))) + entries.append(.sectionId(sectionId, type: .legacy)) + sectionId += 1 + } + + + return entries +} + + private func recoverEmailEntries(emailPattern: String, unavailable: @escaping() -> Void) -> [InputDataEntry] { + var entries: [InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index:Int32 = 0 + + entries.append(.sectionId(sectionId, type: .legacy)) + sectionId += 1 + + entries.append(InputDataEntry.input(sectionId: sectionId, index: index, value: .string(""), error: nil, identifier: _id_email_code, mode: .plain, data: InputDataRowData(), placeholder: InputDataInputPlaceholder(L10n.secureIdEmailActivateCodePlaceholder), inputPlaceholder: L10n.secureIdEmailActivateCodeInputPlaceholder, filter: {String($0.unicodeScalars.filter { CharacterSet.decimalDigits.contains($0)})}, limit: 6)) + index += 1 + + + let info = L10n.twoStepAuthRecoveryCodeHelp + "\n\n\(L10n.twoStepAuthRecoveryEmailUnavailableNew(emailPattern))" + + entries.append(.desc(sectionId: sectionId, index: index, text: .markdown(info, linkHandler: { _ in + unavailable() + }), data: InputDataGeneralTextData(detectBold: false))) + + + index += 1 + + return entries +} + +final class PassportControllerView : View { + let tableView: TableView = TableView() + let authorize: PassportAcceptRowView = PassportAcceptRowView(frame: NSZeroRect) + private var item: PassportAcceptRowItem? + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(tableView) + addSubview(authorize) + updateLocalizationAndTheme(theme: theme) + layout() + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + backgroundColor = theme.colors.background + } + + override func layout() { + super.layout() + tableView.frame = NSMakeRect(0, 0, frame.width, frame.height - 80) + authorize.frame = NSMakeRect(0, frame.height - 80, frame.width, 80) + } + + func updateEnabled(_ enabled: Bool, isVisible: Bool, action: @escaping(Bool)->Void) { + self.item = PassportAcceptRowItem(authorize.frame.size, stableId: 0, enabled: enabled, action: { + action(enabled) + }) + authorize.set(item: item!, animated: false) + authorize.isHidden = !isVisible + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class PassportController: TelegramGenericViewController { + + private let form: EncryptedSecureIdForm? + private let disposable = MetaDisposable() + private let secureIdConfigurationDisposable = MetaDisposable() + private let peer: Peer + private let request: inAppSecureIdRequest? + private var pendingEmailConfirm: () -> Bool = { return false } + init(_ context: AccountContext, _ peer: Peer, request: inAppSecureIdRequest?, _ form: EncryptedSecureIdForm?) { + self.form = form + self.peer = peer + self.request = request + super.init(context) + + } + + + override var enableBack: Bool { + return true + } + + override func backSettings() -> (String, CGImage?) { + return (form == nil ? L10n.navigationBack : "", form == nil ? #imageLiteral(resourceName: "Icon_NavigationBack").precomposed(theme.colors.accentIcon) : theme.icons.dismissPinned) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + window?.set(mouseHandler: { [weak self] event -> KeyHandlerResult in + guard let `self` = self else {return .rejected} + + let index = self.genericView.tableView.row(at: self.genericView.tableView.documentView!.convert(event.locationInWindow, from: nil)) + + if index > 0, let view = self.genericView.tableView.item(at: index).view { + if view.mouseInsideField { + if self.window?.firstResponder != view.firstResponder { + _ = self.window?.makeFirstResponder(view.firstResponder) + return .invoked + } + } + } + + return .rejected + }, with: self, for: .leftMouseDown) + } + + override func requestUpdateRightBar() { + rightView?.set(image: theme.icons.passportInfo, for: .Normal) + } + override func requestUpdateBackBar() { + super.requestUpdateBackBar() + (leftBarView as? TextButtonBarView)?.direction = self.request == nil ? .left : .right + } + + private var rightView: TextButtonBarView? + + override func getRightBarViewOnce() -> BarView { + rightView = TextButtonBarView(controller: self, text:"") + rightView?.direction = .right + rightView?.alignment = .Right + return rightView! + } + + override func viewDidLoad() { + super.viewDidLoad() + + + let inAppRequest = self.request + let encryptedForm = self.form + let formValue: Promise<(EncryptedSecureIdForm?, SecureIdForm?)> = Promise() + + formValue.set(.single((form, nil))) + + let initialSize = self.atomicSize + let context = self.context + + let actionsDisposable = DisposableSet() + let checkPassword = MetaDisposable() + let authorizeDisposable = MetaDisposable() + let emailNewActivationDisposable = MetaDisposable() + let phoneNewActivationDisposable = MetaDisposable() + let recoverPasswordDisposable = MetaDisposable() + + actionsDisposable.add(checkPassword) + actionsDisposable.add(authorizeDisposable) + actionsDisposable.add(emailNewActivationDisposable) + actionsDisposable.add(phoneNewActivationDisposable) + actionsDisposable.add(recoverPasswordDisposable) + + let state:ValuePromise = ValuePromise(PassportState(context: context, peer: peer, tmpPwd: context.temporaryPassword, viewState: .plain), ignoreRepeated: true) + + let stateValue:Atomic = Atomic(value: PassportState(context: context, peer: peer, tmpPwd: context.temporaryPassword, viewState: .plain)) + + var _stateValue: PassportState { + return stateValue.modify({$0}) + } + + let updateState:((PassportState)->PassportState) -> Void = { f in + state.set(stateValue.modify(f)) + } + + let closeAfterSuccessful:()->Void = { [weak self] in + _ = self?.window?.closeInterceptor?() + } + + let closeController:()->Void = { [weak self] in + self?.navigationController?.back() + } + + + let passwordVerificationData: Promise = Promise() + + + + let emailActivation = MetaDisposable() + let saveValueDisposable = MetaDisposable() + actionsDisposable.add(emailActivation) + actionsDisposable.add(saveValueDisposable) + + let updateVerifyDocumentState: (Int64, SecureIdVerificationLocalDocumentState) -> Void = { id, state in + updateState { current in + return current.withUpdatedFileState(id: id, state: state) + } + } + + var checkPwd:((String) -> Void)? + + self.pendingEmailConfirm = { [weak self] in + + let code = stateValue.with { $0.emailCode } + if code.isEmpty, let `self` = self { + self.genericView.tableView.item(stableId: PassportEntryId.inputEmailCode)?.view?.shakeView() + return false + } + + _ = (passwordVerificationData.get() |> take(1)).start(next: { configuration in + if let configuration = configuration { + let pending: TwoStepVerificationPendingEmail? + switch configuration { + case let .set(_, _, pendingEmail, _, _): + pending = pendingEmail + case let .notSet(pendingEmail): + pending = pendingEmail + } + if let _ = pending { + let code = stateValue.with { $0.emailCode } + let state = _stateValue + if !code.isEmpty { + emailActivation.set(showModalProgress(signal: context.engine.auth.confirmTwoStepRecoveryEmail(code: code) |> deliverOnMainQueue, for: mainWindow).start(error: { error in + + let text: String + switch error { + case .invalidEmail: + text = L10n.twoStepAuthEmailInvalid + case .invalidCode: + text = L10n.twoStepAuthEmailCodeInvalid + case .expired: + text = L10n.twoStepAuthEmailCodeExpired + case .flood: + text = L10n.twoStepAuthFloodError + case .generic: + text = L10n.unknownError + } + updateState { $0.withUpdatedEmailCodeError(InputDataValueError(description: text, target: .data)) } + + }, completed: { + passwordVerificationData.set( showModalProgress(signal: context.engine.auth.twoStepVerificationConfiguration() |> mapToSignal { config in + if let password = state.password { + switch password { + case let .password(password, _): + switch config { + case .set: + if let encryptedForm = encryptedForm { + return context.engine.secureId.accessSecureId(password: password) |> map { values in + return (decryptedSecureIdForm(context: values.context, form: encryptedForm), values.context, values.settings) + } |> deliverOnMainQueue |> map { form, ctx, settings in + + + updateState { current in + var current = current + if let form = form { + current = form.values.reduce(current, { current, value -> PassportState in + return current.withUpdatedValue(value) + }) + } + //return current + return current.withUpdatedAccessContext(ctx).withUpdatedPasswordSettings(settings).withUpdatedVerifyDocumentContext(SecureIdVerificationDocumentsContext(postbox: context.account.postbox, network: context.account.network, context: ctx, update: updateVerifyDocumentState)) + } + formValue.set(.single((nil, form))) + return Optional(config) + } |> `catch` { _ in return .single(nil) } + } else { + let signal = context.engine.secureId.accessSecureId(password: password) |> mapToSignal { values in + return getAllSecureIdValues(network: context.account.network) + |> map { encryptedValues in + return decryptedAllSecureIdValues(context: values.context, encryptedValues: encryptedValues) + } + |> mapError {_ in return SecureIdAccessError.generic} + |> map {($0, values.context, values.settings)} + } |> deliverOnMainQueue + + return signal |> map { values, ctx, settings in + updateState { current in + var current = current.withRemovedValues() + current = values.reduce(current, { current, value -> PassportState in + return current.withUpdatedValue(value) + }) + return current.withUpdatedViewState(.settings).withUpdatedPasswordSettings(settings).withUpdatedAccessContext(ctx).withUpdatedVerifyDocumentContext(SecureIdVerificationDocumentsContext(postbox: context.account.postbox, network: context.account.network, context: ctx, update: updateVerifyDocumentState)) + } + return Optional(config) + } |> `catch` { _ in return .single(nil) } + + } + + default: + return .single(Optional(config)) + } + case .none: + return .single(Optional(config)) + } + } + return .single(Optional(config)) + }, for: mainWindow)) + })) + } + } + } + }) + + + + return true + } + + +// emailActivation.set((combineLatest(isKeyWindow.get() |> deliverOnPrepareQueue, Signal.single(Void()) |> delay(3.0, queue: prepareQueue) |> restart) |> mapToSignal { _ in return combineLatest(passwordVerificationData.get() |> take(1) |> deliverOnPrepareQueue, state.get() |> take(1) |> deliverOnPrepareQueue) }).start(next: { config, state in +// if let config = config { +// } +// })) + + + let presentController:(ViewController)->Void = { [weak self] controller in + self?.navigationController?.push(controller) + } + + let executeCallback:(Bool) -> Void = { [weak self] success in + self?.executeCallback(success) + } + + let previous: Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) + + + + let arguments = PassportArguments(context: context, checkPassword: { value, shake in + if value.isEmpty { + shake() + return + } + if let encryptedForm = encryptedForm { + checkPassword.set((context.engine.secureId.accessSecureId(password: value) |> map { data in + return (decryptedSecureIdForm(context: data.context, form: encryptedForm), data.context, data.settings) + } |> deliverOnMainQueue).start(next: { form, ctx, settings in + + context.setTemporaryPwd(value) + + updateState { current in + var current = current.withRemovedValues() + if let form = form { + current = form.values.reduce(current, { current, value -> PassportState in + return current.withUpdatedValue(value) + }) + var errors:[SecureIdValueKey: [InputDataIdentifier : InputDataValueError]] = [:] + + for value in form.values { + var cErrors = errors[value.value.key] ?? [:] + + for(eKey, eValue) in value.errors { + switch eKey { + case let .field(field): + switch field { + case let .address(f): + cErrors[InputDataIdentifier(f.rawValue)] = InputDataValueError(description: eValue, target: .data) + case let .driversLicense(f): + cErrors[InputDataIdentifier(f.rawValue)] = InputDataValueError(description: eValue, target: .data) + case let .idCard(f): + cErrors[InputDataIdentifier(f.rawValue)] = InputDataValueError(description: eValue, target: .data) + case let .passport(f): + cErrors[InputDataIdentifier(f.rawValue)] = InputDataValueError(description: eValue, target: .data) + case let .personalDetails(f): + cErrors[InputDataIdentifier(f.rawValue)] = InputDataValueError(description: eValue, target: .data) + case let .internalPassport(f): + cErrors[InputDataIdentifier(f.rawValue)] = InputDataValueError(description: eValue, target: .data) + } + case .files: + cErrors[_id_scan] = InputDataValueError(description: eValue, target: .files) + case let .file(hash): + cErrors[InputDataIdentifier("file_\(hash.base64EncodedString())")] = InputDataValueError(description: eValue, target: .files) + case .selfie: + cErrors[_id_selfie] = InputDataValueError(description: eValue, target: .files) + case .frontSide: + cErrors[_id_frontside] = InputDataValueError(description: eValue, target: .files) + case .backSide: + cErrors[_id_backside] = InputDataValueError(description: eValue, target: .files) + case let .translationFile(hash): + cErrors[InputDataIdentifier("file_\(hash.base64EncodedString())")] = InputDataValueError(description: eValue, target: .files) + case .translationFiles: + cErrors[_id_translation] = InputDataValueError(description: eValue, target: .files) + case let .value(valueKey): + if valueKey == value.value.key { + cErrors[InputDataEmptyIdentifier] = InputDataValueError(description: eValue, target: .data) + } + //errors[valueKey] = [InputDataEmptyIdentifier : InputDataValueError(description: eValue, target: .data)] + var bp:Int = 0 + bp += 1 + } + } + errors[value.value.key] = cErrors + } + current = current.withUpdatedErrors(errors) + } + return current.withUpdatedAccessContext(ctx).withUpdatedPasswordSettings(settings).withUpdatedVerifyDocumentContext(SecureIdVerificationDocumentsContext(postbox: context.account.postbox, network: context.account.network, context: ctx, update: updateVerifyDocumentState)).withUpdatedPasswordError(nil) + } + formValue.set(.single((nil, form))) + }, error: { error in + switch error { + case .secretPasswordMismatch: + confirm(for: mainWindow, header: L10n.telegramPassportController, information: "Something going wrong", thridTitle: "Delete All Values", successHandler: { result in + switch result { + case .basic: + break + case .thrid: + _ = showModalProgress(signal: context.engine.auth.updateTwoStepVerificationPassword(currentPassword: value, updatedPassword: .none) |> deliverOnMainQueue, for: mainWindow).start(next: {_ in + updateState { current in + return current.withUpdatedPassword(nil) + } + passwordVerificationData.set(.single(.notSet(pendingEmail: nil))) + }, error: { error in + + }) + } + }) + case .passwordError(let error): + updateState { current in + switch error { + case .invalidPassword: + return current.withUpdatedPasswordError(L10n.secureIdPasswordErrorInvalid) + case .limitExceeded: + return current.withUpdatedPasswordError(L10n.secureIdPasswordErrorLimit) + default: + return current.withUpdatedPasswordError(L10n.secureIdPasswordErrorInvalid) + } + } + shake() + + case .generic: + shake() + } + + })) + } else { + let signal = context.engine.secureId.accessSecureId(password: value) |> mapToSignal { data in + return getAllSecureIdValues(network: context.account.network) + |> map { encryptedValues in + return decryptedAllSecureIdValues(context: data.context, encryptedValues: encryptedValues) + } + |> mapError {_ in return SecureIdAccessError.generic} + |> map {($0, data.context, data.settings)} + } |> deliverOnMainQueue + + + + checkPassword.set(signal.start(next: { values, ctx, passwordSettings in + + context.setTemporaryPwd(value) + + updateState { current in + var current = current.withRemovedValues() + current = values.reduce(current, { current, value -> PassportState in + return current.withUpdatedValue(value) + }) + return current.withUpdatedViewState(.settings).withUpdatedPasswordSettings(passwordSettings).withUpdatedAccessContext(ctx).withUpdatedVerifyDocumentContext(SecureIdVerificationDocumentsContext(postbox: context.account.postbox, network: context.account.network, context: ctx, update: updateVerifyDocumentState)) + } + }, error: { error in + switch error { + case .secretPasswordMismatch: + confirm(for: mainWindow, header: L10n.telegramPassportController, information: "Something going wrong", thridTitle: "Delete All Values", successHandler: { result in + switch result { + case .basic: + break + case .thrid: + _ = showModalProgress(signal: context.engine.auth.updateTwoStepVerificationPassword(currentPassword: value, updatedPassword: .none) |> deliverOnMainQueue, for: mainWindow).start(next: {_ in + updateState { current in + return current.withUpdatedPassword(nil) + } + passwordVerificationData.set(.single(.notSet(pendingEmail: nil))) + }, error: { error in + + }) + } + }) + case .passwordError: + shake() + case .generic: + shake() + } + })) + } + }, requestField: { request, value, relativeValue, relative, editSettings in + + let valueKey = value?.key + let proccessValue:([SecureIdValue])->InputDataValidation = { values in + return .fail(.doSomething(next: { f in + + let signal: Signal<[SecureIdValueWithContext], SaveSecureIdValueError> = state.get() |> take(1) |> mapError {_ in return SaveSecureIdValueError.generic} |> mapToSignal { state in + if let ctx = state.accessContext { + return combineLatest(values.map({ value in + return saveSecureIdValue(postbox: context.account.postbox, network: context.account.network, context: ctx, value: value, uploadedFiles: [:]) + })) + } else { + return .fail(.generic) + } + } |> deliverOnMainQueue + + saveValueDisposable.set(showModalProgress(signal: signal, for: mainWindow).start(next: { values in + updateState { current in + return values.reduce(current, { current, value in + return current.withUpdatedValue(value).withRemovedErrors(for: value.value.key).withUpdatedEmptyErrors(false) + }) + } + f(.success(.navigationBack)) + }, error: { error in + f(.fail(.alert("\(error)"))) + })) + })) + } + + let removeValue:(SecureIdValueKey) -> Void = { valueKey in + saveValueDisposable.set(showModalProgress(signal: deleteSecureIdValues(network: context.account.network, keys: Set(arrayLiteral: valueKey)), for: mainWindow).start(completed: { + updateState { current in + return current.withRemovedValue(valueKey) + } + })) + } + + let removeValueInteractive:([SecureIdValueKey]) -> InputDataValidation = { valueKeys in + return .fail(.doSomething { f in + saveValueDisposable.set(showModalProgress(signal: deleteSecureIdValues(network: context.account.network, keys: Set(valueKeys)) |> deliverOnMainQueue, for: mainWindow).start(completed: { + updateState { current in + return valueKeys.reduce(current, { (current, key) in + return current.withRemovedValue(key) + }) + } + f(.success(.navigationBack)) + })) + }) + } + + switch request.primary { + case .address: + var loadedData: AddressIntermediateState? + let push:(SecureIdRequestedFormFieldValue, SecureIdRequestedFormFieldValue?, Bool) -> Void = { field, relative, hasMainField in + presentController(InputDataController(dataSignal: combineLatest(state.get() |> deliverOnPrepareQueue, appearanceSignal |> deliverOnPrepareQueue) |> map { state, _ in + return addressEntries(state, hasMainField: hasMainField, relative: relative, updateState: updateState) + } |> map { InputDataSignalValue(entries: $0) }, title: relative?.rawValue ?? field.rawValue, validateData: { data in + + if let _ = data[_id_delete] { + return .fail(.doSomething { next in + modernConfirm(for: mainWindow, account: context.account, peerId: nil, header: L10n.telegramPassportController, information: relative == nil ? L10n.secureIdConfirmDeleteAddress : L10n.secureIdConfirmDeleteDocument, thridTitle: hasMainField ? L10n.secureIdConfirmDeleteAddress : nil, successHandler: { result in + var keys: [SecureIdValueKey] = [] + if let relative = relative { + keys.append(relative.valueKey) + } + switch result { + case .basic: + break + case .thrid: + keys.append(field.valueKey) + } + next(removeValueInteractive(keys)) + }) + }) + } + + let current = AddressIntermediateState(data) + + + + let street1 = data[_id_street1]?.stringValue ?? "" + let street2 = data[_id_street2]?.stringValue ?? "" + let city = data[_id_city]?.stringValue ?? "" + let state = data[_id_state]?.stringValue ?? "" + let countryCode = data[_id_country]?.stringValue ?? "" + let postcode = data[_id_postcode]?.stringValue ?? "" + + var fails:[InputDataIdentifier : InputDataValidationFailAction] = [:] + if street1.isEmpty && hasMainField { + fails[_id_street1] = .shake + } + if countryCode.isEmpty && hasMainField { + fails[_id_country] = .shake + } + if city.isEmpty && hasMainField { + fails[_id_city] = .shake + } + if postcode.isEmpty && hasMainField { + fails[_id_postcode] = .shake + } + + var fileIndex: Int = 0 + var verifiedDocuments:[SecureIdVerificationDocumentReference] = [] + while data[InputDataIdentifier("_file_\(fileIndex)")] != nil { + let identifier = InputDataIdentifier("_file_\(fileIndex)") + let value = data[identifier]!.secureIdDocument! + switch value { + case let .remote(reference): + verifiedDocuments.append(.remote(reference)) + case let .local(local): + switch local.state { + case let .uploaded(file): + verifiedDocuments.append(.uploaded(file)) + case .uploading: + fails[identifier] = .shake + } + + } + fileIndex += 1 + } + + var translations:[SecureIdVerificationDocumentReference] = [] + fileIndex = 0 + while data[InputDataIdentifier("_translation_\(fileIndex)")] != nil { + let identifier = InputDataIdentifier("_translation_\(fileIndex)") + let value = data[identifier]!.secureIdDocument! + switch value { + case let .remote(reference): + translations.append(.remote(reference)) + case let .local(local): + switch local.state { + case let .uploaded(file): + translations.append(.uploaded(file)) + case .uploading: + fails[identifier] = .shake + } + + } + fileIndex += 1 + } + + if let relative = relative, relative.hasTranslation, translations.isEmpty, editSettings == nil { + fails[_id_translation] = .shake + } + + if relative != nil, verifiedDocuments.isEmpty { + fails[_id_scan] = .shake + } + + + if !fails.isEmpty { + return .fail(.fields(fails)) + } + + var values:[SecureIdValue] = [] + if let relative = relative { + switch relative { + case .bankStatement: + values.append(SecureIdValue.bankStatement(SecureIdBankStatementValue(verificationDocuments: verifiedDocuments, translations: translations))) + case .rentalAgreement: + values.append(SecureIdValue.rentalAgreement(SecureIdRentalAgreementValue(verificationDocuments: verifiedDocuments, translations: translations))) + case .utilityBill: + values.append(SecureIdValue.utilityBill(SecureIdUtilityBillValue(verificationDocuments: verifiedDocuments, translations: translations))) + case .passportRegistration: + values.append(SecureIdValue.passportRegistration(SecureIdPassportRegistrationValue(verificationDocuments: verifiedDocuments, translations: translations))) + case .temporaryRegistration: + values.append(SecureIdValue.temporaryRegistration(SecureIdTemporaryRegistrationValue(verificationDocuments: verifiedDocuments, translations: translations))) + default: + break + } + } + if hasMainField { + values.append(SecureIdValue.address(SecureIdAddressValue(street1: street1, street2: street2, city: city, state: state, countryCode: countryCode, postcode: postcode))) + } + + if let loadedData = loadedData { + var fails = loadedData.validateErrors(currentState: current, errors: _stateValue.errors(for: .address)) + if let relative = relative { + let errors = _stateValue.errors(for: relative.valueKey) + for error in errors { + var i: Int = 0 + for file in verifiedDocuments { + switch file { + case let .remote(reference): + if error.key == InputDataIdentifier("file_\(reference.fileHash.base64EncodedString())") { + fails[InputDataIdentifier("_file_\(i)")] = .shake + } + i += 1 + default: + break + } + } + for file in translations { + switch file { + case let .remote(reference): + if error.key == InputDataIdentifier("file_\(reference.fileHash.base64EncodedString())") { + fails[InputDataIdentifier("_translation_\(i)")] = .shake + } + i += 1 + default: + break + } + } + } + } + if fails.isEmpty { + if loadedData == current && values.last == value { + return .success(.navigationBack) + } + return proccessValue(values) + } else { + return .fail(.fields(fails)) + } + } + + return .fail(.none) + }, updateDatas: { data in + updateState { current in + var current = current + let address = AddressIntermediateState(data) + var errors = current.errors + + if let loadedData = loadedData { + let updatedErrors = loadedData.removeErrors(currentState: address, errors: errors[request.primary.valueKey]) + errors[request.primary.valueKey] = updatedErrors + current = current.withUpdatedErrors(errors) + } + return current.withUpdatedAddressState(address) + } + + return .fail(.none) + }, afterDisappear: { + updateState { current in + return current.withUpdatedAddressState(nil).withUpdatedValues(current.values).withRemovedInputErrors() + } + }, didLoaded: { _, data in + loadedData = AddressIntermediateState(data) + }, identifier: "passport", backInvocation: { data, f in + if AddressIntermediateState(data) != loadedData { + confirm(for: mainWindow, header: L10n.secureIdDiscardChangesHeader, information: L10n.secureIdDiscardChangesText, okTitle: L10n.alertConfirmDiscard, successHandler: { _ in + f(true) + }) + } else { + f(true) + } + + }, getBackgroundColor: { theme.colors.background })) + } + + if let editSettings = editSettings { + var values:[ValuesSelectorValue] = [] + for relative in relative { + values.append(ValuesSelectorValue(localized: editSettings.hasValue(relative) ? relative.descEdit : relative.descAdd, value: relative)) + } + showModal(with: ValuesSelectorModalController(values: values, selected: values[0], title: L10n.secureIdIdentityDocument, onComplete: { selected in + push(selected.value, selected.value == .address ? nil : selected.value, selected.value == .address) + }), for: mainWindow) + } else if relative.count > 1 { + let values:[ValuesSelectorValue] = relative.map({ValuesSelectorValue(localized: $0.rawValue, value: $0)}) + showModal(with: ValuesSelectorModalController(values: values, selected: values[0], title: L10n.secureIdResidentialAddress, onComplete: { selected in + filePanel(with: photoExts,for: mainWindow, completion: { files in + if let files = files { + push(request.primary, selected.value, request.fillPrimary) + let localFiles:[SecureIdVerificationDocument] = files.map({SecureIdVerificationDocument.local(SecureIdVerificationLocalDocument(id: arc4random64(), resource: LocalFileReferenceMediaResource(localFilePath: $0, randomId: arc4random64()), state: .uploading(0)))}) + updateState { current in + if localFiles.count + (current.files[selected.value.valueKey] ?? []).count > scansLimit { + alert(for: mainWindow, info: L10n.secureIdErrorScansLimit) + } + return current.withAppendFiles(localFiles, for: selected.value.valueKey) + } + } + }) + }), for: mainWindow) + } else if relative.count == 1 { + push(request.primary, relative[0], request.fillPrimary) + } else { + push(request.primary, nil, request.fillPrimary) + } + + + case .personalDetails: + var loadedData:DetailsIntermediateState? + let push:(SecureIdRequestedFormFieldValue, SecureIdRequestedFormFieldValue?, SecureIdRequestedFormFieldValue?) ->Void = { field, relative, primary in + presentController(InputDataController(dataSignal: combineLatest(state.get() |> deliverOnPrepareQueue, appearanceSignal |> deliverOnPrepareQueue) |> map { state, _ in + return identityEntries(state, primary: primary, relative: relative, updateState: updateState) + } |> map { InputDataSignalValue(entries: $0) }, title: relative?.rawValue ?? field.rawValue, validateData: { data in + + + if let _ = data[_id_delete] { + return .fail(.doSomething { next in + modernConfirm(for: mainWindow, account: context.account, peerId: nil, header: L10n.telegramPassportController, information: primary != nil && relative != nil ? L10n.secureIdConfirmDeleteDocument : primary != nil ? L10n.secureIdDeleteConfirmPersonalDetails : L10n.secureIdConfirmDeleteDocument, thridTitle: primary != nil && relative != nil ? L10n.secureIdDeletePersonalDetails : nil, successHandler: { result in + var keys: [SecureIdValueKey] = [] + if let relative = relative { + keys.append(relative.valueKey) + } + switch result { + case .basic: + if primary != nil && relative == nil { + keys.append(field.valueKey) + } + case .thrid: + keys.append(field.valueKey) + } + next(removeValueInteractive(keys)) + }) + }) + } + + let firstName = data[_id_first_name]?.stringValue ?? "" + let lastName = data[_id_last_name]?.stringValue ?? "" + let middleName = data[_id_middle_name]?.stringValue ?? "" + let birthday = data[_id_birthday]?.secureIdDate + let countryCode = data[_id_country]?.stringValue ?? "" + let residence = data[_id_residence]?.stringValue ?? "" + let gender = data[_id_gender]?.gender + let identifier = data[_id_identifier]?.stringValue + + + let firstNameNative = data[_id_first_name_native]?.stringValue ?? "" + let middleNameNative = data[_id_middle_name_native]?.stringValue ?? "" + let lastNameNative = data[_id_last_name_native]?.stringValue ?? "" + + + let expiryDate = data[_id_expire_date]?.secureIdDate + + let selfie = data[_id_selfie]?.secureIdDocument + let frontside = data[_id_frontside]?.secureIdDocument + let backside = data[_id_backside]?.secureIdDocument + + var fails:[InputDataIdentifier : InputDataValidationFailAction] = [:] + if firstName.isEmpty, primary != nil { + fails[_id_first_name] = .shake + } + if lastName.isEmpty, primary != nil { + fails[_id_last_name] = .shake + } + if countryCode.isEmpty && primary != nil { + fails[_id_country] = .shake + } + if residence.isEmpty && primary != nil { + fails[_id_birthday] = .shake + } + if gender == nil && primary != nil { + fails[_id_gender] = .shake + } + if birthday == nil && primary != nil { + fails[_id_birthday] = .shake + } + + if let identifier = identifier, identifier.isEmpty { + fails[_id_identifier] = .shake + } + + + + if let relative = relative, relative.hasSelfie, selfie == nil, editSettings == nil { + fails[_id_selfie_scan] = .shake + } + + var fileIndex: Int = 0 + var translations:[SecureIdVerificationDocumentReference] = [] + while data[InputDataIdentifier("_translation_\(fileIndex)")] != nil { + let identifier = InputDataIdentifier("_translation_\(fileIndex)") + let value = data[identifier]!.secureIdDocument! + switch value { + case let .remote(reference): + translations.append(.remote(reference)) + case let .local(local): + switch local.state { + case let .uploaded(file): + translations.append(.uploaded(file)) + case .uploading: + fails[identifier] = .shake + } + + } + fileIndex += 1 + } + + if let relative = relative, relative.hasTranslation, translations.isEmpty, editSettings == nil { + fails[_id_translation] = .shake + } + + var selfieDocument: SecureIdVerificationDocumentReference? = nil + var frontsideDocument: SecureIdVerificationDocumentReference? = nil + var backsideDocument: SecureIdVerificationDocumentReference? = nil + + if let selfie = selfie { + switch selfie { + case let .remote(reference): + selfieDocument = .remote(reference) + case let .local(local): + switch local.state { + case let .uploaded(file): + selfieDocument = .uploaded(file) + case .uploading: + fails[_id_selfie] = .shake + } + } + } + + if let relative = relative { + if frontside == nil { + fails[_id_frontside] = .shake + } + if relative.hasBacksideDocument { + if backside == nil { + fails[_id_backside] = .shake + } + } + + if let frontside = frontside { + switch frontside { + case let .remote(reference): + frontsideDocument = .remote(reference) + case let .local(local): + switch local.state { + case let .uploaded(file): + frontsideDocument = .uploaded(file) + case .uploading: + fails[_id_frontside] = .shake + } + } + } + if let backside = backside { + switch backside { + case let .remote(reference): + backsideDocument = .remote(reference) + case let .local(local): + switch local.state { + case let .uploaded(file): + backsideDocument = .uploaded(file) + case .uploading: + fails[_id_backside] = .shake + } + } + } + } + + var nativeName: SecureIdPersonName? = nil + if let primary = primary, case let .personalDetails(isNativeName) = primary, isNativeName, data[_id_first_name_native] != nil { + if firstNameNative.isEmpty { + fails[_id_first_name_native] = .shake + } + if lastNameNative.isEmpty { + fails[_id_last_name_native] = .shake + } + if fails.isEmpty { + nativeName = SecureIdPersonName(firstName: firstNameNative, lastName: lastNameNative, middleName: middleNameNative) + } + } + + if !fails.isEmpty { + return .fail(.fields(fails)) + } + + + + + var values: [SecureIdValue] = [] + if primary != nil { + let _birthday = birthday! + let _gender = gender! + values.append(SecureIdValue.personalDetails(SecureIdPersonalDetailsValue(latinName: SecureIdPersonName(firstName: firstName, lastName: lastName, middleName: middleName), nativeName: nativeName, birthdate: _birthday, countryCode: countryCode, residenceCountryCode: residence, gender: _gender))) + } + + if let relative = relative { + let _identifier = identifier! + switch relative.valueKey { + case .idCard: + values.append(SecureIdValue.idCard(SecureIdIDCardValue(identifier: _identifier, expiryDate: expiryDate, verificationDocuments: [], translations: translations, selfieDocument: selfieDocument ?? relativeValue?.selfieVerificationDocument, frontSideDocument: frontsideDocument, backSideDocument: backsideDocument))) + case .passport: + values.append(SecureIdValue.passport(SecureIdPassportValue(identifier: _identifier, expiryDate: expiryDate, verificationDocuments: [], translations: translations, selfieDocument: selfieDocument ?? relativeValue?.selfieVerificationDocument, frontSideDocument: frontsideDocument))) + case .driversLicense: + values.append(SecureIdValue.driversLicense(SecureIdDriversLicenseValue(identifier: _identifier, expiryDate: expiryDate, verificationDocuments: [], translations: translations, selfieDocument: selfieDocument ?? relativeValue?.selfieVerificationDocument, frontSideDocument: frontsideDocument, backSideDocument: backsideDocument))) + case .internalPassport: + values.append(SecureIdValue.internalPassport(SecureIdInternalPassportValue(identifier: _identifier, expiryDate: expiryDate, verificationDocuments: [], translations: translations, selfieDocument: selfieDocument ?? relativeValue?.selfieVerificationDocument, frontSideDocument: frontsideDocument))) + + default: + break + } + } + + let current = DetailsIntermediateState(data) + if let loadedData = loadedData { + var fails = loadedData.validateErrors(currentState: current, errors: _stateValue.errors(for: .personalDetails), relativeErrors: relative != nil ? _stateValue.errors(for: relative!.valueKey) : nil) + if let relative = relative { + let errors = _stateValue.errors(for: relative.valueKey) + for error in errors { + switch error.key { + case _id_selfie: + if let selfieDocument = selfieDocument { + switch selfieDocument { + case .remote: + fails[_id_selfie] = .shake + default: + break + } + } + case _id_frontside: + if let frontsideDocument = frontsideDocument { + switch frontsideDocument { + case .remote: + fails[_id_frontside] = .shake + default: + break + } + } + case _id_frontside: + if let backsideDocument = backsideDocument { + switch backsideDocument { + case .remote: + fails[_id_backside] = .shake + default: + break + } + } + default: + break + } + var i: Int = 0 + for file in translations { + switch file { + case let .remote(reference): + if error.key == InputDataIdentifier("file_\(reference.fileHash.base64EncodedString())") { + fails[InputDataIdentifier("_translation_\(i)")] = .shake + } + i += 1 + default: + break + } + } + } + } + if fails.isEmpty { + if loadedData == current && values.last == value { + return .success(.navigationBack) + } + return proccessValue(values) + } else { + return .fail(.fields(fails)) + } + } + + return .fail(.none) + }, updateDatas: { data in + updateState { current in + var current = current + let details = DetailsIntermediateState(data) + var errors = current.errors + + if let loadedData = loadedData { + let updatedErrors = loadedData.removeErrors(currentState: details, errors: primary != nil ? errors[primary!.valueKey] : nil, relativeErrors: relative != nil ? errors[relative!.valueKey] : nil) + if let primary = primary { + errors[primary.valueKey] = updatedErrors.errors + } + if let relative = relative { + errors[relative.valueKey] = updatedErrors.relativeErrors + } + current = current.withUpdatedErrors(errors) + } + return current.withUpdatedDetailsState(details) + } + return .fail(.none) + }, afterDisappear: { + updateState { current in + return current.withUpdatedDetailsState(nil).withUpdatedValues(current.values).withRemovedInputErrors() + } + }, didLoaded: { _, data in + loadedData = DetailsIntermediateState(data) + }, identifier: "passport", backInvocation: { data, f in + if DetailsIntermediateState(data) != loadedData { + confirm(for: mainWindow, header: L10n.secureIdDiscardChangesHeader, information: L10n.secureIdDiscardChangesText, okTitle: L10n.alertConfirmDiscard, successHandler: { _ in + f(true) + }) + } else { + f(true) + } + + }, getBackgroundColor: { theme.colors.background })) + } + + if let editSettings = editSettings { + var values:[ValuesSelectorValue] = [] + for relative in relative { + values.append(ValuesSelectorValue(localized: editSettings.hasValue(relative) ? relative.descEdit : relative.descAdd, value: relative)) + } + showModal(with: ValuesSelectorModalController(values: values, selected: values[0], title: L10n.secureIdIdentityDocument, onComplete: { selected in + push(selected.value, selected.value.valueKey == .personalDetails ? nil : selected.value, selected.value.valueKey == .personalDetails ? .personalDetails(nativeName: true) : nil) + }), for: mainWindow) + } else if relative.count > 1 { + let values:[ValuesSelectorValue] = relative.map({ValuesSelectorValue(localized: $0.rawValue, value: $0)}) + showModal(with: ValuesSelectorModalController(values: values, selected: values[0], title: L10n.secureIdIdentityDocument, onComplete: { selected in + if let relativeValue = relativeValue, relativeValue.frontSideVerificationDocument != nil { + push(request.primary, selected.value, request.fillPrimary ? request.primary : nil) + } else { + filePanel(with: photoExts, allowMultiple: false, for: mainWindow, completion: { files in + if let file = files?.first { + push(request.primary, selected.value, request.fillPrimary ? request.primary : nil) + updateFrontMrz(file: file, relative: selected.value, updateState: updateState) + } + }) + } + }), for: mainWindow) + } else if relative.count == 1 { + if let relativeValue = relativeValue, relativeValue.frontSideVerificationDocument != nil { + push(request.primary, relative[0], request.fillPrimary ? request.primary : nil) + } else { + filePanel(with: photoExts, allowMultiple: false, for: mainWindow, completion: { files in + if let file = files?.first { + push(request.primary, relative[0], request.fillPrimary ? request.primary : nil) + updateFrontMrz(file: file, relative: relative[0], updateState: updateState) + } + }) + } + + } else { + push(request.primary, nil, request.fillPrimary ? request.primary : nil) + } + + + case .email: + if let valueKey = valueKey { + confirm(for: mainWindow, information: L10n.secureIdRemoveEmail, successHandler: { _ in + _ = removeValue(valueKey) + }) + } else { + let title = L10n.secureIdInstallEmailTitle + var _payload: SecureIdPrepareEmailVerificationPayload? = nil + var _activateEmail: String? = nil + let validate: ([InputDataIdentifier : InputDataValue]) -> InputDataValidation = { data in + let email = data[_id_email_def]?.stringValue ?? data[_id_email_new]?.stringValue + + if let code = data[_id_email_code]?.stringValue, !code.isEmpty, let payload = _payload, let activateEmail = _activateEmail { + return .fail(.doSomething { f in + if let ctx = _stateValue.accessContext { + emailNewActivationDisposable.set(showModalProgress(signal: secureIdCommitEmailVerification(postbox: context.account.postbox, network: context.account.network, context: ctx, payload: payload, code: code) |> deliverOnMainQueue, for: mainWindow).start(error: { error in + f(.fail(.fields([_id_email_new : .shake]))) + }, completed: { + f(proccessValue([SecureIdValue.email(.init(email: activateEmail))])) + })) + } + }) + + } + + if data[_id_email_def] == nil, let email = email, isValidEmail(email) { + return .fail(.doSomething { parent in + emailNewActivationDisposable.set(showModalProgress(signal: secureIdPrepareEmailVerification(network: context.account.network, value: .init(email: email)), for: mainWindow).start(next: { payload in + _payload = payload + _activateEmail = email + updateState { current in + return current.withUpdatedIntermediateEmailState(EmailIntermediateState(email: email, length: payload.length)) + } + }, error: { error in + + })) + + }) + } + + if let email = email, isValidEmail(email) { + return proccessValue([SecureIdValue.email(SecureIdEmailValue(email: email))]) + } else { + if data[_id_email_def] == nil { + return .fail(.fields([_id_email_new : .shake])) + } + } + return .fail(.none) + } + presentController(InputDataController(dataSignal: combineLatest(state.get() |> deliverOnPrepareQueue, appearanceSignal |> deliverOnPrepareQueue) |> map { state, _ in + return emailEntries(state, updateState: updateState) + } |> map { InputDataSignalValue(entries: $0) }, title: title, validateData: validate, updateDatas: { data in + if let payload = _payload, let code = data[_id_email_code]?.stringValue, code.length == payload.length { + return validate(data) + } + return .fail(.none) + }, afterDisappear: { + updateState { current in + return current.withUpdatedIntermediateEmailState(nil) + } + }, identifier: "passport", getBackgroundColor: { theme.colors.background })) + } + + case .phone: + + if let valueKey = valueKey { + confirm(for: mainWindow, information: L10n.secureIdRemovePhoneNumber, successHandler: { _ in + _ = removeValue(valueKey) + }) + } else { + let title = L10n.secureIdInstallPhoneTitle + var _payload: SecureIdPreparePhoneVerificationPayload? + let validate: ([InputDataIdentifier : InputDataValue]) -> InputDataValidation = { data in + let phone = data[_id_phone_def]?.stringValue ?? data[_id_phone_new]?.stringValue + if let phone = phone, !phone.isEmpty { + return .fail(.doSomething { parent in + let result = proccessValue([SecureIdValue.phone(SecureIdPhoneValue(phone: phone))]) + switch result { + case let .fail(progress): + switch progress { + case let .doSomething(next: f): + f { result in + switch result { + case .success: + parent(.success(.navigationBack)) + case .none: + break + case .fail: + phoneNewActivationDisposable.set(showModalProgress(signal: secureIdPreparePhoneVerification(network: context.account.network, value: SecureIdPhoneValue(phone: phone)) |> deliverOnMainQueue, for: mainWindow).start(next: { payload in + + _payload = payload + + let validate: ([InputDataIdentifier : InputDataValue])->InputDataValidation = { data in + return .fail(.doSomething { f in + let code = data[_id_phone_code]?.stringValue ?? "" + if code.isEmpty { + f(.fail(.fields([_id_phone_code : .shake]))) + return + } + if let ctx = _stateValue.accessContext { + phoneNewActivationDisposable.set(showModalProgress(signal: secureIdCommitPhoneVerification(postbox: context.account.postbox, network: context.account.network, context: ctx, payload: payload, code: code) |> deliverOnMainQueue, for: mainWindow).start(next: { value in + updateState { current in + return current.withUpdatedValue(value) + } + f(.success(.navigationBack)) + }, error: { error in + f(.fail(.fields([_id_phone_code : .shake]))) + })) + } + }) + } + + presentController(InputDataController(dataSignal: combineLatest(state.get() |> deliverOnPrepareQueue, appearanceSignal |> deliverOnPrepareQueue) |> map { state, _ in + return confirmPhoneNumberEntries(state, phoneNumber: phone, updateState: updateState) + } |> map { InputDataSignalValue(entries: $0) }, title: title, validateData: validate, updateDatas: { data in + if let payload = _payload, let code = data[_id_phone_code]?.stringValue { + switch payload.type { + case let .sms(length): + if code.length == length { + return validate(data) + } + default: + break + } + } + return .fail(.none) + }, identifier: "passport", getBackgroundColor: { theme.colors.background })) + }, error: { error in + alert(for: mainWindow, info: "\(error)") + })) + + } + } + default: + break + } + default: + break + } + }) + } else { + if data[_id_phone_def] == nil { + return .fail(.fields([_id_phone_new : .shake])) + } + } + return .fail(.none) + } + presentController(InputDataController(dataSignal: state.get() |> map { state in + return phoneNumberEntries(state, updateState: updateState) + } |> map { InputDataSignalValue(entries: $0) }, title: title, validateData: validate, afterDisappear: { + + }, identifier: "passport", getBackgroundColor: { theme.colors.background })) + } + default: + fatalError() + } + + }, createPassword: { + let promise:Promise<[InputDataEntry]> = Promise() + promise.set(combineLatest(state.get() |> deliverOnPrepareQueue, appearanceSignal |> deliverOnPrepareQueue) |> map { state, _ in + return createPasswordEntries(state) + }) + let controller = InputDataController(dataSignal: promise.get() |> map { InputDataSignalValue(entries: $0) }, title: L10n.secureIdCreatePasswordTitle, validateData: { data in + + let password = data[_id_c_password]!.stringValue! + let repassword = data[_id_c_repassword]!.stringValue! + let hint = data[_id_c_hint]!.stringValue! + + var emptyFields:[InputDataIdentifier : InputDataValidationFailAction] = [:] + if password.isEmpty { + emptyFields[_id_c_password] = .shake + } + if repassword.isEmpty { + emptyFields[_id_c_repassword] = .shake + } + + if !emptyFields.isEmpty { + return .fail(.fields(emptyFields)) + } + + if password != repassword { + return .fail(.fields([_id_c_repassword : .shake])) + } + + + let updatePassword: (String, String?) -> Void = { password, email in + updateState { current in + return current.withUpdatedPassword(.password(password: password, pendingEmail: nil)) + } + + passwordVerificationData.set(.single(nil) |> then(context.engine.auth.updateTwoStepVerificationPassword(currentPassword: nil, updatedPassword: .password(password: password, hint: hint, email: email)) + |> `catch` {_ in return .complete()} + |> mapToSignal { result in + + let configuration: TwoStepVerificationConfiguration + switch result { + case let .password(password, pendingEmail): + if let email = email { + configuration = .notSet(pendingEmail: TwoStepVerificationPendingEmail(pattern: email, codeLength: pendingEmail?.codeLength)) + } else { + configuration = .set(hint: hint, hasRecoveryEmail: false, pendingEmail: pendingEmail, hasSecureValues: false, pendingResetTimestamp: nil) + } + + updateState { current in + return current.withUpdatedTmpPwd(password) + } + if email == nil { + checkPwd?(password) + } + + default: + configuration = .notSet(pendingEmail: nil) + } + + return .single(Optional(configuration)) + })) + } + + if let email = data[_id_c_email]?.stringValue { + if isValidEmail(email) { + updatePassword(password, email) + return .success(.navigationBackWithPushAnimation) + } else { + + if email.isEmpty { + return .fail(.doSomething(next: { f in + confirm(for: mainWindow, information: L10n.twoStepAuthEmailSkipAlert, okTitle: L10n.twoStepAuthEmailSkip, successHandler: { result in + updatePassword(password, nil) + f(.success(.navigationBackWithPushAnimation)) + }) + })) + } else { + return .fail(.fields([_id_c_email : .shake])) + } + + } + } + + return .fail(.none) + }, updateDoneValue: { data in + return { f in + f(.enabled(L10n.navigationNext)) + } + }, identifier: "passport", getBackgroundColor: { theme.colors.background }) + + presentController(controller) + }, abortVerification: { + emailActivation.set(nil) + + passwordVerificationData.set(showModalProgress(signal: context.engine.auth.updateTwoStepVerificationPassword(currentPassword: nil, updatedPassword: .none) + |> `catch` {_ in .complete()} + |> mapToSignal { _ in + updateState { current in + return current.withUpdatedPasswordSettings(nil) + } + return .single(TwoStepVerificationConfiguration.notSet(pendingEmail: nil)) + }, for: mainWindow)) + }, authorize: { [weak self] enabled in + + if !enabled { + updateState { current in + return current.withUpdatedEmptyErrors(true) + } + + guard let `self` = self else {return} + var scrollItem:TableRowItem? = nil + + self.genericView.tableView.enumerateItems(with: { item -> Bool in + if let stableId = item.stableId.base as? PassportEntryId { + switch stableId { + case .emptyFieldId: + scrollItem = item + default: + break + } + if scrollItem == nil, let item = item as? GeneralInteractedRowItem, let color = item.descLayout?.attributedString.attribute(NSAttributedString.Key.foregroundColor, at: 0, effectiveRange: nil) as? NSColor { + if color.argb == theme.colors.redUI.argb { + scrollItem = item + } + } + } + return scrollItem == nil + }) + + if let scrollItem = scrollItem { + self.genericView.tableView.scroll(to: TableScrollState.top(id: scrollItem.stableId, innerId: nil, animated: true, focus: .init(focus: true), inset: 0), inset: NSEdgeInsets(), true) + } + + return + } + + + if let inAppRequest = inAppRequest, let encryptedForm = encryptedForm { + authorizeDisposable.set(showModalProgress(signal: state.get() |> take(1) |> mapError { _ in return GrantSecureIdAccessError.generic} |> mapToSignal { state -> Signal in + + var values:[SecureIdValueWithContext] = [] + + let requestedFields = encryptedForm.requestedFields.map { value -> SecureIdRequestedFormField in + switch value { + case let .just(key): + switch key { + case .email, .phone, .personalDetails, .address: + return value + default: + return .oneOf([key]) + } + default: + return value + } + } + + for field in requestedFields { + switch field { + case let .just(field): + if let value = state.values.filter({$0.value.key == field.valueKey}).first { + values.append(value) + } + case let .oneOf(fields): + if fields.count == 1 { + if let value = state.values.filter({$0.value.key == fields[0].valueKey}).first { + values.append(value) + } + } else { + let field = fields.filter({ field in + return state.searchValue(field.valueKey) != nil + }).first + if let field = field, let value = state.values.filter({$0.value.key == field.valueKey}).first { + values.append(value) + } + } + } + } + + return grantSecureIdAccess(network: context.account.network, peerId: inAppRequest.peerId, publicKey: inAppRequest.publicKey, scope: inAppRequest.scope, opaquePayload: inAppRequest.isModern ? Data() : inAppRequest.nonce, opaqueNonce: inAppRequest.isModern ? inAppRequest.nonce : Data(), values: values, requestedFields: encryptedForm.requestedFields) + } |> deliverOnMainQueue, for: mainWindow).start(error: { error in + alert(for: mainWindow, info: "\(error)") + }, completed: { + executeCallback(true) + closeAfterSuccessful() + })) + } + + }, botPrivacy: { [weak self] in + if let url = self?.form?.termsUrl { + execute(inapp: .external(link: url, false)) + } + }, forgotPassword: { + confirm(for: mainWindow, header: L10n.passportResetPasswordConfirmHeader, information: L10n.passportResetPasswordConfirmText, okTitle: L10n.passportResetPasswordConfirmOK, successHandler: { _ in + recoverPasswordDisposable.set(showModalProgress(signal: context.engine.auth.requestTwoStepVerificationPasswordRecoveryCode() |> deliverOnMainQueue, for: mainWindow).start(next: { emailPattern in + let promise:Promise<[InputDataEntry]> = Promise() + promise.set(combineLatest(Signal<[InputDataEntry], NoError>.single(recoverEmailEntries(emailPattern: emailPattern, unavailable: { + alert(for: mainWindow, info: L10n.twoStepAuthRecoveryFailed) + })) |> deliverOnPrepareQueue, appearanceSignal |> deliverOnPrepareQueue) |> map {$0.0}) + presentController(InputDataController(dataSignal: promise.get() |> map { InputDataSignalValue(entries: $0) }, title: L10n.secureIdRecoverPassword, validateData: { data -> InputDataValidation in + + let code = data[_id_email_code]?.stringValue ?? "" + if code.isEmpty { + return .fail(.fields([_id_email_code : .shake])) + } + + return .fail(.doSomething { f in + confirm(for: mainWindow, information: L10n.secureIdWarningDataLost, successHandler: { _ in + recoverPasswordDisposable.set(showModalProgress(signal: context.engine.auth.checkPasswordRecoveryCode(code: code) |> deliverOnMainQueue, for: mainWindow).start(error: { error in + f(.fail(.fields([_id_email_code : .shake]))) + }, completed: { + updateState { current in + return current.withUpdatedPassword(nil) + } + passwordVerificationData.set(.single(.notSet(pendingEmail: nil))) + + f(.success(.navigationBack)) + })) + }) + }) + + }, identifier: "passport", getBackgroundColor: { theme.colors.background })) + })) + }) + + // + }, deletePassport: { + confirm(for: mainWindow, header: L10n.secureIdInfoTitle, information: L10n.secureIdInfoDeletePassport, successHandler: { _ in + updateState { current in + let signal = deleteSecureIdValues(network: context.account.network, keys: Set(current.values.map{$0.value.key})) + + _ = (signal |> deliverOnMainQueue).start(next: { + + }, error:{ error in + alert(for: mainWindow, info: "\(error)") + }, completed: { + updateState { current in + return current.withRemovedValues() + } + closeController() + }) + return current + } + }) + }, updateEmailCode: { [weak self] in + guard let `self` = self else {return} + if let item = self.genericView.tableView.item(stableId: PassportEntryId.inputEmailCode) as? InputDataRowItem { + updateState { state in + return state.withUpdatedEmailCode(item.currentText.string).withUpdatedEmailCodeError(nil) + } + if item.limit == item.currentText.string.length { + _ = self.pendingEmailConfirm() + } + } + }) + + + checkPwd = { value in + arguments.checkPassword((value, {})) + } + + + + + + let botPeerSignal = form != nil ? context.account.postbox.loadedPeerWithId(form!.peerId) |> map {Optional($0)} |> deliverOnPrepareQueue : Signal.single(nil) + + let signal: Signal<(TableUpdateTransition, Bool, Bool), NoError> = combineLatest(appearanceSignal |> deliverOnPrepareQueue, formValue.get() |> deliverOnPrepareQueue, passwordVerificationData.get() |> deliverOnPrepareQueue, state.get() |> deliverOnPrepareQueue, botPeerSignal) |> map { appearance, form, passwordData, state, peer in + + let (entries, enabled) = passportEntries(encryptedForm: form.0, form: form.1, peer: peer, passwordData: passwordData, state: state) + + let converted = entries.map {AppearanceWrapperEntry(entry: $0, appearance: appearance)} + return (prepareTransition(left: previous.swap(converted), right: converted, initialSize: initialSize.modify{$0}, arguments: arguments), enabled, form.1 != nil) + } |> deliverOnMainQueue |> afterDisposed { + actionsDisposable.dispose() + } + + if let pwd = context.temporaryPassword { + actionsDisposable.add((passwordVerificationData.get() |> filter {$0 != nil} |> take(1) |> deliverOnMainQueue).start(next: { _ in + arguments.checkPassword((pwd, { + context.resetTemporaryPwd() + updateState { current in + return current.withUpdatedTmpPwd(nil) + } + })) + })) + } + + passwordVerificationData.set(.single(nil) |> then(context.engine.auth.twoStepVerificationConfiguration() |> map {Optional($0)})) + + + secureIdConfigurationDisposable.set(secureIdConfiguration(postbox: context.account.postbox, network: context.account.network).start(next: { configuration in + updateState { current in + return current.withUpdatedConfiguration(configuration) + } + })) + + actionsDisposable.add((passwordVerificationData.get() |> deliverOnMainQueue).start(next: { [weak self] configuration in + self?.updateRightView(configuration) + })) + + disposable.set(signal.start(next: { [weak self] transition, enabled, isVisible in + guard let `self` = self else {return} + self.genericView.tableView.merge(with: transition) + self.genericView.updateEnabled(enabled, isVisible: isVisible, action: arguments.authorize) + + self.readyOnce() + if self.window?.firstResponder == nil || self.window?.firstResponder == self.window { + _ = self.window?.makeFirstResponder(self.firstResponder()) + } + })) + + } + + private func updateRightView(_ passwordData: TwoStepVerificationConfiguration?) { + + var hasPendingEmail: Bool = false + + let context = self.context + + if let passwordData = passwordData { + switch passwordData { + case let .notSet(pendingEmail): + hasPendingEmail = pendingEmail != nil + case let .set(_, _, pendingEmail, _, _): + hasPendingEmail = pendingEmail != nil + } + } + + rightView?.removeAllHandlers() + if hasPendingEmail { + rightView?.removeImage(for: .Normal) + rightView?.set(text: L10n.navigationDone, for: .Normal) + rightView?.set(handler: { [weak self] _ in + _ = self?.pendingEmailConfirm() + }, for: .Click) + } else { + rightView?.set(handler: { _ in + confirm(for: mainWindow, header: L10n.secureIdInfoTitle, information: L10n.secureIdInfo, cancelTitle: "", thridTitle: L10n.secureIdInfoMore, successHandler: { result in + if result == .thrid { + openFaq(context: context) + } + }) + }, for: .Click) + + rightView?.set(image: theme.icons.passportInfo, for: .Normal) + rightView?.set(text: "", for: .Normal) + } + } + + override func returnKeyAction() -> KeyHandlerResult { + _ = pendingEmailConfirm() + return super.returnKeyAction() + } + + override func becomeFirstResponder() -> Bool? { + return true + } + + override func backKeyAction() -> KeyHandlerResult { + return .invokeNext + } + + private var dismissed:Bool = false + override func invokeNavigationBack() -> Bool { + if form == nil { + return true + } + if !dismissed { + confirm(for: mainWindow, information: L10n.secureIdConfirmCancel, okTitle: L10n.alertConfirmStop, successHandler: { [weak self] _ in + guard let `self` = self else {return} + self.dismissed = true + self.executeCallback(false) + _ = self.executeReturn() + }) + } + + return dismissed + } + + private func executeCallback(_ success: Bool) { + if let request = request, let callback = request.callback { + if callback.hasPrefix("tgbot") { + let r = callback.nsstring.range(of: "://") + if r.location != NSNotFound { + let rawBotId = callback.nsstring.substring(with: NSMakeRange(5, r.location - 5)) + if let botId = Int32(rawBotId) { + let sdkCallback = "tgbot\(botId)://passport" + if sdkCallback == callback { + execute(inapp: .external(link: sdkCallback + (success ? "/success" : "/cancel"), false)) + } + } + } + } else { + execute(inapp: .external(link: addUrlParameter(value: "tg_passport=\(success ? "success" : "cancel")", to: callback), false)) + } + } + } + + deinit { + disposable.dispose() + secureIdConfigurationDisposable.dispose() + } + + override func firstResponder() -> NSResponder? { + var responder: NSResponder? = nil + genericView.tableView.enumerateViews { view -> Bool in + if let view = view as? PassportInsertPasswordRowView { + if self.window?.firstResponder == view.input.textView { + responder = view.input.textView + } else { + responder = view.input + } + } else if let view = view as? InputDataRowView { + responder = view.firstResponder + } + return responder == nil + } + return responder + } + +} diff --git a/Telegram-Mac/PassportDocumentRowItem.swift b/Telegram-Mac/PassportDocumentRowItem.swift new file mode 100644 index 0000000000..42fb224509 --- /dev/null +++ b/Telegram-Mac/PassportDocumentRowItem.swift @@ -0,0 +1,218 @@ +// +// PassportDocumentRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 21/03/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + +class PassportDocumentRowItem: GeneralRowItem, InputDataRowDataValue { + + fileprivate let title: TextViewLayout + fileprivate let status: TextViewLayout + fileprivate let removeAction:(SecureIdVerificationDocument)->Void + fileprivate let context: AccountContext + + fileprivate let documentValue: SecureIdDocumentValue + + var value: InputDataValue { + return .secureIdDocument(documentValue.document) + } + fileprivate var accessContext: SecureIdAccessContext { + return documentValue.context + } + var image: TelegramMediaImage { + return documentValue.image + } + + fileprivate private(set) var uploadingProgress: Float? + + init(_ initialSize: NSSize, context: AccountContext, document: SecureIdDocumentValue, error: InputDataValueError?, header: String, removeAction:@escaping(SecureIdVerificationDocument)->Void) { + self.documentValue = document + self.context = context + title = TextViewLayout(.initialize(string: header, color: theme.colors.text, font: .normal(.text))) + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeZone = NSTimeZone.local + formatter.timeStyle = .short + + switch document.document { + case let .remote(file): + if let error = error { + status = TextViewLayout(.initialize(string: error.description, color: theme.colors.redUI, font: .normal(.text)), maximumNumberOfLines: 1) + } else { + status = TextViewLayout(.initialize(string: formatter.string(from: Date(timeIntervalSince1970: TimeInterval(file.timestamp))), color: theme.colors.grayText, font: .normal(.text)), maximumNumberOfLines: 1) + } + case let .local(file): + switch file.state { + case .uploaded: + status = TextViewLayout(.initialize(string: formatter.string(from: Date()), color: theme.colors.grayText, font: .normal(.text)), maximumNumberOfLines: 1) + case let .uploading(progress): + uploadingProgress = progress + status = TextViewLayout(.initialize(string: L10n.secureIdFileUploadProgress("\(Int(progress * 100.0))"), color: theme.colors.grayText, font: .normal(.text)), maximumNumberOfLines: 1) + } + } + + self.removeAction = removeAction + super.init(initialSize, stableId: document.stableId, error: error) + + _ = makeSize(initialSize.width, oldWidth: 0) + } + + override func viewClass() -> AnyClass { + return PassportDocumentRowView.self + } + + override var instantlyResize: Bool { + return true + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) + title.measure(width: width) + status.measure(width: width) + return success + } + + override var height: CGFloat { + return 60 + } + +} + + +final class PassportDocumentRowView : TableRowView { + private let statusView = TextView() + private let titleView = TextView() + private let imageView = TransformImageView() + private let removeButton = ImageButton() + private let progressView: RadialProgressView = RadialProgressView() + private let downloadingProgress: MetaDisposable = MetaDisposable() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(titleView) + addSubview(statusView) + addSubview(imageView) + addSubview(removeButton) + imageView.setFrameSize(NSMakeSize(60, 50)) + + removeButton.set(handler: { [weak self] _ in + guard let item = self?.item as? PassportDocumentRowItem else {return} + item.removeAction(item.documentValue.document) + }, for: .Click) + } + + override func shakeView() { + statusView.shake() + } + + deinit { + downloadingProgress.dispose() + } + + override func layout() { + super.layout() + + guard let item = item as? GeneralRowItem else {return} + imageView.centerY(x: item.inset.left) + titleView.setFrameOrigin(imageView.frame.maxX + 10, 10) + statusView.setFrameOrigin(imageView.frame.maxX + 10, frame.height - 10 - statusView.frame.height) + removeButton.centerY(x: frame.width - removeButton.frame.width - item.inset.right) + progressView.center() + } + + override func mouseUp(with event: NSEvent) { + if imageView._mouseInside() { + guard let item = item as? PassportDocumentRowItem, let table = item.table else {return} + var passportItems:[PassportDocumentRowItem] = [] + table.enumerateItems { item -> Bool in + if let item = item as? PassportDocumentRowItem { + passportItems.append(item) + } + return true + } + let index = passportItems.index(of: item)! + showSecureIdDocumentsGallery(context: item.context, medias: passportItems.map({$0.documentValue}), firstIndex: index, item.table) + } else { + super.mouseUp(with: event) + } + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + super.draw(layer, in: ctx) + guard let item = item as? PassportDocumentRowItem, let table = item.table else {return} + + ctx.setFillColor(theme.colors.border.cgColor) + ctx.fill(NSMakeRect(imageView.frame.maxX + 10, frame.height - .borderSize, frame.width - imageView.frame.maxX - item.inset.right - 10, .borderSize)) + } + + override func interactionContentView(for innerId: AnyHashable, animateIn: Bool) -> NSView { + return imageView + } + + override func updateColors() { + super.updateColors() + statusView.backgroundColor = theme.colors.background + titleView.backgroundColor = theme.colors.background + } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + + guard let item = item as? PassportDocumentRowItem else {return} + statusView.update(item.status) + titleView.update(item.title) + + if let progress = item.uploadingProgress { + progressView.state = .Fetching(progress: progress, force: false) + if progressView.superview == nil { + imageView.addSubview(progressView) + } + } else { + progressView.state = .None + progressView.removeFromSuperview() + } + + downloadingProgress.set((chatMessagePhotoStatus(account: item.context.account, photo: item.image) |> deliverOnMainQueue).start(next: { [weak self] status in + guard let `self` = self else {return} + guard let item = self.item as? PassportDocumentRowItem else {return} + switch status { + case let .Fetching(_, progress): + self.progressView.state = .Fetching(progress: progress, force: false) + if self.progressView.superview == nil { + self.imageView.addSubview(self.progressView) + self.progressView.center() + } + + default: + if item.uploadingProgress == nil { + self.progressView.state = .None + self.progressView.removeFromSuperview() + } + } + })) + + self.progressView.fetchControls = FetchControls(fetch: { [weak item] in + guard let item = item else {return} + item.removeAction(item.documentValue.document) + }) + + imageView.setSignal(chatWebpageSnippetPhoto(account: item.context.account, imageReference: ImageMediaReference.standalone(media: item.image), scale: backingScaleFactor, small: true, secureIdAccessContext: item.accessContext)) + _ = chatMessagePhotoInteractiveFetched(account: item.context.account, imageReference: ImageMediaReference.standalone(media: item.image)).start() + imageView.set(arguments: TransformImageArguments(corners: .init(radius: .cornerRadius), imageSize: NSMakeSize(60, 50), boundingSize: NSMakeSize(60, 50), intrinsicInsets: NSEdgeInsets())) + removeButton.set(image: theme.icons.stickerPackDelete, for: .Normal) + _ = removeButton.sizeToFit() + needsLayout = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/PassportHeaderItem.swift b/Telegram-Mac/PassportHeaderItem.swift new file mode 100644 index 0000000000..96cd2728b2 --- /dev/null +++ b/Telegram-Mac/PassportHeaderItem.swift @@ -0,0 +1,101 @@ +// +// PassportHeaderItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 20/03/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import Postbox +import TelegramCore + + +class PassportHeaderItem: TableRowItem { + fileprivate let botPhoto: AvatarNodeState + fileprivate let textLayout: TextViewLayout + fileprivate let account: Account + fileprivate let _stableId: AnyHashable + + override var stableId: AnyHashable { + return _stableId + } + init(_ initialSize: NSSize, account: Account, stableId: AnyHashable, requestedFields: [SecureIdRequestedFormField], peer: Peer) { + self.account = account + self._stableId = stableId + self.botPhoto = .PeerAvatar(peer, peer.displayLetters, peer.smallProfileImage, nil, nil) + + let attributed = NSMutableAttributedString() + + _ = attributed.append(string: L10n.secureIdRequestHeader1(peer.displayTitle), color: theme.colors.grayText, font: .normal(.text)) + attributed.detectBoldColorInString(with: .bold(.text)) + self.textLayout = TextViewLayout(attributed, alignment: .left) + + super.init(initialSize) + + _ = makeSize(initialSize.width, oldWidth: 0) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) + textLayout.measure(width: width - 120) + + return success + } + + override var instantlyResize: Bool { + return true + } + + override func viewClass() -> AnyClass { + return PassportHeaderRowView.self + } + + override var height: CGFloat { + return max(50, textLayout.layoutSize.height) + } + +} + + +private final class PassportHeaderRowView : TableRowView { + private let botPhoto: AvatarControl = AvatarControl(font: .avatar(20)) + private let textView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(botPhoto) + addSubview(textView) + textView.userInteractionEnabled = false + textView.isSelectable = false + botPhoto.setFrameSize(50, 50) + } + + override func updateColors() { + super.updateColors() + textView.backgroundColor = theme.colors.background + } + + override func layout() { + super.layout() + + botPhoto.centerY(x: 20) + + textView.centerY(x: botPhoto.frame.maxX + 20) + } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + + guard let item = item as? PassportHeaderItem else {return} + + textView.update(item.textLayout) + botPhoto.setState(account: item.account, state: item.botPhoto) + + needsLayout = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/PassportInsertPasswordItem.swift b/Telegram-Mac/PassportInsertPasswordItem.swift new file mode 100644 index 0000000000..1d187e598c --- /dev/null +++ b/Telegram-Mac/PassportInsertPasswordItem.swift @@ -0,0 +1,231 @@ +// +// PassportInsertPasswordItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 20/03/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import Postbox +import TelegramCore + +import SwiftSignalKit + +private final class PassportInsertPasswordField : NSSecureTextField { + + override func resignFirstResponder() -> Bool { + (self.delegate as? PassportInsertPasswordRowView)?.controlTextDidBeginEditing(Notification(name: NSControl.textDidChangeNotification)) + return super.resignFirstResponder() + } + + override func becomeFirstResponder() -> Bool { + (self.delegate as? PassportInsertPasswordRowView)?.controlTextDidEndEditing(Notification(name: NSControl.textDidChangeNotification)) + return super.becomeFirstResponder() + } + + override func mouseDown(with event: NSEvent) { + superview?.mouseDown(with: event) + } +} + +class PassportInsertPasswordItem: GeneralRowItem { + private let _stableId: AnyHashable + fileprivate let descLayout: TextViewLayout + fileprivate let checkPasswordAction:((String, ()->Void))->Void + fileprivate let forgotPassword: ()->Void + fileprivate let hasRecoveryEmail: Bool + init(_ initialSize: NSSize, stableId: AnyHashable, checkPasswordAction: @escaping((String, ()->Void))->Void, forgotPassword: @escaping()->Void, hasRecoveryEmail: Bool, isSettings: Bool, error: String?) { + self._stableId = stableId + self.checkPasswordAction = checkPasswordAction + self.forgotPassword = forgotPassword + self.hasRecoveryEmail = hasRecoveryEmail + descLayout = TextViewLayout(.initialize(string: error != nil ? error! : (isSettings ? L10n.secureIdInsertPasswordSettingsDescription : L10n.secureIdInsertPasswordDescription), color: error != nil ? theme.colors.redUI : theme.colors.grayText, font: .normal(.text)), alignment: .center) + super.init(initialSize) + _ = makeSize(initialSize.width, oldWidth: 0) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) + descLayout.measure(width: width - inset.left - inset.right) + + return success + } + + override var stableId: AnyHashable { + return _stableId + } + + override var instantlyResize: Bool { + return true + } + + override func viewClass() -> AnyClass { + return PassportInsertPasswordRowView.self + } + + override var height: CGFloat { + return 32 + 36 + 20 + 30 + 25 + } +} + + +final class PassportInsertPasswordRowView : GeneralRowView, NSTextFieldDelegate { + let input:NSSecureTextField + private let inputContainer: View = View() + private let descTextView: TextView = TextView() + private let nextButton: TitleButton = TitleButton() + private let forgotPassword: ImageButton = ImageButton() + required init(frame frameRect: NSRect) { + input = PassportInsertPasswordField(frame: NSZeroRect) + super.init(frame: frameRect) + input.stringValue = "" + + + descTextView.userInteractionEnabled = false + descTextView.isSelectable = false + + addSubview(inputContainer) + inputContainer.setFrameSize(250, 36) + + input.wantsLayer = true + input.isBordered = false + input.isBezeled = false + input.focusRingType = .none + input.delegate = self + input.drawsBackground = false + input.isEditable = true + input.isSelectable = true + input.font = .normal(.text) + inputContainer.backgroundColor = theme.colors.grayBackground + inputContainer.layer?.cornerRadius = .cornerRadius + + inputContainer.addSubview(input) + + + input.target = self + input.action = #selector(checkPasscode) + + addSubview(descTextView) + addSubview(nextButton) + inputContainer.addSubview(forgotPassword) + + nextButton.set(handler: { [weak self] _ in + self?.checkPasscode() + }, for: .Click) + + forgotPassword.set(handler: { [weak self] _ in + if let item = self?.item as? PassportInsertPasswordItem { + if item.hasRecoveryEmail { + item.forgotPassword() + } else { + alert(for: mainWindow, info: L10n.secureIdForgotPasswordNoEmail) + } + } + }, for: .Click) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func mouseDown(with event: NSEvent) { + if inputContainer.mouseInside() || input._mouseInside() { + (window as? Window)?.applyResponderIfNeeded() + } else { + super.mouseDown(with: event) + } + } + + func controlTextDidChange(_ obj: Notification) { + + } + + func controlTextDidBeginEditing(_ obj: Notification) { + input.textView?.insertionPointColor = theme.colors.text + } + + func controlTextDidEndEditing(_ obj: Notification) { + + } + + override func layout() { + input.setFrameSize(NSMakeSize(inputContainer.frame.width - 20 - forgotPassword.frame.width - 10, input.frame.height)) + input.centerY(x: 10) + descTextView.centerX() + inputContainer.centerX(y: 32 + 20) + nextButton.centerX(y: inputContainer.frame.maxY + 15) + forgotPassword.centerY(x: inputContainer.frame.width - forgotPassword.frame.width - 10, addition: backingScaleFactor == 2 ? -0.5 : 0) + } + + @objc func checkPasscode() { + guard let item = item as? PassportInsertPasswordItem else {return} + + item.checkPasswordAction((input.stringValue, { [weak self] in + assertOnMainThread() + (self?.window as? Window)?.applyResponderIfNeeded() + self?.input.shake() + self?.input.textView?.selectAllText() + })) + } + + + + override func updateColors() { + super.updateColors() + input.textColor = theme.colors.text + input.backgroundColor = .clear + descTextView.backgroundColor = theme.colors.background + inputContainer.backgroundColor = theme.colors.grayBackground + + let attr = NSMutableAttributedString() + _ = attr.append(string: L10n.secureIdInsertPasswordPassword, color: theme.colors.grayText, font: .normal(.title)) + input.placeholderAttributedString = attr + input.font = .normal(.title) + input.sizeToFit() + nextButton.set(font: .normal(.title), for: .Normal) + nextButton.set(color: .white, for: .Normal) + nextButton.set(background: theme.colors.accent, for: .Normal) + nextButton.set(text: L10n.secureIdInsertPasswordNext, for: .Normal) + _ = nextButton.sizeToFit(NSMakeSize(40, 0), NSMakeSize(.greatestFiniteMagnitude, 40)) + nextButton.layer?.cornerRadius = 20 + + forgotPassword.set(image: theme.icons.passportForgotPassword, for: .Normal) + _ = forgotPassword.sizeToFit() + + var bp:Int = 0 + bp += 1 + } + + override func viewDidMoveToWindow() { + if let window = window as? Window { + window.applyResponderIfNeeded() + } + } + + override var firstResponder: NSResponder? { + return input + } + + override var mouseInsideField: Bool { + return input._mouseInside() + } + + override func hitTest(_ point: NSPoint) -> NSView? { + switch true { + case NSPointInRect(point, inputContainer.frame): + return input + default: + return super.hitTest(point) + } + } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + guard let item = item as? PassportInsertPasswordItem else {return} + descTextView.update(item.descLayout) + needsLayout = true + } + +} diff --git a/Telegram-Mac/PassportNewPhoneNumberRowItem.swift b/Telegram-Mac/PassportNewPhoneNumberRowItem.swift new file mode 100644 index 0000000000..334700b0fe --- /dev/null +++ b/Telegram-Mac/PassportNewPhoneNumberRowItem.swift @@ -0,0 +1,452 @@ +// +// SecureIdNewPhoneNumberRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 21/03/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import SwiftSignalKit + +private let manager = CountryManager() + + +private final class PassportPhoneNumberArguments { + let sendCode:(String)->Void + init(sendCode:@escaping(String)->Void) { + self.sendCode = sendCode + } +} + +private final class PassportPhoneTextField : NSTextField { + + override func resignFirstResponder() -> Bool { + (self.delegate as? PassportPhoneContainerView)?.controlTextDidBeginEditing(Notification(name: NSControl.textDidChangeNotification)) + return super.resignFirstResponder() + } + + override func becomeFirstResponder() -> Bool { + (self.delegate as? PassportPhoneContainerView)?.controlTextDidEndEditing(Notification(name: NSControl.textDidChangeNotification)) + return super.becomeFirstResponder() + } + + override func mouseDown(with event: NSEvent) { + superview?.mouseDown(with: event) + } +} + + +private class PassportPhoneContainerView : View, NSTextFieldDelegate { + + var arguments:PassportPhoneNumberArguments? + + + private let countrySelector:TitleButton = TitleButton() + + + fileprivate let errorLabel:LoginErrorStateView = LoginErrorStateView() + + let codeText:PassportPhoneTextField = PassportPhoneTextField() + let numberText:PassportPhoneTextField = PassportPhoneTextField() + + fileprivate var selectedItem:CountryItem? + private let manager: CountryManager + + required init(frame frameRect: NSRect, manager: CountryManager) { + self.manager = manager + super.init(frame: frameRect) + + + countrySelector.style = ControlStyle(font: NSFont.medium(.title), foregroundColor: theme.colors.accent, backgroundColor: theme.colors.background) + countrySelector.set(text: "France", for: .Normal) + _ = countrySelector.sizeToFit() + addSubview(countrySelector) + + + + countrySelector.set(handler: { [weak self] _ in + self?.showCountrySelector() + }, for: .Click) + + updateLocalizationAndTheme(theme: theme) + + codeText.stringValue = "+" + + codeText.textColor = theme.colors.text + codeText.font = NSFont.normal(.title) + numberText.textColor = theme.colors.text + numberText.font = NSFont.normal(.title) + + numberText.isBordered = false + numberText.isBezeled = false + numberText.drawsBackground = false + numberText.focusRingType = .none + + codeText.drawsBackground = false + codeText.isBordered = false + codeText.isBezeled = false + codeText.focusRingType = .none + + codeText.delegate = self + codeText.nextResponder = numberText + codeText.nextKeyView = numberText + + numberText.delegate = self + numberText.nextResponder = codeText + numberText.nextKeyView = codeText + addSubview(codeText) + addSubview(numberText) + + errorLabel.layer?.opacity = 0 + addSubview(errorLabel) + + let code = NSLocale.current.regionCode ?? "US" + update(selectedItem: manager.item(bySmallCountryName: code), update: true) + + + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + backgroundColor = theme.colors.background + + numberText.placeholderAttributedString = NSAttributedString.initialize(string: tr(L10n.loginPhoneFieldPlaceholder), color: theme.colors.grayText, font: NSFont.normal(.header), coreText: false) + codeText.textView?.insertionPointColor = theme.colors.indicatorColor + numberText.textView?.insertionPointColor = theme.colors.indicatorColor + + needsLayout = true + } + + func setPhoneError(_ error: AuthorizationCodeRequestError) { + let text:String + switch error { + case .invalidPhoneNumber: + text = tr(L10n.phoneNumberInvalid) + case .limitExceeded: + text = tr(L10n.loginFloodWait) + case .generic: + text = "undefined error" + case .phoneLimitExceeded: + text = "undefined error" + case .phoneBanned: + text = "PHONE BANNED" + case .timeout: + text = "timeout" + } + errorLabel.state.set(.single(.error(text))) + } + + func update(countryCode: Int32, number: String) { + self.codeText.stringValue = "\(countryCode)" + self.numberText.stringValue = formatPhoneNumber(number) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + override func layout() { + super.layout() + codeText.sizeToFit() + numberText.sizeToFit() + + let maxInset: CGFloat = 0 + let contentInset = maxInset + countrySelector.setFrameOrigin(contentInset - 2, floorToScreenPixels(backingScaleFactor, 25 - countrySelector.frame.height/2)) + + codeText.setFrameOrigin(contentInset, floorToScreenPixels(backingScaleFactor, 75 - codeText.frame.height/2)) + numberText.setFrameOrigin(contentInset + separatorInset, floorToScreenPixels(backingScaleFactor, 75 - codeText.frame.height/2)) + errorLabel.centerX(y: 120) + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + super.draw(layer, in: ctx) + + + let maxInset: CGFloat = 0 + ctx.setFillColor(theme.colors.border.cgColor) + ctx.fill(NSMakeRect(maxInset, 50, frame.width - maxInset, .borderSize)) + ctx.fill(NSMakeRect(maxInset, 100, frame.width - maxInset, .borderSize)) + // ctx.fill(NSMakeRect(maxInset + separatorInset, 50, .borderSize, 50)) + } + + + func showCountrySelector() { + + var items:[ContextMenuItem] = [] + for country in manager.countries { + let item = ContextMenuItem(country.fullName, handler: { [weak self] in + self?.update(selectedItem: country, update: true) + }) + items.append(item) + } + if let currentEvent = NSApp.currentEvent { + ContextMenu.show(items: items, view: countrySelector, event: currentEvent, onShow: {(menu) in + + }, onClose: {}) + } + + } + + func controlTextDidBeginEditing(_ obj: Notification) { + codeText.textView?.backgroundColor = theme.colors.background + numberText.textView?.backgroundColor = theme.colors.background + codeText.textView?.insertionPointColor = theme.colors.indicatorColor + numberText.textView?.insertionPointColor = theme.colors.indicatorColor + } + + func controlTextDidEndEditing(_ obj: Notification) { + + } + + func controlTextDidChange(_ obj: Notification) { + + + if let field = obj.object as? NSTextField { + + field.textView?.backgroundColor = theme.colors.background + + let code = codeText.stringValue.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() + let dec = code.prefix(4) + + if field == codeText { + + + if code.length > 4 { + let list = Array(code).map {String($0)} + let reduced = list.reduce([], { current, value -> [String] in + var current = current + current.append((current.last ?? "") + value) + return current + }).map({Int($0)}).filter({$0 != nil}).map({$0!}) + + var found: Bool = false + for _code in reduced { + if let item = manager.item(byCodeNumber: _code) { + codeText.stringValue = "+" + String(_code) + update(selectedItem: item, update: true, updateCode: false) + + let codeString = String(_code) + var formated = formatPhoneNumber(codeString + String(code[codeString.endIndex.. Bool { + if commandSelector == #selector(insertNewline(_:)) { + if control == codeText { + self.window?.makeFirstResponder(self.numberText) + self.numberText.selectText(nil) + } else if !numberText.stringValue.isEmpty { + arguments?.sendCode(number) + } + //Queue.mainQueue().justDispatch { + (control as? NSTextField)?.setCursorToEnd() + //} + return true + } else if commandSelector == #selector(deleteBackward(_:)) { + if control == numberText { + if numberText.stringValue.isEmpty { + Queue.mainQueue().justDispatch { + self.window?.makeFirstResponder(self.codeText) + self.codeText.setCursorToEnd() + } + } + } + return false + + } + return false + } + + func update(selectedItem:CountryItem?, update:Bool, updateCode:Bool = true) -> Void { + self.selectedItem = selectedItem + if update { + countrySelector.set(text: selectedItem?.shortName ?? tr(L10n.loginInvalidCountryCode), for: .Normal) + _ = countrySelector.sizeToFit() + if updateCode { + codeText.stringValue = selectedItem != nil ? "+\(selectedItem!.code)" : "+" + } + needsLayout = true + setNeedsDisplayLayer() + + } + } + + + + var separatorInset:CGFloat { + return codeText.frame.width + 10 + } + +} + + + +class PassportNewPhoneNumberRowItem: GeneralRowItem, InputDataRowDataValue { + + var value: InputDataValue { + return _value + } + + fileprivate var _value: InputDataValue = .string("") + + init(_ initialSize: NSSize, stableId: AnyHashable, action: @escaping()->Void) { + super.init(initialSize, height: 110, stableId: stableId, action: action) + } + + + override func viewClass() -> AnyClass { + return PassportNewPhoneNumberRowView.self + } +} + + +private final class PassportNewPhoneNumberRowView : TableRowView { + fileprivate let container: PassportPhoneContainerView = PassportPhoneContainerView(frame: NSMakeRect(0, 0, 300, 110), manager: manager) + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(container) + } + +// override var firstResponder: NSResponder? { +// if container.numberText._mouseInside() { +// return container.numberText +// } else if container.codeText._mouseInside() { +// return container.codeText +// } +// return container.numberText +// } +// + override var mouseInsideField: Bool { + return container.numberText._mouseInside() || container.codeText._mouseInside() + } + + override func hitTest(_ point: NSPoint) -> NSView? { + switch true { + case NSPointInRect(point, container.numberText.frame): + return container.numberText + case NSPointInRect(point, container.codeText.frame): + return container.codeText + default: + return super.hitTest(point) + } + } + + override func hasFirstResponder() -> Bool { + return true + } + + override var firstResponder: NSResponder? { + let isKeyDown = NSApp.currentEvent?.type == NSEvent.EventType.keyDown && NSApp.currentEvent?.keyCode == KeyboardKey.Tab.rawValue + switch true { + case container.codeText._mouseInside() && !isKeyDown: + return container.codeText + case container.numberText._mouseInside() && !isKeyDown: + return container.numberText + default: + switch true { + case container.codeText.textView == window?.firstResponder: + return container.codeText.textView + case container.numberText.textView == window?.firstResponder: + return container.numberText.textView + default: + return container.numberText + } + } + } + + + override func nextResponder() -> NSResponder? { + if window?.firstResponder == container.codeText.textView { + return container.numberText + } + if window?.firstResponder == container.numberText.textView { + return container.codeText + } + return nil + } + + override func layout() { + super.layout() + + guard let item = item as? GeneralRowItem else { + return + } + container.setFrameSize(frame.width - item.inset.left - item.inset.right, container.frame.height) + container.center() + } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + + container.arguments = PassportPhoneNumberArguments(sendCode: { [weak self] phone in + guard let item = self?.item as? PassportNewPhoneNumberRowItem else {return} + item._value = .string(phone) + }) + } + + override func updateColors() { + super.updateColors() + container.updateLocalizationAndTheme(theme: theme) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + +} diff --git a/Telegram-Mac/PassportSettingsHeader.swift b/Telegram-Mac/PassportSettingsHeader.swift new file mode 100644 index 0000000000..6073c9868d --- /dev/null +++ b/Telegram-Mac/PassportSettingsHeader.swift @@ -0,0 +1,13 @@ +// +// PassportSettingsHeader.swift +// Telegram +// +// Created by keepcoder on 12/06/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa + +class PassportSettingsHeaderItem: TableRowItem { + +} diff --git a/Telegram-Mac/PassportSettingsHeaderItem.swift b/Telegram-Mac/PassportSettingsHeaderItem.swift new file mode 100644 index 0000000000..80c405f0e1 --- /dev/null +++ b/Telegram-Mac/PassportSettingsHeaderItem.swift @@ -0,0 +1,42 @@ +// +// PassportSettingsHeader.swift +// Telegram +// +// Created by keepcoder on 12/06/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +class PassportSettingsHeaderItem: GeneralRowItem { + + init(_ initialSize: NSSize, stableId: AnyHashable) { + super.init(initialSize, height: theme.icons.passportSettings.backingSize.height, stableId: stableId) + } + + override func viewClass() -> AnyClass { + return PassportSettingsHeaderItemView.self + } +} + +private final class PassportSettingsHeaderItemView : TableRowView { + private let imageView: ImageView = ImageView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(imageView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + imageView.image = theme.icons.passportSettings + imageView.sizeToFit() + imageView.center() + } + + +} diff --git a/Telegram-Mac/PassportTwoStepVerificationIntroItem.swift b/Telegram-Mac/PassportTwoStepVerificationIntroItem.swift new file mode 100644 index 0000000000..537017582a --- /dev/null +++ b/Telegram-Mac/PassportTwoStepVerificationIntroItem.swift @@ -0,0 +1,104 @@ +// +// SecureIdTwoStepVerificationIntroItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 22/03/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import Postbox +import TelegramCore + + +class PassportTwoStepVerificationIntroItem: GeneralRowItem { + + fileprivate let headerLayout:TextViewLayout + fileprivate let descLayout:TextViewLayout + init(_ initialSize: NSSize, stableId: AnyHashable, peer: Peer, action: @escaping()->Void) { + let headerAttr = NSMutableAttributedString() + _ = headerAttr.append(string: L10n.secureIdCreatePasswordIntroHeader(peer.displayTitle), color: theme.colors.grayText, font: .normal(.text)) + headerAttr.detectBoldColorInString(with: .medium(.text)) + headerLayout = TextViewLayout(headerAttr, alignment: .center) + + descLayout = TextViewLayout(.initialize(string: L10n.secureIdCreatePasswordIntro, color: theme.colors.grayText, font: .normal(.text)), alignment: .center) + + super.init(initialSize, stableId: stableId, action: action) + _ = makeSize(initialSize.width, oldWidth: 0) + } + + override var height: CGFloat { + return headerLayout.layoutSize.height + (theme.icons.twoStepVerificationCreateIntro.backingSize.height + 40) + descLayout.layoutSize.height + 20 + 20 + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) + headerLayout.measure(width: width - inset.left - inset.right) + descLayout.measure(width: width - inset.left - inset.right) + return success + } + + override func viewClass() -> AnyClass { + return PassportTwoStepVerificationIntroRowView.self + } + +} + +private final class PassportTwoStepVerificationIntroRowView : TableRowView { + private let headerView: TextView = TextView() + private let imageView: ImageView = ImageView() + private let descView: TextView = TextView() + private let button: TitleButton = TitleButton() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(headerView) + addSubview(imageView) + addSubview(descView) + addSubview(button) + + descView.isSelectable = false + descView.isSelectable = false + button.set(font: .normal(.title), for: .Normal) + + + button.set(handler: { [weak self] _ in + (self?.item as? GeneralRowItem)?.action() + }, for: .Click) + } + + override func layout() { + super.layout() + headerView.centerX(y: 0) + imageView.centerX(y: headerView.frame.maxY + 20) + descView.centerX(y: imageView.frame.maxY + 20) + button.centerX(y: descView.frame.maxY + 20) + } + + + override func updateColors() { + super.updateColors() + button.set(color: theme.colors.accent, for: .Normal) + headerView.backgroundColor = theme.colors.background + descView.backgroundColor = theme.colors.background + button.set(background: theme.colors.background, for: .Normal) + imageView.image = theme.icons.twoStepVerificationCreateIntro + imageView.sizeToFit() + } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + + guard let item = item as? PassportTwoStepVerificationIntroItem else {return} + + button.set(text: L10n.secureIdRequestCreatePassword, for: .Normal) + _ = button.sizeToFit() + headerView.update(item.headerLayout) + descView.update(item.descLayout) + needsLayout = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/PassportWindowController.swift b/Telegram-Mac/PassportWindowController.swift new file mode 100644 index 0000000000..7f50ace480 --- /dev/null +++ b/Telegram-Mac/PassportWindowController.swift @@ -0,0 +1,118 @@ +// +// PasswordWindow.swift +// Telegram +// +// Created by Mikhail Filimonov on 26/03/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + +private final class PassportWindowArguments { + let back:()->Void + init(back:@escaping()->Void) { + self.back = back + } +} + + +private(set) var passport: PassportWindowController? = nil + + +class PassportWindowController { + let window: Window + let controller: PassportController + let navigationController: NavigationViewController + init(context: AccountContext, peer: Peer, request: inAppSecureIdRequest, form: EncryptedSecureIdForm) { + + + let screen = NSScreen.main! + let size = NSMakeSize(390, 600) + let center = NSMakeRect(floorToScreenPixels(System.backingScale, (screen.frame.width - size.width)/2), floorToScreenPixels(System.backingScale, (screen.frame.height - size.height)/2), size.width, size.height) + + + + window = Window(contentRect: center, styleMask: [.closable, .resizable, .miniaturizable, .fullSizeContentView, .titled, .unifiedTitleAndToolbar, .texturedBackground], backing: .buffered, defer: true) + + + + controller = PassportController(context, peer, request: request, form) + navigationController = NavigationViewController(controller, window) + + + window.isMovableByWindowBackground = true + window.name = "Telegram.PassportWindow" + //window.initSaver() + navigationController._frameRect = NSMakeRect(0, 0, size.width, size.height - 50) + window.titlebarAppearsTransparent = true + window.minSize = size + window.maxSize = size + window.contentView = navigationController.view + + window.closeInterceptor = { [weak self] in + guard let `self` = self else {return true} + self.window.orderOut(nil) + passport = nil + return true + } + (navigationController.view as? View)?.customHandler.layout = { [weak self] _ in + self?.windowDidNeedSaveState(Notification(name: Notification.Name(rawValue: ""))) + } + + windowDidNeedSaveState(Notification(name: Notification.Name(rawValue: ""))) + + if let titleView = window.titleView { + NotificationCenter.default.addObserver(self, selector: #selector(windowDidNeedSaveState(_:)), name: NSView.frameDidChangeNotification, object: titleView) + } + + navigationController.viewDidAppear(false) + + navigationController.doSomethingOnEmptyBack = { [weak self] in + guard let `self` = self else {return} + self.window.orderOut(nil) + passport = nil + } + + controller.viewDidAppear(false) + } + + var barHeight: CGFloat { + return 50 + } + + + @objc func windowDidNeedSaveState(_ notification: Notification) { + if let titleView = window.titleView { + let frame = NSMakeRect(0, window.frame.height - barHeight, titleView.frame.width, barHeight) + if !NSEqualRects(frame, titleView.frame) { + titleView.frame = frame + } + if let controls = (HackUtils.findElements(byClass: "NSTitlebarView", in: titleView)?.first as? NSView)?.subviews { + var xs:[CGFloat] = [18, 58, 38] + for i in 0 ..< min(controls.count, xs.count) { + let view = controls[i] + view.isHidden = true + view.setFrameOrigin(xs[i], floorToScreenPixels(System.backingScale, (barHeight - view.frame.height)/2)) + } + } + } + } + + deinit { + NotificationCenter.default.removeObserver(self) + window.orderOut(nil) + if let titleView = window.titleView { + NotificationCenter.default.removeObserver(titleView) + } + } + + func show() { + passport = self + window.makeKeyAndOrderFront(nil) + } +} diff --git a/Telegram-Mac/PaymentWebInteractionController.swift b/Telegram-Mac/PaymentWebInteractionController.swift new file mode 100644 index 0000000000..10554225c3 --- /dev/null +++ b/Telegram-Mac/PaymentWebInteractionController.swift @@ -0,0 +1,176 @@ +// +// PaymentWebInteractionController.swift +// Telegram +// +// Created by Mikhail Filimonov on 26.02.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + + +import Cocoa +import TGUIKit +import SwiftSignalKit +import WebKit + +private final class Arguments { + let context: AccountContext + init(context: AccountContext) { + self.context = context + } +} +enum PaymentWebInteractionIntent { + case addPaymentMethod((BotCheckoutPaymentWebToken) -> Void) + case externalVerification((Bool) -> Void) +} + +private class WeakPaymentScriptMessageHandler: NSObject, WKScriptMessageHandler { + private let f: (WKScriptMessage) -> () + + init(_ f: @escaping (WKScriptMessage) -> ()) { + self.f = f + + super.init() + } + + func userContentController(_ controller: WKUserContentController, didReceive scriptMessage: WKScriptMessage) { + self.f(scriptMessage) + } +} + + + +final class PaymentWebInteractionController: ModalViewController, WKNavigationDelegate { + private let url: String + private let intent: PaymentWebInteractionIntent + init(context: AccountContext, url: String, intent: PaymentWebInteractionIntent) { + self.url = url + self.intent = intent + super.init(frame: NSMakeRect(0, 0, 380, 440)) + } + + override func viewDidLoad() { + super.viewDidLoad() + readyOnce() + } + + override var defaultBarTitle: String { + switch intent { + case .addPaymentMethod: + return L10n.checkoutNewCardTitle + case .externalVerification: + return L10n.checkoutWebConfirmationTitle + } + } + + + override var modalHeader: (left: ModalHeaderData?, center: ModalHeaderData?, right: ModalHeaderData?)? { + return (left: ModalHeaderData(image: theme.icons.modalClose, handler: { [weak self] in + self?.close() + }), center: ModalHeaderData(title: defaultBarTitle), right: nil) + } + override var dynamicSize: Bool { + return true + } + + override func measure(size: NSSize) { + self.modal?.resize(with:NSMakeSize(min(max(380, size.width - 20), 500), size.height - 70), animated: false) + } + + override func initializer() -> NSView { + let webView: WKWebView + switch intent { + case .addPaymentMethod: + let js = "var TelegramWebviewProxyProto = function() {}; " + + "TelegramWebviewProxyProto.prototype.postEvent = function(eventName, eventData) { " + + "window.webkit.messageHandlers.performAction.postMessage({'eventName': eventName, 'eventData': eventData}); " + + "}; " + + "var TelegramWebviewProxy = new TelegramWebviewProxyProto();" + + let configuration = WKWebViewConfiguration() + let userController = WKUserContentController() + + let userScript = WKUserScript(source: js, injectionTime: .atDocumentStart, forMainFrameOnly: false) + userController.addUserScript(userScript) + + userController.add(WeakPaymentScriptMessageHandler { [weak self] message in + if let strongSelf = self { + strongSelf.handleScriptMessage(message) + } + }, name: "performAction") + + configuration.userContentController = userController + webView = WKWebView(frame: CGRect(), configuration: configuration) + webView.allowsLinkPreview = false + + case .externalVerification: + webView = WKWebView() + webView.allowsLinkPreview = false + webView.navigationDelegate = self + } + if let parsedUrl = URL(string: url) { + webView.load(URLRequest(url: parsedUrl)) + } + webView.frame = NSMakeRect(0, 0, _frameRect.width, _frameRect.height - 50) + return webView + } + + private func handleScriptMessage(_ message: WKScriptMessage) { + guard let body = message.body as? [String: Any] else { + return + } + + guard let eventName = body["eventName"] as? String else { + return + } + + if eventName == "payment_form_submit" { + guard let eventString = body["eventData"] as? String else { + return + } + + guard let eventData = eventString.data(using: .utf8) else { + return + } + + guard let dict = (try? JSONSerialization.jsonObject(with: eventData, options: [])) as? [String: Any] else { + return + } + + guard let title = dict["title"] as? String else { + return + } + + guard let credentials = dict["credentials"] else { + return + } + + guard let credentialsData = try? JSONSerialization.data(withJSONObject: credentials, options: []) else { + return + } + + guard let credentialsString = String(data: credentialsData, encoding: .utf8) else { + return + } + + if case let .addPaymentMethod(completion) = self.intent { + completion(BotCheckoutPaymentWebToken(title: title, data: credentialsString, saveOnServer: false)) + close() + } + } + } + + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + if case let .externalVerification(completion) = self.intent, let host = navigationAction.request.url?.host { + if host == "t.me" || host == "telegram.me" { + decisionHandler(.cancel) + completion(true) + close() + } else { + decisionHandler(.allow) + } + } else { + decisionHandler(.allow) + } + } + +} diff --git a/Telegram-Mac/PaymentsCheckoutController.swift b/Telegram-Mac/PaymentsCheckoutController.swift new file mode 100644 index 0000000000..0589de9bea --- /dev/null +++ b/Telegram-Mac/PaymentsCheckoutController.swift @@ -0,0 +1,708 @@ +// +// PaymentsCheckoutController.swift +// Telegram +// +// Created by Mikhail Filimonov on 25.02.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + + +import Cocoa +import TGUIKit +import SwiftSignalKit +import TelegramCore + +import Postbox + +enum PaymentProvider : Equatable { + case stripe + case smartglocal +} + +func parseRequestedPaymentMethod(paymentForm: BotPaymentForm?) -> (String, PaymentsPaymentMethodAdditionalFields, PaymentProvider)? { + + if let paymentForm = paymentForm, let nativeProvider = paymentForm.nativeProvider, nativeProvider.name == "stripe" { + + guard let paramsData = nativeProvider.params.data(using: .utf8) else { + return nil + } + guard let nativeParams = (try? JSONSerialization.jsonObject(with: paramsData)) as? [String: Any] else { + return nil + } + guard let publishableKey = nativeParams["publishable_key"] as? String else { + return nil + } + + var additionalFields: PaymentsPaymentMethodAdditionalFields = [] + if let needCardholderName = nativeParams["need_cardholder_name"] as? NSNumber, needCardholderName.boolValue { + additionalFields.insert(.cardholderName) + } + if let needCountry = nativeParams["need_country"] as? NSNumber, needCountry.boolValue { + additionalFields.insert(.country) + } + if let needZip = nativeParams["need_zip"] as? NSNumber, needZip.boolValue { + additionalFields.insert(.zipCode) + } + + return (publishableKey, additionalFields, .stripe) + } else if let paymentForm = paymentForm, let nativeProvider = paymentForm.nativeProvider, nativeProvider.name == "smartglocal" { + guard let paramsData = nativeProvider.params.data(using: .utf8) else { + return nil + } + guard let nativeParams = (try? JSONSerialization.jsonObject(with: paramsData)) as? [String: Any] else { + return nil + } + guard let publishableKey = nativeParams["public_token"] as? String else { + return nil + } + + return (publishableKey, [], .smartglocal) + } + return nil +} + + + +private func currentTotalPrice(paymentForm: BotPaymentForm?, validatedFormInfo: BotPaymentValidatedFormInfo?, shippingOption: BotPaymentShippingOption?, tip:Int64? = nil) -> Int64 { + guard let paymentForm = paymentForm else { + return 0 + } + + var totalPrice: Int64 = 0 + + for price in paymentForm.invoice.prices { + totalPrice += price.amount + } + + if let option = shippingOption { + for price in option.prices { + totalPrice += price.amount + } + } + if let tip = tip { + totalPrice += tip + } + + return totalPrice +} + + +struct BotCheckoutPaymentWebToken: Equatable { + let title: String + let data: String + var saveOnServer: Bool +} + +enum BotCheckoutPaymentMethod: Equatable { + case savedCredentials(BotPaymentSavedCredentials) + case webToken(BotCheckoutPaymentWebToken) + case applePay + + var title: String { + switch self { + case let .savedCredentials(credentials): + switch credentials { + case let .card(_, title): + return title + } + case let .webToken(token): + return token.title + case .applePay: + return "Apple Pay" + } + } +} + + +private final class Arguments { + let context: AccountContext + let openForm:(PaymentsShippingInfoFocus?)->Void + let openShippingMethod:()->Void + let openPaymentMethod:()->Void + let selectTip:(Int64?)->Void + let pay:(TemporaryTwoStepPasswordToken?)->Void + init(context: AccountContext, openForm:@escaping(PaymentsShippingInfoFocus?)->Void, openShippingMethod:@escaping()->Void, openPaymentMethod:@escaping()->Void, pay:@escaping(TemporaryTwoStepPasswordToken?)->Void, selectTip:@escaping(Int64?)->Void) { + self.context = context + self.openForm = openForm + self.openShippingMethod = openShippingMethod + self.openPaymentMethod = openPaymentMethod + self.pay = pay + self.selectTip = selectTip + } +} + +enum PaymentViewMode { + case receipt + case invoice +} + +private struct State : Equatable { + let mode: PaymentViewMode + var message: Message + var form: BotPaymentForm? + var botPeer: PeerEquatable? + var validatedInfo: BotPaymentValidatedFormInfo? + var savedInfo: BotPaymentRequestedInfo + var shippingOptionId: BotPaymentShippingOption? + var paymentMethod: BotCheckoutPaymentMethod? + var currentTip: Int64? + var unfilledInfo: PaymentsShippingInfoFocus? { + if let form = form { + if form.invoice.requestedFields.contains(.shippingAddress) { + if savedInfo.shippingAddress == nil { + return .address + } + } + if form.invoice.requestedFields.contains(.phone) { + if savedInfo.phone == nil { + return .phone + } + } + if form.invoice.requestedFields.contains(.email) { + if savedInfo.email == nil { + return .email + } + } + if form.invoice.requestedFields.contains(.name) { + if savedInfo.name == nil { + return .name + } + } + } + return nil + } +} + +private let _id_checkout_preview = InputDataIdentifier("_id_checkout_preview") +private let _id_checkout_loading = InputDataIdentifier("_id_checkout_loading") +private func _id_checkout_price(_ label: String, index: Int) -> InputDataIdentifier { + return InputDataIdentifier("_id_checkout_price_\(label)_\(index)") +} +private let _id_checkout_payment_method = InputDataIdentifier("_id_checkout_payment_method") +private let _id_checkout_shipping_info = InputDataIdentifier("_id_checkout_shipping_info") +private let _id_checkout_name = InputDataIdentifier("_id_checkout_name") +private let _id_checkout_flex_shipping = InputDataIdentifier("_id_checkout_flex_shipping") +private let _id_checkout_phone_number = InputDataIdentifier("_id_checkout_phone_number") +private let _id_checkout_email = InputDataIdentifier("_id_checkout_email") + +private let _id_tips = InputDataIdentifier("_id_tips") + +private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_checkout_preview, equatable: InputDataEquatable(state.botPeer), comparable: nil, item: { initialSize, stableId in + return PaymentsCheckoutPreviewRowItem(initialSize, stableId: stableId, context: arguments.context, message: state.message, botPeer: state.botPeer?.peer, viewType: .singleItem) + })) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + if let form = state.form { + +// entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.checkoutPriceHeader), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) +// index += 1 + let insets = NSEdgeInsets(top: 7, left: 16, bottom: 7, right: 16) + let first = NSEdgeInsets(top: 14, left: 16, bottom: 7, right: 16) + let last = NSEdgeInsets(top: 7, left: 16, bottom: 14, right: 16) + + struct Tuple: Equatable { + let label: String + let price: String + let viewType: GeneralViewType + let editableTip: PaymentsCheckoutPriceItem.EditableTip? + } + + + + var prices = form.invoice.prices + if let shippingOption = state.shippingOptionId { + prices += shippingOption.prices + } + + if let _ = form.invoice.tip { + prices.append(BotPaymentPrice(label: L10n.paymentsTipLabel, amount: state.currentTip ?? 0)) + } + + for (i, price) in prices.enumerated() { + var viewType = bestGeneralViewType(prices, for: i) + + if i == 0 { + viewType = viewType.withUpdatedInsets(first) + } else { + viewType = viewType.withUpdatedInsets(insets) + } + if price == prices.last { + if prices.count > 1 { + viewType = GeneralViewType.innerItem.withUpdatedInsets(insets) + } else { + viewType = GeneralViewType.firstItem.withUpdatedInsets(insets) + } + } + + let editableTip: PaymentsCheckoutPriceItem.EditableTip? + + if price.label == L10n.paymentsTipLabel, let tip = form.invoice.tip { + editableTip = PaymentsCheckoutPriceItem.EditableTip(currency: form.invoice.currency, current: state.currentTip ?? 0, maxValue: tip.max) + } else { + editableTip = nil + } + + let tuple = Tuple(label:price.label, price: formatCurrencyAmount(price.amount, currency: form.invoice.currency), viewType: viewType, editableTip: editableTip) + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_checkout_price(price.label, index: i), equatable: InputDataEquatable(tuple), comparable: nil, item: { initialSize, stableId in + return PaymentsCheckoutPriceItem(initialSize, stableId: stableId, title: tuple.label, price: tuple.price, font: .normal(.text), color: theme.colors.grayText, viewType: tuple.viewType, editableTip: editableTip, updateValue: arguments.selectTip) + })) + index += 1 + } + + + if let tip = form.invoice.tip, !prices.isEmpty { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_tips, equatable: InputDataEquatable(state), comparable: nil, item: { initialSize, stableId in + return PaymentsTipsRowItem(initialSize, stableId: stableId, viewType: .innerItem, currency: form.invoice.currency, tips: tip, current: state.currentTip, select: arguments.selectTip) + })) + index += 1 + } + + if !prices.isEmpty { + let viewType = GeneralViewType.lastItem.withUpdatedInsets(last) + + let tuple = Tuple(label: L10n.checkoutTotalAmount, price: formatCurrencyAmount(prices.reduce(0, { $0 + $1.amount}), currency: form.invoice.currency), viewType: viewType, editableTip: nil) + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_checkout_price(L10n.checkoutTotalAmount, index: .max), equatable: InputDataEquatable(tuple), comparable: nil, item: { initialSize, stableId in + return PaymentsCheckoutPriceItem(initialSize, stableId: stableId, title: tuple.label, price: tuple.price, font: .medium(.text), color: theme.colors.text, viewType: tuple.viewType) + })) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + } + var paymentMethodTitle = "" + if let paymentMethod = state.paymentMethod { + paymentMethodTitle = paymentMethod.title + } + + var fields = form.invoice.requestedFields.intersection([.shippingAddress, .email, .name, .phone]) + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_checkout_payment_method, data: .init(name: L10n.checkoutPaymentMethod, color: theme.colors.text, type: .nextContext(paymentMethodTitle), viewType: fields.isEmpty ? .singleItem : .firstItem, action: arguments.openPaymentMethod))) + index += 1 + + let savedInfo = state.savedInfo + + + var updated = fields.subtracting(.shippingAddress) + if updated != fields { + fields = updated + var addressString = "" + if let address = savedInfo.shippingAddress { + let components: [String] = [ + address.city, + address.streetLine1, + address.streetLine2, + address.state + ] + for component in components { + if !component.isEmpty { + if !addressString.isEmpty { + addressString.append(", ") + } + addressString.append(component) + } + } + } + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_checkout_shipping_info, data: .init(name: L10n.checkoutShippingAddress, color: theme.colors.text, type: .nextContext(addressString), viewType: fields.isEmpty && state.validatedInfo?.shippingOptions == nil ? .lastItem : .innerItem, action: { + arguments.openForm(.address) + }))) + index += 1 + } + + if let _ = state.validatedInfo?.shippingOptions { + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_checkout_flex_shipping, data: .init(name: L10n.checkoutShippingMethod, color: theme.colors.text, type: .nextContext(state.shippingOptionId?.title ?? ""), viewType: fields.isEmpty ? .lastItem : .innerItem, action: arguments.openShippingMethod))) + index += 1 + } + + updated = fields.subtracting(.name) + if updated != fields { + fields = updated + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_checkout_name, data: .init(name: L10n.checkoutName, color: theme.colors.text, type: .nextContext(savedInfo.name ?? ""), viewType: fields.isEmpty ? .lastItem : .innerItem, action: { + arguments.openForm(.name) + }))) + index += 1 + } + + updated = fields.subtracting(.email) + if updated != fields { + fields = updated + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_checkout_email, data: .init(name: L10n.checkoutEmail, color: theme.colors.text, type: .nextContext(savedInfo.email ?? ""), viewType: fields.isEmpty ? .lastItem : .innerItem, action: { + arguments.openForm(.email) + }))) + index += 1 + } + + updated = fields.subtracting(.phone) + if updated != fields { + fields = updated + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_checkout_phone_number, data: .init(name: L10n.checkoutPhone, color: theme.colors.text, type: .nextContext(formatPhoneNumber(savedInfo.phone ?? "")), viewType: fields.isEmpty ? .lastItem : .innerItem, action: { + arguments.openForm(.phone) + }))) + index += 1 + } + + } else { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_checkout_loading, equatable: nil, comparable: nil, item: { initialSize, stableId in + return GeneralLoadingRowItem(initialSize, stableId: stableId, viewType: .singleItem) + })) + index += 1 + } + + // entries + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func PaymentsCheckoutController(context: AccountContext, message: Message) -> InputDataModalController { + + var close:(()->Void)? = nil + let messageId = message.id + let actionsDisposable = DisposableSet() + + let initialState = State(mode: .invoice, message: message, savedInfo: BotPaymentRequestedInfo(name: nil, phone: nil, email: nil, shippingAddress: nil)) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let arguments = Arguments(context: context, openForm: { focus in + let state = stateValue.with({ $0 }) + if let form = state.form { + showModal(with: PaymentsShippingInfoController(context: context, invoice: form.invoice, messageId: messageId, formInfo: state.savedInfo, focus: focus, formInfoUpdated: { savedInfo, validatedInfo in + updateState { current in + var current = current + current.savedInfo = savedInfo + current.validatedInfo = validatedInfo + return current + } + }), for: context.window) + } + }, openShippingMethod: { + let state = stateValue.with({ $0 }) + if let form = state.form, let options = state.validatedInfo?.shippingOptions { + showModal(with: PaymentsShippingMethodController(context: context, shippingOptions: options, form: form, select: { id in + updateState { current in + var current = current + current.shippingOptionId = id + return current + } + }), for: context.window) + } + }, openPaymentMethod: { + if let form = stateValue.with({ $0.form }), let value = parseRequestedPaymentMethod(paymentForm: form) { + let addPayment:()->Void = { + showModal(with: PaymentsPaymentMethodController(context: context, fields: value.1, publishableKey: value.0, passwordMissing: form.passwordMissing, isTesting: form.invoice.isTest, provider: value.2, completion: { method in + updateState { current in + var current = current + current.paymentMethod = method + return current + } + }), for: context.window) + } + + if let savedCredentials = form.savedCredentials { + showModal(with: PamentsSelectMethodController(context: context, cards: [savedCredentials], form: form, select: { selected in + updateState { current in + var current = current + current.paymentMethod = .savedCredentials(selected) + return current + } + }, addNew: addPayment), for: context.window) + + } else { + addPayment() + } + + } else if let paymentForm = stateValue.with({ $0.form }) { + showModal(with: PaymentWebInteractionController(context: context, url: paymentForm.url, intent: .addPaymentMethod({ token in + updateState { current in + var current = current + current.paymentMethod = .webToken(token) + return current + } + })), for: context.window) + } + }, pay: { savedCredentialsToken in + guard let paymentMethod = stateValue.with ({ $0.paymentMethod }) else { + return + } + let state = stateValue.with { $0 } + + let pay:(BotPaymentCredentials)->Void = { credentials in + + guard let form = state.form else { + return + } + + let pay:()->Void = { + let paySignal = context.engine.payments.sendBotPaymentForm(messageId: messageId, formId: form.id, validatedInfoId: state.validatedInfo?.id, shippingOptionId: state.shippingOptionId?.id, tipAmount: state.form?.invoice.tip != nil ? (state.currentTip ?? 0) : nil, credentials: credentials) + + _ = showModalProgress(signal: paySignal, for: context.window).start(next: { result in + + let success:(Bool)->Void = { value in + if value { + close?() + let invoice = state.message.media.first as! TelegramMediaInvoice + //currencyValue: currencyValue, itemTitle: invoice.title + let totalValue = currentTotalPrice(paymentForm: form, validatedFormInfo: state.validatedInfo, shippingOption: state.shippingOptionId) + + let total = formatCurrencyAmount(totalValue, currency: form.invoice.currency) + + showModalText(for: context.window, text: L10n.paymentsPaid(total, invoice.title)) + } + } + switch result { + case .done: + success(true) + case let .externalVerificationRequired(url: url): + showModal(with: PaymentWebInteractionController(context: context, url: url, intent: .externalVerification(success)), for: context.window) + } + }, error: { error in + let text: String + switch error { + case .alreadyPaid: + text = L10n.checkoutErrorInvoiceAlreadyPaid + case .generic: + text = L10n.unknownError + case .paymentFailed: + text = L10n.checkoutErrorPaymentFailed + case .precheckoutFailed: + text = L10n.checkoutErrorPrecheckoutFailed + } + alert(for: context.window, info: text) + close?() + }) + } + + let messageId = message.id + let botPeer: Signal = context.account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(form.paymentBotId) + } + + let checkSignal = combineLatest(queue: .mainQueue(), ApplicationSpecificNotice.getBotPaymentLiability(accountManager: context.sharedContext.accountManager, peerId: messageId.peerId), botPeer, context.account.postbox.loadedPeerWithId(form.providerId)) + + let _ = checkSignal.start(next: { value, botPeer, providerPeer in + if let botPeer = botPeer { + if value { + pay() + } else { + confirm(for: context.window, header: L10n.paymentsWarninTitle, information: L10n.paymentsWarningText(botPeer.compactDisplayTitle, botPeer.compactDisplayTitle, botPeer.compactDisplayTitle, botPeer.compactDisplayTitle), successHandler: { _ in + pay() + _ = ApplicationSpecificNotice.setBotPaymentLiability(accountManager: context.sharedContext.accountManager, peerId: messageId.peerId).start() + }) + } + } + }) + + } + + switch paymentMethod { + case let .savedCredentials(savedCredentials): + switch savedCredentials { + case let .card(id, title): + if let savedCredentialsToken = savedCredentialsToken { + pay(.saved(id: id, tempPassword: savedCredentialsToken.token)) + } else { + let _ = (context.engine.auth.cachedTwoStepPasswordToken() + |> deliverOnMainQueue).start(next: { token in + let timestamp = context.account.network.getApproximateRemoteTimestamp() + if let token = token, token.validUntilDate > timestamp - 1 * 60 { + pay(.saved(id: id, tempPassword: token.token)) + } else { + showModal(with: InputPasswordController(context: context, title: L10n.checkoutPasswordEntryTitle, desc: L10n.checkoutPasswordEntryText(title), checker: { password in + Signal { subscriber in + let checker = context.engine.auth.requestTemporaryTwoStepPasswordToken(password: password, period: 1 * 60, requiresBiometrics: false) |> deliverOnMainQueue + return checker.start(next: { token in + pay(.saved(id: id, tempPassword: token.token)) + subscriber.putCompletion() + }, error: { error in + switch error { + case .invalidPassword: + subscriber.putError(.wrong) + default: + subscriber.putError(.generic) + } + }) + } + }), for: context.window) + } + }) + } + } + case let .webToken(token): + pay(.generic(data: token.data, saveOnServer: token.saveOnServer)) + default: + alert(for: context.window, info: "Unsupported") + return + } + }, selectTip: { value in + updateState { current in + var current = current + if let value = value { + current.currentTip = min(value, current.form?.invoice.tip?.max ?? .max) + } else { + current.currentTip = nil + } + return current + } + }) + + let signal = statePromise.get() |> deliverOnPrepareQueue |> map { state in + return InputDataSignalValue(entries: entries(state, arguments: arguments)) + } + + let controller = InputDataController(dataSignal: signal, title: L10n.checkoutTitle) + + + let themeParams: [String: Any] = [ + "bg_color": Int32(bitPattern: theme.colors.background.argb), + "text_color": Int32(bitPattern: theme.colors.text.argb), + "link_color": Int32(bitPattern: theme.colors.link.argb), + "button_color": Int32(bitPattern: theme.colors.accent.argb), + "button_text_color": Int32(bitPattern: theme.colors.underSelectedColor.argb) + ] + + + let formAndMaybeValidatedInfo = context.engine.payments.fetchBotPaymentForm(messageId: messageId, themeParams: themeParams) + |> mapToSignal { paymentForm -> Signal<(BotPaymentForm, BotPaymentValidatedFormInfo?), BotPaymentFormRequestError> in + if let current = paymentForm.savedInfo { + return context.engine.payments.validateBotPaymentForm(saveInfo: true, messageId: messageId, formInfo: current) + |> mapError { _ -> BotPaymentFormRequestError in + return .generic + } + |> map { result -> (BotPaymentForm, BotPaymentValidatedFormInfo?) in + return (paymentForm, result) + } + |> `catch` { _ -> Signal<(BotPaymentForm, BotPaymentValidatedFormInfo?), BotPaymentFormRequestError> in + return .single((paymentForm, nil)) + } + } else { + return .single((paymentForm, nil)) + } + } |> deliverOnMainQueue + + let formPromise: Promise<(BotPaymentForm, BotPaymentValidatedFormInfo?)> = Promise() + + formPromise.set(formAndMaybeValidatedInfo |> `catch` { _ in .complete() }) + + + let botPeer: Signal = formPromise.get() |> mapToSignal { value in + return context.account.postbox.transaction { + $0.getPeer(value.0.paymentBotId) + } + } |> castError(BotPaymentFormRequestError.self) + + actionsDisposable.add(combineLatest(formAndMaybeValidatedInfo, botPeer).start(next: { form, botPeer in + updateState { current in + var current = current + current.form = form.0 + current.botPeer = botPeer != nil ? PeerEquatable(botPeer!) : nil + current.validatedInfo = form.1 + if let savedInfo = form.0.savedInfo { + current.savedInfo = savedInfo + } + if let savedCredentials = form.0.savedCredentials { + current.paymentMethod = .savedCredentials(savedCredentials) + } + + return current + } + }, error: { error in + close?() + switch error { + case .generic: + alert(for: context.window, info: L10n.unknownError) + } + })) + + + controller.onDeinit = { + actionsDisposable.dispose() + } + + controller.validateData = { _ in + + let state = stateValue.with ({ $0 }) + + if let focus = state.unfilledInfo { + return .fail(.doSomething(next: { _ in + arguments.openForm(focus) + })) + } + + if state.validatedInfo?.shippingOptions != nil && state.shippingOptionId == nil { + return .fail(.doSomething(next: { _ in + arguments.openShippingMethod() + })) + } + + if state.paymentMethod == nil { + return .fail(.doSomething(next: { _ in + arguments.openPaymentMethod() + })) + } + + return .fail(.doSomething(next: { _ in + arguments.pay(nil) + })) + } + + let modalInteractions = ModalInteractions(acceptTitle: L10n.checkoutPayNone, accept: { [weak controller] in + _ = controller?.returnKeyAction() + }, drawBorder: true, height: 50, singleButton: true) + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) + + close = { [weak modalController] in + modalController?.modal?.close() + } + + controller.afterTransaction = { [weak modalInteractions] controller in + modalInteractions?.updateDone { button in + button.isEnabled = stateValue.with { $0.form != nil } + let state = stateValue.with ({ $0 }) + let text: String + if let form = state.form { + let totalAmount = formatCurrencyAmount(currentTotalPrice(paymentForm: form, validatedFormInfo: state.validatedInfo, shippingOption: state.shippingOptionId, tip: state.currentTip), currency: form.invoice.currency) + text = L10n.checkoutPayPrice("\(totalAmount)") + } else { + text = L10n.checkoutPayNone + } + button.set(text: text, for: .Normal) + } + if stateValue.with ({ $0.form != nil }) { + DispatchQueue.main.async { [weak controller] in + controller?.window?.applyResponderIfNeeded() + } + } + } + + return modalController + +} + diff --git a/Telegram-Mac/PaymentsCheckoutPreviewRowItem.swift b/Telegram-Mac/PaymentsCheckoutPreviewRowItem.swift new file mode 100644 index 0000000000..3b07d49279 --- /dev/null +++ b/Telegram-Mac/PaymentsCheckoutPreviewRowItem.swift @@ -0,0 +1,128 @@ +// +// PaymentsCheckoutPreviewRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 25.02.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TelegramCore + +import SwiftSignalKit +import Postbox +import TGUIKit + +final class PaymentsCheckoutPreviewRowItem : GeneralRowItem { + fileprivate let invoice: TelegramMediaInvoice + fileprivate let textLayout: TextViewLayout + fileprivate let image: TelegramMediaWebFile? + fileprivate let context: AccountContext + init(_ initialSize: NSSize, stableId: AnyHashable, context: AccountContext, message: Message, botPeer: Peer?, viewType: GeneralViewType) { + self.invoice = message.media.first as! TelegramMediaInvoice + self.context = context + self.image = invoice.photo + + let attr = NSMutableAttributedString() + _ = attr.append(string: invoice.title, color: theme.colors.text, font: .medium(.text)) + _ = attr.append(string: "\n") + _ = attr.append(string: invoice.description, color: theme.colors.text, font: .normal(.text)) + _ = attr.append(string: "\n") + _ = attr.append(string: botPeer?.displayTitle ?? "", color: theme.colors.grayText, font: .normal(.text)) + + self.textLayout = TextViewLayout(attr) + super.init(initialSize, viewType: viewType) + } + + private var contentHeight: CGFloat = 0 + fileprivate private(set) var imageSize: NSSize = .zero + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + + var height: CGFloat = 0 + if let image = image { + let imageSize = image.dimensions?.size ?? NSMakeSize(200, 200) + let halfWidth = blockWidth / 2 - viewType.innerInset.right - viewType.innerInset.left - viewType.innerInset.right + let fitted = imageSize.aspectFitted(NSMakeSize(halfWidth, halfWidth)) + self.imageSize = fitted + textLayout.measure(width: blockWidth - fitted.width - viewType.innerInset.right - viewType.innerInset.left - viewType.innerInset.right) + height = max(fitted.height, textLayout.layoutSize.height) + } else { + textLayout.measure(width: blockWidth - viewType.innerInset.left - viewType.innerInset.right) + height += textLayout.layoutSize.height + } + contentHeight = height + return true + } + + override var height: CGFloat { + return viewType.innerInset.bottom + contentHeight + viewType.innerInset.top + } + + override func viewClass() -> AnyClass { + return PaymentsCheckoutPreviewRowView.self + } +} + + +private final class PaymentsCheckoutPreviewRowView : GeneralContainableRowView { + private var imageView: TransformImageView? + private let textView: TextView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(textView) + } + + override func layout() { + super.layout() + + guard let item = item as? PaymentsCheckoutPreviewRowItem else { + return + } + if let imageView = imageView { + imageView.setFrameOrigin(NSMakePoint(item.viewType.innerInset.left, item.viewType.innerInset.top)) + textView.setFrameOrigin(NSMakePoint(imageView.frame.maxX + item.viewType.innerInset.left, item.viewType.innerInset.top)) + } else { + textView.setFrameOrigin(NSMakePoint(item.viewType.innerInset.left, item.viewType.innerInset.top)) + } + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + + guard let item = item as? PaymentsCheckoutPreviewRowItem else { + return + } + + textView.update(item.textLayout) + + if let image = item.image { + if imageView == nil { + self.imageView = TransformImageView() + addSubview(self.imageView!) + } + + self.imageView?.setFrameSize(item.imageSize) + imageView?.setSignal(chatMessageWebFilePhoto(account: item.context.account, photo: image, scale: backingScaleFactor)) + + _ = fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, reference: MediaResourceReference.standalone(resource: image.resource)).start() + + imageView?.set(arguments: TransformImageArguments(corners: .init(radius: .cornerRadius), imageSize: item.imageSize, boundingSize: item.imageSize, intrinsicInsets: .init())) + + } else { + imageView?.removeFromSuperview() + imageView = nil + } + + needsLayout = true + } + + override var firstResponder: NSResponder? { + return nil + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/PaymentsCheckoutPriceItem.swift b/Telegram-Mac/PaymentsCheckoutPriceItem.swift new file mode 100644 index 0000000000..c4e10133d2 --- /dev/null +++ b/Telegram-Mac/PaymentsCheckoutPriceItem.swift @@ -0,0 +1,218 @@ +// +// PaymentsCheckoutPriceItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 25.02.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation + + + +import Foundation +import TelegramCore + +import SwiftSignalKit +import Postbox +import TGUIKit + +final class PaymentsCheckoutPriceItem : GeneralRowItem { + + struct EditableTip : Equatable { + let currency: String + let current: Int64 + let maxValue: Int64 + } + + fileprivate let editableTip: EditableTip? + + fileprivate let titleLayout: TextViewLayout + fileprivate let priceLayout: TextViewLayout + + fileprivate let updateValue:(Int64?)->Void + + init(_ initialSize: NSSize, stableId: AnyHashable, title: String, price: String, font: NSFont, color: NSColor, viewType: GeneralViewType, editableTip: EditableTip? = nil, updateValue: @escaping(Int64?)->Void = { _ in }) { + self.updateValue = updateValue + self.editableTip = editableTip + self.titleLayout = TextViewLayout(.initialize(string: title, color: color, font: font), maximumNumberOfLines: 1) + self.priceLayout = TextViewLayout(.initialize(string: price, color: color, font: font)) + + super.init(initialSize, viewType: viewType) + } + + private var contentHeight: CGFloat = 0 + fileprivate private(set) var imageSize: NSSize = .zero + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + + + priceLayout.measure(width: blockWidth / 2 - 10 - viewType.innerInset.left - viewType.innerInset.right) + + titleLayout.measure(width: blockWidth - 10 - viewType.innerInset.left - viewType.innerInset.right - priceLayout.layoutSize.width) + + contentHeight = max(titleLayout.layoutSize.height, priceLayout.layoutSize.height) + + return true + } + + override var height: CGFloat { + return viewType.innerInset.bottom + contentHeight + viewType.innerInset.top + } + + override func viewClass() -> AnyClass { + return PaymentsCheckoutPriceView.self + } + + override var hasBorder: Bool { + return false + } +} + + +private final class PaymentsCheckoutPriceView : GeneralContainableRowView, NSTextViewDelegate { + private let title: TextView = TextView() + private let price: TextView = TextView() + private let input: NSTextView = NSTextView() + + private var formatterDelegate: CurrencyUITextFieldDelegate? + + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(title) + addSubview(price) + title.userInteractionEnabled = false + title.isSelectable = false +// input.isEditable = true +// input.isSelectable = true + + + input.font = .light(.text) + input.wantsLayer = true + input.isEditable = true + input.isSelectable = true +// input.maximumNumberOfLines = 1 + input.backgroundColor = .clear + input.drawsBackground = false + input.alignment = .right + input.textColor = theme.colors.grayText +// input.isBezeled = false +// input.isBordered = false + input.focusRingType = .none + addSubview(input) + } + + override var firstResponder: NSResponder? { + if input.isHidden { + return nil + } + return input + } + + override func layout() { + super.layout() + + guard let item = item as? PaymentsCheckoutPriceItem else { + return + } + title.setFrameOrigin(NSMakePoint(item.viewType.innerInset.left, item.viewType.innerInset.top)) + price.setFrameOrigin(NSMakePoint(item.blockWidth - item.viewType.innerInset.left - price.frame.width, item.viewType.innerInset.top)) + + input.frame = containerView.bounds.insetBy(dx: item.viewType.innerInset.right - 5, dy: item.viewType.innerInset.top) + } + + + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + + guard let item = item as? PaymentsCheckoutPriceItem else { + return + } + + + input.isHidden = item.editableTip == nil + + self.price.change(opacity: !input.string.isEmpty ? 0 : 1, animated: false) + + if let editableTip = item.editableTip { + + let text: String + if editableTip.current == 0 { + text = "" + } else { + text = formatCurrencyAmount(editableTip.current, currency: editableTip.currency) + } + + if input.string != text { + input.string = text + self.price.change(opacity: !input.string.isEmpty ? 0 : 1, animated: false) + } + + self.formatterDelegate = CurrencyUITextFieldDelegate(formatter: CurrencyFormatter(currency: editableTip.currency, { formatter in + formatter.maxValue = currencyToFractionalAmount(value: editableTip.maxValue, currency: editableTip.currency) ?? 10000.0 + formatter.minValue = 0.0 + formatter.hasDecimals = true + })) + self.formatterDelegate?.passthroughDelegate = self + + self.formatterDelegate?.textUpdated = { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.textDidChange(Notification(name: Notification.Name(""), object: strongSelf.input, userInfo: nil)) + } + +// formatterDelegate?.setFormattedText(in: self.input, inputString: "\(editableTip.current)", range: input.selectedRange()) +// self.input.string = formatterDelegate?.formatter.formattedStringWithAdjustedDecimalSeparator(from: "\(editableTip.current)") ?? "" + + } else { + self.formatterDelegate = nil + } + + self.input.delegate = self.formatterDelegate + + title.update(item.titleLayout) + price.update(item.priceLayout) + + needsLayout = true + } + + func textDidChange(_ notification: Notification) { + let text = input.string + self.price.change(opacity: !text.isEmpty ? 0 : 1, animated: false) + + guard let item = self.item as? PaymentsCheckoutPriceItem else { + return + } + + if text.isEmpty { + item.updateValue(0) + return + } + guard let editableTip = item.editableTip else { + return + } + + guard let unformatted = self.formatterDelegate?.formatter.unformatted(string: text) else { + return + } + + guard let value = Int64(unformatted) else { + return + } + + + item.updateValue(value) + if value > editableTip.maxValue { + self.input.string = self.formatterDelegate?.formatter.formattedStringAdjustedToFitAllowedValues(from: "\(editableTip.maxValue)") ?? "" + } + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/PaymentsPaymentMethodController.swift b/Telegram-Mac/PaymentsPaymentMethodController.swift new file mode 100644 index 0000000000..3078778c21 --- /dev/null +++ b/Telegram-Mac/PaymentsPaymentMethodController.swift @@ -0,0 +1,609 @@ +// +// PaymentsPaymentMethodController.swift +// Telegram +// +// Created by Mikhail Filimonov on 26.02.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + + +import Cocoa +import TGUIKit +import SwiftSignalKit +import Stripe + + + +private final class Arguments { + let context: AccountContext + let toggleSaveInfo:()->Void + let verify: ()->Void + let passwordMissing:Bool + init(context: AccountContext, toggleSaveInfo: @escaping()->Void, verify: @escaping()->Void, passwordMissing:Bool) { + self.context = context + self.toggleSaveInfo = toggleSaveInfo + self.verify = verify + self.passwordMissing = passwordMissing + } +} + + +private let _id_card_number = InputDataIdentifier("_id_card_number") +private let _id_card_date = InputDataIdentifier("_id_card_date") +private let _id_card_cvc = InputDataIdentifier("_id_card_cvc") + +private let _id_card_holder_name = InputDataIdentifier("_id_card_holder_name") + +private let _id_card_country = InputDataIdentifier("_id_card_country") +private let _id_card_zip_code = InputDataIdentifier("_id_card_zip_code") + +private let _id_card_save_info = InputDataIdentifier("_id_card_save_info") + +private struct State : Equatable { + struct Card : Equatable { + var number: String + var date: String + var cvc: String + } + struct BillingAddress : Equatable { + var country: String? + var zipCode: String? + } + var card: Card + var holderName: String? + var billingAddress: BillingAddress + + var stripe: STPCardParams { + let params = STPCardParams() + params.number = STPCardValidator.sanitizedNumericString(for: self.card.number) + if self.card.date.count == 5 { + params.expYear = UInt(self.card.date.suffix(2))! + params.expMonth = UInt(self.card.date.prefix(2))! + } + params.cvc = self.card.cvc + params.name = self.holderName + params.addressCountry = self.billingAddress.country + params.addressZip = self.billingAddress.zipCode + return params + } + + var cardError: InputDataValueError? + var nameError: InputDataValueError? + var billingError: InputDataValueError? + + var saveInfo: Bool + + var unfilledItem: InputDataIdentifier? { + + let normalized = STPCardValidator.sanitizedNumericString(for: card.number) + let brand = STPCardValidator.brand(forNumber: normalized) + let maxCardNumberLength = STPCardValidator.maxLength(for: brand) + let maxCVCLength = STPCardValidator.maxCVCLength(for: brand) + + + if normalized.length == maxCardNumberLength { + let state = STPCardValidator.validationState(forNumber: card.number, validatingCardBrand: true) + switch state { + case .invalid: + return _id_card_number + default: + break + } + } else { + return _id_card_number + } + + if card.date.length == 5 { + let yearState = STPCardValidator.validationState(forExpirationYear: String(card.date.suffix(2)), inMonth: card.date.prefix(2)) + switch yearState { + case .invalid: + return _id_card_date + default: + let monthState = STPCardValidator.validationState(forExpirationMonth: card.date.prefix(2)) + switch monthState { + case .invalid: + return _id_card_date + default: + break + } + } + } else { + return _id_card_date + } + + if card.cvc.length == maxCVCLength { + let state = STPCardValidator.validationState(forCVC: card.cvc, cardBrand: brand) + switch state { + case .invalid: + return _id_card_cvc + default: + break + } + } else { + return _id_card_cvc + } + + if let holder = self.holderName, holder.isEmpty { + return _id_card_holder_name + } + if let country = self.billingAddress.country, country.isEmpty { + return _id_card_country + } + if let zipCode = self.billingAddress.zipCode, zipCode.isEmpty { + return _id_card_zip_code + } + + return nil + } +} + +private func validateSmartGlobal(_ publicToken: String, isTesting: Bool, state: State) -> Signal { + return Signal { subscriber in + + let url: String + if isTesting { + url = "https://tgb-playground.smart-glocal.com/cds/v1/tokenize/card" + } else { + url = "https://tgb.smart-glocal.com/cds/v1/tokenize/card" + } + + let stripe = state.stripe + + let jsonPayload: [String: Any] = [ + "card": [ + "number": stripe.number ?? "", + "expiration_month": "\(state.card.date.prefix(2))", + "expiration_year": "\(state.card.date.suffix(2))", + "security_code": "\(stripe.cvc ?? "")" + ] as [String: Any] + ] + + guard let parsedUrl = URL(string: url) else { + return EmptyDisposable + } + + var request = URLRequest(url: parsedUrl) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue(publicToken, forHTTPHeaderField: "X-PUBLIC-TOKEN") + guard let requestBody = try? JSONSerialization.data(withJSONObject: jsonPayload, options: []) else { + return EmptyDisposable + } + request.httpBody = requestBody + + let session = URLSession.shared + + var cancelled: Bool = false + + let dataTask = session.dataTask(with: request, completionHandler: { data, response, error in + enum ReponseError: Error { + case generic + } + + if cancelled { + return + } else if let error = error { + subscriber.putError(error) + return + } + + do { + guard let data = data else { + throw ReponseError.generic + } + + let jsonRaw = try JSONSerialization.jsonObject(with: data, options: []) + guard let json = jsonRaw as? [String: Any] else { + throw ReponseError.generic + } + guard let resultData = json["data"] as? [String: Any] else { + throw ReponseError.generic + } + guard let resultInfo = resultData["info"] as? [String: Any] else { + throw ReponseError.generic + } + guard let token = resultData["token"] as? String else { + throw ReponseError.generic + } + guard let maskedCardNumber = resultInfo["masked_card_number"] as? String else { + throw ReponseError.generic + } + guard let cardType = resultInfo["card_type"] as? String else { + throw ReponseError.generic + } + + var last4 = maskedCardNumber + if last4.count > 4 { + let lastDigits = String(maskedCardNumber[maskedCardNumber.index(maskedCardNumber.endIndex, offsetBy: -4)...]) + if lastDigits.allSatisfy(\.isNumber) { + last4 = "\(cardType) *\(lastDigits)" + } + } + + let responseJson: [String: Any] = [ + "type": "card", + "token": "\(token)" + ] + + let serializedResponseJson = try JSONSerialization.data(withJSONObject: responseJson, options: []) + + guard let serializedResponseString = String(data: serializedResponseJson, encoding: .utf8) else { + throw ReponseError.generic + } + + subscriber.putNext(.webToken(BotCheckoutPaymentWebToken( + title: last4, + data: serializedResponseString, + saveOnServer: state.saveInfo + ))) + subscriber.putCompletion() + } catch { + subscriber.putError(error) + } + }) + + dataTask.resume() + + + return ActionDisposable { + cancelled = true + dataTask.cancel() + } + } +} + + +struct PaymentsPaymentMethodAdditionalFields: OptionSet { + var rawValue: Int32 + + init(rawValue: Int32) { + self.rawValue = rawValue + } + + static let cardholderName = PaymentsPaymentMethodAdditionalFields(rawValue: 1 << 0) + static let country = PaymentsPaymentMethodAdditionalFields(rawValue: 1 << 1) + static let zipCode = PaymentsPaymentMethodAdditionalFields(rawValue: 1 << 2) +} + +private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.checkoutNewCardPaymentCard), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + let cardBrand = STPCardValidator.brand(forNumber: state.card.number) + let maxCardNumberLength = STPCardValidator.maxLength(for: cardBrand) + let maxCVCLength = STPCardValidator.maxCVCLength(for: cardBrand) + + + let image: NSImage? = STPImageLibrary.brandImage(for: cardBrand) + + let spacesCardLength: Int + switch cardBrand { + case .amex: + spacesCardLength = 2 + default: + spacesCardLength = 3 + } + + entries.append(.input(sectionId: sectionId, index: index, value: .string(state.card.number), error: nil, identifier: _id_card_number, mode: .plain, data: .init(viewType: .firstItem), placeholder: InputDataInputPlaceholder(icon: image?._cgImage), inputPlaceholder: "1234 5678 1234 5678", filter: { value in + + let filtered = value.trimmingCharacters(in: CharacterSet(charactersIn: "1234567890").inverted) + let sanitized = STPCardValidator.sanitizedNumericString(for: filtered) + + let cardSpacing: [Int] + switch cardBrand { + case .amex: + cardSpacing = [4 ,10] + default: + cardSpacing = [4, 8, 12] + } + var chars = Array(sanitized) + + for (i, space) in cardSpacing.enumerated() { + let index = space + i + if chars.count > index { + chars.insert(" ", at: index) + } + } + + return String(chars) + }, limit: Int32(maxCardNumberLength + spacesCardLength))) + index += 1 + + entries.append(.input(sectionId: sectionId, index: index, value: .string(state.card.date), error: nil, identifier: _id_card_date, mode: .plain, data: .init(viewType: .innerItem), placeholder: nil, inputPlaceholder: "MM/YY", filter: { value in + + let filtered = value.trimmingCharacters(in: CharacterSet(charactersIn: "1234567890").inverted) + .replacingOccurrences(of: "/", with: "") + + var chars = Array(filtered).map { String($0) } + + if chars.count > 0 { + let month = Int(String(chars[0]))! + if month > 1 { + chars[0] = "0" + if chars.count == 1 { + chars.append("\(month)") + } else { + chars[1] = "\(month)" + } + } + if chars.count > 2 { + chars.insert("/", at: 2) + } + } + return chars.joined() + }, limit: 5)) + index += 1 + + entries.append(.input(sectionId: sectionId, index: index, value: .string(state.card.cvc), error: state.cardError, identifier: _id_card_cvc, mode: .plain, data: .init(viewType: .lastItem), placeholder: nil, inputPlaceholder: "CVC", filter: { value in + return value.trimmingCharacters(in: CharacterSet(charactersIn: "1234567890").inverted) + }, limit: Int32(maxCVCLength))) + index += 1 + + + if state.holderName != nil { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.checkoutNewCardCardholderNameTitle), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + entries.append(.input(sectionId: sectionId, index: index, value: .string(state.holderName), error: nil, identifier: _id_card_holder_name, mode: .plain, data: .init(viewType: .singleItem), placeholder: nil, inputPlaceholder: L10n.checkoutNewCardCardholderNamePlaceholder, filter: { $0.uppercased() }, limit: 255)) + index += 1 + } + + + if state.billingAddress.country != nil || state.billingAddress.zipCode != nil { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.checkoutNewCardPostcodeTitle), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + if state.billingAddress.country != nil { + + let filedata = try! String(contentsOfFile: Bundle.main.path(forResource: "countries", ofType: nil)!) + + let countries: [ValuesSelectorValue] = filedata.components(separatedBy: "\n").compactMap { country in + let entry = country.components(separatedBy: ";") + if entry.count >= 3 { + return ValuesSelectorValue(localized: entry[2], value: .string(entry[1])) + } else { + return nil + } + }.sorted(by: { $0.localized < $1.localized}) + + + entries.append(.selector(sectionId: sectionId, index: index, value: .string(state.billingAddress.country), error: nil, identifier: _id_card_country, placeholder: L10n.checkoutInfoShippingInfoCountryPlaceholder, viewType: state.billingAddress.zipCode != nil ? .firstItem : .singleItem, values: countries)) + index += 1 + } + if state.billingAddress.zipCode != nil { + let type = STPPostalCodeValidator.postalCodeType(forCountryCode: state.billingAddress.country) + + entries.append(.input(sectionId: sectionId, index: index, value: .string(state.billingAddress.zipCode), error: state.billingError, identifier: _id_card_zip_code, mode: .plain, data: .init(viewType: state.billingAddress.country != nil ? .lastItem : .singleItem), placeholder: InputDataInputPlaceholder(L10n.checkoutNewCardPostcodePlaceholder), inputPlaceholder: L10n.checkoutNewCardPostcodePlaceholder, filter: { value in + switch type { + case .countryPostalCodeTypeAlphanumeric: + return value.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) + case .countryPostalCodeTypeNumericOnly: + return value.trimmingCharacters(in: CharacterSet(charactersIn: "1234567890")) + case .countryPostalCodeTypeNotRequired: + return value + @unknown default: + return value + } + }, limit: 12)) + index += 1 + } + + + } + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_card_save_info, data: .init(name: L10n.checkoutInfoSaveInfo, color: theme.colors.text, type: .switchable(state.saveInfo), viewType: .singleItem, enabled: !arguments.passwordMissing, action: arguments.toggleSaveInfo))) + index += 1 + let desc = arguments.passwordMissing ? L10n.checkout2FAText : L10n.checkoutNewCardSaveInfoHelp + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(desc), data: InputDataGeneralTextData(color: theme.colors.listGrayText, viewType: .textBottomItem))) + index += 1 + + + // entries + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func PaymentsPaymentMethodController(context: AccountContext, fields: PaymentsPaymentMethodAdditionalFields, publishableKey: String, passwordMissing: Bool, isTesting: Bool, provider: PaymentProvider, completion: @escaping (BotCheckoutPaymentMethod) -> Void) -> InputDataModalController { + + let actionsDisposable = DisposableSet() + + var close:(()->Void)? = nil + + let initialState = State(card: .init(number: "", date: "", cvc: ""), holderName: fields.contains(.cardholderName) ? "" : nil, billingAddress: .init(country: fields.contains(.country) ? "" : nil, zipCode: fields.contains(.zipCode) ? "" : nil), saveInfo: false) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let arguments = Arguments(context: context, toggleSaveInfo: { + updateState { current in + var current = current + current.saveInfo = !current.saveInfo + return current + } + }, verify: { + + let tokenSignal:Signal + + switch provider { + case .stripe: + let configuration = STPPaymentConfiguration.shared().copy() as! STPPaymentConfiguration + configuration.smsAutofillDisabled = true + configuration.publishableKey = publishableKey + configuration.appleMerchantIdentifier = "merchant.ph.telegra.Telegraph" + let apiClient = STPAPIClient(configuration: configuration) + let card = stateValue.with { $0.stripe } + let saveOnServer = stateValue.with { $0.saveInfo } + let createToken: Signal = Signal { subscriber in + apiClient.createToken(withCard: card, completion: { token, error in + if let error = error { + subscriber.putError(error) + } else if let token = token { + subscriber.putNext(token) + subscriber.putCompletion() + } + }) + return ActionDisposable { + let _ = apiClient.publishableKey + } + } + tokenSignal = createToken |> map { token in + if let card = token.card { + let last4 = card.last4() + let brand = STPAPIClient.string(with: card.brand) + return .webToken(BotCheckoutPaymentWebToken(title: "\(brand)*\(last4)", data: "{\"type\": \"card\", \"id\": \"\(token.tokenId)\"}", saveOnServer: saveOnServer)) + } + return nil + } + case .smartglocal: + tokenSignal = validateSmartGlobal(publishableKey, isTesting: isTesting, state: stateValue.with { $0 }) |> map(Optional.init) + } + _ = showModalProgress(signal: tokenSignal, for: context.window).start(next: { token in + if let token = token { + completion(token) + close?() + } + }, error: { error in + alert(for: context.window, info: error.localizedDescription) + }) + + }, passwordMissing: passwordMissing) + + let signal = statePromise.get() |> deliverOnPrepareQueue |> map { state in + return InputDataSignalValue(entries: entries(state, arguments: arguments)) + } + + let controller = InputDataController(dataSignal: signal, title: L10n.checkoutNewCardTitle) + + controller.onDeinit = { + actionsDisposable.dispose() + updateState { _ in + State(card: .init(number: "", date: "", cvc: ""), holderName: "", billingAddress: .init(country: "", zipCode: ""), saveInfo: false) + } + } + + controller.updateDatas = { data in + + updateState { current in + var current = current + + current.billingError = nil + current.nameError = nil + + current.card.number = data[_id_card_number]?.stringValue ?? "" + current.card.date = data[_id_card_date]?.stringValue ?? "" + current.card.cvc = data[_id_card_cvc]?.stringValue ?? "" + + current.holderName = data[_id_card_holder_name]?.stringValue + current.billingAddress.country = data[_id_card_country]?.stringValue + current.billingAddress.zipCode = data[_id_card_zip_code]?.stringValue + + + let normalized = STPCardValidator.sanitizedNumericString(for: current.card.number) + let brand = STPCardValidator.brand(forNumber: normalized) + let maxCardNumberLength = STPCardValidator.maxLength(for: brand) + let maxCVCLength = STPCardValidator.maxCVCLength(for: brand) + + + + var cardError: InputDataValueError? = nil + + if normalized.length == maxCardNumberLength { + let state = STPCardValidator.validationState(forNumber: current.card.number, validatingCardBrand: true) + switch state { + case .invalid: + cardError = .init(description: L10n.yourCardsNumberIsInvalid, target: .data) + default: + cardError = nil + } + } + + if current.card.date.length == 5 && cardError == nil { + let yearState = STPCardValidator.validationState(forExpirationYear: String(current.card.date.suffix(2)), inMonth: current.card.date.prefix(2)) + switch yearState { + case .invalid: + cardError = .init(description: L10n.yourCardsExpirationYearIsInvalid, target: .data) + default: + let monthState = STPCardValidator.validationState(forExpirationMonth: current.card.date.prefix(2)) + switch monthState { + case .invalid: + cardError = .init(description: L10n.yourCardsExpirationMonthIsInvalid, target: .data) + default: + cardError = nil + } + } + } + + if current.card.cvc.length == maxCVCLength && cardError == nil { + let state = STPCardValidator.validationState(forCVC: current.card.cvc, cardBrand: brand) + switch state { + case .invalid: + cardError = .init(description: L10n.yourCardsSecurityCodeIsInvalid, target: .data) + default: + cardError = nil + } + } + + current.cardError = cardError + + return current + } + return .none + } + + controller.validateData = { _ in + let state = stateValue.with { $0 } + if let unfilled = state.unfilledItem { + return .fail(.fields([unfilled: .shake])) + } else { + return .fail(.doSomething(next: { _ in + arguments.verify() + })) + } + } + + let modalInteractions = ModalInteractions(acceptTitle: L10n.modalDone, accept: { [weak controller] in + _ = controller?.returnKeyAction() + }, drawBorder: true, height: 50, singleButton: true) + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) + + close = { [weak modalController] in + modalController?.modal?.close() + } + + return modalController + +} + + + + + diff --git a/Telegram-Mac/PaymentsReceiptController.swift b/Telegram-Mac/PaymentsReceiptController.swift new file mode 100644 index 0000000000..458bd3c69c --- /dev/null +++ b/Telegram-Mac/PaymentsReceiptController.swift @@ -0,0 +1,259 @@ +// +// PaymentsReceiptController.swift +// Telegram +// +// Created by Mikhail Filimonov on 26.02.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + + +import Cocoa +import TGUIKit +import SwiftSignalKit +import TelegramCore +import Postbox + +private final class Arguments { + let context: AccountContext + init(context: AccountContext) { + self.context = context + } +} + +private struct State : Equatable { + var message: Message + var botPeer: PeerEquatable? + var receipt: BotPaymentReceipt? +} + +private let _id_loading = InputDataIdentifier("_id_loading") +private let _id_preview = InputDataIdentifier("_id_preview") + +private let _id_checkout_payment_method = InputDataIdentifier("_id_checkout_payment_method") +private let _id_checkout_shipping_info = InputDataIdentifier("_id_checkout_shipping_info") +private let _id_checkout_name = InputDataIdentifier("_id_checkout_name") +private let _id_checkout_flex_shipping = InputDataIdentifier("_id_checkout_flex_shipping") +private let _id_checkout_phone_number = InputDataIdentifier("_id_checkout_phone_number") +private let _id_checkout_email = InputDataIdentifier("_id_checkout_email") + + +private func _id_checkout_price(_ label: String, index: Int) -> InputDataIdentifier { + return InputDataIdentifier("_id_checkout_price_\(label)_\(index)") +} + +private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_preview, equatable: InputDataEquatable(state.botPeer), comparable: nil, item: { initialSize, stableId in + return PaymentsCheckoutPreviewRowItem(initialSize, stableId: stableId, context: arguments.context, message: state.message, botPeer: state.botPeer?.peer, viewType: .singleItem) + })) + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + if let receipt = state.receipt { + + let insets = NSEdgeInsets(top: 7, left: 16, bottom: 7, right: 16) + let first = NSEdgeInsets(top: 14, left: 16, bottom: 7, right: 16) + let last = NSEdgeInsets(top: 7, left: 16, bottom: 14, right: 16) + + struct Tuple: Equatable { + let label: String + let price: String + let viewType: GeneralViewType + } + + var prices = receipt.invoice.prices + if let shippingOption = receipt.shippingOption { + prices += shippingOption.prices + } + + if let tipAmount = receipt.tipAmount { + prices.append(.init(label: L10n.paymentsReceiptTip, amount: tipAmount)) + } + + for (i, price) in prices.enumerated() { + var viewType = bestGeneralViewType(prices, for: i) + + if i == 0 { + viewType = viewType.withUpdatedInsets(first) + } else { + viewType = viewType.withUpdatedInsets(insets) + } + if price == prices.last { + if prices.count > 1 { + viewType = GeneralViewType.innerItem.withUpdatedInsets(insets) + } else { + viewType = GeneralViewType.firstItem.withUpdatedInsets(insets) + } + } + let tuple = Tuple(label:price.label, price: formatCurrencyAmount(price.amount, currency: receipt.invoice.currency), viewType: viewType) + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_checkout_price(price.label, index: i), equatable: InputDataEquatable(tuple), comparable: nil, item: { initialSize, stableId in + return PaymentsCheckoutPriceItem(initialSize, stableId: stableId, title: tuple.label, price: tuple.price, font: .normal(.text), color: theme.colors.grayText, viewType: tuple.viewType) + })) + index += 1 + } + + if !prices.isEmpty { + let viewType = GeneralViewType.lastItem.withUpdatedInsets(last) + + let tuple = Tuple(label: L10n.checkoutTotalAmount, price: formatCurrencyAmount(prices.reduce(0, { $0 + $1.amount}), currency: receipt.invoice.currency), viewType: viewType) + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_checkout_price(L10n.checkoutTotalAmount, index: .max), equatable: InputDataEquatable(tuple), comparable: nil, item: { initialSize, stableId in + return PaymentsCheckoutPriceItem(initialSize, stableId: stableId, title: tuple.label, price: tuple.price, font: .medium(.text), color: theme.colors.text, viewType: tuple.viewType) + })) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + } + + var fields = receipt.invoice.requestedFields.intersection([.shippingAddress, .email, .name, .phone]) + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_checkout_payment_method, data: .init(name: L10n.checkoutPaymentMethod, color: theme.colors.text, type: .context(receipt.credentialsTitle), viewType: fields.isEmpty ? .singleItem : .firstItem, enabled: false))) + index += 1 + + + + var updated = fields.subtracting(.shippingAddress) + if updated != fields { + fields = updated + var addressString = "" + if let address = receipt.info?.shippingAddress { + let components: [String] = [ + address.city, + address.streetLine1, + address.streetLine2, + address.state + ] + for component in components { + if !component.isEmpty { + if !addressString.isEmpty { + addressString.append(", ") + } + addressString.append(component) + } + } + } + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_checkout_shipping_info, data: .init(name: L10n.checkoutShippingAddress, color: theme.colors.text, type: .context(addressString), viewType: fields.isEmpty && receipt.shippingOption == nil ? .lastItem : .innerItem, enabled: false))) + index += 1 + } + + if let _ = receipt.shippingOption { + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_checkout_flex_shipping, data: .init(name: L10n.checkoutShippingMethod, color: theme.colors.text, type: .context(receipt.shippingOption?.title ?? ""), viewType: fields.isEmpty ? .lastItem : .innerItem, enabled: false))) + index += 1 + } + + updated = fields.subtracting(.name) + if updated != fields { + fields = updated + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_checkout_name, data: .init(name: L10n.checkoutName, color: theme.colors.text, type: .context(receipt.info?.name ?? ""), viewType: fields.isEmpty ? .lastItem : .innerItem, enabled: false))) + index += 1 + } + + updated = fields.subtracting(.email) + if updated != fields { + fields = updated + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_checkout_email, data: .init(name: L10n.checkoutEmail, color: theme.colors.text, type: .context(receipt.info?.email ?? ""), viewType: fields.isEmpty ? .lastItem : .innerItem, enabled: false))) + index += 1 + } + + updated = fields.subtracting(.phone) + if updated != fields { + fields = updated + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_checkout_phone_number, data: .init(name: L10n.checkoutPhone, color: theme.colors.text, type: .nextContext(formatPhoneNumber(receipt.info?.phone ?? "")), viewType: fields.isEmpty ? .lastItem : .innerItem, enabled: false))) + index += 1 + } + + } else { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_loading, equatable: nil, comparable: nil, item: { initialSize, stableId in + return GeneralLoadingRowItem(initialSize, stableId: stableId, viewType: .singleItem) + })) + } + + // entries + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func PaymentsReceiptController(context: AccountContext, messageId: MessageId, message: Message) -> InputDataModalController { + + var close:(()->Void)? = nil + let actionsDisposable = DisposableSet() + + let initialState = State(message: message) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let arguments = Arguments(context: context) + + let signal = statePromise.get() |> deliverOnPrepareQueue |> map { state in + return InputDataSignalValue(entries: entries(state, arguments: arguments)) + } + + let controller = InputDataController(dataSignal: signal, title:L10n.checkoutReceiptTitle) + + controller.onDeinit = { + actionsDisposable.dispose() + } + + controller.validateData = { _ in + close?() + return .none + } + + let modalInteractions = ModalInteractions(acceptTitle: L10n.modalDone, accept: { [weak controller] in + _ = controller?.returnKeyAction() + }, drawBorder: true, height: 50, singleButton: true) + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) + + close = { [weak modalController] in + modalController?.modal?.close() + } + + + //BotPaymentReceipt, RequestBotPaymentReceiptError + let receiptPromise: Promise = Promise() + receiptPromise.set(context.engine.payments.requestBotPaymentReceipt(messageId: messageId) |> `catch` { _ in return .complete() }) + + + let botPeer = receiptPromise.get() |> mapToSignal { value in + return context.account.postbox.transaction { $0.getPeer(value.botPaymentId) } + } + + actionsDisposable.add(combineLatest(receiptPromise.get(), botPeer).start(next: { receipt, botPeer in + updateState { current in + var current = current + current.receipt = receipt + current.botPeer = botPeer != nil ? PeerEquatable(botPeer!) : nil + return current + } + })) + + + + return modalController +} + + + + diff --git a/Telegram-Mac/PaymentsShippingInfoController.swift b/Telegram-Mac/PaymentsShippingInfoController.swift new file mode 100644 index 0000000000..e395313a5a --- /dev/null +++ b/Telegram-Mac/PaymentsShippingInfoController.swift @@ -0,0 +1,360 @@ +// +// PaymentsShippingInfoController.swift +// Telegram +// +// Created by Mikhail Filimonov on 25.02.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + + +import Cocoa +import TGUIKit +import SwiftSignalKit +import TelegramCore + +import Postbox + +private let cManager: CountryManager = CountryManager() + + +private final class Arguments { + let context: AccountContext + let toggleSaveInfo:()->Void + init(context: AccountContext, toggleSaveInfo: @escaping()->Void) { + self.context = context + self.toggleSaveInfo = toggleSaveInfo + } +} + +private let _id_checkout_info_address1 = InputDataIdentifier("_id_checkout_info_address1") +private let _id_checkout_info_address2 = InputDataIdentifier("_id_checkout_info_address2") +private let _id_checkout_info_city = InputDataIdentifier("_id_checkout_info_city") +private let _id_checkout_info_state = InputDataIdentifier("_id_checkout_info_state") +private let _id_checkout_info_country = InputDataIdentifier("_id_checkout_info_country") +private let _id_checkout_info_postcode = InputDataIdentifier("_id_checkout_info_postcode") +private let _id_checkout_info_name = InputDataIdentifier("_id_checkout_info_name") +private let _id_checkout_info_email = InputDataIdentifier("_id_checkout_info_email") +private let _id_checkout_info_phone = InputDataIdentifier("_id_checkout_info_phone") +private let _id_checkout_info_save_info = InputDataIdentifier("_id_checkout_info_save_info") + + +private struct State : Equatable { + + struct Address : Equatable { + var address1: String + var address2: String + var city: String + var state: String + var country: String + var postcode: String + } + + var address: Address? + var name: String? + var email: String? + var phone: String? + + var saveInfo: Bool + + var errors:[InputDataIdentifier: InputDataValueError] + + var firstEmptyId: InputDataIdentifier? { + if let address = address { + if address.address1.isEmpty { + return _id_checkout_info_address1 + } + if address.address2.isEmpty { + return _id_checkout_info_address2 + } + if address.city.count < 2 { + return _id_checkout_info_city + } + if address.state.count < 2 { + return _id_checkout_info_state + } + if address.country.isEmpty { + return _id_checkout_info_country + } + if address.postcode.count < 2 { + return _id_checkout_info_postcode + } + } + if let value = name, value.isEmpty { + return _id_checkout_info_name + } + if let value = email, value.isEmpty { + return _id_checkout_info_email + } + if let value = phone, value.isEmpty { + return _id_checkout_info_phone + } + return nil + } + + var formInfo: BotPaymentRequestedInfo { + var shippingAddress: BotPaymentShippingAddress? + if let address = self.address { + shippingAddress = BotPaymentShippingAddress(streetLine1: address.address1, streetLine2: address.address2, city: address.city, state: address.state, countryIso2: address.country, postCode: address.postcode) + } + return BotPaymentRequestedInfo(name: self.name, phone: self.phone, email: self.email, shippingAddress: shippingAddress) + } + +} + + +private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + if let address = state.address { + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.checkoutInfoShippingInfoTitle), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + entries.append(.input(sectionId: sectionId, index: index, value: .string(address.address1), error: nil, identifier: _id_checkout_info_address1, mode: .plain, data: .init(viewType: .firstItem), placeholder: InputDataInputPlaceholder(L10n.checkoutInfoShippingInfoAddress1), inputPlaceholder: L10n.checkoutInfoShippingInfoAddress1Placeholder, filter: { $0 }, limit: 64)) + index += 1 + + entries.append(.input(sectionId: sectionId, index: index, value: .string(address.address2), error: nil, identifier: _id_checkout_info_address2, mode: .plain, data: .init(viewType: .innerItem), placeholder: InputDataInputPlaceholder(L10n.checkoutInfoShippingInfoAddress2), inputPlaceholder: L10n.checkoutInfoShippingInfoAddress2Placeholder, filter: { $0 }, limit: 64)) + index += 1 + + entries.append(.input(sectionId: sectionId, index: index, value: .string(address.city), error: nil, identifier: _id_checkout_info_city, mode: .plain, data: .init(viewType: .innerItem), placeholder: InputDataInputPlaceholder(L10n.checkoutInfoShippingInfoCity), inputPlaceholder: L10n.checkoutInfoShippingInfoCityPlaceholder, filter: { $0 }, limit: 64)) + index += 1 + + entries.append(.input(sectionId: sectionId, index: index, value: .string(address.state), error: nil, identifier: _id_checkout_info_state, mode: .plain, data: .init(viewType: .innerItem), placeholder: InputDataInputPlaceholder(L10n.checkoutInfoShippingInfoState), inputPlaceholder: L10n.checkoutInfoShippingInfoStatePlaceholder, filter: { $0 }, limit: 64)) + index += 1 + + + let filedata = try! String(contentsOfFile: Bundle.main.path(forResource: "countries", ofType: nil)!) + + let countries: [ValuesSelectorValue] = filedata.components(separatedBy: "\n").compactMap { country in + let entry = country.components(separatedBy: ";") + if entry.count >= 3 { + return ValuesSelectorValue(localized: entry[2], value: .string(entry[1])) + } else { + return nil + } + }.sorted(by: { $0.localized < $1.localized}) + + entries.append(InputDataEntry.selector(sectionId: sectionId, index: index, value: .string(state.address?.country), error: nil, identifier: _id_checkout_info_country, placeholder: L10n.checkoutInfoShippingInfoCountry, viewType: .innerItem, values: countries)) + index += 1 + + + + entries.append(.input(sectionId: sectionId, index: index, value: .string(address.postcode), error: nil, identifier: _id_checkout_info_postcode, mode: .plain, data: .init(viewType: .lastItem), placeholder: InputDataInputPlaceholder(L10n.checkoutInfoShippingInfoPostcode), inputPlaceholder: L10n.checkoutInfoShippingInfoPostcodePlaceholder, filter: { $0 }, limit: 12)) + index += 1 + + } + + if state.email != nil || state.name != nil || state.phone != nil { + if state.address != nil { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + } + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.checkoutInfoReceiverInfoTitle), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + + struct Tuple : Equatable { + let id: InputDataIdentifier + let name: String + let placeholder: String + let value: InputDataValue + let error: InputDataValueError? + } + + var items:[Tuple] = [] + + if let name = state.name { + items.append(Tuple(id: _id_checkout_info_name, name: L10n.checkoutInfoReceiverInfoName, placeholder: L10n.checkoutInfoReceiverInfoNamePlaceholder, value: .string(name), error: state.errors[_id_checkout_info_name])) + } + if let email = state.email { + items.append(Tuple(id: _id_checkout_info_email, name: L10n.checkoutInfoReceiverInfoEmail, placeholder: L10n.checkoutInfoReceiverInfoEmailPlaceholder, value: .string(email), error: state.errors[_id_checkout_info_email])) + } + if let phone = state.phone { + items.append(Tuple(id: _id_checkout_info_phone, name: L10n.checkoutInfoReceiverInfoPhone, placeholder: L10n.checkoutInfoReceiverInfoPhone, value: .string(phone), error: state.errors[_id_checkout_info_phone])) + } + + for item in items { + entries.append(.input(sectionId: sectionId, index: index, value: item.value, error: item.error, identifier: item.id, mode: .plain, data: .init(viewType: bestGeneralViewType(items, for: item)), placeholder: InputDataInputPlaceholder(item.name), inputPlaceholder: item.placeholder, filter: { $0 }, limit: 255)) + index += 1 + } + } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: _id_checkout_info_save_info, data: .init(name: L10n.checkoutInfoSaveInfo, color: theme.colors.text, type: .switchable(state.saveInfo), viewType: .singleItem, action: arguments.toggleSaveInfo))) + index += 1 + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.checkoutInfoSaveInfoHelp), data: InputDataGeneralTextData.init(color: theme.colors.listGrayText, viewType: .textBottomItem))) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +enum PaymentsShippingInfoFocus { + case address + case name + case email + case phone +} + +func PaymentsShippingInfoController(context: AccountContext, invoice: BotPaymentInvoice, messageId: MessageId, formInfo: BotPaymentRequestedInfo, focus: PaymentsShippingInfoFocus?, formInfoUpdated: @escaping (BotPaymentRequestedInfo, BotPaymentValidatedFormInfo) -> Void) -> InputDataModalController { + + let actionsDisposable = DisposableSet() + var close:(()->Void)? = nil + + let initialState = State(address: invoice.requestedFields.contains(.shippingAddress) ? State.Address(address1: formInfo.shippingAddress?.streetLine1 ?? "", address2: formInfo.shippingAddress?.streetLine2 ?? "", city: formInfo.shippingAddress?.city ?? "", state: formInfo.shippingAddress?.state ?? "", country: formInfo.shippingAddress?.countryIso2 ?? "", postcode: formInfo.shippingAddress?.postCode ?? "") : nil, name: invoice.requestedFields.contains(.name) ? formInfo.name ?? "" : nil, email: invoice.requestedFields.contains(.email) ? formInfo.email ?? "" : nil, phone: invoice.requestedFields.contains(.phone) ? formInfo.phone ?? "" : nil, saveInfo: true, errors: [:]) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let arguments = Arguments(context: context, toggleSaveInfo: { + updateState { current in + var current = current + current.saveInfo = !current.saveInfo + return current + } + }) + + let signal = statePromise.get() |> deliverOnMainQueue |> map { state in + return InputDataSignalValue(entries: entries(state, arguments: arguments)) + } + + let controller = InputDataController(dataSignal: signal, title: L10n.checkoutInfoTitle) + + controller.onDeinit = { + actionsDisposable.dispose() + } + + controller.didAppear = { controller in + DispatchQueue.main.async { + if let focus = focus { + let id: InputDataIdentifier + switch focus { + case .address: + id = _id_checkout_info_address1 + case .name: + id = _id_checkout_info_name + case .email: + id = _id_checkout_info_email + case .phone: + id = _id_checkout_info_phone + } + controller.makeFirstResponderIfPossible(for: id, focusIdentifier: id) + } + } + } + + controller.validateData = { _ in + + let emptyId = stateValue.with { $0.firstEmptyId } + if let emptyId = emptyId { + return .fail(.fields([emptyId: .shake])) + } + + let state = stateValue.with { $0 } + let formInfo = state.formInfo + + return .fail(.doSomething(next: { f in + _ = showModalProgress(signal: context.engine.payments.validateBotPaymentForm(saveInfo: state.saveInfo, messageId: messageId, formInfo: formInfo), for: context.window).start(next: { result in + + formInfoUpdated(formInfo, result) + close?() + }, error: { error in + let text: String + var id: InputDataIdentifier? = nil + switch error { + case .shippingNotAvailable: + text = L10n.checkoutInfoErrorShippingNotAvailable + case .addressStateInvalid: + text = L10n.checkoutInfoErrorStateInvalid + id = _id_checkout_info_state + case .addressPostcodeInvalid: + text = L10n.checkoutInfoErrorPostcodeInvalid + id = _id_checkout_info_postcode + case .addressCityInvalid: + text = L10n.checkoutInfoErrorCityInvalid + id = _id_checkout_info_city + case .nameInvalid: + text = L10n.checkoutInfoErrorNameInvalid + id = _id_checkout_info_name + case .emailInvalid: + text = L10n.checkoutInfoErrorEmailInvalid + id = _id_checkout_info_email + case .phoneInvalid: + text = L10n.checkoutInfoErrorPhoneInvalid + id = _id_checkout_info_phone + case .generic: + text = L10n.unknownError + } + alert(for: context.window, info: text) + if let id = id { + f(.fail(.fields([id: .shake]))) + } + }) + })) + + } + + controller.updateDatas = { data in + updateState { current in + var current = current + + current.address?.address1 = data[_id_checkout_info_address1]?.stringValue ?? "" + current.address?.address2 = data[_id_checkout_info_address2]?.stringValue ?? "" + current.address?.city = data[_id_checkout_info_city]?.stringValue ?? "" + current.address?.state = data[_id_checkout_info_state]?.stringValue ?? "" + current.address?.country = data[_id_checkout_info_country]?.stringValue ?? "" + current.address?.postcode = data[_id_checkout_info_postcode]?.stringValue ?? "" + + + if let value = data[_id_checkout_info_name]?.stringValue { + current.name = value + } + if let value = data[_id_checkout_info_email]?.stringValue { + current.email = value + } + if let value = data[_id_checkout_info_phone]?.stringValue { + current.phone = value + } + current.errors = [:] + return current + } + return .none + } + + + + let modalInteractions = ModalInteractions(acceptTitle: L10n.modalDone, accept: { [weak controller] in + _ = controller?.returnKeyAction() + }, drawBorder: true, height: 50, singleButton: true) + + + + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) + + close = { [weak modalController] in + modalController?.modal?.close() + } + + return modalController + +} + + diff --git a/Telegram-Mac/PaymentsShippingMethodController.swift b/Telegram-Mac/PaymentsShippingMethodController.swift new file mode 100644 index 0000000000..06e14b2dc8 --- /dev/null +++ b/Telegram-Mac/PaymentsShippingMethodController.swift @@ -0,0 +1,101 @@ +// +// PaymentsShippingMethodController.swift +// Telegram +// +// Created by Mikhail Filimonov on 25.02.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit +import TelegramCore +import Postbox + + +private final class Arguments { + let context: AccountContext + let select:(BotPaymentShippingOption)->Void + init(context: AccountContext, select: @escaping(BotPaymentShippingOption)->Void) { + self.context = context + self.select = select + } +} + +private struct State : Equatable { + var shippingOptions: [BotPaymentShippingOption] + var form: BotPaymentForm +} + +private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + for option in state.shippingOptions { + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: InputDataIdentifier(option.title), data: .init(name: option.title, color: theme.colors.text, type: .context(formatCurrencyAmount(option.prices.reduce(0, { $0 + $1.amount }), currency: state.form.invoice.currency)), viewType: bestGeneralViewType(state.shippingOptions, for: option), action: { + arguments.select(option) + }))) + index += 1 + } + + // entries + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func PaymentsShippingMethodController(context: AccountContext, shippingOptions: [BotPaymentShippingOption], form: BotPaymentForm, select:@escaping(BotPaymentShippingOption)->Void) -> InputDataModalController { + + var close:(()->Void)? = nil + + let actionsDisposable = DisposableSet() + + let initialState = State(shippingOptions: shippingOptions, form: form) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let arguments = Arguments(context: context, select: { option in + close?() + select(option) + }) + + let signal = statePromise.get() |> deliverOnPrepareQueue |> map { state in + return InputDataSignalValue(entries: entries(state, arguments: arguments)) + } + + let controller = InputDataController(dataSignal: signal, title: L10n.checkoutShippingMethod) + + controller.onDeinit = { + actionsDisposable.dispose() + } + + let modalInteractions = ModalInteractions(acceptTitle: L10n.modalCancel, accept: { + close?() + }, drawBorder: true, height: 50, singleButton: true) + + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions) + + + close = { [weak modalController] in + modalController?.modal?.close() + } + + return modalController +} + + + + + diff --git a/Telegram-Mac/PaymentsTipsRowItem.swift b/Telegram-Mac/PaymentsTipsRowItem.swift new file mode 100644 index 0000000000..b50149b334 --- /dev/null +++ b/Telegram-Mac/PaymentsTipsRowItem.swift @@ -0,0 +1,240 @@ +// +// PaymentsTipsRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 04.04.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import TelegramCore +import SwiftSignalKit + +final class PaymentsTipsRowItem : GeneralRowItem { + + struct Tip { + fileprivate var text: TextViewLayout + fileprivate var value: Int64 + fileprivate var size: NSSize + } + + fileprivate let tips:BotPaymentInvoice.Tip + fileprivate let current: Int64? + fileprivate let currency: String + private(set) var rendered: [[Tip]] = [] + let layouts: [(TextViewLayout, Int64)] + let select:(Int64?)->Void + init(_ initialSize: NSSize, stableId: AnyHashable, viewType: GeneralViewType, currency: String, tips: BotPaymentInvoice.Tip, current: Int64?, select: @escaping(Int64?)->Void) { + self.tips = tips + self.current = current + self.currency = currency + self.select = select + var layouts:[(TextViewLayout, Int64)] = [] + + for amount in tips.suggested { + + let layout = TextViewLayout(.initialize(string: formatCurrencyAmount(amount, currency: self.currency), color: current == amount ? .white : theme.colors.greenUI, font: .medium(.text))) + layout.measure(width: .greatestFiniteMagnitude) + layouts.append((layout, amount)) + } + + self.layouts = layouts + + super.init(initialSize, stableId: stableId, viewType: viewType) + + _ = makeSize(initialSize.width, oldWidth: 0) + } + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + + let width = self.blockWidth - viewType.innerInset.left - viewType.innerInset.right + + var rendered:[[Tip]] = [] + + let insets: NSEdgeInsets = NSEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) + let insetBetween = NSEdgeInsets(top: 5, left: 5, bottom: 5, right: 5) + + + var row:[Tip] = [] + + let layoutRow:()->Void = { + if !row.isEmpty { + let w = (width - row.reduce(0, { $0 + $1.size.width }) - (insetBetween.right * CGFloat(row.count - 1))) + + if w > 0 { + let rest = w / CGFloat(row.count) + for i in 0 ..< row.count { + row[i].size.width += floor(rest) + } + } + rendered.append(row) + row.removeAll() + } + } + + + for tip in layouts { + let minSize = NSMakeSize(tip.0.layoutSize.width + insets.left + insets.right, tip.0.layoutSize.height + insets.bottom + insets.top) + + let tip = Tip(text: tip.0, value: tip.1, size: minSize) + + var i: Int = 0 + + row.append(tip) + + let row_w: CGFloat = row.reduce(0, { current, value in + var current = current + current += tip.size.width + insetBetween.right + if i == row.count - 1 { + current -= insetBetween.right + } + i += 1 + return current + }) + if row_w > width { + row.removeLast() + layoutRow() + row.append(tip) + } + } + layoutRow() + + self.rendered = rendered + + return true + } + + override var height: CGFloat { + return rendered.reduce(0, { current, value in + let height = value.max(by: { $0.size.height < $1.size.height})!.size.height + if current == viewType.innerInset.top || rendered.count == 1 { + return current + height + } else { + return current + height + viewType.innerInset.top + } + }) + } + + var frames:[NSRect] { + var x: CGFloat = viewType.innerInset.left + var y: CGFloat = 0 + var rects:[NSRect] = [] + for row in rendered { + for col in row { + let rect = NSMakeRect(x, y, col.size.width, col.size.height) + rects.append(rect) + x += rect.width + 5 + } + x = viewType.innerInset.left + y += rects.last!.height + viewType.innerInset.top + } + return rects + } + + var count: Int { + return rendered.reduce(0, { current, value in + return current + value.count + }) + } + + var list: [Tip] { + return rendered.reduce([], { current, value in + return current + value + }) + } + + override var hasBorder: Bool { + return false + } + + override func viewClass() -> AnyClass { + return PaymentsTipRowView.self + } +} + +private final class PaymentsTipView : Control { + private let textView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + layer?.cornerRadius = .cornerRadius + addSubview(textView) + textView.userInteractionEnabled = false + textView.isSelectable = false + } + + func update(_ text: PaymentsTipsRowItem.Tip, selected: Bool, animated: Bool, select: @escaping(Int64?)->Void) { + backgroundColor = selected ? theme.colors.greenUI : theme.colors.greenUI.withAlphaComponent(0.4) + if animated { + layer?.animateBackground() + } + textView.update(text.text) + + self.removeAllHandlers() + self.set(handler: { _ in + if selected { + select(nil) + } else { + select(text.value) + } + }, for: .Click) + + textView.center() + } + override func layout() { + super.layout() + textView.center() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + +private final class PaymentsTipRowView : GeneralContainableRowView { + private var contentView:[WeakReference] = [] + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + + guard let item = item as? PaymentsTipsRowItem else { + return + } + for (i, frame) in item.frames.enumerated() { + let view = contentView[i].value + view?.frame = frame + } + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? PaymentsTipsRowItem else { + return + } + + while contentView.count > item.count { + contentView.last?.value?.removeFromSuperview() + contentView.removeLast() + } + while contentView.count < item.count { + let view = PaymentsTipView(frame: .zero) + addSubview(view) + contentView.append(WeakReference(value: view)) + } + + for (i, frame) in item.frames.enumerated() { + let view = contentView[i].value + view?.frame = frame + view?.update(item.list[i], selected: item.current == item.list[i].value, animated: animated, select: item.select) + } + } +} diff --git a/Telegram-Mac/PeerChannelMemberCategoriesContextsManager.swift b/Telegram-Mac/PeerChannelMemberCategoriesContextsManager.swift new file mode 100644 index 0000000000..f7d452c459 --- /dev/null +++ b/Telegram-Mac/PeerChannelMemberCategoriesContextsManager.swift @@ -0,0 +1,421 @@ +// +// PeerChannelMemberCategoriesContextsManager.swift +// Telegram +// +// Created by Mikhail Filimonov on 02/01/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Foundation +import Postbox +import TelegramCore + +import SwiftSignalKit + +enum PeerChannelMemberContextKey: Equatable, Hashable { + case recent + case recentSearch(String) + case mentions(threadId: MessageId?, query: String?) + case admins(String?) + case contacts(String?) + case bots(String?) + case restrictedAndBanned(String?) + case restricted(String?) + case banned(String?) + +} + + +private final class PeerChannelMembersOnlineContext { + let subscribers = Bag<(Int32) -> Void>() + let disposable: Disposable + var value: Int32? + var emptyTimer: SwiftSignalKit.Timer? + + init(disposable: Disposable) { + self.disposable = disposable + } +} + + +private final class PeerChannelMemberCategoriesContextsManagerImpl { + fileprivate var contexts: [PeerId: PeerChannelMemberCategoriesContext] = [:] + fileprivate var onlineContexts: [PeerId: PeerChannelMembersOnlineContext] = [:] + fileprivate var replyThreadHistoryContexts: [MessageId: ReplyThreadHistoryContext] = [:] + + fileprivate let engine: TelegramEngine + fileprivate let account: Account + init(_ engine: TelegramEngine, account: Account) { + self.engine = engine + self.account = account + } + + func getContext(peerId: PeerId, key: PeerChannelMemberContextKey, requestUpdate: Bool, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl) { + if let current = self.contexts[peerId] { + return current.getContext(key: key, requestUpdate: requestUpdate, updated: updated) + } else { + var becameEmptyImpl: ((Bool) -> Void)? + let context = PeerChannelMemberCategoriesContext(engine: engine, account: account, peerId: peerId, becameEmpty: { value in + becameEmptyImpl?(value) + }) + becameEmptyImpl = { [weak self, weak context] value in + assert(Queue.mainQueue().isCurrent()) + if let strongSelf = self { + if let current = strongSelf.contexts[peerId], current === context { + strongSelf.contexts.removeValue(forKey: peerId) + } + } + } + self.contexts[peerId] = context + return context.getContext(key: key, requestUpdate: requestUpdate, updated: updated) + } + } + + func recentOnline(peerId: PeerId, updated: @escaping (Int32) -> Void) -> Disposable { + let context: PeerChannelMembersOnlineContext + if let current = self.onlineContexts[peerId] { + context = current + } else { + let disposable = MetaDisposable() + context = PeerChannelMembersOnlineContext(disposable: disposable) + self.onlineContexts[peerId] = context + + let signal = ( + engine.peers.chatOnlineMembers(peerId: peerId) + |> then( + .complete() + |> delay(30.0, queue: .mainQueue()) + ) + ) |> restart + + disposable.set(signal.start(next: { [weak context] value in + guard let context = context else { + return + } + context.value = value + for f in context.subscribers.copyItems() { + f(value) + } + })) + } + + if let emptyTimer = context.emptyTimer { + emptyTimer.invalidate() + context.emptyTimer = nil + } + + let index = context.subscribers.add({ next in + updated(next) + }) + updated(context.value ?? 0) + + return ActionDisposable { [weak self, weak context] in + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + if let current = strongSelf.onlineContexts[peerId], let context = context, current === context { + current.subscribers.remove(index) + if current.subscribers.isEmpty { + if current.emptyTimer == nil { + let timer = SwiftSignalKit.Timer(timeout: 60.0, repeat: false, completion: { [weak context] in + if let current = strongSelf.onlineContexts[peerId], let context = context, current === context { + if current.subscribers.isEmpty { + strongSelf.onlineContexts.removeValue(forKey: peerId) + current.disposable.dispose() + } + } + }, queue: Queue.mainQueue()) + current.emptyTimer = timer + timer.start() + } + } + } + } + } + } + + + + func loadMore(peerId: PeerId, control: PeerChannelMemberCategoryControl) { + if let context = self.contexts[peerId] { + context.loadMore(control) + } + } +} + +final class PeerChannelMemberCategoriesContextsManager { + private let impl: QueueLocalObject + + private let engine: TelegramEngine + private let account: Account + init(_ engine: TelegramEngine, account: Account) { + self.engine = engine + self.account = account + self.impl = QueueLocalObject(queue: Queue.mainQueue(), generate: { + return PeerChannelMemberCategoriesContextsManagerImpl(engine, account: account) + }) + } + + func loadMore(peerId: PeerId, control: PeerChannelMemberCategoryControl?) { + if let control = control { + self.impl.with { impl in + impl.loadMore(peerId: peerId, control: control) + } + } + } + + private func getContext(peerId: PeerId, key: PeerChannelMemberContextKey, requestUpdate: Bool, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { + assert(Queue.mainQueue().isCurrent()) + if let (disposable, control) = self.impl.syncWith({ impl in + return impl.getContext(peerId: peerId, key: key, requestUpdate: requestUpdate, updated: updated) + }) { + return (disposable, control) + } else { + return (EmptyDisposable, nil) + } + } + + func transferOwnership(peerId: PeerId, memberId: PeerId, password: String) -> Signal { + return engine.peers.updateChannelOwnership(channelId: peerId, memberId: memberId, password: password) + |> map(Optional.init) + |> deliverOnMainQueue + |> beforeNext { [weak self] results in + if let strongSelf = self, let results = results { + strongSelf.impl.with { impl in + for (contextPeerId, context) in impl.contexts { + if peerId == contextPeerId { + context.replayUpdates(results.map { ($0.0, $0.1, nil) }) + } + } + } + } + } + |> mapToSignal { _ -> Signal in + return .complete() + } + } + + + func externallyAdded(peerId: PeerId, participant: RenderedChannelParticipant) { + self.impl.with { impl in + for (contextPeerId, context) in impl.contexts { + if contextPeerId == peerId { + context.replayUpdates([(nil, participant, nil)]) + } + } + } + } + + func mentions(peerId: PeerId, threadMessageId: MessageId?, searchQuery: String? = nil, requestUpdate: Bool = true, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { + let key: PeerChannelMemberContextKey = .mentions(threadId: threadMessageId, query: searchQuery) + return self.getContext(peerId: peerId, key: key, requestUpdate: requestUpdate, updated: updated) + } + + func recent(peerId: PeerId, searchQuery: String? = nil, requestUpdate: Bool = true, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { + let key: PeerChannelMemberContextKey + if let searchQuery = searchQuery { + key = .recentSearch(searchQuery) + } else { + key = .recent + } + return self.getContext(peerId: peerId, key: key, requestUpdate: requestUpdate, updated: updated) + } + + func recentOnline(peerId: PeerId) -> Signal { + return Signal { [weak self] subscriber in + guard let strongSelf = self else { + subscriber.putNext(0) + subscriber.putCompletion() + return EmptyDisposable + } + let disposable = strongSelf.impl.syncWith({ impl -> Disposable in + return impl.recentOnline(peerId: peerId, updated: { value in + subscriber.putNext(value) + }) + }) + return disposable ?? EmptyDisposable + } + |> runOn(Queue.mainQueue()) + } + + + func recentOnlineSmall(peerId: PeerId) -> Signal { + let account = self.account + return Signal { [weak self] subscriber in + guard let strongSelf = self else { + return EmptyDisposable + } + var previousIds: Set? + let statusesDisposable = MetaDisposable() + let disposableAndControl = self?.recent(peerId: peerId, updated: { state in + var idList: [PeerId] = [] + for item in state.list { + idList.append(item.peer.id) + if idList.count >= 200 { + break + } + } + let updatedIds = Set(idList) + if previousIds != updatedIds { + previousIds = updatedIds + let key: PostboxViewKey = .peerPresences(peerIds: updatedIds) + statusesDisposable.set((strongSelf.account.postbox.combinedView(keys: [key]) + |> map { view -> Int32 in + var count: Int32 = 0 + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + if let presences = (view.views[key] as? PeerPresencesView)?.presences { + for (_, presence) in presences { + if let presence = presence as? TelegramUserPresence { + let networkTime = account.network.globalTime > 0 ? account.network.globalTime - timestamp : 0 + + let relativeStatus = relativeUserPresenceStatus(presence, timeDifference: networkTime, relativeTo: Int32(timestamp)) + sw: switch relativeStatus { + case let .online(at: until): + if until > Int32(timestamp) { + count += 1 + } + default: + break sw + } + } + } + } + return count + } + |> distinctUntilChanged + |> deliverOnMainQueue).start(next: { count in + subscriber.putNext(count) + })) + } + }) + return ActionDisposable { + disposableAndControl?.0.dispose() + statusesDisposable.dispose() + } + } + |> runOn(Queue.mainQueue()) + + } + + func admins(peerId: PeerId, searchQuery: String? = nil, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { + return self.getContext(peerId: peerId, key: .admins(searchQuery), requestUpdate: true, updated: updated) + } + + func restricted(peerId: PeerId, searchQuery: String? = nil, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { + return self.getContext(peerId: peerId, key: .restricted(searchQuery), requestUpdate: true, updated: updated) + } + + func banned(peerId: PeerId, searchQuery: String? = nil, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { + return self.getContext(peerId: peerId, key: .banned(searchQuery), requestUpdate: true, updated: updated) + } + + func restrictedAndBanned(peerId: PeerId, searchQuery: String? = nil, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) { + return self.getContext(peerId: peerId, key: .restrictedAndBanned(searchQuery), requestUpdate: true, updated: updated) + } + + func updateMemberBannedRights(peerId: PeerId, memberId: PeerId, bannedRights: TelegramChatBannedRights?) -> Signal { + return engine.peers.updateChannelMemberBannedRights(peerId: peerId, memberId: memberId, rights: bannedRights) + |> deliverOnMainQueue + |> beforeNext { [weak self] (previous, updated, isMember) in + if let strongSelf = self { + strongSelf.impl.with { impl in + for (contextPeerId, context) in impl.contexts { + if peerId == contextPeerId { + context.replayUpdates([(previous, updated, isMember)]) + } + } + } + } + } + |> mapToSignal { _ -> Signal in + return .complete() + } + } + + func updateMemberAdminRights(peerId: PeerId, memberId: PeerId, adminRights: TelegramChatAdminRights?, rank: String?) -> Signal { + return engine.peers.updateChannelAdminRights(peerId: peerId, adminId: memberId, rights: adminRights, rank: rank) + |> map(Optional.init) + |> `catch` { _ -> Signal<(ChannelParticipant?, RenderedChannelParticipant)?, NoError> in + return .single(nil) + } + |> deliverOnMainQueue + |> beforeNext { [weak self] result in + if let strongSelf = self, let (previous, updated) = result { + strongSelf.impl.with { impl in + for (contextPeerId, context) in impl.contexts { + if peerId == contextPeerId { + context.replayUpdates([(previous, updated, nil)]) + } + } + } + } + } + |> mapToSignal { _ -> Signal in + return .complete() + } + } + + func addMember(peerId: PeerId, memberId: PeerId) -> Signal { + return engine.peers.addChannelMember(peerId: peerId, memberId: memberId) + |> map(Optional.init) + |> `catch` { _ -> Signal<(ChannelParticipant?, RenderedChannelParticipant)?, NoError> in + return .single(nil) + } + |> deliverOnMainQueue + |> beforeNext { [weak self] result in + if let strongSelf = self, let (previous, updated) = result { + strongSelf.impl.with { impl in + for (contextPeerId, context) in impl.contexts { + if peerId == contextPeerId { + context.replayUpdates([(previous, updated, nil)]) + } + } + } + } + } + |> mapToSignal { _ -> Signal in + return .complete() + } + } + + func addMembers(peerId: PeerId, memberIds: [PeerId]) -> Signal { + let signals: [Signal<(ChannelParticipant?, RenderedChannelParticipant)?, AddChannelMemberError>] = memberIds.map({ memberId in + return engine.peers.addChannelMember(peerId: peerId, memberId: memberId) + |> map(Optional.init) + |> `catch` { error -> Signal<(ChannelParticipant?, RenderedChannelParticipant)?, AddChannelMemberError> in + if memberIds.count == 1 { + return .fail(error) + } else { + return .single(nil) + } + } + }) + return combineLatest(signals) + |> deliverOnMainQueue + |> beforeNext { [weak self] results in + if let strongSelf = self { + strongSelf.impl.with { impl in + for result in results { + if let (previous, updated) = result { + for (contextPeerId, context) in impl.contexts { + if peerId == contextPeerId { + context.replayUpdates([(previous, updated, nil)]) + } + } + } + } + } + } + } + |> mapToSignal { _ -> Signal in + return .complete() + } + } + + func replyThread(account: Account, messageId: MessageId) -> Signal { + return .complete() + } + + +} diff --git a/Telegram-Mac/PeerEmptyHolderItem.swift b/Telegram-Mac/PeerEmptyHolderItem.swift new file mode 100644 index 0000000000..65e0923b46 --- /dev/null +++ b/Telegram-Mac/PeerEmptyHolderItem.swift @@ -0,0 +1,108 @@ +// +// PeerEmptyHolderItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 17.01.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +class PeerEmptyHolderItem: GeneralRowItem { + fileprivate let photoSize: NSSize + init(_ initialSize: NSSize, stableId: AnyHashable, height: CGFloat, photoSize: NSSize, viewType: GeneralViewType) { + self.photoSize = photoSize + super.init(initialSize, height: height, stableId: stableId, viewType: viewType) + } + + override func viewClass() -> AnyClass { + return PeerEmptyHolderView.self + } +} + +class PeerEmptyHolderView : TableRowView { + private let containerView = GeneralRowContainerView(frame: NSZeroRect) + private let photoView: View = View() + private let firstNameView = View() + private let lastNameView = View() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + containerView.addSubview(firstNameView) + containerView.addSubview(lastNameView) + containerView.addSubview(photoView) + addSubview(containerView) + + firstNameView.setFrameSize(NSMakeSize(50, 10)) + lastNameView.setFrameSize(NSMakeSize(50, 10)) + + firstNameView.layer?.cornerRadius = 5 + lastNameView.layer?.cornerRadius = 5 + } + + override func viewDidMoveToWindow() { + + } + + override var backdorColor: NSColor { + return theme.colors.background + } + + override func updateColors() { + if let item = item as? GeneralRowItem { + containerView.background = backdorColor + backgroundColor = item.viewType.rowBackground + photoView.backgroundColor = theme.colors.grayBackground + firstNameView.backgroundColor = theme.colors.grayBackground + lastNameView.backgroundColor = theme.colors.grayBackground + } + } + + override func layout() { + super.layout() + + if let item = item as? PeerEmptyHolderItem { + switch item.viewType { + case .legacy: + containerView.frame = bounds + case .modern: + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) + } + self.containerView.setCorners(item.viewType.corners) + + + photoView.centerY(x: 10) + + firstNameView.centerY(x: photoView.frame.maxX + 10) + lastNameView.centerY(x: firstNameView.frame.maxX + 10) + } + } + + deinit { + } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + + if let item = item as? PeerEmptyHolderItem { + let contentRect: NSRect + switch item.viewType { + case .legacy: + contentRect = bounds + case .modern: + contentRect = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) + } + self.containerView.change(size: contentRect.size, animated: animated, corners: item.viewType.corners) + self.containerView.change(pos: contentRect.origin, animated: animated) + + photoView.setFrameSize(item.photoSize) + photoView.layer?.cornerRadius = item.photoSize.height / 2 + needsLayout = true + } + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/PeerInfoController.swift b/Telegram-Mac/PeerInfoController.swift index 45f7b1e684..de7ea70d76 100644 --- a/Telegram-Mac/PeerInfoController.swift +++ b/Telegram-Mac/PeerInfoController.swift @@ -8,63 +8,28 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac +import TelegramCore -class PeerInfoTitleBarView : TitledBarView { - private var search:ImageButton = ImageButton() - init(controller: ViewController, title:NSAttributedString, handler:@escaping() ->Void) { - super.init(controller: controller, title) - search.set(handler: { _ in - handler() - }, for: .Click) - addSubview(search) - updateLocalizationAndTheme() - } - - func updateSearchVisibility(_ visible: Bool) { - search.isHidden = !visible - } - - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - search.set(image: theme.icons.chatSearch, for: .Normal) - search.sizeToFit() - backgroundColor = theme.colors.background - needsLayout = true - } - - override func layout() { - super.layout() - search.centerY(x: frame.width - search.frame.width) - } - - - required init(frame frameRect: NSRect) { - fatalError("init(frame:) has not been implemented") - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} +import SwiftSignalKit +import Postbox class PeerInfoArguments { let peerId:PeerId - let account:Account + let context: AccountContext + let isAd: Bool let pushViewController:(ViewController) -> Void let pullNavigation:()->NavigationViewController? + let mediaController: ()->PeerMediaController? + - private let peerInfoDisposable = MetaDisposable() private let toggleNotificationsDisposable = MetaDisposable() private let deleteDisposable = MetaDisposable() private let _statePromise = Promise() - var statePromise:Signal { + var statePromise:Signal { return _statePromise.get() } @@ -74,13 +39,17 @@ class PeerInfoArguments { return value.modify {$0} } - func updateInfoState(_ f: (PeerInfoState) -> PeerInfoState) -> Void { - _statePromise.set(.single(value.modify({f($0)}))) + _statePromise.set(.single(value.modify(f))) } - func updateEditable(_ editable:Bool, peerView:PeerView) { - + func copy(_ string: String) { + copyToClipboard(string) + pullNavigation()?.controller.show(toaster: ControllerToaster(text: L10n.shareLinkCopied)) + } + + func updateEditable(_ editable:Bool, peerView:PeerView, controller: PeerInfoController) -> Bool { + return true } func dismissEdition() { @@ -88,49 +57,55 @@ class PeerInfoArguments { } func peerInfo(_ peerId:PeerId) { - peerInfoDisposable.set((account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue).start(next: { [weak self] peer in - if let strongSelf = self { - strongSelf.pushViewController(PeerInfoController(account: strongSelf.account, peer: peer)) - } - })) + pushViewController(PeerInfoController(context: context, peerId: peerId)) } - func peerChat(_ peerId:PeerId) { - pushViewController(ChatController(account: account, peerId: peerId)) + func peerChat(_ peerId:PeerId, postId: MessageId? = nil) { + pushViewController(ChatAdditionController(context: context, chatLocation: .peer(peerId), messageId: postId)) } - func toggleNotifications() { - toggleNotificationsDisposable.set(togglePeerMuted(account: account, peerId: peerId).start()) + func toggleNotifications(_ currentlyMuted: Bool) { + + toggleNotificationsDisposable.set(context.engine.peers.togglePeerMuted(peerId: peerId).start()) + + pullNavigation()?.controller.show(toaster: ControllerToaster.init(text: currentlyMuted ? L10n.toastUnmuted : L10n.toastMuted)) } func delete() { - let account = self.account + let context = self.context let peerId = self.peerId - - deleteDisposable.set((removeChatInteractively(account: account, peerId:peerId) |> deliverOnMainQueue).start(next: { [weak self] success in - if success { - self?.pullNavigation()?.close() - } - })) + let isEditing = (state as? GroupInfoState)?.editingState != nil || (state as? ChannelInfoState)?.editingState != nil + + let signal = context.account.postbox.peerView(id: peerId) |> take(1) |> mapToSignal { view -> Signal in + return removeChatInteractively(context: context, peerId: peerId, userId: peerViewMainPeer(view)?.id, deleteGroup: isEditing && peerViewMainPeer(view)?.groupAccess.isCreator == true) + } |> deliverOnMainQueue + + deleteDisposable.set(signal.start(completed: { [weak self] in + self?.pullNavigation()?.close() + })) } func sharedMedia() { - pushViewController(PeerMediaController(account: account, peerId: peerId, tagMask: .photoOrVideo)) + if let controller = self.mediaController() { + pushViewController(controller) + } } - init(account:Account, peerId:PeerId, state:PeerInfoState, pushViewController:@escaping(ViewController)->Void, pullNavigation:@escaping()->NavigationViewController?) { + init(context: AccountContext, peerId:PeerId, state:PeerInfoState, isAd: Bool, pushViewController:@escaping(ViewController)->Void, pullNavigation:@escaping()->NavigationViewController?, mediaController: @escaping()->PeerMediaController?) { self.value = Atomic(value: state) _statePromise.set(.single(state)) - self.account = account + self.context = context self.peerId = peerId + self.isAd = isAd self.pushViewController = pushViewController self.pullNavigation = pullNavigation + self.mediaController = mediaController } + deinit { toggleNotificationsDisposable.dispose() - peerInfoDisposable.dispose() deleteDisposable.dispose() } } @@ -183,16 +158,66 @@ private struct PeerInfoSortableEntry: Identifiable, Comparable { } } +struct PeerMediaTabsData : Equatable { + let collections:[PeerMediaCollectionMode] + let loaded: Bool +} -fileprivate func prepareEntries(from:[AppearanceWrapperEntry]?, to:[AppearanceWrapperEntry], account:Account, initialSize:NSSize, peerId:PeerId, arguments: PeerInfoArguments, animated:Bool) -> TableUpdateTransition { - +fileprivate func prepareEntries(from:[AppearanceWrapperEntry]?, to:[AppearanceWrapperEntry], account:Account, initialSize:NSSize, peerId:PeerId, arguments: PeerInfoArguments, animated:Bool) -> Signal { - let (deleted,inserted, updated) = proccessEntries(from, right: to, { (peerInfoSortableEntry) -> TableRowItem in - return peerInfoSortableEntry.entry.entry.item(initialSize: initialSize, arguments: arguments) - }) + return Signal { subscriber in + + var cancelled = false + + if Thread.isMainThread { + var initialIndex:Int = 0 + var height:CGFloat = 0 + var firstInsertion:[(Int, TableRowItem)] = [] + let entries = Array(to) + + let index:Int = 0 + + for i in index ..< entries.count { + let item = entries[i].entry.entry.item(initialSize: initialSize, arguments: arguments) + height += item.height + firstInsertion.append((i, item)) + if initialSize.height < height { + break + } + } + + + initialIndex = firstInsertion.count + subscriber.putNext(TableUpdateTransition(deleted: [], inserted: firstInsertion, updated: [], state: .none(nil))) + + prepareQueue.async { + if !cancelled { + var insertions:[(Int, TableRowItem)] = [] + let updates:[(Int, TableRowItem)] = [] + + for i in initialIndex ..< entries.count { + let item:TableRowItem + item = entries[i].entry.entry.item(initialSize: initialSize, arguments: arguments) + insertions.append((i, item)) + } + subscriber.putNext(TableUpdateTransition(deleted: [], inserted: insertions, updated: updates, state: .none(nil))) + subscriber.putCompletion() + } + } + } else { + let (deleted,inserted, updated) = proccessEntriesWithoutReverse(from, right: to, { (peerInfoSortableEntry) -> TableRowItem in + return peerInfoSortableEntry.entry.entry.item(initialSize: initialSize, arguments: arguments) + }) + subscriber.putNext(TableUpdateTransition(deleted: deleted, inserted: inserted, updated: updated, animated: animated, state: animated ? .none(nil) : .saveVisible(.lower), grouping: true, animateVisibleOnly: false)) + subscriber.putCompletion() + } + + return ActionDisposable { + cancelled = true + } + } - return TableUpdateTransition(deleted: deleted, inserted: inserted, updated: updated, animated:animated, state: animated ? .none(nil) : .saveVisible(.lower)) } @@ -201,87 +226,79 @@ class PeerInfoController: EditableViewController { private let updatedChannelParticipants:MetaDisposable = MetaDisposable() let peerId:PeerId - private let peerViewDisposable:MetaDisposable = MetaDisposable() - private let peerAtomic:Atomic - private let peerView:Atomic = Atomic(value: nil) + private let arguments:Promise = Promise() + + private let peerView:Atomic = Atomic(value: nil) private var _groupArguments:GroupInfoArguments! private var _userArguments:UserInfoArguments! private var _channelArguments:ChannelInfoArguments! + + private let peerInputActivitiesDisposable = MetaDisposable() + + private var argumentsAction: DisposableSet = DisposableSet() var disposable:MetaDisposable = MetaDisposable() - init(account:Account, peer:Peer) { - peerAtomic = Atomic(value: peer) - self.peerId = peer.id - super.init(account) + private let mediaController: PeerMediaController + + init(context: AccountContext, peerId:PeerId, isAd: Bool = false) { + self.peerId = peerId + self.mediaController = PeerMediaController(context: context, peerId: peerId, isProfileIntended: true) + super.init(context) let pushViewController:(ViewController) -> Void = { [weak self] controller in self?.navigationController?.push(controller) } - _groupArguments = GroupInfoArguments(account: account, peerId: peerId, state: GroupInfoState(), pushViewController: pushViewController, pullNavigation:{ [weak self] () -> NavigationViewController? in + _groupArguments = GroupInfoArguments(context: context, peerId: peerId, state: GroupInfoState(), isAd: isAd, pushViewController: pushViewController, pullNavigation:{ [weak self] () -> NavigationViewController? in return self?.navigationController + }, mediaController: { [weak self] in + return self?.mediaController }) - _userArguments = UserInfoArguments(account: account, peerId: peerId, state: UserInfoState(), pushViewController: pushViewController, pullNavigation:{ [weak self] () -> NavigationViewController? in - return self?.navigationController + _userArguments = UserInfoArguments(context: context, peerId: peerId, state: UserInfoState(), isAd: isAd, pushViewController: pushViewController, pullNavigation:{ [weak self] () -> NavigationViewController? in + return self?.navigationController + }, mediaController: { [weak self] in + return self?.mediaController }) - _channelArguments = ChannelInfoArguments(account: account, peerId: peerId, state: ChannelInfoState(), pushViewController: pushViewController, pullNavigation:{ [weak self] () -> NavigationViewController? in + _channelArguments = ChannelInfoArguments(context: context, peerId: peerId, state: ChannelInfoState(), isAd: isAd, pushViewController: pushViewController, pullNavigation:{ [weak self] () -> NavigationViewController? in return self?.navigationController + }, mediaController: { [weak self] in + return self?.mediaController }) - - } - - override func getCenterBarViewOnce() -> TitledBarView { - return PeerInfoTitleBarView(controller: self, title:.initialize(string: defaultBarTitle, color: theme.colors.text, font: .medium(.title)), handler: { [weak self] in - self?.searchSupergroupUsers() - }) - } - - func searchSupergroupUsers() { - _ = (selectModalPeers(account: account, title: "", behavior: SelectChannelMembersBehavior(peerId: peerId, limit: 1, settings: [])) |> deliverOnMainQueue |> map {$0.first}).start(next: { [weak self] peerId in - if let peerId = peerId { - self?._channelArguments.peerInfo(peerId) - } - }) + } deinit { disposable.dispose() updatedChannelParticipants.dispose() + peerInputActivitiesDisposable.dispose() + argumentsAction.dispose() window?.removeAllHandlers(for: self) } - private var arguments:PeerInfoArguments { - let peer = peerAtomic.modify({$0}) - if peer.isGroup || peer.isSupergroup { - return _groupArguments - } else if peer.isChannel { - return _channelArguments - } else { - return _userArguments - } - } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - window?.set(handler: { [weak self] () -> KeyHandlerResult in + window?.set(handler: { [weak self] _ -> KeyHandlerResult in if let strongSelf = self { return strongSelf.returnKeyAction() } return .rejected }, with: self, for: .Return, priority: .high) - window?.set(handler: { [weak self] () -> KeyHandlerResult in + window?.set(handler: { [weak self] _ -> KeyHandlerResult in if let strongSelf = self { return strongSelf.returnKeyAction() } return .rejected }, with: self, for: .Return, priority: .high, modifierFlags: [.command]) + } + override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) window?.removeAllHandlers(for: self) @@ -303,38 +320,108 @@ class PeerInfoController: EditableViewController { return .invokeNext } - + override func viewDidLoad() -> Void { super.viewDidLoad() + self.genericView.getBackgroundColor = { + theme.colors.listBackground + } + + let previousEntries = Atomic<[AppearanceWrapperEntry]?>(value: nil) - let account = self.account + let context = self.context let peerId = self.peerId let initialSize = atomicSize - let arguments = self.arguments + let onMainQueue: Atomic = Atomic(value: true) - let transition = combineLatest(account.viewTracker.peerView(peerId), arguments.statePromise |> distinctUntilChanged, appearanceSignal) |> deliverOn(prepareQueue) - |> map { [weak peerAtomic] view, state, appearance -> (PeerView, TableUpdateTransition) in - - let entries:[AppearanceWrapperEntry] = peerInfoEntries(view: view, arguments: arguments).map({PeerInfoSortableEntry(entry: $0)}).map({AppearanceWrapperEntry(entry: $0, appearance: appearance)}) - - let previous = previousEntries.swap(entries) - - _ = peerAtomic?.modify{ (previous) -> Peer in - if let peer = peerViewMainPeer(view) { - return peer - } else { - return previous - } + mediaController.navigationController = self.navigationController + mediaController._frameRect = bounds + mediaController.bar = .init(height: 0) + + mediaController.loadViewIfNeeded() + + let inputActivity = context.account.peerInputActivities(peerId: .init(peerId: peerId, category: .global)) + |> map { activities -> [PeerId : PeerInputActivity] in + return activities.reduce([:], { (current, activity) -> [PeerId : PeerInputActivity] in + var current = current + current[activity.0] = activity.1 + return current + }) + } + + let inputActivityState: Promise<[PeerId : PeerInputActivity]> = Promise([:]) + + + arguments.set(context.account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue |> mapToSignal { [weak self] peer in + guard let `self` = self else {return .never()} + + if peer.isGroup || peer.isSupergroup { + inputActivityState.set(inputActivity) + } + + if peer.isGroup || peer.isSupergroup { + return .single(self._groupArguments) + } else if peer.isChannel { + return .single(self._channelArguments) + } else { + return .single(self._userArguments) + } + }) + + let actionsDisposable = DisposableSet() + + var loadMoreControl: PeerChannelMemberCategoryControl? + + let channelMembersPromise = Promise<[RenderedChannelParticipant]>() + if peerId.namespace == Namespaces.Peer.CloudChannel { + let (disposable, control) = context.peerChannelMemberCategoriesContextsManager.recent(peerId: peerId, updated: { state in + channelMembersPromise.set(.single(state.list)) + }) + loadMoreControl = control + actionsDisposable.add(disposable) + } else { + channelMembersPromise.set(.single([])) + } + + + let mediaTabsData: Signal = mediaController.tabsValue + let mediaReady = mediaController.ready.get() |> take(1) + + + + + + let transition: Signal<(PeerView, TableUpdateTransition), NoError> = arguments.get() |> mapToSignal { arguments in + + let inviteLinksCount: Signal + if let arguments = arguments as? GroupInfoArguments { + inviteLinksCount = arguments.linksManager.state |> map { + $0.effectiveCount } + } else if let arguments = arguments as? ChannelInfoArguments { + inviteLinksCount = arguments.linksManager.state |> map { + $0.effectiveCount + } + } else { + inviteLinksCount = .single(0) + } + + return combineLatest(queue: prepareQueue, context.account.viewTracker.peerView(peerId, updateData: true), arguments.statePromise, appearanceSignal, inputActivityState.get(), channelMembersPromise.get(), mediaTabsData, mediaReady, inviteLinksCount) + |> mapToQueue { view, state, appearance, inputActivities, channelMembers, mediaTabsData, _, inviteLinksCount -> Signal<(PeerView, TableUpdateTransition), NoError> in + + let entries:[AppearanceWrapperEntry] = peerInfoEntries(view: view, arguments: arguments, inputActivities: inputActivities, channelMembers: channelMembers, mediaTabsData: mediaTabsData, inviteLinksCount: inviteLinksCount).map({PeerInfoSortableEntry(entry: $0)}).map({AppearanceWrapperEntry(entry: $0, appearance: appearance)}) + let previous = previousEntries.swap(entries) + return prepareEntries(from: previous, to: entries, account: context.account, initialSize: initialSize.modify({$0}), peerId: peerId, arguments:arguments, animated: previous != nil) |> runOn(onMainQueue.swap(false) ? .mainQueue() : prepareQueue) |> map { (view, $0) } + + } |> deliverOnMainQueue + } |> afterDisposed { + actionsDisposable.dispose() + } - return (view, prepareEntries(from: previous, to: entries, account: account, initialSize: initialSize.modify({$0}), peerId: peerId, arguments:arguments, animated: previous != nil)) - - } |> deliverOnMainQueue - disposable.set(transition.start(next: { [weak self] (peerView, transition) in - + _ = self?.peerView.swap(peerView) let editable:Bool @@ -342,34 +429,45 @@ class PeerInfoController: EditableViewController { if let peer = peer as? TelegramChannel { switch peer.info { case .broadcast: - editable = peer.hasAdminRights(.canChangeInfo) + editable = peer.adminRights != nil || peer.flags.contains(.isCreator) case .group: - editable = true //peer.adminRights != nil || peer.flags.contains(.isCreator) + editable = peer.adminRights != nil || peer.flags.contains(.isCreator) } - - } else if let peer = peer as? TelegramGroup { - editable = peer.role == .creator || peer.role == .admin || !peer.flags.contains(.adminsEnabled) - } else if peer is TelegramUser { - editable = peerView.peerIsContact && account.peerId != peer.id + } else if let group = peer as? TelegramGroup { + switch group.role { + case .admin, .creator: + editable = true + default: + editable = group.groupAccess.canEditGroupInfo || group.groupAccess.canEditMembers + } + } else if peer is TelegramUser, !peer.isBot, peerView.peerIsContact { + editable = context.account.peerId != peer.id } else { editable = false } - (self?.centerBarView as? PeerInfoTitleBarView)?.updateSearchVisibility(peer.isSupergroup) } else { editable = false } self?.set(editable: editable) - self?.readyOnce() self?.genericView.merge(with:transition) + self?.readyOnce() + })) - if peerId.namespace == Namespaces.Peer.CloudChannel { - let fetchParticipants = account.viewTracker.peerView(peerId) |> filter {$0.cachedData != nil} |> take(1) |> deliverOnMainQueue |> mapToSignal {_ -> Signal in - return account.viewTracker.updatedCachedChannelParticipants(peerId, forceImmediateUpdate: true) + + genericView.setScrollHandler { position in + if let loadMoreControl = loadMoreControl { + switch position.direction { + case .bottom: + context.peerChannelMemberCategoriesContextsManager.loadMore(peerId: peerId, control: loadMoreControl) + default: + break + } } - updatedChannelParticipants.set(fetchParticipants.start()) + } + } @@ -380,19 +478,38 @@ class PeerInfoController: EditableViewController { return .rejected } - + func updateArguments(_ f:@escaping(PeerInfoArguments) -> Void) { + argumentsAction.add((arguments.get() |> take(1)).start(next: { arguments in + f(arguments) + })) + } override func update(with state: ViewControllerState) { - super.update(with: state) - if let peerView = peerView.modify({$0}) { - self.arguments.updateEditable(state == .Edit, peerView: peerView) + + if let peerView = peerView.with ({$0}) { + updateArguments({ [weak self] arguments in + guard let `self` = self else { + return + } + let updateState = arguments.updateEditable(state == .Edit, peerView: peerView, controller: self) + self.genericView.scroll(to: .up(true)) + + if updateState { + self.applyState(state) + } + }) } } + private func applyState(_ state: ViewControllerState) { + super.update(with: state) + } override func escapeKeyAction() -> KeyHandlerResult { if state == .Edit { - arguments.dismissEdition() + updateArguments({ arguments in + arguments.dismissEdition() + }) state = .Normal return .invoked } diff --git a/Telegram-Mac/PeerInfoEntries.swift b/Telegram-Mac/PeerInfoEntries.swift index a685e1fae5..c2ce3387d2 100644 --- a/Telegram-Mac/PeerInfoEntries.swift +++ b/Telegram-Mac/PeerInfoEntries.swift @@ -7,9 +7,10 @@ // import Cocoa -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac +import TelegramCore + +import SwiftSignalKit +import Postbox import TGUIKit @@ -51,18 +52,22 @@ struct IntPeerInfoEntryStableId: PeerInfoEntryStableId { final class PeerInfoUpdatingPhotoState : Equatable { let progress:Float let cancel:()->Void - - init(progress: Float, cancel: @escaping()->Void) { + let image: CGImage? + init(progress: Float, image: CGImage? = nil, cancel: @escaping()->Void) { self.progress = progress self.cancel = cancel + self.image = image + } + func withUpdatedImage(_ image: CGImage) -> PeerInfoUpdatingPhotoState { + return PeerInfoUpdatingPhotoState(progress: progress, image: image, cancel: self.cancel) } func withUpdatedProgress(_ progress: Float) -> PeerInfoUpdatingPhotoState { - return PeerInfoUpdatingPhotoState(progress: progress, cancel: self.cancel) + return PeerInfoUpdatingPhotoState(progress: progress, image: self.image, cancel: self.cancel) } static func ==(lhs:PeerInfoUpdatingPhotoState, rhs: PeerInfoUpdatingPhotoState) -> Bool { - return lhs.progress == rhs.progress + return lhs.progress == rhs.progress && lhs.image == rhs.image } } @@ -75,18 +80,18 @@ protocol PeerInfoEntry { } -func peerInfoEntries(view: PeerView, arguments: PeerInfoArguments) -> [PeerInfoEntry] { +func peerInfoEntries(view: PeerView, arguments: PeerInfoArguments, inputActivities: [PeerId: PeerInputActivity], channelMembers: [RenderedChannelParticipant], mediaTabsData: PeerMediaTabsData, inviteLinksCount: Int32) -> [PeerInfoEntry] { if peerViewMainPeer(view) is TelegramUser { - return userInfoEntries(view: view, arguments: arguments) + return userInfoEntries(view: view, arguments: arguments, mediaTabsData: mediaTabsData) } else if let channel = peerViewMainPeer(view) as? TelegramChannel { switch channel.info { case .broadcast: - return channelInfoEntries(view: view, arguments: arguments) + return channelInfoEntries(view: view, arguments: arguments, mediaTabsData: mediaTabsData, inviteLinksCount: inviteLinksCount) case .group: - return groupInfoEntries(view: view, arguments: arguments) + return groupInfoEntries(view: view, arguments: arguments, inputActivities: inputActivities, channelMembers: channelMembers, mediaTabsData: mediaTabsData, inviteLinksCount: inviteLinksCount) } } else if peerViewMainPeer(view) is TelegramGroup { - return groupInfoEntries(view: view, arguments: arguments) + return groupInfoEntries(view: view, arguments: arguments, inputActivities: inputActivities, mediaTabsData: mediaTabsData, inviteLinksCount: inviteLinksCount) } return [] } diff --git a/Telegram-Mac/PeerInfoHeadItem.swift b/Telegram-Mac/PeerInfoHeadItem.swift new file mode 100644 index 0000000000..4b98ba59a4 --- /dev/null +++ b/Telegram-Mac/PeerInfoHeadItem.swift @@ -0,0 +1,1044 @@ +// +// PeerInfoHeadItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 01/04/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import Postbox +import SwiftSignalKit +import TelegramCore + + + +fileprivate final class ActionButton : Control { + fileprivate let imageView: ImageView = ImageView() + fileprivate let textView: TextView = TextView() + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(imageView) + addSubview(textView) + + self.imageView.animates = true + imageView.isEventLess = true + textView.isEventLess = true + textView.userInteractionEnabled = false + textView.isSelectable = false + set(handler: { control in + control.change(opacity: 0.8, animated: true) + }, for: .Highlight) + + set(handler: { control in + control.change(opacity: 1.0, animated: true) + }, for: .Normal) + + set(handler: { control in + control.change(opacity: 1.0, animated: true) + }, for: .Hover) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func updateAndLayout(item: ActionItem, theme: PresentationTheme) { + self.imageView.image = item.image + self.imageView.sizeToFit() + self.textView.update(item.textLayout) + + self.backgroundColor = theme.colors.background + self.layer?.cornerRadius = 10 + + self.removeAllHandlers() + if let subItems = item.subItems { + self.set(handler: { control in + showPopover(for: control, with: SPopoverViewController(items: subItems.map { SPopoverItem($0.text, $0.action, nil, $0.destruct ? theme.colors.redUI : theme.colors.text) }, visibility: 10), edge: .maxY, inset: NSMakePoint(0, -60)) + }, for: .Down) + } else { + self.set(handler: { [weak item] _ in + item?.action() + }, for: .Click) + } + + + needsLayout = true + + } + + override func layout() { + super.layout() + imageView.centerX(y: 5) + textView.centerX(y: frame.height - textView.frame.height - 11) + } +} + +extension TelegramPeerPhoto : Equatable { + public static func ==(lhs: TelegramPeerPhoto, rhs: TelegramPeerPhoto) -> Bool { + if lhs.date != rhs.date { + return false + } + if !lhs.image.isEqual(to: rhs.image) { + return false + } + if lhs.index != rhs.index { + return false + } + if lhs.messageId != rhs.messageId { + return false + } + if lhs.reference != rhs.reference { + return false + } + if lhs.totalCount != rhs.totalCount { + return false + } + return true + } +} + +fileprivate let photoDimension:CGFloat = 120 +fileprivate let actionItemWidth: CGFloat = 135 +fileprivate let actionItemInsetWidth: CGFloat = 19 + +private struct SubActionItem { + let text: String + let destruct: Bool + let action:()->Void + init(text: String, destruct: Bool = false, action:@escaping()->Void) { + self.text = text + self.action = action + self.destruct = destruct + } +} + +private final class ActionItem { + let text: String + let destruct: Bool + let image: CGImage + let action:()->Void + + let subItems:[SubActionItem]? + + + let textLayout: TextViewLayout + let size: NSSize + + init(text: String, image: CGImage, destruct: Bool = false, action: @escaping()->Void, subItems:[SubActionItem]? = nil) { + self.text = text + self.image = image + self.action = action + self.subItems = subItems + self.destruct = destruct + self.textLayout = TextViewLayout(.initialize(string: text, color: theme.colors.accent, font: .normal(.text)), alignment: .center) + self.textLayout.measure(width: actionItemWidth) + + self.size = NSMakeSize(actionItemWidth, image.backingSize.height + textLayout.layoutSize.height + 10) + } + +} + +private func actionItems(item: PeerInfoHeadItem, width: CGFloat, theme: TelegramPresentationTheme) -> [ActionItem] { + + var items:[ActionItem] = [] + + var rowItemsCount: Int = 1 + + while width - (actionItemWidth + actionItemInsetWidth) > ((actionItemWidth * CGFloat(rowItemsCount)) + (CGFloat(rowItemsCount - 1) * actionItemInsetWidth)) { + rowItemsCount += 1 + } + rowItemsCount = min(rowItemsCount, 4) + + + + if let peer = item.peer as? TelegramUser, let arguments = item.arguments as? UserInfoArguments { + if !(item.peerView.peers[item.peerView.peerId] is TelegramSecretChat) { + items.append(ActionItem(text: L10n.peerInfoActionMessage, image: theme.icons.profile_message, action: arguments.sendMessage)) + } + if peer.canCall && peer.id != item.context.peerId, !isServicePeer(peer) && !peer.rawDisplayTitle.isEmpty { + if let cachedData = item.peerView.cachedData as? CachedUserData, cachedData.voiceCallsAvailable { + items.append(ActionItem(text: L10n.peerInfoActionCall, image: theme.icons.profile_call, action: { + arguments.call(false) + })) + } + } + + let videoConfiguration: VideoCallsConfiguration = VideoCallsConfiguration(appConfiguration: item.context.appConfiguration) + + let isVideoPossible: Bool + switch videoConfiguration.videoCallsSupport { + case .disabled: + isVideoPossible = false + case .full: + isVideoPossible = true + case .onlyVideo: + isVideoPossible = true + } + + + + + if peer.canCall && peer.id != item.context.peerId, !isServicePeer(peer) && !peer.rawDisplayTitle.isEmpty, isVideoPossible { + if let cachedData = item.peerView.cachedData as? CachedUserData, cachedData.videoCallsAvailable { + items.append(ActionItem(text: L10n.peerInfoActionVideoCall, image: theme.icons.profile_video_call, action: { + arguments.call(true) + })) + } + } + let value = item.peerView.notificationSettings?.isRemovedFromTotalUnreadCount(default: false) ?? false + items.append(ActionItem(text: value ? L10n.peerInfoActionUnmute : L10n.peerInfoActionMute, image: value ? theme.icons.profile_unmute : theme.icons.profile_mute, action: { + arguments.toggleNotifications(value) + })) + if !peer.isBot { + if !(item.peerView.peers[item.peerView.peerId] is TelegramSecretChat), arguments.context.peerId != peer.id, !isServicePeer(peer) && !peer.rawDisplayTitle.isEmpty { + items.append(ActionItem(text: L10n.peerInfoActionSecretChat, image: theme.icons.profile_secret_chat, action: arguments.startSecretChat)) + } + if peer.id != item.context.peerId, item.peerView.peerIsContact, peer.phone != nil { + items.append(ActionItem(text: L10n.peerInfoActionShare, image: theme.icons.profile_share, action: arguments.shareContact)) + } + if peer.id != item.context.peerId, let cachedData = item.peerView.cachedData as? CachedUserData, item.peerView.peerIsContact { + items.append(ActionItem(text: (!cachedData.isBlocked ? L10n.peerInfoBlockUser : L10n.peerInfoUnblockUser), image: !cachedData.isBlocked ? theme.icons.profile_block : theme.icons.profile_unblock, destruct: true, action: { + arguments.updateBlocked(peer: peer, !cachedData.isBlocked, false) + })) + } + } else if let botInfo = peer.botInfo { + + if let address = peer.addressName, !address.isEmpty { + items.append(ActionItem(text: L10n.peerInfoBotShare, image: theme.icons.profile_share, action: { + arguments.botShare(address) + })) + } + + if botInfo.flags.contains(.worksWithGroups) { + items.append(ActionItem(text: L10n.peerInfoBotAddToGroup, image: theme.icons.profile_more, action: arguments.botAddToGroup)) + } + + if let cachedData = item.peerView.cachedData as? CachedUserData, let botInfo = cachedData.botInfo { + for command in botInfo.commands { + if command.text == "settings" { + items.append(ActionItem(text: L10n.peerInfoBotSettings, image: theme.icons.profile_more, action: arguments.botSettings)) + } + if command.text == "help" { + items.append(ActionItem(text: L10n.peerInfoBotHelp, image: theme.icons.profile_more, action: arguments.botHelp)) + } + if command.text == "privacy" { + items.append(ActionItem(text: L10n.peerInfoBotPrivacy, image: theme.icons.profile_more, action: arguments.botPrivacy)) + } + } + items.append(ActionItem(text: !cachedData.isBlocked ? L10n.peerInfoStopBot : L10n.peerInfoRestartBot, image: theme.icons.profile_more, destruct: true, action: { + arguments.updateBlocked(peer: peer, !cachedData.isBlocked, true) + })) + } + } + + } else if let peer = item.peer, peer.isSupergroup || peer.isGroup, let arguments = item.arguments as? GroupInfoArguments { + let access = peer.groupAccess + + if access.canAddMembers { + items.append(ActionItem(text: L10n.peerInfoActionAddMembers, image: theme.icons.profile_add_member, action: { + arguments.addMember(access.canCreateInviteLink) + })) + } + if let value = item.peerView.notificationSettings?.isRemovedFromTotalUnreadCount(default: false) { + items.append(ActionItem(text: value ? L10n.peerInfoActionUnmute : L10n.peerInfoActionMute, image: value ? theme.icons.profile_unmute : theme.icons.profile_mute, action: { + arguments.toggleNotifications(value) + })) + } + + + if let cachedData = item.peerView.cachedData as? CachedChannelData, let peer = peer as? TelegramChannel { + if peer.groupAccess.canMakeVoiceChat { + let isLiveStream = peer.isChannel || peer.flags.contains(.isGigagroup) + items.append(ActionItem(text: isLiveStream ? L10n.peerInfoActionLiveStream : L10n.peerInfoActionVoiceChat, image: theme.icons.profile_voice_chat, action: { + arguments.makeVoiceChat(cachedData.activeCall, callJoinPeerId: cachedData.callJoinPeerId) + })) + } + } else if let cachedData = item.peerView.cachedData as? CachedGroupData { + if peer.groupAccess.canMakeVoiceChat { + items.append(ActionItem(text: L10n.peerInfoActionVoiceChat, image: theme.icons.profile_voice_chat, action: { + arguments.makeVoiceChat(cachedData.activeCall, callJoinPeerId: cachedData.callJoinPeerId) + })) + } + } + + if let cachedData = item.peerView.cachedData as? CachedChannelData { + if cachedData.statsDatacenterId > 0, cachedData.flags.contains(.canViewStats) { + items.append(ActionItem(text: L10n.peerInfoActionStatistics, image: theme.icons.profile_stats, action: { + arguments.stats(cachedData.statsDatacenterId) + })) + } + } + if access.canReport { + items.append(ActionItem(text: L10n.peerInfoActionReport, image: theme.icons.profile_report, destruct: false, action: arguments.report)) + } + if let group = peer as? TelegramGroup { + if case .Member = group.membership { + items.append(ActionItem(text: L10n.peerInfoActionLeave, image: theme.icons.profile_leave, destruct: true, action: arguments.delete)) + } + } else if let group = peer as? TelegramChannel { + if case .member = group.participationStatus { + items.append(ActionItem(text: L10n.peerInfoActionLeave, image: theme.icons.profile_leave, destruct: true, action: arguments.delete)) + } + } + + + + } else if let peer = item.peer as? TelegramChannel, peer.isChannel, let arguments = item.arguments as? ChannelInfoArguments { + if let value = item.peerView.notificationSettings?.isRemovedFromTotalUnreadCount(default: false) { + items.append(ActionItem(text: value ? L10n.peerInfoActionUnmute : L10n.peerInfoActionMute, image: value ? theme.icons.profile_unmute : theme.icons.profile_mute, action: { + arguments.toggleNotifications(value) + })) + } + + + if let cachedData = item.peerView.cachedData as? CachedChannelData { + + switch cachedData.linkedDiscussionPeerId { + case let .known(peerId): + if let peerId = peerId { + items.append(ActionItem(text: L10n.peerInfoActionDiscussion, image: theme.icons.profile_message, action: { [weak arguments] in + arguments?.peerChat(peerId) + })) + } + default: + break + } + + if cachedData.statsDatacenterId > 0, cachedData.flags.contains(.canViewStats) { + items.append(ActionItem(text: L10n.peerInfoActionStatistics, image: theme.icons.profile_stats, action: { + arguments.stats(cachedData.statsDatacenterId) + })) + } + } + if let cachedData = item.peerView.cachedData as? CachedChannelData { + if peer.groupAccess.canMakeVoiceChat { + items.append(ActionItem(text: L10n.peerInfoActionVoiceChat, image: theme.icons.profile_voice_chat, action: { + arguments.makeVoiceChat(cachedData.activeCall, callJoinPeerId: cachedData.callJoinPeerId) + })) + } + } + if let address = peer.addressName, !address.isEmpty { + items.append(ActionItem(text: L10n.peerInfoActionShare, image: theme.icons.profile_share, action: arguments.share)) + } + + if peer.groupAccess.canReport { + items.append(ActionItem(text: L10n.peerInfoActionReport, image: theme.icons.profile_report, action: arguments.report)) + } + + + switch peer.participationStatus { + case .member: + items.append(ActionItem(text: L10n.peerInfoActionLeave, image: theme.icons.profile_leave, destruct: true, action: arguments.delete)) + default: + break + } + } + + + if items.count > rowItemsCount { + var subItems:[SubActionItem] = [] + while items.count > rowItemsCount - 1 { + let item = items.removeLast() + subItems.insert(SubActionItem(text: item.text, destruct: item.destruct, action: item.action), at: 0) + } + if !subItems.isEmpty { + items.append(ActionItem(text: L10n.peerInfoActionMore, image: theme.icons.profile_more, action: { }, subItems: subItems)) + } + } + + return items +} + +class PeerInfoHeadItem: GeneralRowItem { + override var height: CGFloat { + let insets = self.viewType.innerInset + var height: CGFloat = 0 + if !editing { + height = photoDimension + insets.top + insets.bottom + nameLayout.layoutSize.height + statusLayout.layoutSize.height + insets.bottom + + if !items.isEmpty { + let maxActionSize: NSSize = items.max(by: { $0.size.height < $1.size.height })!.size + height += maxActionSize.height + insets.top + } + } else { + height = photoDimension + insets.top + insets.bottom + } + return height + } + + fileprivate var statusLayout: TextViewLayout + fileprivate var nameLayout: TextViewLayout + + + let context: AccountContext + let peer:Peer? + let isVerified: Bool + let isScam: Bool + let isFake: Bool + let isMuted: Bool + let peerView:PeerView + var result:PeerStatusStringResult { + didSet { + nameLayout = TextViewLayout(result.title, maximumNumberOfLines: 1) + statusLayout = TextViewLayout(result.status, maximumNumberOfLines: 1, alwaysStaticItems: true) + } + } + + private(set) fileprivate var items: [ActionItem] = [] + + private let fetchPeerAvatar = DisposableSet() + private let onlineMemberCountDisposable = MetaDisposable() + + fileprivate let editing: Bool + fileprivate let updatingPhotoState:PeerInfoUpdatingPhotoState? + fileprivate let updatePhoto:(NSImage?, Control?)->Void + fileprivate let arguments: PeerInfoArguments + + let canEditPhoto: Bool + + + let peerPhotosDisposable = MetaDisposable() + + var photos: [TelegramPeerPhoto] = [] + + init(_ initialSize:NSSize, stableId:AnyHashable, context: AccountContext, arguments: PeerInfoArguments, peerView:PeerView, viewType: GeneralViewType, editing: Bool, updatingPhotoState:PeerInfoUpdatingPhotoState? = nil, updatePhoto:@escaping(NSImage?, Control?)->Void = { _, _ in }) { + let peer = peerViewMainPeer(peerView) + self.peer = peer + self.peerView = peerView + self.context = context + self.editing = editing + self.arguments = arguments + self.isVerified = peer?.isVerified ?? false + self.isScam = peer?.isScam ?? false + self.isFake = peer?.isFake ?? false + self.isMuted = peerView.notificationSettings?.isRemovedFromTotalUnreadCount(default: false) ?? false + self.updatingPhotoState = updatingPhotoState + self.updatePhoto = updatePhoto + + + let canEditPhoto: Bool + if let _ = peer as? TelegramUser { + canEditPhoto = false + } else if let _ = peer as? TelegramSecretChat { + canEditPhoto = false + } else if let peer = peer as? TelegramGroup { + canEditPhoto = peer.groupAccess.canEditGroupInfo + } else if let peer = peer as? TelegramChannel { + canEditPhoto = peer.groupAccess.canEditGroupInfo + } else { + canEditPhoto = false + } + + self.canEditPhoto = canEditPhoto && editing + + if let peer = peer { + if let peerReference = PeerReference(peer) { + if let largeProfileImage = peer.largeProfileImage { + fetchPeerAvatar.add(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: .avatar(peer: peerReference, resource: largeProfileImage.resource)).start()) + } + if let smallProfileImage = peer.smallProfileImage { + fetchPeerAvatar.add(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: .avatar(peer: peerReference, resource: smallProfileImage.resource)).start()) + } + } + + } + self.result = stringStatus(for: peerView, context: context, theme: PeerStatusStringTheme(titleFont: .medium(.huge), highlightIfActivity: false), expanded: true) + nameLayout = TextViewLayout(result.title, maximumNumberOfLines: 1) + statusLayout = TextViewLayout(result.status, maximumNumberOfLines: 1, alwaysStaticItems: true) + + + super.init(initialSize, stableId: stableId, viewType: viewType) + + + if let cachedData = peerView.cachedData as? CachedChannelData { + let onlineMemberCount:Signal + if (cachedData.participantsSummary.memberCount ?? 0) > 200 { + onlineMemberCount = context.peerChannelMemberCategoriesContextsManager.recentOnline(peerId: peerView.peerId) |> map(Optional.init) |> deliverOnMainQueue + } else { + onlineMemberCount = context.peerChannelMemberCategoriesContextsManager.recentOnlineSmall(peerId: peerView.peerId) |> map(Optional.init) |> deliverOnMainQueue + } + self.onlineMemberCountDisposable.set(onlineMemberCount.start(next: { [weak self] count in + guard let `self` = self else { + return + } + let result = stringStatus(for: peerView, context: context, theme: PeerStatusStringTheme(titleFont: .medium(.huge)), onlineMemberCount: count) + if result != self.result { + self.result = result + _ = self.makeSize(self.width, oldWidth: 0) + self.redraw(animated: true, options: .effectFade) + } + })) + } + + _ = self.makeSize(initialSize.width, oldWidth: 0) + + + if let peer = peer { + self.photos = syncPeerPhotos(peerId: peer.id) + let signal = peerPhotos(context: context, peerId: peer.id, force: true) |> deliverOnMainQueue + var first: Bool = true + peerPhotosDisposable.set(signal.start(next: { [weak self] photos in + if self?.photos != photos { + self?.photos = photos + if !first { + self?.redraw(animated: true, options: .effectFade) + } + first = false + } + })) + } + + } + + deinit { + fetchPeerAvatar.dispose() + onlineMemberCountDisposable.dispose() + } + + override func viewClass() -> AnyClass { + return PeerInfoHeadView.self + } + + override func makeSize(_ width: CGFloat, oldWidth:CGFloat) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) + + self.items = editing ? [] : actionItems(item: self, width: blockWidth, theme: theme) + let textWidth = blockWidth - viewType.innerInset.right - viewType.innerInset.left - (stateImage?.backingSize.width ?? 0) - 10 + nameLayout.measure(width: textWidth) + statusLayout.measure(width: textWidth) + + return success + } + + var stateImage: CGImage? { + let image: CGImage? + if isScam { + image = theme.icons.chatScam + } else if isVerified { + image = theme.icons.peerInfoVerifyProfile + } else if isFake { + image = theme.icons.chatFake + } else if isMuted { + image = theme.icons.dialogMuteImage + } else { + image = nil + } + return image + } + + fileprivate var iconSize: NSSize { + if let image = stateImage { + return NSMakeSize(image.backingSize.width + 5, image.backingSize.height) + } + return .zero + } + + fileprivate var nameSize: NSSize { + var stateHeight: CGFloat = 0 + if let image = stateImage { + stateHeight = max(image.backingSize.height, nameLayout.layoutSize.height) + } else { + stateHeight = nameLayout.layoutSize.height + } + var width = nameLayout.layoutSize.width + if let image = stateImage { + width += image.backingSize.width + 5 + } + return NSMakeSize(width, stateHeight) + } + +} + +private final class PeerInfoPhotoEditableView : Control { + private let backgroundView = View() + private let camera: ImageView = ImageView() + private var progressView:RadialProgressContainerView? + private var updatingPhotoState: PeerInfoUpdatingPhotoState? + private var tempImageView: ImageView? + var setup: ((NSImage?, Control?)->Void)? + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + + addSubview(backgroundView) + addSubview(camera) + + camera.image = theme.icons.profile_edit_photo + camera.sizeToFit() + camera.center() + + camera.isEventLess = true + + backgroundView.isEventLess = true + + set(handler: { [weak self] _ in + if self?.updatingPhotoState == nil { + self?.backgroundView.change(opacity: 0.8, animated: true) + self?.camera.change(opacity: 0.8, animated: true) + } + }, for: .Highlight) + + set(handler: { [weak self] _ in + if self?.updatingPhotoState == nil { + self?.backgroundView.change(opacity: 1.0, animated: true) + self?.camera.change(opacity: 1.0, animated: true) + } + }, for: .Normal) + + set(handler: { [weak self] _ in + if self?.updatingPhotoState == nil { + self?.backgroundView.change(opacity: 1.0, animated: true) + self?.camera.change(opacity: 1.0, animated: true) + } + }, for: .Hover) + + backgroundView.backgroundColor = .blackTransparent + backgroundView.frame = bounds + + + set(handler: { [weak self] control in + if self?.updatingPhotoState == nil { + self?.setup?(nil, control) + } + }, for: .Click) + } + + func updateState(_ updatingPhotoState: PeerInfoUpdatingPhotoState?, animated: Bool) { + self.updatingPhotoState = updatingPhotoState + + userInteractionEnabled = updatingPhotoState == nil + + self.camera.change(opacity: updatingPhotoState == nil ? 1.0 : 0.0, animated: true) + + if let uploadState = updatingPhotoState { + if self.progressView == nil { + self.progressView = RadialProgressContainerView(theme: RadialProgressTheme(backgroundColor: .clear, foregroundColor: .white, icon: nil)) + self.progressView!.frame = bounds + progressView?.proggressBackground.backgroundColor = .clear + self.addSubview(progressView!) + } + progressView?.progress.fetchControls = FetchControls(fetch: { + updatingPhotoState?.cancel() + }) + progressView?.progress.state = .Fetching(progress: uploadState.progress, force: false) + + if let _ = uploadState.image, self.tempImageView == nil { + self.tempImageView = ImageView() + self.tempImageView?.contentGravity = .resizeAspect + self.tempImageView!.frame = bounds + self.addSubview(tempImageView!, positioned: .below, relativeTo: backgroundView) + if animated { + self.tempImageView?.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } + self.tempImageView?.image = uploadState.image + } else { + if let progressView = self.progressView { + self.progressView = nil + if animated { + progressView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak progressView] _ in + progressView?.removeFromSuperview() + }) + } else { + progressView.removeFromSuperview() + } + } + if let tempImageView = self.tempImageView { + self.tempImageView = nil + if animated { + tempImageView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak tempImageView] _ in + tempImageView?.removeFromSuperview() + }) + } else { + tempImageView.removeFromSuperview() + } + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private final class NameContainer : View { + let nameView = TextView() + var stateImage: ImageView? + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(nameView) + } + + func update(_ item: PeerInfoHeadItem, animated: Bool) { + self.nameView.update(item.nameLayout) + + if let image = item.stateImage { + if stateImage == nil { + stateImage = ImageView() + addSubview(stateImage!) + } + stateImage?.image = image + _ = stateImage?.sizeToFit() + } else { + if let stateImage = stateImage { + self.stateImage = nil + if animated { + stateImage.layer?.animateAlpha(from: 1, to: 0, duration: 0.3, removeOnCompletion: false, completion: { [weak stateImage] _ in + stateImage?.removeFromSuperview() + }) + stateImage.layer?.animateScaleSpring(from: 1, to: 0.1, duration: 0.3, removeOnCompletion: false, bounce: false) + } + } + } + + needsLayout = true + } + + override func layout() { + super.layout() + + nameView.centerY(x: 0) + stateImage?.centerY(x: nameView.frame.maxX + 5, addition: -1) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + +private final class PeerInfoHeadView : GeneralContainableRowView { + private let photoView: AvatarControl = AvatarControl(font: .avatar(30)) + private var photoVideoView: MediaPlayerView? + private var photoVideoPlayer: MediaPlayer? + + + + private let nameView = NameContainer(frame: .zero) + private let statusView = TextView() + private let actionsView = View() + private var photoEditableView: PeerInfoPhotoEditableView? + + + private var activeDragging: Bool = false { + didSet { + self.item?.redraw(animated: true) + } + } + + override var backdorColor: NSColor { + guard let item = item as? PeerInfoHeadItem else { + return super.backdorColor + } + return item.editing ? super.backdorColor : .clear + } + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + + photoView.setFrameSize(NSMakeSize(photoDimension, photoDimension)) + + addSubview(photoView) + addSubview(nameView) + addSubview(statusView) + addSubview(actionsView) + + photoView.set(handler: { [weak self] _ in + if let item = self?.item as? PeerInfoHeadItem, let peer = item.peer, let _ = peer.largeProfileImage { + showPhotosGallery(context: item.context, peerId: peer.id, firstStableId: item.stableId, item.table, nil) + } + }, for: .Click) + + registerForDraggedTypes([.tiff, .string, .kUrl, .kFileUrl]) + } + + + override public func performDragOperation(_ sender: NSDraggingInfo) -> Bool { + if activeDragging { + activeDragging = false + if let item = item as? PeerInfoHeadItem { + if let tiff = sender.draggingPasteboard.data(forType: .tiff), let image = NSImage(data: tiff) { + item.updatePhoto(image, self.photoEditableView) + return true + } else { + let list = sender.draggingPasteboard.propertyList(forType: .kFilenames) as? [String] + if let list = list { + if let first = list.first, let image = NSImage(contentsOfFile: first) { + item.updatePhoto(image, self.photoEditableView) + return true + } + } + } + } + } + return false + } + + override public func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { + if let item = item as? PeerInfoHeadItem, !item.editing, let peer = item.peer, peer.groupAccess.canEditGroupInfo { + if let tiff = sender.draggingPasteboard.data(forType: .tiff), let _ = NSImage(data: tiff) { + activeDragging = true + } else { + let list = sender.draggingPasteboard.propertyList(forType: .kFilenames) as? [String] + if let list = list { + let list = list.filter { path -> Bool in + if let size = fs(path) { + return size <= 2000 * 1024 * 1024 + } + return false + } + activeDragging = list.count == 1 && NSImage(contentsOfFile: list[0]) != nil + } else { + activeDragging = false + } + } + + } else { + activeDragging = false + } + return .generic + } + override public func draggingExited(_ sender: NSDraggingInfo?) { + activeDragging = false + } + public override func draggingEnded(_ sender: NSDraggingInfo) { + activeDragging = false + } + + @objc func updatePlayerIfNeeded() { + let accept = window != nil && window!.isKeyWindow && !NSIsEmptyRect(visibleRect) && !isDynamicContentLocked + if let photoVideoPlayer = photoVideoPlayer { + if accept { + photoVideoPlayer.play() + } else { + photoVideoPlayer.pause() + } + } + } + + override func addAccesoryOnCopiedView(innerId: AnyHashable, view: NSView) { + photoVideoPlayer?.seek(timestamp: 0) + } + + override func viewDidUpdatedDynamicContent() { + super.viewDidUpdatedDynamicContent() + updatePlayerIfNeeded() + } + + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + updateListeners() + updatePlayerIfNeeded() + } + + override func viewDidMoveToSuperview() { + super.viewDidMoveToSuperview() + updateListeners() + updatePlayerIfNeeded() + } + + func updateListeners() { + if let window = window { + NotificationCenter.default.removeObserver(self) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSWindow.didBecomeKeyNotification, object: window) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSWindow.didResignKeyNotification, object: window) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSView.boundsDidChangeNotification, object: item?.table?.clipView) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSView.boundsDidChangeNotification, object: self) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSView.frameDidChangeNotification, object: item?.table?.view) + } else { + removeNotificationListeners() + } + } + + func removeNotificationListeners() { + NotificationCenter.default.removeObserver(self) + } + + deinit { + removeNotificationListeners() + } + + + + override func layout() { + super.layout() + + guard let item = item as? PeerInfoHeadItem else { + return + } + + photoView.centerX(y: item.viewType.innerInset.top) + nameView.centerX(y: photoView.frame.maxY + item.viewType.innerInset.top) + statusView.centerX(y: nameView.frame.maxY + 4) + actionsView.centerX(y: containerView.frame.height - actionsView.frame.height) + photoEditableView?.centerX(y: item.viewType.innerInset.top) + + photoVideoView?.frame = photoView.frame + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func layoutActionItems(_ items: [ActionItem], animated: Bool) { + + if !items.isEmpty { + let maxActionSize: NSSize = items.max(by: { $0.size.height < $1.size.height })!.size + + + while actionsView.subviews.count > items.count { + actionsView.subviews.removeLast() + } + while actionsView.subviews.count < items.count { + actionsView.addSubview(ActionButton(frame: .zero)) + } + + let inset: CGFloat = 0 + + actionsView.change(size: NSMakeSize(actionItemWidth * CGFloat(items.count) + CGFloat(items.count - 1) * actionItemInsetWidth, maxActionSize.height), animated: animated) + + var x: CGFloat = inset + + for (i, item) in items.enumerated() { + let view = actionsView.subviews[i] as! ActionButton + view.updateAndLayout(item: item, theme: theme) + view.setFrameSize(NSMakeSize(item.size.width, maxActionSize.height)) + view.change(pos: NSMakePoint(x, 0), animated: false) + x += maxActionSize.width + actionItemInsetWidth + } + + } else { + actionsView.removeAllSubviews() + } + + } + + private var videoRepresentation: TelegramMediaImage.VideoRepresentation? + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? PeerInfoHeadItem else { + return + } + + + photoView.setPeer(account: item.context.account, peer: item.peer) + + if !item.photos.isEmpty { + + if let first = item.photos.first, let video = first.image.videoRepresentations.last, item.updatingPhotoState == nil { + + let equal = videoRepresentation?.resource.id.isEqual(to: video.resource.id) ?? false + + if !equal { + + self.photoVideoView?.removeFromSuperview() + self.photoVideoView = nil + + self.photoVideoView = MediaPlayerView(backgroundThread: true) + self.photoVideoView!.layer?.cornerRadius = self.photoView.frame.height / 2 + if let photoEditableView = self.photoEditableView { + self.addSubview(self.photoVideoView!, positioned: .below, relativeTo: photoEditableView) + } else { + self.addSubview(self.photoVideoView!) + + } + self.photoVideoView!.isEventLess = true + + self.photoVideoView!.frame = self.photoView.frame + + + let file = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: first.image.representations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: video.resource.size, attributes: []) + + + let mediaPlayer = MediaPlayer(postbox: item.context.account.postbox, reference: MediaResourceReference.standalone(resource: file.resource), streamable: true, video: true, preferSoftwareDecoding: false, enableSound: false, fetchAutomatically: true) + + mediaPlayer.actionAtEnd = .loop(nil) + + self.photoVideoPlayer = mediaPlayer + + if let seekTo = video.startTimestamp { + mediaPlayer.seek(timestamp: seekTo) + } + mediaPlayer.attachPlayerView(self.photoVideoView!) + self.videoRepresentation = video + updatePlayerIfNeeded() + } + + + + } else { + self.photoVideoPlayer = nil + self.photoVideoView?.removeFromSuperview() + self.photoVideoView = nil + } + } else { + self.photoVideoPlayer = nil + self.photoVideoView?.removeFromSuperview() + self.photoVideoView = nil + } + nameView.change(size: item.nameSize, animated: animated) + nameView.update(item, animated: animated) + nameView.change(pos: NSMakePoint(containerView.focus(item.nameSize).minX, nameView.frame.minY), animated: animated) + + statusView.update(item.statusLayout) + + layoutActionItems(item.items, animated: animated) + + + photoView.userInteractionEnabled = !item.editing + + let containerRect: NSRect + switch item.viewType { + case .legacy: + containerRect = self.bounds + case .modern: + containerRect = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, item.height - item.inset.bottom - item.inset.top) + } + + + if item.canEditPhoto || self.activeDragging || item.updatingPhotoState != nil { + if photoEditableView == nil { + photoEditableView = .init(frame: NSMakeRect(0, 0, photoDimension, photoDimension)) + photoEditableView?.layer?.cornerRadius = photoDimension / 2 + addSubview(photoEditableView!) + if animated { + photoEditableView?.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } + photoEditableView?.updateState(item.updatingPhotoState, animated: animated) + photoEditableView?.setup = item.updatePhoto + } else { + if let photoEditableView = self.photoEditableView { + self.photoEditableView = nil + if animated { + photoEditableView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak photoEditableView] _ in + photoEditableView?.removeFromSuperview() + }) + } else { + photoEditableView.removeFromSuperview() + } + } + } + + containerView.change(size: containerRect.size, animated: animated) + containerView.change(pos: containerRect.origin, animated: animated) + containerView.setCorners(item.viewType.corners, animated: animated) + borderView._change(opacity: item.viewType.hasBorder ? 1.0 : 0.0, animated: animated) + + needsLayout = true + updateListeners() + } + + override func interactionContentView(for innerId: AnyHashable, animateIn: Bool ) -> NSView { + return photoView + } + + override func copy() -> Any { + return photoView.copy() + } + +} diff --git a/Telegram-Mac/PeerInfoHeaderItem.swift b/Telegram-Mac/PeerInfoHeaderItem.swift index d5f593dea0..74765c7351 100644 --- a/Telegram-Mac/PeerInfoHeaderItem.swift +++ b/Telegram-Mac/PeerInfoHeaderItem.swift @@ -8,59 +8,89 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac -class PeerInfoHeaderItem: GeneralRowItem { +import TelegramCore +import Postbox +import SwiftSignalKit +class PeerInfoHeaderItem: GeneralRowItem { - let firstTextEdited:String? - let lastTextEdited:String? + fileprivate var firstTextEdited:String? + fileprivate var lastTextEdited:String? override var height: CGFloat { - return 130.0 + switch self.viewType { + case .legacy: + return max(130.0, titleHeight + secondHeight + 60 + 4) + case let .modern(_, insets): + return max(photoDimension + insets.top + insets.bottom, titleHeight + secondHeight + 2 + insets.top + insets.bottom) + } } - let photoDimension:CGFloat = 70.0 - let textMargin:CGFloat = 15.0 - var textInset:CGFloat { - return self.inset.left + photoDimension + textMargin + override var instantlyResize: Bool { + return true + } + + fileprivate let photoDimension:CGFloat = 70.0 + fileprivate var textInset:CGFloat { + switch viewType { + case .legacy: + return self.inset.left + photoDimension + 15.0 + case let .modern(_, insets): + return insets.left + photoDimension + insets.left + } } - var photo:Signal? - var status:(TextNodeLayout, TextNode)? - var name:(TextNodeLayout, TextNode)? + fileprivate var photo:Signal<(CGImage?, Bool), NoError>? + fileprivate let statusLayout: TextViewLayout + fileprivate let nameLayout: TextViewLayout - let account:Account + fileprivate var titleHeight: CGFloat = 15 + fileprivate var secondHeight: CGFloat = 0 + + let context: AccountContext let peer:Peer? let isVerified: Bool + let isScam: Bool + let isFake: Bool let peerView:PeerView let result:PeerStatusStringResult let editable:Bool let updatingPhotoState:PeerInfoUpdatingPhotoState? let textChangeHandler:(String, String?)->Void let canCall:Bool - init(_ initialSize:NSSize, stableId:AnyHashable, account:Account, peerView:PeerView, editable:Bool = false, updatingPhotoState:PeerInfoUpdatingPhotoState? = nil, firstNameEditableText:String? = nil, lastNameEditableText:String? = nil, textChangeHandler:@escaping (String, String?)->Void = {_,_ in}) { + init(_ initialSize:NSSize, stableId:AnyHashable, context: AccountContext, peerView:PeerView, viewType: GeneralViewType = .legacy, editable:Bool = false, updatingPhotoState:PeerInfoUpdatingPhotoState? = nil, firstNameEditableText:String? = nil, lastNameEditableText:String? = nil, textChangeHandler:@escaping (String, String?)->Void = {_,_ in}) { let peer = peerViewMainPeer(peerView) self.peer = peer self.peerView = peerView self.editable = editable - self.account = account + self.context = context self.updatingPhotoState = updatingPhotoState self.textChangeHandler = textChangeHandler self.firstTextEdited = firstNameEditableText self.lastTextEdited = lastNameEditableText - canCall = peer != nil && (peer!.canCall && peer!.id != account.peerId && !editable) - - isVerified = peer?.isVerified ?? false + self.canCall = peer != nil && (peer!.canCall && peer!.id != context.peerId && !editable) + self.isVerified = peer?.isVerified ?? false + self.isScam = peer?.isScam ?? false + self.isFake = peer?.isFake ?? false if let peer = peer { - photo = peerAvatarImage(account: account, peer: peer, displayDimensions:NSMakeSize(photoDimension, photoDimension)) + photo = peerAvatarImage(account: context.account, photo: .peer(peer, peer.smallProfileImage, peer.displayLetters, nil), displayDimensions:NSMakeSize(photoDimension, photoDimension)) } - self.result = stringStatus(for: peerView, theme: PeerStatusStringTheme(titleFont: .medium(.huge), highlightIfActivity: false)) + self.result = stringStatus(for: peerView, context: context, theme: PeerStatusStringTheme(titleFont: .medium(.huge), highlightIfActivity: false), expanded: true) + - super.init(initialSize, stableId:stableId) + nameLayout = TextViewLayout(result.title, maximumNumberOfLines: 1) + statusLayout = TextViewLayout(result.status, maximumNumberOfLines: 1, alwaysStaticItems: true) + super.init(initialSize, stableId: stableId, viewType: viewType) + + _ = self.makeSize(initialSize.width, oldWidth: 0) + + } + + + fileprivate func calculateHeight() { + _ = self.makeSize(width, oldWidth: 0) } override func viewClass() -> AnyClass { @@ -68,10 +98,417 @@ class PeerInfoHeaderItem: GeneralRowItem { } override func makeSize(_ width: CGFloat, oldWidth:CGFloat) -> Bool { - name = TextNode.layoutText(maybeNode: nil, result.title, nil, 1, .end, NSMakeSize(size.width - textInset - inset.right - (canCall ? 40 : 0), size.height), nil, false, .left) - status = TextNode.layoutText(maybeNode: nil, result.status, nil, 1, .end, NSMakeSize(size.width - textInset - inset.right - (canCall ? 40 : 0), size.height), nil, false, .left) + let success = super.makeSize(width, oldWidth: oldWidth) + + if let firstTextEdited = firstTextEdited { + let textStorage = NSTextStorage(attributedString: .initialize(string: firstTextEdited, font: .normal(.huge), coreText: false)) + let textContainer:NSTextContainer + switch viewType { + case .legacy: + textContainer = NSTextContainer(size: NSMakeSize(width - inset.right - textInset, .greatestFiniteMagnitude)) + case let .modern(_, insets): + textContainer = NSTextContainer(size: NSMakeSize(width - textInset - insets.right - inset.left - inset.right, .greatestFiniteMagnitude)) + } + let layoutManager = NSLayoutManager() + layoutManager.addTextContainer(textContainer) + textStorage.addLayoutManager(layoutManager) + layoutManager.ensureLayout(for: textContainer) + titleHeight = max(layoutManager.usedRect(for: textContainer).height, 34) + } else { + titleHeight = 0 + } + + if let lastTextEdited = lastTextEdited { + let textStorage = NSTextStorage(attributedString: .initialize(string: lastTextEdited, font: .normal(.huge), coreText: false)) + let textContainer:NSTextContainer + switch viewType { + case .legacy: + textContainer = NSTextContainer(size: NSMakeSize(width - inset.right - textInset, .greatestFiniteMagnitude)) + case let .modern(_, insets): + textContainer = NSTextContainer(size: NSMakeSize(width - textInset - insets.right - inset.left - inset.right, .greatestFiniteMagnitude)) + } + let layoutManager = NSLayoutManager() + layoutManager.addTextContainer(textContainer) + textStorage.addLayoutManager(layoutManager) + layoutManager.ensureLayout(for: textContainer) + secondHeight = max(layoutManager.usedRect(for: textContainer).height, 34) + } else { + secondHeight = 0 + } + + switch viewType { + case .legacy: + break + case let .modern(_, inner): + let textWidth = blockWidth - textInset - inner.right - (canCall ? 40 : 0) - (isScam ? theme.icons.scam.backingSize.width + 5 : 0) + nameLayout.measure(width: textWidth) + statusLayout.measure(width: textWidth) + } + return success + } +} + + +class PeerInfoHeaderView: GeneralRowView, TGModernGrowingDelegate { + private let containerView = GeneralRowContainerView(frame: NSZeroRect) + private let image:AvatarControl = AvatarControl(font: .avatar(26.0)) + private let nameTextView = TextView() + private let statusTextView = TextView() + private let imageView = ImageView() + private let firstNameTextView:TGModernGrowingTextView = TGModernGrowingTextView(frame: NSMakeRect(0, 0, 0, 34), unscrollable: true) + private let lastNameTextView:TGModernGrowingTextView = TGModernGrowingTextView(frame: NSMakeRect(0, 0, 0, 34), unscrollable: true) + private let editableContainer:View = View() + private let firstNameSeparator:View = View() + private let separatorView:View = View() + private let progressView:RadialProgressContainerView = RadialProgressContainerView(theme: RadialProgressTheme(backgroundColor: .clear, foregroundColor: .white, icon: nil)) + private let callButton:ImageButton = ImageButton() + private let callDisposable = MetaDisposable() + private let fetchPeerAvatar = MetaDisposable() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + layerContentsRedrawPolicy = .onSetNeedsDisplay + image.frame = NSMakeRect(0, 0, 70, 70) + containerView.addSubview(image) + + containerView.addSubview(nameTextView) + containerView.addSubview(statusTextView) + image.set(handler: { [weak self] _ in + if let item = self?.item as? PeerInfoHeaderItem, let peer = item.peer, let _ = peer.largeProfileImage { + showPhotosGallery(context: item.context, peerId: peer.id, firstStableId: item.stableId, item.table, nil) + } + }, for: .Click) + + firstNameTextView.max_height = 10000 + lastNameTextView.max_height = 10000 + + firstNameTextView.delegate = self + firstNameTextView.textFont = .normal(.huge) + + lastNameTextView.delegate = self + lastNameTextView.textFont = .normal(.huge) + + + editableContainer.addSubview(firstNameTextView) + editableContainer.addSubview(lastNameTextView) + + containerView.addSubview(imageView) + + editableContainer.addSubview(firstNameSeparator) + + + progressView.progress.fetchControls = FetchControls(fetch: { [weak self] in + if let item = self?.item as? PeerInfoHeaderItem { + item.updatingPhotoState?.cancel() + } + }) + + callButton.set(handler: { [weak self] _ in + if let item = self?.item as? PeerInfoHeaderItem, let peerId = item.peer?.id { + let context = item.context + self?.callDisposable.set((phoneCall(account: context.account, sharedContext: context.sharedContext, peerId: peerId) |> deliverOnMainQueue).start(next: { result in + applyUIPCallResult(context.sharedContext, result) + })) + } + }, for: .SingleClick) + + containerView.addSubview(callButton) + containerView.addSubview(separatorView) + progressView.frame = image.bounds + + containerView.userInteractionEnabled = false + containerView.displayDelegate = self + containerView.addSubview(editableContainer) + + addSubview(containerView) + } + + func textViewHeightChanged(_ height: CGFloat, animated: Bool) { + if let item = item as? PeerInfoHeaderItem, let table = item.table { + + switch item.viewType { + case .legacy: + self.containerView.change(size: NSMakeSize(frame.width, item.height), animated: animated) + case .modern: + self.containerView.change(size: NSMakeSize(item.blockWidth, item.height - item.inset.bottom - item.inset.top), animated: animated, corners: item.viewType.corners) + firstNameSeparator.change(pos: NSMakePoint(4, item.titleHeight + 1), animated: animated) + lastNameTextView._change(pos: NSMakePoint(0, item.titleHeight + 2), animated: animated) + self.separatorView.change(pos: NSMakePoint(self.separatorView.frame.minX, self.containerView.frame.height - .borderSize), animated: animated) + } - return super.makeSize(width, oldWidth: oldWidth) + table.noteHeightOfRow(item.index, animated) + change(size: NSMakeSize(frame.width, item.height), animated: animated) + } + } + + func maxCharactersLimit(_ textView: TGModernGrowingTextView!) -> Int32 { + guard let item = item as? PeerInfoHeaderItem else {return 100} + if item.peer is TelegramUser { + return 128 + } + return 255 + } + + func textViewSize(_ textView: TGModernGrowingTextView!) -> NSSize { + if let item = item as? PeerInfoHeaderItem { + return NSMakeSize(frame.width - item.textInset - item.inset.right, textView.frame.height) + } + return NSZeroSize } + func textViewEnterPressed(_ event:NSEvent) -> Bool { + if FastSettings.checkSendingAbility(for: event) { + return true + } + return false + } + + func textViewIsTypingEnabled() -> Bool { + return true + } + + func textViewNeedClose(_ textView: Any) { + + } + + func textViewTextDidChange(_ string: String) { + if let item = item as? PeerInfoHeaderItem { + item.textChangeHandler(firstNameTextView.string(), lastNameTextView.isHidden ? nil : lastNameTextView.string()) + if !firstNameTextView.isHidden { + item.firstTextEdited = firstNameTextView.string() + } + if !lastNameTextView.isHidden { + item.lastTextEdited = lastNameTextView.string() + } + + let titleHeight = item.titleHeight + let secondHeight = item.secondHeight + let prevHeight = item.height + item.calculateHeight() + if (titleHeight != item.titleHeight || secondHeight != item.secondHeight) && prevHeight == item.height { + textViewHeightChanged(0, animated: true) + } + self.needsLayout = true + } + } + + func textViewTextDidChangeSelectedRange(_ range: NSRange) { + + } + + func textViewDidPaste(_ pasteboard: NSPasteboard) -> Bool { + return false + } + + + + override func interactionContentView(for innerId: AnyHashable, animateIn: Bool ) -> NSView { + return image + } + + override func copy() -> Any { + return image.copy() + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var backdorColor: NSColor { + return theme.colors.background + } + + override func updateColors() { + if let item = item as? PeerInfoHeaderItem { + self.containerView.background = backdorColor + editableContainer.backgroundColor = backdorColor + firstNameTextView.textColor = theme.colors.text + lastNameTextView.textColor = theme.colors.text + firstNameTextView.setBackgroundColor(backdorColor) + lastNameTextView.setBackgroundColor(backdorColor) + firstNameSeparator.backgroundColor = theme.colors.border + separatorView.backgroundColor = theme.colors.border + self.background = item.viewType.rowBackground + } + } + override func draw(_ layer: CALayer, in ctx: CGContext) { + + } + + override func setFrameSize(_ newSize: NSSize) { + super.setFrameSize(newSize) + } + + override func set(item:TableRowItem, animated:Bool = false) { + super.set(item: item, animated: animated) + + if let item = item as? PeerInfoHeaderItem { + + callButton.set(image: theme.icons.peerInfoCall, for: .Normal) + _ = callButton.sizeToFit() + + separatorView.isHidden = !item.viewType.hasBorder || item.viewType.isPlainMode + + switch item.viewType { + case .legacy: + self.containerView.change(size: NSMakeSize(frame.width, item.height), animated: animated, corners: item.viewType.corners) + case .modern: + self.containerView.change(size: NSMakeSize(item.blockWidth, item.height - item.inset.bottom - item.inset.top), animated: animated, corners: item.viewType.corners) + } + + if animated { + if item.editable { + self.editableContainer.isHidden = false + } + self.editableContainer.layer?.animateAlpha(from: !item.editable ? 1 : 0, to: item.editable ? 1 : 0, duration: 0.2, completion: { [weak self] completed in + if completed { + self?.editableContainer.isHidden = !item.editable + } + }) + + } else { + editableContainer.isHidden = !item.editable + self.editableContainer.layer?.removeAllAnimations() + } + self.editableContainer.layer?.opacity = item.editable ? 1 : 0 + + firstNameSeparator.isHidden = item.secondHeight == 0 + + + + + if item.isVerified { + imageView.image = theme.icons.peerInfoVerifyProfile + } else if item.isScam { + imageView.image = theme.icons.chatScam + } else { + imageView.image = nil + } + imageView.sizeToFit() + + imageView.isHidden = imageView.image == nil || item.editable + + let containerRect: NSRect + switch item.viewType { + case .legacy: + containerRect = self.bounds + case .modern: + containerRect = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, item.height - item.inset.bottom - item.inset.top) + } + containerView.change(size: containerRect.size, animated: animated) + containerView.change(pos: containerRect.origin, animated: animated) + containerView.setCorners(item.viewType.corners, animated: animated) + separatorView._change(opacity: item.viewType.hasBorder ? 1.0 : 0.0, animated: animated) + + nameTextView.update(item.nameLayout) + nameTextView.isHidden = item.editable + + statusTextView.update(item.statusLayout) + statusTextView.isHidden = item.editable + + self.needsLayout = true + + if let peer = item.peer { + image.setPeer(account: item.context.account, peer: peer) + + if let largeProfileImage = peer.largeProfileImage { + if let peerReference = PeerReference(peer) { + fetchPeerAvatar.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, reference: .avatar(peer: peerReference, resource: largeProfileImage.resource)).start()) + } + } + + if let peer = peer as? TelegramUser { + firstNameTextView.setString(item.firstTextEdited ?? peer.firstName ?? "", animated: false) + lastNameTextView.setString(item.lastTextEdited ?? peer.lastName ?? "", animated: false) + firstNameTextView.setPlaceholderAttributedString(.initialize(string: tr(L10n.peerInfoFirstNamePlaceholder), color: theme.colors.grayText, font: .normal(.header), coreText: false), update: false) + lastNameTextView.setPlaceholderAttributedString(.initialize(string: tr(L10n.peerInfoLastNamePlaceholder), color: theme.colors.grayText, font: .normal(.header), coreText: false), update: false) + lastNameTextView.isHidden = false + } else { + let titleText = item.firstTextEdited ?? peer.displayTitle + if titleText != firstNameTextView.string() { + firstNameTextView.setString(titleText, animated: false) + } + if peer.isChannel { + firstNameTextView.setPlaceholderAttributedString(.initialize(string: L10n.peerInfoChannelNamePlaceholder, color: theme.colors.grayText, font: .normal(.header), coreText: false), update: false) + } else { + firstNameTextView.setPlaceholderAttributedString(.initialize(string: L10n.peerInfoGroupNamePlaceholder, color: theme.colors.grayText, font: .normal(.header), coreText: false), update: false) + } + + lastNameTextView.isHidden = true + } + + if let uploadState = item.updatingPhotoState { + if progressView.superview == nil { + image.addSubview(progressView) + progressView.layer?.opacity = 0 + } + progressView.change(opacity: 1, animated: animated) + progressView.progress.state = .Fetching(progress: uploadState.progress, force: false) + } else { + if animated { + progressView.change(opacity: 0, animated: animated, removeOnCompletion: false, completion: { [weak self] complete in + if complete { + self?.progressView.removeFromSuperview() + self?.progressView.layer?.removeAllAnimations() + } + }) + } else { + progressView.removeFromSuperview() + } + } + + callButton.isHidden = !item.canCall + + + } + + needsLayout = true + containerView.needsDisplay = true + } + } + + override func layout() { + super.layout() + if let item = item as? PeerInfoHeaderItem { + switch item.viewType { + case .legacy: + self.containerView.frame = bounds + break + case let .modern(_, innerInset): + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) + + + image.frame = NSMakeRect(innerInset.left, innerInset.top, image.frame.width, image.frame.height) + + editableContainer.setFrameSize(NSMakeSize(containerView.frame.width - item.textInset - innerInset.right, item.titleHeight + item.secondHeight + 4)) + editableContainer.centerY(x: item.textInset) + + firstNameTextView.setFrameSize(NSMakeSize(editableContainer.frame.width, item.titleHeight)) + lastNameTextView.setFrameSize(NSMakeSize(editableContainer.frame.width, item.secondHeight)) + + + firstNameTextView.setFrameOrigin(0, 0) + firstNameSeparator.frame = NSMakeRect(4, firstNameTextView.frame.maxY + 1, editableContainer.frame.width, .borderSize) + lastNameTextView.setFrameOrigin(0, firstNameTextView.frame.maxY + 2) + + separatorView.frame = NSMakeRect(innerInset.left, containerView.frame.height - .borderSize, containerView.frame.width - innerInset.left - innerInset.right, .borderSize) + + callButton.centerY(x: containerView.frame.width - callButton.frame.width - innerInset.right) + + var nameY:CGFloat = focus(item.nameLayout.layoutSize).minY + let t = item.nameLayout.layoutSize.height + item.statusLayout.layoutSize.height + 4.0 + nameY = (containerView.frame.height - t) / 2.0 + + nameTextView.setFrameOrigin(NSMakePoint(item.textInset, nameY)) + statusTextView.setFrameOrigin(NSMakePoint(item.textInset, nameTextView.frame.maxY + 2)) + imageView.setFrameOrigin(NSMakePoint(item.textInset + item.nameLayout.layoutSize.width + 3, nameY + 3)) + + } + } + } + + deinit { + callDisposable.dispose() + fetchPeerAvatar.dispose() + } } diff --git a/Telegram-Mac/PeerInfoHeaderView.swift b/Telegram-Mac/PeerInfoHeaderView.swift deleted file mode 100644 index 6cc19da9e4..0000000000 --- a/Telegram-Mac/PeerInfoHeaderView.swift +++ /dev/null @@ -1,268 +0,0 @@ -// -// PeerInfoHeaderView.swift -// Telegram-Mac -// -// Created by keepcoder on 12/10/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac - - - -class PeerInfoHeaderView: TableRowView, TGModernGrowingDelegate { - - private let image:AvatarControl = AvatarControl(font: .avatar(.custom(26))) - - private let firstNameTextView:TGModernGrowingTextView = TGModernGrowingTextView(frame: NSZeroRect) - private let lastNameTextView:TGModernGrowingTextView = TGModernGrowingTextView(frame: NSZeroRect) - private let editableContainer:View = View() - private let firstNameSeparator:View = View() - private let lastNameSeparator:View = View() - private let progressView:RadialProgressContainerView = RadialProgressContainerView(theme: RadialProgressTheme(backgroundColor: .clear, foregroundColor: .white, icon: nil)) - private let callButton:ImageButton = ImageButton() - private let callDisposable = MetaDisposable() - required init(frame frameRect: NSRect) { - super.init(frame: frameRect) - image.frame = NSMakeRect(0, 0, 70, 70) - addSubview(image) - - image.set(handler: { [weak self] _ in - if let item = self?.item as? PeerInfoHeaderItem, let peer = item.peer, let _ = peer.largeProfileImage { - showPhotosGallery(account: item.account, peerId: peer.id, firstStableId: item.stableId, item.table, nil) - } - }, for: .Click) - - - firstNameTextView.delegate = self - firstNameTextView.textFont = .normal(.huge) - - firstNameTextView.min_height = 22 - firstNameTextView.isSingleLine = true - firstNameTextView.max_height = 22 - - lastNameTextView.delegate = self - lastNameTextView.textFont = .normal(.huge) - - lastNameTextView.min_height = 22 - lastNameTextView.max_height = 22 - lastNameTextView.isSingleLine = true - - editableContainer.addSubview(firstNameTextView) - editableContainer.addSubview(lastNameTextView) - - - editableContainer.addSubview(firstNameSeparator) - editableContainer.addSubview(lastNameSeparator) - - addSubview(editableContainer) - - progressView.progress.fetchControls = FetchControls(fetch: { [weak self] in - if let item = self?.item as? PeerInfoHeaderItem { - item.updatingPhotoState?.cancel() - } - }) - - - - callButton.set(handler: { [weak self] _ in - if let item = self?.item as? PeerInfoHeaderItem, let peerId = item.peer?.id { - let account = item.account - self?.callDisposable.set((phoneCall(account, peerId: peerId) |> deliverOnMainQueue).start(next: { result in - applyUIPCallResult(account, result) - })) - } - }, for: .SingleClick) - - addSubview(callButton) - - progressView.frame = image.bounds - // image.addSubview(progressView) - } - - func textViewHeightChanged(_ height: CGFloat, animated: Bool) { - - } - - func maxCharactersLimit() -> Int32 { - return 30 - } - - func textViewSize() -> NSSize { - if let item = item as? PeerInfoHeaderItem { - return NSMakeSize(frame.width - item.textInset - item.inset.right, 22) - } - return NSZeroSize - } - - func textViewEnterPressed(_ event:NSEvent) -> Bool { - if FastSettings.checkSendingAbility(for: event) { - return true - } - return false - } - - func textViewIsTypingEnabled() -> Bool { - return true - } - - func textViewNeedClose(_ textView: Any) { - - } - - func textViewTextDidChange(_ string: String) { - if let item = item as? PeerInfoHeaderItem { - item.textChangeHandler(firstNameTextView.string(), lastNameTextView.isHidden ? nil : lastNameTextView.string()) - } - } - - func textViewTextDidChangeSelectedRange(_ range: NSRange) { - - } - - func textViewDidPaste(_ pasteboard: NSPasteboard) -> Bool { - return false - } - - - - override var interactionContentView: NSView { - return image - } - - override func copy() -> Any { - return image.copy() - } - - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override var backdorColor: NSColor { - return theme.colors.background - } - - override func draw(_ layer: CALayer, in ctx: CGContext) { - super.draw(layer, in: ctx) - - if let item = item as? PeerInfoHeaderItem, let name = item.name, !item.editable { - - var nameY:CGFloat = focus(name.0.size).minY - - if let status = item.status { - - let t = name.0.size.height + status.0.size.height + 4.0 - nameY = (frame.height - t) / 2.0 - - let sY = nameY + name.0.size.height + 4.0 - status.1.draw(NSMakeRect(item.textInset, sY, status.0.size.width, status.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor) - - } - - if item.isVerified { - ctx.draw(theme.icons.peerInfoVerify, in: NSMakeRect(item.textInset + name.0.size.width + 3, nameY + 4, theme.icons.peerInfoVerify.backingSize.width, theme.icons.peerInfoVerify.backingSize.height)) - } - - name.1.draw(NSMakeRect(item.textInset, nameY, name.0.size.width, name.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor) - } - - } - - override func set(item:TableRowItem, animated:Bool = false) { - super.set(item: item, animated: animated) - - if let item = item as? PeerInfoHeaderItem { - image.frame = NSMakeRect(item.inset.left, (frame.height - image.frame.height)/2.0, image.frame.width, image.frame.height) - - callButton.set(image: theme.icons.peerInfoCall, for: .Normal) - callButton.sizeToFit() - - editableContainer.isHidden = !item.editable - editableContainer.backgroundColor = theme.colors.background - - firstNameTextView.textColor = theme.colors.text - lastNameTextView.textColor = theme.colors.text - firstNameTextView.background = theme.colors.background - lastNameTextView.background = theme.colors.background - - firstNameSeparator.backgroundColor = theme.colors.border - lastNameSeparator.backgroundColor = theme.colors.border - if let peer = item.peer { - image.setPeer(account: item.account, peer: peer) - if let peer = peer as? TelegramUser { - firstNameTextView.setString(item.firstTextEdited ?? peer.firstName ?? "", animated: false) - lastNameTextView.setString(item.lastTextEdited ?? peer.lastName ?? "", animated: false) - - firstNameTextView.setPlaceholderAttributedString(NSAttributedString.initialize(string: tr(.peerInfoFirstNamePlaceholder), color: theme.colors.grayText, font: .normal(.header), coreText: false), update: false) - lastNameTextView.setPlaceholderAttributedString(NSAttributedString.initialize(string: tr(.peerInfoLastNamePlaceholder), color: theme.colors.grayText, font: .normal(.header), coreText: false), update: false) - - lastNameTextView.isHidden = false - } else { - firstNameTextView.setString(item.firstTextEdited ?? peer.displayTitle, animated: false) - - if peer.isChannel { - firstNameTextView.setPlaceholderAttributedString(NSAttributedString.initialize(string: tr(.peerInfoChannelNamePlaceholder), color: theme.colors.grayText, font: .normal(.header), coreText: false), update: false) - } else { - firstNameTextView.setPlaceholderAttributedString(NSAttributedString.initialize(string: tr(.peerInfoGroupNamePlaceholder), color: theme.colors.grayText, font: .normal(.header), coreText: false), update: false) - } - lastNameTextView.isHidden = true - } - - if let uploadState = item.updatingPhotoState { - if progressView.superview == nil { - image.addSubview(progressView) - progressView.layer?.opacity = 0 - } - progressView.change(opacity: 1, animated: animated) - progressView.progress.state = .Fetching(progress: uploadState.progress, force: false) - } else { - if animated { - progressView.change(opacity: 0, animated: animated, removeOnCompletion: false, completion: { [weak self] complete in - if complete { - self?.progressView.removeFromSuperview() - self?.progressView.layer?.removeAllAnimations() - } - }) - } else { - progressView.removeFromSuperview() - } - } - - callButton.isHidden = !item.canCall - - lastNameSeparator.isHidden = lastNameTextView.isHidden - needsLayout = true - } - } - } - - override func layout() { - super.layout() - if let item = item as? PeerInfoHeaderItem { - - editableContainer.setFrameSize(NSMakeSize(frame.width - item.textInset - item.inset.right, lastNameTextView.isHidden ? 25 : 56)) - - firstNameTextView.setFrameSize(editableContainer.frame.width, 22) - lastNameTextView.setFrameSize(editableContainer.frame.width, 22) - - firstNameSeparator.frame = NSMakeRect(4, 24, editableContainer.frame.width, .borderSize) - firstNameTextView.setFrameOrigin(0, 0) - lastNameTextView.setFrameOrigin(0, 30) - - lastNameSeparator.frame = NSMakeRect(4, 55, editableContainer.frame.width, .borderSize) - - callButton.centerY(x: frame.width - callButton.frame.width - 30) - editableContainer.centerY(x: item.textInset) - } - } - - deinit { - callDisposable.dispose() - } - - -} diff --git a/Telegram-Mac/PeerInfoUtils.swift b/Telegram-Mac/PeerInfoUtils.swift index 28c95d64f6..577331b19b 100644 --- a/Telegram-Mac/PeerInfoUtils.swift +++ b/Telegram-Mac/PeerInfoUtils.swift @@ -7,77 +7,96 @@ // import Cocoa -import TelegramCoreMac -import PostboxMac +import TelegramCore + +import Postbox struct GroupAccess { let highlightAdmins:Bool - let canManageMembers:Bool - let canManageGroup:Bool + let canEditGroupInfo:Bool + let canEditMembers:Bool + let canAddMembers: Bool + let isPublic:Bool let isCreator:Bool + let canCreateInviteLink: Bool + let canReport: Bool + let canMakeVoiceChat: Bool } extension Peer { var groupAccess:GroupAccess { var highlightAdmins = false - var canManageGroup = false - var canManageMembers = false + var canEditGroupInfo = false + var canEditMembers = false + var canAddMembers = false + var isPublic = false var isCreator = false + var canReport = true + var canMakeVoiceChat = false if let group = self as? TelegramGroup { - if group.flags.contains(.adminsEnabled) { - highlightAdmins = true - switch group.role { - case .creator: - canManageGroup = true - canManageMembers = true - isCreator = true - case .admin: - canManageGroup = true - canManageMembers = true - case .member: - break - } - } else { - canManageGroup = group.membership == .Member - canManageMembers = group.membership == .Member - switch group.role { - case .creator: - isCreator = true - default: - break - } + if case .creator = group.role { + isCreator = true + canReport = false + canMakeVoiceChat = true + } + highlightAdmins = true + switch group.role { + case .admin, .creator: + canEditGroupInfo = true + canEditMembers = true + canAddMembers = true + canReport = false + canMakeVoiceChat = true + case .member: + break + } + if !group.hasBannedPermission(.banChangeInfo) { + canEditGroupInfo = true + } + if !group.hasBannedPermission(.banAddMembers) { + canAddMembers = true } } else if let channel = self as? TelegramChannel { highlightAdmins = true + isPublic = channel.username != nil isCreator = channel.flags.contains(.isCreator) - canManageGroup = channel.adminRights != nil || channel.flags.contains(.isCreator) - canManageMembers = channel.hasAdminRights(.canBanUsers) - + canReport = !channel.flags.contains(.isCreator) && channel.adminRights == nil + if channel.hasPermission(.changeInfo) { + canEditGroupInfo = true + } + if channel.hasPermission(.banMembers) { + canEditMembers = true + } + if channel.hasPermission(.inviteMembers) || isCreator || channel.adminRights?.rights.contains(.canInviteUsers) == true { + canAddMembers = true + } } - return GroupAccess(highlightAdmins: highlightAdmins, canManageMembers: canManageMembers, canManageGroup: canManageGroup, isCreator: isCreator) + + var canCreateInviteLink = false + if let group = self as? TelegramGroup { + if case .creator = group.role { + canCreateInviteLink = true + } + } else if let channel = self as? TelegramChannel { + if let adminRights = channel.adminRights, adminRights.rights.contains(.canInviteUsers) { + canCreateInviteLink = true + } + if channel.hasPermission(.manageCalls) { + canMakeVoiceChat = true + } + } + + + + return GroupAccess(highlightAdmins: highlightAdmins, canEditGroupInfo: canEditGroupInfo, canEditMembers: canEditMembers, canAddMembers: canAddMembers, isPublic: isPublic, isCreator: isCreator, canCreateInviteLink: canCreateInviteLink, canReport: canReport, canMakeVoiceChat: canMakeVoiceChat) } var canInviteUsers:Bool { if let peer = self as? TelegramChannel { - switch peer.info { - case .group(let info): - return peer.hasAdminRights(.canInviteUsers) || info.flags.contains(.everyMemberCanInviteMembers) - default: - break - } - return peer.hasAdminRights(.canInviteUsers) + return peer.hasPermission(.inviteMembers) } else if let group = self as? TelegramGroup { - if group.flags.contains(.adminsEnabled) { - switch group.role { - case .creator, .admin: - return true - default: - return false - } - } else { - return true - } + return !group.hasBannedRights(.banAddMembers) } @@ -110,10 +129,12 @@ extension TelegramGroup { extension TelegramChannel { func canRemoveParticipant(_ participant: ChannelParticipant, accountId:PeerId) -> Bool { - let hasRight = hasAdminRights(.canBanUsers) - + let hasRight = hasPermission(.banMembers) + if accountId == participant.peerId { + return false + } switch participant { - case let .member(_, _, adminInfo, _): + case let .member(_, _, adminInfo, _, _): if let adminInfo = adminInfo { return accountId == adminInfo.promotedBy || flags.contains(.isCreator) } else { @@ -166,11 +187,11 @@ func <(lhs:ChannelParticipant, rhs: ChannelParticipant) -> Bool { switch lhs { case .creator: return false - case let .member(lhsId, lhsInvitedAt, lhsAdminInfo, lhsBanInfo): + case let .member(lhsId, lhsInvitedAt, lhsAdminInfo, lhsBanInfo, lhsRank): switch rhs { case .creator: return true - case let .member(rhsId, rhsInvitedAt, rhsAdminInfo, rhsBanInfo): + case let .member(rhsId, rhsInvitedAt, rhsAdminInfo, rhsBanInfo, rhsRank): return lhsInvitedAt < rhsInvitedAt } } diff --git a/Telegram-Mac/PeerMediaBlockRowItem.swift b/Telegram-Mac/PeerMediaBlockRowItem.swift new file mode 100644 index 0000000000..436345cb9d --- /dev/null +++ b/Telegram-Mac/PeerMediaBlockRowItem.swift @@ -0,0 +1,259 @@ +// +// PeerMediaBlockRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 19.03.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import Postbox +import TelegramCore +import SwiftSignalKit +import CoreGraphics + +class PeerMediaBlockRowItem: GeneralRowItem { + + fileprivate var temporaryHeight: CGFloat? + fileprivate let listener: TableScrollListener + fileprivate let controller: PeerMediaController + fileprivate let isMediaVisible: Bool + init(_ initialSize: NSSize, stableId: AnyHashable, controller: PeerMediaController, isVisible: Bool, viewType: GeneralViewType) { + self.controller = controller + self.isMediaVisible = isVisible + self.listener = TableScrollListener(dispatchWhenVisibleRangeUpdated: false, { _ in }) + super.init(initialSize, height: initialSize.height, stableId: stableId, viewType: viewType) + } + + deinit { + if self.controller.isLoaded(), let table = self.table { +// let view = self.controller.genericView +// view.removeFromSuperview() + + if controller.frame.minY == 0 { + table.scroll(to: .up(true)) + if self.controller.genericView.superview != nil { + controller.viewWillDisappear(true) + self.controller.genericView.removeFromSuperview() + controller.viewDidDisappear(true) + } + } + } + + } + + override var instantlyResize: Bool { + return false + } + + override var height: CGFloat { + // return 10000 + + if !isMediaVisible { + return 1 + } else { + if let temporaryHeight = temporaryHeight { + return temporaryHeight + } else { + return table?.frame.height ?? initialSize.height + } + } + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + return true + } + + override func viewClass() -> AnyClass { + return PeerMediaBlockRowView.self + } +} + + +private final class PeerMediaBlockRowView : TableRowView { + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var backdorColor: NSColor { + return theme.colors.listBackground + } + + private func updateOrigin() { + guard let item = item as? PeerMediaBlockRowItem, let table = item.table else { + return + } + item.controller.view.frame = NSMakeRect(0, max(0, self.frame.minY - table.documentOffset.y), self.frame.width, table.frame.height) + } + + override func layout() { + super.layout() + + self.updateOrigin() + } + + override func setFrameOrigin(_ newOrigin: NSPoint) { + super.setFrameOrigin(newOrigin) + self.updateOrigin() + } + + override func removeFromSuperview() { + super.removeFromSuperview() + } + + override func scrollWheel(with event: NSEvent) { + guard let item = item as? PeerMediaBlockRowItem else { + super.scrollWheel(with: event) + return + } + item.controller.view.enclosingScrollView?.scrollWheel(with: event) + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? PeerMediaBlockRowItem else { + return + } + item.controller.bar = .init(height: 0) + item.controller._frameRect = bounds + + var scrollInner: Bool = false + + var scrollingInMediaTable: Bool = false + + + if item.isMediaVisible { + item.listener.handler = { [weak self, weak item] _ in + guard let `self` = self, let table = item?.table, let item = item else { + return + } + scrollInner = table.documentOffset.y >= self.frame.minY + let mediaTable = item.controller.genericView.mainTable + if let mediaTable = mediaTable { + + let offset = table.documentOffset.y - self.frame.minY + var updated = max(0, offset) + if mediaTable.documentSize.height <= table.frame.height, updated > 0 { + updated = max(updated - 30, 0) + } + if !scrollingInMediaTable, updated != mediaTable.documentOffset.y { + mediaTable.clipView.scroll(to: NSMakePoint(0, updated)) + mediaTable.reflectScrolledClipView(mediaTable.clipView) + } + if scrollInner { + + } else { + if mediaTable.documentOffset.y > 0 { + scrollInner = true + } + } + NotificationCenter.default.post(name: NSView.boundsDidChangeNotification, object: mediaTable.clipView) + + if item.temporaryHeight != mediaTable.documentSize.height { + item.temporaryHeight = max(mediaTable.documentSize.height, table.frame.height) + table.noteHeightOfRow(item.index, false) + } + + let previousY = item.controller.view.frame.minY + + item.controller.view.frame = NSMakeRect(0, max(0, self.frame.minY - table.documentOffset.y), self.frame.width, table.frame.height) + + let currentY = item.controller.view.frame.minY + if previousY != currentY { + if currentY == 0, previousY != 0 { + item.controller.viewWillAppear(true) + item.controller.viewDidAppear(true) + } else if previousY == 0 { + item.controller.viewWillDisappear(true) + item.controller.viewDidDisappear(true) + } + } + } + } + + item.table?.addScroll(listener: item.listener) + + item.table?.hasVerticalScroller = false + + item.table?._scrollWillStartLiveScrolling = { + scrollingInMediaTable = false + } + item.controller.genericView.mainTable?.reloadData() + } else { + needsLayout = true + } + + if item.controller.view.superview != item.table { + item.controller.view.removeFromSuperview() + item.table?.addSubview(item.controller.view) + } + if let table = item.table { + item.controller.genericView.change(pos: NSMakePoint(0, max(0, table.rectOf(item: item).minY - table.documentOffset.y)), animated: animated) + } + + if item.isMediaVisible { + item.controller.genericView.isHidden = false + } + + item.controller.genericView.change(opacity: item.isMediaVisible ? 1 : 0, animated: animated, completion: { [weak item] _ in + guard let item = item else { + return + } + item.controller.genericView.isHidden = !item.isMediaVisible + }) + + if item.isMediaVisible { + item.controller.currentMainTableView = { [weak item, weak self] mainTable, animated, updated in + if let item = item, animated { + if item.table?.documentOffset.y == self?.frame.minY { + if !updated { + mainTable?.scroll(to: .up(true)) + } + } else if updated { + item.table?.scroll(to: .top(id: item.stableId, innerId: nil, animated: animated, focus: .init(focus: false), inset: 0)) + } + } + + mainTable?.applyExternalScroll = { [weak self, weak item] event in + guard let `self` = self, let item = item else { + return false + } + if scrollInner { + if event.scrollingDeltaY > 0 { + if let tableView = item.controller.genericView.mainTable, tableView.documentOffset.y <= 0 { + if !item.controller.unableToHide { + scrollInner = false + item.table?.clipView.scroll(to: NSMakePoint(0, self.frame.minY)) + item.table?.scrollWheel(with: event) + scrollingInMediaTable = false + return true + } + + } + } + scrollingInMediaTable = true + return false + } else { + scrollingInMediaTable = false + item.table?.scrollWheel(with: event) + return true + } + } + } + } else { + item.controller.currentMainTableView = nil + } + } + + deinit { + } +} diff --git a/Telegram-Mac/PeerMediaCollectionInterfaceState.swift b/Telegram-Mac/PeerMediaCollectionInterfaceState.swift index 0379fffe75..6ca052ae56 100644 --- a/Telegram-Mac/PeerMediaCollectionInterfaceState.swift +++ b/Telegram-Mac/PeerMediaCollectionInterfaceState.swift @@ -7,8 +7,9 @@ // import Cocoa -import TelegramCoreMac -import PostboxMac +import TelegramCore + +import Postbox import TGUIKit final class PeerMediaCollectionInteraction : InterfaceObserver { @@ -30,12 +31,15 @@ final class PeerMediaCollectionInteraction : InterfaceObserver { } } -enum PeerMediaCollectionMode { - case photoOrVideo - case file - case music - case webpage - +enum PeerMediaCollectionMode : Int32 { + case members = -2 + case photoOrVideo = -1 + case file = 0 + case webpage = 1 + case music = 2 + case voice = 3 + case commonGroups = 4 + case gifs = 5 var tagsValue:MessageTags { switch self { case .photoOrVideo: @@ -46,22 +50,18 @@ enum PeerMediaCollectionMode { return .music case .webpage: return .webPage + case .voice: + return .voiceOrInstantVideo + case .members: + return [] + case .commonGroups: + return [] + case .gifs: + return .gif } } } -func titleForPeerMediaCollectionMode(_ mode: PeerMediaCollectionMode) -> String { - switch mode { - case .photoOrVideo: - return "Shared Media" - case .file: - return "Shared Files" - case .music: - return "Shared Music" - case .webpage: - return "Shared Links" - } -} struct PeerMediaCollectionInterfaceState: Equatable { let peer: Peer? @@ -122,7 +122,7 @@ struct PeerMediaCollectionInterfaceState: Equatable { selectedIds.formUnion(selectionState.selectedIds) } selectedIds.insert(messageId) - return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), mode: self.mode, selectingMode: self.selectingMode) + return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds, lastSelectedId: nil), mode: self.mode, selectingMode: self.selectingMode) } func withToggledSelectedMessage(_ messageId: MessageId) -> PeerMediaCollectionInterfaceState { @@ -135,11 +135,11 @@ struct PeerMediaCollectionInterfaceState: Equatable { } else { selectedIds.insert(messageId) } - return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), mode: self.mode, selectingMode: self.selectingMode) + return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds, lastSelectedId: nil), mode: self.mode, selectingMode: self.selectingMode) } func withSelectionState() -> PeerMediaCollectionInterfaceState { - return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: self.selectionState ?? ChatInterfaceSelectionState(selectedIds: Set()), mode: self.mode, selectingMode: true) + return PeerMediaCollectionInterfaceState(peer: self.peer, selectionState: self.selectionState ?? ChatInterfaceSelectionState(selectedIds: Set(), lastSelectedId: nil), mode: self.mode, selectingMode: true) } func withoutSelectionState() -> PeerMediaCollectionInterfaceState { diff --git a/Telegram-Mac/PeerMediaController.swift b/Telegram-Mac/PeerMediaController.swift index 59b5a933f3..08287888dd 100644 --- a/Telegram-Mac/PeerMediaController.swift +++ b/Telegram-Mac/PeerMediaController.swift @@ -1,177 +1,685 @@ // -// PeerMediaController.swift -// Telegram-Mac -// -// Created by keepcoder on 13/10/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac - + // PeerMediaController.swift + // Telegram-Mac + // + // Created by keepcoder on 13/10/2016. + // Copyright © 2016 Telegram. All rights reserved. + // + + import Cocoa + import TGUIKit + import TelegramCore + + import SwiftSignalKit + import Postbox + + + + + protocol PeerMediaSearchable : ViewController { + func toggleSearch() + func setSearchValue(_ value: Signal) + func setExternalSearch(_ value: Signal, _ loadMore: @escaping()->Void) + var mediaSearchValue:Signal { get } + } -class PeerMediaControllerView : View { + private final class SearchContainerView : View { + fileprivate let searchView: SearchView = SearchView(frame: NSMakeRect(0, 0, 200, 30)) + fileprivate let close: ImageButton = ImageButton() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(searchView) + addSubview(close) + updateLocalizationAndTheme(theme: theme) + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = theme as! TelegramPresentationTheme + borderColor = theme.colors.border + backgroundColor = .clear + close.set(image: theme.icons.dismissPinned, for: .Normal) + _ = close.sizeToFit() + } + + override func layout() { + super.layout() + searchView.setFrameSize(NSMakeSize(frame.width - close.frame.width - 30, 30)) + searchView.centerY(x: 10) + close.centerY(x: searchView.frame.maxX + 10) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + } + + private final class SegmentContainerView : View { + fileprivate let segmentControl: ScrollableSegmentView + required init(frame frameRect: NSRect) { + self.segmentControl = ScrollableSegmentView(frame: NSMakeRect(0, 0, frameRect.width, 50)) + super.init(frame: frameRect) + addSubview(segmentControl) + updateLocalizationAndTheme(theme: theme) + segmentControl.fitToWidth = true + + } + + override func layout() { + super.layout() + + segmentControl.frame = bounds + segmentControl.center() + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + // super.updateLocalizationAndTheme(theme: theme) + segmentControl.theme = ScrollableSegmentTheme(background: .clear, border: .clear, selector: theme.colors.accent, inactiveText: theme.colors.grayText, activeText: theme.colors.text, textFont: .normal(.text)) + backgroundColor = .clear + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + } + + private enum PeerMediaAnimationDirection { + case leftToRight + case rightToLeft + } + private let sectionOffset: CGFloat = 30 + + final class PeerMediaContainerView : View { private let actionsPanelView:MessageActionsPanelView = MessageActionsPanelView(frame: NSMakeRect(0,0,0, 50)) - private weak var mainView:NSView? private let separator:View = View() - private var isSelectionState:Bool = false - private var chatInteraction:ChatInteraction? - required init(frame frameRect:NSRect) { + + fileprivate let view: PeerMediaControllerView + init(frame frameRect: NSRect, isSegmentHidden: Bool) { + view = PeerMediaControllerView(frame: NSMakeRect(0, sectionOffset, min(600, frameRect.width - sectionOffset * 2), frameRect.height - sectionOffset), isSegmentHidden: isSegmentHidden) super.init(frame: frameRect) + addSubview(view) addSubview(actionsPanelView) addSubview(separator) - updateLocalizationAndTheme() + backgroundColor = theme.colors.listBackground + layout() } - - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + backgroundColor = theme.colors.listBackground separator.backgroundColor = theme.colors.border - mainView?.background = theme.colors.background + } + + override func scrollWheel(with event: NSEvent) { + view.scrollWheel(with: event) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + override func layout() { + super.layout() + + let blockWidth = min(600, frame.width - sectionOffset * 2) + + + view.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - blockWidth) / 2), sectionOffset, blockWidth, frame.height - sectionOffset) + + let inset:CGFloat = view.isSelectionState ? 50 : 0 + actionsPanelView.frame = NSMakeRect(0, frame.height - inset, frame.width, 50) + separator.frame = NSMakeRect(0, frame.height - inset, frame.width, .borderSize) + + } + + var mainView:NSView? { + return self.view.mainView + } + + var mainTable: TableView? { + if let tableView = self.view.mainView as? TableView { + return tableView + } else if let view = self.view.mainView as? InputDataView { + return view.tableView + } else if let view = self.view.mainView as? PeerMediaGifsView { + return view.tableView + } + return nil } func updateInteraction(_ chatInteraction:ChatInteraction) { - self.chatInteraction = chatInteraction + self.view.updateInteraction(chatInteraction) actionsPanelView.prepare(with: chatInteraction) } - func updateMainView(with view:NSView, animated:Bool) { - mainView?.removeFromSuperview() - mainView?.background = theme.colors.background - self.mainView = view - addSubview(view) + fileprivate func updateMainView(with view:NSView, animated:PeerMediaAnimationDirection?) { + self.view.updateMainView(with: view, animated: animated) + } + + func updateSearchState(_ state: MediaSearchState, updateSearchState:@escaping(SearchState)->Void, toggle:@escaping()->Void) { + self.view.updateSearchState(state, updateSearchState: updateSearchState, toggle: toggle) + } + + func changeState(selectState:Bool, animated:Bool) { + self.view.changeState(selectState: selectState, animated: animated) + let inset:CGFloat = selectState ? 50 : 0 + actionsPanelView.change(pos: NSMakePoint(0, frame.height - inset), animated: animated) + separator.change(pos: NSMakePoint(0, frame.height - inset), animated: animated) + } + + var activePanel: View { + return self.view.activePanel + } + + + fileprivate var segmentPanelView: SegmentContainerView { + return self.view.segmentPanelView + } + fileprivate var searchPanelView: SearchContainerView? { + return self.view.searchPanelView + } + + func updateCorners(_ corners: GeneralViewItemCorners, animated: Bool) { + view.updateCorners(corners, animated: animated) + } + } + + class PeerMediaControllerView : View { + + private let topPanelView = GeneralRowContainerView(frame: .zero) + fileprivate let segmentPanelView: SegmentContainerView + fileprivate var searchPanelView: SearchContainerView? + + private(set) weak var mainView:NSView? + + private let topPanelSeparatorView = View() + + override func scrollWheel(with event: NSEvent) { + mainTable?.scrollWheel(with: event) + } + + var mainTable: TableView? { + if let tableView = self.mainView as? TableView { + return tableView + } else if let view = self.mainView as? InputDataView { + return view.tableView + } + return nil + } + + fileprivate var corners:GeneralViewItemCorners = [.topLeft, .topRight] + + fileprivate var isSelectionState:Bool = false + private var chatInteraction:ChatInteraction? + private var searchState: SearchState? + init(frame frameRect:NSRect, isSegmentHidden: Bool) { + segmentPanelView = SegmentContainerView(frame: NSMakeRect(0, 0, frameRect.width, 50)) + super.init(frame: frameRect) + addSubview(topPanelView) + topPanelView.isHidden = isSegmentHidden + topPanelView.addSubview(topPanelSeparatorView) + topPanelView.addSubview(segmentPanelView) + updateLocalizationAndTheme(theme: theme) + layout() + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + // super.updateLocalizationAndTheme(theme: theme) + backgroundColor = theme.colors.listBackground + topPanelView.backgroundColor = theme.colors.background + topPanelSeparatorView.backgroundColor = theme.colors.border + } + + func updateInteraction(_ chatInteraction:ChatInteraction) { + self.chatInteraction = chatInteraction + } + + func updateCorners(_ corners: GeneralViewItemCorners, animated: Bool) { + self.corners = corners + self.topPanelView.setCorners(corners, animated: animated) + topPanelSeparatorView.isHidden = corners == .all + } + + fileprivate func updateMainView(with view:NSView, animated:PeerMediaAnimationDirection?) { + addSubview(view, positioned: .below, relativeTo: topPanelView) + + let timingFunction: CAMediaTimingFunctionName = .spring + let duration: TimeInterval = 0.35 + + if let animated = animated { + if let mainView = mainView { + switch animated { + case .leftToRight: + mainView._change(pos: NSMakePoint(-mainView.frame.width, mainView.frame.minY), animated: true, duration: duration, timingFunction: timingFunction, completion: { [weak mainView] completed in + if completed { + mainView?.removeFromSuperview() + } + }) + view.layer?.animatePosition(from: NSMakePoint(view.frame.width, mainView.frame.minY), to: NSMakePoint(0, mainView.frame.minY), duration: duration, timingFunction: timingFunction) + case .rightToLeft: + mainView._change(pos: NSMakePoint(mainView.frame.width, mainView.frame.minY), animated: true, duration: duration, timingFunction: timingFunction, completion: { [weak mainView] completed in + if completed { + mainView?.removeFromSuperview() + } + }) + view.layer?.animatePosition(from: NSMakePoint(-view.frame.width, mainView.frame.minY), to: NSMakePoint(0, mainView.frame.minY), duration: duration, timingFunction: timingFunction) + } + } + self.mainView = view + } else { + mainView?.removeFromSuperview() + self.mainView = view + } needsLayout = true } + func updateSearchState(_ state: MediaSearchState, updateSearchState:@escaping(SearchState)->Void, toggle:@escaping()->Void) { + self.searchState = state.state + switch state.state.state { + case .Focus: + if searchPanelView == nil { + self.searchPanelView = SearchContainerView(frame: NSMakeRect(0, -topPanelView.frame.height, topPanelView.frame.width, 50)) + + guard let searchPanelView = self.searchPanelView else { + fatalError() + } + topPanelView.addSubview(searchPanelView, positioned: .above, relativeTo: topPanelSeparatorView) + searchPanelView.searchView.change(state: .Focus, false) + searchPanelView.searchView.searchInteractions = SearchInteractions({ _, _ in + + }, updateSearchState) + + searchPanelView.close.set(handler: { _ in + toggle() + }, for: .Click) + } + + + guard let searchPanelView = self.searchPanelView else { + fatalError() + } + searchPanelView.searchView.isLoading = state.isLoading + searchPanelView._change(pos: NSZeroPoint, animated: state.animated) + segmentPanelView._change(pos: NSMakePoint(0, topPanelView.frame.height), animated: state.animated) + case .None: + CATransaction.begin() + segmentPanelView.removeFromSuperview() + topPanelView.addSubview(segmentPanelView, positioned: .above, relativeTo: topPanelSeparatorView) + segmentPanelView._change(pos: NSZeroPoint, animated: state.animated) + if let searchPanelView = self.searchPanelView { + self.searchPanelView = nil + searchPanelView._change(pos: NSMakePoint(0, -searchPanelView.frame.height), animated: state.animated, completion: { [weak searchPanelView] completed in + searchPanelView?.removeFromSuperview() + }) + } + CATransaction.commit() + } + } + func changeState(selectState:Bool, animated:Bool) { assert(mainView != nil) + self.isSelectionState = selectState - let inset:CGFloat = selectState ? 50 : 0 - - mainView?.animator().setFrameSize(NSMakeSize(frame.width, frame.height - inset)) - actionsPanelView.change(pos: NSMakePoint(0, frame.height - inset), animated: animated) - separator.change(pos: NSMakePoint(0, frame.height - inset), animated: animated) + } + + var activePanel: View { + if let searchPanel = self.searchPanelView { + return searchPanel + } else { + return segmentPanelView + } } override func layout() { let inset:CGFloat = isSelectionState ? 50 : 0 + topPanelView.frame = NSMakeRect(0, 0, frame.width, 50) + topPanelView.setCorners(self.corners) + topPanelSeparatorView.frame = NSMakeRect(0, topPanelView.frame.height - .borderSize, topPanelView.frame.width, .borderSize) - mainView?.frame = NSMakeRect(0, 0, frame.width, frame.height - inset) - actionsPanelView.frame = NSMakeRect(0, frame.height - inset, frame.width, 50) - separator.frame = NSMakeRect(0, frame.height - inset, frame.width, .borderSize) + if let searchPanelView = self.searchPanelView { + searchPanelView.frame = NSMakeRect(0, 0, frame.width, 50) + segmentPanelView.frame = NSMakeRect(0, topPanelView.frame.height, frame.width, 50) + } else { + segmentPanelView.frame = NSMakeRect(0, 0, topPanelView.frame.width, 50) + } + mainView?.frame = NSMakeRect(0, topPanelView.isHidden ? 0 : topPanelView.frame.height, frame.width, frame.height - inset - (topPanelView.isHidden ? 0 : topPanelView.frame.height)) + + } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } -} - -class PeerMediaController: EditableViewController, Notifable { - + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + } + + private extension PeerMediaCollectionMode { + var title: String { + if self == .members { + return L10n.peerMediaMembers + } + if self == .photoOrVideo { + return L10n.peerMediaMedia + } + if self == .file { + return L10n.peerMediaFiles + } + if self == .webpage { + return L10n.peerMediaLinks + } + if self.tagsValue == .music { + return L10n.peerMediaMusic + } + if self == .voice { + return L10n.peerMediaVoice + } + if self == .commonGroups { + return L10n.peerMediaCommonGroups + } + if self == .gifs { + return L10n.peerMediaGifs + } + return "" + } + } + + struct PeerMediaExternalSearchData { + let searchResult: Signal + let loadMore:()->Void + let initialTags: MessageTags + init(initialTags: MessageTags, searchResult: Signal, loadMore: @escaping()->Void) { + self.searchResult = searchResult + self.loadMore = loadMore + self.initialTags = initialTags + } + + + var initialMode: PeerMediaCollectionMode { + if initialTags == .photo || initialTags == .video || initialTags == .photoOrVideo { + return .photoOrVideo + } else if initialTags == .gif { + return .gifs + } else if initialTags == .file { + return .file + } else if initialTags == .voiceOrInstantVideo { + return .voice + } else if initialTags == .webPage { + return .webpage + } else if initialTags == .music { + return .music + } + preconditionFailure("not supported") + } + } + + class PeerMediaController: EditableViewController, Notifable { + private let peerId:PeerId private var peer:Peer? + private var peerView: PeerView? { + didSet { + if isLoaded(), let peerView = peerView, isProfileIntended { + let context = self.context + + if let cachedData = peerView.cachedData as? CachedChannelData { + let onlineMemberCount:Signal + if (cachedData.participantsSummary.memberCount ?? 0) > 200 { + onlineMemberCount = context.peerChannelMemberCategoriesContextsManager.recentOnline(peerId: self.peerId) |> map(Optional.init) |> deliverOnMainQueue + } else { + onlineMemberCount = context.peerChannelMemberCategoriesContextsManager.recentOnlineSmall(peerId: self.peerId) |> map(Optional.init) |> deliverOnMainQueue + } + + self.onlineMemberCountDisposable.set(onlineMemberCount.start(next: { [weak self] count in + guard let `self` = self else { + return + } + let result = stringStatus(for: peerView, context: context, theme: PeerStatusStringTheme(titleFont: .medium(.title)), onlineMemberCount: count) + self.centerBar.status = result.status + self.centerBar.text = result.title + })) + } else { + let result = stringStatus(for: peerView, context: context, theme: PeerStatusStringTheme(titleFont: .medium(.title)), onlineMemberCount: 0) + self.centerBar.status = result.status + self.centerBar.text = result.title + } + } + } + } + + private let modeValue: ValuePromise = ValuePromise(nil, ignoreRepeated: true) + private let tabsSignal:Promise<(tabs: [PeerMediaCollectionMode], selected: PeerMediaCollectionMode?, hasLoaded: Bool)> = Promise() + + var tabsValue: Signal { + return tabsSignal.get() |> map { + PeerMediaTabsData(collections: $0.tabs, loaded: $0.hasLoaded) + } |> distinctUntilChanged + } + + private let tabsDisposable = MetaDisposable() + private var mode:PeerMediaCollectionMode? - private var tagMask:MessageTags - private var mode:PeerMediaCollectionMode = .photoOrVideo + private let mediaGrid:PeerMediaPhotosController + private let gifs: PeerMediaPhotosController + private let listControllers:[PeerMediaListController] + private let members: ViewController + private let commonGroups: ViewController - private let mediaGrid:PeerMediaGridController - private let mediaList:PeerMediaListController + private let tagsList:[PeerMediaCollectionMode] = [.members, .photoOrVideo, .file, .webpage, .music, .voice, .gifs, .commonGroups] + + + private var currentTagListIndex: Int { + if let mode = self.mode { + return Int(mode.rawValue) + } else { + return 0 + } + } private var interactions:ChatInteraction - private let openPeerInfoDisposable = MetaDisposable() private let messagesActionDisposable:MetaDisposable = MetaDisposable() private let loadFwdMessagesDisposable = MetaDisposable() + private let loadSelectionMessagesDisposable = MetaDisposable() + private let searchValueDisposable = MetaDisposable() + private let onlineMemberCountDisposable = MetaDisposable() + private var searchController: PeerMediaListController? + private let externalSearchData: PeerMediaExternalSearchData? + private let toggleDisposable = MetaDisposable() + private let externalDisposable = MetaDisposable() + private var currentController: ViewController? - override func getCenterBarViewOnce() -> TitledBarView { - return MediaTitleBarView(controller: self, interactions:PeerMediaTypeInteraction(media: { [weak self] in - self?.toggle(with: .photoOrVideo, animated:true) - }, files: { [weak self] in - self?.toggle(with: .file, animated:true) - }, links: { [weak self] in - self?.toggle(with: .webpage, animated:true) - }, audio: { [weak self] in - self?.toggle(with: .music, animated:true) - })) + + + var currentMainTableView:((TableView?, Bool, Bool)->Void)? = nil { + didSet { + if isLoaded() { + currentMainTableView?(genericView.mainTable, false, false) + } + } } + private let isProfileIntended: Bool - init(account:Account, peerId:PeerId, tagMask:MessageTags) { + private let editing: ValuePromise = ValuePromise(false, ignoreRepeated: true) + override var state:ViewControllerState { + didSet { + let newValue = state + + genericView.mainTable?.scroll(to: .up(true), completion: { [weak self] _ in + self?.editing.set(newValue == .Edit) + }) + } + } + + init(context: AccountContext, peerId:PeerId, isProfileIntended:Bool = false, externalSearchData: PeerMediaExternalSearchData? = nil) { + self.externalSearchData = externalSearchData self.peerId = peerId - self.tagMask = tagMask + self.isProfileIntended = isProfileIntended + self.interactions = ChatInteraction(chatLocation: .peer(peerId), context: context) + self.mediaGrid = PeerMediaPhotosController(context, chatInteraction: interactions, peerId: peerId, tags: .photoOrVideo) - interactions = ChatInteraction(peerId: peerId, account: account) + var updateTitle:((ExternalSearchMessages)->Void)? = nil + if let external = externalSearchData { + modeValue.set(external.initialMode) + let signal = external.searchResult |> deliverOnMainQueue + externalDisposable.set(signal.start(next: { result in + if let result = result { + updateTitle?(result) + } + })) + } - mediaGrid = PeerMediaGridController(account: account, peerId: peerId, messageId: nil, tagMask: tagMask, chatInteraction: interactions) - mediaList = PeerMediaListController(account: account, peerId: peerId, chatInteraction: interactions) + var listControllers: [PeerMediaListController] = [] + for _ in tagsList.filter ({ !$0.tagsValue.isEmpty }) { + listControllers.append(PeerMediaListController(context: context, chatLocation: .peer(peerId), chatInteraction: interactions)) + } + self.listControllers = listControllers + self.members = PeerMediaGroupPeersController(context: context, peerId: peerId, editing: editing.get()) + self.commonGroups = GroupsInCommonViewController(context: context, peerId: peerId) + self.gifs = PeerMediaPhotosController(context, chatInteraction: interactions, peerId: peerId, tags: .gif) + super.init(context) - super.init(account) + updateTitle = { [weak self] result in + if let title = result.title { + self?.setCenterTitle(title) + } + } + } + + var unableToHide: Bool { + return self.genericView.activePanel is SearchContainerView || self.state != .Normal } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) interactions.add(observer: self) - if self.mode == .photoOrVideo { - self.mediaGrid.viewDidAppear(animated) - } else { - self.mediaList.viewDidAppear(animated) + + if let mode = self.mode { + self.controller(for: mode).viewDidAppear(animated) + } + + + window?.set(handler: { [weak self] _ -> KeyHandlerResult in + guard let `self` = self, self.mode != .commonGroups, self.externalSearchData == nil else { + return .rejected + } + if self.mode == .members { + self.searchGroupUsers() + return .invoked + } + if self.mode == .photoOrVideo { + (self.controller(for: .photoOrVideo) as? PeerMediaPhotosController)?.toggleSearch() + return .invoked + } + self.listControllers[self.currentTagListIndex].toggleSearch() + return .invoked + }, with: self, for: .F, modifierFlags: [.command]) + + window?.set(handler: { [weak self] _ -> KeyHandlerResult in + guard let `self` = self else { + return .rejected + } + if self.genericView.searchPanelView != nil { + return .rejected + } + // self.genericView.segmentPanelView.segmentControl.selectNext(animated: true) + return .invoked + }, with: self, for: .Tab) + + guard let navigationController = self.navigationController, isProfileIntended else { + return } + + navigationController.swapNavigationBar(leftView: nil, centerView: self.centerBarView, rightView: nil, animation: .crossfade) + navigationController.swapNavigationBar(leftView: nil, centerView: nil, rightView: self.rightBarView, animation: .none) + } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) interactions.remove(observer: self) - if self.mode == .photoOrVideo { - self.mediaGrid.viewDidDisappear(animated) - } else { - self.mediaList.viewDidDisappear(animated) + + if let mode = mode { + let controller = self.controller(for: mode) + controller.viewDidDisappear(animated) + if let controller = controller as? PeerMediaSearchable { + controller.setSearchValue(.single(.init(state: .None, request: nil))) + } + } + + if let navigationController = navigationController, isProfileIntended { + navigationController.swapNavigationBar(leftView: nil, centerView: navigationController.controller.centerBarView, rightView: nil, animation: .crossfade) + navigationController.swapNavigationBar(leftView: nil, centerView: nil, rightView: navigationController.controller.rightBarView, animation: .none) } } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - if self.mode == .photoOrVideo { - self.mediaGrid.viewWillAppear(animated) - } else { - self.mediaList.viewWillAppear(animated) + + if let mode = mode { + let controller = self.controller(for: mode) + controller.viewWillAppear(animated) } } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - if self.mode == .photoOrVideo { - self.mediaGrid.viewWillDisappear(animated) - } else { - self.mediaList.viewWillDisappear(animated) + window?.removeAllHandlers(for: self) + + if let mode = mode { + let controller = self.controller(for: mode) + controller.viewWillDisappear(animated) } } func notify(with value: Any, oldValue: Any, animated: Bool) { if let value = value as? ChatPresentationInterfaceState, let oldValue = oldValue as? ChatPresentationInterfaceState { + + let context = self.context + if value.selectionState != oldValue.selectionState { + if let selectionState = value.selectionState { + let ids = Array(selectionState.selectedIds) + loadSelectionMessagesDisposable.set((context.account.postbox.messagesAtIds(ids) |> deliverOnMainQueue).start( next:{ [weak self] messages in + var canDelete:Bool = !ids.isEmpty + var canForward:Bool = !ids.isEmpty + if let interactions = self?.interactions { + for message in messages { + if !canDeleteMessage(message, account: context.account, mode: .history) { + canDelete = false + } + if !canForwardMessage(message, chatInteraction: interactions) { + canForward = false + } + } + interactions.update({$0.withUpdatedBasicActions((canDelete, canForward))}) + } + + })) + } else { + interactions.update({$0.withUpdatedBasicActions((false, false))}) + } + } + if (value.state == .selecting) != (oldValue.state == .selecting) { self.state = value.state == .selecting ? .Edit : .Normal - genericView.changeState(selectState: value.state == .selecting, animated: animated) - if mode == .photoOrVideo { - self.mediaGrid.genericView.grid.forEachItemNode { itemNode in - if let itemNode = itemNode as? GridMessageItemNode { - itemNode.updateSelectionState(animated: animated) - } - } - } - + genericView.changeState(selectState: value.state == .selecting && self.mode != .members, animated: animated) } } } - + func isEqual(to other: Notifable) -> Bool { if let other = other as? PeerMediaController { @@ -184,170 +692,558 @@ class PeerMediaController: EditableViewController, Noti super.viewDidLoad() genericView.updateInteraction(interactions) + if externalSearchData != nil { + centerBar.updateSearchVisibility(false, animated: false) + } + - interactions.forwardMessages = { [weak self] messageIds in - if let strongSelf = self, let navigation = strongSelf.navigationController { - strongSelf.loadFwdMessagesDisposable.set((strongSelf.account.postbox.messagesAtIds(messageIds) |> deliverOnMainQueue).start(next: { [weak strongSelf] messages in - if let strongSelf = strongSelf { - - let displayName:String = strongSelf.peer?.compactDisplayTitle ?? "Unknown" - let action = FWDNavigationAction(messages: messages, displayName: displayName) - navigation.set(modalAction: action, strongSelf.account.context.layout != .single) - - if strongSelf.account.context.layout == .single { - navigation.push(ForwardChatListController(strongSelf.account)) + let tagsList = self.tagsList + + let context = self.context + let peerId = self.peerId + + + let membersTab:Signal<(tag: PeerMediaCollectionMode, exists: Bool, hasLoaded: Bool), NoError> + let commonGroupsTab:Signal<(tag: PeerMediaCollectionMode, exists: Bool, hasLoaded: Bool), NoError> + + membersTab = context.account.postbox.peerView(id: peerId) |> map { view -> (exist: Bool, loaded: Bool) in + if let cachedData = view.cachedData as? CachedGroupData { + return (exist: Int(cachedData.participants?.participants.count ?? 0 ) > minumimUsersBlock, loaded: true) + } else if let cachedData = view.cachedData as? CachedChannelData { + if let peer = peerViewMainPeer(view), peer.isSupergroup { + return (exist: Int32(cachedData.participantsSummary.memberCount ?? 0) > minumimUsersBlock, loaded: true) + } else { + return (exist: false, loaded: true) + } + } else { + return (exist: false, loaded: true) + } + } |> map { data -> (tag: PeerMediaCollectionMode, exists: Bool, hasLoaded: Bool) in + return (tag: .members, exists: data.exist, hasLoaded: data.loaded) + } + + commonGroupsTab = context.account.postbox.peerView(id: peerId) |> map { view -> (exist: Bool, loaded: Bool) in + if let cachedData = view.cachedData as? CachedUserData { + return (exist: cachedData.commonGroupCount > 0, loaded: true) + } else { + if view.peerId.namespace == Namespaces.Peer.CloudUser || view.peerId.namespace == Namespaces.Peer.SecretChat { + return (exist: false, loaded: false) + } + return (exist: false, loaded: true) + } + } |> map { data -> (tag: PeerMediaCollectionMode, exists: Bool, hasLoaded: Bool) in + return (tag: .commonGroups, exists: data.exist, hasLoaded: data.loaded) + } + + + let tabItems: [Signal<(tag: PeerMediaCollectionMode, exists: Bool, hasLoaded: Bool), NoError>] = self.tagsList.filter { !$0.tagsValue.isEmpty }.map { tags -> Signal<(tag: PeerMediaCollectionMode, exists: Bool, hasLoaded: Bool), NoError> in + return context.account.viewTracker.aroundMessageOfInterestHistoryViewForLocation(.peer(peerId), count: 3, tagMask: tags.tagsValue) + |> map { (view, _, _) -> (tag: PeerMediaCollectionMode, exists: Bool, hasLoaded: Bool) in + let hasLoaded = view.entries.count >= 3 || (!view.isLoading) + return (tag: tags, exists: !view.entries.isEmpty, hasLoaded: hasLoaded) + } + + } + + let mergedTabs = combineLatest(membersTab, combineLatest(tabItems), commonGroupsTab) |> map { members, general, commonGroups -> [(tag: PeerMediaCollectionMode, exists: Bool, hasLoaded: Bool)] in + var general = general + general.insert(members, at: 0) + general.append(commonGroups) + return general + } + + let tabSignal = combineLatest(queue: .mainQueue(), mergedTabs, modeValue.get()) + |> map { tabs, selected -> (tabs: [PeerMediaCollectionMode], selected: PeerMediaCollectionMode?, hasLoaded: Bool) in + var selectedValue = selected + if selected == nil || !tabs.contains(where: { $0.exists && $0.tag == selected }) { + if let selected = selected { + let index = tagsList.firstIndex(of: selected)! + var perhapsBest: PeerMediaCollectionMode? + for i in stride(from: index, to: -1, by: -1) { + if tabs.contains(where: { $0.exists && $0.tag == tagsList[i] }) { + perhapsBest = tagsList[i] + break } - - action.afterInvoke = { [weak strongSelf] in - strongSelf?.interactions.update(animated: false, {$0.withoutSelectionState()}) - strongSelf?.interactions.saveState() + } + selectedValue = perhapsBest ?? tabs.filter { $0.exists }.last?.tag ?? selected + } else { + selectedValue = tabs.filter { $0.exists }.first?.tag + } + + } + return (tabs: tabs.filter { $0.exists }.map { $0.tag }, selected: selectedValue, hasLoaded: tabs.reduce(true, { $0 && $1.hasLoaded })) + } + + tabsSignal.set(tabSignal) + + let data: Signal<(tabs: [PeerMediaCollectionMode], selected: PeerMediaCollectionMode?, hasLoaded: Bool), NoError> = tabsSignal.get() |> deliverOnMainQueue |> mapToSignal { [weak self] data in + guard let `self` = self else { + return .complete() + } + if let selected = data.selected { + switch selected { + case .members: + if !self.members.isLoaded() { + self.members.loadViewIfNeeded(self.genericView.view.bounds) + } + return self.members.ready.get() |> map { ready in + return data + } + case .commonGroups: + if !self.commonGroups.isLoaded() { + self.commonGroups.loadViewIfNeeded(self.genericView.view.bounds) + } + return self.commonGroups.ready.get() |> map { ready in + return data + } + case .photoOrVideo: + if !self.mediaGrid.isLoaded() { + if let externalSearchData = self.externalSearchData { + self.mediaGrid.setExternalSearch(externalSearchData.searchResult, externalSearchData.loadMore) } - + self.mediaGrid.loadViewIfNeeded(self.genericView.view.bounds) } - })) + return self.mediaGrid.ready.get() |> map { _ in + return data + } + case .gifs: + if !self.gifs.isLoaded() { + if let externalSearchData = self.externalSearchData { + self.gifs.setExternalSearch(externalSearchData.searchResult, externalSearchData.loadMore) + } + self.gifs.loadViewIfNeeded(self.genericView.view.bounds) + } + return self.gifs.ready.get() |> map { ready in + return data + } + default: + if !self.listControllers[Int(selected.rawValue)].isLoaded() { + if let externalSearchData = self.externalSearchData { + self.listControllers[Int(selected.rawValue)].setExternalSearch(externalSearchData.searchResult, externalSearchData.loadMore) + } + self.listControllers[Int(selected.rawValue)].loadViewIfNeeded(self.genericView.view.bounds) + self.listControllers[Int(selected.rawValue)].load(with: selected.tagsValue) + } + return self.listControllers[Int(selected.rawValue)].ready.get() |> map { _ in + return data + } + } + } else { + return .single(data) } } + |> distinctUntilChanged(isEqual: { lhs, rhs -> Bool in + if lhs.tabs != rhs.tabs { + return false + } + if lhs.hasLoaded != rhs.hasLoaded { + return false + } + if lhs.selected != rhs.selected { + return false + } + return true + }) + + let ready = data |> map { _ in return true } + + genericView.segmentPanelView.segmentControl.didChangeSelectedItem = { [weak self] item in + let newMode = PeerMediaCollectionMode(rawValue: item.uniqueId)! + + if newMode == self?.mode, let mainTable = self?.genericView.mainTable { + self?.currentMainTableView?(mainTable, true, true) + } + self?.modeValue.set(newMode) + } + + + interactions.forwardMessages = { messageIds in + showModal(with: ShareModalController(ForwardMessagesObject(context, messageIds: messageIds)), for: mainWindow) + } interactions.focusMessageId = { [weak self] _, focusMessageId, animated in if let strongSelf = self { - strongSelf.navigationController?.push(ChatController(account: strongSelf.account, peerId: strongSelf.peerId, messageId: focusMessageId)) + strongSelf.navigationController?.push(ChatController(context: context, chatLocation: .peer(strongSelf.peerId), messageId: focusMessageId)) } } interactions.inlineAudioPlayer = { [weak self] controller in - if let navigation = self?.navigationController, let strongSelf = self { - if let header = navigation.header { - header.show(true) - if let view = header.view as? InlineAudioPlayerView { - view.update(with: controller, tableView: strongSelf.mediaList.genericView) - } - } + guard let navigation = self?.navigationController else { + return } + let tableView = (navigation.first { $0 is ChatController} as? ChatController)?.genericView.tableView + let object = InlineAudioPlayerView.ContextObject(controller: controller, context: context, tableView: tableView, supportTableView: self?.currentTable) + navigation.header?.show(true, contextObject: object) } interactions.openInfo = { [weak self] (peerId, toChat, postId, action) in if let strongSelf = self { if toChat { - strongSelf.navigationController?.push(ChatController(account: strongSelf.account, peerId: peerId, messageId: postId, initialAction: action)) + strongSelf.navigationController?.push(ChatController(context: context, chatLocation: .peer(peerId), messageId: postId, initialAction: action)) } else { - strongSelf.openPeerInfoDisposable.set((strongSelf.account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue).start(next: { [weak strongSelf] peer in - if let strongSelf = strongSelf { - strongSelf.navigationController?.push(PeerInfoController(account: strongSelf.account, peer: peer)) - } - })) + strongSelf.navigationController?.push(PeerInfoController(context: context, peerId: peerId)) } } } interactions.deleteMessages = { [weak self] messageIds in - if let account = self?.account { - self?.messagesActionDisposable.set((account.postbox.messagesAtIds(messageIds) |> deliverOnMainQueue).start( next:{ [weak self] messages in - - var canDelete:Bool = true - var canDeleteForEveryone = true - - for message in messages { - if !canDeleteMessage(message, account: account) { - canDelete = false + if let strongSelf = self, let peer = strongSelf.peer { + + let adminsPromise = ValuePromise<[RenderedChannelParticipant]>([]) + _ = context.peerChannelMemberCategoriesContextsManager.admins(peerId: peerId, updated: { membersState in + if case .loading = membersState.loadingState, membersState.list.isEmpty { + adminsPromise.set([]) + } else { + adminsPromise.set(membersState.list) + } + }) + + + self?.messagesActionDisposable.set(combineLatest(queue: .mainQueue(), context.account.postbox.messagesAtIds(messageIds), adminsPromise.get()).start( next:{ [weak strongSelf] messages, admins in + if let strongSelf = strongSelf { + var canDelete:Bool = true + var canDeleteForEveryone = true + var otherCounter:Int32 = 0 + var _mustDeleteForEveryoneMessage: Bool = true + for message in messages { + if !canDeleteMessage(message, account: context.account, mode: .history) { + canDelete = false + } + if !mustDeleteForEveryoneMessage(message) { + _mustDeleteForEveryoneMessage = false + } + if !canDeleteForEveryoneMessage(message, context: context) { + canDeleteForEveryone = false + } else { + if message.effectiveAuthor?.id != context.peerId && !(context.limitConfiguration.canRemoveIncomingMessagesInPrivateChats && message.peers[message.id.peerId] is TelegramUser) { + if let peer = message.peers[message.id.peerId] as? TelegramGroup { + inner: switch peer.role { + case .member: + otherCounter += 1 + default: + break inner + } + } else { + otherCounter += 1 + } + } + } } - if !canDeleteForEveryoneMessage(message, account: account) { + + if otherCounter > 0 || peer.id == context.peerId { canDeleteForEveryone = false } - } - - if canDelete { - let thrid:String? = canDeleteForEveryone ? tr(.chatConfirmDeleteMessagesForEveryone) : nil + if messages.isEmpty { + strongSelf.interactions.update({$0.withoutSelectionState()}) + return + } - if let window = self?.window { - confirm(for: window, with: tr(.chatConfirmActionUndonable), and: tr(.chatConfirmDeleteMessages), thridTitle:thrid, successHandler: { [weak self] result in - let type:InteractiveMessagesDeletionType - switch result { - case .basic: - type = .forLocalPeer - case .thrid: - type = .forEveryone - } - _ = deleteMessagesInteractively(postbox: account.postbox, messageIds: messageIds, type: type).start() - self?.interactions.update({$0.withoutSelectionState()}) - }) + let context = strongSelf.context + + if canDelete { + let isAdmin = admins.filter({$0.peer.id == messages[0].author?.id}).first != nil + if mustManageDeleteMessages(messages, for: peer, account: strongSelf.context.account), let memberId = messages[0].author?.id, !isAdmin { + + let options:[ModalOptionSet] = [ModalOptionSet(title: L10n.supergroupDeleteRestrictionDeleteMessage, selected: true, editable: true), + ModalOptionSet(title: L10n.supergroupDeleteRestrictionBanUser, selected: false, editable: true), + ModalOptionSet(title: L10n.supergroupDeleteRestrictionReportSpam, selected: false, editable: true), + ModalOptionSet(title: L10n.supergroupDeleteRestrictionDeleteAllMessages, selected: false, editable: true)] + showModal(with: ModalOptionSetController(context: context, options: options, actionText: (L10n.modalOK, theme.colors.accent), title: L10n.supergroupDeleteRestrictionTitle, result: { [weak strongSelf] result in + + var signals:[Signal] = [] + if result[0] == .selected { + signals.append(context.engine.messages.deleteMessagesInteractively(messageIds: messages.map {$0.id}, type: .forEveryone)) + } + if result[1] == .selected { + signals.append(context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(peerId: peer.id, memberId: memberId, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max))) + } + if result[2] == .selected { + signals.append(context.engine.peers.reportPeerMessages(messageIds: messageIds, reason: .spam, message: "")) + } + if result[3] == .selected { + signals.append(context.engine.messages.clearAuthorHistory(peerId: peer.id, memberId: memberId)) + } + + _ = showModalProgress(signal: combineLatest(signals), for: context.window).start() + strongSelf?.interactions.update({$0.withoutSelectionState()}) + + }), for: context.window) + } else { + let thrid:String? = (canDeleteForEveryone ? peer.isUser ? L10n.chatMessageDeleteForMeAndPerson(peer.compactDisplayTitle) : L10n.chatConfirmDeleteMessagesForEveryone : nil) + + modernConfirm(for: context.window, account: context.account, peerId: nil, header: thrid == nil ? L10n.chatConfirmActionUndonable : L10n.chatConfirmDeleteMessages1Countable(messages.count), information: thrid == nil ? _mustDeleteForEveryoneMessage ? L10n.chatConfirmDeleteForEveryoneCountable(messages.count) : L10n.chatConfirmDeleteMessages1Countable(messages.count) : nil, okTitle: L10n.confirmDelete, thridTitle: thrid, successHandler: { [weak strongSelf] result in + + guard let `strongSelf` = strongSelf else { + return + } + let type:InteractiveMessagesDeletionType + switch result { + case .basic: + type = .forLocalPeer + case .thrid: + type = .forEveryone + } + _ = context.engine.messages.deleteMessagesInteractively(messageIds: messageIds, type: type).start() + strongSelf.interactions.update({$0.withoutSelectionState()}) + }) + } } } - })) } } - let peerSignal = account.viewTracker.peerView(peerId) |> deliverOnMainQueue |> beforeNext({ [weak self] peerView in + let peerSignal = context.account.viewTracker.peerView(peerId) |> deliverOnMainQueue |> beforeNext({ [weak self] peerView in self?.peer = peerView.peers[peerView.peerId] + self?.peerView = peerView }) |> map { view -> Bool in return true } - let combined = combineLatest( [peerSignal |> take(1), mediaGrid.ready.get()] ) |> map { result -> Bool in - return result[0] && result[1] + let combined = combineLatest( [peerSignal |> take(1), ready, self.tabsSignal.get() |> map { $0.hasLoaded }] ) |> map { result -> Bool in + return result[0] && result[1] && result[2] } self.ready.set(combined |> deliverOnMainQueue) - } - - override func loadView() { - super.loadView() - - mediaList.loadViewIfNeeded(bounds) - mediaGrid.loadViewIfNeeded(bounds) - mediaGrid.viewWillAppear(false) - genericView.updateMainView(with: mediaGrid.view, animated: false) - mediaGrid.viewDidAppear(false) + - requestUpdateCenterBar() + var firstTabAppear = true + tabsDisposable.set((data |> deliverOnMainQueue).start(next: { [weak self] tabs, selected, hasLoaded in + var items:[ScrollableSegmentItem] = [] + if hasLoaded, let `self` = self { + let insets = NSEdgeInsets(left: 10, right: 10, bottom: 2) + let segmentTheme = ScrollableSegmentTheme(background: .clear, border: .clear, selector: theme.colors.accent, inactiveText: theme.colors.grayText, activeText: theme.colors.accent, textFont: .normal(.title)) + for (i, tab) in tabs.enumerated() { + items.append(ScrollableSegmentItem(title: tab.title, index: i, uniqueId: tab.rawValue, selected: selected == tab, insets: insets, icon: nil, theme: segmentTheme, equatable: nil)) + } + self.genericView.segmentPanelView.segmentControl.updateItems(items, animated: !firstTabAppear) + if let selected = selected { + self.toggle(with: selected, animated: !firstTabAppear) + } + + firstTabAppear = false + + if tabs.isEmpty, self.isProfileIntended { + if self.genericView.superview != nil { + self.viewWillDisappear(true) + self.genericView.removeFromSuperview() + self.viewDidDisappear(true) + } + } + } + })) } - private func toggle(with mode:PeerMediaCollectionMode, animated:Bool = false) { + + private var currentTable: TableView? { + if self.mode == .photoOrVideo || self.mode == .members { + return nil + } else { + return self.listControllers[currentTagListIndex].genericView + } + } + + private func applyReadyController(mode:PeerMediaCollectionMode, animated:Bool) { + genericView.mainTable?.updatedItems = nil + let oldMode = self.mode + self.mode = mode + let previous = self.currentController + + let controller = self.controller(for: mode) + + self.currentController = controller + controller.viewWillAppear(animated) + previous?.viewWillDisappear(animated) + controller.view.frame = self.genericView.view.bounds + let animation: PeerMediaAnimationDirection? - if self.mode != mode { - self.mode = mode - if mode == .photoOrVideo { - mediaGrid.viewWillAppear(animated) - mediaList.viewWillDisappear(animated) - mediaGrid.view.frame = bounds - genericView.updateMainView(with: mediaGrid.view, animated: animated) - mediaGrid.viewDidAppear(animated) - mediaList.removeFromSuperview() - mediaList.viewDidDisappear(animated) + if animated, let oldMode = oldMode { + if oldMode.rawValue > mode.rawValue { + animation = .rightToLeft } else { - mediaList.viewWillAppear(animated) - mediaGrid.viewWillDisappear(animated) - mediaList.view.frame = bounds - genericView.updateMainView(with: mediaList.view, animated: animated) - mediaList.viewDidAppear(animated) - mediaGrid.removeFromSuperview() - mediaGrid.viewDidDisappear(animated) + animation = .leftToRight } + } else { + animation = nil + } + + genericView.updateMainView(with: controller.view, animated: animation) + controller.viewDidAppear(animated) + previous?.viewDidDisappear(animated) + searchValueDisposable.set(nil) + + + centerBar.updateSearchVisibility(mode != .commonGroups && externalSearchData == nil) + + + if let controller = controller as? PeerMediaSearchable { - if mode != .photoOrVideo { - mediaList.load(with: mode.tagsValue) + if let externalSearchData = self.externalSearchData { + controller.setExternalSearch(externalSearchData.searchResult, externalSearchData.loadMore) + } else { + searchValueDisposable.set(controller.mediaSearchValue.start(next: { [weak self, weak controller] state in + self?.genericView.updateSearchState(state, updateSearchState: { searchState in + controller?.setSearchValue(.single(searchState)) + }, toggle: { + controller?.toggleSearch() + }) + })) } } + var firstUpdate: Bool = true + genericView.mainTable?.updatedItems = { [weak self] items in + let filter = items.filter { + !($0 is PeerMediaEmptyRowItem) && !($0.className == "Telegram.GeneralRowItem") && !($0 is SearchEmptyRowItem) + } + self?.genericView.updateCorners(filter.isEmpty ? .all : [.topLeft, .topRight], animated: !firstUpdate) + firstUpdate = false + } + self.currentMainTableView?(genericView.mainTable, animated, previous != controller && genericView.segmentPanelView.segmentControl.contains(oldMode?.rawValue ?? -3)) } - override func requestUpdateCenterBar() { - (self.centerBarView as! MediaTitleBarView).updateLocalizationAndTheme() + func controller(for mode: PeerMediaCollectionMode) -> ViewController { + switch mode { + case .photoOrVideo: + return self.mediaGrid + case .members: + return self.members + case .commonGroups: + return self.commonGroups + case .gifs: + return self.gifs + default: + return self.listControllers[Int(mode.rawValue)] + } + } + + private func toggle(with mode:PeerMediaCollectionMode, animated:Bool = false) { + let isUpdated = self.mode != mode + if isUpdated { + let controller: ViewController = self.controller(for: mode) + + let ready = controller.ready.get() |> take(1) + + toggleDisposable.set(ready.start(next: { [weak self] _ in + self?.applyReadyController(mode: mode, animated: animated) + })) + } else { + self.currentMainTableView?(genericView.mainTable, animated, false) + } + self.modeValue.set(mode) } deinit { messagesActionDisposable.dispose() - openPeerInfoDisposable.dispose() loadFwdMessagesDisposable.dispose() + loadSelectionMessagesDisposable.dispose() + tabsDisposable.dispose() + toggleDisposable.dispose() + onlineMemberCountDisposable.dispose() + externalDisposable.dispose() + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + + for controller in self.listControllers { + if controller.isLoaded() { + controller.updateLocalizationAndTheme(theme: theme) + } + } + } override public func update(with state:ViewControllerState) -> Void { super.update(with:state) interactions.update({state == .Normal ? $0.withoutSelectionState() : $0.withSelectionState()}) } - -} - - + + override func escapeKeyAction() -> KeyHandlerResult { + if genericView.searchPanelView != nil { + self.listControllers[self.currentTagListIndex].toggleSearch() + return .invoked + } else if interactions.presentation.state == .selecting { + interactions.update { $0.withoutSelectionState() } + return .invoked + } else { + return super.escapeKeyAction() + } + } + + private var centerBar: SearchTitleBarView { + return centerBarView as! SearchTitleBarView + } + + private func searchGroupUsers() { + _ = (selectModalPeers(window: context.window, context: context, title: L10n.selectPeersTitleSearchMembers, behavior: peerId.namespace == Namespaces.Peer.CloudGroup ? SelectGroupMembersBehavior(peerId: peerId, limit: 1, settings: []) : SelectChannelMembersBehavior(peerId: peerId, peerChannelMemberContextsManager: context.peerChannelMemberCategoriesContextsManager, limit: 1, settings: [])) |> deliverOnMainQueue |> map {$0.first}).start(next: { [weak self] peerId in + if let peerId = peerId, let context = self?.context { + context.sharedContext.bindings.rootNavigation().push(PeerInfoController(context: context, peerId: peerId)) + } + }) + } + + override func getCenterBarViewOnce() -> TitledBarView { + return SearchTitleBarView(controller: self, title:.initialize(string: defaultBarTitle, color: theme.colors.text, font: .medium(.title)), handler: { [weak self] in + guard let `self` = self else { + return + } + if let mode = self.mode { + switch mode { + case .members: + self.searchGroupUsers() + case .commonGroups: + break + case .photoOrVideo: + (self.controller(for: mode) as? PeerMediaPhotosController)?.toggleSearch() + default: + (self.controller(for: mode) as? PeerMediaListController)?.toggleSearch() + } + } + }) + } + override func becomeFirstResponder() -> Bool? { + return true + } + + override func firstResponder() -> NSResponder? { + return genericView.searchPanelView?.searchView.input + } + + override var defaultBarTitle: String { + return super.defaultBarTitle + } + + override func backSettings() -> (String, CGImage?) { + return super.backSettings() + } + + override func didRemovedFromStack() { + super.didRemovedFromStack() + } + + + + override func initializer() -> PeerMediaContainerView { + return PeerMediaContainerView(frame: initializationRect, isSegmentHidden: self.externalSearchData != nil) + } + + override func navigationHeaderDidNoticeAnimation(_ current: CGFloat, _ previous: CGFloat, _ animated: Bool) -> () -> Void { + for mediaList in listControllers { + if mediaList.view.superview != nil { + return mediaList.navigationHeaderDidNoticeAnimation(current, previous, animated) + } + } + + if mediaGrid.view.superview != nil { + return mediaGrid.navigationHeaderDidNoticeAnimation(current, previous, animated) + } + return {} + } + + } + + + diff --git a/Telegram-Mac/PeerMediaDateItem.swift b/Telegram-Mac/PeerMediaDateItem.swift new file mode 100644 index 0000000000..f8e157e80e --- /dev/null +++ b/Telegram-Mac/PeerMediaDateItem.swift @@ -0,0 +1,140 @@ +// +// PeerMediaDateItem.swift +// Telegram +// +// Created by keepcoder on 27/11/2017. +// Copyright © 2017 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox + +class PeerMediaDateItem: TableStickItem { + + private let _stableId: AnyHashable + private let messageIndex: MessageIndex + fileprivate let textLayout: TextViewLayout + let viewType: GeneralViewType + let inset: NSEdgeInsets + + init(_ initialSize: NSSize, index: MessageIndex, stableId: AnyHashable) { + self.messageIndex = index + self._stableId = stableId + self.viewType = .modern(position: .single, insets: NSEdgeInsetsMake(3, 0, 3, 0)) + self.inset = NSEdgeInsets(left: 0, right: 0) + let timestamp = index.timestamp + + let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + + var t: time_t = time_t(timestamp) + var timeinfo: tm = tm() + localtime_r(&t, &timeinfo) + + var now: time_t = time_t(nowTimestamp) + var timeinfoNow: tm = tm() + localtime_r(&now, &timeinfoNow) + + let text: String + let dateFormatter = makeNewDateFormatter() + dateFormatter.timeZone = NSTimeZone.local + dateFormatter.dateFormat = "MMMM yyyy"; + text = dateFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(timestamp))).uppercased() + + textLayout = TextViewLayout(.initialize(string: text, color: theme.colors.listGrayText, font: .normal(.short))) + super.init(initialSize) + _ = makeSize(initialSize.width, oldWidth: 0) + } + + required init(_ initialSize: NSSize) { + self._stableId = AnyHashable(0) + self.messageIndex = MessageIndex.absoluteLowerBound() + self.textLayout = TextViewLayout(.initialize(string: "")) + self.viewType = .separator + self.inset = NSEdgeInsets(left: 30, right: 30) + super.init(initialSize) + } + + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) + textLayout.measure(width: width - 60) + return success + } + + override var stableId: AnyHashable { + return _stableId + } + + override var height: CGFloat { + return textLayout.layoutSize.height + viewType.innerInset.top + viewType.innerInset.bottom + 9 + } + + override func viewClass() -> AnyClass { + return PeerMediaDateView.self + } +} + +fileprivate class PeerMediaDateView : TableStickView { + private let containerView = GeneralRowContainerView(frame: NSZeroRect) + private let textView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(self.containerView) + containerView.addSubview(self.textView) + self.textView.disableBackgroundDrawing = true + self.textView.isSelectable = false + self.textView.userInteractionEnabled = false + } + + override var header: Bool { + didSet { + updateColors() + } + } + override func updateIsVisible(_ visible: Bool, animated: Bool) { + containerView.change(opacity: visible ? 1 : 0, animated: animated) + } + + override var backdorColor: NSColor { + return theme.colors.listBackground.withAlphaComponent(0.8) + } + + override func updateColors() { + guard let item = item as? PeerMediaDateItem else { + return + } + self.backgroundColor = item.viewType.rowBackground + self.containerView.backgroundColor = backdorColor + } + + override func layout() { + super.layout() + guard let item = item as? PeerMediaDateItem else { + return + } + let blockWidth = min(600, frame.width - item.inset.left - item.inset.right) + + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - blockWidth) / 2), item.inset.top, blockWidth, frame.height - item.inset.bottom - item.inset.top) + self.containerView.setCorners([]) + + textView.centerY(x: item.viewType.innerInset.left + 12) + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? PeerMediaDateItem else { + return + } + self.textView.update(item.textLayout) + + needsLayout = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/PeerMediaEmptyRowItem.swift b/Telegram-Mac/PeerMediaEmptyRowItem.swift index 53a384f2e5..14314b2da9 100644 --- a/Telegram-Mac/PeerMediaEmptyRowItem.swift +++ b/Telegram-Mac/PeerMediaEmptyRowItem.swift @@ -8,7 +8,7 @@ import Cocoa import TGUIKit -import PostboxMac +import Postbox @@ -21,16 +21,16 @@ class PeerMediaEmptyRowItem: TableRowItem { let attr:NSAttributedString if tags.contains(.file) { image = theme.icons.mediaEmptyFiles - attr = .initialize(string: tr(.peerMediaSharedFilesEmptyList), color: theme.colors.grayText, font: .normal(.header)) - } else if tags.contains(.music) { + attr = .initialize(string: tr(L10n.peerMediaSharedFilesEmptyList1), color: theme.colors.grayText, font: .normal(.header)) + } else if tags.contains(.music) || tags.contains(.voiceOrInstantVideo) { image = theme.icons.mediaEmptyMusic - attr = .initialize(string: tr(.peerMediaSharedMusicEmptyList), color: theme.colors.grayText, font: .normal(.header)) + attr = .initialize(string: tags.contains(.voiceOrInstantVideo) ? L10n.peerMediaSharedVoiceEmptyList : L10n.peerMediaSharedMusicEmptyList, color: theme.colors.grayText, font: .normal(.header)) } else if tags.contains(.webPage) { image = theme.icons.mediaEmptyLinks - attr = .initialize(string: tr(.peerMediaSharedLinksEmptyList), color: theme.colors.grayText, font: .normal(.header)) + attr = .initialize(string: tr(L10n.peerMediaSharedLinksEmptyList), color: theme.colors.grayText, font: .normal(.header)) } else { image = theme.icons.mediaEmptyShared - attr = .initialize(string: tr(.peerMediaSharedMediaEmptyList), color: theme.colors.grayText, font: .normal(.header)) + attr = .initialize(string: tr(L10n.peerMediaSharedMediaEmptyList), color: theme.colors.grayText, font: .normal(.header)) } textLayout = TextViewLayout(attr, alignment: .center) super.init(initialSize) @@ -63,23 +63,31 @@ class PeerMediaEmptyRowView : TableRowView { addSubview(imageView) } + override var backdorColor: NSColor { + return theme.colors.listBackground + } + + override func updateColors() { + super.updateColors() + textView.backgroundColor = backdorColor + } + override func layout() { super.layout() if let item = item as? PeerMediaEmptyRowItem { - - item.textLayout.measure(width: frame.width - 40) - let f = focus(item.textLayout.layoutSize) - textView.update(item.textLayout, origin:f.origin) - imageView.centerX(y:f.minY - imageView.frame.height - 20) + imageView.centerX(y: bounds.midY - imageView.frame.height) + item.textLayout.measure(width: frame.width - 60) + textView.update(item.textLayout) + textView.centerX(y: imageView.frame.maxY + 16) } } override func set(item: TableRowItem, animated: Bool) { super.set(item: item, animated: animated) if let item = item as? PeerMediaEmptyRowItem { - textView.backgroundColor = theme.colors.background imageView.image = item.image imageView.sizeToFit() + needsLayout = true } } diff --git a/Telegram-Mac/PeerMediaFileRowContent.swift b/Telegram-Mac/PeerMediaFileRowContent.swift index b1f124dd50..a4d291e90a 100644 --- a/Telegram-Mac/PeerMediaFileRowContent.swift +++ b/Telegram-Mac/PeerMediaFileRowContent.swift @@ -8,18 +8,19 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit class PeerMediaFileRowItem: PeerMediaRowItem { - private(set) var nameLayout:TextViewLayout! - private(set) var actionLayout:TextViewLayout! - private(set) var actionLayoutLocal:TextViewLayout! + private(set) var nameLayout:TextViewLayout + private(set) var actionLayout:TextViewLayout + private(set) var actionLayoutLocal:TextViewLayout private(set) var iconArguments:TransformImageArguments? private(set) var icon:TelegramMediaImage? @@ -27,66 +28,66 @@ class PeerMediaFileRowItem: PeerMediaRowItem { private(set) var docIcon:CGImage? private(set) var docTitle:NSAttributedString? - override init(_ initialSize:NSSize, _ interface:ChatInteraction, _ account:Account, _ object: PeerMediaSharedEntry) { - super.init(initialSize,interface,account,object) - iconSize = NSMakeSize(40, 40) + override init(_ initialSize:NSSize, _ interface:ChatInteraction, _ object: PeerMediaSharedEntry, viewType: GeneralViewType = .legacy) { - if let file = message.media.first as? TelegramMediaFile { - - self.file = file - - nameLayout = TextViewLayout(NSAttributedString.initialize(string: file.fileName ?? "Unknown", color: theme.colors.text, font: .medium(.text)), maximumNumberOfLines: 1, truncationType: .end) - - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "MMM d, yyyy 'at' h a" - - let dateString = dateFormatter.string(from: Date(timeIntervalSince1970: Double(TimeInterval(message.timestamp) - account.context.timeDifference))) - - actionLayout = TextViewLayout(NSAttributedString.initialize(string: "\(dataSizeString(file.size ?? 0)) • \(dateString)",color: theme.colors.grayText, font: NSFont.normal(FontSize.text)), maximumNumberOfLines: 1, truncationType: .end) - - let localAction = NSMutableAttributedString() - let range = localAction.append(string: tr(.contextShowInFinder), color: theme.colors.link, font: .normal(.text)) - localAction.add(link: inAppLink.callback("finder", { _ in - showInFinder(file, account: account) - }), for: range) - actionLayoutLocal = TextViewLayout(localAction, maximumNumberOfLines: 1, truncationType: .end) - actionLayoutLocal.interactions = globalLinkExecutor - - let iconImageRepresentation:TelegramMediaImageRepresentation? = smallestImageRepresentation(file.previewRepresentations) + + let message = object.message! + let file = message.media.first as! TelegramMediaFile + self.file = file + + nameLayout = TextViewLayout(NSAttributedString.initialize(string: file.fileName ?? "Unknown", color: theme.colors.text, font: .medium(.text)), maximumNumberOfLines: 1, truncationType: .end) + + let dateFormatter = makeNewDateFormatter() + dateFormatter.dateFormat = "MMM d, yyyy, h a" + + let dateString = dateFormatter.string(from: Date(timeIntervalSince1970: Double(TimeInterval(message.timestamp) - interface.context.timeDifference))) + + actionLayout = TextViewLayout(NSAttributedString.initialize(string: "\(dataSizeString(file.size ?? 0, formatting: DataSizeStringFormatting.current)) • \(dateString)",color: theme.colors.grayText, font: NSFont.normal(12.5)), maximumNumberOfLines: 1, truncationType: .end) + + let localAction = NSMutableAttributedString() + let range = localAction.append(string: tr(L10n.contextShowInFinder), color: theme.colors.link, font: .normal(.text)) + localAction.add(link: inAppLink.callback("finder", { _ in + showInFinder(file, account: interface.context.account) + }), for: range) + actionLayoutLocal = TextViewLayout(localAction, maximumNumberOfLines: 1, truncationType: .end) + actionLayoutLocal.interactions = globalLinkExecutor + + let iconImageRepresentation:TelegramMediaImageRepresentation? = smallestImageRepresentation(file.previewRepresentations) + + if let iconImageRepresentation = iconImageRepresentation { + iconArguments = TransformImageArguments(corners: ImageCorners(radius: .cornerRadius), imageSize: iconImageRepresentation.dimensions.size.aspectFilled(PeerMediaIconSize), boundingSize: PeerMediaIconSize, intrinsicInsets: NSEdgeInsets()) + icon = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [iconImageRepresentation], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + } else { + let fileName: String = file.fileName ?? "" - if let iconImageRepresentation = iconImageRepresentation { - iconArguments = TransformImageArguments(corners: ImageCorners( radius: iconSize.width / 2), imageSize: iconImageRepresentation.dimensions.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: NSEdgeInsets()) - icon = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [iconImageRepresentation]) - } else { - let fileName: String = file.fileName ?? "" - - var fileExtension: String? - if let range = fileName.range(of: ".", options: [.backwards]) { - fileExtension = fileName.substring(from: range.upperBound).lowercased() - } - docIcon = extensionImage(fileExtension: fileExtension ?? "file") - - - if let fileExtension = fileExtension { - docTitle = NSAttributedString.initialize(string: fileExtension, color: theme.colors.text, font: .medium(.text)) - } + var fileExtension: String = "file" + if let range = fileName.range(of: ".", options: [.backwards]) { + fileExtension = fileName[range.upperBound...].lowercased() } + if fileExtension.length > 5 { + fileExtension = "file" + } + docIcon = extensionImage(fileExtension: fileExtension) + + docTitle = NSAttributedString.initialize(string: fileExtension, color: theme.colors.text, font: .medium(.text)) + } + super.init(initialSize,interface,object, viewType: viewType) } - override func menuItems() -> Signal<[ContextMenuItem], Void> { - let signal = super.menuItems() - let account = self.account + override func menuItems(in location: NSPoint) -> Signal<[ContextMenuItem], NoError> { + let signal = super.menuItems(in: location) + let context = self.interface.context if let file = self.file { - return signal |> mapToSignal { items -> Signal<[ContextMenuItem], Void> in + return signal |> mapToSignal { items -> Signal<[ContextMenuItem], NoError> in var items = items - return account.postbox.mediaBox.resourceData(file.resource) |> deliverOnMainQueue |> map {data in + return context.account.postbox.mediaBox.resourceData(file.resource) |> deliverOnMainQueue |> map {data in if data.complete { - items.append(ContextMenuItem(tr(.contextCopyMedia), handler: { - saveAs(file, account: account) + items.append(ContextMenuItem(L10n.contextCopyMedia, handler: { + saveAs(file, account: context.account) })) - items.append(ContextMenuItem(tr(.contextShowInFinder), handler: { - showInFinder(file, account: account) + items.append(ContextMenuItem(L10n.contextShowInFinder, handler: { + showInFinder(file, account: context.account) })) } return items @@ -98,13 +99,11 @@ class PeerMediaFileRowItem: PeerMediaRowItem { } override func makeSize(_ width: CGFloat, oldWidth:CGFloat) -> Bool { - - nameLayout.measure(width: width - contentInset.left - contentInset.right) - actionLayout.measure(width: width - contentInset.left - contentInset.right) - actionLayoutLocal.measure(width: width - contentInset.left - contentInset.right) - contentSize = NSMakeSize(width, 50) - - return super.makeSize(width, oldWidth: oldWidth) + let success = super.makeSize(width, oldWidth: oldWidth) + nameLayout.measure(width: self.blockWidth - contentInset.left - contentInset.right - self.viewType.innerInset.left - self.viewType.innerInset.right) + actionLayout.measure(width: self.blockWidth - contentInset.left - contentInset.right - self.viewType.innerInset.left - self.viewType.innerInset.right) + actionLayoutLocal.measure(width: self.blockWidth - contentInset.left - contentInset.right - self.viewType.innerInset.left - self.viewType.innerInset.right) + return success } override func viewClass() -> AnyClass { @@ -116,7 +115,7 @@ class PeerMediaFileRowView : PeerMediaRowView { var nameView:TextView = TextView() var actionView:TextView = TextView() - var imageView:TransformImageView = TransformImageView(frame:NSMakeRect(10, 5, 40, 40)) + var imageView:TransformImageView = TransformImageView(frame:NSMakeRect(0, 0, 40, 40)) private var downloadStatusControl:ImageView? private var downloadProgressView:LinearProgressControl? @@ -127,10 +126,9 @@ class PeerMediaFileRowView : PeerMediaRowView { private let fetchDisposable = MetaDisposable() required init(frame frameRect: NSRect) { - nameView.userInteractionEnabled = false nameView.isSelectable = false + nameView.userInteractionEnabled = false actionView.isSelectable = false - super.init(frame: frameRect) addSubview(imageView) addSubview(nameView) @@ -140,43 +138,38 @@ class PeerMediaFileRowView : PeerMediaRowView { func cancel() -> Void { - + cancelFetching() } func delete() -> Void { if let item = item as? PeerMediaFileRowItem { - _ = item.account.postbox.modify({ modifier -> Void in - modifier.deleteMessages([item.message.id]) - }).start() + let messageId = item.message.id + let engine = item.interface.context.engine + _ = item.interface.context.account.postbox.transaction { transaction -> Void in + engine.messages.deleteMessages(transaction: transaction, ids: [messageId]) + }.start() } } func cancelFetching() { if let item = item as? PeerMediaFileRowItem, let file = item.file { - chatMessageFileCancelInteractiveFetch(account: item.account, file: file) + messageMediaFileCancelInteractiveFetch(context: item.interface.context, messageId: item.message.id, fileReference: FileMediaReference.message(message: MessageReference(item.message), media: file)) } } func open() -> Void { if let item = item as? PeerMediaFileRowItem, let file = item.file { if file.isGraphicFile { - showChatGallery(account: item.account, message: item.message, item.table, nil) + showChatGallery(context: item.interface.context, message: item.message, item.table, nil) } else { - QuickLookPreview.current.show(account: item.account, with: file, stableId:item.message.chatStableId, item.table) + QuickLookPreview.current.show(context: item.interface.context, with: file, stableId:item.message.chatStableId, item.table) } } } func fetch() -> Void { if let item = item as? PeerMediaFileRowItem, let file = item.file { - let account = item.account - fetchDisposable.set((chatMessageFileInteractiveFetched(account: item.account, file: file) |> mapToSignal { source -> Signal in - if source == .remote { - return copyToDownloads(file, account: account) - } else { - return .single(Void()) - } - }).start()) + fetchDisposable.set(messageMediaFileInteractiveFetched(context: item.interface.context, messageId: item.message.id, fileReference: FileMediaReference.message(message: MessageReference(item.message), media: file)).start()) } } @@ -210,6 +203,15 @@ class PeerMediaFileRowView : PeerMediaRowView { if imageView._mouseInside() { executeInteraction(true) return + } else if let status = self.fetchStatus { + switch status { + case .Remote: + executeInteraction(true) + case .Fetching: + executeInteraction(true) + default: + break + } } } super.mouseUp(with: event) @@ -218,19 +220,20 @@ class PeerMediaFileRowView : PeerMediaRowView { override func layout() { super.layout() if let item = item as? PeerMediaRowItem { - - downloadProgressView?.frame = NSMakeRect(item.contentInset.left,frame.height - 4,frame.width - item.contentInset.left - item.contentInset.right,4) - + if let downloadProgressView = downloadProgressView { + downloadProgressView.frame = NSMakeRect(item.separatorOffset, containerView.frame.height - 4, containerView.frame.width - item.separatorOffset - item.viewType.innerInset.right, 4) + } if let downloadStatusControl = downloadStatusControl { + downloadStatusControl.setFrameOrigin(NSMakePoint(item.contentInset.left, contentView.frame.height - 4 - downloadStatusControl.frame.height)) actionView.setFrameOrigin(item.contentInset.left + downloadStatusControl.frame.width + 2.0,actionView.frame.minY) } else { - actionView.setFrameOrigin(item.contentInset.left,actionView.frame.minY) + actionView.setFrameOrigin(item.contentInset.left, actionView.frame.minY) } } } - override var interactionContentView: NSView { + override func interactionContentView(for innerId: AnyHashable, animateIn: Bool ) -> NSView { return imageView } @@ -241,7 +244,19 @@ class PeerMediaFileRowView : PeerMediaRowView { fatalError("init(coder:) has not been implemented") } + override func updateSelectingMode(with selectingMode: Bool, animated: Bool = false) { + super.updateSelectingMode(with: selectingMode, animated: animated) + if let status = fetchStatus { + if case .Local = status { + self.actionView.userInteractionEnabled = !selectingMode + } else { + self.actionView.userInteractionEnabled = false + } + } + } + override func set(item: TableRowItem, animated: Bool) { + let previous = self.item as? PeerMediaFileRowItem super.set(item: item, animated: animated) statusDisposable.set(nil) @@ -249,16 +264,18 @@ class PeerMediaFileRowView : PeerMediaRowView { actionView.backgroundColor = theme.colors.background if let item = item as? PeerMediaFileRowItem { nameView.update(item.nameLayout, origin: NSMakePoint(item.contentInset.left, item.contentInset.top + 2)) - actionView.update(item.actionLayout, origin: NSMakePoint(item.contentInset.left, item.contentSize.height - item.actionLayout.layoutSize.height - item.contentInset.bottom - 2)) - - let updateIconImageSignal:Signal<(TransformImageArguments) -> DrawingContext?,NoError> + if actionView.layout == nil { + actionView.update(item.actionLayout, origin: NSMakePoint(item.contentInset.left, item.contentSize.height - item.actionLayout.layoutSize.height - item.contentInset.bottom - 2)) + } + + let updateIconImageSignal:Signal if let icon = item.icon { - updateIconImageSignal = chatWebpageSnippetPhoto(account: item.account, photo: icon, scale: backingScaleFactor, small:true) + updateIconImageSignal = chatWebpageSnippetPhoto(account: item.interface.context.account, imageReference: ImageMediaReference.message(message: MessageReference(item.message), media: icon), scale: backingScaleFactor, small:true) } else { updateIconImageSignal = .complete() } - imageView.setSignal(account: item.account, signal: updateIconImageSignal) + imageView.setSignal( updateIconImageSignal) if let arguments = item.iconArguments { imageView.set(arguments: arguments) } else { @@ -269,9 +286,9 @@ class PeerMediaFileRowView : PeerMediaRowView { var updatedStatusSignal: Signal? var updatedFetchControls: FetchControls? - let account = item.account + let context = item.interface.context if let file = item.file { - updatedStatusSignal = chatMessageFileStatus(account: account, file: file) + updatedStatusSignal = chatMessageFileStatus(account: context.account, file: file, approximateSynchronousValue: false) updatedFetchControls = FetchControls(fetch: { [weak self] in self?.executeInteraction(true) }) @@ -283,31 +300,60 @@ class PeerMediaFileRowView : PeerMediaRowView { self.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak self] status in if let strongSelf = self { strongSelf.fetchStatus = status + if case .Local = status { + strongSelf.actionView.userInteractionEnabled = (strongSelf.item as? PeerMediaRowItem)?.interface.presentation.state != .selecting + } else { + strongSelf.actionView.userInteractionEnabled = false + } + + let initStatusControlIfNeeded = { [weak strongSelf] in + if let strongSelf = strongSelf, strongSelf.downloadStatusControl == nil { + strongSelf.downloadStatusControl = ImageView(frame:NSMakeRect(0, 0, theme.icons.peerMediaDownloadFileStart.backingSize.width, theme.icons.peerMediaDownloadFileStart.backingSize.height)) + strongSelf.downloadStatusControl?.animates = true + strongSelf.addSubview(strongSelf.downloadStatusControl!) + strongSelf.needsLayout = true + } + } let initDownloadControlIfNeeded = { [weak strongSelf] in - if let strongSelf = strongSelf, strongSelf.downloadStatusControl == nil { - strongSelf.downloadStatusControl = ImageView(frame:NSMakeRect(item.contentInset.left, strongSelf.frame.height - theme.icons.peerMediaDownloadFileStart.backingSize.height - item.contentInset.bottom - 4.0, theme.icons.peerMediaDownloadFileStart.backingSize.width, theme.icons.peerMediaDownloadFileStart.backingSize.height)) - strongSelf.addSubview(strongSelf.downloadStatusControl!) - + if let strongSelf = strongSelf { + if strongSelf.downloadStatusControl == nil { + strongSelf.downloadStatusControl = ImageView(frame:NSMakeRect(0, 0, theme.icons.peerMediaDownloadFileStart.backingSize.width, theme.icons.peerMediaDownloadFileStart.backingSize.height)) + strongSelf.downloadStatusControl?.animates = true + strongSelf.addSubview(strongSelf.downloadStatusControl!) + } if strongSelf.downloadProgressView == nil { strongSelf.downloadProgressView = LinearProgressControl() - strongSelf.addSubview(strongSelf.downloadProgressView!) + strongSelf.downloadProgressView?.cornerRadius = 2.0 + strongSelf.containerView.addSubview(strongSelf.downloadProgressView!) } - strongSelf.downloadProgressView?.style = ControlStyle(foregroundColor:theme.colors.blueUI) + strongSelf.downloadProgressView?.style = ControlStyle(foregroundColor:theme.colors.accent) strongSelf.needsLayout = true } - } - let deinitDownloadControls = {[weak strongSelf] in + let deinitDownloadControls = { [weak strongSelf] in if let strongSelf = strongSelf { - strongSelf.downloadProgressView?.removeFromSuperview() - strongSelf.downloadProgressView = nil + if let downloadProgressView = strongSelf.downloadProgressView { + strongSelf.downloadProgressView = nil + downloadProgressView.set(progress: 1.0) + downloadProgressView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak downloadProgressView] _ in + downloadProgressView?.removeFromSuperview() + }) + } strongSelf.downloadStatusControl?.removeFromSuperview() strongSelf.downloadStatusControl = nil } strongSelf?.needsLayout = true } + + let deinitProgressControl = { [weak strongSelf] in + if let strongSelf = strongSelf { + strongSelf.downloadProgressView?.removeFromSuperview() + strongSelf.downloadProgressView = nil + } + strongSelf?.needsLayout = true + } switch status { @@ -315,7 +361,8 @@ class PeerMediaFileRowView : PeerMediaRowView { deinitDownloadControls() strongSelf.actionView.update(item.actionLayoutLocal) case .Remote: - initDownloadControlIfNeeded() + deinitProgressControl() + initStatusControlIfNeeded() strongSelf.downloadStatusControl?.image = theme.icons.peerMediaDownloadFileStart strongSelf.actionView.update(item.actionLayout) break diff --git a/Telegram-Mac/PeerMediaGifsController.swift b/Telegram-Mac/PeerMediaGifsController.swift new file mode 100644 index 0000000000..e8b740caa6 --- /dev/null +++ b/Telegram-Mac/PeerMediaGifsController.swift @@ -0,0 +1,285 @@ +// +// PeerMediaGifsController.swift +// Telegram +// +// Created by Mikhail Filimonov on 12/05/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore +import Postbox +import SwiftSignalKit + + +private final class PeerMediaGifsArguments { + let context: AccountContext + let chatInteraction: ChatInteraction + let gallerySupplyment: InteractionContentViewProtocol + let openMessage: (Message)->Void + let menuItems:(Message, NSView)->Signal<[ContextMenuItem], NoError> + init(context: AccountContext, chatInteraction: ChatInteraction, gallerySupplyment: InteractionContentViewProtocol, openMessage: @escaping(Message)->Void, menuItems: @escaping(Message, NSView)->Signal<[ContextMenuItem], NoError>) { + self.context = context + self.gallerySupplyment = gallerySupplyment + self.chatInteraction = chatInteraction + self.menuItems = menuItems + self.openMessage = openMessage + } +} + + +final class PeerMediaGifsView : View { + let tableView: TableView = TableView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(tableView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + override func layout() { + super.layout() + tableView.frame = bounds + } +} + +private func mediaEntires(state: PeerMediaGifsState, initialSize: NSSize) -> [InputContextEntry] { + + let values = makeChatGridMediaEnties(state.messages, initialSize: NSMakeSize(initialSize.width, 100)) + + var wrapped:[InputContextEntry] = [] + for value in values { + wrapped.append(InputContextEntry.contextMediaResult(nil, value, Int64(arc4random()) | ((Int64(wrapped.count) << 40)))) + } + + return wrapped +} + + +private struct PeerMediaGifsState : Equatable { + let isLoading: Bool + let messages:[Message] + init(isLoading: Bool, messages: [Message]) { + self.isLoading = isLoading + self.messages = messages.reversed() + } + func withAppendMessages(_ collection: [Message]) -> PeerMediaGifsState { + var messages = self.messages + messages.append(contentsOf: collection) + return PeerMediaGifsState(isLoading: self.isLoading, messages: messages) + } + func withUpdatedMessages(_ collection: [Message]) -> PeerMediaGifsState { + return PeerMediaGifsState(isLoading: self.isLoading, messages: collection) + } + func withUpdatedLoading(_ isLoading: Bool) -> PeerMediaGifsState { + return PeerMediaGifsState(isLoading: isLoading, messages: self.messages) + } +} + +private final class PeerMediaGifsSupplyment : InteractionContentViewProtocol { + private weak var tableView: TableView? + init(tableView: TableView) { + self.tableView = tableView + } + + func contentInteractionView(for stableId: AnyHashable, animateIn: Bool) -> NSView? { + if let stableId = stableId.base as? ChatHistoryEntryId, let tableView = tableView { + switch stableId { + case let .message(message): + var found: NSView? = nil + tableView.enumerateItems { item -> Bool in + if let item = item as? ContextMediaRowItem { + if item.contains(message.id) { + found = item.view?.interactionContentView(for: message.id, animateIn: animateIn) + } + } + return found == nil + } + return found + default: + break + } + } + return nil + } + func interactionControllerDidFinishAnimation(interactive: Bool, for stableId: AnyHashable) { + + } + func addAccesoryOnCopiedView(for stableId: AnyHashable, view: NSView) { + if let stableId = stableId.base as? ChatHistoryEntryId, let tableView = tableView { + switch stableId { + case let .message(message): + tableView.enumerateItems { item -> Bool in + if let item = item as? PeerPhotosMonthItem { + if item.contains(message.id) { + item.view?.addAccesoryOnCopiedView(innerId: message.id, view: view) + return false + } + } + return true + } + default: + break + } + } + } + func videoTimebase(for stableId: AnyHashable) -> CMTimebase? { + return nil + } + func applyTimebase(for stableId: AnyHashable, timebase: CMTimebase?) { + + } +} + +fileprivate func prepareTransition(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], animated: Bool, initialSize:NSSize, arguments: PeerMediaGifsArguments) -> TableUpdateTransition { + let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in + switch entry.entry { + case let .contextMediaResult(_, row, index): + return ContextMediaRowItem(initialSize, row, index, arguments.context, ContextMediaArguments(openMessage: arguments.openMessage, messageMenuItems: arguments.menuItems)) + default: + fatalError("not supported") + } + } + return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: animated) +} + +class PeerMediaGifsController: TelegramGenericViewController { + + private let peerId: PeerId + private let historyDisposable = MetaDisposable() + private let disposable = MetaDisposable() + private let chatInteraction: ChatInteraction + private let previous: Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) + init(_ context: AccountContext, chatInteraction: ChatInteraction, peerId: PeerId) { + self.peerId = peerId + self.chatInteraction = chatInteraction + super.init(context) + } + + deinit { + historyDisposable.dispose() + } + + override func viewDidLoad() { + super.viewDidLoad() + + let context = self.context + let peerId = self.peerId + let initialSize = self.atomicSize + let chatInteraction = self.chatInteraction + + + self.genericView.tableView.emptyItem = PeerMediaEmptyRowItem(NSZeroSize, tags: .gif) + + let perPageCount:()->Int = { + var rowCount:Int = 4 + var perWidth: CGFloat = 0 + let blockWidth = min(600, initialSize.with { $0.width } - 60) + while true { + let maximum = blockWidth - 7 - 7 - CGFloat(rowCount * 2) + perWidth = maximum / CGFloat(rowCount) + if perWidth >= 90 { + break + } else { + rowCount -= 1 + } + } + return Int((initialSize.with { $0.height } / perWidth) * CGFloat(rowCount) + CGFloat(rowCount)) + } + + var requestCount = perPageCount() + 20 + + let location: ValuePromise = ValuePromise(.Initial(count: requestCount), ignoreRepeated: true) + + let initialState = PeerMediaGifsState(isLoading: false, messages: []) + let state: ValuePromise = ValuePromise() + let stateValue: Atomic = Atomic(value: initialState) + let updateState:((PeerMediaGifsState)->PeerMediaGifsState) -> Void = { f in + state.set(stateValue.modify(f)) + } + + let supplyment = PeerMediaGifsSupplyment(tableView: genericView.tableView) + + let arguments = PeerMediaGifsArguments(context: context, chatInteraction: chatInteraction, gallerySupplyment: supplyment, openMessage: { message in + showChatGallery(context: context, message: message, supplyment, nil, type: .history, reversed: true) + }, menuItems: { message, view in + return .single([]) + }) + + + let applyHole:() -> Void = { + location.set(.Initial(count: requestCount)) + } + + let history = location.get() |> mapToSignal { location in + return chatHistoryViewForLocation(location, context: context, chatLocation: .peer(peerId), fixedCombinedReadStates: nil, tagMask: [.gif]) + } + + self.historyDisposable.set(history.start(next: { update in + + let isLoading: Bool + let view: MessageHistoryView? + let updateType: ChatHistoryViewUpdateType + switch update { + case let .Loading(_, ut): + view = nil + isLoading = true + updateType = ut + case let .HistoryView(values): + view = values.view + isLoading = values.view.isLoading + updateType = values.type + } + + switch updateType { + case let .Generic(type: type): + switch type { + case .FillHole: + DispatchQueue.main.async(execute: applyHole) + default: + break + } + default: + break + } + let messages = view?.entries.map { value in + return value.message + } ?? [] + + updateState { + $0.withUpdatedMessages(messages).withUpdatedLoading(false).withUpdatedLoading(isLoading) + } + })) + + let previous = self.previous + + let transition: Signal = combineLatest(queue: prepareQueue, state.get(), appearanceSignal) |> mapToSignal { state, appearance in + let entries = mediaEntires(state: state, initialSize: initialSize.with { $0 }).map { AppearanceWrapperEntry(entry: $0, appearance: appearance) } + return .single(prepareTransition(left: previous.swap(entries), right: entries, animated: true, initialSize: initialSize.with { $0 }, arguments: arguments)) + } |> deliverOnMainQueue + + + + disposable.set(transition.start(next: { [weak self] transition in + guard let `self` = self else { + return + } + self.genericView.tableView.merge(with: transition) + self.readyOnce() + })) + + genericView.tableView.setScrollHandler { position in + switch position.direction { + case .bottom: + requestCount += perPageCount() * 10 + location.set(.Initial(count: requestCount)) + default: + break + } + } + } +} diff --git a/Telegram-Mac/PeerMediaGridController.swift b/Telegram-Mac/PeerMediaGridController.swift deleted file mode 100644 index 0a1b991997..0000000000 --- a/Telegram-Mac/PeerMediaGridController.swift +++ /dev/null @@ -1,441 +0,0 @@ -// -// PeerMediaGridController.swift -// Telegram-Mac -// -// Created by keepcoder on 26/10/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac -import TGUIKit - -public enum ChatHistoryNodeHistoryState: Equatable { - case loading - case loaded(isEmpty: Bool) - - public static func ==(lhs: ChatHistoryNodeHistoryState, rhs: ChatHistoryNodeHistoryState) -> Bool { - switch lhs { - case .loading: - if case .loading = rhs { - return true - } else { - return false - } - case let .loaded(isEmpty): - if case .loaded(isEmpty) = rhs { - return true - } else { - return false - } - } - } -} - -struct ChatHistoryGridViewTransition { - let historyView: ChatHistoryView - let topOffsetWithinMonth: Int - let deleteItems: [Int] - let insertItems: [GridNodeInsertItem] - let updateItems: [GridNodeUpdateItem] - let scrollToItem: GridNodeScrollToItem? - let stationaryItems: GridNodeStationaryItems -} - -struct ChatHistoryViewTransitionInsertEntry { - let index: Int - let previousIndex: Int? - let entry: ChatHistoryEntry - let directionHint: ListViewItemOperationDirectionHint? -} - -struct ChatHistoryViewTransitionUpdateEntry { - let index: Int - let previousIndex: Int - let entry: ChatHistoryEntry - let directionHint: ListViewItemOperationDirectionHint? -} - -private func mappedInsertEntries(account: Account, peerId: PeerId, controllerInteraction: ChatInteraction, entries: [ChatHistoryViewTransitionInsertEntry]) -> [GridNodeInsertItem] { - return entries.map { entry -> GridNodeInsertItem in - switch entry.entry { - case let .MessageEntry(message, _, _, _, _): - return GridNodeInsertItem(index: entry.index, item: GridMessageItem(account: account, message: message, chatInteraction: controllerInteraction), previousIndex: entry.previousIndex) - case .HoleEntry: - return GridNodeInsertItem(index: entry.index, item: GridHoleItem(), previousIndex: entry.previousIndex) - case .UnreadEntry: - assertionFailure() - return GridNodeInsertItem(index: entry.index, item: GridHoleItem(), previousIndex: entry.previousIndex) - default: - return GridNodeInsertItem(index: entry.index, item: GridHoleItem(), previousIndex: entry.previousIndex) - } - } -} - -private func mappedUpdateEntries(account: Account, peerId: PeerId, controllerInteraction: ChatInteraction, entries: [ChatHistoryViewTransitionUpdateEntry]) -> [GridNodeUpdateItem] { - return entries.map { entry -> GridNodeUpdateItem in - switch entry.entry { - case let .MessageEntry(message, _, _, _, _): - return GridNodeUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: GridMessageItem(account: account, message: message, chatInteraction: controllerInteraction)) - case .HoleEntry: - return GridNodeUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: GridHoleItem()) - case .UnreadEntry: - assertionFailure() - return GridNodeUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: GridHoleItem()) - default: - assertionFailure() - return GridNodeUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: GridHoleItem()) - } - } -} - -private func mappedChatHistoryViewListTransition(account: Account, peerId: PeerId, controllerInteraction: ChatInteraction, transition: ChatHistoryViewTransition, from: ChatHistoryView?) -> ChatHistoryGridViewTransition { - var mappedScrollToItem: GridNodeScrollToItem? - if let scrollToItem = transition.scrollToItem { - let mappedPosition: GridNodeScrollToItemPosition - switch scrollToItem.position { - case .Top: - mappedPosition = .top - case .Center: - mappedPosition = .center - case .Bottom: - mappedPosition = .bottom - } - let scrollTransition: ContainedViewLayoutTransition - if scrollToItem.animated { - switch scrollToItem.curve { - case .Default: - scrollTransition = .animated(duration: 0.3, curve: .easeInOut) - case let .Spring(duration): - scrollTransition = .animated(duration: duration, curve: .spring) - } - } else { - scrollTransition = .immediate - } - let directionHint: GridNodePreviousItemsTransitionDirectionHint - switch scrollToItem.directionHint { - case .Up: - directionHint = .up - case .Down: - directionHint = .down - } - mappedScrollToItem = GridNodeScrollToItem(index: scrollToItem.index, position: mappedPosition, transition: scrollTransition, directionHint: directionHint, adjustForSection: true, adjustForTopInset: true) - } - - var stationaryItems: GridNodeStationaryItems = .none - if let previousView = from { - if let stationaryRange = transition.stationaryItemRange { - var fromStableIds = Set() - for i in 0 ..< previousView.filteredEntries.count { - if i >= stationaryRange.0 && i <= stationaryRange.1 { - fromStableIds.insert(previousView.filteredEntries[i].entry.stableId) - } - } - var index = 0 - var indices = Set() - for entry in transition.historyView.filteredEntries { - if fromStableIds.contains(entry.entry.stableId) { - indices.insert(transition.historyView.filteredEntries.count - 1 - index) - } - index += 1 - } - stationaryItems = .indices(indices) - } else { - var fromStableIds = Set() - for i in 0 ..< previousView.filteredEntries.count { - fromStableIds.insert(previousView.filteredEntries[i].entry.stableId) - } - var index = 0 - var indices = Set() - for entry in transition.historyView.filteredEntries { - if fromStableIds.contains(entry.entry.stableId) { - indices.insert(transition.historyView.filteredEntries.count - 1 - index) - } - index += 1 - } - stationaryItems = .indices(indices) - } - } - - - return ChatHistoryGridViewTransition(historyView: transition.historyView, topOffsetWithinMonth: 0, deleteItems: transition.deleteItems.map { $0.index }, insertItems: mappedInsertEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, entries: transition.updateEntries), scrollToItem: mappedScrollToItem, stationaryItems: stationaryItems) -} - - - - -private func mappedInsertEntries(account: Account, chatInteraction: ChatInteraction, entries: [(Int,ChatHistoryEntry,Int?)]) -> [GridNodeInsertItem] { - return entries.map { entry -> GridNodeInsertItem in - switch entry.1 { - case let .MessageEntry(message, _, _, _, _): - return GridNodeInsertItem(index: entry.0, item: GridMessageItem(account: account, message: message, chatInteraction: chatInteraction), previousIndex: entry.2) - case .HoleEntry: - return GridNodeInsertItem(index: entry.0, item: GridHoleItem(), previousIndex: entry.2) - case .UnreadEntry: - assertionFailure() - return GridNodeInsertItem(index: entry.0, item: GridHoleItem(), previousIndex: entry.2) - case .DateEntry: - return GridNodeInsertItem(index: entry.0, item: GridHoleItem(), previousIndex: entry.2) - default: - fatalError() - } - } -} - -private func mappedUpdateEntries(account: Account, chatInteraction: ChatInteraction, entries: [(Int,ChatHistoryEntry,Int)]) -> [GridNodeUpdateItem] { - return entries.map { entry -> GridNodeUpdateItem in - switch entry.1 { - case let .MessageEntry(message, _, _, _, _): - return GridNodeUpdateItem(index: entry.0, previousIndex: entry.2, item: GridMessageItem(account: account, message: message, chatInteraction: chatInteraction)) - case .HoleEntry: - return GridNodeUpdateItem(index: entry.0, previousIndex: entry.2, item: GridHoleItem()) - case .UnreadEntry: - assertionFailure() - return GridNodeUpdateItem(index: entry.0, previousIndex: entry.2, item: GridHoleItem()) - case .DateEntry: - return GridNodeUpdateItem(index: entry.0, previousIndex: entry.2, item: GridHoleItem()) - default: - fatalError() - } - } -} - - - -private func itemSizeForContainerLayout(size: CGSize) -> CGSize { - let side = floor(size.width / 4.0) - return CGSize(width: side, height: side) -} - -class PeerMediaGridView : View { - let grid:GridNode - var emptyView:PeerMediaEmptyRowView - var emptyItem:PeerMediaEmptyRowItem = PeerMediaEmptyRowItem(NSZeroSize, tags: .photoOrVideo) - required init(frame frameRect: NSRect) { - grid = GridNode(frame: NSMakeRect(0, 0, frameRect.width, frameRect.height)) - emptyView = PeerMediaEmptyRowView(frame: NSMakeRect(0, 0, frameRect.width, frameRect.height)) - emptyView.set(item: emptyItem, animated: false) - super.init(frame: frameRect) - addSubview(grid) - addSubview(emptyView) - update(hasEntities: true) - } - - func update(hasEntities: Bool) { - grid.isHidden = !hasEntities - emptyView.isHidden = hasEntities - } - - override func layout() { - super.layout() - grid.frame = bounds - emptyView.frame = bounds - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -class PeerMediaGridController: GenericViewController { - - private let account: Account - private let peerId: PeerId - private let messageId: MessageId? - private let tagMask: MessageTags? - private let previousView = Atomic(value: nil) - - public let historyState = ValuePromise() - private var currentHistoryState: ChatHistoryNodeHistoryState? - private var enqueuedHistoryViewTransition: (ChatHistoryGridViewTransition, () -> Void)? - var layoutActionOnViewTransition: ((ChatHistoryGridViewTransition) -> (ChatHistoryGridViewTransition, ListViewUpdateSizeAndInsets?))? - - private var historyView: ChatHistoryView? - - private let historyDisposable = MetaDisposable() - - private let _chatHistoryLocation = ValuePromise(ignoreRepeated: true) - private var chatHistoryLocation: Signal { - return self._chatHistoryLocation.get() - } - - - private var requestCount:Int { - return Int((frame.width / 100) * (frame.height / 100)) * 4 - } - - func enableScroll() -> Void { - genericView.grid.visibleItemsUpdated = { [weak self] visibleItems in - - if let strongSelf = self, let historyView = strongSelf.historyView, let top = visibleItems.top, let bottom = visibleItems.bottom { - if top.0 < 5 && historyView.originalView.laterId != nil { - // - let lastEntry = historyView.filteredEntries[min(max(historyView.filteredEntries.count - 1 - top.0, 0), historyView.filteredEntries.count - 1)] - let location = ChatHistoryLocation.Navigation(index: lastEntry.entry.index, anchorIndex: historyView.originalView.anchorIndex) - - strongSelf._chatHistoryLocation.set(location) - strongSelf.disableScroll() - } else if bottom.0 >= historyView.filteredEntries.count - 5 && historyView.originalView.earlierId != nil { - let firstEntry = historyView.filteredEntries[min(max(historyView.filteredEntries.count - 1 - bottom.0, 0), historyView.filteredEntries.count - 1)] - strongSelf._chatHistoryLocation.set(ChatHistoryLocation.Navigation(index: firstEntry.entry.index, anchorIndex: historyView.originalView.anchorIndex)) - strongSelf.disableScroll() - } - } - } - } - - func disableScroll() -> Void { - genericView.grid.visibleItemsUpdated = nil - } - - override func viewDidLoad() { - super.viewDidLoad() - - genericView.grid.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: CGSize(width: frame.width, height: frame.height), insets: NSEdgeInsets(), preloadSize: self.bounds.width, type: .fixed(itemSize: CGSize(width: 120, height: 120), lineSpacing: 4)), transition: .immediate), itemTransition: .immediate, stationaryItems: .all, updateFirstIndexInSectionOffset: nil), completion: { _ in }) - - self._chatHistoryLocation.set(ChatHistoryLocation.Initial(count: requestCount)) - - - } - - - - public init(account: Account, peerId: PeerId, messageId: MessageId?, tagMask: MessageTags?, chatInteraction: ChatInteraction) { - self.account = account - self.peerId = peerId - self.messageId = messageId - self.tagMask = tagMask - - super.init() - - let historyViewUpdate = self.chatHistoryLocation - |> distinctUntilChanged - |> mapToSignal { (location) in - return chatHistoryViewForLocation(location, account: account, peerId: peerId, fixedCombinedReadState: nil, tagMask: tagMask) - } - - let previousView = self.previousView - - let historyViewTransition = combineLatest(historyViewUpdate, appearanceSignal) |> mapToQueue { [weak self] update, appearance -> Signal in - switch update { - case .Loading: - Queue.mainQueue().async { [weak self] in - if let strongSelf = self { - let historyState: ChatHistoryNodeHistoryState = .loading - if strongSelf.currentHistoryState != historyState { - strongSelf.currentHistoryState = historyState - strongSelf.historyState.set(historyState) - } - } - } - return .complete() - case let .HistoryView(view, type, scrollPosition, _): - let reason: ChatHistoryViewTransitionReason - var prepareOnMainQueue = false - switch type { - case let .Initial(fadeIn): - reason = ChatHistoryViewTransitionReason.Initial(fadeIn: fadeIn) - prepareOnMainQueue = !fadeIn - case let .Generic(genericType): - switch genericType { - case .InitialUnread: - reason = ChatHistoryViewTransitionReason.Initial(fadeIn: false) - case .Generic: - reason = ChatHistoryViewTransitionReason.InteractiveChanges - case .UpdateVisible: - reason = ChatHistoryViewTransitionReason.Reload - case let .FillHole(insertions, deletions): - reason = ChatHistoryViewTransitionReason.HoleChanges(filledHoleDirections: insertions, removeHoleDirections: deletions) - } - } - - let processedView = ChatHistoryView(originalView: view, filteredEntries: messageEntries(view.entries).map({AppearanceWrapperEntry(entry: $0, appearance: appearance)})) - let previous = previousView.swap(processedView) - - - - - return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: chatInteraction, scrollPosition: nil, initialData: nil, keyboardButtonsMessage: nil, cachedData: nil) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: chatInteraction, transition: $0, from: previous) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : prepareQueue) - } - } - - let appliedTransition = historyViewTransition |> deliverOnMainQueue |> mapToQueue { [weak self] transition -> Signal in - if let strongSelf = self { - return strongSelf.enqueueHistoryViewTransition(transition) - } - return .complete() - } - - self.historyDisposable.set(appliedTransition.start()) - - - - - } - - - - - private func dequeueHistoryViewTransition() { - readyOnce() - if let (transition, completion) = self.enqueuedHistoryViewTransition { - self.enqueuedHistoryViewTransition = nil - - let completion: (GridNodeDisplayedItemRange) -> Void = { [weak self] visibleRange in - if let strongSelf = self { - strongSelf.historyView = transition.historyView - - if let range = visibleRange.loadedRange { - strongSelf.account.postbox.updateMessageHistoryViewVisibleRange(transition.historyView.originalView.id, earliestVisibleIndex: transition.historyView.filteredEntries[transition.historyView.filteredEntries.count - 1 - range.upperBound].entry.index, latestVisibleIndex: transition.historyView.filteredEntries[transition.historyView.filteredEntries.count - 1 - range.lowerBound].entry.index) - } - - let historyState: ChatHistoryNodeHistoryState = .loaded(isEmpty: transition.historyView.originalView.entries.isEmpty) - if strongSelf.currentHistoryState != historyState { - strongSelf.currentHistoryState = historyState - strongSelf.historyState.set(historyState) - } - - completion() - } - } - - let updateLayout = GridNodeUpdateLayout(layout: GridNodeLayout(size: CGSize(width: frame.width, height: frame.height), insets: NSEdgeInsets(), preloadSize: self.frame.width, type: .fixed(itemSize: CGSize(width: 120, height: 120), lineSpacing: 4)), transition: .immediate) - - self.genericView.grid.transaction(GridNodeTransaction(deleteItems: transition.deleteItems, insertItems: transition.insertItems, updateItems: transition.updateItems, scrollToItem: transition.scrollToItem, updateLayout: updateLayout, itemTransition: .immediate, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: transition.topOffsetWithinMonth), completion: completion) - - genericView.update(hasEntities: !genericView.grid.isEmpty) - } - } - - private func enqueueHistoryViewTransition(_ transition: ChatHistoryGridViewTransition) -> Signal { - return Signal { [weak self] subscriber in - - if let strongSelf = self { - if let _ = strongSelf.enqueuedHistoryViewTransition { - preconditionFailure() - } - - strongSelf.enqueuedHistoryViewTransition = (transition, { - subscriber.putCompletion() - }) - - strongSelf.dequeueHistoryViewTransition() - - strongSelf.enableScroll() - - } else { - subscriber.putCompletion() - } - - return EmptyDisposable - - } |> runOn(Queue.mainQueue()) - } - - deinit { - self.historyDisposable.dispose() - } - -} diff --git a/Telegram-Mac/PeerMediaGroupPeersController.swift b/Telegram-Mac/PeerMediaGroupPeersController.swift new file mode 100644 index 0000000000..cdf61bef42 --- /dev/null +++ b/Telegram-Mac/PeerMediaGroupPeersController.swift @@ -0,0 +1,584 @@ +// +// PeerMediaGroupPeersController.swift +// Telegram +// +// Created by Mikhail Filimonov on 27/03/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import SwiftSignalKit +import Postbox +import TelegramCore + +import TGUIKit + +private final class GroupPeersArguments { + let context: AccountContext + let removePeer: (PeerId)->Void + let promote:(ChannelParticipant)->Void + let restrict:(ChannelParticipant)->Void + let showMore:()->Void + let chatPreview:(PeerId)->Void + init(context: AccountContext, removePeer:@escaping(PeerId)->Void, showMore: @escaping()->Void, promote:@escaping(ChannelParticipant)->Void, restrict:@escaping(ChannelParticipant)->Void, chatPreview:@escaping(PeerId)->Void) { + self.context = context + self.removePeer = removePeer + self.promote = promote + self.restrict = restrict + self.showMore = showMore + self.chatPreview = chatPreview + } + + func peerInfo(_ peerId:PeerId) { + context.sharedContext.bindings.rootNavigation().push(PeerInfoController(context: context, peerId: peerId)) + } +} + +private struct GroupPeersState : Equatable { + var temporaryParticipants: [TemporaryParticipant] + var successfullyAddedParticipantIds: Set + var removingParticipantIds: Set + var hasShowMoreButton: Bool? +} + +private func _id_peer_id(_ id: PeerId) -> InputDataIdentifier { + return InputDataIdentifier("_id_peer_id_\(id)") +} + +extension GroupInfoEntry : Equatable { + static func ==(lhs: GroupInfoEntry, rhs: GroupInfoEntry) -> Bool { + return lhs.isEqual(to: rhs) + } +} + +private func groupPeersEntries(state: GroupPeersState, isEditing: Bool, view: PeerView, inputActivities: [PeerId: PeerInputActivity], channelMembers: [RenderedChannelParticipant], arguments: GroupPeersArguments) -> [InputDataEntry] { + var entries: [InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index:Int32 = 0 + + var usersBlock:[GroupInfoEntry] = [] + + + func applyBlock(_ block:[GroupInfoEntry]) { + var block = block + for (i, item) in block.enumerated() { + var viewType = bestGeneralViewType(block, for: i) + if i == 0 { + if block.count > 1 { + viewType = .innerItem + } else { + viewType = .lastItem + } + } + viewType = viewType.withUpdatedInsets(NSEdgeInsetsMake(16, 18, 16, 18)) + block[i] = item.withUpdatedViewType(viewType) + + } + for item in block { + switch item { + case let .member(_, _, _, peer, presence, inputActivity, memberStatus, editing, menuItems, enabled, viewType): + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_peer_id(peer!.id), equatable: InputDataEquatable(item), comparable: nil, item: { initialSize, stableId in + let label: String + switch memberStatus { + case let .admin(rank): + label = rank + case .member: + label = "" + } + + var string:String = L10n.peerStatusRecently + var color:NSColor = theme.colors.grayText + + if let peer = peer as? TelegramUser, let botInfo = peer.botInfo { + string = botInfo.flags.contains(.hasAccessToChatHistory) ? L10n.peerInfoBotStatusHasAccess : L10n.peerInfoBotStatusHasNoAccess + } else if let presence = presence as? TelegramUserPresence { + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + (string, _, color) = stringAndActivityForUserPresence(presence, timeDifference: arguments.context.timeDifference, relativeTo: Int32(timestamp)) + } + + let interactionType:ShortPeerItemInteractionType + if let editing = editing { + + interactionType = .deletable(onRemove: { memberId in + arguments.removePeer(memberId) + }, deletable: editing.editable) + } else { + interactionType = .plain + } + + return ShortPeerRowItem(initialSize, peer: peer!, account: arguments.context.account, stableId: stableId, enabled: enabled, height: 36 + 16, photoSize: NSMakeSize(36, 36), titleStyle: ControlStyle(font: .medium(12.5), foregroundColor: theme.colors.text), statusStyle: ControlStyle(font: NSFont.normal(12.5), foregroundColor:color), status: string, inset: NSEdgeInsets(left: 0, right: 0), interactionType: interactionType, generalType: .context(label), viewType: viewType, action:{ + arguments.peerInfo(peer!.id) + }, contextMenuItems: { + return .single(menuItems) + }, inputActivity: inputActivity) + })) + index += 1 + case let .showMore(_, _, viewType): + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("_id_show_more"), equatable: nil, comparable: nil, item: { initialSize, stableId in + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.peerInfoShowMore, nameStyle: blueActionButton, type: .none, viewType: viewType, action: { + arguments.showMore() + }, thumb: GeneralThumbAdditional(thumb: theme.icons.chatSearchUp, textInset: 52, thumbInset: 4), inset: NSEdgeInsetsZero) + })) + index += 1 + default: + break + } + + } + // entries.append(contentsOf: block) + } + + + if let group = peerViewMainPeer(view) { + let access = group.groupAccess + + if let cachedGroupData = view.cachedData as? CachedGroupData, let participants = cachedGroupData.participants, let group = group as? TelegramGroup { + + var updatedParticipants = participants.participants + let existingParticipantIds = Set(updatedParticipants.map { $0.peerId }) + + var peerPresences: [PeerId: PeerPresence] = view.peerPresences + var peers: [PeerId: Peer] = view.peers + var disabledPeerIds = state.removingParticipantIds + + if !state.temporaryParticipants.isEmpty { + for participant in state.temporaryParticipants { + if !existingParticipantIds.contains(participant.peer.id) { + updatedParticipants.append(.member(id: participant.peer.id, invitedBy: arguments.context.account.peerId, invitedAt: participant.timestamp)) + if let presence = participant.presence, peerPresences[participant.peer.id] == nil { + peerPresences[participant.peer.id] = presence + } + if peers[participant.peer.id] == nil { + peers[participant.peer.id] = participant.peer + } + disabledPeerIds.insert(participant.peer.id) + } + } + } + + let sortedParticipants = participants.participants.filter({peers[$0.peerId]?.displayTitle != nil}).sorted(by: { lhs, rhs in + let lhsPresence = view.peerPresences[lhs.peerId] as? TelegramUserPresence + let rhsPresence = view.peerPresences[rhs.peerId] as? TelegramUserPresence + + let lhsActivity = inputActivities[lhs.peerId] + let rhsActivity = inputActivities[rhs.peerId] + + if lhsActivity != nil && rhsActivity == nil { + return true + } else if rhsActivity != nil && lhsActivity == nil { + return false + } + + if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { + return lhsPresence.status > rhsPresence.status + } else if let _ = lhsPresence { + return true + } else if let _ = rhsPresence { + return false + } + + return lhs < rhs + }) + + for i in 0 ..< sortedParticipants.count { + if let peer = view.peers[sortedParticipants[i].peerId] { + let memberStatus: GroupInfoMemberStatus + if access.highlightAdmins { + switch sortedParticipants[i] { + case .admin: + memberStatus = .admin(rank: L10n.chatAdminBadge) + case .creator: + memberStatus = .admin(rank: L10n.chatOwnerBadge) + case .member: + memberStatus = .member + } + } else { + memberStatus = .member + } + + var canRestrict: Bool + if sortedParticipants[i].peerId == arguments.context.peerId { + canRestrict = false + } else { + switch group.role { + case .creator: + canRestrict = true + case .member: + switch sortedParticipants[i] { + case .creator, .admin: + canRestrict = false + case let .member(member): + if member.invitedBy == arguments.context.peerId { + canRestrict = true + } else { + canRestrict = false + } + } + case .admin: + switch sortedParticipants[i] { + case .creator, .admin: + canRestrict = false + case .member: + canRestrict = true + } + } + } + + let editing:ShortPeerDeleting? + + if isEditing { + let deletable:Bool = group.canRemoveParticipant(sortedParticipants[i]) || (sortedParticipants[i].invitedBy == arguments.context.peerId && sortedParticipants[i].peerId != arguments.context.peerId) + editing = ShortPeerDeleting(editable: deletable) + } else { + editing = nil + } + + var menuItems: [ContextMenuItem] = [] + if sortedParticipants[i].peerId != arguments.context.peerId { + menuItems.append(ContextMenuItem(L10n.chatListContextPreview, handler: { + arguments.chatPreview(sortedParticipants[i].peerId) + })) + if canRestrict { + menuItems.append(ContextSeparatorItem()) + } + } + + if canRestrict { + menuItems.append(ContextMenuItem(L10n.peerInfoGroupMenuDelete, handler: { + arguments.removePeer(sortedParticipants[i].peerId) + })) + } + + usersBlock.append(.member(section: Int(sectionId), index: i, peerId: peer.id, peer: peer, presence: view.peerPresences[peer.id], activity: inputActivities[peer.id], memberStatus: memberStatus, editing: editing, menuItems: menuItems, enabled: !disabledPeerIds.contains(peer.id), viewType: .singleItem)) + } + } + } + + if let cachedGroupData = view.cachedData as? CachedChannelData, let channel = group as? TelegramChannel { + let participants = channelMembers + var updatedParticipants = participants + let existingParticipantIds = Set(updatedParticipants.map { $0.peer.id }) + var peerPresences: [PeerId: PeerPresence] = view.peerPresences + var peers: [PeerId: Peer] = view.peers + var disabledPeerIds = state.removingParticipantIds + + + if !state.temporaryParticipants.isEmpty { + for participant in state.temporaryParticipants { + if !existingParticipantIds.contains(participant.peer.id) { + updatedParticipants.append(RenderedChannelParticipant(participant: .member(id: participant.peer.id, invitedAt: participant.timestamp, adminInfo: nil, banInfo: nil, rank: nil), peer: participant.peer)) + if let presence = participant.presence, peerPresences[participant.peer.id] == nil { + peerPresences[participant.peer.id] = presence + } + if participant.peer.id == arguments.context.account.peerId { + peerPresences[participant.peer.id] = TelegramUserPresence(status: .present(until: Int32.max), lastActivity: Int32.max) + } + if peers[participant.peer.id] == nil { + peers[participant.peer.id] = participant.peer + } + disabledPeerIds.insert(participant.peer.id) + } + } + } + + + var sortedParticipants = participants.filter({!$0.peer.rawDisplayTitle.isEmpty}).sorted(by: { lhs, rhs in + let lhsPresence = lhs.presences[lhs.peer.id] as? TelegramUserPresence + let rhsPresence = rhs.presences[rhs.peer.id] as? TelegramUserPresence + + let lhsActivity = inputActivities[lhs.peer.id] + let rhsActivity = inputActivities[rhs.peer.id] + + if lhsActivity != nil && rhsActivity == nil { + return true + } else if rhsActivity != nil && lhsActivity == nil { + return false + } + + if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { + return lhsPresence.status > rhsPresence.status + } else if let _ = lhsPresence { + return true + } else if let _ = rhsPresence { + return false + } + + return lhs < rhs + }) + + if let hasShowMoreButton = state.hasShowMoreButton, hasShowMoreButton, let memberCount = cachedGroupData.participantsSummary.memberCount, memberCount > 100 { + sortedParticipants = Array(sortedParticipants.prefix(min(50, sortedParticipants.count))) + } + + for i in 0 ..< sortedParticipants.count { + let memberStatus: GroupInfoMemberStatus + if access.highlightAdmins { + switch sortedParticipants[i].participant { + case let .creator(_, _, rank): + memberStatus = .admin(rank: rank ?? L10n.chatOwnerBadge) + case let .member(_, _, adminRights, _, rank): + memberStatus = adminRights != nil ? .admin(rank: rank ?? L10n.chatAdminBadge) : .member + } + } else { + memberStatus = .member + } + + var canPromote: Bool + var canRestrict: Bool + if sortedParticipants[i].peer.id == arguments.context.peerId { + canPromote = false + canRestrict = false + } else { + switch sortedParticipants[i].participant { + case .creator: + canPromote = false + canRestrict = false + case let .member(_, _, adminRights, bannedRights, _): + if channel.hasPermission(.addAdmins) { + canPromote = true + } else { + canPromote = false + } + if channel.hasPermission(.banMembers) { + canRestrict = true + } else { + canRestrict = false + } + if canPromote { + if let bannedRights = bannedRights { + if bannedRights.restrictedBy != arguments.context.peerId && !channel.flags.contains(.isCreator) { + canPromote = false + } + } + } + if canRestrict { + if let adminRights = adminRights { + if adminRights.promotedBy != arguments.context.peerId && !channel.flags.contains(.isCreator) { + canRestrict = false + } + } + } + } + } + + var menuItems:[ContextMenuItem] = [] + + + if sortedParticipants[i].participant.peerId != arguments.context.peerId { + menuItems.append(ContextMenuItem(L10n.chatListContextPreview, handler: { + arguments.chatPreview(sortedParticipants[i].participant.peerId) + })) + if canPromote || canRestrict { + menuItems.append(ContextSeparatorItem()) + } + } + + if canPromote { + menuItems.append(ContextMenuItem(L10n.peerInfoGroupMenuPromote, handler: { + arguments.promote(sortedParticipants[i].participant) + })) + } + if canRestrict { + menuItems.append(ContextMenuItem(L10n.peerInfoGroupMenuRestrict, handler: { + arguments.restrict(sortedParticipants[i].participant) + })) + menuItems.append(ContextMenuItem(L10n.peerInfoGroupMenuDelete, handler: { + arguments.removePeer(sortedParticipants[i].peer.id) + })) + } + + let editing:ShortPeerDeleting? + + if isEditing, let group = group as? TelegramChannel { + let deletable:Bool = group.canRemoveParticipant(sortedParticipants[i].participant, accountId: arguments.context.account.peerId) + editing = ShortPeerDeleting(editable: deletable) + } else { + editing = nil + } + + + usersBlock.append(GroupInfoEntry.member(section: Int(sectionId), index: i, peerId: sortedParticipants[i].peer.id, peer: sortedParticipants[i].peer, presence: sortedParticipants[i].presences[sortedParticipants[i].peer.id], activity: inputActivities[sortedParticipants[i].peer.id], memberStatus: memberStatus, editing: editing, menuItems: menuItems, enabled: !disabledPeerIds.contains(sortedParticipants[i].peer.id), viewType: .singleItem)) + } + + if let hasShowMoreButton = state.hasShowMoreButton, hasShowMoreButton, let memberCount = cachedGroupData.participantsSummary.memberCount, memberCount > 100 { + usersBlock.append(.showMore(section: GroupInfoSection.members.rawValue, index: sortedParticipants.count + 1, viewType: .singleItem)) + } + } + + } + + + applyBlock(usersBlock) + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 +// + return entries +} + +func PeerMediaGroupPeersController(context: AccountContext, peerId: PeerId, editing: Signal) -> InputDataController { + + + let initialState = GroupPeersState(temporaryParticipants: [], successfullyAddedParticipantIds: Set(), removingParticipantIds: Set(), hasShowMoreButton: true) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((GroupPeersState) -> GroupPeersState) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let actionsDisposable = DisposableSet() + + + var loadMoreControl: PeerChannelMemberCategoryControl? + + let channelMembersPromise = Promise<[RenderedChannelParticipant]>() + + let inputActivity = context.account.peerInputActivities(peerId: .init(peerId: peerId, category: .global)) + |> map { activities -> [PeerId : PeerInputActivity] in + return activities.reduce([:], { (current, activity) -> [PeerId : PeerInputActivity] in + var current = current + current[activity.0] = activity.1 + return current + }) + } + + if peerId.namespace == Namespaces.Peer.CloudChannel { + let (disposable, control) = context.peerChannelMemberCategoriesContextsManager.recent(peerId: peerId, updated: { state in + channelMembersPromise.set(.single(state.list)) + }) + loadMoreControl = control + actionsDisposable.add(disposable) + } else { + channelMembersPromise.set(.single([])) + } + + let upgradeToSupergroup: (PeerId, @escaping () -> Void) -> Void = { upgradedPeerId, f in + let navigationController = context.sharedContext.bindings.rootNavigation() + + var chatController: ChatController? = ChatController(context: context, chatLocation: .peer(upgradedPeerId)) + + chatController!.navigationController = navigationController + chatController!.loadViewIfNeeded(navigationController.bounds) + + var signal = chatController!.ready.get() |> filter {$0} |> take(1) |> ignoreValues + + var controller: PeerInfoController? = PeerInfoController(context: context, peerId: upgradedPeerId) + + controller!.navigationController = navigationController + controller!.loadViewIfNeeded(navigationController.bounds) + + let mainSignal = combineLatest(controller!.ready.get(), controller!.ready.get()) |> map { $0 && $1 } |> filter {$0} |> take(1) |> ignoreValues + + signal = combineLatest(queue: .mainQueue(), signal, mainSignal) |> ignoreValues + + _ = signal.start(completed: { [weak navigationController] in + navigationController?.removeAll() + navigationController?.push(chatController!, false, style: .none) + navigationController?.push(controller!, false, style: .none) + + chatController = nil + controller = nil + }) + } + + + + let arguments = GroupPeersArguments(context: context, removePeer: { memberId in + + let signal = context.account.postbox.loadedPeerWithId(memberId) + |> deliverOnMainQueue + |> mapToSignal { peer -> Signal in + let result = ValuePromise() + result.set(true) + return result.get() + } + |> mapToSignal { value -> Signal in + if value { + updateState { state in + var state = state + for i in 0 ..< state.temporaryParticipants.count { + if state.temporaryParticipants[i].peer.id == memberId { + state.temporaryParticipants.remove(at: i) + break + } + } + state.successfullyAddedParticipantIds.remove(memberId) + state.removingParticipantIds.insert(memberId) + return state + } + + if peerId.namespace == Namespaces.Peer.CloudChannel { + return context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(peerId: peerId, memberId: memberId, bannedRights: TelegramChatBannedRights(flags: [.banReadMessages], untilDate: Int32.max)) + |> afterDisposed { + updateState { state in + var state = state + state.removingParticipantIds.remove(memberId) + return state + } + } + } + + return context.engine.peers.removePeerMember(peerId: peerId, memberId: memberId) + |> deliverOnMainQueue + |> afterDisposed { + updateState { state in + var state = state + state.removingParticipantIds.remove(memberId) + return state + } + } + } else { + return .complete() + } + } + actionsDisposable.add(signal.start()) + + }, showMore: { + updateState { state in + var state = state + state.hasShowMoreButton = nil + return state + } + }, promote: { participant in + showModal(with: ChannelAdminController(context, peerId: peerId, adminId: participant.peerId, initialParticipant: participant, updated: { _ in }, upgradedToSupergroup: upgradeToSupergroup), for: context.window) + }, restrict: { participant in + showModal(with: RestrictedModalViewController(context, peerId: peerId, memberId: participant.peerId, initialParticipant: participant, updated: { updatedRights in + _ = context.peerChannelMemberCategoriesContextsManager.updateMemberBannedRights(peerId: peerId, memberId: participant.peerId, bannedRights: updatedRights).start() + }), for: context.window) + }, chatPreview: { peerId in + showModal(with: ChatModalPreviewController(location: .peer(peerId), context: context), for: context.window) + }) + + let dataSignal = combineLatest(queue: prepareQueue, statePromise.get(), context.account.postbox.peerView(id: peerId), channelMembersPromise.get(), inputActivity, editing) |> map { + return InputDataSignalValue(entries: groupPeersEntries(state: $0, isEditing: $4, view: $1, inputActivities: $3, channelMembers: $2, arguments: arguments)) + } + + let controller = InputDataController(dataSignal: dataSignal, title: "") + controller.bar = .init(height: 0) + + controller.onDeinit = { + actionsDisposable.dispose() + } + + controller.getBackgroundColor = { + theme.colors.listBackground + } + + controller.didLoaded = { controller, _ in + controller.tableView.setScrollHandler { position in + if let loadMoreControl = loadMoreControl { + switch position.direction { + case .bottom: + context.peerChannelMemberCategoriesContextsManager.loadMore(peerId: peerId, control: loadMoreControl) + default: + break + } + } + } + + } + + return controller +} diff --git a/Telegram-Mac/PeerMediaListController.swift b/Telegram-Mac/PeerMediaListController.swift index 1ebc607e11..f7463b3ce9 100644 --- a/Telegram-Mac/PeerMediaListController.swift +++ b/Telegram-Mac/PeerMediaListController.swift @@ -8,161 +8,228 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit enum PeerMediaSharedEntryStableId : Hashable { case messageId(MessageId) case search case emptySearch - - var hashValue: Int { - switch self { - case let .messageId(messageId): - return messageId.hashValue - case .search: - return 0 - case .emptySearch: - return 1 - } - } - - static func ==(lhs:PeerMediaSharedEntryStableId, rhs: PeerMediaSharedEntryStableId) -> Bool { - switch lhs { - case let .messageId(lhsMessageId): - if case let .messageId(rhsMessageId) = rhs, lhsMessageId == rhsMessageId { - return true - } else { - return false - } - case .search: - if case .search = rhs { - return true - } else { - return false - } - case .emptySearch: - if case .emptySearch = rhs { - return true - } else { - return false - } + case date(MessageIndex) + case sectionId(MessageIndex) +} + +private func bestGeneralViewType(_ array:[PeerMediaSharedEntry], for item: PeerMediaSharedEntry) -> GeneralViewType { + for _ in array { + if item == array.first && item == array.last { + return .modern(position: .single, insets: NSEdgeInsetsMake(7, 7, 7, 12)) + } else if item == array.first { + return .modern(position: .first, insets: NSEdgeInsetsMake(7, 7, 7, 12)) + } else if item == array.last { + return .modern(position: .last, insets: NSEdgeInsetsMake(7, 7, 7, 12)) + } else { + return .modern(position: .inner, insets: NSEdgeInsetsMake(7, 7, 7, 12)) } } + return .modern(position: .single, insets: NSEdgeInsetsMake(6, 6, 6, 12)) } enum PeerMediaSharedEntry : Comparable, Identifiable { - case messageEntry(Message) - case searchEntry(Bool) - case emptySearchEntry - + case messageEntry(Message, [Message], AutomaticMediaDownloadSettings, GeneralViewType) + case emptySearchEntry(Bool) + case date(MessageIndex) + case sectionId(MessageIndex) var stableId: AnyHashable { switch self { - case let .messageEntry(message): + case let .messageEntry(message, _, _, _): return PeerMediaSharedEntryStableId.messageId(message.id) - case .searchEntry: - return PeerMediaSharedEntryStableId.search + case let .date(index): + return PeerMediaSharedEntryStableId.date(index) + case let .sectionId(index): + return PeerMediaSharedEntryStableId.sectionId(index) case .emptySearchEntry: return PeerMediaSharedEntryStableId.emptySearch } } -} - -func <(lhs:PeerMediaSharedEntry, rhs: PeerMediaSharedEntry) -> Bool { - switch lhs { - case .searchEntry: - if case .searchEntry = rhs { - return true - } else { - return false - } - case .emptySearchEntry: - switch rhs { - case .searchEntry: - return true - default: - return false + + var index: MessageIndex { + switch self { + case let .date(index): + return index + case let .sectionId(index): + return index + case let .messageEntry(message, _, _, _): + return MessageIndex(message).peerLocalPredecessor() + case .emptySearchEntry: + return MessageIndex.absoluteLowerBound() } - case let .messageEntry(lhsMessage): - switch rhs { - case let .messageEntry(rhsMessage): - return lhsMessage.id < rhsMessage.id + } + + var message:Message? { + switch self { + case let .messageEntry(message, _, _, _): + return message default: - return true + return nil } } } -func ==(lhs: PeerMediaSharedEntry, rhs: PeerMediaSharedEntry) -> Bool { - switch lhs { - case let .messageEntry(lhsMessage): - if case let .messageEntry(rhsMessage) = rhs { - if lhsMessage.id != rhsMessage.id { - return false - } +func <(lhs:PeerMediaSharedEntry, rhs: PeerMediaSharedEntry) -> Bool { + return lhs.index < rhs.index +} + + +func convertEntries(from update: PeerMediaUpdate, tags: MessageTags, timeDifference: TimeInterval, isExternalSearch: Bool) -> [PeerMediaSharedEntry] { + var converted:[PeerMediaSharedEntry] = [] + + + struct Item { + let date: PeerMediaSharedEntry + let section: PeerMediaSharedEntry + let items:[PeerMediaSharedEntry] + init(_ date: PeerMediaSharedEntry, _ section: PeerMediaSharedEntry, _ items: [PeerMediaSharedEntry]) { + self.date = date + self.section = section + self.items = items.sorted(by: >) + } + } + + + var tempItems:[(PeerMediaSharedEntry, PeerMediaSharedEntry?)] = [] + + for i in 0 ..< update.messages.count { + + let message = update.messages[i] + + let next = i < update.messages.count - 1 ? update.messages[i + 1] : nil + + + + if let nextMessage = next { - if lhsMessage.stableVersion != rhsMessage.stableVersion { - return false + let timestamp = Int32(min(TimeInterval(message.timestamp) - timeDifference, TimeInterval(Int32.max))) + let nextTimestamp = Int32(min(TimeInterval(nextMessage.timestamp) - timeDifference, TimeInterval(Int32.max))) + + + let dateId = mediaDateId(for: timestamp) + let nextDateId = mediaDateId(for: nextTimestamp) + if dateId != nextDateId { + let index = MessageIndex(id: message.id, timestamp: Int32(dateId)) + tempItems.append((.date(index), .sectionId(index.peerLocalSuccessor()))) } - return true } else { - return false + let timestamp = Int32(min(TimeInterval(message.timestamp) - timeDifference, TimeInterval(Int32.max))) + let dateId = mediaDateId(for: timestamp) + let index = MessageIndex(id: message.id, timestamp: Int32(dateId)) + tempItems.append((.date(index), .sectionId(index.peerLocalSuccessor()))) } - case let .searchEntry(lhsProgress): - if case let .searchEntry(rhsProgress) = rhs { - return lhsProgress == rhsProgress + tempItems.append((.messageEntry(message, isExternalSearch ? update.messages : [], update.automaticDownload, .singleItem), nil)) + + } + + + var groupItems:[Item] = [] + var current:[PeerMediaSharedEntry] = [] + for item in tempItems.sorted(by: { $0.0 < $1.0 }) { + switch item.0 { + case .date: + if !current.isEmpty { + groupItems.append(Item(item.0, item.1!, current)) + current.removeAll() + } + case .messageEntry: + current.insert(item.0, at: 0) + default: + fatalError() + } + } + + if !current.isEmpty { + if !groupItems.isEmpty { + let item = groupItems.last! + groupItems[groupItems.count - 1] = Item(item.date, item.section, item.items + current) } else { - return false + groupItems.append(.init(current.first!, .sectionId(current.first!.index.peerLocalSuccessor()), current)) } - default : - return lhs.stableId == rhs.stableId } -} - - -func convertEntries(from update: PeerMediaUpdate) -> [PeerMediaSharedEntry] { - var converted:[PeerMediaSharedEntry] = [] + + + for (i, group) in groupItems.reversed().enumerated() { + if i != 0 { + converted.append(group.section) + converted.append(group.date) + } + + + for item in group.items { + switch item { + case let .messageEntry(message, messages, settings, _): + var viewType = bestGeneralViewType(group.items, for: item) + + if i == 0, item == group.items.first { + if group.items.count > 1 { + viewType = .modern(position: .inner, insets: NSEdgeInsetsMake(7, 7, 7, 12)) + } else { + if !isExternalSearch { + viewType = .modern(position: .last, insets: NSEdgeInsetsMake(7, 7, 7, 12)) + } + } + } + + converted.append(.messageEntry(message, messages, settings, viewType)) + default: + fatalError() + } + } + } + - for message in update.messages { - converted.append(.messageEntry(message)) + + if !tempItems.isEmpty { + converted.append(.sectionId(MessageIndex.absoluteLowerBound())) } if update.updateType == .search { - converted.append(.searchEntry(false)) - if update.messages.isEmpty { - //converted.append(.emptySearchEntry) + if converted.isEmpty { + converted.append(.emptySearchEntry(false)) } } else if update.updateType == .loading { - converted.append(.searchEntry(true)) - // converted.append(.emptySearchEntry) - } else if update.laterId == nil { - if !update.messages.isEmpty { - converted.append(.searchEntry(false)) + if converted.isEmpty { + converted.append(.emptySearchEntry(true)) } } - - return converted.sorted(by: <) + converted = converted.sorted(by: <) + + + + return converted } -fileprivate func preparedMediaTransition(from fromView:[PeerMediaSharedEntry]?, to toView:[PeerMediaSharedEntry], account:Account, initialSize:NSSize, interaction:ChatInteraction, animated:Bool, scroll:TableScrollState, tags:MessageTags, searchInteractions:SearchInteractions) -> TableUpdateTransition { - let (removed,inserted,updated) = proccessEntries(fromView, right: toView, { (entry) -> TableRowItem in +fileprivate func preparedMediaTransition(from fromView:[AppearanceWrapperEntry]?, to toView:[AppearanceWrapperEntry], account:Account, initialSize:NSSize, interaction:ChatInteraction, animated:Bool, scroll:TableScrollState, tags:MessageTags) -> TableUpdateTransition { + let (removed,inserted,updated) = proccessEntries(fromView, right: toView, { entry -> TableRowItem in - switch entry { - case .messageEntry: - if tags == .file { - return PeerMediaFileRowItem(initialSize, interaction, account, entry) + switch entry.entry { + case let .messageEntry(message, _, _, viewType): + if tags == .file, message.media.first is TelegramMediaFile { + return PeerMediaFileRowItem(initialSize, interaction, entry.entry, viewType: viewType) } else if tags == .webPage { - return PeerMediaWebpageRowItem(initialSize,interaction,account,entry) - } else if tags == .music { - return PeerMediaMusicRowItem(initialSize, interaction, account, entry) + return PeerMediaWebpageRowItem(initialSize,interaction, entry.entry, viewType: viewType) + } else if tags == .music, message.media.first is TelegramMediaFile { + return PeerMediaMusicRowItem(initialSize, interaction, entry.entry, viewType: viewType) + } else if tags == .voiceOrInstantVideo, message.media.first is TelegramMediaFile { + return PeerMediaVoiceRowItem(initialSize,interaction, entry.entry, viewType: viewType) } else { - return GeneralRowItem(initialSize, height: 20, stableId: entry.stableId) + return GeneralRowItem(initialSize, height: 1, stableId: entry.stableId) } - case let .searchEntry(isLoading): - return SearchRowItem(initialSize, stableId: entry.stableId, searchInteractions: searchInteractions, isLoading: isLoading, inset: NSEdgeInsets(left: 10, right: 10, top: 10, bottom: 10)) + case .date(let index): + return PeerMediaDateItem(initialSize, index: index, stableId: entry.stableId) + case .sectionId: + return GeneralRowItem(initialSize, height: 20, stableId: entry.stableId, viewType: .separator) case .emptySearchEntry: - return SearchEmptyRowItem(initialSize, stableId: entry.stableId) + return SearchEmptyRowItem(initialSize, stableId: entry.stableId, isLoading: false, viewType: .separator) } }) @@ -188,30 +255,74 @@ struct PeerMediaUpdate { let updateType: PeerMediaUpdateState let laterId: MessageIndex? let earlierId: MessageIndex? - init (messages: [Message] = [], updateType:PeerMediaUpdateState = .loading, laterId:MessageIndex? = nil, earlierId:MessageIndex? = nil) { - self.messages = messages + let automaticDownload: AutomaticMediaDownloadSettings + let searchState: SearchState + let contentSettings: ContentSettings + init (messages: [Message] = [], updateType:PeerMediaUpdateState = .loading, laterId:MessageIndex? = nil, earlierId:MessageIndex? = nil, automaticDownload: AutomaticMediaDownloadSettings = .defaultSettings, searchState: SearchState = SearchState(state: .None, request: nil), contentSettings: ContentSettings = ContentSettings.default) { + self.messages = messages.filter { $0.restrictedText(contentSettings) == nil } self.updateType = updateType self.laterId = laterId self.earlierId = earlierId + self.automaticDownload = automaticDownload + self.searchState = searchState + self.contentSettings = contentSettings + } + + func withUpdatedUpdatedType(_ updateType:PeerMediaUpdateState) -> PeerMediaUpdate { + return PeerMediaUpdate(messages: self.messages, updateType: updateType, laterId: self.laterId, earlierId: self.earlierId, automaticDownload: self.automaticDownload, searchState: self.searchState, contentSettings: contentSettings) } } +struct MediaSearchState : Equatable { + let state: SearchState + let animated: Bool + let isLoading: Bool +} -class PeerMediaListController: GenericViewController { +class PeerMediaListController: TableViewController, PeerMediaSearchable { - private var account:Account - private var peerId:PeerId + private var chatLocation:ChatLocation private var chatInteraction:ChatInteraction private let disposable: MetaDisposable = MetaDisposable() - private let entires = Atomic<[PeerMediaSharedEntry]?>(value: nil) + private let entires = Atomic<[AppearanceWrapperEntry]?>(value: nil) private let updateView = Atomic(value: nil) - private let searchState:ValuePromise = ValuePromise(ignoreRepeated: true) - public init(account: Account, peerId: PeerId, chatInteraction: ChatInteraction) { - self.account = account - self.peerId = peerId + private let mediaSearchState:ValuePromise = ValuePromise(ignoreRepeated: true) + private let searchState:Promise = Promise() + private var isExternalSearch: Bool = false + private let externalSearch:Promise = Promise(nil) + + func setSearchValue(_ value: Signal) { + searchState.set(value) + } + + func setExternalSearch(_ value: Signal, _ loadMore: @escaping () -> Void) { + externalSearch.set(value) + self.isExternalSearch = true + } + + var mediaSearchValue:Signal { + return mediaSearchState.get() + } + private var isSearch: Bool = false { + didSet { + if isSearch { + searchState.set(.single(.init(state: .Focus, request: nil))) + } else { + searchState.set(.single(.init(state: .None, request: nil))) + } + } + } + func toggleSearch() { + let old = self.isSearch + self.isSearch = !old + } + + + public init(context: AccountContext, chatLocation: ChatLocation, chatInteraction: ChatInteraction) { + self.chatLocation = chatLocation self.chatInteraction = chatInteraction - super.init() + super.init(context) } @@ -223,128 +334,191 @@ class PeerMediaListController: GenericViewController { super.viewWillAppear(animated) } + override func findGroupStableId(for stableId: AnyHashable) -> AnyHashable? { + if let stableId = stableId.base as? ChatHistoryEntryId { + switch stableId { + case let .message(message): + return PeerMediaSharedEntryStableId.messageId(message.id) + default: + break + } + } + return nil + } + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) } + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + + + } + override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) genericView.stopMerge() } + private var isFirst: Bool = true + + override func viewDidLoad() { + super.viewDidLoad() + + genericView.getBackgroundColor = { + theme.colors.listBackground + } + } public func load(with tagMask:MessageTags) -> Void { + + genericView.clipView.scroll(to: NSMakePoint(0, 0), animated: false) + + let isFirst = self.isFirst + self.isFirst = false + + let isExternalSearch = self.isExternalSearch + let location = ValuePromise(ignoreRepeated: true) - searchState.set(SearchState(state: .None, request: nil)) + searchState.set(.single(SearchState(state: .None, request: nil))) genericView.emptyItem = PeerMediaEmptyRowItem(atomicSize.modify {$0}, tags: tagMask) - let historyViewUpdate = combineLatest(location.get(), searchState.get()) |> deliverOnMainQueue - |> mapToSignal { [weak self] location, searchState -> Signal in + genericView.set(stickClass: PeerMediaDateItem.self, handler: { item in + + }) + + let historyPromise: Promise = Promise() + let context = self.context + + + let historyViewUpdate = combineLatest(location.get(), searchState.get(), externalSearch.get()) |> deliverOnMainQueue + |> mapToSignal { [weak self] location, searchState, externalSearch -> Signal in if let strongSelf = self { - if searchState.request.isEmpty { - return chatHistoryViewForLocation(location, account: strongSelf.account, peerId: strongSelf.peerId, fixedCombinedReadState: nil, tagMask: tagMask, additionalData: []) |> mapToQueue { view -> Signal in + if let externalSearch = externalSearch { + return .single(PeerMediaUpdate(messages: externalSearch.messages, updateType: .history, laterId: nil, earlierId: nil, searchState: searchState, contentSettings: context.contentSettings)) + } else if searchState.request.isEmpty { + return combineLatest(queue: prepareQueue, chatHistoryViewForLocation(location, context: strongSelf.context, chatLocation: strongSelf.chatLocation, fixedCombinedReadStates: nil, tagMask: tagMask, additionalData: []), automaticDownloadSettings(postbox: strongSelf.context.account.postbox)) |> mapToQueue { view, settings -> Signal in switch view { case .Loading: return .single(PeerMediaUpdate()) case let .HistoryView(view: view, type: _, scrollPosition: _, initialData: _): var messages:[Message] = [] for entry in view.entries { - switch entry { - case let .MessageEntry(message, _, _, _): - messages.append(message) - default: - break - } + messages.append(entry.message) } - return .single(PeerMediaUpdate(messages: messages, updateType: .history, laterId: view.laterId, earlierId: view.earlierId)) + + let laterId = view.laterId + let earlierId = view.earlierId + + return .single(PeerMediaUpdate(messages: messages, updateType: .history, laterId: laterId, earlierId: earlierId, automaticDownload: settings, searchState: searchState, contentSettings: context.contentSettings)) } } } else { - return .single(PeerMediaUpdate()) |> then(searchMessages(account: strongSelf.account, peerId: strongSelf.peerId, query: searchState.request, tagMask: tagMask) |> deliverOnMainQueue |> map { messages -> PeerMediaUpdate in - return PeerMediaUpdate(messages: messages, updateType: .search, laterId: nil, earlierId: nil) - }) + let searchMessagesLocation: SearchMessagesLocation + searchMessagesLocation = .peer(peerId: strongSelf.chatLocation.peerId, fromId: nil, tags: tagMask, topMsgId: nil, minDate: nil, maxDate: nil) + + let signal = context.engine.messages.searchMessages(location: searchMessagesLocation, query: searchState.request, state: nil) |> deliverOnMainQueue |> map {$0.0.messages} |> map { messages -> PeerMediaUpdate in + return PeerMediaUpdate(messages: messages, updateType: .search, laterId: nil, earlierId: nil, searchState: searchState, contentSettings: context.contentSettings) + } + + let update = strongSelf.updateView.modify {$0?.withUpdatedUpdatedType(.loading)} ?? PeerMediaUpdate() + + if isFirst { + return .single(update) |> then(signal) + } else { + return .single(update) |> then(signal) + } + } } return .complete() } - let animated:Atomic = Atomic(value:true) + let animated:Atomic = Atomic(value:false) - let searchInteractions = SearchInteractions({ [weak self] state in - if let strongSelf = self { - strongSelf.searchState.set(state) - } - }, { [weak self] (state) in - if let strongSelf = self { - strongSelf.searchState.set(state) - } - }) - let account = self.account let chatInteraction = self.chatInteraction let initialSize = self.atomicSize + + let _updateView = self.updateView let _entries = self.entires - let historyViewTransition = historyViewUpdate |> deliverOnPrepareQueue |> map { update -> TableUpdateTransition in + + + let historyViewTransition = combineLatest(queue: prepareQueue,historyPromise.get(), appearanceSignal) |> map { update, appearance -> (transition: TableUpdateTransition, previousUpdate: PeerMediaUpdate?, currentUpdate: PeerMediaUpdate) in let animated = animated.swap(true) - let scroll:TableScrollState = animated ? .none(nil) : .saveVisible(.upper) + var scroll:TableScrollState = animated ? .none(nil) : .saveVisible(.upper) + + - let entries = convertEntries(from: update) + let entries = convertEntries(from: update, tags: tagMask, timeDifference: context.timeDifference, isExternalSearch: isExternalSearch).map({AppearanceWrapperEntry(entry: $0, appearance: appearance)}) let previous = _entries.swap(entries) - _ = _updateView.swap(update) + let previousUpdate = _updateView.swap(update) - return preparedMediaTransition(from: previous, to: entries, account: account, initialSize: initialSize.modify({$0}), interaction: chatInteraction, animated: animated, scroll:scroll, tags:tagMask, searchInteractions: searchInteractions) + if previousUpdate?.searchState != update.searchState { + scroll = .up(animated) + } + + let transition = preparedMediaTransition(from: previous, to: entries, account: context.account, initialSize: initialSize.modify({$0}), interaction: chatInteraction, animated: previousUpdate?.searchState.state != update.searchState.state, scroll:scroll, tags:tagMask) + + return (transition: transition, previousUpdate: previousUpdate, currentUpdate: update) } |> deliverOnMainQueue - disposable.set(historyViewTransition.start(next: { [weak self] transition in - self?.genericView.merge(with: transition) + disposable.set(historyViewTransition.start(next: { [weak self] values in + guard let `self` = self else {return} + + let state = MediaSearchState(state: values.currentUpdate.searchState, animated: values.currentUpdate.searchState != values.previousUpdate?.searchState, isLoading: values.currentUpdate.updateType == .loading) + self.genericView.merge(with: values.transition) + self.mediaSearchState.set(state) + self.readyOnce() + if let controller = globalAudio, let header = self.navigationController?.header, header.needShown { + let tableView = (self.navigationController?.first {$0 is ChatController} as? ChatController)?.genericView.tableView + let object = InlineAudioPlayerView.ContextObject(controller: controller, context: context, tableView: tableView, supportTableView: self.genericView) + header.view.update(with: object) + } })) + + historyPromise.set(historyViewUpdate) - location.set(.Scroll(index: MessageIndex.upperBound(peerId: peerId), anchorIndex: MessageIndex.upperBound(peerId: peerId), sourceIndex: MessageIndex.upperBound(peerId: peerId), scrollPosition: .none(nil), animated: false)) + let perPageCount:()->Int = { [weak self] in + guard let `self` = self else { + return 0 + } + return Int(self.frame.height / 50) + } + + var requestCount: Int = perPageCount() + 5 + + + location.set(.Initial(count: requestCount)) genericView.setScrollHandler { [weak self] scroll in - - let view = self?.updateView.modify({$0}) - if let view = view, view.updateType == .history { - var messageIndex:MessageIndex? - switch scroll.direction { - case .bottom: - messageIndex = view.earlierId - case .top: - messageIndex = view.laterId - case .none: - break - } - - if let messageIndex = messageIndex { - let _ = animated.swap(false) - location.set(.Navigation(index: messageIndex, anchorIndex: messageIndex)) + switch scroll.direction { + case .bottom: + if self?.isSearch == false { + _ = animated.swap(false) + requestCount += perPageCount() * 3 + location.set(.Initial(count: requestCount)) } + default: + break } } } + override func navigationHeaderDidNoticeAnimation(_ current: CGFloat, _ previous: CGFloat, _ animated: Bool) -> () -> Void { + return { + + } + } } - - - -// let view = strongSelf.messagesView.modify {$0} -// if let view = view { -// let range = strongSelf.genericView.visibleRows() -// let indexRange = (max(view.entries.count - 1 - range.max,0), max(view.entries.count - 1 - range.min,0)) -// -// if indexRange.1 > 0 { -// strongSelf.account.postbox.updateMessageHistoryViewVisibleRange(view.id, earliestVisibleIndex: view.entries[indexRange.0].index, latestVisibleIndex: view.entries[indexRange.1].index) -// } -// -// } - diff --git a/Telegram-Mac/PeerMediaMusicRowContent.swift b/Telegram-Mac/PeerMediaMusicRowContent.swift index edba940523..1a1b54cfa3 100644 --- a/Telegram-Mac/PeerMediaMusicRowContent.swift +++ b/Telegram-Mac/PeerMediaMusicRowContent.swift @@ -8,110 +8,200 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit class PeerMediaMusicRowItem: PeerMediaRowItem { - fileprivate var textLayout:TextViewLayout? - fileprivate var file:TelegramMediaFile! - override init(_ initialSize:NSSize, _ interface:ChatInteraction, _ account:Account, _ object: PeerMediaSharedEntry) { - super.init(initialSize,interface,account,object) + fileprivate let textLayout:TextViewLayout + fileprivate let descLayout:TextViewLayout? + fileprivate let file:TelegramMediaFile + fileprivate let thumbResource: TelegramMediaResource? + fileprivate let isCompactPlayer: Bool + fileprivate let messages: [Message] + init(_ initialSize:NSSize, _ interface:ChatInteraction, _ object: PeerMediaSharedEntry, isCompactPlayer: Bool = false, viewType: GeneralViewType = .legacy) { + self.isCompactPlayer = isCompactPlayer + + file = object.message!.media[0] as! TelegramMediaFile + + switch object { + case let .messageEntry(_, messages, _, _): + self.messages = messages + default: + self.messages = [] + } - file = message.media[0] as! TelegramMediaFile - let attr = NSMutableAttributedString() let music = file.musicText - _ = attr.append(string: music.0, color: theme.colors.text, font: .medium(.header)) - _ = attr.append(string: "\n") - _ = attr.append(string: music.1, color: theme.colors.grayText, font: .normal(.text)) - textLayout = TextViewLayout(attr, maximumNumberOfLines: 2, truncationType: .middle) + self.textLayout = TextViewLayout(.initialize(string: music.0, color: theme.colors.text, font: .medium(.text)), maximumNumberOfLines: 1, truncationType: .end) + + if !music.1.isEmpty { + let text: String + if let duration = file.duration { + text = timerText(Int(duration)) + " • " + music.1 + } else { + text = music.1 + } + self.descLayout = TextViewLayout(.initialize(string: text, color: theme.colors.grayText, font: .normal(.short)), maximumNumberOfLines: 1) + } else if let size = file.size { + self.descLayout = TextViewLayout(.initialize(string: String.prettySized(with: size), color: theme.colors.grayText, font: .normal(.short)), maximumNumberOfLines: 1) + } else { + descLayout = nil + } + let resource: TelegramMediaResource? + if file.previewRepresentations.isEmpty { + if !file.mimeType.contains("ogg") { + resource = ExternalMusicAlbumArtResource(title: file.musicText.0, performer: file.musicText.1, isThumbnail: true) + } else { + resource = nil + } + } else { + resource = file.previewRepresentations.first!.resource + } + self.thumbResource = resource + + + super.init(initialSize, interface, object, viewType: viewType) + + } + + override var inset: NSEdgeInsets { + if isCompactPlayer { + return NSEdgeInsetsMake(5, 10, 5, 10) + } else { + return NSEdgeInsetsMake(0, 0, 0, 0) + } } override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { - textLayout?.measure(width: width - 70) - return super.makeSize(width, oldWidth: oldWidth) + let success = super.makeSize(width, oldWidth: oldWidth) + textLayout.measure(width: self.blockWidth - contentInset.left - contentInset.right - self.viewType.innerInset.left - self.viewType.innerInset.right) + descLayout?.measure(width: self.blockWidth - contentInset.left - contentInset.right - self.viewType.innerInset.left - self.viewType.innerInset.right) + return success } override func viewClass() -> AnyClass { return PeerMediaMusicRowView.self } + override func menuItems(in location: NSPoint) -> Signal<[ContextMenuItem], NoError> { + if isCompactPlayer { + return .single([]) + } else { + return super.menuItems(in: location) + } + } + } class PeerMediaMusicRowView : PeerMediaRowView, APDelegate { private let textView:TextView = TextView() - let statusView:RadialProgressView = RadialProgressView() + private let descView:TextView = TextView() + + let thumbView:TransformImageView = TransformImageView(frame: NSMakeRect(0, 0, 40, 40)) var fetchStatus: MediaResourceStatus? let statusDisposable = MetaDisposable() let fetchDisposable = MetaDisposable() + private var playAnimationView: PeerMediaPlayerAnimationView? private(set) var fetchControls:FetchControls! required init(frame frameRect: NSRect) { super.init(frame: frameRect) + textView.isSelectable = false + textView.userInteractionEnabled = false + + descView.isSelectable = false + descView.userInteractionEnabled = false + addSubview(textView) - addSubview(statusView) - fetchControls = FetchControls(fetch: { [weak self] in - self?.executeInteraction(true) - }) - statusView.fetchControls = fetchControls + addSubview(descView) + addSubview(thumbView) +// fetchControls = FetchControls(fetch: { [weak self] in +// self?.executeInteraction(true) +// }) + + // thumbView.fetchControls = fetchControls + } + + override func mouseUp(with event: NSEvent) { + guard let item = item as? PeerMediaMusicRowItem else { + super.mouseUp(with: event) + return + } + if item.interface.presentation.state == .normal { + executeInteraction(true) + } else { + super.mouseUp(with: event) + } } override func layout() { super.layout() - if let item = item as? PeerMediaMusicRowItem, let layout = item.textLayout { - let f = focus(layout.layoutSize) - textView.update(layout, origin: NSMakePoint(60, f.minY)) - statusView.centerY(x: 10) + if let item = item as? PeerMediaMusicRowItem { + textView.update(item.textLayout, origin: NSMakePoint(item.contentInset.left, item.contentInset.top + 2)) + + if let descLayout = item.descLayout { + descView.update(descLayout, origin: NSMakePoint(item.contentInset.left, item.contentSize.height - descLayout.layoutSize.height - item.contentInset.bottom - 2)) + } else { + descView.update(nil) + textView.centerY() + } + + thumbView.centerY(x: 0) + playAnimationView?.centerY(x: 0) } } - func songDidChanged(song: APSongItem, for controller: APController) { + func songDidChanged(song: APSongItem, for controller: APController, animated: Bool) { checkState() } - func songDidChangedState(song: APSongItem, for controller: APController) { + func songDidChangedState(song: APSongItem, for controller: APController, animated: Bool) { checkState() } - func songDidStartPlaying(song:APSongItem, for controller:APController) { - + func songDidStartPlaying(song:APSongItem, for controller:APController, animated: Bool) { + checkState() } - func songDidStopPlaying(song:APSongItem, for controller:APController) { - + func songDidStopPlaying(song:APSongItem, for controller:APController, animated: Bool) { + checkState() } - func playerDidChangedTimebase(song:APSongItem, for controller:APController) { - + func playerDidChangedTimebase(song:APSongItem, for controller:APController, animated: Bool) { + //checkState() } - func audioDidCompleteQueue(for controller:APController) { - + func audioDidCompleteQueue(for controller:APController, animated: Bool) { + checkState() } func executeInteraction(_ isControl:Bool) -> Void { - if let fetchStatus = self.fetchStatus { - switch fetchStatus { - case .Fetching: - if isControl { - cancelFetching() - } - case .Remote: - fetch() - case .Local: - open() - break - } - } + open() } func checkState() { if let item = item as? PeerMediaMusicRowItem { if let controller = globalAudio, let song = controller.currentSong { - if song.entry.isEqual(to: item.message), case .playing = song.state { - statusView.theme = RadialProgressTheme(backgroundColor: theme.colors.blueFill, foregroundColor: .white, icon: theme.icons.chatMusicPause, iconInset:NSEdgeInsets(left:1)) + if song.entry.isEqual(to: item.message.id) { + if playAnimationView == nil { + playAnimationView = PeerMediaPlayerAnimationView() + addSubview(playAnimationView!) + playAnimationView?.centerY(x: 0) + } + if case .playing = song.state { + playAnimationView?.isPlaying = true + } else if case .stoped = song.state { + playAnimationView?.removeFromSuperview() + playAnimationView = nil + } else { + playAnimationView?.isPlaying = false + } + } else { - statusView.theme = RadialProgressTheme(backgroundColor: theme.colors.blueFill, foregroundColor: .white, icon: theme.icons.chatMusicPlay, iconInset:NSEdgeInsets(left:1)) + playAnimationView?.removeFromSuperview() + playAnimationView = nil } } else { - statusView.theme = RadialProgressTheme(backgroundColor: theme.colors.blueFill, foregroundColor: .white, icon: theme.icons.chatMusicPlay, iconInset:NSEdgeInsets(left:1)) + playAnimationView?.removeFromSuperview() + playAnimationView = nil } } } @@ -122,71 +212,65 @@ class PeerMediaMusicRowView : PeerMediaRowView, APDelegate { if let item = item as? PeerMediaMusicRowItem { var updatedStatusSignal: Signal? textView.update(item.textLayout) - textView.centerY(x: 60) + textView.centerY(x: item.contentInset.left) textView.backgroundColor = backdorColor globalAudio?.add(listener: self) + + let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0)) + let arguments = TransformImageArguments(corners: imageCorners, imageSize: PeerMediaIconSize, boundingSize: PeerMediaIconSize, intrinsicInsets: NSEdgeInsets()) + + thumbView.layer?.contents = theme.icons.playerMusicPlaceholder + thumbView.layer?.cornerRadius = .cornerRadius + if let resource = item.thumbResource { + let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [TelegramMediaImageRepresentation(dimensions: PixelDimensions(PeerMediaIconSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + + thumbView.setSignal(chatMessagePhotoThumbnail(account: item.interface.context.account, imageReference: ImageMediaReference.message(message: MessageReference(item.message), media: image))) + + thumbView.set(arguments: arguments) + } + + if item.message.flags.contains(.Unsent) && !item.message.flags.contains(.Failed) { - updatedStatusSignal = combineLatest(chatMessageFileStatus(account: item.account, file: item.file), item.account.pendingMessageManager.pendingMessageStatus(item.message.id)) + updatedStatusSignal = combineLatest(chatMessageFileStatus(account: item.interface.context.account, file: item.file), item.interface.context.account.pendingMessageManager.pendingMessageStatus(item.message.id)) |> map { resourceStatus, pendingStatus -> MediaResourceStatus in - if let pendingStatus = pendingStatus { + if let pendingStatus = pendingStatus.0 { return .Fetching(isActive: true, progress: pendingStatus.progress) } else { return resourceStatus } } |> deliverOnMainQueue } else { - updatedStatusSignal = chatMessageFileStatus(account: item.account, file: item.file) |> deliverOnMainQueue + updatedStatusSignal = chatMessageFileStatus(account: item.interface.context.account, file: item.file) |> deliverOnMainQueue } if let updatedStatusSignal = updatedStatusSignal { self.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak self] status in - if let strongSelf = self { - strongSelf.fetchStatus = status - switch status { - case let .Fetching(_, progress): - strongSelf.statusView.state = .Fetching(progress: progress, force: false) - case .Local, .Remote: - strongSelf.statusView.state = .Play - } - } + self?.fetchStatus = status })) checkState() } - + needsLayout = true } } func open() { if let item = item as? PeerMediaMusicRowItem { - if let controller = globalAudio, let song = controller.currentSong, song.entry.isEqual(to: item.message) { - controller.playOrPause() + if let controller = globalAudio, controller.playOrPause(item.message.id) { } else { - let controller = APChatMusicController(account: item.account, peerId: item.message.id.peerId, index: MessageIndex(item.message)) + let controller = APChatMusicController(context: item.interface.context, chatLocationInput: .peer(item.message.id.peerId), mode: .history, index: MessageIndex(item.message), messages: item.messages) item.interface.inlineAudioPlayer(controller) controller.start() - addGlobalAudioToVisible() } } } - - func addGlobalAudioToVisible() { - if let controller = globalAudio { - item?.table?.enumerateViews(with: { (view) in - if let view = (view as? PeerMediaMusicRowView) { - controller.add(listener: view) - } - return true - }) - } - - } + func fetch() { if let item = item as? PeerMediaMusicRowItem { - fetchDisposable.set(chatMessageFileInteractiveFetched(account: item.account, file: item.file).start()) + fetchDisposable.set(messageMediaFileInteractiveFetched(context: item.interface.context, messageId: item.message.id, fileReference: FileMediaReference.message(message: MessageReference(item.message), media: item.file)).start()) } open() } @@ -194,7 +278,7 @@ class PeerMediaMusicRowView : PeerMediaRowView, APDelegate { func cancelFetching() { if let item = item as? PeerMediaMusicRowItem { - fetchDisposable.set(chatMessageFileInteractiveFetched(account: item.account, file: item.file).start()) + messageMediaFileCancelInteractiveFetch(context: item.interface.context, messageId: item.message.id, fileReference: FileMediaReference.message(message: MessageReference(item.message), media: item.file)) } } diff --git a/Telegram-Mac/PeerMediaPhotosController.swift b/Telegram-Mac/PeerMediaPhotosController.swift new file mode 100644 index 0000000000..9ffefb3b88 --- /dev/null +++ b/Telegram-Mac/PeerMediaPhotosController.swift @@ -0,0 +1,460 @@ +// +// PeerMediaPhotosController.swift +// Telegram +// +// Created by Mikhail Filimonov on 17.10.2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit +extension Message : Equatable { + public static func ==(lhs: Message, rhs: Message) -> Bool { + return isEqualMessages(lhs, rhs) + } +} + +private enum PeerMediaMonthEntry : TableItemListNodeEntry { + case month(index: MessageIndex, items: [Message], galleryType: GalleryAppearType, viewType: GeneralViewType) + case date(index: MessageIndex) + case section(index: MessageIndex) + + static func < (lhs: PeerMediaMonthEntry, rhs: PeerMediaMonthEntry) -> Bool { + return lhs.index < rhs.index + } + + var description: String { + let formatter = DateFormatter() + formatter.dateFormat = "MMMM yyyy"; + switch self { + case let .month(index, _, _, _): + let date = Date(timeIntervalSince1970: TimeInterval(index.timestamp)) + return "items: \(formatter.string(from: date))" + case let .date(index): + let date = Date(timeIntervalSince1970: TimeInterval(index.timestamp)) + return "date: \(formatter.string(from: date))" + case let .section(index): + let date = Date(timeIntervalSince1970: TimeInterval(index.timestamp)) + return "section: \(formatter.string(from: date))" + } + } + + func item(_ arguments: PeerMediaPhotosArguments, initialSize: NSSize) -> TableRowItem { + switch self { + case let .month(_, items, galleryType, viewType): + return PeerPhotosMonthItem(initialSize, stableId: stableId, viewType: viewType, context: arguments.context, chatInteraction: arguments.chatInteraction, gallerySupplyment: arguments.gallerySupplyment, items: items, galleryType: galleryType) + case .date: + return PeerMediaDateItem(initialSize, index: index, stableId: stableId) + case .section: + return GeneralRowItem(initialSize, height: 20, stableId: stableId, viewType: .separator) + } + } + + var stableId: MessageIndex { + return self.index + } + + var index: MessageIndex { + switch self { + case let .month(index, _, _, _): + return index + case let .date(index): + return index + case let .section(index): + return index + } + } +} + +private final class PeerMediaPhotosArguments { + let context: AccountContext + let chatInteraction: ChatInteraction + let gallerySupplyment: InteractionContentViewProtocol + init(context: AccountContext, chatInteraction: ChatInteraction, gallerySupplyment: InteractionContentViewProtocol) { + self.context = context + self.gallerySupplyment = gallerySupplyment + self.chatInteraction = chatInteraction + } +} + + + +private struct PeerMediaPhotosState : Equatable { + let isLoading: Bool + let messages:[Message] + let searchState: SearchState + let contentSettings: ContentSettings + init(isLoading: Bool, messages: [Message], searchState: SearchState, contentSettings: ContentSettings) { + self.isLoading = isLoading + self.messages = messages.reversed().filter { $0.restrictedText(contentSettings) == nil } + self.searchState = searchState + self.contentSettings = contentSettings + } + func withAppendMessages(_ collection: [Message]) -> PeerMediaPhotosState { + var messages = self.messages + messages.append(contentsOf: collection) + return PeerMediaPhotosState(isLoading: self.isLoading, messages: messages, searchState: self.searchState, contentSettings: self.contentSettings) + } + func withUpdatedMessages(_ collection: [Message]) -> PeerMediaPhotosState { + return PeerMediaPhotosState(isLoading: self.isLoading, messages: collection, searchState: self.searchState, contentSettings: self.contentSettings) + } + func withUpdatedLoading(_ isLoading: Bool) -> PeerMediaPhotosState { + return PeerMediaPhotosState(isLoading: isLoading, messages: self.messages, searchState: self.searchState, contentSettings: self.contentSettings) + } + func withUpdatedSeachState(_ searchState: SearchState) -> PeerMediaPhotosState { + return PeerMediaPhotosState(isLoading: isLoading, messages: self.messages, searchState: searchState, contentSettings: self.contentSettings) + } +} + +private func mediaEntires(state: PeerMediaPhotosState, arguments: PeerMediaPhotosArguments, isExternalSearch: Bool) -> [PeerMediaMonthEntry] { + var entries:[PeerMediaMonthEntry] = [] + + let galleryType: GalleryAppearType + if isExternalSearch { + galleryType = .messages(state.messages) + } else { + galleryType = .history + } + + let timeDifference = Int32(arguments.context.timeDifference) + var temp:[Message] = [] + for i in 0 ..< state.messages.count { + + let message = state.messages[i] + + temp.append(message) + let next = i < state.messages.count - 1 ? state.messages[i + 1] : nil + if let nextMessage = next { + let dateId = mediaDateId(for: message.timestamp - timeDifference) + let nextDateId = mediaDateId(for: nextMessage.timestamp - timeDifference) + if dateId != nextDateId { + let index = MessageIndex(id: MessageId(peerId: message.id.peerId, namespace: message.id.namespace, id: 0), timestamp: Int32(dateId)) + var viewType: GeneralViewType = .modern(position: .single, insets: NSEdgeInsetsMake(0, 0, 1, 0)) + if !entries.isEmpty { + entries.append(.section(index: index.peerLocalSuccessor())) + entries.append(.date(index: index)) + } else { + if !isExternalSearch { + viewType = .modern(position: .last, insets: NSEdgeInsetsMake(0, 0, 1, 0)) + } + } + entries.append(.month(index: index.peerLocalPredecessor(), items: temp, galleryType: galleryType, viewType: viewType)) + temp.removeAll() + } + } else { + let dateId = mediaDateId(for: message.timestamp - timeDifference) + let index = MessageIndex(id: MessageId(peerId: message.id.peerId, namespace: message.id.namespace, id: 0), timestamp: Int32(dateId)) + + if !entries.isEmpty { + switch entries[entries.count - 1] { + case let .month(prevIndex, items, galleryType, viewType): + let prevDateId = mediaDateId(for: prevIndex.timestamp) + if prevDateId != dateId { + entries.append(.section(index: index.peerLocalSuccessor())) + entries.append(.date(index: index)) + entries.append(.month(index: index.peerLocalPredecessor(), items: temp, galleryType: galleryType, viewType: .modern(position: .single, insets: NSEdgeInsetsMake(0, 0, 1, 0)))) + } else { + entries[entries.count - 1] = .month(index: prevIndex, items: items + temp, galleryType: galleryType, viewType: viewType) + } + default: + assertionFailure() + } + } else { + if isExternalSearch { + entries.append(.month(index: index.peerLocalPredecessor(), items: temp, galleryType: galleryType, viewType: .modern(position: .single, insets: NSEdgeInsetsMake(0, 0, 1, 0)))) + } else { + entries.append(.month(index: index.peerLocalPredecessor(), items: temp, galleryType: galleryType, viewType: .modern(position: .last, insets: NSEdgeInsetsMake(0, 0, 1, 0)))) + } + } + + } + } + if !state.messages.isEmpty { + entries.append(.section(index: MessageIndex.absoluteLowerBound())) + } + + return entries +} + +fileprivate func prepareTransition(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], animated: Bool, initialSize:NSSize, arguments: PeerMediaPhotosArguments) -> TableUpdateTransition { + let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in + return entry.entry.item(arguments, initialSize: initialSize) + } + return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: animated) +} + + +private final class PeerMediaSupplyment : InteractionContentViewProtocol { + private weak var tableView: TableView? + init(tableView: TableView) { + self.tableView = tableView + } + + func contentInteractionView(for stableId: AnyHashable, animateIn: Bool) -> NSView? { + if let stableId = stableId.base as? ChatHistoryEntryId, let tableView = tableView { + switch stableId { + case let .message(message): + var found: NSView? = nil + tableView.enumerateItems { item -> Bool in + if let item = item as? PeerPhotosMonthItem { + if item.contains(message.id) { + found = item.view?.interactionContentView(for: message.id, animateIn: animateIn) + } + } + return found == nil + } + return found + default: + break + } + } + return nil + } + func interactionControllerDidFinishAnimation(interactive: Bool, for stableId: AnyHashable) { + + } + func addAccesoryOnCopiedView(for stableId: AnyHashable, view: NSView) { + if let stableId = stableId.base as? ChatHistoryEntryId, let tableView = tableView { + switch stableId { + case let .message(message): + tableView.enumerateItems { item -> Bool in + if let item = item as? PeerPhotosMonthItem { + if item.contains(message.id) { + item.view?.addAccesoryOnCopiedView(innerId: message.id, view: view) + return false + } + } + return true + } + default: + break + } + } + } + func videoTimebase(for stableId: AnyHashable) -> CMTimebase? { + return nil + } + func applyTimebase(for stableId: AnyHashable, timebase: CMTimebase?) { + + } +} + +class PeerMediaPhotosController: TableViewController, PeerMediaSearchable { + private let peerId: PeerId + private let disposable = MetaDisposable() + private let historyDisposable = MetaDisposable() + private let chatInteraction: ChatInteraction + private let previous: Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) + private let tags: MessageTags + private var isExternalSearch: Bool = false + init(_ context: AccountContext, chatInteraction: ChatInteraction, peerId: PeerId, tags: MessageTags) { + self.peerId = peerId + self.chatInteraction = chatInteraction + self.tags = tags + super.init(context) + } + + override func viewDidLoad() { + super.viewDidLoad() + + let context = self.context + let peerId = self.peerId + let initialSize = self.atomicSize + let tags = self.tags + let isExternalSearch = self.isExternalSearch + + self.genericView.set(stickClass: PeerMediaDateItem.self, handler: { item in + + }) + + self.genericView.needUpdateVisibleAfterScroll = true + self.searchState.set(.single(SearchState(state: .None, request: nil))) + self.genericView.emptyItem = PeerMediaEmptyRowItem(NSZeroSize, tags: self.tags) + + let perPageCount:()->Int = { + var rowCount:Int = 4 + var perWidth: CGFloat = 0 + let blockWidth = min(600, initialSize.with { $0.width } - 60) + while true { + let maximum = blockWidth - 7 - 7 - CGFloat(rowCount * 2) + perWidth = maximum / CGFloat(rowCount) + if perWidth >= 90 { + break + } else { + rowCount -= 1 + } + } + return Int((initialSize.with { $0.height } / perWidth) * CGFloat(rowCount) + CGFloat(rowCount)) + } + + var requestCount = perPageCount() + 20 + + let location: ValuePromise = ValuePromise(.Initial(count: requestCount), ignoreRepeated: true) + + let initialState = PeerMediaPhotosState(isLoading: false, messages: [], searchState: SearchState(state: .None, request: nil), contentSettings: context.contentSettings) + let state: ValuePromise = ValuePromise() + let stateValue: Atomic = Atomic(value: initialState) + let updateState:((PeerMediaPhotosState)->PeerMediaPhotosState) -> Void = { f in + state.set(stateValue.modify(f)) + } + + let supplyment = PeerMediaSupplyment(tableView: genericView) + + let arguments = PeerMediaPhotosArguments(context: context, chatInteraction: chatInteraction, gallerySupplyment: supplyment) + + + let applyHole:() -> Void = { + location.set(.Initial(count: requestCount)) + } + + struct SearchResult { + let result: [Message]? + } + + let history: Signal<(ChatHistoryViewUpdate?, SearchResult?, SearchState), NoError> = combineLatest(searchState.get(), location.get(), externalSearch.get()) |> mapToSignal { search, location, externalSearch in + if let externalSearch = externalSearch { + return .single((nil, SearchResult(result: externalSearch.messages), search)) + } else if !search.request.isEmpty { + + let req = context.engine.messages.searchMessages(location: .peer(peerId: peerId, fromId: nil, tags: .photoOrVideo, topMsgId: nil, minDate: nil, maxDate: nil), query: search.request, state: nil) + + return .single((nil, SearchResult(result: nil), search)) |> then(req |> delay(0.2, queue: .concurrentDefaultQueue()) |> map { (nil, SearchResult(result: $0.0.messages), search) }) + } else { + return chatHistoryViewForLocation(location, context: context, chatLocation: .peer(peerId), fixedCombinedReadStates: nil, tagMask: tags) |> map { ($0, nil, search) } + } + } + + historyDisposable.set(history.start(next: { update in + + var messages:[Message]? = nil + var isLoading: Bool = false + if let update = update.0 { + let view: MessageHistoryView? + let updateType: ChatHistoryViewUpdateType + switch update { + case let .Loading(_, ut): + view = nil + isLoading = true + updateType = ut + case let .HistoryView(values): + view = values.view + isLoading = values.view.isLoading + updateType = values.type + } + + switch updateType { + case let .Generic(type: type): + switch type { + case .FillHole: + DispatchQueue.main.async(execute: applyHole) + default: + break + } + default: + break + } + messages = view?.entries.map { value in + return value.message + } ?? [] + } else if let update = update.1 { + if let search = update.result { + messages = search + } else { + isLoading = true + } + } + + updateState { state in + var state = state + state = state.withUpdatedLoading(isLoading) + if let messages = messages { + if !isExternalSearch { + state = state.withUpdatedMessages(messages.reversed()) + } else { + state = state.withUpdatedMessages(messages) + } + } + state = state.withUpdatedSeachState(update.2) + return state + } + })) + + let previous = self.previous + + let transition: Signal<(TableUpdateTransition, PeerMediaPhotosState), NoError> = combineLatest(queue: prepareQueue, state.get(), appearanceSignal) |> map { state, appearance in + let entries = mediaEntires(state: state, arguments: arguments, isExternalSearch: isExternalSearch).map { AppearanceWrapperEntry(entry: $0, appearance: appearance) } + return (prepareTransition(left: previous.swap(entries), right: entries, animated: true, initialSize: initialSize.with { $0 }, arguments: arguments), state) + } |> deliverOnMainQueue + + + var previousSearch: SearchState = SearchState(state: .None, request: nil) + + disposable.set(transition.start(next: { [weak self] transition, state in + guard let `self` = self else { + return + } + + if previousSearch != state.searchState { + self.scrollup() + } + previousSearch = state.searchState + + self.genericView.merge(with: transition) + let state = MediaSearchState(state: state.searchState, animated: transition.animated, isLoading: state.isLoading) + self.mediaSearchState.set(state) + self.readyOnce() + })) + + genericView.setScrollHandler { position in + switch position.direction { + case .bottom: + requestCount += perPageCount() * 10 + location.set(.Initial(count: requestCount)) + default: + break + } + } + } + + private let mediaSearchState:ValuePromise = ValuePromise(ignoreRepeated: true) + private let searchState:Promise = Promise() + + func setSearchValue(_ value: Signal) { + searchState.set(value) + } + + private let externalSearch:Promise = Promise(nil) + + func setExternalSearch(_ value: Signal, _ loadMore: @escaping () -> Void) { + externalSearch.set(value) + self.isExternalSearch = true + } + + var mediaSearchValue:Signal { + return mediaSearchState.get() + } + private var isSearch: Bool = false { + didSet { + if isSearch { + searchState.set(.single(.init(state: .Focus, request: nil))) + } else { + searchState.set(.single(.init(state: .None, request: nil))) + } + } + } + func toggleSearch() { + let old = self.isSearch + self.isSearch = !old + } + + deinit { + disposable.dispose() + historyDisposable.dispose() + _ = previous.swap([]) + } + +} diff --git a/Telegram-Mac/PeerMediaPlayerAnimationView.swift b/Telegram-Mac/PeerMediaPlayerAnimationView.swift new file mode 100644 index 0000000000..7254598e93 --- /dev/null +++ b/Telegram-Mac/PeerMediaPlayerAnimationView.swift @@ -0,0 +1,97 @@ +// +// PeerMediaPlayerAnimationView.swift +// Telegram +// +// Created by Mikhail Filimonov on 29/06/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + + +class PeerMediaPlayerAnimationView: View { + + var isPlaying: Bool = false { + didSet { + if self.isPlaying != oldValue { + if self.isPlaying { + self.animateToPlaying() + } else { + self.animateToPaused() + } + } + } + } + + private let barNodes: [View] + + override init() { + + let baseSize = CGSize(width: 40, height: 40) + let barSize = CGSize(width: 3.0, height: 3) + let barSpacing: CGFloat = 2.0 + + let barsOrigin = CGPoint(x: floor((baseSize.width - (barSize.width * 4.0 + barSpacing * 3.0)) / 2.0), y: 18) + + var barNodes: [View] = [] + for i in 0 ..< 4 { + let barNode = View() + barNode.flip = false + barNode.frame = CGRect(origin: barsOrigin.offsetBy(dx: CGFloat(i) * (barSize.width + barSpacing), dy: 0.0), size: barSize) + barNode.backgroundColor = .white + barNode.layer?.anchorPoint = CGPoint(x: 0.5, y: 1) + barNodes.append(barNode) + } + self.barNodes = barNodes + + super.init(frame: NSMakeRect(0, 0, baseSize.width, baseSize.height)) + + flip = false + + self.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.5).cgColor + self.layer?.cornerRadius = .cornerRadius + for barNode in self.barNodes { + self.addSubview(barNode) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + func animateToPlaying() { + for barNode in self.barNodes { + let randValueMul = Float(4 % arc4random()) + let randDurationMul = Double(arc4random()) / Double(UInt32.max) + + let animation = CABasicAnimation(keyPath: "transform.scale.y") + animation.toValue = Float(randValueMul) as NSNumber + animation.autoreverses = true + animation.duration = 0.25 + 0.25 * randDurationMul + animation.repeatCount = Float.greatestFiniteMagnitude; + animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn) + + barNode.layer?.removeAnimation(forKey: "transform.scale.y") + barNode.layer?.add(animation, forKey: "transform.scale.y") + } + } + + func animateToPaused() { + for barNode in self.barNodes { + if let presentationLayer = barNode.layer?.presentation() { + let animation = CABasicAnimation(keyPath: "transform.scale.y") + animation.fromValue = (presentationLayer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0 + animation.toValue = 1.0 as NSNumber + animation.duration = 0.25 + animation.isRemovedOnCompletion = false + barNode.layer?.add(animation, forKey: "transform.scale.y") + } + } + } + +} diff --git a/Telegram-Mac/PeerMediaRowContent.swift b/Telegram-Mac/PeerMediaRowContent.swift index b8ed2d4378..9363730e08 100644 --- a/Telegram-Mac/PeerMediaRowContent.swift +++ b/Telegram-Mac/PeerMediaRowContent.swift @@ -8,66 +8,64 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore -class PeerMediaRowItem: TableRowItem { +import Postbox +import SwiftSignalKit + +let PeerMediaIconSize:NSSize = NSMakeSize(40, 40) + +class PeerMediaRowItem: GeneralRowItem { - var iconSize:NSSize = NSZeroSize - let contentInset:NSEdgeInsets = NSEdgeInsets(left: 70.0, right: 10, top: 5, bottom: 5) - var contentSize:NSSize = NSMakeSize(0, 50) + var contentInset:NSEdgeInsets = NSEdgeInsets(left: 50.0, right: 0, top: 0, bottom: 0) - override var stableId: AnyHashable { - return entry.stableId - } + var contentSize:NSSize = NSMakeSize(0, 40) override var height: CGFloat { - return contentSize.height + return contentSize.height + viewType.innerInset.top + viewType.innerInset.bottom + inset.top + inset.bottom } private var entry:PeerMediaSharedEntry - var message:Message - var account:Account - var interface:ChatInteraction + let message:Message + let interface:ChatInteraction + let automaticDownload: AutomaticMediaDownloadSettings - init(_ initialSize:NSSize, _ interface:ChatInteraction, _ account:Account, _ object: PeerMediaSharedEntry) { - + init(_ initialSize:NSSize, _ interface:ChatInteraction, _ object: PeerMediaSharedEntry, viewType: GeneralViewType = .legacy) { self.entry = object - self.account = account self.interface = interface - - if case let .messageEntry(message) = object { + if case let .messageEntry(message, _, automaticDownload, _) = object { self.message = message + self.automaticDownload = automaticDownload } else { fatalError("entry haven't message") } - super.init(initialSize) + super.init(initialSize, stableId: object.stableId, viewType: viewType, inset: NSEdgeInsetsZero) } - override func menuItems() -> Signal<[ContextMenuItem], Void> { + override func menuItems(in location: NSPoint) -> Signal<[ContextMenuItem], NoError> { + var items:[ContextMenuItem] = [] - if canForwardMessage(message, account: account) { - items.append(ContextMenuItem(tr(.messageContextForward), handler: { [weak self] in + if canForwardMessage(message, chatInteraction: interface) { + items.append(ContextMenuItem(L10n.messageContextForward, handler: { [weak self] in if let strongSelf = self { strongSelf.interface.forwardMessages([strongSelf.message.id]) } })) } - if canDeleteMessage(message, account: account) { - items.append(ContextMenuItem(tr(.messageContextDelete), handler: { [weak self] in + if canDeleteMessage(message, account: interface.context.account, mode: .history) { + items.append(ContextMenuItem(L10n.messageContextDelete, handler: { [weak self] in if let strongSelf = self { strongSelf.interface.deleteMessages([strongSelf.message.id]) } })) } - items.append(ContextMenuItem(tr(.messageContextGoto), handler: { [weak self] in + items.append(ContextMenuItem(L10n.messageContextGoto, handler: { [weak self] in if let strongSelf = self { - strongSelf.interface.focusMessageId(nil, strongSelf.message.id, .center(id: 0, animated: false, focus: false, inset: 0)) + strongSelf.interface.focusMessageId(nil, strongSelf.message.id, .center(id: 0, innerId: nil, animated: false, focus: .init(focus: false), inset: 0)) } })) @@ -75,26 +73,69 @@ class PeerMediaRowItem: TableRowItem { return .single(items) } + override var instantlyResize: Bool { + return true + } + override func viewClass() -> AnyClass { return PeerMediaRowView.self } + + var separatorOffset: CGFloat { + return 10 + 40 + viewType.innerInset.left + } } -private let selectedImage = #imageLiteral(resourceName: "Icon_SelectionChecked").precomposed() -private let unselectedImage = #imageLiteral(resourceName: "Icon_SelectionUncheck").precomposed() - class PeerMediaRowView : TableRowView,ViewDisplayDelegate,Notifable { - + let containerView: GeneralRowContainerView = GeneralRowContainerView(frame: NSZeroRect) var contentView:View = View() - private var selectingControl:SelectingControl = SelectingControl(unselectedImage:unselectedImage, selectedImage:selectedImage) + private let separatorView = View() + private var selectingControl:SelectingControl? required init(frame frameRect: NSRect) { super.init(frame: frameRect) - super.addSubview(selectingControl) - super.addSubview(contentView) + containerView.addSubview(contentView) + containerView.addSubview(separatorView) + super.addSubview(containerView) contentView.displayDelegate = self - selectingControl.centerY(x:-selectingControl.frame.width) + + containerView.set(handler: { [weak self] _ in + if let item = self?.item as? PeerMediaRowItem { + if item.interface.presentation.state == .selecting { + item.interface.update({$0.withToggledSelectedMessage(item.message.id)}) + } + } + }, for: .Click) + + } + + override func updateColors() { + guard let item = item as? PeerMediaRowItem else { + return + } + self.backgroundColor = item.viewType.rowBackground + self.containerView.backgroundColor = backdorColor + self.separatorView.backgroundColor = theme.colors.border + } + + override func layout() { + guard let item = item as? PeerMediaRowItem else { + return + } + + let contentX = item.interface.presentation.state == .selecting ? item.viewType.innerInset.left + 22 + item.viewType.innerInset.left : item.viewType.innerInset.left + + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) + self.containerView.setCorners(item.viewType.corners) + + self.contentView.setFrameSize(NSMakeSize(self.containerView.frame.width - item.viewType.innerInset.left - item.viewType.innerInset.right, self.containerView.frame.height - item.viewType.innerInset.bottom - item.viewType.innerInset.top)) + self.contentView.centerY(x: contentX) + + self.separatorView.frame = NSMakeRect(item.separatorOffset + (item.interface.presentation.state == .selecting ? 22 + item.viewType.innerInset.left : 0), self.containerView.frame.height - .borderSize, self.containerView.frame.width - item.separatorOffset - item.viewType.innerInset.right, .borderSize) + + selectingControl?.centerY(x: item.interface.presentation.state == .selecting ? item.viewType.innerInset.left : -22) + super.layout() } func notify(with value: Any, oldValue:Any, animated:Bool) { @@ -116,21 +157,9 @@ class PeerMediaRowView : TableRowView,ViewDisplayDelegate,Notifable { } override func draw(_ layer: CALayer, in ctx: CGContext) { - super.draw(layer, in: ctx) - if layer == contentView.layer { - - if let item = self.item as? PeerMediaRowItem { - ctx.setFillColor(theme.colors.border.cgColor) - ctx.fill(NSMakeRect(item.contentInset.left, layer.frame.height - .borderSize, layer.frame.width - item.contentInset.left - item.contentInset.right, .borderSize)) - } - } - } - - override func setFrameSize(_ newSize: NSSize) { - super.setFrameSize(newSize) - contentView.setFrameSize(newSize) } + override func set(item: TableRowItem, animated: Bool) { @@ -141,8 +170,11 @@ class PeerMediaRowView : TableRowView,ViewDisplayDelegate,Notifable { if let item = self.item as? PeerMediaRowItem { item.interface.add(observer: self) - updateSelectingMode(with: item.interface.presentation.state == .selecting) + updateSelectingMode(with: item.interface.presentation.state == .selecting, animated: animated) + + separatorView.isHidden = !item.viewType.hasBorder } + needsLayout = true } override func viewDidMoveToSuperview() { @@ -152,36 +184,47 @@ class PeerMediaRowView : TableRowView,ViewDisplayDelegate,Notifable { item.interface.remove(observer: self) } else { item.interface.add(observer: self) + updateSelectingMode(with: item.interface.presentation.state == .selecting, animated: !NSIsEmptyRect(visibleRect)) } } } - override func mouseDown(with event: NSEvent) { - super.mouseDown(with: event) - if let item = item as? PeerMediaRowItem { - if item.interface.presentation.state == .selecting { - item.interface.update({$0.withToggledSelectedMessage(item.message.id)}) - } - } - } - - - func updateSelectingMode(with selectingMode:Bool, animated:Bool = false) { if let item = item as? PeerMediaRowItem { + + containerView.userInteractionEnabled = selectingMode + let to:NSPoint if selectingMode { - to = NSMakePoint(35,0) + to = NSMakePoint(item.viewType.innerInset.left + 22 + item.viewType.innerInset.left, self.contentView.frame.minY) } else { - to = NSMakePoint(0,0) + to = NSMakePoint(item.viewType.innerInset.left, self.contentView.frame.minY) } + self.separatorView.change(pos: NSMakePoint(item.separatorOffset + (selectingMode ? 22 + item.viewType.innerInset.left : 0), self.containerView.frame.height - .borderSize), animated: animated) contentView.change(pos: to, animated: animated) - let selectingFrom = NSMakePoint(-selectingControl.frame.width,selectingControl.frame.minY) - let selectingTo = NSMakePoint(20.0 - floorToScreenPixels(selectingControl.frame.width/2.0),selectingControl.frame.minY) - selectingControl.change(pos: selectingMode ? selectingTo : selectingFrom, animated: animated) - selectingControl.set(selected: item.interface.presentation.isSelectedMessageId(item.message.id), animated: animated) + + if selectingMode { + if selectingControl == nil { + selectingControl = SelectingControl(unselectedImage: theme.icons.chatToggleUnselected, selectedImage: theme.icons.chatToggleSelected) + containerView.addSubview(selectingControl!) + selectingControl!.centerY(x: -22) + selectingControl?.change(pos: NSMakePoint(item.viewType.innerInset.left, selectingControl!.frame.minY), animated: animated) + } + } else { + if let selectingControl = selectingControl { + let point = NSMakePoint(-22, selectingControl.frame.minY) + self.selectingControl = nil + selectingControl.change(pos: point, animated: animated, completion: { [weak selectingControl] _ in + selectingControl?.removeFromSuperview() + }) + } + } + selectingControl?.set(selected: item.interface.presentation.isSelectedMessageId(item.message.id), animated: animated) + + +// selectingControl.change(pos: selectingMode ? selectingTo : selectingFrom, animated: animated) } } diff --git a/Telegram-Mac/PeerMediaTouchBar.swift b/Telegram-Mac/PeerMediaTouchBar.swift new file mode 100644 index 0000000000..0cc63c7edd --- /dev/null +++ b/Telegram-Mac/PeerMediaTouchBar.swift @@ -0,0 +1,181 @@ +// +// PeerMediaTouchBar.swift +// Telegram +// +// Created by Mikhail Filimonov on 04/10/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import SwiftSignalKit + +@available(OSX 10.12.2, *) +private func peerMediaTouchBarItems(presentation: ChatPresentationInterfaceState) -> [NSTouchBarItem.Identifier] { + var items: [NSTouchBarItem.Identifier] = [] + items.append(.flexibleSpace) + if presentation.selectionState != nil { + items.append(.forward) + items.append(.delete) + } else { + items.append(.segmentMedias) + } + items.append(.flexibleSpace) + return items +} + +@available(OSX 10.12.2, *) +private extension NSTouchBarItem.Identifier { + static let segmentMedias = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.sharedMedia.segment") + static let forward = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.sharedMedia.forward") + static let delete = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.sharedMedia.delete") + +} + +@available(OSX 10.12.2, *) +class PeerMediaTouchBar: NSTouchBar, NSTouchBarDelegate, Notifable { + + private let modeDisposable = MetaDisposable() + private let chatInteraction: ChatInteraction + private let toggleMode: (PeerMediaCollectionMode) -> Void + private var currentMode: PeerMediaCollectionMode = .photoOrVideo + init(chatInteraction: ChatInteraction, currentMode: Signal, toggleMode: @escaping(PeerMediaCollectionMode) -> Void) { + self.chatInteraction = chatInteraction + self.toggleMode = toggleMode + super.init() + self.delegate = self + chatInteraction.add(observer: self) + self.defaultItemIdentifiers = peerMediaTouchBarItems(presentation: chatInteraction.presentation) + modeDisposable.set(currentMode.start(next: { [weak self] mode in + if let mode = mode { + let view = ((self?.item(forIdentifier: .segmentMedias) as? NSCustomTouchBarItem)?.view as? NSSegmentedControl) + let selected = Int(mode.rawValue + 1) + if selected > 0 { + view?.setSelected(true, forSegment: Int(mode.rawValue + 1)) + self?.currentMode = mode + } + } + })) + } + + private func updateUserInterface() { + for identifier in itemIdentifiers { + switch identifier { + case .forward: + let button = (item(forIdentifier: identifier) as? NSCustomTouchBarItem)?.view as? NSButton + button?.bezelColor = chatInteraction.presentation.canInvokeBasicActions.forward ? theme.colors.accent : nil + button?.isEnabled = chatInteraction.presentation.canInvokeBasicActions.forward + + case .delete: + let button = (item(forIdentifier: identifier) as? NSCustomTouchBarItem)?.view as? NSButton + button?.bezelColor = chatInteraction.presentation.canInvokeBasicActions.delete ? theme.colors.redUI : nil + button?.isEnabled = chatInteraction.presentation.canInvokeBasicActions.delete + case .segmentMedias: + let view = ((item(forIdentifier: identifier) as? NSCustomTouchBarItem)?.view as? NSSegmentedControl) + view?.setSelected(true, forSegment: Int(self.currentMode.rawValue)) + default: + break + } + } + } + + deinit { + chatInteraction.remove(observer: self) + modeDisposable.dispose() + } + + func isEqual(to other: Notifable) -> Bool { + return false + } + + func notify(with value: Any, oldValue: Any, animated: Bool) { + if let value = value as? ChatPresentationInterfaceState { + self.defaultItemIdentifiers = peerMediaTouchBarItems(presentation: value) + updateUserInterface() + } + } + + @objc private func segmentMediasAction(_ sender: Any?) { + if let sender = sender as? NSSegmentedControl { + switch sender.selectedSegment { + case 0: + toggleMode(.photoOrVideo) + case 1: + toggleMode(.file) + case 2: + toggleMode(.webpage) + case 3: + toggleMode(.music) + case 4: + toggleMode(.voice) + default: + break + } + } + } + + @objc private func forwardMessages() { + chatInteraction.forwardSelectedMessages() + } + @objc private func deleteMessages() { + chatInteraction.deleteSelectedMessages() + } + + + func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? { + switch identifier { + case .segmentMedias: + let item = NSCustomTouchBarItem(identifier: identifier) + + let segment = NSSegmentedControl() + segment.segmentStyle = .automatic + segment.segmentCount = 5 + segment.setLabel(L10n.peerMediaMedia, forSegment: 0) + segment.setLabel(L10n.peerMediaFiles, forSegment: 1) + segment.setLabel(L10n.peerMediaLinks, forSegment: 2) + segment.setLabel(L10n.peerMediaAudio, forSegment: 3) + segment.setLabel(L10n.peerMediaVoice, forSegment: 4) + + segment.setWidth(93, forSegment: 0) + segment.setWidth(93, forSegment: 1) + segment.setWidth(93, forSegment: 2) + segment.setWidth(93, forSegment: 3) + segment.setWidth(93, forSegment: 4) + + segment.trackingMode = .selectOne + segment.target = self + segment.action = #selector(segmentMediasAction(_:)) + item.view = segment + return item + case .forward: + let item = NSCustomTouchBarItem(identifier: identifier) + let icon = NSImage(named: NSImage.Name("Icon_TouchBar_MessagesForward"))! + let button = NSButton(title: L10n.messageActionsPanelForward, image: icon, target: self, action: #selector(forwardMessages)) + button.addWidthConstraint(size: 160) + button.bezelColor = theme.colors.accent + button.imageHugsTitle = true + button.isEnabled = chatInteraction.presentation.canInvokeBasicActions.forward + item.view = button + item.customizationLabel = button.title + return item + case .delete: + let item = NSCustomTouchBarItem(identifier: identifier) + let icon = NSImage(named: NSImage.Name("Icon_TouchBar_MessagesDelete"))! + let button = NSButton(title: L10n.messageActionsPanelDelete, image: icon, target: self, action: #selector(deleteMessages)) + button.addWidthConstraint(size: 160) + button.bezelColor = theme.colors.redUI + button.imageHugsTitle = true + button.isEnabled = chatInteraction.presentation.canInvokeBasicActions.delete + item.view = button + item.customizationLabel = button.title + return item + default: + return nil + } + } + + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/Telegram-Mac/PeerMediaVoiceRowItem.swift b/Telegram-Mac/PeerMediaVoiceRowItem.swift new file mode 100644 index 0000000000..c1622756ab --- /dev/null +++ b/Telegram-Mac/PeerMediaVoiceRowItem.swift @@ -0,0 +1,377 @@ +// +// PeerMediaVoiceRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 27/07/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + + +class PeerMediaVoiceRowItem: PeerMediaRowItem { + fileprivate let file:TelegramMediaFile + fileprivate let titleLayout: TextViewLayout + fileprivate let nameLayout: TextViewLayout + override init(_ initialSize:NSSize, _ interface:ChatInteraction, _ object: PeerMediaSharedEntry, viewType: GeneralViewType = .legacy) { + let message = object.message! + file = message.media[0] as! TelegramMediaFile + + let formatter = DateFormatter() + formatter.dateStyle = .medium + + let date = Date(timeIntervalSince1970: TimeInterval(object.message!.timestamp) - interface.context.timeDifference) + + + titleLayout = TextViewLayout(.initialize(string: formatter.string(from: date), color: theme.colors.text, font: .medium(.text)), maximumNumberOfLines: 1) + + var peer:Peer? = message.chatPeer(interface.context.peerId) + + var title:String = peer?.displayTitle ?? "" + if let _peer = messageMainPeer(message) as? TelegramChannel, case .broadcast(_) = _peer.info { + title = _peer.displayTitle + peer = _peer + } + + nameLayout = TextViewLayout(.initialize(string: title, color: theme.colors.grayText, font: .normal(.short)), maximumNumberOfLines: 1) + + super.init(initialSize, interface, object, viewType: viewType) + + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) + nameLayout.measure(width: self.blockWidth - contentInset.left - contentInset.right - self.viewType.innerInset.left - self.viewType.innerInset.right) + titleLayout.measure(width: self.blockWidth - contentInset.left - contentInset.right - self.viewType.innerInset.left - self.viewType.innerInset.right) + return success + } + + override func viewClass() -> AnyClass { + return PeerMediaVoiceRowView.self + } +} + + +final class PeerMediaVoiceRowView : PeerMediaRowView, APDelegate { + private let titleView: TextView = TextView() + private let nameView: TextView = TextView() + private let progressView:RadialProgressView = RadialProgressView() + private let statusDisposable = MetaDisposable() + private let fetchDisposable = MetaDisposable() + private var player:GIFPlayerView = GIFPlayerView() + private let resourceDataDisposable = MetaDisposable() + private let unreadDot: View = View() + private var instantVideoData: AVGifData? { + didSet { + updatePlayerIfNeeded() + } + } + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(titleView) + addSubview(nameView) + addSubview(player) + addSubview(progressView) + addSubview(unreadDot) + player.setFrameSize(40, 40) + unreadDot.setFrameSize(NSMakeSize(6, 6)) + unreadDot.layer?.cornerRadius = 3 + progressView.fetchControls = FetchControls(fetch: { [weak self] in + self?.executeInteraction(true) + }) + + titleView.userInteractionEnabled = false + titleView.isSelectable = false + nameView.userInteractionEnabled = false + nameView.isSelectable = false + } + + func removeNotificationListeners() { + NotificationCenter.default.removeObserver(self) + } + + @objc func updatePlayerIfNeeded() { + player.set(data: acceptVisibility ? instantVideoData : nil) + } + + var acceptVisibility:Bool { + return window != nil && window!.isKeyWindow && !NSIsEmptyRect(visibleRect) + } + + func updateListeners() { + if let window = window { + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSWindow.didBecomeKeyNotification, object: window) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSWindow.didResignKeyNotification, object: window) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSView.boundsDidChangeNotification, object: item?.table?.clipView) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSView.frameDidChangeNotification, object: item?.table?.view) + } else { + removeNotificationListeners() + } + } + + override func viewDidMoveToWindow() { + updateListeners() + updatePlayerIfNeeded() + } + + + func open() { + + guard let item = item as? PeerMediaVoiceRowItem else {return} + + if let controller = globalAudio, controller.playOrPause(item.message.id) { + } else { + let controller:APController = APChatVoiceController(context: item.interface.context, chatLocationInput: .peer(item.message.id.peerId), mode: .history, index: MessageIndex(item.message), volume: FastSettings.volumeRate) + item.interface.inlineAudioPlayer(controller) + controller.start() + } + } + + + + func fetch() { + if let item = item as? PeerMediaVoiceRowItem { + fetchDisposable.set(messageMediaFileInteractiveFetched(context: item.interface.context, messageId: item.message.id, fileReference: FileMediaReference.message(message: MessageReference.init(item.message), media: item.file)).start()) + } + } + + + func cancelFetching() { + if let item = item as? PeerMediaVoiceRowItem { + messageMediaFileCancelInteractiveFetch(context: item.interface.context, messageId: item.message.id, fileReference: FileMediaReference.message(message: MessageReference.init(item.message), media: item.file)) + } + } + + func songDidChanged(song: APSongItem, for controller: APController, animated: Bool) { + checkState() + } + func songDidChangedState(song: APSongItem, for controller: APController, animated: Bool) { + checkState() + } + + func songDidStartPlaying(song:APSongItem, for controller:APController, animated: Bool) { + + } + func songDidStopPlaying(song:APSongItem, for controller:APController, animated: Bool) { + + } + func playerDidChangedTimebase(song:APSongItem, for controller:APController, animated: Bool) { + + } + + func audioDidCompleteQueue(for controller:APController, animated: Bool) { + + } + + func delete() -> Void { + guard let item = item as? PeerMediaVoiceRowItem else {return} + let messageId = item.message.id + let engine = item.interface.context.engine.messages + _ = item.interface.context.account.postbox.transaction { transaction -> Void in + engine.deleteMessages(transaction: transaction, ids: [messageId]) + }.start() + } + + func executeInteraction(_ isControl:Bool) -> Void { + guard let item = item as? PeerMediaVoiceRowItem else {return} + + if let fetchStatus = self.fetchStatus { + switch fetchStatus { + case .Fetching: + if isControl { + if item.message.flags.contains(.Unsent) && !item.message.flags.contains(.Failed) { + delete() + } + cancelFetching() + } else { + //open() + } + case .Remote: + fetch() + //open() + case .Local: + open() + break + } + } + } + + + deinit { + statusDisposable.dispose() + fetchDisposable.dispose() + resourceDataDisposable.dispose() + player.set(data: nil) + removeNotificationListeners() + } + + var fetchStatus: MediaResourceStatus? { + didSet { + if let fetchStatus = fetchStatus { + switch fetchStatus { + case let .Fetching(_, progress): + progressView.state = .Fetching(progress: progress, force: false) + case .Remote: + progressView.state = .Remote + case .Local: + progressView.state = .Play + } + } + } + } + + override func updateSelectingMode(with selectingMode: Bool, animated: Bool = false) { + super.updateSelectingMode(with: selectingMode, animated: animated) + progressView.userInteractionEnabled = !selectingMode + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func updateColors() { + super.updateColors() + titleView.backgroundColor = backdorColor + nameView.backgroundColor = backdorColor + unreadDot.backgroundColor = theme.colors.accent + } + + override func layout() { + super.layout() + + guard let item = item as? PeerMediaVoiceRowItem else {return} + + let center = floorToScreenPixels(backingScaleFactor, contentView.frame.height / 2) + + titleView.setFrameOrigin(item.contentInset.left, center - titleView.frame.height - 1) + nameView.setFrameOrigin(item.contentInset.left, center + 1) + + progressView.centerY(x: 0) + player.centerY(x: 0) + + unreadDot.setFrameOrigin(titleView.frame.maxX + 5, center - titleView.frame.height / 2 - unreadDot.frame.height / 2) + } + + var isIncomingConsumed:Bool { + var isConsumed:Bool = false + if let parent = (item as? PeerMediaRowItem)?.message { + for attr in parent.attributes { + if let attr = attr as? ConsumableContentMessageAttribute { + isConsumed = attr.consumed + break + } + } + } + return isConsumed + } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + + guard let item = item as? PeerMediaVoiceRowItem else {return} + + titleView.update(item.titleLayout) + nameView.update(item.nameLayout) + + unreadDot.isHidden = isIncomingConsumed + + updateListeners() + + if item.file.isInstantVideo { + let size = player.frame.size + player.layer?.cornerRadius = player.frame.height / 2 + + let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: item.file.previewRepresentations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + player.setSignal( chatMessagePhoto(account: item.interface.context.account, imageReference: ImageMediaReference.message(message: MessageReference(item.message), media: image), scale: backingScaleFactor)) + let arguments = TransformImageArguments(corners: ImageCorners(radius: 20), imageSize: size, boundingSize: size, intrinsicInsets: NSEdgeInsets()) + player.set(arguments: arguments) + + + resourceDataDisposable.set((item.interface.context.account.postbox.mediaBox.resourceData(item.file.resource) |> deliverOnResourceQueue |> map { data in return data.complete ? AVGifData.dataFrom(data.path) : nil} |> deliverOnMainQueue).start(next: { [weak self] data in + self?.instantVideoData = data + })) + + } else { + player.setSignal(signal: .single(TransformImageResult(nil, false))) + player.set(data: nil) + instantVideoData = nil + resourceDataDisposable.set(nil) + } + + + + var updatedStatusSignal: Signal + + let file:TelegramMediaFile = item.file + + if item.message.flags.contains(.Unsent) && !item.message.flags.contains(.Failed) { + updatedStatusSignal = combineLatest(chatMessageFileStatus(account: item.interface.context.account, file: file), item.interface.context.account.pendingMessageManager.pendingMessageStatus(item.message.id)) + |> map { resourceStatus, pendingStatus -> MediaResourceStatus in + if let pendingStatus = pendingStatus.0 { + return .Fetching(isActive: true, progress: pendingStatus.progress) + } else { + return resourceStatus + } + } |> deliverOnMainQueue + } else { + updatedStatusSignal = chatMessageFileStatus(account: item.interface.context.account, file: file) |> deliverOnMainQueue + } + + self.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak self] status in + if let strongSelf = self { + strongSelf.fetchStatus = status + + switch status { + case let .Fetching(_, progress): + strongSelf.progressView.state = .Fetching(progress: progress, force: false) + case .Remote: + strongSelf.progressView.state = .Remote + case .Local: + strongSelf.progressView.state = .Play + } + } + })) + + checkState() + + needsLayout = true + + if item.automaticDownload.isDownloable(item.message) { + fetch() + } + + } + + func checkState() { + guard let item = item as? PeerMediaVoiceRowItem else {return} + let backgroundColor: NSColor + let foregroundColor: NSColor + if let media = item.message.media.first as? TelegramMediaFile, media.isInstantVideo { + backgroundColor = .blackTransparent + foregroundColor = .white + } else { + backgroundColor = theme.colors.fileActivityBackground + foregroundColor = theme.colors.fileActivityForeground + } + if let controller = globalAudio, let song = controller.currentSong { + + + if song.entry.isEqual(to: item.message), case .playing = song.state { + progressView.theme = RadialProgressTheme(backgroundColor: backgroundColor, foregroundColor: foregroundColor, icon: theme.icons.chatMusicPause, iconInset:NSEdgeInsets(left:0)) + progressView.state = .Icon(image: theme.icons.chatMusicPause, mode: .normal) + } else { + progressView.theme = RadialProgressTheme(backgroundColor: backgroundColor, foregroundColor: foregroundColor, icon: theme.icons.chatMusicPlay, iconInset:NSEdgeInsets(left:1)) + progressView.state = .Play + } + } else { + progressView.theme = RadialProgressTheme(backgroundColor: backgroundColor, foregroundColor: foregroundColor, icon: theme.icons.chatMusicPlay, iconInset:NSEdgeInsets(left:1)) + } + } + +} diff --git a/Telegram-Mac/PeerMediaWebpageRowContent.swift b/Telegram-Mac/PeerMediaWebpageRowContent.swift index 3b406d6a13..090ef29dc0 100644 --- a/Telegram-Mac/PeerMediaWebpageRowContent.swift +++ b/Telegram-Mac/PeerMediaWebpageRowContent.swift @@ -8,22 +8,64 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit + class PeerMediaWebpageRowItem: PeerMediaRowItem { - var textLayout:TextViewLayout? - var linkLayout:TextViewLayout? + private(set) var textLayout:TextViewLayout? + private(set) var linkLayouts:[TextViewLayout] = [] - var iconText:NSAttributedString? - var firstCharacter:String? - var icon:TelegramMediaImage? - var iconArguments:TransformImageArguments? - var thumb:CGImage? = nil - override init(_ initialSize:NSSize, _ interface:ChatInteraction, _ account:Account, _ object: PeerMediaSharedEntry) { - super.init(initialSize,interface,account,object) - iconSize = NSMakeSize(50, 50) + private(set) var iconText:NSAttributedString? + private(set) var firstCharacter:String? + private(set) var icon:TelegramMediaImage? + private(set) var iconArguments:TransformImageArguments? + private(set) var thumb:CGImage? = nil + override init(_ initialSize:NSSize, _ interface:ChatInteraction, _ object: PeerMediaSharedEntry, viewType: GeneralViewType = .legacy) { + super.init(initialSize,interface,object, viewType: viewType) + + + var linkLayouts:[TextViewLayout] = [] + + var links:[NSAttributedString] = [] + + for attr in message.attributes { + if let attr = attr as? TextEntitiesMessageAttribute { + for entity in attr.entities { + inner: switch entity.type { + case .Email: + let attributed = NSMutableAttributedString() + let link = message.text.nsstring.substring(with: NSMakeRange(min(entity.range.lowerBound, message.text.length), max(min(entity.range.upperBound - entity.range.lowerBound, message.text.length - entity.range.lowerBound), 0))) + let range = attributed.append(string: link, color: theme.colors.link, font: .normal(.text)) + attributed.addAttribute(.link, value: inApp(for: link as NSString, context: interface.context, peerId: interface.peerId, openInfo: interface.openInfo, applyProxy: interface.applyProxy, confirm: false), range: range) + links.append(attributed) + case .Url: + let attributed = NSMutableAttributedString() + let link = message.text.nsstring.substring(with: NSMakeRange(min(entity.range.lowerBound, message.text.length), max(min(entity.range.upperBound - entity.range.lowerBound, message.text.length - entity.range.lowerBound), 0))) + let range = attributed.append(string: link, color: theme.colors.link, font: .normal(.text)) + attributed.addAttribute(.link, value: inApp(for: link as NSString, context: interface.context, peerId: interface.peerId, openInfo: interface.openInfo, applyProxy: interface.applyProxy, confirm: false), range: range) + links.append(attributed) + case let .TextUrl(url): + let attributed = NSMutableAttributedString() + let range = attributed.append(string: url, color: theme.colors.link, font: .normal(.text)) + attributed.addAttribute(.link, value: inApp(for: url as NSString, context: interface.context, peerId: + interface.peerId, openInfo: interface.openInfo, applyProxy: interface.applyProxy, confirm: false), range: range) + links.append(attributed) + default: + break inner + } + } + break + } + } + + for attributed in links { + let linkLayout = TextViewLayout(attributed, maximumNumberOfLines: 1, truncationType: .middle) + linkLayout.interactions = globalLinkExecutor + linkLayouts.append(linkLayout) + } if let webpage = message.media.first as? TelegramMediaWebpage { @@ -39,16 +81,16 @@ class PeerMediaWebpageRowItem: PeerMediaRowItem { var iconImageRepresentation:TelegramMediaImageRepresentation? = nil if let image = content.image { - iconImageRepresentation = smallestImageRepresentation(image.representations) + iconImageRepresentation = largestImageRepresentation(image.representations) } else if let file = content.file { - iconImageRepresentation = smallestImageRepresentation(file.previewRepresentations) + iconImageRepresentation = largestImageRepresentation(file.previewRepresentations) } if let iconImageRepresentation = iconImageRepresentation { - icon = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [iconImageRepresentation]) + icon = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [iconImageRepresentation], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) - let imageCorners = ImageCorners(radius: iconSize.width/2) - iconArguments = TransformImageArguments(corners: imageCorners, imageSize: iconImageRepresentation.dimensions.aspectFilled(iconSize), boundingSize: iconSize, intrinsicInsets: NSEdgeInsets()) + let imageCorners = ImageCorners(radius: .cornerRadius) + iconArguments = TransformImageArguments(corners: imageCorners, imageSize: iconImageRepresentation.dimensions.size.aspectFilled(PeerMediaIconSize), boundingSize: PeerMediaIconSize, intrinsicInsets: NSEdgeInsets()) } @@ -58,39 +100,20 @@ class PeerMediaWebpageRowItem: PeerMediaRowItem { if let text = content.text { let _ = attributedText.append(string: "\n") - let _ = attributedText.append(string: text, color: theme.colors.text, font: NSFont.normal(FontSize.text)) - attributedText.detectLinks(type: [.Links, .Mentions, .Hashtags], account: account, openInfo: interface.openInfo) + let _ = attributedText.append(string: text, color: theme.colors.text, font: .normal(.short)) + attributedText.detectLinks(type: [.Links], context: interface.context, openInfo: interface.openInfo) } - textLayout = TextViewLayout(attributedText, maximumNumberOfLines: 6, truncationType: .end) - - let linkAttributed:NSMutableAttributedString = NSMutableAttributedString() - let _ = linkAttributed.append(string: content.displayUrl, color: theme.colors.link, font: NSFont.normal(FontSize.text)) - linkAttributed.detectLinks(type: [.Links, .Mentions, .Hashtags], account: account, openInfo: interface.openInfo) - - linkLayout = TextViewLayout(linkAttributed, maximumNumberOfLines: 1, truncationType: .end) - } - } else { - - var link:String = "" - let links = ObjcUtils.textCheckingResults(forText: message.text, highlightMentionsAndTags: false, highlightCommands: false) - if let links = links, !links.isEmpty { - let range = (links[0] as! NSValue).rangeValue - link = message.text.nsstring.substring(with: range) - - let attr = NSMutableAttributedString() - _ = attr.append(string: link, color: theme.colors.link, font: .normal(.text)) - attr.detectLinks(type: [.Links]) - - linkLayout = TextViewLayout(attr, maximumNumberOfLines: 1, truncationType: .end) + textLayout = TextViewLayout(attributedText, maximumNumberOfLines: 3, truncationType: .end) } - - var hostName: String = link - if let url = URL(string: link), let host = url.host, !host.isEmpty { + } else if let linkLayout = linkLayouts.first { + let attributed = linkLayout.attributedString + var hostName: String = attributed.string + if let url = URL(string: attributed.string), let host = url.host, !host.isEmpty { hostName = host firstCharacter = host.prefix(1) } else { - firstCharacter = link.prefix(1) + firstCharacter = "L" } let attributedText = NSMutableAttributedString() @@ -99,36 +122,109 @@ class PeerMediaWebpageRowItem: PeerMediaRowItem { if !hostName.isEmpty { let _ = attributedText.append(string: "\n") } - let _ = attributedText.append(string: message.text, color: theme.colors.text, font: NSFont.normal(.text)) - - textLayout = TextViewLayout(attributedText, maximumNumberOfLines: 6, truncationType: .end) + if message.text != linkLayout.attributedString.string { + let _ = attributedText.append(string: message.text, color: theme.colors.text, font: .normal(.short)) + } + textLayout = TextViewLayout(attributedText, maximumNumberOfLines: 3, truncationType: .end) } if icon == nil { - thumb = generateMediaEmptyLinkThumb(color: theme.colors.border, host: firstCharacter?.uppercased() ?? "H") + thumb = generateMediaEmptyLinkThumb(color: theme.colors.listBackground, textColor: theme.colors.listGrayText, host: firstCharacter?.uppercased() ?? "H") } textLayout?.interactions = globalLinkExecutor - linkLayout?.interactions = globalLinkExecutor + if message.stableId != UINT32_MAX { + textLayout?.interactions.menuItems = { [weak self] inside in + guard let `self` = self else {return .complete()} + return self.menuItems(in: NSZeroPoint) |> map { items in + var items = items + if let layout = self.textLayout, layout.selectedRange.hasSelectText { + let text = layout.attributedString.attributedSubstring(from: layout.selectedRange.range) + items.insert(ContextMenuItem(L10n.textCopy, handler: { + copyToClipboard(text.string) + }), at: 0) + items.insert(ContextSeparatorItem(), at: 1) + } + return items + } + } + } + + for linkLayout in linkLayouts { + linkLayout.interactions = TextViewInteractions(processURL: { [weak self] url in + if let webpage = self?.message.media.first as? TelegramMediaWebpage, let `self` = self { + if self.hasInstantPage { + showInstantPage(InstantPageViewController(self.interface.context, webPage: webpage, message: nil, saveToRecent: false)) + return + } + } + globalLinkExecutor.processURL(url) + }, copy: { [weak linkLayout] in + guard let linkLayout = linkLayout else {return false} + copyToClipboard(linkLayout.attributedString.string) + return false + }, localizeLinkCopy: { link in + return L10n.textContextCopyLink + }) + } + + self.linkLayouts = linkLayouts + _ = makeSize(initialSize.width, oldWidth: 0) + + } + + var hasInstantPage: Bool { + if let webpage = message.media.first as? TelegramMediaWebpage { + if case let .Loaded(content) = webpage.content { + if let instantPage = content.instantPage { + let hasInstantPage:()->Bool = { + if instantPage.blocks.count == 3 { + switch instantPage.blocks[2] { + case let .collage(_, caption), let .slideshow(_, caption): + return !attributedStringForRichText(caption.text, styleStack: InstantPageTextStyleStack()).string.isEmpty + default: + break + } + } + return true + } + + if content.websiteName?.lowercased() == "instagram" || content.websiteName?.lowercased() == "twitter" || content.websiteName?.lowercased() == "telegram" || content.type == "telegram_album" { + return false + } + return hasInstantPage() + } + } + } + return false + } + + var isArticle: Bool { + return message.stableId == UINT32_MAX } override func makeSize(_ width: CGFloat, oldWidth:CGFloat) -> Bool { - textLayout?.measure(width: width - contentInset.left - contentInset.right) - linkLayout?.measure(width: width - contentInset.left - contentInset.right) + let result = super.makeSize(width, oldWidth: oldWidth) + textLayout?.measure(width: self.blockWidth - contentInset.left - contentInset.right - self.viewType.innerInset.left - self.viewType.innerInset.right) + + for linkLayout in linkLayouts { + linkLayout.measure(width: self.blockWidth - contentInset.left - contentInset.right - self.viewType.innerInset.left - self.viewType.innerInset.right - (hasInstantPage ? 10 : 0)) + } var textSizes:CGFloat = 0 if let tLayout = textLayout { textSizes += tLayout.layoutSize.height } - if let lLayout = linkLayout { - textSizes += lLayout.layoutSize.height + for linkLayout in linkLayouts { + textSizes += linkLayout.layoutSize.height } - contentSize = NSMakeSize(width, max(textSizes + contentInset.top + contentInset.bottom + 2.0,60)) - return super.makeSize(width, oldWidth: oldWidth) + contentSize = NSMakeSize(width, max(textSizes + contentInset.top + contentInset.bottom + 2.0, 40)) + + return result } override func viewClass() -> AnyClass { @@ -141,48 +237,94 @@ class PeerMediaWebpageRowView : PeerMediaRowView { private var imageView:TransformImageView private var textView:TextView - private var linkView:TextView - + private var linkViews:[TextView] = [] + private var ivImage: ImageView? = nil required init(frame frameRect: NSRect) { - imageView = TransformImageView(frame:NSMakeRect(10, 5, 50.0, 50.0)) + imageView = TransformImageView(frame:NSMakeRect(0, 0, PeerMediaIconSize.width, PeerMediaIconSize.height)) textView = TextView() - linkView = TextView() super.init(frame: frameRect) - linkView.isSelectable = false addSubview(imageView) addSubview(textView) - addSubview(linkView) - } override func layout() { super.layout() if let item = item as? PeerMediaWebpageRowItem { - textView.update(item.textLayout, origin: NSMakePoint(item.contentInset.left,item.contentInset.top)) - linkView.isHidden = item.linkLayout == nil - linkView.update(item.linkLayout, origin: NSMakePoint(item.contentInset.left,textView.frame.maxY + 2.0)) + ivImage?.setFrameOrigin(item.contentInset.left, textView.frame.maxY + 6.0) + textView.setFrameOrigin(NSMakePoint(item.contentInset.left,item.contentInset.top)) + + var linkY: CGFloat = textView.frame.maxY + 2.0 + + for linkView in self.linkViews { + linkView.setFrameOrigin(NSMakePoint(item.contentInset.left + (item.hasInstantPage ? 10 : 0), linkY)) + linkY += linkView.frame.height + } + } } + override func mouseUp(with event: NSEvent) { + guard let item = item as? PeerMediaWebpageRowItem, item.isArticle else { + super.mouseUp(with: event) + return + } + // item.linkLayout?.interactions.processURL(event) + + } + override func set(item: TableRowItem, animated: Bool) { super.set(item: item,animated:animated) textView.backgroundColor = backdorColor - linkView.backgroundColor = backdorColor if let item = item as? PeerMediaWebpageRowItem { + textView.userInteractionEnabled = !item.isArticle + + textView.update(item.textLayout, origin: NSMakePoint(item.contentInset.left,item.contentInset.top)) + + + while self.linkViews.count > item.linkLayouts.count { + let last = self.linkViews.removeLast() + last.removeFromSuperview() + } + while self.linkViews.count < item.linkLayouts.count { + let new = TextView() + addSubview(new) + self.linkViews.append(new) + } + + var linkY: CGFloat = textView.frame.maxY + 2.0 + + for (i, linkView) in self.linkViews.enumerated() { + let linkLayout = item.linkLayouts[i] + linkView.backgroundColor = backdorColor + linkView.update(linkLayout, origin: NSMakePoint(item.contentInset.left + (item.hasInstantPage ? 10 : 0), linkY)) + linkY += linkLayout.layoutSize.height + } + + if item.hasInstantPage { + if ivImage == nil { + ivImage = ImageView() + } + ivImage!.image = theme.icons.chatInstantView + ivImage!.sizeToFit() + addSubview(ivImage!) + } else { + ivImage?.removeFromSuperview() + ivImage = nil + } - let updateIconImageSignal:Signal<(TransformImageArguments) -> DrawingContext?,NoError> + let updateIconImageSignal:Signal if let icon = item.icon { - updateIconImageSignal = chatWebpageSnippetPhoto(account: item.account, photo: icon, scale: backingScaleFactor, small:true) + updateIconImageSignal = chatWebpageSnippetPhoto(account: item.interface.context.account, imageReference: ImageMediaReference.message(message: MessageReference(item.message), media: icon), scale: backingScaleFactor, small:true) } else { - updateIconImageSignal = .single({_ in return nil}) + updateIconImageSignal = .single(ImageDataTransformation()) } if let arguments = item.iconArguments { imageView.set(arguments: arguments) - imageView.setSignal(account: item.account, signal: updateIconImageSignal) + imageView.setSignal( updateIconImageSignal) } if item.icon == nil { @@ -196,7 +338,9 @@ class PeerMediaWebpageRowView : PeerMediaRowView { override func updateSelectingMode(with selectingMode:Bool, animated:Bool = false) { super.updateSelectingMode(with: selectingMode, animated: animated) self.textView.isSelectable = !selectingMode - self.linkView.userInteractionEnabled = !selectingMode + for linkView in self.linkViews { + linkView.userInteractionEnabled = !selectingMode + } self.textView.userInteractionEnabled = !selectingMode } diff --git a/Telegram-Mac/PeerPhotos.swift b/Telegram-Mac/PeerPhotos.swift new file mode 100644 index 0000000000..ea669a4836 --- /dev/null +++ b/Telegram-Mac/PeerPhotos.swift @@ -0,0 +1,134 @@ +// +// PeerPhotos.swift +// Telegram +// +// Created by Mikhail Filimonov on 19/06/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi +import TelegramCore + + +private struct PeerPhotos { + let photos: [TelegramPeerPhoto] + let time: TimeInterval +} + +private var peerAvatars:Atomic<[PeerId: PeerPhotos]> = Atomic(value: [:]) + + +func syncPeerPhotos(peerId: PeerId) -> [TelegramPeerPhoto] { + return peerAvatars.with { $0[peerId].map { $0.photos } ?? [] } +} + +func peerPhotos(context: AccountContext, peerId: PeerId, force: Bool = false) -> Signal<[TelegramPeerPhoto], NoError> { + let photos = peerAvatars.with { $0[peerId] } + if let photos = photos, photos.time > Date().timeIntervalSince1970, !force { + return .single(photos.photos) + } else { + + return .single(peerAvatars.with { $0[peerId]?.photos } ?? []) |> then(combineLatest(context.engine.peers.requestPeerPhotos(peerId: peerId), context.account.postbox.peerView(id: peerId)) |> delay(0.4, queue: .concurrentDefaultQueue()) |> map { photos, peerView in + return peerAvatars.modify { value in + var value = value + var photos = photos + if let cachedData = peerView.cachedData as? CachedChannelData { + if let photo = cachedData.photo { + if photos.firstIndex(where: { $0.image.id == photo.id }) == nil { + photos.insert(TelegramPeerPhoto(image: photo, reference: nil, date: 0, index: 0, totalCount: photos.first?.totalCount ?? 0, messageId: nil), at: 0) + } + } + } + + value[peerId] = PeerPhotos(photos: photos, time: Date().timeIntervalSince1970 + 5 * 60) + return value + }[peerId]?.photos ?? [] + }) + } +} + + +func peerPhotosGalleryEntries(context: AccountContext, peerId: PeerId, firstStableId: AnyHashable) -> Signal<(entries: [GalleryEntry], selected:Int), NoError> { + return combineLatest(queue: prepareQueue, peerPhotos(context: context, peerId: peerId, force: true), context.account.postbox.loadedPeerWithId(peerId)) |> map { photos, peer in + + var entries: [GalleryEntry] = [] + + + var representations:[TelegramMediaImageRepresentation] = []//peer.profileImageRepresentations + if let representation = peer.smallProfileImage { + representations.append(representation) + } + if let representation = peer.largeProfileImage { + representations.append(representation) + } + + let videoRepresentations: [TelegramMediaImage.VideoRepresentation] = [] + + + var image:TelegramMediaImage? = nil + var msg: Message? = nil + if let base = firstStableId.base as? ChatHistoryEntryId, case let .message(message) = base { + let action = message.media.first as! TelegramMediaAction + switch action.action { + case let .photoUpdated(updated): + image = updated + msg = message + default: + break + } + } + + if image == nil { + image = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.CloudImage, id: 0), representations: representations, videoRepresentations: videoRepresentations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + } + + + let firstEntry: GalleryEntry = .photo(index: 0, stableId: firstStableId, photo: image!, reference: nil, peer: peer, message: msg, date: 0) + + var foundIndex: Bool = peerId.namespace == Namespaces.Peer.CloudUser && !photos.isEmpty + var currentIndex: Int = 0 + var foundMessage: Message? = nil + var photosDate:[TimeInterval] = [] + for i in 0 ..< photos.count { + let photo = photos[i] + photosDate.append(TimeInterval(photo.date)) + if let base = firstStableId.base as? ChatHistoryEntryId, case let .message(message) = base { + let action = message.media.first as! TelegramMediaAction + switch action.action { + case let .photoUpdated(updated): + if photo.image.id == updated?.id { + currentIndex = i + foundIndex = true + foundMessage = message + } + default: + break + } + } else if i == 0 { + foundIndex = true + currentIndex = i + + } + } + for i in 0 ..< photos.count { + if currentIndex == i && foundIndex { + let image = TelegramMediaImage(imageId: photos[i].image.imageId, representations: image!.representations, videoRepresentations: photos[i].image.videoRepresentations, immediateThumbnailData: photos[i].image.immediateThumbnailData, reference: photos[i].image.reference, partialReference: photos[i].image.partialReference, flags: photos[i].image.flags) + + entries.append(.photo(index: photos[i].index, stableId: firstStableId, photo: image, reference: photos[i].reference, peer: peer, message: foundMessage, date: photosDate[i])) + } else { + entries.append(.photo(index: photos[i].index, stableId: photos[i].image.imageId, photo: photos[i].image, reference: photos[i].reference, peer: peer, message: nil, date: photosDate[i])) + } + } + + if !foundIndex && entries.isEmpty { + entries.append(firstEntry) + } + + return (entries: entries, selected: currentIndex) + + } +} diff --git a/Telegram-Mac/PeerPhotosMonthItem.swift b/Telegram-Mac/PeerPhotosMonthItem.swift new file mode 100644 index 0000000000..a1a1c3927d --- /dev/null +++ b/Telegram-Mac/PeerPhotosMonthItem.swift @@ -0,0 +1,986 @@ +// +// PeerPhotosMonthItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 17.10.2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import TGUIKit +import Postbox +import SwiftSignalKit + +private struct LayoutItem : Equatable { + static func == (lhs: LayoutItem, rhs: LayoutItem) -> Bool { + return lhs.message == rhs.message && lhs.corners == rhs.corners && lhs.frame == rhs.frame + } + + let message: Message + let frame: NSRect + let viewType:MediaCell.Type + let corners:ImageCorners + let chatInteraction: ChatInteraction +} + +class PeerPhotosMonthItem: GeneralRowItem { + private let items:[Message] + fileprivate let context: AccountContext + private var contentHeight: CGFloat = 0 + + fileprivate private(set) var layoutItems:[LayoutItem] = [] + fileprivate private(set) var itemSize: NSSize = NSZeroSize + fileprivate let chatInteraction: ChatInteraction + fileprivate let gallerySupplyment: InteractionContentViewProtocol + fileprivate let galleryType: GalleryAppearType + init(_ initialSize: NSSize, stableId: AnyHashable, viewType: GeneralViewType, context: AccountContext, chatInteraction: ChatInteraction, gallerySupplyment: InteractionContentViewProtocol, items: [Message], galleryType: GalleryAppearType) { + self.items = items + self.context = context + self.gallerySupplyment = gallerySupplyment + self.chatInteraction = chatInteraction + self.galleryType = galleryType + + super.init(initialSize, stableId: stableId, viewType: viewType, inset: NSEdgeInsets()) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + + + if !items.isEmpty { + var t: time_t = time_t(TimeInterval(items[0].timestamp)) + var timeinfo: tm = tm() + localtime_r(&t, &timeinfo) + + if timeinfo.tm_mon == 2 { + var bp:Int = 0 + bp += 1 + } + + } + + var rowCount:Int = 4 + var perWidth: CGFloat = 0 + while true { + let maximum = self.blockWidth - self.viewType.innerInset.left - self.viewType.innerInset.right - CGFloat(rowCount * 2) + perWidth = maximum / CGFloat(rowCount) + if perWidth >= 90 { + break + } else { + rowCount -= 1 + } + } + assert(rowCount >= 1) + + let itemSize = NSMakeSize(ceil(perWidth) + 2, ceil(perWidth) + 2) + + layoutItems.removeAll() + var point: CGPoint = CGPoint(x: self.viewType.innerInset.left, y: self.viewType.innerInset.top + itemSize.height) + for (i, message) in self.items.enumerated() { + let viewType: MediaCell.Type + if let file = message.media.first as? TelegramMediaFile { + if file.isAnimated && file.isVideo { + viewType = MediaGifCell.self + } else { + viewType = MediaVideoCell.self + } + } else { + viewType = MediaPhotoCell.self + } + + var topLeft: ImageCorner = .Corner(0) + var topRight: ImageCorner = .Corner(0) + var bottomLeft: ImageCorner = .Corner(0) + var bottomRight: ImageCorner = .Corner(0) + + if self.items.count < rowCount { + if message == self.items.first { + if self.viewType.position != .last { + topLeft = .Corner(.cornerRadius) + } + bottomLeft = .Corner(.cornerRadius) + } + } else if self.items.count == rowCount { + if message == self.items.first { + if self.viewType.position != .last { + topLeft = .Corner(.cornerRadius) + } + bottomLeft = .Corner(.cornerRadius) + } else if message == self.items.last { + if message == self.items.last { + if self.viewType.position != .last { + topRight = .Corner(.cornerRadius) + } + bottomRight = .Corner(.cornerRadius) + } + } + } else { + let i = i + 1 + let firstLine = i <= rowCount + let div = (items.count % rowCount) == 0 ? rowCount : (items.count % rowCount) + let lastLine = i > (items.count - div) + + if firstLine { + if self.viewType.position != .last { + if i % rowCount == 1 { + topLeft = .Corner(.cornerRadius) + } else if i % rowCount == 0 { + topRight = .Corner(.cornerRadius) + } + } + } else if lastLine { + if i % rowCount == 1 { + bottomLeft = .Corner(.cornerRadius) + } else if i % rowCount == 0 { + bottomRight = .Corner(.cornerRadius) + } + } + } + + + let corners = ImageCorners(topLeft: topLeft, topRight: topRight, bottomLeft: bottomLeft, bottomRight: bottomRight) + self.layoutItems.append(LayoutItem(message: message, frame: CGRect(origin: point.offsetBy(dx: 0, dy: -itemSize.height), size: itemSize), viewType: viewType, corners: corners, chatInteraction: self.chatInteraction)) + point.x += itemSize.width + if self.layoutItems.count % rowCount == 0, message != self.items.last { + point.y += itemSize.height + point.x = self.viewType.innerInset.left + } + } + self.itemSize = itemSize + self.contentHeight = point.y - self.viewType.innerInset.top + return true + } + + func contains(_ id: MessageId) -> Bool { + return layoutItems.contains(where: { $0.message.id == id}) + } + + override var height: CGFloat { + return self.contentHeight + self.viewType.innerInset.top + self.viewType.innerInset.bottom + } + + override var instantlyResize: Bool { + return true + } + + deinit { + + } + + override func viewClass() -> AnyClass { + return PeerPhotosMonthView.self + } + + override func menuItems(in location: NSPoint) -> Signal<[ContextMenuItem], NoError> { + var items:[ContextMenuItem] = [] + let layoutItem = layoutItems.first(where: { NSPointInRect(location, $0.frame) }) + if let layoutItem = layoutItem { + let message = layoutItem.message + if canForwardMessage(message, chatInteraction: chatInteraction) { + items.append(ContextMenuItem(L10n.messageContextForward, handler: { [weak self] in + self?.chatInteraction.forwardMessages([message.id]) + })) + } + if canDeleteMessage(message, account: context.account, mode: .history) { + items.append(ContextMenuItem(L10n.messageContextDelete, handler: { [weak self] in + self?.chatInteraction.deleteMessages([message.id]) + })) + } + items.append(ContextMenuItem(L10n.messageContextGoto, handler: { [weak self] in + self?.chatInteraction.focusMessageId(nil, message.id, .center(id: 0, innerId: nil, animated: false, focus: .init(focus: true), inset: 0)) + })) + } + return .single(items) + } +} + +private class MediaCell : Control { + private var selectionView:SelectingControl? + fileprivate let imageView: TransformImageView + private(set) var layoutItem: LayoutItem? + fileprivate var context: AccountContext? + required init(frame frameRect: NSRect) { + imageView = TransformImageView(frame: NSMakeRect(1, 1, frameRect.width, frameRect.height)) + super.init(frame: frameRect) + addSubview(imageView) + userInteractionEnabled = false + } + + override func mouseMoved(with event: NSEvent) { + superview?.superview?.mouseMoved(with: event) + } + override func mouseEntered(with event: NSEvent) { + superview?.superview?.mouseEntered(with: event) + } + override func mouseExited(with event: NSEvent) { + superview?.superview?.mouseExited(with: event) + } + func update(layout: LayoutItem, context: AccountContext, table: TableView?) { + let previousLayout = self.layoutItem + self.layoutItem = layout + self.context = context + if previousLayout != layout, !(self is MediaGifCell) { + let media: Media + let imageSize: NSSize + let arguments: TransformImageArguments + let cacheArguments: TransformImageArguments + let signal: Signal + if let image = layout.message.media.first as? TelegramMediaImage, let largestSize = largestImageRepresentation(image.representations)?.dimensions.size { + media = image + imageSize = largestSize.aspectFilled(NSMakeSize(150, 150)) + arguments = TransformImageArguments(corners: layout.corners, imageSize: imageSize, boundingSize: layout.frame.size, intrinsicInsets: NSEdgeInsets()) + cacheArguments = TransformImageArguments(corners: layout.corners, imageSize: imageSize, boundingSize: NSMakeSize(150, 150), intrinsicInsets: NSEdgeInsets()) + signal = mediaGridMessagePhoto(account: context.account, imageReference: ImageMediaReference.message(message: MessageReference(layout.message), media: image), scale: backingScaleFactor) + } else if let file = layout.message.media.first as? TelegramMediaFile { + media = file + let largestSize = file.previewRepresentations.last?.dimensions.size ?? file.imageSize + imageSize = largestSize.aspectFilled(NSMakeSize(150, 150)) + arguments = TransformImageArguments(corners: layout.corners, imageSize: imageSize, boundingSize: layout.frame.size, intrinsicInsets: NSEdgeInsets()) + cacheArguments = TransformImageArguments(corners: layout.corners, imageSize: imageSize, boundingSize: NSMakeSize(150, 150), intrinsicInsets: NSEdgeInsets()) + signal = chatMessageVideo(postbox: context.account.postbox, fileReference: FileMediaReference.message(message: MessageReference(layout.message), media: file), scale: backingScaleFactor) //mediaGridMessageVideo(postbox: context.account.postbox, fileReference: FileMediaReference.message(message: MessageReference(layout.message), media: file), scale: backingScaleFactor) + } else { + return + } + + self.imageView.set(arguments: arguments) + self.imageView.setSignal(signal: cachedMedia(media: media, arguments: cacheArguments, scale: backingScaleFactor), clearInstantly: true) + if !self.imageView.isFullyLoaded { + self.imageView.setSignal(signal, animate: true, cacheImage: { [weak media] result in + if let media = media { + cacheMedia(result, media: media, arguments: cacheArguments, scale: System.backingScale) + } + }) + } + } + updateSelectionState(animated: false) + } + + override func copy() -> Any { + return imageView.copy() + } + + func innerAction() -> InvokeActionResult { + return .gallery + } + + func addAccesoryOnCopiedView(view: NSView) { + + } + + func updateMouse(_ inside: Bool) { + + } + + func updateSelectionState(animated: Bool) { + if let layoutItem = layoutItem { + if let selectionState = layoutItem.chatInteraction.presentation.selectionState { + let selected = selectionState.selectedIds.contains(layoutItem.message.id) + if let selectionView = self.selectionView { + selectionView.set(selected: selected, animated: animated) + } else { + selectionView = SelectingControl(unselectedImage: theme.icons.chatGroupToggleUnselected, selectedImage: theme.icons.chatGroupToggleSelected) + + addSubview(selectionView!) + selectionView?.set(selected: selected, animated: animated) + if animated { + selectionView?.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + selectionView?.layer?.animateScaleCenter(from: 0.5, to: 1.0, duration: 0.2) + } + } + } else { + if let selectionView = selectionView { + self.selectionView = nil + if animated { + selectionView.layer?.animateScaleCenter(from: 1.0, to: 0.5, duration: 0.2) + selectionView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak selectionView] completion in + selectionView?.removeFromSuperview() + }) + } else { + selectionView.removeFromSuperview() + } + } + } + needsLayout = true + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + imageView.frame = NSMakeRect(1, 1, frame.width - 2, frame.height - 2) + + if let selectionView = selectionView { + selectionView.setFrameOrigin(frame.width - selectionView.frame.width - 5, 5) + } + } +} + +private final class MediaPhotoCell : MediaCell { + +} + +private enum InvokeActionResult { + case nothing + case gallery +} + + + +private final class MediaVideoCell : MediaCell { + + + private final class VideoAutoplayView { + let mediaPlayer: MediaPlayer + let view: MediaPlayerView + + fileprivate var playTimer: SwiftSignalKit.Timer? + var status: MediaPlayerStatus? + + init(mediaPlayer: MediaPlayer, view: MediaPlayerView) { + self.mediaPlayer = mediaPlayer + self.view = view + mediaPlayer.actionAtEnd = .loop(nil) + } + + deinit { + view.removeFromSuperview() + playTimer?.invalidate() + } + } + + private let mediaPlayerStatusDisposable = MetaDisposable() + + private let progressView:RadialProgressView = RadialProgressView(theme: RadialProgressTheme(backgroundColor: .blackTransparent, foregroundColor: .white, icon: playerPlayThumb)) + private let videoAccessory: ChatMessageAccessoryView = ChatMessageAccessoryView(frame: NSZeroRect) + private var status:MediaResourceStatus? + private var authenticStatus: MediaResourceStatus? + private let statusDisposable = MetaDisposable() + private let fetchingDisposable = MetaDisposable() + private let partDisposable = MetaDisposable() + + private var videoView:VideoAutoplayView? + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + self.addSubview(self.videoAccessory) + self.progressView.userInteractionEnabled = false + self.addSubview(self.progressView) + } + + override func updateMouse(_ inside: Bool) { + if let layout = self.layoutItem { + let file = layout.message.media.first as! TelegramMediaFile + if inside { + if file.isStreamable { + if videoView == nil { + let context = layout.chatInteraction.context + let player = MediaPlayer(postbox: context.account.postbox, reference: MediaResourceReference.media(media: AnyMediaReference.message(message: MessageReference(layout.message), media: file), resource: file.resource), streamable: true, video: true, preferSoftwareDecoding: true, enableSound: false, fetchAutomatically: false) + videoView = MediaVideoCell.VideoAutoplayView(mediaPlayer: player, view: MediaPlayerView(backgroundThread: true)) + + videoView?.view.setVideoLayerGravity(.resizeAspectFill) + + var posititionFlags: LayoutPositionFlags = [] + if layout.corners.topLeft.corner > 0 { + posititionFlags.insert(.top) + posititionFlags.insert(.left) + } + if layout.corners.topRight.corner > 0 { + posititionFlags.insert(.top) + posititionFlags.insert(.right) + } + if layout.corners.bottomLeft.corner > 0 { + posititionFlags.insert(.bottom) + posititionFlags.insert(.left) + } + if layout.corners.bottomRight.corner > 0 { + posititionFlags.insert(.bottom) + posititionFlags.insert(.right) + } + videoView?.view.positionFlags = posititionFlags.isEmpty ? nil : posititionFlags + videoView?.view.frame = self.imageView.frame + + videoView!.mediaPlayer.attachPlayerView(videoView!.view) + + videoView?.mediaPlayer.play() + + + self.addSubview(videoView!.view, positioned: .above, relativeTo: self.imageView) + + progressView.change(opacity: 0) + } + if let videoView = videoView { + mediaPlayerStatusDisposable.set((videoView.mediaPlayer.status |> deliverOnMainQueue).start(next: { [weak self] status in + self?.updateMediaStatus(status, animated: true) + })) + } + + + } else { + progressView.change(opacity: 1) + videoView = nil + mediaPlayerStatusDisposable.set(nil) + updateVideoAccessory(self.authenticStatus ?? .Remote, mediaPlayerStatus: nil, file: file, animated: true) + } + } else { + progressView.change(opacity: 1) + videoView = nil + mediaPlayerStatusDisposable.set(nil) + updateVideoAccessory(self.authenticStatus ?? .Remote, mediaPlayerStatus: nil, file: file, animated: true) + } + } + } + + private func updateMediaStatus(_ status: MediaPlayerStatus, animated: Bool = false) { + if let videoView = videoView, let media = self.layoutItem?.message.media.first as? TelegramMediaFile { + videoView.status = status + updateVideoAccessory(self.authenticStatus ?? .Local, mediaPlayerStatus: status, file: media, animated: animated) + + switch status.status { + case .playing: + videoView.playTimer?.invalidate() + videoView.playTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in + self?.updateVideoAccessory(self?.authenticStatus ?? .Local, mediaPlayerStatus: status, file: media, animated: animated) + }, queue: .mainQueue()) + + videoView.playTimer?.start() + default: + videoView.playTimer?.invalidate() + } + + + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func fetch() { + if let context = context, let layoutItem = self.layoutItem { + let file = layoutItem.message.media.first as! TelegramMediaFile + fetchingDisposable.set(messageMediaFileInteractiveFetched(context: context, messageId: layoutItem.message.id, fileReference: FileMediaReference.message(message: MessageReference(layoutItem.message), media: file)).start()) + } + } + + private func cancelFetching() { + if let context = context, let layoutItem = self.layoutItem { + let file = layoutItem.message.media.first as! TelegramMediaFile + messageMediaFileCancelInteractiveFetch(context: context, messageId: layoutItem.message.id, fileReference: FileMediaReference.message(message: MessageReference(layoutItem.message), media: file)) + } + } + + override func innerAction() -> InvokeActionResult { + if let file = layoutItem?.message.media.first as? TelegramMediaFile, let window = self.window { + switch progressView.state { + case .Fetching: + if NSPointInRect(self.convert(window.mouseLocationOutsideOfEventStream, from: nil), progressView.frame) { + cancelFetching() + } else if file.isStreamable { + return .gallery + } + case .Remote: + fetch() + default: + return .gallery + } + } + return .nothing + } + + func preloadStreamblePart() { + if let layoutItem = self.layoutItem { + let context = layoutItem.chatInteraction.context + if context.autoplayMedia.preloadVideos { + if let media = layoutItem.message.media.first as? TelegramMediaFile, let fileSize = media.size { + let reference = FileMediaReference.message(message: MessageReference(layoutItem.message), media: media) + let preload = combineLatest(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: reference.resourceReference(media.resource), range: (0 ..< Int(2.0 * 1024 * 1024), .default), statsCategory: .video), fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: reference.resourceReference(media.resource), range: (max(0, fileSize - Int(256 * 1024)) ..< Int(Int32.max), .default), statsCategory: .video)) + partDisposable.set(preload.start()) + } + } + } + } + + private func updateVideoAccessory(_ status: MediaResourceStatus, mediaPlayerStatus: MediaPlayerStatus? = nil, file: TelegramMediaFile, animated: Bool) { + let maxWidth = frame.width - 10 + let text: String + + let status: MediaResourceStatus = .Local + + + if let status = mediaPlayerStatus, status.generationTimestamp > 0, status.duration > 0 { + text = String.durationTransformed(elapsed: Int(status.duration - (status.timestamp + (CACurrentMediaTime() - status.generationTimestamp)))) + } else { + text = String.durationTransformed(elapsed: file.videoDuration) + } + + var isBuffering: Bool = false + if let fetchStatus = self.authenticStatus, let status = mediaPlayerStatus { + switch status.status { + case .buffering: + switch fetchStatus { + case .Local: + break + default: + isBuffering = true + } + default: + break + } + + } + + videoAccessory.updateText(text, maxWidth: maxWidth, status: status, isStreamable: file.isStreamable, isCompact: true, isBuffering: isBuffering, animated: animated, fetch: { [weak self] in + self?.fetch() + }, cancelFetch: { [weak self] in + self?.cancelFetching() + }) + needsLayout = true + } + + override func update(layout: LayoutItem, context: AccountContext, table: TableView?) { + super.update(layout: layout, context: context, table: table) + let file = layout.message.media.first as! TelegramMediaFile + + let updatedStatusSignal = chatMessageFileStatus(account: context.account, file: file) |> deliverOnMainQueue |> map { status -> (MediaResourceStatus, MediaResourceStatus) in + if file.isStreamable && layout.message.id.peerId.namespace != Namespaces.Peer.SecretChat { + return (.Local, status) + } + return (status, status) + } |> deliverOnMainQueue + + var first: Bool = true + + statusDisposable.set(updatedStatusSignal.start(next: { [weak self] status, authentic in + guard let `self` = self else {return} + + self.updateVideoAccessory(authentic, mediaPlayerStatus: self.videoView?.status, file: file, animated: !first) + first = false + self.status = status + self.authenticStatus = authentic + let progressStatus: MediaResourceStatus + switch authentic { + case .Fetching: + progressStatus = authentic + default: + progressStatus = status + } + switch progressStatus { + case let .Fetching(_, progress): + self.progressView.state = .Fetching(progress: progress, force: false) + case .Remote: + self.progressView.state = .Remote + case .Local: + self.progressView.state = .Play + } + })) + partDisposable.set(nil) + self.preloadStreamblePart() + } + + override func addAccesoryOnCopiedView(view: NSView) { + let videoAccessory = self.videoAccessory.copy() as! ChatMessageAccessoryView + if visibleRect.minY < videoAccessory.frame.midY && visibleRect.minY + visibleRect.height > videoAccessory.frame.midY { + videoAccessory.frame.origin.y = frame.height - videoAccessory.frame.maxY + view.addSubview(videoAccessory) + } + + let pView = RadialProgressView(theme: progressView.theme, twist: true) + pView.state = progressView.state + pView.frame = progressView.frame + if visibleRect.minY < progressView.frame.midY && visibleRect.minY + visibleRect.height > progressView.frame.midY { + pView.frame.origin.y = frame.height - progressView.frame.maxY + view.addSubview(pView) + } + } + + override func layout() { + super.layout() + progressView.center() + videoAccessory.setFrameOrigin(5, 5) + videoView?.view.frame = self.imageView.frame + } + + deinit { + statusDisposable.dispose() + fetchingDisposable.dispose() + partDisposable.dispose() + mediaPlayerStatusDisposable.dispose() + } +} + + + +private final class MediaGifCell : MediaCell { + private let gifView: GIFContainerView = GIFContainerView(frame: .zero) + private var status:MediaResourceStatus? + private let statusDisposable = MetaDisposable() + private let fetchingDisposable = MetaDisposable() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + self.addSubview(self.gifView) + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func fetch() { + + } + + private func cancelFetching() { + + } + + override func innerAction() -> InvokeActionResult { + return .gallery + } + + + override func copy() -> Any { + return gifView.copy() + } + + override func update(layout: LayoutItem, context: AccountContext, table: TableView?) { + let previousLayout = self.layoutItem + super.update(layout: layout, context: context, table: table) + if layout != previousLayout { + let file = layout.message.media.first as! TelegramMediaFile + + let messageRefence = MessageReference(layout.message) + + let reference = FileMediaReference.message(message: messageRefence, media: file) + + var effectiveFile = reference + if let preview = file.videoThumbnails.first { + let updated = file.withUpdatedResource(preview.resource) + effectiveFile = FileMediaReference.message(message: messageRefence, media: updated) + } + let signal = chatMessageVideo(postbox: context.account.postbox, fileReference: effectiveFile, scale: backingScaleFactor) + + + gifView.update(with: reference, size: frame.size, viewSize: frame.size, context: context, table: nil, iconSignal: signal) + gifView.userInteractionEnabled = false + + } + + + } + + + override func layout() { + super.layout() + gifView.frame = NSMakeRect(1, 1, frame.width - 2, frame.height - 2) + + } + + deinit { + statusDisposable.dispose() + fetchingDisposable.dispose() + } +} + + +private final class PeerPhotosMonthView : TableRowView, Notifable { + private let containerView = GeneralRowContainerView(frame: NSZeroRect) + private var contentViews:[Optional] = [] { + didSet { + var bp:Int = 0 + bp == 1 + } + } + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + self.addSubview(self.containerView) + + containerView.set(handler: { [weak self] _ in + self?.action(event: .Down) + }, for: .Down) + + containerView.set(handler: { [weak self] _ in + self?.action(event: .MouseDragging) + }, for: .MouseDragging) + + containerView.set(handler: { [weak self] _ in + self?.action(event: .Click) + }, for: .Click) + } + + private var haveToSelectOnDrag: Bool = false + + + private weak var currentMouseCell: MediaCell? + + @objc override func updateMouse() { + super.updateMouse() + guard let window = self.window else { + return + } + let point = self.containerView.convert(window.mouseLocationOutsideOfEventStream, from: nil) + let mediaCell = self.contentViews.first(where: { + return $0 != nil && NSPointInRect(point, $0!.frame) + })?.map { $0 } + + if currentMouseCell != mediaCell { + currentMouseCell?.updateMouse(false) + } + currentMouseCell = mediaCell + mediaCell?.updateMouse(window.isKeyWindow) + + } + + + override func mouseEntered(with event: NSEvent) { + super.mouseEntered(with: event) + updateMouse() + } + override func mouseExited(with event: NSEvent) { + super.mouseExited(with: event) + updateMouse() + } + override func mouseMoved(with event: NSEvent) { + super.mouseMoved(with: event) + updateMouse() + } + + private func action(event: ControlEvent) { + guard let item = self.item as? PeerPhotosMonthItem, let window = window else { + return + } + let point = containerView.convert(window.mouseLocationOutsideOfEventStream, from: nil) + if let layoutItem = item.layoutItems.first(where: { NSPointInRect(point, $0.frame) }) { + if layoutItem.chatInteraction.presentation.state == .selecting { + switch event { + case .MouseDragging: + layoutItem.chatInteraction.update { current in + if !haveToSelectOnDrag { + return current.withRemovedSelectedMessage(layoutItem.message.id) + } else { + return current.withUpdatedSelectedMessage(layoutItem.message.id) + } + } + case .Down: + layoutItem.chatInteraction.update { $0.withToggledSelectedMessage(layoutItem.message.id) } + haveToSelectOnDrag = layoutItem.chatInteraction.presentation.isSelectedMessageId(layoutItem.message.id) + default: + break + } + } else { + switch event { + case .Click: + let view = self.contentViews.compactMap { $0 }.first(where: { $0.layoutItem == layoutItem }) + if let view = view { + switch view.innerAction() { + case .gallery: + showChatGallery(context: item.context, message: layoutItem.message, item.gallerySupplyment, ChatMediaGalleryParameters(showMedia: { _ in}, showMessage: { message in + layoutItem.chatInteraction.focusMessageId(nil, message.id, .center(id: 0, innerId: nil, animated: false, focus: .init(focus: true), inset: 0)) + }, isWebpage: false, media: layoutItem.message.media.first!, automaticDownload: true), type: item.galleryType, reversed: true) + case .nothing: + break + } + } + default: + break + } + } + } + } + + func notify(with value: Any, oldValue:Any, animated:Bool) { + if let value = value as? ChatPresentationInterfaceState, let oldValue = oldValue as? ChatPresentationInterfaceState { + let views = contentViews.compactMap { $0 } + for view in views { + if let item = view.layoutItem { + if (value.state == .selecting) != (oldValue.state == .selecting) || value.isSelectedMessageId(item.message.id) != oldValue.isSelectedMessageId(item.message.id) { + view.updateSelectionState(animated: animated) + } + } + } + } + } + + func isEqual(to other: Notifable) -> Bool { + if let other = other as? PeerPhotosMonthView { + return other == self + } + return false + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var backdorColor: NSColor { + return theme.colors.background + } + + override func updateColors() { + guard let item = item as? PeerPhotosMonthItem else { + return + } + self.backgroundColor = item.viewType.rowBackground + containerView.set(background: self.backdorColor, for: .Normal) + } + + override func viewDidMoveToSuperview() { + super.viewDidMoveToSuperview() + updateVisibleItems() + + if let item = self.item as? PeerPhotosMonthItem { + if superview == nil { + item.chatInteraction.remove(observer: self) + } else { + item.chatInteraction.add(observer: self) + } + } + } + + @objc private func updateVisibleItems() { + layoutVisibleItems(animated: false) + } + + private var previousRange: (Int, Int) = (0, 0) + private var isCleaned: Bool = false + + private func layoutVisibleItems(animated: Bool) { + guard let item = item as? PeerPhotosMonthItem else { + return + } + let visibleRect = NSMakeRect(0, self.visibleRect.minY - item.itemSize.height, self.visibleRect.width, self.visibleRect.height + item.itemSize.height * 2) + let size = item.itemSize + + if self.visibleRect != NSZeroRect && superview != nil && window != nil { + let visibleRange = (Int(ceil(visibleRect.minY / (size.height))), Int(ceil(visibleRect.height / (size.height)))) + if visibleRange != self.previousRange { + self.previousRange = visibleRange + isCleaned = false + } else { + return + } + } else { + self.previousRange = (0, 0) + CATransaction.begin() + if !isCleaned { + for (i, view) in self.contentViews.enumerated() { + view?.removeFromSuperview() + self.contentViews[i] = nil + } + } + isCleaned = true + CATransaction.commit() + return + } + + + CATransaction.begin() + + var unused:[MediaCell] = [] + for (i, layout) in item.layoutItems.enumerated() { + if NSPointInRect(layout.frame.origin, visibleRect) { + var view: MediaCell + if self.contentViews[i] == nil || !self.contentViews[i]!.isKind(of: layout.viewType) { + view = layout.viewType.init(frame: layout.frame) + self.contentViews[i] = view + } else { + view = self.contentViews[i]! + } + if view.layoutItem != layout { + view.update(layout: layout, context: item.context, table: item.table) + } + + view.frame = layout.frame + } else { + if let view = self.contentViews[i] { + unused.append(view) + self.contentViews[i] = nil + } + } + } + + for view in unused { + view.removeFromSuperview() + } + + containerView.subviews = self.contentViews.compactMap { $0 } + + CATransaction.commit() + + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + override func viewDidMoveToWindow() { + if window == nil { + NotificationCenter.default.removeObserver(self) + } else { + NotificationCenter.default.addObserver(self, selector: #selector(updateVisibleItems), name: NSView.boundsDidChangeNotification, object: self.enclosingScrollView?.contentView) + NotificationCenter.default.addObserver(self, selector: #selector(updateMouse), name: NSWindow.didBecomeKeyNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(updateMouse), name: NSWindow.didResignKeyNotification, object: nil) + } + updateVisibleItems() + } + + override func layout() { + super.layout() + guard let item = item as? PeerPhotosMonthItem else { + return + } + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) + self.containerView.setCorners(item.viewType.corners) + + updateVisibleItems() + } + + override func interactionContentView(for innerId: AnyHashable, animateIn: Bool) -> NSView { + if let innerId = innerId.base as? MessageId { + let view = contentViews.compactMap { $0 }.first(where: { $0.layoutItem?.message.id == innerId }) + return view ?? NSView() + } + return self + } + + override func addAccesoryOnCopiedView(innerId: AnyHashable, view: NSView) { + if let innerId = innerId.base as? MessageId { + let cell = contentViews.compactMap { $0 }.first(where: { $0.layoutItem?.message.id == innerId }) + cell?.addAccesoryOnCopiedView(view: view) + } + } + + override func convertWindowPointToContent(_ point: NSPoint) -> NSPoint { + return containerView.convert(point, from: nil) + } + + override func set(item: TableRowItem, animated: Bool = false) { + + super.set(item: item, animated: animated) + + guard let item = item as? PeerPhotosMonthItem else { + return + } + + item.chatInteraction.add(observer: self) + + self.previousRange = (0, 0) + + while self.contentViews.count > item.layoutItems.count { + self.contentViews.removeLast() + } + while self.contentViews.count < item.layoutItems.count { + self.contentViews.append(nil) + } + + + layoutVisibleItems(animated: animated) + } +} + diff --git a/Telegram-Mac/PeerPresenceStatusManager.swift b/Telegram-Mac/PeerPresenceStatusManager.swift index cf75b3e444..c8dc1378ed 100644 --- a/Telegram-Mac/PeerPresenceStatusManager.swift +++ b/Telegram-Mac/PeerPresenceStatusManager.swift @@ -8,12 +8,13 @@ import Cocoa -import SwiftSignalKitMac -import TelegramCoreMac +import SwiftSignalKit +import TelegramCore + final class PeerPresenceStatusManager { private let update: () -> Void - private var timer: SwiftSignalKitMac.Timer? + private var timer: SwiftSignalKit.Timer? init(update: @escaping () -> Void) { self.update = update @@ -23,14 +24,14 @@ final class PeerPresenceStatusManager { self.timer?.invalidate() } - func reset(presence: TelegramUserPresence) { + func reset(presence: TelegramUserPresence, timeDifference: Int32) { timer?.invalidate() timer = nil let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - let timeout = userPresenceStringRefreshTimeout(presence, relativeTo: Int32(timestamp)) + let timeout = userPresenceStringRefreshTimeout(presence, timeDifference: timeDifference, relativeTo: Int32(timestamp)) if timeout.isFinite { - self.timer = SwiftSignalKitMac.Timer(timeout: timeout, repeat: false, completion: { [weak self] in + self.timer = SwiftSignalKit.Timer(timeout: timeout, repeat: false, completion: { [weak self] in if let strongSelf = self { strongSelf.update() } diff --git a/Telegram-Mac/PeerUtils.swift b/Telegram-Mac/PeerUtils.swift new file mode 100644 index 0000000000..64209f3bb6 --- /dev/null +++ b/Telegram-Mac/PeerUtils.swift @@ -0,0 +1,324 @@ +// +// PeerUtils.swift +// Telegram +// +// Created by Mikhail Filimonov on 07/03/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import Postbox + + +let prod_repliesPeerId: PeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1271266957)) +let test_repliesPeerId: PeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(708513)) + + +var repliesPeerId: PeerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1271266957)) + + +extension ChatListFilterPeerCategories { + + static let excludeRead = ChatListFilterPeerCategories(rawValue: 1 << 6) + static let excludeMuted = ChatListFilterPeerCategories(rawValue: 1 << 7) + static let excludeArchived = ChatListFilterPeerCategories(rawValue: 1 << 8) + + static let Namespace: Int32 = 7 +} + + +final class TelegramFilterCategory : Peer { + + + + var id: PeerId + + var indexName: PeerIndexNameRepresentation + + var associatedPeerId: PeerId? + + var notificationSettingsPeerId: PeerId? + + func isEqual(_ other: Peer) -> Bool { + if let other = other as? TelegramFilterCategory { + return other.category == self.category + } + return false + } + + let category: ChatListFilterPeerCategories + + init(category: ChatListFilterPeerCategories) { + self.id = PeerId(namespace: Namespaces.Peer.Empty, id: PeerId.Id._internalFromInt64Value(Int64(category.rawValue))) + self.indexName = .title(title: "", addressName: "") + self.notificationSettingsPeerId = nil + self.category = category + } + + var displayTitle: String? { + if category == .contacts { + return L10n.chatListFilterContacts + } + if category == .nonContacts { + return L10n.chatListFilterNonContacts + } + if category == .groups { + return L10n.chatListFilterGroups + } + if category == .channels { + return L10n.chatListFilterChannels + } + if category == .bots { + return L10n.chatListFilterBots + } + if category == .excludeRead { + return L10n.chatListFilterReadChats + } + if category == .excludeMuted { + return L10n.chatListFilterMutedChats + } + if category == .excludeArchived { + return L10n.chatListFilterArchive + } + return nil + } + + var icon: EmptyAvatartType? { + if category == .contacts { + return .icon(colors: theme.colors.peerColors(5), icon: theme.icons.chat_filter_private_chats_avatar, iconSize: NSMakeSize(24, 24), cornerRadius: nil) + } + if category == .nonContacts { + return .icon(colors: theme.colors.peerColors(1), icon: theme.icons.chat_filter_non_contacts_avatar, iconSize: NSMakeSize(24, 24), cornerRadius: nil) + } + if category == .groups { + return .icon(colors: theme.colors.peerColors(2), icon: theme.icons.chat_filter_large_groups_avatar, iconSize: NSMakeSize(24, 24), cornerRadius: nil) + } + if category == .channels { + return .icon(colors: theme.colors.peerColors(0), icon: theme.icons.chat_filter_channels_avatar, iconSize: NSMakeSize(24, 24), cornerRadius: nil) + } + if category == .bots { + return .icon(colors: theme.colors.peerColors(6), icon: theme.icons.chat_filter_bots_avatar, iconSize: NSMakeSize(24, 24), cornerRadius: nil) + } + if category == .excludeMuted { + return .icon(colors: theme.colors.peerColors(0), icon: theme.icons.chat_filter_muted_avatar, iconSize: NSMakeSize(24, 24), cornerRadius: nil) + } + if category == .excludeRead { + return .icon(colors: theme.colors.peerColors(3), icon: theme.icons.chat_filter_read_avatar, iconSize: NSMakeSize(24, 24), cornerRadius: nil) + } + if category == .excludeArchived { + return .icon(colors: theme.colors.peerColors(5), icon: theme.icons.chat_filter_archive_avatar, iconSize: NSMakeSize(24, 24), cornerRadius: nil) + } + return nil + } + + + init(decoder: PostboxDecoder) { + self.id = PeerId(0) + self.indexName = .title(title: "", addressName: "") + self.notificationSettingsPeerId = nil + self.category = [] + } + func encode(_ encoder: PostboxEncoder) { + + } +} + +extension Peer { + + func hasBannedRights(_ flags: TelegramChatBannedRightsFlags) -> Bool { + if let peer = self as? TelegramChannel { + if let _ = peer.hasBannedPermission(flags) { + return true + } + } else if let peer = self as? TelegramGroup { + return peer.hasBannedPermission(flags) + } + return false + } + + var webUrlRestricted: Bool { + return hasBannedRights([.banEmbedLinks]) + } + + + + + func canSendMessage(_ isThreadMode: Bool = false) -> Bool { + if self.id == repliesPeerId { + return false + } + if let channel = self as? TelegramChannel { + if case .broadcast(_) = channel.info { + return channel.hasPermission(.sendMessages) + } else if case .group = channel.info { + switch channel.participationStatus { + case .member: + return !channel.hasBannedRights(.banSendMessages) + case .left: + if isThreadMode { + return !channel.hasBannedRights(.banSendMessages) + } + return false + case .kicked: + return false + } + } + } else if let group = self as? TelegramGroup { + return group.membership == .Member && !group.hasBannedPermission(.banSendMessages) + } else if let secret = self as? TelegramSecretChat { + switch secret.embeddedState { + case .terminated: + return false + case .handshake: + return false + default: + return true + } + } + + return true + } + + var username:String? { + if let peer = self as? TelegramChannel { + return peer.username + } else if let peer = self as? TelegramGroup { + return peer.username + } else if let peer = self as? TelegramUser { + return peer.username + } + return nil + } + + var emptyAvatar: EmptyAvatartType? { + if let peer = self as? TelegramFilterCategory { + return peer.icon + } + return nil + } + + public var displayTitle: String { + switch self { + case let user as TelegramUser: + if user.firstName == nil && user.lastName == nil { + return L10n.peerDeletedUser + } else { + var name: String = "" + if let firstName = user.firstName { + name += firstName + } + if let lastName = user.lastName { + if user.firstName != nil { + name += " " + } + name += lastName + } + + return name.replacingOccurrences(of: "􀇻", with: "") + } + case let group as TelegramGroup: + return group.title.replacingOccurrences(of: "􀇻", with: "") + case let channel as TelegramChannel: + return channel.title.replacingOccurrences(of: "􀇻", with: "") + case let filter as TelegramFilterCategory: + return filter.displayTitle ?? "" + default: + return "" + } + } + + var rawDisplayTitle: String { + switch self { + case let user as TelegramUser: + if user.firstName == nil && user.lastName == nil { + return "" + } else { + var name: String = "" + if let firstName = user.firstName { + name += firstName + } + if let lastName = user.lastName { + if user.firstName != nil { + name += " " + } + name += lastName + } + return name + } + case let group as TelegramGroup: + return group.title + case let channel as TelegramChannel: + return channel.title + default: + return "" + } + } + + public var compactDisplayTitle: String { + switch self { + case let user as TelegramUser: + if let firstName = user.firstName { + return firstName.replacingOccurrences(of: "􀇻", with: "") + } else if let lastName = user.lastName { + return lastName.replacingOccurrences(of: "􀇻", with: "") + } else { + return tr(L10n.peerDeletedUser) + } + case let group as TelegramGroup: + return group.title.replacingOccurrences(of: "􀇻", with: "") + case let channel as TelegramChannel: + return channel.title.replacingOccurrences(of: "􀇻", with: "") + case let filter as TelegramFilterCategory: + return filter.displayTitle ?? "" + default: + return "" + } + } + + public var displayLetters: [String] { + switch self { + case let user as TelegramUser: + if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty && !lastName.isEmpty { + return [firstName[firstName.startIndex ..< firstName.index(after: firstName.startIndex)].uppercased(), lastName[lastName.startIndex ..< lastName.index(after: lastName.startIndex)].uppercased()] + } else if let firstName = user.firstName, !firstName.isEmpty { + return [firstName[firstName.startIndex ..< firstName.index(after: firstName.startIndex)].uppercased()] + } else if let lastName = user.lastName, !lastName.isEmpty { + return [lastName[lastName.startIndex ..< lastName.index(after: lastName.startIndex)].uppercased()] + } else { + let name = L10n.peerDeletedUser + if !name.isEmpty { + return [name[name.startIndex ..< name.index(after: name.startIndex)].uppercased()] + } + } + + return [] + case let group as TelegramGroup: + if !group.title.isEmpty { + return [group.title[group.title.startIndex ..< group.title.index(after: group.title.startIndex)].uppercased()] + } else { + return [] + } + case let channel as TelegramChannel: + if !channel.title.isEmpty { + return [channel.title[channel.title.startIndex ..< channel.title.index(after: channel.title.startIndex)].uppercased()] + } else { + return [] + } + default: + return [] + } + } + + var isVerified: Bool { + if let peer = self as? TelegramUser { + return peer.flags.contains(.isVerified) + } else if let peer = self as? TelegramChannel { + return peer.flags.contains(.isVerified) + } else { + return false + } + } + +} diff --git a/Telegram-Mac/PeersListController.swift b/Telegram-Mac/PeersListController.swift index 8a1f1b6507..b692053156 100644 --- a/Telegram-Mac/PeersListController.swift +++ b/Telegram-Mac/PeersListController.swift @@ -8,83 +8,397 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac - +import Postbox +import TelegramCore +import SwiftSignalKit +final class RevealAllChatsView : Control { + let textView: TextView = TextView() + var layoutState: SplitViewState = .dual { + didSet { + needsLayout = true + } + } + + + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + textView.userInteractionEnabled = false + textView.isSelectable = false + addSubview(textView) + + let layout = TextViewLayout(.initialize(string: L10n.chatListCloseFilter, color: .white, font: .medium(.title))) + layout.measure(width: max(280, frame.width)) + textView.update(layout) + + let shadow = NSShadow() + shadow.shadowBlurRadius = 5 + shadow.shadowColor = NSColor.black.withAlphaComponent(0.1) + shadow.shadowOffset = NSMakeSize(0, 2) + self.shadow = shadow + set(background: theme.colors.accent, for: .Normal) + } + + override func cursorUpdate(with event: NSEvent) { + NSCursor.pointingHand.set() + } + + override var backgroundColor: NSColor { + didSet { + textView.backgroundColor = backgroundColor + } + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + needsLayout = true + } + + + + override func layout() { + super.layout() + textView.center() + + layer?.cornerRadius = frame.height / 2 + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} +final class FilterTabsView : View { + let tabs: ScrollableSegmentView = ScrollableSegmentView(frame: NSZeroRect) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(tabs) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + tabs.frame = bounds + } +} class PeerListContainerView : View { - let tableView = TableView(frame:NSZeroRect, drawBorder: true) - var searchView:SearchView = SearchView(frame:NSZeroRect) - var compose:ImageButton = ImageButton() + private let backgroundView = BackgroundView(frame: NSZeroRect) + var tableView = TableView(frame:NSZeroRect, drawBorder: true) { + didSet { + oldValue.removeFromSuperview() + addSubview(tableView) + } + } + private let searchContainer: View = View() + + let searchView:SearchView = SearchView(frame:NSMakeRect(10, 0, 0, 0)) + let compose:ImageButton = ImageButton() + fileprivate let proxyButton:ImageButton = ImageButton() + private let proxyConnecting: ProgressIndicator = ProgressIndicator(frame: NSMakeRect(0, 0, 11, 11)) + private var searchState: SearchFieldState = .None + var openSharedMediaWithToken:((PeerId?, MessageTags?)->Void)? = nil + + var mode: PeerListMode = .plain { + didSet { + switch mode { + case .folder: + compose.isHidden = true + case .plain: + compose.isHidden = false + case .filter: + compose.isHidden = true + } + needsLayout = true + } + } required init(frame frameRect: NSRect) { super.init(frame: frameRect) self.border = [.Right] compose.autohighlight = false autoresizesSubviews = false + addSubview(searchContainer) addSubview(tableView) - addSubview(searchView) - addSubview(compose) + searchContainer.addSubview(compose) + searchContainer.addSubview(proxyButton) + searchContainer.addSubview(searchView) + proxyButton.addSubview(proxyConnecting) setFrameSize(frameRect.size) - updateLocalizationAndTheme() + updateLocalizationAndTheme(theme: theme) + proxyButton.disableActions() + addSubview(backgroundView) + backgroundView.isHidden = true + + + + tableView.getBackgroundColor = { + .clear + } + layout() + } + + fileprivate func updateProxyPref(_ pref: ProxySettings, _ connection: ConnectionStatus) { + proxyButton.isHidden = pref.servers.isEmpty && pref.effectiveActiveServer == nil + switch connection { + case .connecting, .waitingForNetwork: + proxyConnecting.isHidden = !pref.enabled + proxyButton.set(image: pref.enabled ? theme.icons.proxyState : theme.icons.proxyEnable, for: .Normal) + case .online, .updating: + proxyConnecting.isHidden = true + if pref.enabled { + proxyButton.set(image: theme.icons.proxyEnabled, for: .Normal) + } else { + proxyButton.set(image: theme.icons.proxyEnable, for: .Normal) + } + } + proxyConnecting.isEventLess = true + proxyConnecting.userInteractionEnabled = false + _ = proxyButton.sizeToFit() + proxyConnecting.centerX() + needsLayout = true + } + + + func searchStateChanged(_ state: SearchFieldState, animated: Bool, updateSearchTags: @escaping(SearchTags)->Void, updatePeerTag:@escaping(@escaping(Peer?)->Void)->Void, updateMessageTags: @escaping(@escaping(MessageTags?)->Void)->Void) { + self.searchState = state + searchView.change(size: NSMakeSize(state == .Focus || !mode.isPlain ? frame.width - searchView.frame.minX * 2 : (frame.width - (36 + compose.frame.width) - (proxyButton.isHidden ? 0 : proxyButton.frame.width + 12)), 30), animated: animated) + compose.change(opacity: state == .Focus ? 0 : 1, animated: animated) + proxyButton.change(opacity: state == .Focus ? 0 : 1, animated: animated) + + var currentTag: MessageTags? + var currentPeerTag: Peer? + + + let tags:[(MessageTags?, String, CGImage)] = [(nil, L10n.searchFilterClearFilter, theme.icons.search_filter), + (.photo, L10n.searchFilterPhotos, theme.icons.search_filter_media), + (.video, L10n.searchFilterVideos, theme.icons.search_filter_media), + (.webPage, L10n.searchFilterLinks, theme.icons.search_filter_links), + (.music, L10n.searchFilterMusic, theme.icons.search_filter_music), + (.voiceOrInstantVideo, L10n.searchFilterVoice, theme.icons.search_filter_music), + (.gif, L10n.searchFilterGIFs, theme.icons.search_filter_media), + (.file, L10n.searchFilterFiles, theme.icons.search_filter_files)] + + let collectTags: ()-> ([String], CGImage) = { + var values: [String] = [] + let image: CGImage + + if let tag = currentPeerTag { + values.append(tag.compactDisplayTitle.prefix(10)) + } + if let tag = currentTag { + if let found = tags.first(where: { $0.0 == tag }) { + values.append(found.1) + image = found.2 + } else { + image = theme.icons.search_filter + } + } else { + image = theme.icons.search_filter + } + return (values, image) + } + + switch state { + case .Focus: + searchView.customSearchControl = CustomSearchController(clickHandler: { control, updateTitle in + + + var items: [SPopoverItem] = [] + + + for tag in tags { + var append: Bool = false + if currentTag != tag.0 { + append = true + } + if append { + items.append(SPopoverItem(tag.1, { + currentTag = tag.0 + updateSearchTags(SearchTags(messageTags: currentTag, peerTag: currentPeerTag?.id)) + let collected = collectTags() + updateTitle(collected.0, collected.1) + })) + } + } + + showPopover(for: control, with: SPopoverViewController(items: items, visibility: 10), edge: .maxY, inset: NSMakePoint(0, -25)) + }, deleteTag: { [weak self] index in + var count: Int = 0 + if currentTag != nil { + count += 1 + } + if currentPeerTag != nil { + count += 1 + } + if index == 1 || count == 1 { + currentTag = nil + } + if index == 0 { + currentPeerTag = nil + } + let collected = collectTags() + updateSearchTags(SearchTags(messageTags: currentTag, peerTag: currentPeerTag?.id)) + self?.searchView.updateTags(collected.0, collected.1) + }, icon: theme.icons.search_filter) + + updatePeerTag( { [weak self] updatedPeerTag in + guard let `self` = self else { + return + } + currentPeerTag = updatedPeerTag + updateSearchTags(SearchTags(messageTags: currentTag, peerTag: currentPeerTag?.id)) + self.searchView.setString("") + let collected = collectTags() + self.searchView.updateTags(collected.0, collected.1) + }) + + updateMessageTags( { [weak self] updatedMessageTags in + guard let `self` = self else { + return + } + currentTag = updatedMessageTags + updateSearchTags(SearchTags(messageTags: currentTag, peerTag: currentPeerTag?.id)) + let collected = collectTags() + self.searchView.updateTags(collected.0, collected.1) + }) + + case .None: + searchView.customSearchControl = nil + } } - override func updateLocalizationAndTheme() { + override func updateLocalizationAndTheme(theme: PresentationTheme) { + let theme = (theme as! TelegramPresentationTheme) self.backgroundColor = theme.colors.background - compose.disableActions() - compose.set(background: theme.colors.background, for: .Normal) - compose.set(background: theme.colors.background, for: .Hover) - compose.set(background: theme.colors.blueFill, for: .Highlight) + compose.background = .clear + compose.set(background: .clear, for: .Normal) + compose.set(background: .clear, for: .Hover) + compose.set(background: theme.colors.accent, for: .Highlight) compose.set(image: theme.icons.composeNewChat, for: .Normal) compose.set(image: theme.icons.composeNewChatActive, for: .Highlight) compose.layer?.cornerRadius = .cornerRadius compose.setFrameSize(NSMakeSize(40, 30)) - super.updateLocalizationAndTheme() + proxyConnecting.progressColor = theme.colors.accentIcon +// proxyConnecting.lineWidth = 1.0 + super.updateLocalizationAndTheme(theme: theme) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func setFrameSize(_ newSize: NSSize) { - super.setFrameSize(newSize) - searchView.setFrameSize(newSize.width - 36 - compose.frame.width, 30) - tableView.setFrameSize(newSize.width, newSize.height - 49) - } - - - + override func layout() { super.layout() + + var offset: CGFloat + switch theme.controllerBackgroundMode { + case .background: + offset = 50 + case .tiled: + offset = 50 + default: + offset = 50 + } + + if frame.width < 200 { + switch self.mode { + case .folder: + offset = 0 + + default: + break + } + } + + searchContainer.frame = NSMakeRect(0, 0, frame.width, offset) + + + searchView.setFrameSize(NSMakeSize(searchState == .Focus || !mode.isPlain ? frame.width - searchView.frame.minX * 2 : (frame.width - (36 + compose.frame.width) - (proxyButton.isHidden ? 0 : proxyButton.frame.width + 12)), 30)) + + + tableView.setFrameSize(frame.width, frame.height - offset) + searchView.isHidden = frame.width < 200 if searchView.isHidden { - compose.centerX(y: floorToScreenPixels((49 - compose.frame.height)/2.0)) + compose.center() + proxyButton.setFrameOrigin(-proxyButton.frame.width, 0) } else { - compose.setFrameOrigin(frame.width - 12 - compose.frame.width, floorToScreenPixels((50 - compose.frame.height)/2.0)) + compose.setFrameOrigin(searchContainer.frame.width - 12 - compose.frame.width, floorToScreenPixels(backingScaleFactor, (searchContainer.frame.height - compose.frame.height)/2.0)) + proxyButton.setFrameOrigin(searchContainer.frame.width - 12 - compose.frame.width - proxyButton.frame.width - 6, floorToScreenPixels(backingScaleFactor, (searchContainer.frame.height - proxyButton.frame.height)/2.0)) } - searchView.setFrameOrigin(10, floorToScreenPixels((49 - searchView.frame.height)/2.0)) - tableView.setFrameOrigin(0, 49) + searchView.setFrameOrigin(10, floorToScreenPixels(backingScaleFactor, (offset - searchView.frame.height)/2.0)) + tableView.setFrameOrigin(0, offset) + + proxyConnecting.centerX() + proxyConnecting.centerY(addition: -(backingScaleFactor == 2.0 ? 0.5 : 0)) + + backgroundView.frame = bounds + self.needsDisplay = true } + +} + + +enum PeerListMode { + case plain + case folder(PeerGroupId) + case filter(Int32) + + var isPlain:Bool { + switch self { + case .plain: + return true + default: + return false + } + } + var groupId: PeerGroupId { + switch self { + case let .folder(groupId): + return groupId + default: + return .root + } + } + var filterId: Int32? { + switch self { + case let .filter(id): + return id + default: + return nil + } + } } class PeersListController: TelegramGenericViewController, TableViewDelegate { - private let globalPeerDisposable:MetaDisposable = MetaDisposable() + + + func findGroupStableId(for stableId: AnyHashable) -> AnyHashable? { + return nil + } + private let progressDisposable = MetaDisposable() private let createSecretChatDisposable = MetaDisposable() private let layoutDisposable = MetaDisposable() + private let actionsDisposable = DisposableSet() private let followGlobal:Bool - private var searchController:SearchController? { + private let searchOptions: AppSearchOptions + let mode:PeerListMode + private(set) var searchController:SearchController? { didSet { if let controller = searchController { genericView.customHandler.size = { [weak controller] size in - controller?.view.setFrameSize(NSMakeSize(size.width, size.height - 50)) + controller?.view.setFrameSize(NSMakeSize(size.width, size.height - 49)) } progressDisposable.set((controller.isLoading.get() |> deliverOnMainQueue).start(next: { [weak self] isLoading in self?.genericView.searchView.isLoading = isLoading @@ -93,18 +407,27 @@ class PeersListController: TelegramGenericViewController, } } - init(_ account:Account, followGlobal:Bool = true) { + init(_ context: AccountContext, followGlobal:Bool = true, mode: PeerListMode = .plain, searchOptions: AppSearchOptions = [.chats, .messages]) { self.followGlobal = followGlobal - - super.init(account) - + self.mode = mode + self.searchOptions = searchOptions + super.init(context) + self.bar = .init(height: !mode.isPlain ? 50 : 0) + } + + override var redirectUserInterfaceCalls: Bool { + return true + } + + override var responderPriority: HandlerPriority { + return .low } deinit { - globalPeerDisposable.dispose() progressDisposable.dispose() createSecretChatDisposable.dispose() layoutDisposable.dispose() + actionsDisposable.dispose() } override func viewDidResized(_ size: NSSize) { @@ -114,170 +437,335 @@ class PeersListController: TelegramGenericViewController, override func viewDidLoad() { super.viewDidLoad() + let context = self.context + + + layoutDisposable.set(context.sharedContext.layoutHandler.get().start(next: { [weak self] state in + if let strongSelf = self, case .minimisize = state { + if strongSelf.genericView.searchView.state == .Focus { + strongSelf.genericView.searchView.change(state: .None, false) + } + } + self?.checkSearchMedia() + self?.genericView.tableView.alwaysOpenRowsOnMouseUp = state == .single + self?.genericView.tableView.reloadData() + Queue.mainQueue().justDispatch { + self?.requestUpdateBackBar() + } + })) + + let actionsDisposable = self.actionsDisposable + + actionsDisposable.add((context.cancelGlobalSearch.get() |> deliverOnMainQueue).start(next: { [weak self] animated in + self?.genericView.searchView.cancel(animated) + })) + + genericView.mode = mode + if followGlobal { - globalPeerDisposable.set((globalPeerHandler.get() |> deliverOnMainQueue).start(next: { [weak self] peerId in - self?.genericView.tableView.changeSelection(stableId: peerId) + actionsDisposable.add((context.globalPeerHandler.get() |> deliverOnMainQueue).start(next: { [weak self] location in + guard let `self` = self else {return} + self.changeSelection(location) + if location == nil { + if !self.genericView.searchView.isEmpty { + _ = self.window?.makeFirstResponder(self.genericView.searchView.input) + } + } })) } if self.navigationController?.modalAction is FWDNavigationAction { - self.setCenterTitle(tr(.chatForwardActionHeader)) + self.setCenterTitle(L10n.chatForwardActionHeader) } if self.navigationController?.modalAction is ShareInlineResultNavigationAction { - self.setCenterTitle(tr(.chatShareInlineResultActionHeader)) + self.setCenterTitle(L10n.chatShareInlineResultActionHeader) } genericView.tableView.delegate = self - let table = genericView.tableView + + var settings:(ProxySettings, ConnectionStatus)? = nil + actionsDisposable.add(combineLatest(proxySettings(accountManager: context.sharedContext.accountManager) |> mapToSignal { ps -> Signal<(ProxySettings, ConnectionStatus), NoError> in + return context.account.network.connectionStatus |> map { status -> (ProxySettings, ConnectionStatus) in + return (ps, status) + } + } |> deliverOnMainQueue, appearanceSignal |> deliverOnMainQueue).start(next: { [weak self] pref, _ in + settings = (pref.0, pref.1) + self?.genericView.updateProxyPref(pref.0, pref.1) + })) + + let pushController:(ViewController)->Void = { [weak self] c in + self?.context.sharedContext.bindings.rootNavigation().push(c) + } + + let openProxySettings:()->Void = { [weak self] in + if let controller = self?.context.sharedContext.bindings.rootNavigation().controller as? InputDataController { + if controller.identifier == "proxy" { + return + } + } + let controller = proxyListController(accountManager: context.sharedContext.accountManager, network: context.account.network, share: { servers in + var message: String = "" + for server in servers { + message += server.link + "\n\n" + } + message = message.trimmed + + showModal(with: ShareModalController(ShareLinkObject(context, link: message)), for: mainWindow) + }, pushController: { controller in + pushController(controller) + }) + pushController(controller) + } + + genericView.proxyButton.set(handler: { _ in + if let settings = settings { + openProxySettings() + } + }, for: .Click) genericView.compose.set(handler: { [weak self] control in if let strongSelf = self, !control.isSelected { - let items = [SPopoverItem(tr(.composePopoverNewGroup), { [weak strongSelf] in - if let strongSelf = strongSelf, let navigation = strongSelf.navigationController { - createGroup(with: strongSelf.account, for: navigation) - } - - }, theme.icons.composeNewGroup),SPopoverItem(tr(.composePopoverNewSecretChat), { [weak strongSelf] in - if let strongSelf = strongSelf, let account = self?.account { - let confirmationImpl:([PeerId])->Signal = { peerIds in - if let first = peerIds.first, peerIds.count == 1 { - return account.postbox.loadedPeerWithId(first) |> deliverOnMainQueue |> mapToSignal { peer in - return confirmSignal(for: mainWindow, header: appName, information: tr(.composeConfirmStartSecretChat(peer.displayTitle))) - } - } - return confirmSignal(for: mainWindow, header: appName, information: tr(.peerInfoConfirmAddMembers(peerIds.count))) - } - let select = selectModalPeers(account: account, title: tr(.composeSelectSecretChat), limit: 1, confirmation: confirmationImpl) - - let create = select |> map { $0.first! } |> mapToSignal { peerId in - return createSecretChat(account: account, peerId: peerId) |> mapError {_ in} - } |> deliverOnMainQueue |> mapToSignal{ peerId -> Signal in - return showModalProgress(signal: .single(peerId), for: mainWindow) - } - - strongSelf.createSecretChatDisposable.set(create.start(next: { [weak self] peerId in - self?.navigationController?.push(ChatController(account: account, peerId: peerId)) - })) - - } - }, theme.icons.composeNewSecretChat),SPopoverItem(tr(.composePopoverNewChannel), { [weak strongSelf] in - if let strongSelf = strongSelf, let navigation = strongSelf.navigationController { - createChannel(with: strongSelf.account, for: navigation) - } + let items = [SPopoverItem(tr(L10n.composePopoverNewGroup), { [weak strongSelf] in + guard let strongSelf = strongSelf else {return} + strongSelf.context.composeCreateGroup() + }, theme.icons.composeNewGroup),SPopoverItem(tr(L10n.composePopoverNewSecretChat), { [weak strongSelf] in + guard let strongSelf = strongSelf else {return} + strongSelf.context.composeCreateSecretChat() + }, theme.icons.composeNewSecretChat),SPopoverItem(tr(L10n.composePopoverNewChannel), { [weak strongSelf] in + guard let strongSelf = strongSelf else {return} + strongSelf.context.composeCreateChannel() }, theme.icons.composeNewChannel)]; - - showPopover(for: control, with: SPopoverViewController(items: items), edge: .maxY, inset: NSMakePoint(-138, -(strongSelf.genericView.compose.frame.maxY + 10))) + if let popover = control.popover { + popover.hide() + } else { + showPopover(for: control, with: SPopoverViewController(items: items), edge: .maxY, inset: NSMakePoint(-138, -(strongSelf.genericView.compose.frame.maxY + 10))) + } } }, for: .Click) - genericView.searchView.searchInteractions = SearchInteractions({[weak self] (state) in - if let strongSelf = self { - switch state.state { - case .Focus: - - assert(strongSelf.searchController == nil) - - let searchController = SearchController(account: strongSelf.account, open:{ [weak strongSelf] (peerId, message, close) in - strongSelf?.open(with: peerId, message:message, close:close) - }, frame:table.frame) - strongSelf.searchController = searchController + genericView.searchView.searchInteractions = SearchInteractions({ [weak self] state, animated in + guard let `self` = self else {return} + switch state.state { + case .Focus: + assert(self.searchController == nil) + self.showSearchController(animated: animated) - searchController.navigationController = strongSelf.navigationController - searchController.viewWillAppear(true) - searchController.view.layer?.opacity = 1.0 - searchController.view.layer?.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion:{[weak strongSelf](complete) in - strongSelf?.searchController?.viewDidAppear(true) - }) - - strongSelf.addSubview(searchController.view) - case .None: - - assert(strongSelf.searchController != nil) - - let searchController = strongSelf.searchController! - searchController.viewWillDisappear(true) - searchController.view.layer?.opacity = 0.0 - searchController.view.layer?.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, completion:{[weak strongSelf](complete) in - - strongSelf?.searchController?.viewDidDisappear(true) - strongSelf?.searchController?.removeFromSuperview() - strongSelf?.searchController = nil - - }) - - } + case .None: + self.hideSearchController(animated: animated) } - - }, { [weak self] state in - self?.searchController?.request(with: state.request) + self.genericView.searchStateChanged(state.state, animated: animated, updateSearchTags: { [weak self] tags in + self?.searchController?.updateSearchTags(tags) + self?.sharedMediaWithToken(tags) + }, updatePeerTag: { [weak self] f in + self?.searchController?.setPeerAsTag = f + }, updateMessageTags: { [weak self] f in + self?.updateSearchMessageTags = f + }) + + }, { [weak self] state in + guard let `self` = self else {return} + self.searchController?.request(with: state.request) + }, responderModified: { [weak self] state in + self?.context.isInGlobalSearch = state.responder }) - readyOnce() } - - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - if animated { - genericView.tableView.layoutItems() + private func checkSearchMedia() { + let destroy:()->Void = { [weak self] in + if let previous = self?.mediaSearchController { + self?.context.sharedContext.bindings.rootNavigation().removeImmediately(previous) + } } - - if account.context.layout == .single && animated { - globalPeerHandler.set(.single(nil)) + guard context.sharedContext.layout == .dual else { + destroy() + return } - layoutDisposable.set(account.context.layoutHandler.get().start(next: { [weak self] state in - if let strongSelf = self, case .minimisize = state { - if strongSelf.genericView.searchView.state == .Focus { - strongSelf.genericView.searchView.change(state: .None, false) - } + guard let _ = self.searchController else { + destroy() + return + } + } + private weak var mediaSearchController: PeerMediaController? + private var updateSearchMessageTags: ((MessageTags?)->Void)? = nil + private func sharedMediaWithToken(_ tags: SearchTags) -> Void { + + let destroy:()->Void = { [weak self] in + if let previous = self?.mediaSearchController { + self?.context.sharedContext.bindings.rootNavigation().removeImmediately(previous) } - self?.genericView.tableView.reloadData() - })) + } - account.context.globalSearch = { [weak self] query in - if let strongSelf = self { - _ = (strongSelf.account.context.layoutHandler.get() |> take(1)).start(next: { [weak strongSelf] state in - if let strongSelf = strongSelf { - - let invoke = { [weak strongSelf] in - strongSelf?.genericView.searchView.change(state: .Focus, false) - strongSelf?.genericView.searchView.setString(query) - } - - switch state { - case .single: - strongSelf.account.context.mainNavigation?.back() - Queue.mainQueue().justDispatch(invoke) - case .minimisize: - (strongSelf.window?.contentView?.subviews.first as? SplitView)?.needFullsize() - Queue.mainQueue().justDispatch { - if strongSelf.navigationController?.controller is ChatController { - strongSelf.navigationController?.back() - Queue.mainQueue().justDispatch(invoke) - } - } - default: - invoke() - } - - } - }) + guard context.sharedContext.layout == .dual else { + destroy() + return + } + guard let searchController = self.searchController else { + destroy() + return + } + guard let messageTags = tags.messageTags else { + destroy() + return + } + if let peerId = tags.peerTag { + + let onDeinit: ()->Void = { [weak self] in + self?.updateSearchMessageTags?(nil) + } + + let navigation = context.sharedContext.bindings.rootNavigation() + + let signal = searchController.externalSearchMessages + |> filter { $0 != nil && $0?.tags == messageTags } + + let controller = PeerMediaController(context: context, peerId: peerId, isProfileIntended: false, externalSearchData: PeerMediaExternalSearchData(initialTags: messageTags, searchResult: signal, loadMore: { })) + + controller.onDeinit = onDeinit + + navigation.push(controller, false, style: nil) + + if let previous = self.mediaSearchController { + previous.onDeinit = nil + navigation.removeImmediately(previous, depencyReady: controller) + } + + self.mediaSearchController = controller + } + } + + override func requestUpdateBackBar() { + self.leftBarView.minWidth = 70 + super.requestUpdateBackBar() + } + + override func getLeftBarViewOnce() -> BarView { + let view = BackNavigationBar(self, canBeEmpty: true) + view.minWidth = 70 + return view + } + + override func backSettings() -> (String, CGImage?) { + return context.sharedContext.layout == .minimisize ? ("", theme.icons.instantViewBack) : super.backSettings() + } + + + func changeSelection(_ location: ChatLocation?) { + if let location = location { + switch location { + case .peer: + self.genericView.tableView.changeSelection(stableId: UIChatListEntryId.chatId(location.peerId, nil)) + case .replyThread: + self.genericView.tableView.changeSelection(stableId: nil) } + } else { + self.genericView.tableView.changeSelection(stableId: nil) + } + } + + private func showSearchController(animated: Bool) { + if searchController == nil { + // delay(0.15, closure: { + let rect = self.genericView.tableView.frame + let searchController = SearchController(context: self.context, open:{ [weak self] (peerId, messageId, close) in + if let peerId = peerId { + self?.open(with: .chatId(peerId, nil), messageId: messageId, close:close) + } else { + self?.genericView.searchView.cancel(true) + } + }, options: self.searchOptions, frame:NSMakeRect(rect.minX, rect.minY, self.frame.width, rect.height)) + + searchController.pinnedItems = self.collectPinnedItems + + self.searchController = searchController +// self.genericView.tableView.change(opacity: 0, animated: animated, completion: { [weak self] _ in +// self?.genericView.tableView.isHidden = true +// }) + searchController.defaultQuery = self.genericView.searchView.query + searchController.navigationController = self.navigationController + searchController.viewWillAppear(true) + + + + if animated { + searchController.view.layer?.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion:{ [weak self] complete in + if complete { + self?.searchController?.viewDidAppear(animated) + // self?.genericView.tableView.isHidden = true + } + }) + searchController.view.layer?.animateScaleSpring(from: 1.05, to: 1.0, duration: 0.4, bounce: false) + searchController.view.layer?.animatePosition(from: NSMakePoint(rect.minX, rect.minY + 15), to: rect.origin, duration: 0.4, timingFunction: .spring) + + } else { + searchController.viewDidAppear(animated) + } + self.addSubview(searchController.view) + // }) } + } + + private func hideSearchController(animated: Bool) { + if let searchController = self.searchController { + searchController.viewWillDisappear(animated) + searchController.view.layer?.opacity = animated ? 1.0 : 0.0 + searchController.viewDidDisappear(true) + self.searchController = nil + self.genericView.tableView.isHidden = false + self.genericView.tableView.change(opacity: 1, animated: animated) + let view = searchController.view + + searchController.view._change(opacity: 0, animated: animated, duration: 0.25, timingFunction: CAMediaTimingFunctionName.spring, completion: { [weak view] completed in + view?.removeFromSuperview() + }) + searchController.view.layer?.animateScaleSpring(from: 1.0, to: 1.05, duration: 0.4, removeOnCompletion: false, bounce: false) + genericView.tableView.layer?.animateScaleSpring(from: 0.95, to: 1.00, duration: 0.4, removeOnCompletion: false, bounce: false) + + } + if let controller = mediaSearchController { + context.sharedContext.bindings.rootNavigation().removeImmediately(controller, upNext: false) + } + } + + override func focusSearch(animated: Bool, text: String? = nil) { + genericView.searchView.change(state: .Focus, animated) + if let text = text { + genericView.searchView.setString(text) + // self?.searchController?.updateSearchTags(tag) + //genericView.searchView.updateTags(<#T##tags: [String]##[String]#>, <#T##image: CGImage##CGImage#>) + } + } + + override func navigationUndoHeaderDidNoticeAnimation(_ current: CGFloat, _ previous: CGFloat, _ animated: Bool) -> ()->Void { + genericView.layer?.animatePosition(from: NSMakePoint(0, previous), to: NSMakePoint(0, current), removeOnCompletion: false) + return { [weak genericView] in + genericView?.layer?.removeAllAnimations() + } + } + + + + + var collectPinnedItems:[PinnedItemId] { + return [] } + + public override func escapeKeyAction() -> KeyHandlerResult { - guard account.context.layout != .minimisize else { + guard context.sharedContext.layout != .minimisize else { + return .invoked + } + if genericView.tableView.highlightedItem() != nil { + genericView.tableView.cancelHighlight() return .invoked } if genericView.searchView.state == .None { @@ -290,20 +778,64 @@ class PeersListController: TelegramGenericViewController, } public override func returnKeyAction() -> KeyHandlerResult { + if let highlighted = genericView.tableView.highlightedItem() { + _ = genericView.tableView.select(item: highlighted) + return .invoked + } return .rejected } - func open(with peerId:PeerId, message:Message? = nil, close:Bool = true) ->Void { - if let navigationController = navigationController { - let chat:ChatController = ChatController(account: self.account, peerId:peerId, messageId:message?.id) - navigationController.push(chat) + func open(with entryId: UIChatListEntryId, messageId:MessageId? = nil, initialAction: ChatInitialAction? = nil, close:Bool = true, addition: Bool = false) ->Void { + + let navigation = context.sharedContext.bindings.rootNavigation() + + var addition = addition + var close = close + if let searchTags = self.searchController?.searchTags { + if searchTags.peerTag != nil && searchTags.messageTags != nil { + addition = true + } + if !searchTags.isEmpty { + close = false + } + } + + switch entryId { + case let .chatId(peerId, _): + + if let modalAction = navigation.modalAction as? FWDNavigationAction, peerId == context.peerId { + _ = Sender.forwardMessages(messageIds: modalAction.messages.map{$0.id}, context: context, peerId: context.peerId).start() + _ = showModalSuccess(for: mainWindow, icon: theme.icons.successModalProgress, delay: 1.0).start() + modalAction.afterInvoke() + navigation.removeModalAction() + } else { + + if let current = navigation.controller as? ChatController, peerId == current.chatInteraction.peerId, let messageId = messageId, current.mode == .history { + current.chatInteraction.focusMessageId(nil, messageId, .center(id: 0, innerId: nil, animated: false, focus: .init(focus: true), inset: 0)) + } else { + let chat:ChatController = addition ? ChatAdditionController(context: context, chatLocation: .peer(peerId), messageId: messageId) : ChatController(context: self.context, chatLocation: .peer(peerId), messageId: messageId, initialAction: initialAction) + navigation.push(chat, context.sharedContext.layout == .single) + } + } + case let .groupId(groupId): + self.navigationController?.push(ChatListController(context, modal: false, groupId: groupId)) + case .reveal: + break + case .empty: + break + case .loading: + break } if close { - genericView.searchView.cancel(true) + self.genericView.searchView.cancel(true) } } - func selectionWillChange(row:Int, item:TableRowItem) -> Bool { + func longSelect(row: Int, item: TableRowItem) { + + } + + func selectionWillChange(row:Int, item:TableRowItem, byClick: Bool) -> Bool { return true } @@ -315,50 +847,76 @@ class PeersListController: TelegramGenericViewController, return true } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) } + private var effectiveTableView: TableView { + switch genericView.searchView.state { + case .Focus: + return searchController?.genericView ?? genericView.tableView + case .None: + return genericView.tableView + } + } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - self.window?.set(handler: { [weak self] in + + if animated { + // genericView.tableView.layoutItems() + } + + if context.sharedContext.layout == .single && animated { + context.globalPeerHandler.set(.single(nil)) + } + + + context.window.set(handler: { [weak self] _ in if let strongSelf = self { return strongSelf.escapeKeyAction() } return .invokeNext }, with: self, for: .Escape, priority:.low) + context.window.set(handler: { [weak self] _ in + if let strongSelf = self { + return strongSelf.returnKeyAction() + } + return .invokeNext + }, with: self, for: .Return, priority:.low) - self.window?.set(handler: {[weak self] () -> KeyHandlerResult in - if let item = self?.genericView.tableView.selectedItem(), item.index > 0 { - self?.genericView.tableView.selectPrev() + context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + if let item = self?.effectiveTableView.selectedItem(), item.index > 0 { + self?.effectiveTableView.selectPrev() } return .invoked }, with: self, for: .UpArrow, priority: .medium, modifierFlags: [.option]) - self.window?.set(handler: {[weak self] () -> KeyHandlerResult in - self?.genericView.tableView.selectNext() + context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.effectiveTableView.selectNext() return .invoked }, with: self, for: .DownArrow, priority:.medium, modifierFlags: [.option]) - self.window?.set(handler: {[weak self] () -> KeyHandlerResult in - self?.genericView.tableView.selectNext() + context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.effectiveTableView.selectNext(turnDirection: false) return .invoked - }, with: self, for: .Tab, priority: .low, modifierFlags: [.control]) + }, with: self, for: .Tab, priority: .modal, modifierFlags: [.control]) - self.window?.set(handler: {[weak self] () -> KeyHandlerResult in - self?.genericView.tableView.selectPrev() + context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.effectiveTableView.selectPrev(turnDirection: false) return .invoked - }, with: self, for: .Tab, priority:.medium, modifierFlags: [.control, .shift]) + }, with: self, for: .Tab, priority: .modal, modifierFlags: [.control, .shift]) + - } + + override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - self.window?.removeAllHandlers(for: self) + context.window.removeAllHandlers(for: self) } diff --git a/Telegram-Mac/PhoneCallWindowController.swift b/Telegram-Mac/PhoneCallWindowController.swift index 82fb2c781f..a46561c653 100644 --- a/Telegram-Mac/PhoneCallWindowController.swift +++ b/Telegram-Mac/PhoneCallWindowController.swift @@ -1,5 +1,5 @@ // -// PhoneCallWindow.swift +// CallWindow.swift // Telegram // // Created by keepcoder on 24/04/2017. @@ -8,100 +8,576 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac -import MtProtoKitMac +import TelegramCore +import SyncCore +import Postbox +import SwiftSignalKit +import TgVoipWebrtc -private class ShadowView : View { + + +private let defaultWindowSize = NSMakeSize(358, 477) + +extension CallState { + var videoIsAvailable: Bool { + switch state { + case .ringing, .requesting: + switch videoState { + case .notAvailable, .possible: + return false + default: + return true + } + case .terminating, .terminated, .connecting: + return false + default: + return true + } + } +} + +extension CallSessionTerminationReason { + var recall: Bool { + let recall:Bool + switch self { + case .ended(let reason): + switch reason { + case .busy: + recall = true + default: + recall = false + } + case .error(let reason): + switch reason { + case .disconnected: + recall = true + default: + recall = false + } + } + return recall + } +} + + +private struct CallControlData { + let text: String + let isVisualEffect: Bool + let icon: CGImage + let iconSize: NSSize + let backgroundColor: NSColor +} + +private final class CallControl : Control { + private let imageView: ImageView = ImageView() + private var imageBackgroundView:NSView? = nil + private let textView: TextView = TextView() + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(textView) + textView.isSelectable = false + textView.userInteractionEnabled = false + + } + + override var mouseDownCanMoveWindow: Bool { + return false + } + + override func stateDidUpdated( _ state: ControlState) { + + switch controlState { + case .Highlight: + imageBackgroundView?._change(opacity: 0.9) + textView.change(opacity: 0.9) + default: + imageBackgroundView?._change(opacity: 1.0) + textView.change(opacity: 1.0) + } + } + + func updateEnabled(_ enabled: Bool, animated: Bool) { + self.isEnabled = enabled + + change(opacity: enabled ? 1 : 0.7, animated: animated) + } + + var size: NSSize { + return imageBackgroundView?.frame.size ?? frame.size + } - override func draw(_ layer: CALayer, in ctx: CGContext) { + func updateWithData(_ data: CallControlData, animated: Bool) { + let layout = TextViewLayout(.initialize(string: data.text, color: .white, font: .normal(12)), maximumNumberOfLines: 1) + layout.measure(width: max(data.iconSize.width, 100)) - ctx.clear(NSMakeRect(0, 0, frame.width, frame.height)) + textView.update(layout) - var locations: [CGFloat] = [1.0, 0.2]; - let colorSpace = CGColorSpaceCreateDeviceRGB() - let gradient = CGGradient(colorsSpace: colorSpace, colors: NSArray(array: [NSColor.black.withAlphaComponent(0.4).cgColor, NSColor.clear.cgColor]), locations: nil)! + if data.isVisualEffect { + if !(self.imageBackgroundView is NSVisualEffectView) || self.imageBackgroundView == nil { + self.imageBackgroundView?.removeFromSuperview() + self.imageBackgroundView = NSVisualEffectView(frame: NSMakeRect(0, 0, data.iconSize.width, data.iconSize.height)) + self.imageBackgroundView?.wantsLayer = true + self.addSubview(self.imageBackgroundView!) + } + let view = self.imageBackgroundView as! NSVisualEffectView + + view.material = .light + view.state = .active + view.blendingMode = .withinWindow + } else { + if self.imageBackgroundView is NSVisualEffectView || self.imageBackgroundView == nil { + self.imageBackgroundView?.removeFromSuperview() + self.imageBackgroundView = View(frame: NSMakeRect(0, 0, data.iconSize.width, data.iconSize.height)) + self.addSubview(self.imageBackgroundView!) + } + self.imageBackgroundView?.background = data.backgroundColor + } + imageView.removeFromSuperview() + self.imageBackgroundView?.addSubview(imageView) + + imageBackgroundView!._change(size: data.iconSize, animated: animated) + imageBackgroundView!.layer?.cornerRadius = data.iconSize.height / 2 + + imageView.animates = animated + imageView.image = data.icon + imageView.sizeToFit() + + change(size: NSMakeSize(max(data.iconSize.width, textView.frame.width), data.iconSize.height + 5 + layout.layoutSize.height), animated: animated) + + if animated { + imageView._change(pos: imageBackgroundView!.focus(imageView.frame.size).origin, animated: animated) + textView._change(pos: NSMakePoint(floorToScreenPixels(backingScaleFactor, (frame.width - textView.frame.width) / 2), imageBackgroundView!.frame.height + 5), animated: animated) + imageBackgroundView!._change(pos: NSMakePoint(floorToScreenPixels(backingScaleFactor, (frame.width - imageBackgroundView!.frame.width) / 2), 0), animated: animated) + } + + needsLayout = true + } + + override func layout() { + super.layout() + + imageView.center() + if let imageBackgroundView = imageBackgroundView { + imageBackgroundView.centerX(y: 0) + textView.centerX(y: imageBackgroundView.frame.height + 5) + } + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + +private final class OutgoingVideoView : GeneralRowContainerView { + + private var progressIndicator: ProgressIndicator? = nil + + fileprivate var videoView: (OngoingCallContextVideoView?, Bool)? { + didSet { + if let videoView = oldValue?.0 { + videoView.view.removeFromSuperview() + progressIndicator?.removeFromSuperview() + progressIndicator = nil + } else if let videoView = videoView?.0 { + addSubview(videoView.view, positioned: .below, relativeTo: self.overlay) + videoView.view.frame = self.bounds + videoView.view.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + + if self.videoView?.1 == true { + videoView.view.background = .blackTransparent + if self.progressIndicator == nil { + self.progressIndicator = ProgressIndicator(frame: NSMakeRect(0, 0, 40, 40)) + self.progressIndicator?.progressColor = .white + addSubview(self.progressIndicator!) + self.progressIndicator!.center() + } + } else if self.videoView?.1 == false { + videoView.view.background = .clear + if notAvailableView == nil { + let current = TextView() + self.notAvailableView = current + let layout = TextViewLayout(.initialize(string: "Camera is unavailable", color: .white, font: .normal(13)), maximumNumberOfLines: 2, alignment: .center) + current.userInteractionEnabled = false + current.isSelectable = false + current.update(layout) + self.notAvailableView = current + addSubview(current, positioned: .below, relativeTo: overlay) + } + } else { + videoView.view.background = .clear + if let notAvailableView = self.notAvailableView { + self.notAvailableView = nil + notAvailableView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak notAvailableView] _ in + notAvailableView?.removeFromSuperview() + }) + } + } + + videoView.setOnFirstFrameReceived({ [weak self] aspectRatio in + self?.videoView?.0?.view.background = .black + if let progressIndicator = self?.progressIndicator { + self?.progressIndicator = nil + progressIndicator.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak progressIndicator] _ in + progressIndicator?.removeFromSuperview() + }) + } + }) + } + needsLayout = true + } + } + + override var isEventLess: Bool { + didSet { + overlay.isEventLess = isEventLess + } + } + + // + + static var defaultSize: NSSize = NSMakeSize(100 * System.cameraAspectRatio, 100) + + enum ResizeDirection { + case topLeft + case topRight + case bottomLeft + case bottomRight + } + + let overlay: Control = Control() + + + private var disabledView: NSVisualEffectView? + private var notAvailableView: TextView? + + + private let maskLayer = CAShapeLayer() + + + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + super.addSubview(overlay) + self.cornerRadius = .cornerRadius + setCorners([.bottomLeft, .bottomRight, .topLeft, .topRight]) + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + + self.overlay.frame = bounds + self.videoView?.0?.view.frame = bounds + self.videoView?.0?.view.subviews.first?.frame = bounds + self.progressIndicator?.center() + self.disabledView?.frame = bounds + + if let textView = notAvailableView { + let layout = textView.layout + layout?.measure(width: frame.width - 40) + textView.update(layout) + textView.center() + } + } + + func setIsPaused(_ paused: Bool, animated: Bool) { + if paused { + if disabledView == nil { + let current = NSVisualEffectView() + current.material = .dark + current.state = .active + current.blendingMode = .withinWindow + current.wantsLayer = true + + current.frame = bounds + self.disabledView = current + addSubview(current, positioned: .below, relativeTo: overlay) + + if animated { + current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } else { + self.disabledView?.frame = bounds + } + } else { + if let disabledView = self.disabledView { + self.disabledView = nil + if animated { + disabledView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak disabledView] _ in + disabledView?.removeFromSuperview() + }) + } else { + disabledView.removeFromSuperview() + } + } + } + } + + override func setFrameSize(_ newSize: NSSize) { + super.setFrameSize(newSize) + } + + func updateFrame(_ frame: NSRect, animated: Bool) { + if self.frame != frame { + let duration: Double = 0.18 + + self.videoView?.0?.view.subviews.first?._change(size: frame.size, animated: animated, duration: duration) + self.videoView?.0?.view._change(size: frame.size, animated: animated, duration: duration) + self.overlay._change(size: frame.size, animated: animated, duration: duration) + self.progressIndicator?.change(pos: frame.focus(NSMakeSize(40, 40)).origin, animated: animated, duration: duration) + + self.disabledView?._change(size: frame.size, animated: animated, duration: duration) + + if let textView = notAvailableView, let layout = textView.layout { + layout.measure(width: frame.width - 40) + textView.update(layout) + textView.change(pos: frame.focus(layout.layoutSize).origin, animated: animated, duration: duration) + } + self.change(size: frame.size, animated: animated, corners: [.bottomLeft, .bottomRight, .topLeft, .topRight]) + self.change(pos: frame.origin, animated: animated, duration: duration) + } + self.frame = frame + updateCursorRects() + } + + private func updateCursorRects() { + resetCursorRects() + if let cursor = NSCursor.set_windowResizeNorthEastSouthWestCursor { + addCursorRect(NSMakeRect(0, frame.height - 10, 10, 10), cursor: cursor) + addCursorRect(NSMakeRect(frame.width - 10, 0, 10, 10), cursor: cursor) + } + if let cursor = NSCursor.set_windowResizeNorthWestSouthEastCursor { + addCursorRect(NSMakeRect(0, 0, 10, 10), cursor: cursor) + addCursorRect(NSMakeRect(frame.width - 10, frame.height - 10, 10, 10), cursor: cursor) + } + } + + override func cursorUpdate(with event: NSEvent) { + super.cursorUpdate(with: event) + updateCursorRects() + } + + func runResizer(at point: NSPoint) -> ResizeDirection? { + let rects: [(NSRect, ResizeDirection)] = [(NSMakeRect(0, frame.height - 10, 10, 10), .bottomLeft), + (NSMakeRect(frame.width - 10, 0, 10, 10), .topRight), + (NSMakeRect(0, 0, 10, 10), .topLeft), + (NSMakeRect(frame.width - 10, frame.height - 10, 10, 10), .bottomRight)] + for rect in rects { + if NSPointInRect(point, rect.0) { + return rect.1 + } + } + return nil + } + + override var mouseDownCanMoveWindow: Bool { + return isEventLess + } +} + +private final class IncomingVideoView : Control { + + var updateAspectRatio:((Float)->Void)? = nil + + private var disabledView: NSVisualEffectView? + fileprivate var videoView: OngoingCallContextVideoView? { + didSet { + if let videoView = oldValue { + videoView.view.removeFromSuperview() + } else if let videoView = videoView { + addSubview(videoView.view, positioned: .below, relativeTo: self.subviews.first) + videoView.view.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + videoView.view.background = .blackTransparent + + videoView.setOnFirstFrameReceived({ [weak self] aspectRatio in + self?.videoView?.view.background = .black + self?.updateAspectRatio?(aspectRatio) + }) + + } + needsLayout = true + } + } + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + self.layer?.cornerRadius = .cornerRadius + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + for subview in subviews { + subview.frame = bounds + } - ctx.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: frame.height), options: CGGradientDrawingOptions()) + if let textView = disabledView?.subviews.first as? TextView { + let layout = textView.layout + layout?.measure(width: frame.width - 40) + textView.update(layout) + textView.center() + } + } + + func setIsPaused(_ paused: Bool, peer: TelegramUser?, animated: Bool) { + if paused { + if disabledView == nil { + let current = NSVisualEffectView() + current.material = .dark + current.state = .active + current.blendingMode = .withinWindow + current.wantsLayer = true + current.frame = bounds + + self.disabledView = current + addSubview(current) + + if animated { + current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } else { + self.disabledView?.frame = bounds + } + self.disabledView?.removeAllSubviews() + if let peer = peer { + let textView = TextView() + let layout = TextViewLayout(.initialize(string: L10n.callVideoPaused(peer.compactDisplayTitle), color: .white, font: .normal(.header)), maximumNumberOfLines: 1) + textView.userInteractionEnabled = false + textView.isSelectable = false + textView.update(layout) + self.disabledView?.addSubview(textView) + } + + } else { + if let disabledView = self.disabledView { + self.disabledView = nil + if animated { + disabledView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak disabledView] _ in + disabledView?.removeFromSuperview() + }) + } else { + disabledView.removeFromSuperview() + } + } + } + needsLayout = true } + + override var mouseDownCanMoveWindow: Bool { + return true + } } -private class PhoneCallWindowView : View { - //private var avatar:AvatarControl = AvatarControl +private class CallWindowView : View { fileprivate let imageView:TransformImageView = TransformImageView() - fileprivate let controls:NSVisualEffectView = NSVisualEffectView() - private let backgroundView:View = View() - let acceptControl:ImageButton = ImageButton() - let declineControl:ImageButton = ImageButton() + fileprivate let controls:View = View() + fileprivate let backgroundView:Control = Control() + let acceptControl:CallControl = CallControl(frame: .zero) + let declineControl:CallControl = CallControl(frame: .zero) + + + let b_Mute:CallControl = CallControl(frame: .zero) + let b_VideoCamera:CallControl = CallControl(frame: .zero) + let muteControl:ImageButton = ImageButton() - let closeMissedControl:ImageButton = ImageButton() private var textNameView: NSTextField = NSTextField() - private var statusTextView:NSTextField = NSTextField() + private var statusTimer: SwiftSignalKit.Timer? + + var status: CallControllerStatusValue = .text("") { + didSet { + if self.status != oldValue { + self.statusTimer?.invalidate() + if case .timer = self.status { + self.statusTimer = SwiftSignalKit.Timer(timeout: 0.5, repeat: true, completion: { [weak self] in + self?.updateStatus() + }, queue: Queue.mainQueue()) + self.statusTimer?.start() + self.updateStatus() + } else { + self.updateStatus() + } + } + } + } + + + private var statusTextView:NSTextField = NSTextField() private let secureTextView:TextView = TextView() - fileprivate let secureContainerView:NSView = NSView() + + fileprivate var incomingVideoView: IncomingVideoView? + fileprivate var outgoingVideoView: OutgoingVideoView + private var outgoingVideoViewRequested: Bool = false + private var incomingVideoViewRequested: Bool = false + + private var imageDimension: NSSize? = nil + + private var basicControls: View = View() + + private var state: CallState? + + private let fetching = MetaDisposable() + + var updateAspectRatio:((Float)->Void)? = nil + + required init(frame frameRect: NSRect) { + outgoingVideoView = OutgoingVideoView(frame: NSMakeRect(0, 0, frameRect.width, frameRect.height)) super.init(frame: frameRect) - addSubview(backgroundView) addSubview(imageView) - addSubview(controls) - self.backgroundColor = NSColor(0x000000, 0.8) - - // shadowView.layer?.shadowColor = NSColor.blackTransparent.cgColor - // controls.backgroundColor = NSColor(0x000000, 0.85) - controls.material = .dark - controls.blendingMode = .behindWindow + imageView.layer?.contentsGravity = .resizeAspectFill - secureContainerView.wantsLayer = true - secureContainerView.background = NSColor(0x000000, 0.75) - secureTextView.backgroundColor = .clear + addSubview(backgroundView) + addSubview(outgoingVideoView) + + controls.isEventLess = true + basicControls.isEventLess = true - secureContainerView.addSubview(secureTextView) + let shadow = NSShadow() + shadow.shadowBlurRadius = 4 + shadow.shadowColor = NSColor.black.withAlphaComponent(0.6) + shadow.shadowOffset = NSMakeSize(0, 0) + outgoingVideoView.shadow = shadow - addSubview(secureContainerView) + addSubview(controls) + controls.addSubview(basicControls) + self.backgroundColor = NSColor(0x000000, 0.8) + secureTextView.backgroundColor = .clear + secureTextView.isSelectable = false + secureTextView.userInteractionEnabled = false + addSubview(secureTextView) - backgroundView.backgroundColor = .clear + backgroundView.backgroundColor = NSColor(0x000000, 0.2) backgroundView.frame = NSMakeRect(0, 0, frameRect.width, frameRect.height) - acceptControl.autohighlight = false - acceptControl.set(image: theme.icons.callWindowAccept, for: .Normal) - acceptControl.sizeToFit() - - declineControl.autohighlight = false - declineControl.set(image: theme.icons.callWindowDecline, for: .Normal) - declineControl.sizeToFit() - - muteControl.autohighlight = false - muteControl.set(image: theme.icons.callWindowMute, for: .Normal) - muteControl.sizeToFit() - controls.addSubview(muteControl) - - closeMissedControl.autohighlight = false - closeMissedControl.set(image: theme.icons.callWindowCancel, for: .Normal) - closeMissedControl.setFrameSize(50,50) - closeMissedControl.layer?.cornerRadius = 25 - closeMissedControl.layer?.borderWidth = 2 - closeMissedControl.layer?.borderColor = theme.colors.border.cgColor - - + + self.addSubview(textNameView) + self.addSubview(statusTextView) + controls.addSubview(acceptControl) controls.addSubview(declineControl) - controls.addSubview(textNameView) - controls.addSubview(statusTextView) - controls.addSubview(closeMissedControl) - textNameView.font = .medium(.custom(18)) + textNameView.font = .medium(36) textNameView.drawsBackground = false textNameView.backgroundColor = .clear - textNameView.textColor = darkPallete.text + textNameView.textColor = nightAccentPalette.text textNameView.isSelectable = false textNameView.isEditable = false textNameView.isBordered = false @@ -110,348 +586,714 @@ private class PhoneCallWindowView : View { textNameView.alignment = .center textNameView.cell?.truncatesLastVisibleLine = true textNameView.lineBreakMode = .byTruncatingTail - statusTextView.font = .normal(.custom(15)) + statusTextView.font = .normal(18) statusTextView.drawsBackground = false statusTextView.backgroundColor = .clear - statusTextView.textColor = darkPallete.text + statusTextView.textColor = nightAccentPalette.text statusTextView.isSelectable = false statusTextView.isEditable = false statusTextView.isBordered = false statusTextView.focusRingType = .none + + imageView.setFrameSize(frameRect.size.width, frameRect.size.height) - // self.backgroundView.backgroundColor = .blackTransparent - let controlsSize = NSMakeSize(frameRect.width, 160) - controls.frame = NSMakeRect(0, frameRect.height - controlsSize.height, controlsSize.width, controlsSize.height) - // controls.backgroundColor = .blackTransparent - // controls.flip = false - //controls.material = .ultraDark - //controls.blendingMode = .behindWindow - //controls.state = .followsWindowActiveState - imageView.setFrameSize(frameRect.size.width, frameRect.size.height - controlsSize.height) - declineControl.setFrameOrigin(80, 30) - acceptControl.setFrameOrigin(frame.width - acceptControl.frame.width - 80, 30) + acceptControl.updateWithData(CallControlData(text: L10n.callAccept, isVisualEffect: false, icon: theme.icons.callWindowAccept, iconSize: NSMakeSize(60, 60), backgroundColor: .greenUI), animated: false) + declineControl.updateWithData(CallControlData(text: L10n.callDecline, isVisualEffect: false, icon: theme.icons.callWindowDecline, iconSize: NSMakeSize(60, 60), backgroundColor: .redUI), animated: false) - layer?.cornerRadius = 6 - closeMissedControl.isHidden = true - closeMissedControl.layer?.opacity = 0 + basicControls.addSubview(b_VideoCamera) + basicControls.addSubview(b_Mute) + + + var start: NSPoint? = nil + var resizeOutgoingVideoDirection: OutgoingVideoView.ResizeDirection? = nil + outgoingVideoView.overlay.set(handler: { [weak self] control in + guard let `self` = self, let window = self.window, self.outgoingVideoView.frame != self.bounds else { + start = nil + return + } + start = self.convert(window.mouseLocationOutsideOfEventStream, from: nil) + resizeOutgoingVideoDirection = self.outgoingVideoView.runResizer(at: self.outgoingVideoView.convert(window.mouseLocationOutsideOfEventStream, from: nil)) + + + }, for: .Down) + + outgoingVideoView.overlay.set(handler: { [weak self] control in + guard let `self` = self, let window = self.window, let startPoint = start else { + return + } + + let current = self.convert(window.mouseLocationOutsideOfEventStream, from: nil) + let difference = current - startPoint + + if let resizeDirection = resizeOutgoingVideoDirection { + let frame = self.outgoingVideoView.frame + let size: NSSize + let point: NSPoint + let value_w = difference.x + let value_h = difference.x * (frame.height / frame.width) + + switch resizeDirection { + case .topLeft: + size = NSMakeSize(frame.width - value_w, frame.height - value_h) + point = NSMakePoint(frame.minX + value_w, frame.minY + value_h) + case .topRight: + size = NSMakeSize(frame.width + value_w, frame.height + value_h) + point = NSMakePoint(frame.minX, frame.minY - value_h) + case .bottomLeft: + size = NSMakeSize(frame.width - value_w, frame.height - value_h) + point = NSMakePoint(frame.minX + value_w, frame.minY) + case .bottomRight: + size = NSMakeSize(frame.width + value_w, frame.height + value_h) + point = NSMakePoint(frame.minX, frame.minY) + } + if size.width < OutgoingVideoView.defaultSize.width || size.height < OutgoingVideoView.defaultSize.height { + return + } + if point.x < 20 || + point.y < 20 || + (self.frame.width - (point.x + size.width)) < 20 || + (self.frame.height - (point.y + size.height)) < 20 || + size.width > (defaultWindowSize.width - 40) || + size.height > (defaultWindowSize.height - 40) { + return + } + self.outgoingVideoView.updateFrame(CGRect(origin: point, size: size), animated: false) + + } else { + self.outgoingVideoView.setFrameOrigin(self.outgoingVideoView.frame.origin + difference) + } + start = current + + }, for: .MouseDragging) + + outgoingVideoView.overlay.set(handler: { [weak self] control in + guard let `self` = self, let _ = start else { + return + } + + + let frame = self.outgoingVideoView.frame + var point = self.outgoingVideoView.frame.origin + + + var size = frame.size + if let event = NSApp.currentEvent, event.clickCount == 2 { + + let inside = self.outgoingVideoView.convert(event.locationInWindow, from: nil) + + if frame.width > OutgoingVideoView.defaultSize.width { + size = OutgoingVideoView.defaultSize + point.x += floor(inside.x / 2) + point.y += floor(inside.y / 2) + } else { + size = NSMakeSize(defaultWindowSize.width - 40, (defaultWindowSize.width - 40) * (OutgoingVideoView.defaultSize.height / OutgoingVideoView.defaultSize.width)) + point.x -= floor(inside.x / 2) + point.y -= floor(inside.y / 2) + } + + } + + if (size.width + point.x) > self.frame.width - 20 { + point.x = self.frame.width - size.width - 20 + } else if point.x - 20 < 0 { + point.x = 20 + } + + if (size.height + point.y) > self.frame.height - 20 { + point.y = self.frame.height - size.height - 20 + } else if point.y - 20 < 0 { + point.y = 20 + } + + let updatedRect = CGRect(origin: point, size: size) + self.outgoingVideoView.updateFrame(updatedRect, animated: true) + + start = nil + resizeOutgoingVideoDirection = nil + }, for: .Up) + + outgoingVideoView.frame = NSMakeRect(frame.width - outgoingVideoView.frame.width - 20, frame.height - 140 - outgoingVideoView.frame.height, outgoingVideoView.frame.width, outgoingVideoView.frame.height) + + } + + private func mainControlY(_ control: NSView) -> CGFloat { + return controls.frame.height - control.frame.height - 40 + } + + private func mainControlCenter(_ control: NSView) -> CGFloat { + return floorToScreenPixels(backingScaleFactor, (controls.frame.width - control.frame.width) / 2) + } + + private var previousFrame: NSRect = .zero + + override func layout() { + super.layout() + + backgroundView.frame = bounds + imageView.frame = bounds + + incomingVideoView?.frame = bounds + + + textNameView.setFrameSize(NSMakeSize(controls.frame.width - 40, 36)) + textNameView.centerX(y: 50) + statusTextView.setFrameSize(statusTextView.sizeThatFits(NSMakeSize(controls.frame.width - 40, 25))) + statusTextView.centerX(y: textNameView.frame.maxY + 4) + + secureTextView.centerX(y: statusTextView.frame.maxY + 4) + + let controlsSize = NSMakeSize(frame.width, 220) + controls.frame = NSMakeRect(0, frame.height - controlsSize.height, controlsSize.width, controlsSize.height) + + basicControls.frame = controls.bounds + + guard let state = self.state else { + return + } + + switch state.state { + case .ringing, .requesting, .terminating, .terminated: + let videoFrame = bounds + outgoingVideoView.updateFrame(videoFrame, animated: false) + default: + var point = outgoingVideoView.frame.origin + + let size = outgoingVideoView.frame.size + + if previousFrame.size != frame.size { + + point.x += (frame.width - point.x) - (previousFrame.width - point.x) + point.y += (frame.height - point.y) - (previousFrame.height - point.y) + + point.x = max(min(frame.width - size.width - 20, point.x), 20) + point.y = max(min(frame.height - size.height - 20, point.y), 20) + } + + let videoFrame = NSMakeRect(point.x, point.y, size.width, size.height) + outgoingVideoView.updateFrame(videoFrame, animated: false) + } + + + switch state.state { + case .connecting, .active, .requesting: + let activeViews = self.allActiveControlsViews + let restWidth = self.allControlRestWidth + var x: CGFloat = floor(restWidth / 2) + for activeView in activeViews { + activeView.setFrameOrigin(NSMakePoint(x, mainControlY(acceptControl))) + x += activeView.size.width + 45 + } + case .terminating: + acceptControl.setFrameOrigin(frame.width - acceptControl.frame.width - 80, mainControlY(acceptControl)) + case let .terminated(_, reason, _): + if let reason = reason, reason.recall { + let activeViews = self.activeControlsViews + let restWidth = self.controlRestWidth + var x: CGFloat = floor(restWidth / 2) + for activeView in activeViews { + activeView.setFrameOrigin(NSMakePoint(x, 0)) + x += activeView.size.width + 45 + } + acceptControl.setFrameOrigin(frame.width - acceptControl.frame.width - 80, mainControlY(acceptControl)) + declineControl.setFrameOrigin(80, mainControlY(acceptControl)) + } else { + let activeViews = self.allActiveControlsViews + let restWidth = self.allControlRestWidth + var x: CGFloat = floor(restWidth / 2) + for activeView in activeViews { + activeView.setFrameOrigin(NSMakePoint(x, mainControlY(acceptControl))) + x += activeView.size.width + 45 + } + } + case .ringing: + declineControl.setFrameOrigin(80, mainControlY(declineControl)) + acceptControl.setFrameOrigin(frame.width - acceptControl.frame.width - 80, mainControlY(acceptControl)) + + let activeViews = self.activeControlsViews + + let restWidth = self.controlRestWidth + var x: CGFloat = floor(restWidth / 2) + for activeView in activeViews { + activeView.setFrameOrigin(NSMakePoint(x, 0)) + x += activeView.size.width + 45 + } + + case .waiting: + break + case .reconnecting(_, _, _): + break + } + + if let dimension = imageDimension { + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: dimension, boundingSize: self.imageView.frame.size, intrinsicInsets: NSEdgeInsets()) + self.imageView.set(arguments: arguments) + } + - + previousFrame = self.frame } + var activeControlsViews:[CallControl] { + return basicControls.subviews.filter { + !$0.isHidden + }.compactMap { $0 as? CallControl } + } - - override func layout() { - super.layout() - - textNameView.setFrameSize(NSMakeSize(controls.frame.width - 40, 24)) - textNameView.centerX(y: controls.frame.height - textNameView.frame.height - 20) - statusTextView.setFrameSize(statusTextView.sizeThatFits(NSMakeSize(controls.frame.width - 40, 30))) - statusTextView.centerX(y: controls.frame.height - textNameView.frame.height - 20 - 20) - - secureTextView.center() - secureTextView.setFrameOrigin(secureTextView.frame.minX + 2, secureTextView.frame.minY) - secureContainerView.centerX(y: frame.height - 170 - secureContainerView.frame.height) - muteControl.setFrameOrigin(frame.width - 60 - muteControl.frame.width, 30 + floorToScreenPixels((declineControl.frame.height - muteControl.frame.height)/2)) - - closeMissedControl.setFrameOrigin(80, 30) - + var allActiveControlsViews: [CallControl] { + let values = basicControls.subviews.filter { + !$0.isHidden + }.compactMap { $0 as? CallControl } + return values + controls.subviews.filter { + $0 is CallControl && !$0.isHidden + }.compactMap { $0 as? CallControl } + } + + var controlRestWidth: CGFloat { + return controls.frame.width - CGFloat(activeControlsViews.count - 1) * 45 - CGFloat(activeControlsViews.count) * 50 } + var allControlRestWidth: CGFloat { + return controls.frame.width - CGFloat(allActiveControlsViews.count - 1) * 45 - CGFloat(allActiveControlsViews.count) * 50 + } + func updateName(_ name:String) { textNameView.stringValue = name needsLayout = true } - func setDuration(_ duration:TimeInterval) { - statusTextView.stringValue = String.durationTransformed(elapsed: Int(duration)) + func updateStatus() { + var statusText: String = "" + switch self.status { + case let .text(text): + statusText = text + case let .timer(referenceTime): + let duration = Int32(CFAbsoluteTimeGetCurrent() - referenceTime) + 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]) + } + statusText = durationString + } + statusTextView.stringValue = statusText + statusTextView.alignment = .center needsLayout = true } - func updateState(_ state:CallSessionState, animated: Bool) { - switch state { - case .accepting: - statusTextView.stringValue = tr(.callStatusConnecting) - case .active(_, let visual, _): - let layout = TextViewLayout(.initialize(string: ObjcUtils.callEmojies(visual), color: .black, font: .normal(.custom(16))), alignment: .center) + func updateControlsVisibility() { + if let state = state { + switch state.state { + case .active: + self.backgroundView.change(opacity: self.mouseInside() ? 1.0 : 0.0) + self.controls.change(opacity: self.mouseInside() ? 1.0 : 0.0) + self.textNameView._change(opacity: self.mouseInside() ? 1.0 : 0.0) + self.secureTextView._change(opacity: self.mouseInside() ? 1.0 : 0.0) + self.statusTextView._change(opacity: self.mouseInside() ? 1.0 : 0.0) + default: + self.backgroundView.change(opacity: 1.0) + self.controls.change(opacity: 1.0) + self.textNameView._change(opacity: 1.0) + self.secureTextView._change(opacity: 1.0) + self.statusTextView._change(opacity: 1.0) + } + } + + } + + func updateState(_ state:CallState, session:PCallSession, accountPeer: Peer?, peer: TelegramUser?, animated: Bool) { + + let inputCameraIsActive: Bool + switch state.videoState { + case .active: + inputCameraIsActive = !state.isOutgoingVideoPaused + case .outgoingRequested: + inputCameraIsActive = !state.isOutgoingVideoPaused + default: + inputCameraIsActive = false + } + self.b_VideoCamera.updateWithData(CallControlData(text: L10n.callCamera, isVisualEffect: !inputCameraIsActive, icon: inputCameraIsActive ? theme.icons.callWindowVideoActive : theme.icons.callWindowVideo, iconSize: NSMakeSize(50, 50), backgroundColor: .white), animated: false) + + self.b_Mute.updateWithData(CallControlData(text: L10n.callMute, isVisualEffect: !state.isMuted, icon: state.isMuted ? theme.icons.callWindowMuteActive : theme.icons.callWindowMute, iconSize: NSMakeSize(50, 50), backgroundColor: .white), animated: false) + + self.b_VideoCamera.isHidden = !session.isVideoPossible + self.b_VideoCamera.updateEnabled(state.videoIsAvailable, animated: animated) + + self.state = state + self.status = state.state.statusText(accountPeer, state.videoState) + + switch state.state { + case let .active(_, _, visual): + let layout = TextViewLayout(.initialize(string: ObjcUtils.callEmojies(visual), color: .black, font: .normal(16.0)), alignment: .center) layout.measure(width: .greatestFiniteMagnitude) secureTextView.update(layout) - secureContainerView.isHidden = false - secureContainerView.setFrameSize(NSMakeSize(layout.layoutSize.width + 16, layout.layoutSize.height + 10)) - secureContainerView.layer?.cornerRadius = secureContainerView.frame.height / 2 + default: + break + } + + + switch state.videoState { + case .active, .incomingRequested: + if !self.incomingVideoViewRequested { + self.incomingVideoViewRequested = true + session.makeIncomingVideoView(completion: { [weak self] view in + if let view = view, let `self` = self { + if self.incomingVideoView == nil { + self.incomingVideoView = IncomingVideoView(frame: self.imageView.frame) + self.imageView.addSubview(self.incomingVideoView!) + } + self.incomingVideoView?.updateAspectRatio = self.updateAspectRatio + self.incomingVideoView?.videoView = view + self.needsLayout = true + } + }) + } + default: + break + } + + + switch state.videoState { + case let .outgoingRequested(possible), let .active(possible): + if !self.outgoingVideoViewRequested { + self.outgoingVideoViewRequested = true + session.makeOutgoingVideoView(completion: { [weak self] view in + if let view = view, let `self` = self { + self.outgoingVideoView.videoView = (view, possible) + self.needsLayout = true + } + }) + } + default: + break + } + + switch state.state { + case .active, .connecting, .requesting: + self.acceptControl.isHidden = true + let activeViews = self.allActiveControlsViews + let restWidth = self.allControlRestWidth + var x: CGFloat = floor(restWidth / 2) + for activeView in activeViews { + activeView._change(pos: NSMakePoint(x, mainControlY(acceptControl)), animated: animated, duration: 0.3, timingFunction: .spring) + x += activeView.size.width + 45 + } + declineControl.updateWithData(CallControlData(text: L10n.callDecline, isVisualEffect: false, icon: theme.icons.callWindowDeclineSmall, iconSize: NSMakeSize(50, 50), backgroundColor: .redUI), animated: animated) - statusTextView.stringValue = tr(.callStatusConnecting) case .ringing: - statusTextView.stringValue = tr(.callStatusCalling) - case .terminated(let error, _): - switch error { - case .ended(let reason): + break + case .terminated(_, let reason, _): + if let reason = reason, reason.recall { + let activeViews = self.activeControlsViews + let restWidth = self.controlRestWidth + var x: CGFloat = floor(restWidth / 2) + for activeView in activeViews { + activeView._change(pos: NSMakePoint(x, 0), animated: animated, duration: 0.3, timingFunction: .spring) + x += activeView.size.width + 45 + } + acceptControl.updateWithData(CallControlData(text: L10n.callRecall, isVisualEffect: false, icon: theme.icons.callWindowAccept, iconSize: NSMakeSize(60, 60), backgroundColor: .greenUI), animated: animated) - switch reason { - case .busy: - statusTextView.stringValue = tr(.callStatusBusy) - case .missed: - statusTextView.stringValue = tr(.callStatusEnded) - default: - statusTextView.stringValue = tr(.callStatusEnded) - acceptControl.isEnabled = false - acceptControl.change(opacity: 0.8) + declineControl.updateWithData(CallControlData(text: L10n.callClose, isVisualEffect: false, icon: theme.icons.callWindowCancel, iconSize: NSMakeSize(60, 60), backgroundColor: .redUI), animated: animated) + + declineControl.change(pos: NSMakePoint(frame.width - acceptControl.frame.width - 80, mainControlY(acceptControl)), animated: animated, duration: 0.3, timingFunction: .spring) + acceptControl.change(pos: NSMakePoint(80, mainControlY(acceptControl)), animated: animated, duration: 0.3, timingFunction: .spring) + + incomingVideoView?.removeFromSuperview() + incomingVideoView = nil + + } else { + self.acceptControl.isHidden = true + + declineControl.updateWithData(CallControlData(text: L10n.callDecline, isVisualEffect: false, icon: theme.icons.callWindowDeclineSmall, iconSize: NSMakeSize(50, 50), backgroundColor: .redUI), animated: false) + + let activeViews = self.allActiveControlsViews + let restWidth = self.allControlRestWidth + var x: CGFloat = floor(restWidth / 2) + for activeView in activeViews { + activeView._change(pos: NSMakePoint(x, mainControlY(acceptControl)), animated: animated, duration: 0.3, timingFunction: .spring) + x += activeView.size.width + 45 } - case .error: - statusTextView.stringValue = tr(.callStatusFailed) - acceptControl.isEnabled = false - acceptControl.change(opacity: 0.8) + _ = allActiveControlsViews.map { + $0.updateEnabled(false, animated: animated) + } } - - - case .requesting(let ringing): - statusTextView.stringValue = !ringing ? tr(.callStatusRequesting) : tr(.callStatusRinging) - default: + case .terminating: + break + case .waiting: + break + case .reconnecting(_, _, _): break } - switch state { - case .active, .accepting, .requesting: - - declineControl.change(opacity: 0, animated: animated, completion: { [weak self] complete in - self?.declineControl.isHidden = true - }) - acceptControl.change(pos: NSMakePoint(floorToScreenPixels((frame.width - acceptControl.frame.width) / 2), 30), animated: animated) - acceptControl.set(image: theme.icons.callWindowDecline, for: .Normal) - - muteControl.isHidden = false - muteControl.change(opacity: 1, animated: animated) - - closeMissedControl.change(opacity: 0, animated: animated, completion: { [weak self] completed in - self?.closeMissedControl.isHidden = true - }) + + outgoingVideoView.setIsPaused(state.isOutgoingVideoPaused, animated: animated) + + incomingVideoView?.setIsPaused(state.remoteVideoState == .inactive, peer: peer, animated: animated) + + + + switch state.state { + case .ringing, .requesting, .terminating, .terminated: + outgoingVideoView.isEventLess = true + default: + switch state.videoState { + case .outgoingRequested: + outgoingVideoView.isEventLess = true + case .incomingRequested: + outgoingVideoView.isEventLess = false + case .active: + outgoingVideoView.isEventLess = false + default: + outgoingVideoView.isEventLess = true + } + } + + var point = outgoingVideoView.frame.origin + var size = outgoingVideoView.frame.size + + if !outgoingVideoView.isEventLess { + if point == .zero { + point = NSMakePoint(frame.width - OutgoingVideoView.defaultSize.width - 20, frame.height - 140 - OutgoingVideoView.defaultSize.height) + size = OutgoingVideoView.defaultSize + } + } else { + point = .zero + size = frame.size + } + let videoFrame = CGRect(origin: point, size: size) + outgoingVideoView.updateFrame(videoFrame, animated: animated) + + + if let peer = peer { + updatePeerUI(peer, session: session) + } + + needsLayout = true + } + + + private func updatePeerUI(_ user:TelegramUser, session: PCallSession) { + + let id = user.profileImageRepresentations.first?.resource.id.hashValue ?? Int(user.id.toInt64()) + + let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: MediaId.Id(id)), representations: user.profileImageRepresentations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + + if let dimension = user.profileImageRepresentations.last?.dimensions.size { - case .ringing: - declineControl.isHidden = false - muteControl.change(opacity: 0, animated: animated, completion: { [weak self] complete in - if complete { - self?.muteControl.isHidden = true - } - }) - acceptControl.set(image: theme.icons.callWindowAccept, for: .Normal) - acceptControl.change(pos: NSMakePoint(frame.width - acceptControl.frame.width - 80, 30), animated: animated) - declineControl.change(opacity: 1, animated: animated) + self.imageDimension = dimension - closeMissedControl.change(opacity: 0, animated: animated, completion: { [weak self] completed in - self?.closeMissedControl.isHidden = true + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: dimension, boundingSize: self.imageView.frame.size, intrinsicInsets: NSEdgeInsets()) + self.imageView.setSignal(signal: cachedMedia(media: media, arguments: arguments, scale: self.backingScaleFactor), clearInstantly: true) + self.imageView.setSignal(chatMessagePhoto(account: session.account, imageReference: ImageMediaReference.standalone(media: media), peer: user, scale: self.backingScaleFactor), clearInstantly: false, animate: true, cacheImage: { result in + cacheMedia(result, media: media, arguments: arguments, scale: System.backingScale) }) + self.imageView.set(arguments: arguments) - case .terminated(let reason, _): - - let recall:Bool - - switch reason { - case .ended(let reason): - switch reason { - case .busy: - recall = true - default: - recall = false - } - case .error(let reason): - switch reason { - case .disconnected: - recall = true - default: - recall = false - } + if let reference = PeerReference(user) { + fetching.set(fetchedMediaResource(mediaBox: session.account.postbox.mediaBox, reference: .avatar(peer: reference, resource: media.representations.last!.resource)).start()) } - if recall { - closeMissedControl.isHidden = false - closeMissedControl.change(opacity: 1, animated: animated) - - muteControl.change(opacity: 0, animated: animated, completion: { [weak self] complete in - if complete { - self?.muteControl.isHidden = true - } - }) - - acceptControl.set(image: theme.icons.callWindowAccept, for: .Normal) - acceptControl.change(pos: NSMakePoint(frame.width - acceptControl.frame.width - 80, 30), animated: animated) - } - case .dropping: - break + } else { + self.imageDimension = nil + self.imageView.setSignal(signal: generateEmptyRoundAvatar(self.imageView.frame.size, font: .avatar(90.0), account: session.account, peer: user) |> map { TransformImageResult($0, true) }) } + self.updateName(user.displayTitle) - needsLayout = true } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + deinit { + fetching.dispose() + statusTimer?.invalidate() + } } -class PhoneCallWindowController { - let window:NSWindow +class CallWindowController { + let window:Window + fileprivate var view:CallWindowView + let updateLocalizationAndThemeDisposable = MetaDisposable() fileprivate var session:PCallSession! { didSet { + view = CallWindowView(frame: NSMakeRect(0, 0, window.frame.width, window.frame.height)) + window.contentView = view + first = true sessionDidUpdated() + + if let monitor = eventLocalMonitor { + NSEvent.removeMonitor(monitor) + } + if let monitor = eventGlobalMonitor { + NSEvent.removeMonitor(monitor) + } + + eventLocalMonitor = NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved, .mouseEntered, .mouseExited, .leftMouseDown, .leftMouseUp], handler: { [weak self] event in + self?.view.updateControlsVisibility() + return event + }) + // + eventGlobalMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved, .mouseEntered, .mouseExited, .leftMouseDown, .leftMouseUp], handler: { [weak self] event in + self?.view.updateControlsVisibility() + }) + } } var first:Bool = true private func sessionDidUpdated() { - view.secureContainerView.isHidden = true - peerDisposable.set((session.account.viewTracker.peerView( session.peerId) |> deliverOnMainQueue).start(next: { [weak self] peerView in - if let strongSelf = self { - if let user = peerView.peers[peerView.peerId] as? TelegramUser { - strongSelf.updatePeerUI(user) - } + + + let account = session.account + + let accountPeer: Signal = session.sharedContext.activeAccounts |> mapToSignal { accounts in + if accounts.accounts.count == 1 { + return .single(nil) + } else { + return account.postbox.loadedPeerWithId(account.peerId) |> map(Optional.init) } - })) + } + + let peer = session.account.viewTracker.peerView(session.peerId) |> map { + return $0.peers[$0.peerId] as? TelegramUser + } - stateDisposable.set((session.state.get() |> deliverOnMainQueue).start(next: { [weak self] state in + stateDisposable.set(combineLatest(queue: .mainQueue(), session.state, accountPeer, peer).start(next: { [weak self] state, accountPeer, peer in if let strongSelf = self { - strongSelf.applyState(state, animated: !strongSelf.first) + strongSelf.applyState(state, session: strongSelf.session!, accountPeer: accountPeer, peer: peer, animated: !strongSelf.first) strongSelf.first = false + } })) - durationDisposable.set(session.durationPromise.get().start(next: { [weak self] duration in - self?.view.setDuration(duration) - })) + view.updateAspectRatio = { [weak self] aspectRatio in + self?.updateWindowAspectRatio(aspectRatio) + } + } - fileprivate let view:PhoneCallWindowView - private var state:CallSessionState? = nil + private var state:CallState? = nil private let disposable:MetaDisposable = MetaDisposable() private let stateDisposable = MetaDisposable() private let durationDisposable = MetaDisposable() private let recallDisposable = MetaDisposable() - private let peerDisposable = MetaDisposable() private let keyStateDisposable = MetaDisposable() + private let readyDisposable = MetaDisposable() + + + private let ready: ValuePromise = ValuePromise(ignoreRepeated: true) + + fileprivate var eventLocalMonitor: Any? + fileprivate var eventGlobalMonitor: Any? + + init(_ session:PCallSession) { self.session = session - - let size = NSMakeSize(300, 460) + let size = defaultWindowSize if let screen = NSScreen.main { - self.window = NSWindow(contentRect: NSMakeRect(floorToScreenPixels((screen.frame.width - size.width) / 2), floorToScreenPixels((screen.frame.height - size.height) / 2), size.width, size.height), styleMask: [.titled, .fullSizeContentView], backing: .buffered, defer: false, screen: screen) - self.window.level = .screenSaver + self.window = Window(contentRect: NSMakeRect(floorToScreenPixels(System.backingScale, (screen.frame.width - size.width) / 2), floorToScreenPixels(System.backingScale, (screen.frame.height - size.height) / 2), size.width, size.height), styleMask: [.fullSizeContentView, .borderless, .resizable, .miniaturizable, .titled], backing: .buffered, defer: true, screen: screen) + self.window.minSize = size + self.window.isOpaque = true } else { fatalError("screen not found") } - view = PhoneCallWindowView(frame: NSMakeRect(0, 0, size.width, size.height)) + view = CallWindowView(frame: NSMakeRect(0, 0, size.width, size.height)) NotificationCenter.default.addObserver(self, selector: #selector(windowDidBecomeKey), name: NSWindow.didBecomeKeyNotification, object: window) NotificationCenter.default.addObserver(self, selector: #selector(windowDidResignKey), name: NSWindow.didResignKeyNotification, object: window) - view.acceptControl.set(handler: { [weak self] _ in if let state = self?.state { - switch state { - case .ringing: - self?.session.acceptCallSession() - case .terminated(let reason, _): - - let recall:Bool - switch reason { - case .ended(let reason): - switch reason { - case .busy, .missed: - recall = true - default: - recall = false - } - case .error(let reason): - switch reason { - case .disconnected: - recall = true - default: - recall = false - } - } - - if recall { + switch state.state { + case .ringing: + self?.session.acceptCallSession() + case .terminated(_, let reason, _): + if let reason = reason, reason.recall { self?.recall() - } else { - self?.session.hangUpCurrentCall() } default: - self?.session.hangUpCurrentCall() + break } - - } else { - closeCall() } }, for: .Click) - view.closeMissedControl.set(handler: { _ in - closeCall() + + self.view.b_VideoCamera.set(handler: { [weak self] _ in + if let session = self?.session, let callState = self?.state { + + switch callState.videoState { + case .incomingRequested: + session.acceptVideo() + default: + if !session.isVideo { + session.requestVideo() + } else { + session.toggleOutgoingVideo() + } + } + } }, for: .Click) - view.muteControl.set(handler: { [weak self] control in - if let session = self?.session, let control = control as? ImageButton { + self.view.b_Mute.set(handler: { [weak self] _ in + if let session = self?.session { session.toggleMute() - control.set(image: session.isMute ? theme.icons.callWindowUnmute : theme.icons.callWindowMute, for: .Normal) } }, for: .Click) - view.muteControl.set(image: session.isMute ? theme.icons.callWindowUnmute : theme.icons.callWindowMute, for: .Normal) - view.declineControl.set(handler: { [weak self] _ in - self?.session.hangUpCurrentCall() - }, for: .Click) - - - /* view.deviceSettingsButton.set(handler: { [weak self] _ in - - if let session = self?.session, let strongSelf = self { - _ = (combineLatest(session.inputDevices(), session.outputDevices(), session.currentInputDeviceId(), session.currentOutputDeviceId()) |> deliverOnMainQueue).start(next: { [weak strongSelf] input, output, currentInputId, currentOutputId in - - var settingsWindow:NSWindow! - - let deviceSettings = NativeCallSettingsViewController(inputDevices: input, outputDevices: output, currentInputDeviceId: currentInputId, currentOutputDeviceId: currentOutputId, onSave: { [weak strongSelf] (inputDevice, outputDevice) in - strongSelf?.session.setCurrentInputDevice(inputDevice) - strongSelf?.session.setCurrentOutputDevice(outputDevice) - }, onCancel: { - }) - - settingsWindow = NSWindow(contentViewController: deviceSettings) - settingsWindow.styleMask = [.borderless] - //settingsWindow.appearance = mainWindow.appearance - settingsWindow.contentView?.wantsLayer = true - settingsWindow.contentView?.layer?.cornerRadius = 4 - //settingsWindow.contentView?.background = theme.colors.background - //settingsWindow.backgroundColor = theme.colors.background - //settingsWindow.isOpaque = false - - strongSelf?.window.beginSheet(settingsWindow, completionHandler: { response in - - }) - }) - - + if let _ = self?.state { + self?.session.hangUpCurrentCall() + } else { + closeCall() } - }, for: .Click) - */ + self.window.contentView = view - self.window.backgroundColor = .clear - self.window.contentView?.layer?.cornerRadius = 4 self.window.titlebarAppearsTransparent = true - self.window.isMovableByWindowBackground = true sessionDidUpdated() + + + window.set(mouseHandler: { [weak self] _ -> KeyHandlerResult in + guard let `self` = self else {return .rejected} + self.view.updateControlsVisibility() + return .rejected + }, with: self.view, for: .mouseMoved) + + window.set(mouseHandler: { [weak self] _ -> KeyHandlerResult in + guard let `self` = self else {return .rejected} + self.view.updateControlsVisibility() + return .rejected + }, with: self.view, for: .mouseEntered) + + window.set(mouseHandler: { [weak self] _ -> KeyHandlerResult in + guard let `self` = self else {return .rejected} + self.view.updateControlsVisibility() + return .rejected + }, with: self.view, for: .mouseExited) + + + + self.view.backgroundView.set(handler: { [weak self] _ in + self?.view.updateControlsVisibility() + + }, for: .Click) + + window.animationBehavior = .utilityWindow } private func recall() { - let account = session.account - let peerId = session.peerId - - recallDisposable.set((phoneCall(account, peerId: peerId, ignoreSame: true) |> deliverOnMainQueue).start(next: { [weak self] result in + recallDisposable.set((phoneCall(account: session.account, sharedContext: session.sharedContext, peerId: session.peerId, ignoreSame: true) |> deliverOnMainQueue).start(next: { [weak self] result in switch result { case let .success(session): self?.session = session @@ -463,15 +1305,37 @@ class PhoneCallWindowController { })) } + private var aspectRatio: Float = 0 + private func updateWindowAspectRatio(_ aspectRatio: Float) { + if aspectRatio > 0 && self.aspectRatio != aspectRatio, let screen = window.screen { + let closestSide: CGFloat + if aspectRatio > 1 { + closestSide = min(window.frame.width, window.frame.height) + } else { + closestSide = max(window.frame.width, window.frame.height) + } + var updatedSize = NSMakeSize(closestSide * CGFloat(aspectRatio), closestSide) + + if screen.frame.width <= updatedSize.width || screen.frame.height <= updatedSize.height { + let closest = min(updatedSize.width, updatedSize.height) + updatedSize = NSMakeSize(closest * CGFloat(aspectRatio), closest) + } + + window.setFrame(CGRect(origin: window.frame.origin.offsetBy(dx: (window.frame.width - updatedSize.width) / 2, dy: (window.frame.height - updatedSize.height) / 2), size: updatedSize), display: true, animate: true) + window.aspectRatio = updatedSize + self.aspectRatio = aspectRatio + } + } + @objc open func windowDidBecomeKey() { keyStateDisposable.set(nil) } @objc open func windowDidResignKey() { - keyStateDisposable.set((session.state.get() |> deliverOnMainQueue).start(next: { [weak self] state in + keyStateDisposable.set((session.state |> deliverOnMainQueue).start(next: { [weak self] state in if let strongSelf = self { - if case .active = state, !strongSelf.window.isKeyWindow { + if case .active = state.state, !strongSelf.session.isVideo, !strongSelf.window.isKeyWindow { closeCall() } @@ -479,165 +1343,139 @@ class PhoneCallWindowController { })) } - private func applyState(_ state:CallSessionState, animated: Bool) { + private func applyState(_ state:CallState, session: PCallSession, accountPeer: Peer?, peer: TelegramUser?, animated: Bool) { self.state = state - view.updateState(state, animated: animated) - switch state { + view.updateState(state, session: session, accountPeer: accountPeer, peer: peer, animated: animated) + session.sharedContext.showCallHeader(with: session) + switch state.state { case .ringing: break - case .accepting: + case .connecting: break case .requesting: break case .active: - session.account.context.showCallHeader(with: session) - case .dropping: break - case .terminated(let error, _): + case .terminating: + break + case .terminated(_, let error, _): switch error { - case .ended(let reason): - switch reason { - case .hungUp, .missed: - closeCall(1.0) - default: - break - } - case let .error(error): - closeCall(1.0) - disposable.set((session.account.viewTracker.peerView( session.peerId) |> deliverOnMainQueue).start(next: { peerView in - if let peer = peerViewMainPeer(peerView) { - switch error { - case .privacyRestricted: - alert(for: mainWindow, info: tr(.callPrivacyErrorMessage(peer.compactDisplayTitle))) - case .notSupportedByPeer: - alert(for: mainWindow, info: tr(.callParticipantVersionOutdatedError(peer.compactDisplayTitle))) - case .serverProvided(let serverError): - alert(for: mainWindow, info: serverError) - case .generic: - alert(for: mainWindow, info: tr(.callUndefinedError)) - default: - break - } - + case .ended(let reason)?: + break + case let .error(error)?: + disposable.set((session.account.postbox.loadedPeerWithId(session.peerId) |> deliverOnMainQueue).start(next: { peer in + switch error { + case .privacyRestricted: + alert(for: mainWindow, info: L10n.callPrivacyErrorMessage(peer.compactDisplayTitle)) + case .notSupportedByPeer: + alert(for: mainWindow, info: L10n.callParticipantVersionOutdatedError(peer.compactDisplayTitle)) + case .serverProvided(let serverError): + alert(for: mainWindow, info: serverError) + case .generic: + alert(for: mainWindow, info: L10n.callUndefinedError) + default: + break } })) + case .none: + break } - - - + case .waiting: + break + case .reconnecting: + break } + self.ready.set(true) } deinit { + cleanup() + } + + fileprivate func cleanup() { disposable.dispose() stateDisposable.dispose() durationDisposable.dispose() recallDisposable.dispose() - peerDisposable.dispose() keyStateDisposable.dispose() + readyDisposable.dispose() updateLocalizationAndThemeDisposable.dispose() NotificationCenter.default.removeObserver(self) + self.window.removeAllHandlers(for: self.view) } func show() { - var first: Bool = true - disposable.set((session.account.viewTracker.peerView( session.peerId) |> take(1) |> deliverOnMainQueue).start(next: { [weak self] peerView in - if let strongSelf = self { - if let user = peerView.peers[peerView.peerId] as? TelegramUser { - strongSelf.updatePeerUI(user) - } - if first { - first = false - strongSelf.window.makeKeyAndOrderFront(self) - strongSelf.view.layer?.animateScaleSpring(from: 0.2, to: 1.0, duration: 0.4) - strongSelf.view.layer?.animateAlpha(from: 0.2, to: 1.0, duration: 0.3) - } + let ready = self.ready.get() |> filter { $0 } |> take(1) + + readyDisposable.set(ready.start(next: { [weak self] _ in + if self?.window.isVisible == false { + self?.window.makeKeyAndOrderFront(self) + self?.view.layer?.animateScaleSpring(from: 0.2, to: 1.0, duration: 0.4) + self?.view.layer?.animateAlpha(from: 0.2, to: 1.0, duration: 0.3) } })) - } - - private func updatePeerUI(_ user:TelegramUser) { - - let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: user.profileImageRepresentations) - +} - - if let dimension = user.profileImageRepresentations.last?.dimensions { - let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: dimension, boundingSize: view.imageView.frame.size, intrinsicInsets: NSEdgeInsets()) - view.imageView.setSignal(signal: cachedMedia(media: media, size: arguments.imageSize, scale: view.backingScaleFactor)) - view.imageView.setSignal(account: session.account, signal: chatMessagePhoto(account: session.account, photo: media, scale: view.backingScaleFactor), clearInstantly: false, animate: true, cacheImage: { [weak self] image in - if let strongSelf = self { - return cacheMedia(signal: image, media: media, size: arguments.imageSize, scale: strongSelf.view.backingScaleFactor) - } else { - return .complete() - } - }) - view.imageView.set(arguments: arguments) +private let controller:Atomic = Atomic(value: nil) +private let closeDisposable = MetaDisposable() +func makeKeyAndOrderFrontCallWindow() -> Bool { + return controller.with { value in + if let value = value { + value.window.makeKeyAndOrderFront(nil) + return true } else { - view.imageView.setSignal(signal: generateEmptyRoundAvatar(view.imageView.frame.size, font: .avatar(.custom(90)), account: session.account, peer: user)) + return false } - - - - - _ = chatMessagePhotoInteractiveFetched(account: session.account, photo: media).start() - - - view.updateName(user.displayTitle) - } - } -private var controller:PhoneCallWindowController? - - -func showPhoneCallWindow(_ session:PCallSession) { - Queue.mainQueue().async { - controller?.session.hangUpCurrentCall() - if let controller = controller { - controller.session = session - } else { - controller = PhoneCallWindowController(session) - controller?.show() +func showCallWindow(_ session:PCallSession) { + _ = controller.modify { controller in + if session.peerId != controller?.session.peerId { + controller?.session.hangUpCurrentCall() + if let controller = controller { + controller.session = session + return controller + } else { + return CallWindowController(session) + } } - + return controller } + controller.with { $0?.show() } + + let signal = session.canBeRemoved |> deliverOnMainQueue + closeDisposable.set(signal.start(next: { value in + if value { + closeCall() + } + })) } -private let closeDisposable = MetaDisposable() -func closeCall(_ timeout:TimeInterval? = nil) { - var signal = Signal.single(Void()) |> deliverOnMainQueue - if let timeout = timeout { - signal = signal |> delay(timeout, queue: Queue.mainQueue()) - } - closeDisposable.set(signal.start(completed: { - controller?.window.styleMask = [.borderless] - controller?.view.controls.removeFromSuperview() - controller?.window.contentView?._change(opacity: 0.0, removeOnCompletion: false, completion: { completed in - controller?.window.orderOut(nil) - controller = nil - }) - })) +func closeCall(minimisize: Bool = false) { + _ = controller.modify { controller in + controller?.cleanup() + controller?.window.orderOut(nil) + return nil + } } -func applyUIPCallResult(_ account:Account, _ result:PCallResult) { +func applyUIPCallResult(_ sharedContext: SharedAccountContext, _ result:PCallResult) { assertOnMainThread() - switch result { case let .success(session): - showPhoneCallWindow(session) + showCallWindow(session) case .fail: break case let .samePeer(session): - if let header = account.context.mainNavigation?.callHeader, header.needShown { + if let header = sharedContext.bindings.rootNavigation().callHeader, header.needShown { (header.view as? CallNavigationHeaderView)?.hide() - showPhoneCallWindow(session) + showCallWindow(session) } else { - controller?.window.orderFront(nil) + controller.with { $0?.window.orderFront(nil) } } } } diff --git a/Telegram-Mac/PhoneCountries.txt b/Telegram-Mac/PhoneCountries.txt index fc3b5f36f7..417ab7fecc 100644 --- a/Telegram-Mac/PhoneCountries.txt +++ b/Telegram-Mac/PhoneCountries.txt @@ -228,4 +228,5 @@ 1;US;USA 1;PR;Puerto Rico 1;DO;Dominican Rep. -1;CA;Canada \ No newline at end of file +1;CA;Canada +383;XK;Kosovo diff --git a/Telegram-Mac/PhoneNumberConfirmController.swift b/Telegram-Mac/PhoneNumberConfirmController.swift index 63c72fee2c..af7e651ae9 100644 --- a/Telegram-Mac/PhoneNumberConfirmController.swift +++ b/Telegram-Mac/PhoneNumberConfirmController.swift @@ -8,141 +8,28 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac -import MtProtoKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit + private let manager = CountryManager() -private final class ChangePhoneNumberArguments { - let sendCode:(String)->Void - init(sendCode:@escaping(String)->Void) { - self.sendCode = sendCode - } -} + class ChangePhoneNumberView : View { - fileprivate let container: ChangePhoneNumberContainerView = ChangePhoneNumberContainerView(frame: NSMakeRect(0, 0, 300, 110)) + fileprivate let container: ChangePhoneNumberContainerView = ChangePhoneNumberContainerView(frame: NSMakeRect(0, 0, 300, 110), manager: manager) required init(frame frameRect: NSRect) { super.init(frame: frameRect) addSubview(container) + updateLocalizationAndTheme(theme: theme) } - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func layout() { - container.centerX(y: 20) - } -} - - class ChangePhoneNumberContainerView : View, NSTextFieldDelegate { - - fileprivate var arguments:ChangePhoneNumberArguments? - - - private let countrySelector:TitleButton = TitleButton() - - let countryLabel:TextViewLabel = TextViewLabel() - let numberLabel:TextViewLabel = TextViewLabel() - - fileprivate let errorLabel:LoginErrorStateView = LoginErrorStateView() - - let codeText:NSTextField = NSTextField() - let numberText:NSTextField = NSTextField() - - fileprivate var selectedItem:CountryItem? - - - required init(frame frameRect: NSRect) { - super.init(frame: frameRect) - - - countrySelector.style = ControlStyle(font: NSFont.medium(.title), foregroundColor: theme.colors.blueUI, backgroundColor: theme.colors.background) - countrySelector.set(text: "France", for: .Normal) - countrySelector.sizeToFit() - addSubview(countrySelector) - - - - - addSubview(countryLabel) - addSubview(numberLabel) - - countrySelector.set(handler: { [weak self] _ in - self?.showCountrySelector() - }, for: .Click) - - updateLocalizationAndTheme() - - codeText.stringValue = "+" - - codeText.textColor = theme.colors.text - codeText.font = NSFont.normal(.title) - numberText.textColor = theme.colors.text - numberText.font = NSFont.normal(.title) - - numberText.isBordered = false - numberText.isBezeled = false - numberText.drawsBackground = false - numberText.focusRingType = .none - - codeText.drawsBackground = false - codeText.isBordered = false - codeText.isBezeled = false - codeText.focusRingType = .none - - codeText.delegate = self - codeText.nextResponder = numberText - codeText.nextKeyView = numberText - - numberText.delegate = self - numberText.nextResponder = codeText - numberText.nextKeyView = codeText - addSubview(codeText) - addSubview(numberText) - - errorLabel.layer?.opacity = 0 - addSubview(errorLabel) - - let code = NSLocale.current.regionCode ?? "US" - update(selectedItem: manager.item(bySmallCountryName: code), update: true) - - } - - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - - countryLabel.attributedString = .initialize(string: tr(.loginCountryLabel), color: theme.colors.grayText, font: NSFont.normal(FontSize.title)) - countryLabel.sizeToFit() - - numberLabel.attributedString = .initialize(string: tr(.loginYourPhoneLabel), color: theme.colors.grayText, font: NSFont.normal(FontSize.title)) - numberLabel.sizeToFit() - - numberText.placeholderAttributedString = NSAttributedString.initialize(string: tr(.loginPhoneFieldPlaceholder), color: theme.colors.grayText, font: NSFont.normal(.header), coreText: false) - - needsLayout = true - } - - func setPhoneError(_ error: AuthorizationCodeRequestError) { - let text:String - switch error { - case .invalidPhoneNumber: - text = tr(.phoneNumberInvalid) - case .limitExceeded: - text = tr(.loginFloodWait) - case .generic: - text = "undefined error" - } - errorLabel.state.set(.single(.error(text))) - } - - func update(countryCode: Int32, number: String) { - self.codeText.stringValue = "\(countryCode)" - self.numberText.stringValue = formatPhoneNumber(number) + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + backgroundColor = theme.colors.background } required init?(coder: NSCoder) { @@ -150,171 +37,11 @@ class ChangePhoneNumberView : View { } override func layout() { - super.layout() - codeText.sizeToFit() - numberText.sizeToFit() - - let maxInset = max(countryLabel.frame.width,numberLabel.frame.width) - let contentInset = maxInset + 20 + 5 - countrySelector.setFrameOrigin(contentInset, floorToScreenPixels(25 - countrySelector.frame.height/2)) - - countryLabel.setFrameOrigin(maxInset - countryLabel.frame.width, floorToScreenPixels(25 - countryLabel.frame.height/2)) - numberLabel.setFrameOrigin(maxInset - numberLabel.frame.width, floorToScreenPixels(75 - numberLabel.frame.height/2)) - - codeText.setFrameOrigin(contentInset, floorToScreenPixels(75 - codeText.frame.height/2)) - numberText.setFrameOrigin(contentInset + separatorInset, floorToScreenPixels(75 - codeText.frame.height/2)) - errorLabel.centerX(y: 120) - } - - override func draw(_ layer: CALayer, in ctx: CGContext) { - super.draw(layer, in: ctx) - - - let maxInset = max(countryLabel.frame.width,numberLabel.frame.width) + 20 - ctx.setFillColor(theme.colors.border.cgColor) - ctx.fill(NSMakeRect(maxInset, 50, frame.width - maxInset, .borderSize)) - ctx.fill(NSMakeRect(maxInset, 100, frame.width - maxInset, .borderSize)) - // ctx.fill(NSMakeRect(maxInset + separatorInset, 50, .borderSize, 50)) - } - - - func showCountrySelector() { - - var items:[ContextMenuItem] = [] - for country in manager.countries { - let item = ContextMenuItem(country.fullName, handler: { [weak self] in - self?.update(selectedItem: country, update: true) - }) - items.append(item) - } - if let currentEvent = NSApp.currentEvent { - ContextMenu.show(items: items, view: countrySelector, event: currentEvent, onShow: {(menu) in - - }, onClose: {}) - } - - } - - override func controlTextDidChange(_ obj: Notification) { - - if let field = obj.object as? NSTextField { - let code = codeText.stringValue.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() - let dec = code.prefix(4) - - if field == codeText { - - - if code.length > 4 { - let list = Array(code.characters).map {String($0)} - let reduced = list.reduce([], { current, value -> [String] in - var current = current - current.append((current.last ?? "") + value) - return current - }).map({Int($0)}).filter({$0 != nil}).map({$0!}) - - var found: Bool = false - for _code in reduced { - if let item = manager.item(byCodeNumber: _code) { - codeText.stringValue = "+" + String(_code) - update(selectedItem: item, update: true, updateCode: false) - - let codeString = String(_code) - var formated = formatPhoneNumber(codeString + String(code[codeString.endIndex.. Bool { - if commandSelector == #selector(insertNewline(_:)) { - if control == codeText { - self.window?.makeFirstResponder(self.numberText) - self.numberText.selectText(nil) - } else if !numberText.stringValue.isEmpty { - arguments?.sendCode(number) - } - //Queue.mainQueue().justDispatch { - (control as? NSTextField)?.setCursorToEnd() - //} - return true - } else if commandSelector == #selector(deleteBackward(_:)) { - if control == numberText { - if numberText.stringValue.isEmpty { - Queue.mainQueue().justDispatch { - self.window?.makeFirstResponder(self.codeText) - self.codeText.setCursorToEnd() - } - } - } - return false - - } - return false - } - - func update(selectedItem:CountryItem?, update:Bool, updateCode:Bool = true) -> Void { - self.selectedItem = selectedItem - if update { - countrySelector.set(text: selectedItem?.shortName ?? tr(.loginInvalidCountryCode), for: .Normal) - countrySelector.sizeToFit() - if updateCode { - codeText.stringValue = selectedItem != nil ? "+\(selectedItem!.code)" : "+" - } - needsLayout = true - setNeedsDisplayLayer() - - } - } - - - - var separatorInset:CGFloat { - return codeText.frame.width + 10 + container.centerX(y: 20) } - } + class PhoneNumberConfirmController: TelegramGenericViewController { private let actionDisposable = MetaDisposable() @@ -322,40 +49,40 @@ class PhoneNumberConfirmController: TelegramGenericViewController deliverOnMainQueue, for: mainWindow).start(next: { [weak strongSelf] data in + strongSelf.actionDisposable.set(showModalProgress(signal: context.engine.accountData.requestChangeAccountPhoneNumberVerification(phoneNumber: phoneNumber) |> deliverOnMainQueue, for: mainWindow).start(next: { [weak strongSelf] data in - strongSelf?.navigationController?.push(PhoneNumberInputCodeController(account, data: data, formattedNumber: formatPhoneNumber(phoneNumber))) + strongSelf?.navigationController?.push(PhoneNumberInputCodeController(context, data: data, formattedNumber: formatPhoneNumber(phoneNumber))) }, error: { error in let text: String switch error { case .limitExceeded: - text = tr(.changeNumberSendDataErrorLimitExceeded) + text = tr(L10n.changeNumberSendDataErrorLimitExceeded) case .invalidPhoneNumber: - text = tr(.changeNumberSendDataErrorInvalidPhoneNumber) + text = tr(L10n.changeNumberSendDataErrorInvalidPhoneNumber) case .phoneNumberOccupied: - text = tr(.changeNumberSendDataErrorPhoneNumberOccupied(phoneNumber)) + text = tr(L10n.changeNumberSendDataErrorPhoneNumberOccupied(phoneNumber)) case .generic: - text = tr(.changeNumberSendDataErrorGeneric) + text = tr(L10n.changeNumberSendDataErrorGeneric) + case .phoneBanned: + text = tr(L10n.changeNumberSendDataErrorGeneric) } - alert(for: mainWindow, header: appName, info: text) + alert(for: mainWindow, info: text) })) }) genericView.container.arguments = arguments - (self.rightBarView as? TextButtonBarView)?.button.set(handler:{ [weak self] _ in + self.rightBarView.set(handler:{ [weak self] _ in if let strongSelf = self { arguments.sendCode(strongSelf.genericView.container.number) } @@ -385,7 +112,7 @@ class PhoneNumberConfirmController: TelegramGenericViewController BarView { - return TextButtonBarView(controller: self, text: tr(.composeNext), style: navigationButtonStyle, alignment:.Right) + return TextButtonBarView(controller: self, text: tr(L10n.composeNext), style: navigationButtonStyle, alignment:.Right) } } diff --git a/Telegram-Mac/PhoneNumberInputCodeController.swift b/Telegram-Mac/PhoneNumberInputCodeController.swift index 9849b1675b..4d132c75b1 100644 --- a/Telegram-Mac/PhoneNumberInputCodeController.swift +++ b/Telegram-Mac/PhoneNumberInputCodeController.swift @@ -8,9 +8,10 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit private final class ConfirmCodeArguments { @@ -60,7 +61,7 @@ class PhoneNumberInputCodeView : View, NSTextFieldDelegate { codeText.delegate = self - updateLocalizationAndTheme() + updateLocalizationAndTheme(theme: theme) } func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { @@ -70,7 +71,7 @@ class PhoneNumberInputCodeView : View, NSTextFieldDelegate { return false } - override func controlTextDidChange(_ obj: Notification) { + func controlTextDidChange(_ obj: Notification) { let code = codeText.stringValue.components(separatedBy: CharacterSet.decimalDigits.inverted).joined().prefix(Int(codeLength)) codeText.stringValue = code codeText.sizeToFit() @@ -93,17 +94,17 @@ class PhoneNumberInputCodeView : View, NSTextFieldDelegate { var nextText: String = "" switch nextType { case .call: - nextText = tr(.loginWillCall(minutes, secValue)) + nextText = tr(L10n.loginWillCall(minutes, secValue)) break case .sms: - nextText = tr(.loginWillSendSms(minutes, secValue)) + nextText = tr(L10n.loginWillSendSms(minutes, secValue)) break default: break } layout = TextViewLayout(.initialize(string: nextText, color: theme.colors.grayText, font: .normal(.text))) } else { - layout = TextViewLayout(.initialize(string: tr(.loginPhoneDialed), color: theme.colors.grayText, font: .normal(.text))) + layout = TextViewLayout(.initialize(string: tr(L10n.loginPhoneDialed), color: theme.colors.grayText, font: .normal(.text))) } layout.measure(width: frame.width - 60) callField.update(layout) @@ -120,21 +121,21 @@ class PhoneNumberInputCodeView : View, NSTextFieldDelegate { super.draw(layer, in: ctx) } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) yourCodeField.backgroundColor = theme.colors.grayBackground sentCodeField.backgroundColor = theme.colors.grayBackground callField.backgroundColor = theme.colors.grayBackground - let yourCodeLayout = TextViewLayout(.initialize(string: tr(.loginYourCodeLabel).uppercased(), color: theme.colors.grayText, font: .normal(.text)), maximumNumberOfLines: 1) + let yourCodeLayout = TextViewLayout(.initialize(string: tr(L10n.loginYourCodeLabel).uppercased(), color: theme.colors.grayText, font: .normal(.text)), maximumNumberOfLines: 1) - let sentCodeLayout = TextViewLayout(.initialize(string: tr(.loginJustSentSms), color: theme.colors.grayText, font: .normal(.text))) + let sentCodeLayout = TextViewLayout(.initialize(string: tr(L10n.loginJustSentSms), color: theme.colors.grayText, font: .normal(.text))) yourCodeField.update(yourCodeLayout) sentCodeField.update(sentCodeLayout) let attr = NSMutableAttributedString() - _ = attr.append(string: tr(.loginCodePlaceholder), color: theme.colors.grayText, font: .normal(.title)) + _ = attr.append(string: tr(L10n.loginCodePlaceholder), color: theme.colors.grayText, font: .normal(.title)) attr.setAlignment(.center, range: attr.range) codeText.placeholderAttributedString = attr backgroundColor = theme.colors.grayBackground @@ -167,10 +168,10 @@ class PhoneNumberInputCodeController: TelegramGenericViewControllerVoid { - changePhoneDisposable.set(showModalProgress(signal: requestChangeAccountPhoneNumber(account: account, phoneNumber: formattedNumber, phoneCodeHash: data.hash, phoneCode: code) |> deliverOnMainQueue, for: mainWindow).start(error: { [weak self] error in + changePhoneDisposable.set(showModalProgress(signal: context.engine.accountData.requestChangeAccountPhoneNumber(phoneNumber: formattedNumber, phoneCodeHash: data.hash, phoneCode: code) |> deliverOnMainQueue, for: context.window).start(error: { [weak self] error in var alertText: String = "" switch error { case .generic: - alertText = tr(.changeNumberConfirmCodeErrorGeneric) + alertText = tr(L10n.changeNumberConfirmCodeErrorGeneric) case .invalidCode: self?.genericView.codeText.shake() self?.genericView.codeText.setSelectionRange(NSMakeRange(0, code.length)) return case .codeExpired: - alertText = tr(.changeNumberConfirmCodeErrorCodeExpired) + alertText = tr(L10n.changeNumberConfirmCodeErrorCodeExpired) case .limitExceeded: - alertText = tr(.changeNumberConfirmCodeErrorLimitExceeded) + alertText = tr(L10n.changeNumberConfirmCodeErrorLimitExceeded) } - alert(for: mainWindow, header: appName, info: alertText) + alert(for: mainWindow, info: alertText) }, completed: { [weak self] in if let strongSelf = self { strongSelf.navigationController?.close(animated: true) - alert(for: mainWindow, header: appName, info: tr(.changeNumberConfirmCodeSuccess(strongSelf.formattedNumber))) + alert(for: mainWindow, info: tr(L10n.changeNumberConfirmCodeSuccess(strongSelf.formattedNumber))) } })) } @@ -230,7 +231,7 @@ class PhoneNumberInputCodeController: TelegramGenericViewController BarView { - return TextButtonBarView(controller: self, text: tr(.composeNext), style: navigationButtonStyle, alignment:.Right) + return TextButtonBarView(controller: self, text: tr(L10n.composeNext), style: navigationButtonStyle, alignment:.Right) } } diff --git a/Telegram-Mac/PhoneNumberIntroController.swift b/Telegram-Mac/PhoneNumberIntroController.swift index baf2ec96a5..78f95e8bca 100644 --- a/Telegram-Mac/PhoneNumberIntroController.swift +++ b/Telegram-Mac/PhoneNumberIntroController.swift @@ -8,8 +8,9 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac +import TelegramCore + +import SwiftSignalKit class ChaneNumberIntroView : NSScrollView, AppearanceViewProtocol { let imageView:ImageView = ImageView() @@ -22,11 +23,11 @@ class ChaneNumberIntroView : NSScrollView, AppearanceViewProtocol { documentView?.addSubview(imageView) documentView?.addSubview(textView) - updateLocalizationAndTheme() + updateLocalizationAndTheme(theme: theme) } - func updateLocalizationAndTheme() { - + func updateLocalizationAndTheme(theme: PresentationTheme) { + let theme = (theme as! TelegramPresentationTheme) imageView.image = theme.icons.changePhoneNumberIntro imageView.sizeToFit() @@ -34,7 +35,7 @@ class ChaneNumberIntroView : NSScrollView, AppearanceViewProtocol { textView.background = theme.colors.background documentView?.background = theme.colors.background let attr = NSMutableAttributedString() - _ = attr.append(string: tr(.changePhoneNumberIntroDescription), color: theme.colors.grayText, font: .normal(.text)) + _ = attr.append(string: tr(L10n.changePhoneNumberIntroDescription), color: theme.colors.grayText, font: .normal(.text)) attr.detectBoldColorInString(with: .bold(.text)) textView.set(layout: TextViewLayout(attr, alignment:.center)) @@ -63,14 +64,14 @@ class PhoneNumberIntroController: EmptyComposeController deliverOnMainQueue |> map { [weak self] peer -> Bool in + ready.set(context.account.postbox.loadedPeerWithId(context.peerId) |> deliverOnMainQueue |> map { [weak self] peer -> Bool in if let phone = (peer as? TelegramUser)?.phone { self?.setCenterTitle(formatPhoneNumber("+" + phone)) } return true }) - (self.rightBarView as? TextButtonBarView)?.button.set(handler:{ [weak self] _ in + self.rightBarView.set(handler:{ [weak self] _ in self?.executeNext() }, for: .Click) @@ -85,13 +86,13 @@ class PhoneNumberIntroController: EmptyComposeController BarView { - return TextButtonBarView(controller: self, text: tr(.composeNext), style: navigationButtonStyle, alignment:.Right) + return TextButtonBarView(controller: self, text: L10n.composeNext, style: navigationButtonStyle, alignment:.Right) } func executeNext() { - confirm(for: mainWindow, with: appName, and: tr(.changePhoneNumberIntroAlert), successHandler: { [weak self] _ in - if let account = self?.account { - self?.navigationController?.push(PhoneNumberConfirmController(account)) + confirm(for: mainWindow, information: L10n.changePhoneNumberIntroAlert, successHandler: { [weak self] _ in + if let context = self?.context { + self?.navigationController?.push(PhoneNumberConfirmController(context)) } }) } diff --git a/Telegram-Mac/PhoneNumberUtils.swift b/Telegram-Mac/PhoneNumberUtils.swift new file mode 100644 index 0000000000..50edc47a4d --- /dev/null +++ b/Telegram-Mac/PhoneNumberUtils.swift @@ -0,0 +1,49 @@ +// +// PhoneNumberUtils.swift +// Telegram +// +// Created by Mikhail Filimonov on 01.11.2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import libphonenumber + +private let phoneNumberUtil = NBPhoneNumberUtil() +func formatPhoneNumber(_ string: String) -> String { + do { + let number = try phoneNumberUtil.parse("+" + string, defaultRegion: nil) + return try phoneNumberUtil.format(number, numberFormat: .INTERNATIONAL) + } catch _ { + return string + } +} + +func isViablePhoneNumber(_ string: String) -> Bool { + return phoneNumberUtil.isViablePhoneNumber(string) +} + +class ParsedPhoneNumber: Equatable { + let rawPhoneNumber: NBPhoneNumber? + + init?(string: String) { + if let number = try? phoneNumberUtil.parse(string, defaultRegion: NB_UNKNOWN_REGION) { + self.rawPhoneNumber = number + } else { + return nil + } + } + + static func == (lhs: ParsedPhoneNumber, rhs: ParsedPhoneNumber) -> Bool { + var error: NSError? + let result = phoneNumberUtil.isNumberMatch(lhs.rawPhoneNumber, second: rhs.rawPhoneNumber, error: &error) + if error != nil { + return false + } + if result != .NO_MATCH && result != .NOT_A_NUMBER { + return true + } else { + return false + } + } +} diff --git a/Telegram-Mac/PhotoCache.swift b/Telegram-Mac/PhotoCache.swift index a7aa8125c4..84ec86c737 100644 --- a/Telegram-Mac/PhotoCache.swift +++ b/Telegram-Mac/PhotoCache.swift @@ -7,33 +7,104 @@ // import Cocoa -import SwiftSignalKitMac -import TelegramCoreMac -import PostboxMac +import SwiftSignalKit +import TelegramCore -struct PhotoCachedRecord { +import Postbox +import TGUIKit + + +enum ThemeSource : Equatable { + case local(ColorPalette, TelegramTheme?) + case cloud(TelegramTheme) +} + +private final class PhotoCachedRecord { let date:TimeInterval let image:CGImage let size:Int init(image:CGImage, size:Int) { - self.date = CFAbsoluteTimeGetCurrent() + self.date = CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970 self.size = size self.image = image } } +public final class TransformImageResult { + let image: CGImage? + let highQuality: Bool + init(_ image: CGImage?, _ highQuality: Bool) { + self.image = image + self.highQuality = highQuality + } + deinit { + + } +} + +enum AppearanceThumbSource : Int32 { + case general + case widget +} + enum PhotoCacheKeyEntry : Hashable { case avatar(PeerId, TelegramMediaImageRepresentation, NSSize, CGFloat) case emptyAvatar(PeerId, String, NSColor, NSSize, CGFloat) - case media(Media, NSSize, CGFloat) + case media(Media, TransformImageArguments, CGFloat, LayoutPositionFlags?) + case slot(SlotMachineValue, TransformImageArguments, CGFloat) + case platformTheme(TelegramThemeSettings, TransformImageArguments, CGFloat, LayoutPositionFlags?) + case messageId(stableId: Int64, TransformImageArguments, CGFloat, LayoutPositionFlags) + case theme(ThemeSource, Bool, AppearanceThumbSource) var hashValue:Int { + return 0 + } + + var stringValue: NSString { switch self { - case let .avatar(peerId, _, _, _): - return peerId.id.hashValue - case let .emptyAvatar(peerId, _, _, _, _): - return peerId.id.hashValue - case .media: - return 0 + case let .avatar(peerId, rep, size, scale): + return "avatar-\(peerId.toInt64())-\(rep.resource.id.hashValue)-\(size.width)-\(size.height)-\(scale)".nsstring + case let .emptyAvatar(peerId, letters, color, size, scale): + return "emptyAvatar-\(peerId.toInt64())-\(letters)-\(color.hexString)-\(size.width)-\(size.height)-\(scale)".nsstring + case let .media(media, transform, scale, layout): + var addition: String = "" + if let media = media as? TelegramMediaMap { + addition = "\(media.longitude)-\(media.latitude)" + } + if let media = media as? TelegramMediaFile { + addition += "\(media.resource.id.uniqueId)-\(String(describing: media.resource.size))" + #if !SHARE + if let fitz = media.animatedEmojiFitzModifier { + addition += "fitz-\(fitz.rawValue)" + } + #endif + } + return "media-\(String(describing: media.id?.id))-\(transform)-\(scale)-\(String(describing: layout?.rawValue))-\(addition)".nsstring + case let .slot(slot, transform, scale): + return "slot-\(slot.left.hashValue)\(slot.center.hashValue)\(slot.right.hashValue)-\(transform)-\(scale)".nsstring + case let .messageId(stableId, transform, scale, layout): + return "messageId-\(stableId)-\(transform)-\(scale)-\(layout.rawValue)".nsstring + case let .theme(source, bubbled, thumbSource): + switch source { + case let .local(palette, cloud): + if let settings = cloud?.settings { + #if !SHARE + return "theme-local-\(palette.name)-bubbled\(bubbled ? 1 : 0)-\(settings.desc)-\(thumbSource.rawValue)".nsstring + #else + return "" + #endif + } else { + return "theme-local-\(palette.name)-bubbled\(bubbled ? 1 : 0)-\(palette.accent.argb)-\(thumbSource.rawValue)".nsstring + } + case let .cloud(cloud): + return "theme-remote-\(cloud.id)\(String(describing: cloud.file?.id))-bubbled\(bubbled ? 1 : 0)-\(thumbSource.rawValue)".nsstring + } + case let .platformTheme(settings, arguments, scale, layout): + #if !SHARE + return "theme-\(settings.desc)-\(arguments)-\(scale)-\(String(describing: layout?.rawValue))".nsstring + #else + return "" + #endif + } } @@ -63,14 +134,22 @@ enum PhotoCacheKeyEntry : Hashable { } else { return false } - case let .media(lhsMedia, lhsSize, lhsScale): - if case let .media(rhsMedia, rhsSize, rhsScale) = rhs { - if !lhsMedia.isEqual(rhsMedia) { + case let .media(lhsMedia, lhsSize, lhsScale, lhsPositionFlags): + if case let .media(rhsMedia, rhsSize, rhsScale, rhsPositionFlags) = rhs { + if lhsMedia.id != rhsMedia.id { return false } + if let lhsMedia = lhsMedia as? TelegramMediaMap, let rhsMedia = rhsMedia as? TelegramMediaMap { + if lhsMedia.latitude != rhsMedia.latitude || lhsMedia.longitude != rhsMedia.longitude { + return false + } + } if lhsSize != rhsSize { return false } + if lhsPositionFlags != rhsPositionFlags { + return false + } if lhsScale != rhsScale { return false } @@ -78,109 +157,231 @@ enum PhotoCacheKeyEntry : Hashable { } else { return false } + case let .slot(value, size, scale): + if case .slot(value, size, scale) = rhs { + return true + } else { + return false + } + case let .messageId(stableId, size, scale, positionFlags): + if case .messageId(stableId, size, scale, positionFlags) = rhs { + return true + } else { + return false + } + case let .theme(source, bubbled, thumbSource): + if case .theme(source, bubbled, thumbSource) = rhs { + return true + } else { + return false + } + case let .platformTheme(settings, arguments, scale, position): + if case .platformTheme(settings, arguments, scale, position) = rhs { + return true + } else { + return false + } } } } -class PhotoCache { +private class PhotoCache { let memoryLimit:Int - let maxCount:Int = 1000 - private var values:[PhotoCacheKeyEntry:PhotoCachedRecord] = [:] - private let queue:Queue = Queue() + let maxCount:Int = 50 + private var values:NSCache = NSCache() - init(_ memoryLimit:Int = 16*1024*1024) { + init(_ memoryLimit:Int = 15) { self.memoryLimit = memoryLimit + self.values.countLimit = memoryLimit } - func cacheImage(_ image:CGImage, for key:PhotoCacheKeyEntry) { - queue.justDispatch { - self.values[key] = PhotoCachedRecord(image: image, size: Int(image.size.width * image.size.height * 4)) - self.freeMemoryIfNeeded() - } + fileprivate func cacheImage(_ image:CGImage, for key:PhotoCacheKeyEntry) { + self.values.setObject(PhotoCachedRecord(image: image, size: Int(image.backingSize.width * image.backingSize.height * 4)), forKey: key.stringValue) } private func freeMemoryIfNeeded() { - assert(queue.isCurrent()) - - let total = values.reduce(0, { (current, value: (key: PhotoCacheKeyEntry, value: PhotoCachedRecord)) -> Int in - return current + value.value.size - }) - - if total > memoryLimit { - let list = values.map ({($0.key, $0.value)}).sorted(by: { lhs, rhs -> Bool in - return lhs.1.date < rhs.1.date - }) - - var clearedMemorySize: Int = 0 - - for entry in list { - values.removeValue(forKey: entry.0) - clearedMemorySize += entry.1.size - - if total - clearedMemorySize < memoryLimit { - break - } - } - } } func cachedImage(for key:PhotoCacheKeyEntry) -> CGImage? { var image:CGImage? = nil - queue.sync { - image = self.values[key]?.image - } + image = self.values.object(forKey: key.stringValue)?.image return image } func removeRecord(for key:PhotoCacheKeyEntry) { - queue.justDispatch { - self.values.removeValue(forKey: key) - } + self.values.removeObject(forKey: key.stringValue) + } + + func clearAll() { + self.values.removeAllObjects() } } -private let peerPhotoCache = PhotoCache() -private let stickersCache = PhotoCache(32 * 1024 * 1024) +private let peerPhotoCache = PhotoCache(100) +private let photosCache = PhotoCache(50) +private let photoThumbsCache = PhotoCache(50) +private let themeThums = PhotoCache(100) + +private let stickersCache = PhotoCache(500) + +func clearImageCache() -> Signal { + return Signal { subscriber -> Disposable in + photosCache.clearAll() + photoThumbsCache.clearAll() + peerPhotoCache.clearAll() + subscriber.putNext(Void()) + subscriber.putCompletion() + return EmptyDisposable + } +} -func cachedPeerPhoto(_ peerId:PeerId, representation: TelegramMediaImageRepresentation, size: NSSize, scale: CGFloat) -> Signal { +func cachedPeerPhoto(_ peerId:PeerId, representation: TelegramMediaImageRepresentation, size: NSSize, scale: CGFloat) -> Signal { let entry:PhotoCacheKeyEntry = .avatar(peerId, representation, size, scale) return .single(peerPhotoCache.cachedImage(for: entry)) } -func cachePeerPhoto(image:CGImage, peerId:PeerId, representation: TelegramMediaImageRepresentation, size: NSSize, scale: CGFloat) -> Signal { +func cachePeerPhoto(image:CGImage, peerId:PeerId, representation: TelegramMediaImageRepresentation, size: NSSize, scale: CGFloat) -> Signal { let entry:PhotoCacheKeyEntry = .avatar(peerId, representation, size, scale) return .single(peerPhotoCache.cacheImage(image, for: entry)) } -func cachedEmptyPeerPhoto(_ peerId:PeerId, symbol: String, color: NSColor, size: NSSize, scale: CGFloat) -> Signal { +func cachedEmptyPeerPhoto(_ peerId:PeerId, symbol: String, color: NSColor, size: NSSize, scale: CGFloat) -> Signal { let entry:PhotoCacheKeyEntry = .emptyAvatar(peerId, symbol, color, size, scale) return .single(peerPhotoCache.cachedImage(for: entry)) } -func cacheEmptyPeerPhoto(image:CGImage, peerId:PeerId, symbol: String, color: NSColor, size: NSSize, scale: CGFloat) -> Signal { +func cacheEmptyPeerPhoto(image:CGImage, peerId:PeerId, symbol: String, color: NSColor, size: NSSize, scale: CGFloat) -> Signal { let entry:PhotoCacheKeyEntry = .emptyAvatar(peerId, symbol, color, size, scale) return .single(peerPhotoCache.cacheImage(image, for: entry)) } +func cachedPeerPhotoImmediatly(_ peerId:PeerId, representation: TelegramMediaImageRepresentation, size: NSSize, scale: CGFloat) -> CGImage? { + let entry:PhotoCacheKeyEntry = .avatar(peerId, representation, size, scale) + return peerPhotoCache.cachedImage(for: entry) +} +func cachedEmptyPeerPhotoImmediatly(_ peerId:PeerId, symbol: String, color: NSColor, size: NSSize, scale: CGFloat) -> CGImage? { + let entry:PhotoCacheKeyEntry = .emptyAvatar(peerId, symbol, color, size, scale) + return peerPhotoCache.cachedImage(for: entry) +} +func cachedMedia(media: Media, arguments: TransformImageArguments, scale: CGFloat, positionFlags: LayoutPositionFlags? = nil) -> Signal { + let entry:PhotoCacheKeyEntry = .media(media, arguments, scale, positionFlags) + let value: CGImage? + var full: Bool = false + + if arguments.imageSize.width <= 60, let media = media as? TelegramMediaFile, media.isStaticSticker || media.isAnimatedSticker, let image = stickersCache.cachedImage(for: entry) { + value = image + full = true + } else if let image = photosCache.cachedImage(for: entry) { + value = image + full = true + } else { + value = photoThumbsCache.cachedImage(for: entry) + } + return .single(TransformImageResult(value, full)) +} -func cachedMedia(media: Media, size: NSSize, scale: CGFloat) -> Signal { - let entry:PhotoCacheKeyEntry = .media(media, size, scale) - return .single(stickersCache.cachedImage(for: entry)) +func cachedSlot(value: SlotMachineValue, arguments: TransformImageArguments, scale: CGFloat) -> Signal { + let entry:PhotoCacheKeyEntry = .slot(value, arguments, scale) + let value: CGImage? = stickersCache.cachedImage(for: entry) + let full: Bool = value != nil + + return .single(TransformImageResult(value, full)) } -func cacheMedia(signal:Signal, media: Media, size: NSSize, scale: CGFloat) -> Signal { - let entry:PhotoCacheKeyEntry = .media(media, size, scale) +func cachedMedia(media: TelegramThemeSettings, arguments: TransformImageArguments, scale: CGFloat, positionFlags: LayoutPositionFlags? = nil) -> Signal { + let entry:PhotoCacheKeyEntry = .platformTheme(media, arguments, scale, positionFlags) + let value: CGImage? + var full: Bool = false - return signal |> mapToSignal { image -> Signal in - if let image = image { - return .single(stickersCache.cacheImage(image, for: entry)) + if let image = photosCache.cachedImage(for: entry) { + value = image + full = true + } else { + value = nil + } + return .single(TransformImageResult(value, full)) +} + +func cachedMedia(messageId: Int64, arguments: TransformImageArguments, scale: CGFloat, positionFlags: LayoutPositionFlags? = nil) -> Signal { + let entry:PhotoCacheKeyEntry = .messageId(stableId: messageId, arguments, scale, positionFlags ?? []) + let value: CGImage? + var full: Bool = false + if let image = photosCache.cachedImage(for: entry) { + value = image + full = true + } else { + value = photoThumbsCache.cachedImage(for: entry) + } + return .single(TransformImageResult(value, full)) +} + +func cacheMedia(_ result: TransformImageResult, media: Media, arguments: TransformImageArguments, scale: CGFloat, positionFlags: LayoutPositionFlags? = nil) -> Void { + if let image = result.image { + let entry:PhotoCacheKeyEntry = .media(media, arguments, scale, positionFlags) + if arguments.imageSize.width <= 60, result.highQuality, let media = media as? TelegramMediaFile, media.isStaticSticker || media.isAnimatedSticker { + stickersCache.cacheImage(image, for: entry) + } else if !result.highQuality { + photoThumbsCache.cacheImage(image, for: entry) + } else { + photosCache.cacheImage(image, for: entry) } - return .complete() } +} + +func cacheSlot(_ result: TransformImageResult, value: SlotMachineValue, arguments: TransformImageArguments, scale: CGFloat) -> Void { + if let image = result.image { + stickersCache.cacheImage(image, for: .slot(value, arguments, scale)) + } +} + +func cacheMedia(_ result: TransformImageResult, media: TelegramThemeSettings, arguments: TransformImageArguments, scale: CGFloat, positionFlags: LayoutPositionFlags? = nil) -> Void { + if let image = result.image { + let entry:PhotoCacheKeyEntry = .platformTheme(media, arguments, scale, positionFlags) + photosCache.cacheImage(image, for: entry) + } +} + +func cacheMedia(_ result: TransformImageResult, messageId: Int64, arguments: TransformImageArguments, scale: CGFloat, positionFlags: LayoutPositionFlags? = nil) -> Void { + if let image = result.image { + let entry:PhotoCacheKeyEntry = .messageId(stableId: messageId, arguments, scale, positionFlags ?? []) + if !result.highQuality { + photoThumbsCache.cacheImage(image, for: entry) + } else { + photosCache.cacheImage(image, for: entry) + } + } +} + +func cachedThemeThumb(source: ThemeSource, bubbled: Bool, thumbSource: AppearanceThumbSource = .general) -> Signal { + let entry:PhotoCacheKeyEntry = .theme(source, bubbled, thumbSource) + let value: CGImage? + var full: Bool = false + if let image = themeThums.cachedImage(for: entry) { + value = image + full = true + } else { + value = themeThums.cachedImage(for: entry) + } + if value == nil { + var bp:Int = 0 + bp += 1 + } + return .single(TransformImageResult(value, full)) } +func cacheThemeThumb(_ result: TransformImageResult, source: ThemeSource, bubbled: Bool, thumbSource: AppearanceThumbSource = .general) -> Void { + let entry:PhotoCacheKeyEntry = .theme(source, bubbled, thumbSource) + + if let image = result.image { + if !result.highQuality { + themeThums.cacheImage(image, for: entry) + } else { + themeThums.cacheImage(image, for: entry) + } + } +} diff --git a/Telegram-Mac/PicturePicker.swift b/Telegram-Mac/PicturePicker.swift index 576332ebd2..8bc088423e 100644 --- a/Telegram-Mac/PicturePicker.swift +++ b/Telegram-Mac/PicturePicker.swift @@ -15,7 +15,15 @@ fileprivate class PickerObserver { @objc fileprivate func validated(_ picker:IKPictureTaker, _ code:Int, _ contextInfo:Any?) { if code == NSApplication.ModalResponse.OK.rawValue { - let image = picker.outputImage() + var image = picker.outputImage() + if let img = image { + let size = img.size.aspectFilled(NSMakeSize(640, 640)) + let resized = generateImage(size, contextGenerator: { size, ctx in + ctx.clear(NSMakeRect(0, 0, size.width, size.height)) + ctx.draw(img.precomposed(), in: NSMakeRect(0, 0, size.width, size.height)) + })! + image = NSImage(cgImage: resized, size: size) + } completion(image) } } diff --git a/Telegram-Mac/PinchToZoom.swift b/Telegram-Mac/PinchToZoom.swift new file mode 100644 index 0000000000..43064e3ced --- /dev/null +++ b/Telegram-Mac/PinchToZoom.swift @@ -0,0 +1,142 @@ +// +// PinchToZoom.swift +// Telegram +// +// Created by Mikhail Filimonov on 15.04.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit + +protocol PinchableView : NSView { + func update(size: NSSize) + +} + +final class PinchToZoom { + + private weak var parentView: NSView? + private var view: PinchableView? + private var magnify: NSMagnificationGestureRecognizer! + private var initialSize: NSSize = .zero + private var initialOrigin: NSPoint { + guard let parent = parentView, let window = parent.window else { + return .zero + } + var point = window.contentView!.convert(NSZeroPoint, from: parent) + point = point.offset(dx: 0, dy: -parent.frame.height) + return point + } + private var currentMagnify: CGFloat = 0 + private var animation: DisplayLinkAnimator? + private let disposable = MetaDisposable() + init(parentView: NSView?) { + self.parentView = parentView + self.magnify = NSMagnificationGestureRecognizer(target: self, action: #selector(zoomIn(_:))) + } + + func add(to view: PinchableView, size: NSSize) { + self.initialSize = size + if view.isEqual(to: view) { + self.view?.removeGestureRecognizer(magnify) + self.view = view + view.addGestureRecognizer(magnify) + } + } + + func remove() { + self.initialSize = .zero + self.view?.removeGestureRecognizer(magnify) + } + + deinit { + view?.removeGestureRecognizer(magnify) + disposable.dispose() + } + + + @objc func zoomIn(_ gesture: NSMagnificationGestureRecognizer) { + + guard let parentView = parentView, let view = self.view, let window = parentView.window as? Window else { + return + } + + if view.visibleRect == .zero { + return + } + + let maxMagnification: CGFloat = 2 + + self.currentMagnify = min(max(1, 1 + gesture.magnification), maxMagnification) + + disposable.set(nil) + + let updateMagnify:(CGFloat)->Void = { [weak self, weak view] magnifyValue in + guard let `self` = self, let view = view else { + return + } + let updatedSize = NSMakeSize(round(self.initialSize.width * magnifyValue), round(self.initialSize.height * magnifyValue)) + + let lastPoint = window.contentView!.focus(NSMakeSize(self.initialSize.width * maxMagnification, self.initialSize.height * maxMagnification)).origin + + let x = lastPoint.x - self.initialOrigin.x + let y = lastPoint.y - self.initialOrigin.y + + let coef = min((magnifyValue - 1), 1) + let bestPoint = NSMakePoint(round(self.initialOrigin.x + x * coef), round(self.initialOrigin.y + y * coef)) + + view.frame = CGRect(origin: bestPoint, size: updatedSize) + + + self.currentMagnify = magnifyValue + } + + self.animation = nil + + let returnView:(Bool)->Void = { [weak self, weak view] animated in + guard let strongSelf = self else { + return + } + + view?.update(size: strongSelf.initialSize) + + + strongSelf.animation = DisplayLinkAnimator(duration: 0.2, from: strongSelf.currentMagnify, to: 1, update: { current in + updateMagnify(current) + }, completion: { [weak view] in + if let view = view { + view.setFrameOrigin(.zero) + strongSelf.parentView?.addSubview(view) + } + }) + } + + + switch gesture.state { + case .began: + var point = window.contentView!.convert(NSZeroPoint, from: view) + point = point.offset(dx: 0, dy: -view.frame.height) + view.setFrameOrigin(point) + window.contentView?.addSubview(view) + + let updatedSize = NSMakeSize(round(self.initialSize.width * maxMagnification), round(self.initialSize.height * maxMagnification)) + view.update(size: updatedSize) + case .possible: + break + case .changed: + let magnifyValue = min(max(1, 1 + gesture.magnification), maxMagnification) + updateMagnify(magnifyValue) + case .ended: + returnView(true) + case .cancelled: + returnView(true) + case .failed: + returnView(true) + @unknown default: + break + } + } + +} diff --git a/Telegram-Mac/PlayerListController.swift b/Telegram-Mac/PlayerListController.swift new file mode 100644 index 0000000000..42c576944a --- /dev/null +++ b/Telegram-Mac/PlayerListController.swift @@ -0,0 +1,736 @@ +// +// PlayerListController.swift +// Telegram +// +// Created by keepcoder on 26/06/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + +private final class PlayerListArguments { + let chatInteraction: ChatInteraction + init(chatInteraction: ChatInteraction) { + self.chatInteraction = chatInteraction + } +} + +private enum PlayerListEntry: TableItemListNodeEntry { + static func < (lhs: PlayerListEntry, rhs: PlayerListEntry) -> Bool { + return lhs.index < rhs.index + } + + var index: MessageIndex { + switch self { + case let .message(_, message): + return MessageIndex(message) + } + } + + case message(sectionId: Int32, Message) + + var stableId: ChatHistoryEntryId { + switch self { + case let .message(_, message): + return .message(message) + } + } + + func item(_ arguments: PlayerListArguments, initialSize: NSSize) -> TableRowItem { + switch self { + case let .message(_, message): + return PeerMediaMusicRowItem(initialSize, arguments.chatInteraction, .messageEntry(message, [], .defaultSettings, .singleItem), isCompactPlayer: true) + } + } + + static func ==(lhs: PlayerListEntry, rhs: PlayerListEntry) -> Bool { + switch lhs { + case let .message(_, lhsMessage): + if case let .message(_, rhsMessage) = rhs { + return isEqualMessages(lhsMessage, rhsMessage) + } else { + return false + } + } + } +} + + +private func playerAudioEntries(_ update: PeerMediaUpdate, timeDifference: TimeInterval) -> [PlayerListEntry] { + var entries: [PlayerListEntry] = [] + var sectionId: Int32 = 0 + + for message in update.messages { + entries.append(.message(sectionId: sectionId, message)) + } + + return entries +} +fileprivate func preparedAudioListTransition(from fromView:[PlayerListEntry], to toView:[PlayerListEntry], initialSize:NSSize, arguments: PlayerListArguments, animated:Bool, scroll:TableScrollState) -> TableUpdateTransition { + let (removed,inserted,updated) = proccessEntries(fromView, right: toView, { (entry) -> TableRowItem in + + return entry.item(arguments, initialSize: initialSize) + + }) + + for item in inserted { + _ = item.1.makeSize(initialSize.width, oldWidth: initialSize.width) + } + for item in updated { + _ = item.1.makeSize(initialSize.width, oldWidth: initialSize.width) + } + + return TableUpdateTransition(deleted: removed, inserted: inserted, updated:updated, animated:animated, state:scroll) +} + + + +private final class PlayerListTrackView: View { + private let cover: TransformImageView = TransformImageView() + private let trackName: TextView = TextView() + let artistName: TextView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(cover) + addSubview(trackName) + addSubview(artistName) + + trackName.userInteractionEnabled = false + trackName.isSelectable = false + + artistName.isSelectable = false + + artistName.set(handler: { control in + control.alphaValue = 1 + }, for: .Hover) + + artistName.set(handler: { control in + control.alphaValue = 1 + }, for: .Normal) + + artistName.set(handler: { control in + control.alphaValue = 0.8 + }, for: .Highlight) + + updateLocalizationAndTheme(theme: theme) + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + trackName.background = theme.colors.background + artistName.background = theme.colors.background + } + + func update(_ item: APSongItem) { + let trackLayout = TextViewLayout(.initialize(string: item.songName.isEmpty ? item.performerName : item.songName, color: theme.colors.text, font: .medium(.text)), maximumNumberOfLines: 1) + + let artistLayout = TextViewLayout(.initialize(string: item.performerName, color: theme.colors.grayText, font: .normal(.text)), maximumNumberOfLines: 1) + + trackName.update(trackLayout) + artistName.update(artistLayout) + + let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0)) + let arguments = TransformImageArguments(corners: imageCorners, imageSize: PeerMediaIconSize, boundingSize: PeerMediaIconSize, intrinsicInsets: NSEdgeInsets()) + + cover.layer?.contents = theme.icons.playerMusicPlaceholder + cover.layer?.cornerRadius = .cornerRadius + if let imageMediaReference = item.coverImageMediaReference { + cover.setSignal(chatMessagePhotoThumbnail(account: item.account, imageReference: imageMediaReference)) + } + cover.set(arguments: arguments) + needsLayout = true + } + + override func layout() { + super.layout() + + cover.setFrameSize(PeerMediaIconSize) + + trackName.resize(frame.width - 60) + artistName.resize(frame.width - 60) + + trackName.setFrameOrigin(NSMakePoint(50, 2)) + artistName.setFrameOrigin(NSMakePoint(50, frame.height - artistName.frame.height - 2)) + + cover.centerY(x: 0) + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private final class PlayerListHandlingView: View { + fileprivate let playPause = Button() + private let playPauseView = LottiePlayerView() + fileprivate let prev = ImageButton() + fileprivate let next = ImageButton() + fileprivate let order = ImageButton() + fileprivate let iteration = ImageButton() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(playPause) + addSubview(prev) + addSubview(next) + addSubview(order) + addSubview(iteration) + playPause.addSubview(playPauseView) + playPause.setFrameSize(NSMakeSize(40, 40)) + + playPauseView.setFrameSize(playPause.frame.size) + + prev.autohighlight = false + next.autohighlight = false + playPause.autohighlight = false + iteration.autohighlight = false + order.autohighlight = false + + prev.scaleOnClick = true + next.scaleOnClick = true + playPause.scaleOnClick = true + iteration.scaleOnClick = true + order.scaleOnClick = true + + updateLocalizationAndTheme(theme: theme) + + } + + override func layout() { + super.layout() + playPause.centerX() + playPause.centerY(addition: -3) + prev.centerY(x: playPause.frame.minX - prev.frame.width - 10) + next.centerY(x: playPause.frame.maxX + 10) + iteration.centerY(x: frame.width - iteration.frame.width) + order.centerY(x: 0) + } + + func update(_ item: APSongItem, controller: APController, animated: Bool) { + + next.userInteractionEnabled = controller.nextEnabled + prev.userInteractionEnabled = controller.prevEnabled + + switch controller.nextEnabled { + case true: + next.set(image: theme.icons.playlist_next, for: .Normal) + case false: + next.set(image: theme.icons.playlist_next_locked, for: .Normal) + } + + switch controller.prevEnabled { + case true: + prev.set(image: theme.icons.playlist_prev, for: .Normal) + case false: + prev.set(image: theme.icons.playlist_prev_locked, for: .Normal) + } + + switch controller.state.status { + case .playing: + play(animated: animated, sticker: LocalAnimatedSticker.playlist_play_pause) + case .paused: + play(animated: animated, sticker: LocalAnimatedSticker.playlist_pause_play) + default: + break + } + + switch controller.state.orderState { + case .normal: + order.set(image: theme.icons.playlist_order_normal, for: .Normal) + case .reversed: + order.set(image: theme.icons.playlist_order_reversed, for: .Normal) + case .random: + order.set(image: theme.icons.playlist_order_random, for: .Normal) + } + + switch controller.state.repeatState { + case .none: + iteration.set(image: theme.icons.playlist_repeat_none, for: .Normal) + case .one: + iteration.set(image: theme.icons.playlist_repeat_one, for: .Normal) + case .circle: + iteration.set(image: theme.icons.playlist_repeat_circle, for: .Normal) + } + order.sizeToFit() + iteration.sizeToFit() + next.sizeToFit() + prev.sizeToFit() + needsLayout = true + } + private func play(animated: Bool, sticker: LocalAnimatedSticker) { + let data = sticker.data + if let data = data { + + let current: Int32 + let total: Int32 + if playPauseView.animation?.key.key != LottieAnimationKey.bundle(sticker.rawValue) { + current = playPauseView.currentFrame ?? 0 + total = playPauseView.totalFrames ?? 0 + } else { + current = 0 + total = playPauseView.currentFrame ?? 0 + } + let animation = LottieAnimation(compressed: data, key: .init(key: .bundle(sticker.rawValue), size: NSMakeSize(46, 46)), cachePurpose: .none, playPolicy: .toEnd(from: animated ? total - current : .max), colors: [.init(keyPath: "", color: theme.colors.text)], runOnQueue: .mainQueue()) + playPauseView.set(animation) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + + } +} + +private final class PlayerListControlsView : View { + private let separator: View = View() + fileprivate var trackView: PlayerListTrackView? + fileprivate let progress: LinearProgressControl = LinearProgressControl(progressHeight: 5) + private let playedView: TextView = TextView() + private let restView: TextView = TextView() + fileprivate let handlings: PlayerListHandlingView = PlayerListHandlingView(frame: .zero) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(progress) + addSubview(playedView) + addSubview(restView) + addSubview(handlings) + addSubview(separator) + updateLocalizationAndTheme(theme: theme) + separator.layer?.opacity = 0 + + } + + var searchClick:((String)->Void)? = nil + + private var track: APSongItem? + + func update(_ track: APSongItem, controller: APController, animated: Bool) { + if track != self.track { + if let current = self.trackView { + self.trackView = nil + if animated { + current.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak current] _ in + current?.removeFromSuperview() + }) + } else { + current.removeFromSuperview() + } + } + let trackView = PlayerListTrackView(frame: NSMakeRect(10, 10, frame.width - 20, 40)) + self.trackView = trackView + addSubview(trackView) + trackView.update(track) + let trackName = track.performerName + + trackView.artistName.set(handler: { [weak self] _ in + self?.searchClick?(trackName) + }, for: .Click) + + if animated { + trackView.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } + + self.track = track + + var played: Int? = nil + var rest: Int? = nil + + handlings.update(track, controller: controller, animated: animated) + + switch track.state { + case .waiting: + break + case .stoped: + self.progress.set(progress: 0, animated: animated) + case let .playing(current, duration, progress), let .paused(current, duration, progress): + self.progress.set(progress: CGFloat(progress == .nan ? 0 : progress), animated: animated, duration: 0.2) + played = Int(current) + rest = Int(duration - current) + case let .fetching(progress): + self.progress.set(progress: CGFloat(progress), animated: animated) + } + + if let played = played, let rest = rest { + let playedLayout = TextViewLayout.init(.initialize(string: timerText(played), color: theme.colors.grayText, font: .normal(.short))) + let restLayout = TextViewLayout.init(.initialize(string: timerText(rest), color: theme.colors.grayText, font: .normal(.short))) + + playedLayout.measure(width: .greatestFiniteMagnitude) + restLayout.measure(width: .greatestFiniteMagnitude) + + self.playedView.update(playedLayout) + self.restView.update(restLayout) + } + needsLayout = true + } + + + + override func layout() { + separator.frame = NSMakeRect(0, frame.height - .borderSize, frame.width, .borderSize) + trackView?.frame = NSMakeRect(10, 10, frame.width - 20, 40) + progress.frame = NSMakeRect(10, 60, frame.width - 20, 12) + + playedView.setFrameOrigin(NSMakePoint(progress.frame.minX, progress.frame.maxY + 3)) + restView.setFrameOrigin(NSMakePoint(progress.frame.maxX - restView.frame.width, progress.frame.maxY + 3)) + + handlings.frame = NSMakeRect(10, restView.frame.maxY, frame.width - 20, 40) + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = theme as! TelegramPresentationTheme + separator.backgroundColor = theme.colors.border + + progress.insets = NSEdgeInsetsMake(0, 0, 0, 0) + progress.scrubberImage = generateImage(NSMakeSize(8, 8), contextGenerator: { size, ctx in + let rect = CGRect(origin: .zero, size: size) + ctx.clear(rect) + ctx.setFillColor(theme.colors.accent.cgColor) + ctx.fillEllipse(in: rect) + }) + progress.roundCorners = true + progress.alignment = .center + progress.liveScrobbling = false + progress.fetchingColor = theme.colors.grayIcon.withAlphaComponent(0.6) + progress.containerBackground = theme.colors.grayIcon.withAlphaComponent(0.2) + progress.style = ControlStyle(foregroundColor: theme.colors.accent, backgroundColor: .clear, highlightColor: .clear) + progress.set(progress: 0, animated: false, duration: 0) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + fileprivate func updateScroll(_ position: ScrollPosition, tableFrame: NSRect) { + separator.change(opacity: position.rect.minY > tableFrame.height ? 1 : 0) + } +} + +final class PlayerListView : View, APDelegate { + + let tableView: TableView + private let controls: PlayerListControlsView = PlayerListControlsView(frame: .zero) + private let bufferingStatusDisposable = MetaDisposable() + private var ranges: (IndexSet, Int)? + private(set) var controller:APController? { + didSet { + oldValue?.remove(listener: self) + controller?.add(listener: self) + if let controller = controller { + self.bufferingStatusDisposable.set((controller.bufferingStatus + |> deliverOnMainQueue).start(next: { [weak self] status in + if let status = status { + self?.updateStatus(status.0, status.1) + } + })) + } else { + self.bufferingStatusDisposable.set(nil) + } + } + } + + func updateStatus(_ ranges: IndexSet, _ size: Int) { + self.ranges = (ranges, size) + if let ranges = self.ranges, !ranges.0.isEmpty, ranges.1 != 0 { + for range in ranges.0.rangeView { + var progress = (CGFloat(range.count) / CGFloat(ranges.1)) + progress = progress == 1.0 ? 0 : progress + controls.progress.set(fetchingProgress: progress, animated: progress > 0) + break + } + } + } + + required init(frame frameRect: NSRect) { + tableView = TableView(frame: .zero) + super.init(frame: frameRect) + addSubview(tableView) + addSubview(controls) + + controls.progress.onUserChanged = { [weak self] progress in + self?.controller?.set(trackProgress: progress) + self?.controls.progress.set(progress: CGFloat(progress), animated: false) + } + + var paused: Bool = false + + controls.progress.startScrobbling = { [weak self] in + guard let controller = self?.controller else { + return + } + if controller.isPlaying { + _ = self?.controller?.pause() + paused = true + } else { + paused = false + } + } + + controls.progress.endScrobbling = { [weak self] in + if paused { + DispatchQueue.main.async { + _ = self?.controller?.play() + } + } + } + + controls.handlings.next.set(handler: { [weak self] _ in + self?.controller?.next() + self?.scrollToCurrent() + }, for: .Click) + + controls.handlings.prev.set(handler: { [weak self] _ in + self?.controller?.prev() + self?.scrollToCurrent() + }, for: .Click) + + controls.handlings.playPause.set(handler: { [weak self] _ in + self?.controller?.playOrPause() + }, for: .Click) + + controls.handlings.iteration.set(handler: { [weak self] _ in + self?.controller?.nextRepeatState() + }, for: .Click) + + controls.handlings.order.set(handler: { [weak self] _ in + self?.controller?.nextOrderState() + }, for: .Click) + + tableView.addScroll(listener: TableScrollListener(dispatchWhenVisibleRangeUpdated: false, { [weak self] position in + if let `self` = self { + self.controls.updateScroll(position, tableFrame: self.tableView.frame) + } + })) + + controls.searchClick = { [weak self] text in + let context = self?.controller?.context + if let context = context { + context.sharedContext.bindings.mainController().focusSearch(animated: true, text: text) + } + } + } + + + func setController(_ controller: APController?) { + self.controller = controller + if let controller = controller, let item = controller.currentSong { + self.controls.update(item, controller: controller, animated: false) + } + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + controller?.notifyGlobalStateChanged(animated: false) + } + + private func scrollToCurrent() { + if let entry = controller?.currentSong?.entry { + switch entry { + case let .song(message): + self.tableView.scroll(to: .center(id: PeerMediaSharedEntryStableId.messageId(message.id), innerId: nil, animated: true, focus: .init(focus: false), inset: 0)) + default: + break + } + } + } + + override func layout() { + super.layout() + controls.frame = NSMakeRect(0, 0, frame.width, 140) + tableView.frame = NSMakeRect(0, controls.frame.maxY, frame.width, frame.height - controls.frame.height) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func songDidChanged(song: APSongItem, for controller: APController, animated: Bool) { + self.controls.update(song, controller: controller, animated: true) + } + + func songDidChangedState(song: APSongItem, for controller: APController, animated: Bool) { + controls.update(song, controller: controller, animated: true) + } + + func songDidStartPlaying(song: APSongItem, for controller: APController, animated: Bool) { + controls.update(song, controller: controller, animated: true) + } + + func songDidStopPlaying(song: APSongItem, for controller: APController, animated: Bool) { + controls.update(song, controller: controller, animated: true) + } + + func playerDidChangedTimebase(song: APSongItem, for controller: APController, animated: Bool) { + controls.update(song, controller: controller, animated: true) + } + + func audioDidCompleteQueue(for controller: APController, animated: Bool) { + } + +} + + +class PlayerListController: TelegramGenericViewController { + private let audioPlayer: InlineAudioPlayerView + private let chatInteraction: ChatInteraction + private let disposable = MetaDisposable() + private let messageIndex: MessageIndex + private let messages: [Message] + init(audioPlayer: InlineAudioPlayerView, context: AccountContext, currentContext: AccountContext, messageIndex: MessageIndex, messages: [Message] = []) { + self.chatInteraction = ChatInteraction(chatLocation: .peer(messageIndex.id.peerId), context: context) + self.messageIndex = messageIndex + self.audioPlayer = audioPlayer + + self.messages = messages + super.init(context) + + + chatInteraction.inlineAudioPlayer = { [weak self] controller in + let object = InlineAudioPlayerView.ContextObject(controller: controller, context: currentContext, tableView: self?.tableView, supportTableView: nil) + self?.audioPlayer.update(with: object) + self?.genericView.setController(controller) + } + } + + var tableView: TableView { + return genericView.tableView + } + + deinit { + disposable.dispose() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + window?.set(handler: { [weak self] event in + self?.genericView.controller?.prev() + return .invoked + }, with: self, for: .LeftArrow, priority: .modal) + + window?.set(handler: { [weak self] event in + self?.genericView.controller?.next() + return .invoked + }, with: self, for: .RightArrow, priority: .modal) + } + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + window?.removeObserver(for: self) + } + + override func viewDidLoad() { + super.viewDidLoad() + let chatLocationInput = (self.audioPlayer.controller as? APChatController)?.chatLocationInput + tableView.getBackgroundColor = { + return theme.colors.background + } + + let location = ValuePromise(ignoreRepeated: true) + + let historyViewUpdate = location.get() |> deliverOnMainQueue + |> mapToSignal { [weak self] location -> Signal<(PeerMediaUpdate, TableScrollState?), NoError> in + + guard let `self` = self else {return .complete()} + + return chatHistoryViewForLocation(location, context: self.chatInteraction.context, chatLocation: self.chatInteraction.chatLocation, fixedCombinedReadStates: nil, tagMask: [.music], additionalData: [], chatLocationInput: chatLocationInput) |> mapToQueue { view -> Signal<(PeerMediaUpdate, TableScrollState?), NoError> in + switch view { + case .Loading: + return .single((PeerMediaUpdate(), nil)) + case let .HistoryView(view: view, _, scroll, _): + var messages:[Message] = [] + for entry in view.entries { + messages.append(entry.message) + } + let laterId = view.laterId + let earlierId = view.earlierId + + var state: TableScrollState? + if let scroll = scroll { + switch scroll { + case let .index(_, position, _, _): + state = position + default: + break + } + } + return .single((PeerMediaUpdate(messages: messages, updateType: .history, laterId: laterId, earlierId: earlierId), state)) + } + } + } + + let animated: Atomic = Atomic(value: false) + let context = self.chatInteraction.context + let previous:Atomic<[PlayerListEntry]> = Atomic(value: []) + let updateView = Atomic(value: nil) + + + let arguments = PlayerListArguments(chatInteraction: chatInteraction) + + let historyViewTransition: Signal + if messages.isEmpty { + historyViewTransition = historyViewUpdate |> deliverOnPrepareQueue |> map { update, scroll -> TableUpdateTransition in + let animated = animated.swap(true) + let scroll:TableScrollState = scroll ?? (animated ? .none(nil) : .saveVisible(.upper)) + + let entries = playerAudioEntries(update, timeDifference: context.timeDifference) + _ = updateView.swap(update) + + return preparedAudioListTransition(from: previous.swap(entries), to: entries, initialSize: NSMakeSize(300, 0), arguments: arguments, animated: animated, scroll: scroll) + + } |> deliverOnMainQueue + } else { + let update = PeerMediaUpdate(messages: messages, updateType: .search, laterId: nil, earlierId: nil, automaticDownload: .defaultSettings, searchState: .init(state: .None, request: nil)) + let entries = playerAudioEntries(update, timeDifference: context.timeDifference) + _ = updateView.swap(update) + let transition = preparedAudioListTransition(from: previous.swap(entries), to: entries, initialSize: NSMakeSize(300, 0), arguments: arguments, animated: false, scroll: .none(nil)) + historyViewTransition = .single(transition) + } + + + + + disposable.set(historyViewTransition.start(next: { [weak self] transition in + guard let `self` = self else {return} + self.tableView.merge(with: transition) + if !self.didSetReady, !self.tableView.isEmpty { + self.view.setFrameSize(300, min(self.tableView.listHeight + 140, 325)) + self.tableView.scroll(to: .top(id: PeerMediaSharedEntryStableId.messageId(self.messageIndex.id), innerId: nil, animated: false, focus: .init(focus: false), inset: -25)) + self.genericView.setController(self.audioPlayer.controller) + self.readyOnce() + } + })) + + location.set(.Navigation(index: MessageHistoryAnchorIndex.message(messageIndex), anchorIndex: MessageHistoryAnchorIndex.message(messageIndex), count: 50, side: .upper)) + + + tableView.setScrollHandler { scroll in + let view = updateView.modify({$0}) + if let view = view { + var messageIndex:MessageIndex? + switch scroll.direction { + case .bottom: + messageIndex = view.earlierId + case .top: + messageIndex = view.laterId + case .none: + break + } + + if let messageIndex = messageIndex { + let _ = animated.swap(false) + location.set(.Navigation(index: MessageHistoryAnchorIndex.message(messageIndex), anchorIndex: MessageHistoryAnchorIndex.message(messageIndex), count: 50, side: scroll.direction == .bottom ? .lower : .upper)) + } + } + } + } + +} diff --git a/Telegram-Mac/PollResultController.swift b/Telegram-Mac/PollResultController.swift new file mode 100644 index 0000000000..32351af83f --- /dev/null +++ b/Telegram-Mac/PollResultController.swift @@ -0,0 +1,386 @@ +// +// PollResultController.swift +// Telegram +// +// Created by Mikhail Filimonov on 07.01.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit +import TelegramCore +import Postbox + + + +private struct PollResultState : Equatable { + let results: PollResultsState? + let shouldLoadMore: Data? + let poll: TelegramMediaPoll + let expandedOptions: [Data: Int] + + init(results: PollResultsState?, poll: TelegramMediaPoll, shouldLoadMore: Data?, expandedOptions: [Data: Int]) { + self.results = results + self.poll = poll + self.shouldLoadMore = nil + self.expandedOptions = expandedOptions + } + func withUpdatedResults(_ results: PollResultsState?) -> PollResultState { + return PollResultState(results: results, poll: self.poll, shouldLoadMore: self.shouldLoadMore, expandedOptions: self.expandedOptions) + } + func withUpdatedShouldLoadMore(_ shouldLoadMore: Data?) -> PollResultState { + return PollResultState(results: self.results, poll: self.poll, shouldLoadMore: shouldLoadMore, expandedOptions: self.expandedOptions) + } + func withAddedExpandedOption(_ identifier: Data) -> PollResultState { + var expandedOptions = self.expandedOptions + if let optionState = results?.options[identifier] { + expandedOptions[identifier] = optionState.peers.count + } + + return PollResultState(results: self.results, poll: self.poll, shouldLoadMore: self.shouldLoadMore, expandedOptions: expandedOptions) + } + func withRemovedExpandedOption(_ identifier: Data) -> PollResultState { + var expandedOptions = self.expandedOptions + expandedOptions.removeValue(forKey: identifier) + return PollResultState(results: self.results, poll: self.poll, shouldLoadMore: self.shouldLoadMore, expandedOptions: expandedOptions) + } +} +private func _id_option(_ identifier: Data, _ peerId: PeerId) -> InputDataIdentifier { + return InputDataIdentifier("_id_option_\(identifier.base64EncodedString())_\(peerId.toInt64())") +} +private func _id_load_more(_ identifier: Data) -> InputDataIdentifier { + return InputDataIdentifier("_id_load_more_\(identifier.base64EncodedString())") +} +private func _id_loading_for(_ identifier: Data) -> InputDataIdentifier { + return InputDataIdentifier("_id_loading_for_\(identifier.base64EncodedString())") +} +private func _id_option_header(_ identifier: Data) -> InputDataIdentifier { + return InputDataIdentifier("_id_option_header_\(identifier.base64EncodedString())") +} +private func _id_option_empty(_ index: Int) -> InputDataIdentifier { + return InputDataIdentifier("_id_option_empty_\(index)") +} + +private let collapsedResultCount: Int = 10 +private let collapsedInitialLimit: Int = 14 + + +private let _id_loading = InputDataIdentifier("_id_loading") + +private func pollResultEntries(_ state: PollResultState, context: AccountContext, openProfile:@escaping(PeerId)->Void, expandOption: @escaping(Data)->Void, collapseOption: @escaping(Data)->Void) -> [InputDataEntry] { + var sectionId: Int32 = 0 + var index: Int32 = 0 + + var entries:[InputDataEntry] = [] + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(state.poll.text), data: InputDataGeneralTextData(color: theme.colors.text, detectBold: true, viewType: .modern(position: .inner, insets: NSEdgeInsetsMake(0, 16, 0, 16)), fontSize: .huge))) + index += 1 + + + + let poll = state.poll + + var votes:[Int] = [] + + + + for option in poll.options { + let count = Int(poll.results.voters?.first(where: {$0.opaqueIdentifier == option.opaqueIdentifier})?.count ?? 0) + votes.append(count) + } + + let percents = countNicePercent(votes: votes, total: Int(poll.results.totalVoters ?? 0)) + + struct Option : Equatable { + let option: TelegramMediaPollOption + let percent: Int + let voters:PollResultsOptionState? + let votesCount: Int + } + + + var options:[Option] = [] + for (i, option) in poll.options.enumerated() { + if let voters = state.results?.options[option.opaqueIdentifier], !voters.peers.isEmpty { + let votesCount = Int(poll.results.voters?.first(where: {$0.opaqueIdentifier == option.opaqueIdentifier})?.count ?? 0) + options.append(Option(option: option, percent: percents[i], voters: voters, votesCount: votesCount)) + } else { + let votesCount = Int(poll.results.voters?.first(where: {$0.opaqueIdentifier == option.opaqueIdentifier})?.count ?? 0) + options.append(Option(option: option, percent: percents[i], voters: nil, votesCount: votesCount)) + } + } + + + var isEmpty = false + if let resultsState = state.results { + for (_, optionState) in resultsState.options { + if !optionState.hasLoadedOnce { + isEmpty = true + break + } + } + } + + + for option in options { + if option.votesCount > 0 { + if option == options.first { + entries.append(.sectionId(sectionId, type: .customModern(16))) + sectionId += 1 + } else { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + } + + let text = option.option.text + let additionText:String = " — \(option.percent)%" + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_option_header(option.option.opaqueIdentifier), equatable: InputDataEquatable(state), comparable: nil, item: { initialSize, stableId in + + let collapse:(()->Void)? + if state.expandedOptions[option.option.opaqueIdentifier] != nil { + collapse = { + collapseOption(option.option.opaqueIdentifier) + } + } else { + collapse = nil + } + + return PollResultStickItem(initialSize, stableId: stableId, left: text, additionText: additionText, right: poll.isQuiz ? L10n.chatQuizTotalVotesCountable(option.votesCount) : L10n.chatPollTotalVotes1Countable(option.votesCount), collapse: collapse, viewType: .textTopItem) + + })) + index += 1 + + if let optionState = option.voters { + + let optionExpandedAtCount = state.expandedOptions[option.option.opaqueIdentifier] + + var peers = optionState.peers + let count = optionState.count + + let displayCount: Int + if peers.count > collapsedInitialLimit + 1 { + if optionExpandedAtCount != nil { + displayCount = peers.count + } else { + displayCount = collapsedResultCount + } + } else { + if let optionExpandedAtCount = optionExpandedAtCount { + if optionExpandedAtCount == collapsedInitialLimit + 1 && optionState.canLoadMore { + displayCount = collapsedResultCount + } else { + displayCount = peers.count + } + } else { + if !optionState.canLoadMore { + displayCount = peers.count + } else { + displayCount = collapsedResultCount + } + } + } + + peers = Array(peers.prefix(displayCount)) + + for (i, voter) in peers.enumerated() { + if let peer = voter.peer { + var viewType = bestGeneralViewType(peers, for: i) + if i == peers.count - 1, optionState.canLoadMore { + if peers.count == 1 { + viewType = .firstItem + } else { + viewType = .innerItem + } + } + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_option(option.option.opaqueIdentifier, peer.id), equatable: InputDataEquatable(option), comparable: nil, item: { initialSize, stableId in + return ShortPeerRowItem(initialSize, peer: peer, account: context.account, stableId: stableId, height: 46, photoSize: NSMakeSize(32, 32), inset: NSEdgeInsets(left: 30, right: 30), generalType: .none, viewType: viewType, action: { + openProfile(peer.id) + }) + })) + index += 1 + } + } + + let remainingCount = count - peers.count + + + + if remainingCount > 0 { + if optionState.isLoadingMore && state.expandedOptions[option.option.opaqueIdentifier] != nil { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_loading_for(option.option.opaqueIdentifier), equatable: InputDataEquatable(option), comparable: nil, item: { initialSize, stableId in + return LoadingTableItem(initialSize, height: 41, stableId: stableId, viewType: .lastItem) + })) + index += 1 + } else { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_load_more(option.option.opaqueIdentifier), equatable: InputDataEquatable(option), comparable: nil, item: { initialSize, stableId in + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.pollResultsLoadMoreCountable(remainingCount), nameStyle: blueActionButton, type: .none, viewType: .lastItem, action: { + expandOption(option.option.opaqueIdentifier) + }, thumb: GeneralThumbAdditional(thumb: theme.icons.chatSearchUp, textInset: 52, thumbInset: 4)) + })) + index += 1 + } + } + } else { + let displayCount: Int + let voterCount = option.votesCount + if voterCount > collapsedInitialLimit { + displayCount = collapsedResultCount + } else { + displayCount = voterCount + } + let remainingCount: Int? + if displayCount < voterCount { + remainingCount = voterCount - displayCount + } else { + remainingCount = nil + } + + var display:[Int] = [] + for peerIndex in 0 ..< displayCount { + display.append(peerIndex) + } + + for peerIndex in display { + var viewType = bestGeneralViewType(display, for: peerIndex) + if peerIndex == displayCount - 1, remainingCount != nil { + if displayCount == 1 { + viewType = .firstItem + } else { + viewType = .innerItem + } + } + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_option_empty(Int(index)), equatable: nil, comparable: nil, item: { initialSize, stableId in + return PeerEmptyHolderItem(initialSize, stableId: stableId, height: 46, photoSize: NSMakeSize(32, 32), viewType: viewType) + })) + index += 1 + } + if let remainingCount = remainingCount { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_load_more(option.option.opaqueIdentifier), equatable: InputDataEquatable(option), comparable: nil, item: { initialSize, stableId in + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.pollResultsLoadMoreCountable(remainingCount), nameStyle: blueActionButton, type: .none, viewType: .lastItem, thumb: GeneralThumbAdditional(thumb: theme.icons.chatSearchUpDisabled, textInset: 52, thumbInset: 4), enabled: false) + })) + index += 1 + } + } + } + + + } + + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func PollResultController(context: AccountContext, message: Message, scrollToOption: Data? = nil) -> InputDataModalController { + + let poll = message.media[0] as! TelegramMediaPoll + + var scrollToOption = scrollToOption + + let resultsContext: PollResultsContext = context.engine.messages.pollResults(messageId: message.id, poll: poll) + + let initialState = PollResultState(results: nil, poll: poll, shouldLoadMore: nil, expandedOptions: [:]) + + let disposable = MetaDisposable() + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((PollResultState) -> PollResultState) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + disposable.set(resultsContext.state.start(next: { results in + updateState { + $0.withUpdatedResults(results) + } + })) + + var openProfile:((PeerId)->Void)? = nil + + let signal = statePromise.get() |> map { + pollResultEntries($0, context: context, openProfile: { peerId in + openProfile?(peerId) + }, expandOption: { identifier in + updateState { + $0.withAddedExpandedOption(identifier) + } + resultsContext.loadMore(optionOpaqueIdentifier: identifier) + }, collapseOption: { identifier in + updateState { + $0.withRemovedExpandedOption(identifier) + } + }) + } |> map { + InputDataSignalValue(entries: $0, animated: true) + } + + let controller = InputDataController(dataSignal: signal, title: !poll.isQuiz ? L10n.pollResultsTitlePoll : L10n.pollResultsTitleQuiz) + + controller.getBackgroundColor = { + theme.colors.background + } + + controller.contextOject = resultsContext + + let modalController = InputDataModalController(controller) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) + + controller.centerModalHeader = ModalHeaderData(title: controller.defaultBarTitle, subtitle: poll.isQuiz ? L10n.chatQuizTotalVotesCountable(Int(poll.results.totalVoters ?? 0)) : L10n.chatPollTotalVotes1Countable(Int(poll.results.totalVoters ?? 0))) + + controller.getBackgroundColor = { + theme.colors.listBackground + } + + + + openProfile = { [weak modalController] peerId in + context.sharedContext.bindings.rootNavigation().push(PeerInfoController(context: context, peerId: peerId)) + modalController?.close() + } + controller.afterTransaction = { controller in + if let scroll = scrollToOption { + let item = controller.tableView.item(stableId: InputDataEntryId.custom(_id_option_header(scroll))) + + if let item = item { + controller.tableView.scroll(to: .top(id: item.stableId, innerId: nil, animated: true, focus: .init(focus: true), inset: -10)) + scrollToOption = nil + } + } + } + + controller.didLoaded = { controller, _ in + controller.tableView.set(stickClass: PollResultStickItem.self, handler: { _ in + + }) + } + +// controller.didLoaded = { controller, _ in +// controller.tableView.setScrollHandler { position in +// switch position.direction { +// case .bottom: +// let shouldLoadMore = stateValue.with { $0.shouldLoadMore } +// if let shouldLoadMore = shouldLoadMore { +// resultsContext.loadMore(optionOpaqueIdentifier: shouldLoadMore) +// } +// break +// default: +// break +// } +// } +// } + + + + return modalController +} diff --git a/Telegram-Mac/PollResultStickItem.swift b/Telegram-Mac/PollResultStickItem.swift new file mode 100644 index 0000000000..913a153b50 --- /dev/null +++ b/Telegram-Mac/PollResultStickItem.swift @@ -0,0 +1,168 @@ +// +// PollResultStickItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 19/01/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +class PollResultStickItem: TableStickItem { + + let leftLayout:TextViewLayout + let leftAdditionLayout: TextViewLayout + let rightLayout: TextViewLayout + let viewType: GeneralViewType + let inset: NSEdgeInsets + let collapse: (()->Void)? + let _stableId: AnyHashable + init(_ initialSize:NSSize, stableId: AnyHashable, left: String, additionText: String, right: String, collapse: (()->Void)?, viewType: GeneralViewType) { + self.viewType = viewType + self._stableId = stableId + self.inset = NSEdgeInsets(left: 30, right: 30) + self.collapse = collapse + self.leftLayout = TextViewLayout(.initialize(string: left, color: theme.colors.listGrayText, font: .normal(11.5)), maximumNumberOfLines: 1, truncationType: .end) + self.leftAdditionLayout = TextViewLayout(.initialize(string: additionText, color: theme.colors.listGrayText, font: .normal(11.5)), maximumNumberOfLines: 1, truncationType: .end) + + if let collapse = collapse { + let attrs = parseMarkdownIntoAttributedString(L10n.pollResultsCollapse, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: .normal(11.5), textColor: theme.colors.listGrayText), bold: MarkdownAttributeSet(font: .bold(11.5), textColor: theme.colors.listGrayText), link: MarkdownAttributeSet(font: .normal(11.5), textColor: theme.colors.link), linkAttribute: { contents in + return (NSAttributedString.Key.link.rawValue, inAppLink.callback(contents, { _ in })) + })) + + self.rightLayout = TextViewLayout(attrs, maximumNumberOfLines: 1, truncationType: .end, alignment: .center) + self.rightLayout.interactions = TextViewInteractions(processURL: { _ in + collapse() + }) + } else { + self.rightLayout = TextViewLayout(.initialize(string: right, color: theme.colors.listGrayText, font: .normal(11.5)), maximumNumberOfLines: 1, truncationType: .end, alignment: .center) + } + + + + super.init(initialSize) + _ = makeSize(initialSize.width, oldWidth: 0) + } + + + + override var canBeAnchor: Bool { + return false + } + + required init(_ initialSize: NSSize) { + self.viewType = .legacy + self.leftLayout = TextViewLayout(NSAttributedString()) + self.rightLayout = TextViewLayout(NSAttributedString()) + self.leftAdditionLayout = TextViewLayout(NSAttributedString()) + self.inset = NSEdgeInsets(left: 30, right: 30) + self.collapse = nil + self._stableId = arc4random() + super.init(initialSize) + } + + override func makeSize(_ width: CGFloat, oldWidth:CGFloat) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) + rightLayout.measure(width: .greatestFiniteMagnitude) + leftAdditionLayout.measure(width: .greatestFiniteMagnitude) + + let blockWidth = min(600, width - inset.left - inset.right) + leftLayout.measure(width: blockWidth - rightLayout.layoutSize.width - viewType.innerInset.left * 3 - leftAdditionLayout.layoutSize.width) + + return success + } + + override var stableId: AnyHashable { + return self._stableId + } + + override var height: CGFloat { + return 30 + } + + override func viewClass() -> AnyClass { + return PollResultStickView.self + } + +} + + +private final class PollResultStickView : TableStickView { + private let containerView = GeneralRowContainerView(frame: NSZeroRect) + private let textView = TextView() + private let textAdditionView = TextView() + + private let rightView = TextView() + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(self.containerView) + containerView.addSubview(self.textView) + containerView.addSubview(self.rightView) + containerView.addSubview(self.textAdditionView) + self.textView.disableBackgroundDrawing = true + self.textView.isSelectable = false + self.textView.userInteractionEnabled = false + + self.textAdditionView.disableBackgroundDrawing = true + self.textAdditionView.isSelectable = false + self.textAdditionView.userInteractionEnabled = false + + self.rightView.disableBackgroundDrawing = true + self.rightView.isSelectable = false + + } + + override var header: Bool { + didSet { + updateColors() + } + } + + + override var backdorColor: NSColor { + return theme.colors.listBackground + } + + override func updateColors() { + guard let item = item as? PollResultStickItem else { + return + } + self.backgroundColor = item.viewType.rowBackground + self.containerView.backgroundColor = backdorColor + } + + override func layout() { + super.layout() + guard let item = item as? PollResultStickItem else { + return + } + + let blockWidth = min(600, frame.width - item.inset.left - item.inset.right) + + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - blockWidth) / 2), item.inset.top, blockWidth, frame.height - item.inset.bottom - item.inset.top) + self.containerView.setCorners([]) + + textView.centerY(x: item.viewType.innerInset.left) + textAdditionView.centerY(x: textView.frame.maxX) + rightView.centerY(x: self.containerView.frame.width - item.viewType.innerInset.left - rightView.frame.width) + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? PollResultStickItem else { + return + } + self.textView.update(item.leftLayout) + self.textAdditionView.update(item.leftAdditionLayout) + self.rightView.update(item.rightLayout) + + needsLayout = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/PollTimerView.swift b/Telegram-Mac/PollTimerView.swift new file mode 100644 index 0000000000..7451573d21 --- /dev/null +++ b/Telegram-Mac/PollTimerView.swift @@ -0,0 +1,273 @@ +// +// PollTimerView.swift +// Telegram +// +// Created by Mikhail Filimonov on 09/04/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + + + +private func textForTimeout(value: Int) -> String { + //TODO: localize + if value > 60 * 60 { + let hours = value / (60 * 60) + return "\(hours)h" + } else { + let minutes = value / 60 + let seconds = value % 60 + let minutesPadding = minutes < 10 ? "0" : "" + let secondsPadding = seconds < 10 ? "0" : "" + return "\(minutesPadding)\(minutes):\(secondsPadding)\(seconds)" + } +} + +private enum ContentState: Equatable { + case clock(NSColor) + case timeout(NSColor, CGFloat) +} + +private struct ContentParticle { + var position: CGPoint + var direction: CGPoint + var velocity: CGFloat + var alpha: CGFloat + var lifetime: Double + var beginTime: Double + + init(position: CGPoint, direction: CGPoint, velocity: CGFloat, alpha: CGFloat, lifetime: Double, beginTime: Double) { + self.position = position + self.direction = direction + self.velocity = velocity + self.alpha = alpha + self.lifetime = lifetime + self.beginTime = beginTime + } +} + +final class PollBubbleTimerView: View { + private struct Params: Equatable { + var regularColor: NSColor + var proximityColor: NSColor + var timeout: Int32 + var deadlineTimestamp: Int32? + } + + private var animator: ConstantDisplayLinkAnimator? + private let textView: TextView = TextView() + private let contentView: ImageView = ImageView() + private var currentContentState: ContentState? + private var particles: [ContentParticle] = [] + + private var currentParams: Params? + + var reachedTimeout: (() -> Void)? + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + + self.addSubview(self.textView) + self.addSubview(self.contentView) + textView.userInteractionEnabled = false + textView.isSelectable = false + textView.isEventLess = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.animator?.invalidate() + } + + func update(regularColor: NSColor, proximityColor: NSColor, timeout: Int32, deadlineTimestamp: Int32?) { + let params = Params( + regularColor: regularColor, + proximityColor: proximityColor, + timeout: timeout, + deadlineTimestamp: deadlineTimestamp + ) + self.currentParams = params + + self.updateValues() + } + + override func viewWillMove(toWindow newWindow: NSWindow?) { + super.viewWillMove(toWindow: newWindow) + self.animator?.isPaused = newWindow == nil + } + + private func updateValues() { + guard let params = self.currentParams else { + return + } + + let fractionalTimeout: Double + + if let deadlineTimestamp = params.deadlineTimestamp { + let fractionalTimestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + fractionalTimeout = min(Double(params.timeout), max(0.0, Double(deadlineTimestamp) + 1.0 - fractionalTimestamp)) + } else { + fractionalTimeout = Double(params.timeout) + } + + let timeout = Int(round(fractionalTimeout)) + + let proximityInterval: Double = 5.0 + let timerInterval: Double = 60.0 + + let isProximity = timeout <= Int(proximityInterval) + let isTimer = timeout <= Int(timerInterval) + + let color = isProximity ? params.proximityColor : params.regularColor + + let attributed = NSAttributedString.initialize(string: textForTimeout(value: timeout), color: color, font: .normal(.short)) + + let textLayout = TextViewLayout(attributed) + textLayout.measure(width: 100) + + + // self.textView.attributedText = NSAttributedString(string: textForTimeout(value: timeout), font: Font.regular(14.0), textColor: color) + let textSize = textLayout.layoutSize + + self.textView.update(textLayout) + + let contentState: ContentState + if isTimer { + var fraction: CGFloat = 1.0 + if fractionalTimeout <= timerInterval { + fraction = CGFloat(fractionalTimeout) / min(CGFloat(timerInterval), CGFloat(params.timeout)) + } + fraction = max(0.0, min(0.99, fraction)) + contentState = .timeout(color, 1.0 - fraction) + } else { + contentState = .clock(color) + } + + if self.currentContentState != contentState { + self.currentContentState = contentState + let image: CGImage? + + let diameter: CGFloat = 14.0 + let inset: CGFloat = 8.0 + let lineWidth: CGFloat = 1.2 + + switch contentState { + case let .clock(color): + image = generateImage(CGSize(width: diameter + inset, height: diameter + inset), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(color.cgColor) + context.setLineWidth(lineWidth) + context.setLineCap(.round) + + let clockFrame = CGRect(origin: CGPoint(x: (size.width - diameter) / 2.0, y: (size.height - diameter) / 2.0), size: CGSize(width: diameter, height: diameter)) + context.strokeEllipse(in: clockFrame.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0)) + + context.move(to: CGPoint(x: size.width / 2.0, y: size.height / 2.0)) + context.addLine(to: CGPoint(x: size.width / 2.0, y: clockFrame.minY + 4.0)) + context.strokePath() + + let topWidth: CGFloat = 4.0 + context.move(to: CGPoint(x: size.width / 2.0 - topWidth / 2.0, y: clockFrame.minY - 2.0)) + context.addLine(to: CGPoint(x: size.width / 2.0 + topWidth / 2.0, y: clockFrame.minY - 2.0)) + context.strokePath() + }) + case let .timeout(color, fraction): + let timestamp = CACurrentMediaTime() + + let center = CGPoint(x: (diameter + inset) / 2.0, y: (diameter + inset) / 2.0) + let radius: CGFloat = (diameter - lineWidth / 2.0) / 2.0 + + let startAngle: CGFloat = -CGFloat.pi / 2.0 + let endAngle: CGFloat = -CGFloat.pi / 2.0 + 2.0 * CGFloat.pi * fraction + + let v = CGPoint(x: sin(endAngle), y: -cos(endAngle)) + let c = CGPoint(x: -v.y * radius + center.x, y: v.x * radius + center.y) + + let dt: CGFloat = 1.0 / 60.0 + var removeIndices: [Int] = [] + for i in 0 ..< self.particles.count { + let currentTime = timestamp - self.particles[i].beginTime + if currentTime > self.particles[i].lifetime { + removeIndices.append(i) + } else { + let input: CGFloat = CGFloat(currentTime / self.particles[i].lifetime) + let decelerated: CGFloat = (1.0 - (1.0 - input) * (1.0 - input)) + self.particles[i].alpha = 1.0 - decelerated + + var p = self.particles[i].position + let d = self.particles[i].direction + let v = self.particles[i].velocity + p = CGPoint(x: p.x + d.x * v * dt, y: p.y + d.y * v * dt) + self.particles[i].position = p + } + } + + for i in removeIndices.reversed() { + self.particles.remove(at: i) + } + + let newParticleCount = 1 + for _ in 0 ..< newParticleCount { + let degrees: CGFloat = CGFloat(arc4random_uniform(140)) - 40.0 + let angle: CGFloat = degrees * CGFloat.pi / 180.0 + + let direction = CGPoint(x: v.x * cos(angle) - v.y * sin(angle), y: v.x * sin(angle) + v.y * cos(angle)) + let velocity = (20.0 + (CGFloat(arc4random()) / CGFloat(UINT32_MAX)) * 4.0) * 0.3 + + let lifetime = Double(0.4 + CGFloat(arc4random_uniform(100)) * 0.01) + + let particle = ContentParticle(position: c, direction: direction, velocity: velocity, alpha: 1.0, lifetime: lifetime, beginTime: timestamp) + self.particles.append(particle) + } + + image = generateImage(CGSize(width: diameter + inset, height: diameter + inset), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(color.cgColor) + context.setFillColor(color.cgColor) + context.setLineWidth(lineWidth) + context.setLineCap(.round) + + let path = CGMutablePath() + path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true) + context.addPath(path) + context.strokePath() + + for particle in self.particles { + let size: CGFloat = 1.15 + context.setAlpha(particle.alpha) + context.fillEllipse(in: CGRect(origin: CGPoint(x: particle.position.x - size / 2.0, y: particle.position.y - size / 2.0), size: CGSize(width: size, height: size))) + } + }) + } + + self.contentView.image = image + self.contentView.sizeToFit() + self.contentView.centerY(x: frame.width - contentView.frame.width) + + self.textView.centerY(x: frame.width - contentView.frame.width - textSize.width - 4) + } + + if let reachedTimeout = self.reachedTimeout, fractionalTimeout <= .ulpOfOne { + reachedTimeout() + } + + if fractionalTimeout <= .ulpOfOne { + self.animator?.invalidate() + self.animator = nil + } else { + if self.animator == nil { + let animator = ConstantDisplayLinkAnimator(update: { [weak self] in + self?.updateValues() + }) + animator.isPaused = self.window == nil + self.animator = animator +// animator.isPaused = self.inHierarchyValue + } + } + } +} diff --git a/Telegram-Mac/PopularPeersRowItem.swift b/Telegram-Mac/PopularPeersRowItem.swift new file mode 100644 index 0000000000..fee6991a5b --- /dev/null +++ b/Telegram-Mac/PopularPeersRowItem.swift @@ -0,0 +1,283 @@ +// +// PopularPeersRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 05/07/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import Postbox +import SwiftSignalKit +import TelegramCore + + +enum PopularItemType : Hashable { + + + var hashValue: Int { + return 0 + } + + static func == (lhs: PopularItemType, rhs: PopularItemType) -> Bool { + switch lhs { + case .savedMessages: + if case .savedMessages = rhs { + return true + } else { + return false + } + case let .articles(unreadCount): + if case .articles(unreadCount) = rhs { + return true + } else { + return false + } + case let .peer(lhsPeer, lhsBadge, lhsActive): + if case let .peer(rhsPeer, rhsBadge, rhsActive) = rhs { + return lhsPeer.isEqual(rhsPeer) && lhsBadge == rhsBadge && lhsActive == rhsActive + } else { + return false + } + } + } + + case savedMessages(Peer) + case articles(Int32) + case peer(Peer, UnreadSearchBadge?, Bool) + + + +} + +private final class PopularPeerItem : TableRowItem { + fileprivate let type: PopularItemType + fileprivate let context: AccountContext + fileprivate let actionHandler: (PopularItemType)->Void + init(type: PopularItemType, context: AccountContext, action: @escaping(PopularItemType)->Void) { + self.type = type + self.context = context + self.actionHandler = action + super.init(NSZeroSize) + } + + override var height: CGFloat { + return 66 + } + + override var width: CGFloat { + return 74 + } + + override var stableId: AnyHashable { + return type + } + + override func menuItems(in location: NSPoint) -> Signal<[ContextMenuItem], NoError> { + var items:[ContextMenuItem] = [] + switch type { + case let .peer(peer, _, _): + items.append(ContextMenuItem(L10n.searchPopularDelete, handler: { [weak self] in + guard let `self` = self else {return} + // self.table?.remove(at: self.index, redraw: true, animation: .effectFade) + _ = self.context.engine.peers.removeRecentPeer(peerId: peer.id).start() + + })) + default: + break + } + + + return .single(items) + } + + override func viewClass() -> AnyClass { + return PopularPeerItemView.self + } +} + + +private final class PopularPeerItemView : HorizontalRowView { + private let imageView: AvatarControl = AvatarControl(font: .avatar(18)) + private let textView: TextView = TextView() + private let badgeView: View = View() + private let activeImage: ImageView = ImageView() + private var badgeNode: BadgeNode? + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + imageView.setFrameSize(45, 45) + addSubview(imageView) + addSubview(textView) + addSubview(activeImage) + activeImage.isEventLess = true + textView.isSelectable = false + textView.userInteractionEnabled = false + badgeView.userInteractionEnabled = false + badgeView.isEventLess = true + imageView.set(handler: { [weak self] _ in + guard let item = self?.item as? PopularPeerItem else {return} + item.actionHandler(item.type) + }, for: .Click) + + + } +// +// override var backdorColor: NSColor { +// return .random +// } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + + guard let item = item as? PopularPeerItem else {return} + badgeView.removeFromSuperview() + activeImage.isHidden = true + badgeNode = nil + let text: String + switch item.type { + case .savedMessages: + let icon = theme.icons.searchSaved + imageView.setSignal(generateEmptyPhoto(imageView.frame.size, type: .icon(colors: theme.colors.peerColors(5), icon: icon, iconSize: icon.backingSize.aspectFitted(NSMakeSize(imageView.frame.size.width - 20, imageView.frame.size.height - 20)), cornerRadius: nil)) |> map {($0, false)}) + text = L10n.searchPopularSavedMessages + case let .articles(unreadCount): + let icon = theme.icons.searchArticle + imageView.setSignal(generateEmptyPhoto(imageView.frame.size, type: .icon(colors: theme.colors.peerColors(4), icon: icon, iconSize: icon.backingSize.aspectFitted(NSMakeSize(imageView.frame.size.width - 20, imageView.frame.size.height - 20)), cornerRadius: nil)) |> map {($0, false)}) + text = L10n.searchPopularArticles + if unreadCount > 0 { + let node = BadgeNode(NSAttributedString.initialize(string: "\(unreadCount)", color: .white, font: .medium(11)), theme.chatList.badgeBackgroundColor) + node.view = badgeView + self.badgeNode = node + badgeView.setFrameSize(node.size) + addSubview(badgeView) + } else { + badgeView.removeFromSuperview() + } + case let .peer(peer, unreadBadge, isActive): + imageView.setPeer(account: item.context.account, peer: peer) + text = peer.compactDisplayTitle + + activeImage.isHidden = !isActive + activeImage.image = theme.icons.hintPeerActive + activeImage.sizeToFit() + + if let unreadBadge = unreadBadge { + let isMuted: Bool + let count: Int32? + switch unreadBadge { + case let .muted(c): + isMuted = true + count = c + case let .unmuted(c): + isMuted = false + count = c + case .none: + isMuted = true + count = nil + } + if let unreadCount = count { + let node = BadgeNode(.initialize(string: "\(unreadCount)", color: .white, font: .medium(11)), isMuted ? theme.chatList.badgeMutedBackgroundColor : theme.chatList.badgeBackgroundColor) + node.view = badgeView + self.badgeNode = node + badgeView.setFrameSize(node.size) + addSubview(badgeView) + } else { + badgeView.removeFromSuperview() + } + } else { + badgeView.removeFromSuperview() + } + } + let layout = TextViewLayout(.initialize(string: text, color: theme.colors.text, font: .normal(11)), maximumNumberOfLines: 1) + layout.measure(width: frame.width - 10) + textView.update(layout) + + self.needsLayout = true + } + + override func layout() { + super.layout() + imageView.centerX(addition: -4) + textView.centerX(y: imageView.frame.maxY + 5, addition: -4) + badgeView.setFrameOrigin(imageView.frame.maxX - badgeView.frame.width / 2, 0) + activeImage.setFrameOrigin(imageView.frame.maxX - activeImage.frame.width - 1, imageView.frame.maxY - 12) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class PopularPeersRowItem: GeneralRowItem { + + let peers: [Peer] + fileprivate let context: AccountContext + fileprivate let unreadArticles: Int32 + fileprivate let selfPeer: Peer + fileprivate let actionHandler: (PopularItemType)->Void + fileprivate let articlesEnabled: Bool + fileprivate let unread: [PeerId : UnreadSearchBadge] + fileprivate let online: [PeerId: Bool] + init(_ initialSize: NSSize, stableId: AnyHashable, context: AccountContext, selfPeer: Peer, articlesEnabled: Bool, unreadArticles: Int32, peers:[Peer], unread: [PeerId : UnreadSearchBadge], online: [PeerId: Bool], action: @escaping(PopularItemType)->Void) { + self.peers = Array(peers.prefix(15)) + self.context = context + self.unread = unread + self.online = online + self.articlesEnabled = articlesEnabled + self.selfPeer = selfPeer + self.actionHandler = action + self.unreadArticles = unreadArticles + super.init(initialSize, height: 74, stableId: stableId) + } + + override func viewClass() -> AnyClass { + return PopularPeersRowView.self + } + +} + + +private final class PopularPeersRowView : TableRowView { + + + + private let tableView: HorizontalTableView + private let separator: View = View() + required init(frame frameRect: NSRect) { + tableView = HorizontalTableView(frame: NSMakeRect(0, 0, frameRect.width, frameRect.height)) + super.init(frame: frameRect) + addSubview(tableView) + addSubview(separator) + } + + override func layout() { + super.layout() + tableView.frame = bounds + separator.frame = NSMakeRect(frame.width - .borderSize, 0, .borderSize, frame.height) + } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + + + tableView.beginTableUpdates() + tableView.removeAll(animation: .effectFade) + + guard let item = item as? PopularPeersRowItem else {return} + _ = tableView.addItem(item: PopularPeerItem(type: .savedMessages(item.selfPeer), context: item.context, action: item.actionHandler)) + if item.articlesEnabled { + _ = tableView.addItem(item: PopularPeerItem(type: .articles(item.unreadArticles), context: item.context, action: item.actionHandler)) + } + + for peer in item.peers { + _ = tableView.addItem(item: PopularPeerItem(type: .peer(peer, item.unread[peer.id], item.online[peer.id] ?? false), context: item.context, action: item.actionHandler)) + } + + tableView.endTableUpdates() + separator.backgroundColor = theme.colors.border + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/PreHistoryControllerStructures.swift b/Telegram-Mac/PreHistoryControllerStructures.swift index dc02ca5a5e..4e355b669b 100644 --- a/Telegram-Mac/PreHistoryControllerStructures.swift +++ b/Telegram-Mac/PreHistoryControllerStructures.swift @@ -8,136 +8,5 @@ import Cocoa import TGUIKit -import TelegramCoreMac +import TelegramCore -final class PreHistoryArguments { - fileprivate let account: Account - fileprivate let preHistory:(Bool)->Void - init(account:Account, preHistory:@escaping(Bool)->Void) { - self.account = account - self.preHistory = preHistory - } -} - -enum PreHistoryEntryId : Hashable { - case type(Int32) - case text(Int32) - case section(Int32) - var hashValue: Int { - switch self { - case .type(let index): - return Int(index) - case .text(let index): - return Int(index) - case .section(let index): - return Int(index) - } - } -} - -func ==(lhs: PreHistoryEntryId, rhs: PreHistoryEntryId) -> Bool { - switch lhs { - case .type(let index): - if case .type(index) = rhs { - return true - } else { - return false - } - case .text(let index): - if case .text(index) = rhs { - return true - } else { - return false - } - case .section(let index): - if case .section(index) = rhs { - return true - } else { - return false - } - } -} - -enum PreHistoryEntry : TableItemListNodeEntry { - case section(Int32) - case type(sectionId:Int32, index: Int32, text: String, enabled: Bool, selected: Bool) - case text(sectionId:Int32, index: Int32, text: String) - - var stableId: PreHistoryEntryId { - switch self { - case .type(_, let index, _, _, _): - return .type(index) - case .text(_, let index, _): - return .text(index) - case .section(let index): - return .section(index) - } - } - - var index:Int32 { - switch self { - case let .type(sectionId, index, _, _, _): - return (sectionId * 1000) + index - case let .text(sectionId, index, _): - return (sectionId * 1000) + index - case let .section(sectionId): - return (sectionId + 1) * 1000 - sectionId - } - } - - func item(_ arguments: PreHistoryArguments, initialSize: NSSize) -> TableRowItem { - switch self { - case .section: - return GeneralRowItem(initialSize, height: 20, stableId: stableId) - case let .type(_, _, text, enabled, selected): - return GeneralInteractedRowItem.init(initialSize, stableId: stableId, name: text, type: .selectable(stateback: { () -> Bool in - return selected - }), action: { - arguments.preHistory(enabled) - }) - case let .text(_, _, text): - return GeneralTextRowItem(initialSize, stableId: stableId, text: text) - } - } - -} - -func <(lhs: PreHistoryEntry, rhs: PreHistoryEntry) -> Bool { - return lhs.index < rhs.index -} - -func ==(lhs: PreHistoryEntry, rhs: PreHistoryEntry) -> Bool { - switch lhs { - case let .type(section, index, text, enabled, selected: Bool): - if case .type(section, index, text, enabled, selected: Bool) = rhs { - return true - } else { - return false - } - case let .text(section, index, text): - if case .text(section, index, text) = rhs { - return true - } else { - return false - } - case let .section(index): - if case .section(index) = rhs { - return true - } else { - return false - } - } -} - -final class PreHistoryControllerState : Equatable { - let enabled: Bool? - init(enabled:Bool? = nil) { - self.enabled = enabled - } - func withUpdatedEnabled(_ enabled: Bool) -> PreHistoryControllerState { - return PreHistoryControllerState(enabled: enabled) - } -} -func ==(lhs: PreHistoryControllerState, rhs: PreHistoryControllerState) -> Bool { - return lhs.enabled == rhs.enabled -} diff --git a/Telegram-Mac/PreHistorySettingsController.swift b/Telegram-Mac/PreHistorySettingsController.swift index 54891cc130..970c473a7a 100644 --- a/Telegram-Mac/PreHistorySettingsController.swift +++ b/Telegram-Mac/PreHistorySettingsController.swift @@ -8,9 +8,104 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit + + + +private final class PreHistoryArguments { + fileprivate let context: AccountContext + fileprivate let preHistory:(Bool)->Void + init(context: AccountContext, preHistory:@escaping(Bool)->Void) { + self.context = context + self.preHistory = preHistory + } +} + +private enum PreHistoryEntryId : Hashable { + case type(Int32) + case text(Int32) + case section(Int32) + var hashValue: Int { + switch self { + case .type(let index): + return Int(index) + case .text(let index): + return Int(index) + case .section(let index): + return Int(index) + } + } +} + + +private enum PreHistoryEntry : TableItemListNodeEntry { + case section(Int32) + case type(sectionId:Int32, index: Int32, text: String, enabled: Bool, selected: Bool, viewType: GeneralViewType) + case text(sectionId:Int32, index: Int32, text: String, viewType: GeneralViewType) + + var stableId: PreHistoryEntryId { + switch self { + case .type(_, let index, _, _, _, _): + return .type(index) + case .text(_, let index, _, _): + return .text(index) + case .section(let index): + return .section(index) + } + } + + var index:Int32 { + switch self { + case let .type(sectionId, index, _, _, _, _): + return (sectionId * 1000) + index + case let .text(sectionId, index, _, _): + return (sectionId * 1000) + index + case let .section(sectionId): + return (sectionId + 1) * 1000 - sectionId + } + } + + func item(_ arguments: PreHistoryArguments, initialSize: NSSize) -> TableRowItem { + switch self { + case .section: + return GeneralRowItem(initialSize, height: 30, stableId: stableId, viewType: .separator) + case let .type(_, _, text, enabled, selected, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: text, type: .selectable(enabled), viewType: viewType, action: { + arguments.preHistory(selected) + }) + case let .text(_, _, text, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, viewType: viewType) + } + } + +} + +private func <(lhs: PreHistoryEntry, rhs: PreHistoryEntry) -> Bool { + return lhs.index < rhs.index +} + + +private struct PreHistoryControllerState : Equatable { + let enabled: Bool? + var applyingSetting: Bool = false + + + init(enabled:Bool? = nil, applyingSetting: Bool = false) { + self.enabled = enabled + self.applyingSetting = applyingSetting + } + func withUpdatedEnabled(_ enabled: Bool) -> PreHistoryControllerState { + return PreHistoryControllerState(enabled: enabled, applyingSetting: self.applyingSetting) + } + func withUpdatedApplyingSetting(_ applyingSetting: Bool) -> PreHistoryControllerState { + return PreHistoryControllerState(enabled: enabled, applyingSetting: self.applyingSetting) + } +} + + fileprivate func prepareEntries(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], initialSize:NSSize, arguments:PreHistoryArguments) -> TableUpdateTransition { @@ -21,7 +116,7 @@ fileprivate func prepareEntries(left:[AppearanceWrapperEntry], return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true) } -fileprivate func preHistoryEntries(cachedData: CachedChannelData?, state: PreHistoryControllerState) -> [PreHistoryEntry] { +fileprivate func preHistoryEntries(cachedData: CachedChannelData?, isGrpup: Bool, state: PreHistoryControllerState) -> [PreHistoryEntry] { var entries:[PreHistoryEntry] = [] @@ -31,35 +126,43 @@ fileprivate func preHistoryEntries(cachedData: CachedChannelData?, state: PreHis entries.append(.section(sectionId)) sectionId += 1 - entries.append(.text(sectionId: sectionId, index: index, text: tr(.preHistorySettingsHeader))) + entries.append(.text(sectionId: sectionId, index: index, text: L10n.preHistorySettingsHeader, viewType: .textTopItem)) index += 1 let enabled = state.enabled ?? cachedData?.flags.contains(.preHistoryEnabled) ?? false - entries.append(.type(sectionId: sectionId, index: index, text: tr(.peerInfoPreHistoryVisible), enabled: true, selected: enabled)) + entries.append(.type(sectionId: sectionId, index: index, text: L10n.peerInfoPreHistoryVisible, enabled: enabled, selected: true, viewType: .firstItem)) index += 1 - entries.append(.type(sectionId: sectionId, index: index, text: tr(.peerInfoPreHistoryHidden), enabled: false, selected: !enabled)) + entries.append(.type(sectionId: sectionId, index: index, text: L10n.peerInfoPreHistoryHidden, enabled: !enabled, selected: false, viewType: .lastItem)) index += 1 - entries.append(.text(sectionId: sectionId, index: index, text: enabled ? tr(.preHistorySettingsDescriptionVisible) : tr(.preHistorySettingsDescriptionHidden))) + entries.append(.text(sectionId: sectionId, index: index, text: enabled ? L10n.preHistorySettingsDescriptionVisible : isGrpup ? L10n.preHistorySettingsDescriptionGroupHidden : L10n.preHistorySettingsDescriptionHidden, viewType: .textBottomItem)) index += 1 return entries } -class PreHistorySettingsController: EmptyComposeController { +class PreHistorySettingsController: EmptyComposeController { private let peerId: PeerId private let statePromise = ValuePromise(PreHistoryControllerState(), ignoreRepeated: true) private let stateValue = Atomic(value: PreHistoryControllerState()) private let disposable = MetaDisposable() - init(_ account: Account, peerId:PeerId) { + private let applyDisposable = MetaDisposable() + init(_ context: AccountContext, peerId:PeerId) { self.peerId = peerId - super.init(account) + super.init(context) } override func viewDidLoad() { super.viewDidLoad() + genericView.getBackgroundColor = { + theme.colors.listBackground + } + + let context = self.context + let peerId = self.peerId + let previous:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) let updateState: ((PreHistoryControllerState) -> PreHistoryControllerState) -> Void = { [weak self] f in @@ -69,30 +172,107 @@ class PreHistorySettingsController: EmptyComposeController = combineLatest(account.postbox.combinedView(keys: [key]) |> deliverOnPrepareQueue, appearanceSignal |> deliverOnPrepareQueue, statePromise.get() |> deliverOnPrepareQueue) |> map { view, appearance, state in + + let signal: Signal<(TableUpdateTransition, PreHistoryControllerState, Bool, CachedChannelData?, Peer?), NoError> = combineLatest(queue: prepareQueue, context.account.postbox.peerView(id: peerId), appearanceSignal, statePromise.get()) |> map { peerView, appearance, state in - let cachedData = view.views[key] as? CachedPeerDataView - let entries = preHistoryEntries(cachedData: cachedData?.cachedPeerData as? CachedChannelData, state: state).map{AppearanceWrapperEntry(entry: $0, appearance: appearance)} + let cachedData = peerView.cachedData as? CachedChannelData + let peer = peerViewMainPeer(peerView) + let entries = preHistoryEntries(cachedData: cachedData, isGrpup: peerId.namespace == Namespaces.Peer.CloudGroup, state: state).map{AppearanceWrapperEntry(entry: $0, appearance: appearance)} + let defaultValue: Bool = cachedData?.flags.contains(.preHistoryEnabled) ?? false - return (prepareEntries(left: previous.swap(entries), right: entries, initialSize: initialSize.modify{$0}, arguments: arguments), state) + + return (prepareEntries(left: previous.swap(entries), right: entries, initialSize: initialSize.modify{$0}, arguments: arguments), state, defaultValue, cachedData, peer) } |> deliverOnMainQueue - disposable.set(signal.start(next: { [weak self] transition, state in + disposable.set(signal.start(next: { [weak self] transition, state, defaultValue, cachedData, peer in self?.genericView.merge(with: transition) self?.readyOnce() + + self?.doneButton?.removeAllHandlers() + self?.doneButton?.set(handler: { _ in + var value: Bool? + updateState { state in + value = state.enabled + return state.withUpdatedApplyingSetting(true) + } + if let value = value, value != defaultValue { + if peerId.namespace == Namespaces.Peer.CloudGroup { + let signal = context.engine.peers.convertGroupToSupergroup(peerId: peerId) + |> map(Optional.init) + |> mapToSignal { upgradedPeerId -> Signal in + guard let upgradedPeerId = upgradedPeerId else { + return .single(nil) + } + return context.engine.peers.updateChannelHistoryAvailabilitySettingsInteractively(peerId: upgradedPeerId, historyAvailableForNewMembers: value) + |> mapError { _ in + return ConvertGroupToSupergroupError.generic + } + |> mapToSignal { _ -> Signal in + return .complete() + } + |> then(.single(upgradedPeerId) |> mapError { ConvertGroupToSupergroupError.generic }) + } + |> deliverOnMainQueue + + _ = showModalProgress(signal: signal, for: context.window).start(next: { [weak self] peerId in + self?.onComplete.set(.single(peerId)) + }, error: { error in + switch error { + case .tooManyChannels: + showInactiveChannels(context: context, source: .upgrade) + case .generic: + alert(for: context.window, info: L10n.unknownError) + } + }) + + } else { + let signal: Signal = context.engine.peers.updateChannelHistoryAvailabilitySettingsInteractively(peerId: peerId, historyAvailableForNewMembers: value) |> deliverOnMainQueue |> `catch` { _ in return .complete() } |> map { _ in return nil } + + if let cachedData = cachedData, let linkedDiscussionPeerId = cachedData.linkedDiscussionPeerId.peerId, let peer = peer as? TelegramChannel { + confirm(for: context.window, information: L10n.preHistoryConfirmUnlink(peer.displayTitle), successHandler: { [weak self] _ in + if peer.adminRights == nil || !peer.hasPermission(.pinMessages) { + alert(for: context.window, info: L10n.channelErrorDontHavePermissions) + } else { + let signal = context.engine.peers.updateGroupDiscussionForChannel(channelId: linkedDiscussionPeerId, groupId: nil) + |> `catch` { _ in return .complete() } + |> map { _ -> PeerId? in return nil } + |> then(signal) + self?.onComplete.set(showModalProgress(signal: signal, for: context.window)) + } + + }) + } else { + self?.onComplete.set(showModalProgress(signal: signal, for: mainWindow)) + } + + } + } else { + self?.onComplete.set(.single(nil)) + } + + }, for: .SingleClick) + })) + } + var doneButton:Control? { + return rightBarView + } + + override func getRightBarViewOnce() -> BarView { + let button = TextButtonBarView(controller: self, text: L10n.navigationDone) + + return button } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - onComplete.set(statePromise.get() |> filter {$0.enabled != nil} |> map {$0.enabled!}) } override var enableBack: Bool { @@ -101,5 +281,6 @@ class PreHistorySettingsController: EmptyComposeController = Promise() + private let queue: Queue = Queue() + init(_ path: String, context: AccountContext, id: Int64) { + self.path = path + self.id = id + + context.engine.resources.preUpload(id: id, encrypt: false, tag: nil, source: resource.get(), onComplete: { + unlink(path) + }) + } + + + func fileDidChangedSize(_ complete: Bool) { + self.queue.async { + if let size = fileSize(self.path), self.previousSize != size || complete { + self.previousSize = size + self.resource.set(.single(MediaResourceData(path: self.path, offset: 0, size: size, complete: complete))) + } + } + } + + +} diff --git a/Telegram-Mac/Preferences.swift b/Telegram-Mac/Preferences.swift new file mode 100644 index 0000000000..432d37b1f0 --- /dev/null +++ b/Telegram-Mac/Preferences.swift @@ -0,0 +1,120 @@ +// +// Preferences.swift +// Muse +// +// Created by Marco Albera on 28/07/2017. +// Copyright © 2017 Edge Apps. All rights reserved. +// + +import Cocoa + +enum PreferenceKey: String { + + case peekToolbarsOnHover = "peekToolbarsOnHover" + + case menuBarTitle = "menuBarTitle" + + case controlStripItem = "controlStripItem" + + case controlStripHUD = "controlStripHUD" + + case actionBar = "actionBar" + + var name: RawValue { + return rawValue + } + + var defaultValue: Any { + return PreferenceKey.defaults[self]! + } + + static let defaults: [PreferenceKey: Any] = [.peekToolbarsOnHover: true, + .menuBarTitle: true, + .controlStripItem: true, + .controlStripHUD: true, + .actionBar: true] + + static func registerDefaults() { + UserDefaults.standard.register(defaults: defaults.userDefaultsCompatible) + } + +} + +protocol Preferenceable { + + associatedtype ValueType + + var key: PreferenceKey { get } + +} + +extension Preferenceable { + + var userDefaults: UserDefaults { + return UserDefaults.standard + } + + var value: ValueType { + return userDefaults.object(for: key) as? ValueType ?? defaultValue + } + + func set(_ value: ValueType) { + userDefaults.set(value, for: key) + } + + var defaultValue: ValueType { + return key.defaultValue as! ValueType + } + +} + +struct Preference: Preferenceable { + + typealias ValueType = T + + var key: PreferenceKey + + init (_ key: PreferenceKey) { + self.key = key + } + +} + +extension Dictionary { + + /** + Initializes a Dictionary from two given sequences by zipping them into one + - parameter sequence1: the first sequence + - parameter sequence2: the second sequence + */ + init(_ sequence1: [Key], _ sequence2: [Value]) { + self.init() + + zip(sequence1, sequence2).forEach { self[$0] = $1 } + } + +} + +extension Dictionary where Key == PreferenceKey { + + /** + UserDefaults needs string keys, so we build a new dicitionary + with PreferenceKey's rawValues + */ + var userDefaultsCompatible: [String: Any] { + return Dictionary(self.keys.map { $0.name }, self.values.map { $0 }) + } + +} + +extension UserDefaults { + + func set(_ value: Any, for preferenceKey: PreferenceKey) { + set(value, forKey: preferenceKey.name) + } + + func object(for preferenceKey: PreferenceKey) -> Any? { + return object(forKey: preferenceKey.name) + } + +} diff --git a/Telegram-Mac/PreparedChatHistoryViewTransition.swift b/Telegram-Mac/PreparedChatHistoryViewTransition.swift deleted file mode 100644 index 73e3749922..0000000000 --- a/Telegram-Mac/PreparedChatHistoryViewTransition.swift +++ /dev/null @@ -1,198 +0,0 @@ -// -// PreparedChatHistoryViewTransition.swift -// Telegram -// -// Created by keepcoder on 19/04/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - -import Cocoa -import SwiftSignalKitMac -import PostboxMac -import TelegramCoreMac - -struct ChatHistoryViewTransition { - let historyView: ChatHistoryView - let deleteItems: [ListViewDeleteItem] - let insertEntries: [ChatHistoryViewTransitionInsertEntry] - let updateEntries: [ChatHistoryViewTransitionUpdateEntry] - let options: ListViewDeleteAndInsertOptions - let scrollToItem: ListViewScrollToItem? - let stationaryItemRange: (Int, Int)? - let initialData: InitialMessageHistoryData? - let keyboardButtonsMessage: Message? - let cachedData: CachedPeerData? - let scrolledToIndex: MessageIndex? -} - - -enum ChatHistoryViewGridScrollPosition { - case Unread(index: MessageIndex) - case Index(index: MessageIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool) -} - -func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toView: ChatHistoryView, reason: ChatHistoryViewTransitionReason, account: Account, peerId: PeerId, controllerInteraction: ChatInteraction, scrollPosition: ChatHistoryViewGridScrollPosition?, initialData: InitialMessageHistoryData?, keyboardButtonsMessage: Message?, cachedData: CachedPeerData?) -> Signal { - return Signal { subscriber in - let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries) - - var adjustedDeleteIndices: [ListViewDeleteItem] = [] - let previousCount: Int - if let fromView = fromView { - previousCount = fromView.filteredEntries.count - } else { - previousCount = 0; - } - for index in deleteIndices { - adjustedDeleteIndices.append(ListViewDeleteItem(index: previousCount - 1 - index, directionHint: nil)) - } - - var adjustedIndicesAndItems: [ChatHistoryViewTransitionInsertEntry] = [] - var adjustedUpdateItems: [ChatHistoryViewTransitionUpdateEntry] = [] - let updatedCount = toView.filteredEntries.count - - var options: ListViewDeleteAndInsertOptions = [] - var maxAnimatedInsertionIndex = -1 - var stationaryItemRange: (Int, Int)? - var scrollToItem: ListViewScrollToItem? - - switch reason { - case let .Initial(fadeIn): - if fadeIn { - let _ = options.insert(.AnimateAlpha) - } else { - let _ = options.insert(.LowLatency) - let _ = options.insert(.Synchronous) - } - case .InteractiveChanges: - let _ = options.insert(.AnimateAlpha) - let _ = options.insert(.AnimateInsertion) - - for (index, _, _) in indicesAndItems.sorted(by: { $0.0 > $1.0 }) { - let adjustedIndex = updatedCount - 1 - index - if adjustedIndex == maxAnimatedInsertionIndex + 1 { - maxAnimatedInsertionIndex += 1 - } - } - case .Reload: - break - case let .HoleChanges(filledHoleDirections, removeHoleDirections): - if let (_, removeDirection) = removeHoleDirections.first { - switch removeDirection { - case .LowerToUpper: - var holeIndex: MessageIndex? - for (index, _) in filledHoleDirections { - if holeIndex == nil || index < holeIndex! { - holeIndex = index - } - } - - if let holeIndex = holeIndex { - for i in 0 ..< toView.filteredEntries.count { - if toView.filteredEntries[i].entry.index >= holeIndex { - let index = toView.filteredEntries.count - 1 - (i - 1) - stationaryItemRange = (index, Int.max) - break - } - } - } - case .UpperToLower: - break - case .AroundIndex: - break - } - } - } - - for (index, entry, previousIndex) in indicesAndItems { - let adjustedIndex = updatedCount - 1 - index - - let adjustedPrevousIndex: Int? - if let previousIndex = previousIndex { - adjustedPrevousIndex = previousCount - 1 - previousIndex - } else { - adjustedPrevousIndex = nil - } - - var directionHint: ListViewItemOperationDirectionHint? - if maxAnimatedInsertionIndex >= 0 && adjustedIndex <= maxAnimatedInsertionIndex { - directionHint = .Down - } - - adjustedIndicesAndItems.append(ChatHistoryViewTransitionInsertEntry(index: adjustedIndex, previousIndex: adjustedPrevousIndex, entry: entry.entry, directionHint: directionHint)) - } - - for (index, entry, previousIndex) in updateIndices { - let adjustedIndex = updatedCount - 1 - index - let adjustedPreviousIndex = previousCount - 1 - previousIndex - - let directionHint: ListViewItemOperationDirectionHint? = nil - adjustedUpdateItems.append(ChatHistoryViewTransitionUpdateEntry(index: adjustedIndex, previousIndex: adjustedPreviousIndex, entry: entry.entry, directionHint: directionHint)) - } - - var scrolledToIndex: MessageIndex? - - if let scrollPosition = scrollPosition { - switch scrollPosition { - case let .Unread(unreadIndex): - var index = toView.filteredEntries.count - 1 - for entry in toView.filteredEntries { - if case .UnreadEntry = entry.entry { - scrollToItem = ListViewScrollToItem(index: index, position: .Bottom, animated: false, curve: .Default, directionHint: .Down) - break - } - index -= 1 - } - - if scrollToItem == nil { - var index = toView.filteredEntries.count - 1 - for entry in toView.filteredEntries { - if entry.entry.index >= unreadIndex { - scrollToItem = ListViewScrollToItem(index: index, position: .Bottom, animated: false, curve: .Default, directionHint: .Down) - break - } - index -= 1 - } - } - - if scrollToItem == nil { - var index = 0 - for entry in toView.filteredEntries.reversed() { - if entry.entry.index < unreadIndex { - scrollToItem = ListViewScrollToItem(index: index, position: .Bottom, animated: false, curve: .Default, directionHint: .Down) - break - } - index += 1 - } - } - case let .Index(scrollIndex, position, directionHint, animated): - if case .Center = position { - scrolledToIndex = scrollIndex - } - var index = toView.filteredEntries.count - 1 - for entry in toView.filteredEntries { - if entry.entry.index >= scrollIndex { - scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint) - break - } - index -= 1 - } - - if scrollToItem == nil { - var index = 0 - for entry in toView.filteredEntries.reversed() { - if entry.entry.index < scrollIndex { - scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint) - break - } - index += 1 - } - } - } - } - - subscriber.putNext(ChatHistoryViewTransition(historyView: toView, deleteItems: adjustedDeleteIndices, insertEntries: adjustedIndicesAndItems, updateEntries: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: initialData, keyboardButtonsMessage: keyboardButtonsMessage, cachedData: cachedData, scrolledToIndex: scrolledToIndex)) - subscriber.putCompletion() - - return EmptyDisposable - } -} diff --git a/Telegram-Mac/PresenceStrings.swift b/Telegram-Mac/PresenceStrings.swift index 250207f3f2..1cd4625082 100644 --- a/Telegram-Mac/PresenceStrings.swift +++ b/Telegram-Mac/PresenceStrings.swift @@ -7,9 +7,12 @@ // import Cocoa -import PostboxMac -import TelegramCoreMac +import Postbox +import TelegramCore + import TGUIKit +import MapKit + func stringForTimestamp(day: Int32, month: Int32, year: Int32) -> String { return String(format: "%d.%02d.%02d", day, month, year - 100) } @@ -27,11 +30,11 @@ func stringForUserPresence(day: UserPresenceDay, hours: Int32, minutes: Int32) - let dayString: String switch day { case .today: - dayString = tr(.peerStatusToday) + dayString = tr(L10n.peerStatusToday) case .yesterday: - dayString = tr(.peerStatusYesterday) + dayString = tr(L10n.peerStatusYesterday) } - return tr(.peerStatusLastSeenAt(dayString, stringForTime(hours: hours, minutes: minutes))) + return tr(L10n.peerStatusLastSeenAt(dayString, stringForTime(hours: hours, minutes: minutes))) } enum RelativeUserPresenceLastSeen { @@ -53,18 +56,26 @@ enum RelativeUserPresenceStatus { case lastMonth } -func relativeUserPresenceStatus(_ presence: TelegramUserPresence, relativeTo timestamp: Int32) -> RelativeUserPresenceStatus { +func relativeUserPresenceStatus(_ presence: TelegramUserPresence, timeDifference: TimeInterval, relativeTo timestamp: Int32) -> RelativeUserPresenceStatus { switch presence.status { case .none: return .offline case let .present(statusTimestamp): + let statusTimestampInt: Int = Int(statusTimestamp) + let statusTimestamp = Int32(min(statusTimestampInt - Int(timeDifference), Int(INT32_MAX))) if statusTimestamp >= timestamp { return .online(at: statusTimestamp) } else { return .lastSeen(at: statusTimestamp) } + case .recently: - return .recently + let activeUntil = presence.lastActivity - Int32(timeDifference) + 30 + if activeUntil >= timestamp { + return .online(at: activeUntil) + } else { + return .recently + } case .lastWeek: return .lastWeek case .lastMonth: @@ -72,21 +83,24 @@ func relativeUserPresenceStatus(_ presence: TelegramUserPresence, relativeTo tim } } -func stringAndActivityForUserPresence(_ presence: TelegramUserPresence, relativeTo timestamp: Int32) -> (String, Bool, NSColor) { +func stringAndActivityForUserPresence(_ presence: TelegramUserPresence, timeDifference: TimeInterval, relativeTo timestamp: Int32, expanded: Bool = false, customTheme: GeneralRowItem.Theme? = nil) -> (String, Bool, NSColor) { + switch presence.status { case .none: - return (tr(.peerStatusRecently), false, theme.colors.grayText) + return (L10n.peerStatusLongTimeAgo, false, customTheme?.grayTextColor ?? theme.colors.grayText) case let .present(statusTimestamp): - if statusTimestamp >= timestamp { - return (tr(.peerStatusOnline), true, theme.colors.blueText) + let statusTimestampInt: Int = Int(statusTimestamp) + let statusTimestamp = Int32(min(statusTimestampInt - Int(timeDifference), Int(INT32_MAX))) + if statusTimestamp > timestamp { + return (L10n.peerStatusOnline, true, customTheme?.accentColor ?? theme.colors.accent) } else { let difference = timestamp - statusTimestamp if difference < 59 { - return (tr(.peerStatusJustNow), false, theme.colors.grayText) - } else if difference < 60 * 60 { + return (tr(L10n.peerStatusJustNow), false, customTheme?.grayTextColor ?? theme.colors.grayText) + } else if difference < 60 * 60 && !expanded { let minutes = max(difference / 60, 1) - return (tr(.peerStatusMinAgoCountable(Int(minutes))), false, theme.colors.grayText) + return (L10n.peerStatusMinAgoCountable(Int(minutes)), false, customTheme?.grayTextColor ?? theme.colors.grayText) } else { var t: time_t = time_t(statusTimestamp) var timeinfo: tm = tm() @@ -97,35 +111,53 @@ func stringAndActivityForUserPresence(_ presence: TelegramUserPresence, relative localtime_r(&now, &timeinfoNow) if timeinfo.tm_year != timeinfoNow.tm_year { - return ("\(tr(.timeLastSeen)) \(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year))", false, theme.colors.grayText) + return ("\(L10n.timeLastSeen) \(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year))", false, customTheme?.grayTextColor ?? theme.colors.grayText) } let dayDifference = timeinfo.tm_yday - timeinfoNow.tm_yday if dayDifference == 0 || dayDifference == -1 { let day: UserPresenceDay if dayDifference == 0 { - day = .today + if expanded { + day = .today + } else { + let minutes = difference / (60 * 60) + + return (L10n.lastSeenHoursAgoCountable(Int(minutes)), false, customTheme?.grayTextColor ?? theme.colors.grayText) + } } else { day = .yesterday } - return (stringForUserPresence(day: day, hours: timeinfo.tm_hour, minutes: timeinfo.tm_min), false, theme.colors.grayText) + return (stringForUserPresence(day: day, hours: timeinfo.tm_hour, minutes: timeinfo.tm_min), false, customTheme?.grayTextColor ?? theme.colors.grayText) } else { - return ("\(tr(.timeLastSeen)) \(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year))", false, theme.colors.grayText) + return ("\(L10n.timeLastSeen) \(stringForTimestamp(day: timeinfo.tm_mday, month: timeinfo.tm_mon + 1, year: timeinfo.tm_year))", false, customTheme?.grayTextColor ?? theme.colors.grayText) } } } case .recently: - return (tr(.peerStatusRecently), false, theme.colors.grayText) + let activeUntil = presence.lastActivity - Int32(timeDifference) + 30 + if activeUntil >= timestamp { + return (L10n.peerStatusOnline, true, customTheme?.accentColor ?? theme.colors.accent) + } else { + return (L10n.peerStatusRecently, false, customTheme?.grayTextColor ?? theme.colors.grayText) + } case .lastWeek: - return (tr(.peerStatusLastWeek), false, theme.colors.grayText) + return (L10n.peerStatusLastWeek, false, customTheme?.grayTextColor ?? theme.colors.grayText) case .lastMonth: - return (tr(.peerStatusLastMonth), false, theme.colors.grayText) + return (L10n.peerStatusLastMonth, false, customTheme?.grayTextColor ?? theme.colors.grayText) } } -func userPresenceStringRefreshTimeout(_ presence: TelegramUserPresence, relativeTo timestamp: Int32) -> Double { +func userPresenceStringRefreshTimeout(_ presence: TelegramUserPresence, timeDifference: Int32, relativeTo timestamp: Int32) -> Double { switch presence.status { case let .present(statusTimestamp): + + let statusTimestampInt: Int = Int(statusTimestamp) + let statusTimestamp = Int32(min(statusTimestampInt, Int(INT32_MAX))) + + if statusTimestamp > INT32_MAX - 1 { + return Double.infinity + } if statusTimestamp >= timestamp { return Double(statusTimestamp - timestamp) } else { @@ -138,7 +170,161 @@ func userPresenceStringRefreshTimeout(_ presence: TelegramUserPresence, relative return Double.infinity } } - case .recently, .none, .lastWeek, .lastMonth: + case .recently: + let activeUntil = presence.lastActivity - timeDifference + 30 + if activeUntil >= timestamp { + return Double(activeUntil - timestamp + 1) + } else { + return Double.infinity + } + + case .none, .lastWeek, .lastMonth: return Double.infinity } } + + +func stringForRelativeSymbolicTimestamp(relativeTimestamp: Int32, relativeTo timestamp: Int32) -> String { + var t: time_t = time_t(relativeTimestamp) + var timeinfo: tm = tm() + localtime_r(&t, &timeinfo) + + var now: time_t = time_t(timestamp) + var timeinfoNow: tm = tm() + localtime_r(&now, &timeinfoNow) + + let dayDifference = timeinfo.tm_yday - timeinfoNow.tm_yday + + let hours = timeinfo.tm_hour + let minutes = timeinfo.tm_min + + if dayDifference == 0 { + return L10n.timeTodayAt(stringForShortTimestamp(hours: hours, minutes: minutes)) + } else { + return stringForFullDate(timestamp: relativeTimestamp) + } +} + + + +func stringForShortTimestamp(hours: Int32, minutes: Int32) -> String { + let hourString: String + if hours == 0 { + hourString = "12" + } else if hours > 12 { + hourString = "\(hours - 12)" + } else { + hourString = "\(hours)" + } + + let periodString: String + if hours >= 12 { + periodString = "PM" + } else { + periodString = "AM" + } + if minutes >= 10 { + return "\(hourString):\(minutes) \(periodString)" + } else { + return "\(hourString):0\(minutes) \(periodString)" + } +} + + + +func stringForFullDate(timestamp: Int32) -> String { + var t: time_t = Int(timestamp) + var timeinfo = tm() + localtime_r(&t, &timeinfo); + + switch timeinfo.tm_mon + 1 { + case 1: + return L10n.timePreciseDateM1("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min))) + case 2: + return L10n.timePreciseDateM2("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min))) + case 3: + return L10n.timePreciseDateM3("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min))) + case 4: + return L10n.timePreciseDateM4("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min))) + case 5: + return L10n.timePreciseDateM5("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min))) + case 6: + return L10n.timePreciseDateM6("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min))) + case 7: + return L10n.timePreciseDateM7("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min))) + case 8: + return L10n.timePreciseDateM8("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min))) + case 9: + return L10n.timePreciseDateM9("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min))) + case 10: + return L10n.timePreciseDateM10("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min))) + case 11: + return L10n.timePreciseDateM11("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min))) + case 12: + return L10n.timePreciseDateM12("\(timeinfo.tm_mday)", "\(2000 + timeinfo.tm_year - 100)", stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min))) + default: + return "" + } +} + +func stringForMediumDate(timestamp: Int32) -> String { + var t: time_t = Int(timestamp) + var timeinfo = tm() + localtime_r(&t, &timeinfo); + let formatter = DateFormatter() + formatter.timeStyle = .short + + let date = Date(timeIntervalSince1970: TimeInterval(timestamp)) + let time = formatter.string(from: date) + + if date.isToday || date.timeIntervalSince1970 < Date().timeIntervalSince1970 { + return DateUtils.string(forLastSeen: timestamp) + } else if date.isTomorrow { + return L10n.timeTomorrowAt(time) + } + + + + switch timeinfo.tm_mon + 1 { + case 1: + return L10n.timePreciseMediumDateM1("\(timeinfo.tm_mday)", time) + case 2: + return L10n.timePreciseMediumDateM2("\(timeinfo.tm_mday)", time) + case 3: + return L10n.timePreciseMediumDateM3("\(timeinfo.tm_mday)", time) + case 4: + return L10n.timePreciseMediumDateM4("\(timeinfo.tm_mday)", time) + case 5: + return L10n.timePreciseMediumDateM5("\(timeinfo.tm_mday)", time) + case 6: + return L10n.timePreciseMediumDateM6("\(timeinfo.tm_mday)", time) + case 7: + return L10n.timePreciseMediumDateM7("\(timeinfo.tm_mday)", time) + case 8: + return L10n.timePreciseMediumDateM8("\(timeinfo.tm_mday)", time) + case 9: + return L10n.timePreciseMediumDateM9("\(timeinfo.tm_mday)", time) + case 10: + return L10n.timePreciseMediumDateM10("\(timeinfo.tm_mday)", time) + case 11: + return L10n.timePreciseMediumDateM11("\(timeinfo.tm_mday)", time) + case 12: + return L10n.timePreciseMediumDateM12("\(timeinfo.tm_mday)", time) + default: + return "" + } +} + +private var sharedDistanceFormatter: MKDistanceFormatter? +func stringForDistance(distance: CLLocationDistance) -> String { + let distanceFormatter: MKDistanceFormatter + if let currentDistanceFormatter = sharedDistanceFormatter { + distanceFormatter = currentDistanceFormatter + } else { + distanceFormatter = MKDistanceFormatter() + distanceFormatter.unitStyle = .full + sharedDistanceFormatter = distanceFormatter + } + + return distanceFormatter.string(fromDistance: distance) +} diff --git a/Telegram-Mac/PresentationGroupCall.swift b/Telegram-Mac/PresentationGroupCall.swift new file mode 100644 index 0000000000..1c09cd1741 --- /dev/null +++ b/Telegram-Mac/PresentationGroupCall.swift @@ -0,0 +1,2804 @@ +import Cocoa +import Postbox +import TelegramCore + +import SwiftSignalKit +import AVFoundation +import TelegramVoip +import TGUIKit + +func getGroupCallPanelData(context: AccountContext, peerId: PeerId) -> Signal { + let account = context.account + let availableGroupCall: Signal + if peerId.namespace == Namespaces.Peer.CloudChannel || peerId.namespace == Namespaces.Peer.CloudGroup { + availableGroupCall = context.account.viewTracker.peerView(peerId) + |> map { peerView -> CachedChannelData.ActiveCall? in + if let cachedData = peerView.cachedData as? CachedChannelData { + return cachedData.activeCall + } + if let cachedData = peerView.cachedData as? CachedGroupData { + return cachedData.activeCall + } + return nil + } + |> distinctUntilChanged + |> mapToSignal { activeCall -> Signal in + guard let activeCall = activeCall else { + return .single(nil) + } + return context.sharedContext.groupCallContext |> mapToSignal { groupCall in + if let context = groupCall, context.call.peerId == peerId && context.call.account.id == account.id { + return context.call.summaryState + |> map { summary -> GroupCallPanelData in + if let summary = summary { + return GroupCallPanelData( + peerId: peerId, + info: summary.info, + topParticipants: summary.topParticipants, + participantCount: summary.participantCount, + activeSpeakers: summary.activeSpeakers, + groupCall: context + ) + } else { + return GroupCallPanelData(peerId: peerId, info: nil, topParticipants: [], participantCount: 0, activeSpeakers: [], groupCall: context) + } + } + } else { + return Signal { subscriber in + let disposable = MetaDisposable() + let callContext = context.cachedGroupCallContexts + callContext.impl.syncWith { impl in + let callContext = impl.get(context: context, peerId: peerId, call: activeCall) + disposable.set((callContext.context.panelData + |> deliverOnMainQueue).start(next: { panelData in + callContext.keep() + subscriber.putNext(panelData) + })) + } + return disposable + } + } + } + } + + } else { + availableGroupCall = .single(nil) + } + return availableGroupCall +} + +protocol AccountGroupCallContext: class { +} + +protocol AccountGroupCallContextCache: class { +} + + +private extension GroupCallParticipantsContext.Participant { + var allSsrcs: Set { + var participantSsrcs = Set() + if let ssrc = self.ssrc { + participantSsrcs.insert(ssrc) + } + if let videoDescription = self.videoDescription { + for group in videoDescription.ssrcGroups { + for ssrc in group.ssrcs { + participantSsrcs.insert(ssrc) + } + } + } + if let presentationDescription = self.presentationDescription { + for group in presentationDescription.ssrcGroups { + for ssrc in group.ssrcs { + participantSsrcs.insert(ssrc) + } + } + } + return participantSsrcs + } + + var videoSsrcs: Set { + var participantSsrcs = Set() + if let videoDescription = self.videoDescription { + for group in videoDescription.ssrcGroups { + for ssrc in group.ssrcs { + participantSsrcs.insert(ssrc) + } + } + } + return participantSsrcs + } + + var presentationSsrcs: Set { + var participantSsrcs = Set() + if let presentationDescription = self.presentationDescription { + for group in presentationDescription.ssrcGroups { + for ssrc in group.ssrcs { + participantSsrcs.insert(ssrc) + } + } + } + return participantSsrcs + } +} + + +final class AccountGroupCallContextImpl: AccountGroupCallContext { + final class Proxy { + let context: AccountGroupCallContextImpl + let removed: () -> Void + + init(context: AccountGroupCallContextImpl, removed: @escaping () -> Void) { + self.context = context + self.removed = removed + } + + deinit { + self.removed() + } + + func keep() { + } + } + + var disposable: Disposable? + var participantsContext: GroupCallParticipantsContext? + + private let panelDataPromise = Promise() + var panelData: Signal { + return self.panelDataPromise.get() + } + + init(context: AccountContext, peerId: PeerId, call: CachedChannelData.ActiveCall) { + self.panelDataPromise.set(.single(GroupCallPanelData( + peerId: peerId, + info: GroupCallInfo( + id: call.id, + accessHash: call.accessHash, + participantCount: 0, + streamDcId: nil, + title: call.title, + scheduleTimestamp: nil, + subscribedToScheduled: false, + recordingStartTimestamp: nil, + sortAscending: true, + defaultParticipantsAreMuted: nil, + isVideoEnabled: false, + unmutedVideoLimit: 0 + ), + topParticipants: [], + participantCount: 0, + activeSpeakers: Set(), + groupCall: nil + ))) + + self.disposable = (context.engine.calls.getGroupCallParticipants(callId: call.id, accessHash: call.accessHash, offset: "", ssrcs: [], limit: 100, sortAscending: nil) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let strongSelf = self, let state = state else { + return + } + let context = context.engine.calls.groupCall( + peerId: peerId, + myPeerId: context.account.peerId, + id: call.id, + accessHash: call.accessHash, + state: state, + previousServiceState: nil + ) + + strongSelf.participantsContext = context + strongSelf.panelDataPromise.set(combineLatest(queue: .mainQueue(), + context.state, + context.activeSpeakers + ) + |> map { state, activeSpeakers -> GroupCallPanelData in + var topParticipants: [GroupCallParticipantsContext.Participant] = [] + for participant in state.participants { + if topParticipants.count >= 3 { + break + } + topParticipants.append(participant) + } + return GroupCallPanelData( + peerId: peerId, + info: GroupCallInfo(id: call.id, accessHash: call.accessHash, participantCount: state.totalCount, streamDcId: nil, title: state.title, scheduleTimestamp: state.scheduleTimestamp, subscribedToScheduled: state.subscribedToScheduled, recordingStartTimestamp: state.recordingStartTimestamp, sortAscending: state.sortAscending, defaultParticipantsAreMuted: state.defaultParticipantsAreMuted, isVideoEnabled: state.isVideoEnabled, unmutedVideoLimit: state.unmutedVideoLimit), + topParticipants: topParticipants, + participantCount: state.totalCount, + activeSpeakers: activeSpeakers, + groupCall: nil + ) + }) + }) + } + + deinit { + self.disposable?.dispose() + } +} + +final class AccountGroupCallContextCacheImpl: AccountGroupCallContextCache { + class Impl { + private class Record { + let context: AccountGroupCallContextImpl + let subscribers = Bag() + var removeTimer: SwiftSignalKit.Timer? + + init(context: AccountGroupCallContextImpl) { + self.context = context + } + } + + private let queue: Queue + private var contexts: [Int64: Record] = [:] + + private let leaveDisposables = DisposableSet() + + init(queue: Queue) { + self.queue = queue + } + + func get(context: AccountContext, peerId: PeerId, call: CachedChannelData.ActiveCall) -> AccountGroupCallContextImpl.Proxy { + let result: Record + if let current = self.contexts[call.id] { + result = current + } else { + let context = AccountGroupCallContextImpl(context: context, peerId: peerId, call: call) + result = Record(context: context) + self.contexts[call.id] = result + } + + let index = result.subscribers.add(Void()) + result.removeTimer?.invalidate() + result.removeTimer = nil + return AccountGroupCallContextImpl.Proxy(context: result.context, removed: { [weak self, weak result] in + Queue.mainQueue().async { + if let strongResult = result, let strongSelf = self, strongSelf.contexts[call.id] === strongResult { + strongResult.subscribers.remove(index) + if strongResult.subscribers.isEmpty { + let removeTimer = SwiftSignalKit.Timer(timeout: 30, repeat: false, completion: { + if let result = result, let strongSelf = self, strongSelf.contexts[call.id] === result, result.subscribers.isEmpty { + strongSelf.contexts.removeValue(forKey: call.id) + } + }, queue: .mainQueue()) + strongResult.removeTimer = removeTimer + removeTimer.start() + } + } + } + }) + } + + func leaveInBackground(context: AccountContext, id: Int64, accessHash: Int64, source: UInt32) { + let disposable = context.engine.calls.leaveGroupCall(callId: id, accessHash: accessHash, source: source).start() + self.leaveDisposables.add(disposable) + } + } + + let queue: Queue = .mainQueue() + let impl: QueueLocalObject + + init() { + let queue = self.queue + self.impl = QueueLocalObject(queue: queue, generate: { + return Impl(queue: queue) + }) + } +} + +private extension PresentationGroupCallState { + static func initialValue(myPeerId: PeerId, title: String?, scheduledTimestamp: Int32?, subscribedToScheduled: Bool) -> PresentationGroupCallState { + return PresentationGroupCallState( + myPeerId: myPeerId, + networkState: .connecting, + canManageCall: false, + adminIds: Set(), + muteState: GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false), + defaultParticipantMuteState: nil, + recordingStartTimestamp: nil, + title: title, + raisedHand: false, + scheduleTimestamp: scheduledTimestamp, + subscribedToScheduled: subscribedToScheduled, + isVideoEnabled: false + ) + } +} + +final class PresentationGroupCallImpl: PresentationGroupCall { + + var peer: Peer? = nil + private let loadPeerDisposable = MetaDisposable() +// var activeCall: CachedChannelData.ActiveCall? + + private let startDisposable = MetaDisposable() + private let subscribeDisposable = MetaDisposable() + private let updateGroupCallJoinAsDisposable = MetaDisposable() + + + private let devicesContext: DevicesContext + private let devicesDisposable = MetaDisposable() + + private let displayAsPeersValue: Promise<[FoundPeer]?> = Promise(nil) + var displayAsPeers: Signal<[FoundPeer]?, NoError> { + return displayAsPeersValue.get() + } + private let loadDisplayAsPeerDisposable = MetaDisposable() + + + var permissions: (PresentationGroupCallMuteAction, @escaping(Bool)->Void)->Void = { _, f in f(true) } + + var sharedContext: SharedAccountContext { + return accountContext.sharedContext + } + + private enum InternalState { + case requesting + case active(GroupCallInfo) + case established(info: GroupCallInfo, connectionMode: JoinGroupCallResult.ConnectionMode, clientParams: String, localSsrc: UInt32, initialState: GroupCallParticipantsContext.State) + + var callInfo: GroupCallInfo? { + switch self { + case .requesting: + return nil + case let .active(info): + return info + case let .established(info, _, _, _, _): + return info + } + } + } + + private struct SummaryInfoState: Equatable { + var info: GroupCallInfo + + init( + info: GroupCallInfo + ) { + self.info = info + } + } + + private struct SummaryParticipantsState: Equatable { + var participantCount: Int + var topParticipants: [GroupCallParticipantsContext.Participant] + var activeSpeakers: Set + + init( + participantCount: Int, + topParticipants: [GroupCallParticipantsContext.Participant], + activeSpeakers: Set + ) { + self.participantCount = participantCount + self.topParticipants = topParticipants + self.activeSpeakers = activeSpeakers + } + } + + private class SpeakingParticipantsContext { + private let speakingLevelThreshold: Float = 0.1 + private let cutoffTimeout: Int32 = 3 + private let silentTimeout: Int32 = 2 + + struct Participant { + let ssrc: UInt32 + let timestamp: Int32 + let level: Float + } + + private var participants: [PeerId: Participant] = [:] + private let speakingParticipantsPromise = ValuePromise<[PeerId: UInt32]>(ignoreRepeated: true) + private var speakingParticipants = [PeerId: UInt32]() { + didSet { + self.speakingParticipantsPromise.set(self.speakingParticipants) + } + } + + private let audioLevelsPromise = Promise<[(PeerId, UInt32, Float, Bool)]>() + + init() { + } + + func update(levels: [(PeerId, UInt32, Float, Bool)]) { + let timestamp = Int32(CFAbsoluteTimeGetCurrent()) + let currentParticipants: [PeerId: Participant] = self.participants + + var validSpeakers: [PeerId: Participant] = [:] + var silentParticipants = Set() + var speakingParticipants = [PeerId: UInt32]() + for (peerId, ssrc, level, hasVoice) in levels { + if level > speakingLevelThreshold && hasVoice { + validSpeakers[peerId] = Participant(ssrc: ssrc, timestamp: timestamp, level: level) + speakingParticipants[peerId] = ssrc + } else { + silentParticipants.insert(peerId) + } + } + + for (peerId, participant) in currentParticipants { + if let _ = validSpeakers[peerId] { + } else { + let delta = timestamp - participant.timestamp + if silentParticipants.contains(peerId) { + if delta < silentTimeout { + validSpeakers[peerId] = participant + speakingParticipants[peerId] = participant.ssrc + } + } else if delta < cutoffTimeout { + validSpeakers[peerId] = participant + speakingParticipants[peerId] = participant.ssrc + } + } + } + + var audioLevels: [(PeerId, UInt32, Float, Bool)] = [] + for (peerId, source, level, hasVoice) in levels { + if level > 0.001 { + audioLevels.append((peerId, source, level, hasVoice)) + } + } + + self.participants = validSpeakers + self.speakingParticipants = speakingParticipants + self.audioLevelsPromise.set(.single(audioLevels)) + } + + func get() -> Signal<[PeerId: UInt32], NoError> { + return self.speakingParticipantsPromise.get() + } + + func getAudioLevels() -> Signal<[(PeerId, UInt32, Float, Bool)], NoError> { + return self.audioLevelsPromise.get() |> distinctUntilChanged(isEqual: { lhs, rhs in + if lhs.count != rhs.count { + return false + } else { + for (i, lhsValue) in lhs.enumerated() { + let rhsValue = rhs[i] + if lhsValue != rhsValue { + return false + } + } + } + return true + }) + } + } + + let account: Account + let accountContext: AccountContext + + var engine: TelegramEngine { + return accountContext.engine + } + + private var initialCall: CachedChannelData.ActiveCall? + let internalId: CallSessionInternalId + let peerId: PeerId + private var invite: String? + private var joinAsPeerIdSignal:ValuePromise = ValuePromise(ignoreRepeated: true) + var joinAsPeerIdValue:Signal { + return joinAsPeerIdSignal.get() + } + private(set) var joinAsPeerId: PeerId { + didSet { + joinAsPeerIdSignal.set(joinAsPeerId) + } + } + private var ignorePreviousJoinAsPeerId: (PeerId, UInt32)? + private var reconnectingAsPeer: Peer? + + public private(set) var hasVideo: Bool + public private(set) var hasScreencast: Bool + + + + private let updateTitleDisposable = MetaDisposable() + + private var temporaryJoinTimestamp: Int32 + private var temporaryActivityTimestamp: Double? + private var temporaryActivityRank: Int? + private var temporaryRaiseHandRating: Int64? + private var temporaryHasRaiseHand: Bool = false + private var temporaryVideoJoined: Bool = true + private var temporaryMuteState: GroupCallParticipantsContext.Participant.MuteState? + + private var internalState: InternalState = .requesting + private let internalStatePromise = Promise(.requesting) + private var currentLocalSsrc: UInt32? + + private var genericCallContext: OngoingGroupCallContext? + private var currentConnectionMode: OngoingGroupCallContext.ConnectionMode = .none + private var screencastCallContext: OngoingGroupCallContext? + + private struct SsrcMapping { + var peerId: PeerId + var isPresentation: Bool + } + private var ssrcMapping: [UInt32: SsrcMapping] = [:] + + private var requestedSsrcs = Set() + + private var summaryInfoState = Promise(nil) + + var callInfo: Signal { + return summaryInfoState.get() |> map { $0?.info } + } + + private var summaryParticipantsState = Promise(nil) + + private let summaryStatePromise = Promise(nil) + var summaryState: Signal { + return self.summaryStatePromise.get() |> distinctUntilChanged + } + private var summaryStateDisposable: Disposable? + + private var isMutedValue: PresentationGroupCallMuteAction = .muted(isPushToTalkActive: false) { + didSet { + if self.isMutedValue != oldValue { + } + } + } + private let isMutedPromise = ValuePromise(.muted(isPushToTalkActive: false)) + var isMuted: Signal { + return self.isMutedPromise.get() + |> map { value -> Bool in + switch value { + case let .muted(isPushToTalkActive): + return !isPushToTalkActive + case .unmuted: + return false + } + } + } + + private var settingsDisposable: Disposable? + + private var audioLevelsDisposable = MetaDisposable() + + private let speakingParticipantsContext = SpeakingParticipantsContext() + private var speakingParticipantsReportTimestamp: [PeerId: Double] = [:] + var audioLevels: Signal<[(PeerId, UInt32, Float, Bool)], NoError> { + return self.speakingParticipantsContext.getAudioLevels() + } + + private var participantsContextStateDisposable = MetaDisposable() + private var temporaryParticipantsContext: GroupCallParticipantsContext? + private var participantsContext: GroupCallParticipantsContext? + + private let myAudioLevelPipe = ValuePipe() + var myAudioLevel: Signal { + return self.myAudioLevelPipe.signal() + } + private var myAudioLevelDisposable = MetaDisposable() + + private let typingDisposable = MetaDisposable() + + private let _canBeRemoved = Promise(false) + var canBeRemoved: Signal { + return self._canBeRemoved.get() + } + private var markedAsCanBeRemoved = false + + private let wasRemoved = Promise(false) + private var leaving = false + + private var stateValue: PresentationGroupCallState { + didSet { + if self.stateValue != oldValue { + self.statePromise.set(self.stateValue) + } + } + } + private let statePromise: ValuePromise + var state: Signal { + return self.statePromise.get() + } + + private var stateVersionValue: Int = 0 { + didSet { + if self.stateVersionValue != oldValue { + self.stateVersionPromise.set(self.stateVersionValue) + } + } + } + private let stateVersionPromise = ValuePromise(0) + public var stateVersion: Signal { + return self.stateVersionPromise.get() + } + + + private var membersValue: PresentationGroupCallMembers? { + didSet { + if self.membersValue != oldValue { + self.membersPromise.set(self.membersValue) + } + } + } + private let membersPromise = ValuePromise(nil) + var members: Signal { + return self.membersPromise.get() + } + + private var invitedPeersValue: [PeerId] = [] { + didSet { + if self.invitedPeersValue != oldValue { + self.inivitedPeersPromise.set(self.invitedPeersValue) + } + } + } + private let inivitedPeersPromise = ValuePromise<[PeerId]>([]) + var invitedPeers: Signal<[PeerId], NoError> { + return self.inivitedPeersPromise.get() + } + + private let memberEventsPipe = ValuePipe() + var memberEvents: Signal { + return self.memberEventsPipe.signal() + } + private let memberEventsPipeDisposable = MetaDisposable() + + private let reconnectedAsEventsPipe = ValuePipe() + var reconnectedAsEvents: Signal { + return self.reconnectedAsEventsPipe.signal() + } + + private let joinDisposable = MetaDisposable() + private let screencastJoinDisposable = MetaDisposable() + private let requestDisposable = MetaDisposable() + private var groupCallParticipantUpdatesDisposable: Disposable? + + private let networkStateDisposable = MetaDisposable() + private let isMutedDisposable = MetaDisposable() + private let memberStatesDisposable = MetaDisposable() + private let leaveDisposable = MetaDisposable() + + private var isReconnectingAsSpeaker = false { + didSet { + if self.isReconnectingAsSpeaker != oldValue { + self.isReconnectingAsSpeakerPromise.set(self.isReconnectingAsSpeaker) + } + } + } + private let isReconnectingAsSpeakerPromise = ValuePromise(false) + + private var checkCallDisposable: Disposable? + private var isCurrentlyConnecting: Bool? + + private var myAudioLevelTimer: SwiftSignalKit.Timer? + + private var proximityManagerIndex: Int? + + private var removedChannelMembersDisposable: Disposable? + + private var didStartConnectingOnce: Bool = false + private var didConnectOnce: Bool = false + + private var videoCapturer: OngoingCallVideoCapturer? + + private var screenCapturer: OngoingCallVideoCapturer? + private let screencastEndpointIdValue: ValuePromise = ValuePromise(nil, ignoreRepeated: true) + private var screencastEndpointId: String? = nil { + didSet { + screencastEndpointIdValue.set(screencastEndpointId) + } + } + + public private(set) var schedulePending = false + private var isScheduled = false + private var isScheduledStarted = false + + private let isSpeakingPromise = ValuePromise(false, ignoreRepeated: true) + public var isSpeaking: Signal { + return self.isSpeakingPromise.get() + } + + + private var peerUpdatesSubscription: Disposable? + + init( + accountContext: AccountContext, + initialCall: CachedChannelData.ActiveCall?, + internalId: CallSessionInternalId, + peerId: PeerId, + invite: String?, + joinAsPeerId: PeerId?, + initialInfo: GroupCallInfo? + ) { + self.account = accountContext.account + self.accountContext = accountContext + + self.initialCall = initialCall + self.internalId = internalId + self.peerId = peerId + self.invite = invite + self.joinAsPeerId = joinAsPeerId ?? accountContext.account.peerId + self.joinAsPeerIdSignal.set(self.joinAsPeerId) + let peerSignal = account.postbox.peerView(id: peerId) + |> map { peerViewMainPeer($0) } + |> deliverOnMainQueue + + self.stateValue = PresentationGroupCallState.initialValue(myPeerId: self.joinAsPeerId, title: initialCall?.title, scheduledTimestamp: initialCall?.scheduleTimestamp, subscribedToScheduled: initialCall?.subscribedToScheduled ?? false) + self.statePromise = ValuePromise(self.stateValue) + + self.temporaryJoinTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + + self.hasVideo = false + self.hasScreencast = false + + self.devicesContext = accountContext.sharedContext.devicesContext + + devicesDisposable.set(devicesContext.updater().start(next: { [weak self] values in + guard let `self` = self else { + return + } + if let id = values.input { + self.genericCallContext?.switchAudioInput(id) + } + if let id = values.output { + self.genericCallContext?.switchAudioOutput(id) + } + })) + + self.loadPeerDisposable.set(peerSignal.start(next: { [weak self] peer in + self?.peer = peer + })) + + self.groupCallParticipantUpdatesDisposable = (self.account.stateManager.groupCallParticipantUpdates + |> deliverOnMainQueue).start(next: { [weak self] updates in + guard let strongSelf = self else { + return + } + if case let .established(callInfo, _, _, _, _) = strongSelf.internalState { + var addedParticipants: [(UInt32, String?, String?)] = [] + var removedSsrc: [UInt32] = [] + for (callId, update) in updates { + if callId == callInfo.id { + switch update { + case let .state(update): + for participantUpdate in update.participantUpdates { + if case .left = participantUpdate.participationStatusChange { + if let ssrc = participantUpdate.ssrc { + removedSsrc.append(ssrc) + } + + if participantUpdate.peerId == strongSelf.joinAsPeerId { + if case let .established(_, _, _, ssrc, _) = strongSelf.internalState, ssrc == participantUpdate.ssrc { + strongSelf.markAsCanBeRemoved() + } + } + } else if participantUpdate.peerId == strongSelf.joinAsPeerId { + if case let .established(_, connectionMode, _, ssrc, _) = strongSelf.internalState { + if ssrc != participantUpdate.ssrc { + strongSelf.markAsCanBeRemoved() + } else if case .broadcast = connectionMode { + let canUnmute: Bool + if let muteState = participantUpdate.muteState { + canUnmute = muteState.canUnmute + } else { + canUnmute = true + } + + if canUnmute { + strongSelf.requestCall(movingFromBroadcastToRtc: true) + } + } + } + } else if case .joined = participantUpdate.participationStatusChange { + } else if let ssrc = participantUpdate.ssrc, strongSelf.ssrcMapping[ssrc] == nil { + } + } + case let .call(isTerminated, _, _, _, _, _): + if isTerminated { + strongSelf.markAsCanBeRemoved() + } + } + } + } + if !removedSsrc.isEmpty { + strongSelf.genericCallContext?.removeSsrcs(ssrcs: removedSsrc) + } + //strongSelf.callContext?.addParticipants(participants: addedParticipants) + } + }) + + self.displayAsPeersValue.set(accountContext.engine.calls.cachedGroupCallDisplayAsAvailablePeers(peerId: peerId) |> map(Optional.init)) + + + self.summaryStatePromise.set(combineLatest(queue: .mainQueue(), + self.summaryInfoState.get(), + self.summaryParticipantsState.get(), + self.statePromise.get() + ) + |> map { infoState, participantsState, callState -> PresentationGroupCallSummaryState? in + guard let participantsState = participantsState else { + return nil + } + return PresentationGroupCallSummaryState( + info: infoState?.info, + participantCount: participantsState.participantCount, + callState: callState, + topParticipants: participantsState.topParticipants, + activeSpeakers: participantsState.activeSpeakers + ) + }) + + if let initialCall = initialCall, let temporaryParticipantsContext = self.accountContext.cachedGroupCallContexts.impl.syncWith({ impl in + impl.get(context: accountContext, peerId: peerId, call: initialCall) + }) { + self.switchToTemporaryParticipantsContext(sourceContext: temporaryParticipantsContext.context.participantsContext, oldMyPeerId: self.joinAsPeerId) + } else { + self.switchToTemporaryParticipantsContext(sourceContext: nil, oldMyPeerId: self.joinAsPeerId) + } + + + let _ = (self.account.postbox.loadedPeerWithId(peerId) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let strongSelf = self else { + return + } + var canManageCall = false + if let peer = peer as? TelegramGroup { + if case .creator = peer.role { + canManageCall = true + } else if case let .admin(rights, _) = peer.role, rights.rights.contains(.canManageCalls) { + canManageCall = true + } + } else if let peer = peer as? TelegramChannel { + if peer.flags.contains(.isCreator) { + canManageCall = true + } else if (peer.adminRights?.rights.contains(.canManageCalls) == true) { + canManageCall = true + } + strongSelf.peerUpdatesSubscription = strongSelf.accountContext.account.viewTracker.polledChannel(peerId: peer.id).start() + } + var updatedValue = strongSelf.stateValue + updatedValue.canManageCall = canManageCall + strongSelf.stateValue = updatedValue + }) + + // if initialCall?.scheduleTimestamp == nil { + self.requestCall(movingFromBroadcastToRtc: false) + // } + if let initialInfo = initialInfo { + summaryInfoState.set(.single(.init(info: initialInfo))) + } + } + + deinit { + self.summaryStateDisposable?.dispose() + self.joinDisposable.dispose() + self.requestDisposable.dispose() + self.groupCallParticipantUpdatesDisposable?.dispose() + self.leaveDisposable.dispose() + self.isMutedDisposable.dispose() + self.memberStatesDisposable.dispose() + self.networkStateDisposable.dispose() + self.checkCallDisposable?.dispose() + self.audioLevelsDisposable.dispose() + self.participantsContextStateDisposable.dispose() + self.myAudioLevelDisposable.dispose() + self.memberEventsPipeDisposable.dispose() + self.screencastJoinDisposable.dispose() + self.myAudioLevelTimer?.invalidate() + self.typingDisposable.dispose() + self.updateTitleDisposable.dispose() + self.removedChannelMembersDisposable?.dispose() + + self.peerUpdatesSubscription?.dispose() + self.devicesDisposable.dispose() + self.loadPeerDisposable.dispose() + self.startDisposable.dispose() + self.subscribeDisposable.dispose() + self.updateGroupCallJoinAsDisposable.dispose() + self.settingsDisposable?.dispose() + } + + private func switchToTemporaryParticipantsContext(sourceContext: GroupCallParticipantsContext?, oldMyPeerId: PeerId) { + let myPeerId = self.joinAsPeerId + let myPeer = self.accountContext.account.postbox.transaction { transaction -> (Peer, CachedPeerData?)? in + if let peer = transaction.getPeer(myPeerId) { + return (peer, transaction.getPeerCachedData(peerId: myPeerId)) + } else { + return nil + } + } + if let sourceContext = sourceContext, let initialState = sourceContext.immediateState { + let temporaryParticipantsContext = accountContext.engine.calls.groupCall(peerId: self.peerId, myPeerId: myPeerId, id: sourceContext.id, accessHash: sourceContext.accessHash, state: initialState, previousServiceState: sourceContext.serviceState) + self.temporaryParticipantsContext = temporaryParticipantsContext + self.participantsContextStateDisposable.set((combineLatest(queue: .mainQueue(), + myPeer, + temporaryParticipantsContext.state, + temporaryParticipantsContext.activeSpeakers + ) + |> take(1)).start(next: { [weak self] myPeerAndCachedData, state, activeSpeakers in + guard let strongSelf = self else { + return + } + + var topParticipants: [GroupCallParticipantsContext.Participant] = [] + + var members = PresentationGroupCallMembers( + participants: [], + speakingParticipants: [], + totalCount: 0, + loadMoreToken: nil + ) + + var updatedInvitedPeers = strongSelf.invitedPeersValue + var didUpdateInvitedPeers = false + + var participants = state.participants + + if oldMyPeerId != myPeerId { + for i in 0 ..< participants.count { + if participants[i].peer.id == oldMyPeerId { + participants.remove(at: i) + break + } + } + } + + if !participants.contains(where: { $0.peer.id == myPeerId }) { + if let (myPeer, cachedData) = myPeerAndCachedData { + let about: String? + if let cachedData = cachedData as? CachedUserData { + about = cachedData.about + } else if let cachedData = cachedData as? CachedUserData { + about = cachedData.about + } else { + about = nil + } + participants.append(GroupCallParticipantsContext.Participant( + peer: myPeer, + ssrc: nil, + videoDescription: nil, + presentationDescription: nil, + joinTimestamp: strongSelf.temporaryJoinTimestamp, + raiseHandRating: strongSelf.temporaryRaiseHandRating, + hasRaiseHand: strongSelf.temporaryHasRaiseHand, + activityTimestamp: strongSelf.temporaryActivityTimestamp, + activityRank: strongSelf.temporaryActivityRank, + muteState: strongSelf.temporaryMuteState ?? GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false), + volume: nil, + about: about, + joinedVideo: strongSelf.temporaryVideoJoined + )) + participants.sort(by: { GroupCallParticipantsContext.Participant.compare(lhs: $0, rhs: $1, sortAscending: state.sortAscending) }) + } + } + + for participant in participants { + members.participants.append(participant) + + if topParticipants.count < 3 { + topParticipants.append(participant) + } + + if let index = updatedInvitedPeers.firstIndex(of: participant.peer.id) { + updatedInvitedPeers.remove(at: index) + didUpdateInvitedPeers = true + } + } + + members.totalCount = state.totalCount + members.loadMoreToken = state.nextParticipantsFetchOffset + + strongSelf.membersValue = members + + var stateValue = strongSelf.stateValue + stateValue.myPeerId = strongSelf.joinAsPeerId + stateValue.adminIds = state.adminIds + + stateValue.scheduleTimestamp = state.scheduleTimestamp + strongSelf.stateValue = stateValue + + strongSelf.summaryParticipantsState.set(.single(SummaryParticipantsState( + participantCount: state.totalCount, + topParticipants: topParticipants, + activeSpeakers: activeSpeakers + ))) + + if didUpdateInvitedPeers { + strongSelf.invitedPeersValue = updatedInvitedPeers + } + })) + } else { + self.temporaryParticipantsContext = nil + self.participantsContextStateDisposable.set((myPeer + |> deliverOnMainQueue + |> take(1)).start(next: { [weak self] myPeerAndCachedData in + guard let strongSelf = self else { + return + } + + var topParticipants: [GroupCallParticipantsContext.Participant] = [] + + var members = PresentationGroupCallMembers( + participants: [], + speakingParticipants: [], + totalCount: 0, + loadMoreToken: nil + ) + + var participants: [GroupCallParticipantsContext.Participant] = [] + + if let (myPeer, cachedData) = myPeerAndCachedData { + let about: String? + if let cachedData = cachedData as? CachedUserData { + about = cachedData.about + } else if let cachedData = cachedData as? CachedUserData { + about = cachedData.about + } else { + about = nil + } + participants.append(GroupCallParticipantsContext.Participant( + peer: myPeer, + ssrc: nil, + videoDescription: nil, + presentationDescription: nil, + joinTimestamp: strongSelf.temporaryJoinTimestamp, + raiseHandRating: strongSelf.temporaryRaiseHandRating, + hasRaiseHand: strongSelf.temporaryHasRaiseHand, + activityTimestamp: strongSelf.temporaryActivityTimestamp, + activityRank: strongSelf.temporaryActivityRank, + muteState: strongSelf.temporaryMuteState ?? GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false), + volume: nil, + about: about, + joinedVideo: strongSelf.temporaryVideoJoined + )) + } + + for participant in participants { + members.participants.append(participant) + + if topParticipants.count < 3 { + topParticipants.append(participant) + } + } + + strongSelf.membersValue = members + + var stateValue = strongSelf.stateValue + stateValue.myPeerId = strongSelf.joinAsPeerId + + strongSelf.stateValue = stateValue + })) + } + } + + private func updateSessionState(internalState: InternalState) { + + let previousInternalState = self.internalState + self.internalState = internalState + self.internalStatePromise.set(.single(internalState)) + + var shouldJoin = false + let activeCallInfo: GroupCallInfo? + switch previousInternalState { + case let .active(previousCallInfo): + if case let .active(callInfo) = internalState { + shouldJoin = previousCallInfo.scheduleTimestamp != nil && callInfo.scheduleTimestamp == nil + activeCallInfo = callInfo + } else { + activeCallInfo = nil + } + default: + if case let .active(callInfo) = internalState { + shouldJoin = callInfo.scheduleTimestamp == nil + activeCallInfo = callInfo + } else { + activeCallInfo = nil + } + } + + + switch previousInternalState { + case .requesting: + break + default: + if case .requesting = internalState { + self.isCurrentlyConnecting = nil + } + } + + if shouldJoin, let callInfo = activeCallInfo { + let genericCallContext: OngoingGroupCallContext + if let current = self.genericCallContext { + genericCallContext = current + } else { + genericCallContext = OngoingGroupCallContext(inputDeviceId: devicesContext.currentMicroId ?? "", outputDeviceId: devicesContext.currentOutputId ?? "", video: self.videoCapturer, requestMediaChannelDescriptions: { [weak self] ssrcs, completion in + let disposable = MetaDisposable() + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + disposable.set(strongSelf.requestMediaChannelDescriptions(ssrcs: ssrcs, completion: completion)) + } + return disposable + }, audioStreamData: OngoingGroupCallContext.AudioStreamData(engine: self.accountContext.engine, callId: callInfo.id, accessHash: callInfo.accessHash), rejoinNeeded: { [weak self] in + Queue.mainQueue().async { + guard let strongSelf = self else { + return + } + if case .established = strongSelf.internalState { + strongSelf.requestCall(movingFromBroadcastToRtc: false) + } + } + }, outgoingAudioBitrateKbit: nil, videoContentType: .generic, enableNoiseSuppression: false) + + self.settingsDisposable = (voiceCallSettings(self.sharedContext.accountManager) |> deliverOnMainQueue).start(next: { [weak self] settings in + self?.genericCallContext?.setIsNoiseSuppressionEnabled(settings.noiseSuppression) + }) + + self.genericCallContext = genericCallContext + self.stateVersionValue += 1 + } + self.joinDisposable.set((genericCallContext.joinPayload + |> distinctUntilChanged(isEqual: { lhs, rhs in + if lhs.0 != rhs.0 { + return false + } + if lhs.1 != rhs.1 { + return false + } + return true + }) + |> deliverOnMainQueue).start(next: { [weak self] joinPayload, ssrc in + guard let strongSelf = self else { + return + } + + let peerAdminIds: Signal<[PeerId], NoError> + let peerId = strongSelf.peerId + if strongSelf.peerId.namespace == Namespaces.Peer.CloudChannel { + peerAdminIds = Signal { subscriber in + let (disposable, _) = strongSelf.accountContext.peerChannelMemberCategoriesContextsManager.admins(peerId: peerId, updated: { list in + var peerIds = Set() + for item in list.list { + if let adminInfo = item.participant.adminInfo, adminInfo.rights.rights.contains(.canManageCalls) { + peerIds.insert(item.peer.id) + } + } + subscriber.putNext(Array(peerIds)) + }) + return disposable + } + |> distinctUntilChanged + |> runOn(.mainQueue()) + } else { + peerAdminIds = strongSelf.account.postbox.transaction { transaction -> [PeerId] in + var result: [PeerId] = [] + if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedGroupData { + if let participants = cachedData.participants { + for participant in participants.participants { + if case .creator = participant { + result.append(participant.peerId) + } else if case .admin = participant { + result.append(participant.peerId) + } + } + } + } + return result + } + } + + strongSelf.currentLocalSsrc = ssrc + strongSelf.requestDisposable.set((strongSelf.accountContext.engine.calls.joinGroupCall( + peerId: strongSelf.peerId, + joinAs: strongSelf.joinAsPeerId, + callId: callInfo.id, + accessHash: callInfo.accessHash, + preferMuted: true, + joinPayload: joinPayload, + peerAdminIds: peerAdminIds, + inviteHash: strongSelf.invite + ) + |> deliverOnMainQueue).start(next: { joinCallResult in + guard let strongSelf = self else { + return + } + let clientParams = joinCallResult.jsonParams + + strongSelf.ssrcMapping.removeAll() + for participant in joinCallResult.state.participants { + if let ssrc = participant.ssrc { + strongSelf.ssrcMapping[ssrc] = SsrcMapping(peerId: participant.peer.id, isPresentation: false) + } + if let presentationSsrc = participant.presentationDescription?.audioSsrc { + strongSelf.ssrcMapping[presentationSsrc] = SsrcMapping(peerId: participant.peer.id, isPresentation: true) + } + } + + switch joinCallResult.connectionMode { + case .rtc: + strongSelf.currentConnectionMode = .rtc + strongSelf.genericCallContext?.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false) + strongSelf.genericCallContext?.setJoinResponse(payload: clientParams) + case .broadcast: + strongSelf.currentConnectionMode = .broadcast + strongSelf.genericCallContext?.setConnectionMode(.broadcast, keepBroadcastConnectedIfWasEnabled: false) + } + + strongSelf.updateSessionState(internalState: .established(info: joinCallResult.callInfo, connectionMode: joinCallResult.connectionMode, clientParams: clientParams, localSsrc: ssrc, initialState: joinCallResult.state)) + + }, error: { error in + guard let strongSelf = self else { + return + } + + if case .anonymousNotAllowed = error { + alert(for: strongSelf.accountContext.window, info: L10n.voiceChatAnonymousDisabledAlertText) + } else if case .tooManyParticipants = error { + alert(for: strongSelf.accountContext.window, info: L10n.voiceChatJoinErrorTooMany) + } + strongSelf.markAsCanBeRemoved() + })) + })) + self.networkStateDisposable.set((genericCallContext.networkState + |> deliverOnMainQueue).start(next: { [weak self] state in + guard let strongSelf = self else { + return + } + let mappedState: PresentationGroupCallState.NetworkState + if state.isConnected { + mappedState = .connected + } else { + mappedState = .connecting + } + + let wasConnecting = strongSelf.stateValue.networkState == .connecting + if strongSelf.stateValue.networkState != mappedState { + strongSelf.stateValue.networkState = mappedState + } + let isConnecting = mappedState == .connecting + + if strongSelf.isCurrentlyConnecting != isConnecting { + strongSelf.isCurrentlyConnecting = isConnecting + if isConnecting { + strongSelf.startCheckingCallIfNeeded() + } else { + strongSelf.checkCallDisposable?.dispose() + strongSelf.checkCallDisposable = nil + } + } + + strongSelf.isReconnectingAsSpeaker = state.isTransitioningFromBroadcastToRtc + + if isConnecting { + strongSelf.didStartConnectingOnce = true + } + + if state.isConnected { + if !strongSelf.didConnectOnce { + strongSelf.didConnectOnce = true + } + + if let peer = strongSelf.reconnectingAsPeer { + strongSelf.reconnectingAsPeer = nil + strongSelf.reconnectedAsEventsPipe.putNext(peer) + } + } + })) + + self.audioLevelsDisposable.set((genericCallContext.audioLevels + |> deliverOnMainQueue).start(next: { [weak self] levels in + guard let strongSelf = self else { + return + } + var result: [(PeerId, UInt32, Float, Bool)] = [] + var myLevel: Float = 0.0 + var myLevelHasVoice: Bool = false + var orignalMyLevelHasVoice: Bool = false + var missingSsrcs = Set() + for (ssrcKey, level, hasVoice) in levels { + var peerId: PeerId? + let ssrcValue: UInt32 + switch ssrcKey { + case .local: + peerId = strongSelf.joinAsPeerId + ssrcValue = 0 + case let .source(ssrc): + if let mapping = strongSelf.ssrcMapping[ssrc] { + if mapping.isPresentation { + peerId = nil + ssrcValue = 0 + } else { + peerId = mapping.peerId + ssrcValue = ssrc + } + } else { + ssrcValue = ssrc + } + } + if let peerId = peerId { + if case .local = ssrcKey { + orignalMyLevelHasVoice = hasVoice + myLevel = level + myLevelHasVoice = hasVoice + } + result.append((peerId, ssrcValue, level, hasVoice)) + } else if ssrcValue != 0 { + missingSsrcs.insert(ssrcValue) + } + + } + + strongSelf.speakingParticipantsContext.update(levels: result) + + if strongSelf.stateValue.muteState == nil { + let mappedLevel = myLevel * 1.5 + strongSelf.myAudioLevelPipe.putNext(mappedLevel) + strongSelf.processMyAudioLevel(level: mappedLevel, hasVoice: myLevelHasVoice && orignalMyLevelHasVoice) + } else { + strongSelf.myAudioLevelPipe.putNext(0) + strongSelf.processMyAudioLevel(level: 0, hasVoice: false) + } + + strongSelf.isSpeakingPromise.set(orignalMyLevelHasVoice) + if !missingSsrcs.isEmpty { + strongSelf.participantsContext?.ensureHaveParticipants(ssrcs: missingSsrcs) + } + })) + } + + switch previousInternalState { + case .established: + break + default: + if case let .established(callInfo, _, _, _, initialState) = internalState { + self.summaryInfoState.set(.single(SummaryInfoState(info: callInfo))) + + var stateValue = self.stateValue + + stateValue.canManageCall = initialState.isCreator || initialState.adminIds.contains(self.accountContext.account.peerId) + if stateValue.canManageCall && initialState.defaultParticipantsAreMuted.canChange { + stateValue.defaultParticipantMuteState = initialState.defaultParticipantsAreMuted.isMuted ? .muted : .unmuted + } + stateValue.recordingStartTimestamp = initialState.recordingStartTimestamp + stateValue.title = initialState.title + + stateValue.scheduleTimestamp = initialState.scheduleTimestamp + stateValue.subscribedToScheduled = initialState.subscribedToScheduled + + self.stateValue = stateValue + + let accountContext = self.accountContext + let peerId = self.peerId + let rawAdminIds: Signal, NoError> + if peerId.namespace == Namespaces.Peer.CloudChannel { + rawAdminIds = Signal { subscriber in + let (disposable, _) = accountContext.peerChannelMemberCategoriesContextsManager.admins(peerId: peerId, updated: { list in + var peerIds = Set() + for item in list.list { + if let adminInfo = item.participant.adminInfo, adminInfo.rights.rights.contains(.canManageCalls) { + peerIds.insert(item.peer.id) + } + } + subscriber.putNext(peerIds) + }) + return disposable + } + |> distinctUntilChanged + |> runOn(.mainQueue()) + } else { + rawAdminIds = accountContext.account.postbox.combinedView(keys: [.cachedPeerData(peerId: peerId)]) + |> map { views -> Set in + guard let view = views.views[.cachedPeerData(peerId: peerId)] as? CachedPeerDataView else { + return Set() + } + guard let cachedData = view.cachedPeerData as? CachedGroupData, let participants = cachedData.participants else { + return Set() + } + return Set(participants.participants.compactMap { item -> PeerId? in + switch item { + case .creator, .admin: + return item.peerId + default: + return nil + } + }) + } + |> distinctUntilChanged + } + + let adminIds = combineLatest(queue: .mainQueue(), + rawAdminIds, + accountContext.account.postbox.combinedView(keys: [.basicPeer(peerId)]) + ) + |> map { rawAdminIds, view -> Set in + var rawAdminIds = rawAdminIds + if let peerView = view.views[.basicPeer(peerId)] as? BasicPeerView, let peer = peerView.peer as? TelegramChannel { + if peer.hasPermission(.manageCalls) { + rawAdminIds.insert(accountContext.account.peerId) + } else { + rawAdminIds.remove(accountContext.account.peerId) + } + } + return rawAdminIds + } + |> distinctUntilChanged + + let myPeerId = self.joinAsPeerId + + var initialState = initialState + var serviceState: GroupCallParticipantsContext.ServiceState? + if let participantsContext = self.participantsContext, let immediateState = participantsContext.immediateState { + initialState.mergeActivity(from: immediateState, myPeerId: myPeerId, previousMyPeerId: self.ignorePreviousJoinAsPeerId?.0, mergeActivityTimestamps: true) + serviceState = participantsContext.serviceState + } + + let participantsContext = accountContext.engine.calls.groupCall( + peerId: self.peerId, + myPeerId: self.joinAsPeerId, + id: callInfo.id, + accessHash: callInfo.accessHash, + state: initialState, + previousServiceState: serviceState + ) + self.temporaryParticipantsContext = nil + self.participantsContext = participantsContext + let myPeer = self.accountContext.account.postbox.transaction { transaction -> (Peer, CachedPeerData?)? in + if let peer = transaction.getPeer(myPeerId) { + return (peer, transaction.getPeerCachedData(peerId: myPeerId)) + } else { + return nil + } + } + self.participantsContextStateDisposable.set(combineLatest(queue: .mainQueue(), + participantsContext.state, + participantsContext.activeSpeakers, + self.speakingParticipantsContext.get(), + adminIds, + myPeer, + accountContext.account.postbox.peerView(id: peerId), + self.isReconnectingAsSpeakerPromise.get() + ).start(next: { [weak self] state, activeSpeakers, speakingParticipants, adminIds, myPeerAndCachedData, view, isReconnectingAsSpeaker in + guard let strongSelf = self else { + return + } + + strongSelf.participantsContext?.updateAdminIds(adminIds) + + var topParticipants: [GroupCallParticipantsContext.Participant] = [] + + var reportSpeakingParticipants: [PeerId: UInt32] = [:] + let timestamp = CACurrentMediaTime() + for (peerId, ssrc) in speakingParticipants { + let shouldReport: Bool + if let previousTimestamp = strongSelf.speakingParticipantsReportTimestamp[peerId] { + shouldReport = previousTimestamp + 1.0 < timestamp + } else { + shouldReport = true + } + if shouldReport { + strongSelf.speakingParticipantsReportTimestamp[peerId] = timestamp + reportSpeakingParticipants[peerId] = ssrc + } + } + + if !reportSpeakingParticipants.isEmpty { + Queue.mainQueue().justDispatch { + self?.participantsContext?.reportSpeakingParticipants(ids: reportSpeakingParticipants) + } + } + + var members = PresentationGroupCallMembers( + participants: [], + speakingParticipants: Set(speakingParticipants.keys), + totalCount: 0, + loadMoreToken: nil + ) + + var updatedInvitedPeers = strongSelf.invitedPeersValue + var didUpdateInvitedPeers = false + + var participants = state.participants + + if let (ignorePeerId, ignoreSsrc) = strongSelf.ignorePreviousJoinAsPeerId { + for i in 0 ..< participants.count { + if participants[i].peer.id == ignorePeerId && participants[i].ssrc == ignoreSsrc { + participants.remove(at: i) + break + } + } + } + + if !participants.contains(where: { $0.peer.id == myPeerId }) && !strongSelf.leaving { + if let (myPeer, cachedData) = myPeerAndCachedData { + let about: String? + if let cachedData = cachedData as? CachedUserData { + about = cachedData.about + } else if let cachedData = cachedData as? CachedChannelData { + about = cachedData.about + } else { + about = nil + } + + participants.append(GroupCallParticipantsContext.Participant( + peer: myPeer, + ssrc: nil, + videoDescription: nil, + presentationDescription: nil, + joinTimestamp: strongSelf.temporaryJoinTimestamp, + raiseHandRating: strongSelf.temporaryRaiseHandRating, + hasRaiseHand: strongSelf.temporaryHasRaiseHand, + activityTimestamp: strongSelf.temporaryActivityTimestamp, + activityRank: strongSelf.temporaryActivityRank, + muteState: strongSelf.temporaryMuteState ?? GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false), + volume: nil, + about: about, + joinedVideo: strongSelf.temporaryVideoJoined + )) + participants.sort(by: { GroupCallParticipantsContext.Participant.compare(lhs: $0, rhs: $1, sortAscending: state.sortAscending) }) + } + } + + var otherParticipantsWithVideo = 0 + + for participant in participants { + var participant = participant + + if topParticipants.count < 3 { + topParticipants.append(participant) + } + + if let ssrc = participant.ssrc { + strongSelf.ssrcMapping[ssrc] = SsrcMapping(peerId: participant.peer.id, isPresentation: false) + } + if let presentationSsrc = participant.presentationDescription?.audioSsrc { + strongSelf.ssrcMapping[presentationSsrc] = SsrcMapping(peerId: participant.peer.id, isPresentation: true) + } + + + if participant.peer.id == strongSelf.joinAsPeerId { + var filteredMuteState = participant.muteState + if isReconnectingAsSpeaker || strongSelf.currentConnectionMode != .rtc { + filteredMuteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: false, mutedByYou: false) + participant.muteState = filteredMuteState + } + + if !(strongSelf.stateValue.muteState?.canUnmute ?? false) { + strongSelf.stateValue.raisedHand = participant.hasRaiseHand + } + + if let muteState = filteredMuteState { + if muteState.canUnmute { + switch strongSelf.isMutedValue { + case let .muted(isPushToTalkActive): + if !isPushToTalkActive { + strongSelf.genericCallContext?.setIsMuted(true) + } + case .unmuted: + strongSelf.isMutedValue = .muted(isPushToTalkActive: false) + strongSelf.genericCallContext?.setIsMuted(true) + } + } else { + strongSelf.isMutedValue = .muted(isPushToTalkActive: false) + strongSelf.genericCallContext?.setIsMuted(true) + } + strongSelf.stateValue.muteState = muteState + } else if let currentMuteState = strongSelf.stateValue.muteState, !currentMuteState.canUnmute { + strongSelf.isMutedValue = .muted(isPushToTalkActive: false) + strongSelf.stateValue.muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false) + strongSelf.genericCallContext?.setIsMuted(true) + } + } else { + if let ssrc = participant.ssrc { + if let volume = participant.volume { + strongSelf.genericCallContext?.setVolume(ssrc: ssrc, volume: Double(volume) / 10000.0) + } else if participant.muteState?.mutedByYou == true { + strongSelf.genericCallContext?.setVolume(ssrc: ssrc, volume: 0.0) + } + } + if let presentationSsrc = participant.presentationDescription?.audioSsrc { + if let volume = participant.volume { + strongSelf.genericCallContext?.setVolume(ssrc: presentationSsrc, volume: Double(volume) / 10000.0) + } else if participant.muteState?.mutedByYou == true { + strongSelf.genericCallContext?.setVolume(ssrc: presentationSsrc, volume: 0.0) + } + } + + if participant.videoDescription != nil || participant.presentationDescription != nil { + otherParticipantsWithVideo += 1 + } + + } + + if let index = updatedInvitedPeers.firstIndex(of: participant.peer.id) { + updatedInvitedPeers.remove(at: index) + didUpdateInvitedPeers = true + } + + members.participants.append(participant) + } + + members.totalCount = state.totalCount + members.loadMoreToken = state.nextParticipantsFetchOffset + + strongSelf.membersValue = members + + var stateValue = strongSelf.stateValue + + stateValue.adminIds = adminIds + + stateValue.canManageCall = state.isCreator || adminIds.contains(strongSelf.accountContext.account.peerId) + if (state.isCreator || stateValue.adminIds.contains(strongSelf.accountContext.account.peerId)) && state.defaultParticipantsAreMuted.canChange { + stateValue.defaultParticipantMuteState = state.defaultParticipantsAreMuted.isMuted ? .muted : .unmuted + } + stateValue.recordingStartTimestamp = state.recordingStartTimestamp + stateValue.title = state.title + stateValue.scheduleTimestamp = state.scheduleTimestamp + stateValue.subscribedToScheduled = state.subscribedToScheduled + stateValue.isVideoEnabled = state.isVideoEnabled && otherParticipantsWithVideo < state.unmutedVideoLimit + + + strongSelf.stateValue = stateValue + + strongSelf.summaryInfoState.set(.single(SummaryInfoState(info: GroupCallInfo( + id: callInfo.id, + accessHash: callInfo.accessHash, + participantCount: state.totalCount, + streamDcId: nil, + title: state.title, + scheduleTimestamp: state.scheduleTimestamp, + subscribedToScheduled: state.subscribedToScheduled, + recordingStartTimestamp: state.recordingStartTimestamp, + sortAscending: state.sortAscending, + defaultParticipantsAreMuted: state.defaultParticipantsAreMuted, + isVideoEnabled: state.isVideoEnabled, + unmutedVideoLimit: state.unmutedVideoLimit + )))) + + strongSelf.summaryParticipantsState.set(.single(SummaryParticipantsState( + participantCount: state.totalCount, + topParticipants: topParticipants, + activeSpeakers: activeSpeakers + ))) + + if didUpdateInvitedPeers { + strongSelf.invitedPeersValue = updatedInvitedPeers + } + })) + + let postbox = self.accountContext.account.postbox + self.memberEventsPipeDisposable.set((participantsContext.memberEvents + |> mapToSignal { event -> Signal in + return postbox.transaction { transaction -> Signal in + if let peer = transaction.getPeer(event.peerId) { + return .single(PresentationGroupCallMemberEvent(peer: peer, joined: event.joined)) + } else { + return .complete() + } + } + |> switchToLatest + } + |> deliverOnMainQueue).start(next: { [weak self] event in + guard let strongSelf = self else { + return + } + if event.peer.id == strongSelf.stateValue.myPeerId { + return + } + strongSelf.memberEventsPipe.putNext(event) + })) + + if let isCurrentlyConnecting = self.isCurrentlyConnecting, isCurrentlyConnecting { + self.startCheckingCallIfNeeded() + } + } else if case let .active(callInfo) = internalState, callInfo.scheduleTimestamp != nil { + let accountContext = self.accountContext + let peerId = self.peerId + let rawAdminIds: Signal, NoError> + if peerId.namespace == Namespaces.Peer.CloudChannel { + rawAdminIds = Signal { subscriber in + let (disposable, _) = accountContext.peerChannelMemberCategoriesContextsManager.admins(peerId: peerId, updated: { list in + var peerIds = Set() + for item in list.list { + if let adminInfo = item.participant.adminInfo, adminInfo.rights.rights.contains(.canManageCalls) { + peerIds.insert(item.peer.id) + } + } + subscriber.putNext(peerIds) + }) + return disposable + } + |> distinctUntilChanged + |> runOn(.mainQueue()) + } else { + rawAdminIds = accountContext.account.postbox.combinedView(keys: [.cachedPeerData(peerId: peerId)]) + |> map { views -> Set in + guard let view = views.views[.cachedPeerData(peerId: peerId)] as? CachedPeerDataView else { + return Set() + } + guard let cachedData = view.cachedPeerData as? CachedGroupData, let participants = cachedData.participants else { + return Set() + } + return Set(participants.participants.compactMap { item -> PeerId? in + switch item { + case .creator, .admin: + return item.peerId + default: + return nil + } + }) + } + |> distinctUntilChanged + } + + let adminIds = combineLatest(queue: .mainQueue(), + rawAdminIds, + accountContext.account.postbox.combinedView(keys: [.basicPeer(peerId)]) + ) + |> map { rawAdminIds, view -> Set in + var rawAdminIds = rawAdminIds + if let peerView = view.views[.basicPeer(peerId)] as? BasicPeerView, let peer = peerView.peer as? TelegramChannel { + if peer.hasPermission(.manageCalls) { + rawAdminIds.insert(accountContext.account.peerId) + } else { + rawAdminIds.remove(accountContext.account.peerId) + } + } + return rawAdminIds + } + |> distinctUntilChanged + + let participantsContext = accountContext.engine.calls.groupCall( + peerId: self.peerId, + myPeerId: self.joinAsPeerId, + id: callInfo.id, + accessHash: callInfo.accessHash, + state: GroupCallParticipantsContext.State( + participants: [], + nextParticipantsFetchOffset: nil, + adminIds: Set(), + isCreator: false, + defaultParticipantsAreMuted: GroupCallParticipantsContext.State.DefaultParticipantsAreMuted(isMuted: self.stateValue.defaultParticipantMuteState == .muted, canChange: false), + sortAscending: true, + recordingStartTimestamp: nil, + title: self.stateValue.title, + scheduleTimestamp: self.stateValue.scheduleTimestamp, + subscribedToScheduled: self.stateValue.subscribedToScheduled, + totalCount: 0, + isVideoEnabled: callInfo.isVideoEnabled, + unmutedVideoLimit: callInfo.unmutedVideoLimit, + version: 0 + ), + previousServiceState: nil + ) + self.temporaryParticipantsContext = nil + self.participantsContext = participantsContext + + let myPeerId = self.joinAsPeerId + let myPeer = self.accountContext.account.postbox.transaction { transaction -> (Peer, CachedPeerData?)? in + if let peer = transaction.getPeer(myPeerId) { + return (peer, transaction.getPeerCachedData(peerId: myPeerId)) + } else { + return nil + } + } + self.participantsContextStateDisposable.set(combineLatest(queue: .mainQueue(), + participantsContext.state, + adminIds, + myPeer, + accountContext.account.postbox.peerView(id: peerId) + ).start(next: { [weak self] state, adminIds, myPeerAndCachedData, view in + guard let strongSelf = self else { + return + } + + var members = PresentationGroupCallMembers( + participants: [], + speakingParticipants: Set(), + totalCount: state.totalCount, + loadMoreToken: state.nextParticipantsFetchOffset + ) + + var participants: [GroupCallParticipantsContext.Participant] = [] + var topParticipants: [GroupCallParticipantsContext.Participant] = [] + if let (myPeer, cachedData) = myPeerAndCachedData { + let about: String? + if let cachedData = cachedData as? CachedUserData { + about = cachedData.about + } else if let cachedData = cachedData as? CachedChannelData { + about = cachedData.about + } else { + about = nil + } + participants.append(GroupCallParticipantsContext.Participant( + peer: myPeer, + ssrc: nil, + videoDescription: nil, + presentationDescription: nil, + joinTimestamp: strongSelf.temporaryJoinTimestamp, + raiseHandRating: strongSelf.temporaryRaiseHandRating, + hasRaiseHand: strongSelf.temporaryHasRaiseHand, + activityTimestamp: strongSelf.temporaryActivityTimestamp, + activityRank: strongSelf.temporaryActivityRank, + muteState: strongSelf.temporaryMuteState ?? GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false), + volume: nil, + about: about, + joinedVideo: strongSelf.temporaryVideoJoined + )) + } + + for participant in participants { + members.participants.append(participant) + + if topParticipants.count < 3 { + topParticipants.append(participant) + } + } + + strongSelf.membersValue = members + + var stateValue = strongSelf.stateValue + + stateValue.adminIds = adminIds + stateValue.canManageCall = state.isCreator || adminIds.contains(strongSelf.accountContext.account.peerId) + if (state.isCreator || stateValue.adminIds.contains(strongSelf.accountContext.account.peerId)) && state.defaultParticipantsAreMuted.canChange { + stateValue.defaultParticipantMuteState = state.defaultParticipantsAreMuted.isMuted ? .muted : .unmuted + } + stateValue.recordingStartTimestamp = state.recordingStartTimestamp + + + if let activeCall = (view.cachedData as? CachedGroupData)?.activeCall { + stateValue.title = activeCall.title + } else if let activeCall = (view.cachedData as? CachedChannelData)?.activeCall { + stateValue.title = activeCall.title + } else { + stateValue.title = state.title + } + + + stateValue.scheduleTimestamp = strongSelf.isScheduledStarted ? nil : state.scheduleTimestamp + + strongSelf.stateValue = stateValue + + if state.scheduleTimestamp == nil && !strongSelf.isScheduledStarted { + strongSelf.updateSessionState(internalState: .active(GroupCallInfo(id: callInfo.id, accessHash: callInfo.accessHash, participantCount: state.totalCount, streamDcId: callInfo.streamDcId, title: state.title, scheduleTimestamp: nil, subscribedToScheduled: false, recordingStartTimestamp: nil, sortAscending: true, defaultParticipantsAreMuted: callInfo.defaultParticipantsAreMuted ?? state.defaultParticipantsAreMuted, isVideoEnabled: callInfo.isVideoEnabled, unmutedVideoLimit: callInfo.unmutedVideoLimit))) + } else if !strongSelf.isScheduledStarted { + strongSelf.summaryInfoState.set(.single(SummaryInfoState(info: GroupCallInfo( + id: callInfo.id, + accessHash: callInfo.accessHash, + participantCount: state.totalCount, + streamDcId: nil, + title: state.title, + scheduleTimestamp: state.scheduleTimestamp, + subscribedToScheduled: state.subscribedToScheduled, + recordingStartTimestamp: state.recordingStartTimestamp, + sortAscending: state.sortAscending, + defaultParticipantsAreMuted: state.defaultParticipantsAreMuted, + isVideoEnabled: state.isVideoEnabled, + unmutedVideoLimit: state.unmutedVideoLimit + )))) + + strongSelf.summaryParticipantsState.set(.single(SummaryParticipantsState( + participantCount: state.totalCount, + topParticipants: topParticipants, + activeSpeakers: Set() + ))) + } + })) + } + + } + } + + + private func requestMediaChannelDescriptions(ssrcs: Set, completion: @escaping ([OngoingGroupCallContext.MediaChannelDescription]) -> Void) -> Disposable { + func extractMediaChannelDescriptions(remainingSsrcs: inout Set, participants: [GroupCallParticipantsContext.Participant], into result: inout [OngoingGroupCallContext.MediaChannelDescription]) { + for participant in participants { + guard let audioSsrc = participant.ssrc else { + continue + } + + if remainingSsrcs.contains(audioSsrc) { + remainingSsrcs.remove(audioSsrc) + + result.append(OngoingGroupCallContext.MediaChannelDescription( + kind: .audio, + audioSsrc: audioSsrc, + videoDescription: nil + )) + } + + if let screencastSsrc = participant.presentationDescription?.audioSsrc { + if remainingSsrcs.contains(screencastSsrc) { + remainingSsrcs.remove(screencastSsrc) + + result.append(OngoingGroupCallContext.MediaChannelDescription( + kind: .audio, + audioSsrc: screencastSsrc, + videoDescription: nil + )) + } + } + } + } + + var remainingSsrcs = ssrcs + var result: [OngoingGroupCallContext.MediaChannelDescription] = [] + + if let membersValue = self.membersValue { + extractMediaChannelDescriptions(remainingSsrcs: &remainingSsrcs, participants: membersValue.participants, into: &result) + } + + if !remainingSsrcs.isEmpty, let callInfo = self.internalState.callInfo { + return (accountContext.engine.calls.getGroupCallParticipants(callId: callInfo.id, accessHash: callInfo.accessHash, offset: "", ssrcs: Array(remainingSsrcs), limit: 100, sortAscending: callInfo.sortAscending) + |> deliverOnMainQueue).start(next: { state in + extractMediaChannelDescriptions(remainingSsrcs: &remainingSsrcs, participants: state.participants, into: &result) + + completion(result) + }) + } else { + completion(result) + return EmptyDisposable + } + } + + + private func startCheckingCallIfNeeded() { + if self.checkCallDisposable != nil { + return + } + if case let .established(callInfo, connectionMode, _, ssrc, _) = self.internalState, case .rtc = connectionMode { + let checkSignal = accountContext.engine.calls.checkGroupCall(callId: callInfo.id, accessHash: callInfo.accessHash, ssrcs: [ssrc]) + + self.checkCallDisposable = (( + checkSignal + |> castError(Bool.self) + |> delay(4.0, queue: .mainQueue()) + |> mapToSignal { result -> Signal in + var foundAll = true + for value in [ssrc] { + if !result.contains(value) { + foundAll = false + break + } + } + if foundAll { + return .fail(true) + } else { + return .single(true) + } + } + ) + |> restartIfError + |> take(1) + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.checkCallDisposable = nil + strongSelf.requestCall(movingFromBroadcastToRtc: false) + }) + } + } + + private func markAsCanBeRemoved() { + if self.markedAsCanBeRemoved { + return + } + self.markedAsCanBeRemoved = true + + self.genericCallContext?.stop() + self.screencastCallContext?.stop() + self._canBeRemoved.set(.single(true)) + + if self.didConnectOnce { + } + } + + func joinAsSpeakerIfNeeded(_ joinHash: String) { + self.invite = joinHash + if let muteState = self.stateValue.muteState, !muteState.canUnmute { + requestCall(movingFromBroadcastToRtc: true) + } + } + func resetListenerLink() { + self.participantsContext?.resetInviteLinks() + } + + func reconnect(as peerId: PeerId) { + if peerId == self.joinAsPeerId { + return + } + + if self.stateValue.scheduleTimestamp != nil { + updateGroupCallJoinAsDisposable.set(accountContext.engine.calls.updateGroupCallJoinAsPeer(peerId: self.peerId, joinAs: peerId).start()) + } + + let _ = (self.accountContext.account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peerId) + } + |> deliverOnMainQueue).start(next: { [weak self] myPeer in + guard let strongSelf = self, let _ = myPeer else { + return + } + + strongSelf.reconnectingAsPeer = myPeer + + let previousPeerId = strongSelf.joinAsPeerId + if let localSsrc = strongSelf.currentLocalSsrc { + strongSelf.ignorePreviousJoinAsPeerId = (previousPeerId, localSsrc) + } + strongSelf.joinAsPeerId = peerId + + if let participantsContext = strongSelf.participantsContext, let immediateState = participantsContext.immediateState { + for participant in immediateState.participants { + if participant.peer.id == previousPeerId { + strongSelf.temporaryJoinTimestamp = participant.joinTimestamp + strongSelf.temporaryActivityTimestamp = participant.activityTimestamp + strongSelf.temporaryVideoJoined = participant.joinedVideo + strongSelf.temporaryActivityRank = participant.activityRank + strongSelf.temporaryRaiseHandRating = participant.raiseHandRating + strongSelf.temporaryHasRaiseHand = participant.hasRaiseHand + strongSelf.temporaryMuteState = participant.muteState + } + } + strongSelf.switchToTemporaryParticipantsContext(sourceContext: participantsContext, oldMyPeerId: previousPeerId) + } else { + strongSelf.stateValue.myPeerId = peerId + } + + strongSelf.requestCall(movingFromBroadcastToRtc: false) + }) + } + + func leave(terminateIfPossible: Bool) -> Signal { + self.leaving = true + if let callInfo = self.internalState.callInfo, let localSsrc = self.currentLocalSsrc { + if terminateIfPossible { + self.leaveDisposable.set((accountContext.engine.calls.stopGroupCall(peerId: self.peerId, callId: callInfo.id, accessHash: callInfo.accessHash) + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.markAsCanBeRemoved() + })) + } else { + let contexts = self.accountContext.cachedGroupCallContexts + let accountContext = self.accountContext + let id = callInfo.id + let accessHash = callInfo.accessHash + let source = localSsrc + contexts.impl.with { impl in + impl.leaveInBackground(context: accountContext, id: id, accessHash: accessHash, source: source) + } + self.markAsCanBeRemoved() + } + } else if let callInfo = self.initialCall, terminateIfPossible { + self.leaveDisposable.set((accountContext.engine.calls.stopGroupCall(peerId: self.peerId, callId: callInfo.id, accessHash: callInfo.accessHash) + |> deliverOnMainQueue).start(completed: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.markAsCanBeRemoved() + })) + } else { + self.markAsCanBeRemoved() + } + return self._canBeRemoved.get() + } + + func toggleIsMuted() { + + if stateValue.networkState == .connecting || stateValue.scheduleTimestamp != nil { + return + } + + switch self.isMutedValue { + case .muted: + self.setIsMuted(action: .unmuted) + case .unmuted: + self.setIsMuted(action: .muted(isPushToTalkActive: false)) + } + } + + func setIsMuted(action: PresentationGroupCallMuteAction) { + self.permissions(action, { [weak self] permission in + guard let `self` = self else { + return + } + if !permission { + return + } + if self.isMutedValue == action { + return + } + if let muteState = self.stateValue.muteState, !muteState.canUnmute { + return + } + self.isMutedValue = action + self.isMutedPromise.set(self.isMutedValue) + let isEffectivelyMuted: Bool + let isVisuallyMuted: Bool + switch self.isMutedValue { + case let .muted(isPushToTalkActive): + isEffectivelyMuted = !isPushToTalkActive + isVisuallyMuted = true + let _ = self.updateMuteState(peerId: self.joinAsPeerId, isMuted: true) + case .unmuted: + isEffectivelyMuted = false + isVisuallyMuted = false + let _ = self.updateMuteState(peerId: self.joinAsPeerId, isMuted: false) + } + self.genericCallContext?.setIsMuted(isEffectivelyMuted) + + if isVisuallyMuted { + self.stateValue.muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false) + } else { + self.stateValue.muteState = nil + } + }) + } + + func raiseHand() { + guard let membersValue = self.membersValue else { + return + } + for participant in membersValue.participants { + if participant.peer.id == self.joinAsPeerId { + if participant.hasRaiseHand { + return + } + break + } + } + + self.participantsContext?.raiseHand() + } + + func lowerHand() { + guard let membersValue = self.membersValue else { + return + } + for participant in membersValue.participants { + if participant.peer.id == self.joinAsPeerId { + if !participant.hasRaiseHand { + return + } + break + } + } + + self.participantsContext?.lowerHand() + } + + var mustStopSharing:(()->Void)? + var mustStopVideo:(()->Void)? + + public func requestScreencast(deviceId: String) { + if self.screencastCallContext != nil { + return + } + + let maybeCallInfo: GroupCallInfo? = self.internalState.callInfo + + guard let callInfo = maybeCallInfo else { + return + } + + if self.screenCapturer == nil { + let screenCapturer = OngoingCallVideoCapturer(deviceId) + self.screenCapturer = screenCapturer + } + + self.screenCapturer?.setOnFatalError({ [weak self] in + self?.mustStopSharing?() + }) + + self.screenCapturer?.setOnPause({ [weak self] paused in + guard let strongSelf = self else { + return + } + strongSelf.participantsContext?.updateVideoState(peerId: strongSelf.joinAsPeerId, isVideoMuted: nil, isVideoPaused: false, isPresentationPaused: paused) + }) + + let screencastCallContext = OngoingGroupCallContext( + video: self.screenCapturer, + requestMediaChannelDescriptions: { _, completion in + completion([]) + return EmptyDisposable + }, + audioStreamData: nil, + rejoinNeeded: {}, + outgoingAudioBitrateKbit: nil, + videoContentType: .screencast, + enableNoiseSuppression: false + ) + + self.screencastCallContext = screencastCallContext + self.hasScreencast = true + + + self.screencastJoinDisposable.set((screencastCallContext.joinPayload + |> distinctUntilChanged(isEqual: { lhs, rhs in + if lhs.0 != rhs.0 { + return false + } + if lhs.1 != rhs.1 { + return false + } + return true + }) + |> deliverOnMainQueue).start(next: { [weak self] joinPayload, _ in + guard let strongSelf = self else { + return + } + + strongSelf.requestDisposable.set((strongSelf.accountContext.engine.calls.joinGroupCallAsScreencast( + peerId: strongSelf.peerId, + callId: callInfo.id, + accessHash: callInfo.accessHash, + joinPayload: joinPayload + ) + |> deliverOnMainQueue).start(next: { joinCallResult in + guard let strongSelf = self, let screencastCallContext = strongSelf.screencastCallContext else { + return + } + let clientParams = joinCallResult.jsonParams + + screencastCallContext.setConnectionMode(.rtc, keepBroadcastConnectedIfWasEnabled: false) + screencastCallContext.setJoinResponse(payload: clientParams) + + strongSelf.screencastEndpointId = joinCallResult.endpointId + + }, error: { error in + guard let _ = self else { + return + } + })) + })) + + } + + public func disableScreencast() { + self.hasScreencast = false + + self.screencastEndpointId = nil + if let screencastCallContext = self.screencastCallContext { + self.screencastCallContext = nil + screencastCallContext.stop() + + let maybeCallInfo: GroupCallInfo? = self.internalState.callInfo + + if let callInfo = maybeCallInfo { + self.screencastJoinDisposable.set(accountContext.engine.calls.leaveGroupCallAsScreencast( + callId: callInfo.id, + accessHash: callInfo.accessHash + ).start()) + } + } + if let _ = self.screenCapturer { + self.screenCapturer = nil + self.screencastCallContext?.disableVideo() + } + } + + + + public func requestVideo(deviceId: String) { + if self.videoCapturer == nil { + let videoCapturer = OngoingCallVideoCapturer(deviceId) + self.videoCapturer = videoCapturer + } + + self.videoCapturer?.setOnFatalError({ [weak self] in + self?.mustStopVideo?() + }) + self.hasVideo = true + if let videoCapturer = self.videoCapturer { + self.genericCallContext?.requestVideo(videoCapturer) + self.participantsContext?.updateVideoState(peerId: self.joinAsPeerId, isVideoMuted: false, isVideoPaused: false, isPresentationPaused: nil) + } + } + + public func disableVideo() { + self.hasVideo = false + if let _ = self.videoCapturer { + self.videoCapturer = nil + self.genericCallContext?.disableVideo() + self.participantsContext?.updateVideoState(peerId: self.joinAsPeerId, isVideoMuted: true, isVideoPaused: false, isPresentationPaused: nil) + } + } + + + + public func setVolume(peerId: PeerId, volume: Int32, sync: Bool) { + var found = false + for (ssrc, mapping) in self.ssrcMapping { + if mapping.peerId == peerId { + self.genericCallContext?.setVolume(ssrc: ssrc, volume: Double(volume) / 10000.0) + found = true + } + } + if found && sync { + self.participantsContext?.updateMuteState(peerId: peerId, muteState: nil, volume: volume, raiseHand: nil) + } + } + + public func setRequestedVideoList(items: [PresentationGroupCallRequestedVideo]) { + self.genericCallContext?.setRequestedVideoChannels(items.compactMap { item -> OngoingGroupCallContext.VideoChannel in + let mappedMinQuality: OngoingGroupCallContext.VideoChannel.Quality + let mappedMaxQuality: OngoingGroupCallContext.VideoChannel.Quality + switch item.minQuality { + case .thumbnail: + mappedMinQuality = .thumbnail + case .medium: + mappedMinQuality = .medium + case .full: + mappedMinQuality = .full + } + switch item.maxQuality { + case .thumbnail: + mappedMaxQuality = .thumbnail + case .medium: + mappedMaxQuality = .medium + case .full: + mappedMaxQuality = .full + } + return OngoingGroupCallContext.VideoChannel( + audioSsrc: item.audioSsrc, + endpointId: item.endpointId, + ssrcGroups: item.ssrcGroups.map { group in + return OngoingGroupCallContext.VideoChannel.SsrcGroup(semantics: group.semantics, ssrcs: group.ssrcs) + }, + minQuality: mappedMinQuality, + maxQuality: mappedMaxQuality + ) + }) + } + + + + public func updateMuteState(peerId: PeerId, isMuted: Bool) -> GroupCallParticipantsContext.Participant.MuteState? { + let canThenUnmute: Bool + if isMuted { + var mutedByYou = false + if peerId == self.joinAsPeerId { + canThenUnmute = true + } else if self.stateValue.canManageCall { + if self.stateValue.adminIds.contains(peerId) { + canThenUnmute = true + } else { + canThenUnmute = false + } + } else if self.stateValue.adminIds.contains(self.accountContext.account.peerId) { + canThenUnmute = true + } else { + self.setVolume(peerId: peerId, volume: 0, sync: false) + mutedByYou = true + canThenUnmute = true + } + let muteState = isMuted ? GroupCallParticipantsContext.Participant.MuteState(canUnmute: canThenUnmute, mutedByYou: mutedByYou) : nil + self.participantsContext?.updateMuteState(peerId: peerId, muteState: muteState, volume: nil, raiseHand: nil) + return muteState + } else { + if peerId == self.joinAsPeerId { + self.participantsContext?.updateMuteState(peerId: peerId, muteState: nil, volume: nil, raiseHand: nil) + return nil + } else if self.stateValue.canManageCall || self.stateValue.adminIds.contains(self.accountContext.account.peerId) { + let muteState = GroupCallParticipantsContext.Participant.MuteState(canUnmute: true, mutedByYou: false) + self.participantsContext?.updateMuteState(peerId: peerId, muteState: muteState, volume: nil, raiseHand: nil) + return muteState + } else { + self.setVolume(peerId: peerId, volume: 10000, sync: true) + self.participantsContext?.updateMuteState(peerId: peerId, muteState: nil, volume: nil, raiseHand: nil) + return nil + } + } + } + + func setShouldBeRecording(_ shouldBeRecording: Bool, title: String?, videoOrientation: Bool?) { + if !self.stateValue.canManageCall { + return + } + if (self.stateValue.recordingStartTimestamp != nil) == shouldBeRecording { + return + } + self.participantsContext?.updateShouldBeRecording(shouldBeRecording, title: title, videoOrientation: videoOrientation) + } + + private func requestCall(movingFromBroadcastToRtc: Bool) { + self.currentConnectionMode = .none + self.genericCallContext?.setConnectionMode(.none, keepBroadcastConnectedIfWasEnabled: movingFromBroadcastToRtc) + + self.internalState = .requesting + self.internalStatePromise.set(.single(.requesting)) + self.isCurrentlyConnecting = nil + + enum CallError { + case generic + } + + let account = self.account + + let currentCall: Signal + if let initialCall = self.initialCall { + currentCall = accountContext.engine.calls.getCurrentGroupCall(callId: initialCall.id, accessHash: initialCall.accessHash, peerId: peerId) + |> mapError { _ -> CallError in + return .generic + } + |> map { summary -> GroupCallInfo? in + return summary?.info + } + } else { + currentCall = .single(nil) + } + + let currentOrRequestedCall = currentCall + |> mapToSignal { callInfo -> Signal in + if let callInfo = callInfo { + return .single(callInfo) + } else { + return .single(nil) + } + } + + self.networkStateDisposable.set(nil) + self.joinDisposable.set(nil) + + self.checkCallDisposable?.dispose() + self.checkCallDisposable = nil + + if movingFromBroadcastToRtc { + self.stateValue.networkState = .connected + } else { + self.stateValue.networkState = .connecting + } + + self.requestDisposable.set((currentOrRequestedCall + |> deliverOnMainQueue).start(next: { [weak self] value in + guard let strongSelf = self else { + return + } + + if let value = value { + strongSelf.initialCall = CachedChannelData.ActiveCall(id: value.id, accessHash: value.accessHash, title: value.title, scheduleTimestamp: value.scheduleTimestamp, subscribedToScheduled: value.subscribedToScheduled) + strongSelf.updateSessionState(internalState: .active(value)) + } else { + strongSelf.markAsCanBeRemoved() + } + })) + } + + func invitePeer(_ peerId: PeerId) -> Bool { + guard case let .established(callInfo, _, _, _, _) = self.internalState, !self.invitedPeersValue.contains(peerId) else { + return false + } + if let channel = self.peer as? TelegramChannel { + if channel.isChannel { + return false + } + } + var updatedInvitedPeers = self.invitedPeersValue + updatedInvitedPeers.insert(peerId, at: 0) + self.invitedPeersValue = updatedInvitedPeers + + let _ = accountContext.engine.calls.inviteToGroupCall(callId: callInfo.id, accessHash: callInfo.accessHash, peerId: peerId).start() + + return true + } + + func removedPeer(_ peerId: PeerId) { + var updatedInvitedPeers = self.invitedPeersValue + updatedInvitedPeers.removeAll(where: { $0 == peerId}) + self.invitedPeersValue = updatedInvitedPeers + } + + func updateTitle(_ title: String, force: Bool) { + guard let callInfo = self.internalState.callInfo else { + return + } + self.stateValue.title = title.isEmpty ? nil : title + + var signal = accountContext.engine.calls.editGroupCallTitle(callId: callInfo.id, accessHash: callInfo.accessHash, title: title) + if !force { + signal = signal |> delay(0.2, queue: .mainQueue()) + } + updateTitleDisposable.set(signal.start()) + } + + var inviteLinks: Signal { + let accountContext = self.accountContext + let internalStatePromise = self.internalStatePromise + return self.state |> take(1) + |> map { state -> PeerId in + return state.myPeerId + } + |> distinctUntilChanged + |> mapToSignal { _ -> Signal in + return internalStatePromise.get() + |> filter { state -> Bool in + if case .requesting = state { + return false + } else { + return true + } + } |> take(1) + |> mapToSignal { state in + if let callInfo = state.callInfo { + return accountContext.engine.calls.groupCallInviteLinks(callId: callInfo.id, accessHash: callInfo.accessHash) + } else { + return .complete() + } + } + } + } + + private var currentMyAudioLevel: Float = 0.0 + private var currentMyAudioLevelTimestamp: Double = 0.0 + private var isSendingTyping: Bool = false + + + private func restartMyAudioLevelTimer() { + self.myAudioLevelTimer?.invalidate() + let myAudioLevelTimer = SwiftSignalKit.Timer(timeout: 0.1, repeat: false, completion: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.myAudioLevelTimer = nil + + let timestamp = CACurrentMediaTime() + + var shouldBeSendingTyping = false + if strongSelf.currentMyAudioLevel > 0.01 && timestamp < strongSelf.currentMyAudioLevelTimestamp + 1.0 { + strongSelf.restartMyAudioLevelTimer() + shouldBeSendingTyping = true + } else { + if timestamp < strongSelf.currentMyAudioLevelTimestamp + 1.0 { + strongSelf.restartMyAudioLevelTimer() + shouldBeSendingTyping = true + } + } + if shouldBeSendingTyping != strongSelf.isSendingTyping { + strongSelf.isSendingTyping = shouldBeSendingTyping + if shouldBeSendingTyping { + strongSelf.typingDisposable.set(strongSelf.accountContext.account.acquireLocalInputActivity(peerId: PeerActivitySpace(peerId: strongSelf.peerId, category: .voiceChat), activity: .speakingInGroupCall(timestamp: 0))) + strongSelf.restartMyAudioLevelTimer() + } else { + strongSelf.typingDisposable.set(nil) + } + } + }, queue: .mainQueue()) + self.myAudioLevelTimer = myAudioLevelTimer + myAudioLevelTimer.start() + } + + private func processMyAudioLevel(level: Float, hasVoice: Bool) { + self.currentMyAudioLevel = level + + if level > 0.01 && hasVoice { + self.currentMyAudioLevelTimestamp = CACurrentMediaTime() + + if self.myAudioLevelTimer == nil { + self.restartMyAudioLevelTimer() + } + } + } + + func updateDefaultParticipantsAreMuted(isMuted: Bool) { + self.participantsContext?.updateDefaultParticipantsAreMuted(isMuted: isMuted) + } + + func switchVideoInput(_ deviceId: String) { + videoCapturer?.switchVideoInput(deviceId) + } + + func makeVideoView(endpointId: String, videoMode: GroupCallVideoMode, completion: @escaping (PresentationCallVideoView?) -> Void) { + let context: OngoingGroupCallContext? + switch videoMode { + case .video: + context = self.genericCallContext + case .screencast: + context = self.screencastCallContext + } + context?.makeIncomingVideoView(endpointId: endpointId, requestClone: false, completion: { view, _ in + if let view = view { + let setOnFirstFrameReceived = view.setOnFirstFrameReceived + let setOnOrientationUpdated = view.setOnOrientationUpdated + let setOnIsMirroredUpdated = view.setOnIsMirroredUpdated + completion(PresentationCallVideoView( + holder: view, + view: view.view, + setOnFirstFrameReceived: { f in + setOnFirstFrameReceived(f) + + }, + getOrientation: { [weak view] in + if let view = view { + let mappedValue: PresentationCallVideoView.Orientation + switch view.getOrientation() { + case .rotation0: + mappedValue = .rotation0 + case .rotation90: + mappedValue = .rotation90 + case .rotation180: + mappedValue = .rotation180 + case .rotation270: + mappedValue = .rotation270 + } + return mappedValue + } else { + return .rotation0 + } + }, + getAspect: { [weak view] in + if let view = view { + return view.getAspect() + } else { + return 0.0 + } + }, setVideoContentMode: { [weak view] mode in + view?.setVideoContentMode(mode) + }, + setOnOrientationUpdated: { f in + setOnOrientationUpdated { value, aspect in + let mappedValue: PresentationCallVideoView.Orientation + switch value { + case .rotation0: + mappedValue = .rotation0 + case .rotation90: + mappedValue = .rotation90 + case .rotation180: + mappedValue = .rotation180 + case .rotation270: + mappedValue = .rotation270 + } + f?(mappedValue, aspect) + } + }, + setOnIsMirroredUpdated: { f in + setOnIsMirroredUpdated { value in + f?(value) + } + }, setIsPaused: { [weak view] paused in + view?.setIsPaused(paused) + }, renderToSize: { [weak view] size, animated in + view?.renderToSize(size, animated) + } + )) + } else { + completion(nil) + } + }) + } + + func loadMore() { + if let token = self.membersValue?.loadMoreToken { + self.participantsContext?.loadMore(token: token) + } + } + public func startScheduled() { + guard case let .active(callInfo) = self.internalState else { + return + } + self.isScheduledStarted = true + self.stateValue.scheduleTimestamp = nil + + self.startDisposable.set((accountContext.engine.calls.startScheduledGroupCall(peerId: self.peerId, callId: callInfo.id, accessHash: callInfo.accessHash) + |> deliverOnMainQueue).start(next: { [weak self] callInfo in + guard let strongSelf = self else { + return + } + strongSelf.updateSessionState(internalState: .active(callInfo)) + })) + } + + public func toggleScheduledSubscription(_ subscribe: Bool) { + guard case let .active(callInfo) = self.internalState, callInfo.scheduleTimestamp != nil else { + return + } + + self.stateValue.subscribedToScheduled = subscribe + + self.subscribeDisposable.set((accountContext.engine.calls.toggleScheduledGroupCallSubscription(peerId: self.peerId, callId: callInfo.id, accessHash: callInfo.accessHash, subscribe: subscribe) + |> deliverOnMainQueue).start()) + } + + +} + + +func requestOrJoinGroupCall(context: AccountContext, peerId: PeerId, joinAs: PeerId, initialCall: CachedChannelData.ActiveCall?, initialInfo: GroupCallInfo? = nil, joinHash: String? = nil) -> Signal { + let sharedContext = context.sharedContext + let accounts = context.sharedContext.activeAccounts |> take(1) + let account = context.account + + return combineLatest(queue: .mainQueue(), accounts, account.postbox.loadedPeerWithId(peerId)) |> mapToSignal { accounts, peer in + if let context = sharedContext.bindings.groupCall(), context.call.peerId == peerId, context.call.account.id == account.id { + return .single(.samePeer(context)) + } else { + return makeNewCallConfirmation(account: account, sharedContext: sharedContext, newPeerId: peerId, newCallType: .voiceChat) + |> mapToSignal { _ in + return sharedContext.endCurrentCall() + } |> map { _ in + let call: CachedChannelData.ActiveCall? + if let info = initialInfo { + call = .init(id: info.id, accessHash: info.accessHash, title: info.title, scheduleTimestamp: info.scheduleTimestamp, subscribedToScheduled: info.subscribedToScheduled) + } else { + call = initialCall + } + return .success(startGroupCall(context: context, peerId: peerId, joinAs: joinAs, initialCall: call, initialInfo: initialInfo, joinHash: joinHash, peer: peer)) + } + } + } + +} + + +private func startGroupCall(context: AccountContext, peerId: PeerId, joinAs: PeerId, initialCall: CachedChannelData.ActiveCall?, initialInfo: GroupCallInfo? = nil, internalId: CallSessionInternalId = CallSessionInternalId(), joinHash: String? = nil, peer: Peer? = nil) -> GroupCallContext { + + + + return GroupCallContext(call: PresentationGroupCallImpl(accountContext: context, initialCall: initialCall, internalId: internalId, peerId: peerId, invite: joinHash, joinAsPeerId: joinAs, initialInfo: initialInfo), peerMemberContextsManager: context.peerChannelMemberCategoriesContextsManager) +} + +func createVoiceChat(context: AccountContext, peerId: PeerId, displayAsList: [FoundPeer]? = nil, canBeScheduled: Bool = false) { + let confirmation = makeNewCallConfirmation(account: context.account, sharedContext: context.sharedContext, newPeerId: peerId, newCallType: .voiceChat) |> mapToSignalPromotingError { _ in + return Signal<(GroupCallInfo?, PeerId), CreateGroupCallError> { subscriber in + + let disposable = MetaDisposable() + + let create:(PeerId, Date?)->Void = { joinAs, schedule in + let scheduleDate: Int32? + if let timeInterval = schedule?.timeIntervalSince1970 { + scheduleDate = Int32(timeInterval) + } else { + scheduleDate = nil + } + disposable.set(context.engine.calls.createGroupCall(peerId: peerId, title: nil, scheduleDate: scheduleDate).start(next: { info in + subscriber.putNext((info, joinAs)) + subscriber.putCompletion() + }, error: { error in + subscriber.putError(error) + })) + } + if let displayAsList = displayAsList { + if !displayAsList.isEmpty || canBeScheduled { + showModal(with: GroupCallDisplayAsController(context: context, mode: .create, peerId: peerId, list: displayAsList, completion: create, canBeScheduled: canBeScheduled), for: context.window) + } else { + create(context.peerId, nil) + } + } else { + selectGroupCallJoiner(context: context, peerId: peerId, completion: create, canBeScheduled: canBeScheduled) + } + + return ActionDisposable { + disposable.dispose() + } + } |> runOn(.mainQueue()) + } + + let requestCall: Signal = confirmation |> mapToSignal { call, joinAs in + + let initialCall: CachedChannelData.ActiveCall? + if let call = call { + initialCall = .init(id: call.id, accessHash: call.accessHash, title: call.title, scheduleTimestamp: call.scheduleTimestamp, subscribedToScheduled: call.subscribedToScheduled) + } else { + initialCall = nil + } + + return showModalProgress(signal: requestOrJoinGroupCall(context: context, peerId: peerId, joinAs: joinAs, initialCall: initialCall) |> mapError { _ in .generic }, for: context.window) + } |> deliverOnMainQueue + + _ = requestCall.start(next: { result in + switch result { + case let .success(callContext), let .samePeer(callContext): + applyGroupCallResult(context.sharedContext, callContext) + default: + alert(for: context.window, info: L10n.errorAnError) + } + }, error: { error in + if case .anonymousNotAllowed = error { + alert(for: context.window, info: L10n.voiceChatAnonymousDisabledAlertText) + } else { + alert(for: context.window, info: L10n.errorAnError) + } + }) +} diff --git a/Telegram-Mac/PresentationGroupCallManager.swift b/Telegram-Mac/PresentationGroupCallManager.swift new file mode 100644 index 0000000000..edf04e5876 --- /dev/null +++ b/Telegram-Mac/PresentationGroupCallManager.swift @@ -0,0 +1,318 @@ +import Foundation +import Postbox +import TelegramCore + +import SwiftSignalKit + + +public struct PresentationGroupCallRequestedVideo { + public enum Quality { + case thumbnail + case medium + case full + } + + public struct SsrcGroup { + public var semantics: String + public var ssrcs: [UInt32] + } + + public var audioSsrc: UInt32 + public var endpointId: String + public var ssrcGroups: [SsrcGroup] + public var minQuality: Quality + public var maxQuality: Quality +} + +public extension GroupCallParticipantsContext.Participant { + var videoEndpointId: String? { + return self.videoDescription?.endpointId + } + + var presentationEndpointId: String? { + return self.presentationDescription?.endpointId + } +} + +extension GroupCallParticipantsContext.Participant { + func requestedVideoChannel(minQuality: PresentationGroupCallRequestedVideo.Quality, maxQuality: PresentationGroupCallRequestedVideo.Quality) -> PresentationGroupCallRequestedVideo? { + guard let audioSsrc = self.ssrc else { + return nil + } + guard let videoDescription = self.videoDescription else { + return nil + } + return PresentationGroupCallRequestedVideo(audioSsrc: audioSsrc, endpointId: videoDescription.endpointId, ssrcGroups: videoDescription.ssrcGroups.map { group in + PresentationGroupCallRequestedVideo.SsrcGroup(semantics: group.semantics, ssrcs: group.ssrcs) + }, minQuality: minQuality, maxQuality: maxQuality) + } + + func requestedPresentationVideoChannel(minQuality: PresentationGroupCallRequestedVideo.Quality, maxQuality: PresentationGroupCallRequestedVideo.Quality) -> PresentationGroupCallRequestedVideo? { + guard let audioSsrc = self.ssrc else { + return nil + } + guard let presentationDescription = self.presentationDescription else { + return nil + } + return PresentationGroupCallRequestedVideo(audioSsrc: audioSsrc, endpointId: presentationDescription.endpointId, ssrcGroups: presentationDescription.ssrcGroups.map { group in + PresentationGroupCallRequestedVideo.SsrcGroup(semantics: group.semantics, ssrcs: group.ssrcs) + }, minQuality: minQuality, maxQuality: maxQuality) + } +} + + +final class PresentationCallVideoView { + public enum Orientation { + case rotation0 + case rotation90 + case rotation180 + case rotation270 + } + + public let holder: AnyObject + public let view: NSView + public let setOnFirstFrameReceived: (((Float) -> Void)?) -> Void + + public let getOrientation: () -> Orientation + public let getAspect: () -> CGFloat + public let setOnOrientationUpdated: (((Orientation, CGFloat) -> Void)?) -> Void + public let setVideoContentMode:(CALayerContentsGravity)->Void + public let setOnIsMirroredUpdated: (((Bool) -> Void)?) -> Void + public let setIsPaused: (Bool) -> Void + public let renderToSize: (NSSize, Bool) -> Void + + public init( + holder: AnyObject, + view: NSView, + setOnFirstFrameReceived: @escaping (((Float) -> Void)?) -> Void, + getOrientation: @escaping () -> Orientation, + getAspect: @escaping () -> CGFloat, + setVideoContentMode:@escaping(CALayerContentsGravity)->Void, + setOnOrientationUpdated: @escaping (((Orientation, CGFloat) -> Void)?) -> Void, + setOnIsMirroredUpdated: @escaping (((Bool) -> Void)?) -> Void, + setIsPaused: @escaping(Bool)->Void, + renderToSize: @escaping(NSSize, Bool) -> Void + ) { + self.holder = holder + self.view = view + self.setOnFirstFrameReceived = setOnFirstFrameReceived + self.getOrientation = getOrientation + self.getAspect = getAspect + self.setOnOrientationUpdated = setOnOrientationUpdated + self.setOnIsMirroredUpdated = setOnIsMirroredUpdated + self.setVideoContentMode = setVideoContentMode + self.setIsPaused = setIsPaused + self.renderToSize = renderToSize + } +} + + +struct PresentationGroupCallSummaryState: Equatable { + var info: GroupCallInfo? + var participantCount: Int + var callState: PresentationGroupCallState + var topParticipants: [GroupCallParticipantsContext.Participant] + var activeSpeakers: Set + init( + info: GroupCallInfo?, + participantCount: Int, + callState: PresentationGroupCallState, + topParticipants: [GroupCallParticipantsContext.Participant], + activeSpeakers: Set + ) { + self.info = info + self.participantCount = participantCount + self.callState = callState + self.topParticipants = topParticipants + self.activeSpeakers = activeSpeakers + } +} + + + +enum RequestOrJoinGroupCallResult { + case success(GroupCallContext) + case fail + case samePeer(GroupCallContext) +} + +public enum PresentationGroupCallMuteAction: Equatable { + case muted(isPushToTalkActive: Bool) + case unmuted + + var isEffectivelyMuted: Bool { + switch self { + case let .muted(isPushToTalkActive): + return !isPushToTalkActive + case .unmuted: + return false + } + } + +} + +public struct PresentationGroupCallState: Equatable { + public enum NetworkState { + case connecting + case connected + } + + public enum DefaultParticipantMuteState { + case unmuted + case muted + } + + public struct ScheduleState : Equatable { + var date: Int32 + var subscribed: Bool + } + + public var myPeerId: PeerId + public var networkState: NetworkState + public var canManageCall: Bool + public var adminIds: Set + public var muteState: GroupCallParticipantsContext.Participant.MuteState? + public var defaultParticipantMuteState: DefaultParticipantMuteState? + public var recordingStartTimestamp: Int32? + public var title: String? + public var raisedHand: Bool + public var scheduleTimestamp: Int32? + public var subscribedToScheduled: Bool + public var isVideoEnabled: Bool + public init( + myPeerId: PeerId, + networkState: NetworkState, + canManageCall: Bool, + adminIds: Set, + muteState: GroupCallParticipantsContext.Participant.MuteState?, + defaultParticipantMuteState: DefaultParticipantMuteState?, + recordingStartTimestamp: Int32?, + title: String?, + raisedHand: Bool, + scheduleTimestamp: Int32?, + subscribedToScheduled: Bool, + isVideoEnabled: Bool + ) { + self.myPeerId = myPeerId + self.networkState = networkState + self.canManageCall = canManageCall + self.adminIds = adminIds + self.muteState = muteState + self.defaultParticipantMuteState = defaultParticipantMuteState + self.recordingStartTimestamp = recordingStartTimestamp + self.title = title + self.raisedHand = raisedHand + self.scheduleTimestamp = scheduleTimestamp + self.subscribedToScheduled = subscribedToScheduled + self.isVideoEnabled = isVideoEnabled + } + + var scheduleState: ScheduleState? { + if let scheduleTimestamp = scheduleTimestamp { + return .init(date: scheduleTimestamp, subscribed: subscribedToScheduled) + } else { + return nil + } + } +} +final class PresentationGroupCallMemberEvent { + let peer: Peer + let joined: Bool + + init(peer: Peer, joined: Bool) { + self.peer = peer + self.joined = joined + } +} + + + + +struct PresentationGroupCallMembers: Equatable { + public var participants: [GroupCallParticipantsContext.Participant] + public var speakingParticipants: Set + public var totalCount: Int + public var loadMoreToken: String? + + public init( + participants: [GroupCallParticipantsContext.Participant], + speakingParticipants: Set, + totalCount: Int, + loadMoreToken: String? + ) { + self.participants = participants + self.speakingParticipants = speakingParticipants + self.totalCount = totalCount + self.loadMoreToken = loadMoreToken + } +} + + +enum GroupCallVideoMode { + case video + case screencast +} + +protocol PresentationGroupCall: class { + + + + var account: Account { get } + var engine: TelegramEngine { get } + var accountContext: AccountContext { get } + var sharedContext: SharedAccountContext { get } + var internalId: CallSessionInternalId { get } + var peerId: PeerId { get } + var peer: Peer? { get } + var joinAsPeerId: PeerId { get } + var joinAsPeerIdValue:Signal { get } + var canBeRemoved: Signal { get } + var state: Signal { get } + var members: Signal { get } + var audioLevels: Signal<[(PeerId, UInt32, Float, Bool)], NoError> { get } + var myAudioLevel: Signal { get } + var invitedPeers: Signal<[PeerId], NoError> { get } + var isMuted: Signal { get } + var summaryState: Signal { get } + var callInfo: Signal { get } + var stateVersion: Signal { get } + var isSpeaking: Signal { get } + + var mustStopSharing:(()->Void)? { get set } + var mustStopVideo:(()->Void)? { get set } + +// var activeCall: CachedChannelData.ActiveCall? { get } + var inviteLinks:Signal { get } + + var permissions:(PresentationGroupCallMuteAction, @escaping(Bool)->Void)->Void { get set } + + var displayAsPeers: Signal<[FoundPeer]?, NoError> { get } + + func raiseHand() + func lowerHand() + func resetListenerLink() + + func leave(terminateIfPossible: Bool) -> Signal + func toggleIsMuted() + func setVolume(peerId: PeerId, volume: Int32, sync: Bool) + func setIsMuted(action: PresentationGroupCallMuteAction) + func updateMuteState(peerId: PeerId, isMuted: Bool) -> GroupCallParticipantsContext.Participant.MuteState? + func invitePeer(_ peerId: PeerId) -> Bool + func updateDefaultParticipantsAreMuted(isMuted: Bool) + + func setRequestedVideoList(items: [PresentationGroupCallRequestedVideo]) + func makeVideoView(endpointId: String, videoMode: GroupCallVideoMode, completion: @escaping (PresentationCallVideoView?) -> Void) + func requestVideo(deviceId: String) + func disableVideo() + func requestScreencast(deviceId: String) + func disableScreencast() + + func loadMore() + + func joinAsSpeakerIfNeeded(_ joinHash: String) + func reconnect(as peerId: PeerId) -> Void + func updateTitle(_ title: String, force: Bool) -> Void + func setShouldBeRecording(_ shouldBeRecording: Bool, title: String?, videoOrientation: Bool?) -> Void + func startScheduled() + func toggleScheduledSubscription(_ subscribe: Bool) +} diff --git a/Telegram-Mac/PrettyGridUtils.swift b/Telegram-Mac/PrettyGridUtils.swift index 9ed53b5d60..d9647ff9bc 100644 --- a/Telegram-Mac/PrettyGridUtils.swift +++ b/Telegram-Mac/PrettyGridUtils.swift @@ -7,8 +7,9 @@ // import Cocoa -import TelegramCoreMac -import PostboxMac +import TelegramCore + +import Postbox import TGUIKit @@ -25,9 +26,9 @@ let kBotInlineTypeFile:String = "file"; let kBotInlineTypeVoice:String = "voice"; enum InputMediaContextEntry : Equatable { - case gif(thumb:TelegramMediaImage?, file:TelegramMediaResource) + case gif(thumb: ImageMediaReference?, file: FileMediaReference) case photo(image:TelegramMediaImage) - case sticker(thumb: TelegramMediaImage?, file:TelegramMediaFile) + case sticker(thumb: TelegramMediaImage?, file: TelegramMediaFile) } @@ -36,12 +37,12 @@ func ==(lhs:InputMediaContextEntry, rhs:InputMediaContextEntry) -> Bool { switch lhs { case let .gif(lhsData): if case let .gif(rhsData) = rhs { - if !lhsData.file.isEqual(to: rhsData.file) { + if !lhsData.file.media.isEqual(to: rhsData.file.media) { return false } if (lhsData.thumb == nil) != (lhsData.thumb == nil) { return false - } else if let lhsThumb = lhsData.thumb, let rhsThumb = rhsData.thumb, lhsThumb != rhsThumb { + } else if let lhsThumb = lhsData.thumb, let rhsThumb = rhsData.thumb, lhsThumb.media != rhsThumb.media { return false } @@ -83,8 +84,16 @@ func ==(lhs:InputMediaContextEntry, rhs:InputMediaContextEntry) -> Bool { struct InputMediaContextRow :Equatable { let entries:[InputMediaContextEntry] let results:[ChatContextResult] + let messages:[Message] let sizes:[NSSize] + init(entries:[InputMediaContextEntry], results: [ChatContextResult], sizes: [NSSize], messages: [Message] = []) { + self.entries = entries + self.results = results + self.sizes = sizes + self.messages = messages + } + func isFilled(for width:CGFloat) -> Bool { let sum:CGFloat = sizes.reduce(0, { (acc, size) -> CGFloat in return acc + size.width @@ -121,7 +130,7 @@ func fitPrettyDimensions(_ dimensions:[NSSize], isLastRow:Bool, fitToHeight:Bool var row:[NSSize] = [] var idx:Int = 0 for dimension in dimensions { - var fitted = dimension.fitted(NSMakeSize(perSize.width, maxHeight)) + var fitted = dimension.aspectFitted(NSMakeSize(floor(perSize.width / 3), maxHeight)) if fitted.width < maxHeight || fitted.height < maxHeight { let more: CGFloat = max(maxHeight - fitted.width, maxHeight - fitted.height) @@ -129,7 +138,7 @@ func fitPrettyDimensions(_ dimensions:[NSSize], isLastRow:Bool, fitToHeight:Bool fitted.height += more } - if !isLastRow && idx == dimensions.count - 1 && !fitToHeight { + if !isLastRow && idx == dimensions.count - 1 { let width:CGFloat = row.reduce(0, { (acc, size) -> CGFloat in return acc + size.width }) @@ -147,28 +156,35 @@ func fitPrettyDimensions(_ dimensions:[NSSize], isLastRow:Bool, fitToHeight:Bool return row } var rows:[NSSize] = [] + var plus: Int = 0 while true { - rows = sizeup(dimensions) - let width:CGFloat = rows.reduce(0, { (acc, size) -> CGFloat in - return acc + size.width - }) - - if width - perSize.width > 0 && dimensions.count > 1 { - dimensions.removeLast() - continue - } - if (width < perSize.width && !isLastRow) && !dimensions.isEmpty && !fitToHeight { - maxHeight += CGFloat(6 * dimensions.count) + let about = Array(dimensions.prefix(Int(ceil(perSize.width / perSize.height)) + plus)) + if !about.isEmpty { + rows = sizeup(about) + + let width:CGFloat = rows.reduce(0, { (acc, size) -> CGFloat in + return acc + size.width + }) + + if perSize.width < width { + plus -= 1 + continue + } + if (width < perSize.width && !isLastRow) && !fitToHeight { + maxHeight += CGFloat(6 * dimensions.count) + } else { + break + } } else { break } + } - return rows } func makeStickerEntries(_ stickers:[FoundStickerItem], initialSize:NSSize, maxSize:NSSize = NSMakeSize(80, 80)) -> [InputMediaStickersRow] { - let s = floorToScreenPixels(initialSize.width/floor(initialSize.width/maxSize.width)) + let s = floorToScreenPixels(System.backingScale, initialSize.width/floor(initialSize.width/maxSize.width)) let perRow = Int(initialSize.width / s) var entries:[InputMediaContextEntry] = [] @@ -180,7 +196,7 @@ func makeStickerEntries(_ stickers:[FoundStickerItem], initialSize:NSSize, maxSi while !stickers.isEmpty { let sticker = stickers[0] - entries.append(.sticker(thumb: TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: sticker.file.previewRepresentations), file: sticker.file)) + entries.append(.sticker(thumb: TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: sticker.file.previewRepresentations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []), file: sticker.file)) sizes.append(NSMakeSize(s, s)) items.append(sticker) if entries.count == perRow { @@ -199,7 +215,7 @@ func makeStickerEntries(_ stickers:[FoundStickerItem], initialSize:NSSize, maxSi return rows } -func makeMediaEnties(_ results:[ChatContextResult], initialSize:NSSize) -> [InputMediaContextRow] { +func makeMediaEnties(_ results:[ChatContextResult], isSavedGifs: Bool, initialSize:NSSize) -> [InputMediaContextRow] { var entries:[InputMediaContextEntry] = [] var rows:[InputMediaContextRow] = [] @@ -214,21 +230,27 @@ func makeMediaEnties(_ results:[ChatContextResult], initialSize:NSSize) -> [Inpu case let .externalReference(data): switch data.type { case kBotInlineTypeGif: - if let dimension = data.dimensions, let contentUrl = data.contentUrl { - var image:TelegramMediaImage? = nil - if let thumbUrl = data.thumbnailUrl { - image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [TelegramMediaImageRepresentation(dimensions: dimension, resource: HttpReferenceMediaResource(url: thumbUrl, size: nil))]) + if let content = data.content { + var image:ImageMediaReference? = nil + if let thumbnail = data.thumbnail, let dimensions = thumbnail.dimensions { + let tmp = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnail.resource, progressiveSizes: [], immediateThumbnailData: nil)], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + image = isSavedGifs ? ImageMediaReference.savedGif(media: tmp) : ImageMediaReference.standalone(media: tmp) } - entries.append(.gif(thumb: image, file: HttpReferenceMediaResource(url: contentUrl, size: nil))) + let file = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/gif", size: content.resource.size, attributes: [TelegramMediaFileAttribute.Animated]) + entries.append(.gif(thumb: image, file: isSavedGifs ? FileMediaReference.savedGif(media: file) : FileMediaReference.standalone(media: file))) } else { removeResultIndexes.append(i) } case kBotInlineTypePhoto: - let dimension = data.dimensions ?? NSMakeSize(100, 100) var image:TelegramMediaImage? = nil - if let thumbUrl = data.thumbnailUrl { - image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [TelegramMediaImageRepresentation(dimensions: dimension, resource: HttpReferenceMediaResource(url: thumbUrl, size: nil))]) + if let content = data.content, let dimensions = content.dimensions { + var representations: [TelegramMediaImageRepresentation] = [] + if let thumbnail = data.thumbnail, let dimensions = thumbnail.dimensions { + representations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnail.resource, progressiveSizes: [], immediateThumbnailData: nil)) + } + representations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: content.resource, progressiveSizes: [], immediateThumbnailData: nil)) + image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) } if let image = image { entries.append(.photo(image: image)) @@ -236,12 +258,12 @@ func makeMediaEnties(_ results:[ChatContextResult], initialSize:NSSize) -> [Inpu removeResultIndexes.append(i) } case kBotInlineTypeSticker: - if let dimension = data.dimensions, let contentUrl = data.contentUrl { + if let content = data.content { var image:TelegramMediaImage? = nil - if let thumbUrl = data.thumbnailUrl { - image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [TelegramMediaImageRepresentation(dimensions: dimension, resource: HttpReferenceMediaResource(url: thumbUrl, size: nil))]) + if let thumbnail = data.thumbnail, let dimensions = thumbnail.dimensions { + image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [TelegramMediaImageRepresentation(dimensions: dimensions, resource: thumbnail.resource, progressiveSizes: [], immediateThumbnailData: nil)], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) } - entries.append(.sticker(thumb: image, file: TelegramMediaFile.init(fileId: MediaId(namespace: 0, id: 0), resource: HttpReferenceMediaResource(url: contentUrl, size: nil), previewRepresentations: [], mimeType: "image/webp", size: nil, attributes: []))) + entries.append(.sticker(thumb: image, file: TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: content.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: nil, attributes: content.attributes))) } else { removeResultIndexes.append(i) } @@ -253,14 +275,21 @@ func makeMediaEnties(_ results:[ChatContextResult], initialSize:NSSize) -> [Inpu case kBotInlineTypeGif: if let file = data.file { dimension = file.videoSize - entries.append(.gif(thumb: data.image, file: file.resource)) + var thumb: ImageMediaReference? = nil + if let image = data.image { + thumb = ImageMediaReference.standalone(media: image) + } else if !file.previewRepresentations.isEmpty { + let tmp = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: file.previewRepresentations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + thumb = isSavedGifs ? ImageMediaReference.savedGif(media: tmp) : ImageMediaReference.standalone(media: tmp) + } + entries.append(.gif(thumb: thumb, file: isSavedGifs ? FileMediaReference.savedGif(media: file) : FileMediaReference.standalone(media: file))) } else { removeResultIndexes.append(i) } case kBotInlineTypePhoto: if let image = data.image, let representation = image.representations.last { - dimension = representation.dimensions + dimension = representation.dimensions.size entries.append(.photo(image: image)) } else { removeResultIndexes.append(i) @@ -284,13 +313,28 @@ func makeMediaEnties(_ results:[ChatContextResult], initialSize:NSSize) -> [Inpu for i in removeResultIndexes.reversed() { results.remove(at: i) } - var fitted:[[NSSize]] = [] let f:Int = Int(round(initialSize.width / initialSize.height)) + + let rowCount = Int(floor(initialSize.width / 100)) + while !dimensions.isEmpty { - let row = fitPrettyDimensions(dimensions, isLastRow: f > dimensions.count, fitToHeight: false, perSize:initialSize) + //let row = fitPrettyDimensions(dimensions, isLastRow: f > dimensions.count, fitToHeight: false, perSize:initialSize) + var row:[NSSize] = [] + + while !dimensions.isEmpty && row.count < rowCount { + dimensions.removeFirst() + row.append(NSMakeSize(floor(initialSize.width / CGFloat(rowCount)), initialSize.height)) + } + fitted.append(row) - dimensions.removeSubrange(0 ..< row.count) + } + + + if fitted.count >= 2, fitted[fitted.count - 1].count == 1 && fitted[fitted.count - 2].reduce(0, { $0 + $1.width}) < (initialSize.width - 50) { + let width = fitted[fitted.count - 2].reduce(0, { $0 + $1.width}) + let last = fitted.removeLast() + fitted[fitted.count - 1] = fitted[fitted.count - 1] + [NSMakeSize(initialSize.width - width, last[0].height)] } for row in fitted { @@ -312,3 +356,65 @@ func makeMediaEnties(_ results:[ChatContextResult], initialSize:NSSize) -> [Inpu +func makeChatGridMediaEnties(_ results:[Message], initialSize:NSSize) -> [InputMediaContextRow] { + var entries:[InputMediaContextEntry] = [] + var rows:[InputMediaContextRow] = [] + + var dimensions:[NSSize] = [] + var removeResultIndexes:[Int] = [] + var results = results + for i in 0 ..< results.count { + + let result = results[i] + + if let file = result.media.first as? TelegramMediaFile { + let dimension:NSSize = file.videoSize + + let imageReference: ImageMediaReference? + if !file.previewRepresentations.isEmpty { + let img = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: file.previewRepresentations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + imageReference = ImageMediaReference.message(message: MessageReference(result), media: img) + } else { + imageReference = nil + } + entries.append(.gif(thumb: imageReference, file: FileMediaReference.message(message: MessageReference(result), media: file))) + + dimensions.append(dimension) + } else { + removeResultIndexes.append(i) + } + } + + for i in removeResultIndexes.reversed() { + results.remove(at: i) + } + var fitted:[[NSSize]] = [] + let f:Int = Int(round(initialSize.width / initialSize.height)) + while !dimensions.isEmpty { + let row = fitPrettyDimensions(dimensions, isLastRow: f > dimensions.count, fitToHeight: false, perSize:initialSize) + fitted.append(row) + dimensions.removeSubrange(0 ..< row.count) + } + + if fitted.count >= 2, fitted[fitted.count - 1].count == 1 && fitted[fitted.count - 2].reduce(0, { $0 + $1.width}) < (initialSize.width - 50) { + let width = fitted[fitted.count - 2].reduce(0, { $0 + $1.width}) + let last = fitted.removeLast() + fitted[fitted.count - 1] = fitted[fitted.count - 1] + [NSMakeSize(initialSize.width - width, last[0].height)] + } + + for row in fitted { + let subentries = Array(entries.prefix(row.count)) + let subresult = Array(results.prefix(row.count)) + rows.append(InputMediaContextRow(entries: subentries, results: [], sizes: row, messages: subresult)) + + if entries.count >= row.count { + entries.removeSubrange(0 ..< row.count) + } + if results.count >= row.count { + results.removeSubrange(0 ..< row.count) + } + } + + return rows +} + diff --git a/Telegram-Mac/PreviewSenderController.swift b/Telegram-Mac/PreviewSenderController.swift index 512e877be1..7fb0d4ef9a 100644 --- a/Telegram-Mac/PreviewSenderController.swift +++ b/Telegram-Mac/PreviewSenderController.swift @@ -8,199 +8,1388 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac +import TelegramCore + +import SwiftSignalKit +import Postbox private enum SecretMediaTtl { case off case seconds(Int32) } -fileprivate class PreviewSenderView : Control { - fileprivate let tableView:TableView = TableView() - fileprivate let textView:TGModernGrowingTextView = TGModernGrowingTextView(frame: NSZeroRect) +private enum PreviewSenderType { + case files + case photo + case video + case gif + case audio + case media +} + +fileprivate struct PreviewSendingState : Hashable { + enum State : Int32 { + case media = 0 + case file = 1 + case archive = 3 + } + let state:State + let isCollage: Bool + + func hash(into hasher: inout Hasher) { + hasher.combine(state) + hasher.combine(isCollage) + } + + func withUpdatedState(_ state: State) -> PreviewSendingState { + return .init(state: state, isCollage: self.isCollage) + } + func withUpdatedIsCollage(_ isCollage: Bool) -> PreviewSendingState { + return .init(state: self.state, isCollage: isCollage) + } + +} + + + +private final class PreviewContextState : Equatable { + let inputQueryResult: ChatPresentationInputQueryResult? + init(inputQueryResult: ChatPresentationInputQueryResult? = nil) { + self.inputQueryResult = inputQueryResult + } + func updatedInputQueryResult(_ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> PreviewContextState { + return PreviewContextState(inputQueryResult: f(self.inputQueryResult)) + } +} + +private func ==(lhs: PreviewContextState, rhs: PreviewContextState) -> Bool { + return lhs.inputQueryResult == rhs.inputQueryResult +} + +private final class PreviewContextInteraction : InterfaceObserver { + private(set) var state: PreviewContextState = PreviewContextState() + + func update(animated:Bool = true, _ f:(PreviewContextState)->PreviewContextState) -> Void { + let oldValue = self.state + self.state = f(state) + if oldValue != state { + notifyObservers(value: state, oldValue:oldValue, animated: animated) + } + } +} + + +fileprivate class PreviewSenderView : Control { + fileprivate let tableView:TableView = TableView(frame: NSZeroRect) + fileprivate let textView:TGModernGrowingTextView = TGModernGrowingTextView(frame: NSMakeRect(0, 0, 280, 34)) + fileprivate let sendButton = ImageButton() + fileprivate let emojiButton = ImageButton() + fileprivate let actionsContainerView: View = View() + fileprivate let headerView: View = View() + fileprivate let draggingView = DraggingView(frame: NSZeroRect) + fileprivate let closeButton = ImageButton() + fileprivate let photoButton = ImageButton() + fileprivate let fileButton = ImageButton() + fileprivate let collageButton = ImageButton() + fileprivate let archiveButton = ImageButton() + fileprivate let textContainerView: View = View() + fileprivate let separator: View = View() + fileprivate let forHelperView: View = View() + fileprivate weak var controller: PreviewSenderController? + fileprivate var stateValueInteractiveUpdate: ((PreviewSendingState)->Void)? + + private var _state: PreviewSendingState = PreviewSendingState(state: .file, isCollage: FastSettings.isNeedCollage) + var state: PreviewSendingState { + set { + _state = newValue + self.fileButton.isSelected = newValue.state == .file + self.photoButton.isSelected = newValue.state == .media + self.collageButton.isSelected = newValue.isCollage + self.archiveButton.isSelected = newValue.state == .archive + + Queue.mainQueue().justDispatch { + removeAllTooltips(mainWindow) + self.fileButton.controlState = .Normal + self.photoButton.controlState = .Normal + self.collageButton.controlState = .Normal + self.archiveButton.controlState = .Normal + } + } + get { + return self._state + } + } + + fileprivate func updateWithSlowMode(_ slowMode: SlowMode?, urlsCount: Int) { + if urlsCount > 1, let _ = slowMode { + self.fileButton.isEnabled = false + self.photoButton.isEnabled = false + self.photoButton.appTooltip = L10n.slowModePreviewSenderFileTooltip + self.fileButton.appTooltip = L10n.slowModePreviewSenderFileTooltip + } else { + self.fileButton.isEnabled = true + self.photoButton.isEnabled = true + self.photoButton.appTooltip = L10n.previewSenderMediaTooltip + self.fileButton.appTooltip = L10n.previewSenderFileTooltip + } + } + + + + private let disposable = MetaDisposable() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + + backgroundColor = theme.colors.background + separator.backgroundColor = theme.colors.border + textContainerView.backgroundColor = theme.colors.background + textView.setBackgroundColor(theme.colors.background) + closeButton.set(image: theme.icons.modalClose, for: .Normal) + _ = closeButton.sizeToFit() + + + photoButton.appTooltip = L10n.previewSenderMediaTooltip + fileButton.appTooltip = L10n.previewSenderFileTooltip + collageButton.appTooltip = L10n.previewSenderCollageTooltip + archiveButton.appTooltip = L10n.previewSenderArchiveTooltip + + photoButton.set(image: ControlStyle(highlightColor: theme.colors.grayIcon).highlight(image: theme.icons.previewSenderPhoto), for: .Normal) + _ = photoButton.sizeToFit() + + let updateValue:((PreviewSendingState)->PreviewSendingState)->Void = { [weak self] f in + guard let `self` = self else { + return + } + self.stateValueInteractiveUpdate?(f(self.state)) + } + + photoButton.set(handler: { _ in + updateValue { + $0.withUpdatedState(.media) + } + }, for: .Click) + + + archiveButton.set(handler: { _ in + updateValue { + $0.withUpdatedState(.archive) + } + }, for: .Click) + + collageButton.set(handler: { _ in + updateValue { + $0.withUpdatedIsCollage(!$0.isCollage) + } + }, for: .Click) + + fileButton.set(handler: { _ in + updateValue { + $0.withUpdatedState(.file) + } + }, for: .Click) + + closeButton.set(handler: { [weak self] _ in + self?.controller?.closeModal() + }, for: .Click) + + fileButton.set(image: ControlStyle(highlightColor: theme.colors.grayIcon).highlight(image: theme.icons.previewSenderFile), for: .Normal) + _ = fileButton.sizeToFit() + + collageButton.set(image: theme.icons.previewSenderCollage, for: .Normal) + _ = collageButton.sizeToFit() + + archiveButton.set(image: theme.icons.previewSenderArchive, for: .Normal) + _ = archiveButton.sizeToFit() + + + + headerView.addSubview(closeButton) + headerView.addSubview(fileButton) + headerView.addSubview(photoButton) + headerView.addSubview(collageButton) + headerView.addSubview(archiveButton) + + sendButton.set(image: theme.icons.chatSendMessage, for: .Normal) + sendButton.autohighlight = false + _ = sendButton.sizeToFit() + + emojiButton.set(image: theme.icons.chatEntertainment, for: .Normal) + _ = emojiButton.sizeToFit() + + actionsContainerView.addSubview(sendButton) + actionsContainerView.addSubview(emojiButton) + + + actionsContainerView.setFrameSize(sendButton.frame.width + emojiButton.frame.width + 40, 50) + + emojiButton.centerY(x: 0) + sendButton.centerY(x: emojiButton.frame.maxX + 20) + + backgroundColor = theme.colors.background + textView.background = theme.colors.background + textView.textFont = .normal(.text) + textView.textColor = theme.colors.text + textView.linkColor = theme.colors.link + textView.max_height = 180 + + emojiButton.set(handler: { [weak self] control in + self?.controller?.showEmoji(for: control) + }, for: .Hover) + + sendButton.set(handler: { [weak self] _ in + self?.controller?.send(false) + }, for: .SingleClick) + + let handler:(Control)->Void = { [weak self] control in + if let controller = self?.controller, let peer = controller.chatInteraction.peer { + + let chatInteraction = controller.chatInteraction + let context = chatInteraction.context + if let slowMode = chatInteraction.presentation.slowMode, slowMode.hasLocked { + return + } + + var items:[SPopoverItem] = [] + + if peer.id != chatInteraction.context.account.peerId { + items.append(SPopoverItem(L10n.chatSendWithoutSound, { [weak controller] in + controller?.send(true) + })) + } + switch chatInteraction.mode { + case .history: + if !peer.isSecretChat { + items.append(SPopoverItem(peer.id == chatInteraction.context.peerId ? L10n.chatSendSetReminder : L10n.chatSendScheduledMessage, { + showModal(with: DateSelectorModalController(context: context, mode: .schedule(peer.id), selectedAt: { [weak controller] date in + controller?.send(false, atDate: date) + }), for: context.window) + })) + } + case .scheduled: + break + case .replyThread: + break + case .pinned, .preview: + break + } + if !items.isEmpty { + showPopover(for: control, with: SPopoverViewController(items: items)) + } + } + } + + sendButton.set(handler: handler, for: .RightDown) + sendButton.set(handler: handler, for: .LongMouseDown) + + textView.setFrameSize(NSMakeSize(280, 34)) + + addSubview(tableView) + + + textContainerView.addSubview(textView) + + addSubview(headerView) + addSubview(forHelperView) + addSubview(textContainerView) + addSubview(actionsContainerView) + addSubview(separator) + addSubview(draggingView) + + draggingView.isEventLess = true + layout() + } + + deinit { + disposable.dispose() + } + + var additionHeight: CGFloat { + return max(50, textView.frame.height + 16) + headerView.frame.height + } + + override func setFrameSize(_ newSize: NSSize) { + super.setFrameSize(newSize) + } + + override func change(size: NSSize, animated: Bool, _ save: Bool = true, removeOnCompletion: Bool = true, duration: Double = 0.2, timingFunction: CAMediaTimingFunctionName = CAMediaTimingFunctionName.easeOut, completion: ((Bool) -> Void)? = nil) { + self.updateHeight(self.textView.frame.height, animated) + super._change(size: size, animated: animated, save, removeOnCompletion: removeOnCompletion, duration: duration, timingFunction: timingFunction, completion: completion) + } + + func updateHeight(_ height: CGFloat, _ animated: Bool) { + CATransaction.begin() + textContainerView.change(size: NSMakeSize(frame.width, height + 16), animated: animated) + textContainerView.change(pos: NSMakePoint(0, frame.height - textContainerView.frame.height), animated: animated) + textView._change(pos: NSMakePoint(10, height == 34 ? 8 : 11), animated: animated) + + actionsContainerView.change(pos: NSMakePoint(frame.width - actionsContainerView.frame.width, frame.height - actionsContainerView.frame.height), animated: animated) + + separator.change(pos: NSMakePoint(0, textContainerView.frame.minY), animated: animated) + CATransaction.commit() + + // needsLayout = true + } + + func applyOptions(_ options:[PreviewOptions], count: Int, canCollage: Bool) { + fileButton.isHidden = false//!options.contains(.media) + photoButton.isHidden = !options.contains(.media) + archiveButton.isHidden = count < 2 + self.collageButton.isHidden = !canCollage + separator.isHidden = false + needsLayout = true + } + + override func layout() { + super.layout() + actionsContainerView.setFrameOrigin(frame.width - actionsContainerView.frame.width, frame.height - actionsContainerView.frame.height) + headerView.setFrameSize(frame.width, 50) + + tableView.setFrameSize(NSMakeSize(frame.width, frame.height - additionHeight)) + tableView.centerX(y: headerView.frame.maxY - 6) + + draggingView.frame = tableView.frame + + + + closeButton.centerY(x: headerView.frame.width - closeButton.frame.width - 10) + collageButton.centerY(x: closeButton.frame.minX - 10 - collageButton.frame.width) + + var inset: CGFloat = 10 + + if !photoButton.isHidden { + photoButton.centerY(x: inset) + inset += photoButton.frame.width + 10 + } + + if !fileButton.isHidden { + fileButton.centerY(x: inset) + inset += fileButton.frame.width + 10 + } + + if !archiveButton.isHidden { + archiveButton.centerY(x: inset) + inset += archiveButton.frame.width + 10 + } + + textContainerView.setFrameSize(frame.width, textView.frame.height + 16) + textContainerView.setFrameOrigin(0, frame.height - textContainerView.frame.height) + textView.setFrameSize(NSMakeSize(textContainerView.frame.width - 10 - actionsContainerView.frame.width, textView.frame.height)) + textView.setFrameOrigin(10, textView.frame.height == 34 ? 8 : 11) + + separator.frame = NSMakeRect(0, textContainerView.frame.minY, frame.width, .borderSize) + + forHelperView.frame = NSMakeRect(0, textContainerView.frame.minY, 0, 0) + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private final class SenderPreviewArguments { + let context: AccountContext + let edit:(URL)->Void + let paint: (URL)->Void + let delete:(URL)->Void + let reorder:(Int, Int) -> Void + init(context: AccountContext, edit: @escaping(URL)->Void, paint: @escaping(URL)->Void, delete: @escaping(URL)->Void, reorder: @escaping(Int, Int) -> Void) { + self.context = context + self.edit = edit + self.paint = paint + self.delete = delete + self.reorder = reorder + } +} + +private struct PreviewState : Equatable { + let urls:[URL] + let medias:[Media] + let currentState: PreviewSendingState + let editedData: [URL : EditedImageData] + init(urls: [URL], medias: [Media], currentState: PreviewSendingState, editedData: [URL : EditedImageData]) { + self.urls = urls + self.medias = medias + self.currentState = currentState + self.editedData = editedData + } + + func withUpdatedEditedData(_ f:([URL : EditedImageData]) -> [URL : EditedImageData]) -> PreviewState { + return PreviewState(urls: self.urls, medias: self.medias, currentState: self.currentState, editedData: f(self.editedData)) + } + func apply(transition: UpdateTransition, urls:[URL], state: PreviewSendingState) -> PreviewState { + var medias:[Media] = self.medias + for rdx in transition.deleted.reversed() { + medias.remove(at: rdx) + } + for (idx, media) in transition.inserted { + medias.insert(media, at: idx) + } + for (idx, item) in transition.updated { + medias[idx] = item + } + + return PreviewState(urls: urls, medias: medias, currentState: state, editedData: self.editedData) + } +} +private func == (lhs: PreviewState, rhs: PreviewState) -> Bool { + if lhs.medias.count != rhs.medias.count { + return false + } else { + for i in 0 ..< lhs.medias.count { + if !lhs.medias[i].isEqual(to: rhs.medias[i]) { + return false + } + } + } + return lhs.urls == rhs.urls && lhs.currentState == rhs.currentState && lhs.editedData == rhs.editedData +} + + +private enum PreviewEntryId : Hashable { + static func == (lhs: PreviewEntryId, rhs: PreviewEntryId) -> Bool { + switch lhs { + case let .media(lhsMedia): + if case let .media(rhsMedia) = rhs { + return lhsMedia.isEqual(to: rhsMedia) + } else { + return false + } + case .mediaGroup: + if case .mediaGroup = rhs { + return true + } else { + return false + } + case let .section(index): + if case .section(index) = rhs { + return true + } else { + return false + } + case .archive: + if case .archive = rhs { + return true + } else { + return false + } + } + } + + func hash(into hasher: inout Hasher) { + + } + + case media(Media) + case mediaGroup + case archive + case section(Int) +} + +private enum PreviewEntry : Comparable, Identifiable { + case section(Int) + case media(index: Int, sectionId: Int, url: URL, media: Media) + case mediaGroup(index: Int, sectionId: Int, urls: [URL], messages: [Message]) + case archive(index: Int, sectionId: Int, urls: [URL], media: Media) + var stableId: PreviewEntryId { + switch self { + case let .section(sectionId): + return .section(sectionId) + case let .media(_, _, _, media): + return .media(media) + case .mediaGroup: + return .mediaGroup + case .archive: + return .archive + } + } + + var index: Int { + switch self { + case let .section(sectionId): + return (sectionId + 1) * 1000 - sectionId + case let .media(index, sectionId, _, _): + return (sectionId * 1000) + index + case let .mediaGroup(index, sectionId, _, _): + return (sectionId * 1000) + index + case let .archive(index, sectionId, _, _): + return (sectionId * 1000) + index + } + } + + func item(arguments: SenderPreviewArguments, state: PreviewState, initialSize: NSSize) -> TableRowItem { + switch self { + case .section: + return GeneralRowItem(initialSize, height: 20, stableId: stableId) + case let .media(_, _, url, media): + return MediaPreviewRowItem(initialSize, media: media, context: arguments.context, editedData: state.editedData[url], edit: { + arguments.edit(url) + }, paint: { + arguments.paint(url) + }, delete: { + arguments.delete(url) + }) + case let .archive(_, _, _, media): + return MediaPreviewRowItem(initialSize, media: media, context: arguments.context, editedData: nil, edit: { + // arguments.edit(url) + }, delete: { + // arguments.delete(url) + }) + case let .mediaGroup(_, _, urls, messages): + return MediaGroupPreviewRowItem(initialSize, messages: messages, urls: urls, editedData: state.editedData, edit: { url in + arguments.edit(url) + }, paint: { url in + arguments.paint(url) + }, delete: { url in + arguments.delete(url) + }, context: arguments.context, reorder: { from, to in + arguments.reorder(from, to) + }) + } + } + +} +private func == (lhs: PreviewEntry, rhs: PreviewEntry) -> Bool { + switch lhs { + case let .media(index, sectionId, url, lhsMedia): + if case .media(index, sectionId, url, let rhsMedia) = rhs { + return lhsMedia.isEqual(to: rhsMedia) + } else { + return false + } + case let .archive(index, sectionId, urls, lhsMedia): + if case .archive(index, sectionId, urls, let rhsMedia) = rhs { + return lhsMedia.isEqual(to: rhsMedia) + } else { + return false + } + case let .mediaGroup(index, sectionId, url, lhsMessages): + if case .mediaGroup(index, sectionId, url, let rhsMessages) = rhs { + if lhsMessages.count != rhsMessages.count { + return false + } else { + for i in 0 ..< lhsMessages.count { + if !isEqualMessages(lhsMessages[i], rhsMessages[i]) { + return false + } + } + return true + } + } else { + return false + } + case let .section(section): + if case .section(section) = rhs { + return true + } else { + return false + } + } +} +private func < (lhs: PreviewEntry, rhs: PreviewEntry) -> Bool { + return lhs.index < rhs.index +} + +private func previewMediaEntries( _ state: PreviewState) -> [PreviewEntry] { + + var entries: [PreviewEntry] = [] + var index: Int = 0 + + let sectionId: Int = 0 + + switch state.currentState.state { + case .archive: + assert(state.medias.count == 1) + entries.append(.archive(index: index, sectionId: sectionId, urls: state.urls, media: state.medias[0])) + case .media: + if state.currentState.isCollage { + var messages: [Message] = [] + for (i, media) in state.medias.enumerated() { + messages.append(Message(media, stableId: UInt32(i), messageId: MessageId(peerId: PeerId(0), namespace: 0, id: MessageId.Id(i)))) + } + if !messages.isEmpty { + entries.append(.mediaGroup(index: index, sectionId: sectionId, urls: state.urls, messages: messages)) + } + } else { + for (i, media) in state.medias.enumerated() { + entries.append(.media(index: index, sectionId: sectionId, url: state.urls[i], media: media)) + index += 1 + } + } + case .file: + for (i, media) in state.medias.enumerated() { + entries.append(.media(index: index, sectionId: sectionId, url: state.urls[i], media: media)) + index += 1 + } + } + + return entries +} + + +fileprivate func prepareTransition(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], state: PreviewState, arguments: SenderPreviewArguments, animated: Bool, initialSize:NSSize) -> TableUpdateTransition { + let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in + return entry.entry.item(arguments: arguments, state: state, initialSize: initialSize) + } + return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: animated, grouping: true) +} + +private enum PreviewMediaId : Hashable { + case container(MediaSenderContainer) + + func hash(into hasher: inout Hasher) { + + } + +} + +private final class PreviewMedia : Comparable, Identifiable { + + static func < (lhs: PreviewMedia, rhs: PreviewMedia) -> Bool { + return lhs.index < rhs.index + } + static func == (lhs: PreviewMedia, rhs: PreviewMedia) -> Bool { + return lhs.container == rhs.container + } + let container: MediaSenderContainer + let index: Int + private(set) var media: Media? + + init(container: MediaSenderContainer, index: Int, media: Media?) { + self.container = container + self.index = index + self.media = media + } + + + + var stableId: PreviewMediaId { + return .container(container) + } + + func withApplyCachedMedia(_ media: Media) -> PreviewMedia { + return PreviewMedia(container: container, index: index, media: media) + } + + func generateMedia(account: Account, isSecretRelated: Bool) -> Media { + + if let media = self.media { + return media + } + + let semaphore = DispatchSemaphore(value: 0) + var generated: Media! + + if let container = container as? ArchiverSenderContainer { + for url in container.files { + try? FileManager.default.copyItem(atPath: url.path, toPath: container.path + "/" + url.path.nsstring.lastPathComponent) + } + } + + _ = Sender.generateMedia(for: container, account: account, isSecretRelated: isSecretRelated).start(next: { media, path in + generated = media + semaphore.signal() + }) + semaphore.wait() + + self.media = generated + + return generated + } +} + + +private func previewMedias(containers:[MediaSenderContainer], savedState: [PreviewMedia]?) -> [PreviewMedia] { + var index: Int = 0 + var result:[PreviewMedia] = [] + for container in containers { + let found = savedState?.first(where: {$0.stableId == .container(container)}) + result.append(PreviewMedia(container: container, index: index, media: found?.media)) + index += 1 + } + return result +} + + +private func prepareMedias(left: [PreviewMedia], right: [PreviewMedia], isSecretRelated: Bool, account: Account) -> UpdateTransition { + let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right, { item in + return item.generateMedia(account: account, isSecretRelated: isSecretRelated) + }) + return UpdateTransition(deleted: removed, inserted: inserted, updated: updated) +} + +private struct UrlAndState : Equatable { + let urls:[URL] + let state: PreviewSendingState + init(_ urls:[URL], _ state: PreviewSendingState) { + self.urls = urls + self.state = state + } +} + + +class PreviewSenderController: ModalViewController, TGModernGrowingDelegate, Notifable { + + private var lockInteractiveChanges: Bool = false + private var _urls:[URL] = [] + + fileprivate var urls:[URL] { + set { + _urls = newValue.uniqueElements + let canCollage: Bool = canCollagesFromUrl(_urls) + if !lockInteractiveChanges { + if self.genericView.state.isCollage, !canCollage { + self.genericView.state = self.genericView.state.withUpdatedIsCollage(false) + } + } + self.genericView.updateWithSlowMode(chatInteraction.presentation.slowMode, urlsCount: _urls.count) + self.urlsAndStateValue.set(UrlAndState(_urls, self.genericView.state)) + } + get { + return _urls + } + } + + fileprivate let urlsAndStateValue:ValuePromise = ValuePromise(ignoreRepeated: true) + + + private let context:AccountContext + let chatInteraction:ChatInteraction + private let disposable = MetaDisposable() + private let emoji: EmojiViewController + private var cachedMedia:[PreviewSendingState: (media: [Media], items: [TableRowItem])] = [:] + private var sent: Bool = false + private let pasteDisposable = MetaDisposable() + + + private var temporaryInputState: ChatTextInputState? + private var contextQueryState: (ChatPresentationInputQuery?, Disposable)? + private let inputContextHelper: InputContextHelper + private let inputInteraction:PreviewContextInteraction = PreviewContextInteraction() + private let contextChatInteraction: ChatInteraction + private let editorDisposable = MetaDisposable() + private let archiverStatusesDisposable = MetaDisposable() + private var archiveStatuses: [ArchiveSource : ArchiveStatus] = [:] + private var genericView:PreviewSenderView { + return self.view as! PreviewSenderView + } + + override var responderPriority: HandlerPriority { + return .high + } + + private var sendCurrentMedia:((Bool, Date?)->Void)? = nil + private var runEditor:((URL, Bool)->Void)? = nil + private var insertAdditionUrls:(([URL]) -> Void)? = nil + + private let animated: Atomic = Atomic(value: false) + + private func updateSize(_ width: CGFloat, animated: Bool) { + if let contentSize = context.window.contentView?.frame.size { + + var listHeight = genericView.tableView.listHeight + if let inputQuery = inputInteraction.state.inputQueryResult { + switch inputQuery { + case let .emoji(emoji, _): + if !emoji.isEmpty { + listHeight = listHeight > 0 ? max(40, listHeight) : 0 + } + default: + //listHeight = listHeight > 0 ? max(150, listHeight) : 0 + break + } + } + + let height = listHeight + max(genericView.additionHeight, 88) + + + self.modal?.resize(with: NSMakeSize(width, min(contentSize.height - 70, height)), animated: animated) + // genericView.layout() + } + } + + override var dynamicSize: Bool { + return true + } + + override func draggingItems(for pasteboard: NSPasteboard) -> [DragItem] { + if let types = pasteboard.types, types.contains(.kFilenames) { + let list = pasteboard.propertyList(forType: .kFilenames) as? [String] + if let list = list { + return [DragItem(title: L10n.previewDraggingAddItemsCountable(list.count), desc: "", handler: { [weak self] in + self?.insertAdditionUrls?(list.map({URL(fileURLWithPath: $0)})) + + })] + } + } + return [] + } + + override func returnKeyAction() -> KeyHandlerResult { + if let currentEvent = NSApp.currentEvent { + if FastSettings.checkSendingAbility(for: currentEvent), didSetReady { + send(false) + return .invoked + } + } + return .invokeNext + } + + override func close(animationType: ModalAnimationCloseBehaviour = .common) { + + let currentText = self.genericView.textView.string() + let basicText = self.temporaryInputState?.inputText ?? "" + if (self.temporaryInputState == nil && !currentText.isEmpty) || (basicText != currentText) { + confirm(for: context.window, header: L10n.mediaSenderDiscardChangesHeader, information: L10n.mediaSenderDiscardChangesText, okTitle: L10n.mediaSenderDiscardChangesOK, successHandler: { [weak self] _ in + self?.closeModal() + }) + } else { + self.closeModal() + } + } + + fileprivate func closeModal() { + super.close() + } + + func send(_ silent: Bool, atDate: Date? = nil) { + + let text = self.genericView.textView.string().trimmed + if text.length > ChatPresentationInterfaceState.maxShortInput { + alert(for: chatInteraction.context.window, info: L10n.chatInputErrorMessageTooLongCountable(text.length - Int(ChatPresentationInterfaceState.maxShortInput))) + return + } + + switch chatInteraction.mode { + case .scheduled: + if let peer = chatInteraction.peer { + showModal(with: DateSelectorModalController(context: context, mode: .schedule(peer.id), selectedAt: { [weak self] date in + self?.sendCurrentMedia?(silent, date) + }), for: context.window) + } + case .history, .replyThread: + sendCurrentMedia?(silent, atDate) + case .pinned, .preview: + break + } + } + + + override func measure(size: NSSize) { + self.modal?.resize(with:NSMakeSize(genericView.frame.width, min(size.height - 70, genericView.tableView.listHeight + max(genericView.additionHeight, 88))), animated: false) + } + + override var handleAllEvents: Bool { + return true + } + + private var inputPlaceholder: String { + var placeholder: String = L10n.previewSenderCommentPlaceholder + if self.genericView.tableView.count == 1 { + if let item = self.genericView.tableView.firstItem { + if let item = item as? MediaPreviewRowItem { + if item.media.canHaveCaption { + placeholder = L10n.previewSenderCaptionPlaceholder + } + } else if item is MediaGroupPreviewRowItem { + placeholder = L10n.previewSenderCaptionPlaceholder + } + } + } + + return placeholder + } + + override func viewDidLoad() { + super.viewDidLoad() + + + genericView.draggingView.controller = self + genericView.controller = self + genericView.textView.delegate = self + inputInteraction.add(observer: self) + + self.genericView.textView.setPlaceholderAttributedString(.initialize(string: self.inputPlaceholder, color: theme.colors.grayText, font: .normal(.text)), update: false) + + if let attributedString = attributedString { + genericView.textView.setAttributedString(attributedString, animated: false) + } else { + self.temporaryInputState = chatInteraction.presentation.interfaceState.inputState + let text = chatInteraction.presentation.interfaceState.inputState.attributedString + + genericView.textView.setAttributedString(text, animated: false) + chatInteraction.update({$0.updatedInterfaceState({$0.withUpdatedInputState(ChatTextInputState())})}) + } + + let interactions = EntertainmentInteractions(.emoji, peerId: chatInteraction.peerId) + + interactions.sendEmoji = { [weak self] emoji in + self?.genericView.textView.appendText(emoji) + } + + emoji.update(with: interactions) + + let actionsDisposable = DisposableSet() + self.disposable.set(actionsDisposable) + + let context = self.context + let initialSize = self.atomicSize + + let initialState = PreviewState(urls: [], medias: [], currentState: .init(state: .media, isCollage: FastSettings.isNeedCollage), editedData: [:]) + + let statePromise:ValuePromise = ValuePromise(ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((PreviewState) -> PreviewState) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let removeTransitionAnimation: Atomic = Atomic(value: false) + + let arguments = SenderPreviewArguments(context: context, edit: { [weak self] url in + self?.runEditor?(url, false) + }, paint: { [weak self] url in + self?.runEditor?(url, true) + }, delete: { [weak self] url in + guard let `self` = self else { return } + self.lockInteractiveChanges = true + self.urls.removeAll(where: {$0 == url}) + self.lockInteractiveChanges = false + }, reorder: { [weak self] from, to in + guard let `self` = self else { return } + _ = removeTransitionAnimation.swap(true) + self.lockInteractiveChanges = true + self.urls.move(at: from, to: to) + self.lockInteractiveChanges = false + }) + + let archiveRandomId = arc4random() + + let isSecretRelated = chatInteraction.peerId.namespace == Namespaces.Peer.SecretChat + + + let previousMedias:Atomic<[PreviewMedia]> = Atomic(value: []) + let savedStateMedias:Atomic<[PreviewSendingState : [PreviewMedia]]> = Atomic(value: [:]) + + let urlSignal = self.urlsAndStateValue.get() |> deliverOnPrepareQueue + + let urlsTransition: Signal<(UpdateTransition, [URL], PreviewSendingState, [PreviewMedia]), NoError> = urlSignal |> map { urlsAndState -> ([PreviewMedia], [URL], PreviewSendingState) in + + let urls = urlsAndState.urls + let state = urlsAndState.state + + var containers = urls.compactMap { url -> MediaSenderContainer? in + switch state.state { + case .media: + return MediaSenderContainer(path: url.path, isFile: false) + case .file: + return MediaSenderContainer(path: url.path, isFile: true) + case .archive: + return nil + } + } + + if state.state == .archive { + let dir = NSTemporaryDirectory() + "tg_temp_archive_\(archiveRandomId)" + try? FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true, attributes: nil) + containers.append(ArchiverSenderContainer(path: dir, files: urls)) + } + + return (previewMedias(containers: containers, savedState: savedStateMedias.with { $0[state]}), urls, state) + } |> map { previews, urls, state in + return (prepareMedias(left: previousMedias.swap(previews), right: previews, isSecretRelated: isSecretRelated, account: context.account), urls, state, previews) + } + + actionsDisposable.add(urlsTransition.start(next: { transition, urls, state, previews in + updateState { + $0.apply(transition: transition, urls: urls, state: state) + } + _ = savedStateMedias.modify { current in + var current = current + current[state] = previews + return current + } + })) - required init(frame frameRect: NSRect) { - super.init(frame: frameRect) - tableView.setFrameSize(frameRect.width, frameRect.height - 34) - backgroundColor = theme.colors.background - textView.setPlaceholderAttributedString(.initialize(string: tr(.previderSenderCaptionPlaceholder), color: theme.colors.grayText, font: .normal(.text)), update: false) - textView.background = theme.colors.background - textView.textFont = .normal(.text) - textView.textColor = theme.colors.text - textView.linkColor = theme.colors.link - textView.max_height = 120 - backgroundColor = theme.colors.background - textView.setFrameSize(NSMakeSize(frameRect.width - 48, 34)) + let previousEntries:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) - addSubview(tableView) - addSubview(textView) - } - - override func layout() { - super.layout() - textView.setFrameOrigin(NSMakePoint(24, frame.height - textView.frame.height)) - } - - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} + let itemsTransition: Signal = combineLatest(queue: prepareQueue, statePromise.get() |> map { state -> ([PreviewEntry], PreviewState) in + return (previewMediaEntries(state), state) + }, appearanceSignal) |> map { datas, appearance in + let entries = datas.0.map { AppearanceWrapperEntry(entry: $0, appearance: appearance) } + return prepareTransition(left: previousEntries.swap(entries), right: entries, state: datas.1, arguments: arguments, animated: !removeTransitionAnimation.swap(false), initialSize: initialSize.with { $0 }) + } |> deliverOnMainQueue + + let first: Atomic = Atomic(value: false) + let scrollAfterTransition: Atomic = Atomic(value: false) + + actionsDisposable.add(itemsTransition.start(next: { [weak self] transition in + guard let `self` = self else {return} + + let state = stateValue.with { $0.currentState } + let medias = stateValue.with { $0.medias } + + + let sources:[ArchiveSource] = medias.filter { media in + if let media = media as? TelegramMediaFile { + return media.resource is LocalFileArchiveMediaResource + } else { + return false + } + }.map { ($0 as! TelegramMediaFile).resource as! LocalFileArchiveMediaResource}.map {.resource($0)} + + self.archiverStatusesDisposable.set(combineLatest(sources.map {archiver.archive($0)}).start(next: { [weak self] statuses in + guard let `self` = self else {return} + self.archiveStatuses.removeAll() + for i in 0 ..< sources.count { + self.archiveStatuses[sources[i]] = statuses[i] + } + })) + + self.genericView.state = state + + let options = takeSenderOptions(for: self.urls) + var canCollage = canCollagesFromUrl(self.urls) + switch state.state { + case .media: + canCollage = canCollage && options == [.media] + case .archive: + canCollage = false + default: + break + } + self.genericView.applyOptions(options, count: self.urls.count, canCollage: canCollage) + + self.genericView.tableView.merge(with: transition) + + self.genericView.textView.setPlaceholderAttributedString(.initialize(string: self.inputPlaceholder, color: theme.colors.grayText, font: .normal(.text)), update: false) -class PreviewSenderController: ModalViewController, TGModernGrowingDelegate { + if self.genericView.tableView.isEmpty { + self.closeModal() + if self.chatInteraction.presentation.effectiveInput.inputText.isEmpty { + let attributedString = self.genericView.textView.attributedString() + let input = ChatTextInputState(inputText: attributedString.string, selectionRange: attributedString.string.length ..< attributedString.string.length, attributes: chatTextAttributes(from: attributedString)) + self.chatInteraction.update({$0.withUpdatedEffectiveInputState(input)}) + } + } else { + let oldSize = self.genericView.frame.size + self.updateSize(320, animated: first.swap(!self.genericView.tableView.isEmpty)) + if scrollAfterTransition.swap(false), self.genericView.frame.size == oldSize { + self.genericView.tableView.scroll(to: .down(true)) + } + } + self.readyOnce() + - private var urls:[URL] - private let account:Account - private let chatInteraction:ChatInteraction - private var isNeedAsMedia:Bool = true - - override func viewClass() -> AnyClass { - return PreviewSenderView.self - } - - private var genericView:PreviewSenderView { - return self.view as! PreviewSenderView - } - - - func makeItems(_ urls:[URL]) -> Signal { - let initialSize = atomicSize - let account = self.account - return Signal {[weak self] (subscriber) in + if self.genericView.tableView.count > 1 { + self.genericView.tableView.resortController = TableResortController(resortRange: NSMakeRange(0, self.genericView.tableView.count), startTimeout: 0.0, start: { _ in }, resort: { _ in }, complete: { from, to in + arguments.reorder(from, to) + }) + } else { + self.genericView.tableView.resortController = nil + } + + })) - if let strongSelf = self { - - let headerItem:TableRowItem? - - let options = takeSenderOptions(for: urls) + + var canCollage: Bool = canCollagesFromUrl(self.urls) + let options = takeSenderOptions(for: self.urls) + let mediaState:PreviewSendingState.State = asMedia ? .media : .file + switch mediaState { + case .media: + canCollage = canCollage && options == [.media] + case .archive: + canCollage = false + default: + break + } + var state: PreviewSendingState = .init(state: mediaState, isCollage: canCollage && FastSettings.isNeedCollage) + if let _ = chatInteraction.presentation.slowMode { + if state.state != .archive && self.urls.count > 1, !state.isCollage { + state = .init(state: .archive, isCollage: false) + } + } + + self.genericView.state = state + self.urlsAndStateValue.set(UrlAndState(self.urls, state)) + self.genericView.updateWithSlowMode(chatInteraction.presentation.slowMode, urlsCount: self.urls.count) + + self.genericView.textView.setPlaceholderAttributedString(.initialize(string: self.inputPlaceholder, color: theme.colors.grayText, font: .normal(.text)), update: false) + + self.genericView.stateValueInteractiveUpdate = { [weak self] state in + guard let `self` = self else { return } + var state = state + var canCollage = canCollagesFromUrl(self.urls) + let options = takeSenderOptions(for: self.urls) + switch state.state { + case .media: + canCollage = canCollage && options == [.media] + case .archive: + canCollage = false + default: + break + } + FastSettings.toggleIsNeedCollage(state.isCollage) + if !canCollage && state.isCollage { + state = state.withUpdatedIsCollage(false) + } + self.genericView.tableView.scroll(to: .up(true)) + self.urlsAndStateValue.set(UrlAndState(self.urls, state)) + } + + self.sendCurrentMedia = { [weak self] silent, atDate in + guard let `self` = self else { return } + + let slowMode = self.chatInteraction.presentation.slowMode + let attributed = self.genericView.textView.attributedString() + + if let slowMode = slowMode, slowMode.hasLocked { + self.genericView.textView.shake() + } else if self.inputPlaceholder != L10n.previewSenderCaptionPlaceholder && slowMode != nil && attributed.length > 0 { + tooltip(for: self.genericView.sendButton, text: L10n.slowModeMultipleError) + self.genericView.textView.setSelectedRange(NSMakeRange(0, attributed.length)) + self.genericView.textView.shake() + } else { + let state = stateValue.with { $0.currentState } + let medias = stateValue.with { $0.medias } - if urls.count == 1 { - let url = urls[0] - let mime = MIMEType(url.path.nsstring.pathExtension.lowercased()) - if mime.hasPrefix("image") && mediaExts.contains(url.path.nsstring.pathExtension.lowercased()) { - headerItem = PreviewThumbRowItem(initialSize.modify({$0}), url: url, account:account) - } else { - headerItem = PreviewDocumentRowItem(initialSize.modify({$0}), url: url, account:account) + for i in 0 ..< medias.count { + if let media = medias[i] as? TelegramMediaFile, let resource = media.resource as? LocalFileArchiveMediaResource { + if let status = self.archiveStatuses[.resource(resource)] { + switch status { + case .waiting, .fail, .none: + self.genericView.tableView.item(at: i).view?.shakeView() + return + default: + break + } + } else { + self.genericView.tableView.item(at: i).view?.shakeView() + return + } } - } else { - headerItem = nil - } - if let headerItem = headerItem { - let _ = strongSelf.genericView.tableView.addItem(item: headerItem) } - if options.contains(.image) || options.contains(.video) { - let _ = strongSelf.genericView.tableView.addItem(item: GeneralRowItem(initialSize.modify({$0}), height:10)) - let _ = strongSelf.genericView.tableView.addItem(item: GeneralInteractedRowItem(initialSize.modify({$0}), name: tr(.previewSenderCompressFile), type: .switchable(stateback: { [weak strongSelf] () -> Bool in - if let strongSelf = strongSelf { - return strongSelf.isNeedAsMedia - } - return true - }), action:{ [weak strongSelf] in - if let strongSelf = strongSelf { - strongSelf.isNeedAsMedia = !strongSelf.isNeedAsMedia - } - })) - + self.sent = true + self.emoji.popover?.hide() + self.closeModal() + + var input:ChatTextInputState = ChatTextInputState(inputText: attributed.string, selectionRange: 0 ..< 0, attributes: chatTextAttributes(from: attributed)).subInputState(from: NSMakeRange(0, attributed.length)) + + if input.attributes.isEmpty { + input = ChatTextInputState(inputText: input.inputText.trimmed) } - + var additionalMessage: ChatTextInputState? = nil - let _ = strongSelf.genericView.tableView.addItem(item: GeneralRowItem(initialSize.modify({$0}), height:10)) - if headerItem == nil { - strongSelf.expandUrls(urls) - } else { - strongSelf.textViewHeightChanged(34, animated: false) + if (medias.count > 1 || (medias.count == 1 && (!medias[0].canHaveCaption))) && !input.inputText.isEmpty { + if !state.isCollage { + additionalMessage = input + input = ChatTextInputState() + } } - subscriber.putNext(true) - subscriber.putCompletion() - + self.chatInteraction.sendMedias(medias, input, state.isCollage, additionalMessage, silent, atDate) } - return EmptyDisposable - } |> runOn(Queue.mainQueue()) - } - - private func expandUrls(_ urls:[URL]) { - var index:Int = -1 - let initialSize = atomicSize.modify({$0}) - var inserted:[(Int, TableRowItem)] = [] - for url in urls { - index += 1 - inserted.append((index, ExpandedPreviewRowItem(initialSize, account:account, url: url, onDelete: { [weak self] item in - if let strongSelf = self { - if let index = strongSelf.genericView.tableView.index(of: item) { - strongSelf.genericView.tableView.remove(at: index, redraw: true, animation: .effectFade) - if let urlIndex = strongSelf.urls.index(of: url) { - strongSelf.urls.remove(at: urlIndex) - } - strongSelf.updateSize() - if strongSelf.urls.isEmpty { - strongSelf.modal?.close() + + } + + self.runEditor = { [weak self] url, paint in + guard let `self` = self else { return } + + let editedData = stateValue.with { $0.editedData } + + let data = editedData[url] ?? EditedImageData(originalUrl: url) + + if paint, let image = NSImage(contentsOf: data.originalUrl) { + var paintings:[EditImageDrawTouch] = data.paintings + let image = image._cgImage! + let editor = EditImageCanvasController(image: image, actions: data.paintings, updatedImage: { updated in + paintings = updated + }, closeHandler: { [weak self] in + guard let `self` = self else {return} + let editedData = data.withUpdatedPaintings(paintings) + let new = EditedImageData.generateNewUrl(data: editedData, selectedRect: CGRect(origin: .zero, size: image.size)) |> deliverOnMainQueue + self.editorDisposable.set(new.start(next: { [weak self] new in + if let index = self?.urls.firstIndex(where: { ($0 as NSURL) === (url as NSURL) }) { + updateState { $0.withUpdatedEditedData { data in + var data = data + data[new] = editedData + if editedData.hasntData { + data.removeValue(forKey: new) + } + return data + }} + self?.urls[index] = new + addAppLogEvent(postbox: context.account.postbox, time: Date().timeIntervalSince1970, type: AppLogEvents.imageEditor.rawValue, peerId: context.peerId, data: [:]) } + })) + }, alone: true) + showModal(with: editor, for: context.window, animationType: .scaleCenter) + + } else { + let editor = EditImageModalController(data.originalUrl, defaultData: data) + showModal(with: editor, for: context.window, animationType: .scaleCenter) + self.editorDisposable.set((editor.result |> deliverOnMainQueue).start(next: { [weak self] new, editedData in + guard let `self` = self else {return} + if let index = self.urls.firstIndex(where: { ($0 as NSURL) === (url as NSURL) }) { + updateState { $0.withUpdatedEditedData { data in + var data = data + if let editedData = editedData { + data[new] = editedData + } else { + data.removeValue(forKey: new) + } + return data + }} + self.urls[index] = new + addAppLogEvent(postbox: context.account.postbox, time: Date().timeIntervalSince1970, type: AppLogEvents.imageEditor.rawValue, peerId: context.peerId, data: [:]) } - } - - }))) + })) + } } - - genericView.tableView.merge(with: TableUpdateTransition(deleted: [0], inserted: inserted, updated: [], animated: false, state: .saveVisible(.lower))) - updateSize() - genericView.tableView.scroll(to: .down(false)) - } - - private func updateSize() { - if let contentSize = self.window?.contentView?.frame.size { - self.modal?.resize(with:NSMakeSize(genericView.frame.width, min(contentSize.height - 70, genericView.tableView.listHeight + genericView.textView.frame.height)), animated: false) + + self.insertAdditionUrls = { [weak self] list in + guard let `self` = self else { return } + let previous = self.urls + _ = scrollAfterTransition.swap(true) + self.urls.append(contentsOf: list) + if previous == self.urls { + _ = scrollAfterTransition.swap(false) + NSSound.beep() + } } - } - - override var modalInteractions: ModalInteractions? { - let chatInteraction = self.chatInteraction + - return ModalInteractions(acceptTitle:tr(.modalSend), accept: { [weak self] in - if let urls = self?.urls, let asMedia = self?.isNeedAsMedia { - let text = self?.genericView.textView.string() ?? "" - var containers:[MediaSenderContainer] = [] - for url in urls { - let asMedia = asMedia && mediaExts.contains(url.path.nsstring.pathExtension.lowercased()) - containers.append(MediaSenderContainer(path:url.path, caption: urls.count == 1 ? text : "", isFile:!asMedia)) - } - if urls.count > 1 && !text.isEmpty { - chatInteraction.forceSendMessage(text) - } - chatInteraction.sendMedia(containers) + context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + guard let `self` = self else {return .rejected} + + let state = stateValue.with { $0.currentState } + let medias = stateValue.with { $0.medias } + + if state.state == .media, medias.count == 1, medias.first is TelegramMediaImage { + self.runEditor?(self.urls[0], false) } - self?.modal?.close() - }, cancelTitle: tr(.modalCancel), drawBorder: true) - } - - override var dynamicSize: Bool { - return true - } - - override func returnKeyAction() -> KeyHandlerResult { - if let currentEvent = NSApp.currentEvent { - if FastSettings.checkSendingAbility(for: currentEvent) { - self.modal?.close(true) + return .invoked + }, with: self, for: .E, priority: .high, modifierFlags: [.command]) + + + context.window.set(handler: { _ -> KeyHandlerResult in + return .invokeNext + }, with: self, for: .LeftArrow, priority: .modal) + + context.window.set(handler: { _ -> KeyHandlerResult in + return .invokeNext + }, with: self, for: .RightArrow, priority: .modal) + + context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + if self?.inputInteraction.state.inputQueryResult != nil { + return .rejected + } + return .invokeNext + }, with: self, for: .UpArrow, priority: .modal) + + context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + if self?.inputInteraction.state.inputQueryResult != nil { + return .rejected + } + return .invokeNext + }, with: self, for: .DownArrow, priority: .modal) + + self.context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + if let strongSelf = self, strongSelf.context.window.firstResponder != strongSelf.genericView.textView.inputView { + _ = strongSelf.context.window.makeFirstResponder(strongSelf.genericView.textView.inputView) return .invoked } - } + return .invoked + }, with: self, for: .Tab, priority: .modal) - return .invokeNext + + context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.genericView.textView.boldWord() + return .invoked + }, with: self, for: .B, priority: .modal, modifierFlags: [.command]) + + context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + guard let `self` = self else {return .rejected} + self.makeUrl(of: self.genericView.textView.selectedRange()) + return .invoked + }, with: self, for: .U, priority: .modal, modifierFlags: [.command]) + + context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.genericView.textView.italicWord() + return .invoked + }, with: self, for: .I, priority: .modal, modifierFlags: [.command]) + + context.window.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.genericView.textView.codeWord() + return .invoked + }, with: self, for: .K, priority: .modal, modifierFlags: [.command, .shift]) + + + context.window.set(mouseHandler: { [weak self] event -> KeyHandlerResult in + guard let `self` = self else {return .rejected} + + if !self.genericView.tableView.isEmpty, let view = self.genericView.tableView.item(at: 0).view as? MediaGroupPreviewRowView { + if view.draggingIndex != nil { + view.mouseUp(with: event) + return .invoked + } + } + return .rejected + }, with: self, for: .leftMouseUp, priority: .high) + + + + context.window.set(mouseHandler: { [weak self] event -> KeyHandlerResult in + guard let `self` = self else { return .rejected } + if !self.genericView.tableView.isEmpty, let view = self.genericView.tableView.item(at: 0).view { + view.pressureChange(with: event) + } + return .invoked + }, with: self, for: .pressure, priority: .high) + + context.window.set(mouseHandler: { [weak self] event -> KeyHandlerResult in + guard let `self` = self else {return .rejected} + + self.genericView.tableView.enumerateViews(with: { view -> Bool in + view.updateMouse() + return true + }) + + return .invokeNext + }, with: self, for: .mouseMoved, priority: .high) + + genericView.tableView.needUpdateVisibleAfterScroll = true } - override func measure(size: NSSize) { - self.modal?.resize(with:NSMakeSize(genericView.frame.width, min(size.height - 70, genericView.tableView.listHeight + genericView.textView.frame.height)), animated: false) + deinit { + inputInteraction.remove(observer: self) + disposable.dispose() + editorDisposable.dispose() + archiverStatusesDisposable.dispose() } - override func viewDidLoad() { - super.viewDidLoad() - genericView.textView.delegate = self - textViewHeightChanged(34, animated: false) - ready.set(makeItems(self.urls)) + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + closeAllPopovers(for: mainWindow) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + if !sent, let temp = temporaryInputState { + chatInteraction.update({$0.updatedInterfaceState({$0.withUpdatedInputState(temp)})}) + } + if !sent { + for (_, cached) in cachedMedia { + for media in cached.media { + if let media = media as? TelegramMediaFile, let resource = media.resource as? LocalFileArchiveMediaResource { + archiver.remove(.resource(resource)) + } + } + } + } + window?.removeAllHandlers(for: self) } override func becomeFirstResponder() -> Bool? { @@ -210,20 +1399,76 @@ class PreviewSenderController: ModalViewController, TGModernGrowingDelegate { return genericView.textView } - init(urls:[URL], account:Account, chatInteraction:ChatInteraction, asMedia:Bool = true) { - self.urls = urls - self.account = account - self.isNeedAsMedia = asMedia + private let asMedia: Bool + private let attributedString: NSAttributedString? + init(urls:[URL], chatInteraction:ChatInteraction, asMedia:Bool = true, attributedString: NSAttributedString? = nil) { + + let filtred = urls.filter { url in + return FileManager.default.fileExists(atPath: url.path) + }.uniqueElements + + self._urls = filtred + + self.attributedString = attributedString + let context = chatInteraction.context + self.asMedia = asMedia + self.context = context + self.emoji = EmojiViewController(context) + + + + self.contextChatInteraction = ChatInteraction(chatLocation: chatInteraction.chatLocation, context: context) + + inputContextHelper = InputContextHelper(chatInteraction: contextChatInteraction) self.chatInteraction = chatInteraction - super.init(frame:NSMakeRect(0,0,350,350)) + super.init(frame:NSMakeRect(0, 0, 320, mainWindow.frame.height - 80)) bar = .init(height: 0) + + + contextChatInteraction.movePeerToInput = { [weak self] peer in + if let strongSelf = self { + let string = strongSelf.genericView.textView.string() + let range = strongSelf.genericView.textView.selectedRange() + let textInputState = ChatTextInputState(inputText: string, selectionRange: range.min ..< range.max, attributes: chatTextAttributes(from: strongSelf.genericView.textView.attributedString())) + strongSelf.contextChatInteraction.update({$0.withUpdatedEffectiveInputState(textInputState)}) + if let (range, _, _) = textInputStateContextQueryRangeAndType(textInputState, includeContext: false) { + let inputText = textInputState.inputText + + let name:String = peer.addressName ?? peer.compactDisplayTitle + + let distance = inputText.distance(from: range.lowerBound, to: range.upperBound) + let replacementText = name + " " + + let atLength = peer.addressName != nil ? 0 : 1 + + let range = strongSelf.contextChatInteraction.appendText(replacementText, selectedRange: textInputState.selectionRange.lowerBound - distance - atLength ..< textInputState.selectionRange.upperBound) + + if peer.addressName == nil { + let state = strongSelf.contextChatInteraction.presentation.effectiveInput + var attributes = state.attributes + attributes.append(.uid(range.lowerBound ..< range.upperBound - 1, peer.id.id._internalGetInt64Value())) + let updatedState = ChatTextInputState(inputText: state.inputText, selectionRange: state.selectionRange, attributes: attributes) + strongSelf.contextChatInteraction.update({$0.withUpdatedEffectiveInputState(updatedState)}) + } + +// let updatedText = strongSelf.contextChatInteraction.presentation.effectiveInput +// +// strongSelf.genericView.textView.setAttributedString(updatedText.attributedString, animated: true) +// strongSelf.genericView.textView.setSelectedRange(NSMakeRange(updatedText.selectionRange.lowerBound, updatedText.selectionRange.lowerBound + updatedText.selectionRange.upperBound)) + } + } + } + + self.contextChatInteraction.add(observer: self) + self.chatInteraction.add(observer: self) } + func showEmoji(for control: Control) { + showPopover(for: control, with: emoji) + } func textViewHeightChanged(_ height: CGFloat, animated: Bool) { - // genericView.tableView.change(size: NSMakeSize(frame.width, frame.height - height), animated: animated) - modal?.resize(with:NSMakeSize(genericView.frame.width, min(mainWindow.frame.height - 80, genericView.tableView.listHeight + genericView.textView.frame.height)), animated: animated) - genericView.textView._change(pos: NSMakePoint(genericView.textView.frame.minX, frame.height - genericView.textView.frame.height), animated: animated) + updateSize(frame.width, animated: animated) } func textViewEnterPressed(_ event: NSEvent) -> Bool { @@ -233,28 +1478,219 @@ class PreviewSenderController: ModalViewController, TGModernGrowingDelegate { return false } + func textViewTextDidChange(_ string: String) { + if FastSettings.isPossibleReplaceEmojies { + let previousString = contextChatInteraction.presentation.effectiveInput.inputText + + if previousString != string { + let difference = string.replacingOccurrences(of: previousString, with: "") + if difference.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).isEmpty { + let replacedEmojies = string.stringEmojiReplacements + if string != replacedEmojies { + self.genericView.textView.setString(replacedEmojies) + } + } + } + + } + + let attributed = genericView.textView.attributedString() + let range = self.genericView.textView.selectedRange() + let state = ChatTextInputState(inputText: attributed.string, selectionRange: range.location ..< range.location + range.length, attributes: chatTextAttributes(from: attributed)) + contextChatInteraction.update({$0.withUpdatedEffectiveInputState(state)}) + } + + func isEqual(to other: Notifable) -> Bool { + return false + } + + func notify(with value: Any, oldValue: Any, animated: Bool) { + if let value = value as? PreviewContextState, let oldValue = oldValue as? PreviewContextState { + if value.inputQueryResult != oldValue.inputQueryResult { + self.updateSize(frame.width, animated: animated) + inputContextHelper.context(with: value.inputQueryResult, for: self.genericView, relativeView: self.genericView.forHelperView, animated: animated) + } + } else if let value = value as? ChatPresentationInterfaceState, let oldValue = oldValue as? ChatPresentationInterfaceState { + if value == self.contextChatInteraction.presentation { + if value.effectiveInput != oldValue.effectiveInput { + updateInput(value, prevState: oldValue, animated) + } + } else if value == self.chatInteraction.presentation { + if value.slowMode != oldValue.slowMode { + let urls = self.urls + self.urls = urls + } + } + } + } + + private func updateInput(_ state:ChatPresentationInterfaceState, prevState: ChatPresentationInterfaceState, _ animated:Bool = true) -> Void { + let textView = genericView.textView + + if textView.string() != state.effectiveInput.inputText || state.effectiveInput.attributes != prevState.effectiveInput.attributes { + textView.animates = false + textView.setAttributedString(state.effectiveInput.attributedString, animated:animated) + textView.animates = true + } + let range = NSMakeRange(state.effectiveInput.selectionRange.lowerBound, state.effectiveInput.selectionRange.upperBound - state.effectiveInput.selectionRange.lowerBound) + if textView.selectedRange().location != range.location || textView.selectedRange().length != range.length { + textView.setSelectedRange(range) + } + textViewTextDidChangeSelectedRange(range) } + func textViewTextDidChangeSelectedRange(_ range: NSRange) { + let animated: Bool = true + let string = genericView.textView.string() + + if let peer = chatInteraction.peer, !string.isEmpty, let (possibleQueryRange, possibleTypes, _) = textInputStateContextQueryRangeAndType(ChatTextInputState(inputText: string, selectionRange: range.min ..< range.max, attributes: []), includeContext: false) { + + if (possibleTypes.contains(.mention) && (peer.isGroup || peer.isSupergroup)) || possibleTypes.contains(.emoji) || possibleTypes.contains(.emojiFast) { + let query = String(string[possibleQueryRange]) + if let (updatedContextQueryState, updatedContextQuerySignal) = chatContextQueryForSearchMention(chatLocations: [chatInteraction.chatLocation], possibleTypes.contains(.emoji) ? .emoji(query, firstWord: false) : possibleTypes.contains(.emojiFast) ? .emoji(query, firstWord: true) : .mention(query: query, includeRecent: false), currentQuery: self.contextQueryState?.0, context: context, filter: .filterSelf(includeNameless: true, includeInlineBots: false)) { + self.contextQueryState?.1.dispose() + var inScope = true + var inScopeResult: ((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?)? + self.contextQueryState = (updatedContextQueryState, (updatedContextQuerySignal |> deliverOnMainQueue).start(next: { [weak self] result in + if let strongSelf = self { + if Thread.isMainThread && inScope { + inScope = false + inScopeResult = result + } else { + strongSelf.inputInteraction.update(animated: animated, { + $0.updatedInputQueryResult { previousResult in + return result(previousResult) + } + }) + + } + } + })) + inScope = false + if let inScopeResult = inScopeResult { + inputInteraction.update(animated: animated, { + $0.updatedInputQueryResult { previousResult in + return inScopeResult(previousResult) + } + }) + } + } + } else { + inputInteraction.update(animated: animated, { + $0.updatedInputQueryResult { _ in + return nil + } + }) + } + + + } else { + inputInteraction.update(animated: animated, { + $0.updatedInputQueryResult { _ in + return nil + } + }) + } + + let attributed = self.genericView.textView.attributedString() + + let state = ChatTextInputState(inputText: attributed.string, selectionRange: range.location ..< range.location + range.length, attributes: chatTextAttributes(from: attributed)) + contextChatInteraction.update({$0.withUpdatedEffectiveInputState(state)}) + + } + + func textViewDidReachedLimit(_ textView: Any) { + genericView.textView.shake() + } + + func canTransformInputText() -> Bool { + return true + } + + + func makeUrl(of range: NSRange) { + guard range.min != range.max, let window = window else { + return + } + var effectiveRange:NSRange = NSMakeRange(NSNotFound, 0) + let defaultTag: TGInputTextTag? = genericView.textView.attributedString().attribute(NSAttributedString.Key(rawValue: TGCustomLinkAttributeName), at: range.location, effectiveRange: &effectiveRange) as? TGInputTextTag + + let defaultUrl = defaultTag?.attachment as? String + + if defaultUrl == nil { + effectiveRange = range + } + if effectiveRange.location == NSNotFound { + effectiveRange = range + } + + showModal(with: InputURLFormatterModalController(string: self.genericView.textView.string().nsstring.substring(with: effectiveRange), defaultUrl: defaultUrl, completion: { [weak self] url in + self?.genericView.textView.addLink(url, range: effectiveRange) + }), for: window) } func textViewDidPaste(_ pasteboard: NSPasteboard) -> Bool { - return false + + let result = InputPasteboardParser.canProccessPasteboard(pasteboard) + + if let data = pasteboard.data(forType: .rtfd) ?? pasteboard.data(forType: .rtf) { + if let attributed = (try? NSAttributedString(data: data, options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.rtfd], documentAttributes: nil)) ?? (try? NSAttributedString(data: data, options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.rtf], documentAttributes: nil)) { + + let (attributed, attachments) = attributed.applyRtf() + let current = genericView.textView.attributedString().copy() as! NSAttributedString + let currentRange = genericView.textView.selectedRange() + let (attributedString, range) = current.appendAttributedString(attributed.attributedSubstring(from: NSMakeRange(0, min(Int(self.maxCharactersLimit(genericView.textView)), attributed.length))), selectedRange: currentRange) + let item = SimpleUndoItem(attributedString: current, be: attributedString, wasRange: currentRange, be: range) + genericView.textView.addSimpleItem(item) + + if !attachments.isEmpty { + pasteDisposable.set((prepareTextAttachments(attachments) |> deliverOnMainQueue).start(next: { [weak self] urls in + if !urls.isEmpty { + self?.insertAdditionUrls?(urls) + } + })) + } + return true + } + } + + if !result { + self.pasteDisposable.set(InputPasteboardParser.getPasteboardUrls(pasteboard).start(next: { [weak self] urls in + self?.insertAdditionUrls?(urls) + })) + } + + return !result + } + + func copyText(withRTF rtf: NSAttributedString!) -> Bool { + return globalLinkExecutor.copyAttributedString(rtf) } - func textViewSize() -> NSSize { - return NSMakeSize(frame.width - 40, genericView.textView.frame.height) + func textViewSize(_ textView: TGModernGrowingTextView!) -> NSSize { + return NSMakeSize(textView.frame.width, textView.frame.height) } func textViewIsTypingEnabled() -> Bool { return true } - func maxCharactersLimit() -> Int32 { - return 200 + func maxCharactersLimit(_ textView: TGModernGrowingTextView!) -> Int32 { + return ChatPresentationInterfaceState.maxInput + } + + override func viewClass() -> AnyClass { + return PreviewSenderView.self + } + + + + override func didResizeView(_ size: NSSize, animated: Bool) { + self.genericView.updateHeight(self.genericView.textView.frame.height, animated) } } diff --git a/Telegram-Mac/PreviewSenderItems.swift b/Telegram-Mac/PreviewSenderItems.swift deleted file mode 100644 index 237289dfd3..0000000000 --- a/Telegram-Mac/PreviewSenderItems.swift +++ /dev/null @@ -1,334 +0,0 @@ -// -// PreviewSenderItems.swift -// TelegramMac -// -// Created by keepcoder on 11/01/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - -import Cocoa -import TelegramCoreMac -import TGUIKit -import SwiftSignalKitMac -class PreviewDocumentRowItem: TableRowItem { - let url:URL - let account:Account - let thumb:CGImage - let name:(TextNodeLayout, TextNode) - init(_ initialSize:NSSize, url:URL, account:Account) { - self.url = url - self.thumb = extensionImage(fileExtension: url.pathExtension.isEmpty ? "F" : url.pathExtension)! - self.account = account - self.name = TextNode.layoutText(maybeNode: nil, .initialize(string: url.path.nsstring.lastPathComponent, color: theme.colors.text, font: .normal(.text)), nil, 1, .end, NSMakeSize(initialSize.width - (30 + 40 + 10 + 30), 20), nil, false, .left) - super.init(initialSize) - } - private let _stableId = Int64(arc4random()) - override var stableId: AnyHashable { - return _stableId - } - - override var height: CGFloat { - return 100 - } - - override func viewClass() -> AnyClass { - return PreviewDocumentRowView.self - } -} - -class PreviewDocumentRowView : TableRowView { - - var imageView:ImageView = ImageView() - - required init(frame frameRect: NSRect) { - super.init(frame: frameRect) - addSubview(imageView) - } - - - override func set(item: TableRowItem, animated: Bool) { - super.set(item: item,animated:animated) - - if let item = item as? PreviewDocumentRowItem { - imageView.image = item.thumb - } - - } - - override func draw(_ layer: CALayer, in ctx: CGContext) { - super.draw(layer, in: ctx) - - if let item = item as? PreviewDocumentRowItem { - let f = focus(item.name.0.size) - item.name.1.draw(NSMakeRect(80, f.minY, item.name.0.size.width, item.name.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor) - } - } - - override func layout() { - super.layout() - if let item = item as? PreviewDocumentRowItem { - imageView.setFrameSize(item.thumb.backingSize) - imageView.centerY(x:30) - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - - - - -class PreviewThumbRowItem :TableRowItem { - let url:URL - let account:Account - let thumbSize:NSSize - init(_ initialSize:NSSize, url:URL, account:Account) { - self.url = url - self.account = account - self.thumbSize = NSImage(contentsOf: url)?.size ?? NSZeroSize - - super.init(initialSize) - } - private let _stableId = Int64(arc4random()) - override var stableId: AnyHashable { - return _stableId - } - - override var height: CGFloat { - return 140 - } - - override func viewClass() -> AnyClass { - return PreviewThumbRowView.self - } -} - -class PreviewThumbRowView : TableRowView { - - var imageView:TransformImageView = TransformImageView() - - required init(frame frameRect: NSRect) { - super.init(frame: frameRect) - addSubview(imageView) - } - - - - override func set(item: TableRowItem, animated: Bool) { - super.set(item: item,animated:animated) - - if let item = item as? PreviewThumbRowItem { - imageView.setSignal(account: item.account, signal: filethumb(with: item.url, account:item.account, scale: backingScaleFactor)) - } - - } - - override func layout() { - super.layout() - if let item = item as? PreviewThumbRowItem { - - let boundingSize = NSMakeSize(frame.size.width - 20, frame.size.height - 20) - - let imageSize = item.thumbSize.aspectFitted(boundingSize) - let arguments = TransformImageArguments(corners: ImageCorners(radius:.cornerRadius), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: NSEdgeInsets()) - - imageView.setFrameSize(arguments.imageSize) - imageView.center() - imageView.set(arguments: arguments) - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - - -//enum MultiplePreviewSetting { -// case files -// case images -// case mixed -//} -// -//class MultiplePreviewRowItem :TableRowItem { -// let urls:[URL] -// let account:Account -// let textLayout:TextViewLayout -// init(_ initialSize:NSSize, urls:[URL], options:[PreviewOptions], account:Account, onExpand:@escaping()->Void) { -// self.urls = urls -// self.account = account -// -// let text:String -// if options.contains(.mixed) { -// text = tr(.previewSenderSendMediaFilesCountable(urls.count)) -// } else if options.contains(.image) { -// text = tr(.previewSenderSendImagesCountable(urls.count)) -// } else if options.contains(.video) { -// text = tr(.previewSenderSendVideosCountable(urls.count)) -// } else { -// text = tr(.previewSenderSendFilesCountable(urls.count)) -// } -// // let text:String = localizedString(localizedKey, countable:urls.count) -// let attr = NSMutableAttributedString() -// _ = attr.append(string: text, color: .text, font: .normal(.title)) -// _ = attr.append(string: " (", color: .text, font: .normal(.title)) -// let range = attr.append(string: tr(.previewSenderExpandItems), color: .link, font: .normal(.title)) -// -// attr.add(link: "expand", for: range) -// _ = attr.append(string: ")", color: .text, font: .normal(.title)) -// textLayout = TextViewLayout(attr) -// textLayout.measure(width: initialSize.width - 60) -// textLayout.interactions = TextViewInteractions(processURL: { (any) in -// onExpand() -// }) -// super.init(initialSize) -// } -// -// override func makeSize(_ width: CGFloat, oldWidth:CGFloat) -> Bool { -// textLayout.measure(width: width - 60) -// return super.makeSize(width, oldWidth: oldWidth) -// } -// -// private let _stableId = Int64(arc4random()) -// override var stableId: AnyHashable { -// return _stableId -// } -// -// override var height: CGFloat { -// return 60 -// } -// -// override func viewClass() -> AnyClass { -// return MultiplePreviewRowView.self -// } -//} -// -//class MultiplePreviewRowView : TableRowView { -// private let textView:TextView = TextView() -// required init(frame frameRect: NSRect) { -// super.init(frame: frameRect) -// addSubview(textView) -// } -// -// override func layout() { -// super.layout() -// if let item = item as? MultiplePreviewRowItem { -// textView.update(item.textLayout) -// textView.centerY(x: 30) -// } -// } -// -// required init?(coder: NSCoder) { -// fatalError("init(coder:) has not been implemented") -// } -//} - - -class ExpandedPreviewRowItem : TableRowItem { - fileprivate let onDelete:(ExpandedPreviewRowItem)->Void - fileprivate let url:URL - fileprivate let textLayout:TextViewLayout - fileprivate let thumbSize:NSSize - fileprivate let account:Account - fileprivate let thumb:CGImage? - init(_ initialSize: NSSize, account:Account, url:URL, onDelete:@escaping(ExpandedPreviewRowItem)->Void) { - self.onDelete = onDelete - self.url = url - self.account = account - self.textLayout = TextViewLayout(.initialize(string: url.path.nsstring.lastPathComponent, color: theme.colors.grayText, font: NSFont.normal(FontSize.text)), maximumNumberOfLines: 2, truncationType: .middle) - self.textLayout.measure(width: initialSize.width - 60 - 20 - 40 - 10 - 10) - let mimeType = MIMEType(url.pathExtension.lowercased()) - if mimeType.hasPrefix("image"), let image = NSImage(contentsOf: url) { - self.thumbSize = image.size.aspectFilled(NSMakeSize(40, 40)) - self.thumb = nil - } else { - self.thumbSize = NSMakeSize(40, 40) - self.thumb = extensionImage(fileExtension: url.path.nsstring.pathExtension.lowercased())! - } - - super.init(initialSize) - } - - override var stableId: AnyHashable { - return url.hashValue - } - - override var height: CGFloat { - return 50 //max(thumbSize.height + 20,50) - } - - override func viewClass() -> AnyClass { - return ExpandedPreviewRowView.self - } -} - -class ExpandedPreviewRowView : TableRowView { - private let textView:TextView = TextView() - private let deleteControl:ImageButton = ImageButton() - private let imageView:TransformImageView = TransformImageView() - private let thumbView:ImageView = ImageView() - private var arguments:TransformImageArguments? - required init(frame frameRect: NSRect) { - super.init(frame: frameRect) - - deleteControl.autohighlight = false - - - deleteControl.set(handler: { [weak self] _ in - if let item = self?.item as? ExpandedPreviewRowItem { - item.onDelete(item) - } - }, for: .Click) - textView.isSelectable = false - addSubview(textView) - addSubview(deleteControl) - addSubview(imageView) - addSubview(thumbView) - } - - override func layout() { - super.layout() - if let item = item as? ExpandedPreviewRowItem { - deleteControl.centerY(x:30) - - - let arguments = TransformImageArguments(corners: ImageCorners(radius: 20), imageSize: item.thumbSize, boundingSize: NSMakeSize(40, 40), intrinsicInsets: NSEdgeInsets()) - - - imageView.setFrameSize(arguments.boundingSize) - imageView.set(arguments: arguments) - - thumbView.setFrameSize(arguments.boundingSize) - thumbView.centerY(x:deleteControl.frame.maxX + 10) - imageView.centerY(x:deleteControl.frame.maxX + 10 + floorToScreenPixels((40 - imageView.frame.width)/2)) - - textView.update(item.textLayout) - textView.centerY(x: deleteControl.frame.maxX + 40 + 20) - } - } - - override func set(item: TableRowItem, animated: Bool) { - super.set(item: item, animated: animated) - if let item = item as? ExpandedPreviewRowItem { - deleteControl.set(image: theme.icons.deleteItem, for: .Normal) - deleteControl.sizeToFit() - textView.backgroundColor = theme.colors.background - imageView.dispose() - if let thumb = item.thumb { - thumbView.image = thumb - } else { - imageView.setSignal(account: item.account, signal: filethumb(with: item.url, account:item.account, scale: backingScaleFactor)) - } - thumbView.isHidden = item.thumb == nil - imageView.isHidden = !thumbView.isHidden - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - -} diff --git a/Telegram-Mac/PrivacyAndSecurityViewController.swift b/Telegram-Mac/PrivacyAndSecurityViewController.swift index d08c26c4b7..8a638fefed 100644 --- a/Telegram-Mac/PrivacyAndSecurityViewController.swift +++ b/Telegram-Mac/PrivacyAndSecurityViewController.swift @@ -8,23 +8,110 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac +import TelegramCore + +import SwiftSignalKit +import Postbox + +/* + + struct InteractiveEmojiConfiguration : Equatable { + static var defaultValue: InteractiveEmojiConfiguration { + return InteractiveEmojiConfiguration(emojis: [], confettiCompitable: [:]) + } + + let emojis: [String] + private let confettiCompitable: [String: InteractiveEmojiConfetti] + + fileprivate init(emojis: [String], confettiCompitable: [String: InteractiveEmojiConfetti]) { + self.emojis = emojis.map { $0.fixed } + self.confettiCompitable = confettiCompitable + } + + static func with(appConfiguration: AppConfiguration) -> InteractiveEmojiConfiguration { + if let data = appConfiguration.data, let value = data["emojies_send_dice"] as? [String] { + let dict:[String : Any]? = data["emojies_send_dice_success"] as? [String:Any] + + var confetti:[String: InteractiveEmojiConfetti] = [:] + if let dict = dict { + for (key, value) in dict { + if let data = value as? [String: Any], let frameStart = data["frame_start"] as? Double, let value = data["value"] as? Double { + confetti[key] = InteractiveEmojiConfetti(playAt: Int32(frameStart), value: Int32(value)) + } + } + } + return InteractiveEmojiConfiguration(emojis: value, confettiCompitable: confetti) + } else { + return .defaultValue + } + } + + func playConfetti(_ emoji: String) -> InteractiveEmojiConfetti? { + return confettiCompitable[emoji] + } + } + */ + +private struct AutoarchiveConfiguration : Equatable { + let autoarchive_setting_available: Bool + init(autoarchive_setting_available: Bool) { + self.autoarchive_setting_available = autoarchive_setting_available + } + static func with(appConfiguration: AppConfiguration) -> AutoarchiveConfiguration { + return AutoarchiveConfiguration(autoarchive_setting_available: appConfiguration.data?["autoarchive_setting_available"] as? Bool ?? false) + } +} + + +enum PrivacyAndSecurityEntryTag: ItemListItemTag { + case accountTimeout + case topPeers + case cloudDraft + case autoArchive + func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? PrivacyAndSecurityEntryTag, self == other { + return true + } else { + return false + } + } + + fileprivate var stableId: AnyHashable { + switch self { + case .accountTimeout: + return PrivacyAndSecurityEntry.accountTimeout(sectionId: 0, "", viewType: .singleItem).stableId + case .topPeers: + return PrivacyAndSecurityEntry.togglePeerSuggestions(sectionId: 0, enabled: false, viewType: .singleItem).stableId + case .cloudDraft: + return PrivacyAndSecurityEntry.clearCloudDrafts(sectionId: 0, viewType: .singleItem).stableId + case .autoArchive: + return PrivacyAndSecurityEntry.autoArchiveToggle(sectionId: 0, value: false, viewType: .singleItem).stableId + } + } +} private final class PrivacyAndSecurityControllerArguments { - let account: Account + let context: AccountContext let openBlockedUsers: () -> Void let openLastSeenPrivacy: () -> Void let openGroupsPrivacy: () -> Void let openVoiceCallPrivacy: () -> Void + let openProfilePhotoPrivacy: () -> Void + let openForwardPrivacy: () -> Void + let openPhoneNumberPrivacy: () -> Void let openPasscode: () -> Void - let openTwoStepVerification: () -> Void - let openActiveSessions: () -> Void + let openTwoStepVerification: (TwoStepVeriticationAccessConfiguration?) -> Void + let openActiveSessions: ([RecentAccountSession]?) -> Void + let openWebAuthorizations: () -> Void let setupAccountAutoremove: () -> Void let openProxySettings:() ->Void - init(account: Account, openBlockedUsers: @escaping () -> Void, openLastSeenPrivacy: @escaping () -> Void, openGroupsPrivacy: @escaping () -> Void, openVoiceCallPrivacy: @escaping () -> Void, openPasscode: @escaping () -> Void, openTwoStepVerification: @escaping () -> Void, openActiveSessions: @escaping () -> Void, setupAccountAutoremove: @escaping () -> Void, openProxySettings:@escaping() ->Void) { - self.account = account + let togglePeerSuggestions:(Bool)->Void + let clearCloudDrafts: () -> Void + let toggleSensitiveContent:(Bool)->Void + let toggleSecretChatWebPreview: (Bool)->Void + let toggleAutoArchive: (Bool)->Void + init(context: AccountContext, openBlockedUsers: @escaping () -> Void, openLastSeenPrivacy: @escaping () -> Void, openGroupsPrivacy: @escaping () -> Void, openVoiceCallPrivacy: @escaping () -> Void, openProfilePhotoPrivacy: @escaping () -> Void, openForwardPrivacy: @escaping () -> Void, openPhoneNumberPrivacy: @escaping() -> Void, openPasscode: @escaping () -> Void, openTwoStepVerification: @escaping (TwoStepVeriticationAccessConfiguration?) -> Void, openActiveSessions: @escaping ([RecentAccountSession]?) -> Void, openWebAuthorizations: @escaping() -> Void, setupAccountAutoremove: @escaping () -> Void, openProxySettings:@escaping() ->Void, togglePeerSuggestions:@escaping(Bool)->Void, clearCloudDrafts: @escaping() -> Void, toggleSensitiveContent: @escaping(Bool)->Void, toggleSecretChatWebPreview: @escaping(Bool)->Void, toggleAutoArchive: @escaping(Bool)->Void) { + self.context = context self.openBlockedUsers = openBlockedUsers self.openLastSeenPrivacy = openLastSeenPrivacy self.openGroupsPrivacy = openGroupsPrivacy @@ -32,100 +119,202 @@ private final class PrivacyAndSecurityControllerArguments { self.openPasscode = openPasscode self.openTwoStepVerification = openTwoStepVerification self.openActiveSessions = openActiveSessions + self.openWebAuthorizations = openWebAuthorizations self.setupAccountAutoremove = setupAccountAutoremove self.openProxySettings = openProxySettings + self.togglePeerSuggestions = togglePeerSuggestions + self.clearCloudDrafts = clearCloudDrafts + self.openProfilePhotoPrivacy = openProfilePhotoPrivacy + self.openForwardPrivacy = openForwardPrivacy + self.openPhoneNumberPrivacy = openPhoneNumberPrivacy + self.toggleSensitiveContent = toggleSensitiveContent + self.toggleSecretChatWebPreview = toggleSecretChatWebPreview + self.toggleAutoArchive = toggleAutoArchive } } private enum PrivacyAndSecurityEntry: Comparable, Identifiable { case privacyHeader(sectionId:Int) - case blockedPeers(sectionId:Int) - case lastSeenPrivacy(sectionId: Int, String) - case groupPrivacy(sectionId: Int, String) - case voiceCallPrivacy(sectionId: Int, String) + case blockedPeers(sectionId:Int, Int?, viewType: GeneralViewType) + case phoneNumberPrivacy(sectionId: Int, String, viewType: GeneralViewType) + case lastSeenPrivacy(sectionId: Int, String, viewType: GeneralViewType) + case groupPrivacy(sectionId: Int, String, viewType: GeneralViewType) + case profilePhotoPrivacy(sectionId: Int, String, viewType: GeneralViewType) + case forwardPrivacy(sectionId: Int, String, viewType: GeneralViewType) + case voiceCallPrivacy(sectionId: Int, String, viewType: GeneralViewType) case securityHeader(sectionId:Int) - case passcode(sectionId:Int) - case twoStepVerification(sectionId:Int) - case activeSessions(sectionId:Int) + case passcode(sectionId:Int, enabled: Bool, viewType: GeneralViewType) + case twoStepVerification(sectionId:Int, configuration: TwoStepVeriticationAccessConfiguration?, viewType: GeneralViewType) + case activeSessions(sectionId:Int, [RecentAccountSession]?, viewType: GeneralViewType) + case webAuthorizationsHeader(sectionId: Int) + case webAuthorizations(sectionId:Int, viewType: GeneralViewType) case accountHeader(sectionId:Int) - case accountTimeout(sectionId: Int, String) + case accountTimeout(sectionId: Int, String, viewType: GeneralViewType) case accountInfo(sectionId:Int) case proxyHeader(sectionId:Int) - case proxySettings(sectionId:Int, String) - case section(sectionId:Int) + case proxySettings(sectionId:Int, String, viewType: GeneralViewType) + case togglePeerSuggestions(sectionId: Int, enabled: Bool, viewType: GeneralViewType) + case togglePeerSuggestionsDesc(sectionId: Int) + case sensitiveContentHeader(sectionId: Int) + case autoArchiveToggle(sectionId: Int, value: Bool?, viewType: GeneralViewType) + case autoArchiveDesc(sectionId: Int) + case autoArchiveHeader(sectionId: Int) + case sensitiveContentToggle(sectionId: Int, value: Bool?, viewType: GeneralViewType) + case sensitiveContentDesc(sectionId: Int) + case clearCloudDraftsHeader(sectionId: Int) + case clearCloudDrafts(sectionId: Int, viewType: GeneralViewType) + + case secretChatWebPreviewHeader(sectionId: Int) + case secretChatWebPreviewToggle(sectionId: Int, value: Bool?, viewType: GeneralViewType) + case secretChatWebPreviewDesc(sectionId: Int) + case section(sectionId:Int) + var sectionId: Int { switch self { case let .privacyHeader(sectionId): return sectionId - case let .blockedPeers(sectionId): + case let .blockedPeers(sectionId, _, _): + return sectionId + case let .phoneNumberPrivacy(sectionId, _, _): + return sectionId + case let .lastSeenPrivacy(sectionId, _, _): + return sectionId + case let .groupPrivacy(sectionId, _, _): return sectionId - case let .lastSeenPrivacy(sectionId, _): + case let .profilePhotoPrivacy(sectionId, _, _): return sectionId - case let .groupPrivacy(sectionId, _): + case let .forwardPrivacy(sectionId, _, _): return sectionId - case let .voiceCallPrivacy(sectionId, _): + case let .voiceCallPrivacy(sectionId, _, _): return sectionId case let .securityHeader(sectionId): return sectionId - case let .passcode(sectionId): + case let .passcode(sectionId, _, _): + return sectionId + case let .twoStepVerification(sectionId, _, _): + return sectionId + case let .activeSessions(sectionId, _, _): + return sectionId + case let .webAuthorizationsHeader(sectionId): + return sectionId + case let .webAuthorizations(sectionId, _): return sectionId - case let .twoStepVerification(sectionId): + case let .autoArchiveHeader(sectionId): return sectionId - case let .activeSessions(sectionId): + case let .autoArchiveToggle(sectionId, _, _): + return sectionId + case let .autoArchiveDesc(sectionId): return sectionId case let .accountHeader(sectionId): return sectionId - case let .accountTimeout(sectionId, _): + case let .accountTimeout(sectionId, _, _): return sectionId case let .accountInfo(sectionId): return sectionId - case let .proxySettings(sectionId, _): + case let .togglePeerSuggestions(sectionId, _, _): + return sectionId + case let .togglePeerSuggestionsDesc(sectionId): + return sectionId + case let .clearCloudDraftsHeader(sectionId): + return sectionId + case let .clearCloudDrafts(sectionId, _): return sectionId case let .proxyHeader(sectionId): return sectionId + case let .proxySettings(sectionId, _, _): + return sectionId + case let .sensitiveContentHeader(sectionId): + return sectionId + case let .sensitiveContentToggle(sectionId, _, _): + return sectionId + case let .sensitiveContentDesc(sectionId): + return sectionId + case let .secretChatWebPreviewHeader(sectionId): + return sectionId + case let .secretChatWebPreviewToggle(sectionId, _, _): + return sectionId + case let .secretChatWebPreviewDesc(sectionId): + return sectionId case let .section(sectionId): return sectionId } } + var stableId:Int { switch self { - case .privacyHeader: - return 0 case .blockedPeers: + return 0 + case .activeSessions: return 1 - case .lastSeenPrivacy: + case .passcode: return 2 - case .groupPrivacy: + case .twoStepVerification: return 3 - case .voiceCallPrivacy: + case .privacyHeader: return 4 - case .securityHeader: + case .phoneNumberPrivacy: return 5 - case .passcode: + case .lastSeenPrivacy: return 6 - case .twoStepVerification: + case .groupPrivacy: return 7 - case .activeSessions: + case .voiceCallPrivacy: return 8 - case .accountHeader: + case .forwardPrivacy: return 9 - case .accountTimeout: + case .profilePhotoPrivacy: return 10 - case .accountInfo: + case .securityHeader: return 11 - case .proxyHeader: + case .autoArchiveHeader: return 12 - case .proxySettings: + case .autoArchiveToggle: return 13 + case .autoArchiveDesc: + return 14 + case .accountHeader: + return 15 + case .accountTimeout: + return 16 + case .accountInfo: + return 17 + case .webAuthorizationsHeader: + return 18 + case .webAuthorizations: + return 19 + case .proxyHeader: + return 20 + case .proxySettings: + return 21 + case .togglePeerSuggestions: + return 22 + case .togglePeerSuggestionsDesc: + return 23 + case .clearCloudDraftsHeader: + return 24 + case .clearCloudDrafts: + return 25 + case .sensitiveContentHeader: + return 26 + case .sensitiveContentToggle: + return 27 + case .sensitiveContentDesc: + return 28 + case .secretChatWebPreviewHeader: + return 29 + case .secretChatWebPreviewToggle: + return 30 + case .secretChatWebPreviewDesc: + return 31 case let .section(sectionId): return (sectionId + 1) * 1000 - sectionId } } - - + + private var stableIndex:Int { switch self { case let .section(sectionId): @@ -135,171 +324,372 @@ private enum PrivacyAndSecurityEntry: Comparable, Identifiable { } } - - static func ==(lhs: PrivacyAndSecurityEntry, rhs: PrivacyAndSecurityEntry) -> Bool { - switch lhs { - case .privacyHeader, .blockedPeers, .securityHeader, .passcode, .twoStepVerification, .activeSessions, .accountHeader, .accountInfo, .proxyHeader, .section: - return lhs.stableId == rhs.stableId && lhs.sectionId == rhs.sectionId - case let .lastSeenPrivacy(sectionId, text): - if case .lastSeenPrivacy(sectionId, text) = rhs { - return true - } else { - return false - } - case let .groupPrivacy(sectionId, text): - if case .groupPrivacy(sectionId, text) = rhs { - return true - } else { - return false - } - case let .proxySettings(sectionId, text): - if case .proxySettings(sectionId, text) = rhs { - return true - } else { - return false - } - case let .voiceCallPrivacy(sectionId, text): - if case .voiceCallPrivacy(sectionId, text) = rhs { - return true - } else { - return false - } - case let .accountTimeout(sectionId, text): - if case .accountTimeout(sectionId, text) = rhs { - return true - } else { - return false - } - } - } - + static func <(lhs: PrivacyAndSecurityEntry, rhs: PrivacyAndSecurityEntry) -> Bool { return lhs.stableIndex < rhs.stableIndex } func item(_ arguments: PrivacyAndSecurityControllerArguments, initialSize: NSSize) -> TableRowItem { switch self { case .privacyHeader: - return GeneralTextRowItem(initialSize, stableId: stableId, text: tr(.privacySettingsPrivacyHeader), drawCustomSeparator: true, inset: NSEdgeInsets(left: 30.0, right: 30.0, top:2, bottom:6)) - case .blockedPeers: - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.privacySettingsBlockedUsers), type: .next, action: { + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.privacySettingsPrivacyHeader, viewType: .textTopItem) + case let .blockedPeers(_, count, viewType): + let text: String + if let count = count, count > 0 { + text = L10n.privacyAndSecurityBlockedUsers("\(count)") + } else { + text = "" + } + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.privacySettingsBlockedUsers, icon: theme.icons.privacySettings_blocked, type: .nextContext(text), viewType: viewType, action: { arguments.openBlockedUsers() }) - case let .lastSeenPrivacy(_, text): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.privacySettingsLastSeen), type: .next, action: { + case let .phoneNumberPrivacy(_, text, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.privacySettingsPhoneNumber, type: .nextContext(text), viewType: viewType, action: { + arguments.openPhoneNumberPrivacy() + }) + case let .lastSeenPrivacy(_, text, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.privacySettingsLastSeen, type: .nextContext(text), viewType: viewType, action: { arguments.openLastSeenPrivacy() }) - case let .groupPrivacy(_, text): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.privacySettingsGroups), type: .next, action: { + case let .groupPrivacy(_, text, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.privacySettingsGroups, type: .nextContext(text), viewType: viewType, action: { arguments.openGroupsPrivacy() }) - case let .voiceCallPrivacy(_, text): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.privacySettingsVoiceCalls), type: .next, action: { + case let .profilePhotoPrivacy(_, text, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.privacySettingsProfilePhoto, type: .nextContext(text), viewType: viewType, action: { + arguments.openProfilePhotoPrivacy() + }) + case let .forwardPrivacy(_, text, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.privacySettingsForwards, type: .nextContext(text), viewType: viewType, action: { + arguments.openForwardPrivacy() + }) + case let .voiceCallPrivacy(_, text, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.privacySettingsVoiceCalls, type: .nextContext(text), viewType: viewType, action: { arguments.openVoiceCallPrivacy() }) case .securityHeader: - return GeneralTextRowItem(initialSize, stableId: stableId, text: tr(.privacySettingsSecurityHeader), drawCustomSeparator: true, inset: NSEdgeInsets(left: 30.0, right: 30.0, top:2, bottom:6)) - case .passcode: - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.privacySettingsPasscode), action: { + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.privacySettingsSecurityHeader, viewType: .textTopItem) + case let .passcode(_, enabled, viewType): + let desc = enabled ? L10n.privacyAndSecurityItemOn : L10n.privacyAndSecurityItemOff + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.privacySettingsPasscode, icon: theme.icons.privacySettings_passcode, type: .nextContext(desc), viewType: viewType, action: { arguments.openPasscode() }) - case .twoStepVerification: - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.privacySettingsTwoStepVerification), action: { - arguments.openTwoStepVerification() + case let .twoStepVerification(_, configuration, viewType): + let desc: String + if let configuration = configuration { + switch configuration { + case .set: + desc = L10n.privacyAndSecurityItemOn + case .notSet: + desc = L10n.privacyAndSecurityItemOff + } + } else { + desc = "" + } + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.privacySettingsTwoStepVerification, icon: theme.icons.privacySettings_twoStep, type: .nextContext(desc), viewType: viewType, action: { + arguments.openTwoStepVerification(configuration) }) - case .activeSessions: - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.privacySettingsActiveSessions), action: { - arguments.openActiveSessions() + case let .activeSessions(_, sessions, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.privacySettingsActiveSessions, icon: theme.icons.privacySettings_activeSessions, type: .nextContext(sessions != nil ? "\(sessions!.count)" : ""), viewType: viewType, action: { + arguments.openActiveSessions(sessions) + }) + case .webAuthorizationsHeader: + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.privacyAndSecurityWebAuthorizationHeader, viewType: .textTopItem) + case let .webAuthorizations(_, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.telegramWebSessionsController, viewType: viewType, action: { + arguments.openWebAuthorizations() }) case .accountHeader: - return GeneralTextRowItem(initialSize, stableId: stableId, text: "tr(.privacySettingsDeleteAccountHeader)", drawCustomSeparator: true, inset: NSEdgeInsets(left: 30.0, right: 30.0, top:2, bottom:6)) - case let .accountTimeout(_, text): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: "tr(.privacySettingsDeleteAccount)", action: { + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.privacySettingsDeleteAccountHeader, viewType: .textTopItem) + case let .accountTimeout(_, text, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.privacySettingsDeleteAccount, type: .context(text), viewType: viewType, action: { arguments.setupAccountAutoremove() }) case .accountInfo: - return GeneralTextRowItem(initialSize, stableId: stableId, text: "tr(.privacySettingsDeleteAccountDescription)") + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.privacySettingsDeleteAccountDescription, viewType: .textBottomItem) case .proxyHeader: - return GeneralTextRowItem(initialSize, stableId: stableId, text: tr(.privacySettingsProxyHeader), drawCustomSeparator: true, inset: NSEdgeInsets(left: 30.0, right: 30.0, top:2, bottom:6)) - case let .proxySettings(_, text): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.privacySettingsUseProxy), type: .context(stateback: { () -> String in - return text - }), action: { + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.privacySettingsProxyHeader, viewType: .textTopItem) + case let .proxySettings(_, text, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.privacySettingsUseProxy, type: .nextContext(text), viewType: viewType, action: { arguments.openProxySettings() }) - case .section : - return GeneralRowItem(initialSize, height:20, stableId: stableId) + case let .togglePeerSuggestions(_, enabled, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.suggestFrequentContacts, type: .switchable(enabled), viewType: viewType, action: { + if enabled { + confirm(for: mainWindow, information: L10n.suggestFrequentContactsAlert, successHandler: { _ in + arguments.togglePeerSuggestions(!enabled) + }) + } else { + arguments.togglePeerSuggestions(!enabled) + } + }, autoswitch: false) + case .togglePeerSuggestionsDesc: + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.suggestFrequentContactsDesc, viewType: .textBottomItem) + case .clearCloudDraftsHeader: + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.privacyAndSecurityClearCloudDraftsHeader, viewType: .textTopItem) + case let .clearCloudDrafts(_, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.privacyAndSecurityClearCloudDrafts, type: .none, viewType: viewType, action: { + arguments.clearCloudDrafts() + }) + case .autoArchiveHeader: + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.privacyAndSecurityAutoArchiveHeader, viewType: .textTopItem) + case let .autoArchiveToggle(_, enabled, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.privacyAndSecurityAutoArchiveText, type: enabled != nil ? .switchable(enabled!) : .loading, viewType: viewType, action: { + if let enabled = enabled { + arguments.toggleAutoArchive(!enabled) + } else { + arguments.toggleAutoArchive(true) + } + }, autoswitch: true) + case .autoArchiveDesc: + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.privacyAndSecurityAutoArchiveDesc, viewType: .textBottomItem) + case .sensitiveContentHeader: + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.privacyAndSecuritySensitiveHeader, viewType: .textTopItem) + case let .sensitiveContentToggle(_, enabled, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.privacyAndSecuritySensitiveText, type: enabled != nil ? .switchable(enabled!) : .loading, viewType: viewType, action: { + if let enabled = enabled { + arguments.toggleSensitiveContent(!enabled) + } + }, autoswitch: true) + case .sensitiveContentDesc: + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.privacyAndSecuritySensitiveDesc, viewType: .textBottomItem) + case .secretChatWebPreviewHeader: + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.privacyAndSecuritySecretChatWebPreviewHeader, viewType: .textTopItem) + case let .secretChatWebPreviewToggle(_, enabled, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.privacyAndSecuritySecretChatWebPreviewText, type: enabled != nil ? .switchable(enabled!) : .loading, viewType: viewType, action: { + if let enabled = enabled { + arguments.toggleSecretChatWebPreview(!enabled) + } + }, autoswitch: true) + case .secretChatWebPreviewDesc: + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.privacyAndSecuritySecretChatWebPreviewDesc, viewType: .textBottomItem) + case .section: + return GeneralRowItem(initialSize, height: 30, stableId: stableId, viewType: .separator) + } + } +} + +func countForSelectivePeers(_ peers: [PeerId: SelectivePrivacyPeer]) -> Int { + var result = 0 + for (_, peer) in peers { + result += peer.userCount + } + return result +} + + +private func stringForSelectiveSettings(settings: SelectivePrivacySettings) -> String { + switch settings { + case let .disableEveryone(enableFor): + if enableFor.isEmpty { + return L10n.privacySettingsControllerNobody + } else { + return L10n.privacySettingsLastSeenNobodyPlus("\(countForSelectivePeers(enableFor))") + } + case let .enableEveryone(disableFor): + if disableFor.isEmpty { + return L10n.privacySettingsControllerEverbody + } else { + return L10n.privacySettingsLastSeenEverybodyMinus("\(countForSelectivePeers(disableFor))") + } + case let .enableContacts(enableFor, disableFor): + if !enableFor.isEmpty && !disableFor.isEmpty { + return L10n.privacySettingsLastSeenContactsMinusPlus("\(countForSelectivePeers(disableFor))", "\(countForSelectivePeers(enableFor))") + } else if !enableFor.isEmpty { + return L10n.privacySettingsLastSeenContactsPlus("\(countForSelectivePeers(enableFor))") + } else if !disableFor.isEmpty { + return L10n.privacySettingsLastSeenContactsMinus("\(countForSelectivePeers(disableFor))") + } else { + return L10n.privacySettingsControllerMyContacts } } } private struct PrivacyAndSecurityControllerState: Equatable { + let updatingAccountTimeoutValue: Int32? + init() { + self.updatingAccountTimeoutValue = nil } - + + init(updatingAccountTimeoutValue: Int32?) { + self.updatingAccountTimeoutValue = updatingAccountTimeoutValue + } + static func ==(lhs: PrivacyAndSecurityControllerState, rhs: PrivacyAndSecurityControllerState) -> Bool { + if lhs.updatingAccountTimeoutValue != rhs.updatingAccountTimeoutValue { + return false + } + return true } + + func withUpdatedUpdatingAccountTimeoutValue(_ updatingAccountTimeoutValue: Int32?) -> PrivacyAndSecurityControllerState { + return PrivacyAndSecurityControllerState(updatingAccountTimeoutValue: updatingAccountTimeoutValue) + } } fileprivate func prepareTransition(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], initialSize:NSSize, arguments:PrivacyAndSecurityControllerArguments) -> TableUpdateTransition { - + let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in return entry.entry.item(arguments, initialSize: initialSize) } - + return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true) } -private func privacyAndSecurityControllerEntries(state: PrivacyAndSecurityControllerState, privacySettings: AccountPrivacySettings?, proxy: ProxySettings?) -> [PrivacyAndSecurityEntry] { +private func privacyAndSecurityControllerEntries(state: PrivacyAndSecurityControllerState, contentConfiguration: ContentSettingsConfiguration?, privacySettings: AccountPrivacySettings?, webSessions: WebSessionsContextState, blockedState: BlockedPeersContextState, proxy: ProxySettings, recentPeers: RecentPeers, configuration: TwoStepVeriticationAccessConfiguration?, activeSessions: ActiveSessionsContextState, passcodeData: PostboxAccessChallengeData, context: AccountContext) -> [PrivacyAndSecurityEntry] { var entries: [PrivacyAndSecurityEntry] = [] - + var sectionId:Int = 1 entries.append(.section(sectionId: sectionId)) sectionId += 1 + + entries.append(.blockedPeers(sectionId: sectionId, blockedState.totalCount, viewType: .firstItem)) + // entries.append(.activeSessions(sectionId: sectionId, activeSessions, viewType: .innerItem)) - entries.append(.privacyHeader(sectionId: sectionId)) - entries.append(.blockedPeers(sectionId: sectionId)) - entries.append(.lastSeenPrivacy(sectionId: sectionId, "")) - entries.append(.groupPrivacy(sectionId: sectionId, "")) - entries.append(.voiceCallPrivacy(sectionId: sectionId, "")) + let hasPasscode: Bool + switch passcodeData { + case .none: + hasPasscode = false + default: + hasPasscode = context.sharedContext.appEncryptionValue.hasPasscode() + } + entries.append(.passcode(sectionId: sectionId, enabled: hasPasscode, viewType: .innerItem)) + entries.append(.twoStepVerification(sectionId: sectionId, configuration: configuration, viewType: .lastItem)) + entries.append(.section(sectionId: sectionId)) sectionId += 1 + entries.append(.privacyHeader(sectionId: sectionId)) + if let privacySettings = privacySettings { + entries.append(.phoneNumberPrivacy(sectionId: sectionId, stringForSelectiveSettings(settings: privacySettings.phoneNumber), viewType: .firstItem)) + entries.append(.lastSeenPrivacy(sectionId: sectionId, stringForSelectiveSettings(settings: privacySettings.presence), viewType: .innerItem)) + entries.append(.groupPrivacy(sectionId: sectionId, stringForSelectiveSettings(settings: privacySettings.groupInvitations), viewType: .innerItem)) + entries.append(.voiceCallPrivacy(sectionId: sectionId, stringForSelectiveSettings(settings: privacySettings.voiceCalls), viewType: .innerItem)) + entries.append(.profilePhotoPrivacy(sectionId: sectionId, stringForSelectiveSettings(settings: privacySettings.profilePhoto), viewType: .innerItem)) + entries.append(.forwardPrivacy(sectionId: sectionId, stringForSelectiveSettings(settings: privacySettings.forwards), viewType: .lastItem)) + } else { + entries.append(.phoneNumberPrivacy(sectionId: sectionId, "", viewType: .firstItem)) + entries.append(.lastSeenPrivacy(sectionId: sectionId, "", viewType: .innerItem)) + entries.append(.groupPrivacy(sectionId: sectionId, "", viewType: .innerItem)) + entries.append(.voiceCallPrivacy(sectionId: sectionId, "", viewType: .innerItem)) + entries.append(.profilePhotoPrivacy(sectionId: sectionId, "", viewType: .innerItem)) + entries.append(.forwardPrivacy(sectionId: sectionId, "", viewType: .lastItem)) + } + + entries.append(.section(sectionId: sectionId)) sectionId += 1 - entries.append(.securityHeader(sectionId: sectionId)) - entries.append(.passcode(sectionId: sectionId)) - entries.append(.twoStepVerification(sectionId: sectionId)) - entries.append(.activeSessions(sectionId: sectionId)) + let autoarchiveConfiguration = AutoarchiveConfiguration.with(appConfiguration: context.appConfiguration) + + + if autoarchiveConfiguration.autoarchive_setting_available { + entries.append(.autoArchiveHeader(sectionId: sectionId)) + entries.append(.autoArchiveToggle(sectionId: sectionId, value: privacySettings?.automaticallyArchiveAndMuteNonContacts, viewType: .singleItem)) + entries.append(.autoArchiveDesc(sectionId: sectionId)) + + entries.append(.section(sectionId: sectionId)) + sectionId += 1 + } + + entries.append(.accountHeader(sectionId: sectionId)) + + + if let privacySettings = privacySettings { + let value: Int32 + if let updatingAccountTimeoutValue = state.updatingAccountTimeoutValue { + value = updatingAccountTimeoutValue + } else { + value = privacySettings.accountRemovalTimeout + } + entries.append(.accountTimeout(sectionId: sectionId, timeIntervalString(Int(value)), viewType: .singleItem)) + + } else { + entries.append(.accountTimeout(sectionId: sectionId, "", viewType: .singleItem)) + } + entries.append(.accountInfo(sectionId: sectionId)) + + entries.append(.section(sectionId: sectionId)) sectionId += 1 - entries.append(.proxyHeader(sectionId: sectionId)) - entries.append(.proxySettings(sectionId: sectionId, proxy != nil ? tr(.proxySettingsSocks5) : tr(.proxySettingsDisabled))) + if let contentConfiguration = contentConfiguration, contentConfiguration.canAdjustSensitiveContent { + #if !APP_STORE + entries.append(.sensitiveContentHeader(sectionId: sectionId)) + entries.append(.sensitiveContentToggle(sectionId: sectionId, value: contentConfiguration.sensitiveContentEnabled, viewType: .singleItem)) + entries.append(.sensitiveContentDesc(sectionId: sectionId)) + + entries.append(.section(sectionId: sectionId)) + sectionId += 1 + #endif + } + + + let enabled: Bool + switch recentPeers { + case .disabled: + enabled = false + case .peers: + enabled = true + } + + entries.append(.togglePeerSuggestions(sectionId: sectionId, enabled: enabled, viewType: .singleItem)) + entries.append(.togglePeerSuggestionsDesc(sectionId: sectionId)) + + entries.append(.section(sectionId: sectionId)) + sectionId += 1 + + entries.append(.clearCloudDraftsHeader(sectionId: sectionId)) + entries.append(.clearCloudDrafts(sectionId: sectionId, viewType: .singleItem)) + + entries.append(.section(sectionId: sectionId)) + sectionId += 1 -// entries.append(.section(sectionId: sectionId)) -// sectionId += 1 -// -// entries.append(.accountHeader(sectionId: sectionId)) -// entries.append(.accountTimeout(sectionId: sectionId, "")) -// entries.append(.accountInfo(sectionId: sectionId)) + + if !webSessions.sessions.isEmpty { + entries.append(.webAuthorizationsHeader(sectionId: sectionId)) + entries.append(.webAuthorizations(sectionId: sectionId, viewType: .singleItem)) + + if FastSettings.isSecretChatWebPreviewAvailable(for: context.account.id.int64) != nil { + entries.append(.section(sectionId: sectionId)) + sectionId += 1 + } + } + + if let value = FastSettings.isSecretChatWebPreviewAvailable(for: context.account.id.int64) { + entries.append(.secretChatWebPreviewHeader(sectionId: sectionId)) + entries.append(.secretChatWebPreviewToggle(sectionId: sectionId, value: value, viewType: .singleItem)) + entries.append(.secretChatWebPreviewDesc(sectionId: sectionId)) + } + + entries.append(.section(sectionId: sectionId)) + sectionId += 1 + return entries } + + + class PrivacyAndSecurityViewController: TableViewController { - private let initialSettings: Signal - -// override var removeAfterDisapper: Bool { -// return true -// } - + private let privacySettingsPromise = Promise() + + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + twoStepAccessConfiguration.set(context.engine.auth.twoStepVerificationConfiguration() |> map { TwoStepVeriticationAccessConfiguration(configuration: $0, password: nil)}) + } + + private let twoStepAccessConfiguration: Promise = Promise(nil) + override func viewDidLoad() { super.viewDidLoad() + let statePromise = ValuePromise(PrivacyAndSecurityControllerState(), ignoreRepeated: true) @@ -307,23 +697,28 @@ class PrivacyAndSecurityViewController: TableViewController { let updateState: ((PrivacyAndSecurityControllerState) -> PrivacyAndSecurityControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } - + let actionsDisposable = DisposableSet() - let account = self.account - - let pushControllerImpl: ((ViewController) -> Void) = { [weak self] c in + let context = self.context + + let pushControllerImpl: (ViewController) -> Void = { [weak self] c in self?.navigationController?.push(c) } + + let settings:Signal = proxySettings(accountManager: context.sharedContext.accountManager) + let currentInfoDisposable = MetaDisposable() actionsDisposable.add(currentInfoDisposable) - - let privacySettingsPromise = Promise() - privacySettingsPromise.set(initialSettings) - - let arguments = PrivacyAndSecurityControllerArguments(account: account, openBlockedUsers: { [weak self] in - if let account = self?.account { - pushControllerImpl(BlockedPeersViewController(account)) + + let updateAccountTimeoutDisposable = MetaDisposable() + actionsDisposable.add(updateAccountTimeoutDisposable) + + let privacySettingsPromise = self.privacySettingsPromise + + let arguments = PrivacyAndSecurityControllerArguments(context: context, openBlockedUsers: { [weak self] in + if let context = self?.context { + pushControllerImpl(BlockedPeersViewController(context)) } }, openLastSeenPrivacy: { let signal = privacySettingsPromise.get() @@ -331,7 +726,7 @@ class PrivacyAndSecurityViewController: TableViewController { |> deliverOnMainQueue currentInfoDisposable.set(signal.start(next: { [weak currentInfoDisposable] info in if let info = info { - pushControllerImpl(SelectivePrivacySettingsController(account: account, kind: .presence, current: info.presence, updated: { updated in + pushControllerImpl(SelectivePrivacySettingsController(context, kind: .presence, current: info.presence, callSettings: nil, phoneDiscoveryEnabled: nil, updated: { updated, _, _ in if let currentInfoDisposable = currentInfoDisposable { let applySetting: Signal = privacySettingsPromise.get() |> filter { $0 != nil } @@ -339,7 +734,7 @@ class PrivacyAndSecurityViewController: TableViewController { |> deliverOnMainQueue |> mapToSignal { value -> Signal in if let value = value { - privacySettingsPromise.set(.single(AccountPrivacySettings(presence: updated, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, accountRemovalTimeout: value.accountRemovalTimeout))) + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: updated, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout))) } return .complete() } @@ -354,7 +749,7 @@ class PrivacyAndSecurityViewController: TableViewController { |> deliverOnMainQueue currentInfoDisposable.set(signal.start(next: { [weak currentInfoDisposable] info in if let info = info { - pushControllerImpl(SelectivePrivacySettingsController(account: account, kind: .groupInvitations, current: info.groupInvitations, updated: { updated in + pushControllerImpl(SelectivePrivacySettingsController(context, kind: .groupInvitations, current: info.groupInvitations, callSettings: nil, phoneDiscoveryEnabled: nil, updated: { updated, _, _ in if let currentInfoDisposable = currentInfoDisposable { let applySetting: Signal = privacySettingsPromise.get() |> filter { $0 != nil } @@ -362,7 +757,7 @@ class PrivacyAndSecurityViewController: TableViewController { |> deliverOnMainQueue |> mapToSignal { value -> Signal in if let value = value { - privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: updated, voiceCalls: value.voiceCalls, accountRemovalTimeout: value.accountRemovalTimeout))) + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: updated, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout))) } return .complete() } @@ -377,7 +772,53 @@ class PrivacyAndSecurityViewController: TableViewController { |> deliverOnMainQueue currentInfoDisposable.set(signal.start(next: { [weak currentInfoDisposable] info in if let info = info { - pushControllerImpl(SelectivePrivacySettingsController(account: account, kind: .voiceCalls, current: info.voiceCalls, updated: { updated in + pushControllerImpl(SelectivePrivacySettingsController(context, kind: .voiceCalls, current: info.voiceCalls, callSettings: info.voiceCallsP2P, phoneDiscoveryEnabled: nil, updated: { updated, p2pUpdated, _ in + if let currentInfoDisposable = currentInfoDisposable { + let applySetting: Signal = privacySettingsPromise.get() + |> filter { $0 != nil } + |> take(1) + |> deliverOnMainQueue + |> mapToSignal { value -> Signal in + if let value = value { + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: updated, voiceCallsP2P: p2pUpdated ?? value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout))) + } + return .complete() + } + currentInfoDisposable.set(applySetting.start()) + } + })) + } + })) + }, openProfilePhotoPrivacy: { + let signal = privacySettingsPromise.get() + |> take(1) + |> deliverOnMainQueue + currentInfoDisposable.set(signal.start(next: { [weak currentInfoDisposable] info in + if let info = info { + pushControllerImpl(SelectivePrivacySettingsController(context, kind: .profilePhoto, current: info.profilePhoto, phoneDiscoveryEnabled: nil, updated: { updated, _, _ in + if let currentInfoDisposable = currentInfoDisposable { + let applySetting: Signal = privacySettingsPromise.get() + |> filter { $0 != nil } + |> take(1) + |> deliverOnMainQueue + |> mapToSignal { value -> Signal in + if let value = value { + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: updated, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout))) + } + return .complete() + } + currentInfoDisposable.set(applySetting.start()) + } + })) + } + })) + }, openForwardPrivacy: { + let signal = privacySettingsPromise.get() + |> take(1) + |> deliverOnMainQueue + currentInfoDisposable.set(signal.start(next: { [weak currentInfoDisposable] info in + if let info = info { + pushControllerImpl(SelectivePrivacySettingsController(context, kind: .forwards, current: info.forwards, phoneDiscoveryEnabled: nil, updated: { updated, _, _ in if let currentInfoDisposable = currentInfoDisposable { let applySetting: Signal = privacySettingsPromise.get() |> filter { $0 != nil } @@ -385,7 +826,30 @@ class PrivacyAndSecurityViewController: TableViewController { |> deliverOnMainQueue |> mapToSignal { value -> Signal in if let value = value { - privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: updated, accountRemovalTimeout: value.accountRemovalTimeout))) + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: updated, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout))) + } + return .complete() + } + currentInfoDisposable.set(applySetting.start()) + } + })) + } + })) + }, openPhoneNumberPrivacy: { + let signal = privacySettingsPromise.get() + |> take(1) + |> deliverOnMainQueue + currentInfoDisposable.set(signal.start(next: { [weak currentInfoDisposable] info in + if let info = info { + pushControllerImpl(SelectivePrivacySettingsController(context, kind: .phoneNumber, current: info.phoneNumber, phoneDiscoveryEnabled: info.phoneDiscoveryEnabled, updated: { updated, _, phoneDiscoveryEnabled in + if let currentInfoDisposable = currentInfoDisposable { + let applySetting: Signal = privacySettingsPromise.get() + |> filter { $0 != nil } + |> take(1) + |> deliverOnMainQueue + |> mapToSignal { value -> Signal in + if let value = value { + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: updated, phoneDiscoveryEnabled: phoneDiscoveryEnabled!, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: value.accountRemovalTimeout))) } return .complete() } @@ -395,49 +859,160 @@ class PrivacyAndSecurityViewController: TableViewController { } })) }, openPasscode: { [weak self] in - if let account = self?.account { - self?.navigationController?.push(PasscodeSettingsViewController(account)) + if let context = self?.context { + self?.navigationController?.push(PasscodeSettingsViewController(context)) } - }, openTwoStepVerification: { [weak self] in - if let account = self?.account { - self?.navigationController?.push(TwoStepVerificationUnlockController(account: account, mode: .access)) + }, openTwoStepVerification: { [weak self] configuration in + if let context = self?.context, let `self` = self { + self.navigationController?.push(twoStepVerificationUnlockController(context: context, mode: .access(configuration), presentController: { [weak self] controller, isRoot, animated in + guard let `self` = self, let navigation = self.navigationController else {return} + if isRoot { + navigation.removeUntil(PrivacyAndSecurityViewController.self) + } + + if !animated { + navigation.stackInsert(controller, at: navigation.stackCount) + } else { + navigation.push(controller) + } + })) + } + }, openActiveSessions: { [weak self] sessions in + if let context = self?.context { + self?.navigationController?.push(RecentSessionsController(context)) } - }, openActiveSessions: { [weak self] in - if let account = self?.account { - self?.navigationController?.push(RecentSessionsController(account)) + }, openWebAuthorizations: { + pushControllerImpl(WebSessionsController(context)) + }, setupAccountAutoremove: { [weak self] in + + if let strongSelf = self { + let signal = privacySettingsPromise.get() + |> take(1) + |> deliverOnMainQueue + updateAccountTimeoutDisposable.set(signal.start(next: { [weak updateAccountTimeoutDisposable, weak strongSelf] privacySettingsValue in + if let _ = privacySettingsValue, let strongSelf = strongSelf { + + let timeoutAction: (Int32) -> Void = { timeout in + if let updateAccountTimeoutDisposable = updateAccountTimeoutDisposable { + updateState { + return $0.withUpdatedUpdatingAccountTimeoutValue(timeout) + } + let applyTimeout: Signal = privacySettingsPromise.get() + |> filter { $0 != nil } + |> take(1) + |> deliverOnMainQueue + |> mapToSignal { value -> Signal in + if let value = value { + privacySettingsPromise.set(.single(AccountPrivacySettings(presence: value.presence, groupInvitations: value.groupInvitations, voiceCalls: value.voiceCalls, voiceCallsP2P: value.voiceCallsP2P, profilePhoto: value.profilePhoto, forwards: value.forwards, phoneNumber: value.phoneNumber, phoneDiscoveryEnabled: value.phoneDiscoveryEnabled, automaticallyArchiveAndMuteNonContacts: value.automaticallyArchiveAndMuteNonContacts, accountRemovalTimeout: timeout))) + } + return .complete() + } + updateAccountTimeoutDisposable.set((context.engine.privacy.updateAccountRemovalTimeout(timeout: timeout) + |> then(applyTimeout) + |> deliverOnMainQueue).start()) + } + } + let timeoutValues: [Int32] = [ + 1 * 30 * 24 * 60 * 60, + 3 * 30 * 24 * 60 * 60, + 180 * 24 * 60 * 60, + 365 * 24 * 60 * 60 + ] + var items: [SPopoverItem] = [] + + items.append(SPopoverItem(tr(L10n.timerMonthsCountable(1)), { + timeoutAction(timeoutValues[0]) + })) + items.append(SPopoverItem(tr(L10n.timerMonthsCountable(3)), { + timeoutAction(timeoutValues[1]) + })) + items.append(SPopoverItem(tr(L10n.timerMonthsCountable(6)), { + timeoutAction(timeoutValues[2]) + })) + items.append(SPopoverItem(tr(L10n.timerYearsCountable(1)), { + timeoutAction(timeoutValues[3]) + })) + + if let index = strongSelf.genericView.index(hash: PrivacyAndSecurityEntry.accountTimeout(sectionId: 0, "", viewType: .singleItem).stableId) { + if let view = (strongSelf.genericView.viewNecessary(at: index) as? GeneralInteractedRowView)?.textView { + showPopover(for: view, with: SPopoverViewController(items: items)) + } + } + } + })) + + } - }, setupAccountAutoremove: { - + }, openProxySettings: { [weak self] in - if let account = self?.account { - self?.navigationController?.push(ProxySettingsViewController(account)) + if let context = self?.context { + + let controller = proxyListController(accountManager: context.sharedContext.accountManager, network: context.account.network, share: { servers in + var message: String = "" + for server in servers { + message += server.link + "\n\n" + } + message = message.trimmed + + showModal(with: ShareModalController(ShareLinkObject(context, link: message)), for: mainWindow) + }, pushController: { controller in + pushControllerImpl(controller) + }) + pushControllerImpl(controller) } + }, togglePeerSuggestions: { enabled in + _ = (context.engine.peers.updateRecentPeersEnabled(enabled: enabled) |> then(enabled ? context.engine.peers.managedUpdatedRecentPeers() : Signal.complete())).start() + }, clearCloudDrafts: { + confirm(for: context.window, information: L10n.privacyAndSecurityConfirmClearCloudDrafts, successHandler: { _ in + _ = showModalProgress(signal: context.engine.messages.clearCloudDraftsInteractively(), for: context.window).start() + }) + }, toggleSensitiveContent: { value in + _ = updateRemoteContentSettingsConfiguration(postbox: context.account.postbox, network: context.account.network, sensitiveContentEnabled: value).start() + }, toggleSecretChatWebPreview: { value in + FastSettings.setSecretChatWebPreviewAvailable(for: context.account.id.int64, value: value) + }, toggleAutoArchive: { value in + _ = showModalProgress(signal: context.engine.privacy.updateAccountAutoArchiveChats(value: value), for: context.window).start() }) - - + + let previous:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) let initialSize = self.atomicSize - let privacySettings: Signal = .single(nil) |> then(requestAccountPrivacySettings(account: account) |> map { Optional($0) }) - |> deliverOnMainQueue + + let contentConfiguration: Signal = .single(nil) |> then(contentSettingsConfiguration(network: context.account.network) |> map(Optional.init)) + - let proxySettings:Signal = account.postbox.preferencesView(keys: [PreferencesKeys.proxySettings]) |> map { view in - return view.values[PreferencesKeys.proxySettings] as? ProxySettings + let signal = combineLatest(queue: .mainQueue(), statePromise.get(), contentConfiguration, appearanceSignal, settings, privacySettingsPromise.get(), context.webSessions.state, combineLatest(queue: .mainQueue(), context.engine.peers.recentPeers(), twoStepAccessConfiguration.get(), context.activeSessionsContext.state, context.sharedContext.accountManager.accessChallengeData()), context.blockedPeersContext.state) + |> map { state, contentConfiguration, appearance, proxy, privacySettings, webSessions, additional, blockedState -> TableUpdateTransition in + let entries = privacyAndSecurityControllerEntries(state: state, contentConfiguration: contentConfiguration, privacySettings: privacySettings, webSessions: webSessions, blockedState: blockedState, proxy: proxy, recentPeers: additional.0, configuration: additional.1, activeSessions: additional.2, passcodeData: additional.3.data, context: context).map{AppearanceWrapperEntry(entry: $0, appearance: appearance)} + return prepareTransition(left: previous.swap(entries), right: entries, initialSize: initialSize.modify {$0}, arguments: arguments) + } |> afterDisposed { + actionsDisposable.dispose() } |> deliverOnMainQueue - genericView.merge(with: combineLatest(statePromise.get() |> deliverOnMainQueue, privacySettings |> deliverOnMainQueue, appearanceSignal, proxySettings) - |> map { state, privacySettings, appearance, proxy -> TableUpdateTransition in - let entries = privacyAndSecurityControllerEntries(state: state, privacySettings: privacySettings, proxy: proxy).map{AppearanceWrapperEntry(entry: $0, appearance: appearance)} - return prepareTransition(left: previous.swap(entries), right: entries, initialSize: initialSize.modify {$0}, arguments: arguments) - } |> afterDisposed { - actionsDisposable.dispose() - }) - + disposable.set(signal.start(next: { [weak self] transition in + self?.genericView.merge(with: transition) + self?.readyOnce() + if let focusOnItemTag = self?.focusOnItemTag { + self?.genericView.scroll(to: .center(id: focusOnItemTag.stableId, innerId: nil, animated: true, focus: .init(focus: true), inset: 0), inset: NSEdgeInsets()) + self?.focusOnItemTag = nil + } + })) - readyOnce() } - init(_ account:Account, initialSettings: Signal) { - self.initialSettings = initialSettings - super.init(account) + deinit { + disposable.dispose() + } + + private var focusOnItemTag: PrivacyAndSecurityEntryTag? + private let disposable = MetaDisposable() + init(_ context: AccountContext, initialSettings: AccountPrivacySettings?, focusOnItemTag: PrivacyAndSecurityEntryTag? = nil) { + self.focusOnItemTag = focusOnItemTag + super.init(context) + + let thenSignal:Signal = context.engine.privacy.requestAccountPrivacySettings() |> map(Optional.init) + + self.privacySettingsPromise.set(.single(initialSettings) |> then(thenSignal)) } } + diff --git a/Telegram-Mac/ProxyListController.swift b/Telegram-Mac/ProxyListController.swift new file mode 100644 index 0000000000..4a196250b2 --- /dev/null +++ b/Telegram-Mac/ProxyListController.swift @@ -0,0 +1,496 @@ +// +// ProxyListController.swift +// Telegram +// +// Created by Mikhail Filimonov on 17/04/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import SwiftSignalKit +import Postbox +import TGUIKit +import MtProtoKit + +private let _p_id_enable: InputDataIdentifier = InputDataIdentifier("_p_id_enable") +private let _p_id_add: InputDataIdentifier = InputDataIdentifier("_p_id_add") +private let _id_calls: InputDataIdentifier = InputDataIdentifier("_id_calls") +private struct ProxyListState : Equatable { + let settings: ProxySettings + init(settings: ProxySettings = ProxySettings.defaultSettings) { + self.settings = settings + } + + func withUpdatedSettings(_ settings: ProxySettings) -> ProxyListState { + return ProxyListState(settings: settings) + } +} + +//private func ==(lhs: ProxyListState, rhs: ProxyListState) -> Bool { +// return lhs.pref == rhs.pref && lhs.current == rhs.current +//} + +extension ProxyServerSettings { + func withHexedStringData() -> ProxyServerSettings { + switch self.connection { + case let .mtp(secret): + let data = MTProxySecret.parseData(secret)?.serializeToString().data(using: .utf8) ?? Data() + return ProxyServerSettings(host: host, port: port, connection: .mtp(secret: data)) + default: + return self + } + } + + func withDataHextString() -> ProxyServerSettings { + switch self.connection { + case let .mtp(secret): + let data = MTProxySecret.parse(String(data: secret, encoding: .utf8) ?? "")?.serialize() ?? Data() + return ProxyServerSettings(host: host, port: port, connection: .mtp(secret: data)) + default: + return self + } + } +} + + + +private func proxyListSettingsEntries(_ state: ProxyListState, status: ConnectionStatus, statuses: [ProxyServerSettings : ProxyServerStatus], arguments: ProxyListArguments, showUseCalls: Bool) -> [InputDataEntry] { + var entries: [InputDataEntry] = [] + + var sectionId: Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + struct UpdateEnableRow : Equatable { + let enabled: Bool + let hasActiveServer: Bool + let hasServers: Bool + } + + let updateEnableRow: UpdateEnableRow = UpdateEnableRow(enabled: state.settings.enabled, hasActiveServer: state.settings.effectiveActiveServer != nil, hasServers: !state.settings.servers.isEmpty) + + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .string(nil), identifier: _p_id_enable, equatable: InputDataEquatable(updateEnableRow), comparable: nil, item: { initialSize, stableId in + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.proxySettingsEnable, type: .switchable(state.settings.effectiveActiveServer != nil), viewType: showUseCalls ? .firstItem : .singleItem, action: { + if state.settings.enabled { + arguments.disconnect() + } else { + arguments.reconnectLatest() + } + }, enabled: !state.settings.servers.isEmpty || state.settings.effectiveActiveServer != nil) + })) + index += 1 + + if showUseCalls { + var enabled = true + if let server = state.settings.effectiveActiveServer { + switch server.connection { + case .mtp: + enabled = false + default: + break + } + } + + struct UseForCallEquatable : Equatable { + let enabled: Bool + let useForCalls: Bool + } + + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .string(nil), identifier: _id_calls, equatable: InputDataEquatable(UseForCallEquatable(enabled: enabled, useForCalls: state.settings.useForCalls)), comparable: nil, item: { initialSize, stableId in + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.proxySettingsUseForCalls, type: .switchable(state.settings.useForCalls && enabled), viewType: .lastItem, action: { + arguments.enableForCalls(!state.settings.useForCalls) + }, enabled: enabled) + })) + } + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + var list: [ProxyServerSettings] = state.settings.servers.uniqueElements + if let current = state.settings.effectiveActiveServer, list.first(where: {$0 == current}) == nil { + list.insert(current, at: 0) + } + + let addViewType: GeneralViewType = list.isEmpty ? .singleItem : .firstItem + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .string(nil), identifier: _p_id_add, equatable: InputDataEquatable(addViewType), comparable: nil, item: { initialSize, stableId in + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.proxySettingsAddProxy, nameStyle: blueActionButton, type: .none, viewType: addViewType, action: { () in + arguments.edit(nil) + }, thumb: GeneralThumbAdditional(thumb: theme.icons.proxyAddProxy, textInset: 30, thumbInset: -5), inset:NSEdgeInsets(left: 30, right: 30)) + })) + index += 1 + + + + for proxy in list { + struct ProxyEquatable : Equatable { + let enabled: Bool + let isActiveServer: Bool + let connectionStatus: ConnectionStatus? + let proxy: ProxyServerSettings + let status: ProxyServerStatus? + let viewType: GeneralViewType + } + + let viewType = list.count == 1 ? .lastItem : (list.first == proxy ? .innerItem : bestGeneralViewType(list, for: proxy)) + + let value = ProxyEquatable(enabled: state.settings.enabled, isActiveServer: state.settings.activeServer == proxy, connectionStatus: proxy == state.settings.effectiveActiveServer ? status : nil, proxy: proxy, status: statuses[proxy], viewType: viewType) + + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .string(nil), identifier: InputDataIdentifier("_proxy_\(proxy.hashValue))"), equatable: InputDataEquatable(value), comparable: nil, item: { initialSize, stableId -> TableRowItem in + return ProxyListRowItem(initialSize, stableId: stableId, proxy: proxy, waiting: !value.enabled && state.settings.activeServer == proxy, connectionStatus: value.connectionStatus, status: value.status, viewType: viewType, action: { + arguments.connect(proxy) + }, info: { + arguments.edit(proxy) + }, delete: { + arguments.delete(proxy) + }) + })) + index += 1 + } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + return entries +} + + +private final class ProxyListArguments { + let edit:(ProxyServerSettings?)->Void + let delete:(ProxyServerSettings)->Void + let connect:(ProxyServerSettings)->Void + let disconnect:()->Void + let reconnectLatest:()->Void + let enableForCalls:(Bool)->Void + init(edit:@escaping(ProxyServerSettings?)->Void, delete: @escaping(ProxyServerSettings)->Void, connect: @escaping(ProxyServerSettings)->Void, disconnect: @escaping()->Void, reconnectLatest:@escaping()->Void, enableForCalls:@escaping(Bool)->Void) { + self.edit = edit + self.delete = delete + self.connect = connect + self.disconnect = disconnect + self.reconnectLatest = reconnectLatest + self.enableForCalls = enableForCalls + } +} + +private extension ProxyServerConnection { + var type: ProxyType { + switch self { + case .socks5: + return .socks5 + case .mtp: + return .mtp + } + } +} + +func proxyListController(accountManager: AccountManager, network: Network, showUseCalls: Bool = true, share:@escaping([ProxyServerSettings])->Void = {_ in}, pushController:@escaping(ViewController) -> Void = { _ in }) -> ViewController { + let actionsDisposable = DisposableSet() + + let updateDisposable = MetaDisposable() + actionsDisposable.add(updateDisposable) + + let statuses: ProxyServersStatuses = ProxyServersStatuses(network: network, servers: proxySettings(accountManager: accountManager) |> map { $0.servers }) + + let stateValue:Atomic = Atomic(value: ProxyListState()) + let statePromise:ValuePromise = ValuePromise(ignoreRepeated: true) + let updateState:(_ f:(ProxyListState)->ProxyListState)-> Void = { f in + statePromise.set(stateValue.modify(f)) + } + + actionsDisposable.add((proxySettings(accountManager: accountManager) |> deliverOnPrepareQueue).start(next: { settings in + updateState { current in + return current.withUpdatedSettings(settings) + } + })) + + let arguments = ProxyListArguments(edit: { proxy in + if let proxy = proxy { + pushController(addProxyController(accountManager: accountManager, network: network, settings: proxy, type: proxy.connection.type)) + } else { + let values: [ValuesSelectorValue] = [ValuesSelectorValue(localized: L10n.proxySettingsSocks5, value: .socks5), ValuesSelectorValue(localized: L10n.proxySettingsMTP, value: .mtp)] + showModal(with: ValuesSelectorModalController(values: values, selected: nil, title: L10n.proxySettingsType, onComplete: { selected in + pushController(addProxyController(accountManager: accountManager, network: network, settings: nil, type: selected.value)) + }), for: mainWindow) + } + }, delete: { proxy in + updateDisposable.set(updateProxySettingsInteractively(accountManager: accountManager, { current in + return current.withRemovedServer(proxy) + }).start()) + }, connect: { proxy in + updateDisposable.set(updateProxySettingsInteractively(accountManager: accountManager, {$0.withUpdatedActiveServer(proxy).withUpdatedEnabled(true)}).start()) + }, disconnect: { + updateDisposable.set(updateProxySettingsInteractively(accountManager: accountManager, {$0.withUpdatedEnabled(false)}).start()) + }, reconnectLatest: { + updateDisposable.set(updateProxySettingsInteractively(accountManager: accountManager, { current in + if !current.enabled, let _ = current.activeServer { + return current.withUpdatedEnabled(true) + } else if let first = current.servers.first { + return current.withUpdatedActiveServer(first).withUpdatedEnabled(true) + } else { + return current + } + }).start()) + }, enableForCalls: { enable in + updateDisposable.set(updateProxySettingsInteractively(accountManager: accountManager, {$0.withUpdatedUseForCalls(enable)}).start()) + }) + + let controller = InputDataController(dataSignal: combineLatest(queue: prepareQueue, statePromise.get(), network.connectionStatus, statuses.statuses(), appearanceSignal) |> map {proxyListSettingsEntries($0.0, status: $0.1, statuses: $0.2, arguments: arguments, showUseCalls: showUseCalls)} |> map { InputDataSignalValue(entries: $0) }, title: L10n.proxySettingsTitle, validateData: { + data in + + if data[_p_id_add] != nil { + arguments.edit(nil) + } + + + return .fail(.none) + }, afterDisappear: { + actionsDisposable.dispose() + }, removeAfterDisappear: false, hasDone: false, identifier: "proxy", customRightButton: { controller in + let view = ImageBarView(controller: controller, theme.icons.webgameShare) + + view.button.set(handler: { control in + showPopover(for: control, with: SPopoverViewController(items: [SPopoverItem(L10n.proxySettingsShareProxyList, { + updateState { current in + share(Array(current.settings.servers.prefix(20))) + return current + } + })]), edge: .minX, inset: NSMakePoint(0,-50)) + }, for: .Click) + view.set(image: theme.icons.webgameShare, highlightImage: nil) + return view + }, afterTransaction: { controller in + controller.rightBarView.isHidden = stateValue.with { $0.settings.servers.isEmpty } + }) + + return controller +} + + +private enum ProxyType { + case socks5 + case mtp + var defaultConnection: ProxyServerConnection { + switch self { + case .socks5: + return .socks5(username: nil, password: nil) + case .mtp: + return .mtp(secret: Data()) + } + } +} + +private func addProxyController(accountManager: AccountManager, network: Network, settings: ProxyServerSettings?, type: ProxyType) -> (InputDataController) { + + let actionsDisposable = DisposableSet() + + let new = settings?.withHexedStringData() ?? ProxyServerSettings(host: "", port: 0, connection: type.defaultConnection) + + let stateValue:Atomic = Atomic(value: ProxySettingsState(server: new)) + let statePromise:ValuePromise = ValuePromise(ProxySettingsState(server: new), ignoreRepeated: false) + let updateState:(_ f:(ProxySettingsState)->ProxySettingsState)-> Void = { f in + statePromise.set(stateValue.modify(f)) + } + + let title: String + switch type { + case .socks5: + title = L10n.proxySettingsSocks5 + case .mtp: + title = L10n.proxySettingsMTP + } + + weak var _controller: ViewController? + + let controller = InputDataController(dataSignal: combineLatest(statePromise.get() |> deliverOnPrepareQueue, appearanceSignal |> deliverOnPrepareQueue) |> map { state, _ in + return addProxySettingsEntries(state: state) + } |> map { InputDataSignalValue(entries: $0) }, title: title, validateData: { data -> InputDataValidation in + if data[_id_export] != nil { + updateState { current in + copyToClipboard(current.server.withDataHextString().link) + _controller?.show(toaster: ControllerToaster(text: L10n.shareLinkCopied)) + return current + } + return .fail(.none) + } + + return .fail(.doSomething { f in + updateState { current in + var fails:[InputDataIdentifier : InputDataValidationFailAction] = [:] + if current.server.host.isEmpty { + fails[_id_host] = .shake + } + if current.server.port == 0 { + fails[_id_port] = .shake + } + switch current.server.connection { + case let .mtp(secret): + if secret.isEmpty { + fails[_id_secret] = .shake + } + default: + break + } + if !fails.isEmpty { + f(.fail(.fields(fails))) + return current + } + + let server = current.server.withDataHextString() + + switch server.connection { + case let .mtp(secret): + if secret.count == 0 { + alert(for: mainWindow, info: L10n.proxySettingsIncorrectSecret) + return current + } + default: + break + } + + actionsDisposable.add((updateProxySettingsInteractively(accountManager: accountManager, { proxySetting in + if let settings = settings { + return proxySetting + .withUpdatedServer(settings, with: server) + } else { + return proxySetting + .withAddedServer(server) + .withUpdatedActiveServer(server) + .withUpdatedEnabled(true) + } + }) |> deliverOnMainQueue).start(next: { _ in + f(.success(.navigationBack)) + })) + return current + } + }) + }, updateDatas: { data in + updateState { current in + let port = data[_id_port]!.stringValue! + switch current.server.connection { + case .mtp: + let secret = data[_id_secret]?.stringValue?.data(using: .utf8) ?? Data() + return current.withUpdatedServer(ProxyServerSettings(host: data[_id_host]?.stringValue ?? "", port: port.isEmpty ? 0 : Int32(port)!, connection: .mtp(secret: secret))) + case .socks5: + return current.withUpdatedServer(ProxyServerSettings(host: data[_id_host]?.stringValue ?? "", port: port.isEmpty ? 0 : Int32(port)!, connection: .socks5(username: data[_id_username]?.stringValue, password: data[_id_pass]?.stringValue))) + } + } + return .fail(.none) + }, afterDisappear: { + actionsDisposable.dispose() + }, identifier: "proxy") + + _controller = controller + + return (controller) +} + + +private struct ProxySettingsState: Equatable { + let server: ProxyServerSettings + init(server: ProxyServerSettings) { + self.server = server + } + + func withUpdatedServer(_ server: ProxyServerSettings) -> ProxySettingsState { + return ProxySettingsState(server: server) + } +} + + + +private let _id_disable = InputDataIdentifier("disable") +private let _id_socks5 = InputDataIdentifier("socks5") +private let _id_export = InputDataIdentifier("export") + +private let _id_host = InputDataIdentifier("host") +private let _id_port = InputDataIdentifier("port") +private let _id_username = InputDataIdentifier("username") +private let _id_secret = InputDataIdentifier("secret") +private let _id_pass = InputDataIdentifier("pass") +private let _id_qrcode = InputDataIdentifier("_id_qrcode") + +private func addProxySettingsEntries(state: ProxySettingsState) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + + let server = state.server + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.proxySettingsConnectionHeader.uppercased()), data: InputDataGeneralTextData(viewType: .textTopItem))) + index += 1 + + + + entries.append(.input(sectionId: sectionId, index: index, value: .string(server.host), error: nil, identifier: _id_host, mode: .plain, data: InputDataRowData(viewType: .firstItem), placeholder: nil, inputPlaceholder: L10n.proxySettingsServer, filter: {$0}, limit: 255)) + index += 1 + + + let portViewType: GeneralViewType + switch server.connection { + case .mtp: + portViewType = .innerItem + case .socks5: + portViewType = .lastItem + } + + entries.append(.input(sectionId: sectionId, index: index, value: .string("\(server.port > 0 ? "\(server.port)" : "")"), error: nil, identifier: _id_port, mode: .plain, data: InputDataRowData(viewType: portViewType), placeholder: nil, inputPlaceholder: L10n.proxySettingsPort, filter: {String($0.unicodeScalars.filter { CharacterSet.decimalDigits.contains($0)})}, limit: 10)) + index += 1 + + switch server.connection { + case let .mtp(secret): + entries.append(.input(sectionId: sectionId, index: index, value: .string(String(data: secret, encoding: .utf8)), error: nil, identifier: _id_secret, mode: .plain, data: InputDataRowData(viewType: .lastItem), placeholder: nil, inputPlaceholder: L10n.proxySettingsSecret, filter: {$0}, limit: 255)) + index += 1 + case let .socks5(username, password): + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.proxySettingsCredentialsHeader), data: InputDataGeneralTextData(viewType: .textTopItem))) + index += 1 + entries.append(.input(sectionId: sectionId, index: index, value: .string(username ?? ""), error: nil, identifier: _id_username, mode: .plain, data: InputDataRowData(viewType: .firstItem), placeholder: nil, inputPlaceholder: L10n.proxySettingsUsername, filter: {$0}, limit: 255)) + index += 1 + + entries.append(.input(sectionId: sectionId, index: index, value: .string(password ?? ""), error: nil, identifier: _id_pass, mode: .secure, data: InputDataRowData(viewType: .lastItem), placeholder: nil, inputPlaceholder: L10n.proxySettingsPassword, filter: {$0}, limit: 255)) + index += 1 + } + + if case .mtp = server.connection { + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.proxySettingsMtpSponsor), data: InputDataGeneralTextData(viewType: .textBottomItem))) + index += 1 + } + + if !server.isEmpty { + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + entries.append(.general(sectionId: sectionId, index: index, value: .string(""), error: nil, identifier: _id_export, data: InputDataGeneralData(name: L10n.proxySettingsCopyLink, color: theme.colors.accent, icon: nil, type: .none, viewType: .singleItem, action: nil))) + index += 1 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + let link = server.withDataHextString().link + + entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_qrcode, equatable: InputDataEquatable(link), comparable: nil, item: { initialSize, stableId in + return ProxyQRCodeRowItem(initialSize, stableId: stableId, link: link) + })) + index += 1 + } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + return entries +} + + + diff --git a/Telegram-Mac/ProxyListRowItem.swift b/Telegram-Mac/ProxyListRowItem.swift new file mode 100644 index 0000000000..e36116e198 --- /dev/null +++ b/Telegram-Mac/ProxyListRowItem.swift @@ -0,0 +1,254 @@ +// +// ProxyListRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 17/04/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox + +class ProxyListRowItem: GeneralRowItem { + fileprivate let headerLayout: TextViewLayout + fileprivate let statusLayout: TextViewLayout + fileprivate let delete:()->Void + fileprivate let info:()->Void + fileprivate let status: (isConnecting: Bool, isCurrent: Bool) + fileprivate let waiting: Bool + init(_ initialSize: NSSize, stableId: AnyHashable, proxy: ProxyServerSettings, waiting: Bool, connectionStatus: ConnectionStatus?, status: ProxyServerStatus?, viewType: GeneralViewType, action:@escaping()->Void, info:@escaping()->Void, delete:@escaping()->Void) { + self.delete = delete + self.info = info + self.waiting = waiting + let attr = NSMutableAttributedString() + let title: String + switch proxy.connection { + case .socks5: + title = L10n.proxySettingsSocks5 + case .mtp: + title = L10n.proxySettingsMTP + } + _ = attr.append(string: "\(proxy.host)", color: theme.colors.text, font: .medium(.text)) + _ = attr.append(string: ":\(proxy.port)", color: theme.colors.grayText, font: .normal(.text)) + + self.headerLayout = TextViewLayout(attr, maximumNumberOfLines: 1) + + var statusText: String + var color: NSColor = theme.colors.grayText + if let connectionStatus = connectionStatus { + switch connectionStatus { + case .connecting: + statusText = L10n.connectingStatusConnecting + self.status = (isConnecting: true, isCurrent: true) + case .waitingForNetwork: + statusText = L10n.connectingStatusConnecting + self.status = (isConnecting: true, isCurrent: true) + case .online, .updating: + statusText = L10n.proxySettingsItemConnected + if let status = status { + switch status { + case let .available(ping): + statusText = L10n.proxySettingsItemConnectedPing("\(Int(ping * 1000))") + default: + break + } + } + color = theme.colors.accent + self.status = (isConnecting: false, isCurrent: true) + } + } else { + statusText = L10n.proxySettingsItemNeverConnected + if let status = status { + switch status { + case .notAvailable: + color = theme.colors.redUI + case let .available(ping): + statusText = L10n.proxySettingsItemAvailable("\(Int(ping * 1000))") //"available (ping: \(ping * 1000)ms)" + case .checking: + statusText = L10n.proxySettingsItemChecking + } + } + + self.status = (isConnecting: false, isCurrent: false) + } + statusText = title.lowercased() + ": " + statusText + + self.statusLayout = TextViewLayout(.initialize(string: statusText, color: color, font: .normal(.text)), maximumNumberOfLines: 1) + super.init(initialSize, height: 50, stableId: stableId, viewType: viewType, action: action, inset: NSEdgeInsetsMake(0, 30, 0, 30)) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) + switch viewType { + case .legacy: + headerLayout.measure(width: width - inset.left - inset.right - 80) + statusLayout.measure(width: width - inset.left - inset.right - 80) + case let .modern(_, insets): + headerLayout.measure(width: blockWidth - insets.left - insets.right - 100) + statusLayout.measure(width: blockWidth - insets.left - insets.right - 100) + } + return success + } + + override func viewClass() -> AnyClass { + return ProxyListRowView.self + } +} + + +private final class ProxyListRowView : TableRowView, ViewDisplayDelegate { + private let containerView = GeneralRowContainerView(frame: NSZeroRect) + private let headerView: TextView = TextView() + private let statusView: TextView = TextView() + private let delete: ImageButton = ImageButton() + private let info: ImageButton = ImageButton() + private let connectingView: ProgressIndicator = ProgressIndicator() + private let connected:ImageView = ImageView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + headerView.userInteractionEnabled = false + statusView.userInteractionEnabled = false + statusView.isSelectable = false + headerView.isSelectable = false + containerView.addSubview(delete) + containerView.addSubview(headerView) + containerView.addSubview(statusView) + containerView.addSubview(info) + containerView.addSubview(connectingView) + containerView.addSubview(connected) + addSubview(containerView) + + containerView.displayDelegate = self + + containerView.set(handler: { [weak self] _ in + guard let item = self?.item as? ProxyListRowItem else {return} + item.action() + }, for: .Click) + + containerView.set(handler: { [weak self] _ in + self?.updateColors() + }, for: .Highlight) + + containerView.set(handler: { [weak self] _ in + self?.updateColors() + }, for: .Hover) + + containerView.set(handler: { [weak self] _ in + self?.updateColors() + }, for: .Normal) + + delete.set(handler: { [weak self] _ in + guard let item = self?.item as? ProxyListRowItem else {return} + item.delete() + }, for: .Click) + + info.set(handler: { [weak self] _ in + guard let item = self?.item as? ProxyListRowItem else {return} + item.info() + }, for: .Click) + } + + + override var backdorColor: NSColor { + return theme.colors.background + } + + override func updateColors() { + guard let item = item as? ProxyListRowItem else {return} + + let highlighted = item.viewType.isPlainMode ? self.backdorColor : theme.colors.grayHighlight + headerView.backgroundColor = containerView.controlState == .Highlight ? highlighted : backdorColor + statusView.backgroundColor = containerView.controlState == .Highlight ? highlighted : backdorColor + self.layer?.backgroundColor = item.viewType.rowBackground.cgColor + + containerView.set(background: self.backdorColor, for: .Normal) + containerView.set(background: highlighted, for: .Highlight) + + } + + override func layout() { + super.layout() + guard let item = item as? ProxyListRowItem else {return} + + switch item.viewType { + case .legacy: + self.containerView.frame = self.bounds + self.containerView.setCorners([]) + headerView.setFrameOrigin(item.inset.left + item.inset.left, 7) + statusView.setFrameOrigin(item.inset.left + item.inset.left, self.containerView.frame.height - statusView.frame.height - 7) + delete.centerY(x: self.containerView.frame.width - delete.frame.width - item.inset.right) + info.centerY(x: self.containerView.frame.width - delete.frame.width - item.inset.right - 10 - info.frame.width) + connected.centerY(x: 30) + connectingView.centerY(x: 30) + case let .modern(position, innerInsets): + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) + self.containerView.setCorners(position.corners) + headerView.setFrameOrigin(innerInsets.left + item.inset.left, 7) + statusView.setFrameOrigin(innerInsets.left + item.inset.left, self.containerView.frame.height - statusView.frame.height - 7) + delete.centerY(x: self.containerView.frame.width - delete.frame.width - innerInsets.right) + info.centerY(x: self.containerView.frame.width - delete.frame.width - innerInsets.right - 10 - info.frame.width) + connected.centerY(x: innerInsets.left - 4) + connectingView.centerY(x: innerInsets.left - 1) + } + + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + super.draw(layer, in: ctx) + + guard let item = item as? ProxyListRowItem else {return} + if layer == containerView.layer { + ctx.setFillColor(theme.colors.border.cgColor) + switch item.viewType { + case .legacy: + ctx.fill(NSMakeRect(item.inset.left, frame.height - .borderSize, frame.width - item.inset.left - item.inset.right, .borderSize)) + case let .modern(position, insets): + switch position { + case .first, .inner: + ctx.fill(NSMakeRect(insets.left + 30, containerView.frame.height - .borderSize, containerView.frame.width - item.inset.left - item.inset.right, .borderSize)) + default: + break + } + } + } + } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + + guard let item = item as? ProxyListRowItem else {return} + + switch item.viewType { + case .legacy: + containerView.setCorners([], animated: animated) + case let .modern(position, _): + containerView.setCorners(position.corners, animated: animated) + } + + headerView.update(item.headerLayout) + statusView.update(item.statusLayout) + + connected.isHidden = (!item.status.isCurrent || item.status.isConnecting) && !item.waiting + connectingView.isHidden = !item.status.isCurrent || !item.status.isConnecting + + connected.image = item.waiting ? theme.icons.proxyNextWaitingListItem : theme.icons.proxyConnectedListItem + connected.sizeToFit() + + connectingView.progressColor = theme.colors.indicatorColor + + delete.set(image: theme.icons.proxyDeleteListItem, for: .Normal) + _ = delete.sizeToFit() + + info.set(image: theme.icons.proxyInfoListItem, for: .Normal) + _ = info.sizeToFit() + layout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/Telegram-Mac/ProxyQRCodeRowItem.swift b/Telegram-Mac/ProxyQRCodeRowItem.swift new file mode 100644 index 0000000000..9a7f8a3275 --- /dev/null +++ b/Telegram-Mac/ProxyQRCodeRowItem.swift @@ -0,0 +1,93 @@ +// +// ProxyQRCodeRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 25/04/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit + +class ProxyQRCodeRowItem: GeneralRowItem { + + let link: String + + + fileprivate let textLayout: TextViewLayout + + init(_ initialSize: NSSize, stableId: AnyHashable, link: String) { + self.link = link + textLayout = TextViewLayout(.initialize(string: L10n.proxySettingsQRText, color: theme.colors.grayText, font: .normal(.text)), alignment: .center, alwaysStaticItems: true) + textLayout.measure(width: 256) + super.init(initialSize, stableId: stableId, viewType: .singleItem) + } + + + + override var height: CGFloat { + return 256.0 + textLayout.layoutSize.height + 30 + } + + override func viewClass() -> AnyClass { + return ProxyQRCodeRowView.self + } +} + + +private final class ProxyQRCodeRowView : TableRowView { + private let disposable = MetaDisposable() + private let imageView: ImageView = ImageView(frame: NSMakeRect(0, 0, 256, 256)) + private let textView: TextView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(imageView) + addSubview(textView) + textView.userInteractionEnabled = false + textView.isSelectable = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var backdorColor: NSColor { + guard let item = item as? ProxyQRCodeRowItem else { return theme.colors.background } + return item.viewType.rowBackground + } + + override func layout() { + super.layout() + textView.centerX(y: 10) + imageView.centerX(y: textView.frame.maxY + 10) + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + + guard let item = item as? ProxyQRCodeRowItem else { return } + + textView.update(item.textLayout) + + disposable.set((qrCode(string: item.link, color: theme.colors.text, backgroundColor: theme.colors.grayBackground, icon: .proxy) + |> map { generator -> CGImage? in + let imageSize = CGSize(width: 256, height: 256) + let context = generator.1(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: NSEdgeInsets())) + + return context?.generateImage() + } + |> deliverOnMainQueue).start(next: { [weak self] image in + if let image = image { + self?.imageView.image = image + } + })) + + needsLayout = true + } + + deinit { + disposable.dispose() + } +} diff --git a/Telegram-Mac/ProxySettingsViewController.swift b/Telegram-Mac/ProxySettingsViewController.swift deleted file mode 100644 index 5676740305..0000000000 --- a/Telegram-Mac/ProxySettingsViewController.swift +++ /dev/null @@ -1,483 +0,0 @@ -// -// ProxySettingsViewController.swift -// Telegram -// -// Created by keepcoder on 19/06/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - -import Cocoa -import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac - -private final class ProxySettingsArguments { - let account:Account - let changeSettingsType:(ProxySettingsStateType)->Void - let changeServerHandler:(String)->Void - let changePortHandler:(String)->Void - let changeUsernameHandler:(String)->Void - let changePasswordHandler:(String)->Void - let copyShareLink:()->Void - let exportProxy:()->Void - init(_ account:Account, changeSettingsType:@escaping(ProxySettingsStateType)->Void, changeServerHandler:@escaping(String)->Void, changePortHandler:@escaping(String)->Void, changeUsernameHandler:@escaping(String)->Void, changePasswordHandler:@escaping(String)->Void, copyShareLink:@escaping()->Void, exportProxy:@escaping()->Void) { - self.account = account - self.changeSettingsType = changeSettingsType - self.changeServerHandler = changeServerHandler - self.changePortHandler = changePortHandler - self.changeUsernameHandler = changeUsernameHandler - self.changePasswordHandler = changePasswordHandler - self.copyShareLink = copyShareLink - self.exportProxy = exportProxy - } -} - -private enum ProxySettingsEntryId : Hashable { - case section(Int32) - case index(Int32) - case header(Int32) - var hashValue: Int { - switch self { - case .index(let index): - return Int(index) - case .section(let section): - return Int(section) - case .header(let index): - return Int(index) - } - } - - static func ==(lhs: ProxySettingsEntryId, rhs: ProxySettingsEntryId) -> Bool { - switch lhs { - case .section(let index): - if case .section(index) = rhs { - return true - } else { - return false - } - case .index(let index): - if case .index(index) = rhs { - return true - } else { - return false - } - case .header(let index): - if case .header(index) = rhs { - return true - } else { - return false - } - } - } -} - -private enum ProxySettingsEntry : TableItemListNodeEntry { - case disabled(Int32, Bool) - case socks5(Int32, Bool) - case server(Int32, String) - case port(Int32, String) - case username(Int32, String) - case password(Int32, String) - case section(Int32) - case header(Int32, Int32, String) - case share(Int32, Int32, String) - case exportProxy(Int32) - var stableId: ProxySettingsEntryId { - switch self { - case .disabled: - return .index(0) - case .socks5: - return .index(1) - case .server: - return .index(2) - case .port: - return .index(3) - case .username: - return .index(4) - case .password: - return .index(5) - case .share: - return .index(6) - case .exportProxy: - return .index(7) - case .header(_, let index, _): - return .header(index) - case .section(let id): - return .section(id) - } - } - - var index:Int32 { - switch self { - case .exportProxy: - return 0 - case .disabled: - return 1 - case .socks5: - return 2 - case .server: - return 3 - case .port: - return 4 - case .username: - return 5 - case .password: - return 6 - case .header(let section, let index, _): - return (section + 1) * 1000 - index - 30 - case .share(let section, let index, _): - return (section + 1) * 1000 - index + 30 - case .section(let index): - return (index + 1) * 1000 - index - } - } - - static func <(lhs:ProxySettingsEntry, rhs: ProxySettingsEntry) -> Bool { - return lhs.index < rhs.index - } - - static func ==(lhs:ProxySettingsEntry, rhs: ProxySettingsEntry) -> Bool { - switch lhs { - case let .disabled(index, value): - if case .disabled(index, value) = rhs { - return true - } else { - return false - } - case let .exportProxy(index): - if case .exportProxy(index) = rhs { - return true - } else { - return false - } - case let .socks5(index, value): - if case .socks5(index, value) = rhs { - return true - } else { - return false - } - case let .server(index, current): - if case .server(index, current) = rhs { - return true - } else { - return false - } - case let .port(index, current): - if case .port(index, current) = rhs { - return true - } else { - return false - } - case let .username(index, current): - if case .username(index, current) = rhs { - return true - } else { - return false - } - case let .password(index, current): - if case .password(index, current) = rhs { - return true - } else { - return false - } - case .header(let section, let index, let text): - if case .header(section, index, text) = rhs { - return true - } else { - return false - } - case .share(let section, let index, let text): - if case .share(section, index, text) = rhs { - return true - } else { - return false - } - case .section(let index): - if case .section(index) = rhs { - return true - } else { - return false - } - } - } - - func item(_ arguments: ProxySettingsArguments, initialSize: NSSize) -> TableRowItem { - switch self { - case .section: - return GeneralRowItem(initialSize, height: 20, stableId: stableId) - case .header(_, _, let text): - return GeneralTextRowItem(initialSize, stableId: stableId, text: text, drawCustomSeparator: true, inset: NSEdgeInsets(left: 30.0, right: 30.0, top:2, bottom:6)) - case .share(_, _, let text): - let attributed = NSMutableAttributedString() - _ = attributed.append(string: text, color: .link, font: .medium(.text)) - return GeneralTextRowItem(initialSize, stableId: stableId, text: attributed, inset: NSEdgeInsets(left: 30.0, right: 30.0, top:10, bottom:2), action: { - arguments.copyShareLink() - }) - case .disabled(_, let value): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.proxySettingsDisabled), type: .selectable(stateback: { () -> Bool in - return value - }), action: { - arguments.changeSettingsType(.disabled) - }) - case .socks5(_, let value): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.proxySettingsSocks5), type: .selectable(stateback: { () -> Bool in - return value - }), action: { - arguments.changeSettingsType(.socks5) - }) - case .server(_, let value): - return GeneralInputRowItem(initialSize, stableId: stableId, placeholder: tr(.proxySettingsServer), text: value, limit: 250, insets: NSEdgeInsets(left:25,right:25,top:10,bottom:3), textChangeHandler: { modified in - arguments.changeServerHandler(modified) - }) - case .port(_, let value): - return GeneralInputRowItem(initialSize, stableId: stableId, placeholder: tr(.proxySettingsPort), text: Int(value) == 0 ? "" : value, limit: 6, insets: NSEdgeInsets(left:25,right:25,top:10,bottom:3), textChangeHandler: { modified in - arguments.changePortHandler(modified) - }, textFilter: { value in - if let _ = Int32(value) { - return value - } else { - return "" - } - }) - case .username(_, let value): - return GeneralInputRowItem(initialSize, stableId: stableId, placeholder: tr(.proxySettingsUsername), text: value, limit: 250, insets: NSEdgeInsets(left:25,right:25,top:10,bottom:3), textChangeHandler: { modified in - arguments.changeUsernameHandler(modified) - }) - case .password(_, let value): - return GeneralInputRowItem(initialSize, stableId: stableId, placeholder: tr(.proxySettingsPassword), text: value, limit: 250, insets: NSEdgeInsets(left:25,right:25,top:10,bottom:3), textChangeHandler: { modified in - arguments.changePasswordHandler(modified) - }) - case .exportProxy: - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.proxySettingsExportLink), nameStyle: blueActionButton, type: .next, action: { - arguments.exportProxy() - }) - } - } -} - -private enum ProxySettingsStateType { - case disabled - case socks5 -} -private class ProxySettingsState: Equatable { - let type:ProxySettingsStateType - let settings: ProxySettings? - init() { - self.type = .disabled - self.settings = nil - } - init(type:ProxySettingsStateType, settings: ProxySettings?) { - self.type = type - self.settings = settings - } - - static func ==(lhs: ProxySettingsState, rhs: ProxySettingsState) -> Bool { - if let lhsSettings = lhs.settings, let rhsSettings = rhs.settings { - if !lhsSettings.isEqual(to: rhsSettings) { - return false - } - } else if (lhs.settings != nil) != (rhs.settings != nil) { - return false - } - return lhs.type == rhs.type - } - func withUpdatedSettings(_ settings: ProxySettings?) -> ProxySettingsState { - return ProxySettingsState(type: self.type, settings: settings) - } - func withUpdatedType(_ type: ProxySettingsStateType) -> ProxySettingsState { - return ProxySettingsState(type: type, settings: self.settings) - } - -} - -private func proxySettingsEntries(_ state: ProxySettingsState) -> [ProxySettingsEntry] { - var entries:[ProxySettingsEntry] = [] - - var sectionId:Int32 = 1 - - entries.append(.section(sectionId)) - sectionId += 1 - - var headerIndex:Int32 = 1 - - entries.append(.exportProxy(sectionId)) - entries.append(.header(sectionId, headerIndex, tr(.proxySettingsExportDescription))) - headerIndex += 1 - - entries.append(.section(sectionId)) - sectionId += 1 - - entries.append(.disabled(sectionId, state.type == .disabled)) - entries.append(.socks5(sectionId, state.type == .socks5)) - - if state.type == .socks5 { - let settings = state.settings - - entries.append(.section(sectionId)) - sectionId += 1 - entries.append(.header(sectionId, headerIndex, tr(.proxySettingsConnectionHeader))) - headerIndex += 1 - - entries.append(.server(sectionId, settings?.host ?? "")) - entries.append(.port(sectionId, settings?.port != nil ? "\(settings!.port)" : "")) - - entries.append(.section(sectionId)) - sectionId += 1 - entries.append(.header(sectionId, headerIndex, tr(.proxySettingsCredentialsHeader))) - headerIndex += 1 - - entries.append(.username(sectionId, settings?.username ?? "")) - entries.append(.password(sectionId, settings?.password ?? "")) - - if !(settings?.host ?? "").isEmpty && (settings?.port ?? 0) > 0 { - entries.append(.share(sectionId, headerIndex, tr(.proxySettingsShare))) - } - } - - return entries -} - -fileprivate func prepareTransition(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], initialSize:NSSize, arguments:ProxySettingsArguments) -> TableUpdateTransition { - - let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in - return entry.entry.item(arguments, initialSize: initialSize) - } - - return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true) -} - - -class ProxySettingsViewController: EditableViewController { - private let preferencesDisposable = MetaDisposable() - private let stateValue:Atomic = Atomic(value: ProxySettingsState()) - - private let statePromise:ValuePromise = ValuePromise(ProxySettingsState(), ignoreRepeated: true) - - override init(_ account: Account) { - super.init(account) - } - - override var enableBack: Bool { - return true - } - - override func viewDidLoad() { - super.viewDidLoad() - let stateValue = self.stateValue - let account = self.account - let statePromise:ValuePromise = self.statePromise - let updateState:(_ f:(ProxySettingsState)->ProxySettingsState)-> Void = { f in - statePromise.set(stateValue.modify(f)) - } - - let arguments = ProxySettingsArguments(account, changeSettingsType: { value in - updateState({ current in - return current.withUpdatedType(value) - }) - - }, changeServerHandler: { updated in - updateState({$0.withUpdatedSettings(ProxySettings(host: updated, port: $0.settings?.port ?? 0, username: $0.settings?.username, password: $0.settings?.password))}) - }, changePortHandler: { updated in - updateState({$0.withUpdatedSettings(ProxySettings(host: $0.settings?.host ?? "", port: Int32(updated) ?? 0, username: $0.settings?.username, password: $0.settings?.password))}) - }, changeUsernameHandler: { updated in - updateState({$0.withUpdatedSettings(ProxySettings(host: $0.settings?.host ?? "", port: $0.settings?.port ?? 0, username: updated, password: $0.settings?.password))}) - - }, changePasswordHandler: { updated in - updateState({$0.withUpdatedSettings(ProxySettings(host: $0.settings?.host ?? "", port: $0.settings?.port ?? 0, username: $0.settings?.username, password: updated))}) - }, copyShareLink: { [weak self] in - if let value = stateValue.modify({$0}).settings { - var link = "https://t.me/socks?server=\(value.host)&port=\(value.port)" - if let username = value.username { - link += "&username=\(username)" - } - if let password = value.password { - link += "&password=\(password)" - } - copyToClipboard(link) - self?.show(toaster: ControllerToaster(text: tr(.shareLinkCopied))) - } - }, exportProxy: { - let link = NSPasteboard.general.string(forType: .string) - var found: Bool = false - if let link = link, !link.isEmpty { - let attributed = NSMutableAttributedString() - _ = attributed.append(string: link) - attributed.detectLinks(type: [.Links], account: account, applyProxy: { settings in - applyExternalProxy(settings, postbox: account.postbox, network: account.network) - }) - attributed.enumerateAttribute(NSAttributedStringKey.link, in: attributed.range, options: NSAttributedString.EnumerationOptions(rawValue: 0), using: { (value, range, stop) in - if let value = value as? inAppLink { - switch value { - case .socks(let proxy, let applyProxy): - applyProxy(proxy) - found = true - stop.pointee = true - default: - break - } - } - }) - } - if !found { - alert(for: mainWindow, info: tr(.proxySettingsProxyNotFound)) - } - }) - - let initialState:Atomic = Atomic(value: ProxySettingsState()) - - let previous:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) - let initialSize = self.atomicSize - preferencesDisposable.set((account.postbox.preferencesView(keys: [PreferencesKeys.proxySettings])).start(next: { view in - let settings = view.values[PreferencesKeys.proxySettings] as? ProxySettings - updateState({ current in - let updated = current.withUpdatedSettings(settings).withUpdatedType(settings == nil ? .disabled : .socks5) - return updated - }) - _ = initialState.swap(stateValue.modify({$0})) - })) - - genericView.merge(with: (combineLatest(statePromise.get() |> deliverOnMainQueue, appearanceSignal) ) |> map { values in - return proxySettingsEntries(values.0).map({AppearanceWrapperEntry(entry: $0, appearance: values.1)}) - } |> map { - return prepareTransition(left: previous.swap($0), right: $0, initialSize: initialSize.modify{$0}, arguments: arguments) - } |> afterNext { [weak self] value -> TableUpdateTransition in - let state = stateValue.modify{$0} - let initial = initialState.modify{$0} - if initial != state { - if state.type == .disabled { - self?.set(editable: initial != state) - } else { - if let settings = state.settings { - self?.set(editable: !settings.host.isEmpty && settings.port > 0) - } else { - self?.set(editable: false) - } - } - } else { - self?.set(editable: false) - } - - - return value - }) - readyOnce() - } - - override var normalString: String { - return tr(.proxySettingsSave) - } - - - - override func changeState() { - let state = stateValue.modify({$0}) - set(editable: false) - _ = applyProxySettings(postbox: account.postbox, network: account.network, settings: state.type == .disabled ? nil : state.settings).start() - - } - - deinit { - preferencesDisposable.dispose() - } -} diff --git a/Telegram-Mac/PushToTalk.swift b/Telegram-Mac/PushToTalk.swift new file mode 100644 index 0000000000..4de1c3f58e --- /dev/null +++ b/Telegram-Mac/PushToTalk.swift @@ -0,0 +1,515 @@ +// +// PushToTalk.swift +// Telegram +// +// Created by Mikhail Filimonov on 02/12/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Foundation +import HotKey +import SwiftSignalKit +import TGUIKit + +extension PushToTalkValue { + func isEqual(_ value: KeyboardGlobalHandler.Result) -> Bool { + return value.keyCodes == self.keyCodes && self.modifierFlags == value.modifierFlags && value.otherMouse == self.otherMouse + } +} + +final class KeyboardGlobalHandler { + + static func hasPermission(askPermission: Bool = true) -> Bool { + let result: Bool + if #available(macOS 10.15, *) { + result = PermissionsManager.checkInputMonitoring(withPrompt: false) + } else if #available(macOS 10.14, *) { + result = PermissionsManager.checkAccessibility(withPrompt: false) + } else { + result = true + } + if !result && askPermission { + self.requestPermission() + } + return result + } + + static func requestPermission() -> Void { + if #available(macOS 10.15, *) { + _ = PermissionsManager.checkInputMonitoring(withPrompt: true) + } else if #available(macOS 10.14, *) { + _ = PermissionsManager.checkAccessibility(withPrompt: true) + } else { + + } + } + + private struct Handler { + let pushToTalkValue: PushToTalkValue? + let success:(Result)->Void + let eventType: NSEvent.EventTypeMask + init(PushToTalkValue: PushToTalkValue?, success:@escaping(Result)->Void, eventType: NSEvent.EventTypeMask) { + self.pushToTalkValue = PushToTalkValue + self.success = success + self.eventType = eventType + } + } + + struct Result { + let keyCodes: [UInt16] + let otherMouse:[Int] + let modifierFlags: [PushToTalkValue.ModifierFlag] + let string: String + let eventType: NSEvent.EventTypeMask + } + + + private var monitors: [Any?] = [] + + private var keyDownHandler: Handler? + private var keyUpHandler: Handler? + + private var eventTap: CFMachPort? + private var runLoopSource:CFRunLoopSource? + + static func getPermission()->Signal { + return Signal { subscriber in + + subscriber.putNext(KeyboardGlobalHandler.hasPermission(askPermission: false)) + subscriber.putCompletion() + + return EmptyDisposable + + } |> runOn(.concurrentDefaultQueue()) |> deliverOnMainQueue + } + + private let disposable = MetaDisposable() + + enum Mode { + case local(WeakReference) + case global + } + private let mode: Mode + + init(mode: Mode) { + self.mode = mode + switch mode { + case .global: + self.disposable.set(KeyboardGlobalHandler.getPermission().start(next: { [weak self] value in + self?.runListener(hasPermission: value) + })) + case .local: + self.runListener(hasPermission: false) + } + } + + private func runListener(hasPermission: Bool) { + final class ProcessEvent { + var process:(NSEvent)->Void = { _ in } + } + + let processEvent = ProcessEvent() + + processEvent.process = { [weak self] event in + self?.process(event) + } + + if hasPermission { + func callback(proxy: CGEventTapProxy, type: CGEventType, event: CGEvent, refcon: UnsafeMutableRawPointer?) -> Unmanaged? { + if let event = NSEvent(cgEvent: event) { + let processor = Unmanaged.fromOpaque(refcon!).takeUnretainedValue() + processor.process(event) + } + return Unmanaged.passRetained(event) + } + let eventMask:Int32 = (1 << CGEventType.keyDown.rawValue) | + (1 << CGEventType.otherMouseDown.rawValue) | + (1 << CGEventType.otherMouseUp.rawValue) | + (1 << CGEventType.keyUp.rawValue) | + (1 << CGEventType.flagsChanged.rawValue) + + self.eventTap = CGEvent.tapCreate(tap: .cghidEventTap, + place: .headInsertEventTap, + options: .listenOnly, + eventsOfInterest: CGEventMask(eventMask), + callback: callback, + userInfo: UnsafeMutableRawPointer(Unmanaged.passRetained(processEvent).toOpaque())) + + if let eventTap = self.eventTap { + let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0) + CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes) + CGEvent.tapEnable(tap: eventTap, enable: true) + self.runLoopSource = runLoopSource + } + } else { + monitors.append(NSEvent.addLocalMonitorForEvents(matching: [.keyUp, .keyDown, .flagsChanged, .otherMouseUp, .otherMouseDown], handler: { [weak self] event in + guard let `self` = self else { + return event + } + self.process(event) + return event + })) + } + } + + deinit { + for monitor in monitors { + if let monitor = monitor { + NSEvent.removeMonitor(monitor) + } + } + if let eventTap = eventTap { + CGEvent.tapEnable(tap: eventTap, enable: false) + } + if let source = self.runLoopSource { + CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, .commonModes) + } + disposable.dispose() + } + + private var downStake:[NSEvent] = [] + private var otherMouseDownStake:[NSEvent] = [] + private var flagsStake:[NSEvent] = [] + + private var currentDownStake:[NSEvent] = [] + private var currentOtherMouseDownStake:[NSEvent] = [] + private var currentFlagsStake:[NSEvent] = [] + + + var activeCount: Int { + var total:Int = 0 + if currentDownStake.count > 0 { + total += currentDownStake.count + } + if currentFlagsStake.count > 0 { + total += currentFlagsStake.count + } + if currentOtherMouseDownStake.count > 0 { + total += currentOtherMouseDownStake.count + } + return total + } + + @discardableResult private func process(_ event: NSEvent) -> Bool { + + switch mode { + case .global: + break + case let .local(window): + if window.value?.windowNumber != event.windowNumber { + return false + } + } + + let oldActiveCount = self.activeCount + + switch event.type { + case .keyUp: + currentDownStake.removeAll(where: { $0.keyCode == event.keyCode }) + case .keyDown: + if !downStake.contains(where: { $0.keyCode == event.keyCode }) { + downStake.append(event) + } + if !currentDownStake.contains(where: { $0.keyCode == event.keyCode }) { + currentDownStake.append(event) + } + case .otherMouseDown: + if !otherMouseDownStake.contains(where: { $0.buttonNumber == event.buttonNumber }) { + otherMouseDownStake.append(event) + } + if !currentOtherMouseDownStake.contains(where: { $0.buttonNumber == event.buttonNumber }) { + currentOtherMouseDownStake.append(event) + } + case .otherMouseUp: + currentOtherMouseDownStake.removeAll(where: { $0.buttonNumber == event.buttonNumber }) + case .flagsChanged: + if !flagsStake.contains(where: { $0.keyCode == event.keyCode }) { + flagsStake.append(event) + } + if !currentFlagsStake.contains(where: { $0.keyCode == event.keyCode }) { + currentFlagsStake.append(event) + } else { + currentFlagsStake.removeAll(where: { $0.keyCode == event.keyCode }) + } + default: + break + } + + let newActiveCount = self.activeCount + if oldActiveCount != newActiveCount { + applyStake(oldActiveCount < newActiveCount) + } + + if self.activeCount == 0 { + self.downStake.removeAll() + self.flagsStake.removeAll() + self.otherMouseDownStake.removeAll() + } + + return false + } + + private var isDownSent: Bool = false + private var isUpSent: Bool = false + @discardableResult private func applyStake(_ isDown: Bool) -> Bool { + var string = "" + + var _flags: [PushToTalkValue.ModifierFlag] = [] + + + let finalFlag = self.flagsStake.max(by: { lhs, rhs in + return lhs.modifierFlags.rawValue < rhs.modifierFlags.rawValue + }) + + if let finalFlag = finalFlag { + string += StringFromKeyCode(finalFlag.keyCode, finalFlag.modifierFlags.rawValue)! + } + + for flag in flagsStake { + _flags.append(PushToTalkValue.ModifierFlag(keyCode: flag.keyCode, flag: flag.modifierFlags.rawValue)) + } + var _keyCodes:[UInt16] = [] + for key in downStake { + string += StringFromKeyCode(key.keyCode, 0)!.uppercased() + if key != downStake.last { + string += " + " + } + _keyCodes.append(key.keyCode) + } + + var _otherMouse:[Int] = [] + for key in otherMouseDownStake { + if !string.isEmpty { + string += " + " + } + string += "MOUSE\(key.buttonNumber)" + if key != otherMouseDownStake.last { + string += " + " + } + _otherMouse.append(key.buttonNumber) + } + + + let result = Result(keyCodes: _keyCodes, otherMouse: _otherMouse, modifierFlags: _flags, string: string, eventType: isDown ? .keyDown : .keyUp) + + string = "" + var flags: [PushToTalkValue.ModifierFlag] = [] + for flag in currentFlagsStake { + flags.append(PushToTalkValue.ModifierFlag(keyCode: flag.keyCode, flag: flag.modifierFlags.rawValue)) + } + var keyCodes:[UInt16] = [] + for key in currentDownStake { + keyCodes.append(key.keyCode) + } + var otherMouses:[Int] = [] + for key in currentOtherMouseDownStake { + otherMouses.append(key.buttonNumber) + } + + let invokeUp:(PushToTalkValue)->Bool = { ptt in + var invoke: Bool = false + for keyCode in ptt.keyCodes { + if !keyCodes.contains(keyCode) { + invoke = true + } + } + for mouse in ptt.otherMouse { + if !otherMouses.contains(mouse) { + invoke = true + } + } + for flag in ptt.modifierFlags { + if !flags.contains(flag) { + invoke = true + } + } + return invoke + } + + let invokeDown:(PushToTalkValue)->Bool = { ptt in + var invoke: Bool = true + for keyCode in ptt.keyCodes { + if !keyCodes.contains(keyCode) { + invoke = false + } + } + for buttonNumber in ptt.otherMouse { + if !otherMouses.contains(buttonNumber) { + invoke = false + } + } + for flag in ptt.modifierFlags { + if !flags.contains(flag) { + invoke = false + } + } + return invoke + } + + var isHandled: Bool = false + + if isDown { + isUpSent = false + if let keyDown = self.keyDownHandler { + if let ptt = keyDown.pushToTalkValue { + if invokeDown(ptt) { + keyDown.success(result) + isDownSent = true + isHandled = true + } + } else { + keyDown.success(result) + isDownSent = true + isHandled = true + } + } + } else { + if let keyUp = self.keyUpHandler { + if let ptt = keyUp.pushToTalkValue { + if invokeUp(ptt), (isDownSent || keyDownHandler == nil), !isUpSent { + keyUp.success(result) + isHandled = true + isUpSent = true + } + } else if (isDownSent || keyDownHandler == nil), !isUpSent { + keyUp.success(result) + isHandled = true + isUpSent = true + } + } + } + if activeCount == 0 { + isDownSent = false + } + return isHandled + } + + func setKeyDownHandler(_ pushToTalkValue: PushToTalkValue?, success: @escaping(Result)->Void) { + self.keyDownHandler = .init(PushToTalkValue: pushToTalkValue, success: success, eventType: .keyDown) + } + + func setKeyUpHandler(_ pushToTalkValue: PushToTalkValue?, success: @escaping(Result)->Void) { + self.keyUpHandler = .init(PushToTalkValue: pushToTalkValue, success: success, eventType: .keyUp) + } + + func removeHandlers() { + self.keyDownHandler = nil + self.keyUpHandler = nil + } + +} + + +final class PushToTalk { + + enum Mode { + case speaking(sound: String?) + case waiting(sound: String?) + case toggle(activate: String?, deactivate: String?) + } + var update: (Mode)->Void = { _ in } + + private let disposable = MetaDisposable() + private let actionDisposable = MetaDisposable() + + private let monitor: KeyboardGlobalHandler + private let spaceMonitor: KeyboardGlobalHandler + + private let spaceEvent = PushToTalkValue(keyCodes: [KeyboardKey.Space.rawValue], otherMouse: [], modifierFlags: [], string: "⎵") + + init(sharedContext: SharedAccountContext, window: Window) { + self.monitor = KeyboardGlobalHandler(mode: .global) + self.spaceMonitor = KeyboardGlobalHandler(mode: .local(WeakReference(value: window))) + let settings = voiceCallSettings(sharedContext.accountManager) |> deliverOnMainQueue + + disposable.set(settings.start(next: { [weak self] settings in + self?.updateSettings(settings) + })) + } + + private func installSpaceMonitor(settings: VoiceCallSettings) { + switch settings.mode { + case .pushToTalk: + self.spaceMonitor.setKeyDownHandler(spaceEvent, success: { [weak self] result in + self?.proccess(result.eventType, false) + }) + self.spaceMonitor.setKeyUpHandler(spaceEvent, success: { [weak self] result in + self?.proccess(result.eventType, false) + }) + case .always: + self.spaceMonitor.setKeyDownHandler(spaceEvent, success: { _ in + }) + self.spaceMonitor.setKeyUpHandler(spaceEvent, success: { [weak self] result in + self?.update(.toggle(activate: nil, deactivate: nil)) + }) + case .none: + self.spaceMonitor.removeHandlers() + } + + } + private func deinstallSpaceMonitor() { + self.spaceMonitor.removeHandlers() + } + + private func updateSettings(_ settings: VoiceCallSettings) { + let performSound: Bool = settings.pushToTalkSoundEffects + switch settings.mode { + case .always: + if let event = settings.pushToTalk { + self.monitor.setKeyUpHandler(event, success: { [weak self] result in + self?.update(.toggle(activate: nil, deactivate: nil)) + }) + self.monitor.setKeyDownHandler(event, success: {_ in + + }) + if event == spaceEvent { + deinstallSpaceMonitor() + } else { + installSpaceMonitor(settings: settings) + } + } else { + self.monitor.removeHandlers() + installSpaceMonitor(settings: settings) + } + case .pushToTalk: + if let event = settings.pushToTalk { + self.monitor.setKeyUpHandler(event, success: { [weak self] result in + self?.proccess(result.eventType, performSound) + }) + self.monitor.setKeyDownHandler(event, success: { [weak self] result in + self?.proccess(result.eventType, performSound) + }) + if event == spaceEvent { + deinstallSpaceMonitor() + } else { + installSpaceMonitor(settings: settings) + } + } else { + self.monitor.removeHandlers() + installSpaceMonitor(settings: settings) + } + case .none: + self.monitor.removeHandlers() + deinstallSpaceMonitor() + } + } + + private func proccess(_ eventType: NSEvent.EventTypeMask, _ performSound: Bool) { + if eventType == .keyUp { + let signal = Signal.complete() |> delay(0.15, queue: .mainQueue()) + actionDisposable.set(signal.start(completed: { [weak self] in + self?.update(.waiting(sound: performSound ? "Pop" : nil)) + })) + } else if eventType == .keyDown { + actionDisposable.set(nil) + self.update(.speaking(sound: performSound ? "Purr" : nil)) + } + } + + deinit { + actionDisposable.dispose() + disposable.dispose() + + } + +} diff --git a/Telegram-Mac/PushToTalkRowItem.swift b/Telegram-Mac/PushToTalkRowItem.swift new file mode 100644 index 0000000000..8d9a67370e --- /dev/null +++ b/Telegram-Mac/PushToTalkRowItem.swift @@ -0,0 +1,256 @@ +// +// PushToTalkRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 02/12/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit + + +final class PushToTalkRowItem : GeneralRowItem { + fileprivate var settings: PushToTalkValue? + fileprivate let checkPermission:()->Void + fileprivate let update:(PushToTalkValue?)->Void + init(_ initialSize: NSSize, stableId: AnyHashable, settings: PushToTalkValue?, update:@escaping(PushToTalkValue?)->Void, checkPermission: @escaping()->Void, viewType: GeneralViewType) { + self.settings = settings + self.update = update + self.checkPermission = checkPermission + super.init(initialSize, height: 50, stableId: stableId, type: .none, viewType: viewType, inset: NSEdgeInsets(top: 3, left: 30, bottom: 3, right: 30), error: nil) + } + + deinit { + + } + + override func viewClass() -> AnyClass { + return PushToTalkRowView.self + } +} + + + +final class PushToTalkRowView: GeneralContainableRowView { + + enum PTTMode { + case normal + case editing + } + + private var textView: TextView? + private let button: Control = Control() + + private(set) var mode: PTTMode = .normal + + private let shimmerView = View() + + private let shortcutView = TextView() + private var eventGlobalMonitor: Any? + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + + self.subviews = [shimmerView, containerView] + + shimmerView.isEventLess = true + + shimmerView.layer?.cornerRadius = 10 + + addSubview(button) + addSubview(shortcutView) + button.layer?.cornerRadius = 8 + + let shadow = NSShadow() + shadow.shadowBlurRadius = 3 + shadow.shadowColor = NSColor.redUI.withAlphaComponent(1) + shadow.shadowOffset = NSMakeSize(0, 0) + shimmerView.shadow = shadow + + shimmerView.background = .random + + + button.set(handler: { [weak self] _ in + self?.toggleMode(animated: true, mode: self?.mode == PTTMode.normal ? .editing : .normal) + }, for: .Click) + + button.scaleOnClick = true + + shortcutView.userInteractionEnabled = false + shortcutView.isSelectable = false + } + + private var recorder:KeyboardGlobalHandler? + + private func toggleMode(animated: Bool, mode: PTTMode) { + + self.mode = mode + + guard let item = item as? PushToTalkRowItem else { + return + } + + switch mode { + case .editing: + let recorder = KeyboardGlobalHandler(mode: .global) + self.recorder = recorder + recorder.setKeyUpHandler(nil, success: { [weak item, weak self] result in + guard let item = item else { + return + } + let settings = PushToTalkValue(keyCodes: result.keyCodes, otherMouse: result.otherMouse, modifierFlags: result.modifierFlags, string: result.string) + item.update(settings) + item.settings = settings + self?.set(item: item, animated: true) + }) + + item.checkPermission() + + case .normal: + recorder = nil + } + + button.background = buttonColor + + if self.textView?.layout?.attributedString.string != buttonText.attributedString.string { + let textView = TextView() + textView.userInteractionEnabled = false + textView.isSelectable = false + textView.update(buttonText) + + button.addSubview(textView) + textView.center() + + button.change(size: NSMakeSize(textView.frame.width + 20, containerView.frame.height - 8), animated: animated) + button.change(pos: NSMakePoint(containerView.frame.width - button.frame.width - 4, button.frame.minY), animated: animated) + + if animated { + textView.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + if let textView = self.textView { + if animated { + textView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak textView] _ in + textView?.removeFromSuperview() + }) + } else { + textView.removeFromSuperview() + } + + } + self.textView = textView + } + + + + if animated { + button.layer?.animateBackground() + } + switch mode { + case .normal: + shimmerView.change(opacity: 0, animated: animated) + case .editing: + shimmerView.layer?.opacity = 1.0 + let animation = CABasicAnimation(keyPath: "opacity") + animation.fromValue = 0.5 + animation.toValue = 1.0 + animation.duration = 0.8 + animation.timingFunction = .init(name: .easeInEaseOut) + animation.repeatCount = .infinity + animation.autoreverses = true + shimmerView.layer?.add(animation, forKey: "opacity") + } + let attr: NSAttributedString + if let settings = item.settings { + attr = .initialize(string: settings.string, color: .white, font: .medium(.header)) + } else { + attr = .initialize(string: L10n.voiceChatSettingsPushToTalkUndefined, color: GroupCallTheme.grayStatusColor, font: .medium(.header)) + } + let layout = TextViewLayout(attr) + layout.measure(width: .greatestFiniteMagnitude) + + shortcutView.update(layout) + needsLayout = true + } + + + override func layout() { + super.layout() + + guard let item = item as? PushToTalkRowItem else { + return + } + if let textView = textView { + button.setFrameSize(NSMakeSize(textView.frame.width + 20, containerView.frame.height - 8)) + button.centerY(x: containerView.frame.width - button.frame.width - 4) + textView.center() + } + + + shortcutView.centerY(x: item.viewType.innerInset.left, addition: 1) + + shimmerView.frame = containerView.frame + } + + var buttonColor: NSColor { + switch mode { + case .editing: + return GroupCallTheme.speakLockedColor.withAlphaComponent(0.2) + case .normal: + return GroupCallTheme.speakInactiveColor.withAlphaComponent(0.2) + } + } + + var buttonText: TextViewLayout { + let textLayout: TextViewLayout + switch self.mode { + case .normal: + textLayout = TextViewLayout(.initialize(string: L10n.voiceChatSettingsPushToTalkEditKeybind, color: GroupCallTheme.speakInactiveColor, font: .medium(.text))) + case .editing: + textLayout = TextViewLayout(.initialize(string: L10n.voiceChatSettingsPushToTalkStopRecording, color: GroupCallTheme.speakLockedColor, font: .medium(.text))) + } + textLayout.measure(width: .greatestFiniteMagnitude) + return textLayout + } + + override func updateColors() { + super.updateColors() + shimmerView.backgroundColor = NSColor.redUI + } + + override var backdorColor: NSColor { + return GroupCallTheme.membersColor + } + + private var effectivePtt: PushToTalkValue? { + guard let item = item as? PushToTalkRowItem else { + return nil + } + return item.settings + } + + + +// override func viewWillMove(toWindow newWindow: NSWindow?) { +// if let window = newWindow as? Window { +// +// window.set(responder: { [weak self] in +// return self +// }, with: self, priority: .supreme) +// +// +// } else if let window = self.window as? Window { +// window.removeAllHandlers(for: self) +// } +// } + + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + self.toggleMode(animated: animated, mode: .normal) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/QRCode.swift b/Telegram-Mac/QRCode.swift new file mode 100644 index 0000000000..98caa292ff --- /dev/null +++ b/Telegram-Mac/QRCode.swift @@ -0,0 +1,303 @@ +// +// QRCode.swift +// Telegram +// +// Created by Mikhail Filimonov on 25/04/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import Foundation +import CoreImage +import SwiftSignalKit +import TGUIKit + +public enum QrCodeIcon { + case none + case cutout + case proxy + case custom(CGImage?) +} + + +private func floorToContextPixels(_ value: CGFloat, scale: CGFloat = System.backingScale) -> CGFloat { + return floor(value * scale) / scale +} + +private func roundToContextPixels(_ value: CGFloat, scale: CGFloat = System.backingScale) -> CGFloat { + return round(value * scale) / scale +} + +public func qrCodeCutout(size: Int, dimensions: CGSize, scale: CGFloat) -> (Int, CGRect, CGFloat) { + var cutoutSize = Int(round(CGFloat(size) * 0.297)) + if size == 39 { + cutoutSize = 11 + } else if cutoutSize % 2 == 0 { + cutoutSize += 1 + } + cutoutSize = min(23, cutoutSize) + + let quadSize = floorToContextPixels(dimensions.width / CGFloat(size), scale: scale) + let cutoutSide = quadSize * CGFloat(cutoutSize - 2) + let cutoutRect = CGRect(x: floorToContextPixels((dimensions.width - cutoutSide) / 2.0, scale: scale), y: floorToContextPixels((dimensions.height - cutoutSide) / 2.0, scale: scale), width: cutoutSide, height: cutoutSide) + + return (cutoutSize, cutoutRect, quadSize) +} + +public func qrCode(string: String, color: NSColor, backgroundColor: NSColor? = nil, icon: QrCodeIcon, ecl: String = "M") -> Signal<(Int, (TransformImageArguments) -> DrawingContext?), NoError> { + return Signal<(Data, Int, Int), NoError> { subscriber in + if let data = string.data(using: .isoLatin1, allowLossyConversion: false), let filter = CIFilter(name: "CIQRCodeGenerator") { + filter.setValue(data, forKey: "inputMessage") + filter.setValue(ecl, forKey: "inputCorrectionLevel") + + if let output = filter.outputImage { + let size = Int(output.extent.width) + let bytesPerRow = (4 * Int(size) + 15) & (~15) + let length = bytesPerRow * size + let bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.noneSkipFirst.rawValue) + + guard let bytes = malloc(length)?.assumingMemoryBound(to: UInt8.self) else { + return EmptyDisposable + } + let data = Data(bytesNoCopy: bytes, count: length, deallocator: .free) + + guard let context = CGContext(data: bytes, width: size, height: size, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo.rawValue) else { + return EmptyDisposable + } + +// context.translateBy(x: CGFloat(size) / 2.0, y: CGFloat(size) / 2.0) +// context.scaleBy(x: 1.0, y: -1.0) +// context.translateBy(x: -CGFloat(size) / 2.0, y: -CGFloat(size) / 2.0) + + let ciContext = CIContext(cgContext: context, options: [CIContextOption.useSoftwareRenderer : NSNumber(value: true)]) + ciContext.draw(output, in: CGRect(x: 0, y: 0, width: size, height: size), from: output.extent) + + + + subscriber.putNext((data, size, bytesPerRow)) + } + } + subscriber.putCompletion() + return EmptyDisposable + } + |> map { data, size, bytesPerRow in + return (size, { arguments in + let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale, clear: true) + + let drawingRect = arguments.drawingRect + let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) + let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) + + let (cutoutSize, clipRect, side) = qrCodeCutout(size: size, dimensions: fittedSize, scale: arguments.scale) + let padding: CGFloat = roundToContextPixels((arguments.drawingSize.width - CGFloat(side * CGFloat(size))) / 2.0, scale: arguments.scale) + + let cutout: (Int, Int)? + if case .none = icon { + cutout = nil + } else { + let start = (size - cutoutSize) / 2 + cutout = (start, start + cutoutSize - 1) + } + func valueAt(x: Int, y: Int) -> Bool { + if x >= 0 && x < size && y >= 0 && y < size { + if let cutout = cutout, x > cutout.0 && x < cutout.1 && y > cutout.0 && y < cutout.1 { + return false + } + + return data.withUnsafeBytes { bytes -> Bool in + if let value = bytes.baseAddress?.advanced(by: y * bytesPerRow + x * 4).assumingMemoryBound(to: UInt8.self).pointee { + return value < 255 + } else { + return false + } + } + } else { + return false + } + } + + let squareSize = CGSize(width: side, height: side) + let tmpContext = DrawingContext(size: CGSize(width: squareSize.width * 4.0, height: squareSize.height), scale: arguments.scale, clear: true) + tmpContext.withContext { c in + if let backgroundColor = backgroundColor { + c.setFillColor(backgroundColor.cgColor) + c.fill(CGRect(origin: CGPoint(), size: squareSize)) + } + c.setFillColor(color.cgColor) + + let outerRadius = squareSize.width / 3.0 + +// var path = NSBezierPath(roundedRect: CGRect(origin: CGPoint(), size: squareSize), byRoundingCorners: .allCorners, cornerRadii: CGSize(width: outerRadius, height: outerRadius)) + c.addPath(CGPath(roundedRect: CGRect(origin: CGPoint(), size: squareSize), cornerWidth: outerRadius, cornerHeight: outerRadius, transform: nil)) + c.fillPath() + + c.fill(CGRect(origin: CGPoint(x: squareSize.width * 2.0, y: 0.0), size: squareSize)) + + c.fill(CGRect(origin: CGPoint(x: squareSize.width, y: 0.0), size: squareSize)) + if let backgroundColor = backgroundColor { + c.setFillColor(backgroundColor.cgColor) + } else { + c.setBlendMode(.clear) + } + + let innerRadius = squareSize.width / 4.0 +// path = NSBezierPath(roundedRect: CGRect(origin: CGPoint(x: squareSize.width, y: 0.0), size: squareSize), byRoundingCorners: .allCorners, cornerRadii: CGSize(width: innerRadius, height: innerRadius)) + c.addPath(CGPath(roundedRect: CGRect(origin: CGPoint(x: squareSize.width, y: 0.0), size: squareSize), cornerWidth: innerRadius, cornerHeight: innerRadius, transform: nil)) + c.fillPath() + + c.fill(CGRect(origin: CGPoint(x: squareSize.width * 3.0, y: 0.0), size: squareSize)) + } + + let scaledSquareSize = Int(squareSize.width * tmpContext.scale) + let scaledPadding = Int(padding * tmpContext.scale) + let halfLen = scaledSquareSize * 4 / 2 + let blockLen = scaledSquareSize * 4 * 2 + + func drawAt(x: Int, y: Int, fill: Bool, corners: NSRectCorner) { + if !fill && corners.isEmpty { + return + } + + for i in 0 ..< scaledSquareSize { + var dst = context.bytes.advanced(by: (scaledPadding + y * scaledSquareSize + i) * context.bytesPerRow + (scaledPadding + x * scaledSquareSize) * 4) + let srcOffset = (fill ? 0 : scaledSquareSize * 4) + let src = tmpContext.bytes.advanced(by: i * tmpContext.bytesPerRow + srcOffset) + + if corners.contains(i < scaledSquareSize / 2 ? .topLeft : .bottomLeft) { + memcpy(dst, src, halfLen) + } else { + memcpy(dst, src + blockLen, halfLen) + } + dst += halfLen + if corners.contains(i < scaledSquareSize / 2 ? .topRight : .bottomRight) { + memcpy(dst, src + halfLen, halfLen) + } else { + memcpy(dst, src + blockLen + halfLen, halfLen) + } + } + } + + context.withContext { c in + if let backgroundColor = backgroundColor { + c.setFillColor(backgroundColor.cgColor) + c.fill(arguments.drawingRect) + } + + var markerSize: Int = 0 + for i in 1 ..< size { + if !valueAt(x: i, y: 1) { + markerSize = i - 1 + break + } + } + + for y in 0 ..< size { + for x in 0 ..< size { + if (y < markerSize + 1 && (x < markerSize + 1 || x > size - markerSize - 2)) || (y > size - markerSize - 2 && x < markerSize + 1) { + continue + } + + var corners: NSRectCorner = [] + if valueAt(x: x, y: y) { + corners = .all + if valueAt(x: x, y: y - 1) { + corners.remove(.topLeft) + corners.remove(.topRight) + } + if valueAt(x: x, y: y + 1) { + corners.remove(.bottomLeft) + corners.remove(.bottomRight) + } + if valueAt(x: x - 1, y: y) { + corners.remove(.topLeft) + corners.remove(.bottomLeft) + } + if valueAt(x: x + 1, y: y) { + corners.remove(.topRight) + corners.remove(.bottomRight) + } + drawAt(x: x, y: y, fill: true, corners: corners) + } else { + if valueAt(x: x - 1, y: y - 1) && valueAt(x: x - 1, y: y) && valueAt(x: x, y: y - 1) { + corners.insert(.topLeft) + } + if valueAt(x: x + 1, y: y - 1) && valueAt(x: x + 1, y: y) && valueAt(x: x, y: y - 1) { + corners.insert(.topRight) + } + if valueAt(x: x - 1, y: y + 1) && valueAt(x: x - 1, y: y) && valueAt(x: x, y: y + 1) { + corners.insert(.bottomLeft) + } + if valueAt(x: x + 1, y: y + 1) && valueAt(x: x + 1, y: y) && valueAt(x: x, y: y + 1) { + corners.insert(.bottomRight) + } + drawAt(x: x, y: y, fill: false, corners: corners) + } + } + } + + c.translateBy(x: padding, y: padding) + + + c.setLineWidth(squareSize.width) + c.setStrokeColor(color.cgColor) + c.setFillColor(color.cgColor) + + let markerSide = floorToContextPixels(CGFloat(markerSize - 1) * squareSize.width * 1.05, scale: arguments.scale) + + func drawMarker(x: CGFloat, y: CGFloat) { + //var path = NSBezierPath(roundedRect: CGRect(x: x + squareSize.width / 2.0, y: y + squareSize.width / 2.0, width: markerSide, height: markerSide), cornerRadius: markerSide / 3.5) + c.addPath(CGPath(roundedRect: CGRect(x: x + squareSize.width / 2.0, y: y + squareSize.width / 2.0, width: markerSide, height: markerSide), cornerWidth: markerSide / 3.5, cornerHeight: markerSide / 3.5, transform: nil)) + c.strokePath() + + let dotSide = markerSide - squareSize.width * 3.0 +// path = NSBezierPath(roundedRect: CGRect(x: x + squareSize.width * 2.0, y: y + squareSize.height * 2.0, width: dotSide, height: dotSide), cornerRadius: dotSide / 3.5) + c.addPath(CGPath(roundedRect: CGRect(x: x + squareSize.width * 2.0, y: y + squareSize.height * 2.0, width: dotSide, height: dotSide), cornerWidth: dotSide / 3.5, cornerHeight: dotSide / 3.5, transform: nil)) + c.fillPath() + } + + drawMarker(x: squareSize.width, y: squareSize.height) + drawMarker(x: CGFloat(size - 2) * squareSize.width - markerSide, y: CGFloat(size - 2) * squareSize.height - markerSide) + drawMarker(x: squareSize.width, y: CGFloat(size - 2) * squareSize.height - markerSide) + + c.translateBy(x: -padding, y: -padding) + + + switch icon { + case .proxy: + let iconScale = clipRect.size.width * 0.01111 + let iconSize = CGSize(width: 65.0 * iconScale, height: 79.0 * iconScale) + let point = CGPoint(x: fittedRect.midX - iconSize.width / 2.0, y: fittedRect.midY + iconSize.height / 2.0) + c.translateBy(x: point.x, y: point.y) + c.scaleBy(x: iconScale, y: -iconScale) + c.setFillColor(color.cgColor) + let _ = try? drawSvgPath(c, path: "M0.0,40 C0,20.3664202 20.1230605,0.0 32.5,0.0 C44.8769395,0.0 65,20.3664202 65,40 C65,47.217934 65,55.5505326 65,64.9977957 L32.5,79 L0.0,64.9977957 C0.0,55.0825772 0.0,46.7499786 0.0,40 Z") + + if let backgroundColor = backgroundColor { + c.setFillColor(backgroundColor.cgColor) + } else { + c.setBlendMode(.clear) + c.setFillColor(NSColor.clear.cgColor) + } + let _ = try? drawSvgPath(c, path: "M7.03608247,43.556701 L18.9836689,32.8350515 L32.5,39.871134 L45.8888139,32.8350515 L57.9639175,43.556701 L57.9639175,60.0 L32.5,71.0 L7.03608247,60.0 Z") + + c.setBlendMode(.normal) + c.setFillColor(color.cgColor) + let _ = try? drawSvgPath(c, path: "M24.1237113,50.5927835 L40.8762887,50.5927835 L40.8762887,60.9793814 L32.5,64.0928525 L24.1237113,60.9793814 Z") + case let .custom(image): + if let image = image { + let fittedSize = image.size.aspectFitted(NSMakeSize(clipRect.width - 3, clipRect.height - 3)) + let fittedRect = CGRect(origin: CGPoint(x: fittedRect.midX - fittedSize.width / 2.0, y: fittedRect.midY - fittedSize.height / 2.0), size: fittedSize) + c.translateBy(x: fittedRect.midX, y: fittedRect.midY) + c.translateBy(x: -fittedRect.midX, y: -fittedRect.midY) + c.draw(image, in: fittedRect) + } + break + default: + break + } + } + + return context + }) + } +} diff --git a/Telegram-Mac/QRLoginConfiguration.swift b/Telegram-Mac/QRLoginConfiguration.swift new file mode 100644 index 0000000000..e44b202d66 --- /dev/null +++ b/Telegram-Mac/QRLoginConfiguration.swift @@ -0,0 +1,64 @@ +// +// QRLoginConfiguration.swift +// Telegram +// +// Created by Mikhail Filimonov on 26.11.2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore +import SyncCore +import Postbox +import SwiftSignalKit +import TelegramApi + +enum QRLoginType : String { + case primary = "primary" + case secondary = "secondary" + case disabled = "disabled" +} + struct UnauthorizedConfiguration { + static var defaultValue: UnauthorizedConfiguration { + return UnauthorizedConfiguration(qr: .disabled) + } + + let qr: QRLoginType + + fileprivate init(qr: QRLoginType) { + self.qr = qr + } + +} + + +func unauthorizedConfiguration(network: Network) -> Signal { + return network.request(Api.functions.help.getAppConfig()) |> retryRequest + |> map { result -> UnauthorizedConfiguration in + if let data = JSON(apiJson: result), let rawQr = data["qr_login_code"] as? String, let qr = QRLoginType(rawValue: rawQr) { + return UnauthorizedConfiguration(qr: .secondary) + } else { + return .defaultValue + } + } +} + + +func managedAppConfigurationUpdates(postbox: Postbox, network: Network) -> Signal { + let poll = Signal { subscriber in + return (network.request(Api.functions.help.getAppConfig()) + |> retryRequest + |> mapToSignal { result -> Signal in + return postbox.transaction { transaction -> Void in + if let data = JSON(apiJson: result) { + updateAppConfiguration(transaction: transaction, { configuration -> AppConfiguration in + var configuration = configuration + configuration.data = data + return configuration + }) + } + } + }).start() + } + return (poll |> then(.complete() |> suspendAwareDelay(12.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart +} diff --git a/Telegram-Mac/QuickLookPreview.swift b/Telegram-Mac/QuickLookPreview.swift index 934929c15b..225f59d102 100644 --- a/Telegram-Mac/QuickLookPreview.swift +++ b/Telegram-Mac/QuickLookPreview.swift @@ -7,9 +7,10 @@ // import Cocoa -import SwiftSignalKitMac -import TelegramCoreMac -import PostboxMac +import SwiftSignalKit +import TelegramCore + +import Postbox import TGUIKit import Quartz import Foundation @@ -32,9 +33,9 @@ private class QuickLookPreviewItem : NSObject, QLPreviewItem { var previewItemTitle: String! { if let media = media as? TelegramMediaFile { - return media.fileName ?? tr(.quickLookPreview) + return media.fileName ?? L10n.quickLookPreview } - return tr(.quickLookPreview) + return L10n.quickLookPreview } } @@ -50,7 +51,7 @@ class QuickLookPreview : NSObject, QLPreviewPanelDelegate, QLPreviewPanelDataSou private weak var delegate:InteractionContentViewProtocol? private var item:QuickLookPreviewItem! - private var account:Account! + private var context: AccountContext! private var media:Media! private var ready:Promise<(String?,String?)> = Promise() private let disposable:MetaDisposable = MetaDisposable() @@ -62,46 +63,73 @@ class QuickLookPreview : NSObject, QLPreviewPanelDelegate, QLPreviewPanelDataSou super.init() } - public func show(account:Account, with media:Media, stableId:ChatHistoryEntryId?, _ delegate:InteractionContentViewProtocol? = nil) { - self.account = account + public func show(context: AccountContext, with media:Media, stableId:ChatHistoryEntryId?, _ delegate:InteractionContentViewProtocol? = nil) { + self.context = context self.media = media self.delegate = delegate self.stableId = stableId panel = QLPreviewPanel.shared() + + var mimeType:String = "image/jpeg" var fileResource:TelegramMediaResource? var fileName:String? = nil + var forceExtension: String? = nil + + let signal:Signal<(String?, String?), NoError> + if let file = media as? TelegramMediaFile { fileResource = file.resource mimeType = file.mimeType fileName = file.fileName - } else if let image = media as? TelegramMediaImage { - fileResource = largestImageRepresentation(image.representations)?.resource - } - - if let fileResource = fileResource { + if let ext = fileName?.nsstring.pathExtension, !ext.isEmpty { + forceExtension = ext + } - let signal = combineLatest(account.postbox.mediaBox.resourceData(fileResource), resourceType(mimeType: mimeType)) - |> mapToSignal({ (data) -> Signal<(String?,String?), Void> in - return .single((data.0.path,data.1)) - }) - |> deliverOnMainQueue - self.ready.set(signal) + signal = copyToDownloads(file, postbox: context.account.postbox) |> map { path in + if let path = path { + return (Optional(path.nsstring.deletingPathExtension), Optional(path.nsstring.pathExtension)) + } else { + return (nil, nil) + } + } + } else if let image = media as? TelegramMediaImage { + fileResource = largestImageRepresentation(image.representations)?.resource + if let fileResource = fileResource { + signal = combineLatest(context.account.postbox.mediaBox.resourceData(fileResource), resourceType(mimeType: mimeType)) + |> mapToSignal({ (data) -> Signal<(String?,String?), NoError> in + + return .single((data.0.path, forceExtension ?? data.1)) + }) |> deliverOnMainQueue + } else { + signal = .complete() + } + + } else { + signal = .complete() } + self.ready.set(signal |> deliverOnMainQueue) + - disposable.set(ready.get().start(next: {[weak self] (path,ext) in + disposable.set(ready.get().start(next: { [weak self] (path,ext) in if let strongSelf = self, let path = path { var ext:String? = ext if ext == nil || ext == "*" { ext = fileName?.nsstring.pathExtension } if let ext = ext { - strongSelf.item = QuickLookPreviewItem(with: strongSelf.media, path:path, ext:ext) - RunLoop.current.add(Timer.scheduledTimer(timeInterval: 0, target: strongSelf, selector: #selector(strongSelf.openPanelInRunLoop), userInfo: nil, repeats: false), forMode: RunLoopMode.modalPanelRunLoopMode) + + let item = QuickLookPreviewItem(with: strongSelf.media, path:path, ext:ext) + if ext == "pkpass" || !FastSettings.openInQuickLook(ext) { + NSWorkspace.shared.openFile(item.path) + return + } + strongSelf.item = item + RunLoop.current.add(Timer.scheduledTimer(timeInterval: 0, target: strongSelf, selector: #selector(strongSelf.openPanelInRunLoop), userInfo: nil, repeats: false), forMode: RunLoop.Mode.modalPanel) } } })) @@ -117,7 +145,7 @@ class QuickLookPreview : NSObject, QLPreviewPanelDelegate, QLPreviewPanelDataSou } else { panel.currentPreviewItemIndex = 0 } - + } @@ -146,10 +174,15 @@ class QuickLookPreview : NSObject, QLPreviewPanelDelegate, QLPreviewPanelDataSou return true } + override class func endPreviewPanelControl(_ panel: QLPreviewPanel!) { + var bp = 0 + bp += 1 + } + func previewPanel(_ panel: QLPreviewPanel!, sourceFrameOnScreenFor item: QLPreviewItem!) -> NSRect { if let stableId = stableId { - let view:NSView? = delegate?.contentInteractionView(for: stableId) + let view:NSView? = delegate?.contentInteractionView(for: stableId, animateIn: false) if let view = view, let window = view.window { // let tframe = view.frame @@ -160,10 +193,12 @@ class QuickLookPreview : NSObject, QLPreviewPanelDelegate, QLPreviewPanelDataSou return NSZeroRect } + + func previewPanel(_ panel: QLPreviewPanel!, transitionImageFor item: QLPreviewItem!, contentRect: UnsafeMutablePointer!) -> Any! { if let stableId = stableId { - let view:NSView? = delegate?.contentInteractionView(for: stableId) + let view:NSView? = delegate?.contentInteractionView(for: stableId, animateIn: true) if let view = view?.copy() as? View, let contents = view.layer?.contents { return NSImage(cgImage: contents as! CGImage, size: view.frame.size) @@ -176,7 +211,7 @@ class QuickLookPreview : NSObject, QLPreviewPanelDelegate, QLPreviewPanelDataSou if isOpened() { panel.orderOut(nil) } - self.account = nil + self.context = nil self.media = nil self.stableId = nil self.disposable.set(nil) diff --git a/Telegram-Mac/QuickSwitcherModalController.swift b/Telegram-Mac/QuickSwitcherModalController.swift index 8557d7b996..97a6abbc45 100644 --- a/Telegram-Mac/QuickSwitcherModalController.swift +++ b/Telegram-Mac/QuickSwitcherModalController.swift @@ -8,14 +8,15 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac +import Postbox +import TelegramCore + +import SwiftSignalKit private class QuickSwitcherArguments { - let account:Account - init(_ account:Account) { - self.account = account + let context: AccountContext + init(_ context:AccountContext) { + self.context = context } } @@ -25,52 +26,34 @@ private enum QuickSwitcherSeparator : Int32 { } private enum QuickSwitcherStableId : Hashable { - case peerId(PeerId) + case peerId(PeerId, SecretChatWrapper?) case separator(QuickSwitcherSeparator) case empty var hashValue: Int { - switch self { - case .peerId(let peerId): - return Int(peerId.id) - case .separator(let id): - return Int(id.hashValue) - case .empty: - return 0 - } + return 0 } - - static func ==(lhs:QuickSwitcherStableId, rhs: QuickSwitcherStableId) -> Bool { - switch lhs { - case .peerId(let peerId): - if case .peerId(peerId) = rhs { - return true - } else { - return false - } - case .separator(let id): - if case .separator(id) = rhs { - return true - } else { - return false - } - case .empty: - if case .empty = rhs { - return true - } else { - return false - } + var effectivePeerId: PeerId? { + switch self { + case let .peerId(peerId, secretPeerId): + return secretPeerId?.peerId ?? peerId + default: + return nil } } } +private struct SecretChatWrapper : Equatable { + let peerId:PeerId +} + private enum QuickSwitcherEntry : TableItemListNodeEntry { - case peer(Int32, Peer, Bool) + case peer(Int32, Peer, Bool, SecretChatWrapper?) case separator(Int32, QuickSwitcherSeparator) case empty var stableId:QuickSwitcherStableId { switch self { - case .peer(_, let peer, _): - return .peerId(peer.id) + case let .peer(_, peer, _, secretChat): + return .peerId(peer.id, secretChat) case .separator(_, let id): return .separator(id) case .empty: @@ -80,7 +63,7 @@ private enum QuickSwitcherEntry : TableItemListNodeEntry { var index:Int32 { switch self { - case .peer(let index, _, _): + case .peer(let index, _, _, _): return index case .separator(let index, _): return index @@ -91,17 +74,17 @@ private enum QuickSwitcherEntry : TableItemListNodeEntry { func item(_ arguments: QuickSwitcherArguments, initialSize: NSSize) -> TableRowItem { switch self { - case .peer(_, let peer, let drawSeparator): - return ShortPeerRowItem(initialSize, peer: peer, account: arguments.account, stableId: stableId, height: 40, photoSize: NSMakeSize(30, 30), drawCustomSeparator: drawSeparator, action: { + case let .peer(_, peer, drawSeparator, secretChat): + return ShortPeerRowItem(initialSize, peer: peer, account: arguments.context.account, stableId: stableId, height: 40, photoSize: NSMakeSize(30, 30), titleStyle: ControlStyle(font: .medium(.text), foregroundColor: secretChat != nil ? theme.colors.accent : theme.colors.text, highlightColor:.white), drawCustomSeparator: drawSeparator, isLookSavedMessage: true, action: { }) case .separator(_, let id): let text:String switch id { case .recently: - text = tr(.quickSwitcherRecently) + text = tr(L10n.quickSwitcherRecently) case .popular: - text = tr(.quickSwitcherPopular) + text = tr(L10n.quickSwitcherPopular) } return SeparatorRowItem(initialSize, stableId, string: text.uppercased()) case .empty: @@ -112,11 +95,14 @@ private enum QuickSwitcherEntry : TableItemListNodeEntry { private func ==(lhs: QuickSwitcherEntry, rhs: QuickSwitcherEntry) -> Bool { switch lhs { - case let .peer(lhsIndex, lhsPeer, lhsDrawSeparator): - if case let .peer(rhsIndex, rhsPeer, rhsDrawSeparator) = rhs { + case let .peer(lhsIndex, lhsPeer, lhsDrawSeparator, lhsSecretChat): + if case let .peer(rhsIndex, rhsPeer, rhsDrawSeparator, rhsSecretChat) = rhs { if lhsIndex != rhsIndex { return false } + if lhsSecretChat != rhsSecretChat { + return false + } if lhsDrawSeparator != rhsDrawSeparator { return false } @@ -162,7 +148,7 @@ private class QuickSwitcherView : View { separator.backgroundColor = theme.colors.border self.backgroundColor = theme.colors.background let attributed = NSMutableAttributedString() - _ = attributed.append(string: tr(.quickSwitcherDescription), color: theme.colors.grayText, font: .normal(.text)) + _ = attributed.append(string: L10n.quickSwitcherDescription, color: theme.colors.grayText, font: .normal(.text)) attributed.detectBoldColorInString(with: .medium(.text)) let descLayout = TextViewLayout(attributed, alignment: .center) descLayout.measure(width: frameRect.width - 20) @@ -173,9 +159,9 @@ private class QuickSwitcherView : View { override func layout() { super.layout() - searchView.centerX(y: floorToScreenPixels((50 - 30)/2)) + searchView.centerX(y: floorToScreenPixels(backingScaleFactor, (50 - 30)/2)) tableView.frame = NSMakeRect(0, 50, frame.width, frame.height - 100) - textView.centerX(y: frame.height - floorToScreenPixels((50 - textView.frame.height)/2) - textView.frame.height) + textView.centerX(y: frame.height - floorToScreenPixels(backingScaleFactor, (50 - textView.frame.height)/2) - textView.frame.height) separator.frame = NSMakeRect(0, frame.height - 50, frame.width, .borderSize) } @@ -185,7 +171,7 @@ private class QuickSwitcherView : View { } -private func searchEntriesForPeers(_ peers:[Peer], account:Account, recentlyUsed:[Peer], isLoading: Bool) -> [QuickSwitcherEntry] { +private func searchEntriesForPeers(_ peers:[Peer], account:Account, recentlyUsed:[(Peer, SecretChatWrapper?)], isLoading: Bool) -> [QuickSwitcherEntry] { var entries: [QuickSwitcherEntry] = [] var index:Int32 = 0 @@ -198,10 +184,10 @@ private func searchEntriesForPeers(_ peers:[Peer], account:Account, recentlyUsed var isset:[PeerId:PeerId] = [:] for peer in recentlyUsed { - if account.peerId != peer.id, isset[peer.id] == nil { - entries.append(.peer(index, peer, peer.id != recentlyUsed.last?.id)) + if isset[peer.0.id] == nil { + entries.append(.peer(index, peer.0, peer.0.id != recentlyUsed.last?.0.id, peer.1)) index += 1 - isset[peer.id] = peer.id + isset[peer.0.id] = peer.0.id } } @@ -211,8 +197,8 @@ private func searchEntriesForPeers(_ peers:[Peer], account:Account, recentlyUsed } for peer in peers { - if account.peerId != peer.id, isset[peer.id] == nil { - entries.append(.peer(index, peer, true)) + if isset[peer.id] == nil { + entries.append(.peer(index, peer, true, nil)) index += 1 isset[peer.id] = peer.id } @@ -231,54 +217,106 @@ fileprivate func prepareTransition(left:[AppearanceWrapperEntry AnyHashable? { + return nil + } + + private let context:AccountContext private let search:ValuePromise = ValuePromise(ignoreRepeated: true) private let disposable = MetaDisposable() - fileprivate func start(account: Account, recentlyUsed:[PeerId], search:Signal) -> Signal<([QuickSwitcherEntry], Bool), Void> { + fileprivate func start(context: AccountContext, recentlyUsed:[PeerId], search:Signal) -> Signal<([QuickSwitcherEntry], Bool), NoError> { - return search |> mapToSignal { search -> Signal<([QuickSwitcherEntry], Bool), Void> in + return search |> mapToSignal { search -> Signal<([QuickSwitcherEntry], Bool), NoError> in if search.request.isEmpty { - return combineLatest(account.postbox.recentPeers(), account.postbox.multiplePeersView(recentlyUsed) |> take(1)) + return combineLatest(context.engine.peers.recentPeers() |> take(1), context.account.postbox.multiplePeersView(recentlyUsed) |> take(1)) |> deliverOn(prepareQueue) - |> mapToSignal { peers, view -> Signal<([QuickSwitcherEntry], Bool), Void> in + |> mapToSignal { recentPeers, view -> Signal<([QuickSwitcherEntry], Bool), NoError> in - var recentl:[Peer] = [] + var peers:[Peer] = [] + + switch recentPeers { + case let .peers(list): + peers = list + default: + break + } + + var recentl:[(Peer, SecretChatWrapper?)] = [] for peerId in recentlyUsed { if let peer = view.peers[peerId] { - recentl.append(peer) + recentl.append((peer, nil)) + } + } + let secretChats = recentl.compactMap { $0.0 as? TelegramSecretChat }.compactMap { $0.associatedPeerId } + + if !secretChats.isEmpty { + return context.account.postbox.multiplePeersView(secretChats) |> take(1) |> map { secretPeers in + var recentl:[(Peer, SecretChatWrapper?)] = [] + for peerId in recentlyUsed { + if let peer = view.peers[peerId] { + if let peer = peer as? TelegramSecretChat { + if let secretPeer = secretPeers.peers[peer.associatedPeerId!] { + recentl.append((secretPeer, SecretChatWrapper(peerId: peer.id))) + } + } else { + recentl.append((peer, nil)) + } + } + } + return (searchEntriesForPeers(peers, account: context.account, recentlyUsed: recentl, isLoading: false), false) } + } else { + return .single((searchEntriesForPeers(peers, account: context.account, recentlyUsed: recentl, isLoading: false), false)) } - return .single((searchEntriesForPeers(peers, account: account, recentlyUsed: recentl, isLoading: false), false)) } } else { - let foundLocalPeers = account.postbox.searchContacts(query: search.request.lowercased()) - let foundRemotePeers = account.postbox.searchPeers(query: search.request.lowercased()) |> map {$0.flatMap({$0.chatMainPeer}).filter({!($0 is TelegramSecretChat)})} + var all = search.request.transformKeyboard + all.insert(search.request.lowercased(), at: 0) + all = all.uniqueElements + let localPeers = combineLatest(all.map { + return context.account.postbox.searchPeers(query: $0) + }) |> map { result in + return result.reduce([], { + return $0 + $1 + }) + } - return combineLatest(foundLocalPeers, foundRemotePeers) |> map { values -> ([Peer], Bool) in - return (uniquePeers(from: (values.1 + values.0)), false) + let foundLocalPeers = localPeers |> map { + return $0.compactMap({$0.chatMainPeer}).filter({!($0 is TelegramSecretChat)}) + } + + + + let foundRemotePeers = Signal<[Peer], NoError>.single([]) |> then(context.engine.peers.searchPeers(query: search.request.lowercased()) |> map { $0.0.map({$0.peer}) + $0.1.map{$0.peer} } ) + + return combineLatest(combineLatest(foundLocalPeers, foundRemotePeers) |> map {$0 + $1}, context.account.postbox.loadedPeerWithId(context.peerId)) |> map { values -> ([Peer], Bool) in + var peers = values.0 + if L10n.peerSavedMessages.lowercased().hasPrefix(search.request.lowercased()) || NSLocalizedString("Peer.SavedMessages", comment: "nil").lowercased().hasPrefix(search.request.lowercased()) { + peers.insert(values.1, at: 0) + } + + return (uniquePeers(from: peers), false) } |> runOn(prepareQueue) |> map { values -> ([QuickSwitcherEntry], Bool) in - return (searchEntriesForPeers(values.0, account: account, recentlyUsed: [], isLoading: values.1), values.1) + return (searchEntriesForPeers(values.0, account: context.account, recentlyUsed: [], isLoading: values.1), values.1) } } - } - } - init(account:Account) { - self.account = account + init(_ context: AccountContext) { + self.context = context super.init(frame: NSMakeRect(0, 0, 300, 360)) bar = .init(height: 0) } @@ -307,7 +345,7 @@ class QuickSwitcherModalController: ModalViewController, TableViewDelegate { return true } - func selectionWillChange(row:Int, item:TableRowItem) -> Bool { + func selectionWillChange(row:Int, item:TableRowItem, byClick: Bool) -> Bool { return item is ShortPeerRowItem } @@ -326,13 +364,13 @@ class QuickSwitcherModalController: ModalViewController, TableViewDelegate { genericView.tableView.delegate = self search.set(SearchState(state: .None, request: nil)) - let searchInteractions = SearchInteractions({ [weak self] state in + let searchInteractions = SearchInteractions({ [weak self] state, _ in self?.search.set(state) }, { [weak self] state in self?.search.set(state) }) - let arguments = QuickSwitcherArguments(account) + let arguments = QuickSwitcherArguments(context) genericView.searchView.searchInteractions = searchInteractions @@ -341,7 +379,7 @@ class QuickSwitcherModalController: ModalViewController, TableViewDelegate { let previous:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) let initialSize = atomicSize - disposable.set((combineLatest(start(account: account, recentlyUsed: account.context.recentlyPeerUsed, search: search.get()), appearanceSignal) |> map { value, appearance -> (TableUpdateTransition, Bool) in + disposable.set((combineLatest(start(context: context, recentlyUsed: Array(context.recentlyPeerUsed.prefix(3)), search: search.get()), appearanceSignal) |> map { value, appearance -> (TableUpdateTransition, Bool) in let entries = value.0.map{AppearanceWrapperEntry(entry: $0, appearance: appearance)} return (prepareTransition(left: previous.swap(entries), right: entries, initialSize: initialSize.modify {$0}, arguments: arguments), value.1) } |> deliverOnMainQueue).start(next: { [weak self] value in @@ -356,26 +394,43 @@ class QuickSwitcherModalController: ModalViewController, TableViewDelegate { override func returnKeyAction() -> KeyHandlerResult { if let selectedItem = genericView.tableView.selectedItem() as? ShortPeerRowItem { - account.context.mainNavigation?.push(ChatController(account: account, peerId: selectedItem.peer.id)) + let query = self.genericView.searchView.query + var peerId = selectedItem.peer.id + var messageId: MessageId? = nil + let link = inApp(for: query as NSString, context: context, peerId: peerId, openInfo: { _, _, _, _ in }, hashtag: nil, command: nil, applyProxy: nil, confirm: false) + switch link { + case let .followResolvedName(_, _, postId, _, _, _): + if let postId = postId { + messageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: postId) + } + default: + break + } + + if let stableId = selectedItem.stableId as? QuickSwitcherStableId, let effectivePeerId = stableId.effectivePeerId { + peerId = effectivePeerId + } + + context.sharedContext.bindings.rootNavigation().push(ChatController(context: context, chatLocation: .peer(peerId), messageId: messageId)) close() } - return .rejected + return .invoked } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - self.window?.set(handler: { [weak self] () -> KeyHandlerResult in + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in self?.genericView.tableView.selectPrev() return .invoked }, with: modal!, for: .UpArrow, priority: .modal) - self.window?.set(handler: { [weak self] () -> KeyHandlerResult in + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in self?.genericView.tableView.selectNext() return .invoked }, with: modal!, for: .DownArrow, priority: .modal) - self.window?.set(handler: { [weak self] () -> KeyHandlerResult in + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in self?.genericView.tableView.selectNext() return .invoked }, with: modal!, for: .Tab, priority: .modal) diff --git a/Telegram-Mac/ReadArticlesListPreferences.swift b/Telegram-Mac/ReadArticlesListPreferences.swift new file mode 100644 index 0000000000..32df01cba1 --- /dev/null +++ b/Telegram-Mac/ReadArticlesListPreferences.swift @@ -0,0 +1,184 @@ +// +// ReadArticlesListPreferences.swift +// Telegram +// +// Created by Mikhail Filimonov on 02/07/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import Postbox +import SwiftSignalKit +import TelegramCore + + +final class ReadArticle : PreferencesEntry, Equatable { + static func == (lhs: ReadArticle, rhs: ReadArticle) -> Bool { + return lhs.messageId == rhs.messageId && lhs.webPage.webpageId == rhs.webPage.webpageId && lhs.percent == rhs.percent && lhs.date == rhs.date + } + + var id: MediaId { + return webPage.webpageId + } + + init(webPage: TelegramMediaWebpage, messageId: MessageId?, percent: Int32, date: Int32) { + self.messageId = messageId + self.webPage = webPage + self.percent = percent + self.date = date + } + let percent: Int32 + let webPage: TelegramMediaWebpage + let messageId: MessageId? + let date: Int32 + func isEqual(to: PreferencesEntry) -> Bool { + if let to = to as? ReadArticle { + return to == self + } else { + return false + } + } + + init(decoder: PostboxDecoder) { + if let messageIdPeerId = decoder.decodeOptionalInt64ForKey("m.p"), let messageIdNamespace = decoder.decodeOptionalInt32ForKey("m.n"), let messageIdId = decoder.decodeOptionalInt32ForKey("m.i") { + self.messageId = MessageId(peerId: PeerId(messageIdPeerId), namespace: messageIdNamespace, id: messageIdId) + } else { + self.messageId = nil + } + self.webPage = decoder.decodeObjectForKey("wp", decoder: {TelegramMediaWebpage(decoder: $0)}) as! TelegramMediaWebpage + self.percent = decoder.decodeInt32ForKey("p", orElse: 0) + self.date = decoder.decodeInt32ForKey("d", orElse: 0) + } + + func encode(_ encoder: PostboxEncoder) { + if let messageId = messageId { + encoder.encodeInt64(messageId.peerId.toInt64(), forKey: "m.p") + encoder.encodeInt32(messageId.namespace, forKey: "m.n") + encoder.encodeInt32(messageId.id, forKey: "m.i") + } else { + encoder.encodeNil(forKey: "m.p") + encoder.encodeNil(forKey: "m.n") + encoder.encodeNil(forKey: "m.i") + } + encoder.encodeObject(webPage, forKey: "wp") + encoder.encodeInt32(percent, forKey: "p") + encoder.encodeInt32(date, forKey: "d") + } + + func withUpdatedPercent(_ percent: Int32, force: Bool = false) -> ReadArticle { + return ReadArticle(webPage: webPage, messageId: messageId, percent: force ? percent : min(max(percent, self.percent), 100), date: force && percent == 100 ? Int32(Date().timeIntervalSince1970) : self.date) + } + + +} + +class ReadArticlesListPreferences: PreferencesEntry, Equatable { + + + let list: [ReadArticle] + + func isEqual(to: PreferencesEntry) -> Bool { + if let to = to as? ReadArticlesListPreferences { + return self == to + } else { + return false + } + } + + init(list: [ReadArticle] = []) { + self.list = list + } + + static func == (lhs: ReadArticlesListPreferences, rhs: ReadArticlesListPreferences) -> Bool { + return lhs.list == rhs.list + } + + required init(decoder: PostboxDecoder) { + self.list = decoder.decodeObjectArrayForKey("l") + } + + func withAddedArticle(_ article: ReadArticle) -> ReadArticlesListPreferences { + var list = self.list + if let index = firstIndex(article) { + list.remove(at: index) + list.insert(article, at: 0) + } else { + list.insert(article, at: 0) + } + return ReadArticlesListPreferences(list: list) + } + + func withReadAll() -> ReadArticlesListPreferences { + var list = self.list + for i in 0 ..< list.count { + list[i] = list[i].withUpdatedPercent(100) + } + return ReadArticlesListPreferences(list: list) + } + + func withRemovedAll() -> ReadArticlesListPreferences { + return ReadArticlesListPreferences(list: []) + } + + func withUpdatedArticle(_ article: ReadArticle) -> ReadArticlesListPreferences { + var list = self.list + + if let index = firstIndex(article) { + list[index] = article + } + return ReadArticlesListPreferences(list: list) + } + + private func firstIndex(_ article: ReadArticle) -> Int? { + for i in 0 ..< list.count { + if list[i].id == article.id { + return i + } + } + return nil + } + + var unreadList: [ReadArticle] { + return list.filter({$0.percent < 100}) + } + + func withRemovedArticles(_ article: ReadArticle) -> ReadArticlesListPreferences { + var list = self.list + if let index = firstIndex(article) { + list.remove(at: index) + } + return ReadArticlesListPreferences(list: list) + } + + func encode(_ encoder: PostboxEncoder) { + encoder.encodeObjectArray(self.list, forKey: "l") + } + + + static var defaultSettings: ReadArticlesListPreferences { + return ReadArticlesListPreferences() + } + +} + + +func readArticlesListPreferences(_ postbox: Postbox) -> Signal { + return postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.readArticles]) |> map { preferences in + return (preferences.values[ApplicationSpecificPreferencesKeys.readArticles] as? ReadArticlesListPreferences) ?? ReadArticlesListPreferences.defaultSettings + } +} + +func updateReadArticlesPreferences(postbox: Postbox, _ f:@escaping(ReadArticlesListPreferences)->ReadArticlesListPreferences) -> Signal { + + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.readArticles, { entry in + let currentSettings: ReadArticlesListPreferences + if let entry = entry as? ReadArticlesListPreferences { + currentSettings = entry + } else { + currentSettings = ReadArticlesListPreferences.defaultSettings + } + return f(currentSettings) + }) + } +} diff --git a/Telegram-Mac/RecentCallsViewController.swift b/Telegram-Mac/RecentCallsViewController.swift index 88b462de9a..340250d6f2 100644 --- a/Telegram-Mac/RecentCallsViewController.swift +++ b/Telegram-Mac/RecentCallsViewController.swift @@ -8,16 +8,17 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit private final class RecentCallsArguments { let call:(PeerId)->Void - let removeCalls:([MessageId]) -> Void - let account:Account - init(account: Account, call:@escaping(PeerId)->Void, removeCalls:@escaping([MessageId]) ->Void ) { - self.account = account + let removeCalls:([MessageId], Peer) -> Void + let context:AccountContext + init(context: AccountContext, call:@escaping(PeerId)->Void, removeCalls:@escaping([MessageId], Peer) ->Void ) { + self.context = context self.removeCalls = removeCalls self.call = call } @@ -52,7 +53,7 @@ private enum RecentCallEntry : TableItemListNodeEntry { return false } - if lhsMessage.stableVersion != rhsMessage.stableVersion { + if lhsMessage.id != rhsMessage.id { return false } @@ -60,7 +61,7 @@ private enum RecentCallEntry : TableItemListNodeEntry { return false } else { for i in 0 ..< lhsMessages.count { - if lhsMessages[i].stableVersion != rhsMessages[i].stableVersion { + if lhsMessages[i].id != rhsMessages[i].id { return false } } @@ -93,17 +94,17 @@ private enum RecentCallEntry : TableItemListNodeEntry { func item(_ arguments: RecentCallsArguments, initialSize: NSSize) -> TableRowItem { switch self { case let .calls(message, messages, editing, failed): - + let peer = messageMainPeer(message)! + let interactionType:ShortPeerItemInteractionType if editing { interactionType = .deletable(onRemove: { peerId in - arguments.removeCalls(messages.map{$0.id}) + arguments.removeCalls(messages.map{ $0.id }, peer) }, deletable: true) } else { interactionType = .plain } - let peer = messageMainPeer(message)! let titleStyle = ControlStyle(font: .medium(.title), foregroundColor: failed ? theme.colors.redUI : theme.colors.text) @@ -118,11 +119,11 @@ private enum RecentCallEntry : TableItemListNodeEntry { let statusText:String if failed { - statusText = tr(.callRecentMissed) + statusText = tr(L10n.callRecentMissed) } else { - let text = outgoing ? tr(.callRecentOutgoing) : tr(.callRecentIncoming) + let text = outgoing ? tr(L10n.callRecentOutgoing) : tr(L10n.callRecentIncoming) if messages.count == 1 { - if let action = messages[0].media.first as? TelegramMediaAction, case .phoneCall(_,_,let duration) = action.action, let value = duration, value > 0 { + if let action = messages[0].media.first as? TelegramMediaAction, case .phoneCall(_, _, let duration, _) = action.action, let value = duration, value > 0 { statusText = text + " (\(String.stringForShortCallDurationSeconds(for: value)))" } else { statusText = text @@ -140,13 +141,17 @@ private enum RecentCallEntry : TableItemListNodeEntry { } - return ShortPeerRowItem(initialSize, peer: peer, account: arguments.account, stableId: stableId, height: 46, titleStyle: titleStyle, titleAddition: countText, leftImage: outgoing ? theme.icons.callOutgoing : nil, status: statusText , borderType: [.Right], drawCustomSeparator:true, deleteInset: 10, inset: NSEdgeInsets( left: outgoing ? 10 : theme.icons.callOutgoing.backingSize.width + 15, right: 10), drawSeparatorIgnoringInset: true, interactionType: interactionType, generalType: .context(stateback: {return DateUtils.string(forMessageListDate: messages.first!.timestamp)}), action: { + return ShortPeerRowItem(initialSize, peer: peer, account: arguments.context.account, stableId: stableId, height: 46, titleStyle: titleStyle, titleAddition: countText, leftImage: outgoing ? theme.icons.callOutgoing : nil, status: statusText , borderType: [.Right], drawCustomSeparator:true, deleteInset: 10, inset: NSEdgeInsets( left: outgoing ? 10 : theme.icons.callOutgoing.backingSize.width + 15, right: 10), drawSeparatorIgnoringInset: true, interactionType: interactionType, generalType: .context(DateUtils.string(forMessageListDate: messages.first!.timestamp)), action: { if !editing { arguments.call(peer.id) } + }, contextMenuItems: { + return .single([ContextMenuItem(L10n.recentCallsDelete, handler: { + arguments.removeCalls(messages.map{ $0.id }, peer) + })]) }) case .empty(let loading): - return SearchEmptyRowItem(initialSize, stableId: stableId, isLoading: loading, text: tr(.recentCallsEmpty), border: [.Right]) + return SearchEmptyRowItem(initialSize, stableId: stableId, isLoading: loading, text: tr(L10n.recentCallsEmpty), border: [.Right]) } } } @@ -155,13 +160,23 @@ private enum RecentCallEntry : TableItemListNodeEntry { class RecentCallsViewController: NavigationViewController { private var layoutController:LayoutRecentCallsViewController - init(_ account:Account) { - self.layoutController = LayoutRecentCallsViewController(account) - super.init(layoutController) + init(_ context:AccountContext) { + self.layoutController = LayoutRecentCallsViewController(context) + super.init(layoutController, context.window) bar = .init(height: 0) + } + + override func viewDidLoad() { + super.viewDidLoad() self.push(layoutController, false) } + override func viewDidResized(_ size: NSSize) { + super.viewDidResized(size) + navigationBar.frame = NSMakeRect(0, 0, bounds.width, layoutController.bar.height) + layoutController.frame = NSMakeRect(0, layoutController.bar.height, bounds.width, bounds.height - layoutController.bar.height) + } + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) @@ -258,8 +273,7 @@ private func makeEntries(from: [CallListViewEntry], state: RecentCallsController var failed:Bool = false let outgoing: Bool = !message.flags.contains(.Incoming) if let action = message.media.first as? TelegramMediaAction { - if case .phoneCall(_, let discardReason, _) = action.action { - + if case .phoneCall(_, let discardReason, _, _) = action.action { var missed: Bool = false if let reason = discardReason { switch reason { @@ -270,7 +284,6 @@ private func makeEntries(from: [CallListViewEntry], state: RecentCallsController } } failed = !outgoing && missed - } } entries.append(.calls( message, messages, state.editing, failed)) @@ -293,7 +306,7 @@ class LayoutRecentCallsViewController: EditableViewController { private let callDisposable:MetaDisposable = MetaDisposable() private let againDisposable:MetaDisposable = MetaDisposable() private var first:Bool = false - + private let disposable = MetaDisposable() var navigation:NavigationViewController? { @@ -301,11 +314,11 @@ class LayoutRecentCallsViewController: EditableViewController { } override var enableBack: Bool { - return false + return true } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - navigationController?.updateLocalizationAndTheme() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + navigationController?.updateLocalizationAndTheme(theme: theme) } override func viewDidLoad() { @@ -313,19 +326,9 @@ class LayoutRecentCallsViewController: EditableViewController { genericView.border = [.Right] self.rightBarView.border = [.Right] - } - - override func update(with state: ViewControllerState) { - super.update(with: state) - self.statePromise.set(stateValue.modify({$0.withUpdatedEditing(state == .Edit)})) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - updateLocalizationAndTheme() let previous = self.previous let initialSize = self.atomicSize - let account = self.account + let context = self.context let updateState: ((RecentCallsControllerState) -> RecentCallsControllerState) -> Void = { [weak self] f in @@ -334,37 +337,49 @@ class LayoutRecentCallsViewController: EditableViewController { } } - let arguments = RecentCallsArguments(account: account, call: { [weak self] peerId in - self?.callDisposable.set((phoneCall(account, peerId: peerId) |> deliverOnMainQueue).start(next: { result in - applyUIPCallResult(account, result) + let arguments = RecentCallsArguments(context: context, call: { [weak self] peerId in + self?.callDisposable.set((phoneCall(account: context.account, sharedContext: context.sharedContext, peerId: peerId) |> deliverOnMainQueue).start(next: { result in + applyUIPCallResult(context.sharedContext, result) })) - }, removeCalls: { [weak self] messageIds in - _ = deleteMessagesInteractively(postbox: account.postbox, messageIds: messageIds, type: .forLocalPeer).start() - updateState({$0.withAdditionalIgnoringIds(messageIds)}) - - if let strongSelf = self { - strongSelf.againDisposable.set((Signal<()->Void, Void>.single({ [weak strongSelf] in - strongSelf?.viewWillAppear(false) + }, removeCalls: { [weak self] messageIds, peer in + modernConfirm(for: context.window, account: context.account, peerId: nil, header: L10n.recentCallsDeleteHeader, information: L10n.recentCallsDeleteCalls, okTitle: L10n.recentCallsDelete, cancelTitle: L10n.modalCancel, thridTitle: L10n.recentCallsDeleteForMeAnd(peer.compactDisplayTitle), thridAutoOn: true, successHandler: { [weak self] result in + + let type: InteractiveMessagesDeletionType + switch result { + case .thrid: + type = .forEveryone + default: + type = .forLocalPeer + } + _ = context.engine.messages.deleteMessagesInteractively(messageIds: messageIds, type: type).start() + updateState({$0.withAdditionalIgnoringIds(messageIds)}) + + self?.againDisposable.set((Signal<()->Void, NoError>.single({ [weak self] in + self?.viewWillAppear(false) }) |> delay(1.5, queue: Queue.mainQueue())).start(next: {value in value()})) - } - self?.viewWillAppear(false) + }) }) let callListView:Atomic = Atomic(value: nil) let location:ValuePromise = ValuePromise() - + let first:Atomic = Atomic(value: true) - let signal = location.get() |> distinctUntilChanged |> mapToSignal { index in - return account.viewTracker.callListView(type: .all, index: index, count: 200) + let signal: Signal = location.get() |> distinctUntilChanged |> mapToSignal { index in + return context.account.viewTracker.callListView(type: .all, index: index, count: 100) } - genericView.merge(with: combineLatest(signal |> deliverOn(prepareQueue), statePromise.get() |> deliverOn(prepareQueue), appearanceSignal |> deliverOn(prepareQueue)) |> map { result in + let transition:Signal = combineLatest(queue: prepareQueue, signal, statePromise.get(), appearanceSignal) |> map { result in _ = callListView.swap(result.0) let entries = makeEntries(from: result.0.entries, state: result.1).map({AppearanceWrapperEntry(entry: $0, appearance: result.2)}) return prepareTransition(left: previous.swap(entries), right: entries, initialSize: initialSize.modify{$0}, arguments: arguments, animated: !first.swap(false)) - } |> deliverOnMainQueue) + } |> deliverOnMainQueue + + disposable.set(transition.start(next: { [weak self] transition in + self?.genericView.merge(with: transition) + })) + readyOnce() @@ -393,6 +408,23 @@ class LayoutRecentCallsViewController: EditableViewController { } + override func update(with state: ViewControllerState) { + super.update(with: state) + self.statePromise.set(stateValue.modify({$0.withUpdatedEditing(state == .Edit)})) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + updateLocalizationAndTheme(theme: theme) + } + + override func backSettings() -> (String, CGImage?) { + return ("", theme.icons.callSettings) + } + + override func executeReturn() { + showModal(with: CallSettingsModalController(context.sharedContext), for: context.window) + } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) @@ -403,6 +435,7 @@ class LayoutRecentCallsViewController: EditableViewController { deinit { callDisposable.dispose() againDisposable.dispose() + disposable.dispose() } } diff --git a/Telegram-Mac/RecentGIFRowItem.swift b/Telegram-Mac/RecentGIFRowItem.swift index fda92991f8..91e6d0504d 100644 --- a/Telegram-Mac/RecentGIFRowItem.swift +++ b/Telegram-Mac/RecentGIFRowItem.swift @@ -8,151 +8,8 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac +import TelegramCore -class RecentGIFRowItem: TableRowItem { - fileprivate let entry:RecentGifRowEntry - fileprivate let row:RecentGifRow - fileprivate let account:Account - fileprivate let arguments:RecentGifsArguments - init(_ initialSize: NSSize, account:Account, entry:RecentGifRowEntry, arguments:RecentGifsArguments) { - self.entry = entry - self.account = account - self.arguments = arguments - switch entry { - case let .gif(index: _, row: r): - self.row = r - } - super.init(initialSize) - } - - override var stableId: AnyHashable { - return entry.stableId - } - - - - override var height: CGFloat { - var height:CGFloat = 120 - for size in row.sizes { - height = min(height, size.height) - } - return height - } - - override func viewClass() -> AnyClass { - return RecentGIFRowView.self - } - -} +import SwiftSignalKit +import Postbox - -private var dif:CGFloat = 0 - -class RecentGIFRowView: TableRowView { - private let stickerFetchedDisposable:MetaDisposable = MetaDisposable() - - deinit { - stickerFetchedDisposable.dispose() - removeAllSubviews() - } - - override func set(item: TableRowItem, animated: Bool) { - super.set(item: item, animated: animated) - removeAllSubviews() - if let item = item as? RecentGIFRowItem { - var inset:CGFloat = 0 - for i in 0 ..< item.row.entries.count { - - let view = GIFContainerView() - - view.playerInset = NSEdgeInsets(left: i == 0 ? 2 : 1, right: i == item.row.entries.count - 1 ? 2 : 1, top: i == 0 ? 2 : 1, bottom: i == item.row.entries.count - 1 ? 2 : 1) - - let signal:Signal<(TransformImageArguments) -> DrawingContext?, NoError> - signal = chatWebpageSnippetPhoto(account: item.account, photo: TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: item.row.results[i].previewRepresentations), scale: backingScaleFactor, small:true) - - - view.update(with: item.row.results[i].resource, size: NSMakeSize(item.row.sizes[i].width, item.height), viewSize: item.row.sizes[i] , account: item.account, table: item.table, iconSignal: signal) - - - addSubview(view) - view.setFrameOrigin(inset, 0) - inset += item.row.sizes[i].width - } - - needsLayout = true - } - } - - - - override func mouseUp(with event: NSEvent) { - super.mouseUp(with: event) - let point = convert(event.locationInWindow, from: nil) - if let item = item as? RecentGIFRowItem { - var inset:CGFloat = 0 - var i:Int = 0 - for size in item.row.sizes { - - if point.x > inset && point.x < inset + size.width { - item.arguments.sendGif(item.row.results[i]) - break - } - inset += size.width - i += 1 - } - } - } - - override func layout() { - super.layout() - - if let item = item as? ContextMediaRowItem { - if item.result.isFilled(for: frame.width) { - let drawn = subviews.reduce(0, { (acc, view) -> CGFloat in - return acc + view.frame.width - }) - if drawn < frame.width { - dif = (frame.width - drawn) / CGFloat(subviews.count + 1) - var inset:CGFloat = dif - for subview in subviews { - subview.setFrameOrigin(inset, 0) - inset += (dif + subview.frame.width) - } - } - } else { - var inset:CGFloat = dif - for subview in subviews { - subview.setFrameOrigin(inset, 0) - inset += (dif + subview.frame.width) - } - } - } - } - - override func menu(for event: NSEvent) -> NSMenu? { - let menu = NSMenu() - menu.addItem(ContextMenuItem(tr(.contextRecentGifRemove), handler: { [weak self] in - if let item = self?.item as? RecentGIFRowItem, let point = self?.convert(mainWindow.mouseLocationOutsideOfEventStream, from: nil) { - var inset:CGFloat = 0 - var i:Int = 0 - for size in item.row.sizes { - - if point.x > inset && point.x < inset + size.width { - if let id = item.row.results[i].id { - _ = removeSavedGif(postbox: item.account.postbox, mediaId: id).start() - } - break - } - inset += size.width - i += 1 - } - } - })) - - return menu - } - -} diff --git a/Telegram-Mac/RecentPeerRowItem.swift b/Telegram-Mac/RecentPeerRowItem.swift index 22ab205fac..e8ae227cfd 100644 --- a/Telegram-Mac/RecentPeerRowItem.swift +++ b/Telegram-Mac/RecentPeerRowItem.swift @@ -8,18 +8,31 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac +import Postbox +import TelegramCore + +import SwiftSignalKit class RecentPeerRowItem: ShortPeerRowItem { - let removeAction:()->Void - let canRemoveFromRecent:Bool - - init(_ initialSize:NSSize, peer: Peer, account:Account, stableId:AnyHashable? = nil, enabled: Bool = true, height:CGFloat = 50, photoSize:NSSize = NSMakeSize(36, 36), titleStyle:ControlStyle = ControlStyle(font:.medium(.title), foregroundColor: theme.colors.text, highlightColor: .white), titleAddition:String? = nil, leftImage:CGImage? = nil, statusStyle:ControlStyle = ControlStyle(font:.normal(.text), foregroundColor: theme.colors.grayText, highlightColor:.white), status:String? = nil, borderType:BorderType = [], drawCustomSeparator:Bool = true, deleteInset:CGFloat? = nil, drawLastSeparator:Bool = false, inset:NSEdgeInsets = NSEdgeInsets(left:10.0), drawSeparatorIgnoringInset: Bool = false, interactionType:ShortPeerItemInteractionType = .plain, generalType:GeneralInteractedType = .none, action:@escaping ()->Void = {}, canRemoveFromRecent: Bool = false, removeAction:@escaping()->Void = {}) { + fileprivate let controlAction:()->Void + fileprivate let canRemoveFromRecent:Bool + fileprivate let badge: BadgeNode? + fileprivate let canAddAsTag: Bool + init(_ initialSize:NSSize, peer: Peer, account:Account, stableId:AnyHashable? = nil, enabled: Bool = true, height:CGFloat = 50, photoSize:NSSize = NSMakeSize(36, 36), titleStyle:ControlStyle = ControlStyle(font:.medium(.title), foregroundColor: theme.colors.text, highlightColor: .white), titleAddition:String? = nil, leftImage:CGImage? = nil, statusStyle:ControlStyle = ControlStyle(font:.normal(.text), foregroundColor: theme.colors.grayText, highlightColor:.white), status:String? = nil, borderType:BorderType = [], drawCustomSeparator:Bool = true, isLookSavedMessage: Bool = false, deleteInset:CGFloat? = nil, drawLastSeparator:Bool = false, inset:NSEdgeInsets = NSEdgeInsets(left:10.0), drawSeparatorIgnoringInset: Bool = false, interactionType:ShortPeerItemInteractionType = .plain, generalType:GeneralInteractedType = .none, action:@escaping ()->Void = {}, canRemoveFromRecent: Bool = false, controlAction:@escaping()->Void = {}, contextMenuItems:@escaping()->Signal<[ContextMenuItem], NoError> = { .single([]) }, unreadBadge: UnreadSearchBadge = .none, canAddAsTag: Bool = false) { self.canRemoveFromRecent = canRemoveFromRecent - self.removeAction = removeAction - super.init(initialSize, peer: peer, account: account, stableId: stableId, enabled: enabled, height: height, photoSize: photoSize, titleStyle: titleStyle, titleAddition: titleAddition, leftImage: leftImage, statusStyle: statusStyle, status: status, borderType: borderType, drawCustomSeparator: drawCustomSeparator, deleteInset: deleteInset, drawLastSeparator: drawLastSeparator, inset: inset, drawSeparatorIgnoringInset: drawSeparatorIgnoringInset, interactionType: interactionType, generalType: generalType, action: action) + self.controlAction = controlAction + self.canAddAsTag = canAddAsTag + switch unreadBadge { + case let .muted(count): + badge = BadgeNode(.initialize(string: "\(count)", color: theme.chatList.badgeTextColor, font: .medium(.small)), theme.chatList.badgeMutedBackgroundColor) + case let .unmuted(count): + badge = BadgeNode(.initialize(string: "\(count)", color: theme.chatList.badgeTextColor, font: .medium(.small)), theme.chatList.badgeBackgroundColor) + case .none: + self.badge = nil + } + + super.init(initialSize, peer: peer, account: account, stableId: stableId, enabled: enabled, height: height, photoSize: photoSize, titleStyle: titleStyle, titleAddition: titleAddition, leftImage: leftImage, statusStyle: statusStyle, status: status, borderType: borderType, drawCustomSeparator: drawCustomSeparator, isLookSavedMessage: isLookSavedMessage, deleteInset: deleteInset, drawLastSeparator: drawLastSeparator, inset: inset, drawSeparatorIgnoringInset: drawSeparatorIgnoringInset, interactionType: interactionType, generalType: generalType, action: action, contextMenuItems: contextMenuItems, highlightVerified: true) } @@ -28,22 +41,24 @@ class RecentPeerRowItem: ShortPeerRowItem { } override var textAdditionInset:CGFloat { - return 15 + return (self.canRemoveFromRecent || self.canAddAsTag ? 5 : 0) + (highlightVerified ? 25 : 0) } } class RecentPeerRowView : ShortPeerRowView { private var trackingArea:NSTrackingArea? - private let removeControl:ImageButton = ImageButton() + private let control:ImageButton = ImageButton() + private var badgeView:View? + required init(frame frameRect: NSRect) { super.init(frame: frameRect) - //removeControl.autohighlight = false - - removeControl.isHidden = true + //control.autohighlight = false + layerContentsRedrawPolicy = .onSetNeedsDisplay + control.isHidden = true - removeControl.set(handler: { [weak self] _ in + control.set(handler: { [weak self] _ in if let item = self?.item as? RecentPeerRowItem { - item.removeAction() + item.controlAction() } }, for: .Click) } @@ -93,30 +108,67 @@ class RecentPeerRowView : ShortPeerRowView { } override func updateMouse() { - if mouseInside() { - removeControl.isHidden = false + if mouseInside(), control.superview != nil { + control.isHidden = false + badgeView?.isHidden = true } else { - removeControl.isHidden = true + control.isHidden = true + badgeView?.isHidden = false } } override func set(item: TableRowItem, animated: Bool) { super.set(item: item, animated: animated) - removeControl.set(image: isSelect ? theme.icons.recentDismissActive : theme.icons.recentDismiss, for: .Normal) - removeControl.sizeToFit() + if let item = item as? RecentPeerRowItem { - if item.canRemoveFromRecent { - addSubview(removeControl) + + + if item.canAddAsTag { + control.set(image: isSelect ? theme.icons.search_filter_add_peer_active : theme.icons.search_filter_add_peer, for: .Normal) + } else { + control.set(image: isSelect ? theme.icons.recentDismissActive : theme.icons.recentDismiss, for: .Normal) + } + _ = control.sizeToFit() + + if item.canRemoveFromRecent || item.canAddAsTag { + addSubview(control) } else { - removeControl.removeFromSuperview() + control.removeFromSuperview() + } + + if let badgeNode = item.badge { + if badgeView == nil { + badgeView = View() + addSubview(badgeView!) + } + badgeView?.setFrameSize(badgeNode.size) + badgeNode.view = badgeView + badgeNode.setNeedDisplay() + } else { + badgeView?.removeFromSuperview() + badgeView = nil } } + updateMouse() needsLayout = true } + override var backdorColor: NSColor { + if let item = item { + return item.isHighlighted && !item.isSelected ? theme.colors.grayForeground : super.backdorColor + } else { + return super.backdorColor + } + } + override func layout() { super.layout() - removeControl.centerY(x: frame.width - removeControl.frame.width - 10) + + + control.centerY(x: frame.width - control.frame.width - 10) + if let badgeView = badgeView { + badgeView.centerY(x: frame.width - badgeView.frame.width - 10) + } } required init?(coder: NSCoder) { diff --git a/Telegram-Mac/RecentSessionRowItem.swift b/Telegram-Mac/RecentSessionRowItem.swift index 9fadf72ed6..61773a67c4 100644 --- a/Telegram-Mac/RecentSessionRowItem.swift +++ b/Telegram-Mac/RecentSessionRowItem.swift @@ -8,52 +8,57 @@ import Cocoa import TGUIKit -import TelegramCoreMac -class RecentSessionRowItem: TableRowItem { +import TelegramCore + +class RecentSessionRowItem: GeneralRowItem { let session:RecentAccountSession - let _stableId:AnyHashable let headerLayout:TextViewLayout let descLayout:TextViewLayout let dateLayout:TextViewLayout let revoke:()->Void - override var stableId: AnyHashable { - return _stableId - } + - init(_ initialSize: NSSize, session:RecentAccountSession, stableId:AnyHashable, revoke: @escaping()->Void) { - self._stableId = stableId + init(_ initialSize: NSSize, session:RecentAccountSession, stableId:AnyHashable, viewType: GeneralViewType, revoke: @escaping()->Void) { self.session = session self.revoke = revoke headerLayout = TextViewLayout(.initialize(string: session.appName + " " + session.appVersion, color: theme.colors.text, font: .normal(.title))) let attr = NSMutableAttributedString() - _ = attr.append(string: session.deviceModel + ", " + session.platform + " " + session.systemVersion, color: theme.colors.text, font: .normal(.text)) + + var trimmed = session.deviceModel.trimmingCharacters(in: CharacterSet(charactersIn: "1234567890,")) + + if trimmed.hasSuffix("Pro") || trimmed.hasSuffix("Air") { + trimmed = trimmed.nsstring.substring(to: trimmed.length - 3) + " " + trimmed.nsstring.substring(from: trimmed.length - 3) + } + + _ = attr.append(string:trimmed + ", " + session.platform + " " + session.systemVersion, color: theme.colors.text, font: .normal(.text)) _ = attr.append(string: "\n") _ = attr.append(string: session.ip + " " + session.country, color: theme.colors.grayText, font: .normal(.text)) - descLayout = TextViewLayout(attr, lineSpacing: 2) + descLayout = TextViewLayout(attr, maximumNumberOfLines: 2, lineSpacing: 2) - dateLayout = TextViewLayout(.initialize(string: session.isCurrent ? tr(.peerStatusOnline) : DateUtils.string(forMessageListDate: session.creationDate), color: session.isCurrent ? theme.colors.blueText : theme.colors.grayText, font: .normal(.text))) + dateLayout = TextViewLayout(.initialize(string: session.isCurrent ? tr(L10n.peerStatusOnline) : DateUtils.string(forMessageListDate: session.activityDate), color: session.isCurrent ? theme.colors.accent : theme.colors.grayText, font: .normal(.text))) - super.init(initialSize) + super.init(initialSize, stableId: stableId, viewType: viewType) _ = makeSize(initialSize.width, oldWidth: initialSize.width) } override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { - headerLayout.measure(width: width - 60) - descLayout.measure(width: width - 60) + let success = super.makeSize(width, oldWidth: oldWidth) + headerLayout.measure(width: blockWidth - 80) + descLayout.measure(width: blockWidth - 80) dateLayout.measure(width: .greatestFiniteMagnitude) - return super.makeSize(width, oldWidth: oldWidth) + return success } override var height: CGFloat { - return 70 + return 75 } override func viewClass() -> AnyClass { @@ -61,7 +66,8 @@ class RecentSessionRowItem: TableRowItem { } } -class RecentSessionRowView : TableRowView { +class RecentSessionRowView : TableRowView, ViewDisplayDelegate { + private let containerView = GeneralRowContainerView(frame: NSZeroRect) let headerTextView = TextView() let descTextView = TextView() let dateTextView = TextView() @@ -71,14 +77,18 @@ class RecentSessionRowView : TableRowView { reset.set(font: .normal(.title), for: .Normal) super.init(frame: frameRect) - addSubview(headerTextView) - addSubview(descTextView) - addSubview(dateTextView) - addSubview(reset) + containerView.addSubview(headerTextView) + containerView.addSubview(descTextView) + containerView.addSubview(dateTextView) + containerView.addSubview(reset) + + addSubview(containerView) + + containerView.displayDelegate = self reset.set(handler: { [weak self] _ in if let item = self?.item as? RecentSessionRowItem { - confirm(for: mainWindow, with: appName, and: tr(.recentSessionsConfirmRevoke), successHandler: { _ in + confirm(for: mainWindow, information: tr(L10n.recentSessionsConfirmRevoke), successHandler: { _ in item.revoke() }) } @@ -86,30 +96,47 @@ class RecentSessionRowView : TableRowView { } override func updateColors() { - super.updateColors() headerTextView.backgroundColor = backdorColor descTextView.backgroundColor = backdorColor dateTextView.backgroundColor = backdorColor + containerView.backgroundColor = backdorColor + if let item = item as? RecentSessionRowItem { + self.background = item.viewType.rowBackground + } + } + + override var backdorColor: NSColor { + return theme.colors.background } override func draw(_ layer: CALayer, in ctx: CGContext) { super.draw(layer, in: ctx) - ctx.setFillColor(theme.colors.border.cgColor) - ctx.fill(NSMakeRect(30, frame.height - .borderSize, frame.width - 60, .borderSize)) + if let item = item as? RecentSessionRowItem, layer == containerView.layer { + ctx.setFillColor(theme.colors.border.cgColor) + switch item.viewType { + case .legacy: + ctx.fill(NSMakeRect(30, containerView.frame.height - .borderSize, frame.width - 60, .borderSize)) + case let .modern(position, insets): + if position.border { + ctx.fill(NSMakeRect(insets.left, containerView.frame.height - .borderSize, containerView.frame.width - insets.left - insets.right, .borderSize)) + } + } + } } override func set(item: TableRowItem, animated: Bool) { super.set(item: item) - - reset.set(text: tr(.recentSessionsRevoke), for: .Normal) - reset.set(color: theme.colors.blueUI, for: .Normal) + + reset.set(text: tr(L10n.recentSessionsRevoke), for: .Normal) + reset.set(color: theme.colors.accent, for: .Normal) reset.set(background: theme.colors.background, for: .Normal) - reset.sizeToFit() + _ = reset.sizeToFit() if let item = item as? RecentSessionRowItem { reset.isHidden = item.session.isCurrent + containerView.setCorners(item.viewType.corners, animated: animated) } self.needsLayout = true @@ -118,10 +145,22 @@ class RecentSessionRowView : TableRowView { override func layout() { super.layout() if let item = item as? RecentSessionRowItem { - headerTextView.update(item.headerLayout, origin: NSMakePoint(30, 10)) - descTextView.update(item.descLayout, origin: NSMakePoint(30, headerTextView.frame.maxY + 4)) - dateTextView.update(item.dateLayout, origin: NSMakePoint(frame.width - 30 - item.dateLayout.layoutSize.width, 10)) - reset.setFrameOrigin(frame.width - 30 - reset.frame.width, frame.height - reset.frame.height - 10) + switch item.viewType { + case .legacy: + self.containerView.frame = self.bounds + self.containerView.setCorners([]) + self.headerTextView.update(item.headerLayout, origin: NSMakePoint(30, 10)) + self.descTextView.update(item.descLayout, origin: NSMakePoint(30, headerTextView.frame.maxY + 4)) + self.dateTextView.update(item.dateLayout, origin: NSMakePoint(self.containerView.frame.width - 30 - item.dateLayout.layoutSize.width, 10)) + self.reset.setFrameOrigin(frame.width - 25 - reset.frame.width, self.containerView.frame.height - reset.frame.height - 10) + case let .modern(position, insets): + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) + self.containerView.setCorners(position.corners) + self.headerTextView.update(item.headerLayout, origin: NSMakePoint(insets.left, insets.top)) + self.descTextView.update(item.descLayout, origin: NSMakePoint(insets.left, headerTextView.frame.maxY + 4)) + self.dateTextView.update(item.dateLayout, origin: NSMakePoint(self.containerView.frame.width - insets.right - item.dateLayout.layoutSize.width, insets.top)) + self.reset.setFrameOrigin(self.containerView.frame.width - insets.right + 5 - reset.frame.width, self.containerView.frame.height - reset.frame.height - 7) + } } } diff --git a/Telegram-Mac/RecentSessionsController.swift b/Telegram-Mac/RecentSessionsController.swift index e3de27c2f2..a1a7ae8e92 100644 --- a/Telegram-Mac/RecentSessionsController.swift +++ b/Telegram-Mac/RecentSessionsController.swift @@ -10,17 +10,18 @@ import Cocoa import Foundation import TGUIKit -import SwiftSignalKitMac -import PostboxMac -import TelegramCoreMac +import SwiftSignalKit +import Postbox +import TelegramCore + private final class RecentSessionsControllerArguments { - let account: Account + let context: AccountContext let removeSession: (Int64) -> Void let terminateOthers:() -> Void - init(account: Account, removeSession: @escaping (Int64) -> Void, terminateOthers: @escaping()->Void) { - self.account = account + init(context: AccountContext, removeSession: @escaping (Int64) -> Void, terminateOthers: @escaping()->Void) { + self.context = context self.removeSession = removeSession self.terminateOthers = terminateOthers } @@ -42,39 +43,21 @@ private enum RecentSessionsEntryStableId: Hashable { } } - static func ==(lhs: RecentSessionsEntryStableId, rhs: RecentSessionsEntryStableId) -> Bool { - switch lhs { - case let .session(hash): - if case .session(hash) = rhs { - return true - } else { - return false - } - case let .index(index): - if case .index(index) = rhs { - return true - } else { - return false - } - case let .section(sectionId): - if case .section(sectionId) = rhs { - return true - } else { - return false - } - } - } } private enum RecentSessionsEntry: Comparable, Identifiable { case loading(sectionId:Int) - case currentSessionHeader(sectionId:Int) - case currentSession(sectionId:Int, RecentAccountSession) - case terminateOtherSessions(sectionId:Int) - case currentSessionInfo(sectionId:Int) + case currentSessionHeader(sectionId:Int, viewType: GeneralViewType) + case currentSession(sectionId:Int, RecentAccountSession, viewType: GeneralViewType) + case terminateOtherSessions(sectionId:Int, viewType: GeneralViewType) + case currentSessionInfo(sectionId:Int, viewType: GeneralViewType) + + case otherSessionsHeader(sectionId:Int, viewType: GeneralViewType) - case otherSessionsHeader(sectionId:Int) - case session(sectionId:Int, index: Int32, session: RecentAccountSession, enabled: Bool, editing: Bool) + case incompleteHeader(sectionId: Int, viewType: GeneralViewType) + case incompleteDesc(sectionId: Int, viewType: GeneralViewType) + + case session(sectionId:Int, index: Int32, session: RecentAccountSession, enabled: Bool, editing: Bool, viewType: GeneralViewType) case section(sectionId:Int) @@ -91,9 +74,13 @@ private enum RecentSessionsEntry: Comparable, Identifiable { return .index(3) case .currentSessionInfo: return .index(4) - case .otherSessionsHeader: + case .incompleteHeader: return .index(5) - case let .session(_, _, session, _, _): + case .incompleteDesc: + return .index(6) + case .otherSessionsHeader: + return .index(7) + case let .session(_, _, session, _, _, _): return .session(session.hash) case let .section(sectionId): return .section(sectionId) @@ -112,9 +99,13 @@ private enum RecentSessionsEntry: Comparable, Identifiable { return 3 case .currentSessionInfo: return 4 - case .otherSessionsHeader: + case .incompleteHeader: return 5 - case let .session(_, _, _, _, _): + case .incompleteDesc: + return 6 + case .otherSessionsHeader: + return 7 + case .session: fatalError() case let .section(sectionId): return (sectionId + 1) * 1000 - sectionId @@ -125,17 +116,21 @@ private enum RecentSessionsEntry: Comparable, Identifiable { switch self { case let .loading(sectionId): return sectionId - case let .currentSessionHeader(sectionId): + case let .currentSessionHeader(sectionId, _): + return sectionId + case let .currentSession(sectionId, _, _): + return sectionId + case let .terminateOtherSessions(sectionId, _): return sectionId - case let .currentSession(sectionId, _): + case let .currentSessionInfo(sectionIdv): return sectionId - case let .terminateOtherSessions(sectionId): + case let .incompleteHeader(sectionId, _): return sectionId - case let .currentSessionInfo(sectionId): + case let .incompleteDesc(sectionId, _): return sectionId - case let .otherSessionsHeader(sectionId): + case let .otherSessionsHeader(sectionId, _): return sectionId - case let .session(sectionId, _, _, _, _): + case let .session(sectionId, _, _, _, _, _): return sectionId case let .section(sectionId): return sectionId @@ -146,43 +141,27 @@ private enum RecentSessionsEntry: Comparable, Identifiable { switch self { case let .loading(sectionId): return (sectionId * 1000) + stableIndex - case let .currentSessionHeader(sectionId): + case let .currentSessionHeader(sectionId, _): return (sectionId * 1000) + stableIndex - case let .currentSession(sectionId, _): + case let .currentSession(sectionId, _, _): return (sectionId * 1000) + stableIndex - case let .terminateOtherSessions(sectionId): + case let .terminateOtherSessions(sectionId, _): return (sectionId * 1000) + stableIndex - case let .currentSessionInfo(sectionId): + case let .currentSessionInfo(sectionId, _): return (sectionId * 1000) + stableIndex - case let .otherSessionsHeader(sectionId): + case let .incompleteHeader(sectionId, _): return (sectionId * 1000) + stableIndex - case let .session(sectionId, index, _, _, _): + case let .incompleteDesc(sectionId, _): + return (sectionId * 1000) + stableIndex + case let .otherSessionsHeader(sectionId, _): + return (sectionId * 1000) + stableIndex + case let .session(sectionId, index, _, _, _, _): return (sectionId * 1000) + Int(index) + 100 case let .section(sectionId): return (sectionId * 1000) + stableIndex } } - static func ==(lhs: RecentSessionsEntry, rhs: RecentSessionsEntry) -> Bool { - switch lhs { - case .currentSessionHeader, .terminateOtherSessions, .currentSessionInfo, .otherSessionsHeader, .section, .loading: - return lhs.stableId == rhs.stableId && lhs.sectionId == rhs.sectionId - case let .currentSession(sectionId, session): - if case .currentSession(sectionId, session) = rhs { - return true - } else { - return false - } - case let .session(sectionId, index, session, enabled, editing): - if case .session(sectionId, index, session, enabled, editing) = rhs { - return true - } else { - return false - } - } - } - - static func <(lhs: RecentSessionsEntry, rhs: RecentSessionsEntry) -> Bool { return lhs.sortIndex < rhs.sortIndex @@ -190,24 +169,28 @@ private enum RecentSessionsEntry: Comparable, Identifiable { func item(_ arguments: RecentSessionsControllerArguments, initialSize: NSSize) -> TableRowItem { switch self { - case .currentSessionHeader: - return GeneralTextRowItem(initialSize, stableId: stableId, text: tr(.sessionsCurrentSessionHeader)) - case let .currentSession(_, session): - return RecentSessionRowItem(initialSize, session: session, stableId: stableId, revoke: {}) - case .terminateOtherSessions: - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.sessionsTerminateOthers), nameStyle: redActionButton, type: .none, action: { + case let .currentSessionHeader(_, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.sessionsCurrentSessionHeader, viewType: viewType) + case let .currentSession(_, session, viewType): + return RecentSessionRowItem(initialSize, session: session, stableId: stableId, viewType: viewType, revoke: {}) + case let .terminateOtherSessions(_, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.sessionsTerminateOthers, nameStyle: redActionButton, type: .none, viewType: viewType, action: { arguments.terminateOthers() }) - case .currentSessionInfo: - return GeneralTextRowItem(initialSize, stableId: stableId, text: tr(.sessionsTerminateDescription)) - case .otherSessionsHeader: - return GeneralTextRowItem(initialSize, stableId: stableId, text: tr(.sessionsActiveSessionsHeader)) - case let .session(_, _, session, _, _): - return RecentSessionRowItem(initialSize, session: session, stableId: stableId, revoke: { + case let .currentSessionInfo(_, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.sessionsTerminateDescription, viewType: viewType) + case let .incompleteHeader(_, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.recentSessionsIncompleteAttemptHeader, viewType: viewType) + case let .incompleteDesc(_, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.recentSessionsIncompleteAttemptDesc, viewType: viewType) + case let .otherSessionsHeader(_, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.sessionsActiveSessionsHeader, viewType: viewType) + case let .session(_, _, session, _, _, viewType): + return RecentSessionRowItem(initialSize, session: session, stableId: stableId, viewType: viewType, revoke: { arguments.removeSession(session.hash) }) case .section(sectionId: _): - return GeneralRowItem(initialSize, height: 20, stableId: stableId) + return GeneralRowItem(initialSize, height: 30, stableId: stableId, viewType: .separator) case .loading: return SearchEmptyRowItem(initialSize, stableId: stableId, isLoading: true) } @@ -268,34 +251,57 @@ private func recentSessionsControllerEntries(state: RecentSessionsControllerStat sectionId += 1 var existingSessionIds = Set() - entries.append(.currentSessionHeader(sectionId: sectionId)) - if let index = sessions.index(where: { $0.hash == 0 }) { + entries.append(.currentSessionHeader(sectionId: sectionId, viewType: .textTopItem)) + if let index = sessions.firstIndex(where: { $0.hash == 0 }) { existingSessionIds.insert(sessions[index].hash) - entries.append(.currentSession(sectionId: sectionId, sessions[index])) + entries.append(.currentSession(sectionId: sectionId, sessions[index], viewType: .firstItem)) } - entries.append(.terminateOtherSessions(sectionId: sectionId)) - entries.append(.currentSessionInfo(sectionId: sectionId)) + entries.append(.terminateOtherSessions(sectionId: sectionId, viewType: .lastItem)) + entries.append(.currentSessionInfo(sectionId: sectionId, viewType: .textBottomItem)) if sessions.count > 1 { entries.append(.section(sectionId: sectionId)) sectionId += 1 - entries.append(.section(sectionId: sectionId)) - sectionId += 1 - entries.append(.otherSessionsHeader(sectionId: sectionId)) - let filteredSessions: [RecentAccountSession] = sessions.sorted(by: { lhs, rhs in return lhs.activityDate > rhs.activityDate }) - for i in 0 ..< filteredSessions.count { - if !existingSessionIds.contains(sessions[i].hash) { - existingSessionIds.insert(sessions[i].hash) - let session = sessions[i] - let enabled = state.removingSessionId != sessions[i].hash - entries.append(.session(sectionId: sectionId, index: Int32(i), session: session, enabled: enabled, editing: state.editing)) + let nonApplied = filteredSessions.filter {$0.flags.contains(.passwordPending)} + let applied = filteredSessions.filter {!$0.flags.contains(.passwordPending)} + + var index: Int32 = 0 + + if !nonApplied.isEmpty { + entries.append(.incompleteHeader(sectionId: sectionId, viewType: .textTopItem)) + + let nonApplied = nonApplied.filter({ + !existingSessionIds.contains($0.hash) + }) + for session in nonApplied { + existingSessionIds.insert(session.hash) + let enabled = state.removingSessionId != session.hash + entries.append(.session(sectionId: sectionId, index: index, session: session, enabled: enabled, editing: state.editing, viewType: bestGeneralViewType(nonApplied, for: session))) + index += 1 } + entries.append(.incompleteDesc(sectionId: sectionId, viewType: .textBottomItem)) + + entries.append(.section(sectionId: sectionId)) + sectionId += 1 } + + entries.append(.otherSessionsHeader(sectionId: sectionId, viewType: .textTopItem)) + let newApplied = applied.filter({ + !existingSessionIds.contains($0.hash) + }) + for session in newApplied { + existingSessionIds.insert(session.hash) + let enabled = state.removingSessionId != session.hash + entries.append(.session(sectionId: sectionId, index: index, session: session, enabled: enabled, editing: state.editing, viewType: bestGeneralViewType(newApplied, for: session))) + index += 1 + } + entries.append(.section(sectionId: sectionId)) + sectionId += 1 } } else { entries.append(.loading(sectionId: 1)) @@ -316,45 +322,30 @@ private func prepareSessions(left:[AppearanceWrapperEntry], class RecentSessionsController : TableViewController { override func viewDidLoad() { + super.viewDidLoad() + + + let statePromise = ValuePromise(RecentSessionsControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: RecentSessionsControllerState()) let updateState: ((RecentSessionsControllerState) -> RecentSessionsControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } - let account = self.account + let context = self.context let initialSize = self.atomicSize let actionsDisposable = DisposableSet() let removeSessionDisposable = MetaDisposable() actionsDisposable.add(removeSessionDisposable) - let sessionsPromise = Promise<[RecentAccountSession]?>(nil) + let sessionsPromise = Promise<[RecentAccountSession]?>() - let arguments = RecentSessionsControllerArguments(account: account, removeSession: { sessionId in + let arguments = RecentSessionsControllerArguments(context: context, removeSession: { sessionId in updateState { return $0.withUpdatedRemovingSessionId(sessionId) } - let applySessions: Signal = sessionsPromise.get() - |> filter { $0 != nil } - |> take(1) - |> deliverOnMainQueue - |> mapToSignal { sessions -> Signal in - if let sessions = sessions { - var updatedSessions = sessions - for i in 0 ..< updatedSessions.count { - if updatedSessions[i].hash == sessionId { - updatedSessions.remove(at: i) - break - } - } - sessionsPromise.set(.single(updatedSessions)) - } - - return .complete() - } - - removeSessionDisposable.set((terminateAccountSession(account: account, hash: sessionId) |> then(applySessions) |> deliverOnMainQueue).start(error: { _ in + removeSessionDisposable.set((context.activeSessionsContext.remove(hash: sessionId) |> deliverOnMainQueue).start(error: { _ in updateState { return $0.withUpdatedRemovingSessionId(nil) } @@ -364,10 +355,16 @@ class RecentSessionsController : TableViewController { } })) }, terminateOthers: { - _ = (confirmSignal(for: mainWindow, header: appName, information: tr(.recentSessionsConfirmTerminateOthers)) |> filter {$0} |> map {_ in} |> mapToSignal{terminateOtherAccountSessions(account: account)}).start() + confirm(for: context.window, information: L10n.recentSessionsConfirmTerminateOthers, successHandler: { _ in + _ = showModalProgress(signal: context.activeSessionsContext.removeOther(), for: context.window).start(error: { error in + + }) + }) }) - let sessionsSignal: Signal<[RecentAccountSession]?, NoError> = .single(nil) |> then(requestRecentAccountSessions(account: account) |> map { Optional($0) }) + let sessionsSignal: Signal<[RecentAccountSession]?, NoError> = context.activeSessionsContext.state |> map { + $0.sessions + } sessionsPromise.set(sessionsSignal) @@ -383,6 +380,16 @@ class RecentSessionsController : TableViewController { } genericView.merge(with: signal) + + genericView.setScrollHandler { position in + switch position.direction { + case .bottom: + context.activeSessionsContext.loadMore() + default: + break + } + } + readyOnce() } diff --git a/Telegram-Mac/RecentUsedEmoji.swift b/Telegram-Mac/RecentUsedEmoji.swift index b35448dac2..deaafa2f46 100644 --- a/Telegram-Mac/RecentUsedEmoji.swift +++ b/Telegram-Mac/RecentUsedEmoji.swift @@ -7,35 +7,107 @@ // import Cocoa -import PostboxMac -import SwiftSignalKitMac +import Postbox +import SwiftSignalKit + +struct EmojiSkinModifier : PostboxCoding, Equatable { + let emoji: String + let modifier: String + init(emoji: String, modifier: String) { + var emoji = emoji + for skin in emoji.emojiSkinToneModifiers { + emoji = emoji.replacingOccurrences(of: skin, with: "") + } + self.emoji = emoji + self.modifier = modifier + } + func encode(_ encoder: PostboxEncoder) { + encoder.encodeString(emoji, forKey: "e") + encoder.encodeString(modifier, forKey: "m") + } + + var modify: String { + var e:String = emoji + if emoji.length == 5 { + let mutable = NSMutableString() + mutable.insert(e, at: 0) + mutable.insert(modifier, at: 2) + e = mutable as String + } else { + e = emoji + modifier + } + return e + } + + init(decoder: PostboxDecoder) { + self.emoji = decoder.decodeStringForKey("e", orElse: "") + self.modifier = decoder.decodeStringForKey("m", orElse: "") + var bp:Int = 0 + bp += 1 + } +} class RecentUsedEmoji: PreferencesEntry, Equatable { - let emojies:[String] - let skinModifiers:[String] - init(emojies:[String], skinModifiers: [String]) { - self.emojies = emojies + private let _emojies:[String] + let skinModifiers:[EmojiSkinModifier] + init(emojies:[String], skinModifiers: [EmojiSkinModifier]) { + self._emojies = emojies self.skinModifiers = skinModifiers } public static var defaultSettings: RecentUsedEmoji { - return RecentUsedEmoji(emojies: ["😂", "😘", "❤️", "😍", "😊", "🤔", "😁", "👍", "☺️", "😔", "😄", "😭", "💋", "😒", "😳", "😜", "🙈", "😉", "😃", "😢", "😝", "😱", "😡", "😏", "😞", "😅", "😚", "🙊", "😌", "😀", "😋", "😆", "😐", "😕", "👎"], skinModifiers: []) + return RecentUsedEmoji(emojies: ["😂", "😘", "❤️", "😍", "😊", "🤔", "😁", "👍", "☺️", "😔", "😄", "😭", "💋", "😒", "😳", "😜", "🙈", "😉", "😃", "😢", "😝", "😱", "😡", "😏", "😞", "😅", "😚", "🙊", "😌", "😀", "😋", "😆", "🌚", "😐", "😕", "👎", diceSymbol, dartSymbol], skinModifiers: []) + } + + var emojies: [String] { + var isset:[String: String] = [:] + var list:[String] = [] + for emoji in _emojies { + if isset[emoji] == nil, emoji != "�", !emoji.emojiSkinToneModifiers.contains(emoji), emoji != "️" { + var emoji = emoji + isset[emoji] = emoji + for skin in skinModifiers { + if skin.emoji == emoji { + emoji = skin.modify + } + } + list.append(emoji.nsstring.substring(with: NSMakeRange(0, min(emoji.length, 8)))) + } + } + return list.reduce([], { current, value in + var value = value + if let modifier = value.emojiSkinToneModifiers.first(where: { value.contains($0) }), value.glyphCount > 1 { + value = value.replacingOccurrences(of: modifier, with: "") + } + if let first = value.first { + return current + [String(first)] + } else { + return current + } + }) } public required init(decoder: PostboxDecoder) { let emojies = decoder.decodeStringArrayForKey("e") - + self.skinModifiers = (try? decoder.decodeObjectArrayWithCustomDecoderForKey("sm_new", decoder: {EmojiSkinModifier(decoder: $0)})) ?? [] + var isset:[String: String] = [:] var list:[String] = [] for emoji in emojies { - if isset[emoji] == nil { - list.append(emoji) + if isset[emoji] == nil, emoji != "�", !emoji.emojiSkinToneModifiers.contains(emoji), emoji != "️" { + var emoji = emoji isset[emoji] = emoji + for skin in skinModifiers { + if skin.emoji == emoji { + emoji = skin.modify + } + } + list.append(emoji) } } - self.emojies = list + self._emojies = list + - self.skinModifiers = decoder.decodeStringArrayForKey("sm") } @@ -49,8 +121,8 @@ class RecentUsedEmoji: PreferencesEntry, Equatable { } public func encode(_ encoder: PostboxEncoder) { - encoder.encodeStringArray(emojies, forKey: "e") - encoder.encodeStringArray(skinModifiers, forKey: "sm") + encoder.encodeStringArray(_emojies, forKey: "e") + encoder.encodeObjectArray(skinModifiers, forKey: "sm_new") } } @@ -59,9 +131,9 @@ func ==(lhs: RecentUsedEmoji, rhs: RecentUsedEmoji) -> Bool { } -func saveUsedEmoji(_ list:[String], postbox:Postbox) -> Signal { - return postbox.modify { modifier -> Void in - modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.recentEmoji, { entry in +func saveUsedEmoji(_ list:[String], postbox:Postbox) -> Signal { + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.recentEmoji, { entry in var emojies: [String] if let entry = entry as? RecentUsedEmoji { emojies = entry.emojies @@ -70,48 +142,48 @@ func saveUsedEmoji(_ list:[String], postbox:Postbox) -> Signal { } for emoji in list.reversed() { - let emoji = emoji.emojiString - if !emoji.isEmpty { - if let index = emojies.index(of: emoji) { - emojies.remove(at: index) + if emoji.containsOnlyEmoji { + let emoji = emoji.emojiString.emojiUnmodified + if !emoji.isEmpty && emoji.count == 1 { + if let index = emojies.firstIndex(of: emoji) { + emojies.remove(at: index) + } + emojies.insert(emoji, at: 0) } - emojies.insert(emoji, at: 0) } } - emojies = Array(emojies.prefix(35)) + emojies = Array(emojies.filter({$0.containsEmoji}).prefix(35)) return RecentUsedEmoji(emojies: emojies, skinModifiers: (entry as? RecentUsedEmoji)?.skinModifiers ?? []) }) } } -func modifySkinEmoji(_ emoji:String, postbox: Postbox) -> Signal { - return postbox.modify { modifier -> Void in - modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.recentEmoji, { entry in - if let settings = (entry as? RecentUsedEmoji) { - var skinModifiers = settings.skinModifiers - var index:Int? = nil - for i in 0 ..< skinModifiers.count { - let local = skinModifiers[i] - if emoji.emojiUnmodified == local.emojiUnmodified { - index = i - } - } - +func modifySkinEmoji(_ emoji:String, modifier: String?, postbox: Postbox) -> Signal { + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.recentEmoji, { entry in + let settings = (entry as? RecentUsedEmoji) ?? RecentUsedEmoji.defaultSettings + var skinModifiers = settings.skinModifiers + let index:Int? = skinModifiers.firstIndex(where: {$0.emoji == emoji}) + + if let modifier = modifier { if let index = index { - skinModifiers[index] = emoji + skinModifiers[index] = EmojiSkinModifier(emoji: emoji, modifier: modifier) } else { - skinModifiers.append(emoji) + skinModifiers.append(EmojiSkinModifier(emoji: emoji, modifier: modifier)) } - return RecentUsedEmoji(emojies: settings.emojies, skinModifiers: skinModifiers) + } else if let index = index { + skinModifiers.remove(at: index) } + + return RecentUsedEmoji(emojies: settings.emojies, skinModifiers: skinModifiers) - return entry }) } } -func recentUsedEmoji(postbox: Postbox) -> Signal { +func recentUsedEmoji(postbox: Postbox) -> Signal { return postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.recentEmoji]) |> map { preferences in + return (preferences.values[ApplicationSpecificPreferencesKeys.recentEmoji] as? RecentUsedEmoji) ?? RecentUsedEmoji.defaultSettings } } diff --git a/Telegram-Mac/Release.xcconfig b/Telegram-Mac/Release.xcconfig index beb2aad321..319a9d8081 100644 --- a/Telegram-Mac/Release.xcconfig +++ b/Telegram-Mac/Release.xcconfig @@ -10,4 +10,4 @@ DSA_PEM_FILE = dsa_pub_prod.pem SIMPLE_SLASH=/ SFEED_URL = https:$(SIMPLE_SLASH)/osx.telegram.org/updates/versions.xml - +APPCENTER_SECRET = 0af668a6-29fa-4a9e-8a70-002913f8efba diff --git a/Telegram-Mac/RenderedTotalUnreadCount.swift b/Telegram-Mac/RenderedTotalUnreadCount.swift new file mode 100644 index 0000000000..53f5cedd29 --- /dev/null +++ b/Telegram-Mac/RenderedTotalUnreadCount.swift @@ -0,0 +1,68 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramCore + +enum RenderedTotalUnreadCountType { + case raw + case filtered +} + +func renderedTotalUnreadCount(transaction: Transaction) -> (Int32, RenderedTotalUnreadCountType) { + let totalUnreadState = transaction.getTotalUnreadState(groupId: .root) + let inAppSettings: InAppNotificationSettings = (transaction.getPreferencesEntry(key: ApplicationSharedPreferencesKeys.inAppNotificationSettings) as? InAppNotificationSettings) ?? .defaultSettings + let type: RenderedTotalUnreadCountType + switch inAppSettings.totalUnreadCountDisplayStyle { + case .raw: + type = .raw + case .filtered: + type = .filtered + } + return (totalUnreadState.count(for: inAppSettings.totalUnreadCountDisplayStyle.category, in: inAppSettings.totalUnreadCountDisplayCategory.statsType, with: inAppSettings.totalUnreadCountIncludeTags), type) +} + +func renderedTotalUnreadCount(inAppSettings: InAppNotificationSettings, totalUnreadState: ChatListTotalUnreadState) -> (Int32, RenderedTotalUnreadCountType) { + let type: RenderedTotalUnreadCountType + switch inAppSettings.totalUnreadCountDisplayStyle { + case .raw: + type = .raw + case .filtered: + type = .filtered + } + return (totalUnreadState.count(for: inAppSettings.totalUnreadCountDisplayStyle.category, in: inAppSettings.totalUnreadCountDisplayCategory.statsType, with: inAppSettings.totalUnreadCountIncludeTags), type) +} + +func renderedTotalUnreadCount(accountManager: AccountManager, postbox: Postbox) -> Signal<(Int32, RenderedTotalUnreadCountType), NoError> { + let unreadCountsKey = PostboxViewKey.unreadCounts(items: [.total(nil)]) + return combineLatest(accountManager.sharedData(keys: [ApplicationSharedPreferencesKeys.inAppNotificationSettings]), postbox.combinedView(keys: [unreadCountsKey])) + |> map { sharedData, view -> (Int32, RenderedTotalUnreadCountType) in + let totalUnreadState: ChatListTotalUnreadState + if let value = view.views[unreadCountsKey] as? UnreadMessageCountsView, let (_, total) = value.total() { + totalUnreadState = total + } else { + totalUnreadState = ChatListTotalUnreadState(absoluteCounters: [:], filteredCounters: [:]) + } + + let inAppSettings: InAppNotificationSettings + if let value = sharedData.entries[ApplicationSharedPreferencesKeys.inAppNotificationSettings] as? InAppNotificationSettings { + inAppSettings = value + } else { + inAppSettings = .defaultSettings + } + let type: RenderedTotalUnreadCountType + switch inAppSettings.totalUnreadCountDisplayStyle { + case .raw: + type = .raw + case .filtered: + type = .filtered + } + if inAppSettings.badgeEnabled { + return (totalUnreadState.count(for: inAppSettings.totalUnreadCountDisplayStyle.category, in: inAppSettings.totalUnreadCountDisplayCategory.statsType, with: inAppSettings.totalUnreadCountIncludeTags), type) + } else { + return (0, type) + } + } + |> distinctUntilChanged(isEqual: { lhs, rhs in + return lhs == rhs + }) +} diff --git a/Telegram-Mac/RepliesHeaderRowItem.swift b/Telegram-Mac/RepliesHeaderRowItem.swift new file mode 100644 index 0000000000..f5973e027a --- /dev/null +++ b/Telegram-Mac/RepliesHeaderRowItem.swift @@ -0,0 +1,63 @@ +// +// RepliesHeaderRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 30/09/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +class RepliesHeaderRowItem: GeneralRowItem { + fileprivate var textLayout: TextViewLayout + init(_ initialSize: NSSize, entry: ChatHistoryEntry) { + self.textLayout = TextViewLayout(.initialize(string: L10n.chatRepliesDesc, color: theme.colors.text, font: .normal(.text)), alignment: .center) + super.init(initialSize, stableId: entry.stableId, viewType: .singleItem, inset: NSEdgeInsetsMake(20, 30, 10, 30)) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + self.textLayout.measure(width: blockWidth - viewType.innerInset.left - viewType.innerInset.right) + return true + } + + override var height: CGFloat { + return self.textLayout.layoutSize.height + viewType.innerInset.top + viewType.innerInset.bottom + inset.top + inset.bottom + } + + override func viewClass() -> AnyClass { + return RepliesHeaderRowView.self + } +} + + +private final class RepliesHeaderRowView : GeneralContainableRowView { + private let textView: TextView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(textView) + + textView.isSelectable = false + textView.userInteractionEnabled = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + textView.center() + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? RepliesHeaderRowItem else { + return + } + + self.textView.update(item.textLayout) + } +} diff --git a/Telegram-Mac/ReplyMarkupNode.swift b/Telegram-Mac/ReplyMarkupNode.swift index 41c5ebaaa9..7227a449d3 100644 --- a/Telegram-Mac/ReplyMarkupNode.swift +++ b/Telegram-Mac/ReplyMarkupNode.swift @@ -7,10 +7,11 @@ // import Cocoa -import TelegramCoreMac +import TelegramCore + import TGUIKit -import PostboxMac -import SwiftSignalKitMac +import Postbox +import SwiftSignalKit class ReplyMarkupButtonLayout { @@ -20,10 +21,10 @@ class ReplyMarkupButtonLayout { let style:ControlStyle let button:ReplyMarkupButton - init(_ button:ReplyMarkupButton, _ style:ControlStyle = ControlStyle(backgroundColor: theme.colors.grayForeground, highlightColor: theme.colors.text)) { + init(button:ReplyMarkupButton, style:ControlStyle = ControlStyle(backgroundColor: theme.colors.grayForeground, highlightColor: theme.colors.text), isInput: Bool, paid: Bool) { self.button = button self.style = style - self.text = TextViewLayout(NSAttributedString.initialize(string: button.title.fixed, color: theme.colors.text, font: .normal(.short)), maximumNumberOfLines: 1, truncationType: .middle, cutout: nil, alignment: .center) + self.text = TextViewLayout(NSAttributedString.initialize(string: paid ? L10n.messageReplyActionButtonShowReceipt : button.title.fixed, color: theme.controllerBackgroundMode.hasWallpaper && !isInput ? theme.chatServiceItemTextColor : theme.colors.text, font: .normal(.short)), maximumNumberOfLines: 1, truncationType: .middle, cutout: nil, alignment: .center) } func measure(_ width:CGFloat) { @@ -31,9 +32,18 @@ class ReplyMarkupButtonLayout { self.width = width } + deinit { + var bp:Int = 0 + bp += 1 + } + } class ReplyMarkupNode: Node { + + static let buttonHeight:CGFloat = 34 + static let buttonPadding:CGFloat = 4 + static let rowHeight = buttonHeight + buttonPadding private var width:CGFloat = 0 private var height:CGFloat = 0 @@ -42,14 +52,15 @@ class ReplyMarkupNode: Node { private let flags:ReplyMarkupMessageFlags private let interactions:ReplyMarkupInteractions - - init(_ rows:[ReplyMarkupRow], _ flags:ReplyMarkupMessageFlags, _ interactions:ReplyMarkupInteractions, _ view:View? = nil) { + private let isInput: Bool + init(_ rows:[ReplyMarkupRow], _ flags:ReplyMarkupMessageFlags, _ interactions:ReplyMarkupInteractions, _ view:View? = nil, _ isInput: Bool = false, paid: Bool = false) { self.flags = flags + self.isInput = isInput self.interactions = interactions var layoutRows:[[ReplyMarkupButtonLayout]] = Array(repeating: [], count: rows.count) for i in 0 ..< rows.count { for button in rows[i].buttons { - layoutRows[i].append(ReplyMarkupButtonLayout(button)) + layoutRows[i].append(ReplyMarkupButtonLayout(button: button, isInput: isInput, paid: paid)) } } self.markup = layoutRows @@ -63,21 +74,33 @@ class ReplyMarkupNode: Node { var urlView:ImageView? switch button.button.action { - case .url, .switchInline: + case let .url(url): + if !url.isSingleEmoji { + urlView = ImageView() + urlView?.image = theme.chat.chatActionUrl(theme: theme) + urlView?.sizeToFit() + } + case .payment: + urlView = ImageView() + urlView?.image = theme.chat.chatInvoiceAction(theme: theme) + urlView?.sizeToFit() + case .switchInline: urlView = ImageView() - urlView?.image = theme.icons.chatActionUrl + urlView?.image = theme.chat.chatActionUrl(theme: theme) urlView?.sizeToFit() default: break } let btnView = TextView() - btnView.set(handler: { [weak self] _ in - self?.proccess(btnView,button.button) + btnView.set(handler: { [weak self, weak button] control in + if let button = button { + self?.proccess(control, button.button) + } }, for: .Click) btnView.set(handler: { control in - control.change(opacity: 0.85, animated: true) + control.change(opacity: 0.7, animated: true) }, for: .Highlight) btnView.set(handler: { control in control.change(opacity: 1.0, animated: true) @@ -87,6 +110,8 @@ class ReplyMarkupNode: Node { }, for: .Hover) btnView.layer?.cornerRadius = .cornerRadius btnView.isSelectable = false + btnView.disableBackgroundDrawing = true + btnView.backgroundColor = button.style.backgroundColor btnView.set(layout:button.text) @@ -100,8 +125,8 @@ class ReplyMarkupNode: Node { } func proccess(_ control:Control, _ button:ReplyMarkupButton) { - interactions.proccess(button, { loading in - control.backgroundColor = loading ? .black : theme.colors.grayBackground + interactions.proccess(button, { [weak control] loading in + // control?.backgroundColor = loading ? .black : theme.colors.grayBackground }) } @@ -117,9 +142,9 @@ class ReplyMarkupNode: Node { if j == row.count - 1 { w = self.width - rect.minX } - rect.size = NSMakeSize(w, 34) + rect.size = NSMakeSize(w, ReplyMarkupNode.buttonHeight) let button:View? = view?.subviews[i] as? View - button?.backgroundColor = theme.colors.grayBackground + button?.backgroundColor = theme.controllerBackgroundMode.hasWallpaper && !isInput ? theme.chatServiceItemColor : theme.colors.grayBackground if let button = button { button.frame = rect button.setNeedsDisplayLayer() @@ -128,11 +153,11 @@ class ReplyMarkupNode: Node { } } - rect = rect.offsetBy(dx: w + 6, dy: 0) + rect = rect.offsetBy(dx: w + ReplyMarkupNode.buttonPadding, dy: 0) i += 1 j += 1 } - y += 40 + y += ReplyMarkupNode.rowHeight } } @@ -143,7 +168,7 @@ class ReplyMarkupNode: Node { override func measureSize(_ width: CGFloat) { for row in markup { let count = row.count - let single:CGFloat = floorToScreenPixels((width - CGFloat(6 * (count - 1))) / CGFloat(count)) + let single:CGFloat = floorToScreenPixels(System.backingScale, (width - CGFloat(6 * (count - 1))) / CGFloat(count)) for button in row { button.measure(single) } diff --git a/Telegram-Mac/ReplyModel.swift b/Telegram-Mac/ReplyModel.swift index 0e4e5e332b..f8a8930d29 100644 --- a/Telegram-Mac/ReplyModel.swift +++ b/Telegram-Mac/ReplyModel.swift @@ -8,35 +8,55 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac +import TelegramCore + +import SwiftSignalKit +import Postbox class ReplyModel: ChatAccessoryModel { - private var account:Account + private let context:AccountContext private(set) var replyMessage:Message? private var disposable:MetaDisposable = MetaDisposable() private let isPinned:Bool private var previousMedia: Media? private var isLoading: Bool = false private let fetchDisposable = MetaDisposable() - init(replyMessageId:MessageId, account:Account, replyMessage:Message? = nil, isPinned: Bool = false) { + private let makesizeCallback:(()->Void)? + private let autodownload: Bool + private let headerAsName: Bool + private let customHeader: String? + init(replyMessageId:MessageId, context: AccountContext, replyMessage:Message? = nil, isPinned: Bool = false, autodownload: Bool = false, presentation: ChatAccessoryPresentation? = nil, headerAsName: Bool = false, customHeader: String? = nil, drawLine: Bool = true, makesizeCallback: (()->Void)? = nil) { self.isPinned = isPinned - self.account = account + self.context = context + self.makesizeCallback = makesizeCallback + self.autodownload = autodownload self.replyMessage = replyMessage - super.init() + self.headerAsName = headerAsName + self.customHeader = customHeader + super.init(presentation: presentation, drawLine: drawLine) + + let messageViewSignal = context.account.postbox.messageView(replyMessageId) |> take(1) |> mapToSignal { view -> Signal in + if let message = view.message { + return .single(message) + } + return context.engine.messages.getMessagesLoadIfNecessary([view.messageId]) |> map {$0.first} + } + if let replyMessage = replyMessage { make(with :replyMessage, display: false) - nodeReady.set(.single(true)) + if isPinned { + nodeReady.set(.single(true) |> then(messageViewSignal |> deliverOn(Queue.mainQueue()) |> map { [weak self] message -> Bool in + self?.make(with: message, isLoading: false, display: true) + return message != nil + })) + } else { + nodeReady.set(.single(true)) + } + } else { make(with: nil, display: false) - nodeReady.set( account.postbox.messageView(replyMessageId) |> mapToSignal { view -> Signal in - if let message = view.message { - return .single(message) - } - return getMessagesLoadIfNecessary([view.messageId], postbox: account.postbox, network: account.network) |> map {$0.first} - } |> deliverOn(Queue.mainQueue().isCurrent() ? Queue.mainQueue() : prepareQueue) |> map { [weak self] message -> Bool in + nodeReady.set( messageViewSignal |> deliverOn(Queue.mainQueue()) |> map { [weak self] message -> Bool in self?.make(with: message, isLoading: false, display: true) return message != nil }) @@ -58,20 +78,26 @@ class ReplyModel: ChatAccessoryModel { override var leftInset: CGFloat { var imageDimensions: CGSize? if let message = replyMessage { - for media in message.media { - if let image = media as? TelegramMediaImage { - if let representation = largestRepresentationForPhoto(image) { - imageDimensions = representation.dimensions + if !message.containsSecretMedia { + for media in message.media { + if let image = media as? TelegramMediaImage { + if let representation = largestRepresentationForPhoto(image) { + imageDimensions = representation.dimensions.size + } + break + } else if let file = media as? TelegramMediaFile, (file.isVideo || file.isSticker) { + if let dimensions = file.dimensions { + imageDimensions = dimensions.size + } else if let representation = largestImageRepresentation(file.previewRepresentations), !file.isStaticSticker { + imageDimensions = representation.dimensions.size + } else if file.isAnimatedSticker { + imageDimensions = NSMakeSize(30, 30) + } + break } - break } -// else if let file = media as? TelegramMediaFile { -// if let representation = largestImageRepresentation(file.previewRepresentations), !file.isSticker { -// imageDimensions = representation.dimensions -// } -// break -// } } + if let _ = imageDimensions { return 30 + super.leftInset * 2 @@ -91,74 +117,105 @@ class ReplyModel: ChatAccessoryModel { } private func updateImageIfNeeded() { - Queue.mainQueue().async { - if let message = self.replyMessage, let view = self.view, view.frame != NSZeroRect { - var updatedMedia: Media? - var imageDimensions: CGSize? + if let message = self.replyMessage, let view = self.view { + var updatedMedia: Media? + var imageDimensions: CGSize? + var hasRoundImage = false + if !message.containsSecretMedia { for media in message.media { if let image = media as? TelegramMediaImage { updatedMedia = image if let representation = largestRepresentationForPhoto(image) { - imageDimensions = representation.dimensions + imageDimensions = representation.dimensions.size + } + break + } else if let file = media as? TelegramMediaFile, (file.isVideo || file.isSticker) { + updatedMedia = file + + if let dimensions = file.dimensions?.size { + imageDimensions = dimensions + } else if let representation = largestImageRepresentation(file.previewRepresentations) { + imageDimensions = representation.dimensions.size + } else if file.isAnimatedSticker { + imageDimensions = NSMakeSize(30, 30) + } + if file.isInstantVideo { + hasRoundImage = true } break } } + } + + + if let imageDimensions = imageDimensions { + let boundingSize = CGSize(width: 30.0, height: 30.0) + let arguments = TransformImageArguments(corners: ImageCorners(radius: 2.0), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: NSEdgeInsets()) - if let imageDimensions = imageDimensions { - let boundingSize = CGSize(width: 30.0, height: 30.0) - let arguments = TransformImageArguments(corners: ImageCorners(radius: 2.0), imageSize: imageDimensions.aspectFilled(boundingSize), boundingSize: boundingSize, intrinsicInsets: NSEdgeInsets()) - - if view.imageView == nil { - view.imageView = TransformImageView() - } - view.imageView?.setFrameSize(boundingSize) + if view.imageView == nil { + view.imageView = TransformImageView() + } + view.imageView?.setFrameSize(boundingSize) + if view.imageView?.superview == nil { view.addSubview(view.imageView!) - view.imageView?.centerY(x: super.leftInset) - - - var mediaUpdated = false - if let updatedMedia = updatedMedia, let previousMedia = self.previousMedia { - mediaUpdated = !updatedMedia.isEqual(previousMedia) - } else if (updatedMedia != nil) != (self.previousMedia != nil) { - mediaUpdated = true + } + + view.imageView?.setFrameOrigin(super.leftInset + (self.isSideAccessory ? 10 : 0), floorToScreenPixels(System.backingScale, self.topOffset + (max(34, self.size.height) - self.topOffset - boundingSize.height)/2)) + + + let mediaUpdated = true + + + var updateImageSignal: Signal? + if mediaUpdated { + if let image = updatedMedia as? TelegramMediaImage { + updateImageSignal = chatMessagePhotoThumbnail(account: self.context.account, imageReference: ImageMediaReference.message(message: MessageReference(message), media: image), scale: view.backingScaleFactor, synchronousLoad: true) + } else if let file = updatedMedia as? TelegramMediaFile { + if file.isVideo { + updateImageSignal = chatMessageVideoThumbnail(account: self.context.account, fileReference: FileMediaReference.message(message: MessageReference(message), media: file), scale: view.backingScaleFactor, synchronousLoad: false) + } else if file.isAnimatedSticker { + updateImageSignal = chatMessageAnimatedSticker(postbox: self.context.account.postbox, file: FileMediaReference.message(message: MessageReference(message), media: file), small: true, scale: view.backingScaleFactor, size: imageDimensions.aspectFitted(boundingSize), fetched: true) + } else if file.isSticker { + updateImageSignal = chatMessageSticker(postbox: self.context.account.postbox, file: FileMediaReference.message(message: MessageReference(message), media: file), small: true, scale: view.backingScaleFactor, fetched: true) + } else if let iconImageRepresentation = smallestImageRepresentation(file.previewRepresentations) { + let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [iconImageRepresentation], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + updateImageSignal = chatWebpageSnippetPhoto(account: self.context.account, imageReference: ImageMediaReference.message(message: MessageReference(message), media: tmpImage), scale: view.backingScaleFactor, small: true, synchronousLoad: true) + } } + } + + + if let updateImageSignal = updateImageSignal, let media = updatedMedia { + view.imageView?.setSignal(signal: cachedMedia(media: media, arguments: arguments, scale: System.backingScale), clearInstantly: false) + - var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? - if mediaUpdated { - if let image = updatedMedia as? TelegramMediaImage { - updateImageSignal = chatMessagePhotoThumbnail(account: self.account, photo: image, scale: view.backingScaleFactor) - } else if let file = updatedMedia as? TelegramMediaFile { - + view.imageView?.setSignal(updateImageSignal, animate: true, synchronousLoad: true, cacheImage: { [weak media] result in + if let media = media { + cacheMedia(result, media: media, arguments: arguments, scale: System.backingScale) } + }) + + if let media = media as? TelegramMediaImage { + self.fetchDisposable.set(chatMessagePhotoInteractiveFetched(account: self.context.account, imageReference: ImageMediaReference.message(message: MessageReference(message), media: media)).start()) } - if let updateImageSignal = updateImageSignal, let media = updatedMedia { - - view.imageView?.setSignal(signal: cachedMedia(media: media, size: arguments.imageSize, scale: view.backingScaleFactor)) - - if view.imageView?.layer?.contents == nil { - view.imageView?.setSignal(account: self.account, signal: updateImageSignal, animate: true, cacheImage: { image in - return cacheMedia(signal: image, media: media, size: arguments.imageSize, scale: System.backingScale) - }) - if let media = media as? TelegramMediaImage { - self.fetchDisposable.set(chatMessagePhotoInteractiveFetched(account: self.account, photo: media).start()) - } - } - - view.imageView?.set(arguments: arguments) + view.imageView?.set(arguments: arguments) + if hasRoundImage { + view.imageView!.layer?.cornerRadius = 15 + } else { + view.imageView?.layer?.cornerRadius = 0 } - } else { - view.imageView?.removeFromSuperview() - view.imageView = nil } - - self.previousMedia = updatedMedia } else { - self.view?.imageView?.removeFromSuperview() - self.view?.imageView = nil + view.imageView?.removeFromSuperview() + view.imageView = nil } + + self.previousMedia = updatedMedia + } else { + self.view?.imageView?.removeFromSuperview() + self.view?.imageView = nil } } @@ -166,27 +223,53 @@ class ReplyModel: ChatAccessoryModel { self.replyMessage = message self.isLoading = isLoading + var display: Bool = display updateImageIfNeeded() if let message = message { + + var title: String? = message.effectiveAuthor?.displayTitle + for attr in message.attributes { + if let _ = attr as? SourceReferenceMessageAttribute { + if let info = message.forwardInfo { + title = info.authorTitle + } + break + } + } + - var text = pullText(from:message, attachEmoji: false) as String + var text = message.restrictedText(context.contentSettings) ?? pullText(from:message, mediaViewType: .text) as String if text.isEmpty { - text = serviceMessageText(message, account: account) + text = serviceMessageText(message, account: context.account, isReplied: true) + } + if let header = customHeader { + self.headerAttr = .initialize(string: header, color: presentation.title, font: .medium(.text)) + } else { + self.headerAttr = .initialize(string: !isPinned || headerAsName ? title : L10n.chatHeaderPinnedMessage, color: presentation.title, font: .medium(.text)) } - self.headerAttr = .initialize(string: !isPinned ? message.author?.displayTitle : tr(.chatHeaderPinnedMessage), color: theme.colors.blueUI, font: .medium(.text)) - self.messageAttr = .initialize(string: text, color: message.media.isEmpty ? theme.colors.text : theme.colors.grayText, font: .normal(.text)) + self.messageAttr = .initialize(string: text, color: message.media.isEmpty || message.media.first is TelegramMediaWebpage ? presentation.enabledText : presentation.disabledText, font: .normal(.text)) } else { self.headerAttr = nil - self.messageAttr = .initialize(string: isLoading ? tr(.messagesReplyLoadingLoading) : tr(.messagesDeletedMessage), color: theme.colors.grayText, font: .normal(.text)) + self.messageAttr = .initialize(string: isLoading ? tr(L10n.messagesReplyLoadingLoading) : tr(L10n.messagesDeletedMessage), color: presentation.disabledText, font: .normal(.text)) + display = true } if !isLoading { - measureSize(size.width) + if let makesizeCallback = makesizeCallback { + messagesViewQueue.async { + makesizeCallback() + } + return + } else { + measureSize(width, sizeToFit: sizeToFit) + display = true + } } if display { Queue.mainQueue().async { + self.view?.setFrameSize(self.size) self.setNeedDisplay() } } diff --git a/Telegram-Mac/ReportDetailsController.swift b/Telegram-Mac/ReportDetailsController.swift new file mode 100644 index 0000000000..ef63e78139 --- /dev/null +++ b/Telegram-Mac/ReportDetailsController.swift @@ -0,0 +1,113 @@ +// +// ReportDetailsController.swift +// Telegram +// +// Created by Mikhail Filimonov on 19.02.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit + +private final class Arguments { + let context: AccountContext + init(context: AccountContext) { + self.context = context + } +} + +private struct State : Equatable { + var value: ReportReasonValue +} + +private let _id_input = InputDataIdentifier("_id_input") + +private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: .init("sticker"), equatable: nil, comparable: nil, item: { initialSize, stableId in + return AnimtedStickerHeaderItem(initialSize, stableId: stableId, context: arguments.context, sticker: .police, text: .initialize(string: L10n.reportAdditionText, color: theme.colors.text, font: .normal(.text))) + })) + index += 1 + // entries + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.input(sectionId: sectionId, index: index, value: .string(state.value.comment), error: nil, identifier: _id_input, mode: .plain, data: InputDataRowData(viewType: .singleItem), placeholder: nil, inputPlaceholder: L10n.reportAdditionTextPlaceholder, filter: { $0 }, limit: 128)) + index += 1 + + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func ReportDetailsController(context: AccountContext, reason: ReportReasonValue, updated: @escaping(ReportReasonValue)->Void) -> InputDataModalController { + + let actionsDisposable = DisposableSet() + + var close:(()->Void)? = nil + + let initialState = State(value: reason) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let arguments = Arguments(context: context) + + let signal = statePromise.get() |> map { state in + return InputDataSignalValue(entries: entries(state, arguments: arguments)) + } + + let controller = InputDataController(dataSignal: signal, title: L10n.reportAdditionTextButton) + + controller.onDeinit = { + actionsDisposable.dispose() + } + controller.updateDatas = { data in + updateState { current in + var current = current + current.value = .init(reason: current.value.reason, comment: data[_id_input]?.stringValue ?? "") + return current + } + return .none + } + + controller.validateData = { _ in + close?() + return .none + } + + + let modalInteractions = ModalInteractions(acceptTitle: L10n.reportAdditionTextButton, accept: { [weak controller] in + _ = controller?.returnKeyAction() + }, drawBorder: true, height: 50, singleButton: true) + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) + + close = { [weak modalController] in + modalController?.modal?.close() + updated(stateValue.with { $0.value }) + } + + return modalController + +} + diff --git a/Telegram-Mac/ReportReasonModalController.swift b/Telegram-Mac/ReportReasonModalController.swift index 789b77ba6d..71c688b392 100644 --- a/Telegram-Mac/ReportReasonModalController.swift +++ b/Telegram-Mac/ReportReasonModalController.swift @@ -8,85 +8,224 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac -import PostboxMac -import TelegramCoreMac +import SwiftSignalKit +import Postbox +import TelegramCore + + +struct ReportReasonValue : Equatable { + let reason: ReportReason + let comment: String +} + + +func reportReasonSelector(context: AccountContext, buttonText: String = L10n.reportReasonReport) -> Signal { + let promise: ValuePromise = ValuePromise() + let controller = ReportReasonController(callback: { reason in + promise.set(reason) + }, buttonText: buttonText) + showModal(with: controller, for: context.window) + + return promise.get() |> take(1) +} -fileprivate class ReportReasonModalController: ModalViewController { - fileprivate var onComplete:Signal { - return _complete.get() |> take(1) +private final class ReportReasonArguments { + let selectReason:(ReportReason)->Void + init(selectReason:@escaping(ReportReason)->Void) { + self.selectReason = selectReason } - private let _complete:Promise = Promise() - private var current:ReportPeerReason = .spam - override func viewClass() -> AnyClass { - return TableView.self +} + +private struct ReportReasonState : Equatable { + let value: ReportReasonValue + init(value: ReportReasonValue) { + self.value = value } - var genericView:TableView { - return self.view as! TableView + func withUpdatedReason(_ value: ReportReasonValue) -> ReportReasonState { + return ReportReasonState(value: value) } - - override func viewDidLoad() { - super.viewDidLoad() - - let updateState:(ReportPeerReason) -> Void = { [weak self] reason in - self?.current = reason - self?.genericView.reloadData() +} + +private let _id_spam = InputDataIdentifier("_id_spam") +private let _id_violence = InputDataIdentifier("_id_violence") +private let _id_porno = InputDataIdentifier("_id_porno") +private let _id_childAbuse = InputDataIdentifier("_id_childAbuse") +private let _id_copyright = InputDataIdentifier("_id_copyright") +private let _id_custom = InputDataIdentifier("_id_custom") +private let _id_fake = InputDataIdentifier("_id_fake") +private let _id_custom_input = InputDataIdentifier("_id_custom_input") + +private extension ReportReason { + var id: InputDataIdentifier { + switch self { + case .spam: + return _id_spam + case .violence: + return _id_violence + case .porno: + return _id_porno + case .childAbuse: + return _id_childAbuse + case .copyright: + return _id_copyright + case .custom: + return _id_custom + case .fake: + return _id_fake + default: + fatalError("unsupported") } - - let initialSize = atomicSize.modify {$0} - _ = genericView.addItem(item: GeneralInteractedRowItem(initialSize, name: tr(.reportReasonSpam), type: .selectable(stateback: { [weak self] () -> Bool in - if let current = self?.current { - return current == .spam + } + var title: String { + switch self { + case .spam: + return L10n.reportReasonSpam + case .violence: + return L10n.reportReasonViolence + case .porno: + return L10n.reportReasonPorno + case .childAbuse: + return L10n.reportReasonChildAbuse + case .copyright: + return L10n.reportReasonCopyright + case .custom: + return L10n.reportReasonOther + case .fake: + return L10n.reportReasonFake + default: + fatalError("unsupported") + } + } + + func isEqual(to other: ReportReason) -> Bool { + switch self { + case .spam: + if case .spam = other { + return true + } + case .violence: + if case .violence = other { + return true + } + case .porno: + if case .porno = other { + return true + } + case .childAbuse: + if case .childAbuse = other { + return true } - return false - }), action: { - updateState(.spam) - })) - - _ = genericView.addItem(item: GeneralInteractedRowItem(initialSize, name: tr(.reportReasonViolence), type: .selectable(stateback: { [weak self] () -> Bool in - if let current = self?.current { - return current == .violence + case .copyright: + if case .copyright = other { + return true } - return false - }), action: { - updateState(.violence) - })) - - _ = genericView.addItem(item: GeneralInteractedRowItem(initialSize, name: tr(.reportReasonPorno), type: .selectable(stateback: { [weak self] () -> Bool in - if let current = self?.current { - return current == .porno + case .custom: + if case .custom = other { + return true } - return false - }), action: { - updateState(.porno) - }, drawCustomSeparator: false)) + case .fake: + if case .fake = other { + return true + } + default: + fatalError("unsupported") + } + return false + } +} - readyOnce() +private func reportReasonEntries(state: ReportReasonState, arguments: ReportReasonArguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index:Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + let reasons:[ReportReason] = [.spam, .fake, .violence, .porno, .childAbuse, .copyright] + + for (i, reason) in reasons.enumerated() { + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: reason.id, data: InputDataGeneralData(name: reason.title, color: theme.colors.text, type: .none, viewType: bestGeneralViewType(reasons, for: i), action: { + arguments.selectReason(reason) + }))) + index += 1 } - override var modalInteractions: ModalInteractions? { - return ModalInteractions(acceptTitle: tr(.modalOK), accept: { [weak self] in - if let strongSelf = self { - self?._complete.set(.single(strongSelf.current)) - self?.close() - } - }, cancelTitle: tr(.modalCancel), drawBorder: true, height: 40) + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + +// entries.append(.input(sectionId: sectionId, index: index, value: .string(state.value.comment), error: nil, identifier: _id_custom_input, mode: .plain, data: InputDataRowData(viewType: .singleItem), placeholder: nil, inputPlaceholder: L10n.reportReasonOtherPlaceholder, filter: { $0 }, limit: 128)) +// index += 1 +// +// entries.append(.sectionId(sectionId, type: .normal)) +// sectionId += 1 + + return entries +} + +func ReportReasonController(callback: @escaping(ReportReasonValue)->Void, buttonText: String = L10n.reportReasonReport) -> InputDataModalController { + let initialState = ReportReasonState(value: .init(reason: .spam, comment: "")) + let state: ValuePromise = ValuePromise(initialState) + let stateValue: Atomic = Atomic(value: initialState) + + let updateState:((ReportReasonState)->ReportReasonState) -> Void = { f in + state.set(stateValue.modify(f)) } - override init() { - super.init(frame: NSMakeRect(0, 0, 260, 130)) - bar = .init(height: 0) + var getModalController:(()->InputDataModalController?)? = nil + + let arguments = ReportReasonArguments(selectReason: { reason in + callback(.init(reason: reason, comment: "")) + getModalController?()?.close() + }) + + let dataSignal = state.get() |> deliverOnPrepareQueue |> map { state in + return reportReasonEntries(state: state, arguments: arguments) + } |> map { entries in + return InputDataSignalValue(entries: entries) } -} -func reportReasonSelector() -> Signal { - let reportModalView = ReportReasonModalController() - showModal(with: reportModalView, for: mainWindow) - return reportModalView.onComplete + let controller = InputDataController(dataSignal: dataSignal, title: L10n.peerInfoReport) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { + getModalController?()?.close() + }) + + controller.updateDatas = { data in + updateState { current in + return current.withUpdatedReason(.init(reason: current.value.reason, comment: data[_id_custom_input]?.stringValue ?? "")) + } + return .none + } + + + let modalInteractions = ModalInteractions(acceptTitle: buttonText, accept: { [weak controller] in + controller?.validateInputValues() + }, drawBorder: true, singleButton: true) + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions, closeHandler: { f in + f() + }, size: NSMakeSize(300, 350)) + + getModalController = { [weak modalController] in + return modalController + } + + controller.validateData = { data in + return .success(.custom { + callback(stateValue.with { $0.value }) + getModalController?()?.close() + }) + } + + + return modalController } + diff --git a/Telegram-Mac/RestictedModalViewController.swift b/Telegram-Mac/RestictedModalViewController.swift index a62482c14d..9c9f51b2c3 100644 --- a/Telegram-Mac/RestictedModalViewController.swift +++ b/Telegram-Mac/RestictedModalViewController.swift @@ -9,104 +9,53 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit private final class RestrictedControllerArguments { - let account: Account - let toggleRight: (TelegramChannelBannedRightsFlags, TelegramChannelBannedRightsFlags) -> Void + let context: AccountContext + let toggleRight: (TelegramChatBannedRightsFlags, Bool) -> Void let changeUntil:()->Void - init(account: Account, toggleRight: @escaping (TelegramChannelBannedRightsFlags, TelegramChannelBannedRightsFlags) -> Void, changeUntil: @escaping () -> Void) { - self.account = account + let alertError:() -> Void + let deleteException:()->Void + init(context: AccountContext, toggleRight: @escaping (TelegramChatBannedRightsFlags, Bool) -> Void, changeUntil: @escaping () -> Void, alertError: @escaping() -> Void, deleteException:@escaping()->Void) { + self.context = context self.toggleRight = toggleRight self.changeUntil = changeUntil + self.alertError = alertError + self.deleteException = deleteException } } private enum RestrictedEntryStableId: Hashable { case info - case right(TelegramChannelBannedRightsFlags) + case right(TelegramChatBannedRightsFlags) case description(Int32) case section(Int32) - case blockFor + case timeout + case exceptionInfo + case delete var hashValue: Int { - switch self { - case .info: - return 0 - case .description(let index): - return Int(index) - case .section(let section): - return Int(section) - case let .right(flags): - return flags.rawValue.hashValue - case .blockFor: - return 1 - } - } - - static func ==(lhs: RestrictedEntryStableId, rhs: RestrictedEntryStableId) -> Bool { - switch lhs { - case .info: - if case .info = rhs { - return true - } else { - return false - } - case .blockFor: - if case .blockFor = rhs { - return true - } else { - return false - } - case let .right(flags): - if case .right(flags) = rhs { - return true - } else { - return false - } - case let .section(section): - if case .section(section) = rhs { - return true - } else { - return false - } - case .description(let text): - if case .description(text) = rhs { - return true - } else { - return false - } - } + return 0 } } private enum RestrictedEntry: TableItemListNodeEntry { - case info(Int32, Peer, TelegramUserPresence?) - case rightItem(Int32, Int32, String, TelegramChannelBannedRightsFlags, TelegramChannelBannedRightsFlags, Bool, Bool) - case description(Int32, Int32, String) + case info(Int32, Peer, TelegramUserPresence?, GeneralViewType) + case rightItem(Int32, Int32, String, TelegramChatBannedRightsFlags, Bool, Bool, GeneralViewType) + case description(Int32, Int32, String, GeneralViewType) case section(Int32) - case blockFor(Int32, Int32, Int32) + case timeout(Int32, Int32, String, String, GeneralViewType) + case exceptionInfo(Int32, Int32, String, GeneralViewType) + case delete(Int32, Int32, String, GeneralViewType) - var stableId: RestrictedEntryStableId { - switch self { - case .info: - return .info - case .blockFor: - return .blockFor - case let .rightItem(_, _, _, right, _, _, _): - return .right(right) - case .description(_, let index, _): - return .description(index) - case .section(let sectionId): - return .section(sectionId) - } - } static func ==(lhs: RestrictedEntry, rhs: RestrictedEntry) -> Bool { switch lhs { - case let .info(lhsSectionId, lhsPeer, lhsPresence): - if case let .info(rhsSectionId, rhsPeer, rhsPresence) = rhs { + case let .info(lhsSectionId, lhsPeer, lhsPresence, lhsViewType): + if case let .info(rhsSectionId, rhsPeer, rhsPresence, rhsViewType) = rhs { if lhsSectionId != rhsSectionId { return false } @@ -116,40 +65,21 @@ private enum RestrictedEntry: TableItemListNodeEntry { if lhsPresence != rhsPresence { return false } - + if lhsViewType != rhsViewType { + return false + } return true } else { return false } - case let .rightItem(lhsSectionId, lhsIndex, lhsText, lhsRight, lhsFlags, lhsValue, lhsEnabled): - if case let .rightItem(rhsSectionId, rhsIndex, rhsText, rhsRight, rhsFlags, rhsValue, rhsEnabled) = rhs { - if lhsSectionId != rhsSectionId { - return false - } - if lhsIndex != rhsIndex { - return false - } - if lhsText != rhsText { - return false - } - if lhsRight != rhsRight { - return false - } - if lhsFlags != rhsFlags { - return false - } - if lhsValue != rhsValue { - return false - } - if lhsEnabled != rhsEnabled { - return false - } + case let .rightItem(sectionId, index, text, flags, value, enabled, viewType): + if case .rightItem(sectionId, index, text, flags, value, enabled, viewType) = rhs { return true } else { return false } - case let .description(sectionId, index, text): - if case .description(sectionId, index, text) = rhs{ + case let .description(sectionId, index, text, viewType): + if case .description(sectionId, index, text, viewType) = rhs{ return true } else { return false @@ -160,8 +90,20 @@ private enum RestrictedEntry: TableItemListNodeEntry { } else { return false } - case let .blockFor(sectionId, index, until): - if case .blockFor(sectionId, index, until) = rhs{ + case let .exceptionInfo(sectionId, index, text, viewType): + if case .exceptionInfo(sectionId, index, text, viewType) = rhs { + return true + } else { + return false + } + case let .delete(sectionId, index, text, viewType): + if case .delete(sectionId, index, text, viewType) = rhs { + return true + } else { + return false + } + case let .timeout(sectionId, index, title, value, viewType): + if case .timeout(sectionId, index, title, value, viewType) = rhs{ return true } else { return false @@ -169,17 +111,41 @@ private enum RestrictedEntry: TableItemListNodeEntry { } } + + var stableId: RestrictedEntryStableId { + switch self { + case .info: + return .info + case .timeout: + return .timeout + case let .rightItem(_, _, _, right, _, _, _): + return .right(right) + case .description(_, let index, _, _): + return .description(index) + case .exceptionInfo: + return .exceptionInfo + case .delete: + return .delete + case .section(let sectionId): + return .section(sectionId) + } + } + var index:Int32 { switch self { - case .info(let sectionId, _, _): + case .info(let sectionId, _, _, _): return (sectionId * 1000) + 0 - case .description(let sectionId, let index, _): + case .description(let sectionId, let index, _, _): + return (sectionId * 1000) + index + case .delete(let sectionId, let index, _, _): + return (sectionId * 1000) + index + case .exceptionInfo(let sectionId, let index, _, _): return (sectionId * 1000) + index case .rightItem(let sectionId, let index, _, _, _, _, _): return (sectionId * 1000) + Int32(index) + 10 case .section(let sectionId): return (sectionId + 1) * 1000 - sectionId - case .blockFor(let sectionId, let index, _): + case .timeout(let sectionId, let index, _, _, _): return (sectionId * 1000) + index } } @@ -191,42 +157,34 @@ private enum RestrictedEntry: TableItemListNodeEntry { func item(_ arguments: RestrictedControllerArguments, initialSize: NSSize) -> TableRowItem { switch self { case .section: - return GeneralRowItem(initialSize, height: 20, stableId: stableId) - case .info(_, let peer, let presence): - var string:String = peer.isBot ? tr(.presenceBot) : tr(.peerStatusRecently) + return GeneralRowItem(initialSize, height: 30, stableId: stableId, viewType: .separator) + case let .info(_, peer, presence, viewType): + var string:String = peer.isBot ? L10n.presenceBot : L10n.peerStatusRecently var color:NSColor = theme.colors.grayText if let presence = presence { let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - (string,_, color) = stringAndActivityForUserPresence(presence, relativeTo: Int32(timestamp)) + (string,_, color) = stringAndActivityForUserPresence(presence, timeDifference: arguments.context.timeDifference, relativeTo: Int32(timestamp)) } - return ShortPeerRowItem(initialSize, peer: peer, account: arguments.account, stableId: stableId, enabled: true, height: 60, photoSize: NSMakeSize(50, 50), statusStyle: ControlStyle(font: NSFont.normal(.custom(14)), foregroundColor: color), status: string, borderType: [], drawCustomSeparator: false, drawLastSeparator: false, inset: NSEdgeInsets(left: 25, right: 25), drawSeparatorIgnoringInset: false, action: {}) - case let .rightItem(_, _, name, right, flags, value, enabled): + return ShortPeerRowItem(initialSize, peer: peer, account: arguments.context.account, stableId: stableId, enabled: true, height: 60, photoSize: NSMakeSize(40, 40), statusStyle: ControlStyle(font: .normal(.title), foregroundColor: color), status: string, borderType: [], drawCustomSeparator: false, drawLastSeparator: false, inset: NSEdgeInsets(left: 25, right: 25), drawSeparatorIgnoringInset: false, viewType: viewType, action: {}) + case let .rightItem(_, _, name, right, value, enabled, viewType): //ControlStyle(font: NSFont.) - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: name, nameStyle: ControlStyle(font: .normal(.title), foregroundColor: enabled ? .text : .gray), type: .switchable(stateback: { () -> Bool in - return value - }), action: { - arguments.toggleRight(right, flags) - }, enabled: enabled, switchAppearance: SwitchViewAppearance(backgroundColor: .white, stateOnColor: theme.colors.blueUI, stateOffColor: theme.colors.redUI, disabledColor: theme.colors.grayBackground, borderColor: .clear)) - case .description(_, _, let name): - return GeneralTextRowItem(initialSize, stableId: stableId, text: name) - case .blockFor(_, _, let until): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.channelBlockUserBlockFor), type: .context(stateback: { () -> String in - if until == 0 || until == .max { - return tr(.channelBanForever) - } else { - let formatter = DateFormatter() - formatter.dateStyle = .medium - formatter.locale = Locale(identifier: appCurrentLanguage.languageCode) - formatter.timeStyle = .short - return formatter.string(from: Date(timeIntervalSince1970: TimeInterval(until))) - } - - }), action: { + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: name, nameStyle: ControlStyle(font: .normal(.title), foregroundColor: enabled ? theme.colors.text : theme.colors.grayText), type: .switchable(value), viewType: viewType, action: { + arguments.toggleRight(right, !value) + }, enabled: enabled, switchAppearance: SwitchViewAppearance(backgroundColor: .white, stateOnColor: theme.colors.accent, stateOffColor: theme.colors.redUI, disabledColor: theme.colors.grayBackground, borderColor: .clear), disabledAction: { + arguments.alertError() + }) + case let .description(_, _, name, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: name, viewType: viewType) + case let .timeout(_, _, title, value, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: title, type: .nextContext(value), viewType: viewType, action: { arguments.changeUntil() }) + case let .exceptionInfo(_, _, text, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, viewType: viewType) + case let .delete(_, _, name, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: name, nameStyle: redActionButton, type: .next, viewType: viewType, action: arguments.deleteException) } - //return TableRowItem(initialSize) } } @@ -239,293 +197,482 @@ private enum RestrictUntil : Int32 { } private struct RestrictedControllerState: Equatable { - let updatedFlags: TelegramChannelBannedRightsFlags? - let until: Int32 - - init(updatedFlags: TelegramChannelBannedRightsFlags? = nil, until: Int32 = 0) { - self.updatedFlags = updatedFlags - self.until = until - } - - static func ==(lhs: RestrictedControllerState, rhs: RestrictedControllerState) -> Bool { - if lhs.updatedFlags != rhs.updatedFlags { - return false - } - if lhs.until != rhs.until { - return false - } - return true - } - - func withUpdatedUpdatedFlags(_ updatedFlags: TelegramChannelBannedRightsFlags?) -> RestrictedControllerState { - return RestrictedControllerState(updatedFlags: updatedFlags, until: self.until) - } - - func withUpdatedUntil(_ until: Int32) -> RestrictedControllerState { - return RestrictedControllerState(updatedFlags: self.updatedFlags, until: until) - } + var referenceTimestamp: Int32 + var updatedFlags: TelegramChatBannedRightsFlags? + var updatedTimeout: Int32? + var updating: Bool = false } - -private func banRightDependencies(_ right: TelegramChannelBannedRightsFlags) -> [TelegramChannelBannedRightsFlags] { - - if right.contains(.banReadMessages) { - return [.banSendMessages, .banSendMedia, .banSendStickers, .banSendGifs, .banEmbedLinks] - } else if right.contains(.banSendMessages) { - return [.banSendMedia, .banSendStickers, .banSendGifs, .banEmbedLinks] - } else if right.contains(.banSendMedia) { - return [.banSendStickers, .banSendGifs, .banEmbedLinks] - } else if right.contains(.banSendStickers) { - return [.banSendGifs] - } - - return [] -} - -private func unbanRightDependencies(_ right: TelegramChannelBannedRightsFlags) -> [TelegramChannelBannedRightsFlags] { - - if right.contains(.banReadMessages) { - return [] - } else if right.contains(.banSendMessages) { - return [.banReadMessages] - } else if right.contains(.banSendMedia) { - return [.banSendMessages, .banReadMessages] - } else if right.contains(.banSendStickers) { - return [.banSendMessages, .banReadMessages, .banSendMedia, .banSendGifs] - } else if right.contains(.banEmbedLinks) { - return [.banSendMessages, .banReadMessages, .banSendMedia] +private func completeRights(_ flags: TelegramChatBannedRightsFlags) -> TelegramChatBannedRightsFlags { + var result = flags + result.remove(.banReadMessages) + if result.contains(.banSendGifs) { + result.insert(.banSendStickers) + result.insert(.banSendGifs) + result.insert(.banSendGames) + result.insert(.banSendInline) + } else { + result.remove(.banSendStickers) + result.remove(.banSendGifs) + result.remove(.banSendGames) + result.insert(.banSendInline) } - - return [] + return result } -private func RestrictedEntries(state: RestrictedControllerState, participant: RenderedChannelParticipant, view: PeerView) -> [RestrictedEntry] { +private func restrictedEntries(state: RestrictedControllerState, accountPeerId: PeerId, channelView: PeerView, memberView: PeerView, initialParticipant: ChannelParticipant?, initialBannedBy: Peer?) -> [RestrictedEntry] { var index:Int32 = 0 - var sectionId:Int32 = 1 + var sectionId:Int32 = 0 var entries:[RestrictedEntry] = [] entries.append(.section(sectionId)) sectionId += 1 - entries.append(.info(sectionId, participant.peer, participant.presences[participant.peer.id] as? TelegramUserPresence)) - entries.append(.section(sectionId)) - sectionId += 1 - entries.append(.description(sectionId, index, tr(.channelUserRestriction))) - index += 1 - - if let peer = peerViewMainPeer(view) as? TelegramChannel { - switch participant.participant { - case .member(_, _, _, let banInfo): - - if let banInfo = banInfo { - let restrictions:[(TelegramChannelBannedRightsFlags,String)] = [(.banReadMessages, tr(.channelBlockUserCanReadMessages)), (.banSendMessages, tr(.channelBlockUserCanSendMessages)), (.banSendMedia, tr(.channelBlockUserCanSendMedia)), ([.banSendStickers], tr(.channelBlockUserCanSendStickers)), (.banEmbedLinks, tr(.channelBlockUserCanEmbedLinks))] - let currentRightsFlags: TelegramChannelBannedRightsFlags - if let updatedFlags = state.updatedFlags { - currentRightsFlags = updatedFlags - } else { - currentRightsFlags = banInfo.rights.flags - } + if let peer = channelView.peers[channelView.peerId] as? TelegramChannel, let defaultBannedRights = peer.defaultBannedRights, let member = memberView.peers[memberView.peerId] { + entries.append(.info(sectionId, member, memberView.peerPresences[member.id] as? TelegramUserPresence, .singleItem)) + + entries.append(.section(sectionId)) + sectionId += 1 + + entries.append(.description(sectionId, index, L10n.groupPermissionSectionTitle, .textTopItem)) + index += 1 + + let currentRightsFlags: TelegramChatBannedRightsFlags + if let updatedFlags = state.updatedFlags { + currentRightsFlags = updatedFlags + } else if let initialParticipant = initialParticipant, case let .member(_, _, _, maybeBanInfo, _) = initialParticipant, let banInfo = maybeBanInfo { + currentRightsFlags = banInfo.rights.flags + } else { + currentRightsFlags = defaultBannedRights.flags + } + + let currentTimeout: Int32 + if let updatedTimeout = state.updatedTimeout { + currentTimeout = updatedTimeout + } else if let initialParticipant = initialParticipant, case let .member(_, _, _, maybeBanInfo, _) = initialParticipant, let banInfo = maybeBanInfo { + currentTimeout = banInfo.rights.untilDate + } else { + currentTimeout = Int32.max + } + + let currentTimeoutString: String + if currentTimeout == 0 || currentTimeout == Int32.max { + currentTimeoutString = L10n.timerForever + } else { + let remainingTimeout = currentTimeout - state.referenceTimestamp + currentTimeoutString = timeIntervalString(Int(remainingTimeout)) + } + + + for right in allGroupPermissionList { + let defaultEnabled = !defaultBannedRights.flags.contains(right) + entries.append(.rightItem(sectionId, index, stringForGroupPermission(right: right), right, defaultEnabled && !currentRightsFlags.contains(right), defaultEnabled && !state.updating, bestGeneralViewType(allGroupPermissionList, for: right))) + index += 1 + } + + entries.append(.section(sectionId)) + sectionId += 1 + + + + if let initialParticipant = initialParticipant, case let .member(member) = initialParticipant, let banInfo = member.banInfo, let initialBannedBy = initialBannedBy { + entries.append(.timeout(sectionId, index, L10n.groupPermissionDuration, currentTimeoutString, .firstItem)) + index += 1 + entries.append(.delete(sectionId, index, L10n.groupPermissionDelete, .lastItem)) + index += 1 + entries.append(.exceptionInfo(sectionId, index, L10n.groupPermissionAddedInfo(initialBannedBy.displayTitle, stringForRelativeSymbolicTimestamp(relativeTimestamp: banInfo.timestamp, relativeTo: state.referenceTimestamp)), .textBottomItem)) + index += 1 + } else { + entries.append(.timeout(sectionId, index, L10n.groupPermissionDuration, currentTimeoutString, .singleItem)) + index += 1 + } + + } else if let group = channelView.peers[channelView.peerId] as? TelegramGroup, let defaultBannedRights = group.defaultBannedRights, let member = memberView.peers[memberView.peerId] { + entries.append(.info(sectionId, member, memberView.peerPresences[member.id] as? TelegramUserPresence, .singleItem)) + + + entries.append(.section(sectionId)) + sectionId += 1 + + entries.append(.description(sectionId, index, L10n.groupPermissionSectionTitle, .textTopItem)) + index += 1 + + let currentRightsFlags: TelegramChatBannedRightsFlags + if let updatedFlags = state.updatedFlags { + currentRightsFlags = updatedFlags + } else if let initialParticipant = initialParticipant, case let .member(_, _, _, maybeBanInfo, _) = initialParticipant, let banInfo = maybeBanInfo { + currentRightsFlags = banInfo.rights.flags + } else { + currentRightsFlags = defaultBannedRights.flags + } + + let currentTimeout: Int32 + if let updatedTimeout = state.updatedTimeout { + currentTimeout = updatedTimeout + } else if let initialParticipant = initialParticipant, case let .member(_, _, _, maybeBanInfo, _) = initialParticipant, let banInfo = maybeBanInfo { + currentTimeout = banInfo.rights.untilDate + } else { + currentTimeout = Int32.max + } + + let currentTimeoutString: String + if currentTimeout == 0 || currentTimeout == Int32.max { + currentTimeoutString = L10n.timerForever + } else { + let remainingTimeout = currentTimeout - state.referenceTimestamp + currentTimeoutString = timeIntervalString(Int(remainingTimeout)) + } - for restriction in restrictions { - entries.append(.rightItem(sectionId, index, restriction.1, restriction.0, currentRightsFlags, !currentRightsFlags.contains(restriction.0) && !currentRightsFlags.contains(.banReadMessages), peer.hasAdminRights(.canBanUsers))) - index += 1 - } - } - default: - break + + for right in allGroupPermissionList { + let defaultEnabled = !defaultBannedRights.flags.contains(right) + entries.append(.rightItem(sectionId, index, stringForGroupPermission(right: right), right, defaultEnabled && !currentRightsFlags.contains(right), defaultEnabled && !state.updating, bestGeneralViewType(allGroupPermissionList, for: right))) + index += 1 + } + + entries.append(.section(sectionId)) + sectionId += 1 + + entries.append(.timeout(sectionId, index, L10n.groupPermissionDuration, currentTimeoutString, .singleItem)) + index += 1 + + if let initialParticipant = initialParticipant, case let .member(member) = initialParticipant, let banInfo = member.banInfo, let initialBannedBy = initialBannedBy { + entries.append(.timeout(sectionId, index, L10n.groupPermissionDuration, currentTimeoutString, .firstItem)) + index += 1 + entries.append(.delete(sectionId, index, L10n.groupPermissionDelete, .lastItem)) + index += 1 + entries.append(.exceptionInfo(sectionId, index, L10n.groupPermissionAddedInfo(initialBannedBy.displayTitle, stringForRelativeSymbolicTimestamp(relativeTimestamp: banInfo.timestamp, relativeTo: state.referenceTimestamp)), .textBottomItem)) + index += 1 + } else { + entries.append(.timeout(sectionId, index, L10n.groupPermissionDuration, currentTimeoutString, .singleItem)) + index += 1 } } - - - - entries.append(.section(sectionId)) - sectionId += 1 - - entries.append(.blockFor(sectionId, index, state.until)) - index += 1 - + + entries.append(.section(sectionId)) sectionId += 1 return entries } -fileprivate func prepareTransition(left:[RestrictedEntry], right: [RestrictedEntry], initialSize:NSSize, arguments:RestrictedControllerArguments) -> TableUpdateTransition { +fileprivate func prepareTransition(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], initialSize:NSSize, arguments:RestrictedControllerArguments) -> TableUpdateTransition { let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in - return entry.item(arguments, initialSize: initialSize) + return entry.entry.item(arguments, initialSize: initialSize) } return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true) } class RestrictedModalViewController: TableModalViewController { - private let participant:RenderedChannelParticipant - private let account:Account + private let initialParticipant:ChannelParticipant? + private let context:AccountContext private let disposable = MetaDisposable() private let peerId:PeerId - private let stateValue:Atomic = Atomic(value: RestrictedControllerState()) - private let unban: Bool - private let updated:(TelegramChannelBannedRights)->Void - init(account:Account, peerId:PeerId, participant:RenderedChannelParticipant, unban: Bool, updated: @escaping(TelegramChannelBannedRights)->Void) { - self.participant = participant - self.account = account - self.unban = unban + private let memberId: PeerId + private let updated:(TelegramChatBannedRights)->Void + + private var okClicked:(()->Void)? + private var cancelClicked:(()->Void)? + + init(_ context: AccountContext, peerId:PeerId, memberId: PeerId, initialParticipant:ChannelParticipant?, updated: @escaping(TelegramChatBannedRights)->Void) { + self.initialParticipant = initialParticipant + self.context = context self.updated = updated self.peerId = peerId - super.init(frame: NSMakeRect(0, 0, 300, 360)) + self.memberId = memberId + super.init(frame: NSMakeRect(0, 0, 350, 360)) bar = .init(height : 0) } override func viewDidLoad() { super.viewDidLoad() - let participant = self.participant - let unban = self.unban - let stateValue = self.stateValue - let statePromise = ValuePromise(RestrictedControllerState(), ignoreRepeated: true) + let initialState = RestrictedControllerState(referenceTimestamp: Int32(Date().timeIntervalSince1970), updatedFlags: nil, updatedTimeout: nil, updating: false) + + genericView.getBackgroundColor = { + theme.colors.listBackground + } + + + let initialParticipant = self.initialParticipant + let memberId = self.memberId + let peerId = self.peerId + let context = self.context + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) let updateState: ((RestrictedControllerState) -> RestrictedControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } - updateState { current in - switch participant.participant { - case let .member(_, _, _, banInfo): - if let banInfo = banInfo { - return current.withUpdatedUpdatedFlags(banInfo.rights.flags).withUpdatedUntil(banInfo.rights.untilDate) - } - default: - break - } - return current - } + + let actionsDisposable = DisposableSet() - let arguments = RestrictedControllerArguments(account: account, toggleRight: { right, flags in - updateState { current in - var updated = flags - - let banDepencies = banRightDependencies(right) - let unbanDepencies = unbanRightDependencies(right) - - if flags == .banReadMessages { - updated = [] + let updateRightsDisposable = MetaDisposable() + actionsDisposable.add(updateRightsDisposable) + + + let peerView = Promise() + peerView.set(context.account.viewTracker.peerView(peerId)) + + + let arguments = RestrictedControllerArguments(context: context, toggleRight: { rights, value in + let _ = (peerView.get() + |> take(1) + |> deliverOnMainQueue).start(next: { view in - for depend in banDepencies { - updated.insert(depend) + var defaultBannedRightsFlagsValue: TelegramChatBannedRightsFlags? + guard let peer = view.peers[peerId] else { + return } - } else { - if flags.contains(right) { - updated.remove(right) - for depend in unbanDepencies { - if updated.contains(depend) { - updated.remove(depend) - } + if let channel = peer as? TelegramChannel, let initialRightFlags = channel.defaultBannedRights?.flags { + defaultBannedRightsFlagsValue = initialRightFlags + } else if let group = peer as? TelegramGroup, let initialRightFlags = group.defaultBannedRights?.flags { + defaultBannedRightsFlagsValue = initialRightFlags + } + guard let defaultBannedRightsFlags = defaultBannedRightsFlagsValue else { + return + } + updateState { state in + var state = state + var effectiveRightsFlags: TelegramChatBannedRightsFlags + if let updatedFlags = state.updatedFlags { + effectiveRightsFlags = updatedFlags + } else if let initialParticipant = initialParticipant, case let .member(member) = initialParticipant, let banInfo = member.banInfo { + effectiveRightsFlags = banInfo.rights.flags + } else { + effectiveRightsFlags = defaultBannedRightsFlags } - } else { - updated.insert(right) - for depend in banDepencies { - if !updated.contains(depend) { - updated.insert(depend) + if value { + effectiveRightsFlags.remove(rights) + effectiveRightsFlags = effectiveRightsFlags.subtracting(groupPermissionDependencies(rights)) + } else { + effectiveRightsFlags.insert(rights) + for right in allGroupPermissionList { + if groupPermissionDependencies(right).contains(rights) { + effectiveRightsFlags.insert(right) + } } } + state.updatedFlags = effectiveRightsFlags + return state } - } + }) + }, changeUntil: { [weak self] in + guard let `self` = self else {return} + + if let index = self.genericView.index(hash: RestrictedEntryStableId.timeout) { - return current.withUpdatedUpdatedFlags(updated) - } - }, changeUntil: { [weak self] in - if let strongSelf = self { - if let index = strongSelf.genericView.index(hash: RestrictedEntryStableId.blockFor) { - if let view = (strongSelf.genericView.viewNecessary(at: index) as? GeneralInteractedRowView)?.textView { - var items:[SPopoverItem] = [] - items.append(SPopoverItem(tr(.timerDaysCountable(1)), { - updateState { - $0.withUpdatedUntil(Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + 24 * 60 * 60)) - } - })) - items.append(SPopoverItem(tr(.timerWeeksCountable(1)), { - updateState { - $0.withUpdatedUntil(Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + 7 * 24 * 60 * 60)) - } - })) - items.append(SPopoverItem(tr(.timerMonthsCountable(1)), { - updateState { - $0.withUpdatedUntil(Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + 30 * 24 * 60 * 60)) - } - })) - items.append(SPopoverItem(tr(.channelBanForever), { - updateState { - $0.withUpdatedUntil(0) - } + let applyValue: (Int32?) -> Void = { value in + updateState { state in + var state = state + state.updatedTimeout = value + return state + } + } + + let intervals: [Int32] = [ + 1 * 60 * 60 * 24, + 7 * 60 * 60 * 24, + 30 * 60 * 60 * 24 + ] + if let view = (self.genericView.viewNecessary(at: index) as? GeneralInteractedRowView)?.textView { + var items:[SPopoverItem] = [] + for interval in intervals { + items.append(SPopoverItem(timeIntervalString(Int(interval)), { + applyValue(initialState.referenceTimestamp + interval) })) - showPopover(for: view, with: SPopoverViewController(items: items), edge: .maxX, inset: NSMakePoint(view.frame.width,-10)) } + items.append(SPopoverItem(tr(L10n.channelBanForever), { + applyValue(Int32.max) + })) + showPopover(for: view, with: SPopoverViewController(items: items), edge: .maxX, inset: NSMakePoint(view.frame.width,-10)) } } - + }, alertError: { [weak self] in + let _ = (peerView.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] view in + if let peer = peerViewMainPeer(view) { + self?.show(toaster: ControllerToaster(text: peer.isSupergroup || peer.isGroup ? L10n.channelExceptionDisabledOptionGroup : L10n.channelExceptionDisabledOptionChannel)) + } + }) + }, deleteException: { [weak self] in + self?.updated(TelegramChatBannedRights(flags: TelegramChatBannedRightsFlags(rawValue: 0), untilDate: 0)) + self?.close() }) + let previous:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) + let initialSize = self.atomicSize + + + var keys: [PostboxViewKey] = [.peer(peerId: peerId, components: .all), .peer(peerId: memberId, components: .all)] + if let banInfo = initialParticipant?.banInfo { + keys.append(.peer(peerId: banInfo.restrictedBy, components: [])) + } + let combinedView = context.account.postbox.combinedView(keys: keys) - let previous:Atomic<[RestrictedEntry]> = Atomic(value: []) - let initialSize = self.atomicSize - let signal:Signal<(TableUpdateTransition, PeerView), Void> = combineLatest(statePromise.get(), account.viewTracker.peerView(peerId)) |> deliverOn(prepareQueue) |> map { state, view in - return (RestrictedEntries(state: state, participant: participant, view: view), view) - } |> map { entries, view in - return (prepareTransition(left: previous.swap(entries), right: entries, initialSize: initialSize.modify{$0}, arguments: arguments), view) + let signal:Signal<(TableUpdateTransition, PeerView, PeerView), NoError> = combineLatest(queue: prepareQueue, appearanceSignal, statePromise.get(), combinedView) |> map { appearance, state, combinedView in + + let channelView = combinedView.views[.peer(peerId: peerId, components: .all)] as! PeerView + let memberView = combinedView.views[.peer(peerId: memberId, components: .all)] as! PeerView + var initialBannedByPeer: Peer? + if let banInfo = initialParticipant?.banInfo { + initialBannedByPeer = (combinedView.views[.peer(peerId: banInfo.restrictedBy, components: [])] as? PeerView)?.peers[banInfo.restrictedBy] + } + + let entries = restrictedEntries(state: state, accountPeerId: context.account.peerId, channelView: channelView, memberView: memberView, initialParticipant: initialParticipant, initialBannedBy: initialBannedByPeer).map {AppearanceWrapperEntry(entry: $0, appearance: appearance)} + + return (prepareTransition(left: previous.swap(entries), right: entries, initialSize: initialSize.with {$0}, arguments: arguments), channelView, memberView) } |> deliverOnMainQueue + let animated:Atomic = Atomic(value: false) - disposable.set(signal.start(next: { [weak self] transition, view in + disposable.set(signal.start(next: { [weak self] transition, channelView, memberView in self?.genericView.merge(with: transition) self?.updateSize(animated.swap(true)) self?.readyOnce() self?.modal?.interactions?.updateDone({ button in - if let peer = peerViewMainPeer(view) as? TelegramChannel { - button.isEnabled = peer.hasAdminRights(.canBanUsers) + if let peer = peerViewMainPeer(memberView) as? TelegramChannel { + button.isEnabled = peer.hasPermission(.banMembers) } }) - self?.modal?.interactions?.updateCancel({ button in - if unban { - button.set(text: tr(.channelBlacklistUnban), for: .Normal) + self?.modal?.interactions?.updateCancel({ [weak self] button in + if self?.genericView.item(stableId: RestrictedEntryStableId.exceptionInfo) != nil { + button.set(text: L10n.groupPermissionDelete, for: .Normal) button.set(color: theme.colors.redUI, for: .Normal) } else { button.set(text: "", for: .Normal) } }) + + + self?.okClicked = { [weak self] in + + let _ = (peerView.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] view in + var defaultBannedRightsFlagsValue: TelegramChatBannedRightsFlags? + guard let peer = view.peers[peerId] else { + return + } + if let channel = peer as? TelegramChannel, let initialRightFlags = channel.defaultBannedRights?.flags { + defaultBannedRightsFlagsValue = initialRightFlags + } else if let group = peer as? TelegramGroup, let initialRightFlags = group.defaultBannedRights?.flags { + defaultBannedRightsFlagsValue = initialRightFlags + } + guard let defaultBannedRightsFlags = defaultBannedRightsFlagsValue else { + return + } + + + var resolvedRights: TelegramChatBannedRights? + if let initialParticipant = initialParticipant { + var updateFlags: TelegramChatBannedRightsFlags? + var updateTimeout: Int32? + updateState { current in + updateFlags = current.updatedFlags + updateTimeout = current.updatedTimeout + return current + } + + if updateFlags == nil && updateTimeout == nil { + if case let .member(_, _, _, maybeBanInfo, _) = initialParticipant { + if maybeBanInfo == nil { + updateFlags = defaultBannedRightsFlags + updateTimeout = Int32.max + } + } + } + + if updateFlags != nil || updateTimeout != nil { + let currentRightsFlags: TelegramChatBannedRightsFlags + if let updatedFlags = updateFlags { + currentRightsFlags = updatedFlags + } else if case let .member(_, _, _, maybeBanInfo, _) = initialParticipant, let banInfo = maybeBanInfo { + currentRightsFlags = banInfo.rights.flags + } else { + currentRightsFlags = defaultBannedRightsFlags + } + + let currentTimeout: Int32 + if let updateTimeout = updateTimeout { + currentTimeout = updateTimeout + } else if case let .member(_, _, _, maybeBanInfo, _) = initialParticipant, let banInfo = maybeBanInfo { + currentTimeout = banInfo.rights.untilDate + } else { + currentTimeout = Int32.max + } + + resolvedRights = TelegramChatBannedRights(flags: completeRights(currentRightsFlags), untilDate: currentTimeout) + } + } else if let _ = channelView.peers[channelView.peerId] as? TelegramChannel { + var updateFlags: TelegramChatBannedRightsFlags? + var updateTimeout: Int32? + updateState { state in + var state = state + updateFlags = state.updatedFlags + updateTimeout = state.updatedTimeout + state.updating = false + return state + } + + if updateFlags == nil { + updateFlags = defaultBannedRightsFlags + } + if updateTimeout == nil { + updateTimeout = Int32.max + } + + if let updateFlags = updateFlags, let updateTimeout = updateTimeout { + resolvedRights = TelegramChatBannedRights(flags: completeRights(updateFlags), untilDate: updateTimeout) + } + } + + var previousRights: TelegramChatBannedRights? + if let initialParticipant = initialParticipant, case let .member(member) = initialParticipant, member.banInfo != nil { + previousRights = member.banInfo?.rights + } + + if let resolvedRights = resolvedRights, previousRights != resolvedRights { + let cleanResolvedRightsFlags = resolvedRights.flags.union(defaultBannedRightsFlags) + let cleanResolvedRights = TelegramChatBannedRights(flags: cleanResolvedRightsFlags, untilDate: resolvedRights.untilDate) + + if cleanResolvedRights.flags.isEmpty && previousRights == nil { + self?.close() + } else { + self?.updated(cleanResolvedRights) + } + + } + }) + + } })) - - } deinit { disposable.dispose() } + override var modalHeader: (left: ModalHeaderData?, center: ModalHeaderData?, right: ModalHeaderData?)? { + return (left: ModalHeaderData(image: theme.icons.modalClose, handler: { [weak self] in + self?.close() + }), center: ModalHeaderData(title: L10n.groupPermissionTitle), right: nil) + } + override var modalInteractions: ModalInteractions? { - return ModalInteractions(acceptTitle: tr(.modalOK), accept: { [weak self] in - if let strongSelf = self { - strongSelf.close() - switch strongSelf.participant.participant { - case let .member(_, _, _, banInfo): - if let banInfo = banInfo { - let state = strongSelf.stateValue.modify({$0}) - let flags = state.updatedFlags ?? banInfo.rights.flags - strongSelf.updated(TelegramChannelBannedRights(flags: flags, untilDate: state.until)) - } - default: - break - } - - } - }, cancelTitle: tr(.modalCancel), cancel: { [weak self] in + return ModalInteractions(acceptTitle: L10n.modalApply, accept: { [weak self] in self?.close() - self?.updated(TelegramChannelBannedRights(flags: [], untilDate: 0)) - }, drawBorder: true, height: 40) + self?.okClicked?() + }, drawBorder: true, height: 50, singleButton: true) } } diff --git a/Telegram-Mac/RingBuffer.h b/Telegram-Mac/RingBuffer.h new file mode 100755 index 0000000000..46f07f9dfb --- /dev/null +++ b/Telegram-Mac/RingBuffer.h @@ -0,0 +1,140 @@ +#import + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + + typedef struct { + void *buffer; + int32_t length; + int32_t tail; + int32_t head; + int32_t fillCount; + } TPCircularBuffer; + + /*! + * Initialise buffer + * + * Note that the length is advisory only: Because of the way the + * memory mirroring technique works, the true buffer length will + * be multiples of the device page size (e.g. 4096 bytes) + * + * If you intend to use the AudioBufferList utilities, you should + * always allocate a bit more space than you need for pure audio + * data, so there's room for the metadata. How much extra is required + * depends on how many AudioBufferList structures are used, which is + * a function of how many audio frames each buffer holds. A good rule + * of thumb is to add 15%, or at least another 2048 bytes or so. + * + * @param buffer Circular buffer + * @param length Length of buffer + */ + bool TPCircularBufferInit(TPCircularBuffer *buffer, int32_t length); + bool _TPCircularBufferInit(TPCircularBuffer *buffer, int32_t length, size_t structSize); + + /*! + * Cleanup buffer + * + * Releases buffer resources. + */ + void TPCircularBufferCleanup(TPCircularBuffer *buffer); + + /*! + * Clear buffer + * + * Resets buffer to original, empty state. + * + * This is safe for use by consumer while producer is accessing + * buffer. + */ + void TPCircularBufferClear(TPCircularBuffer *buffer); + + // Reading (consuming) + + /*! + * Access end of buffer + * + * This gives you a pointer to the end of the buffer, ready + * for reading, and the number of available bytes to read. + * + * @param buffer Circular buffer + * @param availableBytes On output, the number of bytes ready for reading + * @return Pointer to the first bytes ready for reading, or NULL if buffer is empty + */ + static __inline__ __attribute__((always_inline)) void* TPCircularBufferTail(TPCircularBuffer *buffer, int32_t* availableBytes) { + *availableBytes = buffer->fillCount; + if ( *availableBytes == 0 ) return NULL; + return (void*)((char*)buffer->buffer + buffer->tail); + } + + /*! + * Consume bytes in buffer + * + * This frees up the just-read bytes, ready for writing again. + * + * @param buffer Circular buffer + * @param amount Number of bytes to consume + */ + static __inline__ __attribute__((always_inline)) void TPCircularBufferConsume(TPCircularBuffer *buffer, int32_t amount) { + buffer->tail = (buffer->tail + amount) % buffer->length; + buffer->fillCount -= amount; + assert(buffer->fillCount >= 0); + } + + /*! + * Access front of buffer + * + * This gives you a pointer to the front of the buffer, ready + * for writing, and the number of available bytes to write. + * + * @param buffer Circular buffer + * @param availableBytes On output, the number of bytes ready for writing + * @return Pointer to the first bytes ready for writing, or NULL if buffer is full + */ + static __inline__ __attribute__((always_inline)) void* TPCircularBufferHead(TPCircularBuffer *buffer, int32_t* availableBytes) { + *availableBytes = (buffer->length - buffer->fillCount); + if ( *availableBytes == 0 ) return NULL; + return (void*)((char*)buffer->buffer + buffer->head); + } + + // Writing (producing) + + /*! + * Produce bytes in buffer + * + * This marks the given section of the buffer ready for reading. + * + * @param buffer Circular buffer + * @param amount Number of bytes to produce + */ + static __inline__ __attribute__((always_inline)) void TPCircularBufferProduce(TPCircularBuffer *buffer, int32_t amount) { + buffer->head = (buffer->head + amount) % buffer->length; + buffer->fillCount += amount; + assert(buffer->fillCount <= buffer->length); + } + + /*! + * Helper routine to copy bytes to buffer + * + * This copies the given bytes to the buffer, and marks them ready for reading. + * + * @param buffer Circular buffer + * @param src Source buffer + * @param len Number of bytes in source buffer + * @return true if bytes copied, false if there was insufficient space + */ + static __inline__ __attribute__((always_inline)) bool TPCircularBufferProduceBytes(TPCircularBuffer *buffer, const void* src, int32_t len) { + int32_t space; + void *ptr = TPCircularBufferHead(buffer, &space); + if ( space < len ) return false; + memcpy(ptr, src, len); + TPCircularBufferProduce(buffer, len); + return true; + } +#ifdef __cplusplus +} +#endif + diff --git a/Telegram-Mac/RingBuffer.m b/Telegram-Mac/RingBuffer.m new file mode 100755 index 0000000000..9c6db69b7a --- /dev/null +++ b/Telegram-Mac/RingBuffer.m @@ -0,0 +1,121 @@ +#import "RingBuffer.h" + +#include +#include +#include + +#define reportResult(result,operation) (_reportResult((result),(operation),strrchr(__FILE__, '/')+1,__LINE__)) +static inline bool _reportResult(kern_return_t result, const char *operation, const char* file, int line) { + if ( result != ERR_SUCCESS ) { + printf("%s:%d: %s: %s\n", file, line, operation, mach_error_string(result)); + return false; + } + return true; +} + +bool TPCircularBufferInit(TPCircularBuffer *buffer, int32_t length) { + return _TPCircularBufferInit(buffer, length, sizeof(TPCircularBuffer)); +} + +bool _TPCircularBufferInit(TPCircularBuffer *buffer, int32_t length, size_t structSize) { + + assert(length > 0); + + if ( structSize != sizeof(TPCircularBuffer) ) { + fprintf(stderr, "TPCircularBuffer: Header version mismatch. Check for old versions of TPCircularBuffer in your project\n"); + abort(); + } + + // Keep trying until we get our buffer, needed to handle race conditions + int retries = 3; + while ( true ) { + + buffer->length = (int32_t)round_page(length); // We need whole page sizes + + // Temporarily allocate twice the length, so we have the contiguous address space to + // support a second instance of the buffer directly after + vm_address_t bufferAddress; + kern_return_t result = vm_allocate(mach_task_self(), + &bufferAddress, + buffer->length * 2, + VM_FLAGS_ANYWHERE); // allocate anywhere it'll fit + if ( result != ERR_SUCCESS ) { + if ( retries-- == 0 ) { + reportResult(result, "Buffer allocation"); + return false; + } + // Try again if we fail + continue; + } + + // Now replace the second half of the allocation with a virtual copy of the first half. Deallocate the second half... + result = vm_deallocate(mach_task_self(), + bufferAddress + buffer->length, + buffer->length); + if ( result != ERR_SUCCESS ) { + if ( retries-- == 0 ) { + reportResult(result, "Buffer deallocation"); + return false; + } + // If this fails somehow, deallocate the whole region and try again + vm_deallocate(mach_task_self(), bufferAddress, buffer->length); + continue; + } + + // Re-map the buffer to the address space immediately after the buffer + vm_address_t virtualAddress = bufferAddress + buffer->length; + vm_prot_t cur_prot, max_prot; + result = vm_remap(mach_task_self(), + &virtualAddress, // mirror target + buffer->length, // size of mirror + 0, // auto alignment + 0, // force remapping to virtualAddress + mach_task_self(), // same task + bufferAddress, // mirror source + 0, // MAP READ-WRITE, NOT COPY + &cur_prot, // unused protection struct + &max_prot, // unused protection struct + VM_INHERIT_DEFAULT); + if ( result != ERR_SUCCESS ) { + if ( retries-- == 0 ) { + reportResult(result, "Remap buffer memory"); + return false; + } + // If this remap failed, we hit a race condition, so deallocate and try again + vm_deallocate(mach_task_self(), bufferAddress, buffer->length); + continue; + } + + if ( virtualAddress != bufferAddress+buffer->length ) { + // If the memory is not contiguous, clean up both allocated buffers and try again + if ( retries-- == 0 ) { + printf("Couldn't map buffer memory to end of buffer\n"); + return false; + } + + vm_deallocate(mach_task_self(), virtualAddress, buffer->length); + vm_deallocate(mach_task_self(), bufferAddress, buffer->length); + continue; + } + + buffer->buffer = (void*)bufferAddress; + buffer->fillCount = 0; + buffer->head = buffer->tail = 0; + + return true; + } + return false; +} + +void TPCircularBufferCleanup(TPCircularBuffer *buffer) { + vm_deallocate(mach_task_self(), (vm_address_t)buffer->buffer, buffer->length * 2); + memset(buffer, 0, sizeof(TPCircularBuffer)); +} + +void TPCircularBufferClear(TPCircularBuffer *buffer) { + int32_t fillCount; + if ( TPCircularBufferTail(buffer, &fillCount) ) { + TPCircularBufferConsume(buffer, fillCount); + } +} + diff --git a/Telegram-Mac/RingByteBuffer.swift b/Telegram-Mac/RingByteBuffer.swift new file mode 100755 index 0000000000..8ce25943d3 --- /dev/null +++ b/Telegram-Mac/RingByteBuffer.swift @@ -0,0 +1,69 @@ +import Foundation +import Darwin + +public final class RingByteBuffer { + public let size: Int + private var buffer: TPCircularBuffer + + public init(size: Int) { + self.size = size + self.buffer = TPCircularBuffer() + TPCircularBufferInit(&self.buffer, Int32(size)) + } + + deinit { + TPCircularBufferCleanup(&self.buffer) + } + + public func enqueue(data: Data) -> Bool { + return data.withUnsafeBytes { (bytes: UnsafePointer) -> Bool in + return TPCircularBufferProduceBytes(&self.buffer, UnsafeRawPointer(bytes), Int32(data.count)) + } + } + + public func enqueue(_ bytes: UnsafeRawPointer, count: Int) -> Bool { + return TPCircularBufferProduceBytes(&self.buffer, bytes, Int32(count)) + } + + public func withMutableHeadBytes(_ f: (UnsafeMutableRawPointer, Int) -> Int) { + var availableBytes: Int32 = 0 + let bytes = TPCircularBufferHead(&self.buffer, &availableBytes) + let enqueuedBytes = f(bytes!, Int(availableBytes)) + TPCircularBufferProduce(&self.buffer, Int32(enqueuedBytes)) + } + + public func dequeue(_ bytes: UnsafeMutableRawPointer, count: Int) -> Int { + var availableBytes: Int32 = 0 + let tail = TPCircularBufferTail(&self.buffer, &availableBytes) + + let copiedCount = min(count, Int(availableBytes)) + memcpy(bytes, tail, copiedCount) + + TPCircularBufferConsume(&self.buffer, Int32(copiedCount)) + + return copiedCount + } + + public func dequeue(count: Int) -> Data { + var availableBytes: Int32 = 0 + let tail = TPCircularBufferTail(&self.buffer, &availableBytes) + + let copiedCount = min(count, Int(availableBytes)) + let bytes = malloc(copiedCount)! + memcpy(bytes, tail, copiedCount) + + TPCircularBufferConsume(&self.buffer, Int32(copiedCount)) + + return Data(bytesNoCopy: bytes.assumingMemoryBound(to: UInt8.self), count: copiedCount, deallocator: .free) + } + + public func clear() { + TPCircularBufferClear(&self.buffer) + } + + public var availableBytes: Int { + var count: Int32 = 0 + TPCircularBufferTail(&self.buffer, &count) + return Int(count) + } +} diff --git a/Telegram-Mac/SImageView.swift b/Telegram-Mac/SImageView.swift new file mode 100644 index 0000000000..94debc373b --- /dev/null +++ b/Telegram-Mac/SImageView.swift @@ -0,0 +1,59 @@ +// +// SImageView.swift +// Telegram +// +// Created by keepcoder on 04/12/2017. +// Copyright © 2017 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +class SImageView: NSView { + + init() { + super.init(frame: NSZeroRect) + wantsLayer = true + } + + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required override init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + override func hitTest(_ point: NSPoint) -> NSView? { + return nil + } + + var data: (CGImage, NSEdgeInsets)? { + didSet { + if let image = data { + layer?.contentsScale = 2.0 + let imageSize = image.0.backingSize + let insets = image.1 + let halfPixelFudge: CGFloat = 0.49 + let otherPixelFudge: CGFloat = 0.02 + var contentsCenter: CGRect = NSMakeRect(0.0, 0.0, 1.0, 1.0); + if (insets.left > 0 || insets.right > 0) { + contentsCenter.origin.x = ((insets.left + halfPixelFudge) / imageSize.width); + contentsCenter.size.width = (imageSize.width - (insets.left + insets.right + 1.0) + otherPixelFudge) / imageSize.width; + } + if (insets.top > 0 || insets.bottom > 0) { + contentsCenter.origin.y = ((insets.top + halfPixelFudge) / imageSize.height); + contentsCenter.size.height = (imageSize.height - (insets.top + insets.bottom + 1.0) + otherPixelFudge) / imageSize.height; + } + self.layer?.contentsGravity = .resize; + self.layer?.contentsCenter = contentsCenter; + self.layer?.contents = image.0 + } else { + self.layer?.contents = nil + } + + } + } + + +} diff --git a/Telegram-Mac/SPopoverRowItem.swift b/Telegram-Mac/SPopoverRowItem.swift deleted file mode 100644 index c9d12f5ccd..0000000000 --- a/Telegram-Mac/SPopoverRowItem.swift +++ /dev/null @@ -1,132 +0,0 @@ -// -// SPopoverRowItem.swift -// Telegram-Mac -// -// Created by keepcoder on 28/09/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import TGUIKit -import SwiftSignalKitMac -class SPopoverRowItem: TableRowItem { - - override var height: CGFloat { - return 40 - } - - private var unique:Int64 - - override var stableId: AnyHashable { - return unique - } - - let iStyle:ControlStyle = ControlStyle(backgroundColor: theme.colors.blueSelect, highlightColor:.white) - - - // data - let image:CGImage? - let title:TextViewLayout - let activeTitle: TextViewLayout - let clickHandler:() -> Void - - override func viewClass() -> AnyClass { - return SPopoverRowView.self - } - let alignAsImage: Bool - init(_ initialSize:NSSize, image:CGImage? = nil, alignAsImage: Bool, title:String, textColor: NSColor, clickHandler:@escaping() ->Void = {}) { - self.image = image - self.alignAsImage = alignAsImage - self.title = TextViewLayout(.initialize(string: title, color: textColor, font: .normal(.title))) - self.activeTitle = TextViewLayout(.initialize(string: title, color: .white, font: .normal(.title))) - - self.title.measure(width: .greatestFiniteMagnitude) - self.activeTitle.measure(width: .greatestFiniteMagnitude) - self.clickHandler = clickHandler - unique = Int64(arc4random()) - super.init(initialSize) - } - -} - - -private class SPopoverRowView: TableRowView { - - var image:ImageView = ImageView() - - var overlay:OverlayControl = OverlayControl(); - - var text:TextView = TextView(); - - - required init(frame frameRect: NSRect) { - super.init(frame: frameRect) - self.addSubview(overlay) - self.addSubview(image) - - self.addSubview(text) - text.isSelectable = false - text.userInteractionEnabled = false - - overlay.set(handler: {[weak self] (state) in - self?.overlay.backgroundColor = theme.colors.blueSelect - if let item = self?.item as? SPopoverRowItem { - if let image = item.image { - self?.image.image = item.iStyle.highlight(image: image) - } - self?.text.backgroundColor = theme.colors.blueSelect - self?.text.update(item.activeTitle) - } - }, for: .Hover) - - overlay.set(handler: {[weak self] (state) in - self?.overlay.backgroundColor = theme.colors.background - if let item = self?.item as? SPopoverRowItem { - self?.image.image = item.image - self?.text.backgroundColor = theme.colors.background - self?.text.update(item.title) - } - }, for: .Normal) - } - - override func setFrameSize(_ newSize: NSSize) { - super.setFrameSize(newSize) - overlay.setFrameSize(newSize) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - - override func updateMouse() { - overlay.updateState() - } - - override func set(item:TableRowItem, animated:Bool = false) { - super.set(item: item, animated: animated) - - overlay.removeAllHandlers() - overlay.backgroundColor = theme.colors.background - text.backgroundColor = theme.colors.background - if let item = item as? SPopoverRowItem { - image.image = item.image - overlay.removeAllHandlers() - overlay.set(handler: {_ in - item.clickHandler() - }, for: .Click) - image.sizeToFit() - image.centerY(self, x: floorToScreenPixels((45 - image.frame.width) / 2)) - - text.update(item.title) - - if item.image != nil || item.alignAsImage { - text.centerY(self, x: 45) - } else { - text.center() - } - } - - } - -} diff --git a/Telegram-Mac/SPopoverViewController.swift b/Telegram-Mac/SPopoverViewController.swift deleted file mode 100644 index 618ad6e892..0000000000 --- a/Telegram-Mac/SPopoverViewController.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// SPopoverViewController.swift -// Telegram-Mac -// -// Created by keepcoder on 09/11/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import TGUIKit -import SwiftSignalKitMac - -struct SPopoverItem { - let title:String - let image:CGImage? - let textColor: NSColor - let handler:()->Void - init(_ title:String, _ handler:@escaping ()->Void, _ image:CGImage? = nil, _ textColor: NSColor = theme.colors.text) { - self.title = title - self.image = image - self.textColor = textColor - self.handler = handler - } -} - -class SPopoverViewController: GenericViewController { - private let items:[SPopoverRowItem] - private let disposable = MetaDisposable() - override func viewDidLoad() { - super.viewDidLoad() - - genericView.insert(items: items) - genericView.needUpdateVisibleAfterScroll = true - genericView.reloadData() - - readyOnce() - } - - init(items:[SPopoverItem], visibility:Int = 4) { - weak var controller:SPopoverViewController? - let alignAsImage = !items.filter({$0.image != nil}).isEmpty - self.items = items.map({ item in SPopoverRowItem(NSZeroSize, image: item.image, alignAsImage: alignAsImage, title: item.title, textColor: item.textColor, clickHandler: { - Queue.mainQueue().justDispatch { - controller?.popover?.hide() - - _ = (Signal.single(Void()) |> delay(0.15, queue: Queue.mainQueue())).start(next: { - item.handler() - }) - } - })}) - let width: CGFloat = self.items.max(by: {$0.title.layoutSize.width < $1.title.layoutSize.width})!.title.layoutSize.width - let height = min(visibility * 40 + 20, items.count * 40) - super.init(frame: NSMakeRect(0, 0, width + 45 + 18, CGFloat(height))) - bar = .init(height: 0) - controller = self - } - - deinit { - disposable.dispose() - } - - - override func viewWillAppear(_ animated: Bool) { - - } - - -} - - diff --git a/Telegram-Mac/SVideoController.swift b/Telegram-Mac/SVideoController.swift new file mode 100644 index 0000000000..2442a1695f --- /dev/null +++ b/Telegram-Mac/SVideoController.swift @@ -0,0 +1,486 @@ +// +// VideoStreamingTestModalController.swift +// Telegram +// +// Created by Mikhail Filimonov on 12/11/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import SwiftSignalKit +import Postbox +import IOKit.pwr_mgt + +extension MediaPlayerStatus { + func withUpdatedVolume(_ volume: Float) -> MediaPlayerStatus { + return MediaPlayerStatus(generationTimestamp: self.generationTimestamp, duration: self.duration, dimensions: self.dimensions, timestamp: self.timestamp, baseRate: self.baseRate, volume: volume, seekId: self.seekId, status: self.status) + } + func withUpdatedTimestamp(_ timestamp: Double) -> MediaPlayerStatus { + return MediaPlayerStatus(generationTimestamp: self.generationTimestamp, duration: self.duration, dimensions: self.dimensions, timestamp: timestamp, baseRate: self.baseRate, volume: self.volume, seekId: self.seekId, status: self.status) + } + func withUpdatedDuration(_ duration: Double) -> MediaPlayerStatus { + return MediaPlayerStatus(generationTimestamp: self.generationTimestamp, duration: duration, dimensions: self.dimensions, timestamp: self.timestamp, baseRate: self.baseRate, volume: self.volume, seekId: self.seekId, status: self.status) + } +} + +enum SVideoStyle { + case regular + case pictureInPicture +} + +class SVideoController: GenericViewController, PictureInPictureControl { + + + var style: SVideoStyle = .regular + private var fullScreenWindow: Window? + private var fullScreenRestoreState: (rect: NSRect, view: NSView)? + private let mediaPlayer: MediaPlayer + private let reference: FileMediaReference + private let statusDisposable = MetaDisposable() + private let bufferingDisposable = MetaDisposable() + private let hideOnIdleDisposable = MetaDisposable() + private let hideControlsDisposable = MetaDisposable() + private let postbox: Postbox + private var pictureInPicture: Bool = false + private var hideControls: ValuePromise = ValuePromise(true, ignoreRepeated: true) + private var controlsIsHidden: Bool = false + var togglePictureInPictureImpl:((Bool, PictureInPictureControl)->Void)? + + private var isPaused: Bool = true + private var forceHiddenControls: Bool = false + private var _videoFramePreview: MediaPlayerFramePreview? + private var videoFramePreview: MediaPlayerFramePreview { + if let videoFramePreview = _videoFramePreview { + return videoFramePreview + } else { + self._videoFramePreview = MediaPlayerFramePreview(postbox: postbox, fileReference: reference) + } + return _videoFramePreview! + } + + + private var scrubbingFrame = Promise(nil) + private var scrubbingFrames = false + private var scrubbingFrameDisposable: Disposable? + + + init(postbox: Postbox, reference: FileMediaReference, fetchAutomatically: Bool = false) { + self.reference = reference + self.postbox = postbox + mediaPlayer = MediaPlayer(postbox: postbox, reference: reference.resourceReference(reference.media.resource), streamable: reference.media.isStreamable, video: true, preferSoftwareDecoding: false, enableSound: true, volume: FastSettings.volumeRate, fetchAutomatically: fetchAutomatically) + super.init() + bar = .init(height: 0) + } + + var status: Signal { + return mediaPlayer.status + } + + func play(_ startTime: TimeInterval? = nil) { + mediaPlayer.play() + self.isPaused = false + if let startTime = startTime, startTime > 0 { + mediaPlayer.seek(timestamp: startTime) + } + } + + func playOrPause() { + self.isPaused = !self.isPaused + mediaPlayer.togglePlayPause() + if let status = genericView.status { + switch status.status { + case .buffering: + mediaPlayer.seek(timestamp: status.timestamp / status.duration) + default: + break + } + } + } + + func pause() { + self.isPaused = true + mediaPlayer.pause() + } + + func play() { + self.isPaused = false + self.play(nil) + } + + + func didEnter() { + + } + + func didExit() { + + } + + private func updateIdleTimer() { + NSCursor.unhide() + hideOnIdleDisposable.set((Signal.complete() |> delay(1.0, queue: Queue.mainQueue())).start(completed: { [weak self] in + guard let `self` = self else {return} + self.hideControls.set(true) + if !self.pictureInPicture, !self.isPaused { + NSCursor.hide() + } + })) + } + + private func updateControlVisibility(_ isMouseUpOrDown: Bool = false) { + updateIdleTimer() + if let rootView = genericView.superview?.superview { + var hide = !genericView._mouseInside() && !rootView.isHidden && (NSEvent.pressedMouseButtons & (1 << 0)) == 0 + if self.fullScreenWindow != nil && isMouseUpOrDown, !genericView.insideControls { + hide = true + if !self.isPaused { + NSCursor.hide() + } + } + hideControls.set(hide || forceHiddenControls) + } else { + hideControls.set(forceHiddenControls) + } + } + + + + private func setHandlersOn(window: Window) { + + updateIdleTimer() + + let mouseInsidePlayer = genericView.mediaPlayer.mouseInside() + + hideControls.set(!mouseInsidePlayer || forceHiddenControls) + + window.set(mouseHandler: { [weak self] (event) -> KeyHandlerResult in + if let window = self?.genericView.window, let contentView = window.contentView { + let point = contentView.convert(window.mouseLocationOutsideOfEventStream, from: nil) + if contentView.hitTest(point) != nil { + self?.updateControlVisibility() + } + } + return .rejected + }, with: self, for: .mouseMoved, priority: .modal) + + window.set(mouseHandler: { [weak self] (event) -> KeyHandlerResult in + if let window = self?.genericView.window, let contentView = window.contentView { + let point = contentView.convert(window.mouseLocationOutsideOfEventStream, from: nil) + if contentView.hitTest(point) != nil { + self?.updateControlVisibility() + } + } + return .rejected + }, with: self, for: .mouseExited, priority: .modal) + + window.set(mouseHandler: { [weak self] (event) -> KeyHandlerResult in + self?.updateIdleTimer() + + return .rejected + }, with: self, for: .leftMouseDragged, priority: .modal) + + window.set(mouseHandler: { [weak self] (event) -> KeyHandlerResult in + if let window = self?.genericView.window, let contentView = window.contentView { + let point = contentView.convert(window.mouseLocationOutsideOfEventStream, from: nil) + if contentView.hitTest(point) != nil { + self?.updateControlVisibility() + } + } + return .rejected + }, with: self, for: .mouseEntered, priority: .modal) + + window.set(mouseHandler: { [weak self] (event) -> KeyHandlerResult in + if let window = self?.genericView.window, self?.genericView.mediaPlayer.mouseInside() == true { + self?.updateControlVisibility(true) + } + return .rejected + }, with: self, for: .leftMouseDown, priority: .modal) + + window.set(mouseHandler: { [weak self] (event) -> KeyHandlerResult in + guard let `self` = self else {return .rejected} + if let window = self.genericView.window, let contentView = window.contentView { + let point = contentView.convert(window.mouseLocationOutsideOfEventStream, from: nil) + if contentView.hitTest(point) != nil { + self.updateControlVisibility(true) + } + } + self.genericView.subviews.last?.mouseUp(with: event) + return .rejected + }, with: self, for: .leftMouseUp, priority: .modal) + + } + + private var assertionID: IOPMAssertionID = 0 + private var success: IOReturn? + + private func disableScreenSleep() -> Bool? { + guard success == nil else { return nil } + success = IOPMAssertionCreateWithName( kIOPMAssertionTypeNoDisplaySleep as CFString, + IOPMAssertionLevel(kIOPMAssertionLevelOn), + "Video Playing" as CFString, + &assertionID ) + return success == kIOReturnSuccess + } + + private func enableScreenSleep() -> Bool { + if success != nil { + success = IOPMAssertionRelease(assertionID) + success = nil + return true + } + return false + } + + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if let window = window { + setHandlersOn(window: window) + } + + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + hideOnIdleDisposable.set(nil) + _ = enableScreenSleep() + NSCursor.unhide() + window?.removeAllHandlers(for: self) + } + + var isPictureInPicture: Bool { + return self.pictureInPicture + } + + + func hideControlsIfNeeded(_ forceHideControls: Bool = false) -> Bool { + self.forceHiddenControls = forceHideControls + if !controlsIsHidden { + hideControls.set(true) + return true + } + + return false + } + + func unhideControlsIfNeeded(_ forceUnhideControls: Bool = true) -> Bool { + forceHiddenControls = !forceUnhideControls + if controlsIsHidden { + hideControls.set(forceUnhideControls) + return true + } + return false + } + + override func viewDidLoad() { + super.viewDidLoad() + + genericView.layerContentsRedrawPolicy = .duringViewResize + + + mediaPlayer.attachPlayerView(genericView.mediaPlayer) + genericView.isStreamable = reference.media.isStreamable + hideControlsDisposable.set(hideControls.get().start(next: { [weak self] hide in + self?.genericView.hideControls(hide, animated: true) + self?.controlsIsHidden = hide + })) + + + let statusValue:Atomic = Atomic(value: nil) + let updateTemporaryStatus:(_ f: (MediaPlayerStatus?)->MediaPlayerStatus?) -> Void = { [weak self] f in + self?.genericView.status = statusValue.modify(f) + } + + let duration = Double(reference.media.duration ?? 0) + + statusDisposable.set((mediaPlayer.status |> deliverOnMainQueue).start(next: { [weak self] status in + let status = status.withUpdatedDuration(status.duration != 0 ? status.duration : duration) + switch status.status { + case .playing: + _ = self?.disableScreenSleep() + case let .buffering(_, whilePlaying): + if whilePlaying { + _ = self?.disableScreenSleep() + } else { + _ = self?.enableScreenSleep() + } + case .paused: + _ = self?.enableScreenSleep() + } + _ = statusValue.swap(status) + + self?.genericView.status = status + })) + let size = reference.media.resource.size ?? 0 + + let bufferingStatus = postbox.mediaBox.resourceRangesStatus(reference.media.resource) + |> map { ranges -> (IndexSet, Int) in + return (ranges, size) + } |> deliverOnMainQueue + + bufferingDisposable.set(bufferingStatus.start(next: { [weak self] bufferingStatus in + self?.genericView.bufferingStatus = bufferingStatus + })) + + self.scrubbingFrameDisposable = (self.scrubbingFrame.get() + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let `self` = self else { + return + } + if let result = result { + self.genericView.showScrubblerPreviewIfNeeded() + self.genericView.setCurrentScrubblingState(result) + } else { + self.genericView.hideScrubblerPreviewIfNeeded() + // empty image + } + }) + + + genericView.interactions = SVideoInteractions(playOrPause: { [weak self] in + self?.playOrPause() + }, rewind: { [weak self] timestamp in + guard let `self` = self else { return } + self.mediaPlayer.seek(timestamp: timestamp) + }, scrobbling: { [weak self] timecode in + guard let `self` = self else { return } + + if let timecode = timecode { + if !self.scrubbingFrames { + self.scrubbingFrames = true + self.scrubbingFrame.set(self.videoFramePreview.generatedFrames + |> map(Optional.init)) + } + self.videoFramePreview.generateFrame(at: timecode) + } else { + self.scrubbingFrame.set(.single(nil)) + self.videoFramePreview.cancelPendingFrames() + self.scrubbingFrames = false + } + }, volume: { [weak self] value in + self?.mediaPlayer.setVolume(value) + FastSettings.setVolumeRate(value) + updateTemporaryStatus { status in + return status?.withUpdatedVolume(value) + } + }, toggleFullScreen: { [weak self] in + self?.toggleFullScreen() + }, togglePictureInPicture: { [weak self] in + self?.togglePictureInPicture() + }, closePictureInPicture: { + closePipVideo() + }) + + if let duration = reference.media.duration, duration < 30 { + mediaPlayer.actionAtEnd = .loop({ [weak self] in + Queue.mainQueue().async { + self?.updateIdleTimer() + } + }) + } else { + mediaPlayer.actionAtEnd = .action { [weak self] in + Queue.mainQueue().async { + self?.mediaPlayer.seek(timestamp: 0) + self?.mediaPlayer.pause() + self?.updateIdleTimer() + self?.hideControls.set(false) + } + } + } + + readyOnce() + } + + func togglePictureInPicture() { + if let function = togglePictureInPictureImpl { + if fullScreenRestoreState != nil { + toggleFullScreen() + } + self.pictureInPicture = !pictureInPicture + window?.removeAllHandlers(for: self) + function(pictureInPicture, self) + if let window = view.window?.contentView?.window as? Window { + setHandlersOn(window: window) + } + + genericView.set(isInPictureInPicture: pictureInPicture) + } + } + + func togglePlayerOrPause() { + playOrPause() + } + + + func rewindBackward() { + genericView.rewindBackward() + } + func rewindForward() { + genericView.rewindForward() + } + + var isFullscreen: Bool { + return self.fullScreenRestoreState != nil + } + + func toggleFullScreen() { + if let screen = NSScreen.main { + if let window = fullScreenWindow, let state = fullScreenRestoreState { + + + + window.setFrame(NSMakeRect(screen.frame.minX + state.rect.minX, screen.frame.minY + screen.frame.height - state.rect.maxY, state.rect.width, state.rect.height), display: true, animate: true) + window.orderOut(nil) + view.frame = state.rect + state.view.addSubview(view) + + genericView.set(isInFullScreen: false) + genericView.mediaPlayer.setVideoLayerGravity(.resizeAspectFill) + + + window.removeAllHandlers(for: self) + if let window = self.window { + setHandlersOn(window: window) + } + + self.fullScreenWindow = nil + self.fullScreenRestoreState = nil + } else { + + genericView.mediaPlayer.setVideoLayerGravity(.resizeAspect) + + + fullScreenRestoreState = (rect: view.frame, view: view.superview!) + fullScreenWindow = Window(contentRect: NSMakeRect(view.frame.minX, screen.frame.height - view.frame.maxY, view.frame.width, view.frame.height), styleMask: [.fullSizeContentView, .borderless], backing: .buffered, defer: true, screen: screen) + + setHandlersOn(window: fullScreenWindow!) + window?.removeAllHandlers(for: self) + + + fullScreenWindow?.isOpaque = true + fullScreenWindow?.hasShadow = false + fullScreenWindow?.level = .screenSaver + self.view.frame = self.view.bounds + fullScreenWindow?.contentView?.addSubview(self.view) + fullScreenWindow?.orderFront(nil) + genericView.set(isInFullScreen: true) + fullScreenWindow?.becomeKey() + fullScreenWindow?.setFrame(screen.frame, display: true, animate: true) + } + } + } + + deinit { + statusDisposable.dispose() + bufferingDisposable.dispose() + hideOnIdleDisposable.dispose() + hideControlsDisposable.dispose() + _ = IOPMAssertionRelease(assertionID) + NSCursor.unhide() + } + +} diff --git a/Telegram-Mac/SVideoView.swift b/Telegram-Mac/SVideoView.swift new file mode 100644 index 0000000000..9aefe770c6 --- /dev/null +++ b/Telegram-Mac/SVideoView.swift @@ -0,0 +1,789 @@ +// +// SVideoView.swift +// Telegram +// +// Created by Mikhail Filimonov on 12/11/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit + +enum SVideoControlsStyle : Equatable { + case regular(pip: Bool, fullScreen: Bool, hideRewind: Bool) + case compact(pip: Bool, fullScreen: Bool, hideRewind: Bool) + + func withUpdatedPip(_ pip: Bool) -> SVideoControlsStyle { + switch self { + case let .regular(_, fullScreen, hideRewind): + return .regular(pip: pip, fullScreen: fullScreen, hideRewind: hideRewind) + case let .compact(_, fullScreen, hideRewind): + return .compact(pip: pip, fullScreen: fullScreen, hideRewind: hideRewind) + } + } + + func withUpdatedFullScreen(_ fullScreen: Bool) -> SVideoControlsStyle { + switch self { + case let .regular(pip, _, hideRewind): + return .regular(pip: pip, fullScreen: fullScreen, hideRewind: hideRewind) + case let .compact(pip, _, hideRewind): + return .compact(pip: pip, fullScreen: fullScreen, hideRewind: hideRewind) + } + } + + func withUpdatedStyle(compact: Bool) -> SVideoControlsStyle { + switch self { + case let .regular(pip, fullScreen, hideRewind), let .compact(pip, fullScreen, hideRewind): + return compact ? .compact(pip: pip, fullScreen: fullScreen, hideRewind: hideRewind) : .regular(pip: pip, fullScreen: fullScreen, hideRewind: hideRewind) + } + } + func withUpdatedHideRewind(hideRewind: Bool) -> SVideoControlsStyle { + switch self { + case let .regular(pip, fullScreen, _): + return .regular(pip: pip, fullScreen: fullScreen, hideRewind: hideRewind) + case let .compact(pip, fullScreen, _): + return .compact(pip: pip, fullScreen: fullScreen, hideRewind: hideRewind) + } + } + + var isPip: Bool { + switch self { + case let .regular(pip, _, _), let .compact(pip, _, _): + return pip + } + } + var isFullScreen: Bool { + switch self { + case let .regular(_, fullScreen, _), let .compact(_, fullScreen, _): + return fullScreen + } + } + var hideRewind: Bool { + switch self { + case let .regular(_, _, hideRewind), let .compact(_, _, hideRewind): + return hideRewind + } + } + + var isCompact: Bool { + switch self { + case .compact: + return true + case .regular: + return false + } + } +} + + +final class SVideoInteractions { + let playOrPause: ()->Void + let rewind:(Double)->Void + let scrobbling:(Double?)->Void + let volume:(Float) -> Void + let toggleFullScreen:() -> Void + let togglePictureInPicture: ()->Void + let closePictureInPicture: ()->Void + init(playOrPause: @escaping()->Void, rewind: @escaping(Double)->Void, scrobbling: @escaping(Double?)->Void, volume: @escaping(Float) -> Void, toggleFullScreen: @escaping()->Void, togglePictureInPicture: @escaping() -> Void, closePictureInPicture:@escaping()->Void) { + self.playOrPause = playOrPause + self.rewind = rewind + self.scrobbling = scrobbling + self.volume = volume + self.toggleFullScreen = toggleFullScreen + self.togglePictureInPicture = togglePictureInPicture + self.closePictureInPicture = closePictureInPicture + } +} + +private final class SVideoControlsView : Control { + + var bufferingRanges:[Range] = [] { + didSet { + progress.set(fetchingProgressRanges: bufferingRanges, animated: oldValue != bufferingRanges) + } + } + + var scrubberInsideBuffering: Bool { + for range in bufferingRanges { + if range.contains(progress.currentValue) { + return true + } + } + return bufferingRanges.isEmpty + } + + var controlStyle: SVideoControlsStyle = .regular(pip: false, fullScreen: false, hideRewind: false) { + didSet { + rewindBackward.isHidden = controlStyle.hideRewind + rewindForward.isHidden = controlStyle.hideRewind + volumeContainer.isHidden = controlStyle.isCompact + togglePip.set(image: controlStyle.isPip ? theme.icons.videoPlayerPIPOut : theme.icons.videoPlayerPIPIn, for: .Normal) + toggleFullscreen.set(image: controlStyle.isPip ? theme.icons.videoPlayerClose : controlStyle.isFullScreen ? theme.icons.videoPlayerExitFullScreen : theme.icons.videoPlayerEnterFullScreen, for: .Normal) + layout() + } + } + + override func mouseUp(with event: NSEvent) { + if progress.hasTemporaryState { + progress.mouseUp(with: event) + } else if volumeSlider.hasTemporaryState { + volumeSlider.mouseUp(with: event) + } else { + let point = self.convert(event.locationInWindow, from: nil) + let rect = NSMakeRect(self.progress.frame.minX, self.progress.frame.minY - 5, self.progress.frame.width, self.progress.frame.height + 10) + if NSPointInRect(point, rect) { + progress.mouseUp(with: event) + } else { + super.mouseUp(with: event) + } + } + } + + private func updateLivePreview() { + guard let window = window else { + return + } + let point = self.convert(window.mouseLocationOutsideOfEventStream, from: nil) + + let rect = NSMakeRect(self.progress.frame.minX, self.progress.frame.minY - 5, self.progress.frame.width, self.progress.frame.height + 10) + + if NSPointInRect(point, rect) || self.progress.hasTemporaryState { + let point = self.progress.convert(window.mouseLocationOutsideOfEventStream, from: nil) + let result = max(min(point.x, self.progress.frame.width), 0) / self.progress.frame.width + self.livePreview?(Float(result)) + } else { + self.livePreview?(nil) + } + } + + override func mouseMoved(with event: NSEvent) { + super.mouseMoved(with: event) + updateLivePreview() + + } + + override func mouseEntered(with event: NSEvent) { + super.mouseEntered(with: event) + updateLivePreview() + } + + override func mouseExited(with event: NSEvent) { + super.mouseExited(with: event) + updateLivePreview() + } + + + override var mouseDownCanMoveWindow: Bool { + return false + } + + + fileprivate func update(with status: MediaPlayerStatus, animated: Bool) { + volumeSlider.set(progress: CGFloat(status.volume)) + volumeToggle.set(image: status.volume.isZero ? theme.icons.videoPlayerVolumeOff : theme.icons.videoPlayerVolume, for: .Normal) + + rewindForward.isEnabled = status.duration > 30 && !status.generationTimestamp.isZero + rewindBackward.isEnabled = status.duration > 30 && !status.generationTimestamp.isZero + rewindForward.layer?.opacity = rewindForward.isEnabled ? 1.0 : 0.3 + rewindBackward.layer?.opacity = rewindForward.isEnabled ? 1.0 : 0.3 + + playOrPause.isEnabled = status.duration > 0 + progress.isEnabled = status.duration > 0 + + + switch status.status { + case .playing: + playOrPause.set(image: theme.icons.videoPlayerPause, for: .Normal) + progress.set(progress: status.duration == 0 ? 0 : CGFloat(status.timestamp / status.duration), animated: animated, duration: status.duration, beginTime: status.generationTimestamp, offset: status.timestamp, speed: Float(status.baseRate)) + case .paused: + playOrPause.set(image: status.generationTimestamp == 0 ? theme.icons.videoPlayerPause : theme.icons.videoPlayerPlay, for: .Normal) + progress.set(progress: status.duration == 0 ? 0 : CGFloat(status.timestamp / status.duration), animated: false) + case let .buffering(_, whilePlaying): + playOrPause.set(image: whilePlaying ? theme.icons.videoPlayerPause : theme.icons.videoPlayerPlay, for: .Normal) + progress.set(progress: status.duration == 0 ? 0 : CGFloat(status.timestamp / status.duration), animated: false) + } + let currentTimeAttr: NSAttributedString = .initialize(string: status.timestamp == 0 && status.duration == 0 ? "--:--" : String.durationTransformed(elapsed: Int(status.timestamp)), color: .white, font: .medium(11)) + let durationTimeAttr: NSAttributedString = .initialize(string: status.duration == 0 ? "--:--" : String.durationTransformed(elapsed: Int(status.duration)), color: .white, font: .medium(11)) + + let currentTimeLayout = TextViewLayout(currentTimeAttr, alignment: .right) + let durationLayout = TextViewLayout(durationTimeAttr, alignment: .center) + currentTimeLayout.measure(width: .greatestFiniteMagnitude) + durationLayout.measure(width: .greatestFiniteMagnitude) + + currentTimeView.setFrameSize(currentTimeLayout.layoutSize.width, currentTimeView.frame.height) + durationView.setFrameSize(durationLayout.layoutSize.width > 33 ? 40 : 33, durationView.frame.height) + + + currentTimeView.set(layout: currentTimeLayout) + durationView.set(layout: durationLayout) + + currentTimeView.needsDisplay = true + durationView.needsDisplay = true + } + + var status: MediaPlayerStatus? { + didSet { + if let status = status { + let animated = oldValue?.seekId == status.seekId && (oldValue?.timestamp ?? 0) <= status.timestamp && !status.generationTimestamp.isZero && status != oldValue + update(with: status, animated: animated) + } else { + rewindForward.isEnabled = false + rewindBackward.isEnabled = false + playOrPause.isEnabled = false + } + } + } + + let backgroundView: NSVisualEffectView = NSVisualEffectView() + let playOrPause: ImageButton = ImageButton() + let progress: LinearProgressControl = LinearProgressControl(progressHeight: 5) + let rewindForward: ImageButton = ImageButton() + let rewindBackward: ImageButton = ImageButton() + let toggleFullscreen: ImageButton = ImageButton() + let togglePip: ImageButton = ImageButton() + + var livePreview: ((Float?)->Void)? + + let volumeContainer: View = View() + let volumeToggle: ImageButton = ImageButton() + let volumeSlider: LinearProgressControl = LinearProgressControl(progressHeight: 5) + + private let durationView: TextView = TextView() + private let currentTimeView: TextView = TextView() + + private var controlMovePosition: NSPoint? = nil + + + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(backgroundView) + addSubview(playOrPause) + addSubview(progress) + addSubview(rewindForward) + addSubview(rewindBackward) + addSubview(toggleFullscreen) + addSubview(togglePip) + addSubview(durationView) + addSubview(currentTimeView) + + togglePip.hideAnimated = true + + + + durationView.setFrameSize(33, 13) + currentTimeView.setFrameSize(33, 13) + + durationView.userInteractionEnabled = false + durationView.isSelectable = false + durationView.backgroundColor = .clear + + currentTimeView.userInteractionEnabled = false + currentTimeView.isSelectable = false + currentTimeView.backgroundColor = .clear + + + volumeContainer.addSubview(volumeToggle) + volumeContainer.addSubview(volumeSlider) + + volumeToggle.autohighlight = false + volumeToggle.set(image: theme.icons.videoPlayerVolume, for: .Normal) + _ = volumeToggle.sizeToFit() + volumeSlider.setFrameSize(NSMakeSize(60, 12)) + volumeContainer.setFrameSize(NSMakeSize(volumeToggle.frame.width + 60 + 16, volumeToggle.frame.height)) + + volumeSlider.scrubberImage = generateImage(NSMakeSize(8, 8), contextGenerator: { size, ctx in + let rect = CGRect(origin: .zero, size: size) + ctx.clear(rect) + ctx.setFillColor(NSColor.white.cgColor) + ctx.fillEllipse(in: rect) + }) + volumeSlider.roundCorners = true + volumeSlider.alignment = .center + volumeSlider.containerBackground = NSColor.grayBackground.withAlphaComponent(0.2) + volumeSlider.style = ControlStyle(foregroundColor: .white, backgroundColor: .clear, highlightColor: .clear) + volumeSlider.set(progress: 0.8) + + volumeSlider.insets = NSEdgeInsetsMake(0, 4.5, 0, 4.5) + + addSubview(volumeContainer) + + backgroundView.material = .dark + backgroundView.blendingMode = .withinWindow + + playOrPause.autohighlight = false + rewindForward.autohighlight = false + rewindBackward.autohighlight = false + toggleFullscreen.autohighlight = false + togglePip.autohighlight = false + + + rewindForward.set(image: theme.icons.videoPlayerRewind15Forward, for: .Normal) + rewindBackward.set(image: theme.icons.videoPlayerRewind15Backward, for: .Normal) + + playOrPause.set(image: theme.icons.videoPlayerPause, for: .Normal) + + toggleFullscreen.set(image: theme.icons.videoPlayerEnterFullScreen, for: .Normal) + togglePip.set(image: theme.icons.videoPlayerPIPIn, for: .Normal) + + + _ = rewindForward.sizeToFit() + _ = rewindBackward.sizeToFit() + _ = playOrPause.sizeToFit() + _ = toggleFullscreen.sizeToFit() + _ = togglePip.sizeToFit() + + progress.insets = NSEdgeInsetsMake(0, 4.5, 0, 4.5) + progress.scrubberImage = generateImage(NSMakeSize(8, 8), contextGenerator: { size, ctx in + let rect = CGRect(origin: .zero, size: size) + ctx.clear(rect) + ctx.setFillColor(NSColor.white.cgColor) + ctx.fillEllipse(in: rect) + }) + progress.roundCorners = true + progress.alignment = .center + progress.liveScrobbling = false + progress.fetchingColor = NSColor.grayBackground.withAlphaComponent(0.6) + progress.containerBackground = NSColor.grayBackground.withAlphaComponent(0.2) + progress.style = ControlStyle(foregroundColor: .white, backgroundColor: .clear, highlightColor: .clear) + progress.set(progress: 0, animated: false, duration: 0) + wantsLayer = true + layer?.cornerRadius = 15 + + + self.progress.onLiveScrobbling = { [weak self] _ in + if let `self` = self, !self.progress.mouseInside() { + self.updateLivePreview() + } + } + + set(handler: { [weak self] control in + guard let window = control.window, let superview = control.superview else { + return + } + self?.controlMovePosition = superview.convert(window.mouseLocationOutsideOfEventStream, from: nil) + }, for: .Down) + + + + set(handler: { [weak self] control in + guard let window = control.window, let superview = control.superview, let start = self?.controlMovePosition else { + return + } + var mouse = superview.convert(window.mouseLocationOutsideOfEventStream, from: nil) + + var dif = NSMakePoint(mouse.x - start.x, mouse.y - start.y) + var point = NSMakePoint(control.frame.minX + dif.x, control.frame.minY + dif.y) + + if point.x < 2 || point.x > superview.frame.width - control.frame.width - 4 { + mouse.x = start.x + } + if point.y < 2 || point.y > superview.frame.height - control.frame.height - 4 { + mouse.y = start.y + } + self?.controlMovePosition = mouse + + dif = NSMakePoint(mouse.x - start.x, mouse.y - start.y) + point = NSMakePoint(control.frame.minX + dif.x, control.frame.minY + dif.y) + control.setFrameOrigin(point) + + + }, for: .MouseDragging) + } + + override var isFlipped: Bool { + return true + } + + override func viewWillMove(toWindow newWindow: NSWindow?) { + if let window = newWindow as? Window { + window.set(mouseHandler: { [weak self] event -> KeyHandlerResult in + self?.controlMovePosition = nil + return .rejected + }, with: self, for: .leftMouseUp) + } else { + (window as? Window)?.remove(object: self, for: .leftMouseUp) + } + } + + override func setFrameSize(_ newSize: NSSize) { + super.setFrameSize(newSize) + } + + override func layout() { + super.layout() + backgroundView.frame = bounds + + playOrPause.centerX(y: 16) + + rewindBackward.setFrameOrigin(playOrPause.frame.minX - rewindBackward.frame.width - 36, 16) + rewindForward.setFrameOrigin(playOrPause.frame.maxX + 36, 16) + + toggleFullscreen.setFrameOrigin(frame.width - toggleFullscreen.frame.width - 16, 16) + + switch controlStyle { + case .compact: + togglePip.setFrameOrigin(16, 16) + case .regular: + togglePip.setFrameOrigin(toggleFullscreen.frame.minX - togglePip.frame.width - 24, 16) + } + + volumeContainer.setFrameOrigin(16, 16) + volumeToggle.centerY(x: 0) + volumeSlider.centerY(x: volumeToggle.frame.maxX + 16) + + + switch controlStyle { + case .compact: + progress.setFrameOrigin(16 + currentTimeView.frame.width + 16, frame.height - 20 - progress.frame.height + (progress.frame.height - progress.progressHeight) / 2) + case .regular: + progress.setFrameOrigin(volumeContainer.frame.minX + volumeSlider.frame.minX, frame.height - 20 - progress.frame.height + (progress.frame.height - progress.progressHeight) / 2) + } + progress.setFrameSize(NSMakeSize(frame.width - progress.frame.origin.x - 16 - 16 - durationView.frame.width, 12)) + + currentTimeView.setFrameOrigin(16, progress.frame.minY) + durationView.setFrameOrigin(frame.width - durationView.frame.width - 16, progress.frame.minY) + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private final class PreviewView : View { + fileprivate let imageView: ImageView = ImageView() + fileprivate let duration: TextView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(imageView) + addSubview(duration) + background = .black + duration.background = darkPalette.grayBackground.withAlphaComponent(0.85) + duration.disableBackgroundDrawing = true + duration.layer?.cornerRadius = 2 + } + + override func layout() { + super.layout() + self.imageView.frame = bounds + self.duration.centerX(y: frame.height - self.duration.frame.height + 1) + } + + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + +class SVideoView: NSView { + + var initialedSize: NSSize = NSZeroSize + + var controlsStyle:SVideoControlsStyle = .regular(pip: false, fullScreen: false, hideRewind: false) { + didSet { + if oldValue != controlsStyle { + controls.controlStyle = controlsStyle + + if let status = status { + self.controls.update(with: status, animated: false) + self.controls.update(with: status, animated: true) + } + let bufferingStatus = self.bufferingStatus + self.bufferingStatus = bufferingStatus + } + } + } + private let bufferingIndicatorValueDisposable = MetaDisposable() + let bufferingIndicatorValue: Promise = Promise(false) + + var interactions: SVideoInteractions? + + var isStreamable: Bool = true + + private var previewView: PreviewView? + + var status: MediaPlayerStatus? = nil { + didSet { + if status != oldValue { + controls.status = status + if let status = status { + switch status.status { + case .buffering: + bufferingIndicatorValue.set(.single(!isStreamable) |> delay(0.2, queue: Queue.mainQueue())) + default: + bufferingIndicatorValue.set(.single(true)) + } + } else { + bufferingIndicatorValue.set(.single(!isStreamable)) + } + } + + } + } + var bufferingStatus: (IndexSet, Int)? { + didSet { + if let ranges = bufferingStatus { + var bufRanges: [Range] = [] + for range in ranges.0.rangeView { + let low = CGFloat(range.lowerBound) / CGFloat(ranges.1) + let high = CGFloat(range.upperBound) / CGFloat(ranges.1) + let br: Range = Range(uncheckedBounds: (lower: low, upper: high)) + bufRanges.append(br) + } + controls.bufferingRanges = bufRanges + } else { + controls.bufferingRanges = [Range(uncheckedBounds: (lower: -1, upper: -1))] + } + } + } + private let bufferingIndicator: ProgressIndicator = ProgressIndicator(frame: NSMakeRect(0, 0, 40, 40)) + private let controls: SVideoControlsView = SVideoControlsView(frame: NSZeroRect) + let mediaPlayer: MediaPlayerView = MediaPlayerView() + private let backgroundView: NSView = NSView() + override func layout() { + super.layout() + let oldSize = mediaPlayer.frame.size + mediaPlayer.frame = bounds + mediaPlayer.updateLayout() + let previousIsCompact: Bool = self.controlsStyle.isCompact + self.controlsStyle = self.controlsStyle.withUpdatedStyle(compact: frame.width < 300).withUpdatedHideRewind(hideRewind: frame.width < 400) + controls.setFrameSize(self.controlsStyle.isCompact ? 220 : min(frame.width - 10, 510), 94) + let bufferingStatus = self.bufferingStatus + self.bufferingStatus = bufferingStatus + if controls.frame.origin == .zero || previousIsCompact != self.controlsStyle.isCompact { + controls.centerX(y: frame.height - controls.frame.height - 24) + } else if oldSize != frame.size { + let dif = oldSize - frame.size + var point = NSMakePoint(controls.frame.minX - dif.width / 2, controls.frame.minY - dif.height / 2) + point.x = min(max(2, point.x), frame.width - controls.frame.width - 4) + point.y = min(max(2, point.y), frame.height - controls.frame.height - 4) + + controls.setFrameOrigin(point) + } + bufferingIndicator.center() + bufferingIndicator.progressColor = .white + backgroundView.frame = bounds + + } + + override var mouseDownCanMoveWindow: Bool { + return true + } + + func hideControls(_ hide: Bool, animated: Bool) { + if !hide { + controls.isHidden = false + } + if hide { + self.hideScrubblerPreviewIfNeeded() + } + + controls._change(opacity: hide ? 0 : 1, animated: animated, duration: 0.2, timingFunction: .linear, completion: { [weak self] completed in + if completed { + self?.controls.isHidden = hide + } + }) + } + + override var isOpaque: Bool { + return true + } + + override func setFrameSize(_ newSize: NSSize) { + super.setFrameSize(newSize) + if initialedSize == NSZeroSize { + self.initialedSize = newSize + } + } + + override func mouseUp(with event: NSEvent) { + let point = self.convert(event.locationInWindow, from: nil) + if !NSPointInRect(point, controls.frame) { + super.mouseUp(with: event) + } + } + + override func mouseMoved(with event: NSEvent) { + super.mouseMoved(with: event) + } + + + var insideControls: Bool { + guard let window = window else {return false} + let point = self.convert(window.mouseLocationOutsideOfEventStream, from: nil) + return NSPointInRect(point, controls.frame) && !controls.isHidden + } + + private func updateLayout() { + + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var isFlipped: Bool { + return true + } + + func rewindBackward() { + controls.rewindBackward.send(event: .Click) + } + func rewindForward() { + controls.rewindForward.send(event: .Click) + } + + required override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(backgroundView) + addSubview(mediaPlayer) + addSubview(bufferingIndicator) + addSubview(controls) + bufferingIndicator.innerInset = 8.0 + backgroundView.wantsLayer = true + backgroundView.background = .black + + bufferingIndicator.backgroundColor = .blackTransparent + bufferingIndicator.layer?.cornerRadius = 20 + + backgroundView.isHidden = true + + + controls.playOrPause.set(handler: { [weak self] _ in + self?.interactions?.playOrPause() + }, for: .Click) + + controls.livePreview = { [weak self] value in + guard let `self` = self else {return} + if let status = self.status { + self.interactions?.scrobbling(value != nil ? status.duration * Double(value!) : nil) + self.setCurrentScrubblingState(self.currentPreviewState) + } + } + + controls.progress.onUserChanged = { [weak self] value in + guard let `self` = self else {return} + if let status = self.status { + let result = min(status.duration * Double(value), status.duration) + self.status = status.withUpdatedTimestamp(result) + self.interactions?.rewind(result) + } + } + + controls.volumeSlider.onUserChanged = { [weak self] value in + guard let `self` = self else {return} + self.interactions?.volume(value) + } + controls.volumeToggle.set(handler: { [weak self] _ in + guard let `self` = self else {return} + if let status = self.status { + self.interactions?.volume(status.volume == 0 ? 0.8 : 0) + } + }, for: .Click) + + controls.rewindForward.set(handler: { [weak self] _ in + guard let `self` = self else {return} + if let status = self.status { + self.interactions?.rewind(min(status.timestamp + 15, status.duration)) + } + }, for: .Click) + + controls.rewindBackward.set(handler: { [weak self] _ in + guard let `self` = self else {return} + if let status = self.status { + self.interactions?.rewind(max(status.timestamp - 15, 0)) + } + }, for: .Click) + + controls.toggleFullscreen.set(handler: { [weak self] _ in + guard let `self` = self else {return} + if self.controlsStyle.isPip { + self.interactions?.closePictureInPicture() + } else { + self.interactions?.toggleFullScreen() + } + }, for: .Click) + + controls.togglePip.set(handler: { [weak self] _ in + self?.interactions?.togglePictureInPicture() + }, for: .Click) + + + bufferingIndicatorValueDisposable.set(bufferingIndicatorValue.get().start(next: { [weak self] isHidden in + self?.bufferingIndicator.isHidden = isHidden + })) + } + + deinit { + bufferingIndicatorValueDisposable.dispose() + } + + func set(isInPictureInPicture: Bool) { + self.controlsStyle = self.controlsStyle.withUpdatedPip(isInPictureInPicture) + } + + func set(isInFullScreen: Bool) { + self.controlsStyle = self.controlsStyle.withUpdatedFullScreen(isInFullScreen) + backgroundView.isHidden = !isInFullScreen + } + + func showScrubblerPreviewIfNeeded() { + if previewView == nil { + previewView = PreviewView(frame: NSZeroRect) + previewView?.background = .black + addSubview(previewView!) + } + previewView?.setFrameSize(initialedSize.aspectFitted(NSMakeSize(150, 150))) + } + + private var currentPreviewState: MediaPlayerFramePreviewResult? + + func setCurrentScrubblingState(_ state: MediaPlayerFramePreviewResult?) { + self.currentPreviewState = state + guard let previewView = self.previewView, let window = self.window, let status = self.status, !self.controls.isHidden else { + self.previewView?.removeFromSuperview() + self.previewView = nil + return + } + let point = self.controls.progress.convert(window.mouseLocationOutsideOfEventStream, from: nil) + + if let state = currentPreviewState { + switch state { + case let .image(image): + previewView.imageView.image = image + previewView.imageView.isHidden = false + case .waitingForData: + break + } + } + + + let progressPoint = NSMakePoint(max(0, min(point.x, self.controls.progress.frame.width)), 0) + let converted = self.convert(progressPoint, from: self.controls.progress) + previewView.setFrameOrigin(NSMakePoint(max(10, min(frame.width - previewView.frame.width - 10, converted.x - previewView.frame.width / 2)), self.controls.frame.minY - previewView.frame.height - 10)) + + + let currentTime = Int(round(progressPoint.x / self.controls.progress.frame.width * CGFloat(status.duration))) + + + let duration = String.durationTransformed(elapsed: currentTime) + let layout = TextViewLayout(.initialize(string: duration, color: .white, font: .medium(.text)), maximumNumberOfLines: 1, alignment: .center, alwaysStaticItems: true) + + layout.measure(width: .greatestFiniteMagnitude) + + previewView.duration.update(layout) + previewView.duration.setFrameSize(NSMakeSize(layout.layoutSize.width + 10, layout.layoutSize.height + 10)) + previewView.duration.display() + previewView.needsLayout = true + } + + + func hideScrubblerPreviewIfNeeded() { + previewView?.removeFromSuperview() + previewView = nil + self.currentPreviewState = nil + } +} diff --git a/Telegram-Mac/SampleBufferPool.swift b/Telegram-Mac/SampleBufferPool.swift new file mode 100644 index 0000000000..e1cd1cb3a5 --- /dev/null +++ b/Telegram-Mac/SampleBufferPool.swift @@ -0,0 +1,73 @@ +// +// SampleBufferPool.swift +// Telegram +// +// Created by Mikhail Filimonov on 26/05/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import Foundation +import AVFoundation +import SwiftSignalKit + + +private final class SampleBufferLayerImplNullAction: NSObject, CAAction { + @objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) { + } +} + +private final class SampleBufferLayerImpl: AVSampleBufferDisplayLayer { + override func action(forKey event: String) -> CAAction? { + return SampleBufferLayerImplNullAction() + } +} + +final class SampleBufferLayer { + let layer: AVSampleBufferDisplayLayer + private let enqueue: (AVSampleBufferDisplayLayer) -> Void + + + var isFreed: Bool = false + fileprivate init(layer: AVSampleBufferDisplayLayer, enqueue: @escaping (AVSampleBufferDisplayLayer) -> Void) { + self.layer = layer + self.enqueue = enqueue + } + + deinit { + if !isFreed { + self.enqueue(self.layer) + } + } +} + +private let pool = Atomic<[AVSampleBufferDisplayLayer]>(value: []) + +func clearSampleBufferLayerPoll() { + let _ = pool.modify { _ in return [] } +} + +func takeSampleBufferLayer() -> SampleBufferLayer { + var layer: AVSampleBufferDisplayLayer? +// let _ = pool.modify { list in +// var list = list +// if !list.isEmpty { +// layer = list.removeLast() +// } +// return list +// } + if layer == nil { + layer = SampleBufferLayerImpl() + } + return SampleBufferLayer(layer: layer!, enqueue: { layer in + Queue.mainQueue().async { + layer.flushAndRemoveImage() + layer.setAffineTransform(CGAffineTransform.identity) +// let _ = pool.modify { list in +// var list = list +// list.append(layer) +// return list +// } + } + }) +} diff --git a/Telegram-Mac/SaveModalController.swift b/Telegram-Mac/SaveModalController.swift new file mode 100644 index 0000000000..012a37c895 --- /dev/null +++ b/Telegram-Mac/SaveModalController.swift @@ -0,0 +1,142 @@ +// +// SaveModalController.swift +// Telegram +// +// Created by Mikhail Filimonov on 19.02.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit + + +private final class SaveModalView : View { + private let imageView:MediaAnimatedStickerView = MediaAnimatedStickerView(frame: NSZeroRect) + private let textView: TextView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(imageView) + addSubview(textView) + self.wantsLayer = true + self.layer?.cornerRadius = 10.0 + self.autoresizingMask = [] + backgroundColor = .blackTransparent +// self.autoresizesSubviews = false +// self.material = .ultraDark +// self.blendingMode = .withinWindow +// self.state = .active + } + + override func viewDidMoveToSuperview() { + super.viewDidMoveToSuperview() + } + + override func mouseDown(with event: NSEvent) { + super.mouseDown(with: event) + } + + override var isFlipped: Bool { + return true + } + + override func layout() { + super.layout() + + if !textView.isHidden { + imageView.centerX(y: 0) + textView.centerX(y: imageView.frame.maxY - 15) + } else { + imageView.centerX(y: 0) + } + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(animation: LocalAnimatedSticker, size: NSSize, context: AccountContext, text: TextViewLayout?) { + imageView.update(with: animation.file, size: size, context: context, parent: nil, table: nil, parameters: animation.parameters, animated: false, positionFlags: nil, approximateSynchronousValue: false) + textView.isSelectable = false + textView.isHidden = text == nil + textView.update(text) + needsLayout = true + } +} + +class SaveModalController : ModalViewController { + override var background: NSColor { + return .clear + } + + override var contentBelowBackground: Bool { + return true + } + + override var containerBackground: NSColor { + return .clear + } + + override func viewClass() -> AnyClass { + return SaveModalView.self + } + private var genericView: SaveModalView { + return self.view as! SaveModalView + } + + override var redirectMouseAfterClosing: Bool { + return true + } + + + override func viewDidResized(_ size: NSSize) { + super.viewDidResized(size) + } + + override func viewDidLoad() { + super.viewDidLoad() + genericView.update(animation: self.animation, size: NSMakeSize(200, 150), context: self.context, text: self.text) + readyOnce() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } + + private let animation: LocalAnimatedSticker + private let text: TextViewLayout? + private let context: AccountContext + init(_ animation: LocalAnimatedSticker, context: AccountContext, text: TextViewLayout? = nil) { + self.animation = animation + self.text = text + self.context = context + super.init(frame: NSMakeRect(0, 0, 200, 200)) + self.bar = .init(height: 0) + } +} + + + +func showSaveModal(for window: Window, context: AccountContext, animation: LocalAnimatedSticker, text: TextViewLayout? = nil, delay _delay: Double) -> Signal { + + let modal = SaveModalController(animation, context: context, text: text) + + return Signal({ _ -> Disposable in + showModal(with: modal, for: window, animationType: .scaleCenter) + return ActionDisposable { + modal.close() + } + }) |> timeout(_delay, queue: Queue.mainQueue(), alternate: Signal({ _ -> Disposable in + modal.close() + return EmptyDisposable + })) +} diff --git a/Telegram-Mac/SearchController.swift b/Telegram-Mac/SearchController.swift index cc4c75a943..82c2f8b542 100644 --- a/Telegram-Mac/SearchController.swift +++ b/Telegram-Mac/SearchController.swift @@ -8,19 +8,131 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac -import PostboxMac -import TelegramCoreMac +import SwiftSignalKit +import Postbox +import TelegramCore +private final class SearchCacheData { + + fileprivate struct MessageCacheKey : Hashable { + let query: String + let tags: SearchTags? + init(query: String, tags: SearchTags? = nil) { + self.query = query + self.tags = tags + } + + static func key(query: String, tags: SearchTags? = nil) -> MessageCacheKey { + return .init(query: query, tags: tags) + } + } + + private var previousMessages:[ChatListSearchEntry] = [] + private var previousLocalPeers:[ChatListSearchEntry] = [] + private var previousRemotePeers:[ChatListSearchEntry] = [] + + private var messages: [MessageCacheKey: [ChatListSearchEntry]] = [:] + private var remotePeers: [String: [ChatListSearchEntry]] = [:] + private var localPeers: [String: [ChatListSearchEntry]] = [:] + + func cacheMessages(_ messages:[ChatListSearchEntry], for key: MessageCacheKey) -> Void { + self.messages[key] = messages + previousMessages = messages + } + func cacheRemotePeers(_ peers:[ChatListSearchEntry], for key: String) -> Void { + self.remotePeers[key] = peers + previousRemotePeers = peers + + var stableIds:Set = Set() + for peer in peers { + assert(!stableIds.contains(peer.stableId)) + stableIds.insert(peer.stableId) + } + + } + func cacheLocalPeers(_ peers:[ChatListSearchEntry], for key: String) -> Void { + self.localPeers[key] = peers + previousLocalPeers = peers + + var stableIds:Set = Set() + for peer in peers { + assert(!stableIds.contains(peer.stableId)) + stableIds.insert(peer.stableId) + } + + } + func cachedMessages(for key: MessageCacheKey) -> [ChatListSearchEntry] { + let value = self.messages[key] ?? previousMessages + return value + } + func cachedRemotePeers(for key: String) -> [ChatListSearchEntry] { + let value = self.remotePeers[key] ?? previousRemotePeers + return value + } + func cachedLocalPeers(for key: String) -> [ChatListSearchEntry] { + let value = self.localPeers[key] ?? previousLocalPeers + return value + } +} + + +struct ExternalSearchMessages { + let messages:[Message] + let count: Int32 + let tags: MessageTags? + init(messages: [Message] = [], count: Int32 = 0, tags: MessageTags? = nil) { + self.messages = messages + self.count = count + self.tags = tags + } + func withUpdatedTags(_ tags: MessageTags?) -> ExternalSearchMessages { + return ExternalSearchMessages(messages: self.messages, count: self.count, tags: tags) + } + var title: String? { + let text: String? + if tags == .photoOrVideo { + text = L10n.peerMediaTitleSearchMediaCountable(Int(count)) + } else if tags == .photo { + text = L10n.peerMediaTitleSearchPhotosCountable(Int(count)) + } else if tags == .video { + text = L10n.peerMediaTitleSearchVideosCountable(Int(count)) + } else if tags == .gif { + text = L10n.peerMediaTitleSearchGIFsCountable(Int(count)) + } else if tags == .file { + text = L10n.peerMediaTitleSearchFilesCountable(Int(count)) + } else if tags == .webPage { + text = L10n.peerMediaTitleSearchLinksCountable(Int(count)) + } else if tags == .music { + text = L10n.peerMediaTitleSearchMusicCountable(Int(count)) + } else { + text = nil + } + if let text = text { + return text.replacingOccurrences(of: "\(count)", with: count.formattedWithSeparator) + } + return text + } +} + +enum UnreadSearchBadge : Equatable { + case none + case muted(Int32) + case unmuted(Int32) +} + final class SearchControllerArguments { - let account: Account + let context: AccountContext let removeRecentPeerId:(PeerId)->Void let clearRecent:()->Void - init(account: Account, removeRecentPeerId:@escaping(PeerId)->Void, clearRecent:@escaping()->Void) { - self.account = account + let openTopPeer:(PopularItemType)->Void + let setPeerAsTag:(Peer)->Void + init(context: AccountContext, removeRecentPeerId:@escaping(PeerId)->Void, clearRecent:@escaping()->Void, openTopPeer:@escaping(PopularItemType)->Void, setPeerAsTag: @escaping(Peer)->Void) { + self.context = context self.removeRecentPeerId = removeRecentPeerId self.clearRecent = clearRecent + self.openTopPeer = openTopPeer + self.setPeerAsTag = setPeerAsTag } } @@ -28,57 +140,13 @@ final class SearchControllerArguments { enum ChatListSearchEntryStableId: Hashable { case localPeerId(PeerId) case secretChat(PeerId) + case savedMessages case recentSearchPeerId(PeerId) case globalPeerId(PeerId) case messageId(MessageId) + case topPeers case separator(Int) case emptySearch - static func ==(lhs: ChatListSearchEntryStableId, rhs: ChatListSearchEntryStableId) -> Bool { - switch lhs { - case let .localPeerId(lhsPeerId): - if case let .localPeerId(rhsPeerId) = rhs { - return lhsPeerId == rhsPeerId - } else { - return false - } - case let .secretChat(peerId): - if case .secretChat(peerId) = rhs { - return true - } else { - return false - } - case let .recentSearchPeerId(lhsPeerId): - if case let .recentSearchPeerId(rhsPeerId) = rhs { - return lhsPeerId == rhsPeerId - } else { - return false - } - case let .globalPeerId(lhsPeerId): - if case let .globalPeerId(rhsPeerId) = rhs { - return lhsPeerId == rhsPeerId - } else { - return false - } - case let .messageId(lhsMessageId): - if case let .messageId(rhsMessageId) = rhs { - return lhsMessageId == rhsMessageId - } else { - return false - } - case let .separator(lhsIndex): - if case let .separator(rhsIndex) = rhs { - return lhsIndex == rhsIndex - } else { - return false - } - case .emptySearch: - if case .emptySearch = rhs { - return true - } else { - return false - } - } - } var hashValue: Int { switch self { @@ -90,49 +158,56 @@ enum ChatListSearchEntryStableId: Hashable { return peerId.hashValue case let .globalPeerId(peerId): return peerId.hashValue + case .savedMessages: + return 1000 case let .messageId(messageId): return messageId.hashValue case let .separator(index): return index case .emptySearch: return 0 + case .topPeers: + return -1 } } } private struct SearchSecretChatWrapper : Equatable { let peerId:PeerId - static func ==(lhs: SearchSecretChatWrapper, rhs: SearchSecretChatWrapper) -> Bool { - return lhs.peerId == rhs.peerId - } } fileprivate enum ChatListSearchEntry: Comparable, Identifiable { - case localPeer(Peer, Int, SearchSecretChatWrapper?, Bool) - case recentlySearch(Peer, Int, SearchSecretChatWrapper?, Bool) - case globalPeer(Peer, Int) - case message(Message,Int) + case localPeer(Peer, Int, SearchSecretChatWrapper?, UnreadSearchBadge, Bool, Bool) + case recentlySearch(Peer, Int, SearchSecretChatWrapper?, PeerStatusStringResult, UnreadSearchBadge, Bool) + case globalPeer(FoundPeer, UnreadSearchBadge, Int) + case savedMessages(Peer) + case message(Message, String, CombinedPeerReadState?, Int) case separator(text: String, index:Int, state:SeparatorBlockState) + case topPeers(Int, articlesEnabled: Bool, unreadArticles: Int32, selfPeer: Peer, peers: [Peer], unread: [PeerId: UnreadSearchBadge], online: [PeerId : Bool]) case emptySearch var stableId: ChatListSearchEntryStableId { switch self { - case let .localPeer(peer, _, secretChat, _): + case let .localPeer(peer, _, secretChat, _, _, _): if let secretChat = secretChat { return .secretChat(secretChat.peerId) } return .localPeerId(peer.id) - case let .globalPeer(peer, _): - return .globalPeerId(peer.id) - case let .message(message,_): + case let .globalPeer(found, _, _): + return .globalPeerId(found.peer.id) + case let .message(message, _, _, _): return .messageId(message.id) + case .savedMessages: + return .savedMessages case let .separator(_,index, _): return .separator(index) - case let .recentlySearch(peer, _, secretChat, _): + case let .recentlySearch(peer, _, secretChat, _, _, _): if let secretChat = secretChat { return .secretChat(secretChat.peerId) } return .recentSearchPeerId(peer.id) + case .topPeers: + return .topPeers case .emptySearch: return .emptySearch } @@ -140,15 +215,19 @@ fileprivate enum ChatListSearchEntry: Comparable, Identifiable { var index:Int { switch self { - case let .localPeer(_,index, _, _): + case let .localPeer(_,index, _, _, _, _): return index - case let .globalPeer(_,index): + case let .globalPeer(_, _,index): return index - case let .message(_,index): + case let .message(_, _, _, index): return index + case .savedMessages: + return -1 case let .separator(_,index, _): return index - case let .recentlySearch(_,index, _, _): + case let .recentlySearch(_,index, _, _, _, _): + return index + case let .topPeers(index, _, _, _, _, _, _): return index case .emptySearch: return 0 @@ -157,30 +236,33 @@ fileprivate enum ChatListSearchEntry: Comparable, Identifiable { static func ==(lhs: ChatListSearchEntry, rhs: ChatListSearchEntry) -> Bool { switch lhs { - case let .localPeer(lhsPeer, lhsIndex, lhsSecretChat, lhsDrawBorder): - if case let .localPeer(rhsPeer, rhsIndex, rhsSecretChat, rhsDrawBorder) = rhs, lhsPeer.isEqual(rhsPeer) && lhsIndex == rhsIndex && lhsSecretChat == rhsSecretChat && lhsDrawBorder == rhsDrawBorder { + case let .localPeer(lhsPeer, index, isSecretChat, badge, drawBorder, canAddAsTag): + if case .localPeer(let rhsPeer, index, isSecretChat, badge, drawBorder, canAddAsTag) = rhs, lhsPeer.isEqual(rhsPeer) { return true } else { return false } - case let .recentlySearch(lhsPeer, lhsIndex, lhsSecretChat, lhsDrawBorder): - if case let .recentlySearch(rhsPeer, rhsIndex, rhsSecretChat, rhsDrawBorder) = rhs, lhsPeer.isEqual(rhsPeer) && lhsIndex == rhsIndex && lhsSecretChat == rhsSecretChat && lhsDrawBorder == rhsDrawBorder { + case let .recentlySearch(lhsPeer, index, isSecretChat, status, badge, drawBorder): + if case .recentlySearch(let rhsPeer, index, isSecretChat, status, badge, drawBorder) = rhs, lhsPeer.isEqual(rhsPeer) { return true } else { return false } - case let .globalPeer(lhsPeer, lhsIndex): - if case let .globalPeer(rhsPeer, rhsIndex) = rhs, lhsPeer.isEqual(rhsPeer) && lhsIndex == rhsIndex { + case let .globalPeer(lhsPeer, badge, index): + if case .globalPeer(let rhsPeer, badge, index) = rhs, lhsPeer.peer.isEqual(rhsPeer.peer) && lhsPeer.subscribers == rhsPeer.subscribers { return true } else { return false } - case let .message(lhsMessage, lhsIndex): - if case let .message(rhsMessage, rhsIndex) = rhs { - - if lhsIndex != rhsIndex { - return false - } + case .savedMessages: + if case .savedMessages = rhs { + return true + } else { + return false + } + case let .message(lhsMessage, text, combinedState, index): + if case .message(let rhsMessage, text, combinedState, index) = rhs { + if lhsMessage.id != rhsMessage.id { return false } @@ -207,6 +289,29 @@ fileprivate enum ChatListSearchEntry: Comparable, Identifiable { } else { return false } + case let .topPeers(index, articlesEnabled, unreadArticles, lhsSelfPeer, lhsPeers, lhsUnread, online): + if case .topPeers(index, articlesEnabled, unreadArticles, let rhsSelfPeer, let rhsPeers, let rhsUnread, online) = rhs { + if !lhsSelfPeer.isEqual(rhsSelfPeer) { + return false + } + + if lhsUnread != rhsUnread { + return false + } + + if lhsPeers.count != rhsPeers.count { + return false + } else { + for i in 0 ..< lhsPeers.count { + if !lhsPeers[i].isEqual(lhsPeers[i]) { + return false + } + } + return true + } + } else { + return false + } } } @@ -216,64 +321,271 @@ fileprivate enum ChatListSearchEntry: Comparable, Identifiable { } -fileprivate func prepareEntries(from:[AppearanceWrapperEntry]?, to:[AppearanceWrapperEntry], arguments:SearchControllerArguments, initialSize:NSSize) -> TableEntriesTransition<[AppearanceWrapperEntry]> { +private func peerContextMenuItems(peer: Peer, pinnedItems:[PinnedItemId], arguments: SearchControllerArguments) -> Signal<[ContextMenuItem], NoError> { + var items:[ContextMenuItem] = [] + + let togglePin:(Peer) -> Void = { peer in + let updatePeer = arguments.context.account.postbox.transaction { transaction -> Void in + updatePeers(transaction: transaction, peers: [peer], update: { (_, updated) -> Peer? in + return updated + }) + } |> mapToSignal { _ -> Signal in + return arguments.context.engine.peers.toggleItemPinned(location: .group(.root), itemId: .peer(peer.id)) + } |> deliverOnMainQueue + + _ = updatePeer.start(next: { result in + switch result { + case .limitExceeded: + confirm(for: arguments.context.window, information: L10n.chatListContextPinErrorNew2, okTitle: L10n.alertOK, cancelTitle: "", thridTitle: L10n.chatListContextPinErrorNewSetupFolders, successHandler: { result in + switch result { + case .thrid: + arguments.context.sharedContext.bindings.rootNavigation().push(ChatListFiltersListController(context: arguments.context)) + default: + break + } + }) + default: + break + } + }) + } + + var isPinned: Bool = false + for item in pinnedItems { + switch item { + case let .peer(peerId): + if peerId == peer.id { + isPinned = true + break + } + } + } + + items.append(ContextMenuItem(isPinned ? L10n.chatListContextUnpin : L10n.chatListContextPin, handler: { + togglePin(peer) + })) + + let peerId = peer.id + + return .single(items) |> mapToSignal { items in + return chatListFilterPreferences(engine: arguments.context.engine) |> deliverOnMainQueue |> take(1) |> map { filters -> [ContextMenuItem] in + var items = items + var submenu: [ContextMenuItem] = [] + if peerId.namespace != Namespaces.Peer.SecretChat { + for item in filters.list { + submenu.append(ContextMenuItem(item.title, handler: { + _ = arguments.context.engine.peers.updateChatListFiltersInteractively({ list in + var list = list + for (i, folder) in list.enumerated() { + var folder = folder + if folder.id == item.id { + if item.data.includePeers.peers.contains(peerId) { + var peers = folder.data.includePeers.peers + peers.removeAll(where: { $0 == peerId }) + folder.data.includePeers.setPeers(peers) + } else { + folder.data.includePeers.setPeers(folder.data.includePeers.peers + [peerId]) + } + list[i] = folder + + } + } + return list + }).start() + }, state: item.data.includePeers.peers.contains(peerId) ? NSControl.StateValue.on : nil)) + } + } + + if !submenu.isEmpty { + items.append(ContextSeparatorItem()) + let item = ContextMenuItem(L10n.chatListFilterAddToFolder) + let menu = NSMenu() + for item in submenu { + menu.addItem(item) + } + item.submenu = menu + items.append(item) + } + return items + } + } +} + + +fileprivate func prepareEntries(from:[AppearanceWrapperEntry]?, to:[AppearanceWrapperEntry], arguments:SearchControllerArguments, pinnedItems:[PinnedItemId], initialSize:NSSize, animated: Bool) -> TableEntriesTransition<[AppearanceWrapperEntry]> { let (deleted,inserted, updated) = proccessEntriesWithoutReverse(from, right: to, { entry -> TableRowItem in switch entry.entry { - case let .message(message,_): - let item = ChatListMessageRowItem(initialSize, account: arguments.account, message: message, renderedPeer: RenderedPeer(message: message)) + case let .message(message, query, combinedState, _): + var peer = RenderedPeer(message: message) + if let group = message.peers[message.id.peerId] as? TelegramGroup, let migrationReference = group.migrationReference { + if let channelPeer = message.peers[migrationReference.peerId] { + peer = RenderedPeer(peer: channelPeer) + } + } + let item = ChatListMessageRowItem(initialSize, context: arguments.context, message: message, query: query, renderedPeer: peer, readState: combinedState) return item - case let .globalPeer(peer,_): - return RecentPeerRowItem(initialSize, peer: peer, account: arguments.account, stableId: entry.stableId, borderType: [.Right]) - case let .localPeer(peer, _, secretChat, drawBorder), let .recentlySearch(peer, _, secretChat, drawBorder): - - var canRemoveFromRecent: Bool = false - if case .recentlySearch = entry.entry { - canRemoveFromRecent = true + case let .globalPeer(foundPeer, badge, _): + var status: String? = nil + if let addressName = foundPeer.peer.addressName { + status = "@\(addressName)" } - - let item = RecentPeerRowItem(initialSize, peer: peer, account: arguments.account, stableId: entry.stableId, titleStyle: ControlStyle(font: .medium(.text), foregroundColor: secretChat != nil ? theme.colors.blueUI : theme.colors.text, highlightColor:.white), borderType: [.Right], drawCustomSeparator: drawBorder, canRemoveFromRecent: canRemoveFromRecent, removeAction: { - arguments.removeRecentPeerId(peer.id) + if let subscribers = foundPeer.subscribers, let username = status { + if foundPeer.peer.isChannel { + status = tr(L10n.searchGlobalChannel1Countable(username, Int(subscribers))) + } else if foundPeer.peer.isSupergroup || foundPeer.peer.isGroup { + status = tr(L10n.searchGlobalGroup1Countable(username, Int(subscribers))) + } + } + return RecentPeerRowItem(initialSize, peer: foundPeer.peer, account: arguments.context.account, stableId: entry.stableId, statusStyle:ControlStyle(font:.normal(.text), foregroundColor: theme.colors.grayText, highlightColor:.white), status: status, borderType: [.Right], contextMenuItems: { + return peerContextMenuItems(peer: foundPeer.peer, pinnedItems: pinnedItems, arguments: arguments) + }, unreadBadge: badge) + case let .localPeer(peer, _, secretChat, badge, drawBorder, canAddAsTag): + return RecentPeerRowItem(initialSize, peer: peer, account: arguments.context.account, stableId: entry.stableId, titleStyle: ControlStyle(font: .medium(.text), foregroundColor: secretChat != nil ? theme.colors.accent : theme.colors.text, highlightColor:.white), borderType: [.Right], drawCustomSeparator: drawBorder, isLookSavedMessage: true, drawLastSeparator: true, canRemoveFromRecent: false, controlAction: { + arguments.setPeerAsTag(peer) + }, contextMenuItems: { + return peerContextMenuItems(peer: peer, pinnedItems: pinnedItems, arguments: arguments) + }, unreadBadge: badge, canAddAsTag: canAddAsTag) + case let .recentlySearch(peer, _, secretChat, status, badge, drawBorder): + return RecentPeerRowItem(initialSize, peer: peer, account: arguments.context.account, stableId: entry.stableId, titleStyle: ControlStyle(font: .medium(.text), foregroundColor: secretChat != nil ? theme.colors.accent : theme.colors.text, highlightColor:.white), statusStyle: ControlStyle(font:.normal(.text), foregroundColor: status.status.attribute(NSAttributedString.Key.foregroundColor, at: 0, effectiveRange: nil) as? NSColor ?? theme.colors.grayText, highlightColor:.white), status: status.status.string, borderType: [.Right], drawCustomSeparator: drawBorder, isLookSavedMessage: true, drawLastSeparator: true, canRemoveFromRecent: true, controlAction: { + if let secretChat = secretChat { + arguments.removeRecentPeerId(secretChat.peerId) + } else { + arguments.removeRecentPeerId(peer.id) + } + }, contextMenuItems: { + return peerContextMenuItems(peer: peer, pinnedItems: pinnedItems, arguments: arguments) + }, unreadBadge: badge) + case let .savedMessages(peer): + return RecentPeerRowItem(initialSize, peer: peer, account: arguments.context.account, stableId: entry.stableId, titleStyle: ControlStyle(font: .medium(.text), foregroundColor: theme.colors.text, highlightColor:.white), borderType: [.Right], drawCustomSeparator: false, isLookSavedMessage: true, contextMenuItems: { + return peerContextMenuItems(peer: peer, pinnedItems: pinnedItems, arguments: arguments) }) - - return item case let .separator(text, index, state): let right:String? switch state { case .short: - right = tr(.separatorShowMore) + right = tr(L10n.separatorShowMore) case .all: - right = tr(.separatorShowLess) + right = tr(L10n.separatorShowLess) case .clear: - right = tr(.separatorClear) + right = tr(L10n.separatorClear) default: right = nil } - return SeparatorRowItem(initialSize, ChatListSearchEntryStableId.separator(index), string: text.uppercased(), right: right?.lowercased(), state: state) + return SeparatorRowItem(initialSize, ChatListSearchEntryStableId.separator(index), string: text.uppercased(), right: right?.lowercased(), state: state, border: [.Right]) case .emptySearch: return SearchEmptyRowItem(initialSize, stableId: ChatListSearchEntryStableId.emptySearch, border: [.Right]) + case let .topPeers(_, articlesEnabled, unreadArticles, selfPeer, peers, unread, online): + return PopularPeersRowItem(initialSize, stableId: entry.stableId, context: arguments.context, selfPeer: selfPeer, articlesEnabled: articlesEnabled, unreadArticles: unreadArticles, peers: peers, unread: unread, online: online, action: { type in + arguments.openTopPeer(type) + }) } }) - return TableEntriesTransition(deleted: deleted, inserted: inserted, updated:updated, entries: to, animated:true, state: .none(nil)) + return TableEntriesTransition(deleted: deleted, inserted: inserted, updated:updated, entries: to, animated: animated, state: .none(nil)) } +struct AppSearchOptions : OptionSet { + public var rawValue: UInt32 + + init(rawValue: UInt32) { + self.rawValue = rawValue + } + + init() { + self.rawValue = 0 + } + + init(_ flags: AppSearchOptions) { + var rawValue: UInt32 = 0 + + if flags.contains(AppSearchOptions.messages) { + rawValue |= AppSearchOptions.messages.rawValue + } + + if flags.contains(AppSearchOptions.chats) { + rawValue |= AppSearchOptions.chats.rawValue + } + + self.rawValue = rawValue + } + + static let messages = AppSearchOptions(rawValue: 1) + static let chats = AppSearchOptions(rawValue: 2) +} + + +struct SearchTags : Hashable { + let messageTags:MessageTags? + let peerTag: PeerId? + + var isEmpty: Bool { + return messageTags == nil && peerTag == nil + } + + var location: SearchMessagesLocation { + if let peerTag = peerTag { + return .peer(peerId: peerTag, fromId: nil, tags: messageTags, topMsgId: nil, minDate: nil, maxDate: nil) + } else { + return .general(tags: messageTags, minDate: nil, maxDate: nil) + } + } +} + class SearchController: GenericViewController,TableViewDelegate { - private let account:Account - private let arguments:SearchControllerArguments - private var open:(PeerId, Message?, Bool) -> Void = {_,_,_ in} + func findGroupStableId(for stableId: AnyHashable) -> AnyHashable? { + return nil + } + + + var defaultQuery: String? = nil + private let context:AccountContext + private var marked: Bool = false + private let arguments:SearchControllerArguments + private var open:(PeerId?, MessageId?, Bool) -> Void = {_,_,_ in} + private let groupId: PeerGroupId private let searchQuery:Promise = Promise() + private var query: String? = nil private let openPeerDisposable:MetaDisposable = MetaDisposable() private let statePromise:Promise<(SeparatorBlockState,SeparatorBlockState)> = Promise((SeparatorBlockState.short, SeparatorBlockState.short)) private let disposable:MetaDisposable = MetaDisposable() + private let pinnedPromise: ValuePromise<[PinnedItemId]> = ValuePromise([], ignoreRepeated: true) + + private let isRevealed: ValuePromise = ValuePromise(false, ignoreRepeated: true) + + private let globalTagsValue: ValuePromise = ValuePromise(SearchTags(messageTags: nil, peerTag: nil), ignoreRepeated: true) + + + var setPeerAsTag: ((Peer?)->Void)? = nil + + var pinnedItems:[PinnedItemId] = [] { + didSet { + pinnedPromise.set(pinnedItems) + } + } + let isLoading = Promise(false) + private(set) var searchTags: SearchTags? + public func updateSearchTags(_ globalTags: SearchTags) { + self._messagesValue.set(.single((ExternalSearchMessages(), false))) + self.globalTagsValue.set(globalTags) + self.searchTags = globalTags + } + + private var _messagesValue:Promise<(ExternalSearchMessages?, Bool)> = Promise() + + var externalSearchMessages:Signal { + return combineLatest(self._messagesValue.get() |> filter { $0.1 } |> map { $0.0 }, self.globalTagsValue.get() |> map { $0.messageTags }) |> map { + return $0.0?.withUpdatedTags($0.1) + } + } override func viewDidLoad() { super.viewDidLoad() @@ -281,23 +593,90 @@ class SearchController: GenericViewController,TableViewDelegate { genericView.needUpdateVisibleAfterScroll = true genericView.border = [.Right] - let account = self.account + genericView.getBackgroundColor = { + theme.colors.background + } + + let context = self.context + let options = self.options + let searchMessagesState: ValuePromise = ValuePromise() + let searchMessagesStateValue: Atomic = Atomic(value: nil) - + let isRevealed = self.isRevealed.get() + + let cachedData: Atomic = Atomic(value: SearchCacheData()) + let arguments = self.arguments let statePromise = self.statePromise.get() let atomicSize = self.atomicSize let previousSearchItems = Atomic<[AppearanceWrapperEntry]>(value: []) + let groupId: PeerGroupId = self.groupId + let searchItems = combineLatest(globalTagsValue.get(), searchQuery.get()) |> mapToSignal { globalTags, query -> Signal<([ChatListSearchEntry], Bool, Bool, SearchMessagesState?, SearchMessagesResult?), NoError> in + let query = query ?? "" + if !query.isEmpty || !globalTags.isEmpty { + - let searchItems = searchQuery.get() |> mapToSignal { (query) -> Signal<([ChatListSearchEntry], Bool), Void> in - if let query = query, !query.isEmpty { var ids:[PeerId:PeerId] = [:] - let foundLocalPeers = combineLatest(account.postbox.searchPeers(query: query.lowercased()),account.postbox.searchContacts(query: query.lowercased())) - |> map { peers, contacts -> [ChatListSearchEntry] in + + let foundQueryPeers: Promise = Promise() + + let callback:(PeerId, Bool, MessageId?, ChatInitialAction?)->Void = { peerId, _, _, _ in } + + let link = inApp(for: query as NSString, context: context, peerId: nil, openInfo: callback, hashtag: nil, command: nil, applyProxy: nil, confirm: false) + + switch link { + case let .followResolvedName(_, username, _, context, _, _): + foundQueryPeers.set(resolveUsername(username: username, context: context)) + default: + foundQueryPeers.set(.single(nil)) + } + + var all = query.transformKeyboard + all.insert(query.lowercased(), at: 0) + all = all.uniqueElements + let localPeers:Signal<([RenderedPeer], [PeerId: UnreadSearchBadge]), NoError> = combineLatest(all.map { + return context.account.postbox.searchPeers(query: $0) + }) |> map { result in + return Array(result.joined()) + } |> mapToSignal { peers in + return combineLatest(peers.map { context.account.viewTracker.peerView($0.peerId) |> take(1) }) |> map { ($0, peers) } + } |> mapToSignal { peerViews, peers in + return context.account.postbox.unreadMessageCountsView(items: peers.map {.peer($0.peerId)}) |> take(1) |> map { values in + var unread:[PeerId: UnreadSearchBadge] = [:] + for peerView in peerViews { + let isMuted = peerView.isMuted + let unreadCount = values.count(for: .peer(peerView.peerId)) + if let unreadCount = unreadCount, unreadCount > 0 { + unread[peerView.peerId] = isMuted ? .muted(unreadCount) : .unmuted(unreadCount) + } + } + return (peers, unread) + } + } + + + let foundLocalPeers: Signal<[ChatListSearchEntry], NoError> = query.hasPrefix("#") || !options.contains(.chats) || !globalTags.isEmpty ? .single([]) : combineLatest(localPeers, context.account.postbox.loadedPeerWithId(context.peerId), foundQueryPeers.get()) + |> map { peers, accountPeer, inLinkPeer -> [ChatListSearchEntry] in var entries: [ChatListSearchEntry] = [] + + + if L10n.peerSavedMessages.lowercased().hasPrefix(query.lowercased()) || NSLocalizedString("Peer.SavedMessages", comment: "nil").lowercased().hasPrefix(query.lowercased()) { + entries.append(.savedMessages(accountPeer)) + ids[accountPeer.id] = accountPeer.id + } + var index = 1 - for rendered in peers { + + if let peer = inLinkPeer { + if ids[peer.id] == nil { + ids[peer.id] = peer.id + entries.append(.localPeer(peer, index, nil, .none, true, false)) + index += 1 + } + } + + for rendered in peers.0 { if ids[rendered.peerId] == nil { ids[rendered.peerId] = rendered.peerId if let peer = rendered.chatMainPeer { @@ -305,7 +684,7 @@ class SearchController: GenericViewController,TableViewDelegate { if rendered.peers[rendered.peerId] is TelegramSecretChat { wrapper = SearchSecretChatWrapper(peerId: rendered.peerId) } - entries.append(.localPeer(peer, index, wrapper, true)) + entries.append(.localPeer(peer, index, wrapper, peers.1[rendered.peerId] ?? .none, true, true)) index += 1 } @@ -315,109 +694,252 @@ class SearchController: GenericViewController,TableViewDelegate { return entries } - let foundRemotePeers: Signal<([ChatListSearchEntry], Bool), NoError> = .single(([], true)) |> then(searchPeers(account: account, query: query) - |> delay(0.2, queue: prepareQueue) - |> map { peers -> [Peer] in - return peers.filter { (peer) -> Bool in - let first = ids[peer.id] == nil - ids[peer.id] = peer.id - return first - } - } - |> map { peers -> ([ChatListSearchEntry], Bool) in - var entries: [ChatListSearchEntry] = [] - var index = 10001 - for peer in peers { - entries.append(.globalPeer(peer, index)) - index += 1 - } - return (entries, false) - }) + let foundRemotePeers: Signal<([ChatListSearchEntry], [ChatListSearchEntry], Bool), NoError> - let foundRemoteMessages: Signal<([ChatListSearchEntry], Bool), NoError> = .single(([], true)) |> then(searchMessages(account: account, peerId:nil , query: query) - |> delay(0.2, queue: prepareQueue) - |> map { messages -> ([ChatListSearchEntry], Bool) in + let location: SearchMessagesLocation + if groupId != .root { + location = .group(groupId: groupId, tags: nil, minDate: nil, maxDate: nil) + foundRemotePeers = .single(([], [], false)) + } else if query.hasPrefix("#") || !options.contains(.chats) { + location = globalTags.location + foundRemotePeers = .single(([], [], false)) + } else { + location = globalTags.location + if globalTags.isEmpty { - var entries: [ChatListSearchEntry] = [] - var index = 20001 - for message in messages { - entries.append(.message(message, index)) - index += 1 - } - return (entries, false) - }) + foundRemotePeers = .single(([], [], true)) |> then(context.engine.peers.searchPeers(query: query) + |> delay(0.2, queue: prepareQueue) + |> map { founds -> ([FoundPeer], [FoundPeer]) in + + return (founds.0.filter { found -> Bool in + let first = ids[found.peer.id] == nil + ids[found.peer.id] = found.peer.id + return first + }, founds.1.filter { found -> Bool in + let first = ids[found.peer.id] == nil + ids[found.peer.id] = found.peer.id + return first + }) + + } + |> mapToSignal { peers -> Signal<([FoundPeer], [FoundPeer], [PeerId : UnreadSearchBadge]), NoError> in + let all = peers.0 + peers.1 + return combineLatest(all.map { context.account.viewTracker.peerView($0.peer.id) |> take(1) }) |> mapToSignal { peerViews in + return context.account.postbox.unreadMessageCountsView(items: all.map {.peer($0.peer.id)}) |> take(1) |> map { values in + var unread:[PeerId: UnreadSearchBadge] = [:] + outer: for peerView in peerViews { + let isMuted = peerView.isMuted + let unreadCount = values.count(for: .peer(peerView.peerId)) + if let unreadCount = unreadCount, unreadCount > 0 { + if let peer = peerViewMainPeer(peerView) { + if let peer = peer as? TelegramChannel { + inner: switch peer.participationStatus { + case .member: + break inner + default: + continue outer + } + } + if let peer = peer as? TelegramGroup { + inner: switch peer.membership { + case .Member: + break inner + default: + continue outer + } + } + } + unread[peerView.peerId] = isMuted ? .muted(unreadCount) : .unmuted(unreadCount) + } + } + return (peers.0, peers.1, unread) + } + } + } + |> map { _local, _remote, unread -> ([ChatListSearchEntry], [ChatListSearchEntry], Bool) in + var local: [ChatListSearchEntry] = [] + var index = 1000 + for peer in _local { + local.append(.localPeer(peer.peer, index, nil, unread[peer.peer.id] ?? .none, true, true)) + index += 1 + } + + var remote: [ChatListSearchEntry] = [] + index = 10001 + for peer in _remote { + remote.append(.globalPeer(peer, unread[peer.peer.id] ?? .none, index)) + index += 1 + } + return (local, remote, false) + }) + } else { + foundRemotePeers = .single(([], [], false)) + } + } + + searchMessagesState.set(nil) - return combineLatest(foundLocalPeers, foundRemotePeers, foundRemoteMessages) - |> map { localPeers, remotePeers, remoteMessages -> ([ChatListSearchEntry], Bool) in + + let remoteSearch = searchMessagesState.get() |> mapToSignal { state in + return context.engine.messages.searchMessages(location: location , query: query, state: state) + |> delay(0.2, queue: prepareQueue) + |> map { result -> ([ChatListSearchEntry], Bool, SearchMessagesState?, SearchMessagesResult?) in + + var entries: [ChatListSearchEntry] = [] + var index = 20001 + for message in result.0.messages { + entries.append(.message(message, query, result.0.readStates[message.id.peerId], index)) + index += 1 + } + + return (entries, false, result.1, result.0) + } + } + //cachedData.with { $0.cachedMessages(for: .key(query: query, tags: globalTags)) } + + let foundRemoteMessages: Signal<([ChatListSearchEntry], Bool, SearchMessagesState?, SearchMessagesResult?), NoError> = !options.contains(.messages) ? .single(([], false, nil, nil)) : .single(([], true, nil, nil)) |> then(remoteSearch) + + return combineLatest(queue: prepareQueue, foundLocalPeers, foundRemotePeers, foundRemoteMessages, isRevealed) + |> map { localPeers, remotePeers, remoteMessages, isRevealed -> ([ChatListSearchEntry], Bool, SearchMessagesState?, SearchMessagesResult?) in + + _ = cachedData.with { value -> Void in + value.cacheMessages(remoteMessages.0, for: .key(query: query, tags: globalTags)) + value.cacheLocalPeers(remotePeers.0, for: query) + value.cacheRemotePeers(remotePeers.1, for: query) + } var entries:[ChatListSearchEntry] = [] - if !localPeers.isEmpty { - entries.append(.separator(text: tr(.searchSeparatorChatsAndContacts), index: 0, state: .none)) + if !localPeers.isEmpty || !remotePeers.0.isEmpty { - entries += localPeers + let peers = (localPeers + remotePeers.0) + + entries.append(.separator(text: L10n.searchSeparatorChatsAndContacts, index: 0, state: .none)) + if !remoteMessages.0.isEmpty { + entries += peers + } else { + entries += peers + } } - if !remotePeers.0.isEmpty { - entries.append(.separator(text: tr(.searchSeparatorGlobalPeers), index: 10000, state: .none)) - entries += remotePeers.0 + if !remotePeers.1.isEmpty { + + let state: SeparatorBlockState + if remotePeers.1.count > 5 { + if isRevealed { + state = .all + } else { + state = .short + } + } else { + state = .none + } + + entries.append(.separator(text: L10n.searchSeparatorGlobalPeers, index: 10000, state: state)) + + if !isRevealed { + entries += remotePeers.1.prefix(5) + } else { + entries += remotePeers.1 + } } if !remoteMessages.0.isEmpty { - entries.append(.separator(text: tr(.searchSeparatorMessages), index: 20000, state: .none)) + entries.append(.separator(text: L10n.searchSeparatorMessages, index: 20000, state: .none)) entries += remoteMessages.0 } - if entries.isEmpty && !remotePeers.1 && !remoteMessages.1 { + if entries.isEmpty && !remotePeers.2 && !remoteMessages.1 { entries.append(.emptySearch) } - return (entries, remotePeers.1 || remoteMessages.1) - } + return (entries, remotePeers.2 || remoteMessages.1, remoteMessages.2, remoteMessages.3) + } |> map { value in + return (value.0, value.1, false, value.2, value.3) + } } else { + + let recently = context.engine.peers.recentlySearchedPeers() |> mapToSignal { recently -> Signal<[PeerView], NoError> in + return combineLatest(recently.map {context.account.viewTracker.peerView($0.peer.peerId)}) + } |> map { peerViews -> [PeerView] in + return peerViews.filter { peerView in + if let group = peerViewMainPeer(peerView) as? TelegramGroup, group.migrationReference != nil { + return false + } + return true + } + } |> mapToSignal { peerViews -> Signal<([PeerView], [PeerId: UnreadSearchBadge]), NoError> in + return context.account.postbox.unreadMessageCountsView(items: peerViews.map {.peer($0.peerId)}) |> map { values in + + var unread:[PeerId: UnreadSearchBadge] = [:] + for peerView in peerViews { + let isMuted = peerView.isMuted + let unreadCount = values.count(for: .peer(peerView.peerId)) + if let unreadCount = unreadCount, unreadCount > 0 { + unread[peerView.peerId] = isMuted ? .muted(unreadCount) : .unmuted(unreadCount) + } + } + return (peerViews, unread) + } + + } |> deliverOnPrepareQueue + + let top: Signal<([Peer], [PeerId : UnreadSearchBadge], [PeerId : Bool]), NoError> = context.engine.peers.recentPeers() |> mapToSignal { recent in + switch recent { + case .disabled: + return .single(([], [:], [:])) + case let .peers(peers): + return combineLatest(peers.map {context.account.viewTracker.peerView($0.id)}) |> mapToSignal { peerViews -> Signal<([Peer], [PeerId: UnreadSearchBadge], [PeerId : Bool]), NoError> in + return context.account.postbox.unreadMessageCountsView(items: peerViews.map {.peer($0.peerId)}) |> map { values in + + var peers:[Peer] = [] + var unread:[PeerId: UnreadSearchBadge] = [:] + var online: [PeerId : Bool] = [:] + for peerView in peerViews { + if let peer = peerViewMainPeer(peerView) { + var isActive:Bool = false + if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence { + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + (_, isActive, _) = stringAndActivityForUserPresence(presence, timeDifference: context.timeDifference, relativeTo: Int32(timestamp)) + } + let isMuted = peerView.isMuted + let unreadCount = values.count(for: .peer(peerView.peerId)) + if let unreadCount = unreadCount, unreadCount > 0 { + unread[peerView.peerId] = isMuted ? .muted(unreadCount) : .unmuted(unreadCount) + } + + online[peer.id] = isActive + peers.append(peer) + } + } + return (peers, unread, online) + } + } + } + } |> deliverOnPrepareQueue - return combineLatest(recentPeers(account: account), recentlySearchedPeers(postbox: account.postbox), statePromise) |> map { (top, recent, state) -> ([ChatListSearchEntry], Bool) in + return combineLatest(queue: prepareQueue, context.account.postbox.loadedPeerWithId(context.peerId), top, recently, statePromise) |> map { user, top, recent, state -> ([ChatListSearchEntry], Bool) in var entries:[ChatListSearchEntry] = [] var i:Int = 0 var ids:[PeerId:PeerId] = [:] - var topIds:[PeerId:PeerId] = [:] - for t in top { - topIds[t.id] = t.id - } - var recent = recent.filter({topIds[$0.peerId] == nil}) - - if recent.count > 0 && top.count > 5 { - entries.append(.separator(text: tr(.searchSeparatorPopular), index: i, state: state.0)) - } + ids[context.peerId] = context.peerId - for peer in top { - if ids[peer.id] == nil { - ids[peer.id] = peer.id - var stop:Bool = false - recent = recent.filter({ids[$0.peerId] == nil}) - if case .short = state.0, (i == 4 && recent.count > 0) { - stop = true - } - entries.append(.localPeer(peer, i, nil, !stop)) - i += 1 - if stop { - break - } - } - - } - if recent.count > 0 { - entries.append(.separator(text: tr(.searchSeparatorRecent), index: i, state: .clear)) + entries.append(ChatListSearchEntry.topPeers(i, articlesEnabled: false, unreadArticles: 0, selfPeer: user, peers: top.0, unread: top.1, online: top.2)) + + if recent.0.count > 0 { + entries.append(.separator(text: L10n.searchSeparatorRecent, index: i, state: .clear)) i += 1 - for rendered in recent { - if ids[rendered.peerId] == nil { - ids[rendered.peerId] = rendered.peerId - if let peer = rendered.chatMainPeer { + for peerView in recent.0 { + if ids[peerView.peerId] == nil { + ids[peerView.peerId] = peerView.peerId + if let peer = peerViewMainPeer(peerView) { var wrapper:SearchSecretChatWrapper? = nil - if rendered.peers[rendered.peerId] is TelegramSecretChat { - wrapper = SearchSecretChatWrapper(peerId: rendered.peerId) + if peerView.peers[peerView.peerId] is TelegramSecretChat { + wrapper = SearchSecretChatWrapper(peerId: peerView.peerId) } - entries.append(.recentlySearch(peer, i, wrapper, true)) + let result = stringStatus(for: peerView, context: context, theme: PeerStatusStringTheme(titleFont: .medium(.title))) + + entries.append(.recentlySearch(peer, i, wrapper, result, recent.1[peerView.peerId] ?? .none, true)) i += 1 } @@ -429,35 +951,70 @@ class SearchController: GenericViewController,TableViewDelegate { entries.append(.emptySearch) } - return (entries, false) + return (entries.sorted(by: <), false) + } |> map {value in + return (value.0, value.1, true, nil, nil) } } } - isLoading.set(searchItems |> mapToSignal { values -> Signal in - return .single(values.1) - }) - let transition = combineLatest(searchItems, appearanceSignal) |> map { value, appearance in - return value.0.map {AppearanceWrapperEntry(entry: $0, appearance: appearance)} + let transition = combineLatest(queue: prepareQueue, searchItems, appearanceSignal, context.globalPeerHandler.get() |> distinctUntilChanged, pinnedPromise.get()) |> map { value, appearance, location, pinnedItems in + return (value.0.map {AppearanceWrapperEntry(entry: $0, appearance: appearance)}, value.1, value.2 ? nil : location, value.2, pinnedItems, value.3, value.4) } - |> map { entries -> TableEntriesTransition<[AppearanceWrapperEntry]> in - return prepareEntries(from: previousSearchItems.swap(entries) , to: entries, arguments: arguments, initialSize:atomicSize.modify { $0 }) + |> map { entries, loading, location, animated, pinnedItems, searchMessagesState, searchMessagesResult -> (TableUpdateTransition, Bool, ChatLocation?, SearchMessagesState?, SearchMessagesResult?) in + let transition = prepareEntries(from: previousSearchItems.swap(entries) , to: entries, arguments: arguments, pinnedItems: pinnedItems, initialSize: atomicSize.modify { $0 }, animated: animated) + return (transition, loading, location, searchMessagesState, searchMessagesResult) } |> deliverOnMainQueue - disposable.set(transition.start(next: { [weak self] transition in - self?.genericView.merge(with: transition) + + disposable.set(transition.start(next: { [weak self] (transition, loading, location, searchMessagesState, searchMessagesResult) in + guard let `self` = self else {return} + self.genericView.merge(with: transition) + self.isLoading.set(.single(loading)) + if self.scrollupOnNextTransition { + self.scrollup() + } + self.scrollupOnNextTransition = false + _ = searchMessagesStateValue.swap(searchMessagesState) + + + + self._messagesValue.set(.single((ExternalSearchMessages(messages: searchMessagesResult?.messages ?? [], count: searchMessagesResult?.totalCount ?? 0), searchMessagesResult != nil))) + + if let location = location { + if !(self.genericView.selectedItem() is ChatListMessageRowItem) { + switch location { + case let .peer(peerId): + let item = self.genericView.item(stableId: ChatListSearchEntryStableId.globalPeerId(peerId)) ?? self.genericView.item(stableId: ChatListSearchEntryStableId.localPeerId(peerId)) + if let item = item { + _ = self.genericView.select(item: item, notify: false, byClick: false) + } + case .replyThread: + break + } + } + } else { + self.genericView.cancelSelection() + } })) + genericView.setScrollHandler { position in + switch position.direction { + case .bottom: + searchMessagesState.set(searchMessagesStateValue.swap(nil)) + default: + break + } + } + ready.set(.single(true)) } override func initializer() -> TableView { - let vz = TableView.self - //controller.bar.height - return vz.init(frame: NSMakeRect(_frameRect.minX, _frameRect.minY, _frameRect.width, _frameRect.height - bar.height), drawBorder: true); + return TableView(frame: NSMakeRect(_frameRect.minX, _frameRect.minY, _frameRect.width, _frameRect.height - bar.height), drawBorder: true); } override func viewWillDisappear(_ animated: Bool) { @@ -465,64 +1022,168 @@ class SearchController: GenericViewController,TableViewDelegate { isLoading.set(.single(false)) self.window?.remove(object: self, for: .UpArrow) self.window?.remove(object: self, for: .DownArrow) + openPeerDisposable.set(nil) + globalDisposable.set(nil) + disposable.set(nil) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - self.window?.set(handler: { [weak self] () -> KeyHandlerResult in - if let item = self?.genericView.selectedItem(), item.index > 0 { - self?.genericView.selectPrev() - if self?.genericView.selectedItem() is SeparatorRowItem { - self?.genericView.selectPrev() + + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in + guard let `self` = self else {return .rejected} + + if self.window?.firstResponder?.className != "TGUIKit.SearchTextField" { + return .rejected + } + + if let highlighted = self.genericView.highlightedItem() { + _ = self.genericView.select(item: highlighted) + self.closeNext = true + return .invoked + } else if !self.marked { + self.genericView.cancelSelection() + self.genericView.selectNext() + self.closeNext = true + return .invoked + } + + return .rejected + }, with: self, for: .Return, priority: .modal) + + + setHighlightEvents() + + } + + func updateHighlightEvents(_ hasChat: Bool) { + if !hasChat { + setHighlightEvents() + } else { + removeHighlightEvents() + } + } + + private func setHighlightEvents() { + + removeHighlightEvents() + + + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in + if self?.window?.firstResponder?.className != "TGUIKit.SearchTextField" { + return .rejected + } + if let item = self?.genericView.highlightedItem(), item.index > 0 { + self?.genericView.highlightPrev(turnDirection: false) + while self?.genericView.highlightedItem() is PopularPeersRowItem || self?.genericView.highlightedItem() is SeparatorRowItem { + self?.genericView.highlightNext(turnDirection: false) } } return .invoked - }, with: self, for: .UpArrow, priority: .modal, modifierFlags: [.option]) + }, with: self, for: .UpArrow, priority: .modal) + - self.window?.set(handler: { [weak self] () -> KeyHandlerResult in - self?.genericView.selectNext() - if self?.genericView.selectedItem() is SeparatorRowItem { - self?.genericView.selectNext() + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in + if self?.window?.firstResponder?.className != "TGUIKit.SearchTextField" { + return .rejected } + self?.genericView.highlightNext(turnDirection: false) + + while self?.genericView.highlightedItem() is PopularPeersRowItem || self?.genericView.highlightedItem() is SeparatorRowItem { + self?.genericView.highlightNext(turnDirection: false) + } + return .invoked - }, with: self, for: .DownArrow, priority: .modal, modifierFlags: [.option]) + }, with: self, for: .DownArrow, priority: .modal) + + + self.window?.set(handler: { _ -> KeyHandlerResult in + return .rejected + }, with: self, for: .UpArrow, priority: .modal, modifierFlags: [.command]) + + self.window?.set(handler: { _ -> KeyHandlerResult in + return .rejected + }, with: self, for: .DownArrow, priority: .modal, modifierFlags: [.command]) + + + } + + private func removeHighlightEvents() { + genericView.cancelHighlight() + self.window?.remove(object: self, for: .DownArrow) + self.window?.remove(object: self, for: .UpArrow) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - genericView.startMerge() - request(with: nil) + request(with: self.defaultQuery) } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) - self.genericView.removeAll() - genericView.stopMerge() } - + private let globalDisposable = MetaDisposable() + private let options: AppSearchOptions deinit { openPeerDisposable.dispose() + globalDisposable.dispose() + disposable.dispose() } - init(account: Account, open:@escaping(PeerId,Message?, Bool) ->Void , frame:NSRect = NSZeroRect) { - self.account = account + init(context: AccountContext, open:@escaping(PeerId?, MessageId?, Bool) ->Void, options: AppSearchOptions = [.chats, .messages], frame:NSRect = NSZeroRect, groupId: PeerGroupId = .root) { + self.context = context self.open = open - self.arguments = SearchControllerArguments(account: account, removeRecentPeerId: { peerId in - _ = removeRecentlySearchedPeer(postbox: account.postbox, peerId: peerId).start() + self.options = options + self.groupId = groupId + + var setPeerAsTag:((Peer?)->Void)? = nil + + self.arguments = SearchControllerArguments(context: context, removeRecentPeerId: { peerId in + _ = context.engine.peers.removeRecentlySearchedPeer(peerId: peerId).start() }, clearRecent: { - _ = (recentlySearchedPeers(postbox: account.postbox) |> take(1) |> mapToSignal { - return combineLatest($0.map {removeRecentlySearchedPeer(postbox: account.postbox, peerId: $0.peerId)}) - }).start() + confirm(for: context.window, information: L10n.searchConfirmClearHistory, successHandler: { _ in + _ = (context.engine.peers.recentlySearchedPeers() |> take(1) |> mapToSignal { + return combineLatest($0.map {context.engine.peers.removeRecentlySearchedPeer(peerId: $0.peer.peerId)}) + }).start() + }) + + }, openTopPeer: { type in + switch type { + case let .peer(peer, _, _): + open(peer.id, nil, false) + _ = context.engine.peers.addRecentlySearchedPeer(peerId: peer.id).start() + case let .savedMessages(peer): + open(peer.id, nil, false) + case .articles: + break + } + }, setPeerAsTag: { peer in + setPeerAsTag?(peer) }) super.init(frame:frame) self.bar = .init(height: 0) + + setPeerAsTag = { [weak self] peer in + self?.setPeerAsTag?(peer) + } + + globalDisposable.set(context.globalPeerHandler.get().start(next: { [weak self] peerId in + if peerId == nil { + self?.genericView.cancelSelection() + } + })) } + private var scrollupOnNextTransition: Bool = false + func request(with query:String?) -> Void { + setHighlightEvents() + self.query = query + self.scrollupOnNextTransition = true if let query = query, !query.isEmpty { searchQuery.set(.single(query)) } else { @@ -530,66 +1191,158 @@ class SearchController: GenericViewController,TableViewDelegate { } } + override func scrollup(force: Bool = false) { + genericView.clipView.scroll(to: NSMakePoint(0, 50), animated: false) + } + private var closeNext: Bool = false func selectionDidChange(row:Int, item:TableRowItem, byClick:Bool, isNew:Bool) -> Void { - var peer:Peer! - var peerId:PeerId! - var message:Message? + var peer:Peer? + var peerId:PeerId? + var messageId:MessageId? + var isGlobal = false + let context = self.context + if let item = item as? ChatListMessageRowItem { peer = item.peer - message = item.message - peerId = item.message!.id.peerId + messageId = item.message?.id + peerId = item.peer?.id } else if let item = item as? ShortPeerRowItem { if let stableId = item.stableId.base as? ChatListSearchEntryStableId { switch stableId { - case let .localPeerId(pId), let .recentSearchPeerId(pId), let .secretChat(pId), let .globalPeerId(pId): + case let .localPeerId(pId), let .recentSearchPeerId(pId), let .secretChat(pId): + peerId = pId + case let .globalPeerId(pId): + isGlobal = true peerId = pId + case .savedMessages: + peerId = context.peerId default: break } } peer = item.peer } else if let item = item as? SeparatorRowItem { - switch item.state { - case .short: - statePromise.set(.single((.all, .short))) - case .all: - statePromise.set(.single((.short, .short))) - case .clear: - arguments.clearRecent() - default: - break + if item.stableId == AnyHashable(ChatListSearchEntryStableId.separator(10000)) { + switch item.state { + case .short: + self.isRevealed.set(true) + case .all: + self.isRevealed.set(false) + default: + break + } + } else { + switch item.state { + case .short: + statePromise.set(.single((.all, .short))) + case .all: + statePromise.set(.single((.short, .short))) + case .clear: + arguments.clearRecent() + default: + break + } } + return + } else if item is PopularPeersRowItem { + peerId = context.peerId } - - let storedPeer = account.postbox.modify { modifier -> Void in - if modifier.getPeer(peer.id) == nil { - updatePeers(modifier: modifier, peers: [peer], update: { (previous, updated) -> Peer? in - return updated - }) + var storedPeer: Signal + if let peer = peer { + storedPeer = context.account.postbox.transaction { transaction -> Void in + if transaction.getPeer(peer.id) == nil { + updatePeers(transaction: transaction, peers: [peer], update: { (previous, updated) -> Peer? in + return updated + }) + } + + } |> mapToSignal { + return storedMessageFromSearchPeer(account: context.account, peer: peer) } - + } else if let peerId = peerId { + storedPeer = .single(peerId) + } else { + storedPeer = .complete() } + if let query = query, let peerId = peerId { + let link = inApp(for: query as NSString, context: context, peerId: peerId, openInfo: { _, _, _, _ in }, hashtag: nil, command: nil, applyProxy: nil, confirm: false) + switch link { + case let .followResolvedName(_, _, postId, _, _, _): + if let postId = postId { + messageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: postId) + } + default: + break + } + } + - let recently = (searchQuery.get() |> take(1)) |> mapToSignal { [weak self] query -> Signal in - if let _ = query, let account = self?.account, !(item is ChatListMessageRowItem) { - return addRecentlySearchedPeer(postbox: account.postbox, peerId: peerId) + let recently: Signal + if let peerId = peerId { + recently = (searchQuery.get() |> take(1)) |> mapToSignal { [weak self] query -> Signal in + if let context = self?.context, !(item is ChatListMessageRowItem) { + return context.engine.peers.addRecentlySearchedPeer(peerId: peerId) + } + return .single(Void()) } - return .complete() + } else { + recently = .single(Void()) } - openPeerDisposable.set((combineLatest(storedPeer, recently) |> deliverOnMainQueue).start( completed: { [weak self] in - self?.open(peerId, message, !(item is ChatListMessageRowItem) && byClick) - })) + _ = combineLatest(storedPeer, recently).start() + + removeHighlightEvents() + + marked = true + + if let peerId = peerId { + self.open(peerId, messageId, self.closeNext || (messageId == nil && !isGlobal)) + } } - func selectionWillChange(row: Int, item: TableRowItem) -> Bool { + func selectionWillChange(row: Int, item: TableRowItem, byClick: Bool) -> Bool { + + + + var peer: Peer? = nil + if let item = item as? ChatListMessageRowItem { + peer = item.peer + } else if let item = item as? ShortPeerRowItem { + peer = item.peer + } else if let item = item as? SeparatorRowItem { + switch item.state { + case .none: + return false + default: + return true + } + } + + if let peer = peer, let modalAction = navigationController?.modalAction { + + if !modalAction.isInvokable(for: peer) { + modalAction.alertError(for: peer, with:window!) + return false + } + modalAction.afterInvoke() + + if let modalAction = modalAction as? FWDNavigationAction { + if peer.id == context.peerId { + _ = Sender.forwardMessages(messageIds: modalAction.messages.map{$0.id}, context: context, peerId: context.peerId).start() + _ = showModalSuccess(for: mainWindow, icon: theme.icons.successModalProgress, delay: 1.0).start() + navigationController?.removeModalAction() + return false + } + } + + } return !(item is SearchEmptyRowItem) } diff --git a/Telegram-Mac/SearchEmptyRowItem.swift b/Telegram-Mac/SearchEmptyRowItem.swift index 4155a90c83..5d19833fc2 100644 --- a/Telegram-Mac/SearchEmptyRowItem.swift +++ b/Telegram-Mac/SearchEmptyRowItem.swift @@ -9,34 +9,29 @@ import Cocoa import TGUIKit -class SearchEmptyRowItem: TableRowItem { +class SearchEmptyRowItem: GeneralRowItem { - private let _stableId:AnyHashable let isLoading:Bool let icon:CGImage - let border:BorderType let text:TextViewLayout? - override var stableId: AnyHashable { - return _stableId - } + - init(_ initialSize: NSSize, stableId:AnyHashable, isLoading:Bool = false, icon:CGImage = theme.icons.emptySearch, text:String? = nil, border:BorderType = []) { - _stableId = stableId - self.border = border + init(_ initialSize: NSSize, stableId:AnyHashable, isLoading:Bool = false, icon:CGImage = theme.icons.emptySearch, text:String? = nil, border:BorderType = [], viewType: GeneralViewType = .legacy, customTheme: GeneralRowItem.Theme? = nil) { self.isLoading = isLoading self.icon = icon if let text = text { - self.text = TextViewLayout(.initialize(string: text, color: theme.colors.grayText, font: .normal(.title)), alignment: .center) + self.text = TextViewLayout(.initialize(string: text, color: customTheme?.grayTextColor ?? theme.colors.grayText, font: .normal(.title)), alignment: .center) self.text?.measure(width: initialSize.width - 60) } else { self.text = nil } - super.init(initialSize) + super.init(initialSize, stableId: stableId, viewType: viewType, border: border, customTheme: customTheme) } override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) text?.measure(width: width - 60) - return super.makeSize(width, oldWidth: oldWidth) + return success } override var height: CGFloat { @@ -50,7 +45,7 @@ class SearchEmptyRowItem: TableRowItem { } return true }) - return table.frame.height - basic - 50 + return table.frame.height - basic } else { return initialSize.height } @@ -76,6 +71,16 @@ class SearchEmptyRowView : TableRowView { } + override var backdorColor: NSColor { + if let item = item as? SearchEmptyRowItem { + if let customTheme = item.customTheme { + return customTheme.backgroundColor + } + return item.viewType.rowBackground + } else { + return super.backdorColor + } + } override func layout() { super.layout() diff --git a/Telegram-Mac/SearchPeerMembers.swift b/Telegram-Mac/SearchPeerMembers.swift new file mode 100644 index 0000000000..c8b4d0d86f --- /dev/null +++ b/Telegram-Mac/SearchPeerMembers.swift @@ -0,0 +1,100 @@ +// +// SearchPeerMembers.swift +// Telegram +// +// Created by Mikhail Filimonov on 02/01/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa + +import Foundation +import Postbox +import TelegramCore + +import SwiftSignalKit + + +func searchPeerMembers(context: AccountContext, peerId: PeerId, chatLocation: ChatLocation, query: String) -> Signal<[Peer], NoError> { + if peerId.namespace == Namespaces.Peer.CloudChannel { + return context.account.postbox.transaction { transaction -> CachedChannelData? in + return transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData + } + |> mapToSignal { cachedData -> Signal<([Peer], Bool), NoError> in + if case .peer = chatLocation, let cachedData = cachedData, let memberCount = cachedData.participantsSummary.memberCount, memberCount <= 64 { + return Signal { subscriber in + let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.recent(peerId: peerId, searchQuery: nil, requestUpdate: false, updated: { state in + if case .ready = state.loadingState { + let normalizedQuery = query.lowercased() + subscriber.putNext((state.list.compactMap { participant -> Peer? in + if participant.peer.isDeleted { + return nil + } + if normalizedQuery.isEmpty { + return participant.peer + } + if normalizedQuery.isEmpty { + return participant.peer + } else { + if participant.peer.indexName.matchesByTokens(normalizedQuery) { + return participant.peer + } + if let addressName = participant.peer.addressName, addressName.lowercased().hasPrefix(normalizedQuery) { + return participant.peer + } + + return nil + } + }, true)) + } + }) + + return ActionDisposable { + disposable.dispose() + } + } + |> runOn(Queue.mainQueue()) + } + + return Signal { subscriber in + switch chatLocation { + case let .peer(peerId): + let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.recent(peerId: peerId, searchQuery: query.isEmpty ? nil : query, updated: { state in + if case .ready = state.loadingState { + subscriber.putNext((state.list.compactMap { participant in + if participant.peer.isDeleted { + return nil + } + return participant.peer + }, true)) + } + }) + + return ActionDisposable { + disposable.dispose() + } + case let .replyThread(replyThreadMessage): + let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.mentions(peerId: peerId, threadMessageId: replyThreadMessage.messageId, searchQuery: query.isEmpty ? nil : query, updated: { state in + if case .ready = state.loadingState { + subscriber.putNext((state.list.compactMap { participant in + if participant.peer.isDeleted { + return nil + } + return participant.peer + }, true)) + } + }) + + return ActionDisposable { + disposable.dispose() + } + } + } |> runOn(Queue.mainQueue()) + } + |> mapToSignal { result, isReady -> Signal<[Peer], NoError> in + return .single(result) + } + } else { + return context.engine.peers.searchGroupMembers(peerId: peerId, query: query) + } +} diff --git a/Telegram-Mac/SearchResultModalController.swift b/Telegram-Mac/SearchResultModalController.swift index d2356ee0f4..fa4181b79f 100644 --- a/Telegram-Mac/SearchResultModalController.swift +++ b/Telegram-Mac/SearchResultModalController.swift @@ -8,9 +8,10 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit fileprivate enum SearchResultEntry : Comparable, Identifiable { case message(Message) @@ -52,15 +53,15 @@ fileprivate class SearchResultModalView : View { required init(frame frameRect: NSRect) { super.init(frame: frameRect) - separator.backgroundColor = .border - + separator.backgroundColor = theme.colors.border + textView.backgroundColor = theme.colors.background addSubview(table) addSubview(textView) addSubview(separator) } func updateTitle(_ string:String) { - textView.set(layout: TextViewLayout(.initialize(string: string, color: .text, font: .medium(.title)), maximumNumberOfLines: 1, truncationType:.middle)) + textView.set(layout: TextViewLayout(.initialize(string: string, color: theme.colors.text, font: .medium(.title)), maximumNumberOfLines: 1, truncationType:.middle)) self.needsLayout = true } @@ -68,7 +69,7 @@ fileprivate class SearchResultModalView : View { super.layout() textView.layout?.measure(width: frame.width - 40) textView.update(textView.layout) - textView.centerX(y:floorToScreenPixels((50 - textView.frame.height)/2.0)) + textView.centerX(y:floorToScreenPixels(backingScaleFactor, (50 - textView.frame.height)/2.0)) separator.frame = NSMakeRect(0, 50 - .borderSize, frame.width, .borderSize) table.frame = NSMakeRect(0, 50, frame.width, frame.height - 50) @@ -78,32 +79,37 @@ fileprivate class SearchResultModalView : View { } } -fileprivate func prepareEntries(from:[SearchResultEntry], to:[SearchResultEntry], initialSize:NSSize, account:Account) -> TableUpdateTransition { +fileprivate func prepareEntries(from:[SearchResultEntry], to:[SearchResultEntry], initialSize:NSSize, context: AccountContext) -> TableUpdateTransition { let (removed,inserted,updated) = proccessEntriesWithoutReverse(from, right: to) { entry -> TableRowItem in switch entry { case let .message(message): - return ChatListMessageRowItem(initialSize, account: account, message: message, renderedPeer: RenderedPeer(message: message)) + return ChatListMessageRowItem(initialSize, context: context, message: message, query: "", renderedPeer: RenderedPeer(message: message), readState: nil) } } return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated) } class SearchResultModalController: ModalViewController, TableViewDelegate { - private let account:Account + + func findGroupStableId(for stableId: AnyHashable) -> AnyHashable? { + return nil + } + + private let context:AccountContext private let entries:Atomic<[SearchResultEntry]> = Atomic(value:[]) private let promise:Promise<[Message]> = Promise() private let query:String private let chatInteraction:ChatInteraction - init(_ account:Account, messages:[Message] = [], query:String, chatInteraction:ChatInteraction) { - self.account = account + init(_ context: AccountContext, messages:[Message] = [], query:String, chatInteraction:ChatInteraction) { + self.context = context self.query = query self.chatInteraction = chatInteraction promise.set(.single(messages)) super.init(frame: NSMakeRect(0, 0, 300, 360)) } - init(_ account:Account, request:Signal<[Message],Void>, query:String, chatInteraction:ChatInteraction) { - self.account = account + init(_ context: AccountContext, request:Signal<[Message], NoError>, query:String, chatInteraction:ChatInteraction) { + self.context = context self.query = query promise.set(request) self.chatInteraction = chatInteraction @@ -125,7 +131,7 @@ class SearchResultModalController: ModalViewController, TableViewDelegate { genericView.updateTitle(query) let entries = self.entries let initialSize = self.atomicSize - let account = self.account + let context = self.context genericView.table.delegate = self genericView.table.merge(with: promise.get() @@ -133,17 +139,17 @@ class SearchResultModalController: ModalViewController, TableViewDelegate { return messages.map({.message($0)}) } |> map { [weak self] new -> TableUpdateTransition in self?.readyOnce() - return prepareEntries(from: entries.swap(new), to: entries.modify({$0}), initialSize: initialSize.modify({$0}), account: account) + return prepareEntries(from: entries.swap(new), to: entries.modify({$0}), initialSize: initialSize.modify({$0}), context: context) }) } func selectionDidChange(row:Int, item:TableRowItem, byClick:Bool, isNew:Bool) -> Void { if let item = item as? ChatListMessageRowItem, let message = item.message { - chatInteraction.focusMessageId(nil, message.id, .center(id: 0, animated: true, focus: true, inset: 0)) + chatInteraction.focusMessageId(nil, message.id, .CenterEmpty) } close() } - func selectionWillChange(row:Int, item:TableRowItem) -> Bool { + func selectionWillChange(row:Int, item:TableRowItem, byClick: Bool) -> Bool { return true } func isSelectable(row:Int, item:TableRowItem) -> Bool { diff --git a/Telegram-Mac/SearchRowItem.swift b/Telegram-Mac/SearchRowItem.swift index 51476b99c6..06cb57ab0c 100644 --- a/Telegram-Mac/SearchRowItem.swift +++ b/Telegram-Mac/SearchRowItem.swift @@ -21,12 +21,13 @@ class SearchRowItem: GeneralRowItem { } - init(_ initialSize: NSSize, stableId: AnyHashable, searchInteractions:SearchInteractions, isLoading:Bool = false, drawCustomSeparator: Bool = true, border: BorderType = [], inset: NSEdgeInsets = NSEdgeInsets(left:30,right:30, top: 10, bottom: 10)) { + init(_ initialSize: NSSize, stableId: AnyHashable, searchInteractions:SearchInteractions, isLoading:Bool = false, drawCustomSeparator: Bool = true, border: BorderType = [], inset: NSEdgeInsets = NSEdgeInsets(left:30,right:30, top: 10, bottom: 10), viewType: GeneralViewType = .legacy) { self.searchInteractions = searchInteractions self.isLoading = isLoading - super.init(initialSize, height: 0, stableId: stableId, type: .none, drawCustomSeparator: drawCustomSeparator, border: border, inset: inset) + super.init(initialSize, height: 0, stableId: stableId, type: .none, viewType: viewType, drawCustomSeparator: drawCustomSeparator, border: border, inset: inset) } + } @@ -40,15 +41,7 @@ class SearchRowView : TableRowView { super.init(frame: frameRect) addSubview(searchView) - searchView.searchInteractions = SearchInteractions ({ [weak self] state in - if let item = self?.item as? SearchRowItem { - item.searchInteractions.stateModified(state) - } - }, { [weak self] text in - if let item = self?.item as? SearchRowItem { - item.searchInteractions.textModified(text) - } - }) + } @@ -65,7 +58,18 @@ class SearchRowView : TableRowView { super.set(item: item) if let item = item as? SearchRowItem { self.searchView.isLoading = item.isLoading - self.searchView.updateLocalizationAndTheme() + self.searchView.updateLocalizationAndTheme(theme: theme) + + + searchView.searchInteractions = SearchInteractions ({ [weak self] state, animated in + if let item = self?.item as? SearchRowItem { + item.searchInteractions.stateModified(state, animated) + } + }, { [weak self] text in + if let item = self?.item as? SearchRowItem { + item.searchInteractions.textModified(text) + } + }) } } @@ -77,6 +81,24 @@ class SearchRowView : TableRowView { return searchView.input } + override func onRemove(_ animation: NSTableView.AnimationOptions) { + self.isHidden = true +// searchView.cancel(false) +// searchView.isHidden = true + } + + override func onInsert(_ animation: NSTableView.AnimationOptions) { + + if animation.contains(.effectFade) { + self.isHidden = true + self.searchView.layer?.animateAlpha(from: 0, to: 1, duration: 0.2, timingFunction: .easeOut, completion: { [weak self] _ in + self?.isHidden = false + }) + } + // searchView.cancel(false) + // searchView.isHidden = true + } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/Telegram-Mac/SearchSettingsController.swift b/Telegram-Mac/SearchSettingsController.swift new file mode 100644 index 0000000000..478fbe833b --- /dev/null +++ b/Telegram-Mac/SearchSettingsController.swift @@ -0,0 +1,179 @@ +// +// SearchSettingsController.swift +// Telegram +// +// Created by Mikhail Filimonov on 02.12.2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import SwiftSignalKit +import Postbox +import TelegramCore + +import TGUIKit + +private func searchSettingsEntries(context: AccountContext, items:[SettingsSearchableItem], recent: Bool) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + let sectionId: Int32 = 0 + var index: Int32 = 0 + + var previousIcon: SettingsSearchableItemIcon? + + if recent, !items.isEmpty { + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("separator"), equatable: InputDataEquatable(true), comparable: nil, item: { initialSize, stableId in + return SeparatorRowItem(initialSize, stableId, string: L10n.settingsSearchRecent, right: L10n.settingsSearchRecentClear, state: .clear, height: 20, action: { + clearRecentSettingsSearchItems(postbox: context.account.postbox) + }) + })) + index += 1 + } + + for item in items { + var image: CGImage? = nil + var leftInset: CGFloat = 21 + if previousIcon != item.icon { + image = item.icon.thumb + } else { + leftInset += 33 + } + previousIcon = item.icon + + let desc = item.breadcrumbs.joined(separator: " ") + + entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("search_\(item.id.index)"), equatable: InputDataEquatable(item.id), comparable: nil, item: { initialSize, stableId in + + let icon:GeneralThumbAdditional? + if let image = image { + icon = GeneralThumbAdditional(thumb: image, textInset: 33, thumbInset: 0) + } else { + icon = nil + } + + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: item.title, description: desc.isEmpty ? nil : desc, type: .context(" "), action: { + + addRecentSettingsSearchItem(postbox: context.account.postbox, item: item.id) + + item.present(context, context.sharedContext.bindings.rootNavigation(), { presentation, controller in + switch presentation { + case .push: + if let controller = controller { + context.sharedContext.bindings.rootNavigation().push(controller) + } + default: + break + } + }) + }, thumb: icon, border:[BorderType.Right], inset:NSEdgeInsets(left: leftInset)) + })) + index += 1 + +// entries.append(InputDataEntry.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: InputDataIdentifier("search_\(item.id.index)"), data: InputDataGeneralData(name: item.title, color: theme.colors.text, icon: nil, type: .none, description: item.breadcrumbs.joined(separator: " "), action: { +// +// }))) +// index += 1 + } + + /* + return + */ + + return entries +} + +func SearchSettingsController(context: AccountContext, searchQuery: Signal, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal) -> InputDataController { + + let searchableItems = Promise<[SettingsSearchableItem]>() + searchableItems.set(settingsSearchableItems(context: context, archivedStickerPacks: archivedStickerPacks, privacySettings: privacySettings)) + + + let previousRecentlySearchedItemOrder = Atomic<[SettingsSearchableItemId]>(value: []) + let fixedRecentlySearchedItems = settingsSearchRecentItems(postbox: context.account.postbox) + |> map { recentIds -> [SettingsSearchableItemId] in + var result: [SettingsSearchableItemId] = [] + let _ = previousRecentlySearchedItemOrder.modify { current in + var updated: [SettingsSearchableItemId] = [] + for id in current { + inner: for recentId in recentIds { + if recentId == id { + updated.append(id) + result.append(recentId) + break inner + } + } + } + for recentId in recentIds.reversed() { + if !updated.contains(recentId) { + updated.insert(recentId, at: 0) + result.insert(recentId, at: 0) + } + } + return updated + } + return result + } + + + let items:Signal<([SettingsSearchableItem], Bool), NoError> = searchQuery |> mapToSignal { state in + switch state.state { + case .Focus: + if !state.request.isEmpty { + return combineLatest(searchableItems.get(), faqSearchableItems(context: context)) + |> mapToSignal { searchableItems, faqSearchableItems -> Signal<([SettingsSearchableItem], Bool), NoError> in + let results = searchSettingsItems(items: searchableItems, query: state.request) + let faqResults = searchSettingsItems(items: faqSearchableItems, query: state.request) + let finalResults: [SettingsSearchableItem] + if faqResults.first?.id == .faq(1) { + finalResults = faqResults + results + } else { + finalResults = results + faqResults + } + return .single((finalResults, false)) + } + } else { + return combineLatest(searchableItems.get(), fixedRecentlySearchedItems) + |> map { searchableItems, recentItems -> ([SettingsSearchableItem], Bool) in + let searchableItemsMap = searchableItems.reduce([SettingsSearchableItemId : SettingsSearchableItem]()) { (map, item) -> [SettingsSearchableItemId: SettingsSearchableItem] in + var map = map + map[item.id] = item + return map + } + var result: [SettingsSearchableItem] = [] + for itemId in recentItems { + if let searchItem = searchableItemsMap[itemId] { + if case let .language(id) = searchItem.id, id > 0 { + } else { + result.append(searchItem) + } + } + } + return (result, true) + } + } + case .None: + return .complete() + } + } + + let entries:Signal = items |> map { items, recent in + return searchSettingsEntries(context: context, items: items, recent: recent) + } |> map { + return InputDataSignalValue(entries: $0, animated: false) + } + + let controller = InputDataController(dataSignal: entries, title: "") + + controller.didLoaded = { controller, _ in + controller.genericView.tableView.needUpdateVisibleAfterScroll = true + controller.genericView.tableView.border = [.Right] + controller.tableView.emptyItem = SearchSettingsEmptyItem(NSZeroSize) + } + + + controller.getBackgroundColor = { + return theme.colors.background + } + + return controller +} diff --git a/Telegram-Mac/SearchSettingsEmptyItem.swift b/Telegram-Mac/SearchSettingsEmptyItem.swift new file mode 100644 index 0000000000..69ab987c40 --- /dev/null +++ b/Telegram-Mac/SearchSettingsEmptyItem.swift @@ -0,0 +1,72 @@ +// +// SearchSettingsEmptyItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 14.01.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +class SearchSettingsEmptyItem: TableRowItem { + let textLayout:TextViewLayout + override init(_ initialSize:NSSize) { + textLayout = TextViewLayout(.initialize(string: L10n.settingsSearchEmptyItem, color: theme.colors.grayText, font: .normal(.title)), alignment: .center) + super.init(initialSize) + } + + override var height: CGFloat { + if let table = table { + return table.frame.height + } else { + return initialSize.height + } + } + + override func makeSize(_ width: CGFloat, oldWidth:CGFloat) -> Bool { + return true + } + + override func viewClass() -> AnyClass { + return SearchSettingsEmptyView.self + } +} + +class SearchSettingsEmptyView : TableRowView { + private let textView:TextView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(textView) + textView.isSelectable = false + border = [.Right] + } + + override var backdorColor: NSColor { + return theme.colors.background + } + + override func updateColors() { + super.updateColors() + textView.backgroundColor = backdorColor + } + + override func layout() { + super.layout() + if let item = item as? SearchSettingsEmptyItem { + item.textLayout.measure(width: frame.width - 60) + textView.update(item.textLayout) + textView.center() + } + } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + needsLayout = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/Telegram-Mac/SearchUtils.swift b/Telegram-Mac/SearchUtils.swift new file mode 100644 index 0000000000..7e1bd5b28f --- /dev/null +++ b/Telegram-Mac/SearchUtils.swift @@ -0,0 +1,101 @@ +// +// SearchUtils.swift +// Telegram +// +// Created by Mikhail Filimonov on 08/04/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + + +func rangeOfSearch(_ query: String, in text: String) -> NSRange? { + + guard !text.isEmpty && !query.isEmpty else { + return nil + } + + let range = text.lowercased().nsstring.range(of: query.lowercased()) + + if range.location != NSNotFound { + return range + } else { + return nil + } +// +// let query = (query.components(separatedBy: " ").max(by: { $0.count < $1.count}) ?? query).lowercased().trimmed.nsstring +// let text = text.lowercased().nsstring +// var start: Int = -1 +// var length: Int = -1 +// let N1 = text.length +// for a in 0 ..< N1 { +// var currentLen:Int = 0 +// let N2 = min(query.length, N1 - a) +// loop: for b in 0 ..< N2 { +// let match = text.character(at: a + b) == query.character(at: b) +// if match { +// currentLen += 1 +// } +// if !match || b == N2 - 1 { +// if currentLen > 0 && currentLen > length { +// length = currentLen +// start = a +// } +// break loop +// } +// } +// } +// if start == -1 { +// return nil +// } +// let punctuationsChars = " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" +// loop: for a in start + length ..< text.length { +// if !punctuationsChars.contains(text.substring(with: NSMakeRange(a, 1))) { +// length += 1 +// } else { +// break loop +// } +// } +// +// return start != NSNotFound ? NSMakeRange(start, length) : nil + +// text = text.toLowerCase(); +// String message = messageObject.messageOwner.message.toLowerCase(); +// int start = -1; +// int length = -1; +// for (int a = 0, N1 = message.length(); a < N1; a++) { +// int currentLen = 0; +// for (int b = 0, N2 = Math.min(text.length(), N1 - a); b < N2; b++) { +// boolean match = message.charAt(a + b) == text.charAt(b); +// if (match) { +// currentLen++; +// } +// if (!match || b == N2 - 1) { +// if (currentLen > 0 && currentLen > length) { +// length = currentLen; +// start = a; +// } +// break; +// } +// } +// } +// if (start == -1) { +// if (!urlPathSelection.isEmpty()) { +// linkSelectionBlockNum = -1; +// resetUrlPaths(true); +// invalidate(); +// } +// return; +// } +// String punctuationsChars = " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"; +// for (int a = start + length, N = message.length(); a < N; a++) { +// if (punctuationsChars.indexOf(message.charAt(a)) < 0) { +// length++; +// } else { +// break; +// } +// } + + +} diff --git a/Telegram-Mac/SecretChatKeyViewController.swift b/Telegram-Mac/SecretChatKeyViewController.swift index 156d16faef..d9c470bce7 100644 --- a/Telegram-Mac/SecretChatKeyViewController.swift +++ b/Telegram-Mac/SecretChatKeyViewController.swift @@ -8,9 +8,10 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac +import Postbox +import TelegramCore + +import SwiftSignalKit class SecretChatKeyView : View { let imageView:ImageView = ImageView() @@ -22,7 +23,7 @@ class SecretChatKeyView : View { addSubview(textView) addSubview(descriptionView) descriptionView.userInteractionEnabled = false - updateLocalizationAndTheme() + updateLocalizationAndTheme(theme: theme) } required init?(coder: NSCoder) { @@ -55,7 +56,7 @@ class SecretChatKeyView : View { style.lineSpacing = 3.0 style.lineBreakMode = .byWordWrapping style.alignment = .center - let attributedString = NSAttributedString(string: text, attributes: [.paragraphStyle: style, NSAttributedStringKey.font: NSFont.code(.title), .foregroundColor: theme.colors.text]) + let attributedString = NSAttributedString(string: text, attributes: [.paragraphStyle: style, NSAttributedString.Key.font: NSFont.code(.title), .foregroundColor: theme.colors.text]) let layout = TextViewLayout(attributedString, maximumNumberOfLines: 4, alignment: .center) @@ -63,7 +64,7 @@ class SecretChatKeyView : View { textView.update(layout) let attr = NSMutableAttributedString() - _ = attr.append(string: tr(.encryptionKeyDescription(participant.compactDisplayTitle, participant.compactDisplayTitle)), color: theme.colors.text, font: .normal(.text)) + _ = attr.append(string: tr(L10n.encryptionKeyDescription(participant.compactDisplayTitle, participant.compactDisplayTitle)), color: theme.colors.grayText, font: .normal(.text)) attr.detectBoldColorInString(with: .medium(.text)) @@ -77,21 +78,21 @@ class SecretChatKeyView : View { needsLayout = true } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - backgroundColor = theme.colors.background - textView.backgroundColor = theme.colors.background - descriptionView.backgroundColor = theme.colors.background + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + backgroundColor = theme.colors.grayBackground + textView.backgroundColor = theme.colors.grayBackground + descriptionView.backgroundColor = theme.colors.grayBackground } override func layout() { super.layout() - imageView.centerX(y: 20) - textView.centerX(y: imageView.frame.maxY + 20) + imageView.centerX(y: 30) + textView.centerX(y: imageView.frame.maxY + 30) descriptionView.layout?.measure(width: frame.width - 60) - descriptionView.centerX(y: textView.frame.maxY + 20) + descriptionView.centerX(y: textView.frame.maxY + 30) } } @@ -102,7 +103,7 @@ class SecretChatKeyViewController: TelegramGenericViewController deliverOnMainQueue).start(next: { [weak self] view, peerView, _ in + disposable.set((combineLatest(context.account.postbox.combinedView(keys: [.peerChatState(peerId: peerId)]), context.account.viewTracker.peerView( peerId), appearanceSignal) |> deliverOnMainQueue).start(next: { [weak self] view, peerView, _ in if let peerId = self?.peerId, let view = view.views[.peerChatState(peerId: peerId)] as? PeerChatStateView, let state = view.chatState as? SecretChatKeyState { if let keyFingerprint = state.keyFingerprint { @@ -124,8 +125,8 @@ class SecretChatKeyViewController: TelegramGenericViewController Bool { + switch self { + case let .uploading(progress): + if case .uploading(progress) = to { + return true + } else { + return false + } + case let .uploaded(file): + if case .uploaded(file) = to { + return true + } else { + return false + } + } + } +} + +struct SecureIdVerificationLocalDocument { + let id: Int64 + let resource: TelegramMediaResource + var state: SecureIdVerificationLocalDocumentState + + func isEqual(to: SecureIdVerificationLocalDocument) -> Bool { + if self.id != to.id { + return false + } + if !self.resource.isEqual(to: to.resource) { + return false + } + if !self.state.isEqual(to: to.state) { + return false + } + return true + } +} + +enum SecureIdVerificationDocumentId: Hashable { + case remote(Int64) + case local(Int64) + + static func ==(lhs: SecureIdVerificationDocumentId, rhs: SecureIdVerificationDocumentId) -> Bool { + switch lhs { + case let .remote(id): + if case .remote(id) = rhs { + return true + } else { + return false + } + case let .local(id): + if case .local(id) = rhs { + return true + } else { + return false + } + } + } + + var hashValue: Int { + switch self { + case let .local(id): + return Int(id) + case let .remote(id): + return Int(id) + } + } +} + +enum SecureIdVerificationDocument : Equatable { + case remote(SecureIdFileReference) + case local(SecureIdVerificationLocalDocument) + + var id: SecureIdVerificationDocumentId { + switch self { + case let .remote(file): + return .remote(file.id) + case let .local(file): + return .local(file.id) + } + } + + var resource: TelegramMediaResource { + switch self { + case let .remote(file): + return SecureFileMediaResource(file: file) + case let .local(file): + return file.resource + } + } + + func isEqual(to: SecureIdVerificationDocument) -> Bool { + switch self { + case let .remote(reference): + if case .remote(reference) = to { + return true + } else { + return false + } + case let .local(lhsDocument): + if case let .local(rhsDocument) = to, lhsDocument.isEqual(to: rhsDocument) { + return true + } else { + return false + } + } + } + static func ==(lhs: SecureIdVerificationDocument, rhs: SecureIdVerificationDocument) -> Bool { + return lhs.isEqual(to: rhs) + } +} diff --git a/Telegram-Mac/SecureIdVerificationDocumentsContext.swift b/Telegram-Mac/SecureIdVerificationDocumentsContext.swift new file mode 100644 index 0000000000..0787acca9d --- /dev/null +++ b/Telegram-Mac/SecureIdVerificationDocumentsContext.swift @@ -0,0 +1,78 @@ +import Foundation +import Postbox +import TelegramCore + +import SwiftSignalKit + +private final class DocumentContext { + private let disposable: Disposable + + init(disposable: Disposable) { + self.disposable = disposable + } + + deinit { + self.disposable.dispose() + } +} + +final class SecureIdVerificationDocumentsContext { + private let context: SecureIdAccessContext + private let postbox: Postbox + private let network: Network + private let update: (Int64, SecureIdVerificationLocalDocumentState) -> Void + private var contexts: [Int64: DocumentContext] = [:] + + init(postbox: Postbox, network: Network, context: SecureIdAccessContext, update: @escaping (Int64, SecureIdVerificationLocalDocumentState) -> Void) { + self.postbox = postbox + self.network = network + self.context = context + self.update = update + } + + func stateUpdated(_ documents: [SecureIdVerificationDocument]) { + var validIds = Set() + + for document in documents { + switch document { + case let .local(info): + validIds.insert(info.id) + if self.contexts[info.id] == nil { + let disposable = MetaDisposable() + self.contexts[info.id] = DocumentContext(disposable: disposable) + disposable.set((uploadSecureIdFile(context: self.context, postbox: self.postbox, network: self.network, resource: info.resource) + |> deliverOnMainQueue).start(next: { [weak self] result in + if let strongSelf = self { + switch result { + case let .progress(value): + if strongSelf.contexts[info.id] != nil { + strongSelf.update(info.id, .uploading(value)) + } + case let .result(file): + if strongSelf.contexts[info.id] != nil { + strongSelf.update(info.id, .uploaded(file.0)) + } + } + } + }, error: { [weak self] _ in + if let strongSelf = self { + + } + })) + } + case .remote: + break + } + } + + var removeIds: [Int64] = [] + for (id, _) in self.contexts { + if !validIds.contains(id) { + removeIds.append(id) + } + } + for id in removeIds { + self.contexts.removeValue(forKey: id) + } + } +} diff --git a/Telegram-Mac/SelectPeersController.swift b/Telegram-Mac/SelectPeersController.swift index 32858c0be9..0a13495652 100644 --- a/Telegram-Mac/SelectPeersController.swift +++ b/Telegram-Mac/SelectPeersController.swift @@ -8,95 +8,60 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit enum SelectPeerEntryStableId : Hashable { case search - case peerId(PeerId) + case peerId(PeerId, Int32) case searchEmpty case separator(Int32) - var hashValue: Int { - switch self { - case .search: - return 0 - case .searchEmpty: - return 1 - case .separator(let index): - return Int(index) - case let .peerId(peerId): - return peerId.hashValue - } - } - - static func ==(lhs:SelectPeerEntryStableId, rhs:SelectPeerEntryStableId) -> Bool { - switch lhs { - case .search: - if case .search = rhs { - return true - } else { - return false - } - case .searchEmpty: - if case .searchEmpty = rhs { - return true - } else { - return false - } - case let .peerId(peerId): - if case .peerId(peerId) = rhs { - return true - } else { - return false - } - case let .separator(index): - if case .separator(index) = rhs { - return true - } else { - return false - } - } - } + case inviteLink(Int) } enum SelectPeerEntry : Comparable, Identifiable { - case peer(Peer, Int32, PeerPresence?, Bool) - case searchEmpty - case separator(Int32, String) + case peer(SelectPeerValue, Int32, Bool) + case searchEmpty(GeneralRowItem.Theme, CGImage) + case separator(Int32, GeneralRowItem.Theme, String) + case inviteLink(String, CGImage, Int, GeneralRowItem.Theme, (Int)->Void) var stableId: SelectPeerEntryStableId { switch self { case .searchEmpty: return .searchEmpty - case .separator(let index, _): + case .separator(let index, _, _): return .separator(index) - case let .peer(peer, _, _, _): - return .peerId(peer.id) + case let .peer(peer, index, _): + return .peerId(peer.peer.id, index) + case let .inviteLink(_, _, index, _, _): + return .inviteLink(index) } } static func ==(lhs:SelectPeerEntry, rhs:SelectPeerEntry) -> Bool { switch lhs { - case .searchEmpty: - if case .searchEmpty = rhs { + case let .searchEmpty(theme, _): + if case .searchEmpty(theme, _) = rhs { return true } else { return false } - case .separator(let index, let text): - if case .separator(index, text) = rhs { + case let .separator(index, customTheme, text): + if case .separator(index, customTheme, text) = rhs { return true } else { return false } - case let .peer(lhsPeer, lhsIndex, lhsPresence, lhsEnabled): + case let .inviteLink(text, image, index, customTheme, _): + if case .inviteLink(text, image, index, customTheme, _) = rhs { + return true + } else { + return false + } + case let .peer(peer, index, enabled): switch rhs { - case let .peer(rhsPeer, rhsIndex, rhsPresence, rhsEnabled) where lhsPeer.isEqual(rhsPeer) && lhsIndex == rhsIndex && lhsEnabled == rhsEnabled: - if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence { - return lhsPresence.isEqual(to: rhsPresence) - } else if (lhsPresence != nil) != (rhsPresence != nil) { - return false - } + case .peer(peer, index, enabled): return true default: return false @@ -108,9 +73,11 @@ enum SelectPeerEntry : Comparable, Identifiable { switch self { case .searchEmpty: return 1 - case .separator(let index, _): + case .inviteLink: + return -1 + case .separator(let index, _, _): return index - case .peer(_, let index, _, _): + case .peer(_, let index, _): return index } } @@ -150,7 +117,7 @@ private extension PeerIndexNameRepresentation { if lastResult == .orderedSame { let f = lhsFirst.prefix(1) - if let character = f.characters.first { + if let character = f.first { let characterString = String(character) let scalars = characterString.unicodeScalars @@ -176,8 +143,91 @@ func <(lhs:Peer, rhs:Peer) -> Bool { return lhs.indexName.isLessThan(other: rhs.indexName) == .orderedAscending } -private func entriesForView(_ view: ContactPeersView, searchPeers:[PeerId], searchView:MultiplePeersView, excludeIds:[PeerId] = []) -> [SelectPeerEntry] { +struct SelectPeerValue : Equatable { + + + let peer: Peer + let presence: PeerPresence? + let subscribers: Int? + let customTheme: GeneralRowItem.Theme? + let ignoreStatus: Bool + init(peer: Peer, presence: PeerPresence?, subscribers: Int?, customTheme: GeneralRowItem.Theme? = nil, ignoreStatus: Bool = false) { + self.peer = peer + self.presence = presence + self.subscribers = subscribers + self.customTheme = customTheme + self.ignoreStatus = ignoreStatus + } + + static func == (lhs: SelectPeerValue, rhs: SelectPeerValue) -> Bool { + if !lhs.peer.isEqual(rhs.peer) { + return false + } + if let lhsPresence = lhs.presence, let rhsPresence = rhs.presence { + if !lhsPresence.isEqual(to: rhsPresence) { + return false + } + } else if (lhs.presence != nil) != (rhs.presence != nil) { + return false + } + if lhs.ignoreStatus != rhs.ignoreStatus { + return false + } + + if lhs.subscribers != rhs.subscribers { + return false + } + if lhs.customTheme != rhs.customTheme { + return false + } + + return true + } + + func status(_ account: Account) -> (String?, NSColor) { + + let difference: TimeInterval + if account.network.globalTime > 0 { + difference = floor(account.network.globalTime - Date().timeIntervalSince1970) + } else { + difference = 0 + } + + var color:NSColor = customTheme?.grayTextColor ?? theme.colors.grayText + var string:String = L10n.peerStatusLongTimeAgo + + if let count = subscribers, peer.isGroup || peer.isSupergroup { + let countValue = L10n.privacySettingsGroupMembersCountCountable(count) + string = countValue.replacingOccurrences(of: "\(count)", with: count.separatedNumber) + } else if peer.isGroup || peer.isSupergroup { + return (nil, color) + } else if let presence = presence as? TelegramUserPresence { + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + (string, _, color) = stringAndActivityForUserPresence(presence, timeDifference: difference, relativeTo: Int32(timestamp), customTheme: customTheme) + } else { + if let addressName = peer.addressName { + color = customTheme?.accentColor ?? theme.colors.accent + string = "@\(addressName)" + } + } + if peer.isBot { + string = L10n.presenceBot.lowercased() + } + if ignoreStatus { + return (nil, customTheme?.grayTextColor ?? theme.colors.grayText) + } + return (string, color) + } +} + +private func entriesForView(_ view: ContactPeersView, searchPeers:[PeerId], searchView:MultiplePeersView, excludeIds:[PeerId] = [], linkInvation: ((Int)->Void)? = nil) -> [SelectPeerEntry] { var entries: [SelectPeerEntry] = [] + + if let linkInvation = linkInvation { + let icon = NSImage(named: "Icon_InviteViaLink")!.precomposed(theme.colors.accent, flipVertical: true) + entries.append(SelectPeerEntry.inviteLink(L10n.peerSelectInviteViaLink, icon, 0, GeneralRowItem.Theme(), linkInvation)) + } + //entries.append(.search(false)) if let accountPeer = view.accountPeer { var index:Int32 = 0 @@ -194,7 +244,8 @@ private func entriesForView(_ view: ContactPeersView, searchPeers:[PeerId], sear continue } } - entries.append(.peer(peer,index,searchView.presences[peer.id], !excludeIds.contains(peer.id))) + + entries.append(.peer(SelectPeerValue(peer: peer, presence: searchView.presences[peer.id], subscribers: nil), index, !excludeIds.contains(peer.id))) index += 1 } } @@ -207,16 +258,17 @@ private func entriesForView(_ view: ContactPeersView, searchPeers:[PeerId], sear continue } } - entries.append(.peer(peer,index,view.peerPresences[peer.id], !excludeIds.contains(peer.id))) + + entries.append(.peer(SelectPeerValue(peer: peer, presence: view.peerPresences[peer.id], subscribers: nil), index, !excludeIds.contains(peer.id))) index += 1 - if index == 150 { + if index == 230 { break } } } if entries.count == 1 { - entries.append(.searchEmpty) + entries.append(.searchEmpty(.init(), theme.icons.emptySearch)) } } @@ -224,69 +276,159 @@ private func entriesForView(_ view: ContactPeersView, searchPeers:[PeerId], sear return entries } -private func searchEntriesForPeers(_ peers:[Peer], account:Account, view:MultiplePeersView, isLoading: Bool, excludeIds:[PeerId] = []) -> [SelectPeerEntry] { +private func searchEntriesForPeers(_ peers:[SelectPeerValue], _ global: [SelectPeerValue], account: Account, isLoading: Bool, excludeIds:Set = Set()) -> [SelectPeerEntry] { var entries: [SelectPeerEntry] = [] + var excludeIds = excludeIds var index:Int32 = 0 for peer in peers { - if account.peerId != peer.id { - if let peer = peer as? TelegramUser, let botInfo = peer.botInfo { + if account.peerId != peer.peer.id { + if let peer = peer.peer as? TelegramUser, let botInfo = peer.botInfo { if !botInfo.flags.contains(.worksWithGroups) { continue } } - entries.append(.peer(peer,index,view.presences[peer.id], !excludeIds.contains(peer.id))) + + entries.append(.peer(peer, index, !excludeIds.contains(peer.peer.id))) + excludeIds.insert(peer.peer.id) index += 1 } } - if entries.count == 1 && !isLoading { - entries.append(.searchEmpty) + if !global.isEmpty { + + let global = global.filter { peer in + if account.peerId != peer.peer.id, !excludeIds.contains(peer.peer.id) { + if let peer = peer.peer as? TelegramUser, let botInfo = peer.botInfo { + if !botInfo.flags.contains(.worksWithGroups) { + return false + } + } + return true + } else { + return false + } + } + + if !global.isEmpty { + entries.append(.separator(index, GeneralRowItem.Theme(), L10n.searchSeparatorGlobalPeers)) + index += 1 + + } + + for peer in global { + entries.append(.peer(peer, index, !excludeIds.contains(peer.peer.id))) + excludeIds.insert(peer.peer.id) + index += 1 + } } - + + if entries.isEmpty && !isLoading { + entries.append(.searchEmpty(.init(), theme.icons.emptySearch)) + } + return entries } -fileprivate func prepareEntries(from:[SelectPeerEntry]?, to:[SelectPeerEntry], account:Account, initialSize:NSSize, animated:Bool, interactions:SelectPeerInteraction, singleAction:((Peer)->Void)? = nil) -> TableUpdateTransition { - let (deleted,inserted,updated) = proccessEntriesWithoutReverse(from, right: to, { entry -> TableRowItem in +fileprivate func prepareEntries(from:[SelectPeerEntry]?, to:[SelectPeerEntry], account: Account, initialSize:NSSize, animated:Bool, interactions:SelectPeerInteraction, singleAction:((Peer)->Void)? = nil, scroll: TableScrollState = .none(nil)) -> Signal { + return Signal { subscriber in + var cancelled = false - var item:TableRowItem - - switch entry { - case let .peer(peer, _, presence, enabled): + func makeItem(_ entry: SelectPeerEntry) -> TableRowItem { + var item:TableRowItem - var color:NSColor = theme.colors.grayText - var string:String = tr(.peerStatusRecently) - if let presence = presence as? TelegramUserPresence { - let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - (string, _, color) = stringAndActivityForUserPresence(presence, relativeTo: Int32(timestamp)) + switch entry { + case let .peer(peer, _, enabled): + + + + let interactionType:ShortPeerItemInteractionType + if singleAction != nil { + interactionType = .plain + } else { + interactionType = .selectable(interactions) + } + + let (status, color) = peer.status(account) + + item = ShortPeerRowItem(initialSize, peer: peer.peer, account: account, stableId: entry.stableId, enabled: enabled, titleStyle: ControlStyle(font: .medium(.title), foregroundColor: peer.customTheme?.textColor ?? theme.colors.text, highlightColor: .white), statusStyle: ControlStyle(foregroundColor: color), status: status, isLookSavedMessage: true, drawLastSeparator: true, inset:NSEdgeInsets(left: 10, right:10), interactionType:interactionType, action: { + if let singleAction = singleAction { + singleAction(peer.peer) + } + }, customTheme: peer.customTheme) + case let .searchEmpty(theme, icon): + return SearchEmptyRowItem(initialSize, stableId: entry.stableId, icon: icon, customTheme: theme) + case let .separator(_, customTheme, text): + return SeparatorRowItem(initialSize, entry.stableId, string: text.uppercased(), customTheme: customTheme) + case let .inviteLink(text, image, index, customTheme, action): + let style = ControlStyle(font: .normal(.title), foregroundColor: customTheme.accentColor) + return GeneralInteractedRowItem(initialSize, stableId: entry.stableId, name: text, nameStyle: style, type: .none, action: { + action(index) + interactions.close() + }, thumb: GeneralThumbAdditional(thumb: image, textInset: 39), inset: NSEdgeInsetsMake(0, 16, 0, 10), customTheme: customTheme) } - let interactionType:ShortPeerItemInteractionType - if singleAction != nil { - interactionType = .plain - } else { - interactionType = .selectable(interactions) + let _ = item.makeSize(initialSize.width) + + return item + } + + + + if Thread.isMainThread { + var initialIndex:Int = 0 + var height:CGFloat = 0 + var firstInsertion:[(Int, TableRowItem)] = [] + let entries = Array(to) + + let index:Int = 0 + + for i in index ..< entries.count { + let item = makeItem(to[i]) + height += item.height + firstInsertion.append((i, item)) + if initialSize.height < height { + break + } } - item = ShortPeerRowItem(initialSize, peer: peer, account: account, stableId: entry.stableId, enabled: enabled, statusStyle: ControlStyle(foregroundColor:color), status: string, inset:NSEdgeInsets(left: 10, right:10), interactionType:interactionType, action: { - if let singleAction = singleAction { - singleAction(peer) + + initialIndex = firstInsertion.count + subscriber.putNext(TableUpdateTransition(deleted: [], inserted: firstInsertion, updated: [], state: scroll)) + + prepareQueue.async { + if !cancelled { + + + var insertions:[(Int, TableRowItem)] = [] + let updates:[(Int, TableRowItem)] = [] + + for i in initialIndex ..< entries.count { + let item:TableRowItem + item = makeItem(to[i]) + insertions.append((i, item)) + } + + + subscriber.putNext(TableUpdateTransition(deleted: [], inserted: insertions, updated: updates, state: scroll)) + subscriber.putCompletion() } + } + } else { + let (deleted,inserted,updated) = proccessEntriesWithoutReverse(from, right: to, { entry -> TableRowItem in + return makeItem(entry) }) - case .searchEmpty: - return SearchEmptyRowItem(initialSize, stableId: entry.stableId) - case .separator(_, let text): - return SeparatorRowItem(initialSize, entry.stableId, string: text.uppercased()) + + subscriber.putNext(TableUpdateTransition(deleted: deleted, inserted: inserted, updated:updated, animated:animated, state: scroll)) + subscriber.putCompletion() } - let _ = item.makeSize(initialSize.width) + return ActionDisposable { + cancelled = true + } - return item - - }) + } - return TableUpdateTransition(deleted: deleted, inserted: inserted, updated:updated, animated:animated) } @@ -311,12 +453,16 @@ public struct SelectPeerSettings: OptionSet { if flags.contains(SelectPeerSettings.contacts) { rawValue |= SelectPeerSettings.contacts.rawValue } - + if flags.contains(SelectPeerSettings.excludeBots) { + rawValue |= SelectPeerSettings.excludeBots.rawValue + } self.rawValue = rawValue } - public static let remote = SelectPeerSettings(rawValue: 1) - public static let contacts = SelectPeerSettings(rawValue: 2) + public static let remote = SelectPeerSettings(rawValue: 1 << 1) + public static let contacts = SelectPeerSettings(rawValue: 1 << 2) + public static let groups = SelectPeerSettings(rawValue: 1 << 3) + public static let excludeBots = SelectPeerSettings(rawValue: 1 << 4) } class SelectPeersBehavior { @@ -326,80 +472,214 @@ class SelectPeersBehavior { fileprivate let _peersResult:Atomic<[PeerId:TemporaryPeer]> = Atomic(value: [:]) + var participants:[PeerId:RenderedChannelParticipant] { + return [:] + } + + var okTitle: String? { + return nil + } fileprivate let inSearchSelected:Atomic<[PeerId]> = Atomic(value:[]) fileprivate let settings:SelectPeerSettings fileprivate let excludePeerIds:[PeerId] fileprivate let limit:Int32 - - init(settings:SelectPeerSettings = [.contacts, .remote], excludePeerIds:[PeerId] = [], limit: Int32 = INT32_MAX) { + let customTheme:()->GeneralRowItem.Theme + init(settings:SelectPeerSettings = [.contacts, .remote], excludePeerIds:[PeerId] = [], limit: Int32 = INT32_MAX, customTheme: @escaping()->GeneralRowItem.Theme = { GeneralRowItem.Theme() }) { self.settings = settings self.excludePeerIds = excludePeerIds self.limit = limit + self.customTheme = customTheme } - func start(account: Account, search:Signal) -> Signal<[SelectPeerEntry], Void> { + func start(context: AccountContext, search:Signal, linkInvation: ((Int)->Void)? = nil) -> Signal<([SelectPeerEntry], Bool), NoError> { return .complete() } } -class SelectChannelMembersBehavior : SelectPeersBehavior { + + +class SelectGroupMembersBehavior : SelectPeersBehavior { fileprivate let peerId:PeerId private let _renderedResult:Atomic<[PeerId:RenderedChannelParticipant]> = Atomic(value: [:]) + override var participants:[PeerId:RenderedChannelParticipant] { + return _renderedResult.modify({$0}) + } - var participants:[PeerId:RenderedChannelParticipant] { + init(peerId:PeerId, limit: Int32 = .max, settings: SelectPeerSettings = [.remote]) { + self.peerId = peerId + super.init(settings: settings, limit: limit) + } + + override func start(context: AccountContext, search: Signal, linkInvation: ((Int)->Void)? = nil) -> Signal<([SelectPeerEntry], Bool), NoError> { + let peerId = self.peerId + let _renderedResult = self._renderedResult + let account = context.account + + let previousSearch = Atomic(value: nil) + + return search |> map { SearchState(state: .Focus, request: $0.request) } |> distinctUntilChanged |> mapToSignal { search -> Signal<([SelectPeerEntry], Bool), NoError> in + + let participantsPromise: Promise<[RenderedChannelParticipant]> = Promise() + + + let viewKey = PostboxViewKey.peer(peerId: peerId, components: .all) + + participantsPromise.set(account.postbox.combinedView(keys: [viewKey]) |> map { combinedView in + let peerView = combinedView.views[viewKey] as? PeerView + + if let peerView = peerView { + if let cachedData = peerView.cachedData as? CachedGroupData, let participants = cachedData.participants { + + var creatorPeer: Peer? + for participant in participants.participants { + if let peer = peerView.peers[participant.peerId] { + switch participant { + case .creator: + creatorPeer = peer + default: + break + } + } + } + guard let creator = creatorPeer else { + return [] + } + + return participants.participants.compactMap { participant in + + if let peer = peerView.peers[participant.peerId] { + + let rendered: RenderedChannelParticipant + + switch participant { + case .creator: + rendered = RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: nil), peer: peer) + case .admin: + var peers: [PeerId: Peer] = [:] + peers[creator.id] = creator + peers[peer.id] = peer + rendered = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: .groupSpecific), promotedBy: creator.id, canBeEditedByAccountPeer: creator.id == account.peerId), banInfo: nil, rank: nil), peer: peer, peers: peers) + case .member: + var peers: [PeerId: Peer] = [:] + peers[creator.id] = creator + peers[peer.id] = peer + rendered = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil), peer: peer, peers: peers) + } + + if search.request.isEmpty { + return rendered + } else { + let found = !rendered.peer.displayTitle.lowercased().components(separatedBy: " ").filter {$0.hasPrefix(search.request.lowercased())}.isEmpty + if found { + return rendered + } else { + return nil + } + } + } else { + return nil + } + } + + } + } + return [] + }) + + return participantsPromise.get() |> map { participants in + _ = _renderedResult.swap(participants.toDictionary(with: { $0.peer.id })) + let updatedSearch = previousSearch.swap(search.request) != search.request + return (channelMembersEntries(participants, users: [], remote: [], account: account, isLoading: false), updatedSearch) + } + } + } + + deinit { + _ = _renderedResult.swap([:]) + + } +} + +class SelectChannelMembersBehavior : SelectPeersBehavior { + fileprivate let peerId:PeerId + private let _renderedResult:Atomic<[PeerId:RenderedChannelParticipant]> = Atomic(value: [:]) + private let peerChannelMemberContextsManager: PeerChannelMemberCategoriesContextsManager + private let loadDisposable = MetaDisposable() + override var participants:[PeerId:RenderedChannelParticipant] { return _renderedResult.modify({$0}) } - init(peerId:PeerId, limit: Int32 = .max, settings: SelectPeerSettings = [.remote]) { + init(peerId:PeerId, peerChannelMemberContextsManager: PeerChannelMemberCategoriesContextsManager, limit: Int32 = .max, settings: SelectPeerSettings = [.remote]) { self.peerId = peerId + self.peerChannelMemberContextsManager = peerChannelMemberContextsManager super.init(settings: settings, limit: limit) } - override func start(account: Account, search: Signal) -> Signal<[SelectPeerEntry], Void> { + override func start(context: AccountContext, search: Signal, linkInvation: ((Int)->Void)? = nil) -> Signal<([SelectPeerEntry], Bool), NoError> { let peerId = self.peerId let _renderedResult = self._renderedResult let _peersResult = self._peersResult let settings = self.settings - return search |> map {SearchState(state: .Focus, request: $0.request)} |> distinctUntilChanged |> mapToSignal { search -> Signal<[SelectPeerEntry], Void> in - - let filter:ChannelMembersFilter - - if !search.request.isEmpty { - filter = .search(search.request) + let loadDisposable = self.loadDisposable + let account = context.account + let peerChannelMemberContextsManager = self.peerChannelMemberContextsManager + let previousSearch = Atomic(value: nil) + return search |> mapToSignal { query -> Signal in + if query.request.isEmpty { + return .single(query) } else { - filter = .none + return .single(query) |> delay(0.2, queue: .mainQueue()) } + } |> map { SearchState(state: .Focus, request: $0.request) } |> distinctUntilChanged |> mapToSignal { search -> Signal<([SelectPeerEntry], Bool), NoError> in + + let participantsPromise: Promise<[RenderedChannelParticipant]> = Promise() - let participantsSignal:Signal<[RenderedChannelParticipant]?, Void> = channelMembers(account: account, peerId: peerId, filter: filter) |> map {_ = _renderedResult.swap($0.reduce([:], { current, participant in - var current = current - current[participant.peer.id] = participant - return current - })); return $0} + var isListLoading: Bool = false + let value = peerChannelMemberContextsManager.recent(peerId: peerId, searchQuery: search.request.isEmpty ? nil : search.request, requestUpdate: true, updated: { state in + + let applyList: Bool + + if case .loading = state.loadingState { + isListLoading = true + applyList = search.request.isEmpty + } else { + applyList = true + isListLoading = false + } + if applyList { + participantsPromise.set(.single(state.list)) + _ = _renderedResult.swap(state.list.toDictionary(with: {$0.peer.id})) + } + }) + + + loadDisposable.set(value.0) + let foundLocalPeers = account.postbox.searchContacts(query: search.request.lowercased()) - let foundRemotePeers:Signal<([Peer], Bool), Void> = .single(([], true)) |> then ( searchPeers(account: account, query: search.request.lowercased()) |> map {($0, false)} ) + let foundRemotePeers:Signal<([Peer], [Peer], Bool), NoError> = context.engine.peers.searchPeers(query: search.request.lowercased()) |> map {($0.map{$0.peer}, $1.map{$0.peer}, false)} - let contactsSearch: Signal<([TemporaryPeer], [TemporaryPeer], Bool), Void> + let contactsSearch: Signal<([TemporaryPeer], [TemporaryPeer], Bool), NoError> if settings.contains(.remote) { - contactsSearch = combineLatest(foundLocalPeers, foundRemotePeers) |> map { values -> ([Peer], [Peer], Bool) in - return (values.0, values.1.0, values.1.1 && search.request.length >= 5) + contactsSearch = combineLatest(foundLocalPeers |> map {$0.0}, foundRemotePeers) |> map { values -> ([Peer], [Peer], Bool) in + return (values.0 + values.1.0, values.1.1, values.1.2 && search.request.length >= 5) } - |> mapToSignal { values -> Signal<([Peer], [Peer], MultiplePeersView, Bool), Void> in + |> mapToSignal { values -> Signal<([Peer], [Peer], MultiplePeersView, Bool), NoError> in return account.postbox.multiplePeersView(values.0.map {$0.id}) |> take(1) |> map { views in return (values.0, values.1, views, values.2) } } |> map { value -> ([TemporaryPeer], [TemporaryPeer], Bool) in - let contacts = value.0.map({TemporaryPeer(peer: $0, presence: value.2.presences[$0.id])}) - let global = value.1.map({TemporaryPeer(peer: $0, presence: value.2.presences[$0.id])}) + let contacts = value.0.filter {$0.isUser || ($0.isBot && !settings.contains(.excludeBots))}.map({TemporaryPeer(peer: $0, presence: value.2.presences[$0.id])}) + let global = value.1.filter {$0.isUser || ($0.isBot && !settings.contains(.excludeBots))}.map({TemporaryPeer(peer: $0, presence: value.2.presences[$0.id])}) let _ = _peersResult.swap((contacts + global).reduce([:], { current, peer in var current = current @@ -415,23 +695,27 @@ class SelectChannelMembersBehavior : SelectPeersBehavior { if !search.request.isEmpty { - return combineLatest(participantsSignal, contactsSearch) |> map { participants, peers in - return channelMembersEntries(participants ?? [], users: peers.0, remote: peers.1, account: account, isLoading: participants == nil && peers.2) + return combineLatest(participantsPromise.get(), contactsSearch) |> map { participants, peers in + let updatedSearch = previousSearch.swap(search.request) != search.request + return (channelMembersEntries(participants, users: peers.0, remote: peers.1, account: account, isLoading: isListLoading && peers.2), updatedSearch) } } else { - return participantsSignal |> map { participants in - return channelMembersEntries(participants ?? [], account: account, isLoading: participants == nil) + return participantsPromise.get() |> map { participants in + let updatedSearch = previousSearch.swap(search.request) != search.request + return (channelMembersEntries(participants, account: account, isLoading: isListLoading), updatedSearch) } } } } deinit { + loadDisposable.dispose() _ = _renderedResult.swap([:]) + } } -private func channelMembersEntries(_ participants:[RenderedChannelParticipant], users:[TemporaryPeer]? = nil, remote:[TemporaryPeer] = [], account:Account, isLoading: Bool) -> [SelectPeerEntry] { +private func channelMembersEntries(_ participants:[RenderedChannelParticipant], users:[TemporaryPeer]? = nil, remote:[TemporaryPeer] = [], account: Account, isLoading: Bool) -> [SelectPeerEntry] { var entries: [SelectPeerEntry] = [] var peerIds:[PeerId:PeerId] = [:] @@ -454,39 +738,41 @@ private func channelMembersEntries(_ participants:[RenderedChannelParticipant], var index:Int32 = 0 if !participants.isEmpty { - //entries.append(.separator(index, tr(.channelSelectPeersMembers))) + //entries.append(.separator(index, tr(L10n.channelSelectPeersMembers))) index += 1 for participant in participants { if account.peerId != participant.peer.id { - entries.append(.peer(participant.peer, index, participant.presences[participant.peer.id], true)) + + entries.append(.peer(SelectPeerValue(peer: participant.peer, presence: participant.presences[participant.peer.id], subscribers: nil), index, true)) index += 1 } } } if let users = users, !users.isEmpty { - entries.append(.separator(index, tr(.channelSelectPeersContacts))) + entries.append(.separator(index, GeneralRowItem.Theme(), tr(L10n.channelSelectPeersContacts))) index += 1 for peer in users { if account.peerId != peer.peer.id { - entries.append(.peer(peer.peer, index, peer.presence, true)) + + entries.append(.peer(SelectPeerValue(peer: peer.peer, presence: peer.presence, subscribers: nil), index, true)) index += 1 } } } if !remote.isEmpty { - entries.append(.separator(index, tr(.channelSelectPeersGlobal))) + entries.append(.separator(index, GeneralRowItem.Theme(), tr(L10n.channelSelectPeersGlobal))) index += 1 for peer in remote { if account.peerId != peer.peer.id { - entries.append(.peer(peer.peer, index, peer.presence, true)) + entries.append(.peer(SelectPeerValue(peer: peer.peer, presence: peer.presence, subscribers: nil), index, true)) index += 1 } } } - if entries.count == 1 && !isLoading { - entries.append(.searchEmpty) + if entries.isEmpty && !isLoading { + entries.append(.searchEmpty(.init(), theme.icons.emptySearch)) } return entries @@ -494,18 +780,22 @@ private func channelMembersEntries(_ participants:[RenderedChannelParticipant], final class SelectChatsBehavior: SelectPeersBehavior { - override func start(account: Account, search: Signal) -> Signal<[SelectPeerEntry], Void> { - return search |> distinctUntilChanged |> mapToSignal { search -> Signal<[SelectPeerEntry], Void> in + override func start(context: AccountContext, search: Signal, linkInvation: ((Int)->Void)? = nil) -> Signal<([SelectPeerEntry], Bool), NoError> { + + let previousSearch = Atomic(value: nil) + let account = context.account + return search |> distinctUntilChanged |> mapToSignal { search -> Signal<([SelectPeerEntry], Bool), NoError> in if search.request.isEmpty { - return account.viewTracker.tailChatListView(count: 200) |> deliverOn(prepareQueue) |> mapToQueue { value -> Signal<[SelectPeerEntry], Void> in + + return account.viewTracker.tailChatListView(groupId: .root, count: 200) |> deliverOn(prepareQueue) |> mapToQueue { value -> Signal<([SelectPeerEntry], Bool), NoError> in var entries:[Peer] = [] for entry in value.0.entries.reversed() { switch entry { - case let .MessageEntry(_, _, _, _, _, renderedPeer, _): - if let peer = renderedPeer.chatMainPeer, peer.canSendMessage, peer.canInviteUsers, peer.isSupergroup || peer.isGroup { + case let .MessageEntry(_, _, _, _, _, renderedPeer, _, _, _, _): + if let peer = renderedPeer.chatMainPeer, peer.canSendMessage(false), peer.canInviteUsers, peer.isSupergroup || peer.isGroup { entries.append(peer) } default: @@ -513,37 +803,41 @@ final class SelectChatsBehavior: SelectPeersBehavior { } } - var common:[SelectPeerEntry] = [] + let updatedSearch = previousSearch.swap(search.request) != search.request + if entries.isEmpty { - common.append(.searchEmpty) + return .single(([.searchEmpty(.init(), theme.icons.emptySearch)], updatedSearch)) } else { + var common:[SelectPeerEntry] = [] var index:Int32 = 0 - for peer in entries { - common.append(.peer(peer, index, nil, true)) + for value in entries { + common.append(.peer(SelectPeerValue(peer: value, presence: nil, subscribers: nil), index, true)) index += 1 } - + return .single((common, updatedSearch)) } - return .single(common) } } else { - return account.postbox.searchPeers(query: search.request.lowercased()) |> map { - return $0.flatMap({$0.chatMainPeer}).filter {($0.isSupergroup || $0.isGroup) && $0.canInviteUsers} - } |> deliverOn(prepareQueue) |> map { entries -> [SelectPeerEntry] in + return account.postbox.searchPeers(query: search.request.lowercased()) |> map { + return $0.compactMap({$0.chatMainPeer}).filter {($0.isSupergroup || $0.isGroup) && $0.canInviteUsers} + } |> deliverOn(prepareQueue) |> map { entries -> ([SelectPeerEntry], Bool) in var common:[SelectPeerEntry] = [] + let updatedSearch = previousSearch.swap(search.request) != search.request + + if entries.isEmpty { - common.append(.searchEmpty) + common.append(.searchEmpty(.init(), theme.icons.emptySearch)) } else { var index:Int32 = 0 for peer in entries { - common.append(.peer(peer, index, nil, true)) + common.append(.peer(SelectPeerValue(peer: peer, presence: nil, subscribers: nil), index, true)) index += 1 } } - return common + return (common, updatedSearch) } } @@ -552,38 +846,236 @@ final class SelectChatsBehavior: SelectPeersBehavior { } } + +class SelectUsersAndGroupsBehavior : SelectPeersBehavior { + + + override func start(context: AccountContext, search:Signal, linkInvation: ((Int)->Void)? = nil) -> Signal<([SelectPeerEntry], Bool), NoError> { + + let account = context.account + let previousSearch = Atomic(value: nil) + + return search |> mapToSignal { [weak self] search -> Signal<([SelectPeerEntry], Bool), NoError> in + + let settings = self?.settings ?? SelectPeerSettings() + let excludePeerIds = (self?.excludePeerIds ?? []) + + if search.request.isEmpty { + let inSearch:[PeerId] = self?.inSearchSelected.modify({$0}) ?? [] + + + return account.viewTracker.tailChatListView(groupId: .root, count: 200) |> take(1) |> mapToSignal { view in + let entries = view.0.entries + var peers:[Peer] = [] + var presences:[PeerId : PeerPresence] = [:] + for entry in entries.reversed() { + switch entry { + case let .MessageEntry(_, _, _, _, _, peer, presence, _, _, _): + if let peer = peer.chatMainPeer, !peer.isChannel && !peer.isBot { + peers.append(peer) + if let presence = presence { + presences[peer.id] = presence + } + } + default: + break + } + } + + return account.postbox.transaction { transaction -> [PeerId: CachedPeerData] in + var cachedData:[PeerId: CachedPeerData] = [:] + for peer in peers { + if peer.isSupergroup, let data = transaction.getPeerCachedData(peerId: peer.id) { + cachedData[peer.id] = data + } + } + return cachedData + } |> map { cachedData in + let local = peers.map { peer -> SelectPeerValue in + if let cachedData = cachedData[peer.id] as? CachedChannelData { + let subscribers: Int? + if let count = cachedData.participantsSummary.memberCount { + subscribers = Int(count) + } else { + subscribers = nil + } + return SelectPeerValue(peer: peer, presence: nil, subscribers: subscribers) + } else if let peer = peer as? TelegramGroup { + return SelectPeerValue(peer: peer, presence: nil, subscribers: peer.participantCount) + } else { + return SelectPeerValue(peer: peer, presence: presences[peer.id], subscribers: nil) + } + } + let updatedSearch = previousSearch.swap(search.request) != search.request + + return (searchEntriesForPeers(local, [], account: account, isLoading: false), updatedSearch) + } + } + + } else { + let foundLocalPeers = account.postbox.searchPeers(query: search.request.lowercased()) + let foundRemotePeers:Signal<([Peer], [Peer], Bool), NoError> = settings.contains(.remote) ? .single(([], [], true)) |> then (context.engine.peers.searchPeers(query: search.request.lowercased()) |> map {($0.map{$0.peer}, $1.map{$0.peer}, false)} ) : .single(([], [], false)) + + return combineLatest(foundLocalPeers |> map {$0.compactMap( {$0.chatMainPeer })}, foundRemotePeers) |> map { values -> ([Peer], [Peer], Bool) in + return (uniquePeers(from: values.0), values.1.0 + values.1.1, values.1.2 && search.request.length >= 5) + } + |> runOn(prepareQueue) + |> mapToSignal { values -> Signal<([SelectPeerEntry], Bool), NoError> in + + var values = values + if settings.contains(.excludeBots) { + values.0 = values.0.filter {!$0.isBot} + } + values.0 = values.0.filter { !$0.isChannel } + values.1 = values.1.filter { !$0.isChannel } + + let local = uniquePeers(from: values.0 + values.1) + + return account.postbox.transaction { transaction -> ([PeerId : PeerPresence], [PeerId : CachedPeerData]) in + var presences: [PeerId : PeerPresence] = [:] + var cachedData: [PeerId : CachedPeerData] = [:] + for peer in local { + if peer.isSupergroup { + if let data = transaction.getPeerCachedData(peerId: peer.id) { + cachedData[peer.id] = data + } + } else { + if let presence = transaction.getPeerPresence(peerId: peer.id) { + presences[peer.id] = presence + } + } + } + return (presences, cachedData) + } |> map { (presences, cachedData) -> ([SelectPeerEntry], Bool) in + let local:[SelectPeerValue] = local.map { peer in + if let cachedData = cachedData[peer.id] as? CachedChannelData { + let subscribers: Int? + if let count = cachedData.participantsSummary.memberCount { + subscribers = Int(count) + } else { + subscribers = nil + } + return SelectPeerValue(peer: peer, presence: nil, subscribers: subscribers) + } else if let peer = peer as? TelegramGroup { + return SelectPeerValue(peer: peer, presence: nil, subscribers: peer.participantCount) + } else { + return SelectPeerValue(peer: peer, presence: presences[peer.id], subscribers: nil) + } + } + let updatedSearch = previousSearch.swap(search.request) != search.request + + return (searchEntriesForPeers(local, [], account: account, isLoading: values.2), updatedSearch) + } + + } + } + + } + + } + +} + + fileprivate class SelectContactsBehavior : SelectPeersBehavior { fileprivate let index: PeerNameIndex = .lastNameFirst - + private var previousGlobal:Atomic<[SelectPeerValue]> = Atomic(value: []) - override func start(account: Account, search:Signal) -> Signal<[SelectPeerEntry], Void> { + var defaultSelected: Set = Set() + + deinit { + var bp:Int = 0 + bp += 1 + _ = previousGlobal.swap([]) + } + override func start(context: AccountContext, search:Signal, linkInvation: ((Int)->Void)? = nil) -> Signal<([SelectPeerEntry], Bool), NoError> { - return search |> mapToSignal { [weak self] search -> Signal<[SelectPeerEntry], Void> in + let previousGlobal = self.previousGlobal + let previousSearch = Atomic(value: nil) + let account = context.account + return search |> mapToSignal { [weak self] search -> Signal<([SelectPeerEntry], Bool), NoError> in let settings = self?.settings ?? SelectPeerSettings() let excludePeerIds = (self?.excludePeerIds ?? []) if search.request.isEmpty { let inSearch:[PeerId] = self?.inSearchSelected.modify({$0}) ?? [] - return combineLatest(account.postbox.contactPeersView(accountPeerId: account.peerId), account.postbox.multiplePeersView(inSearch)) + return combineLatest(account.postbox.contactPeersView(accountPeerId: account.peerId, includePresences: true), account.postbox.multiplePeersView(inSearch)) |> deliverOn(prepareQueue) - |> mapToQueue { view, searchView -> Signal<[SelectPeerEntry], Void> in - return .single(entriesForView(view, searchPeers: inSearch, searchView: searchView, excludeIds: excludePeerIds)) + |> map { view, searchView -> ([SelectPeerEntry], Bool) in + let updatedSearch = previousSearch.swap(search.request) != search.request + return (entriesForView(view, searchPeers: inSearch, searchView: searchView, excludeIds: excludePeerIds, linkInvation: linkInvation), updatedSearch) } } else { let foundLocalPeers = account.postbox.searchContacts(query: search.request.lowercased()) + let foundRemotePeers:Signal<([Peer], [Peer], Bool), NoError> = settings.contains(.remote) ? .single(([], [], true)) |> then (context.engine.peers.searchPeers(query: search.request.lowercased()) |> map {($0.map{$0.peer}, $1.map{$0.peer}, false)} ) : .single(([], [], false)) - let foundRemotePeers:Signal<([Peer], Bool), Void> = settings.contains(.remote) ? .single(([], true)) |> then ( searchPeers(account: account, query: search.request.lowercased()) |> map {($0, false)} ) : .single(([], false)) - - return combineLatest(foundLocalPeers, foundRemotePeers) |> map { values -> ([Peer], Bool) in - return (uniquePeers(from: (values.0 + values.1.0)), values.1.1 && search.request.length >= 5) - } + return combineLatest(foundLocalPeers |> map {$0.0}, foundRemotePeers) |> map { values -> ([Peer], [Peer], Bool) in + return (uniquePeers(from: values.0), values.1.0 + values.1.1, values.1.2 && search.request.length >= 5) + } |> runOn(prepareQueue) - |> mapToSignal { values -> Signal<[SelectPeerEntry], Void> in - return account.postbox.multiplePeersView(values.0.map {$0.id}) |> take(1) |> map { view -> [SelectPeerEntry] in - return searchEntriesForPeers(values.0, account: account, view: view, isLoading: values.1, excludeIds: excludePeerIds) + |> mapToSignal { values -> Signal<([SelectPeerEntry], Bool), NoError> in + var values = values + if settings.contains(.excludeBots) { + values.0 = values.0.filter {!$0.isBot} + } + values.0 = values.0.filter {!$0.isChannel && (settings.contains(.groups) || (!$0.isSupergroup && !$0.isGroup))} + values.1 = values.1.filter {!$0.isChannel && (settings.contains(.groups) || (!$0.isSupergroup && !$0.isGroup))} + let local = values.0 + let global = values.1 + + return account.postbox.transaction { transaction -> [PeerId : PeerPresence] in + var presences: [PeerId : PeerPresence] = [:] + for peer in local { + if let presence = transaction.getPeerPresence(peerId: peer.id) { + presences[peer.id] = presence + } + } + return presences + } |> map { presences -> ([SelectPeerEntry], Bool) in + let local:[SelectPeerValue] = local.map { peer in + return SelectPeerValue(peer: peer, presence: presences[peer.id], subscribers: nil) + } + + var filteredLocal:[SelectPeerValue] = [] + var excludeIds = Set() + for peer in local { + if account.peerId != peer.peer.id { + if let peer = peer.peer as? TelegramUser, let botInfo = peer.botInfo { + if !botInfo.flags.contains(.worksWithGroups) { + continue + } + } + excludeIds.insert(peer.peer.id) + filteredLocal.append(peer) + } + } + + var global:[SelectPeerValue] = global.map { peer in + return SelectPeerValue(peer: peer, presence: presences[peer.id], subscribers: nil) + }.filter { peer in + if account.peerId != peer.peer.id, !excludeIds.contains(peer.peer.id) { + if let peer = peer.peer as? TelegramUser, let botInfo = peer.botInfo { + if !botInfo.flags.contains(.worksWithGroups) { + return false + } + } + return true + } else { + return false + } + } + + + if !global.isEmpty { + _ = previousGlobal.swap(global) + } else { + global = previousGlobal.with { $0 } + } + let updatedSearch = previousSearch.swap(search.request) != search.request + return (searchEntriesForPeers(local, global, account: account, isLoading: values.2), updatedSearch) } } } @@ -609,11 +1101,12 @@ final class SelectPeersControllerView: View, TokenizedProtocol { addSubview(separatorView) tokenView.delegate = self needsLayout = true - updateLocalizationAndTheme() + updateLocalizationAndTheme(theme: theme) } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + backgroundColor = theme.colors.background separatorView.backgroundColor = theme.colors.border } @@ -637,7 +1130,71 @@ final class SelectPeersControllerView: View, TokenizedProtocol { } } -class SelectPeersController: ComposeViewController<[PeerId], Void, SelectPeersControllerView>, Notifable { +class SelectPeersMainController: GenericViewController where R:NSView { + let onChange:Promise = Promise() + let onComplete:Promise = Promise() + let onCancel:Promise = Promise() + var previousResult:ComposeState? = nil + func restart(with result:ComposeState) { + self.previousResult = result + } + + let titles:ComposeTitles + fileprivate(set) var enableNext:Bool = true + + override func getRightBarViewOnce() -> BarView { + return TextButtonBarView(controller: self, text: titles.done, style: navigationButtonStyle, alignment:.Right) + } + + override func executeReturn() -> Void { + onCancel.set(Signal.single(Void())) + super.executeReturn() + } + + override func requestUpdateRightBar() { + super.requestUpdateRightBar() + rightBarView.style = navigationButtonStyle + } + + public override func returnKeyAction() -> KeyHandlerResult { + self.executeNext() + return .invoked + } + + func nextEnabled(_ enable:Bool) { + self.enableNext = enable + rightBarView.isEnabled = enable + } + + func executeNext() -> Void { + + } + + override var enableBack: Bool { + return true + } + + override func loadView() { + super.loadView() + + setCenterTitle(titles.center) + self.rightBarView.set(handler:{ [weak self] _ in + self?.executeNext() + }, for: .Click) + } + + let context: AccountContext + + public init(titles:ComposeTitles, context: AccountContext) { + self.titles = titles + self.context = context + super.init() + } + +} + +class SelectPeersController: SelectPeersMainController<[PeerId], Void, SelectPeersControllerView>, Notifable { + private let behavior:SelectContactsBehavior private let search:Promise = Promise() @@ -645,6 +1202,21 @@ class SelectPeersController: ComposeViewController<[PeerId], Void, SelectPeersCo let interactions:SelectPeerInteraction = SelectPeerInteraction() private var previous:Atomic<[SelectPeerEntry]?> = Atomic(value:nil) private let tokenDisposable: MetaDisposable = MetaDisposable() + private let isNewGroup: Bool + private var limitsConfiguration: LimitsConfiguration? { + didSet { + if oldValue == nil { + requestUpdateCenterBar() + return + } + if let limitsConfiguration = limitsConfiguration { + self.interactions.update({$0.withUpdateLimit(limitsConfiguration.maxGroupMemberCount)}) + if limitsConfiguration.isEqual(to: oldValue!) == false { + requestUpdateCenterBar() + } + } + } + } func notify(with value: Any, oldValue: Any, animated: Bool) { if let value = value as? SelectPeerPresentation, let oldValue = oldValue as? SelectPeerPresentation { @@ -652,30 +1224,61 @@ class SelectPeersController: ComposeViewController<[PeerId], Void, SelectPeersCo let added = value.selected.subtracting(oldValue.selected) let removed = oldValue.selected.subtracting(value.selected) - for item in added { - genericView.tokenView.addToken(token: SearchToken(name: value.peers[item]?.compactDisplayTitle ?? tr(.peerDeletedUser), uniqueId: item.toInt64()), animated: animated) + if added.count == 0 && value.isLimitReached { + alert(for: mainWindow, info: L10n.composeCreateGroupLimitError) + } + + let tokens = added.map { + return SearchToken(name: value.peers[$0]?.compactDisplayTitle ?? L10n.peerDeletedUser, uniqueId: $0.toInt64()) } + genericView.tokenView.addTokens(tokens: tokens, animated: animated) - for item in removed { - genericView.tokenView.removeToken(uniqueId: item.toInt64(), animated: animated) + let idsToRemove:[Int64] = removed.map { + $0.toInt64() } + genericView.tokenView.removeTokens(uniqueIds: idsToRemove, animated: animated) self.nextEnabled(!value.selected.isEmpty) + + + + if let limits = limitsConfiguration { + let attributed = NSMutableAttributedString() + _ = attributed.append(string: L10n.telegramSelectPeersController, color: theme.colors.text, font: .medium(.title)) + _ = attributed.append(string: " ") + _ = attributed.append(string: "\(interactions.presentation.selected.count.formattedWithSeparator)/\(limits.maxSupergroupMemberCount.formattedWithSeparator)", color: theme.colors.grayText, font: .normal(.title)) + self.centerBarView.text = attributed + } else { + setCenterTitle(defaultBarTitle) + } + } } + override func requestUpdateCenterBar() { + notify(with: interactions.presentation, oldValue: interactions.presentation, animated: false) + } + func isEqual(to other: Notifable) -> Bool { if other is SelectPeersController { return true } return false } + + override func returnKeyAction() -> KeyHandlerResult { + if !self.interactions.presentation.selected.isEmpty { + return super.returnKeyAction() + } + return .rejected + } override func viewDidLoad() { super.viewDidLoad() self.nextEnabled(false) - let account = self.account + let context = self.context + let account = self.context.account let interactions = self.interactions interactions.add(observer: self) @@ -694,25 +1297,52 @@ class SelectPeersController: ComposeViewController<[PeerId], Void, SelectPeersCo let previous = self.previous let initialSize = atomicSize - let transition = behavior.start(account: account, search: search.get() |> distinctUntilChanged |> map {SearchState(state: .None, request: $0)}) |> deliverOn(prepareQueue) |> map { entries -> TableUpdateTransition in - return prepareEntries(from: previous.swap(entries), to: entries, account: account, initialSize: initialSize.modify({$0}), animated: true, interactions:interactions) + let limitsSignal:Signal = isNewGroup ? account.postbox.preferencesView(keys: [PreferencesKeys.limitsConfiguration]) |> map { values -> LimitsConfiguration? in + return values.values[PreferencesKeys.limitsConfiguration] as? LimitsConfiguration + } : .single(nil) + + let first: Atomic = Atomic(value: true) + + let transition = combineLatest(queue: prepareQueue, behavior.start(context: context, search: search.get() |> distinctUntilChanged |> map {SearchState(state: .None, request: $0)}), limitsSignal) |> mapToQueue { entries, limits -> Signal<(TableUpdateTransition, LimitsConfiguration?), NoError> in + return prepareEntries(from: previous.swap(entries.0), to: entries.0, account: account, initialSize: initialSize.modify({$0}), animated: false, interactions: interactions, scroll: entries.1 ? .up(false) : .none(nil)) |> runOn(first.swap(false) ? .mainQueue() : prepareQueue) |> map { ($0, limits) } } |> deliverOnMainQueue - disposable.set(transition.start(next: { [weak self] transition in - self?.genericView.tableView.merge(with: transition) + disposable.set(transition.start(next: { [weak self] transition, limits in self?.readyOnce() + self?.genericView.tableView.merge(with: transition) + self?.limitsConfiguration = limits })) } + var tokenView:TokenizedView { return genericView.tokenView } - init(titles: ComposeTitles, account: Account, settings:SelectPeerSettings = [.contacts], excludePeerIds:[PeerId] = [], limit: Int32 = INT32_MAX) { - self.behavior = SelectContactsBehavior(settings: settings, excludePeerIds: excludePeerIds, limit: limit) - super.init(titles: titles, account: account) + init(titles: ComposeTitles, context: AccountContext, settings:SelectPeerSettings = [.contacts], excludePeerIds:[PeerId] = [], limit: Int32 = INT32_MAX, isNewGroup: Bool = false, selectedPeers:Set = Set()) { + let behavior = SelectContactsBehavior(settings: settings, excludePeerIds: excludePeerIds, limit: limit) + self.behavior = behavior + self.isNewGroup = isNewGroup + super.init(titles: titles, context: context) + + let peers = context.account.postbox.transaction { transaction in + return selectedPeers.map { + transaction.getPeer($0) + }.compactMap { $0 } + } |> deliverOnMainQueue + + _ = peers.start(next: { [weak self] peers in + self?.interactions.update { state in + var state = state + for peer in peers { + state = state.withToggledSelected(peer.id, peer: peer) + } + return state + } + }) + } override func firstResponder() -> NSResponder? { @@ -739,30 +1369,63 @@ fileprivate class SelectPeersView : View, TokenizedProtocol { let tableView:TableView = TableView() let tokenView: TokenizedView let separatorView: View = View() + + var customTheme: (()->GeneralRowItem.Theme)? = nil { + didSet { + updateLocalizationAndTheme(theme: theme) + } + } + required init(frame frameRect: NSRect) { + + var makeTheme:()->TokenizedView.Theme = { TokenizedView.Theme() } + tokenView = TokenizedView(frame: NSMakeRect(0, 0, frameRect.width - 20, 30), localizationFunc: { key in return translate(key: key, []) - }, placeholderKey: "SearchField.Search") + }, placeholderKey: "SearchField.Search", customTheme: { + return makeTheme() + }) super.init(frame: frameRect) addSubview(tokenView) addSubview(tableView) addSubview(separatorView) tokenView.delegate = self - backgroundColor = theme.colors.background - needsLayout = true - updateLocalizationAndTheme() + + makeTheme = { [weak self] in + if let custom = self?.customTheme?() { + return TokenizedView.Theme(background: custom.backgroundColor, + grayBackground: custom.grayBackground, + textColor: custom.textColor, + grayTextColor: custom.grayTextColor, + underSelectColor: custom.underSelectedColor, accentColor: custom.accentColor, + accentSelectColor: custom.accentSelectColor, + redColor: custom.redColor) + } else { + return TokenizedView.Theme() + } + } + + tableView.getBackgroundColor = { [weak self] in + return self?.customTheme?().backgroundColor ?? theme.colors.background + } + + updateLocalizationAndTheme(theme: theme) + layout() } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() - separatorView.backgroundColor = theme.colors.border + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + separatorView.backgroundColor = customTheme?().backgroundColor ?? theme.colors.border + backgroundColor = customTheme?().backgroundColor ?? theme.colors.background + + } fileprivate override func layout() { super.layout() tableView.frame = NSMakeRect(0, 50, frame.width , frame.height - 50) - tokenView.frame = NSMakeRect(10, 10, frame.width - 20, frame.height - 50) + tokenView.frame = NSMakeRect(10, 10, frame.width - 20, 50) } func tokenizedViewDidChangedHeight(_ view: TokenizedView, height: CGFloat, animated: Bool) { @@ -786,9 +1449,9 @@ private class SelectPeersModalController : ModalViewController, Notifable { private let disposable:MetaDisposable = MetaDisposable() let interactions:SelectPeerInteraction = SelectPeerInteraction() private var previous:Atomic<[SelectPeerEntry]?> = Atomic(value:nil) - private let account:Account + private let context: AccountContext private let defaultTitle:String - private let confirmation:([PeerId])->Signal + private let confirmation:([PeerId])->Signal fileprivate let onComplete:Promise<[PeerId]> = Promise() private let completeDisposable = MetaDisposable() private let tokenDisposable = MetaDisposable() @@ -799,14 +1462,15 @@ private class SelectPeersModalController : ModalViewController, Notifable { let added = value.selected.subtracting(oldValue.selected) let removed = oldValue.selected.subtracting(value.selected) - for item in added { - genericView.tokenView.addToken(token: SearchToken(name: value.peers[item]?.compactDisplayTitle ?? tr(.peerDeletedUser), uniqueId: item.toInt64()), animated: animated) + let tokens = added.map { + return SearchToken(name: value.peers[$0]?.compactDisplayTitle ?? L10n.peerDeletedUser, uniqueId: $0.toInt64()) } + genericView.tokenView.addTokens(tokens: tokens, animated: animated) - for item in removed { - genericView.tokenView.removeToken(uniqueId: item.toInt64(), animated: animated) + let idsToRemove:[Int64] = removed.map { + $0.toInt64() } - + genericView.tokenView.removeTokens(uniqueIds: idsToRemove, animated: animated) modal?.interactions?.updateEnables(!value.selected.isEmpty) } @@ -821,7 +1485,19 @@ private class SelectPeersModalController : ModalViewController, Notifable { return tokenView } + override var dynamicSize: Bool { + return true + } + override open func measure(size: NSSize) { + self.modal?.resize(with:NSMakeSize(genericView.frame.width, min(size.height - 120, max(genericView.tableView.listHeight, 350))), animated: false) + } + + public func updateSize(_ animated: Bool) { + if let contentSize = self.modal?.window.contentView?.frame.size { + self.modal?.resize(with:NSMakeSize(genericView.frame.width, min(contentSize.height - 120, max(genericView.tableView.listHeight, 350))), animated: animated) + } + } override func escapeKeyAction() -> KeyHandlerResult { return .rejected @@ -853,10 +1529,16 @@ private class SelectPeersModalController : ModalViewController, Notifable { override func viewDidLoad() { super.viewDidLoad() - let account = self.account + let context = self.context let interactions = self.interactions let initialSize = atomicSize + let account = context.account + genericView.customTheme = behavior.customTheme + + interactions.close = { [weak self] in + self?.close() + } interactions.add(observer: self) @@ -877,30 +1559,37 @@ private class SelectPeersModalController : ModalViewController, Notifable { if behavior.limit == 1 { singleAction = { [weak self] peer in - _ = (self?.account.postbox.modify { modifier -> Void in - updatePeers(modifier: modifier, peers: [peer], update: { _, updated -> Peer? in + _ = (account.postbox.transaction { transaction -> Void in + updatePeers(transaction: transaction, peers: [peer], update: { _, updated -> Peer? in return updated }) - })?.start() - self?.confirmSelected([peer.id], [peer]) + }).start() + self?.confirmSelected([peer.id], [peer]) } } - let transition = behavior.start(account: account, search: search.get() |> distinctUntilChanged) |> deliverOn(prepareQueue) |> map { entries -> TableUpdateTransition in - return prepareEntries(from: previous.swap(entries), to: entries, account: account, initialSize: initialSize.modify({$0}), animated: true, interactions:interactions, singleAction: singleAction) + let first: Atomic = Atomic(value: true) + + + let transition = behavior.start(context: context, search: search.get() |> distinctUntilChanged, linkInvation: linkInvation) |> mapToQueue { entries, updateSearch -> Signal in + return prepareEntries(from: previous.swap(entries), to: entries, account: account, initialSize: initialSize.modify({$0}), animated: false, interactions:interactions, singleAction: singleAction, scroll: updateSearch ? .up(false) : .none(nil)) |> runOn(first.swap(false) ? .mainQueue() : prepareQueue) } |> deliverOnMainQueue disposable.set(transition.start(next: { [weak self] transition in self?.genericView.tableView.merge(with: transition) self?.readyOnce() + + })) } + private let linkInvation: ((Int)->Void)? - init(account: Account, title:String, settings:SelectPeerSettings = [.contacts, .remote], excludePeerIds:[PeerId] = [], limit: Int32 = INT32_MAX, confirmation:@escaping([PeerId])->Signal, behavior: SelectPeersBehavior? = nil) { - self.account = account + init(context: AccountContext, title:String, settings:SelectPeerSettings = [.contacts, .remote], excludePeerIds:[PeerId] = [], limit: Int32 = INT32_MAX, confirmation:@escaping([PeerId])->Signal, behavior: SelectPeersBehavior? = nil, linkInvation:((Int)->Void)? = nil) { + self.context = context self.defaultTitle = title self.confirmation = confirmation + self.linkInvation = linkInvation self.behavior = behavior ?? SelectContactsBehavior(settings: settings, excludePeerIds: excludePeerIds, limit: limit) super.init(frame: NSMakeRect(0, 0, 360, 380)) @@ -911,11 +1600,11 @@ private class SelectPeersModalController : ModalViewController, Notifable { } func confirmSelected(_ peerIds:[PeerId], _ peers:[Peer]) { - let signal = account.postbox.modify { modifier -> Void in - updatePeers(modifier: modifier, peers: peers, update: { (_, updated) -> Peer? in + let signal = context.account.postbox.transaction { transaction -> Void in + updatePeers(transaction: transaction, peers: peers, update: { (_, updated) -> Peer? in return updated }) - } |> deliverOnMainQueue |> mapToSignal { [weak self] () -> Signal<[PeerId], Void> in + } |> deliverOnMainQueue |> mapToSignal { [weak self] () -> Signal<[PeerId], NoError> in if let strongSelf = self { return strongSelf.confirmation(peerIds) |> filter {$0} |> map { _ -> [PeerId] in return peerIds @@ -926,15 +1615,38 @@ private class SelectPeersModalController : ModalViewController, Notifable { onComplete.set(signal) } + override func returnKeyAction() -> KeyHandlerResult { + if !interactions.presentation.peers.values.isEmpty { + self.confirmSelected(Array(interactions.presentation.selected), Array(interactions.presentation.peers.values)) + } + + return .invoked + } + + override var modalHeader: (left: ModalHeaderData?, center: ModalHeaderData?, right: ModalHeaderData?)? { + return (left: ModalHeaderData(image: #imageLiteral(resourceName: "Icon_ChatSearchCancel").precomposed(behavior.customTheme().accentColor), handler: { [weak self] in + self?.close() + }), center: ModalHeaderData(title: self.defaultTitle), right: nil) + } + + override var modalTheme: ModalViewController.Theme { + let customTheme = behavior.customTheme() + return .init(text: customTheme.textColor, grayText: customTheme.grayTextColor, background: customTheme.backgroundColor, border: customTheme.borderColor, accent: customTheme.accentColor, grayForeground: customTheme.grayBackground) + } + override var modalInteractions: ModalInteractions? { if behavior.limit == 1 { - return ModalInteractions(acceptTitle: tr(.modalCancel), drawBorder: true, height: 40) + return nil } else { - return ModalInteractions(acceptTitle: tr(.modalOK), accept: { [weak self] in + return ModalInteractions(acceptTitle: behavior.okTitle ?? L10n.modalOK, accept: { [weak self] in if let interactions = self?.interactions { self?.confirmSelected(Array(interactions.presentation.selected), Array(interactions.presentation.peers.values)) } - }, cancelTitle: tr(.modalCancel), drawBorder: true, height: 40) + }, drawBorder: true, height: 50, singleButton: true, customTheme: { [weak self] in + return self?.modalTheme ?? .init() + }) + + } } @@ -949,11 +1661,11 @@ private class SelectPeersModalController : ModalViewController, Notifable { } -func selectModalPeers(account:Account, title:String , settings:SelectPeerSettings = [.contacts, .remote], excludePeerIds:[PeerId] = [], limit: Int32 = INT_MAX, behavior: SelectPeersBehavior? = nil, confirmation:@escaping ([PeerId]) -> Signal = {_ in return .single(true) }) -> Signal<[PeerId], Void> { +func selectModalPeers(window: Window, context: AccountContext, title:String , settings:SelectPeerSettings = [.contacts, .remote], excludePeerIds:[PeerId] = [], limit: Int32 = INT_MAX, behavior: SelectPeersBehavior? = nil, confirmation:@escaping ([PeerId]) -> Signal = {_ in return .single(true) }, linkInvation:((Int)->Void)? = nil) -> Signal<[PeerId], NoError> { - let modal = SelectPeersModalController(account: account, title: title, settings: settings, excludePeerIds: excludePeerIds, limit: limit, confirmation: confirmation, behavior: behavior) + let modal = SelectPeersModalController(context: context, title: title, settings: settings, excludePeerIds: excludePeerIds, limit: limit, confirmation: confirmation, behavior: behavior, linkInvation: linkInvation) - showModal(with: modal, for: mainWindow) + showModal(with: modal, for: window) return modal.onComplete.get() |> take(1) diff --git a/Telegram-Mac/SelectSizeRowItem.swift b/Telegram-Mac/SelectSizeRowItem.swift new file mode 100644 index 0000000000..e6e3998b4f --- /dev/null +++ b/Telegram-Mac/SelectSizeRowItem.swift @@ -0,0 +1,394 @@ +// +// SelectSizeRowItem.swift +// Telegram +// +// Created by keepcoder on 15/12/2017. +// Copyright © 2017 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + + + +class SelectSizeRowItem: GeneralRowItem { + + fileprivate let titles:[String]? + fileprivate let sizes: [Int32] + fileprivate var current: Int32 + fileprivate let initialCurrent: Int32 + fileprivate let selectAction:(Int)->Void + fileprivate let hasMarkers: Bool + fileprivate let dottedIndexes: [Int] + init(_ initialSize: NSSize, stableId: AnyHashable, current: Int32, sizes: [Int32], hasMarkers: Bool, titles:[String]? = nil, dottedIndexes:[Int] = [], viewType: GeneralViewType = .legacy, selectAction: @escaping(Int)->Void) { + self.sizes = sizes + self.titles = titles + self.dottedIndexes = dottedIndexes + self.initialCurrent = current + self.hasMarkers = hasMarkers + self.current = current + self.selectAction = selectAction + super.init(initialSize, height: titles != nil ? 70 : 40, stableId: stableId, viewType: viewType, inset: NSEdgeInsets(left: 30, right: 30)) + } + + override func viewClass() -> AnyClass { + return SelectSizeRowView.self + } + + +} + +private class SelectSizeRowView : TableRowView, ViewDisplayDelegate { + + private var availableRects:[NSRect] = [] + private let containerView = GeneralRowContainerView(frame: NSZeroRect) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + self.addSubview(containerView) + containerView.displayDelegate = self + containerView.userInteractionEnabled = false + containerView.isEventLess = true + layerContentsRedrawPolicy = .onSetNeedsDisplay + } + + + + + override func mouseDragged(with event: NSEvent) { + guard let item = item as? SelectSizeRowItem, !item.sizes.isEmpty else { + super.mouseDragged(with: event) + return + } + + if item.sizes.count == availableRects.count { + let point = containerView.convert(event.locationInWindow, from: nil) + for i in 0 ..< availableRects.count { + if NSPointInRect(point, availableRects[i]), item.current != i { + item.current = item.sizes[i] + containerView.needsDisplay = true + } + } + } + } + + override func mouseUp(with event: NSEvent) { + guard let item = item as? SelectSizeRowItem, !item.sizes.isEmpty else { + super.mouseUp(with: event) + return + } + + if item.sizes.count == availableRects.count { + let point = containerView.convert(event.locationInWindow, from: nil) + for i in 0 ..< availableRects.count { + if NSPointInRect(point, availableRects[i]), item.sizes.firstIndex(of: item.current) != i { + item.selectAction(i) + return + } + } + if item.initialCurrent != item.current { + item.selectAction(item.sizes.firstIndex(of: item.current)!) + } + } + + } + + func _focus(_ size: NSSize) -> NSRect { + var focus = self.containerView.focus(size) + if let item = item as? SelectSizeRowItem { + switch item.viewType { + case .legacy: + if item.titles != nil { + focus.origin.y += 20 + } + case let .modern(_, insets): + if item.titles != nil { + focus.origin.y += (24 - insets.bottom) + } + } + + } + return focus + } + + + override func draw(_ layer: CALayer, in ctx: CGContext) { + super.draw(layer, in: ctx) + + guard let item = item as? SelectSizeRowItem, !item.sizes.isEmpty, containerView.layer == layer else {return} + + switch item.viewType { + case .legacy: + let minFontSize = CGFloat(item.sizes.first!) + let maxFontSize = CGFloat(item.sizes.last!) + + let minNode = TextNode.layoutText(.initialize(string: "A", color: theme.colors.text, font: .normal(min(minFontSize, 11))), backdorColor, 1, .end, NSMakeSize(.greatestFiniteMagnitude, .greatestFiniteMagnitude), nil, false, .left) + + let maxNode = TextNode.layoutText(.initialize(string: "A", color: theme.colors.text, font: .normal(min(maxFontSize, 15))), backdorColor, 1, .end, NSMakeSize(.greatestFiniteMagnitude, .greatestFiniteMagnitude), nil, false, .left) + + let minF = _focus(item.hasMarkers ? minNode.0.size : NSZeroSize) + let maxF = _focus(item.hasMarkers ? maxNode.0.size : NSZeroSize) + + if item.hasMarkers { + minNode.1.draw(NSMakeRect(item.inset.left, minF.minY, minF.width, minF.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) + maxNode.1.draw(NSMakeRect(containerView.frame.width - item.inset.right - maxF.width, maxF.minY, maxF.width, maxF.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) + } + + let count = CGFloat(item.sizes.count) + + let insetBetweenFont: CGFloat = item.hasMarkers ? 20 : 0 + + let width: CGFloat = containerView.frame.width - (item.inset.left + minF.width) - (item.inset.right + maxF.width) - insetBetweenFont * 2 + + let per = floorToScreenPixels(backingScaleFactor, width / (count - 1)) + + ctx.setFillColor(theme.colors.accent.cgColor) + let lineSize = NSMakeSize(width, 2) + let lc = _focus(lineSize) + let minX = item.inset.left + minF.width + insetBetweenFont + + let interactionRect = NSMakeRect(minX, lc.minY, lc.width, lc.height) + + ctx.fill(interactionRect) + + let current: CGFloat = CGFloat(item.sizes.firstIndex(of: item.current) ?? 0) + + let selectSize = NSMakeSize(20, 20) + + let selectPoint = NSMakePoint(minX + floorToScreenPixels(backingScaleFactor, interactionRect.width / CGFloat(item.sizes.count - 1)) * current - selectSize.width / 2, _focus(selectSize).minY) + + ctx.setFillColor(theme.colors.grayText.cgColor) + let unMinX = selectPoint.x + selectSize.width / 2 + ctx.fill(NSMakeRect(unMinX, lc.minY, lc.maxX - unMinX, lc.height)) + + + for i in 0 ..< item.sizes.count { + let perSize = NSMakeSize(10, 10) + let perF = _focus(perSize) + let point = NSMakePoint(minX + per * CGFloat(i) - (i > 0 ? perSize.width / 2 : 0), perF.minY) + ctx.setFillColor(theme.colors.background.cgColor) + ctx.fill(NSMakeRect(point.x, point.y, perSize.width, perSize.height)) + + ctx.setFillColor(item.sizes[i] <= item.current ? theme.colors.accent.cgColor : theme.colors.grayText.cgColor) + ctx.fillEllipse(in: NSMakeRect(point.x + perSize.width/2 - 2, point.y + 3, 4, 4)) + + if let titles = item.titles, titles.count == item.sizes.count { + let title = titles[i] + let titleNode = TextNode.layoutText(.initialize(string: title, color: theme.colors.text, font: .normal(.short)), backdorColor, 1, .end, NSMakeSize(.greatestFiniteMagnitude, .greatestFiniteMagnitude), nil, false, .left) + titleNode.1.draw(NSMakeRect(min(max(point.x - titleNode.0.size.width / 2 + 3, minX), frame.width - titleNode.0.size.width - minX), point.y - 15 - titleNode.0.size.height, titleNode.0.size.width, titleNode.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) + } + } + + if let titles = item.titles, titles.count == 1, let title = titles.first { + let perSize = NSMakeSize(10, 10) + let perF = _focus(perSize) + let titleNode = TextNode.layoutText(.initialize(string: title, color: theme.colors.text, font: .normal(.short)), backdorColor, 1, .end, NSMakeSize(.greatestFiniteMagnitude, .greatestFiniteMagnitude), nil, false, .left) + titleNode.1.draw(NSMakeRect(_focus(titleNode.0.size).minX, perF.minY - 15 - titleNode.0.size.height, titleNode.0.size.width, titleNode.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) + } + + + ctx.setFillColor(theme.colors.border.cgColor) + ctx.fillEllipse(in: NSMakeRect(selectPoint.x, selectPoint.y, selectSize.width, selectSize.height)) + + ctx.setFillColor(.white) + ctx.fillEllipse(in: NSMakeRect(selectPoint.x + 1, selectPoint.y + 1, selectSize.width - 2, selectSize.height - 2)) + + resetCursorRects() + availableRects.removeAll() + + for i in 0 ..< item.sizes.count { + let perF = _focus(selectSize) + let point = NSMakePoint(interactionRect.minX + floorToScreenPixels(backingScaleFactor, interactionRect.width / (count - 1)) * CGFloat(i) - selectSize.width / 2, perF.minY) + let rect = NSMakeRect(point.x, point.y, selectSize.width, selectSize.height) + addCursorRect(rect, cursor: NSCursor.pointingHand) + availableRects.append(rect) + } + case let .modern(_, insets): + let minFontSize = CGFloat(item.sizes.first!) + let maxFontSize = CGFloat(item.sizes.last!) + + let minNode = TextNode.layoutText(.initialize(string: "A", color: theme.colors.text, font: .normal(min(minFontSize, 11))), backdorColor, 1, .end, NSMakeSize(.greatestFiniteMagnitude, .greatestFiniteMagnitude), nil, false, .left) + + let maxNode = TextNode.layoutText(.initialize(string: "A", color: theme.colors.text, font: .normal(min(maxFontSize, 15))), backdorColor, 1, .end, NSMakeSize(.greatestFiniteMagnitude, .greatestFiniteMagnitude), nil, false, .left) + + let minF = _focus(item.hasMarkers ? minNode.0.size : NSZeroSize) + let maxF = _focus(item.hasMarkers ? maxNode.0.size : NSZeroSize) + + if item.hasMarkers { + minNode.1.draw(NSMakeRect(insets.left, minF.minY, minF.width, minF.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) + maxNode.1.draw(NSMakeRect(containerView.frame.width - insets.right - maxF.width, maxF.minY, maxF.width, maxF.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) + } + + let count = CGFloat(item.sizes.count) + + let insetBetweenFont: CGFloat = item.hasMarkers ? 20 : 0 + + let width: CGFloat = containerView.frame.width - (insets.left + minF.width) - (insets.right + maxF.width) - insetBetweenFont * 2 + + let per = floorToScreenPixels(backingScaleFactor, width / (count - 1)) + + ctx.setFillColor(theme.colors.accent.cgColor) + let lineSize = NSMakeSize(width, 2) + let lc = _focus(lineSize) + let minX = insets.left + minF.width + insetBetweenFont + + let interactionRect = NSMakeRect(minX, lc.minY, lc.width, lc.height) + + ctx.fill(interactionRect) + + let current: CGFloat = CGFloat(item.sizes.firstIndex(of: item.current) ?? 0) + + let selectSize = NSMakeSize(20, 20) + + let selectPoint = NSMakePoint(minX + floorToScreenPixels(backingScaleFactor, interactionRect.width / CGFloat(item.sizes.count - 1)) * current - selectSize.width / 2, _focus(selectSize).minY) + + ctx.setFillColor(theme.colors.grayText.cgColor) + let unMinX = selectPoint.x + selectSize.width / 2 + + + + ctx.fill(NSMakeRect(unMinX, lc.minY, lc.maxX - unMinX, lc.height)) + + + for i in 0 ..< item.sizes.count { + let perSize = NSMakeSize(10, 10) + let perF = _focus(perSize) + let point = NSMakePoint(minX + per * CGFloat(i) - (i > 0 ? perSize.width / 2 : 0), perF.minY) + ctx.setFillColor(theme.colors.background.cgColor) + ctx.fill(NSMakeRect(point.x, point.y, perSize.width, perSize.height)) + + ctx.setFillColor(i <= (item.sizes.firstIndex(of: item.current) ?? 0) ? theme.colors.accent.cgColor : theme.colors.grayText.cgColor) + ctx.fillEllipse(in: NSMakeRect(point.x + perSize.width/2 - 2, point.y + 3, 4, 4)) + + + if item.dottedIndexes.contains(i), i > 0 { + let prevPoint = NSMakePoint(minX + per * CGFloat(i - 1) + (i == 1 ? perSize.width : perSize.width / 2), lc.minY) + let rect = NSMakeRect(prevPoint.x, lc.minY, point.x - prevPoint.x, lc.height) + ctx.clear(rect) + let w: CGFloat = 16 + + + let count = Int(floor(rect.width / w)) + let total = CGFloat(count) * w + + let inset: CGFloat = ceil((rect.width - total) / 2) + + for j in 0 ..< count { + let rect = NSMakeRect(rect.minX + CGFloat(j) * w, rect.minY, w, rect.height) + ctx.saveGState() + ctx.setFillColor(i <= (item.sizes.firstIndex(of: item.current) ?? 0) ? theme.colors.accent.cgColor : theme.colors.grayText.cgColor) + ctx.fill(NSMakeRect(rect.minX + inset + 2, rect.minY, w - 4, rect.height)) + ctx.restoreGState() + } + } + + if let titles = item.titles, titles.count == item.sizes.count { + let title = titles[i] + let titleNode = TextNode.layoutText(.initialize(string: title, color: theme.colors.grayText, font: .normal(.short)), backdorColor, 1, .end, NSMakeSize(.greatestFiniteMagnitude, .greatestFiniteMagnitude), nil, false, .left) + + var rect = NSMakeRect(min(max(point.x - titleNode.0.size.width / 2 + 3, minX), frame.width - titleNode.0.size.width - minX), point.y - 15 - titleNode.0.size.height, titleNode.0.size.width, titleNode.0.size.height) + + if i == titles.count - 1 { + rect.origin.x = min(rect.minX, (point.x + 5) - titleNode.0.size.width) + } + + titleNode.1.draw(rect, in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) + } + } + + if let titles = item.titles, titles.count == 1, let title = titles.first { + let titleNode = TextNode.layoutText(.initialize(string: title, color: theme.colors.grayText, font: .normal(.short)), backdorColor, 1, .end, NSMakeSize(.greatestFiniteMagnitude, .greatestFiniteMagnitude), nil, false, .left) + titleNode.1.draw(NSMakeRect(_focus(titleNode.0.size).minX, insets.top , titleNode.0.size.width, titleNode.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) + } + + + ctx.setFillColor(theme.colors.border.cgColor) + ctx.fillEllipse(in: NSMakeRect(selectPoint.x, selectPoint.y, selectSize.width, selectSize.height)) + + ctx.setFillColor(.white) + ctx.fillEllipse(in: NSMakeRect(selectPoint.x + 1, selectPoint.y + 1, selectSize.width - 2, selectSize.height - 2)) + + resetCursorRects() + availableRects.removeAll() + + for i in 0 ..< item.sizes.count { + let perF = _focus(selectSize) + let point = NSMakePoint(interactionRect.minX + floorToScreenPixels(backingScaleFactor, interactionRect.width / (count - 1)) * CGFloat(i) - selectSize.width / 2, perF.minY) + let rect = NSMakeRect(point.x, point.y, selectSize.width, selectSize.height) + addCursorRect(rect, cursor: NSCursor.pointingHand) + availableRects.append(rect) + } + } + + layout() + + } + + override var backdorColor: NSColor { + return theme.colors.background + } + + override func layout() { + super.layout() + guard let item = item as? SelectSizeRowItem else { + return + } + switch item.viewType { + case .legacy: + self.containerView.frame = bounds + case let .modern(position, _): + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) + self.containerView.setCorners(position.corners) + } + } + + + override var firstResponder: NSResponder? { + return self + } + + override func updateColors() { + guard let item = item as? SelectSizeRowItem else { + return + } + containerView.backgroundColor = backdorColor + self.backgroundColor = item.viewType.rowBackground + + } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + + guard let item = item as? SelectSizeRowItem else { + return + } + switch item.viewType { + case .legacy: + self.containerView.setCorners([]) + case let .modern(position, _): + self.containerView.setCorners(position.corners) + } + + needsLayout = true + containerView.needsDisplay = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + + + + + +struct SliderSelectorItem : Equatable { + let value: Int32? + let localizedText: String? + init(value: Int32?, localizedText:String? = nil) { + assert(value != nil || localizedText != nil) + self.value = value + self.localizedText = localizedText + } +} + diff --git a/Telegram-Mac/SelectivePrivacySettingsController.swift b/Telegram-Mac/SelectivePrivacySettingsController.swift index 29b03462e4..391e28844d 100644 --- a/Telegram-Mac/SelectivePrivacySettingsController.swift +++ b/Telegram-Mac/SelectivePrivacySettingsController.swift @@ -8,21 +8,25 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac -import TelegramCoreMac -import PostboxMac +import SwiftSignalKit +import TelegramCore + +import Postbox enum SelectivePrivacySettingsKind { case presence case groupInvitations case voiceCalls + case profilePhoto + case forwards + case phoneNumber } private enum SelectivePrivacySettingType { case everybody case contacts case nobody - + init(_ setting: SelectivePrivacySettings) { switch setting { case .disableEveryone: @@ -35,18 +39,27 @@ private enum SelectivePrivacySettingType { } } +enum SelectivePrivacySettingsPeerTarget { + case main + case callP2P +} + + private final class SelectivePrivacySettingsControllerArguments { - let account: Account - + let context: AccountContext + let updateType: (SelectivePrivacySettingType) -> Void - let openEnableFor: () -> Void - let openDisableFor: () -> Void - - init(account: Account, updateType: @escaping (SelectivePrivacySettingType) -> Void, openEnableFor: @escaping () -> Void, openDisableFor: @escaping () -> Void) { - self.account = account + let openEnableFor: (SelectivePrivacySettingsPeerTarget) -> Void + let openDisableFor: (SelectivePrivacySettingsPeerTarget) -> Void + let p2pMode: (SelectivePrivacySettingType) -> Void + let updatePhoneDiscovery:(Bool)->Void + init(context: AccountContext, updateType: @escaping (SelectivePrivacySettingType) -> Void, openEnableFor: @escaping (SelectivePrivacySettingsPeerTarget) -> Void, openDisableFor: @escaping (SelectivePrivacySettingsPeerTarget) -> Void, p2pMode: @escaping(SelectivePrivacySettingType) -> Void, updatePhoneDiscovery:@escaping(Bool)->Void) { + self.context = context self.updateType = updateType self.openEnableFor = openEnableFor self.openDisableFor = openDisableFor + self.updatePhoneDiscovery = updatePhoneDiscovery + self.p2pMode = p2pMode } } @@ -57,327 +70,390 @@ private enum SelectivePrivacySettingsSection: Int32 { private func stringForUserCount(_ count: Int) -> String { if count == 0 { - return tr(.privacySettingsControllerAddUsers) + return tr(L10n.privacySettingsControllerAddUsers) } else { - return tr(.privacySettingsControllerUserCountCountable(count)) + return tr(L10n.privacySettingsControllerUserCountCountable(count)) } } private enum SelectivePrivacySettingsEntry: TableItemListNodeEntry { - case settingHeader(Int32, String) - case everybody(Int32, Bool) - case contacts(Int32, Bool) - case nobody(Int32, Bool) - case settingInfo(Int32, String) - case disableFor(Int32, String, Int) - case enableFor(Int32, String, Int) - case peersInfo(Int32) + case settingHeader(Int32, String, GeneralViewType) + case everybody(Int32, Bool, GeneralViewType) + case contacts(Int32, Bool, GeneralViewType) + case nobody(Int32, Bool, GeneralViewType) + case p2pAlways(Int32, Bool, GeneralViewType) + case p2pContacts(Int32, Bool, GeneralViewType) + case p2pNever(Int32, Bool, GeneralViewType) + case p2pHeader(Int32, String, GeneralViewType) + case p2pDesc(Int32, String, GeneralViewType) + case settingInfo(Int32, String, GeneralViewType) + case disableFor(Int32, String, Int, GeneralViewType) + case enableFor(Int32, String, Int, GeneralViewType) + case p2pDisableFor(Int32, String, Int, GeneralViewType) + case p2pEnableFor(Int32, String, Int, GeneralViewType) + case p2pPeersInfo(Int32, GeneralViewType) + case phoneDiscoveryHeader(Int32, String, GeneralViewType) + case phoneDiscoveryEverybody(Int32, String, Bool, GeneralViewType) + case phoneDiscoveryMyContacts(Int32, String, Bool, GeneralViewType) + case phoneDiscoveryInfo(Int32, String, GeneralViewType) + case peersInfo(Int32, GeneralViewType) case section(Int32) - + var stableId: Int32 { switch self { - case .settingHeader: - return 0 - case .everybody: - return 1 - case .contacts: - return 2 - case .nobody: - return 3 - case .settingInfo: - return 4 - case .disableFor: - return 5 - case .enableFor: - return 6 - case .peersInfo: - return 7 - case .section(let sectionId): - return (sectionId + 1) * 1000 - sectionId + case .settingHeader: return 0 + case .everybody: return 1 + case .contacts: return 2 + case .nobody: return 3 + case .settingInfo: return 4 + case .disableFor: return 5 + case .enableFor: return 6 + case .peersInfo: return 7 + case .p2pHeader: return 8 + case .p2pAlways: return 9 + case .p2pContacts: return 10 + case .p2pNever: return 11 + case .p2pDesc: return 12 + case .p2pDisableFor: return 13 + case .p2pEnableFor: return 14 + case .p2pPeersInfo: return 15 + case .phoneDiscoveryHeader: return 16 + case .phoneDiscoveryEverybody: return 17 + case .phoneDiscoveryMyContacts: return 18 + case .phoneDiscoveryInfo: return 19 + + case .section(let sectionId): return (sectionId + 1) * 1000 - sectionId } } - + var index:Int32 { switch self { - case .settingHeader(let sectionId, _): - return (sectionId * 1000) + stableId - case .everybody(let sectionId, _): - return (sectionId * 1000) + stableId - case .contacts(let sectionId, _): - return (sectionId * 1000) + stableId - case .nobody(let sectionId, _): - return (sectionId * 1000) + stableId - case .settingInfo(let sectionId, _): - return (sectionId * 1000) + stableId - case .disableFor(let sectionId, _, _): - return (sectionId * 1000) + stableId - case .enableFor(let sectionId, _, _): - return (sectionId * 1000) + stableId - case .peersInfo(let sectionId): - return (sectionId * 1000) + stableId - case .section(let sectionId): - return (sectionId + 1) * 1000 - sectionId - } - } - - static func ==(lhs: SelectivePrivacySettingsEntry, rhs: SelectivePrivacySettingsEntry) -> Bool { - switch lhs { - case let .settingHeader(sectionId, text): - if case .settingHeader(sectionId, text) = rhs { - return true - } else { - return false - } - case let .everybody(sectionId, value): - if case .everybody(sectionId, value) = rhs { - return true - } else { - return false - } - case let .contacts(sectionId, value): - if case .contacts(sectionId, value) = rhs { - return true - } else { - return false - } - case let .nobody(sectionId, value): - if case .nobody(sectionId, value) = rhs { - return true - } else { - return false - } - case let .settingInfo(sectionId, text): - if case .settingInfo(sectionId, text) = rhs { - return true - } else { - return false - } - case let .disableFor(sectionId, title, count): - if case .disableFor(sectionId, title, count) = rhs { - return true - } else { - return false - } - case let .enableFor(sectionId, title, count): - if case .enableFor(sectionId, title, count) = rhs { - return true - } else { - return false - } - case .peersInfo(let sectionId): - if case .peersInfo(sectionId) = rhs { - return true - } else { - return false - } - case .section(let sectionId): - if case .section(sectionId) = rhs { - return true - } else { - return false - } + case .settingHeader(let sectionId, _, _): return (sectionId * 1000) + stableId + case .everybody(let sectionId, _, _): return (sectionId * 1000) + stableId + case .contacts(let sectionId, _, _): return (sectionId * 1000) + stableId + case .nobody(let sectionId, _, _): return (sectionId * 1000) + stableId + case .settingInfo(let sectionId, _, _): return (sectionId * 1000) + stableId + case .disableFor(let sectionId, _, _, _): return (sectionId * 1000) + stableId + case .enableFor(let sectionId, _, _, _): return (sectionId * 1000) + stableId + case .peersInfo(let sectionId, _): return (sectionId * 1000) + stableId + case .p2pAlways(let sectionId, _, _): return (sectionId * 1000) + stableId + case .p2pContacts(let sectionId, _, _): return (sectionId * 1000) + stableId + case .p2pNever(let sectionId, _, _): return (sectionId * 1000) + stableId + case .p2pHeader(let sectionId, _, _): return (sectionId * 1000) + stableId + case .p2pDesc(let sectionId, _, _): return (sectionId * 1000) + stableId + case .p2pDisableFor(let sectionId, _, _, _): return (sectionId * 1000) + stableId + case .p2pEnableFor(let sectionId, _, _, _): return (sectionId * 1000) + stableId + case .p2pPeersInfo(let sectionId, _): return (sectionId * 1000) + stableId + case .phoneDiscoveryHeader(let sectionId, _, _): return (sectionId * 1000) + stableId + case .phoneDiscoveryEverybody(let sectionId, _, _, _): return (sectionId * 1000) + stableId + case .phoneDiscoveryMyContacts(let sectionId, _, _, _): return (sectionId * 1000) + stableId + case .phoneDiscoveryInfo(let sectionId, _, _): return (sectionId * 1000) + stableId + case .section(let sectionId): return (sectionId + 1) * 1000 - sectionId } } - + + static func <(lhs: SelectivePrivacySettingsEntry, rhs: SelectivePrivacySettingsEntry) -> Bool { return lhs.index < rhs.index } - + func item(_ arguments: SelectivePrivacySettingsControllerArguments, initialSize: NSSize) -> TableRowItem { switch self { - case let .settingHeader(_, text): - return GeneralTextRowItem(initialSize, stableId: stableId, text: text, drawCustomSeparator: true, inset: NSEdgeInsets(left: 30.0, right: 30.0, top:2, bottom:6)) - case let .everybody(_, value): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.privacySettingsControllerEverbody), type: .selectable(stateback: { () -> Bool in - return value - }), action: { + case let .settingHeader(_, text, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, viewType: viewType) + case let .everybody(_, value, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.privacySettingsControllerEverbody, type: .selectable(value), viewType: viewType, action: { arguments.updateType(.everybody) }) - case let .contacts(_, value): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.privacySettingsControllerMyContacts), type: .selectable(stateback: { () -> Bool in - return value - }), action: { + case let .contacts(_, value, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.privacySettingsControllerMyContacts, type: .selectable(value), viewType: viewType, action: { arguments.updateType(.contacts) }) - case let .nobody(_, value): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.privacySettingsControllerNobody), type: .selectable(stateback: { () -> Bool in - return value - }), action: { + case let .nobody(_, value, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.privacySettingsControllerNobody, type: .selectable(value), viewType: viewType, action: { arguments.updateType(.nobody) }) - case let .settingInfo(_, text): - return GeneralTextRowItem(initialSize, stableId: stableId, text: text) - case let .disableFor(_, title, count): - - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: title, type: .context(stateback: { () -> String in - return stringForUserCount(count) - }), action: { - arguments.openDisableFor() + case let .p2pHeader(_, text, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, viewType: viewType) + case let .p2pAlways(_, value, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.privacySettingsControllerP2pAlways, type: .selectable(value), viewType: viewType, action: { + arguments.p2pMode(.everybody) }) - - case let .enableFor(_, title, count): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: title, type: .context(stateback: { () -> String in - return stringForUserCount(count) - }), action: { - arguments.openEnableFor() + case let .p2pContacts(_, value, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.privacySettingsControllerP2pContacts, type: .selectable(value), viewType: viewType, action: { + arguments.p2pMode(.contacts) + }) + case let .p2pNever(_, value, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.privacySettingsControllerP2pNever, type: .selectable(value), viewType: viewType, action: { + arguments.p2pMode(.nobody) + }) + case let .p2pDesc(_, text, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, viewType: viewType) + case let .settingInfo(_, text, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, viewType: viewType) + case let .disableFor(_, title, count, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: title, type: .context(stringForUserCount(count)), viewType: viewType, action: { + arguments.openDisableFor(.main) + }) + case let .enableFor(_, title, count, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: title, type: .context(stringForUserCount(count)), viewType: viewType, action: { + arguments.openEnableFor(.main) + }) + case let .peersInfo(_, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.privacySettingsControllerPeerInfo, viewType: viewType) + case let .p2pDisableFor(_, title, count, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: title, type: .context(stringForUserCount(count)), viewType: viewType, action: { + arguments.openDisableFor(.callP2P) }) - case .peersInfo: - return GeneralTextRowItem(initialSize, stableId: stableId, text: tr(.privacySettingsControllerPeerInfo)) + case let .p2pEnableFor(_, title, count, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: title, type: .context(stringForUserCount(count)), viewType: viewType, action: { + arguments.openEnableFor(.callP2P) + }) + case let .p2pPeersInfo(_, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: L10n.privacySettingsControllerPeerInfo, viewType: viewType) + case let .phoneDiscoveryHeader(_, title, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: title, viewType: viewType) + case let .phoneDiscoveryEverybody(_, title, selected, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: title, type: .selectable(selected), viewType: viewType, action: { + arguments.updatePhoneDiscovery(true) + }) + case let .phoneDiscoveryMyContacts(_, title, selected, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: title, type: .selectable(selected), viewType: viewType, action: { + arguments.updatePhoneDiscovery(false) + }) + case let .phoneDiscoveryInfo(_, text, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, viewType: viewType) case .section: - return GeneralRowItem(initialSize, height: 20, stableId: stableId) + return GeneralRowItem(initialSize, height: 30, stableId: stableId, viewType: .separator) } } } private struct SelectivePrivacySettingsControllerState: Equatable { let setting: SelectivePrivacySettingType - let enableFor: Set - let disableFor: Set - + let enableFor: [PeerId: SelectivePrivacyPeer] + let disableFor: [PeerId: SelectivePrivacyPeer] + + let saving: Bool - - init(setting: SelectivePrivacySettingType, enableFor: Set, disableFor: Set, saving: Bool) { + + let callP2PMode: SelectivePrivacySettingType? + let callP2PEnableFor: [PeerId: SelectivePrivacyPeer] + let callP2PDisableFor: [PeerId: SelectivePrivacyPeer] + let phoneDiscoveryEnabled: Bool? + + init(setting: SelectivePrivacySettingType, enableFor: [PeerId: SelectivePrivacyPeer], disableFor: [PeerId: SelectivePrivacyPeer], saving: Bool, callP2PMode: SelectivePrivacySettingType?, callP2PEnableFor: [PeerId: SelectivePrivacyPeer], callP2PDisableFor: [PeerId: SelectivePrivacyPeer], phoneDiscoveryEnabled: Bool?) { self.setting = setting self.enableFor = enableFor self.disableFor = disableFor self.saving = saving + self.callP2PMode = callP2PMode + self.callP2PEnableFor = callP2PEnableFor + self.callP2PDisableFor = callP2PDisableFor + self.phoneDiscoveryEnabled = phoneDiscoveryEnabled + } - - static func ==(lhs: SelectivePrivacySettingsControllerState, rhs: SelectivePrivacySettingsControllerState) -> Bool { - if lhs.setting != rhs.setting { - return false - } - if lhs.enableFor != rhs.enableFor { - return false - } - if lhs.disableFor != rhs.disableFor { - return false - } - if lhs.saving != rhs.saving { - return false - } - - return true - } - + func withUpdatedSetting(_ setting: SelectivePrivacySettingType) -> SelectivePrivacySettingsControllerState { - return SelectivePrivacySettingsControllerState(setting: setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving) + return SelectivePrivacySettingsControllerState(setting: setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled) } - - func withUpdatedEnableFor(_ enableFor: Set) -> SelectivePrivacySettingsControllerState { - return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: enableFor, disableFor: self.disableFor, saving: self.saving) + + func withUpdatedEnableFor(_ enableFor: [PeerId: SelectivePrivacyPeer]) -> SelectivePrivacySettingsControllerState { + return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: enableFor, disableFor: self.disableFor, saving: self.saving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled) } - - func withUpdatedDisableFor(_ disableFor: Set) -> SelectivePrivacySettingsControllerState { - return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: disableFor, saving: self.saving) + + func withUpdatedDisableFor(_ disableFor: [PeerId: SelectivePrivacyPeer]) -> SelectivePrivacySettingsControllerState { + return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: disableFor, saving: self.saving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled) } - + func withUpdatedSaving(_ saving: Bool) -> SelectivePrivacySettingsControllerState { - return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: saving) + return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: saving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled) + } + + func withUpdatedCallP2PMode(_ mode: SelectivePrivacySettingType) -> SelectivePrivacySettingsControllerState { + return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callP2PMode: mode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled) + } + + func withUpdatedCallP2PEnableFor(_ enableFor: [PeerId: SelectivePrivacyPeer]) -> SelectivePrivacySettingsControllerState { + return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callP2PMode: self.callP2PMode, callP2PEnableFor: enableFor, callP2PDisableFor: self.callP2PDisableFor, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled) + } + + func withUpdatedCallP2PDisableFor(_ disableFor: [PeerId: SelectivePrivacyPeer]) -> SelectivePrivacySettingsControllerState { + return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: disableFor, phoneDiscoveryEnabled: self.phoneDiscoveryEnabled) + } + func withUpdatedPhoneDiscoveryEnabled(_ phoneDiscoveryEnabled: Bool?) -> SelectivePrivacySettingsControllerState { + return SelectivePrivacySettingsControllerState(setting: self.setting, enableFor: self.enableFor, disableFor: self.disableFor, saving: self.saving, callP2PMode: self.callP2PMode, callP2PEnableFor: self.callP2PEnableFor, callP2PDisableFor: self.callP2PDisableFor, phoneDiscoveryEnabled: phoneDiscoveryEnabled) } + } private func selectivePrivacySettingsControllerEntries(kind: SelectivePrivacySettingsKind, state: SelectivePrivacySettingsControllerState) -> [SelectivePrivacySettingsEntry] { var entries: [SelectivePrivacySettingsEntry] = [] - + var sectionId:Int32 = 1 entries.append(.section(sectionId)) sectionId += 1 - + let settingTitle: String - let settingInfoText: String + let settingInfoText: String? let disableForText: String let enableForText: String switch kind { case .presence: - settingTitle = tr(.privacySettingsControllerLastSeenHeader) - settingInfoText = tr(.privacySettingsControllerLastSeenDescription) - disableForText = tr(.privacySettingsControllerNeverShareWith) - enableForText = tr(.privacySettingsControllerAlwaysShareWith) + settingTitle = L10n.privacySettingsControllerLastSeenHeader + settingInfoText = L10n.privacySettingsControllerLastSeenDescription + disableForText = L10n.privacySettingsControllerNeverShareWith + enableForText = L10n.privacySettingsControllerAlwaysShareWith case .groupInvitations: - settingTitle = tr(.privacySettingsControllerGroupHeader) - settingInfoText = tr(.privacySettingsControllerGroupDescription) - disableForText = tr(.privacySettingsControllerNeverAllow) - enableForText = tr(.privacySettingsControllerAlwaysAllow) + settingTitle = L10n.privacySettingsControllerGroupHeader + settingInfoText = L10n.privacySettingsControllerGroupDescription + disableForText = L10n.privacySettingsControllerNeverAllow + enableForText = L10n.privacySettingsControllerAlwaysAllow case .voiceCalls: - settingTitle = tr(.privacySettingsControllerPhoneCallHeader) - settingInfoText = tr(.privacySettingsControllerPhoneCallDescription) - disableForText = tr(.privacySettingsControllerNeverAllow) - enableForText = tr(.privacySettingsControllerAlwaysAllow) + settingTitle = L10n.privacySettingsControllerPhoneCallHeader + settingInfoText = L10n.privacySettingsControllerPhoneCallDescription + disableForText = L10n.privacySettingsControllerNeverAllow + enableForText = L10n.privacySettingsControllerAlwaysAllow + case .profilePhoto: + settingTitle = L10n.privacySettingsControllerProfilePhotoWhoCanSeeMyPhoto + settingInfoText = L10n.privacySettingsControllerProfilePhotoCustomHelp + disableForText = L10n.privacySettingsControllerNeverShareWith + enableForText = L10n.privacySettingsControllerAlwaysShareWith + case .forwards: + settingTitle = L10n.privacySettingsControllerForwardsWhoCanForward + settingInfoText = L10n.privacySettingsControllerForwardsCustomHelp + disableForText = L10n.privacySettingsControllerNeverAllow + enableForText = L10n.privacySettingsControllerAlwaysAllow + case .phoneNumber: + if state.setting == .nobody { + settingInfoText = nil + } else { + settingInfoText = L10n.privacySettingsControllerPhoneNumberCustomHelp + } + settingTitle = L10n.privacySettingsControllerPhoneNumberWhoCanSeePhoneNumber + disableForText = L10n.privacySettingsControllerNeverShareWith + enableForText = L10n.privacySettingsControllerAlwaysShareWith + } + + entries.append(.settingHeader(sectionId, settingTitle, .textTopItem)) + + entries.append(.everybody(sectionId, state.setting == .everybody, .firstItem)) - entries.append(.settingHeader(sectionId, settingTitle)) - - entries.append(.everybody(sectionId, state.setting == .everybody)) - entries.append(.contacts(sectionId, state.setting == .contacts)) switch kind { - case .presence, .voiceCalls: - entries.append(.nobody(sectionId, state.setting == .nobody)) - case .groupInvitations: - break + case .presence, .voiceCalls, .forwards, .phoneNumber: + entries.append(.contacts(sectionId, state.setting == .contacts, .innerItem)) + entries.append(.nobody(sectionId, state.setting == .nobody, .lastItem)) + default: + entries.append(.contacts(sectionId, state.setting == .contacts, .lastItem)) + } + if let settingInfoText = settingInfoText { + entries.append(.settingInfo(sectionId, settingInfoText, .textBottomItem)) } - entries.append(.settingInfo(sectionId, settingInfoText)) + entries.append(.section(sectionId)) sectionId += 1 + if case .phoneNumber = kind, state.setting == .nobody { + entries.append(.phoneDiscoveryHeader(sectionId, L10n.privacyPhoneNumberSettingsDiscoveryHeader, .textTopItem)) + entries.append(.phoneDiscoveryEverybody(sectionId, L10n.privacySettingsControllerEverbody, state.phoneDiscoveryEnabled != false, .firstItem)) + entries.append(.phoneDiscoveryMyContacts(sectionId, L10n.privacySettingsControllerMyContacts, state.phoneDiscoveryEnabled == false, .lastItem)) + entries.append(.phoneDiscoveryInfo(sectionId, L10n.privacyPhoneNumberSettingsCustomDisabledHelp, .textBottomItem)) + + entries.append(.section(sectionId)) + sectionId += 1 + } + + + switch state.setting { case .everybody: - entries.append(.disableFor(sectionId, disableForText, state.disableFor.count)) + entries.append(.disableFor(sectionId, disableForText, countForSelectivePeers(state.disableFor), .singleItem)) case .contacts: - entries.append(.disableFor(sectionId, disableForText, state.disableFor.count)) - entries.append(.enableFor(sectionId, enableForText, state.enableFor.count)) + entries.append(.disableFor(sectionId, disableForText, countForSelectivePeers(state.disableFor), .firstItem)) + entries.append(.enableFor(sectionId, enableForText, countForSelectivePeers(state.enableFor), .lastItem)) case .nobody: - entries.append(.enableFor(sectionId, enableForText, state.enableFor.count)) + entries.append(.enableFor(sectionId, enableForText, countForSelectivePeers(state.enableFor), .singleItem)) } - entries.append(.peersInfo(sectionId)) - + entries.append(.peersInfo(sectionId, .textBottomItem)) + + if let callSettings = state.callP2PMode { + switch kind { + case .voiceCalls: + entries.append(.section(sectionId)) + sectionId += 1 + entries.append(.p2pHeader(sectionId, L10n.privacySettingsControllerP2pHeader, .textTopItem)) + entries.append(.p2pAlways(sectionId, callSettings == .everybody, .firstItem)) + entries.append(.p2pContacts(sectionId, callSettings == .contacts, .innerItem)) + entries.append(.p2pNever(sectionId, callSettings == .nobody, .lastItem)) + entries.append(.p2pDesc(sectionId, L10n.privacySettingsControllerP2pDesc, .textBottomItem)) + + entries.append(.section(sectionId)) + sectionId += 1 + + switch callSettings { + case .everybody: + entries.append(.p2pDisableFor(sectionId, disableForText, countForSelectivePeers(state.callP2PDisableFor), .singleItem)) + case .contacts: + entries.append(.p2pDisableFor(sectionId, disableForText, countForSelectivePeers(state.callP2PDisableFor), .firstItem)) + entries.append(.p2pEnableFor(sectionId, enableForText, countForSelectivePeers(state.callP2PEnableFor), .lastItem)) + case .nobody: + entries.append(.p2pEnableFor(sectionId, enableForText, countForSelectivePeers(state.callP2PEnableFor), .singleItem)) + } + entries.append(.p2pPeersInfo(sectionId, .textBottomItem)) + + default: + break + } + } + + return entries } fileprivate func prepareTransition(left:[SelectivePrivacySettingsEntry], right: [SelectivePrivacySettingsEntry], initialSize:NSSize, arguments:SelectivePrivacySettingsControllerArguments) -> TableUpdateTransition { - + let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in return entry.item(arguments, initialSize: initialSize) } - + return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true) } -class SelectivePrivacySettingsController: EditableViewController { +class SelectivePrivacySettingsController: TableViewController { private let kind: SelectivePrivacySettingsKind private let current: SelectivePrivacySettings - private let updated: (SelectivePrivacySettings) -> Void + private let updated: (SelectivePrivacySettings, SelectivePrivacySettings?, Bool?) -> Void private var savePressed:(()->Void)? - init(account: Account, kind: SelectivePrivacySettingsKind, current: SelectivePrivacySettings, updated: @escaping (SelectivePrivacySettings) -> Void) { + private let callSettings: SelectivePrivacySettings? + private let phoneDiscoveryEnabled: Bool? + init(_ context: AccountContext, kind: SelectivePrivacySettingsKind, current: SelectivePrivacySettings, callSettings: SelectivePrivacySettings? = nil, phoneDiscoveryEnabled: Bool?, updated: @escaping (SelectivePrivacySettings, SelectivePrivacySettings?, Bool?) -> Void) { self.kind = kind self.current = current self.updated = updated - super.init(account) - } - - override func changeState() { - super.changeState() - savePressed?() - } - - override var normalString:String { - return "" + self.phoneDiscoveryEnabled = phoneDiscoveryEnabled + self.callSettings = callSettings + super.init(context) } - + + override func viewDidLoad() { - let account = self.account + super.viewDidLoad() + + let context = self.context let kind = self.kind let current = self.current let updated = self.updated - + let initialSize = self.atomicSize let previous:Atomic<[SelectivePrivacySettingsEntry]> = Atomic(value: []) - - var initialEnableFor = Set() - var initialDisableFor = Set() + + var initialEnableFor: [PeerId: SelectivePrivacyPeer] = [:] + var initialDisableFor: [PeerId: SelectivePrivacyPeer] = [:] + switch current { case let .disableEveryone(enableFor): initialEnableFor = enableFor @@ -387,149 +463,252 @@ class SelectivePrivacySettingsController: EditableViewController { case let .enableEveryone(disableFor): initialDisableFor = disableFor } - let initialState = SelectivePrivacySettingsControllerState(setting: SelectivePrivacySettingType(current), enableFor: initialEnableFor, disableFor: initialDisableFor, saving: false) - + + var initialCallP2PEnableFor: [PeerId: SelectivePrivacyPeer] = [:] + var initialCallP2PDisableFor: [PeerId: SelectivePrivacyPeer] = [:] + + if let callCurrent = callSettings { + switch callCurrent { + case let .disableEveryone(enableFor): + initialCallP2PEnableFor = enableFor + initialCallP2PDisableFor = [:] + case let .enableContacts(enableFor, disableFor): + initialCallP2PEnableFor = enableFor + initialCallP2PDisableFor = disableFor + case let .enableEveryone(disableFor): + initialCallP2PEnableFor = [:] + initialCallP2PDisableFor = disableFor + } + + } + + + let initialState = SelectivePrivacySettingsControllerState(setting: SelectivePrivacySettingType(current), enableFor: initialEnableFor, disableFor: initialDisableFor, saving: false, callP2PMode: callSettings != nil ? SelectivePrivacySettingType(callSettings!) : nil, callP2PEnableFor: initialCallP2PEnableFor, callP2PDisableFor: initialCallP2PDisableFor, phoneDiscoveryEnabled: phoneDiscoveryEnabled) + let statePromise = ValuePromise(initialState, ignoreRepeated: true) let stateValue = Atomic(value: initialState) let updateState: ((SelectivePrivacySettingsControllerState) -> SelectivePrivacySettingsControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } - + var dismissImpl: (() -> Void)? var pushControllerImpl: ((ViewController) -> Void)? - + let actionsDisposable = DisposableSet() - + let updateSettingsDisposable = MetaDisposable() - actionsDisposable.add(updateSettingsDisposable) - - let arguments = SelectivePrivacySettingsControllerArguments(account: account, updateType: { type in + // actionsDisposable.add(updateSettingsDisposable) + + + let arguments = SelectivePrivacySettingsControllerArguments(context: context, updateType: { type in updateState { $0.withUpdatedSetting(type) } - }, openEnableFor: { + }, openEnableFor: { target in let title: String switch kind { case .presence: - title = tr(.privacySettingsControllerAlwaysShare) + title = L10n.privacySettingsControllerAlwaysShare case .groupInvitations: - title = tr(.privacySettingsControllerAlwaysAllow) + title = L10n.privacySettingsControllerAlwaysAllow case .voiceCalls: - title = tr(.privacySettingsControllerAlwaysAllow) + title = L10n.privacySettingsControllerAlwaysAllow + case .profilePhoto: + title = L10n.privacySettingsControllerAlwaysShare + case .forwards: + title = L10n.privacySettingsControllerAlwaysAllow + case .phoneNumber: + title = L10n.privacySettingsControllerAlwaysShareWith } - var peerIds = Set() + var peerIds:[PeerId: SelectivePrivacyPeer] = [:] updateState { state in peerIds = state.enableFor return state } - - - pushControllerImpl?(SelectivePrivacySettingsPeersController(account: account, title: title, initialPeerIds: Array(peerIds), updated: { updatedPeerIds in + pushControllerImpl?(SelectivePrivacySettingsPeersController(context, title: title, initialPeers: peerIds, updated: { updatedPeerIds in updateState { state in - return state.withUpdatedEnableFor(Set(updatedPeerIds)).withUpdatedDisableFor(state.disableFor.subtracting(Set(updatedPeerIds))) + switch target { + case .main: + var disableFor = state.disableFor + for (key, _) in updatedPeerIds { + disableFor.removeValue(forKey: key) + } + return state.withUpdatedEnableFor(updatedPeerIds).withUpdatedDisableFor(disableFor) + case .callP2P: + var callP2PDisableFor = state.callP2PDisableFor + //var disableFor = state.disableFor + for (key, _) in updatedPeerIds { + callP2PDisableFor.removeValue(forKey: key) + } + return state.withUpdatedCallP2PEnableFor(updatedPeerIds).withUpdatedCallP2PDisableFor(callP2PDisableFor) + } } })) - }, openDisableFor: { + }, openDisableFor: { target in let title: String switch kind { case .presence: - title = tr(.privacySettingsControllerNeverShareWith) + title = L10n.privacySettingsControllerNeverShareWith case .groupInvitations: - title = tr(.privacySettingsControllerNeverAllow) + title = L10n.privacySettingsControllerNeverAllow case .voiceCalls: - title = tr(.privacySettingsControllerNeverAllow) + title = L10n.privacySettingsControllerNeverAllow + case .profilePhoto: + title = L10n.privacySettingsControllerNeverShareWith + case .forwards: + title = L10n.privacySettingsControllerNeverAllow + case .phoneNumber: + title = L10n.privacySettingsControllerNeverShareWith } - var peerIds = Set() + var peerIds:[PeerId: SelectivePrivacyPeer] = [:] updateState { state in peerIds = state.disableFor return state } - pushControllerImpl?(SelectivePrivacySettingsPeersController(account: account, title: title, initialPeerIds: Array(peerIds), updated: { updatedPeerIds in + pushControllerImpl?(SelectivePrivacySettingsPeersController(context, title: title, initialPeers: peerIds, updated: { updatedPeerIds in updateState { state in - return state.withUpdatedDisableFor(Set(updatedPeerIds)).withUpdatedEnableFor(state.enableFor.subtracting(Set(updatedPeerIds))) + switch target { + case .main: + var enableFor = state.enableFor + for (key, _) in updatedPeerIds { + enableFor.removeValue(forKey: key) + } + return state.withUpdatedDisableFor(updatedPeerIds).withUpdatedEnableFor(enableFor) + case .callP2P: + var callP2PEnableFor = state.callP2PEnableFor + for (key, _) in updatedPeerIds { + callP2PEnableFor.removeValue(forKey: key) + } + return state.withUpdatedCallP2PDisableFor(updatedPeerIds).withUpdatedCallP2PEnableFor(callP2PEnableFor) + } } })) + }, p2pMode: { mode in + updateState { state in + return state.withUpdatedCallP2PMode(mode) + } + }, updatePhoneDiscovery: { value in + updateState { state in + return state.withUpdatedPhoneDiscoveryEnabled(value) + } }) - - let signal = statePromise.get() |> deliverOnMainQueue - |> map { [weak self] state -> TableUpdateTransition in - - if state.saving { - self?.state = .Edit - } else { - self?.state = initialState == state ? .Normal : .Edit - - self?.savePressed = { - var wasSaving = false - var settings: SelectivePrivacySettings? - updateState { state in - wasSaving = state.saving - switch state.setting { - case .everybody: - settings = SelectivePrivacySettings.enableEveryone(disableFor: state.disableFor) - case .contacts: - settings = SelectivePrivacySettings.enableContacts(enableFor: state.enableFor, disableFor: state.disableFor) - case .nobody: - settings = SelectivePrivacySettings.disableEveryone(enableFor: state.enableFor) - } - return state.withUpdatedSaving(true) - } - - if let settings = settings, !wasSaving { - let type: UpdateSelectiveAccountPrivacySettingsType - switch kind { - case .presence: - type = .presence - case .groupInvitations: - type = .groupInvitations - case .voiceCalls: - type = .voiceCalls - } - - updateSettingsDisposable.set((updateSelectiveAccountPrivacySettings(account: account, type: type, settings: settings) |> deliverOnMainQueue).start(completed: { - updateState { state in - return state.withUpdatedSaving(false) - } - updated(settings) - dismissImpl?() - })) - } + + + savePressed = { + var wasSaving = false + var settings: SelectivePrivacySettings? + var callSettings: SelectivePrivacySettings? + var phoneDiscoveryEnabled: Bool? = nil + updateState { state in + phoneDiscoveryEnabled = state.phoneDiscoveryEnabled + wasSaving = state.saving + switch state.setting { + case .everybody: + settings = SelectivePrivacySettings.enableEveryone(disableFor: state.disableFor) + case .contacts: + settings = SelectivePrivacySettings.enableContacts(enableFor: state.enableFor, disableFor: state.disableFor) + case .nobody: + settings = SelectivePrivacySettings.disableEveryone(enableFor: state.enableFor) + } + + if let mode = state.callP2PMode { + switch mode { + case .everybody: + callSettings = SelectivePrivacySettings.enableEveryone(disableFor: state.callP2PDisableFor) + case .contacts: + callSettings = SelectivePrivacySettings.enableContacts(enableFor: state.callP2PEnableFor, disableFor: state.callP2PDisableFor) + case .nobody: + callSettings = SelectivePrivacySettings.disableEveryone(enableFor: state.callP2PEnableFor) } } + + return state.withUpdatedSaving(true) + } + + if let settings = settings, !wasSaving { + let type: UpdateSelectiveAccountPrivacySettingsType + switch kind { + case .presence: + type = .presence + case .groupInvitations: + type = .groupInvitations + case .voiceCalls: + type = .voiceCalls + case .profilePhoto: + type = .profilePhoto + case .forwards: + type = .forwards + case .phoneNumber: + type = .phoneNumber + } + var updatePhoneDiscoverySignal: Signal = Signal.complete() + if let phoneDiscoveryEnabled = phoneDiscoveryEnabled { + updatePhoneDiscoverySignal = context.engine.privacy.updatePhoneNumberDiscovery(value: phoneDiscoveryEnabled) + } + + let basic = context.engine.privacy.updateSelectiveAccountPrivacySettings(type: type, settings: settings) + + + updateSettingsDisposable.set(combineLatest(queue: .mainQueue(), updatePhoneDiscoverySignal, basic).start(completed: { + updateState { state in + return state.withUpdatedSaving(false) + } + updated(settings, callSettings, phoneDiscoveryEnabled) + dismissImpl?() + })) + } + } + + let signal = statePromise.get() |> deliverOnMainQueue + |> map { [weak self] state -> TableUpdateTransition in + + let title: String switch kind { case .presence: - title = tr(.privacySettingsLastSeen) + title = L10n.privacySettingsLastSeen case .groupInvitations: - title = tr(.privacySettingsGroups) + title = L10n.privacySettingsGroups case .voiceCalls: - title = tr(.privacySettingsVoiceCalls) + title = L10n.privacySettingsVoiceCalls + case .profilePhoto: + title = L10n.privacySettingsProfilePhoto + case .forwards: + title = L10n.privacySettingsForwards + case .phoneNumber: + title = L10n.privacySettingsPhoneNumber } - + self?.setCenterTitle(title) - + let entries = selectivePrivacySettingsControllerEntries(kind: kind, state: state) return prepareTransition(left: previous.swap(entries), right: entries, initialSize: initialSize.modify{$0}, arguments: arguments) } |> afterDisposed { actionsDisposable.dispose() } - + genericView.merge(with: signal) readyOnce() - + pushControllerImpl = { [weak self] c in self?.navigationController?.push(c) } dismissImpl = { [weak self] in - self?.navigationController?.back() + if self?.navigationController?.controller == self { + self?.navigationController?.back() + } } - + } - + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + } + override func didRemovedFromStack() { super.didRemovedFromStack() - + savePressed?() } } diff --git a/Telegram-Mac/SelectivePrivacySettingsPeersController.swift b/Telegram-Mac/SelectivePrivacySettingsPeersController.swift index 78fa256f69..305222595f 100644 --- a/Telegram-Mac/SelectivePrivacySettingsPeersController.swift +++ b/Telegram-Mac/SelectivePrivacySettingsPeersController.swift @@ -8,19 +8,20 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac +import TelegramCore + +import SwiftSignalKit +import Postbox private final class SelectivePrivacyPeersControllerArguments { - let account: Account - + let context: AccountContext + let removePeer: (PeerId) -> Void let addPeer: () -> Void let openInfo:(Peer) -> Void - init(account: Account, removePeer: @escaping (PeerId) -> Void, addPeer: @escaping () -> Void, openInfo:@escaping(Peer) -> Void) { - self.account = account + init(context: AccountContext, removePeer: @escaping (PeerId) -> Void, addPeer: @escaping () -> Void, openInfo:@escaping(Peer) -> Void) { + self.context = context self.removePeer = removePeer self.addPeer = addPeer self.openInfo = openInfo @@ -43,185 +44,145 @@ private enum SelectivePrivacyPeersEntryStableId: Hashable { return 100 + Int(id) } } - - static func ==(lhs: SelectivePrivacyPeersEntryStableId, rhs: SelectivePrivacyPeersEntryStableId) -> Bool { - switch lhs { - case let .peer(peerId): - if case .peer(peerId) = rhs { - return true - } else { - return false - } - case .add: - if case .add = rhs { - return true - } else { - return false - } - case let .section(sectionId): - if case .section(sectionId) = rhs { - return true - } else { - return false - } - } - } } private enum SelectivePrivacyPeersEntry: TableItemListNodeEntry { - case peerItem(Int32, Int32, Peer, ShortPeerDeleting?) - case addItem(Int32, Bool) + case addItem(Int32, Bool, GeneralViewType) + case peerItem(Int32, Int32, SelectivePrivacyPeer, ShortPeerDeleting?, GeneralViewType) case section(Int32) - + var stableId: SelectivePrivacyPeersEntryStableId { switch self { - case let .peerItem(_, _, peer, _): - return .peer(peer.id) + case let .peerItem(_, _, peer, _, _): + return .peer(peer.peer.id) case .addItem: return .add case let .section(sectionId): return .section(sectionId) } } - + var stableIndex:Int32 { switch self { - case let .peerItem(sectionId, index, _, _): - return (sectionId * 1000) + index + 100 - case .addItem(let sectionId, _): - return (sectionId * 1000) + 9999 + case .addItem(let sectionId, _, _): + return (sectionId * 1000) + 1_000_000 + case let .peerItem(sectionId, index, _, _, _): + return (sectionId * 1000) + index case .section(let sectionId): return (sectionId + 1) * 1000 - sectionId } } - - static func ==(lhs: SelectivePrivacyPeersEntry, rhs: SelectivePrivacyPeersEntry) -> Bool { - switch lhs { - case let .peerItem(lhsSectionId, lhsIndex, lhsPeer, lhsEditing): - if case let .peerItem(rhsSectionId, rhsIndex, rhsPeer, rhsEditing) = rhs { - - if lhsSectionId != rhsSectionId { - return false - } - if lhsIndex != rhsIndex { - return false - } - if !lhsPeer.isEqual(rhsPeer) { - return false - } - if lhsEditing != rhsEditing { - return false - } - return true - } else { - return false - } - case let .addItem(sectionId, editing): - if case .addItem(sectionId, editing) = rhs { - return true - } else { - return false - } - case let .section(sectionId): - if case .section(sectionId) = rhs { - return true - } else { - return false - } - } - } - + static func <(lhs: SelectivePrivacyPeersEntry, rhs: SelectivePrivacyPeersEntry) -> Bool { return lhs.stableIndex < rhs.stableIndex } - + func item(_ arguments: SelectivePrivacyPeersControllerArguments, initialSize: NSSize) -> TableRowItem { switch self { - case let .peerItem(_, _, peer, editing): - - - + case let .peerItem(_, _, peer, editing, viewType): + + + let interactionType:ShortPeerItemInteractionType if let editing = editing { - + interactionType = .deletable(onRemove: { peerId in arguments.removePeer(peerId) }, deletable: editing.editable) } else { interactionType = .plain } - - return ShortPeerRowItem(initialSize, peer: peer, account: arguments.account, stableId: stableId, enabled: true, height:44, photoSize: NSMakeSize(32, 32), drawLastSeparator: true, inset: NSEdgeInsets(left: 30, right: 30), interactionType: interactionType, generalType: .none, action: { - arguments.openInfo(peer) + + var status: String? = nil + let count = peer.participantCount + if let count = count { + let count = Int(count) + let countValue = L10n.privacySettingsGroupMembersCountCountable(count) + status = countValue.replacingOccurrences(of: "\(count)", with: count.separatedNumber) + } + + + return ShortPeerRowItem(initialSize, peer: peer.peer, account: arguments.context.account, stableId: stableId, enabled: true, height:44, photoSize: NSMakeSize(30, 30), status: status, drawLastSeparator: true, inset: NSEdgeInsets(left: 30, right: 30), interactionType: interactionType, generalType: .none, viewType: viewType, action: { + arguments.openInfo(peer.peer) + }, contextMenuItems: { + return .single([ContextMenuItem(L10n.confirmDelete, handler: { + arguments.removePeer(peer.peer.id) + })]) }) - case .addItem: - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(.privacySettingsPeerSelectAddNew), nameStyle: blueActionButton, type: .none, action: { + case let .addItem(_, _, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.privacySettingsPeerSelectAddUserOrGroup, nameStyle: blueActionButton, type: .none, viewType: viewType, action: { arguments.addPeer() }) case .section: - return GeneralRowItem(initialSize, height: 20, stableId: stableId) + return GeneralRowItem(initialSize, height: 30, stableId: stableId, viewType: .separator) } } } private struct SelectivePrivacyPeersControllerState: Equatable { let editing: Bool - + init() { self.editing = false } - + init(editing: Bool) { self.editing = editing } - + static func ==(lhs: SelectivePrivacyPeersControllerState, rhs: SelectivePrivacyPeersControllerState) -> Bool { if lhs.editing != rhs.editing { return false } return true } - + func withUpdatedEditing(_ editing: Bool) -> SelectivePrivacyPeersControllerState { return SelectivePrivacyPeersControllerState(editing: editing) } - + func withUpdatedPeerIdWithRevealedOptions(_ peerIdWithRevealedOptions: PeerId?) -> SelectivePrivacyPeersControllerState { return SelectivePrivacyPeersControllerState(editing: self.editing) } } -private func selectivePrivacyPeersControllerEntries(state: SelectivePrivacyPeersControllerState, peers: [Peer]) -> [SelectivePrivacyPeersEntry] { +private func selectivePrivacyPeersControllerEntries(state: SelectivePrivacyPeersControllerState, peers: [SelectivePrivacyPeer]) -> [SelectivePrivacyPeersEntry] { var entries: [SelectivePrivacyPeersEntry] = [] - + var sectionId:Int32 = 1 + + entries.append(.section(sectionId)) + sectionId += 1 + entries.append(.addItem(sectionId, state.editing, .singleItem)) + entries.append(.section(sectionId)) sectionId += 1 var index: Int32 = 0 - for peer in peers { + for (i, peer) in peers.enumerated() { var deleting:ShortPeerDeleting? = nil if state.editing { deleting = ShortPeerDeleting(editable: true) } - entries.append(.peerItem(sectionId, index, peer, deleting)) + entries.append(.peerItem(sectionId, index, peer, deleting, bestGeneralViewType(peers, for: i))) index += 1 } - entries.append(.addItem(sectionId, state.editing)) - + entries.append(.section(sectionId)) + sectionId += 1 + return entries } fileprivate func prepareTransition(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], initialSize:NSSize, arguments:SelectivePrivacyPeersControllerArguments) -> TableUpdateTransition { - + let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in return entry.entry.item(arguments, initialSize: initialSize) } - + return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true) } @@ -229,84 +190,95 @@ fileprivate func prepareTransition(left:[AppearanceWrapperEntry { private let title:String - private let initialPeerIds:[PeerId] - private let updated:([PeerId])->Void + private let initialPeers:[PeerId: SelectivePrivacyPeer] + private let updated:([PeerId: SelectivePrivacyPeer])->Void private let statePromise = ValuePromise(SelectivePrivacyPeersControllerState(), ignoreRepeated: true) private let stateValue = Atomic(value: SelectivePrivacyPeersControllerState()) - init(account: Account, title: String, initialPeerIds: [PeerId], updated: @escaping ([PeerId]) -> Void) { + init(_ context: AccountContext, title: String, initialPeers: [PeerId: SelectivePrivacyPeer], updated: @escaping ([PeerId: SelectivePrivacyPeer]) -> Void) { self.title = title - self.initialPeerIds = initialPeerIds + self.initialPeers = initialPeers self.updated = updated - super.init(account) + super.init(context) } - + override func viewDidLoad() { super.viewDidLoad() - let account = self.account + genericView.getBackgroundColor = { + theme.colors.listBackground + } + + let context = self.context let title = self.title self.setCenterTitle(title) - let initialPeerIds = self.initialPeerIds + let initialPeers = self.initialPeers let updated = self.updated let initialSize = self.atomicSize - + let statePromise = self.statePromise let stateValue = self.stateValue - + let actionsDisposable = DisposableSet() - + let addPeerDisposable = MetaDisposable() actionsDisposable.add(addPeerDisposable) - + let removePeerDisposable = MetaDisposable() actionsDisposable.add(removePeerDisposable) - - let peersPromise = Promise<[Peer]>() - peersPromise.set(account.postbox.modify { modifier -> [Peer] in - var result: [Peer] = [] - for peerId in initialPeerIds { - if let peer = modifier.getPeer(peerId) { - result.append(peer) - } - } - return result + + let peersPromise = Promise<[SelectivePrivacyPeer]>() + peersPromise.set(context.account.postbox.transaction { transaction -> [SelectivePrivacyPeer] in + return Array(initialPeers.values) }) - + + var currentPeerIds:[PeerId] = [] - - let arguments = SelectivePrivacyPeersControllerArguments(account: account, removePeer: { memberId in + + let arguments = SelectivePrivacyPeersControllerArguments(context: context, removePeer: { memberId in let applyPeers: Signal = peersPromise.get() |> take(1) |> deliverOnMainQueue |> mapToSignal { peers -> Signal in var updatedPeers = peers for i in 0 ..< updatedPeers.count { - if updatedPeers[i].id == memberId { + if updatedPeers[i].peer.id == memberId { updatedPeers.remove(at: i) break } } peersPromise.set(.single(updatedPeers)) - updated(updatedPeers.map { $0.id }) - + + var updatedPeerDict: [PeerId: SelectivePrivacyPeer] = [:] + for peer in updatedPeers { + updatedPeerDict[peer.peer.id] = peer + } + updated(updatedPeerDict) + return .complete() } - + removePeerDisposable.set(applyPeers.start()) + }, addPeer: { - - addPeerDisposable.set(selectModalPeers(account: account, title: title, settings: [.contacts], excludePeerIds: currentPeerIds, limit: 0, confirmation: {_ in return .single(true)}).start(next: { peerIds in - + + addPeerDisposable.set(selectModalPeers(window: context.window, context: context, title: title, excludePeerIds: currentPeerIds, limit: 0, behavior: SelectUsersAndGroupsBehavior(), confirmation: {_ in return .single(true)}).start(next: { peerIds in let applyPeers: Signal = peersPromise.get() |> take(1) - |> mapToSignal { peers -> Signal<[Peer], NoError> in - return account.postbox.modify { modifier -> [Peer] in + |> mapToSignal { peers -> Signal<[SelectivePrivacyPeer], NoError> in + return context.account.postbox.transaction { transaction -> [SelectivePrivacyPeer] in var updatedPeers = peers - var existingIds = Set(updatedPeers.map { $0.id }) + var existingIds = Set(updatedPeers.map { $0.peer.id }) for peerId in peerIds { - if let peer = modifier.getPeer(peerId), !existingIds.contains(peerId) { + if let peer = transaction.getPeer(peerId), !existingIds.contains(peerId) { existingIds.insert(peerId) - updatedPeers.append(peer) + var participantCount: Int32? + if let channel = peer as? TelegramChannel, case .group = channel.info { + if let cachedData = transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData { + participantCount = cachedData.participantsSummary.memberCount + } + } + + updatedPeers.append(SelectivePrivacyPeer(peer: peer, participantCount: participantCount)) } } return updatedPeers @@ -315,39 +287,48 @@ class SelectivePrivacySettingsPeersController: EditableViewController |> deliverOnMainQueue |> mapToSignal { updatedPeers -> Signal in peersPromise.set(.single(updatedPeers)) - updated(updatedPeers.map { $0.id }) + + var updatedPeerDict: [PeerId: SelectivePrivacyPeer] = [:] + for peer in updatedPeers { + updatedPeerDict[peer.peer.id] = peer + } + updated(updatedPeerDict) + return .complete() } - + removePeerDisposable.set(applyPeers.start()) })) }, openInfo: { [weak self] peer in - self?.navigationController?.push(PeerInfoController(account: account, peer: peer)) + self?.navigationController?.push(PeerInfoController(context: context, peerId: peer.id)) }) - + let previous:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) - + let signal = combineLatest(statePromise.get() |> deliverOnMainQueue, peersPromise.get() |> deliverOnMainQueue, appearanceSignal) |> map { state, peers, appearance -> TableUpdateTransition in - - currentPeerIds = peers.map({$0.id}) - + + currentPeerIds = peers.map { $0.peer.id } + let entries = selectivePrivacyPeersControllerEntries(state: state, peers: peers).map{AppearanceWrapperEntry(entry: $0, appearance: appearance)} return prepareTransition(left: previous.swap(entries), right: entries, initialSize: initialSize.modify{$0}, arguments: arguments) - + } |> afterDisposed { actionsDisposable.dispose() } - genericView.merge(with: signal) + actionsDisposable.add(signal.start(next: { [weak self] transition in + guard let `self` = self else { return } + self.genericView.merge(with: transition) + self.readyOnce() + + })) - readyOnce() - } override func update(with state: ViewControllerState) { super.update(with: state) self.statePromise.set(stateValue.modify({$0.withUpdatedEditing(state == .Edit)})) } - + } diff --git a/Telegram-Mac/SenderController.swift b/Telegram-Mac/SenderController.swift index 3ae0d42fcf..e5e59accab 100644 --- a/Telegram-Mac/SenderController.swift +++ b/Telegram-Mac/SenderController.swift @@ -7,11 +7,19 @@ // import Cocoa -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit import AVFoundation -class MediaSenderContainer { +import QuickLook +import TGUIKit +import libwebp +let diceSymbol: String = "🎲" +let dartSymbol: String = "🎯" + + +class MediaSenderContainer : Equatable { let path:String let caption:String let isFile:Bool @@ -20,13 +28,33 @@ class MediaSenderContainer { self.caption = caption self.isFile = isFile } + + static func ==(lhs: MediaSenderContainer, rhs: MediaSenderContainer) -> Bool { + return lhs.path == rhs.path && lhs.caption == rhs.caption && lhs.isFile == rhs.isFile + } } +class ArchiverSenderContainer : MediaSenderContainer { + let files: [URL] + public init(path:String, caption:String = "", isFile:Bool = true, files: [URL] = []) { + self.files = files + super.init(path: path, caption: caption, isFile: isFile) + } + + static func ==(lhs: ArchiverSenderContainer, rhs: ArchiverSenderContainer) -> Bool { + return lhs.path == rhs.path && lhs.caption == rhs.caption && lhs.isFile == rhs.isFile && lhs.files == rhs.files + } +} + + class VoiceSenderContainer : MediaSenderContainer { fileprivate let data:RecordedAudioData - public init(data:RecordedAudioData) { + fileprivate let id:Int64? + public init(data:RecordedAudioData, id: Int64?) { self.data = data - super.init(path: data.path) + self.id = id + let path: String = data.path + super.init(path: path) } } @@ -34,9 +62,11 @@ class VoiceSenderContainer : MediaSenderContainer { class VideoMessageSenderContainer : MediaSenderContainer { fileprivate let duration:Int fileprivate let size: CGSize - public init(path:String, duration: Int, size: CGSize) { + fileprivate let id:Int64? + public init(path:String, duration: Int, size: CGSize, id: Int64?) { self.duration = duration self.size = size + self.id = id super.init(path: path, caption: "", isFile: false) } } @@ -44,21 +74,41 @@ class VideoMessageSenderContainer : MediaSenderContainer { class Sender: NSObject { - private static func previewForFile(_ path: String, account: Account) -> [TelegramMediaImageRepresentation] { + private static func previewForFile(_ path: String, isSecretRelated: Bool, account: Account) -> [TelegramMediaImageRepresentation] { var preview:[TelegramMediaImageRepresentation] = [] - let options = NSMutableDictionary() - options.setValue(90 as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) - options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) +// if isDirectory(path) { +// let image = NSWorkspace.shared.icon(forFile: path) +// image.lockFocus() +// let imageRep = NSBitmapImageRep(focusedViewRect: NSMakeRect(0, 0, image.size.width, image.size.height)) +// image.unlockFocus() +// +// let compressedData: Data? = imageRep?.representation(using: .jpeg, properties: [:]) +// if let compressedData = compressedData { +// let resource = LocalFileMediaResource(fileId: arc4random64()) +// account.postbox.mediaBox.storeResourceData(resource.id, data: compressedData) +// preview.append(TelegramMediaImageRepresentation(dimensions: image.size, resource: resource)) +// } +// return preview +// } - let colorQuality: Float = 0.6 - options.setObject(colorQuality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString) - + let mimeType = MIMEType(path) + - if path.nsstring.pathExtension.hasPrefix("mp4") { + if mimeType.hasPrefix("video") { + + + + let options = NSMutableDictionary() + options.setValue(320 as NSNumber, forKey: kCGImageDestinationImageMaxPixelSize as String) + options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailWithTransform as String) + + let colorQuality: Float = 0.3 + options.setObject(colorQuality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString) + let asset = AVAsset(url: URL(fileURLWithPath: path)) let imageGenerator = AVAssetImageGenerator(asset: asset) - imageGenerator.maximumSize = CGSize(width: 200, height: 200) + imageGenerator.maximumSize = CGSize(width: 320, height: 320) imageGenerator.appliesPreferredTrackTransform = true let fullSizeImage = try? imageGenerator.copyCGImage(at: CMTime(seconds: 0.0, preferredTimescale: asset.duration.timescale), actualTime: nil) @@ -70,30 +120,41 @@ class Sender: NSObject { CGImageDestinationAddImage(colorDestination, image, options as CFDictionary) if CGImageDestinationFinalize(colorDestination) { - let resource = LocalFileMediaResource(fileId: arc4random64()) + let resource = LocalFileMediaResource(fileId: arc4random64(), isSecretRelated: isSecretRelated) account.postbox.mediaBox.storeResourceData(resource.id, data: mutableData as Data) - preview.append(TelegramMediaImageRepresentation(dimensions: image.size, resource: resource)) + preview.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(image.size), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) } } - - } + } else if (mimeType.hasPrefix("image") || mimeType.hasSuffix("pdf") && !mimeType.hasPrefix("image/webp")), let thumbData = try? Data(contentsOf: URL(fileURLWithPath: path)) { - - } else if let thumbData = try? Data(contentsOf: URL(fileURLWithPath: path)) { + let options = NSMutableDictionary() + options.setValue(320 as NSNumber, forKey: kCGImageDestinationImageMaxPixelSize as String) - if let imageSource = CGImageSourceCreateWithData(thumbData as CFData, options) { - if let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options) { - let imageRep = NSBitmapImageRep(cgImage: image) - let options: [NSBitmapImageRep.PropertyKey: Any] = [NSBitmapImageRep.PropertyKey.compressionFactor: 0.6] - let compressedData: Data? = imageRep.representation(using: NSBitmapImageRep.FileType.jpeg, properties: options) + let colorQuality: Float = 0.7 + options.setObject(colorQuality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString) + options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailWithTransform as String) + + let sourceOptions = NSMutableDictionary() + sourceOptions.setValue(320 as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) + sourceOptions.setObject(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as NSString) + sourceOptions.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailWithTransform as String) + + if let imageSource = CGImageSourceCreateWithData(thumbData as CFData, sourceOptions) { + let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, sourceOptions) + if let image = image { - if let compressedData = compressedData { - let resource = LocalFileMediaResource(fileId: arc4random64()) - account.postbox.mediaBox.storeResourceData(resource.id, data: compressedData) - preview.append(TelegramMediaImageRepresentation(dimensions: image.size, resource: resource)) - } + let mutableData: CFMutableData = NSMutableData() as CFMutableData + if let colorDestination = CGImageDestinationCreateWithData(mutableData, kUTTypeJPEG, 1, options) { + CGImageDestinationSetProperties(colorDestination, nil) + CGImageDestinationAddImage(colorDestination, image, options as CFDictionary) + if CGImageDestinationFinalize(colorDestination) { + let resource = LocalFileMediaResource(fileId: arc4random64(), isSecretRelated: isSecretRelated) + account.postbox.mediaBox.storeResourceData(resource.id, data: mutableData as Data) + preview.append(TelegramMediaImageRepresentation(dimensions: image.size.pixel, resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) + } + } } } @@ -101,50 +162,91 @@ class Sender: NSObject { return preview } - public static func enqueue( input:ChatTextInputState, account:Account, peerId:PeerId, replyId:MessageId?, disablePreview:Bool = false) ->Signal<[MessageId?],NoError> { + public static func enqueue( input:ChatTextInputState, context: AccountContext, peerId:PeerId, replyId:MessageId?, disablePreview:Bool = false, silent: Bool = false, atDate:Date? = nil, mediaPreview: TelegramMediaWebpage? = nil, emptyHandler:(()->Void)? = nil) ->Signal<[MessageId?],NoError> { var inset:Int = 0 var input:ChatTextInputState = input - let emojis = ObjcUtils.getEmojiFrom(input.inputText.fixed) + let emojis = Array(input.inputText.fixed.emojiString).map { String($0) }.compactMap {!$0.isEmpty ? $0 : nil} if input.attributes.isEmpty { input = ChatTextInputState(inputText: input.inputText.trimmed) } - let mapped = cut_long_message( input.inputText, 4096).map { message -> EnqueueMessage in + + + if FastSettings.isPossibleReplaceEmojies { + let text = input.attributedString.stringEmojiReplacements + if text != input.attributedString { + input = ChatTextInputState(inputText: text.string, selectionRange: 0 ..< text.string.length, attributes: chatTextAttributes(from: text)) + } + } + + var mediaReference: AnyMediaReference? = nil + + + let dices = InteractiveEmojiConfiguration.with(appConfiguration: context.appConfiguration) + if dices.emojis.contains(input.inputText), peerId.namespace != Namespaces.Peer.SecretChat { + mediaReference = AnyMediaReference.standalone(media: TelegramMediaDice(emoji: input.inputText, value: nil)) + input = ChatTextInputState(inputText: "") + } + + if let media = mediaPreview, !disablePreview { + mediaReference = AnyMediaReference.standalone(media: media) + } + + + let parsingUrlType: ParsingType + if peerId.namespace != Namespaces.Peer.SecretChat { + parsingUrlType = [.Hashtags] + } else { + parsingUrlType = [.Links, .Hashtags] + } + + let mapped = cut_long_message( input.inputText, 4096).compactMap { message -> EnqueueMessage? in let subState = input.subInputState(from: NSMakeRange(inset, message.length)) inset += message.length - var attributes:[MessageAttribute] = [TextEntitiesMessageAttribute(entities: subState.messageTextEntities)] + + var attributes:[MessageAttribute] = [TextEntitiesMessageAttribute(entities: subState.messageTextEntities(parsingUrlType))] + if let date = atDate { + attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: Int32(date.timeIntervalSince1970))) + } if disablePreview { attributes.append(OutgoingContentInfoMessageAttribute(flags: [.disableLinkPreviews])) } - if FastSettings.isChannelMessagesMuted(peerId) { + if FastSettings.isChannelMessagesMuted(peerId) || silent { attributes.append(NotificationInfoMessageAttribute(flags: [.muted])) } - - - return EnqueueMessage.message(text: subState.inputText, attributes: attributes, media: nil, replyToMessageId: replyId) + if !subState.inputText.isEmpty || mediaReference != nil { + return .message(text: subState.inputText, attributes: attributes, mediaReference: mediaReference, replyToMessageId: replyId, localGroupingKey: nil, correlationId: nil) + } else { + return nil + } } - return enqueueMessages(account: account, peerId: peerId, messages: mapped) |> mapToSignal { value in - if !emojis.isEmpty { - return saveUsedEmoji(emojis, postbox: account.postbox) |> map { - return value + if !mapped.isEmpty { + return enqueueMessages(account: context.account, peerId: peerId, messages: mapped) |> mapToSignal { value in + if !emojis.isEmpty { + return saveUsedEmoji(emojis, postbox: context.account.postbox) |> map { + return value + } } + return .single(value) + } |> deliverOnMainQueue + } else { + DispatchQueue.main.async { + emptyHandler?() } - return .single(value) - } |> deliverOnMainQueue - + return .complete() + } } - public static func enqueue(message:EnqueueMessage, account:Account, peerId:PeerId) ->Signal<[MessageId?],NoError> { - return enqueueMessages(account: account, peerId: peerId, messages: [message]) + public static func enqueue(message:EnqueueMessage, context: AccountContext, peerId:PeerId) ->Signal<[MessageId?],NoError> { + return enqueueMessages(account: context.account, peerId: peerId, messages: [message]) |> deliverOnMainQueue - } - private static func generateMedia(for container:MediaSenderContainer, account: Account) -> Signal<(Media,String),Void> { + static func generateMedia(for container:MediaSenderContainer, account: Account, isSecretRelated: Bool) -> Signal<(Media,String), NoError> { return Signal { (subscriber) in let path = container.path @@ -154,14 +256,14 @@ class Sender: NSObject { arc4random_buf(&randomId, 8) func makeFileMedia(_ isMedia: Bool) { - let mimeType = MIMEType(path.nsstring.pathExtension) + let mimeType = MIMEType(path) let attrs:[TelegramMediaFileAttribute] = fileAttributes(for:mimeType, path:path, isMedia: isMedia) - let resource = LocalFileReferenceMediaResource(localFilePath:path,randomId:randomId, size: fileSize(path)) - media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), resource: resource, previewRepresentations: [], mimeType: mimeType, size: nil, attributes: attrs) + let resource: TelegramMediaResource = path.isDirectory ? LocalFileArchiveMediaResource(randomId: randomId, path: path) : LocalFileReferenceMediaResource(localFilePath:path,randomId:randomId, size: fs(path)) + media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: previewForFile(path, isSecretRelated: isSecretRelated, account: account), videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: nil, attributes: attrs) } if !container.isFile { - let mimeType = MIMEType(path.nsstring.pathExtension.lowercased()) + let mimeType = MIMEType(path) if let container = container as? VoiceSenderContainer { let mimeType = voiceMime var attrs:[TelegramMediaFileAttribute] = [] @@ -169,14 +271,36 @@ class Sender: NSObject { if let waveformData = container.data.waveform { memoryWaveform = MemoryBuffer(data: waveformData) } + + let resource: TelegramMediaResource + if let id = container.id, let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { + resource = LocalFileMediaResource(fileId: id, size: fileSize(path), isSecretRelated: isSecretRelated) + account.postbox.mediaBox.storeResourceData(resource.id, data: data) + } else { + resource = LocalFileReferenceMediaResource(localFilePath:path, randomId: randomId, isUniquelyReferencedTemporaryFile: true, size: fs(path)) + } + attrs.append(.Audio(isVoice: true, duration: Int(container.data.duration), title: nil, performer: nil, waveform: memoryWaveform)) - media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), resource: LocalFileReferenceMediaResource(localFilePath:path,randomId: randomId, isUniquelyReferencedTemporaryFile: true, size: fileSize(path)), previewRepresentations: [], mimeType: mimeType, size: nil, attributes: attrs) + media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: nil, attributes: attrs) } else if let container = container as? VideoMessageSenderContainer { var attrs:[TelegramMediaFileAttribute] = [] + + let resource: TelegramMediaResource + if let id = container.id, let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { + resource = LocalFileMediaResource(fileId: id, size: fileSize(path), isSecretRelated: isSecretRelated) + account.postbox.mediaBox.storeResourceData(resource.id, data: data) + } else { + resource = LocalFileReferenceMediaResource(localFilePath:path, randomId: randomId, isUniquelyReferencedTemporaryFile: true, size: fs(path)) + } + + + attrs.append(TelegramMediaFileAttribute.Video(duration: Int(container.duration), size: PixelDimensions(container.size), flags: [.instantRoundVideo])) + media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: previewForFile(path, isSecretRelated: isSecretRelated, account: account), videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: nil, attributes: attrs) - attrs.append(TelegramMediaFileAttribute.Video(duration: Int(container.duration), size: container.size, flags: [.instantRoundVideo])) - media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), resource: LocalFileReferenceMediaResource(localFilePath:path,randomId: randomId, isUniquelyReferencedTemporaryFile: true, size: fileSize(path)), previewRepresentations: previewForFile(path, account: account), mimeType: mimeType, size: nil, attributes: attrs) - + } else if mimeType.hasPrefix("image/webp") { + let resource = LocalFileReferenceMediaResource(localFilePath:path, randomId: randomId, isUniquelyReferencedTemporaryFile: false, size: fs(path)) + + media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: previewForFile(path, isSecretRelated: isSecretRelated, account: account), videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: nil, attributes: fileAttributes(for: mimeType, path: path, isMedia: true)) } else if mimeType.hasPrefix("image/") && !mimeType.hasSuffix("gif"), let imageData = try? Data(contentsOf: URL(fileURLWithPath: path)) { let options = NSMutableDictionary() @@ -196,16 +320,16 @@ class Sender: NSObject { if size.width / 10 > size.height || size.height < 40 { makeFileMedia(true) } else { - let imageRep = NSBitmapImageRep(cgImage: image) - let options: [NSBitmapImageRep.PropertyKey: Any] = [NSBitmapImageRep.PropertyKey.compressionFactor: Float(0.83)] + let data = compressImageToJPEG(image, quality: 0.83) let path = NSTemporaryDirectory() + "tg_image_\(arc4random()).jpeg" - try? imageRep.representation(using: NSBitmapImageRep.FileType.jpeg, properties: options)?.write(to: URL(fileURLWithPath: path)) + if let data = data { + try? data.write(to: URL(fileURLWithPath: path)) + } - - let scaledSize = size.aspectFilled(CGSize(width: 1280.0, height: 1280.0)) + let scaledSize = size.fitted(CGSize(width: 1280.0, height: 1280.0)) let resource = LocalFileReferenceMediaResource(localFilePath:path,randomId:randomId, isUniquelyReferencedTemporaryFile: true) - media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: scaledSize, resource: resource)]) + media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: PixelDimensions(scaledSize), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) } } else { @@ -216,13 +340,14 @@ class Sender: NSObject { } - } else if mimeType.hasSuffix("gif") { + } else if mimeType.hasPrefix("video") { let attrs:[TelegramMediaFileAttribute] = fileAttributes(for:mimeType, path:path, isMedia: true) + media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: LocalFileVideoMediaResource(randomId: randomId, path: container.path), previewRepresentations: previewForFile(path, isSecretRelated: isSecretRelated, account: account), videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: attrs) + } else if mimeType.hasPrefix("image/gif") { + let attrs:[TelegramMediaFileAttribute] = fileAttributes(for:mimeType, path:path, isMedia: true) - - - media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), resource: LocalFileGifMediaResource(randomId: arc4random64(), path: container.path), previewRepresentations: previewForFile(path, account: account), mimeType: "video/mp4", size: nil, attributes: attrs) + media = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: LocalFileGifMediaResource(randomId: randomId, path: container.path), previewRepresentations: previewForFile(path, isSecretRelated: isSecretRelated, account: account), videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: attrs) } else { makeFileMedia(true) } @@ -262,66 +387,122 @@ class Sender: NSObject { } attrs.append(.Audio(isVoice: false, duration: Int(CMTimeGetSeconds(asset.duration)), title: defaultTitle, performer: defaultPerformer, waveform: nil)) } - if mime.hasPrefix("video"), isMedia { let asset = AVURLAsset(url: URL(fileURLWithPath: path)) let video = asset.tracks(withMediaType: AVMediaType.video).first let audio = asset.tracks(withMediaType: AVMediaType.audio).first if let video = video { - attrs.append(TelegramMediaFileAttribute.Video(duration: Int(CMTimeGetSeconds(asset.duration)), size: video.naturalSize, flags: [])) - } - if audio == nil { - attrs.append(TelegramMediaFileAttribute.Animated) + var size = video.naturalSize.applying(video.preferredTransform) + size = NSMakeSize(floor(abs(size.width)), floor(abs(size.height))) + attrs.append(TelegramMediaFileAttribute.Video(duration: Int(CMTimeGetSeconds(asset.duration)), size: PixelDimensions(size), flags: [])) + attrs.append(TelegramMediaFileAttribute.FileName(fileName: path.nsstring.lastPathComponent.nsstring.deletingPathExtension.appending(".mp4"))) + if audio == nil, let size = fileSize(path), size < Int32(10 * 1024 * 1024), mime.hasSuffix("mp4") { + attrs.append(TelegramMediaFileAttribute.Animated) + } + if !mime.hasSuffix("mp4") { + attrs.append(.hintFileIsLarge) + } + return attrs } } if mime.hasSuffix("gif"), isMedia { - attrs.append(TelegramMediaFileAttribute.Video(duration: 0, size:TGGifConverter.gifDimensionSize(path), flags: [])) + attrs.append(TelegramMediaFileAttribute.Video(duration: 0, size:TGGifConverter.gifDimensionSize(path).pixel, flags: [])) attrs.append(TelegramMediaFileAttribute.Animated) attrs.append(TelegramMediaFileAttribute.FileName(fileName: path.nsstring.lastPathComponent.nsstring.deletingPathExtension.appending(".mp4"))) - } else { + } else if mime.hasPrefix("image"), let image = NSImage(contentsOf: URL(fileURLWithPath: path)), !mime.hasPrefix("image/webp") { + var size = image.size + if size.width == .infinity || size.height == .infinity { + size = image.cgImage(forProposedRect: nil, context: nil, hints: nil)!.size + } + attrs.append(TelegramMediaFileAttribute.ImageSize(size: size.pixel)) attrs.append(TelegramMediaFileAttribute.FileName(fileName: path.nsstring.lastPathComponent)) + + if mime.hasPrefix("image/webp") { + attrs.append(.Sticker(displayText: "", packReference: nil, maskData: nil)) + } + } else if mime.hasPrefix("image/webp") { + var size: NSSize = NSMakeSize(512, 512) + if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) { + size = convertFromWebP(data)?.size ?? size + } + + attrs.append(TelegramMediaFileAttribute.ImageSize(size: size.pixel)) + attrs.append(TelegramMediaFileAttribute.FileName(fileName: path.nsstring.lastPathComponent)) + attrs.append(.Sticker(displayText: "", packReference: nil, maskData: nil)) + + } else { + let getname:(String)->String = { path in + var result: String = path.nsstring.lastPathComponent + if result.contains("tg_temp_archive_") { + result = "Telegram Archive" + } + if path.isDirectory { + result += ".zip" + } + return result + } + attrs.append(TelegramMediaFileAttribute.FileName(fileName: getname(path))) } return attrs } - public static func forwardMessages(messageIds:[MessageId], account:Account, peerId:PeerId) -> Signal<[MessageId?], NoError> { + public static func forwardMessages(messageIds:[MessageId], context: AccountContext, peerId:PeerId, hideNames: Bool = false, silent: Bool = false, atDate: Date? = nil) -> Signal<[MessageId?], NoError> { var fwdMessages:[EnqueueMessage] = [] let sorted = messageIds.sorted(by: >) + var attributes: [MessageAttribute] = [] + if FastSettings.isChannelMessagesMuted(peerId) || silent { + attributes.append(NotificationInfoMessageAttribute(flags: [.muted])) + } + if hideNames { + attributes.append(ForwardOptionsMessageAttribute(hideNames: hideNames, hideCaptions: false)) + } + + if let date = atDate { + attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: Int32(date.timeIntervalSince1970))) + } for msgId in sorted { - fwdMessages.append(EnqueueMessage.forward(source: msgId)) + fwdMessages.append(EnqueueMessage.forward(source: msgId, grouping: messageIds.count > 1 ? .auto : .none, attributes: attributes, correlationId: nil)) } - return enqueueMessages(account: account, peerId: peerId, messages: fwdMessages.reversed()) + return enqueueMessages(account: context.account, peerId: peerId, messages: fwdMessages.reversed()) } - public static func shareContact(account:Account, peerId:PeerId, contact:TelegramUser) -> Signal<[MessageId?], NoError> { + public static func shareContact(context: AccountContext, peerId:PeerId, contact:TelegramUser) -> Signal<[MessageId?], NoError> { var attributes:[MessageAttribute] = [] if FastSettings.isChannelMessagesMuted(peerId) { attributes.append(NotificationInfoMessageAttribute(flags: [.muted])) } - return enqueueMessages(account: account, peerId: peerId, messages: [EnqueueMessage.message(text: "", attributes: attributes, media: TelegramMediaContact(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumber: contact.phone ?? "", peerId: contact.id), replyToMessageId: nil)]) + return enqueueMessages(account: context.account, peerId: peerId, messages: [EnqueueMessage.message(text: "", attributes: attributes, mediaReference: AnyMediaReference.standalone(media: TelegramMediaContact(firstName: contact.firstName ?? "", lastName: contact.lastName ?? "", phoneNumber: contact.phone ?? "", peerId: contact.id, vCardData: nil)), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)]) } - public static func enqueue(media:[MediaSenderContainer], account:Account, peerId:PeerId, chatInteraction:ChatInteraction) ->Signal<[MessageId?],NoError> { - var senders:[Signal<[MessageId?],NoError>] = [] + public static func enqueue(media:[MediaSenderContainer], context: AccountContext, peerId:PeerId, chatInteraction:ChatInteraction, silent: Bool = false, atDate:Date? = nil, query: String? = nil) ->Signal<[MessageId?], NoError> { + var senders:[Signal<[MessageId?], NoError>] = [] + var attributes:[MessageAttribute] = [] - if FastSettings.isChannelMessagesMuted(peerId) { + if FastSettings.isChannelMessagesMuted(peerId) || silent { attributes.append(NotificationInfoMessageAttribute(flags: [.muted])) } + if let date = atDate { + attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: Int32(date.timeIntervalSince1970))) + } + if let query = query, !query.isEmpty { + attributes.append(EmojiSearchQueryMessageAttribute(query: query)) + } - for path in media { - senders.append(generateMedia(for: path, account: account) |> mapToSignal { media, caption -> Signal< [MessageId?], NoError> in - - return enqueueMessages(account: account, peerId: peerId, messages: [EnqueueMessage.message(text: caption, attributes:attributes, media: media, replyToMessageId: chatInteraction.presentation.interfaceState.replyMessageId)]) + let replyId = chatInteraction.presentation.interfaceState.replyMessageId ?? chatInteraction.mode.threadId + + for path in media { + senders.append(generateMedia(for: path, account: context.account, isSecretRelated: peerId.namespace == Namespaces.Peer.SecretChat) |> mapToSignal { media, caption -> Signal< [MessageId?], NoError> in + return enqueueMessages(account: context.account, peerId: peerId, messages: [EnqueueMessage.message(text: caption, attributes:attributes, mediaReference: AnyMediaReference.standalone(media: media), replyToMessageId: replyId, localGroupingKey: nil, correlationId: nil)]) }) } @@ -338,16 +519,64 @@ class Sender: NSObject { } } - public static func enqueue(media:TelegramMediaFile, account:Account, peerId:PeerId, chatInteraction:ChatInteraction) ->Signal<[MessageId?],NoError> { + public static func enqueue(media:Media, context: AccountContext, peerId:PeerId, chatInteraction:ChatInteraction, silent: Bool = false, atDate: Date? = nil, query: String? = nil) ->Signal<[MessageId?],NoError> { + return enqueue(media: [media], caption: ChatTextInputState(), context: context, peerId: peerId, chatInteraction: chatInteraction, silent: silent, atDate: atDate, query: query) + } + + public static func enqueue(media:[Media], caption: ChatTextInputState, context: AccountContext, peerId:PeerId, chatInteraction:ChatInteraction, isCollage: Bool = false, additionText: ChatTextInputState? = nil, silent: Bool = false, atDate: Date? = nil, query: String? = nil) ->Signal<[MessageId?],NoError> { - var attributes:[MessageAttribute] = [] - if FastSettings.isChannelMessagesMuted(peerId) { + + let parsingUrlType: ParsingType + if peerId.namespace != Namespaces.Peer.SecretChat { + parsingUrlType = [.Hashtags] + } else { + parsingUrlType = [.Links, .Hashtags] + } + + var attributes:[MessageAttribute] = [TextEntitiesMessageAttribute(entities: caption.messageTextEntities(parsingUrlType))] + let caption = Atomic(value: caption) + if FastSettings.isChannelMessagesMuted(peerId) || silent { attributes.append(NotificationInfoMessageAttribute(flags: [.muted])) } + if let date = atDate { + attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: Int32(date.timeIntervalSince1970))) + } + if let query = query, !query.isEmpty { + attributes.append(EmojiSearchQueryMessageAttribute(query: query)) + } + + let replyId = chatInteraction.presentation.interfaceState.replyMessageId ?? chatInteraction.mode.threadId - return enqueueMessages(account: account, peerId: peerId, messages: [EnqueueMessage.message(text: "", attributes: attributes, media: media, replyToMessageId: chatInteraction.presentation.interfaceState.replyMessageId)]) |> deliverOnMainQueue |> afterNext({ (value) -> Void in + let localGroupingKey = isCollage ? arc4random64() : nil + + var messages = media.map({EnqueueMessage.message(text: caption.swap(ChatTextInputState()).inputText, attributes: attributes, mediaReference: AnyMediaReference.standalone(media: $0), replyToMessageId: replyId, localGroupingKey: localGroupingKey, correlationId: nil)}) + if let input = additionText { + var inset:Int = 0 + var input:ChatTextInputState = input + + if input.attributes.isEmpty { + input = ChatTextInputState(inputText: input.inputText.trimmed) + } + let mapped = cut_long_message( input.inputText, 4096).map { message -> EnqueueMessage in + let subState = input.subInputState(from: NSMakeRange(inset, message.length)) + inset += message.length + + var attributes:[MessageAttribute] = [TextEntitiesMessageAttribute(entities: subState.messageTextEntities(parsingUrlType))] + + if FastSettings.isChannelMessagesMuted(peerId) || silent { + attributes.append(NotificationInfoMessageAttribute(flags: [.muted])) + } + if let date = atDate { + attributes.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: Int32(date.timeIntervalSince1970))) + } + + return EnqueueMessage.message(text: subState.inputText, attributes: attributes, mediaReference: nil, replyToMessageId: replyId, localGroupingKey: nil, correlationId: nil) + } + messages.insert(contentsOf: mapped, at: 0) + } + return enqueueMessages(account: context.account, peerId: peerId, messages: messages) |> deliverOnMainQueue |> afterNext { _ -> Void in chatInteraction.update({$0.updatedInterfaceState({$0.withUpdatedReplyMessageId(nil)})}) - }) + } |> take(1) } diff --git a/Telegram-Mac/SendingClockProgress.swift b/Telegram-Mac/SendingClockProgress.swift index 3f47233502..7075450a73 100644 --- a/Telegram-Mac/SendingClockProgress.swift +++ b/Telegram-Mac/SendingClockProgress.swift @@ -24,23 +24,46 @@ class SendingClockProgress: View { override init() { clockFrame = CALayer() - clockFrame.contents = theme.icons.chatSendingFrame - clockFrame.frame = NSMakeRect(0, 0, theme.icons.chatSendingFrame.backingSize.width, theme.icons.chatSendingFrame.backingSize.height) + clockFrame.contents = theme.icons.chatSendingOutFrame + clockFrame.frame = theme.icons.chatSendingOutFrame.backingBounds clockHour = CALayer() - clockHour.contents = theme.icons.chatSendingHour - clockHour.frame = NSMakeRect(0, 0, theme.icons.chatSendingHour.backingSize.width, theme.icons.chatSendingHour.backingSize.height) + clockHour.contents = theme.icons.chatSendingOutHour + clockHour.frame = theme.icons.chatSendingOutHour.backingBounds clockMin = CALayer() - clockMin.contents = theme.icons.chatSendingMin - clockMin.frame = NSMakeRect(0, 0, theme.icons.chatSendingMin.backingSize.width, theme.icons.chatSendingMin.backingSize.height) + clockMin.contents = theme.icons.chatSendingOutMin + clockMin.frame = theme.icons.chatSendingOutMin.backingBounds super.init(frame:NSMakeRect(0, 0, 12, 12)) - self.backgroundColor = .white + self.backgroundColor = .clear self.layer?.addSublayer(clockFrame) self.layer?.addSublayer(clockHour) self.layer?.addSublayer(clockMin) + + } + + override func layout() { + super.layout() + + clockMin.frame = focus(theme.icons.chatSendingOutMin.backingSize) + clockHour.frame = focus(theme.icons.chatSendingOutHour.backingSize) + } + + + func set(item: ChatRowItem) { + clockFrame.contents = item.presentation.chat.sendingFrameIcon(item) + clockHour.contents = item.presentation.chat.sendingHourIcon(item) + clockMin.contents = item.presentation.chat.sendingMinIcon(item) + viewDidMoveToWindow() + } + + func applyGray() { + clockFrame.contents = theme.icons.chatSendingOutFrame + clockHour.contents = theme.icons.chatSendingOutHour + clockMin.contents = theme.icons.chatSendingOutMin + viewDidMoveToWindow() } required init?(coder: NSCoder) { @@ -68,21 +91,27 @@ class SendingClockProgress: View { private func animateHour() -> Void { let animation = CABasicAnimation(keyPath: "transform.rotation.z") - animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) - animation.duration = (minute_duration * 4.0) + 0.6 - animation.repeatCount = .greatestFiniteMagnitude - animation.toValue = (Double.pi * 2.0) as NSNumber - clockHour.add(animation, forKey: "rotate") + animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) + animation.duration = 6 + animation.repeatCount = .infinity + animation.fromValue = 0 + animation.toValue = (Double.pi * 2.0) + animation.beginTime = 1.0 + animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) + clockHour.add(animation, forKey: "clockFrameAnimation") } private func animateMin() -> Void { let animation = CABasicAnimation(keyPath: "transform.rotation.z") - animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) - animation.duration = minute_duration - animation.repeatCount = .greatestFiniteMagnitude - animation.toValue = (Double.pi * 2.0) as NSNumber - clockMin.add(animation, forKey: "rotate") + animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) + animation.duration = 1 + animation.repeatCount = .infinity + animation.fromValue = 0 + animation.toValue = (Double.pi * 2.0) + animation.beginTime = 1.0 + animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) + clockMin.add(animation, forKey: "clockFrameAnimation") } public func stopAnimating() -> Void { @@ -95,8 +124,16 @@ class SendingClockProgress: View { clockMin.removeAllAnimations() } + + override func viewDidMoveToSuperview() { + if window != nil && superview != nil { + startAnimating() + } else { + stopAnimating() + } + } override func viewDidMoveToWindow() { - if window != nil { + if window != nil && superview != nil { startAnimating() } else { stopAnimating() diff --git a/Telegram-Mac/SeparatorRowItem.swift b/Telegram-Mac/SeparatorRowItem.swift index 5f59998e1e..76ac271a0b 100644 --- a/Telegram-Mac/SeparatorRowItem.swift +++ b/Telegram-Mac/SeparatorRowItem.swift @@ -18,34 +18,30 @@ enum SeparatorBlockState { -class SeparatorRowItem: TableRowItem { +class SeparatorRowItem: GeneralRowItem { public var text:NSAttributedString; - private let h:CGFloat let rightText:NSAttributedString? - var border:BorderType = [.Right] let state:SeparatorBlockState - override var height: CGFloat { - return h - } - private let _stableId:AnyHashable - override var stableId: AnyHashable { - return _stableId - } - init(_ initialSize:NSSize, _ stableId:AnyHashable, string:String, right:String? = nil, state: SeparatorBlockState = .none, height:CGFloat = 20.0) { - self._stableId = stableId - self.h = height + let leftInset: CGFloat? + let itemAction: (()->Void)? + init(_ initialSize:NSSize, _ stableId:AnyHashable, string:String, right:String? = nil, state: SeparatorBlockState = .none, height:CGFloat = 20.0, action: (()->Void)? = nil, leftInset: CGFloat? = nil, border:BorderType = [], customTheme: GeneralRowItem.Theme = GeneralRowItem.Theme()) { + self.leftInset = leftInset self.state = state - text = .initialize(string: string, color: theme.colors.grayText, font:.normal(.short)) + self.itemAction = action + text = .initialize(string: string, color: customTheme.grayTextColor, font:.normal(.short)) if let right = right { - self.rightText = .initialize(string: right, color: theme.colors.grayText, font:.normal(.short)) + self.rightText = .initialize(string: right, color: customTheme.grayTextColor, font:.normal(.short)) } else { rightText = nil } - super.init(initialSize) + super.init(initialSize, height: height, stableId: stableId, type: .none, viewType: .legacy, border: border, error: nil, customTheme: customTheme) + } + override var instantlyResize: Bool { + return true } @@ -62,9 +58,16 @@ class SeparatorRowView: TableRowView { required init(frame frameRect: NSRect) { super.init(frame: frameRect) + layerContentsRedrawPolicy = .onSetNeedsDisplay } override var backdorColor: NSColor { + if let item = item as? GeneralRowItem, let customTheme = item.customTheme { + return customTheme.grayBackground + } + if let backgroundColor = (item as? SeparatorRowItem)?.backgroundColor { + return backgroundColor + } return theme.colors.grayBackground } @@ -72,25 +75,49 @@ class SeparatorRowView: TableRowView { fatalError("init(coder:) has not been implemented") } + override func mouseDown(with event: NSEvent) { + guard let item = item as? SeparatorRowItem else {return} + let point = convert(event.locationInWindow, from: nil) + + if let text = item.rightText { + let (layout, _) = TextNode.layoutText(maybeNode: stateText, text, nil, 1, .end, NSMakeSize(frame.width, frame.height), nil, false, .left) + + let rect = NSMakeRect(frame.width - 10 - layout.size.width, round((frame.height - layout.size.height)/2.0), layout.size.width, frame.height) + if NSPointInRect(point, rect) { + if let itemAction = item.itemAction { + itemAction() + } else { + super.mouseDown(with: event) + } + } + } else { + super.mouseDown(with: event) + } + } + override func draw(_ layer: CALayer, in ctx: CGContext) { super.draw(layer, in: ctx) + if backingScaleFactor == 1.0 { + ctx.setFillColor(backdorColor.cgColor) + ctx.fill(layer.bounds) + } if let item = self.item as? SeparatorRowItem { let (layout, apply) = TextNode.layoutText(maybeNode: text, item.text, nil, 1, .end, NSMakeSize(frame.width, frame.height), nil,false, .left) let textPoint:NSPoint if let text = item.rightText { - textPoint = NSMakePoint(10, round((frame.height - layout.size.height)/2.0)) + textPoint = NSMakePoint(item.leftInset ?? 10, round((frame.height - layout.size.height)/2.0) - 1) let (layout, apply) = TextNode.layoutText(maybeNode: stateText, text, nil, 1, .end, NSMakeSize(frame.width, frame.height), nil, false, .left) - apply.draw(NSMakeRect(frame.width - 10 - layout.size.width, round((frame.height - layout.size.height)/2.0), layout.size.width, layout.size.height), in: ctx, backingScaleFactor: backingScaleFactor) + apply.draw(NSMakeRect(frame.width - 10 - layout.size.width, round((frame.height - layout.size.height)/2.0) - 1, layout.size.width, layout.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) } else { - textPoint = NSMakePoint(10, round((frame.height - layout.size.height)/2.0)) + textPoint = NSMakePoint(item.leftInset ?? 10, round((frame.height - layout.size.height)/2.0) - 1) } - apply.draw(NSMakeRect(textPoint.x, textPoint.y, layout.size.width, layout.size.height), in: ctx, backingScaleFactor: backingScaleFactor) + apply.draw(NSMakeRect(textPoint.x, textPoint.y, layout.size.width, layout.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) } } @@ -99,7 +126,7 @@ class SeparatorRowView: TableRowView { if let item = item as? SeparatorRowItem { self.border = item.border } - + needsDisplay = true } } diff --git a/Telegram-Mac/SettingsSearchRecentQueries.swift b/Telegram-Mac/SettingsSearchRecentQueries.swift new file mode 100644 index 0000000000..7abd26bd22 --- /dev/null +++ b/Telegram-Mac/SettingsSearchRecentQueries.swift @@ -0,0 +1,78 @@ +// +// SettingsSearchRecentQueries.swift +// Telegram +// +// Created by Mikhail Filimonov on 02.12.2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import Postbox +import SwiftSignalKit + +private struct SettingsSearchRecentQueryItemId { + public let rawValue: MemoryBuffer + + var value: Int64 { + return self.rawValue.makeData().withUnsafeBytes { $0.pointee } as Int64 + } + + init(_ rawValue: MemoryBuffer) { + self.rawValue = rawValue + } + + init(_ value: Int64) { + var value = value + self.rawValue = MemoryBuffer(data: Data(bytes: &value, count: MemoryLayout.size(ofValue: value))) + } +} + +public final class RecentSettingsSearchQueryItem: OrderedItemListEntryContents { + public init() { + } + + public init(decoder: PostboxDecoder) { + } + + public func encode(_ encoder: PostboxEncoder) { + } +} + +func addRecentSettingsSearchItem(postbox: Postbox, item: SettingsSearchableItemId) { + let _ = (postbox.transaction { transaction in + let itemId = SettingsSearchRecentQueryItemId(item.index) + transaction.addOrMoveToFirstPositionOrderedItemListItem(collectionId: ApplicationSpecificOrderedItemListCollectionId.settingsSearchRecentItems, item: OrderedItemListEntry(id: itemId.rawValue, contents: RecentSettingsSearchQueryItem()), removeTailIfCountExceeds: 100) + }).start() +} + +func removeRecentSettingsSearchItem(postbox: Postbox, item: SettingsSearchableItemId) { + let _ = (postbox.transaction { transaction -> Void in + let itemId = SettingsSearchRecentQueryItemId(item.index) + transaction.removeOrderedItemListItem(collectionId: ApplicationSpecificOrderedItemListCollectionId.settingsSearchRecentItems, itemId: itemId.rawValue) + }).start() +} + +func clearRecentSettingsSearchItems(postbox: Postbox) { + let _ = (postbox.transaction { transaction -> Void in + transaction.replaceOrderedItemListItems(collectionId: ApplicationSpecificOrderedItemListCollectionId.settingsSearchRecentItems, items: []) + }).start() +} + +func settingsSearchRecentItems(postbox: Postbox) -> Signal<[SettingsSearchableItemId], NoError> { + return postbox.combinedView(keys: [.orderedItemList(id: ApplicationSpecificOrderedItemListCollectionId.settingsSearchRecentItems)]) + |> mapToSignal { view -> Signal<[SettingsSearchableItemId], NoError> in + return postbox.transaction { transaction -> [SettingsSearchableItemId] in + var result: [SettingsSearchableItemId] = [] + if let view = view.views[.orderedItemList(id: ApplicationSpecificOrderedItemListCollectionId.settingsSearchRecentItems)] as? OrderedItemListView { + for item in view.items { + let index = SettingsSearchRecentQueryItemId(item.id).value + if let itemId = SettingsSearchableItemId(index: index) { + result.append(itemId) + } + } + } + return result + } + } +} + diff --git a/Telegram-Mac/SettingsSearchableItems.swift b/Telegram-Mac/SettingsSearchableItems.swift new file mode 100644 index 0000000000..f1a46482ce --- /dev/null +++ b/Telegram-Mac/SettingsSearchableItems.swift @@ -0,0 +1,761 @@ +// +// SettingsSearchableItems.swift +// Telegram +// +// Created by Mikhail Filimonov on 02.12.2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import Postbox +import TGUIKit +import SwiftSignalKit +import TelegramCore + + +enum SettingsSearchableItemIcon { + case profile + case proxy + case savedMessages + case calls + case stickers + case notifications + case privacy + case data + case appearance + case language + case watch + case wallet + case passport + case support + case faq +} + +extension SettingsSearchableItemIcon { + var thumb: CGImage? { + switch self { + case .profile: + return theme.icons.settingsProfile + case .proxy: + return theme.icons.settingsProxy + case .stickers: + return theme.icons.settingsStickers + case .notifications: + return theme.icons.settingsNotifications + case .privacy: + return theme.icons.settingsSecurity + case .data: + return theme.icons.settingsStorage + case .appearance: + return theme.icons.settingsAppearance + case .language: + return theme.icons.settingsLanguage + case .support: + return theme.icons.settingsAskQuestion + case .faq: + return theme.icons.settingsFaq + default: + return nil + } + } +} + + + + +enum SettingsSearchableItemId: Hashable { + case profile(Int32) + case proxy(Int32) + case savedMessages(Int32) + case calls(Int32) + case stickers(Int32) + case notifications(Int32) + case privacy(Int32) + case data(Int32) + case appearance(Int32) + case language(Int32) + case watch(Int32) + case passport(Int32) + case wallet(Int32) + case support(Int32) + case faq(Int32) + + private var namespace: Int32 { + switch self { + case .profile: + return 1 + case .proxy: + return 2 + case .savedMessages: + return 3 + case .calls: + return 4 + case .stickers: + return 5 + case .notifications: + return 6 + case .privacy: + return 7 + case .data: + return 8 + case .appearance: + return 9 + case .language: + return 10 + case .watch: + return 11 + case .passport: + return 12 + case .wallet: + return 13 + case .support: + return 14 + case .faq: + return 15 + } + } + + private var id: Int32 { + switch self { + case let .profile(id), + let .proxy(id), + let .savedMessages(id), + let .calls(id), + let .stickers(id), + let .notifications(id), + let .privacy(id), + let .data(id), + let .appearance(id), + let .language(id), + let .watch(id), + let .passport(id), + let .wallet(id), + let .support(id), + let .faq(id): + return id + } + } + + var index: Int64 { + return (Int64(self.namespace) << 32) | Int64(self.id) + } + + init?(index: Int64) { + let namespace = Int32((index >> 32) & 0x7fffffff) + let id = Int32(bitPattern: UInt32(index & 0xffffffff)) + switch namespace { + case 1: + self = .profile(id) + case 2: + self = .proxy(id) + case 3: + self = .savedMessages(id) + case 4: + self = .calls(id) + case 5: + self = .stickers(id) + case 6: + self = .notifications(id) + case 7: + self = .privacy(id) + case 8: + self = .data(id) + case 9: + self = .appearance(id) + case 10: + self = .language(id) + case 11: + self = .watch(id) + case 12: + self = .passport(id) + case 13: + self = .wallet(id) + case 14: + self = .support(id) + case 15: + self = .faq(id) + default: + return nil + } + } +} + +enum SettingsSearchableItemPresentation { + case push + case modal + case immediate + case dismiss +} + + + +struct SettingsSearchableItem { + let id: SettingsSearchableItemId + let title: String + let alternate: [String] + let icon: SettingsSearchableItemIcon + let breadcrumbs: [String] + let present: (AccountContext, NavigationViewController?, @escaping (SettingsSearchableItemPresentation, ViewController?) -> Void) -> Void +} + + + +func searchSettingsItems(items: [SettingsSearchableItem], query: String) -> [SettingsSearchableItem] { + let queryTokens = stringTokens(query.lowercased()) + + var result: [SettingsSearchableItem] = [] + for item in items { + var string = item.title + if !item.alternate.isEmpty { + for alternate in item.alternate { + let trimmed = alternate.trimmingCharacters(in: .whitespacesAndNewlines) + if !trimmed.isEmpty { + string += " \(trimmed)" + } + } + } + if item.breadcrumbs.count > 1 { + string += " \(item.breadcrumbs.suffix(from: 1).joined(separator: " "))" + } + + let tokens = stringTokens(string) + if matchStringTokens(tokens, with: queryTokens) { + result.append(item) + } + } + + return result +} + + +private func synonyms(_ string: String?) -> [String] { + if let string = string, !string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return string.components(separatedBy: "\n") + } else { + return [] + } +} + + + +private func stringTokens(_ string: String) -> [ValueBoxKey] { + let nsString = string.folding(options: .diacriticInsensitive, locale: .current).lowercased() as NSString + + let flag = UInt(kCFStringTokenizerUnitWord) + let tokenizer = CFStringTokenizerCreate(kCFAllocatorDefault, nsString, CFRangeMake(0, nsString.length), flag, CFLocaleCopyCurrent()) + var tokenType = CFStringTokenizerAdvanceToNextToken(tokenizer) + var tokens: [ValueBoxKey] = [] + + var addedTokens = Set() + while tokenType != [] { + let currentTokenRange = CFStringTokenizerGetCurrentTokenRange(tokenizer) + + if currentTokenRange.location >= 0 && currentTokenRange.length != 0 { + let token = ValueBoxKey(length: currentTokenRange.length * 2) + nsString.getCharacters(token.memory.assumingMemoryBound(to: unichar.self), range: NSMakeRange(currentTokenRange.location, currentTokenRange.length)) + if !addedTokens.contains(token) { + tokens.append(token) + addedTokens.insert(token) + } + } + tokenType = CFStringTokenizerAdvanceToNextToken(tokenizer) + } + + return tokens +} + +private func matchStringTokens(_ tokens: [ValueBoxKey], with other: [ValueBoxKey]) -> Bool { + if other.isEmpty { + return false + } else if other.count == 1 { + let otherToken = other[0] + for token in tokens { + if otherToken.isPrefix(to: token) { + return true + } + } + } else { + for otherToken in other { + var found = false + for token in tokens { + if otherToken.isPrefix(to: token) { + found = true + break + } + } + if !found { + return false + } + } + return true + } + return false +} + + +private func profileSearchableItems(context: AccountContext, canAddAccount: Bool) -> [SettingsSearchableItem] { + let icon: SettingsSearchableItemIcon = .profile + + let presentProfileSettings: (AccountContext, @escaping (SettingsSearchableItemPresentation, ViewController?) -> Void, EditSettingsEntryTag?) -> Void = { context, present, itemTag in + EditAccountInfoController(context: context, focusOnItemTag: itemTag, f: { controller in + present(.push, controller) + }) + } + + var items: [SettingsSearchableItem] = [] + items.append(SettingsSearchableItem(id: .profile(0), title: L10n.editAccountTitle, alternate: synonyms(L10n.settingsSearchSynonymsEditProfileTitle), icon: icon, breadcrumbs: [], present: { context, _, present in + presentProfileSettings(context, present, nil) + })) + + items.append(SettingsSearchableItem(id: .profile(1), title: L10n.accountSettingsBio, alternate: synonyms(L10n.settingsSearchSynonymsEditProfileTitle), icon: icon, breadcrumbs: [L10n.editAccountTitle], present: { context, _, present in + presentProfileSettings(context, present, .bio) + })) + items.append(SettingsSearchableItem(id: .profile(2), title: L10n.editAccountChangeNumber, alternate: synonyms(L10n.settingsSearchSynonymsEditProfilePhoneNumber), icon: icon, breadcrumbs: [L10n.editAccountTitle], present: { context, _, present in + present(.push, PhoneNumberIntroController(context)) + })) + items.append(SettingsSearchableItem(id: .profile(3), title: L10n.editAccountUsername, alternate: synonyms(L10n.settingsSearchSynonymsEditProfileUsername), icon: icon, breadcrumbs: [L10n.editAccountTitle], present: { context, _, present in + present(.push, UsernameSettingsViewController(context)) + })) + if canAddAccount { + items.append(SettingsSearchableItem(id: .profile(4), title: L10n.editAccountAddAccount, alternate: synonyms(L10n.settingsSearchSynonymsEditProfileAddAccount), icon: icon, breadcrumbs: [L10n.editAccountTitle], present: { context, _, present in + let isTestingEnvironment = NSApp.currentEvent?.modifierFlags.contains(.command) == true + context.sharedContext.beginNewAuth(testingEnvironment: isTestingEnvironment) + })) + } + items.append(SettingsSearchableItem(id: .profile(5), title: L10n.editAccountLogout, alternate: synonyms(L10n.settingsSearchSynonymsEditProfileLogout), icon: icon, breadcrumbs: [L10n.editAccountTitle], present: { context, navigationController, present in + showModal(with: LogoutViewController(context: context, f: { controller in + present(.push, controller) + }), for: context.window) + })) + return items +} + + + +private func stickerSearchableItems(context: AccountContext, archivedStickerPacks: [ArchivedStickerPackItem]?) -> [SettingsSearchableItem] { + let icon: SettingsSearchableItemIcon = .stickers + + let presentStickerSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void, InstalledStickerPacksEntryTag?) -> Void = { context, present, itemTag in + present(.push, InstalledStickerPacksController(context, focusOnItemTag: itemTag)) + } + + var items: [SettingsSearchableItem] = [] + + items.append(SettingsSearchableItem(id: .stickers(0), title: L10n.accountSettingsStickers, alternate: synonyms(L10n.settingsSearchSynonymsStickersTitle), icon: icon, breadcrumbs: [], present: { context, _, present in + presentStickerSettings(context, present, nil) + })) + items.append(SettingsSearchableItem(id: .stickers(1), title: L10n.stickersSuggestStickers, alternate: synonyms(L10n.settingsSearchSynonymsStickersSuggestStickers), icon: icon, breadcrumbs: [L10n.accountSettingsStickers], present: { context, _, present in + presentStickerSettings(context, present, .suggestOptions) + })) + items.append(SettingsSearchableItem(id: .stickers(3), title: L10n.installedStickersTranding, alternate: synonyms(L10n.settingsSearchSynonymsStickersFeaturedPacks), icon: icon, breadcrumbs: [L10n.accountSettingsStickers], present: { context, _, present in + present(.push, FeaturedStickerPacksController(context)) + })) + items.append(SettingsSearchableItem(id: .stickers(4), title: L10n.installedStickersArchived, alternate: synonyms(L10n.settingsSearchSynonymsStickersArchivedPacks), icon: icon, breadcrumbs: [L10n.accountSettingsStickers], present: { context, _, present in + present(.push, ArchivedStickerPacksController(context, archived: nil, updatedPacks: { _ in })) + })) + return items +} + +private func notificationSearchableItems(context: AccountContext, settings: GlobalNotificationSettingsSet) -> [SettingsSearchableItem] { + let icon: SettingsSearchableItemIcon = .notifications + + let presentNotificationSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void, NotificationsAndSoundsEntryTag?) -> Void = { context, present, itemTag in + present(.push, NotificationPreferencesController(context, focusOnItemTag: itemTag)) + } + + return [ + SettingsSearchableItem(id: .notifications(0), title: L10n.accountSettingsNotifications, alternate: synonyms(L10n.settingsSearchSynonymsNotificationsTitle), icon: icon, breadcrumbs: [], present: { context, _, present in + presentNotificationSettings(context, present, nil) + }), + SettingsSearchableItem(id: .notifications(2), title: L10n.notificationSettingsMessagesPreview, alternate: synonyms(L10n.settingsSearchSynonymsNotificationsMessageNotificationsPreview), icon: icon, breadcrumbs: [L10n.accountSettingsNotifications, L10n.notificationSettingsToggleNotificationsHeader], present: { context, _, present in + presentNotificationSettings(context, present, .messagePreviews) + }), +// SettingsSearchableItem(id: .notifications(18), title: L10n.notificationSettingsIncludeGroups, alternate: synonyms(L10n.settingsSearchSynonymsNotificationsBadgeIncludeMutedPublicGroups), icon: icon, breadcrumbs: [L10n.accountSettingsNotifications, L10n.notificationSettingsBadgeHeader], present: { context, _, present in +// presentNotificationSettings(context, present, .includePublicGroups) +// }), + SettingsSearchableItem(id: .notifications(19), title: L10n.notificationSettingsIncludeChannels, alternate: synonyms(L10n.settingsSearchSynonymsNotificationsBadgeIncludeMutedChannels), icon: icon, breadcrumbs: [L10n.accountSettingsNotifications, L10n.notificationSettingsBadgeHeader], present: { context, _, present in + presentNotificationSettings(context, present, .includeChannels) + }), + SettingsSearchableItem(id: .notifications(20), title: L10n.notificationSettingsCountUnreadMessages, alternate: synonyms(L10n.settingsSearchSynonymsNotificationsBadgeCountUnreadMessages), icon: icon, breadcrumbs: [L10n.accountSettingsNotifications, L10n.notificationSettingsBadgeHeader], present: { context, _, present in + presentNotificationSettings(context, present, .unreadCountCategory) + }), + SettingsSearchableItem(id: .notifications(21), title: L10n.notificationSettingsContactJoined, alternate: synonyms(L10n.settingsSearchSynonymsNotificationsContactJoined), icon: icon, breadcrumbs: [L10n.accountSettingsNotifications], present: { context, _, present in + presentNotificationSettings(context, present, .joinedNotifications) + }), + SettingsSearchableItem(id: .notifications(22), title: L10n.notificationSettingsResetNotifications, alternate: synonyms(L10n.settingsSearchSynonymsNotificationsResetAllNotifications), icon: icon, breadcrumbs: [L10n.accountSettingsNotifications], present: { context, _, present in + presentNotificationSettings(context, present, .reset) + }) + ] +} + +private func privacySearchableItems(context: AccountContext, privacySettings: AccountPrivacySettings?) -> [SettingsSearchableItem] { + let icon: SettingsSearchableItemIcon = .privacy + + let presentPrivacySettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void, PrivacyAndSecurityEntryTag?) -> Void = { context, present, itemTag in + present(.push, PrivacyAndSecurityViewController(context, initialSettings: nil, focusOnItemTag: itemTag)) + } + + let presentSelectivePrivacySettings: (AccountContext, SelectivePrivacySettingsKind, @escaping (SettingsSearchableItemPresentation, ViewController?) -> Void) -> Void = { context, kind, present in + let privacySignal: Signal + if let privacySettings = privacySettings { + privacySignal = .single(privacySettings) + } else { + privacySignal = context.engine.privacy.requestAccountPrivacySettings() + } + let callsSignal: Signal<(VoiceCallSettings, VoipConfiguration)?, NoError> + if case .voiceCalls = kind { + callsSignal = combineLatest(context.sharedContext.accountManager.sharedData(keys: [ApplicationSharedPreferencesKeys.voiceCallSettings]), context.account.postbox.preferencesView(keys: [PreferencesKeys.voipConfiguration])) + |> take(1) + |> map { sharedData, view -> (VoiceCallSettings, VoipConfiguration)? in + let voiceCallSettings: VoiceCallSettings = sharedData.entries[ApplicationSharedPreferencesKeys.voiceCallSettings] as? VoiceCallSettings ?? .defaultSettings + let voipConfiguration = view.values[PreferencesKeys.voipConfiguration] as? VoipConfiguration ?? .defaultValue + return (voiceCallSettings, voipConfiguration) + } + } else { + callsSignal = .single(nil) + } + + let _ = (combineLatest(privacySignal, callsSignal) + |> deliverOnMainQueue).start(next: { info, callSettings in + let current: SelectivePrivacySettings + switch kind { + case .presence: + current = info.presence + case .groupInvitations: + current = info.groupInvitations + case .voiceCalls: + current = info.voiceCalls + case .profilePhoto: + current = info.profilePhoto + case .forwards: + current = info.forwards + case .phoneNumber: + current = info.phoneNumber + } + + present(.push, SelectivePrivacySettingsController(context, kind: kind, current: current, callSettings: kind == .voiceCalls ? info.voiceCallsP2P : nil, phoneDiscoveryEnabled: nil, updated: { updated, updatedCallSettings, _ in })) + }) + } + + + + let passcodeTitle: String = L10n.privacySettingsPasscode + + + return [ + SettingsSearchableItem(id: .privacy(0), title: L10n.accountSettingsPrivacyAndSecurity, alternate: synonyms(L10n.settingsSearchSynonymsPrivacyTitle), icon: icon, breadcrumbs: [], present: { context, _, present in + presentPrivacySettings(context, present, nil) + }), + SettingsSearchableItem(id: .privacy(1), title: L10n.privacySettingsBlockedUsers, alternate: synonyms(L10n.settingsSearchSynonymsPrivacyBlockedUsers), icon: icon, breadcrumbs: [L10n.accountSettingsPrivacyAndSecurity], present: { context, _, present in + present(.push, BlockedPeersViewController(context)) + }), + SettingsSearchableItem(id: .privacy(2), title: L10n.privacySettingsLastSeen, alternate: synonyms(L10n.settingsSearchSynonymsPrivacyLastSeen), icon: icon, breadcrumbs: [L10n.accountSettingsPrivacyAndSecurity], present: { context, _, present in + presentSelectivePrivacySettings(context, .presence, present) + }), + SettingsSearchableItem(id: .privacy(3), title: L10n.privacySettingsProfilePhoto, alternate: synonyms(L10n.settingsSearchSynonymsPrivacyProfilePhoto), icon: icon, breadcrumbs: [L10n.accountSettingsPrivacyAndSecurity], present: { context, _, present in + presentSelectivePrivacySettings(context, .profilePhoto, present) + }), + SettingsSearchableItem(id: .privacy(4), title: L10n.privacySettingsForwards, alternate: synonyms(L10n.settingsSearchSynonymsPrivacyForwards), icon: icon, breadcrumbs: [L10n.accountSettingsPrivacyAndSecurity], present: { context, _, present in + presentSelectivePrivacySettings(context, .forwards, present) + }), + SettingsSearchableItem(id: .privacy(5), title: L10n.privacySettingsVoiceCalls, alternate: synonyms(L10n.settingsSearchSynonymsPrivacyCalls), icon: icon, breadcrumbs: [L10n.accountSettingsPrivacyAndSecurity], present: { context, _, present in + presentSelectivePrivacySettings(context, .voiceCalls, present) + }), + SettingsSearchableItem(id: .privacy(6), title: L10n.privacySettingsGroups, alternate: synonyms(L10n.settingsSearchSynonymsPrivacyGroupsAndChannels), icon: icon, breadcrumbs: [L10n.accountSettingsPrivacyAndSecurity], present: { context, _, present in + presentSelectivePrivacySettings(context, .groupInvitations, present) + }), + SettingsSearchableItem(id: .privacy(7), title: passcodeTitle, alternate: [], icon: icon, breadcrumbs: [L10n.accountSettingsPrivacyAndSecurity], present: { context, _, present in + present(.push, PasscodeSettingsViewController(context)) + }), + SettingsSearchableItem(id: .privacy(8), title: L10n.privacySettingsTwoStepVerification, alternate: synonyms(L10n.settingsSearchSynonymsPrivacyTwoStepAuth), icon: icon, breadcrumbs: [L10n.accountSettingsPrivacyAndSecurity], present: { context, navigation, present in + present(.push, twoStepVerificationUnlockController(context: context, mode: .access(nil), presentController: { controller, root, animated in + guard let navigation = navigation else {return} + if root { + navigation.removeUntil(PrivacyAndSecurityViewController.self) + } + if !animated { + navigation.stackInsert(controller, at: navigation.stackCount) + } else { + navigation.push(controller) + } + })) + }), + SettingsSearchableItem(id: .privacy(9), title: L10n.privacySettingsActiveSessions, alternate: synonyms(L10n.settingsSearchSynonymsPrivacyAuthSessions), icon: icon, breadcrumbs: [L10n.accountSettingsPrivacyAndSecurity], present: { context, _, present in + present(.push, RecentSessionsController(context)) + }), + SettingsSearchableItem(id: .privacy(10), title: L10n.privacySettingsDeleteAccountHeader, alternate: synonyms(L10n.settingsSearchSynonymsPrivacyDeleteAccountIfAwayFor), icon: icon, breadcrumbs: [L10n.accountSettingsPrivacyAndSecurity], present: { context, _, present in + presentPrivacySettings(context, present, .accountTimeout) + }), + SettingsSearchableItem(id: .privacy(14), title: L10n.suggestFrequentContacts, alternate: synonyms(L10n.settingsSearchSynonymsPrivacyDataTopPeers), icon: icon, breadcrumbs: [L10n.accountSettingsPrivacyAndSecurity], present: { context, _, present in + presentPrivacySettings(context, present, .topPeers) + }), + SettingsSearchableItem(id: .privacy(15), title: L10n.privacyAndSecurityClearCloudDrafts, alternate: synonyms(L10n.settingsSearchSynonymsPrivacyDataDeleteDrafts), icon: icon, breadcrumbs: [L10n.accountSettingsPrivacyAndSecurity], present: { context, _, present in + presentPrivacySettings(context, present, .cloudDraft) + }) + ] +} + +private func dataSearchableItems(context: AccountContext) -> [SettingsSearchableItem] { + let icon: SettingsSearchableItemIcon = .data + + let presentDataSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void, DataAndStorageEntryTag?) -> Void = { context, present, itemTag in + present(.push, DataAndStorageViewController(context, focusOnItemTag: itemTag)) + } + + return [ + SettingsSearchableItem(id: .data(0), title: L10n.accountSettingsDataAndStorage, alternate: synonyms(L10n.settingsSearchSynonymsDataTitle), icon: icon, breadcrumbs: [], present: { context, _, present in + presentDataSettings(context, present, nil) + }), + SettingsSearchableItem(id: .data(1), title: L10n.dataAndStorageStorageUsage, alternate: synonyms(L10n.settingsSearchSynonymsDataStorageTitle), icon: icon, breadcrumbs: [L10n.accountSettingsDataAndStorage], present: { context, _, present in + present(.push, StorageUsageController(context)) + }), + SettingsSearchableItem(id: .data(2), title: L10n.storageUsageKeepMedia, alternate: synonyms(L10n.settingsSearchSynonymsDataStorageKeepMedia), icon: icon, breadcrumbs: [L10n.accountSettingsDataAndStorage, L10n.dataAndStorageStorageUsage], present: { context, _, present in + present(.push, StorageUsageController(context)) + }), + SettingsSearchableItem(id: .data(3), title: L10n.logoutOptionsClearCacheTitle, alternate: synonyms(L10n.settingsSearchSynonymsDataStorageClearCache), icon: icon, breadcrumbs: [L10n.accountSettingsDataAndStorage, L10n.dataAndStorageStorageUsage], present: { context, _, present in + present(.push, StorageUsageController(context)) + }), + SettingsSearchableItem(id: .data(4), title: L10n.dataAndStorageNetworkUsage, alternate: synonyms(L10n.settingsSearchSynonymsDataNetworkUsage), icon: icon, breadcrumbs: [L10n.accountSettingsDataAndStorage], present: { context, _, present in + present(.push, networkUsageStatsController(context: context)) + }), + SettingsSearchableItem(id: .data(7), title: L10n.dataAndStorageAutomaticDownloadReset, alternate: synonyms(L10n.settingsSearchSynonymsDataAutoDownloadReset), icon: icon, breadcrumbs: [L10n.accountSettingsDataAndStorage], present: { context, _, present in + presentDataSettings(context, present, .automaticDownloadReset) + }), + SettingsSearchableItem(id: .data(8), title: L10n.dataAndStorageAutoplayGIFs, alternate: synonyms(L10n.settingsSearchSynonymsDataAutoplayGifs), icon: icon, breadcrumbs: [L10n.accountSettingsDataAndStorage, L10n.dataAndStorageAutoplayHeader], present: { context, _, present in + presentDataSettings(context, present, .autoplayGifs) + }), + SettingsSearchableItem(id: .data(9), title: L10n.dataAndStorageAutoplayVideos, alternate: synonyms(L10n.settingsSearchSynonymsDataAutoplayVideos), icon: icon, breadcrumbs: [L10n.accountSettingsDataAndStorage, L10n.dataAndStorageAutoplayHeader], present: { context, _, present in + presentDataSettings(context, present, .autoplayVideos) + }) + ] +} + +private func proxySearchableItems(context: AccountContext, servers: [ProxyServerSettings]) -> [SettingsSearchableItem] { + let icon: SettingsSearchableItemIcon = .proxy + + let presentProxySettings: (AccountContext, @escaping(SettingsSearchableItemPresentation, ViewController?) -> Void) -> Void = { context, present in + let controller = proxyListController(accountManager: context.sharedContext.accountManager, network: context.account.network, share: { servers in + var message: String = "" + for server in servers { + message += server.link + "\n\n" + } + message = message.trimmed + + showModal(with: ShareModalController(ShareLinkObject(context, link: message)), for: mainWindow) + }, pushController: { controller in + present(.push, controller) + }) + present(.push, controller) + } + + var items: [SettingsSearchableItem] = [] + items.append(SettingsSearchableItem(id: .proxy(0), title: L10n.accountSettingsProxy, alternate: synonyms(L10n.settingsSearchSynonymsProxyTitle), icon: icon, breadcrumbs: [], present: { context, _, present in + presentProxySettings(context, present) + })) + items.append(SettingsSearchableItem(id: .proxy(1), title: L10n.proxySettingsAddProxy, alternate: synonyms(L10n.settingsSearchSynonymsProxyAddProxy), icon: icon, breadcrumbs: [L10n.accountSettingsProxy], present: { context, _, present in + presentProxySettings(context, present) + })) + + var hasSocksServers = false + for server in servers { + if case .socks5 = server.connection { + hasSocksServers = true + break + } + } + if hasSocksServers { + items.append(SettingsSearchableItem(id: .proxy(2), title: L10n.proxySettingsUseForCalls, alternate: synonyms(L10n.settingsSearchSynonymsProxyUseForCalls), icon: icon, breadcrumbs: [L10n.accountSettingsProxy], present: { context, _, present in + presentProxySettings(context, present) + })) + } + return items +} + +private func appearanceSearchableItems(context: AccountContext) -> [SettingsSearchableItem] { + let icon: SettingsSearchableItemIcon = .appearance + + let presentAppearanceSettings: (AccountContext, (SettingsSearchableItemPresentation, ViewController?) -> Void, ThemeSettingsEntryTag?) -> Void = { context, present, itemTag in + present(.push, AppAppearanceViewController(context: context, focusOnItemTag: itemTag)) + } + + return [ + SettingsSearchableItem(id: .appearance(0), title: L10n.accountSettingsTheme, alternate: synonyms(L10n.settingsSearchSynonymsAppearanceTitle), icon: icon, breadcrumbs: [], present: { context, _, present in + presentAppearanceSettings(context, present, nil) + }), + SettingsSearchableItem(id: .appearance(1), title: L10n.appearanceSettingsTextSizeHeader, alternate: synonyms(L10n.settingsSearchSynonymsAppearanceTextSize), icon: icon, breadcrumbs: [L10n.accountSettingsTheme], present: { context, _, present in + presentAppearanceSettings(context, present, .fontSize) + }), + SettingsSearchableItem(id: .appearance(2), title: L10n.generalSettingsChatBackground, alternate: synonyms(L10n.settingsSearchSynonymsAppearanceChatBackground), icon: icon, breadcrumbs: [L10n.accountSettingsTheme], present: { context, _, present in + showModal(with: ChatWallpaperModalController(context), for: context.window) + }), + SettingsSearchableItem(id: .appearance(5), title: L10n.appearanceSettingsAutoNight, alternate: synonyms(L10n.settingsSearchSynonymsAppearanceAutoNightTheme), icon: icon, breadcrumbs: [L10n.accountSettingsTheme], present: { context, _, present in + present(.push, AutoNightSettingsController(context: context)) + }), + SettingsSearchableItem(id: .appearance(6), title: L10n.appearanceSettingsColorThemeHeader, alternate: synonyms(L10n.settingsSearchSynonymsAppearanceColorTheme), icon: icon, breadcrumbs: [L10n.accountSettingsTheme], present: { context, _, present in + presentAppearanceSettings(context, present, .accentColor) + }), + SettingsSearchableItem(id: .appearance(6), title: L10n.appearanceSettingsChatViewHeader, alternate: synonyms(L10n.settingsSearchSynonymsAppearanceChatMode), icon: icon, breadcrumbs: [L10n.accountSettingsTheme], present: { context, _, present in + presentAppearanceSettings(context, present, .chatMode) + }), + ] +} + +private func languageSearchableItems(context: AccountContext, localizations: [LocalizationInfo]) -> [SettingsSearchableItem] { + let icon: SettingsSearchableItemIcon = .language + + let applyLocalization: (AccountContext, @escaping (SettingsSearchableItemPresentation, ViewController?) -> Void, String) -> Void = { context, present, languageCode in + _ = showModalProgress(signal: context.engine.localization.downloadAndApplyLocalization(accountManager: context.sharedContext.accountManager, languageCode: languageCode), for: context.window).start() + } + + var items: [SettingsSearchableItem] = [] + items.append(SettingsSearchableItem(id: .language(0), title: L10n.accountSettingsLanguage, alternate: synonyms(L10n.settingsSearchSynonymsAppLanguage), icon: icon, breadcrumbs: [], present: { context, _, present in + present(.push, LanguageViewController(context)) + })) + var index: Int32 = 1 + for localization in localizations { + items.append(SettingsSearchableItem(id: .language(index), title: localization.localizedTitle, alternate: [localization.title], icon: icon, breadcrumbs: [L10n.accountSettingsLanguage], present: { context, _, present in + applyLocalization(context, present, localization.languageCode) + })) + index += 1 + } + return items +} + +func settingsSearchableItems(context: AccountContext, archivedStickerPacks: Signal<[ArchivedStickerPackItem]?, NoError>, privacySettings: Signal) -> Signal<[SettingsSearchableItem], NoError> { + + let canAddAccount = activeAccountsAndPeers(context: context) + |> take(1) + |> map { accountsAndPeers -> Bool in + return accountsAndPeers.1.count + 1 < maximumNumberOfAccounts + } + + let notificationSettings = context.account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications]) + |> take(1) + |> map { view -> GlobalNotificationSettingsSet in + let viewSettings: GlobalNotificationSettingsSet + if let settings = view.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings { + viewSettings = settings.effective + } else { + viewSettings = GlobalNotificationSettingsSet.defaultSettings + } + return viewSettings + } + + let archivedStickerPacks = archivedStickerPacks + |> take(1) + + let privacySettings = privacySettings + |> take(1) + + let proxyServers = context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.proxySettings]) + |> map { sharedData -> ProxySettings in + if let value = sharedData.entries[SharedDataKeys.proxySettings] as? ProxySettings { + return value + } else { + return ProxySettings.defaultSettings + } + } + |> map { settings -> [ProxyServerSettings] in + return settings.servers + } + + let localizationPreferencesKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.localizationListState])) + let localizations = combineLatest(context.account.postbox.combinedView(keys: [localizationPreferencesKey]), context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.localizationSettings])) + |> map { view, sharedData -> [LocalizationInfo] in + if let localizationListState = (view.views[localizationPreferencesKey] as? PreferencesView)?.values[PreferencesKeys.localizationListState] as? LocalizationListState, !localizationListState.availableOfficialLocalizations.isEmpty { + + var existingIds = Set() + let availableSavedLocalizations = localizationListState.availableSavedLocalizations.filter({ info in !localizationListState.availableOfficialLocalizations.contains(where: { $0.languageCode == info.languageCode }) }) + + var activeLanguageCode: String? + if let localizationSettings = sharedData.entries[SharedDataKeys.localizationSettings] as? LocalizationSettings { + activeLanguageCode = localizationSettings.primaryComponent.languageCode + } + + var localizationItems: [LocalizationInfo] = [] + if !availableSavedLocalizations.isEmpty { + for info in availableSavedLocalizations { + if existingIds.contains(info.languageCode) || info.languageCode == activeLanguageCode { + continue + } + existingIds.insert(info.languageCode) + localizationItems.append(info) + } + } + for info in localizationListState.availableOfficialLocalizations { + if existingIds.contains(info.languageCode) || info.languageCode == activeLanguageCode { + continue + } + existingIds.insert(info.languageCode) + localizationItems.append(info) + } + + return localizationItems + } else { + return [] + } + } + + return combineLatest(canAddAccount, localizations, notificationSettings, archivedStickerPacks, proxyServers, privacySettings) + |> map { canAddAccount, localizations, notificationSettings, archivedStickerPacks, proxyServers, privacySettings in + + var allItems: [SettingsSearchableItem] = [] + + let profileItems = profileSearchableItems(context: context, canAddAccount: canAddAccount) + allItems.append(contentsOf: profileItems) + + + let stickerItems = stickerSearchableItems(context: context, archivedStickerPacks: archivedStickerPacks) + allItems.append(contentsOf: stickerItems) + + let notificationItems = notificationSearchableItems(context: context, settings: notificationSettings) + allItems.append(contentsOf: notificationItems) + + let privacyItems = privacySearchableItems(context: context, privacySettings: privacySettings) + allItems.append(contentsOf: privacyItems) + + let dataItems = dataSearchableItems(context: context) + allItems.append(contentsOf: dataItems) + + let proxyItems = proxySearchableItems(context: context, servers: proxyServers) + allItems.append(contentsOf: proxyItems) + + let appearanceItems = appearanceSearchableItems(context: context) + allItems.append(contentsOf: appearanceItems) + + let languageItems = languageSearchableItems(context: context, localizations: localizations) + allItems.append(contentsOf: languageItems) + + + let support = SettingsSearchableItem(id: .support(0), title: L10n.accountSettingsAskQuestion, alternate: synonyms(L10n.settingsSearchSynonymsSupport), icon: .support, breadcrumbs: [], present: { context, _, present in + confirm(for: context.window, information: L10n.accountConfirmAskQuestion, thridTitle: L10n.accountConfirmGoToFaq, successHandler: { result in + switch result { + case .basic: + _ = showModalProgress(signal: context.engine.peers.supportPeerId(), for: context.window).start(next: { peerId in + if let peerId = peerId { + present(.push, ChatController(context: context, chatLocation: .peer(peerId))) + } + }) + case .thrid: + let _ = (cachedFaqInstantPage(context: context) |> deliverOnMainQueue).start(next: { resolvedUrl in + execute(inapp: resolvedUrl) + }) + } + }) + }) + allItems.append(support) + + let faq = SettingsSearchableItem(id: .faq(0), title: L10n.accountSettingsFAQ, alternate: synonyms(L10n.settingsSearchSynonymsFAQ), icon: .faq, breadcrumbs: [], present: { context, navigationController, present in + let _ = (cachedFaqInstantPage(context: context) |> deliverOnMainQueue).start(next: { resolvedUrl in + execute(inapp: resolvedUrl) + }) + }) + allItems.append(faq) + + return allItems + } +} + + + + diff --git a/Telegram-Mac/ShareInlineResultNavigationAction.swift b/Telegram-Mac/ShareInlineResultNavigationAction.swift index 9072dc48b2..97a0713c5d 100644 --- a/Telegram-Mac/ShareInlineResultNavigationAction.swift +++ b/Telegram-Mac/ShareInlineResultNavigationAction.swift @@ -8,20 +8,21 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac +import Postbox +import TelegramCore + +import SwiftSignalKit class ShareInlineResultNavigationAction: NavigationModalAction { let payload:String init(payload:String, botName:String) { self.payload = payload - super.init(reason: tr(.inlineModalActionTitle), desc: tr(.inlineModalActionDesc(botName))) + super.init(reason: tr(L10n.inlineModalActionTitle), desc: tr(L10n.inlineModalActionDesc(botName))) } override func isInvokable(for value:Any) -> Bool { - if let value = value as? Peer, value.canSendMessage { + if let value = value as? Peer, value.canSendMessage(false) { return true } return false @@ -29,7 +30,7 @@ class ShareInlineResultNavigationAction: NavigationModalAction { override func alertError(for value:Any, with window:Window) -> Void { if let _ = value as? Peer { - alert(for: window, header: appName, info: tr(.alertForwardError)) + alert(for: window, info: tr(L10n.alertForwardError)) } } } diff --git a/Telegram-Mac/ShareModalController.swift b/Telegram-Mac/ShareModalController.swift index 257f2c9a01..3a3c04a240 100644 --- a/Telegram-Mac/ShareModalController.swift +++ b/Telegram-Mac/ShareModalController.swift @@ -8,9 +8,10 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac -import TelegramCoreMac -import PostboxMac +import SwiftSignalKit +import TelegramCore + +import Postbox @@ -22,15 +23,15 @@ fileprivate class ShareButton : Control { super.init(frame: frameRect) addSubview(badgeView) addSubview(shareText) - let layout = TextViewLayout(.initialize(string: tr(.modalShare).uppercased(), color: .white, font: .normal(.header)), maximumNumberOfLines: 1) + let layout = TextViewLayout(.initialize(string: tr(L10n.modalShare).uppercased(), color: .white, font: .normal(.header)), maximumNumberOfLines: 1) layout.measure(width: .greatestFiniteMagnitude) shareText.update(layout) setFrameSize(NSMakeSize(22 + shareText.frame.width + 47, 41)) layer?.cornerRadius = 20 - set(background: theme.colors.blueFill, for: .Hover) - set(background: theme.colors.blueFill, for: .Normal) - set(background: theme.colors.blueFill, for: .Highlight) - shareText.backgroundColor = theme.colors.blueFill + set(background: theme.colors.accent, for: .Hover) + set(background: theme.colors.accent, for: .Normal) + set(background: theme.colors.accent, for: .Highlight) + shareText.backgroundColor = theme.colors.accent needsLayout = true updateCount(0) shareText.userInteractionEnabled = false @@ -47,7 +48,7 @@ fileprivate class ShareButton : Control { } func updateCount(_ count:Int) -> Void { - badge = BadgeNode(.initialize(string: "\(max(count, 1))", color: theme.colors.blueFill, font: .medium(.small)), .white) + badge = BadgeNode(.initialize(string: "\(max(count, 1))", color: theme.colors.accent, font: .medium(.small)), .white) badgeView.setFrameSize(badge!.size) badge?.view = badgeView badge?.setNeedDisplay() @@ -60,14 +61,25 @@ fileprivate class ShareButton : Control { } fileprivate class ShareModalView : View, TokenizedProtocol { - let searchView:TokenizedView + let tokenizedView:TokenizedView + let basicSearchView: SearchView = SearchView(frame: NSMakeRect(0,0, 260, 30)) let tableView:TableView = TableView() fileprivate let share:ImageButton = ImageButton() fileprivate let dismiss:ImageButton = ImageButton() - private let separator = View() - fileprivate let invokeButton = ShareButton(frame: NSZeroRect) - private let shadowView: View = ShadowView() + deinit { + var bp:Int = 0 + bp += 1 + } + + fileprivate let textView:TGModernGrowingTextView = TGModernGrowingTextView(frame: NSZeroRect) + fileprivate let sendButton = ImageButton() + fileprivate let emojiButton = ImageButton() + fileprivate let actionsContainerView: View = View() + fileprivate let textContainerView: View = View() + fileprivate let bottomSeparator: View = View() + + private let topSeparator = View() fileprivate var hasShareMenu: Bool = true { didSet { share.isHidden = !hasShareMenu @@ -75,65 +87,164 @@ fileprivate class ShareModalView : View, TokenizedProtocol { } } - required init(frame frameRect: NSRect) { - searchView = TokenizedView(frame: NSMakeRect(0, 0, 260, 30), localizationFunc: { key in + + required init(frame frameRect: NSRect, shareObject: ShareObject) { + tokenizedView = TokenizedView(frame: NSMakeRect(0, 0, 300, 30), localizationFunc: { key in return translate(key: key, []) - }, placeholderKey: "ShareModal.Search.Placeholder") + }, placeholderKey: shareObject.searchPlaceholderKey) super.init(frame: frameRect) - addSubview(searchView) + + backgroundColor = theme.colors.background + textContainerView.backgroundColor = theme.colors.background + actionsContainerView.backgroundColor = theme.colors.background + textView.setBackgroundColor(theme.colors.background) + + addSubview(tokenizedView) + addSubview(basicSearchView) addSubview(tableView) - addSubview(separator) - searchView.delegate = self - separator.backgroundColor = theme.colors.border + addSubview(topSeparator) + tokenizedView.delegate = self + bottomSeparator.backgroundColor = theme.colors.border + topSeparator.backgroundColor = theme.colors.border + self.backgroundColor = theme.colors.background share.set(image: theme.icons.modalShare, for: .Normal) dismiss.set(image: theme.icons.modalClose, for: .Normal) - share.sizeToFit() - dismiss.sizeToFit() + _ = share.sizeToFit() + _ = dismiss.sizeToFit() addSubview(share) addSubview(dismiss) - shadowView.backgroundColor = theme.colors.background.withAlphaComponent(1.0) - shadowView.setFrameSize(frame.width, 70) - addSubview(shadowView) - addSubview(invokeButton) + + + sendButton.set(image: theme.icons.chatSendMessage, for: .Normal) + sendButton.autohighlight = false + _ = sendButton.sizeToFit() + + emojiButton.set(image: theme.icons.chatEntertainment, for: .Normal) + _ = emojiButton.sizeToFit() + + actionsContainerView.addSubview(sendButton) + actionsContainerView.addSubview(emojiButton) + + + actionsContainerView.setFrameSize(sendButton.frame.width + emojiButton.frame.width + 40, 50) + + emojiButton.centerY(x: 0) + sendButton.centerY(x: emojiButton.frame.maxX + 20) + + backgroundColor = theme.colors.background + textView.background = theme.colors.background + textView.textFont = .normal(.text) + textView.textColor = theme.colors.text + textView.linkColor = theme.colors.link + textView.max_height = 120 + + textView.setFrameSize(NSMakeSize(0, 34)) + textView.setPlaceholderAttributedString(.initialize(string: tr(L10n.previewSenderCommentPlaceholder), color: theme.colors.grayText, font: .normal(.text)), update: false) + + + textContainerView.addSubview(textView) + + addSubview(textContainerView) + addSubview(actionsContainerView) + addSubview(bottomSeparator) } - private var count:Int = 0 - func updateCount(_ count:Int, animated: Bool) -> Void { - self.count = count - invokeButton.updateCount(count) - if count == 0 { - invokeButton.change(pos: NSMakePoint(invokeButton.frame.minX, frame.height), animated: animated, timingFunction: kCAMediaTimingFunctionSpring) - shadowView.change(pos: NSMakePoint(shadowView.frame.minX, frame.height), animated: animated, timingFunction: kCAMediaTimingFunctionSpring) + var searchView: NSView { + if hasCaptionView { + return tokenizedView } else { - invokeButton.change(pos: NSMakePoint(invokeButton.frame.minX, frame.height - invokeButton.frame.height - 16), animated: animated, timingFunction: kCAMediaTimingFunctionSpring) - shadowView.change(pos: NSMakePoint(shadowView.frame.minX, frame.height - shadowView.frame.height), animated: animated, timingFunction: kCAMediaTimingFunctionSpring) + return basicSearchView + } + } + + var hasCaptionView: Bool = true { + didSet { + textContainerView.isHidden = !hasCaptionView + actionsContainerView.isHidden = !hasCaptionView + bottomSeparator.isHidden = !hasCaptionView + + basicSearchView.isHidden = hasCaptionView + tokenizedView.isHidden = !hasCaptionView + dismiss.isHidden = !hasCaptionView + needsLayout = true + } + } + + var hasCommentView: Bool = true { + didSet { + textContainerView.isHidden = !hasCommentView + bottomSeparator.isHidden = !hasCommentView + actionsContainerView.isHidden = !hasCommentView + needsLayout = true + } + } + + var hasSendView: Bool = true { + didSet { + sendButton.isHidden = !hasSendView + needsLayout = true } } + + func tokenizedViewDidChangedHeight(_ view: TokenizedView, height: CGFloat, animated: Bool) { searchView._change(pos: NSMakePoint(50, 10), animated: animated) - tableView.change(size: NSMakeSize(frame.width, frame.height - height - 20), animated: animated) + tableView.change(size: NSMakeSize(frame.width, frame.height - height - 20 - (textContainerView.isHidden ? 0 : textContainerView.frame.height)), animated: animated) tableView.change(pos: NSMakePoint(0, height + 20), animated: animated) - separator.change(pos: NSMakePoint(0, searchView.frame.maxY + 10), animated: animated) + topSeparator.change(pos: NSMakePoint(0, searchView.frame.maxY + 10), animated: animated) + } + + func textViewUpdateHeight(_ height: CGFloat, _ animated: Bool) { + CATransaction.begin() + textContainerView.change(size: NSMakeSize(frame.width, height + 16), animated: animated) + textContainerView.change(pos: NSMakePoint(0, frame.height - textContainerView.frame.height), animated: animated) + textView._change(pos: NSMakePoint(10, height == 34 ? 8 : 11), animated: animated) + tableView.change(size: NSMakeSize(frame.width, frame.height - searchView.frame.height - 20 - (!textContainerView.isHidden ? 50 : 0)), animated: animated) + + actionsContainerView.change(pos: NSMakePoint(frame.width - actionsContainerView.frame.width, frame.height - actionsContainerView.frame.height), animated: animated) + + bottomSeparator.change(pos: NSMakePoint(0, textContainerView.frame.minY), animated: animated) + CATransaction.commit() + + needsLayout = true + } + + var additionHeight: CGFloat { + return textView.frame.height + 16 + searchView.frame.height + 20 } fileprivate override func layout() { super.layout() - searchView.setFrameSize(frame.width - 50 - (share.isHidden ? 10 : 50), searchView.frame.height) + + emojiButton.centerY(x: 0) + actionsContainerView.setFrameSize((sendButton.isHidden ? 0 : (sendButton.frame.width + 20)) + emojiButton.frame.width + 20, 50) + + sendButton.centerY(x: emojiButton.frame.maxX + 20) + + searchView.setFrameSize(frame.width - 10 - (!dismiss.isHidden ? 40 : 0) - (share.isHidden ? 10 : 50), searchView.frame.height) share.setFrameOrigin(frame.width - share.frame.width - 10, 10) dismiss.setFrameOrigin(10, 10) - searchView.setFrameOrigin(50, 10) - tableView.frame = NSMakeRect(0, searchView.frame.maxY + 10, frame.width, frame.height - searchView.frame.height - 20) - separator.frame = NSMakeRect(0, searchView.frame.maxY + 10, frame.width, .borderSize) - invokeButton.centerX(y: count == 0 ? frame.height : frame.height - invokeButton.frame.height - 16) - shadowView.setFrameOrigin(0, count == 0 ? frame.height : frame.height - shadowView.frame.height) + searchView.setFrameOrigin(10 + (!dismiss.isHidden ? 40 : 0), 10) + tableView.frame = NSMakeRect(0, searchView.frame.maxY + 10, frame.width, frame.height - searchView.frame.height - 20 - (!textContainerView.isHidden ? 50 : 0)) + topSeparator.frame = NSMakeRect(0, searchView.frame.maxY + 10, frame.width, .borderSize) + actionsContainerView.setFrameOrigin(frame.width - actionsContainerView.frame.width, frame.height - actionsContainerView.frame.height) + + textContainerView.setFrameSize(frame.width, textView.frame.height + 16) + textContainerView.setFrameOrigin(0, frame.height - textContainerView.frame.height) + + + textView.setFrameSize(NSMakeSize(textContainerView.frame.width - 10 - actionsContainerView.frame.width, textView.frame.height)) + textView.setFrameOrigin(10, textView.frame.height == 34 ? 8 : 11) + bottomSeparator.frame = NSMakeRect(0, textContainerView.frame.minY, frame.width, .borderSize) + } @@ -141,16 +252,73 @@ fileprivate class ShareModalView : View, TokenizedProtocol { fatalError("init(coder:) has not been implemented") } + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + +} + +final class ShareAdditionItem { + let peer: Peer + let status: String? + init(peer: Peer, status: String?) { + self.peer = peer + self.status = status + } +} + +final class ShareAdditionItems { + let items: [ShareAdditionItem] + let topSeparator: String + let bottomSeparator: String + init(items: [ShareAdditionItem], topSeparator: String, bottomSeparator: String) { + self.items = items + self.topSeparator = topSeparator + self.bottomSeparator = bottomSeparator + } } class ShareObject { - let account:Account - init(_ account:Account) { - self.account = account + + let additionTopItems:ShareAdditionItems? + + let context: AccountContext + let emptyPerformOnClose: Bool + let excludePeerIds: Set + let defaultSelectedIds:Set + let limit: Int? + init(_ context:AccountContext, emptyPerformOnClose: Bool = false, excludePeerIds:Set = [], defaultSelectedIds: Set = [], additionTopItems:ShareAdditionItems? = nil, limit: Int? = nil) { + self.limit = limit + self.context = context + self.emptyPerformOnClose = emptyPerformOnClose + self.excludePeerIds = excludePeerIds + self.additionTopItems = additionTopItems + self.defaultSelectedIds = defaultSelectedIds + } + + var multipleSelection: Bool { + return true + } + var hasCaptionView: Bool { + return true + } + var interactionOk: String { + return L10n.modalOK } - func perform(to entries:[PeerId]) { + var searchPlaceholderKey: String { + return "ShareModal.Search.Placeholder" + } + + var alwaysEnableDone: Bool { + return false + } + + func perform(to entries:[PeerId], comment: ChatTextInputState? = nil) -> Signal { + return .complete() + } + func limitReached() { } @@ -163,15 +331,17 @@ class ShareObject { } func possibilityPerformTo(_ peer:Peer) -> Bool { - return peer.canSendMessage + return peer.canSendMessage(false) && !self.excludePeerIds.contains(peer.id) } + + } class ShareLinkObject : ShareObject { let link:String - init(_ account:Account, link:String) { - self.link = link - super.init(account) + init(_ context: AccountContext, link:String) { + self.link = link.removingPercentEncoding ?? link + super.init(context) } override var hasLink: Bool { @@ -182,58 +352,122 @@ class ShareLinkObject : ShareObject { copyToClipboard(link) } - override func perform(to peerIds:[PeerId]) { + override func perform(to peerIds:[PeerId], comment: ChatTextInputState? = nil) -> Signal { for peerId in peerIds { + var link = self.link + if let comment = comment, !comment.inputText.isEmpty { + link += "\n\(comment.inputText)" + } + var attributes:[MessageAttribute] = [] if FastSettings.isChannelMessagesMuted(peerId) { attributes.append(NotificationInfoMessageAttribute(flags: [.muted])) } - _ = enqueueMessages(account: account, peerId: peerId, messages: [EnqueueMessage.message(text: link, attributes: attributes, media: nil, replyToMessageId: nil)]).start() + _ = enqueueMessages(account: context.account, peerId: peerId, messages: [EnqueueMessage.message(text: link, attributes: attributes, mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)]).start() } + return .complete() + } +} + + +class ShareUrlObject : ShareObject { + let url:String + init(_ context: AccountContext, url:String) { + self.url = url + super.init(context) + } + + override var hasLink: Bool { + return true + } + + override func shareLink() { + copyToClipboard(url) + } + + override func perform(to peerIds:[PeerId], comment: ChatTextInputState? = nil) -> Signal { + for peerId in peerIds { + + var attributes:[MessageAttribute] = [] + if FastSettings.isChannelMessagesMuted(peerId) { + attributes.append(NotificationInfoMessageAttribute(flags: [.muted])) + } + + let media = TelegramMediaFile(fileId: MediaId.init(namespace: 0, id: 0), partialReference: nil, resource: LocalFileReferenceMediaResource.init(localFilePath: url, randomId: arc4random64()), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "text/plain", size: nil, attributes: [.FileName(fileName: url.nsstring.lastPathComponent)]) + + _ = enqueueMessages(account: context.account, peerId: peerId, messages: [EnqueueMessage.message(text: "", attributes: attributes, mediaReference: AnyMediaReference.standalone(media: media), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)]).start() + } + return .complete() } } class ShareContactObject : ShareObject { let user:TelegramUser - init(_ account:Account, user:TelegramUser) { + init(_ context: AccountContext, user:TelegramUser) { self.user = user - super.init(account) + super.init(context) } - override func perform(to peerIds:[PeerId]) { + override func perform(to peerIds:[PeerId], comment: ChatTextInputState? = nil) -> Signal { for peerId in peerIds { - _ = Sender.shareContact(account: account, peerId: peerId, contact: user).start() + if let comment = comment, !comment.inputText.isEmpty { + var attributes:[MessageAttribute] = [] + if FastSettings.isChannelMessagesMuted(peerId) { + attributes.append(NotificationInfoMessageAttribute(flags: [.muted])) + } + _ = enqueueMessages(account: context.account, peerId: peerId, messages: [EnqueueMessage.message(text: comment.inputText, attributes: attributes, mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)]).start() + } + _ = Sender.shareContact(context: context, peerId: peerId, contact: user).start() } + return .complete() } } +class ShareCallbackObject : ShareObject { + private let callback:([PeerId])->Signal + init(_ context: AccountContext, callback:@escaping([PeerId])->Signal) { + self.callback = callback + super.init(context) + } + + override func perform(to peerIds:[PeerId], comment: ChatTextInputState? = nil) -> Signal { + return callback(peerIds) |> mapError { _ in return String() } + } + +} + + + + class ShareMessageObject : ShareObject { fileprivate let messageIds:[MessageId] private let message:Message let link:String? private let exportLinkDisposable = MetaDisposable() - init(_ account:Account, _ message:Message) { - self.messageIds = [message.id] + init(_ context: AccountContext, _ message:Message, _ groupMessages:[Message] = []) { + self.messageIds = groupMessages.isEmpty ? [message.id] : groupMessages.map{$0.id} self.message = message - let peer:TelegramChannel? + var peer = messageMainPeer(message) as? TelegramChannel + var messageId = message.id if let author = message.forwardInfo?.author as? TelegramChannel { peer = author - } else { - peer = messageMainPeer(message) as? TelegramChannel + messageId = message.forwardInfo?.sourceMessageId ?? message.id } + // peer = messageMainPeer(message) as? TelegramChannel + // } if let peer = peer, let address = peer.username { switch peer.info { case .broadcast: - self.link = "https://t.me/" + address + "/" + "\(message.id.id)" + self.link = "https://t.me/" + address + "/" + "\(messageId.id)" default: self.link = nil } } else { self.link = nil } - super.init(account) + super.init(context) } override var hasLink: Bool { @@ -242,7 +476,7 @@ class ShareMessageObject : ShareObject { override func shareLink() { if let link = link { - exportLinkDisposable.set(exportMessageLink(account: account, peerId: messageIds[0].peerId, messageId: messageIds[0]).start(next: { valueLink in + exportLinkDisposable.set(context.engine.messages.exportMessageLink(peerId: messageIds[0].peerId, messageId: messageIds[0]).start(next: { valueLink in if let valueLink = valueLink { copyToClipboard(valueLink) } else { @@ -256,10 +490,22 @@ class ShareMessageObject : ShareObject { exportLinkDisposable.dispose() } - override func perform(to peerIds:[PeerId]) { + override func perform(to peerIds:[PeerId], comment: ChatTextInputState? = nil) -> Signal { for peerId in peerIds { - _ = Sender.forwardMessages(messageIds: messageIds, account: account, peerId: peerId).start() + if let comment = comment, !comment.inputText.isEmpty { + let parsingUrlType: ParsingType + if peerId.namespace != Namespaces.Peer.SecretChat { + parsingUrlType = [.Hashtags] + } else { + parsingUrlType = [.Links, .Hashtags] + } + let attributes:[MessageAttribute] = [TextEntitiesMessageAttribute(entities: comment.messageTextEntities(parsingUrlType))] + _ = Sender.enqueue(message: EnqueueMessage.message(text: comment.inputText, attributes: attributes, mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil), context: context, peerId: peerId).start() + } + + _ = Sender.forwardMessages(messageIds: messageIds, context: context, peerId: peerId).start() } + return .complete() } override func possibilityPerformTo(_ peer:Peer) -> Bool { @@ -267,13 +513,114 @@ class ShareMessageObject : ShareObject { } } +final class ForwardMessagesObject : ShareObject { + fileprivate let messageIds: [MessageId] + private let disposable = MetaDisposable() + init(_ context: AccountContext, messageIds: [MessageId], emptyPerformOnClose: Bool = false) { + self.messageIds = messageIds + super.init(context, emptyPerformOnClose: emptyPerformOnClose) + } + + deinit { + disposable.dispose() + } + + override var multipleSelection: Bool { + return false + } + + override func perform(to peerIds: [PeerId], comment: ChatTextInputState? = nil) -> Signal { + let context = self.context + let comment = comment != nil ? comment!.inputText.isEmpty ? nil : comment : nil + let peers = context.account.postbox.transaction { transaction -> Peer? in + for peerId in peerIds { + if let peer = transaction.getPeer(peerId) { + return peer + } + } + return nil + } + + return combineLatest(context.account.postbox.messagesAtIds(messageIds), peers) + |> deliverOnMainQueue + |> mapError { _ in return String() } + |> mapToSignal { messages, peer in + + let messageIds = messages.map { $0.id } + + if let peer = peer, peer.isChannel { + for message in messages { + if message.isPublicPoll { + return .fail(L10n.pollForwardError) + } + } + } + + let navigation = self.context.sharedContext.bindings.rootNavigation() + if let peerId = peerIds.first { + if peerId == context.peerId { + if let comment = comment, !comment.inputText.isEmpty { + let parsingUrlType: ParsingType + if peerId.namespace != Namespaces.Peer.SecretChat { + parsingUrlType = [.Hashtags] + } else { + parsingUrlType = [.Links, .Hashtags] + } + let attributes:[MessageAttribute] = [TextEntitiesMessageAttribute(entities: comment.messageTextEntities(parsingUrlType))] + _ = Sender.enqueue(message: EnqueueMessage.message(text: comment.inputText, attributes: attributes, mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil), context: context, peerId: peerId).start() + } + _ = Sender.forwardMessages(messageIds: messageIds, context: context, peerId: context.account.peerId).start() + if let controller = context.sharedContext.bindings.rootNavigation().controller as? ChatController { + controller.chatInteraction.update({$0.withoutSelectionState()}) + } + delay(0.2, closure: { + _ = showModalSuccess(for: context.window, icon: theme.icons.successModalProgress, delay: 1.0).start() + }) + } else { + if let controller = navigation.controller as? ChatController, controller.chatInteraction.peerId == peerId { + controller.chatInteraction.update({$0.withoutSelectionState().updatedInterfaceState({$0.withUpdatedForwardMessageIds(messageIds).withUpdatedInputState(comment ?? $0.inputState)})}) + } else { + (navigation.controller as? ChatController)?.chatInteraction.update({ $0.withoutSelectionState() }) + + var existed: Bool = false + navigation.enumerateControllers { controller, _ in + if let controller = controller as? ChatController, controller.chatInteraction.peerId == peerId { + existed = true + } + return existed + } + let newone: ChatController + if existed { + newone = ChatController(context: context, chatLocation: .peer(peerId), initialAction: .forward(messageIds: messageIds, text: comment, behavior: .automatic)) + } else { + newone = ChatAdditionController(context: context, chatLocation: .peer(peerId), initialAction: .forward(messageIds: messageIds, text: comment, behavior: .automatic)) + } + navigation.push(newone) + + return newone.ready.get() |> filter {$0} |> take(1) |> ignoreValues |> mapError { _ in return String() } + } + } + } else { + if let controller = navigation.controller as? ChatController { + controller.chatInteraction.update({$0.withoutSelectionState().updatedInterfaceState({$0.withUpdatedForwardMessageIds(messageIds)})}) + } + } + return .complete() + } + } + + override var searchPlaceholderKey: String { + return "ShareModal.Search.ForwardPlaceholder" + } +} + enum SelectablePeersEntryStableId : Hashable { - case plain(PeerId) + case plain(PeerId, ChatListIndex) case emptySearch case separator(ChatListIndex) var hashValue: Int { switch self { - case let .plain(peerId): + case let .plain(peerId, _): return peerId.hashValue case .separator(let index): return index.hashValue @@ -281,39 +628,19 @@ enum SelectablePeersEntryStableId : Hashable { return 0 } } - - static func ==(lhs:SelectablePeersEntryStableId, rhs:SelectablePeersEntryStableId) -> Bool { - switch lhs { - case let .plain(peerId): - if case .plain(peerId) = rhs { - return true - } else { - return false - } - case let .separator(index): - if case .separator(index) = rhs { - return true - } else { - return false - } - case .emptySearch: - if case .emptySearch = rhs { - return true - } else { - return false - } - } - } } enum SelectablePeersEntry : Comparable, Identifiable { + case secretChat(Peer, PeerId, ChatListIndex, PeerStatusStringResult?, Bool) case plain(Peer, ChatListIndex, PeerStatusStringResult?, Bool) case separator(String, ChatListIndex) case emptySearch var stableId: SelectablePeersEntryStableId { switch self { - case let .plain(peer,_, _, _): - return .plain(peer.id) + case let .plain(peer, index, _, _): + return .plain(peer.id, index) + case let .secretChat(_, peerId, index, _, _): + return .plain(peerId, index) case let .separator(_, index): return .separator(index) case .emptySearch: @@ -325,6 +652,8 @@ enum SelectablePeersEntry : Comparable, Identifiable { switch self { case let .plain(_, id, _, _): return id + case let .secretChat(_, _, id, _, _): + return id case let .separator(_, index): return index case .emptySearch: @@ -345,6 +674,12 @@ func ==(lhs:SelectablePeersEntry, rhs:SelectablePeersEntry) -> Bool { } else { return false } + case let .secretChat(lhsPeer, lhsPeerId, lhsIndex, lhsPresence, lhsSeparator): + if case let .secretChat(rhsPeer, rhsPeerId, rhsIndex, rhsPresence, rhsSeparator) = rhs { + return lhsPeer.isEqual(rhsPeer) && lhsIndex == rhsIndex && lhsPresence == rhsPresence && lhsSeparator == rhsSeparator && lhsPeerId == rhsPeerId + } else { + return false + } case let .separator(text, index): if case .separator(text, index) = rhs { return true @@ -362,14 +697,20 @@ func ==(lhs:SelectablePeersEntry, rhs:SelectablePeersEntry) -> Bool { -fileprivate func prepareEntries(from:[SelectablePeersEntry]?, to:[SelectablePeersEntry], account:Account, initialSize:NSSize, animated:Bool, selectInteraction:SelectPeerInteraction) -> TableUpdateTransition { +fileprivate func prepareEntries(from:[SelectablePeersEntry]?, to:[SelectablePeersEntry], account:Account, initialSize:NSSize, animated:Bool, multipleSelection: Bool, selectInteraction:SelectPeerInteraction) -> TableUpdateTransition { let (deleted,inserted,updated) = proccessEntries(from, right: to, { entry -> TableRowItem in switch entry { case let .plain(peer, _, presence, drawSeparator): - let color = presence?.status.attribute(NSAttributedStringKey.foregroundColor, at: 0, effectiveRange: nil) as? NSColor - return ShortPeerRowItem(initialSize, peer: peer, account:account, stableId: entry.stableId, height: 48, photoSize:NSMakeSize(36, 36), statusStyle: ControlStyle(font: .normal(.text), foregroundColor: color ?? theme.colors.grayText, highlightColor:.white), status: presence?.status.string, drawCustomSeparator: drawSeparator, inset:NSEdgeInsets(left: 10, right: 10), interactionType:.selectable(selectInteraction)) + let color = presence?.status.string.isEmpty == false ? presence?.status.attribute(NSAttributedString.Key.foregroundColor, at: 0, effectiveRange: nil) as? NSColor : nil + return ShortPeerRowItem(initialSize, peer: peer, account:account, stableId: entry.stableId, height: 48, photoSize:NSMakeSize(36, 36), statusStyle: ControlStyle(font: .normal(.text), foregroundColor: peer.id == account.peerId ? theme.colors.grayText : color ?? theme.colors.grayText, highlightColor:.white), status: peer.id == account.peerId ? (multipleSelection ? nil : L10n.forwardToSavedMessages) : presence?.status.string, drawCustomSeparator: drawSeparator, isLookSavedMessage : peer.id == account.peerId, inset:NSEdgeInsets(left: 10, right: 10), drawSeparatorIgnoringInset: true, interactionType: multipleSelection ? .selectable(selectInteraction) : .plain, action: { + selectInteraction.action(peer.id) + }) + case let .secretChat(peer, peerId, _, _, drawSeparator): + return ShortPeerRowItem(initialSize, peer: peer, account :account, peerId: peerId, stableId: entry.stableId, height: 48, photoSize:NSMakeSize(36, 36), titleStyle: ControlStyle(font: .medium(.title), foregroundColor: theme.colors.accent, highlightColor: .white), statusStyle: ControlStyle(font: .normal(.text), foregroundColor: theme.colors.grayText, highlightColor:.white), status: L10n.composeSelectSecretChat.lowercased(), drawCustomSeparator: drawSeparator, isLookSavedMessage : peer.id == account.peerId, inset:NSEdgeInsets(left: 10, right: 10), drawSeparatorIgnoringInset: true, interactionType: multipleSelection ? .selectable(selectInteraction) : .plain, action: { + selectInteraction.action(peerId) + }) case let .separator(text, _): return SeparatorRowItem(initialSize, entry.stableId, string: text) case .emptySearch: @@ -380,37 +721,149 @@ fileprivate func prepareEntries(from:[SelectablePeersEntry]?, to:[SelectablePeer }) - return TableUpdateTransition(deleted: deleted, inserted: inserted, updated: updated, animated: animated, state: animated ? .none(nil) : .saveVisible(.lower), grouping: !animated, animateVisibleOnly: false) + return TableUpdateTransition(deleted: deleted, inserted: inserted, updated: updated, animated: animated, state: animated ? .none(nil) : .saveVisible(.lower), grouping: true, animateVisibleOnly: false) } -class ShareModalController: ModalViewController, Notifable { +class ShareModalController: ModalViewController, Notifable, TGModernGrowingDelegate, TableViewDelegate { + + private let share:ShareObject private let selectInteractions:SelectPeerInteraction = SelectPeerInteraction() - private let search:Promise = Promise() + private let search:Promise = Promise() private let inSearchSelected:Atomic<[PeerId]> = Atomic(value:[]) private let disposable:MetaDisposable = MetaDisposable() private let exportLinkDisposable:MetaDisposable = MetaDisposable() private let tokenDisposable: MetaDisposable = MetaDisposable() + private var contextQueryState: (ChatPresentationInputQuery?, Disposable)? + private let inputContextHelper: InputContextHelper + private let contextChatInteraction: ChatInteraction + + func notify(with value: Any, oldValue: Any, animated: Bool) { if let value = value as? SelectPeerPresentation, let oldValue = oldValue as? SelectPeerPresentation { let added = value.selected.subtracting(oldValue.selected) let removed = oldValue.selected.subtracting(value.selected) - for item in added { - genericView.searchView.addToken(token: SearchToken(name: value.peers[item]?.compactDisplayTitle ?? tr(.peerDeletedUser), uniqueId: item.toInt64()), animated: animated) + + let selected = value.selected.filter { + $0.namespace._internalGetInt32Value() != ChatListFilterPeerCategories.Namespace } - for item in removed { - genericView.searchView.removeToken(uniqueId: item.toInt64(), animated: animated) + if let limit = self.share.limit, selected.count > limit { + DispatchQueue.main.async { [unowned self] in + self.selectInteractions.update(animated: true, { current in + var current = current + for peerId in added { + if let peer = current.peers[peerId] { + current = current.withToggledSelected(peerId, peer: peer) + } + } + return current + }) + self.share.limitReached() + } + return + } + + let tokens:[SearchToken] = added.map { item in + let title = item == share.context.account.peerId ? L10n.peerSavedMessages : value.peers[item]?.compactDisplayTitle ?? L10n.peerDeletedUser + return SearchToken(name: title, uniqueId: item.toInt64()) } - genericView.updateCount(value.selected.count, animated: animated) + genericView.tokenizedView.addTokens(tokens: tokens, animated: animated) + let idsToRemove:[Int64] = removed.map { + $0.toInt64() + } + genericView.tokenizedView.removeTokens(uniqueIds: idsToRemove, animated: animated) + self.modal?.interactions?.updateEnables(!value.selected.isEmpty || share.alwaysEnableDone) + + if value.inputQueryResult != oldValue.inputQueryResult { + inputContextHelper.context(with: value.inputQueryResult, for: self.genericView, relativeView: self.genericView.textContainerView, position: .above, selectIndex: nil, animated: animated) + } + if let (possibleQueryRange, possibleTypes, _) = textInputStateContextQueryRangeAndType(ChatTextInputState(inputText: value.comment.string, selectionRange: value.comment.range.min ..< value.comment.range.max, attributes: []), includeContext: false) { + if possibleTypes.contains(.mention) { + let peers: [Peer] = value.peers.compactMap { (_, value) in + if value.isGroup || value.isSupergroup { + return value + } else { + return nil + } + } + let query = String(value.comment.string[possibleQueryRange]) + if let (updatedContextQueryState, updatedContextQuerySignal) = chatContextQueryForSearchMention(chatLocations: peers.map { .peer($0.id) }, .mention(query: query, includeRecent: false), currentQuery: self.contextQueryState?.0, context: share.context) { + self.contextQueryState?.1.dispose() + var inScope = true + var inScopeResult: ((ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?)? + self.contextQueryState = (updatedContextQueryState, (updatedContextQuerySignal |> deliverOnMainQueue).start(next: { [weak self] result in + if let strongSelf = self { + if Thread.isMainThread && inScope { + inScope = false + inScopeResult = result + } else { + strongSelf.selectInteractions.update(animated: animated, { + $0.updatedInputQueryResult { previousResult in + return result(previousResult) + } + }) + + } + } + })) + inScope = false + if let inScopeResult = inScopeResult { + selectInteractions.update(animated: animated, { + $0.updatedInputQueryResult { previousResult in + return inScopeResult(previousResult) + } + }) + } + } else { + selectInteractions.update(animated: animated, { + $0.updatedInputQueryResult { _ in + return nil + } + }) + } + } else { + selectInteractions.update(animated: animated, { + $0.updatedInputQueryResult { _ in + return nil + } + }) + } + } else { + selectInteractions.update(animated: animated, { + $0.updatedInputQueryResult { _ in + return nil + } + }) + } + } else if let value = value as? ChatPresentationInterfaceState, let oldValue = oldValue as? ChatPresentationInterfaceState { + if value.effectiveInput != oldValue.effectiveInput { + updateInput(value, prevState: oldValue, animated) + } + } + } + + private func updateInput(_ state:ChatPresentationInterfaceState, prevState: ChatPresentationInterfaceState, _ animated:Bool = true) -> Void { + + let textView = genericView.textView + + if textView.string() != state.effectiveInput.inputText || state.effectiveInput.attributes != prevState.effectiveInput.attributes { + textView.animates = false + textView.setAttributedString(state.effectiveInput.attributedString, animated:animated) + textView.animates = true } + let range = NSMakeRange(state.effectiveInput.selectionRange.lowerBound, state.effectiveInput.selectionRange.upperBound - state.effectiveInput.selectionRange.lowerBound) + if textView.selectedRange().location != range.location || textView.selectedRange().length != range.length { + textView.setSelectedRange(range) + } + textViewTextDidChangeSelectedRange(range) } func isEqual(to other: Notifable) -> Bool { @@ -428,47 +881,246 @@ class ShareModalController: ModalViewController, Notifable { return ShareModalView.self } + override func initializer() -> NSView { + let vz = viewClass() as! ShareModalView.Type + return vz.init(frame: NSMakeRect(_frameRect.minX, _frameRect.minY, _frameRect.width, _frameRect.height - bar.height), shareObject: share); + } + + override var modal: Modal? { didSet { modal?.interactions?.updateEnables(false) } } + func selectionDidChange(row: Int, item: TableRowItem, byClick: Bool, isNew: Bool) { + + } + + func selectionWillChange(row: Int, item: TableRowItem, byClick: Bool) -> Bool { + return !self.share.multipleSelection && !(item is SeparatorRowItem) + } + + func isSelectable(row: Int, item: TableRowItem) -> Bool { + return !self.share.multipleSelection + } + + func findGroupStableId(for stableId: AnyHashable) -> AnyHashable? { + return nil + } + + private func invokeShortCut(_ index: Int) { + if genericView.tableView.count > index, let item = self.genericView.tableView.item(at: index) as? ShortPeerRowItem { + _ = self.genericView.tableView.select(item: item) + (item.view as? ShortPeerRowView)?.invokeAction(item, clickCount: 1) + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + let context = self.share.context + + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.genericView.tableView.highlightPrev() + return .invoked + }, with: self, for: .UpArrow, priority: .modal) + + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.genericView.tableView.highlightNext() + return .invoked + }, with: self, for: .DownArrow, priority: .modal) + + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in + guard let `self` = self else {return .rejected} + if let highlighted = self.genericView.tableView.highlightedItem() as? ShortPeerRowItem { + _ = self.genericView.tableView.select(item: highlighted) + (highlighted.view as? ShortPeerRowView)?.invokeAction(highlighted, clickCount: 1) + } + + return .rejected + }, with: self, for: .Return, priority: .low) + + + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.selectInteractions.action(context.peerId) + return .invoked + }, with: self, for: .Zero, priority: self.responderPriority, modifierFlags: [.command]) + + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.invokeShortCut(0) + return .invoked + }, with: self, for: .One, priority: self.responderPriority, modifierFlags: [.command]) + + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.invokeShortCut(1) + return .invoked + }, with: self, for: .Two, priority: self.responderPriority, modifierFlags: [.command]) + + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.invokeShortCut(2) + return .invoked + }, with: self, for: .Three, priority: self.responderPriority, modifierFlags: [.command]) + + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.invokeShortCut(3) + return .invoked + }, with: self, for: .Four, priority: self.responderPriority, modifierFlags: [.command]) + + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.invokeShortCut(4) + return .invoked + }, with: self, for: .Five, priority: self.responderPriority, modifierFlags: [.command]) + + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.invokeShortCut(5) + return .invoked + }, with: self, for: .Six, priority: self.responderPriority, modifierFlags: [.command]) + + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.invokeShortCut(6) + return .invoked + }, with: self, for: .Seven, priority: self.responderPriority, modifierFlags: [.command]) + + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.invokeShortCut(7) + return .invoked + }, with: self, for: .Eight, priority: self.responderPriority, modifierFlags: [.command]) + + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.invokeShortCut(8) + return .invoked + }, with: self, for: .Nine, priority: self.responderPriority, modifierFlags: [.command]) + + + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.genericView.textView.boldWord() + return .invoked + }, with: self, for: .B, priority: self.responderPriority, modifierFlags: [.command]) + + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.makeUrl() + return .invoked + }, with: self, for: .U, priority: self.responderPriority, modifierFlags: [.command]) + + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.genericView.textView.italicWord() + return .invoked + }, with: self, for: .I, priority: self.responderPriority, modifierFlags: [.command]) + + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in + self?.genericView.textView.codeWord() + return .invoked + }, with: self, for: .K, priority: self.responderPriority, modifierFlags: [.command, .shift]) + } + + + private func makeUrl() { + let range = self.genericView.textView.selectedRange() + guard range.min != range.max, let window = window else { + return + } + var effectiveRange:NSRange = NSMakeRange(NSNotFound, 0) + let defaultTag: TGInputTextTag? = genericView.textView.attributedString().attribute(NSAttributedString.Key(rawValue: TGCustomLinkAttributeName), at: range.location, effectiveRange: &effectiveRange) as? TGInputTextTag + let defaultUrl = defaultTag?.attachment as? String + if effectiveRange.location == NSNotFound || defaultTag == nil { + effectiveRange = range + } + showModal(with: InputURLFormatterModalController(string: genericView.textView.string().nsstring.substring(with: effectiveRange), defaultUrl: defaultUrl, completion: { [weak self] url in + self?.genericView.textView.addLink(url, range: effectiveRange) + }), for: window) + + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.window?.removeAllHandlers(for: self) + self.contextChatInteraction.remove(observer: self) + } + + override var handleAllEvents: Bool { + return true + } + + override var responderPriority: HandlerPriority { + return .modal + } + override func viewDidLoad() { super.viewDidLoad() + + self.contextChatInteraction.add(observer: self) + + + genericView.tableView.delegate = self + + let interactions = EntertainmentInteractions(.emoji, peerId: PeerId(0)) + interactions.sendEmoji = { [weak self] emoji in + self?.genericView.textView.appendText(emoji) + _ = self?.window?.makeFirstResponder(self?.genericView.textView.inputView) + } + emoji.update(with: interactions) + + genericView.emojiButton.set(handler: { [weak self] control in + self?.showEmoji(for: control) + }, for: .Hover) + + + genericView.textView.delegate = self genericView.hasShareMenu = self.share.hasLink - search.set(genericView.searchView.textUpdater) + genericView.hasCaptionView = self.share.multipleSelection + genericView.hasCommentView = self.share.hasCaptionView + genericView.hasSendView = self.share.multipleSelection + if self.share.multipleSelection { + search.set(combineLatest(genericView.tokenizedView.textUpdater, genericView.tokenizedView.stateValue.get()) |> map { SearchState(state: $1, request: $0)}) + } else { + search.set(genericView.basicSearchView.searchValue) + } genericView.dismiss.set(handler: { [weak self] _ in self?.close() }, for: .Click) - let initialSize = self.atomicSize.modify({$0}) let request = Promise() - let account = self.share.account + let context = self.share.context let selectInteraction = self.selectInteractions + + + let share = self.share selectInteraction.add(observer: self) let previous:Atomic<[SelectablePeersEntry]?> = Atomic(value: nil) + selectInteraction.action = { [weak self] peerId in + guard let `self` = self else { return } + _ = share.perform(to: [peerId], comment: self.contextChatInteraction.presentation.interfaceState.inputState).start(error: { error in + alert(for: context.window, info: error) + }, completed: { [weak self] in + self?.close() + }) + } genericView.share.set(handler: { [weak self] control in - showPopover(for: control, with: SPopoverViewController(items: [SPopoverItem(tr(.modalCopyLink), { + showPopover(for: control, with: SPopoverViewController(items: [SPopoverItem(L10n.modalCopyLink, { if share.hasLink { share.shareLink() - self?.show(toaster: ControllerToaster(text: tr(.shareLinkCopied), height:50), for: 2.0, animated: true) + self?.show(toaster: ControllerToaster(text: L10n.shareLinkCopied), for: 2.0, animated: true) } })]), edge: .maxY, inset: NSMakePoint(-100, -40)) }, for: .Click) - genericView.invokeButton.set(handler: { [weak self] _ in - share.perform(to: selectInteraction.presentation.selected.map{$0}) - self?.close() - }, for: .Click) - tokenDisposable.set(genericView.searchView.tokensUpdater.start(next: { tokens in + genericView.sendButton.set(handler: { [weak self] _ in + if let strongSelf = self, !selectInteraction.presentation.selected.isEmpty { + _ = strongSelf.invoke() + } + }, for: .SingleClick) + + + + tokenDisposable.set(genericView.tokenizedView.tokensUpdater.start(next: { tokens in let ids = Set(tokens.map({PeerId($0.uniqueId)})) let unselected = selectInteraction.presentation.selected.symmetricDifference(ids) @@ -478,149 +1130,257 @@ class ShareModalController: ModalViewController, Notifable { })) - let list:Signal = combineLatest(request.get() |> distinctUntilChanged |> deliverOnPrepareQueue, search.get() |> distinctUntilChanged |> deliverOnPrepareQueue, genericView.searchView.stateValue.get() |> deliverOnPrepareQueue) |> mapToSignal { location, search, state -> Signal in + let previousChatList:Atomic = Atomic(value: nil) + + let multipleSelection = self.share.multipleSelection + + + let defaultItems = context.account.postbox.transaction { transaction -> [Peer] in + var peers:[Peer] = [] - if state == .None { - return combineLatest(recentPeers(account: account) |> deliverOnPrepareQueue, recentlySearchedPeers(postbox: account.postbox) |> deliverOnPrepareQueue) |> map { top, recent -> TableUpdateTransition in - - var entries:[SelectablePeersEntry] = [] - - var contains:[PeerId:PeerId] = [:] - - var indexId:Int32 = Int32.max - - let chatListIndex:()-> ChatListIndex = { - let index = MessageIndex(id: MessageId(peerId: PeerId(0), namespace: 1, id: indexId), timestamp: indexId) - indexId -= 1 - return ChatListIndex(pinningIndex: nil, messageIndex: index) + if let addition = share.additionTopItems { + for item in addition.items { + if share.defaultSelectedIds.contains(item.peer.id) { + peers.append(item.peer) } - - if !top.isEmpty { - entries.insert(.separator(tr(.searchSeparatorPopular).uppercased(), chatListIndex()), at: 0) + } + } + + for peerId in share.defaultSelectedIds { + if let peer = transaction.getPeer(peerId) { + peers.append(peer) + } + } + return peers + } + + let list:Signal = combineLatest(request.get() |> distinctUntilChanged |> deliverOnPrepareQueue, search.get() |> distinctUntilChanged |> deliverOnPrepareQueue) |> mapToSignal { location, query -> Signal in + + if query.request.isEmpty { + if !multipleSelection && query.state == .Focus { + return combineLatest(context.account.postbox.loadedPeerWithId(context.peerId), context.engine.peers.recentPeers() |> deliverOnPrepareQueue, context.engine.peers.recentlySearchedPeers() |> deliverOnPrepareQueue) |> map { user, rawTop, recent -> TableUpdateTransition in - var count: Int32 = 0 - for peer in top { - if contains[peer.id] == nil { - if share.possibilityPerformTo(peer) { - entries.insert(.plain(peer, chatListIndex(), nil, count < 4), at: 0) - contains[peer.id] = peer.id - count += 1 - } - } - if count >= 5 { - break - } + var entries:[SelectablePeersEntry] = [] + + let top:[Peer] + switch rawTop { + case let .peers(peers): + top = peers + default: + top = [] } - } - - if !recent.isEmpty { - entries.insert(.separator(tr(.searchSeparatorRecent).uppercased(), chatListIndex()), at: 0) - - for rendered in recent { - if let peer = rendered.chatMainPeer { + + var contains:[PeerId:PeerId] = [:] + + var indexId:Int32 = Int32.max + + let chatListIndex:()-> ChatListIndex = { + let index = MessageIndex(id: MessageId(peerId: PeerId(0), namespace: 1, id: indexId), timestamp: indexId) + indexId -= 1 + return ChatListIndex(pinningIndex: nil, messageIndex: index) + } + + entries.append(.plain(user, ChatListIndex(pinningIndex: 0, messageIndex: MessageIndex(id: MessageId(peerId: PeerId(0), namespace: 0, id: Int32.max), timestamp: Int32.max)), nil, top.isEmpty && recent.isEmpty)) + contains[user.id] = user.id + + if !top.isEmpty { + entries.insert(.separator(L10n.searchSeparatorPopular.uppercased(), chatListIndex()), at: 0) + + var count: Int32 = 0 + for peer in top { if contains[peer.id] == nil { if share.possibilityPerformTo(peer) { - entries.insert(.plain(peer, chatListIndex(), nil, true), at: 0) + entries.insert(.plain(peer, chatListIndex(), nil, count < 4), at: 0) contains[peer.id] = peer.id + count += 1 + } + } + if count >= 5 { + break + } + } + } + + if !recent.isEmpty { + + entries.insert(.separator(L10n.searchSeparatorRecent.uppercased(), chatListIndex()), at: 0) + + for rendered in recent { + if let peer = rendered.peer.chatMainPeer { + if contains[peer.id] == nil { + if share.possibilityPerformTo(peer) { + entries.insert(.plain(peer, chatListIndex(), nil, true), at: 0) + contains[peer.id] = peer.id + } } } } } + + entries.sort(by: <) + + return prepareEntries(from: previous.swap(entries), to: entries, account: context.account, initialSize: initialSize, animated: true, multipleSelection: multipleSelection, selectInteraction:selectInteraction) + } + } else { + var signal:Signal<(ChatListView,ViewUpdateType), NoError> - entries.sort(by: <) - return prepareEntries(from: previous.swap(entries), to: entries, account: account, initialSize: initialSize, animated: true, selectInteraction:selectInteraction) - - } - } else if search.isEmpty { - - - var signal:Signal<(ChatListView,ViewUpdateType),Void> - - switch(location) { - case let .Initial(count, _): - signal = account.viewTracker.tailChatListView(count: count) - case let .Index(index): - signal = account.viewTracker.aroundChatListView(index: index, count: 30) - } - - return signal |> deliverOnPrepareQueue |> mapToSignal { value -> Signal<(ChatListView,ViewUpdateType, [PeerId: PeerStatusStringResult]), Void> in - var peerIds:[PeerId] = [] - for entry in value.0.entries { - switch entry { - case let .MessageEntry(_, _, _, _, _, renderedPeer, _): - peerIds.append(renderedPeer.peerId) - default: - break - } + switch(location) { + case let .Initial(count, _): + signal = context.account.viewTracker.tailChatListView(groupId: .root, filterPredicate: nil, count: count) |> take(1) + case let .Index(index, _): + signal = context.account.viewTracker.aroundChatListView(groupId: .root, filterPredicate: nil, index: index, count: 30) |> take(1) } - let keys = peerIds.map {PostboxViewKey.peer(peerId: $0)} - return account.postbox.combinedView(keys: keys) |> map { values -> (ChatListView,ViewUpdateType, [PeerId: PeerStatusStringResult]) in + + return signal |> deliverOnPrepareQueue |> mapToSignal { value -> Signal<(ChatListView,ViewUpdateType, [PeerId: PeerStatusStringResult], Peer), NoError> in + var peerIds:[PeerId] = [] + for entry in value.0.entries { + switch entry { + case let .MessageEntry(_, _, _, _, _, renderedPeer, _, _, _, _): + peerIds.append(renderedPeer.peerId) + default: + break + } + } - var presences:[PeerId: PeerStatusStringResult] = [:] - for value in values.views { - if let view = value.value as? PeerView { - presences[view.peerId] = stringStatus(for: view) + _ = previousChatList.swap(value.0) + + let keys = peerIds.map {PostboxViewKey.peer(peerId: $0, components: .all)} + return combineLatest(context.account.postbox.combinedView(keys: keys), context.account.postbox.loadedPeerWithId(context.peerId)) |> map { values, selfPeer in + var presences:[PeerId: PeerStatusStringResult] = [:] + for value in values.views { + if let view = value.value as? PeerView { + presences[view.peerId] = stringStatus(for: view, context: context) + } + } + + return (value.0, value.1, presences, selfPeer) + + } |> take(1) + } |> deliverOn(prepareQueue) |> take(1) |> map { value -> TableUpdateTransition in + var entries:[SelectablePeersEntry] = [] + + var contains:[PeerId:PeerId] = [:] + + var offset: Int32 = Int32.max + + if let additionTopItems = share.additionTopItems { + var index = ChatListIndex(pinningIndex: 0, messageIndex: MessageIndex(id: MessageId(peerId: PeerId(0), namespace: 0, id: offset), timestamp: offset)) + entries.append(.separator(additionTopItems.topSeparator, index)) + offset -= 1 + + + for item in additionTopItems.items { + index = ChatListIndex(pinningIndex: 0, messageIndex: MessageIndex(id: MessageId(peerId: PeerId(0), namespace: 0, id: offset), timestamp: offset)) + let theme = PeerStatusStringTheme() + + let status = NSAttributedString.initialize(string: item.status, color: theme.statusColor, font: theme.statusFont) + let title = NSAttributedString.initialize(string: item.peer.displayTitle, color: theme.titleColor, font: theme.titleFont) + entries.append(.plain(item.peer, index, PeerStatusStringResult(title, status), true)) + offset -= 1 } + + index = ChatListIndex(pinningIndex: 0, messageIndex: MessageIndex(id: MessageId(peerId: PeerId(0), namespace: 0, id: offset), timestamp: offset)) + entries.append(.separator(additionTopItems.bottomSeparator, index)) + offset -= 1 } - return (value.0, value.1, presences) + if !share.excludePeerIds.contains(value.3.id) { + entries.append(.plain(value.3, ChatListIndex(pinningIndex: 0, messageIndex: MessageIndex(id: MessageId(peerId: PeerId(0), namespace: 0, id: offset), timestamp: offset)), nil, true)) + contains[value.3.id] = value.3.id + } - } |> take(1) - } |> deliverOn(prepareQueue) |> take(1) |> map { value -> TableUpdateTransition in - var entries:[SelectablePeersEntry] = [] - - var contains:[PeerId:PeerId] = [:] - - for entry in value.0.entries { - switch entry { - case let .MessageEntry(id, _, _, _, _, renderedPeer, _): - if let peer = renderedPeer.chatMainPeer { - if contains[peer.id] == nil { - if share.possibilityPerformTo(peer) { - entries.append(.plain(peer,id, value.2[peer.id], true)) - contains[peer.id] = peer.id + for entry in value.0.entries { + switch entry { + case let .MessageEntry(id, _, _, _, _, renderedPeer, _, _, _, _): + if let main = renderedPeer.peer { + if contains[main.id] == nil { + if share.possibilityPerformTo(main) { + if let peer = renderedPeer.chatMainPeer { + if main.id.namespace == Namespaces.Peer.SecretChat { + entries.append(.secretChat(peer, main.id, id, value.2[peer.id], true)) + } else { + entries.append(.plain(peer, id, value.2[peer.id], true)) + } + } + contains[main.id] = main.id + } } } + default: + break } - default: - break } + + entries.sort(by: <) + + return prepareEntries(from: previous.swap(entries), to: entries, account: context.account, initialSize: initialSize, animated: true, multipleSelection: multipleSelection, selectInteraction:selectInteraction) } - - entries.sort(by: <) - - return prepareEntries(from: previous.swap(entries), to: entries, account: account, initialSize: initialSize, animated: true, selectInteraction:selectInteraction) } + + } else { - return account.postbox.searchPeers(query: search.lowercased()) |> map { - return $0.flatMap({$0.chatMainPeer}).filter({!($0 is TelegramSecretChat)}) - } |> mapToSignal { peers -> Signal<([Peer], [PeerId: PeerStatusStringResult]), Void> in - let keys = peers.map {PostboxViewKey.peer(peerId: $0.id)} - return account.postbox.combinedView(keys: keys) |> map { values -> ([Peer], [PeerId: PeerStatusStringResult]) in + + _ = previousChatList.swap(nil) + + var all = query.request.transformKeyboard + all.insert(query.request.lowercased(), at: 0) + all = all.uniqueElements + let localPeers = combineLatest(all.map { + return context.account.postbox.searchPeers(query: $0) + }) |> map { result in + return result.reduce([], { + return $0 + $1 + }) + } + + let remotePeers = Signal<[RenderedPeer], NoError>.single([]) |> then( context.engine.peers.searchPeers(query: query.request.lowercased()) |> map { $0.0.map {RenderedPeer($0)} + $0.1.map {RenderedPeer($0)} } ) + + return combineLatest(localPeers, remotePeers) |> map {$0 + $1} |> mapToSignal { peers -> Signal<([RenderedPeer], [PeerId: PeerStatusStringResult], Peer), NoError> in + let keys = peers.map {PostboxViewKey.peer(peerId: $0.peerId, components: .all)} + return combineLatest(context.account.postbox.combinedView(keys: keys), context.account.postbox.loadedPeerWithId(context.peerId)) |> map { values, selfPeer -> ([RenderedPeer], [PeerId: PeerStatusStringResult], Peer) in var presences:[PeerId: PeerStatusStringResult] = [:] for value in values.views { if let view = value.value as? PeerView { - presences[view.peerId] = stringStatus(for: view) + presences[view.peerId] = stringStatus(for: view, context: context) } } - return (peers, presences) + return (peers, presences, selfPeer) } |> take(1) - } |> deliverOn(prepareQueue) |> take(1) |> map { values -> TableUpdateTransition in + } |> deliverOn(prepareQueue) |> map { values -> TableUpdateTransition in var entries:[SelectablePeersEntry] = [] var contains:[PeerId:PeerId] = [:] var i:Int32 = Int32.max - for peer in values.0 { - if share.possibilityPerformTo(peer), contains[peer.id] == nil { - let index = MessageIndex(id: MessageId(peerId: PeerId(0), namespace: Namespaces.Message.Cloud, id: i), timestamp: i) - entries.append(.plain(peer, ChatListIndex(pinningIndex: nil, messageIndex: index), values.1[peer.id], true)) - i -= 1 - contains[peer.id] = peer.id + if L10n.peerSavedMessages.lowercased().hasPrefix(query.request.lowercased()) || NSLocalizedString("Peer.SavedMessages", comment: "nil").lowercased().hasPrefix(query.request.lowercased()) || values.0.contains(where: {$0.peerId == context.peerId}), !share.excludePeerIds.contains(values.2.id) { + let index = MessageIndex(id: MessageId(peerId: PeerId(0), namespace: 0, id: i), timestamp: i) + entries.append(.plain(values.2, ChatListIndex(pinningIndex: 0, messageIndex: index), nil, true)) + i -= 1 + contains[values.2.id] = values.2.id + } + for renderedPeer in values.0 { + if let main = renderedPeer.peer { + if contains[main.id] == nil { + if share.possibilityPerformTo(main) { + if let peer = renderedPeer.chatMainPeer { + + let index = MessageIndex(id: MessageId(peerId: PeerId(0), namespace: 0, id: i), timestamp: i) + let id = ChatListIndex(pinningIndex: nil, messageIndex: index) + i -= 1 + + if main.id.namespace == Namespaces.Peer.SecretChat { + entries.append(.secretChat(peer, main.id, id, values.1[peer.id], true)) + } else { + entries.append(.plain(peer, id, values.1[peer.id], true)) + } + } + contains[main.id] = main.id + } + } } } if entries.isEmpty { @@ -629,20 +1389,42 @@ class ShareModalController: ModalViewController, Notifable { entries.sort(by: <) - return prepareEntries(from: previous.swap(entries), to: entries, account: account, initialSize: initialSize, animated: true, selectInteraction:selectInteraction) + return prepareEntries(from: previous.swap(entries), to: entries, account: context.account, initialSize: initialSize, animated: false, multipleSelection: multipleSelection, selectInteraction:selectInteraction) } } } |> deliverOnMainQueue - disposable.set(list.start(next: { [weak self] transition in + let signal:Signal = defaultItems |> deliverOnMainQueue |> mapToSignal { [weak self] defaultSelected in + + self?.selectInteractions.update(animated: false, { value in + var value = value + for peer in defaultSelected { + value = value.withToggledSelected(peer.id, peer: peer) + } + return value + }) + + return list + } |> deliverOnMainQueue + + disposable.set(signal.start(next: { [weak self] transition in self?.genericView.tableView.resetScrollNotifies() - self?.genericView.tableView.merge(with:transition) + self?.genericView.tableView.scroll(to: .up(false)) + self?.genericView.tableView.merge(with: transition) + self?.genericView.tableView.cancelHighlight() + self?.readyOnce() + })) request.set(.single(.Initial(100, nil))) + + self.genericView.tableView.setScrollHandler { position in + let view = previousChatList.modify({$0}) + } + } override var canBecomeResponder: Bool { @@ -650,96 +1432,308 @@ class ShareModalController: ModalViewController, Notifable { } override func becomeFirstResponder() -> Bool? { + _ = window?.makeFirstResponder(nil) return false } override func firstResponder() -> NSResponder? { - return genericView.searchView.responder + if window?.firstResponder == genericView.textView.inputView { + return genericView.textView.inputView + } + + if let event = NSApp.currentEvent { + if event.type == .keyDown { + switch event.keyCode { + case KeyboardKey.UpArrow.rawValue: + return window?.firstResponder + case KeyboardKey.DownArrow.rawValue: + return window?.firstResponder + default: + break + } + } + } + + if self.share.multipleSelection { + return genericView.tokenizedView.responder + } else { + return genericView.basicSearchView.input + } } override func returnKeyAction() -> KeyHandlerResult { - if !selectInteractions.presentation.peers.isEmpty { - share.perform(to: selectInteractions.presentation.peers.map {$0.key}) - modal?.close(true) + if let event = NSApp.currentEvent, !FastSettings.checkSendingAbility(for: event) { + return .rejected + } + return invoke() + } + + private func invoke() -> KeyHandlerResult { + if !genericView.tokenizedView.query.isEmpty { + if !genericView.tableView.isEmpty, let item = genericView.tableView.item(at: 0) as? ShortPeerRowItem { + selectInteractions.update({$0.withToggledSelected(item.peer.id, peer: item.peer)}) + } return .invoked } + if !selectInteractions.presentation.peers.isEmpty || share.alwaysEnableDone { + + let ids = selectInteractions.presentation.selected + + let account = share.context.account + + let peerAndData:Signal<[(TelegramChannel, CachedChannelData?)], NoError> = share.context.account.postbox.transaction { transaction in + var result:[(TelegramChannel, CachedChannelData?)] = [] + for id in ids { + if let peer = transaction.getPeer(id) as? TelegramChannel { + result.append((peer, transaction.getPeerCachedData(peerId: id) as? CachedChannelData)) + } + } + return result + } |> deliverOnMainQueue + + + + let signal = peerAndData |> mapToSignal { peerAndData in + return account.postbox.unsentMessageIdsView() |> take(1) |> map { + (peerAndData, Set($0.ids.map { $0.peerId })) + } + } |> deliverOnMainQueue + + _ = signal.start(next: { [weak self] (peerAndData, unsentIds) in + guard let `self` = self else { return } + let share = self.share + let comment = self.genericView.textView.string() + + enum ShareFailedTarget { + case token + case comment + } + struct ShareFailedReason { + let peerId:PeerId + let reason: String + let target: ShareFailedTarget + } + + var failed:[ShareFailedReason] = [] + + for (peer, cachedData) in peerAndData { + inner: switch peer.info { + case let .group(info): + if info.flags.contains(.slowModeEnabled) && (peer.adminRights == nil && !peer.flags.contains(.isCreator)) { + if let cachedData = cachedData, let validUntil = cachedData.slowModeValidUntilTimestamp { + if validUntil > share.context.timestamp { + failed.append(ShareFailedReason(peerId: peer.id, reason: slowModeTooltipText(validUntil - share.context.timestamp), target: .token)) + } + } + if !comment.isEmpty { + failed.append(ShareFailedReason(peerId: peer.id, reason: L10n.slowModeForwardCommentError, target: .comment)) + } + if unsentIds.contains(peer.id) { + failed.append(ShareFailedReason(peerId: peer.id, reason: L10n.slowModeMultipleError, target: .token)) + } + } + + default: + break inner + } + } + if failed.isEmpty { + self.genericView.tokenizedView.removeAllFailed(animated: true) + _ = share.perform(to: Array(ids), comment: self.contextChatInteraction.presentation.interfaceState.inputState).start() + self.emoji.popover?.hide() + self.close() + } else { + self.genericView.tokenizedView.markAsFailed(failed.map { + $0.peerId.toInt64() + }, animated: true) + + let last = failed.last! + + switch last.target { + case .comment: + self.genericView.textView.shake() + tooltip(for: self.genericView.bottomSeparator, text: last.reason) + case .token: + self.genericView.tokenizedView.addTooltip(for: last.peerId.toInt64(), text: last.reason) + } + } + }) + + return .invoked + } + + if share is ForwardMessagesObject { + if genericView.tableView.highlightedItem() == nil, !genericView.tableView.isEmpty { + let item = genericView.tableView.item(at: 0) + if let item = item as? ShortPeerRowItem { + item.action() + } + _ = genericView.tableView.select(item: item) + return .invoked + } + } + return .rejected } override func escapeKeyAction() -> KeyHandlerResult { - - if genericView.searchView.state == .Focus { - window?.makeFirstResponder(nil) + if genericView.tableView.highlightedItem() != nil { + genericView.tableView.cancelHighlight() + return .invoked + } + if genericView.tokenizedView.state == .Focus { + _ = window?.makeFirstResponder(nil) return .invoked } return .rejected } + private let emoji: EmojiViewController init(_ share:ShareObject) { self.share = share + emoji = EmojiViewController(share.context) + self.contextChatInteraction = ChatInteraction(chatLocation: .peer(PeerId(0)), context: share.context) + inputContextHelper = InputContextHelper(chatInteraction: contextChatInteraction) super.init(frame: NSMakeRect(0, 0, 360, 400)) bar = .init(height: 0) + + + contextChatInteraction.movePeerToInput = { [weak self] peer in + if let strongSelf = self { + let string = strongSelf.genericView.textView.string() + let range = strongSelf.genericView.textView.selectedRange() + let textInputState = ChatTextInputState(inputText: string, selectionRange: range.min ..< range.max, attributes: chatTextAttributes(from: strongSelf.genericView.textView.attributedString())) + strongSelf.contextChatInteraction.update({$0.withUpdatedEffectiveInputState(textInputState)}) + if let (range, _, _) = textInputStateContextQueryRangeAndType(textInputState, includeContext: false) { + let inputText = textInputState.inputText + + let name:String = peer.addressName ?? peer.compactDisplayTitle + + let distance = inputText.distance(from: range.lowerBound, to: range.upperBound) + let replacementText = name + " " + + let atLength = peer.addressName != nil ? 0 : 1 + + let range = strongSelf.contextChatInteraction.appendText(replacementText, selectedRange: textInputState.selectionRange.lowerBound - distance - atLength ..< textInputState.selectionRange.upperBound) + + if peer.addressName == nil { + let state = strongSelf.contextChatInteraction.presentation.effectiveInput + var attributes = state.attributes + attributes.append(.uid(range.lowerBound ..< range.upperBound - 1, peer.id.id._internalGetInt64Value())) + let updatedState = ChatTextInputState(inputText: state.inputText, selectionRange: state.selectionRange, attributes: attributes) + strongSelf.contextChatInteraction.update({$0.withUpdatedEffectiveInputState(updatedState)}) + } + } + } + } + + + bar = .init(height: 0) + } + + func showEmoji(for control: Control) { + showPopover(for: control, with: emoji) + } + + func textViewHeightChanged(_ height: CGFloat, animated: Bool) { + + updateSize(frame.width, animated: animated) + + genericView.textViewUpdateHeight(height, animated) + + } + + func textViewEnterPressed(_ event: NSEvent) -> Bool { + if FastSettings.checkSendingAbility(for: event) { + _ = returnKeyAction() + return true + } + return false + } + + func textViewTextDidChange(_ string: String) { + let range = self.genericView.textView.selectedRange() + self.selectInteractions.update { + $0.withUpdatedComment(.init(string: string, range: range)) + } + let attributed = genericView.textView.attributedString() + let state = ChatTextInputState(inputText: attributed.string, selectionRange: range.location ..< range.location + range.length, attributes: chatTextAttributes(from: attributed)) + contextChatInteraction.update({$0.withUpdatedEffectiveInputState(state)}) + + } + + func textViewTextDidChangeSelectedRange(_ range: NSRange) { + let string = self.genericView.textView.string() + self.selectInteractions.update { + $0.withUpdatedComment(.init(string: string, range: range)) + } + let attributed = genericView.textView.attributedString() + let state = ChatTextInputState(inputText: attributed.string, selectionRange: range.location ..< range.location + range.length, attributes: chatTextAttributes(from: attributed)) + contextChatInteraction.update({$0.withUpdatedEffectiveInputState(state)}) + } + + func textViewDidReachedLimit(_ textView: Any) { + genericView.textView.shake() + } + + func textViewDidPaste(_ pasteboard: NSPasteboard) -> Bool { + return false + } + + func textViewSize(_ textView: TGModernGrowingTextView!) -> NSSize { + return NSMakeSize(frame.width - 40, textView.frame.height) + } + + func textViewIsTypingEnabled() -> Bool { + return true + } + + func canTransformInputText() -> Bool { + return true + } + + func maxCharactersLimit(_ textView: TGModernGrowingTextView!) -> Int32 { + return 1024 + } + + private func updateSize(_ width: CGFloat, animated: Bool) { + if let contentSize = self.window?.contentView?.frame.size { + self.modal?.resize(with:NSMakeSize(width, min(contentSize.height - 100, genericView.tableView.listHeight + max(genericView.additionHeight, 88))), animated: animated) + } + } + + override var modalInteractions: ModalInteractions? { + if !share.hasCaptionView { + return ModalInteractions(acceptTitle: share.interactionOk, accept: { [weak self] in + _ = self?.invoke() + }, drawBorder: true, height: 50, singleButton: true) + } else { + return nil + } + } + + override func measure(size: NSSize) { + self.modal?.resize(with:NSMakeSize(genericView.frame.width, min(size.height - 100, genericView.tableView.listHeight + max(genericView.additionHeight, 88))), animated: false) + } + + override var dynamicSize: Bool { + return true } -// override var modalInteractions: ModalInteractions? { -// if let share = share as? ShareMessageObject { -// if let link = share.link { -// return ModalInteractions(acceptTitle:tr(.modalShare), accept:{ [weak self] in -// if let interactions = self?.selectInteractions, let share = self?.share { -// share.perform(to: interactions.presentation.selected.map({$0})) -// } -// self?.modal?.close() -// }, cancelTitle:tr(.modalCopyLink), cancel: { [weak self] in -// if let strongSelf = self, let share = strongSelf.share as? ShareMessageObject { -// -// self?.exportLinkDisposable.set(exportMessageLink(account: strongSelf.share.account, peerId: share.messageIds[0].peerId, messageId: share.messageIds[0]).start(next: { valueLink in -// if let valueLink = valueLink { -// copyToClipboard(valueLink) -// } else { -// copyToClipboard(link) -// } -// })) -// } -// -// self?.show(toaster: ControllerToaster(text: tr(.shareLinkCopied), height:50), for: 2.0, animated: true) -// }, drawBorder:true, height:40) -// } else { -// return ModalInteractions(acceptTitle:tr(.modalShare), accept:{ [weak self] in -// if let interactions = self?.selectInteractions, let share = self?.share { -// share.perform(to: interactions.presentation.selected.map({$0})) -// } -// self?.modal?.close() -// }, cancelTitle: tr(.modalCancel), drawBorder:true, height:40) -// } -// -// } else if let share = share as? ShareLinkObject { -// return ModalInteractions(acceptTitle: tr(.modalShare), accept:{ [weak self] in -// if let interactions = self?.selectInteractions, let share = self?.share { -// share.perform(to: interactions.presentation.selected.map({$0})) -// } -// self?.modal?.close() -// }, cancelTitle: tr(.modalCopyLink), cancel: { [weak self] in -// copyToClipboard(share.link) -// self?.show(toaster: ControllerToaster(text: tr(.shareLinkCopied), height:50), for: 2.0, animated: true) -// }, drawBorder:true, height:40) -// -// } else if let _ = share as? ShareContactObject { -// return ModalInteractions(acceptTitle: tr(.modalShare), accept:{ [weak self] in -// if let interactions = self?.selectInteractions, let share = self?.share { -// share.perform(to: interactions.presentation.selected.map({$0})) -// } -// self?.modal?.close() -// }, drawBorder:true, height:40) -// } -// return nil -// } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) } + override func close(animationType: ModalAnimationCloseBehaviour = .common) { + if self.share.emptyPerformOnClose { + _ = self.share.perform(to: []).start() + } + super.close(animationType: animationType) + } + deinit { disposable.dispose() tokenDisposable.dispose() diff --git a/Telegram-Mac/SharedAccountContext.swift b/Telegram-Mac/SharedAccountContext.swift new file mode 100644 index 0000000000..cf9feb8b7b --- /dev/null +++ b/Telegram-Mac/SharedAccountContext.swift @@ -0,0 +1,656 @@ +// +// SharedAccountContext.swift +// Telegram +// +// Created by Mikhail Filimonov on 25/02/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import Postbox +import SwiftSignalKit +import TGUIKit + + + + +private struct AccountAttributes: Equatable { + let sortIndex: Int32 + let isTestingEnvironment: Bool + let backupData: AccountBackupData? +} + + + +private enum AddedAccountsResult { + case upgrading(Float) + case ready([(AccountRecordId, Account?, Int32)]) +} +private enum AddedAccountResult { + case upgrading(Float) + case ready(AccountRecordId, Account?, Int32) +} + + + + +public final class AccountWithInfo: Equatable { + public let account: Account + public let peer: Peer + + init(account: Account, peer: Peer) { + self.account = account + self.peer = peer + } + + public static func ==(lhs: AccountWithInfo, rhs: AccountWithInfo) -> Bool { + if lhs.account !== rhs.account { + return false + } + if !arePeersEqual(lhs.peer, rhs.peer) { + return false + } + return true + } +} + + + + +class SharedAccountContext { + let accountManager: AccountManager + var bindings: AccountContextBindings = AccountContextBindings() + + #if !SHARE + let inputSource: InputSources = InputSources() + let devicesContext: DevicesContext + private let _baseSettings: Atomic = Atomic(value: BaseApplicationSettings.defaultSettings) + + var baseSettings: BaseApplicationSettings { + return _baseSettings.with { $0 } + } + #endif + + private let managedAccountDisposables = DisposableDict() + + + private let appEncryption: Atomic + + var appEncryptionValue: AppEncryptionParameters { + return appEncryption.with { $0 } + } + + func updateAppEncryption(_ f: (AppEncryptionParameters)->AppEncryptionParameters) { + _ = self.appEncryption.modify(f) + } + + + private var activeAccountsValue: (primary: Account?, accounts: [(AccountRecordId, Account, Int32)], currentAuth: UnauthorizedAccount?)? + private let activeAccountsPromise = Promise<(primary: Account?, accounts: [(AccountRecordId, Account, Int32)], currentAuth: UnauthorizedAccount?)>() + var activeAccounts: Signal<(primary: Account?, accounts: [(AccountRecordId, Account, Int32)], currentAuth: UnauthorizedAccount?), NoError> { + return self.activeAccountsPromise.get() + } + private var activeAccountsInfoValue:(primary: AccountRecordId?, accounts: [AccountWithInfo])? + private let activeAccountsWithInfoPromise = Promise<(primary: AccountRecordId?, accounts: [AccountWithInfo])>() + var activeAccountsWithInfo: Signal<(primary: AccountRecordId?, accounts: [AccountWithInfo]), NoError> { + return self.activeAccountsWithInfoPromise.get() + } + + private var accountPhotos: [PeerId : CGImage] = [:] + private var cleaningUpAccounts = false + + private(set) var layout:SplitViewState = .none + let layoutHandler:ValuePromise = ValuePromise(ignoreRepeated:true) + + private var statusItem: NSStatusItem? + + + func updateStatusBarImage(_ image: NSImage?) -> Void { + let icon = image ?? NSImage(named: "StatusIcon") + // icon?.isTemplate = true + statusItem?.image = icon + } + + private func updateStatusBarMenuItem() { + let menu = NSMenu() + + if let activeAccountsInfoValue = activeAccountsInfoValue, activeAccountsInfoValue.accounts.count > 1 { + var activeAccountsInfoValue = activeAccountsInfoValue + for (i, value) in activeAccountsInfoValue.accounts.enumerated() { + if value.account.id == activeAccountsInfoValue.primary { + activeAccountsInfoValue.accounts.swapAt(i, 0) + break + } + } + for account in activeAccountsInfoValue.accounts { + let state: NSControl.StateValue? + if account.account.id == activeAccountsInfoValue.primary { + state = .on + } else { + state = nil + } + let image: NSImage? + if let cgImage = self.accountPhotos[account.account.peerId] { + image = NSImage(cgImage: cgImage, size: NSMakeSize(16, 16)) + } else { + image = nil + } + + menu.addItem(ContextMenuItem(account.peer.displayTitle, handler: { + self.switchToAccount(id: account.account.id, action: nil) + }, image: image, state: state)) + + if account.account.id == activeAccountsInfoValue.primary { + menu.addItem(ContextSeparatorItem()) + } + } + + + menu.addItem(ContextSeparatorItem()) + } + + menu.addItem(ContextMenuItem(L10n.statusBarActivate, handler: { + if !mainWindow.isKeyWindow { + NSApp.activate(ignoringOtherApps: true) + mainWindow.deminiaturize(nil) + } else { + NSApp.hide(nil) + } + + }, dynamicTitle: { + return !mainWindow.isKeyWindow ? L10n.statusBarActivate : L10n.statusBarHide + })) + + menu.addItem(ContextMenuItem(L10n.statusBarQuit, handler: { + NSApp.terminate(nil) + })) + + statusItem?.menu = menu + } + + private func updateStatusBar(_ show: Bool) { + if show { + if statusItem == nil { + statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) + } + } else { + if let statusItem = statusItem { + NSStatusBar.system.removeStatusItem(statusItem) + self.statusItem = nil + } + } + } + + private let layoutDisposable = MetaDisposable() + private let displayUpgradeProgress: (Float?) -> Void + + + + init(accountManager: AccountManager, networkArguments: NetworkInitializationArguments, rootPath: String, encryptionParameters: ValueBoxEncryptionParameters, appEncryption: AppEncryptionParameters, displayUpgradeProgress: @escaping(Float?) -> Void) { + self.accountManager = accountManager + self.displayUpgradeProgress = displayUpgradeProgress + self.appEncryption = Atomic(value: appEncryption) + #if !SHARE + self.devicesContext = DevicesContext(accountManager) + self.accountManager.mediaBox.fetchCachedResourceRepresentation = { (resource, representation) -> Signal in + return fetchCachedSharedResourceRepresentation(accountManager: accountManager, resource: resource, representation: representation) + } + _ = (baseAppSettings(accountManager: accountManager) |> deliverOnMainQueue).start(next: { settings in + _ = self._baseSettings.swap(settings) + self.updateStatusBar(settings.statusBar) + forceUpdateStatusBarIconByDockTile(sharedContext: self) + }) + #endif + + + layoutDisposable.set(layoutHandler.get().start(next: { state in + self.layout = state + })) + + var supplementary: Bool = false + #if SHARE + supplementary = true + #endif + + + + + + + let differenceDisposable = MetaDisposable() + let _ = (accountManager.accountRecords() + |> map { view -> (AccountRecordId?, [AccountRecordId: AccountAttributes], (AccountRecordId, Bool)?) in + var result: [AccountRecordId: AccountAttributes] = [:] + for record in view.records { + let isLoggedOut = record.attributes.contains(where: { attribute in + if case .loggedOut = attribute { + return true + } else { + return false + } + }) + if isLoggedOut { + continue + } + + let isTestingEnvironment = record.attributes.contains(where: { attribute in + if case let .environment(environment) = attribute, case .test = environment.environment { + return true + } else { + return false + } + }) + + var backupData: AccountBackupData? + var sortIndex: Int32 = 0 + for attribute in record.attributes { + if case let .sortOrder(sortOrder) = attribute { + sortIndex = sortOrder.order + } else if case let .backupData(backupDataValue) = attribute { + backupData = backupDataValue.data + } + } + + result[record.id] = AccountAttributes(sortIndex: sortIndex, isTestingEnvironment: isTestingEnvironment, backupData: backupData) + } + let authRecord: (AccountRecordId, Bool)? = view.currentAuthAccount.flatMap({ authAccount in + let isTestingEnvironment = authAccount.attributes.contains(where: { attribute in + if case let .environment(environment) = attribute, case .test = environment.environment { + return true + } else { + return false + } + }) + return (authAccount.id, isTestingEnvironment) + }) + + return (view.currentRecord?.id, result, authRecord) + } + |> distinctUntilChanged(isEqual: { lhs, rhs in + if lhs.0 != rhs.0 { + return false + } + if lhs.1 != rhs.1 { + return false + } + if lhs.2?.0 != rhs.2?.0 { + return false + } + if lhs.2?.1 != rhs.2?.1 { + return false + } + return true + }) + |> deliverOnMainQueue).start(next: { primaryId, records, authRecord in + var addedSignals: [Signal] = [] + var addedAuthSignal: Signal = .single(nil) + for (id, attributes) in records { + if self.activeAccountsValue?.accounts.firstIndex(where: { $0.0 == id}) == nil { + addedSignals.append(accountWithId(accountManager: accountManager, networkArguments: networkArguments, id: id, encryptionParameters: encryptionParameters, supplementary: supplementary, rootPath: rootPath, beginWithTestingEnvironment: attributes.isTestingEnvironment, backupData: attributes.backupData, auxiliaryMethods: telegramAccountAuxiliaryMethods) + |> map { result -> AddedAccountResult in + switch result { + case let .authorized(account): + #if SHARE + setupAccount(account, fetchCachedResourceRepresentation: nil, transformOutgoingMessageMedia: nil, preFetchedResourcePath: { resource in + return nil + }) + #else + setupAccount(account, fetchCachedResourceRepresentation: fetchCachedResourceRepresentation, transformOutgoingMessageMedia: transformOutgoingMessageMedia, preFetchedResourcePath: { resource in + return nil + }) + #endif + + return .ready(id, account, attributes.sortIndex) + case let .upgrading(progress): + return .upgrading(progress) + default: + return .ready(id, nil, attributes.sortIndex) + } + }) + + } + } + if let authRecord = authRecord, authRecord.0 != self.activeAccountsValue?.currentAuth?.id { + addedAuthSignal = accountWithId(accountManager: accountManager, networkArguments: networkArguments, id: authRecord.0, encryptionParameters: encryptionParameters, supplementary: supplementary, rootPath: rootPath, beginWithTestingEnvironment: authRecord.1, backupData: nil, auxiliaryMethods: telegramAccountAuxiliaryMethods) + |> map { result -> UnauthorizedAccount? in + switch result { + case let .unauthorized(account): + return account + default: + return nil + } + } + } + + let mappedAddedAccounts = combineLatest(queue: .mainQueue(), addedSignals) + |> map { results -> AddedAccountsResult in + var readyAccounts: [(AccountRecordId, Account?, Int32)] = [] + var totalProgress: Float = 0.0 + var hasItemsWithProgress = false + for result in results { + switch result { + case let .ready(id, account, sortIndex): + readyAccounts.append((id, account, sortIndex)) + totalProgress += 1.0 + case let .upgrading(progress): + hasItemsWithProgress = true + totalProgress += progress + } + } + if hasItemsWithProgress, !results.isEmpty { + return .upgrading(totalProgress / Float(results.count)) + } else { + return .ready(readyAccounts) + } + } + + differenceDisposable.set(combineLatest(queue: .mainQueue(), mappedAddedAccounts, addedAuthSignal).start(next: { mappedAddedAccounts, authAccount in + var addedAccounts: [(AccountRecordId, Account?, Int32)] = [] + switch mappedAddedAccounts { + case let .upgrading(progress): + self.displayUpgradeProgress(progress) + return + case let .ready(value): + addedAccounts = value + } + + var hadUpdates = false + if self.activeAccountsValue == nil { + self.activeAccountsValue = (nil, [], nil) + hadUpdates = true + } + + struct AccountPeerKey: Hashable { + let peerId: PeerId + let isTestingEnvironment: Bool + } + + + var existingAccountPeerKeys = Set() + for accountRecord in addedAccounts { + if let account = accountRecord.1 { + if existingAccountPeerKeys.contains(AccountPeerKey(peerId: account.peerId, isTestingEnvironment: account.testingEnvironment)) { + let _ = accountManager.transaction({ transaction in + transaction.updateRecord(accountRecord.0, { _ in + return nil + }) + }).start() + } else { + existingAccountPeerKeys.insert(AccountPeerKey(peerId: account.peerId, isTestingEnvironment: account.testingEnvironment)) + if let index = self.activeAccountsValue?.accounts.firstIndex(where: { $0.0 == account.id }) { + self.activeAccountsValue?.accounts.remove(at: index) + assertionFailure() + } + self.activeAccountsValue!.accounts.append((account.id, account, accountRecord.2)) + self.managedAccountDisposables.set(self.updateAccountBackupData(account: account).start(), forKey: account.id) + account.resetStateManagement() + hadUpdates = true + } + } else { + let _ = accountManager.transaction({ transaction in + transaction.updateRecord(accountRecord.0, { _ in + return nil + }) + }).start() + } + } + var removedIds: [AccountRecordId] = [] + for id in self.activeAccountsValue!.accounts.map({ $0.0 }) { + if records[id] == nil { + removedIds.append(id) + } + } + for id in removedIds { + hadUpdates = true + if let index = self.activeAccountsValue?.accounts.firstIndex(where: { $0.0 == id }) { + self.activeAccountsValue?.accounts.remove(at: index) + self.managedAccountDisposables.set(nil, forKey: id) + } + } + var primary: Account? + if let primaryId = primaryId { + if let index = self.activeAccountsValue?.accounts.firstIndex(where: { $0.0 == primaryId }) { + primary = self.activeAccountsValue?.accounts[index].1 + } + } + if primary == nil && !self.activeAccountsValue!.accounts.isEmpty { + primary = self.activeAccountsValue!.accounts.first?.1 + } + if primary !== self.activeAccountsValue!.primary { + hadUpdates = true + self.activeAccountsValue!.primary?.postbox.clearCaches() + self.activeAccountsValue!.primary = primary + } + if self.activeAccountsValue!.currentAuth?.id != authRecord?.0 { + hadUpdates = true + self.activeAccountsValue!.currentAuth?.postbox.clearCaches() + self.activeAccountsValue!.currentAuth = nil + } + if let authAccount = authAccount { + hadUpdates = true + self.activeAccountsValue!.currentAuth = authAccount + } + if hadUpdates { + self.activeAccountsValue!.accounts.sort(by: { $0.2 < $1.2 }) + self.activeAccountsPromise.set(.single(self.activeAccountsValue!)) + } + + if self.activeAccountsValue!.primary == nil && self.activeAccountsValue!.currentAuth == nil { + self.beginNewAuth(testingEnvironment: false) + } + + if (authAccount != nil || self.activeAccountsValue!.primary != nil) && !self.cleaningUpAccounts { + self.cleaningUpAccounts = true + let _ = managedCleanupAccounts(networkArguments: networkArguments, accountManager: self.accountManager, rootPath: rootPath, auxiliaryMethods: telegramAccountAuxiliaryMethods, encryptionParameters: encryptionParameters).start() + } + })) + }) + + + + + self.activeAccountsWithInfoPromise.set(self.activeAccounts + |> mapToSignal { primary, accounts, _ -> Signal<(primary: AccountRecordId?, accounts: [AccountWithInfo]), NoError> in + return combineLatest(accounts.map { _, account, _ -> Signal in + let peerViewKey: PostboxViewKey = .peer(peerId: account.peerId, components: []) + return account.postbox.combinedView(keys: [peerViewKey]) + |> map { view -> AccountWithInfo? in + guard let peerView = view.views[peerViewKey] as? PeerView, let peer = peerView.peers[peerView.peerId] else { + return nil + } + return AccountWithInfo(account: account, peer: peer) + } + |> distinctUntilChanged + }) + |> map { accountsWithInfo -> (primary: AccountRecordId?, accounts: [AccountWithInfo]) in + var accountsWithInfoResult: [AccountWithInfo] = [] + for info in accountsWithInfo { + if let info = info { + accountsWithInfoResult.append(info) + } + } + return (primary?.id, accountsWithInfoResult) + } + }) + + let signal = self.activeAccountsWithInfoPromise.get() |> mapToSignal { (primary, accounts) -> Signal<(primary: AccountRecordId?, accounts: [AccountWithInfo], [PeerId : CGImage]), NoError> in + let photos:[Signal<(PeerId, CGImage?), NoError>] = accounts.map { info in + return peerAvatarImage(account: info.account, photo: .peer(info.peer, info.peer.smallProfileImage, info.peer.displayLetters, nil), displayDimensions: NSMakeSize(32, 32)) |> map { + (info.account.peerId, $0.0) + } + } + return combineLatest(photos) |> map { photos in + let photos = photos.compactMap { + return $0.1 == nil ? nil : ($0.0, $0.1!) + } + let dict:[PeerId: CGImage] = photos.reduce([:], { result, current in + var result = result + result[current.0] = current.1 + return result + }) + return (primary, accounts, dict) + } + + } |> deliverOnMainQueue + + #if !SHARE + var spotlights:[AccountRecordId : SpotlightContext] = [:] + + _ = signal.start(next: { (primary, accounts, photos) in + self.activeAccountsInfoValue = (primary, accounts) + self.accountPhotos = photos + self.updateStatusBarMenuItem() + + #if !SHARE + spotlights.removeAll() + for info in accounts { + spotlights[info.account.id] = SpotlightContext(engine: TelegramEngine(account: info.account)) + } + #endif + }) + #endif + } + + public func beginNewAuth(testingEnvironment: Bool) { + let _ = self.accountManager.transaction({ transaction -> Void in + let _ = transaction.createAuth([.environment(AccountEnvironmentAttribute(environment: testingEnvironment ? .test : .production))]) + }).start() + } + + private var launchActions:[AccountRecordId : LaunchNavigation] = [:] + + + func setLaunchAction(_ action: LaunchNavigation, for accountId: AccountRecordId) -> Void { + assert(Queue.mainQueue().isCurrent()) + launchActions[accountId] = action + } + + func getLaunchActionOnce(for accountId: AccountRecordId) -> LaunchNavigation? { + assert(Queue.mainQueue().isCurrent()) + let action = launchActions[accountId] + launchActions.removeValue(forKey: accountId) + return action + } + + #if !SHARE + private let crossCallSession: Atomic = Atomic(value: nil) + + func getCrossAccountCallSession() -> PCallSession? { + return crossCallSession.swap(nil) + } + + private let crossGroupCall: Atomic = Atomic(value: nil) + + func getCrossAccountGroupCall() -> GroupCallContext? { + return crossGroupCall.swap(nil) + } + #endif + + + private func updateAccountBackupData(account: Account) -> Signal { + return accountBackupData(postbox: account.postbox) + |> mapToSignal { backupData -> Signal in + return self.accountManager.transaction { transaction -> Void in + transaction.updateRecord(account.id, { record in + guard let record = record else { + return nil + } + let attributes = record.attributes.filter { + if case .backupData = $0 { + return false + } else { + return true + } + } + + return AccountRecord(id: record.id, attributes: attributes, temporarySessionId: record.temporarySessionId) + }) + } + |> ignoreValues + } + } + + + public func switchToAccount(id: AccountRecordId, action: LaunchNavigation?) { + if self.activeAccountsValue?.primary?.id == id { + return + } + if let action = action { + setLaunchAction(action, for: id) + } + + + assert(Queue.mainQueue().isCurrent()) + + #if SHARE + if let activeAccountsValue = self.activeAccountsValue, let account = activeAccountsValue.accounts.first(where: {$0.0 == id}) { + var activeAccountsValue = activeAccountsValue + activeAccountsValue.primary = account.1 + self.activeAccountsPromise.set(.single(activeAccountsValue)) + self.activeAccountsValue = activeAccountsValue + } + return + #else + + _ = crossCallSession.swap(bindings.callSession()) + _ = crossGroupCall.swap(bindings.groupCall()) + + _ = self.accountManager.transaction({ transaction in + if transaction.getCurrent()?.0 != id { + transaction.setCurrentId(id) + } + }).start() + #endif + + } + + + + #if !SHARE + + var hasActiveCall:Bool { + return bindings.callSession() != nil || bindings.groupCall() != nil + } + + func endCurrentCall() -> Signal { + if let groupCall = bindings.groupCall() { + return groupCall.leaveSignal() |> filter { $0 } + } else if let callSession = bindings.callSession() { + return callSession.hangUpCurrentCall() |> filter { $0 } + } + return .single(true) + } + + func showCall(with session:PCallSession) { + let callHeader = bindings.rootNavigation().callHeader + callHeader?.show(true, contextObject: session) + } + private let groupCallContextValue:Promise = Promise(nil) + var groupCallContext:Signal { + return groupCallContextValue.get() + } + func showGroupCall(with context: GroupCallContext) { + let callHeader = bindings.rootNavigation().callHeader + callHeader?.show(true, contextObject: context) + } + + func updateCurrentGroupCallValue(_ value: GroupCallContext?) -> Void { + groupCallContextValue.set(.single(value)) + } + + func endGroupCall(terminate: Bool) -> Signal { + if let groupCall = bindings.groupCall() { + return groupCall.call.leave(terminateIfPossible: terminate) |> filter { $0 } |> take(1) + } else { + return .single(true) + } + } + + #endif + deinit { + layoutDisposable.dispose() + } + +} diff --git a/Telegram-Mac/SharedAccountInfo.swift b/Telegram-Mac/SharedAccountInfo.swift new file mode 100644 index 0000000000..4ff472c696 --- /dev/null +++ b/Telegram-Mac/SharedAccountInfo.swift @@ -0,0 +1,64 @@ +// +// SharedAccountInfo.swift +// Telegram +// +// Created by Mikhail Filimonov on 28/02/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa + + +struct AccountNotificationKey: Codable { + let id: Data + let data: Data +} + +struct AccountDatacenterKey: Codable { + let id: Int64 + let data: Data +} + +struct AccountDatacenterAddress: Codable { + let host: String + let port: Int32 + let isMedia: Bool + let secret: Data? +} + +struct AccountDatacenterInfo: Codable { + let masterKey: AccountDatacenterKey + let addressList: [AccountDatacenterAddress] +} + +struct AccountProxyConnection: Codable { + let host: String + let port: Int32 + let username: String? + let password: String? + let secret: Data? +} + +struct StoredAccountInfo: Codable { + let id: Int64 + let primaryId: Int32 + let isTestingEnvironment: Bool + let peerName: String + let datacenters: [Int32: AccountDatacenterInfo] + let notificationKey: AccountNotificationKey +} + +struct StoredAccountInfos: Codable { + let proxy: AccountProxyConnection? + let accounts: [StoredAccountInfo] +} + +func loadAccountsData(rootPath: String) -> StoredAccountInfos { + guard let data = try? Data(contentsOf: URL(fileURLWithPath: rootPath + "/accounts-shared-data")) else { + return StoredAccountInfos(proxy: nil, accounts: []) + } + guard let value = try? JSONDecoder().decode(StoredAccountInfos.self, from: data) else { + return StoredAccountInfos(proxy: nil, accounts: []) + } + return value +} diff --git a/Telegram-Mac/SharedNotificationManager.swift b/Telegram-Mac/SharedNotificationManager.swift new file mode 100644 index 0000000000..c0a846f3b1 --- /dev/null +++ b/Telegram-Mac/SharedNotificationManager.swift @@ -0,0 +1,479 @@ +// +// SharedNotificationManager.swift +// Telegram +// +// Created by Mikhail Filimonov on 01/03/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import SwiftSignalKit +import Postbox +import TelegramCore + +import TGUIKit + +func getNotificationMessageId(userInfo:[AnyHashable: Any], for prefix: String) -> MessageId? { + if let msgId = userInfo["\(prefix).message.id"] as? Int32, let msgNamespace = userInfo["\(prefix).message.namespace"] as? Int32, let namespace = userInfo["\(prefix).peer.namespace"] as? Int32, let id = userInfo["\(prefix).peer.id"] as? Int64 { + return MessageId(peerId: PeerId(namespace: PeerId.Namespace._internalFromInt32Value(namespace), id: PeerId.Id._internalFromInt64Value(id)), namespace: msgNamespace, id: msgId) + } + return nil +} + +struct LockNotificationsData : Equatable { + let screenLock:Bool + let passcodeLock:Bool + + init() { + self.screenLock = false + self.passcodeLock = false + } + + init(screenLock: Bool, passcodeLock: Bool) { + self.screenLock = screenLock + self.passcodeLock = passcodeLock + } + + func withUpdatedScreenLock(_ lock: Bool) -> LockNotificationsData { + return LockNotificationsData(screenLock: lock, passcodeLock: passcodeLock) + } + func withUpdatedPasscodeLock(_ lock: Bool) -> LockNotificationsData { + return LockNotificationsData(screenLock: screenLock, passcodeLock: lock) + } + + static func ==(lhs:LockNotificationsData, rhs: LockNotificationsData) -> Bool { + return lhs.screenLock == rhs.screenLock && lhs.passcodeLock == rhs.screenLock + } + + var isLocked: Bool { + return screenLock || passcodeLock + } +} + +final class SharedNotificationBindings { + let navigateToChat:(Account, PeerId) -> Void + let navigateToThread:(Account, MessageId, MessageId) -> Void // threadId, fromId + let updateCurrectController:()->Void + let applyMaxReadIndexInteractively:(MessageIndex)->Void + init(navigateToChat: @escaping(Account, PeerId) -> Void, navigateToThread: @escaping(Account, MessageId, MessageId) -> Void, updateCurrectController: @escaping()->Void, applyMaxReadIndexInteractively:@escaping(MessageIndex)->Void) { + self.navigateToChat = navigateToChat + self.navigateToThread = navigateToThread + self.updateCurrectController = updateCurrectController + self.applyMaxReadIndexInteractively = applyMaxReadIndexInteractively + } +} + + +final class SharedNotificationManager : NSObject, NSUserNotificationCenterDelegate { + + private let screenLocked:Promise = Promise(LockNotificationsData()) + private(set) var _lockedValue:LockNotificationsData = LockNotificationsData() { + didSet { + didUpdateLocked?(_lockedValue) + } + } + + var didUpdateLocked:((LockNotificationsData)->Void)? = nil + + private let _passlock = Promise() + + var passlocked: Signal { + return _passlock.get() + } + + + private func updateLocked(_ f:(LockNotificationsData) -> LockNotificationsData) { + _lockedValue = f(_lockedValue) + screenLocked.set(.single(_lockedValue)) + } + + private let disposableDict: DisposableDict = DisposableDict() + let accountManager: AccountManager + var resignTimestamp:Int32? = nil + let window: Window + + var activeAccounts: (primary: Account?, accounts: [(AccountRecordId, Account)]) = (primary: nil, accounts: []) + let bindings: SharedNotificationBindings + private let appEncryption: AppEncryptionParameters + init(activeAccounts: Signal<(primary: Account?, accounts: [(AccountRecordId, Account)]), NoError>, appEncryption: AppEncryptionParameters, accountManager: AccountManager, window: Window, bindings: SharedNotificationBindings) { + self.accountManager = accountManager + self.window = window + self.bindings = bindings + self.appEncryption = appEncryption + + + super.init() + + UNUserNotifications.initialize(manager: self) + + + + NotificationCenter.default.addObserver(self, selector: #selector(windowDidBecomeKey), name: NSWindow.didBecomeKeyNotification, object: window) + NotificationCenter.default.addObserver(self, selector: #selector(windowDidResignKey), name: NSWindow.didResignKeyNotification, object: window) + + + DistributedNotificationCenter.default().addObserver(self, selector: #selector(screenIsLocked), name: NSNotification.Name(rawValue: "com.apple.screenIsLocked"), object: nil) + DistributedNotificationCenter.default().addObserver(self, selector: #selector(screenIsUnlocked), name: NSNotification.Name(rawValue: "com.apple.screenIsUnlocked"), object: nil) + + + _ = (_passlock.get() |> mapToSignal { show in additionalSettings(accountManager: accountManager) |> map { (show, $0) }} |> deliverOnMainQueue |> mapToSignal { show, settings -> Signal in + if show { + let controller = PasscodeLockController(accountManager, useTouchId: settings.useTouchId, logoutImpl: { + return self.logout() + }, updateCurrectController: bindings.updateCurrectController) + closeAllModals() + closeInstantView() + closeGalleryViewer(false) + showModal(with: controller, for: window, isOverlay: true) + return .single(show) |> then( controller.doneValue |> map {_ in return false} |> take(1) ) + } + return .never() + } |> deliverOnMainQueue).start(next: { [weak self] lock in + for subview in window.contentView!.subviews { + if let subview = subview as? SplitView { + subview.isHidden = lock + break + } + } + self?.updateLocked { previous -> LockNotificationsData in + return previous.withUpdatedPasscodeLock(lock) + } + }) + + _ = (activeAccounts |> deliverOnMainQueue).start(next: { accounts in + for account in accounts.accounts { + self.startNotifyListener(with: account.1, primary: account.0 == accounts.primary?.id) + } + self.activeAccounts = accounts + }) + + + let passlock = Signal.single(Void()) |> delay(10, queue: Queue.concurrentDefaultQueue()) |> restart |> mapToSignal { () -> Signal in + return accountManager.transaction { transaction -> Int32? in + if transaction.getAccessChallengeData().isLockable { + return passcodeSettings(transaction).timeout + } else { + return nil + } + } + } |> map { [weak self] timeout -> Bool in + if let timeout = timeout { + if let resignTimestamp = self?.resignTimestamp { + let current = Int32(Date().timeIntervalSince1970) + if current - resignTimestamp > timeout { + return true + } + } + return Int64(timeout) < SystemIdleTime() + } else { + return false + } + } + |> filter { _ in + return !self._lockedValue.passcodeLock + } + |> deliverOnMainQueue + + window.set(handler: { _ -> KeyHandlerResult in + + if !self._lockedValue.passcodeLock { + self._passlock.set(accountManager.transaction { transaction -> Bool in + switch transaction.getAccessChallengeData() { + case .none: + return false + default: + return true + } + }) + } + + return .invoked + }, with: self, for: .L, priority: .modal, modifierFlags: [.command]) + + _passlock.set(passlock) + + } + + + func logout() -> Signal { + let accountManager = self.accountManager + let signal = combineLatest(self.activeAccounts.accounts.map { logoutFromAccount(id: $0.0, accountManager: self.accountManager, alreadyLoggedOutRemotely: false) }) |> deliverOnMainQueue + appEncryption.remove() + let removePasscode = accountManager.transaction { $0.setAccessChallengeData(.none) } |> deliverOnMainQueue + return combineLatest(removePasscode, signal) |> ignoreValues + } + + + @objc public func windowDidBecomeKey() { + self.resignTimestamp = nil + } + + @objc public func windowDidResignKey() { + self.resignTimestamp = Int32(Date().timeIntervalSince1970) + } + + @objc func screenIsLocked() { + + if !_lockedValue.passcodeLock { + _passlock.set(accountManager.transaction { transaction -> Bool in + switch transaction.getAccessChallengeData() { + case .none: + return false + default: + return true + } + }) + } + + updateLocked { (previous) -> LockNotificationsData in + return previous.withUpdatedScreenLock(true) + } + } + + @objc func screenIsUnlocked() { + updateLocked { (previous) -> LockNotificationsData in + return previous.withUpdatedScreenLock(false) + } + } + + + var isLocked: Bool { + return _lockedValue.isLocked + } + + private(set) var snoofEnabled: Bool = true + private(set) var requestUserAttention: Bool = false + + func startNotifyListener(with account: Account, primary: Bool) { + let screenLocked = self.screenLocked + var alreadyNotified:Set = Set() + + disposableDict.set((account.stateManager.notificationMessages |> mapToSignal { messages -> Signal<([([Message], PeerGroupId)], InAppNotificationSettings), NoError> in + return appNotificationSettings(accountManager: self.accountManager) |> take(1) |> mapToSignal { inAppSettings -> Signal<([([Message], PeerGroupId)], InAppNotificationSettings), NoError> in + self.snoofEnabled = inAppSettings.showNotificationsOutOfFocus + self.requestUserAttention = inAppSettings.requestUserAttention + if inAppSettings.enabled && inAppSettings.muteUntil < Int32(Date().timeIntervalSince1970) { + + return .single((messages.filter({$0.2 || ($0.0.isEmpty || $0.0[0].wasScheduled)}).map {($0.0, $0.1)}, inAppSettings)) + } else { + return .complete() + } + + } + } + |> mapToSignal { messages, inAppSettings -> Signal<([([Message], PeerGroupId)],[MessageId:NSImage], InAppNotificationSettings), NoError> in + + var photos:[Signal<(MessageId, CGImage?),NoError>] = [] + for message in messages.reduce([], { current, value in return current + value.0}) { + var peer = message.author + if let mainPeer = messageMainPeer(message) { + if mainPeer is TelegramChannel || mainPeer is TelegramGroup || message.wasScheduled { + peer = mainPeer + } + } + if message.id.peerId == repliesPeerId { + peer = message.chatPeer(account.peerId) + } + if let peer = peer { + photos.append(peerAvatarImage(account: account, photo: .peer(peer, peer.smallProfileImage, peer.displayLetters, message), genCap: false) |> map { data in return (message.id, data.0)}) + } + } + + return combineLatest(photos) |> map { resources in + var images:[MessageId:NSImage] = [:] + for (messageId,image) in resources { + if let image = image { + images[messageId] = NSImage(cgImage: image, size: NSMakeSize(50,50)) + } + } + return (messages, images, inAppSettings) + } + } |> mapToSignal { messages, images, inAppSettings -> Signal<([([Message], PeerGroupId)],[MessageId:NSImage], InAppNotificationSettings, Bool), NoError> in + return screenLocked.get() + |> take(1) + |> map { data in return (messages, images, inAppSettings, data.isLocked)} + } + |> mapToSignal { values in + return account.postbox.loadedPeerWithId(account.peerId) |> map { peer in + return (values.0, values.1, values.2, values.3, peer) + } + } |> deliverOnMainQueue).start(next: { messages, images, inAppSettings, screenIsLocked, accountPeer in + + if !primary, !inAppSettings.notifyAllAccounts { + return + } + + for (messages, groupId) in messages { + for message in messages { + + if alreadyNotified.contains(message.id) { + continue + } + + if message.isImported { + continue + } + + if message.author?.id != account.peerId || message.wasScheduled { + var title:String = message.author?.displayTitle ?? "" + var hasReplyButton:Bool = !screenIsLocked + if let peer = message.peers[message.id.peerId] { + if peer.isSupergroup || peer.isGroup { + title = peer.displayTitle + hasReplyButton = peer.canSendMessage(false) + } else if message.id.peerId == repliesPeerId { + if let peerId = message.sourceReference?.messageId.peerId, let sourcePeer = message.peers[peerId] { + hasReplyButton = sourcePeer.canSendMessage(true) + } + } else if peer.isChannel { + hasReplyButton = false + } + } + + if message.wasScheduled { + hasReplyButton = false + } + + if screenIsLocked { + title = appName + } + + + + + var text = chatListText(account: account, for: message, applyUserName: true).string.nsstring + var subText:String? + if text.contains("\n") { + let parts = text.components(separatedBy: "\n") + text = parts[1] as NSString + subText = parts[0] + } + + if message.wasScheduled { + if message.id.peerId == account.peerId { + title = L10n.notificationReminder + } else { + title = "📆 \(title)" + } + subText = nil + } + if message.id.peerId == repliesPeerId { + subText = message.chatPeer(account.peerId)?.displayTitle + } + + + if !inAppSettings.displayPreviews || message.peers[message.id.peerId] is TelegramSecretChat || screenIsLocked { + text = L10n.notificationLockedPreview.nsstring + subText = nil + } + + let notification = NSUserNotification() + + notification.identifier = "msg_\(message.id.toInt64())" + + if #available(macOS 10.14, *) { + switch inAppSettings.tone { + case .none: + notification.soundName = nil + default: + notification.soundName = fileNameForNotificationSound(inAppSettings.tone, defaultSound: nil) + } + } else { + switch inAppSettings.tone { + case .none: + notification.soundName = nil + default: + break + } + } + + if message.muted { + notification.soundName = nil + title += " 🔕" + } + if screenIsLocked { + notification.soundName = nil + } + + if self.activeAccounts.accounts.count > 1 && !screenIsLocked { + title += " → \(accountPeer.addressName ?? accountPeer.displayTitle)" + } + + notification.title = title + notification.informativeText = text as String + notification.subtitle = subText + notification.contentImage = screenIsLocked ? nil : images[message.id] + notification.hasReplyButton = hasReplyButton + + notification.hasActionButton = !message.wasScheduled + notification.otherButtonTitle = L10n.notificationMarkAsRead + + var dict: [String : Any] = [:] + + if message.wasScheduled { + dict["wasScheduled"] = true + } + + if let sourceReference = message.sourceReference, let threadId = message.replyAttribute?.threadMessageId, message.id.peerId == repliesPeerId { + dict["source.message.id"] = sourceReference.messageId.id + dict["source.message.namespace"] = sourceReference.messageId.namespace + dict["source.peer.id"] = sourceReference.messageId.peerId.id._internalGetInt64Value() + dict["source.peer.namespace"] = sourceReference.messageId.peerId.namespace._internalGetInt32Value() + + dict["thread.message.id"] = threadId.id + dict["thread.message.namespace"] = threadId.namespace + dict["thread.peer.id"] = threadId.peerId.id._internalGetInt64Value() + dict["thread.peer.namespace"] = threadId.peerId.namespace._internalGetInt32Value() + } + + dict["reply.message.id"] = message.id.id + dict["reply.message.namespace"] = message.id.namespace + dict["reply.peer.id"] = message.id.peerId.id._internalGetInt64Value() + dict["reply.peer.namespace"] = message.id.peerId.namespace._internalGetInt32Value() + + dict["groupId"] = groupId.rawValue + + dict["accountId"] = account.id.int64 + dict["timestamp"] = Int32(Date().timeIntervalSince1970) + + alreadyNotified.insert(message.id) + + notification.userInfo = dict +// NSUserNotificationCenter.default.deliver(notification) + + if self.shouldPresent(dict) { + _ = UNUserNotifications.authorizationStatus.start(next: { status in + switch status { + case .authorized: + UNUserNotifications.current?.add(notification) + default: + break + } + }) + } + } + } + } + }), forKey: account.id) + } + + + private func shouldPresent(_ userInfo:[AnyHashable : Any]?) -> Bool { + guard let id = userInfo?["accountId"] as? Int64 else { + return false + } + let accountId = AccountRecordId(rawValue: id) + + if accountId != self.activeAccounts.primary?.id { + return true + } + + let wasScheduled = userInfo?["wasScheduled"] as? Bool ?? false + + let result = !snoofEnabled || !window.isKeyWindow || wasScheduled + + return result + } + + +} diff --git a/Telegram-Mac/SharedWakeupManager.swift b/Telegram-Mac/SharedWakeupManager.swift new file mode 100644 index 0000000000..3369ad9ee8 --- /dev/null +++ b/Telegram-Mac/SharedWakeupManager.swift @@ -0,0 +1,155 @@ +// +// SharedWakeupManager.swift +// Telegram +// +// Created by Mikhail Filimonov on 01/03/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import SwiftSignalKit +import Postbox +import TelegramCore + + + +private struct AccountTasks { + let stateSynchronization: Bool + let importantTasks: AccountRunningImportantTasks + let backgroundDownloads: Bool + let backgroundAudio: Bool + let activeCalls: Bool + let userInterfaceInUse: Bool + + var isEmpty: Bool { + if self.stateSynchronization { + return false + } + if !self.importantTasks.isEmpty { + return false + } + if self.backgroundDownloads { + return false + } + if self.backgroundAudio { + return false + } + if self.activeCalls { + return false + } + if self.userInterfaceInUse { + return false + } + return true + } +} + + + +class SharedWakeupManager { + private var accountsAndTasks: [(Account, Bool, AccountTasks)] = [] + private let sharedContext: SharedAccountContext + + private var stateManagmentReseted: Set = Set() + private var ringingStatesActivated: Set = Set() + private var inForeground: Bool = false + + private(set) var isSleeping: Bool = false { + didSet { + onSleepValueUpdated?(isSleeping) + } + } + + var onSleepValueUpdated:((Bool)->Void)? + + init(sharedContext: SharedAccountContext, inForeground: Signal) { + self.sharedContext = sharedContext + + NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(receiveWakeNote(_:)), name: NSWorkspace.screensDidWakeNotification, object: nil) + NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(receiveWakeNote(_:)), name: NSWorkspace.screensDidSleepNotification, object: nil) + + + _ = (inForeground |> deliverOnMainQueue).start(next: { value in + self.inForeground = value + self.checkTasks() + }) + + + let signal = (sharedContext.activeAccounts |> map { ($0.0, $0.1.map { ($0.0, $0.1) }) } |> mapToSignal { primary, accounts -> Signal<[(Account, Bool, AccountTasks)], NoError> in + + let result: [Signal<(Account, Bool, AccountTasks), NoError>] = accounts.map { (_, account) -> Signal<(Account, Bool, AccountTasks), NoError> in + return account.importantTasksRunning |> map { importantTasks in + return (account, primary?.id == account.id, AccountTasks(stateSynchronization: false, importantTasks: importantTasks, backgroundDownloads: false, backgroundAudio: false, activeCalls: false, userInterfaceInUse: false)) + } + } + + return combineLatest(result) + + } |> deliverOnMainQueue) + + + _ = signal.start(next: { accountsAndTasks in + self.accountsAndTasks = accountsAndTasks + self.updateRindingsStatuses(self.accountsAndTasks.map( { $0.0 } )) + self.checkTasks() + }) + } + + private func checkTasks() { + updateAccounts() + } + + @objc func receiveSleepNote(_ notification: Notification) { + self.isSleeping = true + } + + @objc func receiveWakeNote(_ notificaiton:Notification) { + for (account, _, _) in self.accountsAndTasks { + account.shouldBeServiceTaskMaster.set(.single(.never) |> then(.single(.always))) + } + self.isSleeping = false + } + + private func updateRindingsStatuses(_ accounts:[Account]) { + + self.ringingStatesActivated = ringingStatesActivated.intersection(accounts.map { $0.id }) + + for account in accounts { + if !ringingStatesActivated.contains(account.id) { + + let combine = combineLatest(queue: .mainQueue(), account.stateManager.isUpdating, account.callSessionManager.ringingStates()) + + _ = combine.start(next: { isUpdating, states in + if isUpdating { + return + } + if let state = states.first { + if self.sharedContext.hasActiveCall { + account.callSessionManager.drop(internalId: state.id, reason: .busy, debugLog: .single(nil)) + } else { + showCallWindow(PCallSession(account: account, sharedContext: self.sharedContext, isOutgoing: false, peerId: state.peerId, id: state.id, initialState: nil, startWithVideo: state.isVideo, isVideoPossible: state.isVideoPossible)) + } + } + }) + ringingStatesActivated.insert(account.id) + } + + } + + } + + private func updateAccounts() { + for (account, primary, tasks) in self.accountsAndTasks { + account.shouldBeServiceTaskMaster.set(.single(.always)) + account.shouldExplicitelyKeepWorkerConnections.set(.single(tasks.backgroundAudio)) + account.shouldKeepOnlinePresence.set(.single(primary && self.inForeground)) + account.shouldKeepBackgroundDownloadConnections.set(.single(tasks.backgroundDownloads)) + + if !stateManagmentReseted.contains(account.id) { + account.resetStateManagement() + stateManagmentReseted.insert(account.id) + } + } + } + +} diff --git a/Telegram-Mac/ShortPeerRowItem.swift b/Telegram-Mac/ShortPeerRowItem.swift index 56811cae55..b175716a22 100644 --- a/Telegram-Mac/ShortPeerRowItem.swift +++ b/Telegram-Mac/ShortPeerRowItem.swift @@ -8,20 +8,35 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit final class SelectPeerPresentation : Equatable { + + struct Comment : Equatable { + let string: String + let range: NSRange + } + let selected:Set let peers:[PeerId: Peer] + let limit:Int32 + let inputQueryResult: ChatPresentationInputQueryResult? + let comment: Comment + private let someFlagsAsNotice: Bool static func ==(lhs:SelectPeerPresentation, rhs:SelectPeerPresentation) -> Bool { - return lhs.selected == rhs.selected + return lhs.selected == rhs.selected && lhs.limit == rhs.limit && lhs.someFlagsAsNotice == rhs.someFlagsAsNotice && lhs.inputQueryResult == rhs.inputQueryResult && lhs.comment == rhs.comment } - init(_ selected:Set = Set(), peers:[PeerId: Peer] = [:]) { + init(_ selected:Set = Set(), peers:[PeerId: Peer] = [:], limit: Int32 = 0, someFlagsAsNotice:Bool = false, inputQueryResult: ChatPresentationInputQueryResult? = nil, comment: Comment = Comment(string: "", range: NSMakeRange(0, 0))) { self.selected = selected self.peers = peers + self.limit = limit + self.someFlagsAsNotice = someFlagsAsNotice + self.inputQueryResult = inputQueryResult + self.comment = comment } func deselect(peerId:PeerId) -> SelectPeerPresentation { @@ -30,10 +45,27 @@ final class SelectPeerPresentation : Equatable { selectedIds.formUnion(selected) let _ = selectedIds.remove(peerId) peers.removeValue(forKey: peerId) - return SelectPeerPresentation(selectedIds, peers: peers) + return SelectPeerPresentation(selectedIds, peers: peers, limit: limit, someFlagsAsNotice: someFlagsAsNotice, inputQueryResult: inputQueryResult, comment: comment) + } + + var isLimitReached: Bool { + return limit > 0 && limit == selected.count + } + + func withUpdateLimit(_ limit: Int32) -> SelectPeerPresentation { + return SelectPeerPresentation(selected, peers: peers, limit: limit, someFlagsAsNotice: someFlagsAsNotice, inputQueryResult: inputQueryResult, comment: comment) + } + + func updatedInputQueryResult(_ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> SelectPeerPresentation { + return SelectPeerPresentation(selected, peers: peers, limit: limit, someFlagsAsNotice: someFlagsAsNotice, inputQueryResult: f(inputQueryResult), comment: comment) + } + + func withUpdatedComment(_ comment: Comment) -> SelectPeerPresentation { + return SelectPeerPresentation(selected, peers: peers, limit: limit, someFlagsAsNotice: someFlagsAsNotice, inputQueryResult: inputQueryResult, comment: comment) } func withToggledSelected(_ peerId: PeerId, peer:Peer) -> SelectPeerPresentation { + var someFlagsAsNotice: Bool = self.someFlagsAsNotice var selectedIds:Set = Set() var peers:[PeerId: Peer] = self.peers selectedIds.formUnion(selected) @@ -41,23 +73,31 @@ final class SelectPeerPresentation : Equatable { let _ = selectedIds.remove(peerId) peers.removeValue(forKey: peerId) } else { - selectedIds.insert(peerId) - peers[peerId] = peer + if limit == 0 || selected.count < limit { + selectedIds.insert(peerId) + peers[peerId] = peer + } else { + someFlagsAsNotice = !someFlagsAsNotice + } } - return SelectPeerPresentation(selectedIds, peers: peers) + return SelectPeerPresentation(selectedIds, peers: peers, limit: limit, someFlagsAsNotice: someFlagsAsNotice, inputQueryResult: inputQueryResult, comment: comment) } } final class SelectPeerInteraction : InterfaceObserver { private(set) var presentation:SelectPeerPresentation = SelectPeerPresentation() - + var close: ()->Void = {} + var action:(PeerId)->Void = {_ in} + var singleUpdater:((SelectPeerPresentation)->Void)? = nil func update(animated:Bool = true, _ f:(SelectPeerPresentation)->SelectPeerPresentation)->Void { let oldValue = self.presentation presentation = f(presentation) + if oldValue != presentation { notifyObservers(value: presentation, oldValue:oldValue, animated:animated) } + self.singleUpdater?(presentation) } } @@ -89,9 +129,14 @@ class ShortPeerRowItem: GeneralRowItem { let interactionType:ShortPeerItemInteractionType let drawSeparatorIgnoringInset:Bool var textInset:CGFloat { - return inset.left + photoSize.width + 10.0 + (leftImage != nil ? leftImage!.backingSize.width + 5 : 0) + switch viewType { + case .legacy: + return inset.left + photoSize.width + 10.0 + (leftImage != nil ? leftImage!.backingSize.width + 5 : 0) + case let .modern(_, insets): + return photoSize.width + min(10, insets.left) + (leftImage != nil ? leftImage!.backingSize.width + 5 : 0) + } } - + let badgeNode: GlobalBadgeNode? let deleteInset:CGFloat let photoSize:NSSize @@ -100,52 +145,82 @@ class ShortPeerRowItem: GeneralRowItem { private var titleNode:TextNode = TextNode() private var statusNode:TextNode = TextNode() - private var title:(TextNodeLayout, TextNode)? - private var status:(TextNodeLayout, TextNode)? + private(set) var title:(TextNodeLayout, TextNode)? + private(set) var status:(TextNodeLayout, TextNode)? - private var titleSelected:(TextNodeLayout, TextNode)? - private var statusSelected:(TextNodeLayout, TextNode)? + private(set) var titleSelected:(TextNodeLayout, TextNode)? + private(set) var statusSelected:(TextNodeLayout, TextNode)? let leftImage:CGImage? - private(set) var photo:Signal? + private(set) var photo:Signal<(CGImage?, Bool), NoError>? - + let isLookSavedMessage: Bool let titleStyle:ControlStyle let statusStyle:ControlStyle private var titleAttr:NSAttributedString? private var statusAttr:NSAttributedString? - + let inputActivity: PeerInputActivity? let drawLastSeparator:Bool - - init(_ initialSize:NSSize, peer: Peer, account:Account, stableId:AnyHashable? = nil, enabled: Bool = true, height:CGFloat = 50, photoSize:NSSize = NSMakeSize(36, 36), titleStyle:ControlStyle = ControlStyle(font: .medium(.title), foregroundColor: theme.colors.text, highlightColor: .white), titleAddition:String? = nil, leftImage:CGImage? = nil, statusStyle:ControlStyle = ControlStyle(font:.normal(.text), foregroundColor: theme.colors.grayText, highlightColor:.white), status:String? = nil, borderType:BorderType = [], drawCustomSeparator:Bool = true, deleteInset:CGFloat? = nil, drawLastSeparator:Bool = false, inset:NSEdgeInsets = NSEdgeInsets(left:10.0), drawSeparatorIgnoringInset: Bool = false, interactionType:ShortPeerItemInteractionType = .plain, generalType:GeneralInteractedType = .none, action:@escaping ()->Void = {}) { + let highlightVerified: Bool + let highlightOnHover: Bool + let alwaysHighlight: Bool + private let contextMenuItems:()->Signal<[ContextMenuItem], NoError> + fileprivate let _peerId: PeerId? + init(_ initialSize:NSSize, peer: Peer, account:Account, peerId: PeerId? = nil, stableId:AnyHashable? = nil, enabled: Bool = true, height:CGFloat = 50, photoSize:NSSize = NSMakeSize(36, 36), titleStyle:ControlStyle = ControlStyle(font: .medium(.title), foregroundColor: theme.colors.text, highlightColor: .white), titleAddition:String? = nil, leftImage:CGImage? = nil, statusStyle:ControlStyle = ControlStyle(font:.normal(.text), foregroundColor: theme.colors.grayText, highlightColor:.white), status:String? = nil, borderType:BorderType = [], drawCustomSeparator:Bool = true, isLookSavedMessage: Bool = false, deleteInset:CGFloat? = nil, drawLastSeparator:Bool = false, inset:NSEdgeInsets = NSEdgeInsets(left:10.0), drawSeparatorIgnoringInset: Bool = false, interactionType:ShortPeerItemInteractionType = .plain, generalType:GeneralInteractedType = .none, viewType: GeneralViewType = .legacy, action:@escaping ()->Void = {}, contextMenuItems:@escaping()->Signal<[ContextMenuItem], NoError> = { .single([]) }, inputActivity: PeerInputActivity? = nil, highlightOnHover: Bool = false, alwaysHighlight: Bool = false, badgeNode: GlobalBadgeNode? = nil, compactText: Bool = false, highlightVerified: Bool = false, customTheme: GeneralRowItem.Theme? = nil) { self.peer = peer + self.contextMenuItems = contextMenuItems self.account = account + self._peerId = peerId self.photoSize = photoSize self.leftImage = leftImage + self.inputActivity = inputActivity if let deleteInset = deleteInset { self.deleteInset = deleteInset } else { - self.deleteInset = inset.left + switch viewType { + case .legacy: + self.deleteInset = inset.left + case let .modern(_, insets): + self.deleteInset = insets.left + } } + + self.badgeNode = badgeNode + self.badgeNode?.customLayout = true + self.alwaysHighlight = alwaysHighlight + self.highlightOnHover = highlightOnHover self.interactionType = interactionType self.drawLastSeparator = drawLastSeparator self.drawSeparatorIgnoringInset = drawSeparatorIgnoringInset self.titleStyle = titleStyle self.statusStyle = statusStyle + self.isLookSavedMessage = isLookSavedMessage + self.highlightVerified = highlightVerified - photo = peerAvatarImage(account: account, peer: peer, displayDimensions: photoSize) - let tAttr:NSMutableAttributedString = NSMutableAttributedString() - let _ = tAttr.append(string: peer.displayTitle, color: enabled ? titleStyle.foregroundColor : theme.colors.grayText, font: self.titleStyle.font) + if isLookSavedMessage && account.peerId == peer.id { + let icon = theme.icons.searchSaved + photo = generateEmptyPhoto(photoSize, type: .icon(colors: theme.colors.peerColors(5), icon: icon, iconSize: icon.backingSize.aspectFitted(NSMakeSize(photoSize.width - 15, photoSize.height - 15)), cornerRadius: nil)) |> map {($0, false)} + } else if isLookSavedMessage && peer.id == repliesPeerId { + let icon = theme.icons.chat_replies_avatar + photo = generateEmptyPhoto(photoSize, type: .icon(colors: theme.colors.peerColors(5), icon: icon, iconSize: icon.backingSize.aspectFitted(NSMakeSize(photoSize.width - 17, photoSize.height - 17)), cornerRadius: nil)) |> map {($0, false)} + + } + + if let emptyAvatar = peer.emptyAvatar { + self.photo = generateEmptyPhoto(photoSize, type: emptyAvatar) |> map {($0, false)} + } + + let _ = tAttr.append(string: isLookSavedMessage && account.peerId == peer.id ? L10n.peerSavedMessages : (compactText ? peer.compactDisplayTitle + (account.testingEnvironment ? " [🤖]" : "") : peer.displayTitle), color: enabled ? titleStyle.foregroundColor : customTheme?.grayTextColor ?? theme.colors.grayText, font: self.titleStyle.font) if let titleAddition = titleAddition { - _ = tAttr.append(string: titleAddition, color: enabled ? titleStyle.foregroundColor : theme.colors.grayText, font: self.titleStyle.font) + _ = tAttr.append(string: titleAddition, color: enabled ? titleStyle.foregroundColor : customTheme?.grayTextColor ?? theme.colors.grayText, font: self.titleStyle.font) } - tAttr.addAttribute(.selectedColor, value: NSColor.white, range: tAttr.range) + tAttr.addAttribute(.selectedColor, value: customTheme?.underSelectedColor ?? theme.colors.underSelectedColor, range: tAttr.range) titleAttr = tAttr.copy() as? NSAttributedString @@ -153,18 +228,27 @@ class ShortPeerRowItem: GeneralRowItem { if let status = status { let sAttr:NSMutableAttributedString = NSMutableAttributedString() - let _ = sAttr.append(string: status, color: enabled ? self.statusStyle.foregroundColor : theme.colors.grayText, font: self.statusStyle.font, coreText: true) - sAttr.addAttribute(.selectedColor, value: NSColor.white, range: sAttr.range) + let _ = sAttr.append(string: status, color: enabled ? self.statusStyle.foregroundColor : customTheme?.grayTextColor ?? theme.colors.grayText, font: self.statusStyle.font, coreText: true) + sAttr.addAttribute(.selectedColor, value: customTheme?.underSelectedColor ?? theme.colors.underSelectedColor, range: sAttr.range) statusAttr = sAttr.copy() as? NSAttributedString } - super.init(initialSize, height: height, stableId: stableId ?? AnyHashable(peer.id), type:generalType, action:action, drawCustomSeparator:drawCustomSeparator, border:borderType,inset:inset, enabled: enabled) + super.init(initialSize, height: height, stableId: stableId ?? AnyHashable(peerId ?? peer.id), type:generalType, viewType: viewType, action:action, drawCustomSeparator:drawCustomSeparator, border:borderType,inset:inset, enabled: enabled, customTheme: customTheme) } + var peerId: PeerId { + return _peerId ?? peer.id + } + + override func menuItems(in location: NSPoint) -> Signal<[ContextMenuItem], NoError> { + return contextMenuItems() + } + override func makeSize(_ width: CGFloat, oldWidth:CGFloat) -> Bool { + let result = super.makeSize(width, oldWidth: oldWidth) prepare(self.isSelected) - return super.makeSize(width, oldWidth: oldWidth) + return result } var textAdditionInset:CGFloat { @@ -179,7 +263,7 @@ class ShortPeerRowItem: GeneralRowItem { case .selectable(_): addition += 30 case .deletable: - addition += 30 + addition += 24 + 12 default: break } @@ -189,20 +273,49 @@ class ShortPeerRowItem: GeneralRowItem { addition += 48 case .selectable: addition += 20 - case .context: - addition += 48 + case let .context(text): + let attr = NSAttributedString.initialize(string: text, color: .text, font: statusStyle.font) + addition += attr.size().width + 10 + case let .nextContext(text): + let attr = NSAttributedString.initialize(string: text, color: .text, font: statusStyle.font) + addition += attr.size().width + 10 default: break } - if let titleAttr = titleAttr { - title = TextNode.layoutText(maybeNode: nil, titleAttr, nil, 1, .end, NSMakeSize(self.size.width - textInset - (inset.right == 0 ? 10 : inset.right) - addition - textAdditionInset, 20), nil,false, .left) - titleSelected = TextNode.layoutText(maybeNode: nil, titleAttr, nil, 1, .end, NSMakeSize(self.size.width - textInset - inset.right - addition - textAdditionInset, 20), nil,true, .left) + if let _ = badgeNode { + addition += 40 } - if let statusAttr = statusAttr { - status = TextNode.layoutText(maybeNode: nil, statusAttr, nil, 1, .end, NSMakeSize(self.size.width - textInset - (inset.right == 0 ? 10 : inset.right) - addition - textAdditionInset, 20), nil,false, .left) - statusSelected = TextNode.layoutText(maybeNode: nil, statusAttr, nil, 1, .end, NSMakeSize(self.size.width - textInset - inset.right - addition - textAdditionInset, 20), nil,true, .left) + + if self.peer.isScam { + addition += 20 + } + if self.peer.isFake { + addition += 20 } + switch viewType { + case .legacy: + if let titleAttr = titleAttr { + title = TextNode.layoutText(maybeNode: nil, titleAttr, nil, 1, .end, NSMakeSize(self.size.width - textInset - (inset.right) - addition - textAdditionInset, 20), nil,false, .left) + titleSelected = TextNode.layoutText(maybeNode: nil, titleAttr, nil, 1, .end, NSMakeSize(self.size.width - textInset - (inset.right) - addition - textAdditionInset, 20), nil,true, .left) + } + if let statusAttr = statusAttr { + status = TextNode.layoutText(maybeNode: nil, statusAttr, nil, 1, .end, NSMakeSize(self.size.width - textInset - (inset.right) - addition - textAdditionInset, 20), nil,false, .left) + statusSelected = TextNode.layoutText(maybeNode: nil, statusAttr, nil, 1, .end, NSMakeSize(self.size.width - textInset - inset.right - addition - textAdditionInset, 20), nil,true, .left) + } + case let .modern(_, insets): + let textSize = NSMakeSize(self.width - textInset - insets.left - insets.right - inset.left - inset.right - addition - textAdditionInset, 20) + if let titleAttr = titleAttr { + title = TextNode.layoutText(maybeNode: nil, titleAttr, nil, 1, .end, textSize, nil, false, .left) + titleSelected = TextNode.layoutText(maybeNode: nil, titleAttr, nil, 1, .end, textSize, nil,true, .left) + } + if let statusAttr = statusAttr { + status = TextNode.layoutText(maybeNode: nil, statusAttr, nil, 1, .end, textSize, nil,false, .left) + statusSelected = TextNode.layoutText(maybeNode: nil, statusAttr, nil, 1, .end, textSize, nil,true, .left) + } + } + + } var ctxTitle:(TextNodeLayout, TextNode)? { @@ -214,4 +327,13 @@ class ShortPeerRowItem: GeneralRowItem { override func viewClass() -> AnyClass { return ShortPeerRowView.self } + + override var instantlyResize: Bool { + return true + } + + deinit { + var bp:Int = 0 + bp += 1 + } } diff --git a/Telegram-Mac/ShortPeerRowView.swift b/Telegram-Mac/ShortPeerRowView.swift index 751ef39327..0a058afc7c 100644 --- a/Telegram-Mac/ShortPeerRowView.swift +++ b/Telegram-Mac/ShortPeerRowView.swift @@ -8,29 +8,89 @@ import Cocoa import TGUIKit +import TelegramCore +import Postbox //FB2126 class ShortPeerRowView: TableRowView, Notifable, ViewDisplayDelegate { - + private let containerView: GeneralRowContainerView = GeneralRowContainerView(frame: NSZeroRect) private var image:AvatarControl = AvatarControl(font: .avatar(.text)) private var deleteControl:ImageButton? private var selectControl:SelectingControl? - private let container:View = View() + private let container:Control = Control() private var switchView:SwitchView? private var contextLabel:TextViewLabel? private var choiceControl:ImageView? + #if !SHARE + private var activities: ChatActivitiesModel? + #endif private let rightSeparatorView:View = View() + private let separator:View = View() + + private var hiddenStatus: Bool = true + private var badgeNode: View? = nil + required init(frame frameRect: NSRect) { super.init(frame: frameRect) container.frame = bounds container.addSubview(image) container.displayDelegate = self - addSubview(container) + containerView.addSubview(container) image.userInteractionEnabled = false - addSubview(rightSeparatorView) + containerView.addSubview(rightSeparatorView) + containerView.addSubview(separator) + + container.set(handler: { [weak self] _ in + self?.updateMouse() + }, for: .Hover) + + container.set(handler: { [weak self] _ in + self?.updateMouse() + }, for: .Normal) + + container.userInteractionEnabled = false + + addSubview(self.containerView) + + containerView.set(handler: { [weak self] _ in + self?.invokeIfNeededDown() + }, for: .Down) + + containerView.set(handler: { [weak self] _ in + self?.invokeIfNeededUp() + }, for: .Up) + + containerView.set(handler: { [weak self] _ in + self?.updateColors() + }, for: .Normal) + containerView.set(handler: { [weak self] _ in + self?.updateColors() + }, for: .Highlight) + } + + private func invokeIfNeededUp() { + if let event = NSApp.currentEvent { + super.mouseUp(with: event) + if let item = item as? ShortPeerRowItem, let table = item.table, table.alwaysOpenRowsOnMouseUp, mouseInside() { + if item.enabled { + invokeAction(item, clickCount: event.clickCount) + } + } + } + + } + private func invokeIfNeededDown() { + if let event = NSApp.currentEvent { + super.mouseDown(with: event) + if let item = item as? ShortPeerRowItem, let table = item.table, !table.alwaysOpenRowsOnMouseUp, let event = NSApp.currentEvent, mouseInside() { + if item.enabled { + invokeAction(item, clickCount: event.clickCount) + } + } + } } override var border: BorderType? { @@ -39,11 +99,32 @@ class ShortPeerRowView: TableRowView, Notifable, ViewDisplayDelegate { } } + + + private var isRowSelected: Bool { + if let item = item as? ShortPeerRowItem { + if item.highlightOnHover { + return self.mouseInside() || item.isSelected + } else if item.alwaysHighlight { + return false + } + } + return item?.isSelected ?? false + } + override var backdorColor: NSColor { - return item?.isSelected ?? false ? theme.colors.blueSelect : theme.colors.background + if let item = item as? ShortPeerRowItem, let theme = item.customTheme { + return item.isHighlighted || item.isSelected ? theme.highlightColor : theme.backgroundColor + } + if let item = item as? ShortPeerRowItem, item.alwaysHighlight { + return item.isSelected ? theme.colors.grayForeground : theme.colors.background + } + return isRowSelected ? theme.colors.accentSelect : item?.isHighlighted ?? false ? theme.colors.grayForeground : theme.colors.background } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -51,89 +132,208 @@ class ShortPeerRowView: TableRowView, Notifable, ViewDisplayDelegate { override func draw(_ layer: CALayer, in ctx: CGContext) { if let item = item as? ShortPeerRowItem { - - ctx.setFillColor(backdorColor.cgColor) - ctx.fill(NSMakeRect(0, 0, layer.bounds.width - .borderSize, layer.bounds.height)) if layer == container.layer { - - let canSeparate: Bool = item.index != item.table!.count - 1 - - - if !item.isSelected && item.drawCustomSeparator && (canSeparate || item.drawLastSeparator) { - ctx.setFillColor(theme.colors.border.cgColor) - ctx.fill(NSMakeRect(item.textInset, container.frame.height - .borderSize, container.frame.width - (item.drawSeparatorIgnoringInset ? 0 : item.inset.right), .borderSize)) - } - - if let leftImage = item.leftImage { - let focus = container.focus(leftImage.backingSize) - ctx.draw(leftImage, in: NSMakeRect(item.inset.left, focus.minY, focus.width, focus.height)) - } - - if let title = item.ctxTitle { - var tY = NSMinY(focus(title.0.size)) - - if let status = item.ctxStatus { - let t = title.0.size.height + status.0.size.height + 1.0 - tY = (NSHeight(self.frame) - t) / 2.0 + switch item.viewType { + case .legacy: + if backingScaleFactor == 1.0 { + ctx.setFillColor(backdorColor.cgColor) + ctx.fill(NSMakeRect(0, 0, layer.bounds.width - .borderSize, layer.bounds.height)) + } + if let leftImage = item.leftImage { + let focus = container.focus(leftImage.backingSize) + ctx.draw(leftImage, in: NSMakeRect(item.inset.left, focus.minY, focus.width, focus.height)) + } + if let title = (isRowSelected ? item.titleSelected : item.title) { + var tY = NSMinY(focus(title.0.size)) + + if let status = (isRowSelected ? item.statusSelected : item.status) { + let t = title.0.size.height + status.0.size.height + 1.0 + tY = floorToScreenPixels(backingScaleFactor, (self.frame.height - t) / 2.0) + + let sY = tY + title.0.size.height + 1.0 + if hiddenStatus { + status.1.draw(NSMakeRect(item.textInset, sY, status.0.size.width, status.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) + } + } + + title.1.draw(NSMakeRect(item.textInset, tY, title.0.size.width, title.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) - let sY = tY + title.0.size.height + 1.0 - status.1.draw(NSMakeRect(item.textInset, sY, status.0.size.width, status.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor) + if item.peer.isVerified && item.highlightVerified { + ctx.draw(isRowSelected ? theme.icons.verifyDialogActive : theme.icons.verifyDialog, in: NSMakeRect(item.textInset + title.0.size.width - 1, tY - 3, 24, 24)) + } else if item.peer.isScam && item.highlightVerified { + ctx.draw(isRowSelected ? theme.icons.scamActive : theme.icons.scam, in: NSMakeRect(item.textInset + title.0.size.width + 5, tY + 1, theme.icons.scam.backingSize.width, theme.icons.scam.backingSize.height)) + } else if item.peer.isFake && item.highlightVerified { + ctx.draw(isRowSelected ? theme.icons.fakeActive : theme.icons.fake, in: NSMakeRect(item.textInset + title.0.size.width + 5, tY + 1, theme.icons.fake.backingSize.width, theme.icons.fake.backingSize.height)) + } + } + case .modern: + if backingScaleFactor == 1.0 { + ctx.setFillColor(backdorColor.cgColor) + ctx.fill(NSMakeRect(0, 0, layer.bounds.width - .borderSize, layer.bounds.height)) + } + if let leftImage = item.leftImage { + let focus = container.focus(leftImage.backingSize) + ctx.draw(leftImage, in: NSMakeRect(0, focus.minY, focus.width, focus.height)) + } + if let title = (isRowSelected ? item.titleSelected : item.title) { + var tY = NSMinY(focus(title.0.size)) + + if let status = (isRowSelected ? item.statusSelected : item.status) { + let t = title.0.size.height + status.0.size.height + 1.0 + tY = (NSHeight(self.frame) - t) / 2.0 + + let sY = tY + title.0.size.height + 1.0 + if hiddenStatus { + status.1.draw(NSMakeRect(item.textInset, sY, status.0.size.width, status.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) + } + } + + title.1.draw(NSMakeRect(item.textInset, tY, title.0.size.width, title.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) + + if item.peer.isVerified && item.highlightVerified { + ctx.draw(isRowSelected ? theme.icons.verifyDialogActive : theme.icons.verifyDialog, in: NSMakeRect(item.textInset + title.0.size.width - 1, tY - 3, 24, 24)) + } + if item.peer.isScam && item.highlightVerified { + ctx.draw(isRowSelected ? theme.icons.scamActive : theme.icons.scam, in: NSMakeRect(item.textInset + title.0.size.width + 5, tY + 1, theme.icons.scam.backingSize.width, theme.icons.scam.backingSize.height)) + } } - - title.1.draw(NSMakeRect(item.textInset, tY, title.0.size.width, title.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor) - } - } else { - super.draw(layer, in: ctx) - let canSeparate: Bool = item.index != item.table!.count - 1 - if !item.isSelected && item.drawCustomSeparator && (canSeparate || item.drawLastSeparator) { - ctx.setFillColor(theme.colors.border.cgColor) - ctx.fill(NSMakeRect(30, container.frame.height - .borderSize, frame.width, .borderSize)) } - } } } + override func updateColors() { + + guard let item = item as? ShortPeerRowItem else { + return + } + + let highlighted = backdorColor + + let customTheme = item.customTheme + + self.containerView.background = backdorColor + self.separator.backgroundColor = customTheme?.borderColor ?? theme.colors.border + self.contextLabel?.background = backdorColor + containerView.set(background: backdorColor, for: .Normal) + containerView.set(background: highlighted, for: .Highlight) + + + self.background = item.viewType.rowBackground + needsDisplay = true + } + override func updateMouse() { + super.updateMouse() + updateColors() + container.needsDisplay = true + guard let item = item as? ShortPeerRowItem else { + return + } + item.badgeNode?.isSelected = isRowSelected + } override func layout() { super.layout() if let item = item as? ShortPeerRowItem { - - if let border = border, border.contains(.Right) { - rightSeparatorView.isHidden = false - rightSeparatorView.frame = NSMakeRect(frame.width - .borderSize, 0, .borderSize, frame.height) - } else { - rightSeparatorView.isHidden = true - } - - switch item.interactionType { - case .plain: - container.frame = bounds - case .selectable: - container.frame = .init(x: 0, y: 0, width: frame.width, height: frame.height) - default : - container.frame = .init(x: 30, y: 0, width: frame.width - 30, height: frame.height) - } - - if let deleteControl = deleteControl { - deleteControl.centerY(x: item.deleteInset) - } - if let selectControl = selectControl { - selectControl.centerY(x: frame.width - selectControl.frame.width - item.inset.right) - } - image.frame = NSMakeRect(item.inset.left + (item.leftImage != nil ? item.leftImage!.backingSize.width + 5 : 0), NSMinY(focus(item.photoSize)), item.photoSize.width, item.photoSize.height) - if let switchView = switchView { - switchView.centerY(x:container.frame.width - switchView.frame.width - item.inset.right) - } - if let contextLabel = contextLabel { - contextLabel.centerY(x:frame.width - contextLabel.frame.width - item.inset.right) - } - container.needsDisplay = true - - if let choiceControl = choiceControl { - choiceControl.centerY(x: frame.width - choiceControl.frame.width - item.inset.right) + switch item.viewType { + case .legacy: + self.containerView.frame = bounds + self.containerView.setCorners([]) + if let border = border, border.contains(.Right) { + rightSeparatorView.isHidden = false + rightSeparatorView.frame = NSMakeRect(frame.width - .borderSize, 0, .borderSize, frame.height) + } else { + rightSeparatorView.isHidden = true + } + switch item.interactionType { + case .plain: + container.frame = bounds + case .selectable: + container.frame = .init(x: 0, y: 0, width: frame.width, height: frame.height) + default : + container.frame = .init(x: 30, y: 0, width: frame.width - 30, height: frame.height) + } + + if let deleteControl = deleteControl { + deleteControl.centerY(x: item.deleteInset) + } + if let selectControl = selectControl { + selectControl.centerY(x: frame.width - selectControl.frame.width - item.inset.right) + } + image.frame = NSMakeRect(item.inset.left + (item.leftImage != nil ? item.leftImage!.backingSize.width + 5 : 0), NSMinY(focus(item.photoSize)), item.photoSize.width, item.photoSize.height) + if let switchView = switchView { + switchView.centerY(x:container.frame.width - switchView.frame.width - item.inset.right) + } + if let contextLabel = contextLabel { + contextLabel.centerY(x:frame.width - contextLabel.frame.width - item.inset.right) + } + container.needsDisplay = true + + if let choiceControl = choiceControl { + choiceControl.centerY(x: frame.width - choiceControl.frame.width - item.inset.right) + } + + if let badgeNode = badgeNode, let itemNode = item.badgeNode { + badgeNode.setFrameSize(itemNode.size) + badgeNode.centerY(x: containerView.frame.width - badgeNode.frame.width - item.inset.left) + } + + separator.frame = NSMakeRect(item.textInset, containerView.frame.height - .borderSize, containerView.frame.width - (item.drawSeparatorIgnoringInset ? 0 : item.inset.right) - item.textInset, .borderSize) + + #if !SHARE + if let view = activities?.view { + view.setFrameOrigin(item.textInset - 2, floorToScreenPixels(backingScaleFactor, frame.height / 2 + 1)) + } + #endif + case let .modern(position, innerInsets): + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) + self.containerView.setCorners(position.corners) + self.rightSeparatorView.isHidden = true + + switch item.interactionType { + case .plain: + container.frame = .init(x: innerInsets.left, y: 0, width: containerView.frame.width - innerInsets.left - innerInsets.right, height: containerView.frame.height) + case .selectable: + container.frame = .init(x: innerInsets.left, y: 0, width: containerView.frame.width - innerInsets.left - innerInsets.right, height: containerView.frame.height) + case .deletable: + let offset = innerInsets.left + 24 + innerInsets.left + container.frame = .init(x: offset, y: 0, width: containerView.frame.width - offset - innerInsets.right, height: containerView.frame.height) + } + + if let deleteControl = deleteControl { + deleteControl.centerY(x: item.deleteInset) + } + if let selectControl = selectControl { + selectControl.centerY(x: containerView.frame.width - selectControl.frame.width - innerInsets.right) + } + image.frame = NSMakeRect((item.leftImage != nil ? item.leftImage!.backingSize.width + 5 : 0), NSMinY(focus(item.photoSize)), item.photoSize.width, item.photoSize.height) + if let switchView = switchView { + switchView.centerY(x: containerView.frame.width - switchView.frame.width - innerInsets.right) + } + if let contextLabel = contextLabel { + contextLabel.centerY(x: containerView.frame.width - contextLabel.frame.width - innerInsets.right) + } + + if let choiceControl = choiceControl { + choiceControl.centerY(x: containerView.frame.width - choiceControl.frame.width - innerInsets.right) + } + if let badgeNode = badgeNode, let itemNode = item.badgeNode { + badgeNode.setFrameSize(itemNode.size) + badgeNode.centerY(x: containerView.frame.width - badgeNode.frame.width - innerInsets.right) + } + + separator.frame = NSMakeRect(container.frame.minX + item.textInset, containerView.frame.height - .borderSize, container.frame.width - item.textInset, .borderSize) + + #if !SHARE + if let view = activities?.view { + view.setFrameOrigin(item.textInset - 2, floorToScreenPixels(backingScaleFactor, frame.height / 2 + 1)) + } + #endif + + container.needsDisplay = true + } } @@ -149,9 +349,52 @@ class ShortPeerRowView: TableRowView, Notifable, ViewDisplayDelegate { interactive = true } + let containerRect: NSRect + let separatorRect: NSRect + switch item.viewType { + case .legacy: + containerRect = CGRect(origin: NSMakePoint((interactive ? 30 : 0), 0), size: NSMakeSize(containerView.frame.width - (interactive ? 30 : 0), containerView.frame.height)) + separatorRect = NSMakeRect(item.textInset, containerRect.height - .borderSize, containerRect.width - (item.drawSeparatorIgnoringInset ? 0 : item.inset.right) - item.textInset, .borderSize) + case let .modern(_, innerInsets): + switch item.interactionType { + case .plain: + containerRect = .init(x: innerInsets.left, y: 0, width: containerView.frame.width - innerInsets.left - innerInsets.right, height: containerView.frame.height) + case .selectable: + containerRect = .init(x: innerInsets.left, y: 0, width: containerView.frame.width - innerInsets.left - innerInsets.right, height: containerView.frame.height) + case .deletable: + let offset = innerInsets.left + 24 + innerInsets.left + containerRect = .init(x: offset, y: 0, width: containerView.frame.width - offset - innerInsets.right, height: containerView.frame.height) + } + separatorRect = NSMakeRect(containerRect.minX + item.textInset, containerRect.height - .borderSize, containerRect.width - item.textInset, .borderSize) + + if let contextLabel = contextLabel { + var rect = containerView.focus(contextLabel.frame.size) + rect.origin.x = containerView.frame.width - contextLabel.frame.width - innerInsets.right + contextLabel.change(pos: rect.origin, animated: animated) + } + if let switchView = switchView { + var rect = containerView.focus(switchView.frame.size) + rect.origin.x = containerView.frame.width - switchView.frame.width - innerInsets.right + switchView.change(pos: rect.origin, animated: animated) + } + if let choiceControl = choiceControl { + var rect = containerView.focus(choiceControl.frame.size) + rect.origin.x = containerView.frame.width - choiceControl.frame.width - innerInsets.right + choiceControl.change(pos: rect.origin, animated: animated) + } + } + + + + self.separator.change(size: separatorRect.size, animated: animated) + self.separator.change(pos: separatorRect.origin, animated: animated) + + + self.container.change(size: containerRect.size, animated: false) + self.container.change(pos: containerRect.origin, animated: animated) - self.container.change(size: NSMakeSize(frame.width - (interactive ? 30 : 0), frame.height), animated: false) - self.container.change(pos: NSMakePoint((interactive ? 30 : 0), 0), animated: animated) + + switch interactionType { case .plain: @@ -179,11 +422,14 @@ class ShortPeerRowView: TableRowView, Notifable, ViewDisplayDelegate { case let .selectable(interaction): if selectControl == nil { - selectControl = SelectingControl(unselectedImage: theme.icons.chatToggleUnselected, selectedImage: theme.icons.chatToggleSelected) + let unselected: CGImage = item.customTheme?.unselectedImage ?? theme.icons.chatToggleUnselected + let selected: CGImage = item.customTheme?.selectedImage ?? theme.icons.chatToggleSelected + + selectControl = SelectingControl(unselectedImage: unselected, selectedImage: selected) } - selectControl?.set(selected: interaction.presentation.selected.contains(item.peer.id), animated: animated) + selectControl?.set(selected: interaction.presentation.selected.contains(item.peerId), animated: animated) - addSubview(selectControl!) + containerView.addSubview(selectControl!) deleteControl?.removeFromSuperview() deleteControl = nil @@ -195,13 +441,13 @@ class ShortPeerRowView: TableRowView, Notifable, ViewDisplayDelegate { deleteControl = ImageButton() deleteControl?.autohighlight = false deleteControl?.set(image: theme.icons.deleteItem, for: .Normal) - deleteControl?.sizeToFit() + _ = deleteControl?.sizeToFit() - addSubview(deleteControl!) + containerView.addSubview(deleteControl!) deleteControl?.layer?.opacity = 0 + deleteControl?.centerY(x: -theme.icons.deleteItem.backingSize.width) } - deleteControl?.centerY(x: -theme.icons.deleteItem.backingSize.width) if item.enabled { deleteControl?.set(image: theme.icons.deleteItem, for: .Normal) @@ -215,7 +461,7 @@ class ShortPeerRowView: TableRowView, Notifable, ViewDisplayDelegate { deleteControl?.removeAllHandlers() deleteControl?.set(handler: { [weak item] _ in if let item = item, item.enabled { - interaction.onRemove(item.peer.id) + interaction.onRemove(item.peerId) } }, for: .Click) @@ -226,11 +472,11 @@ class ShortPeerRowView: TableRowView, Notifable, ViewDisplayDelegate { override func set(item:TableRowItem, animated:Bool = false) { - var previousType:ShortPeerItemInteractionType = .plain + let previousType:ShortPeerItemInteractionType = self.item == nil ? .plain : (self.item as? ShortPeerRowItem)!.interactionType + + + guard let item = item as? ShortPeerRowItem else {return} - if let item = self.item as? ShortPeerRowItem { - previousType = item.interactionType - } switch previousType { case let .selectable(interaction): @@ -244,6 +490,50 @@ class ShortPeerRowView: TableRowView, Notifable, ViewDisplayDelegate { super.set(item: item, animated: animated) + containerView.setCorners(item.viewType.corners, animated: animated && item.viewType != .legacy) + + + self.border = item.border + + if let badge = item.badgeNode { + if badgeNode == nil { + badgeNode = View() + containerView.addSubview(badgeNode!) + } + badge.view = badgeNode + badge.view?.needsDisplay = true + badge.onUpdate = { [weak self] in + self?.needsLayout = true + } + } else { + self.badgeNode?.removeFromSuperview() + self.badgeNode = nil + } + + + #if !SHARE + if let activity = item.inputActivity { + if activities == nil { + activities = ChatActivitiesModel() + } + guard let activities = activities else {return} + + let inputActivites: (PeerId, [(Peer, PeerInputActivity)]) = (item.peerId, [(item.peer, activity)]) + + activities.update(with: inputActivites, for: max(frame.width - 60, 160), theme:theme.activity(key: 4, foregroundColor: item.customTheme?.accentColor ?? theme.colors.accent, backgroundColor: backdorColor), layout: { [weak self] show in + self?.needsLayout = true + self?.hiddenStatus = !show + self?.needsDisplay = true + self?.activities?.view?.isHidden = !show + }) + container.addSubview(activities.view!) + + } else { + hiddenStatus = true + activities?.view?.removeFromSuperview() + } + #endif + switch previousType { case let .selectable(interaction): interaction.add(observer: self) @@ -253,92 +543,96 @@ class ShortPeerRowView: TableRowView, Notifable, ViewDisplayDelegate { break } - if let item = item as? ShortPeerRowItem { - self.border = item.border - image.setFrameSize(item.photoSize) + switch item.viewType { + case .legacy: + let canSeparate: Bool = item.index != item.table!.count - 1 + separator.isHidden = !(!isRowSelected && item.drawCustomSeparator && (canSeparate || item.drawLastSeparator)) + case let .modern(position, _): + separator.isHidden = !position.border || !item.drawCustomSeparator + } + + image.setFrameSize(item.photoSize) + if let photo = item.photo { + image.setSignal(photo) + } else { image.setPeer(account: item.account, peer: item.peer) + } + + self.updateInteractionType(previousType, item.interactionType, item:item, animated:animated) + choiceControl?.removeFromSuperview() + choiceControl = nil + + switch item.type { + case let .switchable(stateback): + contextLabel?.removeFromSuperview() + contextLabel = nil + if switchView == nil { + switchView = SwitchView() + containerView.addSubview(switchView!) + } + switchView?.stateChanged = item.action + switchView?.setIsOn(stateback,animated:animated) + switchView?.isEnabled = item.enabled + case let .context(stateback:stateback): + switchView?.removeFromSuperview() + switchView = nil - self.updateInteractionType(previousType,item.interactionType, item:item, animated:animated) - choiceControl?.removeFromSuperview() - choiceControl = nil - - switch item.type { - case let .switchable(stateback: stateback): - contextLabel?.removeFromSuperview() - contextLabel = nil - if switchView == nil { - switchView = SwitchView() - container.addSubview(switchView!) - } - switchView?.stateChanged = item.action - switchView?.setIsOn(stateback(),animated:animated) - switchView?.isEnabled = item.enabled - case let .context(stateback:stateback): - switchView?.removeFromSuperview() - switchView = nil - - let label = stateback() - if !label.isEmpty { - if contextLabel == nil { - contextLabel = TextViewLabel() - addSubview(contextLabel!) - } - contextLabel?.attributedString = .initialize(string: label, color: theme.colors.grayText, font: item.statusStyle.font) - contextLabel?.sizeToFit() - } else { - contextLabel?.removeFromSuperview() - contextLabel = nil + let label = stateback + if !label.isEmpty { + if contextLabel == nil { + contextLabel = TextViewLabel() + containerView.addSubview(contextLabel!) } - case let .selectable(stateback: stateback): - if stateback() { - choiceControl = ImageView() - choiceControl?.image = theme.icons.generalSelect - choiceControl?.sizeToFit() - addSubview(choiceControl!) - } - - default: - switchView?.removeFromSuperview() - switchView = nil + contextLabel?.attributedString = .initialize(string: label, color: item.customTheme?.secondaryColor ?? theme.colors.grayText, font: item.statusStyle.font) + contextLabel?.sizeToFit() + } else { contextLabel?.removeFromSuperview() contextLabel = nil - break + } + case let .selectable(stateback: stateback): + if stateback { + choiceControl = ImageView() + choiceControl?.image = #imageLiteral(resourceName: "Icon_UsernameAvailability").precomposed(item.customTheme?.accentColor ?? theme.colors.accent) + choiceControl?.sizeToFit() + containerView.addSubview(choiceControl!) } - + default: + switchView?.removeFromSuperview() + switchView = nil + contextLabel?.removeFromSuperview() + contextLabel = nil + break } + self.image._change(opacity: item.enabled ? 1 : 0.8, animated: animated) rightSeparatorView.backgroundColor = theme.colors.border contextLabel?.backgroundColor = backdorColor needsLayout = true self.container.setNeedsDisplayLayer() } - override func mouseDown(with event: NSEvent) { - super.mouseDown(with:event) - - if let item = item as? ShortPeerRowItem { - if item.enabled { - switch item.interactionType { - case let .selectable(interaction): - interaction.update({$0.withToggledSelected(item.peer.id, peer: item.peer)}) - default: - if event.clickCount == 1 { - item.action() - self.focusAnimation() - } - } - } + func invokeAction(_ item: ShortPeerRowItem, clickCount: Int) { + switch item.interactionType { + case let .selectable(interaction): + interaction.update({$0.withToggledSelected(item.peerId, peer: item.peer)}) + default: + if clickCount <= 1 { + item.action() + // self.focusAnimation(nil) + } } } + + func notify(with value: Any, oldValue: Any, animated: Bool) { if let item = item as? ShortPeerRowItem { switch item.interactionType { case .selectable(_): if let value = value as? SelectPeerPresentation, let oldValue = oldValue as? SelectPeerPresentation { - let new = value.selected.contains(item.peer.id) - let old = oldValue.selected.contains(item.peer.id) + let new = value.selected.contains(item.peerId) + let old = oldValue.selected.contains(item.peerId) if new != old { selectControl?.set(selected: new, animated: animated) } diff --git a/Telegram-Mac/ShortcutListController.swift b/Telegram-Mac/ShortcutListController.swift new file mode 100644 index 0000000000..42cdcc0426 --- /dev/null +++ b/Telegram-Mac/ShortcutListController.swift @@ -0,0 +1,165 @@ +// +// ShortcutListController.swift +// Telegram +// +// Created by Mikhail Filimonov on 11.02.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit + +private func shortcutEntires() -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + // chat + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.shortcutsControllerChat), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: InputDataIdentifier("chat_open_info"), data: InputDataGeneralData(name: L10n.shortcutsControllerChatOpenInfo, color: theme.colors.text, icon: nil, type: .context("→"), viewType: .firstItem, enabled: true, description: nil))) + index += 1 + + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: InputDataIdentifier("reply_to_message"), data: InputDataGeneralData(name: L10n.shortcutsControllerChatSelectMessageToReply, color: theme.colors.text, icon: nil, type: .context("⌘↑ / ⌘↓"), viewType: .innerItem, enabled: true, description: nil))) + index += 1 + + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: InputDataIdentifier("edit_message"), data: InputDataGeneralData(name: L10n.shortcutsControllerChatEditLastMessage, color: theme.colors.text, icon: nil, type: .context("↑"), viewType: .innerItem, enabled: true, description: nil))) + index += 1 + + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: InputDataIdentifier("edit_media"), data: InputDataGeneralData(name: L10n.shortcutsControllerChatRecordVoiceMessage, color: theme.colors.text, icon: nil, type: .context("⌘R"), viewType: .innerItem, enabled: true, description: nil))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: InputDataIdentifier("search_in_chat"), data: InputDataGeneralData(name: L10n.shortcutsControllerChatSearchMessages, color: theme.colors.text, icon: nil, type: .context("⌘F"), viewType: .lastItem, enabled: true, description: nil))) + index += 1 + + // video chat + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.shortcutsControllerVideoChat), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: InputDataIdentifier("toggle_camera"), data: InputDataGeneralData(name: L10n.shortcutsControllerVideoChatToggleCamera, color: theme.colors.text, icon: nil, type: .context("⌘E"), viewType: .firstItem, enabled: true, description: nil))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: InputDataIdentifier("toggle_screen"), data: InputDataGeneralData(name: L10n.shortcutsControllerVideoChatToggleScreencast, color: theme.colors.text, icon: nil, type: .context("⌘T"), viewType: .lastItem, enabled: true, description: nil))) + index += 1 + + + //search + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.shortcutsControllerSearch), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: InputDataIdentifier("quick_search"), data: InputDataGeneralData(name: L10n.shortcutsControllerSearchQuickSearch, color: theme.colors.text, icon: nil, type: .context("⌘K"), viewType: .firstItem, enabled: true, description: nil))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: InputDataIdentifier("global_search"), data: InputDataGeneralData(name: L10n.shortcutsControllerSearchGlobalSearch, color: theme.colors.text, icon: nil, type: .context("⇧⌘F"), viewType: .lastItem, enabled: true, description: nil))) + index += 1 + + + //MARKDOWN + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.shortcutsControllerMarkdown), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: InputDataIdentifier("markdown_bold"), data: InputDataGeneralData(name: L10n.shortcutsControllerMarkdownBold, color: theme.colors.text, icon: nil, type: .context("⌘B / **"), viewType: .firstItem, enabled: true, description: nil))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: InputDataIdentifier("markdown_italic"), data: InputDataGeneralData(name: L10n.shortcutsControllerMarkdownItalic, color: theme.colors.text, icon: nil, type: .context("⌘I / __"), viewType: .innerItem, enabled: true, description: nil))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: InputDataIdentifier("markdown_monospace"), data: InputDataGeneralData(name: L10n.shortcutsControllerMarkdownMonospace, color: theme.colors.text, icon: nil, type: .context("⇧⌘K / `"), viewType: .innerItem, enabled: true, description: nil))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: InputDataIdentifier("markdown_url"), data: InputDataGeneralData(name: L10n.shortcutsControllerMarkdownHyperlink, color: theme.colors.text, icon: nil, type: .context("⌘U"), viewType: .innerItem, enabled: true, description: nil))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: InputDataIdentifier("markdown_strikethrough"), data: InputDataGeneralData(name: L10n.shortcutsControllerMarkdownStrikethrough, color: theme.colors.text, icon: nil, type: .context("~~"), viewType: .lastItem, enabled: true, description: nil))) + index += 1 + + // OTHERS + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.shortcutsControllerOthers), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: InputDataIdentifier("lock_passcode"), data: InputDataGeneralData(name: L10n.shortcutsControllerOthersLockByPasscode, color: theme.colors.text, icon: nil, type: .context("⌘L"), viewType: .singleItem, enabled: true, description: nil))) + index += 1 + + + // MOUSE + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.shortcutsControllerMouse), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: InputDataIdentifier("fast_reply"), data: InputDataGeneralData(name: L10n.shortcutsControllerMouseFastReply, color: theme.colors.text, icon: nil, type: .context(L10n.shortcutsControllerMouseFastReplyValue), viewType: .firstItem, enabled: true, description: nil))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: InputDataIdentifier("schedule"), data: InputDataGeneralData(name: L10n.shortcutsControllerMouseScheduleMessage, color: theme.colors.text, icon: nil, type: .context(L10n.shortcutsControllerMouseScheduleMessageValue), viewType: .lastItem, enabled: true, description: nil))) + index += 1 + + + //Trackpad Gesture + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.shortcutsControllerGestures), data: .init(color: theme.colors.listGrayText, viewType: .textTopItem))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: InputDataIdentifier("swipe_reply"), data: InputDataGeneralData(name: L10n.shortcutsControllerGesturesReply, color: theme.colors.text, icon: nil, type: .context(L10n.shortcutsControllerGesturesReplyValue), viewType: .firstItem, enabled: true, description: nil))) + index += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: InputDataIdentifier("swipe_actions"), data: InputDataGeneralData(name: L10n.shortcutsControllerGesturesChatAction, color: theme.colors.text, icon: nil, type: .context(L10n.shortcutsControllerGesturesChatActionValue), viewType: .innerItem, enabled: true, description: nil))) + index += 1 + + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: InputDataIdentifier("swipe_navigation"), data: InputDataGeneralData(name: L10n.shortcutsControllerGesturesNavigation, color: theme.colors.text, icon: nil, type: .context(L10n.shortcutsControllerGesturesNavigationsValue), viewType: .innerItem, enabled: true, description: nil))) + index += 1 + + + entries.append(.general(sectionId: sectionId, index: index, value: .none, error: nil, identifier: InputDataIdentifier("swipe_stickers"), data: InputDataGeneralData(name: L10n.shortcutsControllerGesturesStickers, color: theme.colors.text, icon: nil, type: .context(L10n.shortcutsControllerGesturesStickersValue), viewType: .lastItem, enabled: true, description: nil))) + index += 1 + + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + return entries +} + +func ShortcutListController(context: AccountContext) -> ViewController { + + let controller = InputDataController(dataSignal: .single(InputDataSignalValue(entries: shortcutEntires())), title: L10n.shortcutsControllerTitle, validateData: { data in + return .fail(.none) + }, removeAfterDisappear: true, hasDone: false, identifier: "shortcuts") + + controller._abolishWhenNavigationSame = true + + return controller +} diff --git a/Telegram-Mac/SidebarCapViewController.swift b/Telegram-Mac/SidebarCapViewController.swift index 383c6dbf32..a86ee63b05 100644 --- a/Telegram-Mac/SidebarCapViewController.swift +++ b/Telegram-Mac/SidebarCapViewController.swift @@ -8,17 +8,19 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac +import Postbox +import TelegramCore + +import SwiftSignalKit class SidebarCapView : View { private let text:NSTextField = NSTextField() fileprivate let close:TitleButton = TitleButton() + fileprivate var restrictedByPeer: Bool = false required init(frame frameRect: NSRect) { super.init(frame: frameRect) - text.font = .normal(.custom(15)) + text.font = .normal(.header) text.drawsBackground = false // text.backgroundColor = .clear text.isSelectable = false @@ -34,22 +36,26 @@ class SidebarCapView : View { addSubview(close) - updateLocalizationAndTheme() + updateLocalizationAndTheme(theme: theme) } - override func updateLocalizationAndTheme() { - super.updateLocalizationAndTheme() + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) text.textColor = theme.colors.grayText - text.stringValue = tr(.sidebarAvalability); + text.stringValue = restrictedByPeer ? L10n.sidebarPeerRestricted : L10n.sidebarAvalability text.setFrameSize(text.sizeThatFits(NSMakeSize(300, 100))) self.background = theme.colors.background.withAlphaComponent(0.97) - close.set(color: theme.colors.blueUI, for: .Normal) - close.set(text: tr(.navigationClose), for: .Normal) - close.sizeToFit() + close.set(color: theme.colors.accent, for: .Normal) + close.set(text: tr(L10n.sidebarHide), for: .Normal) + _ = close.sizeToFit() needsLayout = true } + override func viewDidMoveToSuperview() { + super.viewDidMoveToSuperview() + } + override func scrollWheel(with event: NSEvent) { } @@ -70,41 +76,79 @@ class SidebarCapView : View { } class SidebarCapViewController: GenericViewController { - private let account:Account - init(account:Account) { - self.account = account + private let context:AccountContext + private let globalPeerDisposable = MetaDisposable() + private var inChatAbility: Bool = true { + didSet { + navigationWillChangeController() + } + } + init(_ context:AccountContext) { + self.context = context super.init() } override func viewDidLoad() { super.viewDidLoad() - navigation?.add(listener: WeakReference(value: self)) + self.navigationController = context.sharedContext.bindings.rootNavigation() + (navigationController as? MajorNavigationController)?.add(listener: WeakReference(value: self)) genericView.close.set(handler: { [weak self] _ in - self?.navigation?.closeSidebar() + self?.context.sharedContext.bindings.rootNavigation().closeSidebar() FastSettings.toggleSidebarShown(false) - self?.account.context.entertainment.closedBySide() + self?.context.sharedContext.bindings.entertainment().closedBySide() }, for: .Click) + + let postbox = self.context.account.postbox + + globalPeerDisposable.set((context.globalPeerHandler.get() |> mapToSignal { value -> Signal in + if let value = value { + switch value { + case .peer: + return postbox.peerView(id: value.peerId) |> map { + return peerViewMainPeer($0)?.canSendMessage(false) ?? false + } + case .replyThread: + return postbox.peerView(id: value.peerId) |> map { + return peerViewMainPeer($0)?.canSendMessage(true) ?? false + } + } + + } else { + return .single(false) + } + } |> deliverOnMainQueue).start(next: { [weak self] accept in + self?.readyOnce() + self?.inChatAbility = accept + })) } deinit { - navigation?.remove(listener: WeakReference(value: self)) + } - var navigation:MajorNavigationController? { - return self.account.context.mainNavigation as? MajorNavigationController + override func viewDidChangedNavigationLayout(_ state: SplitViewState) { + super.viewDidChangedNavigationLayout(state) + navigationWillChangeController() } + override func navigationWillChangeController() { - self.view.setFrameSize(account.context.entertainment.frame.size) - if navigation?.controller is ChatController { + self.genericView.restrictedByPeer = !inChatAbility + self.genericView.updateLocalizationAndTheme(theme: theme) + + self.view.setFrameSize(context.sharedContext.bindings.entertainment().frame.size) + + if let controller = navigationController as? MajorNavigationController, controller.genericView.state != .dual { + view.removeFromSuperview() + } else if context.sharedContext.bindings.rootNavigation().controller is ChatController, inChatAbility { view.removeFromSuperview() } else { - self.account.context.entertainment.addSubview(view) + context.sharedContext.bindings.entertainment().addSubview(view) } - NotificationCenter.default.post(name: NSWindow.didBecomeKeyNotification, object: mainWindow) + // NotificationCenter.default.post(name: NSWindow.didBecomeKeyNotification, object: mainWindow) } diff --git a/Telegram-Mac/SignalUtils.swift b/Telegram-Mac/SignalUtils.swift index 376eab27df..c3d60caf85 100644 --- a/Telegram-Mac/SignalUtils.swift +++ b/Telegram-Mac/SignalUtils.swift @@ -7,14 +7,14 @@ // import Cocoa -import SwiftSignalKitMac +import SwiftSignalKit func countdown(_ count:Double, delay:Double) -> Signal { return Signal { subscriber in var value:Double = count subscriber.putNext(value) - var timer:SwiftSignalKitMac.Timer? = nil - timer = SwiftSignalKitMac.Timer(timeout: delay, repeat: true, completion: { + var timer:SwiftSignalKit.Timer? = nil + timer = SwiftSignalKit.Timer(timeout: delay, repeat: true, completion: { value -= delay subscriber.putNext(max(value,0)) if value <= 0 { @@ -35,9 +35,9 @@ public func `repeat`(_ delay:Double, onQueue:Queue) -> (Signal) -> S return Signal { subscriber in // let disposable:MEtadi = DisposableSet() - var timer:SwiftSignalKitMac.Timer? = nil + var timer:SwiftSignalKit.Timer? = nil - timer = SwiftSignalKitMac.Timer(timeout: delay, repeat: true, completion: { + timer = SwiftSignalKit.Timer(timeout: delay, repeat: true, completion: { _ = signal.start(next: { (next) in subscriber.putNext(next) }) diff --git a/Telegram-Mac/Signature.swift b/Telegram-Mac/Signature.swift new file mode 100644 index 0000000000..ef409bb82c --- /dev/null +++ b/Telegram-Mac/Signature.swift @@ -0,0 +1,56 @@ +// +// Signature.swift +// Telegram +// +// Created by Mikhail Filimonov on 21.11.2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import Security +import CommonCrypto + +func evaluateApiData() -> String? { + var rawStaticCode: SecStaticCode? = nil + var result = SecStaticCodeCreateWithPath(URL(fileURLWithPath: Bundle.main.bundlePath) as CFURL, [], &rawStaticCode) + + guard result == 0, let staticCode = rawStaticCode else { + return nil + } + + var dictionary: CFDictionary? = nil + + let flags: SecCSFlags = SecCSFlags(rawValue: kSecCSSigningInformation); + + result = SecCodeCopySigningInformation(staticCode, flags, &dictionary) + + guard result == 0, let info = dictionary as? [String: Any] else { + return nil + } + + guard let rawTrast = info[kSecCodeInfoTrust as String] else { + return nil + } + + guard let _ = info[kSecCodeInfoIdentifier as String] else { + return nil + } + + let trust = (rawTrast as! SecTrust) + let certsCount = SecTrustGetCertificateCount(trust) + var certsData: Data = Data() + + for i in 0 ..< certsCount { + if let cert = SecTrustGetCertificateAtIndex(trust, i) { + certsData.append(SecCertificateCopyData(cert) as Data) + } else { + return nil + } + } + var digest = [UInt8](repeating: 0, count:Int(CC_SHA1_DIGEST_LENGTH)) + certsData.withUnsafeBytes { + _ = CC_SHA1($0, CC_LONG(certsData.count), &digest) + } + let hexBytes = digest.map { String(format: "%02hhx", $0) } + return hexBytes.joined() +} diff --git a/Telegram-Mac/SlotMachineValue.swift b/Telegram-Mac/SlotMachineValue.swift new file mode 100644 index 0000000000..f68c02bc90 --- /dev/null +++ b/Telegram-Mac/SlotMachineValue.swift @@ -0,0 +1,139 @@ +// +// SlotMachineValue.swift +// Telegram +// +// Created by Mikhail Filimonov on 15/10/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa + +struct SlotMachineValue : Equatable { + enum ReelValue : Int32 { + case rolling + case bar + case berries + case lemon + case seven + case sevenWin + } + + let left: ReelValue + let center: ReelValue + let right: ReelValue + + init(rawValue: Int32?) { + if let rawValue = rawValue, rawValue > 0 { + let rawValue = rawValue - 1 + + let leftRawValue = rawValue & 3 + let centerRawValue = rawValue >> 2 & 3 + let rightRawValue = rawValue >> 4 + + func reelValue(for rawValue: Int32) -> ReelValue { + switch rawValue { + case 0: + return .bar + case 1: + return .berries + case 2: + return .lemon + case 3: + return .seven + default: + return .rolling + } + } + var leftReelValue = reelValue(for: leftRawValue) + var centerReelValue = reelValue(for: centerRawValue) + var rightReelValue = reelValue(for: rightRawValue) + + + if leftReelValue == .seven && centerReelValue == .seven && rightReelValue == .seven { + leftReelValue = .sevenWin + centerReelValue = .sevenWin + rightReelValue = .sevenWin + } + + self.left = leftReelValue + self.center = centerReelValue + self.right = rightReelValue + } else { + self.left = .rolling + self.center = .rolling + self.right = .rolling + } + } + + var is777: Bool { + return self.left == .sevenWin && self.center == .sevenWin && self.right == .sevenWin + } + var jackpot: Bool { + switch self.left { + case .sevenWin: + return center == .sevenWin && right == .sevenWin + case .berries: + return center == .berries && right == .berries + case .lemon: + return center == .lemon && right == .lemon + case .bar: + return center == .bar && right == .bar + default: + return false + } + } + + var packIndex: [Int] { + + let leftIndex: Int + let centerIndex: Int + let rightIndex: Int + + if (left == .bar) { + leftIndex = 5 + } else if (left == .berries) { + leftIndex = 6 + } else if (left == .lemon) { + leftIndex = 7 + } else if (left == .seven) { + leftIndex = 4 + } else if (left == .sevenWin) { + leftIndex = 3 + } else { + leftIndex = 8 + } + + if (center == .bar) { + centerIndex = 11 + } else if (center == .berries) { + centerIndex = 12 + } else if (center == .lemon) { + centerIndex = 13 + } else if (center == .seven) { + centerIndex = 10 + } else if (center == .sevenWin) { + centerIndex = 9 + } else { + centerIndex = 14 + } + + if (right == .bar) { + rightIndex = 17 + } else if (right == .berries) { + rightIndex = 18 + } else if (right == .lemon) { + rightIndex = 19 + } else if (right == .seven) { + rightIndex = 16 + } else if (right == .sevenWin) { + rightIndex = 15 + } else { + rightIndex = 20 + } + + return [leftIndex, centerIndex, rightIndex] + } +} + + +let slotsEmoji: String = "🎰" diff --git a/Telegram-Mac/SlotsMediaContentView.swift b/Telegram-Mac/SlotsMediaContentView.swift new file mode 100644 index 0000000000..27efcca7a3 --- /dev/null +++ b/Telegram-Mac/SlotsMediaContentView.swift @@ -0,0 +1,293 @@ +// +// SlotsMediaContentView.swift +// Telegram +// +// Created by Mikhail Filimonov on 15/10/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import Postbox +import TelegramCore + +import TGUIKit +import SwiftSignalKit + + + +class SlotsMediaContentView: ChatMediaContentView { + private let idlePlayer: LottiePlayerView = LottiePlayerView(frame: NSMakeRect(0, 0, 240, 240)) + private let pullPlayer: LottiePlayerView = LottiePlayerView(frame: NSMakeRect(0, 0, 240, 240)) + + private let spin1Player: LottiePlayerView = LottiePlayerView(frame: NSMakeRect(0, 0, 240, 240)) + private let spin2Player: LottiePlayerView = LottiePlayerView(frame: NSMakeRect(0, 0, 240, 240)) + private let spin3Player: LottiePlayerView = LottiePlayerView(frame: NSMakeRect(0, 0, 240, 240)) + + + private var value: SlotMachineValue = SlotMachineValue(rawValue: nil) + + private let thumbView = TransformImageView() + private let loadResourceDisposable = MetaDisposable() + private let stateDisposable = MetaDisposable() + private let view = NSView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + view.addSubview(self.idlePlayer) + view.addSubview(self.spin1Player) + view.addSubview(self.spin2Player) + view.addSubview(self.spin3Player) + view.addSubview(self.pullPlayer) + view.addSubview(self.thumbView) + + addSubview(view) + view.frame = bounds + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func clean() { + loadResourceDisposable.dispose() + } + + deinit { + clean() + } + + func removeNotificationListeners() { + NotificationCenter.default.removeObserver(self) + } + + override func viewDidUpdatedDynamicContent() { + super.viewDidUpdatedDynamicContent() + updatePlayerIfNeeded() + } + + + + + @objc func updatePlayerIfNeeded() { + + } + + private var nextForceAccept: Bool = false + + + override func executeInteraction(_ isControl: Bool) { + + let media = self.media as? TelegramMediaDice + + if let media = media, let message = self.parent { + let item = self.table?.item(stableId: ChatHistoryEntryId.message(message)) + + if let item = item as? ChatRowItem, let peer = item.peer, peer.canSendMessage(item.chatInteraction.mode.isThreadMode) { + let text: String + + switch media.emoji { + case diceSymbol: + text = L10n.chatEmojiDiceResultNew + case dartSymbol: + text = L10n.chatEmojiDartResultNew + default: + text = L10n.chatEmojiDefResultNew(media.emoji) + } + let view: NSView + if !thumbView.isHidden { + view = thumbView + } else { + view = idlePlayer + } + tooltip(for: view, text: text, interactions: globalLinkExecutor, button: (L10n.chatEmojiSend, { [weak item] in + item?.chatInteraction.sendPlainText(media.emoji) + }), offset: NSMakePoint(0, -30)) + } + } + } + + var chatLoopAnimated: Bool { + if let context = self.context { + return context.autoplayMedia.loopAnimatedStickers + } + return true + } + + func updateListeners() { + if let window = window { + NotificationCenter.default.removeObserver(self) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSWindow.didBecomeKeyNotification, object: window) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSWindow.didResignKeyNotification, object: window) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSView.boundsDidChangeNotification, object: table?.contentView) + NotificationCenter.default.addObserver(self, selector: #selector(updatePlayerIfNeeded), name: NSView.frameDidChangeNotification, object: self.enclosingScrollView?.documentView) + } else { + removeNotificationListeners() + } + } + + override func viewWillDraw() { + super.viewWillDraw() + updatePlayerIfNeeded() + } + + override func willRemove() { + super.willRemove() + updateListeners() + updatePlayerIfNeeded() + } + + override func viewDidMoveToSuperview() { + updateListeners() + updatePlayerIfNeeded() + } + + override func viewDidMoveToWindow() { + updateListeners() + updatePlayerIfNeeded() + } + + var players:[LottiePlayerView] { + return [idlePlayer, pullPlayer, spin1Player, spin2Player, spin3Player] + } + + override func update(with media: Media, size: NSSize, context: AccountContext, parent: Message?, table: TableView?, parameters: ChatMediaLayoutParameters?, animated: Bool, positionFlags: LayoutPositionFlags?, approximateSynchronousValue: Bool) { + + + if parent?.stableId != self.parent?.stableId { + _ = players.map { + $0.set(nil) + } + } + + super.update(with: media, size: size, context: context, parent: parent, table: table, parameters: parameters, animated: animated, positionFlags: positionFlags, approximateSynchronousValue: approximateSynchronousValue) + + guard let media = media as? TelegramMediaDice, let parent = parent else { + return + } + + let value = SlotMachineValue(rawValue: media.value) + let previousValue = self.value + self.value = value + + let sent: Bool = media.value != nil + + let played: Bool = FastSettings.diceHasAlreadyPlayed(parent) + + + + let data: Signal<[(String, Data?, TelegramMediaFile)], NoError> = context.diceCache.interactiveSymbolData(baseSymbol: media.emoji, synchronous: approximateSynchronousValue) + + + if previousValue != value { + _ = players.map { + $0.animation?.triggerOn = nil + $0.animation?.onFinish = nil + } + } + + let spinPolicy: LottiePlayPolicy + if sent { + if played { + spinPolicy = .toEnd(from: .max) + } else { + spinPolicy = .onceEnd + } + } else { + spinPolicy = .loop + } + + self.loadResourceDisposable.set((data |> deliverOnMainQueue).start(next: { [weak self] data in + guard let `self` = self else { + return + } + if data.count < 21 { + return + } + + let idleData = data[1] + if let data = idleData.1 { + let policy: LottiePlayPolicy + if value.jackpot && !played { + policy = .onceEnd + } else { + policy = .onceToFrame(1) + } + let animation = LottieAnimation(compressed: data, key: LottieAnimationEntryKey(key: .media(idleData.2.id), size: size), cachePurpose: .none, playPolicy: policy, maximumFps: 60) + self.idlePlayer.set(animation) + } + let pullData = data[2] + if let data = pullData.1 { + let animation = LottieAnimation(compressed: data, key: LottieAnimationEntryKey(key: .media(pullData.2.id), size: size), cachePurpose: .none, playPolicy: played ? .toEnd(from: .max) : .onceEnd, maximumFps: 60) + self.pullPlayer.set(animation) + } + + let indexes = value.packIndex + + + + var spinViews:[LottiePlayerView] = [self.spin1Player, self.spin2Player, self.spin3Player] + + for (i, index) in indexes.enumerated() { + let view = spinViews[i] + let spinData = data[index] + if let data = spinData.1 { + let animation = LottieAnimation(compressed: data, key: LottieAnimationEntryKey(key: .media(spinData.2.id), size: size), cachePurpose: .none, playPolicy: spinPolicy, maximumFps: 60) + if sent && view.animation != nil { + view.animation?.triggerOn = (.first, { [weak view] in + view?.set(animation) + }, {}) + } else { + view.set(animation) + } + if sent { + animation.onFinish = { + FastSettings.markDiceAsPlayed(parent) + if !played, value.is777, !parent.isIncoming(context.account, theme.bubbled) { + PlayConfetti(for: context.window) + } + } + } + } + } + + })) + + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: NSEdgeInsets()) + + self.thumbView.setSignal(signal: cachedSlot(value: value, arguments: arguments, scale: self.backingScaleFactor), clearInstantly: true) + //if !self.thumbView.isFullyLoaded { + self.thumbView.setSignal(chatMessageSlotSticker(postbox: context.account.postbox, value: value, scale: self.backingScaleFactor, size: size), cacheImage: { result in + cacheSlot(result, value: value, arguments: arguments, scale: System.backingScale) + }) + self.thumbView.set(arguments: arguments) + + + self.stateDisposable.set((self.spin1Player.state |> deliverOnMainQueue).start(next: { [weak self] state in + guard let `self` = self else { return } + switch state { + case .playing: + self.thumbView.isHidden = true + case .stoped: + switch spinPolicy { + case .onceEnd: + self.thumbView.isHidden = true + default: + self.thumbView.isHidden = false + } + default: + break + } + })) + + } + + override func layout() { + super.layout() + view.frame = bounds + _ = players.map { + $0.frame = bounds + } + self.thumbView.frame = bounds + } + +} + diff --git a/Telegram-Mac/SoftwareGradientBackgroundItem.swift b/Telegram-Mac/SoftwareGradientBackgroundItem.swift new file mode 100644 index 0000000000..996e120ae7 --- /dev/null +++ b/Telegram-Mac/SoftwareGradientBackgroundItem.swift @@ -0,0 +1,46 @@ +// +// SoftwareGradientBackgroundItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 02.07.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit + +final class SoftwareGradientBackgroundItem : GeneralRowItem { + init(_ initialSize: NSSize, _ stableId: AnyHashable) { + super.init(initialSize, height: 300, stableId: stableId) + } + + override func viewClass() -> AnyClass { + return SoftwareGradientBackgroundView.self + } +} + + +private final class SoftwareGradientBackgroundView: TableRowView { + private let view: AnimatedGradientBackgroundView + required init(frame frameRect: NSRect) { + view = AnimatedGradientBackgroundView(colors: nil, useSharedAnimationPhase: true) + super.init(frame: frameRect) + addSubview(view) + } + + override func layout() { + super.layout() + view.frame = bounds + view.updateLayout(size: bounds.size, transition: .immediate) + } + + override func mouseDown(with event: NSEvent) { + super.mouseDown(with: event) + + view.animateEvent(transition: .animated(duration: 0.5, curve: .spring)) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/SoftwareVideoLayerFrameManager.swift b/Telegram-Mac/SoftwareVideoLayerFrameManager.swift new file mode 100644 index 0000000000..79b3619e36 --- /dev/null +++ b/Telegram-Mac/SoftwareVideoLayerFrameManager.swift @@ -0,0 +1,233 @@ +// +// SoftwareVideoLayerFrameManager.swift +// Telegram +// +// Created by Mikhail Filimonov on 26/05/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import Foundation +import TGUIKit +import Postbox +import TelegramCore + +import SwiftSignalKit +import CoreMedia + + +private let applyQueue = Queue() +private let workers = ThreadPool(threadCount: 3, threadPriority: 0.2) +private var nextWorker = 0 + +final class SoftwareVideoLayerFrameManager { + private var dataDisposable = MetaDisposable() + private let source = Atomic(value: nil) + + private var baseTimestamp: Double? + private var frames: [MediaTrackFrame] = [] + private var minPts: CMTime? + private var maxPts: CMTime? + + private let account: Account + private let resource: MediaResource + private let secondaryResource: MediaResource? + private let queue: ThreadPoolQueue + private let layerHolder: SampleBufferLayer + + private var rotationAngle: CGFloat = 0.0 + private var aspect: CGFloat = 1.0 + + private var layerRotationAngleAndAspect: (CGFloat, CGFloat)? + + init(account: Account, fileReference: FileMediaReference, layerHolder: SampleBufferLayer) { + var resource = fileReference.media.resource + var secondaryResource: MediaResource? + for attribute in fileReference.media.attributes { + if case .Video = attribute { + if let thumbnail = fileReference.media.videoThumbnails.first { + resource = thumbnail.resource + secondaryResource = fileReference.media.resource + } + } + } + + nextWorker += 1 + self.account = account + self.resource = resource + self.secondaryResource = secondaryResource + self.queue = ThreadPoolQueue(threadPool: workers) + self.layerHolder = layerHolder + layerHolder.layer.videoGravity = .resizeAspectFill + layerHolder.layer.masksToBounds = true + } + + deinit { + self.dataDisposable.dispose() + } + + func start() { + let secondarySignal: Signal + if let secondaryResource = self.secondaryResource { + secondarySignal = self.account.postbox.mediaBox.resourceData(secondaryResource, option: .complete(waitUntilFetchStatus: false)) + |> map { data -> String? in + if data.complete { + return data.path + } else { + return nil + } + } + } else { + secondarySignal = .single(nil) + } + + let firstReady: Signal = combineLatest( + self.account.postbox.mediaBox.resourceData(self.resource, option: .complete(waitUntilFetchStatus: false)), + secondarySignal + ) + |> mapToSignal { first, second -> Signal in + if let second = second { + return .single(second) + } else if first.complete { + return .single(first.path) + } else { + return .complete() + } + } + + self.dataDisposable.set((firstReady |> deliverOn(applyQueue)).start(next: { [weak self] path in + if let strongSelf = self { + let _ = strongSelf.source.swap(SoftwareVideoSource(path: path)) + } + })) + } + + func tick(timestamp: Double) { + applyQueue.async { + if self.baseTimestamp == nil && !self.frames.isEmpty { + self.baseTimestamp = timestamp + } + + if let baseTimestamp = self.baseTimestamp { + var index = 0 + var latestFrameIndex: Int? + while index < self.frames.count { + if baseTimestamp + self.frames[index].position.seconds + self.frames[index].duration.seconds <= timestamp { + latestFrameIndex = index + //print("latestFrameIndex = \(index)") + } + index += 1 + } + if let latestFrameIndex = latestFrameIndex { + let frame = self.frames[latestFrameIndex] + for i in (0 ... latestFrameIndex).reversed() { + self.frames.remove(at: i) + } + if self.layerHolder.layer.status == .failed { + self.layerHolder.layer.flush() + } + //if frame.resetDecoder { +// self.layerHolder.layer.flushAndRemoveImage() + // } + + + /*if self.layerRotationAngleAndAspect?.0 != self.rotationAngle || self.layerRotationAngleAndAspect?.1 != self.aspect { + self.layerRotationAngleAndAspect = (self.rotationAngle, self.aspect) + var transform = CGAffineTransform(rotationAngle: CGFloat(self.rotationAngle)) + if !self.rotationAngle.isZero { + transform = transform.scaledBy(x: CGFloat(self.aspect), y: CGFloat(1.0 / self.aspect)) + } + self.layerHolder.layer.setAffineTransform(transform) + }*/ + self.layerHolder.layer.enqueue(frame.sampleBuffer) + } + } + + self.poll() + } + } + + private var polling = false + + private func poll() { + if self.frames.count < 2 && !self.polling, self.source.with ({ $0 != nil }) { + self.polling = true + let minPts = self.minPts + let maxPts = self.maxPts + self.queue.addTask(ThreadPoolTask { [weak self] state in + if state.cancelled.with({ $0 }) { + return + } + if let strongSelf = self { + var frameAndLoop: (MediaTrackFrame?, CGFloat, CGFloat, Bool)? + + var hadLoop = false + for _ in 0 ..< 1 { + frameAndLoop = (strongSelf.source.with { $0 })?.readFrame(maxPts: maxPts) + if let frameAndLoop = frameAndLoop { + if frameAndLoop.0 != nil || minPts != nil { + break + } else { + if frameAndLoop.3 { + hadLoop = true + } + //print("skip nil frame loop: \(frameAndLoop.3)") + } + } else { + break + } + } + if let loop = frameAndLoop?.3, loop { + hadLoop = true + } + + applyQueue.async { + if let strongSelf = self { + strongSelf.polling = false + if let (_, rotationAngle, aspect, _) = frameAndLoop { + strongSelf.rotationAngle = rotationAngle + strongSelf.aspect = aspect + } + var noFrame = false + if let frame = frameAndLoop?.0 { + if strongSelf.minPts == nil || CMTimeCompare(strongSelf.minPts!, frame.position) < 0 { + var position = CMTimeAdd(frame.position, frame.duration) + for _ in 0 ..< 1 { + position = CMTimeAdd(position, frame.duration) + } + strongSelf.minPts = position + } + strongSelf.frames.append(frame) + strongSelf.frames.sort(by: { lhs, rhs in + if CMTimeCompare(lhs.position, rhs.position) < 0 { + return true + } else { + return false + } + }) + //print("add frame at \(CMTimeGetSeconds(frame.position))") + //let positions = strongSelf.frames.map { CMTimeGetSeconds($0.position) } + //print("frames: \(positions)") + } else { + noFrame = true + //print("not adding frames") + } + if hadLoop { + strongSelf.maxPts = strongSelf.minPts + strongSelf.minPts = nil + //print("loop at \(strongSelf.minPts)") + } + if strongSelf.source.with ({ $0 == nil }) || noFrame { + delay(0.2, onQueue: applyQueue.queue, closure: { [weak strongSelf] in + strongSelf?.poll() + }) + } else { + strongSelf.poll() + } + } + } + } + }) + } + } +} diff --git a/Telegram-Mac/SoftwareVideoSource.swift b/Telegram-Mac/SoftwareVideoSource.swift new file mode 100644 index 0000000000..4a18d6c1e7 --- /dev/null +++ b/Telegram-Mac/SoftwareVideoSource.swift @@ -0,0 +1,265 @@ +// +// SoftwareVideoSource.swift +// Telegram +// +// Created by Mikhail Filimonov on 24/07/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import FFMpegBinding +import Foundation +import CoreMedia +import SwiftSignalKit + +private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: UnsafeMutablePointer?, bufferSize: Int32) -> Int32 { + let context = Unmanaged.fromOpaque(userData!).takeUnretainedValue() + if let fd = context.fd { + return Int32(read(fd, buffer, Int(bufferSize))) + } + return 0 +} + +private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whence: Int32) -> Int64 { + let context = Unmanaged.fromOpaque(userData!).takeUnretainedValue() + if let fd = context.fd { + if (whence & FFMPEG_AVSEEK_SIZE) != 0 { + return Int64(context.size) + } else { + lseek(fd, off_t(offset), SEEK_SET) + return offset + } + } + return 0 +} + +private final class SoftwareVideoStream { + let index: Int + let fps: CMTime + let timebase: CMTime + let duration: CMTime + let decoder: FFMpegMediaVideoFrameDecoder + let rotationAngle: Double + let aspect: Double + + init(index: Int, fps: CMTime, timebase: CMTime, duration: CMTime, decoder: FFMpegMediaVideoFrameDecoder, rotationAngle: Double, aspect: Double) { + self.index = index + self.fps = fps + self.timebase = timebase + self.duration = duration + self.decoder = decoder + self.rotationAngle = rotationAngle + self.aspect = aspect + } +} + +public final class SoftwareVideoSource { + private var readingError = false + private var videoStream: SoftwareVideoStream? + private var avIoContext: FFMpegAVIOContext? + private var avFormatContext: FFMpegAVFormatContext? + private let path: String + fileprivate let fd: Int32? + fileprivate let size: Int32 + + private var enqueuedFrames: [(MediaTrackFrame, CGFloat, CGFloat, Bool)] = [] + private var hasReadToEnd: Bool = false + + public init(path: String) { + let _ = FFMpegMediaFrameSourceContextHelpers.registerFFMpegGlobals + + var s = stat() + stat(path, &s) + self.size = Int32(s.st_size) + + let fd = open(path, O_RDONLY, S_IRUSR) + if fd >= 0 { + self.fd = fd + } else { + self.fd = nil + } + + self.path = path + + let avFormatContext = FFMpegAVFormatContext() + + let ioBufferSize = 64 * 1024 + + let avIoContext = FFMpegAVIOContext(bufferSize: Int32(ioBufferSize), opaqueContext: Unmanaged.passUnretained(self).toOpaque(), readPacket: readPacketCallback, writePacket: nil, seek: seekCallback) + self.avIoContext = avIoContext + + avFormatContext.setIO(self.avIoContext!) + + if !avFormatContext.openInput() { + self.readingError = true + return + } + + if !avFormatContext.findStreamInfo() { + self.readingError = true + return + } + + self.avFormatContext = avFormatContext + + var videoStream: SoftwareVideoStream? + + for streamIndexNumber in avFormatContext.streamIndices(for: FFMpegAVFormatStreamTypeVideo) { + let streamIndex = streamIndexNumber.int32Value + if avFormatContext.isAttachedPic(atStreamIndex: streamIndex) { + continue + } + + let codecId = avFormatContext.codecId(atStreamIndex: streamIndex) + + let fpsAndTimebase = avFormatContext.fpsAndTimebase(forStreamIndex: streamIndex, defaultTimeBase: CMTimeMake(value: 1, timescale: 40000)) + let (fps, timebase) = (fpsAndTimebase.fps, fpsAndTimebase.timebase) + + let duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: streamIndex), timescale: timebase.timescale) + + let metrics = avFormatContext.metricsForStream(at: streamIndex) + + let rotationAngle: Double = metrics.rotationAngle + let aspect = Double(metrics.width) / Double(metrics.height) + + if let codec = FFMpegAVCodec.find(forId: codecId) { + let codecContext = FFMpegAVCodecContext(codec: codec) + if avFormatContext.codecParams(atStreamIndex: streamIndex, to: codecContext) { + if codecContext.open() { + videoStream = SoftwareVideoStream(index: Int(streamIndex), fps: fps, timebase: timebase, duration: duration, decoder: FFMpegMediaVideoFrameDecoder(codecContext: codecContext), rotationAngle: rotationAngle, aspect: aspect) + break + } + } + } + } + + self.videoStream = videoStream + + if let videoStream = self.videoStream { + avFormatContext.seekFrame(forStreamIndex: Int32(videoStream.index), pts: 0, positionOnKeyframe: true) + } + } + + deinit { + if let fd = self.fd { + close(fd) + } + } + + private func readPacketInternal() -> FFMpegPacket? { + guard let avFormatContext = self.avFormatContext else { + return nil + } + + let packet = FFMpegPacket() + if avFormatContext.readFrame(into: packet) { + return packet + } else { + return nil + } + } + + func readDecodableFrame() -> (MediaTrackDecodableFrame?, Bool) { + var frames: [MediaTrackDecodableFrame] = [] + var endOfStream = false + + while !self.readingError && frames.isEmpty { + if let packet = self.readPacketInternal() { + if let videoStream = videoStream, Int(packet.streamIndex) == videoStream.index { + let packetPts = packet.pts + + let pts = CMTimeMake(value: packetPts, timescale: videoStream.timebase.timescale) + let dts = CMTimeMake(value: packet.dts, timescale: videoStream.timebase.timescale) + + let duration: CMTime + + let frameDuration = packet.duration + if frameDuration != 0 { + duration = CMTimeMake(value: frameDuration * videoStream.timebase.value, timescale: videoStream.timebase.timescale) + } else { + duration = videoStream.fps + } + + let frame = MediaTrackDecodableFrame(type: .video, packet: packet, pts: pts, dts: dts, duration: duration) + frames.append(frame) + } + } else { + if endOfStream { + break + } else { + if let avFormatContext = self.avFormatContext, let videoStream = self.videoStream { + endOfStream = true + break + } else { + endOfStream = true + break + } + } + } + } + + return (frames.first, endOfStream) + } + + func readFrame(maxPts: CMTime?) -> (MediaTrackFrame?, CGFloat, CGFloat, Bool) { + guard let videoStream = self.videoStream, let avFormatContext = self.avFormatContext else { + return (nil, 0.0, 1.0, false) + } + + if !self.enqueuedFrames.isEmpty { + let value = self.enqueuedFrames.removeFirst() + return (value.0, value.1, value.2, value.3) + } + + let (decodableFrame, loop) = self.readDecodableFrame() + var result: (MediaTrackFrame?, CGFloat, CGFloat, Bool) + if let decodableFrame = decodableFrame { + var ptsOffset: CMTime? + if let maxPts = maxPts, CMTimeCompare(decodableFrame.pts, maxPts) < 0 { + ptsOffset = maxPts + } + result = (videoStream.decoder.decode(frame: decodableFrame, ptsOffset: ptsOffset), CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop) + } else { + result = (nil, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop) + } + if loop { + let _ = videoStream.decoder.sendEndToDecoder() + let remainingFrames = videoStream.decoder.receiveRemainingFrames(ptsOffset: maxPts) + for i in 0 ..< remainingFrames.count { + self.enqueuedFrames.append((remainingFrames[i], CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), i == remainingFrames.count - 1)) + } + videoStream.decoder.reset() + avFormatContext.seekFrame(forStreamIndex: Int32(videoStream.index), pts: 0, positionOnKeyframe: true) + + if result.0 == nil && !self.enqueuedFrames.isEmpty { + let value = self.enqueuedFrames.removeFirst() + result = (value.0, value.1, value.2, value.3) + } + } + return result + } + + func readImage() -> (CGImage?, CGFloat, CGFloat, Bool) { + if let videoStream = self.videoStream { + for _ in 0 ..< 10 { + let (decodableFrame, loop) = self.readDecodableFrame() + if let decodableFrame = decodableFrame { + if let renderedFrame = videoStream.decoder.render(frame: decodableFrame) { + return (renderedFrame._cgImage, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop) + } + } + } + return (nil, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), true) + } else { + return (nil, 0.0, 1.0, false) + } + } + + public func seek(timestamp: Double) { + if let stream = self.videoStream, let avFormatContext = self.avFormatContext { + let pts = CMTimeMakeWithSeconds(timestamp, preferredTimescale: stream.timebase.timescale) + avFormatContext.seekFrame(forStreamIndex: Int32(stream.index), pts: pts.value, positionOnKeyframe: true) + stream.decoder.reset() + } + } +} diff --git a/Telegram-Mac/SoftwareVideoThumbnailLayer.swift b/Telegram-Mac/SoftwareVideoThumbnailLayer.swift new file mode 100644 index 0000000000..037ecc7227 --- /dev/null +++ b/Telegram-Mac/SoftwareVideoThumbnailLayer.swift @@ -0,0 +1,74 @@ +// +// SoftwareVideoThumbnailLayer.swift +// Telegram +// +// Created by Mikhail Filimonov on 27/05/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa + +import Foundation +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + +private final class SoftwareVideoThumbnailLayerNullAction: NSObject, CAAction { + @objc func run(forKey event: String, object anObject: Any, arguments dict: [AnyHashable : Any]?) { + } +} + +final class SoftwareVideoThumbnailView: NSView { + private var asolutePosition: (CGRect, CGSize)? + + var disposable = MetaDisposable() + + var ready: (() -> Void)? { + didSet { + if self.layer?.contents != nil { + self.ready?() + } + } + } + + init(account: Account, fileReference: FileMediaReference, synchronousLoad: Bool) { + super.init(frame: .zero) + + + self.layer?.backgroundColor = NSColor.clear.cgColor + self.layer?.contentsGravity = .resizeAspectFill + self.layer?.masksToBounds = true + + if let dimensions = fileReference.media.dimensions { + self.disposable.set((mediaGridMessageVideo(postbox: account.postbox, fileReference: fileReference, scale: backingScaleFactor, synchronousLoad: synchronousLoad) + |> deliverOnMainQueue).start(next: { [weak self] transform in + var boundingSize = dimensions.size.aspectFilled(CGSize(width: 93.0, height: 93.0)) + let imageSize = boundingSize + boundingSize.width = min(200.0, boundingSize.width) + + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: NSEdgeInsets(), resizeMode: .fill(.clear)) + + if let image = transform.execute(arguments, transform.data)?.generateImage() { + Queue.mainQueue().async { + if let strongSelf = self { + strongSelf.layer?.contents = image + strongSelf.ready?() + } + } + } + })) + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.disposable.dispose() + } + +} + diff --git a/Telegram-Mac/SoundEffectPlayQueue.swift b/Telegram-Mac/SoundEffectPlayQueue.swift new file mode 100644 index 0000000000..2e68125d82 --- /dev/null +++ b/Telegram-Mac/SoundEffectPlayQueue.swift @@ -0,0 +1,33 @@ +// +// SoundEffectPlayQueue.swift +// Telegram +// +// Created by Mikhail Filimonov on 04.12.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Foundation +import SwiftSignalKit +import Postbox + +import TelegramCore +import TGUIKit +final class SoundEffectPlay { + private static var queue: [Int64: MediaPlayer] = [:] + static func play(postbox: Postbox, name: String?, type: String = "mp3", volume: Float = 1.0) { + if let name = name, let filePath = Bundle.main.path(forResource: name, ofType: type) { + let id = arc4random64() + let resource = LocalFileReferenceMediaResource(localFilePath: filePath, randomId: id) + let player = MediaPlayer(postbox: postbox, reference: MediaResourceReference.standalone(resource: resource), streamable: false, video: false, preferSoftwareDecoding: false, enableSound: true, volume: volume, fetchAutomatically: true) + // player.setVolume(0.6) + queue[id] = player + player.play() + player.actionAtEnd = .action({ + DispatchQueue.main.async { + SoundEffectPlay.queue.removeValue(forKey: id) + } + }) + } + + } +} diff --git a/Telegram-Mac/SoundEffects.swift b/Telegram-Mac/SoundEffects.swift new file mode 100644 index 0000000000..70c91a0da2 --- /dev/null +++ b/Telegram-Mac/SoundEffects.swift @@ -0,0 +1,47 @@ +// +// SoundEffects.swift +// Telegram +// +// Created by Mikhail Filimonov on 10.01.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa + +enum SoundEffect { + case quizCorrect + case quizIncorrect + case confetti + var name: String { + switch self { + case .quizCorrect: + return "quiz-correct" + case .quizIncorrect: + return "quiz-incorrect" + case .confetti: + return "confetti" + } + } + var ext: String { + switch self { + case .quizCorrect, .quizIncorrect, .confetti: + return "mp3" + } + } +} + + +func playSoundEffect(_ sound: SoundEffect) { + let afterSentSound:NSSound? = { + + let p = Bundle.main.path(forResource: sound.name, ofType: sound.ext) + var sound:NSSound? + if let p = p { + sound = NSSound(contentsOfFile: p, byReference: true) + sound?.volume = 0.1 + } + + return sound + }() + afterSentSound?.play() +} diff --git a/Telegram-Mac/Spotlight.swift b/Telegram-Mac/Spotlight.swift new file mode 100644 index 0000000000..6cc7a13c6a --- /dev/null +++ b/Telegram-Mac/Spotlight.swift @@ -0,0 +1,191 @@ +// +// TestSpotlight.swift +// Telegram +// +// Created by Mikhail Filimonov on 20.11.2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import CoreSpotlight +import TelegramCore + +import Postbox +import SwiftSignalKit +import TGUIKit + +enum SpotlightIdentifierSource : Equatable { + case peerId(PeerId) + + fileprivate var stringValue: String { + switch self { + case let .peerId(peerId): + return "peerId:\(peerId.toInt64())" + } + } +} + +struct SpotlightIdentifier : Hashable { + let recordId: AccountRecordId + let source:SpotlightIdentifierSource + + + func hash(into hasher: inout Hasher) { + hasher.combine(stringValue) + } + + fileprivate var stringValue: String { + return "accountId=\(recordId.int64)&source=\(source.stringValue)" + } +} +@available(macOS 10.13, *) +private func makeSearchItem(for peer: Peer, index: Int, accountPeer: Peer, accountId: AccountRecordId) -> SpotlightItem { + let key = SpotlightIdentifier(recordId: accountId, source: .peerId(peer.id)) + let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeData as String) + attributeSet.title = peer.displayTitle + " → \(accountPeer.addressName ?? accountPeer.displayTitle)" + attributeSet.contentDescription = "Popular contact in telegram" + attributeSet.thumbnailData = theme.icons.appUpdate.data + attributeSet.creator = "Telegram" + attributeSet.kind = "Contact" + + return .recentPeer(key, index, CSSearchableItem(uniqueIdentifier: key.stringValue, domainIdentifier: Bundle.main.bundleIdentifier!, attributeSet: attributeSet), peer) +} + +private enum SpotlightItem : Identifiable, Comparable { + static func == (lhs: SpotlightItem, rhs: SpotlightItem) -> Bool { + switch lhs { + case let .recentPeer(id, index, _, lhsPeer): + if case .recentPeer(id, index, _, let rhsPeer) = rhs { + return lhsPeer.isEqual(rhsPeer) + } else { + return false + } + } + } + + case recentPeer(SpotlightIdentifier, Int, CSSearchableItem, Peer) + + static func < (lhs: SpotlightItem, rhs: SpotlightItem) -> Bool { + return lhs.index < rhs.index + } + var index: Int { + switch self { + case let .recentPeer(_, index, _, _): + return index + } + } + var stableId: SpotlightIdentifier { + switch self { + case let .recentPeer(id, _, _, _): + return id + } + } + var item:CSSearchableItem { + switch self { + case let .recentPeer(_, _, item, _): + return item + } + } +} + + + +final class SpotlightContext { + let engine: TelegramEngine + private let disposable = MetaDisposable() + private var previousItems:[SpotlightItem] = [] + init(engine: TelegramEngine) { + self.engine = engine + if #available(macOS 10.12, *) { + let accountPeer = engine.account.postbox.loadedPeerWithId(engine.account.peerId) + + + let recently = engine.peers.recentlySearchedPeers() |> map { + $0.compactMap { $0.peer.chatMainPeer } + } |> distinctUntilChanged(isEqual: { previous, current -> Bool in + return previous.count == current.count + }) + + let peers:Signal<[Peer], NoError> = combineLatest(recently, engine.peers.recentPeers() |> mapToSignal { recent in + switch recent { + case .disabled: + return .single([]) + case let .peers(peers): + return .single(peers) + } + }) |> map { + $0 + $1 + } |> distinctUntilChanged(isEqual: { previous, current -> Bool in + return previous.count == current.count + }) + + + + let signal = combineLatest(queue: .mainQueue(), accountPeer, peers) + + + + disposable.set(signal.start(next: { [weak self] accountPeer, peers in + guard let `self` = self else { + return + } + var items: [SpotlightItem] = [] + for (i, peer) in peers.enumerated() { + if #available(OSX 10.13, *) { + items.append(makeSearchItem(for: peer, index: i, accountPeer: accountPeer, accountId: engine.account.id)) + } else { + // Fallback on earlier versions + } + } + + let (delete, insert, update) = mergeListsStableWithUpdates(leftList: self.previousItems, rightList: items) + + if !insert.isEmpty || !update.isEmpty { + CSSearchableIndex.default().indexSearchableItems(insert.map { $0.1.item }, completionHandler: nil) + CSSearchableIndex.default().indexSearchableItems(update.map { $0.1.item }, completionHandler: nil) + } + + var deleted: [SpotlightItem] = [] + for index in delete.reversed() { + deleted.append(self.previousItems.remove(at: index)) + } + + self.previousItems = items + if !deleted.isEmpty { + CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: deleted.map { $0.stableId.stringValue }, completionHandler: nil) + } + })) + } + + } + + deinit { + if #available(OSX 10.12, *) { + CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: previousItems.map { $0.stableId.stringValue }, completionHandler: nil) + } + } +} + +func parseSpotlightIdentifier(_ unique: String) -> SpotlightIdentifier? { + let (vars, _) = urlVars(with: unique) + + if let source = vars["source"], let rawAccountId = vars["accountId"], let int64AccountId = Int64(rawAccountId) { + let accountId = AccountRecordId(rawValue: int64AccountId) + let sourceComponents = source.components(separatedBy: ":") + if sourceComponents.count == 2 { + switch sourceComponents[0] { + case "peerId": + if let id = Int64(sourceComponents[1]) { + let peerId = PeerId(id) + return SpotlightIdentifier(recordId: accountId, source: .peerId(peerId)) + } + default: + break + } + } + } + + + return nil +} + diff --git a/Telegram-Mac/StatisticRowItem.swift b/Telegram-Mac/StatisticRowItem.swift new file mode 100644 index 0000000000..69cabe62de --- /dev/null +++ b/Telegram-Mac/StatisticRowItem.swift @@ -0,0 +1,324 @@ +// +// StatisticRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 24.02.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import GraphUI +import GraphCore +import SwiftSignalKit +public enum ChartItemType { + case lines + case twoAxis + case pie + case area + case bars + case step + case twoAxisStep + case hourlyStep + case twoAxisHourlyStep + case twoAxis5MinStep +} + + + +class StatisticRowItem: GeneralRowItem { + let collection: ChartsCollection + let controller: BaseChartController + init(_ initialSize: NSSize, stableId: AnyHashable, context: AccountContext, collection: ChartsCollection, viewType: GeneralViewType, type: ChartItemType, getDetailsData: @escaping (Date, @escaping (String?) -> Void) -> Void) { + self.collection = collection + + let controller: BaseChartController + switch type { + case .lines: + controller = GeneralLinesChartController(chartsCollection: collection) + controller.isZoomable = false + case .twoAxis: + controller = TwoAxisLinesChartController(chartsCollection: collection) + controller.isZoomable = false + case .pie: + controller = PercentPieChartController(chartsCollection: collection) + case .area: + controller = PercentPieChartController(chartsCollection: collection, initiallyZoomed: false) + case .bars: + controller = StackedBarsChartController(chartsCollection: collection) + controller.isZoomable = false + case .step: + controller = StepBarsChartController(chartsCollection: collection) + case .twoAxisStep: + controller = TwoAxisStepBarsChartController(chartsCollection: collection) + case .hourlyStep: + controller = StepBarsChartController(chartsCollection: collection, hourly: true) + controller.isZoomable = false + case .twoAxisHourlyStep: + let stepController = TwoAxisStepBarsChartController(chartsCollection: collection) + stepController.hourly = true + controller = stepController + controller.isZoomable = false + case .twoAxis5MinStep: + let stepController = TwoAxisStepBarsChartController(chartsCollection: collection) + stepController.min5 = true + controller = stepController + controller.isZoomable = false + } + + + + controller.getDetailsData = { date, completion in + let signal:Signal = Signal { subscriber -> Disposable in + var cancelled: Bool = false + getDetailsData(date, { detailsData in + if let detailsData = detailsData, let data = detailsData.data(using: .utf8), !cancelled { + ChartsDataManager.readChart(data: data, extraCopiesCount: 0, sync: true, success: { collection in + if !cancelled { + subscriber.putNext(collection) + subscriber.putCompletion() + } + + }) { error in + if !cancelled { + subscriber.putNext(nil) + subscriber.putCompletion() + } + } + } else { + if !cancelled { + subscriber.putNext(nil) + subscriber.putCompletion() + } + } + }) + + return ActionDisposable { + cancelled = true + } + } + + _ = showModalProgress(signal: signal, for: context.window).start(next: { collection in + completion(collection) + }) + } + self.controller = controller + + super.init(initialSize, stableId: stableId, viewType: viewType) + _ = makeSize(initialSize.width, oldWidth: 0) + } + + private var graphHeight: CGFloat = 0 + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + graphHeight = self.controller.height(for: blockWidth - (viewType.innerInset.left + viewType.innerInset.right)) + viewType.innerInset.bottom + viewType.innerInset.top + return true + } + + override var height: CGFloat { + return graphHeight + } + + override func viewClass() -> AnyClass { + return StatisticRowView.self + } + + override var instantlyResize: Bool { + return false + } +} +class StatisticRowView: TableRowView { + private let chartView: ChartStackSection = ChartStackSection() + private let containerView = GeneralRowContainerView(frame: NSZeroRect) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(self.containerView) + self.containerView.addSubview(chartView) + + } + + override func updateMouse() { + super.updateMouse() + chartView.updateMouse() + } + + private var chartTheme: ChartTheme { + let chartTheme = (theme.colors.isDark ? ChartTheme.defaultNightTheme : ChartTheme.defaultDayTheme) + return ChartTheme(chartTitleColor: theme.colors.text, actionButtonColor: theme.colors.accent, chartBackgroundColor: theme.colors.background, chartLabelsColor: theme.colors.grayText, chartHelperLinesColor: chartTheme.chartHelperLinesColor, chartStrongLinesColor: chartTheme.chartStrongLinesColor, barChartStrongLinesColor: chartTheme.barChartStrongLinesColor, chartDetailsTextColor: theme.colors.grayText, chartDetailsArrowColor: theme.colors.grayText, chartDetailsViewColor: theme.colors.grayBackground, rangeViewFrameColor: chartTheme.rangeViewFrameColor, rangeViewTintColor: theme.colors.grayForeground.withAlphaComponent(0.4), rangeViewMarkerColor: chartTheme.rangeViewTintColor, rangeCropImage: chartTheme.rangeCropImage) + } + + override var backdorColor: NSColor { + return chartTheme.chartBackgroundColor + } + + override func updateColors() { + guard let item = item as? StatisticRowItem else { + return + } + self.backgroundColor = item.viewType.rowBackground + self.containerView.backgroundColor = backdorColor + } + override func layout() { + super.layout() + guard let item = item as? StatisticRowItem else { + return + } + + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) + self.containerView.setCorners(item.viewType.corners) + + chartView.frame = NSMakeRect(item.viewType.innerInset.left, item.viewType.innerInset.top, self.containerView.frame.width - item.viewType.innerInset.left - item.viewType.innerInset.right, self.containerView.frame.height - item.viewType.innerInset.top - item.viewType.innerInset.bottom) + chartView.layout() + } + + private var first: Bool = true + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? StatisticRowItem else { + return + } + + layout() + + chartView.setup(controller: item.controller, title: "Test") + + chartView.apply(theme: chartTheme, strings: ChartStrings(zoomOut: L10n.graphZoomOut, total: L10n.graphTotal), animated: false) + + if first { + chartView.layer?.animateAlpha(from: 0, to: 1, duration: 0.25) + } + first = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + +class StatisticLoadingRowItem: GeneralRowItem { + fileprivate let errorTextLayout: TextViewLayout? + init(_ initialSize: NSSize, stableId: AnyHashable, error: String?) { + let height: CGFloat = 308 + GeneralViewType.singleItem.innerInset.bottom + GeneralViewType.singleItem.innerInset.top + 30 + if let error = error { + self.errorTextLayout = TextViewLayout.init(.initialize(string: error, color: theme.colors.grayText, font: .normal(.text))) + } else { + self.errorTextLayout = nil + } + super.init(initialSize, height: height, stableId: stableId, viewType: .singleItem) + _ = self.makeSize(initialSize.width) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + + errorTextLayout?.measure(width: blockWidth - viewType.innerInset.left - viewType.innerInset.right) + return true + } + + override func viewClass() -> AnyClass { + return StatisticLoadingRowView.self + } + + override var instantlyResize: Bool { + return false + } +} +class StatisticLoadingRowView: TableRowView { + private let containerView = GeneralRowContainerView(frame: NSZeroRect) + private var errorView: TextView? + private var progressIndicator: ProgressIndicator? + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(self.containerView) + + } + + override var backdorColor: NSColor { + return theme.colors.background + } + + override func updateColors() { + guard let item = item as? StatisticLoadingRowItem else { + return + } + self.backgroundColor = item.viewType.rowBackground + self.containerView.backgroundColor = backdorColor + } + override func layout() { + super.layout() + guard let item = item as? StatisticLoadingRowItem else { + return + } + + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) + self.containerView.setCorners(item.viewType.corners) + + errorView?.center() + progressIndicator?.center() + } + + + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? StatisticLoadingRowItem else { + return + } + + if let error = item.errorTextLayout { + if self.errorView == nil { + self.errorView = TextView() + self.errorView?.isSelectable = false + self.containerView.addSubview(self.errorView!) + self.errorView?.center() + if animated { + self.errorView?.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } + if animated { + if let progress = self.progressIndicator { + self.progressIndicator = nil + progress.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak progress] _ in + progress?.removeFromSuperview() + }) + } + } else { + self.progressIndicator?.removeFromSuperview() + self.progressIndicator = nil + } + self.errorView?.update(error) + } else { + if self.progressIndicator == nil { + self.progressIndicator = ProgressIndicator(frame: NSMakeRect(0, 0, 30, 30)) + self.containerView.addSubview(self.progressIndicator!) + self.errorView?.center() + if animated { + self.progressIndicator?.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + } + if animated { + self.progressIndicator?.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + if let errorView = self.errorView { + self.errorView = nil + errorView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak errorView] _ in + errorView?.removeFromSuperview() + }) + } + } else { + self.errorView?.removeFromSuperview() + self.errorView = nil + } + } + + needsLayout = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/StatisticsLoadingRowItem.swift b/Telegram-Mac/StatisticsLoadingRowItem.swift new file mode 100644 index 0000000000..6ee25be7cd --- /dev/null +++ b/Telegram-Mac/StatisticsLoadingRowItem.swift @@ -0,0 +1,141 @@ +// +// StatisticsLoadingRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 18.03.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit + +class StatisticsLoadingRowItem: GeneralRowItem { + + let text:TextViewLayout? + + let context: AccountContext + init(_ initialSize: NSSize, stableId:AnyHashable, context: AccountContext, text:String? = nil, viewType: GeneralViewType = .legacy) { + self.context = context + if let text = text { + let attr = NSMutableAttributedString() + _ = attr.append(string: text, color: theme.colors.grayText, font: .normal(.title)) + attr.detectBoldColorInString(with: .medium(.title)) + self.text = TextViewLayout(attr, alignment: .center) + self.text?.measure(width: initialSize.width - 60) + } else { + self.text = nil + } + super.init(initialSize, stableId: stableId, viewType: viewType) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) + text?.measure(width: width - 60) + return success + } + + override var instantlyResize: Bool { + return false + } + + override var height: CGFloat { + if let table = table { + var basic:CGFloat = 0 + table.enumerateItems(with: { [weak self] item in + if let strongSelf = self { + if item.index < strongSelf.index { + basic += item.height + } + } + return true + }) + return table.frame.height - basic + } else { + return initialSize.height + } + } + + override func viewClass() -> AnyClass { + return StatisticsLoadingRowView.self + } +} + + +class StatisticsLoadingRowView : TableRowView { + private let imageView:MediaAnimatedStickerView = MediaAnimatedStickerView(frame: NSZeroRect) + private let textView:TextView = TextView() + private let disposable = MetaDisposable() + private let progressIndicator = ProgressIndicator(frame: NSMakeRect(0, 0, 30, 30)) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(progressIndicator) + addSubview(imageView) + addSubview(textView) + textView.isSelectable = false + + + + self.imageView.change(opacity: 0, animated: false) + self.textView.change(opacity: 0, animated: false) + self.progressIndicator.change(opacity: 1, animated: false) + + let signal = Signal.complete() |> delay(1.5, queue: .mainQueue()) + + disposable.set(signal.start(completed: { [weak self] in + self?.imageView.change(opacity: 1, animated: true) + self?.textView.change(opacity: 1, animated: true) + self?.progressIndicator.change(opacity: 0, animated: true) + })) + } + + + override var backdorColor: NSColor { + if let item = item as? StatisticsLoadingRowItem { + return item.viewType.rowBackground + } else { + return super.backdorColor + } + } + + override func updateColors() { + super.updateColors() + textView.backgroundColor = backdorColor + progressIndicator.progressColor = theme.colors.text + } + + override func layout() { + super.layout() + + progressIndicator.center() + + if let item = item as? StatisticsLoadingRowItem { + textView.update(item.text) + textView.centerX(y: frame.midY + 5) + imageView.centerX(y: frame.midY - imageView.frame.height - 5) + } else { + imageView.center() + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + disposable.dispose() + } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item) + + if let item = item as? StatisticsLoadingRowItem { + + imageView.update(with: LocalAnimatedSticker.graph_loading.file, size: NSMakeSize(80, 80), context: item.context, parent: nil, table: item.table, parameters: LocalAnimatedSticker.graph_loading.parameters, animated: animated, positionFlags: nil, approximateSynchronousValue: false) + + self.needsLayout = true + + + } + } +} diff --git a/Telegram-Mac/StickerPackGridItem.swift b/Telegram-Mac/StickerPackGridItem.swift index e0d2ab86ac..e8a426831b 100644 --- a/Telegram-Mac/StickerPackGridItem.swift +++ b/Telegram-Mac/StickerPackGridItem.swift @@ -8,9 +8,10 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac +import Postbox +import TelegramCore + +import SwiftSignalKit final class StickerPackGridItem: GridItem { @@ -19,39 +20,213 @@ final class StickerPackGridItem: GridItem { return nil } - let account: Account + let context: AccountContext let file: TelegramMediaFile let selected: () -> Void - let send:(TelegramMediaFile) -> Void - init(account: Account, file: TelegramMediaFile, send:@escaping(TelegramMediaFile) -> Void, selected: @escaping () -> Void) { - self.account = account + let send:(TelegramMediaFile, NSView) -> Void + init(context: AccountContext, file: TelegramMediaFile, send:@escaping(TelegramMediaFile, NSView) -> Void, selected: @escaping () -> Void) { + self.context = context self.file = file self.send = send self.selected = selected } - func node(layout: GridNodeLayout, gridNode:GridNode) -> GridItemNode { - let node = StickerGridItemView(gridNode) - node.inputNodeInteraction = EStickersInteraction(navigateToCollectionId: {_ in}, sendSticker: { [weak self] file in - self?.send(file) - }, previewStickerSet: {_ in}) - node.setup(account: self.account, file: self.file) - node.selected = self.selected - return node + func node(layout: GridNodeLayout, gridNode:GridNode, cachedNode: GridItemNode?) -> GridItemNode { + if self.file.isAnimatedSticker { + let node = AnimatedStickerGridItemView(gridNode) + node.sendFile = { [weak self] file, view in + self?.send(file, view) + } + node.setup(context: self.context, file: self.file) + node.selected = self.selected + return node + } else { + let node = StickerGridItemView(gridNode) + node.sendFile = { [weak self] file, view in + self?.send(file, view) + } + node.setup(context: self.context, file: self.file) + node.selected = self.selected + return node + } } func update(node: GridItemNode) { - guard let node = node as? StickerGridItemView else { - assertionFailure() - return + + if let node = node as? StickerGridItemView { + node.setup(context: self.context, file: self.file) + node.selected = self.selected + } else if let node = node as? AnimatedStickerGridItemView { + node.setup(context: self.context, file: self.file) + node.selected = self.selected } - node.inputNodeInteraction = EStickersInteraction(navigateToCollectionId: {_ in}, sendSticker: { [weak self] file in - self?.send(file) - }, previewStickerSet: {_ in}) - node.setup(account: self.account, file: self.file) - node.selected = self.selected } } + + +final class AnimatedStickerGridItemView: GridItemNode, ModalPreviewRowViewProtocol { + private var currentState: (AccountContext, TelegramMediaFile, CGSize)? + + private let view: MediaAnimatedStickerView = MediaAnimatedStickerView(frame: NSZeroRect) + + func fileAtPoint(_ point: NSPoint) -> (QuickPreviewMedia, NSView?)? { + if let currentState = currentState { + let reference = currentState.1.stickerReference != nil ? FileMediaReference.stickerPack(stickerPack: currentState.1.stickerReference!, media: currentState.1) : FileMediaReference.standalone(media: currentState.1) + return (.file(reference, AnimatedStickerPreviewModalView.self), view) + } + return nil + } + + override func menu(for event: NSEvent) -> NSMenu? { + return nil + } + + private let stickerFetchedDisposable = MetaDisposable() + + var sendFile: ((TelegramMediaFile, NSView)->Void)? + var selected: (() -> Void)? + + override init(_ grid:GridNode) { + super.init(grid) + + //backgroundColor = .random + //layer?.cornerRadius = .cornerRadius + addSubview(view) + view.userInteractionEnabled = false + + set(handler: { [weak self] (control) in + if let window = self?.window as? Window, let currentState = self?.currentState, let grid = self?.grid { + _ = startModalPreviewHandle(grid, window: window, context: currentState.0) + } + }, for: .LongMouseDown) + + set(handler: { [weak self] _ in + self?.click() + }, for: .SingleClick) + } + + private func click() { + if mouseInside() || view._mouseInside() { + if let (_, file, _) = currentState { + sendFile?(file, self) + } + } + } + + override func layout() { + view.center() + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + + func setup(context: AccountContext, file: TelegramMediaFile) { + let size = NSMakeSize(60, 60) + self.currentState = (context, file, size) + view.update(with: file, size: size, context: context, parent: nil, table: nil, parameters: ChatAnimatedStickerMediaLayoutParameters(playPolicy: nil, alwaysAccept: nil, cache: nil, media: file), animated: false, positionFlags: nil, approximateSynchronousValue: false) + } + + +} + + + + +final class StickerGridItemView: GridItemNode, ModalPreviewRowViewProtocol { + private var currentState: (AccountContext, TelegramMediaFile, CGSize)? + + + private let imageView: TransformImageView = TransformImageView() + + func fileAtPoint(_ point: NSPoint) -> (QuickPreviewMedia, NSView?)? { + if let currentState = currentState { + let reference = currentState.1.stickerReference != nil ? FileMediaReference.stickerPack(stickerPack: currentState.1.stickerReference!, media: currentState.1) : FileMediaReference.standalone(media: currentState.1) + return (.file(reference, StickerPreviewModalView.self), imageView) + } + return nil + } + + override func menu(for event: NSEvent) -> NSMenu? { + return nil + } + + private let stickerFetchedDisposable = MetaDisposable() + + var sendFile: ((TelegramMediaFile, NSView)->Void)? + var selected: (() -> Void)? + + override init(_ grid:GridNode) { + super.init(grid) + + //backgroundColor = .random + //layer?.cornerRadius = .cornerRadius + addSubview(imageView) + + + set(handler: { [weak self] (control) in + if let window = self?.window as? Window, let currentState = self?.currentState, let grid = self?.grid { + _ = startModalPreviewHandle(grid, window: window, context: currentState.0) + } + }, for: .LongMouseDown) + set(handler: { [weak self] _ in + self?.click() + }, for: .SingleClick) + } + + private func click() { + if mouseInside() || imageView._mouseInside() { + if let (_, file, _) = currentState { + self.sendFile?(file, self) + } + } + } + + override func setFrameSize(_ newSize: NSSize) { + super.setFrameSize(newSize) + imageView.center() + + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + stickerFetchedDisposable.dispose() + } + + func setup(context: AccountContext, file: TelegramMediaFile) { + if let dimensions = file.dimensions?.size { + + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: NSMakeSize(60, 60), boundingSize: NSMakeSize(60, 60), intrinsicInsets: NSEdgeInsets()) + imageView.setSignal(signal: cachedMedia(media: file, arguments: arguments, scale: backingScaleFactor)) + imageView.setSignal(chatMessageSticker(postbox: context.account.postbox, file: stickerPackFileReference(file), small: false, scale: backingScaleFactor, fetched: true), cacheImage: { result in + cacheMedia(result, media: file, arguments: arguments, scale: System.backingScale) + }) + + let imageSize = dimensions.aspectFitted(NSMakeSize(60, 60)) + imageView.set(arguments: arguments) + + imageView.setFrameSize(imageSize) + currentState = (context, file, dimensions) + } + } + + +} diff --git a/Telegram-Mac/StickerPackItems.swift b/Telegram-Mac/StickerPackItems.swift new file mode 100644 index 0000000000..3674ad4416 --- /dev/null +++ b/Telegram-Mac/StickerPackItems.swift @@ -0,0 +1,469 @@ +// +// StickerPackItems.swift +// Telegram +// +// Created by Mikhail Filimonov on 09/07/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + +class StickerPackRowItem: TableRowItem { + + override var height:CGFloat { + return 40.0 + } + + override var width: CGFloat { + return 40 + } + + let info:StickerPackCollectionInfo + let topItem:StickerPackItem? + let context: AccountContext + + let _stableId:StickerPackCollectionId + override var stableId:AnyHashable { + return _stableId + } + let packIndex: Int + + init(_ initialSize:NSSize, packIndex: Int, context:AccountContext, stableId: StickerPackCollectionId, info:StickerPackCollectionInfo, topItem:StickerPackItem?) { + self.context = context + self.packIndex = packIndex + self._stableId = stableId + self.info = info + self.topItem = topItem + super.init(initialSize) + } + + override func menuItems(in location: NSPoint) -> Signal<[ContextMenuItem], NoError> { + var items:[ContextMenuItem] = [] + let context = self.context + + switch _stableId { + case let .pack(id): + items.append(ContextMenuItem(L10n.stickersContextArchive, handler: { + _ = context.engine.stickers.removeStickerPackInteractively(id: id, option: RemoveStickerPackOption.archive).start() + })) + + + #if BETA || ALPHA || DEBUG + if let resource = info.thumbnail?.resource { + items.append(ContextMenuItem("Copy Pack Thumbnail (Dev.)", handler: { + let signal = context.account.postbox.mediaBox.resourceData(resource) |> take(1) |> deliverOnMainQueue + _ = signal.start(next: { data in + if let data = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { + _ = getAnimatedStickerThumb(data: data, size: NSMakeSize(128, 128)).start(next: { path in + if let path = path { + let pb = NSPasteboard.general + pb.clearContents() + pb.writeObjects([NSURL(fileURLWithPath: path)]) + } + }) + } + }) + })) + } + #endif + default: + break + } + return .single(items) + } + + func contentNode()->ChatMediaContentView.Type { + if let file = topItem?.file, file.isAnimatedSticker { + return MediaAnimatedStickerView.self + } else { + return ChatStickerContentView.self + } + } + + override func viewClass() -> AnyClass { + if let file = topItem?.file, file.isAnimatedSticker { + return AnimatedStickerPackRowView.self + } else { + return StickerPackRowView.self + } + } +} + +class RecentPackRowItem: TableRowItem { + + override var height:CGFloat { + return 40.0 + } + override var width: CGFloat { + return 40.0 + } + + let _stableId:StickerPackCollectionId + override var stableId:AnyHashable { + return _stableId + } + + init(_ initialSize:NSSize, _ stableId:StickerPackCollectionId) { + self._stableId = stableId + super.init(initialSize) + } + + override func viewClass() -> AnyClass { + return RecentPackRowView.self + } +} + + +class StickerPackRowView: HorizontalRowView { + + + private let stickerFetchedDisposable = MetaDisposable() + + private var imageView:TransformImageView? + + private let overlay:ImageButton = ImageButton() + + required init(frame frameRect:NSRect) { + super.init(frame:frameRect) + + overlay.setFrameSize(35, 35) + overlay.userInteractionEnabled = false + overlay.autohighlight = false + overlay.canHighlight = false + addSubview(overlay) + + } + + override var backdorColor: NSColor { + return .clear + } + + override func layout() { + super.layout() + + self.imageView?.center() + self.overlay.center() + } + + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + if window == nil { + self.imageView?.removeFromSuperview() + self.imageView = nil + } else if let item = item, self.imageView == nil { + self.set(item: item, animated: false) + } + } + + deinit { + stickerFetchedDisposable.dispose() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func set(item:TableRowItem, animated:Bool = false) { + + var mediaUpdated = true + if let lhs = (self.item as? StickerPackRowItem)?.topItem, let rhs = (item as? StickerPackRowItem)?.topItem { + mediaUpdated = !lhs.file.isEqual(to: rhs.file) + } + + super.set(item: item, animated: animated) + overlay.set(image: theme.icons.stickerPackSelection, for: .Normal) + overlay.set(image: theme.icons.stickerPackSelectionActive, for: .Highlight) + overlay.isSelected = item.isSelected + + if let item = item as? StickerPackRowItem { + var thumbnailItem: TelegramMediaImageRepresentation? + var resourceReference: MediaResourceReference? + + var file: TelegramMediaFile? + + + if let thumbnail = item.info.thumbnail { + thumbnailItem = thumbnail + resourceReference = MediaResourceReference.stickerPackThumbnail(stickerPack: .id(id: item.info.id.id, accessHash: item.info.accessHash), resource: thumbnail.resource) + file = TelegramMediaFile(fileId: MediaId(namespace: 0, id: item.info.id.id), partialReference: nil, resource: thumbnail.resource, previewRepresentations: [thumbnail], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: nil, attributes: [.FileName(fileName: "sticker.webp"), .Sticker(displayText: "", packReference: .id(id: item.info.id.id, accessHash: item.info.accessHash), maskData: nil)]) + } else if let item = item.topItem, let dimensions = item.file.dimensions, let resource = chatMessageStickerResource(file: item.file, small: true) as? TelegramMediaResource { + thumbnailItem = TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil) + resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: resource) + file = item.file + } + + if self.imageView == nil { + self.imageView = TransformImageView() + self.addSubview(self.imageView!) + } + guard let imageView = self.imageView else { + return + } + + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: NSMakeSize(30, 30), boundingSize: NSMakeSize(30, 30), intrinsicInsets: NSEdgeInsets()) + + if let thumbnailItem = thumbnailItem { + if let file = file { + imageView.setSignal(signal: cachedMedia(media: file , arguments: arguments, scale: backingScaleFactor)) + } + if !imageView.isFullyLoaded { + imageView.setSignal( chatMessageStickerPackThumbnail(postbox: item.context.account.postbox, representation: thumbnailItem, scale: backingScaleFactor, synchronousLoad: false), cacheImage: { result in + if let file = file { + cacheMedia(result, media: file, arguments: arguments, scale: System.backingScale) + } + }) + } + } + imageView.set(arguments:arguments) + imageView.setFrameSize(arguments.imageSize) + if let resourceReference = resourceReference { + stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, reference: resourceReference, statsCategory: .file).start()) + } + self.needsLayout = true + } + + + } + +} + +class RecentPackRowView: HorizontalRowView { + + var imageView:ImageView = ImageView() + + var overlay:ImageButton = ImageButton() + + required init(frame frameRect:NSRect) { + super.init(frame:frameRect) + + overlay.setFrameSize(35, 35) + overlay.userInteractionEnabled = false + overlay.autohighlight = false + overlay.canHighlight = false + imageView.setFrameSize(30, 30) + + addSubview(overlay) + addSubview(imageView) + + } + + override func layout() { + super.layout() + imageView.center() + overlay.center() + } + + deinit { + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func set(item:TableRowItem, animated:Bool = false) { + + super.set(item: item, animated: animated) + overlay.set(image: theme.icons.stickerPackSelection, for: .Normal) + overlay.set(image: theme.icons.stickerPackSelectionActive, for: .Highlight) + + overlay.isSelected = item.isSelected + + if let item = item as? RecentPackRowItem { + self.needsLayout = true + switch item._stableId { + case .saved: + imageView.image = theme.icons.stickersTabFave + case .recent: + imageView.image = theme.icons.stickersTabRecent + case let .featured(hasUnred): + imageView.image = hasUnred ? theme.icons.stickers_add_featured_unread : theme.icons.stickers_add_featured + default: + break + } + imageView.sizeToFit() + } + needsLayout = true + } + +} + + + +class StickerSpecificPackItem: TableRowItem { + override var height:CGFloat { + return 40.0 + } + override var width: CGFloat { + return 40.0 + } + fileprivate let specificPack: (StickerPackCollectionInfo, Peer) + fileprivate let account: Account + let _stableId:StickerPackCollectionId + override var stableId:AnyHashable { + return _stableId + } + + init(_ initialSize:NSSize, stableId:StickerPackCollectionId, specificPack: (StickerPackCollectionInfo, Peer), account: Account) { + self._stableId = stableId + self.specificPack = specificPack + self.account = account + super.init(initialSize) + } + + override func viewClass() -> AnyClass { + return StickerSpecificPackView.self + } +} + +class StickerSpecificPackView: HorizontalRowView { + + + var imageView:AvatarControl = AvatarControl(font: .medium(.short)) + + var overlay:ImageButton = ImageButton() + + required init(frame frameRect:NSRect) { + super.init(frame:frameRect) + + imageView.setFrameSize(30, 30) + overlay.setFrameSize(35, 35) + overlay.userInteractionEnabled = false + overlay.autohighlight = false + overlay.canHighlight = false + imageView.userInteractionEnabled = false + addSubview(overlay) + addSubview(imageView) + } + + override func layout() { + super.layout() + imageView.center() + overlay.center() + } + + + deinit { + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func set(item:TableRowItem, animated:Bool = false) { + super.set(item: item, animated: animated) + overlay.set(image: theme.icons.stickerPackSelection, for: .Normal) + overlay.set(image: theme.icons.stickerPackSelectionActive, for: .Highlight) + overlay.isSelected = item.isSelected + if let item = item as? StickerSpecificPackItem { + imageView.setPeer(account: item.account, peer: item.specificPack.1) + } + } + +} + +private final class AnimatedStickerPackRowView : HorizontalRowView { + + + var overlay:ImageButton = ImageButton() + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + overlay.setFrameSize(35, 35) + overlay.autohighlight = false + overlay.canHighlight = false + overlay.userInteractionEnabled = false + addSubview(overlay) + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + fileprivate(set) var contentNode:ChatMediaContentView? + + + override var backgroundColor: NSColor { + didSet { + contentNode?.backgroundColor = backdorColor + } + } + + override var backdorColor: NSColor { + return .clear + } + + override func shakeView() { + contentNode?.shake() + } + + + override func updateMouse() { + super.updateMouse() + self.contentNode?.updateMouse() + } + + + override func viewWillMove(toSuperview newSuperview: NSView?) { + if newSuperview == nil { + self.contentNode?.willRemove() + } + } + + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + if window == nil { + contentNode?.removeFromSuperview() + contentNode = nil + } else if let item = item, contentNode == nil { + self.set(item: item, animated: false) + } + } + + override func set(item:TableRowItem, animated:Bool = false) { + if let item = item as? StickerPackRowItem { + if contentNode == nil || !contentNode!.isKind(of: item.contentNode()) { + self.contentNode?.removeFromSuperview() + let node = item.contentNode() + self.contentNode = node.init(frame:NSZeroRect) + self.addSubview(self.contentNode!) + } + + var file: TelegramMediaFile? + if let thumbnail = item.info.thumbnail { + file = TelegramMediaFile(fileId: MediaId(namespace: 0, id: item.info.id.id), partialReference: nil, resource: thumbnail.resource, previewRepresentations: [thumbnail], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/x-tgsticker", size: nil, attributes: [.FileName(fileName: "sticker.tgs"), .Sticker(displayText: "", packReference: .id(id: item.info.id.id, accessHash: item.info.accessHash), maskData: nil)]) + } else if let item = item.topItem { + file = item.file + } + self.contentNode?.userInteractionEnabled = false + self.contentNode?.isEventLess = true + if let file = file { + self.contentNode?.update(with: file, size: NSMakeSize(30, 30), context: item.context, parent: nil, table: item.table, parameters: nil, animated: animated, positionFlags: nil, approximateSynchronousValue: false) + } + + } + + overlay.set(image: theme.icons.stickerPackSelection, for: .Normal) + overlay.set(image: theme.icons.stickerPackSelectionActive, for: .Highlight) + + overlay.isSelected = item.isSelected + + super.set(item: item, animated: animated) + + needsLayout = true + } + + override func layout() { + super.layout() + + self.contentNode?.center() + overlay.center() + } +} diff --git a/Telegram-Mac/StickerPackPanelRowItem.swift b/Telegram-Mac/StickerPackPanelRowItem.swift new file mode 100644 index 0000000000..e0883a796e --- /dev/null +++ b/Telegram-Mac/StickerPackPanelRowItem.swift @@ -0,0 +1,489 @@ +// +// StickerPackPanelRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 08/07/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + + +class StickerPackPanelRowItem: TableRowItem { + let files: [(TelegramMediaFile, ChatMediaContentView.Type, NSPoint)] + let packNameLayout: TextViewLayout? + let context: AccountContext + let arguments: StickerPanelArguments + let namePoint: NSPoint + let packInfo: StickerPackInfo + let collectionId: StickerPackCollectionId + + private let _height: CGFloat + override var stableId: AnyHashable { + return collectionId + } + let packReference: StickerPackReference? + + private let preloadFeaturedDisposable = MetaDisposable() + + init(_ initialSize: NSSize, context: AccountContext, arguments: StickerPanelArguments, files:[TelegramMediaFile], packInfo: StickerPackInfo, collectionId: StickerPackCollectionId) { + self.context = context + self.arguments = arguments + var filesAndPoints:[(TelegramMediaFile, ChatMediaContentView.Type, NSPoint)] = [] + let size: NSSize = NSMakeSize(60, 60) + + + let title: String? + var count: Int32 = 0 + switch packInfo { + case let .pack(info, _, _): + title = info?.title ?? info?.shortName ?? "" + count = info?.count ?? 0 + if let info = info { + self.packReference = .id(id: info.id.id, accessHash: info.accessHash) + } else { + self.packReference = nil + } + case .recent: + title = L10n.stickersRecent + self.packReference = nil + case .saved: + title = nil + self.packReference = nil + case .emojiRelated: + title = nil + self.packReference = nil + case let .speficicPack(info): + title = info?.title ?? info?.shortName ?? "" + if let info = info { + self.packReference = .id(id: info.id.id, accessHash: info.accessHash) + } else { + self.packReference = nil + } + } + + if let title = title { + let attributed = NSMutableAttributedString() + if packInfo.featured { + _ = attributed.append(string: title.uppercased(), color: theme.colors.text, font: .medium(14)) + _ = attributed.append(string: "\n") + _ = attributed.append(string: L10n.stickersCountCountable(Int(count)), color: theme.colors.grayText, font: .normal(12)) + } else { + _ = attributed.append(string: title.uppercased(), color: theme.colors.grayText, font: .medium(.text)) + } + let layout = TextViewLayout(attributed, alwaysStaticItems: true) + layout.measure(width: 300) + self.packNameLayout = layout + + self.namePoint = NSMakePoint(10, floorToScreenPixels(System.backingScale, ((packInfo.featured ? 50 : 30) - layout.layoutSize.height) / 2)) + } else { + namePoint = NSZeroPoint + self.packNameLayout = nil + } + + + + var point: NSPoint = NSMakePoint(5, title == nil ? 5 : !packInfo.featured ? 35 : 55) + for (i, file) in files.enumerated() { + filesAndPoints.append((file, ChatLayoutUtils.contentNode(for: file, packs: true), point)) + point.x += size.width + 10 + if (i + 1) % 5 == 0 { + point.y += size.height + 5 + point.x = 5 + } + } + + self.files = filesAndPoints + self.packInfo = packInfo + self.collectionId = collectionId + + let rows = ceil((CGFloat(files.count) / 5.0)) + _height = (title == nil ? 0 : !packInfo.featured ? 30 : 50) + 60.0 * rows + ((rows + 1) * 5) + + + + if packInfo.featured, let id = collectionId.itemCollectionId { + preloadFeaturedDisposable.set(preloadedFeaturedStickerSet(network: context.account.network, postbox: context.account.postbox, id: id).start()) + } + + super.init(initialSize) + + } + + override func menuItems(in location: NSPoint) -> Signal<[ContextMenuItem], NoError> { + var items:[ContextMenuItem] = [] + let context = self.context + if arguments.mode != .common { + return .single([]) + } + for file in files { + let rect = NSMakeRect(file.2.x, file.2.y, 60, 60) + let file = file.0 + if NSPointInRect(location, rect) { + inner: switch packInfo { + case .saved, .recent: + if let reference = file.stickerReference { + items.append(ContextMenuItem(L10n.contextViewStickerSet, handler: { [weak self] in + self?.arguments.showPack(reference) + })) + } + default: + break inner + } + inner: switch packInfo { + case .saved: + if let mediaId = file.id { + items.append(ContextMenuItem(L10n.contextRemoveFaveSticker, handler: { + _ = removeSavedSticker(postbox: context.account.postbox, mediaId: mediaId).start() + })) + } + default: + if packInfo.installed { + items.append(ContextMenuItem(L10n.chatContextAddFavoriteSticker, handler: { + _ = addSavedSticker(postbox: context.account.postbox, network: context.account.network, file: file).start() + })) + } + } + items.append(ContextMenuItem(L10n.chatSendWithoutSound, handler: { [weak self] in + guard let `self` = self else { + return + } + let contentView = (self.view as? StickerPackPanelRowView)?.subviews.compactMap { $0 as? ChatMediaContentView}.first(where: { view -> Bool in + return view.media?.isEqual(to: file) ?? false + }) + + if let contentView = contentView { + self.arguments.sendMedia(file, contentView, true) + } + })) + + break + } + } + return .single(items) + } + + deinit { + preloadFeaturedDisposable.dispose() + NotificationCenter.default.removeObserver(self) + } + + override var height: CGFloat { + return _height + } + + override func viewClass() -> AnyClass { + return StickerPackPanelRowView.self + } +} + +private final class StickerPackPanelRowView : TableRowView, ModalPreviewRowViewProtocol { + + func fileAtPoint(_ point: NSPoint) -> (QuickPreviewMedia, NSView?)? { + for subview in self.subviews { + if let contentView = subview as? ChatMediaContentView { + if NSPointInRect(point, subview.frame) { + if let file = contentView.media as? TelegramMediaFile { + let reference = file.stickerReference != nil ? FileMediaReference.stickerPack(stickerPack: file.stickerReference!, media: file) : FileMediaReference.standalone(media: file) + if file.isStaticSticker { + return (.file(reference, StickerPreviewModalView.self), contentView) + } else if file.isAnimatedSticker { + return (.file(reference, AnimatedStickerPreviewModalView.self), contentView) + } + } + } + } + + } + return nil + } + + private var contentViews:[Optional] = [] + private let packNameView = TextView() + private var clearRecentButton: ImageButton? + private var addButton:TitleButton? + private let longDisposable = MetaDisposable() + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(packNameView) + packNameView.userInteractionEnabled = false + packNameView.isSelectable = false + wantsLayer = false + + } + private var isMouseDown: Bool = false + + override func mouseDown(with event: NSEvent) { + super.mouseDown(with: event) + longDisposable.set(nil) + + self.isMouseDown = true + + guard event.clickCount == 1 else { + return + } + + let point = convert(event.locationInWindow, from: nil) + for subview in self.subviews { + if NSPointInRect(point, subview.frame) { + if subview is ChatMediaContentView { + let signal = Signal.complete() |> delay(0.2, queue: .mainQueue()) + longDisposable.set(signal.start(completed: { [weak self] in + if let `self` = self, self.mouseInside(), + let item = self.item as? StickerPackPanelRowItem, + let table = item.table, + let window = self.window as? Window { + startModalPreviewHandle(table, window: window, context: item.context) + } + })) + } + return + } + } + + } + + override func mouseUp(with event: NSEvent) { + //super.mouseUp(with: event) + longDisposable.set(nil) + if isMouseDown, mouseInside(), event.clickCount == 1 { + let point = convert(event.locationInWindow, from: nil) + + if let item = item as? StickerPackPanelRowItem { + if self.packNameView.mouseInside() { + if let reference = item.packReference { + item.arguments.showPack(reference) + } + } else { + for subview in self.subviews { + if NSPointInRect(point, subview.frame) { + if let contentView = subview as? ChatMediaContentView, let media = contentView.media { + if let reference = item.packReference, item.packInfo.featured { + item.arguments.showPack(reference) + } else { + item.arguments.sendMedia(media, contentView, false) + } + } + break + } + } + } + } + } + isMouseDown = false + } + deinit { + longDisposable.dispose() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var previousRange: (Int, Int) = (0, 0) + private var isCleaned: Bool = false + + override func layout() { + super.layout() + + guard let item = item as? StickerPackPanelRowItem else { + return + } + packNameView.setFrameOrigin(item.namePoint) + + self.clearRecentButton?.setFrameOrigin(frame.width - 34, item.namePoint.y - 10) + + updateVisibleItems() + } + + override func setFrameSize(_ newSize: NSSize) { + super.setFrameSize(newSize) + updateVisibleItems() + } + + override var backdorColor: NSColor { + return .clear + } + + override func viewDidMoveToSuperview() { + super.viewDidMoveToSuperview() + updateVisibleItems() + } + + @objc func updateVisibleItems() { + + guard let item = item as? StickerPackPanelRowItem else { + return + } + + let size: NSSize = NSMakeSize(60, 60) + + let visibleRect = NSMakeRect(0, self.visibleRect.minY - 120, self.visibleRect.width, self.visibleRect.height + 240) + + if self.visibleRect != NSZeroRect && superview != nil && window != nil { + let visibleRange = (Int(ceil(visibleRect.minY / (size.height + 10))), Int(ceil(visibleRect.height / (size.height + 10)))) + if visibleRange != self.previousRange { + self.previousRange = visibleRange + isCleaned = false + } else { + return + } + } else { + self.previousRange = (0, 0) + CATransaction.begin() + if !isCleaned { + for (i, view) in self.contentViews.enumerated() { + view?.removeFromSuperview() + self.contentViews[i] = nil + } + } + isCleaned = true + CATransaction.commit() + return + } + + + CATransaction.begin() + + var unused:[ChatMediaContentView] = [] + for (i, data) in item.files.enumerated() { + let file = data.0 + let point = data.2 + let viewType = data.1 + if NSPointInRect(point, visibleRect) { + var view: ChatMediaContentView + if self.contentViews[i] == nil || !self.contentViews[i]!.isKind(of: viewType) { + if unused.isEmpty { + view = viewType.init(frame: NSZeroRect) + } else { + view = unused.removeFirst() + } + self.contentViews[i] = view + } else { + view = self.contentViews[i]! + } + if view.media?.id != file.id { + view.update(with: file, size: size, context: item.context, parent: nil, table: item.table) + } + view.userInteractionEnabled = false + view.setFrameOrigin(point) + + } else { + if let view = self.contentViews[i] { + unused.append(view) + self.contentViews[i] = nil + } + } + } + + for view in unused { + view.clean() + view.removeFromSuperview() + } + + self.subviews = (self.clearRecentButton != nil ? [self.clearRecentButton!] : []) + (self.addButton != nil ? [self.addButton!] : []) + [self.packNameView] + self.contentViews.compactMap { $0 } + + CATransaction.commit() + + + } + + override func viewDidMoveToWindow() { + if window == nil { + NotificationCenter.default.removeObserver(self) + } else { + NotificationCenter.default.addObserver(self, selector: #selector(updateVisibleItems), name: NSView.boundsDidChangeNotification, object: self.enclosingScrollView?.contentView) + } + updateVisibleItems() + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? StickerPackPanelRowItem else { + return + } + + packNameView.update(item.packNameLayout) + + if item.arguments.mode == .common { + switch item.packInfo { + case .recent: + if self.clearRecentButton == nil { + self.clearRecentButton = ImageButton() + addSubview(self.clearRecentButton!) + } + self.clearRecentButton?.set(image: theme.icons.wallpaper_color_close, for: .Normal) + _ = self.clearRecentButton?.sizeToFit(NSMakeSize(5, 5), thatFit: false) + + self.clearRecentButton?.removeAllHandlers() + + self.clearRecentButton?.set(handler: { [weak item] _ in + item?.arguments.clearRecent() + }, for: .Click) + default: + self.clearRecentButton?.removeFromSuperview() + self.clearRecentButton = nil + } + } else { + self.clearRecentButton?.removeFromSuperview() + self.clearRecentButton = nil + } + + + self.previousRange = (0, 0) + + while self.contentViews.count > item.files.count { + self.contentViews.removeLast() + } + while self.contentViews.count < item.files.count { + self.contentViews.append(nil) + } + + self.addButton?.removeFromSuperview() + self.addButton = nil + + if let reference = item.packReference, item.packInfo.featured { + if !item.packInfo.installed { + self.addButton = TitleButton() + self.addButton!.set(background: theme.colors.accentSelect, for: .Normal) + self.addButton!.set(background: theme.colors.accentSelect.withAlphaComponent(0.8), for: .Highlight) + self.addButton!.set(font: .medium(.text), for: .Normal) + self.addButton!.set(color: theme.colors.underSelectedColor, for: .Normal) + self.addButton!.set(text: L10n.stickersSearchAdd, for: .Normal) + _ = self.addButton!.sizeToFit(NSMakeSize(14, 8), thatFit: true) + self.addButton!.layer?.cornerRadius = .cornerRadius + self.addButton!.setFrameOrigin(frame.width - self.addButton!.frame.width - 10, 13) + + self.addButton!.set(handler: { [weak item] _ in + item?.arguments.addPack(reference) + }, for: .Click) + } else { + self.addButton = TitleButton() + self.addButton!.set(background: theme.colors.grayForeground, for: .Normal) + self.addButton!.set(background: theme.colors.grayForeground.withAlphaComponent(0.8), for: .Highlight) + self.addButton!.set(font: .medium(.text), for: .Normal) + self.addButton!.set(color: theme.colors.underSelectedColor, for: .Normal) + self.addButton!.set(text: L10n.stickersSearchAdded, for: .Normal) + _ = self.addButton!.sizeToFit(NSMakeSize(14, 8), thatFit: true) + self.addButton!.layer?.cornerRadius = .cornerRadius + self.addButton!.setFrameOrigin(frame.width - self.addButton!.frame.width - 10, 13) + + self.addButton!.set(handler: { [weak item] _ in + if let item = item { + item.arguments.removePack(item.collectionId) + } + }, for: .Click) + } + } + + updateVisibleItems() + } + +} diff --git a/Telegram-Mac/StickerPackPreviewModalController.swift b/Telegram-Mac/StickerPackPreviewModalController.swift new file mode 100644 index 0000000000..947a2f0728 --- /dev/null +++ b/Telegram-Mac/StickerPackPreviewModalController.swift @@ -0,0 +1,294 @@ +// +// StickerPackPreviewModalController.swift +// Telegram +// +// Created by keepcoder on 27/02/2017. +// Copyright © 2017 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import Postbox +import TelegramCore + +import SwiftSignalKit + + + + +final class StickerPackArguments { + let context: AccountContext + let send:(TelegramMediaFile, NSView)->Void + let addpack:(StickerPackCollectionInfo, [ItemCollectionItem], Bool)->Void + let share:(String)->Void + let close:()->Void + init(context: AccountContext, send:@escaping(Media, NSView)->Void, addpack:@escaping(StickerPackCollectionInfo, [ItemCollectionItem], Bool)->Void, share:@escaping(String)->Void, close:@escaping()->Void) { + self.context = context + self.send = send + self.addpack = addpack + self.share = share + self.close = close + } +} + + + +private class StickersModalView : View { + private let grid:GridNode = GridNode(frame: NSZeroRect) + private let add:TitleButton = TitleButton() + private let shareView:ImageButton = ImageButton() + private let close: ImageButton = ImageButton() + private let headerTitle:TextView = TextView() + private let headerSeparatorView:View = View() + private let dismiss:ImageButton = ImageButton() + private var indicatorView:ProgressIndicator? + private let shadowView: ShadowView = ShadowView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + backgroundColor = theme.colors.background + addSubview(grid) + //addSubview(interactionView) + addSubview(headerTitle) + addSubview(shareView) + addSubview(close) + addSubview(headerSeparatorView) + addSubview(dismiss) + + shadowView.shadowBackground = theme.colors.background + shadowView.setFrameSize(frame.width, 70) + + addSubview(shadowView) + + dismiss.set(image: theme.icons.stickerPackDelete, for: .Normal) + _ = dismiss.sizeToFit() + add.disableActions() + add.setFrameSize(170, 40) + add.layer?.cornerRadius = 20 + + add.set(color: theme.colors.underSelectedColor, for: .Normal) + add.set(font: .medium(.title), for: .Normal) + add.set(background: theme.colors.accent, for: .Normal) + add.set(background: theme.colors.accent, for: .Hover) + add.set(background: theme.colors.accent, for: .Highlight) + add.set(text: L10n.stickerPackAdd1Countable(0), for: .Normal) + + addSubview(add) + headerTitle.backgroundColor = theme.colors.background + headerSeparatorView.backgroundColor = theme.colors.border + + shareView.set(image: theme.icons.stickersShare, for: .Normal) + close.set(image: theme.icons.stickerPackClose, for: .Normal) + _ = shareView.sizeToFit() + _ = close.sizeToFit() + + + } + + + func layout(with result: LoadedStickerPack, arguments: StickerPackArguments) -> Void { + + + switch result { + case .none: + break + case .fetching: + dismiss.isHidden = true + shareView.isHidden = true + if self.indicatorView == nil { + self.indicatorView = ProgressIndicator(frame: NSMakeRect(0, 0, 30, 30)) + addSubview(self.indicatorView!) + } + self.indicatorView?.center() + add.isHidden = true + shadowView.isHidden = true + case let .result(info: info, items: collectionItems, installed: installed): + if let indicatorView = self.indicatorView { + self.indicatorView = nil + indicatorView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak indicatorView] _ in + indicatorView?.removeFromSuperview() + }) + } + dismiss.isHidden = !installed + shareView.isHidden = false + add.set(text: tr(L10n.stickerPackAdd1Countable(collectionItems .count)).uppercased(), for: .Normal) + _ = add.sizeToFit(NSMakeSize(20, 0), NSMakeSize(frame.width - 40, 40), thatFit: false) + add.isHidden = installed + shadowView.isHidden = installed + let attr = NSMutableAttributedString() + + _ = attr.append(string: info.title, color: theme.colors.text, font: .medium(16.0)) + attr.detectLinks(type: [.Mentions], context: arguments.context, color: theme.colors.accent, openInfo: { (peerId, _, _, _) in + _ = (arguments.context.account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue).start(next: { peer in + arguments.close() + if peer.isUser || peer.isBot { + arguments.context.sharedContext.bindings.rootNavigation().push(PeerInfoController(context: arguments.context, peerId: peerId)) + } else { + arguments.context.sharedContext.bindings.rootNavigation().push(ChatAdditionController(context: arguments.context, chatLocation: .peer(peer.id))) + } + }) + }) + let layout = TextViewLayout(attr, maximumNumberOfLines: 2, alignment: .center) + layout.interactions = globalLinkExecutor + + + layout.measure(width: frame.width - 160) + headerTitle.update(layout) + + let items = collectionItems.filter({ item -> Bool in + return item is StickerPackItem + }).map ({ item -> StickerPackGridItem in + return StickerPackGridItem(context: arguments.context, file: (item as! StickerPackItem).file, send: arguments.send, selected: {}) + }) + + var insert:[GridNodeInsertItem] = [] + + for index in 0 ..< items.count { + insert.append(GridNodeInsertItem(index: index, item: items[index], previousIndex: nil)) + } + + grid.removeAllItems() + + grid.transaction(GridNodeTransaction(deleteItems: [], insertItems: insert, updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: CGSize(width: frame.width, height: frame.height), insets: NSEdgeInsets(left: 0, right: 0, top: 10, bottom: installed ? 0 : 60), preloadSize: self.bounds.width, type: .fixed(itemSize: CGSize(width: 70, height: 70), lineSpacing: 10)), transition: .immediate), itemTransition: .immediate, stationaryItems: .all, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + + grid.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + self.needsLayout = true + + + + shareView.set(handler: { _ in + arguments.share("https://t.me/addstickers/\(info.shortName)") + }, for: .SingleClick) + + add.removeAllHandlers() + dismiss.removeAllHandlers() + close.removeAllHandlers() + + func action(_ control:Control) { + arguments.addpack(info, collectionItems, installed) + } + + + add.set(handler: action, for: .SingleClick) + dismiss.set(handler: action, for: .SingleClick) + + close.set(handler: { _ in + arguments.close() + }, for: .Click) + + } + + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + + let headerHeight:CGFloat = 50 + + grid.frame = NSMakeRect(0, headerHeight, frame.width, frame.height - headerHeight) + + headerTitle.centerX(y : floorToScreenPixels(backingScaleFactor, (headerHeight - headerTitle.frame.height)/2) + 1) + headerSeparatorView.frame = NSMakeRect(0, headerHeight - .borderSize, frame.width, .borderSize) + shareView.setFrameOrigin(frame.width - close.frame.width - 12, floorToScreenPixels(backingScaleFactor, (headerHeight - shareView.frame.height)/2)) + close.setFrameOrigin(12, floorToScreenPixels(backingScaleFactor, (headerHeight - shareView.frame.height)/2)) + add.centerX(y: frame.height - add.frame.height - 15) + dismiss.setFrameOrigin(NSMakePoint(shareView.frame.minX - dismiss.frame.width - 15, floorToScreenPixels(backingScaleFactor, (headerHeight - shareView.frame.height)/2))) + + shadowView.setFrameOrigin(0, frame.height - shadowView.frame.height) + } +} + + + +class StickerPackPreviewModalController: ModalViewController { + private let context:AccountContext + private let peerId:PeerId? + private let reference:StickerPackReference + private let disposable: MetaDisposable = MetaDisposable() + private var arguments:StickerPackArguments! + private var onAdd:(()->Void)? = nil + init(_ context: AccountContext, peerId:PeerId?, reference:StickerPackReference, onAdd:(()->Void)? = nil) { + self.context = context + self.peerId = peerId + self.reference = reference + self.onAdd = onAdd + super.init(frame: NSMakeRect(0, 0, 350, 400)) + bar = .init(height: 0) + arguments = StickerPackArguments(context: context, send: { [weak self] media, view in + let interactions = (context.sharedContext.bindings.rootNavigation().controller as? ChatController)?.chatInteraction + + if let interactions = interactions, let media = media as? TelegramMediaFile, media.maskData == nil { + if let slowMode = interactions.presentation.slowMode, slowMode.hasLocked { + showSlowModeTimeoutTooltip(slowMode, for: view) + } else { + interactions.sendAppFile(media, false, nil) + self?.close() + } + } + }, addpack: { [weak self] info, items, installed in + self?.close() + self?.disposable.dispose() + if !installed { + _ = context.engine.stickers.addStickerPackInteractively(info: info, items: items).start() + } else { + _ = context.engine.stickers.removeStickerPackInteractively(id: info.id, option: .archive).start() + } + self?.onAdd?() + + }, share: { [weak self] link in + self?.close() + showModal(with: ShareModalController(ShareLinkObject(context, link: link)), for: context.window) + }, close: { [weak self] in + self?.close() + }) + } + + fileprivate var genericView:StickersModalView { + return self.view as! StickersModalView + } + + override func viewClass() -> AnyClass { + return StickersModalView.self + } + + + override var dynamicSize: Bool { + return true + } + + + override func measure(size: NSSize) { + // self.modal?.resize(with:NSMakeSize(genericView.frame.width, min(size.height - 70, genericView.listHeight)), animated: false) + } + + + override func viewDidLoad() { + super.viewDidLoad() + + + + disposable.set((context.engine.stickers.loadedStickerPack(reference: reference, forceActualized: true) |> deliverOnMainQueue).start(next: { [weak self] result in + guard let `self` = self else {return} + switch result { + case .none: + alert(for: mainWindow, info: L10n.stickerSetDontExist) + self.close() + default: + self.genericView.layout(with: result, arguments: self.arguments) + self.readyOnce() + } + + })) + + } + + + deinit { + disposable.dispose() + } + +} diff --git a/Telegram-Mac/StickerPackTrendingItem.swift b/Telegram-Mac/StickerPackTrendingItem.swift new file mode 100644 index 0000000000..c96d329e21 --- /dev/null +++ b/Telegram-Mac/StickerPackTrendingItem.swift @@ -0,0 +1,384 @@ +// +// StickerPackTrendingItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 12.08.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import TelegramCore +import SwiftSignalKit +import Postbox + +private final class FeaturedHorizontalItem : TableRowItem { + + fileprivate let item: FeaturedStickerPackItem + fileprivate let context: AccountContext + fileprivate let click:(FeaturedStickerPackItem)->Void + init(_ initialSize: NSSize, context: AccountContext, item: FeaturedStickerPackItem, click:@escaping(FeaturedStickerPackItem)->Void) { + self.item = item + self.click = click + self.context = context + super.init(initialSize) + } + + var unread: Bool { + return item.unread + } + + override var stableId: AnyHashable { + return item.topItems.first?.file.id?.id ?? arc4random64() + } + override var height: CGFloat { + return 30 + } + override var width: CGFloat { + return 30 + } + func contentNode()->ChatMediaContentView.Type { + if let file = item.topItems.first?.file, file.isAnimatedSticker { + return MediaAnimatedStickerView.self + } else { + return ChatStickerContentView.self + } + } + + override func viewClass() -> AnyClass { + if let file = item.topItems.first?.file, file.isAnimatedSticker { + return FeaturedAnimatedHorizontalView.self + } else { + return FeaturedHorizontalView.self + } + } +} + +private final class FeaturedAnimatedHorizontalView : HorizontalRowView { + + private let unread: View = View(frame: NSMakeRect(0, 0, 6, 6)) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(unread) + unread.layer?.cornerRadius = 3 + + } + + override func mouseUp(with event: NSEvent) { + super.mouseUp(with: event) + if let item = self.item as? FeaturedHorizontalItem { + item.click(item.item) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + fileprivate(set) var contentNode:ChatMediaContentView? + + + override var backgroundColor: NSColor { + didSet { + contentNode?.backgroundColor = backdorColor + } + } + + override var backdorColor: NSColor { + return .clear + } + + override func shakeView() { + contentNode?.shake() + } + + + override func updateMouse() { + super.updateMouse() + self.contentNode?.updateMouse() + } + + + override func viewWillMove(toSuperview newSuperview: NSView?) { + if newSuperview == nil { + self.contentNode?.willRemove() + } + } + + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + if window == nil { + contentNode?.removeFromSuperview() + contentNode = nil + } else if let item = item, contentNode == nil { + self.set(item: item, animated: false) + } + } + + override func set(item:TableRowItem, animated:Bool = false) { + if let item = item as? FeaturedHorizontalItem { + if contentNode == nil || !contentNode!.isKind(of: item.contentNode()) { + self.contentNode?.removeFromSuperview() + let node = item.contentNode() + self.contentNode = node.init(frame:NSZeroRect) + self.addSubview(self.contentNode!) + } + unread.isHidden = !item.unread + unread.backgroundColor = theme.colors.accent + + var file: TelegramMediaFile? + if let thumbnail = item.item.info.thumbnail { + file = TelegramMediaFile(fileId: MediaId(namespace: 0, id: item.item.info.id.id), partialReference: nil, resource: thumbnail.resource, previewRepresentations: [thumbnail], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/x-tgsticker", size: nil, attributes: [.FileName(fileName: "sticker.tgs"), .Sticker(displayText: "", packReference: .id(id: item.item.info.id.id, accessHash: item.item.info.accessHash), maskData: nil)]) + } else if let item = item.item.topItems.first { + file = item.file + } + self.contentNode?.userInteractionEnabled = false + self.contentNode?.isEventLess = true + if let file = file { + self.contentNode?.update(with: file, size: NSMakeSize(25, 25), context: item.context, parent: nil, table: item.table, parameters: nil, animated: animated, positionFlags: nil, approximateSynchronousValue: false) + } + + } + + + super.set(item: item, animated: animated) + + needsLayout = true + } + + override func layout() { + super.layout() + + self.contentNode?.center() + unread.setFrameOrigin(NSMakePoint(frame.width - unread.frame.width, 0)) + } +} + +private final class FeaturedHorizontalView : HorizontalRowView { + + private let stickerFetchedDisposable = MetaDisposable() + private var imageView:TransformImageView? + + private let unread: View = View(frame: NSMakeRect(0, 0, 6, 6)) + + required init(frame frameRect:NSRect) { + super.init(frame:frameRect) + + addSubview(unread) + unread.layer?.cornerRadius = 3 + + } + + override func mouseUp(with event: NSEvent) { + super.mouseUp(with: event) + if let item = self.item as? FeaturedHorizontalItem { + item.click(item.item) + } + } + + override var backdorColor: NSColor { + return .clear + } + + override func layout() { + super.layout() + + self.imageView?.center() + unread.setFrameOrigin(NSMakePoint(frame.width - unread.frame.width, 0)) + } + + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + if window == nil { + self.imageView?.removeFromSuperview() + self.imageView = nil + } else if let item = item, self.imageView == nil { + self.set(item: item, animated: false) + } + } + + deinit { + stickerFetchedDisposable.dispose() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func set(item:TableRowItem, animated:Bool = false) { + + super.set(item: item, animated: animated) + + guard let context = (item as? FeaturedHorizontalItem)?.context else { + return + } + + guard let rowItem = item as? FeaturedHorizontalItem else { + return + } + + unread.isHidden = !rowItem.unread + unread.backgroundColor = theme.colors.accent + + let item = rowItem.item + + var thumbnailItem: TelegramMediaImageRepresentation? + var resourceReference: MediaResourceReference? + + var file: TelegramMediaFile? + + + if let thumbnail = item.info.thumbnail { + thumbnailItem = thumbnail + resourceReference = MediaResourceReference.stickerPackThumbnail(stickerPack: .id(id: item.info.id.id, accessHash: item.info.accessHash), resource: thumbnail.resource) + file = TelegramMediaFile(fileId: MediaId(namespace: 0, id: item.info.id.id), partialReference: nil, resource: thumbnail.resource, previewRepresentations: [thumbnail], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: nil, attributes: [.FileName(fileName: "sticker.webp"), .Sticker(displayText: "", packReference: .id(id: item.info.id.id, accessHash: item.info.accessHash), maskData: nil)]) + } else if let item = item.topItems.first, let dimensions = item.file.dimensions, let resource = chatMessageStickerResource(file: item.file, small: true) as? TelegramMediaResource { + thumbnailItem = TelegramMediaImageRepresentation(dimensions: dimensions, resource: resource, progressiveSizes: [], immediateThumbnailData: nil) + resourceReference = MediaResourceReference.media(media: .standalone(media: item.file), resource: resource) + file = item.file + } + + if self.imageView == nil { + self.imageView = TransformImageView() + self.addSubview(self.imageView!) + } + guard let imageView = self.imageView else { + return + } + + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: NSMakeSize(25, 25), boundingSize: NSMakeSize(25, 25), intrinsicInsets: NSEdgeInsets()) + + if let thumbnailItem = thumbnailItem { + if let file = file { + imageView.setSignal(signal: cachedMedia(media: file , arguments: arguments, scale: backingScaleFactor)) + } + if !imageView.isFullyLoaded { + imageView.setSignal(chatMessageStickerPackThumbnail(postbox: context.account.postbox, representation: thumbnailItem, scale: backingScaleFactor, synchronousLoad: false), cacheImage: { result in + if let file = file { + cacheMedia(result, media: file, arguments: arguments, scale: System.backingScale) + } + }) + } + } + imageView.set(arguments:arguments) + imageView.setFrameSize(arguments.imageSize) + if let resourceReference = resourceReference { + stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: resourceReference, statsCategory: .file).start()) + } + self.needsLayout = true + + } +} + +final class StickerPackTrendingItem : GeneralRowItem { + fileprivate let collectionId: StickerPackCollectionId + fileprivate let featured:[FeaturedStickerPackItem] + fileprivate let items: [FeaturedHorizontalItem] + fileprivate let close: (Int64)->Void + init(_ initialSize: NSSize, context: AccountContext, featured: [FeaturedStickerPackItem], collectionId: StickerPackCollectionId, close: @escaping(Int64)->Void, click:@escaping(FeaturedStickerPackItem)->Void) { + self.collectionId = collectionId + self.featured = featured + self.close = close + var items:[FeaturedHorizontalItem] = [] + for featured in featured { + items.append(.init(NSMakeSize(30, 30), context: context, item: featured, click: click)) + } + self.items = items + super.init(initialSize, height: 50, stableId: collectionId) + } + + + override func viewClass() -> AnyClass { + return StickerPackTrendingView.self + } +} + +private final class HorizontalInsetItem : TableRowItem { + override var width: CGFloat { + return 8 + } + override var height: CGFloat { + return 8 + } + override var stableId: AnyHashable { + return arc4random() + } + override func viewClass() -> AnyClass { + return HorizontalInsetView.self + } +} +private final class HorizontalInsetView: HorizontalRowView { + +} + +private final class StickerPackTrendingView : TableRowView { + private let tableView: HorizontalTableView = HorizontalTableView(frame: .zero, isFlipped: true, bottomInset: 0, drawBorder: false) + private let textView = TextView() + private let close: ImageButton = ImageButton() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(tableView) + addSubview(textView) + addSubview(close) +// close.autohighlight = false +// close.scaleOnClick = true + tableView.getBackgroundColor = { + .clear + } + + self.close.set(handler: { [weak self] _ in + if let item = self?.item as? StickerPackTrendingItem { + if let id = item.featured.first?.info.id.id { + item.close(id) + } + } + }, for: .Click) + } + override func mouseUp(with event: NSEvent) { + super.mouseUp(with: event) + } + + override func updateColors() { + super.updateColors() + } + override var backdorColor: NSColor { + return theme.colors.background + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? StickerPackTrendingItem else { + return + } + + self.close.set(image: theme.icons.wallpaper_color_close, for: .Normal) + _ = self.close.sizeToFit() + + + let layout = TextViewLayout.init(.initialize(string: L10n.stickersTrending.uppercased(), color: theme.colors.grayText, font: .medium(.text))) + layout.measure(width: frame.width - 30) + textView.update(layout) + + tableView.beginTableUpdates() + tableView.removeAll() + _ = tableView.addItem(item: HorizontalInsetItem(.zero), animation: animated ? .effectFade : .none) + for item in item.items { + _ = tableView.addItem(item: item, animation: animated ? .effectFade : .none) + } + _ = tableView.addItem(item: HorizontalInsetItem(.zero), animation: animated ? .effectFade : .none) + tableView.endTableUpdates() + + } + + override func layout() { + super.layout() + textView.setFrameOrigin(NSMakePoint(10, 0)) + close.setFrameOrigin(NSMakePoint(frame.width - close.frame.width, -8)) + tableView.frame = NSMakeRect(0, frame.height - 30, frame.width, 30) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/StickerPreviewHandler.swift b/Telegram-Mac/StickerPreviewHandler.swift index 815b15f356..d2b4a57dd1 100644 --- a/Telegram-Mac/StickerPreviewHandler.swift +++ b/Telegram-Mac/StickerPreviewHandler.swift @@ -1,5 +1,5 @@ // -// StickerPreviewHandler.swift +// ModalPreviewHandler.swift // Telegram // // Created by keepcoder on 02/02/2017. @@ -7,33 +7,79 @@ // import Cocoa -import TelegramCoreMac +import TelegramCore + import TGUIKit -import SwiftSignalKitMac +import SwiftSignalKit + +enum QuickPreviewMedia : Equatable { + case file(FileMediaReference, ModalPreviewControllerView.Type) + case image(ImageMediaReference, ModalPreviewControllerView.Type) + + static func ==(lhs: QuickPreviewMedia, rhs: QuickPreviewMedia) -> Bool { + switch lhs { + case let .file(lhsReference, _): + if case let .file(rhsReference, _) = rhs { + return lhsReference.media.isEqual(to: rhsReference.media) + } else { + return false + } + case let .image(lhsReference, _): + if case let .image(rhsReference, _) = rhs { + return lhsReference.media.isEqual(to: rhsReference.media) + } else { + return false + } + } + } + + var fileReference: FileMediaReference? { + switch self { + case let .file(reference, _): + return reference + default: + return nil + } + } + var imageReference: ImageMediaReference? { + switch self { + case let .image(reference, _): + return reference + default: + return nil + } + } + + var viewType: ModalPreviewControllerView.Type { + switch self { + case let .file(_, type), let .image(_, type): + return type + } + } +} -extension GridNode : StickerPreviewProtocol { - func stickerAtLocationInWindow(_ point: NSPoint) -> TelegramMediaFile? { +extension GridNode : ModalPreviewProtocol { + func fileAtLocationInWindow(_ point: NSPoint) -> (QuickPreviewMedia, NSView?)? { let point = self.documentView!.convert(point, from: nil) - var file:TelegramMediaFile? = nil + var reference: (QuickPreviewMedia, NSView?)? = nil self.forEachItemNode { node in if NSPointInRect(point, node.frame) { - if let c = node as? StickerPreviewRowViewProtocol { - file = c.fileAtPoint(node.convert(point, from: nil)) - return + if let c = node as? ModalPreviewRowViewProtocol { + reference = c.fileAtPoint(node.convert(point, from: nil)) } } } - return file + return reference } } -extension TableView : StickerPreviewProtocol { - func stickerAtLocationInWindow(_ point: NSPoint) -> TelegramMediaFile? { +extension TableView : ModalPreviewProtocol { + func fileAtLocationInWindow(_ point: NSPoint) ->(QuickPreviewMedia, NSView?)? { let index = self.row(at: documentView!.convert(point, from: nil)) if index != -1 { let item = self.item(at: index) - if let view = self.viewNecessary(at: item.index), let c = view as? StickerPreviewRowViewProtocol { + if let view = self.viewNecessary(at: item.index), let c = view as? ModalPreviewRowViewProtocol { return c.fileAtPoint(view.convert(point, from: nil)) } } @@ -42,60 +88,197 @@ extension TableView : StickerPreviewProtocol { } } -protocol StickerPreviewRowViewProtocol { - func fileAtPoint(_ point:NSPoint) -> TelegramMediaFile? +protocol ModalPreviewRowViewProtocol { + func fileAtPoint(_ point:NSPoint) -> (QuickPreviewMedia, NSView?)? } -protocol StickerPreviewProtocol { - func stickerAtLocationInWindow(_ point:NSPoint) -> TelegramMediaFile? +protocol ModalPreviewProtocol { + func fileAtLocationInWindow(_ point:NSPoint) -> (QuickPreviewMedia, NSView?)? + } -fileprivate var handler:StickerPreviewHandler? +protocol ModalPreviewControllerView : NSView { + func update(with reference: QuickPreviewMedia, context: AccountContext, animated: Bool) + func getContentView() -> NSView +} + +fileprivate var handler:ModalPreviewHandler? + -func startStickerPreviewHandle(_ global:StickerPreviewProtocol, window:Window, account:Account) { - handler = StickerPreviewHandler(global, window: window, account: account) + +func startModalPreviewHandle(_ global:ModalPreviewProtocol, window:Window, context: AccountContext) { + handler = ModalPreviewHandler(global, window: window, context: context) handler?.startHandler() } -class StickerPreviewHandler : NSObject { - private let global:StickerPreviewProtocol - private let account:Account +class ModalPreviewHandler : NSObject { + private let global:ModalPreviewProtocol + private let context:AccountContext private let window:Window - private let modal:StickerPreviewModalController - init(_ global:StickerPreviewProtocol, window:Window, account:Account) { + private let modal:PreviewModalController + init(_ global:ModalPreviewProtocol, window:Window, context: AccountContext) { self.global = global self.window = window - self.account = account - self.modal = StickerPreviewModalController(account) + self.context = context + + self.modal = PreviewModalController(context) } func startHandler() { - - modal.update(with: global.stickerAtLocationInWindow(window.mouseLocationOutsideOfEventStream)) - showModal(with: modal, for: window) - - window.set(mouseHandler: { [weak self] (_) -> KeyHandlerResult in - if let strongSelf = self, let file = strongSelf.global.stickerAtLocationInWindow(strongSelf.window.mouseLocationOutsideOfEventStream) { - strongSelf.modal.update(with: file) + let initial = global.fileAtLocationInWindow(window.mouseLocationOutsideOfEventStream) + if let initial = initial { + modal.update(with: initial.0) + let animation:ModalAnimationType + if let view = initial.1 { + var rect = view.convert(view.bounds, to: nil) + rect.origin.y = window.contentView!.frame.maxY - rect.maxY + animation = .scaleFrom(rect) + } else { + animation = .bottomToCenter } - return .invokeNext + showModal(with: modal, for: window, animationType: animation) + + window.set(mouseHandler: { [weak self] (_) -> KeyHandlerResult in + if let strongSelf = self, let reference = strongSelf.global.fileAtLocationInWindow(strongSelf.window.mouseLocationOutsideOfEventStream) { + strongSelf.modal.update(with: reference.0) + } + return .invoked }, with: self, for: .leftMouseDragged, priority: .modal) - - window.set(mouseHandler: { [weak self] (_) -> KeyHandlerResult in - self?.stopHandler() - return .invokeNext - }, with: self, for: .leftMouseUp, priority: .modal) + + window.set(mouseHandler: { [weak self] (_) -> KeyHandlerResult in + self?.stopHandler() + return .invoked + }, with: self, for: .leftMouseUp, priority: .modal) + } + } func stopHandler() { - window.remove(object: self, for: .leftMouseDragged) - window.remove(object: self, for: .leftMouseUp) - modal.close() + window.removeAllHandlers(for: self) + if let view = self.global.fileAtLocationInWindow(self.window.mouseLocationOutsideOfEventStream)?.1 { + let content = modal.genericView.contentView?.getContentView() ?? modal.genericView + let rect = view.convert(view.bounds, to: modal.genericView.superview?.superview) + modal.close(animationType: .scaleToRect(rect, content)) + } else { + modal.close() + } + handler = nil } deinit { - stopHandler() + + } +} + + +private final class PreviewModalView: View { + fileprivate var contentView: ModalPreviewControllerView? + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) } + deinit { + var bp:Int = 0 + bp += 1 + } + + override func layout() { + super.layout() + } + + func update(with preview: QuickPreviewMedia, context: AccountContext, animated: Bool) { + + let viewType = preview.viewType + var changed = false + if contentView == nil || !contentView!.isKind(of: viewType) { + if animated { + let current = self.contentView + self.contentView = nil + current?.layer?.animateScaleSpring(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak current] completed in + if completed { + current?.removeFromSuperview() + } + }) + } else { + self.contentView?.removeFromSuperview() + } + + self.contentView = viewType.init(frame:NSZeroRect) + self.addSubview(self.contentView!) + changed = true + } + contentView?.frame = bounds + contentView?.update(with: preview, context: context, animated: animated && !changed) + + if animated { + contentView?.layer?.animateScaleSpring(from: 0.5, to: 1.0, duration: 0.2) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class PreviewModalController: ModalViewController { + fileprivate let context:AccountContext + fileprivate var reference:QuickPreviewMedia? + init(_ context: AccountContext) { + self.context = context + + super.init(frame: NSMakeRect(0, 0, min(context.window.frame.width - 50, 500), min(500, context.window.frame.height - 50))) + bar = .init(height: 0) + } + + override var hasOwnTouchbar: Bool { + return false + } + + override var containerBackground: NSColor { + return .clear + } + + override func becomeFirstResponder() -> Bool? { + return nil + } + + override var handleEvents:Bool { + return false + } + + override func viewDidLoad() { + super.viewDidLoad() + if let reference = reference { + genericView.update(with: reference, context: context, animated: false) + } + readyOnce() + } + + func update(with reference:QuickPreviewMedia?) { + if self.reference != reference { + self.reference = reference + if isLoaded(), let reference = reference { + genericView.update(with: reference, context: context, animated: true) + } + } + } + + fileprivate var genericView:PreviewModalView { + return view as! PreviewModalView + } + + override func viewClass() -> AnyClass { + return PreviewModalView.self + } + + deinit { + var bp:Int = 0 + bp += 1 + } + + // override var isFullScreen: Bool { + // return true + // } + } diff --git a/Telegram-Mac/StickerPreviewModalController.swift b/Telegram-Mac/StickerPreviewModalController.swift deleted file mode 100644 index a78432dd77..0000000000 --- a/Telegram-Mac/StickerPreviewModalController.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// StickerPreviewModalController.swift -// Telegram -// -// Created by keepcoder on 02/02/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - -import Cocoa -import TGUIKit -import TelegramCoreMac -import PostboxMac - -fileprivate class StickerPreviewModalView : View { - fileprivate let imageView:TransformImageView = TransformImageView() - fileprivate let textView:TextView = TextView() - required init(frame frameRect: NSRect) { - super.init(frame: frameRect) - addSubview(imageView) - addSubview(textView) - textView.backgroundColor = .clear - imageView.setFrameSize(100,100) - self.background = .clear - } - - override func layout() { - super.layout() - imageView.center() - - } - - func update(with file:TelegramMediaFile, account:Account) -> Void { - imageView.setSignal(account: account, signal: chatMessageSticker(account: account, file: file, type: .full, scale: backingScaleFactor), clearInstantly: true, animate:true) - let size = file.dimensions?.aspectFitted(NSMakeSize(frame.size.width, frame.size.height - 100)) ?? frame.size - imageView.set(arguments: TransformImageArguments(corners: ImageCorners(), imageSize: size, boundingSize: size, intrinsicInsets: NSEdgeInsets())) - imageView.frame = NSMakeRect(0, frame.height - size.height, size.width, size.height) - imageView.layer?.animateScaleSpring(from: 0.5, to: 1.0, duration: 0.2) - - let layout = TextViewLayout(.initialize(string: file.stickerText?.fixed, color: nil, font: .normal(.custom(30)))) - layout.measure(width: .greatestFiniteMagnitude) - textView.update(layout) - textView.centerX() - - textView.layer?.animateScaleSpring(from: 0.5, to: 1.0, duration: 0.2) - - needsLayout = true - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } -} - -class StickerPreviewModalController: ModalViewController { - fileprivate let account:Account - fileprivate var file:TelegramMediaFile? - init(_ account:Account) { - self.account = account - - super.init(frame: NSMakeRect(0, 0, 360, 400)) - } - - override var containerBackground: NSColor { - return .clear - } - - override var handleEvents:Bool { - return false - } - - override func viewDidLoad() { - super.viewDidLoad() - if let file = file { - genericView.update(with: file, account: account) - } - readyOnce() - } - - func update(with file:TelegramMediaFile?) { - if self.file != file { - self.file = file - if isLoaded(), let file = file { - genericView.update(with: file, account: account) - } - } - } - - fileprivate var genericView:StickerPreviewModalView { - return view as! StickerPreviewModalView - } - - override func viewClass() -> AnyClass { - return StickerPreviewModalView.self - } - - // override var isFullScreen: Bool { - // return true - // } - -} diff --git a/Telegram-Mac/StickerSetTableRowItem.swift b/Telegram-Mac/StickerSetTableRowItem.swift index 3a411757eb..2390f3e020 100644 --- a/Telegram-Mac/StickerSetTableRowItem.swift +++ b/Telegram-Mac/StickerSetTableRowItem.swift @@ -8,9 +8,10 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac +import TelegramCore +import Postbox +import SwiftSignalKit enum ItemListStickerPackItemControl: Equatable { @@ -19,94 +20,52 @@ enum ItemListStickerPackItemControl: Equatable { case remove case empty case selected - static func ==(lhs: ItemListStickerPackItemControl, rhs: ItemListStickerPackItemControl) -> Bool { - switch lhs { - case .none: - if case .none = rhs { - return true - } else { - return false - } - case .remove: - if case .remove = rhs { - return true - } else { - return false - } - case .selected: - if case .selected = rhs { - return true - } else { - return false - } - case .empty: - if case .empty = rhs { - return true - } else { - return false - } - case let .installation(installed): - if case .installation(installed) = rhs { - return true - } else { - return false - } - - } - } } -class StickerSetTableRowItem: TableRowItem { +class StickerSetTableRowItem: GeneralRowItem { - fileprivate let account:Account + fileprivate let context: AccountContext fileprivate let info:StickerPackCollectionInfo fileprivate let topItem:StickerPackItem? fileprivate let unread: Bool fileprivate let editing: ItemListStickerPackItemEditing - fileprivate let enabled:Bool fileprivate let _stableId:AnyHashable fileprivate let itemCount:Int32 fileprivate let control: ItemListStickerPackItemControl fileprivate let nameLayout:TextViewLayout fileprivate let countLayout:TextViewLayout - let action: () -> Void let addPack: () -> Void let removePack: () -> Void - fileprivate let insets: NSEdgeInsets = NSEdgeInsets(left: 30, right: 30) - - override var stableId: AnyHashable { - return _stableId - } - init(_ initialSize: NSSize, account:Account, stableId:AnyHashable, info:StickerPackCollectionInfo, topItem:StickerPackItem?, itemCount:Int32, unread: Bool, editing: ItemListStickerPackItemEditing, enabled: Bool, control: ItemListStickerPackItemControl, action:@escaping()->Void, addPack:@escaping()->Void = {}, removePack:@escaping() -> Void = {}) { - self.account = account + init(_ initialSize: NSSize, context:AccountContext, stableId:AnyHashable, info:StickerPackCollectionInfo, topItem:StickerPackItem?, itemCount:Int32, unread: Bool, editing: ItemListStickerPackItemEditing, enabled: Bool, control: ItemListStickerPackItemControl, viewType: GeneralViewType = .legacy, action:@escaping()->Void, addPack:@escaping()->Void = {}, removePack:@escaping() -> Void = {}) { + self.context = context self._stableId = stableId self.info = info self.topItem = topItem self.unread = unread self.editing = editing - self.enabled = enabled self.itemCount = itemCount self.control = control - self.action = action self.addPack = addPack self.removePack = removePack nameLayout = TextViewLayout(.initialize(string: info.title, color: theme.colors.text, font: .normal(.title)), maximumNumberOfLines: 1) - countLayout = TextViewLayout(.initialize(string: tr(.stickersSetCount(Int(itemCount))), color: theme.colors.grayText, font: .normal(.text)), maximumNumberOfLines: 1) - nameLayout.measure(width: initialSize.width - 50 - insets.left - insets.right) - countLayout.measure(width: initialSize.width - 50 - insets.left - insets.right) - super.init(initialSize) + countLayout = TextViewLayout(.initialize(string: L10n.stickersSetCount1Countable(Int(itemCount)), color: theme.colors.grayText, font: .normal(.text)), maximumNumberOfLines: 1) + super.init(initialSize, height: 50, stableId: stableId, type: .none, viewType: viewType, action: action, inset: NSEdgeInsets(left: 30, right: 30), enabled: enabled) + _ = makeSize(initialSize.width, oldWidth: 0) } - - override var height: CGFloat { - return 50 - } - + override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { - nameLayout.measure(width: width - 50 - insets.left - insets.right) - countLayout.measure(width: width - 50 - insets.left - insets.right) - return super.makeSize(width, oldWidth: oldWidth) + let success = super.makeSize(width, oldWidth: oldWidth) + switch self.viewType { + case .legacy: + nameLayout.measure(width: width - 50 - inset.left - inset.right) + countLayout.measure(width: width - 50 - inset.left - inset.right) + case let .modern(_, insets): + nameLayout.measure(width: self.blockWidth - 80 - insets.left - insets.right) + countLayout.measure(width: self.blockWidth - 80 - insets.left - insets.right) + } + return success } override func viewClass() -> AnyClass { @@ -114,40 +73,56 @@ class StickerSetTableRowItem: TableRowItem { } } -class StickerSetTableRowView : TableRowView { +class StickerSetTableRowView : TableRowView, ViewDisplayDelegate { + + private let containerView = GeneralRowContainerView(frame: NSZeroRect) + private let imageView:TransformImageView = TransformImageView() private let nameView:TextView = TextView() private let countView:TextView = TextView() private let installationControl:ImageView = ImageView() private let removeControl = ImageButton() + private var animatedView: MediaAnimatedStickerView? + private let loadedStickerPackDisposable = MetaDisposable() required init(frame frameRect: NSRect) { super.init(frame: frameRect) - addSubview(imageView) + containerView.addSubview(imageView) imageView.setFrameSize(NSMakeSize(35, 35)) - addSubview(nameView) - addSubview(countView) + containerView.addSubview(nameView) + containerView.addSubview(countView) countView.userInteractionEnabled = false nameView.userInteractionEnabled = false - addSubview(installationControl) + containerView.addSubview(installationControl) - removeControl.set(handler: { [weak self] _ in - if let item = self?.item as? StickerSetTableRowItem { - item.removePack() + containerView.displayDelegate = self + + containerView.set(handler: { control in + if let event = NSApp.currentEvent { + control.superview?.mouseDown(with: event) } - }, for: .SingleClick) - addSubview(removeControl) - } - - override func mouseUp(with event: NSEvent) { - if mouseInside() { - if let item = item as? StickerSetTableRowItem { - let point = convert(event.locationInWindow, from: nil) - if NSPointInRect(point, NSMakeRect(installationControl.frame.minX, 0, installationControl.frame.width, frame.height)) { + }, for: .Down) + + containerView.set(handler: { control in + if let event = NSApp.currentEvent { + control.superview?.mouseDragged(with: event) + } + }, for: .MouseDragging) + + containerView.set(handler: { control in + if let event = NSApp.currentEvent { + control.superview?.mouseUp(with: event) + } + }, for: .Up) + + containerView.set(handler: { [weak self] _ in + if let `self` = self, let item = self.item as? StickerSetTableRowItem, let event = NSApp.currentEvent { + let point = self.containerView.convert(event.locationInWindow, from: nil) + if NSPointInRect(point, self.installationControl.frame) { switch item.control { case .installation: item.addPack() case .none: - break + break case .remove: item.removePack() case .empty: @@ -159,80 +134,175 @@ class StickerSetTableRowView : TableRowView { item.action() } } - } - + }, for: .Click) + + removeControl.set(handler: { [weak self] _ in + if let item = self?.item as? StickerSetTableRowItem { + item.removePack() + } + }, for: .SingleClick) + containerView.addSubview(removeControl) + self.addSubview(containerView) } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func draw(_ layer: CALayer, in ctx: CGContext) { super.draw(layer, in: ctx) - if let item = item as? StickerSetTableRowItem { + + if let item = item as? StickerSetTableRowItem, layer == containerView.layer { ctx.setFillColor(theme.colors.border.cgColor) - ctx.fill(NSMakeRect(item.insets.left + 50, frame.height - .borderSize, frame.width - item.insets.left - item.insets.right - 50, .borderSize)) + switch item.viewType { + case .legacy: + ctx.fill(NSMakeRect(item.inset.left + 50, frame.height - .borderSize, frame.width - item.inset.left - item.inset.right - 50, .borderSize)) + case let .modern(position, insets): + switch position { + case .first, .inner: + ctx.fill(NSMakeRect(insets.left + 50, containerView.frame.height - .borderSize, containerView.frame.width - insets.left - insets.right - 50, .borderSize)) + default: + break + } + } } } override func layout() { super.layout() if let item = item as? StickerSetTableRowItem { - imageView.centerY(x: item.insets.left) - nameView.update(item.nameLayout, origin: NSMakePoint(item.insets.left + 50, 7)) - countView.update(item.countLayout, origin: NSMakePoint(item.insets.left + 50, frame.height - item.countLayout.layoutSize.height - 7)) - installationControl.centerY(x: frame.width - item.insets.left - installationControl.frame.width) - removeControl.centerY(x: frame.width - item.insets.left - removeControl.frame.width) - + switch item.viewType { + case .legacy: + self.containerView.frame = self.bounds + self.containerView.setCorners([]) + imageView.centerY(x: item.inset.left) + nameView.update(item.nameLayout, origin: NSMakePoint(item.inset.left + 50, 7)) + countView.update(item.countLayout, origin: NSMakePoint(item.inset.left + 50, containerView.frame.height - item.countLayout.layoutSize.height - 7)) + installationControl.centerY(x: containerView.frame.width - item.inset.left - installationControl.frame.width) + removeControl.centerY(x: containerView.frame.width - item.inset.left - removeControl.frame.width) + animatedView?.centerY(x: item.inset.left) + case let .modern(position, innerInsets): + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) + self.containerView.setCorners(position.corners) + imageView.centerY(x: innerInsets.left) + nameView.update(item.nameLayout, origin: NSMakePoint(innerInsets.left + 50, 7)) + countView.update(item.countLayout, origin: NSMakePoint(innerInsets.left + 50, containerView.frame.height - item.countLayout.layoutSize.height - 7)) + installationControl.centerY(x: containerView.frame.width - innerInsets.right - installationControl.frame.width) + removeControl.centerY(x: containerView.frame.width - innerInsets.right - removeControl.frame.width) + animatedView?.centerY(x: innerInsets.left) + } + + } + } + + override var backdorColor: NSColor { + return theme.colors.background + } + + override func updateColors() { + nameView.backgroundColor = backdorColor + countView.backgroundColor = backdorColor + containerView.background = backdorColor + if let item = item as? GeneralRowItem { + self.backgroundColor = item.viewType.rowBackground } } override func set(item: TableRowItem, animated: Bool) { super.set(item: item, animated: animated) - + self.updateMouse() if let item = item as? StickerSetTableRowItem { - if let topItem = item.topItem { - nameView.backgroundColor = backdorColor - countView.backgroundColor = backdorColor + + removeControl.set(image: theme.icons.stickerPackDelete, for: .Normal) + _ = removeControl.sizeToFit() + + if item.info.flags.contains(.isAnimated) { - removeControl.set(image: theme.icons.stickerPackDelete, for: .Normal) - removeControl.sizeToFit() - imageView.setSignal(account: item.account, signal: chatMessageSticker(account: item.account, file: topItem.file, type: .thumb, scale: backingScaleFactor)) - imageView.set(arguments: TransformImageArguments(corners: ImageCorners(), imageSize: NSMakeSize(35, 35), boundingSize: NSMakeSize(35, 35), intrinsicInsets: NSEdgeInsets())) - _ = fileInteractiveFetched(account: item.account, file: topItem.file).start() - nameView.update(item.nameLayout, origin: NSMakePoint(item.insets.left + 50, 7)) - countView.update(item.countLayout, origin: NSMakePoint(item.insets.left + 50, frame.height - item.countLayout.layoutSize.height - 7)) - switch item.control { - case let .installation(installed: installed): - installationControl.isHidden = false - removeControl.isHidden = true - installationControl.image = installed ? theme.icons.stickersAddedFeatured : theme.icons.stickersAddFeatured - installationControl.sizeToFit() - installationControl.centerY(x: frame.width - item.insets.left - installationControl.frame.width) - case .none: - installationControl.isHidden = true - removeControl.isHidden = false - removeControl.centerY(x: frame.width - item.insets.left - removeControl.frame.width) - case .remove: - removeControl.isHidden = true - installationControl.isHidden = false - installationControl.image = theme.icons.stickersRemove - installationControl.sizeToFit() - installationControl.centerY(x: frame.width - item.insets.left - installationControl.frame.width) - case .empty: - removeControl.isHidden = true - installationControl.isHidden = true - case .selected: - removeControl.isHidden = true - installationControl.isHidden = false - installationControl.image = theme.icons.generalSelect - installationControl.sizeToFit() - installationControl.centerY(x: frame.width - item.insets.left - installationControl.frame.width) + if self.animatedView == nil { + self.animatedView = MediaAnimatedStickerView(frame: NSZeroRect) + containerView.addSubview(self.animatedView!) + } + self.imageView.isHidden = true + + var file: TelegramMediaFile? + if let thumbnail = item.info.thumbnail { + file = TelegramMediaFile(fileId: MediaId(namespace: 0, id: item.info.id.id), partialReference: nil, resource: thumbnail.resource, previewRepresentations: [thumbnail], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/x-tgsticker", size: nil, attributes: [.FileName(fileName: "sticker.tgs"), .Sticker(displayText: "", packReference: .id(id: item.info.id.id, accessHash: item.info.accessHash), maskData: nil)]) + } else if let item = item.topItem { + file = item.file + } + self.animatedView?.userInteractionEnabled = false + if let file = file { + self.animatedView?.update(with: file, size: NSMakeSize(35, 35), context: item.context, parent: nil, table: item.table, parameters: nil, animated: animated, positionFlags: nil, approximateSynchronousValue: false) } + } else { + + self.animatedView?.removeFromSuperview() + self.animatedView = nil + + self.imageView.isHidden = false + + var thumbnailItem: TelegramMediaImageRepresentation? + var resourceReference: MediaResourceReference? + + if let thumbnail = item.info.thumbnail { + thumbnailItem = thumbnail + resourceReference = MediaResourceReference.stickerPackThumbnail(stickerPack: .id(id: item.info.id.id, accessHash: item.info.accessHash), resource: thumbnail.resource) + } else if let topItem = item.topItem { + let dimensions = topItem.file.dimensions?.size ?? NSMakeSize(35, 35) + thumbnailItem = TelegramMediaImageRepresentation(dimensions: PixelDimensions(dimensions), resource: topItem.file.resource, progressiveSizes: [], immediateThumbnailData: nil) + resourceReference = MediaResourceReference.media(media: .stickerPack(stickerPack: .id(id: item.info.id.id, accessHash: item.info.accessHash), media: topItem.file), resource: topItem.file.resource) + } + if let thumbnailItem = thumbnailItem { + imageView.setSignal(chatMessageStickerPackThumbnail(postbox: item.context.account.postbox, representation: thumbnailItem, scale: backingScaleFactor, synchronousLoad: false)) + } + if let resourceReference = resourceReference { + _ = fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, reference: resourceReference, statsCategory: .file).start() + } + imageView.set(arguments: TransformImageArguments(corners: ImageCorners(), imageSize: NSMakeSize(35, 35), boundingSize: NSMakeSize(35, 35), intrinsicInsets: NSEdgeInsets())) + } + + nameView.update(item.nameLayout) + countView.update(item.countLayout) + switch item.control { + case let .installation(installed: installed): + installationControl.image = installed ? theme.icons.stickersAddedFeatured : theme.icons.stickersAddFeatured + installationControl.sizeToFit() + case .remove: + installationControl.image = theme.icons.stickersRemove + installationControl.sizeToFit() + case .selected: + installationControl.image = theme.icons.generalSelect + installationControl.sizeToFit() + default: + break } + + switch item.control { + case .installation: + installationControl.isHidden = false//!containerView.mouseInside() + removeControl.isHidden = true + case .none: + installationControl.isHidden = true + removeControl.isHidden = false//!containerView.mouseInside() + case .remove: + removeControl.isHidden = true + installationControl.isHidden = false//!containerView.mouseInside() + case .empty: + removeControl.isHidden = true + installationControl.isHidden = true + case .selected: + removeControl.isHidden = true + installationControl.isHidden = false//!containerView.mouseInside() + } + } needsLayout = true } + deinit { + loadedStickerPackDisposable.dispose() + } } diff --git a/Telegram-Mac/StickerSettings.swift b/Telegram-Mac/StickerSettings.swift new file mode 100644 index 0000000000..1a5b59d559 --- /dev/null +++ b/Telegram-Mac/StickerSettings.swift @@ -0,0 +1,73 @@ +import Foundation +import Postbox +import SwiftSignalKit + +enum EmojiStickerSuggestionMode: Int32 { + case none + case all + case installed +} + +struct StickerSettings: PreferencesEntry, Equatable { + var emojiStickerSuggestionMode: EmojiStickerSuggestionMode + var trendingClosedOn: Int64? + static var defaultSettings: StickerSettings { + return StickerSettings(emojiStickerSuggestionMode: .all, trendingClosedOn: nil) + } + + init(emojiStickerSuggestionMode: EmojiStickerSuggestionMode, trendingClosedOn: Int64?) { + self.emojiStickerSuggestionMode = emojiStickerSuggestionMode + self.trendingClosedOn = trendingClosedOn + } + + init(decoder: PostboxDecoder) { + self.emojiStickerSuggestionMode = EmojiStickerSuggestionMode(rawValue: decoder.decodeInt32ForKey("emojiStickerSuggestionMode", orElse: 0))! + self.trendingClosedOn = decoder.decodeOptionalInt64ForKey("t.c.o") + } + + func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(self.emojiStickerSuggestionMode.rawValue, forKey: "emojiStickerSuggestionMode") + if let trendingClosedOn = self.trendingClosedOn { + encoder.encodeInt64(trendingClosedOn, forKey: "t.c.o") + } + } + + func isEqual(to: PreferencesEntry) -> Bool { + if let to = to as? StickerSettings { + return self == to + } else { + return false + } + } + + func withUpdatedEmojiStickerSuggestionMode(_ emojiStickerSuggestionMode: EmojiStickerSuggestionMode) -> StickerSettings { + return StickerSettings(emojiStickerSuggestionMode: emojiStickerSuggestionMode, trendingClosedOn: self.trendingClosedOn) + } + func withUpdatedTrendingClosedOn(_ trendingClosedOn: Int64?) -> StickerSettings { + return StickerSettings(emojiStickerSuggestionMode: self.emojiStickerSuggestionMode, trendingClosedOn: trendingClosedOn) + } +} + +func updateStickerSettingsInteractively(postbox: Postbox, _ f: @escaping (StickerSettings) -> StickerSettings) -> Signal { + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.stickerSettings, { entry in + let currentSettings: StickerSettings + if let entry = entry as? StickerSettings { + currentSettings = entry + } else { + currentSettings = StickerSettings.defaultSettings + } + return f(currentSettings) + }) + } +} + +func stickerSettings(postbox: Postbox) -> Signal { + return postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.stickerSettings]) |> map { preferencesView in + var stickerSettings = StickerSettings.defaultSettings + if let value = preferencesView.values[ApplicationSpecificPreferencesKeys.stickerSettings] as? StickerSettings { + stickerSettings = value + } + return stickerSettings + } +} diff --git a/Telegram-Mac/StickerShimmerEffectView.swift b/Telegram-Mac/StickerShimmerEffectView.swift new file mode 100644 index 0000000000..4959aef237 --- /dev/null +++ b/Telegram-Mac/StickerShimmerEffectView.swift @@ -0,0 +1,749 @@ +import TGUIKit +import Foundation + + +private final class ShimmerEffectForegroundView: View { + private var currentBackgroundColor: NSColor? + private var currentForegroundColor: NSColor? + private let imageViewContainer: View + private let imageView: ImageView + + private var absoluteLocation: (CGRect, CGSize)? + private var isCurrentlyInHierarchy = false + private var shouldBeAnimating = false + + override init() { + self.imageViewContainer = View() + self.imageView = ImageView() + super.init() + self.imageViewContainer.addSubview(self.imageView) + self.addSubview(self.imageViewContainer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + + self.isCurrentlyInHierarchy = self.window != nil + self.updateAnimation() + + } + + func update(backgroundColor: NSColor, foregroundColor: NSColor) { + if let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor) { + return + } + self.currentBackgroundColor = backgroundColor + self.currentForegroundColor = foregroundColor + + let image = generateImage(CGSize(width: 320.0, height: 16.0), opaque: false, scale: 1.0, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(backgroundColor.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + + context.clip(to: CGRect(origin: CGPoint(), size: size)) + + let transparentColor = foregroundColor.withAlphaComponent(0.0).cgColor + let peakColor = foregroundColor.cgColor + + var locations: [CGFloat] = [0.0, 0.5, 1.0] + let colors: [CGColor] = [transparentColor, peakColor, transparentColor] + + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions()) + }) + self.imageView.image = image + } + + func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + if let absoluteLocation = self.absoluteLocation, absoluteLocation.0 == rect && absoluteLocation.1 == containerSize { + return + } + let sizeUpdated = self.absoluteLocation?.1 != containerSize + let frameUpdated = self.absoluteLocation?.0 != rect + self.absoluteLocation = (rect, containerSize) + + if sizeUpdated { + if self.shouldBeAnimating { + self.imageView.layer?.removeAnimation(forKey: "shimmer") + self.addImageAnimation() + } else { + self.updateAnimation() + } + } + + if frameUpdated { + self.imageViewContainer.frame = CGRect(origin: CGPoint(x: -rect.minX, y: -rect.minY), size: containerSize) + } + } + + private func updateAnimation() { + let shouldBeAnimating = self.isCurrentlyInHierarchy && self.absoluteLocation != nil + if shouldBeAnimating != self.shouldBeAnimating { + self.shouldBeAnimating = shouldBeAnimating + if shouldBeAnimating { + self.addImageAnimation() + } else { + self.imageView.layer?.removeAnimation(forKey: "shimmer") + } + } + } + + private func addImageAnimation() { + guard let containerSize = self.absoluteLocation?.1 else { + return + } + let gradientHeight: CGFloat = 320.0 + self.imageView.frame = CGRect(origin: CGPoint(x: -gradientHeight, y: 0.0), size: CGSize(width: gradientHeight, height: containerSize.height)) + let animation = self.imageView.layer!.makeAnimation(from: 0.0 as NSNumber, to: (containerSize.width + gradientHeight) as NSNumber, keyPath: "position.x", timingFunction: .easeOut, duration: 1.3 * 1.0, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true) + animation.repeatCount = Float.infinity + animation.beginTime = 1.0 + self.imageView.layer?.add(animation, forKey: "shimmer") + } +} + +private let decodingMap: [String] = ["A", "A", "C", "A", "A", "A", "A", "H", "A", "A", "A", "L", "M", "A", "A", "A", "Q", "A", "S", "T", "A", "V", "A", "A", "A", "Z", "a", "a", "c", "a", "a", "a", "a", "h", "a", "a", "a", "l", "m", "a", "a", "a", "q", "a", "s", "t", "a", "v", "a", ".", "a", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "-", ","] +private func decodeStickerThumbnailData(_ data: Data) -> String { + var string = "M" + data.forEach { byte in + if byte >= 128 + 64 { + string.append(decodingMap[Int(byte) - 128 - 64]) + } else { + if byte >= 128 { + string.append(",") + } else if byte >= 64 { + string.append("-") + } + string.append("\(byte & 63)") + } + } + string.append("z") + return string +} + +class StickerShimmerEffectView: View { + private let backgroundView: View + private let effectView: ShimmerEffectForegroundView + private let foregroundView: ImageView + + private var maskView: ImageView? + + private var currentData: Data? + private var currentBackgroundColor: NSColor? + private var currentForegroundColor: NSColor? + private var currentShimmeringColor: NSColor? + private var currentSize = CGSize() + + override init() { + self.backgroundView = View() + self.effectView = ShimmerEffectForegroundView() + self.foregroundView = ImageView() + + super.init() + + self.addSubview(self.backgroundView) + self.addSubview(self.effectView) + self.addSubview(self.foregroundView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + public func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) { + self.effectView.updateAbsoluteRect(rect, within: containerSize) + } + + public func update(backgroundColor: NSColor?, foregroundColor: NSColor, shimmeringColor: NSColor, data: Data?, size: CGSize) { + if self.currentData == data, let currentBackgroundColor = self.currentBackgroundColor, currentBackgroundColor.isEqual(backgroundColor), let currentForegroundColor = self.currentForegroundColor, currentForegroundColor.isEqual(foregroundColor), let currentShimmeringColor = self.currentShimmeringColor, currentShimmeringColor.isEqual(shimmeringColor), self.currentSize == size { + return + } + + self.currentBackgroundColor = backgroundColor + self.currentForegroundColor = foregroundColor + self.currentShimmeringColor = shimmeringColor + self.currentData = data + self.currentSize = size + + self.backgroundView.backgroundColor = foregroundColor + + self.effectView.update(backgroundColor: backgroundColor == nil ? .clear : foregroundColor, foregroundColor: shimmeringColor) + + let image = generateImage(size, rotatedContext: { size, context in + if let backgroundColor = backgroundColor { + context.setFillColor(backgroundColor.cgColor) + context.setBlendMode(.copy) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(NSColor.clear.cgColor) + } else { + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(NSColor.black.cgColor) + } + + if let data = data { + var path = decodeStickerThumbnailData(data) + if !path.hasPrefix("z") { + path = "\(path)z" + } + let reader = PathDataReader(input: path) + let segments = reader.read() + + let scale = size.width / 512.0 + context.scaleBy(x: scale, y: scale) + renderPath(segments, context: context) + } else { + let path = CGMutablePath() + path.addRoundedRect(in: CGRect(origin: CGPoint(), size: size), cornerWidth: min(10, size.height / 2), cornerHeight: min(size.height / 2, 10)) + context.addPath(path) + context.fillPath() + } + }) + + if backgroundColor == nil { + self.foregroundView.image = nil + + let maskView: ImageView + if let current = self.maskView { + maskView = current + } else { + maskView = ImageView() + maskView.frame = CGRect(origin: CGPoint(), size: size) + self.maskView = maskView + self.layer?.mask = maskView.layer + } + + } else { + self.foregroundView.image = image + + if let _ = self.maskView { + self.layer?.mask = nil + self.maskView = nil + } + } + + self.maskView?.image = image + + self.backgroundView.frame = CGRect(origin: CGPoint(), size: size) + self.foregroundView.frame = CGRect(origin: CGPoint(), size: size) + self.effectView.frame = CGRect(origin: CGPoint(), size: size) + } +} + +open class PathSegment: Equatable { + public enum SegmentType { + case M + case L + case C + case Q + case A + case z + case H + case V + case S + case T + case m + case l + case c + case q + case a + case h + case v + case s + case t + case E + case e + } + + public let type: SegmentType + public let data: [Double] + + public init(type: PathSegment.SegmentType = .M, data: [Double] = []) { + self.type = type + self.data = data + } + + open func isAbsolute() -> Bool { + switch type { + case .M, .L, .H, .V, .C, .S, .Q, .T, .A, .E: + return true + default: + return false + } + } + + public static func == (lhs: PathSegment, rhs: PathSegment) -> Bool { + return lhs.type == rhs.type && lhs.data == rhs.data + } +} + +private func renderPath(_ segments: [PathSegment], context: CGContext) { + var currentPoint: CGPoint? + var cubicPoint: CGPoint? + var quadrPoint: CGPoint? + var initialPoint: CGPoint? + + func M(_ x: Double, y: Double) { + let point = CGPoint(x: CGFloat(x), y: CGFloat(y)) + context.move(to: point) + setInitPoint(point) + } + + func m(_ x: Double, y: Double) { + if let cur = currentPoint { + let next = CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y) + context.move(to: next) + setInitPoint(next) + } else { + M(x, y: y) + } + } + + func L(_ x: Double, y: Double) { + lineTo(CGPoint(x: CGFloat(x), y: CGFloat(y))) + } + + func l(_ x: Double, y: Double) { + if let cur = currentPoint { + lineTo(CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y)) + } else { + L(x, y: y) + } + } + + func H(_ x: Double) { + if let cur = currentPoint { + lineTo(CGPoint(x: CGFloat(x), y: CGFloat(cur.y))) + } + } + + func h(_ x: Double) { + if let cur = currentPoint { + lineTo(CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(cur.y))) + } + } + + func V(_ y: Double) { + if let cur = currentPoint { + lineTo(CGPoint(x: CGFloat(cur.x), y: CGFloat(y))) + } + } + + func v(_ y: Double) { + if let cur = currentPoint { + lineTo(CGPoint(x: CGFloat(cur.x), y: CGFloat(y) + cur.y)) + } + } + + func lineTo(_ p: CGPoint) { + context.addLine(to: p) + setPoint(p) + } + + func c(_ x1: Double, y1: Double, x2: Double, y2: Double, x: Double, y: Double) { + if let cur = currentPoint { + let endPoint = CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y) + let controlPoint1 = CGPoint(x: CGFloat(x1) + cur.x, y: CGFloat(y1) + cur.y) + let controlPoint2 = CGPoint(x: CGFloat(x2) + cur.x, y: CGFloat(y2) + cur.y) + context.addCurve(to: endPoint, control1: controlPoint1, control2: controlPoint2) + setCubicPoint(endPoint, cubic: controlPoint2) + } + } + + func C(_ x1: Double, y1: Double, x2: Double, y2: Double, x: Double, y: Double) { + let endPoint = CGPoint(x: CGFloat(x), y: CGFloat(y)) + let controlPoint1 = CGPoint(x: CGFloat(x1), y: CGFloat(y1)) + let controlPoint2 = CGPoint(x: CGFloat(x2), y: CGFloat(y2)) + context.addCurve(to: endPoint, control1: controlPoint1, control2: controlPoint2) + setCubicPoint(endPoint, cubic: controlPoint2) + } + + func s(_ x2: Double, y2: Double, x: Double, y: Double) { + if let cur = currentPoint { + let nextCubic = CGPoint(x: CGFloat(x2) + cur.x, y: CGFloat(y2) + cur.y) + let next = CGPoint(x: CGFloat(x) + cur.x, y: CGFloat(y) + cur.y) + + let xy1: CGPoint + if let curCubicVal = cubicPoint { + xy1 = CGPoint(x: CGFloat(2 * cur.x) - curCubicVal.x, y: CGFloat(2 * cur.y) - curCubicVal.y) + } else { + xy1 = cur + } + context.addCurve(to: next, control1: xy1, control2: nextCubic) + setCubicPoint(next, cubic: nextCubic) + } + } + + func S(_ x2: Double, y2: Double, x: Double, y: Double) { + if let cur = currentPoint { + let nextCubic = CGPoint(x: CGFloat(x2), y: CGFloat(y2)) + let next = CGPoint(x: CGFloat(x), y: CGFloat(y)) + let xy1: CGPoint + if let curCubicVal = cubicPoint { + xy1 = CGPoint(x: CGFloat(2 * cur.x) - curCubicVal.x, y: CGFloat(2 * cur.y) - curCubicVal.y) + } else { + xy1 = cur + } + context.addCurve(to: next, control1: xy1, control2: nextCubic) + setCubicPoint(next, cubic: nextCubic) + } + } + + func z() { + context.fillPath() + } + + func setQuadrPoint(_ p: CGPoint, quadr: CGPoint) { + currentPoint = p + quadrPoint = quadr + cubicPoint = nil + } + + func setCubicPoint(_ p: CGPoint, cubic: CGPoint) { + currentPoint = p + cubicPoint = cubic + quadrPoint = nil + } + + func setInitPoint(_ p: CGPoint) { + setPoint(p) + initialPoint = p + } + + func setPoint(_ p: CGPoint) { + currentPoint = p + cubicPoint = nil + quadrPoint = nil + } + + for segment in segments { + var data = segment.data + switch segment.type { + case .M: + M(data[0], y: data[1]) + data.removeSubrange(Range(uncheckedBounds: (lower: 0, upper: 2))) + while data.count >= 2 { + L(data[0], y: data[1]) + data.removeSubrange((0 ..< 2)) + } + case .m: + m(data[0], y: data[1]) + data.removeSubrange((0 ..< 2)) + while data.count >= 2 { + l(data[0], y: data[1]) + data.removeSubrange((0 ..< 2)) + } + case .L: + while data.count >= 2 { + L(data[0], y: data[1]) + data.removeSubrange((0 ..< 2)) + } + case .l: + while data.count >= 2 { + l(data[0], y: data[1]) + data.removeSubrange((0 ..< 2)) + } + case .H: + H(data[0]) + case .h: + h(data[0]) + case .V: + V(data[0]) + case .v: + v(data[0]) + case .C: + while data.count >= 6 { + C(data[0], y1: data[1], x2: data[2], y2: data[3], x: data[4], y: data[5]) + data.removeSubrange((0 ..< 6)) + } + case .c: + while data.count >= 6 { + c(data[0], y1: data[1], x2: data[2], y2: data[3], x: data[4], y: data[5]) + data.removeSubrange((0 ..< 6)) + } + case .S: + while data.count >= 4 { + S(data[0], y2: data[1], x: data[2], y: data[3]) + data.removeSubrange((0 ..< 4)) + } + case .s: + while data.count >= 4 { + s(data[0], y2: data[1], x: data[2], y: data[3]) + data.removeSubrange((0 ..< 4)) + } + case .z: + z() + default: + print("unknown") + break + } + } +} + +private class PathDataReader { + private let input: String + private var current: UnicodeScalar? + private var previous: UnicodeScalar? + private var iterator: String.UnicodeScalarView.Iterator + + private static let spaces: Set = Set("\n\r\t ,".unicodeScalars) + + init(input: String) { + self.input = input + self.iterator = input.unicodeScalars.makeIterator() + } + + public func read() -> [PathSegment] { + readNext() + var segments = [PathSegment]() + while let array = readSegments() { + segments.append(contentsOf: array) + } + return segments + } + + private func readSegments() -> [PathSegment]? { + if let type = readSegmentType() { + let argCount = getArgCount(segment: type) + if argCount == 0 { + return [PathSegment(type: type)] + } + var result = [PathSegment]() + let data: [Double] + if type == .a || type == .A { + data = readDataOfASegment() + } else { + data = readData() + } + var index = 0 + var isFirstSegment = true + while index < data.count { + let end = index + argCount + if end > data.count { + break + } + var currentType = type + if type == .M && !isFirstSegment { + currentType = .L + } + if type == .m && !isFirstSegment { + currentType = .l + } + result.append(PathSegment(type: currentType, data: Array(data[index.. [Double] { + var data = [Double]() + while true { + skipSpaces() + if let value = readNum() { + data.append(value) + } else { + return data + } + } + } + + private func readDataOfASegment() -> [Double] { + let argCount = getArgCount(segment: .A) + var data: [Double] = [] + var index = 0 + while true { + skipSpaces() + let value: Double? + let indexMod = index % argCount + if indexMod == 3 || indexMod == 4 { + value = readFlag() + } else { + value = readNum() + } + guard let doubleValue = value else { + return data + } + data.append(doubleValue) + index += 1 + } + return data + } + + private func skipSpaces() { + var currentCharacter = current + while let character = currentCharacter, Self.spaces.contains(character) { + currentCharacter = readNext() + } + } + + private func readFlag() -> Double? { + guard let ch = current else { + return .none + } + readNext() + switch ch { + case "0": + return 0 + case "1": + return 1 + default: + return .none + } + } + + fileprivate func readNum() -> Double? { + guard let ch = current else { + return .none + } + + guard ch >= "0" && ch <= "9" || ch == "." || ch == "-" else { + return .none + } + + var chars = [ch] + var hasDot = ch == "." + while let ch = readDigit(&hasDot) { + chars.append(ch) + } + + var buf = "" + buf.unicodeScalars.append(contentsOf: chars) + guard let value = Double(buf) else { + return .none + } + return value + } + + fileprivate func readDigit(_ hasDot: inout Bool) -> UnicodeScalar? { + if let ch = readNext() { + if (ch >= "0" && ch <= "9") || ch == "e" || (previous == "e" && ch == "-") { + return ch + } else if ch == "." && !hasDot { + hasDot = true + return ch + } + } + return nil + } + + fileprivate func isNum(ch: UnicodeScalar, hasDot: inout Bool) -> Bool { + switch ch { + case "0"..."9": + return true + case ".": + if hasDot { + return false + } + hasDot = true + default: + return true + } + return false + } + + @discardableResult + private func readNext() -> UnicodeScalar? { + previous = current + current = iterator.next() + return current + } + + private func isAcceptableSeparator(_ ch: UnicodeScalar?) -> Bool { + if let ch = ch { + return "\n\r\t ,".contains(String(ch)) + } + return false + } + + private func readSegmentType() -> PathSegment.SegmentType? { + while true { + if let type = getPathSegmentType() { + readNext() + return type + } + if readNext() == nil { + return nil + } + } + } + + fileprivate func getPathSegmentType() -> PathSegment.SegmentType? { + if let ch = current { + switch ch { + case "M": + return .M + case "m": + return .m + case "L": + return .L + case "l": + return .l + case "C": + return .C + case "c": + return .c + case "Q": + return .Q + case "q": + return .q + case "A": + return .A + case "a": + return .a + case "z", "Z": + return .z + case "H": + return .H + case "h": + return .h + case "V": + return .V + case "v": + return .v + case "S": + return .S + case "s": + return .s + case "T": + return .T + case "t": + return .t + default: + break + } + } + return nil + } + + fileprivate func getArgCount(segment: PathSegment.SegmentType) -> Int { + switch segment { + case .H, .h, .V, .v: + return 1 + case .M, .m, .L, .l, .T, .t: + return 2 + case .S, .s, .Q, .q: + return 4 + case .C, .c: + return 6 + case .A, .a: + return 7 + default: + return 0 + } + } +} diff --git a/Telegram-Mac/StickersPackPreviewModalController.swift b/Telegram-Mac/StickersPackPreviewModalController.swift index 0db0e3ccf0..297e35a095 100644 --- a/Telegram-Mac/StickersPackPreviewModalController.swift +++ b/Telegram-Mac/StickersPackPreviewModalController.swift @@ -16,13 +16,13 @@ import MtProtoKitMac final class StickerPackArguments { - let account: Account - let send:(TelegramMediaFile)->Void + let context: AccountContext + let send:(TelegramMediaFile, NSView)->Void let addpack:(StickerPackCollectionInfo, [ItemCollectionItem], Bool)->Void let share:(String)->Void let close:()->Void - init(account:Account, send:@escaping(Media)->Void, addpack:@escaping(StickerPackCollectionInfo, [ItemCollectionItem], Bool)->Void, share:@escaping(String)->Void, close:@escaping()->Void) { - self.account = account + init(context: AccountContext, send:@escaping(Media, NSView)->Void, addpack:@escaping(StickerPackCollectionInfo, [ItemCollectionItem], Bool)->Void, share:@escaping(String)->Void, close:@escaping()->Void) { + self.context = context self.send = send self.addpack = addpack self.share = share @@ -41,7 +41,7 @@ private class StickersModalView : View { private let headerSeparatorView:View = View() private let dismiss:ImageButton = ImageButton() private let indicatorView:ProgressIndicator = ProgressIndicator(frame: NSMakeRect(0, 0, 25, 25)) - private let shadowView: View = ShadowView() + private let shadowView: ShadowView = ShadowView() required init(frame frameRect: NSRect) { super.init(frame: frameRect) backgroundColor = theme.colors.background @@ -53,23 +53,23 @@ private class StickersModalView : View { addSubview(headerSeparatorView) addSubview(dismiss) - shadowView.backgroundColor = theme.colors.background.withAlphaComponent(1.0) + shadowView.shadowBackground = theme.colors.background shadowView.setFrameSize(frame.width, 70) addSubview(shadowView) dismiss.set(image: theme.icons.stickerPackDelete, for: .Normal) - dismiss.sizeToFit() + _ = dismiss.sizeToFit() add.disableActions() add.setFrameSize(170, 40) add.layer?.cornerRadius = 20 - add.set(color: .white, for: .Normal) + add.set(color: theme.colors.underSelectedColor, for: .Normal) add.set(font: .medium(.title), for: .Normal) - add.set(background: theme.colors.blueFill, for: .Normal) - add.set(background: theme.colors.blueFill, for: .Hover) - add.set(background: theme.colors.blueFill, for: .Highlight) - add.set(text: tr(.stickerPackAdd(0)), for: .Normal) + add.set(background: theme.colors.accent, for: .Normal) + add.set(background: theme.colors.accent, for: .Hover) + add.set(background: theme.colors.accent, for: .Highlight) + add.set(text: L10n.stickerPackAdd1Countable(0), for: .Normal) addSubview(add) headerTitle.backgroundColor = theme.colors.background @@ -77,8 +77,8 @@ private class StickersModalView : View { shareView.set(image: theme.icons.stickersShare, for: .Normal) close.set(image: theme.icons.stickerPackClose, for: .Normal) - shareView.sizeToFit() - close.sizeToFit() + _ = shareView.sizeToFit() + _ = close.sizeToFit() } @@ -104,29 +104,34 @@ private class StickersModalView : View { indicatorView.removeFromSuperview() dismiss.isHidden = !installed shareView.isHidden = false - add.set(text: tr(.stickerPackAdd(collectionItems .count)).uppercased(), for: .Normal) + add.set(text: tr(L10n.stickerPackAdd1Countable(collectionItems .count)).uppercased(), for: .Normal) + _ = add.sizeToFit(NSMakeSize(20, 0), NSMakeSize(frame.width - 40, 40), thatFit: false) add.isHidden = installed shadowView.isHidden = installed let attr = NSMutableAttributedString() - _ = attr.append(string: info.title, color: theme.colors.text, font: .medium(.custom(16))) - attr.detectLinks(type: [.Mentions], account: arguments.account, color: .blueUI, openInfo: { (peerId, _, _, _) in - _ = (arguments.account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue).start(next: { peer in + _ = attr.append(string: info.title, color: theme.colors.text, font: .medium(16.0)) + attr.detectLinks(type: [.Mentions], context: arguments.context, color: .accent, openInfo: { (peerId, _, _, _) in + _ = (arguments.context.account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue).start(next: { peer in arguments.close() - arguments.account.context.mainNavigation?.push(PeerInfoController(account: arguments.account, peer: peer)) + if peer.isUser || peer.isBot { + arguments.context.sharedContext.bindings.rootNavigation().push(PeerInfoController(context: arguments.context, peerId: peerId)) + } else { + arguments.context.sharedContext.bindings.rootNavigation().push(ChatAdditionController(context: arguments.context, chatLocation: .peer(peer.id))) + } }) }) - let layout = TextViewLayout(attr, maximumNumberOfLines: 1) + let layout = TextViewLayout(attr, maximumNumberOfLines: 2, alignment: .center) layout.interactions = globalLinkExecutor - layout.measure(width: frame.width - 140) + layout.measure(width: frame.width - 160) headerTitle.update(layout) let items = collectionItems.filter({ item -> Bool in return item is StickerPackItem }).map ({ item -> StickerPackGridItem in - return StickerPackGridItem(account: arguments.account, file: (item as! StickerPackItem).file, send: arguments.send, selected: {}) + return StickerPackGridItem(context: arguments.context, file: (item as! StickerPackItem).file, send: arguments.send, selected: {}) }) var insert:[GridNodeInsertItem] = [] @@ -137,7 +142,7 @@ private class StickersModalView : View { grid.removeAllItems() - grid.transaction(GridNodeTransaction(deleteItems: [], insertItems: insert, updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: CGSize(width: frame.width, height: frame.height), insets: NSEdgeInsets(left: 10, right: 10, top: 10, bottom: installed ? 0 : 60), preloadSize: self.bounds.width, type: .fixed(itemSize: CGSize(width: 80, height: 80), lineSpacing: 10)), transition: .immediate), itemTransition: .immediate, stationaryItems: .all, updateFirstIndexInSectionOffset: nil), completion: { _ in }) + grid.transaction(GridNodeTransaction(deleteItems: [], insertItems: insert, updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: CGSize(width: frame.width, height: frame.height), insets: NSEdgeInsets(left: 0, right: 0, top: 10, bottom: installed ? 0 : 60), preloadSize: self.bounds.width, type: .fixed(itemSize: CGSize(width: 70, height: 70), lineSpacing: 10)), transition: .immediate), itemTransition: .immediate, stationaryItems: .all, updateFirstIndexInSectionOffset: nil), completion: { _ in }) grid.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) self.needsLayout = true @@ -180,12 +185,12 @@ private class StickersModalView : View { grid.frame = NSMakeRect(0, headerHeight, frame.width, frame.height - headerHeight) - headerTitle.centerX(y : floorToScreenPixels((headerHeight - headerTitle.frame.height)/2) + 1) + headerTitle.centerX(y : floorToScreenPixels(backingScaleFactor, (headerHeight - headerTitle.frame.height)/2) + 1) headerSeparatorView.frame = NSMakeRect(0, headerHeight - .borderSize, frame.width, .borderSize) - shareView.setFrameOrigin(frame.width - close.frame.width - 12, floorToScreenPixels((headerHeight - shareView.frame.height)/2)) - close.setFrameOrigin(12, floorToScreenPixels((headerHeight - shareView.frame.height)/2)) + shareView.setFrameOrigin(frame.width - close.frame.width - 12, floorToScreenPixels(backingScaleFactor, (headerHeight - shareView.frame.height)/2)) + close.setFrameOrigin(12, floorToScreenPixels(backingScaleFactor, (headerHeight - shareView.frame.height)/2)) add.centerX(y: frame.height - add.frame.height - 15) - dismiss.setFrameOrigin(NSMakePoint(shareView.frame.minX - dismiss.frame.width - 15, floorToScreenPixels((headerHeight - shareView.frame.height)/2))) + dismiss.setFrameOrigin(NSMakePoint(shareView.frame.minX - dismiss.frame.width - 15, floorToScreenPixels(backingScaleFactor, (headerHeight - shareView.frame.height)/2))) shadowView.setFrameOrigin(0, frame.height - shadowView.frame.height) } @@ -193,41 +198,37 @@ private class StickersModalView : View { -class StickersPackPreviewModalController: ModalViewController { - private let account:Account +class StickerPackPreviewModalController: ModalViewController { + private let context:AccountContext private let peerId:PeerId? private let reference:StickerPackReference private let disposable: MetaDisposable = MetaDisposable() private var arguments:StickerPackArguments! - init(_ account:Account, peerId:PeerId?, reference:StickerPackReference) { - self.account = account + init(_ context: AccountContext, peerId:PeerId?, reference:StickerPackReference) { + self.context = context self.peerId = peerId self.reference = reference - super.init(frame: NSMakeRect(0, 0, 360, 400)) + super.init(frame: NSMakeRect(0, 0, 350, 400)) bar = .init(height: 0) - arguments = StickerPackArguments(account: account, send: { [weak self] media in - self?.close() - if let peerId = peerId, let strongSelf = self { - - var attributes:[MessageAttribute] = [] - if FastSettings.isChannelMessagesMuted(peerId) { - attributes.append(NotificationInfoMessageAttribute(flags: [.muted])) + arguments = StickerPackArguments(context: context, send: { [weak self] media, view in + let interactions = (context.sharedContext.bindings.rootNavigation().controller as? ChatController)?.chatInteraction + + if let interactions = interactions, let media = media as? TelegramMediaFile, media.maskData == nil { + if let slowMode = interactions.presentation.slowMode, slowMode.hasLocked { + showSlowModeTimeoutTooltip(slowMode, for: view) + } else { + interactions.sendAppFile(media) + self?.close() } - - _ = (strongSelf.account.postbox.loadedPeerWithId(peerId) |> filter {$0.canSendMessage && !$0.stickersRestricted} |> mapToSignal { _ in - return enqueueMessages(account: account, peerId: peerId, messages: [EnqueueMessage.message(text: "", attributes: attributes, media: media, replyToMessageId: nil)]) - }) .start() - } }, addpack: { [weak self] info, items, installed in self?.close() self?.disposable.dispose() - //installStickerSetInteractively(account: account, info: info, items: items) - _ = (!installed ? addStickerPackInteractively(postbox: account.postbox, info: info, items: items) : removeStickerPackInteractively(postbox: account.postbox, id: info.id)).start() + _ = (!installed ? addStickerPackInteractively(postbox: context.account.postbox, info: info, items: items) : removeStickerPackInteractively(postbox: context.account.postbox, id: info.id, option: .archive)).start() }, share: { [weak self] link in self?.close() - showModal(with: ShareModalController(ShareLinkObject(account, link: link)), for: mainWindow) + showModal(with: ShareModalController(ShareLinkObject(context, link: link)), for: mainWindow) }, close: { [weak self] in self?.close() }) @@ -256,17 +257,25 @@ class StickersPackPreviewModalController: ModalViewController { super.viewDidLoad() - disposable.set((loadedStickerPack(postbox: account.postbox, network: account.network, reference: reference) |> deliverOnMainQueue).start(next: { [weak self] result in - if let strongSelf = self { - strongSelf.genericView.layout(with: result, arguments: strongSelf.arguments) - strongSelf.readyOnce() + disposable.set((loadedStickerPack(postbox: context.account.postbox, network: context.account.network, reference: reference, forceActualized: false) |> deliverOnMainQueue).start(next: { [weak self] result in + guard let `self` = self else {return} + switch result { + case .none: + alert(for: mainWindow, info: L10n.stickerSetDontExist) + self.close() + default: + self.genericView.layout(with: result, arguments: self.arguments) + self.readyOnce() } - }, error: { error in - + })) } + override func becomeFirstResponder() -> Bool? { + return false + } + deinit { disposable.dispose() } diff --git a/Telegram-Mac/StickersViewController.swift b/Telegram-Mac/StickersViewController.swift new file mode 100644 index 0000000000..277f6ff36f --- /dev/null +++ b/Telegram-Mac/StickersViewController.swift @@ -0,0 +1,1324 @@ +// +// StickersViewController.swift +// Telegram +// +// Created by Mikhail Filimonov on 08/07/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit +import TelegramCore + +import Postbox + +final class StickerPanelArguments { + let context: AccountContext + let sendMedia:(Media, NSView, Bool)->Void + let showPack:(StickerPackReference)->Void + let navigate:(ItemCollectionViewEntryIndex)->Void + let addPack: (StickerPackReference)->Void + let clearRecent:()->Void + let removePack:(StickerPackCollectionId)->Void + let closeInlineFeatured:(Int64)->Void + let openFeatured:(FeaturedStickerPackItem)->Void + let mode: EntertainmentViewController.Mode + init(context: AccountContext, sendMedia: @escaping(Media, NSView, Bool)->Void, showPack: @escaping(StickerPackReference)->Void, addPack: @escaping(StickerPackReference)->Void, navigate: @escaping(ItemCollectionViewEntryIndex)->Void, clearRecent:@escaping()->Void, removePack:@escaping(StickerPackCollectionId)->Void, closeInlineFeatured:@escaping(Int64)->Void, openFeatured:@escaping(FeaturedStickerPackItem)->Void, mode: EntertainmentViewController.Mode) { + self.context = context + self.sendMedia = sendMedia + self.showPack = showPack + self.addPack = addPack + self.navigate = navigate + self.clearRecent = clearRecent + self.removePack = removePack + self.closeInlineFeatured = closeInlineFeatured + self.openFeatured = openFeatured + self.mode = mode + } +} + +extension FoundStickerSets { + func updateInfos(_ f:([(ItemCollectionId, ItemCollectionInfo, ItemCollectionItem?, Bool)])->[(ItemCollectionId, ItemCollectionInfo, ItemCollectionItem?, Bool)]) -> FoundStickerSets { + return FoundStickerSets.init(infos: f(self.infos), entries: self.entries) + } +} + +struct SpecificPackData : Equatable { + let info: StickerPackCollectionInfo + let peer: Peer + + static func ==(lhs: SpecificPackData, rhs: SpecificPackData) -> Bool { + if lhs.info != rhs.info { + return false + } else if !lhs.peer.isEqual(rhs.peer) { + return false + } else { + return true + } + } +} + +enum PackEntry: Comparable, Identifiable { + case stickerPack(index:Int, stableId: StickerPackCollectionId, info: StickerPackCollectionInfo, topItem: StickerPackItem?) + case recent + case saved + case featured(hasUnread: Bool) + case specificPack(data: SpecificPackData) + + var stableId: StickerPackCollectionId { + switch self { + case let .stickerPack(data): + return data.stableId + case .recent: + return .recent + case .saved: + return .saved + case let .featured(hasUnread): + return .featured(hasUnred: hasUnread) + case let .specificPack(data): + return .specificPack(data.info.id) + } + } + + var index: Int { + switch self { + case .featured: + return -1 + case .saved: + return 0 + case .recent: + return 2 + case .specificPack: + return 3 + case let .stickerPack(index, _, _, _): + return 4 + index + } + } + + static func <(lhs: PackEntry, rhs: PackEntry) -> Bool { + return lhs.index < rhs.index + } + + +} + + +private enum StickerPacksUpdate { + case generic(animated: Bool, scrollToTop: Bool?) + case scroll(animated: Bool) + case navigate(StickerPacksIndex, animated: Bool) +} + + +private enum StickerPacksIndex : Hashable, Comparable { + case sticker(ItemCollectionViewEntryIndex) + case speficicPack(ItemCollectionId) + case recent(Int) + case saved(Int) + case featured(Int, Bool) + case emojiRelated(Int) + var packIndex:ItemCollectionViewEntryIndex { + switch self { + case let .sticker(index): + return index + case let .saved(index), let .recent(index), let .featured(index, _), let .emojiRelated(index): + return ItemCollectionViewEntryIndex.lowerBound(collectionIndex: Int32(index), collectionId: ItemCollectionId(namespace: 0, id: 0)) + case let .speficicPack(id): + return ItemCollectionViewEntryIndex.lowerBound(collectionIndex: 2, collectionId: id) + } + } + + var collectionId: StickerPackCollectionId { + switch self { + case let .sticker(index): + return .pack(index.collectionId) + case .recent: + return .recent + case .saved: + return .saved + case let .speficicPack(id): + return .specificPack(id) + case let .featured(_, hasUnread): + return .featured(hasUnred: hasUnread) + case .emojiRelated: + return .emojiRelated + } + } + + func hash(into hasher: inout Hasher) { + + } + + var index: Int { + switch self { + case .emojiRelated: + return -2 + case .featured: + return -1 + case .saved: + return 0 + case .recent: + return 1 + case .speficicPack: + return 2 + case .sticker: + return 3 + } + } + + static func <(lhs: StickerPacksIndex, rhs: StickerPacksIndex) -> Bool { + switch lhs { + case let .sticker(lhsIndex): + if case let .sticker(rhsIndex) = rhs { + return lhsIndex < rhsIndex + } else { + return lhs.index < rhs.index + } + default: + return lhs.index < rhs.index + } + } +} + +private enum StickerPacksScrollState: Equatable { + static func == (lhs: StickerPacksScrollState, rhs: StickerPacksScrollState) -> Bool { + switch lhs { + case .initial: + if case .initial = rhs { + return true + } else { + return false + } + case let .loadFeaturedMore(lhsFound): + if case .loadFeaturedMore(let rhsFound) = rhs { + return lhsFound.sets.infos.map { $0.0 } == rhsFound.sets.infos.map { $0.0 } + } else { + return false + } + case let .scroll(aroundIndex): + if case .scroll(aroundIndex) = rhs { + return true + } else { + return false + } + case let .navigate(aroundIndex): + if case .navigate(aroundIndex) = rhs { + return true + } else { + return false + } + } + } + + case initial + case loadFeaturedMore(StickerPacksSearchData) + case scroll(aroundIndex: StickerPacksIndex) + case navigate(index: StickerPacksIndex) +} + +private struct StickerPacksSearchData { + let sets: FoundStickerSets + let loading: Bool + let basicFeaturedCount: Int + let emojiRelated: [FoundStickerItem] +} + +private struct StickerPacksUpdateData { + let view: ItemCollectionsView? + let update: StickerPacksUpdate + let specificPack:Tuple2? + let searchData: StickerPacksSearchData? + let hasUnread: Bool + let featured: [FeaturedStickerPackItem] + let settings: StickerSettings + let mode: EntertainmentViewController.Mode + init(view: ItemCollectionsView?, update: StickerPacksUpdate, specificPack: Tuple2?, searchData: StickerPacksSearchData? = nil, hasUnread: Bool, featured: [FeaturedStickerPackItem], settings: StickerSettings = .defaultSettings, mode: EntertainmentViewController.Mode) { + self.view = view + self.update = update + self.specificPack = specificPack + self.searchData = searchData + self.hasUnread = hasUnread + self.featured = featured + self.settings = settings + self.mode = mode + } + + func withUpdatedHasUnread(_ hasUnread: Bool) -> StickerPacksUpdateData { + return .init(view: self.view, update: self.update, specificPack: self.specificPack, searchData: self.searchData, hasUnread: hasUnread, featured: self.featured, settings: self.settings, mode: self.mode) + } +} +enum StickerPackInfo : Equatable { + case pack(StickerPackCollectionInfo?, installed: Bool, featured: Bool) + case speficicPack(StickerPackCollectionInfo?) + case recent + case saved + case emojiRelated + + var installed: Bool { + switch self { + case let .pack(_, installed, _): + return installed + default: + return true + } + } + var featured: Bool { + switch self { + case let .pack(_, _, featured): + return featured + default: + return false + } + } +} + +enum StickerPackCollectionId : Hashable { + case pack(ItemCollectionId) + case recent + case featured(hasUnred: Bool) + case specificPack(ItemCollectionId) + case saved + case inlineFeatured(hasUnred: Bool) + case emojiRelated + var itemCollectionId:ItemCollectionId? { + switch self { + case let .pack(collectionId): + return collectionId + case let .specificPack(collectionId): + return collectionId + default: + return nil + } + } + +} + + +private enum StickerPackEntry : TableItemListNodeEntry { + case pack(index: StickerPacksIndex, files:[TelegramMediaFile], packInfo: StickerPackInfo, collectionId: StickerPackCollectionId) + case trending(index: StickerPacksIndex, featured: [FeaturedStickerPackItem], collectionId: StickerPackCollectionId) + + static func < (lhs: StickerPackEntry, rhs: StickerPackEntry) -> Bool { + return lhs.index < rhs.index + } + + static func == (lhs: StickerPackEntry, rhs: StickerPackEntry) -> Bool { + switch lhs { + case let .pack(index, lhsFiles, packInfo, collectionId): + if case .pack(index, let rhsFiles, packInfo, collectionId) = rhs { + if lhsFiles.count != rhsFiles.count { + return false + } else { + for (i, lhsFile) in lhsFiles.enumerated() { + if !lhsFile.isEqual(to: rhsFiles[i]) { + return false + } + } + } + return true + } else { + return false + } + case let .trending(index, lhsFeatured, collectionId): + if case .trending(index, let rhsFeatured, collectionId) = rhs { + if lhsFeatured.count != rhsFeatured.count { + return false + } else { + for (i, lhsItem) in lhsFeatured.enumerated() { + if lhsItem.info.id != rhsFeatured[i].info.id { + return false + } + } + } + return true + } else { + return false + } + } + } + + var index: StickerPacksIndex { + switch self { + case let .pack(index, _, _, _): + return index + case let .trending(index, _, _): + return index + } + } + + var stableId: StickerPackCollectionId { + switch self { + case let .pack(_, _, _, collectionId): + return collectionId + case let .trending(_, _, collectionId): + return collectionId + } + } + + func item(_ arguments: StickerPanelArguments, initialSize: NSSize) -> TableRowItem { + switch self { + case let .pack(_, files, packInfo, collectionId): + return StickerPackPanelRowItem(initialSize, context: arguments.context, arguments: arguments, files: files, packInfo: packInfo, collectionId: collectionId) + case let .trending(_, items, collectionId): + return StickerPackTrendingItem(initialSize, context: arguments.context, featured: items, collectionId: collectionId, close: arguments.closeInlineFeatured, click: arguments.openFeatured) + } + } +} + +private func stickersEntries(view: ItemCollectionsView?, featured:[FeaturedStickerPackItem], settings: StickerSettings, searchData: StickerPacksSearchData?, specificPack:Tuple2?, mode: EntertainmentViewController.Mode) -> [StickerPackEntry] { + var entries:[StickerPackEntry] = [] + + if let view = view { + var available: [ItemCollectionViewEntry] = view.entries + var index: Int32 = 0 + + var ids:[MediaId : MediaId] = [:] + + if view.lower == nil { + if !view.orderedItemListsViews[1].items.isEmpty { + var files:[TelegramMediaFile] = [] + for item in view.orderedItemListsViews[1].items { + if let entry = item.contents as? SavedStickerItem { + if let id = entry.file.id, ids[id] == nil, entry.file.isStaticSticker || entry.file.isAnimatedSticker { + ids[id] = id + files.append(entry.file) + } + } + } + if !files.isEmpty { + entries.append(.pack(index: .saved(0), files: files, packInfo: .saved, collectionId: .saved)) + } + } + + if !featured.isEmpty, mode == .common { + if settings.trendingClosedOn != featured.first?.info.id.id { + entries.append(.trending(index: .saved(1), featured: featured, collectionId: .inlineFeatured(hasUnred: featured.contains(where: { $0.unread })))) + } + } + + + + if !view.orderedItemListsViews[0].items.isEmpty { + var files:[TelegramMediaFile] = [] + for item in view.orderedItemListsViews[0].items { + if let entry = item.contents as? RecentMediaItem { + if let file = entry.media as? TelegramMediaFile, let id = file.id, ids[id] == nil, file.isStaticSticker || file.isAnimatedSticker { + ids[id] = id + files.append(file) + } + } + if files.count == 20 { + break + } + } + if !files.isEmpty { + entries.append(.pack(index: .recent(1), files: files, packInfo: .recent, collectionId: .recent)) + } + } + + + if let specificPack = specificPack, let info = specificPack._0.packInfo { + var files:[TelegramMediaFile] = [] + for item in info.1 { + if let item = item as? StickerPackItem { + if let id = item.file.id, ids[id] == nil, item.file.isStaticSticker || item.file.isAnimatedSticker { + ids[id] = id + files.append(item.file) + } + } + } + if !files.isEmpty { + entries.append(.pack(index: .speficicPack(info.0.id), files: files, packInfo: .speficicPack(info.0), collectionId: .specificPack(info.0.id))) + } + } + + } + + for (id, info, item) in view.collectionInfos { + if !available.isEmpty, let item = item { + var files: [TelegramMediaFile] = [] + if let info = info as? StickerPackCollectionInfo { + let items = available.enumerated().reversed() + for (i, entry) in items { + if entry.index.collectionId == info.id { + if let item = available.remove(at: i).item as? StickerPackItem { + files.insert(item.file, at: 0) + } + } + } + if !files.isEmpty { + entries.append(.pack(index: .sticker(ItemCollectionViewEntryIndex(collectionIndex: index, collectionId: id, itemIndex: item.index)), files: files, packInfo: .pack(info, installed: true, featured: false), collectionId: .pack(id))) + } + } + } else { + break + } + index += 1 + } + } else if let searchData = searchData { + if !searchData.loading { + var available = searchData.sets.entries + var index: Int32 = 0 + + if !searchData.emojiRelated.isEmpty { + + var validIds:Set = Set() + + let files:[TelegramMediaFile] = searchData.emojiRelated.map { $0.file }.reduce([], { current, value in + var current = current + guard let id = value.id else { + return current + } + if !validIds.contains(id) { + validIds.insert(id) + current.append(value) + } + return current + }).sorted(by: { lhs, rhs in + if lhs.isAnimatedSticker && !rhs.isAnimatedSticker { + return true + } else { + return false + } + }) + entries.append(.pack(index: .emojiRelated(0), files: files, packInfo: .emojiRelated, collectionId: .emojiRelated)) + + index += 1 + } + if mode == .common { + for set in searchData.sets.infos { + if !available.isEmpty { + var files: [TelegramMediaFile] = [] + if let info = set.1 as? StickerPackCollectionInfo { + let items = available.enumerated().reversed() + for (i, entry) in items { + if entry.index.collectionId == info.id { + if let item = available.remove(at: i).item as? StickerPackItem { + files.insert(item.file, at: 0) + } + } + } + if !files.isEmpty { + entries.append(.pack(index: .sticker(ItemCollectionViewEntryIndex(collectionIndex: index, collectionId: info.id, itemIndex: .init(index: 0, id: 0))), files: Array(files.prefix(5)), packInfo: .pack(info, installed: set.3, featured: true), collectionId: .pack(info.id))) + } + } + } else { + break + } + index += 1 + } + } + } + + } + + return entries +} + +private func packEntries(view: ItemCollectionsView?, specificPack:Tuple2?, hasUnread: Bool, featured:[FeaturedStickerPackItem], settings: StickerSettings, mode: EntertainmentViewController.Mode) -> [PackEntry] { + var entries:[PackEntry] = [] + var index: Int = 0 + + if let view = view { + if !featured.isEmpty, mode == .common { + if settings.trendingClosedOn == featured.first?.info.id.id { + entries.append(.featured(hasUnread: hasUnread)) + } + } + + if !view.orderedItemListsViews[1].items.isEmpty { + entries.append(.saved) + } + if !view.orderedItemListsViews[0].items.isEmpty { + entries.append(.recent) + } + if let specificPack = specificPack, let info = specificPack._0.packInfo?.0 { + entries.append(.specificPack(data: SpecificPackData(info: info, peer: specificPack._1))) + } + + for (_, info, item) in view.collectionInfos { + if let info = info as? StickerPackCollectionInfo { + entries.append(.stickerPack(index: index, stableId: .pack(info.id), info: info, topItem: item as? StickerPackItem)) + index += 1 + } + } + } + + return entries +} + + +private func prepareStickersTransition(from:[AppearanceWrapperEntry], to: [AppearanceWrapperEntry], initialSize: NSSize, arguments: StickerPanelArguments, update: StickerPacksUpdate) -> TableUpdateTransition { + let (removed,inserted,updated) = proccessEntriesWithoutReverse(from, right: to, { entry -> TableRowItem in + return entry.entry.item(arguments, initialSize: initialSize) + }) + let state: TableScrollState + var anim: Bool + switch update { + case let .generic(animated, scrollToTop): + anim = animated + if let scrollToTop = scrollToTop { + if scrollToTop { + state = .up(animated) + } else { + state = .saveVisible(.lower) + } + } else { + state = .none(nil) + } + + case let .scroll(animated): + state = .saveVisible(.upper) + anim = animated + case let .navigate(index, animated): + state = .top(id: index.collectionId, innerId: nil, animated: true, focus: .init(focus: false), inset: 0) + anim = animated + } + return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: anim, state: state, grouping: !anim) +} + +fileprivate func preparePackTransition(from:[AppearanceWrapperEntry]?, to:[AppearanceWrapperEntry], context: AccountContext, initialSize:NSSize) -> TableUpdateTransition { + + let (deleted,inserted,updated) = proccessEntriesWithoutReverse(from, right: to, { (entry) -> TableRowItem in + switch entry.entry { + case let .stickerPack(index, stableId, info, topItem): + return StickerPackRowItem(initialSize, packIndex: index, context: context, stableId: stableId, info: info, topItem: topItem) + case .recent: + return RecentPackRowItem(initialSize, entry.entry.stableId) + case .featured: + return RecentPackRowItem(initialSize, entry.entry.stableId) + case .saved: + return RecentPackRowItem(initialSize, entry.entry.stableId) + case let .specificPack(data): + return StickerSpecificPackItem(initialSize, stableId: entry.entry.stableId, specificPack: (data.info, data.peer), account: context.account) + } + }) + + return TableUpdateTransition(deleted: deleted, inserted: inserted, updated:updated, animated: true, state: .none(nil)) + +} + +class NStickersView : View { + fileprivate let tableView:TableView = TableView(frame: NSZeroRect) + fileprivate var restrictedView:RestrictionWrappedView? + private let emptySearchView = ImageView() + private let emptySearchContainer: View = View() + + let searchView = SearchView(frame: .zero) + private let searchContainer = View() + fileprivate let packsView:HorizontalTableView = HorizontalTableView(frame: NSZeroRect) + private let separator:View = View() + fileprivate let tabsContainer: View = View() + + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + + addSubview(tableView) + + searchContainer.addSubview(searchView) + addSubview(searchContainer) + + emptySearchContainer.addSubview(emptySearchView) + tabsContainer.addSubview(packsView) + tabsContainer.addSubview(separator) + addSubview(tabsContainer) + addSubview(emptySearchContainer) + + emptySearchContainer.isHidden = true + emptySearchContainer.isEventLess = true + + updateLocalizationAndTheme(theme: theme) + } + + func updateRestricion(_ peer: Peer?) { + if let peer = peer, let text = permissionText(from: peer, for: .banSendStickers) { + restrictedView?.removeFromSuperview() + restrictedView = RestrictionWrappedView(text) + addSubview(restrictedView!) + } else { + restrictedView?.removeFromSuperview() + restrictedView = nil + } + setFrameSize(frame.size) + needsLayout = true + } + + func updateEmpties(isEmpty: Bool, animated: Bool) { + + let emptySearchHidden: Bool = !isEmpty + + if !emptySearchHidden { + emptySearchContainer.isHidden = false + } + + emptySearchContainer.change(opacity: emptySearchHidden ? 0 : 1, animated: animated, completion: { [weak self] completed in + if completed { + self?.emptySearchContainer.isHidden = emptySearchHidden + } + }) + + needsLayout = true + } + + private var searchState: SearchState? = nil + + func updateSearchState(_ searchState: SearchState, animated: Bool) { + self.searchState = searchState + switch searchState.state { + case .Focus: + tabsContainer.change(pos: NSMakePoint(0, -tabsContainer.frame.height), animated: animated) + searchContainer.change(pos: NSMakePoint(0, tabsContainer.frame.maxY), animated: animated) + case .None: + tabsContainer.change(pos: NSMakePoint(0, 0), animated: animated) + searchContainer.change(pos: NSMakePoint(0, tabsContainer.frame.maxY), animated: animated) + } + tableView.change(size: NSMakeSize(frame.width, frame.height - searchContainer.frame.maxY), animated: animated) + tableView.change(pos: NSMakePoint(0, searchContainer.frame.maxY), animated: animated) + + } + + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + self.restrictedView?.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) + self.separator.backgroundColor = theme.colors.border + self.tableView.updateLocalizationAndTheme(theme: theme) + self.tableView.backgroundColor = theme.colors.background + self.tableView.documentView?.background = theme.colors.background + self.emptySearchView.image = theme.icons.stickersEmptySearch + self.emptySearchView.sizeToFit() + self.emptySearchContainer.backgroundColor = theme.colors.background + self.searchView.updateLocalizationAndTheme(theme: theme) + } + + override func setFrameSize(_ newSize: NSSize) { + super.setFrameSize(newSize) + } + + override func layout() { + super.layout() + + let initial: CGFloat = searchState?.state == .Focus ? -50 : 0 + + tabsContainer.frame = NSMakeRect(0, initial, frame.width, 50) + separator.frame = NSMakeRect(0, tabsContainer.frame.height - .borderSize, tabsContainer.frame.width, .borderSize) + packsView.frame = tabsContainer.focus(NSMakeSize(frame.width, 40)) + + + searchContainer.frame = NSMakeRect(0, tabsContainer.frame.maxY, frame.width, 50) + searchView.setFrameSize(NSMakeSize(frame.width - 20, 30)) + searchView.center() + + + tableView.frame = NSMakeRect(0, searchContainer.frame.maxY, frame.width, frame.height - searchContainer.frame.maxY) + restrictedView?.setFrameSize(frame.size) + + emptySearchContainer.frame = tableView.frame + emptySearchView.center() + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + + +class NStickersViewController: TelegramGenericViewController, TableViewDelegate, Notifable { + + private let searchValue = ValuePromise(.init(state: .None, request: nil)) + private var searchState: SearchState = .init(state: .None, request: nil) { + didSet { + self.searchValue.set(searchState) + } + } + private let position = ValuePromise(ignoreRepeated: true) + private let disposable = MetaDisposable() + private let searchStateDisposable = MetaDisposable() + private let specificPeerId = ValuePromise(PeerId(0), ignoreRepeated: true) + private var listener: TableScrollListener! + private var interactions: EntertainmentInteractions? + private weak var chatInteraction: ChatInteraction? + var makeSearchCommand:((ESearchCommand)->Void)? + + + var mode: EntertainmentViewController.Mode = .common + + override init(_ context: AccountContext) { + super.init(context) + bar = .init(height: 0) + _frameRect = NSMakeRect(0, 0, 350, 350) + } + + private func updateSearchState(_ state: SearchState) { + self.position.set(.initial) + self.searchState = state + if !state.request.isEmpty { + self.makeSearchCommand?(.loading) + } + if self.isLoaded() == true { + self.genericView.updateSearchState(state, animated: true) + self.genericView.tableView.scroll(to: .up(true)) + + } + } + + deinit { + disposable.dispose() + searchStateDisposable.dispose() + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + self.genericView.packsView.updateLocalizationAndTheme(theme: theme) + } + + func update(with interactions:EntertainmentInteractions, chatInteraction: ChatInteraction) { + self.interactions = interactions + self.chatInteraction?.remove(observer: self) + self.chatInteraction = chatInteraction + chatInteraction.add(observer: self) + if isLoaded() { + genericView.updateRestricion(chatInteraction.presentation.peer) + } + self.specificPeerId.set(chatInteraction.peerId) + } + + func notify(with value: Any, oldValue: Any, animated: Bool) { + if let value = value as? ChatPresentationInterfaceState, let oldValue = oldValue as? ChatPresentationInterfaceState { + if let peer = value.peer, let oldPeer = oldValue.peer { + if permissionText(from: peer, for: .banSendStickers) != permissionText(from: oldPeer, for: .banSendStickers) { + genericView.updateRestricion(peer) + } + } else if (oldValue.peer != nil) != (value.peer != nil), let peer = value.peer { + genericView.updateRestricion(peer) + } + } + } + + func isEqual(to other: Notifable) -> Bool { + return other === self + } + + func isSelectable(row: Int, item: TableRowItem) -> Bool { + return true + } + func selectionWillChange(row: Int, item: TableRowItem, byClick: Bool) -> Bool { + return true + } + func selectionDidChange(row:Int, item:TableRowItem, byClick:Bool, isNew:Bool) { + if byClick, let collectionId = item.stableId.base as? StickerPackCollectionId { + if let item = genericView.tableView.item(stableId: collectionId) { + self.genericView.tableView.removeScroll(listener: self.listener) + self.genericView.tableView.scroll(to: .top(id: item.stableId, innerId: nil, animated: true, focus: .init(focus: false), inset: 0), completion: { [weak self] _ in + if let `self` = self { + self.genericView.tableView.addScroll(listener: self.listener) + } + }) + } else { + var index: StickerPacksIndex? = nil + switch collectionId { + case let .pack(id): + if let item = item as? StickerPackRowItem { + index = .sticker(ItemCollectionViewEntryIndex.lowerBound(collectionIndex: Int32(item.packIndex), collectionId: id)) + } + case .featured, .inlineFeatured: + self.interactions?.toggleSearch() + case .saved: + index = .saved(0) + case .recent: + index = .recent(1) + case let .specificPack(id): + index = .speficicPack(id) + case .emojiRelated: + break + } + if let index = index { + self.genericView.tableView.removeScroll(listener: self.listener) + self.position.set(.navigate(index: index)) + } + } + + } + } + func findGroupStableId(for stableId: AnyHashable) -> AnyHashable? { + return nil + } + + private func shouldSendActivity(_ isPresent: Bool) { + if let chatInteraction = chatInteraction { + if chatInteraction.peerId.toInt64() != 0 { + chatInteraction.context.account.updateLocalInputActivity(peerId: chatInteraction.activitySpace, activity: .choosingSticker, isPresent: isPresent) + + } + } + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + self.shouldSendActivity(false) + } + + override func viewDidLoad() { + super.viewDidLoad() + let context = self.context + let initialSize = self.atomicSize + + genericView.tableView.addScroll(listener: TableScrollListener.init(dispatchWhenVisibleRangeUpdated: false, { [weak self] _ in + self?.shouldSendActivity(true) + })) + + + let searchInteractions = SearchInteractions({ [weak self] state, _ in + self?.updateSearchState(state) + }, { [weak self] state in + self?.updateSearchState(state) + }) + + genericView.searchView.searchInteractions = searchInteractions + + listener = TableScrollListener(dispatchWhenVisibleRangeUpdated: false, { [weak self] position in + guard let `self` = self, position.visibleRows.length > 0 else { + return + } + let item = self.genericView.tableView.item(at: position.visibleRows.location) + self.genericView.packsView.changeSelection(stableId: item.stableId) + self.genericView.packsView.scroll(to: .center(id: item.stableId, innerId: nil, animated: true, focus: .init(focus: false), inset: 0)) + }) + + self.genericView.packsView.delegate = self + + let previous:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) + + let foundPacks: Atomic = Atomic(value: nil) + + let previousPacks:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) + + let arguments = StickerPanelArguments(context: context, sendMedia: { [weak self] media, view, silent in + guard let `self` = self else { return } + if let chatInteraction = self.chatInteraction, let slowMode = chatInteraction.presentation.slowMode, slowMode.hasLocked { + showSlowModeTimeoutTooltip(slowMode, for: view) + } else if let file = media as? TelegramMediaFile { + self.interactions?.sendSticker(file, silent) + } + }, showPack: { [weak self] reference in + if let peerId = self?.chatInteraction?.peerId { + showModal(with: StickerPackPreviewModalController(context, peerId: peerId, reference: reference), for: context.window) + } + }, addPack: { [weak self] reference in + + + + _ = showModalProgress(signal: context.engine.stickers.loadedStickerPack(reference: reference, forceActualized: false) + |> filter { result in + switch result { + case .result: + return true + default: + return false + } + } + |> take(1) + |> mapToSignal { result -> Signal in + switch result { + case let .result(info, items, _): + return context.engine.stickers.addStickerPackInteractively(info: info, items: items) |> map { info.id } + default: + return .complete() + } + } + |> deliverOnMainQueue, for: context.window).start(next: { [weak self] result in + if let `self` = self { + if !self.searchState.request.isEmpty { + self.makeSearchCommand?(.close) + self.position.set(.navigate(index: StickerPacksIndex.sticker(ItemCollectionViewEntryIndex.lowerBound(collectionIndex: 0, collectionId: result)))) + } + } + }) + }, navigate: { [weak self] index in + self?.position.set(.navigate(index: .sticker(index))) + }, clearRecent: { + confirm(for: context.window, header: L10n.stickersConfirmClearRecentHeader, information: L10n.stickersConfirmClearRecentText, okTitle: L10n.stickersConfirmClearRecentOK, successHandler: { _ in + _ = context.account.postbox.transaction({ transaction in + clearRecentlyUsedStickers(transaction: transaction) + }).start() + }) + }, removePack: { collectionId in + if let id = collectionId.itemCollectionId { + _ = showModalProgress(signal: context.engine.stickers.removeStickerPackInteractively(id: id, option: .delete), for: context.window).start() + } + }, closeInlineFeatured: { id in + _ = updateStickerSettingsInteractively(postbox: context.account.postbox, { + $0.withUpdatedTrendingClosedOn(id) + }).start() + }, openFeatured: { [weak self] featured in + self?.genericView.searchView.change(state: .Focus, true) + }, mode: mode) + + let specificPackData: Signal?, NoError> = self.specificPeerId.get() |> mapToSignal { peerId -> Signal in + if peerId.toInt64() == 0 { + return .single(nil) + } else { + return context.account.postbox.transaction { + $0.getPeer(peerId) + } + } + } |> mapToSignal { peer -> Signal?, NoError> in + if let peer = peer, peer.isSupergroup { + return context.engine.peers.peerSpecificStickerPack(peerId: peer.id) |> map { data in + return Tuple2(data, peer) + } + } else { + return .single(nil) + } + } + let mode = self.mode + + let signal = combineLatest(queue: prepareQueue, self.searchValue.get(), self.position.get()) |> mapToSignal { values -> Signal in + + let count = initialSize.with { size -> Int in + return Int(round((size.height * (values.1 == .initial ? 2 : 20)) / 60 * 5)) + } + if values.0.state == .None { + var firstTime: Bool = true + + let settings = stickerSettings(postbox: context.account.postbox) + switch values.1 { + case .initial: + let packsView = context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudSavedStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: nil, count: count) + let featuredView = context.account.viewTracker.featuredStickerPacks() + + return combineLatest(packsView, featuredView, settings) |> mapToSignal { view, featured, settings in + return specificPackData |> map { specificPack in + let scrollToTop = firstTime + firstTime = false + return StickerPacksUpdateData(view: view, update: .generic(animated: scrollToTop, scrollToTop: scrollToTop), specificPack: specificPack, hasUnread: false, featured: featured, settings: settings, mode: mode) + } + } + case let .scroll(aroundIndex): + var firstTime = true + let packsView = context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudSavedStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: aroundIndex.packIndex, count: count) + let featuredView = context.account.viewTracker.featuredStickerPacks() + + + return combineLatest(packsView, featuredView, settings) + |> mapToSignal { view, featured, settings in + return specificPackData |> map { specificPack in + let update: StickerPacksUpdate + if firstTime { + firstTime = false + update = .scroll(animated: false) + } else { + update = .generic(animated: false, scrollToTop: false) + } + return StickerPacksUpdateData(view: view, update: update, specificPack: specificPack, hasUnread: false, featured: featured, settings: settings, mode: mode) + } + } + case let .navigate(index): + var firstTime = true + return context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudSavedStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: index.packIndex, count: count) + |> mapToSignal { view in + return specificPackData |> map { specificPack in + let update: StickerPacksUpdate + if firstTime { + firstTime = false + update = .navigate(index, animated: true) + } else { + update = .generic(animated: false, scrollToTop: false) + } + return StickerPacksUpdateData(view: view, update: update, specificPack: specificPack, hasUnread: false, featured: [], mode: mode) + } + } + case .loadFeaturedMore: + fatalError("load featured for basic packs is not possible") + } + } else { + let searchText = values.0.request.lowercased() + if values.0.request.isEmpty { + switch values.1 { + case .initial: + return combineLatest(context.account.viewTracker.featuredStickerPacks(), context.account.postbox.combinedView(keys: [.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])])) |> map { value, view in + var found = FoundStickerSets() + + var installedPacks = Set() + if let stickerPacksView = view.views[.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])] as? ItemCollectionInfosView { + if let packsEntries = stickerPacksView.entriesByNamespace[Namespaces.ItemCollection.CloudStickerPacks] { + for entry in packsEntries { + installedPacks.insert(entry.id) + } + } + } + + for (collectionIndex, set) in value.enumerated() { + var entries:[ItemCollectionViewEntry] = [] + + for item in set.topItems { + entries.append(ItemCollectionViewEntry(index: ItemCollectionViewEntryIndex(collectionIndex: Int32(collectionIndex), collectionId: set.info.id, itemIndex: item.index), item: item)) + } + if !entries.isEmpty { + found = found.merge(with: FoundStickerSets(infos: [(set.info.id, set.info, nil, installedPacks.contains(set.info.id))], entries: entries)) + } + } + let searchData = StickerPacksSearchData(sets: found, loading: false, basicFeaturedCount: found.infos.count, emojiRelated: []) + return StickerPacksUpdateData(view: nil, update: .generic(animated: true, scrollToTop: true), specificPack: nil, searchData: searchData, hasUnread: false, featured: [], mode: mode) + } + case let .loadFeaturedMore(current): + return combineLatest(requestOldFeaturedStickerPacks(network: context.account.network, postbox: context.account.postbox, offset: current.sets.infos.count - current.basicFeaturedCount, limit: 50), context.account.postbox.combinedView(keys: [.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])])) |> map { values, view in + var found = current.sets + + + var installedPacks = Set() + if let stickerPacksView = view.views[.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])] as? ItemCollectionInfosView { + if let packsEntries = stickerPacksView.entriesByNamespace[Namespaces.ItemCollection.CloudStickerPacks] { + for entry in packsEntries { + installedPacks.insert(entry.id) + } + } + } + + found = found.updateInfos( { infos in + var infos = infos + for (i, info) in infos.enumerated() { + infos[i] = (info.0, info.1, info.2, installedPacks.contains(info.0)) + } + return infos + }) + + for (collectionIndex, set) in values.enumerated() { + var entries:[ItemCollectionViewEntry] = [] + + for item in set.topItems { + entries.append(ItemCollectionViewEntry(index: ItemCollectionViewEntryIndex(collectionIndex: Int32(collectionIndex), collectionId: set.info.id, itemIndex: item.index), item: item)) + } + if !entries.isEmpty { + found = found.merge(with: FoundStickerSets(infos: [(set.info.id, set.info, nil, installedPacks.contains(set.info.id))], entries: entries)) + } + } + let searchData = StickerPacksSearchData(sets: found, loading: false, basicFeaturedCount: current.basicFeaturedCount, emojiRelated: []) + return StickerPacksUpdateData(view: nil, update: .generic(animated: false, scrollToTop: nil), specificPack: nil, searchData: searchData, hasUnread: false, featured: [], mode: mode) + } + default: + fatalError() + } + + } else { + let searchLocal = context.engine.stickers.searchStickerSets(query: searchText) |> delay(0.2, queue: prepareQueue) |> map(Optional.init) + let searchRemote = context.engine.stickers.searchStickerSetsRemotely(query: searchText) |> delay(0.2, queue: prepareQueue) |> map(Optional.init) + + let emojiRelated: Signal<[FoundStickerItem], NoError> = context.sharedContext.inputSource.searchEmoji(postbox: context.account.postbox, engine: context.engine, sharedContext: context.sharedContext, query: searchText, completeMatch: true, checkPrediction: false) |> mapToSignal { emojis in + + let signals = emojis.map { + context.engine.stickers.searchStickers(query: $0, scope: [.installed]) + } + return combineLatest(signals) |> map { + $0.reduce([], { current, value in + return current + value.filter { $0.file.stickerText != nil && emojis.contains($0.file.stickerText!) } + }) + } + } |> delay(0.2, queue: prepareQueue) + + return combineLatest(searchLocal, searchRemote, emojiRelated) |> map { local, remote, emojiRelated in + var value = FoundStickerSets() + if let local = local { + value = value.merge(with: local) + } + if let remote = remote { + value = value.merge(with: remote) + } + + let searchData = StickerPacksSearchData(sets: value, loading: remote == nil && value.entries.isEmpty, basicFeaturedCount: 0, emojiRelated: emojiRelated) + return StickerPacksUpdateData(view: nil, update: .generic(animated: true, scrollToTop: nil), specificPack: nil, searchData: searchData, hasUnread: false, featured: [], mode: mode) + } + } + + } + + } |> deliverOnPrepareQueue + |> mapToSignal { data -> Signal in + let hasUnread = context.account.viewTracker.featuredStickerPacks() |> map { featured in + return featured.contains(where: { $0.unread }) + } + return hasUnread |> map { + return data.withUpdatedHasUnread($0) + } + } + + let transition = combineLatest(queue: prepareQueue, appearanceSignal, signal) + |> map { appearance, data -> (TableUpdateTransition, TableUpdateTransition, [AppearanceWrapperEntry]) in + + _ = foundPacks.swap(data.searchData) + + let entries = stickersEntries(view: data.view, featured: data.featured, settings: data.settings, searchData: data.searchData, specificPack: data.specificPack, mode: mode).map { AppearanceWrapperEntry(entry: $0, appearance: appearance) } + let from = previous.swap(entries) + + let entriesPack = packEntries(view: data.view, specificPack: data.specificPack, hasUnread: data.hasUnread, featured: data.featured, settings: data.settings, mode: mode).map { AppearanceWrapperEntry(entry: $0, appearance: appearance) } + let fromPacks = previousPacks.swap(entriesPack) + + let transition = prepareStickersTransition(from: from, to: entries, initialSize: initialSize.with { $0 }, arguments: arguments, update: data.update) + let packTransition = preparePackTransition(from: fromPacks, to: entriesPack, context: context, initialSize: initialSize.with { $0 }) + + return (transition, packTransition, entriesPack) + } |> deliverOnMainQueue + + var first: Bool = true + + disposable.set(transition.start(next: { [weak self] (transition, packTransition, entriesPack) in + guard let `self` = self else { return } + + self.genericView.tableView.removeScroll(listener: self.listener) + self.genericView.tableView.merge(with: transition) + self.genericView.packsView.merge(with: packTransition) + self.genericView.updateEmpties(isEmpty: self.genericView.tableView.isEmpty, animated: !first) + + self.genericView.tableView.addScroll(listener: self.listener) + first = false + + var visibleRows = self.genericView.tableView.visibleRows() + if visibleRows.length == 0, !self.genericView.tableView.isEmpty { + visibleRows.location = 0 + visibleRows.length = 1 + } + if visibleRows.length > 0 { + let item = self.genericView.tableView.item(at: visibleRows.location) + self.genericView.packsView.changeSelection(stableId: item.stableId) + } + + self.makeSearchCommand?(.normal) + + + if !packTransition.isEmpty { + var resortRange: NSRange = NSMakeRange(0, 0) + let entries = entriesPack.map( {$0.entry }) + + for entry in entries { + switch entry { + case .saved, .recent, .specificPack, .featured: + resortRange.location += 1 + default: + break + } + } + if entries.count > resortRange.location { + resortRange.length = entries.count - resortRange.location + } + self.genericView.packsView.resortController = TableResortController(resortRange: resortRange, start: { _ in }, resort: { _ in }, complete: { fromIndex, toIndex in + + + if fromIndex == toIndex { + return + } + + let entries = entriesPack.map( {$0.entry }) + + + let fromEntry = entries[fromIndex] + + guard case let .stickerPack(_, _, fromPackInfo, _) = fromEntry else { + return + } + + var referenceId: ItemCollectionId? + var beforeAll = false + var afterAll = false + if toIndex < entries.count { + switch entries[toIndex] { + case let .stickerPack(_, _, toPackInfo, _): + referenceId = toPackInfo.id + default: + if entries[toIndex] < fromEntry { + beforeAll = true + } else { + afterAll = true + } + } + } else { + afterAll = true + } + + + let _ = (context.account.postbox.transaction { transaction -> Void in + var infos = transaction.getItemCollectionsInfos(namespace: Namespaces.ItemCollection.CloudStickerPacks) + var reorderInfo: ItemCollectionInfo? + for i in 0 ..< infos.count { + if infos[i].0 == fromPackInfo.id { + reorderInfo = infos[i].1 + infos.remove(at: i) + break + } + } + if let reorderInfo = reorderInfo { + if let referenceId = referenceId { + var inserted = false + for i in 0 ..< infos.count { + if infos[i].0 == referenceId { + if fromIndex < toIndex { + infos.insert((fromPackInfo.id, reorderInfo), at: i + 1) + } else { + infos.insert((fromPackInfo.id, reorderInfo), at: i) + } + inserted = true + break + } + } + if !inserted { + infos.append((fromPackInfo.id, reorderInfo)) + } + } else if beforeAll { + infos.insert((fromPackInfo.id, reorderInfo), at: 0) + } else if afterAll { + infos.append((fromPackInfo.id, reorderInfo)) + } + addSynchronizeInstalledStickerPacksOperation(transaction: transaction, namespace: Namespaces.ItemCollection.CloudStickerPacks, content: .sync, noDelay: false) + transaction.replaceItemCollectionInfos(namespace: Namespaces.ItemCollection.CloudStickerPacks, itemCollectionInfos: infos) + } + } |> deliverOnMainQueue).start(completed: { [weak self] in + if let `self` = self { + self.genericView.tableView.removeScroll(listener: self.listener) + } + }) + }) + } + + self.readyOnce() + })) + + self.genericView.tableView.setScrollHandler { [weak self] position in + if let `self` = self { + let entries = previous.with ({ $0 }) + let index:StickerPacksIndex? + + if let foundPacks = foundPacks.with ({ $0 }), self.searchState.state == .Focus { + self.position.set(.loadFeaturedMore(foundPacks)) + } else { + switch position.direction { + case .bottom: + index = entries.last?.entry.index + case .top: + index = entries.first?.entry.index + case .none: + index = nil + } + if let index = index, self.searchState.state == .None { + self.position.set(.scroll(aroundIndex: index)) + } + } + } + } + + self.position.set(.initial) + + } + override func scrollup(force: Bool = false) { + self.position.set(.initial) + self.genericView.packsView.scroll(to: .up(true)) + // self.genericView.tableView.scroll(to: .up(true)) + } + + override var supportSwipes: Bool { + return !self.genericView.packsView._mouseInside() + } + +} diff --git a/Telegram-Mac/StorageUsageCleanProgressRowItem.swift b/Telegram-Mac/StorageUsageCleanProgressRowItem.swift new file mode 100644 index 0000000000..647f38782d --- /dev/null +++ b/Telegram-Mac/StorageUsageCleanProgressRowItem.swift @@ -0,0 +1,78 @@ +// +// StorageUsageCleanProgressRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 03/08/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit + +class StorageUsageCleanProgressRowItem: GeneralRowItem { + private let task: CCTaskData + fileprivate var currentProgress: Float + init(_ initialSize: NSSize, stableId: AnyHashable, task: CCTaskData, viewType: GeneralViewType) { + self.task = task + self.currentProgress = task.currentProgress + super.init(initialSize, height: 40, stableId: stableId, viewType: viewType) + } + + override func viewClass() -> AnyClass { + return StorageUsageCleanProgressRowView.self + } + + + fileprivate var progress: Signal { + return self.task.progress |> deliverOnMainQueue + } +} + + +private final class StorageUsageCleanProgressRowView : GeneralContainableRowView { + + private let disposable = MetaDisposable() + + private let progressView: LinearProgressControl = LinearProgressControl(progressHeight: 4) + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(progressView) + } + + override func layout() { + super.layout() + + guard let item = item as? StorageUsageCleanProgressRowItem else { + return + } + progressView.setFrameSize(NSMakeSize(item.blockWidth - item.viewType.innerInset.left - item.viewType.innerInset.right, 4)) + progressView.center() + + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? StorageUsageCleanProgressRowItem else { + return + } + progressView.style = ControlStyle(foregroundColor: theme.colors.accent, backgroundColor: theme.colors.grayUI) + progressView.set(progress: CGFloat(item.currentProgress), animated: animated, duration: 0.2, timingFunction: .linear) + progressView.cornerRadius = 2 + + disposable.set(item.progress.start(next: { [weak self] value in + self?.progressView.set(progress: CGFloat(value), animated: true, duration: 0.2, timingFunction: .linear) + })) + + } + + deinit { + disposable.dispose() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/StorageUsageController.swift b/Telegram-Mac/StorageUsageController.swift index d24a083afc..a986a13d9d 100644 --- a/Telegram-Mac/StorageUsageController.swift +++ b/Telegram-Mac/StorageUsageController.swift @@ -8,19 +8,23 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac +import Postbox +import TelegramCore + +import SwiftSignalKit private final class StorageUsageControllerArguments { - let account: Account + let context: AccountContext let updateKeepMedia: () -> Void + let updateMediaLimit: (Int32) -> Void let openPeerMedia: (PeerId) -> Void - - init(account: Account, updateKeepMedia: @escaping () -> Void, openPeerMedia: @escaping (PeerId) -> Void) { - self.account = account + let clearAll:()->Void + init(context: AccountContext, updateKeepMedia: @escaping () -> Void, updateMediaLimit: @escaping(Int32)->Void, openPeerMedia: @escaping (PeerId) -> Void, clearAll: @escaping () -> Void) { + self.context = context self.updateKeepMedia = updateKeepMedia self.openPeerMedia = openPeerMedia + self.clearAll = clearAll + self.updateMediaLimit = updateMediaLimit } } @@ -30,28 +34,45 @@ private enum StorageUsageSection: Int32 { } private enum StorageUsageEntry: TableItemListNodeEntry { - case keepMedia(Int32, String, String) - case keepMediaInfo(Int32, String) - - case collecting(Int32, String) - case peersHeader(Int32, String) - case peer(Int32, Int32, Peer, String) + case keepMedia(Int32, String, String, GeneralViewType) + case keepMediaInfo(Int32, String, GeneralViewType) + case keepMediaLimitHeader(Int32, String, GeneralViewType) + case keepMediaLimit(Int32, Int32, GeneralViewType) + case keepMediaLimitInfo(Int32, String, GeneralViewType) + case ccTaskValue(Int32, CCTaskData, GeneralViewType) + case ccTaskValueDesc(Int32, String, GeneralViewType) + case clearAll(Int32, Bool, GeneralViewType) + case collecting(Int32, String, GeneralViewType) + case peersHeader(Int32, String, GeneralViewType) + case peer(Int32, Int32, Peer, String, GeneralViewType) case section(Int32) - var stableId: Int32 { + var stableId: Int64 { switch self { case .keepMedia: return 0 case .keepMediaInfo: return 1 - case .collecting: + case .keepMediaLimitHeader: return 2 - case .peersHeader: + case .keepMediaLimit: return 3 - case let .peer(_, _, peer, _): - return Int32(peer.id.hashValue) + case .keepMediaLimitInfo: + return 4 + case .ccTaskValue: + return 5 + case .ccTaskValueDesc: + return 6 + case .clearAll: + return 7 + case .collecting: + return 8 + case .peersHeader: + return 9 + case let .peer(_, _, peer, _, _): + return peer.id.toInt64() case .section(let sectionId): - return (sectionId + 1) * 1000 - sectionId + return Int64((sectionId + 1) * 1000 - sectionId) } } @@ -61,12 +82,24 @@ private enum StorageUsageEntry: TableItemListNodeEntry { return 0 case .keepMediaInfo: return 1 - case .collecting: + case .keepMediaLimitHeader: return 2 - case .peersHeader: + case .keepMediaLimit: return 3 - case let .peer(_, index, _, _): - return 4 + index + case .keepMediaLimitInfo: + return 4 + case .ccTaskValue: + return 5 + case .ccTaskValueDesc: + return 6 + case .clearAll: + return 7 + case .collecting: + return 8 + case .peersHeader: + return 9 + case let .peer(_, index, _, _, _): + return 10 + index case .section(let sectionId): return (sectionId + 1) * 1000 - sectionId } @@ -74,15 +107,27 @@ private enum StorageUsageEntry: TableItemListNodeEntry { var index:Int32 { switch self { - case .keepMedia(let sectionId, _, _): + case .keepMedia(let sectionId, _, _, _): + return (sectionId * 1000) + stableIndex + case .keepMediaInfo(let sectionId, _, _): + return (sectionId * 1000) + stableIndex + case .keepMediaLimitHeader(let sectionId, _, _): + return (sectionId * 1000) + stableIndex + case .keepMediaLimit(let sectionId, _, _): return (sectionId * 1000) + stableIndex - case .keepMediaInfo(let sectionId, _): + case .keepMediaLimitInfo(let sectionId, _, _): return (sectionId * 1000) + stableIndex - case .collecting(let sectionId, _): + case .clearAll(let sectionId, _, _): return (sectionId * 1000) + stableIndex - case .peersHeader(let sectionId, _): + case .ccTaskValue(let sectionId, _, _): return (sectionId * 1000) + stableIndex - case let .peer(sectionId, _, _, _): + case .ccTaskValueDesc(let sectionId, _, _): + return (sectionId * 1000) + stableIndex + case .collecting(let sectionId, _, _): + return (sectionId * 1000) + stableIndex + case .peersHeader(let sectionId, _, _): + return (sectionId * 1000) + stableIndex + case let .peer(sectionId, _, _, _, _): return (sectionId * 1000) + stableIndex case .section(let sectionId): return (sectionId + 1) * 1000 - sectionId @@ -91,26 +136,62 @@ private enum StorageUsageEntry: TableItemListNodeEntry { static func ==(lhs: StorageUsageEntry, rhs: StorageUsageEntry) -> Bool { switch lhs { - case let .keepMedia(sectionId, text, value): - if case .keepMedia(sectionId, text, value) = rhs { + case let .keepMedia(sectionId, text, value, viewType): + if case .keepMedia(sectionId, text, value, viewType) = rhs { + return true + } else { + return false + } + case let .keepMediaInfo(sectionId, text, viewType): + if case .keepMediaInfo(sectionId, text, viewType) = rhs { + return true + } else { + return false + } + case let .keepMediaLimitHeader(sectionId, value, viewType): + if case .keepMediaLimitHeader(sectionId, value, viewType) = rhs { + return true + } else { + return false + } + case let .keepMediaLimit(sectionId, value, viewType): + if case .keepMediaLimit(sectionId, value, viewType) = rhs { + return true + } else { + return false + } + case let .keepMediaLimitInfo(sectionId, value, viewType): + if case .keepMediaLimitInfo(sectionId, value, viewType) = rhs { + return true + } else { + return false + } + case let .ccTaskValue(sectionId, task, viewType): + if case .ccTaskValue(sectionId, task, viewType) = rhs { + return true + } else { + return false + } + case let .ccTaskValueDesc(sectionId, value, viewType): + if case .ccTaskValueDesc(sectionId, value, viewType) = rhs { return true } else { return false } - case let .keepMediaInfo(sectionId, text): - if case .keepMediaInfo(sectionId, text) = rhs { + case let .clearAll(sectionId, enabled, viewType): + if case .clearAll(sectionId, enabled, viewType) = rhs { return true } else { return false } - case let .collecting(sectionId, text): - if case .collecting(sectionId, text) = rhs { + case let .collecting(sectionId, text, viewType): + if case .collecting(sectionId, text, viewType) = rhs { return true } else { return false } - case let .peersHeader(sectionId, text): - if case .peersHeader(sectionId, text) = rhs { + case let .peersHeader(sectionId, text, viewType): + if case .peersHeader(sectionId, text, viewType) = rhs { return true } else { return false @@ -121,8 +202,8 @@ private enum StorageUsageEntry: TableItemListNodeEntry { } else { return false } - case let .peer(lhsSectionId, lhsIndex, lhsPeer, lhsValue): - if case let .peer(rhsSectionId, rhsIndex, rhsPeer, rhsValue) = rhs { + case let .peer(lhsSectionId, lhsIndex, lhsPeer, lhsValue, lhsViewType): + if case let .peer(rhsSectionId, rhsIndex, rhsPeer, rhsValue, rhsViewType) = rhs { if lhsIndex != rhsIndex { return false } @@ -132,6 +213,9 @@ private enum StorageUsageEntry: TableItemListNodeEntry { if !arePeersEqual(lhsPeer, rhsPeer) { return false } + if lhsViewType != rhsViewType { + return false + } if lhsValue != rhsValue { return false } @@ -149,42 +233,58 @@ private enum StorageUsageEntry: TableItemListNodeEntry { func item(_ arguments: StorageUsageControllerArguments, initialSize: NSSize) -> TableRowItem { switch self { - case let .keepMedia(_, text, value): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: text, type: .context(stateback: { - return value - }), action: { + case let .keepMedia(_, text, value, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: text, type: .context(value), viewType: viewType, action: { arguments.updateKeepMedia() }) - - case let .keepMediaInfo(_, text): - return GeneralTextRowItem(initialSize, stableId: stableId, text: text) - case let .collecting(_, text): - return GeneralTextRowItem(initialSize, stableId: stableId, text: text) - case let .peersHeader(_, text): - return GeneralTextRowItem(initialSize, stableId: stableId, text: text) - case let .peer(_, _, peer, value): - return ShortPeerRowItem(initialSize, peer: peer, account: arguments.account, stableId: stableId, enabled: true, height: 40, photoSize: NSMakeSize(30, 30), drawCustomSeparator: true, drawLastSeparator: true, inset: NSEdgeInsets(left: 30, right: 30), generalType: .context(stateback: { () -> String in - return value - }), action: { + case let .keepMediaInfo(_, text, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, viewType: viewType) + case let .keepMediaLimitHeader(_, text, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, viewType: viewType) + case let .keepMediaLimit(_, value, viewType): + let values = [5, 16, 32, Int32.max] + var value = value + if !values.contains(value) { + value = Int32.max + } + return SelectSizeRowItem(initialSize, stableId: stableId, current: value, sizes: values, hasMarkers: false, titles: ["5GB", "16GB", "32GB", L10n.storageUsageLimitNoLimit], viewType: viewType, selectAction: { selected in + arguments.updateMediaLimit(values[selected]) + }) + case let .keepMediaLimitInfo(_, text, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, viewType: viewType) + case let .collecting(_, text, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, alignment: .center, additionLoading: true, viewType: viewType) + case let .ccTaskValue(_, task, viewType): + return StorageUsageCleanProgressRowItem(initialSize, stableId: stableId, task: task, viewType: viewType) + case let .ccTaskValueDesc(_, text, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, viewType: viewType) + case let .clearAll(_, enabled, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: tr(L10n.storageClearAll), type: .next, viewType: viewType, action: { + arguments.clearAll() + }, enabled: enabled) + case let .peersHeader(_, text, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, viewType: viewType) + case let .peer(_, _, peer, value, viewType): + return ShortPeerRowItem(initialSize, peer: peer, account: arguments.context.account, stableId: stableId, height: 44, photoSize: NSMakeSize(30, 30), isLookSavedMessage: true, inset: NSEdgeInsets(left: 30, right: 30), generalType: .context(value), viewType: viewType, action: { arguments.openPeerMedia(peer.id) }) case .section: - return GeneralRowItem(initialSize, height: 20, stableId: stableId) + return GeneralRowItem(initialSize, height: 30, stableId: stableId, viewType: .separator) } } } private func stringForKeepMediaTimeout(_ timeout: Int32) -> String { if timeout <= 7 * 24 * 60 * 60 { - return tr(.timerWeeksCountable(1)) + return tr(L10n.timerWeeksCountable(1)) } else if timeout <= 1 * 31 * 24 * 60 * 60 { - return tr(.timerMonthsCountable(1)) + return tr(L10n.timerMonthsCountable(1)) } else { - return tr(.timerForever) + return tr(L10n.timerForever) } } -private func storageUsageControllerEntries(cacheSettings: CacheStorageSettings, cacheStats: CacheUsageStatsResult?) -> [StorageUsageEntry] { +private func storageUsageControllerEntries(cacheSettings: CacheStorageSettings, cacheStats: CacheUsageStatsResult?, ccTask: CCTaskData?) -> [StorageUsageEntry] { var entries: [StorageUsageEntry] = [] var sectionId:Int32 = 1 @@ -192,47 +292,81 @@ private func storageUsageControllerEntries(cacheSettings: CacheStorageSettings, entries.append(.section(sectionId)) sectionId += 1 - entries.append(.keepMedia(sectionId, tr(.storageUsageKeepMedia), stringForKeepMediaTimeout(cacheSettings.defaultCacheStorageTimeout))) - entries.append(.keepMediaInfo(sectionId, tr(.storageUsageKeepMediaDescription))) + entries.append(.keepMedia(sectionId, L10n.storageUsageKeepMedia, stringForKeepMediaTimeout(cacheSettings.defaultCacheStorageTimeout), .singleItem)) + + entries.append(.keepMediaInfo(sectionId, L10n.storageUsageKeepMediaDescription1, .textBottomItem)) + + entries.append(.section(sectionId)) + sectionId += 1 + - var addedHeader = false + // + entries.append(.keepMediaLimitHeader(sectionId, L10n.storageUsageLimitHeader, .textTopItem)) + entries.append(.keepMediaLimit(sectionId, cacheSettings.defaultCacheStorageLimitGigabytes, .singleItem)) + entries.append(.keepMediaLimitInfo(sectionId, L10n.storageUsageLimitDesc, .textBottomItem)) + entries.append(.section(sectionId)) sectionId += 1 - var exists:[PeerId:PeerId] = [:] - if let cacheStats = cacheStats, case let .result(stats) = cacheStats { - var statsByPeerId: [(PeerId, Int64)] = [] - for (peerId, categories) in stats.media { - if exists[peerId] == nil { - var combinedSize: Int64 = 0 - for (_, media) in categories { - for (_, size) in media { - combinedSize += size + + if let ccTask = ccTask { + entries.append(.ccTaskValue(sectionId, ccTask, .singleItem)) + entries.append(.ccTaskValueDesc(sectionId, L10n.storageUsageCleaningProcess, .textBottomItem)) + } else { + + var exists:[PeerId:PeerId] = [:] + if let cacheStats = cacheStats, case let .result(stats) = cacheStats { + + entries.append(.clearAll(sectionId, !stats.peers.isEmpty, .singleItem)) + + entries.append(.section(sectionId)) + sectionId += 1 + + var statsByPeerId: [(PeerId, Int64)] = [] + for (peerId, categories) in stats.media { + if exists[peerId] == nil { + var combinedSize: Int64 = 0 + for (_, media) in categories { + for (_, size) in media { + combinedSize += size + } } + statsByPeerId.append((peerId, combinedSize)) + exists[peerId] = peerId } - statsByPeerId.append((peerId, combinedSize)) - exists[peerId] = peerId + } + var index: Int32 = 0 - } - var index: Int32 = 0 - for (peerId, size) in statsByPeerId.sorted(by: { $0.1 > $1.1 }) { - if size >= 32 * 1024 { - if let peer = stats.peers[peerId], !peer.isSecretChat { - if !addedHeader { - addedHeader = true - entries.append(.peersHeader(sectionId, tr(.storageUsageChatsHeader))) - } - entries.append(.peer(sectionId, index, peer, dataSizeString(Int(size)))) - index += 1 - } + let filtered = statsByPeerId.sorted(by: { $0.1 > $1.1 }).filter { peerId, size -> Bool in + return size >= 32 * 1024 && stats.peers[peerId] != nil && !stats.peers[peerId]!.isSecretChat + } + + if !filtered.isEmpty { + entries.append(.peersHeader(sectionId, L10n.storageUsageChatsHeader, .textTopItem)) } + + for (i, value) in filtered.enumerated() { + let peer = stats.peers[value.0]! + entries.append(.peer(sectionId, index, peer, dataSizeString(Int(value.1), formatting: DataSizeStringFormatting.current), bestGeneralViewType(filtered, for: i))) + index += 1 + } + } else { + + entries.append(.clearAll(sectionId, true, .singleItem)) + + entries.append(.section(sectionId)) + sectionId += 1 + + entries.append(.collecting(sectionId, L10n.storageUsageCalculating, .singleItem)) } - } else { - entries.append(.collecting(sectionId, tr(.storageUsageCalculating))) } + + entries.append(.section(sectionId)) + sectionId += 1 + return entries } @@ -253,49 +387,45 @@ class StorageUsageController: TableViewController { readyOnce() - let account = self.account + let context = self.context let initialSize = self.atomicSize let cacheSettingsPromise = Promise() - cacheSettingsPromise.set(account.postbox.preferencesView(keys: [PreferencesKeys.cacheStorageSettings]) + cacheSettingsPromise.set(context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.cacheStorageSettings]) |> map { view -> CacheStorageSettings in - let cacheSettings: CacheStorageSettings - if let value = view.values[PreferencesKeys.cacheStorageSettings] as? CacheStorageSettings { - cacheSettings = value - } else { - cacheSettings = CacheStorageSettings.defaultSettings - } - - return cacheSettings + return view.entries[SharedDataKeys.cacheStorageSettings] as? CacheStorageSettings ?? CacheStorageSettings.defaultSettings }) - let statsPromise = Promise() - statsPromise.set(.single(nil) |> then(collectCacheUsageStats(account: account) |> map { Optional($0) })) + statsPromise.set(.single(nil) |> then(context.engine.resources.collectCacheUsageStats(additionalCachePaths: [], logFilesPath: ApiEnvironment.containerURL!.appendingPathComponent("logs").path) |> map { Optional($0) })) let actionDisposables = DisposableSet() let clearDisposable = MetaDisposable() actionDisposables.add(clearDisposable) - let arguments = StorageUsageControllerArguments(account: account, updateKeepMedia: { [weak self] in + let arguments = StorageUsageControllerArguments(context: context, updateKeepMedia: { [weak self] in if let strongSelf = self { let timeoutAction: (Int32) -> Void = { timeout in - let _ = updateCacheStorageSettingsInteractively(postbox: account.postbox, { current in + let _ = updateCacheStorageSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in return current.withUpdatedDefaultCacheStorageTimeout(timeout) }).start() } - if let item = strongSelf.genericView.item(stableId: StorageUsageEntry.keepMedia(0, "", "").stableId), let view = (strongSelf.genericView.viewNecessary(at: item.index) as? GeneralInteractedRowView)?.textView { + if let item = strongSelf.genericView.item(stableId: StorageUsageEntry.keepMedia(0, "", "", .singleItem).stableId), let view = (strongSelf.genericView.viewNecessary(at: item.index) as? GeneralInteractedRowView)?.textView { - showPopover(for: view, with: SPopoverViewController(items: [SPopoverItem(tr(.timerWeeksCountable(1)), { + showPopover(for: view, with: SPopoverViewController(items: [SPopoverItem(tr(L10n.timerWeeksCountable(1)), { timeoutAction(7 * 24 * 60 * 60) - }), SPopoverItem(tr(.timerMonthsCountable(1)), { + }), SPopoverItem(tr(L10n.timerMonthsCountable(1)), { timeoutAction(1 * 31 * 24 * 60 * 60) - }), SPopoverItem(tr(.timerForever), { + }), SPopoverItem(tr(L10n.timerForever), { timeoutAction(Int32.max) })]), edge: .minX, inset: NSMakePoint(0,-30)) } } + }, updateMediaLimit: { limit in + let _ = updateCacheStorageSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in + return current.withUpdatedDefaultCacheStorageLimitGigabytes(limit) + }).start() }, openPeerMedia: { peerId in let _ = (statsPromise.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak statsPromise] result in if let result = result, case let .result(stats) = result { @@ -329,26 +459,30 @@ class StorageUsageController: TableViewController { } } } + statsPromise.set(.single(.result(CacheUsageStats(media: media, mediaResourceIds: stats.mediaResourceIds, peers: stats.peers, otherSize: stats.otherSize, otherPaths: stats.otherPaths, cacheSize: stats.cacheSize, tempPaths: stats.tempPaths, tempSize: stats.tempSize, immutableSize: stats.immutableSize)))) - statsPromise.set(.single(.result(CacheUsageStats(media: media, mediaResourceIds: stats.mediaResourceIds, peers: stats.peers)))) - - clearDisposable.set(clearCachedMediaResources(account: account, mediaResourceIds: clearResourceIds).start()) + clearDisposable.set(context.engine.resources.clearCachedMediaResources(mediaResourceIds: clearResourceIds).start()) } }), for: mainWindow) } } }) + }, clearAll: { + confirm(for: context.window, information: L10n.storageClearAllConfirmDescription, okTitle: L10n.storageClearAll, successHandler: { _ in + context.cacheCleaner.run() + statsPromise.set(.single(CacheUsageStatsResult.result(.init(media: [:], mediaResourceIds: [:], peers: [:], otherSize: 0, otherPaths: [], cacheSize: 0, tempPaths: [], tempSize: 0, immutableSize: 0)))) + }) }) let previous:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) - self.genericView.merge(with: combineLatest(cacheSettingsPromise.get() |> deliverOnPrepareQueue, statsPromise.get() |> deliverOnPrepareQueue, appearanceSignal |> deliverOnPrepareQueue) + self.genericView.merge(with: combineLatest(queue: prepareQueue, cacheSettingsPromise.get(), statsPromise.get(), context.cacheCleaner.task, appearanceSignal) - |> map { cacheSettings, cacheStats, appearance -> TableUpdateTransition in + |> map { cacheSettings, cacheStats, ccTask, appearance -> TableUpdateTransition in - let entries = storageUsageControllerEntries(cacheSettings: cacheSettings, cacheStats: cacheStats).map {AppearanceWrapperEntry(entry: $0, appearance: appearance)} + let entries = storageUsageControllerEntries(cacheSettings: cacheSettings, cacheStats: cacheStats, ccTask: ccTask).map {AppearanceWrapperEntry(entry: $0, appearance: appearance)} return prepareTransition(left: previous.swap(entries), right: entries, initialSize: initialSize.modify({$0}), arguments: arguments) } |> afterDisposed { diff --git a/Telegram-Mac/StoredMessageFromSearchPeer.swift b/Telegram-Mac/StoredMessageFromSearchPeer.swift new file mode 100644 index 0000000000..a1378eb5c0 --- /dev/null +++ b/Telegram-Mac/StoredMessageFromSearchPeer.swift @@ -0,0 +1,62 @@ +// +// StoredMessageFromSearchPeer.swift +// Telegram +// +// Created by Mikhail Filimonov on 19/01/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Foundation +import Postbox +import TelegramCore + +import SwiftSignalKit + + +func storedMessageFromSearchPeer(account: Account, peer: Peer) -> Signal { + return account.postbox.transaction { transaction -> PeerId in + if transaction.getPeer(peer.id) == nil { + updatePeers(transaction: transaction, peers: [peer], update: { previousPeer, updatedPeer in + return updatedPeer + }) + } + if let group = transaction.getPeer(peer.id) as? TelegramGroup, let migrationReference = group.migrationReference { + return migrationReference.peerId + } + return peer.id + } +} + +func storedMessageFromSearch(account: Account, message: Message) -> Signal { + return account.postbox.transaction { transaction -> Void in + if transaction.getMessage(message.id) == nil { + for (_, peer) in message.peers { + if transaction.getPeer(peer.id) == nil { + updatePeers(transaction: transaction, peers: [peer], update: { previousPeer, updatedPeer in + return updatedPeer + }) + } + } + + let storeMessage = StoreMessage(id: .Id(message.id), globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, threadId: nil, timestamp: message.timestamp, flags: StoreMessageFlags(message.flags), tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: message.forwardInfo.flatMap(StoreMessageForwardInfo.init), authorId: message.author?.id, text: message.text, attributes: message.attributes, media: message.media) + + let _ = transaction.addMessages([storeMessage], location: .Random) + } + } +} + +func storeMessageFromSearch(transaction: Transaction, message: Message) { + if transaction.getMessage(message.id) == nil { + for (_, peer) in message.peers { + if transaction.getPeer(peer.id) == nil { + updatePeers(transaction: transaction, peers: [peer], update: { previousPeer, updatedPeer in + return updatedPeer + }) + } + } + + let storeMessage = StoreMessage(id: .Id(message.id), globallyUniqueId: message.globallyUniqueId, groupingKey: message.groupingKey, threadId: message.threadId, timestamp: message.timestamp, flags: StoreMessageFlags(message.flags), tags: message.tags, globalTags: message.globalTags, localTags: message.localTags, forwardInfo: message.forwardInfo.flatMap(StoreMessageForwardInfo.init), authorId: message.author?.id, text: message.text, attributes: message.attributes, media: message.media) + + let _ = transaction.addMessages([storeMessage], location: .Random) + } +} diff --git a/Telegram-Mac/String.swift b/Telegram-Mac/String.swift new file mode 100644 index 0000000000..1957101553 --- /dev/null +++ b/Telegram-Mac/String.swift @@ -0,0 +1,69 @@ +// +// String.swift +// CurrencyText +// +// Created by Felipe Lefèvre Marino on 4/3/18. +// Copyright © 2018 Felipe Lefèvre Marino. All rights reserved. +// + +import Foundation + +public protocol CurrencyString { + var representsZero: Bool { get } + var hasNumbers: Bool { get } + var lastNumberOffsetFromEnd: Int? { get } + func numeralFormat() -> String + mutating func updateDecimalSeparator(decimalDigits: Int) +} + +//Currency String Extension +extension String: CurrencyString { + + // MARK: Properties + + /// Informs with the string represents the value of zero + public var representsZero: Bool { + return numeralFormat().replacingOccurrences(of: "0", with: "").count == 0 + } + + /// Returns if the string does have any character that represents numbers + public var hasNumbers: Bool { + return numeralFormat().count > 0 + } + + /// The offset from end index to the index _right after_ the last number in the String. + /// e.g. For the String "123some", the last number position is 4, because from the _end index_ to the index of _3_ + /// there is an offset of 4, "e, m, o and s". + public var lastNumberOffsetFromEnd: Int? { + guard let indexOfLastNumber = lastIndex(where: { $0.isNumber }) else { return nil } + let indexAfterLastNumber = index(after: indexOfLastNumber) + return distance(from: indexAfterLastNumber, to: endIndex) + } + + // MARK: Functions + + /// Updates a currency string decimal separator position based on + /// the amount of decimal digits desired + /// + /// - Parameter decimalDigits: The amount of decimal digits of the currency formatted string + public mutating func updateDecimalSeparator(decimalDigits: Int) { + guard decimalDigits != 0 && count >= decimalDigits else { return } + let decimalsRange = index(endIndex, offsetBy: -decimalDigits).. String { + return replacingOccurrences(of:"[^0-9]", with: "", options: .regularExpression) + } +} + +// MARK: - Static constants + +extension String { + public static let negativeSymbol = "-" +} diff --git a/Telegram-Mac/StringFormat.swift b/Telegram-Mac/StringFormat.swift new file mode 100644 index 0000000000..9d92b02743 --- /dev/null +++ b/Telegram-Mac/StringFormat.swift @@ -0,0 +1,52 @@ +import Foundation + +// Incuding at least one Objective-C class in a swift file ensures that it doesn't get stripped by the linker +private final class LinkHelperClass: NSObject { +} + +public func dataSizeString(_ size: Int, forceDecimal: Bool = false, formatting: DataSizeStringFormatting) -> String { + return dataSizeString(Int64(size), forceDecimal: forceDecimal, formatting: formatting) +} + +public struct DataSizeStringFormatting { + let decimalSeparator: String + let byte: (String) -> (String, [(Int, NSRange)]) + let kilobyte: (String) -> (String, [(Int, NSRange)]) + let megabyte: (String) -> (String, [(Int, NSRange)]) + let gigabyte: (String) -> (String, [(Int, NSRange)]) + + public init(decimalSeparator: String, byte: @escaping (String) -> (String, [(Int, NSRange)]), kilobyte: @escaping (String) -> (String, [(Int, NSRange)]), megabyte: @escaping (String) -> (String, [(Int, NSRange)]), gigabyte: @escaping (String) -> (String, [(Int, NSRange)])) { + self.decimalSeparator = decimalSeparator + self.byte = byte + self.kilobyte = kilobyte + self.megabyte = megabyte + self.gigabyte = gigabyte + } +} + +public func dataSizeString(_ size: Int64, forceDecimal: Bool = false, formatting: DataSizeStringFormatting) -> String { + if size >= 1024 * 1024 * 1024 { + let remainder = Int64((Double(size % (1024 * 1024 * 1024)) / (1024 * 1024 * 102.4)).rounded(.down)) + if remainder != 0 || forceDecimal { + return formatting.gigabyte("\(size / (1024 * 1024 * 1024))\(formatting.decimalSeparator)\(remainder)").0 + } else { + return formatting.gigabyte("\(size / (1024 * 1024 * 1024))").0 + } + } else if size >= 1024 * 1024 { + let remainder = Int64((Double(size % (1024 * 1024)) / (1024.0 * 102.4)).rounded(.down)) + if remainder != 0 || forceDecimal { + return formatting.megabyte( "\(size / (1024 * 1024))\(formatting.decimalSeparator)\(remainder)").0 + } else { + return formatting.megabyte("\(size / (1024 * 1024))").0 + } + } else if size >= 1024 { + let remainder = (size % (1024)) / (102) + if remainder != 0 || forceDecimal { + return formatting.kilobyte("\(size / 1024)\(formatting.decimalSeparator)\(remainder)").0 + } else { + return formatting.kilobyte("\(size / 1024)").0 + } + } else { + return formatting.byte("\(size)").0 + } +} diff --git a/Telegram-Mac/StringPluralization.swift b/Telegram-Mac/StringPluralization.swift index 790f3991ba..2ea0520852 100644 --- a/Telegram-Mac/StringPluralization.swift +++ b/Telegram-Mac/StringPluralization.swift @@ -27,6 +27,9 @@ enum PluralizationForm { } func presentationStringsPluralizationForm(_ lc: UInt32, _ value: Int32) -> PluralizationForm { + if value == 0 { + return .zero + } switch numberPluralizationForm(lc, value) { case .zero: return .zero @@ -42,3 +45,5 @@ func presentationStringsPluralizationForm(_ lc: UInt32, _ value: Int32) -> Plura return .other } } + + diff --git a/Telegram-Mac/SuggestionLocalizationViewController.swift b/Telegram-Mac/SuggestionLocalizationViewController.swift index 73aeb249eb..0e4f808ba6 100644 --- a/Telegram-Mac/SuggestionLocalizationViewController.swift +++ b/Telegram-Mac/SuggestionLocalizationViewController.swift @@ -8,9 +8,10 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit private class SuggestionControllerView : View { let textView:TextView = TextView() @@ -24,10 +25,12 @@ private class SuggestionControllerView : View { addSubview(tableView) addSubview(suggestTextView) + textView.backgroundColor = theme.colors.background + suggestTextView.backgroundColor = theme.colors.background tableView.setFrameSize(NSMakeSize(frameRect.width, frameRect.height - 50)) separatorView.setFrameSize(frameRect.width, .borderSize) - separatorView.backgroundColor = .border + separatorView.backgroundColor = theme.colors.border layout() } @@ -58,11 +61,11 @@ private class SuggestionControllerView : View { } class SuggestionLocalizationViewController: ModalViewController { - private let account:Account + private let context:AccountContext private let suggestionInfo:SuggestedLocalizationInfo private var languageCode:String = "en" - init(_ account:Account, suggestionInfo: SuggestedLocalizationInfo) { - self.account = account + init(_ context: AccountContext, suggestionInfo: SuggestedLocalizationInfo) { + self.context = context self.suggestionInfo = suggestionInfo super.init(frame: NSMakeRect(0, 0, 280, 198)) bar = .init(height: 0) @@ -73,11 +76,12 @@ class SuggestionLocalizationViewController: ModalViewController { } override var modalInteractions: ModalInteractions? { - return ModalInteractions(acceptTitle: tr(.modalOK), accept: { [weak self] in + return ModalInteractions(acceptTitle: L10n.modalOK, accept: { [weak self] in if let strongSelf = self { strongSelf.close() - _ = markSuggestedLocalizationAsSeenInteractively(postbox: strongSelf.account.postbox, languageCode: strongSelf.suggestionInfo.languageCode).start() - _ = showModalProgress(signal: downoadAndApplyLocalization(postbox: strongSelf.account.postbox, network: strongSelf.account.network, languageCode: strongSelf.languageCode), for: mainWindow).start() + let engine = strongSelf.context.engine.localization + _ = engine.markSuggestedLocalizationAsSeenInteractively(languageCode: strongSelf.suggestionInfo.languageCode).start() + _ = showModalProgress(signal: engine.downloadAndApplyLocalization(accountManager: strongSelf.context.sharedContext.accountManager, languageCode: strongSelf.languageCode), for: strongSelf.context.window).start() } }, drawBorder: true, height: 40) } @@ -129,29 +133,28 @@ class SuggestionLocalizationViewController: ModalViewController { if let info = enInfo { - _ = genericView.tableView.insert(item: LanguageRowItem(initialSize: initialSize, stableId: 0, selected: selected == 0, value: info, action: { [weak self] in + _ = genericView.tableView.insert(item: LanguageRowItem(initialSize: initialSize, stableId: 0, selected: selected == 0, deletable: false, value: info, action: { [weak self] in self?.reloadItems(0, swap) }, reversed: true), at: 0) } if let info = currentInfo { - _ = genericView.tableView.insert(item: LanguageRowItem(initialSize: initialSize, stableId: 1, selected: selected == 1, value: info, action: { [weak self] in + _ = genericView.tableView.insert(item: LanguageRowItem(initialSize: initialSize, stableId: 1, selected: selected == 1, deletable: false, value: info, action: { [weak self] in self?.reloadItems(1, swap) }, reversed: true), at: swap ? 0 : 1) } - - let otherInfo = LocalizationInfo(languageCode: "", title: NativeLocalization("Suggest.Localization.Other"), localizedTitle: suggestionInfo.localizedKey("Suggest.Localization.Other") ) - _ = genericView.tableView.addItem(item: LanguageRowItem(initialSize: initialSize, stableId: 10, selected: false, value: otherInfo, action: { [weak self] in + // public init(languageCode: String, baseLanguageCode: String?, customPluralizationCode: String?, title: String, localizedTitle: String, isOfficial: Bool, totalStringCount: Int32, translatedStringCount: Int32, platformUrl: String) { + let otherInfo = LocalizationInfo(languageCode: "", baseLanguageCode: nil, customPluralizationCode: nil, title: NativeLocalization("Suggest.Localization.Other"), localizedTitle: suggestionInfo.localizedKey("Suggest.Localization.Other"), isOfficial: true, totalStringCount: 0, translatedStringCount: 0, platformUrl: "" ) + + _ = genericView.tableView.addItem(item: LanguageRowItem(initialSize: initialSize, stableId: 10, selected: false, deletable: false, value: otherInfo, action: { [weak self] in if let strongSelf = self { strongSelf.close() - strongSelf.account.context.mainNavigation?.push(LanguageViewController(strongSelf.account)) - _ = markSuggestedLocalizationAsSeenInteractively(postbox: strongSelf.account.postbox, languageCode: strongSelf.suggestionInfo.languageCode).start() + strongSelf.context.sharedContext.bindings.rootNavigation().push(LanguageViewController(strongSelf.context)) + let engine = strongSelf.context.engine.localization + _ = engine.markSuggestedLocalizationAsSeenInteractively(languageCode: strongSelf.suggestionInfo.languageCode).start() } }, reversed: true)) - -// _ = genericView.tableView.addItem(item: GeneralInteractedRowItem(initialSize, name: suggestionInfo.localizedKey("Suggest.Localization.Other"), type: .next, action: { [weak self] in -// -// }, drawCustomSeparator: false, inset: NSEdgeInsets(left: 25, right: 25))) + } } diff --git a/Telegram-Mac/SyncCoreExtension.swift b/Telegram-Mac/SyncCoreExtension.swift new file mode 100644 index 0000000000..189527968a --- /dev/null +++ b/Telegram-Mac/SyncCoreExtension.swift @@ -0,0 +1,31 @@ +// +// SyncCoreExtension.swift +// Telegram +// +// Created by Mikhail Filimonov on 01.11.2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +extension PixelDimensions { + var size: CGSize { + return CGSize(width: CGFloat(self.width), height: CGFloat(self.height)) + } + init(_ size: CGSize) { + self.init(width: Int32(abs(size.width)), height: Int32(abs(size.height))) + } + init(_ width: Int32, _ height: Int32) { + self.init(width: width, height: height) + } +} +extension CGSize { + var pixel: PixelDimensions { + return PixelDimensions(self) + } +} + +enum AppLogEvents : String { + case imageEditor = "image_editor_used" +} diff --git a/Telegram-Mac/System.swift b/Telegram-Mac/System.swift index 30f3d333cc..3ee86c3e14 100644 --- a/Telegram-Mac/System.swift +++ b/Telegram-Mac/System.swift @@ -7,10 +7,13 @@ // import Cocoa -import SwiftSignalKitMac -import TelegramCoreMac +import SwiftSignalKit +import TelegramCore + import TGUIKit -import PostboxMac +import Postbox +import CoreMediaIO + private let _dQueue = Queue.init(name: "chatListQueue") private let _sQueue = Queue.init(name: "ChatQueue") @@ -36,10 +39,25 @@ var mainWindow:Window { fatalError("window not found") } +var systemAppearance: NSAppearance { + if #available(OSX 10.14, *) { + return NSApp.effectiveAppearance + } else { + return NSAppearance.current + } +} + public func deliverOnPrepareQueue(_ signal: Signal) -> Signal { return signal |> deliverOn(prepareQueue) } +public func deliverOnMessagesViewQueue(_ signal: Signal) -> Signal { + return signal |> deliverOn(messagesViewQueue) +} + +public func deliverOnResourceQueue(_ signal: Signal) -> Signal { + return signal |> deliverOn(resourcesQueue) +} func proccessEntriesWithoutReverse(_ left:[R]?,right:[R],_ convertEntry:@escaping (R) -> T) -> ([Int],[(Int,T)],[(Int,T)]) where R:Comparable, R:Identifiable { @@ -102,32 +120,28 @@ func link(path:String?, ext:String) -> String? { if let path = path, path.nsstring.pathExtension.length == 0 && FileManager.default.fileExists(atPath: path) { let path = path.nsstring.appendingPathExtension(ext)! if !FileManager.default.fileExists(atPath: path) { - do { - try FileManager.default.removeItem(atPath: path) - } - catch { - } - do { - try FileManager.default.createSymbolicLink(atPath: path, withDestinationPath: realPath!) - } - catch { - } + try? FileManager.default.removeItem(atPath: path) + try? FileManager.default.createSymbolicLink(atPath: path, withDestinationPath: realPath!) } realPath = path } return realPath } -func delay(_ delay:Double, closure:@escaping ()->()) { - let when = DispatchTime.now() + delay - DispatchQueue.main.asyncAfter(deadline: when, execute: closure) -} - -func fileSize(_ path:String) -> Int32? { +func fs(_ path:String) -> Int32? { - if let attrs = try? FileManager.default.attributesOfItem(atPath: path) as NSDictionary { + if var attrs = try? FileManager.default.attributesOfItem(atPath: path) as NSDictionary { + + if attrs["NSFileType"] as? String == "NSFileTypeSymbolicLink" { + if let path = try? FileManager.default.destinationOfSymbolicLink(atPath: path) { + attrs = (try? FileManager.default.attributesOfItem(atPath: path) as NSDictionary) ?? attrs + } + } + + let size = attrs.fileSize() + if size > UInt64(INT32_MAX) { return INT32_MAX } @@ -138,3 +152,37 @@ func fileSize(_ path:String) -> Int32? { + +func DALDevices() -> [AVCaptureDevice] { + let video = AVCaptureDevice.devices(for: .video) + let muxed:[AVCaptureDevice] = AVCaptureDevice.devices(for: .muxed) //[]// + // && $0.hasMediaType(.video) + + + return (video + muxed).filter { $0.isConnected && !$0.isSuspended } +} + +func shouldBeMirrored(_ device: AVCaptureDevice) -> Bool { + + if !device.hasMediaType(.video) { + return false + } + + var latency_pa = CMIOObjectPropertyAddress( + mSelector: CMIOObjectPropertySelector(kCMIODevicePropertyLatency), + mScope: CMIOObjectPropertyScope(kCMIOObjectPropertyScopeWildcard), + mElement: CMIOObjectPropertyElement(kCMIOObjectPropertyElementWildcard) + ) + var dataSize = UInt32(0) + + let id = device.value(forKey: "_connectionID") as? CMIOObjectID + + if let id = id { + if CMIOObjectGetPropertyDataSize(id, &latency_pa, 0, nil, &dataSize) == OSStatus(kCMIOHardwareNoError) { + return false + } else { + return true + } + } + return true +} diff --git a/Telegram-Mac/TGCallConnectionDescription.h b/Telegram-Mac/TGCallConnectionDescription.h deleted file mode 100644 index eff41367ce..0000000000 --- a/Telegram-Mac/TGCallConnectionDescription.h +++ /dev/null @@ -1,34 +0,0 @@ -// -// TGCallConnectionDescription.h -// Telegram -// -// Created by keepcoder on 03/05/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - -#import - - -@interface TGCallConnectionDescription : NSObject - - @property (nonatomic, readonly) int64_t identifier; - @property (nonatomic, strong, readonly) NSString *ipv4; - @property (nonatomic, strong, readonly) NSString *ipv6; - @property (nonatomic, readonly) int32_t port; - @property (nonatomic, strong, readonly) NSData *peerTag; - -- (instancetype)initWithIdentifier:(int64_t)identifier ipv4:(NSString *)ipv4 ipv6:(NSString *)ipv6 port:(int32_t)port peerTag:(NSData *)peerTag; - - @end - - -@interface TGCallConnection : NSObject - - @property (nonatomic, strong, readonly) NSData *key; - @property (nonatomic, strong, readonly) NSData *keyHash; - @property (nonatomic, strong, readonly) TGCallConnectionDescription *defaultConnection; - @property (nonatomic, strong, readonly) NSArray *alternativeConnections; - -- (instancetype)initWithKey:(NSData *)key keyHash:(NSData *)keyHash defaultConnection:(TGCallConnectionDescription *)defaultConnection alternativeConnections:(NSArray *)alternativeConnections; - -@end diff --git a/Telegram-Mac/TGCallConnectionDescription.m b/Telegram-Mac/TGCallConnectionDescription.m deleted file mode 100644 index 08555a2224..0000000000 --- a/Telegram-Mac/TGCallConnectionDescription.m +++ /dev/null @@ -1,41 +0,0 @@ -// -// TGCallConnectionDescription.m -// Telegram -// -// Created by keepcoder on 03/05/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - -#import "TGCallConnectionDescription.h" - -@implementation TGCallConnectionDescription - -- (instancetype)initWithIdentifier:(int64_t)identifier ipv4:(NSString *)ipv4 ipv6:(NSString *)ipv6 port:(int32_t)port peerTag:(NSData *)peerTag { - self = [super init]; - if (self != nil) { - _identifier = identifier; - _ipv4 = ipv4; - _ipv6 = ipv6; - _port = port; - _peerTag = peerTag; - } - return self; -} - - @end - - -@implementation TGCallConnection - -- (instancetype)initWithKey:(NSData *)key keyHash:(NSData *)keyHash defaultConnection:(TGCallConnectionDescription *)defaultConnection alternativeConnections:(NSArray *)alternativeConnections { - self = [super init]; - if (self != nil) { - _key = key; - _keyHash = keyHash; - _defaultConnection = defaultConnection; - _alternativeConnections = alternativeConnections; - } - return self; -} - -@end diff --git a/Telegram-Mac/TGCallUtils.h b/Telegram-Mac/TGCallUtils.h index 9f5aacb045..1c6e26eea1 100644 --- a/Telegram-Mac/TGCallUtils.h +++ b/Telegram-Mac/TGCallUtils.h @@ -1,10 +1,10 @@ #import - -void TGCallAesIgeEncryptInplace(uint8_t *inBytes, uint8_t *outBytes, size_t length, uint8_t *key, uint8_t *iv); -void TGCallAesIgeDecryptInplace(uint8_t *inBytes, uint8_t *outBytes, size_t length, uint8_t *key, uint8_t *iv); - -void TGCallSha1(uint8_t *msg, size_t length, uint8_t *output); -void TGCallSha256(uint8_t *msg, size_t length, uint8_t *output); - -void TGCallRandomBytes(uint8_t *buffer, size_t length); -void TGCallAesCtrEncrypt(uint8_t *inOut, size_t length, uint8_t *key, uint8_t *iv, uint8_t *ecount, uint32_t *num); +// +//void TGCallAesIgeEncryptInplace(uint8_t *inBytes, uint8_t *outBytes, size_t length, uint8_t *key, uint8_t *iv); +//void TGCallAesIgeDecryptInplace(uint8_t *inBytes, uint8_t *outBytes, size_t length, uint8_t *key, uint8_t *iv); +// +//void TGCallSha1(uint8_t *msg, size_t length, uint8_t *output); +//void TGCallSha256(uint8_t *msg, size_t length, uint8_t *output); +// +//void TGCallRandomBytes(uint8_t *buffer, size_t length); +//void TGCallAesCtrEncrypt(uint8_t *inOut, size_t length, uint8_t *key, uint8_t *iv, uint8_t *ecount, uint32_t *num); diff --git a/Telegram-Mac/TGCallUtils.mm b/Telegram-Mac/TGCallUtils.mm index d541767c57..1921477d60 100644 --- a/Telegram-Mac/TGCallUtils.mm +++ b/Telegram-Mac/TGCallUtils.mm @@ -3,342 +3,342 @@ #import #import #import - - -# define AES_MAXNR 14 -# define AES_BLOCK_SIZE 16 - -#define N_WORDS (AES_BLOCK_SIZE / sizeof(unsigned long)) -typedef struct { - unsigned long data[N_WORDS]; -} aes_block_t; - -/* XXX: probably some better way to do this */ -#if defined(__i386__) || defined(__x86_64__) -# define UNALIGNED_MEMOPS_ARE_FAST 1 -#else -# define UNALIGNED_MEMOPS_ARE_FAST 0 -#endif - -#if UNALIGNED_MEMOPS_ARE_FAST -# define load_block(d, s) (d) = *(const aes_block_t *)(s) -# define store_block(d, s) *(aes_block_t *)(d) = (s) -#else -# define load_block(d, s) memcpy((d).data, (s), AES_BLOCK_SIZE) -# define store_block(d, s) memcpy((d), (s).data, AES_BLOCK_SIZE) -#endif - -void TGCallAesIgeEncrypt(uint8_t *inBytes, uint8_t *outBytes, size_t length, uint8_t *key, uint8_t *iv) { - size_t len; - size_t n; - uint8_t const *inB; - uint8_t *outB; - - unsigned char aesIv[AES_BLOCK_SIZE]; - memcpy(aesIv, iv, AES_BLOCK_SIZE); - unsigned char ccIv[AES_BLOCK_SIZE]; - memcpy(ccIv, (void *)((uint8_t *)iv + AES_BLOCK_SIZE), AES_BLOCK_SIZE); - - assert(((size_t)inBytes | (size_t)outBytes | (size_t)aesIv | (size_t)ccIv) % sizeof(long) == - 0); - - void *tmpInBytes = malloc(length); - len = length / AES_BLOCK_SIZE; - inB = (uint8_t *)inBytes; - outB = (uint8_t *)tmpInBytes; - - aes_block_t *inp = (aes_block_t *)inB; - aes_block_t *outp = (aes_block_t *)outB; - for (n = 0; n < N_WORDS; ++n) { - outp->data[n] = inp->data[n]; - } - - --len; - inB += AES_BLOCK_SIZE; - outB += AES_BLOCK_SIZE; - uint8_t const *inBCC = (uint8_t *)inBytes; - - aes_block_t const *iv3p = (aes_block_t *)ccIv; - - if (len > 0) { - while (len) { - aes_block_t *inp = (aes_block_t *)inB; - aes_block_t *outp = (aes_block_t *)outB; - - for (n = 0; n < N_WORDS; ++n) { - outp->data[n] = inp->data[n] ^ iv3p->data[n]; - } - - iv3p = (const aes_block_t *)inBCC; - --len; - inBCC += AES_BLOCK_SIZE; - inB += AES_BLOCK_SIZE; - outB += AES_BLOCK_SIZE; - } - } - - size_t realOutLength = 0; - CCCryptorStatus result = CCCrypt(kCCEncrypt, kCCAlgorithmAES128, 0, key, 32, aesIv, tmpInBytes, length, outBytes, length, &realOutLength); - free(tmpInBytes); - - assert(result == kCCSuccess); - - len = length / AES_BLOCK_SIZE; - - aes_block_t const *ivp = (aes_block_t *)inB; - aes_block_t *iv2p = (aes_block_t *)ccIv; - - inB = (uint8_t *)inBytes; - outB = (uint8_t *)outBytes; - - while (len) { - aes_block_t *inp = (aes_block_t *)inB; - aes_block_t *outp = (aes_block_t *)outB; - - for (n = 0; n < N_WORDS; ++n) { - outp->data[n] ^= iv2p->data[n]; - } - ivp = outp; - iv2p = inp; - --len; - inB += AES_BLOCK_SIZE; - outB += AES_BLOCK_SIZE; - } - - memcpy(iv, ivp->data, AES_BLOCK_SIZE); - memcpy((void *)((uint8_t *)iv + AES_BLOCK_SIZE), iv2p->data, AES_BLOCK_SIZE); -} - -void TGCallAesIgeEncryptInplace(uint8_t *inBytes, uint8_t *outBytes, size_t length, uint8_t *key, uint8_t *iv) -{ - uint8_t *outData = (uint8_t *)malloc(length); - TGCallAesIgeEncrypt(inBytes, outData, length, key, iv); - memcpy(outBytes, outData, length); - free(outData); -} - -void TGCallAesIgeDecrypt(uint8_t *inBytes, uint8_t *outBytes, size_t length, uint8_t *key, uint8_t *iv) { - unsigned char aesIv[AES_BLOCK_SIZE]; - memcpy(aesIv, iv, AES_BLOCK_SIZE); - unsigned char ccIv[AES_BLOCK_SIZE]; - memcpy(ccIv, (void *)((uint8_t *)iv + AES_BLOCK_SIZE), AES_BLOCK_SIZE); - - assert(((size_t)inBytes | (size_t)outBytes | (size_t)aesIv | (size_t)ccIv) % sizeof(long) == - 0); - - CCCryptorRef decryptor = NULL; - CCCryptorCreate(kCCDecrypt, kCCAlgorithmAES128, kCCOptionECBMode, key, 32, nil, &decryptor); - if (decryptor != NULL) { - size_t len; - size_t n; - - len = length / AES_BLOCK_SIZE; - - aes_block_t *ivp = (aes_block_t *)(aesIv); - aes_block_t *iv2p = (aes_block_t *)(ccIv); - - uint8_t *inB = (uint8_t *)inBytes; - uint8_t *outB = (uint8_t *)outBytes; - - while (len) { - aes_block_t tmp; - aes_block_t *inp = (aes_block_t *)inB; - aes_block_t *outp = (aes_block_t *)outB; - - for (n = 0; n < N_WORDS; ++n) - tmp.data[n] = inp->data[n] ^ iv2p->data[n]; - - size_t dataOutMoved = 0; - CCCryptorStatus result = CCCryptorUpdate(decryptor, &tmp, AES_BLOCK_SIZE, outB, AES_BLOCK_SIZE, &dataOutMoved); - assert(result == kCCSuccess); - assert(dataOutMoved == AES_BLOCK_SIZE); - - for (n = 0; n < N_WORDS; ++n) - outp->data[n] ^= ivp->data[n]; - - ivp = inp; - iv2p = outp; - - inB += AES_BLOCK_SIZE; - outB += AES_BLOCK_SIZE; - - --len; - } - - memcpy(iv, ivp->data, AES_BLOCK_SIZE); - memcpy((void *)((uint8_t *)iv + AES_BLOCK_SIZE), iv2p->data, AES_BLOCK_SIZE); - - CCCryptorRelease(decryptor); - } -} - -void TGCallAesIgeDecryptInplace(uint8_t *inBytes, uint8_t *outBytes, size_t length, uint8_t *key, uint8_t *iv) { - uint8_t *outData = (uint8_t *)malloc(length); - TGCallAesIgeDecrypt(inBytes, outData, length, key, iv); - memcpy(outBytes, outData, length); - free(outData); -} - -void TGCallSha1(uint8_t *msg, size_t length, uint8_t *output) -{ - CC_SHA1(msg, (CC_LONG)length, output); -} - -void TGCallSha256(uint8_t *msg, size_t length, uint8_t *output) -{ - CC_SHA256(msg, (CC_LONG)length, output); -} - -void TGCallRandomBytes(uint8_t *buffer, size_t length) -{ - arc4random_buf(buffer, length); -} - - -static void ctr128_inc(unsigned char *counter) -{ - uint32_t n = 16, c = 1; - - do { - --n; - c += counter[n]; - counter[n] = (uint8_t)c; - c >>= 8; - } while (n); -} - -static void ctr128_inc_aligned(unsigned char *counter) -{ - size_t *data, c, d, n; - const union { - long one; - char little; - } is_endian = { - 1 - }; - - if (is_endian.little || ((size_t)counter % sizeof(size_t)) != 0) { - ctr128_inc(counter); - return; - } - - data = (size_t *)counter; - c = 1; - n = 16 / sizeof(size_t); - do { - --n; - d = data[n] += c; - /* did addition carry? */ - c = ((d - c) ^ d) >> (sizeof(size_t) * 8 - 1); - } while (n); -} - -@interface TGCallAesCtr : NSObject { - CCCryptorRef _cryptor; - - unsigned char _ivec[16]; - unsigned int _num; - unsigned char _ecount[16]; -} - -@end - -@implementation TGCallAesCtr - -- (instancetype)initWithKey:(const void *)key keyLength:(int)keyLength iv:(const void *)iv ecount:(void *)ecount num:(uint32_t)num { - self = [super init]; - if (self != nil) { - _num = num; - memcpy(_ecount, ecount, 16); - memcpy(_ivec, iv, 16); - - CCCryptorCreate(kCCEncrypt, kCCAlgorithmAES128, kCCOptionECBMode, key, keyLength, nil, &_cryptor); - } - return self; -} - -- (void)dealloc { - if (_cryptor) { - CCCryptorRelease(_cryptor); - } -} - -- (uint32_t)num { - return _num; -} - -- (void *)ecount { - return _ecount; -} - -- (void)encryptIn:(const unsigned char *)in out:(unsigned char *)out len:(size_t)len { - unsigned int n; - size_t l = 0; - - assert(in && out); - assert(_num < 16); - - n = _num; - - if (16 % sizeof(size_t) == 0) { /* always true actually */ - do { - while (n && len) { - *(out++) = *(in++) ^ _ecount[n]; - --len; - n = (n + 1) % 16; - } - - while (len >= 16) { - size_t dataOutMoved; - CCCryptorUpdate(_cryptor, _ivec, 16, _ecount, 16, &dataOutMoved); - ctr128_inc_aligned(_ivec); - for (n = 0; n < 16; n += sizeof(size_t)) - *(size_t *)(out + n) = - *(size_t *)(in + n) ^ *(size_t *)(_ecount + n); - len -= 16; - out += 16; - in += 16; - n = 0; - } - if (len) { - size_t dataOutMoved; - CCCryptorUpdate(_cryptor, _ivec, 16, _ecount, 16, &dataOutMoved); - ctr128_inc_aligned(_ivec); - while (len--) { - out[n] = in[n] ^ _ecount[n]; - ++n; - } - } - _num = n; - return; - } while (0); - } - /* the rest would be commonly eliminated by x86* compiler */ - - while (l < len) { - if (n == 0) { - size_t dataOutMoved; - CCCryptorUpdate(_cryptor, _ivec, 16, _ecount, 16, &dataOutMoved); - ctr128_inc(_ivec); - } - out[l] = in[l] ^ _ecount[n]; - ++l; - n = (n + 1) % 16; - } - - _num = n; -} - -@end - - -void TGCallAesCtrEncrypt(uint8_t *inOut, size_t length, uint8_t *key, uint8_t *iv, uint8_t *ecount, uint32_t *num) -{ - uint8_t *outData = (uint8_t *)malloc(length); - TGCallAesCtr *aesCtr = [[TGCallAesCtr alloc] initWithKey:key keyLength:32 iv:iv ecount:ecount num:*num]; - [aesCtr encryptIn:inOut out:outData len:length]; - memcpy(inOut, outData, length); - - memcpy(ecount, [aesCtr ecount], 16); - *num = [aesCtr num]; -} +// +// +//# define AES_MAXNR 14 +//# define AES_BLOCK_SIZE 16 +// +//#define N_WORDS (AES_BLOCK_SIZE / sizeof(unsigned long)) +//typedef struct { +// unsigned long data[N_WORDS]; +//} aes_block_t; +// +///* XXX: probably some better way to do this */ +//#if defined(__i386__) || defined(__x86_64__) +//# define UNALIGNED_MEMOPS_ARE_FAST 1 +//#else +//# define UNALIGNED_MEMOPS_ARE_FAST 0 +//#endif +// +//#if UNALIGNED_MEMOPS_ARE_FAST +//# define load_block(d, s) (d) = *(const aes_block_t *)(s) +//# define store_block(d, s) *(aes_block_t *)(d) = (s) +//#else +//# define load_block(d, s) memcpy((d).data, (s), AES_BLOCK_SIZE) +//# define store_block(d, s) memcpy((d), (s).data, AES_BLOCK_SIZE) +//#endif +// +//void TGCallAesIgeEncrypt(uint8_t *inBytes, uint8_t *outBytes, size_t length, uint8_t *key, uint8_t *iv) { +// size_t len; +// size_t n; +// uint8_t const *inB; +// uint8_t *outB; +// +// unsigned char aesIv[AES_BLOCK_SIZE]; +// memcpy(aesIv, iv, AES_BLOCK_SIZE); +// unsigned char ccIv[AES_BLOCK_SIZE]; +// memcpy(ccIv, (void *)((uint8_t *)iv + AES_BLOCK_SIZE), AES_BLOCK_SIZE); +// +// assert(((size_t)inBytes | (size_t)outBytes | (size_t)aesIv | (size_t)ccIv) % sizeof(long) == +// 0); +// +// void *tmpInBytes = malloc(length); +// len = length / AES_BLOCK_SIZE; +// inB = (uint8_t *)inBytes; +// outB = (uint8_t *)tmpInBytes; +// +// aes_block_t *inp = (aes_block_t *)inB; +// aes_block_t *outp = (aes_block_t *)outB; +// for (n = 0; n < N_WORDS; ++n) { +// outp->data[n] = inp->data[n]; +// } +// +// --len; +// inB += AES_BLOCK_SIZE; +// outB += AES_BLOCK_SIZE; +// uint8_t const *inBCC = (uint8_t *)inBytes; +// +// aes_block_t const *iv3p = (aes_block_t *)ccIv; +// +// if (len > 0) { +// while (len) { +// aes_block_t *inp = (aes_block_t *)inB; +// aes_block_t *outp = (aes_block_t *)outB; +// +// for (n = 0; n < N_WORDS; ++n) { +// outp->data[n] = inp->data[n] ^ iv3p->data[n]; +// } +// +// iv3p = (const aes_block_t *)inBCC; +// --len; +// inBCC += AES_BLOCK_SIZE; +// inB += AES_BLOCK_SIZE; +// outB += AES_BLOCK_SIZE; +// } +// } +// +// size_t realOutLength = 0; +// CCCryptorStatus result = CCCrypt(kCCEncrypt, kCCAlgorithmAES128, 0, key, 32, aesIv, tmpInBytes, length, outBytes, length, &realOutLength); +// free(tmpInBytes); +// +// assert(result == kCCSuccess); +// +// len = length / AES_BLOCK_SIZE; +// +// aes_block_t const *ivp = (aes_block_t *)inB; +// aes_block_t *iv2p = (aes_block_t *)ccIv; +// +// inB = (uint8_t *)inBytes; +// outB = (uint8_t *)outBytes; +// +// while (len) { +// aes_block_t *inp = (aes_block_t *)inB; +// aes_block_t *outp = (aes_block_t *)outB; +// +// for (n = 0; n < N_WORDS; ++n) { +// outp->data[n] ^= iv2p->data[n]; +// } +// ivp = outp; +// iv2p = inp; +// --len; +// inB += AES_BLOCK_SIZE; +// outB += AES_BLOCK_SIZE; +// } +// +// memcpy(iv, ivp->data, AES_BLOCK_SIZE); +// memcpy((void *)((uint8_t *)iv + AES_BLOCK_SIZE), iv2p->data, AES_BLOCK_SIZE); +//} +// +//void TGCallAesIgeEncryptInplace(uint8_t *inBytes, uint8_t *outBytes, size_t length, uint8_t *key, uint8_t *iv) +//{ +// uint8_t *outData = (uint8_t *)malloc(length); +// TGCallAesIgeEncrypt(inBytes, outData, length, key, iv); +// memcpy(outBytes, outData, length); +// free(outData); +//} +// +//void TGCallAesIgeDecrypt(uint8_t *inBytes, uint8_t *outBytes, size_t length, uint8_t *key, uint8_t *iv) { +// unsigned char aesIv[AES_BLOCK_SIZE]; +// memcpy(aesIv, iv, AES_BLOCK_SIZE); +// unsigned char ccIv[AES_BLOCK_SIZE]; +// memcpy(ccIv, (void *)((uint8_t *)iv + AES_BLOCK_SIZE), AES_BLOCK_SIZE); +// +// assert(((size_t)inBytes | (size_t)outBytes | (size_t)aesIv | (size_t)ccIv) % sizeof(long) == +// 0); +// +// CCCryptorRef decryptor = NULL; +// CCCryptorCreate(kCCDecrypt, kCCAlgorithmAES128, kCCOptionECBMode, key, 32, nil, &decryptor); +// if (decryptor != NULL) { +// size_t len; +// size_t n; +// +// len = length / AES_BLOCK_SIZE; +// +// aes_block_t *ivp = (aes_block_t *)(aesIv); +// aes_block_t *iv2p = (aes_block_t *)(ccIv); +// +// uint8_t *inB = (uint8_t *)inBytes; +// uint8_t *outB = (uint8_t *)outBytes; +// +// while (len) { +// aes_block_t tmp; +// aes_block_t *inp = (aes_block_t *)inB; +// aes_block_t *outp = (aes_block_t *)outB; +// +// for (n = 0; n < N_WORDS; ++n) +// tmp.data[n] = inp->data[n] ^ iv2p->data[n]; +// +// size_t dataOutMoved = 0; +// CCCryptorStatus result = CCCryptorUpdate(decryptor, &tmp, AES_BLOCK_SIZE, outB, AES_BLOCK_SIZE, &dataOutMoved); +// assert(result == kCCSuccess); +// assert(dataOutMoved == AES_BLOCK_SIZE); +// +// for (n = 0; n < N_WORDS; ++n) +// outp->data[n] ^= ivp->data[n]; +// +// ivp = inp; +// iv2p = outp; +// +// inB += AES_BLOCK_SIZE; +// outB += AES_BLOCK_SIZE; +// +// --len; +// } +// +// memcpy(iv, ivp->data, AES_BLOCK_SIZE); +// memcpy((void *)((uint8_t *)iv + AES_BLOCK_SIZE), iv2p->data, AES_BLOCK_SIZE); +// +// CCCryptorRelease(decryptor); +// } +//} +// +//void TGCallAesIgeDecryptInplace(uint8_t *inBytes, uint8_t *outBytes, size_t length, uint8_t *key, uint8_t *iv) { +// uint8_t *outData = (uint8_t *)malloc(length); +// TGCallAesIgeDecrypt(inBytes, outData, length, key, iv); +// memcpy(outBytes, outData, length); +// free(outData); +//} +// +//void TGCallSha1(uint8_t *msg, size_t length, uint8_t *output) +//{ +// CC_SHA1(msg, (CC_LONG)length, output); +//} +// +//void TGCallSha256(uint8_t *msg, size_t length, uint8_t *output) +//{ +// CC_SHA256(msg, (CC_LONG)length, output); +//} +// +//void TGCallRandomBytes(uint8_t *buffer, size_t length) +//{ +// arc4random_buf(buffer, length); +//} +// +// +//static void ctr128_inc(unsigned char *counter) +//{ +// uint32_t n = 16, c = 1; +// +// do { +// --n; +// c += counter[n]; +// counter[n] = (uint8_t)c; +// c >>= 8; +// } while (n); +//} +// +//static void ctr128_inc_aligned(unsigned char *counter) +//{ +// size_t *data, c, d, n; +// const union { +// long one; +// char little; +// } is_endian = { +// 1 +// }; +// +// if (is_endian.little || ((size_t)counter % sizeof(size_t)) != 0) { +// ctr128_inc(counter); +// return; +// } +// +// data = (size_t *)counter; +// c = 1; +// n = 16 / sizeof(size_t); +// do { +// --n; +// d = data[n] += c; +// /* did addition carry? */ +// c = ((d - c) ^ d) >> (sizeof(size_t) * 8 - 1); +// } while (n); +//} +// +//@interface TGCallAesCtr : NSObject { +// CCCryptorRef _cryptor; +// +// unsigned char _ivec[16]; +// unsigned int _num; +// unsigned char _ecount[16]; +//} +// +//@end +// +//@implementation TGCallAesCtr +// +//- (instancetype)initWithKey:(const void *)key keyLength:(int)keyLength iv:(const void *)iv ecount:(void *)ecount num:(uint32_t)num { +// self = [super init]; +// if (self != nil) { +// _num = num; +// memcpy(_ecount, ecount, 16); +// memcpy(_ivec, iv, 16); +// +// CCCryptorCreate(kCCEncrypt, kCCAlgorithmAES128, kCCOptionECBMode, key, keyLength, nil, &_cryptor); +// } +// return self; +//} +// +//- (void)dealloc { +// if (_cryptor) { +// CCCryptorRelease(_cryptor); +// } +//} +// +//- (uint32_t)num { +// return _num; +//} +// +//- (void *)ecount { +// return _ecount; +//} +// +//- (void)encryptIn:(const unsigned char *)in out:(unsigned char *)out len:(size_t)len { +// unsigned int n; +// size_t l = 0; +// +// assert(in && out); +// assert(_num < 16); +// +// n = _num; +// +// if (16 % sizeof(size_t) == 0) { /* always true actually */ +// do { +// while (n && len) { +// *(out++) = *(in++) ^ _ecount[n]; +// --len; +// n = (n + 1) % 16; +// } +// +// while (len >= 16) { +// size_t dataOutMoved; +// CCCryptorUpdate(_cryptor, _ivec, 16, _ecount, 16, &dataOutMoved); +// ctr128_inc_aligned(_ivec); +// for (n = 0; n < 16; n += sizeof(size_t)) +// *(size_t *)(out + n) = +// *(size_t *)(in + n) ^ *(size_t *)(_ecount + n); +// len -= 16; +// out += 16; +// in += 16; +// n = 0; +// } +// if (len) { +// size_t dataOutMoved; +// CCCryptorUpdate(_cryptor, _ivec, 16, _ecount, 16, &dataOutMoved); +// ctr128_inc_aligned(_ivec); +// while (len--) { +// out[n] = in[n] ^ _ecount[n]; +// ++n; +// } +// } +// _num = n; +// return; +// } while (0); +// } +// /* the rest would be commonly eliminated by x86* compiler */ +// +// while (l < len) { +// if (n == 0) { +// size_t dataOutMoved; +// CCCryptorUpdate(_cryptor, _ivec, 16, _ecount, 16, &dataOutMoved); +// ctr128_inc(_ivec); +// } +// out[l] = in[l] ^ _ecount[n]; +// ++l; +// n = (n + 1) % 16; +// } +// +// _num = n; +//} +// +//@end +// +// +//void TGCallAesCtrEncrypt(uint8_t *inOut, size_t length, uint8_t *key, uint8_t *iv, uint8_t *ecount, uint32_t *num) +//{ +// uint8_t *outData = (uint8_t *)malloc(length); +// TGCallAesCtr *aesCtr = [[TGCallAesCtr alloc] initWithKey:key keyLength:32 iv:iv ecount:ecount num:*num]; +// [aesCtr encryptIn:inOut out:outData len:length]; +// memcpy(inOut, outData, length); +// +// memcpy(ecount, [aesCtr ecount], 16); +// *num = [aesCtr num]; +//} diff --git a/Telegram-Mac/TGModernGrowingTextView.h b/Telegram-Mac/TGModernGrowingTextView.h index a2f4d0d1a6..77b691690a 100644 --- a/Telegram-Mac/TGModernGrowingTextView.h +++ b/Telegram-Mac/TGModernGrowingTextView.h @@ -9,7 +9,24 @@ #import #import "TGInputTextTag.h" -extern NSString * _Nonnull const TGMentionUidAttributeName; +extern NSString * _Nonnull const TGCustomLinkAttributeName; +@class TGModernGrowingTextView; + +@interface MarkdownUndoItem : NSObject +@property (nonatomic, strong) NSAttributedString *was; +@property (nonatomic, strong) NSAttributedString *be; +@property (nonatomic, assign) NSRange inRange; +-(id)initWithAttributedString:(NSAttributedString *)was be: (NSAttributedString *)be inRange:(NSRange)inRange; +@end + + +@interface SimpleUndoItem : NSObject +@property (nonatomic, strong) NSAttributedString *was; +@property (nonatomic, strong) NSAttributedString *be; +@property (nonatomic, assign) NSRange wasRange; +@property (nonatomic, assign) NSRange beRange; +-(id)initWithAttributedString:(NSAttributedString *)was be: (NSAttributedString *)be wasRange:(NSRange)wasRange beRange:(NSRange)beRange; +@end @protocol TGModernGrowingDelegate @@ -18,24 +35,36 @@ extern NSString * _Nonnull const TGMentionUidAttributeName; -(void) textViewTextDidChange:(NSString * __nonnull)string; -(void) textViewTextDidChangeSelectedRange:(NSRange)range; -(BOOL)textViewDidPaste:(NSPasteboard * __nonnull)pasteboard; --(int)maxCharactersLimit; +-(int)maxCharactersLimit:(TGModernGrowingTextView *)textView; --(NSSize)textViewSize; +-(NSSize)textViewSize:(TGModernGrowingTextView *)textView; -(BOOL)textViewIsTypingEnabled; @optional - (void) textViewNeedClose:(id __nonnull)textView; - (BOOL) canTransformInputText; +- (BOOL) supportContinuityCamera; +- (void)textViewDidReachedLimit:(id __nonnull)textView; +- (void)makeUrlOfRange: (NSRange)range; +- (BOOL)copyTextWithRTF:(NSAttributedString *)rtf; +- (NSArray *)textView:(NSTextView *)textView shouldUpdateTouchBarItemIdentifiers:(NSArray *)identifiers; +//func textView(_ textView: NSTextView, shouldUpdateTouchBarItemIdentifiers identifiers: [NSTouchBarItemIdentifier]) -> [NSTouchBarItemIdentifier] { @end +void setInputLocalizationFunc(NSString* _Nonnull (^ _Nonnull localizationF)(NSString * _Nonnull key)); +void setTextViewEnableTouchBar(BOOL enableTouchBar); - -@interface TGGrowingTextView : NSTextView +@interface TGGrowingTextView : NSTextView @property (nonatomic,weak) id __nullable weakd; +@property (nonatomic,weak) TGModernGrowingTextView * _Nullable weakTextView; + + @end -@interface TGModernGrowingTextView : NSView +@interface TGModernGrowingTextView : NSView + +-(instancetype)initWithFrame:(NSRect)frameRect unscrollable:(BOOL)unscrollable; @property (nonatomic,assign) BOOL animates; @@ -49,7 +78,6 @@ extern NSString * _Nonnull const TGMentionUidAttributeName; @property (nonatomic,strong) NSColor* _Nonnull textColor; @property (nonatomic,strong) NSColor* _Nonnull linkColor; @property (nonatomic,strong) NSFont* _Nonnull textFont; -@property (nonatomic,strong) NSString* _Nonnull defaultText; @property (nonatomic,strong,readonly) TGGrowingTextView* _Nonnull inputView; @@ -73,7 +101,7 @@ extern NSString * _Nonnull const TGMentionUidAttributeName; -(void)appendText:(id __nonnull)aString; -(void)insertText:(id __nonnull)aString replacementRange:(NSRange)replacementRange; -(void)addInputTextTag:(TGInputTextTag * __nonnull)tag range:(NSRange)range; - +-(void)scrollToCursor; -(void)replaceMention:(NSString * __nonnull)mention username:(bool)username userId:(int32_t)userId; -(void)paste:(id __nonnull)sender; @@ -87,7 +115,13 @@ extern NSString * _Nonnull const TGMentionUidAttributeName; -(void)codeWord; -(void)italicWord; -(void)boldWord; - +-(void)removeAllAttributes; +-(void)addLink:(NSString *_Nullable)link; +-(void)addLink:(NSString *_Nullable)link range: (NSRange)range; - (void)textDidChange:( NSNotification * _Nullable )notification; +- (void)addSimpleItem:(SimpleUndoItem *)item; + +-(void)setBackgroundColor:(NSColor * __nonnull)color; + @end diff --git a/Telegram-Mac/TGModernGrowingTextView.m b/Telegram-Mac/TGModernGrowingTextView.m index b42d501bec..a9d57fff94 100644 --- a/Telegram-Mac/TGModernGrowingTextView.m +++ b/Telegram-Mac/TGModernGrowingTextView.m @@ -8,40 +8,161 @@ #import "TGModernGrowingTextView.h" #import +#import "DateUtils.h" +#import "ObjcUtils.h" -@interface GrowingScrollView : NSScrollView +@interface TGTextFieldPlaceholder : NSTextField + + @end -@end +@interface TGModernGrowingTextView () + @property (nonatomic,strong) TGTextFieldPlaceholder *placeholder; + @end + +@implementation MarkdownUndoItem + -(id)initWithAttributedString:(NSAttributedString *)was be: (NSAttributedString *)be inRange:(NSRange)inRange { + if (self = [super init]) { + self.was = was; + self.be = be; + self.inRange = inRange; + } + return self; + } + @end + + +@implementation SimpleUndoItem +-(id)initWithAttributedString:(NSAttributedString *)was be: (NSAttributedString *)be wasRange:(NSRange)wasRange beRange:(NSRange)beRange { + if (self = [super init]) { + self.was = was; + self.be = be; + self.wasRange = wasRange; + self.beRange = beRange; + } + return self; +} + +-(void)setWas:(NSAttributedString *)was { + self->_was = was; +} + + @end + +static NSString* (^localizationFunc)(NSString *key); + +void setInputLocalizationFunc(NSString* (^localizationF)(NSString *key)) { + localizationFunc = localizationF; +} + +NSString * NSLocalized(NSString * key, NSString *comment) { + if (localizationFunc != nil) { + return localizationFunc(key); + } else { + return NSLocalizedString(key, comment); + } +} + +static BOOL textViewEnableTouchBar = true; + +void setTextViewEnableTouchBar(BOOL enableTouchBar) { + textViewEnableTouchBar = enableTouchBar; +} + +@interface GrowingScrollView : NSScrollView + + @end @implementation GrowingScrollView + + + @end -//-(void)scrollWheel:(NSEvent *)event { -// if ([self documentView].frame.size.height > self.frame.size.height) { -// [super scrollWheel:event]; -// } else { -// [[self superview].enclosingScrollView scrollWheel:event]; -// } -//} +@interface UnscrollableTextScrollView : NSScrollView + + @end -@end +@implementation UnscrollableTextScrollView + +-(void)scrollWheel:(NSEvent *)event { + [[self superview].enclosingScrollView scrollWheel:event]; +} + + @end @interface NSTextView () -(void)_shareServiceSelected:(id)sender; -@end + @end @interface TGModernGrowingTextView () + @property (nonatomic, assign) NSRange _selectedRange; + - (void)refreshAttributes; -@end + @end + +NSString *const TGCustomLinkAttributeName = @"TGCustomLinkAttributeName"; -NSString *const TGMentionUidAttributeName = @"TGMentionUidAttributeName"; @interface TGGrowingTextView () -@end + @property (nonatomic, strong) NSUndoManager *undo; + @property (nonatomic, strong) NSMutableArray *markdownItems; + @property (nonatomic, strong) NSTrackingArea *trackingArea; + + + @end -@implementation TGGrowingTextView + +@interface TGModernGrowingTextView () +-(void)textDidChange:(NSNotification *)notification; + + @end + +@implementation TGGrowingTextView + +-(instancetype)initWithFrame:(NSRect)frameRect { + if(self = [super initWithFrame:frameRect]) { + self.markdownItems = [NSMutableArray array]; + + NSTrackingArea *trackingArea = [[NSTrackingArea alloc]initWithRect:self.bounds options:NSTrackingCursorUpdate | NSTrackingActiveInActiveApp owner:self userInfo:nil]; + [self addTrackingArea:trackingArea]; + +#ifdef __MAC_10_12_2 + // self.allowsCharacterPickerTouchBarItem = false; +#endif + } + return self; +} + + - (void)mouseMoved:(NSEvent *)event + { + } + +- (void)updateTrackingAreas { + [super updateTrackingAreas]; + [self removeTrackingArea:_trackingArea]; + _trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] options: (NSTrackingMouseMoved | NSTrackingActiveInKeyWindow | NSTrackingMouseEnteredAndExited | NSTrackingCursorUpdate) owner:self userInfo:nil]; + [self addTrackingArea:_trackingArea]; +} + +-(id)validRequestorForSendType:(NSPasteboardType)sendType returnType:(NSPasteboardType)returnType { + if (([NSImage.imageTypes containsObject:returnType]) && [self.weakd respondsToSelector:@selector(supportContinuityCamera)] && [self.weakd supportContinuityCamera]) { + return self; + } else { + return nil; + } +} + +-(BOOL)readSelectionFromPasteboard:(NSPasteboard *)pboard { + if([pboard canReadItemWithDataConformingToTypes:NSImage.imageTypes]) { + [self.weakd textViewDidPaste:pboard]; + return YES; + } else { + return [super readSelectionFromPasteboard:pboard]; + } +} + -(NSPoint)textContainerOrigin { if(NSHeight(self.frame) <= 34) { @@ -53,26 +174,58 @@ -(NSPoint)textContainerOrigin { return [super textContainerOrigin]; } - + +-(void)drawRect:(NSRect)dirtyRect { + + + CGContextRef context = (CGContextRef)[[NSGraphicsContext currentContext] + graphicsPort]; + + BOOL isRetina = self.window.backingScaleFactor == 2.0; + + CGContextSetAllowsAntialiasing(context, true); + CGContextSetShouldSmoothFonts(context, !isRetina); + CGContextSetAllowsFontSmoothing(context,!isRetina); + + [super drawRect:dirtyRect]; + +} + + +-(void)setSelectedRange:(NSRange)selectedRange { + [super setSelectedRange:selectedRange]; +} +-(void)rightMouseDown:(NSEvent *)event { + [self.window makeFirstResponder:self]; + [super rightMouseDown:event]; +} + -(void)paste:(id)sender { if (![self.weakd textViewDidPaste:[NSPasteboard generalPasteboard]]) { [super paste:sender]; } } - + -(BOOL)becomeFirstResponder { return [super becomeFirstResponder]; } - + +-(BOOL)resignFirstResponder { + return [super resignFirstResponder]; +} + -(void)changeLayoutOrientation:(id)sender { } - + -(NSMenu *)menuForEvent:(NSEvent *)event { NSMenu *menu = [super menuForEvent:event]; NSMutableArray *removeItems = [[NSMutableArray alloc] init]; + __block BOOL addedTransformations = false; + + [menu.itemArray enumerateObjectsUsingBlock:^(NSMenuItem * _Nonnull item, NSUInteger idx, BOOL * _Nonnull s) { if (item.action == @selector(submenuAction:)) { @@ -81,12 +234,15 @@ -(NSMenu *)menuForEvent:(NSEvent *)event { [removeItems addObject:item]; *stop = YES; } else if (subItem.action == @selector(capitalizeWord:)) { + addedTransformations = true; if ([_weakd respondsToSelector:@selector(canTransformInputText)]) { if (self.selectedRange.length > 0) { if ([_weakd canTransformInputText]) { [self.transformItems enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { [item.submenu insertItem:obj atIndex:0]; }]; + [item.submenu insertItem:[NSMenuItem separatorItem] atIndex: self.transformItems.count]; + // [item.submenu insertItem:[[NSMenuItem alloc] initWithTitle:@"Remove All Transformations" action:nil keyEquivalent:nil] atIndex:0]; } } else { [removeItems addObject:item]; @@ -97,58 +253,153 @@ -(NSMenu *)menuForEvent:(NSEvent *)event { } }]; + if (!addedTransformations) { + if ([_weakd respondsToSelector:@selector(canTransformInputText)]) { + if (self.selectedRange.length > 0) { + if ([_weakd canTransformInputText]) { + NSMenuItem *sep = [NSMenuItem separatorItem]; + [menu addItem: sep]; + + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:NSLocalized(@"Text.View.Transformations", nil) action:nil keyEquivalent:@""]; + + item.submenu = [[NSMenu alloc] init]; + + [self.transformItems enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + [item.submenu insertItem:obj atIndex:0]; + }]; + [menu addItem:item]; + } + } + } + } + [removeItems enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { [menu removeItem:obj]; }]; + [self.window makeFirstResponder:self]; return menu; } - + -(NSArray *)transformItems { - NSMenuItem *bold = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"TextView.Transform.Bold", nil) action:@selector(boldWord:) keyEquivalent:@"b"]; + NSMenuItem *bold = [[NSMenuItem alloc] initWithTitle:NSLocalized(@"TextView.Transform.Bold", nil) action:@selector(boldWord:) keyEquivalent:@"b"]; [bold setKeyEquivalentModifierMask: NSCommandKeyMask]; - NSMenuItem *italic = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"TextView.Transform.Italic", nil) action:@selector(italicWord:) keyEquivalent:@"i"]; + NSMenuItem *italic = [[NSMenuItem alloc] initWithTitle:NSLocalized(@"TextView.Transform.Italic", nil) action:@selector(italicWord:) keyEquivalent:@"i"]; [italic setKeyEquivalentModifierMask: NSCommandKeyMask]; - NSMenuItem *code = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"TextView.Transform.Code", nil) action:@selector(codeWord:) keyEquivalent:@"k"]; + NSMenuItem *code = [[NSMenuItem alloc] initWithTitle:NSLocalized(@"TextView.Transform.Code", nil) action:@selector(codeWord:) keyEquivalent:@"k"]; [code setKeyEquivalentModifierMask: NSShiftKeyMask | NSCommandKeyMask]; - return @[code, italic, bold]; + + NSMenuItem *url = [[NSMenuItem alloc] initWithTitle:NSLocalized(@"TextView.Transform.URL", nil) action:@selector(makeUrl:) keyEquivalent:@"u"]; + [url setKeyEquivalentModifierMask: NSCommandKeyMask]; + + NSMenuItem *removeAll = [[NSMenuItem alloc] initWithTitle:NSLocalized(@"TextView.Transform.RemoveAll", nil) action:@selector(removeAll:) keyEquivalent:@""]; + + + return @[removeAll, [NSMenuItem separatorItem], code, italic, bold, url]; } - - - --(void)boldWord:(id)sender { - [self changeFontMarkdown:[NSFont boldSystemFontOfSize:self.font.pointSize]]; - // [self.textStorage addAttribute:NSFontAttributeName value:[NSFont boldSystemFontOfSize:self.font.pointSize] range:self.selectedRange]; - // [_weakd textViewTextDidChangeSelectedRange:self.selectedRange]; + + +-(NSTouchBar *)makeTouchBar { + return textViewEnableTouchBar ? [super makeTouchBar] : nil; } + +-(void)removeAll:(id)sender { + NSRange selectedRange = self.selectedRange; + NSMutableAttributedString *attr = [self.attributedString mutableCopy]; + [attr removeAttribute:TGCustomLinkAttributeName range:selectedRange]; + [attr addAttribute:NSFontAttributeName value:[NSFont systemFontOfSize:self.font.pointSize] range: selectedRange]; + + [self.textStorage setAttributedString:attr]; + [self setSelectedRange:NSMakeRange(selectedRange.location + selectedRange.length, 0)]; + // [attr enumerateAttributesInRange:selectedRange options:nil usingBlock:^(NSDictionary * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) { + // + // }]; +} + +-(void)boldWord:(id)sender { + if(self.selectedRange.length == 0) { + return; + } --(void)italicWord:(id)sender { - [self changeFontMarkdown:[[NSFontManager sharedFontManager] convertFont:[NSFont systemFontOfSize:self.font.pointSize] toHaveTrait:NSFontItalicTrait]]; + NSRange effectiveRange; + NSFont *effectiveFont; + for (int i = self.selectedRange.location; i < self.selectedRange.location + self.selectedRange.length; i++) { + effectiveFont = [self.textStorage attribute:NSFontAttributeName atIndex:i effectiveRange:&effectiveRange]; + if (![effectiveFont.fontName hasPrefix:@".AppleColorEmojiUI"]) { + break; + } + } + + [self changeFontMarkdown:[NSFontManager.sharedFontManager convertFont:effectiveFont toHaveTrait:NSBoldFontMask] makeBold:YES makeItalic:NO]; + + // [self.textStorage addAttribute:NSFontAttributeName value:[NSFont boldSystemFontOfSize:self.font.pointSize] range:self.selectedRange]; + // [_weakd textViewTextDidChangeSelectedRange:self.selectedRange]; +} -// [self.textStorage addAttribute:NSFontAttributeName value:[[NSFontManager sharedFontManager] convertFont:[NSFont systemFontOfSize:13] toHaveTrait:NSFontItalicTrait] range:self.selectedRange]; -// [_weakd textViewTextDidChangeSelectedRange:self.selectedRange]; +-(void)makeUrl:(id)sender { + [self.weakd makeUrlOfRange:self.selectedRange]; +} + +-(void)addLink:(NSString *)link { + [self.textStorage addAttribute:NSLinkAttributeName value: link range:self.selectedRange]; +} +-(void)addLink:(NSString *)link range: (NSRange)range { + [self.textStorage addAttribute:NSLinkAttributeName value: link range: range]; } + +-(void)italicWord:(id)sender { + if(self.selectedRange.length == 0) { + return; + } --(void)codeWord:(id)sender { - [self changeFontMarkdown:[NSFont fontWithName:@"Menlo-Regular" size:self.font.pointSize]]; -// [self.textStorage addAttribute:NSFontAttributeName value:[NSFont fontWithName:@"Menlo-Regular" size:self.font.pointSize] range:self.selectedRange]; -// [_weakd textViewTextDidChangeSelectedRange:self.selectedRange]; + NSRange effectiveRange; + NSFont *effectiveFont; + for (int i = self.selectedRange.location; i < self.selectedRange.location + self.selectedRange.length; i++) { + effectiveFont = [self.textStorage attribute:NSFontAttributeName atIndex:i effectiveRange:&effectiveRange]; + if (![effectiveFont.fontName hasPrefix:@".AppleColorEmojiUI"]) { + break; + } + } + [self changeFontMarkdown:[[NSFontManager sharedFontManager] convertFont:effectiveFont toHaveTrait:NSFontItalicTrait] makeBold:NO makeItalic:YES]; + + // [self.textStorage addAttribute:NSFontAttributeName value:[[NSFontManager sharedFontManager] convertFont:[NSFont systemFontOfSize:13] toHaveTrait:NSFontItalicTrait] range:self.selectedRange]; + // [_weakd textViewTextDidChangeSelectedRange:self.selectedRange]; + } + + +-(void)codeWord:(id)sender { + if(self.selectedRange.length == 0) { + return; + } --(void)changeFontMarkdown:(NSFont *)font { + [self changeFontMarkdown:[NSFont fontWithName:@"Menlo-Regular" size:self.font.pointSize] makeBold:NO makeItalic:NO]; + // [self.textStorage addAttribute:NSFontAttributeName value:[NSFont fontWithName:@"Menlo-Regular" size:self.font.pointSize] range:self.selectedRange]; + // [_weakd textViewTextDidChangeSelectedRange:self.selectedRange]; +} + +-(void)changeFontMarkdown:(NSFont *)font makeBold:(BOOL)makeBold makeItalic:(BOOL)makeItalic { if(self.selectedRange.length == 0) { return; } - NSRange effectiveRange; - NSFont *effectiveFont = [self.textStorage attribute:NSFontAttributeName atIndex:self.selectedRange.location effectiveRange:&effectiveRange]; + NSAttributedString *was = [self.attributedString attributedSubstringFromRange:self.selectedRange]; + + NSRange effectiveRange; + NSFont *effectiveFont; + for (int i = self.selectedRange.location; i < self.selectedRange.location + self.selectedRange.length; i++) { + effectiveFont = [self.textStorage attribute:NSFontAttributeName atIndex:i effectiveRange:&effectiveRange]; + if (![effectiveFont.fontName hasPrefix:@".AppleColorEmojiUI"]) { + break; + } + } NSFontDescriptor *descriptor = font.fontDescriptor; NSFontSymbolicTraits symTraits = [descriptor symbolicTraits]; @@ -164,125 +415,200 @@ -(void)changeFontMarkdown:(NSFont *)font { dispatch_block_t block = ^{ + + NSFont *newFont = [[NSFontManager sharedFontManager] convertFont:font toNotHaveTrait:makeBold ? NSBoldFontMask : makeItalic ? NSItalicFontMask : 0]; + if (self.selectedRange.location >= effectiveRange.location && self.selectedRange.location + self.selectedRange.length <= effectiveRange.location + effectiveRange.length) { - [self.textStorage addAttribute:NSFontAttributeName value:[NSFont systemFontOfSize:13] range:self.selectedRange]; + [self.textStorage addAttribute:NSFontAttributeName value:newFont range:self.selectedRange]; } else if (self.selectedRange.location >= effectiveRange.location) { [self.textStorage addAttribute:NSFontAttributeName value:font range:self.selectedRange]; } else { - [self.textStorage addAttribute:NSFontAttributeName value:[NSFont systemFontOfSize:13] range:self.selectedRange]; + [self.textStorage addAttribute:NSFontAttributeName value:newFont range:self.selectedRange]; } }; + BOOL doNext = YES; + if (isBold) { - if (isEffectiveBold) { + if (isEffectiveBold && makeBold) { block(); + doNext = NO; } else { [self.textStorage addAttribute:NSFontAttributeName value:font range:self.selectedRange]; } - } else if (isItalic) { - if (isEffectiveItalic) { + } + if (isItalic) { + if (isEffectiveItalic && makeItalic) { block(); - } else { + } else if (doNext) { [self.textStorage addAttribute:NSFontAttributeName value:font range:self.selectedRange]; + doNext = NO; } - } else if (isMonospace) { + } + if (isMonospace) { if (isEffectiveMonospace) { block(); - } else { + } else if (doNext) { [self.textStorage addAttribute:NSFontAttributeName value:font range:self.selectedRange]; } } + NSAttributedString *be = [self.attributedString attributedSubstringFromRange:self.selectedRange]; + + + [_weakd textViewTextDidChangeSelectedRange:self.selectedRange]; - + + MarkdownUndoItem *item = [[MarkdownUndoItem alloc] initWithAttributedString:was be:be inRange:self.selectedRange]; + [self addItem:item]; + } - - - + + +- (void)addItem:(MarkdownUndoItem *)item { + [[self undoManager] registerUndoWithTarget:self selector:@selector(removeItem:) object:item]; + if (![[self undoManager] isUndoing]) { + [[self undoManager] setActionName:NSLocalizedString(@"actions.add-item", @"Add Item")]; + } + [[self textStorage] replaceCharactersInRange:item.inRange withAttributedString:item.be]; + [self.markdownItems addObject:item]; + [self.weakd textViewTextDidChangeSelectedRange:self.selectedRange]; +} + +- (void)removeItem:(MarkdownUndoItem *)item { + [[self undoManager] registerUndoWithTarget:self selector:@selector(addItem:) object:item]; + if (![[self undoManager] isUndoing]) { + [[self undoManager] setActionName:NSLocalizedString(@"actions.remove-item", @"Remove Item")]; + } + if ([self.markdownItems indexOfObject:item] != NSNotFound) { + [[self textStorage] replaceCharactersInRange:item.inRange withAttributedString:item.was]; + [self.markdownItems removeObject:item]; + [self.weakd textViewTextDidChangeSelectedRange:self.selectedRange]; + } +} + +- (void)addSimpleItem:(SimpleUndoItem *)item { + [[self undoManager] registerUndoWithTarget:self selector:@selector(removeSimpleItem:) object:item]; + if (![[self undoManager] isUndoing]) { + [[self undoManager] setActionName:NSLocalizedString(@"actions.add-item", @"Add Item")]; + } + [[self textStorage] setAttributedString:item.be]; + [self setSelectedRange:item.beRange]; + [self.weakd textViewTextDidChangeSelectedRange:self.selectedRange]; +} + +- (void)removeSimpleItem:(SimpleUndoItem *)item { + [[self undoManager] registerUndoWithTarget:self selector:@selector(addSimpleItem:) object:item]; + if (![[self undoManager] isUndoing]) { + [[self undoManager] setActionName:NSLocalizedString(@"actions.remove-item", @"Remove Item")]; + } + [[self textStorage] setAttributedString:item.was]; + [self setSelectedRange:item.wasRange]; + [self.weakd textViewTextDidChangeSelectedRange:item.wasRange]; + [self.weakTextView update:YES]; +} + + + -(BOOL)validateMenuItem:(NSMenuItem *)menuItem { if(menuItem.action == @selector(changeLayoutOrientation:)) { return NO; } + if(menuItem.action == @selector(copy:)) { + return self.selectedRange.length > 0; + } return [super validateMenuItem:menuItem]; } - - -- (void)setContinuousSpellCheckingEnabled:(BOOL)flag -{ - [[NSUserDefaults standardUserDefaults] setBool: flag forKey:[NSString stringWithFormat:@"ContinuousSpellCheckingEnabled%@",NSStringFromClass([self class])]]; - [super setContinuousSpellCheckingEnabled: flag]; + +-(void)copy:(id)sender { + if (self.selectedRange.length > 0) { + if ([self.weakd respondsToSelector:@selector(copyTextWithRTF:)]) { + if (![self.weakd copyTextWithRTF: [self.attributedString attributedSubstringFromRange:self.selectedRange]]) { + [super copy:sender]; + } + } else { + [super copy:sender]; + } + } } - + + +- (void)setContinuousSpellCheckingEnabled:(BOOL)flag + { + [[NSUserDefaults standardUserDefaults] setBool: flag forKey:[NSString stringWithFormat:@"ContinuousSpellCheckingEnabled%@",NSStringFromClass([self class])]]; + [super setContinuousSpellCheckingEnabled: flag]; + } + -(BOOL)isContinuousSpellCheckingEnabled { return [[NSUserDefaults standardUserDefaults] boolForKey:[NSString stringWithFormat:@"ContinuousSpellCheckingEnabled%@",NSStringFromClass([self class])]]; } - + -(void)setGrammarCheckingEnabled:(BOOL)flag { [[NSUserDefaults standardUserDefaults] setBool: flag forKey:[NSString stringWithFormat:@"GrammarCheckingEnabled%@",NSStringFromClass([self class])]]; [super setGrammarCheckingEnabled: flag]; } - + -(BOOL)isGrammarCheckingEnabled { return [[NSUserDefaults standardUserDefaults] boolForKey:[NSString stringWithFormat:@"GrammarCheckingEnabled%@",NSStringFromClass([self class])]]; } - - + + -(void)setAutomaticSpellingCorrectionEnabled:(BOOL)flag { [[NSUserDefaults standardUserDefaults] setBool: flag forKey:[NSString stringWithFormat:@"AutomaticSpellingCorrectionEnabled%@",NSStringFromClass([self class])]]; [super setAutomaticSpellingCorrectionEnabled: flag]; } - + -(BOOL)isAutomaticSpellingCorrectionEnabled { return [[NSUserDefaults standardUserDefaults] boolForKey:[NSString stringWithFormat:@"AutomaticSpellingCorrectionEnabled%@",NSStringFromClass([self class])]]; } - - - + + + -(void)setAutomaticQuoteSubstitutionEnabled:(BOOL)flag { [[NSUserDefaults standardUserDefaults] setBool: flag forKey:[NSString stringWithFormat:@"AutomaticQuoteSubstitutionEnabled%@",NSStringFromClass([self class])]]; [super setAutomaticSpellingCorrectionEnabled: flag]; } - + -(BOOL)isAutomaticQuoteSubstitutionEnabled { return [[NSUserDefaults standardUserDefaults] boolForKey:[NSString stringWithFormat:@"AutomaticQuoteSubstitutionEnabled%@",NSStringFromClass([self class])]]; } - - + + -(void)setAutomaticLinkDetectionEnabled:(BOOL)flag { [[NSUserDefaults standardUserDefaults] setBool: flag forKey:[NSString stringWithFormat:@"AutomaticLinkDetectionEnabled%@",NSStringFromClass([self class])]]; [super setAutomaticSpellingCorrectionEnabled: flag]; } - + -(BOOL)isAutomaticLinkDetectionEnabled { return [[NSUserDefaults standardUserDefaults] boolForKey:[NSString stringWithFormat:@"AutomaticLinkDetectionEnabled%@",NSStringFromClass([self class])]]; } - - + + -(void)setAutomaticDataDetectionEnabled:(BOOL)flag { [[NSUserDefaults standardUserDefaults] setBool: flag forKey:[NSString stringWithFormat:@"AutomaticDataDetectionEnabled%@",NSStringFromClass([self class])]]; [super setAutomaticSpellingCorrectionEnabled: flag]; } - + -(BOOL)isAutomaticDataDetectionEnabled { return [[NSUserDefaults standardUserDefaults] boolForKey:[NSString stringWithFormat:@"AutomaticDataDetectionEnabled%@",NSStringFromClass([self class])]]; } - - - + + + -(void)setAutomaticDashSubstitutionEnabled:(BOOL)flag { [[NSUserDefaults standardUserDefaults] setBool: flag forKey:[NSString stringWithFormat:@"AutomaticDashSubstitutionEnabled%@",NSStringFromClass([self class])]]; [super setAutomaticSpellingCorrectionEnabled: flag]; } - + -(BOOL)isAutomaticDashSubstitutionEnabled { return [[NSUserDefaults standardUserDefaults] boolForKey:[NSString stringWithFormat:@"AutomaticDashSubstitutionEnabled%@",NSStringFromClass([self class])]]; } - - - + + + -(NSUInteger)numberOfLines { NSString *s = [self string]; @@ -294,15 +620,19 @@ -(NSUInteger)numberOfLines { } return numberOfLines; } - -BOOL isEnterEvent(NSEvent *theEvent) { - BOOL isEnter = (theEvent.keyCode == 0x24 || theEvent.keyCode == 0x4C); // VK_RETURN - return isEnter; + BOOL isEnterEvent(NSEvent *theEvent) { + BOOL isEnter = (theEvent.keyCode == 0x24 || theEvent.keyCode == 0x4C); // VK_RETURN + + return isEnter; + } + +-(void)insertNewline:(id)sender { + [super insertNewline:sender]; } - + - (void) keyDown:(NSEvent *)theEvent { if(_weakd.textViewIsTypingEnabled) { @@ -311,153 +641,203 @@ - (void) keyDown:(NSEvent *)theEvent { BOOL result = [_weakd textViewEnterPressed:theEvent]; - if(!result) { - [self insertNewline:self]; + if ((!result && (theEvent.modifierFlags & NSEventModifierFlagCommand)) || (!result && (theEvent.modifierFlags & NSEventModifierFlagShift))) { + [super insertNewline:self]; + return; } - return; - } else if(theEvent.keyCode == 53 && [_weakd respondsToSelector:@selector(textViewNeedClose:)]) { + + if (result) { + return; + } + } else if(theEvent.keyCode == 53 && [_weakd respondsToSelector:@selector(textViewNeedClose:)]) { [_weakd textViewNeedClose:self]; return; } - + if (!(theEvent.modifierFlags & NSEventModifierFlagCommand) || !isEnterEvent(theEvent)) { + [super keyDown:theEvent]; + } + } else if(_weakd == nil) { [super keyDown:theEvent]; } } - + -(void)setFrameSize:(NSSize)newSize { [super setFrameSize:newSize]; } - - - --(BOOL)resignFirstResponder { - return [super resignFirstResponder]; -} - + + + + -(void)setString:(NSString *)string { [super setString:string]; } - - + @end -@interface TGTextFieldPlaceholder : NSTextField -@end @implementation TGTextFieldPlaceholder + +-(void)drawRect:(NSRect)dirtyRect { + + CGContextRef context = (CGContextRef)[[NSGraphicsContext currentContext] + graphicsPort]; + + BOOL isRetina = self.window.backingScaleFactor == 2.0; + + if (isRetina) { + CGContextSetAllowsAntialiasing(context, true); + CGContextSetShouldSmoothFonts(context, false); + CGContextSetAllowsFontSmoothing(context, false); + } + [super drawRect:dirtyRect]; + +} + +-(NSMenu *)menuForEvent:(NSEvent *)event { + return [self.superview menuForEvent:event]; +} - +-(void)mouseDown:(NSEvent *)event { + [super mouseDown:event]; +} + @end @interface TGModernGrowingTextView () { int _last_height; } -@property (nonatomic,strong) TGGrowingTextView *textView; -@property (nonatomic,strong) GrowingScrollView *scrollView; -@property (nonatomic,strong) TGTextFieldPlaceholder *placeholder; -@property (nonatomic,assign) BOOL notify_next; -@property (nonatomic, strong) NSUndoManager *_undo; -@end + @property (nonatomic,strong) TGGrowingTextView *textView; + @property (nonatomic,strong) NSScrollView *scrollView; + @property (nonatomic,assign) BOOL notify_next; + @property (nonatomic, strong) NSUndoManager *_undo; + @end @implementation TGModernGrowingTextView - - - - + + -(instancetype)initWithFrame:(NSRect)frameRect { + if (self = [self initWithFrame: frameRect unscrollable: false]) { + + } + return self; +} + +-(instancetype)initWithFrame:(NSRect)frameRect unscrollable:(BOOL)unscrollable { if(self = [super initWithFrame:frameRect]) { _min_height = 34; _max_height = 200; _animates = YES; _cursorColor = [NSColor blackColor]; - + _textView = [[[self _textViewClass] alloc] initWithFrame:self.bounds]; [_textView setRichText:NO]; [_textView setImportsGraphics:NO]; - _textView.backgroundColor = [NSColor clearColor]; _textView.insertionPointColor = _cursorColor; - self.scrollView.backgroundColor = [NSColor clearColor]; [_textView setAllowsUndo:YES]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(selectionDidChanged:) name:NSTextViewDidChangeSelectionNotification object:_textView]; self._undo = [[NSUndoManager alloc] init]; + self.textView.undo = self._undo; self.autoresizesSubviews = YES; _textView.delegate = self; - [_textView setDrawsBackground:YES]; + [_textView setDrawsBackground:NO]; + + + if (unscrollable) { + self.scrollView = [[UnscrollableTextScrollView alloc] initWithFrame:self.bounds]; + } else { + self.scrollView = [[GrowingScrollView alloc] initWithFrame:self.bounds]; + } + - self.scrollView = [[GrowingScrollView alloc] initWithFrame:self.bounds]; [[self.scrollView verticalScroller] setControlSize:NSSmallControlSize]; self.scrollView.documentView = _textView; - [self.scrollView setDrawsBackground:NO]; [self.scrollView setFrame:NSMakeRect(0, 0, NSWidth(self.frame), NSHeight(self.frame))]; [self addSubview:self.scrollView]; + [self.scrollView setDrawsBackground:NO]; self.wantsLayer = _textView.wantsLayer = _scrollView.wantsLayer = YES; - + _placeholder = [[TGTextFieldPlaceholder alloc] init]; + _placeholder.layer.opacity = 0.7; _placeholder.wantsLayer = YES; [_placeholder setBordered:NO]; [_placeholder setDrawsBackground:NO]; [_placeholder setSelectable:NO]; [_placeholder setEditable:NO]; - [[_placeholder cell] setLineBreakMode:NSLineBreakByTruncatingTail]; [_placeholder setEnabled:NO]; + [_placeholder setLineBreakMode:NSLineBreakByTruncatingTail]; + [_placeholder setMaximumNumberOfLines:0]; + + [[_placeholder cell] setLineBreakMode:NSLineBreakByTruncatingTail]; + [[_placeholder cell] setTruncatesLastVisibleLine:YES]; [self addSubview:_placeholder]; - + + _textView.weakTextView = self; + } return self; } - + -(NSUndoManager *)undoManagerForTextView:(NSTextView *)view { return self._undo; } - + -(void)setCursorColor:(NSColor *)cursorColor { _cursorColor = cursorColor; _textView.insertionPointColor = _cursorColor; } - + -(void)setTextColor:(NSColor *)textColor { _textColor = textColor; _textView.insertionPointColor = _textColor; + _textView.textColor = _textColor; [self textDidChange:nil]; } - + -(void)setTextFont:(NSFont *)textFont { _textFont = textFont; _textView.font = textFont; } - + + -(void)selectionDidChanged:(NSNotification *)notification { if (!_notify_next) { _notify_next = YES; return; } - [self.delegate textViewTextDidChangeSelectedRange:self.textView.selectedRange]; + + + if ((self._selectedRange.location != self.textView.selectedRange.location) || (self._selectedRange.length != self.textView.selectedRange.length)) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.delegate textViewTextDidChangeSelectedRange:self.textView.selectedRange]; + }); + self._selectedRange = self.textView.selectedRange; + } NSRect newRect = [_textView.layoutManager usedRectForTextContainer:_textView.textContainer]; NSSize size = newRect.size; size.width = NSWidth(self.frame); NSSize newSize = NSMakeSize(size.width, size.height); - newSize.height+= 8; + newSize.height+= 2; newSize.height = MIN(MAX(newSize.height,_min_height),_max_height); - [self updatePlaceholder:false newSize:newSize]; + [self updatePlaceholder:self.animates newSize:newSize]; } - + -(void)mouseDown:(NSEvent *)theEvent { [super mouseDown:theEvent]; if(self.window.firstResponder != _textView) { @@ -465,79 +845,60 @@ -(void)mouseDown:(NSEvent *)theEvent { } [self update:NO]; } - + -(BOOL)becomeFirstResponder { - // if(self.window.firstResponder != _textView) { - [self.window makeFirstResponder:_textView]; - // } + // if(self.window.firstResponder != _textView) { + [self.window makeFirstResponder:_textView]; + // } return YES; } - + -(BOOL)resignFirstResponder { return [_textView resignFirstResponder]; } - + -(int)height { return _last_height; } - + -(void)setDelegate:(id)delegate { _delegate = _textView.weakd = delegate; } - - + + -(void)update:(BOOL)notify { [self textDidChange:[NSNotification notificationWithName:NSTextDidChangeNotification object:notify ? _textView : nil]]; - [__undo removeAllActionsWithTarget:_textView]; - [__undo removeAllActions]; - } - - + + - (void)drawRect:(NSRect)dirtyRect { [super drawRect:dirtyRect]; } --(BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector { - if ((commandSelector == @selector(deleteBackward:) || commandSelector == @selector(deleteForward:)) && _defaultText.length > 0) { - if ([textView.string isEqualToString:_defaultText]) { - return true; - } - } - return false; -} - --(BOOL)textView:(NSTextView *)textView shouldChangeTextInRanges:(NSArray *)affectedRanges replacementStrings:(NSArray *)replacementStrings { - if (_defaultText.length > 0) { - __block BOOL cancel = true; - [affectedRanges enumerateObjectsUsingBlock:^(NSValue * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - NSRange range = obj.rangeValue; - if (range.location < _defaultText.length) { - cancel = false; - *stop = YES; - } - }]; - if (cancel) { - [self setSelectedRange:NSMakeRange(textView.string.length, 0)]; - } - return cancel; - } - return true; -} - - - - -- (void)textDidChange:(NSNotification *)notification { +-(NSArray *)textView:(NSTextView *)textView shouldUpdateTouchBarItemIdentifiers:(NSArray *)identifiers { + if ([self.delegate respondsToSelector:@selector(textView:shouldUpdateTouchBarItemIdentifiers:)]) { + return [self.delegate textView: textView shouldUpdateTouchBarItemIdentifiers: identifiers]; + } + return identifiers; +} - int limit = self.delegate == nil ? INT32_MAX : [self.delegate maxCharactersLimit]; +- (void)textDidChange:(NSNotification *)notification { + int limit = self.delegate == nil ? INT32_MAX : [self.delegate maxCharactersLimit: self]; - if (self.string != nil && self.string.length > 0 && self.string.length - _defaultText.length > limit) { - NSString *sub = [self.string substringWithRange:NSMakeRange(_defaultText.length, limit)]; - [self setString:sub animated: notification != nil]; + if (self.string != nil && self.string.length > 0 && self.string.length > limit) { + + NSAttributedString *string = [self.attributedString attributedSubstringFromRange:NSMakeRange(0, MIN(limit, self.attributedString.string.length))]; + + NSMutableAttributedString *attr = [[NSMutableAttributedString alloc] initWithAttributedString: string]; + NSRange selectedRange = _textView.selectedRange; + [_textView.textStorage setAttributedString:attr]; + [self update:notification != nil]; + [self setSelectedRange:NSMakeRange(MIN(selectedRange.location, string.length), 0)]; + if ([self.delegate respondsToSelector:@selector(textViewDidReachedLimit:)]) + [self.delegate textViewDidReachedLimit: self]; return; } @@ -556,6 +917,15 @@ - (void)textDidChange:(NSNotification *)notification { } } + if(notification.object) { + NSString *text = self.string; + [self.delegate textViewTextDidChange:text]; + if (![text isEqualToString:self.string]) { + return; + } + + } + self.scrollView.verticalScrollElasticity = NSHeight(_scrollView.contentView.documentRect) <= NSHeight(_scrollView.frame) ? NSScrollElasticityNone : NSScrollElasticityAllowed; [_textView.layoutManager ensureLayoutForTextContainer:_textView.textContainer]; @@ -564,16 +934,16 @@ - (void)textDidChange:(NSNotification *)notification { NSSize size = newRect.size; size.width = NSWidth(self.frame); - + NSSize newSize = NSMakeSize(size.width, size.height); - - newSize.height+= 8; + + newSize.height+= 2; newSize.height = MIN(MAX(newSize.height,_min_height),_max_height); - BOOL animated = self.animates; + BOOL animated = self.animates && ![self.window inLiveResize]; if(_last_height != newSize.height) { @@ -588,7 +958,7 @@ - (void)textDidChange:(NSNotification *)notification { [_textView.layoutManager ensureLayoutForTextContainer:_textView.textContainer]; - newSize.width = [_delegate textViewSize].width; + newSize.width = [_delegate textViewSize: self].width; NSSize layoutSize = NSMakeSize(roundf(newSize.width), roundf(newSize.height)); @@ -638,23 +1008,24 @@ - (void)textDidChange:(NSNotification *)notification { [self setFrame:NSMakeRect(NSMinX(self.frame), NSMinY(self.frame), layoutSize.width, layoutSize.height)]; [_scrollView setFrameSize:layoutSize]; - - future(); [CATransaction commit]; - + } else { [self setFrameSize:layoutSize]; future(); } + } else { + int bp = 0; + bp += 1; } - // if(self._needShowPlaceholder) { + // if(self._needShowPlaceholder) { [self updatePlaceholder: animated newSize: newSize]; @@ -662,31 +1033,64 @@ - (void)textDidChange:(NSNotification *)notification { [self setNeedsDisplay:YES]; if (_textView.selectedRange.location != NSNotFound) { - [_textView setSelectedRange:_textView.selectedRange]; + [self setSelectedRange:_textView.selectedRange]; } [self setNeedsDisplay:YES]; - if(notification.object) { - NSString *text = self.string; - if (_defaultText.length > 0) { - NSRange range = [text rangeOfString:_defaultText]; - if (range.location != NSNotFound) { - text = [text substringFromIndex:range.location + range.length]; - } else if ([_defaultText containsString:text]) { - text = @""; - } + + [_textView setNeedsDisplay:YES]; + + [self refreshAttributes]; + +} + +- (NSRect) highlightRectForRange:(NSRange)aRange + { + if (aRange.location > self.string.length || self.string.length == 0) { + return NSZeroRect; + } + NSRange r = aRange; + NSRange startLineRange = [[self string] lineRangeForRange:NSMakeRange(r.location, 0)]; + NSInteger er = NSMaxRange(r)-1; + NSString *text = [self string]; + + if (er >= [text length]) { + return NSZeroRect; + } + if (er < r.location) { + er = r.location; } - dispatch_async(dispatch_get_main_queue(), ^{ - [self.delegate textViewTextDidChange:text]; - }); + + NSRange gr = [[self.textView layoutManager] glyphRangeForCharacterRange:aRange + actualCharacterRange:NULL]; + NSRect br = [[self.textView layoutManager] boundingRectForGlyphRange:gr inTextContainer:[self.textView textContainer]]; + NSRect b = [self bounds]; + CGFloat h = br.size.height; + CGFloat w = b.size.width; + CGFloat y = br.origin.y; + NSPoint containerOrigin = [self.textView textContainerOrigin]; + NSRect aRect = NSMakeRect(0, y, w, h); + // Convert from view coordinates to container coordinates + aRect = NSOffsetRect(aRect, containerOrigin.x, containerOrigin.y); + return aRect; } - [self refreshAttributes]; +-(void)scrollToCursor { + [_textView.layoutManager ensureLayoutForTextContainer:_textView.textContainer]; + + NSRect lineRect = [self highlightRectForRange:self.selectedRange]; + CGFloat maxY = [self.scrollView.contentView documentRect].size.height; + maxY = MIN(MAX(lineRect.origin.y, 0), maxY - self.scrollView.frame.size.height); + + NSPoint point = NSMakePoint(lineRect.origin.x, maxY); + if (!NSPointInRect(lineRect.origin, _scrollView.documentVisibleRect) && _scrollView.documentVisibleRect.size.width > 0) { + [self.scrollView.contentView scrollToPoint:point]; + } } - + -(void)updatePlaceholder:(BOOL)animated newSize:(NSSize)newSize { if(_placeholderAttributedString) { @@ -702,7 +1106,7 @@ -(void)updatePlaceholder:(BOOL)animated newSize:(NSSize)newSize { if(presentLayer && [_placeholder.layer animationForKey:@"opacity"]) { presentOpacity = [[presentLayer valueForKeyPath:@"opacity"] floatValue]; } - [_placeholder setHidden:NO]; + [self addSubview:_placeholder]; CABasicAnimation *oAnim = [CABasicAnimation animationWithKeyPath:@"opacity"]; oAnim.fromValue = @(presentOpacity); @@ -717,26 +1121,32 @@ -(void)updatePlaceholder:(BOOL)animated newSize:(NSSize)newSize { [_placeholder.layer addAnimation:oAnim forKey:@"opacity"]; + NSPoint toPoint = self._needShowPlaceholder ? NSMakePoint(self._startXPlaceholder, fabsf(roundf((newSize.height - NSHeight(_placeholder.frame))/2.0))) : NSMakePoint(self._endXPlaceholder, fabsf(roundf((newSize.height - NSHeight(_placeholder.frame))/2.0))); + + CABasicAnimation *pAnim = [CABasicAnimation animationWithKeyPath:@"position"]; pAnim.removedOnCompletion = YES; pAnim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; pAnim.duration = 0.2; - pAnim.fromValue = [NSValue valueWithPoint:NSMakePoint(presentX, NSMinY(_placeholder.frame))]; - pAnim.toValue = [NSValue valueWithPoint:self._needShowPlaceholder ? NSMakePoint(self._startXPlaceholder, roundf((newSize.height - NSHeight(_placeholder.frame))/2.0)) : NSMakePoint(self._endXPlaceholder, NSMinY(_placeholder.frame))]; - + pAnim.fromValue = [NSValue valueWithPoint:NSMakePoint(presentX, fabsf(roundf((newSize.height - NSHeight(_placeholder.frame))/2.0)))]; + pAnim.toValue = [NSValue valueWithPoint:toPoint]; + pAnim.delegate = self; [_placeholder.layer removeAnimationForKey:@"position"]; [_placeholder.layer addAnimation:pAnim forKey:@"position"]; - - - } else { - [_placeholder setHidden:!self._needShowPlaceholder]; + if (_placeholder.layer.animationKeys.count == 0) { + if (self._needShowPlaceholder) { + [self addSubview:_placeholder]; + } else { + [_placeholder removeFromSuperview]; + } + } } - [_placeholder setFrameOrigin:self._needShowPlaceholder ? NSMakePoint(self._startXPlaceholder, roundf((newSize.height - NSHeight(_placeholder.frame))/2.0)) : NSMakePoint(NSMinX(_placeholder.frame) + 30, roundf((newSize.height - NSHeight(_placeholder.frame))/2.0))]; + [_placeholder setFrameOrigin:self._needShowPlaceholder ? NSMakePoint(self._startXPlaceholder, fabsf(roundf((newSize.height - NSHeight(_placeholder.frame))/2.0))) : NSMakePoint(NSMinX(_placeholder.frame) + 30, fabsf(roundf((newSize.height - NSHeight(_placeholder.frame))/2.0)))]; _placeholder.layer.opacity = self._needShowPlaceholder ? 1.0 : 0.0; @@ -744,61 +1154,83 @@ -(void)updatePlaceholder:(BOOL)animated newSize:(NSSize)newSize { [self needsDisplay]; } } - + -(TGGrowingTextView *)inputView { return _textView; } - + +-(void)setMax_height:(int)max_height { + self->_max_height = max_height; + [_scrollView setFrame:NSMakeRect(0, 0, NSWidth(_scrollView.frame), MIN(NSHeight(_scrollView.frame), (CGFloat)max_height))]; +} + -(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag { - [_placeholder setHidden:!self._needShowPlaceholder]; + if (self._needShowPlaceholder) { + [self addSubview:_placeholder]; + } else { + [_placeholder removeFromSuperview]; + } } - - + +-(void)addSubview:(NSView *)view { + [super addSubview:view]; +} + +-(void)setLinkColor:(NSColor *)linkColor { + _linkColor = linkColor; +} + -(void)setFrameSize:(NSSize)newSize { [super setFrameSize:newSize]; [_scrollView setFrame:NSMakeRect(0, 0, newSize.width, newSize.height)]; [_textView setFrame:NSMakeRect(0, 0, NSWidth(_scrollView.frame), NSHeight(_textView.frame))]; - [_placeholder sizeToFit]; - [_placeholder setFrameSize:NSMakeSize(MIN(NSWidth(_textView.frame) - self._startXPlaceholder - 10,NSWidth(_placeholder.frame)), NSHeight(_placeholder.frame))]; + NSSize size = [_placeholder.attributedStringValue size]; + [_placeholder setFrameSize:NSMakeSize(MIN(NSWidth(_textView.frame) - self._startXPlaceholder, size.width + 10), size.height)]; + [_placeholder setFrameOrigin:self._needShowPlaceholder ? NSMakePoint(self._startXPlaceholder, fabsf(roundf((newSize.height - NSHeight(_placeholder.frame))/2.0))) : NSMakePoint(NSMinX(_placeholder.frame) + 30, fabsf(roundf((newSize.height - NSHeight(_placeholder.frame))/2.0)))]; } - + -(BOOL)_needShowPlaceholder { return self.string.length == 0 && _placeholderAttributedString && !_textView.hasMarkedText; } - + -(void)setPlaceholderAttributedString:(NSAttributedString *)placeholderAttributedString update:(BOOL)update { if([_placeholderAttributedString isEqualToAttributedString:placeholderAttributedString]) - return; + return; - _placeholderAttributedString = placeholderAttributedString; - [_placeholder setAttributedStringValue:_placeholderAttributedString]; - [_placeholder sizeToFit]; - [_placeholder setFrameSize:NSMakeSize(MIN(NSWidth(_textView.frame) - self._startXPlaceholder - 10,NSWidth(_placeholder.frame)), NSHeight(_placeholder.frame))]; + [_placeholder setAttributedStringValue:placeholderAttributedString]; + + _placeholderAttributedString = placeholderAttributedString; + [_placeholder setAttributedStringValue:placeholderAttributedString]; + + // [_placeholder sizeToFit]; + NSSize size = [_placeholder.attributedStringValue size]; + [_placeholder setFrameSize:NSMakeSize(MIN(NSWidth(_textView.frame) - self._startXPlaceholder - 10, size.width + 10), size.height)]; + [_placeholder setFrameOrigin:self._needShowPlaceholder ? NSMakePoint(self._startXPlaceholder, fabsf(roundf((self.frame.size.height - NSHeight(_placeholder.frame))/2.0))) : NSMakePoint(NSMinX(_placeholder.frame) + 30, fabsf(roundf((self.frame.size.height - NSHeight(_placeholder.frame))/2.0)))]; BOOL animates = _animates; _animates = NO; if (self.string.length == 0) { - [self update:update]; + [self update:update]; } _animates = animates; - + } - + -(void)setPlaceholderAttributedString:(NSAttributedString *)placeholderAttributedString { [self setPlaceholderAttributedString:placeholderAttributedString update:YES]; } - + -(NSParagraphStyle *)defaultParagraphStyle { static NSMutableParagraphStyle *para; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - para = [[NSMutableParagraphStyle alloc] init]; + para = [[NSMutableParagraphStyle alloc] init]; }); [para setLineSpacing:0]; @@ -807,8 +1239,8 @@ -(NSParagraphStyle *)defaultParagraphStyle { return para; } - - + + - (void)refreshAttributes { @try { @@ -817,12 +1249,14 @@ - (void)refreshAttributes { return; } - [_textView.textStorage addAttribute:NSForegroundColorAttributeName value:self.textColor range:NSMakeRange(0, string.length)]; + + + [self.textView.textStorage addAttribute:NSForegroundColorAttributeName value:self.textColor range:NSMakeRange(0, string.length)]; __block NSMutableArray *inputTextTags = [[NSMutableArray alloc] init]; - [string enumerateAttribute:TGMentionUidAttributeName inRange:NSMakeRange(0, string.length) options:0 usingBlock:^(__unused id value, NSRange range, __unused BOOL *stop) { + [string enumerateAttribute:TGCustomLinkAttributeName inRange:NSMakeRange(0, string.length) options:0 usingBlock:^(__unused id value, NSRange range, __unused BOOL *stop) { if ([value isKindOfClass:[TGInputTextTag class]]) { [inputTextTags addObject:[[TGInputTextTagAndRange alloc] initWithTag:value range:range]]; } @@ -840,7 +1274,7 @@ - (void)refreshAttributes { TGInputTextTagAndRange *tagAndRange = inputTextTags[i]; if ([removeTags containsObject:@(tagAndRange.tag.uniqueId)]) { [inputTextTags removeObjectAtIndex:i]; - [_textView.textStorage removeAttribute:TGMentionUidAttributeName range:tagAndRange.range]; + [self.textView.textStorage removeAttribute:TGCustomLinkAttributeName range:tagAndRange.range]; i--; } else { @@ -855,9 +1289,9 @@ - (void)refreshAttributes { if (j != (NSInteger)tagAndRange.range.location) { NSRange updatedRange = NSMakeRange(j, tagAndRange.range.location + tagAndRange.range.length - j); - [_textView.textStorage removeAttribute:TGMentionUidAttributeName range:tagAndRange.range]; + [self.textView.textStorage removeAttribute:TGCustomLinkAttributeName range:tagAndRange.range]; - [_textView.textStorage addAttribute:TGMentionUidAttributeName value:tagAndRange.tag range:updatedRange]; + [self.textView.textStorage addAttribute:TGCustomLinkAttributeName value:tagAndRange.tag range:updatedRange]; inputTextTags[i] = [[TGInputTextTagAndRange alloc] initWithTag:tagAndRange.tag range:updatedRange]; @@ -876,9 +1310,9 @@ - (void)refreshAttributes { if (j < ((NSInteger)tagAndRange.range.location)) { NSRange updatedRange = NSMakeRange(j, tagAndRange.range.location + tagAndRange.range.length - j); - [_textView.textStorage removeAttribute:TGMentionUidAttributeName range:tagAndRange.range]; + [self.textView.textStorage removeAttribute:TGCustomLinkAttributeName range:tagAndRange.range]; - [_textView.textStorage addAttribute:TGMentionUidAttributeName value:tagAndRange.tag range:updatedRange]; + [self.textView.textStorage addAttribute:TGCustomLinkAttributeName value:tagAndRange.tag range:updatedRange]; inputTextTags[i] = [[TGInputTextTagAndRange alloc] initWithTag:tagAndRange.tag range:updatedRange]; @@ -893,23 +1327,24 @@ - (void)refreshAttributes { NSInteger candidateStart = tagAndRange.range.location + tagAndRange.range.length; NSInteger candidateEnd = nextTagAndRange == nil ? string.length : nextTagAndRange.range.location; NSInteger j = candidateStart; - while (j < candidateEnd) { - unichar c = [string.string characterAtIndex:j]; - NSCharacterSet *alphanumericSet = [NSCharacterSet alphanumericCharacterSet]; - if (![alphanumericSet characterIsMember:c]) { - break; + if (candidateStart > 0 && [alphanumericSet characterIsMember:[string.string characterAtIndex:candidateStart - 1]]) { + while (j < candidateEnd) { + unichar c = [string.string characterAtIndex:j]; + NSCharacterSet *alphanumericSet = [NSCharacterSet alphanumericCharacterSet]; + if (![alphanumericSet characterIsMember:c]) { + break; + } + j++; } - j++; } - if (j == candidateStart) { [removeTags addObject:@(tagAndRange.tag.uniqueId)]; - [_textView.textStorage addAttribute:tagAndRange.tag.attribute.name value:tagAndRange.tag.attribute.value range:tagAndRange.range]; + [self.textView.textStorage addAttribute:tagAndRange.tag.attribute.name value:tagAndRange.tag.attribute.value range:tagAndRange.range]; } else { - [_textView.textStorage removeAttribute:TGMentionUidAttributeName range:tagAndRange.range]; + [self.textView.textStorage removeAttribute:TGCustomLinkAttributeName range:tagAndRange.range]; NSRange updatedRange = NSMakeRange(tagAndRange.range.location, j - tagAndRange.range.location); - [_textView.textStorage addAttribute:TGMentionUidAttributeName value:tagAndRange.tag range:updatedRange]; + [self.textView.textStorage addAttribute:TGCustomLinkAttributeName value:tagAndRange.tag range:updatedRange]; inputTextTags[i] = [[TGInputTextTagAndRange alloc] initWithTag:tagAndRange.tag range:updatedRange]; i--; @@ -929,203 +1364,284 @@ - (void)refreshAttributes { } if (j == candidateEnd) { - [_textView.textStorage removeAttribute:TGMentionUidAttributeName range:tagAndRange.range]; + [self.textView.textStorage removeAttribute:TGCustomLinkAttributeName range:tagAndRange.range]; - [_textView.textStorage removeAttribute:TGMentionUidAttributeName range:nextTagAndRange.range]; + [self.textView.textStorage removeAttribute:TGCustomLinkAttributeName range:nextTagAndRange.range]; NSRange updatedRange = NSMakeRange(tagAndRange.range.location, nextTagAndRange.range.location + nextTagAndRange.range.length - tagAndRange.range.location); - [_textView.textStorage addAttribute:TGMentionUidAttributeName value:tagAndRange.tag range:updatedRange]; + [self.textView.textStorage addAttribute:TGCustomLinkAttributeName value:tagAndRange.tag range:updatedRange]; inputTextTags[i] = [[TGInputTextTagAndRange alloc] initWithTag:tagAndRange.tag range:updatedRange]; [inputTextTags removeObjectAtIndex:i + 1]; i--; } else if (j != candidateStart) { - [_textView.textStorage removeAttribute:TGMentionUidAttributeName range:tagAndRange.range]; + [self.textView.textStorage removeAttribute:TGCustomLinkAttributeName range:tagAndRange.range]; NSRange updatedRange = NSMakeRange(tagAndRange.range.location, j - tagAndRange.range.location); - [_textView.textStorage addAttribute:TGMentionUidAttributeName value:tagAndRange.tag range:updatedRange]; + [self.textView.textStorage addAttribute:TGCustomLinkAttributeName value:tagAndRange.tag range:updatedRange]; inputTextTags[i] = [[TGInputTextTagAndRange alloc] initWithTag:tagAndRange.tag range:updatedRange]; i--; } else { [removeTags addObject:@(tagAndRange.tag.uniqueId)]; - [_textView.textStorage addAttribute:tagAndRange.tag.attribute.name value:tagAndRange.tag.attribute.value range:tagAndRange.range]; + [self.textView.textStorage addAttribute:tagAndRange.tag.attribute.name value:tagAndRange.tag.attribute.value range:tagAndRange.range]; } } } } } } - + + [self setSelectedRange:self.selectedRange]; } @catch (NSException *exception) { } + + } - + -(void)boldWord { [self.textView boldWord:nil]; } - +-(void)removeAllAttributes { + if(self.selectedRange.length == 0) { + return; + } + NSMutableAttributedString *attr = [self.textView.attributedString mutableCopy]; + [attr removeAttribute:NSFontAttributeName range:self.selectedRange]; + [attr removeAttribute:TGCustomLinkAttributeName range:self.selectedRange]; + [self.textView.textStorage setAttributedString:attr]; +} + -(void)italicWord { [self.textView italicWord:nil]; } - + -(void)codeWord { [self.textView codeWord:nil]; } - - - + + + + + -(NSString *)string { if (_textView.string == nil) { return @""; } return [_textView.string copy]; } - + -(NSAttributedString *)attributedString { return _textView.attributedString; } - + -(void)setAttributedString:(NSAttributedString *)attributedString animated:(BOOL)animated { + int limit = self.delegate == nil ? INT32_MAX : [self.delegate maxCharactersLimit: self]; - NSMutableAttributedString *attr = [[NSMutableAttributedString alloc] initWithAttributedString:attributedString]; + NSAttributedString *string = [attributedString attributedSubstringFromRange:NSMakeRange(0, MIN(limit, attributedString.string.length))]; - [attributedString enumerateAttribute:NSFontAttributeName inRange:NSMakeRange(0, attributedString.length) options:0 usingBlock:^(NSFont *value, NSRange range, BOOL * _Nonnull stop) { - + NSMutableAttributedString *attr = [[NSMutableAttributedString alloc] initWithAttributedString: string]; + + [string enumerateAttribute:NSFontAttributeName inRange:NSMakeRange(0, string.length) options:0 usingBlock:^(NSFont *value, NSRange range, BOOL * _Nonnull stop) { [attr addAttribute:NSFontAttributeName value:[[NSFontManager sharedFontManager] convertFont:value toSize:_textFont.pointSize] range:range]; - }]; + NSRange selectedRange = _textView.selectedRange; + if (selectedRange.location == self.textView.string.length) { + selectedRange = NSMakeRange(attr.length, 0); + } [_textView.textStorage setAttributedString:attr]; BOOL o = self.animates; self.animates = animated; [self update:animated]; self.animates = o; -} + + [self setSelectedRange:NSMakeRange(MIN(selectedRange.location, string.length), 0)]; --(NSString *)textWithDefault:(NSString *)string { - NSString *text = _defaultText.length > 0 ? [_defaultText stringByAppendingString:string] : string; - - return text; } - + + -(void)setString:(NSString *)string { - - if (![string isEqualToString:[self textWithDefault:self.string]]) { - [self setString:string animated:YES]; + + if (![string isEqualToString:self.string]) { + [self setString:string animated:self.animates]; } } - + -(void)setString:(NSString *)string animated:(BOOL)animated { - [_textView setString:[self textWithDefault:string]]; BOOL o = self.animates; self.animates = animated; + [_textView setString:string]; [self update:animated]; self.animates = o; } -(NSRange)selectedRange { return _textView.selectedRange; } - + -(void)insertText:(id)aString replacementRange:(NSRange)replacementRange { [_textView insertText:aString replacementRange:replacementRange]; } - + -(void)appendText:(id)aString { [_textView insertText:aString replacementRange:self.selectedRange]; } - + +- (void)addSimpleItem:(SimpleUndoItem *)item { + [self.inputView addSimpleItem:item]; + [self update: YES]; +} + -(void)addInputTextTag:(TGInputTextTag *)tag range:(NSRange)range { - [_textView.textStorage addAttribute:TGMentionUidAttributeName value:tag range:range]; + NSAttributedString *was = [self.textView.textStorage attributedSubstringFromRange:range]; + [_textView.textStorage addAttribute:TGCustomLinkAttributeName value:tag range:range]; + MarkdownUndoItem *item = [[MarkdownUndoItem alloc] initWithAttributedString:was be:[self.textView.textStorage attributedSubstringFromRange:range] inRange:range]; + [self.textView addItem:item]; } - - -- (void)replaceMention:(NSString *)mention username:(bool)username userId:(int32_t)userId -{ - NSString *replacementText = [mention stringByAppendingString:@" "]; - NSMutableAttributedString *text = _textView.attributedString == nil ? [[NSMutableAttributedString alloc] init] : [[NSMutableAttributedString alloc] initWithAttributedString:_textView.attributedString]; + static int64_t nextId = 0; - NSRange selRange = _textView.selectedRange; - NSUInteger selStartPos = selRange.location; +-(void)addLink:(NSString *)link { + if (self.selectedRange.length == 0) + return; + if (link == nil) { + NSMutableAttributedString *copy = [self.attributedString mutableCopy]; + [copy removeAttribute:TGCustomLinkAttributeName range: self.selectedRange]; + [self setAttributedString:copy animated:false]; + } else { + id tag = [[TGInputTextTag alloc] initWithUniqueId:++nextId attachment:link attribute:[[TGInputTextAttribute alloc] initWithName:NSForegroundColorAttributeName value:_linkColor]]; + [self addInputTextTag:tag range:self.selectedRange]; + [self update:YES]; + } + +} + +-(void)addLink:(NSString *)link range: (NSRange)range { + if (range.length == 0) + return; - NSInteger idx = selStartPos; - idx--; + if (link == nil) { + NSMutableAttributedString *copy = [self.attributedString mutableCopy]; + [copy removeAttribute:TGCustomLinkAttributeName range: range]; + [self setAttributedString:copy animated:false]; + } else { + id tag = [[TGInputTextTag alloc] initWithUniqueId:++nextId attachment:link attribute:[[TGInputTextAttribute alloc] initWithName:NSForegroundColorAttributeName value:_linkColor]]; + [self addInputTextTag:tag range:range]; + [self update:YES]; + } +} - NSRange candidateMentionRange = NSMakeRange(NSNotFound, 0); - if (idx >= 0 && idx < (int)text.length) +- (void)replaceMention:(NSString *)mention username:(bool)username userId:(int32_t)userId { - for (NSInteger i = idx; i >= 0; i--) + NSString *replacementText = [mention stringByAppendingString:@" "]; + + NSMutableAttributedString *text = _textView.attributedString == nil ? [[NSMutableAttributedString alloc] init] : [[NSMutableAttributedString alloc] initWithAttributedString:_textView.attributedString]; + + NSRange selRange = _textView.selectedRange; + NSUInteger selStartPos = selRange.location; + + NSInteger idx = selStartPos; + idx--; + + NSRange candidateMentionRange = NSMakeRange(NSNotFound, 0); + + if (idx >= 0 && idx < (int)text.length) { - unichar c = [text.string characterAtIndex:i]; - if (c == '@') + for (NSInteger i = idx; i >= 0; i--) { - if (i == idx) + unichar c = [text.string characterAtIndex:i]; + if (c == '@') + { + if (i == idx) candidateMentionRange = NSMakeRange(i + 1, selRange.length); - else + else candidateMentionRange = NSMakeRange(i + 1, idx - i); + break; + } + + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_')) break; } - - if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_')) - break; } - } - - if (candidateMentionRange.location != NSNotFound) - { - if (!username) { - candidateMentionRange.location -= 1; - candidateMentionRange.length += 1; - - [text replaceCharactersInRange:candidateMentionRange withString:replacementText]; + + if (candidateMentionRange.location != NSNotFound) + { + if (!username) { + candidateMentionRange.location -= 1; + candidateMentionRange.length += 1; + + [text replaceCharactersInRange:candidateMentionRange withString:replacementText]; + + nextId++; + [text addAttributes:@{TGCustomLinkAttributeName: [[TGInputTextTag alloc] initWithUniqueId:nextId attachment:@(userId) attribute:[[TGInputTextAttribute alloc] initWithName:NSForegroundColorAttributeName value:_linkColor]]} range:NSMakeRange(candidateMentionRange.location, replacementText.length - 1)]; + } else { + [text replaceCharactersInRange:candidateMentionRange withString:replacementText]; + } - static int64_t nextId = 0; - nextId++; - [text addAttributes:@{TGMentionUidAttributeName: [[TGInputTextTag alloc] initWithUniqueId:nextId attachment:@(userId) attribute:[[TGInputTextAttribute alloc] initWithName:NSForegroundColorAttributeName value:_linkColor]]} range:NSMakeRange(candidateMentionRange.location, replacementText.length - 1)]; - } else { - [text replaceCharactersInRange:candidateMentionRange withString:replacementText]; + [_textView.textStorage setAttributedString:text]; } - [_textView.textStorage setAttributedString:text]; + [self update:YES]; } - [self update:YES]; -} - -(void)paste:(id)sender { [_textView paste:sender]; } - + +-(void)rightMouseDown:(NSEvent *)event { + [super rightMouseDown:event]; +} + +-(NSMenu *)menuForEvent:(NSEvent *)event { + return [self.textView menuForEvent:event]; +} + +-(id)validRequestorForSendType:(NSPasteboardType)sendType returnType:(NSPasteboardType)returnType { + return [self.textView validRequestorForSendType:sendType returnType:returnType]; + +} + +-(BOOL)readSelectionFromPasteboard:(NSPasteboard *)pboard { + [self.delegate textViewDidPaste:pboard]; + return YES; +} + -(void)setSelectedRange:(NSRange)range { _notify_next = NO; if(range.location != NSNotFound) - [_textView setSelectedRange:range]; + [_textView setSelectedRange:range]; } - + -(Class)_textViewClass { return [TGGrowingTextView class]; } - + -(void)dealloc { [__undo removeAllActionsWithTarget:_textView]; [__undo removeAllActions]; } - - + + + -(int)_startXPlaceholder { return NSMinX(_scrollView.frame) + 4; } - + -(int)_endXPlaceholder { return self._startXPlaceholder + 30; } +-(void)setBackgroundColor:(NSColor * __nonnull)color { + self.scrollView.backgroundColor = color; + self.textView.backgroundColor = color; + _placeholder.backgroundColor = [NSColor redColor]; +} + @end diff --git a/Telegram-Mac/TabBadgeItem.swift b/Telegram-Mac/TabBadgeItem.swift index e52f0b52d5..3ba6ffaa87 100644 --- a/Telegram-Mac/TabBadgeItem.swift +++ b/Telegram-Mac/TabBadgeItem.swift @@ -8,16 +8,135 @@ import Cocoa import TGUIKit -import PostboxMac -import SwiftSignalKitMac -import TelegramCoreMac +import Postbox +import SwiftSignalKit +import TelegramCore + + +private final class AvatarTabContainer : View { + private let avatar = AvatarControl(font: .avatar(12)) + private var selected: Bool = false + private let circle: View = View() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + avatar.setFrameSize(frameRect.size) + avatar.userInteractionEnabled = false + circle.setFrameSize(frameRect.size) + circle.layer?.cornerRadius = frameRect.height / 2 + circle.layer?.borderWidth = 1.33 + circle.layer?.borderColor = theme.colors.accentIcon.cgColor + addSubview(circle) + addSubview(avatar) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + func setPeer(account: Account, peer: Peer?) { + avatar.setPeer(account: account, peer: peer) + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + circle.layer?.borderColor = theme.colors.accentIcon.cgColor + } + + + override func layout() { + super.layout() + avatar.center() + } + + func setSelected(_ selected: Bool, animated: Bool) { + self.selected = selected + + circle.change(opacity: selected ? 1 : 0, animated: animated, duration: 0.4, timingFunction: .spring) + + avatar.setFrameSize(frame.size) + + if animated { + let from: CGFloat = selected ? 1 : 24 / frame.height + let to: CGFloat = selected ? 24 / frame.height : 1 + avatar.layer?.animateScaleSpring(from: from, to: to, duration: 0.3, removeOnCompletion: false, bounce: false, completion: { completed in + + }) + if selected { + circle.layer?.animateScaleSpring(from: 0.5, to: 1.0, duration: 0.3, bounce: false) + } else { + circle.layer?.animateScaleSpring(from: 1.0, to: 0.5, duration: 0.3, removeOnCompletion: false, bounce: false) + } + } else { + if selected { + avatar.setFrameSize(NSMakeSize(24, 24)) + } else { + avatar.setFrameSize(frame.size) + } + } + + needsLayout = true + } + +} class TabBadgeItem: TabItem { - private let account:Account - init(_ account:Account, controller:ViewController, image: CGImage, selectedImage: CGImage) { - self.account = account - super.init(image: image, selectedImage: selectedImage, controller: controller, subNode:GlobalBadgeNode(account)) + private let context:AccountContext + init(_ context: AccountContext, controller:ViewController, image: CGImage, selectedImage: CGImage, longHoverHandler:((Control)->Void)? = nil) { + self.context = context + super.init(image: image, selectedImage: selectedImage, controller: controller, subNode:GlobalBadgeNode(context.account, sharedContext: context.sharedContext, dockTile: true, view: View(), removeWhenSidebar: true), longHoverHandler: longHoverHandler) + } + override func withUpdatedImages(_ image: CGImage, _ selectedImage: CGImage) -> TabItem { + return TabBadgeItem(context, controller: self.controller, image: image, selectedImage: selectedImage, longHoverHandler: self.longHoverHandler) + } +} +class TabAllBadgeItem: TabItem { + private let context:AccountContext + private let disposable = MetaDisposable() + private var peer: Peer? + init(_ context: AccountContext, image: CGImage, selectedImage: CGImage, controller:ViewController, subNode:Node? = nil, longHoverHandler:((Control)->Void)? = nil) { + self.context = context + super.init(image: image, selectedImage: selectedImage, controller: controller, subNode:GlobalBadgeNode(context.account, sharedContext: context.sharedContext, collectAllAccounts: true, view: View(), applyFilter: false), longHoverHandler: longHoverHandler) + } + deinit { + disposable.dispose() + } + + override func withUpdatedImages(_ image: CGImage, _ selectedImage: CGImage) -> TabItem { + return TabAllBadgeItem(context, image: image, selectedImage: selectedImage, controller: self.controller, subNode: self.subNode, longHoverHandler: self.longHoverHandler) + } + + override func makeView() -> NSView { + let context = self.context + + let semaphore = DispatchSemaphore(value: 0) + var isMultiple = true + _ = (context.sharedContext.activeAccounts |> take(1)).start(next: { accounts in + isMultiple = accounts.accounts.count > 1 + semaphore.signal() + }) + + semaphore.wait() + + if !isMultiple { + return super.makeView() + } + + let view = AvatarTabContainer(frame: NSMakeRect(0, 0, 30, 30)) + /* + |> distinctUntilChanged(isEqual: { lhs, rhs -> Bool in + return lhs?.smallProfileImage != rhs?.smallProfileImage + }) + */ + disposable.set((context.account.postbox.peerView(id: context.account.peerId) |> map { $0.peers[$0.peerId] } |> deliverOnMainQueue).start(next: { [weak view] peer in + view?.setPeer(account: context.account, peer: peer) + })) + + return view + } + + override func setSelected(_ selected: Bool, for view: NSView, animated: Bool) { + (view as? AvatarTabContainer)?.setSelected(selected, animated: animated) + super.setSelected(selected, for: view, animated: animated) } } diff --git a/Telegram-Mac/TableUtils.swift b/Telegram-Mac/TableUtils.swift index 0f7ec2a833..17d52f1462 100644 --- a/Telegram-Mac/TableUtils.swift +++ b/Telegram-Mac/TableUtils.swift @@ -9,7 +9,8 @@ import Cocoa import TGUIKit -import TelegramCoreMac +import TelegramCore + protocol TableItemListNodeEntry: Comparable, Identifiable { associatedtype ItemGenerationArguments @@ -17,3 +18,7 @@ protocol TableItemListNodeEntry: Comparable, Identifiable { func item(_ arguments: ItemGenerationArguments, initialSize: NSSize) -> TableRowItem } +protocol ItemListItemTag { + func isEqual(to other: ItemListItemTag) -> Bool +} + diff --git a/Telegram-Mac/Telegram-Mac-Bridging-Header.h b/Telegram-Mac/Telegram-Mac-Bridging-Header.h index b2bc9a9ca3..c318de46a8 100644 --- a/Telegram-Mac/Telegram-Mac-Bridging-Header.h +++ b/Telegram-Mac/Telegram-Mac-Bridging-Header.h @@ -16,691 +16,39 @@ #import #import #import - -//#import -//#include -//#import -//#import - - - - - -#if !__has_feature(nullability) -#define NS_ASSUME_NONNULL_BEGIN -#define NS_ASSUME_NONNULL_END -#define nullable +#import "MP4Atom.h" +#import "HackUtils.h" +#import "BuildConfig.h" +#import "TGModernGrowingTextView.h" + + +#ifndef SHARE + +#import "GZip.h" +#import "Svg.h" +#import "TGGifConverter.h" +#import "FastBlur.h" +#import "YTVimeoVideo.h" +#import "XCDYouTubeVideo.h" +#import "XCDYouTubeOperation.h" +#import "XCDYouTubeClient.h" +#import "YTVimeoExtractor.h" +#import "TGVideoCameraGLRenderer.h" +#import "TGVideoCameraMovieRecorder.h" +#import "EmojiSuggestionBridge.h" +#import "TGCurrencyFormatter.h" #endif -void telegramFastBlur(int imageWidth, int imageHeight, int imageStride, void * __nullable pixels); -NSArray * __nonnull cut_long_message(NSString * __nonnull message, int max_length); -int64_t SystemIdleTime(void); -NSDictionary * __nonnull audioTags(AVURLAsset * __nonnull asset); -NSImage * __nonnull TGIdenticonImage(NSData * __nonnull data, NSData * __nonnull additionalData, CGSize size); - -CGImageRef __nullable convertFromWebP(NSData *__nonnull data); - -@interface ObjcUtils : NSObject -+ (NSArray * __nullable)textCheckingResultsForText:(NSString *__nonnull)text highlightMentionsAndTags:(bool)highlightMentionsAndTags highlightCommands:(bool)highlightCommands; -+(NSString * __nonnull) md5:(NSString *__nonnull)string; -+(NSArray *__nonnull)findElementsByClass:(NSString *__nonnull)className inView:(NSView *__nonnull)view; -+(NSString * __nonnull)stringForEmojiHashOfData:(NSData *__nonnull)data count:(NSInteger)count positionExtractor:(int32_t (^__nonnull)(uint8_t *__nonnull, int32_t, int32_t))positionExtractor; -+(NSArray *)bufferList:(CMSampleBufferRef)sampleBuffer; -+(NSString * __nonnull)callEmojies:(NSData *__nonnull)keySha256; -+ (NSArray * __nonnull)getEmojiFromString:(NSString * __nonnull)string; -+(NSOpenPanel * __nonnull)openPanel; -+(NSSavePanel * __nonnull)savePanel; -+(NSEvent * __nonnull)scrollEvent:(NSEvent *__nonnull)from; -+(NSSize)gifDimensionSize:(NSString * __nonnull)path; -+(int)colorMask:(int)idValue mainId:(int)mainId; -+(NSArray * __nonnull)notificationTones:(NSString * __nonnull)def; -+(NSString * __nullable)youtubeIdentifier:(NSString * __nonnull)url;; -@end - -int colorIndexForGroupId(int64_t groupId); -int64_t TGPeerIdFromChannelId(int32_t channelId); -int colorIndexForUid(int32_t uid, int32_t myUserId); - -@interface NSData (TG) -- (NSString *__nonnull)stringByEncodingInHex; -@end - - -@interface NSFileManager (Extension) -+ (NSString * __nonnull)xattrStringValueForKey:(NSString *__nonnull)key atURL:(NSURL *__nonnull)URL; -+ (BOOL)setXAttrStringValue:(NSString *__nonnull)value forKey:(NSString *__nonnull)key atURL:(NSURL *__nonnull)URL; -@end - -@interface NSMutableAttributedString(Extension) --(void)detectBoldColorInStringWithFont:(NSFont *__nonnull)font; -@end - -@interface CalendarUtils : NSObject - -+ (BOOL) isSameDate:(NSDate*__nonnull)d1 date:(NSDate* __nonnull)d2 checkDay:(BOOL)checkDay; -+ (NSString*__nonnull) dd:(NSDate*__nonnull)d; -+ (NSInteger) colForDay:(NSInteger)day; -+ (NSInteger) lastDayOfTheMonth:(NSDate *__nonnull)date; -+ (NSDate*__nonnull) toUTC:(NSDate*__nonnull)d; -+ (NSDate*__nonnull) monthDay:(NSInteger)day date:(NSDate *__nonnull)date; -+ (NSInteger)weekDay:(NSDate *__nonnull)date; -+ (NSDate *__nonnull) stepMonth:(NSInteger)dm date:(NSDate *__nonnull)date; - -@end - -extern NSString *__nonnull const TGMentionUidAttributeName; - - -@interface TGInputTextAttribute : NSObject -@property (nonatomic,strong,readonly) NSString * __nonnull name; -@property (nonatomic,strong,readonly) id __nonnull value; --(id __nonnull )initWithName:(NSString * __nonnull)name value:(id __nonnull)value; -@end - -@interface TGInputTextTag : NSTextAttachment - -@property (nonatomic, readonly) int64_t uniqueId; -@property (nonatomic, strong, readonly) id __nonnull attachment; - -@property (nonatomic,strong, readonly) TGInputTextAttribute * __nonnull attribute; - --(instancetype __nonnull )initWithUniqueId:(int64_t)uniqueId attachment:(id __nonnull )attachment attribute:(TGInputTextAttribute * __nonnull )attribute; - -@end - -@interface TGInputTextTagAndRange : NSObject - -@property (nonatomic, strong, readonly) TGInputTextTag *__nonnull tag; -@property (nonatomic) NSRange range; - -- (instancetype __nonnull )initWithTag:(TGInputTextTag * __nonnull )tag range:(NSRange)range; - -@end - -@protocol TGModernGrowingDelegate - --(void) textViewHeightChanged:(CGFloat)height animated:(BOOL)animated; --(BOOL) textViewEnterPressed:(NSEvent * __nonnull)event; --(void) textViewTextDidChange:(NSString * __nonnull)string; --(void) textViewTextDidChangeSelectedRange:(NSRange)range; --(BOOL)textViewDidPaste:(NSPasteboard * __nonnull)pasteboard; --(NSSize)textViewSize; --(BOOL)textViewIsTypingEnabled; --(int)maxCharactersLimit; - -@optional -- (void) textViewNeedClose:(id __nonnull)textView; -- (BOOL) canTransformInputText; -@end - - - - -@interface TGGrowingTextView : NSTextView -@property (nonatomic,weak) id __nullable weakd; -@end - -@interface TGModernGrowingTextView : NSView - -@property (nonatomic,assign) BOOL animates; -@property (nonatomic,assign) int min_height; -@property (nonatomic,assign) int max_height; - -@property (nonatomic,assign) BOOL isSingleLine; -@property (nonatomic,assign) BOOL isWhitespaceDisabled; -@property (nonatomic,strong) NSColor * __nonnull cursorColor; -@property (nonatomic,strong) NSColor * __nonnull textColor; -@property (nonatomic,strong) NSColor * __nonnull linkColor; -@property (nonatomic,strong) NSFont * __nonnull textFont; -@property (nonatomic,strong,readonly) TGGrowingTextView * __nonnull inputView; -@property (nonatomic,strong) NSString * __nonnull defaultText; - -@property (nonatomic,strong, nullable) NSAttributedString *placeholderAttributedString; - --(void)setPlaceholderAttributedString:(NSAttributedString * __nonnull)placeholderAttributedString update:(BOOL)update; - -@property (nonatomic,weak) id __nullable delegate; - --(int)height; - - --(void)update:(BOOL)notify; - --(NSAttributedString * __nonnull)attributedString; --(void)setAttributedString:(NSAttributedString * __nonnull)attributedString animated:(BOOL)animated; --(NSString * __nonnull)string; --(void)setString:(NSString * __nonnull)string animated:(BOOL)animated; --(void)setString:(NSString * __nonnull)string; --(NSRange)selectedRange; --(void)appendText:(id __nonnull)aString; --(void)insertText:(id __nonnull)aString replacementRange:(NSRange)replacementRange; --(void)addInputTextTag:(TGInputTextTag * __nonnull)tag range:(NSRange)range; - --(void)replaceMention:(NSString * __nonnull)mention username:(bool)username userId:(int32_t)userId; - --(void)paste:(id __nonnull)sender; - --(void)setSelectedRange:(NSRange)range; - --(Class __nonnull)_textViewClass; --(int)_startXPlaceholder; --(BOOL)_needShowPlaceholder; - --(void)codeWord; --(void)italicWord; --(void)boldWord; -- (void)textDidChange:( NSNotification * _Nullable )notification; -@end - - -@interface NSWeakReference : NSObject - -@property (nonatomic, weak) id __nullable value; - -- (instancetype __nonnull)initWithValue:(id __nonnull)value; - -@end - -@interface OpusObjcBridge : NSObject - -@end - -@protocol OpusBridgeDelegate -- (void)audioPlayerDidFinishPlaying:(OpusObjcBridge * __nonnull)audioPlayer; -- (void)audioPlayerDidStartPlaying:(OpusObjcBridge * __nonnull)audioPlayer; -- (void)audioPlayerDidPause:(OpusObjcBridge * __nonnull)audioPlayer; -@end - -@interface OpusObjcBridge () - -@property (nonatomic, weak) id __nullable delegate; - -+ (bool)canPlayFile:(NSString * __nonnull)path; -+ (NSTimeInterval)durationFile:(NSString * __nonnull)path; -- (instancetype __nonnull)initWithPath:(NSString * __nonnull)path; -- (void)play; -- (void)playFromPosition:(NSTimeInterval)position; -- (void)pause; -- (void)stop; -- (void)reset; -- (NSTimeInterval)currentPositionSync:(bool)sync; -- (NSTimeInterval)duration; --(void)setCurrentPosition:(NSTimeInterval)position; -- (BOOL)isPaused; -- (BOOL)isEqualToPath:(NSString * __nonnull)path; -@end - - -//BEGIN AUDIO HEADER -@interface TGDataItem : NSObject - -- (instancetype __nonnull)initWithTempFile; -- (instancetype __nonnull)initWithFilePath:(NSString * __nonnull)filePath; - -- (void)moveToPath:(NSString * __nonnull)path; -- (void)remove; - -- (void)appendData:(NSData * __nonnull)data; -- (NSData * __nonnull)readDataAtOffset:(NSUInteger)offset length:(NSUInteger)length; -- (NSUInteger)length; - -- (NSString * __nonnull)path; - -@end - -@interface TGAudioWaveform : NSObject - -@property (nonatomic, strong, readonly) NSData * __nonnull samples; -@property (nonatomic, readonly) int32_t peak; - -- (instancetype __nonnull)initWithSamples:(NSData * __nonnull)samples peak:(int32_t)peak; -- (instancetype __nonnull)initWithBitstream:(NSData * __nonnull)bitstream bitsPerSample:(NSUInteger)bitsPerSample; - -- (NSData * __nonnull)bitstream; -- (uint16_t * __nonnull)sampleList; -@end - -@interface TGOpusAudioRecorder : NSObject - -@property (nonatomic, copy) void (^__nullable pauseRecording)(); -@property (nonatomic, copy) void (^__nullable micLevel)(CGFloat); - -- (instancetype __nonnull)initWithFileEncryption:(bool)fileEncryption; - -- (void)_beginAudioSession:(bool)speaker; -- (void)prepareRecord:(bool)playTone completion:(void (^__nonnull)())completion; -- (void)record; -- (TGDataItem * __nonnull)stopRecording:(NSTimeInterval * __nonnull)recordedDuration waveform:(__autoreleasing TGAudioWaveform * __nullable* __nullable)waveform; -- (NSTimeInterval)currentDuration; - -@end - -double mappingRange(double x, double in_min, double in_max, double out_min, double out_max); - - -@interface TGOggOpusWriter : NSObject - -- (bool)beginWithDataItem:(TGDataItem * __nonnull)dataItem; -- (bool)writeFrame:(uint8_t * __nullable)framePcmBytes frameByteCount:(NSUInteger)frameByteCount; -- (NSUInteger)encodedBytes; -- (NSTimeInterval)encodedDuration; - -@end - - -@interface DateUtils : NSObject - -+ (NSString * __nonnull)stringForShortTime:(int)time; -+ (NSString * __nonnull)stringForDialogTime:(int)time; -+ (NSString * __nonnull)stringForDayOfMonth:(int)date dayOfMonth:(int * __nonnull)dayOfMonth; -+ (NSString * __nonnull)stringForDayOfWeek:(int)date; -+ (NSString * __nonnull)stringForMessageListDate:(int)date; -+ (NSString * __nonnull)stringForLastSeen:(int)date; -+ (NSString * __nonnull)stringForLastSeenShort:(int)date; -+ (NSString * __nonnull)stringForRelativeLastSeen:(int)date; -+ (NSString * __nonnull)stringForUntil:(int)date; -+ (NSString * __nonnull)stringForDayOfMonthFull:(int)date dayOfMonth:(int * __nonnull)dayOfMonth; - -+ (void)setDateLocalizationFunc:(NSString* __nonnull (^__nonnull)(NSString * __nonnull key))localizationF; -@end - - - -NS_ASSUME_NONNULL_BEGIN -typedef NS_ENUM(NSUInteger, YTVimeoVideoThumbnailQuality) { - YTVimeoVideoThumbnailQualitySmall = 640, - YTVimeoVideoThumbnailQualityMedium = 960, - YTVimeoVideoThumbnailQualityHD = 1280, -}; - -typedef NS_ENUM(NSUInteger, YTVimeoVideoQuality) { - YTVimeoVideoQualityLow270 = 270, - YTVimeoVideoQualityMedium360 = 360, - YTVimeoVideoQualityMedium480 = 480, - YTVimeoVideoQualityMedium540 = 540, - YTVimeoVideoQualityHD720 = 720, - YTVimeoVideoQualityHD1080 = 1080, -}; - - - -@interface YTVimeoVideo : NSObject - -@property (nonatomic, readonly) NSString *identifier; - -@property (nonatomic, readonly) NSString *title; - -@property (nonatomic, readonly) NSTimeInterval duration; - - -#if __has_feature(objc_generics) -@property (nonatomic, readonly) NSDictionary *streamURLs; -#else -@property (nonatomic, readonly) NSDictionary *streamURLs; -#endif - - -#if __has_feature(objc_generics) -@property (nonatomic, readonly) NSDictionary *__nullable thumbnailURLs; -#else -@property (nonatomic, readonly) NSDictionary *thumbnailURLs; -#endif - - -@property (nonatomic, readonly) NSDictionary *metaData; - --(NSURL *)highestQualityStreamURL; - --(NSURL *)lowestQualityStreamURL; - -@property (nonatomic, readonly, nullable) NSURL *HTTPLiveStreamURL; - -@end - -@interface YTVimeoExtractor : NSObject - -+(instancetype)sharedExtractor; - --(void)fetchVideoWithIdentifier:(NSString *)videoIdentifier withReferer:(NSString *__nullable)referer completionHandler:(void (^)(YTVimeoVideo * __nullable video, NSError * __nullable error))completionHandler; - --(void)fetchVideoWithVimeoURL:(NSString *)videoURL withReferer:(NSString *__nullable)referer completionHandler:(void (^)(YTVimeoVideo * __nullable video, NSError * __nullable error))completionHandler; - -@end - -typedef NS_ENUM(NSUInteger, XCDYouTubeVideoQuality) { - XCDYouTubeVideoQualitySmall240 = 36, - XCDYouTubeVideoQualityMedium360 = 18, - XCDYouTubeVideoQualityHD720 = 22, - XCDYouTubeVideoQualityHD1080 DEPRECATED_MSG_ATTRIBUTE("YouTube has removed 1080p mp4 videos.") = 37, -}; - -extern NSString *const XCDYouTubeVideoQualityHTTPLiveStreaming; - -@interface XCDYouTubeVideo : NSObject - - -@property (nonatomic, readonly) NSString *identifier; -@property (nonatomic, readonly) NSString *title; -@property (nonatomic, readonly) NSTimeInterval duration; -@property (nonatomic, readonly, nullable) NSURL *smallThumbnailURL; -@property (nonatomic, readonly, nullable) NSURL *mediumThumbnailURL; -@property (nonatomic, readonly, nullable) NSURL *largeThumbnailURL; -@property (nonatomic, readonly) NSDictionary *streamURLs; -@property (nonatomic, readonly, nullable) NSDate *expirationDate; - -@end - -@protocol XCDYouTubeOperation - -- (void) cancel; - -@end - -@interface XCDYouTubeClient : NSObject -+ (instancetype) defaultClient; -- (instancetype) initWithLanguageIdentifier:(nullable NSString *)languageIdentifier; -@property (nonatomic, readonly) NSString *languageIdentifier; -- (id) getVideoWithIdentifier:(nullable NSString *)videoIdentifier completionHandler:(void (^)(XCDYouTubeVideo * __nullable video, NSError * __nullable error))completionHandler; - -@end - - -// -// SSKeychain.h -// SSToolkit -// -// Created by Sam Soffes on 5/19/10. -// Copyright (c) 2009-2011 Sam Soffes. All rights reserved. -// - - -/** Error codes that can be returned in NSError objects. */ -typedef enum { - SSKeychainErrorNone = noErr, - SSKeychainErrorBadArguments = -1001, - SSKeychainErrorNoPassword = -1002, - SSKeychainErrorInvalidParameter = errSecParam, - SSKeychainErrorFailedToAllocated = errSecAllocate, - SSKeychainErrorNotAvailable = errSecNotAvailable, - SSKeychainErrorAuthorizationFailed = errSecAuthFailed, - SSKeychainErrorDuplicatedItem = errSecDuplicateItem, - SSKeychainErrorNotFound = errSecItemNotFound, - SSKeychainErrorInteractionNotAllowed = errSecInteractionNotAllowed, - SSKeychainErrorFailedToDecode = errSecDecode -} SSKeychainErrorCode; - -extern NSString *const kSSKeychainErrorDomain; -extern NSString *const kSSKeychainAccountKey; -extern NSString *const kSSKeychainCreatedAtKey; -extern NSString *const kSSKeychainClassKey; -extern NSString *const kSSKeychainDescriptionKey; -extern NSString *const kSSKeychainLabelKey; -extern NSString *const kSSKeychainLastModifiedKey; -extern NSString *const kSSKeychainWhereKey; - -@interface SSKeychain : NSObject - -+ (NSArray *)allAccounts; -+ (NSArray *)allAccounts:(NSError **)error; -+ (NSArray *)accountsForService:(NSString *)serviceName; -+ (NSArray *)accountsForService:(NSString *)serviceName error:(NSError **)error; -+ (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account; -+ (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error; -+ (NSData * __nullable)passwordDataForService:(NSString *)serviceName account:(NSString *)account; -+ (NSData * __nullable)passwordDataForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error; -+ (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account; -+ (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error; -+ (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account; -+ (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error; -+ (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account; -+ (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error; - -@end - - -@interface SPMediaKeyTap : NSObject -+ (NSArray*)defaultMediaKeyUserBundleIdentifiers; - --(id)initWithDelegate:(id)delegate; - -+(BOOL)usesGlobalMediaKeyTap; --(void)startWatchingMediaKeys; --(void)stopWatchingMediaKeys; --(void)handleAndReleaseMediaKeyEvent:(NSEvent *)event; -@end - -@interface NSObject (SPMediaKeyTapDelegate) --(void)mediaKeyTap:(SPMediaKeyTap*)keyTap receivedMediaKeyEvent:(NSEvent*)event; -@end - -@interface TimeObserver : NSObject - -void test_start_group(NSString * timeGroup); -void test_step_group(NSString *group); -void test_release_group(NSString *group); - -@end - -BOOL isEnterAccessObjc(NSEvent *theEvent, BOOL byCmdEnter); -BOOL isEnterEventObjc(NSEvent *theEvent); - - -@interface TGGifConverter : NSObject -+ (void)convertGifToMp4:(NSData *)data exportPath:(NSString *)exportPath completionHandler:(void (^)(NSString *path))completionHandler errorHandler:(dispatch_block_t)errorHandler cancelHandler:(BOOL (^)())cancelHandler; - -+(NSSize)gifDimensionSize:(NSString *)path; -@end - - - -@interface TGCallConnectionDescription : NSObject - - @property (nonatomic, readonly) int64_t identifier; - @property (nonatomic, strong, readonly) NSString *ipv4; - @property (nonatomic, strong, readonly) NSString *ipv6; - @property (nonatomic, readonly) int32_t port; - @property (nonatomic, strong, readonly) NSData *peerTag; - -- (instancetype)initWithIdentifier:(int64_t)identifier ipv4:(NSString *)ipv4 ipv6:(NSString *)ipv6 port:(int32_t)port peerTag:(NSData *)peerTag; - -@end - - -@interface TGCallConnection : NSObject - - @property (nonatomic, strong, readonly) NSData *key; - @property (nonatomic, strong, readonly) NSData *keyHash; - @property (nonatomic, strong, readonly) TGCallConnectionDescription *defaultConnection; - @property (nonatomic, strong, readonly) NSArray *alternativeConnections; - -- (instancetype)initWithKey:(NSData *)key keyHash:(NSData *)keyHash defaultConnection:(TGCallConnectionDescription *)defaultConnection alternativeConnections:(NSArray *)alternativeConnections; - -@end - -@interface AudioDevice : NSObject -@property(nonatomic, strong, readonly) NSString *deviceId; -@property(nonatomic, strong, readonly) NSString *deviceName; --(id)initWithDeviceId:(NSString*)deviceId deviceName:(NSString *)deviceName; -@end - -@interface CallBridge : NSObject --(void)startTransmissionIfNeeded:(bool)outgoing connection:(TGCallConnection *)connection; - --(void)mute; --(void)unmute; --(BOOL)isMuted; - --(NSString *)currentOutputDeviceId; --(NSString *)currentInputDeviceId; --(NSArray *)outputDevices; --(NSArray *)inputDevices; --(void)setCurrentOutputDeviceId:(NSString *)deviceId; --(void)setCurrentInputDeviceId:(NSString *)deviceId; - -@property (nonatomic, copy) void (^stateChangeHandler)(int); - -@end - -@interface TGCurrencyFormatterEntry : NSObject - -@property (nonatomic, strong, readonly) NSString *symbol; -@property (nonatomic, strong, readonly) NSString *thousandsSeparator; -@property (nonatomic, strong, readonly) NSString *decimalSeparator; -@property (nonatomic, readonly) bool symbolOnLeft; -@property (nonatomic, readonly) bool spaceBetweenAmountAndSymbol; -@property (nonatomic, readonly) int decimalDigits; - -@end - -@interface TGCurrencyFormatter : NSObject - -+ (TGCurrencyFormatter *)shared; - -- (NSString *)formatAmount:(int64_t)amount currency:(NSString *)currency; - -@end - - -typedef NS_ENUM(int32_t, NumberPluralizationForm) { - NumberPluralizationFormZero, - NumberPluralizationFormOne, - NumberPluralizationFormTwo, - NumberPluralizationFormFew, - NumberPluralizationFormMany, - NumberPluralizationFormOther -}; - -NumberPluralizationForm numberPluralizationForm(unsigned int lc, int n); -unsigned int languageCodehash(NSString *code); -NS_ASSUME_NONNULL_END - - -@interface CEmojiSuggestion : NSObject -@property(nonatomic, strong) NSString * __nonnull emoji; -@property(nonatomic, strong) NSString * __nonnull label; -@property(nonatomic, strong) NSString * __nonnull replacement; -@end - -@interface EmojiSuggestionBridge : NSObject -+(NSArray * __nonnull)getSuggestions:(NSString * __nonnull)q; -@end - -typedef enum { - MIHSliderTransitionFade, - MIHSliderTransitionPushVertical, - MIHSliderTransitionPushHorizontalFromLeft, - MIHSliderTransitionPushHorizontalFromRight -} MIHSliderTransition; - -@class MIHSliderDotsControl; - -@interface MIHSliderView : NSView - -@property (retain, readonly) NSArray * __nonnull slides; - -- (void)addSlide:(NSView * __nonnull)aSlide; -- (void)removeSlide:(NSView * __nonnull)aSlide; -@property (assign, readonly) NSUInteger indexOfDisplayedSlide; -@property (retain, readonly) NSView * __nonnull displayedSlide; -- (void)displaySlideAtIndex:(NSUInteger)aIndex; -@property (assign) MIHSliderTransition transitionStyle; -@property (assign) BOOL scheduledTransition; -@property (assign) BOOL repeatingScheduledTransition; -@property (assign) NSTimeInterval scheduledTransitionInterval; -@property (assign) NSTimeInterval transitionAnimationDuration; - -@property (retain) MIHSliderDotsControl * __nonnull dotsControl; - -@end - -@interface MIHSliderDotsControl : NSView - -@property (retain) NSImage * __nullable normalDotImage; - -@property (retain) NSImage * __nullable highlightedDotImage; - -@end - -@interface TGVideoCameraGLRenderer : NSObject - -@property (nonatomic, readonly) __attribute__((NSObject)) CMFormatDescriptionRef outputFormatDescription; -@property (nonatomic, assign) AVCaptureVideoOrientation orientation; -@property (nonatomic, assign) bool mirror; -@property (nonatomic, assign) CGFloat opacity; -@property (nonatomic, readonly) bool hasPreviousPixelbuffer; - -- (void)prepareForInputWithFormatDescription:(CMFormatDescriptionRef)inputFormatDescription outputRetainedBufferCountHint:(size_t)outputRetainedBufferCountHint; -- (void)reset; - -- (CVPixelBufferRef)copyRenderedPixelBuffer:(CVPixelBufferRef)pixelBuffer; -- (void)setPreviousPixelBuffer:(CVPixelBufferRef)previousPixelBuffer; - -@end - -@interface TGPaintShader : NSObject - -@property (nonatomic, readonly) GLuint program; -@property (nonatomic, readonly) NSDictionary *uniforms; - -- (instancetype)initWithVertexShader:(NSString *)vertexShader fragmentShader:(NSString *)fragmentShader attributes:(NSArray *)attributes uniforms:(NSArray *)uniforms; - -- (GLuint)uniformForKey:(NSString *)key; - -- (void)cleanResources; - -@end - - -@protocol TGVideoCameraMovieRecorderDelegate; - -@interface TGVideoCameraMovieRecorder : NSObject - -@property (nonatomic, assign) bool paused; - -- (instancetype __nonnull)initWithURL:(NSURL *)URL delegate:(id)delegate callbackQueue:(dispatch_queue_t)queue; - -- (void)addVideoTrackWithSourceFormatDescription:(CMFormatDescriptionRef)formatDescription transform:(CGAffineTransform)transform settings:(NSDictionary *)videoSettings; -- (void)addAudioTrackWithSourceFormatDescription:(CMFormatDescriptionRef)formatDescription settings:(NSDictionary *)audioSettings; - - -- (void)prepareToRecord; - -- (void)appendVideoPixelBuffer:(CVPixelBufferRef)pixelBuffer withPresentationTime:(CMTime)presentationTime; -- (void)appendAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer; - -- (void)finishRecording; - -- (NSTimeInterval)videoDuration; - -@end - -@protocol TGVideoCameraMovieRecorderDelegate -@required -- (void)movieRecorderDidFinishPreparing:(TGVideoCameraMovieRecorder *)recorder; -- (void)movieRecorder:(TGVideoCameraMovieRecorder *)recorder didFailWithError:(NSError *)error; -- (void)movieRecorderDidFinishRecording:(TGVideoCameraMovieRecorder *)recorder; -@end - -typedef enum -{ - TGMediaVideoConversionPresetCompressedDefault, - TGMediaVideoConversionPresetCompressedVeryLow, - TGMediaVideoConversionPresetCompressedLow, - TGMediaVideoConversionPresetCompressedMedium, - TGMediaVideoConversionPresetCompressedHigh, - TGMediaVideoConversionPresetCompressedVeryHigh, - TGMediaVideoConversionPresetAnimation, - TGMediaVideoConversionPresetVideoMessage -} TGMediaVideoConversionPreset; - - - -@interface TGMediaVideoConversionPresetSettings : NSObject +#import "OngoingCallThreadLocalContext.h" -+ (CGSize)maximumSizeForPreset:(TGMediaVideoConversionPreset)preset; -+ (NSDictionary *)videoSettingsForPreset:(TGMediaVideoConversionPreset)preset dimensions:(CGSize)dimensions; -+ (NSDictionary *)audioSettingsForPreset:(TGMediaVideoConversionPreset)preset; -@end +#import "CalendarUtils.h" +#import "RingBuffer.h" +#import "ocr.h" +#import "TGPassportMRZ.h" +#import "EDSunriseSet.h" +#import "ObjcUtils.h" +#import "DateUtils.h" +#import "NumberPluralizationForm.h" #endif /* Telegram_Mac_Bridging_Header_h */ diff --git a/Telegram-Mac/Telegram-Mac.entitlements b/Telegram-Mac/Telegram-Mac.entitlements index 0fe126a453..543aaa4b87 100644 --- a/Telegram-Mac/Telegram-Mac.entitlements +++ b/Telegram-Mac/Telegram-Mac.entitlements @@ -3,22 +3,17 @@ com.apple.security.app-sandbox - + com.apple.security.application-groups - $(TeamIdentifierPrefix)$(PRODUCT_BUNDLE_IDENTIFIER) + 6N38VWS5BX.ru.keepcoder.Telegram + 6N38VWS5BX.ru.keepcoder.Telegram.TelegramShare - com.apple.security.device.camera - - com.apple.security.device.microphone + com.apple.security.device.audio-input - com.apple.security.files.downloads.read-write + com.apple.security.cs.disable-library-validation - com.apple.security.files.user-selected.read-write - - com.apple.security.network.client - - com.apple.security.network.server + com.apple.security.device.camera diff --git a/Telegram-Mac/Telegram-Sandbox.entitlements b/Telegram-Mac/Telegram-Sandbox.entitlements new file mode 100644 index 0000000000..db1406ea99 --- /dev/null +++ b/Telegram-Mac/Telegram-Sandbox.entitlements @@ -0,0 +1,38 @@ + + + + + com.apple.security.cs.disable-library-validation + + com.apple.developer.maps + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + 6N38VWS5BX.ru.keepcoder.Telegram + 6N38VWS5BX.ru.keepcoder.Telegram.TelegramShare + + com.apple.security.device.audio-input + + com.apple.security.device.camera + + com.apple.security.device.microphone + + com.apple.security.files.downloads.read-write + + com.apple.security.files.user-selected.read-write + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.personal-information.location + + keychain-access-groups + + 6N38VWS5BX.ru.keepcoder.Telegram + 6N38VWS5BX.ru.keepcoder.TelegramShare + + + diff --git a/Telegram-Mac/TelegramAccountAuxiliaryMethods.swift b/Telegram-Mac/TelegramAccountAuxiliaryMethods.swift index 4f5998c68b..abbe5cfd54 100644 --- a/Telegram-Mac/TelegramAccountAuxiliaryMethods.swift +++ b/Telegram-Mac/TelegramAccountAuxiliaryMethods.swift @@ -7,27 +7,25 @@ // import Foundation -import TelegramCoreMac -import PostboxMac +import TelegramCore -public let telegramAccountAuxiliaryMethods = AccountAuxiliaryMethods(updatePeerChatInputState: { interfaceState, inputState -> PeerChatInterfaceState? in - if interfaceState == nil { - return ChatInterfaceState().withUpdatedSynchronizeableInputState(inputState) - } else if let interfaceState = interfaceState as? ChatInterfaceState { - return interfaceState.withUpdatedSynchronizeableInputState(inputState) - } else { - return interfaceState - } -}, fetchResource: { account, resource, range, tag in - +import Postbox + +public let telegramAccountAuxiliaryMethods = AccountAuxiliaryMethods(fetchResource: { account, resource, range, tag in if let resource = resource as? LocalFileGifMediaResource { return fetchGifMediaResource(resource: resource) + } else if let resource = resource as? LocalFileArchiveMediaResource { + return fetchArchiveMediaResource(account: account, resource: resource) + } else if let resource = resource as? ExternalMusicAlbumArtResource { + return fetchExternalMusicAlbumArtResource(account: account, resource: resource) + } else if let resource = resource as? LocalFileVideoMediaResource { + return fetchMovMediaResource(resource: resource) + } else if let resource = resource as? LottieSoundMediaResource { + return fetchLottieSoundData(resource: resource) } - -// if let resource = resource as? VideoLibraryMediaResource { -// return fetchVideoLibraryMediaResource(resource: resource) -// } else if let resource = resource as? LocalFileVideoMediaResource { -// return fetchLocalFileVideoMediaResource(resource: resource) -// } + return nil +}, fetchResourceMediaReferenceHash: { resource in + return .single(nil) +}, prepareSecretThumbnailData: { resource in return nil }) diff --git a/Telegram-Mac/TelegramApplicationContext.swift b/Telegram-Mac/TelegramApplicationContext.swift deleted file mode 100644 index 99a222fe2e..0000000000 --- a/Telegram-Mac/TelegramApplicationContext.swift +++ /dev/null @@ -1,119 +0,0 @@ -// -// TelegramApplicationContext.swift -// TelegramMac -// -// Created by keepcoder on 28/11/2016. -// Copyright © 2016 Telegram. All rights reserved. -// - -import Cocoa -import TGUIKit -import SwiftSignalKitMac -import TelegramCoreMac -import PostboxMac - -public var isDebug = false - -class TelegramApplicationContext : NSObject { - var layout:SplitViewState = .none - let layoutHandler:ValuePromise = ValuePromise(ignoreRepeated:true) - private(set) var mediaKeyTap:SPMediaKeyTap? - let entertainment:EntertainmentViewController - private var _recentlyPeerUsed:[PeerId] = [] - let cachedAdminIds: CachedAdminIds = CachedAdminIds() - private(set) var timeDifference:TimeInterval = 0 - private(set) var recentlyPeerUsed:[PeerId] { - set { - _recentlyPeerUsed = newValue - } - get { - if _recentlyPeerUsed.count > 2 { - return Array(_recentlyPeerUsed.prefix(through: 2)) - } else { - return _recentlyPeerUsed - } - } - } - - var globalSearch:((String)->Void)? - - private let logoutDisposable = MetaDisposable() - - weak var mainNavigation:NavigationViewController? - private let updateDifferenceDisposable = MetaDisposable() - init(_ mainNavigation:NavigationViewController?, _ entertainment:EntertainmentViewController, network: Network) { - self.mainNavigation = mainNavigation - self.entertainment = entertainment - timeDifference = network.globalTime - Date().timeIntervalSince1970 - super.init() - - - _ = layoutHandler.get().start(next: { [weak self] (state) in - self?.layout = state - }) - - updateDifferenceDisposable.set((Signal.single(Void()) - |> delay(5 * 60, queue: Queue.mainQueue()) |> restart).start(next: { [weak self, weak network] in - if let network = network { - self?.timeDifference = network.globalTime - Date().timeIntervalSince1970 - } - })) - - } - - func showCallHeader(with session:PCallSession) { - mainNavigation?.callHeader?.show(true) - if let view = mainNavigation?.callHeader?.view as? CallNavigationHeaderView { - view.update(with: session) - } - } - - func checkFirstRecentlyForDuplicate(peerId:PeerId) { - if let index = recentlyPeerUsed.index(of: peerId), index == 0 { - recentlyPeerUsed.remove(at: index) - } - } - - func addRecentlyUsedPeer(peerId:PeerId) { - if let index = recentlyPeerUsed.index(of: peerId) { - recentlyPeerUsed.remove(at: index) - } - recentlyPeerUsed.insert(peerId, at: 0) - if recentlyPeerUsed.count > 4 { - recentlyPeerUsed = Array(recentlyPeerUsed.prefix(through: 4)) - } - } - - deinit { - updateDifferenceDisposable.dispose() - } - - - func deinitMediaKeyTap() { - mediaKeyTap?.stopWatchingMediaKeys() - mediaKeyTap = nil - } - - func initMediaKeyTap() { - mediaKeyTap = SPMediaKeyTap(delegate: self) - } - - override func mediaKeyTap(_ keyTap: SPMediaKeyTap, receivedMediaKeyEvent event: NSEvent) { - let keyCode: Int32 = (Int32((event.data1 & 0xffff0000) >> 16)) - let keyFlags: Int = (event.data1 & 0x0000ffff) - let keyIsPressed: Bool = ((keyFlags & 0xff00) >> 8) == 0xa - if keyIsPressed { - switch keyCode { - case NX_KEYTYPE_PLAY: - globalAudio?.playOrPause() - case NX_KEYTYPE_FAST: - globalAudio?.next() - case NX_KEYTYPE_REWIND: - globalAudio?.prev() - default: - break - } - } - } - -} diff --git a/Telegram-Mac/TelegramIconsTheme.swift b/Telegram-Mac/TelegramIconsTheme.swift new file mode 100644 index 0000000000..39912a603c --- /dev/null +++ b/Telegram-Mac/TelegramIconsTheme.swift @@ -0,0 +1,10028 @@ +import SwiftSignalKit + +final class TelegramIconsTheme { + private var cached:Atomic<[String: CGImage]> = Atomic(value: [:]) + private var cachedWithInset:Atomic<[String: (CGImage, NSEdgeInsets)]> = Atomic(value: [:]) + + var dialogMuteImage: CGImage { + if let image = cached.with({ $0["dialogMuteImage"] }) { + return image + } else { + let image = _dialogMuteImage() + _ = cached.modify { current in + var current = current + current["dialogMuteImage"] = image + return current + } + return image + } + } + var dialogMuteImageSelected: CGImage { + if let image = cached.with({ $0["dialogMuteImageSelected"] }) { + return image + } else { + let image = _dialogMuteImageSelected() + _ = cached.modify { current in + var current = current + current["dialogMuteImageSelected"] = image + return current + } + return image + } + } + var outgoingMessageImage: CGImage { + if let image = cached.with({ $0["outgoingMessageImage"] }) { + return image + } else { + let image = _outgoingMessageImage() + _ = cached.modify { current in + var current = current + current["outgoingMessageImage"] = image + return current + } + return image + } + } + var readMessageImage: CGImage { + if let image = cached.with({ $0["readMessageImage"] }) { + return image + } else { + let image = _readMessageImage() + _ = cached.modify { current in + var current = current + current["readMessageImage"] = image + return current + } + return image + } + } + var outgoingMessageImageSelected: CGImage { + if let image = cached.with({ $0["outgoingMessageImageSelected"] }) { + return image + } else { + let image = _outgoingMessageImageSelected() + _ = cached.modify { current in + var current = current + current["outgoingMessageImageSelected"] = image + return current + } + return image + } + } + var readMessageImageSelected: CGImage { + if let image = cached.with({ $0["readMessageImageSelected"] }) { + return image + } else { + let image = _readMessageImageSelected() + _ = cached.modify { current in + var current = current + current["readMessageImageSelected"] = image + return current + } + return image + } + } + var sendingImage: CGImage { + if let image = cached.with({ $0["sendingImage"] }) { + return image + } else { + let image = _sendingImage() + _ = cached.modify { current in + var current = current + current["sendingImage"] = image + return current + } + return image + } + } + var sendingImageSelected: CGImage { + if let image = cached.with({ $0["sendingImageSelected"] }) { + return image + } else { + let image = _sendingImageSelected() + _ = cached.modify { current in + var current = current + current["sendingImageSelected"] = image + return current + } + return image + } + } + var secretImage: CGImage { + if let image = cached.with({ $0["secretImage"] }) { + return image + } else { + let image = _secretImage() + _ = cached.modify { current in + var current = current + current["secretImage"] = image + return current + } + return image + } + } + var secretImageSelected: CGImage { + if let image = cached.with({ $0["secretImageSelected"] }) { + return image + } else { + let image = _secretImageSelected() + _ = cached.modify { current in + var current = current + current["secretImageSelected"] = image + return current + } + return image + } + } + var pinnedImage: CGImage { + if let image = cached.with({ $0["pinnedImage"] }) { + return image + } else { + let image = _pinnedImage() + _ = cached.modify { current in + var current = current + current["pinnedImage"] = image + return current + } + return image + } + } + var pinnedImageSelected: CGImage { + if let image = cached.with({ $0["pinnedImageSelected"] }) { + return image + } else { + let image = _pinnedImageSelected() + _ = cached.modify { current in + var current = current + current["pinnedImageSelected"] = image + return current + } + return image + } + } + var verifiedImage: CGImage { + if let image = cached.with({ $0["verifiedImage"] }) { + return image + } else { + let image = _verifiedImage() + _ = cached.modify { current in + var current = current + current["verifiedImage"] = image + return current + } + return image + } + } + var verifiedImageSelected: CGImage { + if let image = cached.with({ $0["verifiedImageSelected"] }) { + return image + } else { + let image = _verifiedImageSelected() + _ = cached.modify { current in + var current = current + current["verifiedImageSelected"] = image + return current + } + return image + } + } + var errorImage: CGImage { + if let image = cached.with({ $0["errorImage"] }) { + return image + } else { + let image = _errorImage() + _ = cached.modify { current in + var current = current + current["errorImage"] = image + return current + } + return image + } + } + var errorImageSelected: CGImage { + if let image = cached.with({ $0["errorImageSelected"] }) { + return image + } else { + let image = _errorImageSelected() + _ = cached.modify { current in + var current = current + current["errorImageSelected"] = image + return current + } + return image + } + } + var chatSearch: CGImage { + if let image = cached.with({ $0["chatSearch"] }) { + return image + } else { + let image = _chatSearch() + _ = cached.modify { current in + var current = current + current["chatSearch"] = image + return current + } + return image + } + } + var chatSearchActive: CGImage { + if let image = cached.with({ $0["chatSearchActive"] }) { + return image + } else { + let image = _chatSearchActive() + _ = cached.modify { current in + var current = current + current["chatSearchActive"] = image + return current + } + return image + } + } + var chatCall: CGImage { + if let image = cached.with({ $0["chatCall"] }) { + return image + } else { + let image = _chatCall() + _ = cached.modify { current in + var current = current + current["chatCall"] = image + return current + } + return image + } + } + var chatCallActive: CGImage { + if let image = cached.with({ $0["chatCallActive"] }) { + return image + } else { + let image = _chatCallActive() + _ = cached.modify { current in + var current = current + current["chatCallActive"] = image + return current + } + return image + } + } + var chatActions: CGImage { + if let image = cached.with({ $0["chatActions"] }) { + return image + } else { + let image = _chatActions() + _ = cached.modify { current in + var current = current + current["chatActions"] = image + return current + } + return image + } + } + var chatFailedCall_incoming: CGImage { + if let image = cached.with({ $0["chatFailedCall_incoming"] }) { + return image + } else { + let image = _chatFailedCall_incoming() + _ = cached.modify { current in + var current = current + current["chatFailedCall_incoming"] = image + return current + } + return image + } + } + var chatFailedCall_outgoing: CGImage { + if let image = cached.with({ $0["chatFailedCall_outgoing"] }) { + return image + } else { + let image = _chatFailedCall_outgoing() + _ = cached.modify { current in + var current = current + current["chatFailedCall_outgoing"] = image + return current + } + return image + } + } + var chatCall_incoming: CGImage { + if let image = cached.with({ $0["chatCall_incoming"] }) { + return image + } else { + let image = _chatCall_incoming() + _ = cached.modify { current in + var current = current + current["chatCall_incoming"] = image + return current + } + return image + } + } + var chatCall_outgoing: CGImage { + if let image = cached.with({ $0["chatCall_outgoing"] }) { + return image + } else { + let image = _chatCall_outgoing() + _ = cached.modify { current in + var current = current + current["chatCall_outgoing"] = image + return current + } + return image + } + } + var chatFailedCallBubble_incoming: CGImage { + if let image = cached.with({ $0["chatFailedCallBubble_incoming"] }) { + return image + } else { + let image = _chatFailedCallBubble_incoming() + _ = cached.modify { current in + var current = current + current["chatFailedCallBubble_incoming"] = image + return current + } + return image + } + } + var chatFailedCallBubble_outgoing: CGImage { + if let image = cached.with({ $0["chatFailedCallBubble_outgoing"] }) { + return image + } else { + let image = _chatFailedCallBubble_outgoing() + _ = cached.modify { current in + var current = current + current["chatFailedCallBubble_outgoing"] = image + return current + } + return image + } + } + var chatCallBubble_incoming: CGImage { + if let image = cached.with({ $0["chatCallBubble_incoming"] }) { + return image + } else { + let image = _chatCallBubble_incoming() + _ = cached.modify { current in + var current = current + current["chatCallBubble_incoming"] = image + return current + } + return image + } + } + var chatCallBubble_outgoing: CGImage { + if let image = cached.with({ $0["chatCallBubble_outgoing"] }) { + return image + } else { + let image = _chatCallBubble_outgoing() + _ = cached.modify { current in + var current = current + current["chatCallBubble_outgoing"] = image + return current + } + return image + } + } + var chatFallbackCall: CGImage { + if let image = cached.with({ $0["chatFallbackCall"] }) { + return image + } else { + let image = _chatFallbackCall() + _ = cached.modify { current in + var current = current + current["chatFallbackCall"] = image + return current + } + return image + } + } + var chatFallbackCallBubble_incoming: CGImage { + if let image = cached.with({ $0["chatFallbackCallBubble_incoming"] }) { + return image + } else { + let image = _chatFallbackCallBubble_incoming() + _ = cached.modify { current in + var current = current + current["chatFallbackCallBubble_incoming"] = image + return current + } + return image + } + } + var chatFallbackCallBubble_outgoing: CGImage { + if let image = cached.with({ $0["chatFallbackCallBubble_outgoing"] }) { + return image + } else { + let image = _chatFallbackCallBubble_outgoing() + _ = cached.modify { current in + var current = current + current["chatFallbackCallBubble_outgoing"] = image + return current + } + return image + } + } + var chatFallbackVideoCall: CGImage { + if let image = cached.with({ $0["chatFallbackVideoCall"] }) { + return image + } else { + let image = _chatFallbackVideoCall() + _ = cached.modify { current in + var current = current + current["chatFallbackVideoCall"] = image + return current + } + return image + } + } + var chatFallbackVideoCallBubble_incoming: CGImage { + if let image = cached.with({ $0["chatFallbackVideoCallBubble_incoming"] }) { + return image + } else { + let image = _chatFallbackVideoCallBubble_incoming() + _ = cached.modify { current in + var current = current + current["chatFallbackVideoCallBubble_incoming"] = image + return current + } + return image + } + } + var chatFallbackVideoCallBubble_outgoing: CGImage { + if let image = cached.with({ $0["chatFallbackVideoCallBubble_outgoing"] }) { + return image + } else { + let image = _chatFallbackVideoCallBubble_outgoing() + _ = cached.modify { current in + var current = current + current["chatFallbackVideoCallBubble_outgoing"] = image + return current + } + return image + } + } + var chatToggleSelected: CGImage { + if let image = cached.with({ $0["chatToggleSelected"] }) { + return image + } else { + let image = _chatToggleSelected() + _ = cached.modify { current in + var current = current + current["chatToggleSelected"] = image + return current + } + return image + } + } + var chatToggleUnselected: CGImage { + if let image = cached.with({ $0["chatToggleUnselected"] }) { + return image + } else { + let image = _chatToggleUnselected() + _ = cached.modify { current in + var current = current + current["chatToggleUnselected"] = image + return current + } + return image + } + } + var chatMusicPlay: CGImage { + if let image = cached.with({ $0["chatMusicPlay"] }) { + return image + } else { + let image = _chatMusicPlay() + _ = cached.modify { current in + var current = current + current["chatMusicPlay"] = image + return current + } + return image + } + } + var chatMusicPlayBubble_incoming: CGImage { + if let image = cached.with({ $0["chatMusicPlayBubble_incoming"] }) { + return image + } else { + let image = _chatMusicPlayBubble_incoming() + _ = cached.modify { current in + var current = current + current["chatMusicPlayBubble_incoming"] = image + return current + } + return image + } + } + var chatMusicPlayBubble_outgoing: CGImage { + if let image = cached.with({ $0["chatMusicPlayBubble_outgoing"] }) { + return image + } else { + let image = _chatMusicPlayBubble_outgoing() + _ = cached.modify { current in + var current = current + current["chatMusicPlayBubble_outgoing"] = image + return current + } + return image + } + } + var chatMusicPause: CGImage { + if let image = cached.with({ $0["chatMusicPause"] }) { + return image + } else { + let image = _chatMusicPause() + _ = cached.modify { current in + var current = current + current["chatMusicPause"] = image + return current + } + return image + } + } + var chatMusicPauseBubble_incoming: CGImage { + if let image = cached.with({ $0["chatMusicPauseBubble_incoming"] }) { + return image + } else { + let image = _chatMusicPauseBubble_incoming() + _ = cached.modify { current in + var current = current + current["chatMusicPauseBubble_incoming"] = image + return current + } + return image + } + } + var chatMusicPauseBubble_outgoing: CGImage { + if let image = cached.with({ $0["chatMusicPauseBubble_outgoing"] }) { + return image + } else { + let image = _chatMusicPauseBubble_outgoing() + _ = cached.modify { current in + var current = current + current["chatMusicPauseBubble_outgoing"] = image + return current + } + return image + } + } + var chatGradientBubble_incoming: CGImage { + if let image = cached.with({ $0["chatGradientBubble_incoming"] }) { + return image + } else { + let image = _chatGradientBubble_incoming() + _ = cached.modify { current in + var current = current + current["chatGradientBubble_incoming"] = image + return current + } + return image + } + } + var chatGradientBubble_outgoing: CGImage { + if let image = cached.with({ $0["chatGradientBubble_outgoing"] }) { + return image + } else { + let image = _chatGradientBubble_outgoing() + _ = cached.modify { current in + var current = current + current["chatGradientBubble_outgoing"] = image + return current + } + return image + } + } + var chatBubble_none_incoming_withInset: (CGImage, NSEdgeInsets) { + if let image = cachedWithInset.with({ $0["chatBubble_none_incoming_withInset"] }) { + return image + } else { + let image = _chatBubble_none_incoming_withInset() + _ = cachedWithInset.modify { current in + var current = current + current["chatBubble_none_incoming_withInset"] = image + return current + } + return image + } + } + var chatBubble_none_outgoing_withInset: (CGImage, NSEdgeInsets) { + if let image = cachedWithInset.with({ $0["chatBubble_none_outgoing_withInset"] }) { + return image + } else { + let image = _chatBubble_none_outgoing_withInset() + _ = cachedWithInset.modify { current in + var current = current + current["chatBubble_none_outgoing_withInset"] = image + return current + } + return image + } + } + var chatBubbleBorder_none_incoming_withInset: (CGImage, NSEdgeInsets) { + if let image = cachedWithInset.with({ $0["chatBubbleBorder_none_incoming_withInset"] }) { + return image + } else { + let image = _chatBubbleBorder_none_incoming_withInset() + _ = cachedWithInset.modify { current in + var current = current + current["chatBubbleBorder_none_incoming_withInset"] = image + return current + } + return image + } + } + var chatBubbleBorder_none_outgoing_withInset: (CGImage, NSEdgeInsets) { + if let image = cachedWithInset.with({ $0["chatBubbleBorder_none_outgoing_withInset"] }) { + return image + } else { + let image = _chatBubbleBorder_none_outgoing_withInset() + _ = cachedWithInset.modify { current in + var current = current + current["chatBubbleBorder_none_outgoing_withInset"] = image + return current + } + return image + } + } + var chatBubble_both_incoming_withInset: (CGImage, NSEdgeInsets) { + if let image = cachedWithInset.with({ $0["chatBubble_both_incoming_withInset"] }) { + return image + } else { + let image = _chatBubble_both_incoming_withInset() + _ = cachedWithInset.modify { current in + var current = current + current["chatBubble_both_incoming_withInset"] = image + return current + } + return image + } + } + var chatBubble_both_outgoing_withInset: (CGImage, NSEdgeInsets) { + if let image = cachedWithInset.with({ $0["chatBubble_both_outgoing_withInset"] }) { + return image + } else { + let image = _chatBubble_both_outgoing_withInset() + _ = cachedWithInset.modify { current in + var current = current + current["chatBubble_both_outgoing_withInset"] = image + return current + } + return image + } + } + var chatBubbleBorder_both_incoming_withInset: (CGImage, NSEdgeInsets) { + if let image = cachedWithInset.with({ $0["chatBubbleBorder_both_incoming_withInset"] }) { + return image + } else { + let image = _chatBubbleBorder_both_incoming_withInset() + _ = cachedWithInset.modify { current in + var current = current + current["chatBubbleBorder_both_incoming_withInset"] = image + return current + } + return image + } + } + var chatBubbleBorder_both_outgoing_withInset: (CGImage, NSEdgeInsets) { + if let image = cachedWithInset.with({ $0["chatBubbleBorder_both_outgoing_withInset"] }) { + return image + } else { + let image = _chatBubbleBorder_both_outgoing_withInset() + _ = cachedWithInset.modify { current in + var current = current + current["chatBubbleBorder_both_outgoing_withInset"] = image + return current + } + return image + } + } + var composeNewChat: CGImage { + if let image = cached.with({ $0["composeNewChat"] }) { + return image + } else { + let image = _composeNewChat() + _ = cached.modify { current in + var current = current + current["composeNewChat"] = image + return current + } + return image + } + } + var composeNewChatActive: CGImage { + if let image = cached.with({ $0["composeNewChatActive"] }) { + return image + } else { + let image = _composeNewChatActive() + _ = cached.modify { current in + var current = current + current["composeNewChatActive"] = image + return current + } + return image + } + } + var composeNewGroup: CGImage { + if let image = cached.with({ $0["composeNewGroup"] }) { + return image + } else { + let image = _composeNewGroup() + _ = cached.modify { current in + var current = current + current["composeNewGroup"] = image + return current + } + return image + } + } + var composeNewSecretChat: CGImage { + if let image = cached.with({ $0["composeNewSecretChat"] }) { + return image + } else { + let image = _composeNewSecretChat() + _ = cached.modify { current in + var current = current + current["composeNewSecretChat"] = image + return current + } + return image + } + } + var composeNewChannel: CGImage { + if let image = cached.with({ $0["composeNewChannel"] }) { + return image + } else { + let image = _composeNewChannel() + _ = cached.modify { current in + var current = current + current["composeNewChannel"] = image + return current + } + return image + } + } + var contactsNewContact: CGImage { + if let image = cached.with({ $0["contactsNewContact"] }) { + return image + } else { + let image = _contactsNewContact() + _ = cached.modify { current in + var current = current + current["contactsNewContact"] = image + return current + } + return image + } + } + var chatReadMarkInBubble1_incoming: CGImage { + if let image = cached.with({ $0["chatReadMarkInBubble1_incoming"] }) { + return image + } else { + let image = _chatReadMarkInBubble1_incoming() + _ = cached.modify { current in + var current = current + current["chatReadMarkInBubble1_incoming"] = image + return current + } + return image + } + } + var chatReadMarkInBubble2_incoming: CGImage { + if let image = cached.with({ $0["chatReadMarkInBubble2_incoming"] }) { + return image + } else { + let image = _chatReadMarkInBubble2_incoming() + _ = cached.modify { current in + var current = current + current["chatReadMarkInBubble2_incoming"] = image + return current + } + return image + } + } + var chatReadMarkInBubble1_outgoing: CGImage { + if let image = cached.with({ $0["chatReadMarkInBubble1_outgoing"] }) { + return image + } else { + let image = _chatReadMarkInBubble1_outgoing() + _ = cached.modify { current in + var current = current + current["chatReadMarkInBubble1_outgoing"] = image + return current + } + return image + } + } + var chatReadMarkInBubble2_outgoing: CGImage { + if let image = cached.with({ $0["chatReadMarkInBubble2_outgoing"] }) { + return image + } else { + let image = _chatReadMarkInBubble2_outgoing() + _ = cached.modify { current in + var current = current + current["chatReadMarkInBubble2_outgoing"] = image + return current + } + return image + } + } + var chatReadMarkOutBubble1: CGImage { + if let image = cached.with({ $0["chatReadMarkOutBubble1"] }) { + return image + } else { + let image = _chatReadMarkOutBubble1() + _ = cached.modify { current in + var current = current + current["chatReadMarkOutBubble1"] = image + return current + } + return image + } + } + var chatReadMarkOutBubble2: CGImage { + if let image = cached.with({ $0["chatReadMarkOutBubble2"] }) { + return image + } else { + let image = _chatReadMarkOutBubble2() + _ = cached.modify { current in + var current = current + current["chatReadMarkOutBubble2"] = image + return current + } + return image + } + } + var chatReadMarkOverlayBubble1: CGImage { + if let image = cached.with({ $0["chatReadMarkOverlayBubble1"] }) { + return image + } else { + let image = _chatReadMarkOverlayBubble1() + _ = cached.modify { current in + var current = current + current["chatReadMarkOverlayBubble1"] = image + return current + } + return image + } + } + var chatReadMarkOverlayBubble2: CGImage { + if let image = cached.with({ $0["chatReadMarkOverlayBubble2"] }) { + return image + } else { + let image = _chatReadMarkOverlayBubble2() + _ = cached.modify { current in + var current = current + current["chatReadMarkOverlayBubble2"] = image + return current + } + return image + } + } + var sentFailed: CGImage { + if let image = cached.with({ $0["sentFailed"] }) { + return image + } else { + let image = _sentFailed() + _ = cached.modify { current in + var current = current + current["sentFailed"] = image + return current + } + return image + } + } + var chatChannelViewsInBubble_incoming: CGImage { + if let image = cached.with({ $0["chatChannelViewsInBubble_incoming"] }) { + return image + } else { + let image = _chatChannelViewsInBubble_incoming() + _ = cached.modify { current in + var current = current + current["chatChannelViewsInBubble_incoming"] = image + return current + } + return image + } + } + var chatChannelViewsInBubble_outgoing: CGImage { + if let image = cached.with({ $0["chatChannelViewsInBubble_outgoing"] }) { + return image + } else { + let image = _chatChannelViewsInBubble_outgoing() + _ = cached.modify { current in + var current = current + current["chatChannelViewsInBubble_outgoing"] = image + return current + } + return image + } + } + var chatChannelViewsOutBubble: CGImage { + if let image = cached.with({ $0["chatChannelViewsOutBubble"] }) { + return image + } else { + let image = _chatChannelViewsOutBubble() + _ = cached.modify { current in + var current = current + current["chatChannelViewsOutBubble"] = image + return current + } + return image + } + } + var chatChannelViewsOverlayBubble: CGImage { + if let image = cached.with({ $0["chatChannelViewsOverlayBubble"] }) { + return image + } else { + let image = _chatChannelViewsOverlayBubble() + _ = cached.modify { current in + var current = current + current["chatChannelViewsOverlayBubble"] = image + return current + } + return image + } + } + var chatNavigationBack: CGImage { + if let image = cached.with({ $0["chatNavigationBack"] }) { + return image + } else { + let image = _chatNavigationBack() + _ = cached.modify { current in + var current = current + current["chatNavigationBack"] = image + return current + } + return image + } + } + var peerInfoAddMember: CGImage { + if let image = cached.with({ $0["peerInfoAddMember"] }) { + return image + } else { + let image = _peerInfoAddMember() + _ = cached.modify { current in + var current = current + current["peerInfoAddMember"] = image + return current + } + return image + } + } + var chatSearchUp: CGImage { + if let image = cached.with({ $0["chatSearchUp"] }) { + return image + } else { + let image = _chatSearchUp() + _ = cached.modify { current in + var current = current + current["chatSearchUp"] = image + return current + } + return image + } + } + var chatSearchUpDisabled: CGImage { + if let image = cached.with({ $0["chatSearchUpDisabled"] }) { + return image + } else { + let image = _chatSearchUpDisabled() + _ = cached.modify { current in + var current = current + current["chatSearchUpDisabled"] = image + return current + } + return image + } + } + var chatSearchDown: CGImage { + if let image = cached.with({ $0["chatSearchDown"] }) { + return image + } else { + let image = _chatSearchDown() + _ = cached.modify { current in + var current = current + current["chatSearchDown"] = image + return current + } + return image + } + } + var chatSearchDownDisabled: CGImage { + if let image = cached.with({ $0["chatSearchDownDisabled"] }) { + return image + } else { + let image = _chatSearchDownDisabled() + _ = cached.modify { current in + var current = current + current["chatSearchDownDisabled"] = image + return current + } + return image + } + } + var chatSearchCalendar: CGImage { + if let image = cached.with({ $0["chatSearchCalendar"] }) { + return image + } else { + let image = _chatSearchCalendar() + _ = cached.modify { current in + var current = current + current["chatSearchCalendar"] = image + return current + } + return image + } + } + var dismissAccessory: CGImage { + if let image = cached.with({ $0["dismissAccessory"] }) { + return image + } else { + let image = _dismissAccessory() + _ = cached.modify { current in + var current = current + current["dismissAccessory"] = image + return current + } + return image + } + } + var chatScrollUp: CGImage { + if let image = cached.with({ $0["chatScrollUp"] }) { + return image + } else { + let image = _chatScrollUp() + _ = cached.modify { current in + var current = current + current["chatScrollUp"] = image + return current + } + return image + } + } + var chatScrollUpActive: CGImage { + if let image = cached.with({ $0["chatScrollUpActive"] }) { + return image + } else { + let image = _chatScrollUpActive() + _ = cached.modify { current in + var current = current + current["chatScrollUpActive"] = image + return current + } + return image + } + } + var chatSendMessage: CGImage { + if let image = cached.with({ $0["chatSendMessage"] }) { + return image + } else { + let image = _chatSendMessage() + _ = cached.modify { current in + var current = current + current["chatSendMessage"] = image + return current + } + return image + } + } + var chatSaveEditedMessage: CGImage { + if let image = cached.with({ $0["chatSaveEditedMessage"] }) { + return image + } else { + let image = _chatSaveEditedMessage() + _ = cached.modify { current in + var current = current + current["chatSaveEditedMessage"] = image + return current + } + return image + } + } + var chatRecordVoice: CGImage { + if let image = cached.with({ $0["chatRecordVoice"] }) { + return image + } else { + let image = _chatRecordVoice() + _ = cached.modify { current in + var current = current + current["chatRecordVoice"] = image + return current + } + return image + } + } + var chatEntertainment: CGImage { + if let image = cached.with({ $0["chatEntertainment"] }) { + return image + } else { + let image = _chatEntertainment() + _ = cached.modify { current in + var current = current + current["chatEntertainment"] = image + return current + } + return image + } + } + var chatInlineDismiss: CGImage { + if let image = cached.with({ $0["chatInlineDismiss"] }) { + return image + } else { + let image = _chatInlineDismiss() + _ = cached.modify { current in + var current = current + current["chatInlineDismiss"] = image + return current + } + return image + } + } + var chatActiveReplyMarkup: CGImage { + if let image = cached.with({ $0["chatActiveReplyMarkup"] }) { + return image + } else { + let image = _chatActiveReplyMarkup() + _ = cached.modify { current in + var current = current + current["chatActiveReplyMarkup"] = image + return current + } + return image + } + } + var chatDisabledReplyMarkup: CGImage { + if let image = cached.with({ $0["chatDisabledReplyMarkup"] }) { + return image + } else { + let image = _chatDisabledReplyMarkup() + _ = cached.modify { current in + var current = current + current["chatDisabledReplyMarkup"] = image + return current + } + return image + } + } + var chatSecretTimer: CGImage { + if let image = cached.with({ $0["chatSecretTimer"] }) { + return image + } else { + let image = _chatSecretTimer() + _ = cached.modify { current in + var current = current + current["chatSecretTimer"] = image + return current + } + return image + } + } + var chatForwardMessagesActive: CGImage { + if let image = cached.with({ $0["chatForwardMessagesActive"] }) { + return image + } else { + let image = _chatForwardMessagesActive() + _ = cached.modify { current in + var current = current + current["chatForwardMessagesActive"] = image + return current + } + return image + } + } + var chatForwardMessagesInactive: CGImage { + if let image = cached.with({ $0["chatForwardMessagesInactive"] }) { + return image + } else { + let image = _chatForwardMessagesInactive() + _ = cached.modify { current in + var current = current + current["chatForwardMessagesInactive"] = image + return current + } + return image + } + } + var chatDeleteMessagesActive: CGImage { + if let image = cached.with({ $0["chatDeleteMessagesActive"] }) { + return image + } else { + let image = _chatDeleteMessagesActive() + _ = cached.modify { current in + var current = current + current["chatDeleteMessagesActive"] = image + return current + } + return image + } + } + var chatDeleteMessagesInactive: CGImage { + if let image = cached.with({ $0["chatDeleteMessagesInactive"] }) { + return image + } else { + let image = _chatDeleteMessagesInactive() + _ = cached.modify { current in + var current = current + current["chatDeleteMessagesInactive"] = image + return current + } + return image + } + } + var generalNext: CGImage { + if let image = cached.with({ $0["generalNext"] }) { + return image + } else { + let image = _generalNext() + _ = cached.modify { current in + var current = current + current["generalNext"] = image + return current + } + return image + } + } + var generalNextActive: CGImage { + if let image = cached.with({ $0["generalNextActive"] }) { + return image + } else { + let image = _generalNextActive() + _ = cached.modify { current in + var current = current + current["generalNextActive"] = image + return current + } + return image + } + } + var generalSelect: CGImage { + if let image = cached.with({ $0["generalSelect"] }) { + return image + } else { + let image = _generalSelect() + _ = cached.modify { current in + var current = current + current["generalSelect"] = image + return current + } + return image + } + } + var chatVoiceRecording: CGImage { + if let image = cached.with({ $0["chatVoiceRecording"] }) { + return image + } else { + let image = _chatVoiceRecording() + _ = cached.modify { current in + var current = current + current["chatVoiceRecording"] = image + return current + } + return image + } + } + var chatVideoRecording: CGImage { + if let image = cached.with({ $0["chatVideoRecording"] }) { + return image + } else { + let image = _chatVideoRecording() + _ = cached.modify { current in + var current = current + current["chatVideoRecording"] = image + return current + } + return image + } + } + var chatRecord: CGImage { + if let image = cached.with({ $0["chatRecord"] }) { + return image + } else { + let image = _chatRecord() + _ = cached.modify { current in + var current = current + current["chatRecord"] = image + return current + } + return image + } + } + var deleteItem: CGImage { + if let image = cached.with({ $0["deleteItem"] }) { + return image + } else { + let image = _deleteItem() + _ = cached.modify { current in + var current = current + current["deleteItem"] = image + return current + } + return image + } + } + var deleteItemDisabled: CGImage { + if let image = cached.with({ $0["deleteItemDisabled"] }) { + return image + } else { + let image = _deleteItemDisabled() + _ = cached.modify { current in + var current = current + current["deleteItemDisabled"] = image + return current + } + return image + } + } + var chatAttach: CGImage { + if let image = cached.with({ $0["chatAttach"] }) { + return image + } else { + let image = _chatAttach() + _ = cached.modify { current in + var current = current + current["chatAttach"] = image + return current + } + return image + } + } + var chatAttachFile: CGImage { + if let image = cached.with({ $0["chatAttachFile"] }) { + return image + } else { + let image = _chatAttachFile() + _ = cached.modify { current in + var current = current + current["chatAttachFile"] = image + return current + } + return image + } + } + var chatAttachPhoto: CGImage { + if let image = cached.with({ $0["chatAttachPhoto"] }) { + return image + } else { + let image = _chatAttachPhoto() + _ = cached.modify { current in + var current = current + current["chatAttachPhoto"] = image + return current + } + return image + } + } + var chatAttachCamera: CGImage { + if let image = cached.with({ $0["chatAttachCamera"] }) { + return image + } else { + let image = _chatAttachCamera() + _ = cached.modify { current in + var current = current + current["chatAttachCamera"] = image + return current + } + return image + } + } + var chatAttachLocation: CGImage { + if let image = cached.with({ $0["chatAttachLocation"] }) { + return image + } else { + let image = _chatAttachLocation() + _ = cached.modify { current in + var current = current + current["chatAttachLocation"] = image + return current + } + return image + } + } + var chatAttachPoll: CGImage { + if let image = cached.with({ $0["chatAttachPoll"] }) { + return image + } else { + let image = _chatAttachPoll() + _ = cached.modify { current in + var current = current + current["chatAttachPoll"] = image + return current + } + return image + } + } + var mediaEmptyShared: CGImage { + if let image = cached.with({ $0["mediaEmptyShared"] }) { + return image + } else { + let image = _mediaEmptyShared() + _ = cached.modify { current in + var current = current + current["mediaEmptyShared"] = image + return current + } + return image + } + } + var mediaEmptyFiles: CGImage { + if let image = cached.with({ $0["mediaEmptyFiles"] }) { + return image + } else { + let image = _mediaEmptyFiles() + _ = cached.modify { current in + var current = current + current["mediaEmptyFiles"] = image + return current + } + return image + } + } + var mediaEmptyMusic: CGImage { + if let image = cached.with({ $0["mediaEmptyMusic"] }) { + return image + } else { + let image = _mediaEmptyMusic() + _ = cached.modify { current in + var current = current + current["mediaEmptyMusic"] = image + return current + } + return image + } + } + var mediaEmptyLinks: CGImage { + if let image = cached.with({ $0["mediaEmptyLinks"] }) { + return image + } else { + let image = _mediaEmptyLinks() + _ = cached.modify { current in + var current = current + current["mediaEmptyLinks"] = image + return current + } + return image + } + } + var stickersAddFeatured: CGImage { + if let image = cached.with({ $0["stickersAddFeatured"] }) { + return image + } else { + let image = _stickersAddFeatured() + _ = cached.modify { current in + var current = current + current["stickersAddFeatured"] = image + return current + } + return image + } + } + var stickersAddedFeatured: CGImage { + if let image = cached.with({ $0["stickersAddedFeatured"] }) { + return image + } else { + let image = _stickersAddedFeatured() + _ = cached.modify { current in + var current = current + current["stickersAddedFeatured"] = image + return current + } + return image + } + } + var stickersRemove: CGImage { + if let image = cached.with({ $0["stickersRemove"] }) { + return image + } else { + let image = _stickersRemove() + _ = cached.modify { current in + var current = current + current["stickersRemove"] = image + return current + } + return image + } + } + var peerMediaDownloadFileStart: CGImage { + if let image = cached.with({ $0["peerMediaDownloadFileStart"] }) { + return image + } else { + let image = _peerMediaDownloadFileStart() + _ = cached.modify { current in + var current = current + current["peerMediaDownloadFileStart"] = image + return current + } + return image + } + } + var peerMediaDownloadFilePause: CGImage { + if let image = cached.with({ $0["peerMediaDownloadFilePause"] }) { + return image + } else { + let image = _peerMediaDownloadFilePause() + _ = cached.modify { current in + var current = current + current["peerMediaDownloadFilePause"] = image + return current + } + return image + } + } + var stickersShare: CGImage { + if let image = cached.with({ $0["stickersShare"] }) { + return image + } else { + let image = _stickersShare() + _ = cached.modify { current in + var current = current + current["stickersShare"] = image + return current + } + return image + } + } + var emojiRecentTab: CGImage { + if let image = cached.with({ $0["emojiRecentTab"] }) { + return image + } else { + let image = _emojiRecentTab() + _ = cached.modify { current in + var current = current + current["emojiRecentTab"] = image + return current + } + return image + } + } + var emojiSmileTab: CGImage { + if let image = cached.with({ $0["emojiSmileTab"] }) { + return image + } else { + let image = _emojiSmileTab() + _ = cached.modify { current in + var current = current + current["emojiSmileTab"] = image + return current + } + return image + } + } + var emojiNatureTab: CGImage { + if let image = cached.with({ $0["emojiNatureTab"] }) { + return image + } else { + let image = _emojiNatureTab() + _ = cached.modify { current in + var current = current + current["emojiNatureTab"] = image + return current + } + return image + } + } + var emojiFoodTab: CGImage { + if let image = cached.with({ $0["emojiFoodTab"] }) { + return image + } else { + let image = _emojiFoodTab() + _ = cached.modify { current in + var current = current + current["emojiFoodTab"] = image + return current + } + return image + } + } + var emojiSportTab: CGImage { + if let image = cached.with({ $0["emojiSportTab"] }) { + return image + } else { + let image = _emojiSportTab() + _ = cached.modify { current in + var current = current + current["emojiSportTab"] = image + return current + } + return image + } + } + var emojiCarTab: CGImage { + if let image = cached.with({ $0["emojiCarTab"] }) { + return image + } else { + let image = _emojiCarTab() + _ = cached.modify { current in + var current = current + current["emojiCarTab"] = image + return current + } + return image + } + } + var emojiObjectsTab: CGImage { + if let image = cached.with({ $0["emojiObjectsTab"] }) { + return image + } else { + let image = _emojiObjectsTab() + _ = cached.modify { current in + var current = current + current["emojiObjectsTab"] = image + return current + } + return image + } + } + var emojiSymbolsTab: CGImage { + if let image = cached.with({ $0["emojiSymbolsTab"] }) { + return image + } else { + let image = _emojiSymbolsTab() + _ = cached.modify { current in + var current = current + current["emojiSymbolsTab"] = image + return current + } + return image + } + } + var emojiFlagsTab: CGImage { + if let image = cached.with({ $0["emojiFlagsTab"] }) { + return image + } else { + let image = _emojiFlagsTab() + _ = cached.modify { current in + var current = current + current["emojiFlagsTab"] = image + return current + } + return image + } + } + var emojiRecentTabActive: CGImage { + if let image = cached.with({ $0["emojiRecentTabActive"] }) { + return image + } else { + let image = _emojiRecentTabActive() + _ = cached.modify { current in + var current = current + current["emojiRecentTabActive"] = image + return current + } + return image + } + } + var emojiSmileTabActive: CGImage { + if let image = cached.with({ $0["emojiSmileTabActive"] }) { + return image + } else { + let image = _emojiSmileTabActive() + _ = cached.modify { current in + var current = current + current["emojiSmileTabActive"] = image + return current + } + return image + } + } + var emojiNatureTabActive: CGImage { + if let image = cached.with({ $0["emojiNatureTabActive"] }) { + return image + } else { + let image = _emojiNatureTabActive() + _ = cached.modify { current in + var current = current + current["emojiNatureTabActive"] = image + return current + } + return image + } + } + var emojiFoodTabActive: CGImage { + if let image = cached.with({ $0["emojiFoodTabActive"] }) { + return image + } else { + let image = _emojiFoodTabActive() + _ = cached.modify { current in + var current = current + current["emojiFoodTabActive"] = image + return current + } + return image + } + } + var emojiSportTabActive: CGImage { + if let image = cached.with({ $0["emojiSportTabActive"] }) { + return image + } else { + let image = _emojiSportTabActive() + _ = cached.modify { current in + var current = current + current["emojiSportTabActive"] = image + return current + } + return image + } + } + var emojiCarTabActive: CGImage { + if let image = cached.with({ $0["emojiCarTabActive"] }) { + return image + } else { + let image = _emojiCarTabActive() + _ = cached.modify { current in + var current = current + current["emojiCarTabActive"] = image + return current + } + return image + } + } + var emojiObjectsTabActive: CGImage { + if let image = cached.with({ $0["emojiObjectsTabActive"] }) { + return image + } else { + let image = _emojiObjectsTabActive() + _ = cached.modify { current in + var current = current + current["emojiObjectsTabActive"] = image + return current + } + return image + } + } + var emojiSymbolsTabActive: CGImage { + if let image = cached.with({ $0["emojiSymbolsTabActive"] }) { + return image + } else { + let image = _emojiSymbolsTabActive() + _ = cached.modify { current in + var current = current + current["emojiSymbolsTabActive"] = image + return current + } + return image + } + } + var emojiFlagsTabActive: CGImage { + if let image = cached.with({ $0["emojiFlagsTabActive"] }) { + return image + } else { + let image = _emojiFlagsTabActive() + _ = cached.modify { current in + var current = current + current["emojiFlagsTabActive"] = image + return current + } + return image + } + } + var stickerBackground: CGImage { + if let image = cached.with({ $0["stickerBackground"] }) { + return image + } else { + let image = _stickerBackground() + _ = cached.modify { current in + var current = current + current["stickerBackground"] = image + return current + } + return image + } + } + var stickerBackgroundActive: CGImage { + if let image = cached.with({ $0["stickerBackgroundActive"] }) { + return image + } else { + let image = _stickerBackgroundActive() + _ = cached.modify { current in + var current = current + current["stickerBackgroundActive"] = image + return current + } + return image + } + } + var stickersTabRecent: CGImage { + if let image = cached.with({ $0["stickersTabRecent"] }) { + return image + } else { + let image = _stickersTabRecent() + _ = cached.modify { current in + var current = current + current["stickersTabRecent"] = image + return current + } + return image + } + } + var stickersTabGIF: CGImage { + if let image = cached.with({ $0["stickersTabGIF"] }) { + return image + } else { + let image = _stickersTabGIF() + _ = cached.modify { current in + var current = current + current["stickersTabGIF"] = image + return current + } + return image + } + } + var chatSendingInFrame_incoming: CGImage { + if let image = cached.with({ $0["chatSendingInFrame_incoming"] }) { + return image + } else { + let image = _chatSendingInFrame_incoming() + _ = cached.modify { current in + var current = current + current["chatSendingInFrame_incoming"] = image + return current + } + return image + } + } + var chatSendingInHour_incoming: CGImage { + if let image = cached.with({ $0["chatSendingInHour_incoming"] }) { + return image + } else { + let image = _chatSendingInHour_incoming() + _ = cached.modify { current in + var current = current + current["chatSendingInHour_incoming"] = image + return current + } + return image + } + } + var chatSendingInMin_incoming: CGImage { + if let image = cached.with({ $0["chatSendingInMin_incoming"] }) { + return image + } else { + let image = _chatSendingInMin_incoming() + _ = cached.modify { current in + var current = current + current["chatSendingInMin_incoming"] = image + return current + } + return image + } + } + var chatSendingInFrame_outgoing: CGImage { + if let image = cached.with({ $0["chatSendingInFrame_outgoing"] }) { + return image + } else { + let image = _chatSendingInFrame_outgoing() + _ = cached.modify { current in + var current = current + current["chatSendingInFrame_outgoing"] = image + return current + } + return image + } + } + var chatSendingInHour_outgoing: CGImage { + if let image = cached.with({ $0["chatSendingInHour_outgoing"] }) { + return image + } else { + let image = _chatSendingInHour_outgoing() + _ = cached.modify { current in + var current = current + current["chatSendingInHour_outgoing"] = image + return current + } + return image + } + } + var chatSendingInMin_outgoing: CGImage { + if let image = cached.with({ $0["chatSendingInMin_outgoing"] }) { + return image + } else { + let image = _chatSendingInMin_outgoing() + _ = cached.modify { current in + var current = current + current["chatSendingInMin_outgoing"] = image + return current + } + return image + } + } + var chatSendingOutFrame: CGImage { + if let image = cached.with({ $0["chatSendingOutFrame"] }) { + return image + } else { + let image = _chatSendingOutFrame() + _ = cached.modify { current in + var current = current + current["chatSendingOutFrame"] = image + return current + } + return image + } + } + var chatSendingOutHour: CGImage { + if let image = cached.with({ $0["chatSendingOutHour"] }) { + return image + } else { + let image = _chatSendingOutHour() + _ = cached.modify { current in + var current = current + current["chatSendingOutHour"] = image + return current + } + return image + } + } + var chatSendingOutMin: CGImage { + if let image = cached.with({ $0["chatSendingOutMin"] }) { + return image + } else { + let image = _chatSendingOutMin() + _ = cached.modify { current in + var current = current + current["chatSendingOutMin"] = image + return current + } + return image + } + } + var chatSendingOverlayFrame: CGImage { + if let image = cached.with({ $0["chatSendingOverlayFrame"] }) { + return image + } else { + let image = _chatSendingOverlayFrame() + _ = cached.modify { current in + var current = current + current["chatSendingOverlayFrame"] = image + return current + } + return image + } + } + var chatSendingOverlayHour: CGImage { + if let image = cached.with({ $0["chatSendingOverlayHour"] }) { + return image + } else { + let image = _chatSendingOverlayHour() + _ = cached.modify { current in + var current = current + current["chatSendingOverlayHour"] = image + return current + } + return image + } + } + var chatSendingOverlayMin: CGImage { + if let image = cached.with({ $0["chatSendingOverlayMin"] }) { + return image + } else { + let image = _chatSendingOverlayMin() + _ = cached.modify { current in + var current = current + current["chatSendingOverlayMin"] = image + return current + } + return image + } + } + var chatActionUrl: CGImage { + if let image = cached.with({ $0["chatActionUrl"] }) { + return image + } else { + let image = _chatActionUrl() + _ = cached.modify { current in + var current = current + current["chatActionUrl"] = image + return current + } + return image + } + } + var callInlineDecline: CGImage { + if let image = cached.with({ $0["callInlineDecline"] }) { + return image + } else { + let image = _callInlineDecline() + _ = cached.modify { current in + var current = current + current["callInlineDecline"] = image + return current + } + return image + } + } + var callInlineMuted: CGImage { + if let image = cached.with({ $0["callInlineMuted"] }) { + return image + } else { + let image = _callInlineMuted() + _ = cached.modify { current in + var current = current + current["callInlineMuted"] = image + return current + } + return image + } + } + var callInlineUnmuted: CGImage { + if let image = cached.with({ $0["callInlineUnmuted"] }) { + return image + } else { + let image = _callInlineUnmuted() + _ = cached.modify { current in + var current = current + current["callInlineUnmuted"] = image + return current + } + return image + } + } + var eventLogTriangle: CGImage { + if let image = cached.with({ $0["eventLogTriangle"] }) { + return image + } else { + let image = _eventLogTriangle() + _ = cached.modify { current in + var current = current + current["eventLogTriangle"] = image + return current + } + return image + } + } + var channelIntro: CGImage { + if let image = cached.with({ $0["channelIntro"] }) { + return image + } else { + let image = _channelIntro() + _ = cached.modify { current in + var current = current + current["channelIntro"] = image + return current + } + return image + } + } + var chatFileThumb: CGImage { + if let image = cached.with({ $0["chatFileThumb"] }) { + return image + } else { + let image = _chatFileThumb() + _ = cached.modify { current in + var current = current + current["chatFileThumb"] = image + return current + } + return image + } + } + var chatFileThumbBubble_incoming: CGImage { + if let image = cached.with({ $0["chatFileThumbBubble_incoming"] }) { + return image + } else { + let image = _chatFileThumbBubble_incoming() + _ = cached.modify { current in + var current = current + current["chatFileThumbBubble_incoming"] = image + return current + } + return image + } + } + var chatFileThumbBubble_outgoing: CGImage { + if let image = cached.with({ $0["chatFileThumbBubble_outgoing"] }) { + return image + } else { + let image = _chatFileThumbBubble_outgoing() + _ = cached.modify { current in + var current = current + current["chatFileThumbBubble_outgoing"] = image + return current + } + return image + } + } + var chatSecretThumb: CGImage { + if let image = cached.with({ $0["chatSecretThumb"] }) { + return image + } else { + let image = _chatSecretThumb() + _ = cached.modify { current in + var current = current + current["chatSecretThumb"] = image + return current + } + return image + } + } + var chatSecretThumbSmall: CGImage { + if let image = cached.with({ $0["chatSecretThumbSmall"] }) { + return image + } else { + let image = _chatSecretThumbSmall() + _ = cached.modify { current in + var current = current + current["chatSecretThumbSmall"] = image + return current + } + return image + } + } + var chatMapPin: CGImage { + if let image = cached.with({ $0["chatMapPin"] }) { + return image + } else { + let image = _chatMapPin() + _ = cached.modify { current in + var current = current + current["chatMapPin"] = image + return current + } + return image + } + } + var chatSecretTitle: CGImage { + if let image = cached.with({ $0["chatSecretTitle"] }) { + return image + } else { + let image = _chatSecretTitle() + _ = cached.modify { current in + var current = current + current["chatSecretTitle"] = image + return current + } + return image + } + } + var emptySearch: CGImage { + if let image = cached.with({ $0["emptySearch"] }) { + return image + } else { + let image = _emptySearch() + _ = cached.modify { current in + var current = current + current["emptySearch"] = image + return current + } + return image + } + } + var calendarBack: CGImage { + if let image = cached.with({ $0["calendarBack"] }) { + return image + } else { + let image = _calendarBack() + _ = cached.modify { current in + var current = current + current["calendarBack"] = image + return current + } + return image + } + } + var calendarNext: CGImage { + if let image = cached.with({ $0["calendarNext"] }) { + return image + } else { + let image = _calendarNext() + _ = cached.modify { current in + var current = current + current["calendarNext"] = image + return current + } + return image + } + } + var calendarBackDisabled: CGImage { + if let image = cached.with({ $0["calendarBackDisabled"] }) { + return image + } else { + let image = _calendarBackDisabled() + _ = cached.modify { current in + var current = current + current["calendarBackDisabled"] = image + return current + } + return image + } + } + var calendarNextDisabled: CGImage { + if let image = cached.with({ $0["calendarNextDisabled"] }) { + return image + } else { + let image = _calendarNextDisabled() + _ = cached.modify { current in + var current = current + current["calendarNextDisabled"] = image + return current + } + return image + } + } + var newChatCamera: CGImage { + if let image = cached.with({ $0["newChatCamera"] }) { + return image + } else { + let image = _newChatCamera() + _ = cached.modify { current in + var current = current + current["newChatCamera"] = image + return current + } + return image + } + } + var peerInfoVerify: CGImage { + if let image = cached.with({ $0["peerInfoVerify"] }) { + return image + } else { + let image = _peerInfoVerify() + _ = cached.modify { current in + var current = current + current["peerInfoVerify"] = image + return current + } + return image + } + } + var peerInfoVerifyProfile: CGImage { + if let image = cached.with({ $0["peerInfoVerifyProfile"] }) { + return image + } else { + let image = _peerInfoVerifyProfile() + _ = cached.modify { current in + var current = current + current["peerInfoVerifyProfile"] = image + return current + } + return image + } + } + var peerInfoCall: CGImage { + if let image = cached.with({ $0["peerInfoCall"] }) { + return image + } else { + let image = _peerInfoCall() + _ = cached.modify { current in + var current = current + current["peerInfoCall"] = image + return current + } + return image + } + } + var callOutgoing: CGImage { + if let image = cached.with({ $0["callOutgoing"] }) { + return image + } else { + let image = _callOutgoing() + _ = cached.modify { current in + var current = current + current["callOutgoing"] = image + return current + } + return image + } + } + var recentDismiss: CGImage { + if let image = cached.with({ $0["recentDismiss"] }) { + return image + } else { + let image = _recentDismiss() + _ = cached.modify { current in + var current = current + current["recentDismiss"] = image + return current + } + return image + } + } + var recentDismissActive: CGImage { + if let image = cached.with({ $0["recentDismissActive"] }) { + return image + } else { + let image = _recentDismissActive() + _ = cached.modify { current in + var current = current + current["recentDismissActive"] = image + return current + } + return image + } + } + var webgameShare: CGImage { + if let image = cached.with({ $0["webgameShare"] }) { + return image + } else { + let image = _webgameShare() + _ = cached.modify { current in + var current = current + current["webgameShare"] = image + return current + } + return image + } + } + var chatSearchCancel: CGImage { + if let image = cached.with({ $0["chatSearchCancel"] }) { + return image + } else { + let image = _chatSearchCancel() + _ = cached.modify { current in + var current = current + current["chatSearchCancel"] = image + return current + } + return image + } + } + var chatSearchFrom: CGImage { + if let image = cached.with({ $0["chatSearchFrom"] }) { + return image + } else { + let image = _chatSearchFrom() + _ = cached.modify { current in + var current = current + current["chatSearchFrom"] = image + return current + } + return image + } + } + var callWindowDecline: CGImage { + if let image = cached.with({ $0["callWindowDecline"] }) { + return image + } else { + let image = _callWindowDecline() + _ = cached.modify { current in + var current = current + current["callWindowDecline"] = image + return current + } + return image + } + } + var callWindowDeclineSmall: CGImage { + if let image = cached.with({ $0["callWindowDeclineSmall"] }) { + return image + } else { + let image = _callWindowDeclineSmall() + _ = cached.modify { current in + var current = current + current["callWindowDeclineSmall"] = image + return current + } + return image + } + } + var callWindowAccept: CGImage { + if let image = cached.with({ $0["callWindowAccept"] }) { + return image + } else { + let image = _callWindowAccept() + _ = cached.modify { current in + var current = current + current["callWindowAccept"] = image + return current + } + return image + } + } + var callWindowVideo: CGImage { + if let image = cached.with({ $0["callWindowVideo"] }) { + return image + } else { + let image = _callWindowVideo() + _ = cached.modify { current in + var current = current + current["callWindowVideo"] = image + return current + } + return image + } + } + var callWindowVideoActive: CGImage { + if let image = cached.with({ $0["callWindowVideoActive"] }) { + return image + } else { + let image = _callWindowVideoActive() + _ = cached.modify { current in + var current = current + current["callWindowVideoActive"] = image + return current + } + return image + } + } + var callWindowMute: CGImage { + if let image = cached.with({ $0["callWindowMute"] }) { + return image + } else { + let image = _callWindowMute() + _ = cached.modify { current in + var current = current + current["callWindowMute"] = image + return current + } + return image + } + } + var callWindowMuteActive: CGImage { + if let image = cached.with({ $0["callWindowMuteActive"] }) { + return image + } else { + let image = _callWindowMuteActive() + _ = cached.modify { current in + var current = current + current["callWindowMuteActive"] = image + return current + } + return image + } + } + var callWindowClose: CGImage { + if let image = cached.with({ $0["callWindowClose"] }) { + return image + } else { + let image = _callWindowClose() + _ = cached.modify { current in + var current = current + current["callWindowClose"] = image + return current + } + return image + } + } + var callWindowDeviceSettings: CGImage { + if let image = cached.with({ $0["callWindowDeviceSettings"] }) { + return image + } else { + let image = _callWindowDeviceSettings() + _ = cached.modify { current in + var current = current + current["callWindowDeviceSettings"] = image + return current + } + return image + } + } + var callSettings: CGImage { + if let image = cached.with({ $0["callSettings"] }) { + return image + } else { + let image = _callSettings() + _ = cached.modify { current in + var current = current + current["callSettings"] = image + return current + } + return image + } + } + var callWindowCancel: CGImage { + if let image = cached.with({ $0["callWindowCancel"] }) { + return image + } else { + let image = _callWindowCancel() + _ = cached.modify { current in + var current = current + current["callWindowCancel"] = image + return current + } + return image + } + } + var chatActionEdit: CGImage { + if let image = cached.with({ $0["chatActionEdit"] }) { + return image + } else { + let image = _chatActionEdit() + _ = cached.modify { current in + var current = current + current["chatActionEdit"] = image + return current + } + return image + } + } + var chatActionInfo: CGImage { + if let image = cached.with({ $0["chatActionInfo"] }) { + return image + } else { + let image = _chatActionInfo() + _ = cached.modify { current in + var current = current + current["chatActionInfo"] = image + return current + } + return image + } + } + var chatActionMute: CGImage { + if let image = cached.with({ $0["chatActionMute"] }) { + return image + } else { + let image = _chatActionMute() + _ = cached.modify { current in + var current = current + current["chatActionMute"] = image + return current + } + return image + } + } + var chatActionUnmute: CGImage { + if let image = cached.with({ $0["chatActionUnmute"] }) { + return image + } else { + let image = _chatActionUnmute() + _ = cached.modify { current in + var current = current + current["chatActionUnmute"] = image + return current + } + return image + } + } + var chatActionClearHistory: CGImage { + if let image = cached.with({ $0["chatActionClearHistory"] }) { + return image + } else { + let image = _chatActionClearHistory() + _ = cached.modify { current in + var current = current + current["chatActionClearHistory"] = image + return current + } + return image + } + } + var chatActionDeleteChat: CGImage { + if let image = cached.with({ $0["chatActionDeleteChat"] }) { + return image + } else { + let image = _chatActionDeleteChat() + _ = cached.modify { current in + var current = current + current["chatActionDeleteChat"] = image + return current + } + return image + } + } + var dismissPinned: CGImage { + if let image = cached.with({ $0["dismissPinned"] }) { + return image + } else { + let image = _dismissPinned() + _ = cached.modify { current in + var current = current + current["dismissPinned"] = image + return current + } + return image + } + } + var chatActionsActive: CGImage { + if let image = cached.with({ $0["chatActionsActive"] }) { + return image + } else { + let image = _chatActionsActive() + _ = cached.modify { current in + var current = current + current["chatActionsActive"] = image + return current + } + return image + } + } + var chatEntertainmentSticker: CGImage { + if let image = cached.with({ $0["chatEntertainmentSticker"] }) { + return image + } else { + let image = _chatEntertainmentSticker() + _ = cached.modify { current in + var current = current + current["chatEntertainmentSticker"] = image + return current + } + return image + } + } + var chatEmpty: CGImage { + if let image = cached.with({ $0["chatEmpty"] }) { + return image + } else { + let image = _chatEmpty() + _ = cached.modify { current in + var current = current + current["chatEmpty"] = image + return current + } + return image + } + } + var stickerPackClose: CGImage { + if let image = cached.with({ $0["stickerPackClose"] }) { + return image + } else { + let image = _stickerPackClose() + _ = cached.modify { current in + var current = current + current["stickerPackClose"] = image + return current + } + return image + } + } + var stickerPackDelete: CGImage { + if let image = cached.with({ $0["stickerPackDelete"] }) { + return image + } else { + let image = _stickerPackDelete() + _ = cached.modify { current in + var current = current + current["stickerPackDelete"] = image + return current + } + return image + } + } + var modalShare: CGImage { + if let image = cached.with({ $0["modalShare"] }) { + return image + } else { + let image = _modalShare() + _ = cached.modify { current in + var current = current + current["modalShare"] = image + return current + } + return image + } + } + var modalClose: CGImage { + if let image = cached.with({ $0["modalClose"] }) { + return image + } else { + let image = _modalClose() + _ = cached.modify { current in + var current = current + current["modalClose"] = image + return current + } + return image + } + } + var ivChannelJoined: CGImage { + if let image = cached.with({ $0["ivChannelJoined"] }) { + return image + } else { + let image = _ivChannelJoined() + _ = cached.modify { current in + var current = current + current["ivChannelJoined"] = image + return current + } + return image + } + } + var chatListMention: CGImage { + if let image = cached.with({ $0["chatListMention"] }) { + return image + } else { + let image = _chatListMention() + _ = cached.modify { current in + var current = current + current["chatListMention"] = image + return current + } + return image + } + } + var chatListMentionActive: CGImage { + if let image = cached.with({ $0["chatListMentionActive"] }) { + return image + } else { + let image = _chatListMentionActive() + _ = cached.modify { current in + var current = current + current["chatListMentionActive"] = image + return current + } + return image + } + } + var chatListMentionArchived: CGImage { + if let image = cached.with({ $0["chatListMentionArchived"] }) { + return image + } else { + let image = _chatListMentionArchived() + _ = cached.modify { current in + var current = current + current["chatListMentionArchived"] = image + return current + } + return image + } + } + var chatListMentionArchivedActive: CGImage { + if let image = cached.with({ $0["chatListMentionArchivedActive"] }) { + return image + } else { + let image = _chatListMentionArchivedActive() + _ = cached.modify { current in + var current = current + current["chatListMentionArchivedActive"] = image + return current + } + return image + } + } + var chatMention: CGImage { + if let image = cached.with({ $0["chatMention"] }) { + return image + } else { + let image = _chatMention() + _ = cached.modify { current in + var current = current + current["chatMention"] = image + return current + } + return image + } + } + var chatMentionActive: CGImage { + if let image = cached.with({ $0["chatMentionActive"] }) { + return image + } else { + let image = _chatMentionActive() + _ = cached.modify { current in + var current = current + current["chatMentionActive"] = image + return current + } + return image + } + } + var sliderControl: CGImage { + if let image = cached.with({ $0["sliderControl"] }) { + return image + } else { + let image = _sliderControl() + _ = cached.modify { current in + var current = current + current["sliderControl"] = image + return current + } + return image + } + } + var sliderControlActive: CGImage { + if let image = cached.with({ $0["sliderControlActive"] }) { + return image + } else { + let image = _sliderControlActive() + _ = cached.modify { current in + var current = current + current["sliderControlActive"] = image + return current + } + return image + } + } + var stickersTabFave: CGImage { + if let image = cached.with({ $0["stickersTabFave"] }) { + return image + } else { + let image = _stickersTabFave() + _ = cached.modify { current in + var current = current + current["stickersTabFave"] = image + return current + } + return image + } + } + var chatInstantView: CGImage { + if let image = cached.with({ $0["chatInstantView"] }) { + return image + } else { + let image = _chatInstantView() + _ = cached.modify { current in + var current = current + current["chatInstantView"] = image + return current + } + return image + } + } + var chatInstantViewBubble_incoming: CGImage { + if let image = cached.with({ $0["chatInstantViewBubble_incoming"] }) { + return image + } else { + let image = _chatInstantViewBubble_incoming() + _ = cached.modify { current in + var current = current + current["chatInstantViewBubble_incoming"] = image + return current + } + return image + } + } + var chatInstantViewBubble_outgoing: CGImage { + if let image = cached.with({ $0["chatInstantViewBubble_outgoing"] }) { + return image + } else { + let image = _chatInstantViewBubble_outgoing() + _ = cached.modify { current in + var current = current + current["chatInstantViewBubble_outgoing"] = image + return current + } + return image + } + } + var instantViewShare: CGImage { + if let image = cached.with({ $0["instantViewShare"] }) { + return image + } else { + let image = _instantViewShare() + _ = cached.modify { current in + var current = current + current["instantViewShare"] = image + return current + } + return image + } + } + var instantViewActions: CGImage { + if let image = cached.with({ $0["instantViewActions"] }) { + return image + } else { + let image = _instantViewActions() + _ = cached.modify { current in + var current = current + current["instantViewActions"] = image + return current + } + return image + } + } + var instantViewActionsActive: CGImage { + if let image = cached.with({ $0["instantViewActionsActive"] }) { + return image + } else { + let image = _instantViewActionsActive() + _ = cached.modify { current in + var current = current + current["instantViewActionsActive"] = image + return current + } + return image + } + } + var instantViewSafari: CGImage { + if let image = cached.with({ $0["instantViewSafari"] }) { + return image + } else { + let image = _instantViewSafari() + _ = cached.modify { current in + var current = current + current["instantViewSafari"] = image + return current + } + return image + } + } + var instantViewBack: CGImage { + if let image = cached.with({ $0["instantViewBack"] }) { + return image + } else { + let image = _instantViewBack() + _ = cached.modify { current in + var current = current + current["instantViewBack"] = image + return current + } + return image + } + } + var instantViewCheck: CGImage { + if let image = cached.with({ $0["instantViewCheck"] }) { + return image + } else { + let image = _instantViewCheck() + _ = cached.modify { current in + var current = current + current["instantViewCheck"] = image + return current + } + return image + } + } + var groupStickerNotFound: CGImage { + if let image = cached.with({ $0["groupStickerNotFound"] }) { + return image + } else { + let image = _groupStickerNotFound() + _ = cached.modify { current in + var current = current + current["groupStickerNotFound"] = image + return current + } + return image + } + } + var settingsAskQuestion: CGImage { + if let image = cached.with({ $0["settingsAskQuestion"] }) { + return image + } else { + let image = _settingsAskQuestion() + _ = cached.modify { current in + var current = current + current["settingsAskQuestion"] = image + return current + } + return image + } + } + var settingsFaq: CGImage { + if let image = cached.with({ $0["settingsFaq"] }) { + return image + } else { + let image = _settingsFaq() + _ = cached.modify { current in + var current = current + current["settingsFaq"] = image + return current + } + return image + } + } + var settingsGeneral: CGImage { + if let image = cached.with({ $0["settingsGeneral"] }) { + return image + } else { + let image = _settingsGeneral() + _ = cached.modify { current in + var current = current + current["settingsGeneral"] = image + return current + } + return image + } + } + var settingsLanguage: CGImage { + if let image = cached.with({ $0["settingsLanguage"] }) { + return image + } else { + let image = _settingsLanguage() + _ = cached.modify { current in + var current = current + current["settingsLanguage"] = image + return current + } + return image + } + } + var settingsNotifications: CGImage { + if let image = cached.with({ $0["settingsNotifications"] }) { + return image + } else { + let image = _settingsNotifications() + _ = cached.modify { current in + var current = current + current["settingsNotifications"] = image + return current + } + return image + } + } + var settingsSecurity: CGImage { + if let image = cached.with({ $0["settingsSecurity"] }) { + return image + } else { + let image = _settingsSecurity() + _ = cached.modify { current in + var current = current + current["settingsSecurity"] = image + return current + } + return image + } + } + var settingsStickers: CGImage { + if let image = cached.with({ $0["settingsStickers"] }) { + return image + } else { + let image = _settingsStickers() + _ = cached.modify { current in + var current = current + current["settingsStickers"] = image + return current + } + return image + } + } + var settingsStorage: CGImage { + if let image = cached.with({ $0["settingsStorage"] }) { + return image + } else { + let image = _settingsStorage() + _ = cached.modify { current in + var current = current + current["settingsStorage"] = image + return current + } + return image + } + } + var settingsSessions: CGImage { + if let image = cached.with({ $0["settingsSessions"] }) { + return image + } else { + let image = _settingsSessions() + _ = cached.modify { current in + var current = current + current["settingsSessions"] = image + return current + } + return image + } + } + var settingsProxy: CGImage { + if let image = cached.with({ $0["settingsProxy"] }) { + return image + } else { + let image = _settingsProxy() + _ = cached.modify { current in + var current = current + current["settingsProxy"] = image + return current + } + return image + } + } + var settingsAppearance: CGImage { + if let image = cached.with({ $0["settingsAppearance"] }) { + return image + } else { + let image = _settingsAppearance() + _ = cached.modify { current in + var current = current + current["settingsAppearance"] = image + return current + } + return image + } + } + var settingsPassport: CGImage { + if let image = cached.with({ $0["settingsPassport"] }) { + return image + } else { + let image = _settingsPassport() + _ = cached.modify { current in + var current = current + current["settingsPassport"] = image + return current + } + return image + } + } + var settingsWallet: CGImage { + if let image = cached.with({ $0["settingsWallet"] }) { + return image + } else { + let image = _settingsWallet() + _ = cached.modify { current in + var current = current + current["settingsWallet"] = image + return current + } + return image + } + } + var settingsUpdate: CGImage { + if let image = cached.with({ $0["settingsUpdate"] }) { + return image + } else { + let image = _settingsUpdate() + _ = cached.modify { current in + var current = current + current["settingsUpdate"] = image + return current + } + return image + } + } + var settingsFilters: CGImage { + if let image = cached.with({ $0["settingsFilters"] }) { + return image + } else { + let image = _settingsFilters() + _ = cached.modify { current in + var current = current + current["settingsFilters"] = image + return current + } + return image + } + } + var settingsAskQuestionActive: CGImage { + if let image = cached.with({ $0["settingsAskQuestionActive"] }) { + return image + } else { + let image = _settingsAskQuestionActive() + _ = cached.modify { current in + var current = current + current["settingsAskQuestionActive"] = image + return current + } + return image + } + } + var settingsFaqActive: CGImage { + if let image = cached.with({ $0["settingsFaqActive"] }) { + return image + } else { + let image = _settingsFaqActive() + _ = cached.modify { current in + var current = current + current["settingsFaqActive"] = image + return current + } + return image + } + } + var settingsGeneralActive: CGImage { + if let image = cached.with({ $0["settingsGeneralActive"] }) { + return image + } else { + let image = _settingsGeneralActive() + _ = cached.modify { current in + var current = current + current["settingsGeneralActive"] = image + return current + } + return image + } + } + var settingsLanguageActive: CGImage { + if let image = cached.with({ $0["settingsLanguageActive"] }) { + return image + } else { + let image = _settingsLanguageActive() + _ = cached.modify { current in + var current = current + current["settingsLanguageActive"] = image + return current + } + return image + } + } + var settingsNotificationsActive: CGImage { + if let image = cached.with({ $0["settingsNotificationsActive"] }) { + return image + } else { + let image = _settingsNotificationsActive() + _ = cached.modify { current in + var current = current + current["settingsNotificationsActive"] = image + return current + } + return image + } + } + var settingsSecurityActive: CGImage { + if let image = cached.with({ $0["settingsSecurityActive"] }) { + return image + } else { + let image = _settingsSecurityActive() + _ = cached.modify { current in + var current = current + current["settingsSecurityActive"] = image + return current + } + return image + } + } + var settingsStickersActive: CGImage { + if let image = cached.with({ $0["settingsStickersActive"] }) { + return image + } else { + let image = _settingsStickersActive() + _ = cached.modify { current in + var current = current + current["settingsStickersActive"] = image + return current + } + return image + } + } + var settingsStorageActive: CGImage { + if let image = cached.with({ $0["settingsStorageActive"] }) { + return image + } else { + let image = _settingsStorageActive() + _ = cached.modify { current in + var current = current + current["settingsStorageActive"] = image + return current + } + return image + } + } + var settingsSessionsActive: CGImage { + if let image = cached.with({ $0["settingsSessionsActive"] }) { + return image + } else { + let image = _settingsSessionsActive() + _ = cached.modify { current in + var current = current + current["settingsSessionsActive"] = image + return current + } + return image + } + } + var settingsProxyActive: CGImage { + if let image = cached.with({ $0["settingsProxyActive"] }) { + return image + } else { + let image = _settingsProxyActive() + _ = cached.modify { current in + var current = current + current["settingsProxyActive"] = image + return current + } + return image + } + } + var settingsAppearanceActive: CGImage { + if let image = cached.with({ $0["settingsAppearanceActive"] }) { + return image + } else { + let image = _settingsAppearanceActive() + _ = cached.modify { current in + var current = current + current["settingsAppearanceActive"] = image + return current + } + return image + } + } + var settingsPassportActive: CGImage { + if let image = cached.with({ $0["settingsPassportActive"] }) { + return image + } else { + let image = _settingsPassportActive() + _ = cached.modify { current in + var current = current + current["settingsPassportActive"] = image + return current + } + return image + } + } + var settingsWalletActive: CGImage { + if let image = cached.with({ $0["settingsWalletActive"] }) { + return image + } else { + let image = _settingsWalletActive() + _ = cached.modify { current in + var current = current + current["settingsWalletActive"] = image + return current + } + return image + } + } + var settingsUpdateActive: CGImage { + if let image = cached.with({ $0["settingsUpdateActive"] }) { + return image + } else { + let image = _settingsUpdateActive() + _ = cached.modify { current in + var current = current + current["settingsUpdateActive"] = image + return current + } + return image + } + } + var settingsFiltersActive: CGImage { + if let image = cached.with({ $0["settingsFiltersActive"] }) { + return image + } else { + let image = _settingsFiltersActive() + _ = cached.modify { current in + var current = current + current["settingsFiltersActive"] = image + return current + } + return image + } + } + var settingsProfile: CGImage { + if let image = cached.with({ $0["settingsProfile"] }) { + return image + } else { + let image = _settingsProfile() + _ = cached.modify { current in + var current = current + current["settingsProfile"] = image + return current + } + return image + } + } + var generalCheck: CGImage { + if let image = cached.with({ $0["generalCheck"] }) { + return image + } else { + let image = _generalCheck() + _ = cached.modify { current in + var current = current + current["generalCheck"] = image + return current + } + return image + } + } + var settingsAbout: CGImage { + if let image = cached.with({ $0["settingsAbout"] }) { + return image + } else { + let image = _settingsAbout() + _ = cached.modify { current in + var current = current + current["settingsAbout"] = image + return current + } + return image + } + } + var settingsLogout: CGImage { + if let image = cached.with({ $0["settingsLogout"] }) { + return image + } else { + let image = _settingsLogout() + _ = cached.modify { current in + var current = current + current["settingsLogout"] = image + return current + } + return image + } + } + var fastSettingsLock: CGImage { + if let image = cached.with({ $0["fastSettingsLock"] }) { + return image + } else { + let image = _fastSettingsLock() + _ = cached.modify { current in + var current = current + current["fastSettingsLock"] = image + return current + } + return image + } + } + var fastSettingsDark: CGImage { + if let image = cached.with({ $0["fastSettingsDark"] }) { + return image + } else { + let image = _fastSettingsDark() + _ = cached.modify { current in + var current = current + current["fastSettingsDark"] = image + return current + } + return image + } + } + var fastSettingsSunny: CGImage { + if let image = cached.with({ $0["fastSettingsSunny"] }) { + return image + } else { + let image = _fastSettingsSunny() + _ = cached.modify { current in + var current = current + current["fastSettingsSunny"] = image + return current + } + return image + } + } + var fastSettingsMute: CGImage { + if let image = cached.with({ $0["fastSettingsMute"] }) { + return image + } else { + let image = _fastSettingsMute() + _ = cached.modify { current in + var current = current + current["fastSettingsMute"] = image + return current + } + return image + } + } + var fastSettingsUnmute: CGImage { + if let image = cached.with({ $0["fastSettingsUnmute"] }) { + return image + } else { + let image = _fastSettingsUnmute() + _ = cached.modify { current in + var current = current + current["fastSettingsUnmute"] = image + return current + } + return image + } + } + var chatRecordVideo: CGImage { + if let image = cached.with({ $0["chatRecordVideo"] }) { + return image + } else { + let image = _chatRecordVideo() + _ = cached.modify { current in + var current = current + current["chatRecordVideo"] = image + return current + } + return image + } + } + var inputChannelMute: CGImage { + if let image = cached.with({ $0["inputChannelMute"] }) { + return image + } else { + let image = _inputChannelMute() + _ = cached.modify { current in + var current = current + current["inputChannelMute"] = image + return current + } + return image + } + } + var inputChannelUnmute: CGImage { + if let image = cached.with({ $0["inputChannelUnmute"] }) { + return image + } else { + let image = _inputChannelUnmute() + _ = cached.modify { current in + var current = current + current["inputChannelUnmute"] = image + return current + } + return image + } + } + var changePhoneNumberIntro: CGImage { + if let image = cached.with({ $0["changePhoneNumberIntro"] }) { + return image + } else { + let image = _changePhoneNumberIntro() + _ = cached.modify { current in + var current = current + current["changePhoneNumberIntro"] = image + return current + } + return image + } + } + var peerSavedMessages: CGImage { + if let image = cached.with({ $0["peerSavedMessages"] }) { + return image + } else { + let image = _peerSavedMessages() + _ = cached.modify { current in + var current = current + current["peerSavedMessages"] = image + return current + } + return image + } + } + var previewSenderCollage: CGImage { + if let image = cached.with({ $0["previewSenderCollage"] }) { + return image + } else { + let image = _previewSenderCollage() + _ = cached.modify { current in + var current = current + current["previewSenderCollage"] = image + return current + } + return image + } + } + var previewSenderPhoto: CGImage { + if let image = cached.with({ $0["previewSenderPhoto"] }) { + return image + } else { + let image = _previewSenderPhoto() + _ = cached.modify { current in + var current = current + current["previewSenderPhoto"] = image + return current + } + return image + } + } + var previewSenderFile: CGImage { + if let image = cached.with({ $0["previewSenderFile"] }) { + return image + } else { + let image = _previewSenderFile() + _ = cached.modify { current in + var current = current + current["previewSenderFile"] = image + return current + } + return image + } + } + var previewSenderCrop: CGImage { + if let image = cached.with({ $0["previewSenderCrop"] }) { + return image + } else { + let image = _previewSenderCrop() + _ = cached.modify { current in + var current = current + current["previewSenderCrop"] = image + return current + } + return image + } + } + var previewSenderDelete: CGImage { + if let image = cached.with({ $0["previewSenderDelete"] }) { + return image + } else { + let image = _previewSenderDelete() + _ = cached.modify { current in + var current = current + current["previewSenderDelete"] = image + return current + } + return image + } + } + var previewSenderDeleteFile: CGImage { + if let image = cached.with({ $0["previewSenderDeleteFile"] }) { + return image + } else { + let image = _previewSenderDeleteFile() + _ = cached.modify { current in + var current = current + current["previewSenderDeleteFile"] = image + return current + } + return image + } + } + var previewSenderArchive: CGImage { + if let image = cached.with({ $0["previewSenderArchive"] }) { + return image + } else { + let image = _previewSenderArchive() + _ = cached.modify { current in + var current = current + current["previewSenderArchive"] = image + return current + } + return image + } + } + var chatGroupToggleSelected: CGImage { + if let image = cached.with({ $0["chatGroupToggleSelected"] }) { + return image + } else { + let image = _chatGroupToggleSelected() + _ = cached.modify { current in + var current = current + current["chatGroupToggleSelected"] = image + return current + } + return image + } + } + var chatGroupToggleUnselected: CGImage { + if let image = cached.with({ $0["chatGroupToggleUnselected"] }) { + return image + } else { + let image = _chatGroupToggleUnselected() + _ = cached.modify { current in + var current = current + current["chatGroupToggleUnselected"] = image + return current + } + return image + } + } + var successModalProgress: CGImage { + if let image = cached.with({ $0["successModalProgress"] }) { + return image + } else { + let image = _successModalProgress() + _ = cached.modify { current in + var current = current + current["successModalProgress"] = image + return current + } + return image + } + } + var accentColorSelect: CGImage { + if let image = cached.with({ $0["accentColorSelect"] }) { + return image + } else { + let image = _accentColorSelect() + _ = cached.modify { current in + var current = current + current["accentColorSelect"] = image + return current + } + return image + } + } + var transparentBackground: CGImage { + if let image = cached.with({ $0["transparentBackground"] }) { + return image + } else { + let image = _transparentBackground() + _ = cached.modify { current in + var current = current + current["transparentBackground"] = image + return current + } + return image + } + } + var lottieTransparentBackground: CGImage { + if let image = cached.with({ $0["lottieTransparentBackground"] }) { + return image + } else { + let image = _lottieTransparentBackground() + _ = cached.modify { current in + var current = current + current["lottieTransparentBackground"] = image + return current + } + return image + } + } + var passcodeTouchId: CGImage { + if let image = cached.with({ $0["passcodeTouchId"] }) { + return image + } else { + let image = _passcodeTouchId() + _ = cached.modify { current in + var current = current + current["passcodeTouchId"] = image + return current + } + return image + } + } + var passcodeLogin: CGImage { + if let image = cached.with({ $0["passcodeLogin"] }) { + return image + } else { + let image = _passcodeLogin() + _ = cached.modify { current in + var current = current + current["passcodeLogin"] = image + return current + } + return image + } + } + var confirmDeleteMessagesAccessory: CGImage { + if let image = cached.with({ $0["confirmDeleteMessagesAccessory"] }) { + return image + } else { + let image = _confirmDeleteMessagesAccessory() + _ = cached.modify { current in + var current = current + current["confirmDeleteMessagesAccessory"] = image + return current + } + return image + } + } + var alertCheckBoxSelected: CGImage { + if let image = cached.with({ $0["alertCheckBoxSelected"] }) { + return image + } else { + let image = _alertCheckBoxSelected() + _ = cached.modify { current in + var current = current + current["alertCheckBoxSelected"] = image + return current + } + return image + } + } + var alertCheckBoxUnselected: CGImage { + if let image = cached.with({ $0["alertCheckBoxUnselected"] }) { + return image + } else { + let image = _alertCheckBoxUnselected() + _ = cached.modify { current in + var current = current + current["alertCheckBoxUnselected"] = image + return current + } + return image + } + } + var confirmPinAccessory: CGImage { + if let image = cached.with({ $0["confirmPinAccessory"] }) { + return image + } else { + let image = _confirmPinAccessory() + _ = cached.modify { current in + var current = current + current["confirmPinAccessory"] = image + return current + } + return image + } + } + var confirmDeleteChatAccessory: CGImage { + if let image = cached.with({ $0["confirmDeleteChatAccessory"] }) { + return image + } else { + let image = _confirmDeleteChatAccessory() + _ = cached.modify { current in + var current = current + current["confirmDeleteChatAccessory"] = image + return current + } + return image + } + } + var stickersEmptySearch: CGImage { + if let image = cached.with({ $0["stickersEmptySearch"] }) { + return image + } else { + let image = _stickersEmptySearch() + _ = cached.modify { current in + var current = current + current["stickersEmptySearch"] = image + return current + } + return image + } + } + var twoStepVerificationCreateIntro: CGImage { + if let image = cached.with({ $0["twoStepVerificationCreateIntro"] }) { + return image + } else { + let image = _twoStepVerificationCreateIntro() + _ = cached.modify { current in + var current = current + current["twoStepVerificationCreateIntro"] = image + return current + } + return image + } + } + var secureIdAuth: CGImage { + if let image = cached.with({ $0["secureIdAuth"] }) { + return image + } else { + let image = _secureIdAuth() + _ = cached.modify { current in + var current = current + current["secureIdAuth"] = image + return current + } + return image + } + } + var ivAudioPlay: CGImage { + if let image = cached.with({ $0["ivAudioPlay"] }) { + return image + } else { + let image = _ivAudioPlay() + _ = cached.modify { current in + var current = current + current["ivAudioPlay"] = image + return current + } + return image + } + } + var ivAudioPause: CGImage { + if let image = cached.with({ $0["ivAudioPause"] }) { + return image + } else { + let image = _ivAudioPause() + _ = cached.modify { current in + var current = current + current["ivAudioPause"] = image + return current + } + return image + } + } + var proxyEnable: CGImage { + if let image = cached.with({ $0["proxyEnable"] }) { + return image + } else { + let image = _proxyEnable() + _ = cached.modify { current in + var current = current + current["proxyEnable"] = image + return current + } + return image + } + } + var proxyEnabled: CGImage { + if let image = cached.with({ $0["proxyEnabled"] }) { + return image + } else { + let image = _proxyEnabled() + _ = cached.modify { current in + var current = current + current["proxyEnabled"] = image + return current + } + return image + } + } + var proxyState: CGImage { + if let image = cached.with({ $0["proxyState"] }) { + return image + } else { + let image = _proxyState() + _ = cached.modify { current in + var current = current + current["proxyState"] = image + return current + } + return image + } + } + var proxyDeleteListItem: CGImage { + if let image = cached.with({ $0["proxyDeleteListItem"] }) { + return image + } else { + let image = _proxyDeleteListItem() + _ = cached.modify { current in + var current = current + current["proxyDeleteListItem"] = image + return current + } + return image + } + } + var proxyInfoListItem: CGImage { + if let image = cached.with({ $0["proxyInfoListItem"] }) { + return image + } else { + let image = _proxyInfoListItem() + _ = cached.modify { current in + var current = current + current["proxyInfoListItem"] = image + return current + } + return image + } + } + var proxyConnectedListItem: CGImage { + if let image = cached.with({ $0["proxyConnectedListItem"] }) { + return image + } else { + let image = _proxyConnectedListItem() + _ = cached.modify { current in + var current = current + current["proxyConnectedListItem"] = image + return current + } + return image + } + } + var proxyAddProxy: CGImage { + if let image = cached.with({ $0["proxyAddProxy"] }) { + return image + } else { + let image = _proxyAddProxy() + _ = cached.modify { current in + var current = current + current["proxyAddProxy"] = image + return current + } + return image + } + } + var proxyNextWaitingListItem: CGImage { + if let image = cached.with({ $0["proxyNextWaitingListItem"] }) { + return image + } else { + let image = _proxyNextWaitingListItem() + _ = cached.modify { current in + var current = current + current["proxyNextWaitingListItem"] = image + return current + } + return image + } + } + var passportForgotPassword: CGImage { + if let image = cached.with({ $0["passportForgotPassword"] }) { + return image + } else { + let image = _passportForgotPassword() + _ = cached.modify { current in + var current = current + current["passportForgotPassword"] = image + return current + } + return image + } + } + var confirmAppAccessoryIcon: CGImage { + if let image = cached.with({ $0["confirmAppAccessoryIcon"] }) { + return image + } else { + let image = _confirmAppAccessoryIcon() + _ = cached.modify { current in + var current = current + current["confirmAppAccessoryIcon"] = image + return current + } + return image + } + } + var passportPassport: CGImage { + if let image = cached.with({ $0["passportPassport"] }) { + return image + } else { + let image = _passportPassport() + _ = cached.modify { current in + var current = current + current["passportPassport"] = image + return current + } + return image + } + } + var passportIdCardReverse: CGImage { + if let image = cached.with({ $0["passportIdCardReverse"] }) { + return image + } else { + let image = _passportIdCardReverse() + _ = cached.modify { current in + var current = current + current["passportIdCardReverse"] = image + return current + } + return image + } + } + var passportIdCard: CGImage { + if let image = cached.with({ $0["passportIdCard"] }) { + return image + } else { + let image = _passportIdCard() + _ = cached.modify { current in + var current = current + current["passportIdCard"] = image + return current + } + return image + } + } + var passportSelfie: CGImage { + if let image = cached.with({ $0["passportSelfie"] }) { + return image + } else { + let image = _passportSelfie() + _ = cached.modify { current in + var current = current + current["passportSelfie"] = image + return current + } + return image + } + } + var passportDriverLicense: CGImage { + if let image = cached.with({ $0["passportDriverLicense"] }) { + return image + } else { + let image = _passportDriverLicense() + _ = cached.modify { current in + var current = current + current["passportDriverLicense"] = image + return current + } + return image + } + } + var chatOverlayVoiceRecording: CGImage { + if let image = cached.with({ $0["chatOverlayVoiceRecording"] }) { + return image + } else { + let image = _chatOverlayVoiceRecording() + _ = cached.modify { current in + var current = current + current["chatOverlayVoiceRecording"] = image + return current + } + return image + } + } + var chatOverlayVideoRecording: CGImage { + if let image = cached.with({ $0["chatOverlayVideoRecording"] }) { + return image + } else { + let image = _chatOverlayVideoRecording() + _ = cached.modify { current in + var current = current + current["chatOverlayVideoRecording"] = image + return current + } + return image + } + } + var chatOverlaySendRecording: CGImage { + if let image = cached.with({ $0["chatOverlaySendRecording"] }) { + return image + } else { + let image = _chatOverlaySendRecording() + _ = cached.modify { current in + var current = current + current["chatOverlaySendRecording"] = image + return current + } + return image + } + } + var chatOverlayLockArrowRecording: CGImage { + if let image = cached.with({ $0["chatOverlayLockArrowRecording"] }) { + return image + } else { + let image = _chatOverlayLockArrowRecording() + _ = cached.modify { current in + var current = current + current["chatOverlayLockArrowRecording"] = image + return current + } + return image + } + } + var chatOverlayLockerBodyRecording: CGImage { + if let image = cached.with({ $0["chatOverlayLockerBodyRecording"] }) { + return image + } else { + let image = _chatOverlayLockerBodyRecording() + _ = cached.modify { current in + var current = current + current["chatOverlayLockerBodyRecording"] = image + return current + } + return image + } + } + var chatOverlayLockerHeadRecording: CGImage { + if let image = cached.with({ $0["chatOverlayLockerHeadRecording"] }) { + return image + } else { + let image = _chatOverlayLockerHeadRecording() + _ = cached.modify { current in + var current = current + current["chatOverlayLockerHeadRecording"] = image + return current + } + return image + } + } + var locationPin: CGImage { + if let image = cached.with({ $0["locationPin"] }) { + return image + } else { + let image = _locationPin() + _ = cached.modify { current in + var current = current + current["locationPin"] = image + return current + } + return image + } + } + var locationMapPin: CGImage { + if let image = cached.with({ $0["locationMapPin"] }) { + return image + } else { + let image = _locationMapPin() + _ = cached.modify { current in + var current = current + current["locationMapPin"] = image + return current + } + return image + } + } + var locationMapLocate: CGImage { + if let image = cached.with({ $0["locationMapLocate"] }) { + return image + } else { + let image = _locationMapLocate() + _ = cached.modify { current in + var current = current + current["locationMapLocate"] = image + return current + } + return image + } + } + var locationMapLocated: CGImage { + if let image = cached.with({ $0["locationMapLocated"] }) { + return image + } else { + let image = _locationMapLocated() + _ = cached.modify { current in + var current = current + current["locationMapLocated"] = image + return current + } + return image + } + } + var passportSettings: CGImage { + if let image = cached.with({ $0["passportSettings"] }) { + return image + } else { + let image = _passportSettings() + _ = cached.modify { current in + var current = current + current["passportSettings"] = image + return current + } + return image + } + } + var passportInfo: CGImage { + if let image = cached.with({ $0["passportInfo"] }) { + return image + } else { + let image = _passportInfo() + _ = cached.modify { current in + var current = current + current["passportInfo"] = image + return current + } + return image + } + } + var editMessageMedia: CGImage { + if let image = cached.with({ $0["editMessageMedia"] }) { + return image + } else { + let image = _editMessageMedia() + _ = cached.modify { current in + var current = current + current["editMessageMedia"] = image + return current + } + return image + } + } + var playerMusicPlaceholder: CGImage { + if let image = cached.with({ $0["playerMusicPlaceholder"] }) { + return image + } else { + let image = _playerMusicPlaceholder() + _ = cached.modify { current in + var current = current + current["playerMusicPlaceholder"] = image + return current + } + return image + } + } + var chatMusicPlaceholder: CGImage { + if let image = cached.with({ $0["chatMusicPlaceholder"] }) { + return image + } else { + let image = _chatMusicPlaceholder() + _ = cached.modify { current in + var current = current + current["chatMusicPlaceholder"] = image + return current + } + return image + } + } + var chatMusicPlaceholderCap: CGImage { + if let image = cached.with({ $0["chatMusicPlaceholderCap"] }) { + return image + } else { + let image = _chatMusicPlaceholderCap() + _ = cached.modify { current in + var current = current + current["chatMusicPlaceholderCap"] = image + return current + } + return image + } + } + var searchArticle: CGImage { + if let image = cached.with({ $0["searchArticle"] }) { + return image + } else { + let image = _searchArticle() + _ = cached.modify { current in + var current = current + current["searchArticle"] = image + return current + } + return image + } + } + var searchSaved: CGImage { + if let image = cached.with({ $0["searchSaved"] }) { + return image + } else { + let image = _searchSaved() + _ = cached.modify { current in + var current = current + current["searchSaved"] = image + return current + } + return image + } + } + var archivedChats: CGImage { + if let image = cached.with({ $0["archivedChats"] }) { + return image + } else { + let image = _archivedChats() + _ = cached.modify { current in + var current = current + current["archivedChats"] = image + return current + } + return image + } + } + var hintPeerActive: CGImage { + if let image = cached.with({ $0["hintPeerActive"] }) { + return image + } else { + let image = _hintPeerActive() + _ = cached.modify { current in + var current = current + current["hintPeerActive"] = image + return current + } + return image + } + } + var hintPeerActiveSelected: CGImage { + if let image = cached.with({ $0["hintPeerActiveSelected"] }) { + return image + } else { + let image = _hintPeerActiveSelected() + _ = cached.modify { current in + var current = current + current["hintPeerActiveSelected"] = image + return current + } + return image + } + } + var chatSwiping_delete: CGImage { + if let image = cached.with({ $0["chatSwiping_delete"] }) { + return image + } else { + let image = _chatSwiping_delete() + _ = cached.modify { current in + var current = current + current["chatSwiping_delete"] = image + return current + } + return image + } + } + var chatSwiping_mute: CGImage { + if let image = cached.with({ $0["chatSwiping_mute"] }) { + return image + } else { + let image = _chatSwiping_mute() + _ = cached.modify { current in + var current = current + current["chatSwiping_mute"] = image + return current + } + return image + } + } + var chatSwiping_unmute: CGImage { + if let image = cached.with({ $0["chatSwiping_unmute"] }) { + return image + } else { + let image = _chatSwiping_unmute() + _ = cached.modify { current in + var current = current + current["chatSwiping_unmute"] = image + return current + } + return image + } + } + var chatSwiping_read: CGImage { + if let image = cached.with({ $0["chatSwiping_read"] }) { + return image + } else { + let image = _chatSwiping_read() + _ = cached.modify { current in + var current = current + current["chatSwiping_read"] = image + return current + } + return image + } + } + var chatSwiping_unread: CGImage { + if let image = cached.with({ $0["chatSwiping_unread"] }) { + return image + } else { + let image = _chatSwiping_unread() + _ = cached.modify { current in + var current = current + current["chatSwiping_unread"] = image + return current + } + return image + } + } + var chatSwiping_pin: CGImage { + if let image = cached.with({ $0["chatSwiping_pin"] }) { + return image + } else { + let image = _chatSwiping_pin() + _ = cached.modify { current in + var current = current + current["chatSwiping_pin"] = image + return current + } + return image + } + } + var chatSwiping_unpin: CGImage { + if let image = cached.with({ $0["chatSwiping_unpin"] }) { + return image + } else { + let image = _chatSwiping_unpin() + _ = cached.modify { current in + var current = current + current["chatSwiping_unpin"] = image + return current + } + return image + } + } + var chatSwiping_archive: CGImage { + if let image = cached.with({ $0["chatSwiping_archive"] }) { + return image + } else { + let image = _chatSwiping_archive() + _ = cached.modify { current in + var current = current + current["chatSwiping_archive"] = image + return current + } + return image + } + } + var chatSwiping_unarchive: CGImage { + if let image = cached.with({ $0["chatSwiping_unarchive"] }) { + return image + } else { + let image = _chatSwiping_unarchive() + _ = cached.modify { current in + var current = current + current["chatSwiping_unarchive"] = image + return current + } + return image + } + } + var galleryPrev: CGImage { + if let image = cached.with({ $0["galleryPrev"] }) { + return image + } else { + let image = _galleryPrev() + _ = cached.modify { current in + var current = current + current["galleryPrev"] = image + return current + } + return image + } + } + var galleryNext: CGImage { + if let image = cached.with({ $0["galleryNext"] }) { + return image + } else { + let image = _galleryNext() + _ = cached.modify { current in + var current = current + current["galleryNext"] = image + return current + } + return image + } + } + var galleryMore: CGImage { + if let image = cached.with({ $0["galleryMore"] }) { + return image + } else { + let image = _galleryMore() + _ = cached.modify { current in + var current = current + current["galleryMore"] = image + return current + } + return image + } + } + var galleryShare: CGImage { + if let image = cached.with({ $0["galleryShare"] }) { + return image + } else { + let image = _galleryShare() + _ = cached.modify { current in + var current = current + current["galleryShare"] = image + return current + } + return image + } + } + var galleryFastSave: CGImage { + if let image = cached.with({ $0["galleryFastSave"] }) { + return image + } else { + let image = _galleryFastSave() + _ = cached.modify { current in + var current = current + current["galleryFastSave"] = image + return current + } + return image + } + } + var galleryRotate: CGImage { + if let image = cached.with({ $0["galleryRotate"] }) { + return image + } else { + let image = _galleryRotate() + _ = cached.modify { current in + var current = current + current["galleryRotate"] = image + return current + } + return image + } + } + var galleryZoomIn: CGImage { + if let image = cached.with({ $0["galleryZoomIn"] }) { + return image + } else { + let image = _galleryZoomIn() + _ = cached.modify { current in + var current = current + current["galleryZoomIn"] = image + return current + } + return image + } + } + var galleryZoomOut: CGImage { + if let image = cached.with({ $0["galleryZoomOut"] }) { + return image + } else { + let image = _galleryZoomOut() + _ = cached.modify { current in + var current = current + current["galleryZoomOut"] = image + return current + } + return image + } + } + var editMessageCurrentPhoto: CGImage { + if let image = cached.with({ $0["editMessageCurrentPhoto"] }) { + return image + } else { + let image = _editMessageCurrentPhoto() + _ = cached.modify { current in + var current = current + current["editMessageCurrentPhoto"] = image + return current + } + return image + } + } + var videoPlayerPlay: CGImage { + if let image = cached.with({ $0["videoPlayerPlay"] }) { + return image + } else { + let image = _videoPlayerPlay() + _ = cached.modify { current in + var current = current + current["videoPlayerPlay"] = image + return current + } + return image + } + } + var videoPlayerPause: CGImage { + if let image = cached.with({ $0["videoPlayerPause"] }) { + return image + } else { + let image = _videoPlayerPause() + _ = cached.modify { current in + var current = current + current["videoPlayerPause"] = image + return current + } + return image + } + } + var videoPlayerEnterFullScreen: CGImage { + if let image = cached.with({ $0["videoPlayerEnterFullScreen"] }) { + return image + } else { + let image = _videoPlayerEnterFullScreen() + _ = cached.modify { current in + var current = current + current["videoPlayerEnterFullScreen"] = image + return current + } + return image + } + } + var videoPlayerExitFullScreen: CGImage { + if let image = cached.with({ $0["videoPlayerExitFullScreen"] }) { + return image + } else { + let image = _videoPlayerExitFullScreen() + _ = cached.modify { current in + var current = current + current["videoPlayerExitFullScreen"] = image + return current + } + return image + } + } + var videoPlayerPIPIn: CGImage { + if let image = cached.with({ $0["videoPlayerPIPIn"] }) { + return image + } else { + let image = _videoPlayerPIPIn() + _ = cached.modify { current in + var current = current + current["videoPlayerPIPIn"] = image + return current + } + return image + } + } + var videoPlayerPIPOut: CGImage { + if let image = cached.with({ $0["videoPlayerPIPOut"] }) { + return image + } else { + let image = _videoPlayerPIPOut() + _ = cached.modify { current in + var current = current + current["videoPlayerPIPOut"] = image + return current + } + return image + } + } + var videoPlayerRewind15Forward: CGImage { + if let image = cached.with({ $0["videoPlayerRewind15Forward"] }) { + return image + } else { + let image = _videoPlayerRewind15Forward() + _ = cached.modify { current in + var current = current + current["videoPlayerRewind15Forward"] = image + return current + } + return image + } + } + var videoPlayerRewind15Backward: CGImage { + if let image = cached.with({ $0["videoPlayerRewind15Backward"] }) { + return image + } else { + let image = _videoPlayerRewind15Backward() + _ = cached.modify { current in + var current = current + current["videoPlayerRewind15Backward"] = image + return current + } + return image + } + } + var videoPlayerVolume: CGImage { + if let image = cached.with({ $0["videoPlayerVolume"] }) { + return image + } else { + let image = _videoPlayerVolume() + _ = cached.modify { current in + var current = current + current["videoPlayerVolume"] = image + return current + } + return image + } + } + var videoPlayerVolumeOff: CGImage { + if let image = cached.with({ $0["videoPlayerVolumeOff"] }) { + return image + } else { + let image = _videoPlayerVolumeOff() + _ = cached.modify { current in + var current = current + current["videoPlayerVolumeOff"] = image + return current + } + return image + } + } + var videoPlayerClose: CGImage { + if let image = cached.with({ $0["videoPlayerClose"] }) { + return image + } else { + let image = _videoPlayerClose() + _ = cached.modify { current in + var current = current + current["videoPlayerClose"] = image + return current + } + return image + } + } + var videoPlayerSliderInteractor: CGImage { + if let image = cached.with({ $0["videoPlayerSliderInteractor"] }) { + return image + } else { + let image = _videoPlayerSliderInteractor() + _ = cached.modify { current in + var current = current + current["videoPlayerSliderInteractor"] = image + return current + } + return image + } + } + var streamingVideoDownload: CGImage { + if let image = cached.with({ $0["streamingVideoDownload"] }) { + return image + } else { + let image = _streamingVideoDownload() + _ = cached.modify { current in + var current = current + current["streamingVideoDownload"] = image + return current + } + return image + } + } + var videoCompactFetching: CGImage { + if let image = cached.with({ $0["videoCompactFetching"] }) { + return image + } else { + let image = _videoCompactFetching() + _ = cached.modify { current in + var current = current + current["videoCompactFetching"] = image + return current + } + return image + } + } + var compactStreamingFetchingCancel: CGImage { + if let image = cached.with({ $0["compactStreamingFetchingCancel"] }) { + return image + } else { + let image = _compactStreamingFetchingCancel() + _ = cached.modify { current in + var current = current + current["compactStreamingFetchingCancel"] = image + return current + } + return image + } + } + var customLocalizationDelete: CGImage { + if let image = cached.with({ $0["customLocalizationDelete"] }) { + return image + } else { + let image = _customLocalizationDelete() + _ = cached.modify { current in + var current = current + current["customLocalizationDelete"] = image + return current + } + return image + } + } + var pollAddOption: CGImage { + if let image = cached.with({ $0["pollAddOption"] }) { + return image + } else { + let image = _pollAddOption() + _ = cached.modify { current in + var current = current + current["pollAddOption"] = image + return current + } + return image + } + } + var pollDeleteOption: CGImage { + if let image = cached.with({ $0["pollDeleteOption"] }) { + return image + } else { + let image = _pollDeleteOption() + _ = cached.modify { current in + var current = current + current["pollDeleteOption"] = image + return current + } + return image + } + } + var resort: CGImage { + if let image = cached.with({ $0["resort"] }) { + return image + } else { + let image = _resort() + _ = cached.modify { current in + var current = current + current["resort"] = image + return current + } + return image + } + } + var chatPollVoteUnselected: CGImage { + if let image = cached.with({ $0["chatPollVoteUnselected"] }) { + return image + } else { + let image = _chatPollVoteUnselected() + _ = cached.modify { current in + var current = current + current["chatPollVoteUnselected"] = image + return current + } + return image + } + } + var chatPollVoteUnselectedBubble_incoming: CGImage { + if let image = cached.with({ $0["chatPollVoteUnselectedBubble_incoming"] }) { + return image + } else { + let image = _chatPollVoteUnselectedBubble_incoming() + _ = cached.modify { current in + var current = current + current["chatPollVoteUnselectedBubble_incoming"] = image + return current + } + return image + } + } + var chatPollVoteUnselectedBubble_outgoing: CGImage { + if let image = cached.with({ $0["chatPollVoteUnselectedBubble_outgoing"] }) { + return image + } else { + let image = _chatPollVoteUnselectedBubble_outgoing() + _ = cached.modify { current in + var current = current + current["chatPollVoteUnselectedBubble_outgoing"] = image + return current + } + return image + } + } + var peerInfoAdmins: CGImage { + if let image = cached.with({ $0["peerInfoAdmins"] }) { + return image + } else { + let image = _peerInfoAdmins() + _ = cached.modify { current in + var current = current + current["peerInfoAdmins"] = image + return current + } + return image + } + } + var peerInfoPermissions: CGImage { + if let image = cached.with({ $0["peerInfoPermissions"] }) { + return image + } else { + let image = _peerInfoPermissions() + _ = cached.modify { current in + var current = current + current["peerInfoPermissions"] = image + return current + } + return image + } + } + var peerInfoBanned: CGImage { + if let image = cached.with({ $0["peerInfoBanned"] }) { + return image + } else { + let image = _peerInfoBanned() + _ = cached.modify { current in + var current = current + current["peerInfoBanned"] = image + return current + } + return image + } + } + var peerInfoMembers: CGImage { + if let image = cached.with({ $0["peerInfoMembers"] }) { + return image + } else { + let image = _peerInfoMembers() + _ = cached.modify { current in + var current = current + current["peerInfoMembers"] = image + return current + } + return image + } + } + var chatUndoAction: CGImage { + if let image = cached.with({ $0["chatUndoAction"] }) { + return image + } else { + let image = _chatUndoAction() + _ = cached.modify { current in + var current = current + current["chatUndoAction"] = image + return current + } + return image + } + } + var appUpdate: CGImage { + if let image = cached.with({ $0["appUpdate"] }) { + return image + } else { + let image = _appUpdate() + _ = cached.modify { current in + var current = current + current["appUpdate"] = image + return current + } + return image + } + } + var inlineVideoSoundOff: CGImage { + if let image = cached.with({ $0["inlineVideoSoundOff"] }) { + return image + } else { + let image = _inlineVideoSoundOff() + _ = cached.modify { current in + var current = current + current["inlineVideoSoundOff"] = image + return current + } + return image + } + } + var inlineVideoSoundOn: CGImage { + if let image = cached.with({ $0["inlineVideoSoundOn"] }) { + return image + } else { + let image = _inlineVideoSoundOn() + _ = cached.modify { current in + var current = current + current["inlineVideoSoundOn"] = image + return current + } + return image + } + } + var logoutOptionAddAccount: CGImage { + if let image = cached.with({ $0["logoutOptionAddAccount"] }) { + return image + } else { + let image = _logoutOptionAddAccount() + _ = cached.modify { current in + var current = current + current["logoutOptionAddAccount"] = image + return current + } + return image + } + } + var logoutOptionSetPasscode: CGImage { + if let image = cached.with({ $0["logoutOptionSetPasscode"] }) { + return image + } else { + let image = _logoutOptionSetPasscode() + _ = cached.modify { current in + var current = current + current["logoutOptionSetPasscode"] = image + return current + } + return image + } + } + var logoutOptionClearCache: CGImage { + if let image = cached.with({ $0["logoutOptionClearCache"] }) { + return image + } else { + let image = _logoutOptionClearCache() + _ = cached.modify { current in + var current = current + current["logoutOptionClearCache"] = image + return current + } + return image + } + } + var logoutOptionChangePhoneNumber: CGImage { + if let image = cached.with({ $0["logoutOptionChangePhoneNumber"] }) { + return image + } else { + let image = _logoutOptionChangePhoneNumber() + _ = cached.modify { current in + var current = current + current["logoutOptionChangePhoneNumber"] = image + return current + } + return image + } + } + var logoutOptionContactSupport: CGImage { + if let image = cached.with({ $0["logoutOptionContactSupport"] }) { + return image + } else { + let image = _logoutOptionContactSupport() + _ = cached.modify { current in + var current = current + current["logoutOptionContactSupport"] = image + return current + } + return image + } + } + var disableEmojiPrediction: CGImage { + if let image = cached.with({ $0["disableEmojiPrediction"] }) { + return image + } else { + let image = _disableEmojiPrediction() + _ = cached.modify { current in + var current = current + current["disableEmojiPrediction"] = image + return current + } + return image + } + } + var scam: CGImage { + if let image = cached.with({ $0["scam"] }) { + return image + } else { + let image = _scam() + _ = cached.modify { current in + var current = current + current["scam"] = image + return current + } + return image + } + } + var scamActive: CGImage { + if let image = cached.with({ $0["scamActive"] }) { + return image + } else { + let image = _scamActive() + _ = cached.modify { current in + var current = current + current["scamActive"] = image + return current + } + return image + } + } + var chatScam: CGImage { + if let image = cached.with({ $0["chatScam"] }) { + return image + } else { + let image = _chatScam() + _ = cached.modify { current in + var current = current + current["chatScam"] = image + return current + } + return image + } + } + var fake: CGImage { + if let image = cached.with({ $0["fake"] }) { + return image + } else { + let image = _fake() + _ = cached.modify { current in + var current = current + current["fake"] = image + return current + } + return image + } + } + var fakeActive: CGImage { + if let image = cached.with({ $0["fakeActive"] }) { + return image + } else { + let image = _fakeActive() + _ = cached.modify { current in + var current = current + current["fakeActive"] = image + return current + } + return image + } + } + var chatFake: CGImage { + if let image = cached.with({ $0["chatFake"] }) { + return image + } else { + let image = _chatFake() + _ = cached.modify { current in + var current = current + current["chatFake"] = image + return current + } + return image + } + } + var chatUnarchive: CGImage { + if let image = cached.with({ $0["chatUnarchive"] }) { + return image + } else { + let image = _chatUnarchive() + _ = cached.modify { current in + var current = current + current["chatUnarchive"] = image + return current + } + return image + } + } + var chatArchive: CGImage { + if let image = cached.with({ $0["chatArchive"] }) { + return image + } else { + let image = _chatArchive() + _ = cached.modify { current in + var current = current + current["chatArchive"] = image + return current + } + return image + } + } + var privacySettings_blocked: CGImage { + if let image = cached.with({ $0["privacySettings_blocked"] }) { + return image + } else { + let image = _privacySettings_blocked() + _ = cached.modify { current in + var current = current + current["privacySettings_blocked"] = image + return current + } + return image + } + } + var privacySettings_activeSessions: CGImage { + if let image = cached.with({ $0["privacySettings_activeSessions"] }) { + return image + } else { + let image = _privacySettings_activeSessions() + _ = cached.modify { current in + var current = current + current["privacySettings_activeSessions"] = image + return current + } + return image + } + } + var privacySettings_passcode: CGImage { + if let image = cached.with({ $0["privacySettings_passcode"] }) { + return image + } else { + let image = _privacySettings_passcode() + _ = cached.modify { current in + var current = current + current["privacySettings_passcode"] = image + return current + } + return image + } + } + var privacySettings_twoStep: CGImage { + if let image = cached.with({ $0["privacySettings_twoStep"] }) { + return image + } else { + let image = _privacySettings_twoStep() + _ = cached.modify { current in + var current = current + current["privacySettings_twoStep"] = image + return current + } + return image + } + } + var deletedAccount: CGImage { + if let image = cached.with({ $0["deletedAccount"] }) { + return image + } else { + let image = _deletedAccount() + _ = cached.modify { current in + var current = current + current["deletedAccount"] = image + return current + } + return image + } + } + var stickerPackSelection: CGImage { + if let image = cached.with({ $0["stickerPackSelection"] }) { + return image + } else { + let image = _stickerPackSelection() + _ = cached.modify { current in + var current = current + current["stickerPackSelection"] = image + return current + } + return image + } + } + var stickerPackSelectionActive: CGImage { + if let image = cached.with({ $0["stickerPackSelectionActive"] }) { + return image + } else { + let image = _stickerPackSelectionActive() + _ = cached.modify { current in + var current = current + current["stickerPackSelectionActive"] = image + return current + } + return image + } + } + var entertainment_Emoji: CGImage { + if let image = cached.with({ $0["entertainment_Emoji"] }) { + return image + } else { + let image = _entertainment_Emoji() + _ = cached.modify { current in + var current = current + current["entertainment_Emoji"] = image + return current + } + return image + } + } + var entertainment_Stickers: CGImage { + if let image = cached.with({ $0["entertainment_Stickers"] }) { + return image + } else { + let image = _entertainment_Stickers() + _ = cached.modify { current in + var current = current + current["entertainment_Stickers"] = image + return current + } + return image + } + } + var entertainment_Gifs: CGImage { + if let image = cached.with({ $0["entertainment_Gifs"] }) { + return image + } else { + let image = _entertainment_Gifs() + _ = cached.modify { current in + var current = current + current["entertainment_Gifs"] = image + return current + } + return image + } + } + var entertainment_Search: CGImage { + if let image = cached.with({ $0["entertainment_Search"] }) { + return image + } else { + let image = _entertainment_Search() + _ = cached.modify { current in + var current = current + current["entertainment_Search"] = image + return current + } + return image + } + } + var entertainment_Settings: CGImage { + if let image = cached.with({ $0["entertainment_Settings"] }) { + return image + } else { + let image = _entertainment_Settings() + _ = cached.modify { current in + var current = current + current["entertainment_Settings"] = image + return current + } + return image + } + } + var entertainment_SearchCancel: CGImage { + if let image = cached.with({ $0["entertainment_SearchCancel"] }) { + return image + } else { + let image = _entertainment_SearchCancel() + _ = cached.modify { current in + var current = current + current["entertainment_SearchCancel"] = image + return current + } + return image + } + } + var scheduledAvatar: CGImage { + if let image = cached.with({ $0["scheduledAvatar"] }) { + return image + } else { + let image = _scheduledAvatar() + _ = cached.modify { current in + var current = current + current["scheduledAvatar"] = image + return current + } + return image + } + } + var scheduledInputAction: CGImage { + if let image = cached.with({ $0["scheduledInputAction"] }) { + return image + } else { + let image = _scheduledInputAction() + _ = cached.modify { current in + var current = current + current["scheduledInputAction"] = image + return current + } + return image + } + } + var verifyDialog: CGImage { + if let image = cached.with({ $0["verifyDialog"] }) { + return image + } else { + let image = _verifyDialog() + _ = cached.modify { current in + var current = current + current["verifyDialog"] = image + return current + } + return image + } + } + var verifyDialogActive: CGImage { + if let image = cached.with({ $0["verifyDialogActive"] }) { + return image + } else { + let image = _verifyDialogActive() + _ = cached.modify { current in + var current = current + current["verifyDialogActive"] = image + return current + } + return image + } + } + var chatInputScheduled: CGImage { + if let image = cached.with({ $0["chatInputScheduled"] }) { + return image + } else { + let image = _chatInputScheduled() + _ = cached.modify { current in + var current = current + current["chatInputScheduled"] = image + return current + } + return image + } + } + var appearanceAddPlatformTheme: CGImage { + if let image = cached.with({ $0["appearanceAddPlatformTheme"] }) { + return image + } else { + let image = _appearanceAddPlatformTheme() + _ = cached.modify { current in + var current = current + current["appearanceAddPlatformTheme"] = image + return current + } + return image + } + } + var wallet_close: CGImage { + if let image = cached.with({ $0["wallet_close"] }) { + return image + } else { + let image = _wallet_close() + _ = cached.modify { current in + var current = current + current["wallet_close"] = image + return current + } + return image + } + } + var wallet_qr: CGImage { + if let image = cached.with({ $0["wallet_qr"] }) { + return image + } else { + let image = _wallet_qr() + _ = cached.modify { current in + var current = current + current["wallet_qr"] = image + return current + } + return image + } + } + var wallet_receive: CGImage { + if let image = cached.with({ $0["wallet_receive"] }) { + return image + } else { + let image = _wallet_receive() + _ = cached.modify { current in + var current = current + current["wallet_receive"] = image + return current + } + return image + } + } + var wallet_send: CGImage { + if let image = cached.with({ $0["wallet_send"] }) { + return image + } else { + let image = _wallet_send() + _ = cached.modify { current in + var current = current + current["wallet_send"] = image + return current + } + return image + } + } + var wallet_settings: CGImage { + if let image = cached.with({ $0["wallet_settings"] }) { + return image + } else { + let image = _wallet_settings() + _ = cached.modify { current in + var current = current + current["wallet_settings"] = image + return current + } + return image + } + } + var wallet_update: CGImage { + if let image = cached.with({ $0["wallet_update"] }) { + return image + } else { + let image = _wallet_update() + _ = cached.modify { current in + var current = current + current["wallet_update"] = image + return current + } + return image + } + } + var wallet_passcode_visible: CGImage { + if let image = cached.with({ $0["wallet_passcode_visible"] }) { + return image + } else { + let image = _wallet_passcode_visible() + _ = cached.modify { current in + var current = current + current["wallet_passcode_visible"] = image + return current + } + return image + } + } + var wallet_passcode_hidden: CGImage { + if let image = cached.with({ $0["wallet_passcode_hidden"] }) { + return image + } else { + let image = _wallet_passcode_hidden() + _ = cached.modify { current in + var current = current + current["wallet_passcode_hidden"] = image + return current + } + return image + } + } + var wallpaper_color_close: CGImage { + if let image = cached.with({ $0["wallpaper_color_close"] }) { + return image + } else { + let image = _wallpaper_color_close() + _ = cached.modify { current in + var current = current + current["wallpaper_color_close"] = image + return current + } + return image + } + } + var wallpaper_color_add: CGImage { + if let image = cached.with({ $0["wallpaper_color_add"] }) { + return image + } else { + let image = _wallpaper_color_add() + _ = cached.modify { current in + var current = current + current["wallpaper_color_add"] = image + return current + } + return image + } + } + var wallpaper_color_swap: CGImage { + if let image = cached.with({ $0["wallpaper_color_swap"] }) { + return image + } else { + let image = _wallpaper_color_swap() + _ = cached.modify { current in + var current = current + current["wallpaper_color_swap"] = image + return current + } + return image + } + } + var wallpaper_color_rotate: CGImage { + if let image = cached.with({ $0["wallpaper_color_rotate"] }) { + return image + } else { + let image = _wallpaper_color_rotate() + _ = cached.modify { current in + var current = current + current["wallpaper_color_rotate"] = image + return current + } + return image + } + } + var wallpaper_color_play: CGImage { + if let image = cached.with({ $0["wallpaper_color_play"] }) { + return image + } else { + let image = _wallpaper_color_play() + _ = cached.modify { current in + var current = current + current["wallpaper_color_play"] = image + return current + } + return image + } + } + var login_cap: CGImage { + if let image = cached.with({ $0["login_cap"] }) { + return image + } else { + let image = _login_cap() + _ = cached.modify { current in + var current = current + current["login_cap"] = image + return current + } + return image + } + } + var login_qr_cap: CGImage { + if let image = cached.with({ $0["login_qr_cap"] }) { + return image + } else { + let image = _login_qr_cap() + _ = cached.modify { current in + var current = current + current["login_qr_cap"] = image + return current + } + return image + } + } + var login_qr_empty_cap: CGImage { + if let image = cached.with({ $0["login_qr_empty_cap"] }) { + return image + } else { + let image = _login_qr_empty_cap() + _ = cached.modify { current in + var current = current + current["login_qr_empty_cap"] = image + return current + } + return image + } + } + var chat_failed_scroller: CGImage { + if let image = cached.with({ $0["chat_failed_scroller"] }) { + return image + } else { + let image = _chat_failed_scroller() + _ = cached.modify { current in + var current = current + current["chat_failed_scroller"] = image + return current + } + return image + } + } + var chat_failed_scroller_active: CGImage { + if let image = cached.with({ $0["chat_failed_scroller_active"] }) { + return image + } else { + let image = _chat_failed_scroller_active() + _ = cached.modify { current in + var current = current + current["chat_failed_scroller_active"] = image + return current + } + return image + } + } + var poll_quiz_unselected: CGImage { + if let image = cached.with({ $0["poll_quiz_unselected"] }) { + return image + } else { + let image = _poll_quiz_unselected() + _ = cached.modify { current in + var current = current + current["poll_quiz_unselected"] = image + return current + } + return image + } + } + var poll_selected: CGImage { + if let image = cached.with({ $0["poll_selected"] }) { + return image + } else { + let image = _poll_selected() + _ = cached.modify { current in + var current = current + current["poll_selected"] = image + return current + } + return image + } + } + var poll_selection: CGImage { + if let image = cached.with({ $0["poll_selection"] }) { + return image + } else { + let image = _poll_selection() + _ = cached.modify { current in + var current = current + current["poll_selection"] = image + return current + } + return image + } + } + var poll_selected_correct: CGImage { + if let image = cached.with({ $0["poll_selected_correct"] }) { + return image + } else { + let image = _poll_selected_correct() + _ = cached.modify { current in + var current = current + current["poll_selected_correct"] = image + return current + } + return image + } + } + var poll_selected_incorrect: CGImage { + if let image = cached.with({ $0["poll_selected_incorrect"] }) { + return image + } else { + let image = _poll_selected_incorrect() + _ = cached.modify { current in + var current = current + current["poll_selected_incorrect"] = image + return current + } + return image + } + } + var poll_selected_incoming: CGImage { + if let image = cached.with({ $0["poll_selected_incoming"] }) { + return image + } else { + let image = _poll_selected_incoming() + _ = cached.modify { current in + var current = current + current["poll_selected_incoming"] = image + return current + } + return image + } + } + var poll_selection_incoming: CGImage { + if let image = cached.with({ $0["poll_selection_incoming"] }) { + return image + } else { + let image = _poll_selection_incoming() + _ = cached.modify { current in + var current = current + current["poll_selection_incoming"] = image + return current + } + return image + } + } + var poll_selected_correct_incoming: CGImage { + if let image = cached.with({ $0["poll_selected_correct_incoming"] }) { + return image + } else { + let image = _poll_selected_correct_incoming() + _ = cached.modify { current in + var current = current + current["poll_selected_correct_incoming"] = image + return current + } + return image + } + } + var poll_selected_incorrect_incoming: CGImage { + if let image = cached.with({ $0["poll_selected_incorrect_incoming"] }) { + return image + } else { + let image = _poll_selected_incorrect_incoming() + _ = cached.modify { current in + var current = current + current["poll_selected_incorrect_incoming"] = image + return current + } + return image + } + } + var poll_selected_outgoing: CGImage { + if let image = cached.with({ $0["poll_selected_outgoing"] }) { + return image + } else { + let image = _poll_selected_outgoing() + _ = cached.modify { current in + var current = current + current["poll_selected_outgoing"] = image + return current + } + return image + } + } + var poll_selection_outgoing: CGImage { + if let image = cached.with({ $0["poll_selection_outgoing"] }) { + return image + } else { + let image = _poll_selection_outgoing() + _ = cached.modify { current in + var current = current + current["poll_selection_outgoing"] = image + return current + } + return image + } + } + var poll_selected_correct_outgoing: CGImage { + if let image = cached.with({ $0["poll_selected_correct_outgoing"] }) { + return image + } else { + let image = _poll_selected_correct_outgoing() + _ = cached.modify { current in + var current = current + current["poll_selected_correct_outgoing"] = image + return current + } + return image + } + } + var poll_selected_incorrect_outgoing: CGImage { + if let image = cached.with({ $0["poll_selected_incorrect_outgoing"] }) { + return image + } else { + let image = _poll_selected_incorrect_outgoing() + _ = cached.modify { current in + var current = current + current["poll_selected_incorrect_outgoing"] = image + return current + } + return image + } + } + var chat_filter_edit: CGImage { + if let image = cached.with({ $0["chat_filter_edit"] }) { + return image + } else { + let image = _chat_filter_edit() + _ = cached.modify { current in + var current = current + current["chat_filter_edit"] = image + return current + } + return image + } + } + var chat_filter_add: CGImage { + if let image = cached.with({ $0["chat_filter_add"] }) { + return image + } else { + let image = _chat_filter_add() + _ = cached.modify { current in + var current = current + current["chat_filter_add"] = image + return current + } + return image + } + } + var chat_filter_bots: CGImage { + if let image = cached.with({ $0["chat_filter_bots"] }) { + return image + } else { + let image = _chat_filter_bots() + _ = cached.modify { current in + var current = current + current["chat_filter_bots"] = image + return current + } + return image + } + } + var chat_filter_channels: CGImage { + if let image = cached.with({ $0["chat_filter_channels"] }) { + return image + } else { + let image = _chat_filter_channels() + _ = cached.modify { current in + var current = current + current["chat_filter_channels"] = image + return current + } + return image + } + } + var chat_filter_custom: CGImage { + if let image = cached.with({ $0["chat_filter_custom"] }) { + return image + } else { + let image = _chat_filter_custom() + _ = cached.modify { current in + var current = current + current["chat_filter_custom"] = image + return current + } + return image + } + } + var chat_filter_groups: CGImage { + if let image = cached.with({ $0["chat_filter_groups"] }) { + return image + } else { + let image = _chat_filter_groups() + _ = cached.modify { current in + var current = current + current["chat_filter_groups"] = image + return current + } + return image + } + } + var chat_filter_muted: CGImage { + if let image = cached.with({ $0["chat_filter_muted"] }) { + return image + } else { + let image = _chat_filter_muted() + _ = cached.modify { current in + var current = current + current["chat_filter_muted"] = image + return current + } + return image + } + } + var chat_filter_private_chats: CGImage { + if let image = cached.with({ $0["chat_filter_private_chats"] }) { + return image + } else { + let image = _chat_filter_private_chats() + _ = cached.modify { current in + var current = current + current["chat_filter_private_chats"] = image + return current + } + return image + } + } + var chat_filter_read: CGImage { + if let image = cached.with({ $0["chat_filter_read"] }) { + return image + } else { + let image = _chat_filter_read() + _ = cached.modify { current in + var current = current + current["chat_filter_read"] = image + return current + } + return image + } + } + var chat_filter_secret_chats: CGImage { + if let image = cached.with({ $0["chat_filter_secret_chats"] }) { + return image + } else { + let image = _chat_filter_secret_chats() + _ = cached.modify { current in + var current = current + current["chat_filter_secret_chats"] = image + return current + } + return image + } + } + var chat_filter_unmuted: CGImage { + if let image = cached.with({ $0["chat_filter_unmuted"] }) { + return image + } else { + let image = _chat_filter_unmuted() + _ = cached.modify { current in + var current = current + current["chat_filter_unmuted"] = image + return current + } + return image + } + } + var chat_filter_unread: CGImage { + if let image = cached.with({ $0["chat_filter_unread"] }) { + return image + } else { + let image = _chat_filter_unread() + _ = cached.modify { current in + var current = current + current["chat_filter_unread"] = image + return current + } + return image + } + } + var chat_filter_large_groups: CGImage { + if let image = cached.with({ $0["chat_filter_large_groups"] }) { + return image + } else { + let image = _chat_filter_large_groups() + _ = cached.modify { current in + var current = current + current["chat_filter_large_groups"] = image + return current + } + return image + } + } + var chat_filter_non_contacts: CGImage { + if let image = cached.with({ $0["chat_filter_non_contacts"] }) { + return image + } else { + let image = _chat_filter_non_contacts() + _ = cached.modify { current in + var current = current + current["chat_filter_non_contacts"] = image + return current + } + return image + } + } + var chat_filter_archive: CGImage { + if let image = cached.with({ $0["chat_filter_archive"] }) { + return image + } else { + let image = _chat_filter_archive() + _ = cached.modify { current in + var current = current + current["chat_filter_archive"] = image + return current + } + return image + } + } + var chat_filter_bots_avatar: CGImage { + if let image = cached.with({ $0["chat_filter_bots_avatar"] }) { + return image + } else { + let image = _chat_filter_bots_avatar() + _ = cached.modify { current in + var current = current + current["chat_filter_bots_avatar"] = image + return current + } + return image + } + } + var chat_filter_channels_avatar: CGImage { + if let image = cached.with({ $0["chat_filter_channels_avatar"] }) { + return image + } else { + let image = _chat_filter_channels_avatar() + _ = cached.modify { current in + var current = current + current["chat_filter_channels_avatar"] = image + return current + } + return image + } + } + var chat_filter_custom_avatar: CGImage { + if let image = cached.with({ $0["chat_filter_custom_avatar"] }) { + return image + } else { + let image = _chat_filter_custom_avatar() + _ = cached.modify { current in + var current = current + current["chat_filter_custom_avatar"] = image + return current + } + return image + } + } + var chat_filter_groups_avatar: CGImage { + if let image = cached.with({ $0["chat_filter_groups_avatar"] }) { + return image + } else { + let image = _chat_filter_groups_avatar() + _ = cached.modify { current in + var current = current + current["chat_filter_groups_avatar"] = image + return current + } + return image + } + } + var chat_filter_muted_avatar: CGImage { + if let image = cached.with({ $0["chat_filter_muted_avatar"] }) { + return image + } else { + let image = _chat_filter_muted_avatar() + _ = cached.modify { current in + var current = current + current["chat_filter_muted_avatar"] = image + return current + } + return image + } + } + var chat_filter_private_chats_avatar: CGImage { + if let image = cached.with({ $0["chat_filter_private_chats_avatar"] }) { + return image + } else { + let image = _chat_filter_private_chats_avatar() + _ = cached.modify { current in + var current = current + current["chat_filter_private_chats_avatar"] = image + return current + } + return image + } + } + var chat_filter_read_avatar: CGImage { + if let image = cached.with({ $0["chat_filter_read_avatar"] }) { + return image + } else { + let image = _chat_filter_read_avatar() + _ = cached.modify { current in + var current = current + current["chat_filter_read_avatar"] = image + return current + } + return image + } + } + var chat_filter_secret_chats_avatar: CGImage { + if let image = cached.with({ $0["chat_filter_secret_chats_avatar"] }) { + return image + } else { + let image = _chat_filter_secret_chats_avatar() + _ = cached.modify { current in + var current = current + current["chat_filter_secret_chats_avatar"] = image + return current + } + return image + } + } + var chat_filter_unmuted_avatar: CGImage { + if let image = cached.with({ $0["chat_filter_unmuted_avatar"] }) { + return image + } else { + let image = _chat_filter_unmuted_avatar() + _ = cached.modify { current in + var current = current + current["chat_filter_unmuted_avatar"] = image + return current + } + return image + } + } + var chat_filter_unread_avatar: CGImage { + if let image = cached.with({ $0["chat_filter_unread_avatar"] }) { + return image + } else { + let image = _chat_filter_unread_avatar() + _ = cached.modify { current in + var current = current + current["chat_filter_unread_avatar"] = image + return current + } + return image + } + } + var chat_filter_large_groups_avatar: CGImage { + if let image = cached.with({ $0["chat_filter_large_groups_avatar"] }) { + return image + } else { + let image = _chat_filter_large_groups_avatar() + _ = cached.modify { current in + var current = current + current["chat_filter_large_groups_avatar"] = image + return current + } + return image + } + } + var chat_filter_non_contacts_avatar: CGImage { + if let image = cached.with({ $0["chat_filter_non_contacts_avatar"] }) { + return image + } else { + let image = _chat_filter_non_contacts_avatar() + _ = cached.modify { current in + var current = current + current["chat_filter_non_contacts_avatar"] = image + return current + } + return image + } + } + var chat_filter_archive_avatar: CGImage { + if let image = cached.with({ $0["chat_filter_archive_avatar"] }) { + return image + } else { + let image = _chat_filter_archive_avatar() + _ = cached.modify { current in + var current = current + current["chat_filter_archive_avatar"] = image + return current + } + return image + } + } + var group_invite_via_link: CGImage { + if let image = cached.with({ $0["group_invite_via_link"] }) { + return image + } else { + let image = _group_invite_via_link() + _ = cached.modify { current in + var current = current + current["group_invite_via_link"] = image + return current + } + return image + } + } + var tab_contacts: CGImage { + if let image = cached.with({ $0["tab_contacts"] }) { + return image + } else { + let image = _tab_contacts() + _ = cached.modify { current in + var current = current + current["tab_contacts"] = image + return current + } + return image + } + } + var tab_contacts_active: CGImage { + if let image = cached.with({ $0["tab_contacts_active"] }) { + return image + } else { + let image = _tab_contacts_active() + _ = cached.modify { current in + var current = current + current["tab_contacts_active"] = image + return current + } + return image + } + } + var tab_calls: CGImage { + if let image = cached.with({ $0["tab_calls"] }) { + return image + } else { + let image = _tab_calls() + _ = cached.modify { current in + var current = current + current["tab_calls"] = image + return current + } + return image + } + } + var tab_calls_active: CGImage { + if let image = cached.with({ $0["tab_calls_active"] }) { + return image + } else { + let image = _tab_calls_active() + _ = cached.modify { current in + var current = current + current["tab_calls_active"] = image + return current + } + return image + } + } + var tab_chats: CGImage { + if let image = cached.with({ $0["tab_chats"] }) { + return image + } else { + let image = _tab_chats() + _ = cached.modify { current in + var current = current + current["tab_chats"] = image + return current + } + return image + } + } + var tab_chats_active: CGImage { + if let image = cached.with({ $0["tab_chats_active"] }) { + return image + } else { + let image = _tab_chats_active() + _ = cached.modify { current in + var current = current + current["tab_chats_active"] = image + return current + } + return image + } + } + var tab_chats_active_filters: CGImage { + if let image = cached.with({ $0["tab_chats_active_filters"] }) { + return image + } else { + let image = _tab_chats_active_filters() + _ = cached.modify { current in + var current = current + current["tab_chats_active_filters"] = image + return current + } + return image + } + } + var tab_settings: CGImage { + if let image = cached.with({ $0["tab_settings"] }) { + return image + } else { + let image = _tab_settings() + _ = cached.modify { current in + var current = current + current["tab_settings"] = image + return current + } + return image + } + } + var tab_settings_active: CGImage { + if let image = cached.with({ $0["tab_settings_active"] }) { + return image + } else { + let image = _tab_settings_active() + _ = cached.modify { current in + var current = current + current["tab_settings_active"] = image + return current + } + return image + } + } + var profile_add_member: CGImage { + if let image = cached.with({ $0["profile_add_member"] }) { + return image + } else { + let image = _profile_add_member() + _ = cached.modify { current in + var current = current + current["profile_add_member"] = image + return current + } + return image + } + } + var profile_call: CGImage { + if let image = cached.with({ $0["profile_call"] }) { + return image + } else { + let image = _profile_call() + _ = cached.modify { current in + var current = current + current["profile_call"] = image + return current + } + return image + } + } + var profile_video_call: CGImage { + if let image = cached.with({ $0["profile_video_call"] }) { + return image + } else { + let image = _profile_video_call() + _ = cached.modify { current in + var current = current + current["profile_video_call"] = image + return current + } + return image + } + } + var profile_leave: CGImage { + if let image = cached.with({ $0["profile_leave"] }) { + return image + } else { + let image = _profile_leave() + _ = cached.modify { current in + var current = current + current["profile_leave"] = image + return current + } + return image + } + } + var profile_message: CGImage { + if let image = cached.with({ $0["profile_message"] }) { + return image + } else { + let image = _profile_message() + _ = cached.modify { current in + var current = current + current["profile_message"] = image + return current + } + return image + } + } + var profile_more: CGImage { + if let image = cached.with({ $0["profile_more"] }) { + return image + } else { + let image = _profile_more() + _ = cached.modify { current in + var current = current + current["profile_more"] = image + return current + } + return image + } + } + var profile_mute: CGImage { + if let image = cached.with({ $0["profile_mute"] }) { + return image + } else { + let image = _profile_mute() + _ = cached.modify { current in + var current = current + current["profile_mute"] = image + return current + } + return image + } + } + var profile_unmute: CGImage { + if let image = cached.with({ $0["profile_unmute"] }) { + return image + } else { + let image = _profile_unmute() + _ = cached.modify { current in + var current = current + current["profile_unmute"] = image + return current + } + return image + } + } + var profile_search: CGImage { + if let image = cached.with({ $0["profile_search"] }) { + return image + } else { + let image = _profile_search() + _ = cached.modify { current in + var current = current + current["profile_search"] = image + return current + } + return image + } + } + var profile_secret_chat: CGImage { + if let image = cached.with({ $0["profile_secret_chat"] }) { + return image + } else { + let image = _profile_secret_chat() + _ = cached.modify { current in + var current = current + current["profile_secret_chat"] = image + return current + } + return image + } + } + var profile_edit_photo: CGImage { + if let image = cached.with({ $0["profile_edit_photo"] }) { + return image + } else { + let image = _profile_edit_photo() + _ = cached.modify { current in + var current = current + current["profile_edit_photo"] = image + return current + } + return image + } + } + var profile_block: CGImage { + if let image = cached.with({ $0["profile_block"] }) { + return image + } else { + let image = _profile_block() + _ = cached.modify { current in + var current = current + current["profile_block"] = image + return current + } + return image + } + } + var profile_report: CGImage { + if let image = cached.with({ $0["profile_report"] }) { + return image + } else { + let image = _profile_report() + _ = cached.modify { current in + var current = current + current["profile_report"] = image + return current + } + return image + } + } + var profile_share: CGImage { + if let image = cached.with({ $0["profile_share"] }) { + return image + } else { + let image = _profile_share() + _ = cached.modify { current in + var current = current + current["profile_share"] = image + return current + } + return image + } + } + var profile_stats: CGImage { + if let image = cached.with({ $0["profile_stats"] }) { + return image + } else { + let image = _profile_stats() + _ = cached.modify { current in + var current = current + current["profile_stats"] = image + return current + } + return image + } + } + var profile_unblock: CGImage { + if let image = cached.with({ $0["profile_unblock"] }) { + return image + } else { + let image = _profile_unblock() + _ = cached.modify { current in + var current = current + current["profile_unblock"] = image + return current + } + return image + } + } + var chat_quiz_explanation: CGImage { + if let image = cached.with({ $0["chat_quiz_explanation"] }) { + return image + } else { + let image = _chat_quiz_explanation() + _ = cached.modify { current in + var current = current + current["chat_quiz_explanation"] = image + return current + } + return image + } + } + var chat_quiz_explanation_bubble_incoming: CGImage { + if let image = cached.with({ $0["chat_quiz_explanation_bubble_incoming"] }) { + return image + } else { + let image = _chat_quiz_explanation_bubble_incoming() + _ = cached.modify { current in + var current = current + current["chat_quiz_explanation_bubble_incoming"] = image + return current + } + return image + } + } + var chat_quiz_explanation_bubble_outgoing: CGImage { + if let image = cached.with({ $0["chat_quiz_explanation_bubble_outgoing"] }) { + return image + } else { + let image = _chat_quiz_explanation_bubble_outgoing() + _ = cached.modify { current in + var current = current + current["chat_quiz_explanation_bubble_outgoing"] = image + return current + } + return image + } + } + var stickers_add_featured: CGImage { + if let image = cached.with({ $0["stickers_add_featured"] }) { + return image + } else { + let image = _stickers_add_featured() + _ = cached.modify { current in + var current = current + current["stickers_add_featured"] = image + return current + } + return image + } + } + var stickers_add_featured_unread: CGImage { + if let image = cached.with({ $0["stickers_add_featured_unread"] }) { + return image + } else { + let image = _stickers_add_featured_unread() + _ = cached.modify { current in + var current = current + current["stickers_add_featured_unread"] = image + return current + } + return image + } + } + var channel_info_promo: CGImage { + if let image = cached.with({ $0["channel_info_promo"] }) { + return image + } else { + let image = _channel_info_promo() + _ = cached.modify { current in + var current = current + current["channel_info_promo"] = image + return current + } + return image + } + } + var channel_info_promo_bubble_incoming: CGImage { + if let image = cached.with({ $0["channel_info_promo_bubble_incoming"] }) { + return image + } else { + let image = _channel_info_promo_bubble_incoming() + _ = cached.modify { current in + var current = current + current["channel_info_promo_bubble_incoming"] = image + return current + } + return image + } + } + var channel_info_promo_bubble_outgoing: CGImage { + if let image = cached.with({ $0["channel_info_promo_bubble_outgoing"] }) { + return image + } else { + let image = _channel_info_promo_bubble_outgoing() + _ = cached.modify { current in + var current = current + current["channel_info_promo_bubble_outgoing"] = image + return current + } + return image + } + } + var chat_share_message: CGImage { + if let image = cached.with({ $0["chat_share_message"] }) { + return image + } else { + let image = _chat_share_message() + _ = cached.modify { current in + var current = current + current["chat_share_message"] = image + return current + } + return image + } + } + var chat_goto_message: CGImage { + if let image = cached.with({ $0["chat_goto_message"] }) { + return image + } else { + let image = _chat_goto_message() + _ = cached.modify { current in + var current = current + current["chat_goto_message"] = image + return current + } + return image + } + } + var chat_swipe_reply: CGImage { + if let image = cached.with({ $0["chat_swipe_reply"] }) { + return image + } else { + let image = _chat_swipe_reply() + _ = cached.modify { current in + var current = current + current["chat_swipe_reply"] = image + return current + } + return image + } + } + var chat_like_message: CGImage { + if let image = cached.with({ $0["chat_like_message"] }) { + return image + } else { + let image = _chat_like_message() + _ = cached.modify { current in + var current = current + current["chat_like_message"] = image + return current + } + return image + } + } + var chat_like_message_unlike: CGImage { + if let image = cached.with({ $0["chat_like_message_unlike"] }) { + return image + } else { + let image = _chat_like_message_unlike() + _ = cached.modify { current in + var current = current + current["chat_like_message_unlike"] = image + return current + } + return image + } + } + var chat_like_inside: CGImage { + if let image = cached.with({ $0["chat_like_inside"] }) { + return image + } else { + let image = _chat_like_inside() + _ = cached.modify { current in + var current = current + current["chat_like_inside"] = image + return current + } + return image + } + } + var chat_like_inside_bubble_incoming: CGImage { + if let image = cached.with({ $0["chat_like_inside_bubble_incoming"] }) { + return image + } else { + let image = _chat_like_inside_bubble_incoming() + _ = cached.modify { current in + var current = current + current["chat_like_inside_bubble_incoming"] = image + return current + } + return image + } + } + var chat_like_inside_bubble_outgoing: CGImage { + if let image = cached.with({ $0["chat_like_inside_bubble_outgoing"] }) { + return image + } else { + let image = _chat_like_inside_bubble_outgoing() + _ = cached.modify { current in + var current = current + current["chat_like_inside_bubble_outgoing"] = image + return current + } + return image + } + } + var chat_like_inside_bubble_overlay: CGImage { + if let image = cached.with({ $0["chat_like_inside_bubble_overlay"] }) { + return image + } else { + let image = _chat_like_inside_bubble_overlay() + _ = cached.modify { current in + var current = current + current["chat_like_inside_bubble_overlay"] = image + return current + } + return image + } + } + var chat_like_inside_empty: CGImage { + if let image = cached.with({ $0["chat_like_inside_empty"] }) { + return image + } else { + let image = _chat_like_inside_empty() + _ = cached.modify { current in + var current = current + current["chat_like_inside_empty"] = image + return current + } + return image + } + } + var chat_like_inside_empty_bubble_incoming: CGImage { + if let image = cached.with({ $0["chat_like_inside_empty_bubble_incoming"] }) { + return image + } else { + let image = _chat_like_inside_empty_bubble_incoming() + _ = cached.modify { current in + var current = current + current["chat_like_inside_empty_bubble_incoming"] = image + return current + } + return image + } + } + var chat_like_inside_empty_bubble_outgoing: CGImage { + if let image = cached.with({ $0["chat_like_inside_empty_bubble_outgoing"] }) { + return image + } else { + let image = _chat_like_inside_empty_bubble_outgoing() + _ = cached.modify { current in + var current = current + current["chat_like_inside_empty_bubble_outgoing"] = image + return current + } + return image + } + } + var chat_like_inside_empty_bubble_overlay: CGImage { + if let image = cached.with({ $0["chat_like_inside_empty_bubble_overlay"] }) { + return image + } else { + let image = _chat_like_inside_empty_bubble_overlay() + _ = cached.modify { current in + var current = current + current["chat_like_inside_empty_bubble_overlay"] = image + return current + } + return image + } + } + var gif_trending: CGImage { + if let image = cached.with({ $0["gif_trending"] }) { + return image + } else { + let image = _gif_trending() + _ = cached.modify { current in + var current = current + current["gif_trending"] = image + return current + } + return image + } + } + var chat_list_thumb_play: CGImage { + if let image = cached.with({ $0["chat_list_thumb_play"] }) { + return image + } else { + let image = _chat_list_thumb_play() + _ = cached.modify { current in + var current = current + current["chat_list_thumb_play"] = image + return current + } + return image + } + } + var call_tooltip_battery_low: CGImage { + if let image = cached.with({ $0["call_tooltip_battery_low"] }) { + return image + } else { + let image = _call_tooltip_battery_low() + _ = cached.modify { current in + var current = current + current["call_tooltip_battery_low"] = image + return current + } + return image + } + } + var call_tooltip_camera_off: CGImage { + if let image = cached.with({ $0["call_tooltip_camera_off"] }) { + return image + } else { + let image = _call_tooltip_camera_off() + _ = cached.modify { current in + var current = current + current["call_tooltip_camera_off"] = image + return current + } + return image + } + } + var call_tooltip_micro_off: CGImage { + if let image = cached.with({ $0["call_tooltip_micro_off"] }) { + return image + } else { + let image = _call_tooltip_micro_off() + _ = cached.modify { current in + var current = current + current["call_tooltip_micro_off"] = image + return current + } + return image + } + } + var call_screen_sharing: CGImage { + if let image = cached.with({ $0["call_screen_sharing"] }) { + return image + } else { + let image = _call_screen_sharing() + _ = cached.modify { current in + var current = current + current["call_screen_sharing"] = image + return current + } + return image + } + } + var call_screen_sharing_active: CGImage { + if let image = cached.with({ $0["call_screen_sharing_active"] }) { + return image + } else { + let image = _call_screen_sharing_active() + _ = cached.modify { current in + var current = current + current["call_screen_sharing_active"] = image + return current + } + return image + } + } + var call_screen_settings: CGImage { + if let image = cached.with({ $0["call_screen_settings"] }) { + return image + } else { + let image = _call_screen_settings() + _ = cached.modify { current in + var current = current + current["call_screen_settings"] = image + return current + } + return image + } + } + var search_filter: CGImage { + if let image = cached.with({ $0["search_filter"] }) { + return image + } else { + let image = _search_filter() + _ = cached.modify { current in + var current = current + current["search_filter"] = image + return current + } + return image + } + } + var search_filter_media: CGImage { + if let image = cached.with({ $0["search_filter_media"] }) { + return image + } else { + let image = _search_filter_media() + _ = cached.modify { current in + var current = current + current["search_filter_media"] = image + return current + } + return image + } + } + var search_filter_files: CGImage { + if let image = cached.with({ $0["search_filter_files"] }) { + return image + } else { + let image = _search_filter_files() + _ = cached.modify { current in + var current = current + current["search_filter_files"] = image + return current + } + return image + } + } + var search_filter_links: CGImage { + if let image = cached.with({ $0["search_filter_links"] }) { + return image + } else { + let image = _search_filter_links() + _ = cached.modify { current in + var current = current + current["search_filter_links"] = image + return current + } + return image + } + } + var search_filter_music: CGImage { + if let image = cached.with({ $0["search_filter_music"] }) { + return image + } else { + let image = _search_filter_music() + _ = cached.modify { current in + var current = current + current["search_filter_music"] = image + return current + } + return image + } + } + var search_filter_add_peer: CGImage { + if let image = cached.with({ $0["search_filter_add_peer"] }) { + return image + } else { + let image = _search_filter_add_peer() + _ = cached.modify { current in + var current = current + current["search_filter_add_peer"] = image + return current + } + return image + } + } + var search_filter_add_peer_active: CGImage { + if let image = cached.with({ $0["search_filter_add_peer_active"] }) { + return image + } else { + let image = _search_filter_add_peer_active() + _ = cached.modify { current in + var current = current + current["search_filter_add_peer_active"] = image + return current + } + return image + } + } + var chat_reply_count_bubble_incoming: CGImage { + if let image = cached.with({ $0["chat_reply_count_bubble_incoming"] }) { + return image + } else { + let image = _chat_reply_count_bubble_incoming() + _ = cached.modify { current in + var current = current + current["chat_reply_count_bubble_incoming"] = image + return current + } + return image + } + } + var chat_reply_count_bubble_outgoing: CGImage { + if let image = cached.with({ $0["chat_reply_count_bubble_outgoing"] }) { + return image + } else { + let image = _chat_reply_count_bubble_outgoing() + _ = cached.modify { current in + var current = current + current["chat_reply_count_bubble_outgoing"] = image + return current + } + return image + } + } + var chat_reply_count: CGImage { + if let image = cached.with({ $0["chat_reply_count"] }) { + return image + } else { + let image = _chat_reply_count() + _ = cached.modify { current in + var current = current + current["chat_reply_count"] = image + return current + } + return image + } + } + var chat_reply_count_overlay: CGImage { + if let image = cached.with({ $0["chat_reply_count_overlay"] }) { + return image + } else { + let image = _chat_reply_count_overlay() + _ = cached.modify { current in + var current = current + current["chat_reply_count_overlay"] = image + return current + } + return image + } + } + var channel_comments_bubble: CGImage { + if let image = cached.with({ $0["channel_comments_bubble"] }) { + return image + } else { + let image = _channel_comments_bubble() + _ = cached.modify { current in + var current = current + current["channel_comments_bubble"] = image + return current + } + return image + } + } + var channel_comments_bubble_next: CGImage { + if let image = cached.with({ $0["channel_comments_bubble_next"] }) { + return image + } else { + let image = _channel_comments_bubble_next() + _ = cached.modify { current in + var current = current + current["channel_comments_bubble_next"] = image + return current + } + return image + } + } + var channel_comments_list: CGImage { + if let image = cached.with({ $0["channel_comments_list"] }) { + return image + } else { + let image = _channel_comments_list() + _ = cached.modify { current in + var current = current + current["channel_comments_list"] = image + return current + } + return image + } + } + var channel_comments_overlay: CGImage { + if let image = cached.with({ $0["channel_comments_overlay"] }) { + return image + } else { + let image = _channel_comments_overlay() + _ = cached.modify { current in + var current = current + current["channel_comments_overlay"] = image + return current + } + return image + } + } + var chat_replies_avatar: CGImage { + if let image = cached.with({ $0["chat_replies_avatar"] }) { + return image + } else { + let image = _chat_replies_avatar() + _ = cached.modify { current in + var current = current + current["chat_replies_avatar"] = image + return current + } + return image + } + } + var group_selection_foreground: CGImage { + if let image = cached.with({ $0["group_selection_foreground"] }) { + return image + } else { + let image = _group_selection_foreground() + _ = cached.modify { current in + var current = current + current["group_selection_foreground"] = image + return current + } + return image + } + } + var group_selection_foreground_bubble_incoming: CGImage { + if let image = cached.with({ $0["group_selection_foreground_bubble_incoming"] }) { + return image + } else { + let image = _group_selection_foreground_bubble_incoming() + _ = cached.modify { current in + var current = current + current["group_selection_foreground_bubble_incoming"] = image + return current + } + return image + } + } + var group_selection_foreground_bubble_outgoing: CGImage { + if let image = cached.with({ $0["group_selection_foreground_bubble_outgoing"] }) { + return image + } else { + let image = _group_selection_foreground_bubble_outgoing() + _ = cached.modify { current in + var current = current + current["group_selection_foreground_bubble_outgoing"] = image + return current + } + return image + } + } + var chat_pinned_list: CGImage { + if let image = cached.with({ $0["chat_pinned_list"] }) { + return image + } else { + let image = _chat_pinned_list() + _ = cached.modify { current in + var current = current + current["chat_pinned_list"] = image + return current + } + return image + } + } + var chat_pinned_message: CGImage { + if let image = cached.with({ $0["chat_pinned_message"] }) { + return image + } else { + let image = _chat_pinned_message() + _ = cached.modify { current in + var current = current + current["chat_pinned_message"] = image + return current + } + return image + } + } + var chat_pinned_message_bubble_incoming: CGImage { + if let image = cached.with({ $0["chat_pinned_message_bubble_incoming"] }) { + return image + } else { + let image = _chat_pinned_message_bubble_incoming() + _ = cached.modify { current in + var current = current + current["chat_pinned_message_bubble_incoming"] = image + return current + } + return image + } + } + var chat_pinned_message_bubble_outgoing: CGImage { + if let image = cached.with({ $0["chat_pinned_message_bubble_outgoing"] }) { + return image + } else { + let image = _chat_pinned_message_bubble_outgoing() + _ = cached.modify { current in + var current = current + current["chat_pinned_message_bubble_outgoing"] = image + return current + } + return image + } + } + var chat_pinned_message_overlay_bubble: CGImage { + if let image = cached.with({ $0["chat_pinned_message_overlay_bubble"] }) { + return image + } else { + let image = _chat_pinned_message_overlay_bubble() + _ = cached.modify { current in + var current = current + current["chat_pinned_message_overlay_bubble"] = image + return current + } + return image + } + } + var chat_voicechat_can_unmute: CGImage { + if let image = cached.with({ $0["chat_voicechat_can_unmute"] }) { + return image + } else { + let image = _chat_voicechat_can_unmute() + _ = cached.modify { current in + var current = current + current["chat_voicechat_can_unmute"] = image + return current + } + return image + } + } + var chat_voicechat_cant_unmute: CGImage { + if let image = cached.with({ $0["chat_voicechat_cant_unmute"] }) { + return image + } else { + let image = _chat_voicechat_cant_unmute() + _ = cached.modify { current in + var current = current + current["chat_voicechat_cant_unmute"] = image + return current + } + return image + } + } + var chat_voicechat_unmuted: CGImage { + if let image = cached.with({ $0["chat_voicechat_unmuted"] }) { + return image + } else { + let image = _chat_voicechat_unmuted() + _ = cached.modify { current in + var current = current + current["chat_voicechat_unmuted"] = image + return current + } + return image + } + } + var profile_voice_chat: CGImage { + if let image = cached.with({ $0["profile_voice_chat"] }) { + return image + } else { + let image = _profile_voice_chat() + _ = cached.modify { current in + var current = current + current["profile_voice_chat"] = image + return current + } + return image + } + } + var chat_voice_chat: CGImage { + if let image = cached.with({ $0["chat_voice_chat"] }) { + return image + } else { + let image = _chat_voice_chat() + _ = cached.modify { current in + var current = current + current["chat_voice_chat"] = image + return current + } + return image + } + } + var chat_voice_chat_active: CGImage { + if let image = cached.with({ $0["chat_voice_chat_active"] }) { + return image + } else { + let image = _chat_voice_chat_active() + _ = cached.modify { current in + var current = current + current["chat_voice_chat_active"] = image + return current + } + return image + } + } + var editor_draw: CGImage { + if let image = cached.with({ $0["editor_draw"] }) { + return image + } else { + let image = _editor_draw() + _ = cached.modify { current in + var current = current + current["editor_draw"] = image + return current + } + return image + } + } + var editor_delete: CGImage { + if let image = cached.with({ $0["editor_delete"] }) { + return image + } else { + let image = _editor_delete() + _ = cached.modify { current in + var current = current + current["editor_delete"] = image + return current + } + return image + } + } + var editor_crop: CGImage { + if let image = cached.with({ $0["editor_crop"] }) { + return image + } else { + let image = _editor_crop() + _ = cached.modify { current in + var current = current + current["editor_crop"] = image + return current + } + return image + } + } + var fast_copy_link: CGImage { + if let image = cached.with({ $0["fast_copy_link"] }) { + return image + } else { + let image = _fast_copy_link() + _ = cached.modify { current in + var current = current + current["fast_copy_link"] = image + return current + } + return image + } + } + var profile_channel_sign: CGImage { + if let image = cached.with({ $0["profile_channel_sign"] }) { + return image + } else { + let image = _profile_channel_sign() + _ = cached.modify { current in + var current = current + current["profile_channel_sign"] = image + return current + } + return image + } + } + var profile_channel_type: CGImage { + if let image = cached.with({ $0["profile_channel_type"] }) { + return image + } else { + let image = _profile_channel_type() + _ = cached.modify { current in + var current = current + current["profile_channel_type"] = image + return current + } + return image + } + } + var profile_group_type: CGImage { + if let image = cached.with({ $0["profile_group_type"] }) { + return image + } else { + let image = _profile_group_type() + _ = cached.modify { current in + var current = current + current["profile_group_type"] = image + return current + } + return image + } + } + var profile_group_destruct: CGImage { + if let image = cached.with({ $0["profile_group_destruct"] }) { + return image + } else { + let image = _profile_group_destruct() + _ = cached.modify { current in + var current = current + current["profile_group_destruct"] = image + return current + } + return image + } + } + var profile_group_discussion: CGImage { + if let image = cached.with({ $0["profile_group_discussion"] }) { + return image + } else { + let image = _profile_group_discussion() + _ = cached.modify { current in + var current = current + current["profile_group_discussion"] = image + return current + } + return image + } + } + var profile_removed: CGImage { + if let image = cached.with({ $0["profile_removed"] }) { + return image + } else { + let image = _profile_removed() + _ = cached.modify { current in + var current = current + current["profile_removed"] = image + return current + } + return image + } + } + var profile_links: CGImage { + if let image = cached.with({ $0["profile_links"] }) { + return image + } else { + let image = _profile_links() + _ = cached.modify { current in + var current = current + current["profile_links"] = image + return current + } + return image + } + } + var destruct_clear_history: CGImage { + if let image = cached.with({ $0["destruct_clear_history"] }) { + return image + } else { + let image = _destruct_clear_history() + _ = cached.modify { current in + var current = current + current["destruct_clear_history"] = image + return current + } + return image + } + } + var chat_gigagroup_info: CGImage { + if let image = cached.with({ $0["chat_gigagroup_info"] }) { + return image + } else { + let image = _chat_gigagroup_info() + _ = cached.modify { current in + var current = current + current["chat_gigagroup_info"] = image + return current + } + return image + } + } + var playlist_next: CGImage { + if let image = cached.with({ $0["playlist_next"] }) { + return image + } else { + let image = _playlist_next() + _ = cached.modify { current in + var current = current + current["playlist_next"] = image + return current + } + return image + } + } + var playlist_prev: CGImage { + if let image = cached.with({ $0["playlist_prev"] }) { + return image + } else { + let image = _playlist_prev() + _ = cached.modify { current in + var current = current + current["playlist_prev"] = image + return current + } + return image + } + } + var playlist_next_locked: CGImage { + if let image = cached.with({ $0["playlist_next_locked"] }) { + return image + } else { + let image = _playlist_next_locked() + _ = cached.modify { current in + var current = current + current["playlist_next_locked"] = image + return current + } + return image + } + } + var playlist_prev_locked: CGImage { + if let image = cached.with({ $0["playlist_prev_locked"] }) { + return image + } else { + let image = _playlist_prev_locked() + _ = cached.modify { current in + var current = current + current["playlist_prev_locked"] = image + return current + } + return image + } + } + var playlist_random: CGImage { + if let image = cached.with({ $0["playlist_random"] }) { + return image + } else { + let image = _playlist_random() + _ = cached.modify { current in + var current = current + current["playlist_random"] = image + return current + } + return image + } + } + var playlist_order_normal: CGImage { + if let image = cached.with({ $0["playlist_order_normal"] }) { + return image + } else { + let image = _playlist_order_normal() + _ = cached.modify { current in + var current = current + current["playlist_order_normal"] = image + return current + } + return image + } + } + var playlist_order_reversed: CGImage { + if let image = cached.with({ $0["playlist_order_reversed"] }) { + return image + } else { + let image = _playlist_order_reversed() + _ = cached.modify { current in + var current = current + current["playlist_order_reversed"] = image + return current + } + return image + } + } + var playlist_order_random: CGImage { + if let image = cached.with({ $0["playlist_order_random"] }) { + return image + } else { + let image = _playlist_order_random() + _ = cached.modify { current in + var current = current + current["playlist_order_random"] = image + return current + } + return image + } + } + var playlist_repeat_none: CGImage { + if let image = cached.with({ $0["playlist_repeat_none"] }) { + return image + } else { + let image = _playlist_repeat_none() + _ = cached.modify { current in + var current = current + current["playlist_repeat_none"] = image + return current + } + return image + } + } + var playlist_repeat_circle: CGImage { + if let image = cached.with({ $0["playlist_repeat_circle"] }) { + return image + } else { + let image = _playlist_repeat_circle() + _ = cached.modify { current in + var current = current + current["playlist_repeat_circle"] = image + return current + } + return image + } + } + var playlist_repeat_one: CGImage { + if let image = cached.with({ $0["playlist_repeat_one"] }) { + return image + } else { + let image = _playlist_repeat_one() + _ = cached.modify { current in + var current = current + current["playlist_repeat_one"] = image + return current + } + return image + } + } + var audioplayer_next: CGImage { + if let image = cached.with({ $0["audioplayer_next"] }) { + return image + } else { + let image = _audioplayer_next() + _ = cached.modify { current in + var current = current + current["audioplayer_next"] = image + return current + } + return image + } + } + var audioplayer_prev: CGImage { + if let image = cached.with({ $0["audioplayer_prev"] }) { + return image + } else { + let image = _audioplayer_prev() + _ = cached.modify { current in + var current = current + current["audioplayer_prev"] = image + return current + } + return image + } + } + var audioplayer_dismiss: CGImage { + if let image = cached.with({ $0["audioplayer_dismiss"] }) { + return image + } else { + let image = _audioplayer_dismiss() + _ = cached.modify { current in + var current = current + current["audioplayer_dismiss"] = image + return current + } + return image + } + } + var audioplayer_repeat_none: CGImage { + if let image = cached.with({ $0["audioplayer_repeat_none"] }) { + return image + } else { + let image = _audioplayer_repeat_none() + _ = cached.modify { current in + var current = current + current["audioplayer_repeat_none"] = image + return current + } + return image + } + } + var audioplayer_repeat_circle: CGImage { + if let image = cached.with({ $0["audioplayer_repeat_circle"] }) { + return image + } else { + let image = _audioplayer_repeat_circle() + _ = cached.modify { current in + var current = current + current["audioplayer_repeat_circle"] = image + return current + } + return image + } + } + var audioplayer_repeat_one: CGImage { + if let image = cached.with({ $0["audioplayer_repeat_one"] }) { + return image + } else { + let image = _audioplayer_repeat_one() + _ = cached.modify { current in + var current = current + current["audioplayer_repeat_one"] = image + return current + } + return image + } + } + var audioplayer_locked_next: CGImage { + if let image = cached.with({ $0["audioplayer_locked_next"] }) { + return image + } else { + let image = _audioplayer_locked_next() + _ = cached.modify { current in + var current = current + current["audioplayer_locked_next"] = image + return current + } + return image + } + } + var audioplayer_locked_prev: CGImage { + if let image = cached.with({ $0["audioplayer_locked_prev"] }) { + return image + } else { + let image = _audioplayer_locked_prev() + _ = cached.modify { current in + var current = current + current["audioplayer_locked_prev"] = image + return current + } + return image + } + } + var audioplayer_volume: CGImage { + if let image = cached.with({ $0["audioplayer_volume"] }) { + return image + } else { + let image = _audioplayer_volume() + _ = cached.modify { current in + var current = current + current["audioplayer_volume"] = image + return current + } + return image + } + } + var audioplayer_volume_off: CGImage { + if let image = cached.with({ $0["audioplayer_volume_off"] }) { + return image + } else { + let image = _audioplayer_volume_off() + _ = cached.modify { current in + var current = current + current["audioplayer_volume_off"] = image + return current + } + return image + } + } + var audioplayer_speed_x1: CGImage { + if let image = cached.with({ $0["audioplayer_speed_x1"] }) { + return image + } else { + let image = _audioplayer_speed_x1() + _ = cached.modify { current in + var current = current + current["audioplayer_speed_x1"] = image + return current + } + return image + } + } + var audioplayer_speed_x2: CGImage { + if let image = cached.with({ $0["audioplayer_speed_x2"] }) { + return image + } else { + let image = _audioplayer_speed_x2() + _ = cached.modify { current in + var current = current + current["audioplayer_speed_x2"] = image + return current + } + return image + } + } + var chat_info_voice_chat: CGImage { + if let image = cached.with({ $0["chat_info_voice_chat"] }) { + return image + } else { + let image = _chat_info_voice_chat() + _ = cached.modify { current in + var current = current + current["chat_info_voice_chat"] = image + return current + } + return image + } + } + var chat_info_create_group: CGImage { + if let image = cached.with({ $0["chat_info_create_group"] }) { + return image + } else { + let image = _chat_info_create_group() + _ = cached.modify { current in + var current = current + current["chat_info_create_group"] = image + return current + } + return image + } + } + var chat_info_change_colors: CGImage { + if let image = cached.with({ $0["chat_info_change_colors"] }) { + return image + } else { + let image = _chat_info_change_colors() + _ = cached.modify { current in + var current = current + current["chat_info_change_colors"] = image + return current + } + return image + } + } + var empty_chat_system: CGImage { + if let image = cached.with({ $0["empty_chat_system"] }) { + return image + } else { + let image = _empty_chat_system() + _ = cached.modify { current in + var current = current + current["empty_chat_system"] = image + return current + } + return image + } + } + var empty_chat_dark: CGImage { + if let image = cached.with({ $0["empty_chat_dark"] }) { + return image + } else { + let image = _empty_chat_dark() + _ = cached.modify { current in + var current = current + current["empty_chat_dark"] = image + return current + } + return image + } + } + var empty_chat_light: CGImage { + if let image = cached.with({ $0["empty_chat_light"] }) { + return image + } else { + let image = _empty_chat_light() + _ = cached.modify { current in + var current = current + current["empty_chat_light"] = image + return current + } + return image + } + } + var empty_chat_system_active: CGImage { + if let image = cached.with({ $0["empty_chat_system_active"] }) { + return image + } else { + let image = _empty_chat_system_active() + _ = cached.modify { current in + var current = current + current["empty_chat_system_active"] = image + return current + } + return image + } + } + var empty_chat_dark_active: CGImage { + if let image = cached.with({ $0["empty_chat_dark_active"] }) { + return image + } else { + let image = _empty_chat_dark_active() + _ = cached.modify { current in + var current = current + current["empty_chat_dark_active"] = image + return current + } + return image + } + } + var empty_chat_light_active: CGImage { + if let image = cached.with({ $0["empty_chat_light_active"] }) { + return image + } else { + let image = _empty_chat_light_active() + _ = cached.modify { current in + var current = current + current["empty_chat_light_active"] = image + return current + } + return image + } + } + var empty_chat_storage_clear: CGImage { + if let image = cached.with({ $0["empty_chat_storage_clear"] }) { + return image + } else { + let image = _empty_chat_storage_clear() + _ = cached.modify { current in + var current = current + current["empty_chat_storage_clear"] = image + return current + } + return image + } + } + var empty_chat_storage_low: CGImage { + if let image = cached.with({ $0["empty_chat_storage_low"] }) { + return image + } else { + let image = _empty_chat_storage_low() + _ = cached.modify { current in + var current = current + current["empty_chat_storage_low"] = image + return current + } + return image + } + } + var empty_chat_storage_medium: CGImage { + if let image = cached.with({ $0["empty_chat_storage_medium"] }) { + return image + } else { + let image = _empty_chat_storage_medium() + _ = cached.modify { current in + var current = current + current["empty_chat_storage_medium"] = image + return current + } + return image + } + } + var empty_chat_storage_high: CGImage { + if let image = cached.with({ $0["empty_chat_storage_high"] }) { + return image + } else { + let image = _empty_chat_storage_high() + _ = cached.modify { current in + var current = current + current["empty_chat_storage_high"] = image + return current + } + return image + } + } + var empty_chat_storage_low_active: CGImage { + if let image = cached.with({ $0["empty_chat_storage_low_active"] }) { + return image + } else { + let image = _empty_chat_storage_low_active() + _ = cached.modify { current in + var current = current + current["empty_chat_storage_low_active"] = image + return current + } + return image + } + } + var empty_chat_storage_medium_active: CGImage { + if let image = cached.with({ $0["empty_chat_storage_medium_active"] }) { + return image + } else { + let image = _empty_chat_storage_medium_active() + _ = cached.modify { current in + var current = current + current["empty_chat_storage_medium_active"] = image + return current + } + return image + } + } + var empty_chat_storage_high_active: CGImage { + if let image = cached.with({ $0["empty_chat_storage_high_active"] }) { + return image + } else { + let image = _empty_chat_storage_high_active() + _ = cached.modify { current in + var current = current + current["empty_chat_storage_high_active"] = image + return current + } + return image + } + } + var empty_chat_stickers_none: CGImage { + if let image = cached.with({ $0["empty_chat_stickers_none"] }) { + return image + } else { + let image = _empty_chat_stickers_none() + _ = cached.modify { current in + var current = current + current["empty_chat_stickers_none"] = image + return current + } + return image + } + } + var empty_chat_stickers_mysets: CGImage { + if let image = cached.with({ $0["empty_chat_stickers_mysets"] }) { + return image + } else { + let image = _empty_chat_stickers_mysets() + _ = cached.modify { current in + var current = current + current["empty_chat_stickers_mysets"] = image + return current + } + return image + } + } + var empty_chat_stickers_allsets: CGImage { + if let image = cached.with({ $0["empty_chat_stickers_allsets"] }) { + return image + } else { + let image = _empty_chat_stickers_allsets() + _ = cached.modify { current in + var current = current + current["empty_chat_stickers_allsets"] = image + return current + } + return image + } + } + var empty_chat_stickers_none_active: CGImage { + if let image = cached.with({ $0["empty_chat_stickers_none_active"] }) { + return image + } else { + let image = _empty_chat_stickers_none_active() + _ = cached.modify { current in + var current = current + current["empty_chat_stickers_none_active"] = image + return current + } + return image + } + } + var empty_chat_stickers_mysets_active: CGImage { + if let image = cached.with({ $0["empty_chat_stickers_mysets_active"] }) { + return image + } else { + let image = _empty_chat_stickers_mysets_active() + _ = cached.modify { current in + var current = current + current["empty_chat_stickers_mysets_active"] = image + return current + } + return image + } + } + var empty_chat_stickers_allsets_active: CGImage { + if let image = cached.with({ $0["empty_chat_stickers_allsets_active"] }) { + return image + } else { + let image = _empty_chat_stickers_allsets_active() + _ = cached.modify { current in + var current = current + current["empty_chat_stickers_allsets_active"] = image + return current + } + return image + } + } + var chat_action_dismiss: CGImage { + if let image = cached.with({ $0["chat_action_dismiss"] }) { + return image + } else { + let image = _chat_action_dismiss() + _ = cached.modify { current in + var current = current + current["chat_action_dismiss"] = image + return current + } + return image + } + } + var chat_action_edit_message: CGImage { + if let image = cached.with({ $0["chat_action_edit_message"] }) { + return image + } else { + let image = _chat_action_edit_message() + _ = cached.modify { current in + var current = current + current["chat_action_edit_message"] = image + return current + } + return image + } + } + var chat_action_forward_message: CGImage { + if let image = cached.with({ $0["chat_action_forward_message"] }) { + return image + } else { + let image = _chat_action_forward_message() + _ = cached.modify { current in + var current = current + current["chat_action_forward_message"] = image + return current + } + return image + } + } + var chat_action_reply_message: CGImage { + if let image = cached.with({ $0["chat_action_reply_message"] }) { + return image + } else { + let image = _chat_action_reply_message() + _ = cached.modify { current in + var current = current + current["chat_action_reply_message"] = image + return current + } + return image + } + } + var chat_action_url_preview: CGImage { + if let image = cached.with({ $0["chat_action_url_preview"] }) { + return image + } else { + let image = _chat_action_url_preview() + _ = cached.modify { current in + var current = current + current["chat_action_url_preview"] = image + return current + } + return image + } + } + var chat_action_menu_update_chat: CGImage { + if let image = cached.with({ $0["chat_action_menu_update_chat"] }) { + return image + } else { + let image = _chat_action_menu_update_chat() + _ = cached.modify { current in + var current = current + current["chat_action_menu_update_chat"] = image + return current + } + return image + } + } + var chat_action_menu_selected: CGImage { + if let image = cached.with({ $0["chat_action_menu_selected"] }) { + return image + } else { + let image = _chat_action_menu_selected() + _ = cached.modify { current in + var current = current + current["chat_action_menu_selected"] = image + return current + } + return image + } + } + var widget_peers_favorite: CGImage { + if let image = cached.with({ $0["widget_peers_favorite"] }) { + return image + } else { + let image = _widget_peers_favorite() + _ = cached.modify { current in + var current = current + current["widget_peers_favorite"] = image + return current + } + return image + } + } + var widget_peers_recent: CGImage { + if let image = cached.with({ $0["widget_peers_recent"] }) { + return image + } else { + let image = _widget_peers_recent() + _ = cached.modify { current in + var current = current + current["widget_peers_recent"] = image + return current + } + return image + } + } + var widget_peers_both: CGImage { + if let image = cached.with({ $0["widget_peers_both"] }) { + return image + } else { + let image = _widget_peers_both() + _ = cached.modify { current in + var current = current + current["widget_peers_both"] = image + return current + } + return image + } + } + var widget_peers_favorite_active: CGImage { + if let image = cached.with({ $0["widget_peers_favorite_active"] }) { + return image + } else { + let image = _widget_peers_favorite_active() + _ = cached.modify { current in + var current = current + current["widget_peers_favorite_active"] = image + return current + } + return image + } + } + var widget_peers_recent_active: CGImage { + if let image = cached.with({ $0["widget_peers_recent_active"] }) { + return image + } else { + let image = _widget_peers_recent_active() + _ = cached.modify { current in + var current = current + current["widget_peers_recent_active"] = image + return current + } + return image + } + } + var widget_peers_both_active: CGImage { + if let image = cached.with({ $0["widget_peers_both_active"] }) { + return image + } else { + let image = _widget_peers_both_active() + _ = cached.modify { current in + var current = current + current["widget_peers_both_active"] = image + return current + } + return image + } + } + + private let _dialogMuteImage: ()->CGImage + private let _dialogMuteImageSelected: ()->CGImage + private let _outgoingMessageImage: ()->CGImage + private let _readMessageImage: ()->CGImage + private let _outgoingMessageImageSelected: ()->CGImage + private let _readMessageImageSelected: ()->CGImage + private let _sendingImage: ()->CGImage + private let _sendingImageSelected: ()->CGImage + private let _secretImage: ()->CGImage + private let _secretImageSelected: ()->CGImage + private let _pinnedImage: ()->CGImage + private let _pinnedImageSelected: ()->CGImage + private let _verifiedImage: ()->CGImage + private let _verifiedImageSelected: ()->CGImage + private let _errorImage: ()->CGImage + private let _errorImageSelected: ()->CGImage + private let _chatSearch: ()->CGImage + private let _chatSearchActive: ()->CGImage + private let _chatCall: ()->CGImage + private let _chatCallActive: ()->CGImage + private let _chatActions: ()->CGImage + private let _chatFailedCall_incoming: ()->CGImage + private let _chatFailedCall_outgoing: ()->CGImage + private let _chatCall_incoming: ()->CGImage + private let _chatCall_outgoing: ()->CGImage + private let _chatFailedCallBubble_incoming: ()->CGImage + private let _chatFailedCallBubble_outgoing: ()->CGImage + private let _chatCallBubble_incoming: ()->CGImage + private let _chatCallBubble_outgoing: ()->CGImage + private let _chatFallbackCall: ()->CGImage + private let _chatFallbackCallBubble_incoming: ()->CGImage + private let _chatFallbackCallBubble_outgoing: ()->CGImage + private let _chatFallbackVideoCall: ()->CGImage + private let _chatFallbackVideoCallBubble_incoming: ()->CGImage + private let _chatFallbackVideoCallBubble_outgoing: ()->CGImage + private let _chatToggleSelected: ()->CGImage + private let _chatToggleUnselected: ()->CGImage + private let _chatMusicPlay: ()->CGImage + private let _chatMusicPlayBubble_incoming: ()->CGImage + private let _chatMusicPlayBubble_outgoing: ()->CGImage + private let _chatMusicPause: ()->CGImage + private let _chatMusicPauseBubble_incoming: ()->CGImage + private let _chatMusicPauseBubble_outgoing: ()->CGImage + private let _chatGradientBubble_incoming: ()->CGImage + private let _chatGradientBubble_outgoing: ()->CGImage + private let _chatBubble_none_incoming_withInset: ()->(CGImage, NSEdgeInsets) + private let _chatBubble_none_outgoing_withInset: ()->(CGImage, NSEdgeInsets) + private let _chatBubbleBorder_none_incoming_withInset: ()->(CGImage, NSEdgeInsets) + private let _chatBubbleBorder_none_outgoing_withInset: ()->(CGImage, NSEdgeInsets) + private let _chatBubble_both_incoming_withInset: ()->(CGImage, NSEdgeInsets) + private let _chatBubble_both_outgoing_withInset: ()->(CGImage, NSEdgeInsets) + private let _chatBubbleBorder_both_incoming_withInset: ()->(CGImage, NSEdgeInsets) + private let _chatBubbleBorder_both_outgoing_withInset: ()->(CGImage, NSEdgeInsets) + private let _composeNewChat: ()->CGImage + private let _composeNewChatActive: ()->CGImage + private let _composeNewGroup: ()->CGImage + private let _composeNewSecretChat: ()->CGImage + private let _composeNewChannel: ()->CGImage + private let _contactsNewContact: ()->CGImage + private let _chatReadMarkInBubble1_incoming: ()->CGImage + private let _chatReadMarkInBubble2_incoming: ()->CGImage + private let _chatReadMarkInBubble1_outgoing: ()->CGImage + private let _chatReadMarkInBubble2_outgoing: ()->CGImage + private let _chatReadMarkOutBubble1: ()->CGImage + private let _chatReadMarkOutBubble2: ()->CGImage + private let _chatReadMarkOverlayBubble1: ()->CGImage + private let _chatReadMarkOverlayBubble2: ()->CGImage + private let _sentFailed: ()->CGImage + private let _chatChannelViewsInBubble_incoming: ()->CGImage + private let _chatChannelViewsInBubble_outgoing: ()->CGImage + private let _chatChannelViewsOutBubble: ()->CGImage + private let _chatChannelViewsOverlayBubble: ()->CGImage + private let _chatNavigationBack: ()->CGImage + private let _peerInfoAddMember: ()->CGImage + private let _chatSearchUp: ()->CGImage + private let _chatSearchUpDisabled: ()->CGImage + private let _chatSearchDown: ()->CGImage + private let _chatSearchDownDisabled: ()->CGImage + private let _chatSearchCalendar: ()->CGImage + private let _dismissAccessory: ()->CGImage + private let _chatScrollUp: ()->CGImage + private let _chatScrollUpActive: ()->CGImage + private let _chatSendMessage: ()->CGImage + private let _chatSaveEditedMessage: ()->CGImage + private let _chatRecordVoice: ()->CGImage + private let _chatEntertainment: ()->CGImage + private let _chatInlineDismiss: ()->CGImage + private let _chatActiveReplyMarkup: ()->CGImage + private let _chatDisabledReplyMarkup: ()->CGImage + private let _chatSecretTimer: ()->CGImage + private let _chatForwardMessagesActive: ()->CGImage + private let _chatForwardMessagesInactive: ()->CGImage + private let _chatDeleteMessagesActive: ()->CGImage + private let _chatDeleteMessagesInactive: ()->CGImage + private let _generalNext: ()->CGImage + private let _generalNextActive: ()->CGImage + private let _generalSelect: ()->CGImage + private let _chatVoiceRecording: ()->CGImage + private let _chatVideoRecording: ()->CGImage + private let _chatRecord: ()->CGImage + private let _deleteItem: ()->CGImage + private let _deleteItemDisabled: ()->CGImage + private let _chatAttach: ()->CGImage + private let _chatAttachFile: ()->CGImage + private let _chatAttachPhoto: ()->CGImage + private let _chatAttachCamera: ()->CGImage + private let _chatAttachLocation: ()->CGImage + private let _chatAttachPoll: ()->CGImage + private let _mediaEmptyShared: ()->CGImage + private let _mediaEmptyFiles: ()->CGImage + private let _mediaEmptyMusic: ()->CGImage + private let _mediaEmptyLinks: ()->CGImage + private let _stickersAddFeatured: ()->CGImage + private let _stickersAddedFeatured: ()->CGImage + private let _stickersRemove: ()->CGImage + private let _peerMediaDownloadFileStart: ()->CGImage + private let _peerMediaDownloadFilePause: ()->CGImage + private let _stickersShare: ()->CGImage + private let _emojiRecentTab: ()->CGImage + private let _emojiSmileTab: ()->CGImage + private let _emojiNatureTab: ()->CGImage + private let _emojiFoodTab: ()->CGImage + private let _emojiSportTab: ()->CGImage + private let _emojiCarTab: ()->CGImage + private let _emojiObjectsTab: ()->CGImage + private let _emojiSymbolsTab: ()->CGImage + private let _emojiFlagsTab: ()->CGImage + private let _emojiRecentTabActive: ()->CGImage + private let _emojiSmileTabActive: ()->CGImage + private let _emojiNatureTabActive: ()->CGImage + private let _emojiFoodTabActive: ()->CGImage + private let _emojiSportTabActive: ()->CGImage + private let _emojiCarTabActive: ()->CGImage + private let _emojiObjectsTabActive: ()->CGImage + private let _emojiSymbolsTabActive: ()->CGImage + private let _emojiFlagsTabActive: ()->CGImage + private let _stickerBackground: ()->CGImage + private let _stickerBackgroundActive: ()->CGImage + private let _stickersTabRecent: ()->CGImage + private let _stickersTabGIF: ()->CGImage + private let _chatSendingInFrame_incoming: ()->CGImage + private let _chatSendingInHour_incoming: ()->CGImage + private let _chatSendingInMin_incoming: ()->CGImage + private let _chatSendingInFrame_outgoing: ()->CGImage + private let _chatSendingInHour_outgoing: ()->CGImage + private let _chatSendingInMin_outgoing: ()->CGImage + private let _chatSendingOutFrame: ()->CGImage + private let _chatSendingOutHour: ()->CGImage + private let _chatSendingOutMin: ()->CGImage + private let _chatSendingOverlayFrame: ()->CGImage + private let _chatSendingOverlayHour: ()->CGImage + private let _chatSendingOverlayMin: ()->CGImage + private let _chatActionUrl: ()->CGImage + private let _callInlineDecline: ()->CGImage + private let _callInlineMuted: ()->CGImage + private let _callInlineUnmuted: ()->CGImage + private let _eventLogTriangle: ()->CGImage + private let _channelIntro: ()->CGImage + private let _chatFileThumb: ()->CGImage + private let _chatFileThumbBubble_incoming: ()->CGImage + private let _chatFileThumbBubble_outgoing: ()->CGImage + private let _chatSecretThumb: ()->CGImage + private let _chatSecretThumbSmall: ()->CGImage + private let _chatMapPin: ()->CGImage + private let _chatSecretTitle: ()->CGImage + private let _emptySearch: ()->CGImage + private let _calendarBack: ()->CGImage + private let _calendarNext: ()->CGImage + private let _calendarBackDisabled: ()->CGImage + private let _calendarNextDisabled: ()->CGImage + private let _newChatCamera: ()->CGImage + private let _peerInfoVerify: ()->CGImage + private let _peerInfoVerifyProfile: ()->CGImage + private let _peerInfoCall: ()->CGImage + private let _callOutgoing: ()->CGImage + private let _recentDismiss: ()->CGImage + private let _recentDismissActive: ()->CGImage + private let _webgameShare: ()->CGImage + private let _chatSearchCancel: ()->CGImage + private let _chatSearchFrom: ()->CGImage + private let _callWindowDecline: ()->CGImage + private let _callWindowDeclineSmall: ()->CGImage + private let _callWindowAccept: ()->CGImage + private let _callWindowVideo: ()->CGImage + private let _callWindowVideoActive: ()->CGImage + private let _callWindowMute: ()->CGImage + private let _callWindowMuteActive: ()->CGImage + private let _callWindowClose: ()->CGImage + private let _callWindowDeviceSettings: ()->CGImage + private let _callSettings: ()->CGImage + private let _callWindowCancel: ()->CGImage + private let _chatActionEdit: ()->CGImage + private let _chatActionInfo: ()->CGImage + private let _chatActionMute: ()->CGImage + private let _chatActionUnmute: ()->CGImage + private let _chatActionClearHistory: ()->CGImage + private let _chatActionDeleteChat: ()->CGImage + private let _dismissPinned: ()->CGImage + private let _chatActionsActive: ()->CGImage + private let _chatEntertainmentSticker: ()->CGImage + private let _chatEmpty: ()->CGImage + private let _stickerPackClose: ()->CGImage + private let _stickerPackDelete: ()->CGImage + private let _modalShare: ()->CGImage + private let _modalClose: ()->CGImage + private let _ivChannelJoined: ()->CGImage + private let _chatListMention: ()->CGImage + private let _chatListMentionActive: ()->CGImage + private let _chatListMentionArchived: ()->CGImage + private let _chatListMentionArchivedActive: ()->CGImage + private let _chatMention: ()->CGImage + private let _chatMentionActive: ()->CGImage + private let _sliderControl: ()->CGImage + private let _sliderControlActive: ()->CGImage + private let _stickersTabFave: ()->CGImage + private let _chatInstantView: ()->CGImage + private let _chatInstantViewBubble_incoming: ()->CGImage + private let _chatInstantViewBubble_outgoing: ()->CGImage + private let _instantViewShare: ()->CGImage + private let _instantViewActions: ()->CGImage + private let _instantViewActionsActive: ()->CGImage + private let _instantViewSafari: ()->CGImage + private let _instantViewBack: ()->CGImage + private let _instantViewCheck: ()->CGImage + private let _groupStickerNotFound: ()->CGImage + private let _settingsAskQuestion: ()->CGImage + private let _settingsFaq: ()->CGImage + private let _settingsGeneral: ()->CGImage + private let _settingsLanguage: ()->CGImage + private let _settingsNotifications: ()->CGImage + private let _settingsSecurity: ()->CGImage + private let _settingsStickers: ()->CGImage + private let _settingsStorage: ()->CGImage + private let _settingsSessions: ()->CGImage + private let _settingsProxy: ()->CGImage + private let _settingsAppearance: ()->CGImage + private let _settingsPassport: ()->CGImage + private let _settingsWallet: ()->CGImage + private let _settingsUpdate: ()->CGImage + private let _settingsFilters: ()->CGImage + private let _settingsAskQuestionActive: ()->CGImage + private let _settingsFaqActive: ()->CGImage + private let _settingsGeneralActive: ()->CGImage + private let _settingsLanguageActive: ()->CGImage + private let _settingsNotificationsActive: ()->CGImage + private let _settingsSecurityActive: ()->CGImage + private let _settingsStickersActive: ()->CGImage + private let _settingsStorageActive: ()->CGImage + private let _settingsSessionsActive: ()->CGImage + private let _settingsProxyActive: ()->CGImage + private let _settingsAppearanceActive: ()->CGImage + private let _settingsPassportActive: ()->CGImage + private let _settingsWalletActive: ()->CGImage + private let _settingsUpdateActive: ()->CGImage + private let _settingsFiltersActive: ()->CGImage + private let _settingsProfile: ()->CGImage + private let _generalCheck: ()->CGImage + private let _settingsAbout: ()->CGImage + private let _settingsLogout: ()->CGImage + private let _fastSettingsLock: ()->CGImage + private let _fastSettingsDark: ()->CGImage + private let _fastSettingsSunny: ()->CGImage + private let _fastSettingsMute: ()->CGImage + private let _fastSettingsUnmute: ()->CGImage + private let _chatRecordVideo: ()->CGImage + private let _inputChannelMute: ()->CGImage + private let _inputChannelUnmute: ()->CGImage + private let _changePhoneNumberIntro: ()->CGImage + private let _peerSavedMessages: ()->CGImage + private let _previewSenderCollage: ()->CGImage + private let _previewSenderPhoto: ()->CGImage + private let _previewSenderFile: ()->CGImage + private let _previewSenderCrop: ()->CGImage + private let _previewSenderDelete: ()->CGImage + private let _previewSenderDeleteFile: ()->CGImage + private let _previewSenderArchive: ()->CGImage + private let _chatGroupToggleSelected: ()->CGImage + private let _chatGroupToggleUnselected: ()->CGImage + private let _successModalProgress: ()->CGImage + private let _accentColorSelect: ()->CGImage + private let _transparentBackground: ()->CGImage + private let _lottieTransparentBackground: ()->CGImage + private let _passcodeTouchId: ()->CGImage + private let _passcodeLogin: ()->CGImage + private let _confirmDeleteMessagesAccessory: ()->CGImage + private let _alertCheckBoxSelected: ()->CGImage + private let _alertCheckBoxUnselected: ()->CGImage + private let _confirmPinAccessory: ()->CGImage + private let _confirmDeleteChatAccessory: ()->CGImage + private let _stickersEmptySearch: ()->CGImage + private let _twoStepVerificationCreateIntro: ()->CGImage + private let _secureIdAuth: ()->CGImage + private let _ivAudioPlay: ()->CGImage + private let _ivAudioPause: ()->CGImage + private let _proxyEnable: ()->CGImage + private let _proxyEnabled: ()->CGImage + private let _proxyState: ()->CGImage + private let _proxyDeleteListItem: ()->CGImage + private let _proxyInfoListItem: ()->CGImage + private let _proxyConnectedListItem: ()->CGImage + private let _proxyAddProxy: ()->CGImage + private let _proxyNextWaitingListItem: ()->CGImage + private let _passportForgotPassword: ()->CGImage + private let _confirmAppAccessoryIcon: ()->CGImage + private let _passportPassport: ()->CGImage + private let _passportIdCardReverse: ()->CGImage + private let _passportIdCard: ()->CGImage + private let _passportSelfie: ()->CGImage + private let _passportDriverLicense: ()->CGImage + private let _chatOverlayVoiceRecording: ()->CGImage + private let _chatOverlayVideoRecording: ()->CGImage + private let _chatOverlaySendRecording: ()->CGImage + private let _chatOverlayLockArrowRecording: ()->CGImage + private let _chatOverlayLockerBodyRecording: ()->CGImage + private let _chatOverlayLockerHeadRecording: ()->CGImage + private let _locationPin: ()->CGImage + private let _locationMapPin: ()->CGImage + private let _locationMapLocate: ()->CGImage + private let _locationMapLocated: ()->CGImage + private let _passportSettings: ()->CGImage + private let _passportInfo: ()->CGImage + private let _editMessageMedia: ()->CGImage + private let _playerMusicPlaceholder: ()->CGImage + private let _chatMusicPlaceholder: ()->CGImage + private let _chatMusicPlaceholderCap: ()->CGImage + private let _searchArticle: ()->CGImage + private let _searchSaved: ()->CGImage + private let _archivedChats: ()->CGImage + private let _hintPeerActive: ()->CGImage + private let _hintPeerActiveSelected: ()->CGImage + private let _chatSwiping_delete: ()->CGImage + private let _chatSwiping_mute: ()->CGImage + private let _chatSwiping_unmute: ()->CGImage + private let _chatSwiping_read: ()->CGImage + private let _chatSwiping_unread: ()->CGImage + private let _chatSwiping_pin: ()->CGImage + private let _chatSwiping_unpin: ()->CGImage + private let _chatSwiping_archive: ()->CGImage + private let _chatSwiping_unarchive: ()->CGImage + private let _galleryPrev: ()->CGImage + private let _galleryNext: ()->CGImage + private let _galleryMore: ()->CGImage + private let _galleryShare: ()->CGImage + private let _galleryFastSave: ()->CGImage + private let _galleryRotate: ()->CGImage + private let _galleryZoomIn: ()->CGImage + private let _galleryZoomOut: ()->CGImage + private let _editMessageCurrentPhoto: ()->CGImage + private let _videoPlayerPlay: ()->CGImage + private let _videoPlayerPause: ()->CGImage + private let _videoPlayerEnterFullScreen: ()->CGImage + private let _videoPlayerExitFullScreen: ()->CGImage + private let _videoPlayerPIPIn: ()->CGImage + private let _videoPlayerPIPOut: ()->CGImage + private let _videoPlayerRewind15Forward: ()->CGImage + private let _videoPlayerRewind15Backward: ()->CGImage + private let _videoPlayerVolume: ()->CGImage + private let _videoPlayerVolumeOff: ()->CGImage + private let _videoPlayerClose: ()->CGImage + private let _videoPlayerSliderInteractor: ()->CGImage + private let _streamingVideoDownload: ()->CGImage + private let _videoCompactFetching: ()->CGImage + private let _compactStreamingFetchingCancel: ()->CGImage + private let _customLocalizationDelete: ()->CGImage + private let _pollAddOption: ()->CGImage + private let _pollDeleteOption: ()->CGImage + private let _resort: ()->CGImage + private let _chatPollVoteUnselected: ()->CGImage + private let _chatPollVoteUnselectedBubble_incoming: ()->CGImage + private let _chatPollVoteUnselectedBubble_outgoing: ()->CGImage + private let _peerInfoAdmins: ()->CGImage + private let _peerInfoPermissions: ()->CGImage + private let _peerInfoBanned: ()->CGImage + private let _peerInfoMembers: ()->CGImage + private let _chatUndoAction: ()->CGImage + private let _appUpdate: ()->CGImage + private let _inlineVideoSoundOff: ()->CGImage + private let _inlineVideoSoundOn: ()->CGImage + private let _logoutOptionAddAccount: ()->CGImage + private let _logoutOptionSetPasscode: ()->CGImage + private let _logoutOptionClearCache: ()->CGImage + private let _logoutOptionChangePhoneNumber: ()->CGImage + private let _logoutOptionContactSupport: ()->CGImage + private let _disableEmojiPrediction: ()->CGImage + private let _scam: ()->CGImage + private let _scamActive: ()->CGImage + private let _chatScam: ()->CGImage + private let _fake: ()->CGImage + private let _fakeActive: ()->CGImage + private let _chatFake: ()->CGImage + private let _chatUnarchive: ()->CGImage + private let _chatArchive: ()->CGImage + private let _privacySettings_blocked: ()->CGImage + private let _privacySettings_activeSessions: ()->CGImage + private let _privacySettings_passcode: ()->CGImage + private let _privacySettings_twoStep: ()->CGImage + private let _deletedAccount: ()->CGImage + private let _stickerPackSelection: ()->CGImage + private let _stickerPackSelectionActive: ()->CGImage + private let _entertainment_Emoji: ()->CGImage + private let _entertainment_Stickers: ()->CGImage + private let _entertainment_Gifs: ()->CGImage + private let _entertainment_Search: ()->CGImage + private let _entertainment_Settings: ()->CGImage + private let _entertainment_SearchCancel: ()->CGImage + private let _scheduledAvatar: ()->CGImage + private let _scheduledInputAction: ()->CGImage + private let _verifyDialog: ()->CGImage + private let _verifyDialogActive: ()->CGImage + private let _chatInputScheduled: ()->CGImage + private let _appearanceAddPlatformTheme: ()->CGImage + private let _wallet_close: ()->CGImage + private let _wallet_qr: ()->CGImage + private let _wallet_receive: ()->CGImage + private let _wallet_send: ()->CGImage + private let _wallet_settings: ()->CGImage + private let _wallet_update: ()->CGImage + private let _wallet_passcode_visible: ()->CGImage + private let _wallet_passcode_hidden: ()->CGImage + private let _wallpaper_color_close: ()->CGImage + private let _wallpaper_color_add: ()->CGImage + private let _wallpaper_color_swap: ()->CGImage + private let _wallpaper_color_rotate: ()->CGImage + private let _wallpaper_color_play: ()->CGImage + private let _login_cap: ()->CGImage + private let _login_qr_cap: ()->CGImage + private let _login_qr_empty_cap: ()->CGImage + private let _chat_failed_scroller: ()->CGImage + private let _chat_failed_scroller_active: ()->CGImage + private let _poll_quiz_unselected: ()->CGImage + private let _poll_selected: ()->CGImage + private let _poll_selection: ()->CGImage + private let _poll_selected_correct: ()->CGImage + private let _poll_selected_incorrect: ()->CGImage + private let _poll_selected_incoming: ()->CGImage + private let _poll_selection_incoming: ()->CGImage + private let _poll_selected_correct_incoming: ()->CGImage + private let _poll_selected_incorrect_incoming: ()->CGImage + private let _poll_selected_outgoing: ()->CGImage + private let _poll_selection_outgoing: ()->CGImage + private let _poll_selected_correct_outgoing: ()->CGImage + private let _poll_selected_incorrect_outgoing: ()->CGImage + private let _chat_filter_edit: ()->CGImage + private let _chat_filter_add: ()->CGImage + private let _chat_filter_bots: ()->CGImage + private let _chat_filter_channels: ()->CGImage + private let _chat_filter_custom: ()->CGImage + private let _chat_filter_groups: ()->CGImage + private let _chat_filter_muted: ()->CGImage + private let _chat_filter_private_chats: ()->CGImage + private let _chat_filter_read: ()->CGImage + private let _chat_filter_secret_chats: ()->CGImage + private let _chat_filter_unmuted: ()->CGImage + private let _chat_filter_unread: ()->CGImage + private let _chat_filter_large_groups: ()->CGImage + private let _chat_filter_non_contacts: ()->CGImage + private let _chat_filter_archive: ()->CGImage + private let _chat_filter_bots_avatar: ()->CGImage + private let _chat_filter_channels_avatar: ()->CGImage + private let _chat_filter_custom_avatar: ()->CGImage + private let _chat_filter_groups_avatar: ()->CGImage + private let _chat_filter_muted_avatar: ()->CGImage + private let _chat_filter_private_chats_avatar: ()->CGImage + private let _chat_filter_read_avatar: ()->CGImage + private let _chat_filter_secret_chats_avatar: ()->CGImage + private let _chat_filter_unmuted_avatar: ()->CGImage + private let _chat_filter_unread_avatar: ()->CGImage + private let _chat_filter_large_groups_avatar: ()->CGImage + private let _chat_filter_non_contacts_avatar: ()->CGImage + private let _chat_filter_archive_avatar: ()->CGImage + private let _group_invite_via_link: ()->CGImage + private let _tab_contacts: ()->CGImage + private let _tab_contacts_active: ()->CGImage + private let _tab_calls: ()->CGImage + private let _tab_calls_active: ()->CGImage + private let _tab_chats: ()->CGImage + private let _tab_chats_active: ()->CGImage + private let _tab_chats_active_filters: ()->CGImage + private let _tab_settings: ()->CGImage + private let _tab_settings_active: ()->CGImage + private let _profile_add_member: ()->CGImage + private let _profile_call: ()->CGImage + private let _profile_video_call: ()->CGImage + private let _profile_leave: ()->CGImage + private let _profile_message: ()->CGImage + private let _profile_more: ()->CGImage + private let _profile_mute: ()->CGImage + private let _profile_unmute: ()->CGImage + private let _profile_search: ()->CGImage + private let _profile_secret_chat: ()->CGImage + private let _profile_edit_photo: ()->CGImage + private let _profile_block: ()->CGImage + private let _profile_report: ()->CGImage + private let _profile_share: ()->CGImage + private let _profile_stats: ()->CGImage + private let _profile_unblock: ()->CGImage + private let _chat_quiz_explanation: ()->CGImage + private let _chat_quiz_explanation_bubble_incoming: ()->CGImage + private let _chat_quiz_explanation_bubble_outgoing: ()->CGImage + private let _stickers_add_featured: ()->CGImage + private let _stickers_add_featured_unread: ()->CGImage + private let _channel_info_promo: ()->CGImage + private let _channel_info_promo_bubble_incoming: ()->CGImage + private let _channel_info_promo_bubble_outgoing: ()->CGImage + private let _chat_share_message: ()->CGImage + private let _chat_goto_message: ()->CGImage + private let _chat_swipe_reply: ()->CGImage + private let _chat_like_message: ()->CGImage + private let _chat_like_message_unlike: ()->CGImage + private let _chat_like_inside: ()->CGImage + private let _chat_like_inside_bubble_incoming: ()->CGImage + private let _chat_like_inside_bubble_outgoing: ()->CGImage + private let _chat_like_inside_bubble_overlay: ()->CGImage + private let _chat_like_inside_empty: ()->CGImage + private let _chat_like_inside_empty_bubble_incoming: ()->CGImage + private let _chat_like_inside_empty_bubble_outgoing: ()->CGImage + private let _chat_like_inside_empty_bubble_overlay: ()->CGImage + private let _gif_trending: ()->CGImage + private let _chat_list_thumb_play: ()->CGImage + private let _call_tooltip_battery_low: ()->CGImage + private let _call_tooltip_camera_off: ()->CGImage + private let _call_tooltip_micro_off: ()->CGImage + private let _call_screen_sharing: ()->CGImage + private let _call_screen_sharing_active: ()->CGImage + private let _call_screen_settings: ()->CGImage + private let _search_filter: ()->CGImage + private let _search_filter_media: ()->CGImage + private let _search_filter_files: ()->CGImage + private let _search_filter_links: ()->CGImage + private let _search_filter_music: ()->CGImage + private let _search_filter_add_peer: ()->CGImage + private let _search_filter_add_peer_active: ()->CGImage + private let _chat_reply_count_bubble_incoming: ()->CGImage + private let _chat_reply_count_bubble_outgoing: ()->CGImage + private let _chat_reply_count: ()->CGImage + private let _chat_reply_count_overlay: ()->CGImage + private let _channel_comments_bubble: ()->CGImage + private let _channel_comments_bubble_next: ()->CGImage + private let _channel_comments_list: ()->CGImage + private let _channel_comments_overlay: ()->CGImage + private let _chat_replies_avatar: ()->CGImage + private let _group_selection_foreground: ()->CGImage + private let _group_selection_foreground_bubble_incoming: ()->CGImage + private let _group_selection_foreground_bubble_outgoing: ()->CGImage + private let _chat_pinned_list: ()->CGImage + private let _chat_pinned_message: ()->CGImage + private let _chat_pinned_message_bubble_incoming: ()->CGImage + private let _chat_pinned_message_bubble_outgoing: ()->CGImage + private let _chat_pinned_message_overlay_bubble: ()->CGImage + private let _chat_voicechat_can_unmute: ()->CGImage + private let _chat_voicechat_cant_unmute: ()->CGImage + private let _chat_voicechat_unmuted: ()->CGImage + private let _profile_voice_chat: ()->CGImage + private let _chat_voice_chat: ()->CGImage + private let _chat_voice_chat_active: ()->CGImage + private let _editor_draw: ()->CGImage + private let _editor_delete: ()->CGImage + private let _editor_crop: ()->CGImage + private let _fast_copy_link: ()->CGImage + private let _profile_channel_sign: ()->CGImage + private let _profile_channel_type: ()->CGImage + private let _profile_group_type: ()->CGImage + private let _profile_group_destruct: ()->CGImage + private let _profile_group_discussion: ()->CGImage + private let _profile_removed: ()->CGImage + private let _profile_links: ()->CGImage + private let _destruct_clear_history: ()->CGImage + private let _chat_gigagroup_info: ()->CGImage + private let _playlist_next: ()->CGImage + private let _playlist_prev: ()->CGImage + private let _playlist_next_locked: ()->CGImage + private let _playlist_prev_locked: ()->CGImage + private let _playlist_random: ()->CGImage + private let _playlist_order_normal: ()->CGImage + private let _playlist_order_reversed: ()->CGImage + private let _playlist_order_random: ()->CGImage + private let _playlist_repeat_none: ()->CGImage + private let _playlist_repeat_circle: ()->CGImage + private let _playlist_repeat_one: ()->CGImage + private let _audioplayer_next: ()->CGImage + private let _audioplayer_prev: ()->CGImage + private let _audioplayer_dismiss: ()->CGImage + private let _audioplayer_repeat_none: ()->CGImage + private let _audioplayer_repeat_circle: ()->CGImage + private let _audioplayer_repeat_one: ()->CGImage + private let _audioplayer_locked_next: ()->CGImage + private let _audioplayer_locked_prev: ()->CGImage + private let _audioplayer_volume: ()->CGImage + private let _audioplayer_volume_off: ()->CGImage + private let _audioplayer_speed_x1: ()->CGImage + private let _audioplayer_speed_x2: ()->CGImage + private let _chat_info_voice_chat: ()->CGImage + private let _chat_info_create_group: ()->CGImage + private let _chat_info_change_colors: ()->CGImage + private let _empty_chat_system: ()->CGImage + private let _empty_chat_dark: ()->CGImage + private let _empty_chat_light: ()->CGImage + private let _empty_chat_system_active: ()->CGImage + private let _empty_chat_dark_active: ()->CGImage + private let _empty_chat_light_active: ()->CGImage + private let _empty_chat_storage_clear: ()->CGImage + private let _empty_chat_storage_low: ()->CGImage + private let _empty_chat_storage_medium: ()->CGImage + private let _empty_chat_storage_high: ()->CGImage + private let _empty_chat_storage_low_active: ()->CGImage + private let _empty_chat_storage_medium_active: ()->CGImage + private let _empty_chat_storage_high_active: ()->CGImage + private let _empty_chat_stickers_none: ()->CGImage + private let _empty_chat_stickers_mysets: ()->CGImage + private let _empty_chat_stickers_allsets: ()->CGImage + private let _empty_chat_stickers_none_active: ()->CGImage + private let _empty_chat_stickers_mysets_active: ()->CGImage + private let _empty_chat_stickers_allsets_active: ()->CGImage + private let _chat_action_dismiss: ()->CGImage + private let _chat_action_edit_message: ()->CGImage + private let _chat_action_forward_message: ()->CGImage + private let _chat_action_reply_message: ()->CGImage + private let _chat_action_url_preview: ()->CGImage + private let _chat_action_menu_update_chat: ()->CGImage + private let _chat_action_menu_selected: ()->CGImage + private let _widget_peers_favorite: ()->CGImage + private let _widget_peers_recent: ()->CGImage + private let _widget_peers_both: ()->CGImage + private let _widget_peers_favorite_active: ()->CGImage + private let _widget_peers_recent_active: ()->CGImage + private let _widget_peers_both_active: ()->CGImage + + init( + dialogMuteImage: @escaping()->CGImage, + dialogMuteImageSelected: @escaping()->CGImage, + outgoingMessageImage: @escaping()->CGImage, + readMessageImage: @escaping()->CGImage, + outgoingMessageImageSelected: @escaping()->CGImage, + readMessageImageSelected: @escaping()->CGImage, + sendingImage: @escaping()->CGImage, + sendingImageSelected: @escaping()->CGImage, + secretImage: @escaping()->CGImage, + secretImageSelected: @escaping()->CGImage, + pinnedImage: @escaping()->CGImage, + pinnedImageSelected: @escaping()->CGImage, + verifiedImage: @escaping()->CGImage, + verifiedImageSelected: @escaping()->CGImage, + errorImage: @escaping()->CGImage, + errorImageSelected: @escaping()->CGImage, + chatSearch: @escaping()->CGImage, + chatSearchActive: @escaping()->CGImage, + chatCall: @escaping()->CGImage, + chatCallActive: @escaping()->CGImage, + chatActions: @escaping()->CGImage, + chatFailedCall_incoming: @escaping()->CGImage, + chatFailedCall_outgoing: @escaping()->CGImage, + chatCall_incoming: @escaping()->CGImage, + chatCall_outgoing: @escaping()->CGImage, + chatFailedCallBubble_incoming: @escaping()->CGImage, + chatFailedCallBubble_outgoing: @escaping()->CGImage, + chatCallBubble_incoming: @escaping()->CGImage, + chatCallBubble_outgoing: @escaping()->CGImage, + chatFallbackCall: @escaping()->CGImage, + chatFallbackCallBubble_incoming: @escaping()->CGImage, + chatFallbackCallBubble_outgoing: @escaping()->CGImage, + chatFallbackVideoCall: @escaping()->CGImage, + chatFallbackVideoCallBubble_incoming: @escaping()->CGImage, + chatFallbackVideoCallBubble_outgoing: @escaping()->CGImage, + chatToggleSelected: @escaping()->CGImage, + chatToggleUnselected: @escaping()->CGImage, + chatMusicPlay: @escaping()->CGImage, + chatMusicPlayBubble_incoming: @escaping()->CGImage, + chatMusicPlayBubble_outgoing: @escaping()->CGImage, + chatMusicPause: @escaping()->CGImage, + chatMusicPauseBubble_incoming: @escaping()->CGImage, + chatMusicPauseBubble_outgoing: @escaping()->CGImage, + chatGradientBubble_incoming: @escaping()->CGImage, + chatGradientBubble_outgoing: @escaping()->CGImage, + chatBubble_none_incoming_withInset: @escaping()->(CGImage, NSEdgeInsets), + chatBubble_none_outgoing_withInset: @escaping()->(CGImage, NSEdgeInsets), + chatBubbleBorder_none_incoming_withInset: @escaping()->(CGImage, NSEdgeInsets), + chatBubbleBorder_none_outgoing_withInset: @escaping()->(CGImage, NSEdgeInsets), + chatBubble_both_incoming_withInset: @escaping()->(CGImage, NSEdgeInsets), + chatBubble_both_outgoing_withInset: @escaping()->(CGImage, NSEdgeInsets), + chatBubbleBorder_both_incoming_withInset: @escaping()->(CGImage, NSEdgeInsets), + chatBubbleBorder_both_outgoing_withInset: @escaping()->(CGImage, NSEdgeInsets), + composeNewChat: @escaping()->CGImage, + composeNewChatActive: @escaping()->CGImage, + composeNewGroup: @escaping()->CGImage, + composeNewSecretChat: @escaping()->CGImage, + composeNewChannel: @escaping()->CGImage, + contactsNewContact: @escaping()->CGImage, + chatReadMarkInBubble1_incoming: @escaping()->CGImage, + chatReadMarkInBubble2_incoming: @escaping()->CGImage, + chatReadMarkInBubble1_outgoing: @escaping()->CGImage, + chatReadMarkInBubble2_outgoing: @escaping()->CGImage, + chatReadMarkOutBubble1: @escaping()->CGImage, + chatReadMarkOutBubble2: @escaping()->CGImage, + chatReadMarkOverlayBubble1: @escaping()->CGImage, + chatReadMarkOverlayBubble2: @escaping()->CGImage, + sentFailed: @escaping()->CGImage, + chatChannelViewsInBubble_incoming: @escaping()->CGImage, + chatChannelViewsInBubble_outgoing: @escaping()->CGImage, + chatChannelViewsOutBubble: @escaping()->CGImage, + chatChannelViewsOverlayBubble: @escaping()->CGImage, + chatNavigationBack: @escaping()->CGImage, + peerInfoAddMember: @escaping()->CGImage, + chatSearchUp: @escaping()->CGImage, + chatSearchUpDisabled: @escaping()->CGImage, + chatSearchDown: @escaping()->CGImage, + chatSearchDownDisabled: @escaping()->CGImage, + chatSearchCalendar: @escaping()->CGImage, + dismissAccessory: @escaping()->CGImage, + chatScrollUp: @escaping()->CGImage, + chatScrollUpActive: @escaping()->CGImage, + chatSendMessage: @escaping()->CGImage, + chatSaveEditedMessage: @escaping()->CGImage, + chatRecordVoice: @escaping()->CGImage, + chatEntertainment: @escaping()->CGImage, + chatInlineDismiss: @escaping()->CGImage, + chatActiveReplyMarkup: @escaping()->CGImage, + chatDisabledReplyMarkup: @escaping()->CGImage, + chatSecretTimer: @escaping()->CGImage, + chatForwardMessagesActive: @escaping()->CGImage, + chatForwardMessagesInactive: @escaping()->CGImage, + chatDeleteMessagesActive: @escaping()->CGImage, + chatDeleteMessagesInactive: @escaping()->CGImage, + generalNext: @escaping()->CGImage, + generalNextActive: @escaping()->CGImage, + generalSelect: @escaping()->CGImage, + chatVoiceRecording: @escaping()->CGImage, + chatVideoRecording: @escaping()->CGImage, + chatRecord: @escaping()->CGImage, + deleteItem: @escaping()->CGImage, + deleteItemDisabled: @escaping()->CGImage, + chatAttach: @escaping()->CGImage, + chatAttachFile: @escaping()->CGImage, + chatAttachPhoto: @escaping()->CGImage, + chatAttachCamera: @escaping()->CGImage, + chatAttachLocation: @escaping()->CGImage, + chatAttachPoll: @escaping()->CGImage, + mediaEmptyShared: @escaping()->CGImage, + mediaEmptyFiles: @escaping()->CGImage, + mediaEmptyMusic: @escaping()->CGImage, + mediaEmptyLinks: @escaping()->CGImage, + stickersAddFeatured: @escaping()->CGImage, + stickersAddedFeatured: @escaping()->CGImage, + stickersRemove: @escaping()->CGImage, + peerMediaDownloadFileStart: @escaping()->CGImage, + peerMediaDownloadFilePause: @escaping()->CGImage, + stickersShare: @escaping()->CGImage, + emojiRecentTab: @escaping()->CGImage, + emojiSmileTab: @escaping()->CGImage, + emojiNatureTab: @escaping()->CGImage, + emojiFoodTab: @escaping()->CGImage, + emojiSportTab: @escaping()->CGImage, + emojiCarTab: @escaping()->CGImage, + emojiObjectsTab: @escaping()->CGImage, + emojiSymbolsTab: @escaping()->CGImage, + emojiFlagsTab: @escaping()->CGImage, + emojiRecentTabActive: @escaping()->CGImage, + emojiSmileTabActive: @escaping()->CGImage, + emojiNatureTabActive: @escaping()->CGImage, + emojiFoodTabActive: @escaping()->CGImage, + emojiSportTabActive: @escaping()->CGImage, + emojiCarTabActive: @escaping()->CGImage, + emojiObjectsTabActive: @escaping()->CGImage, + emojiSymbolsTabActive: @escaping()->CGImage, + emojiFlagsTabActive: @escaping()->CGImage, + stickerBackground: @escaping()->CGImage, + stickerBackgroundActive: @escaping()->CGImage, + stickersTabRecent: @escaping()->CGImage, + stickersTabGIF: @escaping()->CGImage, + chatSendingInFrame_incoming: @escaping()->CGImage, + chatSendingInHour_incoming: @escaping()->CGImage, + chatSendingInMin_incoming: @escaping()->CGImage, + chatSendingInFrame_outgoing: @escaping()->CGImage, + chatSendingInHour_outgoing: @escaping()->CGImage, + chatSendingInMin_outgoing: @escaping()->CGImage, + chatSendingOutFrame: @escaping()->CGImage, + chatSendingOutHour: @escaping()->CGImage, + chatSendingOutMin: @escaping()->CGImage, + chatSendingOverlayFrame: @escaping()->CGImage, + chatSendingOverlayHour: @escaping()->CGImage, + chatSendingOverlayMin: @escaping()->CGImage, + chatActionUrl: @escaping()->CGImage, + callInlineDecline: @escaping()->CGImage, + callInlineMuted: @escaping()->CGImage, + callInlineUnmuted: @escaping()->CGImage, + eventLogTriangle: @escaping()->CGImage, + channelIntro: @escaping()->CGImage, + chatFileThumb: @escaping()->CGImage, + chatFileThumbBubble_incoming: @escaping()->CGImage, + chatFileThumbBubble_outgoing: @escaping()->CGImage, + chatSecretThumb: @escaping()->CGImage, + chatSecretThumbSmall: @escaping()->CGImage, + chatMapPin: @escaping()->CGImage, + chatSecretTitle: @escaping()->CGImage, + emptySearch: @escaping()->CGImage, + calendarBack: @escaping()->CGImage, + calendarNext: @escaping()->CGImage, + calendarBackDisabled: @escaping()->CGImage, + calendarNextDisabled: @escaping()->CGImage, + newChatCamera: @escaping()->CGImage, + peerInfoVerify: @escaping()->CGImage, + peerInfoVerifyProfile: @escaping()->CGImage, + peerInfoCall: @escaping()->CGImage, + callOutgoing: @escaping()->CGImage, + recentDismiss: @escaping()->CGImage, + recentDismissActive: @escaping()->CGImage, + webgameShare: @escaping()->CGImage, + chatSearchCancel: @escaping()->CGImage, + chatSearchFrom: @escaping()->CGImage, + callWindowDecline: @escaping()->CGImage, + callWindowDeclineSmall: @escaping()->CGImage, + callWindowAccept: @escaping()->CGImage, + callWindowVideo: @escaping()->CGImage, + callWindowVideoActive: @escaping()->CGImage, + callWindowMute: @escaping()->CGImage, + callWindowMuteActive: @escaping()->CGImage, + callWindowClose: @escaping()->CGImage, + callWindowDeviceSettings: @escaping()->CGImage, + callSettings: @escaping()->CGImage, + callWindowCancel: @escaping()->CGImage, + chatActionEdit: @escaping()->CGImage, + chatActionInfo: @escaping()->CGImage, + chatActionMute: @escaping()->CGImage, + chatActionUnmute: @escaping()->CGImage, + chatActionClearHistory: @escaping()->CGImage, + chatActionDeleteChat: @escaping()->CGImage, + dismissPinned: @escaping()->CGImage, + chatActionsActive: @escaping()->CGImage, + chatEntertainmentSticker: @escaping()->CGImage, + chatEmpty: @escaping()->CGImage, + stickerPackClose: @escaping()->CGImage, + stickerPackDelete: @escaping()->CGImage, + modalShare: @escaping()->CGImage, + modalClose: @escaping()->CGImage, + ivChannelJoined: @escaping()->CGImage, + chatListMention: @escaping()->CGImage, + chatListMentionActive: @escaping()->CGImage, + chatListMentionArchived: @escaping()->CGImage, + chatListMentionArchivedActive: @escaping()->CGImage, + chatMention: @escaping()->CGImage, + chatMentionActive: @escaping()->CGImage, + sliderControl: @escaping()->CGImage, + sliderControlActive: @escaping()->CGImage, + stickersTabFave: @escaping()->CGImage, + chatInstantView: @escaping()->CGImage, + chatInstantViewBubble_incoming: @escaping()->CGImage, + chatInstantViewBubble_outgoing: @escaping()->CGImage, + instantViewShare: @escaping()->CGImage, + instantViewActions: @escaping()->CGImage, + instantViewActionsActive: @escaping()->CGImage, + instantViewSafari: @escaping()->CGImage, + instantViewBack: @escaping()->CGImage, + instantViewCheck: @escaping()->CGImage, + groupStickerNotFound: @escaping()->CGImage, + settingsAskQuestion: @escaping()->CGImage, + settingsFaq: @escaping()->CGImage, + settingsGeneral: @escaping()->CGImage, + settingsLanguage: @escaping()->CGImage, + settingsNotifications: @escaping()->CGImage, + settingsSecurity: @escaping()->CGImage, + settingsStickers: @escaping()->CGImage, + settingsStorage: @escaping()->CGImage, + settingsSessions: @escaping()->CGImage, + settingsProxy: @escaping()->CGImage, + settingsAppearance: @escaping()->CGImage, + settingsPassport: @escaping()->CGImage, + settingsWallet: @escaping()->CGImage, + settingsUpdate: @escaping()->CGImage, + settingsFilters: @escaping()->CGImage, + settingsAskQuestionActive: @escaping()->CGImage, + settingsFaqActive: @escaping()->CGImage, + settingsGeneralActive: @escaping()->CGImage, + settingsLanguageActive: @escaping()->CGImage, + settingsNotificationsActive: @escaping()->CGImage, + settingsSecurityActive: @escaping()->CGImage, + settingsStickersActive: @escaping()->CGImage, + settingsStorageActive: @escaping()->CGImage, + settingsSessionsActive: @escaping()->CGImage, + settingsProxyActive: @escaping()->CGImage, + settingsAppearanceActive: @escaping()->CGImage, + settingsPassportActive: @escaping()->CGImage, + settingsWalletActive: @escaping()->CGImage, + settingsUpdateActive: @escaping()->CGImage, + settingsFiltersActive: @escaping()->CGImage, + settingsProfile: @escaping()->CGImage, + generalCheck: @escaping()->CGImage, + settingsAbout: @escaping()->CGImage, + settingsLogout: @escaping()->CGImage, + fastSettingsLock: @escaping()->CGImage, + fastSettingsDark: @escaping()->CGImage, + fastSettingsSunny: @escaping()->CGImage, + fastSettingsMute: @escaping()->CGImage, + fastSettingsUnmute: @escaping()->CGImage, + chatRecordVideo: @escaping()->CGImage, + inputChannelMute: @escaping()->CGImage, + inputChannelUnmute: @escaping()->CGImage, + changePhoneNumberIntro: @escaping()->CGImage, + peerSavedMessages: @escaping()->CGImage, + previewSenderCollage: @escaping()->CGImage, + previewSenderPhoto: @escaping()->CGImage, + previewSenderFile: @escaping()->CGImage, + previewSenderCrop: @escaping()->CGImage, + previewSenderDelete: @escaping()->CGImage, + previewSenderDeleteFile: @escaping()->CGImage, + previewSenderArchive: @escaping()->CGImage, + chatGroupToggleSelected: @escaping()->CGImage, + chatGroupToggleUnselected: @escaping()->CGImage, + successModalProgress: @escaping()->CGImage, + accentColorSelect: @escaping()->CGImage, + transparentBackground: @escaping()->CGImage, + lottieTransparentBackground: @escaping()->CGImage, + passcodeTouchId: @escaping()->CGImage, + passcodeLogin: @escaping()->CGImage, + confirmDeleteMessagesAccessory: @escaping()->CGImage, + alertCheckBoxSelected: @escaping()->CGImage, + alertCheckBoxUnselected: @escaping()->CGImage, + confirmPinAccessory: @escaping()->CGImage, + confirmDeleteChatAccessory: @escaping()->CGImage, + stickersEmptySearch: @escaping()->CGImage, + twoStepVerificationCreateIntro: @escaping()->CGImage, + secureIdAuth: @escaping()->CGImage, + ivAudioPlay: @escaping()->CGImage, + ivAudioPause: @escaping()->CGImage, + proxyEnable: @escaping()->CGImage, + proxyEnabled: @escaping()->CGImage, + proxyState: @escaping()->CGImage, + proxyDeleteListItem: @escaping()->CGImage, + proxyInfoListItem: @escaping()->CGImage, + proxyConnectedListItem: @escaping()->CGImage, + proxyAddProxy: @escaping()->CGImage, + proxyNextWaitingListItem: @escaping()->CGImage, + passportForgotPassword: @escaping()->CGImage, + confirmAppAccessoryIcon: @escaping()->CGImage, + passportPassport: @escaping()->CGImage, + passportIdCardReverse: @escaping()->CGImage, + passportIdCard: @escaping()->CGImage, + passportSelfie: @escaping()->CGImage, + passportDriverLicense: @escaping()->CGImage, + chatOverlayVoiceRecording: @escaping()->CGImage, + chatOverlayVideoRecording: @escaping()->CGImage, + chatOverlaySendRecording: @escaping()->CGImage, + chatOverlayLockArrowRecording: @escaping()->CGImage, + chatOverlayLockerBodyRecording: @escaping()->CGImage, + chatOverlayLockerHeadRecording: @escaping()->CGImage, + locationPin: @escaping()->CGImage, + locationMapPin: @escaping()->CGImage, + locationMapLocate: @escaping()->CGImage, + locationMapLocated: @escaping()->CGImage, + passportSettings: @escaping()->CGImage, + passportInfo: @escaping()->CGImage, + editMessageMedia: @escaping()->CGImage, + playerMusicPlaceholder: @escaping()->CGImage, + chatMusicPlaceholder: @escaping()->CGImage, + chatMusicPlaceholderCap: @escaping()->CGImage, + searchArticle: @escaping()->CGImage, + searchSaved: @escaping()->CGImage, + archivedChats: @escaping()->CGImage, + hintPeerActive: @escaping()->CGImage, + hintPeerActiveSelected: @escaping()->CGImage, + chatSwiping_delete: @escaping()->CGImage, + chatSwiping_mute: @escaping()->CGImage, + chatSwiping_unmute: @escaping()->CGImage, + chatSwiping_read: @escaping()->CGImage, + chatSwiping_unread: @escaping()->CGImage, + chatSwiping_pin: @escaping()->CGImage, + chatSwiping_unpin: @escaping()->CGImage, + chatSwiping_archive: @escaping()->CGImage, + chatSwiping_unarchive: @escaping()->CGImage, + galleryPrev: @escaping()->CGImage, + galleryNext: @escaping()->CGImage, + galleryMore: @escaping()->CGImage, + galleryShare: @escaping()->CGImage, + galleryFastSave: @escaping()->CGImage, + galleryRotate: @escaping()->CGImage, + galleryZoomIn: @escaping()->CGImage, + galleryZoomOut: @escaping()->CGImage, + editMessageCurrentPhoto: @escaping()->CGImage, + videoPlayerPlay: @escaping()->CGImage, + videoPlayerPause: @escaping()->CGImage, + videoPlayerEnterFullScreen: @escaping()->CGImage, + videoPlayerExitFullScreen: @escaping()->CGImage, + videoPlayerPIPIn: @escaping()->CGImage, + videoPlayerPIPOut: @escaping()->CGImage, + videoPlayerRewind15Forward: @escaping()->CGImage, + videoPlayerRewind15Backward: @escaping()->CGImage, + videoPlayerVolume: @escaping()->CGImage, + videoPlayerVolumeOff: @escaping()->CGImage, + videoPlayerClose: @escaping()->CGImage, + videoPlayerSliderInteractor: @escaping()->CGImage, + streamingVideoDownload: @escaping()->CGImage, + videoCompactFetching: @escaping()->CGImage, + compactStreamingFetchingCancel: @escaping()->CGImage, + customLocalizationDelete: @escaping()->CGImage, + pollAddOption: @escaping()->CGImage, + pollDeleteOption: @escaping()->CGImage, + resort: @escaping()->CGImage, + chatPollVoteUnselected: @escaping()->CGImage, + chatPollVoteUnselectedBubble_incoming: @escaping()->CGImage, + chatPollVoteUnselectedBubble_outgoing: @escaping()->CGImage, + peerInfoAdmins: @escaping()->CGImage, + peerInfoPermissions: @escaping()->CGImage, + peerInfoBanned: @escaping()->CGImage, + peerInfoMembers: @escaping()->CGImage, + chatUndoAction: @escaping()->CGImage, + appUpdate: @escaping()->CGImage, + inlineVideoSoundOff: @escaping()->CGImage, + inlineVideoSoundOn: @escaping()->CGImage, + logoutOptionAddAccount: @escaping()->CGImage, + logoutOptionSetPasscode: @escaping()->CGImage, + logoutOptionClearCache: @escaping()->CGImage, + logoutOptionChangePhoneNumber: @escaping()->CGImage, + logoutOptionContactSupport: @escaping()->CGImage, + disableEmojiPrediction: @escaping()->CGImage, + scam: @escaping()->CGImage, + scamActive: @escaping()->CGImage, + chatScam: @escaping()->CGImage, + fake: @escaping()->CGImage, + fakeActive: @escaping()->CGImage, + chatFake: @escaping()->CGImage, + chatUnarchive: @escaping()->CGImage, + chatArchive: @escaping()->CGImage, + privacySettings_blocked: @escaping()->CGImage, + privacySettings_activeSessions: @escaping()->CGImage, + privacySettings_passcode: @escaping()->CGImage, + privacySettings_twoStep: @escaping()->CGImage, + deletedAccount: @escaping()->CGImage, + stickerPackSelection: @escaping()->CGImage, + stickerPackSelectionActive: @escaping()->CGImage, + entertainment_Emoji: @escaping()->CGImage, + entertainment_Stickers: @escaping()->CGImage, + entertainment_Gifs: @escaping()->CGImage, + entertainment_Search: @escaping()->CGImage, + entertainment_Settings: @escaping()->CGImage, + entertainment_SearchCancel: @escaping()->CGImage, + scheduledAvatar: @escaping()->CGImage, + scheduledInputAction: @escaping()->CGImage, + verifyDialog: @escaping()->CGImage, + verifyDialogActive: @escaping()->CGImage, + chatInputScheduled: @escaping()->CGImage, + appearanceAddPlatformTheme: @escaping()->CGImage, + wallet_close: @escaping()->CGImage, + wallet_qr: @escaping()->CGImage, + wallet_receive: @escaping()->CGImage, + wallet_send: @escaping()->CGImage, + wallet_settings: @escaping()->CGImage, + wallet_update: @escaping()->CGImage, + wallet_passcode_visible: @escaping()->CGImage, + wallet_passcode_hidden: @escaping()->CGImage, + wallpaper_color_close: @escaping()->CGImage, + wallpaper_color_add: @escaping()->CGImage, + wallpaper_color_swap: @escaping()->CGImage, + wallpaper_color_rotate: @escaping()->CGImage, + wallpaper_color_play: @escaping()->CGImage, + login_cap: @escaping()->CGImage, + login_qr_cap: @escaping()->CGImage, + login_qr_empty_cap: @escaping()->CGImage, + chat_failed_scroller: @escaping()->CGImage, + chat_failed_scroller_active: @escaping()->CGImage, + poll_quiz_unselected: @escaping()->CGImage, + poll_selected: @escaping()->CGImage, + poll_selection: @escaping()->CGImage, + poll_selected_correct: @escaping()->CGImage, + poll_selected_incorrect: @escaping()->CGImage, + poll_selected_incoming: @escaping()->CGImage, + poll_selection_incoming: @escaping()->CGImage, + poll_selected_correct_incoming: @escaping()->CGImage, + poll_selected_incorrect_incoming: @escaping()->CGImage, + poll_selected_outgoing: @escaping()->CGImage, + poll_selection_outgoing: @escaping()->CGImage, + poll_selected_correct_outgoing: @escaping()->CGImage, + poll_selected_incorrect_outgoing: @escaping()->CGImage, + chat_filter_edit: @escaping()->CGImage, + chat_filter_add: @escaping()->CGImage, + chat_filter_bots: @escaping()->CGImage, + chat_filter_channels: @escaping()->CGImage, + chat_filter_custom: @escaping()->CGImage, + chat_filter_groups: @escaping()->CGImage, + chat_filter_muted: @escaping()->CGImage, + chat_filter_private_chats: @escaping()->CGImage, + chat_filter_read: @escaping()->CGImage, + chat_filter_secret_chats: @escaping()->CGImage, + chat_filter_unmuted: @escaping()->CGImage, + chat_filter_unread: @escaping()->CGImage, + chat_filter_large_groups: @escaping()->CGImage, + chat_filter_non_contacts: @escaping()->CGImage, + chat_filter_archive: @escaping()->CGImage, + chat_filter_bots_avatar: @escaping()->CGImage, + chat_filter_channels_avatar: @escaping()->CGImage, + chat_filter_custom_avatar: @escaping()->CGImage, + chat_filter_groups_avatar: @escaping()->CGImage, + chat_filter_muted_avatar: @escaping()->CGImage, + chat_filter_private_chats_avatar: @escaping()->CGImage, + chat_filter_read_avatar: @escaping()->CGImage, + chat_filter_secret_chats_avatar: @escaping()->CGImage, + chat_filter_unmuted_avatar: @escaping()->CGImage, + chat_filter_unread_avatar: @escaping()->CGImage, + chat_filter_large_groups_avatar: @escaping()->CGImage, + chat_filter_non_contacts_avatar: @escaping()->CGImage, + chat_filter_archive_avatar: @escaping()->CGImage, + group_invite_via_link: @escaping()->CGImage, + tab_contacts: @escaping()->CGImage, + tab_contacts_active: @escaping()->CGImage, + tab_calls: @escaping()->CGImage, + tab_calls_active: @escaping()->CGImage, + tab_chats: @escaping()->CGImage, + tab_chats_active: @escaping()->CGImage, + tab_chats_active_filters: @escaping()->CGImage, + tab_settings: @escaping()->CGImage, + tab_settings_active: @escaping()->CGImage, + profile_add_member: @escaping()->CGImage, + profile_call: @escaping()->CGImage, + profile_video_call: @escaping()->CGImage, + profile_leave: @escaping()->CGImage, + profile_message: @escaping()->CGImage, + profile_more: @escaping()->CGImage, + profile_mute: @escaping()->CGImage, + profile_unmute: @escaping()->CGImage, + profile_search: @escaping()->CGImage, + profile_secret_chat: @escaping()->CGImage, + profile_edit_photo: @escaping()->CGImage, + profile_block: @escaping()->CGImage, + profile_report: @escaping()->CGImage, + profile_share: @escaping()->CGImage, + profile_stats: @escaping()->CGImage, + profile_unblock: @escaping()->CGImage, + chat_quiz_explanation: @escaping()->CGImage, + chat_quiz_explanation_bubble_incoming: @escaping()->CGImage, + chat_quiz_explanation_bubble_outgoing: @escaping()->CGImage, + stickers_add_featured: @escaping()->CGImage, + stickers_add_featured_unread: @escaping()->CGImage, + channel_info_promo: @escaping()->CGImage, + channel_info_promo_bubble_incoming: @escaping()->CGImage, + channel_info_promo_bubble_outgoing: @escaping()->CGImage, + chat_share_message: @escaping()->CGImage, + chat_goto_message: @escaping()->CGImage, + chat_swipe_reply: @escaping()->CGImage, + chat_like_message: @escaping()->CGImage, + chat_like_message_unlike: @escaping()->CGImage, + chat_like_inside: @escaping()->CGImage, + chat_like_inside_bubble_incoming: @escaping()->CGImage, + chat_like_inside_bubble_outgoing: @escaping()->CGImage, + chat_like_inside_bubble_overlay: @escaping()->CGImage, + chat_like_inside_empty: @escaping()->CGImage, + chat_like_inside_empty_bubble_incoming: @escaping()->CGImage, + chat_like_inside_empty_bubble_outgoing: @escaping()->CGImage, + chat_like_inside_empty_bubble_overlay: @escaping()->CGImage, + gif_trending: @escaping()->CGImage, + chat_list_thumb_play: @escaping()->CGImage, + call_tooltip_battery_low: @escaping()->CGImage, + call_tooltip_camera_off: @escaping()->CGImage, + call_tooltip_micro_off: @escaping()->CGImage, + call_screen_sharing: @escaping()->CGImage, + call_screen_sharing_active: @escaping()->CGImage, + call_screen_settings: @escaping()->CGImage, + search_filter: @escaping()->CGImage, + search_filter_media: @escaping()->CGImage, + search_filter_files: @escaping()->CGImage, + search_filter_links: @escaping()->CGImage, + search_filter_music: @escaping()->CGImage, + search_filter_add_peer: @escaping()->CGImage, + search_filter_add_peer_active: @escaping()->CGImage, + chat_reply_count_bubble_incoming: @escaping()->CGImage, + chat_reply_count_bubble_outgoing: @escaping()->CGImage, + chat_reply_count: @escaping()->CGImage, + chat_reply_count_overlay: @escaping()->CGImage, + channel_comments_bubble: @escaping()->CGImage, + channel_comments_bubble_next: @escaping()->CGImage, + channel_comments_list: @escaping()->CGImage, + channel_comments_overlay: @escaping()->CGImage, + chat_replies_avatar: @escaping()->CGImage, + group_selection_foreground: @escaping()->CGImage, + group_selection_foreground_bubble_incoming: @escaping()->CGImage, + group_selection_foreground_bubble_outgoing: @escaping()->CGImage, + chat_pinned_list: @escaping()->CGImage, + chat_pinned_message: @escaping()->CGImage, + chat_pinned_message_bubble_incoming: @escaping()->CGImage, + chat_pinned_message_bubble_outgoing: @escaping()->CGImage, + chat_pinned_message_overlay_bubble: @escaping()->CGImage, + chat_voicechat_can_unmute: @escaping()->CGImage, + chat_voicechat_cant_unmute: @escaping()->CGImage, + chat_voicechat_unmuted: @escaping()->CGImage, + profile_voice_chat: @escaping()->CGImage, + chat_voice_chat: @escaping()->CGImage, + chat_voice_chat_active: @escaping()->CGImage, + editor_draw: @escaping()->CGImage, + editor_delete: @escaping()->CGImage, + editor_crop: @escaping()->CGImage, + fast_copy_link: @escaping()->CGImage, + profile_channel_sign: @escaping()->CGImage, + profile_channel_type: @escaping()->CGImage, + profile_group_type: @escaping()->CGImage, + profile_group_destruct: @escaping()->CGImage, + profile_group_discussion: @escaping()->CGImage, + profile_removed: @escaping()->CGImage, + profile_links: @escaping()->CGImage, + destruct_clear_history: @escaping()->CGImage, + chat_gigagroup_info: @escaping()->CGImage, + playlist_next: @escaping()->CGImage, + playlist_prev: @escaping()->CGImage, + playlist_next_locked: @escaping()->CGImage, + playlist_prev_locked: @escaping()->CGImage, + playlist_random: @escaping()->CGImage, + playlist_order_normal: @escaping()->CGImage, + playlist_order_reversed: @escaping()->CGImage, + playlist_order_random: @escaping()->CGImage, + playlist_repeat_none: @escaping()->CGImage, + playlist_repeat_circle: @escaping()->CGImage, + playlist_repeat_one: @escaping()->CGImage, + audioplayer_next: @escaping()->CGImage, + audioplayer_prev: @escaping()->CGImage, + audioplayer_dismiss: @escaping()->CGImage, + audioplayer_repeat_none: @escaping()->CGImage, + audioplayer_repeat_circle: @escaping()->CGImage, + audioplayer_repeat_one: @escaping()->CGImage, + audioplayer_locked_next: @escaping()->CGImage, + audioplayer_locked_prev: @escaping()->CGImage, + audioplayer_volume: @escaping()->CGImage, + audioplayer_volume_off: @escaping()->CGImage, + audioplayer_speed_x1: @escaping()->CGImage, + audioplayer_speed_x2: @escaping()->CGImage, + chat_info_voice_chat: @escaping()->CGImage, + chat_info_create_group: @escaping()->CGImage, + chat_info_change_colors: @escaping()->CGImage, + empty_chat_system: @escaping()->CGImage, + empty_chat_dark: @escaping()->CGImage, + empty_chat_light: @escaping()->CGImage, + empty_chat_system_active: @escaping()->CGImage, + empty_chat_dark_active: @escaping()->CGImage, + empty_chat_light_active: @escaping()->CGImage, + empty_chat_storage_clear: @escaping()->CGImage, + empty_chat_storage_low: @escaping()->CGImage, + empty_chat_storage_medium: @escaping()->CGImage, + empty_chat_storage_high: @escaping()->CGImage, + empty_chat_storage_low_active: @escaping()->CGImage, + empty_chat_storage_medium_active: @escaping()->CGImage, + empty_chat_storage_high_active: @escaping()->CGImage, + empty_chat_stickers_none: @escaping()->CGImage, + empty_chat_stickers_mysets: @escaping()->CGImage, + empty_chat_stickers_allsets: @escaping()->CGImage, + empty_chat_stickers_none_active: @escaping()->CGImage, + empty_chat_stickers_mysets_active: @escaping()->CGImage, + empty_chat_stickers_allsets_active: @escaping()->CGImage, + chat_action_dismiss: @escaping()->CGImage, + chat_action_edit_message: @escaping()->CGImage, + chat_action_forward_message: @escaping()->CGImage, + chat_action_reply_message: @escaping()->CGImage, + chat_action_url_preview: @escaping()->CGImage, + chat_action_menu_update_chat: @escaping()->CGImage, + chat_action_menu_selected: @escaping()->CGImage, + widget_peers_favorite: @escaping()->CGImage, + widget_peers_recent: @escaping()->CGImage, + widget_peers_both: @escaping()->CGImage, + widget_peers_favorite_active: @escaping()->CGImage, + widget_peers_recent_active: @escaping()->CGImage, + widget_peers_both_active: @escaping()->CGImage + ) { + self._dialogMuteImage = dialogMuteImage + self._dialogMuteImageSelected = dialogMuteImageSelected + self._outgoingMessageImage = outgoingMessageImage + self._readMessageImage = readMessageImage + self._outgoingMessageImageSelected = outgoingMessageImageSelected + self._readMessageImageSelected = readMessageImageSelected + self._sendingImage = sendingImage + self._sendingImageSelected = sendingImageSelected + self._secretImage = secretImage + self._secretImageSelected = secretImageSelected + self._pinnedImage = pinnedImage + self._pinnedImageSelected = pinnedImageSelected + self._verifiedImage = verifiedImage + self._verifiedImageSelected = verifiedImageSelected + self._errorImage = errorImage + self._errorImageSelected = errorImageSelected + self._chatSearch = chatSearch + self._chatSearchActive = chatSearchActive + self._chatCall = chatCall + self._chatCallActive = chatCallActive + self._chatActions = chatActions + self._chatFailedCall_incoming = chatFailedCall_incoming + self._chatFailedCall_outgoing = chatFailedCall_outgoing + self._chatCall_incoming = chatCall_incoming + self._chatCall_outgoing = chatCall_outgoing + self._chatFailedCallBubble_incoming = chatFailedCallBubble_incoming + self._chatFailedCallBubble_outgoing = chatFailedCallBubble_outgoing + self._chatCallBubble_incoming = chatCallBubble_incoming + self._chatCallBubble_outgoing = chatCallBubble_outgoing + self._chatFallbackCall = chatFallbackCall + self._chatFallbackCallBubble_incoming = chatFallbackCallBubble_incoming + self._chatFallbackCallBubble_outgoing = chatFallbackCallBubble_outgoing + self._chatFallbackVideoCall = chatFallbackVideoCall + self._chatFallbackVideoCallBubble_incoming = chatFallbackVideoCallBubble_incoming + self._chatFallbackVideoCallBubble_outgoing = chatFallbackVideoCallBubble_outgoing + self._chatToggleSelected = chatToggleSelected + self._chatToggleUnselected = chatToggleUnselected + self._chatMusicPlay = chatMusicPlay + self._chatMusicPlayBubble_incoming = chatMusicPlayBubble_incoming + self._chatMusicPlayBubble_outgoing = chatMusicPlayBubble_outgoing + self._chatMusicPause = chatMusicPause + self._chatMusicPauseBubble_incoming = chatMusicPauseBubble_incoming + self._chatMusicPauseBubble_outgoing = chatMusicPauseBubble_outgoing + self._chatGradientBubble_incoming = chatGradientBubble_incoming + self._chatGradientBubble_outgoing = chatGradientBubble_outgoing + self._chatBubble_none_incoming_withInset = chatBubble_none_incoming_withInset + self._chatBubble_none_outgoing_withInset = chatBubble_none_outgoing_withInset + self._chatBubbleBorder_none_incoming_withInset = chatBubbleBorder_none_incoming_withInset + self._chatBubbleBorder_none_outgoing_withInset = chatBubbleBorder_none_outgoing_withInset + self._chatBubble_both_incoming_withInset = chatBubble_both_incoming_withInset + self._chatBubble_both_outgoing_withInset = chatBubble_both_outgoing_withInset + self._chatBubbleBorder_both_incoming_withInset = chatBubbleBorder_both_incoming_withInset + self._chatBubbleBorder_both_outgoing_withInset = chatBubbleBorder_both_outgoing_withInset + self._composeNewChat = composeNewChat + self._composeNewChatActive = composeNewChatActive + self._composeNewGroup = composeNewGroup + self._composeNewSecretChat = composeNewSecretChat + self._composeNewChannel = composeNewChannel + self._contactsNewContact = contactsNewContact + self._chatReadMarkInBubble1_incoming = chatReadMarkInBubble1_incoming + self._chatReadMarkInBubble2_incoming = chatReadMarkInBubble2_incoming + self._chatReadMarkInBubble1_outgoing = chatReadMarkInBubble1_outgoing + self._chatReadMarkInBubble2_outgoing = chatReadMarkInBubble2_outgoing + self._chatReadMarkOutBubble1 = chatReadMarkOutBubble1 + self._chatReadMarkOutBubble2 = chatReadMarkOutBubble2 + self._chatReadMarkOverlayBubble1 = chatReadMarkOverlayBubble1 + self._chatReadMarkOverlayBubble2 = chatReadMarkOverlayBubble2 + self._sentFailed = sentFailed + self._chatChannelViewsInBubble_incoming = chatChannelViewsInBubble_incoming + self._chatChannelViewsInBubble_outgoing = chatChannelViewsInBubble_outgoing + self._chatChannelViewsOutBubble = chatChannelViewsOutBubble + self._chatChannelViewsOverlayBubble = chatChannelViewsOverlayBubble + self._chatNavigationBack = chatNavigationBack + self._peerInfoAddMember = peerInfoAddMember + self._chatSearchUp = chatSearchUp + self._chatSearchUpDisabled = chatSearchUpDisabled + self._chatSearchDown = chatSearchDown + self._chatSearchDownDisabled = chatSearchDownDisabled + self._chatSearchCalendar = chatSearchCalendar + self._dismissAccessory = dismissAccessory + self._chatScrollUp = chatScrollUp + self._chatScrollUpActive = chatScrollUpActive + self._chatSendMessage = chatSendMessage + self._chatSaveEditedMessage = chatSaveEditedMessage + self._chatRecordVoice = chatRecordVoice + self._chatEntertainment = chatEntertainment + self._chatInlineDismiss = chatInlineDismiss + self._chatActiveReplyMarkup = chatActiveReplyMarkup + self._chatDisabledReplyMarkup = chatDisabledReplyMarkup + self._chatSecretTimer = chatSecretTimer + self._chatForwardMessagesActive = chatForwardMessagesActive + self._chatForwardMessagesInactive = chatForwardMessagesInactive + self._chatDeleteMessagesActive = chatDeleteMessagesActive + self._chatDeleteMessagesInactive = chatDeleteMessagesInactive + self._generalNext = generalNext + self._generalNextActive = generalNextActive + self._generalSelect = generalSelect + self._chatVoiceRecording = chatVoiceRecording + self._chatVideoRecording = chatVideoRecording + self._chatRecord = chatRecord + self._deleteItem = deleteItem + self._deleteItemDisabled = deleteItemDisabled + self._chatAttach = chatAttach + self._chatAttachFile = chatAttachFile + self._chatAttachPhoto = chatAttachPhoto + self._chatAttachCamera = chatAttachCamera + self._chatAttachLocation = chatAttachLocation + self._chatAttachPoll = chatAttachPoll + self._mediaEmptyShared = mediaEmptyShared + self._mediaEmptyFiles = mediaEmptyFiles + self._mediaEmptyMusic = mediaEmptyMusic + self._mediaEmptyLinks = mediaEmptyLinks + self._stickersAddFeatured = stickersAddFeatured + self._stickersAddedFeatured = stickersAddedFeatured + self._stickersRemove = stickersRemove + self._peerMediaDownloadFileStart = peerMediaDownloadFileStart + self._peerMediaDownloadFilePause = peerMediaDownloadFilePause + self._stickersShare = stickersShare + self._emojiRecentTab = emojiRecentTab + self._emojiSmileTab = emojiSmileTab + self._emojiNatureTab = emojiNatureTab + self._emojiFoodTab = emojiFoodTab + self._emojiSportTab = emojiSportTab + self._emojiCarTab = emojiCarTab + self._emojiObjectsTab = emojiObjectsTab + self._emojiSymbolsTab = emojiSymbolsTab + self._emojiFlagsTab = emojiFlagsTab + self._emojiRecentTabActive = emojiRecentTabActive + self._emojiSmileTabActive = emojiSmileTabActive + self._emojiNatureTabActive = emojiNatureTabActive + self._emojiFoodTabActive = emojiFoodTabActive + self._emojiSportTabActive = emojiSportTabActive + self._emojiCarTabActive = emojiCarTabActive + self._emojiObjectsTabActive = emojiObjectsTabActive + self._emojiSymbolsTabActive = emojiSymbolsTabActive + self._emojiFlagsTabActive = emojiFlagsTabActive + self._stickerBackground = stickerBackground + self._stickerBackgroundActive = stickerBackgroundActive + self._stickersTabRecent = stickersTabRecent + self._stickersTabGIF = stickersTabGIF + self._chatSendingInFrame_incoming = chatSendingInFrame_incoming + self._chatSendingInHour_incoming = chatSendingInHour_incoming + self._chatSendingInMin_incoming = chatSendingInMin_incoming + self._chatSendingInFrame_outgoing = chatSendingInFrame_outgoing + self._chatSendingInHour_outgoing = chatSendingInHour_outgoing + self._chatSendingInMin_outgoing = chatSendingInMin_outgoing + self._chatSendingOutFrame = chatSendingOutFrame + self._chatSendingOutHour = chatSendingOutHour + self._chatSendingOutMin = chatSendingOutMin + self._chatSendingOverlayFrame = chatSendingOverlayFrame + self._chatSendingOverlayHour = chatSendingOverlayHour + self._chatSendingOverlayMin = chatSendingOverlayMin + self._chatActionUrl = chatActionUrl + self._callInlineDecline = callInlineDecline + self._callInlineMuted = callInlineMuted + self._callInlineUnmuted = callInlineUnmuted + self._eventLogTriangle = eventLogTriangle + self._channelIntro = channelIntro + self._chatFileThumb = chatFileThumb + self._chatFileThumbBubble_incoming = chatFileThumbBubble_incoming + self._chatFileThumbBubble_outgoing = chatFileThumbBubble_outgoing + self._chatSecretThumb = chatSecretThumb + self._chatSecretThumbSmall = chatSecretThumbSmall + self._chatMapPin = chatMapPin + self._chatSecretTitle = chatSecretTitle + self._emptySearch = emptySearch + self._calendarBack = calendarBack + self._calendarNext = calendarNext + self._calendarBackDisabled = calendarBackDisabled + self._calendarNextDisabled = calendarNextDisabled + self._newChatCamera = newChatCamera + self._peerInfoVerify = peerInfoVerify + self._peerInfoVerifyProfile = peerInfoVerifyProfile + self._peerInfoCall = peerInfoCall + self._callOutgoing = callOutgoing + self._recentDismiss = recentDismiss + self._recentDismissActive = recentDismissActive + self._webgameShare = webgameShare + self._chatSearchCancel = chatSearchCancel + self._chatSearchFrom = chatSearchFrom + self._callWindowDecline = callWindowDecline + self._callWindowDeclineSmall = callWindowDeclineSmall + self._callWindowAccept = callWindowAccept + self._callWindowVideo = callWindowVideo + self._callWindowVideoActive = callWindowVideoActive + self._callWindowMute = callWindowMute + self._callWindowMuteActive = callWindowMuteActive + self._callWindowClose = callWindowClose + self._callWindowDeviceSettings = callWindowDeviceSettings + self._callSettings = callSettings + self._callWindowCancel = callWindowCancel + self._chatActionEdit = chatActionEdit + self._chatActionInfo = chatActionInfo + self._chatActionMute = chatActionMute + self._chatActionUnmute = chatActionUnmute + self._chatActionClearHistory = chatActionClearHistory + self._chatActionDeleteChat = chatActionDeleteChat + self._dismissPinned = dismissPinned + self._chatActionsActive = chatActionsActive + self._chatEntertainmentSticker = chatEntertainmentSticker + self._chatEmpty = chatEmpty + self._stickerPackClose = stickerPackClose + self._stickerPackDelete = stickerPackDelete + self._modalShare = modalShare + self._modalClose = modalClose + self._ivChannelJoined = ivChannelJoined + self._chatListMention = chatListMention + self._chatListMentionActive = chatListMentionActive + self._chatListMentionArchived = chatListMentionArchived + self._chatListMentionArchivedActive = chatListMentionArchivedActive + self._chatMention = chatMention + self._chatMentionActive = chatMentionActive + self._sliderControl = sliderControl + self._sliderControlActive = sliderControlActive + self._stickersTabFave = stickersTabFave + self._chatInstantView = chatInstantView + self._chatInstantViewBubble_incoming = chatInstantViewBubble_incoming + self._chatInstantViewBubble_outgoing = chatInstantViewBubble_outgoing + self._instantViewShare = instantViewShare + self._instantViewActions = instantViewActions + self._instantViewActionsActive = instantViewActionsActive + self._instantViewSafari = instantViewSafari + self._instantViewBack = instantViewBack + self._instantViewCheck = instantViewCheck + self._groupStickerNotFound = groupStickerNotFound + self._settingsAskQuestion = settingsAskQuestion + self._settingsFaq = settingsFaq + self._settingsGeneral = settingsGeneral + self._settingsLanguage = settingsLanguage + self._settingsNotifications = settingsNotifications + self._settingsSecurity = settingsSecurity + self._settingsStickers = settingsStickers + self._settingsStorage = settingsStorage + self._settingsSessions = settingsSessions + self._settingsProxy = settingsProxy + self._settingsAppearance = settingsAppearance + self._settingsPassport = settingsPassport + self._settingsWallet = settingsWallet + self._settingsUpdate = settingsUpdate + self._settingsFilters = settingsFilters + self._settingsAskQuestionActive = settingsAskQuestionActive + self._settingsFaqActive = settingsFaqActive + self._settingsGeneralActive = settingsGeneralActive + self._settingsLanguageActive = settingsLanguageActive + self._settingsNotificationsActive = settingsNotificationsActive + self._settingsSecurityActive = settingsSecurityActive + self._settingsStickersActive = settingsStickersActive + self._settingsStorageActive = settingsStorageActive + self._settingsSessionsActive = settingsSessionsActive + self._settingsProxyActive = settingsProxyActive + self._settingsAppearanceActive = settingsAppearanceActive + self._settingsPassportActive = settingsPassportActive + self._settingsWalletActive = settingsWalletActive + self._settingsUpdateActive = settingsUpdateActive + self._settingsFiltersActive = settingsFiltersActive + self._settingsProfile = settingsProfile + self._generalCheck = generalCheck + self._settingsAbout = settingsAbout + self._settingsLogout = settingsLogout + self._fastSettingsLock = fastSettingsLock + self._fastSettingsDark = fastSettingsDark + self._fastSettingsSunny = fastSettingsSunny + self._fastSettingsMute = fastSettingsMute + self._fastSettingsUnmute = fastSettingsUnmute + self._chatRecordVideo = chatRecordVideo + self._inputChannelMute = inputChannelMute + self._inputChannelUnmute = inputChannelUnmute + self._changePhoneNumberIntro = changePhoneNumberIntro + self._peerSavedMessages = peerSavedMessages + self._previewSenderCollage = previewSenderCollage + self._previewSenderPhoto = previewSenderPhoto + self._previewSenderFile = previewSenderFile + self._previewSenderCrop = previewSenderCrop + self._previewSenderDelete = previewSenderDelete + self._previewSenderDeleteFile = previewSenderDeleteFile + self._previewSenderArchive = previewSenderArchive + self._chatGroupToggleSelected = chatGroupToggleSelected + self._chatGroupToggleUnselected = chatGroupToggleUnselected + self._successModalProgress = successModalProgress + self._accentColorSelect = accentColorSelect + self._transparentBackground = transparentBackground + self._lottieTransparentBackground = lottieTransparentBackground + self._passcodeTouchId = passcodeTouchId + self._passcodeLogin = passcodeLogin + self._confirmDeleteMessagesAccessory = confirmDeleteMessagesAccessory + self._alertCheckBoxSelected = alertCheckBoxSelected + self._alertCheckBoxUnselected = alertCheckBoxUnselected + self._confirmPinAccessory = confirmPinAccessory + self._confirmDeleteChatAccessory = confirmDeleteChatAccessory + self._stickersEmptySearch = stickersEmptySearch + self._twoStepVerificationCreateIntro = twoStepVerificationCreateIntro + self._secureIdAuth = secureIdAuth + self._ivAudioPlay = ivAudioPlay + self._ivAudioPause = ivAudioPause + self._proxyEnable = proxyEnable + self._proxyEnabled = proxyEnabled + self._proxyState = proxyState + self._proxyDeleteListItem = proxyDeleteListItem + self._proxyInfoListItem = proxyInfoListItem + self._proxyConnectedListItem = proxyConnectedListItem + self._proxyAddProxy = proxyAddProxy + self._proxyNextWaitingListItem = proxyNextWaitingListItem + self._passportForgotPassword = passportForgotPassword + self._confirmAppAccessoryIcon = confirmAppAccessoryIcon + self._passportPassport = passportPassport + self._passportIdCardReverse = passportIdCardReverse + self._passportIdCard = passportIdCard + self._passportSelfie = passportSelfie + self._passportDriverLicense = passportDriverLicense + self._chatOverlayVoiceRecording = chatOverlayVoiceRecording + self._chatOverlayVideoRecording = chatOverlayVideoRecording + self._chatOverlaySendRecording = chatOverlaySendRecording + self._chatOverlayLockArrowRecording = chatOverlayLockArrowRecording + self._chatOverlayLockerBodyRecording = chatOverlayLockerBodyRecording + self._chatOverlayLockerHeadRecording = chatOverlayLockerHeadRecording + self._locationPin = locationPin + self._locationMapPin = locationMapPin + self._locationMapLocate = locationMapLocate + self._locationMapLocated = locationMapLocated + self._passportSettings = passportSettings + self._passportInfo = passportInfo + self._editMessageMedia = editMessageMedia + self._playerMusicPlaceholder = playerMusicPlaceholder + self._chatMusicPlaceholder = chatMusicPlaceholder + self._chatMusicPlaceholderCap = chatMusicPlaceholderCap + self._searchArticle = searchArticle + self._searchSaved = searchSaved + self._archivedChats = archivedChats + self._hintPeerActive = hintPeerActive + self._hintPeerActiveSelected = hintPeerActiveSelected + self._chatSwiping_delete = chatSwiping_delete + self._chatSwiping_mute = chatSwiping_mute + self._chatSwiping_unmute = chatSwiping_unmute + self._chatSwiping_read = chatSwiping_read + self._chatSwiping_unread = chatSwiping_unread + self._chatSwiping_pin = chatSwiping_pin + self._chatSwiping_unpin = chatSwiping_unpin + self._chatSwiping_archive = chatSwiping_archive + self._chatSwiping_unarchive = chatSwiping_unarchive + self._galleryPrev = galleryPrev + self._galleryNext = galleryNext + self._galleryMore = galleryMore + self._galleryShare = galleryShare + self._galleryFastSave = galleryFastSave + self._galleryRotate = galleryRotate + self._galleryZoomIn = galleryZoomIn + self._galleryZoomOut = galleryZoomOut + self._editMessageCurrentPhoto = editMessageCurrentPhoto + self._videoPlayerPlay = videoPlayerPlay + self._videoPlayerPause = videoPlayerPause + self._videoPlayerEnterFullScreen = videoPlayerEnterFullScreen + self._videoPlayerExitFullScreen = videoPlayerExitFullScreen + self._videoPlayerPIPIn = videoPlayerPIPIn + self._videoPlayerPIPOut = videoPlayerPIPOut + self._videoPlayerRewind15Forward = videoPlayerRewind15Forward + self._videoPlayerRewind15Backward = videoPlayerRewind15Backward + self._videoPlayerVolume = videoPlayerVolume + self._videoPlayerVolumeOff = videoPlayerVolumeOff + self._videoPlayerClose = videoPlayerClose + self._videoPlayerSliderInteractor = videoPlayerSliderInteractor + self._streamingVideoDownload = streamingVideoDownload + self._videoCompactFetching = videoCompactFetching + self._compactStreamingFetchingCancel = compactStreamingFetchingCancel + self._customLocalizationDelete = customLocalizationDelete + self._pollAddOption = pollAddOption + self._pollDeleteOption = pollDeleteOption + self._resort = resort + self._chatPollVoteUnselected = chatPollVoteUnselected + self._chatPollVoteUnselectedBubble_incoming = chatPollVoteUnselectedBubble_incoming + self._chatPollVoteUnselectedBubble_outgoing = chatPollVoteUnselectedBubble_outgoing + self._peerInfoAdmins = peerInfoAdmins + self._peerInfoPermissions = peerInfoPermissions + self._peerInfoBanned = peerInfoBanned + self._peerInfoMembers = peerInfoMembers + self._chatUndoAction = chatUndoAction + self._appUpdate = appUpdate + self._inlineVideoSoundOff = inlineVideoSoundOff + self._inlineVideoSoundOn = inlineVideoSoundOn + self._logoutOptionAddAccount = logoutOptionAddAccount + self._logoutOptionSetPasscode = logoutOptionSetPasscode + self._logoutOptionClearCache = logoutOptionClearCache + self._logoutOptionChangePhoneNumber = logoutOptionChangePhoneNumber + self._logoutOptionContactSupport = logoutOptionContactSupport + self._disableEmojiPrediction = disableEmojiPrediction + self._scam = scam + self._scamActive = scamActive + self._chatScam = chatScam + self._fake = fake + self._fakeActive = fakeActive + self._chatFake = chatFake + self._chatUnarchive = chatUnarchive + self._chatArchive = chatArchive + self._privacySettings_blocked = privacySettings_blocked + self._privacySettings_activeSessions = privacySettings_activeSessions + self._privacySettings_passcode = privacySettings_passcode + self._privacySettings_twoStep = privacySettings_twoStep + self._deletedAccount = deletedAccount + self._stickerPackSelection = stickerPackSelection + self._stickerPackSelectionActive = stickerPackSelectionActive + self._entertainment_Emoji = entertainment_Emoji + self._entertainment_Stickers = entertainment_Stickers + self._entertainment_Gifs = entertainment_Gifs + self._entertainment_Search = entertainment_Search + self._entertainment_Settings = entertainment_Settings + self._entertainment_SearchCancel = entertainment_SearchCancel + self._scheduledAvatar = scheduledAvatar + self._scheduledInputAction = scheduledInputAction + self._verifyDialog = verifyDialog + self._verifyDialogActive = verifyDialogActive + self._chatInputScheduled = chatInputScheduled + self._appearanceAddPlatformTheme = appearanceAddPlatformTheme + self._wallet_close = wallet_close + self._wallet_qr = wallet_qr + self._wallet_receive = wallet_receive + self._wallet_send = wallet_send + self._wallet_settings = wallet_settings + self._wallet_update = wallet_update + self._wallet_passcode_visible = wallet_passcode_visible + self._wallet_passcode_hidden = wallet_passcode_hidden + self._wallpaper_color_close = wallpaper_color_close + self._wallpaper_color_add = wallpaper_color_add + self._wallpaper_color_swap = wallpaper_color_swap + self._wallpaper_color_rotate = wallpaper_color_rotate + self._wallpaper_color_play = wallpaper_color_play + self._login_cap = login_cap + self._login_qr_cap = login_qr_cap + self._login_qr_empty_cap = login_qr_empty_cap + self._chat_failed_scroller = chat_failed_scroller + self._chat_failed_scroller_active = chat_failed_scroller_active + self._poll_quiz_unselected = poll_quiz_unselected + self._poll_selected = poll_selected + self._poll_selection = poll_selection + self._poll_selected_correct = poll_selected_correct + self._poll_selected_incorrect = poll_selected_incorrect + self._poll_selected_incoming = poll_selected_incoming + self._poll_selection_incoming = poll_selection_incoming + self._poll_selected_correct_incoming = poll_selected_correct_incoming + self._poll_selected_incorrect_incoming = poll_selected_incorrect_incoming + self._poll_selected_outgoing = poll_selected_outgoing + self._poll_selection_outgoing = poll_selection_outgoing + self._poll_selected_correct_outgoing = poll_selected_correct_outgoing + self._poll_selected_incorrect_outgoing = poll_selected_incorrect_outgoing + self._chat_filter_edit = chat_filter_edit + self._chat_filter_add = chat_filter_add + self._chat_filter_bots = chat_filter_bots + self._chat_filter_channels = chat_filter_channels + self._chat_filter_custom = chat_filter_custom + self._chat_filter_groups = chat_filter_groups + self._chat_filter_muted = chat_filter_muted + self._chat_filter_private_chats = chat_filter_private_chats + self._chat_filter_read = chat_filter_read + self._chat_filter_secret_chats = chat_filter_secret_chats + self._chat_filter_unmuted = chat_filter_unmuted + self._chat_filter_unread = chat_filter_unread + self._chat_filter_large_groups = chat_filter_large_groups + self._chat_filter_non_contacts = chat_filter_non_contacts + self._chat_filter_archive = chat_filter_archive + self._chat_filter_bots_avatar = chat_filter_bots_avatar + self._chat_filter_channels_avatar = chat_filter_channels_avatar + self._chat_filter_custom_avatar = chat_filter_custom_avatar + self._chat_filter_groups_avatar = chat_filter_groups_avatar + self._chat_filter_muted_avatar = chat_filter_muted_avatar + self._chat_filter_private_chats_avatar = chat_filter_private_chats_avatar + self._chat_filter_read_avatar = chat_filter_read_avatar + self._chat_filter_secret_chats_avatar = chat_filter_secret_chats_avatar + self._chat_filter_unmuted_avatar = chat_filter_unmuted_avatar + self._chat_filter_unread_avatar = chat_filter_unread_avatar + self._chat_filter_large_groups_avatar = chat_filter_large_groups_avatar + self._chat_filter_non_contacts_avatar = chat_filter_non_contacts_avatar + self._chat_filter_archive_avatar = chat_filter_archive_avatar + self._group_invite_via_link = group_invite_via_link + self._tab_contacts = tab_contacts + self._tab_contacts_active = tab_contacts_active + self._tab_calls = tab_calls + self._tab_calls_active = tab_calls_active + self._tab_chats = tab_chats + self._tab_chats_active = tab_chats_active + self._tab_chats_active_filters = tab_chats_active_filters + self._tab_settings = tab_settings + self._tab_settings_active = tab_settings_active + self._profile_add_member = profile_add_member + self._profile_call = profile_call + self._profile_video_call = profile_video_call + self._profile_leave = profile_leave + self._profile_message = profile_message + self._profile_more = profile_more + self._profile_mute = profile_mute + self._profile_unmute = profile_unmute + self._profile_search = profile_search + self._profile_secret_chat = profile_secret_chat + self._profile_edit_photo = profile_edit_photo + self._profile_block = profile_block + self._profile_report = profile_report + self._profile_share = profile_share + self._profile_stats = profile_stats + self._profile_unblock = profile_unblock + self._chat_quiz_explanation = chat_quiz_explanation + self._chat_quiz_explanation_bubble_incoming = chat_quiz_explanation_bubble_incoming + self._chat_quiz_explanation_bubble_outgoing = chat_quiz_explanation_bubble_outgoing + self._stickers_add_featured = stickers_add_featured + self._stickers_add_featured_unread = stickers_add_featured_unread + self._channel_info_promo = channel_info_promo + self._channel_info_promo_bubble_incoming = channel_info_promo_bubble_incoming + self._channel_info_promo_bubble_outgoing = channel_info_promo_bubble_outgoing + self._chat_share_message = chat_share_message + self._chat_goto_message = chat_goto_message + self._chat_swipe_reply = chat_swipe_reply + self._chat_like_message = chat_like_message + self._chat_like_message_unlike = chat_like_message_unlike + self._chat_like_inside = chat_like_inside + self._chat_like_inside_bubble_incoming = chat_like_inside_bubble_incoming + self._chat_like_inside_bubble_outgoing = chat_like_inside_bubble_outgoing + self._chat_like_inside_bubble_overlay = chat_like_inside_bubble_overlay + self._chat_like_inside_empty = chat_like_inside_empty + self._chat_like_inside_empty_bubble_incoming = chat_like_inside_empty_bubble_incoming + self._chat_like_inside_empty_bubble_outgoing = chat_like_inside_empty_bubble_outgoing + self._chat_like_inside_empty_bubble_overlay = chat_like_inside_empty_bubble_overlay + self._gif_trending = gif_trending + self._chat_list_thumb_play = chat_list_thumb_play + self._call_tooltip_battery_low = call_tooltip_battery_low + self._call_tooltip_camera_off = call_tooltip_camera_off + self._call_tooltip_micro_off = call_tooltip_micro_off + self._call_screen_sharing = call_screen_sharing + self._call_screen_sharing_active = call_screen_sharing_active + self._call_screen_settings = call_screen_settings + self._search_filter = search_filter + self._search_filter_media = search_filter_media + self._search_filter_files = search_filter_files + self._search_filter_links = search_filter_links + self._search_filter_music = search_filter_music + self._search_filter_add_peer = search_filter_add_peer + self._search_filter_add_peer_active = search_filter_add_peer_active + self._chat_reply_count_bubble_incoming = chat_reply_count_bubble_incoming + self._chat_reply_count_bubble_outgoing = chat_reply_count_bubble_outgoing + self._chat_reply_count = chat_reply_count + self._chat_reply_count_overlay = chat_reply_count_overlay + self._channel_comments_bubble = channel_comments_bubble + self._channel_comments_bubble_next = channel_comments_bubble_next + self._channel_comments_list = channel_comments_list + self._channel_comments_overlay = channel_comments_overlay + self._chat_replies_avatar = chat_replies_avatar + self._group_selection_foreground = group_selection_foreground + self._group_selection_foreground_bubble_incoming = group_selection_foreground_bubble_incoming + self._group_selection_foreground_bubble_outgoing = group_selection_foreground_bubble_outgoing + self._chat_pinned_list = chat_pinned_list + self._chat_pinned_message = chat_pinned_message + self._chat_pinned_message_bubble_incoming = chat_pinned_message_bubble_incoming + self._chat_pinned_message_bubble_outgoing = chat_pinned_message_bubble_outgoing + self._chat_pinned_message_overlay_bubble = chat_pinned_message_overlay_bubble + self._chat_voicechat_can_unmute = chat_voicechat_can_unmute + self._chat_voicechat_cant_unmute = chat_voicechat_cant_unmute + self._chat_voicechat_unmuted = chat_voicechat_unmuted + self._profile_voice_chat = profile_voice_chat + self._chat_voice_chat = chat_voice_chat + self._chat_voice_chat_active = chat_voice_chat_active + self._editor_draw = editor_draw + self._editor_delete = editor_delete + self._editor_crop = editor_crop + self._fast_copy_link = fast_copy_link + self._profile_channel_sign = profile_channel_sign + self._profile_channel_type = profile_channel_type + self._profile_group_type = profile_group_type + self._profile_group_destruct = profile_group_destruct + self._profile_group_discussion = profile_group_discussion + self._profile_removed = profile_removed + self._profile_links = profile_links + self._destruct_clear_history = destruct_clear_history + self._chat_gigagroup_info = chat_gigagroup_info + self._playlist_next = playlist_next + self._playlist_prev = playlist_prev + self._playlist_next_locked = playlist_next_locked + self._playlist_prev_locked = playlist_prev_locked + self._playlist_random = playlist_random + self._playlist_order_normal = playlist_order_normal + self._playlist_order_reversed = playlist_order_reversed + self._playlist_order_random = playlist_order_random + self._playlist_repeat_none = playlist_repeat_none + self._playlist_repeat_circle = playlist_repeat_circle + self._playlist_repeat_one = playlist_repeat_one + self._audioplayer_next = audioplayer_next + self._audioplayer_prev = audioplayer_prev + self._audioplayer_dismiss = audioplayer_dismiss + self._audioplayer_repeat_none = audioplayer_repeat_none + self._audioplayer_repeat_circle = audioplayer_repeat_circle + self._audioplayer_repeat_one = audioplayer_repeat_one + self._audioplayer_locked_next = audioplayer_locked_next + self._audioplayer_locked_prev = audioplayer_locked_prev + self._audioplayer_volume = audioplayer_volume + self._audioplayer_volume_off = audioplayer_volume_off + self._audioplayer_speed_x1 = audioplayer_speed_x1 + self._audioplayer_speed_x2 = audioplayer_speed_x2 + self._chat_info_voice_chat = chat_info_voice_chat + self._chat_info_create_group = chat_info_create_group + self._chat_info_change_colors = chat_info_change_colors + self._empty_chat_system = empty_chat_system + self._empty_chat_dark = empty_chat_dark + self._empty_chat_light = empty_chat_light + self._empty_chat_system_active = empty_chat_system_active + self._empty_chat_dark_active = empty_chat_dark_active + self._empty_chat_light_active = empty_chat_light_active + self._empty_chat_storage_clear = empty_chat_storage_clear + self._empty_chat_storage_low = empty_chat_storage_low + self._empty_chat_storage_medium = empty_chat_storage_medium + self._empty_chat_storage_high = empty_chat_storage_high + self._empty_chat_storage_low_active = empty_chat_storage_low_active + self._empty_chat_storage_medium_active = empty_chat_storage_medium_active + self._empty_chat_storage_high_active = empty_chat_storage_high_active + self._empty_chat_stickers_none = empty_chat_stickers_none + self._empty_chat_stickers_mysets = empty_chat_stickers_mysets + self._empty_chat_stickers_allsets = empty_chat_stickers_allsets + self._empty_chat_stickers_none_active = empty_chat_stickers_none_active + self._empty_chat_stickers_mysets_active = empty_chat_stickers_mysets_active + self._empty_chat_stickers_allsets_active = empty_chat_stickers_allsets_active + self._chat_action_dismiss = chat_action_dismiss + self._chat_action_edit_message = chat_action_edit_message + self._chat_action_forward_message = chat_action_forward_message + self._chat_action_reply_message = chat_action_reply_message + self._chat_action_url_preview = chat_action_url_preview + self._chat_action_menu_update_chat = chat_action_menu_update_chat + self._chat_action_menu_selected = chat_action_menu_selected + self._widget_peers_favorite = widget_peers_favorite + self._widget_peers_recent = widget_peers_recent + self._widget_peers_both = widget_peers_both + self._widget_peers_favorite_active = widget_peers_favorite_active + self._widget_peers_recent_active = widget_peers_recent_active + self._widget_peers_both_active = widget_peers_both_active + } +} \ No newline at end of file diff --git a/Telegram-Mac/TelegramShare-Objc-Bridge-Header.h b/Telegram-Mac/TelegramShare-Objc-Bridge-Header.h new file mode 100644 index 0000000000..f15bfcb98b --- /dev/null +++ b/Telegram-Mac/TelegramShare-Objc-Bridge-Header.h @@ -0,0 +1,15 @@ +// +// TelegramShare-Objc-Bridge-Header.h +// Telegram +// +// Created by Mikhail Filimonov on 19/03/2018. +// Copyright © 2018 Telegram. All rights reserved. +// +#ifndef TelegramShare_Objc_Bridge_Header_h +#define TelegramShare_Objc_Bridge_Header_h + +#import "NumberPluralizationForm.h" + + + +#endif diff --git a/Telegram-Mac/TemplateController.swift b/Telegram-Mac/TemplateController.swift new file mode 100644 index 0000000000..9cd0f612a5 --- /dev/null +++ b/Telegram-Mac/TemplateController.swift @@ -0,0 +1,83 @@ + +import Cocoa +import TGUIKit +import SwiftSignalKit + +private final class Arguments { + let context: AccountContext + init(context: AccountContext) { + self.context = context + } +} + +private struct State : Equatable { + +} + + +private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] { + var entries:[InputDataEntry] = [] + + var sectionId:Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + // entries + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + +func template(context: AccountContext) -> InputDataController { + + let actionsDisposable = DisposableSet() + + let initialState = State() + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let arguments = Arguments(context: context) + + let signal = statePromise.get() |> deliverOnPrepareQueue |> map { state in + return InputDataSignalValue(entries: entries(state, arguments: arguments)) + } + + let controller = InputDataController(dataSignal: signal, title: " ") + + controller.onDeinit = { + actionsDisposable.dispose() + } + + return controller + +} + + +/* + let modalInteractions = ModalInteractions(acceptTitle: "PAY", accept: { [weak controller] in + _ = controller?.returnKeyAction() + }, drawBorder: true, height: 50, singleButton: true) + + let modalController = InputDataModalController(controller, modalInteractions: modalInteractions) + + controller.leftModalHeader = ModalHeaderData(image: theme.icons.modalClose, handler: { [weak modalController] in + modalController?.close() + }) + + close = { [weak modalController] in + modalController?.modal?.close() + } + + return modalController + */ + + + diff --git a/Telegram-Mac/TermsModalController.swift b/Telegram-Mac/TermsModalController.swift new file mode 100644 index 0000000000..487800f7c3 --- /dev/null +++ b/Telegram-Mac/TermsModalController.swift @@ -0,0 +1,196 @@ +// +// TermsModalController.swift +// Telegram +// +// Created by Mikhail Filimonov on 04/06/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + +private final class TermsView : View { + private let headerView: View = View() + private let titleView = TextView() + let tableView = TableView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(headerView) + addSubview(tableView) + headerView.addSubview(titleView) + headerView.border = [.Bottom] + let title: TextViewLayout = TextViewLayout.init(NSAttributedString.initialize(string: L10n.termsOfServiceTitle, color: theme.colors.text, font: .medium(.title))) + title.measure(width: frameRect.width - 20) + titleView.update(title) + } + + override func layout() { + super.layout() + headerView.frame = NSMakeRect(0, 0, frame.width, 50) + tableView.frame = NSMakeRect(0, 60, frame.width, frame.height - 60) + titleView.center() + } + + func updateText(_ text: NSAttributedString, openBot:@escaping(String)->Void) { + tableView.removeAll() + let initialSize = NSMakeSize(380, tableView.frame.height) + let item = GeneralTextRowItem(initialSize, text: text, linkExecutor: TextViewInteractions(processURL: { url in + if let url = url as? String, !url.isEmpty { + if url.hasPrefix("@") { + openBot(url) + } else { + execute(inapp: .external(link: url, false)) + } + } + })) + _ = tableView.addItem(item: item) + + + needsLayout = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} +class TermsModalController: ModalViewController { + + override func viewClass() -> AnyClass { + return TermsView.self + } + + override open func measure(size: NSSize) { + self.modal?.resize(with:NSMakeSize(380, min(size.height - 70, genericView.tableView.listHeight + 70)), animated: false) + } + + public func updateSize(_ animated: Bool) { + if let contentSize = self.modal?.window.contentView?.frame.size { + self.modal?.resize(with:NSMakeSize(380, min(contentSize.height - 70, genericView.tableView.listHeight + 70)), animated: animated) + } + } + override var dynamicSize: Bool { + return true + } + + + override var handleAllEvents: Bool { + return true + } + + override var modalInteractions: ModalInteractions? { + let network = self.context.account.network + let terms = self.terms + let context = self.context + let accept:()->Void = { [weak self] in + guard let `self` = self else {return} + + + + _ = showModalProgress(signal: context.engine.accountData.acceptTermsOfService(id: terms.id) |> deliverOnMainQueue, for: context.window).start(next: { [weak self] in + self?.close() + }) + if let botname = self.proceedBotAfterAgree { + _ = (self.context.engine.peers.resolvePeerByName(name: botname) |> deliverOnMainQueue).start(next: { [weak self] peerId in + guard let `self` = self else {return} + if let peerId = peerId { + self.context.sharedContext.bindings.rootNavigation().push(ChatController(context: self.context, chatLocation: .peer(peerId._asPeer().id))) + } + }) + } + } + return ModalInteractions(acceptTitle: L10n.termsOfServiceAccept, accept: { + if let age = terms.ageConfirmation { + confirm(for: mainWindow, header: L10n.termsOfServiceTitle, information: L10n.termsOfServiceConfirmAge("\(age)"), okTitle: L10n.termsOfServiceAcceptConfirmAge, successHandler: { _ in + accept() + }) + } else { + accept() + } + }, cancelTitle: L10n.termsOfServiceDisagree, cancel: { + confirm(for: context.window, header: L10n.termsOfServiceTitle, information: L10n.termsOfServiceDisagreeText, okTitle: L10n.termsOfServiceDisagreeOK, successHandler: { _ in + confirm(for: context.window, header: L10n.termsOfServiceTitle, information: L10n.termsOfServiceDisagreeTextLast, okTitle: L10n.termsOfServiceDisagreeTextLastOK, successHandler: { _ in + context.engine.accountData.resetAccountDueTermsOfService().start() + }) + }) + }, drawBorder: true, height: 50, alignCancelLeft: true) + + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillDisappear(animated) + modal?.interactions?.updateCancel { control in + control.set(color: theme.colors.redUI, for: .Normal) + } + } + + + private let context: AccountContext + private let terms: TermsOfServiceUpdate + private var proceedBotAfterAgree: String? = nil + init(_ context: AccountContext, terms: TermsOfServiceUpdate) { + self.context = context + self.terms = terms + super.init(frame: NSMakeRect(0, 0, 380, 380)) + } + + override func viewDidResized(_ size: NSSize) { + super.viewDidResized(size) + } + + private var genericView: TermsView { + return self.view as! TermsView + } + + override func escapeKeyAction() -> KeyHandlerResult { + return .invoked + } + + deinit { + } + + + override var closable: Bool { + return false + } + + override func viewDidLoad() { + super.viewDidLoad() + let attributedString: NSMutableAttributedString = NSMutableAttributedString() + + _ = attributedString.append(string: terms.text, color: theme.colors.text, font: .normal(.text)) + + for entity in terms.entities { + switch entity.type { + case .Bold: + attributedString.addAttribute(NSAttributedString.Key.font, value: NSFont.bold(.text), range: NSMakeRange(entity.range.lowerBound, entity.range.upperBound - entity.range.lowerBound)) + case .Italic: + attributedString.addAttribute(NSAttributedString.Key.font, value: NSFont.italic(.text), range: NSMakeRange(entity.range.lowerBound, entity.range.upperBound - entity.range.lowerBound)) + case let .TextUrl(url): + attributedString.addAttribute(NSAttributedString.Key.link, value: url, range: NSMakeRange(entity.range.lowerBound, entity.range.upperBound - entity.range.lowerBound)) + attributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: theme.colors.link, range: NSMakeRange(entity.range.lowerBound, entity.range.upperBound - entity.range.lowerBound)) + case .Mention: + attributedString.addAttribute(NSAttributedString.Key.link, value: terms.text.nsstring.substring(with: NSMakeRange(entity.range.lowerBound, entity.range.upperBound - entity.range.lowerBound)), range: NSMakeRange(entity.range.lowerBound, entity.range.upperBound - entity.range.lowerBound)) + attributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: theme.colors.link, range: NSMakeRange(entity.range.lowerBound, entity.range.upperBound - entity.range.lowerBound)) + default: + break + } + } + + genericView.updateText(attributedString, openBot: { [weak self] botname in + guard let `self` = self else {return} + self.proceedBotAfterAgree = botname + self.show(toaster: ControllerToaster(text: L10n.termsOfServiceProceedBot(botname))) + }) + + + + updateSize(false) + readyOnce() + } + + +} diff --git a/Telegram-Mac/TextAndLabelItem.swift b/Telegram-Mac/TextAndLabelItem.swift index 8bacddc9fc..83debc2040 100644 --- a/Telegram-Mac/TextAndLabelItem.swift +++ b/Telegram-Mac/TextAndLabelItem.swift @@ -8,9 +8,10 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit class TextAndLabelItem: GeneralRowItem { @@ -25,20 +26,72 @@ class TextAndLabelItem: GeneralRowItem { var textLayout:TextViewLayout let isTextSelectable:Bool let callback:()->Void - let account:Account - init(_ initialSize:NSSize, stableId:AnyHashable, label:String, text:String, account:Account, detectLinks:Bool = false, isTextSelectable:Bool = true, callback:@escaping ()->Void = {}, openInfo:((PeerId, Bool, MessageId?, ChatInitialAction?)->Void)? = nil, hashtag:((String)->Void)? = nil) { - self.account = account + let canCopy: Bool + + + var hasMore: Bool? = true { + didSet { + if hasMore == nil { + textLayout.maximumNumberOfLines = 0 + textLayout.cutout = nil + _ = makeSize(width, oldWidth: 0) + + if let table = self.table { + table.enumerateItems { item -> Bool in + item.table?.reloadData(row: item.index, animated: true) + return true + } + } + + } + } + } + + let moreLayout: TextViewLayout + let copyMenuText: String + let _copyToClipboard:(()->Void)? + init(_ initialSize:NSSize, stableId:AnyHashable, label:String, copyMenuText: String, labelColor: NSColor = theme.colors.accent, text:String, context: AccountContext, viewType: GeneralViewType = .legacy, detectLinks:Bool = false, onlyInApp: Bool = false, isTextSelectable:Bool = true, callback:@escaping ()->Void = {}, openInfo:((PeerId, Bool, MessageId?, ChatInitialAction?)->Void)? = nil, hashtag:((String)->Void)? = nil, selectFullWord: Bool = false, canCopy: Bool = true, _copyToClipboard:(()->Void)? = nil) { self.callback = callback self.isTextSelectable = isTextSelectable - self.label = NSAttributedString.initialize(string: label, color: theme.colors.blueUI, font: .normal(FontSize.text)) + self.copyMenuText = copyMenuText + self.label = NSAttributedString.initialize(string: label, color: labelColor, font: .normal(FontSize.text)) let attr = NSMutableAttributedString() _ = attr.append(string: text.trimmed.fullTrimmed, color: theme.colors.text, font: .normal(.title)) if detectLinks { - attr.detectLinks(type: [.Links, .Hashtags, .Mentions], account: account, openInfo: openInfo, hashtag: hashtag) + attr.detectLinks(type: [.Links, .Hashtags, .Mentions], onlyInApp: onlyInApp, context: context, color: theme.colors.link, openInfo: openInfo, hashtag: hashtag, applyProxy: { settings in + applyExternalProxy(settings, accountManager: context.sharedContext.accountManager) + }) } - textLayout = TextViewLayout(attr) + self.canCopy = canCopy + self._copyToClipboard = _copyToClipboard + + textLayout = TextViewLayout(attr, maximumNumberOfLines: 3, alwaysStaticItems: !detectLinks) textLayout.interactions = globalLinkExecutor - super.init(initialSize,stableId: stableId, type: .none, action: callback, drawCustomSeparator: true) + textLayout.selectWholeText = !detectLinks + if selectFullWord { + textLayout.interactions.copy = { + copyToClipboard(text) + return true + } + } + + var showFull:(()->Void)? = nil + + let moreAttr = parseMarkdownIntoAttributedString(L10n.peerInfoShowMoreText, attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: .normal(.title), textColor: theme.colors.text), bold: MarkdownAttributeSet(font: .bold(.title), textColor: theme.colors.text), link: MarkdownAttributeSet(font: .normal(.title), textColor: theme.colors.link), linkAttribute: { contents in + return (NSAttributedString.Key.link.rawValue, inAppLink.callback(contents, { _ in + showFull?() + })) + })) + self.moreLayout = TextViewLayout(moreAttr) + self.moreLayout.interactions = globalLinkExecutor + + + self.moreLayout.measure(width: .greatestFiniteMagnitude) + super.init(initialSize,stableId: stableId, type: .none, viewType: viewType, action: callback, drawCustomSeparator: true) + + showFull = { [weak self] in + self?.hasMore = nil + } } override func viewClass() -> AnyClass { @@ -46,11 +99,21 @@ class TextAndLabelItem: GeneralRowItem { } var textWidth:CGFloat { - return width - inset.left - inset.right + switch viewType { + case .legacy: + return width - inset.left - inset.right + case let .modern(_, inner): + return blockWidth - inner.left - inner.right + } } override var height: CGFloat { - return labelsHeight + 20 + switch viewType { + case .legacy: + return labelsHeight + 20 + case let .modern(_, insets): + return labelsHeight + insets.top + insets.bottom - 4 + } } var labelsHeight:CGFloat { @@ -73,41 +136,74 @@ class TextAndLabelItem: GeneralRowItem { return (height - labelsHeight) / 2.0 } -// override func menuItems() -> Signal<[ContextMenuItem], Void> { -// return .single([ContextMenuItem(tr(.textCopy), handler: { [weak self] in -// if let strongSelf = self { -// copyToClipboard(strongSelf.textLayout.attributedString.string) -// } -// -// })]) -// } -// + override func menuItems(in location: NSPoint) -> Signal<[ContextMenuItem], NoError> { + if !canCopy { + return .single([]) + } else { + return .single([ContextMenuItem(self.copyMenuText, handler: { [weak self] in + if let strongSelf = self { + copyToClipboard(strongSelf.textLayout.attributedString.string) + } + })]) + } + + } +// override func makeSize(_ width: CGFloat, oldWidth:CGFloat) -> Bool { + let result = super.makeSize(width, oldWidth: oldWidth) textLayout.measure(width: textWidth) + + if hasMore != nil { + hasMore = !textLayout.isPerfectSized + } + if hasMore == true { + textLayout.cutout = TextViewCutout(bottomRight: NSMakeSize(moreLayout.layoutSize.width + 10, 0)) + textLayout.measure(width: textWidth) + } + labelLayout = TextNode.layoutText(maybeNode: nil, label, nil, 1, .end, NSMakeSize(textWidth, .greatestFiniteMagnitude), nil, false, .left) - return super.makeSize(width, oldWidth: oldWidth) + return result } } class TextAndLabelRowView: GeneralRowView { - + private let containerView = GeneralRowContainerView(frame: NSZeroRect) private var labelView:TextView = TextView() - + private let moreView: TextView = TextView() + private let copyView: ImageButton = ImageButton() override func draw(_ layer: CALayer, in ctx: CGContext) { - super.draw(layer, in: ctx) - if let item = item as? TextAndLabelItem, let label = item.labelLayout { - - label.1.draw(NSMakeRect(item.inset.left, item.labelY, label.0.size.width, label.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor) - if item.drawCustomSeparator { - ctx.setFillColor(theme.colors.border.cgColor) - ctx.fill(NSMakeRect(item.inset.left, frame.height - .borderSize, frame.width - item.inset.left - item.inset.right, .borderSize)) + if let item = item as? TextAndLabelItem, let label = item.labelLayout, layer == containerView.layer { + switch item.viewType { + case .legacy: + label.1.draw(NSMakeRect(item.inset.left, item.labelY, label.0.size.width, label.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backdorColor) + if item.drawCustomSeparator { + ctx.setFillColor(theme.colors.border.cgColor) + ctx.fill(NSMakeRect(item.inset.left, frame.height - .borderSize, frame.width - item.inset.left - item.inset.right, .borderSize)) + } + case let .modern(position, insets): + label.1.draw(NSMakeRect(insets.left, item.labelY, label.0.size.width, label.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backdorColor) + if position.border { + ctx.setFillColor(theme.colors.border.cgColor) + ctx.fill(NSMakeRect(insets.left, self.containerView.frame.height - .borderSize, self.containerView.frame.width - insets.left - insets.right, .borderSize)) + } } } } + override var backdorColor: NSColor { + return theme.colors.background + } + override func updateColors() { + if let item = item as? TextAndLabelItem { + self.labelView.backgroundColor = backdorColor + self.containerView.backgroundColor = backdorColor + self.background = item.viewType.rowBackground + } + } + override func mouseUp(with event: NSEvent) { if mouseInside() { if let item = item as? TextAndLabelItem { @@ -122,23 +218,56 @@ class TextAndLabelRowView: GeneralRowView { required init(frame frameRect: NSRect) { super.init(frame: frameRect) - self.addSubview(labelView) - + containerView.addSubview(labelView) + self.addSubview(self.containerView) + self.containerView.displayDelegate = self + self.containerView.userInteractionEnabled = false + containerView.addSubview(moreView) labelView.set(handler: { [weak self] _ in if let item = self?.item as? TextAndLabelItem { item.action() } }, for: .Click) + + copyView.autohighlight = true + + copyView.set(handler: { [weak self] _ in + if let item = self?.item as? TextAndLabelItem { + item._copyToClipboard?() + } + }, for: .Click) + + containerView.addSubview(copyView) } override func layout() { super.layout() if let item = item as? TextAndLabelItem { - if let _ = item.labelLayout { - labelView.setFrameOrigin(item.inset.left, item.textY) - } else { - labelView.centerY(x:item.inset.left) + switch item.viewType { + case .legacy: + self.containerView.frame = bounds + + if let _ = item.labelLayout { + labelView.setFrameOrigin(item.inset.left, item.textY) + } else { + labelView.centerY(x:item.inset.left) + } + copyView.centerY(x: containerView.frame.width - copyView.frame.width - item.inset.left) + case let .modern(_, innerInsets): + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) + + if let _ = item.labelLayout { + labelView.setFrameOrigin(innerInsets.left, item.textY) + } else { + labelView.centerY(x: innerInsets.left) + } + + copyView.centerY(x: containerView.frame.width - copyView.frame.width - innerInsets.right) + + moreView.setFrameOrigin(NSMakePoint(containerView.frame.width - moreView.frame.width - innerInsets.right, containerView.frame.height - innerInsets.bottom - moreView.frame.height + 2)) } + self.containerView.setCorners(item.viewType.corners) + } } @@ -149,11 +278,19 @@ class TextAndLabelRowView: GeneralRowView { if let item = item as? TextAndLabelItem { // labelView.userInteractionEnabled = item.isTextSelectable - + labelView.userInteractionEnabled = item.canCopy labelView.isSelectable = item.isTextSelectable labelView.update(item.textLayout) - labelView.backgroundColor = theme.colors.background + + moreView.isHidden = item.hasMore != true + moreView.update(item.moreLayout) + + copyView.set(image: theme.icons.fast_copy_link, for: .Normal) + copyView.sizeToFit() + copyView.scaleOnClick = true + copyView.isHidden = item._copyToClipboard == nil } + containerView.needsDisplay = true needsLayout = true } diff --git a/Telegram-Mac/TextUtils.swift b/Telegram-Mac/TextUtils.swift index 7ffc793066..5370acc8a9 100644 --- a/Telegram-Mac/TextUtils.swift +++ b/Telegram-Mac/TextUtils.swift @@ -7,45 +7,74 @@ // import Cocoa -import TelegramCoreMac -import PostboxMac +import TelegramCore + +import Postbox import TGUIKit +import SwiftSignalKit + +enum MessageTextMediaViewType { + case emoji + case text + case none +} -func pullText(from message:Message, attachEmoji: Bool = true) -> NSString { +func pullText(from message:Message, mediaViewType: MessageTextMediaViewType = .emoji, messagesCount: Int = 1) -> NSString { var messageText: NSString = message.text.fixed.nsstring for media in message.media { switch media { case _ as TelegramMediaImage: if message.id.peerId.namespace == Namespaces.Peer.CloudUser, let _ = message.autoremoveAttribute { - messageText = tr(.chatListServiceDestructingPhoto).nsstring + messageText = tr(L10n.chatListServiceDestructingPhoto).nsstring } else { - messageText = tr(.chatListPhoto).nsstring + messageText = L10n.chatListPhoto1Countable(messagesCount).nsstring if !message.text.isEmpty { - messageText = ((attachEmoji ? "🖼 " : "") + message.text.fixed).nsstring + switch mediaViewType { + case .emoji: + messageText = ("🖼 " + message.text.fixed).nsstring + case .text: + messageText = message.text.fixed.nsstring + case .none: + break + } } } - + case let dice as TelegramMediaDice: + messageText = dice.emoji.nsstring case let fileMedia as TelegramMediaFile: - if fileMedia.isSticker { - messageText = tr(.chatListSticker(fileMedia.stickerText?.fixed ?? "")).nsstring + if fileMedia.isStaticSticker || fileMedia.isAnimatedSticker { + messageText = L10n.chatListSticker(fileMedia.stickerText?.fixed ?? "").nsstring } else if fileMedia.isVoice { - messageText = tr(.chatListVoice).nsstring + messageText = L10n.chatListVoice.nsstring + if !message.text.fixed.isEmpty { + messageText = ("🎤" + " " + message.text.fixed).nsstring + } } else if fileMedia.isMusic { - messageText = (fileMedia.musicText.0 + " - " + fileMedia.musicText.1).nsstring + messageText = ("🎵 " + fileMedia.musicText.0 + " - " + fileMedia.musicText.1).nsstring } else if fileMedia.isInstantVideo { - messageText = tr(.chatListInstantVideo).nsstring + messageText = tr(L10n.chatListInstantVideo).nsstring } else if fileMedia.isVideo { if message.id.peerId.namespace == Namespaces.Peer.CloudUser, let _ = message.autoremoveAttribute { - messageText = tr(.chatListServiceDestructingVideo).nsstring + messageText = tr(L10n.chatListServiceDestructingVideo).nsstring } else { if fileMedia.isAnimated { - messageText = tr(.chatListGIF).nsstring + messageText = L10n.chatListGIF.nsstring + if !message.text.fixed.isEmpty { + messageText = (L10n.chatListGIF + ", " + message.text.fixed).nsstring + } } else { - messageText = tr(.chatListVideo).nsstring - if !message.text.isEmpty { - messageText = ("📹 " + message.text.fixed).nsstring + messageText = L10n.chatListVideo1Countable(messagesCount).nsstring + if !message.text.fixed.isEmpty { + switch mediaViewType { + case .emoji: + messageText = ("📹 " + message.text.fixed).nsstring + case .text: + messageText = message.text.fixed.nsstring + case .none: + break + } } } } @@ -54,17 +83,59 @@ func pullText(from message:Message, attachEmoji: Bool = true) -> NSString { } else { messageText = fileMedia.fileName?.fixed.nsstring ?? "File" if !message.text.isEmpty { - messageText = ("📎 " + message.text.fixed).nsstring + switch mediaViewType { + case .emoji: + messageText = ("📎 " + message.text.fixed).nsstring + case .text: + messageText = message.text.fixed.nsstring + case .none: + break + } } } case _ as TelegramMediaMap: - messageText = tr(.chatListMap).nsstring + messageText = tr(L10n.chatListMap).nsstring case _ as TelegramMediaContact: - messageText = tr(.chatListContact).nsstring + messageText = tr(L10n.chatListContact).nsstring case let game as TelegramMediaGame: messageText = "🎮 \(game.title)".nsstring case let invoice as TelegramMediaInvoice: messageText = invoice.title.nsstring + case let poll as TelegramMediaPoll: + messageText = "📊 \(poll.text)".nsstring + case let webpage as TelegramMediaWebpage: + if case let .Loaded(content) = webpage.content { + if let _ = content.image { + switch mediaViewType { + case .emoji: + messageText = ("🖼 " + message.text.fixed).nsstring + case .text: + messageText = message.text.fixed.nsstring + case .none: + break + } + } else if let file = content.file { + if (file.isVideo && !file.isInstantVideo) { + switch mediaViewType { + case .emoji: + messageText = ("🖼 " + message.text.fixed).nsstring + case .text: + messageText = message.text.fixed.nsstring + case .none: + break + } + } else if file.isGraphicFile { + switch mediaViewType { + case .emoji: + messageText = ("📹 " + message.text.fixed).nsstring + case .text: + messageText = message.text.fixed.nsstring + case .none: + break + } + } + } + } default: break } @@ -73,37 +144,41 @@ func pullText(from message:Message, attachEmoji: Bool = true) -> NSString { } -func chatListText(account:Account, for message:Message?, renderedPeer:RenderedPeer? = nil, embeddedState:PeerChatListEmbeddedInterfaceState? = nil) -> NSAttributedString { +func chatListText(account:Account, for message:Message?, messagesCount: Int = 1, renderedPeer:RenderedPeer? = nil, embeddedState:StoredPeerChatInterfaceState? = nil, folder: Bool = false, applyUserName: Bool = false) -> NSAttributedString { + + let interfaceState = embeddedState.flatMap(_internal_decodeStoredChatInterfaceState).flatMap({ + ChatInterfaceState.parse($0, peerId: nil, context: nil) + }) - if let embeddedState = embeddedState as? ChatEmbeddedInterfaceState { + if let embeddedState = interfaceState, !embeddedState.inputState.inputText.isEmpty { let mutableAttributedText = NSMutableAttributedString() - _ = mutableAttributedText.append(string: tr(.chatListDraft), color: theme.colors.redUI, font: .normal(FontSize.text)) - _ = mutableAttributedText.append(string: " \(embeddedState.text)", color: theme.chatList.grayTextColor, font: .normal(FontSize.text)) - mutableAttributedText.setSelected(color: .white, range: mutableAttributedText.range) + _ = mutableAttributedText.append(string: L10n.chatListDraft, color: theme.colors.redUI, font: .normal(.text)) + _ = mutableAttributedText.append(string: " \(embeddedState.inputState.inputText.fullTrimmed.replacingOccurrences(of: "\n", with: " "))", color: theme.chatList.grayTextColor, font: .normal(.text)) + mutableAttributedText.setSelected(color: theme.colors.underSelectedColor, range: mutableAttributedText.range) return mutableAttributedText } - + if let renderedPeer = renderedPeer { if let peer = renderedPeer.peers[renderedPeer.peerId] as? TelegramSecretChat { let subAttr = NSMutableAttributedString() switch peer.embeddedState { case .terminated: - _ = subAttr.append(string: tr(.chatListSecretChatTerminated), color: theme.chatList.grayTextColor, font: .normal(.text)) + _ = subAttr.append(string: L10n.chatListSecretChatTerminated, color: theme.chatList.grayTextColor, font: .normal(.text)) case .handshake: - _ = subAttr.append(string: tr(.chatListSecretChatExKeys), color: theme.chatList.grayTextColor, font: .normal(.text)) + _ = subAttr.append(string: L10n.chatListSecretChatExKeys, color: theme.chatList.grayTextColor, font: .normal(.text)) case .active: if message == nil { - let title:String = renderedPeer.chatMainPeer?.compactDisplayTitle ?? tr(.peerDeletedUser) + let title:String = renderedPeer.chatMainPeer?.displayTitle ?? L10n.peerDeletedUser switch peer.role { case .creator: - _ = subAttr.append(string: tr(.chatListSecretChatJoined(title)), color: theme.chatList.grayTextColor, font: .normal(.text)) + _ = subAttr.append(string: L10n.chatListSecretChatJoined(title), color: theme.chatList.grayTextColor, font: .normal(.text)) case .participant: - _ = subAttr.append(string: tr(.chatListSecretChatCreated(title)), color: theme.chatList.grayTextColor, font: .normal(.text)) + _ = subAttr.append(string: L10n.chatListSecretChatCreated(title), color: theme.chatList.grayTextColor, font: .normal(.text)) } } } - subAttr.setSelected(color: .white, range: subAttr.range) + subAttr.setSelected(color: theme.colors.underSelectedColor, range: subAttr.range) if subAttr.length > 0 { return subAttr } @@ -111,66 +186,103 @@ func chatListText(account:Account, for message:Message?, renderedPeer:RenderedPe } if let message = message { + + + if message.text.isEmpty && message.media.isEmpty { let attr = NSMutableAttributedString() - _ = attr.append(string: tr(.chatListUnsupportedMessage), color: theme.chatList.grayTextColor, font: .normal(.text)) - attr.setSelected(color: .white, range: attr.range) + _ = attr.append(string: L10n.chatListUnsupportedMessage, color: theme.chatList.grayTextColor, font: .normal(.text)) + attr.setSelected(color: theme.colors.underSelectedColor, range: attr.range) return attr } - let peer = messageMainPeer(message) + var peer = messageMainPeer(message) + + - let messageText: NSString = pullText(from: message) + var mediaViewType: MessageTextMediaViewType = .emoji + if !message.containsSecretMedia { + for media in message.media { + if let _ = media as? TelegramMediaImage { + mediaViewType = .text + } else if let file = media as? TelegramMediaFile { + if (file.isVideo && !file.isInstantVideo) || file.isGraphicFile { + mediaViewType = .text + } + } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { + if let _ = content.image { + mediaViewType = .text + } else if let file = content.file { + if (file.isVideo && !file.isInstantVideo) || file.isGraphicFile { + mediaViewType = .text + } + } + } + } + } + + let messageText: NSString = pullText(from: message, mediaViewType: mediaViewType, messagesCount: messagesCount) + let attributedText: NSMutableAttributedString = NSMutableAttributedString() + if messageText.length > 0 { - var attributedText: NSMutableAttributedString - if let author = message.author as? TelegramUser, let peer = peer, peer as? TelegramUser == nil, !peer.isChannel { - let peerText: NSString = (author.id == account.peerId ? "\(tr(.chatListYou))\n" : author.compactDisplayTitle + "\n") as NSString - let mutableAttributedText = NSMutableAttributedString() + + if folder, let peer = peer { + _ = attributedText.append(string: peer.displayTitle + "\n", color: theme.chatList.peerTextColor, font: .normal(.text)) + } + + if let author = message.author as? TelegramUser, let peer = peer, peer as? TelegramUser == nil, !peer.isChannel, applyUserName { + var peerText: String = (author.id == account.peerId ? "\(L10n.chatListYou)" : author.displayTitle) - _ = mutableAttributedText.append(string: peerText as String, color: theme.chatList.peerTextColor, font: .normal(.text)) - _ = mutableAttributedText.append(string: messageText as String, color: theme.chatList.grayTextColor, font: .normal(.text)) - attributedText = mutableAttributedText; + peerText += (folder ? ": " : "\n") + _ = attributedText.append(string: peerText, color: theme.chatList.peerTextColor, font: .normal(.text)) + _ = attributedText.append(string: messageText as String, color: theme.chatList.grayTextColor, font: .normal(.text)) } else { - attributedText = NSAttributedString.initialize(string: messageText as String, color: theme.chatList.grayTextColor, font: NSFont.normal(FontSize.text)).mutableCopy() as! NSMutableAttributedString + _ = attributedText.append(string: messageText as String, color: theme.chatList.grayTextColor, font: .normal(.text)) } - attributedText.setSelected(color: .white,range: attributedText.range) - return attributedText + + attributedText.setSelected(color: theme.colors.underSelectedColor, range: attributedText.range) } else if message.media.first is TelegramMediaAction { - let attributedText: NSMutableAttributedString = NSMutableAttributedString() _ = attributedText.append(string: serviceMessageText(message, account:account), color: theme.chatList.grayTextColor, font: .normal(.text)) - attributedText.setSelected(color: .white,range: attributedText.range) - return attributedText + attributedText.setSelected(color: theme.colors.underSelectedColor, range: attributedText.range) } else if let media = message.media.first as? TelegramMediaExpiredContent { - let attributedText: NSMutableAttributedString = NSMutableAttributedString() let text:String switch media.data { case .image: - text = tr(.serviceMessageExpiredPhoto) + text = L10n.serviceMessageExpiredPhoto case .file: - text = tr(.serviceMessageExpiredFile) + text = L10n.serviceMessageExpiredVideo } _ = attributedText.append(string: text, color: theme.chatList.grayTextColor, font: .normal(.text)) - attributedText.setSelected(color: .white,range: attributedText.range) - return attributedText + attributedText.setSelected(color: theme.colors.underSelectedColor,range: attributedText.range) } + return attributedText + } return NSAttributedString() } -func serviceMessageText(_ message:Message, account:Account) -> String { +func serviceMessageText(_ message:Message, account:Account, isReplied: Bool = false) -> String { var authorName:String = "" - if let displayTitle = message.author?.compactDisplayTitle { + if let displayTitle = message.author?.displayTitle { if message.author?.id == account.peerId { - authorName = tr(.chatServiceYou) + authorName = tr(L10n.chatServiceYou) } else { authorName = displayTitle } } + if let media = message.media.first as? TelegramMediaExpiredContent { + switch media.data { + case .image: + return L10n.chatListPhoto + case .file: + return L10n.chatListVideo + } + } let authorId:PeerId? = message.author?.id @@ -179,77 +291,102 @@ func serviceMessageText(_ message:Message, account:Account) -> String { switch action.action { case let .addedMembers(peerIds: peerIds): if peerIds.first == authorId { - return tr(.chatServiceGroupAddedSelf(authorName)) + return L10n.chatServiceGroupAddedSelf(authorName) } else { - return tr(.chatServiceGroupAddedMembers(authorName, peerDisplayTitles(peerIds, message.peers))) + return L10n.chatServiceGroupAddedMembers1(authorName, peerDebugDisplayTitles(peerIds, message.peers)) } + case .phoneNumberRequest: + return "phone number request" case .channelMigratedFromGroup: - return tr(.chatServiceGroupMigratedToSupergroup) + return "" case let .groupCreated(title: title): if peer.isChannel { - return tr(.chatServiceChannelCreated) + return L10n.chatServiceChannelCreated } else { - return tr(.chatServiceGroupCreated(authorName, title)) + return L10n.chatServiceGroupCreated1(authorName, title) } case .groupMigratedToChannel: - return tr(.chatServiceGroupMigratedToSupergroup) + return "" case .historyCleared: return "" case .historyScreenshot: - return tr(.chatServiceGroupTookScreenshot(authorName)) + return L10n.chatServiceGroupTookScreenshot(authorName) case let .joinedByLink(inviter: peerId): if peerId == authorId { - return tr(.chatServiceGroupJoinedByLink(tr(.chatServiceYou))) + return L10n.chatServiceGroupJoinedByLink(tr(L10n.chatServiceYou)) } else { - return tr(.chatServiceGroupJoinedByLink(authorName)) + return L10n.chatServiceGroupJoinedByLink(authorName) } case let .messageAutoremoveTimeoutUpdated(seconds): if seconds > 0 { - return tr(.chatServiceSecretChatSetTimer(authorName, autoremoveLocalized(Int(seconds)))) + return L10n.chatServiceSecretChatSetTimer1(authorName, autoremoveLocalized(Int(seconds))) } else { - return tr(.chatServiceSecretChatDisabledTimer(authorName)) + return L10n.chatServiceSecretChatDisabledTimer1(authorName) } case let .photoUpdated(image: image): - if let _ = image { - return peer.isChannel ? tr(.chatServiceChannelUpdatedPhoto) : tr(.chatServiceGroupUpdatedPhoto(authorName)) + if let image = image { + let text: String + if image.videoRepresentations.isEmpty { + text = peer.isChannel ? L10n.chatServiceChannelUpdatedPhoto : L10n.chatServiceGroupUpdatedPhoto(authorName) + } else { + text = peer.isChannel ? L10n.chatServiceChannelUpdatedVideo : L10n.chatServiceGroupUpdatedVideo(authorName) + } + return text } else { - return peer.isChannel ? tr(.chatServiceChannelRemovedPhoto) : tr(.chatServiceGroupRemovedPhoto(authorName)) + return peer.isChannel ? L10n.chatServiceChannelRemovedPhoto : L10n.chatServiceGroupRemovedPhoto(authorName) } case .pinnedMessageUpdated: - return tr(.chatServicePinnedMessage) + if !isReplied { + var authorName:String = "" + if let displayTitle = message.author?.displayTitle { + authorName = displayTitle + if account.peerId == message.author?.id { + authorName = tr(L10n.chatServiceYou) + } + } + + var replyMessageText = "" + for attribute in message.attributes { + if let attribute = attribute as? ReplyMessageAttribute, let message = message.associatedMessages[attribute.messageId] { + replyMessageText = pullText(from: message) as String + } + } + return L10n.chatServiceGroupUpdatedPinnedMessage1(authorName, replyMessageText.prefixWithDots(15)) + } else { + return L10n.chatServicePinnedMessage + } + case let .removedMembers(peerIds: peerIds): if peerIds.first == authorId { - return tr(.chatServiceGroupRemovedSelf(authorName)) + return L10n.chatServiceGroupRemovedSelf(authorName) } else { - return tr(.chatServiceGroupRemovedMembers(authorName, peerCompactDisplayTitles(peerIds, message.peers))) + return L10n.chatServiceGroupRemovedMembers1(authorName, peerCompactDisplayTitles(peerIds, message.peers)) } case let .titleUpdated(title: title): - return peer.isChannel ? tr(.chatServiceChannelUpdatedTitle(title)) : tr(.chatServiceGroupUpdatedTitle(authorName, title)) - case let .phoneCall(callId: _, discardReason: reason, duration: duration): + return peer.isChannel ? L10n.chatServiceChannelUpdatedTitle(title) : L10n.chatServiceGroupUpdatedTitle1(authorName, title) + case let .phoneCall(callId: _, discardReason: reason, duration: duration, isVideo): if let duration = duration, duration > 0 { if message.author?.id == account.peerId { - return tr(.chatListServiceCallOutgoing(.stringForShortCallDurationSeconds(for: duration))) + return isVideo ? L10n.chatListServiceVideoCallOutgoing(.stringForShortCallDurationSeconds(for: duration)) : L10n.chatListServiceCallOutgoing(.stringForShortCallDurationSeconds(for: duration)) } else { - return tr(.chatListServiceCallIncoming(.stringForShortCallDurationSeconds(for: duration))) + return isVideo ? L10n.chatListServiceVideoCallIncoming(.stringForShortCallDurationSeconds(for: duration)) : L10n.chatListServiceCallIncoming(.stringForShortCallDurationSeconds(for: duration)) } } if let reason = reason { + let outgoing = !message.flags.contains(.Incoming) + switch reason { case .busy: - return tr(.chatListServiceCallCancelled) + return outgoing ? (isVideo ? L10n.chatListServiceVideoCallCancelled : L10n.chatListServiceCallCancelled) : (isVideo ? L10n.chatListServiceVideoCallMissed : L10n.chatListServiceCallMissed) case .disconnect: - return tr(.chatListServiceCallDisconnected) + return isVideo ? L10n.chatListServiceVideoCallMissed : L10n.chatListServiceCallMissed case .hangup: - if message.author?.id == account.peerId { - return tr(.chatListServiceCallCancelled) - } else { - return tr(.chatListServiceCallMissed) - } + return outgoing ? (isVideo ? L10n.chatListServiceVideoCallCancelled : L10n.chatListServiceCallCancelled) : (isVideo ? L10n.chatListServiceVideoCallMissed : L10n.chatListServiceCallMissed) case .missed: - return tr(.chatListServiceCallMissed) + return outgoing ? (isVideo ? L10n.chatListServiceVideoCallCancelled : L10n.chatListServiceCallCancelled) : (isVideo ? L10n.chatListServiceVideoCallMissed : L10n.chatListServiceCallMissed) } } case let .gameScore(gameId: _, score: score): @@ -261,7 +398,7 @@ func serviceMessageText(_ message:Message, account:Account) -> String { } } } - var text = tr(.chatListServiceGameScored(Int(score), gameName)) + var text = L10n.chatListServiceGameScored1Countable(Int(score), gameName) if let peer = messageMainPeer(message) { if peer.isGroup || peer.isSupergroup { text = (message.author?.compactDisplayTitle ?? "") + " " + text @@ -269,15 +406,100 @@ func serviceMessageText(_ message:Message, account:Account) -> String { } return text case let .paymentSent(currency, totalAmount): - return tr(.chatListServicePaymentSent(TGCurrencyFormatter.shared().formatAmount(totalAmount, currency: currency))) + return L10n.chatListServicePaymentSent(TGCurrencyFormatter.shared().formatAmount(totalAmount, currency: currency)) case .unknown: break - case .customText(let text): + case .customText(let text, _): + return text + case let .botDomainAccessGranted(domain): + return L10n.chatServiceBotPermissionAllowed(domain) + case let .botSentSecureValues(types): + let permissions = types.map({$0.rawValue}).joined(separator: ", ") + return L10n.chatServiceSecureIdAccessGranted(peer.displayTitle, permissions) + case .peerJoined: + return L10n.chatServicePeerJoinedTelegram(authorName) + case let .geoProximityReached(fromId, toId, distance): + let distanceString = stringForDistance(distance: Double(distance)) + if toId == account.peerId { + return L10n.notificationProximityReachedYou1(message.peers[fromId]?.displayTitle ?? "", distanceString) + } else if fromId == account.peerId { + return L10n.notificationProximityYouReached1(message.peers[toId]?.displayTitle ?? "", distanceString) + } else { + return L10n.notificationProximityReached1(message.peers[fromId]?.displayTitle ?? "", distanceString, message.peers[toId]?.displayTitle ?? "") + } + case let .groupPhoneCall(_, _, scheduledDate, duration): + let text: String + if let duration = duration { + if peer.isChannel { + text = L10n.chatServiceVoiceChatFinishedChannel(autoremoveLocalized(Int(duration))) + } else if authorId == account.peerId { + text = L10n.chatServiceVoiceChatFinishedYou(autoremoveLocalized(Int(duration))) + } else { + text = L10n.chatServiceVoiceChatFinished(authorName, autoremoveLocalized(Int(duration))) + } + } else { + if peer.isChannel { + if let scheduledDate = scheduledDate { + text = L10n.chatListServiceVoiceChatScheduledChannel(stringForMediumDate(timestamp: scheduledDate)) + } else { + text = L10n.chatListServiceVoiceChatStartedChannel + } + } else if authorId == account.peerId { + if let scheduledDate = scheduledDate { + text = L10n.chatListServiceVoiceChatScheduledYou(stringForMediumDate(timestamp: scheduledDate)) + } else { + text = L10n.chatListServiceVoiceChatStartedYou + } + } else { + if let scheduledDate = scheduledDate { + text = L10n.chatListServiceVoiceChatScheduled(authorName, stringForMediumDate(timestamp: scheduledDate)) + } else { + text = L10n.chatListServiceVoiceChatStarted(authorName) + } + } + } + return text + case let .inviteToGroupPhoneCall(_, _, peerIds): + let text: String + + var list = "" + for peerId in peerIds { + if let peer = message.peers[peerId] { + list += peer.displayTitle + if peerId != peerIds.last { + list += ", " + } + } + } + + if message.author?.id == account.peerId { + text = L10n.chatListServiceVoiceChatInvitationByYou(list) + } else if peerIds.first == account.peerId { + text = L10n.chatListServiceVoiceChatInvitationForYou(authorName) + } else { + text = L10n.chatListServiceVoiceChatInvitation(authorName, list) + } + return text + case let .setChatTheme(emoji): + let text: String + if message.author?.id == account.peerId { + if emoji.isEmpty { + text = L10n.chatServiceDisabledThemeYou + } else { + text = L10n.chatServiceUpdateThemeYou(emoji) + } + } else { + if emoji.isEmpty { + text = L10n.chatServiceDisabledTheme(authorName) + } else { + text = L10n.chatServiceUpdateTheme(authorName, emoji) + } + } return text } } - return tr(.chatMessageUnsupported) + return tr(L10n.chatMessageUnsupported) } struct PeerStatusStringTheme { @@ -287,7 +509,7 @@ struct PeerStatusStringTheme { let statusColor:NSColor let highlightColor:NSColor let highlightIfActivity:Bool - init(titleFont:NSFont = .normal(.title), titleColor:NSColor = theme.colors.text, statusFont:NSFont = .normal(.short), statusColor:NSColor = theme.colors.grayText, highlightColor:NSColor = theme.colors.blueUI, highlightIfActivity:Bool = true) { + init(titleFont:NSFont = .normal(.title), titleColor:NSColor = theme.colors.text, statusFont:NSFont = .normal(.short), statusColor:NSColor = theme.colors.grayText, highlightColor:NSColor = theme.colors.accent, highlightIfActivity:Bool = true) { self.titleFont = titleFont self.titleColor = titleColor self.statusFont = statusFont @@ -306,6 +528,18 @@ struct PeerStatusStringResult : Equatable { self.status = status self.presence = presence } + + func withUpdatedTitle(_ string: String) -> PeerStatusStringResult { + let title = self.title.mutableCopy() as! NSMutableAttributedString + title.replaceCharacters(in: title.range, with: string) + return PeerStatusStringResult(title, self.status, presence: presence) + } + + func withUpdatedStatus(_ status: String) -> PeerStatusStringResult { + let status = self.status.mutableCopy() as! NSMutableAttributedString + status.replaceCharacters(in: status.range, with: status) + return PeerStatusStringResult(self.title, status, presence: presence) + } } func ==(lhs: PeerStatusStringResult, rhs: PeerStatusStringResult) -> Bool { @@ -323,24 +557,27 @@ func ==(lhs: PeerStatusStringResult, rhs: PeerStatusStringResult) -> Bool { return true } -func stringStatus(for peerView:PeerView, theme:PeerStatusStringTheme = PeerStatusStringTheme()) -> PeerStatusStringResult { +func stringStatus(for peerView:PeerView, context: AccountContext, theme:PeerStatusStringTheme = PeerStatusStringTheme(), onlineMemberCount: Int32? = nil, expanded: Bool = false) -> PeerStatusStringResult { if let peer = peerViewMainPeer(peerView) { - let title:NSAttributedString = .initialize(string: peer.displayTitle, color: theme.titleColor, font: theme.titleFont) if let user = peer as? TelegramUser { if user.phone == "42777" || user.phone == "42470" || user.phone == "4240004" { - return PeerStatusStringResult(title, .initialize(string: tr(.peerServiceNotifications), color: theme.statusColor, font: theme.statusFont)) + return PeerStatusStringResult(title, .initialize(string: L10n.peerServiceNotifications, color: theme.statusColor, font: theme.statusFont)) } - if let _ = user.botInfo { - return PeerStatusStringResult(title, .initialize(string: tr(.presenceBot), color: theme.statusColor, font: theme.statusFont)) + if user.id == repliesPeerId { + return PeerStatusStringResult(title, .initialize(string: L10n.peerRepliesNotifications, color: theme.statusColor, font: theme.statusFont)) + } else if user.flags.contains(.isSupport) { + return PeerStatusStringResult(title, .initialize(string: L10n.presenceSupport, color: theme.statusColor, font: theme.statusFont)) + } else if let _ = user.botInfo { + return PeerStatusStringResult(title, .initialize(string: L10n.presenceBot, color: theme.statusColor, font: theme.statusFont)) } else if let presence = peerView.peerPresences[peer.id] as? TelegramUserPresence { let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - let (string, activity, _) = stringAndActivityForUserPresence(presence, relativeTo: Int32(timestamp)) + let (string, activity, _) = stringAndActivityForUserPresence(presence, timeDifference: context.timeDifference, relativeTo: Int32(timestamp), expanded: expanded) return PeerStatusStringResult(title, .initialize(string: string, color: activity && theme.highlightIfActivity ? theme.highlightColor : theme.statusColor, font: theme.statusFont), presence: presence) } else { - return PeerStatusStringResult(title, .initialize(string: tr(.peerStatusRecently), color: theme.statusColor, font: theme.statusFont)) + return PeerStatusStringResult(title, .initialize(string: L10n.peerStatusRecently, color: theme.statusColor, font: theme.statusFont)) } } else if let group = peer as? TelegramGroup { var onlineCount = 0 @@ -348,7 +585,7 @@ func stringStatus(for peerView:PeerView, theme:PeerStatusStringTheme = PeerStatu let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 for participant in participants.participants { if let presence = peerView.peerPresences[participant.peerId] as? TelegramUserPresence { - let relativeStatus = relativeUserPresenceStatus(presence, relativeTo: Int32(timestamp)) + let relativeStatus = relativeUserPresenceStatus(presence, timeDifference: context.timeDifference, relativeTo: Int32(timestamp)) switch relativeStatus { case .online: onlineCount += 1 @@ -361,50 +598,50 @@ func stringStatus(for peerView:PeerView, theme:PeerStatusStringTheme = PeerStatu if onlineCount > 1 { let string = NSMutableAttributedString() - let _ = string.append(string: "\(tr(.peerStatusMemberCountable(group.participantCount))), ", color: theme.statusColor, font: theme.statusFont) - let _ = string.append(string: tr(.peerStatusMemberOnlineCountable(onlineCount)), color: theme.statusColor, font: theme.statusFont) + let _ = string.append(string: "\(L10n.peerStatusMemberCountable(group.participantCount).replacingOccurrences(of: "\(group.participantCount)", with: group.participantCount.formattedWithSeparator)), ", color: theme.statusColor, font: theme.statusFont) + let _ = string.append(string: L10n.peerStatusMemberOnlineCountable(onlineCount), color: theme.statusColor, font: theme.statusFont) return PeerStatusStringResult(title, string) } else { - let string = NSAttributedString.initialize(string: tr(.peerStatusMemberCountable(group.participantCount)), color: theme.statusColor, font: theme.statusFont) + let string = NSAttributedString.initialize(string: L10n.peerStatusMemberCountable(group.participantCount).replacingOccurrences(of: "\(group.participantCount)", with: group.participantCount.formattedWithSeparator), color: theme.statusColor, font: theme.statusFont) return PeerStatusStringResult(title, string) } } else if let channel = peer as? TelegramChannel { - var onlineCount = 0 + let onlineCount: Int = Int(onlineMemberCount ?? 0) if let cachedChannelData = peerView.cachedData as? CachedChannelData, let memberCount = cachedChannelData.participantsSummary.memberCount { + - if let participants = cachedChannelData.topParticipants { - let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - for participant in participants.participants { - if let presence = peerView.peerPresences[participant.peerId] as? TelegramUserPresence { - let relativeStatus = relativeUserPresenceStatus(presence, relativeTo: Int32(timestamp)) - switch relativeStatus { - case .online: - onlineCount += 1 - default: - break - } - } + let membersLocalized: String + if channel.isChannel { + membersLocalized = L10n.peerStatusSubscribersCountable(Int(memberCount)) + } else { + if memberCount > 0 { + membersLocalized = L10n.peerStatusMemberCountable(Int(memberCount)) + } else { + membersLocalized = L10n.peerStatusGroup } } - if onlineCount > 1, memberCount <= 200, case .group = channel.info { + + let countString = membersLocalized.replacingOccurrences(of: "\(memberCount)", with: memberCount.formattedWithSeparator) + if onlineCount > 1, case .group = channel.info { let string = NSMutableAttributedString() - let _ = string.append(string: "\(tr(.peerStatusMemberCountable(Int(memberCount)))), ", color: theme.statusColor, font: theme.statusFont) - let _ = string.append(string: tr(.peerStatusMemberOnlineCountable(onlineCount)), color: theme.statusColor, font: theme.statusFont) + let _ = string.append(string: "\(countString), ", color: theme.statusColor, font: theme.statusFont) + let _ = string.append(string: L10n.peerStatusMemberOnlineCountable(onlineCount), color: theme.statusColor, font: theme.statusFont) return PeerStatusStringResult(title, string) } else { - let string = NSAttributedString.initialize(string: tr(.peerStatusMemberCountable(Int(memberCount))), color: theme.statusColor, font: theme.statusFont) + + let string = NSAttributedString.initialize(string: countString, color: theme.statusColor, font: theme.statusFont) return PeerStatusStringResult(title, string) } } else { switch channel.info { case .group: - let string = NSAttributedString.initialize(string: tr(.peerStatusGroup), color: theme.statusColor, font: theme.statusFont) + let string = NSAttributedString.initialize(string: L10n.peerStatusGroup, color: theme.statusColor, font: theme.statusFont) return PeerStatusStringResult(title, string) case .broadcast: - let string = NSAttributedString.initialize(string: tr(.peerStatusChannel), color: theme.statusColor, font: theme.statusFont) + let string = NSAttributedString.initialize(string: L10n.peerStatusChannel, color: theme.statusColor, font: theme.statusFont) return PeerStatusStringResult(title, string) } } @@ -414,22 +651,89 @@ func stringStatus(for peerView:PeerView, theme:PeerStatusStringTheme = PeerStatu return PeerStatusStringResult(NSAttributedString(), NSAttributedString()) } - func autoremoveLocalized(_ ttl: Int) -> String { +func autoremoveLocalized(_ ttl: Int, roundToCeil: Bool = false) -> String { var localized: String = "" if ttl <= 59 { - localized = tr(.timerSecondsCountable(ttl)) + localized = L10n.timerSecondsCountable(ttl) } else if ttl <= 3599 { - localized = tr(.timerMinutesCountable(ttl / 60)) + localized = L10n.timerMinutesCountable(ttl / 60) } else if ttl <= 86399 { - localized = tr(.timerHoursCountable(ttl / 60 / 60)) - } else if ttl <= 604799 { - localized = tr(.timerDaysCountable(ttl / 60 / 60 / 24)) + localized = L10n.timerHoursCountable(ttl / 60 / 60) + } else if ttl <= 604800 { + if roundToCeil { + localized = L10n.timerDaysCountable(Int(ceil(Float(ttl) / 60 / 60 / 24))) + } else { + localized = L10n.timerDaysCountable(ttl / 60 / 60 / 24) + } } else { - localized = tr(.timerWeeksCountable(ttl / 60 / 60 / 24 / 7)) + if roundToCeil { + localized = L10n.timerWeeksCountable(Int(ceil(Float(ttl) / 60 / 60 / 24 / 7))) + } else { + let weeks = ttl / 60 / 60 / 24 / 7 + if weeks >= 4 { + localized = L10n.timerMonthsCountable(weeks / 4) + } else { + localized = L10n.timerWeeksCountable(weeks) + } + } } return localized } +public func shortTimeIntervalString(value: Int32) -> String { + if value < 60 { + return L10n.messageTimerShortSeconds("\(max(1, value))") + } else if value < 60 * 60 { + return L10n.messageTimerShortMinutes("\(max(1, value / 60))") + } else if value < 60 * 60 * 24 { + return L10n.messageTimerShortHours("\(max(1, value / (60 * 60)))") + } else if value <= 60 * 60 * 24 * 7 { + return L10n.messageTimerShortDays("\(max(1, value / (60 * 60 * 24)))") + } else { + let weeks = max(1, value / (60 * 60 * 24 * 7)) + if weeks < 4 { + return L10n.messageTimerShortWeeks("\(weeks)") + } else { + return L10n.messageTimerShortMonths("\(weeks / 4)") + } + } +} + + +func slowModeTooltipText(_ timeout: Int32) -> String { + let minutes = timeout / 60 + let seconds = timeout % 60 + return L10n.channelSlowModeToolTip(minutes < 10 ? "0\(minutes)" : "\(minutes)", seconds < 10 ? "0\(seconds)" : "\(seconds)") +} +func showSlowModeTimeoutTooltip(_ slowMode: SlowMode, for view: NSView) { + if let errorText = slowMode.errorText { + if let validUntil = slowMode.validUntil { + tooltip(for: view, text: errorText, updateText: { f in + var timer:SwiftSignalKit.Timer? + timer = SwiftSignalKit.Timer(timeout: 0.1, repeat: true, completion: { + + let timeout = (validUntil - Int32(Date().timeIntervalSince1970)) + + var result: Bool = false + if timeout > 0 { + result = f(slowModeTooltipText(timeout)) + } + if !result { + timer?.invalidate() + timer = nil + } + + }, queue: .mainQueue()) + + timer?.start() + }) + } else { + tooltip(for: view, text: errorText) + } + + } +} + let preCharacter = "`" let codeCharacter = "```" func parseTextEntities(_ message:String) -> (String, [MessageTextEntity]) { @@ -472,5 +776,49 @@ func parseTextEntities(_ message:String) -> (String, [MessageTextEntity]) { } +func timeIntervalString( _ value: Int) -> String { + if value < 60 { + return tr(L10n.timerSecondsCountable(value)) + } else if value < 60 * 60 { + return tr(L10n.timerMinutesCountable(max(1, value / 60))) + } else if value < 60 * 60 * 24 { + return tr(L10n.timerHoursCountable(max(1, value / (60 * 60)))) + } else if value < 60 * 60 * 24 * 7 { + return tr(L10n.timerDaysCountable(max(1, value / (60 * 60 * 24)))) + } else if value < 60 * 60 * 24 * 30 { + return tr(L10n.timerWeeksCountable(max(1, value / (60 * 60 * 24 * 7)))) + } else if value < 60 * 60 * 24 * 360 { + return tr(L10n.timerMonthsCountable(max(1, value / (60 * 60 * 24 * 30)))) + } else { + return tr(L10n.timerYearsCountable(max(1, value / (60 * 60 * 24 * 365)))) + } +} + +func timerText(_ durationValue: Int, addminus: Bool = true) -> String { + + let duration = abs(durationValue) + let days = Int(duration) / (3600 * 24) + let hours = (Int(duration) - (days * 3600 * 24)) / 3600 + let minutes = Int(duration) / 60 % 60 + let seconds = Int(duration) % 60 + + + + var formatted: String + if days >= 1 { + formatted = timeIntervalString(duration) + } else if days != 0 { + formatted = String(format:"%d:%02i:%02i:%02i", days, hours, minutes, seconds) + } else if hours != 0 { + formatted = String(format:"%02i:%02i:%02i", hours, minutes, seconds) + } else { + formatted = String(format:"%02i:%02i", minutes, seconds) + } + if addminus { + return durationValue < 0 ? "-" + formatted : formatted + } else { + return formatted + } +} diff --git a/Telegram-Mac/ThemeGridControllerItem.swift b/Telegram-Mac/ThemeGridControllerItem.swift new file mode 100644 index 0000000000..a128744cd1 --- /dev/null +++ b/Telegram-Mac/ThemeGridControllerItem.swift @@ -0,0 +1,258 @@ +// +// ThemeGridControllerItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 11/01/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import SwiftSignalKit +import Postbox + + +final class SettingsThemeWallpaperView: BackgroundView { + private var wallpaper: Wallpaper? + let imageView = TransformImageView() + private let fetchDisposable = MetaDisposable() + var delete: (() -> Void)? + private let label: TextView = TextView() + init() { + super.init(frame: NSZeroRect) + layer?.borderColor = theme.colors.border.cgColor + layer?.borderWidth = .borderSize + //addSubview(label) + self.addSubview(self.imageView) + label.isEventLess = true + label.userInteractionEnabled = false + label.isSelectable = false + let layout = TextViewLayout(.initialize(string: L10n.chatWallpaperEmpty, color: theme.colors.grayText, font: .normal(.title)), maximumNumberOfLines: 1) + layout.measure(width: .greatestFiniteMagnitude) + label.update(layout) + label.backgroundColor = theme.chatBackground + label.disableBackgroundDrawing = true + } + + deinit { + fetchDisposable.dispose() + } + + override func menu(for event: NSEvent) -> NSMenu? { + let menu = NSMenu(title: "") + if let wallpaper = self.wallpaper { + switch wallpaper { + case .file: + menu.addItem(ContextMenuItem(L10n.messageContextDelete, handler: { [weak self] in + self?.delete?() + })) + default: + break + } + } + + return menu + } + + override func layout() { + super.layout() + label.center() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required override init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + func setWallpaper(account: Account, wallpaper: Wallpaper, size: CGSize) { + self.imageView.frame = CGRect(origin: CGPoint(), size: size) + + + + self.wallpaper = wallpaper + switch wallpaper { + case .builtin: + self.label.isHidden = true + self.imageView.isHidden = false + + let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: -1), representations: [], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: CGSize(), boundingSize: size, intrinsicInsets: NSEdgeInsets()) + self.imageView.setSignal(signal: cachedMedia(media: media, arguments: arguments, scale: backingScaleFactor)) + + + self.imageView.setSignal(settingsBuiltinWallpaperImage(account: account, scale: backingScaleFactor), cacheImage: { [weak media] result in + if let media = media { + cacheMedia(result, media: media, arguments: arguments, scale: System.backingScale) + } + }) + + self.imageView.set(arguments: arguments) + + self.backgroundMode = .gradient(colors: [0xdbddbb, 0x6ba587, 0xd5d88d, 0x88b884].map { .init(argb: $0) }, rotation: nil) + + case let .color(color): + self.imageView.isHidden = true + self.label.isHidden = true + self.backgroundMode = .color(color: NSColor(UInt32(color))) + // backgroundColor = NSColor(UInt32(color)) + case let .gradient(_, colors, rotation): + self.imageView.isHidden = true + self.label.isHidden = true + self.backgroundMode = .gradient(colors: colors.map { NSColor(argb: $0) }, rotation: rotation) + case let .image(representations, _): + self.label.isHidden = true + self.imageView.isHidden = false + self.imageView.setSignal(chatWallpaper(account: account, representations: representations, mode: .thumbnail, isPattern: false, autoFetchFullSize: true, scale: backingScaleFactor)) + self.imageView.set(arguments: TransformImageArguments(corners: ImageCorners(), imageSize: largestImageRepresentation(representations)!.dimensions.size.aspectFilled(size), boundingSize: size, intrinsicInsets: NSEdgeInsets(), emptyColor: nil)) + self.backgroundMode = .plain + fetchDisposable.set(fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: MediaResourceReference.wallpaper(wallpaper: nil, resource: largestImageRepresentation(representations)!.resource)).start()) + case let .file(slug, file, settings, isPattern): + self.label.isHidden = true + self.imageView.isHidden = false + var patternColor: TransformImageEmptyColor? = nil// = NSColor(rgb: 0xd6e2ee, alpha: 0.5) + + var representations:[TelegramMediaImageRepresentation] = [] +// representations.append(contentsOf: file.previewRepresentations) + if let dimensions = file.dimensions { + representations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: file.resource, progressiveSizes: [], immediateThumbnailData: nil)) + } else { + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(NSMakeSize(600, 600)), resource: file.resource, progressiveSizes: [], immediateThumbnailData: nil)) + } + + let sz = largestImageRepresentation(representations)?.dimensions.size ?? size + + if isPattern { + var patternIntensity: CGFloat = 0.5 + if let intensity = settings.intensity { + patternIntensity = CGFloat(intensity) / 100.0 + } + if settings.colors.count == 1, let color = settings.colors.first { + patternColor = .color(NSColor(rgb: color, alpha: patternIntensity)) + } else { + patternColor = .gradient(colors: settings.colors.map { NSColor(rgb: $0) }, intensity: patternIntensity, rotation: settings.rotation) + } + } + + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: sz.aspectFilled(isPattern ? NSMakeSize(300, 300) : size), boundingSize: size, intrinsicInsets: NSEdgeInsets(), emptyColor: patternColor) + + + self.imageView.setSignal(signal: cachedMedia(media: file, arguments: arguments, scale: backingScaleFactor)) + + self.imageView.setSignal(chatWallpaper(account: account, representations: representations, file: file, mode: .thumbnail, isPattern: isPattern, autoFetchFullSize: true, scale: backingScaleFactor), clearInstantly: false, cacheImage: { result in + cacheMedia(result, media: file, arguments: arguments, scale: System.backingScale) + }) + + + self.imageView.set(arguments: arguments) + + + fetchDisposable.set(fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: MediaResourceReference.wallpaper(wallpaper: .slug(slug), resource: largestImageRepresentation(representations)!.resource)).start()) + + self.backgroundMode = .plain + default: + self.backgroundMode = .plain + } + } + +} + +final class ThemeGridControllerItem: GridItem { + let account: Account + let wallpaper: Wallpaper + let telegramWallpaper: TelegramWallpaper? + let interaction: ThemeGridControllerInteraction + + let section: GridSection? = nil + let isSelected: Bool + init(account: Account, wallpaper: Wallpaper, telegramWallpaper: TelegramWallpaper?, interaction: ThemeGridControllerInteraction, isSelected: Bool) { + self.account = account + self.isSelected = isSelected + self.wallpaper = wallpaper + self.telegramWallpaper = telegramWallpaper + self.interaction = interaction + } + + + func node(layout: GridNodeLayout, gridNode: GridNode, cachedNode: GridItemNode?) -> GridItemNode { + let node = ThemeGridControllerItemNode(gridNode) + node.setup(account: self.account, wallpaper: self.wallpaper, telegramWallpaper: self.telegramWallpaper, interaction: self.interaction, isSelected: isSelected) + return node + } + + func update(node: GridItemNode) { + guard let node = node as? ThemeGridControllerItemNode else { + assertionFailure() + return + } + node.setup(account: self.account, wallpaper: self.wallpaper, telegramWallpaper: self.telegramWallpaper, interaction: self.interaction, isSelected: self.isSelected) + } +} + +final class ThemeGridControllerItemNode: GridItemNode { + private let wallpaperView: SettingsThemeWallpaperView + + private var currentState: (Account, Wallpaper, TelegramWallpaper?)? + private var interaction: ThemeGridControllerInteraction? + private let imageView: ImageView = ImageView() + override init(_ grid: GridNode) { + self.wallpaperView = SettingsThemeWallpaperView() + + super.init(grid) + self.addSubview(self.wallpaperView) + addSubview(imageView) + imageView.image = theme.icons.chatGroupToggleSelected + imageView.sizeToFit() + + wallpaperView.delete = { [weak self] in + if let (_, wallpaper, telegramWallapper) = self?.currentState { + if let telegramWallapper = telegramWallapper { + self?.interaction?.deleteWallpaper(wallpaper, telegramWallapper) + } + } + } + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + + func setup(account: Account, wallpaper: Wallpaper, telegramWallpaper: TelegramWallpaper?, interaction: ThemeGridControllerInteraction, isSelected: Bool) { + self.interaction = interaction + + if self.currentState == nil || self.currentState!.0 !== account || wallpaper != self.currentState!.1 { + self.currentState = (account, wallpaper, telegramWallpaper) + self.needsLayout = true + } + imageView.isHidden = !isSelected + } + + override func mouseUp(with event: NSEvent) { + if mouseInside() { + if let (_, wallpaper, telegramWallpaper) = self.currentState { + self.interaction?.openWallpaper(wallpaper, telegramWallpaper) + } + } + } + + override func layout() { + super.layout() + + let bounds = self.bounds + self.wallpaperView.frame = bounds + if let (account, wallpaper, _) = self.currentState { + self.wallpaperView.setWallpaper(account: account, wallpaper: wallpaper, size: bounds.size) + } + imageView.setFrameOrigin(frame.width - imageView.frame.width - 10, 10) + } +} diff --git a/Telegram-Mac/ThemeListRowItem.swift b/Telegram-Mac/ThemeListRowItem.swift new file mode 100644 index 0000000000..e5173acda3 --- /dev/null +++ b/Telegram-Mac/ThemeListRowItem.swift @@ -0,0 +1,357 @@ +// +// ThemeListRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 14/09/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + + +private final class HorizontalThemeFirstItem : GeneralRowItem { + override var width: CGFloat { + return 10 + } + override var height: CGFloat { + return 10 + } + override func viewClass() -> AnyClass { + return HorizontalRowView.self + } +} + +private final class ThemeCachedItem { + let source: InstallThemeSource + init(source: InstallThemeSource) { + self.source = source + } +} + +private let cache:NSCache = NSCache() + +private final class HorizontalThemeItem : GeneralRowItem { + fileprivate let themeType: ThemeSource + fileprivate let titleLayout: TextViewLayout + fileprivate let selected: Bool + fileprivate let theme: TelegramPresentationTheme + fileprivate let context: AccountContext + fileprivate let togglePalette: (InstallThemeSource)->Void + fileprivate let menuItems: (ThemeSource)->[ContextMenuItem] + init(_ initialSize: NSSize, stableId: AnyHashable, context: AccountContext, theme: TelegramPresentationTheme, themeType: ThemeSource, selected: Bool, togglePalette: @escaping(InstallThemeSource)->Void, menuItems: @escaping(ThemeSource)->[ContextMenuItem]) { + self.themeType = themeType + self.selected = selected + self.theme = theme + self.menuItems = menuItems + self.togglePalette = togglePalette + self.context = context + let attr: NSAttributedString + switch themeType { + case let .local(palette, _): + attr = .initialize(string: localizedString("AppearanceSettings.ColorTheme.\(palette.name)"), color: selected ? theme.colors.accent : theme.colors.text, font: selected ? .medium(12) : .normal(12)) + case let .cloud(cloud): + attr = .initialize(string: cloud.title, color: selected ? theme.colors.accent : theme.colors.text, font: selected ? .medium(12) : .normal(12)) + } + self.titleLayout = TextViewLayout(attr, maximumNumberOfLines: 1, truncationType: .end, alignment: .center, alwaysStaticItems: true) + self.titleLayout.measure(width: 80) + super.init(initialSize, height: 100, stableId: stableId) + } + + + override func viewClass() -> AnyClass { + return HorizontalThemeView.self + } + + override var width: CGFloat { + return 100 + } +} + +struct LocalPaletteWithReference { + let palette: ColorPalette + let cloud: TelegramTheme? + init(palette: ColorPalette, cloud: TelegramTheme?) { + self.palette = palette + self.cloud = cloud + } + func withAccentColor(_ color: PaletteAccentColor) -> LocalPaletteWithReference { + return LocalPaletteWithReference(palette: self.palette.withAccentColor(color), cloud: self.cloud) + } +} + +private final class HorizontalThemeView : HorizontalRowView { + private let containerView = View(frame: NSMakeRect(0, 26, 100, 74)) + private let holderView: View = View() + private let selectionView: View = View() + private let imageView = TransformImageView(frame: NSMakeRect(0, 0, 80, 60)) + private let nameView: TextView = TextView() + private let overlay: Control = Control() + private let progressIndicator = ProgressIndicator(frame: NSMakeRect(0, 0, 20, 20)) + private let disposable = MetaDisposable() + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(containerView) + nameView.userInteractionEnabled = false + nameView.isSelectable = false + containerView.addSubview(nameView) + containerView.addSubview(holderView) + containerView.addSubview(imageView) + containerView.addSubview(selectionView) + containerView.addSubview(overlay) + holderView.addSubview(progressIndicator) + selectionView.layer?.cornerRadius = 10 + selectionView.layer?.borderWidth = 2.5 + holderView.layer?.cornerRadius = 10 + } + + override func set(item: TableRowItem, animated: Bool) { + + + super.set(item: item, animated: animated) + + guard let item = item as? HorizontalThemeItem else { + return + } + + overlay.removeAllHandlers() + + var cachedData: InstallThemeSource? = cache.object(forKey: PhotoCacheKeyEntry.theme(item.themeType, item.theme.bubbled, .general).stringValue)?.source + + overlay.set(handler: { [weak item] _ in + if let cachedData = cachedData { + item?.togglePalette(cachedData) + } + }, for: .Click) + + overlay.set(handler: { [weak item] control in + if let item = item, let event = NSApp.currentEvent { + ContextMenu.show(items: item.menuItems(item.themeType), view: control, event: event) + } + }, for: .RightDown) + + progressIndicator.progressColor = item.theme.colors.grayIcon + + let signal = themeAppearanceThumbAndData(context: item.context, bubbled: item.theme.bubbled, source: item.themeType) |> deliverOnMainQueue + + self.imageView.setSignal(signal: cachedThemeThumb(source: item.themeType, bubbled: item.theme.bubbled), clearInstantly: false) + + var animated: Bool = !self.imageView.hasImage + + switch item.themeType { + case .local: + progressIndicator.isHidden = true + animated = false + self.imageView.layer?.contentsGravity = .resize + case let .cloud(cloud): + progressIndicator.isHidden = self.imageView.hasImage + self.imageView.layer?.contentsGravity = cloud.file != nil ? .resize : .center + } + + disposable.set(signal.start(next: { [weak self] image, data in + self?.imageView.setSignal(signal: .single(image), clearInstantly: true, animate: animated) + self?.progressIndicator.isHidden = true + cacheThemeThumb(image, source: item.themeType, bubbled: item.theme.bubbled) + cache.setObject(ThemeCachedItem(source: data), forKey: PhotoCacheKeyEntry.theme(item.themeType, item.theme.bubbled, .general).stringValue) + cachedData = data + })) + + + selectionView.layer?.borderWidth = item.selected ? 2 : 1 + + + nameView.update(item.titleLayout) + needsLayout = true + } + + override var backdorColor: NSColor { + guard let item = item as? HorizontalThemeItem else { + return theme.colors.background + } + return item.theme.colors.background + } + + override func updateColors() { + guard let item = item as? HorizontalThemeItem else { + return + } + backgroundColor = backdorColor + selectionView.layer?.borderColor = item.selected ? item.theme.colors.accentSelect.cgColor : item.theme.colors.border.cgColor + containerView.backgroundColor = backdorColor + switch item.themeType { + case .local: + holderView.backgroundColor = item.theme.colors.grayBackground + case let .cloud(cloud): + holderView.backgroundColor = cloud.file != nil ? item.theme.colors.grayBackground : item.theme.colors.background + } + containerView.backgroundColor = backdorColor + } + + override func layout() { + super.layout() + holderView.frame = NSMakeRect(10, 0, 80, 55) + selectionView.frame = NSMakeRect(10, 0, 80, 55) + imageView.frame = NSMakeRect(11, 1, 78, 53) + overlay.frame = NSMakeRect(10, 0, 80, 55) + nameView.centerX(y: containerView.frame.height - nameView.frame.height) + progressIndicator.center() + } + + + deinit { + disposable.dispose() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + + +class ThemeListRowItem: GeneralRowItem { + fileprivate let context: AccountContext + fileprivate let theme: TelegramPresentationTheme + fileprivate let cloudThemes:[TelegramTheme] + fileprivate let local:[LocalPaletteWithReference] + fileprivate let togglePalette: (InstallThemeSource)->Void + fileprivate let menuItems: (ThemeSource)->[ContextMenuItem] + fileprivate let selected: ThemeSource + init(_ initialSize: NSSize, stableId: AnyHashable, context: AccountContext, theme: TelegramPresentationTheme, selected: ThemeSource, local:[LocalPaletteWithReference], cloudThemes:[TelegramTheme], viewType: GeneralViewType, togglePalette: @escaping(InstallThemeSource)->Void, menuItems: @escaping(ThemeSource)->[ContextMenuItem]) { + self.context = context + self.theme = theme + self.local = local + self.selected = selected + self.cloudThemes = cloudThemes + self.togglePalette = togglePalette + self.menuItems = menuItems + super.init(initialSize, height: 74 + viewType.innerInset.top + viewType.innerInset.bottom, stableId: stableId, viewType: viewType) + } + + override func viewClass() -> AnyClass { + return ThemeListRowView.self + } +} + + +private final class ThemeListRowView : TableRowView { + private var containerView = GeneralRowContainerView(frame: NSZeroRect) + private let borderView: View = View() + private let tableView = HorizontalTableView(frame: NSZeroRect) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + self.containerView.addSubview(self.tableView) + self.containerView.addSubview(self.borderView) + + + self.addSubview(containerView) + } + + + override func updateColors() { + guard let item = item as? ThemeListRowItem else { + return + } + self.containerView.backgroundColor = item.theme.colors.background + self.borderView.backgroundColor = item.theme.colors.border + self.backgroundColor = item.viewType.rowBackground + } + + override var backdorColor: NSColor { + guard let item = item as? ThemeListRowItem else { + return theme.colors.background + } + return item.theme.colors.background + } + + override func layout() { + super.layout() + guard let item = item as? ThemeListRowItem else { + return + } + + let innerInset = item.viewType.innerInset + + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) + self.containerView.setCorners(item.viewType.corners) + self.borderView.frame = NSMakeRect(innerInset.left, self.containerView.frame.height - .borderSize, self.containerView.frame.width - innerInset.left - innerInset.right, .borderSize) + + self.tableView.frame = NSMakeRect(0, innerInset.top, self.containerView.frame.width, self.containerView.frame.height - innerInset.bottom - innerInset.top) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func set(item: TableRowItem, animated: Bool = false) { + + let previous: ThemeListRowItem? = self.item as? ThemeListRowItem + super.set(item: item, animated: animated) + + guard let item = item as? ThemeListRowItem else { + return + } + + self.tableView.getBackgroundColor = { + item.theme.colors.background + } + + borderView.isHidden = !item.viewType.hasBorder + + self.layout() + + if previous?.cloudThemes == item.cloudThemes && previous?.theme == item.theme && item.selected == previous?.selected { + return + } + + let reloadAnimated = animated && previous?.cloudThemes.count != item.cloudThemes.count + + tableView.beginTableUpdates() + tableView.removeAll(animation: reloadAnimated ? .effectFade : .none) + _ = tableView.addItem(item: HorizontalThemeFirstItem(tableView.frame.size), animation: reloadAnimated ? .effectFade : .none) + + let localPalettes:[LocalPaletteWithReference] = item.local + var scrollItem:HorizontalThemeItem? = nil + for palette in localPalettes { + let selected: Bool + switch item.selected { + case let .local(local, _): + selected = local.parent == palette.palette.parent + default: + selected = false + } + let item = HorizontalThemeItem(tableView.frame.size, stableId: palette.palette.name, context: item.context, theme: item.theme, themeType: .local(palette.palette, palette.cloud), selected: selected, togglePalette: item.togglePalette, menuItems: item.menuItems) + _ = tableView.addItem(item: item) + if item.selected && scrollItem == nil { + scrollItem = item + } + } + + for cloud in item.cloudThemes { + let selected: Bool + switch item.selected { + case let .cloud(theme): + selected = theme.id == cloud.id + default: + selected = false + } + let item = HorizontalThemeItem(tableView.frame.size, stableId: cloud.id, context: item.context, theme: item.theme, themeType: .cloud(cloud), selected: selected, togglePalette: item.togglePalette, menuItems: item.menuItems) + _ = tableView.addItem(item: item, animation: reloadAnimated ? .effectFade : .none) + if item.selected && scrollItem == nil { + scrollItem = item + } + } + + _ = tableView.addItem(item: HorizontalThemeFirstItem(tableView.frame.size), animation: reloadAnimated ? .effectFade : .none) + tableView.endTableUpdates() + + if let item = scrollItem { + self.tableView.scroll(to: .center(id: item.stableId, innerId: nil, animated: reloadAnimated, focus: .init(focus: false), inset: 0), true) + } + } +} diff --git a/Telegram-Mac/ThemePreviewModalController.swift b/Telegram-Mac/ThemePreviewModalController.swift new file mode 100644 index 0000000000..55d28ba0aa --- /dev/null +++ b/Telegram-Mac/ThemePreviewModalController.swift @@ -0,0 +1,423 @@ +// +// ThemePreviewModalController.swift +// Telegram +// +// Created by Mikhail Filimonov on 27/08/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + +private final class ThemePreviewView : BackgroundView { + fileprivate let segmentControl = CatalinaStyledSegmentController(frame: NSMakeRect(0, 0, 290, 30)) + private let segmentContainer = View() + private let tableView: TableView = TableView(frame: NSZeroRect, isFlipped: false) + weak var controller: ModalViewController? + private let context: AccountContext + required init(frame frameRect: NSRect, context: AccountContext) { + self.context = context + super.init(frame: frameRect) + self.addSubview(tableView) + segmentContainer.addSubview(segmentControl.view) + self.addSubview(segmentContainer) + + self.useSharedAnimationPhase = false + self.checkDarkPattern = false + + + layout() + + + tableView.addScroll(listener: TableScrollListener(dispatchWhenVisibleRangeUpdated: false, { [weak self] position in + guard let `self` = self else { + return + } + self.tableView.enumerateVisibleViews(with: { view in + if let view = view as? ChatRowView { + view.updateBackground(animated: false, item: view.item) + } + }) + })) + + tableView.afterSetupItem = { view, item in + if let view = view as? ChatRowView { + view.updateBackground(animated: false, item: view.item) + } + } + + + } + + override func layout() { + super.layout() + segmentContainer.frame = NSMakeRect(0, 0, frame.width, 50) + self.segmentControl.view.center() + tableView.frame = NSMakeRect(0, 50, frame.width, frame.height - 50) + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required override init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + fileprivate func addTableItems(_ context: AccountContext, theme: TelegramPresentationTheme) { + self.tableView.getBackgroundColor = { + if theme.bubbled { + return .clear + } else { + return theme.chatBackground + } + } + segmentContainer.backgroundColor = theme.colors.background + segmentContainer.borderColor = theme.colors.border + segmentContainer.border = [.Bottom] + segmentControl.theme = CatalinaSegmentTheme(backgroundColor: theme.colors.listBackground, foregroundColor: theme.colors.background, activeTextColor: theme.colors.text, inactiveTextColor: theme.colors.listGrayText) + + tableView.removeAll() + tableView.updateLocalizationAndTheme(theme: theme) + tableView.backgroundColor = theme.colors.background + _ = tableView.addItem(item: GeneralRowItem(frame.size, height: 10, stableId: 0, backgroundColor: .clear)) + + let chatInteraction = ChatInteraction(chatLocation: .peer(PeerId(0)), context: context, disableSelectAbility: true) + + chatInteraction.getGradientOffsetRect = { [weak self] in + guard let `self` = self else { + return .zero + } + let offset = self.tableView.scrollPosition().current.rect.origin + return CGRect(origin: offset, size: NSMakeSize(350, 400)) + } + let fromUser1 = TelegramUser(id: PeerId(1), accessHash: nil, firstName: L10n.appearanceSettingsChatPreviewUserName1, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + + let fromUser2 = TelegramUser(id: PeerId(2), accessHash: nil, firstName: L10n.appearanceSettingsChatPreviewUserName2, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + + let firstMessage = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: fromUser1.id, namespace: 0, id: 1), globallyUniqueId: 0, groupingKey: 0, groupInfo: nil, threadId: nil, timestamp: 60 * 18 + 60*60*18, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: fromUser1, text: L10n.appearanceSettingsChatPreview1, attributes: [], media: [], peers:SimpleDictionary([fromUser2.id : fromUser2, fromUser1.id : fromUser1]) , associatedMessages: SimpleDictionary(), associatedMessageIds: []) + + let firstEntry: ChatHistoryEntry = .MessageEntry(firstMessage, MessageIndex(firstMessage), true, theme.bubbled ? .bubble : .list, .Full(rank: nil, header: .normal), nil, ChatHistoryEntryData(nil, MessageEntryAdditionalData(), AutoplayMediaPreferences.defaultSettings)) + + let secondMessage = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: fromUser1.id, namespace: 0, id: 0), globallyUniqueId: 0, groupingKey: 0, groupInfo: nil, threadId: nil, timestamp: 60 * 20 + 60*60*18, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: fromUser2, text: tr(L10n.appearanceSettingsChatPreview2), attributes: [ReplyMessageAttribute(messageId: firstMessage.id, threadMessageId: nil)], media: [], peers:SimpleDictionary([fromUser2.id : fromUser2, fromUser1.id : fromUser1]) , associatedMessages: SimpleDictionary([firstMessage.id : firstMessage]), associatedMessageIds: []) + + let secondEntry: ChatHistoryEntry = .MessageEntry(secondMessage, MessageIndex(secondMessage), true, theme.bubbled ? .bubble : .list, .Full(rank: nil, header: .normal), nil, ChatHistoryEntryData(nil, MessageEntryAdditionalData(), AutoplayMediaPreferences.defaultSettings)) + + let thridMessage = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: fromUser1.id, namespace: 0, id: 1), globallyUniqueId: 0, groupingKey: 0, groupInfo: nil, threadId: nil, timestamp: 60 * 22 + 60*60*18, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: fromUser1, text: L10n.appearanceSettingsChatPreview3, attributes: [], media: [], peers:SimpleDictionary([fromUser2.id : fromUser2, fromUser1.id : fromUser1]) , associatedMessages: SimpleDictionary(), associatedMessageIds: []) + + let thridEntry: ChatHistoryEntry = .MessageEntry(thridMessage, MessageIndex(thridMessage), true, theme.bubbled ? .bubble : .list, .Full(rank: nil, header: .normal), nil, ChatHistoryEntryData(nil, MessageEntryAdditionalData(), AutoplayMediaPreferences.defaultSettings)) + + let item1 = ChatRowItem.item(frame.size, from: firstEntry, interaction: chatInteraction, theme: theme) + let item2 = ChatRowItem.item(frame.size, from: secondEntry, interaction: chatInteraction, theme: theme) + let item3 = ChatRowItem.item(frame.size, from: thridEntry, interaction: chatInteraction, theme: theme) + + _ = item2.makeSize(frame.width, oldWidth: 0) + _ = item3.makeSize(frame.width, oldWidth: 0) + _ = item1.makeSize(frame.width, oldWidth: 0) + + tableView.beginTableUpdates() + _ = tableView.addItem(item: item3) + _ = tableView.addItem(item: item2) + _ = tableView.addItem(item: item1) + tableView.endTableUpdates() + + self.tableView.enumerateVisibleViews(with: { view in + if let view = view as? ChatRowView { + view.updateBackground(animated: false, item: view.item) + } + }) + } + + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + } + +} + +enum ThemePreviewSource { + case localTheme(TelegramPresentationTheme, name: String?) + case cloudTheme(TelegramTheme) +} + + + + + +class ThemePreviewModalController: ModalViewController { + + private let context: AccountContext + private let source:ThemePreviewSource + private let disposable = MetaDisposable() + private var currentTheme: TelegramPresentationTheme = theme + private var fetchDisposable = MetaDisposable() + init(context: AccountContext, source: ThemePreviewSource) { + self.context = context + self.source = source + super.init(frame: NSMakeRect(0, 0, 350, 350)) + self.bar = .init(height: 0) + } + + deinit { + disposable.dispose() + fetchDisposable.dispose() + } + + + override func viewDidLoad() { + super.viewDidLoad() + + genericView.controller = self + let context = self.context + + let updateChatMode:(Bool)->Void = { [weak self] bubbled in + guard let `self` = self else { + return + } + let newTheme = self.currentTheme.withUpdatedChatMode(bubbled).withUpdatedBackgroundSize(NSMakeSize(350, 350)) + self.currentTheme = newTheme + self.genericView.addTableItems(self.context, theme: newTheme) + self.genericView.backgroundMode = newTheme.controllerBackgroundMode + } + + self.genericView.segmentControl.add(segment: CatalinaSegmentedItem(title: L10n.appearanceSettingsChatViewBubbles, handler: { + updateChatMode(true) + })) + + self.genericView.segmentControl.add(segment: CatalinaSegmentedItem(title: L10n.appearanceSettingsChatViewClassic, handler: { + updateChatMode(false) + })) + + switch self.source { + case let .localTheme(theme, _): + self.readyOnce() + self.currentTheme = theme.withUpdatedChatMode(true) + genericView.addTableItems(self.context, theme: theme) + modal?.updateLocalizationAndTheme(theme: theme) + genericView.backgroundMode = theme.controllerBackgroundMode + case let .cloudTheme(theme): + if let settings = theme.settings { + let palette = settings.palette + let wallpaper: Wallpaper + let cloud = settings.wallpaper + if let cloud = cloud { + wallpaper = Wallpaper(cloud) + } else { + if settings.baseTheme == .classic { + wallpaper = .builtin + } else { + wallpaper = .none + } + } + self.disposable.set(showModalProgress(signal: moveWallpaperToCache(postbox: context.account.postbox, wallpaper: wallpaper), for: context.window).start(next: { [weak self] wallpaper in + guard let `self` = self else { + return + } + self.readyOnce() + let newTheme = self.currentTheme + .withUpdatedColors(palette) + .withUpdatedWallpaper(ThemeWallpaper(wallpaper: wallpaper, associated: AssociatedWallpaper(cloud: cloud, wallpaper: wallpaper))) + .withUpdatedChatMode(true) + .withUpdatedBackgroundSize(WallpaperDimensions.aspectFilled(NSMakeSize(600, 600))) + self.currentTheme = newTheme + self.genericView.addTableItems(context, theme: newTheme) + self.modal?.updateLocalizationAndTheme(theme: newTheme) + self.genericView.backgroundMode = newTheme.controllerBackgroundMode + + })) + + } else if let file = theme.file { + let signal = loadCloudPaletteAndWallpaper(context: context, file: file) + disposable.set(showModalProgress(signal: signal |> deliverOnMainQueue, for: context.window).start(next: { [weak self] data in + guard let `self` = self else { + return + } + if let (palette, wallpaper, cloud) = data { + self.readyOnce() + let newTheme = self.currentTheme + .withUpdatedColors(palette) + .withUpdatedWallpaper(ThemeWallpaper(wallpaper: wallpaper, associated: AssociatedWallpaper(cloud: cloud, wallpaper: wallpaper))) + .withUpdatedChatMode(true) + self.currentTheme = newTheme + self.genericView.addTableItems(context, theme: newTheme) + self.modal?.updateLocalizationAndTheme(theme: newTheme) + self.genericView.backgroundMode = newTheme.controllerBackgroundMode + } else { + self.close() + alert(for: context.window, info: L10n.unknownError) + } + + })) + fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, reference: MediaResourceReference.media(media: AnyMediaReference.standalone(media: file), resource: file.resource)).start()) + } + } + + } + + override var modalHeader: (left: ModalHeaderData?, center: ModalHeaderData?, right: ModalHeaderData?)? { + switch self.source { + case let .cloudTheme(theme): + + let count:Int32 = theme.installCount ?? 0 + + var countTitle = L10n.themePreviewUsesCountCountable(Int(count)) + countTitle = countTitle.replacingOccurrences(of: "\(count)", with: count.formattedWithSeparator) + + return (left: ModalHeaderData(image: currentTheme.icons.modalClose, handler: { [weak self] in + self?.close() + }), center: ModalHeaderData(title: theme.title, subtitle: count > 0 ? countTitle : nil), right: ModalHeaderData(image: currentTheme.icons.modalShare, handler: { [weak self] in + self?.share() + })) + case let .localTheme(theme, name): + return (left: ModalHeaderData(image: theme.icons.modalClose, handler: { [weak self] in + self?.close() + }), center: ModalHeaderData(title: name ?? localizedString("AppearanceSettings.ColorTheme.\(theme.colors.name)")), right: nil) + } + + } + + private func share() { + switch self.source { + case let .cloudTheme(theme): + showModal(with: ShareModalController(ShareLinkObject(self.context, link: "https://t.me/addtheme/\(theme.slug)")), for: self.context.window) + default: + break + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } + + private func saveAccent() { + + let context = self.context + let currentTheme = self.currentTheme + let colors = currentTheme.colors + + let cloudTheme: TelegramTheme? + switch self.source { + case let .cloudTheme(t): + cloudTheme = t + default: + cloudTheme = nil + } + _ = updateThemeInteractivetly(accountManager: context.sharedContext.accountManager, f: { settings in + var settings = settings + .withUpdatedPalette(colors) + .updateWallpaper { _ in + return currentTheme.wallpaper + } + .withUpdatedCloudTheme(cloudTheme) + .withUpdatedBubbled(currentTheme.bubbled) + + + let defaultTheme: DefaultTheme + + if let cloudTheme = cloudTheme { + defaultTheme = DefaultTheme(local: colors.parent, cloud: DefaultCloudTheme(cloud: cloudTheme, palette: colors, wallpaper: currentTheme.wallpaper.associated ?? AssociatedWallpaper(cloud: currentTheme.wallpaper.associated?.cloud, wallpaper: currentTheme.wallpaper.wallpaper))) + } else { + defaultTheme = DefaultTheme(local: colors.parent, cloud: nil) + } + + if colors.isDark { + settings = settings.withUpdatedDefaultDark(defaultTheme) + } else { + settings = settings.withUpdatedDefaultDay(defaultTheme) + } + settings = settings.withUpdatedDefaultIsDark(colors.isDark).saveDefaultAccent(color: PaletteAccentColor(colors.accent, colors.bubbleBackground_outgoing)).saveDefaultWallpaper().withSavedAssociatedTheme() + return settings + }).start() + + delay(0.1, closure: { [weak self] in + self?.close() + }) + } + + override var modalInteractions: ModalInteractions? { + return ModalInteractions(acceptTitle: L10n.modalSet, accept: { [weak self] in + self?.saveAccent() + }, drawBorder: true, singleButton: true, customTheme: { [weak self] in + return self?.modalTheme ?? .init() + }) + } + + override var dynamicSize: Bool { + return true + } + + override func initializer() -> NSView { + return ThemePreviewView(frame: NSMakeRect(_frameRect.minX, _frameRect.minY, _frameRect.width, _frameRect.height - bar.height), context: self.context) + } + + override func measure(size: NSSize) { + self.modal?.resize(with: NSMakeSize(350, 350), animated: false) + } + + private var genericView:ThemePreviewView { + return self.view as! ThemePreviewView + } + override func viewClass() -> AnyClass { + return ThemePreviewView.self + } + + override var modalTheme: ModalViewController.Theme { + return .init(text: currentTheme.colors.text, grayText: currentTheme.colors.grayText, background: currentTheme.colors.background, border: currentTheme.colors.border, accent: currentTheme.colors.accent, grayForeground: currentTheme.colors.grayForeground) + } +} + + +func paletteFromFile(context: AccountContext, file: TelegramMediaFile) -> ColorPalette? { + let path = context.account.postbox.mediaBox.resourcePath(file.resource) + + return importPalette(path) +} + +func loadCloudPaletteAndWallpaper(context: AccountContext, file: TelegramMediaFile) -> Signal<(ColorPalette, Wallpaper, TelegramWallpaper?)?, NoError> { + return context.account.postbox.mediaBox.resourceData(file.resource) + |> filter { $0.complete } + |> take(1) + |> map { importPalette($0.path) } + |> mapToSignal { palette -> Signal<(ColorPalette, Wallpaper, TelegramWallpaper?)?, NoError> in + if let palette = palette { + switch palette.wallpaper { + case .builtin: + return .single((palette, Wallpaper.builtin, nil)) + case .none: + return .single((palette, Wallpaper.none, nil)) + case let .color(color): + return .single((palette, Wallpaper.color(color.argb), nil)) + case let .url(url): + let link = inApp(for: url as NSString, context: context) + switch link { + case let .wallpaper(values): + switch values.preview { + case let .slug(slug, settings): + return getWallpaper(network: context.account.network, slug: slug) + |> mapToSignal { cloud in + return moveWallpaperToCache(postbox: context.account.postbox, wallpaper: Wallpaper(cloud).withUpdatedSettings(settings)) |> map { wallpaper in + return (palette, wallpaper, cloud) + } |> castError(GetWallpaperError.self) + } + |> `catch` { _ in + return .single((palette, .none, nil)) + } + default: + break + } + default: + break + } + return .single(nil) + } + } else { + return .single(nil) + } + } +} diff --git a/Telegram-Mac/ThemePreviewRowItem.swift b/Telegram-Mac/ThemePreviewRowItem.swift new file mode 100644 index 0000000000..93720085c4 --- /dev/null +++ b/Telegram-Mac/ThemePreviewRowItem.swift @@ -0,0 +1,184 @@ +// +// ThemePreviewRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 14/09/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + + +class ThemePreviewRowItem: GeneralRowItem { + + fileprivate let theme: TelegramPresentationTheme + fileprivate let items:[TableRowItem] + init(_ initialSize: NSSize, stableId: AnyHashable, context: AccountContext, theme: TelegramPresentationTheme, viewType: GeneralViewType) { + self.theme = theme.withUpdatedBackgroundSize(WallpaperDimensions.aspectFilled(NSMakeSize(200, 200))) + + let chatInteraction = ChatInteraction(chatLocation: .peer(PeerId(0)), context: context, disableSelectAbility: true) + + + + let fromUser1 = TelegramUser(id: PeerId(1), accessHash: nil, firstName: L10n.appearanceSettingsChatPreviewUserName1, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + + let fromUser2 = TelegramUser(id: PeerId(2), accessHash: nil, firstName: L10n.appearanceSettingsChatPreviewUserName2, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + + + + let firstMessage = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: fromUser1.id, namespace: 0, id: 1), globallyUniqueId: 0, groupingKey: 0, groupInfo: nil, threadId: nil, timestamp: 60 * 18 + 60*60*18, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: fromUser1, text: L10n.appearanceSettingsChatPreview1, attributes: [], media: [], peers:SimpleDictionary([fromUser2.id : fromUser2, fromUser1.id : fromUser1]) , associatedMessages: SimpleDictionary(), associatedMessageIds: []) + + let firstEntry: ChatHistoryEntry = .MessageEntry(firstMessage, MessageIndex(firstMessage), true, theme.bubbled ? .bubble : .list, .Full(rank: nil, header: .normal), nil, ChatHistoryEntryData(nil, MessageEntryAdditionalData(), AutoplayMediaPreferences.defaultSettings)) + + + let timestamp1: Int32 = 60 * 20 + 60 * 60 * 18 + + let secondMessage = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: fromUser1.id, namespace: 0, id: 0), globallyUniqueId: 0, groupingKey: 0, groupInfo: nil, threadId: nil, timestamp: timestamp1, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: fromUser2, text: tr(L10n.appearanceSettingsChatPreview2), attributes: [ReplyMessageAttribute(messageId: firstMessage.id, threadMessageId: nil)], media: [], peers:SimpleDictionary([fromUser2.id : fromUser2, fromUser1.id : fromUser1]) , associatedMessages: SimpleDictionary([firstMessage.id : firstMessage]), associatedMessageIds: []) + + let secondEntry: ChatHistoryEntry = .MessageEntry(secondMessage, MessageIndex(secondMessage), true, theme.bubbled ? .bubble : .list, .Full(rank: nil, header: .normal), nil, ChatHistoryEntryData(nil, MessageEntryAdditionalData(), AutoplayMediaPreferences.defaultSettings)) + + let timestamp2: Int32 = 60 * 22 + 60 * 60 * 18 + + let thridMessage = Message(stableId: 2, stableVersion: 0, id: MessageId(peerId: fromUser1.id, namespace: 0, id: 1), globallyUniqueId: 0, groupingKey: 0, groupInfo: nil, threadId: nil, timestamp: timestamp2, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: fromUser1, text: L10n.appearanceSettingsChatPreview3, attributes: [], media: [], peers:SimpleDictionary([fromUser2.id : fromUser2, fromUser1.id : fromUser1]) , associatedMessages: SimpleDictionary(), associatedMessageIds: []) + + let thridEntry: ChatHistoryEntry = .MessageEntry(thridMessage, MessageIndex(thridMessage), true, theme.bubbled ? .bubble : .list, .Full(rank: nil, header: .normal), nil, ChatHistoryEntryData(nil, MessageEntryAdditionalData(), AutoplayMediaPreferences.defaultSettings)) + + + let item1 = ChatRowItem.item(initialSize, from: firstEntry, interaction: chatInteraction, theme: theme) + let item2 = ChatRowItem.item(initialSize, from: secondEntry, interaction: chatInteraction, theme: theme) + let item3 = ChatRowItem.item(initialSize, from: thridEntry, interaction: chatInteraction, theme: theme) + + + self.items = [item1, item2, item3] + + super.init(initialSize, stableId: stableId, viewType: viewType) + + chatInteraction.getGradientOffsetRect = { [weak self] in + guard let `self` = self else { + return .zero + } + return CGRect(origin: NSMakePoint(0, self.height), size: NSMakeSize(self.width, self.height)) + } + + _ = makeSize(initialSize.width, oldWidth: 0) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + + let itemWidth = self.blockWidth - self.viewType.innerInset.left - self.viewType.innerInset.right + for item in items { + _ = item.makeSize(itemWidth, oldWidth: 0) + } + return true + } + + override var instantlyResize: Bool { + return true + } + + override var height: CGFloat { + var height: CGFloat = self.viewType.innerInset.top + self.viewType.innerInset.bottom + + for item in self.items { + height += item.height + } + return height + } + + override func viewClass() -> AnyClass { + return ThemePreviewRowView.self + } + +} + +private final class ThemePreviewRowView : TableRowView { + private var containerView = GeneralRowContainerView(frame: NSZeroRect) + private let backgroundView: BackgroundView + private let itemsView = View() + private let borderView: View = View() + required init(frame frameRect: NSRect) { + backgroundView = BackgroundView(frame: NSMakeRect(0, 0, frameRect.width, frameRect.height)) + backgroundView.useSharedAnimationPhase = false + super.init(frame: frameRect) + self.containerView.addSubview(self.backgroundView) + self.containerView.addSubview(self.borderView) + self.addSubview(containerView) + self.backgroundView.addSubview(itemsView) + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? ThemePreviewRowItem else { + return + } + + self.layout() + + self.itemsView.removeAllSubviews() + + + switch item.theme.backgroundMode { + case .background, .tiled: + borderView.isHidden = item.theme.bubbled + case .plain: + borderView.isHidden = false + case .gradient: + borderView.isHidden = item.theme.bubbled + case let .color(color): + borderView.isHidden = color != item.theme.colors.background + } + + var y: CGFloat = item.viewType.innerInset.top + for item in item.items { + let vz = item.viewClass() as! TableRowView.Type + let view = vz.init(frame:NSMakeRect(0, y, self.backgroundView.frame.width, item.height)) + view.set(item: item, animated: false) + self.itemsView.addSubview(view) + + if let view = view as? ChatRowView { + view.updateBackground(animated: false, item: view.item, rotated: true) + } + + y += item.height + } + + + } + + override func updateColors() { + guard let item = item as? ThemePreviewRowItem else { + return + } + self.containerView.backgroundColor = background + self.backgroundView.backgroundMode = item.theme.bubbled ? item.theme.backgroundMode : .color(color: item.theme.colors.chatBackground) + self.borderView.backgroundColor = theme.colors.border + self.backgroundColor = item.viewType.rowBackground + } + + override var backdorColor: NSColor { + return theme.colors.background + } + + override func layout() { + super.layout() + guard let item = item as? ThemePreviewRowItem else { + return + } + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) + self.containerView.setCorners(item.viewType.corners) + self.backgroundView.frame = self.containerView.bounds + self.borderView.frame = NSMakeRect(0, self.containerView.frame.height - .borderSize, self.containerView.frame.width, .borderSize) + itemsView.frame = backgroundView.bounds + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/ThemeSettings.swift b/Telegram-Mac/ThemeSettings.swift index 320244aafb..d4921742f5 100644 --- a/Telegram-Mac/ThemeSettings.swift +++ b/Telegram-Mac/ThemeSettings.swift @@ -6,183 +6,800 @@ // Copyright © 2017 Telegram. All rights reserved. // import Cocoa -import PostboxMac -import SwiftSignalKitMac +import Postbox +import SwiftSignalKit +import TelegramCore + import TGUIKit + public enum PresentationThemeParsingError: Error { case generic } -private func parseColor(_ decoder: PostboxDecoder, _ key: String) -> NSColor { +private func parseColor(_ decoder: PostboxDecoder, _ key: String) -> NSColor? { if let value = decoder.decodeOptionalInt32ForKey(key) { return NSColor(argb: UInt32(bitPattern: value)) + } + return nil +} +private func parseColorArray(_ decoder: PostboxDecoder, _ key: String) -> [NSColor]? { + let value = decoder.decodeInt32ArrayForKey(key) + let list = value.map { + NSColor(argb: UInt32(bitPattern: $0)) + } + if list.isEmpty { + return nil } else { - return NSColor(0x000000) - } -} - -struct ThemePalleteSettings: PreferencesEntry, Equatable { - let background: NSColor - let text: NSColor - let grayText:NSColor - let link:NSColor - let blueUI:NSColor - let redUI:NSColor - let greenUI:NSColor - let blackTransparent:NSColor - let grayTransparent:NSColor - let grayUI:NSColor - let darkGrayText:NSColor - let blueText:NSColor - let blueSelect:NSColor - let selectText:NSColor - let blueFill:NSColor - let border:NSColor - let grayBackground:NSColor - let grayForeground:NSColor - let grayIcon:NSColor - let blueIcon:NSColor - let badgeMuted:NSColor - let badge:NSColor - let indicatorColor: NSColor - let selectMessage: NSColor - let dark: Bool + return list + } + return nil +} + + +extension PaletteWallpaper { + var wallpaper: Wallpaper { + switch self { + case .none: + return .none + case .builtin: + return .builtin + case let .color(color): + return .color(color.argb) + default: + return .none + } + } +} + + +struct AssociatedWallpaper : PostboxCoding, Equatable { + let cloud: TelegramWallpaper? + let wallpaper: Wallpaper + init(decoder: PostboxDecoder) { + self.cloud = decoder.decodeObjectForKey("c", decoder: { TelegramWallpaper(decoder: $0) }) as? TelegramWallpaper + self.wallpaper = decoder.decodeObjectForKey("w", decoder: { Wallpaper(decoder: $0) }) as! Wallpaper + } + + static func ==(lhs: AssociatedWallpaper, rhs: AssociatedWallpaper) -> Bool { + if let lhsCloud = lhs.cloud, let rhsCloud = rhs.cloud { + switch lhsCloud { + case let .file(file): + if case .file(file) = rhsCloud { + return true + } else { + return lhsCloud == rhsCloud && lhs.wallpaper == rhs.wallpaper + } + default: + return lhsCloud == rhsCloud && lhs.wallpaper == rhs.wallpaper + } + } + return lhs.cloud == rhs.cloud && lhs.wallpaper == rhs.wallpaper + } + + init() { + self.cloud = nil + self.wallpaper = .none + } + init(cloud: TelegramWallpaper?, wallpaper: Wallpaper) { + self.cloud = cloud + self.wallpaper = wallpaper + } + + func encode(_ encoder: PostboxEncoder) { + if let cloud = cloud { + encoder.encodeObject(cloud, forKey: "c") + } else { + encoder.encodeNil(forKey: "c") + } + encoder.encodeObject(self.wallpaper, forKey: "w") + } + +} + +struct ThemeWallpaper : PostboxCoding, Equatable { + let wallpaper: Wallpaper + let associated: AssociatedWallpaper? + + init() { + self.wallpaper = .none + self.associated = nil + } + init(wallpaper: Wallpaper, associated: AssociatedWallpaper?) { + self.wallpaper = wallpaper + self.associated = associated + } + + init(decoder: PostboxDecoder) { + self.wallpaper = decoder.decodeObjectForKey("w", decoder: { Wallpaper(decoder: $0) }) as? Wallpaper ?? .none + self.associated = decoder.decodeObjectForKey("aw", decoder: { AssociatedWallpaper(decoder: $0) }) as? AssociatedWallpaper + } + + func encode(_ encoder: PostboxEncoder) { + if let associated = associated { + encoder.encodeObject(associated, forKey: "aw") + } else { + encoder.encodeNil(forKey: "aw") + } + encoder.encodeObject(self.wallpaper, forKey: "w") + } + + func withUpdatedWallpaper(_ wallpaper: Wallpaper) -> ThemeWallpaper { + return ThemeWallpaper(wallpaper: wallpaper, associated: self.associated) + } + func withUpdatedAssociated(_ associated: AssociatedWallpaper?) -> ThemeWallpaper { + return ThemeWallpaper(wallpaper: self.wallpaper, associated: associated) + } + + var paletteWallpaper: PaletteWallpaper { + switch self.wallpaper { + case let .file(slug, _, settings, isPattern): + var options: [String] = [] + if settings.blur { + options.append("mode=blur") + } + if isPattern { + if let pattern = settings.colors.first { + var color = NSColor(argb: pattern).hexString.lowercased() + color = String(color[color.index(after: color.startIndex) ..< color.endIndex]) + options.append("bg_color=\(color)") + } + if let intensity = settings.intensity { + options.append("intensity=\(intensity)") + } + } + var optionsString = "" + if !options.isEmpty { + optionsString = "?\(options.joined(separator: "&"))" + } + return .url("https://t.me/bg/\(slug)\(optionsString)") + case .builtin: + return .builtin + default: + return .none + } + } + +} + +extension PaletteAccentColor { + static func initWith(decoder: PostboxDecoder) -> PaletteAccentColor { + let accent = NSColor(argb: UInt32(bitPattern: decoder.decodeInt32ForKey("c", orElse: 0))) + let messages: [NSColor]? + let colors = decoder.decodeInt32ArrayForKey("bc") + + if colors.isEmpty { + if let rawTop = decoder.decodeOptionalInt32ForKey("bt"), let rawBottom = decoder.decodeOptionalInt32ForKey("bb") { + messages = [NSColor(argb: UInt32(bitPattern: rawTop)), NSColor(argb: UInt32(bitPattern: rawBottom))] + } else { + messages = nil + } + } else { + messages = colors.map { NSColor(argb: UInt32(bitPattern: $0)) } + } + + return PaletteAccentColor(accent, messages) + } + + func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(Int32(bitPattern: self.accent.argb), forKey: "c") + if let messages = self.messages { + encoder.encodeInt32Array(messages.map { Int32(bitPattern: $0.argb) }, forKey: "bc") + } else { + encoder.encodeNil(forKey: "bc") + } + } + +} + +extension ColorPalette { + func encode(_ encoder: PostboxEncoder) { + + encoder.encodeBool(self.isNative, forKey: "isNative") + encoder.encodeString(self.name, forKey: "name") + encoder.encodeString(self.copyright, forKey: "copyright") + encoder.encodeString(self.parent.rawValue, forKey: "parent") + encoder.encodeBool(self.isDark, forKey: "dark") + encoder.encodeBool(self.tinted, forKey: "tinted") + encoder.encodeString(self.wallpaper.toString, forKey: "pw") + encoder.encodeObjectArrayWithEncoder(self.accentList, forKey: "accentList_1", encoder: { value, encoder in + return value.encode(encoder) + }) + + for child in Mirror(reflecting: self).children { + if let label = child.label { + if let value = child.value as? NSColor { + var label = label + _ = label.removeFirst() + encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) + } else if let value = child.value as? [NSColor] { + var label = label + _ = label.removeFirst() + encoder.encodeInt32Array(value.map { Int32(bitPattern: $0.argb) }, forKey: label) + } + } + } + + //encoder.encodeString(self.accentList.map { $0.hexString }.joined(separator: ","), forKey: "accentList") + } + + static func initWith(decoder: PostboxDecoder) -> ColorPalette { + let dark = decoder.decodeBoolForKey("dark", orElse: false) + let tinted = decoder.decodeBoolForKey("tinted", orElse: false) + + let parent: TelegramBuiltinTheme = TelegramBuiltinTheme(rawValue: decoder.decodeStringForKey("parent", orElse: TelegramBuiltinTheme.dayClassic.rawValue)) ?? (dark ? .nightAccent : .dayClassic) + let copyright = decoder.decodeStringForKey("copyright", orElse: "Telegram") + + let isNative = decoder.decodeBoolForKey("isNative", orElse: false) + let name = decoder.decodeStringForKey("name", orElse: "Default") + + let palette: ColorPalette = parent.palette + let pw = PaletteWallpaper(decoder.decodeStringForKey("pw", orElse: "none")) + + + let accentList: [PaletteAccentColor] = (try? decoder.decodeObjectArrayWithCustomDecoderForKey("accentList_1", decoder: { PaletteAccentColor.initWith(decoder: $0) })) ?? [] + + let bubbleBackground_outgoing:[NSColor] + if let colors = parseColorArray(decoder, "bubbleBackground_outgoing") { + bubbleBackground_outgoing = colors + } else { + let colors = [parseColor(decoder, "bubbleBackgroundTop_outgoing"), parseColor(decoder, "bubbleBackgroundBottom_outgoing")].compactMap { $0 } + if colors.isEmpty { + bubbleBackground_outgoing = palette.bubbleBackground_outgoing + } else { + bubbleBackground_outgoing = colors + } + } + + + return ColorPalette(isNative: isNative, + isDark: dark, + tinted: tinted, + name: name, + parent: parent, + wallpaper: pw ?? palette.wallpaper, + copyright: copyright, + accentList: accentList, + basicAccent: parseColor(decoder, "basicAccent") ?? palette.basicAccent, + background: parseColor(decoder, "background") ?? palette.background, + text: parseColor(decoder, "text") ?? palette.text, + grayText: parseColor(decoder, "grayText") ?? palette.grayText, + link: parseColor(decoder, "link") ?? palette.link, + accent: parseColor(decoder, "accent") ?? palette.accent, + redUI: parseColor(decoder, "redUI") ?? palette.redUI, + greenUI: parseColor(decoder, "greenUI") ?? palette.greenUI, + blackTransparent: parseColor(decoder, "blackTransparent") ?? palette.blackTransparent, + grayTransparent: parseColor(decoder, "grayTransparent") ?? palette.grayTransparent, + grayUI: parseColor(decoder, "grayUI") ?? palette.grayUI, + darkGrayText: parseColor(decoder, "darkGrayText") ?? palette.darkGrayText, + accentSelect: parseColor(decoder, "accentSelect") ?? palette.accentSelect, + selectText: parseColor(decoder, "selectText") ?? palette.selectText, + border: parseColor(decoder, "border") ?? palette.border, + grayBackground: parseColor(decoder, "grayBackground") ?? palette.grayBackground, + grayForeground: parseColor(decoder, "grayForeground") ?? palette.grayForeground, + grayIcon: parseColor(decoder, "grayIcon") ?? palette.grayIcon, + accentIcon: parseColor(decoder, "accentIcon") ?? parseColor(decoder, "blueIcon") ?? palette.accentIcon, + badgeMuted: parseColor(decoder, "badgeMuted") ?? palette.badgeMuted, + badge: parseColor(decoder, "badge") ?? palette.badge, + indicatorColor: parseColor(decoder, "indicatorColor") ?? palette.indicatorColor, + selectMessage: parseColor(decoder, "selectMessage") ?? palette.selectMessage, + monospacedPre: parseColor(decoder, "monospacedPre") ?? palette.monospacedPre, + monospacedCode: parseColor(decoder, "monospacedCode") ?? palette.monospacedCode, + monospacedPreBubble_incoming: parseColor(decoder, "monospacedPreBubble_incoming") ?? palette.monospacedPreBubble_incoming, + monospacedPreBubble_outgoing: parseColor(decoder, "monospacedPreBubble_outgoing") ?? palette.monospacedPreBubble_outgoing, + monospacedCodeBubble_incoming: parseColor(decoder, "monospacedCodeBubble_incoming") ?? palette.monospacedCodeBubble_incoming, + monospacedCodeBubble_outgoing: parseColor(decoder, "monospacedCodeBubble_outgoing") ?? palette.monospacedCodeBubble_outgoing, + selectTextBubble_incoming: parseColor(decoder, "selectTextBubble_incoming") ?? palette.selectTextBubble_incoming, + selectTextBubble_outgoing: parseColor(decoder, "selectTextBubble_outgoing") ?? palette.selectTextBubble_outgoing, + bubbleBackground_incoming: parseColor(decoder, "bubbleBackground_incoming") ?? palette.bubbleBackground_incoming, + bubbleBackground_outgoing: bubbleBackground_outgoing, + bubbleBorder_incoming: parseColor(decoder, "bubbleBorder_incoming") ?? palette.bubbleBorder_incoming, + bubbleBorder_outgoing: parseColor(decoder, "bubbleBorder_outgoing") ?? palette.bubbleBorder_outgoing, + grayTextBubble_incoming: parseColor(decoder, "grayTextBubble_incoming") ?? palette.grayTextBubble_incoming, + grayTextBubble_outgoing: parseColor(decoder, "grayTextBubble_outgoing") ?? palette.grayTextBubble_outgoing, + grayIconBubble_incoming: parseColor(decoder, "grayIconBubble_incoming") ?? palette.grayIconBubble_incoming, + grayIconBubble_outgoing: parseColor(decoder, "grayIconBubble_outgoing") ?? palette.grayIconBubble_outgoing, + accentIconBubble_incoming: parseColor(decoder, "accentIconBubble_incoming") ?? parseColor(decoder, "blueIconBubble_incoming") ?? palette.accentIconBubble_incoming, + accentIconBubble_outgoing: parseColor(decoder, "accentIconBubble_outgoing") ?? parseColor(decoder, "blueIconBubble_outgoing") ?? palette.accentIconBubble_outgoing, + linkBubble_incoming: parseColor(decoder, "linkBubble_incoming") ?? palette.linkBubble_incoming, + linkBubble_outgoing: parseColor(decoder, "linkBubble_outgoing") ?? palette.linkBubble_outgoing, + textBubble_incoming: parseColor(decoder, "textBubble_incoming") ?? palette.textBubble_incoming, + textBubble_outgoing: parseColor(decoder, "textBubble_outgoing") ?? palette.textBubble_outgoing, + selectMessageBubble: parseColor(decoder, "selectMessageBubble") ?? palette.selectMessageBubble, + fileActivityBackground: parseColor(decoder, "fileActivityBackground") ?? palette.fileActivityBackground, + fileActivityForeground: parseColor(decoder, "fileActivityForeground") ?? palette.fileActivityForeground, + fileActivityBackgroundBubble_incoming: parseColor(decoder, "fileActivityBackgroundBubble_incoming") ?? palette.fileActivityBackgroundBubble_incoming, + fileActivityBackgroundBubble_outgoing: parseColor(decoder, "fileActivityBackgroundBubble_outgoing") ?? palette.fileActivityBackgroundBubble_outgoing, + fileActivityForegroundBubble_incoming: parseColor(decoder, "fileActivityForegroundBubble_incoming") ?? palette.fileActivityForegroundBubble_incoming, + fileActivityForegroundBubble_outgoing: parseColor(decoder, "fileActivityForegroundBubble_outgoing") ?? palette.fileActivityForegroundBubble_outgoing, + waveformBackground: parseColor(decoder, "waveformBackground") ?? palette.waveformBackground, + waveformForeground: parseColor(decoder, "waveformForeground") ?? palette.waveformForeground, + waveformBackgroundBubble_incoming: parseColor(decoder, "waveformBackgroundBubble_incoming") ?? palette.waveformBackgroundBubble_incoming, + waveformBackgroundBubble_outgoing: parseColor(decoder, "waveformBackgroundBubble_outgoing") ?? palette.waveformBackgroundBubble_outgoing, + waveformForegroundBubble_incoming: parseColor(decoder, "waveformForegroundBubble_incoming") ?? palette.waveformForegroundBubble_incoming, + waveformForegroundBubble_outgoing: parseColor(decoder, "waveformForegroundBubble_outgoing") ?? palette.waveformForegroundBubble_outgoing, + webPreviewActivity: parseColor(decoder, "webPreviewActivity") ?? palette.webPreviewActivity, + webPreviewActivityBubble_incoming: parseColor(decoder, "webPreviewActivityBubble_incoming") ?? palette.webPreviewActivityBubble_incoming, + webPreviewActivityBubble_outgoing: parseColor(decoder, "webPreviewActivityBubble_outgoing") ?? palette.webPreviewActivityBubble_outgoing, + redBubble_incoming: parseColor(decoder, "redBubble_incoming") ?? palette.redBubble_incoming, + redBubble_outgoing: parseColor(decoder, "redBubble_outgoing") ?? palette.redBubble_outgoing, + greenBubble_incoming: parseColor(decoder, "greenBubble_incoming") ?? palette.greenBubble_incoming, + greenBubble_outgoing: parseColor(decoder, "greenBubble_outgoing") ?? palette.greenBubble_outgoing, + chatReplyTitle: parseColor(decoder, "chatReplyTitle") ?? palette.chatReplyTitle, + chatReplyTextEnabled: parseColor(decoder, "chatReplyTextEnabled") ?? palette.chatReplyTextEnabled, + chatReplyTextDisabled: parseColor(decoder, "chatReplyTextDisabled") ?? palette.chatReplyTextDisabled, + chatReplyTitleBubble_incoming: parseColor(decoder, "chatReplyTitleBubble_incoming") ?? palette.chatReplyTitleBubble_incoming, + chatReplyTitleBubble_outgoing: parseColor(decoder, "chatReplyTitleBubble_outgoing") ?? palette.chatReplyTitleBubble_outgoing, + chatReplyTextEnabledBubble_incoming: parseColor(decoder, "chatReplyTextEnabledBubble_incoming") ?? palette.chatReplyTextEnabledBubble_incoming, + chatReplyTextEnabledBubble_outgoing: parseColor(decoder, "chatReplyTextEnabledBubble_outgoing") ?? palette.chatReplyTextEnabledBubble_outgoing, + chatReplyTextDisabledBubble_incoming: parseColor(decoder, "chatReplyTextDisabledBubble_incoming") ?? palette.chatReplyTextDisabledBubble_incoming, + chatReplyTextDisabledBubble_outgoing: parseColor(decoder, "chatReplyTextDisabledBubble_outgoing") ?? palette.chatReplyTextDisabledBubble_outgoing, + groupPeerNameRed: parseColor(decoder, "groupPeerNameRed") ?? palette.groupPeerNameRed, + groupPeerNameOrange: parseColor(decoder, "groupPeerNameOrange") ?? palette.groupPeerNameOrange, + groupPeerNameViolet:parseColor(decoder, "groupPeerNameViolet") ?? palette.groupPeerNameViolet, + groupPeerNameGreen:parseColor(decoder, "groupPeerNameGreen") ?? palette.groupPeerNameGreen, + groupPeerNameCyan: parseColor(decoder, "groupPeerNameCyan") ?? palette.groupPeerNameCyan, + groupPeerNameLightBlue: parseColor(decoder, "groupPeerNameLightBlue") ?? palette.groupPeerNameLightBlue, + groupPeerNameBlue: parseColor(decoder, "groupPeerNameBlue") ?? palette.groupPeerNameBlue, + peerAvatarRedTop: parseColor(decoder, "peerAvatarRedTop") ?? palette.peerAvatarRedTop, + peerAvatarRedBottom: parseColor(decoder, "peerAvatarRedBottom") ?? palette.peerAvatarRedBottom, + peerAvatarOrangeTop: parseColor(decoder, "peerAvatarOrangeTop") ?? palette.peerAvatarOrangeTop, + peerAvatarOrangeBottom: parseColor(decoder, "peerAvatarOrangeBottom") ?? palette.peerAvatarOrangeBottom, + peerAvatarVioletTop: parseColor(decoder, "peerAvatarVioletTop") ?? palette.peerAvatarVioletTop, + peerAvatarVioletBottom: parseColor(decoder, "peerAvatarVioletBottom") ?? palette.peerAvatarVioletBottom, + peerAvatarGreenTop: parseColor(decoder, "peerAvatarGreenTop") ?? palette.peerAvatarGreenTop, + peerAvatarGreenBottom: parseColor(decoder, "peerAvatarGreenBottom") ?? palette.peerAvatarGreenBottom, + peerAvatarCyanTop: parseColor(decoder, "peerAvatarCyanTop") ?? palette.peerAvatarCyanTop, + peerAvatarCyanBottom: parseColor(decoder, "peerAvatarCyanBottom") ?? palette.peerAvatarCyanBottom, + peerAvatarBlueTop: parseColor(decoder, "peerAvatarBlueTop") ?? palette.peerAvatarBlueTop, + peerAvatarBlueBottom: parseColor(decoder, "peerAvatarBlueBottom") ?? palette.peerAvatarBlueBottom, + peerAvatarPinkTop: parseColor(decoder, "peerAvatarPinkTop") ?? palette.peerAvatarPinkTop, + peerAvatarPinkBottom: parseColor(decoder, "peerAvatarPinkBottom") ?? palette.peerAvatarPinkBottom, + bubbleBackgroundHighlight_incoming: parseColor(decoder, "bubbleBackgroundHighlight_incoming") ?? palette.bubbleBackgroundHighlight_incoming, + bubbleBackgroundHighlight_outgoing: parseColor(decoder, "bubbleBackgroundHighlight_outgoing") ?? palette.bubbleBackgroundHighlight_outgoing, + chatDateActive: parseColor(decoder, "chatDateActive") ?? palette.chatDateActive, + chatDateText: parseColor(decoder, "chatDateText") ?? palette.chatDateText, + revealAction_neutral1_background: parseColor(decoder, "revealAction_neutral1_background") ?? palette.revealAction_neutral1_background, + revealAction_neutral1_foreground: parseColor(decoder, "revealAction_neutral1_foreground") ?? palette.revealAction_neutral1_foreground, + revealAction_neutral2_background: parseColor(decoder, "revealAction_neutral2_background") ?? palette.revealAction_neutral2_background, + revealAction_neutral2_foreground: parseColor(decoder, "revealAction_neutral2_foreground") ?? palette.revealAction_neutral2_foreground, + revealAction_destructive_background: parseColor(decoder, "revealAction_destructive_background") ?? palette.revealAction_destructive_background, + revealAction_destructive_foreground: parseColor(decoder, "revealAction_destructive_foreground") ?? palette.revealAction_destructive_foreground, + revealAction_constructive_background: parseColor(decoder, "revealAction_constructive_background") ?? palette.revealAction_constructive_background, + revealAction_constructive_foreground: parseColor(decoder, "revealAction_constructive_foreground") ?? palette.revealAction_constructive_foreground, + revealAction_accent_background: parseColor(decoder, "revealAction_accent_background") ?? palette.revealAction_accent_background, + revealAction_accent_foreground: parseColor(decoder, "revealAction_accent_foreground") ?? palette.revealAction_accent_foreground, + revealAction_warning_background: parseColor(decoder, "revealAction_warning_background") ?? palette.revealAction_warning_background, + revealAction_warning_foreground: parseColor(decoder, "revealAction_warning_foreground") ?? palette.revealAction_warning_foreground, + revealAction_inactive_background: parseColor(decoder, "revealAction_inactive_background") ?? palette.revealAction_inactive_background, + revealAction_inactive_foreground: parseColor(decoder, "revealAction_inactive_foreground") ?? palette.revealAction_inactive_foreground, + chatBackground: parseColor(decoder, "chatBackground") ?? palette.chatBackground, + listBackground: parseColor(decoder, "listBackground") ?? palette.listBackground, + listGrayText: parseColor(decoder, "listGrayText") ?? palette.listGrayText, + grayHighlight: parseColor(decoder, "grayHighlight") ?? palette.grayHighlight, + focusAnimationColor: parseColor(decoder, "focusAnimationColor") ?? palette.focusAnimationColor + ) + } +} + +struct DefaultCloudTheme : Equatable, PostboxCoding { + let cloud: TelegramTheme + let palette: ColorPalette + let wallpaper: AssociatedWallpaper + + init(cloud: TelegramTheme, palette: ColorPalette, wallpaper: AssociatedWallpaper) { + self.cloud = cloud + self.palette = palette + self.wallpaper = wallpaper + } + + init(decoder: PostboxDecoder) { + self.cloud = decoder.decodeObjectForKey("c", decoder: { TelegramTheme(decoder: $0) }) as! TelegramTheme + self.palette = decoder.decodeAnyObjectForKey("p", decoder: { ColorPalette.initWith(decoder: $0) }) as! ColorPalette + self.wallpaper = decoder.decodeObjectForKey("w", decoder: { AssociatedWallpaper(decoder: $0) }) as! AssociatedWallpaper + } + + func encode(_ encoder: PostboxEncoder) { + encoder.encodeObject(self.cloud, forKey: "c") + encoder.encodeObjectWithEncoder(self.palette, encoder: { self.palette.encode($0) }, forKey: "p") + encoder.encodeObject(self.wallpaper, forKey: "w") + } +} + + + +struct DefaultTheme : Equatable, PostboxCoding { + let local: TelegramBuiltinTheme + let cloud: DefaultCloudTheme? + init(local: TelegramBuiltinTheme, cloud: DefaultCloudTheme?) { + self.local = local + self.cloud = cloud + } + func encode(_ encoder: PostboxEncoder) { + encoder.encodeString(self.local.rawValue, forKey: "dl_1") + if let cloud = cloud { + encoder.encodeObject(cloud, forKey: "dc") + } else { + encoder.encodeNil(forKey: "dc") + } + } + init(decoder: PostboxDecoder) { + self.local = TelegramBuiltinTheme(rawValue: decoder.decodeStringForKey("dl_1", orElse: TelegramBuiltinTheme.dayClassic.rawValue)) ?? .dayClassic + self.cloud = decoder.decodeObjectForKey("dc", decoder: { DefaultCloudTheme(decoder: $0) }) as? DefaultCloudTheme + } + func withUpdatedLocal(_ local: TelegramBuiltinTheme) -> DefaultTheme { + return DefaultTheme(local: local, cloud: self.cloud) + } + func updateCloud(_ f: (DefaultCloudTheme?)->DefaultCloudTheme?) -> DefaultTheme { + return DefaultTheme(local: self.local, cloud: f(self.cloud)) + } +} + +struct LocalWallapper : Equatable, PostboxCoding { + let name: TelegramBuiltinTheme + let cloud: TelegramTheme? + let wallpaper: AssociatedWallpaper + let associated: AssociatedWallpaper? + let accentColor: UInt32 + init(name: TelegramBuiltinTheme, accentColor: UInt32, wallpaper: AssociatedWallpaper, associated: AssociatedWallpaper?, cloud: TelegramTheme?) { + self.name = name + self.accentColor = accentColor + self.wallpaper = wallpaper + self.cloud = cloud + self.associated = associated + } + + func isEqual(to other: ColorPalette) -> Bool { + if self.name != other.parent { + return false + } + if self.accentColor != 0 { + return self.accentColor == other.accent.argb + } + return self.cloud == nil + } + + init(decoder: PostboxDecoder) { + self.name = TelegramBuiltinTheme(rawValue: decoder.decodeStringForKey("name", orElse: dayClassicPalette.name)) ?? .dayClassic + self.wallpaper = decoder.decodeObjectForKey("aw", decoder: { AssociatedWallpaper(decoder: $0) }) as! AssociatedWallpaper + self.associated = decoder.decodeObjectForKey("as", decoder: { AssociatedWallpaper(decoder: $0) }) as? AssociatedWallpaper + self.cloud = decoder.decodeObjectForKey("cloud", decoder: { TelegramTheme(decoder: $0) }) as? TelegramTheme + self.accentColor = UInt32(bitPattern: decoder.decodeInt32ForKey("ac", orElse: 0)) + } + + func encode(_ encoder: PostboxEncoder) { + encoder.encodeObject(self.wallpaper, forKey: "aw") + encoder.encodeString(self.name.rawValue, forKey: "name") + if let cloud = cloud { + encoder.encodeObject(cloud, forKey: "cloud") + } else { + encoder.encodeNil(forKey: "cloud") + } + if let associated = self.associated { + encoder.encodeObject(associated, forKey: "as") + } else { + encoder.encodeNil(forKey: "as") + } + encoder.encodeInt32(Int32(bitPattern: self.accentColor), forKey: "ac") + } +} + +struct LocalAccentColor : Equatable, PostboxCoding { + let name: TelegramBuiltinTheme + let color: PaletteAccentColor + let cloud: TelegramTheme? + init(name: TelegramBuiltinTheme, color: PaletteAccentColor, cloud: TelegramTheme?) { + self.name = name + self.color = color + self.cloud = cloud + } + + init(decoder: PostboxDecoder) { + self.name = TelegramBuiltinTheme(rawValue: decoder.decodeStringForKey("name", orElse: dayClassicPalette.name)) ?? .dayClassic + if let hex = decoder.decodeOptionalStringForKey("color"), let color = NSColor(hexString: hex) { + self.color = PaletteAccentColor(color) + } else if let value = decoder.decodeAnyObjectForKey("pac", decoder: { PaletteAccentColor.initWith(decoder: $0) }) as? PaletteAccentColor { + self.color = value + } else { + self.color = PaletteAccentColor(self.name.palette.basicAccent) + } + self.cloud = decoder.decodeObjectForKey("cloud") as? TelegramTheme + } + func encode(_ encoder: PostboxEncoder) { + encoder.encodeString(self.name.rawValue, forKey: "name") + encoder.encodeObjectWithEncoder(self.color, encoder: self.color.encode, forKey: "pac") + if let cloud = self.cloud { + encoder.encodeObject(cloud, forKey: "cloud") + } else { + encoder.encodeNil(forKey: "cloud") + } + } +} + +struct ThemePaletteSettings: PreferencesEntry, Equatable { + let palette: ColorPalette + let bubbled: Bool + let defaultIsDark: Bool let fontSize: CGFloat + let defaultDark: DefaultTheme + let defaultDay: DefaultTheme + let associated:[DefaultTheme] + let wallpapers: [LocalWallapper] + let accents:[LocalAccentColor] + let wallpaper: ThemeWallpaper + let cloudTheme: TelegramTheme? - init(background:NSColor, text: NSColor, grayText: NSColor, link: NSColor, blueUI:NSColor, redUI:NSColor, greenUI:NSColor, blackTransparent:NSColor, grayTransparent:NSColor, grayUI:NSColor, darkGrayText:NSColor, blueText:NSColor, blueSelect:NSColor, selectText:NSColor, blueFill:NSColor, border:NSColor, grayBackground:NSColor, grayForeground:NSColor, grayIcon:NSColor, blueIcon:NSColor, badgeMuted:NSColor, badge:NSColor, indicatorColor: NSColor, selectMessage: NSColor, dark:Bool, fontSize: CGFloat) { - self.background = background - self.text = text - self.grayText = grayText - self.link = link - self.blueUI = blueUI - self.redUI = redUI - self.greenUI = greenUI - self.blackTransparent = blackTransparent - self.grayTransparent = grayTransparent - self.grayUI = grayUI - self.darkGrayText = darkGrayText - self.blueText = blueText - self.blueSelect = blueSelect - self.selectText = selectText - self.blueFill = blueFill - self.border = border - self.grayBackground = grayBackground - self.grayForeground = grayForeground - self.grayIcon = grayIcon - self.blueIcon = blueIcon - self.badgeMuted = badgeMuted - self.badge = badge - self.indicatorColor = indicatorColor - self.selectMessage = selectMessage - self.dark = dark + init(palette: ColorPalette, + bubbled: Bool, + fontSize: CGFloat, + wallpaper: ThemeWallpaper, + defaultDark: DefaultTheme, + defaultDay: DefaultTheme, + defaultIsDark: Bool, + wallpapers: [LocalWallapper], + accents: [LocalAccentColor], + cloudTheme: TelegramTheme?, + associated: [DefaultTheme]) { + + self.palette = palette + self.bubbled = bubbled self.fontSize = fontSize + self.wallpaper = wallpaper + self.defaultDark = defaultDark + self.defaultDay = defaultDay + self.cloudTheme = cloudTheme + self.wallpapers = wallpapers + self.accents = accents + self.defaultIsDark = defaultIsDark + self.associated = associated.filter({$0.cloud?.cloud.settings != nil}) } public func isEqual(to: PreferencesEntry) -> Bool { - if let to = to as? ThemePalleteSettings { + if let to = to as? ThemePaletteSettings { return self == to } else { return false } } init(decoder: PostboxDecoder) { - self.background = parseColor(decoder, "background") - self.text = parseColor(decoder, "text") - self.grayText = parseColor(decoder, "grayText") - self.link = parseColor(decoder, "link") - self.blueUI = parseColor(decoder, "blueUI") - self.redUI = parseColor(decoder, "redUI") - self.greenUI = parseColor(decoder, "greenUI") - self.blackTransparent = parseColor(decoder, "blackTransparent") - self.grayTransparent = parseColor(decoder, "grayTransparent") - self.grayUI = parseColor(decoder, "grayUI") - self.darkGrayText = parseColor(decoder, "darkGrayText") - self.blueText = parseColor(decoder, "blueText") - self.blueSelect = parseColor(decoder, "blueSelect") - self.selectText = parseColor(decoder, "selectText") - self.blueFill = parseColor(decoder, "blueFill") - self.border = parseColor(decoder, "border") - self.grayBackground = parseColor(decoder, "grayBackground") - self.grayForeground = parseColor(decoder, "grayForeground") - self.grayIcon = parseColor(decoder, "grayIcon") - self.blueIcon = parseColor(decoder, "blueIcon") - self.badgeMuted = parseColor(decoder, "badgeMuted") - self.badge = parseColor(decoder, "badge") - self.indicatorColor = parseColor(decoder, "indicatorColor") - self.selectMessage = parseColor(decoder, "selectMessage") - self.dark = decoder.decodeBoolForKey("dark", orElse: false) - self.fontSize = CGFloat(decoder.decodeDoubleForKey("fontSize", orElse: 13.0)) + + self.wallpaper = (decoder.decodeObjectForKey("wallpaper", decoder: { ThemeWallpaper(decoder: $0) }) as? ThemeWallpaper) ?? ThemeWallpaper() + self.palette = ColorPalette.initWith(decoder: decoder) + + self.bubbled = decoder.decodeBoolForKey("bubbled", orElse: false) + self.fontSize = CGFloat(decoder.decodeDoubleForKey("fontSize", orElse: 13)) + + let defDark = DefaultTheme(local: .nightAccent, cloud: nil) + let defDay = DefaultTheme(local: .dayClassic, cloud: nil) + + self.defaultDark = decoder.decodeObjectForKey("defaultDark_1", decoder: { DefaultTheme(decoder: $0) }) as? DefaultTheme ?? defDark + self.defaultDay = decoder.decodeObjectForKey("defaultDay_1", decoder: { DefaultTheme(decoder: $0) }) as? DefaultTheme ?? defDay + + self.cloudTheme = decoder.decodeObjectForKey("cloudTheme", decoder: { TelegramTheme(decoder: $0) }) as? TelegramTheme + + self.wallpapers = (try? decoder.decodeObjectArrayWithCustomDecoderForKey("local_wallpapers", decoder: { LocalWallapper(decoder: $0) })) ?? [] + self.accents = (try? decoder.decodeObjectArrayWithCustomDecoderForKey("local_accents", decoder: { LocalAccentColor(decoder: $0) })) ?? [] + + self.defaultIsDark = decoder.decodeBoolForKey("defaultIsDark", orElse: self.palette.isDark) + + self.associated = (try? decoder.decodeObjectArrayWithCustomDecoderForKey("associated", decoder: { DefaultTheme(decoder: $0) })) ?? [] + } public func encode(_ encoder: PostboxEncoder) { - for child in Mirror(reflecting: self).children { - if let label = child.label { - if let value = child.value as? NSColor { - encoder.encodeInt32(Int32(bitPattern: value.argb), forKey: label) + + self.palette.encode(encoder) + encoder.encodeBool(bubbled, forKey: "bubbled") + encoder.encodeDouble(Double(fontSize), forKey: "fontSize") + encoder.encodeObject(wallpaper, forKey: "wallpaper") + + encoder.encodeObject(defaultDay, forKey: "defaultDay_1") + encoder.encodeObject(defaultDark, forKey: "defaultDark_1") + encoder.encodeObjectArray(self.wallpapers, forKey: "local_wallpapers") + encoder.encodeObjectArray(self.accents, forKey: "local_accents") + encoder.encodeObjectArray(self.associated, forKey: "associated") + + encoder.encodeBool(self.defaultIsDark, forKey: "defaultIsDark") + + + if let cloudTheme = self.cloudTheme { + encoder.encodeObject(cloudTheme, forKey: "cloudTheme") + } else { + encoder.encodeNil(forKey: "cloudTheme") + } + } + + func withUpdatedPalette(_ palette: ColorPalette) -> ThemePaletteSettings { + return ThemePaletteSettings(palette: palette, bubbled: self.bubbled, fontSize: self.fontSize, wallpaper: self.wallpaper, defaultDark: self.defaultDark, defaultDay: self.defaultDay, defaultIsDark: self.defaultIsDark, wallpapers: self.wallpapers, accents: self.accents, cloudTheme: self.cloudTheme, associated: self.associated) + } + func withUpdatedBubbled(_ bubbled: Bool) -> ThemePaletteSettings { + return ThemePaletteSettings(palette: self.palette, bubbled: bubbled, fontSize: self.fontSize, wallpaper: self.wallpaper, defaultDark: self.defaultDark, defaultDay: self.defaultDay, defaultIsDark: self.defaultIsDark, wallpapers: self.wallpapers, accents: self.accents, cloudTheme: self.cloudTheme, associated: self.associated) + } + func withUpdatedFontSize(_ fontSize: CGFloat) -> ThemePaletteSettings { + return ThemePaletteSettings(palette: self.palette, bubbled: self.bubbled, fontSize: fontSize, wallpaper: self.wallpaper, defaultDark: self.defaultDark, defaultDay: self.defaultDay, defaultIsDark: self.defaultIsDark, wallpapers: self.wallpapers, accents: self.accents, cloudTheme: self.cloudTheme, associated: self.associated) + } + + func updateWallpaper(_ f:(ThemeWallpaper)->ThemeWallpaper) -> ThemePaletteSettings { + let updated = f(self.wallpaper) + + return ThemePaletteSettings(palette: self.palette, bubbled: self.bubbled, fontSize: self.fontSize, wallpaper: updated, defaultDark: self.defaultDark, defaultDay: self.defaultDay, defaultIsDark: self.defaultIsDark, wallpapers: self.wallpapers, accents: self.accents, cloudTheme: self.cloudTheme, associated: self.associated) + } + + func saveDefaultWallpaper() -> ThemePaletteSettings { + var wallpapers = self.wallpapers + let local = LocalWallapper(name: self.palette.parent, accentColor: self.palette.accent.argb, wallpaper: AssociatedWallpaper(cloud: self.wallpaper.associated?.cloud, wallpaper: self.wallpaper.wallpaper), associated: self.wallpaper.associated, cloud: self.cloudTheme) + + if let cloud = cloudTheme { + if let index = wallpapers.firstIndex(where: { $0.cloud?.id == cloud.id }) { + wallpapers[index] = local + } else { + wallpapers.append(local) + } + } else { + if let index = wallpapers.firstIndex(where: { $0.isEqual(to: self.palette) }) { + wallpapers[index] = local + } else { + wallpapers.append(local) + } + } + return ThemePaletteSettings(palette: self.palette, bubbled: self.bubbled, fontSize: self.fontSize, wallpaper: self.wallpaper, defaultDark: self.defaultDark, defaultDay: self.defaultDay, defaultIsDark: self.defaultIsDark, wallpapers: wallpapers, accents: self.accents, cloudTheme: self.cloudTheme, associated: self.associated) + } + + func installDefaultWallpaper() -> ThemePaletteSettings { + + let wallpaper:ThemeWallpaper + if let cloud = self.cloudTheme { + let first = self.wallpapers.first(where: { $0.cloud?.id == cloud.id }) + wallpaper = ThemeWallpaper(wallpaper: first?.wallpaper.wallpaper ?? self.palette.wallpaper.wallpaper, associated: first?.associated) + } else { + let first = self.wallpapers.first(where: { $0.isEqual(to: self.palette) }) + wallpaper = ThemeWallpaper(wallpaper: first?.wallpaper.wallpaper ?? self.palette.wallpaper.wallpaper, associated: nil) + } + + return ThemePaletteSettings(palette: self.palette, bubbled: self.bubbled, fontSize: self.fontSize, wallpaper: wallpaper, defaultDark: self.defaultDark, defaultDay: self.defaultDay, defaultIsDark: self.defaultIsDark, wallpapers: self.wallpapers, accents: self.accents, cloudTheme: self.cloudTheme, associated: self.associated) + } + + func saveDefaultAccent(color: PaletteAccentColor) -> ThemePaletteSettings { + var accents = self.accents + let local = LocalAccentColor(name: self.palette.parent, color: color, cloud: self.cloudTheme) + if let index = accents.firstIndex(where: { $0.name == palette.parent && $0.cloud?.id == self.cloudTheme?.id }) { + accents[index] = local + } else { + accents.append(local) + } + return ThemePaletteSettings(palette: self.palette, bubbled: self.bubbled, fontSize: self.fontSize, wallpaper: self.wallpaper, defaultDark: self.defaultDark, defaultDay: self.defaultDay, defaultIsDark: self.defaultIsDark, wallpapers: self.wallpapers, accents: accents, cloudTheme: self.cloudTheme, associated: self.associated) + } + + + func installDefaultAccent() -> ThemePaletteSettings { + let accent: LocalAccentColor? = self.accents.first(where: { $0.name == self.palette.parent && $0.cloud?.id == self.cloudTheme?.id }) + var palette: ColorPalette = self.palette.withoutAccentColor() + if let accent = accent { + palette = palette.withAccentColor(accent.color) + } + return ThemePaletteSettings(palette: palette, bubbled: self.bubbled, fontSize: self.fontSize, wallpaper: self.wallpaper, defaultDark: self.defaultDark, defaultDay: self.defaultDay, defaultIsDark: self.defaultIsDark, wallpapers: self.wallpapers, accents: self.accents, cloudTheme: self.cloudTheme, associated: self.associated) + } + + func withUpdatedDefaultDay(_ defaultDay: DefaultTheme) -> ThemePaletteSettings { + return ThemePaletteSettings(palette: self.palette, bubbled: self.bubbled, fontSize: self.fontSize, wallpaper: wallpaper, defaultDark: self.defaultDark, defaultDay: defaultDay, defaultIsDark: self.defaultIsDark, wallpapers: self.wallpapers, accents: self.accents, cloudTheme: self.cloudTheme, associated: self.associated) + } + func withUpdatedDefaultDark(_ defaultDark: DefaultTheme) -> ThemePaletteSettings { + return ThemePaletteSettings(palette: self.palette, bubbled: self.bubbled, fontSize: self.fontSize, wallpaper: wallpaper, defaultDark: defaultDark, defaultDay: self.defaultDay, defaultIsDark: self.defaultIsDark, wallpapers: self.wallpapers, accents: self.accents, cloudTheme: self.cloudTheme, associated: self.associated) + } + func withUpdatedDefaultIsDark(_ defaultIsDark: Bool) -> ThemePaletteSettings { + return ThemePaletteSettings(palette: self.palette, bubbled: self.bubbled, fontSize: self.fontSize, wallpaper: wallpaper, defaultDark: self.defaultDark, defaultDay: self.defaultDay, defaultIsDark: defaultIsDark, wallpapers: self.wallpapers, accents: self.accents, cloudTheme: self.cloudTheme, associated: self.associated) + } + func withUpdatedCloudTheme(_ cloudTheme: TelegramTheme?) -> ThemePaletteSettings { + return ThemePaletteSettings(palette: self.palette, bubbled: self.bubbled, fontSize: self.fontSize, wallpaper: self.wallpaper, defaultDark: defaultDark, defaultDay: defaultDay, defaultIsDark: self.defaultIsDark, wallpapers: self.wallpapers, accents: self.accents, cloudTheme: cloudTheme, associated: self.associated) + } + + func withSavedAssociatedTheme() -> ThemePaletteSettings { + var associated = self.associated + if let cloudTheme = self.cloudTheme { + if cloudTheme.settings != nil { + let value = DefaultTheme(local: self.palette.parent, cloud: DefaultCloudTheme(cloud: cloudTheme, palette: self.palette, wallpaper: AssociatedWallpaper(cloud: self.wallpaper.associated?.cloud, wallpaper: self.wallpaper.wallpaper))) + if let index = associated.firstIndex(where: { $0.local == self.palette.parent }) { + associated[index] = value + } else { + associated.append(value) } + } + } else { + let value = DefaultTheme(local: self.palette.parent, cloud: nil) + if let index = associated.firstIndex(where: { $0.local == self.palette.parent }) { + associated[index] = value + } else { + associated.append(value) } } - encoder.encodeBool(dark, forKey: "dark") - encoder.encodeDouble(Double(fontSize), forKey: "fontSize") + return ThemePaletteSettings(palette: self.palette, bubbled: self.bubbled, fontSize: self.fontSize, wallpaper: self.wallpaper, defaultDark: self.defaultDark, defaultDay: self.defaultDay, defaultIsDark: self.defaultIsDark, wallpapers: self.wallpapers, accents: self.accents, cloudTheme: self.cloudTheme, associated: associated) } - static var defaultTheme: ThemePalleteSettings { - return ThemePalleteSettings(whitePallete, dark: false, fontSize: 13.0) - } -} - -func ==(lhs: ThemePalleteSettings, rhs: ThemePalleteSettings) -> Bool { - return lhs.background == rhs.background && - lhs.text == rhs.text && - lhs.grayText == rhs.grayText && - lhs.link == rhs.link && - lhs.blueUI == rhs.blueUI && - lhs.redUI == rhs.redUI && - lhs.greenUI == rhs.greenUI && - lhs.blackTransparent == rhs.blackTransparent && - lhs.grayTransparent == rhs.grayTransparent && - lhs.grayUI == rhs.grayUI && - lhs.darkGrayText == rhs.darkGrayText && - lhs.blueText == rhs.blueText && - lhs.blueSelect == rhs.blueSelect && - lhs.selectText == rhs.selectText && - lhs.blueFill == rhs.blueFill && - lhs.border == rhs.border && - lhs.grayBackground == rhs.grayBackground && - lhs.grayForeground == rhs.grayForeground && - lhs.grayIcon == rhs.grayIcon && - lhs.blueIcon == rhs.blueIcon && - lhs.badgeMuted == rhs.badgeMuted && - lhs.badge == rhs.badge && - lhs.indicatorColor == rhs.indicatorColor && - lhs.selectMessage == rhs.selectMessage && - lhs.dark == rhs.dark && - lhs.fontSize == rhs.fontSize -} - -extension ThemePalleteSettings { - init(_ pallete: ColorPallete, dark: Bool, fontSize: CGFloat) { - self.init(background: pallete.background, text: pallete.text, grayText: pallete.grayText, link: pallete.link, blueUI: pallete.blueUI, redUI: pallete.redUI, greenUI: pallete.greenUI, blackTransparent: pallete.blackTransparent, grayTransparent: pallete.grayTransparent, grayUI: pallete.grayUI, darkGrayText: pallete.darkGrayText, blueText: pallete.blueText, blueSelect: pallete.blueSelect, selectText: pallete.selectText, blueFill: pallete.blueFill, border: pallete.border, grayBackground: pallete.grayBackground, grayForeground: pallete.grayForeground, grayIcon: pallete.grayIcon, blueIcon: pallete.blueIcon, badgeMuted: pallete.badgeMuted, badge: pallete.badge, indicatorColor: pallete.indicatorColor, selectMessage: pallete.selectMessage, dark: dark, fontSize: fontSize) - } -} - -func updateThemeSettings(postbox: Postbox, pallete: ColorPallete, dark: Bool) -> Signal { - return postbox.modify { modifier -> Void in - modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.themeSettings, { entry in - let current = entry as? ThemePalleteSettings - return ThemePalleteSettings(pallete, dark: dark, fontSize: current?.fontSize ?? 13.0) - }) + func withUpdatedToDefault(dark: Bool, onlyLocal: Bool = false) -> ThemePaletteSettings { + if dark { + if let cloud = self.defaultDark.cloud, !onlyLocal { + return self.withUpdatedPalette(cloud.palette) + .withUpdatedCloudTheme(cloud.cloud) + .installDefaultWallpaper() + } else { + return self.withUpdatedPalette(self.defaultDark.local.palette) + .withUpdatedCloudTheme(nil) + .installDefaultAccent() + .installDefaultWallpaper() + } + } else { + if let cloud = self.defaultDay.cloud, !onlyLocal { + return self.withUpdatedPalette(cloud.palette) + .withUpdatedCloudTheme(cloud.cloud) + .installDefaultWallpaper() + } else { + return self.withUpdatedPalette(self.defaultDay.local.palette) + .withUpdatedCloudTheme(nil) + .installDefaultAccent() + .installDefaultWallpaper() + } + } + } + + static var defaultTheme: ThemePaletteSettings { + let defDark = DefaultTheme(local: .nightAccent, cloud: nil) + let defDay = DefaultTheme(local: .dayClassic, cloud: nil) + return ThemePaletteSettings(palette: dayClassicPalette, bubbled: false, fontSize: 13, wallpaper: ThemeWallpaper(), defaultDark: defDark, defaultDay: defDay, defaultIsDark: false, wallpapers: [LocalWallapper(name: .dayClassic, accentColor: dayClassicPalette.accent.argb, wallpaper: AssociatedWallpaper(cloud: nil, wallpaper: .builtin), associated: nil, cloud: nil)], accents: [], cloudTheme: nil, associated: []) } } -func updateApplicationFontSize(postbox: Postbox, fontSize: CGFloat) -> Signal { - return postbox.modify { modifier -> Void in - modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.themeSettings, { entry in - let current = entry as? ThemePalleteSettings ?? ThemePalleteSettings.defaultTheme - return ThemePalleteSettings(ColorPallete(current), dark: current.dark, fontSize: fontSize) +func ==(lhs: ThemePaletteSettings, rhs: ThemePaletteSettings) -> Bool { + if lhs.palette != rhs.palette { + return false + } + if lhs.fontSize != rhs.fontSize { + return false + } + if lhs.bubbled != rhs.bubbled { + return false + } + if lhs.wallpaper != rhs.wallpaper { + return false + } + if lhs.defaultDay != rhs.defaultDay { + return false + } + if lhs.defaultDark != rhs.defaultDark { + return false + } + if lhs.cloudTheme != rhs.cloudTheme { + return false + } + if lhs.wallpapers != rhs.wallpapers { + return false + } + if lhs.defaultIsDark != rhs.defaultIsDark { + return false + } + if lhs.associated != rhs.associated { + return false + } + return true +} + + +func themeSettingsView(accountManager: AccountManager)-> Signal { + return accountManager.sharedData(keys: [ApplicationSharedPreferencesKeys.themeSettings]) |> map { $0.entries[ApplicationSharedPreferencesKeys.themeSettings] as? ThemePaletteSettings ?? ThemePaletteSettings.defaultTheme } +} + +func themeUnmodifiedSettings(accountManager: AccountManager)-> Signal { + return accountManager.sharedData(keys: [ApplicationSharedPreferencesKeys.themeSettings]) |> map { $0.entries[ApplicationSharedPreferencesKeys.themeSettings] as? ThemePaletteSettings ?? ThemePaletteSettings.defaultTheme } +} + + +func updateThemeInteractivetly(accountManager: AccountManager, f:@escaping (ThemePaletteSettings)->ThemePaletteSettings)-> Signal { + var bp:Int = 0 + bp += 1 + return accountManager.transaction { transaction -> Void in + transaction.updateSharedData(ApplicationSharedPreferencesKeys.themeSettings, { entry in + return f(entry as? ThemePaletteSettings ?? ThemePaletteSettings.defaultTheme) }) } } diff --git a/Telegram-Mac/ThumbUtils.swift b/Telegram-Mac/ThumbUtils.swift index e428b37bac..a458c7dc48 100644 --- a/Telegram-Mac/ThumbUtils.swift +++ b/Telegram-Mac/ThumbUtils.swift @@ -7,7 +7,7 @@ // import Cocoa -import SwiftSignalKitMac +import SwiftSignalKit import TGUIKit private let extensionImageCache = Atomic<[String: CGImage]>(value: [:]) @@ -36,57 +36,57 @@ func generateExtensionImage(colors: (UInt32, UInt32), ext:String) -> CGImage? { return generateImage(CGSize(width: 42.0, height: 42.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) + context.round(CGRect(origin: CGPoint(), size: size), flags: [.left, .bottom, .right]) + 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 + 1.0, y: -size.height / 2.0 + 1.0) -// -// let radius: CGFloat = 2.0 -// let cornerSize: CGFloat = 10.0 - let size = CGSize(width: 42.0, height: 42.0) - - context.setFillColor(NSColor(colors.0).cgColor) - // context.beginPath() - context.fillEllipse(in: NSMakeRect(0, 0, size.width - 2, size.height - 2)) -// context.move(to: CGPoint(x: 0.0, y: radius)) -// if !radius.isZero { -// context.addArc(tangent1End: CGPoint(x: 0.0, y: 0.0), tangent2End: CGPoint(x: radius, y: 0.0), radius: radius) -// } -// context.addLine(to: CGPoint(x: size.width - cornerSize, y: 0.0)) -// context.addLine(to: CGPoint(x: size.width - cornerSize + cornerSize / 4.0, y: cornerSize - cornerSize / 4.0)) -// context.addLine(to: CGPoint(x: size.width, y: cornerSize)) -// context.addLine(to: CGPoint(x: size.width, y: size.height - radius)) -// if !radius.isZero { -// context.addArc(tangent1End: CGPoint(x: size.width, y: size.height), tangent2End: CGPoint(x: size.width - radius, y: size.height), radius: radius) -// } -// context.addLine(to: CGPoint(x: radius, y: size.height)) -// -// if !radius.isZero { -// context.addArc(tangent1End: CGPoint(x: 0.0, y: size.height), tangent2End: CGPoint(x: 0.0, y: size.height - radius), radius: radius) -// } -// context.closePath() -// context.fillPath() -// -// context.setFillColor(NSColor(colors.1).cgColor) -// context.beginPath() -// context.move(to: CGPoint(x: size.width - cornerSize, y: 0.0)) -// context.addLine(to: CGPoint(x: size.width, y: cornerSize)) -// context.addLine(to: CGPoint(x: size.width - cornerSize + radius, y: cornerSize)) -// -// if !radius.isZero { -// context.addArc(tangent1End: CGPoint(x: size.width - cornerSize, y: cornerSize), tangent2End: CGPoint(x: size.width - cornerSize, y: cornerSize - radius), radius: radius) -// } -// - // context.closePath() - // context.fillPath() - - - - let layout = TextViewLayout(.initialize(string: ext, color: .white, font: .normal(.text)), maximumNumberOfLines: 1, truncationType: .middle) + + let radius: CGFloat = .cornerRadius + let cornerSize: CGFloat = 10.0 + + context.setFillColor(NSColor(rgb: colors.0).cgColor) + context.beginPath() + context.move(to: CGPoint(x: 0.0, y: radius)) + if !radius.isZero { + context.addArc(tangent1End: CGPoint(x: 0.0, y: 0.0), tangent2End: CGPoint(x: radius, y: 0.0), radius: radius) + } + context.addLine(to: CGPoint(x: size.width - cornerSize, y: 0.0)) + context.addLine(to: CGPoint(x: size.width - cornerSize + cornerSize / 4.0, y: cornerSize - cornerSize / 4.0)) + context.addLine(to: CGPoint(x: size.width, y: cornerSize)) + context.addLine(to: CGPoint(x: size.width, y: size.height - radius)) + if !radius.isZero { + context.addArc(tangent1End: CGPoint(x: size.width, y: size.height), tangent2End: CGPoint(x: size.width - radius, y: size.height), radius: radius) + } + context.addLine(to: CGPoint(x: radius, y: size.height)) + + if !radius.isZero { + context.addArc(tangent1End: CGPoint(x: 0.0, y: size.height), tangent2End: CGPoint(x: 0.0, y: size.height - 5), radius: 5) + } + context.closePath() + context.fillPath() + + context.setFillColor(NSColor(rgb: colors.1).cgColor) + context.beginPath() + context.move(to: CGPoint(x: size.width - cornerSize, y: 0.0)) + context.addLine(to: CGPoint(x: size.width, y: cornerSize)) + context.addLine(to: CGPoint(x: size.width - cornerSize + radius, y: cornerSize)) + + if !radius.isZero { + context.addArc(tangent1End: CGPoint(x: size.width - cornerSize, y: cornerSize), tangent2End: CGPoint(x: size.width - cornerSize, y: cornerSize - radius), radius: radius) + } + + context.closePath() + context.fillPath() + + + + let layout = TextViewLayout(.initialize(string: ext, color: .white, font: .medium(.short)), maximumNumberOfLines: 1, truncationType: .middle) layout.measure(width: size.width - 4) if !layout.lines.isEmpty { let line = layout.lines[0] context.textMatrix = CGAffineTransform(scaleX: 1.0, y: -1.0) - context.textPosition = NSMakePoint(floorToScreenPixels((size.width - line.frame.width)/2.0) - 1, floorToScreenPixels((size.height )/2.0) + 4) + context.textPosition = NSMakePoint(floorToScreenPixels(System.backingScale, (size.width - line.frame.width)/2.0) - 1, floorToScreenPixels(System.backingScale, (size.height )/2.0) + 4) CTLineDraw(line.line, context) } @@ -94,20 +94,20 @@ func generateExtensionImage(colors: (UInt32, UInt32), ext:String) -> CGImage? { } -func generateMediaEmptyLinkThumb(color: NSColor, host:String) -> CGImage? { - return generateImage(CGSize(width: 50, height: 50), contextGenerator: { size, context in +func generateMediaEmptyLinkThumb(color: NSColor, textColor: NSColor, host:String) -> CGImage? { + return generateImage(CGSize(width: 40, height: 40), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) let host = host.isEmpty ? "L" : host - context.round(size, 25) + context.round(size, .cornerRadius) context.setFillColor(color.cgColor) context.fill(CGRect(origin: CGPoint(), size: size)) if !host.isEmpty { - let layout = TextViewLayout(.initialize(string: host, color: .white, font: .normal(.custom(16))), maximumNumberOfLines: 1, truncationType: .middle) + let layout = TextViewLayout(.initialize(string: host, color: textColor, font: .bold(.huge)), maximumNumberOfLines: 1, truncationType: .middle) layout.measure(width: size.width - 4) let line = layout.lines[0] context.textMatrix = CGAffineTransform(scaleX: 1.0, y: 1.0) - context.textPosition = NSMakePoint(floorToScreenPixels((size.width - line.frame.width)/2.0) , floorToScreenPixels((size.height - line.frame.width)/2.0)) + context.textPosition = NSMakePoint(floorToScreenPixels(System.backingScale, (size.width - line.frame.width)/2.0) , floorToScreenPixels(System.backingScale, (size.height - line.frame.width)/2.0)) CTLineDraw(line.line, context) } }) @@ -150,8 +150,8 @@ func capIcon(for text:NSAttributedString, size:NSSize = NSMakeSize(50, 50), corn let rect = CTLineGetBoundsWithOptions(line, [.excludeTypographicLeading]) - ctx.textMatrix = CGAffineTransform(scaleX: 1.0, y: -1.0) - ctx.textPosition = NSMakePoint(floorToScreenPixels((size.width - rect.width)/2.0), size.height - floorToScreenPixels((size.height - rect.height)/2.0) - 6 ) + ctx.textMatrix = CGAffineTransform(scaleX: 1.0, y: 1.0) + ctx.textPosition = NSMakePoint(floorToScreenPixels(System.backingScale, (size.width - rect.width)/2.0), floorToScreenPixels(System.backingScale, (size.height - rect.height)/2.0) + 6 ) CTLineDraw(line, ctx) @@ -181,6 +181,13 @@ let playerPauseThumb = generateImage(CGSize(width: 40, height: 40), contextGener }) +let stopFetchStreamableControl = generateImage(CGSize(width: 6, height: 6), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.round(size, 2) + context.setFillColor(NSColor.white.cgColor) + context.fill(NSMakeRect(0, 0, size.width, size.height)) +}) + public struct PreviewOptions: OptionSet { public var rawValue: UInt32 @@ -196,53 +203,48 @@ public struct PreviewOptions: OptionSet { public init(_ flags: PreviewOptions) { var rawValue: UInt32 = 0 - if flags.contains(PreviewOptions.image) { - rawValue |= PreviewOptions.image.rawValue - } - - if flags.contains(PreviewOptions.video) { - rawValue |= PreviewOptions.video.rawValue - } if flags.contains(PreviewOptions.file) { rawValue |= PreviewOptions.file.rawValue } - if flags.contains(PreviewOptions.mixed) { - rawValue |= PreviewOptions.mixed.rawValue + if flags.contains(PreviewOptions.media) { + rawValue |= PreviewOptions.media.rawValue } self.rawValue = rawValue } - public static let image = PreviewOptions(rawValue: 1) - public static let video = PreviewOptions(rawValue: 2) - public static let file = PreviewOptions(rawValue: 4) - public static let mixed = PreviewOptions(rawValue: 8) + public static let media = PreviewOptions(rawValue: 1) + public static let file = PreviewOptions(rawValue: 8) } func takeSenderOptions(for urls:[URL]) -> [PreviewOptions] { var options:[PreviewOptions] = [] for url in urls { - let mime = MIMEType(url.path.nsstring.pathExtension) - let isImage = mime.hasPrefix("image") && !mime.hasSuffix("gif") - let isVideo = mime.hasPrefix("video/mp4") - if isImage && !options.contains(.image) { - options.append(.image) - } - if isVideo && !options.contains(.video) { - options.append(.video) - } + let mime = MIMEType(url.path) - if !isImage && !isVideo { - if !options.contains(.file) { - options.append(.file) + if mime.hasPrefix("image") { + if let image = NSImage(contentsOf: url) { + if image.size.width / 10 > image.size.height || image.size.height < 40 { + continue + } else if image.size.height / 10 > image.size.width || image.size.width < 40 { + continue + } + } else { + continue } } - if options.count > 1 && (options.contains(.video) || options.contains(.image)) { - if !options.contains(.mixed) { - options.append(.mixed) + let media = mime.hasPrefix("image") || mime.hasSuffix("gif") || mime.hasPrefix("video/mp4") || mime.hasPrefix("video/mov") || mime.hasSuffix("m4v") + + if media { + if !options.contains(.media) { + options.append(.media) + } + } else { + if !options.contains(.file) { + options.append(.file) } } } @@ -364,6 +366,55 @@ private func generateRecordingAnimatedImage(_ animationValue:CGFloat, color:UInt } +private func generateChoosingStickerAnimatedImage(_ animationValue:CGFloat, color:NSColor) -> CGImage { + return generateImage(NSMakeSize(24, 20), contextGenerator: { (size, context) in + context.clear(size.bounds) + context.setFillColor(color.cgColor) + context.setStrokeColor(color.cgColor) + var heightProgress: CGFloat = animationValue * 4.0 + if heightProgress > 3.0 { + heightProgress = 4.0 - heightProgress + } else if heightProgress > 2.0 { + heightProgress = heightProgress - 2.0 + heightProgress *= heightProgress + } else if heightProgress > 1.0 { + heightProgress = 2.0 - heightProgress + } else { + heightProgress *= heightProgress + } + + var pupilProgress: CGFloat = animationValue * 4.0 + if pupilProgress > 2.0 { + pupilProgress = 3.0 - pupilProgress + } + pupilProgress = min(1.0, max(0.0, pupilProgress)) + pupilProgress *= pupilProgress + + var positionProgress: CGFloat = animationValue * 2.0 + if positionProgress > 1.0 { + positionProgress = 2.0 - positionProgress + } + + let eyeWidth: CGFloat = 6.0 + let eyeHeight: CGFloat = 11.0 - 2.0 * heightProgress + + let eyeOffset: CGFloat = -1.0 + positionProgress * 2.0 + let leftCenter = CGPoint(x: size.bounds.width / 2.0 - eyeWidth - 1.0 + eyeOffset, y: size.bounds.height / 2.0) + let rightCenter = CGPoint(x: size.bounds.width / 2.0 + 1.0 + eyeOffset, y: size.bounds.height / 2.0) + + let pupilSize: CGFloat = 4.0 + let pupilCenter = CGPoint(x: -1.0 + pupilProgress * 2.0, y: 0.0) + + context.strokeEllipse(in: CGRect(x: leftCenter.x - eyeWidth / 2.0, y: leftCenter.y - eyeHeight / 2.0, width: eyeWidth, height: eyeHeight)) + context.fillEllipse(in: CGRect(x: leftCenter.x - pupilSize / 2.0 + pupilCenter.x * eyeWidth / 4.0, y: leftCenter.y - pupilSize / 2.0, width: pupilSize, height: pupilSize)) + + context.strokeEllipse(in: CGRect(x: rightCenter.x - eyeWidth / 2.0, y: rightCenter.y - eyeHeight / 2.0, width: eyeWidth, height: eyeHeight)) + context.fillEllipse(in: CGRect(x: rightCenter.x - pupilSize / 2.0 + pupilCenter.x * eyeWidth / 4.0, y: rightCenter.y - pupilSize / 2.0, width: pupilSize, height: pupilSize)) + + })! + +} + private func generateUploadFileAnimatedImage(_ animationValue:CGFloat, backgroundColor:UInt32, foregroundColor: UInt32) -> CGImage { return generateImage(NSMakeSize(26, 20), contextGenerator: { (size, context) in context.clear(NSMakeRect(0,0,size.width, size.height)) @@ -375,15 +426,27 @@ private func generateUploadFileAnimatedImage(_ animationValue:CGFloat, backgroun // let round: CGFloat = 1.25 var dotsColor = NSColor(backgroundColor) context.setFillColor(dotsColor.cgColor) - context.fill(CGRect(x: leftPadding, y: topPadding, width: progressWidth, height: progressHeight)) + context.addPath(CGPath(roundedRect: CGRect(x: leftPadding, y: topPadding, width: progressWidth, height: progressHeight), cornerWidth: 2, cornerHeight: 2, transform: nil)) + context.closePath() + context.fillPath() + // context.fill(CGRect(x: leftPadding, y: topPadding, width: progressWidth, height: progressHeight)) dotsColor = NSColor(foregroundColor, 0.3) context.setFillColor(dotsColor.cgColor) - context.fill(CGRect(x: leftPadding, y: topPadding, width: progressWidth, height: progressHeight)) + + context.addPath(CGPath(roundedRect: CGRect(x: leftPadding, y: topPadding, width: progressWidth, height: progressHeight), cornerWidth: 2, cornerHeight: 2, transform: nil)) + context.closePath() + context.fillPath() + +// context.fill(CGRect(x: leftPadding, y: topPadding, width: progressWidth, height: progressHeight)) progress = interpolate(from: 0.0, to: progressWidth * 2.0, value: animationValue) dotsColor = NSColor(foregroundColor, 1.0) context.setFillColor(dotsColor.cgColor) context.setBlendMode(.sourceIn) - context.fill(CGRect(x: CGFloat(leftPadding - progressWidth + progress), y: topPadding, width: progressWidth, height: progressHeight)) + // context.fill(CGRect(x: CGFloat(leftPadding - progressWidth + progress), y: topPadding, width: progressWidth, height: progressHeight)) + context.addPath(CGPath(roundedRect: CGRect(x: CGFloat(leftPadding - progressWidth + progress), y: topPadding, width: progressWidth, height: progressHeight), cornerWidth: 2, cornerHeight: 2, transform: nil)) + context.closePath() + context.fillPath() + })! @@ -442,6 +505,14 @@ func recordVoiceActivityAnimation(_ color: NSColor) -> [CGImage] { return steps } +func choosingStickerActivityAnimation(_ color: NSColor) -> [CGImage] { + var steps:[CGImage] = [] + for i in 0 ..< 120 { + steps.append(generateChoosingStickerAnimatedImage( CGFloat(i) / 120, color: color)) + } + return steps +} + func uploadFileActivityAnimation(_ foregroundColor: NSColor, _ backgroundColor: NSColor) -> [CGImage] { var steps:[CGImage] = [] diff --git a/Telegram-Mac/TouchBarEmojiItemView.swift b/Telegram-Mac/TouchBarEmojiItemView.swift new file mode 100644 index 0000000000..e02090f831 --- /dev/null +++ b/Telegram-Mac/TouchBarEmojiItemView.swift @@ -0,0 +1,36 @@ +// +// TouchBarEmojiItemView.swift +// Telegram +// +// Created by Mikhail Filimonov on 14/09/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa + +@available(OSX 10.12.2, *) +class TouchBarEmojiItemView: NSScrubberItemView { + private let textView: NSTextField = NSTextField() + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(textView) + textView.backgroundColor = .clear + textView.font = .normal(30) + } + + func update(_ emoji: String) { + textView.stringValue = emoji + } + + override func layout() { + super.layout() + textView.setFrameSize(38, 40) + textView.center() + } + + + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/Telegram-Mac/TouchBarEmojiPicker.swift b/Telegram-Mac/TouchBarEmojiPicker.swift new file mode 100644 index 0000000000..d1cb4786da --- /dev/null +++ b/Telegram-Mac/TouchBarEmojiPicker.swift @@ -0,0 +1,172 @@ +// +// TouchBarEmojiPicker.swift +// Telegram +// +// Created by Mikhail Filimonov on 14/09/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +@available(OSX 10.12.2, *) +class TGScrubber : NSScrubber { + private let leftShadow = ShadowView(frame: NSMakeRect(0, 0, 20, 40)) + private let rightShadow = ShadowView(frame: NSMakeRect(0, 0, 20, 40)) + init() { + super.init(frame: NSZeroRect) + leftShadow.shadowBackground = .black + leftShadow.direction = .horizontal(false) + addSubview(leftShadow) + + rightShadow.shadowBackground = .black + rightShadow.direction = .horizontal(true) + addSubview(rightShadow) + } + + + override func layout() { + super.layout() + leftShadow.frame = NSMakeRect(0, 0, 20, frame.height) + rightShadow.frame = NSMakeRect(frame.width - rightShadow.frame.width, 0, 20, frame.height) + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +enum TouchBarEmojiPickerEntry { + case header(TextViewLayout) + case emoji(String) +} + +@available(OSX 10.12.2, *) +fileprivate extension NSTouchBarItem.Identifier { + static let emoji = NSTouchBarItem.Identifier("\(Bundle.main.bundleIdentifier!).touchBar.emoji") +} + +@available(OSX 10.12.2, *) +private extension NSTouchBar.CustomizationIdentifier { + static let emojiScrubber = NSTouchBar.CustomizationIdentifier("\(Bundle.main.bundleIdentifier!).touchBar.EmojiScrubber") +} + + +@available(OSX 10.12.2, *) +private class EmojiScrubberBarItem: NSCustomTouchBarItem, NSScrubberDelegate, NSScrubberDataSource, NSScrubberFlowLayoutDelegate { + + private static let emojiItemViewIdentifier = "EmojiItemViewIdentifier" + private static let headerItemViewIdentifier = "HeaderItemViewIdentifier" + + private let entries: [TouchBarEmojiPickerEntry] + private let selectedEmoji: (String)->Void + init(identifier: NSTouchBarItem.Identifier, selectedEmoji:@escaping(String)->Void, entries: [TouchBarEmojiPickerEntry]) { + self.entries = entries + self.selectedEmoji = selectedEmoji + super.init(identifier: identifier) + + let scrubber = TGScrubber() + scrubber.register(TouchBarEmojiItemView.self, forItemIdentifier: NSUserInterfaceItemIdentifier(rawValue: EmojiScrubberBarItem.emojiItemViewIdentifier)) + scrubber.register(TouchBarScrubberHeaderItemView.self, forItemIdentifier: NSUserInterfaceItemIdentifier(rawValue: EmojiScrubberBarItem.headerItemViewIdentifier)) + + scrubber.mode = .free + scrubber.selectionBackgroundStyle = .roundedBackground + scrubber.delegate = self + scrubber.dataSource = self + + self.view = scrubber + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + + + + func numberOfItems(for scrubber: NSScrubber) -> Int { + return entries.count + } + + + func scrubber(_ scrubber: NSScrubber, viewForItemAt index: Int) -> NSScrubberItemView { + let itemView: NSScrubberItemView + switch entries[index] { + case let .header(title): + let view = scrubber.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: EmojiScrubberBarItem.headerItemViewIdentifier), owner: nil) as! TouchBarScrubberHeaderItemView + view.update(title) + itemView = view + case let .emoji(emoji): + let view = scrubber.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: EmojiScrubberBarItem.emojiItemViewIdentifier), owner: nil) as! TouchBarEmojiItemView + view.update(emoji) + itemView = view + } + + return itemView + } + + func scrubber(_ scrubber: NSScrubber, layout: NSScrubberFlowLayout, sizeForItemAt itemIndex: Int) -> NSSize { + switch entries[itemIndex] { + case let .header(layout): + return NSMakeSize(layout.layoutSize.width + 20, 30) + case .emoji: + return NSSize(width: 42, height: 30) + } + } + + + func scrubber(_ scrubber: NSScrubber, didSelectItemAt index: Int) { + switch entries[index] { + case let .emoji(emoji): + selectedEmoji(emoji) + default: + break + } + scrubber.selectedIndex = -1 + } +} + +@available(OSX 10.12.2, *) +class TouchBarEmojiPicker: NSTouchBar, NSTouchBarDelegate { + private let selectedEmoji: (String) -> Void + private let entries: [TouchBarEmojiPickerEntry] + init(recent: [String], segments: [EmojiSegment : [String]], selectedEmoji: @escaping(String) -> Void) { + var entries: [TouchBarEmojiPickerEntry] = [] + if !recent.isEmpty { + let layout = TextViewLayout(.initialize(string: L10n.touchBarRecentlyUsed, color: .grayText, font: .normal(.header))) + layout.measure(width: .greatestFiniteMagnitude) + entries.append(.header(layout)) + entries.append(contentsOf: recent.map {.emoji($0)}) + } + + for segment in segments.sorted(by: {$0.key < $1.key}) { + let layout = TextViewLayout(.initialize(string: segment.key.localizedString, color: .grayText, font: .normal(.header))) + layout.measure(width: .greatestFiniteMagnitude) + entries.append(.header(layout)) + entries.append(contentsOf: segment.value.map {.emoji($0)}) + } + + self.entries = entries + self.selectedEmoji = selectedEmoji + super.init() + delegate = self + customizationIdentifier = .emojiScrubber + defaultItemIdentifiers = [.emoji] + customizationAllowedItemIdentifiers = [.emoji] + } + + + func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? { + switch identifier { + case .emoji: + let scrubberItem: NSCustomTouchBarItem = EmojiScrubberBarItem(identifier: identifier, selectedEmoji: selectedEmoji, entries: self.entries) + return scrubberItem + default: + return nil + } + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/TouchBarScrubberHeaderItemView.swift b/Telegram-Mac/TouchBarScrubberHeaderItemView.swift new file mode 100644 index 0000000000..4b5a3527e8 --- /dev/null +++ b/Telegram-Mac/TouchBarScrubberHeaderItemView.swift @@ -0,0 +1,37 @@ +// +// TouchBarStickerHeaderItemView.swift +// Telegram +// +// Created by Mikhail Filimonov on 14/09/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +@available(OSX 10.12.2, *) +class TouchBarScrubberHeaderItemView: NSScrubberItemView { + private let textView: TextView = TextView() + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(textView) + textView.backgroundColor = .clear + } + + func update(_ layout: TextViewLayout) { + textView.update(layout) + } + + override func layout() { + super.layout() + textView.centerX() + textView.centerY(addition: -1) + } + + + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + +} diff --git a/Telegram-Mac/TouchBarStickerItemView.swift b/Telegram-Mac/TouchBarStickerItemView.swift new file mode 100644 index 0000000000..ae307b70ff --- /dev/null +++ b/Telegram-Mac/TouchBarStickerItemView.swift @@ -0,0 +1,101 @@ +// +// TouchBarThumbailItemView.swift +// Telegram +// +// Created by Mikhail Filimonov on 14/09/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import SwiftSignalKit +import TelegramCore + +import TGUIKit + + + +@available(OSX 10.12.2, *) +class TouchBarStickerItemView: NSScrubberItemView { + private var animatedSticker:MediaAnimatedStickerView? + private var imageView: TransformImageView? + private let fetchDisposable = MetaDisposable() + private(set) var file: TelegramMediaFile? + required override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + +// let gesture = NSPressGestureRecognizer(target: self, action: #selector(pressGesture)) +// gesture.minimumPressDuration = 0.5 +// self.addGestureRecognizer(gesture) + } + + var quickPreview: QuickPreviewMedia? { + if let file = file { + let reference = file.stickerReference != nil ? FileMediaReference.stickerPack(stickerPack: file.stickerReference!, media: file) : FileMediaReference.standalone(media: file) + if file.isAnimatedSticker { + return .file(reference, AnimatedStickerPreviewModalView.self) + } else { + return .file(reference, StickerPreviewModalView.self) + } + } + return nil + } + + + func update(context: AccountContext, file: TelegramMediaFile, animated: Bool) { + self.file = file + if file.isAnimatedSticker, animated { + self.imageView?.removeFromSuperview() + self.imageView = nil + + if self.animatedSticker == nil { + self.animatedSticker = MediaAnimatedStickerView(frame: NSZeroRect) + addSubview(self.animatedSticker!) + } + guard let animatedSticker = self.animatedSticker else { + return + } + animatedSticker.update(with: file, size: NSMakeSize(30, 30), context: context, parent: nil, table: nil, parameters: nil, animated: false, positionFlags: nil, approximateSynchronousValue: false) + } else { + self.animatedSticker?.removeFromSuperview() + self.animatedSticker = nil + if self.imageView == nil { + self.imageView = TransformImageView() + addSubview(self.imageView!) + } + guard let imageView = self.imageView else { + return + } + let dimensions = file.dimensions?.size ?? frame.size + let imageSize = NSMakeSize(30, 30) + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: dimensions.aspectFitted(imageSize), boundingSize: imageSize, intrinsicInsets: NSEdgeInsets()) + + imageView.setSignal(signal: cachedMedia(media: file, arguments: arguments, scale: backingScaleFactor), clearInstantly: true) + imageView.setSignal(chatMessageSticker(postbox: context.account.postbox, file: stickerPackFileReference(file), small: true, scale: backingScaleFactor, fetched: true), cacheImage: { result in + cacheMedia(result, media: file, arguments: arguments, scale: System.backingScale) + }) + imageView.set(arguments: arguments) + imageView.setFrameSize(imageSize) + } + + // fetchDisposable.set(fileInteractiveFetched(account: account, fileReference: FileMediaReference.stickerPack(stickerPack: file.stickerReference!, media: file)).start()) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func updateLayer() { + // layer?.backgroundColor = NSColor.controlColor.cgColor + } + + deinit { + fetchDisposable.dispose() + } + + override func layout() { + super.layout() + + imageView?.center() + animatedSticker?.center() + } +} diff --git a/Telegram-Mac/TouchBarThumbailItemView.swift b/Telegram-Mac/TouchBarThumbailItemView.swift new file mode 100644 index 0000000000..394399c300 --- /dev/null +++ b/Telegram-Mac/TouchBarThumbailItemView.swift @@ -0,0 +1,62 @@ +// +// TouchBarThumbailItemView.swift +// Telegram +// +// Created by Mikhail Filimonov on 14/09/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import SwiftSignalKitMac +import TelegramCoreMac +import TGUIKit + +@available(OSX 10.12.2, *) +class TouchBarStickerItemView: NSScrubberItemView { + + private let imageView: TransformImageView = TransformImageView() + + private let spinner: NSProgressIndicator + required override init(frame frameRect: NSRect) { + + spinner = NSProgressIndicator() + + super.init(frame: frameRect) + + spinner.isIndeterminate = true + spinner.style = .spinning + spinner.sizeToFit() + spinner.frame = bounds.insetBy(dx: (bounds.width - spinner.frame.width)/2, dy: (bounds.height - spinner.frame.height)/2) + spinner.isHidden = true + spinner.controlSize = .small + spinner.appearance = NSAppearance(named: NSAppearance.Name.vibrantDark) + spinner.autoresizingMask = [.minXMargin, .maxXMargin, .minYMargin, .maxXMargin] + + subviews = [imageView, spinner] + } + + func update(account: Account, file: TelegramMediaFile) { + let dimensions = file.dimensions ?? frame.size + let imageSize = NSMakeSize(30, 30) + imageView.setSignal(chatMessageSticker(account: account, fileReference: FileMediaReference.stickerPack(stickerPack: file.stickerReference!, media: file), type: .thumb, scale: backingScaleFactor)) + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize:dimensions.aspectFitted(imageSize), boundingSize: imageSize, intrinsicInsets: NSEdgeInsets()) + imageView.set(arguments: arguments) + imageView.setFrameSize(imageSize) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func updateLayer() { + // layer?.backgroundColor = NSColor.controlColor.cgColor + } + + override func layout() { + super.layout() + + imageView.center() + spinner.sizeToFit() + spinner.frame = bounds.insetBy(dx: (bounds.width - spinner.frame.width)/2, dy: (bounds.height - spinner.frame.height)/2) + } +} diff --git a/Telegram-Mac/TransformImageView.swift b/Telegram-Mac/TransformImageView.swift index 024f45b060..7f5abd1d96 100644 --- a/Telegram-Mac/TransformImageView.swift +++ b/Telegram-Mac/TransformImageView.swift @@ -7,23 +7,48 @@ // import Cocoa -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit import TGUIKit + +private let threadPool = ThreadPool(threadCount: 1, threadPriority: 0.1) + + + open class TransformImageView: NSView { - public var imageUpdated: (() -> Void)? - public var alphaTransitionOnFirstUpdate = false + public var imageUpdated: ((Any?) -> Void)? private let disposable = MetaDisposable() - private let cachedDisposable = MetaDisposable() public var animatesAlphaOnFirstTransition:Bool = false private let argumentsPromise = Promise() + private(set) var isFullyLoaded: Bool = false + public var ignoreFullyLoad:Bool = false private var first:Bool = true public init() { super.init(frame: NSZeroRect) self.wantsLayer = true self.layer?.disableActions() self.background = .clear + layerContentsRedrawPolicy = .never + } + + open override var isFlipped: Bool { + return true + } + + var image: CGImage? { + set { + layer?.contents = newValue + imageUpdated?(newValue) + } + get { + if let any = layer?.contents { + return any as! CGImage + } else { + return nil + } + } } required public override init(frame frameRect: NSRect) { @@ -31,6 +56,7 @@ open class TransformImageView: NSView { self.wantsLayer = true self.layer?.disableActions() self.background = .clear + layerContentsRedrawPolicy = .never } required public init?(coder aDecoder: NSCoder) { @@ -39,7 +65,6 @@ open class TransformImageView: NSView { deinit { self.disposable.dispose() - cachedDisposable.dispose() } @@ -47,56 +72,115 @@ open class TransformImageView: NSView { disposable.set(nil) } - public func setSignal(signal: Signal) { - self.disposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] image in - self?.layer?.contents = image + public func setSignal(signal: Signal, clearInstantly: Bool = true, animate: Bool = false) { + self.disposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] result in + + let hasImage = self?.image != nil + + if clearInstantly { + self?.image = result.image + } else if let image = result.image { + self?.image = image + } + if !hasImage && animate { + self?.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } else if animate { + self?.layer?.animateContents() + } + self?.isFullyLoaded = result.highQuality })) } - public func setSignal(account: Account, signal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>, clearInstantly: Bool = true, animate:Bool = false, cacheImage:(Signal) -> Signal = {_ in return .single(Void())}) { + open override func setFrameOrigin(_ newOrigin: NSPoint) { + super.setFrameOrigin(newOrigin) + } + + + public func setSignal(_ signal: Signal, clearInstantly: Bool = false, animate:Bool = false, synchronousLoad: Bool = false, cacheImage:@escaping(TransformImageResult) -> Void = { _ in } ) { if clearInstantly { - self.layer?.contents = nil + self.image = nil } - let result = combineLatest(signal, argumentsPromise.get() |> distinctUntilChanged) |> deliverOn(account.graphicsThreadPool) |> mapToThrottled { transform, arguments -> Signal in - return deferred { - return Signal.single(transform(arguments)?.generateImage()) - } + + if isFullyLoaded && !ignoreFullyLoad { + disposable.set(nil) + isFullyLoaded = false + return } - cachedDisposable.set(cacheImage(result).start()) + var combine = combineLatest(signal, argumentsPromise.get() |> distinctUntilChanged) - self.disposable.set((result |> deliverOnMainQueue).start(next: {[weak self] next in - + if !synchronousLoad { + combine = combine |> deliverOn(threadPool) + } + + let result = combine |> map { data, arguments -> TransformImageResult in + autoreleasepool { + let context = data.execute(arguments, data.data) + let image = context?.generateImage() + return TransformImageResult(image, context?.isHighQuality ?? false) + } + } |> deliverOnMainQueue + + self.disposable.set(result.start(next: { [weak self] result in if let strongSelf = self { - if strongSelf.layer?.contents == nil && strongSelf.animatesAlphaOnFirstTransition { + if strongSelf.image == nil && strongSelf.animatesAlphaOnFirstTransition { strongSelf.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) } - - self?.layer?.contents = next - + self?.image = result.image if !strongSelf.first && animate { self?.layer?.animateContents() } strongSelf.first = false + cacheImage(result) } - })) + + } + + + open override var isHidden: Bool { + didSet { + + } + } + + public var hasImage: Bool { + return image != nil } public func set(arguments:TransformImageArguments) ->Void { argumentsPromise.set(.single(arguments)) } + override open func copy() -> Any { let view = NSView() view.wantsLayer = true view.background = .clear - view.layer?.frame = NSMakeRect(0, visibleRect.minY == 0 ? 0 : visibleRect.height - frame.height, frame.width, frame.height) - view.layer?.contents = self.layer?.contents - view.layer?.masksToBounds = true + view.layer?.contents = self.image view.frame = self.visibleRect + view.layer?.masksToBounds = true + + + if bounds != visibleRect { + if let image = self.layer?.contents { + view.layer?.contents = generateImage(bounds.size, contextGenerator: { size, ctx in + ctx.clear(bounds) + ctx.setFillColor(.clear) + ctx.fill(bounds) + if visibleRect.minY != 0 { + ctx.clip(to: NSMakeRect(0, 0, bounds.width, bounds.height - ( bounds.height - visibleRect.height))) + } else { + ctx.clip(to: NSMakeRect(0, (bounds.height - visibleRect.height), bounds.width, bounds.height - ( bounds.height - visibleRect.height))) + } + ctx.draw(image as! CGImage, in: bounds) + }, opaque: false) + } + } + view.layer?.shouldRasterize = true view.layer?.rasterizationScale = backingScaleFactor + return view } diff --git a/Telegram-Mac/TransformOutgoingMessageMedia.swift b/Telegram-Mac/TransformOutgoingMessageMedia.swift index a3d1faa318..9f0a201de0 100644 --- a/Telegram-Mac/TransformOutgoingMessageMedia.swift +++ b/Telegram-Mac/TransformOutgoingMessageMedia.swift @@ -7,16 +7,17 @@ // import Foundation -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore +import Postbox +import SwiftSignalKit import TGUIKit -public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, media: Media, opportunistic: Bool) -> Signal { - switch media { + +public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, reference: AnyMediaReference, opportunistic: Bool) -> Signal { + switch reference.media { case let file as TelegramMediaFile: let signal = Signal<(MediaResourceData, String?), NoError> { subscriber in - let fetch = postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .file)).start() + let fetch = fetchedMediaResource(mediaBox: postbox.mediaBox, reference: reference.resourceReference(file.resource), statsCategory: .file).start() //postbox.mediaBox.fetchedResource(file.resource, tag: TelegramMediaResourceFetchTag(statsCategory: .file)).start() let dataSignal = resourceType(mimeType: file.mimeType) |> mapToSignal { ext in return postbox.mediaBox.resourceData(file.resource, option: .complete(waitUntilFetchStatus: true)) |> map { result in return (result, ext) @@ -43,25 +44,37 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me } return result - |> mapToSignal { data -> Signal in + |> mapToSignal { data -> Signal in if data.0.complete { return Signal { subscriber in + let resource = (file.resource as? LocalFileReferenceMediaResource) + var size = resource?.size + + if resource == nil { + size = Int32(data.0.size) + } + var thumbImage:CGImage? = nil let thumbedFile:String - if file.isVideo && file.isAnimated { - thumbedFile = data.0.path + ".mp4" + if let resource = resource { + thumbedFile = resource.localFilePath } else { - thumbedFile = data.0.path.appending(".\(file.fileName?.nsstring.pathExtension ?? data.1 ?? "txt")") + if file.isVideo && file.isAnimated { + thumbedFile = data.0.path + ".mp4" + } else { + thumbedFile = data.0.path.appending(".\(file.fileName?.nsstring.pathExtension ?? data.1 ?? "mp4")") + } } - try? FileManager.default.linkItem(atPath: data.0.path, toPath: thumbedFile) + + try? FileManager.default.createSymbolicLink(atPath: data.0.path, withDestinationPath: thumbedFile) if file.mimeType.hasPrefix("image/") { if let thumbData = try? Data(contentsOf: URL(fileURLWithPath: thumbedFile)) { let options = NSMutableDictionary() - options.setValue(90 as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) + options.setValue(320 as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) if let imageSource = CGImageSourceCreateWithData(thumbData as CFData, nil) { @@ -73,28 +86,41 @@ public func transformOutgoingMessageMedia(postbox: Postbox, network: Network, me } else if file.mimeType.hasPrefix("video/") { let asset = AVAsset(url: URL(fileURLWithPath: thumbedFile)) let imageGenerator = AVAssetImageGenerator(asset: asset) - imageGenerator.maximumSize = CGSize(width: 90, height: 90) + imageGenerator.maximumSize = CGSize(width: 320, height: 320) imageGenerator.appliesPreferredTrackTransform = true thumbImage = try? imageGenerator.copyCGImage(at: CMTime(seconds: 0.0, preferredTimescale: asset.duration.timescale), actualTime: nil) } + if thumbedFile != resource?.localFilePath { + try? FileManager.default.removeItem(atPath: thumbedFile) + } - if let thumbImage = thumbImage { - let imageRep = NSBitmapImageRep(cgImage: thumbImage) - let options: [NSBitmapImageRep.PropertyKey: Any] = [.compressionFactor: 0.6] - let compressedData: Data? = imageRep.representation(using: .jpeg, properties: options) + if let image = thumbImage { - if let compressedData = compressedData { - let thumbnailResource = LocalFileMediaResource(fileId: arc4random64()) - postbox.mediaBox.storeResourceData(thumbnailResource.id, data: compressedData) - subscriber.putNext(file.withUpdatedSize(data.0.size).withUpdatedPreviewRepresentations([TelegramMediaImageRepresentation(dimensions: thumbImage.size, resource: thumbnailResource)])) + let options = NSMutableDictionary() + options.setValue(320 as NSNumber, forKey: kCGImageDestinationImageMaxPixelSize as String) + + let colorQuality: Float = 0.2 + options.setObject(colorQuality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString) + + + let mutableData: CFMutableData = NSMutableData() as CFMutableData + if let colorDestination = CGImageDestinationCreateWithData(mutableData, kUTTypeJPEG, 1, options) { + CGImageDestinationSetProperties(colorDestination, nil) - return EmptyDisposable + CGImageDestinationAddImage(colorDestination, image, options as CFDictionary) + if CGImageDestinationFinalize(colorDestination) { + let thumbnailResource = LocalFileMediaResource(fileId: arc4random64(), isSecretRelated: false) + postbox.mediaBox.storeResourceData(thumbnailResource.id, data: mutableData as Data) + subscriber.putNext(AnyMediaReference.standalone(media: file.withUpdatedSize(Int(size ?? 0)).withUpdatedPreviewRepresentations([TelegramMediaImageRepresentation(dimensions: PixelDimensions(image.size), resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil)]))) + + return EmptyDisposable + } } - } + } - subscriber.putNext(file.withUpdatedSize(data.0.size)) + subscriber.putNext(AnyMediaReference.standalone(media: file.withUpdatedSize(Int(size ?? 0)))) subscriber.putCompletion() diff --git a/Telegram-Mac/Tuple.swift b/Telegram-Mac/Tuple.swift new file mode 100644 index 0000000000..4c2c48f40d --- /dev/null +++ b/Telegram-Mac/Tuple.swift @@ -0,0 +1,43 @@ +import Foundation + +public final class Tuple1 { + public let _0: T0 + + public init(_ _0: T0) { + self._0 = _0 + } +} + +public final class Tuple2 { + public let _0: T0 + public let _1: T1 + + public init(_ _0: T0, _ _1: T1) { + self._0 = _0 + self._1 = _1 + } +} + +public final class Tuple3 { + public let _0: T0 + public let _1: T1 + public let _2: T2 + + public init(_ _0: T0, _ _1: T1, _ _2: T2) { + self._0 = _0 + self._1 = _1 + self._2 = _2 + } +} + +public func Tuple(_ _0: T0) -> Tuple1 { + return Tuple1(_0) +} + +public func Tuple(_ _0: T0, _ _1: T1) -> Tuple2 { + return Tuple2(_0, _1) +} + +public func Tuple(_ _0: T0, _ _1: T1, _ _2: T2) -> Tuple3 { + return Tuple3(_0, _1, _2) +} diff --git a/Telegram-Mac/TurnOnNotificationsRowItem.swift b/Telegram-Mac/TurnOnNotificationsRowItem.swift new file mode 100644 index 0000000000..5182e3ce60 --- /dev/null +++ b/Telegram-Mac/TurnOnNotificationsRowItem.swift @@ -0,0 +1,90 @@ +// +// TurnOnNotificationsRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 17.08.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit + +final class TurnOnNotificationsRowItem : GeneralRowItem { + fileprivate let header: TextViewLayout + fileprivate let text: TextViewLayout + init(_ initialSize: NSSize, stableId: AnyHashable, viewType: GeneralViewType) { + let hAttr: NSAttributedString = .initialize(string: L10n.notificationSettingsTurnOnTextTitle, color: theme.colors.text, font: .medium(.text)) + let tAttr: NSAttributedString = .initialize(string: L10n.notificationSettingsTurnOnTextText, color: theme.colors.text, font: .normal(.text)) + header = .init(hAttr) + text = .init(tAttr) + + super.init(initialSize, stableId: stableId, viewType: viewType) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + + header.measure(width: width - viewType.innerInset.left - viewType.innerInset.right - 30) + text.measure(width: width - viewType.innerInset.left - viewType.innerInset.right) + + return true + } + + override var height: CGFloat { + return viewType.innerInset.bottom + viewType.innerInset.top + 6 + header.layoutSize.height + text.layoutSize.height + } + override func viewClass() -> AnyClass { + return TurnOnNotificationsRowView.self + } + +} + + +private final class TurnOnNotificationsRowView: GeneralContainableRowView { + private let imageView: ImageView = ImageView() + private let textView = TextView() + private let headerView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + + textView.userInteractionEnabled = false + textView.isSelectable = false + + headerView.userInteractionEnabled = false + headerView.isSelectable = false + + addSubview(imageView) + addSubview(headerView) + addSubview(textView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + + guard let item = item as? TurnOnNotificationsRowItem else { + return + } + + imageView.setFrameOrigin(NSMakePoint(item.viewType.innerInset.left, item.viewType.innerInset.top)) + headerView.setFrameOrigin(NSMakePoint(imageView.frame.maxX + 6, item.viewType.innerInset.top)) + textView.setFrameOrigin(NSMakePoint(item.viewType.innerInset.left, headerView.frame.maxY + 6)) + + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + guard let item = item as? TurnOnNotificationsRowItem else { + return + } + + imageView.image = #imageLiteral(resourceName: "Icon_MessageSentFailed").precomposed() + imageView.sizeToFit() + + textView.update(item.text) + headerView.update(item.header) + } +} diff --git a/Telegram-Mac/TwoStepVerification.swift b/Telegram-Mac/TwoStepVerification.swift index bf472caa29..d45f7f255b 100644 --- a/Telegram-Mac/TwoStepVerification.swift +++ b/Telegram-Mac/TwoStepVerification.swift @@ -7,43 +7,79 @@ // import Cocoa -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac -import MtProtoKitMac - - - -func apiInputPeer(_ peer: Peer) -> Api.InputPeer? { - - switch peer { - case let user as TelegramUser where user.accessHash != nil: - return Api.InputPeer.inputPeerUser(userId: user.id.id, accessHash: user.accessHash!) - case let group as TelegramGroup: - return Api.InputPeer.inputPeerChat(chatId: group.id.id) - case let channel as TelegramChannel: - if let accessHash = channel.accessHash { - return Api.InputPeer.inputPeerChannel(channelId: channel.id.id, accessHash: accessHash) - } else { - return nil - } - default: - return nil - } -} - -func apiInputChannel(_ peer: Peer) -> Api.InputChannel? { - if let channel = peer as? TelegramChannel, let accessHash = channel.accessHash { - return Api.InputChannel.inputChannel(channelId: channel.id.id, accessHash: accessHash) - } else { - return nil - } -} - -func apiInputUser(_ peer: Peer) -> Api.InputUser? { - if let user = peer as? TelegramUser, let accessHash = user.accessHash { - return Api.InputUser.inputUser(userId: user.id.id, accessHash: accessHash) - } else { - return nil - } -} +import Postbox +import TelegramCore + +import SwiftSignalKit + + + + +// +//func apiInputChannel(_ peer: Peer) -> Api.InputChannel? { +// if let channel = peer as? TelegramChannel, let accessHash = channel.accessHash { +// return Api.InputChannel.inputChannel(channelId: channel.id.id, accessHash: accessHash) +// } else { +// return nil +// } +//} +// +//func apiInputUser(_ peer: Peer) -> Api.InputUser? { +// if let user = peer as? TelegramUser, let accessHash = user.accessHash { +// return Api.InputUser.inputUser(userId: user.id.id, accessHash: accessHash) +// } else { +// return nil +// } +//} +// +// +// +//public func reportMessages(postbox: Postbox, network: Network, peerId: PeerId, messageIds: [MessageId], reason:ReportReason) -> Signal { +// return postbox.modify{ transaction -> Void in +// if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) { +// // return Api.functions.messages. +// } +// } +//} + +//public func getCountryCode(network: Network)->Signal { +// return network.request(Api.functions.help.getNearestDc()) |> retryRequest |> map { value in +// switch value { +// case let .nearestDc(country, _, _): +// return country +// } +// } +//} + + + +//public func dropSecureId(network: Network, currentPassword: String) -> Signal { +// return twoStepAuthData(network) +// |> mapError { _ -> AuthorizationPasswordVerificationError in +// return .generic +// } +// |> mapToSignal { authData -> Signal in +// if let currentSalt = authData.currentSalt { +// var data = Data() +// data.append(currentSalt) +// data.append(currentPassword.data(using: .utf8, allowLossyConversion: true)!) +// data.append(currentSalt) +// currentPasswordHash = Buffer(data: sha256Digest(data)) +// } else { +// currentPasswordHash = Buffer(data: Data()) +// } +// +// let flags: Int32 = 1 << 1 +// +// let settings = network.request(Api.functions.account.getPasswordSettings(currentPasswordHash: currentPasswordHash), automaticFloodWait: false) |> mapError {_ in return AuthorizationPasswordVerificationError.generic} +// +// +// return settings |> mapToSignal { value -> Signal in +// switch value { +// case let .passwordSettings(email, secureSalt, _, _): +// return network.request(Api.functions.account.updatePasswordSettings(currentPasswordHash: currentPasswordHash, newSettings: Api.account.PasswordInputSettings.passwordInputSettings(flags: flags, newSalt: secureSalt, newPasswordHash: currentPasswordHash, hint: nil, email: email, newSecureSalt: secureSalt, newSecureSecret: nil, newSecureSecretId: nil)), automaticFloodWait: false) |> map {_ in} |> mapError {_ in return AuthorizationPasswordVerificationError.generic} +// } +// } +// } +//} + diff --git a/Telegram-Mac/TwoStepVerificationPasswordEntryController.swift b/Telegram-Mac/TwoStepVerificationPasswordEntryController.swift deleted file mode 100644 index 0df240e821..0000000000 --- a/Telegram-Mac/TwoStepVerificationPasswordEntryController.swift +++ /dev/null @@ -1,307 +0,0 @@ -// -// TwoStepVerificationPasswordEntryController.swift -// Telegram -// -// Created by keepcoder on 17/10/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - -import Cocoa -import SwiftSignalKitMac -import TelegramCoreMac -import TGUIKit - -fileprivate func prepareTransition(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], initialSize:NSSize, arguments:TwoStepVerificationPasswordEntryControllerArguments) -> TableUpdateTransition { - - let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in - return entry.entry.item(arguments, initialSize: initialSize) - } - - return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true) -} - -class TwoStepVerificationPasswordEntryController: TableViewController { - - fileprivate let mode: TwoStepVerificationPasswordEntryMode - fileprivate let result: Promise - fileprivate var nextAction:(()->Void)? - fileprivate let disposable = MetaDisposable() - init(account: Account, mode: TwoStepVerificationPasswordEntryMode, result: Promise) { - self.mode = mode - self.result = result - super.init(account) - } - - override func viewDidLoad() { - super.viewDidLoad() - - let account = self.account - let mode = self.mode - let result = self.result - - let initialStage: PasswordEntryStage - switch mode { - case .setup, .change: - initialStage = .entry(text: "") - case .setupEmail: - initialStage = .email(password: "", hint: "", text: "") - } - let initialState = TwoStepVerificationPasswordEntryControllerState(stage: initialStage, updating: false) - - let statePromise = ValuePromise(initialState, ignoreRepeated: true) - let stateValue = Atomic(value: initialState) - let updateState: ((TwoStepVerificationPasswordEntryControllerState) -> TwoStepVerificationPasswordEntryControllerState) -> Void = { f in - statePromise.set(stateValue.modify { f($0) }) - } - - - let actionsDisposable = DisposableSet() - - let updatePasswordDisposable = MetaDisposable() - actionsDisposable.add(updatePasswordDisposable) - - func checkPassword(_ skipEmail:Bool = false) { - var passwordHintEmail: (String, String, String)? - var invalidReentry = false - updateState { state in - if state.updating { - return state - } else { - switch state.stage { - case let .entry(text): - if text.isEmpty { - return state - } else { - return state.withUpdatedStage(.reentry(first: text, text: "")) - } - case let .reentry(first, text): - if text.isEmpty { - return state - } else if text != first { - invalidReentry = true - return state.withUpdatedStage(.entry(text: "")) - } else { - return state.withUpdatedStage(.hint(password: text, text: "")) - } - case let .hint(password, text): - switch mode { - case .setup: - return state.withUpdatedStage(.email(password: password, hint: text, text: "")) - case .change: - passwordHintEmail = (password, text, "") - return state.withUpdatedUpdating(true) - case .setupEmail: - preconditionFailure() - } - case let .email(password, hint, text): - passwordHintEmail = (password, hint, text) - return state.withUpdatedUpdating(true) - } - } - } - if let (password, hint, email) = passwordHintEmail { - switch mode { - case .setup, .change: - var currentPassword: String? - if case let .change(current) = mode { - currentPassword = current - } - updatePasswordDisposable.set((updateTwoStepVerificationPassword(account: account, currentPassword: currentPassword, updatedPassword: .password(password: password, hint: hint, email: skipEmail ? "" : email)) |> deliverOnMainQueue).start(next: { update in - updateState { - $0.withUpdatedUpdating(false) - } - switch update { - case let .password(password, pendingEmailPattern): - result.set(.single(TwoStepVerificationPasswordEntryResult(password: password, pendingEmailPattern: pendingEmailPattern))) - case .none: - break - } - }, error: { error in - updateState { - $0.withUpdatedUpdating(false) - } - let alertText: String - switch error { - case .generic: - alertText = tr(.twoStepAuthErrorGeneric) - case .invalidEmail: - alertText = tr(.twoStepAuthErrorInvalidEmail) - } - alert(for: mainWindow, header: appName, info: alertText) - })) - case let .setupEmail(password): - updatePasswordDisposable.set((updateTwoStepVerificationEmail(account: account, currentPassword: password, updatedEmail: email) |> deliverOnMainQueue).start(next: { update in - updateState { - $0.withUpdatedUpdating(false) - } - switch update { - case let .password(password, pendingEmailPattern): - result.set(.single(TwoStepVerificationPasswordEntryResult(password: password, pendingEmailPattern: pendingEmailPattern))) - case .none: - break - } - }, error: { error in - updateState { - $0.withUpdatedUpdating(false) - } - let alertText: String - switch error { - case .generic: - alertText = tr(.twoStepAuthErrorGeneric) - case .invalidEmail: - alertText = tr(.twoStepAuthErrorInvalidEmail) - } - alert(for: mainWindow, header: appName, info: alertText) - })) - } - } else if invalidReentry { - alert(for: mainWindow, header: appName, info: tr(.twoStepAuthErrorPasswordsDontMatch)) - } - } - - let arguments = TwoStepVerificationPasswordEntryControllerArguments(updateEntryText: { updatedText in - updateState { - $0.withUpdatedStage($0.stage.updateCurrentText(updatedText)) - } - }, next: { [weak self] in - - if (self?.rightBarView as? TextButtonBarView)?.button.isEnabled == false { - NSSound.beep() - return - } - - let value = stateValue.modify({$0}) - - if !value.updating { - switch value.stage { - case let .email(password: _, hint: _, text): - switch mode { - case .setupEmail: - checkPassword() - return - default: - break - } - if text.isEmpty { - confirm(for: mainWindow, with: appName, and: tr(.twoStepAuthEmailSkipAlert), successHandler: { _ in - checkPassword() - }) - } else { - checkPassword() - } - return - default: - break - } - } - - checkPassword() - }, skipEmail: { - confirm(for: mainWindow, with: appName, and: tr(.twoStepAuthEmailSkipAlert), successHandler: { _ in - checkPassword(true) - }) - }) - - let previous:Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) - let initialSize = self.atomicSize - - let signal = combineLatest(appearanceSignal, statePromise.get()) |> deliverOnMainQueue - |> map { appearance, state -> (TableUpdateTransition, Bool, String) in - - var nextEnabled = true - var title: String = "Password" - - switch state.stage { - case .entry: - title = tr(.twoStepAuthSetupPasswordTitle) - case .reentry: - title = tr(.twoStepAuthSetupPasswordTitle) - case .hint: - title = tr(.twoStepAuthSetupHintTitle) - case .email: - title = tr(.twoStepAuthSetupEmailTitle) - } - - if state.updating { - nextEnabled = false - } else { - switch state.stage { - case let .entry(text): - if text.isEmpty { - nextEnabled = false - } - case let.reentry(_, text): - if text.isEmpty { - nextEnabled = false - } - case .hint: - break - case .email(let text): - switch mode { - case .setupEmail: - nextEnabled = !text.text.isEmpty - default: - nextEnabled = true - } - } - - } - - let entries = twoStepVerificationPasswordEntryControllerEntries(state: state, mode: mode).map{AppearanceWrapperEntry(entry: $0, appearance: appearance)} - - return (prepareTransition(left: previous.swap(entries), right: entries, initialSize: initialSize.modify{$0}, arguments: arguments), nextEnabled, title) - } |> afterDisposed { - actionsDisposable.dispose() - } |> deliverOnMainQueue - - nextAction = arguments.next - - disposable.set(signal.start(next: { [weak self] transition, nextEnabled, title in - self?.genericView.merge(with: transition) - self?.readyOnce() - self?.setCenterTitle(title) - (self?.rightBarView as? TextButtonBarView)?.button.isEnabled = nextEnabled - })) - } - - deinit { - disposable.dispose() - } - - override func getRightBarViewOnce() -> BarView { - let button = TextButtonBarView(controller: self, text: tr(.composeNext)) - - button.button.set(handler: { [weak self] _ in - self?.nextAction?() - }, for: .Click) - - return button - } - - override func returnKeyAction() -> KeyHandlerResult { - nextAction?() - return .invoked - } - - override func firstResponder() -> NSResponder? { - if genericView.count > 1 { - if !(window?.firstResponder is NSTextView) { - return (genericView.viewNecessary(at: 1) as? GeneralInputRowView)?.firstResponder - } - } - return window?.firstResponder - } - - override func backKeyAction() -> KeyHandlerResult { - return .invokeNext - } - - override func becomeFirstResponder() -> Bool? { - return true - } - - override var removeAfterDisapper: Bool { - return true - } - -} diff --git a/Telegram-Mac/TwoStepVerificationResetController.swift b/Telegram-Mac/TwoStepVerificationResetController.swift deleted file mode 100644 index 35d6a239e1..0000000000 --- a/Telegram-Mac/TwoStepVerificationResetController.swift +++ /dev/null @@ -1,198 +0,0 @@ -// -// TwoStepVerificationResetController.swift -// Telegram -// -// Created by keepcoder on 18/10/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - -import Cocoa -import TelegramCoreMac -import SwiftSignalKitMac -import TGUIKit - - - - -private func twoStepVerificationResetControllerEntries(state: TwoStepVerificationResetControllerState, emailPattern: String) -> [TwoStepVerificationResetEntry] { - var entries: [TwoStepVerificationResetEntry] = [] - - var sectionId:Int32 = 0 - entries.append(.section(sectionId)) - sectionId += 1 - - entries.append(.codeEntry(sectionId : sectionId, state.codeText)) - entries.append(.codeInfo(sectionId : sectionId, tr(.twoStepAuthRecoveryCodeHelp) + "\n\n[\(tr(.twoStepAuthRecoveryEmailUnavailable(emailPattern)))]()")) - return entries -} - - -fileprivate func prepareTransition(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], initialSize:NSSize, arguments:TwoStepVerificationResetControllerArguments) -> TableUpdateTransition { - - let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in - return entry.entry.item(arguments, initialSize: initialSize) - } - - return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true) -} - -class TwoStepVerificationResetController : TableViewController { - fileprivate let emailPattern: String - fileprivate let result: Promise - fileprivate let disposable = MetaDisposable() - fileprivate var nextAction:(()->Void)? - init(account: Account, emailPattern: String, result: Promise) { - self.emailPattern = emailPattern - self.result = result - super.init(account) - } - - override var defaultBarTitle: String { - return tr(.twoStepAuthRecoveryTitle) - } - - deinit { - disposable.dispose() - } - - override func viewDidLoad() { - let account = self.account - let result = self.result - let emailPattern = self.emailPattern - let initialSize = self.atomicSize - let initialState = TwoStepVerificationResetControllerState(codeText: "", checking: false) - - let statePromise = ValuePromise(initialState, ignoreRepeated: true) - let stateValue = Atomic(value: initialState) - let updateState: ((TwoStepVerificationResetControllerState) -> TwoStepVerificationResetControllerState) -> Void = { f in - statePromise.set(stateValue.modify { f($0) }) - } - - let actionsDisposable = DisposableSet() - - let resetPasswordDisposable = MetaDisposable() - actionsDisposable.add(resetPasswordDisposable) - - let checkCode: () -> Void = { [weak self] in - - if (self?.rightBarView as? TextButtonBarView)?.button.isEnabled == false { - NSSound.beep() - return - } - - var code: String? - updateState { state in - if state.checking || state.codeText.isEmpty { - return state - } else { - code = state.codeText - return state.withUpdatedChecking(true) - } - } - if let code = code { - resetPasswordDisposable.set((recoverTwoStepVerificationPassword(account: account, code: code) |> deliverOnMainQueue).start(error: { error in - updateState { - return $0.withUpdatedChecking(false) - } - let alertText: String - switch error { - case .generic: - alertText = tr(.twoStepAuthGenericError) - case .invalidCode: - alertText = tr(.twoStepAuthRecoveryCodeInvalid) - case .codeExpired: - alertText = tr(.twoStepAuthRecoveryCodeExpired) - case .limitExceeded: - alertText = tr(.twoStepAuthFloodError) - } - alert(for: mainWindow, header: appName, info: alertText) - - }, completed: { - updateState { - return $0.withUpdatedChecking(false) - } - result.set(.single(true)) - })) - } - } - - let arguments = TwoStepVerificationResetControllerArguments(updateEntryText: { updatedText in - updateState { - $0.withUpdatedCodeText(updatedText) - } - }, next: { - checkCode() - }, openEmailInaccessible: { - alert(for: mainWindow, info: tr(.twoStepAuthErrorHaventEmail)) - }) - - - self.nextAction = checkCode - - let previous: Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) - - let signal = combineLatest(appearanceSignal, statePromise.get()) |> deliverOnMainQueue - |> map { appearance, state -> (TableUpdateTransition, Bool) in - - var nextEnabled = true - - - if state.checking { - nextEnabled = false - } else { - if state.codeText.isEmpty { - nextEnabled = false - } - } - let entries = twoStepVerificationResetControllerEntries(state: state, emailPattern: emailPattern).map{AppearanceWrapperEntry(entry: $0, appearance: appearance)} - - return (prepareTransition(left: previous.swap(entries), right: entries, initialSize: initialSize.modify{$0}, arguments: arguments), nextEnabled) - } |> afterDisposed { - actionsDisposable.dispose() - } - - disposable.set(signal.start(next: { [weak self] transition, enabled in - self?.genericView.merge(with: transition) - self?.readyOnce() - (self?.rightBarView as? TextButtonBarView)?.button.isEnabled = enabled - })) - } - - override func getRightBarViewOnce() -> BarView { - let button = TextButtonBarView(controller: self, text: tr(.composeNext)) - - button.button.set(handler: { [weak self] _ in - self?.nextAction?() - }, for: .Click) - - return button - } - - override func returnKeyAction() -> KeyHandlerResult { - nextAction?() - return .invoked - } - - override func firstResponder() -> NSResponder? { - if genericView.count > 1 { - return (genericView.viewNecessary(at: 1) as? GeneralInputRowView)?.firstResponder - } - return nil - } - - override func backKeyAction() -> KeyHandlerResult { - return .invokeNext - } - - override func becomeFirstResponder() -> Bool? { - return true - } - - override var removeAfterDisapper: Bool { - return true - } - -} - - - diff --git a/Telegram-Mac/TwoStepVerificationUnlockController.swift b/Telegram-Mac/TwoStepVerificationUnlockController.swift index cb7eecea70..a6f0a4862c 100644 --- a/Telegram-Mac/TwoStepVerificationUnlockController.swift +++ b/Telegram-Mac/TwoStepVerificationUnlockController.swift @@ -8,485 +8,1482 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac -import TelegramCoreMac -import PostboxMac +import SwiftSignalKit +import TelegramCore -private func twoStepVerificationUnlockSettingsControllerEntries(state: TwoStepVerificationUnlockSettingsControllerState,data: TwoStepVerificationUnlockSettingsControllerData) -> [TwoStepVerificationUnlockSettingsEntry] { - var entries: [TwoStepVerificationUnlockSettingsEntry] = [] +import Postbox + + + + +private struct TwoStepVerificationUnlockSettingsControllerState: Equatable { + let passwordText: String + let checking: Bool + let emailCode: String + let errors:[InputDataIdentifier : InputDataValueError] + let data: TwoStepVerificationUnlockSettingsControllerData + + init(passwordText: String, checking: Bool, emailCode: String, errors: [InputDataIdentifier : InputDataValueError], data: TwoStepVerificationUnlockSettingsControllerData) { + self.passwordText = passwordText + self.checking = checking + self.emailCode = emailCode + self.errors = errors + self.data = data + } + + func withUpdatedError(_ error: InputDataValueError?, for key: InputDataIdentifier) -> TwoStepVerificationUnlockSettingsControllerState { + var errors = self.errors + if let error = error { + errors[key] = error + } else { + errors.removeValue(forKey: key) + } + return TwoStepVerificationUnlockSettingsControllerState(passwordText: self.passwordText, checking: self.checking, emailCode: self.emailCode, errors: errors, data: self.data) + } + + func withUpdatedPasswordText(_ passwordText: String) -> TwoStepVerificationUnlockSettingsControllerState { + return TwoStepVerificationUnlockSettingsControllerState(passwordText: passwordText, checking: self.checking, emailCode: self.emailCode, errors: self.errors, data: self.data) + } + func withUpdatedEmailCode(_ emailCode: String) -> TwoStepVerificationUnlockSettingsControllerState { + return TwoStepVerificationUnlockSettingsControllerState(passwordText: self.passwordText, checking: self.checking, emailCode: emailCode, errors: self.errors, data: self.data) + } + + func withUpdatedChecking(_ checking: Bool) -> TwoStepVerificationUnlockSettingsControllerState { + return TwoStepVerificationUnlockSettingsControllerState(passwordText: self.passwordText, checking: checking, emailCode: self.emailCode, errors: self.errors, data: self.data) + } + + func withUpdatedControllerData(_ data: TwoStepVerificationUnlockSettingsControllerData) -> TwoStepVerificationUnlockSettingsControllerState { + return TwoStepVerificationUnlockSettingsControllerState(passwordText: self.passwordText, checking: self.checking, emailCode: self.emailCode, errors: self.errors, data: data) + } +} + + +enum TwoStepVerificationUnlockSettingsControllerMode { + case access(TwoStepVeriticationAccessConfiguration?) + case manage(password: String, email: String, pendingEmail: TwoStepVerificationPendingEmail?, hasSecureValues: Bool) +} + +private enum TwoStepVerificationUnlockSettingsControllerData : Equatable { + case access(configuration: TwoStepVeriticationAccessConfiguration?) + case manage(password: String, emailSet: Bool, pendingEmail: TwoStepVerificationPendingEmail?, hasSecureValues: Bool) +} + + + +struct PendingEmailState : Equatable { + let password: String? + let email: TwoStepVerificationPendingEmail +} + + +private final class TwoStepVerificationPasswordEntryControllerArguments { + let updateEntryText: (String) -> Void + let next: () -> Void + let skipEmail:() ->Void + init(updateEntryText: @escaping (String) -> Void, next: @escaping () -> Void, skipEmail:@escaping()->Void) { + self.updateEntryText = updateEntryText + self.next = next + self.skipEmail = skipEmail + } +} + + + +enum PasswordEntryStage: Equatable { + case entry(text: String) + case reentry(first: String, text: String) + case hint(password: String, text: String) + case email(password: String, hint: String, text: String, change: Bool) + case code(text: String, codeLength: Int32?, pattern: String) + + func updateCurrentText(_ text: String) -> PasswordEntryStage { + switch self { + case .entry: + return .entry(text: text) + case let .reentry(first, _): + return .reentry(first: first, text: text) + case let .hint(password, _): + return .hint(password: password, text: text) + case let .email(password, hint, _, change): + return .email(password: password, hint: hint, text: text, change: change) + case let .code(_, codeLength, pattern): + return .code(text: text, codeLength: codeLength, pattern: pattern) + } + } + +} + +private struct TwoStepVerificationPasswordEntryControllerState: Equatable { + let stage: PasswordEntryStage + let updating: Bool + let errors: [InputDataIdentifier : InputDataValueError] + init(stage: PasswordEntryStage, updating: Bool, errors: [InputDataIdentifier : InputDataValueError]) { + self.stage = stage + self.updating = updating + self.errors = errors + } + + func withUpdatedError(_ error: InputDataValueError?, for key: InputDataIdentifier) -> TwoStepVerificationPasswordEntryControllerState { + var errors = self.errors + if let error = error { + errors[key] = error + } else { + errors.removeValue(forKey: key) + } + return TwoStepVerificationPasswordEntryControllerState(stage: self.stage, updating: self.updating, errors: errors) + } + + func withUpdatedStage(_ stage: PasswordEntryStage) -> TwoStepVerificationPasswordEntryControllerState { + return TwoStepVerificationPasswordEntryControllerState(stage: stage, updating: self.updating, errors: self.errors) + } + + func withUpdatedUpdating(_ updating: Bool) -> TwoStepVerificationPasswordEntryControllerState { + return TwoStepVerificationPasswordEntryControllerState(stage: self.stage, updating: updating, errors: self.errors) + } +} + + +enum TwoStepVerificationPasswordEntryMode { + case setup + case change(current: String) + case setupEmail(password: String, change: Bool) + case enterCode(codeLength: Int32?, pattern: String) +} + + +enum TwoStepVeriticationAccessConfiguration : Equatable { + case notSet(pendingEmail: PendingEmailState?) + case set(hint: String, hasRecoveryEmail: Bool, hasSecureValues: Bool, pendingResetTimestamp: Int32?) + + init(configuration: TwoStepVerificationConfiguration, password: String?) { + switch configuration { + case let .notSet(pendingEmail): + self = .notSet(pendingEmail: pendingEmail.flatMap({ PendingEmailState(password: password, email: $0) })) + case let .set(hint, hasRecoveryEmail, _, hasSecureValues, pendingResetTimestamp): + self = .set(hint: hint, hasRecoveryEmail: hasRecoveryEmail, hasSecureValues: hasSecureValues, pendingResetTimestamp: pendingResetTimestamp) + } + } +} + +enum SetupTwoStepVerificationStateUpdate { + case noPassword + case awaitingEmailConfirmation(password: String, pattern: String, codeLength: Int32?) + case passwordSet(password: String?, hasRecoveryEmail: Bool, hasSecureValues: Bool) + case emailSet +} + + + + +final class TwoStepVerificationResetControllerArguments { + let updateEntryText: (String) -> Void + let next: () -> Void + let openEmailInaccessible: () -> Void + + init(updateEntryText: @escaping (String) -> Void, next: @escaping () -> Void, openEmailInaccessible: @escaping () -> Void) { + self.updateEntryText = updateEntryText + self.next = next + self.openEmailInaccessible = openEmailInaccessible + } +} + + +struct TwoStepVerificationResetControllerState: Equatable { + let codeText: String + let checking: Bool + + init(codeText: String, checking: Bool) { + self.codeText = codeText + self.checking = checking + } + + + func withUpdatedCodeText(_ codeText: String) -> TwoStepVerificationResetControllerState { + return TwoStepVerificationResetControllerState(codeText: codeText, checking: self.checking) + } + + func withUpdatedChecking(_ checking: Bool) -> TwoStepVerificationResetControllerState { + return TwoStepVerificationResetControllerState(codeText: self.codeText, checking: checking) + } +} + + + +private let _id_input_enter_pwd = InputDataIdentifier("input_password") +private let _id_change_pwd = InputDataIdentifier("change_pwd") +private let _id_remove_pwd = InputDataIdentifier("remove_pwd") +private let _id_setup_email = InputDataIdentifier("setup_email") +private let _id_enter_email_code = InputDataIdentifier("enter_email_code") +private let _id_set_password = InputDataIdentifier("set_password") +private let _id_input_enter_email_code = InputDataIdentifier("_id_input_enter_email_code") + +private func twoStepVerificationUnlockSettingsControllerEntries(state: TwoStepVerificationUnlockSettingsControllerState, forgotPassword:@escaping()->Void, cancelReset:@escaping() -> Void, abort:@escaping()-> Void) -> [InputDataEntry] { + var entries: [InputDataEntry] = [] var sectionId:Int32 = 0 - entries.append(.section(sectionId)) + + entries.append(.sectionId(sectionId, type: .normal)) sectionId += 1 - switch data { + var index: Int32 = 0 + + + switch state.data { case let .access(configuration): if let configuration = configuration { switch configuration { - case let .notSet(pendingEmailPattern): - if pendingEmailPattern.isEmpty { - entries.append(.passwordSetup(sectionId: sectionId, tr(.twoStepAuthSetPassword))) - entries.append(.passwordSetupInfo(sectionId: sectionId, tr(.twoStepAuthSetPasswordHelp))) + case let .notSet(pendingEmail): + if let pendingEmail = pendingEmail { + + entries.append(.input(sectionId: sectionId, index: index, value: .string(state.emailCode), error: state.errors[_id_input_enter_email_code], identifier: _id_input_enter_email_code, mode: .plain, data: InputDataRowData(viewType: .singleItem), placeholder: nil, inputPlaceholder: L10n.twoStepAuthRecoveryCode, filter: {String($0.unicodeScalars.filter { CharacterSet.decimalDigits.contains($0)})}, limit: pendingEmail.email.codeLength ?? 255)) + index += 1 + + entries.append(.desc(sectionId: sectionId, index: index, text: .markdown(L10n.twoStepAuthConfirmationTextNew + "\n\n\(pendingEmail.email.pattern)\n\n[" + L10n.twoStepAuthConfirmationAbort + "]()", linkHandler: { url in + abort() + }), data: InputDataGeneralTextData(detectBold: false, viewType: .textBottomItem))) + index += 1 + + } else { - entries.append(.pendingEmailInfo(sectionId: sectionId, tr(.twoStepAuthConfirmationText) + "\n\n\(pendingEmailPattern)\n\n[" + tr(.twoStepAuthConfirmationAbort) + "]()")) + entries.append(.general(sectionId: sectionId, index: index, value: .string(nil), error: nil, identifier: _id_set_password, data: InputDataGeneralData(name: L10n.twoStepAuthSetPassword, color: theme.colors.text, icon: nil, type: .none, viewType: .singleItem, action: nil))) + index += 1 + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.twoStepAuthSetPasswordHelp), data: InputDataGeneralTextData(viewType: .textBottomItem))) + index += 1 } - case let .set(hint, _, _): - entries.append(.passwordEntry(sectionId: sectionId, tr(.twoStepAuthEnterPasswordPassword), state.passwordText)) - if hint.isEmpty { - entries.append(.passwordEntryInfo(sectionId: sectionId, tr(.twoStepAuthEnterPasswordHelp) + "\n\n[" + tr(.twoStepAuthEnterPasswordForgot) + "](forgot)")) + case let .set(hint, hasRecoveryEmail, _, pendingResetTimestamp): + entries.append(.input(sectionId: sectionId, index: index, value: .string(state.passwordText), error: state.errors[_id_input_enter_pwd], identifier: _id_input_enter_pwd, mode: .secure, data: InputDataRowData(viewType: .singleItem), placeholder: nil, inputPlaceholder: L10n.twoStepAuthEnterPasswordPassword, filter: { $0 }, limit: 255)) + index += 1 + if let timestamp = pendingResetTimestamp { + + if timestamp.isFuture { + entries.append(.desc(sectionId: sectionId, index: index, text: .markdown(L10n.twoStepAuthEnterPasswordHelp + "\n\n" + L10n.twoStepAuthResetPending(autoremoveLocalized(Int(timestamp - Int32(Date().timeIntervalSince1970)))) + "\n[" + L10n.twoStepAuthCancelReset + "](reset)", linkHandler: { link in + confirm(for: mainWindow, header: L10n.twoStepAuthCancelResetConfirm, information: L10n.twoStepAuthCancelResetText, okTitle: L10n.alertYes, cancelTitle: L10n.alertNO, successHandler: { _ in + cancelReset() + }) + }), data: InputDataGeneralTextData(viewType: .textBottomItem))) + } else { + entries.append(.desc(sectionId: sectionId, index: index, text: .markdown(L10n.twoStepAuthEnterPasswordHelp + "\n\n" + "[" + L10n.twoStepAuthReset + "](reset)", linkHandler: { link in + forgotPassword() + }), data: InputDataGeneralTextData(viewType: .textBottomItem))) + } + index += 1 + } else { - entries.append(.passwordEntryInfo(sectionId: sectionId, tr(.twoStepAuthEnterPasswordHint(hint)) + "\n\n" + tr(.twoStepAuthEnterPasswordHelp) + "\n\n[" + tr(.twoStepAuthEnterPasswordForgot) + "](forgot)")) + + let forgot:()->Void = { + if !hasRecoveryEmail { + confirm(for: mainWindow, header: L10n.twoStepAuthErrorHaventEmailResetHeader, information: L10n.twoStepAuthErrorHaventEmail, okTitle: L10n.twoStepAuthErrorHaventEmailReset, successHandler: { _ in + forgotPassword() + }) + } else { + forgotPassword() + } + } + + if hint.isEmpty { + entries.append(.desc(sectionId: sectionId, index: index, text: .markdown(L10n.twoStepAuthEnterPasswordHelp + "\n\n[" + L10n.twoStepAuthEnterPasswordForgot + "](forgot)", linkHandler: { link in + forgot() + }), data: InputDataGeneralTextData(viewType: .textBottomItem))) + } else { + entries.append(.desc(sectionId: sectionId, index: index, text: .markdown(L10n.twoStepAuthEnterPasswordHint(hint) + "\n\n" + L10n.twoStepAuthEnterPasswordHelp + "\n\n[" + L10n.twoStepAuthEnterPasswordForgot + "](forgot)", linkHandler: { link in + forgot() + }), data: InputDataGeneralTextData(viewType: .textBottomItem))) + } + index += 1 + } + + } + } else { + return [.loading] + } + case let .manage(_, emailSet, pendingEmail, _): + + entries.append(.general(sectionId: sectionId, index: index, value: .string(nil), error: nil, identifier: _id_change_pwd, data: InputDataGeneralData(name: L10n.twoStepAuthChangePassword, color: theme.colors.text, icon: nil, type: .none, viewType: .firstItem, action: nil))) + index += 1 + entries.append(.general(sectionId: sectionId, index: index, value: .string(nil), error: nil, identifier: _id_remove_pwd, data: InputDataGeneralData(name: L10n.twoStepAuthRemovePassword, color: theme.colors.text, icon: nil, type: .none, viewType: .innerItem, action: nil))) + index += 1 + entries.append(.general(sectionId: sectionId, index: index, value: .string(nil), error: nil, identifier: _id_setup_email, data: InputDataGeneralData(name: emailSet ? L10n.twoStepAuthChangeEmail : L10n.twoStepAuthSetupEmail, color: theme.colors.text, icon: nil, type: .none, viewType: .lastItem, action: nil))) + index += 1 + + + if let _ = pendingEmail { + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + entries.append(.general(sectionId: sectionId, index: index, value: .string(nil), error: nil, identifier: _id_enter_email_code, data: InputDataGeneralData(name: L10n.twoStepAuthEnterEmailCode, color: theme.colors.text, icon: nil, type: .none, viewType: .singleItem, action: nil))) + index += 1 + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.twoStepAuthEmailSent), data: InputDataGeneralTextData(viewType: .textBottomItem))) + index += 1 + + } else { + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.twoStepAuthGenericHelp), data: InputDataGeneralTextData(viewType: .textBottomItem))) + index += 1 + } + + } + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + return entries +} + + + + +func twoStepVerificationUnlockController(context: AccountContext, mode: TwoStepVerificationUnlockSettingsControllerMode, presentController:@escaping((controller: ViewController, root:Bool, animated: Bool))->Void) -> InputDataController { + + let actionsDisposable = DisposableSet() + + + let checkDisposable = MetaDisposable() + actionsDisposable.add(checkDisposable) + + let setupDisposable = MetaDisposable() + actionsDisposable.add(setupDisposable) + + let setupResultDisposable = MetaDisposable() + actionsDisposable.add(setupResultDisposable) + + + let data: TwoStepVerificationUnlockSettingsControllerData + + switch mode { + case let .access(configuration): + data = .access(configuration: configuration) + case let .manage(password, email, pendingEmail, hasSecureValues): + data = .manage(password: password, emailSet: !email.isEmpty, pendingEmail: pendingEmail, hasSecureValues: hasSecureValues) + } + // + let initialState = TwoStepVerificationUnlockSettingsControllerState(passwordText: "", checking: false, emailCode: "", errors: [:], data: data) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((TwoStepVerificationUnlockSettingsControllerState) -> TwoStepVerificationUnlockSettingsControllerState) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + switch mode { + case .access: + actionsDisposable.add((context.engine.auth.twoStepVerificationConfiguration() |> map { TwoStepVerificationUnlockSettingsControllerData.access(configuration: TwoStepVeriticationAccessConfiguration(configuration: $0, password: nil)) } |> deliverOnMainQueue).start(next: { data in + updateState { + $0.withUpdatedControllerData(data) + } + })) + default: + break + } + + + + let disablePassword: () -> InputDataValidation = { + return .fail(.doSomething { f in + + switch data { + case .access: + break + case let .manage(password, _, _, hasSecureValues): + + var text: String = L10n.twoStepAuthConfirmDisablePassword + if hasSecureValues { + text += "\n\n" + text += L10n.secureIdWarningDataLost + } + + confirm(for: context.window, information: text, successHandler: { result in + var disablePassword = false + updateState { state in + if state.checking { + return state + } else { + disablePassword = true + return state.withUpdatedChecking(true) + } + } + context.hasPassportSettings.set(.single(false)) + + if disablePassword { + let resetPassword = context.engine.auth.updateTwoStepVerificationPassword(currentPassword: password, updatedPassword: .none) |> deliverOnMainQueue + + setupDisposable.set(resetPassword.start(next: { value in + updateState { + $0.withUpdatedChecking(false) + } + context.resetTemporaryPwd() + presentController((controller: twoStepVerificationUnlockController(context: context, mode: .access(.notSet(pendingEmail: nil)), presentController: presentController), root: true, animated: true)) + _ = showModalSuccess(for: context.window, icon: theme.icons.successModalProgress, delay: 1.0).start() + }, error: { error in + alert(for: context.window, info: L10n.unknownError) + })) + } + }) + } + }) + } + + let checkEmailConfirmation: () -> InputDataValidation = { + + return .fail(.doSomething { f in + let data: TwoStepVerificationUnlockSettingsControllerData = stateValue.with { $0.data } + + var pendingEmailData: PendingEmailState? + switch data { + case let .access(configuration): + guard let configuration = configuration else { + return + } + switch configuration { + case let .notSet(pendingEmail): + pendingEmailData = pendingEmail + case .set: + break + } + case let .manage(password, _, pendingEmail, _): + if let pendingEmail = pendingEmail { + pendingEmailData = PendingEmailState(password: password, email: pendingEmail) + } + } + if let pendingEmail = pendingEmailData { + var code: String? + updateState { state in + if !state.checking { + code = state.emailCode + return state.withUpdatedChecking(true) + } + return state + } + if let code = code { + setupDisposable.set((context.engine.auth.confirmTwoStepRecoveryEmail(code: code) + |> deliverOnMainQueue).start(error: { error in + updateState { state in + return state.withUpdatedChecking(false) + } + let text: String + switch error { + case .invalidEmail: + text = L10n.twoStepAuthEmailInvalid + case .invalidCode: + text = L10n.twoStepAuthEmailCodeInvalid + case .expired: + text = L10n.twoStepAuthEmailCodeExpired + case .flood: + text = L10n.twoStepAuthFloodError + case .generic: + text = L10n.unknownError + } + updateState { + $0.withUpdatedError(InputDataValueError(description: text, target: .data), for: _id_input_enter_email_code) + } + f(.fail(.fields([_id_input_enter_email_code:.shake]))) + }, completed: { + switch data { + case .access: + if let password = pendingEmail.password { + presentController((controller: twoStepVerificationUnlockController(context: context, mode: .manage(password: password, email: "", pendingEmail: nil, hasSecureValues: false), presentController: presentController), root: true, animated: true)) + } else { + presentController((controller: twoStepVerificationUnlockController(context: context, mode: .access(.set(hint: "", hasRecoveryEmail: true, hasSecureValues: false, pendingResetTimestamp: nil)), presentController: presentController), root: true, animated: true)) + } + case let .manage(manage): + presentController((controller: twoStepVerificationUnlockController(context: context, mode: .manage(password: manage.password, email: "", pendingEmail: nil, hasSecureValues: manage.hasSecureValues), presentController: presentController), root: true, animated: true)) + } + + updateState { state in + return state.withUpdatedChecking(false).withUpdatedEmailCode("") + } + })) + } + } + + }) + } + + + + let validateAccessPassword:([InputDataIdentifier : InputDataValue]) -> InputDataValidation = { data in + var wasChecking: Bool = false + updateState { state in + wasChecking = state.checking + return state + } + + updateState { state in + return state.withUpdatedChecking(!wasChecking) + } + + if !wasChecking, let password = data[_id_input_enter_pwd]?.stringValue { + + return .fail(.doSomething(next: { f in + + checkDisposable.set((context.engine.auth.requestTwoStepVerifiationSettings(password: password) + |> mapToSignal { settings -> Signal<(TwoStepVerificationSettings, TwoStepVerificationPendingEmail?), AuthorizationPasswordVerificationError> in + return context.engine.auth.twoStepVerificationConfiguration() + |> mapError { _ -> AuthorizationPasswordVerificationError in + return .generic + } + |> map { configuration in + var pendingEmail: TwoStepVerificationPendingEmail? + if case let .set(configuration) = configuration { + pendingEmail = configuration.pendingEmail + } + return (settings, pendingEmail) + } + } + |> deliverOnMainQueue).start(next: { settings, pendingEmail in + updateState { + $0.withUpdatedChecking(false) + } + presentController((controller: twoStepVerificationUnlockController(context: context, mode: .manage(password: password, email: settings.email, pendingEmail: pendingEmail, hasSecureValues: settings.secureSecret != nil), presentController: presentController), root: true, animated: true)) + f(.none) + }, error: { error in + let text: String + switch error { + case .limitExceeded: + text = L10n.twoStepAuthErrorLimitExceeded + case .invalidPassword: + text = L10n.twoStepAuthInvalidPasswordError + case .generic: + text = L10n.twoStepAuthErrorGeneric + } + updateState { + $0.withUpdatedChecking(false).withUpdatedError(InputDataValueError(description: text, target: .data), for: _id_input_enter_pwd) + } + + f(.fail(.fields([_id_input_enter_pwd : .shake]))) + + })) + + })) + + + } else { + checkDisposable.set(nil) + } + + return .none + } + + let proccessEntryResult:(SetupTwoStepVerificationStateUpdate) -> Void = { update in + switch update { + case .noPassword: + presentController((controller: twoStepVerificationUnlockController(context: context, mode: .access(.notSet(pendingEmail: nil)), presentController: presentController), root: true, animated: true)) + case let .awaitingEmailConfirmation(password, pattern, codeLength): + + let data = stateValue.with {$0.data} + + let hasSecureValues: Bool + + switch data { + case let .manage(_, _, _, _hasSecureValues): + hasSecureValues = _hasSecureValues + case .access: + hasSecureValues = false + } + + + + let pendingEmail = TwoStepVerificationPendingEmail(pattern: pattern, codeLength: codeLength) + + let root = twoStepVerificationUnlockController(context: context, mode: .manage(password: password, email: "", pendingEmail: pendingEmail, hasSecureValues: hasSecureValues), presentController: presentController) + + presentController((controller: root, root: true, animated: false)) + + presentController((controller: twoStepVerificationPasswordEntryController(context: context, mode: .enterCode(codeLength: pendingEmail.codeLength, pattern: pendingEmail.pattern), initialStage: nil, result: { _ in + presentController((controller: twoStepVerificationUnlockController(context: context, mode: .manage(password: password, email: "email", pendingEmail: nil, hasSecureValues: hasSecureValues), presentController: presentController), root: true, animated: true)) + _ = showModalSuccess(for: context.window, icon: theme.icons.successModalProgress, delay: 1.0).start() + }, presentController: presentController), root: false, animated: true)) + + + case .emailSet: + let data = stateValue.with {$0.data} + + switch data { + case let .manage(password, _, _, hasSecureValues): + presentController((controller: twoStepVerificationUnlockController(context: context, mode: .manage(password: password, email: "email", pendingEmail: nil, hasSecureValues: hasSecureValues), presentController: presentController), root: true, animated: true)) + _ = showModalSuccess(for: context.window, icon: theme.icons.successModalProgress, delay: 1.0).start() + default: + break + } + case let .passwordSet(password, hasRecoveryEmail, hasSecureValues): + if let password = password { + presentController((controller: twoStepVerificationUnlockController(context: context, mode: .manage(password: password, email: hasRecoveryEmail ? "email" : "", pendingEmail: nil, hasSecureValues: hasSecureValues), presentController: presentController), root: true, animated: true)) + _ = showModalSuccess(for: context.window, icon: theme.icons.successModalProgress, delay: 1.0).start() + } else { + presentController((controller: twoStepVerificationUnlockController(context: context, mode: .access(.set(hint: "", hasRecoveryEmail: hasRecoveryEmail, hasSecureValues: hasSecureValues, pendingResetTimestamp: nil)), presentController: presentController), root: true, animated: true)) + } + } + } + + let setupPassword:() -> InputDataValidation = { + let controller = twoStepVerificationPasswordEntryController(context: context, mode: .setup, initialStage: nil, result: proccessEntryResult, presentController: presentController) + presentController((controller: controller, root: false, animated: true)) + return .none + } + + let changePassword: (_ current: String) -> InputDataValidation = { current in + let controller = twoStepVerificationPasswordEntryController(context: context, mode: .change(current: current), initialStage: nil, result: proccessEntryResult, presentController: presentController) + presentController((controller: controller, root: false, animated: true)) + return .none + } + + let setupRecoveryEmail:() -> InputDataValidation = { + + let data = stateValue.with {$0.data} + + switch data { + case .access: + break + case let .manage(password, emailSet, _, _): + let controller = twoStepVerificationPasswordEntryController(context: context, mode: .setupEmail(password: password, change: emailSet), initialStage: nil, result: proccessEntryResult, presentController: presentController) + presentController((controller: controller, root: false, animated: true)) + } + + return .none + } + + let enterCode:() -> InputDataValidation = { + let data = stateValue.with {$0.data} + + switch data { + case .access: + break + case let .manage(_, _, pendingEmail, _): + if let pendingEmail = pendingEmail { + let controller = twoStepVerificationPasswordEntryController(context: context, mode: .enterCode(codeLength: pendingEmail.codeLength, pattern: pendingEmail.pattern), initialStage: nil, result: proccessEntryResult, presentController: presentController) + presentController((controller: controller, root: false, animated: true)) + } + } + + return .none + } + + let cancelReset: () -> Void = { + let _ = (context.engine.auth.declineTwoStepPasswordReset() + |> deliverOnMainQueue).start(completed: { + + _ = showModalProgress(signal: context.engine.auth.twoStepVerificationConfiguration(), for: context.window).start(next: { configuration in + updateState { + $0.withUpdatedControllerData(.access(configuration: .init(configuration: configuration, password: nil))) + } + }) + + }) + } + + + let forgotPassword:() -> Void = { + + let data = stateValue.with {$0.data} + switch data { + case let .access(configuration): + if let configuration = configuration { + switch configuration { + case let .set(hint, hasRecoveryEmail, hasSecureValues, _): + if hasRecoveryEmail { + updateState { state in + return state.withUpdatedChecking(true) + } + + setupResultDisposable.set((context.engine.auth.requestTwoStepVerificationPasswordRecoveryCode() + |> deliverOnMainQueue).start(next: { emailPattern in + + updateState { state in + return state.withUpdatedChecking(false) + } + + presentController((controller: twoStepVerificationResetPasswordController(context: context, emailPattern: emailPattern, success: { + presentController((controller: twoStepVerificationUnlockController(context: context, mode: .access(.notSet(pendingEmail: nil)), presentController: presentController), root: true, animated: true)) + }), root: false, animated: true)) + + }, error: { _ in + updateState { state in + return state.withUpdatedChecking(false) + } + alert(for: context.window, info: L10n.twoStepAuthAnError) + })) + } else { + + let reset:()->Void = { + _ = showModalProgress(signal: context.engine.auth.requestTwoStepPasswordReset(), for: context.window).start(next: { result in + switch result { + case .done: + updateState { + $0.withUpdatedControllerData(.access(configuration: .notSet(pendingEmail: nil))) + } + confirm(for: context.window, header: L10n.twoStepAuthResetSuccessHeader, information: L10n.twoStepAuthResetSuccess, okTitle: L10n.alertYes, cancelTitle: L10n.alertNO, successHandler: { _ in + let controller = twoStepVerificationPasswordEntryController(context: context, mode: .setup, initialStage: nil, result: proccessEntryResult, presentController: presentController) + presentController((controller: controller, root: true, animated: true)) + }) + case let .error(reason): + switch reason { + case let .limitExceeded(retryin): + if let retryin = retryin { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .medium + formatter.timeZone = NSTimeZone.local + alert(for: context.window, info: L10n.twoStepAuthUnableToReset(formatter.string(from: Date.init(timeIntervalSince1970: TimeInterval(retryin))))) + } else { + alert(for: context.window, info: L10n.errorAnError) + } + default: + alert(for: context.window, info: L10n.errorAnError) + } + case .declined: + break + case let .waitingForReset(resetAtTimestamp): + updateState { + $0.withUpdatedControllerData(.access(configuration: .set(hint: hint, hasRecoveryEmail: hasRecoveryEmail, hasSecureValues: hasSecureValues, pendingResetTimestamp: resetAtTimestamp))) + } + } + }) + } + + reset() + + } + + default: + break } + } + case .manage: + break } - case let .manage(_, emailSet, pendingEmailPattern): - entries.append(.changePassword(sectionId: sectionId, tr(.twoStepAuthChangePassword))) - entries.append(.turnPasswordOff(sectionId: sectionId, tr(.twoStepAuthRemovePassword))) - entries.append(.setupRecoveryEmail(sectionId: sectionId, emailSet ? tr(.twoStepAuthChangeEmail) : tr(.twoStepAuthSetupEmail))) - if pendingEmailPattern.isEmpty { - entries.append(.passwordInfo(sectionId: sectionId, tr(.twoStepAuthGenericHelp))) + + } + + let abort: () -> Void = { + updateState { $0.withUpdatedChecking(true) } + let resetPassword = context.engine.auth.updateTwoStepVerificationPassword(currentPassword: nil, updatedPassword: .none) |> deliverOnMainQueue + + setupDisposable.set(resetPassword.start(next: { value in + updateState { $0.withUpdatedChecking(false) } + presentController((controller: twoStepVerificationUnlockController(context: context, mode: .access(.notSet(pendingEmail: nil)), presentController: presentController), root: true, animated: true)) + }, error: { error in + alert(for: context.window, info: L10n.unknownError) + })) + } + + let _repeat:Signal = (.single(Void()) |> then(.single(Void()) |> suspendAwareDelay(1, queue: Queue.concurrentDefaultQueue()))) |> restart + + + let signal: Signal<[InputDataEntry], NoError> = combineLatest(statePromise.get(), _repeat) |> map { state, _ -> [InputDataEntry] in + return twoStepVerificationUnlockSettingsControllerEntries(state: state, forgotPassword: forgotPassword, cancelReset: cancelReset, abort: abort) + } + + + return InputDataController(dataSignal: signal |> map { InputDataSignalValue(entries: $0) }, title: L10n.privacySettingsTwoStepVerification, validateData: { validateData -> InputDataValidation in + + let data = stateValue.with {$0.data} + let loading = stateValue.with {$0.checking} + + if !loading { + switch mode { + case .access: + switch data { + case let .access(configuration): + if let configuration = configuration { + switch configuration { + case let .notSet(pendingEmail): + if let _ = pendingEmail { + return checkEmailConfirmation() + } else { + return setupPassword() + } + case .set: + return validateAccessPassword(validateData) + } + } + case .manage: + break + } + case let .manage(password, _, _, _): + if let _ = validateData[_id_remove_pwd] { + return disablePassword() + } else if let _ = validateData[_id_change_pwd] { + return changePassword(password) + } else if let _ = validateData[_id_setup_email] { + return setupRecoveryEmail() + } else if let _ = validateData[_id_enter_email_code] { + return enterCode() + } + + } } else { - entries.append(.passwordInfo(sectionId: sectionId, tr(.twoStepAuthPendingEmailHelp(pendingEmailPattern)))) + NSSound.beep() } + + return .none + + }, updateDatas: { data in + if let password = data[_id_input_enter_pwd]?.stringValue { + updateState { state in + return state.withUpdatedPasswordText(password).withUpdatedError(nil, for: _id_input_enter_pwd) + } + } else if let code = data[_id_input_enter_email_code]?.stringValue { + updateState { state in + return state.withUpdatedEmailCode(code).withUpdatedError(nil, for: _id_input_enter_email_code) + } + } + return .none + }, afterDisappear: { + actionsDisposable.dispose() + }, updateDoneValue: { data in + return { f in + + let data = stateValue.with {$0.data} + + switch mode { + case .access: + switch data { + case let .access(configuration: configuration): + if let configuration = configuration { + switch configuration { + case let .notSet(pendingEmail): + if let _ = pendingEmail { + var checking: Bool = false + var codeEmpty: Bool = true + updateState { state in + checking = state.checking + codeEmpty = state.emailCode.isEmpty + return state + } + return f(checking ? .loading : codeEmpty ? .disabled(L10n.navigationDone) : .enabled(L10n.navigationDone)) + } else { + + } + case .set: + var checking: Bool = false + var pwdEmpty: Bool = true + updateState { state in + checking = state.checking + pwdEmpty = state.passwordText.isEmpty + return state + } + return f(checking ? .loading : pwdEmpty ? .disabled(L10n.navigationDone) : .enabled(L10n.navigationDone)) + } + } else { + return f(.invisible) + } + case .manage: + break + } + + default: + break + } + + var checking: Bool = false + updateState { state in + checking = state.checking + return state + } + return f(checking ? .loading : .invisible) + + } + }, removeAfterDisappear: false, hasDone: true, identifier: "tsv-unlock") +} + + + + + +private struct TwoStepVerificationResetState : Equatable { + let code: String + let checking: Bool + let emailPattern: String + let errors: [InputDataIdentifier : InputDataValueError] + init(emailPattern: String, code: String, checking: Bool, errors: [InputDataIdentifier : InputDataValueError] = [:]) { + self.code = code + self.checking = checking + self.emailPattern = emailPattern + self.errors = errors + } + + func withUpdatedCode(_ code: String) -> TwoStepVerificationResetState { + return TwoStepVerificationResetState(emailPattern: self.emailPattern, code: code, checking: self.checking, errors: self.errors) + } + func withUpdatedChecking(_ checking: Bool) -> TwoStepVerificationResetState { + return TwoStepVerificationResetState(emailPattern: self.emailPattern, code: self.code, checking: checking, errors: self.errors) + } + func withUpdatedError(_ error: InputDataValueError?, for key: InputDataIdentifier) -> TwoStepVerificationResetState { + var errors = self.errors + if let error = error { + errors[key] = error + } else { + errors.removeValue(forKey: key) + } + return TwoStepVerificationResetState(emailPattern: self.emailPattern, code: self.code, checking: checking, errors: errors) } +} + +private let _id_input_recovery_code = InputDataIdentifier("_id_input_recovery_code") + +private func twoStepVerificationResetPasswordEntries( state: TwoStepVerificationResetState, unavailable: @escaping()-> Void) -> [InputDataEntry] { + + var entries: [InputDataEntry] = [] + + var sectionId: Int32 = 0 + var index: Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + + entries.append(.input(sectionId: sectionId, index: index, value: .string(state.code), error: state.errors[_id_input_recovery_code], identifier: _id_input_recovery_code, mode: .plain, data: InputDataRowData(viewType: .singleItem), placeholder: nil, inputPlaceholder: L10n.twoStepAuthRecoveryCode, filter: {String($0.unicodeScalars.filter { CharacterSet.decimalDigits.contains($0)})}, limit: 255)) + index += 1 + + let info = L10n.twoStepAuthRecoveryCodeHelp + "\n\n\(L10n.twoStepAuthRecoveryEmailUnavailableNew(state.emailPattern))" + + entries.append(.desc(sectionId: sectionId, index: index, text: .markdown(info, linkHandler: { _ in + unavailable() + }), data: InputDataGeneralTextData(detectBold: false, viewType: .textBottomItem))) + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 return entries } -fileprivate func prepareTransition(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], initialSize:NSSize, arguments:TwoStepVerificationUnlockSettingsControllerArguments) -> TableUpdateTransition { + + +private func twoStepVerificationResetPasswordController(context: AccountContext, emailPattern: String, success: @escaping()->Void) -> InputDataController { + + + + let initialState = TwoStepVerificationResetState(emailPattern: emailPattern, code: "", checking: false) - let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in - return entry.entry.item(arguments, initialSize: initialSize) + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((TwoStepVerificationResetState) -> TwoStepVerificationResetState) -> Void = { f in + statePromise.set(stateValue.modify(f)) + } + + let resetDisposable = MetaDisposable() + + let signal: Signal<[InputDataEntry], NoError> = statePromise.get() |> map { state in + return twoStepVerificationResetPasswordEntries(state: state, unavailable: { + alert(for: context.window, info: L10n.twoStepAuthRecoveryFailed) + }) + } + + let checkRecoveryCode: (String) -> InputDataValidation = { code in + return .fail(.doSomething { f in + + updateState { + return $0.withUpdatedChecking(true) + } + + + resetDisposable.set((context.engine.auth.checkPasswordRecoveryCode(code: code) |> deliverOnMainQueue).start(error: { error in + + let errorText: String + switch error { + case .generic: + errorText = L10n.twoStepAuthGenericError + case .invalidCode: + errorText = L10n.twoStepAuthRecoveryCodeInvalid + case .expired: + errorText = L10n.twoStepAuthRecoveryCodeExpired + case .limitExceeded: + errorText = L10n.twoStepAuthFloodError + } + + updateState { + return $0.withUpdatedError(InputDataValueError(description: errorText, target: .data), for: _id_input_recovery_code).withUpdatedChecking(false) + } + + f(.fail(.fields([_id_input_recovery_code: .shake]))) + + }, completed: { + updateState { + return $0.withUpdatedChecking(false) + } + success() + })) + }) } - return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true) + return InputDataController(dataSignal: signal |> map { InputDataSignalValue(entries: $0) }, title: L10n.twoStepAuthRecoveryTitle, validateData: { data in + + let code = stateValue.with {$0.code} + let loading = stateValue.with {$0.checking} + + if !loading { + return checkRecoveryCode(code) + } else { + NSSound.beep() + } + return .none + }, updateDatas: { data in + updateState { current in + return current.withUpdatedCode(data[_id_input_recovery_code]?.stringValue ?? current.code).withUpdatedError(nil, for: _id_input_recovery_code) + } + return .none + }, afterDisappear: { + resetDisposable.dispose() + }, updateDoneValue: { data in + return { f in + let code = stateValue.with {$0.code} + let loading = stateValue.with {$0.checking} + f(loading ? .loading : code.isEmpty ? .disabled(L10n.navigationDone) : .enabled(L10n.navigationDone)) + } + }, removeAfterDisappear: true, hasDone: true, identifier: "tsv-reset") } -class TwoStepVerificationUnlockController: TableViewController { - private let mode:TwoStepVerificationUnlockSettingsControllerMode - private var invokeNextAction:(()->Void)? - private let disposable = MetaDisposable() - private var removeOnDisappear: Bool = false - init(account: Account, mode: TwoStepVerificationUnlockSettingsControllerMode) { - self.mode = mode - super.init(account) - } + + +private let _id_input_entry_pwd = InputDataIdentifier("_id_input_entry_pwd") +private let _id_input_reentry_pwd = InputDataIdentifier("_id_input_reentry_pwd") +private let _id_input_entry_hint = InputDataIdentifier("_id_input_entry_hint") +private let _id_input_entry_email = InputDataIdentifier("_id_input_entry_email") +private let _id_input_entry_code = InputDataIdentifier("_id_input_entry_code") + +private func twoStepVerificationPasswordEntryControllerEntries(state: TwoStepVerificationPasswordEntryControllerState, mode: TwoStepVerificationPasswordEntryMode) -> [InputDataEntry] { + var entries: [InputDataEntry] = [] - override func viewDidLoad() { - super.viewDidLoad() - - let account = self.account - let mode = self.mode - - let initialState = TwoStepVerificationUnlockSettingsControllerState(passwordText: "", checking: false) + var sectionId:Int32 = 0 + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + var index: Int32 = 0 + + + switch state.stage { + case let .entry(text): - let statePromise = ValuePromise(initialState, ignoreRepeated: true) - let stateValue = Atomic(value: initialState) - let updateState: ((TwoStepVerificationUnlockSettingsControllerState) -> TwoStepVerificationUnlockSettingsControllerState) -> Void = { f in - statePromise.set(stateValue.modify { f($0) }) + let placeholder:String + switch mode { + case .change: + placeholder = L10n.twoStepAuthEnterPasswordPassword + default: + placeholder = L10n.twoStepAuthEnterPasswordPassword } - var presentControllerImpl: ((ViewController) -> Void)? + entries.append(.input(sectionId: sectionId, index: index, value: .string(text), error: state.errors[_id_input_entry_pwd], identifier: _id_input_entry_pwd, mode: .secure, data: InputDataRowData(viewType: .singleItem), placeholder: nil, inputPlaceholder: placeholder, filter: { $0 }, limit: 255)) + index += 1 - let actionsDisposable = DisposableSet() - - let checkDisposable = MetaDisposable() - actionsDisposable.add(checkDisposable) + switch mode { + case .setup: + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.twoStepAuthSetupPasswordDesc), data: InputDataGeneralTextData(viewType: .textBottomItem))) + index += 1 + case .change: + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.twoStepAuthChangePasswordDesc), data: InputDataGeneralTextData(viewType: .textBottomItem))) + index += 1 + default: + break + } - let setupDisposable = MetaDisposable() - actionsDisposable.add(setupDisposable) + case let .reentry(_, text): + entries.append(.input(sectionId: sectionId, index: index, value: .string(text), error: state.errors[_id_input_reentry_pwd], identifier: _id_input_reentry_pwd, mode: .secure, data: InputDataRowData(viewType: .singleItem), placeholder: nil, inputPlaceholder: L10n.twoStepAuthEnterPasswordPassword, filter: { $0 }, limit: 255)) + index += 1 + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.twoStepAuthSetupPasswordConfirmPassword), data: InputDataGeneralTextData(viewType: .textBottomItem))) + index += 1 + case let .hint(_, text): + entries.append(.input(sectionId: sectionId, index: index, value: .string(text), error: state.errors[_id_input_entry_hint], identifier: _id_input_entry_hint, mode: .plain, data: InputDataRowData(viewType: .singleItem), placeholder: nil, inputPlaceholder: L10n.twoStepAuthSetupHintPlaceholder, filter: { $0 }, limit: 255)) + index += 1 + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.twoStepAuthSetupHintDesc), data: InputDataGeneralTextData(viewType: .textBottomItem))) + index += 1 + case let .email(_, _, text, change): - let setupResultDisposable = MetaDisposable() - actionsDisposable.add(setupResultDisposable) + entries.append(.input(sectionId: sectionId, index: index, value: .string(text), error: state.errors[_id_input_entry_email], identifier: _id_input_entry_email, mode: .plain, data: InputDataRowData(viewType: .singleItem), placeholder: nil, inputPlaceholder: L10n.twoStepAuthEmail, filter: { $0 }, limit: 255)) + index += 1 - let dataPromise = Promise() + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(change ? L10n.twoStepAuthEmailHelpChange : L10n.twoStepAuthEmailHelp), data: InputDataGeneralTextData(viewType: .textBottomItem))) + case let .code(text, codeLength, pattern): + entries.append(.input(sectionId: sectionId, index: index, value: .string(text), error: state.errors[_id_input_entry_code], identifier: _id_input_entry_code, mode: .plain, data: InputDataRowData(viewType: .singleItem), placeholder: nil, inputPlaceholder: L10n.twoStepAuthRecoveryCode, filter: {String($0.unicodeScalars.filter { CharacterSet.decimalDigits.contains($0)})}, limit: codeLength ?? 255)) + index += 1 + entries.append(.desc(sectionId: sectionId, index: index, text: .plain(L10n.twoStepAuthConfirmEmailCodeDesc(pattern)), data: InputDataGeneralTextData(detectBold: false, viewType: .textBottomItem))) + } + + entries.append(.sectionId(sectionId, type: .normal)) + sectionId += 1 + + return entries +} + + + +func twoStepVerificationPasswordEntryController(context: AccountContext, mode: TwoStepVerificationPasswordEntryMode, initialStage: PasswordEntryStage?, result: @escaping(SetupTwoStepVerificationStateUpdate) -> Void, presentController: @escaping((controller: ViewController, root: Bool, animated: Bool)) -> Void) -> InputDataController { + + + let network = context.account.network + + var initialStage: PasswordEntryStage! = initialStage + if initialStage == nil { switch mode { - case .access: - dataPromise.set(.single(TwoStepVerificationUnlockSettingsControllerData.access(configuration: nil)) |> then(twoStepVerificationConfiguration(account: account) |> map { TwoStepVerificationUnlockSettingsControllerData.access(configuration: $0) })) - case let .manage(password, email, pendingEmailPattern): - dataPromise.set(.single(.manage(password: password, emailSet: !email.isEmpty, pendingEmailPattern: pendingEmailPattern))) + case .setup, .change: + initialStage = .entry(text: "") + case let .setupEmail(password, change): + initialStage = .email(password: password, hint: "", text: "", change: change) + case let .enterCode(codeLength, pattern): + initialStage = .code(text: "", codeLength: codeLength, pattern: pattern) } - - let arguments = TwoStepVerificationUnlockSettingsControllerArguments(updatePasswordText: { updatedText in - updateState { - $0.withUpdatedPasswordText(updatedText) + } + + let initialState = TwoStepVerificationPasswordEntryControllerState(stage: initialStage, updating: false, errors: [:]) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((TwoStepVerificationPasswordEntryControllerState) -> TwoStepVerificationPasswordEntryControllerState) -> Void = { f in + statePromise.set(stateValue.modify { f($0) }) + } + + let signal: Signal<[InputDataEntry], NoError> = statePromise.get() |> map { state in + return twoStepVerificationPasswordEntryControllerEntries(state: state, mode: mode) + } + + + let actionsDisposable = DisposableSet() + + let updatePasswordDisposable = MetaDisposable() + actionsDisposable.add(updatePasswordDisposable) + + + func checkAndSaveState(context:AccountContext) -> InputDataValidation { + var passwordHintEmail: (String, String, String)? + var enterCode: String? + updateState { state in + if state.updating { + return state + } else { + switch state.stage { + case .entry: + break + case .reentry: + break + case let .hint(password, text): + switch mode { + case .change: + passwordHintEmail = (password, text, "") + default: + preconditionFailure() + } + case let .email(password, hint, text, _): + passwordHintEmail = (password, hint, text) + case let .code(text, _, _): + enterCode = text + } } - }, openForgotPassword: { - setupDisposable.set((dataPromise.get() |> take(1) |> deliverOnMainQueue).start(next: { data in - switch data { - case let .access(configuration): - if let configuration = configuration { - switch configuration { - case let .set(_, hasRecoveryEmail, _): - if hasRecoveryEmail { - updateState { - $0.withUpdatedChecking(true) - } - setupResultDisposable.set((requestTwoStepVerificationPasswordRecoveryCode(account: account) |> deliverOnMainQueue).start(next: { emailPattern in - updateState { - $0.withUpdatedChecking(false) - } - let result = Promise() - let controller = TwoStepVerificationResetController(account: account, emailPattern: emailPattern, result: result) - presentControllerImpl?(controller) - - setupDisposable.set((result.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak controller] _ in - dataPromise.set(.single(TwoStepVerificationUnlockSettingsControllerData.access(configuration: TwoStepVerificationConfiguration.notSet(pendingEmailPattern: "")))) - controller?.dismiss() - })) - }, error: { _ in - updateState { - $0.withUpdatedChecking(false) - } - alert(for: mainWindow, info: tr(.twoStepAuthAnError)) - })) + return state + } + + + return .fail(.doSomething { f in + if let (password, hint, email) = passwordHintEmail { + + updateState { + $0.withUpdatedUpdating(true) + } + + switch mode { + case .setup, .change: + var currentPassword: String? + if case let .change(current) = mode { + currentPassword = current + } + + updatePasswordDisposable.set((context.engine.auth.updateTwoStepVerificationPassword(currentPassword: currentPassword, updatedPassword: .password(password: password, hint: hint, email: email)) |> deliverOnMainQueue).start(next: { update in + updateState { + $0.withUpdatedUpdating(false) + } + switch update { + case let .password(password, pendingEmail): + if let pendingEmail = pendingEmail { + result(.awaitingEmailConfirmation(password: password, pattern: email, codeLength: pendingEmail.codeLength)) } else { - alert(for: mainWindow, info: tr(.twoStepAuthErrorHaventEmail)) + result(.passwordSet(password: password, hasRecoveryEmail: false, hasSecureValues: false)) } - case .notSet: + case .none: break } - } - case .manage: - break - } - })) - }, openSetupPassword: { - setupDisposable.set((dataPromise.get() |> take(1) |> deliverOnMainQueue).start(next: { data in - switch data { - case let .access(configuration): - if let configuration = configuration { - switch configuration { - case .notSet: - let result = Promise() - let controller = TwoStepVerificationPasswordEntryController(account: account, mode: .setup, result: result) - presentControllerImpl?(controller) - setupResultDisposable.set((result.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak controller] updatedPassword in - if let updatedPassword = updatedPassword { - if let pendingEmailPattern = updatedPassword.pendingEmailPattern { - dataPromise.set(.single(TwoStepVerificationUnlockSettingsControllerData.access(configuration: TwoStepVerificationConfiguration.notSet(pendingEmailPattern: pendingEmailPattern)))) - } else { - dataPromise.set(.single(TwoStepVerificationUnlockSettingsControllerData.manage(password: updatedPassword.password, emailSet: false, pendingEmailPattern: ""))) - } - controller?.dismiss() - } - })) - case .set: - break + }, error: { error in + updateState { + $0.withUpdatedUpdating(false) } - } - case let .manage(password, emailSet, pendingEmailPattern): - let result = Promise() - let controller = TwoStepVerificationPasswordEntryController(account: account, mode: .change(current: password), result: result) - presentControllerImpl?(controller) - setupResultDisposable.set((result.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak controller] updatedPassword in - if let updatedPassword = updatedPassword { - dataPromise.set(.single(TwoStepVerificationUnlockSettingsControllerData.manage(password: updatedPassword.password, emailSet: emailSet, pendingEmailPattern: pendingEmailPattern))) - controller?.dismiss() + switch error { + case .generic: + alert(for: context.window, info: L10n.twoStepAuthErrorGeneric) + case .invalidEmail: + updateState { + $0.withUpdatedError(InputDataValueError(description: L10n.twoStepAuthErrorInvalidEmail, target: .data), for: _id_input_entry_email) + } + f(.fail(.fields([_id_input_entry_email: .shake]))) } + })) - } - })) - }, openDisablePassword: { - - confirm(for: mainWindow, with: appName, and: tr(.twoStepAuthConfirmDisablePassword), successHandler: { _ in - var disablePassword = false - updateState { state in - if state.checking { - return state - } else { - disablePassword = true - return state.withUpdatedChecking(true) - } - } - if disablePassword { - setupDisposable.set((dataPromise.get() - |> take(1) - |> mapError { _ -> UpdateTwoStepVerificationPasswordError in return .generic } - |> mapToSignal { data -> Signal in - switch data { - case .access: - return .complete() - case let .manage(password, _, _): - return updateTwoStepVerificationPassword(account: account, currentPassword: password, updatedPassword: .none) - |> mapToSignal { _ -> Signal in - return .complete() - } - } + case let .setupEmail(password, _): + updatePasswordDisposable.set((context.engine.auth.updateTwoStepVerificationEmail(currentPassword: password, updatedEmail: email) |> deliverOnMainQueue).start(next: { update in + updateState { + $0.withUpdatedUpdating(false) } - |> deliverOnMainQueue).start(error: { _ in - updateState { - $0.withUpdatedChecking(false) - } - }, completed: { - updateState { - $0.withUpdatedChecking(false) + switch update { + case let .password(password, pendingEmail): + if let pendingEmail = pendingEmail { + result(.awaitingEmailConfirmation(password: password, pattern: email, codeLength: pendingEmail.codeLength)) + } else { + result(.passwordSet(password: password, hasRecoveryEmail: true, hasSecureValues: false)) } - dataPromise.set(.single(TwoStepVerificationUnlockSettingsControllerData.access(configuration: .notSet(pendingEmailPattern: "")))) - })) - } - }) - - }, openSetupEmail: { - setupDisposable.set((dataPromise.get() |> take(1) |> deliverOnMainQueue).start(next: { data in - switch data { - case .access: - break - case let .manage(password, _, _): - let result = Promise() - let controller = TwoStepVerificationPasswordEntryController(account: account, mode: .setupEmail(password: password), result: result) - presentControllerImpl?(controller) - setupResultDisposable.set((result.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak controller] updatedPassword in - if let updatedPassword = updatedPassword { - dataPromise.set(.single(TwoStepVerificationUnlockSettingsControllerData.manage(password: updatedPassword.password, emailSet: true, pendingEmailPattern: updatedPassword.pendingEmailPattern ?? ""))) - controller?.dismiss() + case .none: + break + } + }, error: { error in + updateState { + $0.withUpdatedUpdating(false) } + let errorText: String + switch error { + case .generic: + errorText = L10n.twoStepAuthErrorGeneric + case .invalidEmail: + errorText = L10n.twoStepAuthErrorInvalidEmail + } + updateState { + $0.withUpdatedError(InputDataValueError(description: errorText, target: .data), for: _id_input_entry_email) + } + f(.fail(.fields([_id_input_entry_email: .shake]))) })) + case .enterCode: + fatalError() } - })) - }, openResetPendingEmail: { - updateState { state in - return state.withUpdatedChecking(true) - } - setupDisposable.set((updateTwoStepVerificationPassword(account: account, currentPassword: nil, updatedPassword: .none) |> deliverOnMainQueue).start(next: { _ in - updateState { state in - return state.withUpdatedChecking(false) - } - dataPromise.set(.single(TwoStepVerificationUnlockSettingsControllerData.access(configuration: .notSet(pendingEmailPattern: "")))) - }, error: { _ in - updateState { state in - return state.withUpdatedChecking(false) + } else if let code = enterCode { + updateState { + $0.withUpdatedUpdating(true) } - })) + updatePasswordDisposable.set((context.engine.auth.confirmTwoStepRecoveryEmail(code: code) |> deliverOnMainQueue).start(error: { error in + updateState { + $0.withUpdatedUpdating(false) + } + let errorText: String + switch error { + case .generic: + errorText = L10n.twoStepAuthGenericError + case .invalidCode: + errorText = L10n.twoStepAuthRecoveryCodeInvalid + case .expired: + errorText = L10n.twoStepAuthRecoveryCodeExpired + case .flood: + errorText = L10n.twoStepAuthFloodError + case .invalidEmail: + errorText = L10n.twoStepAuthErrorInvalidEmail + } + updateState { + $0.withUpdatedError(InputDataValueError(description: errorText, target: .data), for: _id_input_entry_code) + } + f(.fail(.fields([_id_input_entry_code: .shake]))) + + }, completed: { + updateState { + $0.withUpdatedUpdating(false) + } + result(.emailSet) + })) + } }) - let previous: Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) - let initialSize = self.atomicSize - var nextAction:(()->Void)? = nil + } + + + return InputDataController(dataSignal: signal |> map { InputDataSignalValue(entries: $0) }, title: "", validateData: { data -> InputDataValidation in + + var stage: PasswordEntryStage? + var allowPerform: Bool = true - let shake:()->Void = { [weak self] in - (self?.firstResponder() as? NSTextView)?.shake() - (self?.firstResponder() as? NSTextView)?.selectAll(nil) - NSSound.beep() - } + let loading = stateValue.with {$0.updating} - let signal = combineLatest(appearanceSignal, statePromise.get(), dataPromise.get() |> deliverOnMainQueue) - |> map { appearance, state, data -> (TableUpdateTransition, String, TwoStepVerificationUnlockSettingsControllerData) in - - - let entries = twoStepVerificationUnlockSettingsControllerEntries(state: state, data: data).map{AppearanceWrapperEntry(entry: $0, appearance: appearance)} - - var title: String = tr(.twoStepAuthPasswordTitle) - switch data { - case let .access(configuration): - if let configuration = configuration { - if state.checking { - nextAction = nil - } else { - switch configuration { - case .notSet: - title = tr(.telegramTwoStepVerificationUnlockController) - case let .set(_, _, pendingEmailPattern): - title = tr(.twoStepAuthPasswordTitle) - nextAction = { - - var wasChecking = false - var password: String? - updateState { state in - wasChecking = state.checking - password = state.passwordText - return state.withUpdatedChecking(true) - } + if !loading { + return .fail(.doSomething { f in + var skipEmail: Bool = false + updateState { state in + var state = state + if state.updating { + return state + } else { + switch state.stage { + case let .entry(text): + if text.isEmpty { + return state + } else { + stage = .reentry(first: text, text: "") + } + case let .reentry(first, text): + if text.isEmpty { - if let password = password, !wasChecking { - checkDisposable.set((requestTwoStepVerifiationSettings(account: account, password: password) |> deliverOnMainQueue).start(next: { settings in - updateState { - $0.withUpdatedChecking(false) - } - presentControllerImpl?(TwoStepVerificationUnlockController(account: account, mode: .manage(password: password, email: settings.email, pendingEmailPattern: pendingEmailPattern))) - }, error: { error in - updateState { - $0.withUpdatedChecking(false) - } - - switch error { - case .limitExceeded: - alert(for: mainWindow, info: tr(.twoStepAuthErrorLimitExceeded)) - case .invalidPassword: - shake() - //text = tr(.twoStepAuthErrorInvalidPassword) - case .generic: - alert(for: mainWindow, info: tr(.twoStepAuthErrorGeneric)) - } - - })) - } - } + } else if text != first { + state = state.withUpdatedError(InputDataValueError(description: L10n.twoStepAuthSetupPasswordConfirmFailed, target: .data), for: _id_input_reentry_pwd) + f(.fail(.fields([_id_input_reentry_pwd : .shake]))) + } else { + stage = .hint(password: text, text: "") + } + case let .hint(password, text): + switch mode { + case .setup: + stage = .email(password: password, hint: text, text: "", change: false) + default: + break + } + case let .email(_, _, text, _): + if text.isEmpty { + skipEmail = true + } + case let .code(text, codeLength, _): + if text.isEmpty { + allowPerform = false + } else if let codeLength = codeLength, text.length != codeLength { + allowPerform = false + } else { + allowPerform = true } } + return state } - case .manage: - title = tr(.telegramTwoStepVerificationUnlockController) - if state.checking { - nextAction = nil + } + if allowPerform { + if let stage = stage { + presentController((controller: twoStepVerificationPasswordEntryController(context: context, mode: mode, initialStage: stage, result: result, presentController: presentController), root: false, animated: true)) + } else { + if skipEmail { + confirm(for: context.window, information: L10n.twoStepAuthEmailSkipAlert, okTitle: L10n.twoStepAuthEmailSkip, successHandler: { _ in + f(checkAndSaveState(context: context)) + }) + } else { + f(checkAndSaveState(context: context)) + } } } - - return (prepareTransition(left: previous.swap(entries), right: entries, initialSize: initialSize.modify{$0}, arguments: arguments), title, data) - } |> afterDisposed { - actionsDisposable.dispose() - } |> deliverOnMainQueue + }) + } else { + NSSound.beep() + return .none + } + }, updateDatas: { data -> InputDataValidation in - self.invokeNextAction = { - nextAction?() + let previousCode: String? + switch stateValue.with ({ $0.stage }) { + case let .code(text, _, _): + previousCode = text + default: + previousCode = nil } - disposable.set(signal.start(next: { [weak self] transition, title, data in - self?.genericView.merge(with: transition) - - switch mode { - case .access: - switch data { - case let .access(configuration): - self?.removeOnDisappear = false - if let configuration = configuration { - switch configuration { - case .notSet: - self?.rightBarView.isHidden = true - self?.removeOnDisappear = false - case .set: - self?.rightBarView.isHidden = false - self?.removeOnDisappear = true + updateState { state in + switch state.stage { + case let .entry(text): + return state.withUpdatedStage(.entry(text: data[_id_input_entry_pwd]?.stringValue ?? text)) + case let .reentry(first, text): + return state.withUpdatedStage(.reentry(first: first, text: data[_id_input_reentry_pwd]?.stringValue ?? text)).withUpdatedError(nil, for: _id_input_reentry_pwd) + case let .hint(password, text): + return state.withUpdatedStage(.hint(password: password, text: data[_id_input_entry_hint]?.stringValue ?? text)).withUpdatedError(nil, for: _id_input_entry_hint) + case let .email(password, hint, text, change): + return state.withUpdatedStage(.email(password: password, hint: hint, text: data[_id_input_entry_email]?.stringValue ?? text, change: change)).withUpdatedError(nil, for: _id_input_entry_email) + case let .code(text, codeLength, pattern): + return state.withUpdatedStage(.code(text: data[_id_input_entry_code]?.stringValue ?? text, codeLength: codeLength, pattern: pattern)).withUpdatedError(nil, for: _id_input_entry_code) + } + } + + switch stateValue.with ({ $0.stage }) { + case let .code(text, codeLength, _): + if Int32(text.length) == codeLength, previousCode != text { + return checkAndSaveState(context: context) + } + default: + break + } + + return .none + }, afterDisappear: { + actionsDisposable.dispose() + }, updateDoneValue: { data in + return { f in + updateState { state in + + if state.updating { + f(.loading) + } else { + switch state.stage { + case let .entry(text): + if text.isEmpty { + f(.disabled(L10n.navigationNext)) + } else { + f(.enabled(L10n.navigationNext)) + } + case let .reentry(_, text): + if text.isEmpty { + f(.disabled(L10n.navigationNext)) + } else { + f(.enabled(L10n.navigationNext)) + } + case let .hint(_, text): + if text.isEmpty { + f(.enabled(L10n.twoStepAuthEmailSkip)) + } else { + f(.enabled(L10n.navigationNext)) + } + case let .email(_, _, text, _): + switch mode { + case .setupEmail: + f(text.isEmpty ? .disabled(L10n.navigationNext) : .enabled(L10n.navigationNext)) + default: + f(text.isEmpty ? .enabled(L10n.twoStepAuthEmailSkip) : .enabled(L10n.navigationNext)) + } + case let .code(text, codeLength, _): + if let codeLength = codeLength { + f(text.length < codeLength ? .disabled(L10n.navigationNext) : .enabled(L10n.navigationNext)) + } else { + f(text.isEmpty ? .disabled(L10n.navigationNext) : .enabled(L10n.navigationNext)) } - } else { - self?.rightBarView.isHidden = true } - case .manage: - self?.removeOnDisappear = false - self?.rightBarView.isHidden = true } - case .manage: - self?.removeOnDisappear = false - self?.rightBarView.isHidden = true + return state } - - - self?.setCenterTitle(title) - })) - - readyOnce() - - presentControllerImpl = { [weak self] controller in - self?.navigationController?.push(controller) } - } - - override var removeAfterDisapper: Bool { - return removeOnDisappear - } - - override func becomeFirstResponder() -> Bool? { - return true - } - - override func firstResponder() -> NSResponder? { - if genericView.count > 1 { - if !(window?.firstResponder is NSTextView) { - return (genericView.viewNecessary(at: 1) as? GeneralInputRowView)?.firstResponder + }, removeAfterDisappear: false, hasDone: true, identifier: "tsv-entry", afterTransaction: { controller in + var stage: PasswordEntryStage? + updateState { state in + stage = state.stage + return state + } + if let stage = stage { + var title: String = "" + + switch stage { + case .entry: + switch mode { + case .change: + title = L10n.twoStepAuthChangePassword + case .setup: + title = L10n.twoStepAuthSetupPasswordTitle + case .setupEmail: + title = L10n.twoStepAuthSetupPasswordTitle + case .enterCode: + preconditionFailure() + } + + case .reentry: + switch mode { + case .change: + title = L10n.twoStepAuthChangePassword + case .setup: + title = L10n.twoStepAuthSetupPasswordTitle + case .setupEmail: + title = L10n.twoStepAuthSetupPasswordTitle + case .enterCode: + preconditionFailure() + } + case .hint: + title = L10n.twoStepAuthSetupHintTitle + case .email: + title = L10n.twoStepAuthSetupEmailTitle + case .code: + title = L10n.twoStepAuthSetupEmailTitle } + controller.setCenterTitle(title) } - return window?.firstResponder - } - - override func getRightBarViewOnce() -> BarView { - let button = TextButtonBarView(controller: self, text: tr(.composeNext)) - - button.button.set(handler: { [weak self] _ in - self?.invokeNextAction?() - }, for: .Click) - - return button - } - - deinit { - disposable.dispose() - } - - override func returnKeyAction() -> KeyHandlerResult { - invokeNextAction?() - return .invoked - } + }) } - -/* - - var rightNavigationButton: ItemListNavigationButton? - var emptyStateItem: ItemListControllerEmptyStateItem? - let title: String - switch data { - case let .access(configuration): - title = presentationData.strings.TwoStepAuth_Title - if let configuration = configuration { - if state.checking { - rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) - } else { - switch configuration { - case .notSet: - break - case .set: - rightNavigationButton = ItemListNavigationButton(title: presentationData.strings.Common_Next, style: .bold, enabled: true, action: { - var wasChecking = false - var password: String? - updateState { state in - wasChecking = state.checking - password = state.passwordText - return state.withUpdatedChecking(true) - } - - if let password = password, !wasChecking { - checkDisposable.set((requestTwoStepVerifiationSettings(account: account, password: password) |> deliverOnMainQueue).start(next: { settings in - updateState { - $0.withUpdatedChecking(false) - } - - replaceControllerImpl?(twoStepVerificationUnlockSettingsController(account: account, mode: .manage(password: password, email: settings.email, pendingEmailPattern: ""))) - }, error: { error in - updateState { - $0.withUpdatedChecking(false) - } - - let text: String - switch error { - case .limitExceeded: - text = "You have entered invalid password too many times. Please try again later." - case .invalidPassword: - text = "Invalid password. Please try again." - case .generic: - text = "An error occured. Please try again later." - } - - presentControllerImpl?(standardTextAlertController(title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: "OK", action: {})]), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - })) - } - }) - } - } - } else { - emptyStateItem = ItemListLoadingIndicatorEmptyStateItem() - } - case .manage: - title = presentationData.strings.PrivacySettings_TwoStepAuth - if state.checking { - rightNavigationButton = ItemListNavigationButton(title: "", style: .activity, enabled: true, action: {}) - } - } - - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(title), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) - let listState = ItemListNodeState(entries: twoStepVerificationUnlockSettingsControllerEntries(presentationData: presentationData, state: state, data: data), style: .blocks, focusItemTag: TwoStepVerificationUnlockSettingsEntryTag.password, emptyStateItem: emptyStateItem, animateChanges: false) - - return (controllerState, (listState, arguments)) - */ diff --git a/Telegram-Mac/TwoStepVerificationUnlockStructures.swift b/Telegram-Mac/TwoStepVerificationUnlockStructures.swift deleted file mode 100644 index 7df352d454..0000000000 --- a/Telegram-Mac/TwoStepVerificationUnlockStructures.swift +++ /dev/null @@ -1,615 +0,0 @@ -// -// TwoStepVerificationUnlockStructures.swift -// Telegram -// -// Created by keepcoder on 16/10/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - -import Cocoa -import TelegramCoreMac -import TGUIKit - - -final class TwoStepVerificationUnlockSettingsControllerArguments { - let updatePasswordText: (String) -> Void - let openForgotPassword: () -> Void - let openSetupPassword: () -> Void - let openDisablePassword: () -> Void - let openSetupEmail: () -> Void - let openResetPendingEmail: () -> Void - - init(updatePasswordText: @escaping (String) -> Void, openForgotPassword: @escaping () -> Void, openSetupPassword: @escaping () -> Void, openDisablePassword: @escaping () -> Void, openSetupEmail: @escaping () -> Void, openResetPendingEmail: @escaping () -> Void) { - self.updatePasswordText = updatePasswordText - self.openForgotPassword = openForgotPassword - self.openSetupPassword = openSetupPassword - self.openDisablePassword = openDisablePassword - self.openSetupEmail = openSetupEmail - self.openResetPendingEmail = openResetPendingEmail - } -} - -enum TwoStepVerificationUnlockSettingsSection: Int32 { - case password -} - - -enum TwoStepVerificationUnlockSettingsEntry: TableItemListNodeEntry { - case passwordEntry(sectionId: Int32, String, String) - case passwordEntryInfo(sectionId: Int32, String) - - case passwordSetup(sectionId: Int32, String) - case passwordSetupInfo(sectionId: Int32, String) - - case changePassword(sectionId: Int32, String) - case turnPasswordOff(sectionId: Int32, String) - case setupRecoveryEmail(sectionId: Int32, String) - case passwordInfo(sectionId: Int32, String) - - case pendingEmailInfo(sectionId: Int32, String) - case section(Int32) - - - var stableId: Int32 { - switch self { - case .passwordEntry: - return 0 - case .passwordEntryInfo: - return 1 - case .passwordSetup: - return 2 - case .passwordSetupInfo: - return 3 - case .changePassword: - return 4 - case .turnPasswordOff: - return 5 - case .setupRecoveryEmail: - return 6 - case .passwordInfo: - return 7 - case .pendingEmailInfo: - return 8 - case .section(let id): - return (id + 1) * 1000 - id - } - } - - static func ==(lhs: TwoStepVerificationUnlockSettingsEntry, rhs: TwoStepVerificationUnlockSettingsEntry) -> Bool { - switch lhs { - case let .passwordEntry(lhsSection, lhsText, lhsValue): - if case let .passwordEntry(rhsSection, rhsText, rhsValue) = rhs, lhsSection == rhsSection, lhsText == rhsText, lhsValue == rhsValue { - return true - } else { - return false - } - case let .passwordEntryInfo(lhsSection, lhsText): - if case let .passwordEntryInfo(rhsSection, rhsText) = rhs, lhsSection == rhsSection, lhsText == rhsText { - return true - } else { - return false - } - case let .passwordSetupInfo(lhsSection, lhsText): - if case let .passwordSetupInfo(rhsSection, rhsText) = rhs, lhsSection == rhsSection, lhsText == rhsText { - return true - } else { - return false - } - case let .setupRecoveryEmail(lhsSection, lhsText): - if case let .setupRecoveryEmail(rhsSection, rhsText) = rhs, lhsSection == rhsSection, lhsText == rhsText { - return true - } else { - return false - } - case let .passwordInfo(lhsSection, lhsText): - if case let .passwordInfo(rhsSection, rhsText) = rhs, lhsSection == rhsSection, lhsText == rhsText { - return true - } else { - return false - } - case let .pendingEmailInfo(lhsSection, lhsText): - if case let .pendingEmailInfo(rhsSection, rhsText) = rhs, lhsSection == rhsSection, lhsText == rhsText { - return true - } else { - return false - } - case let .passwordSetup(lhsSection, lhsText): - if case let .passwordSetup(rhsSection, rhsText) = rhs, lhsSection == rhsSection, lhsText == rhsText { - return true - } else { - return false - } - case let .changePassword(lhsSection, lhsText): - if case let .changePassword(rhsSection, rhsText) = rhs, lhsSection == rhsSection, lhsText == rhsText { - return true - } else { - return false - } - case let .turnPasswordOff(lhsSection, lhsText): - if case let .turnPasswordOff(rhsSection, rhsText) = rhs, lhsSection == rhsSection, lhsText == rhsText { - return true - } else { - return false - } - case let .section(section): - if case .section(section) = rhs { - return true - } else { - return false - } - } - } - - var index: Int32 { - switch self { - case let .changePassword(sectionId, _): - return (sectionId * 1000) + stableId - case let .passwordEntry(sectionId, _, _): - return (sectionId * 1000) + stableId - case let .passwordEntryInfo(sectionId, _): - return (sectionId * 1000) + stableId - case let .passwordSetup(sectionId, _): - return (sectionId * 1000) + stableId - case let .passwordSetupInfo(sectionId, _): - return (sectionId * 1000) + stableId - case let .turnPasswordOff(sectionId, _): - return (sectionId * 1000) + stableId - case let .setupRecoveryEmail(sectionId, _): - return (sectionId * 1000) + stableId - case let .passwordInfo(sectionId, _): - return (sectionId * 1000) + stableId - case let .pendingEmailInfo(sectionId, _): - return (sectionId * 1000) + stableId - case let .section(id): - return (id + 1) * 1000 - id - } - } - - static func <(lhs: TwoStepVerificationUnlockSettingsEntry, rhs: TwoStepVerificationUnlockSettingsEntry) -> Bool { - return lhs.index < rhs.index - } - - - func item(_ arguments: TwoStepVerificationUnlockSettingsControllerArguments, initialSize: NSSize) -> TableRowItem { - switch self { - case let .passwordEntry(_, text, value): - return GeneralInputRowItem(initialSize, stableId: stableId, placeholder: tr(.twoStepAuthEnterPasswordPassword), text: value, limit: INT32_MAX, textChangeHandler: { updatedText in - arguments.updatePasswordText(updatedText) - }, inputType: .secure) - case let .passwordEntryInfo(_, text): - return GeneralTextRowItem(initialSize, stableId: stableId, text: .markdown(text, linkHandler: { _ in - arguments.openForgotPassword() - }), inset: NSEdgeInsetsMake(5, 28, 5, 28)) - - case let .passwordSetup(_, text): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: text, type: .next, action: { - arguments.openSetupPassword() - }) - case let .passwordSetupInfo(_, text): - return GeneralTextRowItem(initialSize, stableId: stableId, text: .markdown(text, linkHandler: { _ in }), inset: NSEdgeInsetsMake(5, 28, 5, 28)) - case let .changePassword(_, text): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: text, type: .next, action: { - arguments.openSetupPassword() - }) - case let .turnPasswordOff(_, text): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: text, type: .next, action: { - arguments.openDisablePassword() - }) - case let .setupRecoveryEmail(_, text): - return GeneralInteractedRowItem(initialSize, stableId: stableId, name: text, type: .next, action: { - arguments.openSetupEmail() - }) - case let .passwordInfo(_, text): - return GeneralTextRowItem(initialSize, text: .plain(text), inset: NSEdgeInsetsMake(5, 28, 5, 28)) - case let .pendingEmailInfo(_, text): - return GeneralTextRowItem(initialSize, stableId: stableId, text: .markdown(text, linkHandler: {_ in - arguments.openResetPendingEmail() - }), inset: NSEdgeInsetsMake(5, 28, 5, 28)) - case .section: - return GeneralRowItem(initialSize, height: 20, stableId: stableId) - } - } -} - -struct TwoStepVerificationUnlockSettingsControllerState: Equatable { - let passwordText: String - let checking: Bool - - init(passwordText: String, checking: Bool) { - self.passwordText = passwordText - self.checking = checking - } - - static func ==(lhs: TwoStepVerificationUnlockSettingsControllerState, rhs: TwoStepVerificationUnlockSettingsControllerState) -> Bool { - if lhs.passwordText != rhs.passwordText { - return false - } - if lhs.checking != rhs.checking { - return false - } - - return true - } - - func withUpdatedPasswordText(_ passwordText: String) -> TwoStepVerificationUnlockSettingsControllerState { - return TwoStepVerificationUnlockSettingsControllerState(passwordText: passwordText, checking: self.checking) - } - - func withUpdatedChecking(_ cheking: Bool) -> TwoStepVerificationUnlockSettingsControllerState { - return TwoStepVerificationUnlockSettingsControllerState(passwordText: self.passwordText, checking: cheking) - } -} - - -enum TwoStepVerificationUnlockSettingsControllerMode { - case access - case manage(password: String, email: String, pendingEmailPattern: String) -} - -enum TwoStepVerificationUnlockSettingsControllerData { - case access(configuration: TwoStepVerificationConfiguration?) - case manage(password: String, emailSet: Bool, pendingEmailPattern: String) -} - - - - - - -final class TwoStepVerificationPasswordEntryControllerArguments { - let updateEntryText: (String) -> Void - let next: () -> Void - let skipEmail:() ->Void - init(updateEntryText: @escaping (String) -> Void, next: @escaping () -> Void, skipEmail:@escaping()->Void) { - self.updateEntryText = updateEntryText - self.next = next - self.skipEmail = skipEmail - } -} - - - -enum TwoStepVerificationPasswordEntryEntry: TableItemListNodeEntry { - case passwordEntry(sectionId:Int32, String, String) - - case hintEntry(sectionId:Int32, String, String) - - case emailEntry(sectionId:Int32, String) - case emailInfo(sectionId:Int32, String) - case section(Int32) - - var stableId: Int32 { - switch self { - case .passwordEntry: - return 1 - case .hintEntry: - return 3 - case .emailEntry: - return 5 - case .emailInfo: - return 6 - case .section(let id): - return (id + 1) * 1000 - id - } - } - - var index: Int32 { - switch self { - case let .passwordEntry(sectionId, _, _): - return (sectionId * 1000) + stableId - case let .hintEntry(sectionId, _, _): - return (sectionId * 1000) + stableId - case let .emailEntry(sectionId, _): - return (sectionId * 1000) + stableId - case let .emailInfo(sectionId, _): - return (sectionId * 1000) + stableId - case let .section(id): - return (id + 1) * 1000 - id - } - } - - static func ==(lhs: TwoStepVerificationPasswordEntryEntry, rhs: TwoStepVerificationPasswordEntryEntry) -> Bool { - switch lhs { - case let .passwordEntry(sectionId, text, placeholder): - if case .passwordEntry(sectionId, text, placeholder) = rhs { - return true - } else { - return false - } - case let .hintEntry(sectionId, text, placeholder): - if case .hintEntry(sectionId, text, placeholder) = rhs { - return true - } else { - return false - } - case let .emailEntry(lhsSectionId, lhsText): - if case let .emailEntry(rhsSectionId, rhsText) = rhs, lhsSectionId == rhsSectionId, lhsText == rhsText { - return true - } else { - return false - } - case let .emailInfo(lhsSectionId, lhsText): - if case let .emailInfo(rhsSectionId, rhsText) = rhs, lhsSectionId == rhsSectionId, lhsText == rhsText { - return true - } else { - return false - } - case let .section(id): - if case .section(id) = rhs { - return true - } else { - return false - } - } - } - - static func <(lhs: TwoStepVerificationPasswordEntryEntry, rhs: TwoStepVerificationPasswordEntryEntry) -> Bool { - return lhs.index < rhs.index - } - - func item(_ arguments: TwoStepVerificationPasswordEntryControllerArguments, initialSize: NSSize) -> TableRowItem { - switch self { - case let .passwordEntry(_, text, placeholder): - return GeneralInputRowItem(initialSize, stableId: stableId, placeholder: placeholder, text: text, limit: INT32_MAX, textChangeHandler: { updatedText in - arguments.updateEntryText(updatedText) - }, inputType: .secure) - case let .hintEntry(_, text, placeholder): - return GeneralInputRowItem(initialSize, stableId: stableId, placeholder: placeholder, text: text, limit: 30, textChangeHandler: { updatedText in - arguments.updateEntryText(updatedText) - }, inputType: .plain) - case let .emailEntry(_, text): - return GeneralInputRowItem(initialSize, stableId: stableId, placeholder: tr(.twoStepAuthEmail), text: text, limit: 40, textChangeHandler: { updatedText in - arguments.updateEntryText(updatedText) - }, inputType: .plain) - case let .emailInfo(_, text): - return GeneralTextRowItem(initialSize, stableId: stableId, text: .markdown(text, linkHandler: { _ in - arguments.skipEmail() - }), inset: NSEdgeInsetsMake(5, 28, 5, 28)) - case .section: - return GeneralRowItem(initialSize, height: 20, stableId: stableId) - } - } -} - -enum PasswordEntryStage: Equatable { - case entry(text: String) - case reentry(first: String, text: String) - case hint(password: String, text: String) - case email(password: String, hint: String, text: String) - - func updateCurrentText(_ text: String) -> PasswordEntryStage { - switch self { - case .entry: - return .entry(text: text) - case let .reentry(first, _): - return .reentry(first: first, text: text) - case let .hint(password, _): - return .hint(password: password, text: text) - case let .email(password, hint, _): - return .email(password: password, hint: hint, text: text) - } - } - - static func ==(lhs: PasswordEntryStage, rhs: PasswordEntryStage) -> Bool { - switch lhs { - case let .entry(text): - if case .entry(text) = rhs { - return true - } else { - return false - } - case let .reentry(first, text): - if case .reentry(first, text) = rhs { - return true - } else { - return false - } - case let .hint(password, text): - if case .hint(password, text) = rhs { - return true - } else { - return false - } - case let .email(password, hint, text): - if case .email(password, hint, text) = rhs { - return true - } else { - return false - } - } - } -} - -struct TwoStepVerificationPasswordEntryControllerState: Equatable { - let stage: PasswordEntryStage - let updating: Bool - - init(stage: PasswordEntryStage, updating: Bool) { - self.stage = stage - self.updating = updating - } - - static func ==(lhs: TwoStepVerificationPasswordEntryControllerState, rhs: TwoStepVerificationPasswordEntryControllerState) -> Bool { - if lhs.stage != rhs.stage { - return false - } - if lhs.updating != rhs.updating { - return false - } - - return true - } - - func withUpdatedStage(_ stage: PasswordEntryStage) -> TwoStepVerificationPasswordEntryControllerState { - return TwoStepVerificationPasswordEntryControllerState(stage: stage, updating: self.updating) - } - - func withUpdatedUpdating(_ updating: Bool) -> TwoStepVerificationPasswordEntryControllerState { - return TwoStepVerificationPasswordEntryControllerState(stage: self.stage, updating: updating) - } -} - -func twoStepVerificationPasswordEntryControllerEntries(state: TwoStepVerificationPasswordEntryControllerState, mode: TwoStepVerificationPasswordEntryMode) -> [TwoStepVerificationPasswordEntryEntry] { - var entries: [TwoStepVerificationPasswordEntryEntry] = [] - - var sectionId:Int32 = 0 - - entries.append(.section(sectionId)) - sectionId += 1 - - switch state.stage { - case let .entry(text): - let placeholder:String - switch mode { - case .change: - placeholder = tr(.twoStepAuthSetupPasswordEnterPasswordNew) - default: - placeholder = tr(.twoStepAuthSetupPasswordEnterPassword) - } - entries.append(.passwordEntry(sectionId: sectionId, text, placeholder)) - case let .reentry(_, text): - entries.append(.passwordEntry(sectionId: sectionId, text, tr(.twoStepAuthSetupPasswordConfirmPassword))) - case let .hint(_, text): - entries.append(.hintEntry(sectionId: sectionId, text, tr(.twoStepAuthSetupHint))) - case let .email(_, _, text): - - var emailText = tr(.twoStepAuthEmailHelp) - switch mode { - case .setupEmail: - break - default: - emailText += "\n\n[\(tr(.twoStepAuthEmailSkip))]()" - } - entries.append(.emailEntry(sectionId: sectionId, text)) - entries.append(.emailInfo(sectionId: sectionId, emailText)) - } - - return entries -} - -enum TwoStepVerificationPasswordEntryMode { - case setup - case change(current: String) - case setupEmail(password: String) -} - -struct TwoStepVerificationPasswordEntryResult { - let password: String - let pendingEmailPattern: String? -} - - - - -final class TwoStepVerificationResetControllerArguments { - let updateEntryText: (String) -> Void - let next: () -> Void - let openEmailInaccessible: () -> Void - - init(updateEntryText: @escaping (String) -> Void, next: @escaping () -> Void, openEmailInaccessible: @escaping () -> Void) { - self.updateEntryText = updateEntryText - self.next = next - self.openEmailInaccessible = openEmailInaccessible - } -} - -enum TwoStepVerificationResetEntry: TableItemListNodeEntry { - case codeEntry(sectionId:Int32, String) - case codeInfo(sectionId:Int32, String) - case section(Int32) - - var stableId: Int32 { - switch self { - case .codeEntry: - return 0 - case .codeInfo: - return 1 - case .section(let id): - return (id + 1) * 1000 - id - } - } - - static func ==(lhs: TwoStepVerificationResetEntry, rhs: TwoStepVerificationResetEntry) -> Bool { - switch lhs { - case let .codeEntry(sectionId, text): - if case .codeEntry(sectionId, text) = rhs { - return true - } else { - return false - } - case let .codeInfo(sectionId, text): - if case .codeInfo(sectionId, text) = rhs { - return true - } else { - return false - } - case .section(let id): - if case .section(id) = rhs { - return true - } else { - return false - } - } - } - - var index: Int32 { - switch self { - case let .codeInfo(sectionId, _): - return (sectionId * 1000) + stableId - case let .codeEntry(sectionId, _): - return (sectionId * 1000) + stableId - case let .section(id): - return (id + 1) * 1000 - id - } - } - - static func <(lhs: TwoStepVerificationResetEntry, rhs: TwoStepVerificationResetEntry) -> Bool { - return lhs.index < rhs.index - } - - func item(_ arguments: TwoStepVerificationResetControllerArguments, initialSize: NSSize) -> TableRowItem { - switch self { - case let .codeEntry(_, text): - return GeneralInputRowItem(initialSize, stableId: stableId, placeholder: tr(.twoStepAuthRecoveryCode), text: text, limit: 6, textChangeHandler: { updatedText in - arguments.updateEntryText(updatedText) - }, textFilter: { text -> String in - return text.trimmingCharacters(in: CharacterSet.decimalDigits.inverted) - }) - case let .codeInfo(_, text): - return GeneralTextRowItem(initialSize, stableId: stableId, text: .markdown(text, linkHandler: { _ in - arguments.openEmailInaccessible() - })) - case .section: - return GeneralRowItem(initialSize, height: 20, stableId: stableId) - } - } -} - -struct TwoStepVerificationResetControllerState: Equatable { - let codeText: String - let checking: Bool - - init(codeText: String, checking: Bool) { - self.codeText = codeText - self.checking = checking - } - - static func ==(lhs: TwoStepVerificationResetControllerState, rhs: TwoStepVerificationResetControllerState) -> Bool { - if lhs.codeText != rhs.codeText { - return false - } - if lhs.checking != rhs.checking { - return false - } - - return true - } - - func withUpdatedCodeText(_ codeText: String) -> TwoStepVerificationResetControllerState { - return TwoStepVerificationResetControllerState(codeText: codeText, checking: self.checking) - } - - func withUpdatedChecking(_ checking: Bool) -> TwoStepVerificationResetControllerState { - return TwoStepVerificationResetControllerState(codeText: self.codeText, checking: checking) - } -} diff --git a/Telegram-Mac/UITextField.swift b/Telegram-Mac/UITextField.swift new file mode 100644 index 0000000000..e94552390e --- /dev/null +++ b/Telegram-Mac/UITextField.swift @@ -0,0 +1,60 @@ +// +// UITextField.swift +// CurrencyText +// +// Created by Felipe Lefèvre Marino on 12/26/18. +// + +import Cocoa + +public extension NSTextView { + + // MARK: Public + + var selectedTextRangeOffsetFromEnd: Int { + return self.string.length - selectedRange.min + } + + /// Sets the selected text range when the text field is starting to be edited. + /// _Should_ be called when text field start to be the first responder. + func setInitialSelectedTextRange() { + // update selected text range if needed + adjustSelectedTextRange(lastOffsetFromEnd: 0) // at the end when first selected + } + + /// Interface to update the selected text range as expected. + /// - Parameter lastOffsetFromEnd: The last stored selected text range offset from end. Used to keep it concise with pre-formatting. + func updateSelectedTextRange(lastOffsetFromEnd: Int) { + adjustSelectedTextRange(lastOffsetFromEnd: lastOffsetFromEnd) + } + + // MARK: Private + + /// Adjust the selected text range to match the best position. + private func adjustSelectedTextRange(lastOffsetFromEnd: Int) { + /// If text is empty the offset is set to zero, the selected text range does need to be changed. + let text = self.string + if text.isEmpty { + return + } + + var offsetFromEnd = lastOffsetFromEnd + + /// Adjust offset if needed. When the last number character offset from end is less than the current offset, + /// or in other words, is more distant to the end of the string, the offset is readjusted to it, + /// so the selected text range is correctly set to the last index with a number. + if let lastNumberOffsetFromEnd = text.lastNumberOffsetFromEnd, + case let shouldOffsetBeAdjusted = lastNumberOffsetFromEnd < offsetFromEnd, + shouldOffsetBeAdjusted { + + offsetFromEnd = lastNumberOffsetFromEnd + } + + updateSelectedTextRange(offsetFromEnd: offsetFromEnd) + } + + /// Update the selected text range with given offset from end. + private func updateSelectedTextRange(offsetFromEnd: Int) { + self.setSelectedRange(NSMakeRange(string.length - offsetFromEnd, self.selectedRange.length)) + } +} diff --git a/Telegram-Mac/UNUserNotifications.swift b/Telegram-Mac/UNUserNotifications.swift new file mode 100644 index 0000000000..770fdfcd10 --- /dev/null +++ b/Telegram-Mac/UNUserNotifications.swift @@ -0,0 +1,365 @@ +// +// UNUserNotifications.swift +// Telegram +// +// Created by Mikhail Filimonov on 17.08.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import UserNotifications +import SwiftSignalKit +import TelegramCore +import Postbox +import TGUIKit + + + +class UNUserNotifications : NSObject { + + enum AuthorizationStatus : Int { + case notDetermined = 0 + case denied = 1 + case authorized = 2 + case provisional = 3 + } + fileprivate let manager: SharedNotificationManager + fileprivate let queue:Queue = Queue(name: "org.telegram.notifies") + internal required init(manager: SharedNotificationManager) { + self.manager = manager + super.init() + + registerCategories() + } + + fileprivate var bindings: SharedNotificationBindings { + return manager.bindings + } + + + func registerCategories() { + + } + static var _current:UNUserNotifications? + static func initialize(manager: SharedNotificationManager) { + if #available(macOS 10.14, *) { + _current = UNUserNotificationsNew(manager: manager) + } else { + _current = UNUserNotificationsOld(manager: manager) + } + } + static var current:UNUserNotifications? { + return _current + } + + static func recurrentAuthorizationStatus(_ context: AccountContext) -> Signal { + return context.window.keyWindowUpdater |> mapToSignal { _ in + return (authorizationStatus |> then(.complete() |> suspendAwareDelay(1 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart + } + } + + static var authorizationStatus: Signal { + return Signal { subscriber in + if #available(macOS 10.14, *) { + UNUserNotificationCenter.current().getNotificationSettings { settings in + if let value = AuthorizationStatus(rawValue: settings.authorizationStatus.rawValue) { + subscriber.putNext(value) + subscriber.putCompletion() + } + } + } else { + subscriber.putNext(.authorized) + subscriber.putCompletion() + } + return EmptyDisposable + } + } + + fileprivate func activateNotification(userInfo:[AnyHashable : Any], replyText: String? = nil) { + if let messageId = getNotificationMessageId(userInfo: userInfo, for: "reply"), let accountId = userInfo["accountId"] as? Int64 { + + let accountId = AccountRecordId(rawValue: accountId) + + guard let account = manager.activeAccounts.accounts.first(where: {$0.0 == accountId})?.1 else { + return + } + + closeAllModals() + + if let text = replyText { + if let sourceMessageId = getNotificationMessageId(userInfo: userInfo, for: "source") { + var replyToMessageId:MessageId? + if sourceMessageId.peerId.namespace != Namespaces.Peer.CloudUser { + replyToMessageId = sourceMessageId + } + _ = enqueueMessages(account: account, peerId: sourceMessageId.peerId, messages: [EnqueueMessage.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: replyToMessageId, localGroupingKey: nil, correlationId: nil)]).start() + + } else { + var replyToMessageId:MessageId? + if messageId.peerId.namespace != Namespaces.Peer.CloudUser { + replyToMessageId = messageId + } + _ = enqueueMessages(account: account, peerId: messageId.peerId, messages: [EnqueueMessage.message(text: text, attributes: [], mediaReference: nil, replyToMessageId: replyToMessageId, localGroupingKey: nil, correlationId: nil)]).start() + } + } else { + if let threadId = getNotificationMessageId(userInfo: userInfo, for: "thread"), let fromId = getNotificationMessageId(userInfo: userInfo, for: "source") { + self.bindings.navigateToThread(account, threadId, fromId) + } else { + self.bindings.navigateToChat(account, messageId.peerId) + } + + manager.window.makeKeyAndOrderFront(nil) + NSApp.activate(ignoringOtherApps: true) + } + } else { + manager.window.makeKeyAndOrderFront(nil) + NSApp.activate(ignoringOtherApps: true) + } + } + + func add(_ notification: NSUserNotification) -> Void { + + } + + func clearNotifies(_ peerId:PeerId, maxId:MessageId) { + + } + + + func clearNotifies(by msgIds: [MessageId]) { + + } + +} + + +final class UNUserNotificationsOld : UNUserNotifications, NSUserNotificationCenterDelegate { + func userNotificationCenter(_ center: NSUserNotificationCenter, didDeliver notification: NSUserNotification) { + if manager.requestUserAttention && !manager.window.isKeyWindow { + NSApp.requestUserAttention(.informationalRequest) + } + } + + required init(manager: SharedNotificationManager) { + super.init(manager: manager) + NSUserNotificationCenter.default.delegate = self + } + + @objc func userNotificationCenter(_ center: NSUserNotificationCenter, didDismissAlert notification: NSUserNotification) { + if let userInfo = notification.userInfo, let timestamp = userInfo["timestamp"] as? Int32, let _ = userInfo["accountId"] as? Int64, let messageId = getNotificationMessageId(userInfo: userInfo, for: "reply") { + + bindings.applyMaxReadIndexInteractively(MessageIndex(id: messageId, timestamp: timestamp)) + } + } + + func userNotificationCenter(_ center: NSUserNotificationCenter, didActivate notification: NSUserNotification) { + center.removeDeliveredNotification(notification) + } + + override func clearNotifies(_ peerId:PeerId, maxId:MessageId) { + queue.async { + + let deliveredNotifications = NSUserNotificationCenter.default.deliveredNotifications + + for notification in deliveredNotifications { + if let notificationMessageId = getNotificationMessageId(userInfo: notification.userInfo ?? [:], for: "reply") { + + let timestamp = notification.userInfo?["timestamp"] as? Int32 ?? 0 + + if notificationMessageId.peerId == peerId, notificationMessageId <= maxId { + NSUserNotificationCenter.default.removeDeliveredNotification(notification) + } else if timestamp == 0 || timestamp + 24 * 60 * 60 < Int32(Date().timeIntervalSince1970) { + NSUserNotificationCenter.default.removeDeliveredNotification(notification) + } + } + } + } + } + + + override func clearNotifies(by msgIds: [MessageId]) { + queue.async { + let deliveredNotifications = NSUserNotificationCenter.default.deliveredNotifications + + for notification in deliveredNotifications { + if let notificationMessageId = getNotificationMessageId(userInfo: notification.userInfo ?? [:], for: "reply") { + for msgId in msgIds { + if notificationMessageId == msgId { + NSUserNotificationCenter.default.removeDeliveredNotification(notification) + } + } + } + } + } + } + + override func add(_ notification: NSUserNotification) -> Void { + if let soundName = notification.soundName { + if soundName != "default" { + appDelegate?.playSound(soundName) + notification.soundName = nil + } + } + NSUserNotificationCenter.default.deliver(notification) + } +} + +@available(macOS 10.14, *) +final class UNUserNotificationsNew : UNUserNotifications, UNUserNotificationCenterDelegate { + + private var soundSettings: UNNotificationSetting? = nil + required init(manager: SharedNotificationManager) { + super.init(manager: manager) + UNUserNotificationCenter.current().delegate = self + } + + func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + + //If you don't want to show notification when app is open, do something here else and make a return here. + //Even you you don't implement this delegate method, you will not see the notification on the specified controller. So, you have to implement this delegate and make sure the below line execute. i.e. completionHandler. + + completionHandler([.alert, .sound]) + } + + func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { + + switch response.actionIdentifier { + case UNNotificationDismissActionIdentifier: // Notification was dismissed by user + completionHandler() + case UNNotificationDefaultActionIdentifier: + activateNotification(userInfo: response.notification.request.content.userInfo) + if manager.requestUserAttention && !manager.window.isKeyWindow { + NSApp.requestUserAttention(.informationalRequest) + } + completionHandler() + case UNNotification.replyCategory: + if let textResponse = response as? UNTextInputNotificationResponse { + let reply = textResponse.userText + activateNotification(userInfo: response.notification.request.content.userInfo, replyText: reply) + completionHandler() + } + default: + completionHandler() + } + } + + override func registerCategories() { + let replyAction = UNTextInputNotificationAction(identifier: "reply", title: L10n.notificationReply, options: [], textInputButtonTitle: L10n.notificationTitleReply, textInputPlaceholder: L10n.notificationInputReply) + + + let replyCategory = UNNotificationCategory(identifier: "reply", actions: [replyAction], intentIdentifiers: [], options: []) + UNUserNotificationCenter.current().setNotificationCategories([replyCategory]) + + UNUserNotificationCenter.current().getNotificationSettings { [weak self] settings in + self?.soundSettings = settings.soundSetting + } + } + override func add(_ notification: NSUserNotification) -> Void { + let content = UNMutableNotificationContent() + content.title = notification.title ?? "" + content.body = notification.informativeText ?? "" + content.subtitle = notification.subtitle ?? "" + if let soundName = notification.soundName { + if soundName == "default" { + content.sound = .default + } else { + if let soundSettings = soundSettings { + switch soundSettings { + case .enabled: + appDelegate?.playSound(soundName) + default: + break + } + } + } + + } + if notification.hasActionButton { + content.categoryIdentifier = UNNotification.replyCategory + } + + if let image = notification.contentImage { + if let attachment = UNNotificationAttachment.create(identifier: "image", image: image, options: nil) { + content.attachments = [attachment] + } + } + content.userInfo = notification.userInfo ?? [:] + + UNUserNotificationCenter.current().add(UNNotificationRequest(identifier: notification.identifier ?? "", content: content, trigger: nil), withCompletionHandler: { error in + var bp = 0 + bp += 1 + }) + } + + override func clearNotifies(_ peerId:PeerId, maxId:MessageId) { + queue.async { + let manager = UNUserNotificationCenter.current() + manager.getDeliveredNotifications(completionHandler: { notifications in + for notification in notifications { + let userInfo = notification.request.content.userInfo + if let notificationMessageId = getNotificationMessageId(userInfo: userInfo, for: "reply") { + let timestamp = userInfo["timestamp"] as? Int32 ?? 0 + if notificationMessageId.peerId == peerId, notificationMessageId <= maxId { + manager.removeDeliveredNotifications(withIdentifiers: [notification.request.identifier]) + } else if timestamp == 0 || timestamp + 24 * 60 * 60 < Int32(Date().timeIntervalSince1970) { + manager.removeDeliveredNotifications(withIdentifiers: [notification.request.identifier]) + } + } + } + }) + } + } + + + override func clearNotifies(by msgIds: [MessageId]) { + queue.async { + UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { notifications in + var remove: Set = Set() + for notification in notifications { + if let notificationMessageId = getNotificationMessageId(userInfo: notification.request.content.userInfo, for: "reply") { + for msgId in msgIds { + if notificationMessageId == msgId { + remove.insert(notification.request.identifier) + } + } + } + } + UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: Array(remove)) + }) + } + } +} + + + + + + + +@available(macOS 10.14, *) +private extension UNNotificationAttachment { + + static func create(identifier: String, image: NSImage, options: [NSObject : AnyObject]?) -> UNNotificationAttachment? { + let fileManager = FileManager.default + let tmpSubFolderName = ProcessInfo.processInfo.globallyUniqueString + let tmpSubFolderURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(tmpSubFolderName, isDirectory: true) + do { + try fileManager.createDirectory(at: tmpSubFolderURL, withIntermediateDirectories: true, attributes: nil) + let imageFileIdentifier = identifier+".jpeg" + let fileURL = tmpSubFolderURL.appendingPathComponent(imageFileIdentifier) + let imageData = image.tiffRepresentation(using: .jpeg, factor: 1) + try imageData?.write(to: fileURL) + let imageAttachment = try UNNotificationAttachment(identifier: imageFileIdentifier, url: fileURL, options: options) + return imageAttachment + } catch { + print("error " + error.localizedDescription) + } + return nil + } +} + + +@available(macOS 10.14, *) +private extension UNNotification { + static let replyCategory: String = "reply" +} diff --git a/Telegram-Mac/UnauthorizedConfiguration.swift b/Telegram-Mac/UnauthorizedConfiguration.swift new file mode 100644 index 0000000000..876d5dc77b --- /dev/null +++ b/Telegram-Mac/UnauthorizedConfiguration.swift @@ -0,0 +1,86 @@ +// +// QRLoginConfiguration.swift +// Telegram +// +// Created by Mikhail Filimonov on 26.11.2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import Postbox +import SwiftSignalKit +import TelegramApi + +enum QRLoginType : String { + case primary = "primary" + case secondary = "secondary" + case disabled = "disabled" +} + struct UnauthorizedConfiguration { + static var defaultValue: UnauthorizedConfiguration { + return UnauthorizedConfiguration(qr: .disabled) + } + + let qr: QRLoginType + + fileprivate init(qr: QRLoginType) { + self.qr = qr + } + public static func with(appConfiguration: AppConfiguration) -> UnauthorizedConfiguration { + if let data = appConfiguration.data, let rawType = data["qr_login_code"] as? String, let qr = QRLoginType(rawValue: rawType) { + return UnauthorizedConfiguration(qr: qr) + } else { + return .defaultValue + } + } +} + + +func unauthorizedConfiguration(accountManager: AccountManager) -> Signal { + return accountManager.sharedData(keys: [ApplicationSharedPreferencesKeys.appConfiguration]) |> mapToSignal { view in + if let appConfiguration = view.entries[ApplicationSharedPreferencesKeys.appConfiguration] as? AppConfiguration { + let configuration = UnauthorizedConfiguration.with(appConfiguration: appConfiguration) + return .single(configuration) + } else { + return .never() + } + } |> deliverOnMainQueue +} + +private func currentUnauthorizedAppConfiguration(transaction:AccountManagerModifier) -> AppConfiguration { + if let entry = transaction.getSharedData(ApplicationSharedPreferencesKeys.appConfiguration) as? AppConfiguration { + return entry + } else { + return AppConfiguration.defaultValue + } +} + +private func updateAppConfiguration(transaction: AccountManagerModifier, _ f: (AppConfiguration) -> AppConfiguration) { + let current = currentUnauthorizedAppConfiguration(transaction: transaction) + let updated = f(current) + transaction.updateSharedData(ApplicationSharedPreferencesKeys.appConfiguration, { _ in + return updated + }) +} + + +func managedAppConfigurationUpdates(accountManager: AccountManager, network: Network) -> Signal { + let poll = Signal { subscriber in + return (network.request(Api.functions.help.getAppConfig()) + |> retryRequest + |> mapToSignal { result -> Signal in + return accountManager.transaction { transaction -> Void in + if let data = JSON(apiJson: result) { + updateAppConfiguration(transaction: transaction, { configuration -> AppConfiguration in + var configuration = configuration + configuration.data = data + return configuration + }) + } + } + }).start() + } + return (poll |> then(.complete() |> suspendAwareDelay(12.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart +} diff --git a/Telegram-Mac/UndoOverlayHeaderView.swift b/Telegram-Mac/UndoOverlayHeaderView.swift new file mode 100644 index 0000000000..2f4691459f --- /dev/null +++ b/Telegram-Mac/UndoOverlayHeaderView.swift @@ -0,0 +1,191 @@ +// +// UndoOverlayHeaderView.swift +// Telegram +// +// Created by Mikhail Filimonov on 09/01/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + + +class UndoOverlayHeaderView: NavigationHeaderView { + private let progress = RadialProgressView(theme: RadialProgressTheme(backgroundColor: .clear, foregroundColor: .white, lineWidth: 2, clockwise: false), twist: false, size: NSMakeSize(20, 20)) + private let manager: ChatUndoManager + private let disposable = MetaDisposable() + private var didSetReady: Bool = false + private var timer: SwiftSignalKit.Timer? + private var progressValue: Double = 0.0 + private var secondsUntilFinish: Int = 0 + private let undoButton = TitleButton() + private let textView = TextView() + private let durationContainer: View = View(frame: NSMakeRect(0, 0, 18, 18)) + init(_ header: NavigationHeader, manager: ChatUndoManager) { + self.manager = manager + super.init(header) + addSubview(progress) + addSubview(undoButton) + addSubview(durationContainer) + addSubview(textView) + undoButton.set(font: .medium(.title), for: .Normal) + undoButton.direction = .right + undoButton.autohighlight = false + border = [.Bottom] + progress.state = .ImpossibleFetching(progress: 0, force: true) + + updateDuration(value: 5, animated: false) + + disposable.set((manager.allStatuses() |> deliverOnMainQueue).start(next: { [weak self] statuses in + self?.update(statuses: statuses) + })) + + + undoButton.set(handler: { [weak self] _ in + self?.manager.cancelAll() + }, for: .Click) + + updateLocalizationAndTheme(theme: theme) + } + + var removeAnimationForNextTransition: Bool = true + + private func updateProgress(force: Bool) { + progress.state = .ImpossibleFetching(progress: Float(progressValue), force: force) + } + private func updateDuration(value: Int, animated: Bool) { + if self.secondsUntilFinish != value { + let reversed: Bool = self.secondsUntilFinish < value + self.self.secondsUntilFinish = value + + let textView = TextView() + let layout = TextViewLayout.init(.initialize(string: "\(value)", color: theme.colors.text, font: .medium(12))) + layout.measure(width: .greatestFiniteMagnitude) + + + if animated { + for view in durationContainer.subviews { + textView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false) + view._change(pos: NSMakePoint(view.frame.minX, reversed ? -view.frame.height : durationContainer.frame.height), animated: true, removeOnCompletion: false, duration: 0.2, completion: { [weak view] completed in + view?.removeFromSuperview() + }) + } + } else { + durationContainer.removeAllSubviews() + } + + textView.update(layout) + durationContainer.addSubview(textView) + textView.center() + + if animated { + textView.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + textView.layer?.animatePosition(from: NSMakePoint(textView.frame.minX, reversed ? durationContainer.frame.height : -textView.frame.height), to: textView.frame.origin, duration: 0.2) + } + } + + } + + private func update(statuses: ChatUndoStatuses) { + + if statuses.hasProcessingActions { + + let newValue = 1.0 - min(1.0, max(0, statuses.secondsUntilFinish / statuses.maximumDuration)) + + let removeAnimationForNextTransition = self.removeAnimationForNextTransition + self.removeAnimationForNextTransition = false + + + timer?.invalidate() + + + timer = SwiftSignalKit.Timer(timeout: 0.016, repeat: true, completion: { [weak self] in + self?.progressValue = 1.0 - min(1.0, max(0, statuses.secondsUntilFinish / statuses.maximumDuration)) + self?.updateDuration(value: Int(round(max(1, statuses.secondsUntilFinish))), animated: true) + self?.updateProgress(force: true) + }, queue: Queue.mainQueue()) + + if progressValue > newValue { + delay(0.2, closure: { [weak self] in + self?.timer?.start() + }) + } else { + timer?.start() + } + + let layout = TextViewLayout(.initialize(string: statuses.activeDescription, color: theme.colors.text, font: .medium(.text)), maximumNumberOfLines: 10) + textView.update(layout) + + progressValue = min(max(newValue, 0), 1.0) + updateProgress(force: false) + updateDuration(value: Int(round(max(1, statuses.secondsUntilFinish))), animated: !removeAnimationForNextTransition) + + } else { + timer?.invalidate() + timer = nil + } + + + if !didSetReady { + self.ready.set(.single(true)) + didSetReady = true + } + + needsLayout = true + } + + deinit { + timer?.invalidate() + disposable.dispose() + } + + override func layout() { + super.layout() + progress.centerY(x: 28) + undoButton.centerY(x: frame.width - undoButton.frame.width - 20) + durationContainer.centerY(x: progress.frame.minX + 1, addition: -1) + + if let layout = textView.layout { + layout.measure(width: frame.width - (progress.frame.maxX + 18) - undoButton.frame.width - 20) + textView.update(layout) + } + textView.centerY(x: progress.frame.maxX + 18, addition: -1) + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) + + self.progress.theme = RadialProgressTheme(backgroundColor: .clear, foregroundColor: theme.colors.text, lineWidth: 2, clockwise: false) + + let attributed = textView.layout?.attributedString.mutableCopy() as? NSMutableAttributedString + if let attributed = attributed { + attributed.addAttribute(.foregroundColor, value: theme.colors.text, range: attributed.range) + self.textView.update(TextViewLayout(attributed, maximumNumberOfLines: 10)) + } + + self.borderColor = theme.colors.border + + undoButton.set(text: L10n.chatUndoManagerUndo, for: .Normal) + undoButton.set(image: theme.icons.chatUndoAction, for: .Normal) + undoButton.set(color: theme.colors.accent, for: .Normal) + + _ = undoButton.sizeToFit() + backgroundColor = theme.colors.background + + needsLayout = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + +} diff --git a/Telegram-Mac/UndoTooltipController.swift b/Telegram-Mac/UndoTooltipController.swift new file mode 100644 index 0000000000..06fa88d6ee --- /dev/null +++ b/Telegram-Mac/UndoTooltipController.swift @@ -0,0 +1,318 @@ +// +// UndoTooltipController.swift +// Telegram +// +// Created by Mikhail Filimonov on 26/04/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import Postbox +import SwiftSignalKit +import TelegramCore + + + + + final class UndoTooltipControl : NSObject { + var current: UndoTooltipController? + private let disposable = MetaDisposable() + private let context: AccountContext + init(context: AccountContext) { + self.context = context + super.init() + + let invocation: (NSEvent)-> KeyHandlerResult = { [weak self] _ in + self?.hideCurrentIfNeeded() + return .rejected + } + + self.context.window.set(mouseHandler: invocation, with: self, for: .leftMouseUp, priority: .supreme) + self.context.window.set(mouseHandler: invocation, with: self, for: .rightMouseUp, priority: .supreme) + self.context.window.set(mouseHandler: invocation, with: self, for: .rightMouseDown, priority: .supreme) + + + self.context.window.set(handler: { [weak self] _ in + self?.hideCurrentIfNeeded() + return .rejected + }, with: self, for: .All, priority: .supreme) + } + + var getYInset:()->CGFloat = { return 10 } { + didSet { + self.current?.getYInset = self.getYInset + } + } + + func add(controller: ViewController) { + let context = self.context + if self.current == nil { + let new = UndoTooltipController(context, controller: controller, undoManager: context.chatUndoManager) + new.getYInset = self.getYInset + new.show() + + self.current = new + + new.view.layer?.animatePosition(from: NSMakePoint(new.view.frame.minX, new.view.frame.maxY), to: new.view.frame.origin, duration: 0.25, timingFunction: .spring) + new.view.layer?.animateAlpha(from: 0, to: 1, duration: 0.2, timingFunction: .spring) + } + disposable.set((Signal.complete() |> delay(5.0, queue: .mainQueue())).start(completed: { [weak self] in + self?.hideCurrentIfNeeded() + })) + } + + private func hideCurrentIfNeeded(animated: Bool = true) { + if let current = self.current { + self.current = nil + let view = current.view + if animated { + view.layer?.animatePosition(from: view.frame.origin, to: NSMakePoint(view.frame.minX, view.frame.maxY), duration: 0.25, timingFunction: .spring, removeOnCompletion: false) + view._change(opacity: 0, duration: 0.25, timingFunction: .spring, completion: { [weak view] completed in + view?.removeFromSuperview() + }) + } else { + view.removeFromSuperview() + } + } + } + + deinit { + disposable.dispose() + context.window.removeAllHandlers(for: self) + } +} + + +final class UndoTooltipView : NSVisualEffectView, AppearanceViewProtocol { + private let progress = RadialProgressView(theme: RadialProgressTheme(backgroundColor: .clear, foregroundColor: .white, lineWidth: 2, clockwise: false), twist: false, size: NSMakeSize(20, 20)) + private let textView: TextView = TextView() + private var undoButton: TitleButton = TitleButton() + + private let manager: ChatUndoManager + private let disposable = MetaDisposable() + private var didSetReady: Bool = false + private var timer: SwiftSignalKit.Timer? + private var progressValue: Double = 0.0 + private var secondsUntilFinish: Int = 0 + private let durationContainer: View = View(frame: NSMakeRect(0, 0, 18, 18)) + + + init(frame frameRect: NSRect, undoManager: ChatUndoManager, undo: @escaping()->Void) { + self.manager = undoManager + super.init(frame: frameRect) + self.wantsLayer = true + self.blendingMode = .withinWindow + self.material = .dark + + addSubview(progress) + addSubview(undoButton) + addSubview(durationContainer) + addSubview(textView) + + self.layer?.cornerRadius = 10.0 + + + + undoButton.set(font: .medium(.title), for: .Normal) + undoButton.direction = .right + undoButton.autohighlight = false + + textView.userInteractionEnabled = false + textView.isSelectable = false + textView.disableBackgroundDrawing = true + + progress.state = .ImpossibleFetching(progress: 0, force: true) + + updateDuration(value: 5, animated: false) + + disposable.set((manager.allStatuses() |> deliverOnMainQueue).start(next: { [weak self] statuses in + self?.update(statuses: statuses) + })) + + undoButton.set(handler: { _ in + undo() + }, for: .Down) + } + + var removeAnimationForNextTransition: Bool = true + + private func updateProgress(force: Bool) { + progress.state = .ImpossibleFetching(progress: Float(progressValue), force: force) + } + private func updateDuration(value: Int, animated: Bool) { + if self.secondsUntilFinish != value { + let reversed: Bool = self.secondsUntilFinish < value + self.self.secondsUntilFinish = value + + let textView = TextView() + let layout = TextViewLayout.init(.initialize(string: "\(value)", color: .white, font: .medium(12))) + layout.measure(width: .greatestFiniteMagnitude) + + + if animated { + for view in durationContainer.subviews { + textView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false) + view._change(pos: NSMakePoint(view.frame.minX, reversed ? -view.frame.height : durationContainer.frame.height), animated: true, removeOnCompletion: false, duration: 0.2, completion: { [weak view] completed in + view?.removeFromSuperview() + }) + } + } else { + durationContainer.removeAllSubviews() + } + + textView.update(layout) + durationContainer.addSubview(textView) + textView.center() + + if animated { + textView.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + textView.layer?.animatePosition(from: NSMakePoint(textView.frame.minX, reversed ? durationContainer.frame.height : -textView.frame.height), to: textView.frame.origin, duration: 0.2) + } + } + + } + + private func update(statuses: ChatUndoStatuses) { + if statuses.hasProcessingActions { + + let newValue = 1.0 - min(1.0, max(0, statuses.secondsUntilFinish / statuses.maximumDuration)) + + let removeAnimationForNextTransition = self.removeAnimationForNextTransition + self.removeAnimationForNextTransition = false + + + timer?.invalidate() + + + timer = SwiftSignalKit.Timer(timeout: 0.016, repeat: true, completion: { [weak self] in + self?.progressValue = 1.0 - min(1.0, max(0, statuses.secondsUntilFinish / statuses.maximumDuration)) + self?.updateDuration(value: Int(round(max(1, statuses.secondsUntilFinish))), animated: true) + self?.updateProgress(force: true) + }, queue: Queue.mainQueue()) + + if progressValue > newValue { + delay(0.2, closure: { [weak self] in + self?.timer?.start() + }) + } else { + timer?.start() + } + + let layout = TextViewLayout(.initialize(string: statuses.activeDescription, color: .white, font: .medium(.text)), maximumNumberOfLines: 10) + textView.update(layout) + + progressValue = min(max(newValue, 0), 1.0) + updateProgress(force: false) + updateDuration(value: Int(round(max(1, statuses.secondsUntilFinish))), animated: !removeAnimationForNextTransition) + + } else { + timer?.invalidate() + timer = nil + } + + needsLayout = true + } + + deinit { + timer?.invalidate() + disposable.dispose() + } + + func updateLocalizationAndTheme(theme: PresentationTheme) { + + self.progress.theme = RadialProgressTheme(backgroundColor: .clear, foregroundColor: .white, lineWidth: 2, clockwise: false) + + let attributed = textView.layout?.attributedString.mutableCopy() as? NSMutableAttributedString + if let attributed = attributed { + attributed.addAttribute(.foregroundColor, value: NSColor.white, range: attributed.range) + self.textView.update(TextViewLayout(attributed, maximumNumberOfLines: 1)) + } + undoButton.set(text: L10n.chatUndoManagerUndo, for: .Normal) + undoButton.set(color: .white, for: .Normal) + + _ = undoButton.sizeToFit() + } + + + override var isFlipped: Bool { + return true + } + + override func layout() { + super.layout() + progress.centerY(x: 18) + undoButton.centerY(x: frame.width - undoButton.frame.width - 10) + durationContainer.centerY(x: progress.frame.minX + 1, addition: -1) + + if let layout = textView.layout { + layout.measure(width: frame.width - (progress.frame.maxX + 8) - undoButton.frame.width - 10) + textView.update(layout) + } + textView.centerY(x: progress.frame.maxX + 8, addition: -1) + } + + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required override init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } +} + +class UndoTooltipController: TelegramGenericViewController { + private let undoManager: ChatUndoManager + private weak var controller: ViewController? + private(set) var cancelled: Bool = false + init(_ context: AccountContext, controller: ViewController, undoManager: ChatUndoManager) { + self.undoManager = undoManager + self.controller = controller + super.init(context) + self.bar = .init(height: 0) + self._frameRect = NSMakeRect(0, 0, min(controller.frame.width - 20, 330), 40) + } + + override func viewDidLoad() { + super.viewDidLoad() + readyOnce() + } + override func initializer() -> UndoTooltipView { + return UndoTooltipView(frame: NSMakeRect(_frameRect.minX, _frameRect.minY, _frameRect.width, _frameRect.height - bar.height), undoManager: undoManager, undo: { [weak self] in + if let `self` = self { + self.undoManager.cancelAll() + self.cancelled = true + } + }) + } + + + func show() { + + guard let controller = controller else { return } + loadViewIfNeeded() + controller.view.addSubview(self.view) + self.parentFrameDidChange(Notification(name: Notification.Name(""))) + NotificationCenter.default.addObserver(self, selector: #selector(parentFrameDidChange(_:)), name: NSView.frameDidChangeNotification, object: controller.view) + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + genericView.updateLocalizationAndTheme(theme: theme) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + var getYInset:()->CGFloat = { return 10 } + + @objc private func parentFrameDidChange(_ notification:Notification) { + + guard let controller = controller else { return } + + self.view.isHidden = controller.frame.width < 100 + self.view.frame = NSMakeRect(0, 0, min(controller.frame.width - 20, 330), self.frame.height) + + self.view.centerX(y: controller.frame.height - self.frame.height - self.getYInset()) + } + +} diff --git a/Telegram-Mac/UniversalSoftwareVideoSource.swift b/Telegram-Mac/UniversalSoftwareVideoSource.swift new file mode 100644 index 0000000000..6f3e5060a3 --- /dev/null +++ b/Telegram-Mac/UniversalSoftwareVideoSource.swift @@ -0,0 +1,417 @@ +import Foundation +import SwiftSignalKit +import Postbox +import TelegramCore + +import FFMpegBinding + +private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: UnsafeMutablePointer?, bufferSize: Int32) -> Int32 { + let context = Unmanaged.fromOpaque(userData!).takeUnretainedValue() + + let data: Signal<(Data, Bool), NoError> + + let resourceSize: Int = context.size + let readCount = min(256 * 1024, Int(bufferSize)) + let requestRange: Range = context.readingOffset ..< (context.readingOffset + readCount) + + context.currentNumberOfReads += 1 + context.currentReadBytes += readCount + + let semaphore = DispatchSemaphore(value: 0) + data = context.mediaBox.resourceData(context.fileReference.media.resource, size: context.size, in: requestRange, mode: .partial) + let requiredDataIsNotLocallyAvailable = context.requiredDataIsNotLocallyAvailable + var fetchedData: Data? + let fetchDisposable = MetaDisposable() + let isInitialized = context.videoStream != nil + let mediaBox = context.mediaBox + let reference = context.fileReference.resourceReference(context.fileReference.media.resource) + let disposable = data.start(next: { result in + let (data, isComplete) = result + if data.count == readCount || isComplete { + fetchedData = data + semaphore.signal() + } else { + if isInitialized { + fetchDisposable.set(fetchedMediaResource(mediaBox: mediaBox, reference: reference, ranges: [(requestRange, .maximum)]).start()) + } + requiredDataIsNotLocallyAvailable?() + } + }) + let cancelDisposable = context.cancelRead.start(next: { value in + if value { + semaphore.signal() + } + }) + semaphore.wait() + + disposable.dispose() + cancelDisposable.dispose() + fetchDisposable.dispose() + + if let fetchedData = fetchedData { + fetchedData.withUnsafeBytes { (bytes: UnsafePointer) -> Void in + memcpy(buffer, bytes, fetchedData.count) + } + let fetchedCount = Int32(fetchedData.count) + context.readingOffset += Int(fetchedCount) + return fetchedCount + } else { + return 0 + } +} + +private func seekCallback(userData: UnsafeMutableRawPointer?, offset: Int64, whence: Int32) -> Int64 { + let context = Unmanaged.fromOpaque(userData!).takeUnretainedValue() + if (whence & FFMPEG_AVSEEK_SIZE) != 0 { + return Int64(context.size) + } else { + context.readingOffset = Int(offset) + return offset + } +} + +private final class SoftwareVideoStream { + let index: Int + let fps: CMTime + let timebase: CMTime + let duration: CMTime + let decoder: FFMpegMediaVideoFrameDecoder + let rotationAngle: Double + let aspect: Double + + init(index: Int, fps: CMTime, timebase: CMTime, duration: CMTime, decoder: FFMpegMediaVideoFrameDecoder, rotationAngle: Double, aspect: Double) { + self.index = index + self.fps = fps + self.timebase = timebase + self.duration = duration + self.decoder = decoder + self.rotationAngle = rotationAngle + self.aspect = aspect + } +} + +private final class UniversalSoftwareVideoSourceImpl { + fileprivate let mediaBox: MediaBox + fileprivate let fileReference: FileMediaReference + fileprivate let size: Int + + fileprivate let state: ValuePromise + + fileprivate var avIoContext: FFMpegAVIOContext! + fileprivate var avFormatContext: FFMpegAVFormatContext! + fileprivate var videoStream: SoftwareVideoStream! + + fileprivate var readingOffset: Int = 0 + + fileprivate var cancelRead: Signal + fileprivate var requiredDataIsNotLocallyAvailable: (() -> Void)? + fileprivate var currentNumberOfReads: Int = 0 + fileprivate var currentReadBytes: Int = 0 + + init?(mediaBox: MediaBox, fileReference: FileMediaReference, state: ValuePromise, cancelInitialization: Signal) { + guard let size = fileReference.media.size else { + return nil + } + + self.mediaBox = mediaBox + self.fileReference = fileReference + self.size = size + + self.state = state + state.set(.initializing) + + self.cancelRead = cancelInitialization + + let ioBufferSize = 1 * 1024 + + guard let avIoContext = FFMpegAVIOContext(bufferSize: Int32(ioBufferSize), opaqueContext: Unmanaged.passUnretained(self).toOpaque(), readPacket: readPacketCallback, writePacket: nil, seek: seekCallback) else { + return nil + } + self.avIoContext = avIoContext + + let avFormatContext = FFMpegAVFormatContext() + avFormatContext.setIO(avIoContext) + + if !avFormatContext.openInput() { + return nil + } + + if !avFormatContext.findStreamInfo() { + return nil + } + + self.avFormatContext = avFormatContext + + var videoStream: SoftwareVideoStream? + + for streamIndexNumber in avFormatContext.streamIndices(for: FFMpegAVFormatStreamTypeVideo) { + let streamIndex = streamIndexNumber.int32Value + if avFormatContext.isAttachedPic(atStreamIndex: streamIndex) { + continue + } + + let codecId = avFormatContext.codecId(atStreamIndex: streamIndex) + + let fpsAndTimebase = avFormatContext.fpsAndTimebase(forStreamIndex: streamIndex, defaultTimeBase: CMTimeMake(value: 1, timescale: 40000)) + let (fps, timebase) = (fpsAndTimebase.fps, fpsAndTimebase.timebase) + + let duration = CMTimeMake(value: avFormatContext.duration(atStreamIndex: streamIndex), timescale: timebase.timescale) + + let metrics = avFormatContext.metricsForStream(at: streamIndex) + + let rotationAngle: Double = metrics.rotationAngle + let aspect = Double(metrics.width) / Double(metrics.height) + + if let codec = FFMpegAVCodec.find(forId: codecId) { + let codecContext = FFMpegAVCodecContext(codec: codec) + if avFormatContext.codecParams(atStreamIndex: streamIndex, to: codecContext) { + if codecContext.open() { + videoStream = SoftwareVideoStream(index: Int(streamIndex), fps: fps, timebase: timebase, duration: duration, decoder: FFMpegMediaVideoFrameDecoder(codecContext: codecContext), rotationAngle: rotationAngle, aspect: aspect) + break + } + } + } + } + + if let videoStream = videoStream { + self.videoStream = videoStream + } else { + return nil + } + + state.set(.ready) + } + + private func readPacketInternal() -> FFMpegPacket? { + guard let avFormatContext = self.avFormatContext else { + return nil + } + + let packet = FFMpegPacket() + if avFormatContext.readFrame(into: packet) { + return packet + } else { + return nil + } + } + + func readDecodableFrame() -> (MediaTrackDecodableFrame?, Bool) { + var frames: [MediaTrackDecodableFrame] = [] + var endOfStream = false + + while frames.isEmpty { + if let packet = self.readPacketInternal() { + if let videoStream = videoStream, Int(packet.streamIndex) == videoStream.index { + let packetPts = packet.pts + + let pts = CMTimeMake(value: packetPts, timescale: videoStream.timebase.timescale) + let dts = CMTimeMake(value: packet.dts, timescale: videoStream.timebase.timescale) + + let duration: CMTime + + let frameDuration = packet.duration + if frameDuration != 0 { + duration = CMTimeMake(value: frameDuration * videoStream.timebase.value, timescale: videoStream.timebase.timescale) + } else { + duration = videoStream.fps + } + + let frame = MediaTrackDecodableFrame(type: .video, packet: packet, pts: pts, dts: dts, duration: duration) + frames.append(frame) + } + } else { + endOfStream = true + break + } + } + + if endOfStream { + if let videoStream = self.videoStream { + videoStream.decoder.reset() + } + } + + return (frames.first, endOfStream) + } + + private func seek(timestamp: Double) { + if let stream = self.videoStream, let avFormatContext = self.avFormatContext { + let pts = CMTimeMakeWithSeconds(timestamp, preferredTimescale: stream.timebase.timescale) + avFormatContext.seekFrame(forStreamIndex: Int32(stream.index), pts: pts.value, positionOnKeyframe: true) + stream.decoder.reset() + } + } + + func readImage(at timestamp: Double) -> (CGImage?, CGFloat, CGFloat, Bool) { + guard let videoStream = self.videoStream, let _ = self.avFormatContext else { + return (nil, 0.0, 1.0, false) + } + + self.seek(timestamp: timestamp) + + self.currentNumberOfReads = 0 + self.currentReadBytes = 0 + for _ in 0 ..< 10 { + let (decodableFrame, loop) = self.readDecodableFrame() + if let decodableFrame = decodableFrame { + if let renderedFrame = videoStream.decoder.render(frame: decodableFrame) { + //print("Frame rendered in \(self.currentNumberOfReads) reads, \(self.currentReadBytes) bytes, total frames read: \(i + 1)") + return (renderedFrame._cgImage, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), loop) + } + } + } + return (nil, CGFloat(videoStream.rotationAngle), CGFloat(videoStream.aspect), true) + } +} + +private enum UniversalSoftwareVideoSourceState { + case initializing + case failed + case ready + case generatingFrame +} + +private final class UniversalSoftwareVideoSourceThreadParams: NSObject { + let mediaBox: MediaBox + let fileReference: FileMediaReference + let state: ValuePromise + let cancelInitialization: Signal + + init(mediaBox: MediaBox, fileReference: FileMediaReference, state: ValuePromise, cancelInitialization: Signal) { + self.mediaBox = mediaBox + self.fileReference = fileReference + self.state = state + self.cancelInitialization = cancelInitialization + } +} + +private final class UniversalSoftwareVideoSourceTakeFrameParams: NSObject { + let timestamp: Double + let completion: (CGImage?) -> Void + let cancel: Signal + let requiredDataIsNotLocallyAvailable: () -> Void + + init(timestamp: Double, completion: @escaping (CGImage?) -> Void, cancel: Signal, requiredDataIsNotLocallyAvailable: @escaping () -> Void) { + self.timestamp = timestamp + self.completion = completion + self.cancel = cancel + self.requiredDataIsNotLocallyAvailable = requiredDataIsNotLocallyAvailable + } +} + +private final class UniversalSoftwareVideoSourceThread: NSObject { + @objc static func entryPoint(_ params: UniversalSoftwareVideoSourceThreadParams) { + let runLoop = RunLoop.current + + let timer = Timer(fireAt: .distantFuture, interval: 0.0, target: UniversalSoftwareVideoSourceThread.self, selector: #selector(UniversalSoftwareVideoSourceThread.none), userInfo: nil, repeats: false) + runLoop.add(timer, forMode: .common) + + let source = UniversalSoftwareVideoSourceImpl(mediaBox: params.mediaBox, fileReference: params.fileReference, state: params.state, cancelInitialization: params.cancelInitialization) + Thread.current.threadDictionary["source"] = source + + while true { + runLoop.run(mode: .default, before: .distantFuture) + if Thread.current.threadDictionary["UniversalSoftwareVideoSourceThread_stop"] != nil { + break + } + } + + Thread.current.threadDictionary.removeObject(forKey: "source") + } + + @objc static func none() { + } + + @objc static func stop() { + Thread.current.threadDictionary["UniversalSoftwareVideoSourceThread_stop"] = "true" + } + + @objc static func takeFrame(_ params: UniversalSoftwareVideoSourceTakeFrameParams) { + guard let source = Thread.current.threadDictionary["source"] as? UniversalSoftwareVideoSourceImpl else { + params.completion(nil) + return + } + source.cancelRead = params.cancel + source.requiredDataIsNotLocallyAvailable = params.requiredDataIsNotLocallyAvailable + source.state.set(.generatingFrame) + let result = source.readImage(at: params.timestamp) + + + + var image = result.0 + + func rad2deg(_ number: CGFloat) -> CGFloat { + return number * 180 / .pi + } + + if !result.1.isZero, let img = image { + let degress = rad2deg(result.1) + + switch degress { + case 90: + image = img.createMatchingBackingDataWithImage(orienation: .left) + case 180: + image = img.createMatchingBackingDataWithImage(orienation: .down) + case -90: + image = img.createMatchingBackingDataWithImage(orienation: .right) + default: + break + } + } + + source.cancelRead = .single(false) + source.requiredDataIsNotLocallyAvailable = nil + source.state.set(.ready) + params.completion(image) + } +} + +enum UniversalSoftwareVideoSourceTakeFrameResult { + case waitingForData + case image(CGImage?) +} + +final class UniversalSoftwareVideoSource { + private let thread: Thread + private let stateValue: ValuePromise = ValuePromise(.initializing, ignoreRepeated: true) + private let cancelInitialization: ValuePromise = ValuePromise(false) + + var ready: Signal { + return self.stateValue.get() + |> map { value -> Bool in + switch value { + case .ready: + return true + default: + return false + } + } + } + + init(mediaBox: MediaBox, fileReference: FileMediaReference) { + self.thread = Thread(target: UniversalSoftwareVideoSourceThread.self, selector: #selector(UniversalSoftwareVideoSourceThread.entryPoint(_:)), object: UniversalSoftwareVideoSourceThreadParams(mediaBox: mediaBox, fileReference: fileReference, state: self.stateValue, cancelInitialization: self.cancelInitialization.get())) + self.thread.name = "UniversalSoftwareVideoSource" + self.thread.start() + } + + deinit { + UniversalSoftwareVideoSourceThread.self.perform(#selector(UniversalSoftwareVideoSourceThread.stop), on: self.thread, with: nil, waitUntilDone: false) + self.cancelInitialization.set(true) + } + + public func takeFrame(at timestamp: Double) -> Signal { + return Signal { subscriber in + let cancel = ValuePromise(false) + UniversalSoftwareVideoSourceThread.self.perform(#selector(UniversalSoftwareVideoSourceThread.takeFrame(_:)), on: self.thread, with: UniversalSoftwareVideoSourceTakeFrameParams(timestamp: timestamp, completion: { image in + subscriber.putNext(.image(image)) + subscriber.putCompletion() + }, cancel: cancel.get(), requiredDataIsNotLocallyAvailable: { + subscriber.putNext(.waitingForData) + }), waitUntilDone: false) + + return ActionDisposable { + cancel.set(true) + } + } + } +} diff --git a/Telegram-Mac/UpdateModalController.swift b/Telegram-Mac/UpdateModalController.swift new file mode 100644 index 0000000000..f4323762e0 --- /dev/null +++ b/Telegram-Mac/UpdateModalController.swift @@ -0,0 +1,175 @@ +// +// UpdateModalController.swift +// Telegram +// +// Created by keepcoder on 10/07/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import Postbox +import TelegramCore + +import SwiftSignalKit + +private class UpdateTableItem : GeneralRowItem { + fileprivate let titleLayout: TextViewLayout + fileprivate let descLayout: TextViewLayout + + init(_ initialSize: NSSize) { + + titleLayout = TextViewLayout(.initialize(string: "Telegram 4.2", color: theme.colors.text, font: .medium(.title)), maximumNumberOfLines: 1) + titleLayout.measure(width: initialSize.width - 150) + + descLayout = TextViewLayout(.initialize(string: "You'll need to update Telegram to the latest version before you can use the app.", color: theme.colors.text, font: .normal(.text))) + descLayout.measure(width: initialSize.width - 50) + super.init(initialSize, height: 100 + descLayout.layoutSize.height, stableId: 0) + } + + override func viewClass() -> AnyClass { + return UpdateTableView.self + } +} + +private final class UpdateTableView : TableRowView { + private let titleView: TextView = TextView() + private let descView: TextView = TextView() + private let logoView: ImageView = ImageView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + logoView.image = theme.icons.confirmAppAccessoryIcon + logoView.setFrameSize(50,50) + addSubview(logoView) + addSubview(titleView) + addSubview(descView) + } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + + guard let item = item as? UpdateTableItem else {return} + + titleView.update(item.titleLayout) + descView.update(item.descLayout) + + needsLayout = true + } + + override func layout() { + super.layout() + logoView.setFrameOrigin(25, 10) + titleView.setFrameOrigin(logoView.frame.maxX + 10, floorToScreenPixels(backingScaleFactor, logoView.frame.minY + (logoView.frame.height - titleView.frame.height)/2)) + descView.setFrameOrigin(logoView.frame.minX, logoView.frame.maxY + 20) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private final class UpdateView : View { + private let headerView: View = View() + private let titleView = TextView() + let tableView = TableView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(headerView) + addSubview(tableView) + headerView.addSubview(titleView) + headerView.border = [.Bottom] + let title: TextViewLayout = TextViewLayout(.initialize(string: "Telegram Update", color: theme.colors.text, font: .medium(.title))) + title.measure(width: frameRect.width - 20) + titleView.update(title) + } + + func update() { + tableView.removeAll() + _ = tableView.addItem(item: UpdateTableItem(frame.size)) + } + + override func layout() { + super.layout() + headerView.frame = NSMakeRect(0, 0, frame.width, 50) + tableView.frame = NSMakeRect(0, 60, frame.width, frame.height - 60) + titleView.center() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +class UpdateModalController: ModalViewController { + + private let postbox: Postbox + private let network: Network + init(postbox: Postbox, network: Network) { + self.postbox = postbox + self.network = network + super.init(frame: NSMakeRect(0, 0, 320, 350)) + } + + override var modalInteractions: ModalInteractions? { + return ModalInteractions(acceptTitle: "Update Telegram", accept: { + #if APP_STORE + execute(inapp: inAppLink.external(link: "https://apps.apple.com/us/app/telegram/id747648890", false)) + #else + (NSApp.delegate as? AppDelegate)?.checkForUpdates("") + #endif + }, cancelTitle: L10n.modalCancel, cancel: { [weak self] in + self?.close() + }, drawBorder: true, height: 50, alignCancelLeft: true) + + } + + override func viewClass() -> AnyClass { + return UpdateView.self + } + + override func viewDidResized(_ size: NSSize) { + super.viewDidResized(size) + } + + override open func measure(size: NSSize) { + self.modal?.resize(with:NSMakeSize(320, min(size.height - 70, genericView.tableView.listHeight + 70)), animated: false) + } + + public func updateSize(_ animated: Bool) { + if let contentSize = self.modal?.window.contentView?.frame.size { + self.modal?.resize(with:NSMakeSize(320, min(contentSize.height - 70, genericView.tableView.listHeight + 70)), animated: animated) + } + } + + override var handleAllEvents: Bool { + return true + } + + private var genericView: UpdateView { + return self.view as! UpdateView + } + + override func escapeKeyAction() -> KeyHandlerResult { + return .invoked + } + + deinit { + } + + override func viewDidLoad() { + super.viewDidLoad() + readyOnce() + + genericView.update() + } + + override var dynamicSize: Bool { + return true + } + + override var closable: Bool { + return false + } + +} diff --git a/Telegram-Mac/UpdaterNotifySettings.swift b/Telegram-Mac/UpdaterNotifySettings.swift new file mode 100644 index 0000000000..7f95c030c9 --- /dev/null +++ b/Telegram-Mac/UpdaterNotifySettings.swift @@ -0,0 +1,230 @@ +// +// LaunchSettings.swift +// Telegram +// +// Created by Mikhail Filimonov on 04/02/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import Postbox +import SwiftSignalKit + +enum LaunchNavigation : PostboxCoding, Equatable { + case chat(PeerId, necessary: Bool) + case thread(MessageId, MessageId, necessary: Bool) + case profile(PeerId, necessary: Bool) + case settings + + func encode(_ encoder: PostboxEncoder) { + switch self { + case let .chat(peerId, necessary): + encoder.encodeInt32(0, forKey: "t") + encoder.encodeInt32(peerId.namespace._internalGetInt32Value(), forKey: "p.n") + encoder.encodeInt64(peerId.id._internalGetInt64Value(), forKey: "p.id") + encoder.encodeBool(necessary, forKey: "n") + case .settings: + encoder.encodeInt32(1, forKey: "t") + case let .thread(threadId, fromId, necessary): + encoder.encodeInt32(2, forKey: "t") + + encoder.encodeInt32(threadId.peerId.namespace._internalGetInt32Value(), forKey: "t.p.n") + encoder.encodeInt64(threadId.peerId.id._internalGetInt64Value(), forKey: "t.p.id") + encoder.encodeInt32(threadId.id, forKey: "t.m.id") + encoder.encodeInt32(threadId.namespace, forKey: "t.m.n") + + encoder.encodeInt32(fromId.peerId.namespace._internalGetInt32Value(), forKey: "f.p.n") + encoder.encodeInt64(fromId.peerId.id._internalGetInt64Value(), forKey: "f.p.id") + encoder.encodeInt32(fromId.id, forKey: "f.m.id") + encoder.encodeInt32(fromId.namespace, forKey: "f.m.n") + + encoder.encodeBool(necessary, forKey: "n") + case let .profile(peerId, necessary: necessary): + encoder.encodeInt32(3, forKey: "t") + + encoder.encodeInt32(peerId.namespace._internalGetInt32Value(), forKey: "p.n") + encoder.encodeInt64(peerId.id._internalGetInt64Value(), forKey: "p.id") + + encoder.encodeBool(necessary, forKey: "n") + + } + } + + init(decoder: PostboxDecoder) { + switch decoder.decodeInt32ForKey("t", orElse: 0) { + case 0: + let peerId = PeerId(namespace: PeerId.Namespace._internalFromInt32Value(decoder.decodeInt32ForKey("p.n", orElse: 0)), id: PeerId.Id._internalFromInt64Value(decoder.decodeInt64ForKey("p.id", orElse: 0))) + self = .chat(peerId, necessary: decoder.decodeBoolForKey("n", orElse: false)) + case 1: + self = .settings + case 2: + + let threadPeerId = PeerId(namespace: PeerId.Namespace._internalFromInt32Value(decoder.decodeInt32ForKey("t.p.n", orElse: 0)), id: PeerId.Id._internalFromInt64Value(decoder.decodeInt64ForKey("t.p.id", orElse: 0))) + + + let threadId = MessageId(peerId: threadPeerId, namespace: decoder.decodeInt32ForKey("t.m.id", orElse: 0), id: decoder.decodeInt32ForKey("t.m.n", orElse: 0)) + + + let fromPeerId = PeerId(namespace: PeerId.Namespace._internalFromInt32Value(decoder.decodeInt32ForKey("f.p.n", orElse: 0)), id: PeerId.Id._internalFromInt64Value(decoder.decodeInt64ForKey("f.p.id", orElse: 0))) + + let fromId = MessageId(peerId: fromPeerId, namespace: decoder.decodeInt32ForKey("f.m.id", orElse: 0), id: decoder.decodeInt32ForKey("f.m.n", orElse: 0)) + + self = .thread(threadId, fromId, necessary: decoder.decodeBoolForKey("n", orElse: false)) + case 3: + + let peerId = PeerId(namespace: PeerId.Namespace._internalFromInt32Value(decoder.decodeInt32ForKey("p.n", orElse: 0)), id: PeerId.Id._internalFromInt64Value(decoder.decodeInt64ForKey("p.id", orElse: 0))) + self = .profile(peerId, necessary: decoder.decodeBoolForKey("n", orElse: false)) + default: + fatalError() + } + } +} + +struct LaunchSettings: PreferencesEntry, Equatable { + + let navigation: LaunchNavigation? + let applyText: String? + let previousText: String? + let openAtLaunch: Bool + init(applyText: String?, previousText: String?, navigation: LaunchNavigation?, openAtLaunch: Bool) { + self.applyText = applyText + self.navigation = navigation + self.previousText = previousText + self.openAtLaunch = openAtLaunch + } + + init(decoder: PostboxDecoder) { + self.applyText = decoder.decodeOptionalStringForKey("at") + self.navigation = decoder.decodeObjectForKey("n1", decoder: { LaunchNavigation(decoder: $0) }) as? LaunchNavigation + self.previousText = decoder.decodeOptionalStringForKey("pt") + self.openAtLaunch = decoder.decodeBoolForKey("oat", orElse: true) + } + + func encode(_ encoder: PostboxEncoder) { + if let applyText = applyText { + encoder.encodeString(applyText, forKey: "at") + } else { + encoder.encodeNil(forKey: "at") + } + if let navigation = navigation { + encoder.encodeObject(navigation, forKey: "n1") + } else { + encoder.encodeNil(forKey: "n1") + } + if let previousText = previousText { + encoder.encodeString(previousText, forKey: "pt") + } else { + encoder.encodeNil(forKey: "pt") + } + encoder.encodeBool(self.openAtLaunch, forKey: "oat") + } + + func isEqual(to: PreferencesEntry) -> Bool { + if let to = to as? LaunchSettings { + return self == to + } else { + return false + } + } + + + func withUpdatedApplyText(_ applyText: String?) -> LaunchSettings { + return LaunchSettings(applyText: applyText, previousText: self.previousText, navigation: self.navigation, openAtLaunch: self.openAtLaunch) + } + func withUpdatedNavigation(_ navigation: LaunchNavigation?) -> LaunchSettings { + return LaunchSettings(applyText: self.applyText, previousText: self.previousText, navigation: navigation, openAtLaunch: self.openAtLaunch) + } + func withUpdatedPreviousText(_ previousText: String?) -> LaunchSettings { + return LaunchSettings(applyText: self.applyText, previousText: previousText, navigation: self.navigation, openAtLaunch: self.openAtLaunch) + } + func withUpdatedOpenAtLaunch(_ openAtLaunch: Bool) -> LaunchSettings { + return LaunchSettings(applyText: self.applyText, previousText: self.previousText, navigation: self.navigation, openAtLaunch: openAtLaunch) + } + + static var defaultSettings: LaunchSettings { + return LaunchSettings(applyText: nil, previousText: nil, navigation: nil, openAtLaunch: true) + } +} + + +/* + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.inAppNotificationSettings, { entry in + let currentSettings: InAppNotificationSettings + if let entry = entry as? InAppNotificationSettings { + currentSettings = entry + } else { + currentSettings = InAppNotificationSettings.defaultSettings + } + return f(currentSettings) + }) + } + */ + +func addAppUpdateText(_ postbox: Postbox, applyText: String?) -> Signal{ + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.launchSettings, { pref in + let settings = pref as? LaunchSettings ?? LaunchSettings.defaultSettings + return settings.withUpdatedApplyText(applyText) + }) + } |> ignoreValues +} + + +func updateLaunchSettings(_ postbox: Postbox, _ f: @escaping(LaunchSettings)->LaunchSettings) -> Signal{ + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.launchSettings, { pref in + let settings = pref as? LaunchSettings ?? LaunchSettings.defaultSettings + return f(settings) + }) + } |> ignoreValues |> deliverOnMainQueue +} + + +func appLaunchSettings(postbox: Postbox) -> Signal { + return postbox.transaction { transaction -> LaunchSettings in + return transaction.getPreferencesEntry(key: ApplicationSpecificPreferencesKeys.launchSettings) as? LaunchSettings ?? LaunchSettings.defaultSettings + } +} + + +func applyUpdateTextIfNeeded(_ postbox: Postbox) -> Signal { + return postbox.transaction { transaction -> Void in + var applyText: String? + var previousText: String? + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.launchSettings, { pref in + applyText = (pref as? LaunchSettings)?.applyText + previousText = (pref as? LaunchSettings)?.previousText + return (pref as? LaunchSettings)?.withUpdatedApplyText(nil) + }) + if let applyText = applyText { + var attributes: [MessageAttribute] = [] + + let index = applyText.firstIndex(of: "\n") + if let index = index { + let boldLine = MessageTextEntity(range: 0 ..< index.encodedOffset, type: .Bold) + attributes.append(TextEntitiesMessageAttribute(entities: [boldLine])) + + if let previousText = previousText, let prevIndex = previousText.firstIndex(of: "\n") { + let apply = String(applyText[index...]) + let previous = String(previousText[prevIndex...]) + if apply == previous { + return + } + } + } + + + let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(777000)) + let message = StoreMessage(peerId: peerId, namespace: Namespaces.Message.Local, globallyUniqueId: nil, groupingKey: nil, threadId: nil, timestamp: Int32(Date().timeIntervalSince1970), flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: peerId, text: applyText, attributes: attributes, media: []) + _ = transaction.addMessages([message], location: .UpperHistoryBlock) + + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.launchSettings, { pref in + return (pref as? LaunchSettings)?.withUpdatedPreviousText(applyText) + }) + + } + } |> ignoreValues +} diff --git a/Telegram-Mac/UpgradedAccount.swift b/Telegram-Mac/UpgradedAccount.swift new file mode 100644 index 0000000000..a7ff1ee2c3 --- /dev/null +++ b/Telegram-Mac/UpgradedAccount.swift @@ -0,0 +1,225 @@ +// +// UpgradedAccount.swift +// Telegram +// +// Created by Mikhail Filimonov on 08/03/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import Postbox +import SwiftSignalKit + + +private enum LegacyPreferencesKeyValues: Int32 { + case cacheStorageSettings = 1 + case localizationSettings = 2 + case proxySettings = 5 + + var key: ValueBoxKey { + let key = ValueBoxKey(length: 4) + key.setInt32(0, value: self.rawValue) + return key + } +} + +private enum UpgradedSharedDataKeyValues: Int32 { + case cacheStorageSettings = 2 + case localizationSettings = 3 + case proxySettings = 4 + + var key: ValueBoxKey { + let key = ValueBoxKey(length: 4) + key.setInt32(0, value: self.rawValue) + return key + } +} + + + +private enum LegacyApplicationSpecificPreferencesKeyValues: Int32 { + case inAppNotificationSettings = 0 + case baseAppSettings = 1 + case themeSettings = 22 + case autoNight = 26 + case additionalSettings = 15 + case voiceCallSettings = 34 + var key: ValueBoxKey { + return applicationSpecificPreferencesKey(self.rawValue) + } +} + +private enum UpgradedApplicationSpecificSharedDataKeyValues: Int32 { + case inAppNotificationSettings = 0 + case baseAppSettings = 1 + case themeSettings = 22 + case autoNight = 26 + case additionalSettings = 15 + case voiceCallSettings = 34 + var key: ValueBoxKey { + return applicationSpecificSharedDataKey(self.rawValue) + } +} + +private let preferencesKeyMapping: [LegacyPreferencesKeyValues: UpgradedSharedDataKeyValues] = [ + .cacheStorageSettings: .cacheStorageSettings, + .localizationSettings: .localizationSettings, + .proxySettings: .proxySettings +] + + + +private let applicationSpecificPreferencesKeyMapping: [LegacyApplicationSpecificPreferencesKeyValues: UpgradedApplicationSpecificSharedDataKeyValues] = [ + .inAppNotificationSettings: .inAppNotificationSettings, + .themeSettings: .themeSettings +] + +private func upgradedSharedDataValue(_ value: PreferencesEntry?) -> PreferencesEntry? { + return value +} + + +public func upgradedAccounts(accountManager: AccountManager, rootPath: String, encryptionParameters: ValueBoxEncryptionParameters) -> Signal { + return accountManager.transaction { transaction -> (Int32?, AccountRecordId?) in + return (transaction.getVersion(), transaction.getCurrent()?.0) + } + |> mapToSignal { version, currentId -> Signal in + guard let version = version else { + return accountManager.transaction { transaction -> Void in + transaction.setVersion(4) + } + |> ignoreValues + |> mapToSignal { _ -> Signal in + return .complete() + } + } + var signal: Signal = .complete() + if version < 1 { + if let currentId = currentId { + let upgradePreferences = accountPreferenceEntries(rootPath: rootPath, id: currentId, keys: Set(preferencesKeyMapping.keys.map({ $0.key }) + applicationSpecificPreferencesKeyMapping.keys.map({ $0.key })), encryptionParameters: encryptionParameters) + |> mapToSignal { result -> Signal in + switch result { + case let .progress(progress): + return .single(progress) + case let .result(path, values): + return accountManager.transaction { transaction -> Void in + for (key, value) in values { + var upgradedKey: ValueBoxKey? + for (k, v) in preferencesKeyMapping { + if k.key == key { + upgradedKey = v.key + break + } + } + for (k, v) in applicationSpecificPreferencesKeyMapping { + if k.key == key { + upgradedKey = v.key + break + } + } + if let upgradedKey = upgradedKey { + transaction.updateSharedData(upgradedKey, { _ in + return upgradedSharedDataValue(value) + }) + } + } + + transaction.setVersion(1) + } + |> mapToSignal { _ -> Signal in + return .complete() + } + } + } + signal = signal |> then(upgradePreferences) + } else { + let upgradePreferences = accountManager.transaction { transaction -> Void in + transaction.setVersion(1) + } + |> mapToSignal { _ -> Signal in + return .complete() + } + signal = signal |> then(upgradePreferences) + } + } + if version < 2 { + if let currentId = currentId { + let upgradeNotices = accountNoticeEntries(rootPath: rootPath, id: currentId, encryptionParameters: encryptionParameters) + |> mapToSignal { result -> Signal in + switch result { + case let .progress(progress): + return .single(progress) + case let .result(path, values): + return accountManager.transaction { transaction -> Void in + for (key, value) in values { + transaction.setNotice(NoticeEntryKey(namespace: ValueBoxKey(length: 0), key: key), value) + } + + transaction.setVersion(2) + } + |> mapToSignal { _ -> Signal in + return .complete() + } + } + } + signal = signal |> then(upgradeNotices) + } else { + let upgradeNotices = accountManager.transaction { transaction -> Void in + transaction.setVersion(2) + } + |> mapToSignal { _ -> Signal in + return .complete() + } + signal = signal |> then(upgradeNotices) + } + + let upgradeSortOrder = accountManager.transaction { transaction -> Void in + var index: Int32 = 0 + for record in transaction.getRecords() { + transaction.updateRecord(record.id, { _ in + return AccountRecord(id: record.id, attributes: record.attributes + [.sortOrder(AccountSortOrderAttribute(order: index))], temporarySessionId: record.temporarySessionId) + }) + index += 1 + } + } + |> mapToSignal { _ -> Signal in + return .complete() + } + signal = signal |> then(upgradeSortOrder) + } + if version < 3 { + if let currentId = currentId { + let upgradeAccessChallengeData = accountLegacyAccessChallengeData(rootPath: rootPath, id: currentId, encryptionParameters: encryptionParameters) + |> mapToSignal { result -> Signal in + switch result { + case let .progress(progress): + return .single(progress) + case let .result(accessChallengeData): + return accountManager.transaction { transaction -> Void in + if case .none = transaction.getAccessChallengeData() { + transaction.setAccessChallengeData(accessChallengeData) + } + + transaction.setVersion(3) + } + |> mapToSignal { _ -> Signal in + return .complete() + } + } + } + signal = signal |> then(upgradeAccessChallengeData) + } else { + let upgradeAccessChallengeData = accountManager.transaction { transaction -> Void in + transaction.setVersion(3) + } + |> mapToSignal { _ -> Signal in + return .complete() + } + signal = signal |> then(upgradeAccessChallengeData) + } + } + return signal + } +} diff --git a/Telegram-Mac/UserInfoEntries.swift b/Telegram-Mac/UserInfoEntries.swift index ea7e5a651f..ccd547eff3 100644 --- a/Telegram-Mac/UserInfoEntries.swift +++ b/Telegram-Mac/UserInfoEntries.swift @@ -7,9 +7,10 @@ // import Cocoa -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac +import TelegramCore + +import SwiftSignalKit +import Postbox import TGUIKit @@ -46,10 +47,11 @@ final class UserInfoState : PeerInfoState { let editingState: UserInfoEditingState? let savingData: Bool - init(editingState: UserInfoEditingState?, savingData: Bool) { self.editingState = editingState self.savingData = savingData + + } override init() { @@ -93,22 +95,71 @@ class UserInfoArguments : PeerInfoArguments { private let startSecretChatDisposable = MetaDisposable() private let updatePeerNameDisposable = MetaDisposable() private let deletePeerContactDisposable = MetaDisposable() + private let callDisposable = MetaDisposable() func shareContact() { - shareDisposable.set((account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue).start(next: { [weak self] peer in - if let account = self?.account { - showModal(with: ShareModalController(ShareContactObject(account, user: peer as! TelegramUser)), for: mainWindow) + let context = self.context + let peer = context.account.postbox.peerView(id: peerId) |> take(1) |> map { + return peerViewMainPeer($0) + } |> deliverOnMainQueue + + + + shareDisposable.set(peer.start(next: { [weak self] peer in + if let context = self?.context, let peer = peer as? TelegramUser { + showModal(with: ShareModalController(ShareContactObject(context, user: peer)), for: context.window) } })) } + override init(context: AccountContext, peerId: PeerId, state: PeerInfoState, isAd: Bool, pushViewController: @escaping (ViewController) -> Void, pullNavigation: @escaping () -> NavigationViewController?, mediaController: @escaping()->PeerMediaController?) { + super.init(context: context, peerId: peerId, state: state, isAd: isAd, pushViewController: pushViewController, pullNavigation: pullNavigation, mediaController: mediaController) + + let updateState:((UserInfoState)->UserInfoState)->Void = { [weak self] f in + self?.updateState(f) + } + + } + + func shareMyInfo() { + + + let context = self.context + let peerId = self.peerId + + + let peer = context.account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(peerId) + } |> deliverOnMainQueue + + _ = peer.start(next: { [weak self] peer in + if let peer = peer { + confirm(for: mainWindow, information: L10n.peerInfoConfirmShareInfo(peer.displayTitle), successHandler: { [weak self] _ in + let signal: Signal = context.account.postbox.loadedPeerWithId(context.peerId) |> map { $0 as! TelegramUser } |> mapToSignal { peer in + let signal = Sender.enqueue(message: EnqueueMessage.message(text: "", attributes: [], mediaReference: AnyMediaReference.standalone(media: TelegramMediaContact(firstName: peer.firstName ?? "", lastName: peer.lastName ?? "", phoneNumber: peer.phone ?? "", peerId: peer.id, vCardData: nil)), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil), context: context, peerId: peerId) + return signal |> map { _ in} + } + self?.shareDisposable.set(showModalProgress(signal: signal, for: mainWindow).start()) + }) + } + }) + + + } + func addContact() { - shareDisposable.set(addContactPeerInteractively(account: account, peerId: peerId, phone: nil).start()) + let context = self.context + let peerView = context.account.postbox.peerView(id: self.peerId) |> take(1) |> deliverOnMainQueue + _ = peerView.start(next: { peerView in + if let peer = peerViewMainPeer(peerView) { + showModal(with: NewContactController(context: context, peerId: peer.id), for: context.window) + } + }) } - override func updateEditable(_ editable: Bool, peerView: PeerView) { + override func updateEditable(_ editable:Bool, peerView:PeerView, controller: PeerInfoController) -> Bool { - let account = self.account + let context = self.context let peerId = self.peerId let updateState:((UserInfoState)->UserInfoState)->Void = { [weak self] f in self?.updateState(f) @@ -132,10 +183,28 @@ class UserInfoArguments : PeerInfoArguments { } } - let updateNames: Signal + if let firstName = updateValues.firstName, firstName.isEmpty { + controller.genericView.item(stableId: IntPeerInfoEntryStableId(value: 1).hashValue)?.view?.shakeView() + return false + } + - if let firstName = updateValues.firstName, let lastName = updateValues.lastName { - updateNames = showModalProgress(signal: updateContactName(account: account, peerId: peerId, firstName: firstName, lastName: lastName) |> mapError {_ in} |> deliverOnMainQueue, for: mainWindow) + if updateValues.firstName != nil || updateValues.lastName != nil { + updateState { state in + return state.withUpdatedSavingData(true) + } + } else { + updateState { state in + return state.withUpdatedEditingState(nil) + } + } + + + + let updateNames: Signal + + if let firstName = updateValues.firstName { + updateNames = showModalProgress(signal: context.engine.contacts.updateContactName(peerId: peerId, firstName: firstName, lastName: updateValues.lastName ?? "") |> deliverOnMainQueue, for: mainWindow) } else { updateNames = .complete() } @@ -152,37 +221,91 @@ class UserInfoArguments : PeerInfoArguments { } - + return true } + func sendMessage() { + self.peerChat(self.peerId) + } + func call(_ isVideo: Bool) { + let context = self.context + let peer = context.account.postbox.peerView(id: peerId) |> take(1) |> map { + return peerViewMainPeer($0)?.id + } |> filter { $0 != nil } |> map { $0! } + + let call = peer |> mapToSignal { + phoneCall(account: context.account, sharedContext: context.sharedContext, peerId: $0, isVideo: isVideo) + } |> deliverOnMainQueue + + self.callDisposable.set(call.start(next: { result in + applyUIPCallResult(context.sharedContext, result) + })) + } - func startSecretChat() { + func botAddToGroup() { + let context = self.context + let peerId = self.peerId - let signal = account.postbox.modify { [weak self] modifier -> (Peer?, Account?) in - - if let peerId = self?.peerId, let peer = modifier.getPeer(peerId), let account = self?.account { - return (peer, account) + let result = selectModalPeers(window: context.window, context: context, title: L10n.selectPeersTitleSelectChat, behavior: SelectChatsBehavior(limit: 1), confirmation: { peerIds -> Signal in + if let peerId = peerIds.first { + return context.account.postbox.loadedPeerWithId(peerId) |> deliverOnMainQueue |> mapToSignal { peer -> Signal in + return confirmSignal(for: context.window, information: L10n.confirmAddBotToGroup(peer.displayTitle)) + } + } + return .single(false) + }) |> deliverOnMainQueue |> filter {$0.first != nil} |> map {$0.first!} |> mapToSignal { groupId -> Signal in + if groupId.namespace == Namespaces.Peer.CloudGroup { + return showModalProgress(signal: context.engine.peers.addGroupMember(peerId: groupId, memberId: peerId), for: context.window) |> `catch` {_ in .complete()} |> map {groupId} } else { - return (nil, nil) + return showModalProgress(signal: context.peerChannelMemberCategoriesContextsManager.addMember(peerId: groupId, memberId: peerId), for: context.window) |> map { groupId } } + } + + _ = result.start(next: { [weak self] peerId in + self?.peerChat(peerId) + }) + } + func botShare(_ botName: String) { + showModal(with: ShareModalController(ShareLinkObject(context, link: "https://t.me/\(botName)")), for: mainWindow) + } + func botSettings() { + _ = Sender.enqueue(input: ChatTextInputState(inputText: "/settings"), context: context, peerId: peerId, replyId: nil).start() + pullNavigation()?.back() + } + func botHelp() { + _ = Sender.enqueue(input: ChatTextInputState(inputText: "/help"), context: context, peerId: peerId, replyId: nil).start() + pullNavigation()?.back() + } + + func botPrivacy() { + _ = Sender.enqueue(input: ChatTextInputState(inputText: "/privacy"), context: context, peerId: peerId, replyId: nil).start() + pullNavigation()?.back() + } + + func startSecretChat() { + let context = self.context + let peerId = self.peerId + let signal = context.account.postbox.transaction { transaction -> Peer? in - } |> deliverOnMainQueue |> mapToSignal { peer, account -> Signal in - if let peer = peer, let account = account { - let confirm = confirmSignal(for: mainWindow, header: appName, information: tr(.peerInfoConfirmStartSecretChat(peer.displayTitle))) - return confirm |> filter {$0} |> mapToSignal { (_) -> Signal in - return showModalProgress(signal: createSecretChat(account: account, peerId: peer.id), for: mainWindow) |> mapError {_ in} - } - } else { - return .complete() + return transaction.getPeer(peerId) + + } |> deliverOnMainQueue |> mapToSignal { peer -> Signal in + if let peer = peer { + let confirm = confirmSignal(for: context.window, header: L10n.peerInfoConfirmSecretChatHeader, information: L10n.peerInfoConfirmStartSecretChat(peer.displayTitle), okTitle: L10n.peerInfoConfirmSecretChatOK) + return confirm |> filter {$0} |> mapToSignal { (_) -> Signal in + return showModalProgress(signal: context.engine.peers.createSecretChat(peerId: peer.id) |> `catch` { _ in return .complete()}, for: mainWindow) } - } |> deliverOnMainQueue + } else { + return .complete() + } + } |> deliverOnMainQueue startSecretChatDisposable.set(signal.start(next: { [weak self] peerId in if let strongSelf = self { - strongSelf.pushViewController(ChatController(account: strongSelf.account, peerId: peerId)) + strongSelf.pushViewController(ChatController(context: strongSelf.context, chatLocation: .peer(peerId))) } })) } @@ -203,28 +326,55 @@ class UserInfoArguments : PeerInfoArguments { } } - func updateBlocked(_ blocked:Bool) { - blockDisposable.set(requestUpdatePeerIsBlocked(account: account, peerId: peerId, isBlocked: blocked).start()) + func updateBlocked(peer: Peer,_ blocked:Bool, _ isBot: Bool) { + let context = self.context + if blocked { + confirm(for: context.window, header: L10n.peerInfoBlockHeader, information: L10n.peerInfoBlockText(peer.displayTitle), okTitle: L10n.peerInfoBlockOK, successHandler: { [weak self] _ in + let signal = showModalProgress(signal: context.blockedPeersContext.add(peerId: peer.id) |> deliverOnMainQueue, for: context.window) + self?.blockDisposable.set(signal.start(error: { error in + switch error { + case .generic: + alert(for: context.window, info: L10n.unknownError) + } + }, completed: { + + })) + }) + } else { + let signal = showModalProgress(signal: context.blockedPeersContext.remove(peerId: peer.id) |> deliverOnMainQueue, for: context.window) + blockDisposable.set(signal.start(error: { error in + switch error { + case .generic: + alert(for: context.window, info: L10n.unknownError) + } + }, completed: { + + })) + } + + if !blocked && isBot { + pushViewController(ChatController(context: context, chatLocation: .peer(peer.id), initialAction: ChatInitialAction.start(parameter: "", behavior: .automatic))) + } + } func deleteContact() { - let account = self.account + let context = self.context let peerId = self.peerId - deletePeerContactDisposable.set((confirmSignal(for: mainWindow, header: appName, information: tr(.peerInfoConfirmDeleteContact)) + deletePeerContactDisposable.set((confirmSignal(for: context.window, information: tr(L10n.peerInfoConfirmDeleteContact)) |> filter {$0} |> mapToSignal { _ in - showModalProgress(signal: deleteContactPeerInteractively(account: account, peerId: peerId) |> deliverOnMainQueue, for: mainWindow) + showModalProgress(signal: context.engine.contacts.deleteContactPeerInteractively(peerId: peerId) |> deliverOnMainQueue, for: context.window) }).start(completed: { [weak self] in self?.pullNavigation()?.back() })) } func encryptionKey() { - pushViewController(SecretChatKeyViewController(account: account, peerId: peerId)) + pushViewController(SecretChatKeyViewController(context, peerId: peerId)) } - func groupInCommon() -> Void { - pushViewController(GroupsInCommonViewController(account: account, peerId: peerId)) + func groupInCommon(_ peerId: PeerId) -> Void { } deinit { @@ -233,6 +383,7 @@ class UserInfoArguments : PeerInfoArguments { startSecretChatDisposable.dispose() updatePeerNameDisposable.dispose() deletePeerContactDisposable.dispose() + callDisposable.dispose() } } @@ -240,24 +391,66 @@ class UserInfoArguments : PeerInfoArguments { enum UserInfoEntry: PeerInfoEntry { - case info(sectionId:Int, PeerView, editable:Bool) - case about(sectionId:Int, text: String) - case bio(sectionId:Int, text: String) - case phoneNumber(sectionId:Int, index: Int, value: PhoneNumberWithLabel) - case userName(sectionId:Int, value: String) - case sendMessage(sectionId:Int) - case shareContact(sectionId:Int) - case addContact(sectionId:Int) - case startSecretChat(sectionId:Int) - case sharedMedia(sectionId:Int) - case notifications(sectionId:Int, settings: PeerNotificationSettings?) - case groupInCommon(sectionId:Int, count:Int) - case block(sectionId:Int, Bool) - case deleteChat(sectionId: Int) - case deleteContact(sectionId: Int) - case encryptionKey(sectionId: Int) + case info(sectionId:Int, peerView: PeerView, editable:Bool, viewType: GeneralViewType) + case setFirstName(sectionId:Int, text: String, viewType: GeneralViewType) + case setLastName(sectionId:Int, text: String, viewType: GeneralViewType) + case about(sectionId:Int, text: String, viewType: GeneralViewType) + case bio(sectionId:Int, text: String, viewType: GeneralViewType) + case scam(sectionId:Int, title: String, text: String, viewType: GeneralViewType) + case phoneNumber(sectionId:Int, index: Int, value: PhoneNumberWithLabel, canCopy: Bool, viewType: GeneralViewType) + case userName(sectionId:Int, value: String, viewType: GeneralViewType) + case sendMessage(sectionId:Int, viewType: GeneralViewType) + case shareContact(sectionId:Int, viewType: GeneralViewType) + case shareMyInfo(sectionId:Int, viewType: GeneralViewType) + case addContact(sectionId:Int, viewType: GeneralViewType) + case botAddToGroup(sectionId: Int, viewType: GeneralViewType) + case botShare(sectionId: Int, name: String, viewType: GeneralViewType) + case botHelp(sectionId: Int, viewType: GeneralViewType) + case botSettings(sectionId: Int, viewType: GeneralViewType) + case botPrivacy(sectionId: Int, viewType: GeneralViewType) + case startSecretChat(sectionId:Int, viewType: GeneralViewType) + case sharedMedia(sectionId:Int, viewType: GeneralViewType) + case notifications(sectionId:Int, settings: PeerNotificationSettings?, viewType: GeneralViewType) + case groupInCommon(sectionId:Int, count:Int, peerId: PeerId, viewType: GeneralViewType) + case block(sectionId:Int, peer: Peer, blocked: Bool, isBot: Bool, viewType: GeneralViewType) + case deleteChat(sectionId: Int, viewType: GeneralViewType) + case deleteContact(sectionId: Int, viewType: GeneralViewType) + case encryptionKey(sectionId: Int, viewType: GeneralViewType) + case media(sectionId: Int, controller: PeerMediaController, isVisible: Bool, viewType: GeneralViewType) case section(sectionId:Int) + func withUpdatedViewType(_ viewType: GeneralViewType) -> UserInfoEntry { + switch self { + case let .info(sectionId, peerView, editable, _): return .info(sectionId: sectionId, peerView: peerView, editable: editable, viewType: viewType) + case let .setFirstName(sectionId, text, _): return .setFirstName(sectionId: sectionId, text: text, viewType: viewType) + case let .setLastName(sectionId, text, _): return .setLastName(sectionId: sectionId, text: text, viewType: viewType) + case let .about(sectionId, text, _): return .about(sectionId: sectionId, text: text, viewType: viewType) + case let .bio(sectionId, text, _): return .bio(sectionId: sectionId, text: text, viewType: viewType) + case let .scam(sectionId, title, text, _): return .scam(sectionId: sectionId, title: title, text: text, viewType: viewType) + case let .phoneNumber(sectionId, index, value, canCopy, _): return .phoneNumber(sectionId: sectionId, index: index, value: value, canCopy: canCopy, viewType: viewType) + case let .userName(sectionId, value: String, _): return .userName(sectionId: sectionId, value: String, viewType: viewType) + case let .sendMessage(sectionId, _): return .sendMessage(sectionId: sectionId, viewType: viewType) + case let .shareContact(sectionId, _): return .shareContact(sectionId: sectionId, viewType: viewType) + case let .shareMyInfo(sectionId, _): return .shareMyInfo(sectionId: sectionId, viewType: viewType) + case let .addContact(sectionId, _): return .addContact(sectionId: sectionId, viewType: viewType) + case let .botAddToGroup(sectionId, _): return .botAddToGroup(sectionId: sectionId, viewType: viewType) + case let .botShare(sectionId, name, _): return .botShare(sectionId: sectionId, name: name, viewType: viewType) + case let .botHelp(sectionId, _): return .botHelp(sectionId: sectionId, viewType: viewType) + case let .botSettings(sectionId, _): return .botSettings(sectionId: sectionId, viewType: viewType) + case let .botPrivacy(sectionId, _): return .botPrivacy(sectionId: sectionId, viewType: viewType) + case let .startSecretChat(sectionId, _): return .startSecretChat(sectionId: sectionId, viewType: viewType) + case let .sharedMedia(sectionId, _): return .sharedMedia(sectionId: sectionId, viewType: viewType) + case let .notifications(sectionId, settings, _): return .notifications(sectionId: sectionId, settings: settings, viewType: viewType) + case let .groupInCommon(sectionId, count, peerId, _): return .groupInCommon(sectionId: sectionId, count: count, peerId: peerId, viewType: viewType) + case let .block(sectionId, peer, blocked, isBot, _): return .block(sectionId: sectionId, peer: peer, blocked: blocked, isBot: isBot, viewType: viewType) + case let .deleteChat(sectionId, _): return .deleteChat(sectionId: sectionId, viewType: viewType) + case let .deleteContact(sectionId, _): return .deleteContact(sectionId: sectionId, viewType: viewType) + case let .encryptionKey(sectionId, _): return .encryptionKey(sectionId: sectionId, viewType: viewType) + case let .media(sectionId, controller, isVisible, _): return .media(sectionId: sectionId, controller: controller, isVisible: isVisible, viewType: viewType) + case .section: return self + } + } + var stableId: PeerInfoEntryStableId { return IntPeerInfoEntryStableId(value: self.stableIndex) } @@ -268,13 +461,16 @@ enum UserInfoEntry: PeerInfoEntry { } switch self { - case let .info(lhsSectionId, lhsPeerView, lhsEditable): + case let .info(lhsSectionId, lhsPeerView, lhsEditable, lhsViewType): switch entry { - case let .info(rhsSectionId, rhsPeerView, rhsEditable): + case let .info(rhsSectionId, rhsPeerView, rhsEditable, rhsViewType): if lhsSectionId != rhsSectionId { return false } + if lhsViewType != rhsViewType { + return false + } if lhsEditable != rhsEditable { return false @@ -282,19 +478,26 @@ enum UserInfoEntry: PeerInfoEntry { let lhsPeer = peerViewMainPeer(lhsPeerView) let lhsCachedData = lhsPeerView.cachedData - + let lhsNotificationSettings = lhsPeerView.notificationSettings + let rhsPeer = peerViewMainPeer(rhsPeerView) let rhsCachedData = rhsPeerView.cachedData - + let rhsNotificationSettings = rhsPeerView.notificationSettings if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer { if !lhsPeer.isEqual(rhsPeer) { return false } - } else if (lhsPeer == nil) != (rhsPeer != nil) { + } else if (lhsPeer != nil) != (rhsPeer != nil) { return false } - + if let lhsNotificationSettings = lhsNotificationSettings, let rhsNotificationSettings = rhsNotificationSettings { + if !lhsNotificationSettings.isEqual(to: rhsNotificationSettings) { + return false + } + } else if (lhsNotificationSettings == nil) != (rhsNotificationSettings == nil) { + return false + } if let lhsCachedData = lhsCachedData, let rhsCachedData = rhsCachedData { if !lhsCachedData.isEqual(to: rhsCachedData) { @@ -307,115 +510,182 @@ enum UserInfoEntry: PeerInfoEntry { default: return false } - case let .about(sectionId, text): + case let .setFirstName(sectionId, text, viewType): switch entry { - case .about(sectionId, text): + case .setFirstName(sectionId, text, viewType): return true default: return false } - case let .bio(sectionId, text): + case let .setLastName(sectionId, text, viewType): switch entry { - case .bio(sectionId, text): + case .setLastName(sectionId, text, viewType): return true default: return false } - case let .phoneNumber(lhsSectionId, lhsIndex, lhsValue): + case let .about(sectionId, text, viewType): switch entry { - case let .phoneNumber(rhsSectionId, rhsIndex, rhsValue) where lhsIndex == rhsIndex && lhsValue == rhsValue && lhsSectionId == rhsSectionId: + case .about(sectionId, text, viewType): return true default: return false } - case let .userName(sectionId, value): + case let .bio(sectionId, text, viewType): switch entry { - case .userName(sectionId, value): + case .bio(sectionId, text, viewType): return true default: return false } - case let .sendMessage(sectionId): + case let .scam(sectionId, title, text, viewType): switch entry { - case .sendMessage(sectionId): + case .scam(sectionId, title, text, viewType): return true default: return false } - case let .shareContact(sectionId): + case let .phoneNumber(sectionid, index, value, canCopy, viewType): switch entry { - case .shareContact(sectionId): + case .phoneNumber(sectionid, index, value, canCopy, viewType): return true default: return false } - case let .addContact(sectionId): + case let .userName(sectionId, value, viewType): switch entry { - case .addContact(sectionId): + case .userName(sectionId, value, viewType): return true default: return false } - case let .startSecretChat(sectionId): + case let .sendMessage(sectionId, viewType): switch entry { - case .startSecretChat(sectionId): + case .sendMessage(sectionId, viewType): return true default: return false } - case let .sharedMedia(sectionId): + case let .botAddToGroup(sectionId, viewType): switch entry { - case .sharedMedia(sectionId): + case .botAddToGroup(sectionId, viewType): return true default: return false } - case let .notifications(lhsSectionId, lhsSettings): + case let .botShare(sectionId, botName, viewType): switch entry { - case let .notifications(rhsSectionId, rhsSettings): - if lhsSectionId != rhsSectionId { - return false - } + case .botShare(sectionId, botName, viewType): + return true + default: + return false + } + case let .botHelp(sectionId, viewType): + switch entry { + case .botHelp(sectionId, viewType): + return true + default: + return false + } + case let .botSettings(sectionId, viewType): + switch entry { + case .botSettings(sectionId, viewType): + return true + default: + return false + } + case let .botPrivacy(sectionId, viewType): + if case .botPrivacy(sectionId, viewType) = entry { + return true + } else { + return false + } + case let .shareContact(sectionId, viewType): + switch entry { + case .shareContact(sectionId, viewType): + return true + default: + return false + } + case let .shareMyInfo(sectionId, viewType): + switch entry { + case .shareMyInfo(sectionId, viewType): + return true + default: + return false + } + case let .addContact(sectionId, viewType): + switch entry { + case .addContact(sectionId, viewType): + return true + default: + return false + } + case let .startSecretChat(sectionId, viewType): + switch entry { + case .startSecretChat(sectionId, viewType): + return true + default: + return false + } + case let .sharedMedia(sectionId, viewType): + switch entry { + case .sharedMedia(sectionId, viewType): + return true + default: + return false + } + case let .notifications(sectionId, lhsSettings, viewType): + switch entry { + case .notifications(sectionId, let rhsSettings, viewType): if let lhsSettings = lhsSettings, let rhsSettings = rhsSettings { return lhsSettings.isEqual(to: rhsSettings) } else if (lhsSettings != nil) != (rhsSettings != nil) { return false + } else { + return true } - return true default: return false } - case let .block(sectionId, isBlocked): + case let .block(sectionId, lhsPeer, isBlocked, isBot, viewType): switch entry { - case .block(sectionId, isBlocked): + case .block(sectionId, let rhsPeer, isBlocked, isBot, viewType): + return lhsPeer.isEqual(rhsPeer) + default: + return false + } + case let .groupInCommon(sectionId, count, peerId, viewType): + switch entry { + case .groupInCommon(sectionId, count, peerId, viewType): return true default: return false } - case let .groupInCommon(sectionId, count): + case let .deleteChat(sectionId, viewType): switch entry { - case .groupInCommon(sectionId, count): + case .deleteChat(sectionId, viewType): return true default: return false } - case let .deleteChat(sectionId): + case let .deleteContact(sectionId, viewType): switch entry { - case .deleteChat(sectionId): + case .deleteContact(sectionId, viewType): return true default: return false } - case let .deleteContact(sectionId): + case let .encryptionKey(sectionId, viewType): switch entry { - case .deleteContact(sectionId): + case .encryptionKey(sectionId, viewType): return true default: return false } - case let .encryptionKey(sectionId): + case let .media(sectionId, _, isVisible, viewType): switch entry { - case .encryptionKey(sectionId): + case .media(sectionId, _, isVisible, viewType): return true default: return false @@ -434,36 +704,56 @@ enum UserInfoEntry: PeerInfoEntry { switch self { case .info: return 0 - case .about: + case .setFirstName: return 1 - case .phoneNumber: + case .setLastName: return 2 - case .bio: + case .scam: return 3 - case .userName: + case .about: return 4 - case .sendMessage: + case .bio: return 5 - case .shareContact: + case .phoneNumber: return 6 - case .addContact: + case .userName: return 7 - case .startSecretChat: + case .sendMessage: return 8 - case .sharedMedia: + case .botAddToGroup: return 9 - case .notifications: + case .botShare: return 10 - case .encryptionKey: + case .botSettings: return 11 - case .groupInCommon: + case .botHelp: return 12 - case .block: + case .botPrivacy: return 13 - case .deleteChat: + case .shareContact: return 14 - case .deleteContact: + case .shareMyInfo: return 15 + case .addContact: + return 16 + case .startSecretChat: + return 17 + case .sharedMedia: + return 18 + case .notifications: + return 19 + case .encryptionKey: + return 20 + case .groupInCommon: + return 21 + case .block: + return 22 + case .deleteChat: + return 23 + case .deleteContact: + return 24 + case .media: + return 25 case let .section(id): return (id + 1) * 1000 - id } @@ -471,37 +761,57 @@ enum UserInfoEntry: PeerInfoEntry { private var sortIndex:Int { switch self { - case let .info(sectionId, _, _): + case let .info(sectionId, _, _, _): + return (sectionId * 1000) + stableIndex + case let .setFirstName(sectionId, _, _): + return (sectionId * 1000) + stableIndex + case let .setLastName(sectionId, _, _): + return (sectionId * 1000) + stableIndex + case let .about(sectionId, _, _): + return (sectionId * 1000) + stableIndex + case let .bio(sectionId, _, _): + return (sectionId * 1000) + stableIndex + case let .phoneNumber(sectionId, _, _, _, _): + return (sectionId * 1000) + stableIndex + case let .userName(sectionId, _, _): + return (sectionId * 1000) + stableIndex + case let .scam(sectionId, _, _, _): return (sectionId * 1000) + stableIndex - case let .about(sectionId, _): + case let .sendMessage(sectionId, _): return (sectionId * 1000) + stableIndex - case let .bio(sectionId, _): + case let .botAddToGroup(sectionId, _): return (sectionId * 1000) + stableIndex - case let .phoneNumber(sectionId, _, _): + case let .botShare(sectionId, _, _): return (sectionId * 1000) + stableIndex - case let .userName(sectionId, _): + case let .botSettings(sectionId, _): return (sectionId * 1000) + stableIndex - case let .sendMessage(sectionId): + case let .botPrivacy(sectionId, _): return (sectionId * 1000) + stableIndex - case let .shareContact(sectionId): + case let .botHelp(sectionId, _): return (sectionId * 1000) + stableIndex - case let .addContact(sectionId): + case let .shareContact(sectionId, _): return (sectionId * 1000) + stableIndex - case let .startSecretChat(sectionId): + case let .shareMyInfo(sectionId, _): return (sectionId * 1000) + stableIndex - case let .sharedMedia(sectionId): + case let .addContact(sectionId, _): return (sectionId * 1000) + stableIndex - case let .groupInCommon(sectionId, _): + case let .startSecretChat(sectionId, _): return (sectionId * 1000) + stableIndex - case let .notifications(sectionId, _): + case let .sharedMedia(sectionId, _): return (sectionId * 1000) + stableIndex - case let .encryptionKey(sectionId): + case let .groupInCommon(sectionId, _, _, _): return (sectionId * 1000) + stableIndex - case let .block(sectionId, _): + case let .notifications(sectionId, _, _): return (sectionId * 1000) + stableIndex - case let .deleteChat(sectionId): + case let .encryptionKey(sectionId, _): return (sectionId * 1000) + stableIndex - case let .deleteContact(sectionId): + case let .block(sectionId, _, _, _, _): + return (sectionId * 1000) + stableIndex + case let .deleteChat(sectionId, _): + return (sectionId * 1000) + stableIndex + case let .deleteContact(sectionId, _): + return (sectionId * 1000) + stableIndex + case let .media(sectionId, _, _, _): return (sectionId * 1000) + stableIndex case let .section(id): return (id + 1) * 1000 - id @@ -514,7 +824,7 @@ enum UserInfoEntry: PeerInfoEntry { return false } - return self.sortIndex > other.sortIndex + return self.sortIndex < other.sortIndex } @@ -522,78 +832,121 @@ enum UserInfoEntry: PeerInfoEntry { func item( initialSize:NSSize, arguments:PeerInfoArguments) -> TableRowItem { let arguments = arguments as! UserInfoArguments - let state = arguments.state as! UserInfoState + var state:UserInfoState { + return arguments.state as! UserInfoState + } + switch self { - case let .info(_, peerView, editable): - return PeerInfoHeaderItem(initialSize, stableId:stableId.hashValue, account:arguments.account, peerView:peerView, editable: editable, updatingPhotoState: nil, firstNameEditableText: state.editingState?.editingFirstName, lastNameEditableText: state.editingState?.editingLastName, textChangeHandler: { firstName, lastName in - arguments.updateEditingNames(firstName: firstName, lastName: lastName) + case let .info(_, peerView, editable, viewType): + return PeerInfoHeadItem(initialSize, stableId:stableId.hashValue, context: arguments.context, arguments: arguments, peerView: peerView, viewType: viewType, editing: editable) + case let .setFirstName(_, text, viewType): + return InputDataRowItem(initialSize, stableId: stableId.hashValue, mode: .plain, error: nil, viewType: viewType, currentText: text, placeholder: nil, inputPlaceholder: L10n.peerInfoFirstNamePlaceholder, filter: { $0 }, updated: { + arguments.updateEditingNames(firstName: $0, lastName: state.editingState?.editingLastName) + }, limit: 255) + case let .setLastName(_, text, viewType): + return InputDataRowItem(initialSize, stableId: stableId.hashValue, mode: .plain, error: nil, viewType: viewType, currentText: text, placeholder: nil, inputPlaceholder: L10n.peerInfoLastNamePlaceholder, filter: { $0 }, updated: { + arguments.updateEditingNames(firstName: state.editingState?.editingFirstName, lastName: $0) + }, limit: 255) + case let .about(_, text, viewType): + return TextAndLabelItem(initialSize, stableId:stableId.hashValue, label: L10n.peerInfoAbout, copyMenuText: L10n.textCopyLabelAbout, text:text, context: arguments.context, viewType: viewType, detectLinks: true, openInfo: { peerId, toChat, postId, _ in + if toChat { + arguments.peerChat(peerId, postId: postId) + } else { + arguments.peerInfo(peerId) + } + }, hashtag: arguments.context.sharedContext.bindings.globalSearch) + case let .bio(_, text, viewType): + return TextAndLabelItem(initialSize, stableId:stableId.hashValue, label: L10n.peerInfoBio, copyMenuText: L10n.textCopyLabelBio, text:text, context: arguments.context, viewType: viewType, detectLinks: true, onlyInApp: true, openInfo: { peerId, toChat, postId, _ in + if toChat { + arguments.peerChat(peerId, postId: postId) + } else { + arguments.peerInfo(peerId) + } }) - case let .about(_, text): - return TextAndLabelItem(initialSize, stableId:stableId.hashValue, label:tr(.peerInfoAbout), text:text, account: arguments.account, detectLinks:true) - case let .bio(_, text): - return TextAndLabelItem(initialSize, stableId:stableId.hashValue, label:tr(.peerInfoBio), text:text, account: arguments.account, detectLinks:false) - case let .phoneNumber(_, _, value): - return TextAndLabelItem(initialSize, stableId: stableId.hashValue, label:value.label, text:formatPhoneNumber(value.number), account: arguments.account) - case let .userName(_, value): - return TextAndLabelItem(initialSize, stableId: stableId.hashValue, label:tr(.peerInfoUsername), text:"@\(value)", account: arguments.account) - case .sendMessage: - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoSendMessage), nameStyle: blueActionButton, type: .none, action: { + case let .phoneNumber(_, _, value, canCopy, viewType): + return TextAndLabelItem(initialSize, stableId: stableId.hashValue, label:value.label, copyMenuText: L10n.textCopyLabelPhoneNumber, text: value.number, context: arguments.context, viewType: viewType, canCopy: canCopy) + case let .userName(_, value, viewType): + let link = "https://t.me/\(value)" + return TextAndLabelItem(initialSize, stableId: stableId.hashValue, label: L10n.peerInfoUsername, copyMenuText: L10n.textCopyLabelUsername, text:"@\(value)", context: arguments.context, viewType: viewType, _copyToClipboard: { + arguments.copy(link) + }) + case let .scam(_, title, text, viewType): + return TextAndLabelItem(initialSize, stableId:stableId.hashValue, label: title, copyMenuText: L10n.textCopy, labelColor: theme.colors.redUI, text: text, context: arguments.context, viewType: viewType, detectLinks:false) + case let .sendMessage(_, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoSendMessage, nameStyle: blueActionButton, type: .none, viewType: viewType, action: { arguments.peerChat(arguments.peerId) }) - case .shareContact: - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoShareContact), nameStyle: blueActionButton, type: .none, action: { + case let .botAddToGroup(_, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoBotAddToGroup, nameStyle: blueActionButton, type: .none, viewType: viewType, action: { + arguments.botAddToGroup() + }) + case let .botShare(_, name, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoBotShare, nameStyle: blueActionButton, type: .none, viewType: viewType, action: { + arguments.botShare(name) + }) + case let .botSettings(_, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoBotSettings, nameStyle: blueActionButton, type: .none, viewType: viewType, action: { + arguments.botSettings() + }) + case let .botHelp(_, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoBotHelp, nameStyle: blueActionButton, type: .none, viewType: viewType, action: { + arguments.botHelp() + }) + case let .botPrivacy(_, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoBotPrivacy, nameStyle: blueActionButton, type: .none, viewType: viewType, action: { + arguments.botPrivacy() + }) + case let .shareContact(_, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoShareContact, nameStyle: blueActionButton, type: .none, viewType: viewType, action: { arguments.shareContact() }) - case .addContact: - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoAddContact), nameStyle: blueActionButton, type: .none, action: { + case let .shareMyInfo(_, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoShareMyInfo, nameStyle: blueActionButton, type: .none, viewType: viewType, action: { + arguments.shareMyInfo() + }) + case let .addContact(_, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoAddContact, nameStyle: blueActionButton, type: .none, viewType: viewType, action: { arguments.addContact() }) - case .startSecretChat: - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoStartSecretChat), nameStyle: blueActionButton, type: .none, action: { + case let .startSecretChat(_, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoStartSecretChat, nameStyle: blueActionButton, type: .none, viewType: viewType, action: { arguments.startSecretChat() }) - case .sharedMedia: - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoSharedMedia), type: .none, action: { + case let .sharedMedia(_, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoSharedMedia, type: .next, viewType: viewType, action: { arguments.sharedMedia() }) - case let .groupInCommon(sectionId: _, count: count): - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoGroupsInCommon), type: .context(stateback: { () -> String in - return "\(count)" - }), action: { - arguments.groupInCommon() + case let .groupInCommon(sectionId: _, count, peerId, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoGroupsInCommon, type: .nextContext("\(count)"), viewType: viewType, action: { + arguments.groupInCommon(peerId) }) - case let .notifications(_, settings): + case let .notifications(_, settings, viewType): - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoNotifications), type: .switchable(stateback: { () -> Bool in - - if let settings = settings as? TelegramPeerNotificationSettings, case .muted = settings.muteState { - return false - } else { - return true - } - - }), action: { - arguments.toggleNotifications() - }) - case .encryptionKey: - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoEncryptionKey), type: .none, action: { + let settings = settings as? TelegramPeerNotificationSettings + let enabled = !(settings?.isMuted ?? false) + + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoNotifications, type: .switchable(enabled), viewType: viewType, action: {}, enabled: settings != nil) + case let .encryptionKey(_, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoEncryptionKey, type: .next, viewType: viewType, action: { arguments.encryptionKey() }) - case let .block(_, isBlocked): - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: !isBlocked ? tr(.peerInfoBlockUser) : tr(.peerInfoUnblockUser), nameStyle:redActionButton, type: .none, action: { - arguments.updateBlocked(!isBlocked) + case let .block(_, peer, isBlocked, isBot, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: isBot ? (!isBlocked ? L10n.peerInfoStopBot : L10n.peerInfoRestartBot) : (!isBlocked ? L10n.peerInfoBlockUser : L10n.peerInfoUnblockUser), nameStyle:redActionButton, type: .none, viewType: viewType, action: { + arguments.updateBlocked(peer: peer, !isBlocked, isBot) }) - case .deleteChat: - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoDeleteSecretChat), nameStyle: redActionButton, type: .none, action: { + case let .deleteChat(_, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoDeleteSecretChat, nameStyle: redActionButton, type: .none, viewType: viewType, action: { arguments.delete() }) - case .deleteContact: - return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: tr(.peerInfoDeleteContact), nameStyle: redActionButton, type: .none, action: { + case let .deleteContact(_, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId.hashValue, name: L10n.peerInfoDeleteContact, nameStyle: redActionButton, type: .none, viewType: viewType, action: { arguments.deleteContact() }) + case let .media(_, controller, isVisible, viewType): + return PeerMediaBlockRowItem(initialSize, stableId: stableId.hashValue, controller: controller, isVisible: isVisible, viewType: viewType) case .section(_): - return GeneralRowItem(initialSize, height:20, stableId: stableId.hashValue) + return GeneralRowItem(initialSize, height: 30, stableId: stableId.hashValue, viewType: .separator) } } @@ -602,95 +955,115 @@ enum UserInfoEntry: PeerInfoEntry { -func userInfoEntries(view: PeerView, arguments: PeerInfoArguments) -> [PeerInfoEntry] { +func userInfoEntries(view: PeerView, arguments: PeerInfoArguments, mediaTabsData: PeerMediaTabsData) -> [PeerInfoEntry] { let arguments = arguments as! UserInfoArguments let state = arguments.state as! UserInfoState var entries: [PeerInfoEntry] = [] - var sectionId:Int = 1 + var sectionId:Int = 0 + entries.append(UserInfoEntry.section(sectionId: sectionId)) + sectionId += 1 + + + func applyBlock(_ block:[UserInfoEntry]) { + var block = block + for (i, item) in block.enumerated() { + block[i] = item.withUpdatedViewType(bestGeneralViewType(block, for: i)) + } + entries.append(contentsOf: block) + } + + var infoBlock: [UserInfoEntry] = [] + + let editing = state.editingState != nil && (view.peers[view.peerId] as? TelegramUser)?.botInfo == nil && view.peerIsContact + + infoBlock.append(.info(sectionId: sectionId, peerView: view, editable: editing, viewType: .singleItem)) + + if editing { + infoBlock.append(.setFirstName(sectionId: sectionId, text: state.editingState?.editingFirstName ?? "", viewType: .singleItem)) + infoBlock.append(.setLastName(sectionId: sectionId, text: state.editingState?.editingLastName ?? "", viewType: .singleItem)) + } + applyBlock(infoBlock) - entries.append(UserInfoEntry.info(sectionId: sectionId, view, editable: state.editingState != nil)) + entries.append(UserInfoEntry.section(sectionId: sectionId)) + sectionId += 1 if let peer = view.peers[view.peerId] { - if let cachedUserData = view.cachedData as? CachedUserData, state.editingState == nil { - if let about = cachedUserData.about, !about.isEmpty { - if peer.isBot { - entries.append(UserInfoEntry.about(sectionId: sectionId, text: about)) - } else { - entries.append(UserInfoEntry.bio(sectionId: sectionId, text: about)) - } - } - } if let user = peerViewMainPeer(view) as? TelegramUser { + var destructBlock:[UserInfoEntry] = [] + var infoBlock:[UserInfoEntry] = [] + if state.editingState == nil { + if user.isScam { + infoBlock.append(UserInfoEntry.scam(sectionId: sectionId, title: L10n.peerInfoScam, text: L10n.peerInfoScamWarning, viewType: .singleItem)) + } else if user.isFake { + infoBlock.append(UserInfoEntry.scam(sectionId: sectionId, title: L10n.peerInfoFake, text: L10n.peerInfoFakeWarning, viewType: .singleItem)) + } + + if let cachedUserData = view.cachedData as? CachedUserData { + if let about = cachedUserData.about, !about.isEmpty, !user.isScam && !user.isFake { + if peer.isBot { + infoBlock.append(UserInfoEntry.about(sectionId: sectionId, text: about, viewType: .singleItem)) + } else { + infoBlock.append(UserInfoEntry.bio(sectionId: sectionId, text: about, viewType: .singleItem)) + } + } + } + if let phoneNumber = user.phone, !phoneNumber.isEmpty { - entries.append(UserInfoEntry.phoneNumber(sectionId: sectionId, index: 0, value: PhoneNumberWithLabel(label: tr(.peerInfoPhone), number: phoneNumber))) + infoBlock.append(.phoneNumber(sectionId: sectionId, index: 0, value: PhoneNumberWithLabel(label: L10n.peerInfoPhone, number: formatPhoneNumber(phoneNumber)), canCopy: true, viewType: .singleItem)) + } else if view.peerIsContact { + infoBlock.append(.phoneNumber(sectionId: sectionId, index: 0, value: PhoneNumberWithLabel(label: L10n.peerInfoPhone, number: L10n.newContactPhoneHidden), canCopy: false, viewType: .singleItem)) } if let username = user.username, !username.isEmpty { - entries.append(UserInfoEntry.userName(sectionId: sectionId, value: username)) + infoBlock.append(.userName(sectionId: sectionId, value: username, viewType: .singleItem)) } - entries.append(UserInfoEntry.section(sectionId: sectionId)) - sectionId += 1 - if !(peer is TelegramSecretChat) { - entries.append(UserInfoEntry.sendMessage(sectionId: sectionId)) - if let peer = peer as? TelegramUser, let phone = peer.phone, !phone.isEmpty { - if view.peerIsContact { - entries.append(UserInfoEntry.shareContact(sectionId: sectionId)) - } else { - entries.append(UserInfoEntry.addContact(sectionId: sectionId)) + if !user.isBot { + if !view.peerIsContact { + infoBlock.append(.addContact(sectionId: sectionId, viewType: .singleItem)) + if let cachedData = view.cachedData as? CachedUserData { + infoBlock.append(.block(sectionId: sectionId, peer: peer, blocked: cachedData.isBlocked, isBot: peer.isBot, viewType: .singleItem)) } } } - - if arguments.account.peerId != arguments.peerId, !(peer is TelegramSecretChat), let peer = peer as? TelegramUser, peer.botInfo == nil { - entries.append(UserInfoEntry.startSecretChat(sectionId: sectionId)) + if (peer is TelegramSecretChat) { + infoBlock.append(.encryptionKey(sectionId: sectionId, viewType: .singleItem)) } - entries.append(UserInfoEntry.section(sectionId: sectionId)) - sectionId += 1 - entries.append(UserInfoEntry.sharedMedia(sectionId: sectionId)) - } - - entries.append(UserInfoEntry.notifications(sectionId: sectionId, settings: view.notificationSettings)) - - if (peer is TelegramSecretChat) { - entries.append(UserInfoEntry.encryptionKey(sectionId: sectionId)) + applyBlock(infoBlock) } - if let cachedData = view.cachedData as? CachedUserData, arguments.account.peerId != arguments.peerId { - - if state.editingState == nil { - if cachedData.commonGroupCount > 0 { - entries.append(UserInfoEntry.groupInCommon(sectionId: sectionId, count: Int(cachedData.commonGroupCount))) + if let _ = view.cachedData as? CachedUserData, arguments.context.account.peerId != arguments.peerId { + if state.editingState != nil { + if peer is TelegramSecretChat { + destructBlock.append(.deleteChat(sectionId: sectionId, viewType: .singleItem)) + } + if view.peerIsContact { + destructBlock.append(.deleteContact(sectionId: sectionId, viewType: .singleItem)) } - entries.append(UserInfoEntry.section(sectionId: sectionId)) - sectionId += 1 - - entries.append(UserInfoEntry.block(sectionId: sectionId, cachedData.isBlocked)) - } else { - entries.append(UserInfoEntry.section(sectionId: sectionId)) - sectionId += 1 - entries.append(UserInfoEntry.deleteContact(sectionId: sectionId)) } - - + } - if peer is TelegramSecretChat { + applyBlock(destructBlock) + + + if mediaTabsData.loaded && !mediaTabsData.collections.isEmpty, let controller = arguments.mediaController() { + entries.append(UserInfoEntry.media(sectionId: sectionId, controller: controller, isVisible: state.editingState == nil, viewType: .singleItem)) + } else { entries.append(UserInfoEntry.section(sectionId: sectionId)) sectionId += 1 - - entries.append(UserInfoEntry.deleteChat(sectionId: sectionId)) } } } + return entries.sorted(by: { (p1, p2) -> Bool in return p1.isOrderedBefore(p2) }) diff --git a/Telegram-Mac/UsernameInputRowItem.swift b/Telegram-Mac/UsernameInputRowItem.swift index 7c81f142ab..3745f48dcb 100644 --- a/Telegram-Mac/UsernameInputRowItem.swift +++ b/Telegram-Mac/UsernameInputRowItem.swift @@ -8,8 +8,9 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac -import TelegramCoreMac +import SwiftSignalKit +import TelegramCore + class UsernameInputRowItem: GeneralInputRowItem { let status:AddressNameValidationStatus? let changeHandler:(String)->Void @@ -50,20 +51,30 @@ class UsernameInputRowView: GeneralInputRowView { indicator.isHidden = true } + + + override func updateColors() { + super.updateColors() + indicator.progressColor = theme.colors.grayText + } override func layout() { super.layout() if let item = item as? UsernameInputRowItem { - imageView.setFrameOrigin(textView.frame.maxX - imageView.frame.width, textView.frame.maxY - imageView.frame.height - item.insets.bottom) - indicator.setFrameOrigin(textView.frame.maxX - indicator.frame.width, textView.frame.maxY - indicator.frame.height - item.insets.bottom) + imageView.setFrameOrigin(textView.frame.maxX - imageView.frame.width, textView.frame.maxY - imageView.frame.height - item.insets.bottom - 5) + indicator.setFrameOrigin(textView.frame.maxX - indicator.frame.width, textView.frame.maxY - indicator.frame.height - item.insets.bottom - 5) if !imageView.isHidden || !indicator.isHidden { - textView.setFrameSize(frame.width - item.insets.right - 30, textView.frame.height) + textView.setFrameSize(frame.width - item.insets.right - item.insets.left, textView.frame.height) } else { - textView.setFrameSize(frame.width - item.insets.right, textView.frame.height) + textView.setFrameSize(frame.width - item.insets.right - item.insets.left, textView.frame.height) } } } + override func textViewDidReachedLimit(_ textView: Any) { + self.textView.shake() + } + override func textViewHeightChanged(_ height: CGFloat, animated: Bool) { super.textViewHeightChanged(height, animated: animated) imageView.change(pos: NSMakePoint(textView.frame.maxX - imageView.frame.width, textView.frame.maxY - imageView.frame.height), animated: animated) @@ -85,11 +96,19 @@ class UsernameInputRowView: GeneralInputRowView { indicator.isHidden = false indicator.animates = true imageView.isHidden = true - case .availability: - imageView.isHidden = false - imageView.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) - indicator.isHidden = true - indicator.animates = false + case let .availability(status): + + switch status { + case .available: + imageView.isHidden = false + imageView.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + indicator.isHidden = true + indicator.animates = false + default: + imageView.isHidden = true + indicator.isHidden = true + indicator.animates = false + } default: imageView.isHidden = true indicator.isHidden = true diff --git a/Telegram-Mac/UsernameSettingsViewController.swift b/Telegram-Mac/UsernameSettingsViewController.swift index 912a280090..904c28b3b0 100644 --- a/Telegram-Mac/UsernameSettingsViewController.swift +++ b/Telegram-Mac/UsernameSettingsViewController.swift @@ -8,81 +8,72 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore +import Postbox +import SwiftSignalKit -fileprivate enum UsernameEntries : Comparable, Identifiable { - case whiteSpace(Int64, CGFloat) - case inputEntry(placeholder:String, state:AddressNameAvailabilityState) - case stateEntry(text:String, color:NSColor) - case descEntry(String) +fileprivate enum UsernameEntryId : Hashable { + case section(Int32) + case inputEntry + case stateEntry + case descEntry - var index:Int64 { +} + +fileprivate enum UsernameEntry : Comparable, Identifiable { + case section(Int32) + case inputEntry(sectionId: Int32, placeholder:String, state:AddressNameAvailabilityState, viewType: GeneralViewType) + case stateEntry(sectionId:Int32, text:String, color:NSColor, viewType: GeneralViewType) + case descEntry(sectionId:Int32, text: String, viewType: GeneralViewType) + + var index:Int32 { + switch self { + case let .section(sectionId): + return (sectionId + 1) * 1000 - sectionId + case let .inputEntry(sectionId, _, _, _): + return (sectionId * 1000) + 1 + case let .stateEntry(sectionId, _, _, _): + return (sectionId * 1000) + 2 + case let .descEntry(sectionId, _, _): + return (sectionId * 1000) + 3 + } + } + + fileprivate var stableId:UsernameEntryId { switch self { - case let .whiteSpace(index, _): - return index + case let .section(index): + return .section(index) case .inputEntry: - return 1000 + return .inputEntry case .stateEntry: - return 2000 + return .stateEntry case .descEntry: - return 3000 + return .descEntry } } - - fileprivate var stableId:Int64 { - return index - } } -fileprivate func <(lhs:UsernameEntries, rhs:UsernameEntries) ->Bool { +fileprivate func <(lhs:UsernameEntry, rhs:UsernameEntry) ->Bool { return lhs.index < rhs.index } -fileprivate func ==(lhs:UsernameEntries, rhs:UsernameEntries) ->Bool { - switch lhs { - case let .whiteSpace(lhsIndex, lhsHeight): - if case let .whiteSpace(rhsIndex, rhsHeight) = rhs { - return lhsIndex == rhsIndex && lhsHeight == rhsHeight - } - return false - case let .inputEntry(lhsState): - if case let .inputEntry(rhsState) = rhs , lhsState.state == rhsState.state { - return true - } - return false - case let .stateEntry(lhsState): - if case let .stateEntry(rhsState) = rhs, lhsState == rhsState { - return true - } - return false - case let .descEntry(lhsDesc): - if case let .descEntry(rhsDesc) = rhs, lhsDesc == rhsDesc { - return true - } - return false - } -} - -fileprivate func prepareEntries(from:[AppearanceWrapperEntry], to:[AppearanceWrapperEntry], account:Account, initialSize:NSSize, animated:Bool, availability:ValuePromise) -> Signal { +fileprivate func prepareEntries(from:[AppearanceWrapperEntry], to:[AppearanceWrapperEntry], initialSize:NSSize, animated:Bool, availability:ValuePromise) -> Signal { return Signal { subscriber in let (removed, inserted, updated) = proccessEntriesWithoutReverse(from, right: to, { entry -> TableRowItem in switch entry.entry { - case let .whiteSpace(index, height): - return GeneralRowItem(initialSize, height: height, stableId: index) + case .section: + return GeneralRowItem(initialSize, height: 30, stableId: entry.stableId, viewType: .separator) case let .inputEntry(inputState): - - return UsernameInputRowItem(initialSize, stableId: entry.stableId, placeholder: inputState.placeholder, limit: 30, status: nil, text: inputState.state.username ?? "", changeHandler: { value in - availability.set(value) - }) - case let .stateEntry(state): - return GeneralTextRowItem(initialSize, stableId: entry.stableId, text: NSAttributedString.initialize(string: state.text, color: state.color, font: .normal(.text)), alignment: .left, inset:NSEdgeInsets(left: 30.0, right: 30.0, top:6, bottom:4)) - case let .descEntry(desc): - return GeneralTextRowItem(initialSize, stableId: entry.stableId, text: desc) + return InputDataRowItem(initialSize, stableId: entry.stableId, mode: .plain, error: nil, viewType: inputState.viewType, currentText: inputState.state.username ?? "", placeholder: nil, inputPlaceholder: inputState.placeholder, filter: { $0 }, updated: { value in + availability.set(value) + }, limit: 30) + case let .stateEntry(_, text, color, viewType): + return GeneralTextRowItem(initialSize, stableId: entry.stableId, text: NSAttributedString.initialize(string: text, color: color, font: .normal(.text)), viewType: viewType) + case let .descEntry(_, text, viewType): + return GeneralTextRowItem(initialSize, stableId: entry.stableId, text: text, viewType: viewType) } }) @@ -107,11 +98,8 @@ class UsernameSettingsViewController: TableViewController { return true } - var doneButton:Button? { - if let button = rightBarView as? TextButtonBarView { - return button.button - } - return nil + var doneButton:Control? { + return rightBarView } override func backKeyAction() -> KeyHandlerResult { @@ -119,9 +107,9 @@ class UsernameSettingsViewController: TableViewController { } override func getRightBarViewOnce() -> BarView { - let button = TextButtonBarView(controller: self, text: tr(.usernameSettingsDone)) + let button = TextButtonBarView(controller: self, text: L10n.usernameSettingsDone) - button.button.set(handler: { [weak self] _ in + button.set(handler: { [weak self] _ in self?.saveUsername() }, for: .Click) @@ -131,16 +119,25 @@ class UsernameSettingsViewController: TableViewController { func saveUsername() { - if let item = genericView.item(stableId: Int64(1000)) as? UsernameInputRowItem, let window = window { - updateDisposable.set(showModalProgress(signal: updateAddressName(account: account, domain: .account, name: item.text) |> mapError({_ in}), for: window).start()) + if let item = genericView.item(stableId: AnyHashable(UsernameEntryId.inputEntry)) as? InputDataRowItem, let window = window { + + updateDisposable.set(showModalProgress(signal: context.engine.peers.updateAddressName(domain: .account, name: item.currentText.string), for: window).start(error: { error in + switch error { + case .generic: + alert(for: mainWindow, info: L10n.unknownError) + } + }, completed: { [weak self] in + self?.navigationController?.back() + _ = showModalSuccess(for: mainWindow, icon: theme.icons.successModalProgress, delay: 0.5).start() + })) } } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - self.window?.set(handler: { [weak self] () -> KeyHandlerResult in - if let rightView = self?.rightBarView as? TextButtonBarView, rightView.button.isEnabled { + self.window?.set(handler: { [weak self] _ -> KeyHandlerResult in + if let rightView = self?.rightBarView as? TextButtonBarView, rightView.isEnabled { self?.saveUsername() return .rejected } @@ -153,43 +150,39 @@ class UsernameSettingsViewController: TableViewController { - let previous:Atomic<[AppearanceWrapperEntry]> = Atomic(value:[]) - let entries:Promise<[UsernameEntries]> = Promise() + let previous:Atomic<[AppearanceWrapperEntry]> = Atomic(value:[]) + let entries:Promise<[UsernameEntry]> = Promise() let initialSize = self.atomicSize.modify({$0}) - let account = self.account + let context = self.context let availability = self.availability - var mutableItems:[UsernameEntries] = [.whiteSpace(0, 16), - .inputEntry(placeholder: tr(.usernameSettingsInputPlaceholder), state:.none(username: nil)), - .descEntry(tr(.usernameSettingsChangeDescription))] - username.set(account.viewTracker.peerView( account.peerId) |> deliverOnMainQueue |> mapToSignal { peerView -> Signal in - if let peer = peerView.peers[account.peerId] { + username.set(context.account.viewTracker.peerView( context.peerId) |> deliverOnMainQueue |> mapToSignal { peerView -> Signal in + if let peer = peerView.peers[context.peerId] { return .single(peer.username ?? "") } return .complete() }) - self.genericView.merge(with: combineLatest(entries.get(),username.get() |> distinctUntilChanged |> mapToSignal {username -> Signal in + self.genericView.merge(with: combineLatest(entries.get(),username.get() |> distinctUntilChanged |> mapToSignal {username -> Signal in availability.set(username) return .single(username) }, appearanceSignal) |> deliverOnMainQueue - |> mapToSignal { items, username, appearance -> Signal in + |> mapToSignal { items, username, appearance -> Signal in let items = items.map{AppearanceWrapperEntry(entry: $0, appearance: appearance)} - return prepareEntries(from: previous.swap(items), to: items, account: account, initialSize: initialSize, animated: true, availability:availability) + return prepareEntries(from: previous.swap(items), to: items, initialSize: initialSize, animated: true, availability:availability) }) let availabilityChecker = combineLatest(availability.get(), username.get() |> distinctUntilChanged) - |> mapToSignal { (value,username) -> Signal<(AddressNameAvailabilityState,String),Void> in - if let error = checkAddressNameFormat(value) { - return .single((AddressNameAvailabilityState.fail(username: value, formatError: error, availability: .available), username)) - } else { - return .single((AddressNameAvailabilityState.progress(username: value), username)) |> then(addressNameAvailability(account: account, domain: .account, name: value) - |> map { availability -> (AddressNameAvailabilityState,String) in + |> mapToSignal { (value,username) -> Signal<(AddressNameAvailabilityState,String), NoError> in + + return context.engine.peers.validateAddressNameInteractive(domain: .account, name: value) |> map { state in + switch state { + case let .availability(availability): switch availability { case .available: return (AddressNameAvailabilityState.success(username: value), username) @@ -198,52 +191,62 @@ class UsernameSettingsViewController: TableViewController { case .taken: return (AddressNameAvailabilityState.fail(username: value, formatError: nil, availability: availability), username) } - }) + case let .invalidFormat(error): + return (AddressNameAvailabilityState.fail(username: value, formatError: error, availability: .invalid), username) + case .checking: + return (AddressNameAvailabilityState.progress(username: value), username) + } } } |> deliverOnMainQueue - |> mapToSignal { [weak self] (availability,address) -> Signal in - mutableItems[1] = .inputEntry(placeholder: tr(.usernameSettingsInputPlaceholder), state:availability) + |> mapToSignal { [weak self] (availability,address) -> Signal in + // var mutableItems:[UsernameEntry] = [.whiteSpace(0, 16), + // .inputEntry(placeholder: tr(L10n.usernameSettingsInputPlaceholder), state:.none(username: nil)), +// .descEntry(tr(L10n.usernameSettingsChangeDescription))] + + var items:[UsernameEntry] = [] + var sectionId: Int32 = 0 + + items.append(.section(sectionId)) + sectionId += 1 + + items.append(.inputEntry(sectionId: sectionId, placeholder: L10n.usernameSettingsInputPlaceholder, state: availability, viewType: .singleItem)) switch availability { case .none: - if case .stateEntry = mutableItems[2] { - mutableItems.remove(at: 2) - } self?.doneButton?.isEnabled = true break case .progress: self?.doneButton?.isEnabled = false break case let .success(username:username): - if case .stateEntry = mutableItems[2] { - mutableItems.remove(at: 2) - } if address != username { if username?.length != 0 { - mutableItems.insert(.stateEntry(text:tr(.usernameSettingsAvailable(username ?? "")), color: theme.colors.blueUI), at: 2) + items.append(.stateEntry(sectionId: sectionId, text: L10n.usernameSettingsAvailable(username ?? ""), color: theme.colors.accent, viewType: .textBottomItem)) } } self?.doneButton?.isEnabled = address != username case let .fail(fail): - if case .stateEntry = mutableItems[2] { - mutableItems.remove(at: 2) - } - let enabled = fail.username?.length == 0 && address.length != 0 - - let stateEntry:UsernameEntries + let stateEntry:UsernameEntry if let error = fail.formatError { - stateEntry = .stateEntry(text: error.description, color: theme.colors.redUI) + stateEntry = .stateEntry(sectionId: sectionId, text: error.description, color: theme.colors.redUI, viewType: .textBottomItem) } else { - stateEntry = .stateEntry(text: fail.availability.description, color: theme.colors.redUI) + stateEntry = .stateEntry(sectionId: sectionId, text: fail.availability.description(for: address), color: theme.colors.redUI, viewType: .textBottomItem) } if fail.username?.length != 0 { - mutableItems.insert(stateEntry, at: 2) + items.append(stateEntry) } self?.doneButton?.isEnabled = enabled } - entries.set(.single(mutableItems)) + + items.append(.descEntry(sectionId: sectionId, text: L10n.usernameSettingsChangeDescription, viewType: .textBottomItem)) + + + items.append(.section(sectionId)) + sectionId += 1 + + entries.set(.single(items)) self?.readyOnce() return .single(Void()) @@ -268,7 +271,7 @@ class UsernameSettingsViewController: TableViewController { } override func firstResponder() -> NSResponder? { - if let item = genericView.item(stableId: Int64(1000)), let view = genericView.viewNecessary(at: item.index) as? GeneralInputRowView { + if let view = genericView.item(at: 1).view as? InputDataRowView { return view.textView } return nil diff --git a/Telegram-Mac/VCardContactController.swift b/Telegram-Mac/VCardContactController.swift new file mode 100644 index 0000000000..80d89140f4 --- /dev/null +++ b/Telegram-Mac/VCardContactController.swift @@ -0,0 +1,258 @@ +// +// VCardContactController.swift +// Telegram +// +// Created by Mikhail Filimonov on 19/07/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import Contacts +import SwiftSignalKit +// +//private class VCardContactView : View { +// let tableView: TableView = TableView(frame: NSZeroRect) +// private let title: TextView = TextView() +// private let separator : View = View() +// required init(frame frameRect: NSRect) { +// super.init(frame: frameRect) +// addSubview(title) +// addSubview(tableView) +// addSubview(separator) +// separator.backgroundColor = theme.colors.border +// +// self.title.update(TextViewLayout(.initialize(string: "Contact", color: theme.colors.text, font: .medium(.title)), maximumNumberOfLines: 1)) +// needsLayout = true +// } +// +// +// +// required init?(coder: NSCoder) { +// fatalError("init(coder:) has not been implemented") +// } +// +// override func layout() { +// super.layout() +// tableView.frame = NSMakeRect(0, 50, frame.width, frame.height - 50) +// title.layout?.measure(width: frame.width - 60) +// title.update(title.layout) +// title.centerX(y: floorToScreenPixels(backingScaleFactor, (50 - title.frame.height) / 2)) +// separator.frame = NSMakeRect(0, 49, frame.width, .borderSize) +// } +//} +// +//private final class VCardArguments { +// let account: Account +// init(account: Account) { +// self.account = account +// } +//} +// +//private func vCardEntries(vCard: CNContact, contact: TelegramMediaContact, arguments: VCardArguments) -> [InputDataEntry] { +// +// var entries: [InputDataEntry] = [] +// var sectionId:Int32 = 0 +// var index: Int32 = 0 +// +// func getLabel(_ key: String) -> String { +// +// switch key { +// case "_$!!$_": +// return L10n.contactInfoURLLabelHomepage +// case "_$!!$_": +// return L10n.contactInfoPhoneLabelHome +// case "_$!!$_": +// return L10n.contactInfoPhoneLabelWork +// case "_$!!$_": +// return L10n.contactInfoPhoneLabelMobile +// case "_$!
!$_": +// return L10n.contactInfoPhoneLabelMain +// case "_$!!$_": +// return L10n.contactInfoPhoneLabelHomeFax +// case "_$!!$_": +// return L10n.contactInfoPhoneLabelWorkFax +// case "_$!!$_": +// return L10n.contactInfoPhoneLabelPager +// case "_$!!$_": +// return L10n.contactInfoPhoneLabelOther +// default: +// return L10n.contactInfoPhoneLabelOther +// } +// } +// +// entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("header"), equatable: nil, comparable: nil, item: { initialSize, stableId -> TableRowItem in +// return VCardHeaderItem(initialSize, stableId: stableId, account: arguments.account, vCard: vCard, contact: contact) +// })) +// index += 1 +// +// for phoneNumber in vCard.phoneNumbers { +// if let label = phoneNumber.label { +// entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("phone_\(phoneNumber.identifier)"), equatable: nil, comparable: nil, item: { initialSize, stableId -> TableRowItem in +// return TextAndLabelItem(initialSize, stableId: stableId, label: getLabel(label), text: phoneNumber.value.stringValue, account: arguments.account) +// })) +// } +// index += 1 +// } +// +// for email in vCard.emailAddresses { +// if let label = email.label { +// entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("email_\(email.identifier)"), equatable: nil, comparable: nil, item: { initialSize, stableId -> TableRowItem in +// return TextAndLabelItem(initialSize, stableId: stableId, label: getLabel(label), text: email.value as String, account: arguments.account) +// })) +// } +// index += 1 +// } +// +// for address in vCard.urlAddresses { +// if let label = address.label { +// entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("url_\(address.identifier)"), equatable: nil, comparable: nil, item: { initialSize, stableId -> TableRowItem in +// return TextAndLabelItem(initialSize, stableId: stableId, label: getLabel(label), text: address.value as String, account: arguments.account) +// })) +// } +// index += 1 +// } +// +// for address in vCard.postalAddresses { +// if let label = address.label { +// let text: String = address.value.street + "\n" + address.value.city + "\n" + address.value.country +// entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("url_\(address.identifier)"), equatable: nil, comparable: nil, item: { initialSize, stableId -> TableRowItem in +// return TextAndLabelItem(initialSize, stableId: stableId, label: getLabel(label), text: text, account: arguments.account) +// })) +// } +// index += 1 +// } +// +// if let birthday = vCard.birthday { +// let date = Calendar.current.date(from: birthday)! +// +// let dateFormatter = DateFormatter() +// dateFormatter.dateStyle = .long +// +// entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("birthday"), equatable: nil, comparable: nil, item: { initialSize, stableId -> TableRowItem in +// return TextAndLabelItem(initialSize, stableId: stableId, label: L10n.contactInfoBirthdayLabel, text: dateFormatter.string(from: date), account: arguments.account) +// })) +// index += 1 +// } +// +// for social in vCard.socialProfiles { +// entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("social_\(social.identifier)"), equatable: nil, comparable: nil, item: { initialSize, stableId -> TableRowItem in +// return TextAndLabelItem(initialSize, stableId: stableId, label: social.value.service, text: social.value.urlString, account: arguments.account) +// })) +// } +// +// for social in vCard.instantMessageAddresses { +// entries.append(InputDataEntry.custom(sectionId: sectionId, index: index, value: .none, identifier: InputDataIdentifier("instant_\(social.identifier)"), equatable: nil, comparable: nil, item: { initialSize, stableId -> TableRowItem in +// return TextAndLabelItem(initialSize, stableId: stableId, label: social.value.service, text: social.value.username, account: arguments.account) +// })) +// } +// +// +// return entries +//} +// +// +//final class VCardModalController : ModalViewController { +// private let controller: NavigationViewController +// init(_ account: Account, vCard: CNContact, contact: TelegramMediaContact) { +// self.controller = VCardContactController(account, vCard: vCard, contact: contact) +// super.init(frame: controller._frameRect) +// } +// +// public override var handleEvents: Bool { +// return true +// } +// +// public override func firstResponder() -> NSResponder? { +// return controller.controller.firstResponder() +// } +// +// public override func returnKeyAction() -> KeyHandlerResult { +// return controller.controller.returnKeyAction() +// } +// +// public override var haveNextResponder: Bool { +// return true +// } +// +// public override func nextResponder() -> NSResponder? { +// return controller.controller.nextResponder() +// } +// +// var input: InputDataController { +// return controller.controller as! InputDataController +// } +// +// public override func viewDidLoad() { +// super.viewDidLoad() +// ready.set(controller.ready.get()) +// } +// +// override var view: NSView { +// if !controller.isLoaded() { +// controller.loadViewIfNeeded() +// viewDidLoad() +// } +// return controller.view +// } +// +// override var modalInteractions: ModalInteractions? { +// return ModalInteractions(acceptTitle: L10n.modalOK) +// } +// +// override func measure(size: NSSize) { +// self.modal?.resize(with:NSMakeSize(380, min(size.height - 70, input.genericView.listHeight + 70)), animated: false) +// } +// +// public func updateSize(_ animated: Bool) { +// if let contentSize = self.modal?.window.contentView?.frame.size { +// self.modal?.resize(with:NSMakeSize(380, min(contentSize.height - 70, input.genericView.listHeight + 70)), animated: animated) +// } +// } +// override var dynamicSize: Bool { +// return true +// } +// +//} +// +//private class VCardContactController: NavigationViewController { +// +//// override func viewClass() -> AnyClass { +//// return VCardContactView.self +//// } +// +// fileprivate let context: AccountContext +// fileprivate let vCard: CNContact +// fileprivate let contact: TelegramMediaContact +// fileprivate let input: InputDataController +// fileprivate let values: Promise<[InputDataEntry]> = Promise() +// init(_ account: Account, vCard: CNContact, contact: TelegramMediaContact) { +// self.account = account +// self.vCard = vCard +// self.contact = contact +// input = InputDataController(dataSignal: values.get() |> map {($0, true)}, title: L10n.contactInfoContactInfo, hasDone: false) +// super.init(input) +// self._frameRect = NSMakeRect(0, 0, 380, 500) +// } +// +// +// +// +// override func viewDidLoad() { +// super.viewDidLoad() +// ready.set(input.ready.get()) +// let arguments = VCardArguments(account: account) +// let vCard = self.vCard +// let contact = self.contact +// +// values.set(appearanceSignal |> deliverOnPrepareQueue |> map { _ in return vCardEntries(vCard: vCard, contact: contact, arguments: arguments)}) +// } +// +//// private var genericView:VCardContactView { +//// return self.view as! VCardContactView +//// } +// +//} diff --git a/Telegram-Mac/VCardHeaderItem.swift b/Telegram-Mac/VCardHeaderItem.swift new file mode 100644 index 0000000000..75065b3c2c --- /dev/null +++ b/Telegram-Mac/VCardHeaderItem.swift @@ -0,0 +1,15 @@ +// +// VCardHeaderItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 19/07/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import Postbox +import SwiftSignalKit +import TGUIKit +import Contacts diff --git a/Telegram-Mac/VCardLocationRowItem.swift b/Telegram-Mac/VCardLocationRowItem.swift new file mode 100644 index 0000000000..1752d5c532 --- /dev/null +++ b/Telegram-Mac/VCardLocationRowItem.swift @@ -0,0 +1,85 @@ +// +// VCardLocationRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 20/07/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import Contacts +import TelegramCore + + +class VCardLocationRowItem: GeneralRowItem { + fileprivate let address: CNLabeledValue + fileprivate let textLayout: TextViewLayout + init(_ initialSize: NSSize, stableId: AnyHashable, address: CNLabeledValue, account: Account) { + self.address = address + let attr = NSMutableAttributedString() + + if let label = address.label { + _ = attr.append(string: label, color: theme.colors.accent, font: .normal(.text)) + _ = attr.append(string: "\n\n") + } + + _ = attr.append(string: address.value.street, color: theme.colors.text, font: .normal(.text)) + _ = attr.append(string: "\n", color: theme.colors.text, font: .normal(.text)) + _ = attr.append(string: address.value.city, color: theme.colors.text, font: .normal(.text)) + _ = attr.append(string: "\n", color: theme.colors.text, font: .normal(.text)) + _ = attr.append(string: address.value.country, color: theme.colors.text, font: .normal(.text)) + _ = attr.append(string: "\n", color: theme.colors.text, font: .normal(.text)) + + self.textLayout = TextViewLayout(attr) + super.init(initialSize, stableId: stableId) + + } + + override var height: CGFloat { + return max(textLayout.layoutSize.height, 80) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { + let result = super.makeSize(width, oldWidth: oldWidth) + textLayout.measure(width: width - 180) + return result + } + + override func viewClass() -> AnyClass { + return VCardLocationRowView.self + } + +} + +private final class VCardLocationRowView : TableRowView { + fileprivate let textView = TextView() + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(textView) + } + + override func updateColors() { + super.updateColors() + textView.backgroundColor = theme.colors.background + } + + override func layout() { + super.layout() + guard let item = item as? VCardLocationRowItem else { return } + + textView.centerY(x: item.inset.left) + } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + guard let item = item as? VCardLocationRowItem else { return } + textView.update(item.textLayout) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} diff --git a/Telegram-Mac/ValidateAddressNameInteractive.swift b/Telegram-Mac/ValidateAddressNameInteractive.swift deleted file mode 100644 index 75e7b49868..0000000000 --- a/Telegram-Mac/ValidateAddressNameInteractive.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// ValidateAddressNameInteractive.swift -// Telegram -// -// Created by keepcoder on 23/02/2017. -// Copyright © 2017 Telegram. All rights reserved. -// - -import Cocoa - -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac - -enum AddressNameValidationStatus: Equatable { - case checking - case invalidFormat(AddressNameFormatError) - case availability(AddressNameAvailability) - - static func ==(lhs: AddressNameValidationStatus, rhs: AddressNameValidationStatus) -> Bool { - switch lhs { - case .checking: - if case .checking = rhs { - return true - } else { - return false - } - case let .invalidFormat(error): - if case .invalidFormat(error) = rhs { - return true - } else { - return false - } - case let .availability(availability): - if case .availability(availability) = rhs { - return true - } else { - return false - } - } - } -} - -func validateAddressNameInteractive(account: Account, domain: AddressNameDomain, name: String) -> Signal { - if let error = checkAddressNameFormat(name) { - return .single(.invalidFormat(error)) - } else { - return .single(.checking) |> then(addressNameAvailability(account: account, domain: domain, name: name) - |> delay(0.3, queue: Queue.concurrentDefaultQueue()) - |> map { result -> AddressNameValidationStatus in .availability(result) }) - } -} diff --git a/Telegram-Mac/ValuesSelectorModalController.swift b/Telegram-Mac/ValuesSelectorModalController.swift new file mode 100644 index 0000000000..9a3fe6ff9d --- /dev/null +++ b/Telegram-Mac/ValuesSelectorModalController.swift @@ -0,0 +1,287 @@ +// +// ValuesSelectorModalController.swift +// Telegram +// +// Created by Mikhail Filimonov on 21/03/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit + +private final class ValuesSelectorArguments where T : Equatable { + let selectItem:(ValuesSelectorValue)->Void + init(selectItem:@escaping(ValuesSelectorValue)->Void) { + self.selectItem = selectItem + } +} + +private enum ValuesSelectorEntry : TableItemListNodeEntry where T : Equatable { + case sectionId(sectionId: Int32) + case value(sectionId: Int32, index: Int32, value: ValuesSelectorValue, selected: Bool, viewType: GeneralViewType) + var stableId: Int32 { + switch self { + case let .value(_, index, _, _, _): + return index + case let .sectionId(sectionId): + return 1000 + sectionId + } + } + + var index: Int32 { + switch self { + case let .sectionId(sectionId): + return (sectionId + 1) * 1000 - sectionId + case let .value(sectionId, index, _, _, _): + return (sectionId * 1000) + index + } + } + + func item(_ arguments: ValuesSelectorArguments, initialSize: NSSize) -> TableRowItem { + switch self { + case let .value(_, _, value, selected, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: value.localized, type: .none, viewType: viewType, action: { + arguments.selectItem(value) + }) + case .sectionId: + return GeneralRowItem(initialSize, height: 30, stableId: stableId, viewType: .separator) + } + } +} + +private func ==(lhs: ValuesSelectorEntry, rhs: ValuesSelectorEntry) -> Bool { + switch lhs { + case let .value(section, index, value, selected, viewType): + if case .value(section, index, value, selected, viewType) = rhs { + return true + } else { + return false + } + case let .sectionId(sectionId): + if case .sectionId(sectionId) = rhs { + return true + } else { + return false + } + } +} + +private func <(lhs: ValuesSelectorEntry, rhs: ValuesSelectorEntry) -> Bool { + return lhs.index < rhs.index +} + +private final class ValuesSelectorModalView : View { + let tableView: TableView = TableView(frame: NSZeroRect) + fileprivate let searchView: SearchView = SearchView(frame: NSZeroRect) + private let separator : View = View() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(tableView) + addSubview(separator) + addSubview(searchView) + tableView.getBackgroundColor = { + return .clear + } + separator.backgroundColor = theme.colors.border + } + + func hasSearch(_ hasSearch: Bool) { + searchView.isHidden = !hasSearch + separator.isHidden = !hasSearch + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layout() { + super.layout() + + let offset: CGFloat = searchView.isHidden ? 0 : 50 + + tableView.frame = NSMakeRect(0, offset, frame.width, frame.height - offset) + searchView.setFrameSize(NSMakeSize(frame.width - 20, 30)) + searchView.centerX(y: floorToScreenPixels(backingScaleFactor, (50 - searchView.frame.height) / 2)) + separator.frame = NSMakeRect(0, 49, frame.width, .borderSize) + } +} + +private final class ValuesSelectorState : Equatable where T : Equatable { + let selected: ValuesSelectorValue? + let values: [ValuesSelectorValue] + init(selected: ValuesSelectorValue? = nil, values: [ValuesSelectorValue] = []) { + self.selected = selected + self.values = values + } + + func withUpdatedSelected(_ selected: ValuesSelectorValue?) -> ValuesSelectorState { + return ValuesSelectorState(selected: selected, values: self.values) + } + func withUpdatedValues(_ values: [ValuesSelectorValue]) -> ValuesSelectorState { + return ValuesSelectorState(selected: self.selected, values: values) + } +} + +private func ==(lhs: ValuesSelectorState, rhs: ValuesSelectorState) -> Bool { + return lhs.selected == rhs.selected && lhs.values == rhs.values +} + +fileprivate func prepareTransition(left:[ValuesSelectorEntry], right: [ValuesSelectorEntry], initialSize:NSSize, arguments: ValuesSelectorArguments) -> TableUpdateTransition { + let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in + return entry.item(arguments, initialSize: initialSize) + } + return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: false) +} + +struct ValuesSelectorValue : Equatable where T : Equatable { + let localized: String + let value: T + init(localized: String, value: T) { + self.localized = localized + self.value = value + } +} + +func ==(lhs: ValuesSelectorValue, rhs: ValuesSelectorValue) -> Bool { + return lhs.value == rhs.value +} + +class ValuesSelectorModalController: ModalViewController where T : Equatable { + + + private func complete() { + if let selected = stateValue.modify({$0}).selected { + self.onComplete(selected) + } + close() + } + + override var modalHeader: (left: ModalHeaderData?, center: ModalHeaderData?, right: ModalHeaderData?)? { + return (left: ModalHeaderData(image: theme.icons.modalClose, handler: { [weak self] in + self?.close() + }), center: ModalHeaderData(title: self.title), right: nil) + } + + + override func viewClass() -> AnyClass { + return ValuesSelectorModalView.self + } + + private let onComplete:(ValuesSelectorValue)->Void + private let disposable = MetaDisposable() + private let title: String + private let stateValue: Atomic> + init(values: [ValuesSelectorValue], selected: ValuesSelectorValue?, title: String, onComplete:@escaping(ValuesSelectorValue)->Void) { + self.stateValue = Atomic(value: ValuesSelectorState(selected: nil, values: values)) + self.onComplete = onComplete + self.title = title + super.init(frame: NSMakeRect(0, 0, 350, 100)) + self.bar = .init(height: 0) + } + + override func viewDidLoad() { + super.viewDidLoad() + + genericView.hasSearch(self.stateValue.with { $0.values.count > 10 }) + + let search:ValuePromise = ValuePromise(SearchState(state: .None, request: nil), ignoreRepeated: true) + + let searchInteractions = SearchInteractions({ s, _ in + search.set(s) + }, { s in + search.set(s) + }) + + + genericView.searchView.searchInteractions = searchInteractions + + let statePromise: ValuePromise> = ValuePromise(ignoreRepeated: true) + let stateValue = self.stateValue + let updateState:((ValuesSelectorState)->ValuesSelectorState) -> Void = { f in + statePromise.set(stateValue.modify(f)) + } + + updateState { current in + return current + } + + + let arguments = ValuesSelectorArguments(selectItem: { [weak self] selected in + updateState { current in + return current.withUpdatedSelected(selected) + } + self?.complete() + }) + + let initialSize = self.atomicSize + + let previous: Atomic<[ValuesSelectorEntry]> = Atomic(value: []) + + let signal: Signal = combineLatest(statePromise.get() |> deliverOnPrepareQueue, search.get() |> deliverOnPrepareQueue) |> map { state, search in + + var entries:[ValuesSelectorEntry] = [] + var index: Int32 = 0 + var sectionId: Int32 = 0 + +// entries.append(.sectionId(sectionId: sectionId)) +// sectionId += 1 + + let values = state.values.filter { value in + let result = value.localized.split(separator: " ").filter({$0.lowercased().hasPrefix(search.request.lowercased())}) + return search.request.isEmpty || !result.isEmpty + } + for value in values { + entries.append(ValuesSelectorEntry.value(sectionId: sectionId, index: index, value: value, selected: state.selected == value, viewType: .legacy)) + index += 1 + } + entries.append(.sectionId(sectionId: sectionId)) + sectionId += 1 + + return prepareTransition(left: previous.swap(entries), right: entries, initialSize: initialSize.modify{$0}, arguments: arguments) + } |> deliverOnMainQueue + + disposable.set(signal.start(next: { [weak self] transition in + guard let `self` = self else {return} + self.genericView.tableView.merge(with: transition) + self.readyOnce() + })) + + } + + override func becomeFirstResponder() -> Bool? { + return false + } + + override func firstResponder() -> NSResponder? { + return genericView.searchView.isHidden ? nil : genericView.searchView.input + } + + private func updateSize(_ width: CGFloat, animated: Bool) { + if let contentSize = self.window?.contentView?.frame.size { + self.modal?.resize(with:NSMakeSize(width, min(contentSize.height - 150, genericView.tableView.listHeight + 50)), animated: animated) + } + } + + override func measure(size: NSSize) { + self.modal?.resize(with:NSMakeSize(genericView.frame.width, min(size.height - 150, genericView.tableView.listHeight + self.stateValue.with { $0.values.count > 10 ? 50 : 0 })), animated: false) + } + + override func returnKeyAction() -> KeyHandlerResult { + complete() + return .invoked + } + + override var dynamicSize: Bool { + return true + } + + private var genericView:ValuesSelectorModalView { + return self.view as! ValuesSelectorModalView + } + + deinit { + disposable.dispose() + } + +} diff --git a/Telegram-Mac/VerticalTabsView.swift b/Telegram-Mac/VerticalTabsView.swift new file mode 100644 index 0000000000..5ed3fa7bcb --- /dev/null +++ b/Telegram-Mac/VerticalTabsView.swift @@ -0,0 +1,14 @@ +// +// VerticalTabsView.swift +// Telegram +// +// Created by Mikhail Filimonov on 18.03.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +class VerticalTabsView: View { + +} diff --git a/Telegram-Mac/VideoAvatarModalController.swift b/Telegram-Mac/VideoAvatarModalController.swift new file mode 100644 index 0000000000..83654dcddc --- /dev/null +++ b/Telegram-Mac/VideoAvatarModalController.swift @@ -0,0 +1,868 @@ +// +// VideoAvatarModalController.swift +// Telegram +// +// Created by Mikhail Filimonov on 11/06/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore +import Postbox + +import AVKit +import SwiftSignalKit + + +private var magicNumber: CGFloat { + return 8 / 370 +} + +private final class VideoAvatarKeyFramePreviewView: Control { + private let imageView: ImageView = ImageView() + private let flash: View = View() + fileprivate var keyFrame: CGFloat? + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(imageView) + addSubview(flash) + flash.backgroundColor = .white + flash.frame = bounds + imageView.frame = bounds + imageView.animates = true + layout() + } + + func update(with image: CGImage?, value: CGFloat?, animated: Bool, completion: @escaping(Bool)->Void) { + imageView.image = image + self.keyFrame = value + if animated { + flash.layer?.animateAlpha(from: 1, to: 0, duration: 0.8, timingFunction: .easeIn, removeOnCompletion: false, completion: { [weak self] completed in + self?.flash.removeFromSuperview() + completion(completed) + }) + } else { + flash.removeFromSuperview() + } + } + + + + override func layout() { + super.layout() + imageView.frame = bounds + layer?.cornerRadius = frame.width / 2 + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private final class VideoAvatarModalView : View { + private var avPlayer: AVPlayerView + private var videoSize: NSSize = .zero + private let playerContainer: View = View() + private var keyFramePreview: VideoAvatarKeyFramePreviewView? + private var keyFrameDotView: View? + private let controls: View = View() + + fileprivate let ok: TitleButton = TitleButton() + fileprivate let cancel: TitleButton = TitleButton() + + fileprivate let scrubberView: VideoEditorScrubblerControl = VideoEditorScrubblerControl(frame: .zero) + fileprivate let selectionRectView: SelectionRectView + + + private let descView: TextView = TextView() + + required init(frame frameRect: NSRect) { + selectionRectView = SelectionRectView(frame: NSMakeRect(0, 0, frameRect.width, frameRect.height)) + avPlayer = AVPlayerView(frame: NSMakeRect(0, 0, frameRect.width, frameRect.height)) + super.init(frame: frameRect) + playerContainer.addSubview(avPlayer) + avPlayer.controlsStyle = .none + + playerContainer.addSubview(selectionRectView) + controls.addSubview(scrubberView) + selectionRectView.isCircleCap = true + selectionRectView.dimensions = .square + + + addSubview(playerContainer) + addSubview(controls) + + self.addSubview(ok) + self.addSubview(cancel) + + addSubview(descView) + + descView.userInteractionEnabled = false + descView.isSelectable = false + descView.disableBackgroundDrawing = true + + + cancel.set(background: .grayText, for: .Normal) + ok.set(background: .accent, for: .Normal) + + cancel.set(background: NSColor.grayText.withAlphaComponent(0.8), for: .Highlight) + ok.set(background: NSColor.accent.highlighted, for: .Highlight) + + + cancel.set(color: .white, for: .Normal) + cancel.set(text: L10n.videoAvatarButtonCancel, for: .Normal) + + + ok.set(color: .white, for: .Normal) + ok.set(text: L10n.videoAvatarButtonSet, for: .Normal) + + _ = cancel.sizeToFit(.zero, NSMakeSize(80, 20), thatFit: true) + _ = ok.sizeToFit(.zero, NSMakeSize(80, 20), thatFit: true) + + cancel.layer?.cornerRadius = .cornerRadius + ok.layer?.cornerRadius = .cornerRadius + + let shadow = NSShadow() + shadow.shadowBlurRadius = 5 + shadow.shadowColor = NSColor.black.withAlphaComponent(0.2) + shadow.shadowOffset = NSMakeSize(0, 2) + self.cancel.shadow = shadow + self.ok.shadow = shadow + + // cancel.set(image: NSImage(named: "Icon_VideoPlayer_Close")!.precomposed(.white), for: .Normal) + // ok.set(image: NSImage(named: "Icon_SaveEditedMessage")!.precomposed(.accent), for: .Normal) + + setFrameSize(frame.size) + layout() + + + } + + func updateKeyFrameImage(_ image: CGImage?) { + keyFramePreview?.update(with: image, value: keyFramePreview?.keyFrame, animated: false, completion: { _ in }) + } + + func setKeyFrame(value: CGFloat?, highRes: CGImage? = nil, lowRes: CGImage? = nil, animated: Bool, completion: @escaping(Bool)->Void = { _ in}, moveToCurrentKeyFrame: @escaping(CGFloat)->Void = { _ in }) -> Void { + if let keyFramePreview = self.keyFramePreview { + self.keyFramePreview = nil + keyFramePreview.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak keyFramePreview] _ in + keyFramePreview?.removeFromSuperview() + }) + } + if let keyFrameDotView = self.keyFrameDotView { + self.keyFrameDotView = nil + keyFrameDotView.layer?.animateAlpha(from: 1, to: 0, duration: 0.2, removeOnCompletion: false, completion: { [weak keyFrameDotView] _ in + keyFrameDotView?.removeFromSuperview() + }) + } + if let value = value { + let point = self.convert(selectionRectView.selectedRect.origin, from: selectionRectView) + let size = selectionRectView.selectedRect.size + let keyFramePreview = VideoAvatarKeyFramePreviewView(frame: CGRect(origin: point, size: size)) + + + keyFramePreview.set(handler: { _ in + moveToCurrentKeyFrame(value) + }, for: .Click) + + keyFramePreview.update(with: highRes, value: value, animated: animated, completion: { [weak self, weak keyFramePreview] completed in + + if !completed { + keyFramePreview?.removeFromSuperview() + completion(completed) + return + } + + guard let `self` = self, let keyFramePreview = keyFramePreview else { + return + } + + let keyFrameDotView = View() + + + self.addSubview(keyFrameDotView) + keyFrameDotView.backgroundColor = .white + keyFrameDotView.layer?.cornerRadius = 3 + + + let point = NSMakePoint(self.controls.frame.minX + self.scrubberView.frame.minX + value * self.scrubberView.frame.width - 15 + 2, self.controls.frame.maxY - self.scrubberView.frame.height - 30 - 14) + + keyFrameDotView.frame = NSMakeRect(self.controls.frame.minX + self.scrubberView.frame.minX + (value * self.scrubberView.frame.width) - 3 + 2, self.controls.frame.maxY - self.scrubberView.frame.height - 10, 6, 6) + + keyFramePreview.layer?.animateScale(from: 1, to: 30 / keyFramePreview.frame.width, duration: 0.23, removeOnCompletion: false) + keyFramePreview.layer?.animatePosition(from: keyFramePreview.frame.origin, to: point, duration: 0.3, removeOnCompletion: false, completion: { [weak self, weak keyFramePreview, weak keyFrameDotView] complete in + + keyFramePreview?.update(with: lowRes, value: value, animated: false, completion: { _ in }) + keyFramePreview?.frame = CGRect(origin: point, size: NSMakeSize(30, 30)) + keyFramePreview?.layer?.removeAllAnimations() + + self?.keyFrameDotView = keyFrameDotView + self?.keyFramePreview = keyFramePreview + + if !complete { + keyFrameDotView?.removeFromSuperview() + keyFramePreview?.removeFromSuperview() + } + + completion(complete) + }) + + + + keyFrameDotView.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + }) + self.addSubview(keyFramePreview) + } + } + + var playerSize: NSSize { + return playerContainer.frame.size + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + private var localize: String? + + func update(_ player: AVPlayer, localize: String, size: NSSize) { + self.avPlayer.player = player + self.videoSize = size + self.localize = localize + setFrameSize(frame.size) + layout() + + let size = NSMakeSize(200, 200).aspectFitted(playerContainer.frame.size) + let rect = playerContainer.focus(size) + selectionRectView.minimumSize = size.aspectFitted(NSMakeSize(150, 150)) + selectionRectView.applyRect(rect, force: true, dimensions: .square) + } + + func play() { + self.avPlayer.player?.play() + + } + func stop() { + self.avPlayer.player?.pause() + self.avPlayer.player = nil + } + + override func setFrameSize(_ newSize: NSSize) { + let oldSize = self.frame.size + super.setFrameSize(newSize) + + let videoContainerSize = videoSize.aspectFitted(NSMakeSize(frame.width, frame.height - 200)) + let oldVideoContainerSize = playerContainer.frame.size + playerContainer.setFrameSize(videoContainerSize) + + + if oldSize != newSize, oldSize != NSZeroSize, inLiveResize { + let multiplier = NSMakeSize(videoContainerSize.width / oldVideoContainerSize.width, videoContainerSize.height / oldVideoContainerSize.height) + selectionRectView.applyRect(selectionRectView.selectedRect.apply(multiplier: multiplier)) + } + + avPlayer.frame = playerContainer.bounds + selectionRectView.frame = playerContainer.bounds + controls.setFrameSize(NSMakeSize(370, 44)) + scrubberView.setFrameSize(controls.frame.size) + + + + if let localize = localize { + let descLayout = TextViewLayout.init(.initialize(string: localize, color: .white, font: .normal(.text)), maximumNumberOfLines: 1) + descLayout.measure(width: frame.width) + descView.update(descLayout) + } + } + + override func layout() { + super.layout() + + playerContainer.centerX(y: floorToScreenPixels(backingScaleFactor, (frame.height - 184) - playerContainer.frame.height) / 2) + controls.centerX(y: frame.height - controls.frame.height - 100) + scrubberView.centerX(y: controls.frame.height - scrubberView.frame.height) + + ok.centerX(y: frame.height - ok.frame.height - 30, addition: 7 + ok.frame.width / 2) + cancel.centerX(y: frame.height - cancel.frame.height - 30, addition: -(7 + cancel.frame.width / 2)) + + descView.centerX(y: controls.frame.maxY + 15) + + + if let keyFramePreview = keyFramePreview, let keyFrameDotView = keyFrameDotView, let value = keyFramePreview.keyFrame { + let point = NSMakePoint(self.controls.frame.minX + self.scrubberView.frame.minX + value * self.scrubberView.frame.width - 15 + 2, self.controls.frame.maxY - self.scrubberView.frame.height - 30 - 14) + + keyFrameDotView.frame = NSMakeRect(self.controls.frame.minX + self.scrubberView.frame.minX + (value * self.scrubberView.frame.width) - 3 + 2, self.controls.frame.maxY - self.scrubberView.frame.height - 10, 6, 6) + keyFramePreview.frame = CGRect(origin: point, size: NSMakeSize(30, 30)) + + } + + } +} + + +enum VideoAvatarGeneratorState : Equatable { + case start(thumb: String) + case progress(Float) + case complete(thumb: String, video: String, keyFrame: Double?) + case error +} + + +class VideoAvatarModalController: ModalViewController { + private let context: AccountContext + fileprivate let videoSize: NSSize + fileprivate let player: AVPlayer + fileprivate let item: AVPlayerItem + fileprivate let asset: AVComposition + fileprivate let track: AVAssetTrack + fileprivate var appliedKeyFrame: CGFloat? = nil + + private let updateThumbsDisposable = MetaDisposable() + private let rectDisposable = MetaDisposable() + private let valuesDisposable = MetaDisposable() + private let keyFrameGeneratorDisposable = MetaDisposable() + + fileprivate let scrubberValues:Atomic = Atomic(value: VideoScrubberValues(movePos: 0, keyFrame: nil, leftTrim: 0, rightTrim: 1.0, minDist: 0, maxDist: 1, paused: true, suspended: false)) + fileprivate let _scrubberValuesSignal: ValuePromise = ValuePromise(ignoreRepeated: true) + var scrubberValuesSignal: Signal { + return _scrubberValuesSignal.get() |> deliverOnMainQueue + } + + fileprivate func updateValues(_ f: (VideoScrubberValues)->VideoScrubberValues) { + _scrubberValuesSignal.set(scrubberValues.modify(f)) + } + private var firstTime: Bool = true + private var timeObserverToken: Any? + + var completeState: Signal { + return state.get() + } + + private var state: Promise = Promise() + private let localize: String + private let quality: String + init(context: AccountContext, asset: AVComposition, track: AVAssetTrack, localize: String, quality: String) { + self.context = context + self.asset = asset + self.track = track + self.quality = quality + let size = track.naturalSize.applying(track.preferredTransform) + self.videoSize = NSMakeSize(abs(size.width), abs(size.height)) + self.item = AVPlayerItem(asset: asset) + + self.player = AVPlayer(playerItem: item) + + let videoComposition = AVMutableVideoComposition() + videoComposition.renderSize = videoSize + videoComposition.frameDuration = CMTimeMake(value: 1, timescale: 30) + let transformer = AVMutableVideoCompositionLayerInstruction(assetTrack: track) + let instruction = AVMutableVideoCompositionInstruction() + instruction.timeRange = CMTimeRangeMake(start: CMTime.zero, duration: self.asset.duration) + let transform1: CGAffineTransform = track.preferredTransform + transformer.setTransform(transform1, at: CMTime.zero) + instruction.layerInstructions = [transformer] + videoComposition.instructions = [instruction] + self.item.videoComposition = videoComposition + + self.localize = localize + super.init(frame: CGRect(origin: .zero, size: context.window.contentView!.frame.size - NSMakeSize(20, 20))) + self.bar = .init(height: 0) + } + + override open func measure(size: NSSize) { + if let contentSize = self.modal?.window.contentView?.frame.size { + self.modal?.resize(with: contentSize - NSMakeSize(20, 20), animated: false) + } + } + + func updateSize(_ animated: Bool) { + if let contentSize = self.modal?.window.contentView?.frame.size { + self.modal?.resize(with: contentSize - NSMakeSize(20, 20), animated: animated) + } + } + + + override var dynamicSize: Bool { + return true + } + + override var background: NSColor { + return .clear + } + + override var containerBackground: NSColor { + return .clear + } + override var isVisualEffectBackground: Bool { + return true + } + + override func viewClass() -> AnyClass { + return VideoAvatarModalView.self + } + + private var genericView: VideoAvatarModalView { + return self.view as! VideoAvatarModalView + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + player.pause() + + if let timeObserverToken = timeObserverToken { + player.removeTimeObserver(timeObserverToken) + self.timeObserverToken = nil + } + } + + override func returnKeyAction() -> KeyHandlerResult { + self.state.set(generateVideo(asset, composition: self.currentVideoComposition(), quality: self.quality, values: self.scrubberValues.with { $0 })) + close() + + return .invoked + } + + + private func currentVideoComposition() -> AVVideoComposition { + let size = self.videoSize + let naturalSize = self.asset.naturalSize + + enum Orientation { + case up, down, right, left + } + + func orientation(for track: AVAssetTrack) -> Orientation { + let t = track.preferredTransform + + if(t.a == 0 && t.b == 1.0 && t.c == -1.0 && t.d == 0) { + return .up + } else if(t.a == 0 && t.b == -1.0 && t.c == 1.0 && t.d == 0) { + return .down + } else if(t.a == 1.0 && t.b == 0 && t.c == 0 && t.d == 1.0) { + return .right + } else if(t.a == -1.0 && t.b == 0 && t.c == 0 && t.d == -1.0) { + return .left + } else { + return .up + } + } + + func roundSize(_ numToRound: CGFloat) -> CGFloat { + let numToRound = Int(numToRound) + let remainder = numToRound % 16; + if (remainder == 0) { + return CGFloat(numToRound) + } + return CGFloat((numToRound - 16) + (16 - remainder)); + } + + let rotation: Orientation = orientation(for: track) + + var selectedRect = self.genericView.selectionRectView.selectedRect + let viewSize = self.genericView.playerSize + let coefficient = NSMakeSize(size.width / viewSize.width, size.height / viewSize.height) + + selectedRect = selectedRect.apply(multiplier: coefficient) + + selectedRect.size = NSMakeSize(min(selectedRect.width, selectedRect.height), min(selectedRect.width, selectedRect.height)) + + let videoComposition = AVMutableVideoComposition() + + + + videoComposition.renderSize = NSMakeSize(roundSize(selectedRect.width), roundSize(selectedRect.height)) + videoComposition.frameDuration = CMTimeMake(value: 1, timescale: 30) + + let transformer = AVMutableVideoCompositionLayerInstruction(assetTrack: track) + let instruction = AVMutableVideoCompositionInstruction() + + instruction.timeRange = CMTimeRangeMake(start: CMTime.zero, duration: self.asset.duration) + + let point = selectedRect.origin + var finalTransform: CGAffineTransform = CGAffineTransform.identity + + switch rotation { + case .down: + finalTransform = finalTransform + .translatedBy(x: -point.x, y: naturalSize.width - point.y) + .rotated(by: -.pi / 2) + case .left: + finalTransform = finalTransform + .translatedBy(x: naturalSize.width - point.x, y: naturalSize.height - point.y) + .rotated(by: .pi) + case .right: + finalTransform = finalTransform + .translatedBy(x: -point.x, y: -point.y) + .rotated(by: 0) + case .up: + finalTransform = finalTransform + .translatedBy(x: naturalSize.height - point.x, y: -point.y) + .rotated(by: .pi / 2) + } + + transformer.setTransform(finalTransform, at: CMTime.zero) + + instruction.layerInstructions = [transformer] + videoComposition.instructions = [instruction] + + return videoComposition + } + + + deinit { + rectDisposable.dispose() + updateThumbsDisposable.dispose() + valuesDisposable.dispose() + keyFrameGeneratorDisposable.dispose() + NotificationCenter.default.removeObserver(self.item) + + if let timeObserverToken = timeObserverToken { + player.removeTimeObserver(timeObserverToken) + self.timeObserverToken = nil + } + } + + private var generatedRect: NSRect? = nil + + private func updateUserInterface(_ firstTime: Bool) { + let size = NSMakeSize(genericView.scrubberView.frame.height, genericView.scrubberView.frame.height) + + let signal = generateVideoScrubberThumbs(for: asset, composition: currentVideoComposition(), size: size, count: Int(ceil(genericView.scrubberView.frame.width / size.width)), gradually: true, blur: true) + |> delay(0.2, queue: .concurrentDefaultQueue()) + + + let duration = CMTimeGetSeconds(asset.duration) + + let keyFrame = scrubberValues.with { $0.keyFrame } + + let keyFrameSignal: Signal + + if let keyFrame = keyFrame { + keyFrameSignal = generateVideoAvatarPreview(for: asset, composition: self.currentVideoComposition(), highSize: genericView.selectionRectView.selectedRect.size, lowSize: NSMakeSize(30, 30), at: Double(keyFrame) * duration) + |> delay(0.2, queue: .concurrentDefaultQueue()) + |> map { $0.0 } + } else { + keyFrameSignal = .single(nil) + } + + var selectedRect = self.genericView.selectionRectView.selectedRect + let viewSize = self.genericView.playerSize + let coefficient = NSMakeSize(size.width / viewSize.width, size.height / viewSize.height) + + selectedRect = selectedRect.apply(multiplier: coefficient) + + if generatedRect != selectedRect { + updateThumbsDisposable.set(combineLatest(queue: .mainQueue(), signal, keyFrameSignal).start(next: { [weak self] images, keyFrame in + self?.genericView.scrubberView.render(images.0, size: size) + self?.genericView.updateKeyFrameImage(keyFrame) + if self?.firstTime == true { + self?.firstTime = !images.1 + } + self?.generatedRect = selectedRect + })) + } + } + private func applyValuesToPlayer(_ values: VideoScrubberValues) { + if values.movePos > values.rightTrim - (magicNumber + (magicNumber / 2)), !values.paused { + play() + } + if values.paused { + player.rate = 0 + seekToNormal(values) + player.pause() + } else if player.rate == 0, !values.paused { + player.rate = 1 + play() + } + if let keyFrame = values.keyFrame, appliedKeyFrame != keyFrame { + self.runKeyFrameUpdater(keyFrame) + seekToNormal(values) + } else if appliedKeyFrame != nil && values.keyFrame == nil { + self.genericView.setKeyFrame(value: nil, animated: true) + self.appliedKeyFrame = nil + } + } + @discardableResult private func seekToNormal(_ values: VideoScrubberValues) -> CGFloat? { + let duration = CMTimeGetSeconds(asset.duration) + if values.suspended { + self.player.seek(to: CMTimeMakeWithSeconds(TimeInterval(values.movePos) * duration, preferredTimescale: 1000), toleranceBefore: .zero, toleranceAfter: .zero) + return values.keyFrame + } else { + self.player.seek(to: CMTimeMakeWithSeconds(TimeInterval(values.leftTrim + magicNumber) * duration, preferredTimescale: 1000), toleranceBefore: .zero, toleranceAfter: .zero) + return nil + } + } + + private func play() { + player.pause() + let duration = CMTimeGetSeconds(asset.duration) + + if let timeObserverToken = timeObserverToken { + player.removeTimeObserver(timeObserverToken) + self.timeObserverToken = nil + } + + _ = self.scrubberValues.modify { values in + let values = values.withUpdatedPaused(false) + if let result = self.seekToNormal(values) { + return values.withUpdatedMove(result) + } else { + return values.withUpdatedMove(values.leftTrim) + } + } + + let timeScale = CMTimeScale(NSEC_PER_SEC) + let time = CMTime(seconds: 0.016 * 2, preferredTimescale: timeScale) + + + timeObserverToken = player.addPeriodicTimeObserver(forInterval: time, queue: .main) { [weak self] time in + self?.updateValues { current in + if !current.suspended { + return current.withUpdatedMove(CGFloat(CMTimeGetSeconds(time) / duration)) + } else { + return current + } + } + } + + + self.player.play() + } + + private func runKeyFrameUpdater(_ keyFrame: CGFloat) { + let duration = CMTimeGetSeconds(asset.duration) + + let size = genericView.selectionRectView.selectedRect.size + + let signal = generateVideoAvatarPreview(for: self.asset, composition: self.currentVideoComposition(), highSize: size, lowSize: NSMakeSize(30, 30), at: Double(keyFrame) * duration) + |> deliverOnMainQueue + + keyFrameGeneratorDisposable.set(signal.start(next: { [weak self] highRes, lowRes in + self?.genericView.setKeyFrame(value: keyFrame, highRes: highRes, lowRes: lowRes, animated: true, completion: { [weak self] completed in + if completed { + self?.updateValues { + $0.withUpdatedPaused(false) + .withUpdatedMove(keyFrame) + } + self?.updateValues { + $0.withUpdatedSuspended(false) + } + } else { + self?.updateValues { + $0.withUpdatedSuspended(false) + .withUpdatedPaused(false) + } + } + + }, moveToCurrentKeyFrame: { [weak self] keyFrame in + self?.updateValues { + $0.withUpdatedSuspended(true) + .withUpdatedMove(keyFrame) + } + self?.updateValues { [weak self] values in + self?.seekToNormal(values) + return values + } + self?.updateValues { + $0.withUpdatedSuspended(false) + } + }) + self?.appliedKeyFrame = keyFrame + })) + } + + override var closable: Bool { + return false + } + + override func viewDidLoad() { + super.viewDidLoad() + + + genericView.update(self.player, localize: self.localize, size: self.videoSize) + + genericView.cancel.set(handler: { [weak self] _ in + self?.close() + }, for: .Click) + + genericView.ok.set(handler: { [weak self] _ in + _ = self?.returnKeyAction() + }, for: .Click) + + let duration = CMTimeGetSeconds(asset.duration) + + let scrubberSize = genericView.scrubberView.frame.width + + let valueSec = (scrubberSize / CGFloat(duration)) / scrubberSize + + self.updateValues { values in + return values.withUpdatedMinDist(valueSec).withUpdatedMaxDist(valueSec * 10.0).withUpdatedrightTrim(min(1, valueSec * 10.0)) + } + + genericView.scrubberView.updateValues = { [weak self] values in + self?.updateValues { _ in + return values + } + } + + rectDisposable.set(genericView.selectionRectView.updatedRect.start(next: { [weak self] rect in + self?.genericView.selectionRectView.applyRect(rect, force: true, dimensions: .square) + self?.updateUserInterface(self?.firstTime ?? false) + })) + + valuesDisposable.set(self.scrubberValuesSignal.start(next: { [weak self] values in + self?.genericView.scrubberView.apply(values: values) + self?.applyValuesToPlayer(values) + })) + + NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: self.item, queue: .main) { [weak self] _ in + self?.play() + } + + play() + readyOnce() + } + +} + + + +func selectVideoAvatar(context: AccountContext, path: String, localize: String, quality: String = AVAssetExportPresetMediumQuality, signal:@escaping(Signal)->Void) { + let asset = AVURLAsset(url: URL(fileURLWithPath: path)) + let track = asset.tracks(withMediaType: .video).first + if let track = track { + let composition = AVMutableComposition() + guard let compositionVideoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid) else { + return + } + do { + try compositionVideoTrack.insertTimeRange(CMTimeRangeMake(start: .zero, duration: asset.duration), of: track, at: .zero) + let controller = VideoAvatarModalController(context: context, asset: composition, track: track, localize: localize, quality: quality) + showModal(with: controller, for: context.window) + signal(controller.completeState) + } catch { + + } + } +} + + +private func generateVideo(_ asset: AVComposition, composition: AVVideoComposition, quality: String, values: VideoScrubberValues) -> Signal { + return Signal { subscriber in + + let exportSession = AVAssetExportSession(asset: asset, presetName: quality)! + exportSession.outputFileType = .mp4 + exportSession.shouldOptimizeForNetworkUse = true + + let videoPath = NSTemporaryDirectory() + "\(arc4random()).mp4" + let thumbPath = NSTemporaryDirectory() + "\(arc4random()).jpg" + + + let imageGenerator = AVAssetImageGenerator(asset: asset) + imageGenerator.maximumSize = CGSize(width: 640, height: 640) + imageGenerator.appliesPreferredTrackTransform = true + imageGenerator.requestedTimeToleranceBefore = .zero + imageGenerator.requestedTimeToleranceAfter = .zero + + + imageGenerator.videoComposition = composition + let image = try? imageGenerator.copyCGImage(at: CMTimeMakeWithSeconds(Double(values.keyFrame ?? values.leftTrim) * asset.duration.seconds, preferredTimescale: 1000), actualTime: nil) + if let image = image { + let options = NSMutableDictionary() + options.setValue(640 as NSNumber, forKey: kCGImageDestinationImageMaxPixelSize as String) + options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailWithTransform as String) + + let colorQuality: Float = 0.3 + options.setObject(colorQuality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString) + + + let mutableData: CFMutableData = NSMutableData() as CFMutableData + let colorDestination = CGImageDestinationCreateWithData(mutableData, kUTTypeJPEG, 1, options)! + CGImageDestinationSetProperties(colorDestination, nil) + + CGImageDestinationAddImage(colorDestination, image, options as CFDictionary) + CGImageDestinationFinalize(colorDestination) + + try? (mutableData as Data).write(to: URL(fileURLWithPath: thumbPath)) + + subscriber.putNext(.start(thumb: thumbPath)) + + } + + exportSession.outputURL = URL(fileURLWithPath: videoPath) + + exportSession.videoComposition = composition + + + + let wholeDuration = CMTimeGetSeconds(asset.duration) + + let from = TimeInterval(values.leftTrim) * wholeDuration + let to = TimeInterval(values.rightTrim) * wholeDuration + + let start = CMTimeMakeWithSeconds(from, preferredTimescale: 1000) + let duration = CMTimeMakeWithSeconds(to - from, preferredTimescale: 1000) + + if #available(OSX 10.14, *) { + exportSession.fileLengthLimit = 2 * 1024 * 1024 + } + + let timer = SwiftSignalKit.Timer(timeout: 0.05, repeat: true, completion: { + subscriber.putNext(.progress(exportSession.progress)) + }, queue: .concurrentBackgroundQueue()) + + exportSession.timeRange = CMTimeRangeMake(start: start, duration: duration) + + + exportSession.exportAsynchronously(completionHandler: { [weak exportSession] in + + timer.invalidate() + + if let exportSession = exportSession, exportSession.status == .completed, exportSession.error == nil { + subscriber.putNext(.complete(thumb: thumbPath, video: videoPath, keyFrame: values.keyFrame != nil ? Double(values.keyFrame!) * asset.duration.seconds : nil)) + subscriber.putCompletion() + + } else { + subscriber.putNext(.error) + subscriber.putCompletion() + } + + }) + + + + timer.start() + + return ActionDisposable { + exportSession.cancelExport() + timer.invalidate() + } + } |> runOn(.concurrentBackgroundQueue()) +} + + + +/* + - (NSImageOrientation)getVideoOrientationFromAsset:(AVAsset *)asset + { + AVAssetTrack *videoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0]; + CGSize size = [videoTrack naturalSize]; + CGAffineTransform txf = [videoTrack preferredTransform]; + + if (size.width == txf.tx && size.height == txf.ty) + return NSImageOrientationLeft; //return UIInterfaceOrientationLandscapeLeft; + else if (txf.tx == 0 && txf.ty == 0) + return NSImageOrientationRight; //return UIInterfaceOrientationLandscapeRight; + else if (txf.tx == 0 && txf.ty == size.width) + return NSImageOrientationDown; //return UIInterfaceOrientationPortraitUpsideDown; + else + return NSImageOrientationUp; //return UIInterfaceOrientationPortrait; + } + */ diff --git a/Telegram-Mac/VideoCallsConfiguration.swift b/Telegram-Mac/VideoCallsConfiguration.swift new file mode 100644 index 0000000000..9e2df3bb88 --- /dev/null +++ b/Telegram-Mac/VideoCallsConfiguration.swift @@ -0,0 +1,39 @@ +import TelegramCore + +struct VideoCallsConfiguration: Equatable { + enum VideoCallsSupport { + case disabled + case full + case onlyVideo + } + + var videoCallsSupport: VideoCallsSupport + + init(appConfiguration: AppConfiguration) { + var videoCallsSupport: VideoCallsSupport = .full + if let data = appConfiguration.data, let value = data["video_calls_support"] as? String { + switch value { + case "disabled": + videoCallsSupport = .disabled + case "full": + videoCallsSupport = .full + case "only_video": + videoCallsSupport = .onlyVideo + default: + videoCallsSupport = .full + } + } + self.videoCallsSupport = videoCallsSupport + } +} + +extension VideoCallsConfiguration { + var areVideoCallsEnabled: Bool { + switch self.videoCallsSupport { + case .disabled: + return false + case .full, .onlyVideo: + return true + } + } +} diff --git a/Telegram-Mac/VideoCameraStructures.swift b/Telegram-Mac/VideoCameraStructures.swift index 81a7addbaa..acebc9d556 100644 --- a/Telegram-Mac/VideoCameraStructures.swift +++ b/Telegram-Mac/VideoCameraStructures.swift @@ -15,7 +15,7 @@ enum VideoCameraRecordingStatus : Equatable { case madeThumbnail(CGImage) case stoppingRecording case stopped(thumb: CGImage?) - case finishRecording(path:String, duration:Int, thumb: CGImage?) + case finishRecording(path:String, duration:Int, id: Int64?, thumb: CGImage?) } func ==(lhs: VideoCameraRecordingStatus, rhs: VideoCameraRecordingStatus) -> Bool { diff --git a/Telegram-Mac/VideoEditorScrubbler.swift b/Telegram-Mac/VideoEditorScrubbler.swift new file mode 100644 index 0000000000..7f76a4ff12 --- /dev/null +++ b/Telegram-Mac/VideoEditorScrubbler.swift @@ -0,0 +1,436 @@ +// +// VideoEditorScrubbler.swift +// Telegram +// +// Created by Mikhail Filimonov on 16/07/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +private final class ScrubberMoveView: Control { + private let view = View() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + view.isEventLess = true + addSubview(view) + view.layer?.cornerRadius = 2 + view.backgroundColor = .white + } + + + override func layout() { + super.layout() + view.frame = bounds + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private final class ScrubberleftTrim: Control { + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + ctx.round(bounds, flags: [.left, .top, .bottom]) + ctx.setFillColor(NSColor.accent.cgColor) + ctx.fill(bounds) + } + + override func layout() { + super.layout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private final class ScrubberrightTrim: Control { + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + ctx.round(bounds, flags: [.right, .top, .bottom]) + ctx.setFillColor(NSColor.accent.cgColor) + ctx.fill(bounds) + } + + override func layout() { + super.layout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +struct VideoScrubberValues : Equatable { + let movePos: CGFloat + let leftTrim: CGFloat + let rightTrim: CGFloat + let minDist: CGFloat + let maxDist: CGFloat + let paused: Bool + let keyFrame: CGFloat? + let suspended: Bool + init(movePos: CGFloat, keyFrame: CGFloat?, leftTrim: CGFloat, rightTrim: CGFloat, minDist: CGFloat, maxDist: CGFloat, paused: Bool, suspended: Bool) { + self.movePos = movePos + self.keyFrame = keyFrame + self.leftTrim = leftTrim + self.rightTrim = rightTrim + self.minDist = minDist + self.paused = paused + self.maxDist = maxDist + self.suspended = suspended + } + + func withUpdatedMove(_ movePos: CGFloat) -> VideoScrubberValues { + return VideoScrubberValues(movePos: min(max(0, movePos), 1), keyFrame: keyFrame, leftTrim: leftTrim, rightTrim: rightTrim, minDist: minDist, maxDist: maxDist, paused: paused, suspended: suspended) + } + func withUpdatedleftTrim(_ leftTrim: CGFloat) -> VideoScrubberValues { + var keyFrame = self.keyFrame + if let frame = keyFrame, leftTrim > frame { + keyFrame = nil + } + return VideoScrubberValues(movePos: movePos, keyFrame: keyFrame, leftTrim: min(max(0, leftTrim), 1), rightTrim: rightTrim, minDist: minDist, maxDist: maxDist, paused: paused, suspended: suspended) + } + func withUpdatedrightTrim(_ rightTrim: CGFloat) -> VideoScrubberValues { + var keyFrame = self.keyFrame + if let frame = keyFrame, rightTrim < frame { + keyFrame = nil + } + return VideoScrubberValues(movePos: movePos, keyFrame: keyFrame, leftTrim: leftTrim, rightTrim: min(max(0, rightTrim), 1), minDist: minDist, maxDist: maxDist, paused: paused, suspended: suspended) + } + func withUpdatedMinDist(_ minDist: CGFloat) -> VideoScrubberValues { + return VideoScrubberValues(movePos: movePos, keyFrame: keyFrame, leftTrim: leftTrim, rightTrim: rightTrim, minDist: minDist, maxDist: maxDist, paused: paused, suspended: suspended) + } + func withUpdatedMaxDist(_ maxDist: CGFloat) -> VideoScrubberValues { + return VideoScrubberValues(movePos: movePos, keyFrame: keyFrame, leftTrim: leftTrim, rightTrim: rightTrim, minDist: minDist, maxDist: maxDist, paused: paused, suspended: suspended) + } + func withUpdatedPaused(_ paused: Bool) -> VideoScrubberValues { + return VideoScrubberValues(movePos: movePos, keyFrame: keyFrame, leftTrim: leftTrim, rightTrim: rightTrim, minDist: minDist, maxDist: maxDist, paused: paused, suspended: suspended) + } + + func withUpdatedKeyFrame(_ keyFrame: CGFloat?) -> VideoScrubberValues { + return VideoScrubberValues(movePos: movePos, keyFrame: keyFrame, leftTrim: leftTrim, rightTrim: rightTrim, minDist: minDist, maxDist: maxDist, paused: paused, suspended: suspended) + } + func withUpdatedSuspended(_ suspended: Bool) -> VideoScrubberValues { + return VideoScrubberValues(movePos: movePos, keyFrame: keyFrame, leftTrim: leftTrim, rightTrim: rightTrim, minDist: minDist, maxDist: maxDist, paused: paused, suspended: suspended) + } +} + +class VideoEditorScrubblerControl : View, ViewDisplayDelegate { + private let scrubber: ScrubberMoveView + private var imageViewsContainer: View = View() + private let overlay: View = View() + private let distance = Control() + private let leftTrim: ScrubberleftTrim = ScrubberleftTrim(frame: .zero) + private let rightTrim: ScrubberrightTrim = ScrubberrightTrim(frame: .zero) + + private var values = VideoScrubberValues(movePos: 0, keyFrame: nil, leftTrim: 0, rightTrim: 1.0, minDist: 0, maxDist: 1, paused: false, suspended: false) + + var updateValues:((VideoScrubberValues)->Void)? = nil + + required init(frame frameRect: NSRect) { + scrubber = ScrubberMoveView(frame: NSMakeRect(0, 0, 4, frameRect.height)) + super.init(frame: frameRect) + addSubview(self.imageViewsContainer) + imageViewsContainer.layer?.cornerRadius = .cornerRadius + imageViewsContainer.backgroundColor = NSColor.black.withAlphaComponent(0.85) + addSubview(self.scrubber) + + overlay.isEventLess = true + overlay.displayDelegate = self + overlay.layer?.cornerRadius = .cornerRadius + addSubview(overlay) + + addSubview(leftTrim) + addSubview(rightTrim) + + addSubview(distance) + + let shadow = NSShadow() + shadow.shadowBlurRadius = 5 + shadow.shadowColor = NSColor.black.withAlphaComponent(0.2) + shadow.shadowOffset = NSMakeSize(0, 2) + self.scrubber.shadow = shadow + + + let shadow1 = NSShadow() + shadow1.shadowBlurRadius = 5 + shadow1.shadowColor = NSColor.black.withAlphaComponent(0.4) + shadow1.shadowOffset = NSMakeSize(0, 2) + self.shadow = shadow1 + + + func possibleDrag(_ value: CGFloat) -> Bool { + return value >= 0 && value <= 1 + } + func checkDist(_ leftValue: CGFloat, _ rightValue: CGFloat, _ min: CGFloat, _ max: CGFloat) -> Bool { + return (leftValue - rightValue) >= min && (leftValue - rightValue) <= max + } + + var leftTrimStart: NSPoint? = nil + var rightTrimStart: NSPoint? = nil + var distanceStart: NSPoint? = nil + + + leftTrim.set(handler: { control in + leftTrimStart = control.window?.mouseLocationOutsideOfEventStream ?? nil + }, for: .Down) + + leftTrim.set(handler: { [weak self] _ in + guard let `self` = self else { + return + } + leftTrimStart = nil + self.updateValues?(self.values.withUpdatedPaused(false)) + }, for: .Up) + + + leftTrim.set(handler: { [weak self] control in + guard let `self` = self, let start = leftTrimStart, let current = control.window?.mouseLocationOutsideOfEventStream else { + return + } + + let difference = start - current + + let width = self.frame.width - control.frame.width + + let newValue = control.frame.origin - difference + + let percent = newValue.x / width + + if checkDist(self.values.rightTrim, percent, self.values.minDist, self.values.maxDist) { + if possibleDrag(percent) { + leftTrimStart = current + control.setFrameOrigin(newValue) + } + if percent != self.values.leftTrim { + self.updateValues?(self.values.withUpdatedleftTrim(percent).withUpdatedPaused(true)) + } + } + }, for: .MouseDragging) + + + rightTrim.set(handler: { control in + rightTrimStart = control.window?.mouseLocationOutsideOfEventStream ?? nil + }, for: .Down) + + rightTrim.set(handler: { [weak self] _ in + guard let `self` = self else { + return + } + rightTrimStart = nil + self.updateValues?(self.values.withUpdatedPaused(false)) + }, for: .Up) + + + rightTrim.set(handler: { [weak self] control in + guard let `self` = self, let start = rightTrimStart, let current = control.window?.mouseLocationOutsideOfEventStream else { + return + } + let difference = start - current + let width = self.frame.width - control.frame.width + + let newValue = control.frame.origin - difference + var percent = newValue.x / width + + if percent < 0 && self.values.rightTrim > 0 { + percent = 0 + } + if percent > 1 && self.values.rightTrim < 1 { + percent = 1 + } + + if checkDist(percent, self.values.leftTrim, self.values.minDist, self.values.maxDist) { + if possibleDrag(newValue.x / width) { + rightTrimStart = current + control.setFrameOrigin(newValue) + } + self.updateValues?(self.values.withUpdatedrightTrim(percent).withUpdatedPaused(true)) + } + }, for: .MouseDragging) + + + distance.set(handler: { control in + distanceStart = control.window?.mouseLocationOutsideOfEventStream ?? nil + }, for: .Down) + + + var chooseFrame: Bool = false + + distance.set(handler: { [weak self] control in + guard let `self` = self else { + return + } + if !self.values.paused { + let point = self.imageViewsContainer.convert(control.window?.mouseLocationOutsideOfEventStream ?? .zero, from: nil) + let keyFrame = min(1, max(0, (point.x - self.scrubber.frame.width / 2) / self.imageViewsContainer.frame.width)) + self.updateValues?(self.values.withUpdatedSuspended(true).withUpdatedPaused(true).withUpdatedMove(keyFrame)) + chooseFrame = true + } + + }, for: .LongMouseDown) + + + distance.set(handler: { [weak self] control in + guard let `self` = self else { + return + } + var values = self.values + let point = self.imageViewsContainer.convert(control.window?.mouseLocationOutsideOfEventStream ?? .zero, from: nil) + let keyFrame = min(values.rightTrim, max(values.leftTrim, (point.x - self.scrubber.frame.width / 2) / self.imageViewsContainer.frame.width)) + if !self.values.suspended { + if !values.paused { + let point = self.imageViewsContainer.convert(control.window?.mouseLocationOutsideOfEventStream ?? .zero, from: nil) + let keyFrame = min(1, max(0, (point.x - self.scrubber.frame.width / 2) / self.imageViewsContainer.frame.width)) + if keyFrame != values.keyFrame { + values = values.withUpdatedKeyFrame(keyFrame).withUpdatedMove(keyFrame).withUpdatedPaused(true).withUpdatedSuspended(true) + } + } else { + values = values.withUpdatedPaused(false) + } + } else if self.values.suspended && chooseFrame { + if keyFrame != values.keyFrame { + values = values + .withUpdatedKeyFrame(keyFrame) + .withUpdatedMove(keyFrame) + .withUpdatedPaused(true) + .withUpdatedSuspended(true) + } else { + values = values + .withUpdatedMove(keyFrame) + .withUpdatedPaused(false) + .withUpdatedSuspended(false) + } + } + self.updateValues?(values) + chooseFrame = false + distanceStart = nil + }, for: .Up) + + + distance.set(handler: { [weak self] control in + guard let `self` = self, let start = distanceStart, let current = control.window?.mouseLocationOutsideOfEventStream else { + return + } + + let difference = start - current + let value = difference.x / (self.frame.width - self.leftTrim.frame.width) + + if chooseFrame { + let updatedValue = self.values.movePos - value + if updatedValue >= self.values.leftTrim && updatedValue <= self.values.rightTrim { + let newValues = self.values.withUpdatedMove(updatedValue) + self.updateValues?(newValues) + distanceStart = current + } + } else { + var leftValue = self.values.leftTrim - value + var rightValue = self.values.rightTrim - value + if leftValue < 0 && self.values.leftTrim > 0 { + leftValue = 0 + } + if leftValue > 1 && self.values.leftTrim < 1 { + leftValue = 1 + } + if rightValue < 0 && self.values.rightTrim > 0 { + rightValue = 0 + } + if rightValue > 1 && self.values.rightTrim < 1 { + rightValue = 1 + } + if possibleDrag(leftValue) && possibleDrag(rightValue) && (leftValue != self.values.leftTrim || rightValue != self.values.rightTrim) { + distanceStart = current + let newValues = self.values.withUpdatedleftTrim(leftValue).withUpdatedrightTrim(rightValue).withUpdatedPaused(true) + self.updateValues?(newValues) + } + } + + + + + + }, for: .MouseDragging) + + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + if layer == overlay.layer { + ctx.setFillColor(NSColor.black.withAlphaComponent(0.85).cgColor) + if values.leftTrim > 0 { + ctx.fill(NSMakeRect(0, 0, leftTrim.frame.maxX, imageViewsContainer.frame.height)) + } + if values.rightTrim < 1 { + ctx.fill(NSMakeRect(rightTrim.frame.minX, 0, imageViewsContainer.frame.width - self.rightTrim.frame.minX, imageViewsContainer.frame.height)) + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func render(_ images: [CGImage], size: NSSize) { + + while imageViewsContainer.subviews.count > images.count { + imageViewsContainer.subviews.removeLast() + } + while imageViewsContainer.subviews.count < images.count { + let view = ImageView() + view.contentGravity = .resizeAspectFill + view.animates = true + imageViewsContainer.addSubview(view) + } + + var x: CGFloat = 0 + + + for (i, image) in images.enumerated() { + (imageViewsContainer.subviews[i] as? ImageView)?.image = image + (imageViewsContainer.subviews[i] as? ImageView)?.frame = NSMakeRect(x, 0, size.width, size.height) + x += size.width + } + } + + func apply(values: VideoScrubberValues) { + let previousPaused = self.values.paused + let previousSuspdended = self.values.suspended + self.values = values + if previousPaused != values.paused || values.suspended != previousSuspdended { + self.scrubber.change(opacity: values.paused && !values.suspended ? 0 : 1, animated: !values.paused) + } + needsLayout = true + overlay.needsDisplay = true + } + + override func layout() { + super.layout() + self.imageViewsContainer.setFrameSize(NSMakeSize(frame.width, frame.height - 4)) + self.imageViewsContainer.center() + + leftTrim.frame = NSMakeRect(values.leftTrim * (frame.width - 8), 2, 8, frame.height - 4) + rightTrim.frame = NSMakeRect(values.rightTrim * (frame.width - 8), 2, 8, frame.height - 4) + + + self.scrubber.frame = CGRect(origin: NSMakePoint(min(max(values.movePos * frame.width, leftTrim.frame.maxX), rightTrim.frame.minX - self.scrubber.frame.width), 0), size: NSMakeSize(4, frame.height)) + + self.distance.frame = NSMakeRect(leftTrim.frame.maxX, imageViewsContainer.frame.minY, imageViewsContainer.frame.width - leftTrim.frame.maxX - (imageViewsContainer.frame.width - rightTrim.frame.minX), imageViewsContainer.frame.height) + + var x: CGFloat = 0 + for view in imageViewsContainer.subviews { + view.setFrameOrigin(NSMakePoint(x, 0)) + x += view.frame.width + } + + overlay.frame = imageViewsContainer.frame + } +} diff --git a/Telegram-Mac/VideoEditorThumbs.swift b/Telegram-Mac/VideoEditorThumbs.swift new file mode 100644 index 0000000000..09e9ccd212 --- /dev/null +++ b/Telegram-Mac/VideoEditorThumbs.swift @@ -0,0 +1,100 @@ +// +// VideoEditorThumbs.swift +// Telegram +// +// Created by Mikhail Filimonov on 16/07/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import SwiftSignalKit +import TGUIKit + +func generateVideoScrubberThumbs(for asset: AVComposition, composition: AVVideoComposition?, size: NSSize, count: Int, gradually: Bool, blur: Bool) -> Signal<([CGImage], Bool), NoError> { + return Signal { subscriber in + + var cancelled = false + + let videoDuration = asset.duration + + let generator = AVAssetImageGenerator(asset: asset) + + let size = size.multipliedByScreenScale() + + var frameForTimes = [NSValue]() + let sampleCounts = count + let totalTimeLength = Int(videoDuration.seconds * Double(videoDuration.timescale)) + let step = totalTimeLength / sampleCounts + + for i in 0 ..< sampleCounts { + let cmTime = CMTimeMake(value: Int64(i * step), timescale: Int32(videoDuration.timescale)) + frameForTimes.append(NSValue(time: cmTime)) + } + generator.appliesPreferredTrackTransform = true + generator.maximumSize = size + + generator.requestedTimeToleranceBefore = CMTime.zero + generator.requestedTimeToleranceAfter = CMTime.zero + generator.videoComposition = composition + + var images:[(image: CGImage, fake: Bool)] = [] + + var blurred: CGImage? + + generator.generateCGImagesAsynchronously(forTimes: frameForTimes, completionHandler: { (requestedTime, image, actualTime, result, error) in + if let image = image, result == .succeeded { + images.removeAll(where: { $0.fake }) + images.append((image: image, fake: false)) + if images.count < count, let image = images.first?.image, blur { + if blurred == nil { + let thumbnailContext = DrawingContext(size: size, scale: 1.0) + thumbnailContext.withFlippedContext { c in + c.interpolationQuality = .none + c.draw(image, in: CGRect(origin: CGPoint(), size: size)) + } + telegramFastBlurMore(Int32(size.width), Int32(size.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes) + blurred = thumbnailContext.generateImage() + } + if let image = blurred { + while images.count < count { + images.append((image: image, fake: true)) + } + } + } + + } + if gradually { + subscriber.putNext((images.map { $0.image }, false)) + } + if images.filter({ !$0.fake }).count == frameForTimes.count, !cancelled { + subscriber.putNext((images.map { $0.image }, true)) + subscriber.putCompletion() + } + }) + return ActionDisposable { [weak generator] in + Queue.concurrentBackgroundQueue().async { + generator?.cancelAllCGImageGeneration() + cancelled = true + } + } + } |> runOn(.concurrentBackgroundQueue()) +} +func generateVideoAvatarPreview(for asset: AVComposition, composition: AVVideoComposition?, highSize: NSSize, lowSize: NSSize, at seconds: Double) -> Signal<(CGImage?, CGImage?), NoError> { + return Signal { subscriber in + let imageGenerator = AVAssetImageGenerator(asset: asset) + let highSize = highSize.multipliedByScreenScale() + imageGenerator.maximumSize = highSize + imageGenerator.appliesPreferredTrackTransform = true + imageGenerator.requestedTimeToleranceBefore = .zero + imageGenerator.requestedTimeToleranceAfter = .zero + imageGenerator.videoComposition = composition + let highRes = try? imageGenerator.copyCGImage(at: CMTimeMakeWithSeconds(seconds, preferredTimescale: 1000), actualTime: nil) + + + subscriber.putNext((highRes, highRes)) + subscriber.putCompletion() + + return ActionDisposable { + } + } |> runOn(.concurrentDefaultQueue()) +} diff --git a/Telegram-Mac/VideoMessageConfig.swift b/Telegram-Mac/VideoMessageConfig.swift new file mode 100644 index 0000000000..810f955d21 --- /dev/null +++ b/Telegram-Mac/VideoMessageConfig.swift @@ -0,0 +1,43 @@ +// +// VideoMessageConfig.swift +// Telegram +// +// Created by Mikhail Filimonov on 30.07.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TelegramCore + + + +struct VideoMessageConfig : Equatable { + static var defaultValue: VideoMessageConfig { + return VideoMessageConfig(videoBitrate: 1000, audioBitrate: 64, diameter: 384, fileSizeLimit: 12 * 1024 * 1024) + } + + let videoBitrate: Int + let audioBitrate: Int + let fileSizeLimit: Int + let diameter: Int + fileprivate init(videoBitrate: Int, audioBitrate: Int, diameter: Int, fileSizeLimit: Int) { + self.videoBitrate = videoBitrate + self.audioBitrate = audioBitrate + self.fileSizeLimit = fileSizeLimit + self.diameter = diameter + } + + static func with(appConfiguration: AppConfiguration) -> VideoMessageConfig { + if let data = appConfiguration.data, let video = data["round_video_encoding"] as? [String:Any] { + let d = VideoMessageConfig.defaultValue + let videoBitrate = video["video_bitrate"] as? Double ?? Double(d.videoBitrate) + let audioBitrate = video["audio_bitrate"] as? Double ?? Double(d.audioBitrate) + let maxSize = video["max_size"] as? Double ?? Double(d.fileSizeLimit) + let diameter = video["diameter"] as? Double ?? Double(d.diameter) + return .init(videoBitrate: Int(videoBitrate), audioBitrate: Int(audioBitrate), diameter: Int(diameter), fileSizeLimit: Int(maxSize)) + } else { + return .defaultValue + } + } + +} diff --git a/Telegram-Mac/VideoPlayer.swift b/Telegram-Mac/VideoPlayer.swift new file mode 100644 index 0000000000..fa5fc50912 --- /dev/null +++ b/Telegram-Mac/VideoPlayer.swift @@ -0,0 +1,13 @@ +// +// VideoPlayer.swift +// Telegram +// +// Created by Mikhail Filimonov on 29/05/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa + +class VideoPlayer: NSObject { + +} diff --git a/Telegram-Mac/VideoPlayerProxy.swift b/Telegram-Mac/VideoPlayerProxy.swift new file mode 100755 index 0000000000..99d27231f6 --- /dev/null +++ b/Telegram-Mac/VideoPlayerProxy.swift @@ -0,0 +1,117 @@ +import Foundation +import SwiftSignalKit +import AVFoundation + +private final class VideoPlayerProxyContext { + private let queue: Queue + + var updateVideoInHierarchy: ((Bool) -> Void)? + + var node: MediaPlayerView? { + didSet { + self.node?.takeFrameAndQueue = self.takeFrameAndQueue + self.node?.state = state + self.updateVideoInHierarchy?(node?.videoInHierarchy ?? false) + self.node?.updateVideoInHierarchy = { [weak self] value in + self?.updateVideoInHierarchy?(value) + } + } + } + + var takeFrameAndQueue: (Queue, () -> MediaTrackFrameResult)? { + didSet { + self.node?.takeFrameAndQueue = self.takeFrameAndQueue + } + } + + var state: (timebase: CMTimebase, requestFrames: Bool, rotationAngle: Double, aspect: Double)? { + didSet { + self.node?.state = self.state + } + } + + init(queue: Queue) { + self.queue = queue + } + + deinit { + assert(self.queue.isCurrent()) + } +} + +final class VideoPlayerProxy { + var takeFrameAndQueue: (Queue, () -> MediaTrackFrameResult)? { + didSet { + let updatedTakeFrameAndQueue = self.takeFrameAndQueue + self.withContext { context in + context?.takeFrameAndQueue = updatedTakeFrameAndQueue + } + } + } + + var state: (timebase: CMTimebase, requestFrames: Bool, rotationAngle: Double, aspect: Double)? { + didSet { + let updatedState = self.state + self.withContext { context in + context?.state = updatedState + } + } + } + + private let queue: Queue + private let contextQueue = Queue.mainQueue() + private var contextRef: Unmanaged? + + var visibility: Bool = false + var visibilityUpdated: ((Bool) -> Void)? + + init(queue: Queue) { + self.queue = queue + + self.contextQueue.async { + let context = VideoPlayerProxyContext(queue: self.contextQueue) + context.updateVideoInHierarchy = { [weak self] value in + queue.async { + if let strongSelf = self { + if strongSelf.visibility != value { + strongSelf.visibility = value + strongSelf.visibilityUpdated?(value) + } + } + } + } + self.contextRef = Unmanaged.passRetained(context) + } + } + + deinit { + let contextRef = self.contextRef + self.contextQueue.async { + if let contextRef = contextRef { + let context = contextRef.takeUnretainedValue() + context.state = nil + contextRef.release() + } + } + } + + private func withContext(_ f: @escaping (VideoPlayerProxyContext?) -> Void) { + self.contextQueue.async { + if let contextRef = self.contextRef { + let context = contextRef.takeUnretainedValue() + f(context) + } else { + f(nil) + } + } + } + + func attachNodeAndRelease(_ nodeRef: Unmanaged) { + self.withContext { context in + if let context = context { + context.node = nodeRef.takeUnretainedValue() + } + nodeRef.release() + } + } +} diff --git a/Telegram-Mac/VideoRecorderModalController.swift b/Telegram-Mac/VideoRecorderModalController.swift index 1421e53ea4..fb6f0635aa 100644 --- a/Telegram-Mac/VideoRecorderModalController.swift +++ b/Telegram-Mac/VideoRecorderModalController.swift @@ -8,8 +8,9 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac +import TelegramCore + +import SwiftSignalKit class VideoRecorderModalController: ModalViewController { @@ -30,7 +31,7 @@ class VideoRecorderModalController: ModalViewController { init(chatInteraction: ChatInteraction, pipeline: VideoRecorderPipeline) { self.chatInteraction = chatInteraction self.pipeline = pipeline - super.init(frame: NSMakeRect(0, 0, 210, 210)) + super.init(frame: NSMakeRect(0, 0, 300, 300)) bar = .init(height: 0) } @@ -38,16 +39,14 @@ class VideoRecorderModalController: ModalViewController { return NSTemporaryDirectory() + "video_last_thumbnail.jpg" } + private func saveThumbnail(_ thumb: CGImage) { - var blurred: CGImage = thumb - for _ in 0 ..< 10 { - blurred = blurred.blurred - } + let blurred: CGImage = thumb.blurred _ = blurred.saveToFile(pathForThumbnail) } override func initializer() -> NSView { - return VideoRecorderModalView(frame: NSMakeRect(_frameRect.minX, _frameRect.minY, _frameRect.width, _frameRect.height - bar.height), thumbnail: .loadFromFile(pathForThumbnail)) + return VideoRecorderModalView(frame: NSMakeRect(_frameRect.minX, _frameRect.minY, _frameRect.width, _frameRect.height - bar.height), thumbnail: .loadFromFile("")) } @@ -60,20 +59,13 @@ class VideoRecorderModalController: ModalViewController { disposable.set((pipeline.statePromise.get() |> deliverOnMainQueue).start(next: { [weak self] status in if let strongSelf = self { switch status { - case let .finishRecording(path, _, thumb): + case let .finishRecording(path, _, _, thumb): strongSelf.countdownDisposable.set(nil) strongSelf.genericView.updateForPreview(path, preview: thumb) strongSelf.pipeline.stopCapture() case .recording: strongSelf.genericView.didStartedRecording() - strongSelf.countdownDisposable.set(countdown(VideoRecorderPipeline.videoMessageMaxDuration, delay: 0.2).start(next: { [weak strongSelf] value in - strongSelf?.genericView.updateProgress(1.0 - Float(value) / Float(VideoRecorderPipeline.videoMessageMaxDuration)) - - if value <= 0 { - strongSelf?.stopAndMakeRecordedVideo() - } - - })) + strongSelf.runTimer() case .madeThumbnail(let thumb): strongSelf.saveThumbnail(thumb) case let .stopped(thumb): @@ -89,6 +81,23 @@ class VideoRecorderModalController: ModalViewController { } + private func runTimer() { + countdownDisposable.set((pipeline.powerAndDuration.get() |> deliverOnMainQueue).start(next: { [weak self] _, duration in + guard let `self` = self else {return} + self.genericView.updateProgress(Float(duration / VideoRecorderPipeline.videoMessageMaxDuration)) + + if duration >= VideoRecorderPipeline.videoMessageMaxDuration { + self.stopAndMakeRecordedVideo() + if let stateData = self.chatInteraction.presentation.recordingState?.data { + self.chatInteraction.mediaPromise.set(stateData) + } + self.chatInteraction.update({$0.withoutRecordingState()}) + self.close() + } + + })) + } + deinit { disposable.dispose() countdownDisposable.dispose() @@ -109,6 +118,9 @@ class VideoRecorderModalController: ModalViewController { override func escapeKeyAction() -> KeyHandlerResult { + close() + chatInteraction.presentation.recordingState?.stop() + chatInteraction.update({$0.withoutRecordingState()}) return .invoked } diff --git a/Telegram-Mac/VideoRecorderModalView.swift b/Telegram-Mac/VideoRecorderModalView.swift index 1df0c7a733..191a866842 100644 --- a/Telegram-Mac/VideoRecorderModalView.swift +++ b/Telegram-Mac/VideoRecorderModalView.swift @@ -24,7 +24,7 @@ class VideoRecorderModalView: View { } init(frame frameRect: NSRect, thumbnail: CGImage?) { - progressView = RadialProgressView(theme: RadialProgressTheme(backgroundColor: .clear, foregroundColor: theme.colors.blueUI, icon: nil, iconInset: NSEdgeInsets(), lineWidth: 4), twist: false) + progressView = RadialProgressView(theme: RadialProgressTheme(backgroundColor: .clear, foregroundColor: theme.colors.accent, icon: nil, iconInset: NSEdgeInsets(), lineWidth: 4), twist: false) super.init(frame: frameRect) addSubview(shadowView) addSubview(captureContainer) @@ -37,7 +37,7 @@ class VideoRecorderModalView: View { captureContainer.setFrameSize(frameRect.width - 36, frameRect.height - 36) placeholderView.animates = false placeholderView.image = thumbnail ?? #imageLiteral(resourceName: "VideoMessagePlaceholder").precomposed() - placeholderView.sizeToFit() + placeholderView.frame = bounds captureContainer.addSubview(placeholderView) placeholderView.center() @@ -63,7 +63,7 @@ class VideoRecorderModalView: View { } func updateForPreview(_ path:String? = nil, preview: CGImage?) -> Void { - previewPlayer.set(path: path) + previewPlayer.set(data: AVGifData.dataFrom(path)) placeholderView.image = preview previewPlayer.isHidden = path == nil @@ -84,13 +84,13 @@ class VideoRecorderModalView: View { } func updateProgress(_ progress: Float) { - progressView.state = .ImpossibleFetching(progress: progress, force: false) + progressView.state = .ImpossibleFetching(progress: progress, force: true) } func didStartedRecording() { - placeholderView.change(opacity: 0) captureLayer.opacity = 1 - captureLayer.animateAlpha(from: 0, to: 1, duration: 0.2) + placeholderView.change(opacity: 0.0, duration: 1.0) + } required init?(coder: NSCoder) { diff --git a/Telegram-Mac/VideoRecorderPipeline.swift b/Telegram-Mac/VideoRecorderPipeline.swift index 9158faef5a..c457782e96 100644 --- a/Telegram-Mac/VideoRecorderPipeline.swift +++ b/Telegram-Mac/VideoRecorderPipeline.swift @@ -7,9 +7,10 @@ // import Cocoa -import SwiftSignalKitMac +import SwiftSignalKit import Accelerate -import TelegramCoreMac +import TelegramCore + import TGUIKit @@ -30,7 +31,8 @@ class VideoRecorderPipeline : NSObject, AVCaptureVideoDataOutputSampleBufferDele } func movieRecorderDidFinishRecording(_ recorder: TGVideoCameraMovieRecorder!) { - status = .finishRecording(path: url.path, duration: resultDuration, thumb: thumbnail) + liveUploading?.fileDidChangedSize(true) + status = .finishRecording(path: url.path, duration: resultDuration, id: liveUploading?.id, thumb: thumbnail) } @@ -56,6 +58,8 @@ class VideoRecorderPipeline : NSObject, AVCaptureVideoDataOutputSampleBufferDele let session: AVCaptureSession = AVCaptureSession() + let config: VideoMessageConfig + private var status: VideoCameraRecordingStatus = .idle { didSet { statePromise.set(status) @@ -80,11 +84,13 @@ class VideoRecorderPipeline : NSObject, AVCaptureVideoDataOutputSampleBufferDele private let startRecordAfterAudioBuffer:Atomic = Atomic(value: false) - static let videoMessageMaxDuration: Double = 59.6 + static let videoMessageMaxDuration: Double = 60 - - init(url:URL) { + private let liveUploading: PreUploadManager? + init(url:URL, config: VideoMessageConfig, liveUploading: PreUploadManager?) { self.url = url + self.liveUploading = liveUploading + self.config = config super.init() recorder = TGVideoCameraMovieRecorder(url: url, delegate: self, callbackQueue: VideoRecorderPipeline.queue.queue) @@ -92,22 +98,41 @@ class VideoRecorderPipeline : NSObject, AVCaptureVideoDataOutputSampleBufferDele renderer.orientation = .portrait renderer.mirror = true - if session.canSetSessionPreset(.vga640x480) { - session.sessionPreset = .vga640x480 + if session.canSetSessionPreset(.hd1280x720) { + session.sessionPreset = .hd1280x720 } else { session.sessionPreset = .medium } - if let videoDevice = AVCaptureDevice.default(for: .video) { + + let defAudioDevice = AVCaptureDevice.default(for: .audio) + let defVideoDevice = AVCaptureDevice.default(for: .video) + + + var videoDevices = AVCaptureDevice.devices(for: .video).filter({ $0.isConnected && !$0.isSuspended }) + var audioDevices = AVCaptureDevice.devices(for: .audio) + + if !videoDevices.isEmpty, let device = defVideoDevice { + videoDevices.insert(device, at: 0) + } + if !audioDevices.isEmpty, let device = defAudioDevice { + audioDevices.insert(device, at: 0) + } + + let videoDevice = videoDevices.first(where: { $0.isConnected && !$0.isSuspended}) + let audioDevice = audioDevices.first(where: { $0.isConnected && !$0.isSuspended}) + + + if let videoDevice = videoDevice { setSelectedVideoDevice(videoDevice) - if let audioDevice = AVCaptureDevice.default(for: .audio) { - - setSelectedAudioDevice(audioDevice) - } } else if let videoDevice = AVCaptureDevice.default(for: .muxed) { setSelectedVideoDevice(videoDevice) } + if let audioDevice = audioDevice { + setSelectedAudioDevice(audioDevice) + } + videoOutput.alwaysDiscardsLateVideoFrames = false videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA, kCVPixelBufferIOSurfacePropertiesKey as String: [:], kCVPixelBufferWidthKey as String: 500, kCVPixelBufferHeightKey as String: 500] @@ -116,8 +141,6 @@ class VideoRecorderPipeline : NSObject, AVCaptureVideoDataOutputSampleBufferDele session.addOutput(videoOutput) audioOutput.setSampleBufferDelegate(self, queue: VideoRecorderPipeline.queue.queue) - - //averagePowerForChannel session.addOutput(audioOutput) @@ -125,9 +148,7 @@ class VideoRecorderPipeline : NSObject, AVCaptureVideoDataOutputSampleBufferDele videoConnection = videoOutput.connection(with: .video) audioConnection = audioOutput.connection(with: .audio) - - - + _configureFps() } @@ -202,9 +223,11 @@ class VideoRecorderPipeline : NSObject, AVCaptureVideoDataOutputSampleBufferDele func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { + + let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer); - if self.skip.modify({min($0 + 1, 24)}) < 24 { + if self.skip.modify({ $0 + 1 }) < 10 { return } @@ -235,7 +258,7 @@ class VideoRecorderPipeline : NSObject, AVCaptureVideoDataOutputSampleBufferDele } } } - + liveUploading?.fileDidChangedSize(false) } @@ -258,6 +281,7 @@ class VideoRecorderPipeline : NSObject, AVCaptureVideoDataOutputSampleBufferDele } recorder?.appendVideoPixelBuffer(renderedPixelBuffer, withPresentationTime: timestamp) } + } @@ -275,10 +299,10 @@ class VideoRecorderPipeline : NSObject, AVCaptureVideoDataOutputSampleBufferDele status = .recording startTimeInterval = Date().timeIntervalSince1970 - let audioSettings = TGMediaVideoConversionPresetSettings.audioSettings(for: TGMediaVideoConversionPresetVideoMessage) + let audioSettings = TGMediaVideoConversionPresetSettings.audioSettings(for: TGMediaVideoConversionPresetVideoMessage, bitrate: Int32(config.audioBitrate)) recorder.addAudioTrack(withSourceFormatDescription: outputAudioFormatDescription, settings: audioSettings) let size: CGSize = TGMediaVideoConversionPresetSettings.maximumSize(for: TGMediaVideoConversionPresetVideoMessage) - let videoSettings = TGMediaVideoConversionPresetSettings.videoSettings(for: TGMediaVideoConversionPresetVideoMessage, dimensions: size) + let videoSettings = TGMediaVideoConversionPresetSettings.videoSettings(for: TGMediaVideoConversionPresetVideoMessage, dimensions: size, bitrate: Int32(config.videoBitrate)) recorder.addVideoTrack(withSourceFormatDescription: outputVideoFormatDescription, transform: CGAffineTransform.identity, settings: videoSettings) recorder.prepareToRecord() @@ -298,14 +322,19 @@ class VideoRecorderPipeline : NSObject, AVCaptureVideoDataOutputSampleBufferDele func stop() { VideoRecorderPipeline.queue.async { + if case .finishRecording = self.status { + return + } self.status = .stoppingRecording self.videoOutput.setSampleBufferDelegate(nil, queue: nil) self.audioOutput.setSampleBufferDelegate(nil, queue: nil) let duration = self.recorder.videoDuration() - if !duration.isNaN { - self.resultDuration = Int(duration) + if !duration.isNaN && duration >= 0.5 { + self.resultDuration = Int(ceil(duration)) + self.recorder.finishRecording() + } else { + self.dispose() } - self.recorder.finishRecording() } } @@ -317,10 +346,13 @@ class VideoRecorderPipeline : NSObject, AVCaptureVideoDataOutputSampleBufferDele func dispose() { VideoRecorderPipeline.queue.async { + if case .finishRecording = self.status { + return + } self.status = .stopped(thumb: self.thumbnail) let duration = self.recorder.videoDuration() if !duration.isNaN { - self.resultDuration = Int(duration) + self.resultDuration = Int(ceil(duration)) } } } diff --git a/Telegram-Mac/Views.swift b/Telegram-Mac/Views.swift index e54d1be60c..347d3cc7f2 100644 --- a/Telegram-Mac/Views.swift +++ b/Telegram-Mac/Views.swift @@ -9,7 +9,7 @@ import Cocoa import TGUIKit -class RestrictionWrappedView : View { +class RestrictionWrappedView : Control { let textView: TextView = TextView() let text:String required init(frame frameRect: NSRect) { @@ -21,13 +21,14 @@ class RestrictionWrappedView : View { super.init() addSubview(textView) textView.userInteractionEnabled = false - updateLocalizationAndTheme() + updateLocalizationAndTheme(theme: theme) } - override func updateLocalizationAndTheme() { + override func updateLocalizationAndTheme(theme: PresentationTheme) { self.backgroundColor = theme.colors.background - let layout = TextViewLayout(.initialize(string: text, color: theme.colors.grayText, font: .normal(.text)), maximumNumberOfLines: 2, alignment: .center) + let layout = TextViewLayout(.initialize(string: text, color: theme.colors.grayText, font: .normal(.text)), alignment: .center) textView.update(layout) + textView.backgroundColor = theme.colors.background } required init?(coder: NSCoder) { @@ -72,7 +73,7 @@ class VideoDurationView : View { ctx.fill(bounds) let f = focus(textNode.0.size) - textNode.1.draw(f, in: ctx, backingScaleFactor: backingScaleFactor) + textNode.1.draw(f, in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) } required init?(coder: NSCoder) { @@ -83,3 +84,120 @@ class VideoDurationView : View { fatalError("init(frame:) has not been implemented") } } + +class CornerView : View { + + var positionFlags: LayoutPositionFlags? { + didSet { + needsLayout = true + } + } + + var didChangeSuperview: (()->Void)? = nil + + override func viewDidMoveToSuperview() { + didChangeSuperview?() + } + + override var backgroundColor: NSColor { + didSet { + layer?.backgroundColor = .clear + } + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + + if let positionFlags = positionFlags { + ctx.round(frame.size, positionFlags.isEmpty ? 0 : .cornerRadius, positionFlags: positionFlags) + } + ctx.setFillColor(backgroundColor.cgColor) + ctx.fill(bounds) +// if let positionFlags = positionFlags { +// +// let minx:CGFloat = 0, midx = frame.width/2.0, maxx = frame.width +// let miny:CGFloat = 0, midy = frame.height/2.0, maxy = frame.height +// +// ctx.move(to: NSMakePoint(minx, midy)) +// +// var topLeftRadius: CGFloat = .cornerRadius +// var bottomLeftRadius: CGFloat = .cornerRadius +// var topRightRadius: CGFloat = .cornerRadius +// var bottomRightRadius: CGFloat = .cornerRadius +// +// +// if positionFlags.contains(.top) && positionFlags.contains(.left) { +// topLeftRadius = topLeftRadius * 3 + 2 +// } +// if positionFlags.contains(.top) && positionFlags.contains(.right) { +// topRightRadius = topRightRadius * 3 + 2 +// } +// if positionFlags.contains(.bottom) && positionFlags.contains(.left) { +// bottomLeftRadius = bottomLeftRadius * 3 + 2 +// } +// if positionFlags.contains(.bottom) && positionFlags.contains(.right) { +// bottomRightRadius = bottomRightRadius * 3 + 2 +// } +// +// ctx.addArc(tangent1End: NSMakePoint(minx, miny), tangent2End: NSMakePoint(midx, miny), radius: bottomLeftRadius) +// ctx.addArc(tangent1End: NSMakePoint(maxx, miny), tangent2End: NSMakePoint(maxx, midy), radius: bottomRightRadius) +// ctx.addArc(tangent1End: NSMakePoint(maxx, maxy), tangent2End: NSMakePoint(midx, maxy), radius: topLeftRadius) +// ctx.addArc(tangent1End: NSMakePoint(minx, maxy), tangent2End: NSMakePoint(minx, midy), radius: topRightRadius) +// +// ctx.closePath() +// ctx.clip() +// } +// +// ctx.setFillColor(backgroundColor.cgColor) +// ctx.fill(bounds) +// + } + +} + + +class SearchTitleBarView : TitledBarView { + private var search:ImageButton = ImageButton() + init(controller: ViewController, title:NSAttributedString, handler:@escaping() ->Void) { + super.init(controller: controller, title) + search.set(handler: { _ in + handler() + }, for: .Click) + addSubview(search) + updateLocalizationAndTheme(theme: theme) + } + + func updateSearchVisibility(_ visible: Bool, animated: Bool = true) { + if visible { + self.search.isHidden = false + } + search.change(opacity: visible ? 1 : 0, animated: animated, completion: { [weak self] _ in + self?.search.isHidden = !visible + }) + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let theme = (theme as! TelegramPresentationTheme) + search.set(image: theme.icons.chatSearch, for: .Normal) + search.set(image: theme.icons.chatSearchActive, for: .Highlight) + + + _ = search.sizeToFit() + backgroundColor = theme.colors.background + needsLayout = true + } + + override func layout() { + super.layout() + search.centerY(x: frame.width - search.frame.width) + } + + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/VoiceBlobView.swift b/Telegram-Mac/VoiceBlobView.swift new file mode 100644 index 0000000000..3a725b8489 --- /dev/null +++ b/Telegram-Mac/VoiceBlobView.swift @@ -0,0 +1,466 @@ +// +// VoiceBlobView.swift +// Telegram +// +// Created by Mikhail Filimonov on 16.11.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +final class VoiceBlobView: View { + + private let smallBlob: BlobView + private let mediumBlob: BlobView + private let bigBlob: BlobView + + private let maxLevel: CGFloat + + private var displayLinkAnimator: ConstantDisplayLinkAnimator? + + private var audioLevel: CGFloat = 0 + private var presentationAudioLevel: CGFloat = 0 + + private(set) var isAnimating = false + + typealias BlobRange = (min: CGFloat, max: CGFloat) + + init( + frame: CGRect, + maxLevel: CGFloat, + smallBlobRange: BlobRange, + mediumBlobRange: BlobRange, + bigBlobRange: BlobRange + ) { + self.maxLevel = maxLevel + + self.smallBlob = BlobView( + pointsCount: 8, + minRandomness: 0.1, + maxRandomness: 0.5, + minSpeed: 0.2, + maxSpeed: 0.6, + minScale: smallBlobRange.min, + maxScale: smallBlobRange.max, + scaleSpeed: 0.2, + isCircle: true + ) + self.mediumBlob = BlobView( + pointsCount: 8, + minRandomness: 1, + maxRandomness: 1, + minSpeed: 1.5, + maxSpeed: 7, + minScale: mediumBlobRange.min, + maxScale: mediumBlobRange.max, + scaleSpeed: 0.2, + isCircle: false + ) + self.bigBlob = BlobView( + pointsCount: 8, + minRandomness: 1, + maxRandomness: 1, + minSpeed: 1.5, + maxSpeed: 7, + minScale: bigBlobRange.min, + maxScale: bigBlobRange.max, + scaleSpeed: 0.2, + isCircle: false + ) + + super.init(frame: frame) + + addSubview(bigBlob) + addSubview(mediumBlob) + addSubview(smallBlob) + + displayLinkAnimator = ConstantDisplayLinkAnimator(update: { [weak self] in + guard let strongSelf = self, let window = self?.window, window.isVisible else { return } + + strongSelf.presentationAudioLevel = strongSelf.presentationAudioLevel * 0.9 + strongSelf.audioLevel * 0.1 + + strongSelf.smallBlob.level = strongSelf.presentationAudioLevel + strongSelf.mediumBlob.level = strongSelf.presentationAudioLevel + strongSelf.bigBlob.level = strongSelf.presentationAudioLevel + }) + layout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + func setColor(_ color: NSColor) { + smallBlob.setColor(color) + mediumBlob.setColor(color.withAlphaComponent(0.3)) + bigBlob.setColor(color.withAlphaComponent(0.15)) + } + + func updateLevel(_ level: CGFloat) { + let normalizedLevel = min(1, max(level / maxLevel, 0)) + + smallBlob.updateSpeedLevel(to: normalizedLevel) + mediumBlob.updateSpeedLevel(to: normalizedLevel) + bigBlob.updateSpeedLevel(to: normalizedLevel) + + audioLevel = normalizedLevel + } + + func startAnimating() { + guard !isAnimating else { return } + isAnimating = true + + mediumBlob.layer?.animateScaleSpring(from: 0.1, to: 1, duration: 0.6) + bigBlob.layer?.animateScaleSpring(from: 0.1, to: 1, duration: 0.6) + + updateBlobsState() + + displayLinkAnimator?.isPaused = false + } + + func stopAnimating() { + guard isAnimating else { return } + isAnimating = false + + mediumBlob.layer?.animateScaleSpring(from: 1.0, to: 0.1, duration: 0.6, removeOnCompletion: false, bounce: false) + bigBlob.layer?.animateScaleSpring(from: 1.0, to: 0.1, duration: 0.6, removeOnCompletion: false, bounce: false) + + updateBlobsState() + + displayLinkAnimator?.isPaused = true + } + + private func updateBlobsState() { + if isAnimating { + if smallBlob.frame.size != .zero { + smallBlob.startAnimating() + mediumBlob.startAnimating() + bigBlob.startAnimating() + } + } else { + smallBlob.stopAnimating() + mediumBlob.stopAnimating() + bigBlob.stopAnimating() + } + } + + override func layout() { + super.layout() + + smallBlob.frame = bounds + mediumBlob.frame = bounds + bigBlob.frame = bounds + + updateBlobsState() + } +} + +final class BlobView: View { + + let pointsCount: Int + let smoothness: CGFloat + + let minRandomness: CGFloat + let maxRandomness: CGFloat + + let minSpeed: CGFloat + let maxSpeed: CGFloat + + let minScale: CGFloat + let maxScale: CGFloat + let scaleSpeed: CGFloat + + var scaleLevelsToBalance = [CGFloat]() + + // If true ignores randomness and pointsCount + let isCircle: Bool + + var level: CGFloat = 0 { + didSet { + CATransaction.begin() + CATransaction.setDisableActions(true) + let lv = minScale + (maxScale - minScale) * level + shapeLayer.transform = CATransform3DMakeScale(lv, lv, 1) + CATransaction.commit() + } + } + + private var blobAnimation: DisplayLinkAnimator? + + private var speedLevel: CGFloat = 0 + private var scaleLevel: CGFloat = 0 + + private var lastSpeedLevel: CGFloat = 0 + private var lastScaleLevel: CGFloat = 0 + + private let shapeLayer: CAShapeLayer = { + let layer = CAShapeLayer() + layer.strokeColor = nil + return layer + }() + + private var transition: CGFloat = 0 { + didSet { + guard let currentPoints = currentPoints else { return } + shapeLayer.path = CGPath.smoothCurve(through: currentPoints, length: bounds.width, smoothness: smoothness) + } + } + + private var fromPoints: [CGPoint]? + private var toPoints: [CGPoint]? + + private var currentPoints: [CGPoint]? { + guard let fromPoints = fromPoints, let toPoints = toPoints else { return nil } + + return fromPoints.enumerated().map { offset, fromPoint in + let toPoint = toPoints[offset] + return CGPoint( + x: fromPoint.x + (toPoint.x - fromPoint.x) * transition, + y: fromPoint.y + (toPoint.y - fromPoint.y) * transition + ) + } + } + + init( + pointsCount: Int, + minRandomness: CGFloat, + maxRandomness: CGFloat, + minSpeed: CGFloat, + maxSpeed: CGFloat, + minScale: CGFloat, + maxScale: CGFloat, + scaleSpeed: CGFloat, + isCircle: Bool + ) { + self.pointsCount = pointsCount + self.minRandomness = minRandomness + self.maxRandomness = maxRandomness + self.minSpeed = minSpeed + self.maxSpeed = maxSpeed + self.minScale = minScale + self.maxScale = maxScale + self.scaleSpeed = scaleSpeed + self.isCircle = isCircle + + let angle = (CGFloat.pi * 2) / CGFloat(pointsCount) + self.smoothness = ((4 / 3) * tan(angle / 4)) / sin(angle / 2) / 2 + + super.init(frame: .zero) + + layer?.addSublayer(shapeLayer) + + shapeLayer.transform = CATransform3DMakeScale(minScale, minScale, 1) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + func setColor(_ color: NSColor) { + shapeLayer.fillColor = color.cgColor + } + + func updateSpeedLevel(to newSpeedLevel: CGFloat) { + speedLevel = max(speedLevel, newSpeedLevel) + +// if abs(lastSpeedLevel - newSpeedLevel) > 0.5 { +// animateToNewShape() +// } + } + + func startAnimating() { + animateToNewShape() + } + + func stopAnimating() { + fromPoints = currentPoints + toPoints = nil + blobAnimation = nil + } + + private func animateToNewShape() { + guard !isCircle else { return } + + if blobAnimation != nil { + fromPoints = currentPoints + toPoints = nil + blobAnimation = nil + } + + if fromPoints == nil { + fromPoints = generateNextBlob(for: bounds.size) + } + if toPoints == nil { + toPoints = generateNextBlob(for: bounds.size) + } + + let duration = CGFloat(1 / (minSpeed + (maxSpeed - minSpeed) * speedLevel)) + let fromValue: CGFloat = 0 + let toValue: CGFloat = 1 + + let animation = DisplayLinkAnimator(duration: Double(duration), from: fromValue, to: toValue, update: { [weak self] value in + self?.transition = value + }, completion: { [weak self] in + guard let `self` = self else { + return + } + self.fromPoints = self.currentPoints + self.toPoints = nil + self.blobAnimation = nil + self.animateToNewShape() + }) + self.blobAnimation = animation + + lastSpeedLevel = speedLevel + speedLevel = 0 + } + + private func generateNextBlob(for size: CGSize) -> [CGPoint] { + let randomness = minRandomness + (maxRandomness - minRandomness) * speedLevel + return blob(pointsCount: pointsCount, randomness: randomness) + .map { + return CGPoint( + x: $0.x * CGFloat(size.width), + y: $0.y * CGFloat(size.height) + ) + } + } + + func blob(pointsCount: Int, randomness: CGFloat) -> [CGPoint] { + let angle = (CGFloat.pi * 2) / CGFloat(pointsCount) + + let rgen = { () -> CGFloat in + let accuracy: UInt32 = 1000 + let random = arc4random_uniform(accuracy) + return CGFloat(random) / CGFloat(accuracy) + } + let rangeStart: CGFloat = 1 / (1 + randomness / 10) + + let startAngle = angle * CGFloat(arc4random_uniform(100)) / CGFloat(100) + + let points = (0 ..< pointsCount).map { i -> CGPoint in + let randPointOffset = (rangeStart + CGFloat(rgen()) * (1 - rangeStart)) / 2 + let angleRandomness: CGFloat = angle * 0.1 + let randAngle = angle + angle * ((angleRandomness * CGFloat(arc4random_uniform(100)) / CGFloat(100)) - angleRandomness * 0.5) + let pointX = sin(startAngle + CGFloat(i) * randAngle) + let pointY = cos(startAngle + CGFloat(i) * randAngle) + return CGPoint( + x: pointX * randPointOffset, + y: pointY * randPointOffset + ) + } + + return points + } + + override func layout() { + super.layout() + + CATransaction.begin() + CATransaction.setDisableActions(true) + shapeLayer.position = CGPoint(x: bounds.midX, y: bounds.midY) + if isCircle { + let halfWidth = bounds.width * 0.5 + shapeLayer.path = CGPath(roundedRect: bounds.offsetBy(dx: -halfWidth, dy: -halfWidth), cornerWidth: halfWidth, cornerHeight: halfWidth, transform: nil) + } + CATransaction.commit() + } +} + +extension CGPath { + + + static func smoothCurve(through points: [CGPoint], length: CGFloat, smoothness: CGFloat, curve: Bool = false) -> CGPath { + var smoothPoints = [SmoothPoint]() + for index in (0 ..< points.count) { + let prevIndex = index - 1 + let prev = points[prevIndex >= 0 ? prevIndex : points.count + prevIndex] + let curr = points[index] + let next = points[(index + 1) % points.count] + + let angle: CGFloat = { + let dx = next.x - prev.x + let dy = -next.y + prev.y + let angle = atan2(dy, dx) + if angle < 0 { + return abs(angle) + } else { + return 2 * .pi - angle + } + }() + + smoothPoints.append( + SmoothPoint( + point: curr, + inAngle: angle + .pi, + inLength: smoothness * distance(from: curr, to: prev), + outAngle: angle, + outLength: smoothness * distance(from: curr, to: next) + ) + ) + } + + let resultPath = CGMutablePath() + if curve { + resultPath.move(to: CGPoint()) + resultPath.addLine(to: smoothPoints[0].point) + } else { + resultPath.move(to: smoothPoints[0].point) + } + + let smoothCount = curve ? smoothPoints.count - 1 : smoothPoints.count + for index in (0 ..< smoothCount) { + let curr = smoothPoints[index] + let next = smoothPoints[(index + 1) % points.count] + let currSmoothOut = curr.smoothOut() + let nextSmoothIn = next.smoothIn() + resultPath.addCurve(to: next.point, control1: currSmoothOut, control2: nextSmoothIn) + } + if curve { + resultPath.addLine(to: CGPoint(x: length, y: 0.0)) + } + resultPath.closeSubpath() + return resultPath + } + + + static private func distance(from fromPoint: CGPoint, to toPoint: CGPoint) -> CGFloat { + return sqrt((fromPoint.x - toPoint.x) * (fromPoint.x - toPoint.x) + (fromPoint.y - toPoint.y) * (fromPoint.y - toPoint.y)) + } + + struct SmoothPoint { + + let point: CGPoint + + let inAngle: CGFloat + let inLength: CGFloat + + let outAngle: CGFloat + let outLength: CGFloat + + func smoothIn() -> CGPoint { + return smooth(angle: inAngle, length: inLength) + } + + func smoothOut() -> CGPoint { + return smooth(angle: outAngle, length: outLength) + } + + private func smooth(angle: CGFloat, length: CGFloat) -> CGPoint { + return CGPoint( + x: point.x + length * cos(angle), + y: point.y + length * sin(angle) + ) + } + } +} + diff --git a/Telegram-Mac/VoiceCallSettings.swift b/Telegram-Mac/VoiceCallSettings.swift index 9cecca430d..85f89e2ae5 100644 --- a/Telegram-Mac/VoiceCallSettings.swift +++ b/Telegram-Mac/VoiceCallSettings.swift @@ -7,35 +7,152 @@ // import Cocoa -import PostboxMac -import SwiftSignalKitMac +import Postbox +import SwiftSignalKit +import TelegramCore -public enum VoiceCallDataSaving: Int32 { +import TGUIKit + +enum VoiceCallDataSaving: Int32 { case never case cellular case always } -public struct VoiceCallSettings: PreferencesEntry, Equatable { - public let dataSaving: VoiceCallDataSaving +struct PushToTalkValue : Equatable, PostboxCoding { + + struct ModifierFlag : Equatable, PostboxCoding { + let keyCode: UInt16 + let flag: UInt + init(keyCode: UInt16, flag: UInt) { + self.keyCode = keyCode + self.flag = flag + } + func encode(_ encoder: PostboxEncoder) { + encoder.encodeInt32(Int32(self.keyCode), forKey: "kc") + encoder.encodeInt64(Int64(bitPattern: UInt64(flag)), forKey: "f") + } + init(decoder: PostboxDecoder) { + self.keyCode = UInt16(decoder.decodeInt32ForKey("kc", orElse: 0)) + self.flag = UInt(bitPattern: Int(decoder.decodeInt64ForKey("f", orElse: 0))) + } + + } + + var isSpace: Bool { + return keyCodes == [KeyboardKey.Space.rawValue] && modifierFlags.isEmpty && otherMouse.isEmpty + } + + var keyCodes: [UInt16] + var modifierFlags: [ModifierFlag] + var string: String + var otherMouse: [Int] + init(keyCodes: [UInt16], otherMouse: [Int], modifierFlags: [ModifierFlag], string: String) { + self.keyCodes = keyCodes + self.modifierFlags = modifierFlags + self.string = string + self.otherMouse = otherMouse + } + func encode(_ encoder: PostboxEncoder) { + encoder.encodeObjectArray(self.modifierFlags, forKey: "mf") + encoder.encodeInt32Array(self.keyCodes.map { Int32($0) }, forKey: "kc") + encoder.encodeString(string, forKey: "s") + encoder.encodeInt64Array(self.otherMouse.map { Int64($0) }, forKey: "om") + + } + init(decoder: PostboxDecoder) { + self.keyCodes = decoder.decodeInt32ArrayForKey("kc").map { UInt16($0) } + self.modifierFlags = decoder.decodeObjectArrayForKey("mf").compactMap { $0 as? ModifierFlag } + self.string = decoder.decodeStringForKey("s", orElse: "") + self.otherMouse = decoder.decodeInt64ArrayForKey("om").map { Int($0) } + } +} + +enum VoiceChatInputMode : Int32 { + case none = 0 + case always = 1 + case pushToTalk = 2 +} + +struct VoiceCallSettings: PreferencesEntry, Equatable { - public static var defaultSettings: VoiceCallSettings { - return VoiceCallSettings(dataSaving: .never) + + enum Tooltip : Int32 { + case camera = 0 } - init(dataSaving: VoiceCallDataSaving) { - self.dataSaving = dataSaving + let audioInputDeviceId: String? + let cameraInputDeviceId: String? + let audioOutputDeviceId: String? + let mode: VoiceChatInputMode + let pushToTalk: PushToTalkValue? + let pushToTalkSoundEffects: Bool + let noiseSuppression: Bool + let tooltips:[Tooltip] + let visualEffects:Bool + static var defaultSettings: VoiceCallSettings { + return VoiceCallSettings(audioInputDeviceId: nil, cameraInputDeviceId: nil, audioOutputDeviceId: nil, mode: .always, pushToTalk: nil, pushToTalkSoundEffects: false, noiseSuppression: true, tooltips: [.camera], visualEffects: false) } - public init(decoder: PostboxDecoder) { - self.dataSaving = VoiceCallDataSaving(rawValue: decoder.decodeInt32ForKey("ds", orElse: 0))! + init(audioInputDeviceId: String?, cameraInputDeviceId: String?, audioOutputDeviceId: String?, mode: VoiceChatInputMode, pushToTalk: PushToTalkValue?, pushToTalkSoundEffects: Bool, noiseSuppression: Bool, tooltips: [Tooltip], visualEffects: Bool) { + self.audioInputDeviceId = audioInputDeviceId + self.cameraInputDeviceId = cameraInputDeviceId + self.audioOutputDeviceId = audioOutputDeviceId + self.pushToTalk = pushToTalk + self.mode = mode + self.pushToTalkSoundEffects = pushToTalkSoundEffects + self.noiseSuppression = noiseSuppression + self.tooltips = tooltips + self.visualEffects = visualEffects } - public func encode(_ encoder: PostboxEncoder) { - encoder.encodeInt32(self.dataSaving.rawValue, forKey: "ds") + init(decoder: PostboxDecoder) { + self.audioInputDeviceId = decoder.decodeOptionalStringForKey("ai") + self.cameraInputDeviceId = decoder.decodeOptionalStringForKey("ci") + self.audioOutputDeviceId = decoder.decodeOptionalStringForKey("ao") + self.pushToTalk = decoder.decodeObjectForKey("ptt3") as? PushToTalkValue + self.mode = VoiceChatInputMode(rawValue: decoder.decodeInt32ForKey("m1", orElse: 0)) ?? .none + self.pushToTalkSoundEffects = decoder.decodeBoolForKey("se", orElse: false) + self.noiseSuppression = decoder.decodeBoolForKey("ns", orElse: false) + self.tooltips = decoder.decodeInt32ArrayForKey("tt").compactMap { Tooltip(rawValue: $0) } + self.visualEffects = decoder.decodeBoolForKey("ve", orElse: true) } - public func isEqual(to: PreferencesEntry) -> Bool { + func encode(_ encoder: PostboxEncoder) { + if let audioInputDeviceId = audioInputDeviceId { + encoder.encodeString(audioInputDeviceId, forKey: "ai") + } else { + encoder.encodeNil(forKey: "ai") + } + + if let cameraInputDeviceId = cameraInputDeviceId { + encoder.encodeString(cameraInputDeviceId, forKey: "ci") + } else { + encoder.encodeNil(forKey: "ci") + } + + if let audioOutputDeviceId = audioOutputDeviceId { + encoder.encodeString(audioOutputDeviceId, forKey: "ao") + } else { + encoder.encodeNil(forKey: "ao") + } + + if let pushToTalk = pushToTalk { + encoder.encodeObject(pushToTalk, forKey: "ptt3") + } else { + encoder.encodeNil(forKey: "ptt3") + } + encoder.encodeInt32(self.mode.rawValue, forKey: "m1") + + encoder.encodeBool(pushToTalkSoundEffects, forKey: "se") + + encoder.encodeBool(noiseSuppression, forKey: "ns") + encoder.encodeInt32Array(self.tooltips.map { $0.rawValue }, forKey: "tt") + encoder.encodeBool(visualEffects, forKey: "ve") + + } + + func isEqual(to: PreferencesEntry) -> Bool { if let to = to as? VoiceCallSettings { return self == to } else { @@ -43,18 +160,46 @@ public struct VoiceCallSettings: PreferencesEntry, Equatable { } } - public static func ==(lhs: VoiceCallSettings, rhs: VoiceCallSettings) -> Bool { - return lhs.dataSaving == rhs.dataSaving + + func withUpdatedAudioInputDeviceId(_ audioInputDeviceId: String?) -> VoiceCallSettings { + return VoiceCallSettings(audioInputDeviceId: audioInputDeviceId, cameraInputDeviceId: self.cameraInputDeviceId, audioOutputDeviceId: self.audioOutputDeviceId, mode: self.mode, pushToTalk: self.pushToTalk, pushToTalkSoundEffects: self.pushToTalkSoundEffects, noiseSuppression: self.noiseSuppression, tooltips: self.tooltips, visualEffects: self.visualEffects) + } + func withUpdatedCameraInputDeviceId(_ cameraInputDeviceId: String?) -> VoiceCallSettings { + return VoiceCallSettings(audioInputDeviceId: self.audioInputDeviceId, cameraInputDeviceId: cameraInputDeviceId, audioOutputDeviceId: self.audioOutputDeviceId, mode: self.mode, pushToTalk: self.pushToTalk, pushToTalkSoundEffects: self.pushToTalkSoundEffects, noiseSuppression: self.noiseSuppression, tooltips: self.tooltips, visualEffects: self.visualEffects) + } + func withUpdatedAudioOutputDeviceId(_ audioOutputDeviceId: String?) -> VoiceCallSettings { + return VoiceCallSettings(audioInputDeviceId: self.audioInputDeviceId, cameraInputDeviceId: self.cameraInputDeviceId, audioOutputDeviceId: audioOutputDeviceId, mode: self.mode, pushToTalk: self.pushToTalk, pushToTalkSoundEffects: self.pushToTalkSoundEffects, noiseSuppression: self.noiseSuppression, tooltips: self.tooltips, visualEffects: self.visualEffects) + } + func withUpdatedPushToTalk(_ pushToTalk: PushToTalkValue?) -> VoiceCallSettings { + return VoiceCallSettings(audioInputDeviceId: self.audioInputDeviceId, cameraInputDeviceId: self.cameraInputDeviceId, audioOutputDeviceId: self.audioOutputDeviceId, mode: self.mode, pushToTalk: pushToTalk, pushToTalkSoundEffects: self.pushToTalkSoundEffects, noiseSuppression: self.noiseSuppression, tooltips: self.tooltips, visualEffects: self.visualEffects) + } + func withUpdatedMode(_ mode: VoiceChatInputMode) -> VoiceCallSettings { + return VoiceCallSettings(audioInputDeviceId: self.audioInputDeviceId, cameraInputDeviceId: self.cameraInputDeviceId, audioOutputDeviceId: self.audioOutputDeviceId, mode: mode, pushToTalk: self.pushToTalk, pushToTalkSoundEffects: self.pushToTalkSoundEffects, noiseSuppression: self.noiseSuppression, tooltips: self.tooltips, visualEffects: self.visualEffects) + } + func withUpdatedSoundEffects(_ pushToTalkSoundEffects: Bool) -> VoiceCallSettings { + return VoiceCallSettings(audioInputDeviceId: self.audioInputDeviceId, cameraInputDeviceId: self.cameraInputDeviceId, audioOutputDeviceId: self.audioOutputDeviceId, mode: self.mode, pushToTalk: self.pushToTalk, pushToTalkSoundEffects: pushToTalkSoundEffects, noiseSuppression: self.noiseSuppression, tooltips: self.tooltips, visualEffects: self.visualEffects) + } + + func withUpdatedNoiseSuppression(_ noiseSuppression: Bool) -> VoiceCallSettings { + return VoiceCallSettings(audioInputDeviceId: self.audioInputDeviceId, cameraInputDeviceId: self.cameraInputDeviceId, audioOutputDeviceId: self.audioOutputDeviceId, mode: self.mode, pushToTalk: self.pushToTalk, pushToTalkSoundEffects: self.pushToTalkSoundEffects, noiseSuppression: noiseSuppression, tooltips: self.tooltips, visualEffects: self.visualEffects) + } + func withRemovedTooltip(_ tooltip: Tooltip) -> VoiceCallSettings { + + var tooltips = self.tooltips + tooltips.removeAll(where: { $0 == tooltip }) + + return VoiceCallSettings(audioInputDeviceId: self.audioInputDeviceId, cameraInputDeviceId: self.cameraInputDeviceId, audioOutputDeviceId: self.audioOutputDeviceId, mode: self.mode, pushToTalk: self.pushToTalk, pushToTalkSoundEffects: self.pushToTalkSoundEffects, noiseSuppression: self.noiseSuppression, tooltips: tooltips, visualEffects: self.visualEffects) } - func withUpdatedDataSaving(_ dataSaving: VoiceCallDataSaving) -> VoiceCallSettings { - return VoiceCallSettings(dataSaving: dataSaving) + func withUpdatedVisualEffects(_ visualEffects: Bool) -> VoiceCallSettings { + return VoiceCallSettings(audioInputDeviceId: self.audioInputDeviceId, cameraInputDeviceId: self.cameraInputDeviceId, audioOutputDeviceId: self.audioOutputDeviceId, mode: self.mode, pushToTalk: self.pushToTalk, pushToTalkSoundEffects: self.pushToTalkSoundEffects, noiseSuppression: self.noiseSuppression, tooltips: tooltips, visualEffects: visualEffects) } + } -func updateVoiceCallSettingsSettingsInteractively(postbox: Postbox, _ f: @escaping (VoiceCallSettings) -> VoiceCallSettings) -> Signal { - return postbox.modify { modifier -> Void in - modifier.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.voiceCallSettings, { entry in +func updateVoiceCallSettingsSettingsInteractively(accountManager: AccountManager, _ f: @escaping (VoiceCallSettings) -> VoiceCallSettings) -> Signal { + return accountManager.transaction { transaction -> Void in + transaction.updateSharedData(ApplicationSharedPreferencesKeys.voiceCallSettings, { entry in let currentSettings: VoiceCallSettings if let entry = entry as? VoiceCallSettings { currentSettings = entry @@ -65,3 +210,10 @@ func updateVoiceCallSettingsSettingsInteractively(postbox: Postbox, _ f: @escapi }) } } + + +func voiceCallSettings(_ accountManager: AccountManager) -> Signal { + return accountManager.sharedData(keys: [ApplicationSharedPreferencesKeys.voiceCallSettings]) |> map { view in + return view.entries[ApplicationSharedPreferencesKeys.voiceCallSettings] as? VoiceCallSettings ?? VoiceCallSettings.defaultSettings + } +} diff --git a/Telegram-Mac/VoiceChatActionButton.swift b/Telegram-Mac/VoiceChatActionButton.swift new file mode 100644 index 0000000000..1f5c7f94d8 --- /dev/null +++ b/Telegram-Mac/VoiceChatActionButton.swift @@ -0,0 +1,1071 @@ +// +// VoiceChatActionButton.swift +// Telegram +// +// Created by Mikhail Filimonov on 14.12.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit + +private let white = NSColor(rgb: 0xffffff) +private let greyColor = NSColor(rgb: 0x2c2c2e) +private let secondaryGreyColor = NSColor(rgb: 0x1c1c1e) +private let blue = NSColor(rgb: 0x0078ff) +private let lightBlue = NSColor(rgb: 0x59c7f8) +private let green = NSColor(rgb: 0x33c659) +private let activeBlue = NSColor(rgb: 0x00a0b9) + +private let purple = NSColor(rgb: 0x766EE9) +private let lightPurple = NSColor(rgb: 0xF05459) + + + +private let areaSize = CGSize(width: 360, height: 360) +private let blobSize = CGSize(width: 190, height: 190) + +private let progressLineWidth: CGFloat = 3.0 + 1 +private let buttonSize = CGSize(width: 110, height: 110) +private let radius = buttonSize.width / 2.0 + +final class VoiceChatActionButtonBackgroundView: View { + enum State: Equatable { + case connecting + case disabled + case blob(Bool) + } + + private var state: State + private var hasState = false + + private var transition: State? + + var audioLevel: CGFloat = 0.0 { + didSet { + self.maskBlobLayer.updateLevel(audioLevel) + } + } + + var updatedActive: ((Bool) -> Void)? + var updatedOuterColor: ((NSColor?) -> Void)? + + private let backgroundCircleLayer = CAShapeLayer() + private let foregroundCircleLayer = CAShapeLayer() + private let growingForegroundCircleLayer = CAShapeLayer() + + private let foregroundLayer = CALayer() + private let foregroundGradientLayer = CAGradientLayer() + + private let maskLayer = CALayer() + private let maskGradientLayer = CAGradientLayer() + private let maskBlobLayer: VoiceChatBlobLayer + private let maskCircleLayer = CAShapeLayer() + + fileprivate let maskProgressLayer = CAShapeLayer() + + private let maskMediumBlobLayer = CAShapeLayer() + private let maskBigBlobLayer = CAShapeLayer() + + + override init() { + self.state = .connecting + + self.maskBlobLayer = VoiceChatBlobLayer(frame: CGRect(origin: .zero, size: blobSize), maxLevel: 1.5, mediumBlobRange: (0.69, 0.87), bigBlobRange: (0.71, 1.0)) + self.maskBlobLayer.setColor(white) + self.maskBlobLayer.isHidden = true + + + super.init() + + + let circlePath = CGMutablePath() + circlePath.addRoundedRect(in: CGRect(origin: CGPoint(), size: buttonSize), cornerWidth: buttonSize.width / 2, cornerHeight: buttonSize.height / 2) + + + self.backgroundCircleLayer.fillColor = greyColor.cgColor + self.backgroundCircleLayer.path = circlePath + + + let smallerCirclePath = CGMutablePath() + let smallerRect = CGRect(origin: CGPoint(), size: CGSize(width: buttonSize.width - progressLineWidth, height: buttonSize.height - progressLineWidth)) + smallerCirclePath.addRoundedRect(in: smallerRect, cornerWidth: smallerRect.width / 2, cornerHeight: smallerRect.height / 2) + + + self.foregroundCircleLayer.fillColor = greyColor.cgColor + self.foregroundCircleLayer.path = smallerCirclePath + self.foregroundCircleLayer.transform = CATransform3DMakeScale(0.0, 0.0, 1) + self.foregroundCircleLayer.isHidden = true + + self.growingForegroundCircleLayer.fillColor = greyColor.cgColor + self.growingForegroundCircleLayer.path = smallerCirclePath + self.growingForegroundCircleLayer.transform = CATransform3DMakeScale(1.0, 1.0, 1) + self.growingForegroundCircleLayer.isHidden = true + + self.foregroundGradientLayer.type = .radial + self.foregroundGradientLayer.colors = [lightBlue.cgColor, blue.cgColor] + self.foregroundGradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0) + self.foregroundGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0) + + self.maskLayer.backgroundColor = .clear + + self.maskGradientLayer.type = .radial + self.maskGradientLayer.colors = [NSColor(rgb: 0xffffff, alpha: 0.4).cgColor, NSColor(rgb: 0xffffff, alpha: 0.0).cgColor] + self.maskGradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5) + self.maskGradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0) + self.maskGradientLayer.transform = CATransform3DMakeScale(0.3, 0.3, 1.0) + self.maskGradientLayer.isHidden = true + + let path = CGMutablePath() + path.addArc(center: CGPoint(x: (buttonSize.width + 6.0) / 2.0, y: (buttonSize.height + 6.0) / 2.0), radius: radius, startAngle: 0.0, endAngle: CGFloat.pi * 2.0, clockwise: true) + + self.maskProgressLayer.strokeColor = white.cgColor + self.maskProgressLayer.fillColor = NSColor.clear.cgColor + self.maskProgressLayer.lineWidth = progressLineWidth + self.maskProgressLayer.lineCap = .round + self.maskProgressLayer.path = path + + let largerCirclePath = CGMutablePath() + let largerCircleRect = CGRect(origin: CGPoint(), size: CGSize(width: buttonSize.width + progressLineWidth, height: buttonSize.height + progressLineWidth)) + largerCirclePath.addRoundedRect(in: largerCircleRect, cornerWidth: largerCircleRect.width / 2, cornerHeight: largerCircleRect.height / 2) + + self.maskCircleLayer.fillColor = white.cgColor + self.maskCircleLayer.path = largerCirclePath + self.maskCircleLayer.isHidden = true + + + self.layer?.addSublayer(self.backgroundCircleLayer) + + self.layer?.addSublayer(self.foregroundLayer) + self.layer?.addSublayer(self.foregroundCircleLayer) + // self.layer?.addSublayer(self.growingForegroundCircleLayer) + + self.foregroundLayer.addSublayer(self.foregroundGradientLayer) + self.foregroundLayer.mask = self.maskLayer + + self.maskLayer.addSublayer(self.maskGradientLayer) + self.maskLayer.addSublayer(self.maskProgressLayer) + self.maskLayer.addSublayer(self.maskBlobLayer) + self.maskLayer.addSublayer(self.maskCircleLayer) + + self.maskBlobLayer.scaleUpdated = { [weak self] scale in + if let strongSelf = self { + strongSelf.updateGlowScale(strongSelf.isActive == true ? scale : nil) + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + private let occlusionDisposable = MetaDisposable() + private var isCurrentlyInHierarchy: Bool = false { + didSet { + updateAnimations() + } + } + + deinit { + occlusionDisposable.dispose() + } + + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() +// isCurrentlyInHierarchy = window != nil + if let window = window as? Window { + occlusionDisposable.set(window.takeOcclusionState.start(next: { [weak self] value in + self?.isCurrentlyInHierarchy = value.contains(.visible) + })) + } else { + occlusionDisposable.set(nil) + isCurrentlyInHierarchy = false + } + } + + + private func setupGradientAnimations() { + if let _ = self.foregroundGradientLayer.animation(forKey: "movement") { + } else { + let previousValue = self.foregroundGradientLayer.startPoint + let newValue: CGPoint + if self.maskBlobLayer.presentationAudioLevel > 0.22 { + newValue = CGPoint(x: CGFloat.random(in: 0.9 ..< 1.0), y: CGFloat.random(in: 0.1 ..< 0.35)) + } else if self.maskBlobLayer.presentationAudioLevel > 0.01 { + newValue = CGPoint(x: CGFloat.random(in: 0.77 ..< 0.95), y: CGFloat.random(in: 0.1 ..< 0.35)) + } else { + newValue = CGPoint(x: CGFloat.random(in: 0.65 ..< 0.85), y: CGFloat.random(in: 0.1 ..< 0.45)) + } + self.foregroundGradientLayer.startPoint = newValue + + CATransaction.begin() + + let animation = CABasicAnimation(keyPath: "startPoint") + animation.duration = Double.random(in: 0.8 ..< 1.4) + animation.fromValue = previousValue + animation.toValue = newValue + + CATransaction.setCompletionBlock { [weak self] in + if let isCurrentlyInHierarchy = self?.isCurrentlyInHierarchy, isCurrentlyInHierarchy { + self?.setupGradientAnimations() + } + } + + self.foregroundGradientLayer.add(animation, forKey: "movement") + CATransaction.commit() + } + } + + private func setupProgressAnimations() { + if let _ = self.maskProgressLayer.animation(forKey: "progressRotation") { + } else { + self.maskProgressLayer.isHidden = false + + let animation = CABasicAnimation(keyPath: "transform.rotation.z") + animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear) + animation.duration = 1.0 + animation.fromValue = NSNumber(value: Float(0.0)) + animation.toValue = NSNumber(value: Float.pi * 2.0) + animation.repeatCount = Float.infinity + animation.beginTime = 0.0 + self.maskProgressLayer.add(animation, forKey: "progressRotation") + + let shrinkAnimation = CABasicAnimation(keyPath: "strokeEnd") + shrinkAnimation.fromValue = 1.0 + shrinkAnimation.toValue = 0.0 + shrinkAnimation.duration = 1.0 + shrinkAnimation.beginTime = 0.0 + + let growthAnimation = CABasicAnimation(keyPath: "strokeEnd") + growthAnimation.fromValue = 0.0 + growthAnimation.toValue = 1.0 + growthAnimation.duration = 1.0 + growthAnimation.beginTime = 1.0 + + let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z") + rotateAnimation.fromValue = 0.0 + rotateAnimation.toValue = CGFloat.pi * 2 + rotateAnimation.isAdditive = true + rotateAnimation.duration = 1.0 + rotateAnimation.beginTime = 1.0 + + let groupAnimation = CAAnimationGroup() + groupAnimation.repeatCount = Float.infinity + groupAnimation.animations = [shrinkAnimation, growthAnimation, rotateAnimation] + groupAnimation.duration = 2.0 + + self.maskProgressLayer.add(groupAnimation, forKey: "progressGrowth") + } + } + + var glowHidden: Bool = false { + didSet { + if self.glowHidden != oldValue { + let initialAlpha = CGFloat(self.maskProgressLayer.opacity) + let targetAlpha: CGFloat = self.glowHidden ? 0.0 : 1.0 + self.maskGradientLayer.opacity = Float(targetAlpha) + self.maskGradientLayer.animateAlpha(from: initialAlpha, to: targetAlpha, duration: 0.2) + } + } + } + + var disableGlowAnimations = false + func updateGlowScale(_ scale: CGFloat?) { + if self.disableGlowAnimations { + return + } + if let scale = scale { + self.maskGradientLayer.transform = CATransform3DMakeScale(0.89 + 0.11 * scale, 0.89 + 0.11 * scale, 1.0) + } else { + let initialScale: CGFloat = ((self.maskGradientLayer.value(forKeyPath: "presentationLayer.transform.scale.x") as? NSNumber)?.floatValue).flatMap({ CGFloat($0) }) ?? (((self.maskGradientLayer.value(forKeyPath: "transform.scale.x") as? NSNumber)?.floatValue).flatMap({ CGFloat($0) }) ?? (0.89)) + let targetScale: CGFloat = self.isActive == true ? 0.89 : 0.85 + if abs(targetScale - initialScale) > 0.03 { + self.maskGradientLayer.transform = CATransform3DMakeScale(targetScale, targetScale, 1.0) + self.maskGradientLayer.animateScale(from: initialScale, to: targetScale, duration: 0.3) + } + } + } + + func updateGlowAndGradientAnimations(active: Bool?, previousActive: Bool? = nil) { + let effectivePreviousActive = previousActive ?? false + + let initialScale: CGFloat = ((self.maskGradientLayer.value(forKeyPath: "presentationLayer.transform.scale.x") as? NSNumber)?.floatValue).flatMap({ CGFloat($0) }) ?? (((self.maskGradientLayer.value(forKeyPath: "transform.scale.x") as? NSNumber)?.floatValue).flatMap({ CGFloat($0) }) ?? (effectivePreviousActive ? 0.95 : 0.8)) + let initialColors = self.foregroundGradientLayer.colors + + let outerColor: NSColor? + let targetColors: [CGColor] + let targetScale: CGFloat + if let active = active { + if active { + targetColors = [activeBlue.cgColor, green.cgColor] + targetScale = 0.89 + outerColor = NSColor(rgb: 0x21674f) + } else { + targetColors = [lightBlue.cgColor, blue.cgColor] + targetScale = 0.85 + outerColor = NSColor(rgb: 0x1d588d) + } + } else { + targetColors = [purple.cgColor, lightPurple.cgColor] + targetScale = 0.3 + outerColor = nil + } + self.updatedOuterColor?(outerColor) + + self.maskGradientLayer.transform = CATransform3DMakeScale(targetScale, targetScale, 1.0) + if let _ = previousActive { + self.maskGradientLayer.animateScale(from: initialScale, to: targetScale, duration: 0.3) + } else { + self.maskGradientLayer.animateSpring(from: initialScale as NSNumber, to: targetScale as NSNumber, keyPath: "transform.scale", duration: 0.45) + } + + self.foregroundGradientLayer.colors = targetColors + self.foregroundGradientLayer.animate(from: initialColors as AnyObject, to: targetColors as AnyObject, keyPath: "colors", timingFunction: .linear, duration: 0.3) + } + + private func playConnectionDisappearanceAnimation() { + let initialRotation: CGFloat = CGFloat((self.maskProgressLayer.value(forKeyPath: "presentationLayer.transform.rotation.z") as? NSNumber)?.floatValue ?? 0.0) + let initialStrokeEnd: CGFloat = CGFloat((self.maskProgressLayer.value(forKeyPath: "presentationLayer.strokeEnd") as? NSNumber)?.floatValue ?? 1.0) + + let maskProgressLayer = self.maskProgressLayer + + maskProgressLayer.removeAnimation(forKey: "progressGrowth") + maskProgressLayer.removeAnimation(forKey: "progressRotation") + + let duration: Double = (1.0 - Double(initialStrokeEnd)) * 0.6 + + let growthAnimation = CABasicAnimation(keyPath: "strokeEnd") + growthAnimation.fromValue = initialStrokeEnd + growthAnimation.toValue = 0.0 + growthAnimation.duration = duration + growthAnimation.isRemovedOnCompletion = false + growthAnimation.timingFunction = CAMediaTimingFunction(name: .easeIn) + + let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z") + rotateAnimation.fromValue = initialRotation + rotateAnimation.toValue = initialRotation + CGFloat.pi * 2 + rotateAnimation.isAdditive = true + rotateAnimation.duration = duration + rotateAnimation.isRemovedOnCompletion = false + rotateAnimation.timingFunction = CAMediaTimingFunction(name: .easeIn) + + let groupAnimation = CAAnimationGroup() + groupAnimation.animations = [growthAnimation, rotateAnimation] + groupAnimation.duration = duration + groupAnimation.isRemovedOnCompletion = false + + CATransaction.begin() + + self.maskProgressLayer.animateAlpha(from: 1, to: 0, duration: duration, removeOnCompletion: false) + + CATransaction.setCompletionBlock { + maskProgressLayer.isHidden = true + maskProgressLayer.removeAllAnimations() + } + + self.maskProgressLayer.add(groupAnimation, forKey: "progressDisappearance") + CATransaction.commit() + } + + var animatingDisappearance = false + private func playBlobsDisappearanceAnimation(wasActive: Bool? = nil) { + if self.animatingDisappearance { + return + } + self.animatingDisappearance = true + CATransaction.begin() + CATransaction.setDisableActions(true) + self.growingForegroundCircleLayer.isHidden = false + CATransaction.commit() + + self.disableGlowAnimations = true + self.maskGradientLayer.removeAllAnimations() + self.updateGlowAndGradientAnimations(active: wasActive, previousActive: nil) + + self.maskBlobLayer.startAnimating() + CATransaction.begin() + self.maskBlobLayer.animateScale(from: 1.0, to: 0, duration: 0.15, removeOnCompletion: false) + CATransaction.setCompletionBlock { + self.maskBlobLayer.isHidden = true + self.maskBlobLayer.stopAnimating() + self.maskBlobLayer.removeAllAnimations() + } + CATransaction.commit() + CATransaction.begin() + let growthAnimation = CABasicAnimation(keyPath: "transform.scale") + growthAnimation.fromValue = 0.0 + growthAnimation.toValue = 1.0 + growthAnimation.duration = 0.15 + growthAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut) + growthAnimation.isRemovedOnCompletion = false + + CATransaction.setCompletionBlock { + CATransaction.begin() + CATransaction.setDisableActions(true) + self.disableGlowAnimations = false + self.maskGradientLayer.isHidden = true + self.maskCircleLayer.isHidden = true + self.growingForegroundCircleLayer.isHidden = true + self.growingForegroundCircleLayer.removeAllAnimations() + self.animatingDisappearance = false + CATransaction.commit() + } + + self.growingForegroundCircleLayer.add(growthAnimation, forKey: "insideGrowth") + CATransaction.commit() + } + + private func playBlobsAppearanceAnimation(active: Bool) { + CATransaction.begin() + CATransaction.setDisableActions(true) + self.foregroundCircleLayer.isHidden = false + self.maskCircleLayer.isHidden = false + self.maskProgressLayer.isHidden = true + self.maskGradientLayer.isHidden = false + CATransaction.commit() + + self.disableGlowAnimations = true + self.maskGradientLayer.removeAllAnimations() + self.updateGlowAndGradientAnimations(active: active, previousActive: nil) + + self.maskBlobLayer.isHidden = false + self.maskBlobLayer.startAnimating() + self.maskBlobLayer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45) + + CATransaction.begin() + let shrinkAnimation = CABasicAnimation(keyPath: "transform.scale") + shrinkAnimation.fromValue = 1.0 + shrinkAnimation.toValue = 0.0 + shrinkAnimation.duration = 0.15 + shrinkAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn) + + CATransaction.setCompletionBlock { + CATransaction.begin() + CATransaction.setDisableActions(true) + self.disableGlowAnimations = false + self.foregroundCircleLayer.isHidden = true + CATransaction.commit() + } + + self.foregroundCircleLayer.add(shrinkAnimation, forKey: "insideShrink") + CATransaction.commit() + } + + private func setDisabledBlobWithoutAnimation() { + CATransaction.begin() + CATransaction.disableActions() + self.foregroundCircleLayer.isHidden = false + self.maskCircleLayer.isHidden = false + self.maskProgressLayer.isHidden = true + self.maskGradientLayer.isHidden = false + self.updateGlowAndGradientAnimations(active: self.isActive, previousActive: nil) + self.maskBlobLayer.isHidden = false + self.maskBlobLayer.startAnimating() + CATransaction.commit() + } + + private func playConnectionAnimation(active: Bool?, completion: @escaping () -> Void) { + CATransaction.begin() + let initialRotation: CGFloat = CGFloat((self.maskProgressLayer.value(forKeyPath: "presentationLayer.transform.rotation.z") as? NSNumber)?.floatValue ?? 0.0) + let initialStrokeEnd: CGFloat = CGFloat((self.maskProgressLayer.value(forKeyPath: "presentationLayer.strokeEnd") as? NSNumber)?.floatValue ?? 1.0) + + self.maskProgressLayer.removeAnimation(forKey: "progressGrowth") + self.maskProgressLayer.removeAnimation(forKey: "progressRotation") + + let duration: Double = (1.0 - Double(initialStrokeEnd)) * 0.3 + + let growthAnimation = CABasicAnimation(keyPath: "strokeEnd") + growthAnimation.fromValue = initialStrokeEnd + growthAnimation.toValue = 1.0 + growthAnimation.duration = duration + growthAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn) + + let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z") + rotateAnimation.fromValue = initialRotation + rotateAnimation.toValue = initialRotation + CGFloat.pi * 2 + rotateAnimation.isAdditive = true + rotateAnimation.duration = duration + rotateAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn) + + let groupAnimation = CAAnimationGroup() + groupAnimation.animations = [growthAnimation, rotateAnimation] + groupAnimation.duration = duration + + CATransaction.setCompletionBlock { + CATransaction.begin() + CATransaction.setDisableActions(true) + self.foregroundCircleLayer.isHidden = false + self.maskCircleLayer.isHidden = false + self.maskProgressLayer.isHidden = true + self.maskGradientLayer.isHidden = false + CATransaction.commit() + + completion() + + + self.updateGlowAndGradientAnimations(active: self.isActive, previousActive: nil) + + self.maskBlobLayer.isHidden = false + self.maskBlobLayer.startAnimating() + self.maskBlobLayer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.45) + + self.updatedActive?(true) + + CATransaction.begin() + let shrinkAnimation = CABasicAnimation(keyPath: "transform.scale") + shrinkAnimation.fromValue = 1.0 + shrinkAnimation.toValue = 0.0 + shrinkAnimation.duration = 0.15 + shrinkAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn) + + CATransaction.setCompletionBlock { + CATransaction.begin() + CATransaction.setDisableActions(true) + self.foregroundCircleLayer.isHidden = true + CATransaction.commit() + } + + self.foregroundCircleLayer.add(shrinkAnimation, forKey: "insideShrink") + CATransaction.commit() + } + + self.maskProgressLayer.add(groupAnimation, forKey: "progressCompletion") + CATransaction.commit() + } + + var isActive: Bool? = nil { + didSet { + + } + } + func updateAnimations() { + if !self.isCurrentlyInHierarchy { + self.foregroundGradientLayer.removeAllAnimations() + self.maskGradientLayer.removeAllAnimations() + self.maskProgressLayer.removeAllAnimations() + self.maskBlobLayer.stopAnimating() + return + } else { + self.maskBlobLayer.startAnimating() + } + self.setupGradientAnimations() + + switch self.state { + case .connecting: + self.updatedActive?(false) + if let transition = self.transition { + self.updateGlowScale(nil) + if case let .blob(active) = transition { + playBlobsDisappearanceAnimation(wasActive: active) + } else if case .disabled = transition { + playBlobsDisappearanceAnimation(wasActive: nil) + } + self.transition = nil + } + self.setupProgressAnimations() + self.isActive = false + case let .blob(newActive): + if let transition = self.transition { + if transition == .connecting { + self.playConnectionAnimation(active: newActive) { [weak self] in + if self?.transition == transition { + self?.isActive = newActive + } + } + } else if transition == .disabled { + updateGlowAndGradientAnimations(active: newActive, previousActive: nil) + self.transition = nil + self.isActive = newActive + self.updatedActive?(true) + } else if case let .blob(previousActive) = transition { + updateGlowAndGradientAnimations(active: newActive, previousActive: previousActive) + self.transition = nil + self.isActive = newActive + } + self.transition = nil + } else { + self.maskBlobLayer.startAnimating() + } + case .disabled: + self.updatedActive?(true) + self.isActive = nil + + if let transition = self.transition { + if case .connecting = transition { + self.playConnectionAnimation(active: nil) { [weak self] in + self?.isActive = nil + } + } else if case let .blob(previousActive) = transition { + updateGlowAndGradientAnimations(active: nil, previousActive: previousActive) + } + self.transition = nil + } else { + setDisabledBlobWithoutAnimation() + } + break + } + } + + var isDark: Bool = false { + didSet { + if self.isDark != oldValue { + self.updateColors() + } + } + } + + var isSnap: Bool = false { + didSet { + if self.isSnap != oldValue { + self.updateColors() + } + } + } + + var connectingColor: NSColor = NSColor(rgb: 0xb6b6bb) { + didSet { + if self.connectingColor.rgb != oldValue.rgb { + self.updateColors() + } + } + } + + func updateColors() { + let previousColor: CGColor = self.backgroundCircleLayer.fillColor ?? greyColor.cgColor + let targetColor: CGColor + if self.isSnap { + targetColor = self.connectingColor.cgColor + } else if self.isDark { + targetColor = secondaryGreyColor.cgColor + } else { + targetColor = greyColor.cgColor + } + self.backgroundCircleLayer.fillColor = targetColor + self.foregroundCircleLayer.fillColor = targetColor + self.growingForegroundCircleLayer.fillColor = targetColor + self.backgroundCircleLayer.animate(from: previousColor, to: targetColor, keyPath: "fillColor", timingFunction: .linear, duration: 0.3) + self.foregroundCircleLayer.animate(from: previousColor, to: targetColor, keyPath: "fillColor", timingFunction: .linear, duration: 0.3) + self.growingForegroundCircleLayer.animate(from: previousColor, to: targetColor, keyPath: "fillColor", timingFunction: .linear, duration: 0.3) + } + + func update(state: State, animated: Bool) { + var animated = animated + var hadState = true + if !self.hasState { + hadState = false + self.hasState = true + animated = false + } + + if state != self.state || !hadState { + if animated { + self.transition = self.state + } + self.state = state + + self.updateAnimations() + } + + } + + override func layout() { + super.layout() + + let center = CGPoint(x: self.bounds.width / 2.0, y: self.bounds.height / 2.0) + let circleFrame = CGRect(origin: CGPoint(x: (self.bounds.width - buttonSize.width) / 2.0, y: (self.bounds.height - buttonSize.height) / 2.0), size: buttonSize) + self.backgroundCircleLayer.frame = circleFrame + self.foregroundCircleLayer.position = center + self.foregroundCircleLayer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: circleFrame.width - progressLineWidth, height: circleFrame.height - progressLineWidth)) + self.growingForegroundCircleLayer.position = center + self.growingForegroundCircleLayer.bounds = self.foregroundCircleLayer.bounds + self.maskCircleLayer.frame = circleFrame.insetBy(dx: -progressLineWidth / 2.0, dy: -progressLineWidth / 2.0) + self.maskProgressLayer.frame = circleFrame.insetBy(dx: -3.0, dy: -3.0) + self.foregroundLayer.frame = self.bounds + self.foregroundGradientLayer.frame = self.bounds + self.maskGradientLayer.position = center + self.maskGradientLayer.bounds = NSMakeRect(0, 0, bounds.width - 80, bounds.height - 80) + self.maskLayer.frame = self.bounds + + let point = CGPoint(x: (bounds.width - (bounds.width - 170)) / 2.0, y: (bounds.height - (bounds.height - 170)) / 2.0) + + self.maskBlobLayer.frame = .init(origin: point, size: NSMakeSize((bounds.width - 170), (bounds.height - 170))) + } +} + + + + + +final class VoiceChatBlobLayer: CALayer { + private let mediumBlob: BlobLayer + private let bigBlob: BlobLayer + + private let maxLevel: CGFloat + + private var displayLinkAnimator: ConstantDisplayLinkAnimator? + + private var audioLevel: CGFloat = 0.0 + var presentationAudioLevel: CGFloat = 0.0 + + var scaleUpdated: ((CGFloat) -> Void)? { + didSet { + self.bigBlob.scaleUpdated = self.scaleUpdated + } + } + + private(set) var isAnimating = false + + public typealias BlobRange = (min: CGFloat, max: CGFloat) + + public init( + frame: CGRect, + maxLevel: CGFloat, + mediumBlobRange: BlobRange, + bigBlobRange: BlobRange + ) { + self.maxLevel = maxLevel + + self.mediumBlob = BlobLayer( + pointsCount: 8, + minRandomness: 1, + maxRandomness: 1, + minSpeed: 0.9, + maxSpeed: 4.0, + minScale: mediumBlobRange.min, + maxScale: mediumBlobRange.max + ) + self.bigBlob = BlobLayer( + pointsCount: 8, + minRandomness: 1, + maxRandomness: 1, + minSpeed: 1.0, + maxSpeed: 4.4, + minScale: bigBlobRange.min, + maxScale: bigBlobRange.max + ) + + super.init() + + addSublayer(bigBlob) + addSublayer(mediumBlob) + + self.frame = frame + + + displayLinkAnimator = ConstantDisplayLinkAnimator() { [weak self] in + guard let strongSelf = self else { return } + + strongSelf.presentationAudioLevel = strongSelf.presentationAudioLevel * 0.9 + strongSelf.audioLevel * 0.1 + + strongSelf.mediumBlob.level = strongSelf.presentationAudioLevel + strongSelf.bigBlob.level = strongSelf.presentationAudioLevel + } + } + + override init(layer: Any) { + let mediumBlobRange:BlobRange = (0.69, 0.87) + let bigBlobRange:BlobRange = (0.71, 1.0) + self.maxLevel = 1.5 + + self.mediumBlob = BlobLayer( + pointsCount: 8, + minRandomness: 1, + maxRandomness: 1, + minSpeed: 0.9, + maxSpeed: 4.0, + minScale: mediumBlobRange.min, + maxScale: mediumBlobRange.max + ) + self.bigBlob = BlobLayer( + pointsCount: 8, + minRandomness: 1, + maxRandomness: 1, + minSpeed: 1.0, + maxSpeed: 4.4, + minScale: bigBlobRange.min, + maxScale: bigBlobRange.max + ) + super.init(layer: layer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func setColor(_ color: NSColor) { + mediumBlob.setColor(color.withAlphaComponent(0.55)) + bigBlob.setColor(color.withAlphaComponent(0.35)) + } + + public func updateLevel(_ level: CGFloat) { + let normalizedLevel = min(1, max(level / maxLevel, 0)) + + mediumBlob.updateSpeedLevel(to: normalizedLevel) + bigBlob.updateSpeedLevel(to: normalizedLevel) + + audioLevel = normalizedLevel + } + + public func startAnimating() { + guard !isAnimating else { return } + isAnimating = true + + updateBlobsState() + + displayLinkAnimator?.isPaused = false + } + + public func stopAnimating() { + self.stopAnimating(duration: 0.15) + } + + public func stopAnimating(duration: Double) { + guard isAnimating else { return } + isAnimating = false + + updateBlobsState() + + displayLinkAnimator?.isPaused = true + } + + private func updateBlobsState() { + if isAnimating { + if mediumBlob.frame.size != .zero { + mediumBlob.startAnimating() + bigBlob.startAnimating() + } + } else { + mediumBlob.stopAnimating() + bigBlob.stopAnimating() + } + } + + override var frame: CGRect { + didSet { + CATransaction.begin() + CATransaction.disableActions() + mediumBlob.frame = bounds + bigBlob.frame = bounds + + updateBlobsState() + CATransaction.commit() + } + } +} + +final class BlobLayer: CAShapeLayer { + let pointsCount: Int + let smoothness: CGFloat + + let minRandomness: CGFloat + let maxRandomness: CGFloat + + let minSpeed: CGFloat + let maxSpeed: CGFloat + + let minScale: CGFloat + let maxScale: CGFloat + + var scaleUpdated: ((CGFloat) -> Void)? + + private var blobAnimation: DisplayLinkAnimator? + + + private let shapeLayer: CAShapeLayer = { + let layer = CAShapeLayer() + layer.strokeColor = nil + return layer + }() + + + var level: CGFloat = 0 { + didSet { + CATransaction.begin() + CATransaction.setDisableActions(true) + let lv = minScale + (maxScale - minScale) * level + shapeLayer.transform = CATransform3DMakeScale(lv, lv, 1) + if level != oldValue { + self.scaleUpdated?(level) + } + CATransaction.commit() + } + } + + private var speedLevel: CGFloat = 0 + private var lastSpeedLevel: CGFloat = 0 + + + private var transition: CGFloat = 0 { + didSet { + guard let currentPoints = currentPoints else { return } + + shapeLayer.path = CGPath.smoothCurve(through: currentPoints, length: bounds.width, smoothness: smoothness) + } + } + + private var fromPoints: [CGPoint]? + private var toPoints: [CGPoint]? + + private var currentPoints: [CGPoint]? { + guard let fromPoints = fromPoints, let toPoints = toPoints else { return nil } + + return fromPoints.enumerated().map { offset, fromPoint in + let toPoint = toPoints[offset] + return CGPoint( + x: fromPoint.x + (toPoint.x - fromPoint.x) * transition, + y: fromPoint.y + (toPoint.y - fromPoint.y) * transition + ) + } + } + + init( + pointsCount: Int, + minRandomness: CGFloat, + maxRandomness: CGFloat, + minSpeed: CGFloat, + maxSpeed: CGFloat, + minScale: CGFloat, + maxScale: CGFloat + ) { + self.pointsCount = pointsCount + self.minRandomness = minRandomness + self.maxRandomness = maxRandomness + self.minSpeed = minSpeed + self.maxSpeed = maxSpeed + self.minScale = minScale + self.maxScale = maxScale + + let angle = (CGFloat.pi * 2) / CGFloat(pointsCount) + self.smoothness = ((4 / 3) * tan(angle / 4)) / sin(angle / 2) / 2 + + super.init() + + self.addSublayer(shapeLayer) + shapeLayer.transform = CATransform3DMakeScale(minScale, minScale, 1) + + } + + override init(layer: Any) { + let prev = layer as! BlobLayer + self.pointsCount = prev.pointsCount + self.minRandomness = prev.minRandomness + self.maxRandomness = prev.maxRandomness + self.minSpeed = prev.minSpeed + self.maxSpeed = prev.maxSpeed + self.minScale = prev.minScale + self.maxScale = prev.maxScale + let angle = (CGFloat.pi * 2) / CGFloat(pointsCount) + self.smoothness = ((4 / 3) * tan(angle / 4)) / sin(angle / 2) / 2 + super.init(layer: layer) + self.addSublayer(shapeLayer) + shapeLayer.transform = CATransform3DMakeScale(minScale, minScale, 1) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setColor(_ color: NSColor) { + shapeLayer.fillColor = color.cgColor + } + + func updateSpeedLevel(to newSpeedLevel: CGFloat) { + speedLevel = max(speedLevel, newSpeedLevel) + + } + + func startAnimating() { + animateToNewShape() + } + + func stopAnimating() { + fromPoints = currentPoints + toPoints = nil + blobAnimation = nil + } + + private func animateToNewShape() { + if blobAnimation != nil { + fromPoints = currentPoints + toPoints = nil + blobAnimation = nil + } + + if fromPoints == nil { + fromPoints = generateNextBlob(for: bounds.size) + } + if toPoints == nil { + toPoints = generateNextBlob(for: bounds.size) + } + + let duration = CGFloat(1 / (minSpeed + (maxSpeed - minSpeed) * speedLevel)) + let fromValue: CGFloat = 0 + let toValue: CGFloat = 1 + + let animation = DisplayLinkAnimator(duration: Double(duration), from: fromValue, to: toValue, update: { [weak self] value in + self?.transition = value + }, completion: { [weak self] in + guard let `self` = self else { + return + } + self.fromPoints = self.currentPoints + self.toPoints = nil + self.blobAnimation = nil + self.animateToNewShape() + }) + self.blobAnimation = animation + + lastSpeedLevel = speedLevel + speedLevel = 0 + } + + private func generateNextBlob(for size: CGSize) -> [CGPoint] { + let randomness = minRandomness + (maxRandomness - minRandomness) * speedLevel + return blob(pointsCount: pointsCount, randomness: randomness) + .map { + return CGPoint( + x: $0.x * CGFloat(size.width), + y: $0.y * CGFloat(size.height) + ) + } + } + + func blob(pointsCount: Int, randomness: CGFloat) -> [CGPoint] { + let angle = (CGFloat.pi * 2) / CGFloat(pointsCount) + + let rgen = { () -> CGFloat in + let accuracy: UInt32 = 1000 + let random = arc4random_uniform(accuracy) + return CGFloat(random) / CGFloat(accuracy) + } + let rangeStart: CGFloat = 1 / (1 + randomness / 10) + + let startAngle = angle * CGFloat(arc4random_uniform(100)) / CGFloat(100) + + let points = (0 ..< pointsCount).map { i -> CGPoint in + let randPointOffset = (rangeStart + CGFloat(rgen()) * (1 - rangeStart)) / 2 + let angleRandomness: CGFloat = angle * 0.1 + let randAngle = angle + angle * ((angleRandomness * CGFloat(arc4random_uniform(100)) / CGFloat(100)) - angleRandomness * 0.5) + let pointX = sin(startAngle + CGFloat(i) * randAngle) + let pointY = cos(startAngle + CGFloat(i) * randAngle) + return CGPoint( + x: pointX * randPointOffset, + y: pointY * randPointOffset + ) + } + + return points + } + + + override var frame: CGRect { + didSet { + shapeLayer.position = CGPoint(x: bounds.width / 2, y: bounds.height / 2) + } + } +} diff --git a/Telegram-Mac/VoipDerivedState.swift b/Telegram-Mac/VoipDerivedState.swift new file mode 100644 index 0000000000..879b9910fa --- /dev/null +++ b/Telegram-Mac/VoipDerivedState.swift @@ -0,0 +1,53 @@ +// +// VoipDerivedState.swift +// Telegram +// +// Created by Mikhail Filimonov on 23/06/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Foundation +import Postbox +import SwiftSignalKit + +public struct VoipDerivedState: Equatable, PreferencesEntry { + public var data: Data + + public static var `default`: VoipDerivedState { + return VoipDerivedState(data: Data()) + } + + public init(data: Data) { + self.data = data + } + + public init(decoder: PostboxDecoder) { + self.data = decoder.decodeDataForKey("data") ?? Data() + } + + public func encode(_ encoder: PostboxEncoder) { + encoder.encodeData(self.data, forKey: "data") + } + + public func isEqual(to: PreferencesEntry) -> Bool { + if let to = to as? VoipDerivedState { + return self == to + } else { + return false + } + } +} + +public func updateVoipDerivedStateInteractively(postbox: Postbox, _ f: @escaping (VoipDerivedState) -> VoipDerivedState) -> Signal { + return postbox.transaction { transaction -> Void in + transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.voipDerivedState, { entry in + let currentSettings: VoipDerivedState + if let entry = entry as? VoipDerivedState { + currentSettings = entry + } else { + currentSettings = .default + } + return f(currentSettings) + }) + } +} diff --git a/Telegram-Mac/VolumeControllerPopover.swift b/Telegram-Mac/VolumeControllerPopover.swift new file mode 100644 index 0000000000..38e7bee7c6 --- /dev/null +++ b/Telegram-Mac/VolumeControllerPopover.swift @@ -0,0 +1,47 @@ +// +// VolumeControllerPopover.swift +// Telegram +// +// Created by Mikhail Filimonov on 28/07/2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +class VolumeControllerPopover: GenericViewController { + + + private let initialValue: CGFloat + private let updatedValue: (CGFloat)->Void + init(initialValue: CGFloat, updatedValue: @escaping(CGFloat)->Void) { + self.initialValue = initialValue + self.updatedValue = updatedValue + super.init(frame: NSMakeRect(0, 0, 30, 100)) + bar = .init(height: 0) + } + + override var isAutoclosePopover: Bool { + return false + } + + override func viewDidLoad() { + super.viewDidLoad() + + genericView.value = initialValue + genericView.updateInteractiveValue = updatedValue + + readyOnce() + } + + override func becomeFirstResponder() -> Bool? { + return nil + } + + var value:CGFloat = 0 { + didSet { + genericView.value = value + } + } + +} diff --git a/Telegram-Mac/VolumeMenuItemView.swift b/Telegram-Mac/VolumeMenuItemView.swift new file mode 100644 index 0000000000..ed60b2246b --- /dev/null +++ b/Telegram-Mac/VolumeMenuItemView.swift @@ -0,0 +1,144 @@ +// +// VolumeMenuItemView.swift +// Telegram +// +// Created by Mikhail Filimonov on 11.01.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit + + + +final class VolumeMenuItemView : Control { + + var didUpdateValue:((CGFloat, Bool)->Void)? = nil + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + } + + var stateImages:(on: CGImage, off: CGImage)? = nil + + + private var lineRect: CGRect { + if let _ = stateImages { + return .init(origin: .init(x: 40, y: (frame.height - 2 ) / 2), size: .init(width: frame.width - 55, height: 2)) + } else { + return focus(NSMakeSize(frame.width - 30, 2)) + } + } + + private var blobSize: NSSize { + return NSMakeSize(6, 10) + } + + private func updateValue(_ event: NSEvent) { + let point = convert(event.locationInWindow, from: nil) + let percentValue = ((point.x - lineRect.minX + 6 / 2) / lineRect.width) * maxValue + var currentValue = min(max(minValue, percentValue), maxValue) + + let mid = (maxValue - minValue) / 2 + let magnify: CGFloat = 0.08 + + if currentValue > mid - magnify && currentValue < mid + magnify { + currentValue = mid + } + if currentValue <= minValue + magnify { + currentValue = minValue + } + if currentValue >= maxValue - magnify { + currentValue = maxValue + } + self.value = currentValue + } + + override func mouseDown(with event: NSEvent) { + super.mouseDown(with: event) + let oldValue = self.value + updateValue(event) + if oldValue != value { + didUpdateValue?(value, true) + } + + } + override func mouseUp(with event: NSEvent) { + super.mouseUp(with: event) + didUpdateValue?(value, true) + } + override func mouseDragged(with event: NSEvent) { + super.mouseDragged(with: event) + let oldValue = self.value + updateValue(event) + if oldValue != value { + didUpdateValue?(value, true) + } + } + + var value: CGFloat = 1 { + didSet { + needsDisplay = true + } + } + var minValue: CGFloat = 0 { + didSet { + needsDisplay = true + } + } + var maxValue: CGFloat = 2 { + didSet { + needsDisplay = true + } + } + + var lineColor: NSColor = GroupCallTheme.memberSeparatorColor.lighter().withAlphaComponent(0.8) + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + super.draw(layer, in: ctx) + + + if let stateImages = stateImages { + var imageRect = focus(stateImages.on.backingSize) + imageRect.origin.x = 10 + if value == 0 { + ctx.draw(stateImages.off, in: imageRect) + } else { + ctx.draw(stateImages.on, in: imageRect) + } + } + + ctx.setFillColor(lineColor.cgColor) + + let linePath = CGMutablePath() + linePath.addRoundedRect(in: lineRect, cornerWidth: 1, cornerHeight: 1) + + + + linePath.addRoundedRect(in: NSMakeRect(lineRect.minX - 1, (frame.height - 6) / 2, 2, 6), cornerWidth: 1, cornerHeight: 1) + linePath.addRoundedRect(in: NSMakeRect(lineRect.maxX - 1, (frame.height - 6) / 2, 2, 6), cornerWidth: 1, cornerHeight: 1) + linePath.addRoundedRect(in: NSMakeRect(lineRect.midX - 1, (frame.height - 6) / 2, 2, 6), cornerWidth: 1, cornerHeight: 1) + + + ctx.addPath(linePath) + ctx.fillPath() + + + + let blobRect = CGRect(origin: NSMakePoint(lineRect.minX + ((value * lineRect.width) / maxValue) - 5, (frame.height - blobSize.height) / 2), size: blobSize) + + let blobPath = CGMutablePath() + blobPath.addRoundedRect(in: blobRect, cornerWidth: blobRect.width / 2, cornerHeight: blobRect.width / 2) + + ctx.setFillColor(.white) + + ctx.addPath(blobPath) + ctx.fillPath() + + + } +} diff --git a/Telegram-Mac/WPArticleContentView.swift b/Telegram-Mac/WPArticleContentView.swift index fba9fb0108..7532c3692b 100644 --- a/Telegram-Mac/WPArticleContentView.swift +++ b/Telegram-Mac/WPArticleContentView.swift @@ -8,25 +8,45 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac +import Postbox +import TelegramCore + +import SwiftSignalKit class WPArticleContentView: WPContentView { private var durationView:VideoDurationView? private var progressIndicator:ProgressIndicator? private(set) var imageView:TransformImageView? + private(set) var gradientView: BackgroundView? private var playIcon:ImageView? private let openExternalDisposable:MetaDisposable = MetaDisposable() private let loadingStatusDisposable: MetaDisposable = MetaDisposable() + private let fetchDisposable = MetaDisposable() + private let statusDisposable = MetaDisposable() + private var countAccessoryView: ChatMessageAccessoryView? + private var downloadIndicator: RadialProgressView? + + private var groupedContents: [ChatMediaContentView] = [] + private let groupedContentView: View = View() override var backgroundColor: NSColor { didSet { self.setNeedsDisplay() } } - + override func fileAtPoint(_ point: NSPoint) -> (QuickPreviewMedia, NSView?)? { + if let _ = imageView, let content = content as? WPArticleLayout, content.isFullImageSize, let image = content.content.image { + return (.image(ImageMediaReference.webPage(webPage: WebpageReference(content.webPage), media: image), ImagePreviewModalView.self), imageView) + } + return nil + } + + override func previewMediaIfPossible() -> Bool { + guard let window = self.kitWindow, let content = content as? WPArticleLayout, content.isFullImageSize, let table = content.table, let imageView = imageView, imageView._mouseInside(), playIcon == nil, !content.hasInstantPage else {return false} + _ = startModalPreviewHandle(table, window: window, context: content.context) + return true + } required public init() { super.init() @@ -35,6 +55,8 @@ class WPArticleContentView: WPContentView { deinit { openExternalDisposable.dispose() loadingStatusDisposable.dispose() + statusDisposable.dispose() + fetchDisposable.dispose() } override func viewDidMoveToSuperview() { @@ -56,17 +78,29 @@ class WPArticleContentView: WPContentView { fatalError("init(frame:) has not been implemented") } + override func updateMouse() { + super.updateMouse() + for content in groupedContentView.subviews.compactMap({$0 as? ChatMediaContentView}) { + content.updateMouse() + } + } + func open() { if let content = content?.content, let layout = self.content, let window = kitWindow { + + if layout.hasInstantPage { + showInstantPage(InstantPageViewController(layout.context, webPage: layout.parent.media[0] as! TelegramMediaWebpage, message: layout.parent.text)) + return + } + if ExternalVideoLoader.isPlayable(content) { - openExternalDisposable.set((sharedVideoLoader.status(for: content) |> deliverOnMainQueue).start(next: { (status) in if let status = status { switch status { case .fail: execute(inapp: .external(link: content.url, false)) case .loaded: - showChatGallery(account: layout.account, message: layout.parent, layout.table) + showChatGallery(context: layout.context, message: layout.parent, layout.table) default: break } @@ -77,19 +111,53 @@ class WPArticleContentView: WPContentView { return } if content.embedType == "iframe" { - showModal(with: WebpageModalController(content:content,account:layout.account), for: window) - } else if (content.type == "video" && content.type == "video/mp4") || content.type == "photo" { - showChatGallery(account: layout.account, message: layout.parent, layout.table) - } else { + showModal(with: WebpageModalController(content:content, context: layout.context), for: window) + } else if layout.isGalleryAssemble { + showChatGallery(context: layout.context, message: layout.parent, layout.table, type: .alone) + } else if let wallpaper = layout.wallpaper { + execute(inapp: wallpaper) + } else if let link = layout.themeLink { + execute(inapp: link) + } else if !content.url.isEmpty { execute(inapp: .external(link: content.url, false)) } } } + func fetch() { + if let layout = content as? WPArticleLayout { + if let _ = layout.wallpaper, let file = layout.content.file { + + fetchDisposable.set(fetchedMediaResource(mediaBox: layout.context.account.postbox.mediaBox, reference: MediaResourceReference.wallpaper(wallpaper: layout.wallpaperReference, resource: file.resource)).start()) + } else if let image = layout.content.image { + fetchDisposable.set(chatMessagePhotoInteractiveFetched(account: layout.context.account, imageReference: ImageMediaReference.webPage(webPage: WebpageReference(layout.webPage), media: image)).start()) + } else if layout.isTheme, let file = layout.content.file { + fetchDisposable.set(fetchedMediaResource(mediaBox: layout.context.account.postbox.mediaBox, reference: MediaResourceReference.wallpaper(wallpaper: layout.wallpaperReference, resource: file.resource)).start()) + } + } + } + + func cancelFetching() { + if let layout = content as? WPArticleLayout { + if let _ = layout.wallpaper, let file = layout.content.file { + fileCancelInteractiveFetch(account: layout.context.account, file: file) + } else if let image = layout.content.image { + chatMessagePhotoCancelInteractiveFetch(account: layout.context.account, photo: image) + } else if layout.isTheme, let file = layout.content.file { + fileCancelInteractiveFetch(account: layout.context.account, file: file) + } + fetchDisposable.set(nil) + } + } + override func mouseUp(with event: NSEvent) { if let imageView = imageView, imageView._mouseInside(), event.clickCount == 1 { - open() + if let downloadProgressView = downloadIndicator { + downloadProgressView.fetchControls?.fetch() + } else { + open() + } } else { super.mouseUp(with: event) } @@ -97,9 +165,69 @@ class WPArticleContentView: WPContentView { + + override func update(with layout: WPLayout) { - + let newLayout = self.content?.content.displayUrl != layout.content.displayUrl if let layout = layout as? WPArticleLayout { + + let synchronousLoad = layout.approximateSynchronousValue + + if let groupLayout = layout.groupLayout { + addSubview(groupedContentView) + groupedContentView.setFrameSize(groupLayout.dimensions) + + if groupedContents.count > groupLayout.count { + let contentCount = groupedContents.count + let layoutCount = groupLayout.count + + for i in layoutCount ..< contentCount { + groupedContents[i].removeFromSuperview() + } + groupedContents = groupedContents.subarray(with: NSMakeRange(0, layoutCount)) + + for i in 0 ..< groupedContents.count { + if !groupedContents[i].isKind(of: groupLayout.contentNode(for: i)) { + let node = groupLayout.contentNode(for: i) + let view = node.init(frame:NSZeroRect) + replaceSubview(groupedContents[i], with: view) + groupedContents[i] = view + } + } + } else if groupedContents.count < groupLayout.count { + let contentCount = groupedContents.count + for i in contentCount ..< groupLayout.count { + let node = groupLayout.contentNode(for: i) + let view = node.init(frame:NSZeroRect) + groupedContents.append(view) + } + } + + for content in groupedContents { + groupedContentView.addSubview(content) + } + + assert(groupedContents.count == groupLayout.count) + + for i in 0 ..< groupLayout.count { + groupedContents[i].change(size: groupLayout.frame(at: i).size, animated: false) + let positionFlags: LayoutPositionFlags = groupLayout.position(at: i) + + + groupedContents[i].update(with: groupLayout.messages[i].media[0], size: groupLayout.frame(at: i).size, context: layout.context, parent: layout.parent.withUpdatedGroupingKey(groupLayout.messages[i].groupingKey), table: layout.table, parameters: layout.parameters[i], animated: false, positionFlags: positionFlags, approximateSynchronousValue: synchronousLoad) + + groupedContents[i].change(pos: groupLayout.frame(at: i).origin, animated: false) + } + + } else { + while !groupedContents.isEmpty { + groupedContents[0].removeFromSuperview() + groupedContents.removeFirst() + } + groupedContentView.removeFromSuperview() + } + + if ExternalVideoLoader.isPlayable(layout.content) { loadingStatusDisposable.set((sharedVideoLoader.status(for: layout.content) |> deliverOnMainQueue).start(next: { [weak self] status in if let status = status , let strongSelf = self { @@ -110,6 +238,7 @@ class WPArticleContentView: WPContentView { // self?.progressIndicator?.set(color: .white) strongSelf.imageView?.addSubview((strongSelf.progressIndicator)!) } + strongSelf.progressIndicator?.center() strongSelf.progressIndicator?.animates = true default: strongSelf.progressIndicator?.animates = false @@ -125,48 +254,200 @@ class WPArticleContentView: WPContentView { progressIndicator = nil } - - var updateImageSignal:Signal<(TransformImageArguments) -> DrawingContext?, NoError>? - if self.content?.content.image != layout.content.image { - if let image = layout.content.image { - updateImageSignal = chatWebpageSnippetPhoto(account: layout.account, photo: image, scale: backingScaleFactor, small:layout.smallThumb) - - if imageView == nil { - imageView = TransformImageView() - imageView?.alphaTransitionOnFirstUpdate = true - self.addSubview(imageView!) - } - - if ExternalVideoLoader.isPlayable(layout.content) { - if playIcon == nil { - playIcon = ImageView() - imageView?.addSubview(playIcon!) + var image = layout.content.image + if layout.content.image == nil, let file = layout.content.file, let dimension = layout.imageSize { + var representations: [TelegramMediaImageRepresentation] = [] + representations.append(contentsOf: file.previewRepresentations) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(dimension), resource: file.resource, progressiveSizes: [], immediateThumbnailData: nil)) + image = TelegramMediaImage(imageId: file.id ?? MediaId(namespace: 0, id: arc4random64()), representations: representations, immediateThumbnailData: file.immediateThumbnailData, reference: nil, partialReference: file.partialReference, flags: []) + + } + var updateImageSignal:Signal? + if let image = image { + if layout.wallpaper != nil || layout.isTheme { + let isPattern: Bool + if let settings = layout.content.themeSettings, let wallpaper = settings.wallpaper { + switch wallpaper { + case let .file(file): + isPattern = file.isPattern + default: + isPattern = false } - playIcon?.image = ExternalVideoLoader.playIcon(layout.content) - playIcon?.sizeToFit() } else { - playIcon?.removeFromSuperview() - playIcon = nil + isPattern = layout.isPatternWallpaper } - - if let imageSize = layout.imageArguments?.imageSize { - imageView?.setSignal(signal: cachedMedia(media: image, size: imageSize, scale: backingScaleFactor)) - - if let updateImageSignal = updateImageSignal, imageView?.layer?.contents == nil { - imageView?.setSignal(account: layout.account, signal: updateImageSignal, cacheImage: { [weak self] signal in - if let strongSelf = self { - return cacheMedia(signal: signal, media: image, size: imageSize, scale: strongSelf.backingScaleFactor) + updateImageSignal = chatWallpaper(account: layout.context.account, representations: image.representations, file: layout.content.file, webpage: layout.webPage, mode: .thumbnail, isPattern: isPattern, autoFetchFullSize: true, scale: backingScaleFactor, isBlurred: false, synchronousLoad: false) + } else { + updateImageSignal = chatWebpageSnippetPhoto(account: layout.context.account, imageReference: ImageMediaReference.webPage(webPage: WebpageReference(layout.webPage), media: image), scale: backingScaleFactor, small: layout.smallThumb) + } + + if imageView == nil { + imageView = TransformImageView() + self.addSubview(imageView!) + } + + let closestRepresentation = image.representationForDisplayAtSize(PixelDimensions(1280, 1280)) + + if let closestRepresentation = closestRepresentation { + statusDisposable.set((layout.context.account.postbox.mediaBox.resourceStatus(closestRepresentation.resource, approximateSynchronousValue: synchronousLoad) |> deliverOnMainQueue).start(next: { [weak self] status in + + guard let `self` = self else {return} + + var initProgress: Bool = false + var state: RadialProgressState = .None + switch status { + case .Fetching: + state = .Fetching(progress: 0.3, force: false) + initProgress = true + case .Local: + state = .Fetching(progress: 1.0, force: false) + case .Remote: + initProgress = true + state = .Remote + } + if initProgress { + + self.playIcon?.removeFromSuperview() + self.playIcon = nil + + if self.downloadIndicator == nil { + self.downloadIndicator = RadialProgressView() + } + self.imageView?.addSubview(self.downloadIndicator!) + self.downloadIndicator!.center() + + } else { + + let playable = ExternalVideoLoader.isPlayable(layout.content) + if layout.isFullImageSize, let icon = ExternalVideoLoader.playIcon(layout.content) { + if self.playIcon == nil { + self.playIcon = ImageView() + self.imageView?.addSubview(self.playIcon!) + } + self.playIcon?.image = icon + self.playIcon?.sizeToFit() + } else { + self.playIcon?.removeFromSuperview() + self.playIcon = nil + } + + if let progressView = self.downloadIndicator { + progressView.state = state + + self.downloadIndicator = nil + if playable { + progressView.removeFromSuperview() } else { - return .complete() + progressView.layer?.animateAlpha(from: 1, to: 0, duration: 0.25, timingFunction: .linear, removeOnCompletion: false, completion: { [weak progressView] completed in + if completed { + progressView?.removeFromSuperview() + } + }) } - }) + } } + + self.downloadIndicator?.fetchControls = FetchControls(fetch: { [weak self] in + switch status { + case .Remote: + self?.fetch() + case .Fetching: + self?.cancelFetching() + case .Local: + self?.open() + } + }) + + self.downloadIndicator?.state = state + self.needsLayout = true + + })) + } else { + statusDisposable.set(nil) + downloadIndicator?.removeFromSuperview() + downloadIndicator = nil + + } + + + + + if let arguments = layout.imageArguments, let imageView = imageView { + imageView.set(arguments: arguments) + imageView.setSignal(signal: cachedMedia(media: image, arguments: arguments, scale: backingScaleFactor), clearInstantly: newLayout) + + if let updateImageSignal = updateImageSignal, !imageView.isFullyLoaded { + imageView.setSignal(updateImageSignal, animate: true, cacheImage: { result in + cacheMedia(result, media: image, arguments: arguments, scale: System.backingScale) + }) } + } + + } else if let palette = layout.content.crossplatformPalette, let wallpaper = layout.content.crossplatformWallpaper, let settings = layout.content.themeSettings { + updateImageSignal = crossplatformPreview(account: layout.context.account, palette: palette, wallpaper: wallpaper, mode: .thumbnail) + + + self.playIcon?.removeFromSuperview() + self.playIcon = nil + + + if imageView == nil { + imageView = TransformImageView() + self.addSubview(imageView!) + } + + if let arguments = layout.imageArguments, let imageView = imageView { + imageView.set(arguments: arguments) + imageView.setSignal(signal: cachedMedia(media: settings, arguments: arguments, scale: backingScaleFactor), clearInstantly: newLayout) - } else { + if let updateImageSignal = updateImageSignal, !imageView.isFullyLoaded { + imageView.setSignal(updateImageSignal, animate: true, cacheImage: { result in + cacheMedia(result, media: settings, arguments: arguments, scale: System.backingScale) + }) + } + } + } else { + + var removeImageView: Bool = true + var removeGradientView: Bool = true + if let wallpaper = layout.wallpaper { + switch wallpaper { + case let .wallpaper(_, _, preview): + switch preview { + case let .color(color): + if imageView == nil { + imageView = TransformImageView() + self.addSubview(imageView!) + } + imageView?.layer?.cornerRadius = .cornerRadius + imageView?.background = color + removeImageView = false + case let .gradient(_, colors, settings): + if gradientView == nil { + gradientView = BackgroundView(frame: NSZeroRect) + self.addSubview(gradientView!) + } + gradientView?.layer?.cornerRadius = .cornerRadius + gradientView?.backgroundMode = .gradient(colors: colors, rotation: settings.rotation) + removeImageView = true + removeGradientView = false + default: + break + } + default: + break + } + } + if removeImageView { imageView?.removeFromSuperview() imageView = nil } + if removeGradientView { + gradientView?.removeFromSuperview() + gradientView = nil + } + downloadIndicator?.removeFromSuperview() + downloadIndicator = nil } @@ -182,11 +463,25 @@ class WPArticleContentView: WPContentView { durationView?.removeFromSuperview() durationView = nil } - + + if let mediaCount = layout.mediaCount { + if countAccessoryView == nil { + countAccessoryView = ChatMessageAccessoryView(frame: NSZeroRect) + imageView?.addSubview(countAccessoryView!) + } + countAccessoryView?.updateText(L10n.chatWebpageMediaCount1(1, mediaCount), maxWidth: 40, status: nil, isStreamable: false) + } else { + countAccessoryView?.removeFromSuperview() + countAccessoryView = nil + } } super.update(with: layout) + if let layout = layout as? WPArticleLayout, layout.isAutoDownloable { + fetch() + } + } override func layout() { @@ -200,16 +495,24 @@ class WPArticleContentView: WPContentView { playIcon?.isHidden = progressIndicator != nil + if groupedContentView.superview != nil { + var origin:NSPoint = NSZeroPoint + if let textLayout = layout.textLayout { + origin.y += textLayout.layoutSize.height + 6.0 + } + groupedContentView.setFrameOrigin(origin) + } + if let imageView = imageView { - - progressIndicator?.center() - if let arguments = layout.imageArguments { imageView.set(arguments: arguments) imageView.setFrameSize(arguments.boundingSize) } + progressIndicator?.center() + downloadIndicator?.center() + var origin:NSPoint = NSMakePoint(layout.contentRect.width - imageView.frame.width - 10, 0) if layout.textLayout?.cutout == nil { @@ -223,19 +526,35 @@ class WPArticleContentView: WPContentView { imageView.setFrameOrigin(origin.x, origin.y) playIcon?.center() + if let durationView = durationView { - durationView.setFrameOrigin(imageView.frame.width - durationView.frame.width - 10, 10) + durationView.setFrameOrigin(imageView.frame.width - durationView.frame.width - 10, imageView.frame.height - durationView.frame.height - 10) + } + if let countAccessoryView = countAccessoryView { + countAccessoryView.setFrameOrigin(imageView.frame.width - countAccessoryView.frame.width - 10, 10) } } + if let gradientView = gradientView { + if let arguments = layout.imageArguments { + gradientView.setFrameSize(arguments.boundingSize) + } + let origin:NSPoint = NSMakePoint(layout.contentRect.width - gradientView.frame.width, 0) + gradientView.setFrameOrigin(origin.x, origin.y) + } } } - override var interactionContentView:NSView { - return self.imageView ?? self + override func interactionContentView(for innerId: AnyHashable, animateIn: Bool ) -> NSView { + return !groupedContentView.subviews.isEmpty ? groupedContentView : self.imageView ?? self } - + override func convertWindowPointToContent(_ point: NSPoint) -> NSPoint { + if !groupedContents.isEmpty { + return groupedContentView.convert(point, from: nil) + } + return super.convertWindowPointToContent(point) + } } diff --git a/Telegram-Mac/WPArticleLayout.swift b/Telegram-Mac/WPArticleLayout.swift index 58fd479ae5..4ff0e39d7b 100644 --- a/Telegram-Mac/WPArticleLayout.swift +++ b/Telegram-Mac/WPArticleLayout.swift @@ -7,9 +7,12 @@ // import Cocoa -import TelegramCoreMac -import PostboxMac +import TelegramCore + +import Postbox import TGUIKit +import SwiftSignalKit + class WPArticleLayout: WPLayout { @@ -20,81 +23,259 @@ class WPArticleLayout: WPLayout { private(set) var duration:(TextNodeLayout, TextNode)? private let durationAttributed:NSAttributedString? - override init(with content: TelegramMediaWebpageLoadedContent, account:Account, chatInteraction:ChatInteraction, parent:Message, fontSize: CGFloat) { + private let fetchDisposable = MetaDisposable() + private let downloadSettings: AutomaticMediaDownloadSettings + + private(set) var groupLayout: GroupedLayout? + private(set) var parameters:[ChatMediaLayoutParameters] = [] + + init(with content: TelegramMediaWebpageLoadedContent, context: AccountContext, chatInteraction:ChatInteraction, parent:Message, fontSize: CGFloat, presentation: WPLayoutPresentation, approximateSynchronousValue: Bool, downloadSettings: AutomaticMediaDownloadSettings, autoplayMedia: AutoplayMediaPreferences, theme: TelegramPresentationTheme) { if let duration = content.duration { self.durationAttributed = .initialize(string: String.durationTransformed(elapsed: duration), color: .white, font: .normal(.text)) } else { durationAttributed = nil } - super.init(with: content, account:account, chatInteraction: chatInteraction, parent:parent, fontSize: fontSize) + var content = content + if content.type == "telegram_theme" { + for attr in content.attributes { + switch attr { + case let .theme(theme): + for file in theme.files { + if file.mimeType == "application/x-tgtheme-macos", !file.previewRepresentations.isEmpty { + content = content.withUpdatedFile(file) + } + } + case .unsupported: + break + } + } + + } + self.downloadSettings = downloadSettings - if let image = content.image { + super.init(with: content, context: context, chatInteraction: chatInteraction, parent:parent, fontSize: fontSize, presentation: presentation, approximateSynchronousValue: approximateSynchronousValue) + + if let mediaCount = mediaCount, mediaCount > 1 { + var instantMedias = Array(instantPageMedias(for: parent.media[0] as! TelegramMediaWebpage).suffix(10)) - if let dimensions = largestImageRepresentation(image.representations)?.dimensions { + if let file = content.file { + let page = InstantPageMedia(index: 0, media: file, webpage: parent.media[0] as! TelegramMediaWebpage, url: nil, caption: nil, credit: nil) + for i in 0 ..< instantMedias.count { + instantMedias[i] = instantMedias[i].withUpdatedIndex(i + 1) + } + instantMedias.insert(page, at: 0) + } else if let image = content.image { + let page = InstantPageMedia(index: 0, media: image, webpage: parent.media[0] as! TelegramMediaWebpage, url: nil, caption: nil, credit: nil) + for i in 0 ..< instantMedias.count { + instantMedias[i] = instantMedias[i].withUpdatedIndex(i + 1) + } + instantMedias.insert(page, at: 0) + } else { + for i in 0 ..< instantMedias.count { + instantMedias[i] = instantMedias[i].withUpdatedIndex(i) + } + } + + var messages:[Message] = [] + let groupingKey = arc4random64() + for i in 0 ..< instantMedias.count { + let media = instantMedias[i].media + let message = parent.withUpdatedMedia([media]).withUpdatedStableId(arc4random()).withUpdatedId(MessageId(peerId: chatInteraction.peerId, namespace: Namespaces.Message.Local, id: MessageId.Id(i))).withUpdatedGroupingKey(groupingKey) + messages.append(message) + + weak var weakParameters:ChatMediaGalleryParameters? + + let parameters = ChatMediaGalleryParameters(showMedia: { [weak self] _ in + guard let `self` = self else {return} +// + showInstantViewGallery(context: context, medias: instantMedias, firstIndex: i, firstStableId: ChatHistoryEntryId.message(parent), parent: parent, self.table, weakParameters) + + }, showMessage: { [weak chatInteraction] _ in + chatInteraction?.focusMessageId(nil, parent.id, .CenterEmpty) + }, isWebpage: chatInteraction.isLogInteraction, presentation: .make(for: message, account: context.account, renderType: presentation.renderType, theme: theme), media: media, automaticDownload: downloadSettings.isDownloable(message), autoplayMedia: autoplayMedia) + + weakParameters = parameters + + self.parameters.append(parameters) + } + groupLayout = GroupedLayout(messages) + } + + if let image = content.image, groupLayout == nil { + if let dimensions = largestImageRepresentation(image.representations)?.dimensions.size { imageSize = dimensions } - } + if let file = content.file, groupLayout == nil { + if let dimensions = file.dimensions?.size { + imageSize = dimensions + } else if isTheme { + imageSize = NSMakeSize(200, 200) + } + } else if isTheme { + imageSize = NSMakeSize(260, 260) + } + if let wallpaper = wallpaper { + switch wallpaper { + case let .wallpaper(_, _, preview): + switch preview { + case .color: + imageSize = NSMakeSize(150, 150) + case .gradient: + imageSize = NSMakeSize(200, 200) + default: + break + } + default: + break + } + } + + if ExternalVideoLoader.isPlayable(content) { + _ = sharedVideoLoader.fetch(for: content).start() + } + } + + var isAutoDownloable: Bool { + return downloadSettings.isDownloable(parent) + } + + deinit { + fetchDisposable.dispose() } private let mediaTypes:[String] = ["photo","video"] + private let fullSizeSites:[String] = ["instagram","twitter"] + + var isFullImageSize: Bool { + if content.type == "telegram_background" || content.type == "telegram_theme" { + return true + } + let website = content.websiteName?.lowercased() + if let type = content.type, mediaTypes.contains(type) || (fullSizeSites.contains(website ?? "") || content.instantPage != nil) || content.text == nil { + if let imageSize = imageSize { + if imageSize.width < 200 { + return false + } + } + return true + } + return content.text == nil || content.text!.trimmed.isEmpty + } override func measure(width: CGFloat) { - super.measure(width: width) - - var contentSize:NSSize = NSMakeSize(width, 0) - - if let imageSize = imageSize, let type = content.type, mediaTypes.contains(type) { - contrainedImageSize = imageSize.aspectFitted(NSMakeSize(min(width - insets.left, 320), 320)) - textLayout?.cutout = nil - smallThumb = false - contentSize.height += contrainedImageSize.height - if textLayout != nil { - contentSize.height += 6 + if oldWidth != width { + super.measure(width: width) + + let maxw = min(320, width - 50) + + var contentSize:NSSize = NSMakeSize(width - insets.left, 0) + + if let groupLayout = groupLayout { + groupLayout.measure(NSMakeSize(max(contentSize.width, maxw), maxw)) + + contentSize.height += groupLayout.dimensions.height + 6 + contentSize.width = max(groupLayout.dimensions.width, contentSize.width) } - } else { - if imageSize != nil { - contrainedImageSize = NSMakeSize(54, 54) - textLayout?.cutout = TextViewCutout(position: .TopRight, size: NSMakeSize(contrainedImageSize.width + 16, contrainedImageSize.height + 10)) + + var emptyColor: TransformImageEmptyColor? = nil// = NSColor(rgb: 0xd6e2ee, alpha: 0.5) + var isColor: Bool = false + if let wallpaper = wallpaper { + switch wallpaper { + case let .wallpaper(_, _, preview): + switch preview { + case let .slug(_, settings): + if !settings.colors.isEmpty { + var patternIntensity: CGFloat = 0.5 + + let color = settings.colors.first ?? NSColor(rgb: 0xd6e2ee, alpha: 0.5).argb + if let intensity = settings.intensity { + patternIntensity = CGFloat(intensity) / 100.0 + } + if settings.colors.count > 1 { + emptyColor = .gradient(colors: settings.colors.map { NSColor(argb: $0) }, intensity: patternIntensity, rotation: settings.rotation) + } else { + emptyColor = .color(NSColor(argb: color)) + } + } + case .color: + isColor = true + case .gradient: + isColor = true + } + default: + break + } } - } - - if let durationAttributed = durationAttributed { - duration = TextNode.layoutText(durationAttributed, nil, 1, .end, NSMakeSize(width, .greatestFiniteMagnitude), nil, false, .center) - } - - textLayout?.measure(width: width - insets.left) - - if let textLayout = textLayout { - contentSize.height += textLayout.layoutSize.height + if let imageSize = imageSize, isFullImageSize { + + if isTheme { + contrainedImageSize = imageSize.fitted(NSMakeSize(maxw, maxw)) + } else { + contrainedImageSize = imageSize.fitted(NSMakeSize(min(width - insets.left, maxw), maxw)) + } + // if presentation.renderType == .bubble { + if isColor { + contrainedImageSize = imageSize.fitted(NSMakeSize(maxw, maxw)) + } else if !isTheme { + contrainedImageSize.width = max(contrainedImageSize.width, maxw) + } + // } + textLayout?.cutout = nil + smallThumb = false + contentSize.height += contrainedImageSize.height + contentSize.width = contrainedImageSize.width + if textLayout != nil { + contentSize.height += 6 + } + } else { + if let _ = imageSize { + contrainedImageSize = NSMakeSize(54, 54) + textLayout?.cutout = TextViewCutout(topRight: NSMakeSize(contrainedImageSize.width + 16, contrainedImageSize.height + 10)) + } + } + + if let durationAttributed = durationAttributed { + duration = TextNode.layoutText(durationAttributed, nil, 1, .end, NSMakeSize(contentSize.width, .greatestFiniteMagnitude), nil, false, .center) + } + + + textLayout?.measure(width: contentSize.width) - if textLayout.cutout != nil { + + + if let textLayout = textLayout { + + contentSize.height += textLayout.layoutSize.height - contentSize.height = max(content.image != nil ? contrainedImageSize.height : 0,contentSize.height) - contentSize.width = min(max(textLayout.layoutSize.width, (siteName?.0.size.width ?? 0) + contrainedImageSize.width), width - insets.left) - } else if imageSize == nil { - contentSize.width = max(textLayout.layoutSize.width, (siteName?.0.size.width ?? 0)) + if textLayout.cutout != nil { + contentSize.height = max(content.image != nil ? contrainedImageSize.height : 0,contentSize.height) + contentSize.width = min(max(textLayout.layoutSize.width, (siteName?.0.size.width ?? 0) + contrainedImageSize.width), width - insets.left) + } else if imageSize == nil { + contentSize.width = max(max(textLayout.layoutSize.width, groupLayout?.dimensions.width ?? 0), (siteName?.0.size.width ?? 0)) + } } - } - - if imageSize != nil { - let imageArguments = TransformImageArguments(corners: ImageCorners(radius: 4.0), imageSize: contrainedImageSize, boundingSize: contrainedImageSize, intrinsicInsets: NSEdgeInsets()) - if imageArguments != self.imageArguments { - self.imageArguments = imageArguments + if let imageSize = imageSize { + + let imageArguments = TransformImageArguments(corners: ImageCorners(radius: 4.0), imageSize: isTheme ? contrainedImageSize : imageSize.aspectFilled(NSMakeSize(maxw, maxw)), boundingSize: contrainedImageSize, intrinsicInsets: NSEdgeInsets(), resizeMode: .blurBackground, emptyColor: emptyColor) + + if imageArguments != self.imageArguments { + self.imageArguments = imageArguments + } + } else { + self.imageArguments = nil } - } else { - self.imageArguments = nil + + + + layout(with :contentSize) } - - - - layout(with :contentSize) } } diff --git a/Telegram-Mac/WPContentView.swift b/Telegram-Mac/WPContentView.swift index 1170cc85f6..d0385fefdb 100644 --- a/Telegram-Mac/WPContentView.swift +++ b/Telegram-Mac/WPContentView.swift @@ -8,12 +8,17 @@ import Cocoa import TGUIKit -import TelegramCoreMac +import TelegramCore -class WPContentView: View, MultipleSelectable { + +class WPContentView: View, MultipleSelectable, ModalPreviewRowViewProtocol { + func fileAtPoint(_ point: NSPoint) -> (QuickPreviewMedia, NSView?)? { + return nil + } + var header: String? { return nil } @@ -33,11 +38,23 @@ class WPContentView: View, MultipleSelectable { containerView.backgroundColor = backgroundColor for subview in containerView.subviews { - subview.background = backgroundColor + if !(subview is TransformImageView) { + subview.background = backgroundColor + } } - instantPageButton?.set(image: theme.icons.chatInstantView, for: .Normal) - instantPageButton?.layer?.borderColor = theme.colors.blueIcon.cgColor - instantPageButton?.set(color: theme.colors.blueIcon, for: .Normal) + if let content = content { + instantPageButton?.layer?.borderColor = content.presentation.activity.cgColor + instantPageButton?.set(color: content.presentation.activity, for: .Normal) + + if content.hasInstantPage { + instantPageButton?.set(image: content.presentation.ivIcon, for: .Normal) + instantPageButton?.set(image: content.presentation.ivIcon, for: .Highlight) + } else { + instantPageButton?.removeImage(for: .Normal) + instantPageButton?.removeImage(for: .Highlight) + } + } + setNeedsDisplay() } } @@ -46,19 +63,23 @@ class WPContentView: View, MultipleSelectable { return [textView] } + func previewMediaIfPossible() -> Bool { + return false + } + override func draw(_ layer: CALayer, in ctx: CGContext) { super.draw(layer, in: ctx) - ctx.setFillColor(theme.colors.blueFill.cgColor) + guard let content = content else {return} + + ctx.setFillColor(content.presentation.activity.cgColor) let radius:CGFloat = 1.0 ctx.fill(NSMakeRect(0, radius, 2, layer.bounds.height - radius * 2)) ctx.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: radius + radius, height: radius + radius))) ctx.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: layer.bounds.height - radius * 2), size: CGSize(width: radius + radius, height: radius + radius))) - if let content = content { - if let siteName = content.siteName { - siteName.1.draw(NSMakeRect(content.insets.left, 0, siteName.0.size.width, siteName.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor) - } + if let siteName = content.siteName { + siteName.1.draw(NSMakeRect(content.insets.left, 0, siteName.0.size.width, siteName.0.size.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: backgroundColor) } } @@ -69,12 +90,17 @@ class WPContentView: View, MultipleSelectable { if !textView.isEqual(to: content.textLayout) { textView.update(content.textLayout) } - instantPageButton?.sizeToFit(NSZeroSize, NSMakeSize(content.contentRect.width, 30), thatFit: false) + textView.isHidden = content.textLayout == nil + _ = instantPageButton?.sizeToFit(NSZeroSize, NSMakeSize(content.contentRect.width, 30), thatFit: true) instantPageButton?.setFrameOrigin(0, content.contentRect.height - 30) } needsDisplay = true } + func convertWindowPointToContent(_ point: NSPoint) -> NSPoint { + return convert(point, from: nil) + } + required public override init() { super.init() super.addSubview(containerView) @@ -96,11 +122,16 @@ class WPContentView: View, MultipleSelectable { deinit { containerView.removeAllSubviews() } + + func updateMouse() { + + } func update(with layout:WPLayout) -> Void { self.content = layout - if layout.hasInstantPage { + + if layout.hasInstantPage || layout.isProxyConfig { if instantPageButton == nil { instantPageButton = TitleButton() @@ -110,18 +141,23 @@ class WPContentView: View, MultipleSelectable { addSubview(instantPageButton!) } - instantPageButton?.layer?.borderColor = theme.colors.blueIcon.cgColor - instantPageButton?.set(color: theme.colors.blueIcon, for: .Normal) - instantPageButton?.set(image: theme.icons.chatInstantView, for: .Normal) + instantPageButton?.layer?.borderColor = theme.colors.accentIcon.cgColor + + instantPageButton?.set(color: theme.colors.accentIcon, for: .Normal) + instantPageButton?.set(font: .medium(.title), for: .Normal) - instantPageButton?.set(background: theme.colors.background, for: .Normal) - instantPageButton?.set(text: tr(.chatInstantView), for: .Normal) - instantPageButton?.sizeToFit(NSZeroSize, NSMakeSize(layout.contentRect.width, 30), thatFit: false) + instantPageButton?.set(background: .clear, for: .Normal) + instantPageButton?.set(text: layout.isProxyConfig ? L10n.chatApplyProxy : L10n.chatInstantView, for: .Normal) + _ = instantPageButton?.sizeToFit(NSZeroSize, NSMakeSize(layout.contentRect.width, 30), thatFit: false) instantPageButton?.removeAllHandlers() instantPageButton?.set(handler : { [weak layout] _ in if let content = layout { - showInstantPage(InstantPageViewController(content.account, webPage: content.parent.media[0] as! TelegramMediaWebpage, message: content.parent.text)) + if content.hasInstantPage { + showInstantPage(InstantPageViewController(content.context, webPage: content.parent.media[0] as! TelegramMediaWebpage, message: content.parent.text)) + } else if let proxyConfig = content.proxyConfig { + applyExternalProxy(proxyConfig, accountManager: content.context.sharedContext.accountManager) + } } }, for: .Click) @@ -133,7 +169,7 @@ class WPContentView: View, MultipleSelectable { self.needsLayout = true } - var interactionContentView: NSView { + func interactionContentView(for innerId: AnyHashable, animateIn: Bool ) -> NSView { return self.containerView } diff --git a/Telegram-Mac/WPLayout.swift b/Telegram-Mac/WPLayout.swift index a20fce6521..6da4499a3c 100644 --- a/Telegram-Mac/WPLayout.swift +++ b/Telegram-Mac/WPLayout.swift @@ -8,13 +8,24 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac +import Postbox +import TelegramCore + + + +struct WPLayoutPresentation { + let text: NSColor + let activity: NSColor + let link: NSColor + let selectText: NSColor + let ivIcon: CGImage + let renderType: ChatItemRenderType +} class WPLayout: Equatable { let content:TelegramMediaWebpageLoadedContent let parent:Message - let account:Account + let context: AccountContext let fontSize:CGFloat weak var table:TableView? @@ -30,27 +41,84 @@ class WPLayout: Equatable { var insets: NSEdgeInsets = NSEdgeInsets(left:8.0, top:0.0) - init(with content:TelegramMediaWebpageLoadedContent, account:Account, chatInteraction:ChatInteraction, parent:Message, fontSize: CGFloat) { + + var mediaCount: Int? { + if let instantPage = content.instantPage, isGalleryAssemble { + if let block = instantPage.blocks.filter({ value in + if case .slideshow = value { + return true + } else if case .collage = value { + return true + } else { + return false + } + }).last { + switch block { + case let .slideshow(items, _), let .collage(items , _): + if items.count == 1 { + return nil + } + return items.count + default: + break + } + + } + } + return nil + } + + var webPage: TelegramMediaWebpage { + if let game = parent.media.first as? TelegramMediaGame { + return TelegramMediaWebpage(webpageId: MediaId(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent.init(url: "", displayUrl: "", hash: 0, type: "game", websiteName: game.title, title: nil, text: game.description, embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, image: game.image, file: game.file, attributes: [], instantPage: nil))) + } + return parent.media.first as! TelegramMediaWebpage + } + + let presentation: WPLayoutPresentation + + private var _approximateSynchronousValue: Bool = false + var approximateSynchronousValue: Bool { + get { + let result = _approximateSynchronousValue + _approximateSynchronousValue = false + return result + } + } + + init(with content:TelegramMediaWebpageLoadedContent, context: AccountContext, chatInteraction:ChatInteraction, parent:Message, fontSize: CGFloat, presentation: WPLayoutPresentation, approximateSynchronousValue: Bool) { self.content = content - self.account = account + self.context = context + self.presentation = presentation self.parent = parent self.fontSize = fontSize + self._approximateSynchronousValue = approximateSynchronousValue if let websiteName = content.websiteName { - _siteNameAttr = .initialize(string: websiteName, color: theme.colors.link, font: .medium(.text)) + let siteName: String + switch content.type { + case "telegram_background": + siteName = L10n.chatWPBackgroundTitle + case "telegram_voicechat": + siteName = L10n.chatWPVoiceChatTitle + default: + siteName = websiteName + } + _siteNameAttr = .initialize(string: siteName, color: presentation.activity, font: .medium(.text)) _nameNode = TextNode() } let attributedText:NSMutableAttributedString = NSMutableAttributedString() - if let title = content.title ?? content.author { - _ = attributedText.append(string: title, color: theme.colors.text, font: NSFont.medium(.custom(fontSize))) - if content.text != nil { + let text = content.type != "telegram_background" ? content.text?.trimmed : nil + if let title = content.title ?? content.author, content.type != "telegram_background" { + _ = attributedText.append(string: title, color: presentation.text, font: .medium(fontSize)) + if text != nil { _ = attributedText.append(string: "\n") } } - if let text = content.text { - _ = attributedText.append(string: text, color: theme.colors.text, font: NSFont.normal(.custom(fontSize))) + if let text = text { + _ = attributedText.append(string: text, color: presentation.text, font: .normal(fontSize)) } if attributedText.length > 0 { var p: ParsingType = [.Links] @@ -59,9 +127,38 @@ class WPLayout: Equatable { p = [.Links, .Mentions, .Hashtags] } - attributedText.detectLinks(type: p) - textLayout = TextViewLayout(attributedText, maximumNumberOfLines:10, truncationType: .end, cutout: nil) - textLayout?.interactions = TextViewInteractions(processURL: { link in + attributedText.detectLinks(type: p, color: presentation.link, dotInMention: wname == "instagram") + textLayout = TextViewLayout(attributedText, maximumNumberOfLines:10, truncationType: .end, cutout: nil, selectText: presentation.selectText, strokeLinks: presentation.renderType == .bubble, alwaysStaticItems: true) + + let interactions = globalLinkExecutor + interactions.resolveLink = { link in + if let link = link as? inAppLink { + if case .external(let url, _) = link { + switch wname { + case "instagram": + if url.hasPrefix("@") { + return "https://instagram.com/\(url.nsstring.substring(from: 1))" + } + if url.hasPrefix("#") { + return "https://instagram.com/explore/tags/\(url.nsstring.substring(from: 1))" + } + case "twitter": + if url.hasPrefix("@") { + return "https://twitter.com/\(url.nsstring.substring(from: 1))" + } + if url.hasPrefix("#") { + return "https://twitter.com/hashtag/\(url.nsstring.substring(from: 1))" + } + default: + break + } + } + return link.link + } + return nil + + } + interactions.processURL = { link in if let link = link as? inAppLink { var link = link if case .external(let url, _) = link { @@ -81,25 +178,78 @@ class WPLayout: Equatable { link = .external(link: "https://twitter.com/hashtag/\(url.nsstring.substring(from: 1))", false) } default: + link = inApp(for: url.nsstring, context: context, peerId: nil, openInfo: chatInteraction.openInfo, hashtag: nil, command: nil, applyProxy: nil, confirm: false) break } } execute(inapp: link) } - }) + } + + textLayout?.interactions = interactions } attributedText.fixUndefinedEmojies() } + var isGalleryAssemble: Bool { + // && content.instantPage != nil + if (content.type == "video" && content.type == "video/mp4") || content.type == "photo" || ((content.websiteName?.lowercased() == "instagram" || content.websiteName?.lowercased() == "twitter" || content.websiteName?.lowercased() == "telegram")) || content.text == nil { + return !content.url.isEmpty && content.type != "telegram_background" && content.type != "telegram_theme" + } + return content.type == "telegram_album" && content.type != "telegram_background" && content.type != "telegram_theme" + } + + var wallpaper: inAppLink? { + if content.type == "telegram_background" { + return inApp(for: content.url as NSString, context: context) + } + return nil + } + var isPatternWallpaper: Bool { + return content.file?.mimeType == "application/x-tgwallpattern" + } + + var wallpaperReference: WallpaperReference? { + if let wallpaper = wallpaper { + switch wallpaper { + case let .wallpaper(link, context, preview): + inner: switch preview { + case let .slug(slug, _): + return .slug(slug) + default: + break inner + } + default: + break + } + } + return nil + } + + var themeLink: inAppLink? { + if content.type == "telegram_theme" { + return inApp(for: content.url as NSString, context: context) + } + return nil + } + + var isTheme: Bool { + return content.type == "telegram_theme" && (content.file != nil || content.isCrossplatformTheme) + } + func viewClass() -> AnyClass { return WPArticleContentView.self } + private(set) var oldWidth:CGFloat = 0 func measure(width: CGFloat) { - siteName = TextNode.layoutText(maybeNode: _nameNode, _siteNameAttr, nil, 1, .end, NSMakeSize(width, 20), nil, false, .left) + if oldWidth != width { + self.oldWidth = width + siteName = TextNode.layoutText(maybeNode: _nameNode, _siteNameAttr, nil, 1, .end, NSMakeSize(width - 50, 20), nil, false, .left) + } if let siteName = siteName { insets.top = siteName.0.size.height + 2.0 @@ -108,13 +258,36 @@ class WPLayout: Equatable { } func layout(with size:NSSize) -> Void { - let size = NSMakeSize(max(size.width, hasInstantPage ? 160 : size.width) , size.height + (hasInstantPage ? 30 + 6 : 0)) + let size = NSMakeSize(max(size.width, hasInstantPage ? 160 : size.width) , size.height + (hasInstantPage ? 30 + 6 : 0) + (isProxyConfig ? 30 + 6 : 0)) self.contentRect = NSMakeRect(insets.left, insets.top, size.width, size.height) self.size = NSMakeSize(size.width + insets.left + insets.right, size.height + insets.top + insets.bottom) } var hasInstantPage: Bool { - return content.instantPage != nil + if let instantPage = content.instantPage { + if content.websiteName?.lowercased() == "instagram" || content.websiteName?.lowercased() == "twitter" || content.type == "telegram_album" { + return false + } + if instantPage.blocks.count == 3 { + switch instantPage.blocks[2] { + case let .collage(_, caption), let .slideshow(_, caption): + return !attributedStringForRichText(caption.text, styleStack: InstantPageTextStyleStack()).string.isEmpty + default: + break + } + } + + return true + } + return false + } + + var isProxyConfig: Bool { + return content.type == "proxy" + } + + var proxyConfig: ProxyServerSettings? { + return proxySettings(from: content.url).0 } } diff --git a/Telegram-Mac/WPMediaContentView.swift b/Telegram-Mac/WPMediaContentView.swift index c533776719..88ccfae0b1 100644 --- a/Telegram-Mac/WPMediaContentView.swift +++ b/Telegram-Mac/WPMediaContentView.swift @@ -8,9 +8,51 @@ import Cocoa import TGUIKit +import TelegramCore + + class WPMediaContentView: WPContentView { - var contentNode:ChatMediaContentView? + private(set) var contentNode:ChatMediaContentView? + + + override func fileAtPoint(_ point: NSPoint) -> (QuickPreviewMedia, NSView?)? { + if let contentNode = contentNode { + if contentNode is ChatStickerContentView { + if let file = contentNode.media as? TelegramMediaFile { + let reference = contentNode.parent != nil ? FileMediaReference.message(message: MessageReference(contentNode.parent!), media: file) : FileMediaReference.standalone(media: file) + return (.file(reference, StickerPreviewModalView.self), contentNode) + } + } else if contentNode is ChatGIFContentView { + if let file = contentNode.media as? TelegramMediaFile { + let reference = contentNode.parent != nil ? FileMediaReference.message(message: MessageReference(contentNode.parent!), media: file) : FileMediaReference.standalone(media: file) + return (.file(reference, GifPreviewModalView.self), contentNode) + } + } else if contentNode is ChatInteractiveContentView { + if let image = contentNode.media as? TelegramMediaImage { + let reference = contentNode.parent != nil ? ImageMediaReference.message(message: MessageReference(contentNode.parent!), media: image) : ImageMediaReference.standalone(media: image) + return (.image(reference, ImagePreviewModalView.self), contentNode) + } + } else if contentNode is ChatFileContentView { + if let file = contentNode.media as? TelegramMediaFile, file.isGraphicFile, let mediaId = file.id, let dimension = file.dimensions { + var representations: [TelegramMediaImageRepresentation] = [] + representations.append(contentsOf: file.previewRepresentations) + representations.append(TelegramMediaImageRepresentation(dimensions: dimension, resource: file.resource, progressiveSizes: [], immediateThumbnailData: nil)) + let image = TelegramMediaImage(imageId: mediaId, representations: representations, immediateThumbnailData: file.immediateThumbnailData, reference: nil, partialReference: file.partialReference, flags: []) + let reference = contentNode.parent != nil ? ImageMediaReference.message(message: MessageReference(contentNode.parent!), media: image) : ImageMediaReference.standalone(media: image) + return (.image(reference, ImagePreviewModalView.self), contentNode) + } + } + } + + return nil + } + + override func previewMediaIfPossible() -> Bool { + guard let window = self.kitWindow, let content = content as? WPArticleLayout, content.isFullImageSize, let table = content.table, let contentNode = contentNode, contentNode.mouseInside() else {return false} + _ = startModalPreviewHandle(table, window: window, context: content.context) + return true + } override func draw(_ dirtyRect: NSRect) { @@ -36,10 +78,14 @@ class WPMediaContentView: WPContentView { self.addSubview(self.contentNode!) } - self.contentNode?.update(with: layout.media, size: layout.mediaSize, account: layout.account, parent:layout.parent, table:layout.table, parameters: layout.parameters) + self.contentNode?.update(with: layout.media, size: layout.mediaSize, context: layout.context, parent:layout.parent, table:layout.table, parameters: layout.parameters, approximateSynchronousValue: layout.approximateSynchronousValue) } } + override func updateMouse() { + contentNode?.updateMouse() + } + override func layout() { super.layout() if let content = content as? WPMediaLayout { @@ -47,8 +93,8 @@ class WPMediaContentView: WPContentView { } } - override var interactionContentView: NSView { - return contentNode?.interactionContentView ?? self + override func interactionContentView(for innerId: AnyHashable, animateIn: Bool ) -> NSView { + return contentNode?.interactionContentView(for: innerId, animateIn: animateIn) ?? self } } diff --git a/Telegram-Mac/WPMediaLayout.swift b/Telegram-Mac/WPMediaLayout.swift index 73407b0194..8530385e17 100644 --- a/Telegram-Mac/WPMediaLayout.swift +++ b/Telegram-Mac/WPMediaLayout.swift @@ -8,9 +8,10 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac -import PostboxMac -import TelegramCoreMac +import SwiftSignalKit +import Postbox +import TelegramCore + @@ -18,18 +19,21 @@ class WPMediaLayout: WPLayout { var mediaSize:NSSize = NSZeroSize private(set) var media:TelegramMediaFile - var parameters:ChatMediaLayoutParameters? - override init(with content: TelegramMediaWebpageLoadedContent, account: Account, chatInteraction:ChatInteraction, parent:Message, fontSize: CGFloat) { - self.media = content.file! - super.init(with: content, account: account, chatInteraction: chatInteraction, parent:parent, fontSize: fontSize) + let parameters:ChatMediaLayoutParameters? + init(with content: TelegramMediaWebpageLoadedContent, context: AccountContext, chatInteraction:ChatInteraction, parent:Message, fontSize: CGFloat, presentation: WPLayoutPresentation, approximateSynchronousValue: Bool, downloadSettings: AutomaticMediaDownloadSettings, autoplayMedia: AutoplayMediaPreferences, theme: TelegramPresentationTheme) { + self.media = content.file! + if let representations = content.image?.representations { + self.media = self.media.withUpdatedPreviewRepresentations(representations) + } + self.parameters = ChatMediaLayoutParameters.layout(for: content.file!, isWebpage: true, chatInteraction: chatInteraction, presentation: .make(for: parent, account: context.account, renderType: presentation.renderType, theme: theme), automaticDownload: downloadSettings.isDownloable(parent), isIncoming: parent.isIncoming(context.account, presentation.renderType == .bubble), autoplayMedia: autoplayMedia) + super.init(with: content, context: context, chatInteraction: chatInteraction, parent:parent, fontSize: fontSize, presentation: presentation, approximateSynchronousValue: approximateSynchronousValue) - self.parameters = ChatMediaLayoutParameters.layout(for: self.media, isWebpage: true, chatInteraction: chatInteraction) } override func measure(width: CGFloat) { super.measure(width: width) - var contentSize = ChatLayoutUtils.contentSize(for: media, with: width - insets.left) + var contentSize = ChatLayoutUtils.contentSize(for: media, with: width - insets.left, hasText: textLayout != nil && theme.bubbled) self.mediaSize = contentSize textLayout?.measure(width: contentSize.width) @@ -42,10 +46,10 @@ class WPMediaLayout: WPLayout { parameters.name = TextNode.layoutText(maybeNode: parameters.nameNode, NSAttributedString.initialize(string: parameters.fileName , color: theme.colors.text, font: .medium(.text)), nil, 1, .middle, NSMakeSize(width - (parameters.hasThumb ? 80 : 50), 20), nil,false, .left) } + parameters?.makeLabelsForWidth(contentSize.width - 50) + if let parameters = parameters as? ChatMediaMusicLayoutParameters { - parameters.nameLayout.measure(width: contentSize.width - 50) - parameters.durationLayout.measure(width: contentSize.width - 50) - parameters.sizeLayout.measure(width: contentSize.width - 50) + contentSize.width = 50 + max(parameters.nameLayout.layoutSize.width, parameters.durationLayout.layoutSize.width) } layout(with: contentSize) @@ -53,7 +57,7 @@ class WPMediaLayout: WPLayout { } public func contentNode() -> ChatMediaContentView.Type { - return ChatLayoutUtils.contentNode(for: media) + return ChatLayoutUtils.contentNode(for: media) } override func viewClass() -> AnyClass { diff --git a/Telegram-Mac/WalletConfiguration.swift b/Telegram-Mac/WalletConfiguration.swift new file mode 100644 index 0000000000..c4cd03f6f8 --- /dev/null +++ b/Telegram-Mac/WalletConfiguration.swift @@ -0,0 +1,53 @@ +// +// WalletConfiguration.swift +// Telegram +// +// Created by Mikhail Filimonov on 29/09/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import Postbox +import SwiftSignalKit + + +public struct WalletConfiguration { + static var defaultValue: WalletConfiguration { + return WalletConfiguration(config: nil, blockchainName: nil, disableProxy: false) + } + + public let config: String? + public let blockchainName: String? + public let disableProxy: Bool + + fileprivate init(config: String?, blockchainName: String?, disableProxy: Bool) { + self.config = config + self.blockchainName = blockchainName + self.disableProxy = disableProxy + } + + public static func with(appConfiguration: AppConfiguration) -> WalletConfiguration { + if let data = appConfiguration.data, let config = data["wallet_config"] as? String, let blockchainName = data["wallet_blockchain_name"] as? String { + var disableProxy = false + if let value = data["wallet_disable_proxy"] as? String { + disableProxy = value != "0" + } else if let value = data["wallet_disable_proxy"] as? Int { + disableProxy = value != 0 + } + return WalletConfiguration(config: config, blockchainName: blockchainName, disableProxy: disableProxy) + } else { + return .defaultValue + } + } +} + + +func walletConfiguration(postbox: Postbox) -> Signal { + return postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) |> map { view in + let appConfiguration = view.values[PreferencesKeys.appConfiguration] as? AppConfiguration ?? .defaultValue + let configuration = WalletConfiguration.with(appConfiguration: appConfiguration) + return configuration + } |> deliverOnMainQueue +} diff --git a/Telegram-Mac/WalletIntroRowItem.swift b/Telegram-Mac/WalletIntroRowItem.swift new file mode 100644 index 0000000000..4d5bd55e10 --- /dev/null +++ b/Telegram-Mac/WalletIntroRowItem.swift @@ -0,0 +1,150 @@ +// +// WalletSplashRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 19/09/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCoreMac + +extension WalletSplashMode { + var splashAnimation: String { + switch self { + case .intro: + return "❤️" + default: + return "👍" + } + } + var title: String { + switch self { + case .intro: + return "Gram Wallet" + case .created: + return "Congratulations" + case .success: + return "Ready to go!" + case .restoreFailed: + return "Too Bad" + } + } + var desc: String { + switch self { + case .intro: + return "Gram wallet allows you to make fast and secure blockchain-based payments without intermediaries." + case .created: + return "Your Gram wallet has just been created. Only you control it.\n\nTo be able to always have access to it, please write down secret words and\nset up a secure passcode." + case .success: + return "You’re all set. Now you have a wallet that only you control - directly, without middlemen or bankers. " + case .restoreFailed: + return "Without the secret words, you can't'nrestore access to the wallet." + } + } +} + +class WalletSplashRowItem: GeneralRowItem { + fileprivate let mode:WalletSplashMode + fileprivate let descLayout: TextViewLayout + fileprivate let titleLayout: TextViewLayout + fileprivate let animation: TelegramMediaFile? + fileprivate let context: AccountContext + private var h: CGFloat = 0 + init(_ initialSize: NSSize, stableId: AnyHashable, context: AccountContext, mode: WalletSplashMode, animations: [String: TelegramMediaFile], viewType: GeneralViewType) { + self.mode = mode + self.context = context + self.animation = animations[mode.splashAnimation] + self.descLayout = TextViewLayout(.initialize(string: mode.desc, color: theme.colors.text, font: .normal(.text)), alignment: .center) + self.titleLayout = TextViewLayout(.initialize(string: mode.title, color: theme.colors.text, font: .medium(.huge)), alignment: .center) + + super.init(initialSize, stableId: stableId, viewType: viewType) + _ = makeSize(initialSize.width, oldWidth: 0) + } + + override var height: CGFloat { + return self.h + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat = 0) -> Bool { + _ = super.makeSize(width, oldWidth: oldWidth) + + self.descLayout.measure(width: self.blockWidth - self.viewType.innerInset.left - self.viewType.innerInset.right) + self.titleLayout.measure(width: self.blockWidth - self.viewType.innerInset.left - self.viewType.innerInset.right) + + self.h = self.viewType.innerInset.top + self.viewType.innerInset.bottom + self.descLayout.layoutSize.height + self.viewType.innerInset.top + self.titleLayout.layoutSize.height + self.viewType.innerInset.top + 150 + + return true + } + + override func viewClass() -> AnyClass { + return WalletIntroRowView.self + } +} + + +private final class WalletIntroRowView : TableRowView { + private let containerView = GeneralRowContainerView(frame: NSZeroRect) + private let titleView = TextView() + private let descView = TextView() + private let animationView: ChatMediaAnimatedStickerView = ChatMediaAnimatedStickerView(frame: NSZeroRect) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + containerView.addSubview(animationView) + containerView.addSubview(titleView) + containerView.addSubview(descView) + titleView.userInteractionEnabled = false + titleView.isSelectable = false + descView.userInteractionEnabled = false + descView.isSelectable = false + addSubview(containerView) + } + + override var backdorColor: NSColor { + return theme.colors.background + } + + override func updateColors() { + guard let item = item as? WalletSplashRowItem else { + return + } + self.backgroundColor = item.viewType.rowBackground + self.titleView.background = backdorColor + self.descView.background = backdorColor + self.containerView.backgroundColor = backdorColor + } + + override func layout() { + super.layout() + guard let item = item as? WalletSplashRowItem else { + return + } + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) + self.containerView.setCorners(item.viewType.corners) + + animationView.centerX(y: item.viewType.innerInset.top) + titleView.centerX(y: animationView.frame.maxY + item.viewType.innerInset.top) + descView.centerX(y: titleView.frame.maxY + item.viewType.innerInset.top) + + } + + override func set(item: TableRowItem, animated: Bool = false) { + super.set(item: item, animated: animated) + + guard let item = item as? WalletSplashRowItem else { + return + } + titleView.update(item.titleLayout) + descView.update(item.descLayout) + if let animation = item.animation { + animationView.update(with: animation, size: NSMakeSize(150, 150), context: item.context, parent: nil, table: item.table, parameters: nil, animated: animated, positionFlags: nil, approximateSynchronousValue: !animated) + } + + needsLayout = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/WallpaperAdditionColorView.swift b/Telegram-Mac/WallpaperAdditionColorView.swift new file mode 100644 index 0000000000..856d8e7cfc --- /dev/null +++ b/Telegram-Mac/WallpaperAdditionColorView.swift @@ -0,0 +1,123 @@ +// +// WallpaperAdditionColorView.swift +// Telegram +// +// Created by Mikhail Filimonov on 21.07.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit + +final class WallpaperAdditionColorView : View, TGModernGrowingDelegate { + func textViewHeightChanged(_ height: CGFloat, animated: Bool) { + + } + + func textViewEnterPressed(_ event: NSEvent) -> Bool { + return true + } + + func textViewTextDidChange(_ string: String) { + var filtered = String(string.unicodeScalars.filter {CharacterSet(charactersIn: "#0123456789abcdefABCDEF").contains($0)}).uppercased() + if string != filtered { + if filtered.isEmpty { + filtered = "#" + } else if filtered.first != "#" { + filtered = "#" + filtered + } + textView.setString(filtered) + } + if filtered.length == maxCharactersLimit(textView) { + let color = NSColor(hexString: filtered) + if let color = color, !ignoreUpdate { + colorChanged?(color) + } + } + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + background = theme.colors.background + textView.background = theme.colors.background + textView.textColor = theme.colors.text + } + + func textViewTextDidChangeSelectedRange(_ range: NSRange) { + + } + + func textViewDidPaste(_ pasteboard: NSPasteboard) -> Bool { + + let text = pasteboard.string(forType: .string) + if let text = text, let color = NSColor(hexString: text) { + defaultColor = color + } + return true + } + + func textViewSize(_ textView: TGModernGrowingTextView!) -> NSSize { + return textView.frame.size + } + + func textViewIsTypingEnabled() -> Bool { + return true + } + + func maxCharactersLimit(_ textView: TGModernGrowingTextView!) -> Int32 { + return 7 + } + + private var ignoreUpdate: Bool = false + + var defaultColor: NSColor = NSColor(hexString: "#FFFFFF")! { + didSet { + ignoreUpdate = true + textView.setString(defaultColor.hexString) + ignoreUpdate = false + } + } + + var colorChanged: ((NSColor) -> Void)? = nil + var resetClick:(()->Void)? = nil + fileprivate let resetButton = ImageButton() + + let textView: TGModernGrowingTextView = TGModernGrowingTextView(frame: NSZeroRect, unscrollable: true) + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(textView) + + + layer?.cornerRadius = frameRect.height / 2 + layer?.borderWidth = .borderSize + layer?.borderColor = theme.colors.border.cgColor + textView.delegate = self + textView.setString("#") + textView.textFont = .normal(.text) + backgroundColor = theme.colors.background + textView.cursorColor = theme.colors.indicatorColor + resetButton.set(image: theme.icons.wallpaper_color_close, for: .Normal) + _ = resetButton.sizeToFit() + addSubview(resetButton) + + textView.setBackgroundColor(theme.colors.background) + + resetButton.set(handler: { [weak self] _ in + self?.resetClick?() + }, for: .Click) + } + + override func layout() { + super.layout() + updateLayout(size: frame.size, transition: .immediate) + } + + func updateLayout(size: NSSize, transition: ContainedViewLayoutTransition) { + transition.updateFrame(view: textView, frame: NSMakeRect(6, 0, frame.width - resetButton.frame.width - 15, frame.height)) + transition.updateFrame(view: resetButton, frame: resetButton.centerFrameY(x: frame.width - resetButton.frame.width - 5)) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/WallpaperCheckboxView.swift b/Telegram-Mac/WallpaperCheckboxView.swift new file mode 100644 index 0000000000..5f8b377cf4 --- /dev/null +++ b/Telegram-Mac/WallpaperCheckboxView.swift @@ -0,0 +1,354 @@ +// +// WallpaperCheckboxView.swift +// Telegram +// +// Created by Mikhail Filimonov on 22.07.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit + +private final class BlurCheckbox : View { + + var isFullFilled: Bool = false { + didSet { + needsDisplay = true + } + } + + private(set) var isSelected: Bool = false + private var timer: SwiftSignalKit.Timer? + func set(isSelected: Bool, animated: Bool) { + self.isSelected = isSelected + if animated { + timer?.invalidate() + + let fps: CGFloat = 60 + + let tick = isSelected ? ((1 - animationProgress) / (fps * 0.2)) : -(animationProgress / (fps * 0.2)) + timer = SwiftSignalKit.Timer(timeout: 0.016, repeat: true, completion: { [weak self] in + guard let `self` = self else {return} + self.animationProgress += tick + + if self.animationProgress <= 0 || self.animationProgress >= 1 { + self.timer?.invalidate() + self.timer = nil + } + + }, queue: .mainQueue()) + + timer?.start() + } else { + animationProgress = isSelected ? 1.0 : 0.0 + } + } + + deinit { + timer?.invalidate() + } + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var animationProgress: CGFloat = 0.0 { + didSet { + needsDisplay = true + } + } + + override func draw(_ layer: CALayer, in context: CGContext) { + super.draw(layer, in: context) + + let borderWidth: CGFloat = 2.0 + + context.setStrokeColor(.white) + context.setLineWidth(borderWidth) + context.strokeEllipse(in: bounds.insetBy(dx: borderWidth / 2.0, dy: borderWidth / 2.0)) + + let progress: CGFloat = animationProgress + let diameter = bounds.width + let center = CGPoint(x: diameter / 2.0, y: diameter / 2.0) + + + context.setFillColor(.white) + context.fillEllipse(in: bounds.insetBy(dx: (diameter - borderWidth) * (1.0 - animationProgress), dy: (diameter - borderWidth) * (1.0 - animationProgress))) + if !isFullFilled { + let firstSegment: CGFloat = max(0.0, min(1.0, progress * 3.0)) + let s = CGPoint(x: center.x - 4.0, y: center.y + 1.0) + let p1 = CGPoint(x: 3.0, y: 3.0) + let p2 = CGPoint(x: 5.0, y: -6.0) + + if !firstSegment.isZero { + if firstSegment < 1.0 { + context.move(to: CGPoint(x: s.x + p1.x * firstSegment, y: s.y + p1.y * firstSegment)) + context.addLine(to: s) + } else { + let secondSegment = (progress - 0.33) * 1.5 + context.move(to: CGPoint(x: s.x + p1.x + p2.x * secondSegment, y: s.y + p1.y + p2.y * secondSegment)) + context.addLine(to: CGPoint(x: s.x + p1.x, y: s.y + p1.y)) + context.addLine(to: s) + } + } + + + context.setBlendMode(.clear) + context.setLineWidth(borderWidth) + context.setLineCap(.round) + context.setLineJoin(.round) + context.setMiterLimit(10.0) + + + context.strokePath() + } + + } +} + + +final class WallpaperCheckboxView : Control { + + final class ColorsListView : View { + + var colors:[NSColor] = [] { + didSet { + needsDisplay = true + } + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + super.draw(layer, in: ctx) + + ctx.round(frame.size, frame.height / 2) + + if colors.count == 1 { + ctx.setFillColor(colors[0].cgColor) + ctx.fill(bounds) + } else if colors.count == 2 { + ctx.setFillColor(colors[0].cgColor) + ctx.fill(NSMakeRect(0, 0, frame.width / 2, frame.height)) + ctx.setFillColor(colors[1].cgColor) + ctx.fill(NSMakeRect(frame.width / 2, 0, frame.width / 2, frame.height)) + } else if colors.count == 3 { + + ctx.setFillColor(colors[2].cgColor) + ctx.fill(bounds) + + ctx.setFillColor(colors[0].cgColor) + var path = CGMutablePath() + path.move(to: NSMakePoint(0, 0)) + path.addLine(to: CGPoint(x: frame.width / 2, y: 0)) + path.addLine(to: CGPoint(x: frame.width / 2, y: frame.height / 2)) + path.addLine(to: CGPoint(x: 0, y: frame.height * 0.8)) + ctx.addPath(path) + ctx.fillPath() + + ctx.setFillColor(colors[1].cgColor) + path = CGMutablePath() + path.move(to: NSMakePoint(frame.width, 0)) + path.addLine(to: NSMakePoint(frame.width / 2, 0)) + path.addLine(to: CGPoint(x: frame.width / 2, y: frame.height / 2)) + path.addLine(to: CGPoint(x: frame.width, y: frame.height * 0.8)) + ctx.addPath(path) + ctx.fillPath() + +// ctx.setFillColor(colors[2].cgColor) +// path = CGMutablePath() +// path.move(to: NSMakePoint(0, frame.height * 0.8)) +// path.addLine(to: CGPoint(x: frame.width / 2, y: frame.height / 2)) +// path.addLine(to: CGPoint(x: frame.width, y: frame.height * 0.8)) +// ctx.addPath(path) +// ctx.fillPath() + + } else if colors.count == 4 { + ctx.setFillColor(colors[0].cgColor) + ctx.fill(NSMakeRect(0, 0, frame.width / 2, frame.height / 2)) + + ctx.setFillColor(colors[1].cgColor) + ctx.fill(NSMakeRect(frame.width / 2, 0, frame.width / 2, frame.height / 2)) + + ctx.setFillColor(colors[2].cgColor) + ctx.fill(NSMakeRect(0, frame.height / 2, frame.width / 2, frame.height / 2)) + + ctx.setFillColor(colors[3].cgColor) + ctx.fill(NSMakeRect(frame.width / 2, frame.height / 2, frame.width / 2, frame.height / 2)) + + } + } + } + + private let title:(TextNodeLayout,TextNode) + fileprivate let checkbox: BlurCheckbox = BlurCheckbox(frame: NSMakeRect(0, 0, 16, 16)) + + private var _isSelected: Bool = false + override var isSelected: Bool { + get { + return _isSelected + } + set { + _isSelected = newValue + self.update(by: self.bgcolor) + } + } + + + var isFullFilled: Bool = false { + didSet { + //checkbox.isFullFilled = isFullFilled + } + } + + private var colors: ColorsListView? + + required init(frame frameRect: NSRect, title: String) { + self.title = TextNode.layoutText(.initialize(string: title, color: .white, font: .medium(.text)), nil, 1, .end, NSMakeSize(CGFloat.greatestFiniteMagnitude, CGFloat.greatestFiniteMagnitude), nil, false, .left) + super.init(frame: frameRect) + addSubview(checkbox) + + + colors?.colors = [NSColor.random, NSColor.random, NSColor.random] + + layer?.cornerRadius = frameRect.height / 2 + setFrameSize(self.title.0.size.width + 10 + checkbox.frame.width + 10 + 10, frameRect.height) + scaleOnClick = true + + self.set(handler: { [weak self] _ in + if let strongSelf = self { + strongSelf.isSelected = !strongSelf.isSelected + strongSelf.onChangedValue?(strongSelf.isSelected) + strongSelf.update(by: strongSelf.bgcolor) + } + }, for: .Click) + + } + + var hasPattern: Bool = false { + didSet { + checkbox.set(isSelected: hasPattern, animated: false) + } + } + + var colorsValue: [NSColor] = [] { + didSet { + if colors == nil, !colorsValue.isEmpty { + colors = ColorsListView(frame: NSMakeRect(10, 0, 16, 16)) + addSubview(colors!) + } else if colorsValue.isEmpty { + colors?.removeFromSuperview() + colors = nil + } + colors?.colors = colorsValue + checkbox.isHidden = colors != nil + checkbox.set(isSelected: !colorsValue.isEmpty, animated: true) + } + } + + var onChangedValue:((Bool)->Void)? + + override func layout() { + super.layout() + checkbox.centerY(x: 10) + colors?.centerY(x: 10) + } + private var bgcolor: NSColor? = nil + + func update(by color: NSColor?) -> Void { + self.bgcolor = color + if let color = color { + backgroundColor = isSelected ? color.darker() : color + } else { + backgroundColor = isSelected ? theme.chatServiceItemColor.darker() : theme.chatServiceItemColor + } + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + super.draw(layer, in: ctx) + let rect = focus(title.0.size) + title.1.draw(NSMakeRect(frame.width - rect.width - 10, rect.minY, rect.width, rect.height), in: ctx, backingScaleFactor: backingScaleFactor, backgroundColor: .clear) + } + + deinit { + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } +} + + +final class WallpaperPlayRotateView : Control { + + + private let imageView = ImageView() + + var onClick:(()->Void)? + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + layer?.cornerRadius = frameRect.height / 2 + addSubview(imageView) + scaleOnClick = true + self.set(handler: { [weak self] _ in + self?.onClick?() + }, for: .Click) + } + + func set(rotation: Int32?, animated: Bool) { + if let layer = self.imageView.layer { + if let rotation = rotation { + if animated { + if let animatorLayer = self.imageView.animator().layer { + layer.position = CGPoint(x: layer.frame.midX, y: layer.frame.midY) + layer.anchorPoint = CGPoint(x: 0.5, y: 0.5) + + NSAnimationContext.beginGrouping() + NSAnimationContext.current.allowsImplicitAnimation = true + animatorLayer.transform = CATransform3DMakeRotation(CGFloat.pi * CGFloat(rotation) / 180.0, 0, 0, 1) + NSAnimationContext.endGrouping() + } + } else { + layer.position = CGPoint(x: layer.frame.midX, y: layer.frame.midY) + layer.anchorPoint = CGPoint(x: 0.5, y: 0.5) + layer.transform = CATransform3DMakeRotation(CGFloat.pi * CGFloat(rotation) / 180.0, 0, 0, 1) + } + } + } + } + + override func layout() { + super.layout() + imageView.center() + } + + func update(_ image: CGImage) { + self.imageView.image = image + self.imageView.sizeToFit() + self.imageView.center() + } + + func update(by color: NSColor?) -> Void { + if let color = color { + backgroundColor = color + } else { + backgroundColor = theme.chatServiceItemColor + } + } + + deinit { + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/WallpaperColorPicker.swift b/Telegram-Mac/WallpaperColorPicker.swift new file mode 100644 index 0000000000..614bf792c3 --- /dev/null +++ b/Telegram-Mac/WallpaperColorPicker.swift @@ -0,0 +1,449 @@ +// +// WallpaperColorPicker.swift +// Telegram +// +// Created by Mikhail Filimonov on 29/01/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit + +private let shadowImage: CGImage = { + return generateImage(CGSize(width: 45.0, height: 45.0), opaque: false, scale: System.backingScale, rotatedContext: { size, context in + context.setBlendMode(.clear) + context.setFillColor(NSColor.clear.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.setBlendMode(.normal) + context.setShadow(offset: CGSize(width: 0.0, height: 1.5), blur: 4.5, color: NSColor(rgb: 0x000000, alpha: 0.5).cgColor) + context.setFillColor(NSColor(rgb: 0x000000, alpha: 0.5).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: 3.0 + .borderSize, dy: 3.0 + .borderSize)) + })! +}() + +private let smallShadowImage: CGImage = { + return generateImage(CGSize(width: 24.0, height: 24.0), opaque: false, scale: System.backingScale, rotatedContext: { size, context in + context.setBlendMode(.clear) + context.setFillColor(NSColor.clear.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.setBlendMode(.normal) + context.setShadow(offset: CGSize(width: 0.0, height: 1.5), blur: 4.5, color: NSColor(rgb: 0x000000, alpha: 0.65).cgColor) + context.setFillColor(NSColor(rgb: 0x000000, alpha: 0.5).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: 3.0 + .borderSize, dy: 3.0 + .borderSize)) + })! +}() + +private let pointerImage: CGImage = { + return generateImage(CGSize(width: 12.0, height: 42.0), opaque: false, scale: System.backingScale, rotatedContext: { size, context in + context.setBlendMode(.clear) + context.setFillColor(NSColor.clear.cgColor) + context.fill(CGRect(origin: CGPoint(), size: size)) + context.setBlendMode(.normal) + + let lineWidth: CGFloat = 1.0 + context.setFillColor(NSColor.black.cgColor) + context.setStrokeColor(NSColor.white.cgColor) + context.setLineWidth(lineWidth) + context.setLineCap(.round) + + let pointerHeight: CGFloat = 6.0 + context.move(to: CGPoint(x: lineWidth / 2.0, y: lineWidth / 2.0)) + context.addLine(to: CGPoint(x: size.width - lineWidth / 2.0, y: lineWidth / 2.0)) + context.addLine(to: CGPoint(x: size.width / 2.0, y: lineWidth / 2.0 + pointerHeight)) + context.closePath() + context.drawPath(using: .fillStroke) + + context.move(to: CGPoint(x: lineWidth / 2.0, y: size.height - lineWidth / 2.0)) + context.addLine(to: CGPoint(x: size.width / 2.0, y: size.height - lineWidth / 2.0 - pointerHeight)) + context.addLine(to: CGPoint(x: size.width - lineWidth / 2.0, y: size.height - lineWidth / 2.0)) + context.closePath() + context.drawPath(using: .fillStroke) + })! +}() + +private final class HSVParameter: NSObject { + let hue: CGFloat + let saturation: CGFloat + let value: CGFloat + + init(hue: CGFloat, saturation: CGFloat, value: CGFloat) { + self.hue = hue + self.saturation = saturation + self.value = value + super.init() + } +} + +private final class IntensitySliderParameter: NSObject { + let bordered: Bool + let min: HSVParameter + let max: HSVParameter + + init(bordered: Bool, min: HSVParameter, max: HSVParameter) { + self.bordered = bordered + self.min = min + self.max = max + super.init() + } +} + + +private final class WallpaperColorHueSaturationView: View { + var parameters: HSVParameter = HSVParameter(hue: 1.0, saturation: 1.0, value: 1.0) { + didSet { + self.setNeedsDisplay() + } + } + + var value: CGFloat = 1.0 { + didSet { + parameters = HSVParameter(hue: 1.0, saturation: 1.0, value: self.value) + } + } + + override init() { + super.init() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + override func draw(_ layer: CALayer, in context: CGContext) { + + + let colorSpace = deviceColorSpace + + let colors = [NSColor(rgb: 0xff0000).cgColor, NSColor(rgb: 0xffff00).cgColor, NSColor(rgb: 0x00ff00).cgColor, NSColor(rgb: 0x00ffff).cgColor, NSColor(rgb: 0x0000ff).cgColor, NSColor(rgb: 0xff00ff).cgColor, NSColor(rgb: 0xff0000).cgColor] + var locations: [CGFloat] = [0.0, 0.16667, 0.33333, 0.5, 0.66667, 0.83334, 1.0] + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: bounds.width, y: 0.0), options: CGGradientDrawingOptions()) + + let overlayColors = [NSColor(rgb: 0xffffff, alpha: 0.0).cgColor, NSColor(rgb: 0xffffff).cgColor] + var overlayLocations: [CGFloat] = [0.0, 1.0] + let overlayGradient = CGGradient(colorsSpace: colorSpace, colors: overlayColors as CFArray, locations: &overlayLocations)! + context.drawLinearGradient(overlayGradient, start: CGPoint(), end: CGPoint(x: 0.0, y: bounds.height), options: CGGradientDrawingOptions()) + + context.setFillColor(NSColor(rgb: 0x000000, alpha: 1.0 - parameters.value).cgColor) + context.fill(bounds) + } +} + + +private final class WallpaperColorBrightnessView: View { + var hsv: (CGFloat, CGFloat, CGFloat) = (0.0, 1.0, 1.0) { + didSet { + self.setNeedsDisplay() + } + } + + override init() { + super.init() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + var parameters:HSVParameter { + return HSVParameter(hue: self.hsv.0, saturation: self.hsv.1, value: self.hsv.2) + } + + + override func draw(_ layer: CALayer, in context: CGContext) { + let colorSpace = deviceColorSpace + + context.setFillColor(NSColor(white: parameters.value, alpha: 1.0).cgColor) + context.fill(bounds) + + + + let path = NSBezierPath(roundedRect: bounds, xRadius: bounds.height / 2.0, yRadius: bounds.height / 2.0) + context.addPath(path.cgPath) + context.setFillColor(NSColor.white.cgColor) + context.fillPath() + + let innerPath = NSBezierPath(roundedRect: bounds.insetBy(dx: 1.0, dy: 1.0), xRadius: bounds.height / 2.0, yRadius: bounds.height / 2.0) + context.addPath(innerPath.cgPath) + context.clip() + + let color = NSColor(hue: parameters.hue, saturation: parameters.saturation, brightness: 1.0, alpha: 1.0) + let colors = [color.cgColor, NSColor.black.cgColor] + var locations: [CGFloat] = [0.0, 1.0] + let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)! + context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: bounds.width, y: 0.0), options: CGGradientDrawingOptions()) + } + +} + + +private final class WallpaperColorKnobView: View { + var hsv: (CGFloat, CGFloat, CGFloat) = (0.0, 0.0, 1.0) { + didSet { + if self.hsv != oldValue { + self.setNeedsDisplay() + } + } + } + + var parameters: HSVParameter { + return HSVParameter(hue: self.hsv.0, saturation: self.hsv.1, value: self.hsv.2) + } + + override init() { + super.init() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + override func draw(_ layer: CALayer, in context: CGContext) { + // if !isRasterizing { + context.setBlendMode(.copy) + context.setFillColor(NSColor.clear.cgColor) + context.fill(bounds) + // } + + let image = bounds.width > 30.0 ? shadowImage : smallShadowImage + context.draw(image, in: bounds) + + context.setBlendMode(.normal) + context.setFillColor(NSColor.white.cgColor) + context.fillEllipse(in: bounds.insetBy(dx: 3.0, dy: 3.0)) + + let color = NSColor(hue: parameters.hue, saturation: parameters.saturation, brightness: parameters.value, alpha: 1.0) + context.setFillColor(color.cgColor) + + let borderWidth: CGFloat = bounds.width > 30.0 ? 5.0 : 5.0 + context.fillEllipse(in: bounds.insetBy(dx: borderWidth - .borderSize, dy: borderWidth - .borderSize)) + } +} + +private enum PickerChangeValue { + case color + case brightness +} + + + +final class WallpaperColorPickerView: View { + private let brightnessView: WallpaperColorBrightnessView + private let brightnessKnobView: ImageView + private let colorView: WallpaperColorHueSaturationView + + + private let colorKnobView: WallpaperColorKnobView + + private var pickerValue: PickerChangeValue? + + var colorHSV: (CGFloat, CGFloat, CGFloat) = (0.0, 1.0, 1.0) + var color: NSColor { + get { + return NSColor(hue: self.colorHSV.0, saturation: self.colorHSV.1, brightness: self.colorHSV.2, alpha: 1.0) + } + set { + var hue: CGFloat = 0.0 + var saturation: CGFloat = 0.0 + var value: CGFloat = 0.0 + + + newValue.getHue(&hue, saturation: &saturation, brightness: &value, alpha: nil) + let newHSV: (CGFloat, CGFloat, CGFloat) = (hue, saturation, value) + + if newHSV != self.colorHSV { + self.colorHSV = newHSV + self.update() + } + } + } + var colorChanged: ((NSColor) -> Void)? + var colorChangeEnded: ((NSColor) -> Void)? + + + + var adjustingPattern: Bool = false { + didSet { + let value = self.adjustingPattern + self.brightnessView.isHidden = value + self.brightnessKnobView.isHidden = value + self.needsLayout = true + } + } + + override init() { + self.brightnessView = WallpaperColorBrightnessView() + //self.brightnessView.hitTestSlop = NSEdgeInsetsMake(-16.0, -16.0, -16.0, -16.0) + self.brightnessKnobView = ImageView() + self.brightnessKnobView.image = pointerImage + self.colorView = WallpaperColorHueSaturationView() + // self.colorView.hitTestSlop = NSEdgeInsetsMake(-16.0, -16.0, -16.0, -16.0) + self.colorKnobView = WallpaperColorKnobView() + + + + super.init() + + self.backgroundColor = .white + + self.addSubview(self.brightnessView) + self.addSubview(self.colorView) + self.addSubview(self.colorKnobView) + self.addSubview(self.brightnessKnobView) + + let valueChanged: (CGFloat, Bool) -> Void = { [weak self] value, ended in + if let strongSelf = self { + let previousColor = strongSelf.color + strongSelf.colorHSV.2 = 1.0 - value + + if strongSelf.color != previousColor || ended { + strongSelf.update() + if ended { + strongSelf.colorChangeEnded?(strongSelf.color) + } else { + strongSelf.colorChanged?(strongSelf.color) + } + } + } + } + + self.update() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + + private func update() { + if self.adjustingPattern { + self.backgroundColor = .white + } else { + self.backgroundColor = NSColor(white: self.colorHSV.2, alpha: 1.0) + } + self.colorView.value = self.colorHSV.2 + self.brightnessView.hsv = self.colorHSV + self.colorKnobView.hsv = self.colorHSV + needsLayout = true + } + + func updateKnobLayout(size: CGSize, panningColor: Bool) { + let knobSize = CGSize(width: 45.0, height: 45.0) + + let colorHeight = size.height - 40 + var colorKnobFrame = CGRect(x: -knobSize.width / 2.0 + size.width * self.colorHSV.0, y: -knobSize.height / 2.0 + (colorHeight * (1.0 - self.colorHSV.1)), width: knobSize.width, height: knobSize.height) + var origin = colorKnobFrame.origin + if !panningColor { + origin = CGPoint(x: max(0.0, min(origin.x, size.width - knobSize.width)), y: max(0.0, min(origin.y, colorHeight - knobSize.height))) + } else { + origin = origin.offsetBy(dx: 0.0, dy: -32.0) + } + colorKnobFrame.origin = origin + self.colorKnobView.frame = colorKnobFrame + + let inset: CGFloat = 42.0 + let brightnessKnobSize = CGSize(width: 12.0, height: 42.0) + let brightnessKnobFrame = CGRect(x: inset - brightnessKnobSize.width / 2.0 + (size.width - inset * 2.0) * (1.0 - self.colorHSV.2), y: size.height - 46.0, width: brightnessKnobSize.width, height: brightnessKnobSize.height) + self.brightnessKnobView.frame = brightnessKnobFrame + } + + override func layout() { + super.layout() + let size = frame.size + let colorHeight = size.height - 40.0 + colorView.frame = CGRect(x: 0.0, y: 0.0, width: size.width, height: colorHeight) + + let inset: CGFloat = 42.0 + brightnessView.frame = CGRect(x: inset, y: size.height - 40, width: size.width - (inset * 2.0), height: 29.0) + + let slidersInset: CGFloat = 24.0 + + self.updateKnobLayout(size: size, panningColor: false) + } + + override func mouseDown(with event: NSEvent) { + + if brightnessView.mouseInside() || brightnessKnobView._mouseInside() { + pickerValue = .brightness + } else { + pickerValue = .color + } + + guard let pickerValue = pickerValue else { return } + let size = frame.size + let colorHeight = size.height - 40.0 + + switch pickerValue { + case .color: + let location = self.convert(event.locationInWindow, from: nil) + let newHue = max(0.0, min(1.0, location.x / size.width)) + let newSaturation = max(0.0, min(1.0, (1.0 - location.y / colorHeight))) + self.colorHSV.0 = newHue + self.colorHSV.1 = newSaturation + case .brightness: + let location = brightnessView.convert(event.locationInWindow, from: nil) + let brightnessWidth: CGFloat = brightnessView.frame.width + let newValue = max(0.0, min(1.0, 1.0 - location.x / brightnessWidth)) + self.colorHSV.2 = newValue + } + self.updateKnobLayout(size: size, panningColor: false) + self.update() + self.colorChanged?(self.color) + } + + override func mouseDragged(with event: NSEvent) { + let previousColor = self.color + let size = frame.size + let colorHeight = size.height - 40.0 + + guard let pickerValue = pickerValue else { return } + + + switch pickerValue { + case .color: + var location = self.convert(event.locationInWindow, from: nil) + location.x = min(max(location.x, 1.0), frame.width) + + let newHue = max(0.0, min(1.0, location.x / size.width)) + let newSaturation = max(0.0, min(1.0, (1.0 - location.y / colorHeight))) + self.colorHSV.0 = newHue + self.colorHSV.1 = newSaturation + case .brightness: + let location = brightnessView.convert(event.locationInWindow, from: nil) + let brightnessWidth: CGFloat = brightnessView.frame.width + let newValue = max(0.0, min(1.0, 1.0 - location.x / brightnessWidth)) + self.colorHSV.2 = newValue + } + + + self.updateKnobLayout(size: size, panningColor: false) + + if self.color != previousColor { + self.update() + self.colorChanged?(self.color) + } + } + + override func mouseUp(with event: NSEvent) { + self.updateKnobLayout(size: frame.size, panningColor: false) + self.colorChanged?(self.color) + self.colorChangeEnded?(self.color) + pickerValue = nil + } +} diff --git a/Telegram-Mac/WallpaperColorPickerContainerView.swift b/Telegram-Mac/WallpaperColorPickerContainerView.swift new file mode 100644 index 0000000000..96ba728f55 --- /dev/null +++ b/Telegram-Mac/WallpaperColorPickerContainerView.swift @@ -0,0 +1,286 @@ +// +// WallpaperColorPickerContainerView.swift +// Telegram +// +// Created by Mikhail Filimonov on 21.07.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit +import TelegramCore + +import Postbox + +import CoreGraphics + + +private final class ColorsListView : View { + + private class Color : Control { + + var removed: Bool = false + + var click:(()->Void)? = nil + + private var selection:View? + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + layer?.cornerRadius = frameRect.height / 2 + + scaleOnClick = true + + self.set(handler: { [weak self] _ in + if self?.removed == false { + self?.click?() + } + }, for: .Click) + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + func update(color: NSColor, isSelected: Bool, animated: Bool) { + self.backgroundColor = color.withAlphaComponent(1) + if animated { + self.layer?.animateBackground() + } + if isSelected { + let current: View + if let c = self.selection { + current = c + } else { + current = View(frame: self.bounds.insetBy(dx: 2, dy: 2)) + current.layer?.cornerRadius = current.frame.height / 2 + current.layer?.borderWidth = 2 + self.selection = current + addSubview(current) + if animated { + current.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + current.layer?.animateScaleSpring(from: 0.1, to: 1.0, duration: 0.3, removeOnCompletion: false, bounce: false) + } + } + current.layer?.borderColor = theme.colors.grayBackground.cgColor + current.layer?.animateBorderColor() + } else { + if let selection = selection { + self.selection = nil + performSubviewRemoval(selection, animated: animated) + selection.layer?.animateScaleSpring(from: 1, to: 0.1, duration: 0.3, bounce: false) + } + } + } + override func layout() { + super.layout() + selection?.center() + } + } + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + setup() + } + override init() { + super.init(frame: .zero) + setup() + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + private func setup() { + + } + + private(set) var selected: Int = 0 + + var selectedColor: NSColor { + return self.colors[selected] + } + + private var colors: [NSColor] = [] + + var select:((Int)->Void)? = nil + + var count: Int { + return colors.count + } + + func update(colors: [NSColor], selected: Int, animated: Bool) { + self.colors = colors + self.selected = selected + var subviews = self.subviews.filter { + ($0 as? Color)?.removed == false + } + + if subviews.count > colors.count { + while subviews.count != colors.count { + if let view = subviews.removeLast() as? Color { + view.removed = true + performSubviewRemoval(view, animated: animated) + view.layer?.animateScaleSpring(from: 1, to: 0.1, duration: 0.3, removeOnCompletion: false, bounce: false) + } + + } + } else if subviews.count < colors.count { + while subviews.count != colors.count { + let count = CGFloat(subviews.count) + let color = Color(frame: NSMakeRect(30 * count + count * 5, 0, 30, 30)) + self.addSubview(color) + subviews.append(color) + + if animated { + color.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + color.layer?.animateScaleSpring(from: 0.1, to: 1, duration: 0.3, bounce: false) + } + } + } + + self.selected = max(0, min(colors.count - 1, self.selected)) + + for (i, color) in colors.enumerated() { + let view = subviews[i] as? Color + view?.update(color: color, isSelected: selected == i, animated: animated) + view?.click = { [weak self] in + self?.selected = i + self?.select?(i) + } + } + } + + func size() -> NSSize { + let count = CGFloat(colors.count) + return NSMakeSize(30 * count + max(0, (count - 1) * 5), 30) + } + + +} + +final class WallpaperColorPickerContainerView : View { + let colorEditor:WallpaperAdditionColorView = WallpaperAdditionColorView(frame: NSMakeRect(0, 4, 125, 30)) + let colorPicker = WallpaperColorPickerView() + private let colorsContainer: View = View(frame: NSMakeRect(0, 0, 0, 38)) + private let addColor: ImageButton = ImageButton() + private let colorsView = ColorsListView() + + var modeDidUpdate:((WallpaperColorSelectMode)->Void)? = nil + + private(set) var mode: WallpaperColorSelectMode = .single(NSColor(hexString: "#ffffff")!) + + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + colorsContainer.addSubview(colorEditor) + colorsContainer.addSubview(addColor) + colorsContainer.addSubview(colorsView) + addSubview(colorPicker) + addSubview(colorsContainer) + updateLocalizationAndTheme(theme: theme) + + + let updateColor:(NSColor)->Void = { [weak self] color in + guard let strongSelf = self else { + return + } + let mode = strongSelf.mode.withUpdatedColor(color) + strongSelf.modeDidUpdate?(mode) + } + + colorPicker.colorChanged = updateColor + colorEditor.colorChanged = updateColor + + colorEditor.resetClick = { [weak self] in + guard let strongSelf = self else { + return + } + let mode = strongSelf.mode.withRemovedColor(strongSelf.colorsView.selected) + strongSelf.modeDidUpdate?(mode) + } + + addColor.set(handler: { [weak self] _ in + guard let strongSelf = self else { + return + } + let color = strongSelf.colorsView.selectedColor + let index = strongSelf.colorsView.selected + let mode = strongSelf.mode.withAddedColor(color, at: index) + strongSelf.modeDidUpdate?(mode) + }, for: .Click) + + colorsView.select = { [weak self] index in + guard let strongSelf = self else { + return + } + let mode = strongSelf.mode.withUpdatedIndex(index) + strongSelf.modeDidUpdate?(mode) + } + } + + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + let theme = theme as! TelegramPresentationTheme + colorsContainer.backgroundColor = theme.colors.grayBackground + colorsContainer.border = [.Top, .Bottom] + colorsContainer.borderColor = theme.colors.border + backgroundColor = theme.colors.background + + addColor.set(image: theme.icons.wallpaper_color_add, for: .Normal) + _ = addColor.sizeToFit() + } + + func updateMode(_ mode: WallpaperColorSelectMode, animated: Bool) { + self.mode = mode + switch mode { + case let .single(color): + self.colorsView.update(colors: [color], selected: 0, animated: animated) + colorEditor.defaultColor = color + addColor.isHidden = !canUseGradient + case let .gradient(colors, selected, _): + self.colorsView.update(colors: colors, selected: selected, animated: animated) + addColor.isHidden = colors.count > 3 + colorEditor.defaultColor = colors[selected] + } + colorPicker.color = colorEditor.defaultColor + + let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate + updateLayout(size: frame.size, transition: transition) + } + + var canUseGradient: Bool = false { + didSet { + addColor.isHidden = !self.canUseGradient + } + } + + var colorChanged: ((WallpaperColorSelectMode) -> Void)? = nil + + func updateLayout(size: NSSize, transition: ContainedViewLayoutTransition) { + transition.updateFrame(view: colorPicker, frame: NSMakeRect(0, 38, frame.width, frame.height - 38)) + transition.updateFrame(view: colorsContainer, frame: NSMakeRect(0, 0, frame.width, 38)) + + transition.updateFrame(view: colorsView, frame: CGRect(origin: .init(x: 10, y: 4), size: colorsView.size())) + + var c_e_w: CGFloat = colorsView.frame.width > 0 ? frame.width - (colorsView.frame.width + 30) : (frame.width - 20) + + if colorsView.count < 4, !addColor.isHidden { + c_e_w -= (addColor.frame.width + 10) + } + switch self.mode { + case .gradient: + transition.updateFrame(view: colorEditor, frame: NSMakeRect(10 + colorsView.frame.maxX, 4, c_e_w, 30)) + colorEditor.updateLayout(size: colorEditor.frame.size, transition: transition) + case .single: + transition.updateFrame(view: colorEditor, frame: NSMakeRect(10 + colorsView.frame.maxX, 4, c_e_w, 30)) + colorEditor.updateLayout(size: colorEditor.frame.size, transition: transition) + } + transition.updateFrame(view: addColor, frame: addColor.centerFrameY(x: frame.width - addColor.frame.width - 10)) + } + + override func layout() { + updateLayout(size: frame.size, transition: .immediate) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + diff --git a/Telegram-Mac/WallpaperPatternPreviewController.swift b/Telegram-Mac/WallpaperPatternPreviewController.swift new file mode 100644 index 0000000000..09e2a776ed --- /dev/null +++ b/Telegram-Mac/WallpaperPatternPreviewController.swift @@ -0,0 +1,252 @@ +// +// WallpaperPatternPreviewController.swift +// Telegram +// +// Created by Mikhail Filimonov on 22.07.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + +final class WallpaperPatternPreviewView: View { + private let documentView: View = View() + private let scrollView = HorizontalScrollView() + private let sliderView = LinearProgressControl(progressHeight: 5) + private let intensityTextView = TextView() + private let intensityContainerView = View() + private let borderView = View() + var updateIntensity: ((Float) -> Void)? + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(scrollView) + + sliderView.highlightOnHover = false + scrollView.documentView = documentView + backgroundColor = theme.colors.background + + scrollView.backgroundColor = theme.colors.grayBackground.withAlphaComponent(0.7) + + borderView.backgroundColor = theme.colors.border + sliderView.scrubberImage = theme.icons.videoPlayerSliderInteractor + sliderView.roundCorners = true + sliderView.alignment = .center + sliderView.containerBackground = NSColor.grayBackground.withAlphaComponent(0.2) + sliderView.style = ControlStyle(foregroundColor: theme.colors.accent, backgroundColor: .clear, highlightColor: theme.colors.grayForeground) + sliderView.set(progress: 0.8) + sliderView.userInteractionEnabled = true + sliderView.insets = NSEdgeInsetsMake(0, 4.5, 0, 4.5) + sliderView.containerBackground = theme.colors.grayForeground + sliderView.liveScrobbling = true + sliderView.onUserChanged = { [weak self] value in + guard let `self` = self else {return} + self.sliderView.set(progress: CGFloat(value)) + self.updateIntensity?(value) + } + + let layout = TextViewLayout(.initialize(string: L10n.chatWPIntensity, color: theme.colors.grayText, font: .normal(.text))) + layout.measure(width: .greatestFiniteMagnitude) + intensityTextView.update(layout) + + intensityContainerView.addSubview(sliderView) + intensityContainerView.addSubview(intensityTextView) + intensityTextView.userInteractionEnabled = false + intensityTextView.isSelectable = false + addSubview(intensityContainerView) + addSubview(borderView) + } + + func updateColor(_ colors: [NSColor], rotation: Int32?, account: Account) { + self.colors = colors + for subview in self.documentView.subviews { + if let subview = (subview as? WallpaperPatternView) { + subview.update(with: subview.pattern, isSelected: !subview.checkbox.isHidden, account: account, colors: colors, rotation: rotation) + } + } + } + + fileprivate var colors: [NSColor] = [NSColor(rgb: 0xd6e2ee, alpha: 0.5)] + fileprivate var rotation: Int32? = nil + + func updateSelected(_ pattern: Wallpaper?) { + + for subview in self.documentView.subviews { + if let subview = (subview as? WallpaperPatternView) { + if let pattern = pattern { + subview.checkbox.isHidden = subview.pattern == nil || subview.pattern?.isSemanticallyEqual(to: pattern) == false + } else { + subview.checkbox.isHidden = pattern != subview.pattern + } + } + } + + let selectedView = self.documentView.subviews.first { view -> Bool in + return !(view as! WallpaperPatternView).checkbox.isHidden + } + if let selectedView = selectedView { + scrollView.clipView.scroll(to: NSMakePoint(min(max(selectedView.frame.midX - frame.width / 2, 0), max(documentView.frame.width - frame.width, 0)), 0), animated: true) + } + + if let pattern = pattern { + intensityContainerView.isHidden = false + if let intensity = pattern.settings.intensity { + sliderView.set(progress: CGFloat(intensity) / 100.0) + } + } else { + intensityContainerView.isHidden = true + } + } + + func update(with patterns: [Wallpaper?], selected: Wallpaper?, account: Account, select: @escaping(Wallpaper?) -> Void) { + + + var files:Set = Set() + + let patterns = patterns.filter { value in + switch value { + case let .file(_, file, _, _): + if let id = file.id?.id { + if files.contains(id) { + return false + } else { + files.insert(id) + return true + } + } else { + return false + } + default: + return true + } + } + + documentView.removeAllSubviews() + var x: CGFloat = 10 + for pattern in patterns { + let patternView = WallpaperPatternView(frame: NSMakeRect(x, 10, 80, 80)) + patternView.update(with: pattern, isSelected: pattern == selected, account: account, colors: self.colors, rotation: self.rotation) + patternView.set(handler: { [weak self] _ in + select(pattern) + // self.updateSelected(pattern) + }, for: .Click) + documentView.addSubview(patternView) + x += patternView.frame.width + 10 + } + documentView.setFrameSize(NSMakeSize(x, 100)) + } + + override func layout() { + super.layout() + self.updateLayout(size: self.frame.size, transition: .immediate) + } + + func updateLayout(size: NSSize, transition: ContainedViewLayoutTransition) { + + let intensitySize = NSMakeSize(frame.width - 20, intensityTextView.frame.height + 12 + 3) + var intensityRect = focus(intensitySize) + intensityRect.origin.y = 110 + + transition.updateFrame(view: scrollView, frame: NSMakeRect(0, 0, frame.width, 100)) + transition.updateFrame(view: intensityContainerView, frame: intensityRect) + + transition.updateFrame(view: intensityTextView, frame: intensityTextView.centerFrameX(y: 0)) + transition.updateFrame(view: borderView, frame: NSMakeRect(0, 0, frame.width, .borderSize)) + + + let sliderSize = NSMakeSize(intensityContainerView.frame.width, 12) + var sliderRect = intensityContainerView.focus(sliderSize) + sliderRect.origin.y = intensityTextView.frame.height + 3 + + transition.updateFrame(view: sliderView, frame: sliderRect) + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class WallpaperPatternPreviewController: GenericViewController { + private let disposable = MetaDisposable() + private let context: AccountContext + + var colors: ([NSColor], Int32?) = ([NSColor(rgb: 0xd6e2ee, alpha: 0.5)], nil) { + didSet { + genericView.updateColor(self.colors.0, rotation: self.colors.1, account: context.account) + } + } + + var selected:((Wallpaper?) -> Void)? + + var intensity: Int32? = nil { + didSet { + if oldValue != nil, oldValue != intensity { + self.selected?(pattern?.withUpdatedSettings(WallpaperSettings(colors: pattern?.settings.colors ?? [], intensity: intensity))) + } + } + } + + var pattern: Wallpaper? { + didSet { + let intensity = self.intensity ?? pattern?.settings.intensity ?? 80 + self.intensity = intensity + + if let pattern = pattern { + switch pattern { + case .file: + genericView.updateSelected(pattern.withUpdatedSettings(.init(colors: pattern.settings.colors, intensity: intensity, rotation: pattern.settings.rotation))) + default: + break + } + } else { + genericView.updateSelected(nil) + } + } + } + + + init(context: AccountContext) { + self.context = context + super.init() + } + + override func viewDidLoad() { + super.viewDidLoad() + + + genericView.updateIntensity = { [weak self] intensity in + guard let `self` = self else {return} + self.intensity = Int32(intensity * 100) + } + + let signal = telegramWallpapers(postbox: context.account.postbox, network: context.account.network) |> map { wallpapers -> [Wallpaper] in + return wallpapers.compactMap { wallpaper in + switch wallpaper { + case let .file(file): + return file.isPattern ? Wallpaper(wallpaper) : nil + default: + return nil + } + } + } |> deliverOnMainQueue + + disposable.set(signal.start(next: { [weak self] patterns in + guard let `self` = self else {return} + self.genericView.update(with: [nil] + patterns, selected: nil, account: self.context.account, select: { [weak self] wallpaper in + self?.pattern = wallpaper + self?.selected?(wallpaper) + }) + self.pattern = patterns.first + })) + + } + + deinit { + disposable.dispose() + } + +} diff --git a/Telegram-Mac/WallpaperPatternPreviewView.swift b/Telegram-Mac/WallpaperPatternPreviewView.swift new file mode 100644 index 0000000000..53e64163ef --- /dev/null +++ b/Telegram-Mac/WallpaperPatternPreviewView.swift @@ -0,0 +1,106 @@ +// +// WallpaperPatternPreview.swift +// Telegram +// +// Created by Mikhail Filimonov on 29/01/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox +import SwiftSignalKit + +class WallpaperPatternView : Control { + private var backgroundView: BackgroundView? + let imageView = TransformImageView() + let checkbox: ImageView = ImageView() + private let emptyTextView = TextView() + fileprivate(set) var pattern: Wallpaper? + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + + + addSubview(imageView) + addSubview(checkbox) + checkbox.image = theme.icons.chatGroupToggleSelected + checkbox.sizeToFit() + self.layer?.cornerRadius = .cornerRadius + + emptyTextView.userInteractionEnabled = false + emptyTextView.isSelectable = false + } + + override func layout() { + super.layout() + imageView.frame = bounds + emptyTextView.center() + checkbox.setFrameOrigin(NSMakePoint(frame.width - checkbox.frame.width - 5, 5)) + backgroundView?.frame = bounds + } + + func update(with pattern: Wallpaper?, isSelected: Bool, account: Account, colors: [NSColor], rotation: Int32?) { + checkbox.isHidden = !isSelected + self.pattern = pattern + + + let layout = TextViewLayout(.initialize(string: L10n.chatWPPatternNone, color: colors.first!.brightnessAdjustedColor, font: .normal(.title))) + layout.measure(width: 80) + emptyTextView.update(layout) + + if let pattern = pattern { + + self.backgroundView?.removeFromSuperview() + self.backgroundView = nil + + emptyTextView.isHidden = true + imageView.isHidden = false + + let emptyColor: TransformImageEmptyColor + if colors.count > 1 { + let colors = colors.map { + return $0.withAlphaComponent($0.alpha == 0 ? 0.5 : $0.alpha) + } + emptyColor = .gradient(colors: colors, intensity: colors.first!.alpha, rotation: nil) + } else if let color = colors.first { + emptyColor = .color(color) + } else { + emptyColor = .color(NSColor(rgb: 0xd6e2ee, alpha: 0.5)) + } + + imageView.set(arguments: TransformImageArguments(corners: ImageCorners(radius: .cornerRadius), imageSize: pattern.dimensions.aspectFilled(NSMakeSize(300, 300)), boundingSize: bounds.size, intrinsicInsets: NSEdgeInsets(), emptyColor: emptyColor)) + switch pattern { + case let .file(_, file, _, _): + var representations:[TelegramMediaImageRepresentation] = [] + if let dimensions = file.dimensions { + representations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: file.resource, progressiveSizes: [], immediateThumbnailData: nil)) + } else { + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 300, height: 300), resource: file.resource, progressiveSizes: [], immediateThumbnailData: nil)) + } + imageView.setSignal(chatWallpaper(account: account, representations: representations, file: file, mode: .thumbnail, isPattern: true, autoFetchFullSize: true, scale: backingScaleFactor, isBlurred: false, synchronousLoad: false, drawPatternOnly: false), animate: false, synchronousLoad: false) + default: + break + } + } else { + emptyTextView.isHidden = false + imageView.isHidden = true + if self.backgroundView == nil { + let bg = BackgroundView(frame: bounds) + self.backgroundView = bg + addSubview(bg, positioned: .above, relativeTo: imageView) + } + if colors.count > 1 { + backgroundView?.backgroundMode = .gradient(colors: colors, rotation: rotation) + } else { + backgroundView?.backgroundMode = .color(color: colors[0]) + } + } + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/WallpaperPreviewController.swift b/Telegram-Mac/WallpaperPreviewController.swift new file mode 100644 index 0000000000..742bad18b5 --- /dev/null +++ b/Telegram-Mac/WallpaperPreviewController.swift @@ -0,0 +1,1320 @@ +// +// WallpaperPreviewController.swift +// Telegram +// +// Created by Mikhail Filimonov on 17/01/2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit +import TelegramCore + +import Postbox + +import CoreGraphics + +enum WallpaperPreviewMode : Equatable { + case plain + case blurred +} + +private func availableColors() -> [Int32] { + return [ + 0xffffff, + 0xd4dfea, + 0xb3cde1, + 0x6ab7ea, + 0x008dd0, + 0xd3e2da, + 0xc8e6c9, + 0xc5e1a5, + 0x61b06e, + 0xcdcfaf, + 0xa7a895, + 0x7c6f72, + 0xffd7ae, + 0xffb66d, + 0xde8751, + 0xefd5e0, + 0xdba1b9, + 0xffafaf, + 0xf16a60, + 0xe8bcea, + 0x9592ed, + 0xd9bc60, + 0xb17e49, + 0xd5cef7, + 0xdf506b, + 0x8bd2cc, + 0x3c847e, + 0x22612c, + 0x244d7c, + 0x3d3b85, + 0x65717d, + 0x18222d, + 0x000000 + ] +} + + + +extension Wallpaper { + var dimensions: NSSize { + switch self { + case let .file(_, file, _, _): + if let dimensions = file.dimensions { + return dimensions.size + } + return NSMakeSize(300, 300) + case let .image(representations, _): + let largest = largestImageRepresentation(representations) + return largest!.dimensions.size + case let .custom(representation, _): + return representation.dimensions.size + case .color: + return NSMakeSize(300, 300) + default: + return NSZeroSize + } + + } +} + +let WallpaperDimensions: NSSize = NSMakeSize(1040, 1580) + + + + +private enum WallpaperPreviewState { + case color + case pattern + case normal +} + + +enum WallpaperColorSelectMode : Equatable { + case single(NSColor) + case gradient([NSColor], Int, Int32?) + + var colors: [NSColor] { + switch self { + case let .single(color): + return [color] + case let .gradient(colors, _, _): + return colors + } + } + var rotation: Int32? { + switch self { + case .single: + return nil + case let .gradient(_, _, rotation): + return rotation + } + } + + func withRemovedColor(_ index: Int) -> WallpaperColorSelectMode { + switch self { + case .single: + return self + case let .gradient(colors, selected, rotation): + var colors = colors + if colors.count == 1 { + return .gradient(colors, 0, rotation) + } + colors.remove(at: index) + return .gradient(colors, min(selected, colors.count - 1), rotation) + } + } + func withAddedColor(_ color: NSColor, at index: Int) -> WallpaperColorSelectMode { + switch self { + case let .single(current): + return .gradient([current, color], 1, nil) + case let .gradient(colors, _, rotation): + var colors = colors + colors.insert(color, at: index) + return .gradient(colors, index + 1, rotation) + } + } + func withUpdatedRotatation(_ rotation: Int32?) -> WallpaperColorSelectMode { + switch self { + case .single: + return self + case let .gradient(colors, index, _): + return .gradient(colors, index, rotation) + } + } + + func withUpdatedColor(_ color: NSColor) -> WallpaperColorSelectMode { + switch self { + case .single: + return .single(color) + case let .gradient(colors, index, rotation): + var colors = colors + colors[index] = color + return .gradient(colors, index, rotation) + } + } + func withUpdatedIndex(_ index: Int) -> WallpaperColorSelectMode { + switch self { + case let .single(current): + return .single(current) + case let .gradient(colors, _, rotation): + return .gradient(colors, index, rotation) + } + } + +} + + +private final class WallpaperPreviewView: View { + private let updateStateDisposable = MetaDisposable() + private let backgroundView: BackgroundView = BackgroundView(frame: NSZeroRect) + private var image: CGImage? + private let disposable = MetaDisposable() + private let loadImageDisposable = MetaDisposable() + + private var progressView: RadialProgressView? + private let tableView: TableView + private let documentView: NSView + + let blurCheckbox = WallpaperCheckboxView(frame: NSMakeRect(0, 0, 70, 28), title: L10n.wallpaperPreviewBlurred) + let patternCheckbox = WallpaperCheckboxView(frame: NSMakeRect(0, 0, 70, 28), title: L10n.chatWPPattern) + let colorCheckbox = WallpaperCheckboxView(frame: NSMakeRect(0, 0, 70, 28), title: L10n.chatWPColor) + + private let rotateColors: WallpaperPlayRotateView = WallpaperPlayRotateView(frame: NSMakeRect(0, 0, 40, 40)) + + let checkboxContainer: View = View() + + let patternsController: WallpaperPatternPreviewController + let colorPicker = WallpaperColorPickerContainerView(frame: NSZeroRect) + private let controlsBg = View() + + private var previewState: WallpaperPreviewState = .normal + private var imageSize: NSSize = NSZeroSize + private let context: AccountContext + + fileprivate var ready:(()->Void)? = nil + + private(set) var wallpaper: Wallpaper { + didSet { + if oldValue != wallpaper { + let signal = Signal.complete() |> delay(0.05, queue: .mainQueue()) + updateStateDisposable.set(signal.start(completed: { [weak self] in + self?.updateState(synchronousLoad: false) + })) + } + } + } + + + + required init(frame frameRect: NSRect, context: AccountContext, wallpaper: Wallpaper) { + self.context = context + self.wallpaper = wallpaper + self.tableView = TableView(frame: NSMakeRect(0, 0, frameRect.width, frameRect.height), isFlipped: false, drawBorder: false) + self.documentView = tableView.documentView! + self.patternsController = WallpaperPatternPreviewController(context: context) + super.init(frame: frameRect) + backgroundView.useSharedAnimationPhase = false + addSubview(backgroundView) + documentView.removeFromSuperview() + addSubview(documentView) + checkboxContainer.addSubview(blurCheckbox) + checkboxContainer.addSubview(patternCheckbox) + checkboxContainer.addSubview(colorCheckbox) + checkboxContainer.addSubview(rotateColors) + addSubview(checkboxContainer) + + addSubview(controlsBg) + addSubview(colorPicker) + addSubview(patternsController.view) + + + controlsBg.backgroundColor = theme.colors.background + colorPicker.canUseGradient = true + + colorPicker.modeDidUpdate = { [weak self] mode in + guard let strongSelf = self else { + return + } + let wallpaper = strongSelf.wallpaper + switch mode { + case let .single(color): + switch wallpaper { + case let .file(_, _, settings, _): + strongSelf.wallpaper = wallpaper.withUpdatedSettings(settings.withUpdatedColor(color.argb)) + default: + strongSelf.wallpaper = .color(color.argb) + } + case let .gradient(colors, _, rotation): + switch wallpaper { + case let .file(_, _, settings, _): + let updated = WallpaperSettings(blur: settings.blur, motion: settings.motion, colors: colors.map { $0.argb }, intensity: settings.intensity, rotation: rotation) + strongSelf.wallpaper = wallpaper.withUpdatedSettings(updated) + default: + strongSelf.wallpaper = .gradient(nil, colors.map { $0.argb }, rotation) + } + } + strongSelf.updateMode(mode, animated: true) + } + + tableView.backgroundColor = .clear + tableView.layer?.backgroundColor = .clear + + addTableItems(context) + + self.layout() + + + blurCheckbox.onChangedValue = { [weak self] isSelected in + guard let `self` = self else { return } + self.wallpaper = self.wallpaper.withUpdatedBlurrred(isSelected) + } + + colorCheckbox.onChangedValue = { [weak self] isSelected in + guard let `self` = self else { return } + switch self.previewState { + case .color: + self.updateModifyState(.normal, animated: true) + default: + self.updateModifyState(.color, animated: true) + } + } + + patternCheckbox.onChangedValue = { [weak self] isSelected in + guard let `self` = self else { return } + switch self.previewState { + case .pattern: + self.updateModifyState(.normal, animated: true) + default: + self.updateModifyState(.pattern, animated: true) + } + } + + rotateColors.onClick = { [weak self] in + guard let `self` = self else { return } + + let mode = self.colorPicker.mode + let rotation: Int32? + + if mode.colors.count > 2 { + rotation = nil + self.backgroundView.doAction() + } else { + switch mode { + case let .gradient(_, _, r): + if let r = r { + if r + 45 == 360 { + rotation = nil + } else { + rotation = r + 45 + } + } else { + rotation = 45 + } + default: + rotation = nil + } + } + + self.colorPicker.modeDidUpdate?(mode.withUpdatedRotatation(rotation)) + + } + + switch wallpaper { + case let .color(color): + self.updateMode(.single(NSColor(argb: color)), animated: false) + case let .file(_, _, settings, _): + let colors:[NSColor] = settings.colors.map { .init(argb: $0) } + if !colors.isEmpty { + if colors.count == 1 { + self.updateMode(.single(colors[0]), animated: false) + } else { + self.updateMode(.gradient(colors, colors.count - 1, settings.rotation), animated: false) + } + } + case let .gradient(_, colors, rotation): + let colors = colors.map { NSColor(argb: $0) } + self.updateMode(.gradient(colors, 0, rotation), animated: false) + default: + break + } + + + colorPicker.colorChanged = { [weak self] mode in + guard let `self` = self else {return} + switch mode { + case let .single(color): + self.patternsController.colors = ([color], nil) + case let .gradient(colors, _, rotation): + self.patternsController.colors = (colors, rotation) + } + self.updateMode(mode, animated: true) + } + + patternsController.selected = { [weak self] wallpaper in + guard let `self` = self else {return} + if let wallpaper = wallpaper { + switch self.wallpaper { + case let .color(color): + self.wallpaper = wallpaper.withUpdatedSettings(WallpaperSettings(colors: [color], intensity: self.patternsController.intensity)) + case let .gradient(_, colors, r): + self.wallpaper = wallpaper.withUpdatedSettings(WallpaperSettings(colors: colors.map { + NSColor(argb: $0).withAlphaComponent(1.0).argb + }, intensity: self.patternsController.intensity, rotation: r)) + case let .file(_, _, settings, _): + self.wallpaper = wallpaper.withUpdatedSettings(WallpaperSettings(colors: settings.colors, intensity: self.patternsController.intensity, rotation: settings.rotation)) + default: + break + } + } else { + switch self.wallpaper { + case .color: + break + case let .file(_, _, settings, _): + if settings.colors.count == 1 { + self.wallpaper = .color(settings.colors.first!) + } else if settings.colors.count > 1 { + self.wallpaper = .gradient(nil, settings.colors, nil) + } else { + self.wallpaper = .none + } + default: + break + } + } + } + + + tableView.addScroll(listener: TableScrollListener(dispatchWhenVisibleRangeUpdated: false, { [weak self] position in + guard let `self` = self else { + return + } + self.tableView.enumerateVisibleViews(with: { view in + if let view = view as? ChatRowView { + view.updateBackground(animated: false, item: view.item) + } + }) + })) + + } + + private func addTableItems(_ context: AccountContext) { + + + switch wallpaper { + case .color: + _ = tableView.addItem(item: GeneralRowItem(frame.size, height: 50, stableId: 0, backgroundColor: .clear)) + case .file(_, _, _, _): + _ = tableView.addItem(item: GeneralRowItem(frame.size, height: 50, stableId: 0, backgroundColor: .clear)) + default: + _ = tableView.addItem(item: GeneralRowItem(frame.size, height: 50, stableId: 0, backgroundColor: .clear)) + } + + let chatInteraction = ChatInteraction(chatLocation: .peer(PeerId(0)), context: context, disableSelectAbility: true) + + chatInteraction.getGradientOffsetRect = { [weak self] in + guard let `self` = self else { + return .zero + } + return CGRect(origin: NSMakePoint(0, self.documentView.frame.height), size: self.documentView.frame.size) + } + + + let fromUser1 = TelegramUser(id: PeerId(1), accessHash: nil, firstName: L10n.appearanceSettingsChatPreviewUserName1, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + let fromUser2 = TelegramUser(id: PeerId(2), accessHash: nil, firstName: L10n.appearanceSettingsChatPreviewUserName2, lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: []) + + + let firstText: String + let secondText: String + switch wallpaper { + case let .file(_, _, _, isPattern): + if isPattern { + firstText = L10n.chatWPColorFirstMessage + secondText = L10n.chatWPColorSecondMessage + } else { + firstText = L10n.chatWPFirstMessage + secondText = L10n.chatWPSecondMessage + } + case .image: + firstText = L10n.chatWPFirstMessage + secondText = L10n.chatWPSecondMessage + default: + firstText = L10n.chatWPColorFirstMessage + secondText = L10n.chatWPColorSecondMessage + } + + let firstMessage = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: fromUser1.id, namespace: 0, id: 0), globallyUniqueId: 0, groupingKey: 0, groupInfo: nil, threadId: nil, timestamp: 60 * 20 + 60*60*18, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: fromUser2, text: firstText, attributes: [], media: [], peers:SimpleDictionary([fromUser2.id : fromUser2, fromUser1.id : fromUser1]) , associatedMessages: SimpleDictionary(), associatedMessageIds: []) + + let firstEntry: ChatHistoryEntry = .MessageEntry(firstMessage, MessageIndex(firstMessage), true, .bubble, .Full(rank: nil, header: .normal), nil, ChatHistoryEntryData(nil, MessageEntryAdditionalData(), AutoplayMediaPreferences.defaultSettings)) + + let secondMessage = Message(stableId: 1, stableVersion: 0, id: MessageId(peerId: fromUser1.id, namespace: 0, id: 1), globallyUniqueId: 0, groupingKey: 0, groupInfo: nil, threadId: nil, timestamp: 60 * 22 + 60*60*18, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: fromUser1, text: secondText, attributes: [], media: [], peers:SimpleDictionary([fromUser2.id : fromUser2, fromUser1.id : fromUser1]) , associatedMessages: SimpleDictionary(), associatedMessageIds: []) + + let secondEntry: ChatHistoryEntry = .MessageEntry(secondMessage, MessageIndex(secondMessage), true, .bubble, .Full(rank: nil, header: .normal), nil, ChatHistoryEntryData(nil, MessageEntryAdditionalData(), AutoplayMediaPreferences.defaultSettings)) + + + let item1 = ChatRowItem.item(frame.size, from: firstEntry, interaction: chatInteraction, theme: theme) + let item2 = ChatRowItem.item(frame.size, from: secondEntry, interaction: chatInteraction, theme: theme) + + + + _ = tableView.addItem(item: item2) + _ = tableView.addItem(item: item1) + + } + + var croppedRect: NSRect { + let fittedSize = WallpaperDimensions.aspectFitted(imageSize) + return fittedSize.bounds + } + + deinit { + updateStateDisposable.dispose() + disposable.dispose() + loadImageDisposable.dispose() + } + + override func layout() { + super.layout() + self.updateLayout(size: frame.size, transition: .immediate) + } + + + private func updateMode(_ mode: WallpaperColorSelectMode, animated: Bool) { + self.colorPicker.updateMode(mode, animated: animated) + patternsController.colors = (mode.colors, mode.rotation) + self.rotateColors.set(rotation: mode.rotation, animated: animated) + } + + func updateLayout(size: NSSize, transition: ContainedViewLayoutTransition) { + + + var checkboxViews:[NSView] = [] + + if !patternCheckbox.isHidden { + checkboxViews.append(patternCheckbox) + } + if !rotateColors.isHidden { + checkboxViews.append(rotateColors) + } + if !colorCheckbox.isHidden { + checkboxViews.append(colorCheckbox) + } + if !blurCheckbox.isHidden { + checkboxViews.append(blurCheckbox) + } + + let checkboxWidth: CGFloat = checkboxViews.reduce(0, { current, value in + return current + value.frame.width + }) + CGFloat(max(0, checkboxViews.count - 1)) * 10 + + + let colorPickerSize = NSMakeSize(frame.width, 168) + let patternsSize = NSMakeSize(frame.width, 168) + let controlsSize = NSMakeSize(frame.width, 168) + + let checkboxSize = NSMakeSize(checkboxWidth, 50) + let documentSize = NSMakeSize(frame.width, documentView.frame.height) + + + + + transition.updateFrame(view: tableView, frame: bounds) + + if let progressView = progressView { + transition.updateFrame(view: progressView, frame: progressView.centerFrame()) + } + + self.tableView.enumerateVisibleViews(with: { view in + if let view = view as? ChatRowView { + view.updateBackground(animated: transition.isAnimated, item: view.item) + } + }) + + + + let backgroundSize: NSSize + switch self.previewState { + case .color: + backgroundSize = NSMakeSize(frame.width, frame.height - colorPickerSize.height) + case .pattern: + backgroundSize = NSMakeSize(frame.width, frame.height - patternsSize.height) + default: + backgroundSize = frame.size + } + transition.updateFrame(view: backgroundView, frame: backgroundSize.bounds) + backgroundView.updateLayout(size: backgroundSize, transition: transition) + + switch previewState { + case .color, .pattern: + transition.updateFrame(view: documentView, frame: .init(origin: .init(x: 0, y: frame.height - colorPicker.frame.height - tableView.listHeight - 10), size: documentSize)) + + let checkboxRect = CGRect(origin: NSMakePoint(focus(checkboxSize).minX, frame.height - colorPicker.frame.height - checkboxSize.height - 10), size: checkboxSize) + + transition.updateFrame(view: checkboxContainer, frame: checkboxRect) + case .normal: + let checkboxRect = CGRect(origin: NSMakePoint(focus(checkboxSize).minX, frame.height - checkboxSize.height - 10), size: checkboxSize) + + transition.updateFrame(view: checkboxContainer, frame: checkboxRect) + transition.updateFrame(view: documentView, frame: .init(origin: .init(x: 0, y: frame.height - tableView.listHeight - 10), size: documentSize)) + + transition.updateFrame(view: colorPicker, frame: CGRect(origin: NSMakePoint(0, frame.height), size: colorPickerSize)) + + transition.updateFrame(view: patternsController.view, frame: .init(origin: NSMakePoint(0, frame.height), size: patternsSize)) + + transition.updateFrame(view: controlsBg, frame: .init(origin: NSMakePoint(0, frame.height), size: controlsSize)) + + } + + switch previewState { + case .color: + let pickerRect = CGRect(origin: .init(x: 0, y: frame.height - colorPickerSize.height), size: colorPickerSize) + transition.updateFrame(view: colorPicker, frame: pickerRect) + + transition.updateFrame(view: patternsController.view, frame: .init(origin: NSMakePoint(0, frame.height), size: patternsSize)) + + transition.updateFrame(view: controlsBg, frame: pickerRect) + case .pattern: + transition.updateFrame(view: colorPicker, frame: CGRect.init(origin: .init(x: 0, y: frame.height), size: colorPickerSize)) + + let patternsRect: CGRect = .init(origin: NSMakePoint(0, frame.height - patternsSize.height), size: patternsSize) + + transition.updateFrame(view: patternsController.view, frame: patternsRect) + transition.updateFrame(view: controlsBg, frame: patternsRect) + + default: + break + } + + patternsController.genericView.updateLayout(size: patternsSize, transition: transition) + colorPicker.updateLayout(size: colorPickerSize, transition: transition) + + + var x: CGFloat = 0 + for view in checkboxViews { + transition.updateFrame(view: view, frame: view.centerFrameY(x: x)) + x += view.frame.width + 10 + } + + } + + func updateModifyState(_ state: WallpaperPreviewState, animated: Bool) { + + self.previewState = state + switch state { + case .color: + patternCheckbox.isSelected = false + colorCheckbox.isSelected = true + updateBackground(wallpaper, image: self.image) + case .normal: + patternCheckbox.isSelected = false + colorCheckbox.isSelected = false + updateBackground(wallpaper, image: self.image) + case .pattern: + if let selected = patternsController.pattern { + self.wallpaper = selected.withUpdatedSettings(self.wallpaper.settings) + } + patternCheckbox.isSelected = true + colorCheckbox.isSelected = false + + updateBackground(wallpaper, image: self.image) + } + rotateColors.set(rotation: wallpaper.settings.rotation, animated: animated) + updateLayout(size: frame.size, transition: animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate) + } + + private func updateBackground(_ wallpaper: Wallpaper, image: CGImage?) { + switch wallpaper { + case .builtin: + backgroundView.backgroundMode = .plain + case let .color(color): + backgroundView.backgroundMode = .color(color: NSColor(UInt32(color))) + case let .gradient(_, colors, rotation): + backgroundView.backgroundMode = .gradient(colors: colors.map { NSColor(argb: $0) }, rotation: rotation) + case .image: + if let image = image { + backgroundView.backgroundMode = .background(image: NSImage(cgImage: image, size: image.size), intensity: nil, colors: nil, rotation: nil) + } else { + backgroundView.backgroundMode = .plain + } + case let .file(_, _, settings, isPattern): + if isPattern, settings.colors.count > 2 { + if let image = image { + backgroundView.backgroundMode = .background(image: NSImage(cgImage: image, size: image.size), intensity: settings.intensity, colors: settings.colors.map { NSColor(argb: $0) }, rotation: settings.rotation) + } else { + backgroundView.backgroundMode = .gradient(colors: settings.colors.map { NSColor(argb: $0) }, rotation: settings.rotation) + } + } else { + if let image = image { + backgroundView.backgroundMode = .background(image: NSImage(cgImage: image, size: image.size), intensity: settings.intensity, colors: settings.colors.map { NSColor(argb: $0) }, rotation: settings.rotation) + } else { + backgroundView.backgroundMode = .gradient(colors: settings.colors.map { NSColor(argb: $0) }, rotation: settings.rotation) + } + } + default: + backgroundView.backgroundMode = .plain + } + } + + private func loadImage(_ signal:Signal, palette: ColorPalette, boundingSize: NSSize) { + let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: boundingSize, boundingSize: boundingSize, intrinsicInsets: NSEdgeInsets()) + + + let intense = CGFloat(abs(wallpaper.settings.intensity ?? 0)) / 100.0 + let signal: Signal = signal |> map { result in + var image = result.execute(arguments, result.data)?.generateImage() + if palette.isDark, let img = image { + image = generateImage(img.size, contextGenerator: { size, ctx in + ctx.clear(size.bounds) + ctx.setFillColor(NSColor.black.cgColor) + ctx.fill(size.bounds) + ctx.clip(to: size.bounds, mask: img) + + ctx.clear(size.bounds) + + ctx.setFillColor(NSColor.black.withAlphaComponent(1 - intense).cgColor) + ctx.fill(size.bounds) + }) + } + return image + } |> deliverOnMainQueue + + loadImageDisposable.set(signal.start(next: { [weak self] image in + guard let strongSelf = self else { + return + } + strongSelf.image = image + strongSelf.updateBackground(strongSelf.wallpaper, image: image) + strongSelf.ready?() + })) + } + + func updateState(synchronousLoad: Bool) { + let maximumSize: NSSize = WallpaperDimensions + var updatedStatusSignal: Signal? + + switch wallpaper { + case let .color(color): + self.image = nil + blurCheckbox.isHidden = true + colorCheckbox.isHidden = false + patternCheckbox.isHidden = false + self.patternCheckbox.hasPattern = false + rotateColors.isHidden = true + self.colorCheckbox.colorsValue = [color].map { NSColor($0) } + case let .gradient(_, colors, _): + self.image = nil + blurCheckbox.isHidden = true + colorCheckbox.isHidden = false + patternCheckbox.isHidden = false + rotateColors.isHidden = false + self.patternCheckbox.hasPattern = false + self.colorCheckbox.colorsValue = colors.map { NSColor($0) } + self.rotateColors.update(colors.count > 2 ? theme.icons.wallpaper_color_play : theme.icons.wallpaper_color_rotate) + case let .image(representations, settings): + self.patternCheckbox.hasPattern = false + blurCheckbox.isHidden = false + colorCheckbox.isHidden = true + patternCheckbox.isHidden = true + rotateColors.isHidden = true + let dimensions = largestImageRepresentation(representations)!.dimensions.size + let boundingSize = dimensions.fitted(maximumSize) + self.imageSize = dimensions + + loadImage(chatWallpaper(account: context.account, representations: representations, mode: .screen, isPattern: false, autoFetchFullSize: true, scale: backingScaleFactor, isBlurred: settings.blur, synchronousLoad: synchronousLoad, drawPatternOnly: true), palette: theme.colors, boundingSize: boundingSize) + + + updatedStatusSignal = context.account.postbox.mediaBox.resourceStatus(largestImageRepresentation(representations)!.resource, approximateSynchronousValue: synchronousLoad) |> deliverOnMainQueue + + case let .file(_, file, settings, isPattern): + blurCheckbox.isHidden = isPattern + + colorCheckbox.isHidden = !isPattern + patternCheckbox.isHidden = !isPattern + rotateColors.isHidden = !isPattern + + if isPattern { + self.colorCheckbox.colorsValue = settings.colors.map { NSColor($0) } + } + + self.patternCheckbox.hasPattern = isPattern + + var representations:[TelegramMediaImageRepresentation] = [] + representations.append(contentsOf: file.previewRepresentations) + if let dimensions = file.dimensions { + representations.append(TelegramMediaImageRepresentation(dimensions: dimensions, resource: file.resource, progressiveSizes: [], immediateThumbnailData: nil)) + } else { + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(maximumSize), resource: file.resource, progressiveSizes: [], immediateThumbnailData: nil)) + } + + if isPattern { + self.rotateColors.update(settings.colors.count > 2 ? theme.icons.wallpaper_color_play : theme.icons.wallpaper_color_rotate) + } + + let dimensions = largestImageRepresentation(representations)!.dimensions.size + let boundingSize = dimensions.aspectFilled(frame.size) + + + loadImage(chatWallpaper(account: context.account, representations: representations, file: file, mode: .thumbnail, isPattern: isPattern, autoFetchFullSize: true, scale: backingScaleFactor, isBlurred: settings.blur, synchronousLoad: synchronousLoad, drawPatternOnly: true), palette: theme.colors, boundingSize: boundingSize) + + + self.imageSize = dimensions + + updatedStatusSignal = context.account.postbox.mediaBox.resourceStatus(largestImageRepresentation(representations)!.resource, approximateSynchronousValue: synchronousLoad) |> deliverOnMainQueue + default: + break + } + + updateBackground(self.wallpaper, image: self.image) + + + if let updatedStatusSignal = updatedStatusSignal { + disposable.set(updatedStatusSignal.start(next: { [weak self] status in + guard let `self` = self else { return } + switch status { + case let .Fetching(_, progress): + if self.progressView == nil { + self.progressView = RadialProgressView(theme: RadialProgressTheme(backgroundColor: .blackTransparent, foregroundColor: .white), twist: true, size: NSMakeSize(40, 40)) + self.addSubview(self.progressView!) + self.progressView?.center() + } + self.progressView?.state = .ImpossibleFetching(progress: progress, force: false) + break + case .Local: + if let progressView = self.progressView { + progressView.state = .ImpossibleFetching(progress:1.0, force: false) + self.progressView = nil + progressView.layer?.animateAlpha(from: 1, to: 0, duration: 0.25, timingFunction: .linear, removeOnCompletion: false, completion: { [weak progressView] completed in + if completed { + progressView?.removeFromSuperview() + } + }) + } + + case .Remote: + break + } + })) + } else { + progressView?.removeFromSuperview() + progressView = nil + } + + let serviceColor = self.serviceColor + + self.blurCheckbox.update(by: serviceColor) + self.colorCheckbox.update(by: serviceColor) + self.patternCheckbox.update(by: serviceColor) + self.rotateColors.update(by: serviceColor) + + needsLayout = true + } + + var serviceColor: NSColor { + switch wallpaper { + case .builtin, .file, .color, .gradient: + switch backgroundView.backgroundMode { + case let .background(image, _, colors, _): + if let colors = colors, let first = colors.first { + let blended = colors.reduce(first, { color, with in + return color.blended(withFraction: 0.5, of: with)! + }) + return getAverageColor(blended) + } else { + return getAverageColor(image) + } + case let .color(color): + return getAverageColor(color) + case let .gradient(colors, _): + if !colors.isEmpty { + let blended = colors.reduce(colors.first!, { color, with in + return color.blended(withFraction: 0.5, of: with)! + }) + return getAverageColor(blended) + } else { + return getAverageColor(theme.colors.chatBackground) + } + + case let .tiled(image): + return getAverageColor(image) + case .plain: + return getAverageColor(theme.colors.chatBackground) + } + default: + return getAverageColor(theme.colors.chatBackground) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } +} + +enum WallpaperSource { + case link(TelegramWallpaper) + case gallery(TelegramWallpaper) + case none +} + +private func cropWallpaperImage(_ image: CGImage, dimensions: NSSize, rect: NSRect, magnify: CGFloat, settings: WallpaperSettings?) -> CGImage { + let fittedSize = NSMakeSize(dimensions.width * magnify, dimensions.height * magnify)//WallpaperDimensions.aspectFitted(representation.dimensions) + + let image = generateImage(rect.size, contextGenerator: { size, ctx in + ctx.clear(NSMakeRect(0, 0, size.width, size.height)) + ctx.interpolationQuality = .high + ctx.setBlendMode(.normal) + let imageRect = NSMakeRect(-rect.minX, -rect.minY, fittedSize.width, fittedSize.height) + ctx.draw(image, in: imageRect) + }, opaque: false, scale: 1.0)! + + let fitted = WallpaperDimensions.aspectFitted(dimensions) + + return generateImage(fitted, contextGenerator: { size, ctx in + let imageRect = NSMakeRect(0, 0, fitted.width, fitted.height) + ctx.clear(imageRect) + if let settings = settings { + + var _patternColor: NSColor = NSColor(rgb: 0xd6e2ee, alpha: 0.5) + + var patternIntensity: CGFloat = 0.5 + if let color = settings.colors.first { + if let intensity = settings.intensity { + patternIntensity = CGFloat(intensity) / 100.0 + } + _patternColor = NSColor(rgb: color, alpha: patternIntensity) + } + + let color = _patternColor.withAlphaComponent(1.0) + let intensity = _patternColor.alpha + + ctx.setBlendMode(.copy) + ctx.setFillColor(color.cgColor) + ctx.fill(imageRect) + + ctx.setBlendMode(.normal) + ctx.interpolationQuality = .high + + ctx.clip(to: imageRect, mask: image) + ctx.setFillColor(patternColor(for: color, intensity: intensity).cgColor) + ctx.fill(imageRect) + } else { + ctx.draw(image, in: imageRect) + } + }, opaque: false, scale: 1.0)! + +} + +private func cropWallpaperIfNeeded(_ wallpaper: Wallpaper, account: Account, rect: NSRect, magnify: CGFloat = 1.0) -> Signal { + return Signal { subscriber in + + let disposable = MetaDisposable() + switch wallpaper { + case let .image(representations, _): + if let representation = largestImageRepresentation(representations), let resource = representation.resource as? LocalFileReferenceMediaResource { + if let image = NSImage(contentsOfFile: resource.localFilePath)?.cgImage(forProposedRect: nil, context: nil, hints: nil) { + + let fittedImage = cropWallpaperImage(image, dimensions: representation.dimensions.size, rect: rect, magnify: magnify, settings: nil) + + let options = NSMutableDictionary() + options.setValue(90 as NSNumber, forKey: kCGImageDestinationImageMaxPixelSize as String) + var result: [TelegramMediaImageRepresentation] = [] + let colorQuality: Float = 0.1 + options.setObject(colorQuality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString) + let mutableData: CFMutableData = NSMutableData() as CFMutableData + + if let colorDestination = CGImageDestinationCreateWithData(mutableData, kUTTypeJPEG, 1, nil) { + CGImageDestinationAddImage(colorDestination, fittedImage, options as CFDictionary) + if CGImageDestinationFinalize(colorDestination) { + let thumdResource = LocalFileMediaResource(fileId: arc4random64()) + account.postbox.mediaBox.storeResourceData(thumdResource.id, data: mutableData as Data) + result.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(fittedImage.backingSize.aspectFitted(NSMakeSize(90, 90))), resource: thumdResource, progressiveSizes: [], immediateThumbnailData: nil)) + } + } + + let fittedDimensions = WallpaperDimensions.aspectFitted(representation.dimensions.size) + + disposable.set(putToTemp(image: NSImage(cgImage: fittedImage, size: fittedDimensions), compress: false).start(next: { path in + copyToClipboard(path) + let resource = LocalFileReferenceMediaResource(localFilePath: path, randomId: arc4random64()) + result.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(fittedDimensions), resource: resource, progressiveSizes: [], immediateThumbnailData: nil)) + + let wallpaper: Wallpaper = .image(result, settings: wallpaper.settings) + subscriber.putNext(wallpaper) + subscriber.putCompletion() + })) + } + } + case let .file(slug, file, settings, isPattern): + + let dimensions = file.dimensions?.size ?? WallpaperDimensions + if isPattern { + + let path = account.postbox.mediaBox.cachedRepresentationCompletePath(file.resource.id, representation: CachedPatternWallpaperMaskRepresentation(size: nil)) + + if let image = NSImage(contentsOf: URL(fileURLWithPath: path)) { + let size = image.size.aspectFilled(WallpaperDimensions) + + let image = generateImage(size, contextGenerator: { size, ctx in + let imageRect = NSMakeRect(0, 0, size.width, size.height) + + let colors:[NSColor] + var intensity: CGFloat = 0.5 + + if settings.colors.count == 1 { + let combinedColor = NSColor(settings.colors.first!) + if let i = settings.intensity { + intensity = CGFloat(i) / 100.0 + } + intensity = combinedColor.alpha + colors = [combinedColor.withAlphaComponent(1.0)] + } else if settings.colors.count > 1 { + if let i = settings.intensity { + intensity = CGFloat(i) / 100.0 + } + colors = settings.colors.map { NSColor(argb: $0) }.reversed().map { $0.withAlphaComponent(1.0) } + } else { + colors = [NSColor(rgb: 0xd6e2ee, alpha: 0.5)] + } + + ctx.setBlendMode(.copy) + if colors.count == 1, let color = colors.first { + ctx.setFillColor(color.cgColor) + ctx.fill(imageRect) + } else { + let gradientColors = colors.map { $0.cgColor } as CFArray + let delta: CGFloat = 1.0 / (CGFloat(colors.count) - 1.0) + + var locations: [CGFloat] = [] + for i in 0 ..< colors.count { + locations.append(delta * CGFloat(i)) + } + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + + ctx.saveGState() + ctx.translateBy(x: imageRect.width / 2.0, y: imageRect.height / 2.0) + ctx.rotate(by: CGFloat(settings.rotation ?? 0) * CGFloat.pi / -180.0) + ctx.translateBy(x: -imageRect.width / 2.0, y: -imageRect.height / 2.0) + + ctx.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: imageRect.height), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]) + ctx.restoreGState() + } + + + ctx.setBlendMode(.normal) + ctx.interpolationQuality = .medium + ctx.clip(to: imageRect, mask: image.cgImage(forProposedRect: nil, context: nil, hints: nil)!) + + if colors.count == 1, let color = colors.first { + ctx.setFillColor(patternColor(for: color, intensity: intensity).cgColor) + ctx.fill(imageRect) + } else { + let gradientColors = colors.map { patternColor(for: $0, intensity: intensity).cgColor } as CFArray + let delta: CGFloat = 1.0 / (CGFloat(colors.count) - 1.0) + + var locations: [CGFloat] = [] + for i in 0 ..< colors.count { + locations.append(delta * CGFloat(i)) + } + let colorSpace = CGColorSpaceCreateDeviceRGB() + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + + ctx.translateBy(x: imageRect.width / 2.0, y: imageRect.height / 2.0) + ctx.rotate(by: CGFloat(settings.rotation ?? 0) * CGFloat.pi / -180.0) + ctx.translateBy(x: -imageRect.width / 2.0, y: -imageRect.height / 2.0) + + ctx.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: imageRect.height), options: [.drawsBeforeStartLocation, .drawsAfterEndLocation]) + } + + })! + + disposable.set(putToTemp(image: NSImage(cgImage: image, size: size), compress: false).start(next: { path in + let resource = LocalFileReferenceMediaResource(localFilePath: path, randomId: arc4random64()) + + var attributes = file.attributes + loop: for (i, attr) in attributes.enumerated() { + switch attr { + case .ImageSize: + attributes[i] = .ImageSize(size: PixelDimensions(size)) + break loop + default: + break + } + } + let wallpaper: Wallpaper = .file(slug: slug, file: file.withUpdatedResource(resource).withUpdatedAttributes(attributes), settings: settings, isPattern: isPattern) + subscriber.putNext(wallpaper) + subscriber.putCompletion() + })) + } + + subscriber.putNext(wallpaper.withUpdatedSettings(settings)) + subscriber.putCompletion() + } else { + if let path = account.postbox.mediaBox.completedResourcePath(file.resource), let image = NSImage(contentsOfFile: path)?.cgImage(forProposedRect: nil, context: nil, hints: nil) { + let fittedImage = cropWallpaperImage(image, dimensions: dimensions, rect: rect, magnify: magnify, settings: isPattern ? settings : nil) + + let options = NSMutableDictionary() + options.setValue(90 as NSNumber, forKey: kCGImageDestinationImageMaxPixelSize as String) + var result: [TelegramMediaImageRepresentation] = [] + let colorQuality: Float = 0.1 + options.setObject(colorQuality as NSNumber, forKey: kCGImageDestinationLossyCompressionQuality as NSString) + let mutableData: CFMutableData = NSMutableData() as CFMutableData + + if let colorDestination = CGImageDestinationCreateWithData(mutableData, kUTTypeJPEG, 1, nil) { + CGImageDestinationAddImage(colorDestination, fittedImage, options as CFDictionary) + if CGImageDestinationFinalize(colorDestination) { + let thumdResource = LocalFileMediaResource(fileId: arc4random64()) + account.postbox.mediaBox.storeResourceData(thumdResource.id, data: mutableData as Data) + result.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(fittedImage.backingSize.aspectFitted(NSMakeSize(90, 90))), resource: thumdResource, progressiveSizes: [], immediateThumbnailData: nil)) + } + } + + let fittedDimensions = WallpaperDimensions.aspectFitted(dimensions) + + disposable.set(putToTemp(image: NSImage(cgImage: fittedImage, size: fittedDimensions), compress: false).start(next: { path in + let resource = LocalFileReferenceMediaResource(localFilePath: path, randomId: arc4random64()) + + var attributes = file.attributes + loop: for (i, attr) in attributes.enumerated() { + switch attr { + case .ImageSize: + attributes[i] = .ImageSize(size: PixelDimensions(fittedDimensions)) + break loop + default: + break + } + } + + let wallpaper: Wallpaper = .file(slug: slug, file: file.withUpdatedPreviewRepresentations(result).withUpdatedResource(resource).withUpdatedAttributes(attributes), settings: settings, isPattern: isPattern) + subscriber.putNext(wallpaper) + subscriber.putCompletion() + })) + } + } + default: + subscriber.putNext(wallpaper) + subscriber.putCompletion() + } + + return ActionDisposable { + disposable.dispose() + } + } |> runOn(resourcesQueue) +} + + +class WallpaperPreviewController: ModalViewController { + + override func viewClass() -> AnyClass { + return WallpaperPreviewView.self + } + + override var handleAllEvents: Bool { + return false + } + + override func firstResponder() -> NSResponder? { + return genericView.colorPicker.colorEditor.textView.inputView + } + + private let wallpaper: Wallpaper + private let context: AccountContext + + let source: WallpaperSource + + init(_ context: AccountContext, wallpaper: Wallpaper, source: WallpaperSource) { + self.wallpaper = wallpaper.isSemanticallyEqual(to: theme.wallpaper.wallpaper) ? wallpaper.withUpdatedBlurrred(theme.wallpaper.wallpaper.isBlurred) : wallpaper + self.context = context + self.source = source + super.init(frame: NSMakeRect(0, 0, 380, 300)) + bar = .init(height: 0) + } + public override var modalHeader: (left: ModalHeaderData?, center: ModalHeaderData?, right: ModalHeaderData?)? { + let hasShare: Bool + switch self.wallpaper { + case .color, .gradient, .file: + hasShare = true + default: + hasShare = false + } + + return (left: ModalHeaderData.init(image: theme.icons.modalClose, handler: { [weak self] in + self?.close() + }), center: ModalHeaderData(title: L10n.wallpaperPreviewHeader), right: !hasShare ? nil : ModalHeaderData(image: theme.icons.modalShare, handler: { [weak self] in + self?.share() + })) + } + + private func share() { + //close() + + switch genericView.wallpaper { + case let .file(slug, _, settings, isPattern): + var options: [String] = [] + if settings.blur { + options.append("mode=blur") + } + + if isPattern { + + + + + if !settings.colors.isEmpty { + let colors:[String] = settings.colors.map { value in + let color = NSColor(argb: value).hexString.lowercased() + return String(color[color.index(after: color.startIndex) ..< color.endIndex]) + } + let bg = "bg_color=\(colors.joined(separator: "~"))" + options.append(bg) + } + if let intensity = settings.intensity { + options.append("intensity=\(intensity)") + } else { + options.append("intensity=\(50)") + } + if let r = settings.rotation { + options.append("rotation=\(r)") + } + } + + var optionsString = "" + if !options.isEmpty { + optionsString = "?\(options.joined(separator: "&"))" + } + + showModal(with: ShareModalController(ShareLinkObject(context, link: "https://t.me/bg/\(slug)\(optionsString)")), for: context.window) + case let .color(color): + var color = NSColor(argb: color).hexString.lowercased() + color = String(color[color.index(after: color.startIndex) ..< color.endIndex]) + showModal(with: ShareModalController(ShareLinkObject(context, link: "https://t.me/bg/\(color)")), for: context.window) + case let .gradient(_, colors, r): + + let colors:[String] = colors.map { value in + let color = NSColor(argb: value).hexString.lowercased() + return String(color[color.index(after: color.startIndex) ..< color.endIndex]) + } + + var rotation: String = "" + if let r = r { + rotation = "&rotation=\(r)" + } + + let t = colors.joined(separator: "~") + + showModal(with: ShareModalController(ShareLinkObject(context, link: "https://t.me/bg/\(t)" + rotation)), for: context.window) + + + default: + break + } + + } + + override func viewDidLoad() { + super.viewDidLoad() + + genericView.ready = { [weak self] in + self?.readyOnce() + } + + genericView.updateState(synchronousLoad: true) + + + genericView.blurCheckbox.isSelected = wallpaper.isBlurred + + switch wallpaper { + case let .color(color): + genericView.patternsController.colors = ([NSColor(argb: color)], nil) + case let .gradient(_, colors, rotation): + genericView.patternsController.colors = (colors.map { NSColor(argb: $0) }, rotation) + case let .file(_, _, settings, isPattern): + if isPattern { + var colors:[NSColor] = settings.colors.map { NSColor(argb: $0) } + if colors.isEmpty { + colors.append(NSColor(rgb: 0xd6e2ee, alpha: 0.5)) + } + genericView.patternsController.colors = (colors, settings.rotation) + } + default: + break + } + + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + window?.removeAllHandlers(for: self) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + } + + + private func applyAndClose() { + let context = self.context + closeAllModals() + + let signal = cropWallpaperIfNeeded(genericView.wallpaper, account: context.account, rect: genericView.croppedRect) |> mapToSignal { wallpaper in + return moveWallpaperToCache(postbox: context.account.postbox, wallpaper: wallpaper) + } + + _ = showModalProgress(signal: signal, for: context.window).start(next: { wallpaper in + _ = (updateThemeInteractivetly(accountManager: context.sharedContext.accountManager, f: { settings in + return settings.updateWallpaper { $0.withUpdatedWallpaper(wallpaper) }.saveDefaultWallpaper().withSavedAssociatedTheme().withUpdatedBubbled(true) + }) |> deliverOnMainQueue).start(completed: { + var stats:[Signal] = [] + switch self.source { + case let .gallery(wallpaper): + stats = [installWallpaper(account: context.account, wallpaper: wallpaper)] + case let .link(wallpaper): + stats = [installWallpaper(account: context.account, wallpaper: wallpaper), saveWallpaper(account: context.account, wallpaper: wallpaper)] + case .none: + break + } + let _ = combineLatest(stats).start() + }) + }) + + } + + override var modalInteractions: ModalInteractions? { + return ModalInteractions(acceptTitle: L10n.wallpaperPreviewApply, accept: { [weak self] in + self?.applyAndClose() + }, drawBorder: true, height: 50, singleButton: true) + } + override func initializer() -> NSView { + return WallpaperPreviewView(frame: NSMakeRect(_frameRect.minX, _frameRect.minY, _frameRect.width, _frameRect.height - bar.height), context: context, wallpaper: wallpaper); + } + + override var dynamicSize: Bool { + return true + } + + override func measure(size: NSSize) { + let chatSize = NSMakeSize(context.sharedContext.bindings.rootNavigation().frame.width, min(500, size.height - 150)) + let contentSize = WallpaperDimensions.aspectFitted(chatSize) + + self.modal?.resize(with: contentSize, animated: false) + } + + func updateSize(_ animated: Bool) { + if let contentSize = self.modal?.window.contentView?.frame.size { + self.modal?.resize(with:NSMakeSize(genericView.frame.width, contentSize.height - 150), animated: animated) + } + } + + private var genericView: WallpaperPreviewView { + return self.view as! WallpaperPreviewView + } + +} diff --git a/Telegram-Mac/Wallpapers.swift b/Telegram-Mac/Wallpapers.swift new file mode 100644 index 0000000000..98dd0f9304 --- /dev/null +++ b/Telegram-Mac/Wallpapers.swift @@ -0,0 +1,147 @@ +// +// Wallpapers.swift +// Telegram +// +// Created by Mikhail Filimonov on 11/01/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import Postbox +import TelegramCore + +import SwiftSignalKit +// +//public enum TelegramWallpaper: OrderedItemListEntryContents, Equatable { +// case none +// case builtin +// case color(Int32) +// case image([TelegramMediaImageRepresentation]) +// case custom(String) +// public init(decoder: PostboxDecoder) { +// switch decoder.decodeInt32ForKey("v", orElse: 0) { +// case 0: +// self = .builtin +// case 1: +// self = .color(decoder.decodeInt32ForKey("c", orElse: 0)) +// case 2: +// self = .image(decoder.decodeObjectArrayWithDecoderForKey("i")) +// case 3: +// self = .none +// case 4: +// self = .custom(decoder.decodeStringForKey("p", orElse: "")) +// default: +// assertionFailure() +// self = .none +// } +// } +// +// var hasWallpaper: Bool { +// switch self { +// case .none: +// return false +// case .color: +// return false +// default: +// return true +// } +// } +// +// public func encode(_ encoder: PostboxEncoder) { +// switch self { +// case .builtin: +// encoder.encodeInt32(0, forKey: "v") +// case let .color(color): +// encoder.encodeInt32(1, forKey: "v") +// encoder.encodeInt32(color, forKey: "c") +// case let .image(representations): +// encoder.encodeInt32(2, forKey: "v") +// encoder.encodeObjectArray(representations, forKey: "i") +// case .none: +// encoder.encodeInt32(3, forKey: "v") +// case let .custom(path): +// encoder.encodeInt32(4, forKey: "v") +// encoder.encodeString(path, forKey: "p") +// } +// } +// +// public static func ==(lhs: TelegramWallpaper, rhs: TelegramWallpaper) -> Bool { +// switch lhs { +// case .builtin: +// if case .builtin = rhs { +// return true +// } else { +// return false +// } +// case .none: +// if case .none = rhs { +// return true +// } else { +// return false +// } +// case let .color(color): +// if case .color(color) = rhs { +// return true +// } else { +// return false +// } +// case let .custom(path): +// if case .custom(path) = rhs { +// return true +// } else { +// return false +// } +// case let .image(lhsRepresentations): +// if case let .image(rhsRepresentations) = rhs, lhsRepresentations == rhsRepresentations { +// return true +// } else { +// return false +// } +// } +// } +//} +// +//func telegramWallpapers(account: Account) -> Signal<[TelegramWallpaper], NoError> { +// return account.postbox.transaction { transaction -> [TelegramWallpaper] in +// let items = transaction.getOrderedListItems(collectionId: Namespaces.OrderedItemList.CloudWallpapers) +// if items.count == 0 { +// return [.none, .builtin] +// } else { +// return items.map { $0.contents as! TelegramWallpaper } +// } +// } |> mapToSignal { list -> Signal<[TelegramWallpaper], NoError> in +// let remote = account.network.request(Api.functions.account.getWallPapers()) +// |> retryRequest +// |> mapToSignal { result -> Signal<[TelegramWallpaper], NoError> in +// var items: [TelegramWallpaper] = [] +// for item in result { +// switch item { +// case let .wallPaper(_, _, sizes, color): +// items.append(.image(telegramMediaImageRepresentationsFromApiSizes(sizes))) +// case let .wallPaperSolid(_, _, bgColor, color): +// items.append(.color(bgColor)) +// } +// } +// items.removeFirst() +// items.insert(.none, at: 0) +// items.insert(.builtin, at: 1) +// +// if items == list { +// return .complete() +// } else { +// return account.postbox.transaction { transaction -> [TelegramWallpaper] in +// var entries: [OrderedItemListEntry] = [] +// for item in items { +// var intValue = Int32(entries.count) +// let id = MemoryBuffer(data: Data(bytes: &intValue, count: 4)) +// entries.append(OrderedItemListEntry(id: id, contents: item)) +// } +// transaction.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.CloudWallpapers, items: entries) +// +// return items +// } +// } +// } +// return .single(list) |> then(remote) +// } +//} diff --git a/Telegram-Mac/WaveView.swift b/Telegram-Mac/WaveView.swift new file mode 100644 index 0000000000..202f990b00 --- /dev/null +++ b/Telegram-Mac/WaveView.swift @@ -0,0 +1,512 @@ +// +// WaveView.swift +// Telegram +// +// Created by Mikhail Filimonov on 13.11.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import SwiftSignalKit + +private struct Constants { + static let sineWaveSpeed: CGFloat = 0.81 + static let smallWaveRadius: CGFloat = 0.55 + static let smallWaveScale: CGFloat = 0.40 + static let smallWaveScaleSpeed: CGFloat = 0.6 + static let flingDistance: CGFloat = 0.5 + + static let circleRadius: CGFloat = 56.0 + + static let animationSpeed: CGFloat = 0.35 * 0.1 + static let animationSpeedSmall: CGFloat = 0.55 * 0.1 + + static let rotationSpeed: CGFloat = 0.36 * 0.1 + static let waveAngle: CGFloat = 0.03 + static let randomRadiusSize: CGFloat = 0.3 + + static let idleWaveAngle: CGFloat = 0.5 + static let idleScaleSpeed: CGFloat = 0.3 + static let idleRotationSpeed: CGFloat = 0.2 + static let idleRadiusValue: CGFloat = 0.56 + static let idleRotationDiff: CGFloat = 0.1 * idleRotationSpeed +} + +class CombinedWaveView: View { + private let bigWaveView: WaveView + private let smallWaveView: WaveView + + private var level: CGFloat = 0.0 + + init(frame: CGRect, color: NSColor) { + let n = 12 + let bounds = CGRect(origin: CGPoint(), size: frame.size) + self.bigWaveView = WaveView(frame: bounds, n: n, amplitudeRadius: 30.0, isBig: true, color: color.withAlphaComponent(0.3)) + self.smallWaveView = WaveView(frame: bounds, n: n, amplitudeRadius: 35.0, isBig: false, color: color.withAlphaComponent(0.15)) + + super.init(frame: frame) + + self.bigWaveView.rotation = CGFloat.pi / 6.0 + self.bigWaveView.amplitudeWaveDif = 0.02 * Constants.sineWaveSpeed * CGFloat.pi / 180.0 + + self.smallWaveView.amplitudeWaveDif = 0.026 * Constants.sineWaveSpeed + self.smallWaveView.amplitudeRadius = 10.0 + 20.0 * Constants.smallWaveRadius + self.smallWaveView.maxScale = 0.3 * Constants.smallWaveScale + self.smallWaveView.scaleSpeed = 0.001 * Constants.smallWaveScaleSpeed + self.smallWaveView.fling = Constants.flingDistance + + self.addSubview(self.bigWaveView) + self.addSubview(self.smallWaveView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + func updateLevel(_ level: CGFloat) { + let level = level * 0.2 + self.level = level + self.bigWaveView.setLevel(level) + self.smallWaveView.setLevel(level) + } + + func tick(_ level: CGFloat) { + let radius = 56.0 + 30.0 * level * 0.2 + self.bigWaveView.tick(circleRadius: radius) + self.smallWaveView.tick(circleRadius: radius) + } + + func setColor(_ color: NSColor) { + } +} + +class WaveView : View { + var fling: CGFloat = 0.0 + private var animateToAmplitude: CGFloat = 0.0 + private var amplitude: CGFloat = 0.0 + private var slowAmplitude: CGFloat = 0.0 + private var animateAmplitudeDiff: CGFloat = 0.0 + private var animateAmplitudeSlowDiff: CGFloat = 0.0 + + private var lastRadius: CGFloat = 0.0 + private var radiusDiff: CGFloat = 0.0 + private var waveDiff: CGFloat = 0.0 + private var waveAngle: CGFloat = 0.0 + + private var incRandomAdditionals = false + + var rotation: CGFloat = 0.0 + private var idleRotation: CGFloat = 0.0 + private var innerRotation: CGFloat = 0.0 + + var amplitudeWaveDif: CGFloat = 0.0 + + var amplitudeRadius: CGFloat + private let isBig: Bool + + private var idleRadius: CGFloat = 0.0 + private var idleRadiusK: CGFloat = 0.15 * Constants.idleWaveAngle + private var expandIdleRadius = false + private var expandScale = false + + private var isIdle = true + private var scale: CGFloat = 1.0 + private var scaleIdleDif: CGFloat = 0.0 + private var scaleDif: CGFloat = 0.0 + var scaleSpeed: CGFloat = 0.00008 + public var scaleSpeedIdle: CGFloat = 0.0002 * Constants.idleScaleSpeed + var maxScale: CGFloat = 0.0 + + private var flingRadius: CGFloat = 0.0 + + private let randomAdditions: CGFloat = 8.0 * Constants.randomRadiusSize + + private var idleGlobalRadius: CGFloat = 10.0 * Constants.idleRadiusValue + private var sineAngleMax: CGFloat = 0.0 + + private let n: Int + private let l: CGFloat + private var additions: [CGFloat] + + var idleStateDiff: CGFloat = 0.0 + var radius: CGFloat = 60.0; + var cubicBezierK: CGFloat = 1.0; + + var randomK: CGFloat = 0.0 + + var color: NSColor + + init(frame: CGRect, n: Int, amplitudeRadius: CGFloat, isBig: Bool, color: NSColor) { + self.n = n + self.amplitudeRadius = amplitudeRadius + self.isBig = isBig + self.color = color + + self.expandIdleRadius = isBig + self.radiusDiff = 34.0 * 0.0012 + + self.l = 4.0 / 3.0 * tan(CGFloat.pi / (2.0 * CGFloat(self.n))) + self.additions = Array(repeating: 0.0, count: self.n) + + super.init(frame: frame) + + self.backgroundColor = .clear + + self.updateAdditions() + } + + func setLevel(_ level: CGFloat) { + self.animateToAmplitude = level + + let amplitudeDelta: CGFloat + let amplitudeSlowDelta: CGFloat + if self.isBig { + if self.animateToAmplitude > self.amplitude { + amplitudeDelta = 300.0 * Constants.animationSpeed + amplitudeSlowDelta = 500.0 * Constants.animationSpeed + } else { + amplitudeDelta = 500.0 * Constants.animationSpeed + amplitudeSlowDelta = 500.0 * Constants.animationSpeed + } + } else { + if self.animateToAmplitude > self.amplitude { + amplitudeDelta = 400.0 * Constants.animationSpeedSmall + amplitudeSlowDelta = 500.0 * Constants.animationSpeedSmall + } else { + amplitudeDelta = 500.0 * Constants.animationSpeedSmall + amplitudeSlowDelta = 500.0 * Constants.animationSpeedSmall + } + } + + self.animateAmplitudeDiff = (self.animateToAmplitude - self.amplitude) / (100.0 + amplitudeDelta) + self.animateAmplitudeSlowDiff = (self.animateToAmplitude - self.slowAmplitude) / (100.0 + amplitudeSlowDelta) + + let isIdle = level < 0.1 + if self.isIdle != isIdle && isIdle && self.isBig { + // + // + // + } + + self.isIdle = isIdle + } + + private var wasFling = false + + private var timer1: SwiftSignalKit.Timer? = nil + private var timer2: SwiftSignalKit.Timer? = nil + + private func startFling(delta: CGFloat) { + + timer1?.invalidate() + timer2?.invalidate() + + var runSecondAnimation:(()->Void)? + + let fling = self.fling * 2.0 + let flingDistance = delta * self.amplitudeRadius * (self.isBig ? 8.0 : 20.0) * 16.0 * fling + + let duration1 = CGFloat((self.isBig ? 0.2 : 0.35) * fling) + let fromValue1 = self.flingRadius + let toValue1 = flingDistance + let tickCountValue1 = duration1 / 0.016 + let tickValue1 = (toValue1 - fromValue1) / tickCountValue1 + + let toIsBigger = toValue1 >= fromValue1 + + timer1 = SwiftSignalKit.Timer(timeout: 0.016, repeat: true, completion: { [weak self] in + guard let `self` = self else { + return + } + let completion:()->Void = { [weak self] in + self?.timer1?.invalidate() + runSecondAnimation?() + } + self.flingRadius += tickValue1 + if toIsBigger { + if self.flingRadius >= toValue1 { + completion() + } + } else { + if self.flingRadius <= toValue1 { + completion() + } + } + }, queue: .mainQueue()) + + timer1?.start() + + + runSecondAnimation = { [weak self] in + guard let `self` = self else { + return + } + let duration2 = CGFloat((self.isBig ? 0.22 : 0.38) * fling) + let fromValue2 = flingDistance + let toValue2: CGFloat = 0 + let tickCountValue2 = duration2 / 0.016 + let tickValue2 = fromValue2 / tickCountValue2 + + self.timer2 = SwiftSignalKit.Timer(timeout: 0.016, repeat: true, completion: { [weak self] in + guard let `self` = self else { + return + } + let completion:()->Void = { [weak self] in + self?.timer2?.invalidate() + } + self.flingRadius -= tickValue2 + if self.flingRadius <= toValue2 { + completion() + } + }, queue: .mainQueue()) + + self.timer2?.start() + } + } + + private var lastUpdateTime: CGFloat? + func tick(circleRadius: CGFloat) { + let dt: CGFloat + let time = CGFloat(CACurrentMediaTime()) + if let lastUpdateTime = self.lastUpdateTime { + dt = (time - lastUpdateTime) * 1000.0 + } else { + dt = 0.0 + } + self.lastUpdateTime = time + + if self.animateToAmplitude != self.amplitude { + self.amplitude += self.animateAmplitudeDiff * dt + if self.animateAmplitudeDiff > 0.0 { + if self.amplitude > self.animateToAmplitude { + self.amplitude = self.animateToAmplitude + } + } else { + if self.amplitude < self.animateToAmplitude { + self.amplitude = self.animateToAmplitude + } + } + + if abs(self.amplitude - self.animateToAmplitude) * self.amplitudeRadius < 4.0 { + if !self.wasFling { + self.startFling(delta: self.animateAmplitudeDiff) + self.wasFling = true + } + } else { + self.wasFling = false + } + } + + if self.animateToAmplitude != self.slowAmplitude { + self.slowAmplitude += self.animateAmplitudeSlowDiff * dt + if abs(self.slowAmplitude - self.amplitude) > 0.2 { + self.slowAmplitude = self.amplitudeRadius + (self.slowAmplitude > self.amplitude ? 0.2 : -0.2) + } + if self.animateAmplitudeSlowDiff > 0.0 { + if self.slowAmplitude > self.animateToAmplitude { + self.slowAmplitude = self.animateToAmplitude + } + } else { + if self.slowAmplitude < self.animateToAmplitude { + self.slowAmplitude = self.animateToAmplitude + } + } + } + + self.idleRadius = circleRadius * self.idleRadiusK + if self.expandIdleRadius { + self.scaleIdleDif += self.scaleSpeedIdle * dt + if self.scaleIdleDif >= 0.05 { + self.scaleIdleDif = 0.05 + self.expandIdleRadius = false + } + } else { + self.scaleIdleDif -= self.scaleSpeedIdle * dt + if self.scaleIdleDif < 0.0 { + self.scaleIdleDif = 0.0 + self.expandIdleRadius = true + } + } + + if self.maxScale > 0.0 { + if self.expandScale { + self.scaleDif += self.scaleSpeed * dt + if self.scaleDif >= self.maxScale { + self.scaleDif = self.maxScale + self.expandScale = false + } + } else { + self.scaleDif -= self.scaleSpeed * dt + if self.scaleDif < 0.0 { + self.scaleDif = 0.0 + self.expandScale = true + } + } + } + + if self.sineAngleMax > self.animateToAmplitude { + self.sineAngleMax -= 0.25 + if self.sineAngleMax < self.animateToAmplitude { + self.sineAngleMax = self.animateToAmplitude + } + } else if self.sineAngleMax < self.animateToAmplitude { + self.sineAngleMax += 0.25 + if self.sineAngleMax > self.animateToAmplitude { + self.sineAngleMax = self.animateToAmplitude + } + } + + if !self.isIdle { + self.rotation += (Constants.rotationSpeed * 0.5 + Constants.rotationSpeed * 4.0 * (self.amplitude > 0.5 ? 1.0 : self.amplitude / 0.5) * dt) * CGFloat.pi / 180.0 + while self.rotation > CGFloat.pi * 2.0 { + self.rotation -= CGFloat.pi * 2.0 + } + } else { + self.idleRotation += Constants.idleRotationDiff * dt * CGFloat.pi / 180.0 + while self.idleRotation > CGFloat.pi * 2.0 { + self.idleRotation -= CGFloat.pi * 2.0 + } + } + + if self.lastRadius < circleRadius { + self.lastRadius = circleRadius + } else { + self.lastRadius -= self.radiusDiff * dt + if self.lastRadius < circleRadius { + self.lastRadius = circleRadius + } + } + + self.lastRadius = circleRadius + + if !self.isIdle { + self.waveAngle += self.amplitudeWaveDif * self.sineAngleMax * dt + if self.isBig { + self.waveDiff = cos(self.waveAngle) + } else { + self.waveDiff = -cos(self.waveAngle) + } + + if self.waveDiff > 0.0 && self.incRandomAdditionals { + self.updateAdditions() + self.incRandomAdditionals = false + } else if self.waveDiff < 0.0 && !self.incRandomAdditionals { + self.updateAdditions() + self.incRandomAdditionals = true + } + } + + self.prepareDraw() + } + + func updateAdditions() { + self.additions = (0..= self.n { + j = 0 + } + + r = ((j % 2 == 0) ? r1 : r2) + self.randomK * self.additions[j] + + var p3 = CGPoint(x: cx, y: cy - r) + var p4 = CGPoint(x: cx - l + self.randomK * self.additions[j] * self.l, y: cy - r) + + transform = CGAffineTransform.init(translationX: cx, y: cy) + transform = transform.rotated(by: 2 * CGFloat.pi / CGFloat(self.n) * CGFloat(j)) + transform = transform.translatedBy(x: -cx, y: -cy) + + p3 = p3.applying(transform) + p4 = p4.applying(transform) + + if i == 0 { + path.move(to: p1) + } + path.addCurve(to: p3, control1: p2, control2: p4) + } + + ctx.setFillColor(self.color.cgColor) + + ctx.saveGState() + ctx.translateBy(x: rect.width / 2.0, y: rect.height / 2.0) + ctx.scaleBy(x: self.scale, y: self.scale) + ctx.rotate(by: self.innerRotation) + ctx.translateBy(x: -rect.width / 2.0, y: -rect.height / 2.0) + + ctx.addPath(path) + ctx.drawPath(using: .fill) + ctx.restoreGState() + } + +} diff --git a/Telegram-Mac/WebAuthorizationRowItem.swift b/Telegram-Mac/WebAuthorizationRowItem.swift new file mode 100644 index 0000000000..ecb0d8b0d7 --- /dev/null +++ b/Telegram-Mac/WebAuthorizationRowItem.swift @@ -0,0 +1,178 @@ +// +// WebAuthorizationRowItem.swift +// Telegram +// +// Created by Mikhail Filimonov on 12/03/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TGUIKit +import TelegramCore + +import Postbox + +class WebAuthorizationRowItem: GeneralRowItem { + + fileprivate let account: Account + fileprivate let nameLayout: TextViewLayout + fileprivate let photo: AvatarNodeState + fileprivate let statusLayout: TextViewLayout + fileprivate let dateLayout: TextViewLayout + fileprivate let logoutInteraction:()->Void + init(_ initialSize: NSSize, stableId: AnyHashable, account: Account, authorization: WebAuthorization, peer: Peer, viewType: GeneralViewType, logout:@escaping()->Void) { + self.logoutInteraction = logout + self.account = account + self.photo = .PeerAvatar(peer, peer.displayLetters, peer.smallProfileImage, nil, nil) + self.nameLayout = TextViewLayout(.initialize(string: peer.displayTitle, color: theme.colors.text, font: .medium(.title)), maximumNumberOfLines: 1) + let statusAttr = NSMutableAttributedString() + + _ = statusAttr.append(string: authorization.domain, color: theme.colors.text, font: .normal(.text)) + _ = statusAttr.append(string: ", ", color: theme.colors.grayText) + _ = statusAttr.append(string: authorization.browser, color: theme.colors.text, font: .normal(.text)) + _ = statusAttr.append(string: ", ", color: theme.colors.grayText) + _ = statusAttr.append(string: authorization.platform, color: theme.colors.text, font: .normal(.text)) + + _ = statusAttr.append(string: "\n") + + _ = statusAttr.append(string: authorization.ip, color: theme.colors.grayText, font: .normal(.text)) + _ = statusAttr.append(string: " ● ", color: theme.colors.grayText) + _ = statusAttr.append(string: authorization.region, color: theme.colors.grayText, font: .normal(.text)) + + self.statusLayout = TextViewLayout(statusAttr, maximumNumberOfLines: 2) + self.dateLayout = TextViewLayout(.initialize(string: DateUtils.string(forMessageListDate: authorization.dateActive), color: theme.colors.grayText, font: .normal(.text))) + super.init(initialSize, height: 80, stableId: stableId, viewType: viewType, inset: NSEdgeInsetsMake(0, 30, 0, 30)) + _ = makeSize(initialSize.width, oldWidth: 0) + } + + override func makeSize(_ width: CGFloat, oldWidth: CGFloat) -> Bool { + let success = super.makeSize(width, oldWidth: oldWidth) + dateLayout.measure(width: .greatestFiniteMagnitude) + nameLayout.measure(width: width - (inset.left + inset.right) - 20 - dateLayout.layoutSize.width) + statusLayout.measure(width: width - (inset.left + inset.right)) + + return success + } + + override func viewClass() -> AnyClass { + return WebAuthorizationRowView.self + } + +} + + +private class WebAuthorizationRowView : TableRowView, ViewDisplayDelegate { + private let containerView = GeneralRowContainerView(frame: NSZeroRect) + private let botNameView: TextView = TextView() + private let statusTextView: TextView = TextView() + private let dateView: TextView = TextView() + private let photoView: AvatarControl = AvatarControl(font: .avatar(8)) + private let logoutButton: TitleButton = TitleButton() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + botNameView.isSelectable = false + botNameView.userInteractionEnabled = false + containerView.addSubview(botNameView) + containerView.addSubview(statusTextView) + containerView.addSubview(dateView) + containerView.addSubview(photoView) + containerView.addSubview(logoutButton) + photoView.setFrameSize(16, 16) + + + addSubview(containerView) + + containerView.displayDelegate = self + + logoutButton.set(handler: { [weak self] _ in + guard let item = self?.item as? WebAuthorizationRowItem else {return} + item.logoutInteraction() + }, for: .Click) + } + + override func layout() { + super.layout() + guard let item = item as? WebAuthorizationRowItem else {return} + + switch item.viewType { + case .legacy: + self.containerView.frame = bounds + self.containerView.setCorners([]) + photoView.setFrameOrigin(item.inset.left, item.inset.top + 2) + botNameView.setFrameOrigin(photoView.frame.maxX + 4, item.inset.top) + statusTextView.setFrameOrigin(item.inset.left, botNameView.frame.maxY + 4) + dateView.setFrameOrigin(self.containerView.frame.width - item.inset.right - dateView.frame.width, item.inset.top) + logoutButton.setFrameOrigin(self.containerView.frame.width - logoutButton.frame.width - 25, self.containerView.frame.height - logoutButton.frame.height - 10) + case let .modern(position, innerInsets): + self.containerView.frame = NSMakeRect(floorToScreenPixels(backingScaleFactor, (frame.width - item.blockWidth) / 2), item.inset.top, item.blockWidth, frame.height - item.inset.bottom - item.inset.top) + self.containerView.setCorners(position.corners) + photoView.setFrameOrigin(innerInsets.left, innerInsets.top + 2) + botNameView.setFrameOrigin(photoView.frame.maxX + 4, innerInsets.top) + statusTextView.setFrameOrigin(innerInsets.left, botNameView.frame.maxY + 8) + dateView.setFrameOrigin(self.containerView.frame.width - innerInsets.right - dateView.frame.width, innerInsets.top) + logoutButton.setFrameOrigin(self.containerView.frame.width - logoutButton.frame.width - innerInsets.right + 4, self.containerView.frame.height - logoutButton.frame.height - 10) + } + + + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + super.draw(layer, in: ctx) + guard let item = item as? WebAuthorizationRowItem, layer == containerView.layer else {return} + + ctx.setFillColor(theme.colors.border.cgColor) + + switch item.viewType { + case .legacy: + ctx.fill(NSMakeRect(item.inset.left, frame.height - .borderSize, frame.width - item.inset.left, .borderSize)) + case let .modern(position, insets): + if position.border { + ctx.fill(NSMakeRect(insets.left, containerView.frame.height - .borderSize, containerView.frame.width - insets.left, .borderSize)) + } + } + + } + + override func updateColors() { + guard let item = item as? WebAuthorizationRowItem else {return} + logoutButton.set(background: backdorColor, for: .Normal) + botNameView.backgroundColor = backdorColor + statusTextView.backgroundColor = backdorColor + dateView.backgroundColor = backdorColor + containerView.backgroundColor = backdorColor + self.background = item.viewType.rowBackground + } + + override var backdorColor: NSColor { + return theme.colors.background + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func set(item: TableRowItem, animated: Bool) { + super.set(item: item, animated: animated) + + guard let item = item as? WebAuthorizationRowItem else {return} + + switch item.viewType { + case .legacy: + containerView.setCorners([], animated: animated) + case let .modern(position, _): + containerView.setCorners(position.corners, animated: animated) + } + + self.photoView.setState(account: item.account, state: item.photo) + self.botNameView.update(item.nameLayout) + self.statusTextView.update(item.statusLayout) + self.dateView.update(item.dateLayout) + + logoutButton.set(color: theme.colors.accent, for: .Normal) + logoutButton.set(font: .medium(.text), for: .Normal) + logoutButton.set(text: L10n.webAuthorizationsLogout, for: .Normal) + _ = logoutButton.sizeToFit() + + needsLayout = true + } +} diff --git a/Telegram-Mac/WebGameViewController.swift b/Telegram-Mac/WebGameViewController.swift index 151c245279..26c3f147c1 100644 --- a/Telegram-Mac/WebGameViewController.swift +++ b/Telegram-Mac/WebGameViewController.swift @@ -9,9 +9,26 @@ import Cocoa import WebKit import TGUIKit -import SwiftSignalKitMac -import TelegramCoreMac -import PostboxMac +import SwiftSignalKit +import TelegramCore + +import Postbox + +private class WeakGameScriptMessageHandler: NSObject, WKScriptMessageHandler { + private let f: (WKScriptMessage) -> () + + init(_ f: @escaping (WKScriptMessage) -> ()) { + self.f = f + + super.init() + } + + func userContentController(_ controller: WKUserContentController, didReceive scriptMessage: WKScriptMessage) { + self.f(scriptMessage) + } +} + + fileprivate var weakGames:[WeakReference] = [] @@ -24,7 +41,7 @@ fileprivate func game(forKey:String) -> WebGameViewController? { return nil } -class WebGameViewController: TelegramGenericViewController, WebFrameLoadDelegate { +class WebGameViewController: TelegramGenericViewController, WKUIDelegate { private let gameUrl:String private let peerId:PeerId @@ -34,11 +51,11 @@ class WebGameViewController: TelegramGenericViewController, WebFrameLoa private let messageId:MessageId fileprivate let uniqueId:String = "_\(arc4random())" private let loadMessageDisposable = MetaDisposable() - init(_ account:Account, _ peerId:PeerId, _ messageId:MessageId, _ gameUrl:String) { + init(_ context: AccountContext, _ peerId:PeerId, _ messageId:MessageId, _ gameUrl:String) { self.gameUrl = gameUrl self.peerId = peerId self.messageId = messageId - super.init(account) + super.init(context) weakGames.append(WeakReference(value: self)) } @@ -52,9 +69,9 @@ class WebGameViewController: TelegramGenericViewController, WebFrameLoa override func getRightBarViewOnce() -> BarView { let view = ImageBarView(controller: self, theme.icons.webgameShare) - let account = self.account - view.button.set(handler: {_ in - showModal(with: ShareModalController(ShareLinkObject(account, link: "https://t.me/gamebot")), for: mainWindow) + + view.button.set(handler: { [weak self] _ in + self?.share_game("") }, for: .Click) view.set(image: theme.icons.webgameShare, highlightImage: nil) return view @@ -62,13 +79,14 @@ class WebGameViewController: TelegramGenericViewController, WebFrameLoa override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) - genericView.mainFrame.load(URLRequest(url: URL(string:"file://blank")!)) - genericView.mainFrame.stopLoading() + genericView.load(URLRequest(url: URL(string:"file://blank")!)) + genericView.stopLoading() } override func viewDidLoad() { super.viewDidLoad() - loadMessageDisposable.set((account.postbox.messageAtId(messageId) |> deliverOnMainQueue).start(next: { [weak self] message in + genericView.wantsLayer = true + loadMessageDisposable.set((context.account.postbox.messageAtId(messageId) |> deliverOnMainQueue).start(next: { [weak self] message in if let message = message, let game = message.media.first as? TelegramMediaGame, let peer = message.inlinePeer { self?.start(with: game, peer: peer) } @@ -82,36 +100,67 @@ class WebGameViewController: TelegramGenericViewController, WebFrameLoa self.centerBarView.status = .initialize(string: "@\(peer.addressName ?? "gamebot")", color: theme.colors.grayText, font: .normal(.text)) if let url = URL(string:gameUrl) { - genericView.mainFrame.load(URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 15)) + genericView.load(URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 15)) } - genericView.frameLoadDelegate = self; + + + + + readyOnce() } - func webView(_ sender: WebView!, didFinishLoadFor frame: WebFrame!) { - frame.windowObject.evaluateWebScript("TelegramWebviewProxy = { postEvent:function(eventType, eventData) {gameHandler(eventType,eventData)}}") - JSGlobalContextSetName(frame.globalContext, JSStringCreateWithCFString(uniqueId as CFString!)); - let funcName = JSStringCreateWithUTF8CString("gameHandler"); - let funcObj = JSObjectMakeFunctionWithCallback(frame.globalContext, funcName, { (ctx, function, thisObject, argumentCount, arguments, exception) in - if let arguments = arguments, argumentCount == 2 && JSValueGetType (ctx, arguments[0]) == kJSTypeString && JSValueGetType (ctx, arguments[1]) == kJSTypeString { - let eventType = JSStringCopyCFString(kCFAllocatorDefault,JSValueToStringCopy (ctx, arguments[0],exception)) - let data = JSStringCopyCFString(kCFAllocatorDefault,JSValueToStringCopy (ctx, arguments[1],exception)) - let uniqueId = JSStringCopyCFString(kCFAllocatorDefault,JSGlobalContextCopyName(JSContextGetGlobalContext(ctx))) - - if let eventType = eventType as String?, let data = data as String?, let uniqueId = uniqueId as String?, let controller = game(forKey: uniqueId) { - let selector = NSSelectorFromString(eventType + ":") - if controller.responds(to: selector) { - controller.perform(selector, with: data) - } - } + + override func initializer() -> WKWebView { + + let js = "var TelegramWebviewProxyProto = function() {}; " + + "TelegramWebviewProxyProto.prototype.postEvent = function(eventName, eventData) { " + + "window.webkit.messageHandlers.performAction.postMessage({'eventName': eventName, 'eventData': eventData}); " + + "}; " + + "var TelegramWebviewProxy = new TelegramWebviewProxyProto();" + + let configuration = WKWebViewConfiguration() + let userController = WKUserContentController() + + let userScript = WKUserScript(source: js, injectionTime: .atDocumentStart, forMainFrameOnly: false) + userController.addUserScript(userScript) + + userController.add(WeakGameScriptMessageHandler { [weak self] message in + if let strongSelf = self { + strongSelf.handleScriptMessage(message) } - return JSValueMakeNull(ctx); - }); - JSObjectSetProperty(sender.mainFrame.globalContext, JSContextGetGlobalObject(frame.globalContext), funcName, funcObj, JSPropertyAttributes(kJSPropertyAttributeNone), nil); - JSStringRelease(funcName); - + }, name: "performAction") + + + configuration.userContentController = userController + + return WKWebView(frame: NSMakeRect(_frameRect.minX, _frameRect.minY, _frameRect.width, _frameRect.height - bar.height), configuration: configuration) + } + + private func handleScriptMessage(_ message: WKScriptMessage) { + guard let body = message.body as? [String: Any] else { + return + } + + guard let eventName = body["eventName"] as? String else { + return + } + + switch eventName { + case "share_game": + self.share_game("") + case "share_score": + self.share_score("") + case "game_over": + self.game_over("") + case "game_loaded": + self.game_loaded("") + default: + break + } } + @objc func game_loaded(_ data:String) { @@ -120,10 +169,17 @@ class WebGameViewController: TelegramGenericViewController, WebFrameLoa } @objc func share_game(_ data:String) { - showModal(with: ShareModalController(ShareLinkObject(account, link: "https://t.me/gamebot")), for: mainWindow) + showModal(with: ShareModalController(ShareLinkObject(context, link: "https://t.me/\(self.peer.addressName ?? "gamebot")" + "?game=\(self.media.name)")), for: mainWindow) } @objc func share_score(_ data:String) { - showModal(with: ShareModalController(ShareLinkObject(account, link: "https://t.me/gamebot")), for: mainWindow) + + let context = self.context + let messageId = self.messageId + + showModal(with: ShareModalController(ShareCallbackObject(context, callback: { peerIds in + let signals = peerIds.map { context.engine.messages.forwardGameWithScore(messageId: messageId, to: $0) } + return combineLatest(signals) |> map { _ in return } |> ignoreValues + })), for: context.window) } diff --git a/Telegram-Mac/WebSessionsController.swift b/Telegram-Mac/WebSessionsController.swift new file mode 100644 index 0000000000..28bc90c2e0 --- /dev/null +++ b/Telegram-Mac/WebSessionsController.swift @@ -0,0 +1,315 @@ +// +// WebSessionsController.swift +// Telegram +// +// Created by Mikhail Filimonov on 12/03/2018. +// Copyright © 2018 Telegram. All rights reserved. +// + +import Cocoa +import TelegramCore + +import Postbox +import SwiftSignalKit +import TGUIKit + + +private final class WebSessionArguments { + let context: AccountContext + let logoutSession:(WebAuthorization)->Void + let logoutAll:()->Void + init(context: AccountContext, logoutSession:@escaping(WebAuthorization)->Void, logoutAll:@escaping()->Void) { + self.context = context + self.logoutAll = logoutAll + self.logoutSession = logoutSession + } +} + +private enum WebSessionEntryStableId : Hashable { + + case logoutId + case descriptionId(Int32) + case sessionId(Int64) + case loadingId + case sectionId(Int32) + var hashValue: Int { + switch self { + case let .sectionId(id): + return Int(id) + case .logoutId: + return 0 + case .loadingId: + return 1 + case let .sessionId(id): + return Int(id) + case let .descriptionId(id): + return Int(id) + } + } +} + +private enum WebSessionEntry : TableItemListNodeEntry { + case logout(sectionId: Int32, index: Int32, viewType: GeneralViewType) + case description(sectionId: Int32, index: Int32, text: String, viewType: GeneralViewType) + case session(sectionId: Int32, index: Int32, authorization: WebAuthorization, peer: Peer, viewType: GeneralViewType) + case sectionId(Int32) + case loading + + var stableId:WebSessionEntryStableId { + switch self { + case let .sectionId(id): + return .sectionId(id) + case .logout: + return .logoutId + case .loading: + return .loadingId + case .description(_, let index, _, _): + return .descriptionId(index) + case .session(_, _, let authorization, _, _): + return .sessionId(authorization.hash) + } + } + + var index: Int32 { + switch self { + case let .logout(sectionId, index, _): + return (sectionId * 1000) + index + case .loading: + return 0 + case let .description(sectionId, index, _, _): + return (sectionId * 1000) + index + case let .session(sectionId, index, _, _, _): + return (sectionId * 1000) + index + case let .sectionId(sectionId): + return (sectionId * 1000) + sectionId + } + } + + + func item(_ arguments: WebSessionArguments, initialSize: NSSize) -> TableRowItem { + switch self { + case .sectionId: + return GeneralRowItem(initialSize, height: 30, stableId: stableId, viewType: .separator) + case let .description(_, _, text, viewType): + return GeneralTextRowItem(initialSize, stableId: stableId, text: text, viewType: viewType) + case let .logout(_, _, viewType): + return GeneralInteractedRowItem(initialSize, stableId: stableId, name: L10n.webAuthorizationsLogoutAll, nameStyle: ControlStyle(font: .normal(.title), foregroundColor: theme.colors.redUI), type: .none, viewType: viewType, action: { + arguments.logoutAll() + }) + case .loading: + return SearchEmptyRowItem(initialSize, stableId: stableId, isLoading: true) + case let .session(_, _, authorization, peer, viewType): + return WebAuthorizationRowItem(initialSize, stableId: stableId, account: arguments.context.account, authorization: authorization, peer: peer, viewType: viewType, logout: { + arguments.logoutSession(authorization) + }) + } + } +} + +private func ==(lhs: WebSessionEntry, rhs: WebSessionEntry) -> Bool { + switch lhs { + case .loading: + if case .loading = rhs { + return true + } else { + return false + } + case let .description(sectionId, index, text, viewType): + if case .description(sectionId, index, text, viewType) = rhs { + return true + } else { + return false + } + case let .logout(sectionId, index, viewType): + if case .logout(sectionId, index, viewType) = rhs { + return true + } else { + return false + } + case let .sectionId(sectionId): + if case .sectionId(sectionId) = rhs { + return true + } else { + return false + } + case let .session(lhsSectionId, lhsIndex, lhsAuthorization, lhsPeer, lhsViewType): + if case let .session(rhsSectionId, rhsIndex, rhsAuthorization, rhsPeer, rhsViewType) = rhs { + return lhsSectionId == rhsSectionId && lhsIndex == rhsIndex && lhsAuthorization == rhsAuthorization && lhsPeer.isEqual(rhsPeer) && lhsViewType == rhsViewType + } else { + return false + } + } +} +private func <(lhs: WebSessionEntry, rhs: WebSessionEntry) -> Bool { + return lhs.index < rhs.index +} + + +private struct WebSessionsControllerState: Equatable { + let removingSessionId: Int64? + let removedSessions: Set + init() { + self.removingSessionId = nil + self.removedSessions = [] + } + + init(removingSessionId: Int64?, removedSessions: Set) { + self.removingSessionId = removingSessionId + self.removedSessions = removedSessions + } + + static func ==(lhs: WebSessionsControllerState, rhs: WebSessionsControllerState) -> Bool { + if lhs.removingSessionId != rhs.removingSessionId { + return false + } + if lhs.removedSessions != rhs.removedSessions { + return false + } + + return true + } + + func withUpdatedRemovedSessionId(_ sessionId: Int64) -> WebSessionsControllerState { + var sessions = self.removedSessions + if sessions.contains(sessionId) { + sessions.remove(sessionId) + } else { + sessions.insert(sessionId) + } + return WebSessionsControllerState(removingSessionId: removingSessionId, removedSessions: sessions) + } + + func withUpdatedRemovingSessionId(_ removingSessionId: Int64?) -> WebSessionsControllerState { + return WebSessionsControllerState(removingSessionId: removingSessionId, removedSessions: self.removedSessions) + } + + func newState(from value: ([WebAuthorization], [PeerId : Peer])?) -> ([WebAuthorization], [PeerId : Peer])? { + if let value = value { + return (value.0.filter({!removedSessions.contains($0.hash)}), value.1) + } + return nil + } + +} + +private func prepareSessions(left:[AppearanceWrapperEntry], right: [AppearanceWrapperEntry], arguments: WebSessionArguments, initialSize: NSSize) -> TableUpdateTransition { + let (removed, inserted, updated) = proccessEntriesWithoutReverse(left, right: right) { entry -> TableRowItem in + return entry.entry.item(arguments, initialSize: initialSize) + } + + return TableUpdateTransition(deleted: removed, inserted: inserted, updated: updated, animated: true) +} + +private func webAuthorizationEntries(webSessions: WebSessionsContextState, state: WebSessionsControllerState) -> [WebSessionEntry] { + var entries: [WebSessionEntry] = [] + + + + var sectionId:Int32 = 0 + entries.append(.sectionId(sectionId)) + sectionId += 1 + + var index: Int32 = 1 + + + entries.append(.logout(sectionId: sectionId, index: index, viewType: .singleItem)) + index += 1 + + entries.append(.description(sectionId: sectionId, index: index, text: L10n.webAuthorizationsLogoutAllDescription, viewType: .textBottomItem)) + index += 1 + + entries.append(.sectionId(sectionId)) + sectionId += 1 + + + let authorizations = webSessions.sessions.filter {!state.removedSessions.contains($0.hash)} + + if authorizations.count > 0 { + entries.append(.description(sectionId: sectionId, index: index, text: L10n.webAuthorizationsLoggedInDescrpiption, viewType: .textTopItem)) + index += 1 + } + + for auth in webSessions.sessions { + if let peer = webSessions.peers[auth.botId] { + entries.append(.session(sectionId: sectionId, index: index, authorization: auth, peer: peer, viewType: bestGeneralViewType(authorizations, for: auth))) + index += 1 + } + } + + entries.append(.sectionId(sectionId)) + sectionId += 1 + + + return entries +} + +class WebSessionsController: TableViewController { + + private let disposable = MetaDisposable() + + override func viewDidLoad() { + super.viewDidLoad() + + let actionsDisposable = MetaDisposable() + + let state = Atomic(value: WebSessionsControllerState()) + + let context = self.context + + let stateValue = ValuePromise(WebSessionsControllerState(), ignoreRepeated: true) + + let updateState:((WebSessionsControllerState)->WebSessionsControllerState)->Void = { f -> Void in + stateValue.set(state.modify(f)) + } + + let arguments = WebSessionArguments(context: context, logoutSession: { session in + confirm(for: context.window, information: L10n.webAuthorizationsConfirmRevoke, successHandler: { result in + updateState { state in + return state.withUpdatedRemovingSessionId(session.hash) + } + + _ = showModalProgress(signal: context.webSessions.remove(hash: session.hash), for: context.window).start(next: { value in + updateState { state in + return state.withUpdatedRemovedSessionId(session.hash).withUpdatedRemovingSessionId(nil) + } + }) + }) + + }, logoutAll: { [weak self] in + confirm(for: context.window, information: L10n.webAuthorizationsConfirmRevokeAll, successHandler: { result in + self?.navigationController?.back() + _ = showModalProgress(signal: context.webSessions.removeAll(), for: context.window).start() + }) + + }) + let initialSize = self.atomicSize + + let previous: Atomic<[AppearanceWrapperEntry]> = Atomic(value: []) + + let signal = combineLatest(context.webSessions.state, appearanceSignal, stateValue.get()) |> map { webSessions, appearance, state -> (TableUpdateTransition, WebSessionsContextState) in + let entries = webAuthorizationEntries(webSessions: webSessions, state: state).map { + AppearanceWrapperEntry(entry: $0, appearance: appearance) + } + + return (prepareSessions(left: previous.swap(entries), right: entries, arguments: arguments, initialSize: initialSize.modify{$0}), webSessions) + + } |> deliverOnMainQueue |> afterDisposed { + actionsDisposable.dispose() + } + + disposable.set(signal.start(next: { [weak self] transition, state in + self?.genericView.merge(with: transition) + self?.readyOnce() + + if state.sessions.isEmpty { + self?.navigationController?.back() + } + })) + + } + + deinit { + disposable.dispose() + } + +} diff --git a/Telegram-Mac/WebpageModalController.swift b/Telegram-Mac/WebpageModalController.swift index 83a5556eea..014937608f 100644 --- a/Telegram-Mac/WebpageModalController.swift +++ b/Telegram-Mac/WebpageModalController.swift @@ -8,37 +8,51 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import SwiftSignalKitMac -import PostboxMac +import TelegramCore + +import SwiftSignalKit +import Postbox import WebKit -class WebpageModalController: ModalViewController,WebFrameLoadDelegate { - private var webView:WebView! + + +class WebpageModalController: ModalViewController, WKNavigationDelegate { private var indicator:ProgressIndicator! private let content:TelegramMediaWebpageLoadedContent - private let account:Account + private let context:AccountContext + private let webview: WKWebView = WKWebView(frame: NSZeroRect) override func loadView() { super.loadView() - webView = WebView(frame: self.bounds) - webView.frameLoadDelegate = self - webView.wantsLayer = true - addSubview(webView) + webview.wantsLayer = true + webview.removeFromSuperview() + addSubview(webview) indicator = ProgressIndicator(frame: NSMakeRect(0,0,30,30)) addSubview(indicator) indicator.center() - webView.isHidden = true + webview.isHidden = true indicator.animates = true + + webview.navigationDelegate = self + // leakWebview() + if let embed = content.embedUrl, let url = URL(string: embed) { - webView.mainFrame.load(URLRequest(url: url)) + webview.load(URLRequest(url: url)) + readyOnce() } } + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + webview.isHidden = false + webview.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + indicator.isHidden = true + indicator.animates = false + } + override var dynamicSize:Bool { return true } @@ -46,9 +60,10 @@ class WebpageModalController: ModalViewController,WebFrameLoadDelegate { override func measure(size: NSSize) { - if let embedSize = content.embedSize { + if let embedSize = content.embedSize?.size { let size = embedSize.aspectFitted(NSMakeSize(min(size.width - 100, 800), min(size.height - 100, 800))) - webView.setFrameSize(size) + webview.setFrameSize(size) + self.modal?.resize(with:size, animated: false) indicator.center() @@ -57,20 +72,20 @@ class WebpageModalController: ModalViewController,WebFrameLoadDelegate { override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - webView.mainFrame.load(URLRequest(url: URL(string:"file://blank")!)) - webView.mainFrame.stopLoading() + webview.removeFromSuperview() + webview.stopLoading() + webview.loadHTMLString("", baseURL: nil) } - func webView(_ sender: WebView!, didFinishLoadFor frame: WebFrame!) { - webView.isHidden = false - webView.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) - indicator.isHidden = true - indicator.animates = false + deinit { + var bp:Int = 0 + bp += 1 } - init(content:TelegramMediaWebpageLoadedContent, account:Account) { + + init(content:TelegramMediaWebpageLoadedContent, context: AccountContext) { self.content = content - self.account = account + self.context = context super.init(frame:NSMakeRect(0,0,350,270)) } diff --git a/Telegram-Mac/WidgetAppearance.swift b/Telegram-Mac/WidgetAppearance.swift new file mode 100644 index 0000000000..d56ebc5be0 --- /dev/null +++ b/Telegram-Mac/WidgetAppearance.swift @@ -0,0 +1,390 @@ +// +// WidgetAppearance.swift +// Telegram +// +// Created by Mikhail Filimonov on 06.07.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import TelegramCore + +import SwiftSignalKit + + + +private final class ThemePreview : Control { + private class Container : View { + private let disposable = MetaDisposable() + private let imageView = TransformImageView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + setup() + } + override init() { + super.init(frame: .zero) + setup() + } + private func setup() { + addSubview(imageView) + } + + func set(_ source: ThemeSource, bubbled: Bool, context: AccountContext) { + + let signal = themeAppearanceThumbAndData(context: context, bubbled: bubbled, source: source, thumbSource: .widget) |> deliverOnMainQueue + + self.imageView.setSignal(signal: cachedThemeThumb(source: source, bubbled: bubbled, thumbSource: .widget), clearInstantly: false) + + disposable.set(signal.start(next: { [weak self] image, data in + self?.imageView.setSignal(signal: .single(image), clearInstantly: true, animate: false) + cacheThemeThumb(image, source: source, bubbled: bubbled, thumbSource: .widget) + })) + } + + deinit { + disposable.dispose() + } + + override func layout() { + super.layout() + if bounds.size.width >= 4 && bounds.size.height >= 4 { + imageView.frame = bounds.insetBy(dx: 4, dy: 4) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + } + private let container = Container() + private let nameView: TextView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + setup() + } + override init() { + super.init(frame: .zero) + setup() + } + private func setup() { + addSubview(container) + addSubview(nameView) + container.isEventLess = true + nameView.userInteractionEnabled = false + nameView.isSelectable = false + self.scaleOnClick = true + } + + func update(_ text: String, source: ThemeSource, bubbled: Bool, context: AccountContext, isSelected: Bool) { + let layout = TextViewLayout(.initialize(string: text, color: isSelected ? theme.colors.accent : theme.colors.text, font: .medium(.text))) + layout.measure(width: .greatestFiniteMagnitude) + + self.nameView.update(layout) + + container.set(source, bubbled: bubbled, context: context) + + container.layer?.cornerRadius = 20 + container.layer?.borderWidth = isSelected ? 1.66 : 1 + container.layer?.borderColor = isSelected ? theme.colors.accent.cgColor : theme.colors.border.withAlphaComponent(0.6).cgColor + + } + + override func layout() { + super.layout() + container.frame = NSMakeRect(0, 0, frame.width, frame.height - 20) + self.nameView.centerX(y: frame.height - nameView.frame.height) + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + +final class WidgetAppearanceView : View { + + private let minimalist = ThemePreview() + private let colorful = ThemePreview() + + private let modeTitle = TextView() + + + var selectMin:(()->Void)? = nil + var selectColorful:(()->Void)? = nil + + var getContext:(()->AccountContext?)? = nil + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(minimalist) + addSubview(colorful) + addSubview(modeTitle) + minimalist.set(handler: { [weak self] _ in + self?.selectMin?() + }, for: .Click) + + colorful.set(handler: { [weak self] _ in + self?.selectColorful?() + }, for: .Click) + + modeTitle.userInteractionEnabled = false + modeTitle.isSelectable = false + } + + + var dayInstall: InstallThemeSource? + var darkInstall: InstallThemeSource? + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + + let theme = theme as! TelegramPresentationTheme + + if let context = getContext?() { + let installSource: InstallThemeSource? + if theme.colors.isDark { + installSource = darkInstall + } else { + installSource = dayInstall + } + let source: ThemeSource + if let installSource = installSource { + switch installSource { + case let .cloud(theme, _): + source = .cloud(theme) + case let .local(palette): + source = .local(palette, nil) + } + } else { + source = .local(theme.colors, nil) + } + minimalist.update(L10n.emptyChatAppearanceMin, source: source, bubbled: false, context: context, isSelected: !theme.bubbled) + colorful.update(L10n.emptyChatAppearanceColorful, source: source, bubbled: true, context: context, isSelected: theme.bubbled) + } + + let titleLayout = TextViewLayout.init(.initialize(string: L10n.emptyChatAppearanceChatMode, color: theme.colors.text, font: .medium(.text))) + titleLayout.measure(width: frame.width - 20) + modeTitle.update(titleLayout) + + + needsLayout = true + } + + override func layout() { + super.layout() + + modeTitle.resize(frame.width - 20) + modeTitle.centerX() + + + minimalist.frame = NSMakeRect(0, 30, 140, frame.height - 30) + colorful.frame = NSMakeRect(frame.width - 140, 30, 140, frame.height - 30) + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + + + +final class WidgetAppearanceController : TelegramGenericViewController> { + + private struct State : Equatable { + var dayInstall: InstallThemeSource? + var darkInstall: InstallThemeSource? + } + + private let disposable = MetaDisposable() + override init(_ context: AccountContext) { + super.init(context) + bar = .init(height: 0) + } + + deinit { + disposable.dispose() + } + + + override func viewDidLoad() { + super.viewDidLoad() + + let initialState = State() + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let loadSourcesDisposable = MetaDisposable() + + + let nightSettings = autoNightSettings(accountManager: context.sharedContext.accountManager) |> deliverOnMainQueue + + let themeSettings = themeSettingsView(accountManager: context.sharedContext.accountManager) |> deliverOnMainQueue + + + let context = self.context + + + func apply(_ source: InstallThemeSource) { + + let update: Signal + + switch source { + case let .local(palette): + update = updateThemeInteractivetly(accountManager: context.sharedContext.accountManager, f: { settings in + var settings = settings + settings = settings.withUpdatedPalette(palette).withUpdatedCloudTheme(nil) + + let defaultTheme = DefaultTheme(local: palette.parent, cloud: nil) + if palette.isDark { + settings = settings.withUpdatedDefaultDark(defaultTheme) + } else { + settings = settings.withUpdatedDefaultDay(defaultTheme) + } + return settings.installDefaultWallpaper().installDefaultAccent().withUpdatedDefaultIsDark(palette.isDark).withSavedAssociatedTheme() + }) + case let .cloud(cloud, cached): + if let cached = cached { + update = updateThemeInteractivetly(accountManager: context.sharedContext.accountManager, f: { settings in + var settings = settings + settings = settings.withUpdatedPalette(cached.palette) + settings = settings.withUpdatedCloudTheme(cloud) + settings = settings.updateWallpaper { _ in + return ThemeWallpaper(wallpaper: cached.wallpaper, associated: AssociatedWallpaper(cloud: cached.cloudWallpaper, wallpaper: cached.wallpaper)) + } + let defaultTheme = DefaultTheme(local: settings.palette.parent, cloud: DefaultCloudTheme(cloud: cloud, palette: cached.palette, wallpaper: AssociatedWallpaper(cloud: cached.cloudWallpaper, wallpaper: cached.wallpaper))) + if cached.palette.isDark { + settings = settings.withUpdatedDefaultDark(defaultTheme) + } else { + settings = settings.withUpdatedDefaultDay(defaultTheme) + } + return settings.saveDefaultWallpaper().withUpdatedDefaultIsDark(cached.palette.isDark).withSavedAssociatedTheme() + }) + _ = downloadAndApplyCloudTheme(context: context, theme: cloud, install: true).start() + } else if cloud.file != nil || cloud.settings != nil { + _ = showModalProgress(signal: downloadAndApplyCloudTheme(context: context, theme: cloud, install: true), for: context.window).start() + update = .single(Void()) + } else { + update = .single(Void()) + } + } + + let night = updateAutoNightSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in + return current.withUpdatedSchedule(nil).withUpdatedSystemBased(false) + }) + + _ = (update |> then(night)).start() + + } + + genericView.dataView = WidgetAppearanceView(frame: .zero) + + genericView.dataView?.getContext = { [weak self] in + return self?.context + } + + genericView.dataView?.selectColorful = { + let update = updateThemeInteractivetly(accountManager: context.sharedContext.accountManager, f: { settings in + return settings.withUpdatedBubbled(true) + }) + _ = update.start() + } + genericView.dataView?.selectMin = { + let update = updateThemeInteractivetly(accountManager: context.sharedContext.accountManager, f: { settings in + return settings.withUpdatedBubbled(false) + }) + _ = update.start() + } + + disposable.set(combineLatest(nightSettings, statePromise.get(), appearanceSignal).start(next: { [weak self] night, state, _ in + + let isSystemBased: Bool = night.systemBased + + self?.genericView.dataView?.dayInstall = state.dayInstall + self?.genericView.dataView?.darkInstall = state.darkInstall + self?.genericView.updateLocalizationAndTheme(theme: theme) + + var buttons:[WidgetData.Button] = [] + + buttons.append(.init(text: { L10n.emptyChatAppearanceSystem }, selected: { + return isSystemBased + }, image: { + return night.systemBased ? theme.icons.empty_chat_system_active : theme.icons.empty_chat_system + }, click: { + _ = updateAutoNightSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in + return current.withUpdatedSchedule(nil).withUpdatedSystemBased(true) + }).start() + })) + + let darkSelected = theme.colors.isDark && !isSystemBased + let lightSelected = !theme.colors.isDark && !isSystemBased + + + buttons.append(.init(text: { L10n.emptyChatAppearanceDark }, selected: { + return darkSelected + }, image: { + return darkSelected ? theme.icons.empty_chat_dark_active : theme.icons.empty_chat_dark + }, click: { + if let source = state.darkInstall { + apply(source) + } + })) + + buttons.append(.init(text: { L10n.emptyChatAppearanceLight }, selected: { + return lightSelected + }, image: { + return lightSelected ? theme.icons.empty_chat_light_active : theme.icons.empty_chat_light + }, click: { + if let source = state.dayInstall { + apply(source) + } + })) + + self?.genericView.update(.init(title: { L10n.emptyChatAppearance }, desc: { L10n.emptyChatAppearanceDesc }, descClick: { + context.sharedContext.bindings.rootNavigation().push(AppAppearanceViewController(context: context)) + }, buttons: buttons)) + + self?.readyOnce() + })) + + let loadSources: Signal<[InstallThemeSource], NoError> = themeSettings |> mapToSignal { settings in + let daySource: ThemeSource + let darkSource: ThemeSource + if let cloud = settings.defaultDay.cloud?.cloud { + daySource = .cloud(cloud) + } else { + daySource = .local(settings.defaultDay.local.palette, nil) + } + + if let cloud = settings.defaultDark.cloud?.cloud { + darkSource = .cloud(cloud) + } else { + darkSource = .local(settings.defaultDark.local.palette, nil) + } + + return combineLatest(themeInstallSource(context: context, source: daySource), themeInstallSource(context: context, source: darkSource)) |> map { + return [$0, $1] + } + } |> deliverOnMainQueue + + loadSourcesDisposable.set(loadSources.start(next: { sources in + updateState { current in + var current = current + current.dayInstall = sources[0] + current.darkInstall = sources[1] + return current + } + })) + + + } +} diff --git a/Telegram-Mac/WidgetButton.swift b/Telegram-Mac/WidgetButton.swift new file mode 100644 index 0000000000..ddf8794727 --- /dev/null +++ b/Telegram-Mac/WidgetButton.swift @@ -0,0 +1,79 @@ +// +// WidgetButton.swift +// Telegram +// +// Created by Mikhail Filimonov on 08.07.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit + +final class WidgetButton: Button { + private let textView = TextView() + private let image = ImageView() + private var text: String = "" + private let view = View() + override init() { + super.init(frame: .zero) + + setup() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + setup() + } + + private func setup() { + view.addSubview(textView) + view.addSubview(image) + addSubview(view) + textView.userInteractionEnabled = false + textView.isSelectable = false + scaleOnClick = true + } + + + func update(_ isSelected: Bool, icon: CGImage, text: String) { + self.text = text + + let color = isSelected ? theme.colors.accent : theme.colors.text + + let textLayout = TextViewLayout(.initialize(string: text, color: color, font: .medium(.short))) + textLayout.measure(width: .greatestFiniteMagnitude) + textView.update(textLayout) + + self.isSelected = isSelected + layer?.borderColor = isSelected ? theme.colors.accent.cgColor : theme.colors.border.cgColor + layer?.borderWidth = isSelected ? 1.66 : 1 + + image.image = icon + image.sizeToFit() + + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + + } + + override func layout() { + super.layout() + + view.setFrameSize(NSMakeSize(image.frame.width + 4 + textView.frame.width, frame.height)) + image.centerY(x: 0) + layer?.cornerRadius = frame.height / 2 + textView.centerY(x: image.frame.maxX + 4, addition: -1) + + view.center() + } + + func size() -> CGSize { + return NSMakeSize(textView.frame.width + 20 + image.frame.width + 4, 30) + } +} diff --git a/Telegram-Mac/WidgetController.swift b/Telegram-Mac/WidgetController.swift new file mode 100644 index 0000000000..901b4379f5 --- /dev/null +++ b/Telegram-Mac/WidgetController.swift @@ -0,0 +1,301 @@ +// +// WidgetController.swift +// Telegram +// +// Created by Mikhail Filimonov on 06.07.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit + +private final class WidgetNavigationButton : Control { + + + enum Direction { + case left + case right + } + + private let textView = TextView() + private let imageView = ImageView() + private let view = View() + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(view) + view.addSubview(textView) + view.addSubview(imageView) + imageView.isEventLess = true + textView.userInteractionEnabled = false + textView.isSelectable = false + self.layer?.cornerRadius = 16 + scaleOnClick = true + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + + let theme = theme as! TelegramPresentationTheme + self.background = theme.chatServiceItemColor + } + + private var direction: Direction? + + func setup(_ text: String, image: CGImage, direction: Direction) { + self.direction = direction + + let layout = TextViewLayout(.initialize(string: text, color: theme.chatServiceItemTextColor, font: .medium(.text))) + layout.measure(width: .greatestFiniteMagnitude) + textView.update(layout) + + imageView.image = image + imageView.sizeToFit() + updateLocalizationAndTheme(theme: theme) + } + + override func layout() { + super.layout() + + view.setFrameSize(NSMakeSize(textView.frame.width + 4 + imageView.frame.width, frame.height)) + view.center() + + if let direction = direction { + switch direction { + case .left: + imageView.centerY(x: 0) + textView.centerY(x: imageView.frame.maxX + 4) + case .right: + textView.centerY(x: 0) + imageView.centerY(x: textView.frame.maxX + 4) + } + } + } + + func size() -> NSSize { + return NSMakeSize(28 + textView.frame.width + 4 + imageView.frame.width, 32) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +final class WidgetListView: View { + + enum PresentMode { + case immidiate + case leftToRight + case rightToLeft + + var animated: Bool { + return self != .immidiate + } + } + + private let documentView = View() + + private var controller: ViewController? + + private var prev: WidgetNavigationButton? + private var next: WidgetNavigationButton? + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(documentView) + } + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var _next:(()->Void)? + var _prev:(()->Void)? + + + func present(controller: ViewController, hasNext: Bool, hasPrev: Bool, mode: PresentMode) { + let previous = self.controller + self.controller = controller + + let duration: Double = 0.5 + + if let previous = previous { + if mode.animated { + previous.view._change(opacity: 0, duration: duration, timingFunction: .spring, completion: { [weak previous] completed in + if completed { + previous?.removeFromSuperview() + } + }) + } else { + previous.removeFromSuperview() + } + } + + documentView.addSubview(controller.view) + controller.view.centerX(y: 0) + + controller.view._change(opacity: 1, animated: mode.animated, duration: duration, timingFunction: .spring) + if mode.animated { + let to = controller.view.frame.origin + let from: NSPoint + switch mode { + case .leftToRight: + from = NSMakePoint(to.x - 50, to.y) + case .rightToLeft: + from = NSMakePoint(to.x + 50, to.y) + default: + from = to + } + controller.view.layer?.animatePosition(from: from, to: to, duration: duration, timingFunction: .spring) + } + + if hasPrev { + if self.prev == nil { + self.prev = .init(frame: .zero) + if let prev = prev { + addSubview(prev) + } + prev?.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + self.prev?.set(handler: { [weak self] _ in + self?._prev?() + }, for: .Click) + } + } else if let prev = self.prev { + prev.userInteractionEnabled = false + performSubviewRemoval(prev, animated: mode.animated) + self.prev = nil + } + if hasNext { + if self.next == nil { + self.next = .init(frame: .zero) + if let next = next { + addSubview(next) + } + next?.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + self.next?.set(handler: { [weak self] _ in + self?._next?() + }, for: .Click) + } + } else if let next = self.next { + next.userInteractionEnabled = false + performSubviewRemoval(next, animated: mode.animated) + self.next = nil + } + updateLocalizationAndTheme(theme: theme) + + needsLayout = true + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + + backgroundColor = .clear + documentView.backgroundColor = .clear + + let theme = theme as! TelegramPresentationTheme + + if let prev = prev { + prev.setup(L10n.emptyChatNavigationPrev, image: theme.emptyChatNavigationPrev, direction: .left) + prev.setFrameSize(prev.size()) + } + if let next = next { + next.setup(L10n.emptyChatNavigationNext, image: theme.emptyChatNavigationNext, direction: .right) + next.setFrameSize(next.size()) + } + needsLayout = true + } + + override func layout() { + super.layout() + documentView.frame = NSMakeRect(0, 0, frame.width, 320) + + guard let controller = controller else { + return + } + controller.view.centerX(y: 0) + + if let prev = prev { + prev.setFrameOrigin(NSMakePoint(controller.frame.minX, documentView.frame.maxY + 10)) + } + if let next = next { + next.setFrameOrigin(NSMakePoint(controller.frame.maxX - next.frame.width, documentView.frame.maxY + 10)) + } + } +} + +final class WidgetController : TelegramGenericViewController { + + private var controllers:[ViewController] = [] + + private var selected: Int = 0 + + override init(_ context: AccountContext) { + super.init(context) + self.bar = .init(height: 0) + } + + private func loadController(_ controller: ViewController) { + controller._frameRect = NSMakeRect(0, 0, 320, 320) + controller.bar = .init(height: 0) + controller.loadViewIfNeeded() + } + + private func presentSelected(_ mode: WidgetListView.PresentMode) { + let controller = controllers[selected] + loadController(controller) + genericView.present(controller: controller, hasNext: controllers.count - 1 > selected, hasPrev: selected > 0, mode: mode) + } + + override func backKeyAction() -> KeyHandlerResult { + if prev() { + return .invoked + } + return .rejected + } + override func nextKeyAction() -> KeyHandlerResult { + if next() { + return .invoked + } + return .rejected + } + + @discardableResult private func next() -> Bool { + if selected < controllers.count - 1 { + selected += 1 + presentSelected(.rightToLeft) + return true + } + return false + } + @discardableResult private func prev() -> Bool { + if selected > 0 { + selected -= 1 + presentSelected(.leftToRight) + return true + } + return false + } + override func viewDidLoad() { + super.viewDidLoad() + + controllers.append(WidgetRecentPeersController(context)) + controllers.append(WidgetAppearanceController(context)) + controllers.append(WidgetStorageController(context)) + controllers.append(WidgetStickersController(context)) + + let current = controllers[selected] + + loadController(current) + + ready.set(current.ready.get()) + + genericView.present(controller: current, hasNext: controllers.count - 1 > selected, hasPrev: selected > 0, mode: .immidiate) + + genericView._next = { [weak self] in + self?.next() + } + genericView._prev = { [weak self] in + self?.prev() + } + } +} diff --git a/Telegram-Mac/WidgetRecentPeersController.swift b/Telegram-Mac/WidgetRecentPeersController.swift new file mode 100644 index 0000000000..0d3dbce34f --- /dev/null +++ b/Telegram-Mac/WidgetRecentPeersController.swift @@ -0,0 +1,253 @@ +// +// WidgetRecentPeersController.swift +// Telegram +// +// Created by Mikhail Filimonov on 06.09.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit +import TelegramCore +import Postbox + +final class WidgetRecentPeersContainer: View { + + private final class PeerView : Control { + private let avatar: AvatarControl = AvatarControl(font: .avatar(20)) + private let textView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + avatar.setFrameSize(NSMakeSize(56, 56)) + addSubview(avatar) + addSubview(textView) + textView.userInteractionEnabled = false + textView.isSelectable = false + avatar.userInteractionEnabled = false + scaleOnClick = true + } + + override func layout() { + super.layout() + avatar.centerX(y: 0) + textView.resize(frame.width - 8) + textView.centerX(y: avatar.frame.maxY + 6) + } + + func update(_ peer: Peer, context: AccountContext, animated: Bool) { + self.avatar.setPeer(account: context.account, peer: peer, message: nil, size: NSMakeSize(56, 56)) + + let layout = TextViewLayout(.initialize(string: peer.compactDisplayTitle, color: theme.colors.text, font: .medium(.small)), maximumNumberOfLines: 1, alignment: .center) + layout.measure(width: frame.width - 8) + textView.update(layout) + + needsLayout = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + } + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + } + + func update(_ state: WidgetRecentPeersController.State, context: AccountContext, animated: Bool, open: @escaping(PeerId)->Void) { + let peers:[PeerEquatable] + switch state.section { + case .favorite: + peers = state.favorite + case .recent: + peers = state.recent + case .both: + var cur:[PeerEquatable] = Array(state.recent.prefix(4)) + for peer in state.favorite { + let contains = cur.contains(where: { $0.peer.id == peer.peer.id }) + if !contains { + cur.append(peer) + } + if cur.count == 8 { + break + } + } + peers = cur + } + + while subviews.count > peers.count { + subviews.removeLast() + } + + while subviews.count < peers.count { + subviews.append(PeerView(frame: NSMakeRect(0, 0, frame.width / 4, frame.height / 2))) + } + + for (i, peer) in peers.enumerated() { + let view = (subviews[i] as! PeerView) + view.update(peer.peer, context: context, animated: animated) + view.removeAllHandlers() + view.set(handler: { _ in + open(peer.peer.id) + }, for: .Click) + } + needsLayout = true + } + + override func layout() { + super.layout() + + var point: CGPoint = CGPoint(x: 0, y: 0) + + let size = NSMakeSize(frame.width / 4, frame.height / 2) + for (i, view) in subviews.enumerated() { + view.frame = CGRect(origin: point, size: size) + if i == 3 { + point.y += view.frame.height + point.x = 0 + } else { + point.x += view.frame.width + } + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +final class WidgetRecentPeersController : TelegramGenericViewController> { + + struct State : Equatable { + + enum Section : Equatable { + case favorite + case recent + case both + } + + var favorite: [PeerEquatable] = [] + var recent:[PeerEquatable] = [] + + var section: Section + } + + private let disposable = MetaDisposable() + private let actionsDisposable = DisposableSet() + override init(_ context: AccountContext) { + super.init(context) + self.bar = .init(height: 0) + } + + + + deinit { + actionsDisposable.dispose() + disposable.dispose() + } + + override func viewDidLoad() { + super.viewDidLoad() + let context = self.context + + self.genericView.dataView = WidgetRecentPeersContainer(frame: .zero) + + + let initialState = State(section: .favorite) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + var first = true + + let recent: Signal<[PeerEquatable], NoError> = context.recentlyUserPeerIds |> mapToSignal { ids in + return context.account.postbox.transaction { transaction in + let peers = ids.compactMap { transaction.getPeer($0) } + return Array(peers.map { PeerEquatable($0) }.prefix(8)) + } + } + let favorite: Signal<[PeerEquatable], NoError> = context.engine.peers.recentPeers() |> map { recent in + switch recent { + case .disabled: + return [] + case let .peers(peers): + return Array(peers.map { PeerEquatable($0) }.prefix(8)) + } + } + + actionsDisposable.add(combineLatest(recent, favorite).start(next: { recent, favorite in + updateState { current in + var current = current + current.favorite = favorite + current.recent = recent + if current.section == .favorite, favorite.isEmpty { + current.section = .recent + } + if current.section == .recent, recent.isEmpty { + current.section = .favorite + } + return current + } + })) + + disposable.set((statePromise.get() |> deliverOnMainQueue).start(next: { [weak self] state in + var buttons: [WidgetData.Button] = [] + + + if !state.favorite.isEmpty { + buttons.append(.init(text: { L10n.widgetRecentPopular }, selected: { + return state.section == .favorite + }, image: { + return state.section == .favorite ? theme.icons.widget_peers_favorite_active: theme.icons.widget_peers_favorite + }, click: { + updateState { current in + var current = current + current.section = .favorite + return current + } + })) + } + + if !state.recent.isEmpty { + buttons.append(.init(text: { L10n.widgetRecentRecent }, selected: { + return state.section == .recent + }, image: { + return state.section == .recent ? theme.icons.widget_peers_recent_active: theme.icons.widget_peers_recent + }, click: { + updateState { current in + var current = current + current.section = .recent + return current + } + })) + } + + if !state.recent.isEmpty && !state.favorite.isEmpty { + buttons.append(.init(text: { L10n.widgetRecentMixed }, selected: { + return state.section == .both + }, image: { + return state.section == .both ? theme.icons.widget_peers_both_active: theme.icons.widget_peers_both + }, click: { + updateState { current in + var current = current + current.section = .both + return current + } + })) + } + + + let data: WidgetData = .init(title: { L10n.widgetRecentTitle }, desc: { L10n.widgetRecentDesc }, descClick: { + showModal(with: QuickSwitcherModalController(context), for: context.window) + }, buttons: buttons, contentHeight: 180) + + self?.genericView.update(data) + self?.genericView.dataView?.update(state, context: context, animated: !first, open: { peerId in + context.sharedContext.bindings.rootNavigation().push(ChatController(context: context, chatLocation: .peer(peerId)), style: .push) + }) + first = false + })) + } +} diff --git a/Telegram-Mac/WidgetStickersController.swift b/Telegram-Mac/WidgetStickersController.swift new file mode 100644 index 0000000000..3b11c10bcb --- /dev/null +++ b/Telegram-Mac/WidgetStickersController.swift @@ -0,0 +1,319 @@ +// +// WidgetStickersController.swift +// Telegram +// +// Created by Mikhail Filimonov on 13.07.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit +import TelegramCore + +import Postbox + +private final class WidgetStickerView : Control { + private let animatedView = MediaAnimatedStickerView(frame: .zero) + private let nameView = TextView() + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(animatedView) + addSubview(nameView) + nameView.userInteractionEnabled = false + nameView.isSelectable = false + animatedView.userInteractionEnabled = false + scaleOnClick = true + } + + var data: (FeaturedStickerPackItem, AccountContext)? { + didSet { + if let data = data { + let item = data.0 + let context = data.1 + var file: TelegramMediaFile? + if let thumbnail = item.info.thumbnail { + file = TelegramMediaFile(fileId: MediaId(namespace: 0, id: item.info.id.id), partialReference: nil, resource: thumbnail.resource, previewRepresentations: [thumbnail], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/x-tgsticker", size: nil, attributes: [.FileName(fileName: "sticker.tgs"), .Sticker(displayText: "", packReference: .id(id: item.info.id.id, accessHash: item.info.accessHash), maskData: nil)]) + } else if let item = item.topItems.first { + file = item.file + } + if let file = file { + self.animatedView.update(with: file, size: NSMakeSize(72, 72), context: context, parent: nil, table: nil, parameters: nil, animated: true, positionFlags: nil, approximateSynchronousValue: false) + } + nameView.update(TextViewLayout(.initialize(string: item.info.title, color: theme.colors.text, font: .medium(.short)), maximumNumberOfLines: 2, alignment: .center)) + } + needsLayout = true + } + } + + override func layout() { + super.layout() + self.animatedView.centerX(y: 6) + nameView.resize(frame.width - 10) + nameView.centerX(y: self.animatedView.frame.maxY + 6) + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + let data = self.data + self.data = data + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +final class WidgetStickersContainer : View { + private let title = TextView() + + private let stickers: View = View() + private var timer: SwiftSignalKit.Timer? = nil + + var previewPack:((FeaturedStickerPackItem, @escaping()->Void)->Void)? + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(title) + addSubview(stickers) + title.userInteractionEnabled = false + title.isSelectable = false + + } + private var state: WidgetStickersController.State? + private var elements:[FeaturedStickerPackItem]? + private var context: AccountContext? + func update(_ state: WidgetStickersController.State, context: AccountContext, animated: Bool) { + self.state = state + self.context = context + + + runTimer() + + if !animated || stickers.subviews.isEmpty { + self.reload(state, context: context, animated: animated) + } + + updateLocalizationAndTheme(theme: theme) + } + + private func runTimer() { + timer = SwiftSignalKit.Timer(timeout: 60, repeat: true, completion: { [weak self] in + guard let context = self?.context, let state = self?.state else { + return + } + self?.reload(state, context: context, animated: true) + }, queue: .mainQueue()) + timer?.start() + } + + func reload(ignore: Int? = nil) { + runTimer() + guard let context = self.context, let state = self.state else { + return + } + self.reload(state, ignore: ignore, context: context, animated: true) + } + + private func generateRandom(_ state: WidgetStickersController.State, ignore: Int? = nil) -> [FeaturedStickerPackItem] { + if let ignore = ignore, var elements = self.elements { + let element = elements.remove(at: ignore) + while let randomElement = state.stickers.randomElement() { + if !elements.contains(where: { $0.info.id.id == element.info.id.id }) { + elements.insert(randomElement, at: ignore) + break + } + } + return elements + } else { + return state.stickers.randomElements(3) + } + } + + private func reload(_ state: WidgetStickersController.State, ignore: Int? = nil, context: AccountContext, animated: Bool) { + let random = generateRandom(state, ignore: ignore) + self.elements = random + var ignore:Set = Set() + for (i, sticker) in self.stickers.subviews.enumerated() { + if let sticker = sticker as? WidgetStickerView { + if sticker.data?.0.info.id != random[i].info.id { + performSubviewRemoval(sticker, animated: animated) + sticker.data = nil + } else { + ignore.insert(i) + } + } + } + for (i, item) in random.enumerated() { + if !ignore.contains(i) { + let view = WidgetStickerView(frame: .zero) + stickers.subviews.insert(view, at: i) + view.data = (item, context) + if animated { + view.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + } + view.set(handler: { [weak self] _ in + self?.previewPack?(item, { [weak self] in + self?.reload(ignore: i) + }) + }, for: .Click) + } + } + needsLayout = true + } + + deinit { + } + + override func layout() { + super.layout() + title.resize(frame.width - 20) + title.centerX() + + stickers.frame = NSMakeRect(0, title.frame.maxY + 10, frame.width, frame.height - (title.frame.maxY + 10)) + + let subviews = stickers.subviews + .compactMap { $0 as? WidgetStickerView } + .filter { $0.data != nil } + + for (i, sticker) in subviews.enumerated() { + let width = stickers.frame.width / CGFloat(subviews.count) + sticker.frame = NSMakeRect(width * CGFloat(i), 0, width, stickers.frame.height) + } + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + + let theme = theme as! TelegramPresentationTheme + + let titleLayout = TextViewLayout(.initialize(string: L10n.emptyChatStickersTrending, color: theme.colors.text, font: .medium(.text))) + titleLayout.measure(width: frame.width - 20) + title.update(titleLayout) + + needsLayout = true + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +final class WidgetStickersController : TelegramGenericViewController> { + + struct State : Equatable { + static func == (lhs: State, rhs: State) -> Bool { + return false + } + var settings: StickerSettings + var stickers:[FeaturedStickerPackItem] = [] + } + + private let disposable = MetaDisposable() + private let actionsDisposable = DisposableSet() + override init(_ context: AccountContext) { + super.init(context) + self.bar = .init(height: 0) + } + + deinit { + actionsDisposable.dispose() + disposable.dispose() + } + + override func viewDidLoad() { + super.viewDidLoad() + let context = self.context + + self.genericView.dataView = WidgetStickersContainer(frame: .zero) + + self.genericView.dataView?.previewPack = { [weak self] item, f in + showModal(with: StickerPackPreviewModalController(context, peerId: nil, reference: .id(id: item.info.id.id, accessHash: item.info.accessHash), onAdd: f), for: context.window) + } + + let initialState = State(settings: StickerSettings.defaultSettings) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + var first = true + + + let featured = Promise<[FeaturedStickerPackItem]>() + featured.set(context.account.viewTracker.featuredStickerPacks()) + + let stickerSettingsKey = ApplicationSpecificPreferencesKeys.stickerSettings + let preferencesKey: PostboxViewKey = .preferences(keys: Set([stickerSettingsKey])) + let preferencesView = context.account.postbox.combinedView(keys: [preferencesKey]) + + let stickerSettings: Signal = preferencesView |> map { preferencesView in + var stickerSettings = StickerSettings.defaultSettings + if let view = preferencesView.views[preferencesKey] as? PreferencesView { + if let value = view.values[stickerSettingsKey] as? StickerSettings { + stickerSettings = value + } + } + return stickerSettings + } + + actionsDisposable.add(combineLatest(queue: .mainQueue(), stickerSettings, featured.get()).start(next: { settings, featured in + updateState { current in + var current = current + current.stickers = featured + current.settings = settings + return current + } + })) + + disposable.set((statePromise.get() |> deliverOnMainQueue).start(next: { [weak self] state in + + var buttons: [WidgetData.Button] = [] + + let noneSelected = state.settings.emojiStickerSuggestionMode == .none + let mySetsSelected = state.settings.emojiStickerSuggestionMode == .installed + let allSetsSelected = state.settings.emojiStickerSuggestionMode == .all + + buttons.append(.init(text: { L10n.emptyChatStickersNone }, selected: { + return noneSelected + }, image: { + return noneSelected ? theme.icons.empty_chat_stickers_none_active: theme.icons.empty_chat_stickers_none + }, click: { + _ = updateStickerSettingsInteractively(postbox: context.account.postbox, { + $0.withUpdatedEmojiStickerSuggestionMode(.none) + }).start() + })) + + buttons.append(.init(text: { L10n.emptyChatStickersMySets }, selected: { + return mySetsSelected + }, image: { + return mySetsSelected ? theme.icons.empty_chat_stickers_mysets_active : theme.icons.empty_chat_stickers_mysets + }, click: { + _ = updateStickerSettingsInteractively(postbox: context.account.postbox, { + $0.withUpdatedEmojiStickerSuggestionMode(.installed) + }).start() + })) + + buttons.append(.init(text: { L10n.emptyChatStickersAllSets }, selected: { + return allSetsSelected + }, image: { + return allSetsSelected ? theme.icons.empty_chat_stickers_allsets_active : theme.icons.empty_chat_stickers_allsets + }, click: { + _ = updateStickerSettingsInteractively(postbox: context.account.postbox, { + $0.withUpdatedEmojiStickerSuggestionMode(.all) + }).start() + })) + + let data: WidgetData = .init(title: { L10n.emptyChatStickers }, desc: { L10n.emptyChatStickersDesc }, descClick: { + context.sharedContext.bindings.rootNavigation().push(FeaturedStickerPacksController(context)) + }, buttons: buttons) + + self?.genericView.update(data) + self?.genericView.dataView?.update(state, context: context, animated: !first) + first = false + })) + + } +} diff --git a/Telegram-Mac/WidgetStorageController.swift b/Telegram-Mac/WidgetStorageController.swift new file mode 100644 index 0000000000..6cc5836c4b --- /dev/null +++ b/Telegram-Mac/WidgetStorageController.swift @@ -0,0 +1,478 @@ +// +// WidgetStorage.swift +// Telegram +// +// Created by Mikhail Filimonov on 08.07.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit +import SwiftSignalKit +import TelegramCore + +import Postbox + + +private extension CacheUsageStatsResult { + var totalBytes: UInt64 { + switch self { + case .progress: + return 0 + case let .result(stats): + return UInt64(stats.otherSize + stats.cacheSize) + } + } +} + +private final class WidgetStorageProgress: View { + private var animators: [DisplayLinkAnimator] = [] + private var removeAnimators: [Int : DisplayLinkAnimator] = [:] + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + struct Value { + var index: Int + var value: CGFloat + } + + private(set) var progressValue: [Int : CGFloat] = [:] { + didSet { + needsDisplay = true + } + } + + func setProgress(_ values: [Int : CGFloat], tooltips: [Int: String]) { + + self.animators = [] + var toRemove: [Int : CGFloat] = [:] + if !values.isEmpty { + for value in values { + let fromValue = self.progressValue[value.key] ?? 0 + let toValue = max(min(1, value.value), 0) + + self.animators.append(DisplayLinkAnimator(duration: 0.4, from: fromValue, to: toValue, update: { [weak self] updated in + self?.progressValue[value.key] = updated + + }, completion: {})) + + removeAnimators.removeValue(forKey: value.key) + } + toRemove = self.progressValue.filter { value in + return !values.contains(where: { $0.key == value.key }) && removeAnimators[value.key] == nil + } + } else { + let fromValue: CGFloat = self.progressValue[0] ?? 0 + let toValue: CGFloat = fromValue == 1 ? 0 : 1 + + self.animators.append(DisplayLinkAnimator(duration: 3.0, from: fromValue, to: toValue, update: { [weak self] updated in + self?.progressValue[0] = updated + }, completion: { [weak self] in + self?.setProgress([:], tooltips: tooltips) + })) + + toRemove = self.progressValue.filter { + $0.key != 0 && removeAnimators[$0.key] == nil + } + removeAnimators.removeValue(forKey: 0) + } + for value in toRemove { + removeAnimators[value.key] = DisplayLinkAnimator(duration: 0.4, from: value.value, to: 0, update: { [weak self] updated in + self?.progressValue[value.key] = updated + }, completion: { [weak self] in + self?.setProgress([:], tooltips: tooltips) + self?.progressValue.removeValue(forKey: value.key) + }) + } + + self.removeAllSubviews() + + var list:[CGFloat] = Array(repeating: 0, count: values.count) + for value in values { + list[value.key] = value.value + } + + list = normalize(list) + + var width: CGFloat = 0 + for (i, value) in list.enumerated() { + let key = values.first(where: { $0.key == i })?.key + if let key = key { + let control = Control() + addSubview(control) + control.frame = NSMakeRect(width, 0, (value * frame.width) - width, frame.height) + control.appTooltip = tooltips[key] + width = value * frame.width + } + } + } + + private func normalize(_ values:[CGFloat]) -> [CGFloat] { + var values = values + values.sort(by: <) + + var prev: CGFloat? + for i in 0 ..< values.count { + if let prev = prev { + let minStep = max(0.01, values[i] - prev) + values[i] = min(values[i] + 0.01 * (minStep / 0.01), 1) + } + prev = values[i] + } + return values + } + + override func draw(_ layer: CALayer, in ctx: CGContext) { + super.draw(layer, in: ctx) + + ctx.round(frame.size, frame.height / 2) + + let path = CGMutablePath() + + path.addRoundedRect(in: bounds, cornerWidth: frame.height / 2, cornerHeight: frame.height / 2) + + ctx.setStrokeColor(theme.colors.border.cgColor) + ctx.setLineWidth(1.0) + + ctx.addPath(path) + ctx.strokePath() + + + var colors = [theme.colors.accent, theme.colors.peerAvatarBlueTop] + + if self.progressValue.count == 1 { + colors = colors.reversed() + } + + var values:[CGFloat] = Array(repeating: 0, count: self.progressValue.count) + for value in self.progressValue { + values[value.key] = value.value + } + + values = normalize(values) + + for (i, value) in values.reversed().enumerated() { + ctx.setFillColor(colors[i].cgColor) + ctx.fill(NSMakeRect(0, 0, value * frame.width, frame.height)) + } + } + + override func layout() { + super.layout() + } +} + +final class WidgetStorageContainer : View { + private let clearButton = WidgetButton() + private let storageTitle = TextView() + private let progressIndicator: WidgetStorageProgress = WidgetStorageProgress(frame: .zero) + private let storageDesc = TextView() + + var clearAll:(()->Void)? = nil + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(clearButton) + addSubview(storageTitle) + addSubview(progressIndicator) + addSubview(storageDesc) + storageTitle.userInteractionEnabled = false + storageTitle.isSelectable = false + + storageDesc.userInteractionEnabled = false + storageDesc.isSelectable = false + + clearButton.set(handler: { [weak self] _ in + self?.clearAll?() + }, for: .Click) + } + private var state: WidgetStorageController.State? + + private let progressDisposable = MetaDisposable() + + func update(_ state: WidgetStorageController.State, animated: Bool) { + + let progress = state.progressValues + progressIndicator.setProgress(progress.values, tooltips: progress.tooltips) + + self.state = state + updateLocalizationAndTheme(theme: theme) + + clearButton.userInteractionEnabled = state.diskSpace.app != nil + clearButton.change(opacity: state.diskSpace.app == nil ? 0.5 : 1, animated: animated) + } + + deinit { + progressDisposable.dispose() + } + + override func layout() { + super.layout() + progressIndicator.setFrameSize(NSMakeSize(frame.width, 32)) + clearButton.centerX(y: frame.height - clearButton.frame.height) + progressIndicator.centerX(y: storageTitle.frame.maxY + 20) + + + storageTitle.resize(frame.width - 20) + storageTitle.centerX() + + + storageDesc.resize(frame.width - 20) + storageDesc.centerX(y: progressIndicator.frame.maxY + 16) + + + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + + let theme = theme as! TelegramPresentationTheme + + clearButton.update(false, icon: theme.icons.empty_chat_storage_clear, text: L10n.emptyChatStorageUsageClear) + clearButton.setFrameSize(clearButton.size()) + + let titleLayout = TextViewLayout.init(.initialize(string: L10n.emptyChatStorageUsage, color: theme.colors.text, font: .medium(.text))) + titleLayout.measure(width: frame.width - 20) + storageTitle.update(titleLayout) + + let descAttr = NSMutableAttributedString() + if let _ = state?.ccTask { + descAttr.append(.initialize(string: L10n.emptyChatStorageUsageClearing, color: theme.colors.grayText, font: .normal(.text))) + } else if let totalBytes = state?.diskSpace.app { + let text = totalBytes == 0 ? L10n.emptyChatStorageUsageCacheDescEmpty : L10n.emptyChatStorageUsageCacheDesc(String.prettySized(with: Int(totalBytes))) + descAttr.append(.initialize(string: text, color: theme.colors.grayText, font: .normal(.text))) + } else { + descAttr.append(.initialize(string: L10n.emptyChatStorageUsageLoading, color: theme.colors.grayText, font: .normal(.text))) + } + descAttr.detectBoldColorInString(with: .medium(.text)) + let descLayout = TextViewLayout(descAttr) + descLayout.measure(width: frame.width - 20) + storageDesc.update(descLayout) + + + needsLayout = true + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +final class WidgetStorageController : TelegramGenericViewController> { + + struct State : Equatable { + + struct DiskSpace : Equatable { + var free: UInt64 + var total: UInt64 + var app: UInt64? + } + + var ccTask: (CCTaskData, Float)? + var settings: CacheStorageSettings + var diskSpace: DiskSpace + static func ==(lhs:State, rhs: State) -> Bool { + return lhs.diskSpace == rhs.diskSpace && lhs.ccTask?.0 == rhs.ccTask?.0 && lhs.settings == rhs.settings + } + + var progressValues:(values: [Int: CGFloat], tooltips: [Int: String]) { + if let usageBytes = diskSpace.app, diskSpace.total > 0 { + + let appUsageBytes = usageBytes // UInt64(10 * 1024 * 1024 * 1024) + + + + let systemTotalBytes = diskSpace.total * 1024 * 1024 * 1024 + let systemFreeBytes = diskSpace.free * 1024 * 1024 * 1024 + let systemUsedBytes = systemTotalBytes - systemFreeBytes + + + let systemUsedValue = CGFloat(systemUsedBytes - appUsageBytes) / CGFloat(systemTotalBytes) + let appUsedValue = CGFloat(systemUsedBytes) / CGFloat(systemTotalBytes) + + + var values:[Int: CGFloat] = [0 : systemUsedValue, 1: appUsedValue] + + if let ccTask = ccTask { + values[1] = (CGFloat(systemUsedBytes) - CGFloat(appUsageBytes) * CGFloat(ccTask.1)) / CGFloat(systemTotalBytes) + } + + let systemText = String.prettySized(with: Int(systemUsedBytes - appUsageBytes)) + let appText = String.prettySized(with: Int(appUsageBytes)) + + let tooltips:[Int: String] = [0 : L10n.emptyChatStorageUsageTooltipSystem(systemText), 1: L10n.emptyChatStorageUsageTooltipApp(appText)] + + return (values: values, tooltips: tooltips) + } else { + return (values: [:], tooltips: [:]) + } + } + + enum NetworkPreset : Int { + case low + case normal + } + + } + + private let disposable = MetaDisposable() + private let actionsDisposable = DisposableSet() + override init(_ context: AccountContext) { + super.init(context) + self.bar = .init(height: 0) + } + + override func viewDidLoad() { + super.viewDidLoad() + + let initialState = State(settings: .defaultSettings, diskSpace: .init(free: 0, total: 0, app: 0)) + + let statePromise = ValuePromise(initialState, ignoreRepeated: true) + let stateValue = Atomic(value: initialState) + let updateState: ((State) -> State) -> Void = { f in + statePromise.set(stateValue.modify (f)) + } + + let mediaPath = context.account.postbox.mediaBox.basePath + + var diskSpaceUpdater:Signal = Signal { subscriber in + + let systemFree = freeSystemGigabytes() ?? 0 + let systemSize = systemSizeGigabytes() ?? 0 + var totalSize: UInt64 = 0 + scanFiles(at: mediaPath, anyway: { file, fs in + totalSize += UInt64(fs) + }) + subscriber.putNext(.init(free: systemFree, total: systemSize, app: totalSize)) + subscriber.putCompletion() + + return EmptyDisposable + } |> runOn(.concurrentDefaultQueue()) + + + diskSpaceUpdater = (diskSpaceUpdater |> then(.complete() |> suspendAwareDelay(60.0 * 30, queue: Queue.concurrentDefaultQueue()))) |> restart + + let diskUpdater:Promise = Promise() + + diskUpdater.set(.single(.init(free: 0, total: 0, app: nil)) |> then(diskSpaceUpdater)) + + actionsDisposable.add(diskUpdater.get().start(next: { diskSpace in + updateState { current in + var current = current + current.diskSpace = diskSpace + return current + } + })) + + + let cacheSettingsPromise = Promise() + cacheSettingsPromise.set(context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.cacheStorageSettings]) + |> map { view -> CacheStorageSettings in + return view.entries[SharedDataKeys.cacheStorageSettings] as? CacheStorageSettings ?? CacheStorageSettings.defaultSettings + }) + + + + let taskAndProgress:Signal<(CCTaskData, Float)?, NoError> = context.cacheCleaner.task |> mapToSignal { task in + if let task = task { + return task.progress |> map { + return (task, $0) + } + } else { + return .single(nil) + } + } + + + let signal = combineLatest(queue: .mainQueue(), cacheSettingsPromise.get(), taskAndProgress, appearanceSignal) + + actionsDisposable.add(signal.start(next: { settings, ccTask, appearance in + if ccTask == nil && stateValue.with({ $0.ccTask != nil }) { + DispatchQueue.main.async { + diskUpdater.set(diskSpaceUpdater) + } + } + updateState { current in + var current = current + current.settings = settings + current.ccTask = ccTask + return current + } + })) + + let context = self.context + + genericView.dataView = WidgetStorageContainer(frame: .zero) + + + + + genericView.dataView?.clearAll = { + confirm(for: context.window, information: L10n.storageClearAllConfirmDescription, okTitle: L10n.storageClearAll, successHandler: { _ in + context.cacheCleaner.run() + }) + } + + var first = true + + disposable.set((statePromise.get() |> deliverOnMainQueue).start(next: { [weak self] state in + + var buttons: [WidgetData.Button] = [] + + let lowIsSelected = state.settings.defaultCacheStorageLimitGigabytes == 5 + let normalIsSelected = state.settings.defaultCacheStorageLimitGigabytes == 32 + let highIsSelected = state.settings.defaultCacheStorageLimitGigabytes == .max + + buttons.append(.init(text: { L10n.emptyChatStorageUsageLow }, selected: { + return lowIsSelected + }, image: { + return lowIsSelected ? theme.icons.empty_chat_storage_low_active : theme.icons.empty_chat_storage_low + }, click: { + _ = updateCacheStorageSettingsInteractively(accountManager: context.sharedContext.accountManager, { + $0.withUpdatedDefaultCacheStorageLimitGigabytes(5) + }).start() + })) + + buttons.append(.init(text: { L10n.emptyChatStorageUsageMedium }, selected: { + return normalIsSelected + }, image: { + return normalIsSelected ? theme.icons.empty_chat_storage_medium_active : theme.icons.empty_chat_storage_medium + }, click: { + _ = updateCacheStorageSettingsInteractively(accountManager: context.sharedContext.accountManager, { + $0.withUpdatedDefaultCacheStorageLimitGigabytes(32) + }).start() + })) + + buttons.append(.init(text: { L10n.emptyChatStorageUsageNoLimit }, selected: { + return highIsSelected + }, image: { + return highIsSelected ? theme.icons.empty_chat_storage_high_active : theme.icons.empty_chat_storage_high + }, click: { + _ = updateCacheStorageSettingsInteractively(accountManager: context.sharedContext.accountManager, { + $0.withUpdatedDefaultCacheStorageLimitGigabytes(.max) + }).start() + })) + + let data: WidgetData = .init(title: { L10n.emptyChatStorageUsageData }, desc: { L10n.emptyChatStorageUsageDesc }, descClick: { + context.sharedContext.bindings.rootNavigation().push(DataAndStorageViewController(context)) + }, buttons: buttons) + + self?.genericView.update(data) + self?.genericView.dataView?.update(state, animated: !first) + first = false + })) + + } + + deinit { + actionsDisposable.dispose() + disposable.dispose() + } +} diff --git a/Telegram-Mac/WidgetView.swift b/Telegram-Mac/WidgetView.swift new file mode 100644 index 0000000000..e0ea2e7bc1 --- /dev/null +++ b/Telegram-Mac/WidgetView.swift @@ -0,0 +1,138 @@ +// +// WidgetView.swift +// Telegram +// +// Created by Mikhail Filimonov on 08.07.2021. +// Copyright © 2021 Telegram. All rights reserved. +// + +import Foundation +import TGUIKit + + +struct WidgetData { + struct Button { + var text: ()->String + var selected: ()->Bool + var image: ()->CGImage + var click:()->Void + } + + var title: ()->String + var desc: ()->String + var descClick:()->Void + var buttons:[Button] + var contentHeight: CGFloat? = nil +} + +final class WidgetView : View where T:View { + + private let titleView = TextView() + private let descView = TextView() + + + var dataView: T? { + didSet { + oldValue?.removeFromSuperview() + if let dataView = self.dataView { + addSubview(dataView) + } + layout() + } + } + private let buttonsView = View() + + required init(frame frameRect: NSRect) { + super.init(frame: frameRect) + addSubview(titleView) + addSubview(buttonsView) + addSubview(descView) + + layer?.cornerRadius = 20 + + titleView.userInteractionEnabled = false + titleView.isSelectable = false + + descView.userInteractionEnabled = true + descView.isSelectable = false + } + + fileprivate var data: WidgetData? + func update(_ data: WidgetData) { + self.data = data + + buttonsView.removeAllSubviews() + for data in data.buttons { + let button = WidgetButton() + buttonsView.addSubview(button) + button.set(handler: { _ in + data.click() + }, for: .Click) + } + updateLocalizationAndTheme(theme: theme) + needsLayout = true + } + + override func updateLocalizationAndTheme(theme: PresentationTheme) { + super.updateLocalizationAndTheme(theme: theme) + + + self.backgroundColor = theme.colors.background + + + + guard let data = self.data else { + return + } + + for (i, buttonData) in data.buttons.enumerated() { + let button = self.buttonsView.subviews[i] as! WidgetButton + button.update(buttonData.selected(), icon: buttonData.image(), text: buttonData.text()) + } + + let descAttr = parseMarkdownIntoAttributedString(data.desc(), attributes: MarkdownAttributes(body: MarkdownAttributeSet(font: .normal(.text), textColor: theme.colors.grayText), bold: MarkdownAttributeSet(font: .bold(.text), textColor: theme.colors.grayText), link: MarkdownAttributeSet(font: .normal(.text), textColor: theme.colors.link), linkAttribute: { contents in + return (NSAttributedString.Key.link.rawValue, inAppLink.callback(contents, {_ in})) + })) + + let descLayout = TextViewLayout(descAttr, alignment: .center) + + descLayout.interactions = TextViewInteractions(processURL:{ _ in + data.descClick() + }) + + descLayout.measure(width: frame.width - 30) + + descView.update(descLayout) + + let titleLayout = TextViewLayout(.initialize(string: data.title(), color: theme.colors.text, font: .medium(.text))) + titleLayout.measure(width: frame.width - 30) + titleView.update(titleLayout) + } + + override func layout() { + super.layout() + + titleView.centerX(y: 18) + + if buttonsView.subviews.isEmpty { + dataView?.frame = NSMakeRect(15, 53, frame.width - 30, 144 + 50) + } else { + dataView?.frame = NSMakeRect(15, 103, frame.width - 30, self.data?.contentHeight ?? 144) + } + + buttonsView.frame = NSMakeRect(15, 53, frame.width - 30, 30) + + let buttonsCount = CGFloat(buttonsView.subviews.count) + let bestSize = (buttonsView.frame.width - 10 * (buttonsCount - 1)) / buttonsCount + + for (i, button) in buttonsView.subviews.enumerated() { + let index: CGFloat = CGFloat(i) + button.frame = NSMakeRect(index * bestSize + 10 * index, 0, bestSize, 30) + } + descView.centerX(y: frame.height - descView.frame.height - 20) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Telegram-Mac/builtin-wallpaper-svg b/Telegram-Mac/builtin-wallpaper-svg new file mode 100644 index 0000000000..a106f042d6 Binary files /dev/null and b/Telegram-Mac/builtin-wallpaper-svg differ diff --git a/Telegram-Mac/countries b/Telegram-Mac/countries new file mode 100644 index 0000000000..ff8d443b15 --- /dev/null +++ b/Telegram-Mac/countries @@ -0,0 +1,232 @@ +1876;JM;Jamaica;JAM +1869;KN;Saint Kitts & Nevis;KNA +1868;TT;Trinidad & Tobago;TTO +1784;VC;Saint Vincent & the Grenadines;VCT +1767;DM;Dominica;DMA +1758;LC;Saint Lucia;LCA +1721;SX;Sint Maarten;SXM +1684;AS;American Samoa;ASM +1671;GU;Guam;GUM +1670;MP;Northern Mariana Islands;MNP +1664;MS;Montserrat;MSR +1649;TC;Turks & Caicos Islands;TCA +1473;GD;Grenada;GRD +1441;BM;Bermuda;BMU +1345;KY;Cayman Islands;CYM +1340;VI;US Virgin Islands;VIR +1284;VG;British Virgin Islands;VGB +1268;AG;Antigua & Barbuda;ATG +1264;AI;Anguilla;AIA +1246;BB;Barbados;BRB +1242;BS;Bahamas;BHS +998;UZ;Uzbekistan;UZB +996;KG;Kyrgyzstan;KGZ +995;GE;Georgia;GEO +994;AZ;Azerbaijan;AZE +993;TM;Turkmenistan;TKM +992;TJ;Tajikistan;TJK +977;NP;Nepal;NPL +976;MN;Mongolia;MNG +975;BT;Bhutan;BTN +974;QA;Qatar;QAT +973;BH;Bahrain;BHR +972;IL;Israel;ISR +971;AE;United Arab Emirates;ARE +970;PS;Palestine;PSE +968;OM;Oman;OMN +967;YE;Yemen;YEM +966;SA;Saudi Arabia;SAU +965;KW;Kuwait;KWT +964;IQ;Iraq;IRQ +963;SY;Syrian Arab Republic;SYR +962;JO;Jordan;JOR +961;LB;Lebanon;LBN +960;MV;Maldives;MDV +886;TW;Taiwan;TWN +880;BD;Bangladesh;BGD +856;LA;Laos;LAO +855;KH;Cambodia;KHM +853;MO;Macau;MAC +852;HK;Hong Kong;HKG +850;KP;North Korea;PRK +692;MH;Marshall Islands;MHL +691;FM;Micronesia;FSM +690;TK;Tokelau;TKL +689;PF;French Polynesia;PYF +688;TV;Tuvalu;TUV +687;NC;New Caledonia;NCL +686;KI;Kiribati;KIR +685;WS;Samoa;WSM +683;NU;Niue;NIU +682;CK;Cook Islands;COK +681;WF;Wallis & Futuna;WLF +680;PW;Palau;PLW +679;FJ;Fiji;FJI +678;VU;Vanuatu;VUT +677;SB;Solomon Islands;SLB +676;TO;Tonga;TON +675;PG;Papua New Guinea;PNG +674;NR;Nauru;NRU +673;BN;Brunei Darussalam;BRN +672;NF;Norfolk Island;NFK +670;TL;Timor-Leste;TLS +599;BQ;Bonaire, Sint Eustatius & Saba;BES +599;CW;Curaçao;CUW +598;UY;Uruguay;URY +597;SR;Suriname;SUR +596;MQ;Martinique;MTQ +595;PY;Paraguay;PRY +594;GF;French Guiana;GUF +593;EC;Ecuador;ECU +592;GY;Guyana;GUY +591;BO;Bolivia;BOL +590;GP;Guadeloupe;GLP +509;HT;Haiti;HTI +508;PM;Saint Pierre & Miquelon;SPM +507;PA;Panama;PAN +506;CR;Costa Rica;CRI +505;NI;Nicaragua;NIC +504;HN;Honduras;HND +503;SV;El Salvador;SLV +502;GT;Guatemala;GTM +501;BZ;Belize;BLZ +500;FK;Falkland Islands;FLK +423;LI;Liechtenstein;LIE +421;SK;Slovakia;SVK +420;CZ;Czech Republic;CZE +383;XK;Kosovo;UNK +389;MK;Macedonia;MKD +387;BA;Bosnia & Herzegovina;BIH +386;SI;Slovenia;SVN +385;HR;Croatia;HRV +382;ME;Montenegro;MNE +381;RS;Serbia;SRB +380;UA;Ukraine;UKR +378;SM;San Marino;SMR +377;MC;Monaco;MCO +376;AD;Andorra;AND +375;BY;Belarus;BLR +374;AM;Armenia;ARM +373;MD;Moldova;MDA +372;EE;Estonia;EST +371;LV;Latvia;LVA +370;LT;Lithuania;LTU +359;BG;Bulgaria;BGR +358;FI;Finland;FIN +357;CY;Cyprus;CYP +356;MT;Malta;MLT +355;AL;Albania;ALB +354;IS;Iceland;ISL +353;IE;Ireland;IRL +352;LU;Luxembourg;LUX +351;PT;Portugal;PRT +350;GI;Gibraltar;GIB +299;GL;Greenland;GRL +298;FO;Faroe Islands;FRO +297;AW;Aruba;ABW +291;ER;Eritrea;ERI +290;SH;Saint Helena;SHN +269;KM;Comoros;COM +268;SZ;Swaziland;SWZ +267;BW;Botswana;BWA +266;LS;Lesotho;LSO +265;MW;Malawi;MWI +264;NA;Namibia;NAM +263;ZW;Zimbabwe;ZWE +262;RE;Réunion;REU +261;MG;Madagascar;MDG +260;ZM;Zambia;ZMB +258;MZ;Mozambique;MOZ +257;BI;Burundi;BDI +256;UG;Uganda;UGA +255;TZ;Tanzania;TZA +254;KE;Kenya;KEN +253;DJ;Djibouti;DJI +252;SO;Somalia;SOM +251;ET;Ethiopia;ETH +250;RW;Rwanda;RWA +249;SD;Sudan;SDN +248;SC;Seychelles;SYC +247;SH;Saint Helena;SHN +246;IO;Diego Garcia;DGA +245;GW;Guinea-Bissau;GNB +244;AO;Angola;AGO +243;CD;Congo (Dem. Rep.);COD +242;CG;Congo (Rep.);COG +241;GA;Gabon;GAB +240;GQ;Equatorial Guinea;GNQ +239;ST;São Tomé & Príncipe;STP +238;CV;Cape Verde;CPV +237;CM;Cameroon;CMR +236;CF;Central African Rep.;CAF +235;TD;Chad;TCD +234;NG;Nigeria;NGA +233;GH;Ghana;GHA +232;SL;Sierra Leone;SLE +231;LR;Liberia;LBR +230;MU;Mauritius;MUS +229;BJ;Benin;BEN +228;TG;Togo;TGO +227;NE;Niger;NER +226;BF;Burkina Faso;BFA +225;CI;Côte d`Ivoire;CIV +224;GN;Guinea;GIN +223;ML;Mali;MLI +222;MR;Mauritania;MRT +221;SN;Senegal;SEN +220;GM;Gambia;GMB +218;LY;Libya;LBY +216;TN;Tunisia;TUN +213;DZ;Algeria;DZA +212;MA;Morocco;MAR +211;SS;South Sudan;SSD +98;IR;Iran;IRN +95;MM;Myanmar;MMR +94;LK;Sri Lanka;LKA +93;AF;Afghanistan;AFG +92;PK;Pakistan;PAK +91;IN;India;IND +90;TR;Turkey;TUR +86;CN;China;CHN +84;VN;Vietnam;VNM +82;KR;South Korea;KOR +81;JP;Japan;JPN +66;TH;Thailand;THA +65;SG;Singapore;SGP +64;NZ;New Zealand;NZL +63;PH;Philippines;PHL +62;ID;Indonesia;IDN +61;AU;Australia;AUS +60;MY;Malaysia;MYS +58;VE;Venezuela;VEN +57;CO;Colombia;COL +56;CL;Chile;CHL +55;BR;Brazil;BRA +54;AR;Argentina;ARG +53;CU;Cuba;CUB +52;MX;Mexico;MEX +51;PE;Peru;PER +49;DE;Germany;DEU +48;PL;Poland;POL +47;NO;Norway;NOR +46;SE;Sweden;SWE +45;DK;Denmark;DNK +44;GB;United Kingdom;GBR,GBD,GBN,GBO,GBP,GBS +43;AT;Austria;AUT +41;CH;Switzerland;CHE +40;RO;Romania;ROM +39;IT;Italy;ITA +36;HU;Hungary;HUN +34;ES;Spain;ESP +33;FR;France;FRA +32;BE;Belgium;BEL +31;NL;Netherlands;NLD +30;GR;Greece;GRC +27;ZA;South Africa;ZAF +20;EG;Egypt;EGY +7;RU;Russian Federation;RUS +7;KZ;Kazakhstan;KAZ +1;US;USA;USA,UMI +1;PR;Puerto Rico;PRI +1;DO;Dominican Rep.;DOM +1;CA;Canada;CAN diff --git a/Telegram-Mac/currencies.json b/Telegram-Mac/currencies.json index 11f696636a..be114e2c07 100644 --- a/Telegram-Mac/currencies.json +++ b/Telegram-Mac/currencies.json @@ -1356,7 +1356,7 @@ "decimalSeparator": ",", "symbolOnLeft": false, "spaceBetweenAmountAndSymbol": true, - "decimalDigits": 1 + "decimalDigits": 0 }, "VUV": { "code": "VUV", diff --git a/Telegram-Mac/de.lproj/MainMenu.xib b/Telegram-Mac/de.lproj/MainMenu.xib new file mode 100644 index 0000000000..57e428c68e --- /dev/null +++ b/Telegram-Mac/de.lproj/MainMenu.xib @@ -0,0 +1,310 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Telegram-Mac/emoji1014-1.txt b/Telegram-Mac/emoji1014-1.txt new file mode 100644 index 0000000000..8c9eb95899 --- /dev/null +++ b/Telegram-Mac/emoji1014-1.txt @@ -0,0 +1,15 @@ +😀 😁 😂 🤣 😃 😄 😅 😆 😉 😊 😋 😎 😍 😘 🥰 😗 😙 😚 ☺️ 🙂 🤗 🤩 🤔 🤨 😐 😑 😶 🙄 😏 😣 😥 😮 🤐 😯 😪 😫 😴 😌 😛 😜 😝 🤤 😒 😓 😔 😕 🙃 🤑 😲 ☹️ 🙁 😖 😞 😟 😤 😢 😭 😦 😧 😨 😩 🤯 😬 😰 😱 🥵 🥶 😳 🤪 😵 😡 😠 🤬 😷 🤒 🤕 🤢 🤮 🤧 😇 🤠 🤡 🥳 🥴 🥺 🤥 🤫 🤭 🧐 🤓 😈 👿 👹 👺 💀 👻 👽 🤖 💩 😺 😸 😹 😻 😼 😽 🙀 😿 😾 👶 👧 🧒 👦 👩 🧑 👨 👵 🧓 👴 👲 👳‍♀️ 👳‍♂️ 🧕 🧔 👱‍♂️ 👱‍♀️ 👨‍🦰 👩‍🦰 👨‍🦱 👩‍🦱 👨‍🦲 👩‍🦲 👨‍🦳 👩‍🦳 🦸‍♀️ 🦸‍♂️ 🦹‍♀️ 🦹‍♂️ 👮‍♀️ 👮‍♂️ 👷‍♀️ 👷‍♂️ 💂‍♀️ 💂‍♂️ 🕵️‍♀️ 🕵️‍♂️ 👩‍⚕️ 👨‍⚕️ 👩‍🌾 👨‍🌾 👩‍🍳 👨‍🍳 👩‍🎓 👨‍🎓 👩‍🎤 👨‍🎤 👩‍🏫 👨‍🏫 👩‍🏭 👨‍🏭 👩‍💻 👨‍💻 👩‍💼 👨‍💼 👩‍🔧 👨‍🔧 👩‍🔬 👨‍🔬 👩‍🎨 👨‍🎨 👩‍🚒 👨‍🚒 👩‍✈️ 👨‍✈️ 👩‍🚀 👨‍🚀 👩‍⚖️ 👨‍⚖️ 👰 🤵 👸 🤴 🤶 🎅 🧙‍♀️ 🧙‍♂️ 🧝‍♀️ 🧝‍♂️ 🧛‍♀️ 🧛‍♂️ 🧟‍♀️ 🧟‍♂️ 🧞‍♀️ 🧞‍♂️ 🧜‍♀️ 🧜‍♂️ 🧚‍♀️ 🧚‍♂️ 👼 🤰 🤱 🙇‍♀️ 🙇‍♂️ 💁‍♀️ 💁‍♂️ 🙅‍♀️ 🙅‍♂️ 🙆‍♀️ 🙆‍♂️ 🙋‍♀️ 🙋‍♂️ 🤦‍♀️ 🤦‍♂️ 🤷‍♀️ 🤷‍♂️ 🙎‍♀️ 🙎‍♂️ 🙍‍♀️ 🙍‍♂️ 💇‍♀️ 💇‍♂️ 💆‍♀️ 💆‍♂️ 🧖‍♀️ 🧖‍♂️ 💅 🤳 💃 🕺 👯‍♀️ 👯‍♂️ 🕴 🚶‍♀️ 🚶‍♂️ 🏃‍♀️ 🏃‍♂️ 👫 👭 👬 💑 👩‍❤️‍👩 👨‍❤️‍👨 💏 👩‍❤️‍💋‍👩 👨‍❤️‍💋‍👨 👪 👨‍👩‍👧 👨‍👩‍👧‍👦 👨‍👩‍👦‍👦 👨‍👩‍👧‍👧 👩‍👩‍👦 👩‍👩‍👧 👩‍👩‍👧‍👦 👩‍👩‍👦‍👦 👩‍👩‍👧‍👧 👨‍👨‍👦 👨‍👨‍👧 👨‍👨‍👧‍👦 👨‍👨‍👦‍👦 👨‍👨‍👧‍👧 👩‍👦 👩‍👧 👩‍👧‍👦 👩‍👦‍👦 👩‍👧‍👧 👨‍👦 👨‍👧 👨‍👧‍👦 👨‍👦‍👦 👨‍👧‍👧 🤲 👐 🙌 👏 🤝 👍 👎 👊 ✊ 🤛 🤜 🤞 ✌️ 🤟 🤘 👌 👈 👉 👆 👇 ☝️ ✋ 🤚 🖐 🖖 👋 🤙 💪 🦵 🦶 🖕 ✍️ 🙏 💍 💄 💋 👄 👅 👂 👃 👣 👁 👀 🧠 🦴 🦷 🗣 👤 👥 + +🐶 🐱 🐭 🐹 🐰 🦊 🦝 🐻 🐼 🦘 🦡 🐨 🐯 🦁 🐮 🐷 🐽 🐸 🐵 🙈 🙉 🙊 🐒 🐔 🐧 🐦 🐤 🐣 🐥 🦆 🦢 🦅 🦉 🦚 🦜 🦇 🐺 🐗 🐴 🦄 🐝 🐛 🦋 🐌 🐚 🐞 🐜 🦗 🕷 🕸 🦂 🦟 🦠 🐢 🐍 🦎 🦖 🦕 🐙 🦑 🦐 🦀 🐡 🐠 🐟 🐬 🐳 🐋 🦈 🐊 🐅 🐆 🦓 🦍 🐘 🦏 🦛 🐪 🐫 🦙 🦒 🐃 🐂 🐄 🐎 🐖 🐏 🐑 🐐 🦌 🐕 🐩 🐈 🐓 🦃 🕊 🐇 🐁 🐀 🐿 🦔 🐾 🐉 🐲 🌵 🎄 🌲 🌳 🌴 🌱 🌿 ☘️ 🍀 🎍 🎋 🍃 🍂 🍁 🍄 🌾 💐 🌷 🌹 🥀 🌺 🌸 🌼 🌻 🌞 🌝 🌛 🌜 🌚 🌕 🌖 🌗 🌘 🌑 🌒 🌓 🌔 🌙 🌎 🌍 🌏 💫 ⭐️ 🌟 ✨ ⚡️ ☄️ 💥 🔥 🌪 🌈 ☀️ 🌤 ⛅️ 🌥 ☁️ 🌦 🌧 ⛈ 🌩 🌨 ❄️ ☃️ ⛄️ 🌬 💨 💧 💦 ☔️ ☂️ 🌊 🌫 + +🍏 🍎 🍐 🍊 🍋 🍌 🍉 🍇 🍓 🍈 🍒 🍑 🍍 🥭 🥥 🥝 🍅 🍆 🥑 🥦 🥒 🥬 🌶 🌽 🥕 🥔 🍠 🥐 🍞 🥖 🥨 🥯 🧀 🥚 🍳 🥞 🥓 🥩 🍗 🍖 🌭 🍔 🍟 🍕 🥪 🥙 🌮 🌯 🥗 🥘 🥫 🍝 🍜 🍲 🍛 🍣 🍱 🥟 🍤 🍙 🍚 🍘 🍥 🥮 🥠 🍢 🍡 🍧 🍨 🍦 🥧 🍰 🎂 🍮 🍭 🍬 🍫 🍿 🧂 🍩 🍪 🌰 🥜 🍯 🥛 🍼 ☕️ 🍵 🥤 🍶 🍺 🍻 🥂 🍷 🥃 🍸 🍹 🍾 🥄 🍴 🍽 🥣 🥡 🥢 + +⚽️ 🏀 🏈 ⚾️ 🥎 🏐 🏉 🎾 🥏 🎱 🏓 🏸 🥅 🏒 🏑 🥍 🏏 ⛳️ 🏹 🎣 🥊 🥋 🎽 ⛸ 🥌 🛷 🛹 🎿 ⛷ 🏂 🏋️‍♀️ 🏋🏻‍♀️ 🏋🏼‍♀️ 🏋🏽‍♀️ 🏋🏾‍♀️ 🏋🏿‍♀️ 🏋️‍♂️ 🏋🏻‍♂️ 🏋🏼‍♂️ 🏋🏽‍♂️ 🏋🏾‍♂️ 🏋🏿‍♂️ 🤼‍♀️ 🤼‍♂️ 🤸‍♀️ 🤸🏻‍♀️ 🤸🏼‍♀️ 🤸🏽‍♀️ 🤸🏾‍♀️ 🤸🏿‍♀️ 🤸‍♂️ 🤸🏻‍♂️ 🤸🏼‍♂️ 🤸🏽‍♂️ 🤸🏾‍♂️ 🤸🏿‍♂️ ⛹️‍♀️ ⛹🏻‍♀️ ⛹🏼‍♀️ ⛹🏽‍♀️ ⛹🏾‍♀️ ⛹🏿‍♀️ ⛹️‍♂️ ⛹🏻‍♂️ ⛹🏼‍♂️ ⛹🏽‍♂️ ⛹🏾‍♂️ ⛹🏿‍♂️ 🤺 🤾‍♀️ 🤾🏻‍♀️ 🤾🏼‍♀️ 🤾🏾‍♀️ 🤾🏾‍♀️ 🤾🏿‍♀️ 🤾‍♂️ 🤾🏻‍♂️ 🤾🏼‍♂️ 🤾🏽‍♂️ 🤾🏾‍♂️ 🤾🏿‍♂️ 🏌️‍♀️ 🏌🏻‍♀️ 🏌🏼‍♀️ 🏌🏽‍♀️ 🏌🏾‍♀️ 🏌🏿‍♀️ 🏌️‍♂️ 🏌🏻‍♂️ 🏌🏼‍♂️ 🏌🏽‍♂️ 🏌🏾‍♂️ 🏌🏿‍♂️ 🏇 🏇🏻 🏇🏼 🏇🏽 🏇🏾 🏇🏿 🧘‍♀️ 🧘🏻‍♀️ 🧘🏼‍♀️ 🧘🏽‍♀️ 🧘🏾‍♀️ 🧘🏿‍♀️ 🧘‍♂️ 🧘🏻‍♂️ 🧘🏼‍♂️ 🧘🏽‍♂️ 🧘🏾‍♂️ 🧘🏿‍♂️ 🏄‍♀️ 🏄🏻‍♀️ 🏄🏼‍♀️ 🏄🏽‍♀️ 🏄🏾‍♀️ 🏄🏿‍♀️ 🏄‍♂️ 🏄🏻‍♂️ 🏄🏼‍♂️ 🏄🏽‍♂️ 🏄🏾‍♂️ 🏄🏿‍♂️ 🏊‍♀️ 🏊🏻‍♀️ 🏊🏼‍♀️ 🏊🏽‍♀️ 🏊🏾‍♀️ 🏊🏿‍♀️ 🏊‍♂️ 🏊🏻‍♂️ 🏊🏼‍♂️ 🏊🏽‍♂️ 🏊🏾‍♂️ 🏊🏿‍♂️ 🤽‍♀️ 🤽🏻‍♀️ 🤽🏼‍♀️ 🤽🏽‍♀️ 🤽🏾‍♀️ 🤽🏿‍♀️ 🤽‍♂️ 🤽🏻‍♂️ 🤽🏼‍♂️ 🤽🏽‍♂️ 🤽🏾‍♂️ 🤽🏿‍♂️ 🚣‍♀️ 🚣🏻‍♀️ 🚣🏼‍♀️ 🚣🏽‍♀️ 🚣🏾‍♀️ 🚣🏿‍♀️ 🚣‍♂️ 🚣🏻‍♂️ 🚣🏼‍♂️ 🚣🏽‍♂️ 🚣🏾‍♂️ 🚣🏿‍♂️ 🧗‍♀️ 🧗🏻‍♀️ 🧗🏼‍♀️ 🧗🏽‍♀️ 🧗🏾‍♀️ 🧗🏿‍♀️ 🧗‍♂️ 🧗🏻‍♂️ 🧗🏼‍♂️ 🧗🏽‍♂️ 🧗🏾‍♂️ 🧗🏿‍♂️ 🚵‍♀️ 🚵🏻‍♀️ 🚵🏼‍♀️ 🚵🏽‍♀️ 🚵🏾‍♀️ 🚵🏿‍♀️ 🚵‍♂️ 🚵🏻‍♂️ 🚵🏼‍♂️ 🚵🏽‍♂️ 🚵🏾‍♂️ 🚵🏿‍♂️ 🚴‍♀️ 🚴🏻‍♀️ 🚴🏼‍♀️ 🚴🏽‍♀️ 🚴🏾‍♀️ 🚴🏿‍♀️ 🚴‍♂️ 🚴🏻‍♂️ 🚴🏼‍♂️ 🚴🏽‍♂️ 🚴🏾‍♂️ 🚴🏿‍♂️ 🏆 🥇 🥈 🥉 🏅 🎖 🏵 🎗 🎫 🎟 🎪 🤹‍♀️ 🤹🏻‍♀️ 🤹🏼‍♀️ 🤹🏽‍♀️ 🤹🏾‍♀️ 🤹🏿‍♀️ 🤹‍♂️ 🤹🏻‍♂️ 🤹🏼‍♂️ 🤹🏽‍♂️ 🤹🏾‍♂️ 🤹🏿‍♂️ 🎭 🎨 🎬 🎤 🎧 🎼 🎹 🥁 🎷 🎺 🎸 🎻 🎲 🧩 ♟ 🎯 🎳 🎮 🎰 + +🚗 🚕 🚙 🚌 🚎 🏎 🚓 🚑 🚒 🚐 🚚 🚛 🚜 🛴 🚲 🛵 🏍 🚨 🚔 🚍 🚘 🚖 🚡 🚠 🚟 🚃 🚋 🚞 🚝 🚄 🚅 🚈 🚂 🚆 🚇 🚊 🚉 ✈️ 🛫 🛬 🛩 💺 🛰 🚀 🛸 🚁 🛶 ⛵️ 🚤 🛥 🛳 ⛴ 🚢 ⚓️ ⛽️ 🚧 🚦 🚥 🚏 🗺 🗿 🗽 🗼 🏰 🏯 🏟 🎡 🎢 🎠 ⛲️ ⛱ 🏖 🏝 🏜 🌋 ⛰ 🏔 🗻 🏕 ⛺️ 🏠 🏡 🏘 🏚 🏗 🏭 🏢 🏬 🏣 🏤 🏥 🏦 🏨 🏪 🏫 🏩 💒 🏛 ⛪️ 🕌 🕍 🕋 ⛩ 🛤 🛣 🗾 🎑 🏞 🌅 🌄 🌠 🎇 🎆 🌇 🌆 🏙 🌃 🌌 🌉 🌁 + +⌚️ 📱 📲 💻 ⌨️ 🖥 🖨 🖱 🖲 🕹 🗜 💽 💾 💿 📀 📼 📷 📸 📹 🎥 📽 🎞 📞 ☎️ 📟 📠 📺 📻 🎙 🎚 🎛 ⏱ ⏲ ⏰ 🕰 ⌛️ ⏳ 📡 🔋 🔌 💡 🔦 🕯 🗑 🛢 💸 💵 💴 💶 💷 💰 💳 🧾 💎 ⚖️ 🔧 🔨 ⚒ 🛠 ⛏ 🔩 ⚙️ ⛓ 🔫 💣 🔪 🗡 ⚔️ 🛡 🚬 ⚰️ ⚱️ 🏺 🧭 🧱 🔮 🧿 🧸 📿 💈 ⚗️ 🔭 🧰 🧲 🧪 🧫 🧬 🧯 🔬 🕳 💊 💉 🌡 🚽 🚰 🚿 🛁 🛀 🛀🏻 🛀🏼 🛀🏽 🛀🏾 🛀🏿 🧴 🧵 🧶 🧷 🧹 🧺 🧻 🧼 🧽 🛎 🔑 🗝 🚪 🛋 🛏 🛌 🖼 🛍 🧳 🛒 🎁 🎈 🎏 🎀 🎊 🎉 🧨 🎎 🏮 🎐 🧧 ✉️ 📩 📨 📧 💌 📥 📤 📦 🏷 📪 📫 📬 📭 📮 📯 📜 📃 📄 📑 📊 📈 📉 🗒 🗓 📆 📅 📇 🗃 🗳 🗄 📋 📁 📂 🗂 🗞 📰 📓 📔 📒 📕 📗 📘 📙 📚 📖 🔖 🔗 📎 🖇 📐 📏 📌 📍 ✂️ 🖊 🖋 ✒️ 🖌 🖍 📝 ✏️ 🔍 🔎 🔏 🔐 🔒 🔓 + +❤️ 🧡 💛 💚 💙 💜 🖤 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ⏏️ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ ♾ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 🔜 ✔️ ☑️ 🔘 ⚪️ ⚫️ 🔴 🔵 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ ⬛️ ⬜️ 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 👁‍🗨 💬 💭 🗯 ♠️ ♣️ ♥️ ♦️ 🃏 🎴 🀄️ 🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛 🕜 🕝 🕞 🕟 🕠 🕡 🕢 🕣 🕤 🕥 🕦 🕧 + +🏳️ 🏴 🏁 🚩 🏳️‍🌈 🏴‍☠️ 🇦🇫 🇦🇽 🇦🇱 🇩🇿 🇦🇸 🇦🇩 🇦🇴 🇦🇮 🇦🇶 🇦🇬 🇦🇷 🇦🇲 🇦🇼 🇦🇺 🇦🇹 🇦🇿 🇧🇸 🇧🇭 🇧🇩 🇧🇧 🇧🇾 🇧🇪 🇧🇿 🇧🇯 🇧🇲 🇧🇹 🇧🇴 🇧🇦 🇧🇼 🇧🇷 🇮🇴 🇻🇬 🇧🇳 🇧🇬 🇧🇫 🇧🇮 🇰🇭 🇨🇲 🇨🇦 🇮🇨 🇨🇻 🇧🇶 🇰🇾 🇨🇫 🇹🇩 🇨🇱 🇨🇳 🇨🇽 🇨🇨 🇨🇴 🇰🇲 🇨🇬 🇨🇩 🇨🇰 🇨🇷 🇨🇮 🇭🇷 🇨🇺 🇨🇼 🇨🇾 🇨🇿 🇩🇰 🇩🇯 🇩🇲 🇩🇴 🇪🇨 🇪🇬 🇸🇻 🇬🇶 🇪🇷 🇪🇪 🇪🇹 🇪🇺 🇫🇰 🇫🇴 🇫🇯 🇫🇮 🇫🇷 🇬🇫 🇵🇫 🇹🇫 🇬🇦 🇬🇲 🇬🇪 🇩🇪 🇬🇭 🇬🇮 🇬🇷 🇬🇱 🇬🇩 🇬🇵 🇬🇺 🇬🇹 🇬🇬 🇬🇳 🇬🇼 🇬🇾 🇭🇹 🇭🇳 🇭🇰 🇭🇺 🇮🇸 🇮🇳 🇮🇩 🇮🇷 🇮🇶 🇮🇪 🇮🇲 🇮🇱 🇮🇹 🇯🇲 🇯🇵 🎌 🇯🇪 🇯🇴 🇰🇿 🇰🇪 🇰🇮 🇽🇰 🇰🇼 🇰🇬 🇱🇦 🇱🇻 🇱🇧 🇱🇸 🇱🇷 🇱🇾 🇱🇮 🇱🇹 🇱🇺 🇲🇴 🇲🇰 🇲🇬 🇲🇼 🇲🇾 🇲🇻 🇲🇱 🇲🇹 🇲🇭 🇲🇶 🇲🇷 🇲🇺 🇾🇹 🇲🇽 🇫🇲 🇲🇩 🇲🇨 🇲🇳 🇲🇪 🇲🇸 🇲🇦 🇲🇿 🇲🇲 🇳🇦 🇳🇷 🇳🇵 🇳🇱 🇳🇨 🇳🇿 🇳🇮 🇳🇪 🇳🇬 🇳🇺 🇳🇫 🇰🇵 🇲🇵 🇳🇴 🇴🇲 🇵🇰 🇵🇼 🇵🇸 🇵🇦 🇵🇬 🇵🇾 🇵🇪 🇵🇭 🇵🇳 🇵🇱 🇵🇹 🇵🇷 🇶🇦 🇷🇪 🇷🇴 🇷🇺 🇷🇼 🇼🇸 🇸🇲 🇸🇦 🇸🇳 🇷🇸 🇸🇨 🇸🇱 🇸🇬 🇸🇽 🇸🇰 🇸🇮 🇬🇸 🇸🇧 🇸🇴 🇿🇦 🇰🇷 🇸🇸 🇪🇸 🇱🇰 🇧🇱 🇸🇭 🇰🇳 🇱🇨 🇵🇲 🇻🇨 🇸🇩 🇸🇷 🇸🇿 🇸🇪 🇨🇭 🇸🇾 🇹🇼 🇹🇯 🇹🇿 🇹🇭 🇹🇱 🇹🇬 🇹🇰 🇹🇴 🇹🇹 🇹🇳 🇹🇷 🇹🇲 🇹🇨 🇹🇻 🇻🇮 🇺🇬 🇺🇦 🇦🇪 🇬🇧 🏴󠁧󠁢󠁥󠁮󠁧󠁿 🏴󠁧󠁢󠁳󠁣󠁴󠁿 🏴󠁧󠁢󠁷󠁬󠁳󠁿 🇺🇳 🇺🇸 🇺🇾 🇺🇿 🇻🇺 🇻🇦 🇻🇪 🇻🇳 🇼🇫 🇪🇭 🇾🇪 🇿🇲 🇿🇼 diff --git a/Telegram-Mac/emoji1016.txt b/Telegram-Mac/emoji1016.txt new file mode 100644 index 0000000000..6fef9e8cbe --- /dev/null +++ b/Telegram-Mac/emoji1016.txt @@ -0,0 +1,15 @@ +😀 😃 😄 😁 😆 😅 😂 🤣 🥲 ☺️ 😊 😇 🙂 🙃 😉 😌 😍 🥰 😘 😗 😙 😚 😋 😛 😝 😜 🤪 🤨 🧐 🤓 😎 🥸 🤩 🥳 😏 😒 😞 😔 😟 😕 🙁 ☹️ 😣 😖 😫 😩 🥺 😢 😭 😤 😠 😡 🤬 🤯 😳 🥵 🥶 😱 😨 😰 😥 😓 🤗 🤔 🤭 🤫 🤥 😶 😐 😑 😬 🙄 😯 😦 😧 😮 😲 🥱 😴 🤤 😪 😵 🤐 🥴 🤢 🤮 🤧 😷 🤒 🤕 🤑 🤠 😈 👿 👹 👺 🤡 💩 👻 💀 ☠️ 👽 👾 🤖 🎃 😺 😸 😹 😻 😼 😽 🙀 😿 😾 👋 🤚 🖐 ✋ 🖖 👌 🤌 🤏 ✌️ 🤞 🤟 🤘 🤙 👈 👉 👆 🖕 👇 ☝️ 👍 👎 ✊ 👊 🤛 🤜 👏 🙌 👐 🤲 🤝 🙏 ✍️ 💅 🤳 💪 🦾 🦵 🦿 🦶 👣 👂 🦻 👃 🫀 🫁 🧠 🦷 🦴 👀 👁 👅 👄 💋 🩸 👶 👧 🧒 👦 👩 🧑 👨 👩‍🦱 🧑‍🦱 👨‍🦱 👩‍🦰 🧑‍🦰 👨‍🦰 👱‍♀️ 👱 👱‍♂️ 👩‍🦳 🧑‍🦳 👨‍🦳 👩‍🦲 🧑‍🦲 👨‍🦲 🧔 👵 🧓 👴 👲 👳‍♀️ 👳 👳‍♂️ 🧕 👮‍♀️ 👮 👮‍♂️ 👷‍♀️ 👷 👷‍♂️ 💂‍♀️ 💂 💂‍♂️ 🕵️‍♀️ 🕵️ 🕵️‍♂️ 👩‍⚕️ 🧑‍⚕️ 👨‍⚕️ 👩‍🌾 🧑‍🌾 👨‍🌾 👩‍🍳 🧑‍🍳 👨‍🍳 👩‍🎓 🧑‍🎓 👨‍🎓 👩‍🎤 🧑‍🎤 👨‍🎤 👩‍🏫 🧑‍🏫 👨‍🏫 👩‍🏭 🧑‍🏭 👨‍🏭 👩‍💻 🧑‍💻 👨‍💻 👩‍💼 🧑‍💼 👨‍💼 👩‍🔧 🧑‍🔧 👨‍🔧 👩‍🔬 🧑‍🔬 👨‍🔬 👩‍🎨 🧑‍🎨 👨‍🎨 👩‍🚒 🧑‍🚒 👨‍🚒 👩‍✈️ 🧑‍✈️ 👨‍✈️ 👩‍🚀 🧑‍🚀 👨‍🚀 👩‍⚖️ 🧑‍⚖️ 👨‍⚖️ 👰‍♀️ 👰 👰‍♂️ 🤵‍♀️ 🤵 🤵‍♂️ 👸 🤴 🥷 🦸‍♀️ 🦸 🦸‍♂️ 🦹‍♀️ 🦹 🦹‍♂️ 🤶 🧑‍🎄 🎅 🧙‍♀️ 🧙 🧙‍♂️ 🧝‍♀️ 🧝 🧝‍♂️ 🧛‍♀️ 🧛 🧛‍♂️ 🧟‍♀️ 🧟 🧟‍♂️ 🧞‍♀️ 🧞 🧞‍♂️ 🧜‍♀️ 🧜 🧜‍♂️ 🧚‍♀️ 🧚 🧚‍♂️ 👼 🤰 🤱 👩‍🍼 🧑‍🍼 👨‍🍼 🙇‍♀️ 🙇 🙇‍♂️ 💁‍♀️ 💁 💁‍♂️ 🙅‍♀️ 🙅 🙅‍♂️ 🙆‍♀️ 🙆 🙆‍♂️ 🙋‍♀️ 🙋 🙋‍♂️ 🧏‍♀️ 🧏 🧏‍♂️ 🤦‍♀️ 🤦 🤦‍♂️ 🤷‍♀️ 🤷 🤷‍♂️ 🙎‍♀️ 🙎 🙎‍♂️ 🙍‍♀️ 🙍 🙍‍♂️ 💇‍♀️ 💇 💇‍♂️ 💆‍♀️ 💆 💆‍♂️ 🧖‍♀️ 🧖 🧖‍♂️ 💅 🤳 💃 🕺 👯‍♀️ 👯 👯‍♂️ 🕴 👩‍🦽 🧑‍🦽 👨‍🦽 👩‍🦼 🧑‍🦼 👨‍🦼 🚶‍♀️ 🚶 🚶‍♂️ 👩‍🦯 🧑‍🦯 👨‍🦯 🧎‍♀️ 🧎 🧎‍♂️ 🏃‍♀️ 🏃 🏃‍♂️ 🧍‍♀️ 🧍 🧍‍♂️ 👭 🧑‍🤝‍🧑 👬 👫 👩‍❤️‍👩 💑 👨‍❤️‍👨 👩‍❤️‍👨 👩‍❤️‍💋‍👩 💏 👨‍❤️‍💋‍👨 👩‍❤️‍💋‍👨 👪 👨‍👩‍👦 👨‍👩‍👧 👨‍👩‍👧‍👦 👨‍👩‍👦‍👦 👨‍👩‍👧‍👧 👨‍👨‍👦 👨‍👨‍👧 👨‍👨‍👧‍👦 👨‍👨‍👦‍👦 👨‍👨‍👧‍👧 👩‍👩‍👦 👩‍👩‍👧 👩‍👩‍👧‍👦 👩‍👩‍👦‍👦 👩‍👩‍👧‍👧 👨‍👦 👨‍👦‍👦 👨‍👧 👨‍👧‍👦 👨‍👧‍👧 👩‍👦 👩‍👦‍👦 👩‍👧 👩‍👧‍👦 👩‍👧‍👧 🗣 👤 👥 🫂 🧳 🌂 ☂️ 🧵 🪡 🪢 🧶 👓 🕶 🥽 🥼 🦺 👔 👕 👖 🧣 🧤 🧥 🧦 👗 👘 🥻 🩴 🩱 🩲 🩳 👙 👚 👛 👜 👝 🎒 👞 👟 🥾 🥿 👠 👡 🩰 👢 👑 👒 🎩 🎓 🧢 ⛑ 🪖 💄 💍 💼 + +🐶 🐱 🐭 🐹 🐰 🦊 🐻 🐼 🐻‍❄️ 🐨 🐯 🦁 🐮 🐷 🐽 🐸 🐵 🙈 🙉 🙊 🐒 🐔 🐧 🐦 🐤 🐣 🐥 🦆 🦅 🦉 🦇 🐺 🐗 🐴 🦄 🐝 🪱 🐛 🦋 🐌 🐞 🐜 🪰 🪲 🪳 🦟 🦗 🕷 🕸 🦂 🐢 🐍 🦎 🦖 🦕 🐙 🦑 🦐 🦞 🦀 🐡 🐠 🐟 🐬 🐳 🐋 🦈 🐊 🐅 🐆 🦓 🦍 🦧 🦣 🐘 🦛 🦏 🐪 🐫 🦒 🦘 🦬 🐃 🐂 🐄 🐎 🐖 🐏 🐑 🦙 🐐 🦌 🐕 🐩 🦮 🐕‍🦺 🐈 🐈‍⬛ 🪶 🐓 🦃 🦤 🦚 🦜 🦢 🦩 🕊 🐇 🦝 🦨 🦡 🦫 🦦 🦥 🐁 🐀 🐿 🦔 🐾 🐉 🐲 🌵 🎄 🌲 🌳 🌴 🪵 🌱 🌿 ☘️ 🍀 🎍 🪴 🎋 🍃 🍂 🍁 🍄 🐚 🪨 🌾 💐 🌷 🌹 🥀 🌺 🌸 🌼 🌻 🌞 🌝 🌛 🌜 🌚 🌕 🌖 🌗 🌘 🌑 🌒 🌓 🌔 🌙 🌎 🌍 🌏 🪐 💫 ⭐️ 🌟 ✨ ⚡️ ☄️ 💥 🔥 🌪 🌈 ☀️ 🌤 ⛅️ 🌥 ☁️ 🌦 🌧 ⛈ 🌩 🌨 ❄️ ☃️ ⛄️ 🌬 💨 💧 💦 ☔️ ☂️ 🌊 🌫 + +🍏 🍎 🍐 🍊 🍋 🍌 🍉 🍇 🍓 🫐 🍈 🍒 🍑 🥭 🍍 🥥 🥝 🍅 🍆 🥑 🥦 🥬 🥒 🌶 🫑 🌽 🥕 🫒 🧄 🧅 🥔 🍠 🥐 🥯 🍞 🥖 🥨 🧀 🥚 🍳 🧈 🥞 🧇 🥓 🥩 🍗 🍖 🦴 🌭 🍔 🍟 🍕 🫓 🥪 🥙 🧆 🌮 🌯 🫔 🥗 🥘 🫕 🥫 🍝 🍜 🍲 🍛 🍣 🍱 🥟 🦪 🍤 🍙 🍚 🍘 🍥 🥠 🥮 🍢 🍡 🍧 🍨 🍦 🥧 🧁 🍰 🎂 🍮 🍭 🍬 🍫 🍿 🍩 🍪 🌰 🥜 🍯 🥛 🍼 🫖 ☕️ 🍵 🧃 🥤 🧋 🍶 🍺 🍻 🥂 🍷 🥃 🍸 🍹 🧉 🍾 🧊 🥄 🍴 🍽 🥣 🥡 🥢 🧂 + +⚽️ 🏀 🏈 ⚾️ 🥎 🎾 🏐 🏉 🥏 🎱 🪀 🏓 🏸 🏒 🏑 🥍 🏏 🪃 🥅 ⛳️ 🪁 🏹 🎣 🤿 🥊 🥋 🎽 🛹 🛼 🛷 ⛸ 🥌 🎿 ⛷ 🏂 🪂 🏋️‍♀️ 🏋️ 🏋️‍♂️ 🤼‍♀️ 🤼 🤼‍♂️ 🤸‍♀️ 🤸 🤸‍♂️ ⛹️‍♀️ ⛹️ ⛹️‍♂️ 🤺 🤾‍♀️ 🤾 🤾‍♂️ 🏌️‍♀️ 🏌️ 🏌️‍♂️ 🏇 🧘‍♀️ 🧘 🧘‍♂️ 🏄‍♀️ 🏄 🏄‍♂️ 🏊‍♀️ 🏊 🏊‍♂️ 🤽‍♀️ 🤽 🤽‍♂️ 🚣‍♀️ 🚣 🚣‍♂️ 🧗‍♀️ 🧗 🧗‍♂️ 🚵‍♀️ 🚵 🚵‍♂️ 🚴‍♀️ 🚴 🚴‍♂️ 🏆 🥇 🥈 🥉 🏅 🎖 🏵 🎗 🎫 🎟 🎪 🤹 🤹‍♂️ 🤹‍♀️ 🎭 🩰 🎨 🎬 🎤 🎧 🎼 🎹 🥁 🪘 🎷 🎺 🪗 🎸 🪕 🎻 🎲 ♟ 🎯 🎳 🎮 🎰 🧩 + +🚗 🚕 🚙 🚌 🚎 🏎 🚓 🚑 🚒 🚐 🛻 🚚 🚛 🚜 🦯 🦽 🦼 🛴 🚲 🛵 🏍 🛺 🚨 🚔 🚍 🚘 🚖 🚡 🚠 🚟 🚃 🚋 🚞 🚝 🚄 🚅 🚈 🚂 🚆 🚇 🚊 🚉 ✈️ 🛫 🛬 🛩 💺 🛰 🚀 🛸 🚁 🛶 ⛵️ 🚤 🛥 🛳 ⛴ 🚢 ⚓️ 🪝 ⛽️ 🚧 🚦 🚥 🚏 🗺 🗿 🗽 🗼 🏰 🏯 🏟 🎡 🎢 🎠 ⛲️ ⛱ 🏖 🏝 🏜 🌋 ⛰ 🏔 🗻 🏕 ⛺️ 🛖 🏠 🏡 🏘 🏚 🏗 🏭 🏢 🏬 🏣 🏤 🏥 🏦 🏨 🏪 🏫 🏩 💒 🏛 ⛪️ 🕌 🕍 🛕 🕋 ⛩ 🛤 🛣 🗾 🎑 🏞 🌅 🌄 🌠 🎇 🎆 🌇 🌆 🏙 🌃 🌌 🌉 🌁 + +⌚️ 📱 📲 💻 ⌨️ 🖥 🖨 🖱 🖲 🕹 🗜 💽 💾 💿 📀 📼 📷 📸 📹 🎥 📽 🎞 📞 ☎️ 📟 📠 📺 📻 🎙 🎚 🎛 🧭 ⏱ ⏲ ⏰ 🕰 ⌛️ ⏳ 📡 🔋 🔌 💡 🔦 🕯 🪔 🧯 🛢 💸 💵 💴 💶 💷 🪙 💰 💳 💎 ⚖️ 🪜 🧰 🪛 🔧 🔨 ⚒ 🛠 ⛏ 🪚 🔩 ⚙️ 🪤 🧱 ⛓ 🧲 🔫 💣 🧨 🪓 🔪 🗡 ⚔️ 🛡 🚬 ⚰️ 🪦 ⚱️ 🏺 🔮 📿 🧿 💈 ⚗️ 🔭 🔬 🕳 🩹 🩺 💊 💉 🩸 🧬 🦠 🧫 🧪 🌡 🧹 🪠 🧺 🧻 🚽 🚰 🚿 🛁 🛀 🧼 🪥 🪒 🧽 🪣 🧴 🛎 🔑 🗝 🚪 🪑 🛋 🛏 🛌 🧸 🪆 🖼 🪞 🪟 🛍 🛒 🎁 🎈 🎏 🎀 🪄 🪅 🎊 🎉 🎎 🏮 🎐 🧧 ✉️ 📩 📨 📧 💌 📥 📤 📦 🏷 🪧 📪 📫 📬 📭 📮 📯 📜 📃 📄 📑 🧾 📊 📈 📉 🗒 🗓 📆 📅 🗑 📇 🗃 🗳 🗄 📋 📁 📂 🗂 🗞 📰 📓 📔 📒 📕 📗 📘 📙 📚 📖 🔖 🧷 🔗 📎 🖇 📐 📏 🧮 📌 📍 ✂️ 🖊 🖋 ✒️ 🖌 🖍 📝 ✏️ 🔍 🔎 🔏 🔐 🔒 🔓 + +❤️ 🧡 💛 💚 💙 💜 🖤 🤍 🤎 💔 ❣️ 💕 💞 💓 💗 💖 💘 💝 💟 ☮️ ✝️ ☪️ 🕉 ☸️ ✡️ 🔯 🕎 ☯️ ☦️ 🛐 ⛎ ♈️ ♉️ ♊️ ♋️ ♌️ ♍️ ♎️ ♏️ ♐️ ♑️ ♒️ ♓️ 🆔 ⚛️ 🉑 ☢️ ☣️ 📴 📳 🈶 🈚️ 🈸 🈺 🈷️ ✴️ 🆚 💮 🉐 ㊙️ ㊗️ 🈴 🈵 🈹 🈲 🅰️ 🅱️ 🆎 🆑 🅾️ 🆘 ❌ ⭕️ 🛑 ⛔️ 📛 🚫 💯 💢 ♨️ 🚷 🚯 🚳 🚱 🔞 📵 🚭 ❗️ ❕ ❓ ❔ ‼️ ⁉️ 🔅 🔆 〽️ ⚠️ 🚸 🔱 ⚜️ 🔰 ♻️ ✅ 🈯️ 💹 ❇️ ✳️ ❎ 🌐 💠 Ⓜ️ 🌀 💤 🏧 🚾 ♿️ 🅿️ 🛗 🈳 🈂️ 🛂 🛃 🛄 🛅 🚹 🚺 🚼 ⚧ 🚻 🚮 🎦 📶 🈁 🔣 ℹ️ 🔤 🔡 🔠 🆖 🆗 🆙 🆒 🆕 🆓 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 🔢 #️⃣ *️⃣ ⏏️ ▶️ ⏸ ⏯ ⏹ ⏺ ⏭ ⏮ ⏩ ⏪ ⏫ ⏬ ◀️ 🔼 🔽 ➡️ ⬅️ ⬆️ ⬇️ ↗️ ↘️ ↙️ ↖️ ↕️ ↔️ ↪️ ↩️ ⤴️ ⤵️ 🔀 🔁 🔂 🔄 🔃 🎵 🎶 ➕ ➖ ➗ ✖️ ♾ 💲 💱 ™️ ©️ ®️ 〰️ ➰ ➿ 🔚 🔙 🔛 🔝 🔜 ✔️ ☑️ 🔘 🔴 🟠 🟡 🟢 🔵 🟣 ⚫️ ⚪️ 🟤 🔺 🔻 🔸 🔹 🔶 🔷 🔳 🔲 ▪️ ▫️ ◾️ ◽️ ◼️ ◻️ 🟥 🟧 🟨 🟩 🟦 🟪 ⬛️ ⬜️ 🟫 🔈 🔇 🔉 🔊 🔔 🔕 📣 📢 👁‍🗨 💬 💭 🗯 ♠️ ♣️ ♥️ ♦️ 🃏 🎴 🀄️ 🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛 🕜 🕝 🕞 🕟 🕠 🕡 🕢 🕣 🕤 🕥 🕦 🕧 + +🏳️ 🏴 🏁 🚩 🏳️‍🌈 🏳️‍⚧️ 🏴‍☠️ 🇦🇫 🇦🇽 🇦🇱 🇩🇿 🇦🇸 🇦🇩 🇦🇴 🇦🇮 🇦🇶 🇦🇬 🇦🇷 🇦🇲 🇦🇼 🇦🇺 🇦🇹 🇦🇿 🇧🇸 🇧🇭 🇧🇩 🇧🇧 🇧🇾 🇧🇪 🇧🇿 🇧🇯 🇧🇲 🇧🇹 🇧🇴 🇧🇦 🇧🇼 🇧🇷 🇮🇴 🇻🇬 🇧🇳 🇧🇬 🇧🇫 🇧🇮 🇰🇭 🇨🇲 🇨🇦 🇮🇨 🇨🇻 🇧🇶 🇰🇾 🇨🇫 🇹🇩 🇨🇱 🇨🇳 🇨🇽 🇨🇨 🇨🇴 🇰🇲 🇨🇬 🇨🇩 🇨🇰 🇨🇷 🇨🇮 🇭🇷 🇨🇺 🇨🇼 🇨🇾 🇨🇿 🇩🇰 🇩🇯 🇩🇲 🇩🇴 🇪🇨 🇪🇬 🇸🇻 🇬🇶 🇪🇷 🇪🇪 🇪🇹 🇪🇺 🇫🇰 🇫🇴 🇫🇯 🇫🇮 🇫🇷 🇬🇫 🇵🇫 🇹🇫 🇬🇦 🇬🇲 🇬🇪 🇩🇪 🇬🇭 🇬🇮 🇬🇷 🇬🇱 🇬🇩 🇬🇵 🇬🇺 🇬🇹 🇬🇬 🇬🇳 🇬🇼 🇬🇾 🇭🇹 🇭🇳 🇭🇰 🇭🇺 🇮🇸 🇮🇳 🇮🇩 🇮🇷 🇮🇶 🇮🇪 🇮🇲 🇮🇱 🇮🇹 🇯🇲 🇯🇵 🎌 🇯🇪 🇯🇴 🇰🇿 🇰🇪 🇰🇮 🇽🇰 🇰🇼 🇰🇬 🇱🇦 🇱🇻 🇱🇧 🇱🇸 🇱🇷 🇱🇾 🇱🇮 🇱🇹 🇱🇺 🇲🇴 🇲🇰 🇲🇬 🇲🇼 🇲🇾 🇲🇻 🇲🇱 🇲🇹 🇲🇭 🇲🇶 🇲🇷 🇲🇺 🇾🇹 🇲🇽 🇫🇲 🇲🇩 🇲🇨 🇲🇳 🇲🇪 🇲🇸 🇲🇦 🇲🇿 🇲🇲 🇳🇦 🇳🇷 🇳🇵 🇳🇱 🇳🇨 🇳🇿 🇳🇮 🇳🇪 🇳🇬 🇳🇺 🇳🇫 🇰🇵 🇲🇵 🇳🇴 🇴🇲 🇵🇰 🇵🇼 🇵🇸 🇵🇦 🇵🇬 🇵🇾 🇵🇪 🇵🇭 🇵🇳 🇵🇱 🇵🇹 🇵🇷 🇶🇦 🇷🇪 🇷🇴 🇷🇺 🇷🇼 🇼🇸 🇸🇲 🇸🇦 🇸🇳 🇷🇸 🇸🇨 🇸🇱 🇸🇬 🇸🇽 🇸🇰 🇸🇮 🇬🇸 🇸🇧 🇸🇴 🇿🇦 🇰🇷 🇸🇸 🇪🇸 🇱🇰 🇧🇱 🇸🇭 🇰🇳 🇱🇨 🇵🇲 🇻🇨 🇸🇩 🇸🇷 🇸🇿 🇸🇪 🇨🇭 🇸🇾 🇹🇼 🇹🇯 🇹🇿 🇹🇭 🇹🇱 🇹🇬 🇹🇰 🇹🇴 🇹🇹 🇹🇳 🇹🇷 🇹🇲 🇹🇨 🇹🇻 🇻🇮 🇺🇬 🇺🇦 🇦🇪 🇬🇧 🏴󠁧󠁢󠁥󠁮󠁧󠁿 🏴󠁧󠁢󠁳󠁣󠁴󠁿 🏴󠁧󠁢󠁷󠁬󠁳󠁿 🇺🇳 🇺🇸 🇺🇾 🇺🇿 🇻🇺 🇻🇦 🇻🇪 🇻🇳 🇼🇫 🇪🇭 🇾🇪 🇿🇲 🇿🇼 diff --git a/Telegram-Mac/emoji14.txt b/Telegram-Mac/emoji14.txt new file mode 100644 index 0000000000..f86e81b477 Binary files /dev/null and b/Telegram-Mac/emoji14.txt differ diff --git a/Telegram-Mac/en.lproj/Localizable.strings b/Telegram-Mac/en.lproj/Localizable.strings index 2ad26b3e85..fa0b62d432 100644 Binary files a/Telegram-Mac/en.lproj/Localizable.strings and b/Telegram-Mac/en.lproj/Localizable.strings differ diff --git a/Telegram-Mac/es.lproj/MainMenu.xib b/Telegram-Mac/es.lproj/MainMenu.xib new file mode 100644 index 0000000000..57e428c68e --- /dev/null +++ b/Telegram-Mac/es.lproj/MainMenu.xib @@ -0,0 +1,310 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Telegram-Mac/instantPageWebEmbedView.swift b/Telegram-Mac/instantPageWebEmbedView.swift index df29001bd8..8296c4c176 100644 --- a/Telegram-Mac/instantPageWebEmbedView.swift +++ b/Telegram-Mac/instantPageWebEmbedView.swift @@ -7,40 +7,120 @@ // import Cocoa -import TelegramCoreMac +import TelegramCore + import TGUIKit import WebKit -final class instantPageWebEmbedView: View, InstantPageView { +private final class InstantPageWebView : WKWebView { + + var enableScrolling: Bool = true + + override func scrollWheel(with event: NSEvent) { + if enableScrolling { + super.scrollWheel(with: event) + } else { + if event.scrollingDeltaX != 0 { + super.scrollWheel(with: event) + } else { + super.enclosingScrollView?.scrollWheel(with: event) + } + } + } +} + + +private class WeakInstantPageWebEmbedNodeMessageHandler: NSObject, WKScriptMessageHandler { + private let f: (WKScriptMessage) -> () + + init(_ f: @escaping (WKScriptMessage) -> ()) { + self.f = f + + super.init() + } + + func userContentController(_ controller: WKUserContentController, didReceive scriptMessage: WKScriptMessage) { + self.f(scriptMessage) + } +} + +final class InstantPageWebEmbedView: View, InstantPageView { let url: String? let html: String? - private let webView: WebView - - init(frame: CGRect, url: String?, html: String?, enableScrolling: Bool) { + private var webView: InstantPageWebView! + let updateWebEmbedHeight: (CGFloat) -> Void + init(frame: CGRect, url: String?, html: String?, enableScrolling: Bool, updateWebEmbedHeight: @escaping(CGFloat) -> Void) { self.url = url self.html = html + self.updateWebEmbedHeight = updateWebEmbedHeight + super.init() + + - self.webView = WebView(frame: CGRect(origin: CGPoint(), size: frame.size)) + let js = "var TelegramWebviewProxyProto = function() {}; " + + "TelegramWebviewProxyProto.prototype.postEvent = function(eventName, eventData) { " + + "window.webkit.messageHandlers.performAction.postMessage({'eventName': eventName, 'eventData': eventData}); " + + "}; " + + "var TelegramWebviewProxy = new TelegramWebviewProxyProto();" + + let configuration = WKWebViewConfiguration() + let userController = WKUserContentController() + + let userScript = WKUserScript(source: js, injectionTime: .atDocumentStart, forMainFrameOnly: false) + userController.addUserScript(userScript) + + userController.add(WeakInstantPageWebEmbedNodeMessageHandler { [weak self] message in + if let strongSelf = self { + strongSelf.handleScriptMessage(message) + } + }, name: "performAction") + + configuration.userContentController = userController + + let webView = InstantPageWebView(frame: CGRect(origin: CGPoint(), size: frame.size), configuration: configuration) + - webView.background = theme.colors.background - super.init() if let html = html { - self.webView.mainFrame.loadHTMLString(html, baseURL: nil) + webView.loadHTMLString(html, baseURL: nil) } else if let url = url, let parsedUrl = URL(string: url) { var request = URLRequest(url: parsedUrl) - let referrer = "\(parsedUrl.scheme ?? "")://\(parsedUrl.host ?? "")" - request.setValue(referrer, forHTTPHeaderField: "Referer") - self.webView.mainFrame.load(request) + if let scheme = parsedUrl.scheme, let host = parsedUrl.host { + let referrer = "\(scheme)://\(host)" + request.setValue(referrer, forHTTPHeaderField: "Referer") + } + webView.load(request) } + self.webView = webView + webView.enableScrolling = enableScrolling addSubview(webView) } + private func handleScriptMessage(_ message: WKScriptMessage) { + guard let body = message.body as? [String: Any] else { + return + } + + guard let eventName = body["eventName"] as? String, let eventString = body["eventData"] as? String else { + return + } + + guard let eventData = eventString.data(using: .utf8) else { + return + } + + guard let dict = (try? JSONSerialization.jsonObject(with: eventData, options: [])) as? [String: Any] else { + return + } + + if eventName == "resize_frame", let height = dict["height"] as? Int { + self.updateWebEmbedHeight(CGFloat(height)) + } + } deinit { - webView.mainFrame.load(URLRequest(url: URL(string:"file://blank")!)) - webView.mainFrame.stopLoading() + } required init?(coder: NSCoder) { diff --git a/Telegram-Mac/it.lproj/MainMenu.xib b/Telegram-Mac/it.lproj/MainMenu.xib new file mode 100644 index 0000000000..57e428c68e --- /dev/null +++ b/Telegram-Mac/it.lproj/MainMenu.xib @@ -0,0 +1,310 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Telegram-Mac/libwebp/include/webp/decode.h b/Telegram-Mac/libwebp/include/webp/decode.h deleted file mode 100644 index 8d3f7be92b..0000000000 --- a/Telegram-Mac/libwebp/include/webp/decode.h +++ /dev/null @@ -1,503 +0,0 @@ -// Copyright 2010 Google Inc. All Rights Reserved. -// -// Use of this source code is governed by a BSD-style license -// that can be found in the COPYING file in the root of the source -// tree. An additional intellectual property rights grant can be found -// in the file PATENTS. All contributing project authors may -// be found in the AUTHORS file in the root of the source tree. -// ----------------------------------------------------------------------------- -// -// Main decoding functions for WebP images. -// -// Author: Skal (pascal.massimino@gmail.com) - -#ifndef WEBP_WEBP_DECODE_H_ -#define WEBP_WEBP_DECODE_H_ - -#include "./types.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#define WEBP_DECODER_ABI_VERSION 0x0203 // MAJOR(8b) + MINOR(8b) - -// Note: forward declaring enumerations is not allowed in (strict) C and C++, -// the types are left here for reference. -// typedef enum VP8StatusCode VP8StatusCode; -// typedef enum WEBP_CSP_MODE WEBP_CSP_MODE; -typedef struct WebPRGBABuffer WebPRGBABuffer; -typedef struct WebPYUVABuffer WebPYUVABuffer; -typedef struct WebPDecBuffer WebPDecBuffer; -typedef struct WebPIDecoder WebPIDecoder; -typedef struct WebPBitstreamFeatures WebPBitstreamFeatures; -typedef struct WebPDecoderOptions WebPDecoderOptions; -typedef struct WebPDecoderConfig WebPDecoderConfig; - -// Return the decoder's version number, packed in hexadecimal using 8bits for -// each of major/minor/revision. E.g: v2.5.7 is 0x020507. -WEBP_EXTERN(int) WebPGetDecoderVersion(void); - -// Retrieve basic header information: width, height. -// This function will also validate the header and return 0 in -// case of formatting error. -// Pointers 'width' and 'height' can be passed NULL if deemed irrelevant. -WEBP_EXTERN(int) WebPGetInfo(const uint8_t* data, size_t data_size, - int* width, int* height); - -// Decodes WebP images pointed to by 'data' and returns RGBA samples, along -// with the dimensions in *width and *height. The ordering of samples in -// memory is R, G, B, A, R, G, B, A... in scan order (endian-independent). -// The returned pointer should be deleted calling free(). -// Returns NULL in case of error. -WEBP_EXTERN(uint8_t*) WebPDecodeRGBA(const uint8_t* data, size_t data_size, - int* width, int* height); - -// Same as WebPDecodeRGBA, but returning A, R, G, B, A, R, G, B... ordered data. -WEBP_EXTERN(uint8_t*) WebPDecodeARGB(const uint8_t* data, size_t data_size, - int* width, int* height); - -// Same as WebPDecodeRGBA, but returning B, G, R, A, B, G, R, A... ordered data. -WEBP_EXTERN(uint8_t*) WebPDecodeBGRA(const uint8_t* data, size_t data_size, - int* width, int* height); - -// Same as WebPDecodeRGBA, but returning R, G, B, R, G, B... ordered data. -// If the bitstream contains transparency, it is ignored. -WEBP_EXTERN(uint8_t*) WebPDecodeRGB(const uint8_t* data, size_t data_size, - int* width, int* height); - -// Same as WebPDecodeRGB, but returning B, G, R, B, G, R... ordered data. -WEBP_EXTERN(uint8_t*) WebPDecodeBGR(const uint8_t* data, size_t data_size, - int* width, int* height); - - -// Decode WebP images pointed to by 'data' to Y'UV format(*). The pointer -// returned is the Y samples buffer. Upon return, *u and *v will point to -// the U and V chroma data. These U and V buffers need NOT be free()'d, -// unlike the returned Y luma one. The dimension of the U and V planes -// are both (*width + 1) / 2 and (*height + 1)/ 2. -// Upon return, the Y buffer has a stride returned as '*stride', while U and V -// have a common stride returned as '*uv_stride'. -// Return NULL in case of error. -// (*) Also named Y'CbCr. See: http://en.wikipedia.org/wiki/YCbCr -WEBP_EXTERN(uint8_t*) WebPDecodeYUV(const uint8_t* data, size_t data_size, - int* width, int* height, - uint8_t** u, uint8_t** v, - int* stride, int* uv_stride); - -// These five functions are variants of the above ones, that decode the image -// directly into a pre-allocated buffer 'output_buffer'. The maximum storage -// available in this buffer is indicated by 'output_buffer_size'. If this -// storage is not sufficient (or an error occurred), NULL is returned. -// Otherwise, output_buffer is returned, for convenience. -// The parameter 'output_stride' specifies the distance (in bytes) -// between scanlines. Hence, output_buffer_size is expected to be at least -// output_stride x picture-height. -WEBP_EXTERN(uint8_t*) WebPDecodeRGBAInto( - const uint8_t* data, size_t data_size, - uint8_t* output_buffer, size_t output_buffer_size, int output_stride); -WEBP_EXTERN(uint8_t*) WebPDecodeARGBInto( - const uint8_t* data, size_t data_size, - uint8_t* output_buffer, size_t output_buffer_size, int output_stride); -WEBP_EXTERN(uint8_t*) WebPDecodeBGRAInto( - const uint8_t* data, size_t data_size, - uint8_t* output_buffer, size_t output_buffer_size, int output_stride); - -// RGB and BGR variants. Here too the transparency information, if present, -// will be dropped and ignored. -WEBP_EXTERN(uint8_t*) WebPDecodeRGBInto( - const uint8_t* data, size_t data_size, - uint8_t* output_buffer, size_t output_buffer_size, int output_stride); -WEBP_EXTERN(uint8_t*) WebPDecodeBGRInto( - const uint8_t* data, size_t data_size, - uint8_t* output_buffer, size_t output_buffer_size, int output_stride); - -// WebPDecodeYUVInto() is a variant of WebPDecodeYUV() that operates directly -// into pre-allocated luma/chroma plane buffers. This function requires the -// strides to be passed: one for the luma plane and one for each of the -// chroma ones. The size of each plane buffer is passed as 'luma_size', -// 'u_size' and 'v_size' respectively. -// Pointer to the luma plane ('*luma') is returned or NULL if an error occurred -// during decoding (or because some buffers were found to be too small). -WEBP_EXTERN(uint8_t*) WebPDecodeYUVInto( - const uint8_t* data, size_t data_size, - uint8_t* luma, size_t luma_size, int luma_stride, - uint8_t* u, size_t u_size, int u_stride, - uint8_t* v, size_t v_size, int v_stride); - -//------------------------------------------------------------------------------ -// Output colorspaces and buffer - -// Colorspaces -// Note: the naming describes the byte-ordering of packed samples in memory. -// For instance, MODE_BGRA relates to samples ordered as B,G,R,A,B,G,R,A,... -// Non-capital names (e.g.:MODE_Argb) relates to pre-multiplied RGB channels. -// RGBA-4444 and RGB-565 colorspaces are represented by following byte-order: -// RGBA-4444: [r3 r2 r1 r0 g3 g2 g1 g0], [b3 b2 b1 b0 a3 a2 a1 a0], ... -// RGB-565: [r4 r3 r2 r1 r0 g5 g4 g3], [g2 g1 g0 b4 b3 b2 b1 b0], ... -// In the case WEBP_SWAP_16BITS_CSP is defined, the bytes are swapped for -// these two modes: -// RGBA-4444: [b3 b2 b1 b0 a3 a2 a1 a0], [r3 r2 r1 r0 g3 g2 g1 g0], ... -// RGB-565: [g2 g1 g0 b4 b3 b2 b1 b0], [r4 r3 r2 r1 r0 g5 g4 g3], ... - -typedef enum WEBP_CSP_MODE { - MODE_RGB = 0, MODE_RGBA = 1, - MODE_BGR = 2, MODE_BGRA = 3, - MODE_ARGB = 4, MODE_RGBA_4444 = 5, - MODE_RGB_565 = 6, - // RGB-premultiplied transparent modes (alpha value is preserved) - MODE_rgbA = 7, - MODE_bgrA = 8, - MODE_Argb = 9, - MODE_rgbA_4444 = 10, - // YUV modes must come after RGB ones. - MODE_YUV = 11, MODE_YUVA = 12, // yuv 4:2:0 - MODE_LAST = 13 -} WEBP_CSP_MODE; - -// Some useful macros: -static WEBP_INLINE int WebPIsPremultipliedMode(WEBP_CSP_MODE mode) { - return (mode == MODE_rgbA || mode == MODE_bgrA || mode == MODE_Argb || - mode == MODE_rgbA_4444); -} - -static WEBP_INLINE int WebPIsAlphaMode(WEBP_CSP_MODE mode) { - return (mode == MODE_RGBA || mode == MODE_BGRA || mode == MODE_ARGB || - mode == MODE_RGBA_4444 || mode == MODE_YUVA || - WebPIsPremultipliedMode(mode)); -} - -static WEBP_INLINE int WebPIsRGBMode(WEBP_CSP_MODE mode) { - return (mode < MODE_YUV); -} - -//------------------------------------------------------------------------------ -// WebPDecBuffer: Generic structure for describing the output sample buffer. - -struct WebPRGBABuffer { // view as RGBA - uint8_t* rgba; // pointer to RGBA samples - int stride; // stride in bytes from one scanline to the next. - size_t size; // total size of the *rgba buffer. -}; - -struct WebPYUVABuffer { // view as YUVA - uint8_t* y, *u, *v, *a; // pointer to luma, chroma U/V, alpha samples - int y_stride; // luma stride - int u_stride, v_stride; // chroma strides - int a_stride; // alpha stride - size_t y_size; // luma plane size - size_t u_size, v_size; // chroma planes size - size_t a_size; // alpha-plane size -}; - -// Output buffer -struct WebPDecBuffer { - WEBP_CSP_MODE colorspace; // Colorspace. - int width, height; // Dimensions. - int is_external_memory; // If true, 'internal_memory' pointer is not used. - union { - WebPRGBABuffer RGBA; - WebPYUVABuffer YUVA; - } u; // Nameless union of buffer parameters. - uint32_t pad[4]; // padding for later use - - uint8_t* private_memory; // Internally allocated memory (only when - // is_external_memory is false). Should not be used - // externally, but accessed via the buffer union. -}; - -// Internal, version-checked, entry point -WEBP_EXTERN(int) WebPInitDecBufferInternal(WebPDecBuffer*, int); - -// Initialize the structure as empty. Must be called before any other use. -// Returns false in case of version mismatch -static WEBP_INLINE int WebPInitDecBuffer(WebPDecBuffer* buffer) { - return WebPInitDecBufferInternal(buffer, WEBP_DECODER_ABI_VERSION); -} - -// Free any memory associated with the buffer. Must always be called last. -// Note: doesn't free the 'buffer' structure itself. -WEBP_EXTERN(void) WebPFreeDecBuffer(WebPDecBuffer* buffer); - -//------------------------------------------------------------------------------ -// Enumeration of the status codes - -typedef enum VP8StatusCode { - VP8_STATUS_OK = 0, - VP8_STATUS_OUT_OF_MEMORY, - VP8_STATUS_INVALID_PARAM, - VP8_STATUS_BITSTREAM_ERROR, - VP8_STATUS_UNSUPPORTED_FEATURE, - VP8_STATUS_SUSPENDED, - VP8_STATUS_USER_ABORT, - VP8_STATUS_NOT_ENOUGH_DATA -} VP8StatusCode; - -//------------------------------------------------------------------------------ -// Incremental decoding -// -// This API allows streamlined decoding of partial data. -// Picture can be incrementally decoded as data become available thanks to the -// WebPIDecoder object. This object can be left in a SUSPENDED state if the -// picture is only partially decoded, pending additional input. -// Code example: -// -// WebPInitDecBuffer(&buffer); -// buffer.colorspace = mode; -// ... -// WebPIDecoder* idec = WebPINewDecoder(&buffer); -// while (has_more_data) { -// // ... (get additional data) -// status = WebPIAppend(idec, new_data, new_data_size); -// if (status != VP8_STATUS_SUSPENDED || -// break; -// } -// -// // The above call decodes the current available buffer. -// // Part of the image can now be refreshed by calling to -// // WebPIDecGetRGB()/WebPIDecGetYUVA() etc. -// } -// WebPIDelete(idec); - -// Creates a new incremental decoder with the supplied buffer parameter. -// This output_buffer can be passed NULL, in which case a default output buffer -// is used (with MODE_RGB). Otherwise, an internal reference to 'output_buffer' -// is kept, which means that the lifespan of 'output_buffer' must be larger than -// that of the returned WebPIDecoder object. -// The supplied 'output_buffer' content MUST NOT be changed between calls to -// WebPIAppend() or WebPIUpdate() unless 'output_buffer.is_external_memory' is -// set to 1. In such a case, it is allowed to modify the pointers, size and -// stride of output_buffer.u.RGBA or output_buffer.u.YUVA, provided they remain -// within valid bounds. -// All other fields of WebPDecBuffer MUST remain constant between calls. -// Returns NULL if the allocation failed. -WEBP_EXTERN(WebPIDecoder*) WebPINewDecoder(WebPDecBuffer* output_buffer); - -// This function allocates and initializes an incremental-decoder object, which -// will output the RGB/A samples specified by 'csp' into a preallocated -// buffer 'output_buffer'. The size of this buffer is at least -// 'output_buffer_size' and the stride (distance in bytes between two scanlines) -// is specified by 'output_stride'. -// Additionally, output_buffer can be passed NULL in which case the output -// buffer will be allocated automatically when the decoding starts. The -// colorspace 'csp' is taken into account for allocating this buffer. All other -// parameters are ignored. -// Returns NULL if the allocation failed, or if some parameters are invalid. -WEBP_EXTERN(WebPIDecoder*) WebPINewRGB( - WEBP_CSP_MODE csp, - uint8_t* output_buffer, size_t output_buffer_size, int output_stride); - -// This function allocates and initializes an incremental-decoder object, which -// will output the raw luma/chroma samples into a preallocated planes if -// supplied. The luma plane is specified by its pointer 'luma', its size -// 'luma_size' and its stride 'luma_stride'. Similarly, the chroma-u plane -// is specified by the 'u', 'u_size' and 'u_stride' parameters, and the chroma-v -// plane by 'v' and 'v_size'. And same for the alpha-plane. The 'a' pointer -// can be pass NULL in case one is not interested in the transparency plane. -// Conversely, 'luma' can be passed NULL if no preallocated planes are supplied. -// In this case, the output buffer will be automatically allocated (using -// MODE_YUVA) when decoding starts. All parameters are then ignored. -// Returns NULL if the allocation failed or if a parameter is invalid. -WEBP_EXTERN(WebPIDecoder*) WebPINewYUVA( - uint8_t* luma, size_t luma_size, int luma_stride, - uint8_t* u, size_t u_size, int u_stride, - uint8_t* v, size_t v_size, int v_stride, - uint8_t* a, size_t a_size, int a_stride); - -// Deprecated version of the above, without the alpha plane. -// Kept for backward compatibility. -WEBP_EXTERN(WebPIDecoder*) WebPINewYUV( - uint8_t* luma, size_t luma_size, int luma_stride, - uint8_t* u, size_t u_size, int u_stride, - uint8_t* v, size_t v_size, int v_stride); - -// Deletes the WebPIDecoder object and associated memory. Must always be called -// if WebPINewDecoder, WebPINewRGB or WebPINewYUV succeeded. -WEBP_EXTERN(void) WebPIDelete(WebPIDecoder* idec); - -// Copies and decodes the next available data. Returns VP8_STATUS_OK when -// the image is successfully decoded. Returns VP8_STATUS_SUSPENDED when more -// data is expected. Returns error in other cases. -WEBP_EXTERN(VP8StatusCode) WebPIAppend( - WebPIDecoder* idec, const uint8_t* data, size_t data_size); - -// A variant of the above function to be used when data buffer contains -// partial data from the beginning. In this case data buffer is not copied -// to the internal memory. -// Note that the value of the 'data' pointer can change between calls to -// WebPIUpdate, for instance when the data buffer is resized to fit larger data. -WEBP_EXTERN(VP8StatusCode) WebPIUpdate( - WebPIDecoder* idec, const uint8_t* data, size_t data_size); - -// Returns the RGB/A image decoded so far. Returns NULL if output params -// are not initialized yet. The RGB/A output type corresponds to the colorspace -// specified during call to WebPINewDecoder() or WebPINewRGB(). -// *last_y is the index of last decoded row in raster scan order. Some pointers -// (*last_y, *width etc.) can be NULL if corresponding information is not -// needed. -WEBP_EXTERN(uint8_t*) WebPIDecGetRGB( - const WebPIDecoder* idec, int* last_y, - int* width, int* height, int* stride); - -// Same as above function to get a YUVA image. Returns pointer to the luma -// plane or NULL in case of error. If there is no alpha information -// the alpha pointer '*a' will be returned NULL. -WEBP_EXTERN(uint8_t*) WebPIDecGetYUVA( - const WebPIDecoder* idec, int* last_y, - uint8_t** u, uint8_t** v, uint8_t** a, - int* width, int* height, int* stride, int* uv_stride, int* a_stride); - -// Deprecated alpha-less version of WebPIDecGetYUVA(): it will ignore the -// alpha information (if present). Kept for backward compatibility. -static WEBP_INLINE uint8_t* WebPIDecGetYUV( - const WebPIDecoder* idec, int* last_y, uint8_t** u, uint8_t** v, - int* width, int* height, int* stride, int* uv_stride) { - return WebPIDecGetYUVA(idec, last_y, u, v, NULL, width, height, - stride, uv_stride, NULL); -} - -// Generic call to retrieve information about the displayable area. -// If non NULL, the left/right/width/height pointers are filled with the visible -// rectangular area so far. -// Returns NULL in case the incremental decoder object is in an invalid state. -// Otherwise returns the pointer to the internal representation. This structure -// is read-only, tied to WebPIDecoder's lifespan and should not be modified. -WEBP_EXTERN(const WebPDecBuffer*) WebPIDecodedArea( - const WebPIDecoder* idec, int* left, int* top, int* width, int* height); - -//------------------------------------------------------------------------------ -// Advanced decoding parametrization -// -// Code sample for using the advanced decoding API -/* - // A) Init a configuration object - WebPDecoderConfig config; - CHECK(WebPInitDecoderConfig(&config)); - - // B) optional: retrieve the bitstream's features. - CHECK(WebPGetFeatures(data, data_size, &config.input) == VP8_STATUS_OK); - - // C) Adjust 'config', if needed - config.no_fancy_upsampling = 1; - config.output.colorspace = MODE_BGRA; - // etc. - - // Note that you can also make config.output point to an externally - // supplied memory buffer, provided it's big enough to store the decoded - // picture. Otherwise, config.output will just be used to allocate memory - // and store the decoded picture. - - // D) Decode! - CHECK(WebPDecode(data, data_size, &config) == VP8_STATUS_OK); - - // E) Decoded image is now in config.output (and config.output.u.RGBA) - - // F) Reclaim memory allocated in config's object. It's safe to call - // this function even if the memory is external and wasn't allocated - // by WebPDecode(). - WebPFreeDecBuffer(&config.output); -*/ - -// Features gathered from the bitstream -struct WebPBitstreamFeatures { - int width; // Width in pixels, as read from the bitstream. - int height; // Height in pixels, as read from the bitstream. - int has_alpha; // True if the bitstream contains an alpha channel. - int has_animation; // True if the bitstream is an animation. - int format; // 0 = undefined (/mixed), 1 = lossy, 2 = lossless - - // Unused for now: - int no_incremental_decoding; // if true, using incremental decoding is not - // recommended. - int rotate; // TODO(later) - int uv_sampling; // should be 0 for now. TODO(later) - uint32_t pad[2]; // padding for later use -}; - -// Internal, version-checked, entry point -WEBP_EXTERN(VP8StatusCode) WebPGetFeaturesInternal( - const uint8_t*, size_t, WebPBitstreamFeatures*, int); - -// Retrieve features from the bitstream. The *features structure is filled -// with information gathered from the bitstream. -// Returns VP8_STATUS_OK when the features are successfully retrieved. Returns -// VP8_STATUS_NOT_ENOUGH_DATA when more data is needed to retrieve the -// features from headers. Returns error in other cases. -static WEBP_INLINE VP8StatusCode WebPGetFeatures( - const uint8_t* data, size_t data_size, - WebPBitstreamFeatures* features) { - return WebPGetFeaturesInternal(data, data_size, features, - WEBP_DECODER_ABI_VERSION); -} - -// Decoding options -struct WebPDecoderOptions { - int bypass_filtering; // if true, skip the in-loop filtering - int no_fancy_upsampling; // if true, use faster pointwise upsampler - int use_cropping; // if true, cropping is applied _first_ - int crop_left, crop_top; // top-left position for cropping. - // Will be snapped to even values. - int crop_width, crop_height; // dimension of the cropping area - int use_scaling; // if true, scaling is applied _afterward_ - int scaled_width, scaled_height; // final resolution - int use_threads; // if true, use multi-threaded decoding - int dithering_strength; // dithering strength (0=Off, 100=full) -#if WEBP_DECODER_ABI_VERSION > 0x0203 - int flip; // flip output vertically -#endif -#if WEBP_DECODER_ABI_VERSION > 0x0204 - int alpha_dithering_strength; // alpha dithering strength in [0..100] -#endif - - // Unused for now: - int force_rotation; // forced rotation (to be applied _last_) - int no_enhancement; // if true, discard enhancement layer -#if WEBP_DECODER_ABI_VERSION < 0x0203 - uint32_t pad[5]; // padding for later use -#elif WEBP_DECODER_ABI_VERSION < 0x0204 - uint32_t pad[4]; // padding for later use -#else - uint32_t pad[3]; // padding for later use -#endif -}; - -// Main object storing the configuration for advanced decoding. -struct WebPDecoderConfig { - WebPBitstreamFeatures input; // Immutable bitstream features (optional) - WebPDecBuffer output; // Output buffer (can point to external mem) - WebPDecoderOptions options; // Decoding options -}; - -// Internal, version-checked, entry point -WEBP_EXTERN(int) WebPInitDecoderConfigInternal(WebPDecoderConfig*, int); - -// Initialize the configuration as empty. This function must always be -// called first, unless WebPGetFeatures() is to be called. -// Returns false in case of mismatched version. -static WEBP_INLINE int WebPInitDecoderConfig(WebPDecoderConfig* config) { - return WebPInitDecoderConfigInternal(config, WEBP_DECODER_ABI_VERSION); -} - -// Instantiate a new incremental decoder object with the requested -// configuration. The bitstream can be passed using 'data' and 'data_size' -// parameter, in which case the features will be parsed and stored into -// config->input. Otherwise, 'data' can be NULL and no parsing will occur. -// Note that 'config' can be NULL too, in which case a default configuration -// is used. -// The return WebPIDecoder object must always be deleted calling WebPIDelete(). -// Returns NULL in case of error (and config->status will then reflect -// the error condition). -WEBP_EXTERN(WebPIDecoder*) WebPIDecode(const uint8_t* data, size_t data_size, - WebPDecoderConfig* config); - -// Non-incremental version. This version decodes the full data at once, taking -// 'config' into account. Returns decoding status (which should be VP8_STATUS_OK -// if the decoding was successful). -WEBP_EXTERN(VP8StatusCode) WebPDecode(const uint8_t* data, size_t data_size, - WebPDecoderConfig* config); - -#ifdef __cplusplus -} // extern "C" -#endif - -#endif /* WEBP_WEBP_DECODE_H_ */ diff --git a/Telegram-Mac/libwebp/include/webp/demux.h b/Telegram-Mac/libwebp/include/webp/demux.h deleted file mode 100644 index 2da3239dd9..0000000000 --- a/Telegram-Mac/libwebp/include/webp/demux.h +++ /dev/null @@ -1,224 +0,0 @@ -// Copyright 2012 Google Inc. All Rights Reserved. -// -// Use of this source code is governed by a BSD-style license -// that can be found in the COPYING file in the root of the source -// tree. An additional intellectual property rights grant can be found -// in the file PATENTS. All contributing project authors may -// be found in the AUTHORS file in the root of the source tree. -// ----------------------------------------------------------------------------- -// -// Demux API. -// Enables extraction of image and extended format data from WebP files. - -// Code Example: Demuxing WebP data to extract all the frames, ICC profile -// and EXIF/XMP metadata. -/* - WebPDemuxer* demux = WebPDemux(&webp_data); - - uint32_t width = WebPDemuxGetI(demux, WEBP_FF_CANVAS_WIDTH); - uint32_t height = WebPDemuxGetI(demux, WEBP_FF_CANVAS_HEIGHT); - // ... (Get information about the features present in the WebP file). - uint32_t flags = WebPDemuxGetI(demux, WEBP_FF_FORMAT_FLAGS); - - // ... (Iterate over all frames). - WebPIterator iter; - if (WebPDemuxGetFrame(demux, 1, &iter)) { - do { - // ... (Consume 'iter'; e.g. Decode 'iter.fragment' with WebPDecode(), - // ... and get other frame properties like width, height, offsets etc. - // ... see 'struct WebPIterator' below for more info). - } while (WebPDemuxNextFrame(&iter)); - WebPDemuxReleaseIterator(&iter); - } - - // ... (Extract metadata). - WebPChunkIterator chunk_iter; - if (flags & ICCP_FLAG) WebPDemuxGetChunk(demux, "ICCP", 1, &chunk_iter); - // ... (Consume the ICC profile in 'chunk_iter.chunk'). - WebPDemuxReleaseChunkIterator(&chunk_iter); - if (flags & EXIF_FLAG) WebPDemuxGetChunk(demux, "EXIF", 1, &chunk_iter); - // ... (Consume the EXIF metadata in 'chunk_iter.chunk'). - WebPDemuxReleaseChunkIterator(&chunk_iter); - if (flags & XMP_FLAG) WebPDemuxGetChunk(demux, "XMP ", 1, &chunk_iter); - // ... (Consume the XMP metadata in 'chunk_iter.chunk'). - WebPDemuxReleaseChunkIterator(&chunk_iter); - WebPDemuxDelete(demux); -*/ - -#ifndef WEBP_WEBP_DEMUX_H_ -#define WEBP_WEBP_DEMUX_H_ - -#include "./mux_types.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#define WEBP_DEMUX_ABI_VERSION 0x0101 // MAJOR(8b) + MINOR(8b) - -// Note: forward declaring enumerations is not allowed in (strict) C and C++, -// the types are left here for reference. -// typedef enum WebPDemuxState WebPDemuxState; -// typedef enum WebPFormatFeature WebPFormatFeature; -typedef struct WebPDemuxer WebPDemuxer; -typedef struct WebPIterator WebPIterator; -typedef struct WebPChunkIterator WebPChunkIterator; - -//------------------------------------------------------------------------------ - -// Returns the version number of the demux library, packed in hexadecimal using -// 8bits for each of major/minor/revision. E.g: v2.5.7 is 0x020507. -WEBP_EXTERN(int) WebPGetDemuxVersion(void); - -//------------------------------------------------------------------------------ -// Life of a Demux object - -typedef enum WebPDemuxState { - WEBP_DEMUX_PARSE_ERROR = -1, // An error occurred while parsing. - WEBP_DEMUX_PARSING_HEADER = 0, // Not enough data to parse full header. - WEBP_DEMUX_PARSED_HEADER = 1, // Header parsing complete, - // data may be available. - WEBP_DEMUX_DONE = 2 // Entire file has been parsed. -} WebPDemuxState; - -// Internal, version-checked, entry point -WEBP_EXTERN(WebPDemuxer*) WebPDemuxInternal( - const WebPData*, int, WebPDemuxState*, int); - -// Parses the full WebP file given by 'data'. -// Returns a WebPDemuxer object on successful parse, NULL otherwise. -static WEBP_INLINE WebPDemuxer* WebPDemux(const WebPData* data) { - return WebPDemuxInternal(data, 0, NULL, WEBP_DEMUX_ABI_VERSION); -} - -// Parses the possibly incomplete WebP file given by 'data'. -// If 'state' is non-NULL it will be set to indicate the status of the demuxer. -// Returns NULL in case of error or if there isn't enough data to start parsing; -// and a WebPDemuxer object on successful parse. -// Note that WebPDemuxer keeps internal pointers to 'data' memory segment. -// If this data is volatile, the demuxer object should be deleted (by calling -// WebPDemuxDelete()) and WebPDemuxPartial() called again on the new data. -// This is usually an inexpensive operation. -static WEBP_INLINE WebPDemuxer* WebPDemuxPartial( - const WebPData* data, WebPDemuxState* state) { - return WebPDemuxInternal(data, 1, state, WEBP_DEMUX_ABI_VERSION); -} - -// Frees memory associated with 'dmux'. -WEBP_EXTERN(void) WebPDemuxDelete(WebPDemuxer* dmux); - -//------------------------------------------------------------------------------ -// Data/information extraction. - -typedef enum WebPFormatFeature { - WEBP_FF_FORMAT_FLAGS, // Extended format flags present in the 'VP8X' chunk. - WEBP_FF_CANVAS_WIDTH, - WEBP_FF_CANVAS_HEIGHT, - WEBP_FF_LOOP_COUNT, - WEBP_FF_BACKGROUND_COLOR, - WEBP_FF_FRAME_COUNT // Number of frames present in the demux object. - // In case of a partial demux, this is the number of - // frames seen so far, with the last frame possibly - // being partial. -} WebPFormatFeature; - -// Get the 'feature' value from the 'dmux'. -// NOTE: values are only valid if WebPDemux() was used or WebPDemuxPartial() -// returned a state > WEBP_DEMUX_PARSING_HEADER. -WEBP_EXTERN(uint32_t) WebPDemuxGetI( - const WebPDemuxer* dmux, WebPFormatFeature feature); - -//------------------------------------------------------------------------------ -// Frame iteration. - -struct WebPIterator { - int frame_num; - int num_frames; // equivalent to WEBP_FF_FRAME_COUNT. - int fragment_num; - int num_fragments; - int x_offset, y_offset; // offset relative to the canvas. - int width, height; // dimensions of this frame or fragment. - int duration; // display duration in milliseconds. - WebPMuxAnimDispose dispose_method; // dispose method for the frame. - int complete; // true if 'fragment' contains a full frame. partial images - // may still be decoded with the WebP incremental decoder. - WebPData fragment; // The frame or fragment given by 'frame_num' and - // 'fragment_num'. - int has_alpha; // True if the frame or fragment contains transparency. - WebPMuxAnimBlend blend_method; // Blend operation for the frame. - - uint32_t pad[2]; // padding for later use. - void* private_; // for internal use only. -}; - -// Retrieves frame 'frame_number' from 'dmux'. -// 'iter->fragment' points to the first fragment on return from this function. -// Individual fragments may be extracted using WebPDemuxSelectFragment(). -// Setting 'frame_number' equal to 0 will return the last frame of the image. -// Returns false if 'dmux' is NULL or frame 'frame_number' is not present. -// Call WebPDemuxReleaseIterator() when use of the iterator is complete. -// NOTE: 'dmux' must persist for the lifetime of 'iter'. -WEBP_EXTERN(int) WebPDemuxGetFrame( - const WebPDemuxer* dmux, int frame_number, WebPIterator* iter); - -// Sets 'iter->fragment' to point to the next ('iter->frame_num' + 1) or -// previous ('iter->frame_num' - 1) frame. These functions do not loop. -// Returns true on success, false otherwise. -WEBP_EXTERN(int) WebPDemuxNextFrame(WebPIterator* iter); -WEBP_EXTERN(int) WebPDemuxPrevFrame(WebPIterator* iter); - -// Sets 'iter->fragment' to reflect fragment number 'fragment_num'. -// Returns true if fragment 'fragment_num' is present, false otherwise. -WEBP_EXTERN(int) WebPDemuxSelectFragment(WebPIterator* iter, int fragment_num); - -// Releases any memory associated with 'iter'. -// Must be called before any subsequent calls to WebPDemuxGetChunk() on the same -// iter. Also, must be called before destroying the associated WebPDemuxer with -// WebPDemuxDelete(). -WEBP_EXTERN(void) WebPDemuxReleaseIterator(WebPIterator* iter); - -//------------------------------------------------------------------------------ -// Chunk iteration. - -struct WebPChunkIterator { - // The current and total number of chunks with the fourcc given to - // WebPDemuxGetChunk(). - int chunk_num; - int num_chunks; - WebPData chunk; // The payload of the chunk. - - uint32_t pad[6]; // padding for later use - void* private_; -}; - -// Retrieves the 'chunk_number' instance of the chunk with id 'fourcc' from -// 'dmux'. -// 'fourcc' is a character array containing the fourcc of the chunk to return, -// e.g., "ICCP", "XMP ", "EXIF", etc. -// Setting 'chunk_number' equal to 0 will return the last chunk in a set. -// Returns true if the chunk is found, false otherwise. Image related chunk -// payloads are accessed through WebPDemuxGetFrame() and related functions. -// Call WebPDemuxReleaseChunkIterator() when use of the iterator is complete. -// NOTE: 'dmux' must persist for the lifetime of the iterator. -WEBP_EXTERN(int) WebPDemuxGetChunk(const WebPDemuxer* dmux, - const char fourcc[4], int chunk_number, - WebPChunkIterator* iter); - -// Sets 'iter->chunk' to point to the next ('iter->chunk_num' + 1) or previous -// ('iter->chunk_num' - 1) chunk. These functions do not loop. -// Returns true on success, false otherwise. -WEBP_EXTERN(int) WebPDemuxNextChunk(WebPChunkIterator* iter); -WEBP_EXTERN(int) WebPDemuxPrevChunk(WebPChunkIterator* iter); - -// Releases any memory associated with 'iter'. -// Must be called before destroying the associated WebPDemuxer with -// WebPDemuxDelete(). -WEBP_EXTERN(void) WebPDemuxReleaseChunkIterator(WebPChunkIterator* iter); - -//------------------------------------------------------------------------------ - -#ifdef __cplusplus -} // extern "C" -#endif - -#endif /* WEBP_WEBP_DEMUX_H_ */ diff --git a/Telegram-Mac/libwebp/include/webp/encode.h b/Telegram-Mac/libwebp/include/webp/encode.h deleted file mode 100644 index 3c2637489b..0000000000 --- a/Telegram-Mac/libwebp/include/webp/encode.h +++ /dev/null @@ -1,518 +0,0 @@ -// Copyright 2011 Google Inc. All Rights Reserved. -// -// Use of this source code is governed by a BSD-style license -// that can be found in the COPYING file in the root of the source -// tree. An additional intellectual property rights grant can be found -// in the file PATENTS. All contributing project authors may -// be found in the AUTHORS file in the root of the source tree. -// ----------------------------------------------------------------------------- -// -// WebP encoder: main interface -// -// Author: Skal (pascal.massimino@gmail.com) - -#ifndef WEBP_WEBP_ENCODE_H_ -#define WEBP_WEBP_ENCODE_H_ - -#include "./types.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#define WEBP_ENCODER_ABI_VERSION 0x0202 // MAJOR(8b) + MINOR(8b) - -// Note: forward declaring enumerations is not allowed in (strict) C and C++, -// the types are left here for reference. -// typedef enum WebPImageHint WebPImageHint; -// typedef enum WebPEncCSP WebPEncCSP; -// typedef enum WebPPreset WebPPreset; -// typedef enum WebPEncodingError WebPEncodingError; -typedef struct WebPConfig WebPConfig; -typedef struct WebPPicture WebPPicture; // main structure for I/O -typedef struct WebPAuxStats WebPAuxStats; -typedef struct WebPMemoryWriter WebPMemoryWriter; - -// Return the encoder's version number, packed in hexadecimal using 8bits for -// each of major/minor/revision. E.g: v2.5.7 is 0x020507. -WEBP_EXTERN(int) WebPGetEncoderVersion(void); - -//------------------------------------------------------------------------------ -// One-stop-shop call! No questions asked: - -// Returns the size of the compressed data (pointed to by *output), or 0 if -// an error occurred. The compressed data must be released by the caller -// using the call 'free(*output)'. -// These functions compress using the lossy format, and the quality_factor -// can go from 0 (smaller output, lower quality) to 100 (best quality, -// larger output). -WEBP_EXTERN(size_t) WebPEncodeRGB(const uint8_t* rgb, - int width, int height, int stride, - float quality_factor, uint8_t** output); -WEBP_EXTERN(size_t) WebPEncodeBGR(const uint8_t* bgr, - int width, int height, int stride, - float quality_factor, uint8_t** output); -WEBP_EXTERN(size_t) WebPEncodeRGBA(const uint8_t* rgba, - int width, int height, int stride, - float quality_factor, uint8_t** output); -WEBP_EXTERN(size_t) WebPEncodeBGRA(const uint8_t* bgra, - int width, int height, int stride, - float quality_factor, uint8_t** output); - -// These functions are the equivalent of the above, but compressing in a -// lossless manner. Files are usually larger than lossy format, but will -// not suffer any compression loss. -WEBP_EXTERN(size_t) WebPEncodeLosslessRGB(const uint8_t* rgb, - int width, int height, int stride, - uint8_t** output); -WEBP_EXTERN(size_t) WebPEncodeLosslessBGR(const uint8_t* bgr, - int width, int height, int stride, - uint8_t** output); -WEBP_EXTERN(size_t) WebPEncodeLosslessRGBA(const uint8_t* rgba, - int width, int height, int stride, - uint8_t** output); -WEBP_EXTERN(size_t) WebPEncodeLosslessBGRA(const uint8_t* bgra, - int width, int height, int stride, - uint8_t** output); - -//------------------------------------------------------------------------------ -// Coding parameters - -// Image characteristics hint for the underlying encoder. -typedef enum WebPImageHint { - WEBP_HINT_DEFAULT = 0, // default preset. - WEBP_HINT_PICTURE, // digital picture, like portrait, inner shot - WEBP_HINT_PHOTO, // outdoor photograph, with natural lighting - WEBP_HINT_GRAPH, // Discrete tone image (graph, map-tile etc). - WEBP_HINT_LAST -} WebPImageHint; - -// Compression parameters. -struct WebPConfig { - int lossless; // Lossless encoding (0=lossy(default), 1=lossless). - float quality; // between 0 (smallest file) and 100 (biggest) - int method; // quality/speed trade-off (0=fast, 6=slower-better) - - WebPImageHint image_hint; // Hint for image type (lossless only for now). - - // Parameters related to lossy compression only: - int target_size; // if non-zero, set the desired target size in bytes. - // Takes precedence over the 'compression' parameter. - float target_PSNR; // if non-zero, specifies the minimal distortion to - // try to achieve. Takes precedence over target_size. - int segments; // maximum number of segments to use, in [1..4] - int sns_strength; // Spatial Noise Shaping. 0=off, 100=maximum. - int filter_strength; // range: [0 = off .. 100 = strongest] - int filter_sharpness; // range: [0 = off .. 7 = least sharp] - int filter_type; // filtering type: 0 = simple, 1 = strong (only used - // if filter_strength > 0 or autofilter > 0) - int autofilter; // Auto adjust filter's strength [0 = off, 1 = on] - int alpha_compression; // Algorithm for encoding the alpha plane (0 = none, - // 1 = compressed with WebP lossless). Default is 1. - int alpha_filtering; // Predictive filtering method for alpha plane. - // 0: none, 1: fast, 2: best. Default if 1. - int alpha_quality; // Between 0 (smallest size) and 100 (lossless). - // Default is 100. - int pass; // number of entropy-analysis passes (in [1..10]). - - int show_compressed; // if true, export the compressed picture back. - // In-loop filtering is not applied. - int preprocessing; // preprocessing filter: - // 0=none, 1=segment-smooth, 2=pseudo-random dithering - int partitions; // log2(number of token partitions) in [0..3]. Default - // is set to 0 for easier progressive decoding. - int partition_limit; // quality degradation allowed to fit the 512k limit - // on prediction modes coding (0: no degradation, - // 100: maximum possible degradation). - int emulate_jpeg_size; // If true, compression parameters will be remapped - // to better match the expected output size from - // JPEG compression. Generally, the output size will - // be similar but the degradation will be lower. - int thread_level; // If non-zero, try and use multi-threaded encoding. - int low_memory; // If set, reduce memory usage (but increase CPU use). - - uint32_t pad[5]; // padding for later use -}; - -// Enumerate some predefined settings for WebPConfig, depending on the type -// of source picture. These presets are used when calling WebPConfigPreset(). -typedef enum WebPPreset { - WEBP_PRESET_DEFAULT = 0, // default preset. - WEBP_PRESET_PICTURE, // digital picture, like portrait, inner shot - WEBP_PRESET_PHOTO, // outdoor photograph, with natural lighting - WEBP_PRESET_DRAWING, // hand or line drawing, with high-contrast details - WEBP_PRESET_ICON, // small-sized colorful images - WEBP_PRESET_TEXT // text-like -} WebPPreset; - -// Internal, version-checked, entry point -WEBP_EXTERN(int) WebPConfigInitInternal(WebPConfig*, WebPPreset, float, int); - -// Should always be called, to initialize a fresh WebPConfig structure before -// modification. Returns false in case of version mismatch. WebPConfigInit() -// must have succeeded before using the 'config' object. -// Note that the default values are lossless=0 and quality=75. -static WEBP_INLINE int WebPConfigInit(WebPConfig* config) { - return WebPConfigInitInternal(config, WEBP_PRESET_DEFAULT, 75.f, - WEBP_ENCODER_ABI_VERSION); -} - -// This function will initialize the configuration according to a predefined -// set of parameters (referred to by 'preset') and a given quality factor. -// This function can be called as a replacement to WebPConfigInit(). Will -// return false in case of error. -static WEBP_INLINE int WebPConfigPreset(WebPConfig* config, - WebPPreset preset, float quality) { - return WebPConfigInitInternal(config, preset, quality, - WEBP_ENCODER_ABI_VERSION); -} - -#if WEBP_ENCODER_ABI_VERSION > 0x0202 -// Activate the lossless compression mode with the desired efficiency level -// between 0 (fastest, lowest compression) and 9 (slower, best compression). -// A good default level is '6', providing a fair tradeoff between compression -// speed and final compressed size. -// This function will overwrite several fields from config: 'method', 'quality' -// and 'lossless'. Returns false in case of parameter error. -WEBP_EXTERN(int) WebPConfigLosslessPreset(WebPConfig* config, int level); -#endif - -// Returns true if 'config' is non-NULL and all configuration parameters are -// within their valid ranges. -WEBP_EXTERN(int) WebPValidateConfig(const WebPConfig* config); - -//------------------------------------------------------------------------------ -// Input / Output -// Structure for storing auxiliary statistics (mostly for lossy encoding). - -struct WebPAuxStats { - int coded_size; // final size - - float PSNR[5]; // peak-signal-to-noise ratio for Y/U/V/All/Alpha - int block_count[3]; // number of intra4/intra16/skipped macroblocks - int header_bytes[2]; // approximate number of bytes spent for header - // and mode-partition #0 - int residual_bytes[3][4]; // approximate number of bytes spent for - // DC/AC/uv coefficients for each (0..3) segments. - int segment_size[4]; // number of macroblocks in each segments - int segment_quant[4]; // quantizer values for each segments - int segment_level[4]; // filtering strength for each segments [0..63] - - int alpha_data_size; // size of the transparency data - int layer_data_size; // size of the enhancement layer data - - // lossless encoder statistics - uint32_t lossless_features; // bit0:predictor bit1:cross-color transform - // bit2:subtract-green bit3:color indexing - int histogram_bits; // number of precision bits of histogram - int transform_bits; // precision bits for transform - int cache_bits; // number of bits for color cache lookup - int palette_size; // number of color in palette, if used - int lossless_size; // final lossless size - - uint32_t pad[4]; // padding for later use -}; - -// Signature for output function. Should return true if writing was successful. -// data/data_size is the segment of data to write, and 'picture' is for -// reference (and so one can make use of picture->custom_ptr). -typedef int (*WebPWriterFunction)(const uint8_t* data, size_t data_size, - const WebPPicture* picture); - -// WebPMemoryWrite: a special WebPWriterFunction that writes to memory using -// the following WebPMemoryWriter object (to be set as a custom_ptr). -struct WebPMemoryWriter { - uint8_t* mem; // final buffer (of size 'max_size', larger than 'size'). - size_t size; // final size - size_t max_size; // total capacity - uint32_t pad[1]; // padding for later use -}; - -// The following must be called first before any use. -WEBP_EXTERN(void) WebPMemoryWriterInit(WebPMemoryWriter* writer); - -#if WEBP_ENCODER_ABI_VERSION > 0x0203 -// The following must be called to deallocate writer->mem memory. The 'writer' -// object itself is not deallocated. -WEBP_EXTERN(void) WebPMemoryWriterClear(WebPMemoryWriter* writer); -#endif -// The custom writer to be used with WebPMemoryWriter as custom_ptr. Upon -// completion, writer.mem and writer.size will hold the coded data. -#if WEBP_ENCODER_ABI_VERSION > 0x0203 -// writer.mem must be freed by calling WebPMemoryWriterClear. -#else -// writer.mem must be freed by calling 'free(writer.mem)'. -#endif -WEBP_EXTERN(int) WebPMemoryWrite(const uint8_t* data, size_t data_size, - const WebPPicture* picture); - -// Progress hook, called from time to time to report progress. It can return -// false to request an abort of the encoding process, or true otherwise if -// everything is OK. -typedef int (*WebPProgressHook)(int percent, const WebPPicture* picture); - -// Color spaces. -typedef enum WebPEncCSP { - // chroma sampling - WEBP_YUV420 = 0, // 4:2:0 - WEBP_YUV420A = 4, // alpha channel variant - WEBP_CSP_UV_MASK = 3, // bit-mask to get the UV sampling factors - WEBP_CSP_ALPHA_BIT = 4 // bit that is set if alpha is present -} WebPEncCSP; - -// Encoding error conditions. -typedef enum WebPEncodingError { - VP8_ENC_OK = 0, - VP8_ENC_ERROR_OUT_OF_MEMORY, // memory error allocating objects - VP8_ENC_ERROR_BITSTREAM_OUT_OF_MEMORY, // memory error while flushing bits - VP8_ENC_ERROR_NULL_PARAMETER, // a pointer parameter is NULL - VP8_ENC_ERROR_INVALID_CONFIGURATION, // configuration is invalid - VP8_ENC_ERROR_BAD_DIMENSION, // picture has invalid width/height - VP8_ENC_ERROR_PARTITION0_OVERFLOW, // partition is bigger than 512k - VP8_ENC_ERROR_PARTITION_OVERFLOW, // partition is bigger than 16M - VP8_ENC_ERROR_BAD_WRITE, // error while flushing bytes - VP8_ENC_ERROR_FILE_TOO_BIG, // file is bigger than 4G - VP8_ENC_ERROR_USER_ABORT, // abort request by user - VP8_ENC_ERROR_LAST // list terminator. always last. -} WebPEncodingError; - -// maximum width/height allowed (inclusive), in pixels -#define WEBP_MAX_DIMENSION 16383 - -// Main exchange structure (input samples, output bytes, statistics) -struct WebPPicture { - // INPUT - ////////////// - // Main flag for encoder selecting between ARGB or YUV input. - // It is recommended to use ARGB input (*argb, argb_stride) for lossless - // compression, and YUV input (*y, *u, *v, etc.) for lossy compression - // since these are the respective native colorspace for these formats. - int use_argb; - - // YUV input (mostly used for input to lossy compression) - WebPEncCSP colorspace; // colorspace: should be YUV420 for now (=Y'CbCr). - int width, height; // dimensions (less or equal to WEBP_MAX_DIMENSION) - uint8_t *y, *u, *v; // pointers to luma/chroma planes. - int y_stride, uv_stride; // luma/chroma strides. - uint8_t* a; // pointer to the alpha plane - int a_stride; // stride of the alpha plane - uint32_t pad1[2]; // padding for later use - - // ARGB input (mostly used for input to lossless compression) - uint32_t* argb; // Pointer to argb (32 bit) plane. - int argb_stride; // This is stride in pixels units, not bytes. - uint32_t pad2[3]; // padding for later use - - // OUTPUT - /////////////// - // Byte-emission hook, to store compressed bytes as they are ready. - WebPWriterFunction writer; // can be NULL - void* custom_ptr; // can be used by the writer. - - // map for extra information (only for lossy compression mode) - int extra_info_type; // 1: intra type, 2: segment, 3: quant - // 4: intra-16 prediction mode, - // 5: chroma prediction mode, - // 6: bit cost, 7: distortion - uint8_t* extra_info; // if not NULL, points to an array of size - // ((width + 15) / 16) * ((height + 15) / 16) that - // will be filled with a macroblock map, depending - // on extra_info_type. - - // STATS AND REPORTS - /////////////////////////// - // Pointer to side statistics (updated only if not NULL) - WebPAuxStats* stats; - - // Error code for the latest error encountered during encoding - WebPEncodingError error_code; - - // If not NULL, report progress during encoding. - WebPProgressHook progress_hook; - - void* user_data; // this field is free to be set to any value and - // used during callbacks (like progress-report e.g.). - - uint32_t pad3[3]; // padding for later use - - // Unused for now - uint8_t *pad4, *pad5; - uint32_t pad6[8]; // padding for later use - - // PRIVATE FIELDS - //////////////////// - void* memory_; // row chunk of memory for yuva planes - void* memory_argb_; // and for argb too. - void* pad7[2]; // padding for later use -}; - -// Internal, version-checked, entry point -WEBP_EXTERN(int) WebPPictureInitInternal(WebPPicture*, int); - -// Should always be called, to initialize the structure. Returns false in case -// of version mismatch. WebPPictureInit() must have succeeded before using the -// 'picture' object. -// Note that, by default, use_argb is false and colorspace is WEBP_YUV420. -static WEBP_INLINE int WebPPictureInit(WebPPicture* picture) { - return WebPPictureInitInternal(picture, WEBP_ENCODER_ABI_VERSION); -} - -//------------------------------------------------------------------------------ -// WebPPicture utils - -// Convenience allocation / deallocation based on picture->width/height: -// Allocate y/u/v buffers as per colorspace/width/height specification. -// Note! This function will free the previous buffer if needed. -// Returns false in case of memory error. -WEBP_EXTERN(int) WebPPictureAlloc(WebPPicture* picture); - -// Release the memory allocated by WebPPictureAlloc() or WebPPictureImport*(). -// Note that this function does _not_ free the memory used by the 'picture' -// object itself. -// Besides memory (which is reclaimed) all other fields of 'picture' are -// preserved. -WEBP_EXTERN(void) WebPPictureFree(WebPPicture* picture); - -// Copy the pixels of *src into *dst, using WebPPictureAlloc. Upon return, *dst -// will fully own the copied pixels (this is not a view). The 'dst' picture need -// not be initialized as its content is overwritten. -// Returns false in case of memory allocation error. -WEBP_EXTERN(int) WebPPictureCopy(const WebPPicture* src, WebPPicture* dst); - -// Compute PSNR, SSIM or LSIM distortion metric between two pictures. -// Result is in dB, stores in result[] in the Y/U/V/Alpha/All order. -// Returns false in case of error (src and ref don't have same dimension, ...) -// Warning: this function is rather CPU-intensive. -WEBP_EXTERN(int) WebPPictureDistortion( - const WebPPicture* src, const WebPPicture* ref, - int metric_type, // 0 = PSNR, 1 = SSIM, 2 = LSIM - float result[5]); - -// self-crops a picture to the rectangle defined by top/left/width/height. -// Returns false in case of memory allocation error, or if the rectangle is -// outside of the source picture. -// The rectangle for the view is defined by the top-left corner pixel -// coordinates (left, top) as well as its width and height. This rectangle -// must be fully be comprised inside the 'src' source picture. If the source -// picture uses the YUV420 colorspace, the top and left coordinates will be -// snapped to even values. -WEBP_EXTERN(int) WebPPictureCrop(WebPPicture* picture, - int left, int top, int width, int height); - -// Extracts a view from 'src' picture into 'dst'. The rectangle for the view -// is defined by the top-left corner pixel coordinates (left, top) as well -// as its width and height. This rectangle must be fully be comprised inside -// the 'src' source picture. If the source picture uses the YUV420 colorspace, -// the top and left coordinates will be snapped to even values. -// Picture 'src' must out-live 'dst' picture. Self-extraction of view is allowed -// ('src' equal to 'dst') as a mean of fast-cropping (but note that doing so, -// the original dimension will be lost). Picture 'dst' need not be initialized -// with WebPPictureInit() if it is different from 'src', since its content will -// be overwritten. -// Returns false in case of memory allocation error or invalid parameters. -WEBP_EXTERN(int) WebPPictureView(const WebPPicture* src, - int left, int top, int width, int height, - WebPPicture* dst); - -// Returns true if the 'picture' is actually a view and therefore does -// not own the memory for pixels. -WEBP_EXTERN(int) WebPPictureIsView(const WebPPicture* picture); - -// Rescale a picture to new dimension width x height. -// Now gamma correction is applied. -// Returns false in case of error (invalid parameter or insufficient memory). -WEBP_EXTERN(int) WebPPictureRescale(WebPPicture* pic, int width, int height); - -// Colorspace conversion function to import RGB samples. -// Previous buffer will be free'd, if any. -// *rgb buffer should have a size of at least height * rgb_stride. -// Returns false in case of memory error. -WEBP_EXTERN(int) WebPPictureImportRGB( - WebPPicture* picture, const uint8_t* rgb, int rgb_stride); -// Same, but for RGBA buffer. -WEBP_EXTERN(int) WebPPictureImportRGBA( - WebPPicture* picture, const uint8_t* rgba, int rgba_stride); -// Same, but for RGBA buffer. Imports the RGB direct from the 32-bit format -// input buffer ignoring the alpha channel. Avoids needing to copy the data -// to a temporary 24-bit RGB buffer to import the RGB only. -WEBP_EXTERN(int) WebPPictureImportRGBX( - WebPPicture* picture, const uint8_t* rgbx, int rgbx_stride); - -// Variants of the above, but taking BGR(A|X) input. -WEBP_EXTERN(int) WebPPictureImportBGR( - WebPPicture* picture, const uint8_t* bgr, int bgr_stride); -WEBP_EXTERN(int) WebPPictureImportBGRA( - WebPPicture* picture, const uint8_t* bgra, int bgra_stride); -WEBP_EXTERN(int) WebPPictureImportBGRX( - WebPPicture* picture, const uint8_t* bgrx, int bgrx_stride); - -// Converts picture->argb data to the YUV420A format. The 'colorspace' -// parameter is deprecated and should be equal to WEBP_YUV420. -// Upon return, picture->use_argb is set to false. The presence of real -// non-opaque transparent values is detected, and 'colorspace' will be -// adjusted accordingly. Note that this method is lossy. -// Returns false in case of error. -WEBP_EXTERN(int) WebPPictureARGBToYUVA(WebPPicture* picture, - WebPEncCSP /*colorspace = WEBP_YUV420*/); - -// Same as WebPPictureARGBToYUVA(), but the conversion is done using -// pseudo-random dithering with a strength 'dithering' between -// 0.0 (no dithering) and 1.0 (maximum dithering). This is useful -// for photographic picture. -WEBP_EXTERN(int) WebPPictureARGBToYUVADithered( - WebPPicture* picture, WebPEncCSP colorspace, float dithering); - -#if WEBP_ENCODER_ABI_VERSION > 0x0204 -// Performs 'smart' RGBA->YUVA420 downsampling and colorspace conversion. -// Downsampling is handled with extra care in case of color clipping. This -// method is roughly 2x slower than WebPPictureARGBToYUVA() but produces better -// YUV representation. -// Returns false in case of error. -WEBP_EXTERN(int) WebPPictureSmartARGBToYUVA(WebPPicture* picture); -#endif - -// Converts picture->yuv to picture->argb and sets picture->use_argb to true. -// The input format must be YUV_420 or YUV_420A. -// Note that the use of this method is discouraged if one has access to the -// raw ARGB samples, since using YUV420 is comparatively lossy. Also, the -// conversion from YUV420 to ARGB incurs a small loss too. -// Returns false in case of error. -WEBP_EXTERN(int) WebPPictureYUVAToARGB(WebPPicture* picture); - -// Helper function: given a width x height plane of RGBA or YUV(A) samples -// clean-up the YUV or RGB samples under fully transparent area, to help -// compressibility (no guarantee, though). -WEBP_EXTERN(void) WebPCleanupTransparentArea(WebPPicture* picture); - -// Scan the picture 'picture' for the presence of non fully opaque alpha values. -// Returns true in such case. Otherwise returns false (indicating that the -// alpha plane can be ignored altogether e.g.). -WEBP_EXTERN(int) WebPPictureHasTransparency(const WebPPicture* picture); - -// Remove the transparency information (if present) by blending the color with -// the background color 'background_rgb' (specified as 24bit RGB triplet). -// After this call, all alpha values are reset to 0xff. -WEBP_EXTERN(void) WebPBlendAlpha(WebPPicture* pic, uint32_t background_rgb); - -//------------------------------------------------------------------------------ -// Main call - -// Main encoding call, after config and picture have been initialized. -// 'picture' must be less than 16384x16384 in dimension (cf WEBP_MAX_DIMENSION), -// and the 'config' object must be a valid one. -// Returns false in case of error, true otherwise. -// In case of error, picture->error_code is updated accordingly. -// 'picture' can hold the source samples in both YUV(A) or ARGB input, depending -// on the value of 'picture->use_argb'. It is highly recommended to use -// the former for lossy encoding, and the latter for lossless encoding -// (when config.lossless is true). Automatic conversion from one format to -// another is provided but they both incur some loss. -WEBP_EXTERN(int) WebPEncode(const WebPConfig* config, WebPPicture* picture); - -//------------------------------------------------------------------------------ - -#ifdef __cplusplus -} // extern "C" -#endif - -#endif /* WEBP_WEBP_ENCODE_H_ */ diff --git a/Telegram-Mac/libwebp/include/webp/mux.h b/Telegram-Mac/libwebp/include/webp/mux.h deleted file mode 100644 index 1ae03b3482..0000000000 --- a/Telegram-Mac/libwebp/include/webp/mux.h +++ /dev/null @@ -1,399 +0,0 @@ -// Copyright 2011 Google Inc. All Rights Reserved. -// -// Use of this source code is governed by a BSD-style license -// that can be found in the COPYING file in the root of the source -// tree. An additional intellectual property rights grant can be found -// in the file PATENTS. All contributing project authors may -// be found in the AUTHORS file in the root of the source tree. -// ----------------------------------------------------------------------------- -// -// RIFF container manipulation for WebP images. -// -// Authors: Urvang (urvang@google.com) -// Vikas (vikasa@google.com) - -// This API allows manipulation of WebP container images containing features -// like color profile, metadata, animation and fragmented images. -// -// Code Example#1: Create a WebPMux object with image data, color profile and -// XMP metadata. -/* - int copy_data = 0; - WebPMux* mux = WebPMuxNew(); - // ... (Prepare image data). - WebPMuxSetImage(mux, &image, copy_data); - // ... (Prepare ICCP color profile data). - WebPMuxSetChunk(mux, "ICCP", &icc_profile, copy_data); - // ... (Prepare XMP metadata). - WebPMuxSetChunk(mux, "XMP ", &xmp, copy_data); - // Get data from mux in WebP RIFF format. - WebPMuxAssemble(mux, &output_data); - WebPMuxDelete(mux); - // ... (Consume output_data; e.g. write output_data.bytes to file). - WebPDataClear(&output_data); -*/ - -// Code Example#2: Get image and color profile data from a WebP file. -/* - int copy_data = 0; - // ... (Read data from file). - WebPMux* mux = WebPMuxCreate(&data, copy_data); - WebPMuxGetFrame(mux, 1, &image); - // ... (Consume image; e.g. call WebPDecode() to decode the data). - WebPMuxGetChunk(mux, "ICCP", &icc_profile); - // ... (Consume icc_data). - WebPMuxDelete(mux); - free(data); -*/ - -#ifndef WEBP_WEBP_MUX_H_ -#define WEBP_WEBP_MUX_H_ - -#include "./mux_types.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#define WEBP_MUX_ABI_VERSION 0x0101 // MAJOR(8b) + MINOR(8b) - -// Note: forward declaring enumerations is not allowed in (strict) C and C++, -// the types are left here for reference. -// typedef enum WebPMuxError WebPMuxError; -// typedef enum WebPChunkId WebPChunkId; -typedef struct WebPMux WebPMux; // main opaque object. -typedef struct WebPMuxFrameInfo WebPMuxFrameInfo; -typedef struct WebPMuxAnimParams WebPMuxAnimParams; - -// Error codes -typedef enum WebPMuxError { - WEBP_MUX_OK = 1, - WEBP_MUX_NOT_FOUND = 0, - WEBP_MUX_INVALID_ARGUMENT = -1, - WEBP_MUX_BAD_DATA = -2, - WEBP_MUX_MEMORY_ERROR = -3, - WEBP_MUX_NOT_ENOUGH_DATA = -4 -} WebPMuxError; - -// IDs for different types of chunks. -typedef enum WebPChunkId { - WEBP_CHUNK_VP8X, // VP8X - WEBP_CHUNK_ICCP, // ICCP - WEBP_CHUNK_ANIM, // ANIM - WEBP_CHUNK_ANMF, // ANMF - WEBP_CHUNK_FRGM, // FRGM - WEBP_CHUNK_ALPHA, // ALPH - WEBP_CHUNK_IMAGE, // VP8/VP8L - WEBP_CHUNK_EXIF, // EXIF - WEBP_CHUNK_XMP, // XMP - WEBP_CHUNK_UNKNOWN, // Other chunks. - WEBP_CHUNK_NIL -} WebPChunkId; - -//------------------------------------------------------------------------------ - -// Returns the version number of the mux library, packed in hexadecimal using -// 8bits for each of major/minor/revision. E.g: v2.5.7 is 0x020507. -WEBP_EXTERN(int) WebPGetMuxVersion(void); - -//------------------------------------------------------------------------------ -// Life of a Mux object - -// Internal, version-checked, entry point -WEBP_EXTERN(WebPMux*) WebPNewInternal(int); - -// Creates an empty mux object. -// Returns: -// A pointer to the newly created empty mux object. -// Or NULL in case of memory error. -static WEBP_INLINE WebPMux* WebPMuxNew(void) { - return WebPNewInternal(WEBP_MUX_ABI_VERSION); -} - -// Deletes the mux object. -// Parameters: -// mux - (in/out) object to be deleted -WEBP_EXTERN(void) WebPMuxDelete(WebPMux* mux); - -//------------------------------------------------------------------------------ -// Mux creation. - -// Internal, version-checked, entry point -WEBP_EXTERN(WebPMux*) WebPMuxCreateInternal(const WebPData*, int, int); - -// Creates a mux object from raw data given in WebP RIFF format. -// Parameters: -// bitstream - (in) the bitstream data in WebP RIFF format -// copy_data - (in) value 1 indicates given data WILL be copied to the mux -// object and value 0 indicates data will NOT be copied. -// Returns: -// A pointer to the mux object created from given data - on success. -// NULL - In case of invalid data or memory error. -static WEBP_INLINE WebPMux* WebPMuxCreate(const WebPData* bitstream, - int copy_data) { - return WebPMuxCreateInternal(bitstream, copy_data, WEBP_MUX_ABI_VERSION); -} - -//------------------------------------------------------------------------------ -// Non-image chunks. - -// Note: Only non-image related chunks should be managed through chunk APIs. -// (Image related chunks are: "ANMF", "FRGM", "VP8 ", "VP8L" and "ALPH"). -// To add, get and delete images, use WebPMuxSetImage(), WebPMuxPushFrame(), -// WebPMuxGetFrame() and WebPMuxDeleteFrame(). - -// Adds a chunk with id 'fourcc' and data 'chunk_data' in the mux object. -// Any existing chunk(s) with the same id will be removed. -// Parameters: -// mux - (in/out) object to which the chunk is to be added -// fourcc - (in) a character array containing the fourcc of the given chunk; -// e.g., "ICCP", "XMP ", "EXIF" etc. -// chunk_data - (in) the chunk data to be added -// copy_data - (in) value 1 indicates given data WILL be copied to the mux -// object and value 0 indicates data will NOT be copied. -// Returns: -// WEBP_MUX_INVALID_ARGUMENT - if mux, fourcc or chunk_data is NULL -// or if fourcc corresponds to an image chunk. -// WEBP_MUX_MEMORY_ERROR - on memory allocation error. -// WEBP_MUX_OK - on success. -WEBP_EXTERN(WebPMuxError) WebPMuxSetChunk( - WebPMux* mux, const char fourcc[4], const WebPData* chunk_data, - int copy_data); - -// Gets a reference to the data of the chunk with id 'fourcc' in the mux object. -// The caller should NOT free the returned data. -// Parameters: -// mux - (in) object from which the chunk data is to be fetched -// fourcc - (in) a character array containing the fourcc of the chunk; -// e.g., "ICCP", "XMP ", "EXIF" etc. -// chunk_data - (out) returned chunk data -// Returns: -// WEBP_MUX_INVALID_ARGUMENT - if mux, fourcc or chunk_data is NULL -// or if fourcc corresponds to an image chunk. -// WEBP_MUX_NOT_FOUND - If mux does not contain a chunk with the given id. -// WEBP_MUX_OK - on success. -WEBP_EXTERN(WebPMuxError) WebPMuxGetChunk( - const WebPMux* mux, const char fourcc[4], WebPData* chunk_data); - -// Deletes the chunk with the given 'fourcc' from the mux object. -// Parameters: -// mux - (in/out) object from which the chunk is to be deleted -// fourcc - (in) a character array containing the fourcc of the chunk; -// e.g., "ICCP", "XMP ", "EXIF" etc. -// Returns: -// WEBP_MUX_INVALID_ARGUMENT - if mux or fourcc is NULL -// or if fourcc corresponds to an image chunk. -// WEBP_MUX_NOT_FOUND - If mux does not contain a chunk with the given fourcc. -// WEBP_MUX_OK - on success. -WEBP_EXTERN(WebPMuxError) WebPMuxDeleteChunk( - WebPMux* mux, const char fourcc[4]); - -//------------------------------------------------------------------------------ -// Images. - -// Encapsulates data about a single frame/fragment. -struct WebPMuxFrameInfo { - WebPData bitstream; // image data: can be a raw VP8/VP8L bitstream - // or a single-image WebP file. - int x_offset; // x-offset of the frame. - int y_offset; // y-offset of the frame. - int duration; // duration of the frame (in milliseconds). - - WebPChunkId id; // frame type: should be one of WEBP_CHUNK_ANMF, - // WEBP_CHUNK_FRGM or WEBP_CHUNK_IMAGE - WebPMuxAnimDispose dispose_method; // Disposal method for the frame. - WebPMuxAnimBlend blend_method; // Blend operation for the frame. - uint32_t pad[1]; // padding for later use -}; - -// Sets the (non-animated and non-fragmented) image in the mux object. -// Note: Any existing images (including frames/fragments) will be removed. -// Parameters: -// mux - (in/out) object in which the image is to be set -// bitstream - (in) can be a raw VP8/VP8L bitstream or a single-image -// WebP file (non-animated and non-fragmented) -// copy_data - (in) value 1 indicates given data WILL be copied to the mux -// object and value 0 indicates data will NOT be copied. -// Returns: -// WEBP_MUX_INVALID_ARGUMENT - if mux is NULL or bitstream is NULL. -// WEBP_MUX_MEMORY_ERROR - on memory allocation error. -// WEBP_MUX_OK - on success. -WEBP_EXTERN(WebPMuxError) WebPMuxSetImage( - WebPMux* mux, const WebPData* bitstream, int copy_data); - -// Adds a frame at the end of the mux object. -// Notes: (1) frame.id should be one of WEBP_CHUNK_ANMF or WEBP_CHUNK_FRGM -// (2) For setting a non-animated non-fragmented image, use -// WebPMuxSetImage() instead. -// (3) Type of frame being pushed must be same as the frames in mux. -// (4) As WebP only supports even offsets, any odd offset will be snapped -// to an even location using: offset &= ~1 -// Parameters: -// mux - (in/out) object to which the frame is to be added -// frame - (in) frame data. -// copy_data - (in) value 1 indicates given data WILL be copied to the mux -// object and value 0 indicates data will NOT be copied. -// Returns: -// WEBP_MUX_INVALID_ARGUMENT - if mux or frame is NULL -// or if content of 'frame' is invalid. -// WEBP_MUX_MEMORY_ERROR - on memory allocation error. -// WEBP_MUX_OK - on success. -WEBP_EXTERN(WebPMuxError) WebPMuxPushFrame( - WebPMux* mux, const WebPMuxFrameInfo* frame, int copy_data); - -// Gets the nth frame from the mux object. -// The content of 'frame->bitstream' is allocated using malloc(), and NOT -// owned by the 'mux' object. It MUST be deallocated by the caller by calling -// WebPDataClear(). -// nth=0 has a special meaning - last position. -// Parameters: -// mux - (in) object from which the info is to be fetched -// nth - (in) index of the frame in the mux object -// frame - (out) data of the returned frame -// Returns: -// WEBP_MUX_INVALID_ARGUMENT - if mux or frame is NULL. -// WEBP_MUX_NOT_FOUND - if there are less than nth frames in the mux object. -// WEBP_MUX_BAD_DATA - if nth frame chunk in mux is invalid. -// WEBP_MUX_MEMORY_ERROR - on memory allocation error. -// WEBP_MUX_OK - on success. -WEBP_EXTERN(WebPMuxError) WebPMuxGetFrame( - const WebPMux* mux, uint32_t nth, WebPMuxFrameInfo* frame); - -// Deletes a frame from the mux object. -// nth=0 has a special meaning - last position. -// Parameters: -// mux - (in/out) object from which a frame is to be deleted -// nth - (in) The position from which the frame is to be deleted -// Returns: -// WEBP_MUX_INVALID_ARGUMENT - if mux is NULL. -// WEBP_MUX_NOT_FOUND - If there are less than nth frames in the mux object -// before deletion. -// WEBP_MUX_OK - on success. -WEBP_EXTERN(WebPMuxError) WebPMuxDeleteFrame(WebPMux* mux, uint32_t nth); - -//------------------------------------------------------------------------------ -// Animation. - -// Animation parameters. -struct WebPMuxAnimParams { - uint32_t bgcolor; // Background color of the canvas stored (in MSB order) as: - // Bits 00 to 07: Alpha. - // Bits 08 to 15: Red. - // Bits 16 to 23: Green. - // Bits 24 to 31: Blue. - int loop_count; // Number of times to repeat the animation [0 = infinite]. -}; - -// Sets the animation parameters in the mux object. Any existing ANIM chunks -// will be removed. -// Parameters: -// mux - (in/out) object in which ANIM chunk is to be set/added -// params - (in) animation parameters. -// Returns: -// WEBP_MUX_INVALID_ARGUMENT - if mux or params is NULL. -// WEBP_MUX_MEMORY_ERROR - on memory allocation error. -// WEBP_MUX_OK - on success. -WEBP_EXTERN(WebPMuxError) WebPMuxSetAnimationParams( - WebPMux* mux, const WebPMuxAnimParams* params); - -// Gets the animation parameters from the mux object. -// Parameters: -// mux - (in) object from which the animation parameters to be fetched -// params - (out) animation parameters extracted from the ANIM chunk -// Returns: -// WEBP_MUX_INVALID_ARGUMENT - if mux or params is NULL. -// WEBP_MUX_NOT_FOUND - if ANIM chunk is not present in mux object. -// WEBP_MUX_OK - on success. -WEBP_EXTERN(WebPMuxError) WebPMuxGetAnimationParams( - const WebPMux* mux, WebPMuxAnimParams* params); - -//------------------------------------------------------------------------------ -// Misc Utilities. - -#if WEBP_MUX_ABI_VERSION > 0x0101 -// Sets the canvas size for the mux object. The width and height can be -// specified explicitly or left as zero (0, 0). -// * When width and height are specified explicitly, then this frame bound is -// enforced during subsequent calls to WebPMuxAssemble() and an error is -// reported if any animated frame does not completely fit within the canvas. -// * When unspecified (0, 0), the constructed canvas will get the frame bounds -// from the bounding-box over all frames after calling WebPMuxAssemble(). -// Parameters: -// mux - (in) object to which the canvas size is to be set -// width - (in) canvas width -// height - (in) canvas height -// Returns: -// WEBP_MUX_INVALID_ARGUMENT - if mux is NULL; or -// width or height are invalid or out of bounds -// WEBP_MUX_OK - on success. -WEBP_EXTERN(WebPMuxError) WebPMuxSetCanvasSize(WebPMux* mux, - int width, int height); -#endif - -// Gets the canvas size from the mux object. -// Note: This method assumes that the VP8X chunk, if present, is up-to-date. -// That is, the mux object hasn't been modified since the last call to -// WebPMuxAssemble() or WebPMuxCreate(). -// Parameters: -// mux - (in) object from which the canvas size is to be fetched -// width - (out) canvas width -// height - (out) canvas height -// Returns: -// WEBP_MUX_INVALID_ARGUMENT - if mux, width or height is NULL. -// WEBP_MUX_BAD_DATA - if VP8X/VP8/VP8L chunk or canvas size is invalid. -// WEBP_MUX_OK - on success. -WEBP_EXTERN(WebPMuxError) WebPMuxGetCanvasSize(const WebPMux* mux, - int* width, int* height); - -// Gets the feature flags from the mux object. -// Note: This method assumes that the VP8X chunk, if present, is up-to-date. -// That is, the mux object hasn't been modified since the last call to -// WebPMuxAssemble() or WebPMuxCreate(). -// Parameters: -// mux - (in) object from which the features are to be fetched -// flags - (out) the flags specifying which features are present in the -// mux object. This will be an OR of various flag values. -// Enum 'WebPFeatureFlags' can be used to test individual flag values. -// Returns: -// WEBP_MUX_INVALID_ARGUMENT - if mux or flags is NULL. -// WEBP_MUX_BAD_DATA - if VP8X/VP8/VP8L chunk or canvas size is invalid. -// WEBP_MUX_OK - on success. -WEBP_EXTERN(WebPMuxError) WebPMuxGetFeatures(const WebPMux* mux, - uint32_t* flags); - -// Gets number of chunks with the given 'id' in the mux object. -// Parameters: -// mux - (in) object from which the info is to be fetched -// id - (in) chunk id specifying the type of chunk -// num_elements - (out) number of chunks with the given chunk id -// Returns: -// WEBP_MUX_INVALID_ARGUMENT - if mux, or num_elements is NULL. -// WEBP_MUX_OK - on success. -WEBP_EXTERN(WebPMuxError) WebPMuxNumChunks(const WebPMux* mux, - WebPChunkId id, int* num_elements); - -// Assembles all chunks in WebP RIFF format and returns in 'assembled_data'. -// This function also validates the mux object. -// Note: The content of 'assembled_data' will be ignored and overwritten. -// Also, the content of 'assembled_data' is allocated using malloc(), and NOT -// owned by the 'mux' object. It MUST be deallocated by the caller by calling -// WebPDataClear(). It's always safe to call WebPDataClear() upon return, -// even in case of error. -// Parameters: -// mux - (in/out) object whose chunks are to be assembled -// assembled_data - (out) assembled WebP data -// Returns: -// WEBP_MUX_BAD_DATA - if mux object is invalid. -// WEBP_MUX_INVALID_ARGUMENT - if mux or assembled_data is NULL. -// WEBP_MUX_MEMORY_ERROR - on memory allocation error. -// WEBP_MUX_OK - on success. -WEBP_EXTERN(WebPMuxError) WebPMuxAssemble(WebPMux* mux, - WebPData* assembled_data); - -//------------------------------------------------------------------------------ - -#ifdef __cplusplus -} // extern "C" -#endif - -#endif /* WEBP_WEBP_MUX_H_ */ diff --git a/Telegram-Mac/libwebp/include/webp/mux_types.h b/Telegram-Mac/libwebp/include/webp/mux_types.h deleted file mode 100644 index c94043a3c0..0000000000 --- a/Telegram-Mac/libwebp/include/webp/mux_types.h +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2012 Google Inc. All Rights Reserved. -// -// Use of this source code is governed by a BSD-style license -// that can be found in the COPYING file in the root of the source -// tree. An additional intellectual property rights grant can be found -// in the file PATENTS. All contributing project authors may -// be found in the AUTHORS file in the root of the source tree. -// ----------------------------------------------------------------------------- -// -// Data-types common to the mux and demux libraries. -// -// Author: Urvang (urvang@google.com) - -#ifndef WEBP_WEBP_MUX_TYPES_H_ -#define WEBP_WEBP_MUX_TYPES_H_ - -#include // free() -#include // memset() -#include "./types.h" - -#ifdef __cplusplus -extern "C" { -#endif - -// Note: forward declaring enumerations is not allowed in (strict) C and C++, -// the types are left here for reference. -// typedef enum WebPFeatureFlags WebPFeatureFlags; -// typedef enum WebPMuxAnimDispose WebPMuxAnimDispose; -// typedef enum WebPMuxAnimBlend WebPMuxAnimBlend; -typedef struct WebPData WebPData; - -// VP8X Feature Flags. -typedef enum WebPFeatureFlags { - FRAGMENTS_FLAG = 0x00000001, - ANIMATION_FLAG = 0x00000002, - XMP_FLAG = 0x00000004, - EXIF_FLAG = 0x00000008, - ALPHA_FLAG = 0x00000010, - ICCP_FLAG = 0x00000020 -} WebPFeatureFlags; - -// Dispose method (animation only). Indicates how the area used by the current -// frame is to be treated before rendering the next frame on the canvas. -typedef enum WebPMuxAnimDispose { - WEBP_MUX_DISPOSE_NONE, // Do not dispose. - WEBP_MUX_DISPOSE_BACKGROUND // Dispose to background color. -} WebPMuxAnimDispose; - -// Blend operation (animation only). Indicates how transparent pixels of the -// current frame are blended with those of the previous canvas. -typedef enum WebPMuxAnimBlend { - WEBP_MUX_BLEND, // Blend. - WEBP_MUX_NO_BLEND // Do not blend. -} WebPMuxAnimBlend; - -// Data type used to describe 'raw' data, e.g., chunk data -// (ICC profile, metadata) and WebP compressed image data. -struct WebPData { - const uint8_t* bytes; - size_t size; -}; - -// Initializes the contents of the 'webp_data' object with default values. -static WEBP_INLINE void WebPDataInit(WebPData* webp_data) { - if (webp_data != NULL) { - memset(webp_data, 0, sizeof(*webp_data)); - } -} - -// Clears the contents of the 'webp_data' object by calling free(). Does not -// deallocate the object itself. -static WEBP_INLINE void WebPDataClear(WebPData* webp_data) { - if (webp_data != NULL) { - free((void*)webp_data->bytes); - WebPDataInit(webp_data); - } -} - -// Allocates necessary storage for 'dst' and copies the contents of 'src'. -// Returns true on success. -static WEBP_INLINE int WebPDataCopy(const WebPData* src, WebPData* dst) { - if (src == NULL || dst == NULL) return 0; - WebPDataInit(dst); - if (src->bytes != NULL && src->size != 0) { - dst->bytes = (uint8_t*)malloc(src->size); - if (dst->bytes == NULL) return 0; - memcpy((void*)dst->bytes, src->bytes, src->size); - dst->size = src->size; - } - return 1; -} - -#ifdef __cplusplus -} // extern "C" -#endif - -#endif /* WEBP_WEBP_MUX_TYPES_H_ */ diff --git a/Telegram-Mac/libwebp/include/webp/types.h b/Telegram-Mac/libwebp/include/webp/types.h deleted file mode 100644 index 568d1f263f..0000000000 --- a/Telegram-Mac/libwebp/include/webp/types.h +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2010 Google Inc. All Rights Reserved. -// -// Use of this source code is governed by a BSD-style license -// that can be found in the COPYING file in the root of the source -// tree. An additional intellectual property rights grant can be found -// in the file PATENTS. All contributing project authors may -// be found in the AUTHORS file in the root of the source tree. -// ----------------------------------------------------------------------------- -// -// Common types -// -// Author: Skal (pascal.massimino@gmail.com) - -#ifndef WEBP_WEBP_TYPES_H_ -#define WEBP_WEBP_TYPES_H_ - -#include // for size_t - -#ifndef _MSC_VER -#include -#ifdef __STRICT_ANSI__ -#define WEBP_INLINE -#else /* __STRICT_ANSI__ */ -#define WEBP_INLINE inline -#endif -#else -typedef signed char int8_t; -typedef unsigned char uint8_t; -typedef signed short int16_t; -typedef unsigned short uint16_t; -typedef signed int int32_t; -typedef unsigned int uint32_t; -typedef unsigned long long int uint64_t; -typedef long long int int64_t; -#define WEBP_INLINE __forceinline -#endif /* _MSC_VER */ - -#ifndef WEBP_EXTERN -// This explicitly marks library functions and allows for changing the -// signature for e.g., Windows DLL builds. -#define WEBP_EXTERN(type) extern type -#endif /* WEBP_EXTERN */ - -// Macro to check ABI compatibility (same major revision number) -#define WEBP_ABI_IS_INCOMPATIBLE(a, b) (((a) >> 8) != ((b) >> 8)) - -#endif /* WEBP_WEBP_TYPES_H_ */ diff --git a/Telegram-Mac/libwebp/lib/libwebp.a b/Telegram-Mac/libwebp/lib/libwebp.a deleted file mode 100644 index 123650f415..0000000000 Binary files a/Telegram-Mac/libwebp/lib/libwebp.a and /dev/null differ diff --git a/Telegram-Mac/libwebp/lib/libwebpdemux.a b/Telegram-Mac/libwebp/lib/libwebpdemux.a deleted file mode 100644 index c4a3efee21..0000000000 Binary files a/Telegram-Mac/libwebp/lib/libwebpdemux.a and /dev/null differ diff --git a/Telegram-Mac/libwebp/lib/libwebpmux.a b/Telegram-Mac/libwebp/lib/libwebpmux.a deleted file mode 100644 index e746bcf7ac..0000000000 Binary files a/Telegram-Mac/libwebp/lib/libwebpmux.a and /dev/null differ diff --git a/Telegram-Mac/lottie/anim_archive.json b/Telegram-Mac/lottie/anim_archive.json new file mode 100644 index 0000000000..832dacf3a3 --- /dev/null +++ b/Telegram-Mac/lottie/anim_archive.json @@ -0,0 +1 @@ +{"v":"5.5.1","fr":60,"ip":0,"op":30,"w":228,"h":228,"nm":"Archiveic","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"box3","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[30]},{"t":20,"s":[0]}],"ix":10},"p":{"a":0,"k":[30.5,-39,0],"ix":2},"a":{"a":0,"k":[30.5,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.1,0],[0,0],[0,-1.1],[0,0],[0,0],[0,0]],"o":[[0,0],[1.1,0],[0,0],[0,0],[0,0],[0,-1.1]],"v":[[-11,-3],[11,-3],[13,-1],[13,3],[-13,3],[-13,-1]],"c":true},"ix":2},"nm":"Outline 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"box3","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":30,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"box2","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,-13.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[10,3],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":1,"ix":4},"nm":"Rectangle Outline 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.662744998932,0.662744998932,0.678430974483,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"box2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":30,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"box1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[114,93,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":0,"s":[0,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":15,"s":[110,110,100]},{"t":20,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[1.1,0],[0,0],[0,1.1]],"o":[[0,0],[0,0],[0,1.1],[0,0],[-1.1,0],[0,0]],"v":[[-12,-9],[12,-9],[12,7],[10,9],[-10,9],[-12,7]],"c":true},"ix":2},"nm":"Outline 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"box1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":30,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/lottie/anim_delete.json b/Telegram-Mac/lottie/anim_delete.json new file mode 100644 index 0000000000..23b20fd472 --- /dev/null +++ b/Telegram-Mac/lottie/anim_delete.json @@ -0,0 +1 @@ +{"v":"5.1.2","fr":60,"ip":0,"op":3600,"w":228,"h":228,"nm":"delete","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"bin2 Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[114,94,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.872,0.872,-0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p833_0p872_0p333_0","0p833_0p872_0p333_0","0p833_-0p667_0p333_0"],"t":0,"s":[0,0,100],"e":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.426,0.426,0.833]},"n":["0p667_1_0p167_0p426","0p667_1_0p167_0p426","0p667_1_0p167_0p833"],"t":10,"s":[100,100,100],"e":[115,115,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":15,"s":[115,115,100],"e":[100,100,100]},{"t":25}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-3.112,0],[0,0],[-0.269,3.1],[0,0]],"o":[[0,0],[0.269,3.1],[0,0],[3.113,0],[0,0],[0,0]],"v":[[-27,-34.5],[-21.476,29.02],[-15.499,34.5],[15.499,34.5],[21.476,29.02],[27,-34.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,1.057],[-1.884,32.041],[-1.107,0],[0,-1.057],[1.885,-32.041],[1.107,0]],"o":[[0,-0.038],[0.065,-1.105],[1.057,0],[0,0.038],[-0.065,1.105],[-1.058,0]],"v":[[8.586,23.086],[11.413,-25.032],[13.5,-27],[15.414,-25.086],[12.587,23.032],[10.5,25]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,1.105],[0,0],[-1.105,0],[0,-1.105],[0,0],[1.105,0]],"o":[[0,0],[0,-1.105],[1.105,0],[0,0],[0,1.105],[-1.105,0]],"v":[[-2,23],[-2,-25],[0,-27],[2,-25],[2,23],[0,25]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[0.065,1.105],[0,0],[-1.057,0],[-0.066,-1.105],[0,0],[1.057,0]],"o":[[0,0],[-0.004,-1.17],[1.107,0],[0,0],[0.004,1.17],[-1.107,0]],"v":[[-12.587,23.032],[-15.411,-24.973],[-13.5,-27],[-11.412,-25.032],[-8.589,22.973],[-10.5,25]],"c":true},"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[256,256.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":6,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"bin1 Outlines","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[45],"e":[0]},{"t":15}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[270,180,0],"e":[256,211.923,0],"to":[-2.33333325386047,5.32055473327637,0],"ti":[2.33333325386047,-5.32055473327637,0]},{"t":15}],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":0,"s":[100,100,100],"e":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":10,"s":[100,100,100],"e":[115,115,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":15,"s":[115,115,100],"e":[100,100,100]},{"t":25}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0.542,-0.903],[0,0],[0,0],[0,-3.314],[0,0],[3.314,0],[0,0],[0,0],[1.054,0]],"o":[[-1.054,0],[0,0],[0,0],[-3.314,0],[0,0],[0,-3.314],[0,0],[0,0],[-0.542,-0.903],[0,0]],"v":[[-7.301,-6],[-9.874,-4.544],[-12.6,0],[-24,0],[-30,6],[30,6],[24,0],[12.6,0],[9.874,-4.544],[7.301,-6]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[256,256],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-130,"op":3600,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/lottie/anim_group.json b/Telegram-Mac/lottie/anim_group.json new file mode 100644 index 0000000000..cc5a0437e1 --- /dev/null +++ b/Telegram-Mac/lottie/anim_group.json @@ -0,0 +1 @@ +{"v":"5.1.7","fr":60,"ip":0,"op":32,"w":228,"h":228,"nm":"group","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Oval 2 Copy 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[135.5,110.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":6,"s":[0,0,100],"e":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":16,"s":[100,100,100],"e":[115,115,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":21,"s":[115,115,100],"e":[100,100,100]},{"t":31}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[33,33],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval 2 Copy 2","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Oval 2 Copy 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[93.5,110.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":4,"s":[0,0,100],"e":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":14,"s":[100,100,100],"e":[115,115,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":19,"s":[115,115,100],"e":[100,100,100]},{"t":29}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[33,33],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval 2 Copy 3","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Oval 2 Copy","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[135.5,68.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":2,"s":[0,0,100],"e":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":12,"s":[100,100,100],"e":[115,115,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":17,"s":[115,115,100],"e":[100,100,100]},{"t":27}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[33,33],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval 2 Copy","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Oval 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[93.5,68.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":0,"s":[0,0,100],"e":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":10,"s":[100,100,100],"e":[115,115,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":15,"s":[115,115,100],"e":[100,100,100]},{"t":25}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[33,33],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval 2","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":32,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/lottie/anim_hide.json b/Telegram-Mac/lottie/anim_hide.json new file mode 100644 index 0000000000..b972f4c361 --- /dev/null +++ b/Telegram-Mac/lottie/anim_hide.json @@ -0,0 +1 @@ +{"v":"5.5.1","fr":60,"ip":0,"op":30,"w":228,"h":228,"nm":"Hide","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Path 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[114,79.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":0,"s":[0,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":15,"s":[110,110,100]},{"t":20,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-11,0],[11,0]],"c":false},"ix":2},"nm":"Outline 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2.2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 4","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":30,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Path 2","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0.57,0],[0.06,-0.56],[0,0],[0,0],[0,0],[-0.56,0],[-0.07,0.56]],"o":[[0,0],[0,0],[0,0],[-0.06,-0.56],[-0.57,0],[0,0],[0,0],[0,0],[0.07,0.56],[0.56,0],[0,0]],"v":[[1.09,-3.5],[12,-3.5],[12,3.5],[1.1,3.5],[0,2.5],[-1.1,3.5],[-12,3.5],[-12,-3.5],[-1.09,-3.5],[0,-2.5],[1.09,-3.5]],"c":true},"ix":2},"nm":"Outline 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.741176470588,0.741176470588,0.760784313725,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":30,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Path","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[0,-4,0],"to":[0,-0.167,0],"ti":[0,-1.167,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":15,"s":[0,-5,0],"to":[0,1.167,0],"ti":[0,-1.333,0]},{"t":20,"s":[0,3,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0.6,0],[0,0.61],[0,0],[0,0],[0.41,0.45],[-0.45,0.41],[0,0],[-0.42,-0.39],[0,0],[0.41,-0.45],[0.45,0.41]],"o":[[0,0],[0,0.61],[-0.61,0],[0,0],[0,0],[-0.44,0.41],[-0.41,-0.45],[0,0],[0.42,-0.39],[0,0],[0.44,0.41],[-0.41,0.45],[0,0]],"v":[[1.103,-8],[1.103,10.5],[0.003,11.6],[-1.097,10.5],[-1.097,-8],[-3.927,-5.4],[-5.477,-5.47],[-5.407,-7.02],[-0.747,-11.31],[0.743,-11.31],[5.413,-7.02],[5.473,-5.47],[3.923,-5.4]],"c":true},"ix":2},"nm":"Outline 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":30,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/lottie/anim_mute.json b/Telegram-Mac/lottie/anim_mute.json new file mode 100644 index 0000000000..a8d3b47c04 --- /dev/null +++ b/Telegram-Mac/lottie/anim_mute.json @@ -0,0 +1 @@ +{"v":"5.1.2","fr":60,"ip":0,"op":3600,"w":228,"h":228,"nm":"mute","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"un Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[117.875,88.875,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.863,0.863,-0.19]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p833_0p863_0p333_0","0p833_0p863_0p333_0","0p833_-0p19_0p333_0"],"t":0,"s":[0,0,100],"e":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.365,0.365,0.476]},"n":["0p667_1_0p167_0p365","0p667_1_0p167_0p365","0p667_1_0p167_0p476"],"t":10,"s":[100,100,100],"e":[115,115,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":14,"s":[115,115,100],"e":[100,100,100]},{"t":25}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":5,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-34,-34],[-33.992,-34]],"c":false}],"e":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-34,-34],[34,34]],"c":false}]},{"t":15}],"ix":2,"x":"var $bm_rt;\n$bm_rt = content('Group 1').content('Path 1').path;"},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":1,"ml":10,"nm":"Stroke 2","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.584313750267,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":1,"ml":10,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[256,256],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"mute Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[111,88.781,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.863,0.863,-12.69]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p833_0p863_0p333_0","0p833_0p863_0p333_0","0p833_-12p69_0p333_0"],"t":0,"s":[0,0,100],"e":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.365,0.365,5.476]},"n":["0p667_1_0p167_0p365","0p667_1_0p167_0p365","0p667_1_0p167_5p476"],"t":10,"s":[100,100,100],"e":[115,115,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":14,"s":[115,115,100],"e":[100,100,100]},{"t":25}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.995,-0.904],[0,0],[0.746,0],[0,0],[0,-3.866],[0,0],[-3.866,0],[0,0],[-0.552,-0.503],[0,0],[-2.006,2.207],[0,1.343],[0,0],[2.982,0]],"o":[[0,0],[-0.552,0.502],[0,0],[-3.866,0],[0,0],[0,3.866],[0,0],[0.746,0],[0,0],[2.207,2.007],[0.903,-0.995],[0,0],[0,-2.982],[-1.343,0]],"v":[[16.967,-36.589],[-6.142,-15.581],[-8.16,-14.801],[-19,-14.801],[-26,-7.801],[-26,7.199],[-19,14.199],[-8.16,14.199],[-6.142,14.98],[16.967,35.987],[24.596,35.625],[26,31.992],[26,-32.594],[20.6,-37.994]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[256,256.994],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/lottie/anim_pin.json b/Telegram-Mac/lottie/anim_pin.json new file mode 100644 index 0000000000..adb1491446 --- /dev/null +++ b/Telegram-Mac/lottie/anim_pin.json @@ -0,0 +1 @@ +{"v":"5.1.2","fr":60,"ip":0,"op":3600,"w":228,"h":228,"nm":"pinchat","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"pin Outlines","sr":1,"ks":{"o":{"a":0,"k":99,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[113.656,86.516,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.872,0.872,-10.015]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p833_0p872_0p333_0","0p833_0p872_0p333_0","0p833_-10p015_0p333_0"],"t":0,"s":[0,0,100],"e":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.426,0.426,5.508]},"n":["0p667_1_0p167_0p426","0p667_1_0p167_0p426","0p667_1_0p167_5p508"],"t":10,"s":[100,100,100],"e":[115,115,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":15,"s":[115,115,100],"e":[100,100,100]},{"t":24}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.682,-2.385],[0,0],[7.227,-5.009],[0.494,-0.402],[0.078,-0.079],[-1.265,-1.266],[0,0],[-0.087,-0.07],[-1.129,1.388],[-0.211,0.297],[2.361,8.281],[0,0],[3.76,3.76],[0,0],[2.035,0]],"o":[[0,0],[-8.176,-2.331],[-0.37,0.258],[-0.086,0.07],[-1.265,1.265],[0,0],[0.079,0.078],[1.388,1.129],[0.312,-0.384],[5.172,-7.273],[0,0],[4.348,-3.067],[0,0],[-1.701,-1.701],[-2.465,0]],"v":[[10.943,-36.491],[-1.24,-19.219],[-25.643,-15.202],[-26.94,-14.213],[-27.187,-13.99],[-27.187,-9.408],[8.801,26.579],[9.049,26.802],[13.607,26.332],[14.391,25.311],[18.607,0.628],[35.879,-11.556],[36.955,-23.925],[23.313,-37.567],[17.514,-40.1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-40.714,40.1],[-7.985,15.793],[-16.406,7.372]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[256.714,257.1],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/lottie/anim_read.json b/Telegram-Mac/lottie/anim_read.json new file mode 100644 index 0000000000..de017a73ed --- /dev/null +++ b/Telegram-Mac/lottie/anim_read.json @@ -0,0 +1 @@ +{"v":"4.5.6","fr":60,"ip":0,"op":30,"w":228,"h":228,"ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Combined Shape","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[114,89.499,0]},"a":{"a":0,"k":[40,38.499,0]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p667_1_0p167_0p167","0p667_1_0p167_0p167","0p667_0p667_0p167_0p167"],"t":0,"s":[0,0,100],"e":[110,110,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":15,"s":[110,110,100],"e":[100,100,100]},{"t":20}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-3.39,0],[0,19.29],[22.09,0],[0,-19.28],[-9.1,-6.4],[2.59,-3.82],[-1.62,-0.65],[-4.27,2.3],[-1.31,-0.29]],"o":[[22.09,0],[0,-19.28],[-22.09,0],[0,11],[1.17,0.82],[-2.6,3.82],[1,0.4],[6.1,-3.28],[3.14,0.69]],"v":[[40,69.85],[80,34.92],[40,0],[0,34.92],[14.45,61.34],[14.1,70.53],[9.89,76.75],[21.07,75.05],[30.18,68.79]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"mn":"ADBE Vector Group"}],"ip":0,"op":30,"st":0,"bm":0,"sr":1}]} \ No newline at end of file diff --git a/Telegram-Mac/lottie/anim_unarchive.json b/Telegram-Mac/lottie/anim_unarchive.json new file mode 100644 index 0000000000..8105c51653 --- /dev/null +++ b/Telegram-Mac/lottie/anim_unarchive.json @@ -0,0 +1 @@ +{"v":"5.5.1","fr":60,"ip":0,"op":60,"w":288,"h":288,"nm":"Unarchive","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"box2","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[0,-9,0],"to":[0,3.833,0],"ti":[0,-3.833,0]},{"t":15,"s":[0,14,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[10,3],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":1,"ix":4},"nm":"Rectangle Outline 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.662745098039,0.662745098039,0.678431372549,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"box2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"box1","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,18,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0],[1.1,0],[0,0],[0,1.1]],"o":[[0,0],[0,0],[0,1.1],[0,0],[-1.1,0],[0,0]],"v":[[-12,-9],[12,-9],[12,7],[10,9],[-10,9],[-12,7]],"c":true}]},{"t":15,"s":[{"i":[[0,0],[0,0],[0,0],[1.1,0],[0,0],[0,1.1]],"o":[[0,0],[0,0],[0,1.1],[0,0],[-1.1,0],[0,0]],"v":[[-12,-5.167],[12,-5.167],[12,7],[10,9],[-10,9],[-12,7]],"c":true}]}],"ix":2},"nm":"Outline 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"box1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Path 2","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.067,"y":1},"o":{"x":0.936,"y":0},"t":0,"s":[0,28.621,0],"to":[0,-13.167,0],"ti":[0,13.167,0]},{"t":15,"s":[0,-50.379,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.1,0.09],[0,0],[-0.2,-0.2],[0,0],[0.19,-0.2],[0.13,0],[0,0],[0,0],[0.28,0],[0,0],[0,0.28],[0,0],[0,0],[0,0.28]],"o":[[0,0],[0.19,-0.2],[0,0],[0.19,0.19],[-0.09,0.09],[0,0],[0,0],[0,0.28],[0,0],[-0.27,0],[0,0],[0,0],[-0.28,0],[0,-0.13]],"v":[[-5.143,0.544],[-0.353,-4.246],[0.357,-4.246],[5.147,0.544],[5.147,1.254],[4.797,1.394],[1.997,1.394],[1.997,3.894],[1.497,4.394],[-1.503,4.394],[-2.003,3.894],[-2.003,1.394],[-4.793,1.394],[-5.293,0.894]],"c":true},"ix":2},"nm":"Outline 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":3,"nm":"scale","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[144,144,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":0,"s":[0,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":15,"s":[110,110,100]},{"t":20,"s":[100,100,100]}],"ix":6}},"ao":0,"ip":0,"op":60,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/lottie/anim_ungroup.json b/Telegram-Mac/lottie/anim_ungroup.json new file mode 100644 index 0000000000..0ad2298e46 --- /dev/null +++ b/Telegram-Mac/lottie/anim_ungroup.json @@ -0,0 +1 @@ +{"v":"5.1.7","fr":60,"ip":0,"op":32,"w":228,"h":228,"nm":"ungroup","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"un Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[114.75,89.312,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.872,0.872,-11.778]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p833_0p872_0p333_0","0p833_0p872_0p333_0","0p833_-11p778_0p333_0"],"t":0,"s":[0,0,100],"e":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.426,0.426,6.389]},"n":["0p667_1_0p167_0p426","0p667_1_0p167_0p426","0p667_1_0p167_6p389"],"t":10,"s":[100,100,100],"e":[115,115,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":15,"s":[115,115,100],"e":[100,100,100]},{"t":24}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":5,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-34,-34],[-33.992,-34]],"c":false}],"e":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-34,-34],[34,34]],"c":false}]},{"t":15}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":1,"ml":10,"nm":"Stroke 2","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"st","c":{"a":0,"k":[0.097999999102,0.57599995931,0.980000035903,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":1,"ml":10,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[256,256],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Oval 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[93.5,68.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":0,"s":[0,0,100],"e":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":10,"s":[100,100,100],"e":[115,115,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":15,"s":[115,115,100],"e":[100,100,100]},{"t":25}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[33,33],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval 2","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Oval 2 Copy","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[135.5,68.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":2,"s":[0,0,100],"e":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":12,"s":[100,100,100],"e":[115,115,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":17,"s":[115,115,100],"e":[100,100,100]},{"t":27}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[33,33],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval 2 Copy","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Oval 2 Copy 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[93.5,110.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":4,"s":[0,0,100],"e":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":14,"s":[100,100,100],"e":[115,115,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":19,"s":[115,115,100],"e":[100,100,100]},{"t":29}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[33,33],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval 2 Copy 3","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":32,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Oval 2 Copy 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[135.5,110.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":6,"s":[0,0,100],"e":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":16,"s":[100,100,100],"e":[115,115,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":21,"s":[115,115,100],"e":[100,100,100]},{"t":31}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[33,33],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval 2 Copy 2","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":32,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/lottie/anim_unmute.json b/Telegram-Mac/lottie/anim_unmute.json new file mode 100644 index 0000000000..f1059e9a2d --- /dev/null +++ b/Telegram-Mac/lottie/anim_unmute.json @@ -0,0 +1 @@ +{"v":"5.1.2","fr":60,"ip":0,"op":3600,"w":228,"h":228,"nm":"unmute","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"vol1 Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[120,90,0],"e":[131,90,0],"to":[1.83333337306976,0,0],"ti":[-1.83333337306976,0,0]},{"t":10}],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.872,0.872,-11.778]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p833_0p872_0p333_0","0p833_0p872_0p333_0","0p833_-11p778_0p333_0"],"t":0,"s":[0,0,100],"e":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.426,0.426,6.389]},"n":["0p667_1_0p167_0p426","0p667_1_0p167_0p426","0p667_1_0p167_6p389"],"t":10,"s":[100,100,100],"e":[115,115,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":15,"s":[115,115,100],"e":[100,100,100]},{"t":25}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.577,-0.531],[0.016,-0.789],[0,0],[-0.516,-0.561],[0,-5.477],[4.174,-4.561],[0.014,-0.704],[0,0],[-0.631,-0.578],[-1.119,1.223],[0,7.023],[5.163,5.605],[0.809,0]],"o":[[-0.627,0.576],[0,0],[0.015,0.706],[4.17,4.526],[0,5.479],[-0.513,0.56],[0,0],[0.016,0.793],[1.222,1.118],[5.16,-5.636],[0,-7.025],[-0.592,-0.641],[-0.726,0]],"v":[[-5.533,-19.371],[-6.5,-17.224],[-6.5,-17.104],[-5.707,-15.133],[0.5,-0.19],[-5.713,14.81],[-6.5,16.777],[-6.5,16.892],[-5.526,19.048],[-1.287,18.859],[6.5,-0.19],[-1.293,-19.198],[-3.501,-20.165]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[255.5,256.165],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"vol2 Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":2,"s":[120,90,0],"e":[146,90,0],"to":[4.33333349227905,0,0],"ti":[-4.33333349227905,0,0]},{"t":12}],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.872,0.872,-11.778]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p833_0p872_0p333_0","0p833_0p872_0p333_0","0p833_-11p778_0p333_0"],"t":2,"s":[0,0,100],"e":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.426,0.426,6.389]},"n":["0p667_1_0p167_0p426","0p667_1_0p167_0p426","0p667_1_0p167_6p389"],"t":12,"s":[100,100,100],"e":[115,115,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":17,"s":[115,115,100],"e":[100,100,100]},{"t":27}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.56,-0.468],[-1.06,-1.273],[0,-8.078],[6.916,-8.351],[-1.276,-1.057],[-1.057,1.275],[0,9.591],[7.755,9.312],[0.861,0]],"o":[[-1.274,1.06],[6.912,8.299],[0,8.082],[-1.057,1.276],[1.276,1.056],[7.751,-9.362],[0,-9.592],[-0.593,-0.713],[-0.675,0]],"v":[[-7.235,-28.988],[-7.622,-24.763],[2.684,-0.221],[-7.627,24.404],[-7.229,28.628],[-3.005,28.23],[8.684,-0.221],[-3.011,-28.603],[-5.319,-29.684]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[255.316,256.684],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"vol3 Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":4,"s":[120,89.062,0],"e":[162,89.062,0],"to":[7,0,0],"ti":[-7,0,0]},{"t":14}],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.872,0.872,-11.778]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p833_0p872_0p333_0","0p833_0p872_0p333_0","0p833_-11p778_0p333_0"],"t":4,"s":[0,0,100],"e":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.426,0.426,6.389]},"n":["0p667_1_0p167_0p426","0p667_1_0p167_0p426","0p667_1_0p167_6p389"],"t":14,"s":[100,100,100],"e":[115,115,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":19,"s":[115,115,100],"e":[100,100,100]},{"t":29}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.563,-0.475],[-1.067,-1.267],[0,-12.342],[9.568,-11.435],[-1.27,-1.063],[-0.665,-0.01],[0,0],[-0.582,0.697],[0,13.826],[10.437,12.396],[0.855,0]],"o":[[-1.268,1.066],[9.564,11.36],[0,12.345],[-1.063,1.271],[0.549,0.459],[0,0],[0.842,-0.013],[10.432,-12.468],[0,-13.827],[-0.593,-0.705],[-0.683,0]],"v":[[-9.25,-39.794],[-9.613,-35.568],[4.682,-0.056],[-9.619,35.575],[-9.243,39.801],[-7.364,40.5],[-7.274,40.5],[-5.017,39.425],[10.682,-0.056],[-5.024,-39.432],[-7.319,-40.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[255.318,256.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"mute Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":0,"s":[100,88.781,0],"e":[85,88.781,0],"to":[-2.5,0,0],"ti":[2.5,0,0]},{"t":10}],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.872,0.872,-11.778]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p833_0p872_0p333_0","0p833_0p872_0p333_0","0p833_-11p778_0p333_0"],"t":0,"s":[0,0,100],"e":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.426,0.426,6.389]},"n":["0p667_1_0p167_0p426","0p667_1_0p167_0p426","0p667_1_0p167_6p389"],"t":10,"s":[100,100,100],"e":[115,115,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":15,"s":[115,115,100],"e":[100,100,100]},{"t":25}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.995,-0.904],[0,0],[0.746,0],[0,0],[0,-3.866],[0,0],[-3.866,0],[0,0],[-0.552,-0.503],[0,0],[-2.006,2.207],[0,1.343],[0,0],[2.982,0]],"o":[[0,0],[-0.552,0.502],[0,0],[-3.866,0],[0,0],[0,3.866],[0,0],[0.746,0],[0,0],[2.207,2.007],[0.903,-0.995],[0,0],[0,-2.982],[-1.343,0]],"v":[[16.967,-36.589],[-6.142,-15.581],[-8.16,-14.801],[-19,-14.801],[-26,-7.801],[-26,7.199],[-19,14.199],[-8.16,14.199],[-6.142,14.98],[16.967,35.987],[24.596,35.625],[26,31.992],[26,-32.594],[20.6,-37.994]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[256,256.994],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/lottie/anim_unpin.json b/Telegram-Mac/lottie/anim_unpin.json new file mode 100644 index 0000000000..3a0750f7a8 --- /dev/null +++ b/Telegram-Mac/lottie/anim_unpin.json @@ -0,0 +1 @@ +{"v":"5.1.2","fr":60,"ip":0,"op":3600,"w":228,"h":228,"nm":"unpinpinchat","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"un Outlines","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[118,84.062,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.872,0.872,-11.778]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p833_0p872_0p333_0","0p833_0p872_0p333_0","0p833_-11p778_0p333_0"],"t":0,"s":[0,0,100],"e":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.426,0.426,6.389]},"n":["0p667_1_0p167_0p426","0p667_1_0p167_0p426","0p667_1_0p167_6p389"],"t":10,"s":[100,100,100],"e":[115,115,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":15,"s":[115,115,100],"e":[100,100,100]},{"t":24}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":5,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-34,-34],[-33.992,-34]],"c":false}],"e":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-34,-34],[34,34]],"c":false}]},{"t":15}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":1,"ml":10,"nm":"Stroke 2","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"st","c":{"a":0,"k":[0.097999999102,0.57599995931,0.980000035903,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":15,"ix":5},"lc":2,"lj":1,"ml":10,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[256,256],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"pin Outlines","sr":1,"ks":{"o":{"a":0,"k":99,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[113.656,86.516,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.859,0.859,-13.056]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p833_0p859_0p333_0","0p833_0p859_0p333_0","0p833_-13p056_0p333_0"],"t":0,"s":[0,0,100],"e":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.341,0.341,5.111]},"n":["0p667_1_0p167_0p341","0p667_1_0p167_0p341","0p667_1_0p167_5p111"],"t":11,"s":[100,100,100],"e":[115,115,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_1_0p333_0"],"t":15,"s":[115,115,100],"e":[100,100,100]},{"t":24}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[1.682,-2.385],[0,0],[7.227,-5.009],[0.494,-0.402],[0.078,-0.079],[-1.265,-1.266],[0,0],[-0.087,-0.07],[-1.129,1.388],[-0.211,0.297],[2.361,8.281],[0,0],[3.76,3.76],[0,0],[2.035,0]],"o":[[0,0],[-8.176,-2.331],[-0.37,0.258],[-0.086,0.07],[-1.265,1.265],[0,0],[0.079,0.078],[1.388,1.129],[0.312,-0.384],[5.172,-7.273],[0,0],[4.348,-3.067],[0,0],[-1.701,-1.701],[-2.465,0]],"v":[[10.943,-36.491],[-1.24,-19.219],[-25.643,-15.202],[-26.94,-14.213],[-27.187,-13.99],[-27.187,-9.408],[8.801,26.579],[9.049,26.802],[13.607,26.332],[14.391,25.311],[18.607,0.628],[35.879,-11.556],[36.955,-23.925],[23.313,-37.567],[17.514,-40.1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-40.714,40.1],[-7.985,15.793],[-16.406,7.372]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[256.714,257.1],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/lottie/anim_unread.json b/Telegram-Mac/lottie/anim_unread.json new file mode 100644 index 0000000000..b648c6da3f --- /dev/null +++ b/Telegram-Mac/lottie/anim_unread.json @@ -0,0 +1 @@ +{"v":"4.5.6","fr":60,"ip":0,"op":30,"w":228,"h":228,"ddd":0,"assets":[],"layers":[{"ddd":0,"ind":0,"ty":4,"nm":"Oval","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[144,60,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p667_1_0p167_0p167","0p667_1_0p167_0p167","0p667_0p667_0p167_0p167"],"t":10,"s":[0,0,100],"e":[110,110,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":19,"s":[110,110,100],"e":[100,100,100]},{"t":24}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"st","c":{"a":0,"k":[0.13,0.58,0.98,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":5.333},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":3,"mn":"ADBE Vector Group"}],"ip":0,"op":30,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":1,"ty":4,"nm":"Combined Shape","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[114,89.499,0]},"a":{"a":0,"k":[40,38.499,0]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"n":["0p667_1_0p167_0p167","0p667_1_0p167_0p167","0p667_0p667_0p167_0p167"],"t":0,"s":[0,0,100],"e":[110,110,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":15,"s":[110,110,100],"e":[100,100,100]},{"t":20}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-3.39,0],[0,19.29],[22.09,0],[0,-19.28],[-9.1,-6.4],[2.59,-3.82],[-1.62,-0.65],[-4.27,2.3],[-1.31,-0.29]],"o":[[22.09,0],[0,-19.28],[-22.09,0],[0,11],[1.17,0.82],[-2.6,3.82],[1,0.4],[6.1,-3.28],[3.14,0.69]],"v":[[40,69.85],[80,34.92],[40,0],[0,34.92],[14.45,61.34],[14.1,70.53],[9.89,76.75],[21.07,75.05],[30.18,68.79]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"mn":"ADBE Vector Group"}],"ip":0,"op":30,"st":0,"bm":0,"sr":1}]} \ No newline at end of file diff --git a/Telegram-Mac/lottie/archiveAvatar.json b/Telegram-Mac/lottie/archiveAvatar.json new file mode 100644 index 0000000000..56ba375ec1 --- /dev/null +++ b/Telegram-Mac/lottie/archiveAvatar.json @@ -0,0 +1 @@ +{"v":"5.5.1","fr":60,"ip":0,"op":60,"w":288,"h":288,"nm":"ArchiveAvatar","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"box3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":16,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":22,"s":[-10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":28,"s":[5]},{"t":36,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[144,84,0],"to":[0,0.932,0],"ti":[0,-2.239,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":12,"s":[144,59,0],"to":[0,0.817,0],"ti":[0,-1.967,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":16,"s":[144,84,0],"to":[0,0.838,0],"ti":[0,-0.449,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":22,"s":[144,69,0],"to":[0,1.429,0],"ti":[0,-2.755,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":28,"s":[144,84,0],"to":[0,1.416,0],"ti":[0,0.204,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":32,"s":[144,79,0],"to":[0,-0.33,0],"ti":[0,-0.384,0]},{"t":36,"s":[144,84,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.1,0],[0,0],[0,-1.1],[0,0],[0,0],[0,0]],"o":[[0,0],[1.1,0],[0,0],[0,0],[0,0],[0,-1.1]],"v":[[-11,-3],[11,-3],[13,-1],[13,3],[-13,3],[-13,-1]],"c":true},"ix":2},"nm":"Outline 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"box3","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"box2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[144,135,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[10,3],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":1,"ix":4},"nm":"Rectangle Outline 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.662745098039,0.662745098039,0.678431372549,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"box2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"box1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[144,162,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[1.1,0],[0,0],[0,1.1]],"o":[[0,0],[0,0],[0,1.1],[0,0],[-1.1,0],[0,0]],"v":[[-12,-9],[12,-9],[12,7],[10,9],[-10,9],[-12,7]],"c":true},"ix":2},"nm":"Outline 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"box1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/lottie/group_call_chatlist_typing.json b/Telegram-Mac/lottie/group_call_chatlist_typing.json new file mode 100644 index 0000000000..380a6ffe99 --- /dev/null +++ b/Telegram-Mac/lottie/group_call_chatlist_typing.json @@ -0,0 +1 @@ +{"v":"5.5.9","fr":60,"ip":0,"op":120,"w":18,"h":18,"nm":"ic_vcindicator","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[13,9,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[16.667,16.667,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-5],[0,5]],"c":false},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"3","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[45]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":90,"s":[45]},{"t":120,"s":[30]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[70]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[55]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[90]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":90,"s":[55]},{"t":120,"s":[70]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Обрезать контуры 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[9,9,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[16.667,16.667,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-6],[0,6]],"c":false},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[45]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[40]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":90,"s":[20]},{"t":120,"s":[45]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[55]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[90]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[60]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":90,"s":[80]},{"t":120,"s":[55]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Обрезать контуры 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[5,9,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[16.667,16.667,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,-5],[0,5]],"c":false},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[30]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[45]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":90,"s":[45]},{"t":120,"s":[30]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[70]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":30,"s":[55]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":60,"s":[90]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":90,"s":[55]},{"t":120,"s":[70]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Обрезать контуры 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":120,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/lottie/group_call_member_mute.json b/Telegram-Mac/lottie/group_call_member_mute.json new file mode 100644 index 0000000000..a37a9c8609 --- /dev/null +++ b/Telegram-Mac/lottie/group_call_member_mute.json @@ -0,0 +1 @@ +{"v":"5.5.9","fr":60,"ip":0,"op":20,"w":72,"h":72,"nm":"ic_mute","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Path 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[34.5,37.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-8.5,-8.5],[8.5,8.5]],"c":false},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.33,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Path 4","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Обрезать контуры 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Path 5","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[34.5,37.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-8.5,-8.5],[8.5,8.5]],"c":false},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.109803922474,0.109803922474,0.117647059262,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 2","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Path 4","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Обрезать контуры 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Icon","tt":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[36,37.2,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.36,0],[0,-0.36],[3.81,-0.34],[0,0],[0.37,0],[0.04,0.32],[0,0],[0,0],[0,3.9],[-0.36,0],[0,-0.36],[-3.38,0],[0,3.39]],"o":[[0.37,0],[0,3.9],[0,0],[0,0.37],[-0.33,0],[0,0],[0,0],[-3.81,-0.34],[0,-0.36],[0.37,0],[0,3.39],[3.39,0],[0,-0.36]],"v":[[6.796,-1.464],[7.466,-0.804],[0.666,6.636],[0.666,9.196],[-0.004,9.866],[-0.654,9.286],[-0.664,9.196],[-0.664,6.636],[-7.464,-0.804],[-6.804,-1.464],[-6.134,-0.804],[-0.004,5.336],[6.136,-0.804]],"c":true},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-2.35,0],[-0.09,-2.28],[0,0],[0,0],[2.36,0],[0.1,2.27],[0,0],[0,0]],"o":[[2.3,0],[0,0],[0,0],[0,2.36],[-2.29,0],[0,0],[0,0],[0,-2.35]],"v":[[-0.004,-9.864],[4.256,-5.774],[4.266,-5.604],[4.266,-0.804],[-0.004,3.466],[-4.264,-0.624],[-4.264,-0.804],[-4.264,-5.604]],"c":true},"ix":2},"nm":"Контур 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[1.62,0],[0.09,-1.55],[0,0],[0,0],[-1.63,0],[-0.08,1.55],[0,0],[0,0]],"o":[[-1.57,0],[0,0],[0,0],[0,1.62],[1.57,0],[0,0],[0,0],[0,-1.63]],"v":[[0.004,-8.536],[-2.936,-5.756],[-2.936,-5.596],[-2.936,-0.796],[0.004,2.134],[2.934,-0.646],[2.934,-0.796],[2.934,-5.596]],"c":true},"ix":2},"nm":"Контур 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Объединить контуры 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Заливка 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Icon","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":20,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/lottie/group_call_member_unmute.json b/Telegram-Mac/lottie/group_call_member_unmute.json new file mode 100644 index 0000000000..012f4cde55 --- /dev/null +++ b/Telegram-Mac/lottie/group_call_member_unmute.json @@ -0,0 +1 @@ +{"v":"5.5.9","fr":60,"ip":0,"op":20,"w":72,"h":72,"nm":"ic_unmute","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Path 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[34.5,37.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-8.5,-8.5],[8.5,8.5]],"c":false},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.33,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Path 4","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Обрезать контуры 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Path 5","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[34.5,37.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-8.5,-8.5],[8.5,8.5]],"c":false},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.109803922474,0.109803922474,0.117647059262,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 2","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Path 4","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Обрезать контуры 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Icon","tt":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[36,37.2,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.36,0],[0,-0.36],[3.81,-0.34],[0,0],[0.37,0],[0.04,0.32],[0,0],[0,0],[0,3.9],[-0.36,0],[0,-0.36],[-3.38,0],[0,3.39]],"o":[[0.37,0],[0,3.9],[0,0],[0,0.37],[-0.33,0],[0,0],[0,0],[-3.81,-0.34],[0,-0.36],[0.37,0],[0,3.39],[3.39,0],[0,-0.36]],"v":[[6.796,-1.464],[7.466,-0.804],[0.666,6.636],[0.666,9.196],[-0.004,9.866],[-0.654,9.286],[-0.664,9.196],[-0.664,6.636],[-7.464,-0.804],[-6.804,-1.464],[-6.134,-0.804],[-0.004,5.336],[6.136,-0.804]],"c":true},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-2.35,0],[-0.09,-2.28],[0,0],[0,0],[2.36,0],[0.1,2.27],[0,0],[0,0]],"o":[[2.3,0],[0,0],[0,0],[0,2.36],[-2.29,0],[0,0],[0,0],[0,-2.35]],"v":[[-0.004,-9.864],[4.256,-5.774],[4.266,-5.604],[4.266,-0.804],[-0.004,3.466],[-4.264,-0.624],[-4.264,-0.804],[-4.264,-5.604]],"c":true},"ix":2},"nm":"Контур 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[1.62,0],[0.09,-1.55],[0,0],[0,0],[-1.63,0],[-0.08,1.55],[0,0],[0,0]],"o":[[-1.57,0],[0,0],[0,0],[0,1.62],[1.57,0],[0,0],[0,0],[0,-1.63]],"v":[[0.004,-8.536],[-2.936,-5.756],[-2.936,-5.596],[-2.936,-0.796],[0.004,2.134],[2.934,-0.646],[2.934,-0.796],[2.934,-5.596]],"c":true},"ix":2},"nm":"Контур 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Объединить контуры 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Заливка 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Icon","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":20,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/lottie/group_call_speaker_mute.json b/Telegram-Mac/lottie/group_call_speaker_mute.json new file mode 100644 index 0000000000..f317f65ada --- /dev/null +++ b/Telegram-Mac/lottie/group_call_speaker_mute.json @@ -0,0 +1 @@ +{"v":"5.5.9","fr":60,"ip":0,"op":20,"w":180,"h":180,"nm":"ic_buttonmute","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Path 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[86.25,93.75,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[250,250,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-8.5,-8.5],[8.5,8.5]],"c":false},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.33,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Path 4","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Обрезать контуры 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Path 4","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[86.25,93.75,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[250,250,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-8.5,-8.5],[8.5,8.5]],"c":false},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.33,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"st","c":{"a":0,"k":[0.109803922474,0.109803922474,0.117647059262,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 2","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Path 4","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Обрезать контуры 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Icon","tt":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[90,93,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[250,250,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.36,0],[0,-0.36],[3.81,-0.34],[0,0],[0.37,0],[0.04,0.32],[0,0],[0,0],[0,3.9],[-0.36,0],[0,-0.36],[-3.38,0],[0,3.39]],"o":[[0.37,0],[0,3.9],[0,0],[0,0.37],[-0.33,0],[0,0],[0,0],[-3.81,-0.34],[0,-0.36],[0.37,0],[0,3.39],[3.39,0],[0,-0.36]],"v":[[6.796,-1.464],[7.466,-0.804],[0.666,6.636],[0.666,9.196],[-0.004,9.866],[-0.654,9.286],[-0.664,9.196],[-0.664,6.636],[-7.464,-0.804],[-6.804,-1.464],[-6.134,-0.804],[-0.004,5.336],[6.136,-0.804]],"c":true},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-2.35,0],[-0.09,-2.28],[0,0],[0,0],[2.36,0],[0.1,2.27],[0,0],[0,0]],"o":[[2.3,0],[0,0],[0,0],[0,2.36],[-2.29,0],[0,0],[0,0],[0,-2.35]],"v":[[-0.004,-9.864],[4.256,-5.774],[4.266,-5.604],[4.266,-0.804],[-0.004,3.466],[-4.264,-0.624],[-4.264,-0.804],[-4.264,-5.604]],"c":true},"ix":2},"nm":"Контур 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[1.62,0],[0.09,-1.55],[0,0],[0,0],[-1.63,0],[-0.08,1.55],[0,0],[0,0]],"o":[[-1.57,0],[0,0],[0,0],[0,1.62],[1.57,0],[0,0],[0,0],[0,-1.63]],"v":[[0.004,-8.536],[-2.936,-5.756],[-2.936,-5.596],[-2.936,-0.796],[0.004,2.134],[2.934,-0.646],[2.934,-0.796],[2.934,-5.596]],"c":true},"ix":2},"nm":"Контур 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Объединить контуры 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Заливка 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Icon","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":20,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/lottie/group_call_speaker_unmute.json b/Telegram-Mac/lottie/group_call_speaker_unmute.json new file mode 100644 index 0000000000..980dcd381d --- /dev/null +++ b/Telegram-Mac/lottie/group_call_speaker_unmute.json @@ -0,0 +1 @@ +{"v":"5.5.9","fr":60,"ip":0,"op":20,"w":180,"h":180,"nm":"ic_buttonunmute","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Path 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[86.25,93.75,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[250,250,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-8.5,-8.5],[8.5,8.5]],"c":false},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.33,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Path 4","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Обрезать контуры 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Path 4","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[86.25,93.75,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[250,250,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-8.5,-8.5],[8.5,8.5]],"c":false},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.33,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"st","c":{"a":0,"k":[0.109803922474,0.109803922474,0.117647059262,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 2","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Path 4","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Обрезать контуры 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Icon","tt":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[90,93,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[250,250,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.36,0],[0,-0.36],[3.81,-0.34],[0,0],[0.37,0],[0.04,0.32],[0,0],[0,0],[0,3.9],[-0.36,0],[0,-0.36],[-3.38,0],[0,3.39]],"o":[[0.37,0],[0,3.9],[0,0],[0,0.37],[-0.33,0],[0,0],[0,0],[-3.81,-0.34],[0,-0.36],[0.37,0],[0,3.39],[3.39,0],[0,-0.36]],"v":[[6.796,-1.464],[7.466,-0.804],[0.666,6.636],[0.666,9.196],[-0.004,9.866],[-0.654,9.286],[-0.664,9.196],[-0.664,6.636],[-7.464,-0.804],[-6.804,-1.464],[-6.134,-0.804],[-0.004,5.336],[6.136,-0.804]],"c":true},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-2.35,0],[-0.09,-2.28],[0,0],[0,0],[2.36,0],[0.1,2.27],[0,0],[0,0]],"o":[[2.3,0],[0,0],[0,0],[0,2.36],[-2.29,0],[0,0],[0,0],[0,-2.35]],"v":[[-0.004,-9.864],[4.256,-5.774],[4.266,-5.604],[4.266,-0.804],[-0.004,3.466],[-4.264,-0.624],[-4.264,-0.804],[-4.264,-5.604]],"c":true},"ix":2},"nm":"Контур 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[1.62,0],[0.09,-1.55],[0,0],[0,0],[-1.63,0],[-0.08,1.55],[0,0],[0,0]],"o":[[-1.57,0],[0,0],[0,0],[0,1.62],[1.57,0],[0,0],[0,0],[0,-1.63]],"v":[[0.004,-8.536],[-2.936,-5.756],[-2.936,-5.596],[-2.936,-0.796],[0.004,2.134],[2.934,-0.646],[2.934,-0.796],[2.934,-5.596]],"c":true},"ix":2},"nm":"Контур 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Объединить контуры 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Заливка 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Icon","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":20,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/lottie/maccheck.json b/Telegram-Mac/lottie/maccheck.json new file mode 100644 index 0000000000..ab180a705b --- /dev/null +++ b/Telegram-Mac/lottie/maccheck.json @@ -0,0 +1 @@ +{"v":"5.5.1","fr":60,"ip":10,"op":35,"w":144,"h":144,"nm":"Artboard Copy 3","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Path","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,2.576,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0.12,0.12],[0,0],[0,0]],"o":[[0,0],[0,0.01],[-0.12,0.12],[0,0],[0,0],[0,0]],"v":[[4.5,-3.429],[-1.29,3.321],[-1.3,3.341],[-1.73,3.341],[-4.5,0.571],[-4.5,0.571]],"c":false},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.33,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.501],"y":[0]},"t":10,"s":[100]},{"t":20,"s":[0]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Обрезать контуры 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Oval2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[72,72,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,1.667]},"t":18,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[110,110,100]},{"t":30,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[20,20],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Контур эллипса 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Обводка 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Oval2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Oval1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[72,72,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[0,0,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[110,110,100]},{"t":30,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[20,20],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Контур эллипса 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.180391997099,0.650979995728,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Заливка 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Oval1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/mime-types.txt b/Telegram-Mac/mime-types.txt index d04913f1b4..aa8f0a77e4 100644 --- a/Telegram-Mac/mime-types.txt +++ b/Telegram-Mac/mime-types.txt @@ -19,6 +19,7 @@ aip:text/x-audiosoft-intra ani:application/x-navi-animation aos:application/x-nokia-9000-communicator-add-on-software aps:application/mime +tgs:application/x-tgsticker arc:application/octet-stream arj:application/arj arj:application/octet-stream @@ -247,6 +248,8 @@ m:text/plain m:text/x-m m1v:video/mpeg mp4:video/mp4 +mkv:video/mkv +m4v:video/m4v m2a:audio/mpeg m2v:video/mpeg m3u:audio/x-mpequrl @@ -287,7 +290,7 @@ mme:application/base64 mod:audio/mod mod:audio/x-mod moov:video/quicktime -mov:video/quicktime +mov:video/mov movie:video/x-sgi-movie mp2:audio/mpeg mp2:audio/x-mpeg @@ -559,7 +562,7 @@ wml:text/vnd.wap.wml wmlc:application/vnd.wap.wmlc wmls:text/vnd.wap.wmlscript wmlsc:application/vnd.wap.wmlscriptc -word:application/msword +doc:application/msword wp:application/wordperfect wp5:application/wordperfect wp5:application/wordperfect6.0 diff --git a/Telegram-Mac/nl.lproj/MainMenu.xib b/Telegram-Mac/nl.lproj/MainMenu.xib new file mode 100644 index 0000000000..57e428c68e --- /dev/null +++ b/Telegram-Mac/nl.lproj/MainMenu.xib @@ -0,0 +1,310 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Telegram-Mac/notifies/0.m4a b/Telegram-Mac/notifies/0.m4a new file mode 100644 index 0000000000..e3f9bdcb2a Binary files /dev/null and b/Telegram-Mac/notifies/0.m4a differ diff --git a/Telegram-Mac/notifies/1.m4a b/Telegram-Mac/notifies/1.m4a new file mode 100644 index 0000000000..e3f9bdcb2a Binary files /dev/null and b/Telegram-Mac/notifies/1.m4a differ diff --git a/Telegram-Mac/notifies/100.m4a b/Telegram-Mac/notifies/100.m4a new file mode 100644 index 0000000000..e3f9bdcb2a Binary files /dev/null and b/Telegram-Mac/notifies/100.m4a differ diff --git a/Telegram-Mac/notifies/101.m4a b/Telegram-Mac/notifies/101.m4a new file mode 100644 index 0000000000..a9752552fd Binary files /dev/null and b/Telegram-Mac/notifies/101.m4a differ diff --git a/Telegram-Mac/notifies/102.m4a b/Telegram-Mac/notifies/102.m4a new file mode 100644 index 0000000000..a12676f877 Binary files /dev/null and b/Telegram-Mac/notifies/102.m4a differ diff --git a/Telegram-Mac/notifies/103.m4a b/Telegram-Mac/notifies/103.m4a new file mode 100644 index 0000000000..0382f7ae90 Binary files /dev/null and b/Telegram-Mac/notifies/103.m4a differ diff --git a/Telegram-Mac/notifies/104.m4a b/Telegram-Mac/notifies/104.m4a new file mode 100644 index 0000000000..202132e3f3 Binary files /dev/null and b/Telegram-Mac/notifies/104.m4a differ diff --git a/Telegram-Mac/notifies/105.m4a b/Telegram-Mac/notifies/105.m4a new file mode 100644 index 0000000000..6337b98bfa Binary files /dev/null and b/Telegram-Mac/notifies/105.m4a differ diff --git a/Telegram-Mac/notifies/106.m4a b/Telegram-Mac/notifies/106.m4a new file mode 100644 index 0000000000..f4f340a00c Binary files /dev/null and b/Telegram-Mac/notifies/106.m4a differ diff --git a/Telegram-Mac/notifies/107.m4a b/Telegram-Mac/notifies/107.m4a new file mode 100644 index 0000000000..258ad748b8 Binary files /dev/null and b/Telegram-Mac/notifies/107.m4a differ diff --git a/Telegram-Mac/notifies/108.m4a b/Telegram-Mac/notifies/108.m4a new file mode 100644 index 0000000000..9ea727d8d8 Binary files /dev/null and b/Telegram-Mac/notifies/108.m4a differ diff --git a/Telegram-Mac/notifies/109.m4a b/Telegram-Mac/notifies/109.m4a new file mode 100644 index 0000000000..1c7cd9cb50 Binary files /dev/null and b/Telegram-Mac/notifies/109.m4a differ diff --git a/Telegram-Mac/notifies/110.m4a b/Telegram-Mac/notifies/110.m4a new file mode 100644 index 0000000000..c97210d816 Binary files /dev/null and b/Telegram-Mac/notifies/110.m4a differ diff --git a/Telegram-Mac/notifies/111.m4a b/Telegram-Mac/notifies/111.m4a new file mode 100644 index 0000000000..3d336ab95f Binary files /dev/null and b/Telegram-Mac/notifies/111.m4a differ diff --git a/Telegram-Mac/notifies/2.m4a b/Telegram-Mac/notifies/2.m4a new file mode 100644 index 0000000000..cdb4a44213 Binary files /dev/null and b/Telegram-Mac/notifies/2.m4a differ diff --git a/Telegram-Mac/notifies/3.m4a b/Telegram-Mac/notifies/3.m4a new file mode 100644 index 0000000000..ae3f82ca92 Binary files /dev/null and b/Telegram-Mac/notifies/3.m4a differ diff --git a/Telegram-Mac/notifies/4.m4a b/Telegram-Mac/notifies/4.m4a new file mode 100644 index 0000000000..ca9b0e5805 Binary files /dev/null and b/Telegram-Mac/notifies/4.m4a differ diff --git a/Telegram-Mac/notifies/5.m4a b/Telegram-Mac/notifies/5.m4a new file mode 100644 index 0000000000..59f79b964f Binary files /dev/null and b/Telegram-Mac/notifies/5.m4a differ diff --git a/Telegram-Mac/notifies/6.m4a b/Telegram-Mac/notifies/6.m4a new file mode 100644 index 0000000000..84f27d78fa Binary files /dev/null and b/Telegram-Mac/notifies/6.m4a differ diff --git a/Telegram-Mac/notifies/7.m4a b/Telegram-Mac/notifies/7.m4a new file mode 100644 index 0000000000..7c645cbf11 Binary files /dev/null and b/Telegram-Mac/notifies/7.m4a differ diff --git a/Telegram-Mac/notifies/8.m4a b/Telegram-Mac/notifies/8.m4a new file mode 100644 index 0000000000..c8f2c11d06 Binary files /dev/null and b/Telegram-Mac/notifies/8.m4a differ diff --git a/Telegram-Mac/notifies/9.m4a b/Telegram-Mac/notifies/9.m4a new file mode 100644 index 0000000000..9c0e8d9a89 Binary files /dev/null and b/Telegram-Mac/notifies/9.m4a differ diff --git a/Telegram-Mac/ocr_nn.bin b/Telegram-Mac/ocr_nn.bin new file mode 100755 index 0000000000..ccab89e554 Binary files /dev/null and b/Telegram-Mac/ocr_nn.bin differ diff --git a/Telegram-Mac/pt-BR.lproj/MainMenu.xib b/Telegram-Mac/pt-BR.lproj/MainMenu.xib new file mode 100644 index 0000000000..57e428c68e --- /dev/null +++ b/Telegram-Mac/pt-BR.lproj/MainMenu.xib @@ -0,0 +1,310 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Telegram-Mac/ru.lproj/Localizable.strings b/Telegram-Mac/ru.lproj/Localizable.strings new file mode 100644 index 0000000000..ed60b89c47 Binary files /dev/null and b/Telegram-Mac/ru.lproj/Localizable.strings differ diff --git a/Telegram-Mac/ru.lproj/MainMenu.strings b/Telegram-Mac/ru.lproj/MainMenu.strings new file mode 100644 index 0000000000..8567b3ba83 --- /dev/null +++ b/Telegram-Mac/ru.lproj/MainMenu.strings @@ -0,0 +1,159 @@ + +/* Class = "NSMenuItem"; title = "TelegramMac"; ObjectID = "1Xt-HY-uBw"; */ +"1Xt-HY-uBw.title" = "TelegramMac"; + +/* Class = "NSMenuItem"; title = "Transformations"; ObjectID = "2oI-Rn-ZJC"; */ +"2oI-Rn-ZJC.title" = "Transformations"; + +/* Class = "NSMenu"; title = "Spelling"; ObjectID = "3IN-sU-3Bg"; */ +"3IN-sU-3Bg.title" = "Spelling"; + +/* Class = "NSMenuItem"; title = "Enter Full Screen"; ObjectID = "4J7-dP-txa"; */ +"4J7-dP-txa.title" = "Enter Full Screen"; + +/* Class = "NSMenuItem"; title = "Quit Telegram"; ObjectID = "4sb-4s-VLi"; */ +"4sb-4s-VLi.title" = "Quit Telegram"; + +/* Class = "NSMenuItem"; title = "Edit"; ObjectID = "5QF-Oa-p0T"; */ +"5QF-Oa-p0T.title" = "Edit"; + +/* Class = "NSMenuItem"; title = "About Telegram"; ObjectID = "5kV-Vb-QxS"; */ +"5kV-Vb-QxS.title" = "About Telegram"; + +/* Class = "NSMenuItem"; title = "Redo"; ObjectID = "6dh-zS-Vam"; */ +"6dh-zS-Vam.title" = "Redo"; + +/* Class = "NSMenuItem"; title = "Correct Spelling Automatically"; ObjectID = "78Y-hA-62v"; */ +"78Y-hA-62v.title" = "Correct Spelling Automatically"; + +/* Class = "NSMenuItem"; title = "Substitutions"; ObjectID = "9ic-FL-obx"; */ +"9ic-FL-obx.title" = "Substitutions"; + +/* Class = "NSMenuItem"; title = "Smart Copy/Paste"; ObjectID = "9yt-4B-nSM"; */ +"9yt-4B-nSM.title" = "Smart Copy/Paste"; + +/* Class = "NSMenu"; title = "Main Menu"; ObjectID = "AYu-sK-qS6"; */ +"AYu-sK-qS6.title" = "Main Menu"; + +/* Class = "NSMenuItem"; title = "Preferences…"; ObjectID = "BOF-NM-1cW"; */ +"BOF-NM-1cW.title" = "Preferences…"; + +/* Class = "NSMenuItem"; title = "Hide Telegram"; ObjectID = "Cag-YX-WT6"; */ +"Cag-YX-WT6.title" = "Hide Telegram"; + +/* Class = "NSMenuItem"; title = "Spelling and Grammar"; ObjectID = "Dv1-io-Yv7"; */ +"Dv1-io-Yv7.title" = "Spelling and Grammar"; + +/* Class = "NSMenu"; title = "Substitutions"; ObjectID = "FeM-D8-WVr"; */ +"FeM-D8-WVr.title" = "Substitutions"; + +/* Class = "NSMenuItem"; title = "View"; ObjectID = "H8h-7b-M4v"; */ +"H8h-7b-M4v.title" = "View"; + +/* Class = "NSMenuItem"; title = "Text Replacement"; ObjectID = "HFQ-gK-NFA"; */ +"HFQ-gK-NFA.title" = "Text Replacement"; + +/* Class = "NSMenuItem"; title = "Show Spelling and Grammar"; ObjectID = "HFo-cy-zxI"; */ +"HFo-cy-zxI.title" = "Show Spelling and Grammar"; + +/* Class = "NSMenu"; title = "View"; ObjectID = "HyV-fh-RgO"; */ +"HyV-fh-RgO.title" = "View"; + +/* Class = "NSMenuItem"; title = "Show All"; ObjectID = "Kd2-mp-pUS"; */ +"Kd2-mp-pUS.title" = "Show All"; + +/* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "LE2-aR-0XJ"; */ +"LE2-aR-0XJ.title" = "Bring All to Front"; + +/* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "OY7-WF-poV"; */ +"OY7-WF-poV.title" = "Minimize"; + +/* Class = "NSMenuItem"; title = "Hide"; ObjectID = "Olw-nP-bQN"; */ +"Olw-nP-bQN.title" = "Hide"; + +/* Class = "NSWindow"; title = "Telegram"; ObjectID = "QvC-M9-y7g"; */ +"QvC-M9-y7g.title" = "Telegram"; + +/* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "R4o-n2-Eq4"; */ +"R4o-n2-Eq4.title" = "Zoom"; + +/* Class = "NSMenuItem"; title = "Select All"; ObjectID = "Ruw-6m-B2m"; */ +"Ruw-6m-B2m.title" = "Select All"; + +/* Class = "NSMenu"; title = "Window"; ObjectID = "Td7-aD-5lo"; */ +"Td7-aD-5lo.title" = "Window"; + +/* Class = "NSMenuItem"; title = "Capitalize"; ObjectID = "UEZ-Bs-lqG"; */ +"UEZ-Bs-lqG.title" = "Capitalize"; + +/* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "Vdr-fp-XzO"; */ +"Vdr-fp-XzO.title" = "Hide Others"; + +/* Class = "NSMenu"; title = "Edit"; ObjectID = "W48-6f-4Dl"; */ +"W48-6f-4Dl.title" = "Edit"; + +/* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "WeT-3V-zwk"; */ +"WeT-3V-zwk.title" = "Paste and Match Style"; + +/* Class = "NSMenuItem"; title = "Window"; ObjectID = "aUF-d1-5bR"; */ +"aUF-d1-5bR.title" = "Window"; + +/* Class = "NSMenu"; title = "Transformations"; ObjectID = "c8a-y6-VQd"; */ +"c8a-y6-VQd.title" = "Transformations"; + +/* Class = "NSMenuItem"; title = "Smart Links"; ObjectID = "cwL-P1-jid"; */ +"cwL-P1-jid.title" = "Smart Links"; + +/* Class = "NSMenuItem"; title = "Make Lower Case"; ObjectID = "d9M-CD-aMd"; */ +"d9M-CD-aMd.title" = "Make Lower Case"; + +/* Class = "NSMenuItem"; title = "Undo"; ObjectID = "dRJ-4n-Yzg"; */ +"dRJ-4n-Yzg.title" = "Undo"; + +/* Class = "NSMenuItem"; title = "Paste"; ObjectID = "gVA-U4-sdL"; */ +"gVA-U4-sdL.title" = "Paste"; + +/* Class = "NSMenuItem"; title = "Smart Quotes"; ObjectID = "hQb-2v-fYv"; */ +"hQb-2v-fYv.title" = "Smart Quotes"; + +/* Class = "NSMenuItem"; title = "Check Document Now"; ObjectID = "hz2-CU-CR7"; */ +"hz2-CU-CR7.title" = "Check Document Now"; + +/* Class = "NSMenuItem"; title = "Check Grammar With Spelling"; ObjectID = "mK6-2p-4JG"; */ +"mK6-2p-4JG.title" = "Check Grammar With Spelling"; + +/* Class = "NSMenuItem"; title = "Delete"; ObjectID = "pa3-QI-u2k"; */ +"pa3-QI-u2k.title" = "Delete"; + +/* Class = "NSMenuItem"; title = "Check Spelling While Typing"; ObjectID = "rbD-Rh-wIN"; */ +"rbD-Rh-wIN.title" = "Check Spelling While Typing"; + +/* Class = "NSMenuItem"; title = "Smart Dashes"; ObjectID = "rgM-f4-ycn"; */ +"rgM-f4-ycn.title" = "Smart Dashes"; + +/* Class = "NSMenuItem"; title = "Quick Switcher"; ObjectID = "sZh-ct-GQS"; */ +"sZh-ct-GQS.title" = "Quick Switcher"; + +/* Class = "NSMenuItem"; title = "Data Detectors"; ObjectID = "tRr-pd-1PS"; */ +"tRr-pd-1PS.title" = "Data Detectors"; + +/* Class = "NSMenu"; title = "TelegramMac"; ObjectID = "uQy-DD-JDr"; */ +"uQy-DD-JDr.title" = "TelegramMac"; + +/* Class = "NSMenuItem"; title = "Cut"; ObjectID = "uRl-iY-unG"; */ +"uRl-iY-unG.title" = "Cut"; + +/* Class = "NSMenuItem"; title = "Make Upper Case"; ObjectID = "vmV-6d-7jI"; */ +"vmV-6d-7jI.title" = "Make Upper Case"; + +/* Class = "NSMenuItem"; title = "Copy"; ObjectID = "x3v-GG-iWU"; */ +"x3v-GG-iWU.title" = "Copy"; + +/* Class = "NSMenuItem"; title = "Check for Updates"; ObjectID = "xey-M7-XVy"; */ +"xey-M7-XVy.title" = "Check for Updates"; + +/* Class = "NSMenuItem"; title = "Telegram"; ObjectID = "yUb-j6-EX5"; */ +"yUb-j6-EX5.title" = "Telegram"; + +/* Class = "NSMenuItem"; title = "Show Substitutions"; ObjectID = "z6F-FW-3nz"; */ +"z6F-FW-3nz.title" = "Show Substitutions"; diff --git a/Telegram-Mac/sound_a.caf b/Telegram-Mac/sound_a.caf new file mode 100644 index 0000000000..0a9f875f20 Binary files /dev/null and b/Telegram-Mac/sound_a.caf differ diff --git a/Telegram-Mac/sounds/Pop.wav b/Telegram-Mac/sounds/Pop.wav new file mode 100644 index 0000000000..ab67cd5ed4 Binary files /dev/null and b/Telegram-Mac/sounds/Pop.wav differ diff --git a/Telegram-Mac/sounds/Purr.wav b/Telegram-Mac/sounds/Purr.wav new file mode 100644 index 0000000000..bad5d8fda8 Binary files /dev/null and b/Telegram-Mac/sounds/Purr.wav differ diff --git a/Telegram-Mac/sounds/call down.mp3 b/Telegram-Mac/sounds/call down.mp3 new file mode 100644 index 0000000000..a8d79f0aa1 Binary files /dev/null and b/Telegram-Mac/sounds/call down.mp3 differ diff --git a/Telegram-Mac/sounds/call up.mp3 b/Telegram-Mac/sounds/call up.mp3 new file mode 100644 index 0000000000..314b1b16fe Binary files /dev/null and b/Telegram-Mac/sounds/call up.mp3 differ diff --git a/Telegram-Mac/sounds/confetti.mp3 b/Telegram-Mac/sounds/confetti.mp3 new file mode 100644 index 0000000000..9b3ad7553c Binary files /dev/null and b/Telegram-Mac/sounds/confetti.mp3 differ diff --git a/Telegram-Mac/sounds/quiz-correct.mp3 b/Telegram-Mac/sounds/quiz-correct.mp3 new file mode 100644 index 0000000000..1458478169 Binary files /dev/null and b/Telegram-Mac/sounds/quiz-correct.mp3 differ diff --git a/Telegram-Mac/sounds/quiz-incorrect.mp3 b/Telegram-Mac/sounds/quiz-incorrect.mp3 new file mode 100644 index 0000000000..bfb30453df Binary files /dev/null and b/Telegram-Mac/sounds/quiz-incorrect.mp3 differ diff --git a/Telegram-Mac/sounds/reconnecting.mp3 b/Telegram-Mac/sounds/reconnecting.mp3 new file mode 100644 index 0000000000..4e0f60b4f1 Binary files /dev/null and b/Telegram-Mac/sounds/reconnecting.mp3 differ diff --git a/Telegram-Mac/sounds/voip_group_recording_started.mp3 b/Telegram-Mac/sounds/voip_group_recording_started.mp3 new file mode 100644 index 0000000000..0060e82c37 Binary files /dev/null and b/Telegram-Mac/sounds/voip_group_recording_started.mp3 differ diff --git a/Telegram-Mac/sounds/voip_group_unmuted.mp3 b/Telegram-Mac/sounds/voip_group_unmuted.mp3 new file mode 100644 index 0000000000..b5dfaa606c Binary files /dev/null and b/Telegram-Mac/sounds/voip_group_unmuted.mp3 differ diff --git a/Telegram-Mac/tgs/bot_close_menu.tgs b/Telegram-Mac/tgs/bot_close_menu.tgs new file mode 100644 index 0000000000..09ad0b611c --- /dev/null +++ b/Telegram-Mac/tgs/bot_close_menu.tgs @@ -0,0 +1 @@ +{"v":"5.7.8","fr":60,"ip":0,"op":20,"w":30,"h":30,"nm":"CloseMenu","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"3","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[180]},{"t":15,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[0,0,0],"to":[0,5,0],"ti":[0,-5,0]},{"t":15,"s":[0,30,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-5,5],[5,-5]],"c":false}]},{"t":15,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-6.5,0],[6.5,0]],"c":false}]}],"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[90]},{"t":15,"s":[0]}],"ix":10},"p":{"a":0,"k":[15,15,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[16.667,16.667,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-6.5,0],[6.5,0]],"c":false},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[50]},{"t":5,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[50]},{"t":5,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Обрезать контуры 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"1","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[90]},{"t":15,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[0,0,0],"to":[0,-5,0],"ti":[0,5,0]},{"t":15,"s":[0,-30,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-5,5],[5,-5]],"c":false}]},{"t":15,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-6.5,0],[6.5,0]],"c":false}]}],"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":20,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/tgs/bot_menu_close.tgs b/Telegram-Mac/tgs/bot_menu_close.tgs new file mode 100644 index 0000000000..c29ed679ce --- /dev/null +++ b/Telegram-Mac/tgs/bot_menu_close.tgs @@ -0,0 +1 @@ +{"v":"5.7.8","fr":60,"ip":0,"op":20,"w":30,"h":30,"nm":"MenuClose","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"3","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":15,"s":[180]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[0,30,0],"to":[0,-5,0],"ti":[0,5,0]},{"t":15,"s":[0,0,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-6.5,0],[6.5,0]],"c":false}]},{"t":15,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-5,5],[5,-5]],"c":false}]}],"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":15,"s":[90]}],"ix":10},"p":{"a":0,"k":[15,15,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[16.667,16.667,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-6.5,0],[6.5,0]],"c":false},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":5,"s":[50]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":5,"s":[50]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Обрезать контуры 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":20,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"1","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":15,"s":[90]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[0,-30,0],"to":[0,5,0],"ti":[0,-5,0]},{"t":15,"s":[0,0,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-6.5,0],[6.5,0]],"c":false}]},{"t":15,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-5,5],[5,-5]],"c":false}]}],"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Обводка 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":20,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/tgs/brilliant_loading.tgs b/Telegram-Mac/tgs/brilliant_loading.tgs new file mode 100644 index 0000000000..f89875bf97 Binary files /dev/null and b/Telegram-Mac/tgs/brilliant_loading.tgs differ diff --git a/Telegram-Mac/tgs/brilliant_static.tgs b/Telegram-Mac/tgs/brilliant_static.tgs new file mode 100644 index 0000000000..63e8e22514 Binary files /dev/null and b/Telegram-Mac/tgs/brilliant_static.tgs differ diff --git a/Telegram-Mac/tgs/cameraoff.tgs b/Telegram-Mac/tgs/cameraoff.tgs new file mode 100644 index 0000000000..59e893c387 --- /dev/null +++ b/Telegram-Mac/tgs/cameraoff.tgs @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":19,"w":100,"h":100,"nm":"Camera Off","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Line","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.4,"y":0},"t":0,"s":[-37.845,-19,0],"to":[0,0,0],"ti":[0,0,0]},{"t":17,"s":[-20.845,0,0]}],"ix":2},"a":{"a":0,"k":[-20.845,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-148.295,-127.55],[106.605,127.55]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":0,"s":[0]},{"t":17,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Lens","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[129.155,0,0],"ix":2},"a":{"a":0,"k":[129.155,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-5.175,-4.14],[0,-3.645],[0,0],[6.627,0],[2.277,2.847],[0,0],[0,6.812],[0,0],[-4.256,5.319],[0,0]],"o":[[2.847,2.277],[0,0],[0,6.627],[-3.645,0],[0,0],[-4.256,-5.319],[0,0],[0,-6.812],[0,0],[4.14,-5.175]],"v":[[154.651,-72.661],[159.155,-63.291],[159.155,63.291],[147.155,75.291],[137.785,70.787],[105.729,30.717],[99.155,11.977],[99.155,-11.977],[105.729,-30.717],[137.785,-70.787]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Bottom","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-42.326,21.481,0],"ix":2},"a":{"a":0,"k":[-42.326,21.481,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-0.356,4.018],[-7.24,7.49],[-29.064,-26.935],[-4.055,-3.802],[-16.419,-20.572],[7.432,0.162],[0,0],[7.333,3.922],[3.922,7.333],[0,20.059]],"o":[[0,0],[1.418,-1.467],[3.856,3.573],[47.36,44.399],[-5.621,7.372],[-4.82,-0.105],[-20.059,0],[-7.333,-3.922],[-3.922,-7.333],[0,0]],"v":[[-125.308,-62.038],[-117.76,-85.49],[-64.836,-40.48],[-52.953,-29.401],[69.496,93.378],[41.193,104.463],[-68.158,105],[-102.824,98.99],[-119.835,81.979],[-125.845,47.313]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[-0.356,4.018],[0,0],[-6.725,-5.399],[-8.634,13.367],[-11.184,-14.013],[5.309,0.116],[0,0],[7.333,3.922],[3.922,7.333],[0,20.059]],"o":[[0,0],[0,0],[3.474,2.789],[45.245,43.363],[-6.779,7.936],[-4.591,0.027],[-20.059,0],[-7.333,-3.922],[-3.922,-7.333],[0,0]],"v":[[-125.308,-62.038],[-125.79,-62.659],[-100.364,-42.108],[-71.876,-49.032],[68.592,92.067],[41.193,104.463],[-68.158,105],[-102.824,98.99],[-119.835,81.979],[-125.845,47.313]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[-0.356,4.018],[-0.139,1.164],[-35.512,-31.502],[-10.7,15.758],[-8.567,-10.733],[4.247,0.093],[0,0],[7.333,3.922],[3.922,7.333],[0,20.059]],"o":[[0,0],[0.162,-1.357],[3.033,2.691],[44.188,42.845],[-7.357,8.219],[-4.476,0.092],[-20.059,0],[-7.333,-3.922],[-3.922,-7.333],[0,0]],"v":[[-125.308,-62.038],[-125.463,-63.085],[-63.655,-3.449],[-32.391,-13.058],[68.14,91.412],[41.193,104.463],[-68.158,105],[-102.824,98.989],[-119.835,81.979],[-125.845,47.313]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-0.356,4.018],[-0.069,0.582],[-17.756,-15.751],[-15.673,19.67],[-4.283,-5.367],[2.123,0.046],[0,0],[7.333,3.922],[3.922,7.333],[0,20.059]],"o":[[0,0],[0.081,-0.679],[1.517,1.345],[22.094,21.423],[-8.352,10.491],[-4.247,0.224],[-20.059,0],[-7.333,-3.922],[-3.922,-7.333],[0,0]],"v":[[-125.308,-62.038],[-125.303,-62.479],[1.473,63.485],[36.955,57.421],[68.7,90.176],[41.193,104.463],[-68.158,105],[-102.824,98.989],[-119.835,81.979],[-125.845,47.313]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":9,"s":[{"i":[[-0.356,4.018],[-0.035,0.291],[-8.878,-7.876],[0,0],[0,0],[1.062,0.023],[0,0],[7.333,3.922],[3.922,7.333],[0,20.059]],"o":[[0,0],[0.04,-0.339],[0.758,0.673],[0,0],[0,0],[-4.133,0.29],[-20.059,0],[-7.333,-3.922],[-3.922,-7.333],[0,0]],"v":[[-125.308,-62.038],[-125.223,-62.176],[-31.21,31.431],[-17.519,45.655],[41.859,104.708],[41.193,104.463],[-68.158,105],[-102.824,98.989],[-119.835,81.979],[-125.845,47.313]],"c":true}]},{"t":10,"s":[{"i":[[-0.356,4.018],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[7.333,3.922],[3.922,7.333],[0,20.059]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[-4.018,0.356],[-20.059,0],[-7.333,-3.922],[-3.922,-7.333],[0,0]],"v":[[-125.308,-62.038],[-125.143,-61.873],[-63.893,-0.623],[-61.477,1.793],[40.474,103.744],[41.193,104.463],[-68.158,105],[-102.824,98.989],[-119.835,81.979],[-125.845,47.313]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Top","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[245.702,245.695,0],"ix":2},"a":{"a":0,"k":[-10.298,-10.305,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-0.333,0.63],[-20.059,0],[0,0],[-7.333,-3.922],[-3.922,-7.333],[0,-20.059],[0,0],[10.875,-10.515]],"o":[[9,-17.056],[0,0],[20.059,0],[7.333,3.922],[3.922,7.333],[0,0],[0,20.059],[0,0]],"v":[[-117.25,-85.819],[-68.158,-105],[26.468,-105],[61.134,-98.99],[78.145,-81.979],[84.155,-47.313],[84.155,47.313],[69.5,93.515]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[-0.45,0.521],[-20.059,0],[0,0],[-7.333,-3.922],[-3.922,-7.333],[0,-20.059],[0,0],[8.786,-9.908]],"o":[[9,-12.139],[0,0],[20.059,0],[7.333,3.922],[3.922,7.333],[0,0],[0,20.059],[0,0]],"v":[[-111.893,-91.015],[-68.158,-105],[26.468,-105],[61.134,-98.99],[78.145,-81.979],[84.155,-47.313],[84.155,47.313],[67.374,92.541]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-0.528,0.448],[-20.059,0],[0,0],[-7.333,-3.922],[-3.922,-7.333],[0,-20.059],[0,0],[10.778,-11.375]],"o":[[9,-8.861],[0,0],[20.059,0],[7.333,3.922],[3.922,7.333],[0,0],[0,20.059],[0,0]],"v":[[-108.321,-94.479],[-68.158,-105],[26.468,-105],[61.134,-98.99],[78.145,-81.979],[84.155,-47.313],[84.155,47.313],[68.897,90.659]],"c":true}]},{"t":10,"s":[{"i":[[-0.606,0.376],[-20.059,0],[0,0],[-7.333,-3.922],[-3.922,-7.333],[0,-20.059],[0,0],[6,-9.099]],"o":[[9,-5.583],[0,0],[20.059,0],[7.333,3.922],[3.922,7.333],[0,0],[0,20.059],[0,0]],"v":[[-104.75,-97.944],[-68.158,-105],[26.468,-105],[61.134,-98.99],[78.145,-81.979],[84.155,-47.313],[84.155,47.313],[77,84.39]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Pre-comp 1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[13.6,13.6,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":180,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/tgs/cameraon.tgs b/Telegram-Mac/tgs/cameraon.tgs new file mode 100644 index 0000000000..ed42e29a8b --- /dev/null +++ b/Telegram-Mac/tgs/cameraon.tgs @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":24,"w":100,"h":100,"nm":"Camera On","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Line","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.4,"y":0},"t":2,"s":[-20.845,0,0],"to":[0,0,0],"ti":[0,0,0]},{"t":23,"s":[-24.845,-5,0]}],"ix":2},"a":{"a":0,"k":[-20.845,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-148.295,-127.55],[106.605,127.55]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":2,"s":[100]},{"t":23,"s":[0]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":22,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Lens","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[129.155,0,0],"ix":2},"a":{"a":0,"k":[129.155,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-5.175,-4.14],[0,-3.645],[0,0],[6.627,0],[2.277,2.847],[0,0],[0,6.812],[0,0],[-4.256,5.319],[0,0]],"o":[[2.847,2.277],[0,0],[0,6.627],[-3.645,0],[0,0],[-4.256,-5.319],[0,0],[0,-6.812],[0,0],[4.14,-5.175]],"v":[[154.651,-72.661],[159.155,-63.291],[159.155,63.291],[147.155,75.291],[137.785,70.787],[105.729,30.717],[99.155,11.977],[99.155,-11.977],[105.729,-30.717],[137.785,-70.787]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Bottom","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-42.326,21.481,0],"ix":2},"a":{"a":0,"k":[-42.326,21.481,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":7,"s":[{"i":[[-0.356,4.018],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[7.333,3.922],[3.922,7.333],[0,20.059]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[-4.018,0.356],[-20.059,0],[-7.333,-3.922],[-3.922,-7.333],[0,0]],"v":[[-125.308,-62.038],[-125.143,-61.873],[-63.893,-0.623],[-61.477,1.793],[40.474,103.744],[41.193,104.463],[-68.158,105],[-102.824,98.989],[-119.835,81.979],[-125.845,47.313]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-0.356,4.018],[-0.035,0.291],[-8.878,-7.876],[0,0],[0,0],[1.062,0.023],[0,0],[7.333,3.922],[3.922,7.333],[0,20.059]],"o":[[0,0],[0.04,-0.339],[0.758,0.673],[0,0],[0,0],[-4.133,0.29],[-20.059,0],[-7.333,-3.922],[-3.922,-7.333],[0,0]],"v":[[-125.308,-62.038],[-125.223,-62.176],[-31.21,31.431],[-17.519,45.655],[41.859,104.708],[41.193,104.463],[-68.158,105],[-102.824,98.989],[-119.835,81.979],[-125.845,47.313]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":9,"s":[{"i":[[-0.356,4.018],[-0.069,0.582],[-17.756,-15.751],[-15.673,19.67],[-4.283,-5.367],[2.123,0.046],[0,0],[7.333,3.922],[3.922,7.333],[0,20.059]],"o":[[0,0],[0.081,-0.679],[1.517,1.345],[22.094,21.423],[-8.352,10.491],[-4.247,0.224],[-20.059,0],[-7.333,-3.922],[-3.922,-7.333],[0,0]],"v":[[-125.308,-62.038],[-125.303,-62.479],[1.473,63.485],[36.955,57.421],[68.7,90.176],[41.193,104.463],[-68.158,105],[-102.824,98.989],[-119.835,81.979],[-125.845,47.313]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[-0.356,4.018],[-0.139,1.164],[-35.512,-31.502],[-12.664,20.241],[-8.567,-10.733],[4.247,0.093],[0,0],[7.333,3.922],[3.922,7.333],[0,20.059]],"o":[[0,0],[0.162,-1.357],[3.033,2.691],[44.188,42.845],[-7.357,8.219],[-4.476,0.092],[-20.059,0],[-7.333,-3.922],[-3.922,-7.333],[0,0]],"v":[[-125.308,-62.038],[-125.463,-63.085],[-63.655,-3.449],[-28.714,-7.421],[68.14,91.412],[41.193,104.463],[-68.158,105],[-102.824,98.989],[-119.835,81.979],[-125.845,47.313]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[-0.356,4.018],[0,0],[-6.247,-5.946],[-8.634,13.367],[-11.184,-14.013],[5.309,0.116],[0,0],[7.333,3.922],[3.922,7.333],[0,20.059]],"o":[[0,0],[0,0],[9.673,9.207],[45.245,43.363],[-6.779,7.936],[-4.591,0.027],[-20.059,0],[-7.333,-3.922],[-3.922,-7.333],[0,0]],"v":[[-125.308,-62.038],[-125.79,-62.659],[-88.593,-23.96],[-52.747,-27.696],[68.592,92.067],[41.193,104.463],[-68.158,105],[-102.824,98.99],[-119.835,81.979],[-125.845,47.313]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13,"s":[{"i":[[-0.356,4.018],[0,0],[-6.562,-5.596],[-8.634,13.367],[-11.184,-14.013],[5.309,0.116],[0,0],[7.333,3.922],[3.922,7.333],[0,20.059]],"o":[[0,0],[0,0],[8.587,7.323],[45.245,43.363],[-6.779,7.936],[-4.591,0.027],[-20.059,0],[-7.333,-3.922],[-3.922,-7.333],[0,0]],"v":[[-125.308,-62.038],[-125.79,-62.659],[-105.786,-43.118],[-72.883,-47.59],[68.592,92.067],[41.193,104.463],[-68.158,105],[-102.824,98.99],[-119.835,81.979],[-125.845,47.313]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":14,"s":[{"i":[[-0.356,4.018],[-3.62,3.745],[-2.721,-1.768],[-2.961,12.651],[-13.801,-17.292],[6.37,0.139],[0,0],[7.333,3.922],[3.922,7.333],[0,20.059]],"o":[[0,0],[0.709,-0.734],[3.665,3.181],[46.303,43.881],[-6.2,7.654],[-4.705,-0.039],[-20.059,0],[-7.333,-3.922],[-3.922,-7.333],[0,0]],"v":[[-125.308,-62.038],[-124.975,-62.998],[-114.11,-56.321],[-93.932,-64.833],[69.044,92.723],[41.193,104.463],[-68.158,105],[-102.824,98.99],[-119.835,81.979],[-125.845,47.313]],"c":true}]},{"t":15,"s":[{"i":[[-0.356,4.018],[-7.24,7.49],[-29.064,-26.935],[-4.055,-3.802],[-16.419,-20.572],[7.432,0.162],[0,0],[7.333,3.922],[3.922,7.333],[0,20.059]],"o":[[0,0],[1.418,-1.467],[3.856,3.573],[47.36,44.399],[-5.621,7.372],[-4.82,-0.105],[-20.059,0],[-7.333,-3.922],[-3.922,-7.333],[0,0]],"v":[[-125.308,-62.038],[-117.76,-85.49],[-64.836,-40.48],[-52.953,-29.401],[69.496,93.378],[41.193,104.463],[-68.158,105],[-102.824,98.99],[-119.835,81.979],[-125.845,47.313]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Top","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[245.702,245.695,0],"ix":2},"a":{"a":0,"k":[-10.298,-10.305,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":7,"s":[{"i":[[-0.606,0.376],[-20.059,0],[0,0],[-7.333,-3.922],[-3.922,-7.333],[0,-20.059],[0,0],[6,-9.099]],"o":[[9,-5.583],[0,0],[20.059,0],[7.333,3.922],[3.922,7.333],[0,0],[0,20.059],[0,0]],"v":[[-104.75,-97.944],[-68.158,-105],[26.468,-105],[61.134,-98.99],[78.145,-81.979],[84.155,-47.313],[84.155,47.313],[77,84.39]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":9,"s":[{"i":[[-0.528,0.448],[-20.059,0],[0,0],[-7.333,-3.922],[-3.922,-7.333],[0,-20.059],[0,0],[10.778,-11.375]],"o":[[9,-8.861],[0,0],[20.059,0],[7.333,3.922],[3.922,7.333],[0,0],[0,20.059],[0,0]],"v":[[-108.321,-94.479],[-68.158,-105],[26.468,-105],[61.134,-98.99],[78.145,-81.979],[84.155,-47.313],[84.155,47.313],[68.897,90.659]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[-0.45,0.521],[-20.059,0],[0,0],[-7.333,-3.922],[-3.922,-7.333],[0,-20.059],[0,0],[8.786,-9.908]],"o":[[9,-12.139],[0,0],[20.059,0],[7.333,3.922],[3.922,7.333],[0,0],[0,20.059],[0,0]],"v":[[-111.893,-91.015],[-68.158,-105],[26.468,-105],[61.134,-98.99],[78.145,-81.979],[84.155,-47.313],[84.155,47.313],[67.374,92.541]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13,"s":[{"i":[[-0.333,0.63],[-20.059,0],[0,0],[-7.333,-3.922],[-3.922,-7.333],[0,-20.059],[0,0],[10.875,-10.515]],"o":[[9,-17.056],[0,0],[20.059,0],[7.333,3.922],[3.922,7.333],[0,0],[0,20.059],[0,0]],"v":[[-117.25,-85.819],[-68.158,-105],[26.468,-105],[61.134,-98.99],[78.145,-81.979],[84.155,-47.313],[84.155,47.313],[69.5,93.515]],"c":true}]},{"t":15,"s":[{"i":[[-0.333,0.63],[-20.059,0],[0,0],[-7.333,-3.922],[-3.922,-7.333],[0,-20.059],[0,0],[10.875,-10.515]],"o":[[9,-17.056],[0,0],[20.059,0],[7.333,3.922],[3.922,7.333],[0,0],[0,20.059],[0,0]],"v":[[-117.25,-85.819],[-68.158,-105],[26.468,-105],[61.134,-98.99],[78.145,-81.979],[84.155,-47.313],[84.155,47.313],[69.5,93.515]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Pre-comp 1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[13.6,13.6,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":180,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/tgs/chiken_born.tgs b/Telegram-Mac/tgs/chiken_born.tgs new file mode 100644 index 0000000000..e4b8e01649 Binary files /dev/null and b/Telegram-Mac/tgs/chiken_born.tgs differ diff --git a/Telegram-Mac/tgs/dart_idle.tgs b/Telegram-Mac/tgs/dart_idle.tgs new file mode 100644 index 0000000000..ee3753cf7e Binary files /dev/null and b/Telegram-Mac/tgs/dart_idle.tgs differ diff --git a/Telegram-Mac/tgs/destructor.tgs b/Telegram-Mac/tgs/destructor.tgs new file mode 100644 index 0000000000..c65577a62a Binary files /dev/null and b/Telegram-Mac/tgs/destructor.tgs differ diff --git a/Telegram-Mac/tgs/dice_idle.tgs b/Telegram-Mac/tgs/dice_idle.tgs new file mode 100644 index 0000000000..aad33bffe1 Binary files /dev/null and b/Telegram-Mac/tgs/dice_idle.tgs differ diff --git a/Telegram-Mac/tgs/discussion.tgs b/Telegram-Mac/tgs/discussion.tgs new file mode 100644 index 0000000000..b513b9457a Binary files /dev/null and b/Telegram-Mac/tgs/discussion.tgs differ diff --git a/Telegram-Mac/tgs/fly_dollar.tgs b/Telegram-Mac/tgs/fly_dollar.tgs new file mode 100644 index 0000000000..9876c9658e Binary files /dev/null and b/Telegram-Mac/tgs/fly_dollar.tgs differ diff --git a/Telegram-Mac/tgs/folder.tgs b/Telegram-Mac/tgs/folder.tgs new file mode 100644 index 0000000000..41b820a74d Binary files /dev/null and b/Telegram-Mac/tgs/folder.tgs differ diff --git a/Telegram-Mac/tgs/folder_empty.tgs b/Telegram-Mac/tgs/folder_empty.tgs new file mode 100644 index 0000000000..999bdabbf4 Binary files /dev/null and b/Telegram-Mac/tgs/folder_empty.tgs differ diff --git a/Telegram-Mac/tgs/gift.tgs b/Telegram-Mac/tgs/gift.tgs new file mode 100644 index 0000000000..4e87c321ed Binary files /dev/null and b/Telegram-Mac/tgs/gift.tgs differ diff --git a/Telegram-Mac/tgs/gigagroup.tgs b/Telegram-Mac/tgs/gigagroup.tgs new file mode 100644 index 0000000000..665f7bc10f Binary files /dev/null and b/Telegram-Mac/tgs/gigagroup.tgs differ diff --git a/Telegram-Mac/tgs/graph_loading.tgs b/Telegram-Mac/tgs/graph_loading.tgs new file mode 100644 index 0000000000..28b03df976 Binary files /dev/null and b/Telegram-Mac/tgs/graph_loading.tgs differ diff --git a/Telegram-Mac/tgs/invitations.tgs b/Telegram-Mac/tgs/invitations.tgs new file mode 100644 index 0000000000..de9333de44 Binary files /dev/null and b/Telegram-Mac/tgs/invitations.tgs differ diff --git a/Telegram-Mac/tgs/keyboard_typing.tgs b/Telegram-Mac/tgs/keyboard_typing.tgs new file mode 100644 index 0000000000..c8e9940a40 Binary files /dev/null and b/Telegram-Mac/tgs/keyboard_typing.tgs differ diff --git a/Telegram-Mac/tgs/keychain.tgs b/Telegram-Mac/tgs/keychain.tgs new file mode 100644 index 0000000000..8538275214 Binary files /dev/null and b/Telegram-Mac/tgs/keychain.tgs differ diff --git a/Telegram-Mac/tgs/monkey_see.tgs b/Telegram-Mac/tgs/monkey_see.tgs new file mode 100644 index 0000000000..07cca60ece Binary files /dev/null and b/Telegram-Mac/tgs/monkey_see.tgs differ diff --git a/Telegram-Mac/tgs/monkey_unsee.tgs b/Telegram-Mac/tgs/monkey_unsee.tgs new file mode 100644 index 0000000000..f16eca5ada Binary files /dev/null and b/Telegram-Mac/tgs/monkey_unsee.tgs differ diff --git a/Telegram-Mac/tgs/new_folder.tgs b/Telegram-Mac/tgs/new_folder.tgs new file mode 100644 index 0000000000..ee8755f78a Binary files /dev/null and b/Telegram-Mac/tgs/new_folder.tgs differ diff --git a/Telegram-Mac/tgs/playlist_pause_play.tgs b/Telegram-Mac/tgs/playlist_pause_play.tgs new file mode 100644 index 0000000000..91bd4771d9 --- /dev/null +++ b/Telegram-Mac/tgs/playlist_pause_play.tgs @@ -0,0 +1 @@ +{"v":"5.7.1","fr":60,"ip":0,"op":27,"w":320,"h":320,"nm":"Play and Pause 3","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Path 16","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.365],"y":[0]},"t":20,"s":[-95]},{"t":24,"s":[-90]}],"ix":10},"p":{"a":0,"k":[160.5,160.125,0],"ix":2},"a":{"a":0,"k":[8,0.167,0],"ix":1},"s":{"a":0,"k":[300,-300,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[{"i":[[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21]],"o":[[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0]],"v":[[-11.5,-22],[-7.5,-26],[0.5,-26],[4.5,-22],[4.5,22],[0.5,26],[-7.5,26],[-11.5,22]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[0,0],[-2.629,0.034],[-1.514,0.021],[0,-2.21],[0,0],[2.21,0],[0,0],[-0.261,2.088]],"o":[[0.893,-2.862],[0.448,0.002],[1.89,-0.018],[0,0],[0,2.21],[0,0],[-2.492,0],[0.536,-1.372]],"v":[[-4.347,-21.598],[0.914,-25.133],[5.329,-25.126],[7.61,-21.384],[7.633,21.574],[5.299,25.592],[-10.342,25.534],[-14.03,21.126]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[-2.796,0.048],[-2.12,0.03],[0,-2.21],[0,0],[2.21,0],[0,0],[-0.365,2.04]],"o":[[1.25,-3.123],[0.628,0.002],[1.762,-0.025],[0,0],[0,2.21],[0,0],[-2.605,0],[0.75,-1.921]],"v":[[-1.485,-21.438],[4.28,-24.786],[7.728,-24.796],[9.321,-21.156],[9.352,21.384],[7.686,25.41],[-11.479,25.347],[-15.042,20.776]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[-4.472,9.732],[-3.258,-0.034],[-1.795,0.001],[0,-2.21],[0,0],[2.21,0],[0,0],[-0.688,1.889]],"o":[[1.52,-3.307],[1.81,0.019],[2.632,0.001],[0,0],[0,2.21],[0,0],[-2.955,0],[1.415,-3.623]],"v":[[0.18,-17.732],[5.758,-23.992],[7.066,-23.989],[10.962,-20.403],[10.958,20.696],[6.958,24.696],[-13.454,24.715],[-16.628,19.638]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13,"s":[{"i":[[-4.659,9.537],[-2.427,0.018],[-1.618,0.014],[0,-2.21],[0,0],[2.21,0],[0,0],[-0.787,1.842]],"o":[[1.633,-3.342],[1.62,-0.012],[2.673,-0.017],[0,0],[0,2.21],[0,0],[-2.97,0],[1.599,-3.576]],"v":[[-0.299,-14.603],[6.479,-23.844],[7.004,-23.853],[10.945,-20.35],[10.956,20.666],[6.956,24.666],[-13.954,24.66],[-17.072,19.589]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":14,"s":[{"i":[[-4.927,9.356],[-1.952,3.299],[-1.433,0.138],[0,-2.21],[0,0],[2.21,0],[0,0],[-0.878,1.799]],"o":[[1.739,-3.3],[0.659,-1.113],[2.715,-0.313],[0,0],[0,2.21],[0,0],[-2.983,0],[1.767,-3.534]],"v":[[-1.291,-11.822],[4.056,-21.407],[6.933,-23.739],[10.94,-20.326],[10.958,20.355],[6.958,24.355],[-14.444,24.346],[-17.512,19.281]],"c":true}]},{"t":16,"s":[{"i":[[-5.281,9.116],[-1.964,3.326],[-1.188,0.301],[0,-2.21],[0,0],[4.002,-0.075],[0,0],[-0.997,1.742]],"o":[[1.879,-3.243],[1.139,-1.928],[2.77,-0.703],[0,0],[0,2.21],[0,0],[-3,0],[1.99,-3.478]],"v":[[-3,-8.127],[2.924,-18.197],[6.875,-23.572],[10.958,-20.26],[10.979,20.014],[6.979,24.014],[-15.625,24.005],[-18.625,18.946]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":27,"st":15,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Path 15","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.365],"y":[0]},"t":20,"s":[-95]},{"t":26,"s":[-90]}],"ix":10},"p":{"a":0,"k":[160.5,160.125,0],"ix":2},"a":{"a":0,"k":[8,0.167,0],"ix":1},"s":{"a":0,"k":[-300,-300,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[{"i":[[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21]],"o":[[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0]],"v":[[-11.5,-22],[-7.5,-26],[0.5,-26],[4.5,-22],[4.5,22],[0.5,26],[-7.5,26],[-11.5,22]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[0,0],[-2.629,0.034],[-1.514,0.021],[0,-2.21],[0,0],[2.21,0],[0,0],[-0.261,2.088]],"o":[[0.893,-2.862],[0.448,0.002],[1.89,-0.018],[0,0],[0,2.21],[0,0],[-2.492,0],[0.536,-1.372]],"v":[[-4.347,-21.598],[0.914,-25.133],[5.329,-25.126],[7.61,-21.384],[7.633,21.574],[5.299,25.592],[-10.342,25.534],[-14.03,21.126]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[-2.796,0.048],[-2.12,0.03],[0,-2.21],[0,0],[2.21,0],[0,0],[-0.365,2.04]],"o":[[1.25,-3.123],[0.628,0.002],[1.762,-0.025],[0,0],[0,2.21],[0,0],[-2.605,0],[0.75,-1.921]],"v":[[-1.485,-21.438],[4.28,-24.786],[7.728,-24.796],[9.321,-21.156],[9.352,21.384],[7.686,25.41],[-11.479,25.347],[-15.042,20.776]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[-4.472,9.732],[-3.258,-0.034],[-1.795,0.001],[0,-2.21],[0,0],[2.21,0],[0,0],[-0.688,1.889]],"o":[[1.52,-3.307],[1.81,0.019],[2.632,0.001],[0,0],[0,2.21],[0,0],[-2.955,0],[1.415,-3.623]],"v":[[0.18,-17.732],[5.758,-23.992],[7.066,-23.989],[10.962,-20.403],[10.958,20.696],[6.958,24.696],[-13.454,24.715],[-16.628,19.638]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13,"s":[{"i":[[-4.659,9.537],[-2.427,0.018],[-1.618,0.014],[0,-2.21],[0,0],[2.21,0],[0,0],[-0.787,1.842]],"o":[[1.633,-3.342],[1.62,-0.012],[2.673,-0.017],[0,0],[0,2.21],[0,0],[-2.97,0],[1.599,-3.576]],"v":[[-0.299,-14.603],[6.479,-23.844],[7.004,-23.853],[10.945,-20.35],[10.956,20.666],[6.956,24.666],[-13.954,24.66],[-17.072,19.589]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":14,"s":[{"i":[[-4.927,9.356],[-1.952,3.299],[-1.433,0.138],[0,-2.21],[0,0],[2.21,0],[0,0],[-0.878,1.799]],"o":[[1.739,-3.3],[0.659,-1.113],[2.715,-0.313],[0,0],[0,2.21],[0,0],[-2.983,0],[1.767,-3.534]],"v":[[-1.291,-11.822],[4.056,-21.407],[6.933,-23.739],[10.94,-20.326],[10.958,20.355],[6.958,24.355],[-14.444,24.346],[-17.512,19.281]],"c":true}]},{"t":16,"s":[{"i":[[-5.281,9.116],[-1.964,3.326],[-1.188,0.301],[0,-2.21],[0,0],[4.002,-0.075],[0,0],[-0.997,1.742]],"o":[[1.879,-3.243],[1.139,-1.928],[2.77,-0.703],[0,0],[0,2.21],[0,0],[-3,0],[1.99,-3.478]],"v":[[-3,-8.127],[2.924,-18.197],[6.875,-23.572],[10.958,-20.26],[10.979,20.014],[6.979,24.014],[-15.625,24.005],[-18.625,18.946]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":27,"st":15,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/tgs/playlist_play_pause.tgs b/Telegram-Mac/tgs/playlist_play_pause.tgs new file mode 100644 index 0000000000..0f5e2c468f --- /dev/null +++ b/Telegram-Mac/tgs/playlist_play_pause.tgs @@ -0,0 +1 @@ +{"v":"5.7.1","fr":60,"ip":0,"op":27,"w":320,"h":320,"nm":"Play and Pause 2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Path 18","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":0,"s":[-90]},{"i":{"x":[0.683],"y":[1]},"o":{"x":[0.688],"y":[0]},"t":4,"s":[-95]},{"t":26,"s":[0]}],"ix":10},"p":{"a":0,"k":[160.5,160.125,0],"ix":2},"a":{"a":0,"k":[8,0.167,0],"ix":1},"s":{"a":0,"k":[-300,-300,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[-5.281,9.116],[-1.964,3.326],[-1.188,0.301],[0,-2.21],[0,0],[4.002,-0.075],[0,0],[-0.997,1.742]],"o":[[1.879,-3.243],[1.139,-1.928],[2.77,-0.703],[0,0],[0,2.21],[0,0],[-3,0],[1.99,-3.478]],"v":[[-3,-8.127],[2.924,-18.197],[6.875,-23.572],[10.958,-20.26],[10.979,20.014],[6.979,24.014],[-15.625,24.005],[-18.625,18.946]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[-4.927,9.356],[-1.952,3.299],[-1.433,0.138],[0,-2.21],[0,0],[2.21,0],[0,0],[-0.878,1.799]],"o":[[1.739,-3.3],[0.659,-1.113],[2.715,-0.313],[0,0],[0,2.21],[0,0],[-2.983,0],[1.767,-3.534]],"v":[[-1.291,-11.822],[4.056,-21.407],[6.933,-23.739],[10.94,-20.326],[10.958,20.355],[6.958,24.355],[-14.444,24.346],[-17.512,19.281]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13,"s":[{"i":[[-4.659,9.537],[-2.427,0.018],[-1.618,0.014],[0,-2.21],[0,0],[2.21,0],[0,0],[-0.787,1.842]],"o":[[1.633,-3.342],[1.62,-0.012],[2.673,-0.017],[0,0],[0,2.21],[0,0],[-2.97,0],[1.599,-3.576]],"v":[[-0.299,-14.603],[6.479,-23.844],[7.004,-23.853],[10.945,-20.35],[10.956,20.666],[6.956,24.666],[-13.954,24.66],[-17.072,19.589]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":14,"s":[{"i":[[-4.472,9.732],[-3.258,-0.034],[-1.795,0.001],[0,-2.21],[0,0],[2.21,0],[0,0],[-0.688,1.889]],"o":[[1.52,-3.307],[1.81,0.019],[2.632,0.001],[0,0],[0,2.21],[0,0],[-2.955,0],[1.415,-3.623]],"v":[[0.18,-17.732],[5.758,-23.992],[7.066,-23.989],[10.962,-20.403],[10.958,20.696],[6.958,24.696],[-13.454,24.715],[-16.628,19.638]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[0,0],[-2.796,0.048],[-2.12,0.03],[0,-2.21],[0,0],[2.21,0],[0,0],[-0.365,2.04]],"o":[[1.25,-3.123],[0.628,0.002],[1.762,-0.025],[0,0],[0,2.21],[0,0],[-2.605,0],[0.75,-1.921]],"v":[[-1.485,-21.438],[4.28,-24.786],[7.728,-24.796],[9.321,-21.156],[9.352,21.384],[7.686,25.41],[-11.479,25.347],[-15.042,20.776]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[0,0],[-2.629,0.034],[-1.514,0.021],[0,-2.21],[0,0],[2.21,0],[0,0],[-0.261,2.088]],"o":[[0.893,-2.862],[0.448,0.002],[1.89,-0.018],[0,0],[0,2.21],[0,0],[-2.492,0],[0.536,-1.372]],"v":[[-4.347,-21.598],[0.914,-25.133],[5.329,-25.126],[7.61,-21.384],[7.633,21.574],[5.299,25.592],[-10.342,25.534],[-14.03,21.126]],"c":true}]},{"t":23,"s":[{"i":[[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21]],"o":[[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0]],"v":[[-11.5,-22],[-7.5,-26],[0.5,-26],[4.5,-22],[4.5,22],[0.5,26],[-7.5,26],[-11.5,22]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":27,"st":30,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Path 17","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":0,"s":[-90]},{"i":{"x":[0.683],"y":[1]},"o":{"x":[0.688],"y":[0]},"t":4,"s":[-95]},{"t":26,"s":[0]}],"ix":10},"p":{"a":0,"k":[160.5,160.125,0],"ix":2},"a":{"a":0,"k":[8,0.167,0],"ix":1},"s":{"a":0,"k":[300,-300,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[-5.281,9.116],[-1.964,3.326],[-1.188,0.301],[0,-2.21],[0,0],[4.002,-0.075],[0,0],[-0.997,1.742]],"o":[[1.879,-3.243],[1.139,-1.928],[2.77,-0.703],[0,0],[0,2.21],[0,0],[-3,0],[1.99,-3.478]],"v":[[-3,-8.127],[2.924,-18.197],[6.875,-23.572],[10.958,-20.26],[10.979,20.014],[6.979,24.014],[-15.625,24.005],[-18.625,18.946]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[-4.927,9.356],[-1.952,3.299],[-1.433,0.138],[0,-2.21],[0,0],[2.21,0],[0,0],[-0.878,1.799]],"o":[[1.739,-3.3],[0.659,-1.113],[2.715,-0.313],[0,0],[0,2.21],[0,0],[-2.983,0],[1.767,-3.534]],"v":[[-1.291,-11.822],[4.056,-21.407],[6.933,-23.739],[10.94,-20.326],[10.958,20.355],[6.958,24.355],[-14.444,24.346],[-17.512,19.281]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13,"s":[{"i":[[-4.659,9.537],[-2.427,0.018],[-1.618,0.014],[0,-2.21],[0,0],[2.21,0],[0,0],[-0.787,1.842]],"o":[[1.633,-3.342],[1.62,-0.012],[2.673,-0.017],[0,0],[0,2.21],[0,0],[-2.97,0],[1.599,-3.576]],"v":[[-0.299,-14.603],[6.479,-23.844],[7.004,-23.853],[10.945,-20.35],[10.956,20.666],[6.956,24.666],[-13.954,24.66],[-17.072,19.589]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":14,"s":[{"i":[[-4.472,9.732],[-3.258,-0.034],[-1.795,0.001],[0,-2.21],[0,0],[2.21,0],[0,0],[-0.688,1.889]],"o":[[1.52,-3.307],[1.81,0.019],[2.632,0.001],[0,0],[0,2.21],[0,0],[-2.955,0],[1.415,-3.623]],"v":[[0.18,-17.732],[5.758,-23.992],[7.066,-23.989],[10.962,-20.403],[10.958,20.696],[6.958,24.696],[-13.454,24.715],[-16.628,19.638]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[0,0],[-2.796,0.048],[-2.12,0.03],[0,-2.21],[0,0],[2.21,0],[0,0],[-0.365,2.04]],"o":[[1.25,-3.123],[0.628,0.002],[1.762,-0.025],[0,0],[0,2.21],[0,0],[-2.605,0],[0.75,-1.921]],"v":[[-1.485,-21.438],[4.28,-24.786],[7.728,-24.796],[9.321,-21.156],[9.352,21.384],[7.686,25.41],[-11.479,25.347],[-15.042,20.776]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[0,0],[-2.629,0.034],[-1.514,0.021],[0,-2.21],[0,0],[2.21,0],[0,0],[-0.261,2.088]],"o":[[0.893,-2.862],[0.448,0.002],[1.89,-0.018],[0,0],[0,2.21],[0,0],[-2.492,0],[0.536,-1.372]],"v":[[-4.347,-21.598],[0.914,-25.133],[5.329,-25.126],[7.61,-21.384],[7.633,21.574],[5.299,25.592],[-10.342,25.534],[-14.03,21.126]],"c":true}]},{"t":23,"s":[{"i":[[0,0],[-2.21,0],[0,0],[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21]],"o":[[0,-2.21],[0,0],[2.21,0],[0,0],[0,2.21],[0,0],[-2.21,0],[0,0]],"v":[[-11.5,-22],[-7.5,-26],[0.5,-26],[4.5,-22],[4.5,22],[0.5,26],[-7.5,26],[-11.5,22]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":27,"st":30,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/tgs/police.tgs b/Telegram-Mac/tgs/police.tgs new file mode 100644 index 0000000000..02d5a9de67 Binary files /dev/null and b/Telegram-Mac/tgs/police.tgs differ diff --git a/Telegram-Mac/tgs/sad_man.tgs b/Telegram-Mac/tgs/sad_man.tgs new file mode 100644 index 0000000000..9285160db5 Binary files /dev/null and b/Telegram-Mac/tgs/sad_man.tgs differ diff --git a/Telegram-Mac/tgs/screenoff.tgs b/Telegram-Mac/tgs/screenoff.tgs new file mode 100644 index 0000000000..4efa791491 --- /dev/null +++ b/Telegram-Mac/tgs/screenoff.tgs @@ -0,0 +1 @@ +{"v":"5.7.8","fr":60,"ip":0,"op":12,"w":100,"h":100,"nm":"ScreenShare Off","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":2,"ty":4,"nm":"Line","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":1,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.4,"y":0},"t":0,"s":[218.155,237,0],"to":[0,0,0],"ti":[0,0,0]},{"t":10,"s":[235.155,256,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-20.845,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-168.295,-147.55],[126.605,147.55]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Line 2","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.4,"y":0},"t":0,"s":[205.655,252,0],"to":[0,0,0],"ti":[0,0,0]},{"t":10,"s":[222.655,271,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-20.845,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-148.295,-127.55],[106.605,127.55]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape","tt":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[255.983,255.983,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[650,650,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-0.44,-0.17],[0,0],[-0.07,-0.03],[-0.34,-0.65],[-0.03,-0.07],[0,0],[-0.03,-0.92],[0,0],[0,0],[0,0],[0.18,-0.44],[0,0],[0.04,-0.07],[0.65,-0.34],[0.06,-0.03],[0,0],[0.92,-0.03],[0,0],[0,0],[0,0],[0.44,0.18],[0,0],[0.07,0.04],[0.35,0.65],[0.03,0.06],[0,0],[0.04,0.92],[0,0],[0,0],[0,0],[-0.17,0.44],[0,0],[-0.03,0.07],[-0.65,0.35],[-0.07,0.03],[0,0],[-0.92,0.04],[0,0]],"o":[[0,0],[0.92,0.04],[0,0],[0.06,0.03],[0.65,0.35],[0.04,0.07],[0,0],[0.18,0.44],[0,0],[0,0],[0,0],[-0.03,0.92],[0,0],[-0.03,0.06],[-0.34,0.65],[-0.07,0.04],[0,0],[-0.44,0.18],[0,0],[0,0],[0,0],[-0.92,-0.03],[0,0],[-0.07,-0.03],[-0.65,-0.34],[-0.03,-0.07],[0,0],[-0.17,-0.44],[0,0],[0,0],[0,0],[0.04,-0.92],[0,0],[0.03,-0.07],[0.35,-0.65],[0.07,-0.03],[0,0],[0.44,-0.17],[0,0],[0,0]],"v":[[9.142,-4.998],[9.632,-4.988],[11.532,-4.668],[11.752,-4.568],[11.952,-4.468],[13.462,-2.958],[13.572,-2.748],[13.662,-2.528],[13.982,-0.638],[14.002,-0.148],[14.002,6.142],[13.982,6.632],[13.662,8.532],[13.572,8.752],[13.462,8.952],[11.952,10.462],[11.752,10.572],[11.532,10.662],[9.632,10.982],[9.142,11.002],[-3.148,11.002],[-3.638,10.982],[-5.528,10.662],[-5.748,10.572],[-5.958,10.462],[-7.468,8.952],[-7.568,8.752],[-7.668,8.532],[-7.988,6.632],[-7.998,6.142],[-7.998,-0.148],[-7.988,-0.638],[-7.668,-2.528],[-7.568,-2.748],[-7.468,-2.958],[-5.958,-4.468],[-5.748,-4.568],[-5.528,-4.668],[-3.638,-4.988],[-3.148,-4.998]],"c":true},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0.08,0],[0.03,-0.13],[0,0],[0,0],[-0.05,-3.88],[-0.04,-0.02],[-0.04,0.03],[0,0],[0,0],[-2.15,0.06],[0,0],[0,0],[-0.05,-0.06],[-0.12,0.06],[0,0],[0,0],[0.07,0.11],[0,0],[0,0]],"o":[[-0.14,0],[0,0],[0,0],[-3.49,0],[0,0.05],[0.05,0.02],[0,0],[0,0],[0.86,-1.34],[0,0],[0,0],[0,0.08],[0.1,0.1],[0,0],[0,0],[0.09,-0.1],[0,0],[0,0],[-0.06,-0.05]],"v":[[3.96,-1.108],[3.67,-0.878],[3.66,-0.808],[3.66,1.202],[-1.5,7.022],[-1.44,7.132],[-1.3,7.122],[-1.27,7.082],[-1.16,6.902],[3.36,4.802],[3.66,4.802],[3.66,6.802],[3.74,7.012],[4.11,7.072],[4.17,7.032],[8.28,3.202],[8.31,2.832],[8.26,2.782],[4.17,-1.028]],"c":true},"ix":2},"nm":"Контур 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[-0.44,-0.17],[0,0],[-0.07,-0.03],[-0.34,-0.65],[-0.06,-0.98],[0,0],[1.07,-0.57],[0.53,-1],[0,-1.93],[0,0],[0.5,0.27],[0.35,0.65],[0.03,0.06],[0,0],[0.04,0.92],[0,0],[0,0],[0,0],[0,0],[0,0],[-0.17,0.44],[0,0],[-0.03,0.07],[-0.65,0.35],[-0.07,0.03],[0,0],[-0.92,0.04],[0,0]],"o":[[0,0],[0,0],[0,0],[0.92,0.04],[0,0],[0.06,0.03],[0.65,0.35],[0.27,0.5],[0,0],[-1.93,0],[-1,0.53],[-0.57,1.07],[0,0],[-0.98,-0.06],[-0.65,-0.34],[-0.03,-0.07],[0,0],[-0.17,-0.44],[0,0],[0,0],[0,0],[0,0],[0,0],[0.04,-0.92],[0,0],[0.03,-0.07],[0.35,-0.65],[0.07,-0.03],[0,0],[0.44,-0.17],[0,0],[0,0]],"v":[[-9.148,-10.998],[3.142,-10.998],[3.142,-10.998],[3.632,-10.988],[5.532,-10.668],[5.752,-10.568],[5.952,-10.468],[7.462,-8.958],[7.962,-6.998],[-2.868,-6.998],[-6.898,-6.228],[-9.228,-3.898],[-9.998,0.132],[-9.998,4.962],[-11.958,4.462],[-13.468,2.952],[-13.568,2.752],[-13.668,2.532],[-13.988,0.632],[-13.998,0.142],[-13.998,0.142],[-13.998,-6.148],[-13.998,-6.148],[-13.988,-6.638],[-13.668,-8.528],[-13.568,-8.748],[-13.468,-8.958],[-11.958,-10.468],[-11.748,-10.568],[-11.528,-10.668],[-9.638,-10.988],[-9.148,-10.998]],"c":true},"ix":2},"nm":"Контур 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Объединить контуры 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Заливка 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[178.571,178.571],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Shape","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Pre-comp 1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[256,256,0],"ix":1,"l":2},"s":{"a":0,"k":[13.6,13.6,100],"ix":6,"l":2}},"ao":0,"w":512,"h":512,"ip":0,"op":180,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/tgs/screenon.tgs b/Telegram-Mac/tgs/screenon.tgs new file mode 100644 index 0000000000..e494117d7f --- /dev/null +++ b/Telegram-Mac/tgs/screenon.tgs @@ -0,0 +1 @@ +{"v":"5.7.8","fr":60,"ip":0,"op":12,"w":100,"h":100,"nm":"ScreenShare On","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Line 2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":8,"s":[100]},{"t":9,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.4,"y":0},"t":0,"s":[235.155,256,0],"to":[0,0,0],"ti":[0,0,0]},{"t":21,"s":[231.155,251,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-20.845,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-168.295,-147.55],[126.605,147.55]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":3,"nm":"Обрезать контуры 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Line 3","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.4,"y":0},"t":0,"s":[205.655,252,0],"to":[0,0,0],"ti":[0,0,0]},{"t":10,"s":[222.655,271,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-20.845,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-148.295,-127.55],[106.605,127.55]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Обрезать контуры 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape","tt":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[255.983,255.983,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[650,650,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-0.44,-0.17],[0,0],[-0.07,-0.03],[-0.34,-0.65],[-0.03,-0.07],[0,0],[-0.03,-0.92],[0,0],[0,0],[0,0],[0.18,-0.44],[0,0],[0.04,-0.07],[0.65,-0.34],[0.06,-0.03],[0,0],[0.92,-0.03],[0,0],[0,0],[0,0],[0.44,0.18],[0,0],[0.07,0.04],[0.35,0.65],[0.03,0.06],[0,0],[0.04,0.92],[0,0],[0,0],[0,0],[-0.17,0.44],[0,0],[-0.03,0.07],[-0.65,0.35],[-0.07,0.03],[0,0],[-0.92,0.04],[0,0]],"o":[[0,0],[0.92,0.04],[0,0],[0.06,0.03],[0.65,0.35],[0.04,0.07],[0,0],[0.18,0.44],[0,0],[0,0],[0,0],[-0.03,0.92],[0,0],[-0.03,0.06],[-0.34,0.65],[-0.07,0.04],[0,0],[-0.44,0.18],[0,0],[0,0],[0,0],[-0.92,-0.03],[0,0],[-0.07,-0.03],[-0.65,-0.34],[-0.03,-0.07],[0,0],[-0.17,-0.44],[0,0],[0,0],[0,0],[0.04,-0.92],[0,0],[0.03,-0.07],[0.35,-0.65],[0.07,-0.03],[0,0],[0.44,-0.17],[0,0],[0,0]],"v":[[9.142,-4.998],[9.632,-4.988],[11.532,-4.668],[11.752,-4.568],[11.952,-4.468],[13.462,-2.958],[13.572,-2.748],[13.662,-2.528],[13.982,-0.638],[14.002,-0.148],[14.002,6.142],[13.982,6.632],[13.662,8.532],[13.572,8.752],[13.462,8.952],[11.952,10.462],[11.752,10.572],[11.532,10.662],[9.632,10.982],[9.142,11.002],[-3.148,11.002],[-3.638,10.982],[-5.528,10.662],[-5.748,10.572],[-5.958,10.462],[-7.468,8.952],[-7.568,8.752],[-7.668,8.532],[-7.988,6.632],[-7.998,6.142],[-7.998,-0.148],[-7.988,-0.638],[-7.668,-2.528],[-7.568,-2.748],[-7.468,-2.958],[-5.958,-4.468],[-5.748,-4.568],[-5.528,-4.668],[-3.638,-4.988],[-3.148,-4.998]],"c":true},"ix":2},"nm":"Контур 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0.08,0],[0.03,-0.13],[0,0],[0,0],[-0.05,-3.88],[-0.04,-0.02],[-0.04,0.03],[0,0],[0,0],[-2.15,0.06],[0,0],[0,0],[-0.05,-0.06],[-0.12,0.06],[0,0],[0,0],[0.07,0.11],[0,0],[0,0]],"o":[[-0.14,0],[0,0],[0,0],[-3.49,0],[0,0.05],[0.05,0.02],[0,0],[0,0],[0.86,-1.34],[0,0],[0,0],[0,0.08],[0.1,0.1],[0,0],[0,0],[0.09,-0.1],[0,0],[0,0],[-0.06,-0.05]],"v":[[3.96,-1.108],[3.67,-0.878],[3.66,-0.808],[3.66,1.202],[-1.5,7.022],[-1.44,7.132],[-1.3,7.122],[-1.27,7.082],[-1.16,6.902],[3.36,4.802],[3.66,4.802],[3.66,6.802],[3.74,7.012],[4.11,7.072],[4.17,7.032],[8.28,3.202],[8.31,2.832],[8.26,2.782],[4.17,-1.028]],"c":true},"ix":2},"nm":"Контур 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[-0.44,-0.17],[0,0],[-0.07,-0.03],[-0.34,-0.65],[-0.06,-0.98],[0,0],[1.07,-0.57],[0.53,-1],[0,-1.93],[0,0],[0.5,0.27],[0.35,0.65],[0.03,0.06],[0,0],[0.04,0.92],[0,0],[0,0],[0,0],[0,0],[0,0],[-0.17,0.44],[0,0],[-0.03,0.07],[-0.65,0.35],[-0.07,0.03],[0,0],[-0.92,0.04],[0,0]],"o":[[0,0],[0,0],[0,0],[0.92,0.04],[0,0],[0.06,0.03],[0.65,0.35],[0.27,0.5],[0,0],[-1.93,0],[-1,0.53],[-0.57,1.07],[0,0],[-0.98,-0.06],[-0.65,-0.34],[-0.03,-0.07],[0,0],[-0.17,-0.44],[0,0],[0,0],[0,0],[0,0],[0,0],[0.04,-0.92],[0,0],[0.03,-0.07],[0.35,-0.65],[0.07,-0.03],[0,0],[0.44,-0.17],[0,0],[0,0]],"v":[[-9.148,-10.998],[3.142,-10.998],[3.142,-10.998],[3.632,-10.988],[5.532,-10.668],[5.752,-10.568],[5.952,-10.468],[7.462,-8.958],[7.962,-6.998],[-2.868,-6.998],[-6.898,-6.228],[-9.228,-3.898],[-9.998,0.132],[-9.998,4.962],[-11.958,4.462],[-13.468,2.952],[-13.568,2.752],[-13.668,2.532],[-13.988,0.632],[-13.998,0.142],[-13.998,0.142],[-13.998,-6.148],[-13.998,-6.148],[-13.988,-6.638],[-13.668,-8.528],[-13.568,-8.748],[-13.468,-8.958],[-11.958,-10.468],[-11.748,-10.568],[-11.528,-10.668],[-9.638,-10.988],[-9.148,-10.998]],"c":true},"ix":2},"nm":"Контур 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Объединить контуры 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Заливка 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[178.571,178.571],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Shape","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Pre-comp 1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2,"l":2},"a":{"a":0,"k":[256,256,0],"ix":1,"l":2},"s":{"a":0,"k":[13.6,13.6,100],"ix":6,"l":2}},"ao":0,"w":512,"h":512,"ip":0,"op":180,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/tgs/smart_guy.tgs b/Telegram-Mac/tgs/smart_guy.tgs new file mode 100644 index 0000000000..9e1cf7701b Binary files /dev/null and b/Telegram-Mac/tgs/smart_guy.tgs differ diff --git a/Telegram-Mac/tgs/success_saved.tgs b/Telegram-Mac/tgs/success_saved.tgs new file mode 100644 index 0000000000..f66fe78be0 --- /dev/null +++ b/Telegram-Mac/tgs/success_saved.tgs @@ -0,0 +1 @@ +{"v":"5.5.2","fr":60,"ip":0,"op":60,"w":200,"h":140,"nm":"saved","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"luc12","parent":14,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[100]},{"t":20,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-43.5,75.344,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[2.5,0],[-2.5,0]],"c":false},"ix":2},"nm":"contour","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"stroke","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":-60,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"luc12","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[40]},{"t":20,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.697],"y":[0.848]},"o":{"x":[0.527],"y":[0.407]},"t":2,"s":[40]},{"t":5,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":61,"st":-15,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"luc11","parent":14,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[100]},{"t":20,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[43.5,75.344,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[2.5,0],[-2.5,0]],"c":false},"ix":2},"nm":"contour","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"stroke","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":60,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"luc11","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.697],"y":[0.848]},"o":{"x":[0.526],"y":[0.407]},"t":2,"s":[60]},{"t":5,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":2,"s":[60]},{"i":{"x":[0.201],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[60]},{"t":20,"s":[0]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":61,"st":-15,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"luc10","parent":14,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[100]},{"t":20,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,87,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[2.5,0],[-2.5,0]],"c":false},"ix":2},"nm":"contour","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"stroke","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":90,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"luc10","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.697],"y":[0.848]},"o":{"x":[0.526],"y":[0.407]},"t":2,"s":[60]},{"t":5,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":2,"s":[60]},{"i":{"x":[0.201],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[60]},{"t":20,"s":[0]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":61,"st":-15,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"luc9","parent":14,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[100]},{"t":20,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[75.344,43.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[2.5,0],[-2.5,0]],"c":false},"ix":2},"nm":"contour","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"stroke","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":30,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"luc9","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.697],"y":[0.848]},"o":{"x":[0.526],"y":[0.407]},"t":2,"s":[60]},{"t":5,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":2,"s":[60]},{"i":{"x":[0.201],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[60]},{"t":20,"s":[0]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":61,"st":-15,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"luc8","parent":14,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[100]},{"t":20,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[87,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[2.5,0],[-2.5,0]],"c":false},"ix":2},"nm":"contour","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"stroke","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"luc8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.697],"y":[0.848]},"o":{"x":[0.526],"y":[0.407]},"t":2,"s":[60]},{"t":5,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":2,"s":[60]},{"i":{"x":[0.201],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[60]},{"t":20,"s":[0]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":61,"st":-15,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"luc7","parent":14,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[100]},{"t":20,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[75.344,-43.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[2.5,0],[-2.5,0]],"c":false},"ix":2},"nm":"contour","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"stroke","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":-30,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"luc7","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.697],"y":[0.848]},"o":{"x":[0.526],"y":[0.407]},"t":2,"s":[60]},{"t":5,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":2,"s":[60]},{"i":{"x":[0.201],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[60]},{"t":20,"s":[0]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":61,"st":-15,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"luc6","parent":14,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[100]},{"t":20,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[43.5,-75.344,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[2.5,0],[-2.5,0]],"c":false},"ix":2},"nm":"contour","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"stroke","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":-60,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"luc6","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.697],"y":[0.848]},"o":{"x":[0.526],"y":[0.407]},"t":2,"s":[60]},{"t":5,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":2,"s":[60]},{"i":{"x":[0.201],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[60]},{"t":20,"s":[0]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":61,"st":-15,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"luc5","parent":14,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[100]},{"t":20,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,-87,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[2.5,0],[-2.5,0]],"c":false},"ix":2},"nm":"contour","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"stroke","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":90,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"luc5","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[40]},{"t":20,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.697],"y":[0.848]},"o":{"x":[0.527],"y":[0.407]},"t":2,"s":[40]},{"t":5,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":61,"st":-15,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"luc4","parent":14,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[100]},{"t":20,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-43.5,-75.344,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[2.5,0],[-2.5,0]],"c":false},"ix":2},"nm":"contour","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"stroke","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":60,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"luc4","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[40]},{"t":20,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.697],"y":[0.848]},"o":{"x":[0.527],"y":[0.407]},"t":2,"s":[40]},{"t":5,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":61,"st":-15,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"luc3","parent":14,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[100]},{"t":20,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-75.344,-43.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[2.5,0],[-2.5,0]],"c":false},"ix":2},"nm":"contour","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"stroke","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":30,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"luc3","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[40]},{"t":20,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.697],"y":[0.848]},"o":{"x":[0.527],"y":[0.407]},"t":2,"s":[40]},{"t":5,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":61,"st":-15,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"luc2","parent":14,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[100]},{"t":20,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-87,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[2.5,0],[-2.5,0]],"c":false},"ix":2},"nm":"contour","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"stroke","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"luc2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[40]},{"t":20,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.697],"y":[0.848]},"o":{"x":[0.527],"y":[0.407]},"t":2,"s":[40]},{"t":5,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":61,"st":-15,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"luc1","parent":14,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[100]},{"t":20,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-75.344,43.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[2.5,0],[-2.5,0]],"c":false},"ix":2},"nm":"contour","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"stroke","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":-30,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"luc1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[40]},{"t":20,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.697],"y":[0.848]},"o":{"x":[0.527],"y":[0.407]},"t":2,"s":[40]},{"t":5,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":61,"st":-15,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"info1","parent":14,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.5,3.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-28.5,0.5],[-9.5,19.5],[28.5,-19.5]],"c":false},"ix":2},"nm":"contour","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"stroke","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"info1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.799],"y":[0]},"t":5,"s":[0]},{"t":15,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"trim","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":61,"st":0,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"Oval 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[100,70,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.214,0.214,0.333],"y":[0,0,0]},"t":0,"s":[0,0,100]},{"i":{"x":[0.609,0.609,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[55,55,100]},{"t":30,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"contour","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"fill","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[600,600],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Преобразовать"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":61,"st":-15,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/tgs/swap_money.tgs b/Telegram-Mac/tgs/swap_money.tgs new file mode 100644 index 0000000000..74bbb0bf3d Binary files /dev/null and b/Telegram-Mac/tgs/swap_money.tgs differ diff --git a/Telegram-Mac/tgs/think_spectacular.tgs b/Telegram-Mac/tgs/think_spectacular.tgs new file mode 100644 index 0000000000..2dd3799596 Binary files /dev/null and b/Telegram-Mac/tgs/think_spectacular.tgs differ diff --git a/Telegram-Mac/tgs/voice_chat_cancel_reminder.tgs b/Telegram-Mac/tgs/voice_chat_cancel_reminder.tgs new file mode 100644 index 0000000000..266d2f7e82 --- /dev/null +++ b/Telegram-Mac/tgs/voice_chat_cancel_reminder.tgs @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":11,"w":100,"h":100,"nm":"Cancel Reminder","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Line Bell","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.4,"y":0},"t":0,"s":[-21.55,-23.95,0],"to":[0,0,0],"ti":[0,0,0]},{"t":10,"s":[-0.05,-3.45,0]}],"ix":2},"a":{"a":0,"k":[-0.05,-2.95,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-127.5,-130.5],[127.4,124.6]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Bottom Bell","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,137.7,0],"ix":2},"a":{"a":0,"k":[0,137.7,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[20.9,0],[7.5,18.2]],"o":[[-7.5,18.2],[-20.9,0],[0,0]],"v":[[46.3,122.2],[0,153.2],[-46.3,122.2]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Bell","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[255.417,230.945,0],"ix":2},"a":{"a":0,"k":[-0.033,-25.3,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2.354,"s":[{"i":[[0,0],[-3.074,7.353],[-1.513,2.779],[0,0],[0,0],[0,0],[0,8.3],[-3.1,2.9],[0,0],[-1.3,19.1]],"o":[[0,0],[2.875,-6.876],[5.853,-0.665],[0,0],[0,0],[-8.3,0],[0,-4.2],[0,0],[14.1,-13],[0,0]],"v":[[-92.8,-53.2],[-88.262,-72.223],[-82.528,-85.88],[90.086,101.66],[91.179,102.753],[-121.4,102.2],[-136.4,87.2],[-131.5,76.1],[-121.9,67.3],[-97.8,17.4]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[{"i":[[0,0],[-5.612,-5.659],[-2.815,6.442],[0,0],[0,0],[0,0],[0,8.3],[-3.1,2.9],[0,0],[-1.3,19.1]],"o":[[0,0],[6.23,6.281],[5.853,-0.665],[0,0],[0,0],[-8.3,0],[0,-4.2],[0,0],[14.1,-13],[0,0]],"v":[[-92.732,-55.388],[-88.024,-52.395],[-59.755,-57.527],[90.086,101.66],[91.179,102.753],[-121.4,102.2],[-136.4,87.2],[-131.5,76.1],[-121.9,67.3],[-97.8,17.4]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3.529,"s":[{"i":[[0,0],[-5.724,-5.546],[-2.815,6.442],[0,0],[0,0],[0,0],[0,8.3],[-3.1,2.9],[0,0],[-1.3,19.1]],"o":[[0,0],[16.687,16.166],[5.853,-0.665],[0,0],[0,0],[-8.3,0],[0,-4.2],[0,0],[14.1,-13],[0,0]],"v":[[-92.8,-53.2],[-77.713,-40.577],[-42.468,-41.114],[90.086,101.66],[91.179,102.753],[-121.4,102.2],[-136.4,87.2],[-131.5,76.1],[-121.9,67.3],[-97.8,17.4]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[0,0],[-5.724,-5.546],[-2.815,6.442],[0,0],[0,0],[0,0],[0,8.3],[-3.1,2.9],[0,0],[-1.3,19.1]],"o":[[0,0],[16.687,16.166],[5.853,-0.665],[0,0],[0,0],[-8.3,0],[0,-4.2],[0,0],[14.1,-13],[0,0]],"v":[[-92.8,-53.2],[-48.105,-12.045],[-12.86,-12.581],[90.086,101.66],[91.179,102.753],[-121.4,102.2],[-136.4,87.2],[-131.5,76.1],[-121.9,67.3],[-97.8,17.4]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4.705,"s":[{"i":[[0,0],[-5.724,-5.546],[-2.815,6.442],[0,0],[0,0],[0,0],[0,8.3],[-3.1,2.9],[0,0],[-1.3,19.1]],"o":[[0,0],[16.687,16.166],[5.853,-0.665],[0,0],[0,0],[-8.3,0],[0,-4.2],[0,0],[14.1,-13],[0,0]],"v":[[-92.8,-53.2],[4.557,48.045],[42.302,45.009],[90.086,101.66],[91.179,102.753],[-121.4,102.2],[-136.4,87.2],[-131.5,76.1],[-121.9,67.3],[-97.8,17.4]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5.295,"s":[{"i":[[0,0],[-3.664,-3.848],[-1.155,9.616],[0,0],[0,0],[0,0],[0,8.3],[-3.1,2.9],[0,0],[-1.3,19.1]],"o":[[0,0],[19.377,20.345],[4.823,2.104],[0,0],[0,0],[-8.3,0],[0,-4.2],[0,0],[14.1,-13],[0,0]],"v":[[-92.8,-53.2],[28.353,68.46],[71.999,65.402],[100.954,101.764],[102.046,102.857],[-121.4,102.2],[-136.4,87.2],[-131.5,76.1],[-121.9,67.3],[-97.8,17.4]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[0,0],[-1.863,-1.894],[0,0],[0,0],[0,0],[0,0],[0,8.3],[-3.1,2.9],[0,0],[-1.3,19.1]],"o":[[0,0],[18.936,19.254],[0,0],[0,0],[0,0],[-8.3,0],[0,-4.2],[0,0],[14.1,-13],[0,0]],"v":[[-92.8,-53.2],[56.147,92.507],[97.94,81.857],[97.341,101.73],[98.434,102.823],[-121.4,102.2],[-136.4,87.2],[-131.5,76.1],[-121.9,67.3],[-97.8,17.4]],"c":true}]},{"t":7,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,8.3],[-3.1,2.9],[0,0],[-1.3,19.1]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[-8.3,0],[0,-4.2],[0,0],[14.1,-13],[0,0]],"v":[[-92.8,-53.2],[21.339,60.939],[26.803,66.403],[61.507,101.107],[62.6,102.2],[-121.4,102.2],[-136.4,87.2],[-131.5,76.1],[-121.9,67.3],[-97.8,17.4]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2.354,"s":[{"i":[[-22.1,0],[-1.3,-20.9],[0,0],[0,0],[-1.7,-22.9],[0,0],[-14.1,-12.9],[0,0],[6.37,-5.768],[0,0],[3.835,4.419],[0,0],[0,0],[-8,3.2],[0,0]],"o":[[21.2,0],[0,0],[0,0],[21.3,8.5],[0,0],[1.4,19.1],[0,0],[8.373,8.582],[-4.704,4.26],[0,0],[-6.636,-5.045],[0,0],[8.044,-10.337],[0,0],[0,-21.9]],"v":[[0,-152.8],[39.9,-115.3],[40,-112.8],[55.1,-106.8],[92.7,-55.4],[97.9,17.3],[122,67.2],[131.6,76],[130.377,100.18],[90.362,102.509],[-42.618,-41.019],[-57.795,-54.822],[-82.749,-85.808],[-55.1,-107],[-40,-113]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[{"i":[[-22.1,0],[-1.3,-20.9],[0,0],[0,0],[-1.7,-22.9],[0,0],[-14.1,-12.9],[0,0],[6.37,-5.768],[0,0],[3.835,4.419],[0,0],[0,0],[-8,3.2],[0,0]],"o":[[21.2,0],[0,0],[0,0],[21.3,8.5],[0,0],[1.4,19.1],[0,0],[8.373,8.582],[-4.704,4.26],[0,0],[4.005,-3.825],[0,0],[5.8,-6],[0,0],[0,-21.9]],"v":[[0,-152.8],[39.9,-115.3],[40,-112.8],[55.1,-106.8],[92.7,-55.4],[97.9,17.3],[122,67.2],[131.6,76],[130.377,100.18],[90.362,102.509],[-59.905,-57.433],[-57.566,-69.418],[-75.9,-93],[-55.1,-107],[-40,-113]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3.529,"s":[{"i":[[-22.1,0],[-1.3,-20.9],[0,0],[0,0],[-1.7,-22.9],[0,0],[-14.1,-12.9],[0,0],[6.37,-5.768],[0,0],[3.835,4.419],[0,0],[0,0],[-8,3.2],[0,0]],"o":[[21.2,0],[0,0],[0,0],[21.3,8.5],[0,0],[1.4,19.1],[0,0],[8.373,8.582],[-4.704,4.26],[0,0],[4.005,-3.825],[0,0],[5.8,-6],[0,0],[0,-21.9]],"v":[[0,-152.8],[39.9,-115.3],[40,-112.8],[55.1,-106.8],[92.7,-55.4],[97.9,17.3],[122,67.2],[131.6,76],[130.377,100.18],[90.362,102.509],[-42.618,-41.019],[-42.681,-54.994],[-75.9,-93],[-55.1,-107],[-40,-113]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-22.1,0],[-1.3,-20.9],[0,0],[0,0],[-1.7,-22.9],[0,0],[-14.1,-12.9],[0,0],[6.37,-5.768],[0,0],[3.835,4.419],[0,0],[0,0],[-8,3.2],[0,0]],"o":[[21.2,0],[0,0],[0,0],[21.3,8.5],[0,0],[1.4,19.1],[0,0],[8.373,8.582],[-4.704,4.26],[0,0],[4.005,-3.825],[0,0],[5.8,-6],[0,0],[0,-21.9]],"v":[[0,-152.8],[39.9,-115.3],[40,-112.8],[55.1,-106.8],[92.7,-55.4],[97.9,17.3],[122,67.2],[131.6,76],[130.377,100.18],[90.362,102.509],[-13.01,-12.487],[-13.074,-26.462],[-75.9,-93],[-55.1,-107],[-40,-113]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4.705,"s":[{"i":[[-22.1,0],[-1.3,-20.9],[0,0],[0,0],[-1.7,-22.9],[0,0],[-14.1,-12.9],[0,0],[6.37,-5.768],[0,0],[3.835,4.419],[0,0],[0,0],[-8,3.2],[0,0]],"o":[[21.2,0],[0,0],[0,0],[21.3,8.5],[0,0],[1.4,19.1],[0,0],[8.373,8.582],[-4.704,4.26],[0,0],[4.005,-3.825],[0,0],[5.8,-6],[0,0],[0,-21.9]],"v":[[0,-152.8],[39.9,-115.3],[40,-112.8],[55.1,-106.8],[92.7,-55.4],[97.9,17.3],[122,67.2],[131.6,76],[130.377,100.18],[90.362,102.509],[42.152,45.104],[39.553,32.45],[-75.9,-93],[-55.1,-107],[-40,-113]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5.295,"s":[{"i":[[-22.1,0],[-1.3,-20.9],[0,0],[0,0],[-1.7,-22.9],[0,0],[-14.1,-12.9],[0,0],[6.013,-5.979],[0,0],[2.557,2.946],[0,0],[0,0],[-8,3.2],[0,0]],"o":[[21.2,0],[0,0],[0,0],[21.3,8.5],[0,0],[1.4,19.1],[0,0],[7.615,7.588],[-4.636,4.64],[0,0],[2.67,-2.55],[0,0],[5.8,-6],[0,0],[0,-21.9]],"v":[[0,-152.8],[39.9,-115.3],[40,-112.8],[55.1,-106.8],[92.7,-55.4],[97.9,17.3],[122,67.2],[131.6,76],[131.085,99.187],[99.941,102.339],[71.948,65.464],[69.826,56.639],[-75.9,-93],[-55.1,-107],[-40,-113]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[-22.1,0],[-1.3,-20.9],[0,0],[0,0],[-1.7,-22.9],[0,0],[-14.1,-12.9],[0,0],[5.657,-6.189],[0,0],[1.278,1.473],[0,0],[0,0],[-8,3.2],[0,0]],"o":[[21.2,0],[0,0],[0,0],[21.3,8.5],[0,0],[1.4,19.1],[0,0],[6.858,6.594],[-4.568,5.02],[0,0],[1.335,-1.275],[0,0],[5.8,-6],[0,0],[0,-21.9]],"v":[[0,-152.8],[39.9,-115.3],[40,-112.8],[55.1,-106.8],[92.7,-55.4],[97.9,17.3],[122,67.2],[131.6,76],[131.792,98.193],[98.096,102.878],[95.875,82.354],[94.229,77.357],[-75.9,-93],[-55.1,-107],[-40,-113]],"c":true}]},{"t":7,"s":[{"i":[[-22.1,0],[-1.3,-20.9],[0,0],[0,0],[-1.7,-22.9],[0,0],[-14.1,-12.9],[0,0],[5.3,-6.4],[0,0],[0,0],[0,0],[0,0],[-8,3.2],[0,0]],"o":[[21.2,0],[0,0],[0,0],[21.3,8.5],[0,0],[1.4,19.1],[0,0],[6.1,5.6],[-4.5,5.4],[0,0],[0,0],[0,0],[5.8,-6],[0,0],[0,-21.9]],"v":[[0,-152.8],[39.9,-115.3],[40,-112.8],[55.1,-106.8],[92.7,-55.4],[97.9,17.3],[122,67.2],[131.6,76],[132.5,97.2],[119.1,102],[74.435,57.335],[73.265,56.165],[-75.9,-93],[-55.1,-107],[-40,-113]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":3,"op":180,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Bell EXAMPLE","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.467,0.205,0],"ix":2},"a":{"a":0,"k":[0.001,0.2,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-22.091,0],[-1.305,-20.913],[0,0],[0,0],[-1.637,-22.924],[0,0],[-14.13,-12.952],[0,0],[5.598,-6.107],[4.205,0],[0,0],[0,8.284],[-3.099,2.841],[0,0],[-1.366,19.12],[0,0],[-21.339,8.535],[0,0]],"o":[[21.242,0],[0,0],[0,0],[21.339,8.535],[0,0],[1.366,19.12],[0,0],[6.107,5.598],[-2.841,3.099],[0,0],[-8.284,0],[0,-4.205],[0,0],[14.13,-12.952],[0,0],[1.637,-22.924],[0,0],[0,-22.091]],"v":[[0.001,-152.8],[39.922,-115.33],[40.001,-112.8],[55.114,-106.755],[92.678,-55.321],[97.874,17.427],[122.004,67.37],[131.575,76.143],[132.496,97.336],[121.439,102.2],[-121.437,102.2],[-136.437,87.2],[-131.573,76.143],[-122.003,67.37],[-97.873,17.427],[-92.676,-55.321],[-55.112,-106.755],[-39.999,-112.8]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[20.903,0],[7.469,18.212]],"o":[[-7.469,18.212],[-20.903,0],[0,0]],"v":[[46.28,122.161],[0.001,153.2],[-46.279,122.161]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Pre-comp 1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[13.6,13.6,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":180,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/tgs/voice_chat_cancel_reminder_to_mute.tgs b/Telegram-Mac/tgs/voice_chat_cancel_reminder_to_mute.tgs new file mode 100644 index 0000000000..0af40d7530 --- /dev/null +++ b/Telegram-Mac/tgs/voice_chat_cancel_reminder_to_mute.tgs @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":60,"ip":1,"op":37,"w":100,"h":100,"nm":"Cancel Reminder to Mute","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Line Bell","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-0.052,-2.917,0],"ix":2},"a":{"a":0,"k":[-0.052,-2.917,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.28,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-127.5,-131.454],[127.396,123.62]],"c":false}]},{"t":15,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-127.5,-133.454],[127.396,121.62]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.28],"y":[0]},"t":0,"s":[0]},{"t":15,"s":[12]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.28],"y":[0]},"t":0,"s":[100]},{"t":15,"s":[88]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":15,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Body Bell","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.3,"y":0},"t":0,"s":[256.953,252.701,0],"to":[0,0,0],"ti":[0,0,0]},{"t":14,"s":[256.953,237.701,0]}],"ix":2},"a":{"a":0,"k":[0.953,-3.299,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0],[0,8.284],[-3.099,2.841],[0,0],[-1.366,19.12]],"o":[[0,0],[0,0],[-8.284,0],[0,-4.205],[0,0],[14.13,-12.952],[0,0]],"v":[[-92.82,-53.2],[62.565,102.2],[-121.425,102.2],[-136.425,87.2],[-131.561,76.143],[-121.99,67.37],[-97.86,17.427]],"c":true}]},{"t":14,"s":[{"i":[[0,0],[0,0],[11.59,2.088],[5.55,3.175],[1.561,1.982],[0,0],[0.735,10.073]],"o":[[0,0],[0,0],[-10.825,-1.95],[-3.65,-2.088],[0,0],[-2.51,-4.37],[0,0]],"v":[[-59.82,-21.075],[15.065,54.2],[-11.55,54.825],[-32.925,46.45],[-42.561,38.143],[-50.99,28.62],[-59.735,3.052]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[{"i":[[-22.091,0],[-1.305,-20.913],[0,0],[0,0],[-1.637,-22.924],[0,0],[-14.13,-12.952],[0,0],[5.292,-6.373],[0,0],[0,0],[-3.735,2.445],[-3.742,1.497],[0,0]],"o":[[21.242,0],[0,0],[0,0],[21.339,8.535],[0,0],[1.366,19.12],[0,0],[6.107,5.598],[-4.508,5.429],[0,0],[3.062,-3.169],[3.292,-2.155],[0,0],[0,-22.091]],"v":[[0.013,-152.8],[39.934,-115.33],[40.013,-112.8],[55.126,-106.755],[92.69,-55.321],[97.886,17.427],[122.016,67.37],[131.587,76.143],[132.508,97.336],[119.1,102.185],[-75.89,-92.799],[-65.669,-101.254],[-55.1,-106.755],[-39.987,-112.8]],"c":true}]},{"t":14,"s":[{"i":[[-22.057,1.231],[-4.809,-10.795],[-0.9,-1.655],[0,0],[0,0],[0,0],[0.266,-1.068],[0.706,-1.979],[1.304,-2.461],[0,0],[0,0],[-0.127,7.665],[-0.327,1.422],[0,0]],"o":[[23.737,-1.325],[0,0],[2.612,4.8],[3.374,12.255],[0,0],[0,0],[-0.266,1.068],[-0.774,2.17],[-1.755,3.311],[0,0],[0,0],[0.108,-6.482],[2.1,-9.12],[11.987,-15.95]],"v":[[0.013,-154.175],[50.059,-127.705],[52.138,-124.425],[59.126,-108.005],[60.19,-54.821],[60.136,3.177],[59.079,7.995],[56.774,15.518],[53.758,23.086],[48.85,29.685],[-59.89,-78.549],[-59.608,-94.643],[-58.725,-107.88],[-49.237,-128.55]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":14,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Bottom Bell","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256.013,393.681,0],"ix":2},"a":{"a":0,"k":[0.013,137.681,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.4,"y":0},"t":1,"s":[{"i":[[0,0],[9.089,-5.522],[9.491,0],[7.496,4.484],[4.121,10.048]],"o":[[-4.078,9.943],[-7.559,4.592],[-9.37,0],[-9.226,-5.52],[0,0]],"v":[[46.292,122.161],[25.937,145.963],[0.013,153.2],[-25.62,146.138],[-46.267,122.161]],"c":true}]},{"t":10,"s":[{"i":[[0,0],[0.194,-5.092],[6.738,-0.243],[0.19,7.608],[0.04,8.897]],"o":[[0.142,9.195],[-0.316,8.283],[-6.971,0.252],[-0.135,-5.403],[0,0]],"v":[[10,78.286],[10.066,143.342],[-0.112,155.2],[-10.065,143.642],[-9.974,78.286]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":10,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Piece Micro","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[35.717,87.391,0],"ix":2},"a":{"a":0,"k":[35.717,87.391,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":7,"s":[{"i":[[0,0],[-1.044,0.234],[0,0],[3.425,-0.422]],"o":[[1.079,-0.298],[0,0],[-3.888,2.204],[0,0]],"v":[[13.921,81.923],[20.263,80.547],[34.826,96.484],[18.575,101.297]],"c":true}]},{"t":9,"s":[{"i":[[0,0],[-5.475,2.553],[0,0],[9.717,-2.249]],"o":[[6.02,-1.382],[0,0],[-8.509,4.686],[0,0]],"v":[[19.671,80.673],[36.95,74.735],[51.763,89.546],[24.325,100.047]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":7,"op":180,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Arc Micro R","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1,92,0],"ix":2},"a":{"a":0,"k":[1,92,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.29,"y":0},"t":18,"s":[{"i":[[0.267,-5.148],[14.135,-14.996]],"o":[[-1.559,30.052],[-17.225,18.274]],"v":[[97.5,-5.269],[71.365,63.505]],"c":false}]},{"t":28,"s":[{"i":[[0.267,-5.148],[14.849,-16.38]],"o":[[-1.482,28.566],[-17.722,19.55]],"v":[[97.5,-5.269],[71.726,62.502]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":8,"s":[99]},{"t":18,"s":[0]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":10,"op":180,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Arc Micro L","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.31,"y":0},"t":14,"s":[257,334,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.3,"y":0},"t":26,"s":[257,355,0],"to":[0,0,0],"ti":[0,0,0]},{"t":34,"s":[257,348,0]}],"ix":2},"a":{"a":0,"k":[1,92,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.29,"y":0},"t":18,"s":[{"i":[[40.641,-9.352],[8.57,0],[3.849,60.125]],"o":[[-7.86,1.809],[-52.469,0],[-0.359,-5.614]],"v":[[24.514,89.694],[-0.188,92.423],[-97.849,-4.773]],"c":false}]},{"t":28,"s":[{"i":[[40.641,-9.352],[8.57,0],[3.849,60.125]],"o":[[-7.86,1.809],[-52.469,0],[-0.359,-5.614]],"v":[[24.514,89.694],[-0.188,92.423],[-97.849,-4.773]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":5,"s":[15]},{"t":18,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.746],"y":[0.894]},"o":{"x":[0.444],"y":[0]},"t":5,"s":[12]},{"i":{"x":[0.323],"y":[1]},"o":{"x":[0.222],"y":[3.401]},"t":8,"s":[1]},{"t":18,"s":[0]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":7,"op":180,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Leg Micro","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[255.826,401.556,0],"ix":2},"a":{"a":0,"k":[-0.174,145.556,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.31,"y":0},"t":14,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,145.556],[0,87.306]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.3,"y":0},"t":26,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,145.556],[0,108.056]],"c":false}]},{"t":34,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,145.556],[0,96.556]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":10,"s":[100],"h":1}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":10,"op":180,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Line Micro","parent":9,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-0.188,-4.773,0],"ix":2},"a":{"a":0,"k":[-0.188,-4.773,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.2,"y":0},"t":15,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-127.636,-134.81],[127.26,120.264]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.3,"y":0},"t":27,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-130.376,-135.802],[130,124.756]],"c":false}]},{"t":36,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-127.636,-133.31],[127.26,121.764]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.2],"y":[0]},"t":15,"s":[12]},{"t":27,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.2],"y":[0]},"t":15,"s":[88]},{"t":27,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":15,"op":180,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Body Micro","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-0.007,55.352,0],"ix":2},"a":{"a":0,"k":[-0.007,55.352,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[5.374,0],[0,33.137]],"o":[[0,0],[-4.956,1.325],[-33.137,0],[0,0]],"v":[[-60.015,-22.243],[15.538,53.321],[0,55.352],[-60,-4.648]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-33.137,0],[0,-33.137],[0,0],[6.989,-9.81]],"o":[[0,0],[0,-33.137],[33.137,0],[0,0],[0,12.962],[0,0]],"v":[[-60.015,-78.778],[-60,-94.648],[0,-154.648],[60,-94.648],[60,-4.648],[48.901,30.126]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":14,"op":180,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Pre-comp 1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[13.6,13.6,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":180,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/tgs/voice_chat_cancel_reminder_to_raise_hand.tgs b/Telegram-Mac/tgs/voice_chat_cancel_reminder_to_raise_hand.tgs new file mode 100644 index 0000000000..e67df7bdf0 --- /dev/null +++ b/Telegram-Mac/tgs/voice_chat_cancel_reminder_to_raise_hand.tgs @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":37,"w":100,"h":100,"nm":"Cancel Reminder to Rise Hand","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Line Bell","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.3,"y":0},"t":0,"s":[-0.05,-3.45,0],"to":[0,0,0],"ti":[0,0,0]},{"t":12,"s":[-0.05,6.55,0]}],"ix":2},"a":{"a":0,"k":[-0.05,-2.95,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-127.5,-130.5],[127.4,124.6]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.3],"y":[0]},"t":0,"s":[0]},{"t":12,"s":[45]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.3],"y":[0]},"t":0,"s":[100]},{"t":12,"s":[69]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Bell","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.3,"y":0},"t":0,"s":[255.967,230.7,0],"to":[0,0,0],"ti":[0,0,0]},{"t":12,"s":[255.967,270.7,0]}],"ix":2},"a":{"a":0,"k":[-0.033,-25.3,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.3,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,8.3],[-3.1,2.9],[0,0],[-1.3,19.1]],"o":[[0,0],[0,0],[0,0],[-8.3,0],[0,-4.2],[0,0],[14.1,-13],[0,0]],"v":[[-92.8,-53.2],[61.507,101.107],[62.6,102.2],[-121.4,102.2],[-136.4,87.2],[-131.5,76.1],[-121.9,67.3],[-97.8,17.4]],"c":true}]},{"i":{"x":0.39,"y":1},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[0,0],[0,0],[38.659,-1.562],[4.281,3.01],[1.12,7.93],[-0.341,2.329],[0,0],[-1.3,19.1]],"o":[[0,0],[0,0],[-6.977,0.282],[-10.666,-1.589],[-0.564,-3.993],[0,0],[0.484,-12.336],[0,0]],"v":[[-57.671,-46.497],[73.792,78.988],[41.341,102.165],[-39.334,101.192],[-58.438,81.628],[-58.493,71.738],[-58.984,62.939],[-58.394,17.677]],"c":true}]},{"t":12,"s":[{"i":[[0,0],[0,0],[43.698,-1.448],[12.406,4.131],[3.267,7.372],[0.556,1.97],[0,0],[-1.446,18.138]],"o":[[0,0],[0,0],[-11.229,0.372],[-6.46,-2.151],[-1.641,-3.704],[0,0],[-0.111,-11.633],[0,0]],"v":[[-48.211,-24.391],[70.073,75.677],[33.802,102.243],[-28.226,99.869],[-42.829,86.574],[-45.443,77.695],[-46.584,69.338],[-47.029,22.141]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.3,"y":0},"t":0,"s":[{"i":[[-22.1,0],[-1.3,-20.9],[0,0],[0,0],[-1.7,-22.9],[0,0],[-14.1,-12.9],[0,0],[5.3,-6.4],[0,0],[0,0],[-8,3.2],[0,0]],"o":[[21.2,0],[0,0],[0,0],[21.3,8.5],[0,0],[1.4,19.1],[0,0],[6.1,5.6],[-4.5,5.4],[0,0],[5.8,-6],[0,0],[0,-21.9]],"v":[[0,-152.8],[39.9,-115.3],[40,-112.8],[55.1,-106.8],[92.7,-55.4],[97.9,17.3],[122,67.2],[131.6,76],[132.5,97.2],[119.1,102],[-75.9,-93],[-55.1,-107],[-40,-113]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[-22.099,-0.179],[-9.673,0.954],[0,0],[-5.58,-0.001],[-0.404,-15.261],[0,0],[-0.026,-10.474],[0,0],[1.462,-6],[0,0],[0,0],[-8.129,2.691],[0,0]],"o":[[14.199,0.115],[0,0],[0,0],[14.825,0.003],[0,0],[0.501,10.873],[0,0],[-0.049,5.1],[-1.594,16.585],[0,0],[-2.61,-4.059],[0,0],[9.699,0.657]],"v":[[1.801,-54.012],[37.173,-55.601],[42.433,-56.666],[54.675,-57.9],[73.342,-33.104],[75.259,15.053],[77.526,51.827],[78.299,60.503],[75.594,79.518],[57.904,98.584],[-57.39,-40.088],[-48.371,-55.338],[-38.949,-55.054]],"c":true}]},{"i":{"x":0.39,"y":1},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-22.099,-0.225],[-8.365,1.104],[0,0],[0,0],[-0.079,-13.344],[0,0],[0.751,-7.994],[0,0],[0.499,-5.899],[0,0],[0,0],[-5.57,2.731],[0,0]],"o":[[27.812,0.283],[0,0],[0,0],[6.707,1.409],[0,0],[0.276,8.809],[0,0],[0.63,3.135],[-0.176,10.718],[0,0],[0.584,-3.973],[2.662,-1.306],[8.129,-0.583]],"v":[[-0.562,-42.244],[44.865,-46.065],[46.667,-46.335],[55.315,-46.381],[65.736,-28.011],[68.079,14.49],[68.249,48.533],[67.809,59.178],[66.702,71.892],[61.822,89.407],[-48.992,-35.757],[-43.93,-47.443],[-35.437,-46.835]],"c":true}]},{"t":12,"s":[{"i":[[-24.589,0],[-6.676,-0.546],[0,0],[0,0],[0.195,-11.253],[0,0],[-0.278,-10.042],[0,0],[-0.28,-5.528],[0,0],[0,0],[-0.278,0.712],[0,0]],"o":[[23.587,0],[0,0],[0,0],[4.923,0.285],[0,0],[0.111,6.837],[0,0],[-0.25,2.612],[0.556,10.968],[0,0],[-0.257,-3.472],[0,0],[9.763,0]],"v":[[-2.357,-26.148],[40.784,-26.029],[44.372,-26.148],[52.55,-26.267],[70.352,-16.747],[70.852,18.58],[71.381,48.28],[71.075,55.093],[71.103,68.104],[69.406,86.076],[-48.106,-14.853],[-47.946,-25.745],[-46.723,-26.932]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":13,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Bottom Bell","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.39,"y":1},"o":{"x":0.3,"y":0},"t":0,"s":[0,137.7,0],"to":[0,0,0],"ti":[0,0,0]},{"t":12,"s":[12,91.7,0]}],"ix":2},"a":{"a":0,"k":[0,137.7,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.39,"y":1},"o":{"x":0.3,"y":0},"t":0,"s":[{"i":[[0,0],[20.9,0],[7.5,18.2]],"o":[[-7.5,18.2],[-20.9,0],[0,0]],"v":[[46.3,122.2],[0,153.2],[-46.3,122.2]],"c":true}]},{"t":12,"s":[{"i":[[0,0],[36.5,-0.45],[3.3,26.05]],"o":[[-1.55,18.55],[-20.898,0.258],[0,0]],"v":[[52.1,115.7],[-1.45,138.95],[-51,115.2]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":13,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Head","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.39,"y":1},"o":{"x":0.3,"y":0},"t":0,"s":[0.613,-101.537,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.39,"y":1},"o":{"x":0.3,"y":0},"t":13,"s":[11.613,-40.537,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.39,"y":1},"o":{"x":0.3,"y":0},"t":26,"s":[11.613,-89.537,0],"to":[0,0,0],"ti":[0,0,0]},{"t":36,"s":[11.613,-84.037,0]}],"ix":2},"a":{"a":0,"k":[11.613,-84.537,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.3,"y":0},"t":0,"s":[{"i":[[0,-19.31],[19.31,0],[0,19.31],[-19.31,0]],"o":[[0,19.31],[-19.31,0],[0,-19.31],[19.31,0]],"v":[[46.576,-84.537],[11.613,-49.574],[-23.35,-84.537],[11.613,-119.5]],"c":true}]},{"i":{"x":0.39,"y":1},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[0,-25.538],[22.395,0],[0,25.538],[-22.395,0]],"o":[[0,25.538],[-22.395,0],[0,-25.538],[22.395,0]],"v":[[49.653,-76.337],[9.103,-30.097],[-31.446,-76.337],[9.103,-122.577]],"c":true}]},{"i":{"x":0.39,"y":1},"o":{"x":0.3,"y":0},"t":14,"s":[{"i":[[0,-24.735],[24.735,0],[0,24.735],[-24.735,0]],"o":[[0,24.735],[-24.735,0],[0,-24.735],[24.735,0]],"v":[[56.4,-84.537],[11.613,-39.75],[-33.174,-84.537],[11.613,-129.324]],"c":true}]},{"t":25,"s":[{"i":[[0,-26.924],[26.924,0],[0,26.924],[-26.924,0]],"o":[[0,26.924],[-26.924,0],[0,-26.924],[26.924,0]],"v":[[60.363,-84.537],[11.613,-35.787],[-37.137,-84.537],[11.613,-133.287]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Hands","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.05,"y":0},"t":9,"s":[11.336,12.774,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.31,"y":0},"t":12,"s":[11.336,18.274,0],"to":[0,0,0],"ti":[0,0,0]},{"t":25,"s":[11.336,10.774,0]}],"ix":2},"a":{"a":0,"k":[11.336,10.774,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.3,"y":0},"t":7,"s":[{"i":[[0,0],[0.875,44.36],[0.012,11.61]],"o":[[0,0],[-0.348,-17.658],[-0.016,-14.955]],"v":[[-33.583,109.659],[-40.875,50.64],[-45.012,-17.11]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":9,"s":[{"i":[[0,0],[-7.48,30.25],[-7.964,9.648]],"o":[[0,0],[2.73,-11.373],[7.228,-11.196]],"v":[[-57.163,97.688],[-57.996,36.651],[-42.036,0.102]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[-14.611,30.379],[-23.925,1.795]],"o":[[0,0],[7.389,-12.121],[13.171,-8.112]],"v":[[-77.526,99.758],[-81.889,29.906],[-41.075,4.99]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[0,0],[-27.718,19.589],[-10.136,-1.028]],"o":[[0,0],[21.782,-16.411],[19.114,-5.028]],"v":[[-107.888,77.829],[-93.782,16.161],[-42.114,8.877]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13,"s":[{"i":[[0,0],[-27.325,4.127],[-11.558,-0.909]],"o":[[0,0],[20.334,-3.734],[35.444,0.066]],"v":[[-136.225,32.858],[-90.834,7.893],[-39.525,9.472]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":14,"s":[{"i":[[0,0],[-27.129,-3.603],[-14.269,-4.157]],"o":[[0,0],[19.61,2.605],[43.609,2.613]],"v":[[-147.393,-5.627],[-89.61,-2.992],[-38.231,9.77]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[0,0],[-38.825,-22.336],[-19.04,-2.789]],"o":[[0,0],[14.675,9.164],[62.76,8.586]],"v":[[-144.801,-73.381],[-91.175,-7.197],[-39.46,8.757]],"c":false}]},{"t":28,"s":[{"i":[[0,0],[-46.458,-35.583],[-19.738,-3.567]],"o":[[0,0],[8.352,6.397],[79.039,14.286]],"v":[[-142.583,-131.341],[-88.063,-16.235],[-40.512,7.89]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.3,"y":0},"t":7,"s":[{"i":[[0,0],[0.583,25.777],[4.5,19.11]],"o":[[0,0],[-0.238,-10.518],[-18.41,-78.181]],"v":[[63,101.64],[63.167,54.973],[56,-16.61]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":9,"s":[{"i":[[0,0],[2.455,25.149],[6.868,13.265]],"o":[[0,0],[-0.66,-9.745],[-29.265,-65.813]],"v":[[67.548,109.42],[68.368,48.723],[59.021,2.29]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[-11.495,10.995],[15.537,39.692],[13.754,5.326]],"o":[[14.034,-13.424],[-3.378,-8.551],[-47.075,-45.52]],"v":[[79.466,115.674],[91.463,38.058],[62.996,10.174]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":14,"s":[{"i":[[-16.063,9.648],[30.097,36.471],[12.946,2.18]],"o":[[16.791,-10.086],[-5.422,-7.653],[-60.473,-30.253]],"v":[[92.709,118.199],[93.903,31.642],[65.054,12.67]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":25,"s":[{"i":[[0,0],[48,36.333],[11.5,1.167]],"o":[[0,0],[-8.388,-6.35],[-79.91,-8.107]],"v":[[141.25,132.89],[94.667,23.223],[63.5,11.89]],"c":false}]},{"t":28,"s":[{"i":[[0,0],[48,36.333],[11.5,1.167]],"o":[[0,0],[-8.388,-6.35],[-79.91,-8.107]],"v":[[140.75,130.39],[94.667,23.848],[63.5,11.89]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":7,"op":180,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Body","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.3,"y":0},"t":12,"s":[266.676,398.378,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.3,"y":0},"t":23,"s":[266.676,387.378,0],"to":[0,0,0],"ti":[0,0,0]},{"t":34,"s":[266.676,392.378,0]}],"ix":2},"a":{"a":0,"k":[10.676,136.378,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.69,"y":1},"o":{"x":0.31,"y":0},"t":12,"s":[{"i":[[-12.288,0.098],[-12.498,-0.987],[0,0],[3.339,-1.222],[4.897,-9.213],[22.369,0],[7.456,13.82],[-1.892,54.808]],"o":[[8.847,-0.071],[35.724,2.82],[24.706,0],[0,0],[-7.456,13.82],[-22.369,0],[-4.897,-9.213],[-12.353,-3.008]],"v":[[-46.407,7.071],[-18.334,9.585],[61.794,9.679],[70.141,28.952],[68.805,121.176],[13.161,136.5],[-42.484,121.176],[-47.047,25.191]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.31,"y":0},"t":23,"s":[{"i":[[-10.8,-2.3],[-8.2,-0.4],[0,0],[3,-1.3],[4.4,-9.8],[20.1,0],[6.7,14.7],[-1.7,58.3]],"o":[[7.9,1.7],[0,0],[22.2,0],[0,0],[-6.7,14.7],[-20.1,0],[-4.4,-9.8],[-11.1,-3.2]],"v":[[-40.2,-2.3],[-15.1,1.5],[56.9,1.6],[64.4,22.1],[63.2,120.2],[13.2,136.5],[-36.8,120.2],[-40.9,18.1]],"c":true}]},{"t":34,"s":[{"i":[[-10.8,-2.3],[-8.2,-0.4],[0,0],[3,-1.3],[4.4,-9.8],[24.175,0.094],[6.7,14.7],[-1.7,58.3]],"o":[[7.9,1.7],[0,0],[22.2,0],[0,0],[-6.669,14.144],[-20.1,-0.078],[-4.4,-9.8],[-11.1,-3.2]],"v":[[-40.2,-2.3],[-15.1,1.5],[56.9,1.6],[64.4,22.1],[63.044,119.606],[13.2,136.156],[-36.8,120.2],[-40.9,18.1]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":12,"op":180,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Cancel to Rise","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":0,"s":[50,50,0],"to":[0,0,0],"ti":[0,0,0]},{"t":10,"s":[48.25,49.939,0]}],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[13.5,13.5,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":180,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/tgs/voice_chat_hand_off.tgs b/Telegram-Mac/tgs/voice_chat_hand_off.tgs new file mode 100644 index 0000000000..8cfbe1e87d --- /dev/null +++ b/Telegram-Mac/tgs/voice_chat_hand_off.tgs @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":33,"w":100,"h":100,"nm":"hand_1 BIG 1x","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Piece Micro","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[35.759,87.461,0],"ix":2},"a":{"a":0,"k":[35.717,87.391,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.69,"y":1},"o":{"x":0.31,"y":0},"t":18,"s":[{"i":[[0,0],[-5.475,2.553],[0,0],[9.717,-2.249]],"o":[[6.02,-1.382],[0,0],[-8.509,4.686],[0,0]],"v":[[16.421,79.423],[33.7,73.485],[48.513,88.296],[21.075,98.797]],"c":true}]},{"t":26,"s":[{"i":[[0,0],[-5.475,2.553],[0,0],[9.717,-2.249]],"o":[[6.02,-1.382],[0,0],[-8.509,4.686],[0,0]],"v":[[19.671,80.673],[36.95,74.735],[51.763,89.546],[24.325,100.047]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":18,"op":32,"st":-2,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Line Micro","parent":5,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.2,"y":0},"t":10,"s":[-23.146,-29.703,0],"to":[0,0,0],"ti":[0,0,0]},{"t":25,"s":[-0.146,-4.703,0]}],"ix":2},"a":{"a":0,"k":[-0.188,-4.773,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-127.636,-133.31],[127.26,121.764]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.2],"y":[0]},"t":10,"s":[0]},{"t":25,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":5,"op":32,"st":-2,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Arc Micro L","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.05,"y":0},"t":15,"s":[256.674,337.951,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.69,"y":1},"o":{"x":0.31,"y":0},"t":25,"s":[256.674,354.951,0],"to":[0,0,0],"ti":[0,0,0]},{"t":33,"s":[256.674,347.951,0]}],"ix":2},"a":{"a":0,"k":[1,92,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.1,"y":0},"t":12,"s":[{"i":[[3.519,-0.815],[0.013,-0.123],[-27.326,61.322]],"o":[[-5.313,1.23],[-60.987,3.126],[2.29,-5.139]],"v":[[-0.986,88.569],[-2.688,88.923],[-80.849,-31.773]],"c":false}]},{"t":26,"s":[{"i":[[3.519,-0.815],[0.013,-0.123],[3.849,60.125]],"o":[[-5.313,1.23],[-52.469,0],[-0.359,-5.614]],"v":[[24.514,90.069],[-0.188,92.423],[-97.849,-4.773]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":100,"ix":1},"e":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":12,"op":32,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Arc Micro R","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1,92,0],"ix":2},"a":{"a":0,"k":[1,92,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.1,"y":0},"t":12,"s":[{"i":[[-2.075,-4.719],[97.976,-7.574]],"o":[[18.826,42.818],[-26.308,2.034]],"v":[[80.5,-40.769],[2.349,88.623]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":17,"s":[{"i":[[-0.749,-4.962],[67.557,-10.679]],"o":[[7.327,34.748],[-22.552,10.838]],"v":[[92.957,-19.818],[18.269,89.184]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[-0.577,-4.993],[22.817,-13.465]],"o":[[5.836,33.701],[-22.064,11.98]],"v":[[94.573,-17.101],[68.509,63.272]],"c":false}]},{"t":26,"s":[{"i":[[0.267,-5.148],[14.476,-20.074]],"o":[[-1.482,28.566],[-15.434,21.402]],"v":[[97.5,-5.269],[72.849,63.123]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":12,"op":32,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Body Micro","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-0.007,55.352,0],"ix":2},"a":{"a":0,"k":[-0.007,55.352,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.1,"y":0},"t":12,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[5.374,0],[-2.424,35.947]],"o":[[0,0],[0,0],[0,0],[-16.514,10.566],[-33.137,0],[0,0]],"v":[[-58.44,-41.488],[-44.573,-30.19],[-42.08,-27.207],[35.714,48.858],[0.25,58.852],[-58.75,-5.148]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13,"s":[{"i":[[0,0],[-0.149,2.682],[0,0],[0,0],[5.374,0],[-2.167,35.648]],"o":[[0,0],[0.374,-6.725],[0,0],[-16.712,10.779],[-33.137,0],[0,0]],"v":[[-58.599,-37.38],[-58.798,-51.101],[-44.889,-37.038],[36.087,48.007],[0.223,58.48],[-58.883,-5.095]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":14,"s":[{"i":[[0,0],[-5.775,-0.4],[0,0],[0,0],[5.374,0],[-1.869,35.303]],"o":[[0,0],[15.039,1.043],[0,0],[-16.942,11.025],[-33.137,0],[0,0]],"v":[[-58.783,-32.635],[-47.213,-22.994],[-31.97,-34.385],[36.518,47.024],[0.193,58.05],[-59.036,-5.033]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[0,0],[-2.814,-2.916],[0,0],[0,0],[5.374,0],[-1.308,34.653]],"o":[[0,0],[21.697,22.486],[0,0],[-15.8,15.196],[-33.137,0],[0,0]],"v":[[-59.13,-26.99],[-5.122,27.235],[29.396,26.244],[42.126,40.525],[0.135,57.241],[-59.326,-4.918]],"c":true}]},{"i":{"x":0.4,"y":1},"o":{"x":0.167,"y":0.167},"t":17,"s":[{"i":[[0,0],[-2.337,-2.422],[0,0],[0,0],[5.374,0],[-1.087,34.397]],"o":[[0,0],[18.024,18.679],[0,0],[-6.994,4.392],[-33.137,0],[0,0]],"v":[[-59.267,-26.228],[-13.197,19.968],[8.38,41.877],[18.82,51.363],[0.112,56.921],[-59.44,-4.872]],"c":true}]},{"t":25,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[5.374,0],[0,33.137]],"o":[[0,0],[0,0],[0,0],[-6.014,2.816],[-33.137,0],[0,0]],"v":[[-59.94,-22.488],[-52.823,-15.69],[-50.33,-12.707],[15.214,52.858],[0,55.352],[-60,-4.648]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.1,"y":0},"t":12,"s":[{"i":[[0,0],[0,0],[-33.137,0],[0,-33.137],[0,0],[22.328,-14.905],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,-33.137],[33.137,0],[0,0],[0,12.962],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-58.512,-38.912],[-57.5,-94.648],[0,-154.648],[60,-95.148],[60,-4.648],[35.497,49.079],[6.828,25.023],[5.643,23.857],[-45.569,-26.172],[-52.206,-32.704]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":14,"s":[{"i":[[0,0],[0,0],[-33.137,0],[0,-33.137],[0,0],[21.18,-14.252],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,-33.137],[33.137,0],[0,0],[0,12.962],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-58.921,-64.869],[-58.073,-94.648],[0,-154.648],[60,-95.033],[60,-4.648],[35.984,47.676],[8.297,16.878],[7.111,15.711],[-32.106,-34.129],[-43.117,-48.037]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[0,0],[0,0],[-33.137,0],[0,-33.137],[0,0],[11.35,-8.538],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,-33.137],[33.137,0],[0,0],[0,12.962],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-59.174,-61.282],[-58.651,-94.648],[0,-154.648],[60,-94.918],[60,-4.648],[41.725,40.509],[28.231,25.697],[22.421,20.905],[-36.521,-35.942],[-46.219,-47.637]],"c":true}]},{"i":{"x":0.4,"y":1},"o":{"x":0.167,"y":0.167},"t":17,"s":[{"i":[[0,0],[0,0],[-33.137,0],[0,-33.137],[0,0],[12.36,-9.134],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,-33.137],[33.137,0],[0,0],[0,12.962],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-59.273,-59.865],[-58.879,-94.648],[0,-154.648],[60,-94.872],[60,-4.648],[42.031,39.173],[25.878,24.736],[20.85,20.558],[-38.264,-36.658],[-47.444,-47.479]],"c":true}]},{"t":25,"s":[{"i":[[0,0],[0,0],[-33.137,0],[0,-33.137],[0,0],[17.315,-12.056],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,-33.137],[33.137,0],[0,0],[0,12.962],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-59.762,-52.912],[-60,-94.648],[0,-154.648],[60,-94.648],[60,-4.648],[37.622,42.954],[14.328,20.023],[13.143,18.857],[-46.819,-40.172],[-53.456,-46.704]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":12,"op":32,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Bottom","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256,393.7,0],"ix":2},"a":{"a":0,"k":[0,137.7,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.1,"y":0},"t":11,"s":[{"i":[[-23.625,3.905],[2.177,-3.719],[7.061,0],[-0.093,6.444],[1.459,5.025]],"o":[[3.75,3.155],[-2.5,4.272],[-7.091,0],[0.061,-4.171],[0,0]],"v":[[22.875,62.22],[23.25,80.478],[-0.657,84.963],[-23.938,79.806],[-22.5,63.25]],"c":true}]},{"i":{"x":0.69,"y":1},"o":{"x":0.31,"y":0},"t":20,"s":[{"i":[[0,0],[0.125,-11.343],[3.142,0],[-0.042,17],[0.649,13.257]],"o":[[-0.654,13.355],[-0.186,16.872],[-3.156,0],[0.027,-11.003],[0,0]],"v":[[10.25,98.5],[10,137.968],[-0.25,155.075],[-10.277,138.503],[-11,98.25]],"c":true}]},{"t":33,"s":[{"i":[[0,0],[0.125,-11.343],[3.142,0],[-0.042,17],[0.649,13.257]],"o":[[-0.654,13.355],[-0.186,16.872],[-3.156,0],[0.027,-11.003],[0,0]],"v":[[10.25,91.5],[10,137.968],[-0.25,155.075],[-10.277,138.503],[-11,91.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":11,"op":32,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Head","parent":9,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[11.863,-83.787,0],"to":[0,0,0],"ti":[0,0,0]},{"t":15,"s":[13.613,-35.537,0]}],"ix":2},"a":{"a":0,"k":[11.613,-84.537,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[{"i":[[0,-26.924],[26.924,0],[0,26.924],[-26.924,0]],"o":[[0,26.924],[-26.924,0],[0,-26.924],[26.924,0]],"v":[[60.363,-84.537],[11.613,-35.787],[-37.137,-84.537],[11.613,-133.287]],"c":true}]},{"t":15,"s":[{"i":[[0,-31.539],[32.309,0],[0,31.539],[-32.309,0]],"o":[[0,31.539],[-32.309,0],[0,-31.539],[32.309,0]],"v":[[66.5,-92.894],[8,-35.787],[-50.5,-92.894],[8,-150]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":13,"st":-2,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Hands","parent":9,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[11.336,10.774,0],"ix":2},"a":{"a":0,"k":[11.336,10.774,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":0,"s":[{"i":[[0,0],[-46.458,-35.583],[-23.488,-5.64]],"o":[[0,0],[8.352,6.397],[78.1,18.753]],"v":[[-141.583,-130.591],[-87.875,-16.36],[-40.512,7.89]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[{"i":[[0,0],[-36.01,-10.9],[-24.155,1.341]],"o":[[0,0],[17.49,5.6],[69.436,6.533]],"v":[[-154.359,-38.55],[-101.24,-1.379],[-38.096,3.379]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[0,0],[-22.495,10.354],[-22.973,-2.409]],"o":[[0,0],[18.597,-8.552],[52.89,-5.679]],"v":[[-130.223,30.529],[-97.344,4.391],[-37.533,-6.68]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[0,0],[-15.738,20.981],[-22.382,-4.284]],"o":[[0,0],[16.262,-20.519],[44.618,-11.784]],"v":[[-110.655,59.818],[-92.396,11.026],[-37.251,-11.709]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[16.426,34.853],[-16.801,24.541]],"o":[[0,0],[-8.224,-22.916],[15.608,-10.11]],"v":[[-20.782,124.986],[-71.359,85.703],[-65.308,11.471]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[0,0],[23.781,38.026],[-13.402,32.182]],"o":[[0,0],[-13.823,-23.464],[8.974,-9.727]],"v":[[-0.229,139.889],[-67.183,101.348],[-71.932,18.21]],"c":false}]},{"t":15,"s":[{"i":[[0,0],[26.875,39.36],[-23.988,37.11]],"o":[[0,0],[-16.179,-23.695],[6.183,-9.565]],"v":[[8.417,146.159],[-61.875,108.64],[-60.512,10.39]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20.8,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":0,"s":[{"i":[[0,0],[48,36.333],[11.5,1.167]],"o":[[0,0],[-8.388,-6.35],[-79.91,-8.107]],"v":[[141,130.39],[94.542,23.473],[63.5,11.89]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[{"i":[[-0.107,1.301],[43.705,35.181],[12.499,4.424]],"o":[[1.452,-17.63],[-7.425,-6.513],[-75.279,-8.976]],"v":[[133.797,130.851],[92.055,22.64],[62.256,6.25]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[-6.276,1.09],[19.852,26.915],[21.224,10.921]],"o":[[6.157,-12.218],[-8.148,-12.585],[-54.801,-12.818]],"v":[[109.709,105.725],[104.015,23.092],[59.642,-11.414]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[-14.501,0.808],[-4.07,22.818],[16.234,26.478]],"o":[[22.299,-4.21],[2.93,-14.682],[-27.498,-17.941]],"v":[[46.092,123.722],[94.461,75.694],[84.157,4.034]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[-18.614,0.668],[-13.269,19.896],[21.751,46.134]],"o":[[37.453,-1.343],[5.355,-8.676],[-13.846,-20.502]],"v":[[13.534,139.471],[87.934,101.996],[89.914,12.758]],"c":false}]},{"t":15,"s":[{"i":[[-19.258,0.691],[-15.167,22.777],[28.5,51.61]],"o":[[38.75,-1.39],[5.831,-8.757],[-11.559,-20.931]],"v":[[13.25,145.39],[86.667,104.723],[81,0.89]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20.8,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":13,"st":-2,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Body","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[266.676,392.378,0],"to":[0,0,0],"ti":[0,0,0]},{"t":15,"s":[256.676,329.378,0]}],"ix":2},"a":{"a":0,"k":[10.676,136.378,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":0,"s":[{"i":[[-10.795,-2.32],[-8.2,-0.399],[0,0],[3,-1.298],[6.05,-16.428],[20.1,0],[6.7,14.68],[-1.7,58.223]],"o":[[7.9,1.698],[0,0],[22.2,0],[0,0],[-7.825,16.029],[-20.1,0],[-4.4,-9.787],[-11.1,-3.196]],"v":[[-40.2,-2.303],[-15.1,1.492],[56.9,1.592],[64.619,22.065],[63.2,120.034],[13.2,136.313],[-36.8,120.034],[-40.9,18.07]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[-8.633,5.293],[-30.694,1.458],[-5.351,-5.031],[1.258,-10.091],[6.44,-10.804],[20.1,0],[7.449,15.688],[-1.349,43.667]],"o":[[7.117,-6.707],[30.806,-0.042],[11.274,9.219],[0,0],[-6.7,14.7],[-20.1,0],[-6.249,-11.442],[-2.218,-11.127]],"v":[[-40.364,-13.955],[7.946,-9.369],[59.854,-15.63],[65.994,22.18],[62.483,107.448],[12.403,127.733],[-38.553,106.811],[-43.53,19.216]],"c":true}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-3.532,7.461],[-40.135,-1.1],[-0.656,-13.918],[0.242,-12.883],[9.063,-12.095],[20.1,0],[8.412,16.959],[-0.898,24.854]],"o":[[-1.139,-17.662],[45.351,1.243],[3.277,16.039],[0,0],[-6.7,14.7],[-20.1,0],[-8.626,-13.553],[0.197,-17.251]],"v":[[-44.576,-28.939],[4.435,-23.344],[64.508,-30.64],[68.043,22.282],[61.561,91.053],[11.378,116.462],[-40.808,89.596],[-46.912,20.65]],"c":true}]},{"t":15,"s":[{"i":[[-0.825,11.096],[-17.081,-0.811],[-0.9,-19.1],[0.6,-11.1],[10.8,-12.95],[20.1,0],[9.05,17.8],[-0.6,12.4]],"o":[[2.45,-32.95],[31.6,1.5],[0.584,12.387],[0,0],[-6.7,14.7],[-20.1,0],[-10.2,-14.95],[0.65,-14.85]],"v":[[-48.95,-36.8],[9.65,-82],[69.4,-39.9],[69.4,22.35],[60.95,80.2],[10.7,109],[-42.3,78.2],[-49.15,21.6]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":13,"st":-2,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"EXAMPLE MIC","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[255.85,251.45,0],"ix":2},"a":{"a":0,"k":[-0.15,-4.55,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-5.5,2.5],[0,0],[9.7,-2.2]],"o":[[6,-1.4],[0,0],[-8.5,4.7],[0,0]],"v":[[19.7,80.7],[37,74.8],[51.8,89.6],[24.4,100.1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,145.6],[0,96.6]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[52.9,0],[3.9,60.1]],"o":[[-3.3,62.7],[-52.5,0],[0,0]],"v":[[97.5,-5.3],[-0.2,92.4],[-97.9,-4.8]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":17,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":-276,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-127.6,-132.3],[127.3,122.8]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0.5,-1],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[5.3,0],[0,33.1]],"o":[[0,0],[-5,1.3],[-33.1,0],[0,0]],"v":[[-60,-22.2],[15.6,53.4],[0.1,55.4],[-59.9,-4.6]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-33.1,0],[0,-33.1],[0,0],[7,-9.8]],"o":[[0,0],[0,-33.1],[33.1,0],[0,0],[0,13],[0,0]],"v":[[-60,-78.8],[-60,-94.7],[0,-154.7],[60,-94.7],[60,-4.7],[48.9,30.1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":32,"op":179,"st":32,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"512 х 512","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.3,"y":0},"t":0,"s":[48.179,49.929,0],"to":[0,0,0],"ti":[0,0,0]},{"t":10,"s":[50,50,0]}],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4,0.4],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":0,"s":[13.5,13.5,100]},{"t":10,"s":[13.6,13.6,100]}],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":180,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/tgs/voice_chat_hand_on_muted.tgs b/Telegram-Mac/tgs/voice_chat_hand_on_muted.tgs new file mode 100644 index 0000000000..87128e9066 --- /dev/null +++ b/Telegram-Mac/tgs/voice_chat_hand_on_muted.tgs @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":37,"w":100,"h":100,"nm":"Mute to Rise Hand","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Piece Micro","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[35.759,87.461,0],"ix":2},"a":{"a":0,"k":[35.717,87.391,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":0,"s":[{"i":[[0,0],[-5.475,2.553],[0,0],[9.717,-2.249]],"o":[[6.02,-1.382],[0,0],[-8.509,4.686],[0,0]],"v":[[19.671,80.673],[36.95,74.735],[51.763,89.546],[24.325,100.047]],"c":true}]},{"t":3,"s":[{"i":[[0,0],[-5.475,2.553],[0,0],[9.717,-2.249]],"o":[[6.02,-1.382],[0,0],[-8.509,4.686],[0,0]],"v":[[21.421,87.423],[41.95,80.735],[56.263,95.796],[26.075,106.797]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":5,"st":-2,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Line Micro","parent":5,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-0.146,-4.703,0],"ix":2},"a":{"a":0,"k":[-0.188,-4.773,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-127.636,-133.31],[127.26,121.764]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.3],"y":[0]},"t":0,"s":[0]},{"t":8,"s":[31]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.3],"y":[0]},"t":0,"s":[100]},{"t":8,"s":[63]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":9,"st":-2,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Arc Micro L","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256.674,347.951,0],"ix":2},"a":{"a":0,"k":[1,92,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.3,"y":0},"t":0,"s":[{"i":[[3.519,-0.815],[0.013,-0.123],[3.849,60.125]],"o":[[-5.313,1.23],[-52.469,0],[-0.359,-5.614]],"v":[[24.514,90.069],[-0.188,92.423],[-97.849,-4.773]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2,"s":[{"i":[[1.471,-0.93],[-0.261,0.617],[-19.998,61.048]],"o":[[-15.86,2.784],[-47.111,-1.578],[0.165,-4.967]],"v":[[24.185,95.265],[-9.53,96.806],[-77.592,-16.338]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[{"i":[[-0.003,-1.012],[-0.459,1.149],[-37.168,61.713]],"o":[[0.001,0.461],[-43.254,-2.714],[0.543,-4.501]],"v":[[-12.171,99.436],[-17.116,99.103],[-63.006,-24.664]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-0.237,-0.406],[0.535,1.668],[-49.73,52.573]],"o":[[0.175,0.049],[-34.246,-7.492],[1.191,-3.702]],"v":[[-27.176,93.752],[-30.642,93.267],[-48.945,-28.524]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[-0.704,0.808],[2.521,2.704],[-43.353,29.793]],"o":[[0.524,-0.777],[-16.229,-17.046],[2.486,-2.104]],"v":[[-57.186,82.384],[-57.695,81.595],[-41.821,-16.744]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[1.213,2.258],[1.222,3.645],[-48.776,11.726]],"o":[[-1.544,-3.936],[-9.361,-21.271],[3.645,-0.675]],"v":[[-81.46,82.005],[-84.396,73.487],[-38.483,7.977]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":9,"s":[{"i":[[2.172,2.984],[0.573,4.116],[-51.487,2.692]],"o":[[-2.578,-5.516],[-5.927,-23.384],[4.224,0.039]],"v":[[-93.597,81.815],[-97.747,69.433],[-36.438,15.838]],"c":false}]},{"i":{"x":0.7,"y":0.929},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0.604,3.211],[-0.855,5.674],[-53.714,-4.729]],"o":[[-1.028,-4.372],[-0.969,-20.989],[4.7,0.626]],"v":[[-104.911,76.785],[-106.192,63.75],[-35.805,22.631]],"c":false}]},{"t":12,"s":[{"i":[[-1.763,3.555],[-3.008,8.024],[-57.076,-15.928]],"o":[[1.312,-2.645],[6.513,-17.373],[5.419,1.512]],"v":[[-121.986,69.194],[-118.938,55.173],[-34.849,24.727]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":100,"ix":1},"e":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":12,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Arc Micro R","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1,92,0],"ix":2},"a":{"a":0,"k":[1,92,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.3,"y":0},"t":0,"s":[{"i":[[0.267,-5.148],[14.476,-20.074]],"o":[[-1.482,28.566],[-15.434,21.402]],"v":[[97.5,-5.269],[72.849,63.123]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[{"i":[[-0.57,-4.693],[14.667,-13.219]],"o":[[29.416,55.057],[-24.972,4.986]],"v":[[68.91,-29.008],[64.658,77.768]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-1.028,-4.444],[24.895,-14.766]],"o":[[40.169,51.756],[-24.242,6.58]],"v":[[58.657,-30.707],[52.93,79.815]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[-1.943,-3.947],[27.351,-17.359]],"o":[[45.174,41.653],[-22.784,9.769]],"v":[[57.651,-19.104],[60.474,87.408]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-2.928,-3.412],[39.179,-24.327]],"o":[[40.899,28.324],[-21.215,13.198]],"v":[[61.177,7.725],[63.686,108.51]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":9,"s":[{"i":[[-3.42,-3.144],[45.093,-27.812]],"o":[[38.818,28.86],[-20.431,14.912]],"v":[[62.939,17.39],[65.292,119.061]],"c":false}]},{"i":{"x":0.7,"y":0.912},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[-3.831,-2.921],[40.815,-30.179]],"o":[[37.049,25.3],[-19.776,16.343]],"v":[[64.243,23.666],[72.894,123.416]],"c":false}]},{"t":12,"s":[{"i":[[-4.464,-2.577],[34.226,-33.824]],"o":[[34.326,19.818],[-18.768,18.547]],"v":[[66.25,26.981],[84.599,130.123]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":12,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Body Micro","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.39,"y":1},"o":{"x":0.3,"y":0},"t":0,"s":[-0.007,55.352,0],"to":[0,0,0],"ti":[0,0,0]},{"t":12,"s":[12.993,145.352,0]}],"ix":2},"a":{"a":0,"k":[-0.007,55.352,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.3,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[5.374,0],[0,33.137]],"o":[[0,0],[0,0],[0,0],[-6.014,2.816],[-33.137,0],[0,0]],"v":[[-59.94,-22.488],[-52.823,-15.69],[-50.33,-12.707],[15.214,52.858],[0,55.352],[-60,-4.648]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[5.371,-0.126],[-0.237,30.744]],"o":[[0,0],[0,0],[0,0],[-17.997,11.33],[-40.563,1.088],[0,0]],"v":[[-59.94,-36.117],[-45.029,-24.143],[-42.205,-21.969],[43.557,44.171],[-0.14,54.374],[-60,7.559]],"c":true}]},{"i":{"x":0.39,"y":1},"o":{"x":0.167,"y":0.167},"t":7,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[5.37,-0.182],[-0.328,31.102]],"o":[[0,0],[0,0],[0,0],[-15.448,7.691],[-43.418,1.571],[0,0]],"v":[[-59.94,-28.693],[-46.39,-18.448],[-44.092,-16.732],[41.381,46.564],[-0.193,53.933],[-60,10.399]],"c":true}]},{"t":12,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[5.369,-0.225],[-0.424,28.855]],"o":[[0,0],[0,0],[0,0],[-12.764,3.21],[-46.424,1.947],[0,0]],"v":[[-59.94,-13.773],[-47.823,-6.212],[-46.08,-5.124],[39.089,49.839],[-0.25,53.602],[-60,17.194]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.3,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[-33.137,0],[0,-33.137],[0,0],[17.315,-12.056],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,-33.137],[33.137,0],[0,0],[0,12.962],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-59.762,-52.912],[-60,-94.648],[0,-154.648],[60,-94.648],[60,-4.648],[37.622,42.954],[14.328,20.023],[13.143,18.857],[-46.819,-40.172],[-53.456,-46.704]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[0,0],[0,0],[-40.437,-0.678],[0.741,-26.114],[0,0],[20.746,-7.359],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,-24.615],[39.111,0.655],[0,0],[0,9.628],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-59.762,-25.847],[-60.14,-66.023],[0.14,-99.059],[59.301,-66.416],[59.721,10.004],[37.867,47.001],[13.769,29.608],[12.165,28.184],[-46.819,-16.384],[-53.456,-21.236]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[0,0],[0,0],[-41.841,-0.872],[0.883,-26.628],[0,0],[21.405,-6.947],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,-24.709],[40.259,0.839],[0,0],[0,9.665],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-59.762,-25.95],[-60.167,-68.811],[0.167,-98.791],[59.167,-69.314],[59.667,10.039],[37.914,47.63],[13.662,30.071],[11.977,28.488],[-46.819,-16.45],[-53.456,-21.321]],"c":true}]},{"i":{"x":0.39,"y":1},"o":{"x":0.167,"y":0.167},"t":7,"s":[{"i":[[0,0],[0,0],[-43.244,-1.029],[1.026,-25.986],[0,0],[22.065,-6.233],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,-23.731],[41.407,0.986],[0,0],[0,9.283],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-59.762,-22.857],[-60.193,-66.543],[0.193,-92.166],[59.033,-67.134],[59.613,11.706],[37.961,48.26],[13.555,31.296],[11.789,29.623],[-46.819,-13.734],[-53.456,-18.412]],"c":true}]},{"t":12,"s":[{"i":[[0,0],[0,0],[-46.199,-1.318],[1.326,-23.658],[0,0],[23.453,-4.522],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,-20.798],[43.826,1.25],[0,0],[0,8.136],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-59.762,-13.098],[-60.25,-56.793],[0.25,-72.451],[58.75,-57.543],[59.5,17.194],[38.06,50.196],[13.328,35.116],[11.393,33.321],[-46.819,-5.102],[-53.456,-9.202]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":13,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Bottom","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256,393.7,0],"ix":2},"a":{"a":0,"k":[0,137.7,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.3,"y":0},"t":0,"s":[{"i":[[0,0],[0.125,-11.343],[3.142,0],[-0.042,17],[0.649,13.257]],"o":[[-0.654,13.355],[-0.186,16.872],[-3.156,0],[0.027,-11.003],[0,0]],"v":[[10.25,91.5],[10,137.968],[-0.25,155.075],[-10.277,138.503],[-11,91.25]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[{"i":[[0,0],[4.545,-8.511],[4.581,0],[3.261,12.598],[0.642,9.835]],"o":[[-0.152,9.854],[-2.975,12.338],[-4.601,0],[-2.414,-8.262],[42.009,9.552]],"v":[[36.609,82.674],[23.766,130.718],[3.7,143.321],[-17.825,130.983],[-28.759,85.698]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[0,0],[6.548,-7.228],[12.78,-0.245],[4.758,10.603],[0.639,8.285]],"o":[[0.076,8.268],[-4.238,10.283],[-13.239,0.254],[-7.506,-10.326],[15.093,1.012]],"v":[[54.551,67.683],[36.502,120.933],[4.489,136.246],[-27.494,121.576],[-43.941,72.097]],"c":true}]},{"i":{"x":0.39,"y":1},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[0,0],[9.716,-5.879],[18.391,-1.398],[5.995,8.257],[0.713,6.655]],"o":[[0.354,6.6],[-10.172,7.77],[-16.917,1.286],[-9.444,-7.993],[22.501,1.344]],"v":[[62.646,80.214],[42.953,118.48],[7.653,131.398],[-31.882,118.743],[-50.5,83.513]],"c":true}]},{"t":11,"s":[{"i":[[0,0],[15.385,-3.671],[8.11,0],[11.361,4.847],[0.624,3.825]],"o":[[1.08,3.63],[-9.813,4.125],[-8.145,0],[-8.401,-3.584],[35.858,2.885]],"v":[[69.922,113.355],[58.865,130.928],[14.728,135.5],[-34.991,130.534],[-45.75,114.194]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":13,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Head","parent":9,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.39,"y":1},"o":{"x":0.3,"y":0},"t":1,"s":[0.613,-101.537,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.39,"y":1},"o":{"x":0.3,"y":0},"t":12,"s":[11.613,-40.537,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.39,"y":1},"o":{"x":0.3,"y":0},"t":26,"s":[11.613,-89.537,0],"to":[0,0,0],"ti":[0,0,0]},{"t":36,"s":[11.613,-84.537,0]}],"ix":2},"a":{"a":0,"k":[11.613,-84.537,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.2,"y":0},"t":3,"s":[{"i":[[0,-20.996],[31.442,0],[0,20.996],[-31.442,0]],"o":[[0,20.996],[-31.442,0],[0,-20.996],[31.442,0]],"v":[[68.534,-78.114],[11.603,-40.097],[-45.327,-78.114],[11.603,-116.131]],"c":true}]},{"i":{"x":0.39,"y":1},"o":{"x":0.3,"y":0},"t":13,"s":[{"i":[[0,-24.735],[24.735,0],[0,24.735],[-24.735,0]],"o":[[0,24.735],[-24.735,0],[0,-24.735],[24.735,0]],"v":[[56.4,-84.537],[11.613,-39.75],[-33.174,-84.537],[11.613,-129.324]],"c":true}]},{"t":25,"s":[{"i":[[0,-26.924],[26.924,0],[0,26.924],[-26.924,0]],"o":[[0,26.924],[-26.924,0],[0,-26.924],[26.924,0]],"v":[[60.363,-84.537],[11.613,-35.787],[-37.137,-84.537],[11.613,-133.287]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":2,"op":180,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Hands","parent":9,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.05,"y":0},"t":9,"s":[11.336,12.774,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.31,"y":0},"t":12,"s":[11.336,18.274,0],"to":[0,0,0],"ti":[0,0,0]},{"t":25,"s":[11.336,10.774,0]}],"ix":2},"a":{"a":0,"k":[11.336,10.774,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.3,"y":0},"t":7,"s":[{"i":[[0,0],[0.875,44.36],[0.012,11.61]],"o":[[0,0],[-0.348,-17.658],[-0.016,-14.955]],"v":[[-33.583,109.659],[-40.875,50.64],[-45.012,-17.11]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":9,"s":[{"i":[[0,0],[-7.48,30.25],[-7.964,9.648]],"o":[[0,0],[2.73,-11.373],[7.228,-11.196]],"v":[[-57.163,97.688],[-57.996,36.651],[-42.036,0.102]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[-14.611,30.379],[-23.925,1.795]],"o":[[0,0],[7.389,-12.121],[13.171,-8.112]],"v":[[-77.526,99.758],[-81.889,29.906],[-41.075,4.99]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[0,0],[-27.718,19.589],[-10.136,-1.028]],"o":[[0,0],[21.782,-16.411],[19.114,-5.028]],"v":[[-107.888,77.829],[-93.782,16.161],[-42.114,8.877]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13,"s":[{"i":[[0,0],[-27.325,4.127],[-11.558,-0.909]],"o":[[0,0],[20.334,-3.734],[35.444,0.066]],"v":[[-136.225,32.858],[-90.834,7.893],[-39.525,9.472]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":14,"s":[{"i":[[0,0],[-27.129,-3.603],[-14.269,-4.157]],"o":[[0,0],[19.61,2.605],[43.609,2.613]],"v":[[-147.393,-5.627],[-89.61,-2.992],[-38.231,9.77]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[0,0],[-38.825,-22.336],[-19.04,-2.789]],"o":[[0,0],[14.675,9.164],[62.76,8.586]],"v":[[-144.801,-73.381],[-91.175,-7.197],[-39.46,8.757]],"c":false}]},{"t":28,"s":[{"i":[[0,0],[-46.458,-35.583],[-16.613,-2.875]],"o":[[0,0],[8.352,6.397],[79.143,13.697]],"v":[[-142.583,-131.341],[-87.875,-16.36],[-40.512,7.89]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.3,"y":0},"t":7,"s":[{"i":[[0,0],[0.583,25.777],[4.5,19.11]],"o":[[0,0],[-0.238,-10.518],[-18.41,-78.181]],"v":[[63,101.64],[63.167,54.973],[56,-16.61]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":9,"s":[{"i":[[0,0],[2.455,25.149],[6.868,13.265]],"o":[[0,0],[-0.66,-9.745],[-29.265,-65.813]],"v":[[67.548,109.42],[68.368,48.723],[59.021,2.29]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[-11.495,10.995],[15.537,39.692],[13.754,5.326]],"o":[[14.034,-13.424],[-3.378,-8.551],[-47.075,-45.52]],"v":[[79.466,115.674],[91.463,38.058],[62.996,10.174]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":12,"s":[{"i":[[-13.018,10.546],[20.39,38.618],[11.568,3.994]],"o":[[14.953,-12.311],[-4.059,-8.252],[-51.541,-40.431]],"v":[[83.88,116.515],[92.276,35.919],[66.432,12.756]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":14,"s":[{"i":[[-16.063,9.648],[30.097,36.471],[12.946,2.18]],"o":[[16.791,-10.086],[-5.422,-7.653],[-60.473,-30.253]],"v":[[92.709,118.199],[93.903,31.642],[65.054,12.67]],"c":false}]},{"t":25,"s":[{"i":[[0,0],[48,36.333],[11.5,1.167]],"o":[[0,0],[-8.388,-6.35],[-79.91,-8.107]],"v":[[141.25,132.89],[94.667,23.223],[63.5,11.89]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":12,"op":180,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Body","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.3,"y":0},"t":12,"s":[266.676,398.378,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.3,"y":0},"t":23,"s":[266.676,385.878,0],"to":[0,0,0],"ti":[0,0,0]},{"t":34,"s":[266.676,392.378,0]}],"ix":2},"a":{"a":0,"k":[10.676,136.378,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.69,"y":1},"o":{"x":0.31,"y":0},"t":12,"s":[{"i":[[-12.288,0.098],[-12.498,-0.987],[0,0],[3.339,-1.222],[4.897,-9.213],[22.369,0],[7.456,13.82],[-1.892,54.808]],"o":[[8.847,-0.071],[35.724,2.82],[24.706,0],[0,0],[-7.456,13.82],[-22.369,0],[-4.897,-9.213],[-12.353,-3.008]],"v":[[-46.407,7.071],[-18.334,9.585],[61.794,9.679],[70.141,28.952],[68.805,121.176],[13.161,136.5],[-42.484,121.176],[-47.047,25.191]],"c":true}]},{"t":23,"s":[{"i":[[-10.8,-2.3],[-8.2,-0.4],[0,0],[3,-1.3],[4.4,-9.8],[20.1,0],[6.7,14.7],[-1.7,58.3]],"o":[[7.9,1.7],[0,0],[22.2,0],[0,0],[-6.7,14.7],[-20.1,0],[-4.4,-9.8],[-11.1,-3.2]],"v":[[-40.2,-2.3],[-15.1,1.5],[56.9,1.6],[64.4,22.1],[63.2,120.2],[13.2,136.5],[-36.8,120.2],[-40.9,18.1]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":12,"op":180,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"EXAMPLE MIC","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[255.85,251.45,0],"ix":2},"a":{"a":0,"k":[-0.15,-4.55,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-5.5,2.5],[0,0],[9.7,-2.2]],"o":[[6,-1.4],[0,0],[-8.5,4.7],[0,0]],"v":[[19.7,80.7],[37,74.8],[51.8,89.6],[24.4,100.1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,145.6],[0,96.6]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[52.9,0],[3.9,60.1]],"o":[[-3.3,62.7],[-52.5,0],[0,0]],"v":[[97.5,-5.3],[-0.2,92.4],[-97.9,-4.8]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":17,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":-276,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-127.6,-132.3],[127.3,122.8]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[5.3,0],[0,33.1]],"o":[[0,0],[-5,1.3],[-33.1,0],[0,0]],"v":[[-60,-22.2],[15.6,53.4],[0.1,55.4],[-59.9,-4.6]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-33.1,0],[0,-33.1],[0,0],[7,-9.8]],"o":[[0,0],[0,-33.1],[33.1,0],[0,0],[0,13],[0,0]],"v":[[-60,-78.8],[-60,-94.7],[0,-154.7],[60,-94.7],[60,-4.7],[48.9,30.1]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Pre-comp 1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.3,"y":0},"t":0,"s":[50,50,0],"to":[0,0,0],"ti":[0,0,0]},{"t":12,"s":[48.25,50,0]}],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[13.5,13.5,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":180,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/tgs/voice_chat_hand_on_unmuted.tgs b/Telegram-Mac/tgs/voice_chat_hand_on_unmuted.tgs new file mode 100644 index 0000000000..c0e6838f5b Binary files /dev/null and b/Telegram-Mac/tgs/voice_chat_hand_on_unmuted.tgs differ diff --git a/Telegram-Mac/tgs/voice_chat_mute.tgs b/Telegram-Mac/tgs/voice_chat_mute.tgs new file mode 100644 index 0000000000..93eaeb5c8a --- /dev/null +++ b/Telegram-Mac/tgs/voice_chat_mute.tgs @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":10,"w":100,"h":100,"nm":"Mute","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Line Micro","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.4,"y":0},"t":0,"s":[-22.404,-24.971,0],"to":[0,0,0],"ti":[0,0,0]},{"t":10,"s":[-0.188,-4.773,0]}],"ix":2},"a":{"a":0,"k":[-0.188,-4.773,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-127.636,-133.31],[127.26,121.764]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":2,"op":146,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Piece Micro","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[35.717,87.391,0],"ix":2},"a":{"a":0,"k":[35.717,87.391,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-5.475,2.553],[0,0],[9.717,-2.249]],"o":[[6.02,-1.382],[0,0],[-8.509,4.686],[0,0]],"v":[[19.671,80.673],[36.95,74.735],[51.763,89.546],[24.325,100.047]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":2,"op":146,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Arc Micro R","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1,92,0],"ix":2},"a":{"a":0,"k":[1,92,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[0.267,-5.148],[43.123,-20.509]],"o":[[-1.482,28.566],[-23.829,11.333]],"v":[[97.5,-5.269],[42.599,83.623]],"c":false}]},{"t":5.294921875,"s":[{"i":[[0.267,-5.148],[14.849,-16.38]],"o":[[-1.482,28.566],[-17.722,19.55]],"v":[[97.5,-5.269],[71.726,62.502]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":2,"op":146,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Arc Micro L","parent":5,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1,92,0],"ix":2},"a":{"a":0,"k":[1,92,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[40.641,-9.352],[8.57,0],[3.849,60.125]],"o":[[-7.86,1.809],[-52.469,0],[-0.359,-5.614]],"v":[[24.514,89.694],[-0.188,92.423],[-97.849,-4.773]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":100,"ix":1},"e":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":2,"op":146,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Leg Micro","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[255.542,401.577,0],"ix":2},"a":{"a":0,"k":[-0.174,145.556,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,145.556],[0,96.556]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":2,"op":146,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Body Micro","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-0.007,55.352,0],"ix":2},"a":{"a":0,"k":[-0.007,55.352,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[5.374,0],[0,33.137]],"o":[[0,0],[0,0],[0,0],[-15.031,11.505],[-33.137,0],[0,0]],"v":[[-59.94,-53.988],[-47.823,-41.94],[-46.08,-40.207],[37.589,42.983],[0,55.352],[-60,-4.648]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3.529,"s":[{"i":[[0,0],[-4.996,-4.735],[2.304,13.945],[0,0],[5.374,0],[0,33.137]],"o":[[0,0],[11.845,11.225],[12.943,7.13],[-14.926,16.457],[-33.137,0],[0,0]],"v":[[-60.015,-22.243],[-50.225,-12.901],[-16.955,-25.054],[43.665,36.302],[0,55.352],[-60,-4.648]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4.117,"s":[{"i":[[0,0],[-2.48,-2.386],[1.747,9.042],[0,0],[5.374,0],[0,33.137]],"o":[[0,0],[11.912,11.459],[6.472,3.565],[-13.949,17.763],[-33.137,0],[0,0]],"v":[[-60.015,-22.243],[-13.607,23.672],[21.465,14.857],[43.394,36.924],[0,55.352],[-60,-4.648]],"c":true}]},{"t":4.705078125,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[5.374,0],[0,33.137]],"o":[[0,0],[0,0],[0,0],[-4.956,1.325],[-33.137,0],[0,0]],"v":[[-60.015,-22.243],[-50.629,-12.855],[-49.278,-11.504],[15.538,53.321],[0,55.352],[-60,-4.648]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[{"i":[[0,0],[0,0],[-33.137,0],[0,-33.137],[0,0],[17.315,-12.056],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,-33.137],[33.137,0],[0,0],[0,12.962],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-59.762,-52.912],[-60,-94.648],[0,-154.648],[60,-94.648],[60,-4.648],[37.622,42.954],[14.328,20.023],[13.143,18.857],[-46.819,-40.172],[-53.456,-46.704]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3.529,"s":[{"i":[[0,0],[0,0],[-33.137,0],[0,-33.137],[0,0],[13.749,-12.109],[0.144,3.058],[0,0],[0,0],[0,0]],"o":[[0,0],[0,-33.137],[33.137,0],[0,0],[0,12.962],[0,0],[-0.204,-4.337],[0,0],[0,0],[0,0]],"v":[[-59.888,-65.845],[-60,-94.648],[0,-154.648],[60,-94.648],[60,-4.648],[41.387,39.198],[-16.607,-23.328],[-19.879,-31.177],[-52.786,-59.691],[-56.428,-62.846]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4.117,"s":[{"i":[[0,0],[0,0],[-33.137,0],[0,-33.137],[0,0],[11.966,-12.135],[-1.605,4.302],[0,0],[0,0],[0,0]],"o":[[0,0],[0,-33.137],[33.137,0],[0,0],[0,12.962],[0,0],[2.172,-5.82],[0,0],[0,0],[0,0]],"v":[[-59.952,-72.312],[-60,-94.648],[0,-154.648],[60,-94.648],[60,-4.648],[43.27,37.32],[20.885,18.085],[17.089,7.006],[-47.967,-60.145],[-54.112,-66.384]],"c":true}]},{"t":4.705078125,"s":[{"i":[[0,0],[0,0],[-33.137,0],[0,-33.137],[0,0],[6.989,-9.81],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,-33.137],[33.137,0],[0,0],[0,12.962],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-60.015,-78.778],[-60,-94.648],[0,-154.648],[60,-94.648],[60,-4.648],[48.901,30.126],[22.849,4.077],[21.523,2.751],[-45.54,-64.305],[-52.962,-71.726]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":2,"op":146,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Micro EXAMPLE","parent":5,"sr":1,"ks":{"o":{"a":1,"k":[{"t":0,"s":[100],"h":1},{"t":2,"s":[0],"h":1}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.326,0.381,0],"ix":2},"a":{"a":0,"k":[0,0.332,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-33.137,0],[0,-33.137],[0,0],[33.137,0],[0,33.137],[0,0]],"o":[[33.137,0],[0,0],[0,33.137],[-33.137,0],[0,0],[0,-33.137]],"v":[[0,-154.648],[60,-94.648],[60,-4.648],[0,55.352],[-60,-4.648],[-60,-94.648]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-5.501,0],[0,-5.501],[54.673,-5.031],[0,0],[5.501,0],[0.711,4.803],[0,0],[0,0],[0,55.991],[-5.501,0],[0,-5.501],[-48.347,0],[0,48.347]],"o":[[5.501,0],[0,55.986],[0,0],[0,5.501],[-5.001,0],[0,0],[0,0],[-54.68,-5.024],[0,-5.501],[5.501,0],[0,48.347],[48.347,0],[0,-5.501]],"v":[[97.5,-14.608],[107.46,-4.648],[9.973,102.355],[9.96,145.352],[0,155.312],[-9.852,146.824],[-9.96,145.352],[-9.958,102.357],[-107.46,-4.648],[-97.5,-14.608],[-87.54,-4.648],[0,82.892],[87.54,-4.648]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-1,"op":180,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Pre-comp 1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[13.6,13.6,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":180,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/tgs/voice_chat_raise_hand_1.tgs b/Telegram-Mac/tgs/voice_chat_raise_hand_1.tgs new file mode 100644 index 0000000000..fe5cbd6766 --- /dev/null +++ b/Telegram-Mac/tgs/voice_chat_raise_hand_1.tgs @@ -0,0 +1 @@ +{"v":"5.7.6","fr":60,"ip":0,"op":120,"w":100,"h":100,"nm":"hand_1 BIG 1x","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"NULL CONTROL","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256.989,275.127,0],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":119,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"head","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":1,"s":[0]},{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":11,"s":[0]},{"i":{"x":[0.1],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":23,"s":[21]},{"t":107,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":1,"s":[122.706,17.15,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":11,"s":[122.706,68.999,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.589,"y":1},"o":{"x":0.275,"y":0},"t":23,"s":[150.706,5.999,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.206,"y":0},"t":32,"s":[150.706,32.05,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.589,"y":1},"o":{"x":0.275,"y":0},"t":40,"s":[150.706,27.67,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.206,"y":0},"t":47,"s":[150.706,32.05,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.589,"y":1},"o":{"x":0.275,"y":0},"t":54,"s":[150.706,27.67,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.206,"y":0},"t":61,"s":[150.706,32.05,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.1,"y":1},"o":{"x":0.6,"y":0},"t":70,"s":[150.706,27.67,0],"to":[0,0,0],"ti":[0,0,0]},{"t":107,"s":[122.706,17.15,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":1,"s":[500,500,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":5,"s":[485,515,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":11,"s":[525,475,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":16,"s":[485,515,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":23,"s":[525,475,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":32,"s":[500,500,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":40,"s":[500,500,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":47,"s":[500,500,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":54,"s":[500,500,100]},{"i":{"x":[0.2,0.2,0.2],"y":[1,1,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":61,"s":[500,500,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":70,"s":[500,500,100]},{"i":{"x":[0.1,0.1,0.1],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":77,"s":[480,520,100]},{"t":107,"s":[500,500,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-6.341,0],[0,-6.341],[6.341,0],[0,6.341]],"o":[[6.341,0],[0,6.341],[-6.341,0],[0,-6.341]],"v":[[0,-11.482],[11.482,0],[0,11.482],[-11.482,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"NULL CONTROL","parent":7,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.2,"y":1},"o":{"x":0.5,"y":0},"t":21,"s":[-1.541,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.13,"y":1},"o":{"x":0.6,"y":0},"t":66,"s":[-1.541,-20.177,0],"to":[0,0,0],"ti":[0,0,0]},{"t":94,"s":[-1.541,-17.777,0]}],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[20,20,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":3,"nm":"hands","parent":3,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":23,"s":[8]},{"i":{"x":[0.13],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":65,"s":[8]},{"t":94,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[111,115,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[111,155,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":23,"s":[115,115,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":30,"s":[115,121,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":38,"s":[115,115,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":45,"s":[115,121,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":53,"s":[115,115,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":57,"s":[115,121,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.13,"y":1},"o":{"x":0.6,"y":0},"t":65,"s":[115,115,0],"to":[0,0,0],"ti":[0,0,0]},{"t":94,"s":[111,115,0]}],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"hands 3","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,115,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],"o":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],"v":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.6,"y":0},"t":10,"s":[{"i":[[0,0],[-0.71,-5.814],[-13.22,1.67],[-6.666,-0.138]],"o":[[-3.232,5.731],[0.566,4.637],[3.994,-0.505],[1.186,0.024]],"v":[[-18.219,-21.464],[-27.079,-2.224],[-7.644,3.484],[11.485,2.829]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":23,"s":[{"i":[[0,0],[-0.961,-3.853],[-4.491,-2.782],[-8.28,0.661]],"o":[[1.597,6.857],[1.517,6.08],[4.565,2.828],[1.182,-0.094]],"v":[[-20.247,-37.336],[-17.926,-14.859],[-7.359,2.636],[10.724,2.534]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":26,"s":[{"i":[[0,0],[-0.511,-3.901],[-4.724,-3.504],[-8.291,0.526]],"o":[[2.238,7.884],[0.807,6.156],[4.331,3.14],[1.184,-0.075]],"v":[[-18.139,-37.479],[-16.265,-15.877],[-7.359,2.636],[10.941,2.657]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":30,"s":[{"i":[[0,0],[0.125,-3.969],[-5.052,-4.524],[-8.435,1.023]],"o":[[-2.798,7.352],[-0.197,6.263],[4,3.582],[1.177,-0.143]],"v":[[-6.969,-38.346],[-13.919,-17.314],[-7.359,2.636],[9.48,3.009]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":34,"s":[{"i":[[0,0],[-0.447,-3.908],[-4.757,-3.608],[-8.213,0.625]],"o":[[-4.119,5.691],[0.705,6.167],[4.297,3.185],[1.182,-0.09]],"v":[[-10.167,-36.731],[-16.027,-16.023],[-7.359,2.636],[11.49,2.929]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":37.666,"s":[{"i":[[0,0],[-0.961,-3.853],[-4.491,-2.782],[-8.28,0.661]],"o":[[1.597,6.857],[1.517,6.08],[4.565,2.828],[1.182,-0.094]],"v":[[-20.247,-37.336],[-17.926,-14.859],[-7.359,2.636],[10.724,2.534]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":41,"s":[{"i":[[0,0],[-0.511,-3.901],[-4.724,-3.504],[-8.291,0.526]],"o":[[2.238,7.884],[0.807,6.156],[4.331,3.14],[1.184,-0.075]],"v":[[-18.139,-37.479],[-16.265,-15.877],[-7.359,2.636],[10.941,2.657]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":45,"s":[{"i":[[0,0],[0.125,-3.969],[-5.052,-4.524],[-8.435,1.023]],"o":[[-2.798,7.352],[-0.197,6.263],[4,3.582],[1.177,-0.143]],"v":[[-6.969,-38.346],[-13.919,-17.314],[-7.359,2.636],[9.48,3.009]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":48,"s":[{"i":[[0,0],[-0.447,-3.908],[-4.757,-3.608],[-8.213,0.625]],"o":[[-4.119,5.691],[0.705,6.167],[4.297,3.185],[1.182,-0.09]],"v":[[-10.167,-36.731],[-16.027,-16.023],[-7.359,2.636],[11.49,2.929]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":52.334,"s":[{"i":[[0,0],[-0.961,-3.853],[-4.491,-2.782],[-8.28,0.661]],"o":[[1.597,6.857],[1.517,6.08],[4.565,2.828],[1.182,-0.094]],"v":[[-20.247,-37.336],[-17.926,-14.859],[-7.359,2.636],[10.724,2.534]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":56,"s":[{"i":[[0,0],[-0.511,-3.901],[-4.724,-3.504],[-8.291,0.526]],"o":[[2.238,7.884],[0.807,6.156],[4.331,3.14],[1.184,-0.075]],"v":[[-18.139,-37.479],[-16.265,-15.877],[-7.359,2.636],[10.941,2.657]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":60,"s":[{"i":[[0,0],[0.125,-3.969],[-5.052,-4.524],[-8.435,1.023]],"o":[[-2.798,7.352],[-0.197,6.263],[4,3.582],[1.177,-0.143]],"v":[[-6.969,-38.346],[-13.919,-17.314],[-7.359,2.636],[9.48,3.009]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":64,"s":[{"i":[[0,0],[-0.447,-3.908],[-4.757,-3.608],[-8.213,0.625]],"o":[[-4.119,5.691],[0.705,6.167],[4.297,3.185],[1.182,-0.09]],"v":[[-10.167,-36.731],[-16.027,-16.023],[-7.359,2.636],[11.49,2.929]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":68,"s":[{"i":[[0,0],[-0.961,-3.853],[-4.491,-2.782],[-8.28,0.661]],"o":[[1.597,6.857],[1.517,6.08],[4.565,2.828],[1.182,-0.094]],"v":[[-20.247,-37.336],[-17.926,-14.859],[-7.359,2.636],[10.724,2.534]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":71.666,"s":[{"i":[[0,0],[-0.511,-3.901],[-4.724,-3.504],[-8.522,-0.294]],"o":[[2.238,7.884],[0.807,6.156],[4.331,3.14],[1.185,0.041]],"v":[[-18.139,-37.479],[-16.265,-15.877],[-7.359,2.636],[11.49,2.929]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":76,"s":[{"i":[[0,0],[0.125,-3.969],[-5.052,-4.524],[-9.764,-0.338]],"o":[[-2.798,7.352],[-0.197,6.263],[4,3.582],[1.185,0.041]],"v":[[-6.969,-38.346],[-13.919,-17.314],[-7.359,2.636],[11.487,2.829]],"c":false}]},{"i":{"x":0.09,"y":1},"o":{"x":0.167,"y":0.167},"t":84,"s":[{"i":[[0,0],[-1.151,-9.822],[-6.924,-2.401],[-5.542,0.051]],"o":[[-3.654,5.5],[0.733,6.255],[4.389,1.538],[1.186,-0.011]],"v":[[-12.896,-35.078],[-19.976,-13.433],[-7.442,2.05],[9.027,3.064]],"c":false}]},{"t":104,"s":[{"i":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],"o":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],"v":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4.8,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"hands 2","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,115,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],"o":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],"v":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.6,"y":0},"t":10,"s":[{"i":[[-2.832,-0.073],[-2.303,-0.066],[-1.926,-3.085],[-0.239,-5.607]],"o":[[2.794,0.072],[11.894,0.339],[2.109,3.377],[0,0]],"v":[[2.758,2.83],[11.206,2.761],[28.065,7.836],[30.818,22.809]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":23,"s":[{"i":[[-2.816,0.314],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],"o":[[4.14,-0.462],[5.323,2.097],[1.94,4.605],[0,0]],"v":[[2.456,3.576],[12.827,2.873],[22.174,19.291],[30.152,32.241]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":31,"s":[{"i":[[-2.805,0.396],[-2.168,-0.779],[-1.2,-3.73],[-2.259,-5.223]],"o":[[3.592,-0.507],[6.085,2.162],[1.511,4.741],[0,0]],"v":[[2.725,4.04],[12.869,3.17],[22.316,18.867],[27.587,32.67]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":38,"s":[{"i":[[-2.816,0.314],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],"o":[[4.14,-0.462],[5.323,2.097],[1.94,4.605],[0,0]],"v":[[2.456,3.576],[12.827,2.873],[22.174,19.291],[30.152,32.241]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":46,"s":[{"i":[[-2.805,0.396],[-2.168,-0.779],[-1.2,-3.73],[-2.259,-5.223]],"o":[[3.592,-0.507],[6.085,2.162],[1.511,4.741],[0,0]],"v":[[2.725,4.04],[12.869,3.17],[22.316,18.867],[27.587,32.67]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":53,"s":[{"i":[[-2.816,0.314],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],"o":[[4.14,-0.462],[5.323,2.097],[1.94,4.605],[0,0]],"v":[[2.456,3.576],[12.827,2.873],[22.174,19.291],[30.152,32.241]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.5,"y":0},"t":61,"s":[{"i":[[-2.805,0.396],[-2.168,-0.779],[-1.2,-3.73],[-2.259,-5.223]],"o":[[3.592,-0.507],[6.085,2.162],[1.511,4.741],[0,0]],"v":[[2.725,4.04],[12.869,3.17],[22.316,18.867],[27.587,32.67]],"c":false}]},{"i":{"x":0.13,"y":1},"o":{"x":0.6,"y":0},"t":69,"s":[{"i":[[-2.816,0.314],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],"o":[[4.14,-0.462],[5.323,2.097],[1.94,4.605],[0,0]],"v":[[2.456,3.576],[12.827,2.873],[22.174,19.291],[30.152,32.241]],"c":false}]},{"t":100,"s":[{"i":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],"o":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],"v":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4.8,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"body","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[116.171,151.524,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.723,"y":1},"o":{"x":0.395,"y":0},"t":21,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.361,"y":0},"t":30,"s":[116.171,147.924,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.723,"y":1},"o":{"x":0.395,"y":0},"t":38,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.361,"y":0},"t":45,"s":[116.171,147.924,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.723,"y":1},"o":{"x":0.395,"y":0},"t":52,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.361,"y":0},"t":59,"s":[116.171,147.924,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.104,"y":0.104},"o":{"x":0.395,"y":0.395},"t":66,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"t":94,"s":[116.171,146.724,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,14.211,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":5,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.484,0.484,0.484],"y":[0,0,0]},"t":10,"s":[100,100,100]},{"i":{"x":[0.486,0.486,0.486],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":15,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":21,"s":[100,100,100]},{"i":{"x":[0.486,0.486,0.486],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":25,"s":[98,102,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":30,"s":[100,100,100]},{"i":{"x":[0.486,0.486,0.486],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":34,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.48,0.48,0.48],"y":[0,0,0]},"t":38,"s":[100,100,100]},{"i":{"x":[0.471,0.471,0.471],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":41,"s":[98,102,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.537,0.537,0.537],"y":[0,0,0]},"t":45,"s":[100,100,100]},{"i":{"x":[0.582,0.582,0.582],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":49,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.48,0.48,0.48],"y":[0,0,0]},"t":52,"s":[100,100,100]},{"i":{"x":[0.471,0.471,0.471],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":55,"s":[98,102,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.468,0.468,0.468],"y":[0,0,0]},"t":59,"s":[100,100,100]},{"i":{"x":[0.2,0.2,0.2],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":62,"s":[103,97,100]},{"i":{"x":[0.13,0.13,0.13],"y":[1,1,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":66,"s":[100,100,100]},{"t":94,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[{"i":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0],[0,0],[0,0]],"o":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0],[0,0],[0,0]],"v":[[-12.432,-14.575],[-11.977,8.464],[-8.487,13.499],[0.4,14.598],[9.223,13.566],[12.656,8.149],[12.482,-13.225],[8.179,-13.458],[-3.728,-14.103]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[{"i":[[0,0],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0,0],[0,0],[0,0]],"o":[[0,0],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,0],[0,0],[0,0]],"v":[[-14.032,-5.05],[-13.127,7.863],[-9.825,12.562],[0.35,14.211],[10.398,12.616],[13.744,7.749],[14.232,-5.571],[9.351,-5.481],[-4.158,-6.032]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":23,"s":[{"i":[[0.662,-3.59],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-2.532,4.395],[2.35,1.639],[4.409,1.158]],"o":[[-1.585,8.587],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.006,-0.084],[-3.61,-2.518],[-3.578,-0.94]],"v":[[-11.286,-17.808],[-12.133,7.863],[-8.83,12.562],[0.35,14.211],[9.404,12.616],[12.749,7.749],[13.232,-10.927],[9.318,-13.932],[-4.455,-14.008]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[1.056,-3.495],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-3.19,3.944],[2.35,1.814],[4.6,1.543]],"o":[[-2.702,8.946],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.021,-0.037],[-3.61,-2.786],[-3.733,-1.252]],"v":[[-11.366,-17.585],[-11.927,7.863],[-8.625,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[14.152,-8.699],[10.273,-11.929],[-3.437,-12.668]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":38,"s":[{"i":[[0.662,-3.59],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-2.532,4.395],[2.35,1.804],[4.409,1.445]],"o":[[-1.585,8.587],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.006,-0.084],[-3.61,-2.771],[-3.578,-1.173]],"v":[[-11.286,-19.408],[-12.133,7.863],[-8.83,12.562],[0.35,14.211],[9.404,12.616],[12.749,7.749],[13.232,-10.927],[9.318,-14.209],[-5.653,-15.762]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[3.203,-10.603],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-3.19,3.944],[3,-0.7],[3.7,2]],"o":[[-2.702,8.946],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.08,-0.14],[-3,0.7],[-3.7,-2]],"v":[[-11.166,-19.585],[-11.927,7.863],[-8.625,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[14.152,-8.699],[8.332,-14.539],[-4.768,-13.439]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":52,"s":[{"i":[[0.662,-3.59],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-2.532,4.395],[2.352,1.824],[4.412,1.481]],"o":[[-1.585,8.587],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.006,-0.084],[-3.613,-2.802],[-3.581,-1.202]],"v":[[-11.303,-19.608],[-12.133,7.863],[-8.83,12.562],[0.35,14.211],[9.404,12.616],[12.749,7.749],[13.232,-10.927],[9.315,-14.243],[-5.266,-16.412]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":59,"s":[{"i":[[1.056,-3.495],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-3.19,3.944],[2.33,2.05],[4.563,1.956]],"o":[[-2.702,8.946],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.021,-0.037],[-3.578,-3.149],[-3.704,-1.587]],"v":[[-11.164,-19.885],[-11.927,7.863],[-8.625,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[14.152,-8.699],[10.308,-12.326],[-4.786,-14.983]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.6,"y":0},"t":66,"s":[{"i":[[0.662,-3.59],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-2.532,4.395],[2.352,1.886],[4.412,1.589]],"o":[[-1.585,8.587],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.006,-0.084],[-3.613,-2.897],[-3.581,-1.29]],"v":[[-11.303,-20.208],[-12.133,7.863],[-8.83,12.562],[0.35,14.211],[9.404,12.616],[12.749,7.749],[13.232,-10.927],[9.315,-14.347],[-4.166,-15.759]],"c":false}]},{"i":{"x":0.13,"y":1},"o":{"x":0.167,"y":0.167},"t":79,"s":[{"i":[[0.229,-1.092],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-0.774,1.343],[2.54,0.846],[4.54,0.959]],"o":[[-0.964,4.6],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.002,-0.026],[-3.902,-1.3],[-3.684,-0.778]],"v":[[-12.005,-18.139],[-12.127,7.863],[-8.825,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[13.275,-12.666],[9.02,-14.164],[-5.367,-14.041]],"c":false}]},{"t":94,"s":[{"i":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0],[0,0],[0,0]],"o":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0],[0,0],[0,0]],"v":[[-12.432,-14.575],[-11.977,8.464],[-8.487,13.499],[0.4,14.598],[9.223,13.566],[12.656,8.149],[12.482,-13.225],[8.179,-13.458],[-3.728,-14.103]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"hand_1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[49,47.75,0],"ix":2,"l":2},"a":{"a":0,"k":[256,256,0],"ix":1,"l":2},"s":{"a":0,"k":[11.4,11.4,100],"ix":6,"l":2}},"ao":0,"w":512,"h":512,"ip":0,"op":120,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/tgs/voice_chat_raise_hand_2.tgs b/Telegram-Mac/tgs/voice_chat_raise_hand_2.tgs new file mode 100644 index 0000000000..d8cba2d9d3 --- /dev/null +++ b/Telegram-Mac/tgs/voice_chat_raise_hand_2.tgs @@ -0,0 +1 @@ +{"v":"5.7.6","fr":60,"ip":0,"op":120,"w":100,"h":100,"nm":"hand_2 BIG 1x","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"NULL CONTROL","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256.989,275.127,0],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":119,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"head","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":1,"s":[0]},{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":11,"s":[0]},{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":23,"s":[21]},{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":36,"s":[11]},{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":50,"s":[21]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.243],"y":[0]},"t":68,"s":[21]},{"i":{"x":[0.566],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":78,"s":[-15.198]},{"t":88,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":1,"s":[0,-37.327,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":11,"s":[0,-26.927,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":23,"s":[5.6,-39.527,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":36,"s":[4,-29.127,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.167,"y":0},"t":50,"s":[5.6,-39.527,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.5,"y":0},"t":66,"s":[4,-30.327,0],"to":[0,0,0],"ti":[0,0,0]},{"t":88,"s":[0,-37.327,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":1,"s":[100,100,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":5,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":11,"s":[105,95,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":16,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":23,"s":[105,95,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":29,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":36,"s":[105,95,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":43,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":50,"s":[105,95,100]},{"i":{"x":[0.4,0.4,0.4],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":57,"s":[95,105,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":66,"s":[102,98,100]},{"i":{"x":[0.2,0.2,0.2],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":78,"s":[95,105,100]},{"t":88,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-6.341,0],[0,-6.341],[6.341,0],[0,6.341]],"o":[[6.341,0],[0,6.341],[-6.341,0],[0,-6.341]],"v":[[0,-11.482],[11.482,0],[0,11.482],[-11.482,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"hands","parent":6,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":21,"s":[8]},{"i":{"x":[0.4],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":49,"s":[8]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":65,"s":[8]},{"t":89,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[-2.341,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[-2.341,-9.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":21,"s":[-1.541,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":35,"s":[-1.541,-11.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.5,"y":0},"t":49,"s":[-1.541,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.6,"y":0},"t":65,"s":[-1.541,-11.777,0],"to":[0,0,0],"ti":[0,0,0]},{"t":89,"s":[-2.341,-17.777,0]}],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[20,20,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"hands 3","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,115,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],"o":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],"v":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.6,"y":0},"t":10,"s":[{"i":[[0,0],[-0.71,-5.814],[-13.22,1.67],[-6.659,-0.445]],"o":[[-3.232,5.731],[0.566,4.637],[3.994,-0.505],[1.183,0.079]],"v":[[-18.219,-21.464],[-27.079,-2.224],[-7.644,3.484],[10.985,2.86]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":23,"s":[{"i":[[0,0],[-0.961,-3.853],[-4.984,-1.753],[-8.926,0.862]],"o":[[1.597,6.857],[1.517,6.08],[5.484,1.929],[1.18,-0.114]],"v":[[-20.247,-37.336],[-17.926,-14.859],[-7.906,2.713],[8.267,2.627]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":37,"s":[{"i":[[0,0],[-1.803,-4.807],[-6.68,-0.3],[-7.645,-0.484]],"o":[[-1.052,7.028],[2.083,5.552],[5.685,0.256],[1.184,0.075]],"v":[[-20.218,-23.988],[-22.536,-2.921],[-7.359,2.636],[10.433,2.572]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.7,"y":0},"t":51,"s":[{"i":[[0,0],[-0.961,-3.853],[-4.984,-1.753],[-8.926,0.862]],"o":[[1.597,6.857],[1.517,6.08],[5.484,1.929],[1.18,-0.114]],"v":[[-20.247,-37.336],[-17.926,-14.859],[-7.906,2.713],[8.267,2.627]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.4,"y":0},"t":67,"s":[{"i":[[0,0],[-1.803,-4.807],[-6.68,-0.3],[-7.645,-0.484]],"o":[[-1.052,7.028],[2.083,5.552],[5.685,0.256],[1.184,0.075]],"v":[[-20.218,-23.988],[-22.536,-2.921],[-7.359,2.636],[10.433,2.572]],"c":false}]},{"i":{"x":0.1,"y":1},"o":{"x":0.167,"y":0.167},"t":78,"s":[{"i":[[0,0],[-1.746,-7.006],[-9.167,-1.369],[-3.018,-0.266]],"o":[[-3.628,5.112],[1.694,6.8],[4.106,0.209],[1.182,0.076]],"v":[[-20.614,-25.461],[-25.11,-7.259],[-6.208,2.731],[7.297,3.206]],"c":false}]},{"t":105,"s":[{"i":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],"o":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],"v":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4.8,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"hands 2","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,115,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],"o":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],"v":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":10,"s":[{"i":[[-2.832,-0.073],[-2.303,-0.066],[-1.926,-3.085],[-0.239,-5.607]],"o":[[2.794,0.072],[11.894,0.339],[2.109,3.377],[0,0]],"v":[[2.747,2.681],[11.207,2.811],[28.065,7.836],[30.818,22.809]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":22,"s":[{"i":[[-2.805,0.395],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],"o":[[4.23,-0.595],[5.323,2.097],[1.94,4.605],[0,0]],"v":[[2.696,3.339],[12.911,2.962],[22.174,19.291],[30.152,32.241]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":36,"s":[{"i":[[-2.83,0.127],[-2.144,-0.845],[-1.985,-3.535],[-1.3,-6.195]],"o":[[5.352,-0.241],[5.323,2.097],[1.964,3.497],[0,0]],"v":[[1.908,2.538],[13.562,3.072],[22.983,13.903],[27.16,28.662]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.6,"y":0},"t":50,"s":[{"i":[[-2.805,0.395],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],"o":[[4.23,-0.595],[5.323,2.097],[1.94,4.605],[0,0]],"v":[[2.696,3.339],[12.911,2.962],[22.174,19.291],[30.152,32.241]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.167,"y":0.167},"t":58,"s":[{"i":[[-2.818,0.261],[-2.144,-0.845],[-1.78,-3.636],[-2.457,-5.823]],"o":[[4.791,-0.418],[5.323,2.097],[1.952,4.051],[0,0]],"v":[[2.321,3.134],[13.236,3.017],[22.578,16.597],[28.656,30.451]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.4,"y":0},"t":66,"s":[{"i":[[-2.83,0.127],[-2.144,-0.845],[-1.985,-3.535],[-1.3,-6.195]],"o":[[5.352,-0.241],[5.323,2.097],[1.964,3.497],[0,0]],"v":[[1.908,2.538],[13.562,3.072],[22.983,13.903],[27.16,28.662]],"c":false}]},{"i":{"x":0.1,"y":1},"o":{"x":0.167,"y":0.167},"t":76,"s":[{"i":[[-2.803,-0.138],[-2.518,-0.399],[-2.207,-4.957],[2.376,-4.348]],"o":[[3.802,0.187],[4.102,0.65],[1.627,3.656],[0,0]],"v":[[1.941,2.94],[11.88,3.616],[26.524,13.453],[23.943,27.653]],"c":false}]},{"t":97,"s":[{"i":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],"o":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],"v":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4.8,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"body","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[116.171,151.524,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":21,"s":[116.171,140.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":35,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.5,"y":0},"t":49,"s":[116.171,143.924,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":0.2},"o":{"x":0.6,"y":0.6},"t":65,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"t":89,"s":[116.171,146.724,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,14.211,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":5,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.484,0.484,0.484],"y":[0,0,0]},"t":10,"s":[100,100,100]},{"i":{"x":[0.486,0.486,0.486],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":15,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":21,"s":[100,100,100]},{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":27,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":35,"s":[100,100,100]},{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":42,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":49,"s":[100,100,100]},{"i":{"x":[0.4,0.4,0.4],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":56,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":65,"s":[100,100,100]},{"i":{"x":[0.2,0.2,0.2],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":76,"s":[97,103,100]},{"t":89,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[{"i":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0],[0,0]],"o":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0],[0,0]],"v":[[-12.432,-14.575],[-11.977,8.464],[-8.487,13.499],[0.4,14.598],[9.223,13.566],[12.656,8.149],[12.482,-13.225],[-2.127,-14.017]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[{"i":[[0,0],[0,0],[-2.036,-0.83],[-3.175,0],[-2.659,1.063],[-0.093,2.131],[0,0],[0,0]],"o":[[0,0],[0.446,2.497],[2.698,1.099],[3.123,0],[2.111,-0.844],[0,0],[0,0],[0,0]],"v":[[-14.932,-5.55],[-13.915,7.864],[-10.056,12.562],[0.354,14.211],[10.626,12.616],[14.232,7.749],[14.732,-4.571],[-1.668,-6.839]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":21,"s":[{"i":[[0.416,-4.877],[0,0],[-1.748,-0.83],[-2.726,0],[-2.283,1.063],[-0.08,2.131],[-1.162,6.43],[7.773,-0.606]],"o":[[-0.818,9.587],[0.13,2.059],[2.317,1.099],[2.681,0],[1.812,-0.844],[0,0],[0.023,-0.095],[-6.044,0.472]],"v":[[-11.695,-16.726],[-11.827,7.863],[-8.772,12.562],[0.392,14.211],[9.438,12.616],[12.534,7.749],[14.054,-9.469],[3.058,-14.133]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":35,"s":[{"i":[[0.168,-4.901],[0,0],[-1.759,-0.83],[-2.743,0],[-2.297,1.063],[-0.08,2.131],[-1.325,4.892],[7.682,-1.08]],"o":[[-0.283,8.264],[0.131,2.059],[2.331,1.099],[2.698,0],[1.823,-0.844],[0,0],[0.15,-0.385],[-6.216,0.874]],"v":[[-12.874,-10.003],[-11.908,7.863],[-8.834,12.562],[0.377,14.211],[9.469,12.616],[12.584,7.749],[14.152,-3.848],[-1.25,-8.759]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.5,"y":0},"t":49,"s":[{"i":[[0.416,-4.877],[0,0],[-1.748,-0.83],[-2.726,0],[-2.283,1.063],[-0.08,2.131],[-1.162,6.43],[9.673,-2.072]],"o":[[-0.818,9.587],[0.13,2.059],[2.317,1.099],[2.681,0],[1.812,-0.844],[0,0],[0.023,-0.095],[-5.963,1.277]],"v":[[-11.695,-17.126],[-11.827,7.863],[-8.772,12.562],[0.392,14.211],[9.438,12.616],[12.534,7.749],[14.054,-9.469],[-0.642,-13.867]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.6,"y":0},"t":65,"s":[{"i":[[0.168,-4.901],[0,0],[-1.759,-0.83],[-2.743,0],[-2.297,1.063],[-0.08,2.131],[-1.325,4.892],[9.682,-1.898]],"o":[[-0.283,8.264],[0.131,2.059],[2.331,1.099],[2.698,0],[1.823,-0.844],[0,0],[0.15,-0.385],[-6.146,1.205]],"v":[[-12.874,-10.003],[-11.908,7.863],[-8.834,12.562],[0.377,14.211],[9.469,12.616],[12.584,7.749],[14.152,-4.048],[-1.55,-8.541]],"c":false}]},{"t":89,"s":[{"i":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0],[0,0]],"o":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0],[0,0]],"v":[[-12.432,-14.575],[-11.977,8.464],[-8.487,13.499],[0.4,14.598],[9.223,13.566],[12.656,8.149],[12.482,-13.225],[-2.127,-14.017]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"hand_2","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[49,47.75,0],"ix":2,"l":2},"a":{"a":0,"k":[256,256,0],"ix":1,"l":2},"s":{"a":0,"k":[11.4,11.4,100],"ix":6,"l":2}},"ao":0,"w":512,"h":512,"ip":0,"op":120,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/tgs/voice_chat_raise_hand_3.tgs b/Telegram-Mac/tgs/voice_chat_raise_hand_3.tgs new file mode 100644 index 0000000000..36c8d7ddcf --- /dev/null +++ b/Telegram-Mac/tgs/voice_chat_raise_hand_3.tgs @@ -0,0 +1 @@ +{"v":"5.7.6","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"hand_3 BIG 1x","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"NULL CONTROL","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256.989,275.127,0],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":183,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"head","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[0,-37.327,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":11,"s":[0,-27.027,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":23,"s":[0.383,-39.627,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":35,"s":[0.383,-33.606,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":49,"s":[0.383,-39.627,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":63,"s":[0.383,-33.606,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":79,"s":[3.183,-39.627,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.15,"y":1},"o":{"x":0.167,"y":0},"t":93,"s":[3.583,-33.606,0],"to":[0,0,0],"ti":[0,0,0]},{"t":124,"s":[0,-37.327,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":5,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":11,"s":[105,95,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":16,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":23,"s":[100,100,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":29,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":35,"s":[105,95,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":41,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":49,"s":[100,100,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":55,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":63,"s":[105,95,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":70,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":79,"s":[100,100,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":85,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":93,"s":[105,95,100]},{"i":{"x":[0.15,0.15,0.15],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":105,"s":[97,103,100]},{"t":124,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-6.341,0],[0,-6.341],[6.341,0],[0,6.341]],"o":[[6.341,0],[0,6.341],[-6.341,0],[0,-6.341]],"v":[[0,-11.482],[11.482,0],[0,11.482],[-11.482,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":184,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"hands","parent":6,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.16],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":21,"s":[0]},{"t":118,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[-2.341,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[-2.341,-9.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":21,"s":[-1.541,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":33,"s":[-1.541,-15.377,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":47,"s":[-1.541,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":61,"s":[-1.541,-15.377,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":78,"s":[-1.541,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.16,"y":1},"o":{"x":0.5,"y":0},"t":102,"s":[-1.541,-15.377,0],"to":[0,0,0],"ti":[0,0,0]},{"t":118,"s":[-2.341,-17.777,0]}],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[20,20,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":184,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"hands 3","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,115,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],"o":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],"v":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":10,"s":[{"i":[[0,0],[-0.71,-5.814],[-13.22,1.67],[-6.667,-0.441]],"o":[[-3.232,5.731],[0.566,4.637],[3.994,-0.505],[1.183,0.078]],"v":[[-18.219,-21.464],[-27.079,-2.224],[-7.644,3.584],[11.49,2.929]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":23,"s":[{"i":[[0,0],[-1.268,-5.758],[-4.491,-2.782],[-9.089,1.417]],"o":[[-0.238,6.778],[1.347,6.12],[4.565,2.828],[1.172,-0.183]],"v":[[-19.257,-37.475],[-19.531,-14.871],[-9.165,2.078],[11.49,3.527]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":35,"s":[{"i":[[0,0],[-0.961,-3.853],[-6.666,-1.601],[-9.089,1.417]],"o":[[0.03,7.313],[1.517,6.08],[5.221,1.254],[1.172,-0.183]],"v":[[-23.457,-27.675],[-24.527,-6.059],[-7.961,2.838],[11.49,2.929]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":49,"s":[{"i":[[0,0],[-1.268,-5.758],[-4.491,-2.782],[-9.089,1.417]],"o":[[-0.238,6.778],[1.347,6.12],[4.565,2.828],[1.172,-0.183]],"v":[[-19.257,-37.475],[-19.531,-14.871],[-9.165,2.078],[11.49,3.527]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.7,"y":0},"t":59,"s":[{"i":[[0,0],[-0.961,-3.853],[-6.666,-1.601],[-9.116,1.293]],"o":[[0.03,7.313],[1.517,6.08],[5.221,1.254],[1.174,-0.167]],"v":[[-23.457,-27.675],[-24.527,-6.059],[-7.961,2.838],[11.49,2.929]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":65,"s":[{"i":[[0,0],[-0.651,-3.886],[-6.545,-1.827],[-9.086,1.105]],"o":[[4.595,6.728],[1.028,6.132],[5.027,1.403],[1.177,-0.143]],"v":[[-25.036,-29.511],[-22.232,-9.273],[-7.789,2.781],[11.545,2.978]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":73,"s":[{"i":[[0,0],[0.125,-3.969],[-6.039,-3.086],[-8.715,1.918]],"o":[[-2.798,7.352],[-0.197,6.263],[4.981,2.545],[1.158,-0.255]],"v":[[-9.536,-38.346],[-16.487,-17.314],[-7.359,2.636],[11.683,3.1]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":79,"s":[{"i":[[0,0],[-0.447,-3.908],[-4.757,-3.608],[-9.817,2.3]],"o":[[-4.119,5.691],[0.705,6.167],[4.297,3.185],[1.155,-0.271]],"v":[[-10.167,-36.731],[-16.027,-16.023],[-7.359,2.636],[12.29,3.029]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":85,"s":[{"i":[[0,0],[-0.961,-3.853],[-4.491,-2.782],[-7.117,1.703]],"o":[[1.597,6.857],[1.517,6.08],[4.565,2.828],[1.153,-0.276]],"v":[[-20.247,-37.336],[-17.926,-14.859],[-7.359,2.636],[11.59,3.229]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":90,"s":[{"i":[[0,0],[-0.511,-3.901],[-4.724,-3.504],[-8.017,1.106]],"o":[[2.238,7.884],[0.807,6.156],[4.331,3.14],[1.175,-0.162]],"v":[[-18.139,-37.479],[-16.265,-15.877],[-7.667,2.047],[11.49,2.929]],"c":false}]},{"i":{"x":0.1,"y":1},"o":{"x":0.167,"y":0},"t":96,"s":[{"i":[[0,0],[0.125,-3.969],[-6.039,-3.086],[-9.123,0.867]],"o":[[-2.798,7.352],[-0.197,6.263],[4.981,2.545],[1.181,-0.112]],"v":[[-9.536,-38.346],[-16.487,-17.314],[-7.559,2.136],[11.683,3.1]],"c":false}]},{"t":137,"s":[{"i":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],"o":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],"v":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4.8,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":184,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"hands 2","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,115,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],"o":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],"v":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":10,"s":[{"i":[[-2.832,-0.073],[-2.303,-0.066],[-1.926,-3.085],[-0.239,-5.607]],"o":[[2.794,0.072],[11.894,0.339],[2.109,3.377],[0,0]],"v":[[2.763,2.93],[11.216,2.961],[28.065,7.836],[30.818,22.809]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":23,"s":[{"i":[[-2.832,0.111],[-2.264,0.431],[-1.371,7.044],[-0.143,3.51]],"o":[[2.794,-0.11],[8.983,-1.711],[1.168,-6],[0,0]],"v":[[2.891,4.486],[10.849,3.71],[25.852,-14.597],[24.804,-37.956]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":35,"s":[{"i":[[-2.832,0.111],[-2.272,0.384],[-1.192,6.245],[-0.009,3.513]],"o":[[2.794,-0.11],[9.419,-1.594],[1.146,-6.005],[0,0]],"v":[[3.091,3.837],[10.849,3.012],[29.365,-6.408],[27.224,-29.702]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":49,"s":[{"i":[[-2.832,0.111],[-2.264,0.431],[-1.371,7.044],[-0.143,3.51]],"o":[[2.794,-0.11],[8.983,-1.711],[1.168,-6],[0,0]],"v":[[2.891,4.486],[10.849,3.71],[25.852,-14.597],[24.804,-37.956]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.7,"y":0},"t":63,"s":[{"i":[[-2.832,0.111],[-2.272,0.384],[-1.192,6.245],[-0.009,3.513]],"o":[[2.794,-0.11],[9.419,-1.594],[1.146,-6.005],[0,0]],"v":[[3.115,3.787],[10.849,3.012],[29.365,-6.408],[27.224,-29.702]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":68,"s":[{"i":[[-2.832,0.111],[-2.329,0.428],[-1.207,6.31],[-0.019,3.512]],"o":[[2.794,-0.11],[9.405,-1.697],[1.148,-6.004],[0,0]],"v":[[3.081,4.077],[10.905,3.036],[29.081,-7.069],[27.028,-30.369]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":81,"s":[{"i":[[-2.832,0.111],[-2.976,0.923],[-1.371,7.044],[-0.143,3.51]],"o":[[2.794,-0.11],[9.238,-2.866],[1.168,-6],[0,0]],"v":[[2.091,4.836],[11.549,3.31],[25.852,-14.597],[24.804,-37.956]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.7,"y":0},"t":95,"s":[{"i":[[-2.832,0.111],[-2.272,0.384],[-1.192,6.245],[-0.009,3.513]],"o":[[2.794,-0.11],[9.419,-1.594],[1.146,-6.005],[0,0]],"v":[[3.091,3.837],[10.849,3.012],[29.365,-6.408],[27.224,-29.702]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":104,"s":[{"i":[[-2.831,0.08],[-2.289,0.181],[-7.091,2.705],[-0.08,0.435]],"o":[[2.753,-0.078],[9.55,-0.749],[7.091,-2.705],[0,0]],"v":[[2.94,3.427],[10.784,3.015],[28.399,6.452],[28.599,-12.123]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":108,"s":[{"i":[[-2.826,0.181],[-2.296,0.098],[-2.884,-0.516],[0.064,0.955]],"o":[[2.87,-0.184],[9.603,-0.406],[2.056,-0.027],[0,0]],"v":[[2.852,3.381],[10.755,2.875],[28.007,11.666],[29.663,9.28]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":109,"s":[{"i":[[-2.829,0.086],[-2.298,0.077],[-0.947,-0.492],[-0.646,-0.097]],"o":[[2.923,-0.087],[9.617,-0.321],[0.947,0.492],[0,0]],"v":[[2.841,3.319],[10.792,2.896],[27.909,12.969],[29.9,13.784]],"c":false}]},{"i":{"x":0.1,"y":1},"o":{"x":0.167,"y":0.167},"t":110,"s":[{"i":[[-2.832,-0.009],[-2.3,0.057],[-0.78,-1.215],[-0.945,-1.801]],"o":[[2.976,0.009],[9.63,-0.235],[0.78,1.215],[0,0]],"v":[[2.83,3.257],[10.73,2.954],[27.812,14.272],[30.136,18.289]],"c":false}]},{"t":134,"s":[{"i":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],"o":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],"v":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4.8,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":184,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"body","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.31,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[116.171,151.524,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.31,"y":1},"o":{"x":0.69,"y":0},"t":21,"s":[116.171,140.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.31,"y":1},"o":{"x":0.69,"y":0},"t":33,"s":[116.171,150.324,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.31,"y":1},"o":{"x":0.69,"y":0},"t":47,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.31,"y":1},"o":{"x":0.69,"y":0},"t":61,"s":[116.171,150.324,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.31,"y":1},"o":{"x":0.69,"y":0},"t":78,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.1,"y":1},"o":{"x":0.69,"y":0},"t":102,"s":[116.171,150.324,0],"to":[0,0,0],"ti":[0,0,0]},{"t":133,"s":[116.171,146.724,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,14.211,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":5,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.484,0.484,0.484],"y":[0,0,0]},"t":10,"s":[100,100,100]},{"i":{"x":[0.31,0.31,0.31],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":15,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.563,0.563,0.563],"y":[0,0,0]},"t":21,"s":[100,100,100]},{"i":{"x":[0.219,0.219,0.219],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":27,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.406,0.406,0.406],"y":[0,0,0]},"t":33,"s":[100,100,100]},{"i":{"x":[0.233,0.233,0.233],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":40,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.69,0.69,0.69],"y":[0,0,0]},"t":47,"s":[100,100,100]},{"i":{"x":[0.31,0.31,0.31],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":54,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.495,0.495,0.495],"y":[0,0,0]},"t":61,"s":[100,100,100]},{"i":{"x":[0.203,0.203,0.203],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":67,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.69,0.69,0.69],"y":[0,0,0]},"t":78,"s":[100,100,100]},{"i":{"x":[0.31,0.31,0.31],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":89,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.495,0.495,0.495],"y":[0,0,0]},"t":102,"s":[100,100,100]},{"i":{"x":[0.1,0.1,0.1],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":117,"s":[103,97,100]},{"t":133,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[{"i":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0],[0,0]],"o":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0],[0,0]],"v":[[-12.432,-14.575],[-11.977,8.464],[-8.487,13.499],[0.4,14.598],[9.223,13.566],[12.656,8.149],[12.482,-13.225],[0.089,-13.897]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[{"i":[[0,0],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0,0],[0,0]],"o":[[0,0],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,0],[0,0]],"v":[[-13.432,-6.25],[-13.127,7.863],[-9.825,12.562],[0.35,14.211],[10.398,12.616],[13.744,7.749],[13.432,-6.171],[0.069,-6.21]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.5,"y":0},"t":21,"s":[{"i":[[0,0],[0,0],[-1.772,-0.83],[-2.763,0],[-2.314,1.063],[-0.081,2.131],[0,0],[4.6,0.1]],"o":[[0,0],[0.132,2.059],[2.348,1.099],[2.717,0],[1.837,-0.844],[0,0],[0,0],[-4.6,-0.1]],"v":[[-12.732,-16.711],[-11.415,7.863],[-8.318,12.562],[0.348,14.211],[8.894,12.616],[12.032,7.749],[12.732,-14.111],[0.032,-12.939]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":33,"s":[{"i":[[0,0],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0,0],[0,0]],"o":[[0,0],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,0],[0,0]],"v":[[-13.832,-12.011],[-12.127,7.863],[-8.825,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[13.832,-12.011],[0.071,-12.011]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.5,"y":0},"t":47,"s":[{"i":[[0,0],[0,0],[-1.772,-0.83],[-2.763,0],[-2.314,1.063],[-0.081,2.131],[0,0],[0,0]],"o":[[0,0],[0.132,2.059],[2.348,1.099],[2.717,0],[1.837,-0.844],[0,0],[0,0],[0,0]],"v":[[-12.732,-16.711],[-11.415,7.863],[-8.318,12.562],[0.348,14.211],[8.894,12.616],[12.032,7.749],[12.732,-14.111],[0.065,-13.504]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":61,"s":[{"i":[[0,0],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0,0],[0,0]],"o":[[0,0],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,0],[0,0]],"v":[[-13.832,-12.011],[-12.127,7.863],[-8.825,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[13.832,-12.011],[0.071,-12.011]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":68,"s":[{"i":[[0.583,-2.853],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-1.681,3.721],[9.547,0.72]],"o":[[-1.162,5.686],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.172,-0.387],[-6.293,-0.475]],"v":[[-11.685,-13.446],[-12.103,7.863],[-8.8,12.562],[0.35,14.211],[9.374,12.616],[12.719,7.749],[13.832,-14.267],[-0.931,-12.07]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":78,"s":[{"i":[[0.583,-2.853],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-1.202,3.902],[10.545,0.088]],"o":[[-1.162,5.686],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.108,-0.363],[-6.244,-0.052]],"v":[[-11.402,-16.846],[-12.127,7.863],[-8.825,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[13.815,-16.611],[-0.813,-13.327]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":90,"s":[{"i":[[0.279,-1.426],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-0.575,1.951],[6.396,0.322]],"o":[[-0.556,2.843],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.052,-0.181],[-8.294,-0.418]],"v":[[-11.944,-17.745],[-12.154,7.863],[-8.852,12.562],[0.35,14.211],[9.425,12.616],[12.771,7.749],[13.386,-13.461],[-1.432,-12.505]],"c":false}]},{"i":{"x":0.1,"y":1},"o":{"x":0.167,"y":0},"t":102,"s":[{"i":[[0,0],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0,0],[0,0]],"o":[[0,0],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,0],[0,0]],"v":[[-12.832,-15.411],[-12.127,7.863],[-8.825,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[12.932,-11.911],[-1.184,-11.652]],"c":false}]},{"t":133,"s":[{"i":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0],[0,0]],"o":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0],[0,0]],"v":[[-12.432,-14.575],[-11.977,8.464],[-8.487,13.499],[0.4,14.598],[9.223,13.566],[12.656,8.149],[12.482,-13.225],[0.089,-13.897]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":184,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"hand_3","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[49,47.75,0],"ix":2,"l":2},"a":{"a":0,"k":[256,256,0],"ix":1,"l":2},"s":{"a":0,"k":[11.4,11.4,100],"ix":6,"l":2}},"ao":0,"w":512,"h":512,"ip":0,"op":180,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/tgs/voice_chat_raise_hand_4.tgs b/Telegram-Mac/tgs/voice_chat_raise_hand_4.tgs new file mode 100644 index 0000000000..ffc156a46d --- /dev/null +++ b/Telegram-Mac/tgs/voice_chat_raise_hand_4.tgs @@ -0,0 +1 @@ +{"v":"5.7.6","fr":60,"ip":0,"op":120,"w":100,"h":100,"nm":"hand_4 BIG 1x","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"NULL CONTROL","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256.989,275.127,0],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":119,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"head","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":1,"s":[0]},{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":11,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":23,"s":[-13]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0]},"t":66,"s":[-13]},{"i":{"x":[0.09],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":75,"s":[7]},{"t":92,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":1,"s":[0,-37.327,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":11,"s":[0,-26.827,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":23,"s":[-4.4,-39.427,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":36,"s":[-2.401,-30.424,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":50,"s":[-4.4,-39.427,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.09,"y":1},"o":{"x":0.167,"y":0},"t":66,"s":[-2.401,-30.424,0],"to":[0,0,0],"ti":[0,0,0]},{"t":92,"s":[0,-37.327,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":1,"s":[100,100,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":5,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":11,"s":[105,95,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":16,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":23,"s":[105,95,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":28,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":36,"s":[100,100,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":44,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":50,"s":[105,95,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":58,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":66,"s":[100,100,100]},{"i":{"x":[0.09,0.09,0.09],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":75,"s":[95,105,100]},{"t":92,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-6.341,0],[0,-6.341],[6.341,0],[0,6.341]],"o":[[6.341,0],[0,6.341],[-6.341,0],[0,-6.341]],"v":[[0,-11.482],[11.482,0],[0,11.482],[-11.482,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"hands","parent":6,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":21,"s":[-10]},{"i":{"x":[0.17],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":65,"s":[-10]},{"t":93,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[-2.341,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[-2.341,-9.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":21,"s":[-3.741,-17.977,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":35,"s":[-1.541,-11.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.5,"y":0},"t":49,"s":[-1.541,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.17,"y":1},"o":{"x":0.6,"y":0},"t":65,"s":[-1.541,-11.777,0],"to":[0,0,0],"ti":[0,0,0]},{"t":93,"s":[-2.341,-17.777,0]}],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[20,20,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"hands 3","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,115,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],"o":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],"v":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.6,"y":0},"t":10,"s":[{"i":[[0,0],[-0.71,-5.814],[-13.22,1.67],[-6.659,-0.445]],"o":[[-3.232,5.731],[0.566,4.637],[3.994,-0.505],[1.183,0.079]],"v":[[-18.219,-21.464],[-27.079,-2.224],[-7.644,3.484],[11.49,2.929]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[0,0],[-1.902,-5.873],[-9.833,-0.14],[-7.372,0.373]],"o":[[-4.328,1.758],[1.817,5.11],[4.766,0.217],[1.182,0.009]],"v":[[-17.041,-20.099],[-26.446,-5.406],[-7.078,3.541],[9.615,2.763]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":23,"s":[{"i":[[0,0],[-4.205,-5.986],[-9.957,-1.926],[-4.458,0.474]],"o":[[5.753,4.944],[4.232,6.025],[3.773,0.73],[1.179,-0.125]],"v":[[-37.604,-27.175],[-25.224,-11.553],[-5.844,2.761],[7.055,3.069]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":27,"s":[{"i":[[0,0],[-1.728,-7.979],[-9.811,-4.073],[-4.458,0.474]],"o":[[3.018,5.449],[1.024,4.728],[4.574,1.385],[1.179,-0.125]],"v":[[-30.389,-31.532],[-24.556,-12.392],[-6.174,2.474],[7.055,3.069]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":32,"s":[{"i":[[0,0],[-0.133,-5.267],[-9.077,-3.752],[-4.458,0.474]],"o":[[-4.804,4.722],[0.193,7.66],[5.825,2.408],[1.179,-0.125]],"v":[[-10.804,-33.837],[-18.834,-15.906],[-6.69,2.025],[7.055,3.069]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":37,"s":[{"i":[[0,0],[-0.271,-4.621],[-9.517,-2.839],[-4.458,0.474]],"o":[[-3.394,4.297],[0.242,4.13],[4.799,1.569],[1.179,-0.125]],"v":[[-15.907,-30.468],[-22.029,-13.729],[-6.267,2.393],[7.055,3.069]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":42,"s":[{"i":[[0,0],[-3.928,-6.171],[-9.883,-2.273],[-3.875,0.81]],"o":[[5.522,5.2],[3.954,6.211],[4.837,1.112],[1.161,-0.243]],"v":[[-36.207,-29.267],[-24.551,-13.099],[-5.745,2.464],[7.055,3.069]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":48,"s":[{"i":[[0,0],[-1.728,-7.979],[-9.811,-4.073],[-4.458,0.474]],"o":[[3.018,5.449],[1.024,4.728],[4.574,1.385],[1.179,-0.125]],"v":[[-30.389,-31.532],[-24.556,-12.392],[-6.174,2.474],[7.055,3.069]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":54,"s":[{"i":[[0,0],[-0.133,-5.267],[-9.319,-3.103],[-4.568,0.674]],"o":[[-4.804,4.722],[0.193,7.66],[5.735,1.909],[1.173,-0.173]],"v":[[-10.804,-33.837],[-18.834,-15.906],[-6.656,1.832],[7.072,2.972]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":61,"s":[{"i":[[0,0],[-0.271,-4.621],[-9.517,-2.839],[-4.458,0.474]],"o":[[-3.394,4.297],[0.242,4.13],[4.799,1.569],[1.179,-0.125]],"v":[[-15.907,-30.468],[-22.029,-13.729],[-6.267,2.393],[7.055,3.069]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":68,"s":[{"i":[[0,0],[-4.205,-5.986],[-9.957,-1.926],[-4.458,0.474]],"o":[[5.753,4.944],[4.232,6.025],[3.773,0.73],[1.179,-0.125]],"v":[[-37.604,-27.175],[-25.224,-11.553],[-5.844,2.761],[7.055,3.069]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":78,"s":[{"i":[[0,0],[-3.924,-6.477],[-10.111,-0.781],[-3.392,0.105]],"o":[[4.365,3.67],[3.808,6.286],[4.491,0.347],[1.183,-0.028]],"v":[[-37.672,-26.337],[-25.271,-10.97],[-5.844,2.761],[6.463,3.033]],"c":false}]},{"t":95,"s":[{"i":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],"o":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],"v":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4.8,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"hands 2","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,115,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],"o":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],"v":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":10,"s":[{"i":[[-2.831,0.099],[-2.303,-0.066],[-1.926,-3.085],[-0.239,-5.607]],"o":[[3.206,-0.113],[11.894,0.339],[2.109,3.377],[0,0]],"v":[[2.767,2.88],[11.324,2.869],[28.065,7.836],[30.818,22.809]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":22,"s":[{"i":[[-2.833,0.019],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],"o":[[4.963,-0.034],[5.323,2.097],[1.94,4.605],[0,0]],"v":[[2.632,3.384],[12.827,2.873],[22.174,19.291],[30.152,32.241]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":36,"s":[{"i":[[-2.83,-0.135],[-3.536,-1.048],[-1.592,-3.476],[-1.09,-6.235]],"o":[[7.035,0.337],[5.485,1.626],[1.67,3.647],[0,0]],"v":[[1.498,3.488],[12.683,2.542],[23.022,15.874],[26.398,31.323]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":50,"s":[{"i":[[-2.829,0.15],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],"o":[[4.712,-0.25],[5.323,2.097],[1.94,4.605],[0,0]],"v":[[2.417,3.447],[12.827,2.873],[22.174,19.291],[30.152,32.241]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.4,"y":0},"t":66,"s":[{"i":[[-2.833,0.023],[-3.532,-0.206],[-2.149,-3.438],[-1.064,-6.535]],"o":[[4.317,-0.036],[4.623,0.27],[2.442,3.906],[0,0]],"v":[[1.983,3.325],[12.861,3.014],[29.432,13.325],[34.61,28.122]],"c":false}]},{"t":90,"s":[{"i":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],"o":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],"v":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4.8,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"body","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[116.171,151.524,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":21,"s":[116.171,140.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":35,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.5,"y":0},"t":49,"s":[116.171,143.924,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.17,"y":0.17},"o":{"x":0.6,"y":0.6},"t":65,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"t":93,"s":[116.171,146.724,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,14.211,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":5,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.484,0.484,0.484],"y":[0,0,0]},"t":10,"s":[100,100,100]},{"i":{"x":[0.486,0.486,0.486],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":15,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":21,"s":[100,100,100]},{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":27,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":35,"s":[100,100,100]},{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":42,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":49,"s":[100,100,100]},{"i":{"x":[0.4,0.4,0.4],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":56,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":65,"s":[100,100,100]},{"i":{"x":[0.17,0.17,0.17],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":77,"s":[97,103,100]},{"t":93,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[{"i":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0],[0,0]],"o":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0],[0,0]],"v":[[-12.432,-14.575],[-11.977,8.464],[-8.487,13.499],[0.4,14.598],[9.223,13.566],[12.656,8.149],[12.482,-13.225],[-1.232,-13.968]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[{"i":[[0,0],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0,0],[7.2,-1.2]],"o":[[0,0],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,0],[-7.2,1.2]],"v":[[-14.032,-4.05],[-13.127,7.863],[-9.825,12.562],[0.35,14.211],[10.398,12.616],[13.744,7.749],[14.232,-7.571],[-0.968,-7.539]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.5,"y":0},"t":21,"s":[{"i":[[-0.869,-2.458],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[1.3,6.472],[11.956,-3.973]],"o":[[1.863,5.271],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[-0.119,-0.593],[-5.848,1.943]],"v":[[-14.632,-13.411],[-12.127,7.863],[-8.825,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[11.232,-16.011],[-3.825,-14.466]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":35,"s":[{"i":[[-0.896,-2.536],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[2.094,6.249],[8.562,0.638]],"o":[[1.922,5.439],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,-0.172],[-6.543,-0.487]],"v":[[-15.346,-10.627],[-12.527,7.863],[-9.225,12.562],[0.35,14.211],[9.798,12.616],[13.144,7.749],[11.974,-10.116],[-6.43,-8.177]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":42,"s":[{"i":[[-0.878,-2.484],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[1.559,6.399],[11.133,-0.108]],"o":[[1.882,5.326],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[-0.105,-0.49],[-6.188,0.06]],"v":[[-14.481,-12.569],[-12.098,7.863],[-8.796,12.562],[0.35,14.211],[9.369,12.616],[12.715,7.749],[11.484,-13.946],[-3.724,-11.3]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":49,"s":[{"i":[[-0.869,-2.458],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[1.3,6.472],[11.176,-3.111]],"o":[[1.863,5.271],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[-0.119,-0.593],[-6.043,1.682]],"v":[[-14.732,-16.111],[-12.127,7.863],[-8.825,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[11.532,-15.511],[-3.745,-13.528]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":54,"s":[{"i":[[-0.873,-2.469],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[1.413,6.44],[7.166,0.651]],"o":[[1.872,5.295],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[-0.102,-0.533],[-6.261,-0.569]],"v":[[-14.525,-15.487],[-12.348,7.863],[-9.046,12.562],[0.35,14.211],[9.619,12.616],[12.964,7.749],[11.758,-14.741],[-3.318,-12.791]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":57,"s":[{"i":[[-0.878,-2.484],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[1.559,6.399],[7.228,0.668]],"o":[[1.882,5.326],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,0.146],[-6.315,-0.584]],"v":[[-14.816,-12.663],[-12.153,7.863],[-8.85,12.562],[0.35,14.211],[9.424,12.616],[12.769,7.749],[11.588,-12.58],[-3.901,-10.75]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":65,"s":[{"i":[[-0.896,-2.536],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[2.094,6.249],[11.969,-4.333]],"o":[[1.922,5.439],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,-0.172],[-6.23,2.255]],"v":[[-15.524,-9.227],[-12.727,7.863],[-9.425,12.562],[0.35,14.211],[9.998,12.616],[13.344,7.749],[12.174,-10.116],[-3.838,-7.806]],"c":false}]},{"i":{"x":0.17,"y":1},"o":{"x":0.167,"y":0.167},"t":73,"s":[{"i":[[-0.39,-1.104],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0.912,2.72],[13.551,-2.732]],"o":[[0.837,2.368],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,-0.075],[-6.34,1.278]],"v":[[-14.493,-10.166],[-12.749,7.863],[-9.446,12.562],[0.35,14.211],[10.02,12.616],[13.365,7.749],[12.988,-9.89],[-1.256,-9.949]],"c":false}]},{"t":93,"s":[{"i":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0],[0,0]],"o":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0],[0,0]],"v":[[-12.432,-14.575],[-11.977,8.464],[-8.487,13.499],[0.4,14.598],[9.223,13.566],[12.656,8.149],[12.482,-13.225],[-1.232,-13.968]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"hand_4","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[49,47.75,0],"ix":2,"l":2},"a":{"a":0,"k":[256,256,0],"ix":1,"l":2},"s":{"a":0,"k":[11.4,11.4,100],"ix":6,"l":2}},"ao":0,"w":512,"h":512,"ip":0,"op":120,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/tgs/voice_chat_raise_hand_5.tgs b/Telegram-Mac/tgs/voice_chat_raise_hand_5.tgs new file mode 100644 index 0000000000..ef54aa0932 --- /dev/null +++ b/Telegram-Mac/tgs/voice_chat_raise_hand_5.tgs @@ -0,0 +1 @@ +{"v":"5.7.6","fr":60,"ip":0,"op":240,"w":100,"h":100,"nm":"hand_5 BIG 1x","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"NULL CONTROL","parent":6,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":30,"s":[6.742,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":36,"s":[5.537,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":43,"s":[6.742,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":52,"s":[5.537,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":68,"s":[6.742,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":87,"s":[5.537,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.13,"y":1},"o":{"x":0.174,"y":0},"t":115,"s":[6.742,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.34,"y":1},"o":{"x":0.54,"y":0},"t":157,"s":[6.742,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.54,"y":0},"t":168,"s":[6.742,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":172,"s":[1.942,-42.622,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":178,"s":[13.542,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":184,"s":[9.142,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.29,"y":1},"o":{"x":0.167,"y":0},"t":190,"s":[9.542,-39.422,0],"to":[0,0,0],"ti":[0,0,0]},{"t":199,"s":[6.742,-39.422,0]}],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[20,20,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"head","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.781],"y":[1]},"o":{"x":[0.412],"y":[0]},"t":1,"s":[0]},{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":11,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":30,"s":[21]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":54,"s":[21]},{"i":{"x":[0.13],"y":[1]},"o":{"x":[0.811],"y":[0]},"t":71,"s":[21]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.54],"y":[0]},"t":157,"s":[0.516]},{"i":{"x":[0.34],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":162,"s":[61.167]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.54],"y":[0]},"t":169,"s":[0.516]},{"i":{"x":[0.637],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":177,"s":[-21.833]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":183,"s":[0]},{"t":218,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.396,"y":1},"o":{"x":0.683,"y":0},"t":1,"s":[81.289,125.475,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":11,"s":[81.289,176.385,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":30,"s":[115.289,114.475,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.487,"y":0},"t":54,"s":[115.289,148.475,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.7,"y":0},"t":71,"s":[115.289,141.475,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":97,"s":[143.289,239.475,0],"to":[0,0,0],"ti":[0.667,1.333,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":110,"s":[139.289,231.475,0],"to":[-0.667,-1.333,0],"ti":[0,0,0]},{"i":{"x":0.13,"y":1},"o":{"x":0.181,"y":0},"t":117,"s":[139.289,231.475,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.34,"y":1},"o":{"x":0.54,"y":0},"t":157,"s":[150.289,249.475,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.54,"y":0},"t":169,"s":[163.289,257.475,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":183,"s":[70.289,86.475,0],"to":[0,0,0],"ti":[0,0,0]},{"t":218,"s":[81.289,125.475,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":1,"s":[500,500,100]},{"i":{"x":[0.425,0.425,0.425],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":5,"s":[485,515,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":11,"s":[500,500,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":16,"s":[520,484,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":30,"s":[500,500,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":54,"s":[500,500,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.7,0.7,0.7],"y":[0,0,0]},"t":71,"s":[500,500,100]},{"i":{"x":[0.13,0.13,0.13],"y":[1,1,1]},"o":{"x":[0.176,0.176,0.176],"y":[0,0,0]},"t":97,"s":[500,500,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.54,0.54,0.54],"y":[0,0,0]},"t":157,"s":[500,500,100]},{"i":{"x":[0.34,0.34,0.34],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":162,"s":[477,515,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.54,0.54,0.54],"y":[0,0,0]},"t":169,"s":[500,500,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":175,"s":[469,533,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":183,"s":[485,515,100]},{"i":{"x":[0.2,0.2,0.2],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":198,"s":[527,484,100]},{"t":218,"s":[500,500,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":0,"s":[{"i":[[-6.341,0],[0,-6.341],[6.341,0],[0,6.341]],"o":[[6.341,0],[0,6.341],[-6.341,0],[0,-6.341]],"v":[[0,-11.482],[11.482,0],[0,11.482],[-11.482,0]],"c":true}]},{"i":{"x":0.13,"y":1},"o":{"x":0.167,"y":0},"t":117,"s":[{"i":[[-6.395,0],[0,-6.395],[6.395,0],[0,6.395]],"o":[[6.395,0],[0,6.395],[-6.395,0],[0,-6.395]],"v":[[0,-11.579],[11.579,0],[0,11.579],[-11.579,0]],"c":true}]},{"i":{"x":0.34,"y":1},"o":{"x":0.54,"y":0},"t":157,"s":[{"i":[[-6.395,0],[0,-6.395],[6.395,0],[0,6.395]],"o":[[6.395,0],[0,6.395],[-6.395,0],[0,-6.395]],"v":[[0,-11.579],[11.579,0],[0,11.579],[-11.579,0]],"c":true}]},{"i":{"x":0.2,"y":1},"o":{"x":0.54,"y":0},"t":169,"s":[{"i":[[-6.395,0],[0,-6.395],[6.395,0],[0,6.395]],"o":[[6.395,0],[0,6.395],[-6.395,0],[0,-6.395]],"v":[[0,-11.579],[11.579,0],[0,11.579],[-11.579,0]],"c":true}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":183,"s":[{"i":[[-6.395,0],[0,-6.395],[6.395,0],[0,6.395]],"o":[[6.395,0],[0,6.395],[-6.395,0],[0,-6.395]],"v":[[0,-11.579],[11.579,0],[0,11.579],[-11.579,0]],"c":true}]},{"t":218,"s":[{"i":[[-6.395,0],[0,-6.395],[6.395,0],[0,6.395]],"o":[[6.395,0],[0,6.395],[-6.395,0],[0,-6.395]],"v":[[0,-11.579],[11.579,0],[0,11.579],[-11.579,0]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"hands","parent":6,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":28,"s":[8]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.487],"y":[0]},"t":49,"s":[8]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":66,"s":[8]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":95,"s":[15]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":172,"s":[15]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":183,"s":[0]},{"t":215,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.538,"y":1},"o":{"x":0.525,"y":0},"t":0,"s":[-2.341,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[-2.341,-12.256,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":0.3},"o":{"x":0.167,"y":0.167},"t":28,"s":[-1.541,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.487,"y":0},"t":49,"s":[-1.541,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.5,"y":0},"t":66,"s":[-1.541,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":0.2},"o":{"x":0.5,"y":0.5},"t":95,"s":[1.459,-10.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.6,"y":0},"t":172,"s":[1.459,-10.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":183,"s":[-2.341,-24.177,0],"to":[0,0,0],"ti":[0,0,0]},{"t":215,"s":[-2.341,-17.777,0]}],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[20,20,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"hands 3","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,115,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],"o":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],"v":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.6,"y":0},"t":10,"s":[{"i":[[0,0],[-1.37,-5.996],[-12.799,0.954],[-5.623,-0.366]],"o":[[-2.582,5.43],[1.226,5.005],[4.037,-0.259],[1.184,0.071]],"v":[[-21.167,-23.136],[-27.242,-4.081],[-7.372,3.34],[9.979,3.037]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.5,"y":0},"t":24,"s":[{"i":[[0,0],[-0.633,-3.903],[-5.394,-1.449],[-8.926,0.862]],"o":[[1.012,6.936],[0.998,6.158],[5.589,1.532],[1.18,-0.114]],"v":[[-18.065,-33.976],[-19.359,-12.251],[-7.484,2.834],[8.267,2.627]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":30,"s":[{"i":[[0,0],[-1.834,-3.522],[-4.71,-2.732],[-8.926,0.862]],"o":[[4.406,7.624],[2.894,5.558],[5.028,2.917],[1.18,-0.114]],"v":[[-29.396,-35.058],[-19.39,-14.698],[-8.387,2.178],[8.267,2.627]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":37,"s":[{"i":[[0,0],[-0.633,-3.903],[-5.394,-1.449],[-8.926,0.862]],"o":[[1.012,6.936],[0.998,6.158],[5.589,1.532],[1.18,-0.114]],"v":[[-18.065,-33.976],[-19.359,-12.251],[-7.484,2.834],[8.267,2.627]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":46,"s":[{"i":[[0,0],[-0.961,-3.853],[-4.984,-1.753],[-8.926,0.862]],"o":[[1.597,6.857],[1.517,6.08],[5.484,1.929],[1.18,-0.114]],"v":[[-23.166,-35.108],[-20.844,-12.632],[-7.906,2.713],[8.267,2.627]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":62,"s":[{"i":[[0,0],[-0.633,-3.903],[-5.394,-1.449],[-8.926,0.862]],"o":[[1.012,6.936],[0.998,6.158],[5.589,1.532],[1.18,-0.114]],"v":[[-18.065,-33.976],[-19.359,-12.251],[-7.484,2.834],[8.371,3.018]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":83,"s":[{"i":[[0,0],[-0.961,-3.853],[-4.906,-1.962],[-7.467,0.538]],"o":[[1.597,6.857],[1.517,6.08],[4.479,1.791],[1.183,-0.085]],"v":[[-23.166,-35.108],[-20.844,-12.632],[-7.906,2.713],[8.484,3.913]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":110,"s":[{"i":[[0,0],[-0.26,-3.59],[-3.384,-0.79],[-7.192,0.386]],"o":[[0.455,6.368],[0.41,5.665],[5.588,1.254],[1.184,-0.062]],"v":[[-19.285,-31.287],[-20.744,-10.988],[-7.332,2.936],[7.831,4.696]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":159,"s":[{"i":[[0,0],[-0.365,-3.581],[-5.898,-2.25],[-7.859,-0.922]],"o":[[0.643,6.352],[0.576,5.651],[5.898,2.25],[1.177,0.138]],"v":[[-19.77,-28.6],[-20.63,-8.268],[-7.332,2.936],[8.229,6.66]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":170,"s":[{"i":[[0,0],[-0.365,-3.581],[-5.898,-2.25],[-7.859,-0.922]],"o":[[0.643,6.352],[0.576,5.651],[5.898,2.25],[1.177,0.138]],"v":[[-17.58,-28.152],[-18.943,-8.927],[-7.087,3.077],[8.71,7.1]],"c":false}]},{"i":{"x":0.33,"y":1},"o":{"x":0.167,"y":0.167},"t":171,"s":[{"i":[[0,0],[-0.365,-3.581],[-5.898,-2.25],[-7.859,-0.922]],"o":[[0.643,6.352],[0.576,5.651],[5.898,2.25],[1.177,0.138]],"v":[[-17.58,-28.152],[-18.943,-8.927],[-7.087,3.077],[8.683,7.001]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.6,"y":0},"t":172,"s":[{"i":[[0,0],[-0.365,-3.581],[-5.898,-2.25],[-7.859,-0.922]],"o":[[0.643,6.352],[0.576,5.651],[5.898,2.25],[1.177,0.138]],"v":[[-17.58,-28.152],[-18.943,-8.927],[-7.087,3.077],[8.658,6.907]],"c":false}]},{"i":{"x":0.33,"y":1},"o":{"x":0.167,"y":0.167},"t":177,"s":[{"i":[[0,0],[-0.54,-4.745],[-7.23,-2.167],[-5.398,-0.759]],"o":[[-4.605,4.425],[0.657,5.889],[5.467,1.861],[1.174,0.151]],"v":[[-13.923,-29.232],[-18.91,-12.686],[-7.227,2.305],[7.477,5.457]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.6,"y":0},"t":183,"s":[{"i":[[0,0],[-0.757,-6.182],[-8.875,-2.065],[-4.049,-0.033]],"o":[[1.716,5.76],[0.757,6.182],[5.804,0.802],[1.183,0.01]],"v":[[-20.544,-37.522],[-16.784,-18.145],[-6.731,2.836],[6.768,3.871]],"c":false}]},{"i":{"x":0.22,"y":1},"o":{"x":0.167,"y":0.167},"t":197,"s":[{"i":[[0,0],[-1.231,-6.629],[-10.082,-1.99],[-3.033,-0.134]],"o":[[-2.133,4.972],[1.242,6.689],[4.542,1.029],[1.183,0.052]],"v":[[-21.494,-33.181],[-22.796,-14.58],[-6.487,2.795],[6.539,3.624]],"c":false}]},{"t":230,"s":[{"i":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],"o":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],"v":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4.8,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"hands 2","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,115,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],"o":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],"v":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":10,"s":[{"i":[[-2.886,-0.073],[-2.347,-0.053],[-2.059,-3.86],[-0.222,-4.947]],"o":[[2.847,0.072],[11.634,0.272],[2.05,3.678],[0,0]],"v":[[2.805,2.87],[11.318,3.002],[28.486,9.829],[31.187,24.214]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":29,"s":[{"i":[[-2.805,0.395],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],"o":[[4.23,-0.595],[5.323,2.097],[1.94,4.605],[0,0]],"v":[[2.696,3.339],[12.911,2.962],[23.56,19.096],[31.538,32.046]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.4,"y":0},"t":49,"s":[{"i":[[-2.805,0.396],[-2.168,-0.779],[-1.2,-3.73],[-2.259,-5.223]],"o":[[3.592,-0.507],[6.085,2.162],[1.511,4.741],[0,0]],"v":[[2.591,3.352],[12.843,2.769],[22.316,18.867],[27.587,32.67]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":56,"s":[{"i":[[-2.809,0.363],[-2.164,-0.79],[-1.265,-3.731],[-2.492,-5.262]],"o":[[3.71,-0.472],[5.954,2.15],[1.585,4.718],[0,0]],"v":[[2.587,3.473],[12.901,3.159],[22.53,18.907],[28.267,32.563]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.6,"y":0},"t":71,"s":[{"i":[[-2.826,0.202],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],"o":[[4.274,-0.305],[5.323,2.097],[1.94,4.605],[0,0]],"v":[[2.755,3.732],[13.24,3.821],[23.56,19.096],[31.538,32.046]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":77,"s":[{"i":[[-3.178,0.215],[-2.041,-0.845],[-2.499,-1.941],[-4.429,-3.642]],"o":[[4.522,-0.369],[5.066,2.097],[2.711,2.446],[0,0]],"v":[[2.574,4.136],[12.86,4.148],[22.716,18.561],[31.303,23.734]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":83,"s":[{"i":[[-3.695,-0.034],[-1.897,-0.845],[-3.783,0.554],[-5.561,-1.128]],"o":[[4.929,-0.055],[4.71,2.097],[3.783,-0.554],[0,0]],"v":[[2.262,4.258],[12.224,3.929],[21.543,17.816],[30.975,12.182]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":89,"s":[{"i":[[-3.987,-0.175],[-1.816,-0.845],[-2.558,0.538],[-4.267,2.804]],"o":[[4.392,0.151],[4.508,2.097],[4.029,-0.892],[0,0]],"v":[[0.773,4.323],[12.105,4.679],[20.881,17.396],[23.392,2.697]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":97,"s":[{"i":[[-4.068,-0.214],[-1.793,-0.845],[-2.221,0.534],[-3.91,3.887]],"o":[[3.583,0.188],[4.452,2.097],[4.097,-0.985],[0,0]],"v":[[1.673,4.518],[12.072,4.886],[20.698,17.28],[21.304,0.085]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":110,"s":[{"i":[[-4.068,-0.214],[-1.793,-0.845],[-2.221,0.534],[-1.866,5.72]],"o":[[3.583,0.188],[4.452,2.097],[4.097,-0.985],[0,0]],"v":[[1.647,4.421],[11.286,4.268],[20.698,17.28],[20.124,-0.841]],"c":false}]},{"i":{"x":0.13,"y":1},"o":{"x":0.167,"y":0},"t":117,"s":[{"i":[[-4.068,-0.214],[-1.793,-0.845],[-2.221,0.534],[-1.866,5.72]],"o":[[3.583,0.188],[4.452,2.097],[4.097,-0.985],[0,0]],"v":[[1.647,4.421],[11.286,4.268],[20.698,17.28],[20.124,-0.841]],"c":false}]},{"i":{"x":0.47,"y":1},"o":{"x":0.54,"y":0},"t":159,"s":[{"i":[[-3.882,-1.235],[-1.793,-0.845],[-2.236,0.469],[-6.732,3.4]],"o":[[6.962,2.214],[4.452,2.097],[6.913,-1.449],[0,0]],"v":[[-2.326,4.761],[11.274,7.998],[20.698,17.28],[22.603,3.774]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.53,"y":0},"t":170,"s":[{"i":[[-3.882,-1.235],[-1.793,-0.845],[-3.475,0.534],[-2.888,6.054]],"o":[[6.962,2.214],[4.452,2.097],[6.411,-0.985],[0,0]],"v":[[-2.326,4.761],[12.099,7.984],[24.802,16.49],[25.028,3.271]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[{"i":[[-3.711,-1.045],[-1.877,-0.707],[-3.424,-0.312],[-3.136,3.726]],"o":[[6.283,1.865],[4.802,1.755],[5.881,-0.065],[0,0]],"v":[[-1.494,4.593],[11.879,7.297],[24.69,16.195],[35.961,7.352]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":175,"s":[{"i":[[-3.606,-0.928],[-1.929,-0.622],[-3.392,-0.834],[-3.289,2.291]],"o":[[5.864,1.649],[5.017,1.544],[5.553,0.502],[0,0]],"v":[[-1.022,4.755],[11.744,6.874],[24.62,16.013],[36.214,11.333]],"c":false}]},{"i":{"x":0.33,"y":1},"o":{"x":0.167,"y":0.167},"t":177,"s":[{"i":[[-3.396,-0.694],[-2.033,-0.452],[-3.328,-1.877],[-3.596,-0.579]],"o":[[5.027,1.218],[5.448,1.122],[4.899,1.636],[0,0]],"v":[[0.045,4.282],[11.473,6.026],[24.482,15.649],[36.718,19.295]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.6,"y":0},"t":186,"s":[{"i":[[-2.838,-0.073],[-2.309,0],[-3.159,-4.652],[-4.411,-8.209]],"o":[[2.799,0.072],[6.593,0],[3.159,4.652],[0,0]],"v":[[2.773,3.73],[10.753,3.773],[24.114,14.681],[34.584,32.542]],"c":false}]},{"t":222,"s":[{"i":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],"o":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],"v":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4.8,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"body","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.538,"y":1},"o":{"x":0.525,"y":0},"t":0,"s":[262.842,433.748,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[262.842,450.324,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":28,"s":[262.842,403.748,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":0.3},"o":{"x":0.167,"y":0.167},"t":49,"s":[262.842,433.748,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":0.2},"o":{"x":0.167,"y":0.167},"t":66,"s":[262.842,433.748,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":0.2},"o":{"x":0.6,"y":0.6},"t":172,"s":[262.842,433.748,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":0.2},"o":{"x":0.167,"y":0.167},"t":183,"s":[262.842,433.748,0],"to":[0,0,0],"ti":[0,0,0]},{"t":215,"s":[262.842,433.748,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,14.211,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":0,"s":[500,500,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":3,"s":[515,485,100]},{"i":{"x":[0.566,0.566,0.566],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":5,"s":[485,515,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.484,0.484,0.484],"y":[0,0,0]},"t":10,"s":[489.464,510.536,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":15,"s":[515,485,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":28,"s":[500,500,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":37,"s":[485,515,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":49,"s":[500,500,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":57,"s":[515,485,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":66,"s":[500,500,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":78,"s":[485,515,100]},{"i":{"x":[0.2,0.2,0.2],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":95,"s":[500,500,100]},{"i":{"x":[0.2,0.2,0.2],"y":[1,1,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":172,"s":[500,500,100]},{"i":{"x":[0.2,0.2,0.2],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":183,"s":[500,500,100]},{"t":215,"s":[500,500,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[{"i":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0],[0,0]],"o":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0],[0,0]],"v":[[-12.432,-14.575],[-11.977,8.464],[-8.487,13.499],[0.4,14.598],[9.223,13.566],[12.656,8.149],[12.482,-13.225],[-0.568,-13.539]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[{"i":[[0,0],[0,0],[-1.991,-0.83],[-3.105,0],[-2.6,1.063],[-0.091,2.131],[0,0],[0,0]],"o":[[0,0],[0.148,2.059],[2.638,1.099],[3.053,0],[2.064,-0.844],[0,0],[0,0],[0,0]],"v":[[-14.565,-8.333],[-13.375,7.863],[-9.896,12.562],[0.353,14.211],[10.467,12.616],[13.992,7.749],[14.426,-7.239],[-1.077,-7.824]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":24,"s":[{"i":[[0.449,-5.259],[0,0],[-1.748,-0.83],[-2.726,0],[-2.283,1.063],[-0.08,2.131],[-1.162,6.43],[11.513,-1.298]],"o":[[-0.818,9.587],[0.13,2.059],[2.317,1.099],[2.681,0],[1.812,-0.844],[0,0],[0.022,-0.089],[-6.391,0.721]],"v":[[-11.292,-16.725],[-11.425,7.863],[-8.369,12.562],[0.392,14.211],[9.036,12.616],[12.131,7.749],[13.652,-10.268],[-1.301,-13.893]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0.449,-5.259],[0,0],[-1.748,-0.83],[-2.726,0],[-2.283,1.063],[-0.08,2.131],[-1.162,6.43],[9.781,-1.613]],"o":[[-0.818,9.587],[0.13,2.059],[2.317,1.099],[2.681,0],[1.812,-0.844],[0,0],[0.022,-0.089],[-6.342,1.046]],"v":[[-11.74,-17.039],[-11.432,7.863],[-8.377,12.562],[0.392,14.211],[9.044,12.616],[12.139,7.749],[13.308,-10.843],[0.096,-13.359]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":37,"s":[{"i":[[0.449,-5.259],[0,0],[-1.748,-0.83],[-2.726,0],[-2.283,1.063],[-0.08,2.131],[-1.162,6.43],[12.327,-2.172]],"o":[[-0.818,9.587],[0.13,2.059],[2.317,1.099],[2.681,0],[1.812,-0.844],[0,0],[0.022,-0.089],[-6.334,1.116]],"v":[[-11.292,-16.725],[-11.425,7.863],[-8.369,12.562],[0.392,14.211],[9.036,12.616],[12.131,7.749],[13.652,-10.268],[-0.542,-14.411]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":46,"s":[{"i":[[0.449,-5.259],[0,0],[-1.748,-0.83],[-2.726,0],[-2.283,1.063],[-0.08,2.131],[-1.162,6.43],[10.392,-1.531]],"o":[[-0.818,9.587],[0.13,2.059],[2.317,1.099],[2.681,0],[1.812,-0.844],[0,0],[0.022,-0.089],[-6.359,0.937]],"v":[[-11.74,-17.039],[-11.432,7.863],[-8.377,12.562],[0.392,14.211],[9.044,12.616],[12.139,7.749],[13.308,-10.843],[-0.508,-14.557]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":57,"s":[{"i":[[0.449,-5.259],[0,0],[-1.748,-0.83],[-2.726,0],[-2.283,1.063],[-0.08,2.131],[-1.162,6.43],[8.85,-2.482]],"o":[[-0.818,9.587],[0.13,2.059],[2.317,1.099],[2.681,0],[1.812,-0.844],[0,0],[0.022,-0.089],[-6.192,1.737]],"v":[[-11.292,-16.725],[-11.425,7.863],[-8.369,12.562],[0.392,14.211],[9.036,12.616],[12.131,7.749],[13.652,-10.268],[-1.052,-13.771]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.5,"y":0},"t":66,"s":[{"i":[[0.449,-5.259],[0,0],[-1.748,-0.83],[-2.726,0],[-2.283,1.063],[-0.08,2.131],[-1.162,6.43],[11.735,-1.603]],"o":[[-0.818,9.587],[0.13,2.059],[2.317,1.099],[2.681,0],[1.812,-0.844],[0,0],[0.022,-0.089],[-6.369,0.87]],"v":[[-11.74,-17.039],[-11.432,7.863],[-8.377,12.562],[0.392,14.211],[9.044,12.616],[12.139,7.749],[13.308,-10.843],[-2.504,-13.736]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.5,"y":0},"t":95,"s":[{"i":[[3.782,-3.682],[-0.296,-1.817],[-2.373,-1.147],[-2.726,0.008],[-2.179,1.263],[-0.336,1.409],[-1.022,3.53],[4.879,3.507]],"o":[[-7.574,7.373],[0.296,1.817],[2.309,1.116],[2.681,-0.008],[2.218,-1.285],[0.336,-1.409],[0.022,-0.089],[-4.446,-3.196]],"v":[[-11.795,-13.512],[-17.064,5.317],[-13.796,10.808],[-2.427,12.362],[5.214,10.746],[8.296,5.87],[10.454,-3.469],[-3.724,-7.524]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.6,"y":0},"t":172,"s":[{"i":[[3.782,-3.682],[-0.296,-1.817],[-2.373,-1.147],[-2.726,0.008],[-2.179,1.263],[-0.336,1.409],[-1.022,3.53],[4.879,3.507]],"o":[[-7.574,7.373],[0.296,1.817],[2.309,1.116],[2.681,-0.008],[2.218,-1.285],[0.336,-1.409],[0.022,-0.089],[-4.446,-3.196]],"v":[[-11.795,-13.512],[-17.064,5.317],[-13.796,10.808],[-2.427,12.362],[5.214,10.746],[8.296,5.87],[10.079,-2.644],[-4.324,-7.524]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":183,"s":[{"i":[[0,0],[0,0],[-1.638,-0.83],[-2.555,0],[-2.139,1.063],[-0.075,2.131],[0,0],[0,0]],"o":[[0,0],[0.122,2.059],[2.171,1.099],[2.512,0],[1.698,-0.844],[0,0],[0,0],[0,0]],"v":[[-11.432,-21.961],[-10.647,7.863],[-7.784,12.562],[0.304,14.211],[8.281,12.616],[11.182,7.749],[11.432,-19.411],[-0.795,-20.774]],"c":false}]},{"t":215,"s":[{"i":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0],[0,0]],"o":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0],[0,0]],"v":[[-12.432,-14.575],[-11.977,8.464],[-8.487,13.499],[0.4,14.598],[9.223,13.566],[12.656,8.149],[12.482,-13.225],[-0.568,-13.539]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":244,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"hand_5","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[49,47.75,0],"ix":2,"l":2},"a":{"a":0,"k":[256,256,0],"ix":1,"l":2},"s":{"a":0,"k":[11.4,11.4,100],"ix":6,"l":2}},"ao":0,"w":512,"h":512,"ip":0,"op":240,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/tgs/voice_chat_raise_hand_6.tgs b/Telegram-Mac/tgs/voice_chat_raise_hand_6.tgs new file mode 100644 index 0000000000..17b6680d49 --- /dev/null +++ b/Telegram-Mac/tgs/voice_chat_raise_hand_6.tgs @@ -0,0 +1 @@ +{"v":"5.7.6","fr":60,"ip":0,"op":240,"w":100,"h":100,"nm":"hand_6 BIG 1x","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"NULL CONTROL","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256.989,275.127,0],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":243,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"head","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":1,"s":[0]},{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":11,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":23.514,"s":[21]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.17],"y":[0]},"t":159,"s":[21]},{"i":{"x":[0.836],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":169,"s":[-18]},{"i":{"x":[0.14],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":181,"s":[0]},{"t":202,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":1,"s":[122.706,17.15,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":11,"s":[122.706,68.999,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.589,"y":1},"o":{"x":0.275,"y":0},"t":23.514,"s":[150.706,5.999,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.206,"y":0},"t":36.148,"s":[150.706,32.05,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.29,"y":1},"o":{"x":0.295,"y":0},"t":55.52,"s":[150.706,27.67,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.71,"y":0.684},"o":{"x":0.75,"y":0},"t":72,"s":[150.706,31.062,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.205,"y":0.943},"o":{"x":0.097,"y":0.337},"t":100,"s":[158.75,127.141,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.205,"y":0.901},"o":{"x":0.328,"y":0.041},"t":123,"s":[140.564,110.895,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.205,"y":0.205},"o":{"x":0.328,"y":0.328},"t":147,"s":[148.731,122.942,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.71,"y":0},"t":159,"s":[148.731,122.942,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.14,"y":1},"o":{"x":0.167,"y":0},"t":181,"s":[122.706,1.199,0],"to":[0,0,0],"ti":[0,0,0]},{"t":202,"s":[122.706,17.15,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":1,"s":[500,500,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":5,"s":[485,515,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":11,"s":[525,475,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":16,"s":[485,515,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":23.514,"s":[500,500,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":36,"s":[500,500,100]},{"i":{"x":[0.605,0.605,0.605],"y":[1,1,1]},"o":{"x":[0.709,0.709,0.709],"y":[0,0,0]},"t":55.52,"s":[500,500,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":72,"s":[500,500,100]},{"i":{"x":[0.605,0.605,0.605],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":84,"s":[485,515,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.195,0.195,0.195],"y":[0,0,0]},"t":100,"s":[531,465,100]},{"i":{"x":[0.666,0.666,0.666],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":111,"s":[508,482,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":123,"s":[531,465,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":147,"s":[531,465,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.71,0.71,0.71],"y":[0,0,0]},"t":159,"s":[531,465,100]},{"i":{"x":[0.2,0.2,0.2],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":169,"s":[485,515,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":181,"s":[500,500,100]},{"i":{"x":[0.14,0.14,0.14],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":190,"s":[515,485,100]},{"t":202,"s":[500,500,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-6.341,0],[0,-6.341],[6.341,0],[0,6.341]],"o":[[6.341,0],[0,6.341],[-6.341,0],[0,-6.341]],"v":[[0,-11.482],[11.482,0],[0,11.482],[-11.482,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"NULL CONTROL","parent":7,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-1.541,-17.777,0],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[20,20,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":3,"nm":"hands","parent":3,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":23.514,"s":[8]},{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.71],"y":[0]},"t":167,"s":[8]},{"t":187,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[111,115,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[111,155,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":23.514,"s":[115,115,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":32.309,"s":[115,121,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":48.244,"s":[115,115,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":0.5},"o":{"x":0.167,"y":0.167},"t":73.717,"s":[115,121,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":0.5},"o":{"x":0.167,"y":0.167},"t":104,"s":[115,121,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.71,"y":0},"t":167,"s":[115,121,0],"to":[0,0,0],"ti":[0,0,0]},{"t":187,"s":[111,115,0]}],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"hands 3","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,115,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],"o":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],"v":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.6,"y":0},"t":10,"s":[{"i":[[0,0],[-0.71,-5.814],[-13.22,1.67],[-6.666,-0.138]],"o":[[-3.232,5.731],[0.566,4.637],[3.994,-0.505],[1.186,0.024]],"v":[[-18.219,-21.464],[-27.079,-2.224],[-7.644,3.484],[11.485,2.829]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":23.514,"s":[{"i":[[0,0],[-0.961,-3.853],[-4.491,-2.782],[-8.28,0.661]],"o":[[1.597,6.857],[1.517,6.08],[4.565,2.828],[1.182,-0.094]],"v":[[-20.247,-37.336],[-17.926,-14.859],[-7.359,2.636],[10.724,2.534]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":27.281,"s":[{"i":[[0,0],[-0.511,-3.901],[-4.724,-3.504],[-8.291,0.526]],"o":[[2.238,7.884],[0.807,6.156],[4.331,3.14],[1.184,-0.075]],"v":[[-18.139,-37.479],[-16.265,-15.877],[-7.359,2.636],[10.941,2.657]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":32.309,"s":[{"i":[[0,0],[0.125,-3.969],[-5.052,-4.524],[-8.435,1.023]],"o":[[-2.798,7.352],[-0.197,6.263],[4,3.582],[1.177,-0.143]],"v":[[-6.969,-38.346],[-13.919,-17.314],[-7.359,2.636],[9.48,3.009]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":39.988,"s":[{"i":[[0,0],[-0.447,-3.908],[-4.757,-3.608],[-8.213,0.625]],"o":[[-4.119,5.691],[0.705,6.167],[4.297,3.185],[1.182,-0.09]],"v":[[-10.167,-36.731],[-16.027,-16.023],[-7.359,2.636],[11.49,2.929]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":48,"s":[{"i":[[0,0],[-0.961,-3.853],[-4.491,-2.782],[-8.28,0.661]],"o":[[1.597,6.857],[1.517,6.08],[4.565,2.828],[1.182,-0.094]],"v":[[-20.247,-37.336],[-17.926,-14.859],[-7.359,2.636],[10.724,2.534]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":59,"s":[{"i":[[0,0],[-0.745,-3.894],[-5.61,-1.137],[-7.557,0.15]],"o":[[0.04,7.276],[1.176,6.145],[4.685,1.226],[2.785,0.43]],"v":[[-21.221,-29.776],[-21.735,-7.592],[-7.832,2.454],[9.503,3.125]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.7,"y":0},"t":77,"s":[{"i":[[0,0],[-0.534,-3.934],[-6.701,0.467],[-7.857,-1.7]],"o":[[-1.477,7.685],[0.843,6.208],[4.802,-0.335],[4.346,0.94]],"v":[[-22.17,-22.408],[-25.448,-0.509],[-8.293,2.277],[8.34,3.892]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":83,"s":[{"i":[[0,0],[-1.283,-3.822],[-6.716,0.378],[-7.766,-1.789]],"o":[[-1.159,7.502],[1.533,5.663],[4.777,-0.271],[4.347,1.005]],"v":[[-23.938,-21.192],[-25.341,0.043],[-8.232,2.315],[8.497,4.584]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":89,"s":[{"i":[[0,0],[-4.461,-3.345],[-6.782,-0.001],[-7.382,-2.168]],"o":[[0.19,6.726],[4.461,3.345],[4.673,0.001],[4.35,1.277]],"v":[[-31.448,-16.026],[-24.888,2.391],[-7.974,2.476],[8.356,5.481]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":95,"s":[{"i":[[0,0],[-5.006,-0.871],[-6.739,0.618],[-3.595,-1.191]],"o":[[3.138,5.078],[7.808,0.677],[3.051,-0.206],[4.086,1.935]],"v":[[-35.589,-2.901],[-23.582,6.559],[-7.395,2.829],[5.349,5.214]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":96,"s":[{"i":[[0,0],[-5.097,-0.459],[-6.732,0.721],[-5.917,-2.018]],"o":[[3.63,4.803],[8.366,0.233],[2.78,-0.24],[4.048,2.025]],"v":[[-36.279,-0.714],[-23.364,7.254],[-7.298,2.887],[3.95,4.848]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":98,"s":[{"i":[[0,0],[-5.278,0.366],[-6.718,0.928],[-1.571,-0.06]],"o":[[4.612,4.253],[9.482,-0.657],[2.24,-0.309],[3.971,2.206]],"v":[[-37.66,3.661],[-22.928,8.643],[-7.105,3.005],[-2.464,2.943]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":102,"s":[{"i":[[0,0],[-3.051,3.557],[-6.774,-0.323],[-1.497,-0.855]],"o":[[6.234,-0.011],[3.051,-3.557],[1.059,0.05],[4.488,-0.165]],"v":[[-32.128,21.461],[-18.515,15.867],[-6.941,3.526],[-1.608,4.799]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":104,"s":[{"i":[[0,0],[-1.97,4.904],[-6.781,-0.025],[-1.558,-0.596]],"o":[[5.985,-2.499],[1.97,-4.904],[0.507,0.073],[1.002,-1.057]],"v":[[-26.349,30.241],[-16.309,19.479],[-6.859,3.786],[-1.357,4.478]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":106,"s":[{"i":[[0,0],[-0.723,4.978],[-6.358,0.441],[-1.354,-0.39]],"o":[[3.449,-4.195],[0.723,-4.978],[0.651,-0.017],[0.811,0.234]],"v":[[-21.092,35.493],[-14.798,22.142],[-6.816,3.923],[-2.441,4.593]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":116,"s":[{"i":[[0,0],[0.27,3.962],[-5.659,-0.43],[0,0]],"o":[[-2.38,-4.193],[-0.461,-6.764],[0.813,0.105],[0,0]],"v":[[-6.789,40.46],[-11.955,22.079],[-6.74,4.165],[-3.354,4.867]],"c":false}]},{"i":{"x":0.56,"y":1},"o":{"x":0.167,"y":0},"t":132,"s":[{"i":[[0,0],[0.028,3.971],[-6.776,0.271],[0,0]],"o":[[0.92,-6.575],[-0.044,-6.266],[0.437,0.042],[0,0]],"v":[[-14.554,41.248],[-13.978,22.06],[-6.74,4.165],[-4.202,4.326]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.44,"y":0},"t":149,"s":[{"i":[[0,0],[0.028,3.971],[-6.696,-1.076],[0,0]],"o":[[-1.06,-6.297],[-0.044,-6.266],[0.486,0.034],[0,0]],"v":[[-11.825,41.269],[-13.468,22.09],[-6.74,4.165],[-4.523,4.686]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":163,"s":[{"i":[[0,0],[0.028,3.971],[-6.696,-1.076],[0,0]],"o":[[-1.06,-6.297],[-0.044,-6.266],[0.486,0.034],[0,0]],"v":[[-11.825,41.269],[-13.468,22.09],[-6.74,4.165],[-4.523,4.686]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":167,"s":[{"i":[[0,0],[0.028,3.971],[-6.774,0.312],[0.114,0.062]],"o":[[-1.06,-6.297],[-0.044,-6.266],[0.175,-0.024],[0.128,0.161]],"v":[[-11.825,41.269],[-13.468,22.09],[-6.74,4.165],[-3.2,4.045]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":168,"s":[{"i":[[0,0],[-0.332,3.858],[-6.615,-0.065],[-6.227,-2.487]],"o":[[-1.197,-5.882],[0.411,-5.901],[4.645,0.044],[4.032,1.605]],"v":[[-13.447,39.061],[-15.196,20.338],[-6.713,4.082],[8.845,6.116]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":171,"s":[{"i":[[0,0],[-2.006,3.333],[-5.838,-0.364],[-5.143,-1.039]],"o":[[-1.832,-3.948],[2.53,-4.202],[4.517,0.245],[3.33,0.673]],"v":[[-20.998,28.78],[-23.239,12.181],[-6.64,3.301],[7.921,5.13]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":172,"s":[{"i":[[0,0],[-3.833,2.656],[-5.544,-0.477],[-4.639,-1.02]],"o":[[-0.634,-3.479],[4.095,-3.091],[4.469,0.321],[2.949,0.736]],"v":[[-27.21,21.956],[-23.39,8.31],[-6.628,2.86],[7.594,4.819]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":173,"s":[{"i":[[0,0],[-5.661,1.98],[-5.25,-0.59],[-4.136,-1]],"o":[[0.565,-3.01],[5.66,-1.98],[4.421,0.397],[2.568,0.798]],"v":[[-33.422,15.133],[-23.541,4.439],[-6.54,3.01],[7.266,4.508]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":174,"s":[{"i":[[0,0],[-9.318,0.062],[-4.956,-0.704],[-4.288,-0.672]],"o":[[-0.353,0.511],[6.341,-0.042],[4.372,0.473],[2.252,0.625]],"v":[[-39.783,7.131],[-23.543,1.746],[-6.473,2.566],[6.939,4.148]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":175,"s":[{"i":[[0,0],[-7.373,-0.661],[-5.987,-0.908],[-3.473,-0.494]],"o":[[4.936,-1.112],[5.389,0.674],[4.345,0.516],[2.073,0.526]],"v":[[-41.655,1.633],[-24.332,-0.148],[-6.488,2.688],[6.748,3.903]],"c":false}]},{"i":{"x":0.1,"y":1},"o":{"x":0.167,"y":0.167},"t":177,"s":[{"i":[[0,0],[-3.484,-2.106],[-8.05,-1.318],[-3.2,-0.264]],"o":[[2.562,1.927],[3.484,2.106],[4.29,0.602],[1.716,0.329]],"v":[[-36.386,-11.472],[-25.909,-3.937],[-6.358,2.513],[6.464,3.595]],"c":false}]},{"t":195,"s":[{"i":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],"o":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],"v":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4.8,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"hands 2","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,115,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],"o":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],"v":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.6,"y":0},"t":10,"s":[{"i":[[-2.832,-0.073],[-2.303,-0.066],[-1.926,-3.085],[-0.239,-5.607]],"o":[[2.794,0.072],[11.894,0.339],[2.109,3.377],[0,0]],"v":[[2.758,2.83],[11.206,2.761],[28.065,7.836],[30.818,22.809]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":23.514,"s":[{"i":[[-2.816,0.314],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],"o":[[4.14,-0.462],[5.323,2.097],[1.94,4.605],[0,0]],"v":[[2.456,3.576],[12.827,2.873],[22.174,19.291],[30.152,32.241]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":34,"s":[{"i":[[-2.805,0.396],[-2.168,-0.779],[-1.2,-3.73],[-2.259,-5.223]],"o":[[3.592,-0.507],[6.085,2.162],[1.511,4.741],[0,0]],"v":[[2.725,4.04],[12.869,3.17],[22.316,18.867],[27.587,32.67]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":48.244,"s":[{"i":[[-2.816,0.314],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],"o":[[4.14,-0.462],[5.323,2.097],[1.94,4.605],[0,0]],"v":[[2.456,3.576],[12.827,2.873],[22.174,19.291],[30.152,32.241]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":59,"s":[{"i":[[-2.816,0.149],[-2.272,-0.949],[-1.475,-3.735],[-3.255,-5.39]],"o":[[4.388,-0.192],[5.461,2.271],[1.826,4.641],[0,0]],"v":[[2.315,3.354],[12.923,3.555],[22.3,19.809],[29.56,32.985]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":77,"s":[{"i":[[-2.816,-0.309],[-2.628,-1.238],[-1.2,-3.73],[-2.259,-5.223]],"o":[[5.074,0.557],[5.841,2.753],[1.511,4.741],[0,0]],"v":[[1.925,2.738],[13.189,5.447],[22.65,21.244],[27.921,35.047]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":95,"s":[{"i":[[-0.598,-0.181],[-0.539,-0.561],[-0.196,-2.606],[1.687,-4.266]],"o":[[4.741,1.436],[3.565,3.814],[0.23,3.032],[0,0]],"v":[[5.057,5.135],[15.215,9.739],[20.248,24.608],[18.597,38.923]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":96,"s":[{"i":[[-0.457,-0.364],[-0.52,-0.555],[-0.187,-2.596],[1.723,-4.257]],"o":[[3.097,0.541],[3.544,3.823],[0.218,3.016],[0,0]],"v":[[9.643,6.899],[15.234,9.778],[20.225,24.639],[18.511,38.959]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":98,"s":[{"i":[[-0.369,-0.478],[-0.508,-0.551],[-0.181,-2.589],[1.746,-4.251]],"o":[[0.369,0.484],[3.531,3.83],[0.21,3.006],[0,0]],"v":[[14.421,8.736],[15.245,9.803],[20.211,24.659],[18.457,38.982]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":100,"s":[{"i":[[0,0],[-0.074,-0.046],[-0.361,-3.217],[1.172,-4.139]],"o":[[0,0],[3.532,3.723],[0.33,3.294],[0,0]],"v":[[15.242,9.767],[15.359,9.86],[20.37,24.574],[19.626,38.792]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":102,"s":[{"i":[[-0.412,-0.138],[-0.72,-1.127],[-0.593,-4.024],[0.433,-3.996]],"o":[[0.167,0.033],[3.533,3.586],[0.484,3.664],[0,0]],"v":[[12.062,7.641],[15.505,9.932],[20.575,24.466],[21.13,38.547]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":116,"s":[{"i":[[0,0],[-1.009,-0.802],[-1.352,-6.671],[-1.989,-3.524]],"o":[[0,0],[4.03,3.256],[0.988,4.877],[0,0]],"v":[[11.232,6.776],[15.063,8.718],[21.245,24.11],[26.059,37.745]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":124,"s":[{"i":[[0,0],[-1.097,-1.149],[-0.799,-5.031],[-0.839,-3.94]],"o":[[0,0],[3.609,3.446],[0.71,4.75],[0,0]],"v":[[10.966,6.155],[15.063,8.718],[20.372,24.232],[22.707,38.343]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":137,"s":[{"i":[[0,0],[-0.388,-0.392],[-0.475,-4.068],[-0.163,-4.185]],"o":[[0,0],[3.092,3.858],[0.546,4.676],[0,0]],"v":[[14.961,8.53],[15.063,8.718],[19.859,24.304],[20.739,38.695]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":157,"s":[{"i":[[0,0],[-0.58,-0.466],[-1.352,-6.671],[-0.912,-4.483]],"o":[[0,0],[3.124,3.255],[0.988,4.877],[0,0]],"v":[[14.98,9.235],[15.19,9.407],[20.156,24.263],[23.242,38.545]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":165,"s":[{"i":[[-3.102,-0.816],[-1.57,-1.341],[-1.352,-6.671],[-0.912,-4.483]],"o":[[0.548,0.101],[3.658,3.223],[0.988,4.877],[0,0]],"v":[[12.399,7.597],[15.19,9.407],[20.156,24.263],[23.242,38.545]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":167,"s":[{"i":[[-3.663,-0.964],[-1.749,-1.499],[-1.352,-6.671],[-0.912,-4.483]],"o":[[0.647,0.119],[3.754,3.217],[0.988,4.877],[0,0]],"v":[[8.772,6.017],[15.19,9.407],[20.156,24.263],[23.242,38.545]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":168,"s":[{"i":[[-3.718,-0.617],[-1.769,-1.459],[-1.384,-6.679],[-0.892,-4.426]],"o":[[6.299,1.053],[3.934,3.132],[1.01,4.878],[0,0]],"v":[[1.967,4.432],[15.1,9.259],[20.419,24.094],[23.49,38.317]],"c":false}]},{"t":189,"s":[{"i":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],"o":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],"v":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4.8,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":244,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"body","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[116.171,151.524,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.723,"y":1},"o":{"x":0.395,"y":0},"t":21,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.361,"y":0},"t":32.309,"s":[116.171,147.924,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.723,"y":1},"o":{"x":0.395,"y":0},"t":48.244,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.723,"y":0.723},"o":{"x":0.167,"y":0.167},"t":73.717,"s":[116.171,147.924,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.2,"y":1},"o":{"x":0.71,"y":0},"t":167,"s":[116.171,147.924,0],"to":[0,0,0],"ti":[0,0,0]},{"t":187,"s":[116.171,146.724,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,14.211,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":5,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.484,0.484,0.484],"y":[0,0,0]},"t":10,"s":[100,100,100]},{"i":{"x":[0.486,0.486,0.486],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":15,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":21,"s":[100,100,100]},{"i":{"x":[0.486,0.486,0.486],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":26.025,"s":[98,102,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":32.309,"s":[100,100,100]},{"i":{"x":[0.486,0.486,0.486],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":39.988,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.48,0.48,0.48],"y":[0,0,0]},"t":48.244,"s":[100,100,100]},{"i":{"x":[0.471,0.471,0.471],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":59.158,"s":[98,102,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":73.717,"s":[100,100,100]},{"i":{"x":[0.471,0.471,0.471],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":89,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":104,"s":[100,100,100]},{"i":{"x":[0.471,0.471,0.471],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":114,"s":[98,102,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":133,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.71,0.71,0.71],"y":[0,0,0]},"t":167,"s":[100,100,100]},{"i":{"x":[0.2,0.2,0.2],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":175,"s":[98,102,100]},{"t":187,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[{"i":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0],[6.8,-0.7]],"o":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0],[-6.8,0.7]],"v":[[-12.432,-14.575],[-11.977,8.464],[-8.487,13.499],[0.4,14.598],[9.223,13.566],[12.656,8.149],[12.482,-13.225],[-0.668,-13.639]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[{"i":[[0,0],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0,0],[0,0]],"o":[[0,0],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,0],[0,0]],"v":[[-14.632,-6.25],[-13.927,7.863],[-10.625,12.562],[0.35,14.211],[11.198,12.616],[14.544,7.749],[14.632,-5.171],[-0.698,-6.255]],"c":false}]},{"i":{"x":0.833,"y":0.913},"o":{"x":0.5,"y":0},"t":24,"s":[{"i":[[0.95,-5.149],[-0.221,-1.818],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-2.532,4.395],[11.988,-1.713]],"o":[[-1.585,8.587],[0.221,1.818],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.013,-0.168],[-6.408,0.916]],"v":[[-11.288,-18.4],[-12.135,7.863],[-8.833,12.562],[0.35,14.211],[9.406,12.616],[12.752,7.749],[13.837,-10.133],[-0.465,-13.854]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.087},"t":32,"s":[{"i":[[1.497,-5.016],[-0.338,-1.852],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-3.171,3.957],[9.918,-2.158]],"o":[[-2.669,8.935],[0.338,1.852],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.041,-0.077],[-6.425,1.398]],"v":[[-11.587,-18.638],[-12.157,7.863],[-8.855,12.562],[0.35,14.211],[9.428,12.616],[12.773,7.749],[14.172,-9.711],[-0.086,-11.777]],"c":false}]},{"i":{"x":0.833,"y":0.954},"o":{"x":0.5,"y":0},"t":48,"s":[{"i":[[0.95,-5.149],[-0.209,-1.898],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-2.532,4.395],[9.275,-2.03]],"o":[[-1.585,8.587],[0.209,1.898],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.013,-0.168],[-6.261,1.37]],"v":[[-11.08,-17.817],[-12.127,7.863],[-8.825,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[13.83,-9.727],[0.556,-13.41]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":77,"s":[{"i":[[1.514,-5.012],[-0.305,-1.276],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[-0.275,6.886],[10.509,2.653]],"o":[[-2.702,8.946],[0.305,1.276],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0.042,-0.074],[-6.476,-1.635]],"v":[[-11.404,-15.827],[-12.123,7.863],[-8.82,12.562],[0.35,14.211],[9.394,12.616],[12.739,7.749],[14.048,-5.48],[0.675,-13.902]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":95,"s":[{"i":[[2.669,-4.491],[-0.274,-1.374],[-1.915,-1.173],[-2.813,0],[-2.356,1.063],[-0.75,2.369],[-0.131,5.096],[6.672,1.621]],"o":[[-5.397,9.039],[0.274,1.374],[1.958,1.192],[2.767,0],[1.87,-0.844],[0.743,-2.219],[0.042,-0.074],[-5.979,-1.453]],"v":[[-10.068,-13.558],[-13.242,7.677],[-9.903,12.562],[-0.168,14.211],[8.516,12.616],[12.267,7.934],[13.587,-2.439],[2.848,-10.226]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":98,"s":[{"i":[[2.728,-4.464],[-0.346,-1.217],[-1.916,-1.191],[-2.806,0],[-2.35,1.063],[-0.784,2.382],[-0.124,5.004],[9.141,3.198]],"o":[[-5.535,9.043],[0.346,1.217],[1.93,1.197],[2.76,0],[1.866,-0.844],[0.782,-2.332],[0.042,-0.074],[-5.797,-2.028]],"v":[[-9.999,-13.441],[-13.299,7.667],[-9.958,12.562],[-0.195,14.211],[8.471,12.616],[12.243,7.944],[13.58,-2.197],[2.233,-11.752]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0},"t":104,"s":[{"i":[[2.757,-4.451],[-0.459,-1.398],[-1.917,-1.199],[-2.803,0],[-2.348,1.063],[-0.8,2.388],[-0.12,4.96],[7.727,1.828]],"o":[[-5.602,9.046],[0.459,1.398],[1.917,1.199],[2.757,0],[1.863,-0.844],[0.8,-2.388],[0.042,-0.074],[-6.024,-1.425]],"v":[[-9.966,-13.385],[-13.327,7.663],[-9.985,12.562],[-0.208,14.211],[8.449,12.616],[12.232,7.949],[13.872,-2.099],[3.305,-11.967]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":117,"s":[{"i":[[2.757,-4.451],[-0.392,-1.279],[-1.917,-1.199],[-2.803,0],[-2.348,1.063],[-0.8,2.388],[0.974,5.882],[5.185,3.429]],"o":[[-5.602,9.046],[0.392,1.279],[1.917,1.199],[2.757,0],[1.863,-0.844],[0.8,-2.388],[0.042,-0.074],[-4.647,-3.074]],"v":[[-8.982,-13.385],[-12.343,7.663],[-9.001,12.562],[-0.208,14.211],[8.058,12.616],[11.841,7.949],[13.276,-4.427],[3.564,-13.035]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":124,"s":[{"i":[[2.757,-4.451],[-0.23,-1.373],[-1.917,-1.199],[-2.803,0],[-2.348,1.063],[-0.8,2.388],[0.177,5.21],[5.485,3.857]],"o":[[-5.602,9.046],[0.23,1.373],[1.917,1.199],[2.757,0],[1.863,-0.844],[0.8,-2.388],[0.042,-0.074],[-4.916,-3.457]],"v":[[-9.699,-13.385],[-13.06,7.663],[-9.718,12.562],[-0.208,14.211],[8.343,12.616],[12.126,7.949],[13.7,-2.8],[3.371,-12.289]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":134,"s":[{"i":[[2.757,-4.451],[-0.359,-1.248],[-1.917,-1.199],[-2.803,0],[-2.348,1.063],[-0.8,2.388],[-0.12,4.96],[5.542,4.052]],"o":[[-5.602,9.046],[0.359,1.248],[1.917,1.199],[2.757,0],[1.863,-0.844],[0.8,-2.388],[0.042,-0.074],[-4.968,-3.632]],"v":[[-9.966,-13.385],[-13.327,7.663],[-9.985,12.562],[-0.208,14.211],[8.449,12.616],[12.232,7.949],[13.652,-2.058],[3.204,-11.948]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.71,"y":0},"t":167,"s":[{"i":[[2.757,-4.451],[-0.259,-1.248],[-1.917,-1.199],[-2.803,0],[-2.348,1.063],[-0.8,2.388],[-0.12,4.96],[5.595,4.041]],"o":[[-5.602,9.046],[0.259,1.248],[1.917,1.199],[2.757,0],[1.863,-0.844],[0.8,-2.388],[0.042,-0.074],[-5.015,-3.622]],"v":[[-9.966,-13.385],[-13.327,7.663],[-9.985,12.562],[-0.208,14.211],[8.449,12.616],[12.232,7.949],[13.852,-2.099],[3.296,-11.967]],"c":false}]},{"i":{"x":0.2,"y":1},"o":{"x":0.167,"y":0.167},"t":171,"s":[{"i":[[2.328,-4.332],[-0.379,-1.503],[-1.916,-1.181],[-2.81,0],[-2.354,1.063],[-0.764,2.375],[-0.114,4.713],[6.715,2.447]],"o":[[-4.766,9.084],[0.379,1.503],[1.946,1.194],[2.764,0],[1.868,-0.844],[0.76,-2.269],[0.04,-0.07],[-5.447,-1.984]],"v":[[-10.024,-13.626],[-12.944,7.673],[-9.604,12.562],[-0.18,14.211],[8.309,12.616],[12.07,7.939],[13.516,-5.476],[2.049,-11.852]],"c":false}]},{"t":187,"s":[{"i":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0],[6.8,-0.7]],"o":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0],[-6.8,0.7]],"v":[[-12.432,-14.575],[-11.977,8.464],[-8.487,13.499],[0.4,14.598],[9.223,13.566],[12.656,8.149],[12.482,-13.225],[-0.668,-13.639]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":244,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"hand_6","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[49,47.75,0],"ix":2,"l":2},"a":{"a":0,"k":[256,256,0],"ix":1,"l":2},"s":{"a":0,"k":[11.4,11.4,100],"ix":6,"l":2}},"ao":0,"w":512,"h":512,"ip":0,"op":240,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/tgs/voice_chat_raise_hand_7.tgs b/Telegram-Mac/tgs/voice_chat_raise_hand_7.tgs new file mode 100644 index 0000000000..ab3172c00d --- /dev/null +++ b/Telegram-Mac/tgs/voice_chat_raise_hand_7.tgs @@ -0,0 +1 @@ +{"v":"5.7.6","fr":60,"ip":0,"op":180,"w":100,"h":100,"nm":"hand_7 BIG 1x","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"handle","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.6],"y":[0]},"t":10,"s":[174]},{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":16,"s":[147]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.213],"y":[0]},"t":23,"s":[-17]},{"i":{"x":[0.61],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":27,"s":[-35.589]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.5],"y":[0]},"t":32,"s":[27.015]},{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":37,"s":[47.22]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0]},"t":42,"s":[-18]},{"i":{"x":[0.61],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":48,"s":[-35.589]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.5],"y":[0]},"t":54,"s":[27.015]},{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":61,"s":[47.22]},{"i":{"x":[0.785],"y":[-2.394]},"o":{"x":[0.518],"y":[0]},"t":73,"s":[-18]},{"i":{"x":[0.312],"y":[1]},"o":{"x":[0.204],"y":[0.31]},"t":81,"s":[-9.175]},{"t":97,"s":[174]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.6,"y":0},"t":10,"s":[-9.96,1.125,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":16,"s":[-16.661,-20.671,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":23,"s":[-36.588,-27.368,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":27,"s":[-30.271,-30.836,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":32,"s":[-11.215,-32.394,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":37,"s":[-16.116,-31.018,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":42,"s":[-36.588,-27.368,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":48,"s":[-30.271,-30.836,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":54,"s":[-11.215,-32.394,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":61,"s":[-16.116,-31.018,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.5,"y":0},"t":73,"s":[-36.588,-27.368,0],"to":[0,0,0],"ti":[0,0,0]},{"t":97,"s":[-9.802,-0.869,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-215.5,-116,0],"ix":1,"l":2},"s":{"a":0,"k":[20.019,19.981,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-215.5,-116],[-215.146,-127.132]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":14,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-215.5,-116],[-210.023,-182.103]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-215.5,-116],[-207.5,-198.5]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":86,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-215.5,-116],[-207.5,-198.5]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":87,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-215.5,-116],[-211.874,-179.057]],"c":false}]},{"t":89,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-215.5,-116],[-215.991,-154.088]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":10,"op":92,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"flag","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":92.982,"ix":10},"p":{"a":0,"k":[-204.72,-162.79,0],"ix":2,"l":2},"a":{"a":0,"k":[-122.092,14.474,0],"ix":1,"l":2},"s":{"a":0,"k":[99.986,100.014,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.6,"y":0},"t":12,"s":[{"i":[[0.317,-0.377],[9.871,0.008],[-5.426,0.66]],"o":[[-6.226,-0.015],[-1.013,-0.074],[4.743,0.541]],"v":[[-5.96,154.637],[-50.486,154.578],[-27.743,151.11]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":14,"s":[{"i":[[0.317,-1.093],[9.871,0.023],[-3.056,2.72]],"o":[[-6.226,-0.044],[-1.013,-0.215],[7.435,1.476]],"v":[[-5.96,154.637],[-28.783,155.672],[-28.323,148.174]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[0.317,-4.427],[9.871,0.094],[-7.661,10.615]],"o":[[-6.226,-0.178],[-1.013,-0.869],[16.085,12.234]],"v":[[-5.96,154.637],[-39.634,154.55],[-66.371,118.451]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[0.317,-7.761],[9.871,0.165],[-21.058,28.062]],"o":[[-6.226,-0.312],[-1.013,-1.524],[1.096,39.632]],"v":[[-5.96,154.637],[-50.486,153.428],[-40.692,84.982]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0.317,-7.761],[9.871,0.165],[-6.509,17.627]],"o":[[-6.226,-0.312],[-1.013,-1.524],[5.307,24.241]],"v":[[-5.96,154.637],[-50.486,153.428],[-25.785,87.237]],"c":true}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[0.317,-7.761],[9.871,0.165],[-32.021,13.36]],"o":[[-6.226,-0.312],[-1.013,-1.524],[-16.69,26.199]],"v":[[-5.96,154.637],[-50.486,153.428],[15.356,105.805]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":25,"s":[{"i":[[0.317,-7.761],[9.871,0.165],[-5.426,13.583]],"o":[[-6.226,-0.312],[-1.013,-1.524],[4.743,11.147]],"v":[[-5.96,154.637],[-50.486,153.428],[-27.743,82.045]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":27,"s":[{"i":[[0.317,-7.761],[9.871,0.165],[12.378,29.044]],"o":[[-6.226,-0.312],[-1.013,-1.524],[29.83,8.678]],"v":[[-5.96,154.637],[-50.486,153.428],[-51.505,85]],"c":true}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":32,"s":[{"i":[[0.317,7.203],[9.871,-0.153],[-31.325,-39.218]],"o":[[-6.226,0.289],[-1.013,1.414],[-3.169,-41.969]],"v":[[-5.83,154.152],[-51.499,151.85],[-15.606,221.238]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":34,"s":[{"i":[[0.317,7.203],[9.871,-0.153],[-5.426,-12.606]],"o":[[-6.226,0.289],[-1.013,1.414],[4.743,-10.345]],"v":[[-5.83,154.152],[-51.499,151.85],[-27.874,222.489]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":37,"s":[{"i":[[0.317,7.203],[9.871,-0.153],[11.573,-55.334]],"o":[[-6.226,0.289],[-1.013,1.414],[39.449,-25.407]],"v":[[-5.83,154.152],[-51.499,151.85],[-52.752,224.049]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":42,"s":[{"i":[[0.317,-6.39],[9.871,0.136],[-11.942,3.44]],"o":[[-6.226,-0.257],[-1.013,-1.254],[-1.26,15.905]],"v":[[-5.948,154.592],[-50.579,153.284],[-20.684,99.413]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":45,"s":[{"i":[[0.317,-7.761],[9.871,0.165],[12.378,29.044]],"o":[[-6.226,-0.312],[-1.013,-1.524],[29.83,8.678]],"v":[[-5.96,154.637],[-50.486,153.428],[-51.505,85]],"c":true}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":53,"s":[{"i":[[0.317,7.203],[9.871,-0.153],[-31.325,-39.218]],"o":[[-6.226,0.289],[-1.013,1.414],[-3.169,-41.969]],"v":[[-5.83,154.152],[-51.499,151.85],[-15.606,221.238]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":55.842,"s":[{"i":[[0.317,7.203],[9.871,-0.153],[-5.426,-12.606]],"o":[[-6.226,0.289],[-1.013,1.414],[4.743,-10.345]],"v":[[-5.83,154.152],[-51.499,151.85],[-27.874,222.489]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":63,"s":[{"i":[[0.317,7.203],[9.871,-0.153],[11.573,-55.334]],"o":[[-6.226,0.289],[-1.013,1.414],[39.449,-25.407]],"v":[[-5.83,154.152],[-51.499,151.85],[-52.752,224.049]],"c":true}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":71,"s":[{"i":[[0.317,-5.893],[9.871,0.126],[-21.71,21.684]],"o":[[-6.226,-0.237],[-1.013,-1.157],[-2.05,24.036]],"v":[[-5.944,154.576],[-50.612,153.231],[-14.362,101.878]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":75,"s":[{"i":[[0.317,-7.761],[9.871,0.165],[-5.426,13.583]],"o":[[-6.226,-0.312],[-1.013,-1.524],[4.743,11.147]],"v":[[-5.96,154.637],[-50.486,153.428],[-27.743,82.045]],"c":true}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":81,"s":[{"i":[[0.317,5.961],[9.871,-0.127],[30.989,-21.541]],"o":[[-6.226,0.24],[-1.013,1.17],[42.685,-3.8]],"v":[[-5.841,154.192],[-51.415,151.981],[-117.05,184.128]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":83,"s":[{"i":[[0.317,7.203],[9.871,-0.153],[-22.34,-40.1]],"o":[[-6.226,0.289],[-1.013,1.414],[1.595,-36.326]],"v":[[-5.83,154.152],[-51.499,151.85],[-28.747,222.978]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":86,"s":[{"i":[[0.139,7.208],[9.871,0.091],[-5.112,-12.737]],"o":[[-6.231,0.135],[-1.048,1.388],[4.997,-10.225]],"v":[[-5.313,153.232],[-48.29,151.152],[-29.044,221.002]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":87,"s":[{"i":[[0.317,7.203],[9.871,-0.153],[-5.426,-12.606]],"o":[[-13.386,-2.417],[-1.013,1.414],[4.743,-10.345]],"v":[[-4.828,156.44],[-48.115,167.327],[-27.874,222.489]],"c":true}]},{"t":89,"s":[{"i":[[0.317,7.203],[9.871,-0.153],[-0.737,-10.604]],"o":[[-6.226,0.289],[-1.013,1.414],[4.743,-10.345]],"v":[[-5.83,154.152],[-15.405,184.967],[-16.93,216.13]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":12,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-113.297,-133.109],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Polystar 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":14,"op":90,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"NULL CONTROL","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256.989,275.127,0],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":182,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"head","parent":8,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.6],"y":[0]},"t":1,"s":[0]},{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":11,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":23,"s":[-13]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.16],"y":[0]},"t":71,"s":[-13]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0]},"t":98.215,"s":[0]},{"i":{"x":[0.09],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":106.715,"s":[7]},{"t":127.357421875,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":1,"s":[0,-37.327,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":11,"s":[0,-26.827,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":23,"s":[-4.4,-39.427,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":36,"s":[-2.401,-30.424,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0},"t":50,"s":[-4.4,-39.427,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.3,"y":1},"o":{"x":0.315,"y":0},"t":71,"s":[-2.401,-30.424,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.496,"y":1},"o":{"x":0.6,"y":0},"t":98,"s":[5.201,-26.827,0],"to":[0,0,0],"ti":[0,0,0]},{"t":127.357421875,"s":[0,-37.327,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":1,"s":[100,100,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":5,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":11,"s":[105,95,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":16,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":23,"s":[105,95,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":28,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":36,"s":[100,100,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":44,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":50,"s":[105,95,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":58,"s":[97,103,100]},{"i":{"x":[0.3,0.3,0.3],"y":[1,1,1]},"o":{"x":[0.16,0.16,0.16],"y":[0,0,0]},"t":71,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.6,0.6,0.6],"y":[0,0,0]},"t":98.215,"s":[105,95,100]},{"i":{"x":[0.09,0.09,0.09],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":106.715,"s":[95,105,100]},{"t":127.357421875,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-6.341,0],[0,-6.341],[6.341,0],[0,6.341]],"o":[[6.341,0],[0,6.341],[-6.341,0],[0,-6.341]],"v":[[0,-11.482],[11.482,0],[0,11.482],[-11.482,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":183,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":3,"nm":"hands","parent":8,"sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.5],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":21,"s":[-10]},{"i":{"x":[0.17],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":70,"s":[-10]},{"t":128.572265625,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[-2.341,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[-2.341,-9.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":21,"s":[-3.741,-17.977,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":35,"s":[-1.541,-11.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.5,"y":0},"t":49,"s":[-1.541,-17.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.544,"y":0},"t":70,"s":[-1.541,-11.777,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.134,"y":1},"o":{"x":0.5,"y":0},"t":97,"s":[-2.341,-9.777,0],"to":[0,0,0],"ti":[0,0,0]},{"t":128.572265625,"s":[-2.341,-17.777,0]}],"ix":2,"l":2},"a":{"a":0,"k":[115,115,0],"ix":1,"l":2},"s":{"a":0,"k":[20,20,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":183,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"hands 3","parent":5,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,115,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],"o":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],"v":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.6,"y":0},"t":10,"s":[{"i":[[0,0],[-0.71,-5.814],[-13.22,1.67],[-6.659,-0.445]],"o":[[-4.86,-9.215],[0.566,4.637],[3.994,-0.505],[1.183,0.079]],"v":[[-9.125,0.522],[-27.079,-2.224],[-7.644,3.484],[11.49,2.929]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[0,0],[-1.902,-5.873],[-9.833,-0.14],[-7.372,0.373]],"o":[[-4.328,1.758],[1.817,5.11],[4.766,0.217],[1.182,0.009]],"v":[[-17.041,-20.099],[-26.446,-5.406],[-7.078,3.541],[9.615,2.763]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":23,"s":[{"i":[[0,0],[-4.205,-5.986],[-9.957,-1.926],[-4.458,0.474]],"o":[[5.753,4.944],[4.232,6.025],[3.773,0.73],[1.179,-0.125]],"v":[[-37.604,-27.175],[-25.224,-11.553],[-5.844,2.761],[7.055,3.069]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":27,"s":[{"i":[[0,0],[-1.728,-7.979],[-9.811,-4.073],[-4.458,0.474]],"o":[[3.018,5.449],[1.024,4.728],[4.574,1.385],[1.179,-0.125]],"v":[[-30.389,-31.532],[-24.556,-12.392],[-6.174,2.474],[7.055,3.069]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":32,"s":[{"i":[[0,0],[-0.133,-5.267],[-9.077,-3.752],[-4.458,0.474]],"o":[[-4.804,4.722],[0.193,7.66],[5.825,2.408],[1.179,-0.125]],"v":[[-10.804,-33.837],[-18.834,-15.906],[-6.69,2.025],[7.055,3.069]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":37,"s":[{"i":[[0,0],[-0.271,-4.621],[-9.517,-2.839],[-4.458,0.474]],"o":[[-3.394,4.297],[0.242,4.13],[4.799,1.569],[1.179,-0.125]],"v":[[-15.907,-30.468],[-22.029,-13.729],[-6.267,2.393],[7.055,3.069]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":42,"s":[{"i":[[0,0],[-3.928,-6.171],[-9.957,-1.926],[-4.563,0.793]],"o":[[5.522,5.2],[3.954,6.211],[3.773,0.73],[1.168,-0.203]],"v":[[-36.207,-29.267],[-24.551,-13.099],[-5.703,2.786],[7.055,3.069]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":48,"s":[{"i":[[0,0],[-1.728,-7.979],[-9.811,-4.073],[-4.458,0.474]],"o":[[3.018,5.449],[1.024,4.728],[4.574,1.385],[1.179,-0.125]],"v":[[-30.389,-31.532],[-24.556,-12.392],[-6.174,2.474],[7.055,3.069]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":54,"s":[{"i":[[0,0],[-0.133,-5.267],[-9.319,-3.103],[-4.568,0.674]],"o":[[-4.804,4.722],[0.193,7.66],[5.735,1.909],[1.173,-0.173]],"v":[[-10.804,-33.837],[-18.834,-15.906],[-6.656,1.832],[7.072,2.972]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":61,"s":[{"i":[[0,0],[-0.271,-4.621],[-9.517,-2.839],[-4.458,0.474]],"o":[[-3.394,4.297],[0.242,4.13],[4.799,1.569],[1.179,-0.125]],"v":[[-15.907,-30.468],[-22.029,-13.729],[-6.267,2.393],[7.055,3.069]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.5,"y":0},"t":73,"s":[{"i":[[0,0],[-4.205,-5.986],[-9.957,-1.926],[-4.458,0.474]],"o":[[5.753,4.944],[4.232,6.025],[3.773,0.73],[1.179,-0.125]],"v":[[-37.604,-27.175],[-25.224,-11.553],[-5.844,2.761],[7.055,3.069]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.6,"y":0},"t":97,"s":[{"i":[[0,0],[-0.71,-5.814],[-13.22,1.67],[-6.659,-0.445]],"o":[[-4.86,-9.215],[0.566,4.637],[3.994,-0.505],[1.183,0.079]],"v":[[-9.125,0.522],[-27.079,-2.224],[-7.644,3.484],[11.49,2.929]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":110,"s":[{"i":[[0,0],[-3.924,-6.477],[-10.094,-0.98],[-3.546,0.075]],"o":[[4.365,3.67],[3.808,6.286],[3.961,0.384],[1.183,-0.028]],"v":[[-37.672,-26.337],[-25.271,-10.97],[-5.942,2.371],[7.258,2.943]],"c":false}]},{"t":131,"s":[{"i":[[0,0],[-4.026,-6.73],[-10.797,-1.273],[-2.119,-0.01]],"o":[[0.65,4.552],[3.882,6.488],[3.499,0.413],[1.186,0.005]],"v":[[-34.027,-30.664],[-27.902,-11.553],[-6.28,2.761],[4.392,3.022]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4.8,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":183,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"hands 2","parent":5,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[115,115,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[500,500,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":0,"s":[{"i":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],"o":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],"v":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":10,"s":[{"i":[[-2.832,-0.073],[-2.303,-0.066],[-1.926,-3.085],[-0.239,-5.607]],"o":[[2.794,0.072],[11.894,0.339],[2.109,3.377],[0,0]],"v":[[2.763,2.93],[11.324,2.869],[28.065,7.836],[30.818,22.809]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":22,"s":[{"i":[[-2.833,0.019],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],"o":[[4.963,-0.034],[5.323,2.097],[1.94,4.605],[0,0]],"v":[[2.632,3.384],[12.827,2.873],[22.174,19.291],[30.152,32.241]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.7,"y":0},"t":36,"s":[{"i":[[-2.83,-0.135],[-3.536,-1.048],[-1.592,-3.476],[-1.09,-6.235]],"o":[[7.035,0.337],[5.485,1.626],[1.67,3.647],[0,0]],"v":[[1.498,3.488],[12.683,2.542],[23.022,15.874],[26.398,31.323]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.6,"y":0},"t":50,"s":[{"i":[[-2.829,0.15],[-2.144,-0.845],[-1.574,-3.736],[-3.615,-5.451]],"o":[[4.712,-0.25],[5.323,2.097],[1.94,4.605],[0,0]],"v":[[2.417,3.447],[12.827,2.873],[22.174,19.291],[30.152,32.241]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.4,"y":0},"t":71,"s":[{"i":[[-2.833,0.023],[-3.532,-0.206],[-1.416,-3.799],[-0.329,-6.321]],"o":[[4.317,-0.036],[4.623,0.27],[1.401,3.758],[0,0]],"v":[[1.983,3.325],[12.861,3.014],[25.932,11.288],[27.784,26.515]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.6,"y":0},"t":97,"s":[{"i":[[-2.832,-0.073],[-2.303,-0.066],[-1.926,-3.085],[-0.239,-5.607]],"o":[[2.794,0.072],[11.894,0.339],[2.109,3.377],[0,0]],"v":[[2.763,2.93],[11.324,2.869],[28.065,7.836],[30.818,22.809]],"c":false}]},{"t":124.927734375,"s":[{"i":[[-3.103,0.058],[-2.523,0.098],[-2.257,-5.922],[-0.05,-3.076]],"o":[[2.726,-0.051],[10.848,-0.42],[1.636,4.291],[0,0]],"v":[[3.047,3.014],[11.974,3.008],[30.43,17.71],[32.973,31.114]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4.8,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path-8","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":183,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"body","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[116.171,151.524,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":21,"s":[116.171,140.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":35,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.5,"y":0},"t":49,"s":[116.171,143.924,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.544,"y":0},"t":70,"s":[116.171,146.724,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.134,"y":1},"o":{"x":0.5,"y":0},"t":97,"s":[116.171,151.524,0],"to":[0,0,0],"ti":[0,0,0]},{"t":128.572265625,"s":[116.171,146.724,0]}],"ix":2,"l":2},"a":{"a":0,"k":[0,14.211,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":5,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.484,0.484,0.484],"y":[0,0,0]},"t":10,"s":[100,100,100]},{"i":{"x":[0.486,0.486,0.486],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":15,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":21,"s":[100,100,100]},{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":27,"s":[97,103,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":35,"s":[100,100,100]},{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":42,"s":[103,97,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.5,0.5,0.5],"y":[0,0,0]},"t":49,"s":[100,100,100]},{"i":{"x":[0.4,0.4,0.4],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":56,"s":[97,103,100]},{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.422,0.422,0.422],"y":[0,0,0]},"t":70,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.484,0.484,0.484],"y":[0,0,0]},"t":97,"s":[100,100,100]},{"i":{"x":[0.17,0.17,0.17],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":109.143,"s":[97,103,100]},{"t":128.572265625,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":0,"s":[{"i":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0],[9.8,-1]],"o":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0],[-9.8,1]],"v":[[-12.432,-14.575],[-11.977,8.464],[-8.487,13.499],[0.4,14.598],[9.223,13.566],[12.656,8.149],[12.482,-13.225],[-1.968,-14.039]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.5,"y":0},"t":10,"s":[{"i":[[0,0],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0,0],[0,0]],"o":[[0,0],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,0],[0,0]],"v":[[-14.032,-4.05],[-13.127,7.863],[-9.825,12.562],[0.35,14.211],[10.398,12.616],[13.744,7.749],[14.232,-7.571],[-3.266,-5.391]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.5,"y":0},"t":21,"s":[{"i":[[-0.782,-2.214],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[1.3,6.472],[9.97,-4.063]],"o":[[1.863,5.271],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[-0.129,-0.644],[-4.992,2.034]],"v":[[-14.632,-13.411],[-12.127,7.863],[-8.825,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[11.232,-16.011],[-4.338,-14.076]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0},"t":35,"s":[{"i":[[-0.807,-2.284],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[2.094,6.249],[7.889,0.562]],"o":[[1.922,5.439],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,-0.187],[-5.711,-0.407]],"v":[[-15.346,-10.627],[-12.527,7.863],[-9.225,12.562],[0.35,14.211],[9.798,12.616],[13.144,7.749],[11.974,-10.116],[-5.454,-7.937]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":42,"s":[{"i":[[-0.79,-2.237],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[1.559,6.399],[7.467,-0.089]],"o":[[1.882,5.326],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[-0.114,-0.533],[-5.405,0.065]],"v":[[-14.481,-12.569],[-12.098,7.863],[-8.796,12.562],[0.35,14.211],[9.369,12.616],[12.715,7.749],[11.484,-13.946],[-5.352,-10.685]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":49,"s":[{"i":[[-0.782,-2.214],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[1.3,6.472],[14.518,-3.182]],"o":[[1.863,5.271],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[-0.129,-0.644],[-5.344,1.171]],"v":[[-14.732,-16.111],[-12.127,7.863],[-8.825,12.562],[0.35,14.211],[9.398,12.616],[12.744,7.749],[11.532,-15.511],[-5.386,-14.657]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":54,"s":[{"i":[[-0.786,-2.224],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[1.413,6.44],[12.824,-1.959]],"o":[[1.872,5.295],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[-0.111,-0.579],[-5.44,0.831]],"v":[[-14.729,-16.667],[-12.144,7.863],[-8.842,12.562],[0.35,14.211],[9.415,12.616],[12.761,7.749],[11.555,-14.741],[-3.942,-14.06]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0.167},"t":57,"s":[{"i":[[-0.79,-2.237],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[1.559,6.399],[7.512,0.621]],"o":[[1.882,5.326],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,0.159],[-5.438,-0.45]],"v":[[-14.818,-14.61],[-12.155,7.863],[-8.853,12.562],[0.35,14.211],[9.426,12.616],[12.771,7.749],[11.18,-14.235],[-5.611,-12.065]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0},"t":70,"s":[{"i":[[-0.807,-2.284],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[2.094,6.249],[7.883,0.166]],"o":[[1.922,5.439],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,-0.187],[-5.706,-0.12]],"v":[[-15.324,-9.227],[-12.527,7.863],[-9.225,12.562],[0.35,14.211],[9.798,12.616],[13.144,7.749],[11.974,-10.116],[-5.341,-8.17]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.5,"y":0},"t":97,"s":[{"i":[[0,0],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0,0],[0,0]],"o":[[0,0],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,0],[0,0]],"v":[[-14.032,-4.05],[-13.127,7.863],[-9.825,12.562],[0.35,14.211],[10.398,12.616],[13.744,7.749],[14.232,-7.571],[-3.166,-6.891]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":104,"s":[{"i":[[-0.351,-0.994],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0.912,2.72],[7.486,0.008]],"o":[[0.837,2.368],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,-0.082],[-5.419,-0.006]],"v":[[-13.849,-7.596],[-12.139,7.863],[-8.836,12.562],[0.35,14.211],[9.41,12.616],[12.755,7.749],[12.383,-8.209],[-4.212,-8.283]],"c":false}]},{"i":{"x":0.17,"y":1},"o":{"x":0.167,"y":0.167},"t":112,"s":[{"i":[[0,-0.599],[0,0],[-1.89,-0.83],[-2.946,0],[-2.468,1.063],[-0.086,2.131],[0.292,0.87],[7.366,0.668]],"o":[[0,0.803],[0.14,2.059],[2.504,1.099],[2.898,0],[1.959,-0.844],[0,0],[0,-0.026],[-5.332,-0.483]],"v":[[-13.276,-13.757],[-12.148,7.863],[-8.845,12.562],[0.35,14.211],[9.419,12.616],[12.764,7.749],[12.791,-11.807],[-3.347,-13.633]],"c":false}]},{"t":128.572265625,"s":[{"i":[[0,0],[-0.741,-5.903],[-1.922,-0.751],[-2.946,0],[-2.468,1.063],[-0.228,3.004],[0,0],[9.8,-1]],"o":[[0,0],[0.328,2.608],[2.556,0.999],[2.898,0],[1.959,-0.844],[0.638,-8.388],[0,0],[-9.8,1]],"v":[[-12.432,-14.575],[-11.977,8.464],[-8.487,13.499],[0.4,14.598],[9.223,13.566],[12.656,8.149],[12.482,-13.225],[-1.968,-14.039]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":183,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"hand_7","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[49,47.75,0],"ix":2,"l":2},"a":{"a":0,"k":[256,256,0],"ix":1,"l":2},"s":{"a":0,"k":[11.4,11.4,100],"ix":6,"l":2}},"ao":0,"w":512,"h":512,"ip":0,"op":180,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/tgs/voice_chat_set_reminder.tgs b/Telegram-Mac/tgs/voice_chat_set_reminder.tgs new file mode 100644 index 0000000000..b95d53a29a --- /dev/null +++ b/Telegram-Mac/tgs/voice_chat_set_reminder.tgs @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":11,"w":100,"h":100,"nm":"Set Reminder","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Line Bell","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-0.05,-3.45,0],"ix":2},"a":{"a":0,"k":[-0.05,-2.95,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-127.5,-130.5],[127.4,124.6]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Bottom Bell","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,137.7,0],"ix":2},"a":{"a":0,"k":[0,137.7,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[20.9,0],[7.5,18.2]],"o":[[-7.5,18.2],[-20.9,0],[0,0]],"v":[[46.3,122.2],[0,153.2],[-46.3,122.2]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Bell","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[255.5,230.695,0],"ix":2},"a":{"a":0,"k":[-0.033,-25.3,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,8.3],[-3.1,2.9],[0,0],[-1.3,19.1]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[-8.3,0],[0,-4.2],[0,0],[14.1,-13],[0,0]],"v":[[-92.8,-53.2],[21.339,60.939],[26.803,66.403],[27.786,67.386],[27.894,67.494],[61.507,101.107],[62.6,102.2],[-121.4,102.2],[-136.4,87.2],[-131.5,76.1],[-121.9,67.3],[-97.8,17.4]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[{"i":[[0,0],[-3.074,7.353],[-1.513,2.779],[-10.517,-10.963],[-8.322,-11.168],[0,0],[0,0],[0,0],[0,8.3],[-3.1,2.9],[0,0],[-1.3,19.1]],"o":[[0,0],[2.875,-6.876],[1.114,-0.127],[-9.334,-3.92],[47.453,50.11],[0,0],[0,0],[-8.3,0],[0,-4.2],[0,0],[14.1,-13],[0,0]],"v":[[-92.8,-53.2],[-88.262,-72.223],[-82.528,-85.88],[-56.45,-60.075],[-74.961,-34.327],[60.586,101.646],[61.679,102.738],[-121.4,102.2],[-136.4,87.2],[-131.5,76.1],[-121.9,67.3],[-97.8,17.4]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[0,0],[-3.074,7.353],[-1.513,2.779],[-10.518,-10.963],[-8.322,-11.168],[0,0],[0,0],[0,0],[0,8.3],[-3.1,2.9],[0,0],[-1.3,19.1]],"o":[[0,0],[2.875,-6.876],[1.114,-0.127],[-9.334,-3.92],[8.928,8.832],[0,0],[0,0],[-8.3,0],[0,-4.2],[0,0],[14.1,-13],[0,0]],"v":[[-92.8,-53.2],[-88.262,-72.223],[-82.528,-85.88],[0.05,-0.575],[-17.961,23.673],[59.086,101.879],[60.179,102.972],[-121.4,102.2],[-136.4,87.2],[-131.5,76.1],[-121.9,67.3],[-97.8,17.4]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[0,0],[-3.074,7.353],[-1.513,2.779],[-10.517,-10.963],[-8.322,-11.168],[0,0],[0,0],[0,0],[0,8.3],[-3.1,2.9],[0,0],[-1.3,19.1]],"o":[[0,0],[2.875,-6.876],[1.114,-0.127],[-9.334,-3.92],[8.928,8.832],[0,0],[0,0],[-8.3,0],[0,-4.2],[0,0],[14.1,-13],[0,0]],"v":[[-92.8,-53.2],[-88.262,-72.223],[-82.528,-85.88],[48.05,47.925],[30.039,72.173],[59.086,101.879],[60.179,102.972],[-121.4,102.2],[-136.4,87.2],[-131.5,76.1],[-121.9,67.3],[-97.8,17.4]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[0,0],[-3.074,7.353],[-1.513,2.779],[-10.517,-10.963],[-5.322,-11.418],[0,0],[0,0],[0,0],[0,8.3],[-3.1,2.9],[0,0],[-1.3,19.1]],"o":[[0,0],[2.875,-6.876],[1.114,-0.127],[-9.334,-3.92],[1.678,1.832],[0,0],[0,0],[-8.3,0],[0,-4.2],[0,0],[14.1,-13],[0,0]],"v":[[-92.8,-53.2],[-88.262,-72.223],[-82.528,-85.88],[81.05,76.175],[60.039,100.673],[59.086,101.879],[60.179,102.972],[-121.4,102.2],[-136.4,87.2],[-131.5,76.1],[-121.9,67.3],[-97.8,17.4]],"c":true}]},{"t":7,"s":[{"i":[[0,0],[-3.074,7.353],[-1.513,2.779],[-3.51,-3.37],[-1.258,-1.256],[0,0],[0,0],[0,0],[0,8.3],[-3.1,2.9],[0,0],[-1.3,19.1]],"o":[[0,0],[2.875,-6.876],[0.589,-0.067],[1.039,0.998],[36.729,36.67],[0,0],[0,0],[-8.3,0],[0,-4.2],[0,0],[14.1,-13],[0,0]],"v":[[-92.8,-53.2],[-88.262,-72.223],[-82.528,-85.88],[-76.213,-80.733],[-72.763,-77.348],[90.086,101.66],[91.179,102.753],[-121.4,102.2],[-136.4,87.2],[-131.5,76.1],[-121.9,67.3],[-97.8,17.4]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2,"s":[{"i":[[-22.1,0],[-1.3,-20.9],[0,0],[0,0],[-1.7,-22.9],[0,0],[-14.1,-12.9],[0,0],[5.3,-6.4],[0,0],[0,0],[0,0],[0,0],[-8,3.2],[0,0]],"o":[[21.2,0],[0,0],[0,0],[21.3,8.5],[0,0],[1.4,19.1],[0,0],[6.1,5.6],[-4.5,5.4],[0,0],[0,0],[0,0],[5.8,-6],[0,0],[0,-21.9]],"v":[[0,-152.8],[39.9,-115.3],[40,-112.8],[55.1,-106.8],[92.7,-55.4],[97.9,17.3],[122,67.2],[131.6,76],[132.5,97.2],[119.1,102],[74.435,57.335],[73.265,56.165],[-75.9,-93],[-55.1,-107],[-40,-113]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[{"i":[[-22.1,0],[-1.3,-20.9],[0,0],[0,0],[-1.7,-22.9],[0,0],[-14.1,-12.9],[0,0],[6.37,-5.768],[0,0],[3.835,4.419],[0,0],[0,0],[-8,3.2],[0,0]],"o":[[21.2,0],[0,0],[0,0],[21.3,8.5],[0,0],[1.4,19.1],[0,0],[8.373,8.582],[-4.704,4.26],[0,0],[-6.636,-5.045],[0,0],[8.044,-10.337],[0,0],[0,-21.9]],"v":[[0,-152.8],[39.9,-115.3],[40,-112.8],[55.1,-106.8],[92.7,-55.4],[97.9,17.3],[122,67.2],[131.6,76],[130.377,100.18],[99.362,102.508],[-41.868,-44.519],[-58.795,-61.322],[-82.749,-85.808],[-55.1,-107],[-40,-113]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-22.1,0],[-1.3,-20.9],[0,0],[0,0],[-1.7,-22.9],[0,0],[-14.1,-12.9],[0,0],[6.37,-5.768],[0,0],[3.835,4.419],[0,0],[0,0],[-8,3.2],[0,0]],"o":[[21.2,0],[0,0],[0,0],[21.3,8.5],[0,0],[1.4,19.1],[0,0],[8.373,8.582],[-4.704,4.26],[0,0],[-6.636,-5.045],[0,0],[8.044,-10.337],[0,0],[0,-21.9]],"v":[[0,-152.8],[39.9,-115.3],[40,-112.8],[55.1,-106.8],[92.7,-55.4],[97.9,17.3],[122,67.2],[131.6,76],[130.377,100.18],[99.362,102.508],[14.632,14.981],[-2.295,-1.822],[-82.749,-85.808],[-55.1,-107],[-40,-113]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5,"s":[{"i":[[-22.1,0],[-1.3,-20.9],[0,0],[0,0],[-1.7,-22.9],[0,0],[-14.1,-12.9],[0,0],[6.37,-5.768],[0,0],[3.835,4.419],[0,0],[0,0],[-8,3.2],[0,0]],"o":[[21.2,0],[0,0],[0,0],[21.3,8.5],[0,0],[1.4,19.1],[0,0],[8.373,8.582],[-4.704,4.26],[0,0],[-6.636,-5.045],[0,0],[8.044,-10.337],[0,0],[0,-21.9]],"v":[[0,-152.8],[39.9,-115.3],[40,-112.8],[55.1,-106.8],[92.7,-55.4],[97.9,17.3],[122,67.2],[131.6,76],[130.377,100.18],[99.362,102.508],[62.632,63.481],[45.705,46.678],[-82.749,-85.808],[-55.1,-107],[-40,-113]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[-22.1,0],[-1.3,-20.9],[0,0],[0,0],[-1.7,-22.9],[0,0],[-14.1,-12.9],[0,0],[6.37,-5.768],[0,0],[3.835,4.419],[0,0],[0,0],[-8,3.2],[0,0]],"o":[[21.2,0],[0,0],[0,0],[21.3,8.5],[0,0],[1.4,19.1],[0,0],[8.373,8.582],[-4.704,4.26],[0,0],[-6.636,-5.045],[0,0],[8.044,-10.337],[0,0],[0,-21.9]],"v":[[0,-152.8],[39.9,-115.3],[40,-112.8],[55.1,-106.8],[92.7,-55.4],[97.9,17.3],[122,67.2],[131.6,76],[130.377,100.18],[99.362,102.508],[89.882,90.481],[78.705,74.928],[-82.749,-85.808],[-55.1,-107],[-40,-113]],"c":true}]},{"t":7,"s":[{"i":[[-22.1,0],[-1.3,-20.9],[0,0],[0,0],[-1.7,-22.9],[0,0],[-14.1,-12.9],[0,0],[6.37,-5.768],[0,0],[3.835,4.419],[0,0],[0,0],[-8,3.2],[0,0]],"o":[[21.2,0],[0,0],[0,0],[21.3,8.5],[0,0],[1.4,19.1],[0,0],[8.373,8.582],[-4.704,4.26],[0,0],[-6.636,-5.045],[0,0],[8.044,-10.337],[0,0],[0,-21.9]],"v":[[0,-152.8],[39.9,-115.3],[40,-112.8],[55.1,-106.8],[92.7,-55.4],[97.9,17.3],[122,67.2],[131.6,76],[130.377,100.18],[90.362,102.509],[-42.618,-41.019],[-57.795,-54.822],[-82.749,-85.808],[-55.1,-107],[-40,-113]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":7,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Bell EXAMPLE","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.467,0.205,0],"ix":2},"a":{"a":0,"k":[0.001,0.2,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-22.091,0],[-1.305,-20.913],[0,0],[0,0],[-1.637,-22.924],[0,0],[-14.13,-12.952],[0,0],[5.598,-6.107],[4.205,0],[0,0],[0,8.284],[-3.099,2.841],[0,0],[-1.366,19.12],[0,0],[-21.339,8.535],[0,0]],"o":[[21.242,0],[0,0],[0,0],[21.339,8.535],[0,0],[1.366,19.12],[0,0],[6.107,5.598],[-2.841,3.099],[0,0],[-8.284,0],[0,-4.205],[0,0],[14.13,-12.952],[0,0],[1.637,-22.924],[0,0],[0,-22.091]],"v":[[0.001,-152.8],[39.922,-115.33],[40.001,-112.8],[55.114,-106.755],[92.678,-55.321],[97.874,17.427],[122.004,67.37],[131.575,76.143],[132.496,97.336],[121.439,102.2],[-121.437,102.2],[-136.437,87.2],[-131.573,76.143],[-122.003,67.37],[-97.873,17.427],[-92.676,-55.321],[-55.112,-106.755],[-39.999,-112.8]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[20.903,0],[7.469,18.212]],"o":[[-7.469,18.212],[-20.903,0],[0,0]],"v":[[46.28,122.161],[0.001,153.2],[-46.279,122.161]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":7,"op":180,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Pre-comp 1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[13.6,13.6,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":180,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/tgs/voice_chat_set_reminder_to_mute.tgs b/Telegram-Mac/tgs/voice_chat_set_reminder_to_mute.tgs new file mode 100644 index 0000000000..ceaf9ac175 --- /dev/null +++ b/Telegram-Mac/tgs/voice_chat_set_reminder_to_mute.tgs @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":34,"w":100,"h":100,"nm":"Set Reminder to Mute","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Body Bell","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.3,"y":0},"t":0,"s":[256.953,252.701,0],"to":[0,0,0],"ti":[0,0,0]},{"t":14,"s":[256.953,237.701,0]}],"ix":2},"a":{"a":0,"k":[0.953,-3.299,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.3,"y":0},"t":0,"s":[{"i":[[0,0],[-5.19,7.69],[-14.458,-14.946],[0,0],[0,0],[0,0],[0,8.284],[-3.099,2.841],[0,0],[-1.366,19.12]],"o":[[0,0],[3.061,-4.535],[0.667,0.69],[0,0],[0,0],[-8.284,0],[0,-4.205],[0,0],[14.13,-12.952],[0,0]],"v":[[-92.82,-53.2],[-85.81,-78.69],[95.611,100.99],[96.627,102.041],[62.565,102.2],[-121.425,102.2],[-136.425,87.2],[-131.561,76.143],[-121.99,67.37],[-97.86,17.427]],"c":true}]},{"i":{"x":0.708,"y":1},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[0,0],[0,0],[-5.169,-5.343],[0,0],[0,0],[10.151,1.638],[3.566,5.002],[-0.105,2.289],[0,0],[-0.016,13.307]],"o":[[0,0],[0,0],[0.239,0.247],[0,0],[0,0],[-9.938,-1.126],[-2.345,-2.845],[0,0],[-0.376,-7.35],[0,0]],"v":[[-72.027,-32.739],[-71.412,-42.007],[37.716,64.794],[44.043,71.121],[32.047,71.361],[-48.832,71.762],[-68.678,61.019],[-73.38,51.478],[-74.624,42.473],[-73.366,8.191]],"c":true}]},{"t":14,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[15.8,2.55],[5.55,3.175],[1.561,1.982],[0,0],[0.735,10.073]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[-10.859,-1.753],[-3.65,-2.088],[0,0],[-2.51,-4.37],[0,0]],"v":[[-59.678,-20.575],[-59.509,-20.427],[5.5,44.653],[14.781,53.915],[15.065,54.2],[-11.55,54.825],[-32.925,46.45],[-42.561,38.143],[-50.99,28.62],[-59.735,3.052]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.3,"y":0},"t":0,"s":[{"i":[[-22.091,0],[-1.305,-20.913],[0,0],[0,0],[-1.637,-22.924],[0,0],[-14.13,-12.952],[0,0],[5.292,-6.373],[0,0],[0,0],[3.476,5.117],[0,0],[-3.735,2.445],[-3.742,1.497],[0,0]],"o":[[21.242,0],[0,0],[0,0],[21.339,8.535],[0,0],[1.366,19.12],[0,0],[6.107,5.598],[-4.508,5.429],[0,0],[0,0],[2.577,-6.077],[3.062,-3.169],[3.292,-2.155],[0,0],[0,-22.091]],"v":[[0.013,-152.8],[39.934,-115.33],[40.013,-112.8],[55.126,-106.755],[92.69,-55.321],[97.886,17.427],[122.016,67.37],[131.587,76.143],[132.508,97.336],[119.1,102.185],[95.274,102.009],[-85.827,-78.548],[-75.89,-92.799],[-65.669,-101.254],[-55.1,-106.755],[-39.987,-112.8]],"c":true}]},{"i":{"x":0.708,"y":1},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[-22.069,0.791],[-5.439,-10.846],[-0.579,-1.063],[0,0],[-0.585,-8.196],[0,0],[-0.58,-5.848],[0,0],[0,0],[1.642,-3.362],[0,0],[1.243,1.829],[0,0],[-1.417,5.799],[-1.548,1.449],[0,0]],"o":[[22.845,-0.851],[0,0],[1.678,3.084],[5.054,15.432],[0,0],[0.488,6.836],[-0.171,0.686],[0,0],[0,0],[-3.359,6.878],[0,0],[0.198,-4.307],[1.095,-1.133],[1.246,-4.935],[1.349,-5.86],[9.68,-14.707]],"v":[[0.013,-153.683],[51.689,-125.53],[53.053,-122.519],[59.696,-107.808],[71.809,-55],[76.133,8.271],[77.08,28.972],[77.771,36.442],[77.663,49.131],[76.609,53.496],[55.533,59.628],[-68.948,-67.069],[-68.164,-81.433],[-64.775,-97.506],[-60.679,-111.228],[-54.18,-126.169]],"c":true}]},{"t":14,"s":[{"i":[[-22.057,1.231],[-4.809,-10.795],[-0.9,-1.655],[0,0],[0,0],[0,0],[0.266,-1.068],[0.706,-1.979],[1.304,-2.461],[0,0],[0,0],[0,0],[0,0],[-0.127,7.665],[-0.327,1.422],[0,0]],"o":[[23.737,-1.325],[0,0],[2.612,4.8],[3.374,12.255],[0,0],[0,0],[-0.266,1.068],[-0.774,2.17],[-1.755,3.311],[0,0],[0,0],[0,0],[0,0],[0.108,-6.482],[2.1,-9.12],[11.987,-15.95]],"v":[[0.013,-154.175],[50.059,-127.705],[52.138,-124.425],[59.126,-108.005],[60.19,-54.821],[60.136,3.177],[59.079,7.995],[56.774,15.518],[53.758,23.086],[46.35,34.185],[45.482,33.321],[-59.944,-70.02],[-59.973,-70.049],[-59.608,-94.643],[-58.725,-107.88],[-49.237,-128.55]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":15,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Bottom Bell","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256.013,393.681,0],"ix":2},"a":{"a":0,"k":[0.013,137.681,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.4,"y":0},"t":1,"s":[{"i":[[0,0],[9.089,-5.522],[9.491,0],[7.496,4.484],[4.121,10.048]],"o":[[-4.078,9.943],[-7.559,4.592],[-9.37,0],[-9.226,-5.52],[0,0]],"v":[[46.292,122.161],[25.937,145.963],[0.013,153.2],[-25.62,146.138],[-46.267,122.161]],"c":true}]},{"t":10,"s":[{"i":[[0,0],[0.194,-5.092],[6.738,-0.243],[0.19,7.608],[0.04,8.897]],"o":[[0.142,9.195],[-0.316,8.283],[-6.971,0.252],[-0.135,-5.403],[0,0]],"v":[[10,78.286],[10.066,143.342],[-0.112,155.2],[-10.065,143.642],[-9.974,78.286]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":10,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Piece Micro","parent":5,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[35.717,87.391,0],"ix":2},"a":{"a":0,"k":[35.717,87.391,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":7,"s":[{"i":[[0,0],[-1.044,0.234],[0,0],[3.425,-0.422]],"o":[[1.079,-0.298],[0,0],[-3.888,2.204],[0,0]],"v":[[13.921,81.923],[20.263,80.547],[34.826,96.484],[18.575,101.297]],"c":true}]},{"t":9,"s":[{"i":[[0,0],[-5.475,2.553],[0,0],[9.717,-2.249]],"o":[[6.02,-1.382],[0,0],[-8.509,4.686],[0,0]],"v":[[19.671,80.673],[36.95,74.735],[51.763,89.546],[24.325,100.047]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":7,"op":180,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Arc Micro R","parent":5,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1,92,0],"ix":2},"a":{"a":0,"k":[1,92,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.29,"y":0},"t":18,"s":[{"i":[[0.267,-5.148],[14.135,-14.996]],"o":[[-1.559,30.052],[-17.225,18.274]],"v":[[97.5,-5.269],[71.365,63.505]],"c":false}]},{"t":28,"s":[{"i":[[0.267,-5.148],[14.849,-16.38]],"o":[[-1.482,28.566],[-17.722,19.55]],"v":[[97.5,-5.269],[71.726,62.502]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":8,"s":[99]},{"t":18,"s":[0]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":10,"op":180,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Arc Micro L","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.31,"y":0},"t":14,"s":[257,334,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.3,"y":0},"t":26,"s":[257,353,0],"to":[0,0,0],"ti":[0,0,0]},{"t":34,"s":[257,348,0]}],"ix":2},"a":{"a":0,"k":[1,92,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.29,"y":0},"t":18,"s":[{"i":[[40.641,-9.352],[8.57,0],[3.849,60.125]],"o":[[-7.86,1.809],[-52.469,0],[-0.359,-5.614]],"v":[[24.514,89.694],[-0.188,92.423],[-97.849,-4.773]],"c":false}]},{"t":28,"s":[{"i":[[40.641,-9.352],[8.57,0],[3.849,60.125]],"o":[[-7.86,1.809],[-52.469,0],[-0.359,-5.614]],"v":[[24.514,89.694],[-0.188,92.423],[-97.849,-4.773]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":5,"s":[15]},{"t":18,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.746],"y":[0.894]},"o":{"x":[0.444],"y":[0]},"t":5,"s":[12]},{"i":{"x":[0.323],"y":[1]},"o":{"x":[0.222],"y":[3.401]},"t":8,"s":[1]},{"t":18,"s":[0]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":7,"op":180,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Leg Micro","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[255.826,401.556,0],"ix":2},"a":{"a":0,"k":[-0.174,145.556,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.31,"y":0},"t":14,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,145.556],[0,87.306]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.3,"y":0},"t":26,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,145.556],[0,108.056]],"c":false}]},{"t":34,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,145.556],[0,96.556]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"t":0,"s":[0],"h":1},{"t":10,"s":[100],"h":1}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":10,"op":180,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Line Micro","parent":8,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-0.188,-4.773,0],"ix":2},"a":{"a":0,"k":[-0.188,-4.773,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.4,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-127.636,-134.81],[127.26,120.264]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.3,"y":0},"t":14,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-138.364,-144.046],[137.988,132.5]],"c":false}]},{"t":27,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-127.636,-133.31],[127.26,121.764]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.753],"y":[-0.072]},"o":{"x":[0.434],"y":[0]},"t":0,"s":[31]},{"i":{"x":[0.615],"y":[1]},"o":{"x":[0.512],"y":[0.317]},"t":6,"s":[28]},{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":14,"s":[0]},{"t":27,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.7],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":0,"s":[64]},{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":14,"s":[100]},{"t":27,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Body Micro","parent":5,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-0.007,55.352,0],"ix":2},"a":{"a":0,"k":[-0.007,55.352,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[5.374,0],[0,33.137]],"o":[[0,0],[0,0],[-4.956,1.325],[-33.137,0],[0,0]],"v":[[-60.015,-22.243],[15.085,52.868],[15.538,53.321],[0,55.352],[-60,-4.648]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[-33.137,0],[0,-33.137],[0,0],[6.989,-9.81]],"o":[[0,0],[0,-33.137],[33.137,0],[0,0],[0,12.962],[0,0]],"v":[[-60.015,-78.778],[-60,-94.648],[0,-154.648],[60,-94.648],[60,-4.648],[48.901,30.126]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":14,"op":180,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Pre-comp 1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[13.6,13.6,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":180,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/tgs/voice_chat_set_reminder_to_raise_hand.tgs b/Telegram-Mac/tgs/voice_chat_set_reminder_to_raise_hand.tgs new file mode 100644 index 0000000000..58c04ee532 --- /dev/null +++ b/Telegram-Mac/tgs/voice_chat_set_reminder_to_raise_hand.tgs @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":36,"w":100,"h":100,"nm":"Set Reminder to Rise Hand","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Bell","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.3,"y":0},"t":0,"s":[255.967,230.7,0],"to":[0,0,0],"ti":[0,0,0]},{"t":12,"s":[255.967,270.7,0]}],"ix":2},"a":{"a":0,"k":[-0.033,-25.3,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.3,"y":0},"t":0,"s":[{"i":[[0,0],[-6.45,6.7],[0,0],[0,0],[0,0],[0,8.3],[-3.1,2.9],[0,0],[-1.3,19.1]],"o":[[0,0],[4.342,-4.51],[0,0],[0,0],[-8.3,0],[0,-4.2],[0,0],[14.1,-13],[0,0]],"v":[[-92.8,-53.2],[-85.3,-79.7],[92.007,101.057],[93.1,102.15],[-121.4,102.2],[-136.4,87.2],[-131.5,76.1],[-121.9,67.3],[-97.8,17.4]],"c":true}]},{"i":{"x":0.39,"y":1},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[0,0],[0,0],[0,0],[38.659,-1.562],[4.281,3.01],[1.12,7.93],[-0.341,2.329],[0,0],[-1.3,19.1]],"o":[[0,0],[0,0],[0,0],[-6.977,0.282],[-10.666,-1.589],[-0.564,-3.993],[0,0],[0.484,-12.336],[0,0]],"v":[[-57.671,-46.497],[-56.163,-45.057],[73.792,78.988],[41.341,102.165],[-39.334,101.192],[-58.438,81.628],[-58.493,71.738],[-58.984,62.939],[-58.394,17.677]],"c":true}]},{"t":12,"s":[{"i":[[0,0],[0,0],[0,0],[43.698,-1.448],[12.406,4.131],[3.267,7.372],[0.556,1.97],[0,0],[-1.446,18.138]],"o":[[0,0],[0,0],[0,0],[-11.229,0.372],[-6.46,-2.151],[-1.641,-3.704],[0,0],[-0.111,-11.633],[0,0]],"v":[[-48.211,-24.391],[-46.854,-23.243],[70.073,75.677],[33.802,102.243],[-28.226,99.869],[-42.829,86.574],[-45.443,77.695],[-46.584,69.338],[-47.029,22.141]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.3,"y":0},"t":0,"s":[{"i":[[-22.1,0],[-1.3,-20.9],[0,0],[0,0],[-1.7,-22.9],[0,0],[-14.1,-12.9],[0,0],[5.3,-6.4],[0,0],[0,0],[4.852,6.303],[0,0],[-8,3.2],[0,0]],"o":[[21.2,0],[0,0],[0,0],[21.3,8.5],[0,0],[1.4,19.1],[0,0],[6.1,5.6],[-4.5,5.4],[0,0],[0,0],[3.602,-7.072],[5.8,-6],[0,0],[0,-21.9]],"v":[[0,-152.8],[39.9,-115.3],[40,-112.8],[55.1,-106.8],[92.7,-55.4],[97.9,17.3],[122,67.2],[131.6,76],[132.5,97.2],[119.1,102],[93.113,102.137],[-87.102,-77.178],[-75.9,-93],[-55.1,-107],[-40,-113]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[-22.1,-0.104],[-6.168,-8.194],[0,0],[-3.244,-0.001],[-0.946,-18.459],[0,0],[-5.917,-11.49],[0,0],[3.068,-6.167],[0,0],[0,0],[2.031,2.639],[0,0],[-8.075,2.904],[0,0]],"o":[[17.13,0.067],[0,0],[0,0],[17.535,3.56],[0,0],[0.877,14.317],[0,0],[2.525,5.309],[-2.81,11.903],[0,0],[0,0],[0.775,-6.252],[2.763,-5.714],[0,0],[5.639,-8.786]],"v":[[1.047,-95.365],[38.314,-80.591],[41.415,-80.164],[54.853,-78.37],[81.446,-42.437],[84.736,15.994],[96.143,58.262],[100.611,66.99],[101.665,86.92],[82.896,100.639],[63.543,102.152],[-72.15,-51.574],[-69.638,-62.612],[-51.188,-76.964],[-39.389,-79.31]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[-22.099,-0.179],[-9.673,0.954],[0,0],[-5.58,-0.001],[-0.404,-15.261],[0,0],[-0.026,-10.474],[0,0],[1.462,-6],[0,0],[0,0],[0,0],[0,0],[-8.129,2.691],[0,0]],"o":[[14.199,0.115],[0,0],[0,0],[14.825,0.003],[0,0],[0.501,10.873],[0,0],[-0.049,5.1],[-1.594,16.585],[0,0],[0,0],[0,0],[-2.61,-4.059],[0,0],[9.699,0.657]],"v":[[1.801,-54.012],[37.173,-55.601],[42.433,-56.666],[54.675,-57.9],[73.342,-33.104],[75.259,15.053],[77.526,51.827],[78.299,60.503],[75.594,79.518],[57.904,98.584],[57.304,97.862],[-56.439,-38.944],[-57.39,-40.088],[-48.371,-55.338],[-38.949,-55.054]],"c":true}]},{"i":{"x":0.39,"y":1},"o":{"x":0.167,"y":0.167},"t":8,"s":[{"i":[[-22.099,-0.225],[-8.365,1.104],[0,0],[0,0],[-0.079,-13.344],[0,0],[0.751,-7.994],[0,0],[0.499,-5.899],[0,0],[0,0],[0,0],[0,0],[-5.57,2.731],[0,0]],"o":[[27.812,0.283],[0,0],[0,0],[6.707,1.409],[0,0],[0.276,8.809],[0,0],[0.63,3.135],[-0.176,10.718],[0,0],[0,0],[0,0],[0.584,-3.973],[2.662,-1.306],[8.129,-0.583]],"v":[[-0.562,-42.244],[44.865,-46.065],[46.667,-46.335],[55.315,-46.381],[65.736,-28.011],[68.079,14.49],[68.249,48.533],[67.809,59.178],[66.702,71.892],[61.822,89.407],[61.245,88.756],[-51.953,-29.099],[-52.117,-36.757],[-43.93,-47.443],[-35.437,-46.835]],"c":true}]},{"t":12,"s":[{"i":[[-24.589,0],[-6.676,-0.546],[0,0],[0,0],[0.195,-11.253],[0,0],[-0.278,-10.042],[0,0],[-0.28,-5.528],[0,0],[0,0],[0,0],[0,0],[-0.278,0.712],[0,0]],"o":[[23.587,0],[0,0],[0,0],[4.923,0.285],[0,0],[0.111,6.837],[0,0],[-0.25,2.612],[0.556,10.968],[0,0],[0,0],[0,0],[-0.257,-3.472],[0,0],[9.763,0]],"v":[[-2.357,-26.148],[40.784,-26.029],[44.372,-26.148],[52.55,-26.267],[70.352,-16.747],[70.852,18.58],[71.381,48.28],[71.075,55.093],[71.103,68.104],[69.406,86.076],[68.794,85.55],[-47.137,-14.021],[-48.106,-14.853],[-47.946,-25.745],[-46.723,-26.932]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":13,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Bottom Bell","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.39,"y":1},"o":{"x":0.3,"y":0},"t":0,"s":[0,137.7,0],"to":[0,0,0],"ti":[0,0,0]},{"t":12,"s":[12,91.7,0]}],"ix":2},"a":{"a":0,"k":[0,137.7,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.39,"y":1},"o":{"x":0.3,"y":0},"t":0,"s":[{"i":[[0,0],[20.9,0],[7.5,18.2]],"o":[[-7.5,18.2],[-20.9,0],[0,0]],"v":[[46.3,122.2],[0,153.2],[-46.3,122.2]],"c":true}]},{"t":12,"s":[{"i":[[0,0],[36.5,-0.45],[3.3,26.05]],"o":[[-1.55,18.55],[-20.898,0.258],[0,0]],"v":[[52.1,115.7],[-1.45,138.95],[-51,115.2]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":13,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Head","parent":5,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.39,"y":1},"o":{"x":0.3,"y":0},"t":0,"s":[0.613,-101.537,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.39,"y":1},"o":{"x":0.3,"y":0},"t":13,"s":[11.613,-40.537,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.39,"y":1},"o":{"x":0.3,"y":0},"t":26,"s":[11.613,-89.537,0],"to":[0,0,0],"ti":[0,0,0]},{"t":36,"s":[11.613,-84.037,0]}],"ix":2},"a":{"a":0,"k":[11.613,-84.537,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.3,"y":0},"t":0,"s":[{"i":[[0,-19.31],[19.31,0],[0,19.31],[-19.31,0]],"o":[[0,19.31],[-19.31,0],[0,-19.31],[19.31,0]],"v":[[46.576,-84.537],[11.613,-49.574],[-23.35,-84.537],[11.613,-119.5]],"c":true}]},{"i":{"x":0.39,"y":1},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[0,-25.538],[22.395,0],[0,25.538],[-22.395,0]],"o":[[0,25.538],[-22.395,0],[0,-25.538],[22.395,0]],"v":[[49.653,-76.337],[9.103,-30.097],[-31.446,-76.337],[9.103,-122.577]],"c":true}]},{"i":{"x":0.39,"y":1},"o":{"x":0.3,"y":0},"t":14,"s":[{"i":[[0,-24.735],[24.735,0],[0,24.735],[-24.735,0]],"o":[[0,24.735],[-24.735,0],[0,-24.735],[24.735,0]],"v":[[56.4,-84.537],[11.613,-39.75],[-33.174,-84.537],[11.613,-129.324]],"c":true}]},{"t":25,"s":[{"i":[[0,-26.924],[26.924,0],[0,26.924],[-26.924,0]],"o":[[0,26.924],[-26.924,0],[0,-26.924],[26.924,0]],"v":[[60.363,-84.537],[11.613,-35.787],[-37.137,-84.537],[11.613,-133.287]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Hands","parent":5,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.05,"y":0},"t":9,"s":[11.336,12.774,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.31,"y":0},"t":12,"s":[11.336,18.274,0],"to":[0,0,0],"ti":[0,0,0]},{"t":25,"s":[11.336,10.774,0]}],"ix":2},"a":{"a":0,"k":[11.336,10.774,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.3,"y":0},"t":7,"s":[{"i":[[0,0],[0.875,44.36],[0.012,11.61]],"o":[[0,0],[-0.348,-17.658],[-0.016,-14.955]],"v":[[-33.583,109.659],[-40.875,50.64],[-45.012,-17.11]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":9,"s":[{"i":[[0,0],[-7.48,30.25],[-7.964,9.648]],"o":[[0,0],[2.73,-11.373],[7.228,-11.196]],"v":[[-57.163,97.688],[-57.996,36.651],[-42.036,0.102]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[0,0],[-14.611,30.379],[-23.925,1.795]],"o":[[0,0],[7.389,-12.121],[13.171,-8.112]],"v":[[-77.526,99.758],[-81.889,29.906],[-41.075,4.99]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[0,0],[-27.718,19.589],[-10.136,-1.028]],"o":[[0,0],[21.782,-16.411],[19.114,-5.028]],"v":[[-107.888,77.829],[-93.782,16.161],[-42.114,8.877]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13,"s":[{"i":[[0,0],[-27.325,4.127],[-11.558,-0.909]],"o":[[0,0],[20.334,-3.734],[35.444,0.066]],"v":[[-136.225,32.858],[-90.834,7.893],[-39.525,9.472]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":14,"s":[{"i":[[0,0],[-27.129,-3.603],[-14.269,-4.157]],"o":[[0,0],[19.61,2.605],[43.609,2.613]],"v":[[-147.393,-5.627],[-89.61,-2.992],[-38.231,9.77]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":18,"s":[{"i":[[0,0],[-38.825,-22.336],[-19.04,-2.789]],"o":[[0,0],[14.675,9.164],[62.76,8.586]],"v":[[-144.801,-73.381],[-91.175,-7.197],[-39.46,8.757]],"c":false}]},{"t":28,"s":[{"i":[[0,0],[-46.458,-35.583],[-16.613,-2.875]],"o":[[0,0],[8.352,6.397],[79.143,13.697]],"v":[[-142.583,-131.341],[-87.875,-16.36],[-40.512,7.452]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.3,"y":0},"t":7,"s":[{"i":[[0,0],[0.583,25.777],[4.5,19.11]],"o":[[0,0],[-0.238,-10.518],[-18.41,-78.181]],"v":[[63,101.64],[63.167,54.973],[56,-16.61]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":9,"s":[{"i":[[0,0],[2.455,25.149],[6.868,13.265]],"o":[[0,0],[-0.66,-9.745],[-29.265,-65.813]],"v":[[67.548,109.42],[68.368,48.723],[59.021,2.29]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":11,"s":[{"i":[[-11.495,10.995],[15.537,39.692],[13.754,5.326]],"o":[[14.034,-13.424],[-3.378,-8.551],[-47.075,-45.52]],"v":[[79.466,115.674],[91.463,38.058],[62.996,10.174]],"c":false}]},{"i":{"x":0.3,"y":1},"o":{"x":0.167,"y":0.167},"t":14,"s":[{"i":[[-16.063,9.648],[30.097,36.471],[12.946,2.18]],"o":[[16.791,-10.086],[-5.422,-7.653],[-60.473,-30.253]],"v":[[92.709,118.199],[93.903,31.642],[65.054,12.67]],"c":false}]},{"t":25,"s":[{"i":[[0,0],[46.583,35.527],[11.5,1.167]],"o":[[0,0],[-8.365,-6.38],[-79.91,-8.107]],"v":[[140.625,129.765],[94.667,23.223],[63.5,11.89]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":7,"op":180,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Body","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.3,"y":0},"t":12,"s":[266.676,398.378,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.4,"y":1},"o":{"x":0.3,"y":0},"t":23,"s":[266.676,387.628,0],"to":[0,0,0],"ti":[0,0,0]},{"t":34,"s":[266.676,392.378,0]}],"ix":2},"a":{"a":0,"k":[10.676,136.378,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.69,"y":1},"o":{"x":0.31,"y":0},"t":12,"s":[{"i":[[-12.288,0.098],[-12.498,-0.987],[0,0],[3.339,-1.222],[4.897,-9.213],[22.369,0],[7.456,13.82],[-1.892,54.808]],"o":[[8.847,-0.071],[35.724,2.82],[24.706,0],[0,0],[-7.456,13.82],[-22.369,0],[-4.897,-9.213],[-12.353,-3.008]],"v":[[-46.407,7.071],[-18.334,9.585],[61.794,9.679],[70.141,28.952],[68.805,121.176],[13.161,136.5],[-42.484,121.176],[-47.047,25.191]],"c":true}]},{"t":23,"s":[{"i":[[-10.795,-2.304],[-8.2,-0.397],[0,0],[3,-1.29],[4.4,-9.722],[20.1,0],[6.7,14.583],[-1.7,57.835]],"o":[[7.9,1.686],[0,0],[22.2,0],[0,0],[-7.044,16.233],[-20.1,0],[-4.4,-9.722],[-11.1,-3.174]],"v":[[-40.2,-2.318],[-15.1,1.451],[56.9,1.55],[64.4,21.887],[63.044,118.267],[13.2,135.375],[-36.8,119.205],[-40.9,17.919]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":12,"op":180,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Bell EXAMPLE","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":1,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256.001,256.2,0],"ix":2},"a":{"a":0,"k":[0.001,0.2,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-22.091,0],[-1.305,-20.913],[0,0],[0,0],[-1.637,-22.924],[0,0],[-14.13,-12.952],[0,0],[5.598,-6.107],[4.205,0],[0,0],[0,8.284],[-3.099,2.841],[0,0],[-1.366,19.12],[0,0],[-21.339,8.535],[0,0]],"o":[[21.242,0],[0,0],[0,0],[21.339,8.535],[0,0],[1.366,19.12],[0,0],[6.107,5.598],[-2.841,3.099],[0,0],[-8.284,0],[0,-4.205],[0,0],[14.13,-12.952],[0,0],[1.637,-22.924],[0,0],[0,-22.091]],"v":[[0.001,-152.8],[39.922,-115.33],[40.001,-112.8],[55.114,-106.755],[92.678,-55.321],[97.874,17.427],[122.004,67.37],[131.575,76.143],[132.496,97.336],[121.439,102.2],[-121.437,102.2],[-136.437,87.2],[-131.573,76.143],[-122.003,67.37],[-97.873,17.427],[-92.676,-55.321],[-55.112,-106.755],[-39.999,-112.8]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[20.903,0],[7.469,18.212]],"o":[[-7.469,18.212],[-20.903,0],[0,0]],"v":[[46.28,122.161],[0.001,153.2],[-46.279,122.161]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":180,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Pre-comp 1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":0,"s":[50,50,0],"to":[0,0,0],"ti":[0,0,0]},{"t":10,"s":[48.25,50,0]}],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[13.5,13.5,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":180,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/tgs/voice_chat_start_chat_to_mute.tgs b/Telegram-Mac/tgs/voice_chat_start_chat_to_mute.tgs new file mode 100644 index 0000000000..c7c61e6f60 --- /dev/null +++ b/Telegram-Mac/tgs/voice_chat_start_chat_to_mute.tgs @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":60,"ip":6,"op":36,"w":100,"h":100,"nm":"Start Chat to Mute","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Play","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.3,"y":0},"t":6,"s":[284.526,256,0],"to":[0,0,0],"ti":[0,0,0]},{"t":13,"s":[271.526,256,0]}],"ix":2},"a":{"a":0,"k":[2.526,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.27,"y":0},"t":6,"s":[{"i":[[0,-3.846],[0,0],[-11.018,0],[-3.229,2.089],[0,0],[17.559,10.888],[0,0],[5.986,-9.25]],"o":[[0,0],[0,11.018],[3.846,0],[0,0],[9.25,-5.986],[0,0],[-9.25,-5.986],[-2.089,3.229]],"v":[[-117.174,-128.328],[-117.174,128.33],[-97.224,148.28],[-86.386,145.08],[111.941,16.751],[111.941,-16.748],[-86.386,-145.077],[-113.973,-139.166]],"c":true}]},{"t":16,"s":[{"i":[[5.458,-6],[-7.491,-14.817],[-5.586,-2.653],[-14.913,2.075],[-5.47,16.102],[4.303,6.955],[15.227,4.643],[4.738,-1.858]],"o":[[-15.584,17.13],[4.767,9.428],[5.586,2.653],[17.094,-2.379],[3.464,-10.197],[-6.685,-10.805],[-10.357,-3.158],[-1.052,0.413]],"v":[[-39.838,-58.183],[-45.609,-4.008],[-25.47,15.22],[2.016,17.925],[37.223,-11.289],[34.49,-46.953],[3.231,-70.092],[-25.229,-68.355]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":12,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Arc Micro R","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1,92,0],"ix":2},"a":{"a":0,"k":[1,92,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.31,"y":0},"t":14,"s":[{"i":[[0.267,-5.148],[14.849,-16.38]],"o":[[-1.482,28.566],[-17.722,19.55]],"v":[[97.5,-5.269],[71.726,62.502]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.31,"y":0},"t":21,"s":[{"i":[[0.267,-5.681],[14.849,-18.078]],"o":[[-1.482,31.526],[-17.722,21.576]],"v":[[97.5,-14.319],[71.726,60.475]],"c":false}]},{"t":29,"s":[{"i":[[0.267,-5.148],[14.849,-16.38]],"o":[[-1.482,28.566],[-17.722,19.55]],"v":[[97.5,-5.269],[71.726,62.502]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.31],"y":[0]},"t":14,"s":[99]},{"t":21,"s":[0]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":16,"op":194,"st":14,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Arc Micro L","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.31,"y":0},"t":9,"s":[257,348,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.5,"y":1},"o":{"x":0.3,"y":0},"t":19,"s":[257,341.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":28,"s":[257,354,0],"to":[0,0,0],"ti":[0,0,0]},{"t":35,"s":[257,348,0]}],"ix":2},"a":{"a":0,"k":[1,92,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":11,"s":[{"i":[[40.641,-9.352],[8.57,0],[3.849,60.125]],"o":[[-7.86,1.809],[-52.469,0],[-0.359,-5.614]],"v":[[24.514,89.694],[-0.188,92.423],[-97.849,-4.773]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.31,"y":0},"t":14,"s":[{"i":[[35.551,-21.801],[8.57,0],[3.849,60.125]],"o":[[-16.514,10.127],[-52.469,0],[-0.359,-5.614]],"v":[[40.514,81.694],[-0.188,92.423],[-97.849,-4.773]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":15,"s":[{"i":[[29.284,-21.092],[8.57,0],[3.849,60.821]],"o":[[-28.716,26.408],[-52.469,0],[-0.359,-5.679]],"v":[[66.716,65.893],[-0.188,92.423],[-97.849,-5.899]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.167,"y":0.167},"t":16,"s":[{"i":[[37.078,-18.356],[8.57,0],[3.849,61.987]],"o":[[-33.464,33.33],[-52.469,0],[-0.359,-5.788]],"v":[[70.464,63.509],[-0.188,92.423],[-97.849,-7.783]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.31,"y":0},"t":21,"s":[{"i":[[26.615,-32.401],[8.57,0],[3.849,66.332]],"o":[[-26.514,32.278],[-52.469,0],[-0.359,-6.194]],"v":[[72.014,60.412],[-0.188,92.423],[-97.849,-14.806]],"c":false}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[20.637,-21.652],[8.57,0],[3.849,65.787]],"o":[[-30.066,31.503],[-52.469,0],[-0.359,-6.143]],"v":[[70.066,62.273],[-0.188,92.423],[-97.849,-13.926]],"c":false}]},{"i":{"x":0.4,"y":1},"o":{"x":0.167,"y":0.167},"t":23,"s":[{"i":[[10.633,-3.661],[8.57,0],[3.849,64.875]],"o":[[-8.367,2.839],[-52.469,0],[-0.359,-6.058]],"v":[[27.367,88.784],[-0.188,92.423],[-97.849,-12.452]],"c":false}]},{"t":29,"s":[{"i":[[40.641,-9.352],[8.57,0],[3.849,60.125]],"o":[[-7.86,1.809],[-52.469,0],[-0.359,-5.614]],"v":[[24.514,89.694],[-0.188,92.423],[-97.849,-4.773]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.31],"y":[0]},"t":11,"s":[15]},{"t":21,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.746],"y":[0.894]},"o":{"x":[0.444],"y":[0]},"t":11,"s":[12]},{"t":14,"s":[1]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":13,"op":194,"st":14,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Piece Micro 2","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[35.717,87.391,0],"ix":2},"a":{"a":0,"k":[35.717,87.391,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":21,"s":[{"i":[[0,0],[-1.044,0.234],[0,0],[3.425,-0.422]],"o":[[1.079,-0.298],[0,0],[-3.888,2.204],[0,0]],"v":[[13.921,81.923],[20.263,80.547],[34.826,96.484],[18.575,101.297]],"c":true}]},{"t":23,"s":[{"i":[[0,0],[-5.475,2.553],[0,0],[9.717,-2.249]],"o":[[6.02,-1.382],[0,0],[-8.509,4.686],[0,0]],"v":[[19.671,80.673],[36.95,74.735],[51.763,89.546],[24.325,100.047]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":21,"op":194,"st":14,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Leg Micro","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256,401.556,0],"ix":2},"a":{"a":0,"k":[0,145.556,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.31,"y":0},"t":11,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,91.806],[0,91.806]],"c":false}]},{"i":{"x":0.5,"y":1},"o":{"x":0.3,"y":0},"t":19,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,145.556],[0,89.056]],"c":false}]},{"i":{"x":0.7,"y":1},"o":{"x":0.3,"y":0},"t":28,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,145.556],[0,103.556]],"c":false}]},{"t":34,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,145.556],[0,96.556]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":13,"op":192,"st":12,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Line Micro","parent":7,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-0.188,-6.273,0],"ix":2},"a":{"a":0,"k":[-0.188,-4.773,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.5,"y":0},"t":15,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-145.038,-149.908],[109.857,105.166]],"c":false}]},{"t":29,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-127.636,-132.31],[127.26,122.764]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.5],"y":[0]},"t":15,"s":[0]},{"t":29,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":17,"op":191,"st":11,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Body Micro","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.3,"y":1},"o":{"x":0.3,"y":0},"t":6,"s":[25.993,55.352,0],"to":[0,0,0],"ti":[0,0,0]},{"t":13,"s":[-0.007,55.352,0]}],"ix":2},"a":{"a":0,"k":[-0.007,55.352,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.4,0.4,0.4],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":10,"s":[100,100,100]},{"i":{"x":[0.5,0.5,0.5],"y":[1,1,1]},"o":{"x":[0.3,0.3,0.3],"y":[0,0,0]},"t":19,"s":[100,97,100]},{"t":28,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.31,"y":0},"t":10,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[8.767,0.121],[6.988,23.606]],"o":[[0,0],[0,0],[0,0],[-4.064,3.946],[-25.506,-0.352],[-4.3,-14.525]],"v":[[-53,-40.934],[-4.651,0.079],[-1.575,2.689],[15.064,16.804],[-6.494,24.102],[-53.238,-12.106]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.3,"y":0},"t":19,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[12.25,-0.102],[0,35.875]],"o":[[0,0],[0,0],[0,0],[-9.761,16.347],[-31.747,0.264],[0,0]],"v":[[-57.5,-68.155],[14.785,3.986],[19.384,8.575],[44.261,33.403],[0,55.352],[-57.486,-9.606]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,0],[-2.234,0.332],[-3.724,3.577],[0,0],[11.728,-0.094],[0,35.667]],"o":[[0,0],[4.522,-0.672],[0.597,0.444],[-8.912,13.898],[-31.853,0.244],[0,0]],"v":[[-57.644,-36.273],[-50.397,-35.583],[-39.776,-40.769],[41.238,36.487],[0.047,55.399],[-57.629,-9.182]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":21,"s":[{"i":[[0,0],[0,0],[-11.797,9.173],[0,0],[10.872,-0.082],[0,35.326]],"o":[[0,0],[0,0],[1.578,1.173],[-7.518,9.879],[-32.026,0.211],[0,0]],"v":[[-58.02,-26.216],[-18.373,11.067],[5.922,9.893],[37.268,40.061],[0.125,55.477],[-57.865,-8.487]],"c":true}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":22,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[9.879,-0.067],[0,34.931]],"o":[[0,0],[0,0],[0,0],[-4.108,3.006],[-32.226,0.173],[0,0]],"v":[[-58.484,-23.324],[-5.499,29.841],[-2.128,33.223],[16.108,51.52],[0,55.352],[-58.353,-7.896]],"c":true}]},{"t":28,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[5.374,0],[0,33.137]],"o":[[0,0],[0,0],[0,0],[-4.956,1.325],[-33.137,0],[0,0]],"v":[[-60.015,-22.243],[-6.347,31.433],[-2.932,34.848],[15.538,53.321],[0,55.352],[-60,-4.648]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.4,"y":1},"o":{"x":0.31,"y":0},"t":10,"s":[{"i":[[0,0],[0,0],[-26.76,1.093],[-4.75,-11.292],[3,-11.644],[13.971,-6.587],[0,0],[0,0]],"o":[[0,0],[3.363,-11.583],[24.494,-1],[4.75,11.292],[-2.509,9.739],[0,0],[0,0],[0,0]],"v":[[-53.375,-40.479],[-51.613,-47.917],[-9.244,-76.5],[38.25,-46.292],[41.5,-11.856],[15.279,17.087],[1.971,5.927],[1.058,5.162]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.3,"y":0},"t":19,"s":[{"i":[[0,0],[0,0],[-31.748,0],[0,-35.875],[0,0],[8.149,-10.542],[0,0],[0,0]],"o":[[0,0],[0,-35.875],[31.748,0],[0,0],[0,14.033],[0,0],[0,0],[0,0]],"v":[[-57.5,-66.361],[-57.486,-107.042],[0,-172],[57.485,-107.042],[57.485,-9.606],[44.101,33.792],[24.405,14.377],[23.054,13.045]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,0],[0,0],[-31.838,0],[0,-35.699],[0,0],[10.633,-10.802],[0.114,0.124],[1.827,3.027]],"o":[[0,0],[0,-35.699],[31.838,0],[0,0],[0,13.964],[0,0],[2.102,-1.438],[-0.562,-0.931]],"v":[[-57.662,-67.159],[-57.647,-106.246],[0,-170.885],[57.647,-106.246],[57.647,-9.287],[41.002,36.728],[-39.928,-40.63],[-40.452,-49.157]],"c":true}]},{"i":{"x":0.5,"y":1},"o":{"x":0.167,"y":0.167},"t":21,"s":[{"i":[[0,0],[0,0],[-31.984,0],[0,-35.41],[0,0],[14.709,-11.229],[0.3,0.327],[1.835,2.794]],"o":[[0,0],[0,-35.41],[31.984,0],[0,0],[0,13.852],[0,0],[5.55,-3.798],[-1.577,-2.401]],"v":[[-57.927,-68.468],[-57.912,-104.939],[0,-169.055],[57.912,-104.939],[57.912,-8.764],[35.916,41.545],[5.7,9.739],[10.165,-1.353]],"c":true}]},{"t":28,"s":[{"i":[[0,0],[0,0],[-33.137,0],[0,-33.137],[0,0],[6.989,-9.81],[0,0],[0,0]],"o":[[0,0],[0,-33.137],[33.137,0],[0,0],[0,12.962],[0,0],[0,0],[0,0]],"v":[[-60.015,-78.778],[-60,-94.648],[0,-154.648],[60,-94.648],[60,-4.648],[48.901,30.126],[27.787,9.015],[26.339,7.567]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":11,"op":223,"st":43,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Pre-comp 1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[13.6,13.6,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":180,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/tgs/voice_chat_unmute.tgs b/Telegram-Mac/tgs/voice_chat_unmute.tgs new file mode 100644 index 0000000000..e5f0e48066 --- /dev/null +++ b/Telegram-Mac/tgs/voice_chat_unmute.tgs @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":10,"w":100,"h":100,"nm":"Unmute","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Line Micro","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-0.188,-4.773,0],"ix":2},"a":{"a":0,"k":[-0.188,-4.773,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-127.636,-133.31],[127.26,121.764]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.3],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[100]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":9,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Piece Micro","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[35.717,87.391,0],"ix":2},"a":{"a":0,"k":[35.717,87.391,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-5.475,2.553],[0,0],[9.717,-2.249]],"o":[[6.02,-1.382],[0,0],[-8.509,4.686],[0,0]],"v":[[19.671,80.673],[36.95,74.735],[51.763,89.546],[24.325,100.047]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":6,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Arc Micro R","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1,92,0],"ix":2},"a":{"a":0,"k":[1,92,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":5.523,"s":[{"i":[[0.267,-5.148],[14.849,-16.38]],"o":[[-1.482,28.566],[-17.722,19.55]],"v":[[97.5,-5.269],[71.726,62.502]],"c":false}]},{"t":6,"s":[{"i":[[0.267,-5.148],[43.123,-20.509]],"o":[[-1.482,28.566],[-23.829,11.333]],"v":[[97.5,-5.269],[42.599,83.623]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":6,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Arc Micro L","parent":5,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1,92,0],"ix":2},"a":{"a":0,"k":[1,92,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[40.641,-9.352],[8.57,0],[3.849,60.125]],"o":[[-7.86,1.809],[-52.469,0],[-0.359,-5.614]],"v":[[24.514,89.694],[-0.188,92.423],[-97.849,-4.773]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":0,"k":100,"ix":1},"e":{"a":0,"k":0,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":6,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Leg Micro","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[255.5,401.507,0],"ix":2},"a":{"a":0,"k":[-0.174,145.556,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0,145.556],[0,96.556]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":6,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Body Micro","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-0.007,55.352,0],"ix":2},"a":{"a":0,"k":[-0.007,55.352,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[5.374,0],[0,33.137]],"o":[[0,0],[0,0],[0,0],[-4.956,1.325],[-33.137,0],[0,0]],"v":[[-60.015,-22.243],[-50.629,-12.855],[-49.278,-11.504],[15.538,53.321],[0,55.352],[-60,-4.648]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[5.374,0],[0,33.137]],"o":[[0,0],[0,0],[0,0],[-4.764,3.566],[-33.137,0],[0,0]],"v":[[-59.955,-21.988],[-47.573,-9.44],[-45.83,-7.707],[15.339,52.233],[0,55.352],[-60,-4.648]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[0,0],[0,0],[-13.345,-16.369],[0,0],[5.374,0],[0,33.137]],"o":[[0,0],[0,0],[4.149,5.09],[-7.264,2.566],[-33.137,0],[0,0]],"v":[[-59.94,-53.988],[-0.323,-2.69],[-11.83,21.918],[17.589,52.233],[0,55.352],[-60,-4.648]],"c":true}]},{"t":5,"s":[{"i":[[0,0],[0,0],[0,0],[0,0],[5.374,0],[0,33.137]],"o":[[0,0],[0,0],[0,0],[-15.031,11.505],[-33.137,0],[0,0]],"v":[[-59.94,-53.988],[-47.823,-41.94],[-46.08,-40.207],[37.589,42.983],[0,55.352],[-60,-4.648]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":2,"s":[{"i":[[0,0],[0,0],[-33.137,0],[0,-33.137],[0,0],[6.989,-9.81],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,-33.137],[33.137,0],[0,0],[0,12.962],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-60.015,-78.778],[-60,-94.648],[0,-154.648],[60,-94.648],[60,-4.648],[48.901,30.126],[22.849,4.077],[21.523,2.751],[-45.54,-64.305],[-52.962,-71.726]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":3,"s":[{"i":[[0,0],[0,0],[-33.137,0],[0,-33.137],[0,0],[17.315,-12.056],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,-33.137],[33.137,0],[0,0],[0,12.962],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-59.762,-52.912],[-60,-94.648],[0,-154.648],[60,-94.648],[60,-4.648],[37.622,42.954],[14.328,20.023],[13.143,18.857],[-46.819,-40.172],[-53.456,-46.704]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[{"i":[[0,0],[0,0],[-33.137,0],[0,-33.137],[0,0],[17.315,-12.056],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,-33.137],[33.137,0],[0,0],[0,12.962],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-59.762,-52.912],[-60,-94.648],[0,-154.648],[60,-94.648],[60,-4.648],[37.622,42.954],[18.828,14.773],[17.643,13.607],[-46.819,-40.172],[-53.456,-46.704]],"c":true}]},{"t":5,"s":[{"i":[[0,0],[0,0],[-33.137,0],[0,-33.137],[0,0],[17.315,-12.056],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,-33.137],[33.137,0],[0,0],[0,12.962],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[-59.762,-52.912],[-60,-94.648],[0,-154.648],[60,-94.648],[60,-4.648],[37.622,42.954],[14.328,20.023],[13.143,18.857],[-46.819,-40.172],[-53.456,-46.704]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":6,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Micro EXAMPLE","parent":5,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.326,0.381,0],"ix":2},"a":{"a":0,"k":[0,0.332,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-33.137,0],[0,-33.137],[0,0],[33.137,0],[0,33.137],[0,0]],"o":[[33.137,0],[0,0],[0,33.137],[-33.137,0],[0,0],[0,-33.137]],"v":[[0,-154.648],[60,-94.648],[60,-4.648],[0,55.352],[-60,-4.648],[-60,-94.648]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[-5.501,0],[0,-5.501],[54.673,-5.031],[0,0],[5.501,0],[0.711,4.803],[0,0],[0,0],[0,55.991],[-5.501,0],[0,-5.501],[-48.347,0],[0,48.347]],"o":[[5.501,0],[0,55.986],[0,0],[0,5.501],[-5.001,0],[0,0],[0,0],[-54.68,-5.024],[0,-5.501],[5.501,0],[0,48.347],[48.347,0],[0,-5.501]],"v":[[97.5,-14.608],[107.46,-4.648],[9.973,102.355],[9.96,145.352],[0,155.312],[-9.852,146.824],[-9.96,145.352],[-9.958,102.357],[-107.46,-4.648],[-97.5,-14.608],[-87.54,-4.648],[0,82.892],[87.54,-4.648]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":6,"op":180,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Pre-comp 1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[50,50,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[13.6,13.6,100],"ix":6}},"ao":0,"w":512,"h":512,"ip":0,"op":180,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Telegram-Mac/tgs/wallet_success_created.tgs b/Telegram-Mac/tgs/wallet_success_created.tgs new file mode 100644 index 0000000000..456cc1e2fe Binary files /dev/null and b/Telegram-Mac/tgs/wallet_success_created.tgs differ diff --git a/Telegram-Mac/tgs/write_words.tgs b/Telegram-Mac/tgs/write_words.tgs new file mode 100644 index 0000000000..d0bd460e3f Binary files /dev/null and b/Telegram-Mac/tgs/write_words.tgs differ diff --git a/Telegram-Mac/uk.lproj/Localizable.strings b/Telegram-Mac/uk.lproj/Localizable.strings new file mode 100644 index 0000000000..ed60b89c47 Binary files /dev/null and b/Telegram-Mac/uk.lproj/Localizable.strings differ diff --git a/Telegram-Mac/uk.lproj/MainMenu.strings b/Telegram-Mac/uk.lproj/MainMenu.strings new file mode 100644 index 0000000000..8567b3ba83 --- /dev/null +++ b/Telegram-Mac/uk.lproj/MainMenu.strings @@ -0,0 +1,159 @@ + +/* Class = "NSMenuItem"; title = "TelegramMac"; ObjectID = "1Xt-HY-uBw"; */ +"1Xt-HY-uBw.title" = "TelegramMac"; + +/* Class = "NSMenuItem"; title = "Transformations"; ObjectID = "2oI-Rn-ZJC"; */ +"2oI-Rn-ZJC.title" = "Transformations"; + +/* Class = "NSMenu"; title = "Spelling"; ObjectID = "3IN-sU-3Bg"; */ +"3IN-sU-3Bg.title" = "Spelling"; + +/* Class = "NSMenuItem"; title = "Enter Full Screen"; ObjectID = "4J7-dP-txa"; */ +"4J7-dP-txa.title" = "Enter Full Screen"; + +/* Class = "NSMenuItem"; title = "Quit Telegram"; ObjectID = "4sb-4s-VLi"; */ +"4sb-4s-VLi.title" = "Quit Telegram"; + +/* Class = "NSMenuItem"; title = "Edit"; ObjectID = "5QF-Oa-p0T"; */ +"5QF-Oa-p0T.title" = "Edit"; + +/* Class = "NSMenuItem"; title = "About Telegram"; ObjectID = "5kV-Vb-QxS"; */ +"5kV-Vb-QxS.title" = "About Telegram"; + +/* Class = "NSMenuItem"; title = "Redo"; ObjectID = "6dh-zS-Vam"; */ +"6dh-zS-Vam.title" = "Redo"; + +/* Class = "NSMenuItem"; title = "Correct Spelling Automatically"; ObjectID = "78Y-hA-62v"; */ +"78Y-hA-62v.title" = "Correct Spelling Automatically"; + +/* Class = "NSMenuItem"; title = "Substitutions"; ObjectID = "9ic-FL-obx"; */ +"9ic-FL-obx.title" = "Substitutions"; + +/* Class = "NSMenuItem"; title = "Smart Copy/Paste"; ObjectID = "9yt-4B-nSM"; */ +"9yt-4B-nSM.title" = "Smart Copy/Paste"; + +/* Class = "NSMenu"; title = "Main Menu"; ObjectID = "AYu-sK-qS6"; */ +"AYu-sK-qS6.title" = "Main Menu"; + +/* Class = "NSMenuItem"; title = "Preferences…"; ObjectID = "BOF-NM-1cW"; */ +"BOF-NM-1cW.title" = "Preferences…"; + +/* Class = "NSMenuItem"; title = "Hide Telegram"; ObjectID = "Cag-YX-WT6"; */ +"Cag-YX-WT6.title" = "Hide Telegram"; + +/* Class = "NSMenuItem"; title = "Spelling and Grammar"; ObjectID = "Dv1-io-Yv7"; */ +"Dv1-io-Yv7.title" = "Spelling and Grammar"; + +/* Class = "NSMenu"; title = "Substitutions"; ObjectID = "FeM-D8-WVr"; */ +"FeM-D8-WVr.title" = "Substitutions"; + +/* Class = "NSMenuItem"; title = "View"; ObjectID = "H8h-7b-M4v"; */ +"H8h-7b-M4v.title" = "View"; + +/* Class = "NSMenuItem"; title = "Text Replacement"; ObjectID = "HFQ-gK-NFA"; */ +"HFQ-gK-NFA.title" = "Text Replacement"; + +/* Class = "NSMenuItem"; title = "Show Spelling and Grammar"; ObjectID = "HFo-cy-zxI"; */ +"HFo-cy-zxI.title" = "Show Spelling and Grammar"; + +/* Class = "NSMenu"; title = "View"; ObjectID = "HyV-fh-RgO"; */ +"HyV-fh-RgO.title" = "View"; + +/* Class = "NSMenuItem"; title = "Show All"; ObjectID = "Kd2-mp-pUS"; */ +"Kd2-mp-pUS.title" = "Show All"; + +/* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "LE2-aR-0XJ"; */ +"LE2-aR-0XJ.title" = "Bring All to Front"; + +/* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "OY7-WF-poV"; */ +"OY7-WF-poV.title" = "Minimize"; + +/* Class = "NSMenuItem"; title = "Hide"; ObjectID = "Olw-nP-bQN"; */ +"Olw-nP-bQN.title" = "Hide"; + +/* Class = "NSWindow"; title = "Telegram"; ObjectID = "QvC-M9-y7g"; */ +"QvC-M9-y7g.title" = "Telegram"; + +/* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "R4o-n2-Eq4"; */ +"R4o-n2-Eq4.title" = "Zoom"; + +/* Class = "NSMenuItem"; title = "Select All"; ObjectID = "Ruw-6m-B2m"; */ +"Ruw-6m-B2m.title" = "Select All"; + +/* Class = "NSMenu"; title = "Window"; ObjectID = "Td7-aD-5lo"; */ +"Td7-aD-5lo.title" = "Window"; + +/* Class = "NSMenuItem"; title = "Capitalize"; ObjectID = "UEZ-Bs-lqG"; */ +"UEZ-Bs-lqG.title" = "Capitalize"; + +/* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "Vdr-fp-XzO"; */ +"Vdr-fp-XzO.title" = "Hide Others"; + +/* Class = "NSMenu"; title = "Edit"; ObjectID = "W48-6f-4Dl"; */ +"W48-6f-4Dl.title" = "Edit"; + +/* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "WeT-3V-zwk"; */ +"WeT-3V-zwk.title" = "Paste and Match Style"; + +/* Class = "NSMenuItem"; title = "Window"; ObjectID = "aUF-d1-5bR"; */ +"aUF-d1-5bR.title" = "Window"; + +/* Class = "NSMenu"; title = "Transformations"; ObjectID = "c8a-y6-VQd"; */ +"c8a-y6-VQd.title" = "Transformations"; + +/* Class = "NSMenuItem"; title = "Smart Links"; ObjectID = "cwL-P1-jid"; */ +"cwL-P1-jid.title" = "Smart Links"; + +/* Class = "NSMenuItem"; title = "Make Lower Case"; ObjectID = "d9M-CD-aMd"; */ +"d9M-CD-aMd.title" = "Make Lower Case"; + +/* Class = "NSMenuItem"; title = "Undo"; ObjectID = "dRJ-4n-Yzg"; */ +"dRJ-4n-Yzg.title" = "Undo"; + +/* Class = "NSMenuItem"; title = "Paste"; ObjectID = "gVA-U4-sdL"; */ +"gVA-U4-sdL.title" = "Paste"; + +/* Class = "NSMenuItem"; title = "Smart Quotes"; ObjectID = "hQb-2v-fYv"; */ +"hQb-2v-fYv.title" = "Smart Quotes"; + +/* Class = "NSMenuItem"; title = "Check Document Now"; ObjectID = "hz2-CU-CR7"; */ +"hz2-CU-CR7.title" = "Check Document Now"; + +/* Class = "NSMenuItem"; title = "Check Grammar With Spelling"; ObjectID = "mK6-2p-4JG"; */ +"mK6-2p-4JG.title" = "Check Grammar With Spelling"; + +/* Class = "NSMenuItem"; title = "Delete"; ObjectID = "pa3-QI-u2k"; */ +"pa3-QI-u2k.title" = "Delete"; + +/* Class = "NSMenuItem"; title = "Check Spelling While Typing"; ObjectID = "rbD-Rh-wIN"; */ +"rbD-Rh-wIN.title" = "Check Spelling While Typing"; + +/* Class = "NSMenuItem"; title = "Smart Dashes"; ObjectID = "rgM-f4-ycn"; */ +"rgM-f4-ycn.title" = "Smart Dashes"; + +/* Class = "NSMenuItem"; title = "Quick Switcher"; ObjectID = "sZh-ct-GQS"; */ +"sZh-ct-GQS.title" = "Quick Switcher"; + +/* Class = "NSMenuItem"; title = "Data Detectors"; ObjectID = "tRr-pd-1PS"; */ +"tRr-pd-1PS.title" = "Data Detectors"; + +/* Class = "NSMenu"; title = "TelegramMac"; ObjectID = "uQy-DD-JDr"; */ +"uQy-DD-JDr.title" = "TelegramMac"; + +/* Class = "NSMenuItem"; title = "Cut"; ObjectID = "uRl-iY-unG"; */ +"uRl-iY-unG.title" = "Cut"; + +/* Class = "NSMenuItem"; title = "Make Upper Case"; ObjectID = "vmV-6d-7jI"; */ +"vmV-6d-7jI.title" = "Make Upper Case"; + +/* Class = "NSMenuItem"; title = "Copy"; ObjectID = "x3v-GG-iWU"; */ +"x3v-GG-iWU.title" = "Copy"; + +/* Class = "NSMenuItem"; title = "Check for Updates"; ObjectID = "xey-M7-XVy"; */ +"xey-M7-XVy.title" = "Check for Updates"; + +/* Class = "NSMenuItem"; title = "Telegram"; ObjectID = "yUb-j6-EX5"; */ +"yUb-j6-EX5.title" = "Telegram"; + +/* Class = "NSMenuItem"; title = "Show Substitutions"; ObjectID = "z6F-FW-3nz"; */ +"z6F-FW-3nz.title" = "Show Substitutions"; diff --git a/Telegram-Mac/webp.h b/Telegram-Mac/webp.h deleted file mode 100755 index 008fb6ef8b..0000000000 --- a/Telegram-Mac/webp.h +++ /dev/null @@ -1,5 +0,0 @@ -#import - - -CGImageRef convertFromWebP(NSData *data); - diff --git a/Telegram-Mac/webp.m b/Telegram-Mac/webp.m deleted file mode 100755 index ae6f2e35cb..0000000000 --- a/Telegram-Mac/webp.m +++ /dev/null @@ -1,82 +0,0 @@ -#import "webp.h" - -#import "libwebp/include/webp/decode.h" -#import "libwebp/include/webp/encode.h" - - - -CGImageRef convertFromWebP(NSData *imgData) { - - if (imgData == nil) { - return nil; - } - - // `WebPGetInfo` weill return image width and height - int width = 0, height = 0; - if(!WebPGetInfo([imgData bytes], [imgData length], &width, &height)) { - NSMutableDictionary *errorDetail = [NSMutableDictionary dictionary]; - [errorDetail setValue:@"Header formatting error." forKey:NSLocalizedDescriptionKey]; - return nil; - } - - const struct { int width, height; } targetContextSize = { width, height}; - - size_t targetBytesPerRow = ((4 * (int)targetContextSize.width) + 15) & (~15); - - void *targetMemory = malloc((int)(targetBytesPerRow * targetContextSize.height)); - - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; - - CGContextRef targetContext = CGBitmapContextCreate(targetMemory, (int)targetContextSize.width, (int)targetContextSize.height, 8, targetBytesPerRow, colorSpace, bitmapInfo); - - - CGContextSaveGState(targetContext); - - - CGColorSpaceRelease(colorSpace); - - if (WebPDecodeBGRAInto(imgData.bytes, imgData.length, targetMemory, targetBytesPerRow * targetContextSize.height, (int)targetBytesPerRow) == NULL) - { - //[BridgingTrace objc_trace:@"WebP" what:@"error decoding webp"]; - return nil; - } - - for (int y = 0; y < targetContextSize.height; y++) - { - for (int x = 0; x < targetContextSize.width; x++) - { - uint32_t *color = ((uint32_t *)&targetMemory[y * targetBytesPerRow + x * 4]); - - uint32_t a = (*color >> 24) & 0xff; - uint32_t r = ((*color >> 16) & 0xff) * a; - uint32_t g = ((*color >> 8) & 0xff) * a; - uint32_t b = (*color & 0xff) * a; - - r = (r + 1 + (r >> 8)) >> 8; - g = (g + 1 + (g >> 8)) >> 8; - b = (b + 1 + (b >> 8)) >> 8; - - *color = (a << 24) | (r << 16) | (g << 8) | b; - } - - for (size_t i = y * targetBytesPerRow + targetContextSize.width * 4; i < (targetBytesPerRow >> 2); i++) - { - *((uint32_t *)&targetMemory[i]) = 0; - } - } - - - - CGContextRestoreGState(targetContext); - - CGImageRef bitmapImage = CGBitmapContextCreateImage(targetContext); - - CGContextRelease(targetContext); - free(targetMemory); - - return bitmapImage; -} - - - diff --git a/Telegram.xcodeproj/project.pbxproj b/Telegram.xcodeproj/project.pbxproj index 575988a638..81ab427ef1 100644 --- a/Telegram.xcodeproj/project.pbxproj +++ b/Telegram.xcodeproj/project.pbxproj @@ -7,13 +7,437 @@ objects = { /* Begin PBXBuildFile section */ + 270202BF261A1BF500CFCE6D /* PaymentsTipsRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 270202BE261A1BF500CFCE6D /* PaymentsTipsRowItem.swift */; }; + 270202C3261B3D3100CFCE6D /* CurrencyUITextFieldDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 270202C2261B3D3100CFCE6D /* CurrencyUITextFieldDelegate.swift */; }; + 270202C6261B3D7C00CFCE6D /* UITextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 270202C5261B3D7C00CFCE6D /* UITextField.swift */; }; + 270A262A269D8F1000B31B2B /* WidgetStickersController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 270A2629269D8F1000B31B2B /* WidgetStickersController.swift */; }; + 270E2B4B2609F95D00B0738C /* playlist_play_pause.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 270E2B4A2609F95D00B0738C /* playlist_play_pause.tgs */; }; + 270E2B4E2609FDBB00B0738C /* playlist_pause_play.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 270E2B4D2609FDBA00B0738C /* playlist_pause_play.tgs */; }; + 270F020726F0B0830099D2AA /* EmojiAnimationEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 270F020626F0B0830099D2AA /* EmojiAnimationEffectView.swift */; }; + 2711D71126CFBF6A00917305 /* MediaObjectToAvatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2711D71026CFBF6A00917305 /* MediaObjectToAvatar.swift */; }; + 2713B21F260A42FE00CE0EC6 /* MediaFrameSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2713B219260A42FE00CE0EC6 /* MediaFrameSource.swift */; }; + 2713B220260A42FE00CE0EC6 /* MediaTrackDecodableFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2713B21A260A42FE00CE0EC6 /* MediaTrackDecodableFrame.swift */; }; + 2713B221260A42FE00CE0EC6 /* MediaTrackFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2713B21B260A42FE00CE0EC6 /* MediaTrackFrame.swift */; }; + 2713B222260A42FE00CE0EC6 /* MediaPlaybackData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2713B21C260A42FE00CE0EC6 /* MediaPlaybackData.swift */; }; + 2713B223260A42FE00CE0EC6 /* MediaTrackFrameDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2713B21D260A42FE00CE0EC6 /* MediaTrackFrameDecoder.swift */; }; + 2713B224260A42FE00CE0EC6 /* MediaTrackFrameBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2713B21E260A42FE00CE0EC6 /* MediaTrackFrameBuffer.swift */; }; + 2719655425EEB89E00FC9DE5 /* GroupCallRecorderRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2719655325EEB89E00FC9DE5 /* GroupCallRecorderRowItem.swift */; }; + 271B783E25ECD94C007144D7 /* PamentsSelectMethodController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271B783D25ECD94B007144D7 /* PamentsSelectMethodController.swift */; }; + 27200F1B257C256E00574365 /* DevicesContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27200F1A257C256E00574365 /* DevicesContext.swift */; }; + 27200F21257CCFDB00574365 /* HotKey.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 273F9C0A257BC486004EC51B /* HotKey.framework */; }; + 2725785B259CF987001558E8 /* AnimatedBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2725785A259CF987001558E8 /* AnimatedBadgeView.swift */; }; + 2728991926CBC0D100F4D288 /* UNUserNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2728991826CBC0D100F4D288 /* UNUserNotifications.swift */; }; + 2728991C26CBCDA900F4D288 /* TurnOnNotificationsRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2728991B26CBCDA900F4D288 /* TurnOnNotificationsRowItem.swift */; }; + 2729712126135C94006D38E2 /* GroupCallInviteRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2729712026135C94006D38E2 /* GroupCallInviteRowItem.swift */; }; + 272C1830267D00350030B5FB /* CoreMediaIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 272C182F267D00350030B5FB /* CoreMediaIO.framework */; }; + 273274E626A818AE00E04289 /* WallpaperColorPickerContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 273274E526A818AE00E04289 /* WallpaperColorPickerContainerView.swift */; }; + 273274E826A8190F00E04289 /* WallpaperAdditionColorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 273274E726A8190F00E04289 /* WallpaperAdditionColorView.swift */; }; + 27335F0325C8295200FD040C /* GroupVideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27335F0225C8295100FD040C /* GroupVideoView.swift */; }; + 273BE6C926136D4C00AAFE4E /* ImageCompression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC4DA9D21DD1C6C003E2A62 /* ImageCompression.swift */; }; + 273BE6CB26136D5200AAFE4E /* Mozjpeg.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0A51D3524F7EC2100D75641 /* Mozjpeg.framework */; }; + 2740AA3A26BD5F7A004B3BCE /* builtin-wallpaper-svg in Resources */ = {isa = PBXBuildFile; fileRef = 2740AA3926BD5CEF004B3BCE /* builtin-wallpaper-svg */; }; + 2744018526E6344E0035C55D /* WidgetRecentPeersController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2744018426E6344D0035C55D /* WidgetRecentPeersController.swift */; }; + 2744AE6D25B44E1500E8849F /* ExportedInvitationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2744AE6C25B44E1500E8849F /* ExportedInvitationController.swift */; }; + 2744AE7225B58E6C00E8849F /* invitations.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 2744AE7125B58E6C00E8849F /* invitations.tgs */; }; + 2746926C26A99DD800B67817 /* WallpaperCheckboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2746926B26A99DD800B67817 /* WallpaperCheckboxView.swift */; }; + 27478F5C25FBD05F005B8B98 /* voice_chat_raise_hand_3.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 27478F5B25FBD05F005B8B98 /* voice_chat_raise_hand_3.tgs */; }; + 274A12DE2584BFF5006C6FED /* GroupCallInvitation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 274A12DD2584BFF5006C6FED /* GroupCallInvitation.swift */; }; + 274B831826F0C0A700E6E474 /* EmojiScreenEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 274B831726F0C0A700E6E474 /* EmojiScreenEffect.swift */; }; + 274BB56D264013AD00620D03 /* cameraon.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 274BB56C264013AC00620D03 /* cameraon.tgs */; }; + 274BB570264013B900620D03 /* cameraoff.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 274BB56F264013B800620D03 /* cameraoff.tgs */; }; + 275045E525EE271D00872D20 /* GroupCallDisplayAsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 275045E425EE271D00872D20 /* GroupCallDisplayAsController.swift */; }; + 2756EF1D25C463F10062303D /* LegacyReachability.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2756EF1C25C463F10062303D /* LegacyReachability.framework */; }; + 275DDC3A267B8702009CF884 /* bot_menu_close.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 275DDC38267B8701009CF884 /* bot_menu_close.tgs */; }; + 275DDC3B267B8702009CF884 /* bot_close_menu.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 275DDC39267B8702009CF884 /* bot_close_menu.tgs */; }; + 275FD30E266A20FC008EBC99 /* GroupCallTileRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 275FD30D266A20FC008EBC99 /* GroupCallTileRowItem.swift */; }; + 2760E55626970513001C59F8 /* WidgetStorageController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2760E55526970513001C59F8 /* WidgetStorageController.swift */; }; + 2760E55926970672001C59F8 /* WidgetButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2760E55826970672001C59F8 /* WidgetButton.swift */; }; + 2760E55B269706D7001C59F8 /* WidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2760E55A269706D7001C59F8 /* WidgetView.swift */; }; + 2763A159261C91B000C12762 /* DatePickerRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2763A158261C91B000C12762 /* DatePickerRowItem.swift */; }; + 2763A15C261DF05200C12762 /* voice_chat_start_chat_to_mute.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 2763A15B261DF05200C12762 /* voice_chat_start_chat_to_mute.tgs */; }; + 2763A162261DF5CD00C12762 /* voice_chat_cancel_reminder.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 2763A161261DF5CD00C12762 /* voice_chat_cancel_reminder.tgs */; }; + 2763A165261DF5EF00C12762 /* voice_chat_cancel_reminder_to_mute.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 2763A164261DF5EF00C12762 /* voice_chat_cancel_reminder_to_mute.tgs */; }; + 2763A168261DF60A00C12762 /* voice_chat_cancel_reminder_to_raise_hand.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 2763A167261DF60A00C12762 /* voice_chat_cancel_reminder_to_raise_hand.tgs */; }; + 2763A16B261DF65700C12762 /* voice_chat_set_reminder.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 2763A16A261DF65700C12762 /* voice_chat_set_reminder.tgs */; }; + 2763A16E261DF68100C12762 /* voice_chat_set_reminder_to_mute.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 2763A16D261DF68100C12762 /* voice_chat_set_reminder_to_mute.tgs */; }; + 2763A171261DF6A100C12762 /* voice_chat_set_reminder_to_raise_hand.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 2763A170261DF6A100C12762 /* voice_chat_set_reminder_to_raise_hand.tgs */; }; + 2764A5AA25DFB5B300F9A20D /* ReportDetailsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2764A5A925DFB5B300F9A20D /* ReportDetailsController.swift */; }; + 2764A5AE25DFBDE700F9A20D /* police.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 2764A5AC25DFBB4300F9A20D /* police.tgs */; }; + 2764A5B125E0173900F9A20D /* AutoDeleteContextMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2764A5B025E0173900F9A20D /* AutoDeleteContextMenuView.swift */; }; + 27672EAE26F8D62600297DB4 /* 0.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 27672E9A26F8D5B000297DB4 /* 0.m4a */; }; + 27672EAF26F8D62800297DB4 /* 1.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 27672E9926F8D5B000297DB4 /* 1.m4a */; }; + 27672EB026F8D62A00297DB4 /* 2.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 27672EA626F8D5B200297DB4 /* 2.m4a */; }; + 27672EB126F8D62C00297DB4 /* 3.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 27672EA226F8D5B100297DB4 /* 3.m4a */; }; + 27672EB226F8D62E00297DB4 /* 4.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 27672EAD26F8D5B200297DB4 /* 4.m4a */; }; + 27672EB326F8D63100297DB4 /* 5.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 27672EA526F8D5B200297DB4 /* 5.m4a */; }; + 27672EB426F8D63300297DB4 /* 6.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 27672EAA26F8D5B200297DB4 /* 6.m4a */; }; + 27672EB526F8D63500297DB4 /* 7.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 27672EA026F8D5B100297DB4 /* 7.m4a */; }; + 27672EB626F8D63700297DB4 /* 8.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 27672EA726F8D5B200297DB4 /* 8.m4a */; }; + 27672EB726F8D63900297DB4 /* 9.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 27672E9B26F8D5B100297DB4 /* 9.m4a */; }; + 27672EB826F8D63E00297DB4 /* 100.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 27672E9C26F8D5B100297DB4 /* 100.m4a */; }; + 27672EB926F8D64000297DB4 /* 101.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 27672EA326F8D5B100297DB4 /* 101.m4a */; }; + 27672EBA26F8D64300297DB4 /* 102.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 27672E9F26F8D5B100297DB4 /* 102.m4a */; }; + 27672EBB26F8D64400297DB4 /* 103.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 27672E9826F8D5B000297DB4 /* 103.m4a */; }; + 27672EBC26F8D64600297DB4 /* 104.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 27672EA926F8D5B200297DB4 /* 104.m4a */; }; + 27672EBD26F8D64900297DB4 /* 105.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 27672E9D26F8D5B100297DB4 /* 105.m4a */; }; + 27672EBE26F8D64E00297DB4 /* 106.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 27672E9E26F8D5B100297DB4 /* 106.m4a */; }; + 27672EBF26F8D65100297DB4 /* 107.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 27672EA826F8D5B200297DB4 /* 107.m4a */; }; + 27672EC026F8D65200297DB4 /* 108.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 27672EA126F8D5B100297DB4 /* 108.m4a */; }; + 27672EC126F8D65400297DB4 /* 109.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 27672EAB26F8D5B200297DB4 /* 109.m4a */; }; + 27672EC226F8D65600297DB4 /* 110.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 27672EA426F8D5B100297DB4 /* 110.m4a */; }; + 27672EC326F8D65800297DB4 /* 111.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 27672EAC26F8D5B200297DB4 /* 111.m4a */; }; + 276A1D59265D0E600083D6E7 /* GroupCallSpeakingTooltipView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 276A1D58265D0E600083D6E7 /* GroupCallSpeakingTooltipView.swift */; }; + 276A6C0F26C5316F00E4FF34 /* StickerPackTrendingItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 276A6C0E26C5316F00E4FF34 /* StickerPackTrendingItem.swift */; }; + 276B597A261C780F0029FD3F /* GroupCallMainVideoContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 276B5979261C780F0029FD3F /* GroupCallMainVideoContainer.swift */; }; + 276B597D261C78AD0029FD3F /* GroupCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 276B597C261C78AD0029FD3F /* GroupCallView.swift */; }; + 276B5980261C78F40029FD3F /* GroupCallUIState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 276B597F261C78F40029FD3F /* GroupCallUIState.swift */; }; + 276B5983261C79250029FD3F /* GroupCallTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 276B5982261C79250029FD3F /* GroupCallTitleView.swift */; }; + 276B5986261C79940029FD3F /* GroupCallControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 276B5985261C79940029FD3F /* GroupCallControlsView.swift */; }; + 276B5989261C7AC60029FD3F /* GroupCallSchedule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 276B5988261C7AC60029FD3F /* GroupCallSchedule.swift */; }; + 276C1EBF261B451500FE41C9 /* Currency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 276C1EBE261B451500FE41C9 /* Currency.swift */; }; + 2773A53525F91E1500AB45E9 /* voice_chat_raise_hand_1.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 2773A53425F91E1500AB45E9 /* voice_chat_raise_hand_1.tgs */; }; + 2773A53A25FA1D9D00AB45E9 /* JoinVoiceChatAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2773A53925FA1D9D00AB45E9 /* JoinVoiceChatAlertController.swift */; }; + 2773A53E25FA220C00AB45E9 /* JoinVoiceChatAlertRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2773A53D25FA220B00AB45E9 /* JoinVoiceChatAlertRowItem.swift */; }; + 2773A54425FA324B00AB45E9 /* voice_chat_unmute.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 2773A54025FA324900AB45E9 /* voice_chat_unmute.tgs */; }; + 2773A54625FA324B00AB45E9 /* voice_chat_mute.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 2773A54225FA324A00AB45E9 /* voice_chat_mute.tgs */; }; + 2773A54725FA324B00AB45E9 /* voice_chat_hand_off.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 2773A54325FA324B00AB45E9 /* voice_chat_hand_off.tgs */; }; + 2773A55125FA4DAC00AB45E9 /* voice_chat_raise_hand_2.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 2773A55025FA4DAC00AB45E9 /* voice_chat_raise_hand_2.tgs */; }; + 2773A55F25FA4E0C00AB45E9 /* voice_chat_raise_hand_4.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 2773A55E25FA4E0C00AB45E9 /* voice_chat_raise_hand_4.tgs */; }; + 2773A56225FA4E2300AB45E9 /* voice_chat_raise_hand_5.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 2773A56125FA4E2300AB45E9 /* voice_chat_raise_hand_5.tgs */; }; + 2773A56525FA4E3A00AB45E9 /* voice_chat_raise_hand_6.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 2773A56425FA4E3A00AB45E9 /* voice_chat_raise_hand_6.tgs */; }; + 2773A56825FA4E4B00AB45E9 /* voice_chat_raise_hand_7.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 2773A56725FA4E4B00AB45E9 /* voice_chat_raise_hand_7.tgs */; }; + 2773A56C25FAA63E00AB45E9 /* voice_chat_hand_on_unmuted.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 2773A56A25FAA63D00AB45E9 /* voice_chat_hand_on_unmuted.tgs */; }; + 2773A56D25FAA63E00AB45E9 /* voice_chat_hand_on_muted.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 2773A56B25FAA63E00AB45E9 /* voice_chat_hand_on_muted.tgs */; }; + 2775102125E7917F003D58D1 /* PaymentsCheckoutController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2775102025E7917F003D58D1 /* PaymentsCheckoutController.swift */; }; + 2775102525E79233003D58D1 /* PaymentsCheckoutPreviewRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2775102425E79233003D58D1 /* PaymentsCheckoutPreviewRowItem.swift */; }; + 277C34A8267B5BC100B1CAA7 /* ChatInputMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277C34A7267B5BC100B1CAA7 /* ChatInputMenuView.swift */; }; + 2783BA9526E24795004A1591 /* GroupCallVideoOrientationRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2783BA9426E24795004A1591 /* GroupCallVideoOrientationRowItem.swift */; }; + 278680AD26A982DC005FDBB9 /* WallpaperPatternPreviewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 278680AC26A982DC005FDBB9 /* WallpaperPatternPreviewController.swift */; }; + 278C720425E67C2700F1B315 /* emoji1016.txt in Resources */ = {isa = PBXBuildFile; fileRef = 278C720325E67C2700F1B315 /* emoji1016.txt */; }; + 278E86D4265D0F330006685D /* GroupCallAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 278E86D3265D0F330006685D /* GroupCallAvatarView.swift */; }; + 278E86D7265D49100006685D /* MicroListenerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 278E86D6265D49100006685D /* MicroListenerController.swift */; }; + 278FA2E625E813F100280629 /* PaymentsShippingMethodController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 278FA2E525E813F100280629 /* PaymentsShippingMethodController.swift */; }; + 278FA2E925E8C36A00280629 /* PaymentsPaymentMethodController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 278FA2E825E8C36A00280629 /* PaymentsPaymentMethodController.swift */; }; + 278FA43825E8D5CD00280629 /* Stripe.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 278FA43725E8D5CD00280629 /* Stripe.framework */; }; + 27949110260A41EB0003BFA0 /* FFMpegMediaFrameSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2794910A260A41E80003BFA0 /* FFMpegMediaFrameSource.swift */; }; + 27949111260A41EB0003BFA0 /* FFMpegMediaFrameSourceContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2794910B260A41EA0003BFA0 /* FFMpegMediaFrameSourceContext.swift */; }; + 27949112260A41EB0003BFA0 /* FFMpegAudioFrameDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2794910C260A41EA0003BFA0 /* FFMpegAudioFrameDecoder.swift */; }; + 27949113260A41EB0003BFA0 /* FFMpegMediaFrameSourceContextHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2794910D260A41EA0003BFA0 /* FFMpegMediaFrameSourceContextHelpers.swift */; }; + 27949114260A41EB0003BFA0 /* FFMpegMediaPassthroughVideoFrameDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2794910E260A41EA0003BFA0 /* FFMpegMediaPassthroughVideoFrameDecoder.swift */; }; + 27949115260A41EB0003BFA0 /* FFMpegMediaVideoFrameDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2794910F260A41EB0003BFA0 /* FFMpegMediaVideoFrameDecoder.swift */; }; + 279A1E0D25E90D13007D48E7 /* PaymentWebInteractionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279A1E0C25E90D13007D48E7 /* PaymentWebInteractionController.swift */; }; + 279A1E2825E945A2007D48E7 /* PaymentsReceiptController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279A1E2725E945A2007D48E7 /* PaymentsReceiptController.swift */; }; + 27A0A4E325F76F4600B789AF /* GroupCallPeerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A0A4E225F76F4600B789AF /* GroupCallPeerController.swift */; }; + 27A0A4E725F7764600B789AF /* GroupCallPeerAvatarRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A0A4E625F7764600B789AF /* GroupCallPeerAvatarRowItem.swift */; }; + 27A0A4EB25F7814500B789AF /* GroupCallTextAndLabelRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A0A4EA25F7814500B789AF /* GroupCallTextAndLabelRowItem.swift */; }; + 27A7D37D2600F58A00A67737 /* voip_group_unmuted.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 27A7D37B2600F58900A67737 /* voip_group_unmuted.mp3 */; }; + 27A7D37E2600F58A00A67737 /* voip_group_recording_started.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 27A7D37C2600F58A00A67737 /* voip_group_recording_started.mp3 */; }; + 27A9904A257A7435009044DB /* SoundEffectPlayQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27A99049257A7435009044DB /* SoundEffectPlayQueue.swift */; }; + 27A9904E257A8047009044DB /* Pop.wav in Resources */ = {isa = PBXBuildFile; fileRef = 27A9904C257A8044009044DB /* Pop.wav */; }; + 27A9904F257A8047009044DB /* Purr.wav in Resources */ = {isa = PBXBuildFile; fileRef = 27A9904D257A8046009044DB /* Purr.wav */; }; + 27AAFCBF2649288E000B1053 /* GroupCallTileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27AAFCBE2649288E000B1053 /* GroupCallTileView.swift */; }; + 27B15D2F259B2DA700C7F280 /* DesktopCaptureListUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27B15D2E259B2DA700C7F280 /* DesktopCaptureListUI.swift */; }; + 27B15D32259B373800C7F280 /* DesktopCapturePreviewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27B15D31259B373800C7F280 /* DesktopCapturePreviewItem.swift */; }; + 27B8AFEB26380B4E0044C71B /* screenon.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 27B8AFE926380B4D0044C71B /* screenon.tgs */; }; + 27B8AFEC26380B4E0044C71B /* screenoff.tgs in Resources */ = {isa = PBXBuildFile; fileRef = 27B8AFEA26380B4E0044C71B /* screenoff.tgs */; }; + 27BA045726E4F3E9008FC1A3 /* MessageReadMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA045626E4F3E9008FC1A3 /* MessageReadMenuItem.swift */; }; + 27BA045B26E51F55008FC1A3 /* AvatarContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA045A26E51F55008FC1A3 /* AvatarContentView.swift */; }; + 27BF618026567DEE00331308 /* GroupCallContextMenuHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BF617F26567DEE00331308 /* GroupCallContextMenuHeader.swift */; }; + 27C4088E25B0603700372302 /* GeneralLoadingRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27C4088D25B0603700372302 /* GeneralLoadingRowItem.swift */; }; + 27C4089125B068C300372302 /* InviteLinkRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27C4089025B068C300372302 /* InviteLinkRowItem.swift */; }; + 27C4089425B081B500372302 /* FireTimerControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27C4089325B081B500372302 /* FireTimerControl.swift */; }; + 27C4089725B0A1F400372302 /* ClosureInviteLinkController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27C4089625B0A1F300372302 /* ClosureInviteLinkController.swift */; }; + 27C4089A25B0BD4F00372302 /* NumberSelectorController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27C4089925B0BD4E00372302 /* NumberSelectorController.swift */; }; + 27C4F5DE26B3F62E008123EC /* VideoMessageConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27C4F5DD26B3F62E008123EC /* VideoMessageConfig.swift */; }; + 27CF9609268F5B3E0086515A /* SoftwareGradientBackgroundItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27CF9608268F5B3E0086515A /* SoftwareGradientBackgroundItem.swift */; }; + 27D006DF25AF3B1C00EE3EB1 /* ExportedInvitationRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27D006DE25AF3B1C00EE3EB1 /* ExportedInvitationRowItem.swift */; }; + 27D1D62E2611FB0D00684DEA /* rnnoise.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27D1D62D2611FB0D00684DEA /* rnnoise.framework */; }; + 27D4F6DF261B776200CCAE03 /* Notices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27D4F6DE261B776200CCAE03 /* Notices.swift */; }; + 27DBE4AF25B0424100FCEE2A /* InviteLinksController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27DBE4AE25B0424100FCEE2A /* InviteLinksController.swift */; }; + 27DCF4BD267C9D680019EC49 /* AudioCommandCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27DCF4BC267C9D680019EC49 /* AudioCommandCenter.swift */; }; + 27DF650726AADB9E000753AC /* StringFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27DF650626AADB9E000753AC /* StringFormat.swift */; }; + 27E001B526285AC8008786D3 /* PinchToZoom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E001B426285AC8008786D3 /* PinchToZoom.swift */; }; + 27E434142680C57900B05CB1 /* CoreMediaVideoTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E434132680C57900B05CB1 /* CoreMediaVideoTest.swift */; }; + 27E5997A261B3F5800228411 /* CurrencyFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E59979261B3F5800228411 /* CurrencyFormatter.swift */; }; + 27E5997D261B3FA800228411 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E5997C261B3FA800228411 /* String.swift */; }; + 27E59980261B404000228411 /* CurrencyLocale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E5997F261B404000228411 /* CurrencyLocale.swift */; }; + 27E6DB0A2689C71E003D6164 /* MetalFunctions.metal in Sources */ = {isa = PBXBuildFile; fileRef = 27E6DB092689C71E003D6164 /* MetalFunctions.metal */; }; + 27E969E02589172D00CB9F64 /* call up.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 27E969DE2589172B00CB9F64 /* call up.mp3 */; }; + 27E969E12589172D00CB9F64 /* call down.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 27E969DF2589172D00CB9F64 /* call down.mp3 */; }; + 27E969E6258B417200CB9F64 /* reconnecting.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 27E969E5258B417200CB9F64 /* reconnecting.mp3 */; }; + 27F023D425A5C19F008F1C81 /* DesktopCapturerWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27F023D325A5C19F008F1C81 /* DesktopCapturerWindow.swift */; }; + 27F5F23325E7CACE00E8AC69 /* CurrencyFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27F5F23225E7CACE00E8AC69 /* CurrencyFormat.swift */; }; + 27F5F23C25E7CCD700E8AC69 /* PaymentsCheckoutPriceItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27F5F23B25E7CCD600E8AC69 /* PaymentsCheckoutPriceItem.swift */; }; + 27F5F23F25E7DD8300E8AC69 /* PaymentsShippingInfoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27F5F23E25E7DD8300E8AC69 /* PaymentsShippingInfoController.swift */; }; + 27FE779A26F4944800E1C90B /* ChatThemeSelectorController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27FE779926F4944800E1C90B /* ChatThemeSelectorController.swift */; }; + 27FE779C26F4AE4400E1C90B /* ChatThemeRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27FE779B26F4AE4400E1C90B /* ChatThemeRowItem.swift */; }; + 27FFCEC52694BA58006CA024 /* WidgetAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27FFCEC42694BA58006CA024 /* WidgetAppearance.swift */; }; + 27FFCEC72694BC95006CA024 /* WidgetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27FFCEC62694BC95006CA024 /* WidgetController.swift */; }; + 9F0367F0227208E000456348 /* QRCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0367EF227208E000456348 /* QRCode.swift */; }; + 9F0367F22272108800456348 /* ProxyQRCodeRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0367F12272108800456348 /* ProxyQRCodeRowItem.swift */; }; + 9F0367F72273260A00456348 /* UndoTooltipController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0367F62273260A00456348 /* UndoTooltipController.swift */; }; + 9F0368012277091800456348 /* LAnimationButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0368002277091800456348 /* LAnimationButton.swift */; }; + 9F03680F22771A9700456348 /* anim_unarchive.json in Resources */ = {isa = PBXBuildFile; fileRef = 9F03680222771A9600456348 /* anim_unarchive.json */; }; + 9F03681022771A9700456348 /* anim_ungroup.json in Resources */ = {isa = PBXBuildFile; fileRef = 9F03680322771A9600456348 /* anim_ungroup.json */; }; + 9F03681122771A9700456348 /* archiveAvatar.json in Resources */ = {isa = PBXBuildFile; fileRef = 9F03680422771A9600456348 /* archiveAvatar.json */; }; + 9F03681222771A9700456348 /* anim_read.json in Resources */ = {isa = PBXBuildFile; fileRef = 9F03680522771A9600456348 /* anim_read.json */; }; + 9F03681322771A9700456348 /* anim_delete.json in Resources */ = {isa = PBXBuildFile; fileRef = 9F03680622771A9600456348 /* anim_delete.json */; }; + 9F03681422771A9700456348 /* anim_unread.json in Resources */ = {isa = PBXBuildFile; fileRef = 9F03680722771A9600456348 /* anim_unread.json */; }; + 9F03681522771A9700456348 /* anim_hide.json in Resources */ = {isa = PBXBuildFile; fileRef = 9F03680822771A9600456348 /* anim_hide.json */; }; + 9F03681622771A9700456348 /* anim_mute.json in Resources */ = {isa = PBXBuildFile; fileRef = 9F03680922771A9600456348 /* anim_mute.json */; }; + 9F03681722771A9700456348 /* anim_unpin.json in Resources */ = {isa = PBXBuildFile; fileRef = 9F03680A22771A9600456348 /* anim_unpin.json */; }; + 9F03681822771A9700456348 /* anim_group.json in Resources */ = {isa = PBXBuildFile; fileRef = 9F03680B22771A9600456348 /* anim_group.json */; }; + 9F03681922771A9700456348 /* anim_archive.json in Resources */ = {isa = PBXBuildFile; fileRef = 9F03680C22771A9600456348 /* anim_archive.json */; }; + 9F03681A22771A9700456348 /* anim_unmute.json in Resources */ = {isa = PBXBuildFile; fileRef = 9F03680D22771A9700456348 /* anim_unmute.json */; }; + 9F03681B22771A9700456348 /* anim_pin.json in Resources */ = {isa = PBXBuildFile; fileRef = 9F03680E22771A9700456348 /* anim_pin.json */; }; + 9F0AE6872191D29D00A8B53A /* ContextSearchMessageItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0AE6862191D29D00A8B53A /* ContextSearchMessageItem.swift */; }; + 9F0AE6B52199904400A8B53A /* sound_a.caf in Resources */ = {isa = PBXBuildFile; fileRef = 9F0AE6B42199904400A8B53A /* sound_a.caf */; }; + 9F0AE6BC2199BBB900A8B53A /* MediaPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0AE6BB2199BBB900A8B53A /* MediaPlayerView.swift */; }; + 9F0AE6BE2199BEBE00A8B53A /* SVideoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0AE6BD2199BEBE00A8B53A /* SVideoController.swift */; }; + 9F0AE6C02199CDB500A8B53A /* SVideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0AE6BF2199CDB500A8B53A /* SVideoView.swift */; }; + 9F0B8F171FFB7F1A00073D3F /* AccentColorRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0B8F161FFB7F1A00073D3F /* AccentColorRowItem.swift */; }; + 9F0E6F78203ED1380086699C /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F0E6F77203ED1380086699C /* AppKit.framework */; }; + 9F0E6F7A203EFE870086699C /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0E6F79203EFE870086699C /* Preferences.swift */; }; + 9F0F8E82226DCD1C00A97F6A /* OpmizeDatabaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F0F8E81226DCD1C00A97F6A /* OpmizeDatabaseView.swift */; }; + 9F10CE8A20611536002DD61A /* PassportHeaderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F10CE8920611536002DD61A /* PassportHeaderItem.swift */; }; + 9F10CE8E20617C36002DD61A /* PassportInsertPasswordItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F10CE8D20617C36002DD61A /* PassportInsertPasswordItem.swift */; }; + 9F10CE922061BE19002DD61A /* InputDataController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F10CE912061BE19002DD61A /* InputDataController.swift */; }; + 9F10CE942061C8C8002DD61A /* InputDataControllerEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F10CE932061C8C8002DD61A /* InputDataControllerEntries.swift */; }; + 9F10CE962061C98E002DD61A /* InputDataRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F10CE952061C98E002DD61A /* InputDataRowItem.swift */; }; + 9F10CE9820626B1B002DD61A /* PassportDocumentRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F10CE9720626B1B002DD61A /* PassportDocumentRowItem.swift */; }; + 9F10CE9A206284F8002DD61A /* InputDataDateRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F10CE99206284F8002DD61A /* InputDataDateRowItem.swift */; }; + 9F127E15210B1F540080D709 /* PeerMediaVoiceRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F127E14210B1F540080D709 /* PeerMediaVoiceRowItem.swift */; }; + 9F12D343209251CF0072928B /* EditAccountInfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F12D342209251CF0072928B /* EditAccountInfoItem.swift */; }; + 9F13EE172100B05300562E53 /* VCardContactController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F13EE162100B05300562E53 /* VCardContactController.swift */; }; + 9F13EE1B2100BFCA00562E53 /* VCardHeaderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F13EE1A2100BFCA00562E53 /* VCardHeaderItem.swift */; }; + 9F147F6F223014EB00D71BD1 /* PasscodeControllers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F147F6E223014EB00D71BD1 /* PasscodeControllers.swift */; }; + 9F147F7322314B5500D71BD1 /* AccountContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7D422922240403007B68BB /* AccountContext.swift */; }; + 9F147F7422314B5500D71BD1 /* SharedAccountContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7D422B222415C9007B68BB /* SharedAccountContext.swift */; }; + 9F147F7522314F2700D71BD1 /* GlobalBadgeNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2AC9C171E1E687E0085C7DE /* GlobalBadgeNode.swift */; }; + 9F147F7722314FBE00D71BD1 /* PanelUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C226742E1DBE2CB3000BA9ED /* PanelUtils.swift */; }; + 9F147F782231515800D71BD1 /* UpdaterNotifySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3D5F8522085B2000CB0CAA /* UpdaterNotifySettings.swift */; }; + 9F147F79223151D900D71BD1 /* RenderedTotalUnreadCount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4EC947218B459A002B3C56 /* RenderedTotalUnreadCount.swift */; }; + 9F147F7A2231533700D71BD1 /* InAppNotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C25F71471E410DEE0046AF4E /* InAppNotificationSettings.swift */; }; + 9F147F7C2231543800D71BD1 /* PeerUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F147F7B2231543800D71BD1 /* PeerUtils.swift */; }; + 9F147F7D2231543800D71BD1 /* PeerUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F147F7B2231543800D71BD1 /* PeerUtils.swift */; }; + 9F147F7E22315EC200D71BD1 /* ManageSharedAccountInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7B74022227F492006610E4 /* ManageSharedAccountInfo.swift */; }; + 9F147F7F22315EC200D71BD1 /* SharedAccountInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7B74042227F4F2006610E4 /* SharedAccountInfo.swift */; }; + 9F14CBF32007DEB300F22DA9 /* ChatWallpaperModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F14CBF22007DEB300F22DA9 /* ChatWallpaperModalController.swift */; }; + 9F14CBF52007DFD400F22DA9 /* Wallpapers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F14CBF42007DFD400F22DA9 /* Wallpapers.swift */; }; + 9F153D1421F0C7F800B95D82 /* WallpaperPreviewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F153D1321F0C7F800B95D82 /* WallpaperPreviewController.swift */; }; + 9F153D1621F3662700B95D82 /* StoredMessageFromSearchPeer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F153D1521F3662700B95D82 /* StoredMessageFromSearchPeer.swift */; }; + 9F1668B82007E3BC00DD39FB /* ThemeGridControllerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1668B72007E3BC00DD39FB /* ThemeGridControllerItem.swift */; }; + 9F1668C82008F30900DD39FB /* ChatBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1668C72008F30900DD39FB /* ChatBackgroundView.swift */; }; + 9F17E5B9212F173900C25A65 /* AutoNightViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F17E5B8212F173900C25A65 /* AutoNightViewController.swift */; }; + 9F17E5BB212F191F00C25A65 /* AutoNightThemePreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F17E5BA212F191F00C25A65 /* AutoNightThemePreferences.swift */; }; + 9F18908D2237B5A400665EF5 /* InputURLFormatterModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F18908C2237B5A400665EF5 /* InputURLFormatterModalController.swift */; }; + 9F18908F2237E95400665EF5 /* VideoToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C250BA931E6E84880057CD96 /* VideoToolbox.framework */; }; + 9F1890932238F3DC00665EF5 /* DownloadedFilesPaths.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1890922238F3DC00665EF5 /* DownloadedFilesPaths.swift */; }; + 9F18DD93206D8FFD00A2AAD0 /* SecureIdVerificationDocumentsContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F18DD91206D8FFD00A2AAD0 /* SecureIdVerificationDocumentsContext.swift */; }; + 9F18DD94206D8FFD00A2AAD0 /* SecureIdVerificationDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F18DD92206D8FFD00A2AAD0 /* SecureIdVerificationDocument.swift */; }; + 9F1962D82101458C00FFF048 /* VCardLocationRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1962D72101458C00FFF048 /* VCardLocationRowItem.swift */; }; + 9F1AE5E420B6D7AA002A9D8D /* LocationPlaceSuggestionRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1AE5E320B6D7AA002A9D8D /* LocationPlaceSuggestionRowItem.swift */; }; + 9F1AE5E620B70328002A9D8D /* LocationSendCurrentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1AE5E520B70328002A9D8D /* LocationSendCurrentItem.swift */; }; + 9F1BABAE21E5ECE70075C03E /* ChatUndoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1BABAD21E5ECE70075C03E /* ChatUndoManager.swift */; }; + 9F1BABB021E60DCC0075C03E /* UndoOverlayHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1BABAF21E60DCC0075C03E /* UndoOverlayHeaderView.swift */; }; + 9F1BC1A9223FDE6D00F21815 /* InputSources.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1BC1A8223FDE6D00F21815 /* InputSources.swift */; }; + 9F1C279321D38A96003CD033 /* InstantPageScrollableItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1C279221D38A96003CD033 /* InstantPageScrollableItem.swift */; }; + 9F21A7CF21C1552D0037784F /* InstantPageTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F21A7CE21C1552D0037784F /* InstantPageTheme.swift */; }; + 9F21A7D321C167000037784F /* InstantPageImageItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F21A7D221C167000037784F /* InstantPageImageItem.swift */; }; + 9F21A7D521C16CB90037784F /* InstantPagePeerReferenceItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F21A7D421C16CB90037784F /* InstantPagePeerReferenceItem.swift */; }; + 9F21A7DC21C290E00037784F /* InstantPageTableItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F21A7DB21C290E00037784F /* InstantPageTableItem.swift */; }; + 9F21F65520B5A72800332C85 /* LocationModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F21F65420B5A72800332C85 /* LocationModalController.swift */; }; + 9F21F65D20B5A9C900332C85 /* MapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F21F65C20B5A9C900332C85 /* MapKit.framework */; }; + 9F262D5F21BFD5BC006817CD /* LocalizationPreviewModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F262D5E21BFD5BC006817CD /* LocalizationPreviewModalController.swift */; }; + 9F27EFF920C6B8EE00682B76 /* PassportController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F10CE8720610127002DD61A /* PassportController.swift */; }; + 9F291CA12264E57F00C66267 /* BuildConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 9F291C9F2264E57F00C66267 /* BuildConfig.m */; }; + 9F354E9C2270630A006F1D42 /* HapticEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F354E9B2270630A006F1D42 /* HapticEngine.swift */; }; + 9F3D5F6322044D8700CB0CAA /* AppUpdateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3D5F6222044D8700CB0CAA /* AppUpdateViewController.swift */; }; + 9F3D5F8622085B2000CB0CAA /* UpdaterNotifySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3D5F8522085B2000CB0CAA /* UpdaterNotifySettings.swift */; }; + 9F3EAB3B20A5A1EC003FE7E3 /* NetworkUsageStatsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3EAB3A20A5A1EC003FE7E3 /* NetworkUsageStatsController.swift */; }; + 9F3EAB4420A5ED2F003FE7E3 /* ocr.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9F3EAB3E20A5ED2F003FE7E3 /* ocr.mm */; }; + 9F3EAB4520A5ED2F003FE7E3 /* ocr_nn.bin in Resources */ = {isa = PBXBuildFile; fileRef = 9F3EAB3F20A5ED2F003FE7E3 /* ocr_nn.bin */; }; + 9F3EAB4620A5ED2F003FE7E3 /* genann.c in Sources */ = {isa = PBXBuildFile; fileRef = 9F3EAB4220A5ED2F003FE7E3 /* genann.c */; }; + 9F3EAB4720A5ED2F003FE7E3 /* fast-edge.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 9F3EAB4320A5ED2F003FE7E3 /* fast-edge.cpp */; }; + 9F3EAB4B20A5F90B003FE7E3 /* TGPassportMRZ.m in Sources */ = {isa = PBXBuildFile; fileRef = 9F3EAB4A20A5F90B003FE7E3 /* TGPassportMRZ.m */; }; + 9F4EC948218B459A002B3C56 /* RenderedTotalUnreadCount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4EC947218B459A002B3C56 /* RenderedTotalUnreadCount.swift */; }; + 9F4EEF7E21D3C3E3002C3B33 /* InstantPageDetailsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4EEF7D21D3C3E3002C3B33 /* InstantPageDetailsItem.swift */; }; + 9F4EEF8021D3C76E002C3B33 /* InstantPageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4EEF7F21D3C76E002C3B33 /* InstantPageContentView.swift */; }; + 9F4EEF8221D4F584002C3B33 /* ImageTransparency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4EEF8121D4F584002C3B33 /* ImageTransparency.swift */; }; + 9F4EEF8421D4F59C002C3B33 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F4EEF8321D4F59C002C3B33 /* Accelerate.framework */; }; + 9F4EEF8621D4FA68002C3B33 /* InstantPageArticleItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4EEF8521D4FA68002C3B33 /* InstantPageArticleItem.swift */; }; + 9F4EEF8821D515C5002C3B33 /* InstantPageStoredState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F4EEF8721D515C5002C3B33 /* InstantPageStoredState.swift */; }; + 9F52F51A2130286E006FC0B5 /* LocationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F52F5192130286E006FC0B5 /* LocationRequest.swift */; }; + 9F580BE520A0AA7B00F6D56C /* ChatRecorderOverlayWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F580BE420A0AA7B00F6D56C /* ChatRecorderOverlayWindow.swift */; }; + 9F62AE7E202D85B7007FB557 /* FetchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F62AE7D202D85B7007FB557 /* FetchManager.swift */; }; + 9F62AE81202D85E7007FB557 /* FetchManagerLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F62AE80202D85E7007FB557 /* FetchManagerLocation.swift */; }; + 9F62AE83202D8759007FB557 /* FetchMediaUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F62AE82202D8759007FB557 /* FetchMediaUtils.swift */; }; + 9F6314E121CAA0AB009FD379 /* NewPollController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6314E021CAA0AB009FD379 /* NewPollController.swift */; }; + 9F63152621D236CB009FD379 /* ForgotPasswordController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F63152521D236CB009FD379 /* ForgotPasswordController.swift */; }; + 9F63152921D26892009FD379 /* CancelResetAccountController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F63152821D26892009FD379 /* CancelResetAccountController.swift */; }; + 9F6B54C821369B4000748FC1 /* GalleryModernControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F6B54C721369B4000748FC1 /* GalleryModernControls.swift */; }; + 9F72973420B878B00067F815 /* MapResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F72973320B878B00067F815 /* MapResources.swift */; }; + 9F72974020BD9C6A0067F815 /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F72973F20BD9C6A0067F815 /* VideoPlayer.swift */; }; + 9F72974820C597800067F815 /* TermsModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F72974720C597800067F815 /* TermsModalController.swift */; }; + 9F77B3982211979B003B65B8 /* AutoplayPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F77B3972211979B003B65B8 /* AutoplayPreferences.swift */; }; + 9F77B3A6221C1DAC003B65B8 /* LogoutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F77B3A5221C1DAC003B65B8 /* LogoutViewController.swift */; }; + 9F7943B020854E2F00FEDB81 /* ProxyListRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7943AF20854E2F00FEDB81 /* ProxyListRowItem.swift */; }; + 9F7943B220855DC200FEDB81 /* ProxyListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7943B120855DC200FEDB81 /* ProxyListController.swift */; }; + 9F7B5FCB22003DC70087D020 /* WallpaperColorPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7B5FCA22003DC70087D020 /* WallpaperColorPicker.swift */; }; + 9F7B5FCD220099220087D020 /* WallpaperPatternPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7B5FCC220099220087D020 /* WallpaperPatternPreviewView.swift */; }; + 9F7B74032227F492006610E4 /* ManageSharedAccountInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7B74022227F492006610E4 /* ManageSharedAccountInfo.swift */; }; + 9F7B74052227F4F2006610E4 /* SharedAccountInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7B74042227F4F2006610E4 /* SharedAccountInfo.swift */; }; + 9F7B740F2229618E006610E4 /* SharedWakeupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7B740E2229618E006610E4 /* SharedWakeupManager.swift */; }; + 9F7B741122296FD7006610E4 /* SharedNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7B741022296FD7006610E4 /* SharedNotificationManager.swift */; }; + 9F7D421F22203DB1007B68BB /* ChannelStatisticsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7D421E22203DB1007B68BB /* ChannelStatisticsController.swift */; }; + 9F7D422A22240404007B68BB /* AccountContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7D422922240403007B68BB /* AccountContext.swift */; }; + 9F7D422C222415C9007B68BB /* SharedAccountContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F7D422B222415C9007B68BB /* SharedAccountContext.swift */; }; + 9F88A134200FD425007B899E /* Wallpapers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F14CBF42007DFD400F22DA9 /* Wallpapers.swift */; }; + 9F8DF3C8209228B000AED104 /* EditAccountInfoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8DF3C7209228B000AED104 /* EditAccountInfoController.swift */; }; + 9F9206F020727AF30054E581 /* ChangePhoneNumberContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9206EF20727AF30054E581 /* ChangePhoneNumberContainerView.swift */; }; + 9F9483B1202AF816006E873D /* CrashHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9483B0202AF816006E873D /* CrashHandler.swift */; }; + 9FA0E52A20519E33001E5649 /* MP4Atom.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FA0E52920519E33001E5649 /* MP4Atom.m */; }; + 9FA0E52D20519FCC001E5649 /* LiveUploadingHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FA0E52C20519FCC001E5649 /* LiveUploadingHelper.m */; }; + 9FA0E5342051A41A001E5649 /* PreUploadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA0E5332051A41A001E5649 /* PreUploadManager.swift */; }; + 9FA0E53B2052EDFF001E5649 /* HackUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FA0E5392052EDFE001E5649 /* HackUtils.m */; }; + 9FA0E53D205693DA001E5649 /* WebSessionsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA0E53C205693DA001E5649 /* WebSessionsController.swift */; }; + 9FA0E53F2056E159001E5649 /* WebAuthorizationRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA0E53E2056E159001E5649 /* WebAuthorizationRowItem.swift */; }; + 9FB14FBF209889A500688EF9 /* EDSunriseSet.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FB14FBE209889A500688EF9 /* EDSunriseSet.m */; }; + 9FB7CB6D221EB22700888EA9 /* CallSettingsModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FB7CB6C221EB22700888EA9 /* CallSettingsModalController.swift */; }; + 9FBE0EE1201FBEFC0060FD1C /* DownloadSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FBE0EE0201FBEFC0060FD1C /* DownloadSettingsViewController.swift */; }; + 9FC4DA9621DD0B35003E2A62 /* PeerChannelMemberCategoriesContextsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC4DA9521DD0B35003E2A62 /* PeerChannelMemberCategoriesContextsManager.swift */; }; + 9FC4DA9821DD0B86003E2A62 /* ChannelMemberCategoryListContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC4DA9721DD0B86003E2A62 /* ChannelMemberCategoryListContext.swift */; }; + 9FC4DA9A21DD0BE6003E2A62 /* CachedChannelAdmins.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC4DA9921DD0BE6003E2A62 /* CachedChannelAdmins.swift */; }; + 9FC4DA9C21DD187C003E2A62 /* SearchPeerMembers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC4DA9B21DD187C003E2A62 /* SearchPeerMembers.swift */; }; + 9FC4DA9E21DD1C6C003E2A62 /* ImageCompression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC4DA9D21DD1C6C003E2A62 /* ImageCompression.swift */; }; + 9FC4DAA821DE0626003E2A62 /* ChannelPermissionsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC4DAA721DE0626003E2A62 /* ChannelPermissionsController.swift */; }; + 9FC8AD9A2062A5610094F7B4 /* InputDataDataSelectorRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC8AD992062A5610094F7B4 /* InputDataDataSelectorRowItem.swift */; }; + 9FC8AD9C2062AA630094F7B4 /* ValuesSelectorModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC8AD9B2062AA630094F7B4 /* ValuesSelectorModalController.swift */; }; + 9FC8ADA02062D5E70094F7B4 /* PassportAcceptRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC8AD9F2062D5E70094F7B4 /* PassportAcceptRowItem.swift */; }; + 9FC8ADA22062E2DF0094F7B4 /* PassportNewPhoneNumberRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC8ADA12062E2DF0094F7B4 /* PassportNewPhoneNumberRowItem.swift */; }; + 9FC8ADA42063B6450094F7B4 /* PassportTwoStepVerificationIntroItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC8ADA32063B6450094F7B4 /* PassportTwoStepVerificationIntroItem.swift */; }; + 9FC8ADA6206925F60094F7B4 /* PassportWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC8ADA5206925F60094F7B4 /* PassportWindowController.swift */; }; + 9FC8ADA8206A77E00094F7B4 /* countries in Resources */ = {isa = PBXBuildFile; fileRef = 9FC8ADA7206A77E00094F7B4 /* countries */; }; + 9FDA713220E6456A001ED8ED /* ExternalMusicAlbumArtResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDA713120E6456A001ED8ED /* ExternalMusicAlbumArtResources.swift */; }; + 9FDA713420E65D49001ED8ED /* PeerMediaPlayerAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDA713320E65D49001ED8ED /* PeerMediaPlayerAnimationView.swift */; }; + 9FDA713B20EA9532001ED8ED /* ReadArticlesListPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDA713A20EA9532001ED8ED /* ReadArticlesListPreferences.swift */; }; + 9FDA713F20EE2D49001ED8ED /* PopularPeersRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDA713E20EE2D49001ED8ED /* PopularPeersRowItem.swift */; }; + 9FDD78D121C8F0CC00F1B4EF /* ChatPollItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDD78D021C8F0CC00F1B4EF /* ChatPollItem.swift */; }; + 9FDE0A8F21AD41C2001546D7 /* emoji1014-1.txt in Resources */ = {isa = PBXBuildFile; fileRef = 9FDE0A8E21AD41C2001546D7 /* emoji1014-1.txt */; }; + 9FF1DEA6225B699D009512C9 /* SearchUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF1DEA5225B699D009512C9 /* SearchUtils.swift */; }; + 9FF32C7C21B7DF4800BF58B6 /* StickerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF32C7B21B7DF4800BF58B6 /* StickerSettings.swift */; }; + 9FF5A1CB2232A2FF00BC1359 /* UpgradedAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FF5A1CA2232A2FF00BC1359 /* UpgradedAccount.swift */; }; + 9FFAE4F3205A8C89000C028E /* MediaPlayerAudioRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FFAE4E2205A8C83000C028E /* MediaPlayerAudioRenderer.swift */; }; + 9FFAE4FB205A8C89000C028E /* MediaPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FFAE4EA205A8C87000C028E /* MediaPlayer.swift */; }; + 9FFAE502205A9154000C028E /* ManagedAudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FFAE501205A9153000C028E /* ManagedAudioSession.swift */; }; + 9FFAE504205A916B000C028E /* VideoPlayerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FFAE503205A916B000C028E /* VideoPlayerProxy.swift */; }; + 9FFAE506205A928C000C028E /* RingByteBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FFAE505205A928C000C028E /* RingByteBuffer.swift */; }; + 9FFAE509205A92B9000C028E /* RingBuffer.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FFAE508205A92B9000C028E /* RingBuffer.m */; }; + 9FFAE50F205AB4A3000C028E /* libiconv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 9FFAE50E205AB4A3000C028E /* libiconv.tbd */; }; + 9FFAE514205AB50C000C028E /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 9FFAE513205AB50C000C028E /* libz.tbd */; }; + A7029EF8240E3A5400A89ABD /* ChatListFiltersHeaderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7029EF7240E3A5400A89ABD /* ChatListFiltersHeaderItem.swift */; }; + A7029EFA240E3CDA00A89ABD /* folder.tgs in Resources */ = {isa = PBXBuildFile; fileRef = A7029EF9240E3CCF00A89ABD /* folder.tgs */; }; + A71DC82B23858312000EEDE2 /* CoreSpotlight.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A71DC82A23858311000EEDE2 /* CoreSpotlight.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + A71DC82D23858356000EEDE2 /* Spotlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = A71DC82C23858356000EEDE2 /* Spotlight.swift */; }; + A71DC82F2386AADF000EEDE2 /* Signature.swift in Sources */ = {isa = PBXBuildFile; fileRef = A71DC82E2386AADF000EEDE2 /* Signature.swift */; }; + A71DC8342386D512000EEDE2 /* Signature.swift in Sources */ = {isa = PBXBuildFile; fileRef = A71DC82E2386AADF000EEDE2 /* Signature.swift */; }; + A71DC8352386D512000EEDE2 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2BB2DAF1F8BDF6700520255 /* Config.swift */; }; + A72D7AE923C471A7005BAC59 /* PollResultController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A72D7AE823C471A7005BAC59 /* PollResultController.swift */; }; + A731924B25CAE9DC00218E0E /* AutoremoMessagesController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A731924A25CAE9DC00218E0E /* AutoremoMessagesController.swift */; }; + A731924F25CD5F2300218E0E /* destructor.tgs in Resources */ = {isa = PBXBuildFile; fileRef = A731924E25CD5F2200218E0E /* destructor.tgs */; }; + A731925525DA7AA400218E0E /* GigagroupLanding.swift in Sources */ = {isa = PBXBuildFile; fileRef = A731925425DA7AA300218E0E /* GigagroupLanding.swift */; }; + A731925925DBA60000218E0E /* gigagroup.tgs in Resources */ = {isa = PBXBuildFile; fileRef = A731925825DBA60000218E0E /* gigagroup.tgs */; }; + A7377E1B23ACC79100AD3ADD /* ChatNavigateFailed.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7377E1A23ACC79100AD3ADD /* ChatNavigateFailed.swift */; }; + A7388432257E6E0C002E8424 /* GroupCallNavigationHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7388431257E6E0C002E8424 /* GroupCallNavigationHeaderView.swift */; }; + A73884352580E501002E8424 /* CGChatListIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A73884342580E501002E8424 /* CGChatListIndicator.swift */; }; + A7393D352407CAE100CE44CA /* ChatMediaDice.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7393D342407CAE100CE44CA /* ChatMediaDice.swift */; }; + A7393D372407CD7A00CE44CA /* ChatDiceContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7393D362407CD7A00CE44CA /* ChatDiceContentView.swift */; }; + A7393D392407D0F100CE44CA /* GraphCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7393D382407D0F100CE44CA /* GraphCore.framework */; }; + A7393D422407FF1900CE44CA /* dice_idle.tgs in Resources */ = {isa = PBXBuildFile; fileRef = A7393D412407FF0F00CE44CA /* dice_idle.tgs */; }; + A7393D442408F84300CE44CA /* DiceCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7393D432408F84300CE44CA /* DiceCache.swift */; }; + A7393D462409044C00CE44CA /* ChannelOverviewStatsRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7393D452409044C00CE44CA /* ChannelOverviewStatsRowItem.swift */; }; + A742CDCD240FB32F00C6B69B /* ChatListFilterRecommendedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A742CDCC240FB32F00C6B69B /* ChatListFilterRecommendedItem.swift */; }; + A742CE45241A517800C6B69B /* ChannelRecentPostRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A742CE44241A517800C6B69B /* ChannelRecentPostRowItem.swift */; }; + A7565EAE23BE02AF0031EADE /* ChatGradientModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7565EAD23BE02AF0031EADE /* ChatGradientModel.swift */; }; + A766493F236D6BFD00163DF4 /* PasscodeSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A766493E236D6BFD00163DF4 /* PasscodeSettings.swift */; }; + A7664940236D6BFD00163DF4 /* PasscodeSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A766493E236D6BFD00163DF4 /* PasscodeSettings.swift */; }; + A767DD4123F2BB3200366F76 /* ShortcutListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A767DD4023F2BB3200366F76 /* ShortcutListController.swift */; }; + A76C8A9E2420FFE500FDB071 /* folder_empty.tgs in Resources */ = {isa = PBXBuildFile; fileRef = A76C8A9D2420FFE400FDB071 /* folder_empty.tgs */; }; + A76C8AA02422132400FDB071 /* VerticalTabsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76C8A9F2422132400FDB071 /* VerticalTabsView.swift */; }; + A76C8AA224221D3000FDB071 /* graph_loading.tgs in Resources */ = {isa = PBXBuildFile; fileRef = A76C8AA124221D2800FDB071 /* graph_loading.tgs */; }; + A76C8AA424221F5400FDB071 /* StatisticsLoadingRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76C8AA324221F5400FDB071 /* StatisticsLoadingRowItem.swift */; }; + A76C8AB3242366EC00FDB071 /* PeerMediaBlockRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76C8AB2242366EC00FDB071 /* PeerMediaBlockRowItem.swift */; }; + A778DC2A23C75F1100DD307B /* Confetti.swift in Sources */ = {isa = PBXBuildFile; fileRef = A778DC2923C75F1100DD307B /* Confetti.swift */; }; + A778DC2E23C77AD800DD307B /* confetti.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = A778DC2D23C77AD800DD307B /* confetti.mp3 */; }; + A778DC3023C8985300DD307B /* SoundEffects.swift in Sources */ = {isa = PBXBuildFile; fileRef = A778DC2F23C8985300DD307B /* SoundEffects.swift */; }; + A778DC3323C8988300DD307B /* quiz-incorrect.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = A778DC3123C8988100DD307B /* quiz-incorrect.mp3 */; }; + A778DC3423C8988300DD307B /* quiz-correct.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = A778DC3223C8988300DD307B /* quiz-correct.mp3 */; }; + A778DC4223CD9F3C00DD307B /* SearchSettingsEmptyItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A778DC4123CD9F3C00DD307B /* SearchSettingsEmptyItem.swift */; }; + A7831B1C2403CFDD0056AEAC /* ChannelStatsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7831B1B2403CFDD0056AEAC /* ChannelStatsViewController.swift */; }; + A7831B2A24040B6B0056AEAC /* StatisticRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7831B2924040B6B0056AEAC /* StatisticRowItem.swift */; }; + A789E08423E05EAE00AEB34A /* ChatListFiltersListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A789E08323E05EAE00AEB34A /* ChatListFiltersListController.swift */; }; + A789E09723E427CE00AEB34A /* ChatListFilterPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7C1379123DB00D900803ED3 /* ChatListFilterPreferences.swift */; }; + A7918DB424093505002011CA /* GraphUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7918DB324093505002011CA /* GraphUI.framework */; }; + A7919135240D0869002011CA /* MurMurHash32.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7919134240D0869002011CA /* MurMurHash32.framework */; }; + A7919138240D1077002011CA /* ChatListFilterPredicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7919137240D1077002011CA /* ChatListFilterPredicate.swift */; }; + A7ADC47A2587B3750069737A /* VoiceChatActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ADC4792587B3750069737A /* VoiceChatActionButton.swift */; }; + A7B5031023B62E0400C9838E /* nanosvg.c in Sources */ = {isa = PBXBuildFile; fileRef = A7B5030E23B62E0000C9838E /* nanosvg.c */; }; + A7B5031323B62E1A00C9838E /* Svg.m in Sources */ = {isa = PBXBuildFile; fileRef = A7B5031123B62E1700C9838E /* Svg.m */; }; + A7B6DDD723ED935100B8E01C /* think_spectacular.tgs in Resources */ = {isa = PBXBuildFile; fileRef = A7B6DDD623ED8FDF00B8E01C /* think_spectacular.tgs */; }; + A7C1377D23D1A62800803ED3 /* PeerEmptyHolderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7C1377C23D1A62700803ED3 /* PeerEmptyHolderItem.swift */; }; + A7C1379223DB00D900803ED3 /* ChatListFilterPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7C1379123DB00D900803ED3 /* ChatListFilterPreferences.swift */; }; + A7C1379E23DF21EA00803ED3 /* ChatListRevealItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7C1379D23DF21EA00803ED3 /* ChatListRevealItem.swift */; }; + A7C41DB4235862BB00CF9402 /* PeerMediaPhotosController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7C41DB3235862BB00CF9402 /* PeerMediaPhotosController.swift */; }; + A7C41DB623586DC100CF9402 /* PeerPhotosMonthItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7C41DB523586DC100CF9402 /* PeerPhotosMonthItem.swift */; }; + A7C7215723FD45D300CE3F75 /* SaveModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7C7215623FD45D300CE3F75 /* SaveModalController.swift */; }; + A7C7215923FD473E00CE3F75 /* success_saved.tgs in Resources */ = {isa = PBXBuildFile; fileRef = A7C7215823FD473D00CE3F75 /* success_saved.tgs */; }; + A7C7215B23FD4BAA00CE3F75 /* LottieLocalAnimations.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7C7215A23FD4BAA00CE3F75 /* LottieLocalAnimations.swift */; }; + A7D08F48236AC9B6002DC240 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F0BCD9E2087BABC001D8D8A /* Sparkle.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + A7D08F4A236AC9BB002DC240 /* Sparkle.framework in Copy Files */ = {isa = PBXBuildFile; fileRef = 9F0BCD9E2087BABC001D8D8A /* Sparkle.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + A7D28205236C3C0B0000A9BF /* Postbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D28204236C3C0B0000A9BF /* Postbox.framework */; }; + A7D28207236C3C0F0000A9BF /* SwiftSignalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D28206236C3C0F0000A9BF /* SwiftSignalKit.framework */; }; + A7D28209236C3C150000A9BF /* TelegramCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D28208236C3C150000A9BF /* TelegramCore.framework */; }; + A7D2820B236C3C1B0000A9BF /* MtProtoKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D2820A236C3C1B0000A9BF /* MtProtoKit.framework */; }; + A7D2820C236C3C2A0000A9BF /* MtProtoKit.framework in Copy Files */ = {isa = PBXBuildFile; fileRef = A7D2820A236C3C1B0000A9BF /* MtProtoKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + A7D2820D236C3C2F0000A9BF /* TelegramCore.framework in Copy Files */ = {isa = PBXBuildFile; fileRef = A7D28208236C3C150000A9BF /* TelegramCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + A7D2820E236C3C330000A9BF /* SwiftSignalKit.framework in Copy Files */ = {isa = PBXBuildFile; fileRef = A7D28206236C3C0F0000A9BF /* SwiftSignalKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + A7D2820F236C3C3B0000A9BF /* Postbox.framework in Copy Files */ = {isa = PBXBuildFile; fileRef = A7D28204236C3C0B0000A9BF /* Postbox.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + A7D28226236C50EE0000A9BF /* Postbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D28204236C3C0B0000A9BF /* Postbox.framework */; }; + A7D28227236C50F20000A9BF /* SwiftSignalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D28206236C3C0F0000A9BF /* SwiftSignalKit.framework */; }; + A7D28229236C50FC0000A9BF /* TelegramCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D28208236C3C150000A9BF /* TelegramCore.framework */; }; + A7D2822B236C51A50000A9BF /* OpenSSLEncryption.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D2822A236C51A50000A9BF /* OpenSSLEncryption.framework */; }; + A7D2822D236C51F10000A9BF /* OpenSSLEncryption.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D2822C236C51F10000A9BF /* OpenSSLEncryption.framework */; }; + A7D2822F236C549B0000A9BF /* SyncCoreExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D2822E236C549B0000A9BF /* SyncCoreExtension.swift */; }; + A7D28230236C549B0000A9BF /* SyncCoreExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D2822E236C549B0000A9BF /* SyncCoreExtension.swift */; }; + A7D28232236C57DD0000A9BF /* libphonenumber.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D28231236C57DD0000A9BF /* libphonenumber.framework */; }; + A7D28237236C5B2C0000A9BF /* PhoneNumberUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D28236236C5B2C0000A9BF /* PhoneNumberUtils.swift */; }; + A7D28259236C70A50000A9BF /* SSignalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D2823B236C69070000A9BF /* SSignalKit.framework */; }; + A7DF1B6C237415AD00ACC01F /* Zip.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7DF1B6B237415AD00ACC01F /* Zip.framework */; }; + A7E831F0255040B70095A167 /* ChatPresentationInputQueryResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E831EF255040B70095A167 /* ChatPresentationInputQueryResult.swift */; }; + A7E831F1255040D60095A167 /* ChatPresentationInputQueryResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E831EF255040B70095A167 /* ChatPresentationInputQueryResult.swift */; }; + A7E831F3255041020095A167 /* IsEqualMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E831F2255041020095A167 /* IsEqualMessages.swift */; }; + A7E831F4255041020095A167 /* IsEqualMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E831F2255041020095A167 /* IsEqualMessages.swift */; }; + A7E831F82551A85F0095A167 /* ColdStartPasslockController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E831F72551A85F0095A167 /* ColdStartPasslockController.swift */; }; + A7E83212255E8A8E0095A167 /* LinkHoverController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E83211255E8A8E0095A167 /* LinkHoverController.swift */; }; + A7E83214255E992D0095A167 /* WaveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E83213255E992D0095A167 /* WaveView.swift */; }; + A7E832192562CFC70095A167 /* VoiceBlobView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E832182562CFC70095A167 /* VoiceBlobView.swift */; }; + A7ED5DAF236C7CE100040372 /* RLottie.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7ED5DAE236C7CE100040372 /* RLottie.framework */; }; + A7F282B2238D122900742C20 /* UnauthorizedConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F282B1238D122900742C20 /* UnauthorizedConfiguration.swift */; }; + A7F283002395168300742C20 /* SettingsSearchRecentQueries.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F282FF2395168300742C20 /* SettingsSearchRecentQueries.swift */; }; + A7F283022395185B00742C20 /* SettingsSearchableItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F283012395185B00742C20 /* SettingsSearchableItems.swift */; }; + A7F283042395289B00742C20 /* AccountUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F283032395289B00742C20 /* AccountUtils.swift */; }; + A7F2830623954E1B00742C20 /* CachedFaqInstantPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F2830523954E1B00742C20 /* CachedFaqInstantPage.swift */; }; + A7F2830823954EF800742C20 /* CachedInstantPages.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F2830723954EF800742C20 /* CachedInstantPages.swift */; }; + A7F2830A2395570400742C20 /* SearchSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F283092395570400742C20 /* SearchSettingsController.swift */; }; + A7F2831923994B9700742C20 /* AutoplayPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F77B3972211979B003B65B8 /* AutoplayPreferences.swift */; }; + A7F2831B239A496400742C20 /* ContextShowPeersHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F2831A239A496400742C20 /* ContextShowPeersHolder.swift */; }; + A7F28337239A808500742C20 /* TemplateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F28336239A808500742C20 /* TemplateController.swift */; }; C2016F3B1EAA4538003AF981 /* RecentPeerRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2016F3A1EAA4538003AF981 /* RecentPeerRowItem.swift */; }; - C2016F681EAE0A68003AF981 /* PhoneCallWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2016F671EAE0A68003AF981 /* PhoneCallWindowController.swift */; }; + C2016F681EAE0A68003AF981 /* CallWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2016F671EAE0A68003AF981 /* CallWindowController.swift */; }; C201C2321E3B2D1C0026C21E /* FastSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C201C2311E3B2D1C0026C21E /* FastSettings.swift */; }; C20232A81D81D189007C9ADE /* ChatController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20232A71D81D189007C9ADE /* ChatController.swift */; }; C20232AA1D81D19C007C9ADE /* ChatRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20232A91D81D19C007C9ADE /* ChatRowItem.swift */; }; C20232AC1D81D1AE007C9ADE /* ChatMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20232AB1D81D1AE007C9ADE /* ChatMessageView.swift */; }; - C20320FF1F9769BA00143395 /* TwoStepVerificationResetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20320FE1F9769BA00143395 /* TwoStepVerificationResetController.swift */; }; C2057FA51EBBCEA7000423DC /* voip_busy.caf in Resources */ = {isa = PBXBuildFile; fileRef = C2CFCAB91EBB4A2200843F6A /* voip_busy.caf */; }; C2057FA61EBBCEA7000423DC /* voip_end.caf in Resources */ = {isa = PBXBuildFile; fileRef = C2CFCABA1EBB4A2200843F6A /* voip_end.caf */; }; C2057FA71EBBCEA7000423DC /* voip_fail.caf in Resources */ = {isa = PBXBuildFile; fileRef = C2CFCABB1EBB4A2200843F6A /* voip_fail.caf */; }; @@ -22,23 +446,20 @@ C205DB981EE71127003711DF /* ChannelAdminController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C205DB971EE71127003711DF /* ChannelAdminController.swift */; }; C205DB9D1EE88765003711DF /* Views.swift in Sources */ = {isa = PBXBuildFile; fileRef = C205DB9C1EE88762003711DF /* Views.swift */; }; C205FEA61EB39DE400455808 /* SidebarCapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C205FEA51EB39DE400455808 /* SidebarCapViewController.swift */; }; - C2084F041F5D5C6F004713C4 /* ChatReplyPreviewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2084F031F5D5C6F004713C4 /* ChatReplyPreviewController.swift */; }; C209C3731F262537009231FE /* emoji_suggestions_data.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C209C36F1F262537009231FE /* emoji_suggestions_data.cpp */; }; C209C3741F262537009231FE /* emoji_suggestions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C209C3711F262537009231FE /* emoji_suggestions.cpp */; }; C209C3771F26271B009231FE /* EmojiSuggestionBridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = C209C3761F26271B009231FE /* EmojiSuggestionBridge.mm */; }; - C209C3881F262D48009231FE /* libstdc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = C209C3791F262858009231FE /* libstdc++.tbd */; }; C209C38D1F276D4C009231FE /* ChatSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C209C38C1F276D4C009231FE /* ChatSearchView.swift */; }; C20B8F3E1DFC52EE008A354E /* ChatEmptyPeerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20B8F3D1DFC52EE008A354E /* ChatEmptyPeerItem.swift */; }; C20B8F401DFD9999008A354E /* ChatListNothingItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20B8F3F1DFD9999008A354E /* ChatListNothingItem.swift */; }; + C20CAD121FE291E300EFF8BF /* ChatBubbleAccessoryForward.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20CAD111FE291E200EFF8BF /* ChatBubbleAccessoryForward.swift */; }; + C20CAD151FE436E300EFF8BF /* SelectSizeRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20CAD141FE436E300EFF8BF /* SelectSizeRowItem.swift */; }; C20CB7291E60886F00C992AC /* LinkInvationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20CB7281E60886E00C992AC /* LinkInvationController.swift */; }; C20D5AB11DA996480042616A /* EBlockItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20D5AB01DA996480042616A /* EBlockItem.swift */; }; C20D5AB31DA9965B0042616A /* EBlockRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20D5AB21DA9965B0042616A /* EBlockRowView.swift */; }; C21074241E77F5DF006EE5EF /* dsa_pub_prod.pem in Resources */ = {isa = PBXBuildFile; fileRef = C21074231E77F5DF006EE5EF /* dsa_pub_prod.pem */; }; C210742C1E780CC1006EE5EF /* PhotoCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = C210742B1E780CC1006EE5EF /* PhotoCache.swift */; }; C210959A1E9FE04700E10BDB /* ChatVideoMessageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C21095991E9FE04700E10BDB /* ChatVideoMessageContentView.swift */; }; - C21178001F16BB8300AC706D /* BioViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C21177FF1F16BB8300AC706D /* BioViewController.swift */; }; - C21656D41EE4A83E0041A6BA /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = D098C71C1D7E175A007784E4 /* MainMenu.xib */; }; - C21656E81EE576FA0041A6BA /* ChatUserPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = C21656E71EE576F90041A6BA /* ChatUserPopover.swift */; }; C2167E4F1DC220D800F98E03 /* PeerMediaWebpageRowContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2167E4E1DC220D800F98E03 /* PeerMediaWebpageRowContent.swift */; }; C2167E511DC220E900F98E03 /* PeerMediaMusicRowContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2167E501DC220E900F98E03 /* PeerMediaMusicRowContent.swift */; }; C2167E531DC220F600F98E03 /* PeerMediaFileRowContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2167E521DC220F600F98E03 /* PeerMediaFileRowContent.swift */; }; @@ -52,7 +473,6 @@ C218FF9A1F42030B00DD7D35 /* InstantPageChannelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C218FF991F42030B00DD7D35 /* InstantPageChannelItem.swift */; }; C218FF9C1F4204C400DD7D35 /* InstantPageChannelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C218FF9B1F4204C400DD7D35 /* InstantPageChannelView.swift */; }; C219E1D71D8869F20042F0C8 /* ChatHoleRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C219E1D61D8869F20042F0C8 /* ChatHoleRowItem.swift */; }; - C219E1D91D886A160042F0C8 /* ChatHoleRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C219E1D81D886A160042F0C8 /* ChatHoleRowView.swift */; }; C219E1DB1D8884290042F0C8 /* ChatHistoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = C219E1DA1D8884290042F0C8 /* ChatHistoryEntry.swift */; }; C219E1E41D8AC8370042F0C8 /* ChatUnreadRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C219E1E31D8AC8370042F0C8 /* ChatUnreadRowItem.swift */; }; C219E1E81D8AC90B0042F0C8 /* ChatUnreadRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C219E1E71D8AC90B0042F0C8 /* ChatUnreadRowView.swift */; }; @@ -61,12 +481,13 @@ C21A48AE1F7CFBBE0095ADB1 /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C21A48AD1F7CFBBE0095ADB1 /* OpenGL.framework */; }; C21A48B21F7D0D3F0095ADB1 /* VideoMessage.fsh in Resources */ = {isa = PBXBuildFile; fileRef = C21A48B11F7D0D3F0095ADB1 /* VideoMessage.fsh */; }; C21A48B41F7D0D6B0095ADB1 /* VideoMessage.vsh in Resources */ = {isa = PBXBuildFile; fileRef = C21A48B31F7D0D6B0095ADB1 /* VideoMessage.vsh */; }; - C21AAE341DB0F6BC007638C5 /* MediaTitleBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C21AAE331DB0F6BC007638C5 /* MediaTitleBarView.swift */; }; C21AAE361DB22CA5007638C5 /* AvatarLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C21AAE351DB22CA5007638C5 /* AvatarLayer.swift */; }; C21B24611ED9C39F00FC6CDA /* SuggestionLocalizationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C21B24601ED9C39F00FC6CDA /* SuggestionLocalizationViewController.swift */; }; C21B24631EDADC8600FC6CDA /* MMMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C21B24621EDADC8600FC6CDA /* MMMenuItem.swift */; }; C21B24651EDB115700FC6CDA /* StringPluralization.swift in Sources */ = {isa = PBXBuildFile; fileRef = C21B24641EDB115700FC6CDA /* StringPluralization.swift */; }; C21B24681EDB116B00FC6CDA /* NumberPluralizationForm.m in Sources */ = {isa = PBXBuildFile; fileRef = C21B24671EDB116B00FC6CDA /* NumberPluralizationForm.m */; }; + C21BE3AF1FD099AA00C1C849 /* DeveloperViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C21BE3AE1FD099AA00C1C849 /* DeveloperViewController.swift */; }; + C21BE3B11FD14CDB00C1C849 /* ParseAppearanceColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = C21BE3B01FD14CDB00C1C849 /* ParseAppearanceColors.swift */; }; C2203EA21DDE2AB8001E6AB6 /* ChatSelectText.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2203EA11DDE2AB8001E6AB6 /* ChatSelectText.swift */; }; C221ED541EA684BE00471C65 /* DataAndStorageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C221ED531EA684BE00471C65 /* DataAndStorageViewController.swift */; }; C221ED561EA6877300471C65 /* GeneratedMediaStoreSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C221ED551EA6877300471C65 /* GeneratedMediaStoreSettings.swift */; }; @@ -75,27 +496,18 @@ C221ED5C1EA69AA300471C65 /* StorageUsageController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C221ED5B1EA69AA300471C65 /* StorageUsageController.swift */; }; C221ED5E1EA6B36600471C65 /* ChatStorageManagmentModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C221ED5D1EA6B36600471C65 /* ChatStorageManagmentModalController.swift */; }; C22338451F823F8C004AD57C /* VideoCameraStructures.swift in Sources */ = {isa = PBXBuildFile; fileRef = C22338441F823F8C004AD57C /* VideoCameraStructures.swift */; }; + C224675B1FA8546200F03E27 /* GroupedLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = C224675A1FA8546200F03E27 /* GroupedLayout.swift */; }; + C224675D1FA884E300F03E27 /* ChatGroupedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C224675C1FA884E300F03E27 /* ChatGroupedItem.swift */; }; C224A72D1EB75A3100F43F3F /* ExMajorNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C224A72C1EB75A3100F43F3F /* ExMajorNavigationController.swift */; }; - C224F2921E43CD93002FF0B2 /* TelegramCoreMac.framework in Copy Files */ = {isa = PBXBuildFile; fileRef = C2FBC1D61DC61AFF0063A23B /* TelegramCoreMac.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - C224F2931E43CDBE002FF0B2 /* SwiftSignalKitMac.framework in Copy Files */ = {isa = PBXBuildFile; fileRef = C2FBC1D81DC61B050063A23B /* SwiftSignalKitMac.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C224F2941E43CDC4002FF0B2 /* TGUIKit.framework in Copy Files */ = {isa = PBXBuildFile; fileRef = C22E06251D7F16C000A11C88 /* TGUIKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - C224F2951E43CDD8002FF0B2 /* MtProtoKitMac.framework in Copy Files */ = {isa = PBXBuildFile; fileRef = C2FBC1DE1DC61B580063A23B /* MtProtoKitMac.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - C224F2961E43CDDD002FF0B2 /* PostboxMac.framework in Copy Files */ = {isa = PBXBuildFile; fileRef = C234CA901D97E117003023F7 /* PostboxMac.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C225524B1F7BE7000007944D /* VideoRecorderModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C225524A1F7BE7000007944D /* VideoRecorderModalController.swift */; }; C225524D1F7BE8E40007944D /* VideoRecorderModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C225524C1F7BE8E40007944D /* VideoRecorderModalView.swift */; }; C225524F1F7C03B50007944D /* VideoRecorderPipeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = C225524E1F7C03B50007944D /* VideoRecorderPipeline.swift */; }; C2256D981DAB9D5A00494CF4 /* ChatHistoryViewForLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2256D971DAB9D5A00494CF4 /* ChatHistoryViewForLocation.swift */; }; - C226741F1DBCEAC2000BA9ED /* EStickerGridEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = C226741E1DBCEAC2000BA9ED /* EStickerGridEntries.swift */; }; - C22674211DBCECCC000BA9ED /* EStickerGridItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C22674201DBCECCC000BA9ED /* EStickerGridItem.swift */; }; C226742B1DBE16B9000BA9ED /* CachedResourceRepresentations.swift in Sources */ = {isa = PBXBuildFile; fileRef = C226742A1DBE16B9000BA9ED /* CachedResourceRepresentations.swift */; }; C226742F1DBE2CB3000BA9ED /* PanelUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C226742E1DBE2CB3000BA9ED /* PanelUtils.swift */; }; C22674311DBE9B50000BA9ED /* FetchCachedRepresentations.swift in Sources */ = {isa = PBXBuildFile; fileRef = C22674301DBE9B50000BA9ED /* FetchCachedRepresentations.swift */; }; - C22674331DBF665A000BA9ED /* EStickerPackEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = C22674321DBF665A000BA9ED /* EStickerPackEntries.swift */; }; - C22674351DBF6A85000BA9ED /* EStickerPackItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C22674341DBF6A85000BA9ED /* EStickerPackItem.swift */; }; C22674381DC125C1000BA9ED /* PeerMediaCollectionInterfaceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C22674371DC125C1000BA9ED /* PeerMediaCollectionInterfaceState.swift */; }; - C226743A1DC1273E000BA9ED /* PeerMediaGridController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C22674391DC1273E000BA9ED /* PeerMediaGridController.swift */; }; - C226743F1DC12E4E000BA9ED /* GridMessageItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C226743E1DC12E4E000BA9ED /* GridMessageItem.swift */; }; - C22674411DC13165000BA9ED /* GridHoleItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C22674401DC13165000BA9ED /* GridHoleItem.swift */; }; C22674451DC20664000BA9ED /* PeerMediaListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C22674441DC20664000BA9ED /* PeerMediaListController.swift */; }; C2271D9C1DACC027001792B6 /* SearchController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271D9B1DACC027001792B6 /* SearchController.swift */; }; C2271D9E1DACC796001792B6 /* ChatListMessageRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271D9D1DACC796001792B6 /* ChatListMessageRowItem.swift */; }; @@ -111,20 +523,16 @@ C2271DBB1DAE213D001792B6 /* ChannelInfoEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271DBA1DAE213D001792B6 /* ChannelInfoEntries.swift */; }; C2271DBF1DAE563D001792B6 /* PeerInfoHeaderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271DBE1DAE563D001792B6 /* PeerInfoHeaderItem.swift */; }; C2271DC11DAE583E001792B6 /* TextAndLabelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271DC01DAE583E001792B6 /* TextAndLabelItem.swift */; }; - C2271DC41DAE5E46001792B6 /* PeerInfoHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271DC31DAE5E46001792B6 /* PeerInfoHeaderView.swift */; }; C2271DCA1DAED681001792B6 /* PresenceStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271DC91DAED681001792B6 /* PresenceStrings.swift */; }; C2271DD21DAF6DF5001792B6 /* EmptyChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271DD11DAF6DF5001792B6 /* EmptyChatViewController.swift */; }; C2271DD71DAF80D5001792B6 /* PeerMediaController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271DD61DAF80D5001792B6 /* PeerMediaController.swift */; }; C2271F2B1DB3BEB60045E719 /* GlobalHandlers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271F2A1DB3BEB60045E719 /* GlobalHandlers.swift */; }; C2271F381DB4D0490045E719 /* EmojiViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271F371DB4D0490045E719 /* EmojiViewController.swift */; }; - C2271F3A1DB4D0540045E719 /* EStickersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271F391DB4D0540045E719 /* EStickersViewController.swift */; }; C2271F3C1DB4D0630045E719 /* GIFViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271F3B1DB4D0630045E719 /* GIFViewController.swift */; }; C2271F3E1DB4D4240045E719 /* ETabRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271F3D1DB4D4240045E719 /* ETabRowItem.swift */; }; C2271F401DB4D5850045E719 /* ETabRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271F3F1DB4D5850045E719 /* ETabRowView.swift */; }; C2271F471DB4FC130045E719 /* EStickItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271F461DB4FC130045E719 /* EStickItem.swift */; }; C2271F491DB4FC220045E719 /* EStickView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271F481DB4FC220045E719 /* EStickView.swift */; }; - C2271F4F1D9C38F500424F7B /* SPopoverRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271F4E1D9C38F500424F7B /* SPopoverRowItem.swift */; }; - C2271F511D9C392400424F7B /* SPopoverRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271F501D9C392400424F7B /* SPopoverRowView.swift */; }; C2271F541D9D420B00424F7B /* ContactsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271F531D9D420B00424F7B /* ContactsController.swift */; }; C2271F571D9D46BC00424F7B /* ShortPeerRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271F561D9D46BC00424F7B /* ShortPeerRowItem.swift */; }; C2271F591D9D46CA00424F7B /* ShortPeerRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2271F581D9D46CA00424F7B /* ShortPeerRowView.swift */; }; @@ -142,10 +550,10 @@ C2303E731D9966BD00098E12 /* ChatInputActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2303E721D9966BD00098E12 /* ChatInputActionsView.swift */; }; C2303E781D997C3100098E12 /* ChatInputAttachView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2303E771D997C3100098E12 /* ChatInputAttachView.swift */; }; C2303E8A1D9A76D800098E12 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2303E891D9A76D800098E12 /* MainViewController.swift */; }; + C23044831F98F8B400977C51 /* MediaPreviewRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23044821F98F8B400977C51 /* MediaPreviewRowItem.swift */; }; C230B8EF1DD3358C0057F596 /* AccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C230B8EE1DD3358C0057F596 /* AccountViewController.swift */; }; C230B8F11DD348970057F596 /* AccountInfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C230B8F01DD348970057F596 /* AccountInfoItem.swift */; }; C230B8F41DD368D40057F596 /* SelectPeersController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C230B8F31DD368D40057F596 /* SelectPeersController.swift */; }; - C230B8F61DD371430057F596 /* SPopoverViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C230B8F51DD371430057F596 /* SPopoverViewController.swift */; }; C230B90F1DD383820057F596 /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C230B90E1DD383820057F596 /* ComposeViewController.swift */; }; C230B9111DD3866A0057F596 /* ComposeActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C230B9101DD3866A0057F596 /* ComposeActions.swift */; }; C230B9131DD392EB0057F596 /* SearchRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C230B9121DD392EB0057F596 /* SearchRowItem.swift */; }; @@ -165,10 +573,6 @@ C232EA951E1D07E700C4D38C /* icon.icns in Resources */ = {isa = PBXBuildFile; fileRef = C232EA941E1D07E700C4D38C /* icon.icns */; }; C232EA981E1D07E700C4D38C /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C232EA971E1D07E700C4D38C /* ShareViewController.swift */; }; C232EA9B1E1D07E700C4D38C /* ShareViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = C232EA991E1D07E700C4D38C /* ShareViewController.xib */; }; - C232EAA41E1D07FE00C4D38C /* MtProtoKitMac.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C2FBC1DE1DC61B580063A23B /* MtProtoKitMac.framework */; }; - C232EAA51E1D07FE00C4D38C /* PostboxMac.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C234CA901D97E117003023F7 /* PostboxMac.framework */; }; - C232EAA61E1D07FE00C4D38C /* SwiftSignalKitMac.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C2FBC1D81DC61B050063A23B /* SwiftSignalKitMac.framework */; }; - C232EAA71E1D07FE00C4D38C /* TelegramCoreMac.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C2FBC1D61DC61AFF0063A23B /* TelegramCoreMac.framework */; }; C232EAA81E1D07FE00C4D38C /* TGUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C22E06251D7F16C000A11C88 /* TGUIKit.framework */; }; C232EAB01E1D110500C4D38C /* SESelectController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C232EAAF1E1D110500C4D38C /* SESelectController.swift */; }; C232EAB61E1D11CA00C4D38C /* InterfaceObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2167E5E1DC25C6900F98E03 /* InterfaceObserver.swift */; }; @@ -185,7 +589,6 @@ C234A7FB1ED7112400EBBECE /* LocalizableExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C234A7FA1ED7112400EBBECE /* LocalizableExtension.swift */; }; C234A7FE1ED725C300EBBECE /* LanguageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C234A7FD1ED725C300EBBECE /* LanguageViewController.swift */; }; C234A8001ED73A3300EBBECE /* LanguageRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C234A7FF1ED73A3300EBBECE /* LanguageRowItem.swift */; }; - C234CA911D97E117003023F7 /* PostboxMac.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C234CA901D97E117003023F7 /* PostboxMac.framework */; }; C234D4121EEDE6990017DC25 /* LoadingTableItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C234D4111EEDE6990017DC25 /* LoadingTableItem.swift */; }; C236ADDE1F7D318700E8C71A /* TGVideoCameraMovieRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = C236ADDC1F7D318600E8C71A /* TGVideoCameraMovieRecorder.m */; }; C2379D2A1DDCCBF10063AD30 /* ReplyMarkupNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2379D291DDCCBF10063AD30 /* ReplyMarkupNode.swift */; }; @@ -200,24 +603,24 @@ C23BC37E1E9BB28F00D79F92 /* AddContactModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23BC37D1E9BB28F00D79F92 /* AddContactModalController.swift */; }; C23C5AE11E1136D1005903E1 /* GroupNameRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23C5AE01E1136D1005903E1 /* GroupNameRowItem.swift */; }; C23D0D7A1F1A609300AF5151 /* SFCompactRounded-Semibold.otf in Resources */ = {isa = PBXBuildFile; fileRef = C23D0D781F1A609300AF5151 /* SFCompactRounded-Semibold.otf */; }; - C23D0D7C1F1A649900AF5151 /* SFCompactRounded-Semibold.otf in Fonts */ = {isa = PBXBuildFile; fileRef = C23D0D781F1A609300AF5151 /* SFCompactRounded-Semibold.otf */; }; + C23D0D7C1F1A649900AF5151 /* SFCompactRounded-Semibold.otf in Resources */ = {isa = PBXBuildFile; fileRef = C23D0D781F1A609300AF5151 /* SFCompactRounded-Semibold.otf */; }; C23D0D7D1F1A692100AF5151 /* SFCompactRounded-Semibold.otf in CopyFiles */ = {isa = PBXBuildFile; fileRef = C23D0D781F1A609300AF5151 /* SFCompactRounded-Semibold.otf */; }; - C240E9521F96449E00F671FA /* TwoStepVerificationPasswordEntryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C240E9511F96449E00F671FA /* TwoStepVerificationPasswordEntryController.swift */; }; - C2412E071DA795D200588C14 /* GalleryControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2412E061DA795D200588C14 /* GalleryControls.swift */; }; + C23EEC891FCC47C1001371CD /* PeerMediaDateItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23EEC881FCC47C1001371CD /* PeerMediaDateItem.swift */; }; + C241025D1FD5702D00DB8625 /* ChatMessageBubbleImages.swift in Sources */ = {isa = PBXBuildFile; fileRef = C241025C1FD5702D00DB8625 /* ChatMessageBubbleImages.swift */; }; + C241026F1FD58EA900DB8625 /* SImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C241026E1FD58EA800DB8625 /* SImageView.swift */; }; C2423A541F2235080041907F /* InstantPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2423A531F2235080041907F /* InstantPageViewController.swift */; }; C246161B1ED33FFE0026D5BC /* InstantVideoPIP.swift in Sources */ = {isa = PBXBuildFile; fileRef = C246161A1ED33FFE0026D5BC /* InstantVideoPIP.swift */; }; + C246D6281FAB72D4004C17FA /* MediaGroupPreviewRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C246D6271FAB72D4004C17FA /* MediaGroupPreviewRowItem.swift */; }; C248BD211E6F09CC004B9106 /* ChatGameContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C248BD201E6F09CC004B9106 /* ChatGameContentView.swift */; }; C248BD221E705B62004B9106 /* PrivacyAndSecurityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C24D9FC91E25033E002CD3F3 /* PrivacyAndSecurityViewController.swift */; }; C248BD241E706104004B9106 /* BlockedPeersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C248BD231E706104004B9106 /* BlockedPeersViewController.swift */; }; C248BD261E706A05004B9106 /* RecentSessionsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C248BD251E706A05004B9106 /* RecentSessionsController.swift */; }; C248BD291E706DDA004B9106 /* RecentSessionRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C248BD281E706DDA004B9106 /* RecentSessionRowItem.swift */; }; - C24949121E5B704900D7ED5D /* AccountsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C24949111E5B704900D7ED5D /* AccountsListViewController.swift */; }; C24949141E5B763F00D7ED5D /* ApplicationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C24949131E5B763F00D7ED5D /* ApplicationContext.swift */; }; C24BA3BD1E9D30F800E8970B /* DeleteSupergroupMessagesModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C24BA3BC1E9D30F800E8970B /* DeleteSupergroupMessagesModalController.swift */; }; C24D9F911E1F8F85002CD3F3 /* MajorBackNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = C24D9F901E1F8F85002CD3F3 /* MajorBackNavigationBar.swift */; }; C24D9FC41E24FFF3002CD3F3 /* PasscodeLockController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C24D9FC31E24FFF3002CD3F3 /* PasscodeLockController.swift */; }; C24D9FC71E2500AC002CD3F3 /* PasscodeSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C24D9FC61E2500AC002CD3F3 /* PasscodeSettingsViewController.swift */; }; - C24D9FDC1E267932002CD3F3 /* PreviewSenderItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = C24D9FDB1E267932002CD3F3 /* PreviewSenderItems.swift */; }; C24DAB861E08026C005EE404 /* MGalleryVideoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C24DAB851E08026C005EE404 /* MGalleryVideoItem.swift */; }; C24DAB931E0828B6005EE404 /* JavaScriptCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C24DAB921E0828B6005EE404 /* JavaScriptCore.framework */; }; C24DABA21E083185005EE404 /* YTVimeoExtractor.m in Sources */ = {isa = PBXBuildFile; fileRef = C24DAB9A1E083184005EE404 /* YTVimeoExtractor.m */; }; @@ -246,14 +649,9 @@ C250B0371DB7BB09004E9FBE /* mime-types.txt in Resources */ = {isa = PBXBuildFile; fileRef = C250B0361DB7BB09004E9FBE /* mime-types.txt */; }; C250B0391DB7BB2D004E9FBE /* MimeTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = C250B0381DB7BB2D004E9FBE /* MimeTypes.swift */; }; C250BA8F1E6E1CDC0057CD96 /* ChatMessageThrottledProcessingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C250BA8E1E6E1CDC0057CD96 /* ChatMessageThrottledProcessingManager.swift */; }; - C25253271DF03F5700ADBC98 /* TGOpusAudioRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = C25253261DF03F5700ADBC98 /* TGOpusAudioRecorder.m */; }; - C252532A1DF03F9600ADBC98 /* TGAudioWaveform.m in Sources */ = {isa = PBXBuildFile; fileRef = C25253291DF03F9600ADBC98 /* TGAudioWaveform.m */; }; + C251FB4C1FEDCC750035E5D7 /* ChatPresentationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C251FB4B1FEDCC750035E5D7 /* ChatPresentationUtils.swift */; }; C252532D1DF04AF500ADBC98 /* begin_record.caf in Resources */ = {isa = PBXBuildFile; fileRef = C252532C1DF0440300ADBC98 /* begin_record.caf */; }; - C2538E521E770B4600B21DF0 /* GroupAdminsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2538E511E770B4600B21DF0 /* GroupAdminsController.swift */; }; - C253A92D1D8EE1A600CDC850 /* ChatMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C253A92C1D8EE1A600CDC850 /* ChatMediaView.swift */; }; C253A9451D90303200CDC850 /* FastBlur.m in Sources */ = {isa = PBXBuildFile; fileRef = C253A9441D90303200CDC850 /* FastBlur.m */; }; - C253A95C1D9165A400CDC850 /* libwebp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C253A95B1D9165A400CDC850 /* libwebp.a */; }; - C253A95F1D9165CD00CDC850 /* webp.m in Sources */ = {isa = PBXBuildFile; fileRef = C253A95E1D9165CD00CDC850 /* webp.m */; }; C253A9611D917ACD00CDC850 /* ChatStickerContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C253A9601D917ACD00CDC850 /* ChatStickerContentView.swift */; }; C253A9631D91A90100CDC850 /* ChatFileMediaItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C253A9621D91A90100CDC850 /* ChatFileMediaItem.swift */; }; C253A9671D92E3AF00CDC850 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C253A9661D92E3AE00CDC850 /* AVFoundation.framework */; }; @@ -264,18 +662,18 @@ C253E22E1DE33A7C0022A29F /* ChatMusicRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C253E22D1DE33A7C0022A29F /* ChatMusicRowItem.swift */; }; C253E2311DE34FBB0022A29F /* AudioPlayerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C253E2301DE34FBB0022A29F /* AudioPlayerController.swift */; }; C253E2341DE3776A0022A29F /* InlineAudioPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C253E2331DE3776A0022A29F /* InlineAudioPlayerView.swift */; }; - C253E2361DE398580022A29F /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C253E2351DE398580022A29F /* AudioPlayer.swift */; }; - C253E2381DE398980022A29F /* NativeAudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C253E2371DE398980022A29F /* NativeAudioPlayer.swift */; }; C253E23A1DE4D3DB0022A29F /* ChatInterfaceInputContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C253E2391DE4D3DB0022A29F /* ChatInterfaceInputContext.swift */; }; C253E23C1DE4D4080022A29F /* ChatInterfaceStateContextQueries.swift in Sources */ = {isa = PBXBuildFile; fileRef = C253E23B1DE4D4080022A29F /* ChatInterfaceStateContextQueries.swift */; }; C253E23E1DE61CB50022A29F /* ContextListRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C253E23D1DE61CB50022A29F /* ContextListRowItem.swift */; }; + C256A9141FB9CBF10043D497 /* LocalAuthentication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C256A9131FB9CBF10043D497 /* LocalAuthentication.framework */; }; + C256A9161FB9E1490043D497 /* AdditionalSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C256A9151FB9E1490043D497 /* AdditionalSettings.swift */; }; C258D1B41F8D385700458478 /* PreHistorySettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C258D1B31F8D385700458478 /* PreHistorySettingsController.swift */; }; C258D1B61F8D3A0D00458478 /* PreHistoryControllerStructures.swift in Sources */ = {isa = PBXBuildFile; fileRef = C258D1B51F8D3A0D00458478 /* PreHistoryControllerStructures.swift */; }; C25911371DF1A68200671E72 /* ChatInputRecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C25911361DF1A68200671E72 /* ChatInputRecordingView.swift */; }; C2593FBF1F7D242E00F6D2B1 /* TGPaintShader.m in Sources */ = {isa = PBXBuildFile; fileRef = C2593FBE1F7D241E00F6D2B1 /* TGPaintShader.m */; }; C259ED1C1DB8DC78008E6712 /* ChatNavigateScroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C259ED1B1DB8DC78008E6712 /* ChatNavigateScroller.swift */; }; C259ED1E1DB956C1008E6712 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C259ED1D1DB956C1008E6712 /* QuartzCore.framework */; }; - C25BB1691F867FEE0089ED02 /* ChatVideoAccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C25BB1681F867FEE0089ED02 /* ChatVideoAccessoryView.swift */; }; + C25BB1691F867FEE0089ED02 /* ChatMessageAccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C25BB1681F867FEE0089ED02 /* ChatMessageAccessoryView.swift */; }; C25C132D1E8A404F00AE26A1 /* InstalledStickerPacksController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C25C132C1E8A404F00AE26A1 /* InstalledStickerPacksController.swift */; }; C25C132F1E8A405E00AE26A1 /* ArchivedStickerPacksController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C25C132E1E8A405E00AE26A1 /* ArchivedStickerPacksController.swift */; }; C25C13311E8A406D00AE26A1 /* FeaturedStickerPacksController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C25C13301E8A406D00AE26A1 /* FeaturedStickerPacksController.swift */; }; @@ -289,16 +687,15 @@ C26505971E041B91001954DC /* MGalleryGIFItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C26505961E041B90001954DC /* MGalleryGIFItem.swift */; }; C26546CC1EA0AC3C00E3969A /* ChatVideoMessageItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C26546CB1EA0AC3C00E3969A /* ChatVideoMessageItem.swift */; }; C26A37EC1E5DE465006977AC /* ChannelAdminsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C26A37EB1E5DE464006977AC /* ChannelAdminsViewController.swift */; }; - C26A37EE1E5DE48F006977AC /* ChannelBlacklistViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C26A37ED1E5DE48F006977AC /* ChannelBlacklistViewController.swift */; }; + C26A37EE1E5DE48F006977AC /* ChannelBlocklistViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C26A37ED1E5DE48F006977AC /* ChannelBlocklistViewController.swift */; }; C26A71991DC9FA5100F69385 /* EditMessageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C26A71981DC9FA5100F69385 /* EditMessageModel.swift */; }; C26A719B1DC9FB3600F69385 /* InputPasteboardParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C26A719A1DC9FB3600F69385 /* InputPasteboardParser.swift */; }; C26D8A3C1E464944002FAA3F /* JoinLinkPreviewModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C26D8A3B1E464944002FAA3F /* JoinLinkPreviewModalController.swift */; }; C26E82D11E83EFFE0046DF2F /* TimeObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = C26E82D01E83EFFE0046DF2F /* TimeObserver.m */; }; C271EB901EB8E6A40034792D /* SelectivePrivacySettingsPeersController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C271EB8F1EB8E6A40034792D /* SelectivePrivacySettingsPeersController.swift */; }; - C271EB971EB916870034792D /* libtgvoip.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C271EB961EB916870034792D /* libtgvoip.framework */; }; - C271EB9B1EB9DEF00034792D /* CallBridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = C271EB9A1EB9DEF00034792D /* CallBridge.mm */; }; + C271EB9B1EB9DEF00034792D /* OngoingCallThreadLocalContext.mm in Sources */ = {isa = PBXBuildFile; fileRef = C271EB9A1EB9DEF00034792D /* OngoingCallThreadLocalContext.mm */; }; C271EBA21EB9F04E0034792D /* TGCallUtils.mm in Sources */ = {isa = PBXBuildFile; fileRef = C271EBA11EB9F04E0034792D /* TGCallUtils.mm */; }; - C271EBE91EBA22FE0034792D /* TGCallConnectionDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = C271EBE81EBA22FE0034792D /* TGCallConnectionDescription.m */; }; + C271EBE91EBA22FE0034792D /* OngoingCallConnectionDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = C271EBE81EBA22FE0034792D /* OngoingCallConnectionDescription.m */; }; C271EBEB1EBA3BC90034792D /* PCallSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = C271EBEA1EBA3BC90034792D /* PCallSession.swift */; }; C275932E1DF6E1CE00A0807A /* AboutModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C275932D1DF6E1CE00A0807A /* AboutModalController.swift */; }; C275E9EF1F8FCA4200D3D8C0 /* PhoneNumberIntroController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C275E9EE1F8FCA4200D3D8C0 /* PhoneNumberIntroController.swift */; }; @@ -307,10 +704,6 @@ C276248C1D95AF7600FE5B2B /* ObjcUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = C276248B1D95AF7600FE5B2B /* ObjcUtils.m */; }; C276248E1D95B4F300FE5B2B /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C276248D1D95B4F300FE5B2B /* Extensions.swift */; }; C276383E1E8A9A86009E7839 /* StickerSetTableRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C276383D1E8A9A86009E7839 /* StickerSetTableRowItem.swift */; }; - C276AFC71F74F2CF00DEDD8E /* Sparkle.framework in Copy Files */ = {isa = PBXBuildFile; fileRef = C231992D1EE006330011BEBE /* Sparkle.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - C276AFC81F74F2CF00DEDD8E /* HockeySDK.framework in Copy Files */ = {isa = PBXBuildFile; fileRef = C2A71CD81DDA0FA300C69F73 /* HockeySDK.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - C276AFC91F74F2D200DEDD8E /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C231992D1EE006330011BEBE /* Sparkle.framework */; }; - C276AFCA1F74F2D200DEDD8E /* HockeySDK.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C2A71CD81DDA0FA300C69F73 /* HockeySDK.framework */; }; C2777B5B1DCE11A5008B69DD /* SendingClockProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2777B5A1DCE11A5008B69DD /* SendingClockProgress.swift */; }; C2777B601DCF4766008B69DD /* CoreExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2777B5F1DCF4766008B69DD /* CoreExtension.swift */; }; C2777B621DCFB4C9008B69DD /* ChatServiceItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2777B611DCFB4C9008B69DD /* ChatServiceItem.swift */; }; @@ -325,20 +718,20 @@ C27AAFE91DE9DA61009B9629 /* CountryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C27AAFE81DE9DA61009B9629 /* CountryManager.swift */; }; C27AAFED1DEB1D72009B9629 /* SignalUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C27AAFEC1DEB1D72009B9629 /* SignalUtils.swift */; }; C27AAFEF1DEB2EA9009B9629 /* EmptyComposeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C27AAFEE1DEB2EA9009B9629 /* EmptyComposeController.swift */; }; - C28149881EA7F22200BB933E /* PreparedChatHistoryViewTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = C28149871EA7F22200BB933E /* PreparedChatHistoryViewTransition.swift */; }; + C27BAC7A20CFCE68007A7508 /* PassportSettingsHeaderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C27BAC7920CFCE68007A7508 /* PassportSettingsHeaderItem.swift */; }; C281498A1EA7F44300BB933E /* ListViewIntermediateState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C28149891EA7F44300BB933E /* ListViewIntermediateState.swift */; }; C2844AD71DA907E8009308DC /* EntertainmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2844AD61DA907E8009308DC /* EntertainmentViewController.swift */; }; C2844AE01DA90C8A009308DC /* emoji.txt in Resources */ = {isa = PBXBuildFile; fileRef = C2844ADF1DA90C8A009308DC /* emoji.txt */; }; C28BAB271DF980DE0027CE3A /* AudioRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = C28BAB261DF980DE0027CE3A /* AudioRecorder.swift */; }; C28BAB291DF981320027CE3A /* AudioWaveform.swift in Sources */ = {isa = PBXBuildFile; fileRef = C28BAB281DF981320027CE3A /* AudioWaveform.swift */; }; C28BAB2C1DF9C2790027CE3A /* DateUtils.mm in Sources */ = {isa = PBXBuildFile; fileRef = C28BAB2B1DF9C2790027CE3A /* DateUtils.mm */; }; + C2905E1C207E4D9E00990AD7 /* InstantPageAudioView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2905E1B207E4D9E00990AD7 /* InstantPageAudioView.swift */; }; + C2905E1E207E545600990AD7 /* InstantPageAudioItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2905E1D207E545600990AD7 /* InstantPageAudioItem.swift */; }; C291942F1DCC6E2200359491 /* DeclareEncodables.swift in Sources */ = {isa = PBXBuildFile; fileRef = C291942E1DCC6E2200359491 /* DeclareEncodables.swift */; }; C291E2731E8AFA2C00D397BA /* ShareApplicationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C291E2721E8AFA2C00D397BA /* ShareApplicationContext.swift */; }; C291E2741E8B051100D397BA /* ImageUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2EA17801E2FD50A00887153 /* ImageUtils.swift */; }; - C291E2751E8B051900D397BA /* PhotoCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = C210742B1E780CC1006EE5EF /* PhotoCache.swift */; }; C29340F11F506C310074991E /* EmptyGroupstickerSearchRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C29340F01F506C310074991E /* EmptyGroupstickerSearchRowItem.swift */; }; C295C65F1F75808600BA309D /* ChatAdditionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C295C65E1F75808600BA309D /* ChatAdditionController.swift */; }; - C29670791F0FAAC800884DA2 /* AppearanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C29670781F0FAAC800884DA2 /* AppearanceViewController.swift */; }; C296707B1F0FBFB500884DA2 /* ThemeSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C296707A1F0FBFB500884DA2 /* ThemeSettings.swift */; }; C296AF7F1D8D38E5001DBB59 /* ChatRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C296AF7E1D8D38E5001DBB59 /* ChatRowView.swift */; }; C296AF861D8DB178001DBB59 /* MediaUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C296AF851D8DB178001DBB59 /* MediaUtils.swift */; }; @@ -368,11 +761,10 @@ C29B5F4F1DC8F39A00D13E65 /* FWDNavigationAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = C29B5F4E1DC8F39A00D13E65 /* FWDNavigationAction.swift */; }; C29C3E5B1E421F1700193A7E /* ChatAccessoryModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C29C3E5A1E421F1700193A7E /* ChatAccessoryModel.swift */; }; C29C3E6F1E4352C100193A7E /* ContextStickerRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C29C3E6E1E4352C100193A7E /* ContextStickerRowItem.swift */; }; - C29C3E711E43881500193A7E /* StickerPreviewModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C29C3E701E43881500193A7E /* StickerPreviewModalController.swift */; }; + C29C3E711E43881500193A7E /* ModalPreviewViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = C29C3E701E43881500193A7E /* ModalPreviewViews.swift */; }; C29C3E731E4397F300193A7E /* StickerPreviewHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C29C3E721E4397F300193A7E /* StickerPreviewHandler.swift */; }; C29E0EE01F4DC43100C0C7A8 /* InstantViewAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = C29E0EDF1F4DC43100C0C7A8 /* InstantViewAppearance.swift */; }; C29E0EE21F4EFB5100C0C7A8 /* GroupStickerSetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C29E0EE11F4EFB5100C0C7A8 /* GroupStickerSetController.swift */; }; - C29F4C761F45F58B00DBFC00 /* MIHSliderView.m in Sources */ = {isa = PBXBuildFile; fileRef = C29F4C751F45F58B00DBFC00 /* MIHSliderView.m */; }; C29F4C781F45FBFF00DBFC00 /* InstantPageSlideshowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C29F4C771F45FBFF00DBFC00 /* InstantPageSlideshowItem.swift */; }; C29F4C7D1F47283600DBFC00 /* InstantPageBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C29F4C7C1F47283600DBFC00 /* InstantPageBrowser.swift */; }; C2A1054B1E0163D500B01F48 /* GalleryPageController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A1054A1E0163D500B01F48 /* GalleryPageController.swift */; }; @@ -392,7 +784,6 @@ C2A71CE11DDB18FF00C69F73 /* ThumbUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A71CE01DDB18FF00C69F73 /* ThumbUtils.swift */; }; C2A71CE31DDB2EBD00C69F73 /* GeneralSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A71CE21DDB2EBD00C69F73 /* GeneralSettingsViewController.swift */; }; C2A71CE71DDB2F8700C69F73 /* UsernameSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A71CE61DDB2F8700C69F73 /* UsernameSettingsViewController.swift */; }; - C2A71CE91DDB342100C69F73 /* NotificationSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A71CE81DDB342100C69F73 /* NotificationSettingsViewController.swift */; }; C2A71CED1DDB3C1000C69F73 /* ChatHeaderController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A71CEC1DDB3C1000C69F73 /* ChatHeaderController.swift */; }; C2A72D901DEC66F300C3B945 /* LoginErrorStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A72D8F1DEC66F300C3B945 /* LoginErrorStateView.swift */; }; C2A87DE81F4C6910002D3F73 /* InstantViewWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2A87DE71F4C6910002D3F73 /* InstantViewWindow.swift */; }; @@ -403,7 +794,6 @@ C2AC9C181E1E687E0085C7DE /* GlobalBadgeNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2AC9C171E1E687E0085C7DE /* GlobalBadgeNode.swift */; }; C2AF01131F01543200D8AC1D /* ExportProxyModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2AF01121F01543200D8AC1D /* ExportProxyModalController.swift */; }; C2AF011D1F03D4C600D8AC1D /* TGCallAesCtr.m in Sources */ = {isa = PBXBuildFile; fileRef = C2AF011C1F03D4C600D8AC1D /* TGCallAesCtr.m */; }; - C2AF3B821E5CD79200DFDD81 /* ConvertGroupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2AF3B811E5CD79200DFDD81 /* ConvertGroupViewController.swift */; }; C2B0722E1DFEDE430082939D /* UsernameInputRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2B0722D1DFEDE430082939D /* UsernameInputRowItem.swift */; }; C2B1A0EF1D9D94CE00ACB1DD /* SeparatorRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2B1A0EE1D9D94CE00ACB1DD /* SeparatorRowItem.swift */; }; C2B1A0F11D9D94E400ACB1DD /* SeparatorRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2B1A0F01D9D94E400ACB1DD /* SeparatorRowView.swift */; }; @@ -413,7 +803,6 @@ C2B1A1271DA3D84900ACB1DD /* ChatInputAccessory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2B1A1261DA3D84900ACB1DD /* ChatInputAccessory.swift */; }; C2B1A1361DA6587100ACB1DD /* GalleryViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2B1A1351DA6587100ACB1DD /* GalleryViewer.swift */; }; C2B1B11E1E5F151D00895E0D /* ChannelVisibilityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2B1B11D1E5F151D00895E0D /* ChannelVisibilityController.swift */; }; - C2B1B1201E5F170A00895E0D /* ValidateAddressNameInteractive.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2B1B11F1E5F170A00895E0D /* ValidateAddressNameInteractive.swift */; }; C2B1B1261E5F840B00895E0D /* PeerInfoUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2B1B1251E5F840B00895E0D /* PeerInfoUtils.swift */; }; C2B4D0C11E34E07100CBC4E6 /* PrettyGridUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2B4D0C01E34E07100CBC4E6 /* PrettyGridUtils.swift */; }; C2B4D0C81E36048000CBC4E6 /* ChatActivitiesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2B4D0C71E36047F00CBC4E6 /* ChatActivitiesModel.swift */; }; @@ -425,23 +814,21 @@ C2B9BE891EFC5E7000D6B96F /* Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2B9BE881EFC5E7000D6B96F /* Appearance.swift */; }; C2BB120A1ED87C5A00BDE46A /* ControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2BB12091ED87C5A00BDE46A /* ControllerExtension.swift */; }; C2BB2DB01F8BDF6700520255 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2BB2DAF1F8BDF6700520255 /* Config.swift */; }; - C2C5C2051EF822B900AEA252 /* ProxySettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2C5C2041EF822B900AEA252 /* ProxySettingsViewController.swift */; }; - C2C738F11DD898DA00CE9D8A /* AVKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C2C738F01DD898DA00CE9D8A /* AVKit.framework */; }; + C2C415E51FA33D1A00FF36F4 /* InputFormatterPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2C415E41FA33D1A00FF36F4 /* InputFormatterPopover.swift */; }; C2C73A5B1EEAF3AE00DB8420 /* ChannelEventFilterModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2C73A5A1EEAF3AE00DB8420 /* ChannelEventFilterModalController.swift */; }; C2C98FEF1E818FB5009CBDB7 /* ClearUserNotifies.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2C98FEE1E818FB5009CBDB7 /* ClearUserNotifies.swift */; }; C2C9B92C1E80165D00380D79 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C2C9B9281E8011B000380D79 /* IOKit.framework */; }; - C2C9B92D1E80166400380D79 /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C2C9B92A1E8011CD00380D79 /* Carbon.framework */; }; - C2C9B9341E8016C400380D79 /* NSObject+SPInvocationGrabbing.m in Sources */ = {isa = PBXBuildFile; fileRef = C2C9B9331E8016C400380D79 /* NSObject+SPInvocationGrabbing.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; - C2C9B9371E8016D400380D79 /* SPMediaKeyTap.m in Sources */ = {isa = PBXBuildFile; fileRef = C2C9B9361E8016D400380D79 /* SPMediaKeyTap.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; C2CBCAC01D81528700142EC0 /* System.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2CBCABF1D81528700142EC0 /* System.swift */; }; C2CBCAC51D81649E00142EC0 /* ChatListRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2CBCAC41D81649E00142EC0 /* ChatListRowView.swift */; }; + C2CE43E420E2CFE800656543 /* PlayerListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2CE43E320E2CFE700656543 /* PlayerListController.swift */; }; + C2CE43E920F4F74F00656543 /* UpdateModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2CE43E820F4F74F00656543 /* UpdateModalController.swift */; }; C2CFCABE1EBB4A4D00843F6A /* voip_connecting.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = C2CFCABD1EBB4A4D00843F6A /* voip_connecting.mp3 */; }; C2CFCAC01EBB9C8100843F6A /* CallAudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2CFCABF1EBB9C8100843F6A /* CallAudioPlayer.swift */; }; C2D1839D1E4DBF7E001CE25A /* MGalleryPeerPhotoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2D1839C1E4DBF7E001CE25A /* MGalleryPeerPhotoItem.swift */; }; C2D187EE1E28B9CB0038961D /* ShareInlineResultNavigationAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2D187ED1E28B9CB0038961D /* ShareInlineResultNavigationAction.swift */; }; C2D187F01E28C58C0038961D /* ContextSwitchPeerRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2D187EF1E28C58C0038961D /* ContextSwitchPeerRowItem.swift */; }; C2D187F21E28D0840038961D /* ChatSwitchInlineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2D187F11E28D0840038961D /* ChatSwitchInlineController.swift */; }; - C2D2CAED1E64579700939968 /* StickersPackPreviewModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2D2CAEC1E64579700939968 /* StickersPackPreviewModalController.swift */; }; + C2D2CAED1E64579700939968 /* StickerPackPreviewModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2D2CAEC1E64579700939968 /* StickerPackPreviewModalController.swift */; }; C2D2CAF01E64874600939968 /* StickerPackGridItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2D2CAEF1E64874600939968 /* StickerPackGridItem.swift */; }; C2DDA04E1EC0C024003531BB /* opening.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = C2DDA04D1EC0C024003531BB /* opening.mp3 */; }; C2DDA0501EC0C19A003531BB /* opening.m4a in Resources */ = {isa = PBXBuildFile; fileRef = C2DDA04F1EC0C19A003531BB /* opening.m4a */; }; @@ -458,70 +845,232 @@ C2DE5D3C1F3CAE120081EC1E /* InstantPageTextItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2DE5D3B1F3CAE120081EC1E /* InstantPageTextItem.swift */; }; C2DE5D3E1F3CAF2B0081EC1E /* InstantPageShapeItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2DE5D3D1F3CAF2B0081EC1E /* InstantPageShapeItem.swift */; }; C2DE5D401F3CB0380081EC1E /* InstantPageTileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2DE5D3F1F3CB0380081EC1E /* InstantPageTileView.swift */; }; - C2DEC87F1DECB8C800F6544A /* TelegramApplicationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2DEC87E1DECB8C800F6544A /* TelegramApplicationContext.swift */; }; C2DF47961DE71160003AA6C0 /* GIFContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2DF47951DE71160003AA6C0 /* GIFContainerView.swift */; }; - C2DF47BA1DE79574003AA6C0 /* bitwise.c in Sources */ = {isa = PBXBuildFile; fileRef = C2DF479B1DE79574003AA6C0 /* bitwise.c */; }; - C2DF47BB1DE79574003AA6C0 /* framing.c in Sources */ = {isa = PBXBuildFile; fileRef = C2DF479C1DE79574003AA6C0 /* framing.c */; }; - C2DF47BC1DE79574003AA6C0 /* libopus.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C2DF47A71DE79574003AA6C0 /* libopus.a */; }; - C2DF47BD1DE79574003AA6C0 /* diag_range.c in Sources */ = {isa = PBXBuildFile; fileRef = C2DF47A91DE79574003AA6C0 /* diag_range.c */; }; - C2DF47BE1DE79574003AA6C0 /* opus_header.c in Sources */ = {isa = PBXBuildFile; fileRef = C2DF47AB1DE79574003AA6C0 /* opus_header.c */; }; - C2DF47BF1DE79574003AA6C0 /* opusenc.m in Sources */ = {isa = PBXBuildFile; fileRef = C2DF47AE1DE79574003AA6C0 /* opusenc.m */; }; - C2DF47C01DE79574003AA6C0 /* picture.c in Sources */ = {isa = PBXBuildFile; fileRef = C2DF47AF1DE79574003AA6C0 /* picture.c */; }; - C2DF47C11DE79574003AA6C0 /* wav_io.c in Sources */ = {isa = PBXBuildFile; fileRef = C2DF47B11DE79574003AA6C0 /* wav_io.c */; }; - C2DF47C21DE79574003AA6C0 /* info.c in Sources */ = {isa = PBXBuildFile; fileRef = C2DF47B41DE79574003AA6C0 /* info.c */; }; - C2DF47C31DE79574003AA6C0 /* internal.c in Sources */ = {isa = PBXBuildFile; fileRef = C2DF47B51DE79574003AA6C0 /* internal.c */; }; - C2DF47C41DE79574003AA6C0 /* opusfile.c in Sources */ = {isa = PBXBuildFile; fileRef = C2DF47B71DE79574003AA6C0 /* opusfile.c */; }; - C2DF47C51DE79574003AA6C0 /* stream.c in Sources */ = {isa = PBXBuildFile; fileRef = C2DF47B91DE79574003AA6C0 /* stream.c */; }; - C2DF47CB1DE79719003AA6C0 /* TGDataItem.m in Sources */ = {isa = PBXBuildFile; fileRef = C2DF47CA1DE79719003AA6C0 /* TGDataItem.m */; }; C2DF47CE1DE79751003AA6C0 /* ATQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = C2DF47CD1DE79751003AA6C0 /* ATQueue.m */; }; - C2DF47D01DE82475003AA6C0 /* OpusAudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2DF47CF1DE82475003AA6C0 /* OpusAudioPlayer.swift */; }; - C2DF47D31DE824FD003AA6C0 /* OpusObjcBridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = C2DF47D21DE824FD003AA6C0 /* OpusObjcBridge.mm */; }; C2DF47DA1DE82653003AA6C0 /* NSObject+TGLock.m in Sources */ = {isa = PBXBuildFile; fileRef = C2DF47D91DE82653003AA6C0 /* NSObject+TGLock.m */; }; C2DF47E01DE846AB003AA6C0 /* ChatAudioContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2DF47DF1DE846AB003AA6C0 /* ChatAudioContentView.swift */; }; C2DF47E21DE846B8003AA6C0 /* ChatMusicContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2DF47E11DE846B8003AA6C0 /* ChatMusicContentView.swift */; }; C2DF47E41DE84A67003AA6C0 /* ChatVoiceRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2DF47E31DE84A67003AA6C0 /* ChatVoiceRowItem.swift */; }; C2DF47E91DE892E3003AA6C0 /* ChatToasterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2DF47E81DE892E3003AA6C0 /* ChatToasterView.swift */; }; C2DF47EE1DE9A1F3003AA6C0 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2DF47ED1DE9A1F3003AA6C0 /* LoginViewController.swift */; }; - C2E064641ECCB24000387BB8 /* NativeCallSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E064621ECCB24000387BB8 /* NativeCallSettingsViewController.swift */; }; - C2E064651ECCB24000387BB8 /* NativeCallSettingsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = C2E064631ECCB24000387BB8 /* NativeCallSettingsViewController.xib */; }; C2E0646B1ECF137300387BB8 /* ChatInvoiceItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E0646A1ECF137300387BB8 /* ChatInvoiceItem.swift */; }; C2E40A1B1E37ADAF0099AC7D /* PeerMediaEmptyRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E40A1A1E37ADAF0099AC7D /* PeerMediaEmptyRowItem.swift */; }; C2E52A0D1EB8C386009AF87D /* SelectivePrivacySettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E52A0C1EB8C385009AF87D /* SelectivePrivacySettingsController.swift */; }; + C2E6F3CF1F9F85260023653D /* ContextHashtagRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E6F3CE1F9F85260023653D /* ContextHashtagRowItem.swift */; }; C2E8694D1F43500D00BDD0A2 /* ChatNavigationMention.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E8694C1F43500D00BDD0A2 /* ChatNavigationMention.swift */; }; + C2E8BA071FB5EF4C00DEB5E2 /* GalleryThumbsControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E8BA061FB5EF4C00DEB5E2 /* GalleryThumbsControl.swift */; }; + C2E8BA0A1FB5F15900DEB5E2 /* GalleryThumbsControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E8BA091FB5F15900DEB5E2 /* GalleryThumbsControlView.swift */; }; C2EA177F1E2FD50000887153 /* TransformImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2EA177E1E2FD50000887153 /* TransformImageView.swift */; }; C2EA17811E2FD50A00887153 /* ImageUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2EA17801E2FD50A00887153 /* ImageUtils.swift */; }; - C2EA53471F751EF300C183F7 /* GalleryMessageEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2EA53461F751EF300C183F7 /* GalleryMessageEntry.swift */; }; + C2EBBEA11FB5CA94009AD8ED /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C2EBBEA01FB5CA94009AD8ED /* CoreServices.framework */; }; C2F4ED1B1EC5AE1D005F2696 /* CallRatingModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2F4ED1A1EC5AE1D005F2696 /* CallRatingModalViewController.swift */; }; C2F6190D1E844DCD007A051B /* TelegramAccountAuxiliaryMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2F6190C1E844DCD007A051B /* TelegramAccountAuxiliaryMethods.swift */; }; C2F8923D1E3FA51000D98B2D /* PasteboardUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2F8923C1E3FA51000D98B2D /* PasteboardUtils.swift */; }; C2F93A2D1F3C55C500BCD48F /* EmojiToleranceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2F93A2C1F3C55C500BCD48F /* EmojiToleranceController.swift */; }; - C2F952B01F8E1C840056E586 /* CachedAdminIds.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2F952AF1F8E1C840056E586 /* CachedAdminIds.swift */; }; C2F9C4481F9500B4002B2CBF /* TwoStepVerificationUnlockController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2F9C4471F9500B4002B2CBF /* TwoStepVerificationUnlockController.swift */; }; - C2F9C44A1F9500C3002B2CBF /* TwoStepVerificationUnlockStructures.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2F9C4491F9500C3002B2CBF /* TwoStepVerificationUnlockStructures.swift */; }; C2F9C44C1F95FE58002B2CBF /* Markdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2F9C44B1F95FE58002B2CBF /* Markdown.swift */; }; C2FB2FAB1EBF73D00093C8BA /* RecentCallsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2FB2FAA1EBF73CF0093C8BA /* RecentCallsViewController.swift */; }; - C2FBC1D71DC61AFF0063A23B /* TelegramCoreMac.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C2FBC1D61DC61AFF0063A23B /* TelegramCoreMac.framework */; }; - C2FBC1D91DC61B050063A23B /* SwiftSignalKitMac.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C2FBC1D81DC61B050063A23B /* SwiftSignalKitMac.framework */; }; - C2FBC1DF1DC61B580063A23B /* MtProtoKitMac.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C2FBC1DE1DC61B580063A23B /* MtProtoKitMac.framework */; }; C2FBC1E71DC631980063A23B /* SPPreviewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2FBC1E61DC631980063A23B /* SPPreviewController.swift */; }; C2FD33E91E696A86008D13D4 /* GroupsInCommonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2FD33E81E696A86008D13D4 /* GroupsInCommonViewController.swift */; }; C2FD33EE1E697B31008D13D4 /* TransformOutgoingMessageMedia.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2FD33ED1E697B31008D13D4 /* TransformOutgoingMessageMedia.swift */; }; C2FD33F01E697EC2008D13D4 /* QuickLook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C2FD33EF1E697EC2008D13D4 /* QuickLook.framework */; }; C2FD33F21E69CF6D008D13D4 /* ChatUrlPreviewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2FD33F11E69CF6D008D13D4 /* ChatUrlPreviewModel.swift */; }; - C2FD33F81E6C1486008D13D4 /* SSKeychain.m in Sources */ = {isa = PBXBuildFile; fileRef = C2FD33F51E6C1486008D13D4 /* SSKeychain.m */; }; - C2FD33F91E6C1486008D13D4 /* SSKeychainQuery.m in Sources */ = {isa = PBXBuildFile; fileRef = C2FD33F71E6C1486008D13D4 /* SSKeychainQuery.m */; }; C2FD33FB1E6C169C008D13D4 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C2FD33FA1E6C169C008D13D4 /* Security.framework */; }; - C2FD34131E6C2503008D13D4 /* LegacyImportAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2FD34121E6C2503008D13D4 /* LegacyImportAuthorization.swift */; }; C2FD34151E6C9003008D13D4 /* BaseApplicationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2FD34141E6C9003008D13D4 /* BaseApplicationSettings.swift */; }; C2FD382E1DCA1FA3009DC28C /* PreviewSenderController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2FD382D1DCA1FA3009DC28C /* PreviewSenderController.swift */; }; C2FD38321DCA215F009DC28C /* GeneralInputRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2FD38311DCA215F009DC28C /* GeneralInputRow.swift */; }; C2FF14601E532C0A007B7B14 /* SearchEmptyRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2FF145F1E532C0A007B7B14 /* SearchEmptyRowItem.swift */; }; + D001E974243B1A0A009025F9 /* LeftSidebarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D001E973243B1A0A009025F9 /* LeftSidebarController.swift */; }; + D001E976243B385E009025F9 /* FolderIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = D001E975243B385E009025F9 /* FolderIcons.swift */; }; + D001E978243B4639009025F9 /* LeftSidebarFolderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D001E977243B4639009025F9 /* LeftSidebarFolderItem.swift */; }; + D004167522D37AD00000566B /* StickerPackPanelRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D004167422D37AD00000566B /* StickerPackPanelRowItem.swift */; }; + D004167722D4AD3B0000566B /* StickerPackItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D004167622D4AD3B0000566B /* StickerPackItems.swift */; }; + D004BD2B23153415009A54B1 /* ThemePreviewModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D004BD2A23153415009A54B1 /* ThemePreviewModalController.swift */; }; + D00746CA257691760000DF74 /* StickerShimmerEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00746C9257691760000DF74 /* StickerShimmerEffectView.swift */; }; + D00746EE2577A6BE0000DF74 /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C2C9B92A1E8011CD00380D79 /* Carbon.framework */; }; + D00747042577AF0B0000DF74 /* PushToTalkRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00747032577AF0B0000DF74 /* PushToTalkRowItem.swift */; }; + D0076E372568482E007EF588 /* OpusBinding.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0076E362568482E007EF588 /* OpusBinding.framework */; }; + D0076E8B25685339007EF588 /* webrtc.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0076E8A25685339007EF588 /* webrtc.framework */; }; + D0076E9225685596007EF588 /* libwebp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0076E9125685596007EF588 /* libwebp.a */; }; + D0076E98256855A2007EF588 /* libopus.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0076E97256855A2007EF588 /* libopus.a */; }; + D0076E9B256855AB007EF588 /* libmac_framework_objc_static.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0076E9A256855AB007EF588 /* libmac_framework_objc_static.a */; }; + D0076EE825685A50007EF588 /* AppCenter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0076EE725685A50007EF588 /* AppCenter.framework */; }; + D0076EEB25685BB5007EF588 /* libbz2.1.0.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D0076EEA25685BB5007EF588 /* libbz2.1.0.tbd */; }; + D0076FFE25691EDE007EF588 /* FFMpegBinding.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0076FFD25691EDE007EF588 /* FFMpegBinding.framework */; }; + D0077009256925F9007EF588 /* libswresample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0076E9F25685750007EF588 /* libswresample.a */; }; + D007700A256925F9007EF588 /* libavutil.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0076EA125685750007EF588 /* libavutil.a */; }; + D007700B256925F9007EF588 /* libavformat.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0076E9E2568574F007EF588 /* libavformat.a */; }; + D007700C256925F9007EF588 /* libavcodec.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0076EA025685750007EF588 /* libavcodec.a */; }; + D00B191C24E54BA3006CCB87 /* CallReceptionControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00B191B24E54BA3006CCB87 /* CallReceptionControl.swift */; }; + D00B191E24E54F20006CCB87 /* CallStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00B191D24E54F20006CCB87 /* CallStatusView.swift */; }; + D00B318725729F0500D62056 /* DataItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00B318625729F0500D62056 /* DataItem.swift */; }; + D00C73B62302C196004B1E2B /* ChatScheduleController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00C73B52302C196004B1E2B /* ChatScheduleController.swift */; }; + D00CE4F72284D530008C1B4F /* BuildConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 9F291C9F2264E57F00C66267 /* BuildConfig.m */; }; + D00CE4F82284D5E3008C1B4F /* TransformOutgoingMessageMedia.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2FD33ED1E697B31008D13D4 /* TransformOutgoingMessageMedia.swift */; }; + D00CE50D2289C9B7008C1B4F /* MediaAnimatedStickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00CE50C2289C9B7008C1B4F /* MediaAnimatedStickerView.swift */; }; + D00CE50F2289CE87008C1B4F /* ChatAnimatedStickerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00CE50E2289CE87008C1B4F /* ChatAnimatedStickerItem.swift */; }; + D00EECD1252B47B1001EB99F /* CallFeedbackController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00EECD0252B47B1001EB99F /* CallFeedbackController.swift */; }; + D00EECD3252C703C001EB99F /* CallSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00EECD2252C703C001EB99F /* CallSettingsController.swift */; }; + D00EECD5252C9192001EB99F /* CameraPreviewRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00EECD4252C9192001EB99F /* CameraPreviewRowItem.swift */; }; + D00EECD7252CD2EF001EB99F /* MicrophonePreviewRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00EECD6252CD2EF001EB99F /* MicrophonePreviewRowItem.swift */; }; + D014193C22AE939F008667CB /* ModalOptionSetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D014193B22AE939F008667CB /* ModalOptionSetController.swift */; }; + D014193E22AE9A90008667CB /* GeneralLineSeparatorRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D014193D22AE9A90008667CB /* GeneralLineSeparatorRowItem.swift */; }; + D014AA242316CE0700CE5362 /* NewThemeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D014AA232316CE0700CE5362 /* NewThemeController.swift */; }; + D014AA262317D07D00CE5362 /* EditThemeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D014AA252317D07D00CE5362 /* EditThemeController.swift */; }; + D0186731223807D200A77C45 /* ChatListEmptyRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0186730223807D200A77C45 /* ChatListEmptyRowItem.swift */; }; + D01C731722A9814C000DA008 /* InputPasswordController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C731622A9814C000DA008 /* InputPasswordController.swift */; }; + D01CBE2D22A5384700F6A971 /* NotificationPreferencesController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01CBE2C22A5384600F6A971 /* NotificationPreferencesController.swift */; }; + D01E1F0422B39A4800AD6DAE /* LottiePlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01E1F0322B39A4800AD6DAE /* LottiePlayer.swift */; }; + D0276B7D22BD6511003155D8 /* DisplayLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0276B7C22BD6511003155D8 /* DisplayLink.swift */; }; + D02BD7C0232D0FB800D1814A /* AppAppearanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02BD7BF232D0FB800D1814A /* AppAppearanceViewController.swift */; }; + D02BD7C3232D14E800D1814A /* ThemePreviewRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02BD7C2232D14E800D1814A /* ThemePreviewRowItem.swift */; }; + D02BD7C5232D204F00D1814A /* ThemeListRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02BD7C4232D204F00D1814A /* ThemeListRowItem.swift */; }; + D02BD7C7232D4DB200D1814A /* AppearanceThumbs.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02BD7C6232D4DB200D1814A /* AppearanceThumbs.swift */; }; + D02F0A0022E8875800553411 /* SoftwareVideoSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02F09FF22E8875800553411 /* SoftwareVideoSource.swift */; }; + D02F0A0222E88C6E00553411 /* MediaPlayerFramePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02F0A0122E88C6E00553411 /* MediaPlayerFramePreview.swift */; }; + D02F0A0422E88C9900553411 /* UniversalSoftwareVideoSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02F0A0322E88C9900553411 /* UniversalSoftwareVideoSource.swift */; }; + D032AFA42578172400E67215 /* PushToTalk.swift in Sources */ = {isa = PBXBuildFile; fileRef = D032AFA32578172400E67215 /* PushToTalk.swift */; }; + D032AFA82578190C00E67215 /* TelegramShare.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = C232EA911E1D07E700C4D38C /* TelegramShare.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + D048EDDE24FFEC5600977606 /* ChannelCommentsControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = D048EDDD24FFEC5600977606 /* ChannelCommentsControls.swift */; }; + D04D213F230D68B800609388 /* CustomAccentColorModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04D213E230D68B800609388 /* CustomAccentColorModalController.swift */; }; + D04D2144230DB55B00609388 /* TelegramIconsTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04D2143230DB55B00609388 /* TelegramIconsTheme.swift */; }; + D04D2145230DB57300609388 /* TelegramIconsTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04D2143230DB55B00609388 /* TelegramIconsTheme.swift */; }; + D0530D3E24E69133003273BC /* CallControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0530D3D24E69132003273BC /* CallControl.swift */; }; + D0530D4024E693A5003273BC /* CameraViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0530D3F24E693A5003273BC /* CameraViews.swift */; }; + D0530D4224E69459003273BC /* CallTooltipView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0530D4124E69459003273BC /* CallTooltipView.swift */; }; + D0558D7C215141CF006B403D /* ChatInfoTouchbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0558D7B215141CF006B403D /* ChatInfoTouchbar.swift */; }; + D0558D852152B4D3006B403D /* GalleryTouchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0558D842152B4D3006B403D /* GalleryTouchBar.swift */; }; + D0558D872154047E006B403D /* GalleryTouchBarThumbItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0558D862154047E006B403D /* GalleryTouchBarThumbItemView.swift */; }; + D05F392322FB45450040F341 /* DateSelectorModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05F392222FB45450040F341 /* DateSelectorModalController.swift */; }; + D0675D84217F1F27004900A7 /* ArchiverContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0675D83217F1F27004900A7 /* ArchiverContext.swift */; }; + D06ACAEF231AC265002DCD81 /* ChatMessageBubbleImages.swift in Sources */ = {isa = PBXBuildFile; fileRef = C241025C1FD5702D00DB8625 /* ChatMessageBubbleImages.swift */; }; + D06ACAF0231AC3E5002DCD81 /* ParseAppearanceColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = C21BE3B01FD14CDB00C1C849 /* ParseAppearanceColors.swift */; }; + D06C6950253887F600DD9005 /* SlotMachineValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06C694F253887F600DD9005 /* SlotMachineValue.swift */; }; + D06C69522538894C00DD9005 /* SlotsMediaContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06C69512538894C00DD9005 /* SlotsMediaContentView.swift */; }; + D06C69532539E30200DD9005 /* SlotMachineValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06C694F253887F600DD9005 /* SlotMachineValue.swift */; }; + D06C69552539F6BD00DD9005 /* LiveLocationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06C69542539F6BD00DD9005 /* LiveLocationViewController.swift */; }; + D06C695F253DC5DF00DD9005 /* LocationPreviewMapRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06C695E253DC5DF00DD9005 /* LocationPreviewMapRowItem.swift */; }; + D06C7BB5247D6F4B00E67C3C /* SoftwareVideoLayerFrameManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06C7BB4247D6F4B00E67C3C /* SoftwareVideoLayerFrameManager.swift */; }; + D06C7BB7247D6FA900E67C3C /* SampleBufferPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06C7BB6247D6FA900E67C3C /* SampleBufferPool.swift */; }; + D06C7BB9247E664900E67C3C /* SoftwareVideoThumbnailLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06C7BB8247E664900E67C3C /* SoftwareVideoThumbnailLayer.swift */; }; + D06C7BBB247FAE3E00E67C3C /* GifPlayerBufferView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06C7BBA247FAE3E00E67C3C /* GifPlayerBufferView.swift */; }; + D06CD4F022F22F1100E444DF /* PhotoCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = C210742B1E780CC1006EE5EF /* PhotoCache.swift */; }; + D06E38A124A51FE000C7D03A /* TgVoipWebrtc.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D06E38A024A51FE000C7D03A /* TgVoipWebrtc.framework */; }; + D06E38AC24A5F3B700C7D03A /* MetalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D06E38AA24A5F3B700C7D03A /* MetalKit.framework */; }; + D06E38AD24A5F3B700C7D03A /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D06E38AB24A5F3B700C7D03A /* Metal.framework */; }; + D070DB8122D3638F008A0BBE /* StickersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D070DB8022D3638F008A0BBE /* StickersViewController.swift */; }; + D071E8A421496F38001B6024 /* ChatListTouchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D071E8A321496F38001B6024 /* ChatListTouchBar.swift */; }; + D071E8A6214A805C001B6024 /* ChatTouchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D071E8A5214A805C001B6024 /* ChatTouchBar.swift */; }; + D071E8A8214BBE21001B6024 /* ChatStickersTouchBarPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = D071E8A7214BBE21001B6024 /* ChatStickersTouchBarPopover.swift */; }; + D071E8AA214BC589001B6024 /* TouchBarStickerItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D071E8A9214BC589001B6024 /* TouchBarStickerItemView.swift */; }; + D071E8AC214BE6E7001B6024 /* TouchBarScrubberHeaderItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D071E8AB214BE6E7001B6024 /* TouchBarScrubberHeaderItemView.swift */; }; + D071E8B3214C15C7001B6024 /* TouchBarEmojiPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D071E8B2214C15C7001B6024 /* TouchBarEmojiPicker.swift */; }; + D071E8B5214C1935001B6024 /* TouchBarEmojiItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D071E8B4214C1935001B6024 /* TouchBarEmojiItemView.swift */; }; + D07450E6233D61B800769D7F /* brilliant_static.tgs in Resources */ = {isa = PBXBuildFile; fileRef = D07450DC233D61B000769D7F /* brilliant_static.tgs */; }; + D07450E7233D61B800769D7F /* gift.tgs in Resources */ = {isa = PBXBuildFile; fileRef = D07450DD233D61B100769D7F /* gift.tgs */; }; + D07450E8233D61B800769D7F /* brilliant_loading.tgs in Resources */ = {isa = PBXBuildFile; fileRef = D07450DE233D61B200769D7F /* brilliant_loading.tgs */; }; + D07450E9233D61B800769D7F /* keyboard_typing.tgs in Resources */ = {isa = PBXBuildFile; fileRef = D07450DF233D61B200769D7F /* keyboard_typing.tgs */; }; + D07450EA233D61B800769D7F /* write_words.tgs in Resources */ = {isa = PBXBuildFile; fileRef = D07450E0233D61B300769D7F /* write_words.tgs */; }; + D07450EB233D61B800769D7F /* fly_dollar.tgs in Resources */ = {isa = PBXBuildFile; fileRef = D07450E1233D61B400769D7F /* fly_dollar.tgs */; }; + D07450EC233D61B800769D7F /* swap_money.tgs in Resources */ = {isa = PBXBuildFile; fileRef = D07450E2233D61B500769D7F /* swap_money.tgs */; }; + D07450ED233D61B800769D7F /* chiken_born.tgs in Resources */ = {isa = PBXBuildFile; fileRef = D07450E3233D61B600769D7F /* chiken_born.tgs */; }; + D07450EE233D61B800769D7F /* keychain.tgs in Resources */ = {isa = PBXBuildFile; fileRef = D07450E4233D61B700769D7F /* keychain.tgs */; }; + D07450EF233D61B800769D7F /* smart_guy.tgs in Resources */ = {isa = PBXBuildFile; fileRef = D07450E5233D61B800769D7F /* smart_guy.tgs */; }; + D07450F12340DC8200769D7F /* WalletConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07450F02340DC8200769D7F /* WalletConfiguration.swift */; }; + D07450F32340E89100769D7F /* sad_man.tgs in Resources */ = {isa = PBXBuildFile; fileRef = D07450F22340E89000769D7F /* sad_man.tgs */; }; + D07450F52340F28D00769D7F /* wallet_success_created.tgs in Resources */ = {isa = PBXBuildFile; fileRef = D07450F42340F28D00769D7F /* wallet_success_created.tgs */; }; + D074A56524A1DE7700E92F8A /* OngoingCallContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074A56424A1DE7700E92F8A /* OngoingCallContext.swift */; }; + D074A56724A1DF5200E92F8A /* VoipDerivedState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D074A56624A1DF5200E92F8A /* VoipDerivedState.swift */; }; + D076A07E248A5F890077BC0A /* GifPanelTabRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D076A07D248A5F890077BC0A /* GifPanelTabRowItem.swift */; }; + D076F86D22959958004F895A /* InlineLoginController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D076F86C22959958004F895A /* InlineLoginController.swift */; }; + D076F8702295B24D004F895A /* InlineAuthOptionRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D076F86F2295B24D004F895A /* InlineAuthOptionRowItem.swift */; }; + D076F8872296CAB2004F895A /* ChannelDisscussionGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = D076F8862296CAB2004F895A /* ChannelDisscussionGroup.swift */; }; + D076F88D2296FD18004F895A /* AnimtedStickerHeaderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D076F88C2296FD18004F895A /* AnimtedStickerHeaderItem.swift */; }; + D076F88F2297285F004F895A /* DiscussionSetModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D076F88E2297285F004F895A /* DiscussionSetModalController.swift */; }; + D076F891229823DA004F895A /* ChannelDiscussionInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D076F890229823DA004F895A /* ChannelDiscussionInputView.swift */; }; + D07C6D7D234698C600468B1A /* DynamicHeightRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07C6D7C234698C600468B1A /* DynamicHeightRowItem.swift */; }; + D07C6D802346A43000468B1A /* monkey_unsee.tgs in Resources */ = {isa = PBXBuildFile; fileRef = D07C6D7E2346A42E00468B1A /* monkey_unsee.tgs */; }; + D07C6D812346A43000468B1A /* monkey_see.tgs in Resources */ = {isa = PBXBuildFile; fileRef = D07C6D7F2346A42F00468B1A /* monkey_see.tgs */; }; + D0830FC424127473006198E7 /* new_folder.tgs in Resources */ = {isa = PBXBuildFile; fileRef = D0830FC324127468006198E7 /* new_folder.tgs */; }; + D08E5C9822C583CE007B1C09 /* Tuple.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08E5C9722C583CE007B1C09 /* Tuple.swift */; }; + D0906CFC254BFE1D000E6961 /* ModalPreviews.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0906CFB254BFE1D000E6961 /* ModalPreviews.swift */; }; + D093474E242DCFC4000ECA88 /* PeerMediaGroupPeersController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D093474D242DCFC4000ECA88 /* PeerMediaGroupPeersController.swift */; }; + D0983E172568373E00467703 /* AppCenterAnalytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0983E162568373E00467703 /* AppCenterAnalytics.framework */; }; + D0983E192568373E00467703 /* AppCenterCrashes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0983E182568373E00467703 /* AppCenterCrashes.framework */; }; + D0983E1D2568376000467703 /* libopus.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0983E1C2568376000467703 /* libopus.framework */; }; + D0983E1F2568376000467703 /* libwebp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0983E1E2568376000467703 /* libwebp.framework */; }; D098C7191D7E175A007784E4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D098C7181D7E175A007784E4 /* AppDelegate.swift */; }; D098C71B1D7E175A007784E4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D098C71A1D7E175A007784E4 /* Assets.xcassets */; }; D098C71E1D7E175A007784E4 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = D098C71C1D7E175A007784E4 /* MainMenu.xib */; }; D098C73A1D7E1C40007784E4 /* AuthController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D098C7391D7E1C40007784E4 /* AuthController.swift */; }; + D09D836E24A6205500120F73 /* libtgvoip.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D09D836D24A6205500120F73 /* libtgvoip.framework */; }; + D09D9DF1229C27A700378796 /* AnimatedStickerUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09D9DF0229C27A700378796 /* AnimatedStickerUtils.swift */; }; + D09D9DF4229C289F00378796 /* GZip.m in Sources */ = {isa = PBXBuildFile; fileRef = D09D9DF3229C289F00378796 /* GZip.m */; }; + D0A2764E249C9D6B005E3C77 /* PeerPhotos.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A2764D249C9D6B005E3C77 /* PeerPhotos.swift */; }; + D0A27650249CE588005E3C77 /* GroupsStatsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A2764F249CE588005E3C77 /* GroupsStatsController.swift */; }; + D0A51D3624F7EC2100D75641 /* Mozjpeg.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0A51D3524F7EC2100D75641 /* Mozjpeg.framework */; }; + D0A5586E2574F75B00B7C182 /* group_call_chatlist_typing.json in Resources */ = {isa = PBXBuildFile; fileRef = D0A5586D2574F75B00B7C182 /* group_call_chatlist_typing.json */; }; + D0A75F25244843D3001F84A0 /* EditImageCanvasController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A75F24244843D3001F84A0 /* EditImageCanvasController.swift */; }; + D0A75F2724486B6D001F84A0 /* EditImageCanvasColorPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A75F2624486B6D001F84A0 /* EditImageCanvasColorPicker.swift */; }; + D0A75F2A24487A1E001F84A0 /* EditImageCanvasControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A75F2924487A1E001F84A0 /* EditImageCanvasControls.swift */; }; + D0A75F2C2449B689001F84A0 /* dart_idle.tgs in Resources */ = {isa = PBXBuildFile; fileRef = D0A75F2B2449B67D001F84A0 /* dart_idle.tgs */; }; + D0ABA3F325499C5A00031678 /* MessageStatsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ABA3F225499C5A00031678 /* MessageStatsController.swift */; }; + D0ABA3F72549C37E00031678 /* MessageSharedRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ABA3F62549C37E00031678 /* MessageSharedRowItem.swift */; }; + D0B6D72A22AA65D3008E36FE /* NewContactController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B6D72922AA65D3008E36FE /* NewContactController.swift */; }; + D0BDD50123A38660002010A5 /* InactiveChannelsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BDD50023A38660002010A5 /* InactiveChannelsController.swift */; }; + D0BE207E24D032E400038A8B /* VolumeControllerPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BE207D24D032E400038A8B /* VolumeControllerPopover.swift */; }; + D0BEB98F21628C250055B718 /* EditImageModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB98E21628C250055B718 /* EditImageModalController.swift */; }; + D0BEB9952166AF270055B718 /* PeerMediaTouchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB9942166AF270055B718 /* PeerMediaTouchBar.swift */; }; + D0BEB998216BD8A70055B718 /* EditImageControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB997216BD8A70055B718 /* EditImageControls.swift */; }; + D0BEB99A216D10920055B718 /* MediaPreviewEditControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB999216D10920055B718 /* MediaPreviewEditControl.swift */; }; + D0C39EC5233A6077003CD402 /* GeneralBlockTextRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C39EC4233A6077003CD402 /* GeneralBlockTextRowItem.swift */; }; + D0C550C4251127DA00B64966 /* ChatCommentsHeaderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C550C3251127DA00B64966 /* ChatCommentsHeaderItem.swift */; }; + D0CBB0F52492974F00620C65 /* VideoAvatarModalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CBB0F42492974F00620C65 /* VideoAvatarModalController.swift */; }; + D0CC4ADE22BA5C930088F36D /* LottieBufferCompressor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CC4ADD22BA5C930088F36D /* LottieBufferCompressor.swift */; }; + D0D087E2243DF4F100E05317 /* ChatlistFilterVisibilityItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D087E1243DF4F100E05317 /* ChatlistFilterVisibilityItem.swift */; }; + D0D1391B2521FBD5005FCF35 /* discussion.tgs in Resources */ = {isa = PBXBuildFile; fileRef = D0D1391A2521F652005FCF35 /* discussion.tgs */; }; + D0D139252524A31E005FCF35 /* RepliesHeaderRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D139242524A31E005FCF35 /* RepliesHeaderRowItem.swift */; }; + D0D3CE6C23D465FA00864F3C /* PollResultStickItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D3CE6B23D465FA00864F3C /* PollResultStickItem.swift */; }; + D0D71386227ADE9400EC88B1 /* maccheck.json in Resources */ = {isa = PBXBuildFile; fileRef = D0D71385227ADE9400EC88B1 /* maccheck.json */; }; + D0D7520924C03CD60037D73A /* VideoEditorThumbs.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D7520824C03CD60037D73A /* VideoEditorThumbs.swift */; }; + D0D7520B24C04FD60037D73A /* VideoEditorScrubbler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D7520A24C04FD60037D73A /* VideoEditorScrubbler.swift */; }; + D0D752C822F8A76100E2CB74 /* Geocoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D752C722F8A76100E2CB74 /* Geocoding.swift */; }; + D0D81AEE243E2D6F00CB9D20 /* ChatListFilterFolderIconController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D81AED243E2D6F00CB9D20 /* ChatListFilterFolderIconController.swift */; }; + D0D81AF8243F69AD00CB9D20 /* PollTimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D81AF7243F69AD00CB9D20 /* PollTimerView.swift */; }; + D0DD91FE246A8A380039D83D /* PeerMediaGifsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DD91FD246A8A380039D83D /* PeerMediaGifsController.swift */; }; + D0DDECEA2322FA0200E1B359 /* Contacts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F13EE182100B6DA00562E53 /* Contacts.framework */; }; + D0E3684624D80A0D009896D4 /* ClearCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E3684524D80A0D009896D4 /* ClearCache.swift */; }; + D0E3684824D842F7009896D4 /* StorageUsageCleanProgressRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E3684724D842F7009896D4 /* StorageUsageCleanProgressRowItem.swift */; }; + D0E3685824DAB066009896D4 /* VideoCallsConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E3685724DAB066009896D4 /* VideoCallsConfiguration.swift */; }; + D0E52B3222FD66C4000C0306 /* MessageTimecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E52B3122FD66C4000C0306 /* MessageTimecode.swift */; }; + D0E7BD1A256A7D350068644D /* GroupCallWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E7BD19256A7D350068644D /* GroupCallWindow.swift */; }; + D0E7BD6C256AA5570068644D /* TelegramVoip.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0E7BD6B256AA5570068644D /* TelegramVoip.framework */; }; + D0E7BD70256AA6430068644D /* PresentationGroupCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E7BD6F256AA6430068644D /* PresentationGroupCall.swift */; }; + D0E7BD73256AA6B50068644D /* PresentationGroupCallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E7BD72256AA6B50068644D /* PresentationGroupCallManager.swift */; }; + D0E7BD79256AE4B00068644D /* GroupCallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E7BD78256AE4B00068644D /* GroupCallController.swift */; }; + D0E7BD7D256BC9460068644D /* GroupCallParticipantRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E7BD7C256BC9460068644D /* GroupCallParticipantRowItem.swift */; }; + D0E7BD80256C1FB00068644D /* GroupCallSpeakButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E7BD7F256C1FAF0068644D /* GroupCallSpeakButton.swift */; }; + D0E7BE71256D30130068644D /* group_call_speaker_mute.json in Resources */ = {isa = PBXBuildFile; fileRef = D0E7BE6F256D30060068644D /* group_call_speaker_mute.json */; }; + D0E7BE72256D30130068644D /* group_call_speaker_unmute.json in Resources */ = {isa = PBXBuildFile; fileRef = D0E7BE70256D30130068644D /* group_call_speaker_unmute.json */; }; + D0E7BE7B256D56DA0068644D /* group_call_member_mute.json in Resources */ = {isa = PBXBuildFile; fileRef = D0E7BE79256D56C90068644D /* group_call_member_mute.json */; }; + D0E7BE7C256D56DA0068644D /* group_call_member_unmute.json in Resources */ = {isa = PBXBuildFile; fileRef = D0E7BE7A256D56DA0068644D /* group_call_member_unmute.json */; }; + D0E7BED0256EA7140068644D /* GroupCallSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E7BECF256EA7140068644D /* GroupCallSettingsController.swift */; }; + D0E82E002500F10E00E09A20 /* MergedAvatarsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E82DFF2500F10E00E09A20 /* MergedAvatarsView.swift */; }; + D0FCA7622434867400B72F18 /* PeerInfoHeadItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FCA7612434867400B72F18 /* PeerInfoHeadItem.swift */; }; + D0FFC4AC23E184BA0044D305 /* ChatListFilterController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FFC4AB23E184BA0044D305 /* ChatListFilterController.swift */; }; + D0FFCEBE215A7CB700995AFE /* emoji14.txt in Resources */ = {isa = PBXBuildFile; fileRef = D0FFCEBD215A7CB700995AFE /* emoji14.txt */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + D032AFA62578190600E67215 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D098C70D1D7E175A007784E4 /* Project object */; + proxyType = 1; + remoteGlobalIDString = C232EA901E1D07E700C4D38C; + remoteInfo = TelegramShare; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ C20D5AB61DAA94500042616A /* Copy Files */ = { isa = PBXCopyFilesBuildPhase; @@ -529,23 +1078,23 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - C276AFC71F74F2CF00DEDD8E /* Sparkle.framework in Copy Files */, - C276AFC81F74F2CF00DEDD8E /* HockeySDK.framework in Copy Files */, - C224F2961E43CDDD002FF0B2 /* PostboxMac.framework in Copy Files */, - C224F2951E43CDD8002FF0B2 /* MtProtoKitMac.framework in Copy Files */, + A7D2820F236C3C3B0000A9BF /* Postbox.framework in Copy Files */, + A7D2820E236C3C330000A9BF /* SwiftSignalKit.framework in Copy Files */, + A7D2820D236C3C2F0000A9BF /* TelegramCore.framework in Copy Files */, + A7D2820C236C3C2A0000A9BF /* MtProtoKit.framework in Copy Files */, + A7D08F4A236AC9BB002DC240 /* Sparkle.framework in Copy Files */, C224F2941E43CDC4002FF0B2 /* TGUIKit.framework in Copy Files */, - C224F2931E43CDBE002FF0B2 /* SwiftSignalKitMac.framework in Copy Files */, - C224F2921E43CD93002FF0B2 /* TelegramCoreMac.framework in Copy Files */, ); name = "Copy Files"; runOnlyForDeploymentPostprocessing = 0; }; C232EA661E1D041B00C4D38C /* Embed App Extensions */ = { isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; + buildActionMask = 12; dstPath = ""; dstSubfolderSpec = 13; files = ( + D032AFA82578190C00E67215 /* TelegramShare.appex in Embed App Extensions */, ); name = "Embed App Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -566,42 +1115,470 @@ dstPath = fonts; dstSubfolderSpec = 7; files = ( - C23D0D7C1F1A649900AF5151 /* SFCompactRounded-Semibold.otf in Fonts */, ); name = Fonts; runOnlyForDeploymentPostprocessing = 0; }; + C2786FF51FEC128F001FB044 /* Palettes */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = palettes; + dstSubfolderSpec = 7; + files = ( + ); + name = Palettes; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 270202BE261A1BF500CFCE6D /* PaymentsTipsRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentsTipsRowItem.swift; sourceTree = ""; }; + 270202C2261B3D3100CFCE6D /* CurrencyUITextFieldDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrencyUITextFieldDelegate.swift; sourceTree = ""; }; + 270202C5261B3D7C00CFCE6D /* UITextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITextField.swift; sourceTree = ""; }; + 270A2629269D8F1000B31B2B /* WidgetStickersController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetStickersController.swift; sourceTree = ""; }; + 270E2B4A2609F95D00B0738C /* playlist_play_pause.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = playlist_play_pause.tgs; sourceTree = ""; }; + 270E2B4D2609FDBA00B0738C /* playlist_pause_play.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = playlist_pause_play.tgs; sourceTree = ""; }; + 270F020626F0B0830099D2AA /* EmojiAnimationEffectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiAnimationEffectView.swift; sourceTree = ""; }; + 2711D71026CFBF6A00917305 /* MediaObjectToAvatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaObjectToAvatar.swift; sourceTree = ""; }; + 2713B219260A42FE00CE0EC6 /* MediaFrameSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MediaFrameSource.swift; path = "submodules/telegram-ios/submodules/MediaPlayer/Sources/MediaFrameSource.swift"; sourceTree = SOURCE_ROOT; }; + 2713B21A260A42FE00CE0EC6 /* MediaTrackDecodableFrame.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MediaTrackDecodableFrame.swift; path = "submodules/telegram-ios/submodules/MediaPlayer/Sources/MediaTrackDecodableFrame.swift"; sourceTree = SOURCE_ROOT; }; + 2713B21B260A42FE00CE0EC6 /* MediaTrackFrame.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MediaTrackFrame.swift; path = "submodules/telegram-ios/submodules/MediaPlayer/Sources/MediaTrackFrame.swift"; sourceTree = SOURCE_ROOT; }; + 2713B21C260A42FE00CE0EC6 /* MediaPlaybackData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MediaPlaybackData.swift; path = "submodules/telegram-ios/submodules/MediaPlayer/Sources/MediaPlaybackData.swift"; sourceTree = SOURCE_ROOT; }; + 2713B21D260A42FE00CE0EC6 /* MediaTrackFrameDecoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MediaTrackFrameDecoder.swift; path = "submodules/telegram-ios/submodules/MediaPlayer/Sources/MediaTrackFrameDecoder.swift"; sourceTree = SOURCE_ROOT; }; + 2713B21E260A42FE00CE0EC6 /* MediaTrackFrameBuffer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MediaTrackFrameBuffer.swift; path = "submodules/telegram-ios/submodules/MediaPlayer/Sources/MediaTrackFrameBuffer.swift"; sourceTree = SOURCE_ROOT; }; + 2719655325EEB89E00FC9DE5 /* GroupCallRecorderRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallRecorderRowItem.swift; sourceTree = ""; }; + 271999E62683C04B000DE2B7 /* MetalPerformanceShaders.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetalPerformanceShaders.framework; path = System/Library/Frameworks/MetalPerformanceShaders.framework; sourceTree = SDKROOT; }; + 271B783D25ECD94B007144D7 /* PamentsSelectMethodController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PamentsSelectMethodController.swift; sourceTree = ""; }; + 27200F1A257C256E00574365 /* DevicesContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesContext.swift; sourceTree = ""; }; + 2725785A259CF987001558E8 /* AnimatedBadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedBadgeView.swift; sourceTree = ""; }; + 2728991826CBC0D100F4D288 /* UNUserNotifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNUserNotifications.swift; sourceTree = ""; }; + 2728991B26CBCDA900F4D288 /* TurnOnNotificationsRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TurnOnNotificationsRowItem.swift; sourceTree = ""; }; + 2729712026135C94006D38E2 /* GroupCallInviteRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallInviteRowItem.swift; sourceTree = ""; }; + 272C182F267D00350030B5FB /* CoreMediaIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMediaIO.framework; path = System/Library/Frameworks/CoreMediaIO.framework; sourceTree = SDKROOT; }; + 273274E526A818AE00E04289 /* WallpaperColorPickerContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WallpaperColorPickerContainerView.swift; sourceTree = ""; }; + 273274E726A8190F00E04289 /* WallpaperAdditionColorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WallpaperAdditionColorView.swift; sourceTree = ""; }; + 27335F0225C8295100FD040C /* GroupVideoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupVideoView.swift; sourceTree = ""; }; + 273F9C0A257BC486004EC51B /* HotKey.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = HotKey.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 2740AA3926BD5CEF004B3BCE /* builtin-wallpaper-svg */ = {isa = PBXFileReference; lastKnownFileType = file; path = "builtin-wallpaper-svg"; sourceTree = ""; }; + 2744018426E6344D0035C55D /* WidgetRecentPeersController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetRecentPeersController.swift; sourceTree = ""; }; + 2744AE6C25B44E1500E8849F /* ExportedInvitationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportedInvitationController.swift; sourceTree = ""; }; + 2744AE7125B58E6C00E8849F /* invitations.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = invitations.tgs; sourceTree = ""; }; + 2746926B26A99DD800B67817 /* WallpaperCheckboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WallpaperCheckboxView.swift; sourceTree = ""; }; + 27478F5B25FBD05F005B8B98 /* voice_chat_raise_hand_3.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = voice_chat_raise_hand_3.tgs; sourceTree = ""; }; + 274A12DD2584BFF5006C6FED /* GroupCallInvitation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallInvitation.swift; sourceTree = ""; }; + 274B831726F0C0A700E6E474 /* EmojiScreenEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiScreenEffect.swift; sourceTree = ""; }; + 274BB56C264013AC00620D03 /* cameraon.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = cameraon.tgs; sourceTree = ""; }; + 274BB56F264013B800620D03 /* cameraoff.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = cameraoff.tgs; sourceTree = ""; }; + 275045E425EE271D00872D20 /* GroupCallDisplayAsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallDisplayAsController.swift; sourceTree = ""; }; + 2756EF1C25C463F10062303D /* LegacyReachability.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LegacyReachability.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 275DDC38267B8701009CF884 /* bot_menu_close.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = bot_menu_close.tgs; sourceTree = ""; }; + 275DDC39267B8702009CF884 /* bot_close_menu.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = bot_close_menu.tgs; sourceTree = ""; }; + 275FD30D266A20FC008EBC99 /* GroupCallTileRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallTileRowItem.swift; sourceTree = ""; }; + 2760E55526970513001C59F8 /* WidgetStorageController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetStorageController.swift; sourceTree = ""; }; + 2760E55826970672001C59F8 /* WidgetButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetButton.swift; sourceTree = ""; }; + 2760E55A269706D7001C59F8 /* WidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetView.swift; sourceTree = ""; }; + 2763A158261C91B000C12762 /* DatePickerRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerRowItem.swift; sourceTree = ""; }; + 2763A15B261DF05200C12762 /* voice_chat_start_chat_to_mute.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = voice_chat_start_chat_to_mute.tgs; sourceTree = ""; }; + 2763A161261DF5CD00C12762 /* voice_chat_cancel_reminder.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = voice_chat_cancel_reminder.tgs; sourceTree = ""; }; + 2763A164261DF5EF00C12762 /* voice_chat_cancel_reminder_to_mute.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = voice_chat_cancel_reminder_to_mute.tgs; sourceTree = ""; }; + 2763A167261DF60A00C12762 /* voice_chat_cancel_reminder_to_raise_hand.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = voice_chat_cancel_reminder_to_raise_hand.tgs; sourceTree = ""; }; + 2763A16A261DF65700C12762 /* voice_chat_set_reminder.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = voice_chat_set_reminder.tgs; sourceTree = ""; }; + 2763A16D261DF68100C12762 /* voice_chat_set_reminder_to_mute.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = voice_chat_set_reminder_to_mute.tgs; sourceTree = ""; }; + 2763A170261DF6A100C12762 /* voice_chat_set_reminder_to_raise_hand.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = voice_chat_set_reminder_to_raise_hand.tgs; sourceTree = ""; }; + 2764A5A925DFB5B300F9A20D /* ReportDetailsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportDetailsController.swift; sourceTree = ""; }; + 2764A5AC25DFBB4300F9A20D /* police.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = police.tgs; sourceTree = ""; }; + 2764A5B025E0173900F9A20D /* AutoDeleteContextMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoDeleteContextMenuView.swift; sourceTree = ""; }; + 27672E9826F8D5B000297DB4 /* 103.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 103.m4a; sourceTree = ""; }; + 27672E9926F8D5B000297DB4 /* 1.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 1.m4a; sourceTree = ""; }; + 27672E9A26F8D5B000297DB4 /* 0.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 0.m4a; sourceTree = ""; }; + 27672E9B26F8D5B100297DB4 /* 9.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 9.m4a; sourceTree = ""; }; + 27672E9C26F8D5B100297DB4 /* 100.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 100.m4a; sourceTree = ""; }; + 27672E9D26F8D5B100297DB4 /* 105.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 105.m4a; sourceTree = ""; }; + 27672E9E26F8D5B100297DB4 /* 106.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 106.m4a; sourceTree = ""; }; + 27672E9F26F8D5B100297DB4 /* 102.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 102.m4a; sourceTree = ""; }; + 27672EA026F8D5B100297DB4 /* 7.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 7.m4a; sourceTree = ""; }; + 27672EA126F8D5B100297DB4 /* 108.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 108.m4a; sourceTree = ""; }; + 27672EA226F8D5B100297DB4 /* 3.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 3.m4a; sourceTree = ""; }; + 27672EA326F8D5B100297DB4 /* 101.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 101.m4a; sourceTree = ""; }; + 27672EA426F8D5B100297DB4 /* 110.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 110.m4a; sourceTree = ""; }; + 27672EA526F8D5B200297DB4 /* 5.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 5.m4a; sourceTree = ""; }; + 27672EA626F8D5B200297DB4 /* 2.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 2.m4a; sourceTree = ""; }; + 27672EA726F8D5B200297DB4 /* 8.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 8.m4a; sourceTree = ""; }; + 27672EA826F8D5B200297DB4 /* 107.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 107.m4a; sourceTree = ""; }; + 27672EA926F8D5B200297DB4 /* 104.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 104.m4a; sourceTree = ""; }; + 27672EAA26F8D5B200297DB4 /* 6.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 6.m4a; sourceTree = ""; }; + 27672EAB26F8D5B200297DB4 /* 109.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 109.m4a; sourceTree = ""; }; + 27672EAC26F8D5B200297DB4 /* 111.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 111.m4a; sourceTree = ""; }; + 27672EAD26F8D5B200297DB4 /* 4.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = 4.m4a; sourceTree = ""; }; + 276A1D58265D0E600083D6E7 /* GroupCallSpeakingTooltipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallSpeakingTooltipView.swift; sourceTree = ""; }; + 276A6C0E26C5316F00E4FF34 /* StickerPackTrendingItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPackTrendingItem.swift; sourceTree = ""; }; + 276B5979261C780F0029FD3F /* GroupCallMainVideoContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallMainVideoContainer.swift; sourceTree = ""; }; + 276B597C261C78AD0029FD3F /* GroupCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallView.swift; sourceTree = ""; }; + 276B597F261C78F40029FD3F /* GroupCallUIState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallUIState.swift; sourceTree = ""; }; + 276B5982261C79250029FD3F /* GroupCallTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallTitleView.swift; sourceTree = ""; }; + 276B5985261C79940029FD3F /* GroupCallControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallControlsView.swift; sourceTree = ""; }; + 276B5988261C7AC60029FD3F /* GroupCallSchedule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallSchedule.swift; sourceTree = ""; }; + 276C1EBE261B451500FE41C9 /* Currency.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Currency.swift; sourceTree = ""; }; + 2773A53425F91E1500AB45E9 /* voice_chat_raise_hand_1.tgs */ = {isa = PBXFileReference; lastKnownFileType = text; path = voice_chat_raise_hand_1.tgs; sourceTree = ""; }; + 2773A53925FA1D9D00AB45E9 /* JoinVoiceChatAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinVoiceChatAlertController.swift; sourceTree = ""; }; + 2773A53D25FA220B00AB45E9 /* JoinVoiceChatAlertRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinVoiceChatAlertRowItem.swift; sourceTree = ""; }; + 2773A54025FA324900AB45E9 /* voice_chat_unmute.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = voice_chat_unmute.tgs; sourceTree = ""; }; + 2773A54225FA324A00AB45E9 /* voice_chat_mute.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = voice_chat_mute.tgs; sourceTree = ""; }; + 2773A54325FA324B00AB45E9 /* voice_chat_hand_off.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = voice_chat_hand_off.tgs; sourceTree = ""; }; + 2773A55025FA4DAC00AB45E9 /* voice_chat_raise_hand_2.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = voice_chat_raise_hand_2.tgs; sourceTree = ""; }; + 2773A55E25FA4E0C00AB45E9 /* voice_chat_raise_hand_4.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = voice_chat_raise_hand_4.tgs; sourceTree = ""; }; + 2773A56125FA4E2300AB45E9 /* voice_chat_raise_hand_5.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = voice_chat_raise_hand_5.tgs; sourceTree = ""; }; + 2773A56425FA4E3A00AB45E9 /* voice_chat_raise_hand_6.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = voice_chat_raise_hand_6.tgs; sourceTree = ""; }; + 2773A56725FA4E4B00AB45E9 /* voice_chat_raise_hand_7.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = voice_chat_raise_hand_7.tgs; sourceTree = ""; }; + 2773A56A25FAA63D00AB45E9 /* voice_chat_hand_on_unmuted.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = voice_chat_hand_on_unmuted.tgs; sourceTree = ""; }; + 2773A56B25FAA63E00AB45E9 /* voice_chat_hand_on_muted.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = voice_chat_hand_on_muted.tgs; sourceTree = ""; }; + 2775102025E7917F003D58D1 /* PaymentsCheckoutController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentsCheckoutController.swift; sourceTree = ""; }; + 2775102425E79233003D58D1 /* PaymentsCheckoutPreviewRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentsCheckoutPreviewRowItem.swift; sourceTree = ""; }; + 277C34A7267B5BC100B1CAA7 /* ChatInputMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInputMenuView.swift; sourceTree = ""; }; + 2783BA9426E24795004A1591 /* GroupCallVideoOrientationRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallVideoOrientationRowItem.swift; sourceTree = ""; }; + 278680AC26A982DC005FDBB9 /* WallpaperPatternPreviewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WallpaperPatternPreviewController.swift; sourceTree = ""; }; + 278C720325E67C2700F1B315 /* emoji1016.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = emoji1016.txt; sourceTree = ""; }; + 278E86D3265D0F330006685D /* GroupCallAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallAvatarView.swift; sourceTree = ""; }; + 278E86D6265D49100006685D /* MicroListenerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicroListenerController.swift; sourceTree = ""; }; + 278FA2E525E813F100280629 /* PaymentsShippingMethodController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentsShippingMethodController.swift; sourceTree = ""; }; + 278FA2E825E8C36A00280629 /* PaymentsPaymentMethodController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentsPaymentMethodController.swift; sourceTree = ""; }; + 278FA43725E8D5CD00280629 /* Stripe.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Stripe.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 2794910A260A41E80003BFA0 /* FFMpegMediaFrameSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FFMpegMediaFrameSource.swift; path = "submodules/telegram-ios/submodules/MediaPlayer/Sources/FFMpegMediaFrameSource.swift"; sourceTree = SOURCE_ROOT; }; + 2794910B260A41EA0003BFA0 /* FFMpegMediaFrameSourceContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FFMpegMediaFrameSourceContext.swift; path = "submodules/telegram-ios/submodules/MediaPlayer/Sources/FFMpegMediaFrameSourceContext.swift"; sourceTree = SOURCE_ROOT; }; + 2794910C260A41EA0003BFA0 /* FFMpegAudioFrameDecoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FFMpegAudioFrameDecoder.swift; path = "submodules/telegram-ios/submodules/MediaPlayer/Sources/FFMpegAudioFrameDecoder.swift"; sourceTree = SOURCE_ROOT; }; + 2794910D260A41EA0003BFA0 /* FFMpegMediaFrameSourceContextHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FFMpegMediaFrameSourceContextHelpers.swift; path = "submodules/telegram-ios/submodules/MediaPlayer/Sources/FFMpegMediaFrameSourceContextHelpers.swift"; sourceTree = SOURCE_ROOT; }; + 2794910E260A41EA0003BFA0 /* FFMpegMediaPassthroughVideoFrameDecoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FFMpegMediaPassthroughVideoFrameDecoder.swift; path = "submodules/telegram-ios/submodules/MediaPlayer/Sources/FFMpegMediaPassthroughVideoFrameDecoder.swift"; sourceTree = SOURCE_ROOT; }; + 2794910F260A41EB0003BFA0 /* FFMpegMediaVideoFrameDecoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FFMpegMediaVideoFrameDecoder.swift; path = "submodules/telegram-ios/submodules/MediaPlayer/Sources/FFMpegMediaVideoFrameDecoder.swift"; sourceTree = SOURCE_ROOT; }; + 279A1E0C25E90D13007D48E7 /* PaymentWebInteractionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentWebInteractionController.swift; sourceTree = ""; }; + 279A1E2725E945A2007D48E7 /* PaymentsReceiptController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentsReceiptController.swift; sourceTree = ""; }; + 27A0A4E225F76F4600B789AF /* GroupCallPeerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallPeerController.swift; sourceTree = ""; }; + 27A0A4E625F7764600B789AF /* GroupCallPeerAvatarRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallPeerAvatarRowItem.swift; sourceTree = ""; }; + 27A0A4EA25F7814500B789AF /* GroupCallTextAndLabelRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallTextAndLabelRowItem.swift; sourceTree = ""; }; + 27A7D37B2600F58900A67737 /* voip_group_unmuted.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = voip_group_unmuted.mp3; sourceTree = ""; }; + 27A7D37C2600F58A00A67737 /* voip_group_recording_started.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = voip_group_recording_started.mp3; sourceTree = ""; }; + 27A99049257A7435009044DB /* SoundEffectPlayQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundEffectPlayQueue.swift; sourceTree = ""; }; + 27A9904C257A8044009044DB /* Pop.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = Pop.wav; sourceTree = ""; }; + 27A9904D257A8046009044DB /* Purr.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = Purr.wav; sourceTree = ""; }; + 27AAFCBE2649288E000B1053 /* GroupCallTileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallTileView.swift; sourceTree = ""; }; + 27B15D2E259B2DA700C7F280 /* DesktopCaptureListUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DesktopCaptureListUI.swift; sourceTree = ""; }; + 27B15D31259B373800C7F280 /* DesktopCapturePreviewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DesktopCapturePreviewItem.swift; sourceTree = ""; }; + 27B8AFE926380B4D0044C71B /* screenon.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = screenon.tgs; sourceTree = ""; }; + 27B8AFEA26380B4E0044C71B /* screenoff.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = screenoff.tgs; sourceTree = ""; }; + 27BA045626E4F3E9008FC1A3 /* MessageReadMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReadMenuItem.swift; sourceTree = ""; }; + 27BA045A26E51F55008FC1A3 /* AvatarContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarContentView.swift; sourceTree = ""; }; + 27BF617F26567DEE00331308 /* GroupCallContextMenuHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallContextMenuHeader.swift; sourceTree = ""; }; + 27C4088D25B0603700372302 /* GeneralLoadingRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralLoadingRowItem.swift; sourceTree = ""; }; + 27C4089025B068C300372302 /* InviteLinkRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteLinkRowItem.swift; sourceTree = ""; }; + 27C4089325B081B500372302 /* FireTimerControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireTimerControl.swift; sourceTree = ""; }; + 27C4089625B0A1F300372302 /* ClosureInviteLinkController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClosureInviteLinkController.swift; sourceTree = ""; }; + 27C4089925B0BD4E00372302 /* NumberSelectorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberSelectorController.swift; sourceTree = ""; }; + 27C4F5DD26B3F62E008123EC /* VideoMessageConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoMessageConfig.swift; sourceTree = ""; }; + 27CF9608268F5B3E0086515A /* SoftwareGradientBackgroundItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftwareGradientBackgroundItem.swift; sourceTree = ""; }; + 27D006DE25AF3B1C00EE3EB1 /* ExportedInvitationRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportedInvitationRowItem.swift; sourceTree = ""; }; + 27D1D62D2611FB0D00684DEA /* rnnoise.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = rnnoise.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 27D4F6DE261B776200CCAE03 /* Notices.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Notices.swift; sourceTree = ""; }; + 27DBE4AE25B0424100FCEE2A /* InviteLinksController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteLinksController.swift; sourceTree = ""; }; + 27DCF4BC267C9D680019EC49 /* AudioCommandCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioCommandCenter.swift; sourceTree = ""; }; + 27DF650626AADB9E000753AC /* StringFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringFormat.swift; sourceTree = ""; }; + 27E001B426285AC8008786D3 /* PinchToZoom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinchToZoom.swift; sourceTree = ""; }; + 27E434132680C57900B05CB1 /* CoreMediaVideoTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreMediaVideoTest.swift; sourceTree = ""; }; + 27E4343C2680D38600B05CB1 /* CoreMediaMacCapture.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CoreMediaMacCapture.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 27E59979261B3F5800228411 /* CurrencyFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrencyFormatter.swift; sourceTree = ""; }; + 27E5997C261B3FA800228411 /* String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; + 27E5997F261B404000228411 /* CurrencyLocale.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CurrencyLocale.swift; sourceTree = ""; }; + 27E6DB092689C71E003D6164 /* MetalFunctions.metal */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.metal; path = MetalFunctions.metal; sourceTree = ""; }; + 27E969DE2589172B00CB9F64 /* call up.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "call up.mp3"; sourceTree = ""; }; + 27E969DF2589172D00CB9F64 /* call down.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "call down.mp3"; sourceTree = ""; }; + 27E969E5258B417200CB9F64 /* reconnecting.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = reconnecting.mp3; sourceTree = ""; }; + 27F023D325A5C19F008F1C81 /* DesktopCapturerWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DesktopCapturerWindow.swift; sourceTree = ""; }; + 27F5F23225E7CACE00E8AC69 /* CurrencyFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyFormat.swift; sourceTree = ""; }; + 27F5F23B25E7CCD600E8AC69 /* PaymentsCheckoutPriceItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentsCheckoutPriceItem.swift; sourceTree = ""; }; + 27F5F23E25E7DD8300E8AC69 /* PaymentsShippingInfoController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentsShippingInfoController.swift; sourceTree = ""; }; + 27FE779926F4944800E1C90B /* ChatThemeSelectorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThemeSelectorController.swift; sourceTree = ""; }; + 27FE779B26F4AE4400E1C90B /* ChatThemeRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatThemeRowItem.swift; sourceTree = ""; }; + 27FFCEC42694BA58006CA024 /* WidgetAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetAppearance.swift; sourceTree = ""; }; + 27FFCEC62694BC95006CA024 /* WidgetController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetController.swift; sourceTree = ""; }; + 9F0367EF227208E000456348 /* QRCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCode.swift; sourceTree = ""; }; + 9F0367F12272108800456348 /* ProxyQRCodeRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyQRCodeRowItem.swift; sourceTree = ""; }; + 9F0367F62273260A00456348 /* UndoTooltipController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UndoTooltipController.swift; sourceTree = ""; }; + 9F0368002277091800456348 /* LAnimationButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LAnimationButton.swift; sourceTree = ""; }; + 9F03680222771A9600456348 /* anim_unarchive.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = anim_unarchive.json; sourceTree = ""; }; + 9F03680322771A9600456348 /* anim_ungroup.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = anim_ungroup.json; sourceTree = ""; }; + 9F03680422771A9600456348 /* archiveAvatar.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = archiveAvatar.json; sourceTree = ""; }; + 9F03680522771A9600456348 /* anim_read.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = anim_read.json; sourceTree = ""; }; + 9F03680622771A9600456348 /* anim_delete.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = anim_delete.json; sourceTree = ""; }; + 9F03680722771A9600456348 /* anim_unread.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = anim_unread.json; sourceTree = ""; }; + 9F03680822771A9600456348 /* anim_hide.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = anim_hide.json; sourceTree = ""; }; + 9F03680922771A9600456348 /* anim_mute.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = anim_mute.json; sourceTree = ""; }; + 9F03680A22771A9600456348 /* anim_unpin.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = anim_unpin.json; sourceTree = ""; }; + 9F03680B22771A9600456348 /* anim_group.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = anim_group.json; sourceTree = ""; }; + 9F03680C22771A9600456348 /* anim_archive.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = anim_archive.json; sourceTree = ""; }; + 9F03680D22771A9700456348 /* anim_unmute.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = anim_unmute.json; sourceTree = ""; }; + 9F03680E22771A9700456348 /* anim_pin.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = anim_pin.json; sourceTree = ""; }; + 9F0AE6862191D29D00A8B53A /* ContextSearchMessageItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextSearchMessageItem.swift; sourceTree = ""; }; + 9F0AE6B42199904400A8B53A /* sound_a.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = sound_a.caf; sourceTree = ""; }; + 9F0AE6BB2199BBB900A8B53A /* MediaPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerView.swift; sourceTree = ""; }; + 9F0AE6BD2199BEBE00A8B53A /* SVideoController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SVideoController.swift; sourceTree = ""; }; + 9F0AE6BF2199CDB500A8B53A /* SVideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SVideoView.swift; sourceTree = ""; }; + 9F0B8F161FFB7F1A00073D3F /* AccentColorRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccentColorRowItem.swift; sourceTree = ""; }; + 9F0BCD9C2087BA81001D8D8A /* Sparkle.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Sparkle.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9F0BCD9E2087BABC001D8D8A /* Sparkle.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Sparkle.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9F0E6F77203ED1380086699C /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; + 9F0E6F79203EFE870086699C /* Preferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; + 9F0F8E81226DCD1C00A97F6A /* OpmizeDatabaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpmizeDatabaseView.swift; sourceTree = ""; }; + 9F10CE8720610127002DD61A /* PassportController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassportController.swift; sourceTree = ""; }; + 9F10CE8920611536002DD61A /* PassportHeaderItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassportHeaderItem.swift; sourceTree = ""; }; + 9F10CE8D20617C36002DD61A /* PassportInsertPasswordItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassportInsertPasswordItem.swift; sourceTree = ""; }; + 9F10CE912061BE19002DD61A /* InputDataController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputDataController.swift; sourceTree = ""; }; + 9F10CE932061C8C8002DD61A /* InputDataControllerEntries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputDataControllerEntries.swift; sourceTree = ""; }; + 9F10CE952061C98E002DD61A /* InputDataRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputDataRowItem.swift; sourceTree = ""; }; + 9F10CE9720626B1B002DD61A /* PassportDocumentRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassportDocumentRowItem.swift; sourceTree = ""; }; + 9F10CE99206284F8002DD61A /* InputDataDateRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputDataDateRowItem.swift; sourceTree = ""; }; + 9F127E14210B1F540080D709 /* PeerMediaVoiceRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerMediaVoiceRowItem.swift; sourceTree = ""; }; + 9F12D342209251CF0072928B /* EditAccountInfoItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccountInfoItem.swift; sourceTree = ""; }; + 9F13CCE22006719400ECF301 /* ru */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; + 9F13CCE3200671AF00ECF301 /* uk */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; + 9F13EE162100B05300562E53 /* VCardContactController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCardContactController.swift; sourceTree = ""; }; + 9F13EE182100B6DA00562E53 /* Contacts.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Contacts.framework; path = System/Library/Frameworks/Contacts.framework; sourceTree = SDKROOT; }; + 9F13EE1A2100BFCA00562E53 /* VCardHeaderItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCardHeaderItem.swift; sourceTree = ""; }; + 9F147F6E223014EB00D71BD1 /* PasscodeControllers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasscodeControllers.swift; sourceTree = ""; }; + 9F147F7B2231543800D71BD1 /* PeerUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerUtils.swift; sourceTree = ""; }; + 9F14CBF22007DEB300F22DA9 /* ChatWallpaperModalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatWallpaperModalController.swift; sourceTree = ""; }; + 9F14CBF42007DFD400F22DA9 /* Wallpapers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallpapers.swift; sourceTree = ""; }; + 9F153D1321F0C7F800B95D82 /* WallpaperPreviewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WallpaperPreviewController.swift; sourceTree = ""; }; + 9F153D1521F3662700B95D82 /* StoredMessageFromSearchPeer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredMessageFromSearchPeer.swift; sourceTree = ""; }; + 9F1668B72007E3BC00DD39FB /* ThemeGridControllerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeGridControllerItem.swift; sourceTree = ""; }; + 9F1668C72008F30900DD39FB /* ChatBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBackgroundView.swift; sourceTree = ""; }; + 9F17E5B8212F173900C25A65 /* AutoNightViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoNightViewController.swift; sourceTree = ""; }; + 9F17E5BA212F191F00C25A65 /* AutoNightThemePreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoNightThemePreferences.swift; sourceTree = ""; }; + 9F18908C2237B5A400665EF5 /* InputURLFormatterModalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputURLFormatterModalController.swift; sourceTree = ""; }; + 9F1890922238F3DC00665EF5 /* DownloadedFilesPaths.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadedFilesPaths.swift; sourceTree = ""; }; + 9F18DD91206D8FFD00A2AAD0 /* SecureIdVerificationDocumentsContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureIdVerificationDocumentsContext.swift; sourceTree = ""; }; + 9F18DD92206D8FFD00A2AAD0 /* SecureIdVerificationDocument.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureIdVerificationDocument.swift; sourceTree = ""; }; + 9F1962D72101458C00FFF048 /* VCardLocationRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCardLocationRowItem.swift; sourceTree = ""; }; + 9F1AE5E320B6D7AA002A9D8D /* LocationPlaceSuggestionRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPlaceSuggestionRowItem.swift; sourceTree = ""; }; + 9F1AE5E520B70328002A9D8D /* LocationSendCurrentItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSendCurrentItem.swift; sourceTree = ""; }; + 9F1BABAD21E5ECE70075C03E /* ChatUndoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatUndoManager.swift; sourceTree = ""; }; + 9F1BABAF21E60DCC0075C03E /* UndoOverlayHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UndoOverlayHeaderView.swift; sourceTree = ""; }; + 9F1BC1A8223FDE6D00F21815 /* InputSources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputSources.swift; sourceTree = ""; }; + 9F1C279221D38A96003CD033 /* InstantPageScrollableItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageScrollableItem.swift; sourceTree = ""; }; + 9F21A7CE21C1552D0037784F /* InstantPageTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageTheme.swift; sourceTree = ""; }; + 9F21A7D221C167000037784F /* InstantPageImageItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageImageItem.swift; sourceTree = ""; }; + 9F21A7D421C16CB90037784F /* InstantPagePeerReferenceItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPagePeerReferenceItem.swift; sourceTree = ""; }; + 9F21A7DB21C290E00037784F /* InstantPageTableItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageTableItem.swift; sourceTree = ""; }; + 9F21F65420B5A72800332C85 /* LocationModalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationModalController.swift; sourceTree = ""; }; + 9F21F65C20B5A9C900332C85 /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = System/Library/Frameworks/MapKit.framework; sourceTree = SDKROOT; }; + 9F262D5E21BFD5BC006817CD /* LocalizationPreviewModalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationPreviewModalController.swift; sourceTree = ""; }; + 9F291C9F2264E57F00C66267 /* BuildConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BuildConfig.m; sourceTree = ""; }; + 9F291CA02264E57F00C66267 /* BuildConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BuildConfig.h; sourceTree = ""; }; + 9F354E9B2270630A006F1D42 /* HapticEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticEngine.swift; sourceTree = ""; }; + 9F3D5F6222044D8700CB0CAA /* AppUpdateViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdateViewController.swift; sourceTree = ""; }; + 9F3D5F8522085B2000CB0CAA /* UpdaterNotifySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdaterNotifySettings.swift; sourceTree = ""; }; + 9F3EAB3A20A5A1EC003FE7E3 /* NetworkUsageStatsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkUsageStatsController.swift; sourceTree = ""; }; + 9F3EAB3D20A5ED2F003FE7E3 /* genann.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = genann.h; sourceTree = ""; }; + 9F3EAB3E20A5ED2F003FE7E3 /* ocr.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ocr.mm; sourceTree = ""; }; + 9F3EAB3F20A5ED2F003FE7E3 /* ocr_nn.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; path = ocr_nn.bin; sourceTree = ""; }; + 9F3EAB4020A5ED2F003FE7E3 /* fast-edge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "fast-edge.h"; sourceTree = ""; }; + 9F3EAB4120A5ED2F003FE7E3 /* ocr.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ocr.h; sourceTree = ""; }; + 9F3EAB4220A5ED2F003FE7E3 /* genann.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = genann.c; sourceTree = ""; }; + 9F3EAB4320A5ED2F003FE7E3 /* fast-edge.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = "fast-edge.cpp"; sourceTree = ""; }; + 9F3EAB4920A5F90B003FE7E3 /* TGPassportMRZ.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGPassportMRZ.h; sourceTree = ""; }; + 9F3EAB4A20A5F90B003FE7E3 /* TGPassportMRZ.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGPassportMRZ.m; sourceTree = ""; }; + 9F4EC947218B459A002B3C56 /* RenderedTotalUnreadCount.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RenderedTotalUnreadCount.swift; sourceTree = ""; }; + 9F4EEF7D21D3C3E3002C3B33 /* InstantPageDetailsItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageDetailsItem.swift; sourceTree = ""; }; + 9F4EEF7F21D3C76E002C3B33 /* InstantPageContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageContentView.swift; sourceTree = ""; }; + 9F4EEF8121D4F584002C3B33 /* ImageTransparency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageTransparency.swift; sourceTree = ""; }; + 9F4EEF8321D4F59C002C3B33 /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = System/Library/Frameworks/Accelerate.framework; sourceTree = SDKROOT; }; + 9F4EEF8521D4FA68002C3B33 /* InstantPageArticleItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageArticleItem.swift; sourceTree = ""; }; + 9F4EEF8721D515C5002C3B33 /* InstantPageStoredState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageStoredState.swift; sourceTree = ""; }; + 9F52E74E2074C12B0048791C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 9F52E7502074C16D0048791C /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/MainMenu.strings; sourceTree = ""; }; + 9F52E7522074C1710048791C /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/MainMenu.strings; sourceTree = ""; }; + 9F52F5192130286E006FC0B5 /* LocationRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRequest.swift; sourceTree = ""; }; + 9F580BE420A0AA7B00F6D56C /* ChatRecorderOverlayWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRecorderOverlayWindow.swift; sourceTree = ""; }; + 9F62AE7D202D85B7007FB557 /* FetchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchManager.swift; sourceTree = ""; }; + 9F62AE80202D85E7007FB557 /* FetchManagerLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchManagerLocation.swift; sourceTree = ""; }; + 9F62AE82202D8759007FB557 /* FetchMediaUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchMediaUtils.swift; sourceTree = ""; }; + 9F6314E021CAA0AB009FD379 /* NewPollController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewPollController.swift; sourceTree = ""; }; + 9F63152521D236CB009FD379 /* ForgotPasswordController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForgotPasswordController.swift; sourceTree = ""; }; + 9F63152821D26892009FD379 /* CancelResetAccountController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelResetAccountController.swift; sourceTree = ""; }; + 9F6B54C721369B4000748FC1 /* GalleryModernControls.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GalleryModernControls.swift; sourceTree = ""; }; + 9F6FF3212248E298004364FE /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; }; + 9F72973320B878B00067F815 /* MapResources.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MapResources.swift; sourceTree = ""; }; + 9F72973F20BD9C6A0067F815 /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = ""; }; + 9F72974720C597800067F815 /* TermsModalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsModalController.swift; sourceTree = ""; }; + 9F77B3972211979B003B65B8 /* AutoplayPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoplayPreferences.swift; sourceTree = ""; }; + 9F77B3A5221C1DAC003B65B8 /* LogoutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoutViewController.swift; sourceTree = ""; }; + 9F7943AF20854E2F00FEDB81 /* ProxyListRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyListRowItem.swift; sourceTree = ""; }; + 9F7943B120855DC200FEDB81 /* ProxyListController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyListController.swift; sourceTree = ""; }; + 9F7B5FCA22003DC70087D020 /* WallpaperColorPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WallpaperColorPicker.swift; sourceTree = ""; }; + 9F7B5FCC220099220087D020 /* WallpaperPatternPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WallpaperPatternPreviewView.swift; sourceTree = ""; }; + 9F7B74022227F492006610E4 /* ManageSharedAccountInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageSharedAccountInfo.swift; sourceTree = ""; }; + 9F7B74042227F4F2006610E4 /* SharedAccountInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedAccountInfo.swift; sourceTree = ""; }; + 9F7B740E2229618E006610E4 /* SharedWakeupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedWakeupManager.swift; sourceTree = ""; }; + 9F7B741022296FD7006610E4 /* SharedNotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedNotificationManager.swift; sourceTree = ""; }; + 9F7D421E22203DB1007B68BB /* ChannelStatisticsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelStatisticsController.swift; sourceTree = ""; }; + 9F7D422922240403007B68BB /* AccountContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountContext.swift; sourceTree = ""; }; + 9F7D422B222415C9007B68BB /* SharedAccountContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedAccountContext.swift; sourceTree = ""; }; + 9F8DF3C7209228B000AED104 /* EditAccountInfoController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccountInfoController.swift; sourceTree = ""; }; + 9F9206EF20727AF30054E581 /* ChangePhoneNumberContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangePhoneNumberContainerView.swift; sourceTree = ""; }; + 9F9483B0202AF816006E873D /* CrashHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashHandler.swift; sourceTree = ""; }; + 9FA0E52820519E33001E5649 /* MP4Atom.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MP4Atom.h; sourceTree = ""; }; + 9FA0E52920519E33001E5649 /* MP4Atom.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MP4Atom.m; sourceTree = ""; }; + 9FA0E52B20519FCC001E5649 /* LiveUploadingHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LiveUploadingHelper.h; sourceTree = ""; }; + 9FA0E52C20519FCC001E5649 /* LiveUploadingHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LiveUploadingHelper.m; sourceTree = ""; }; + 9FA0E5332051A41A001E5649 /* PreUploadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreUploadManager.swift; sourceTree = ""; }; + 9FA0E5392052EDFE001E5649 /* HackUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HackUtils.m; sourceTree = ""; }; + 9FA0E53A2052EDFF001E5649 /* HackUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HackUtils.h; sourceTree = ""; }; + 9FA0E53C205693DA001E5649 /* WebSessionsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSessionsController.swift; sourceTree = ""; }; + 9FA0E53E2056E159001E5649 /* WebAuthorizationRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebAuthorizationRowItem.swift; sourceTree = ""; }; + 9FB14FBD2098896200688EF9 /* EDSunriseSet.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EDSunriseSet.h; sourceTree = ""; }; + 9FB14FBE209889A500688EF9 /* EDSunriseSet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EDSunriseSet.m; sourceTree = ""; }; + 9FB7CB6C221EB22700888EA9 /* CallSettingsModalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallSettingsModalController.swift; sourceTree = ""; }; + 9FBE0EE0201FBEFC0060FD1C /* DownloadSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadSettingsViewController.swift; sourceTree = ""; }; + 9FC4DA9521DD0B35003E2A62 /* PeerChannelMemberCategoriesContextsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerChannelMemberCategoriesContextsManager.swift; sourceTree = ""; }; + 9FC4DA9721DD0B86003E2A62 /* ChannelMemberCategoryListContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelMemberCategoryListContext.swift; sourceTree = ""; }; + 9FC4DA9921DD0BE6003E2A62 /* CachedChannelAdmins.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedChannelAdmins.swift; sourceTree = ""; }; + 9FC4DA9B21DD187C003E2A62 /* SearchPeerMembers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchPeerMembers.swift; sourceTree = ""; }; + 9FC4DA9D21DD1C6C003E2A62 /* ImageCompression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCompression.swift; sourceTree = ""; }; + 9FC4DAA721DE0626003E2A62 /* ChannelPermissionsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelPermissionsController.swift; sourceTree = ""; }; + 9FC8AD992062A5610094F7B4 /* InputDataDataSelectorRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputDataDataSelectorRowItem.swift; sourceTree = ""; }; + 9FC8AD9B2062AA630094F7B4 /* ValuesSelectorModalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValuesSelectorModalController.swift; sourceTree = ""; }; + 9FC8AD9F2062D5E70094F7B4 /* PassportAcceptRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassportAcceptRowItem.swift; sourceTree = ""; }; + 9FC8ADA12062E2DF0094F7B4 /* PassportNewPhoneNumberRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassportNewPhoneNumberRowItem.swift; sourceTree = ""; }; + 9FC8ADA32063B6450094F7B4 /* PassportTwoStepVerificationIntroItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassportTwoStepVerificationIntroItem.swift; sourceTree = ""; }; + 9FC8ADA5206925F60094F7B4 /* PassportWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassportWindowController.swift; sourceTree = ""; }; + 9FC8ADA7206A77E00094F7B4 /* countries */ = {isa = PBXFileReference; lastKnownFileType = text; path = countries; sourceTree = ""; }; + 9FDA713120E6456A001ED8ED /* ExternalMusicAlbumArtResources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalMusicAlbumArtResources.swift; sourceTree = ""; }; + 9FDA713320E65D49001ED8ED /* PeerMediaPlayerAnimationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerMediaPlayerAnimationView.swift; sourceTree = ""; }; + 9FDA713A20EA9532001ED8ED /* ReadArticlesListPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadArticlesListPreferences.swift; sourceTree = ""; }; + 9FDA713E20EE2D49001ED8ED /* PopularPeersRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopularPeersRowItem.swift; sourceTree = ""; }; + 9FDD78D021C8F0CC00F1B4EF /* ChatPollItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPollItem.swift; sourceTree = ""; }; + 9FDE0A8E21AD41C2001546D7 /* emoji1014-1.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "emoji1014-1.txt"; sourceTree = ""; }; + 9FF1DEA5225B699D009512C9 /* SearchUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchUtils.swift; sourceTree = ""; }; + 9FF32C7B21B7DF4800BF58B6 /* StickerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerSettings.swift; sourceTree = ""; }; + 9FF5A1CA2232A2FF00BC1359 /* UpgradedAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpgradedAccount.swift; sourceTree = ""; }; + 9FFAE4E2205A8C83000C028E /* MediaPlayerAudioRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPlayerAudioRenderer.swift; sourceTree = ""; }; + 9FFAE4EA205A8C87000C028E /* MediaPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPlayer.swift; sourceTree = ""; }; + 9FFAE501205A9153000C028E /* ManagedAudioSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedAudioSession.swift; sourceTree = ""; }; + 9FFAE503205A916B000C028E /* VideoPlayerProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerProxy.swift; sourceTree = ""; }; + 9FFAE505205A928C000C028E /* RingByteBuffer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RingByteBuffer.swift; sourceTree = ""; }; + 9FFAE507205A92B9000C028E /* RingBuffer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RingBuffer.h; sourceTree = ""; }; + 9FFAE508205A92B9000C028E /* RingBuffer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RingBuffer.m; sourceTree = ""; }; + 9FFAE50E205AB4A3000C028E /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = usr/lib/libiconv.tbd; sourceTree = SDKROOT; }; + 9FFAE510205AB4AB000C028E /* libbz2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libbz2.tbd; path = usr/lib/libbz2.tbd; sourceTree = SDKROOT; }; + 9FFAE513205AB50C000C028E /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; + A7029EF7240E3A5400A89ABD /* ChatListFiltersHeaderItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListFiltersHeaderItem.swift; sourceTree = ""; }; + A7029EF9240E3CCF00A89ABD /* folder.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = folder.tgs; sourceTree = ""; }; + A71DC82A23858311000EEDE2 /* CoreSpotlight.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreSpotlight.framework; path = System/Library/Frameworks/CoreSpotlight.framework; sourceTree = SDKROOT; }; + A71DC82C23858356000EEDE2 /* Spotlight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Spotlight.swift; sourceTree = ""; }; + A71DC82E2386AADF000EEDE2 /* Signature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Signature.swift; sourceTree = ""; }; + A72D7AE823C471A7005BAC59 /* PollResultController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollResultController.swift; sourceTree = ""; }; + A731924A25CAE9DC00218E0E /* AutoremoMessagesController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoremoMessagesController.swift; sourceTree = ""; }; + A731924E25CD5F2200218E0E /* destructor.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = destructor.tgs; sourceTree = ""; }; + A731925425DA7AA300218E0E /* GigagroupLanding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GigagroupLanding.swift; sourceTree = ""; }; + A731925825DBA60000218E0E /* gigagroup.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = gigagroup.tgs; sourceTree = ""; }; + A7377E1A23ACC79100AD3ADD /* ChatNavigateFailed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatNavigateFailed.swift; sourceTree = ""; }; + A7377E5B23B5E99500AD3ADD /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = usr/lib/libxml2.tbd; sourceTree = SDKROOT; }; + A7388431257E6E0C002E8424 /* GroupCallNavigationHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallNavigationHeaderView.swift; sourceTree = ""; }; + A73884342580E501002E8424 /* CGChatListIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGChatListIndicator.swift; sourceTree = ""; }; + A7393D342407CAE100CE44CA /* ChatMediaDice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMediaDice.swift; sourceTree = ""; }; + A7393D362407CD7A00CE44CA /* ChatDiceContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDiceContentView.swift; sourceTree = ""; }; + A7393D382407D0F100CE44CA /* GraphCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = GraphCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7393D412407FF0F00CE44CA /* dice_idle.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = dice_idle.tgs; sourceTree = ""; }; + A7393D432408F84300CE44CA /* DiceCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiceCache.swift; sourceTree = ""; }; + A7393D452409044C00CE44CA /* ChannelOverviewStatsRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelOverviewStatsRowItem.swift; sourceTree = ""; }; + A742CDCC240FB32F00C6B69B /* ChatListFilterRecommendedItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListFilterRecommendedItem.swift; sourceTree = ""; }; + A742CE44241A517800C6B69B /* ChannelRecentPostRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelRecentPostRowItem.swift; sourceTree = ""; }; + A74EB06F237961A1005F55AE /* AppCenter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppCenter.framework; path = submodules/AppCenter/AppCenter.framework; sourceTree = ""; }; + A74EB070237961A1005F55AE /* AppCenterCrashes.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppCenterCrashes.framework; path = submodules/AppCenter/AppCenterCrashes.framework; sourceTree = ""; }; + A7565EAD23BE02AF0031EADE /* ChatGradientModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatGradientModel.swift; sourceTree = ""; }; + A766493E236D6BFD00163DF4 /* PasscodeSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasscodeSettings.swift; sourceTree = ""; }; + A767DD4023F2BB3200366F76 /* ShortcutListController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutListController.swift; sourceTree = ""; }; + A76C8A9D2420FFE400FDB071 /* folder_empty.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = folder_empty.tgs; sourceTree = ""; }; + A76C8A9F2422132400FDB071 /* VerticalTabsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalTabsView.swift; sourceTree = ""; }; + A76C8AA124221D2800FDB071 /* graph_loading.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = graph_loading.tgs; sourceTree = ""; }; + A76C8AA324221F5400FDB071 /* StatisticsLoadingRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsLoadingRowItem.swift; sourceTree = ""; }; + A76C8AB2242366EC00FDB071 /* PeerMediaBlockRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerMediaBlockRowItem.swift; sourceTree = ""; }; + A778DC2923C75F1100DD307B /* Confetti.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Confetti.swift; sourceTree = ""; }; + A778DC2D23C77AD800DD307B /* confetti.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = confetti.mp3; sourceTree = ""; }; + A778DC2F23C8985300DD307B /* SoundEffects.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundEffects.swift; sourceTree = ""; }; + A778DC3123C8988100DD307B /* quiz-incorrect.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "quiz-incorrect.mp3"; sourceTree = ""; }; + A778DC3223C8988300DD307B /* quiz-correct.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "quiz-correct.mp3"; sourceTree = ""; }; + A778DC4123CD9F3C00DD307B /* SearchSettingsEmptyItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsEmptyItem.swift; sourceTree = ""; }; + A7831B1B2403CFDD0056AEAC /* ChannelStatsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelStatsViewController.swift; sourceTree = ""; }; + A7831B2924040B6B0056AEAC /* StatisticRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticRowItem.swift; sourceTree = ""; }; + A789E08323E05EAE00AEB34A /* ChatListFiltersListController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListFiltersListController.swift; sourceTree = ""; }; + A7918DB324093505002011CA /* GraphUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = GraphUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7919134240D0869002011CA /* MurMurHash32.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MurMurHash32.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7919137240D1077002011CA /* ChatListFilterPredicate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListFilterPredicate.swift; sourceTree = ""; }; + A7ADC4792587B3750069737A /* VoiceChatActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceChatActionButton.swift; sourceTree = ""; }; + A7B5030E23B62E0000C9838E /* nanosvg.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = nanosvg.c; sourceTree = ""; }; + A7B5030F23B62E0400C9838E /* nanosvg.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = nanosvg.h; sourceTree = ""; }; + A7B5031123B62E1700C9838E /* Svg.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Svg.m; sourceTree = ""; }; + A7B5031223B62E1A00C9838E /* Svg.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Svg.h; sourceTree = ""; }; + A7B6DDD623ED8FDF00B8E01C /* think_spectacular.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = think_spectacular.tgs; sourceTree = ""; }; + A7C1377C23D1A62700803ED3 /* PeerEmptyHolderItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerEmptyHolderItem.swift; sourceTree = ""; }; + A7C1379123DB00D900803ED3 /* ChatListFilterPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListFilterPreferences.swift; sourceTree = ""; }; + A7C1379D23DF21EA00803ED3 /* ChatListRevealItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListRevealItem.swift; sourceTree = ""; }; + A7C41DB3235862BB00CF9402 /* PeerMediaPhotosController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerMediaPhotosController.swift; sourceTree = ""; }; + A7C41DB523586DC100CF9402 /* PeerPhotosMonthItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerPhotosMonthItem.swift; sourceTree = ""; }; + A7C7215623FD45D300CE3F75 /* SaveModalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveModalController.swift; sourceTree = ""; }; + A7C7215823FD473D00CE3F75 /* success_saved.tgs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = success_saved.tgs; sourceTree = ""; }; + A7C7215A23FD4BAA00CE3F75 /* LottieLocalAnimations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieLocalAnimations.swift; sourceTree = ""; }; + A7D28204236C3C0B0000A9BF /* Postbox.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Postbox.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7D28206236C3C0F0000A9BF /* SwiftSignalKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftSignalKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7D28208236C3C150000A9BF /* TelegramCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TelegramCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7D2820A236C3C1B0000A9BF /* MtProtoKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MtProtoKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7D28210236C3D390000A9BF /* SyncCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SyncCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7D2822A236C51A50000A9BF /* OpenSSLEncryption.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OpenSSLEncryption.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7D2822C236C51F10000A9BF /* OpenSSLEncryption.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OpenSSLEncryption.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7D2822E236C549B0000A9BF /* SyncCoreExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncCoreExtension.swift; sourceTree = ""; }; + A7D28231236C57DD0000A9BF /* libphonenumber.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = libphonenumber.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7D28236236C5B2C0000A9BF /* PhoneNumberUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumberUtils.swift; sourceTree = ""; }; + A7D2823B236C69070000A9BF /* SSignalKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SSignalKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7DF1B6B237415AD00ACC01F /* Zip.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Zip.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7E831EF255040B70095A167 /* ChatPresentationInputQueryResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPresentationInputQueryResult.swift; sourceTree = ""; }; + A7E831F2255041020095A167 /* IsEqualMessages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IsEqualMessages.swift; sourceTree = ""; }; + A7E831F72551A85F0095A167 /* ColdStartPasslockController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColdStartPasslockController.swift; sourceTree = ""; }; + A7E83211255E8A8E0095A167 /* LinkHoverController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkHoverController.swift; sourceTree = ""; }; + A7E83213255E992D0095A167 /* WaveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveView.swift; sourceTree = ""; }; + A7E832182562CFC70095A167 /* VoiceBlobView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceBlobView.swift; sourceTree = ""; }; + A7ED5DAE236C7CE100040372 /* RLottie.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = RLottie.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7F282B1238D122900742C20 /* UnauthorizedConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnauthorizedConfiguration.swift; sourceTree = ""; }; + A7F282FF2395168300742C20 /* SettingsSearchRecentQueries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSearchRecentQueries.swift; sourceTree = ""; }; + A7F283012395185B00742C20 /* SettingsSearchableItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSearchableItems.swift; sourceTree = ""; }; + A7F283032395289B00742C20 /* AccountUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountUtils.swift; sourceTree = ""; }; + A7F2830523954E1B00742C20 /* CachedFaqInstantPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedFaqInstantPage.swift; sourceTree = ""; }; + A7F2830723954EF800742C20 /* CachedInstantPages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedInstantPages.swift; sourceTree = ""; }; + A7F283092395570400742C20 /* SearchSettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsController.swift; sourceTree = ""; }; + A7F2831A239A496400742C20 /* ContextShowPeersHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextShowPeersHolder.swift; sourceTree = ""; }; + A7F28336239A808500742C20 /* TemplateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateController.swift; sourceTree = ""; }; C2016F3A1EAA4538003AF981 /* RecentPeerRowItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentPeerRowItem.swift; sourceTree = ""; }; C2016F431EAA4967003AF981 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/ShareViewController.strings"; sourceTree = ""; }; C2016F451EAA4967003AF981 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = ""; }; - C2016F671EAE0A68003AF981 /* PhoneCallWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhoneCallWindowController.swift; sourceTree = ""; }; + C2016F671EAE0A68003AF981 /* CallWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallWindowController.swift; sourceTree = ""; }; C201C2311E3B2D1C0026C21E /* FastSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FastSettings.swift; sourceTree = ""; }; C20232A71D81D189007C9ADE /* ChatController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatController.swift; sourceTree = ""; }; C20232A91D81D19C007C9ADE /* ChatRowItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatRowItem.swift; sourceTree = ""; }; C20232AB1D81D1AE007C9ADE /* ChatMessageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageView.swift; sourceTree = ""; }; - C20320FE1F9769BA00143395 /* TwoStepVerificationResetController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwoStepVerificationResetController.swift; sourceTree = ""; }; C2057FA91EBC6A3C000423DC /* ChatCallRowItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatCallRowItem.swift; sourceTree = ""; }; C205DB971EE71127003711DF /* ChannelAdminController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelAdminController.swift; sourceTree = ""; }; C205DB9C1EE88762003711DF /* Views.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Views.swift; sourceTree = ""; }; C205FEA51EB39DE400455808 /* SidebarCapViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SidebarCapViewController.swift; sourceTree = ""; }; - C2084F031F5D5C6F004713C4 /* ChatReplyPreviewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatReplyPreviewController.swift; sourceTree = ""; }; C209C36F1F262537009231FE /* emoji_suggestions_data.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = emoji_suggestions_data.cpp; sourceTree = ""; }; C209C3701F262537009231FE /* emoji_suggestions_data.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.h; fileEncoding = 4; path = emoji_suggestions_data.h; sourceTree = ""; }; C209C3711F262537009231FE /* emoji_suggestions.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = emoji_suggestions.cpp; sourceTree = ""; }; C209C3721F262537009231FE /* emoji_suggestions.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.h; fileEncoding = 4; path = emoji_suggestions.h; sourceTree = ""; }; C209C3751F26271B009231FE /* EmojiSuggestionBridge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EmojiSuggestionBridge.h; sourceTree = ""; }; C209C3761F26271B009231FE /* EmojiSuggestionBridge.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = EmojiSuggestionBridge.mm; sourceTree = ""; }; - C209C3791F262858009231FE /* libstdc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libstdc++.tbd"; path = "usr/lib/libstdc++.tbd"; sourceTree = SDKROOT; }; C209C37C1F2628D7009231FE /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; }; C209C37D1F2628D7009231FE /* libc++abi.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++abi.tbd"; path = "usr/lib/libc++abi.tbd"; sourceTree = SDKROOT; }; - C209C37E1F2628D7009231FE /* libstdc++.6.0.9.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libstdc++.6.0.9.tbd"; path = "usr/lib/libstdc++.6.0.9.tbd"; sourceTree = SDKROOT; }; - C209C37F1F2628D7009231FE /* libstdc++.6.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libstdc++.6.tbd"; path = "usr/lib/libstdc++.6.tbd"; sourceTree = SDKROOT; }; C209C38C1F276D4C009231FE /* ChatSearchView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatSearchView.swift; sourceTree = ""; }; C20B8F3D1DFC52EE008A354E /* ChatEmptyPeerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatEmptyPeerItem.swift; sourceTree = ""; }; C20B8F3F1DFD9999008A354E /* ChatListNothingItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListNothingItem.swift; sourceTree = ""; }; + C20CAD111FE291E200EFF8BF /* ChatBubbleAccessoryForward.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBubbleAccessoryForward.swift; sourceTree = ""; }; + C20CAD141FE436E300EFF8BF /* SelectSizeRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectSizeRowItem.swift; sourceTree = ""; }; C20CB7281E60886E00C992AC /* LinkInvationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinkInvationController.swift; sourceTree = ""; }; C20D5AB01DA996480042616A /* EBlockItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EBlockItem.swift; sourceTree = ""; }; C20D5AB21DA9965B0042616A /* EBlockRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EBlockRowView.swift; sourceTree = ""; }; @@ -610,18 +1587,16 @@ C21074261E77F7A3006EE5EF /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; C210742B1E780CC1006EE5EF /* PhotoCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCache.swift; sourceTree = ""; }; C21095991E9FE04700E10BDB /* ChatVideoMessageContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatVideoMessageContentView.swift; sourceTree = ""; }; - C21177FF1F16BB8300AC706D /* BioViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BioViewController.swift; sourceTree = ""; }; - C21656CA1EE1CC6F0041A6BA /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = ""; }; - C21656CB1EE1CC730041A6BA /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; - C21656CC1EE1CC760041A6BA /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; - C21656CD1EE1CC7C0041A6BA /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; - C21656CE1EE1CC830041A6BA /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; + C21656CA1EE1CC6F0041A6BA /* pt-BR */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = ""; }; + C21656CB1EE1CC730041A6BA /* nl */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; + C21656CC1EE1CC760041A6BA /* it */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; + C21656CD1EE1CC7C0041A6BA /* de */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; + C21656CE1EE1CC830041A6BA /* es */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; C21656D31EE4A83C0041A6BA /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/MainMenu.strings; sourceTree = ""; }; C21656D51EE4A8400041A6BA /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/MainMenu.strings; sourceTree = ""; }; C21656D61EE4A8430041A6BA /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/MainMenu.strings; sourceTree = ""; }; C21656D81EE4A8470041A6BA /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/MainMenu.strings; sourceTree = ""; }; C21656DA1EE4A8490041A6BA /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/MainMenu.strings"; sourceTree = ""; }; - C21656E71EE576F90041A6BA /* ChatUserPopover.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatUserPopover.swift; sourceTree = ""; }; C2167E4E1DC220D800F98E03 /* PeerMediaWebpageRowContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMediaWebpageRowContent.swift; sourceTree = ""; }; C2167E501DC220E900F98E03 /* PeerMediaMusicRowContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMediaMusicRowContent.swift; sourceTree = ""; }; C2167E521DC220F600F98E03 /* PeerMediaFileRowContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMediaFileRowContent.swift; sourceTree = ""; }; @@ -635,7 +1610,6 @@ C218FF991F42030B00DD7D35 /* InstantPageChannelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageChannelItem.swift; sourceTree = ""; }; C218FF9B1F4204C400DD7D35 /* InstantPageChannelView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageChannelView.swift; sourceTree = ""; }; C219E1D61D8869F20042F0C8 /* ChatHoleRowItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHoleRowItem.swift; sourceTree = ""; }; - C219E1D81D886A160042F0C8 /* ChatHoleRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHoleRowView.swift; sourceTree = ""; }; C219E1DA1D8884290042F0C8 /* ChatHistoryEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryEntry.swift; sourceTree = ""; }; C219E1E31D8AC8370042F0C8 /* ChatUnreadRowItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatUnreadRowItem.swift; sourceTree = ""; }; C219E1E71D8AC90B0042F0C8 /* ChatUnreadRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatUnreadRowView.swift; sourceTree = ""; }; @@ -644,13 +1618,14 @@ C21A48AD1F7CFBBE0095ADB1 /* OpenGL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGL.framework; path = System/Library/Frameworks/OpenGL.framework; sourceTree = SDKROOT; }; C21A48B11F7D0D3F0095ADB1 /* VideoMessage.fsh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.glsl; path = VideoMessage.fsh; sourceTree = ""; }; C21A48B31F7D0D6B0095ADB1 /* VideoMessage.vsh */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.glsl; path = VideoMessage.vsh; sourceTree = ""; }; - C21AAE331DB0F6BC007638C5 /* MediaTitleBarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaTitleBarView.swift; sourceTree = ""; }; C21AAE351DB22CA5007638C5 /* AvatarLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarLayer.swift; sourceTree = ""; }; C21B24601ED9C39F00FC6CDA /* SuggestionLocalizationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SuggestionLocalizationViewController.swift; sourceTree = ""; }; C21B24621EDADC8600FC6CDA /* MMMenuItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MMMenuItem.swift; sourceTree = ""; }; C21B24641EDB115700FC6CDA /* StringPluralization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringPluralization.swift; sourceTree = ""; }; C21B24661EDB116B00FC6CDA /* NumberPluralizationForm.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NumberPluralizationForm.h; sourceTree = ""; }; C21B24671EDB116B00FC6CDA /* NumberPluralizationForm.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NumberPluralizationForm.m; sourceTree = ""; }; + C21BE3AE1FD099AA00C1C849 /* DeveloperViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperViewController.swift; sourceTree = ""; }; + C21BE3B01FD14CDB00C1C849 /* ParseAppearanceColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAppearanceColors.swift; sourceTree = ""; }; C2203E9F1DDE040F001E6AB6 /* libcommonCrypto.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libcommonCrypto.tbd; path = usr/lib/system/libcommonCrypto.tbd; sourceTree = SDKROOT; }; C2203EA11DDE2AB8001E6AB6 /* ChatSelectText.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatSelectText.swift; sourceTree = ""; }; C221ED531EA684BE00471C65 /* DataAndStorageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataAndStorageViewController.swift; sourceTree = ""; }; @@ -660,22 +1635,17 @@ C221ED5B1EA69AA300471C65 /* StorageUsageController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageUsageController.swift; sourceTree = ""; }; C221ED5D1EA6B36600471C65 /* ChatStorageManagmentModalController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatStorageManagmentModalController.swift; sourceTree = ""; }; C22338441F823F8C004AD57C /* VideoCameraStructures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCameraStructures.swift; sourceTree = ""; }; + C224675A1FA8546200F03E27 /* GroupedLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupedLayout.swift; sourceTree = ""; }; + C224675C1FA884E300F03E27 /* ChatGroupedItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatGroupedItem.swift; sourceTree = ""; }; C224A72C1EB75A3100F43F3F /* ExMajorNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExMajorNavigationController.swift; sourceTree = ""; }; C225524A1F7BE7000007944D /* VideoRecorderModalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRecorderModalController.swift; sourceTree = ""; }; C225524C1F7BE8E40007944D /* VideoRecorderModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRecorderModalView.swift; sourceTree = ""; }; C225524E1F7C03B50007944D /* VideoRecorderPipeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRecorderPipeline.swift; sourceTree = ""; }; C2256D971DAB9D5A00494CF4 /* ChatHistoryViewForLocation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryViewForLocation.swift; sourceTree = ""; }; - C226741E1DBCEAC2000BA9ED /* EStickerGridEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EStickerGridEntries.swift; sourceTree = ""; }; - C22674201DBCECCC000BA9ED /* EStickerGridItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EStickerGridItem.swift; sourceTree = ""; }; C226742A1DBE16B9000BA9ED /* CachedResourceRepresentations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachedResourceRepresentations.swift; sourceTree = ""; }; C226742E1DBE2CB3000BA9ED /* PanelUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PanelUtils.swift; sourceTree = ""; }; C22674301DBE9B50000BA9ED /* FetchCachedRepresentations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchCachedRepresentations.swift; sourceTree = ""; }; - C22674321DBF665A000BA9ED /* EStickerPackEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EStickerPackEntries.swift; sourceTree = ""; }; - C22674341DBF6A85000BA9ED /* EStickerPackItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EStickerPackItem.swift; sourceTree = ""; }; C22674371DC125C1000BA9ED /* PeerMediaCollectionInterfaceState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMediaCollectionInterfaceState.swift; sourceTree = ""; }; - C22674391DC1273E000BA9ED /* PeerMediaGridController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMediaGridController.swift; sourceTree = ""; }; - C226743E1DC12E4E000BA9ED /* GridMessageItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridMessageItem.swift; sourceTree = ""; }; - C22674401DC13165000BA9ED /* GridHoleItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridHoleItem.swift; sourceTree = ""; }; C22674441DC20664000BA9ED /* PeerMediaListController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMediaListController.swift; sourceTree = ""; }; C2271D9B1DACC027001792B6 /* SearchController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchController.swift; sourceTree = ""; }; C2271D9D1DACC796001792B6 /* ChatListMessageRowItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListMessageRowItem.swift; sourceTree = ""; }; @@ -691,13 +1661,11 @@ C2271DBA1DAE213D001792B6 /* ChannelInfoEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelInfoEntries.swift; sourceTree = ""; }; C2271DBE1DAE563D001792B6 /* PeerInfoHeaderItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoHeaderItem.swift; sourceTree = ""; }; C2271DC01DAE583E001792B6 /* TextAndLabelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextAndLabelItem.swift; sourceTree = ""; }; - C2271DC31DAE5E46001792B6 /* PeerInfoHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoHeaderView.swift; sourceTree = ""; }; C2271DC91DAED681001792B6 /* PresenceStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresenceStrings.swift; sourceTree = ""; }; C2271DD11DAF6DF5001792B6 /* EmptyChatViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyChatViewController.swift; sourceTree = ""; }; C2271DD61DAF80D5001792B6 /* PeerMediaController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMediaController.swift; sourceTree = ""; }; C2271F2A1DB3BEB60045E719 /* GlobalHandlers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalHandlers.swift; sourceTree = ""; }; C2271F371DB4D0490045E719 /* EmojiViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiViewController.swift; sourceTree = ""; }; - C2271F391DB4D0540045E719 /* EStickersViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EStickersViewController.swift; sourceTree = ""; }; C2271F3B1DB4D0630045E719 /* GIFViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GIFViewController.swift; sourceTree = ""; }; C2271F3D1DB4D4240045E719 /* ETabRowItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ETabRowItem.swift; sourceTree = ""; }; C2271F3F1DB4D5850045E719 /* ETabRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ETabRowView.swift; sourceTree = ""; }; @@ -724,6 +1692,7 @@ C2303E721D9966BD00098E12 /* ChatInputActionsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInputActionsView.swift; sourceTree = ""; }; C2303E771D997C3100098E12 /* ChatInputAttachView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInputAttachView.swift; sourceTree = ""; }; C2303E891D9A76D800098E12 /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; + C23044821F98F8B400977C51 /* MediaPreviewRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewRowItem.swift; sourceTree = ""; }; C230B8EE1DD3358C0057F596 /* AccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountViewController.swift; sourceTree = ""; }; C230B8F01DD348970057F596 /* AccountInfoItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountInfoItem.swift; sourceTree = ""; }; C230B8F31DD368D40057F596 /* SelectPeersController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectPeersController.swift; sourceTree = ""; }; @@ -758,7 +1727,6 @@ C234A7FA1ED7112400EBBECE /* LocalizableExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalizableExtension.swift; sourceTree = ""; }; C234A7FD1ED725C300EBBECE /* LanguageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LanguageViewController.swift; sourceTree = ""; }; C234A7FF1ED73A3300EBBECE /* LanguageRowItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LanguageRowItem.swift; sourceTree = ""; }; - C234CA901D97E117003023F7 /* PostboxMac.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = PostboxMac.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C234D4111EEDE6990017DC25 /* LoadingTableItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadingTableItem.swift; sourceTree = ""; }; C236ADDC1F7D318600E8C71A /* TGVideoCameraMovieRecorder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TGVideoCameraMovieRecorder.m; sourceTree = ""; }; C236ADDD1F7D318700E8C71A /* TGVideoCameraMovieRecorder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TGVideoCameraMovieRecorder.h; sourceTree = ""; }; @@ -780,22 +1748,23 @@ C23CF9711E70BD0B0009CD06 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/ShareViewController.strings; sourceTree = ""; }; C23CF9731E70BD0B0009CD06 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; C23D0D781F1A609300AF5151 /* SFCompactRounded-Semibold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SFCompactRounded-Semibold.otf"; sourceTree = ""; }; - C240E9511F96449E00F671FA /* TwoStepVerificationPasswordEntryController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwoStepVerificationPasswordEntryController.swift; sourceTree = ""; }; - C2412E061DA795D200588C14 /* GalleryControls.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GalleryControls.swift; sourceTree = ""; }; + C23EEC881FCC47C1001371CD /* PeerMediaDateItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerMediaDateItem.swift; sourceTree = ""; }; + C241025C1FD5702D00DB8625 /* ChatMessageBubbleImages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageBubbleImages.swift; sourceTree = ""; }; + C24102661FD587C400DB8625 /* RHARCSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RHARCSupport.h; sourceTree = ""; }; + C241026E1FD58EA800DB8625 /* SImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SImageView.swift; sourceTree = ""; }; C2423A531F2235080041907F /* InstantPageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageViewController.swift; sourceTree = ""; }; C246161A1ED33FFE0026D5BC /* InstantVideoPIP.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantVideoPIP.swift; sourceTree = ""; }; + C246D6271FAB72D4004C17FA /* MediaGroupPreviewRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGroupPreviewRowItem.swift; sourceTree = ""; }; C248BD201E6F09CC004B9106 /* ChatGameContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatGameContentView.swift; sourceTree = ""; }; C248BD231E706104004B9106 /* BlockedPeersViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockedPeersViewController.swift; sourceTree = ""; }; C248BD251E706A05004B9106 /* RecentSessionsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentSessionsController.swift; sourceTree = ""; }; C248BD281E706DDA004B9106 /* RecentSessionRowItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentSessionRowItem.swift; sourceTree = ""; }; - C24949111E5B704900D7ED5D /* AccountsListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsListViewController.swift; sourceTree = ""; }; C24949131E5B763F00D7ED5D /* ApplicationContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApplicationContext.swift; sourceTree = ""; }; C24BA3BC1E9D30F800E8970B /* DeleteSupergroupMessagesModalController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteSupergroupMessagesModalController.swift; sourceTree = ""; }; C24D9F901E1F8F85002CD3F3 /* MajorBackNavigationBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MajorBackNavigationBar.swift; sourceTree = ""; }; C24D9FC31E24FFF3002CD3F3 /* PasscodeLockController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasscodeLockController.swift; sourceTree = ""; }; C24D9FC61E2500AC002CD3F3 /* PasscodeSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasscodeSettingsViewController.swift; sourceTree = ""; }; C24D9FC91E25033E002CD3F3 /* PrivacyAndSecurityViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivacyAndSecurityViewController.swift; sourceTree = ""; }; - C24D9FDB1E267932002CD3F3 /* PreviewSenderItems.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewSenderItems.swift; sourceTree = ""; }; C24DAB851E08026C005EE404 /* MGalleryVideoItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MGalleryVideoItem.swift; sourceTree = ""; }; C24DAB921E0828B6005EE404 /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; C24DAB981E083184005EE404 /* YTVimeoError.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = YTVimeoError.h; sourceTree = ""; }; @@ -839,38 +1808,28 @@ C250B0381DB7BB2D004E9FBE /* MimeTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MimeTypes.swift; sourceTree = ""; }; C250BA8E1E6E1CDC0057CD96 /* ChatMessageThrottledProcessingManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageThrottledProcessingManager.swift; sourceTree = ""; }; C250BA931E6E84880057CD96 /* VideoToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = VideoToolbox.framework; path = System/Library/Frameworks/VideoToolbox.framework; sourceTree = SDKROOT; }; - C25253251DF03F5700ADBC98 /* TGOpusAudioRecorder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGOpusAudioRecorder.h; sourceTree = ""; }; - C25253261DF03F5700ADBC98 /* TGOpusAudioRecorder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGOpusAudioRecorder.m; sourceTree = ""; }; - C25253281DF03F9600ADBC98 /* TGAudioWaveform.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGAudioWaveform.h; sourceTree = ""; }; - C25253291DF03F9600ADBC98 /* TGAudioWaveform.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGAudioWaveform.m; sourceTree = ""; }; + C251FB4B1FEDCC750035E5D7 /* ChatPresentationUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPresentationUtils.swift; sourceTree = ""; }; + C251FB501FEE300E0035E5D7 /* Telegram.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = Telegram.xcodeproj; sourceTree = ""; }; C252532C1DF0440300ADBC98 /* begin_record.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = begin_record.caf; sourceTree = ""; }; - C2538E511E770B4600B21DF0 /* GroupAdminsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupAdminsController.swift; sourceTree = ""; }; - C253A92C1D8EE1A600CDC850 /* ChatMediaView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaView.swift; sourceTree = ""; }; C253A9431D90303200CDC850 /* FastBlur.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FastBlur.h; sourceTree = ""; }; C253A9441D90303200CDC850 /* FastBlur.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FastBlur.m; sourceTree = ""; }; C253A94A1D9032A000CDC850 /* Telegram-Mac-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Telegram-Mac-Bridging-Header.h"; sourceTree = ""; }; - C253A9571D9165A400CDC850 /* decode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = decode.h; sourceTree = ""; }; - C253A9581D9165A400CDC850 /* encode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = encode.h; sourceTree = ""; }; - C253A9591D9165A400CDC850 /* types.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = types.h; sourceTree = ""; }; - C253A95B1D9165A400CDC850 /* libwebp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libwebp.a; sourceTree = ""; }; - C253A95D1D9165CD00CDC850 /* webp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = webp.h; sourceTree = ""; }; - C253A95E1D9165CD00CDC850 /* webp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = webp.m; sourceTree = ""; }; C253A9601D917ACD00CDC850 /* ChatStickerContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatStickerContentView.swift; sourceTree = ""; }; C253A9621D91A90100CDC850 /* ChatFileMediaItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatFileMediaItem.swift; sourceTree = ""; }; C253A9661D92E3AE00CDC850 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; C253A96C1D92F69C00CDC850 /* ReplyModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplyModel.swift; sourceTree = ""; }; - C253A9711D92F9F100CDC850 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + C253A9711D92F9F100CDC850 /* en */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; C253A9731D94182500CDC850 /* ChatRightView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatRightView.swift; sourceTree = ""; }; C253E2251DE335C10022A29F /* ChatVoiceContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatVoiceContentView.swift; sourceTree = ""; }; C253E22D1DE33A7C0022A29F /* ChatMusicRowItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMusicRowItem.swift; sourceTree = ""; }; C253E2301DE34FBB0022A29F /* AudioPlayerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioPlayerController.swift; sourceTree = ""; }; C253E2331DE3776A0022A29F /* InlineAudioPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InlineAudioPlayerView.swift; sourceTree = ""; }; - C253E2351DE398580022A29F /* AudioPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = ""; }; - C253E2371DE398980022A29F /* NativeAudioPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NativeAudioPlayer.swift; sourceTree = ""; }; C253E2391DE4D3DB0022A29F /* ChatInterfaceInputContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceInputContext.swift; sourceTree = ""; }; C253E23B1DE4D4080022A29F /* ChatInterfaceStateContextQueries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceStateContextQueries.swift; sourceTree = ""; }; C253E23D1DE61CB50022A29F /* ContextListRowItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextListRowItem.swift; sourceTree = ""; }; - C25692971EDD85FB009EE421 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/MainMenu.xib; sourceTree = ""; }; + C25692971EDD85FB009EE421 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + C256A9131FB9CBF10043D497 /* LocalAuthentication.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LocalAuthentication.framework; path = System/Library/Frameworks/LocalAuthentication.framework; sourceTree = SDKROOT; }; + C256A9151FB9E1490043D497 /* AdditionalSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdditionalSettings.swift; sourceTree = ""; }; C258D1B31F8D385700458478 /* PreHistorySettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreHistorySettingsController.swift; sourceTree = ""; }; C258D1B51F8D3A0D00458478 /* PreHistoryControllerStructures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreHistoryControllerStructures.swift; sourceTree = ""; }; C25911361DF1A68200671E72 /* ChatInputRecordingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInputRecordingView.swift; sourceTree = ""; }; @@ -878,7 +1837,7 @@ C2593FBE1F7D241E00F6D2B1 /* TGPaintShader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TGPaintShader.m; sourceTree = ""; }; C259ED1B1DB8DC78008E6712 /* ChatNavigateScroller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatNavigateScroller.swift; sourceTree = ""; }; C259ED1D1DB956C1008E6712 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; - C25BB1681F867FEE0089ED02 /* ChatVideoAccessoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatVideoAccessoryView.swift; sourceTree = ""; }; + C25BB1681F867FEE0089ED02 /* ChatMessageAccessoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageAccessoryView.swift; sourceTree = ""; }; C25C132C1E8A404F00AE26A1 /* InstalledStickerPacksController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstalledStickerPacksController.swift; sourceTree = ""; }; C25C132E1E8A405E00AE26A1 /* ArchivedStickerPacksController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArchivedStickerPacksController.swift; sourceTree = ""; }; C25C13301E8A406D00AE26A1 /* FeaturedStickerPacksController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeaturedStickerPacksController.swift; sourceTree = ""; }; @@ -892,7 +1851,7 @@ C26505961E041B90001954DC /* MGalleryGIFItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MGalleryGIFItem.swift; sourceTree = ""; }; C26546CB1EA0AC3C00E3969A /* ChatVideoMessageItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatVideoMessageItem.swift; sourceTree = ""; }; C26A37EB1E5DE464006977AC /* ChannelAdminsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelAdminsViewController.swift; sourceTree = ""; }; - C26A37ED1E5DE48F006977AC /* ChannelBlacklistViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelBlacklistViewController.swift; sourceTree = ""; }; + C26A37ED1E5DE48F006977AC /* ChannelBlocklistViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelBlocklistViewController.swift; sourceTree = ""; }; C26A71981DC9FA5100F69385 /* EditMessageModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditMessageModel.swift; sourceTree = ""; }; C26A719A1DC9FB3600F69385 /* InputPasteboardParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputPasteboardParser.swift; sourceTree = ""; }; C26D8A3B1E464944002FAA3F /* JoinLinkPreviewModalController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JoinLinkPreviewModalController.swift; sourceTree = ""; }; @@ -900,12 +1859,12 @@ C26E82D01E83EFFE0046DF2F /* TimeObserver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TimeObserver.m; sourceTree = ""; }; C271EB8F1EB8E6A40034792D /* SelectivePrivacySettingsPeersController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectivePrivacySettingsPeersController.swift; sourceTree = ""; }; C271EB961EB916870034792D /* libtgvoip.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = libtgvoip.framework; path = "../../Library/Developer/Xcode/DerivedData/Telegram-Mac-hikohhgxyaqnbcboyjbphnuqswbk/Build/Products/Debug/libtgvoip.framework"; sourceTree = ""; }; - C271EB991EB9DEF00034792D /* CallBridge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CallBridge.h; sourceTree = ""; }; - C271EB9A1EB9DEF00034792D /* CallBridge.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = CallBridge.mm; sourceTree = ""; }; + C271EB991EB9DEF00034792D /* OngoingCallThreadLocalContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OngoingCallThreadLocalContext.h; sourceTree = ""; }; + C271EB9A1EB9DEF00034792D /* OngoingCallThreadLocalContext.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = OngoingCallThreadLocalContext.mm; sourceTree = ""; }; C271EBA01EB9F04E0034792D /* TGCallUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGCallUtils.h; sourceTree = ""; }; C271EBA11EB9F04E0034792D /* TGCallUtils.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = TGCallUtils.mm; sourceTree = ""; }; - C271EBE71EBA22FE0034792D /* TGCallConnectionDescription.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGCallConnectionDescription.h; sourceTree = ""; }; - C271EBE81EBA22FE0034792D /* TGCallConnectionDescription.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGCallConnectionDescription.m; sourceTree = ""; }; + C271EBE71EBA22FE0034792D /* OngoingCallConnectionDescription.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OngoingCallConnectionDescription.h; sourceTree = ""; }; + C271EBE81EBA22FE0034792D /* OngoingCallConnectionDescription.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OngoingCallConnectionDescription.m; sourceTree = ""; }; C271EBEA1EBA3BC90034792D /* PCallSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PCallSession.swift; sourceTree = ""; }; C275932D1DF6E1CE00A0807A /* AboutModalController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutModalController.swift; sourceTree = ""; }; C275E9EE1F8FCA4200D3D8C0 /* PhoneNumberIntroController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumberIntroController.swift; sourceTree = ""; }; @@ -930,7 +1889,7 @@ C27AAFE81DE9DA61009B9629 /* CountryManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CountryManager.swift; sourceTree = ""; }; C27AAFEC1DEB1D72009B9629 /* SignalUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalUtils.swift; sourceTree = ""; }; C27AAFEE1DEB2EA9009B9629 /* EmptyComposeController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyComposeController.swift; sourceTree = ""; }; - C28149871EA7F22200BB933E /* PreparedChatHistoryViewTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreparedChatHistoryViewTransition.swift; sourceTree = ""; }; + C27BAC7920CFCE68007A7508 /* PassportSettingsHeaderItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassportSettingsHeaderItem.swift; sourceTree = ""; }; C28149891EA7F44300BB933E /* ListViewIntermediateState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListViewIntermediateState.swift; sourceTree = ""; }; C2844AD61DA907E8009308DC /* EntertainmentViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EntertainmentViewController.swift; sourceTree = ""; }; C2844ADF1DA90C8A009308DC /* emoji.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = emoji.txt; sourceTree = ""; }; @@ -938,11 +1897,12 @@ C28BAB281DF981320027CE3A /* AudioWaveform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioWaveform.swift; sourceTree = ""; }; C28BAB2A1DF9C2790027CE3A /* DateUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DateUtils.h; sourceTree = ""; }; C28BAB2B1DF9C2790027CE3A /* DateUtils.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = DateUtils.mm; sourceTree = ""; }; + C2905E1B207E4D9E00990AD7 /* InstantPageAudioView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageAudioView.swift; sourceTree = ""; }; + C2905E1D207E545600990AD7 /* InstantPageAudioItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantPageAudioItem.swift; sourceTree = ""; }; C291942E1DCC6E2200359491 /* DeclareEncodables.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeclareEncodables.swift; sourceTree = ""; }; C291E2721E8AFA2C00D397BA /* ShareApplicationContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShareApplicationContext.swift; sourceTree = ""; }; C29340F01F506C310074991E /* EmptyGroupstickerSearchRowItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmptyGroupstickerSearchRowItem.swift; sourceTree = ""; }; C295C65E1F75808600BA309D /* ChatAdditionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatAdditionController.swift; sourceTree = ""; }; - C29670781F0FAAC800884DA2 /* AppearanceViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppearanceViewController.swift; sourceTree = ""; }; C296707A1F0FBFB500884DA2 /* ThemeSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThemeSettings.swift; sourceTree = ""; }; C296AF7E1D8D38E5001DBB59 /* ChatRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatRowView.swift; sourceTree = ""; }; C296AF851D8DB178001DBB59 /* MediaUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaUtils.swift; sourceTree = ""; }; @@ -963,12 +1923,10 @@ C29B5F4E1DC8F39A00D13E65 /* FWDNavigationAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FWDNavigationAction.swift; sourceTree = ""; }; C29C3E5A1E421F1700193A7E /* ChatAccessoryModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatAccessoryModel.swift; sourceTree = ""; }; C29C3E6E1E4352C100193A7E /* ContextStickerRowItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextStickerRowItem.swift; sourceTree = ""; }; - C29C3E701E43881500193A7E /* StickerPreviewModalController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickerPreviewModalController.swift; sourceTree = ""; }; + C29C3E701E43881500193A7E /* ModalPreviewViews.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModalPreviewViews.swift; sourceTree = ""; }; C29C3E721E4397F300193A7E /* StickerPreviewHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickerPreviewHandler.swift; sourceTree = ""; }; C29E0EDF1F4DC43100C0C7A8 /* InstantViewAppearance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantViewAppearance.swift; sourceTree = ""; }; C29E0EE11F4EFB5100C0C7A8 /* GroupStickerSetController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupStickerSetController.swift; sourceTree = ""; }; - C29F4C741F45F58B00DBFC00 /* MIHSliderView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MIHSliderView.h; sourceTree = ""; }; - C29F4C751F45F58B00DBFC00 /* MIHSliderView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MIHSliderView.m; sourceTree = ""; }; C29F4C771F45FBFF00DBFC00 /* InstantPageSlideshowItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageSlideshowItem.swift; sourceTree = ""; }; C29F4C7C1F47283600DBFC00 /* InstantPageBrowser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageBrowser.swift; sourceTree = ""; }; C2A1054A1E0163D500B01F48 /* GalleryPageController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GalleryPageController.swift; sourceTree = ""; }; @@ -986,11 +1944,9 @@ C2A506781DF59C9700971A93 /* PicturePicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PicturePicker.swift; sourceTree = ""; }; C2A5067A1DF5BE6900971A93 /* GeneralTextRowItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneralTextRowItem.swift; sourceTree = ""; }; C2A71CD61DD9EEDB00C69F73 /* WebpageModalController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebpageModalController.swift; sourceTree = ""; }; - C2A71CD81DDA0FA300C69F73 /* HockeySDK.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = HockeySDK.framework; sourceTree = ""; }; C2A71CE01DDB18FF00C69F73 /* ThumbUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbUtils.swift; sourceTree = ""; }; C2A71CE21DDB2EBD00C69F73 /* GeneralSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneralSettingsViewController.swift; sourceTree = ""; }; C2A71CE61DDB2F8700C69F73 /* UsernameSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UsernameSettingsViewController.swift; sourceTree = ""; }; - C2A71CE81DDB342100C69F73 /* NotificationSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationSettingsViewController.swift; sourceTree = ""; }; C2A71CEC1DDB3C1000C69F73 /* ChatHeaderController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHeaderController.swift; sourceTree = ""; }; C2A72D8F1DEC66F300C3B945 /* LoginErrorStateView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginErrorStateView.swift; sourceTree = ""; }; C2A87DE71F4C6910002D3F73 /* InstantViewWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantViewWindow.swift; sourceTree = ""; }; @@ -1002,7 +1958,6 @@ C2AF01121F01543200D8AC1D /* ExportProxyModalController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExportProxyModalController.swift; sourceTree = ""; }; C2AF011B1F03D4C600D8AC1D /* TGCallAesCtr.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGCallAesCtr.h; sourceTree = ""; }; C2AF011C1F03D4C600D8AC1D /* TGCallAesCtr.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGCallAesCtr.m; sourceTree = ""; }; - C2AF3B811E5CD79200DFDD81 /* ConvertGroupViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConvertGroupViewController.swift; sourceTree = ""; }; C2B0722D1DFEDE430082939D /* UsernameInputRowItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UsernameInputRowItem.swift; sourceTree = ""; }; C2B1A0EE1D9D94CE00ACB1DD /* SeparatorRowItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeparatorRowItem.swift; sourceTree = ""; }; C2B1A0F01D9D94E400ACB1DD /* SeparatorRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeparatorRowView.swift; sourceTree = ""; }; @@ -1012,7 +1967,6 @@ C2B1A1261DA3D84900ACB1DD /* ChatInputAccessory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInputAccessory.swift; sourceTree = ""; }; C2B1A1351DA6587100ACB1DD /* GalleryViewer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GalleryViewer.swift; sourceTree = ""; }; C2B1B11D1E5F151D00895E0D /* ChannelVisibilityController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelVisibilityController.swift; sourceTree = ""; }; - C2B1B11F1E5F170A00895E0D /* ValidateAddressNameInteractive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValidateAddressNameInteractive.swift; sourceTree = ""; }; C2B1B1251E5F840B00895E0D /* PeerInfoUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoUtils.swift; sourceTree = ""; }; C2B4D0C01E34E07100CBC4E6 /* PrettyGridUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrettyGridUtils.swift; sourceTree = ""; }; C2B4D0C71E36047F00CBC4E6 /* ChatActivitiesModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatActivitiesModel.swift; sourceTree = ""; }; @@ -1026,18 +1980,16 @@ C2BB12091ED87C5A00BDE46A /* ControllerExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControllerExtension.swift; sourceTree = ""; }; C2BB2DAF1F8BDF6700520255 /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; C2C030CC1DD5097400617711 /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; - C2C5C2041EF822B900AEA252 /* ProxySettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProxySettingsViewController.swift; sourceTree = ""; }; + C2C415E41FA33D1A00FF36F4 /* InputFormatterPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputFormatterPopover.swift; sourceTree = ""; }; C2C738F01DD898DA00CE9D8A /* AVKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVKit.framework; path = System/Library/Frameworks/AVKit.framework; sourceTree = SDKROOT; }; C2C73A5A1EEAF3AE00DB8420 /* ChannelEventFilterModalController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelEventFilterModalController.swift; sourceTree = ""; }; C2C98FEE1E818FB5009CBDB7 /* ClearUserNotifies.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClearUserNotifies.swift; sourceTree = ""; }; C2C9B9281E8011B000380D79 /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = System/Library/Frameworks/IOKit.framework; sourceTree = SDKROOT; }; C2C9B92A1E8011CD00380D79 /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; }; - C2C9B9321E8016C400380D79 /* NSObject+SPInvocationGrabbing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSObject+SPInvocationGrabbing.h"; sourceTree = ""; }; - C2C9B9331E8016C400380D79 /* NSObject+SPInvocationGrabbing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSObject+SPInvocationGrabbing.m"; sourceTree = ""; }; - C2C9B9351E8016D400380D79 /* SPMediaKeyTap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPMediaKeyTap.h; sourceTree = ""; }; - C2C9B9361E8016D400380D79 /* SPMediaKeyTap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPMediaKeyTap.m; sourceTree = ""; }; C2CBCABF1D81528700142EC0 /* System.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = System.swift; sourceTree = ""; }; C2CBCAC41D81649E00142EC0 /* ChatListRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListRowView.swift; sourceTree = ""; }; + C2CE43E320E2CFE700656543 /* PlayerListController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerListController.swift; sourceTree = ""; }; + C2CE43E820F4F74F00656543 /* UpdateModalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateModalController.swift; sourceTree = ""; }; C2CFCAB91EBB4A2200843F6A /* voip_busy.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = voip_busy.caf; sourceTree = ""; }; C2CFCABA1EBB4A2200843F6A /* voip_end.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = voip_end.caf; sourceTree = ""; }; C2CFCABB1EBB4A2200843F6A /* voip_fail.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = voip_fail.caf; sourceTree = ""; }; @@ -1048,7 +2000,7 @@ C2D187ED1E28B9CB0038961D /* ShareInlineResultNavigationAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShareInlineResultNavigationAction.swift; sourceTree = ""; }; C2D187EF1E28C58C0038961D /* ContextSwitchPeerRowItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextSwitchPeerRowItem.swift; sourceTree = ""; }; C2D187F11E28D0840038961D /* ChatSwitchInlineController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatSwitchInlineController.swift; sourceTree = ""; }; - C2D2CAEC1E64579700939968 /* StickersPackPreviewModalController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickersPackPreviewModalController.swift; sourceTree = ""; }; + C2D2CAEC1E64579700939968 /* StickerPackPreviewModalController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickerPackPreviewModalController.swift; sourceTree = ""; }; C2D2CAEF1E64874600939968 /* StickerPackGridItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickerPackGridItem.swift; sourceTree = ""; }; C2D70AF11F2BFB3700AE768E /* Telegram.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = Telegram.xcodeproj; sourceTree = ""; }; C2DDA04D1EC0C024003531BB /* opening.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = opening.mp3; sourceTree = ""; }; @@ -1066,43 +2018,12 @@ C2DE5D3B1F3CAE120081EC1E /* InstantPageTextItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageTextItem.swift; sourceTree = ""; }; C2DE5D3D1F3CAF2B0081EC1E /* InstantPageShapeItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageShapeItem.swift; sourceTree = ""; }; C2DE5D3F1F3CB0380081EC1E /* InstantPageTileView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPageTileView.swift; sourceTree = ""; }; - C2DEC87E1DECB8C800F6544A /* TelegramApplicationContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramApplicationContext.swift; sourceTree = ""; }; C2DF47951DE71160003AA6C0 /* GIFContainerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GIFContainerView.swift; sourceTree = ""; }; - C2DF47971DE79540003AA6C0 /* libopus.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libopus.a; path = "thrid-party/opus/lib/libopus.a"; sourceTree = ""; }; - C2DF479B1DE79574003AA6C0 /* bitwise.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = bitwise.c; sourceTree = ""; }; - C2DF479C1DE79574003AA6C0 /* framing.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = framing.c; sourceTree = ""; }; - C2DF479D1DE79574003AA6C0 /* ogg.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ogg.h; sourceTree = ""; }; - C2DF479E1DE79574003AA6C0 /* os_types.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = os_types.h; sourceTree = ""; }; - C2DF47A21DE79574003AA6C0 /* opus.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = opus.h; sourceTree = ""; }; - C2DF47A31DE79574003AA6C0 /* opus_defines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = opus_defines.h; sourceTree = ""; }; - C2DF47A41DE79574003AA6C0 /* opus_multistream.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = opus_multistream.h; sourceTree = ""; }; - C2DF47A51DE79574003AA6C0 /* opus_types.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = opus_types.h; sourceTree = ""; }; - C2DF47A71DE79574003AA6C0 /* libopus.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libopus.a; sourceTree = ""; }; - C2DF47A91DE79574003AA6C0 /* diag_range.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = diag_range.c; sourceTree = ""; }; - C2DF47AA1DE79574003AA6C0 /* diag_range.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = diag_range.h; sourceTree = ""; }; - C2DF47AB1DE79574003AA6C0 /* opus_header.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = opus_header.c; sourceTree = ""; }; - C2DF47AC1DE79574003AA6C0 /* opus_header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = opus_header.h; sourceTree = ""; }; - C2DF47AD1DE79574003AA6C0 /* opusenc.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = opusenc.h; sourceTree = ""; }; - C2DF47AE1DE79574003AA6C0 /* opusenc.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = opusenc.m; sourceTree = ""; }; - C2DF47AF1DE79574003AA6C0 /* picture.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = picture.c; sourceTree = ""; }; - C2DF47B01DE79574003AA6C0 /* picture.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = picture.h; sourceTree = ""; }; - C2DF47B11DE79574003AA6C0 /* wav_io.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = wav_io.c; sourceTree = ""; }; - C2DF47B21DE79574003AA6C0 /* wav_io.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = wav_io.h; sourceTree = ""; }; - C2DF47B41DE79574003AA6C0 /* info.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = info.c; sourceTree = ""; }; - C2DF47B51DE79574003AA6C0 /* internal.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = internal.c; sourceTree = ""; }; - C2DF47B61DE79574003AA6C0 /* internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = internal.h; sourceTree = ""; }; - C2DF47B71DE79574003AA6C0 /* opusfile.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = opusfile.c; sourceTree = ""; }; - C2DF47B81DE79574003AA6C0 /* opusfile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = opusfile.h; sourceTree = ""; }; - C2DF47B91DE79574003AA6C0 /* stream.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = stream.c; sourceTree = ""; }; C2DF47C91DE79719003AA6C0 /* TGDataItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGDataItem.h; sourceTree = ""; }; C2DF47CA1DE79719003AA6C0 /* TGDataItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGDataItem.m; sourceTree = ""; }; C2DF47CC1DE79751003AA6C0 /* ATQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ATQueue.h; sourceTree = ""; }; C2DF47CD1DE79751003AA6C0 /* ATQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ATQueue.m; sourceTree = ""; }; - C2DF47CF1DE82475003AA6C0 /* OpusAudioPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpusAudioPlayer.swift; sourceTree = ""; }; - C2DF47D11DE824FD003AA6C0 /* OpusObjcBridge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OpusObjcBridge.h; sourceTree = ""; }; - C2DF47D21DE824FD003AA6C0 /* OpusObjcBridge.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = OpusObjcBridge.mm; sourceTree = ""; }; C2DF47D41DE82524003AA6C0 /* libc++.1.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.1.tbd"; path = "usr/lib/libc++.1.tbd"; sourceTree = SDKROOT; }; - C2DF47D71DE82582003AA6C0 /* OpusAudioBuffer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OpusAudioBuffer.h; sourceTree = ""; }; C2DF47D81DE82653003AA6C0 /* NSObject+TGLock.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSObject+TGLock.h"; sourceTree = ""; }; C2DF47D91DE82653003AA6C0 /* NSObject+TGLock.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSObject+TGLock.m"; sourceTree = ""; }; C2DF47DF1DE846AB003AA6C0 /* ChatAudioContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatAudioContentView.swift; sourceTree = ""; }; @@ -1110,48 +2031,213 @@ C2DF47E31DE84A67003AA6C0 /* ChatVoiceRowItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatVoiceRowItem.swift; sourceTree = ""; }; C2DF47E81DE892E3003AA6C0 /* ChatToasterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatToasterView.swift; sourceTree = ""; }; C2DF47ED1DE9A1F3003AA6C0 /* LoginViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; - C2E064621ECCB24000387BB8 /* NativeCallSettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NativeCallSettingsViewController.swift; sourceTree = ""; }; - C2E064631ECCB24000387BB8 /* NativeCallSettingsViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NativeCallSettingsViewController.xib; sourceTree = ""; }; C2E0646A1ECF137300387BB8 /* ChatInvoiceItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInvoiceItem.swift; sourceTree = ""; }; C2E40A1A1E37ADAF0099AC7D /* PeerMediaEmptyRowItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMediaEmptyRowItem.swift; sourceTree = ""; }; C2E52A0C1EB8C385009AF87D /* SelectivePrivacySettingsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectivePrivacySettingsController.swift; sourceTree = ""; }; + C2E6F3CE1F9F85260023653D /* ContextHashtagRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextHashtagRowItem.swift; sourceTree = ""; }; C2E8694C1F43500D00BDD0A2 /* ChatNavigationMention.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatNavigationMention.swift; sourceTree = ""; }; + C2E8BA061FB5EF4C00DEB5E2 /* GalleryThumbsControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryThumbsControl.swift; sourceTree = ""; }; + C2E8BA091FB5F15900DEB5E2 /* GalleryThumbsControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryThumbsControlView.swift; sourceTree = ""; }; C2EA177E1E2FD50000887153 /* TransformImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransformImageView.swift; sourceTree = ""; }; C2EA17801E2FD50A00887153 /* ImageUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageUtils.swift; sourceTree = ""; }; - C2EA53461F751EF300C183F7 /* GalleryMessageEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryMessageEntry.swift; sourceTree = ""; }; + C2EBBEA01FB5CA94009AD8ED /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; }; C2F4ED1A1EC5AE1D005F2696 /* CallRatingModalViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallRatingModalViewController.swift; sourceTree = ""; }; C2F6190C1E844DCD007A051B /* TelegramAccountAuxiliaryMethods.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramAccountAuxiliaryMethods.swift; sourceTree = ""; }; C2F8923C1E3FA51000D98B2D /* PasteboardUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasteboardUtils.swift; sourceTree = ""; }; C2F93A2C1F3C55C500BCD48F /* EmojiToleranceController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiToleranceController.swift; sourceTree = ""; }; - C2F952AF1F8E1C840056E586 /* CachedAdminIds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedAdminIds.swift; sourceTree = ""; }; C2F9C4471F9500B4002B2CBF /* TwoStepVerificationUnlockController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwoStepVerificationUnlockController.swift; sourceTree = ""; }; - C2F9C4491F9500C3002B2CBF /* TwoStepVerificationUnlockStructures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwoStepVerificationUnlockStructures.swift; sourceTree = ""; }; C2F9C44B1F95FE58002B2CBF /* Markdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Markdown.swift; sourceTree = ""; }; C2FB2FAA1EBF73CF0093C8BA /* RecentCallsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentCallsViewController.swift; sourceTree = ""; }; - C2FBC1D61DC61AFF0063A23B /* TelegramCoreMac.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = TelegramCoreMac.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - C2FBC1D81DC61B050063A23B /* SwiftSignalKitMac.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = SwiftSignalKitMac.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - C2FBC1DE1DC61B580063A23B /* MtProtoKitMac.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = MtProtoKitMac.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C2FBC1E61DC631980063A23B /* SPPreviewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SPPreviewController.swift; sourceTree = ""; }; C2FD33E81E696A86008D13D4 /* GroupsInCommonViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupsInCommonViewController.swift; sourceTree = ""; }; C2FD33ED1E697B31008D13D4 /* TransformOutgoingMessageMedia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransformOutgoingMessageMedia.swift; sourceTree = ""; }; C2FD33EF1E697EC2008D13D4 /* QuickLook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLook.framework; path = System/Library/Frameworks/QuickLook.framework; sourceTree = SDKROOT; }; C2FD33F11E69CF6D008D13D4 /* ChatUrlPreviewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatUrlPreviewModel.swift; sourceTree = ""; }; - C2FD33F41E6C1486008D13D4 /* SSKeychain.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SSKeychain.h; sourceTree = ""; }; - C2FD33F51E6C1486008D13D4 /* SSKeychain.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SSKeychain.m; sourceTree = ""; }; - C2FD33F61E6C1486008D13D4 /* SSKeychainQuery.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SSKeychainQuery.h; sourceTree = ""; }; - C2FD33F71E6C1486008D13D4 /* SSKeychainQuery.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SSKeychainQuery.m; sourceTree = ""; }; C2FD33FA1E6C169C008D13D4 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; - C2FD34121E6C2503008D13D4 /* LegacyImportAuthorization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyImportAuthorization.swift; sourceTree = ""; }; C2FD34141E6C9003008D13D4 /* BaseApplicationSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseApplicationSettings.swift; sourceTree = ""; }; C2FD382D1DCA1FA3009DC28C /* PreviewSenderController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewSenderController.swift; sourceTree = ""; }; C2FD38311DCA215F009DC28C /* GeneralInputRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneralInputRow.swift; sourceTree = ""; }; C2FF145F1E532C0A007B7B14 /* SearchEmptyRowItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchEmptyRowItem.swift; sourceTree = ""; }; + D001E973243B1A0A009025F9 /* LeftSidebarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeftSidebarController.swift; sourceTree = ""; }; + D001E975243B385E009025F9 /* FolderIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderIcons.swift; sourceTree = ""; }; + D001E977243B4639009025F9 /* LeftSidebarFolderItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeftSidebarFolderItem.swift; sourceTree = ""; }; + D004167422D37AD00000566B /* StickerPackPanelRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPackPanelRowItem.swift; sourceTree = ""; }; + D004167622D4AD3B0000566B /* StickerPackItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerPackItems.swift; sourceTree = ""; }; + D004BD2A23153415009A54B1 /* ThemePreviewModalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemePreviewModalController.swift; sourceTree = ""; }; + D00746C9257691760000DF74 /* StickerShimmerEffectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerShimmerEffectView.swift; sourceTree = ""; }; + D00747002577A7D40000DF74 /* DDHotKey.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DDHotKey.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D00747032577AF0B0000DF74 /* PushToTalkRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushToTalkRowItem.swift; sourceTree = ""; }; + D0076E362568482E007EF588 /* OpusBinding.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OpusBinding.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0076E8A25685339007EF588 /* webrtc.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = webrtc.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0076E9125685596007EF588 /* libwebp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libwebp.a; path = "core-xprojects/libwebp/build/libwebp/lib/libwebp.a"; sourceTree = ""; }; + D0076E97256855A2007EF588 /* libopus.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libopus.a; path = "core-xprojects/libopus/build/libopus/lib/libopus.a"; sourceTree = ""; }; + D0076E9A256855AB007EF588 /* libmac_framework_objc_static.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libmac_framework_objc_static.a; path = "core-xprojects/webrtc/build/webrtc/libmac_framework_objc_static.a"; sourceTree = ""; }; + D0076E9E2568574F007EF588 /* libavformat.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavformat.a; path = "core-xprojects/ffmpeg/build/ffmpeg/lib/libavformat.a"; sourceTree = ""; }; + D0076E9F25685750007EF588 /* libswresample.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libswresample.a; path = "core-xprojects/ffmpeg/build/ffmpeg/lib/libswresample.a"; sourceTree = ""; }; + D0076EA025685750007EF588 /* libavcodec.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavcodec.a; path = "core-xprojects/ffmpeg/build/ffmpeg/lib/libavcodec.a"; sourceTree = ""; }; + D0076EA125685750007EF588 /* libavutil.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavutil.a; path = "core-xprojects/ffmpeg/build/ffmpeg/lib/libavutil.a"; sourceTree = ""; }; + D0076EA825685919007EF588 /* libcrypto.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libcrypto.a; path = "core-xprojects/OpenSSLEncryption/build/openssl/lib/libcrypto.a"; sourceTree = ""; }; + D0076EA925685919007EF588 /* libssl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libssl.a; path = "core-xprojects/OpenSSLEncryption/build/openssl/lib/libssl.a"; sourceTree = ""; }; + D0076EE725685A50007EF588 /* AppCenter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AppCenter.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0076EEA25685BB5007EF588 /* libbz2.1.0.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libbz2.1.0.tbd; path = usr/lib/libbz2.1.0.tbd; sourceTree = SDKROOT; }; + D0076FFD25691EDE007EF588 /* FFMpegBinding.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FFMpegBinding.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D00B191B24E54BA3006CCB87 /* CallReceptionControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallReceptionControl.swift; sourceTree = ""; }; + D00B191D24E54F20006CCB87 /* CallStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallStatusView.swift; sourceTree = ""; }; + D00B318625729F0500D62056 /* DataItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataItem.swift; sourceTree = ""; }; + D00C73B52302C196004B1E2B /* ChatScheduleController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatScheduleController.swift; sourceTree = ""; }; + D00CA6502281BB0900FFACAD /* Telegram-Sandbox.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = "Telegram-Sandbox.entitlements"; sourceTree = ""; }; + D00CE50C2289C9B7008C1B4F /* MediaAnimatedStickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaAnimatedStickerView.swift; sourceTree = ""; }; + D00CE50E2289CE87008C1B4F /* ChatAnimatedStickerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatAnimatedStickerItem.swift; sourceTree = ""; }; + D00EECD0252B47B1001EB99F /* CallFeedbackController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallFeedbackController.swift; sourceTree = ""; }; + D00EECD2252C703C001EB99F /* CallSettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallSettingsController.swift; sourceTree = ""; }; + D00EECD4252C9192001EB99F /* CameraPreviewRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPreviewRowItem.swift; sourceTree = ""; }; + D00EECD6252CD2EF001EB99F /* MicrophonePreviewRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicrophonePreviewRowItem.swift; sourceTree = ""; }; + D014193B22AE939F008667CB /* ModalOptionSetController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalOptionSetController.swift; sourceTree = ""; }; + D014193D22AE9A90008667CB /* GeneralLineSeparatorRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralLineSeparatorRowItem.swift; sourceTree = ""; }; + D014AA232316CE0700CE5362 /* NewThemeController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewThemeController.swift; sourceTree = ""; }; + D014AA252317D07D00CE5362 /* EditThemeController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditThemeController.swift; sourceTree = ""; }; + D0186730223807D200A77C45 /* ChatListEmptyRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListEmptyRowItem.swift; sourceTree = ""; }; + D01C731622A9814C000DA008 /* InputPasswordController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputPasswordController.swift; sourceTree = ""; }; + D01CBE2C22A5384600F6A971 /* NotificationPreferencesController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPreferencesController.swift; sourceTree = ""; }; + D01E1EFF22B261A800AD6DAE /* Alpha.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Alpha.xcconfig; sourceTree = ""; }; + D01E1F0322B39A4800AD6DAE /* LottiePlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottiePlayer.swift; sourceTree = ""; }; + D0276B7C22BD6511003155D8 /* DisplayLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayLink.swift; sourceTree = ""; }; + D02BD7BF232D0FB800D1814A /* AppAppearanceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAppearanceViewController.swift; sourceTree = ""; }; + D02BD7C2232D14E800D1814A /* ThemePreviewRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemePreviewRowItem.swift; sourceTree = ""; }; + D02BD7C4232D204F00D1814A /* ThemeListRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeListRowItem.swift; sourceTree = ""; }; + D02BD7C6232D4DB200D1814A /* AppearanceThumbs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceThumbs.swift; sourceTree = ""; }; + D02F09FF22E8875800553411 /* SoftwareVideoSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftwareVideoSource.swift; sourceTree = ""; }; + D02F0A0122E88C6E00553411 /* MediaPlayerFramePreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerFramePreview.swift; sourceTree = ""; }; + D02F0A0322E88C9900553411 /* UniversalSoftwareVideoSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalSoftwareVideoSource.swift; sourceTree = ""; }; + D032AFA32578172400E67215 /* PushToTalk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushToTalk.swift; sourceTree = ""; }; + D048EDDD24FFEC5600977606 /* ChannelCommentsControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelCommentsControls.swift; sourceTree = ""; }; + D04D213E230D68B800609388 /* CustomAccentColorModalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAccentColorModalController.swift; sourceTree = ""; }; + D04D2143230DB55B00609388 /* TelegramIconsTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelegramIconsTheme.swift; sourceTree = ""; }; + D0530D3D24E69132003273BC /* CallControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallControl.swift; sourceTree = ""; }; + D0530D3F24E693A5003273BC /* CameraViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraViews.swift; sourceTree = ""; }; + D0530D4124E69459003273BC /* CallTooltipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallTooltipView.swift; sourceTree = ""; }; + D0558D7B215141CF006B403D /* ChatInfoTouchbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoTouchbar.swift; sourceTree = ""; }; + D0558D842152B4D3006B403D /* GalleryTouchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryTouchBar.swift; sourceTree = ""; }; + D0558D862154047E006B403D /* GalleryTouchBarThumbItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryTouchBarThumbItemView.swift; sourceTree = ""; }; + D05F392222FB45450040F341 /* DateSelectorModalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateSelectorModalController.swift; sourceTree = ""; }; + D0675D6A217E20B9004900A7 /* Zip.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Zip.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0675D83217F1F27004900A7 /* ArchiverContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchiverContext.swift; sourceTree = ""; }; + D06C694F253887F600DD9005 /* SlotMachineValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlotMachineValue.swift; sourceTree = ""; }; + D06C69512538894C00DD9005 /* SlotsMediaContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SlotsMediaContentView.swift; sourceTree = ""; }; + D06C69542539F6BD00DD9005 /* LiveLocationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveLocationViewController.swift; sourceTree = ""; }; + D06C695E253DC5DF00DD9005 /* LocationPreviewMapRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPreviewMapRowItem.swift; sourceTree = ""; }; + D06C7BB4247D6F4B00E67C3C /* SoftwareVideoLayerFrameManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftwareVideoLayerFrameManager.swift; sourceTree = ""; }; + D06C7BB6247D6FA900E67C3C /* SampleBufferPool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleBufferPool.swift; sourceTree = ""; }; + D06C7BB8247E664900E67C3C /* SoftwareVideoThumbnailLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftwareVideoThumbnailLayer.swift; sourceTree = ""; }; + D06C7BBA247FAE3E00E67C3C /* GifPlayerBufferView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifPlayerBufferView.swift; sourceTree = ""; }; + D06E389E24A51F5C00C7D03A /* TgVoipWebrtc.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TgVoipWebrtc.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D06E38A024A51FE000C7D03A /* TgVoipWebrtc.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TgVoipWebrtc.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D06E38AA24A5F3B700C7D03A /* MetalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetalKit.framework; path = System/Library/Frameworks/MetalKit.framework; sourceTree = SDKROOT; }; + D06E38AB24A5F3B700C7D03A /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; }; + D070DB8022D3638F008A0BBE /* StickersViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickersViewController.swift; sourceTree = ""; }; + D071E8A321496F38001B6024 /* ChatListTouchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListTouchBar.swift; sourceTree = ""; }; + D071E8A5214A805C001B6024 /* ChatTouchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTouchBar.swift; sourceTree = ""; }; + D071E8A7214BBE21001B6024 /* ChatStickersTouchBarPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatStickersTouchBarPopover.swift; sourceTree = ""; }; + D071E8A9214BC589001B6024 /* TouchBarStickerItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchBarStickerItemView.swift; sourceTree = ""; }; + D071E8AB214BE6E7001B6024 /* TouchBarScrubberHeaderItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchBarScrubberHeaderItemView.swift; sourceTree = ""; }; + D071E8B2214C15C7001B6024 /* TouchBarEmojiPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchBarEmojiPicker.swift; sourceTree = ""; }; + D071E8B4214C1935001B6024 /* TouchBarEmojiItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchBarEmojiItemView.swift; sourceTree = ""; }; + D07450DC233D61B000769D7F /* brilliant_static.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = brilliant_static.tgs; sourceTree = ""; }; + D07450DD233D61B100769D7F /* gift.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = gift.tgs; sourceTree = ""; }; + D07450DE233D61B200769D7F /* brilliant_loading.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = brilliant_loading.tgs; sourceTree = ""; }; + D07450DF233D61B200769D7F /* keyboard_typing.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = keyboard_typing.tgs; sourceTree = ""; }; + D07450E0233D61B300769D7F /* write_words.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = write_words.tgs; sourceTree = ""; }; + D07450E1233D61B400769D7F /* fly_dollar.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = fly_dollar.tgs; sourceTree = ""; }; + D07450E2233D61B500769D7F /* swap_money.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = swap_money.tgs; sourceTree = ""; }; + D07450E3233D61B600769D7F /* chiken_born.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = chiken_born.tgs; sourceTree = ""; }; + D07450E4233D61B700769D7F /* keychain.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = keychain.tgs; sourceTree = ""; }; + D07450E5233D61B800769D7F /* smart_guy.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = smart_guy.tgs; sourceTree = ""; }; + D07450F02340DC8200769D7F /* WalletConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConfiguration.swift; sourceTree = ""; }; + D07450F22340E89000769D7F /* sad_man.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = sad_man.tgs; sourceTree = ""; }; + D07450F42340F28D00769D7F /* wallet_success_created.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = wallet_success_created.tgs; sourceTree = ""; }; + D074A56424A1DE7700E92F8A /* OngoingCallContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OngoingCallContext.swift; sourceTree = ""; }; + D074A56624A1DF5200E92F8A /* VoipDerivedState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoipDerivedState.swift; sourceTree = ""; }; + D076A07D248A5F890077BC0A /* GifPanelTabRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifPanelTabRowItem.swift; sourceTree = ""; }; + D076F86C22959958004F895A /* InlineLoginController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineLoginController.swift; sourceTree = ""; }; + D076F86F2295B24D004F895A /* InlineAuthOptionRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineAuthOptionRowItem.swift; sourceTree = ""; }; + D076F8862296CAB2004F895A /* ChannelDisscussionGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelDisscussionGroup.swift; sourceTree = ""; }; + D076F88C2296FD18004F895A /* AnimtedStickerHeaderItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimtedStickerHeaderItem.swift; sourceTree = ""; }; + D076F88E2297285F004F895A /* DiscussionSetModalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscussionSetModalController.swift; sourceTree = ""; }; + D076F890229823DA004F895A /* ChannelDiscussionInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelDiscussionInputView.swift; sourceTree = ""; }; + D07C6D7C234698C600468B1A /* DynamicHeightRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicHeightRowItem.swift; sourceTree = ""; }; + D07C6D7E2346A42E00468B1A /* monkey_unsee.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = monkey_unsee.tgs; sourceTree = ""; }; + D07C6D7F2346A42F00468B1A /* monkey_see.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = monkey_see.tgs; sourceTree = ""; }; + D0830FC324127468006198E7 /* new_folder.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = new_folder.tgs; sourceTree = ""; }; + D08E5C9722C583CE007B1C09 /* Tuple.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tuple.swift; sourceTree = ""; }; + D0906CFB254BFE1D000E6961 /* ModalPreviews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalPreviews.swift; sourceTree = ""; }; + D093474D242DCFC4000ECA88 /* PeerMediaGroupPeersController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerMediaGroupPeersController.swift; sourceTree = ""; }; + D0983E162568373E00467703 /* AppCenterAnalytics.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AppCenterAnalytics.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0983E182568373E00467703 /* AppCenterCrashes.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AppCenterCrashes.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0983E1A2568375F00467703 /* ffmpeg.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ffmpeg.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0983E1C2568376000467703 /* libopus.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = libopus.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0983E1E2568376000467703 /* libwebp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = libwebp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D098C7151D7E175A007784E4 /* Telegram.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Telegram.app; sourceTree = BUILT_PRODUCTS_DIR; }; D098C7181D7E175A007784E4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; D098C71A1D7E175A007784E4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; D098C71F1D7E175A007784E4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D098C7381D7E1A62007784E4 /* Telegram-Mac.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Telegram-Mac.entitlements"; sourceTree = ""; }; D098C7391D7E1C40007784E4 /* AuthController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthController.swift; sourceTree = ""; }; + D09D836D24A6205500120F73 /* libtgvoip.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = libtgvoip.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D09D9DF0229C27A700378796 /* AnimatedStickerUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedStickerUtils.swift; sourceTree = ""; }; + D09D9DF2229C289F00378796 /* GZip.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GZip.h; sourceTree = ""; }; + D09D9DF3229C289F00378796 /* GZip.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GZip.m; sourceTree = ""; }; + D0A2764D249C9D6B005E3C77 /* PeerPhotos.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerPhotos.swift; sourceTree = ""; }; + D0A2764F249CE588005E3C77 /* GroupsStatsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupsStatsController.swift; sourceTree = ""; }; + D0A51D3524F7EC2100D75641 /* Mozjpeg.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Mozjpeg.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0A5586D2574F75B00B7C182 /* group_call_chatlist_typing.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = group_call_chatlist_typing.json; sourceTree = ""; }; + D0A75F24244843D3001F84A0 /* EditImageCanvasController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditImageCanvasController.swift; sourceTree = ""; }; + D0A75F2624486B6D001F84A0 /* EditImageCanvasColorPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditImageCanvasColorPicker.swift; sourceTree = ""; }; + D0A75F2924487A1E001F84A0 /* EditImageCanvasControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditImageCanvasControls.swift; sourceTree = ""; }; + D0A75F2B2449B67D001F84A0 /* dart_idle.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = dart_idle.tgs; sourceTree = ""; }; + D0ABA3F225499C5A00031678 /* MessageStatsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageStatsController.swift; sourceTree = ""; }; + D0ABA3F62549C37E00031678 /* MessageSharedRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSharedRowItem.swift; sourceTree = ""; }; + D0B6D72922AA65D3008E36FE /* NewContactController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewContactController.swift; sourceTree = ""; }; + D0BDD50023A38660002010A5 /* InactiveChannelsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InactiveChannelsController.swift; sourceTree = ""; }; + D0BE207D24D032E400038A8B /* VolumeControllerPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeControllerPopover.swift; sourceTree = ""; }; + D0BEB98E21628C250055B718 /* EditImageModalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditImageModalController.swift; sourceTree = ""; }; + D0BEB9942166AF270055B718 /* PeerMediaTouchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerMediaTouchBar.swift; sourceTree = ""; }; + D0BEB997216BD8A70055B718 /* EditImageControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditImageControls.swift; sourceTree = ""; }; + D0BEB999216D10920055B718 /* MediaPreviewEditControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPreviewEditControl.swift; sourceTree = ""; }; + D0C39EC4233A6077003CD402 /* GeneralBlockTextRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralBlockTextRowItem.swift; sourceTree = ""; }; + D0C550C3251127DA00B64966 /* ChatCommentsHeaderItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatCommentsHeaderItem.swift; sourceTree = ""; }; + D0CBB0F42492974F00620C65 /* VideoAvatarModalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoAvatarModalController.swift; sourceTree = ""; }; + D0CC4ADD22BA5C930088F36D /* LottieBufferCompressor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieBufferCompressor.swift; sourceTree = ""; }; + D0D087E1243DF4F100E05317 /* ChatlistFilterVisibilityItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatlistFilterVisibilityItem.swift; sourceTree = ""; }; + D0D1391A2521F652005FCF35 /* discussion.tgs */ = {isa = PBXFileReference; lastKnownFileType = file; path = discussion.tgs; sourceTree = ""; }; + D0D139242524A31E005FCF35 /* RepliesHeaderRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepliesHeaderRowItem.swift; sourceTree = ""; }; + D0D3CE6B23D465FA00864F3C /* PollResultStickItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollResultStickItem.swift; sourceTree = ""; }; + D0D71385227ADE9400EC88B1 /* maccheck.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = maccheck.json; sourceTree = ""; }; + D0D7520824C03CD60037D73A /* VideoEditorThumbs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoEditorThumbs.swift; sourceTree = ""; }; + D0D7520A24C04FD60037D73A /* VideoEditorScrubbler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoEditorScrubbler.swift; sourceTree = ""; }; + D0D752C722F8A76100E2CB74 /* Geocoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Geocoding.swift; sourceTree = ""; }; + D0D81AED243E2D6F00CB9D20 /* ChatListFilterFolderIconController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListFilterFolderIconController.swift; sourceTree = ""; }; + D0D81AF7243F69AD00CB9D20 /* PollTimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollTimerView.swift; sourceTree = ""; }; + D0DD91FD246A8A380039D83D /* PeerMediaGifsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerMediaGifsController.swift; sourceTree = ""; }; + D0E3684524D80A0D009896D4 /* ClearCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearCache.swift; sourceTree = ""; }; + D0E3684724D842F7009896D4 /* StorageUsageCleanProgressRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageUsageCleanProgressRowItem.swift; sourceTree = ""; }; + D0E3685724DAB066009896D4 /* VideoCallsConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCallsConfiguration.swift; sourceTree = ""; }; + D0E52B3122FD66C4000C0306 /* MessageTimecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageTimecode.swift; sourceTree = ""; }; + D0E7BD19256A7D350068644D /* GroupCallWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallWindow.swift; sourceTree = ""; }; + D0E7BD6B256AA5570068644D /* TelegramVoip.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TelegramVoip.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0E7BD6F256AA6430068644D /* PresentationGroupCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentationGroupCall.swift; sourceTree = ""; }; + D0E7BD72256AA6B50068644D /* PresentationGroupCallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentationGroupCallManager.swift; sourceTree = ""; }; + D0E7BD78256AE4B00068644D /* GroupCallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallController.swift; sourceTree = ""; }; + D0E7BD7C256BC9460068644D /* GroupCallParticipantRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallParticipantRowItem.swift; sourceTree = ""; }; + D0E7BD7F256C1FAF0068644D /* GroupCallSpeakButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallSpeakButton.swift; sourceTree = ""; }; + D0E7BE6F256D30060068644D /* group_call_speaker_mute.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = group_call_speaker_mute.json; sourceTree = ""; }; + D0E7BE70256D30130068644D /* group_call_speaker_unmute.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = group_call_speaker_unmute.json; sourceTree = ""; }; + D0E7BE79256D56C90068644D /* group_call_member_mute.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = group_call_member_mute.json; sourceTree = ""; }; + D0E7BE7A256D56DA0068644D /* group_call_member_unmute.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = group_call_member_unmute.json; sourceTree = ""; }; + D0E7BECF256EA7140068644D /* GroupCallSettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallSettingsController.swift; sourceTree = ""; }; + D0E82DFF2500F10E00E09A20 /* MergedAvatarsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MergedAvatarsView.swift; sourceTree = ""; }; + D0FCA7612434867400B72F18 /* PeerInfoHeadItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeerInfoHeadItem.swift; sourceTree = ""; }; + D0FFC4AB23E184BA0044D305 /* ChatListFilterController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListFilterController.swift; sourceTree = ""; }; + D0FFCEBD215A7CB700995AFE /* emoji14.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = emoji14.txt; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1159,10 +2245,11 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - C232EAA41E1D07FE00C4D38C /* MtProtoKitMac.framework in Frameworks */, - C232EAA51E1D07FE00C4D38C /* PostboxMac.framework in Frameworks */, - C232EAA61E1D07FE00C4D38C /* SwiftSignalKitMac.framework in Frameworks */, - C232EAA71E1D07FE00C4D38C /* TelegramCoreMac.framework in Frameworks */, + 273BE6CB26136D5200AAFE4E /* Mozjpeg.framework in Frameworks */, + A7D2822D236C51F10000A9BF /* OpenSSLEncryption.framework in Frameworks */, + A7D28229236C50FC0000A9BF /* TelegramCore.framework in Frameworks */, + A7D28227236C50F20000A9BF /* SwiftSignalKit.framework in Frameworks */, + A7D28226236C50EE0000A9BF /* Postbox.framework in Frameworks */, C232EAA81E1D07FE00C4D38C /* TGUIKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1171,429 +2258,1123 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 272C1830267D00350030B5FB /* CoreMediaIO.framework in Frameworks */, + 27D1D62E2611FB0D00684DEA /* rnnoise.framework in Frameworks */, + 278FA43825E8D5CD00280629 /* Stripe.framework in Frameworks */, + 2756EF1D25C463F10062303D /* LegacyReachability.framework in Frameworks */, + 27200F21257CCFDB00574365 /* HotKey.framework in Frameworks */, + D00746EE2577A6BE0000DF74 /* Carbon.framework in Frameworks */, + D0E7BD6C256AA5570068644D /* TelegramVoip.framework in Frameworks */, + C22E06261D7F16C000A11C88 /* TGUIKit.framework in Frameworks */, + D0076FFE25691EDE007EF588 /* FFMpegBinding.framework in Frameworks */, + A7D2822B236C51A50000A9BF /* OpenSSLEncryption.framework in Frameworks */, + D0983E1D2568376000467703 /* libopus.framework in Frameworks */, + D0076E8B25685339007EF588 /* webrtc.framework in Frameworks */, + D0983E1F2568376000467703 /* libwebp.framework in Frameworks */, + D0A51D3624F7EC2100D75641 /* Mozjpeg.framework in Frameworks */, + D0076E372568482E007EF588 /* OpusBinding.framework in Frameworks */, + D0076E9B256855AB007EF588 /* libmac_framework_objc_static.a in Frameworks */, + D0076E98256855A2007EF588 /* libopus.a in Frameworks */, + D0076E9225685596007EF588 /* libwebp.a in Frameworks */, + D0077009256925F9007EF588 /* libswresample.a in Frameworks */, + D007700A256925F9007EF588 /* libavutil.a in Frameworks */, + D007700B256925F9007EF588 /* libavformat.a in Frameworks */, + D007700C256925F9007EF588 /* libavcodec.a in Frameworks */, + D0076EEB25685BB5007EF588 /* libbz2.1.0.tbd in Frameworks */, + D0076EE825685A50007EF588 /* AppCenter.framework in Frameworks */, + D0983E172568373E00467703 /* AppCenterAnalytics.framework in Frameworks */, + D0983E192568373E00467703 /* AppCenterCrashes.framework in Frameworks */, + D09D836E24A6205500120F73 /* libtgvoip.framework in Frameworks */, + D06E38A124A51FE000C7D03A /* TgVoipWebrtc.framework in Frameworks */, + D06E38AC24A5F3B700C7D03A /* MetalKit.framework in Frameworks */, + D06E38AD24A5F3B700C7D03A /* Metal.framework in Frameworks */, + A7919135240D0869002011CA /* MurMurHash32.framework in Frameworks */, + A7918DB424093505002011CA /* GraphUI.framework in Frameworks */, + A7393D392407D0F100CE44CA /* GraphCore.framework in Frameworks */, + A71DC82B23858312000EEDE2 /* CoreSpotlight.framework in Frameworks */, + A7DF1B6C237415AD00ACC01F /* Zip.framework in Frameworks */, + A7ED5DAF236C7CE100040372 /* RLottie.framework in Frameworks */, + A7D28259236C70A50000A9BF /* SSignalKit.framework in Frameworks */, + A7D28232236C57DD0000A9BF /* libphonenumber.framework in Frameworks */, + A7D2820B236C3C1B0000A9BF /* MtProtoKit.framework in Frameworks */, + A7D28209236C3C150000A9BF /* TelegramCore.framework in Frameworks */, + A7D28207236C3C0F0000A9BF /* SwiftSignalKit.framework in Frameworks */, + A7D28205236C3C0B0000A9BF /* Postbox.framework in Frameworks */, + A7D08F48236AC9B6002DC240 /* Sparkle.framework in Frameworks */, + D0DDECEA2322FA0200E1B359 /* Contacts.framework in Frameworks */, + C259ED1E1DB956C1008E6712 /* QuartzCore.framework in Frameworks */, + C2FD33FB1E6C169C008D13D4 /* Security.framework in Frameworks */, + 9F18908F2237E95400665EF5 /* VideoToolbox.framework in Frameworks */, + 9F4EEF8421D4F59C002C3B33 /* Accelerate.framework in Frameworks */, + 9FFAE514205AB50C000C028E /* libz.tbd in Frameworks */, + 9FFAE50F205AB4A3000C028E /* libiconv.tbd in Frameworks */, + 9F0E6F78203ED1380086699C /* AppKit.framework in Frameworks */, + C256A9141FB9CBF10043D497 /* LocalAuthentication.framework in Frameworks */, + C2EBBEA11FB5CA94009AD8ED /* CoreServices.framework in Frameworks */, C2A8E14B1F7D2690000FD5E3 /* CoreVideo.framework in Frameworks */, C2A8E1491F7D268B000FD5E3 /* CoreMedia.framework in Frameworks */, C21A48AE1F7CFBBE0095ADB1 /* OpenGL.framework in Frameworks */, - C276AFC91F74F2D200DEDD8E /* Sparkle.framework in Frameworks */, - C276AFCA1F74F2D200DEDD8E /* HockeySDK.framework in Frameworks */, - C209C3881F262D48009231FE /* libstdc++.tbd in Frameworks */, - C271EB971EB916870034792D /* libtgvoip.framework in Frameworks */, - C2C9B92D1E80166400380D79 /* Carbon.framework in Frameworks */, C2C9B92C1E80165D00380D79 /* IOKit.framework in Frameworks */, - C2FD33FB1E6C169C008D13D4 /* Security.framework in Frameworks */, C2FD33F01E697EC2008D13D4 /* QuickLook.framework in Frameworks */, C24DAB931E0828B6005EE404 /* JavaScriptCore.framework in Frameworks */, - C2C738F11DD898DA00CE9D8A /* AVKit.framework in Frameworks */, - C2FBC1DF1DC61B580063A23B /* MtProtoKitMac.framework in Frameworks */, - C2FBC1D91DC61B050063A23B /* SwiftSignalKitMac.framework in Frameworks */, - C2FBC1D71DC61AFF0063A23B /* TelegramCoreMac.framework in Frameworks */, - C259ED1E1DB956C1008E6712 /* QuartzCore.framework in Frameworks */, + 9F21F65D20B5A9C900332C85 /* MapKit.framework in Frameworks */, C250B0351DB78A0A004E9FBE /* Quartz.framework in Frameworks */, C250B0331DB788B3004E9FBE /* Foundation.framework in Frameworks */, - C234CA911D97E117003023F7 /* PostboxMac.framework in Frameworks */, C253A9671D92E3AF00CDC850 /* AVFoundation.framework in Frameworks */, - C2DF47BC1DE79574003AA6C0 /* libopus.a in Frameworks */, - C22E06261D7F16C000A11C88 /* TGUIKit.framework in Frameworks */, - C253A95C1D9165A400CDC850 /* libwebp.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - C2016F661EAE0A49003AF981 /* calls */ = { + 2728991A26CBCD8F00F4D288 /* items */ = { isa = PBXGroup; children = ( - C2E064611ECCB20800387BB8 /* settings */, - C2016F671EAE0A68003AF981 /* PhoneCallWindowController.swift */, - C271EBEA1EBA3BC90034792D /* PCallSession.swift */, - C2CFCABF1EBB9C8100843F6A /* CallAudioPlayer.swift */, - C22A899E1EBCC2AA005B963D /* CallNavigationHeaderView.swift */, - C2F4ED1A1EC5AE1D005F2696 /* CallRatingModalViewController.swift */, + 2728991B26CBCDA900F4D288 /* TurnOnNotificationsRowItem.swift */, ); - name = calls; + name = items; sourceTree = ""; }; - C20232A61D81D178007C9ADE /* chat */ = { + 2760E55726970666001C59F8 /* views */ = { isa = PBXGroup; children = ( - C21656E61EE576650041A6BA /* popover */, - C24616161ED33F850026D5BC /* instant-pip */, - C2379D281DDCCBD00063AD30 /* bot */, - C250B02C1DB78041004E9FBE /* utils */, - C2B1A10F1D9FD09000ACB1DD /* interface */, - C22B635A1D969D7E00085C19 /* input */, - C219E1E61D8AC84B0042F0C8 /* items */, - C219E1E51D8AC83A0042F0C8 /* views */, - C20232A71D81D189007C9ADE /* ChatController.swift */, - C219E1DA1D8884290042F0C8 /* ChatHistoryEntry.swift */, - C2256D971DAB9D5A00494CF4 /* ChatHistoryViewForLocation.swift */, - C2D187F11E28D0840038961D /* ChatSwitchInlineController.swift */, - C295C65E1F75808600BA309D /* ChatAdditionController.swift */, - C2F952AF1F8E1C840056E586 /* CachedAdminIds.swift */, + 2760E55826970672001C59F8 /* WidgetButton.swift */, + 2760E55A269706D7001C59F8 /* WidgetView.swift */, ); - name = chat; + name = views; sourceTree = ""; }; - C205DB991EE800E9003711DF /* ui-controls */ = { + 27672EC426F8D7A900297DB4 /* notifies */ = { + isa = PBXGroup; + children = ( + 27672E9A26F8D5B000297DB4 /* 0.m4a */, + 27672E9926F8D5B000297DB4 /* 1.m4a */, + 27672EA626F8D5B200297DB4 /* 2.m4a */, + 27672EA226F8D5B100297DB4 /* 3.m4a */, + 27672EAD26F8D5B200297DB4 /* 4.m4a */, + 27672EA526F8D5B200297DB4 /* 5.m4a */, + 27672EAA26F8D5B200297DB4 /* 6.m4a */, + 27672EA026F8D5B100297DB4 /* 7.m4a */, + 27672EA726F8D5B200297DB4 /* 8.m4a */, + 27672E9B26F8D5B100297DB4 /* 9.m4a */, + 27672E9C26F8D5B100297DB4 /* 100.m4a */, + 27672EA326F8D5B100297DB4 /* 101.m4a */, + 27672E9F26F8D5B100297DB4 /* 102.m4a */, + 27672E9826F8D5B000297DB4 /* 103.m4a */, + 27672EA926F8D5B200297DB4 /* 104.m4a */, + 27672E9D26F8D5B100297DB4 /* 105.m4a */, + 27672E9E26F8D5B100297DB4 /* 106.m4a */, + 27672EA826F8D5B200297DB4 /* 107.m4a */, + 27672EA126F8D5B100297DB4 /* 108.m4a */, + 27672EAB26F8D5B200297DB4 /* 109.m4a */, + 27672EA426F8D5B100297DB4 /* 110.m4a */, + 27672EAC26F8D5B200297DB4 /* 111.m4a */, + ); + path = notifies; + sourceTree = ""; + }; + 276B5978261C77C20029FD3F /* views */ = { + isa = PBXGroup; + children = ( + D0E7BD7F256C1FAF0068644D /* GroupCallSpeakButton.swift */, + 276B5979261C780F0029FD3F /* GroupCallMainVideoContainer.swift */, + 276B597C261C78AD0029FD3F /* GroupCallView.swift */, + 276B5982261C79250029FD3F /* GroupCallTitleView.swift */, + 276B5985261C79940029FD3F /* GroupCallControlsView.swift */, + 276B5988261C7AC60029FD3F /* GroupCallSchedule.swift */, + 2719655325EEB89E00FC9DE5 /* GroupCallRecorderRowItem.swift */, + 2729712026135C94006D38E2 /* GroupCallInviteRowItem.swift */, + 27335F0225C8295100FD040C /* GroupVideoView.swift */, + 27AAFCBE2649288E000B1053 /* GroupCallTileView.swift */, + D0E7BD7C256BC9460068644D /* GroupCallParticipantRowItem.swift */, + 276A1D58265D0E600083D6E7 /* GroupCallSpeakingTooltipView.swift */, + 278E86D3265D0F330006685D /* GroupCallAvatarView.swift */, + 275FD30D266A20FC008EBC99 /* GroupCallTileRowItem.swift */, + 2783BA9426E24795004A1591 /* GroupCallVideoOrientationRowItem.swift */, + ); + name = views; + sourceTree = ""; + }; + 2773A53825FA1D8C00AB45E9 /* alerts */ = { isa = PBXGroup; children = ( - C205DB9C1EE88762003711DF /* Views.swift */, + 2773A53925FA1D9D00AB45E9 /* JoinVoiceChatAlertController.swift */, + 2773A53D25FA220B00AB45E9 /* JoinVoiceChatAlertRowItem.swift */, ); - name = "ui-controls"; + name = alerts; sourceTree = ""; }; - C205FEAA1EB74F7A00455808 /* ex-kit */ = { + 2775101F25E790E7003D58D1 /* payments */ = { isa = PBXGroup; children = ( - C224A72C1EB75A3100F43F3F /* ExMajorNavigationController.swift */, + 27E59978261B3F4100228411 /* formatter */, + 2775102025E7917F003D58D1 /* PaymentsCheckoutController.swift */, + 2775102425E79233003D58D1 /* PaymentsCheckoutPreviewRowItem.swift */, + 27F5F23B25E7CCD600E8AC69 /* PaymentsCheckoutPriceItem.swift */, + 27F5F23E25E7DD8300E8AC69 /* PaymentsShippingInfoController.swift */, + 278FA2E525E813F100280629 /* PaymentsShippingMethodController.swift */, + 278FA2E825E8C36A00280629 /* PaymentsPaymentMethodController.swift */, + 279A1E0C25E90D13007D48E7 /* PaymentWebInteractionController.swift */, + 279A1E2725E945A2007D48E7 /* PaymentsReceiptController.swift */, + 271B783D25ECD94B007144D7 /* PamentsSelectMethodController.swift */, + 270202BE261A1BF500CFCE6D /* PaymentsTipsRowItem.swift */, ); - name = "ex-kit"; + name = payments; sourceTree = ""; }; - C209C36E1F262520009231FE /* emoji */ = { + 27949108260A41C60003BFA0 /* ffmpeg */ = { isa = PBXGroup; children = ( - C209C36F1F262537009231FE /* emoji_suggestions_data.cpp */, - C209C3701F262537009231FE /* emoji_suggestions_data.h */, - C209C3711F262537009231FE /* emoji_suggestions.cpp */, - C209C3721F262537009231FE /* emoji_suggestions.h */, - C209C3751F26271B009231FE /* EmojiSuggestionBridge.h */, - C209C3761F26271B009231FE /* EmojiSuggestionBridge.mm */, + 2713B219260A42FE00CE0EC6 /* MediaFrameSource.swift */, + 2713B21C260A42FE00CE0EC6 /* MediaPlaybackData.swift */, + 2713B21A260A42FE00CE0EC6 /* MediaTrackDecodableFrame.swift */, + 2713B21B260A42FE00CE0EC6 /* MediaTrackFrame.swift */, + 2713B21E260A42FE00CE0EC6 /* MediaTrackFrameBuffer.swift */, + 2713B21D260A42FE00CE0EC6 /* MediaTrackFrameDecoder.swift */, + 2794910C260A41EA0003BFA0 /* FFMpegAudioFrameDecoder.swift */, + 2794910A260A41E80003BFA0 /* FFMpegMediaFrameSource.swift */, + 2794910B260A41EA0003BFA0 /* FFMpegMediaFrameSourceContext.swift */, + 2794910D260A41EA0003BFA0 /* FFMpegMediaFrameSourceContextHelpers.swift */, + 2794910E260A41EA0003BFA0 /* FFMpegMediaPassthroughVideoFrameDecoder.swift */, + 2794910F260A41EB0003BFA0 /* FFMpegMediaVideoFrameDecoder.swift */, ); - name = emoji; + name = ffmpeg; sourceTree = ""; }; - C210742A1E780CA8006EE5EF /* cache */ = { + 27A0A4E525F7762F00B789AF /* profile */ = { isa = PBXGroup; children = ( - C210742B1E780CC1006EE5EF /* PhotoCache.swift */, + 27A0A4E225F76F4600B789AF /* GroupCallPeerController.swift */, + 27A0A4E625F7764600B789AF /* GroupCallPeerAvatarRowItem.swift */, + 27A0A4EA25F7814500B789AF /* GroupCallTextAndLabelRowItem.swift */, + 27BF617F26567DEE00331308 /* GroupCallContextMenuHeader.swift */, ); - name = cache; + name = profile; sourceTree = ""; }; - C21656E61EE576650041A6BA /* popover */ = { + 27B15D2D259B28C700C7F280 /* capturer */ = { isa = PBXGroup; children = ( - C21656E71EE576F90041A6BA /* ChatUserPopover.swift */, + 27B15D2E259B2DA700C7F280 /* DesktopCaptureListUI.swift */, + 27B15D31259B373800C7F280 /* DesktopCapturePreviewItem.swift */, + 27F023D325A5C19F008F1C81 /* DesktopCapturerWindow.swift */, ); - name = popover; + name = capturer; sourceTree = ""; }; - C2167E541DC220F900F98E03 /* content */ = { + 27D006DD25AF3AD100EE3EB1 /* invitelinks */ = { isa = PBXGroup; children = ( - C2167E4E1DC220D800F98E03 /* PeerMediaWebpageRowContent.swift */, - C2167E501DC220E900F98E03 /* PeerMediaMusicRowContent.swift */, - C2167E521DC220F600F98E03 /* PeerMediaFileRowContent.swift */, - C2167E561DC221E600F98E03 /* PeerMediaRowContent.swift */, - C2E40A1A1E37ADAF0099AC7D /* PeerMediaEmptyRowItem.swift */, + C2B1B11D1E5F151D00895E0D /* ChannelVisibilityController.swift */, + 27D006DE25AF3B1C00EE3EB1 /* ExportedInvitationRowItem.swift */, + 27DBE4AE25B0424100FCEE2A /* InviteLinksController.swift */, + 27C4089025B068C300372302 /* InviteLinkRowItem.swift */, + 27C4089625B0A1F300372302 /* ClosureInviteLinkController.swift */, + 2744AE6C25B44E1500E8849F /* ExportedInvitationController.swift */, ); - name = content; + name = invitelinks; sourceTree = ""; }; - C2167E551DC2210600F98E03 /* content */ = { + 27E59978261B3F4100228411 /* formatter */ = { isa = PBXGroup; children = ( - C226743E1DC12E4E000BA9ED /* GridMessageItem.swift */, - C22674401DC13165000BA9ED /* GridHoleItem.swift */, + 276C1EBE261B451500FE41C9 /* Currency.swift */, + 27E5997F261B404000228411 /* CurrencyLocale.swift */, + 27E5997C261B3FA800228411 /* String.swift */, + 27E59979261B3F5800228411 /* CurrencyFormatter.swift */, + 270202C5261B3D7C00CFCE6D /* UITextField.swift */, + 270202C2261B3D3100CFCE6D /* CurrencyUITextFieldDelegate.swift */, ); - name = content; + name = formatter; sourceTree = ""; }; - C219E1E51D8AC83A0042F0C8 /* views */ = { + 27FE779826F4941200E1C90B /* themes */ = { isa = PBXGroup; children = ( - C2A506731DF438A700971A93 /* elements */, - C2777B561DCE1174008B69DD /* controls */, - C250B01D1DB67934004E9FBE /* webpages */, - C296AF8D1D8EACD3001DBB59 /* content */, - C20232AB1D81D1AE007C9ADE /* ChatMessageView.swift */, - C219E1D81D886A160042F0C8 /* ChatHoleRowView.swift */, - C219E1E71D8AC90B0042F0C8 /* ChatUnreadRowView.swift */, - C296AF7E1D8D38E5001DBB59 /* ChatRowView.swift */, - C253A92C1D8EE1A600CDC850 /* ChatMediaView.swift */, - C253A9731D94182500CDC850 /* ChatRightView.swift */, - C259ED1B1DB8DC78008E6712 /* ChatNavigateScroller.swift */, - C2777B631DCFB559008B69DD /* ChatServiceRowView.swift */, - C2777B671DD20DD2008B69DD /* ChatTitleBarView.swift */, - C2A71CEC1DDB3C1000C69F73 /* ChatHeaderController.swift */, - C2DF47E81DE892E3003AA6C0 /* ChatToasterView.swift */, - C2E8694C1F43500D00BDD0A2 /* ChatNavigationMention.swift */, + 27FE779926F4944800E1C90B /* ChatThemeSelectorController.swift */, + 27FE779B26F4AE4400E1C90B /* ChatThemeRowItem.swift */, ); - name = views; + name = themes; sourceTree = ""; }; - C219E1E61D8AC84B0042F0C8 /* items */ = { + 27FFCEC32694BA2C006CA024 /* widget-cards */ = { isa = PBXGroup; children = ( - C219E1E31D8AC8370042F0C8 /* ChatUnreadRowItem.swift */, - C20232A91D81D19C007C9ADE /* ChatRowItem.swift */, - C219E1D61D8869F20042F0C8 /* ChatHoleRowItem.swift */, - C219E1F51D8C09130042F0C8 /* ChatMessageItem.swift */, - C219E1FB1D8D35FA0042F0C8 /* ChatMediaItem.swift */, - C253A9621D91A90100CDC850 /* ChatFileMediaItem.swift */, - C2777B611DCFB4C9008B69DD /* ChatServiceItem.swift */, - C230B9231DD4C78E0057F596 /* ChatGIFMediaItem.swift */, - C232234C1DE20E610078D738 /* ChatMessageDateHeader.swift */, - C253E22D1DE33A7C0022A29F /* ChatMusicRowItem.swift */, - C2DF47E31DE84A67003AA6C0 /* ChatVoiceRowItem.swift */, - C22EBCB71DFAB36A0034C435 /* ChatMapRowItem.swift */, - C2A12FE51E0C503900EC2239 /* ChatContactRowItem.swift */, - C20B8F3D1DFC52EE008A354E /* ChatEmptyPeerItem.swift */, - C26546CB1EA0AC3C00E3969A /* ChatVideoMessageItem.swift */, - C2057FA91EBC6A3C000423DC /* ChatCallRowItem.swift */, - C2E0646A1ECF137300387BB8 /* ChatInvoiceItem.swift */, + 2760E55726970666001C59F8 /* views */, + 27FFCEC42694BA58006CA024 /* WidgetAppearance.swift */, + 27FFCEC62694BC95006CA024 /* WidgetController.swift */, + 2760E55526970513001C59F8 /* WidgetStorageController.swift */, + 270A2629269D8F1000B31B2B /* WidgetStickersController.swift */, + 2744018426E6344D0035C55D /* WidgetRecentPeersController.swift */, ); - name = items; + name = "widget-cards"; sourceTree = ""; }; - C221ED521EA6849D00471C65 /* data-and-storage */ = { + 9F0367E622707B6400456348 /* lottie */ = { isa = PBXGroup; children = ( - C221ED531EA684BE00471C65 /* DataAndStorageViewController.swift */, - C221ED5B1EA69AA300471C65 /* StorageUsageController.swift */, - C221ED5D1EA6B36600471C65 /* ChatStorageManagmentModalController.swift */, + D0A5586D2574F75B00B7C182 /* group_call_chatlist_typing.json */, + D0E7BE79256D56C90068644D /* group_call_member_mute.json */, + D0E7BE7A256D56DA0068644D /* group_call_member_unmute.json */, + D0E7BE6F256D30060068644D /* group_call_speaker_mute.json */, + D0E7BE70256D30130068644D /* group_call_speaker_unmute.json */, + D0D71385227ADE9400EC88B1 /* maccheck.json */, + 9F03680C22771A9600456348 /* anim_archive.json */, + 9F03680622771A9600456348 /* anim_delete.json */, + 9F03680B22771A9600456348 /* anim_group.json */, + 9F03680822771A9600456348 /* anim_hide.json */, + 9F03680922771A9600456348 /* anim_mute.json */, + 9F03680E22771A9700456348 /* anim_pin.json */, + 9F03680522771A9600456348 /* anim_read.json */, + 9F03680222771A9600456348 /* anim_unarchive.json */, + 9F03680322771A9600456348 /* anim_ungroup.json */, + 9F03680D22771A9700456348 /* anim_unmute.json */, + 9F03680A22771A9600456348 /* anim_unpin.json */, + 9F03680722771A9600456348 /* anim_unread.json */, + 9F03680422771A9600456348 /* archiveAvatar.json */, ); - name = "data-and-storage"; + path = lottie; sourceTree = ""; }; - C22552491F7BE6BB0007944D /* video-record */ = { + 9F0AE6BA2199BBA400A8B53A /* media-player */ = { isa = PBXGroup; children = ( - C225524A1F7BE7000007944D /* VideoRecorderModalController.swift */, - C225524C1F7BE8E40007944D /* VideoRecorderModalView.swift */, - C225524E1F7C03B50007944D /* VideoRecorderPipeline.swift */, - C22338441F823F8C004AD57C /* VideoCameraStructures.swift */, + 9F0AE6C12199CDB800A8B53A /* player */, + 9F0AE6BB2199BBB900A8B53A /* MediaPlayerView.swift */, + 9F0AE6BD2199BEBE00A8B53A /* SVideoController.swift */, ); - name = "video-record"; + name = "media-player"; sourceTree = ""; }; - C22674361DC0E270000BA9ED /* table */ = { + 9F0AE6C12199CDB800A8B53A /* player */ = { isa = PBXGroup; children = ( - C2271F3D1DB4D4240045E719 /* ETabRowItem.swift */, - C2271F3F1DB4D5850045E719 /* ETabRowView.swift */, + 9F0AE6BF2199CDB500A8B53A /* SVideoView.swift */, ); - name = table; + name = player; sourceTree = ""; }; - C226743B1DC12742000BA9ED /* controllers */ = { + 9F10CE862060FFB7002DD61A /* secure-id */ = { isa = PBXGroup; children = ( - C22674431DC20384000BA9ED /* list */, - C22674421DC2037C000BA9ED /* grid */, + 9F10CE8720610127002DD61A /* PassportController.swift */, + 9F10CE8920611536002DD61A /* PassportHeaderItem.swift */, + 9F10CE8D20617C36002DD61A /* PassportInsertPasswordItem.swift */, + 9F10CE9720626B1B002DD61A /* PassportDocumentRowItem.swift */, + 9FC8AD9F2062D5E70094F7B4 /* PassportAcceptRowItem.swift */, + 9FC8ADA12062E2DF0094F7B4 /* PassportNewPhoneNumberRowItem.swift */, + 9FC8ADA32063B6450094F7B4 /* PassportTwoStepVerificationIntroItem.swift */, + 9F18DD92206D8FFD00A2AAD0 /* SecureIdVerificationDocument.swift */, + 9F18DD91206D8FFD00A2AAD0 /* SecureIdVerificationDocumentsContext.swift */, + 9FC8ADA5206925F60094F7B4 /* PassportWindowController.swift */, + C27BAC7920CFCE68007A7508 /* PassportSettingsHeaderItem.swift */, ); - name = controllers; + name = "secure-id"; sourceTree = ""; }; - C22674421DC2037C000BA9ED /* grid */ = { + 9F13EE152100AFDC00562E53 /* contacts */ = { isa = PBXGroup; children = ( - C2167E551DC2210600F98E03 /* content */, - C22674391DC1273E000BA9ED /* PeerMediaGridController.swift */, + 9F13EE162100B05300562E53 /* VCardContactController.swift */, + 9F13EE1A2100BFCA00562E53 /* VCardHeaderItem.swift */, + 9F1962D72101458C00FFF048 /* VCardLocationRowItem.swift */, ); - name = grid; + name = contacts; sourceTree = ""; }; - C22674431DC20384000BA9ED /* list */ = { + 9F1BABAC21E5EBFE0075C03E /* undo */ = { isa = PBXGroup; children = ( - C2167E541DC220F900F98E03 /* content */, - C22674441DC20664000BA9ED /* PeerMediaListController.swift */, + 9F1BABAD21E5ECE70075C03E /* ChatUndoManager.swift */, + 9F1BABAF21E60DCC0075C03E /* UndoOverlayHeaderView.swift */, ); - name = list; + name = undo; sourceTree = ""; }; - C2271D9A1DACC011001792B6 /* search */ = { + 9F21F65320B5A41B00332C85 /* location */ = { isa = PBXGroup; children = ( - C2271D9B1DACC027001792B6 /* SearchController.swift */, - C2271D9D1DACC796001792B6 /* ChatListMessageRowItem.swift */, - C2271D9F1DACC7F7001792B6 /* ChatListMessageRowView.swift */, + 9F21F65420B5A72800332C85 /* LocationModalController.swift */, + 9F1AE5E320B6D7AA002A9D8D /* LocationPlaceSuggestionRowItem.swift */, + 9F1AE5E520B70328002A9D8D /* LocationSendCurrentItem.swift */, + D0D752C722F8A76100E2CB74 /* Geocoding.swift */, + D06C69542539F6BD00DD9005 /* LiveLocationViewController.swift */, + D06C695E253DC5DF00DD9005 /* LocationPreviewMapRowItem.swift */, ); - name = search; + name = location; sourceTree = ""; }; - C2271DA81DAD867E001792B6 /* PeerInfo */ = { + 9F354E9A227062F3006F1D42 /* haptic */ = { isa = PBXGroup; children = ( - C2AF3B801E5CD77800DFDD81 /* controllers */, - C2271DC21DAE5E32001792B6 /* table */, - C2271DA91DAD8716001792B6 /* PeerInfoController.swift */, - C2271DAB1DAE1116001792B6 /* PeerInfoEntries.swift */, - C2271DAD1DAE1172001792B6 /* UserInfoEntries.swift */, - C2271DB81DAE2125001792B6 /* GroupInfoEntries.swift */, - C2271DBA1DAE213D001792B6 /* ChannelInfoEntries.swift */, - C2B1B1251E5F840B00895E0D /* PeerInfoUtils.swift */, + 9F354E9B2270630A006F1D42 /* HapticEngine.swift */, ); - name = PeerInfo; + name = haptic; sourceTree = ""; }; - C2271DAF1DAE124C001792B6 /* general */ = { + 9F3D5F6122044D3500CB0CAA /* updater */ = { isa = PBXGroup; children = ( - C2271DB01DAE126B001792B6 /* GeneralRowItem.swift */, - C2271DB21DAE127A001792B6 /* GeneralRowView.swift */, - C2271DB41DAE131C001792B6 /* GeneralInteractedRowItem.swift */, - C2271DB61DAE132C001792B6 /* GeneralInteractedRowView.swift */, - C2FD38311DCA215F009DC28C /* GeneralInputRow.swift */, - C2A5067A1DF5BE6900971A93 /* GeneralTextRowItem.swift */, + 9F3D5F6222044D8700CB0CAA /* AppUpdateViewController.swift */, + 9F3D5F8522085B2000CB0CAA /* UpdaterNotifySettings.swift */, ); - name = general; + name = updater; sourceTree = ""; }; - C2271DC21DAE5E32001792B6 /* table */ = { + 9F3EAB3C20A5ED2F003FE7E3 /* ocr */ = { isa = PBXGroup; children = ( - C2271DBE1DAE563D001792B6 /* PeerInfoHeaderItem.swift */, - C2271DC31DAE5E46001792B6 /* PeerInfoHeaderView.swift */, + 9F3EAB3D20A5ED2F003FE7E3 /* genann.h */, + 9F3EAB3E20A5ED2F003FE7E3 /* ocr.mm */, + 9F3EAB4020A5ED2F003FE7E3 /* fast-edge.h */, + 9F3EAB4120A5ED2F003FE7E3 /* ocr.h */, + 9F3EAB4220A5ED2F003FE7E3 /* genann.c */, + 9F3EAB4320A5ED2F003FE7E3 /* fast-edge.cpp */, ); - name = table; + path = ocr; sourceTree = ""; }; - C2271DD01DAF6DD9001792B6 /* settings-controllers */ = { + 9F3EAB4820A5F90B003FE7E3 /* mrz */ = { isa = PBXGroup; children = ( - C275E9ED1F8FC99600D3D8C0 /* change-phone-number */, - C2C5C2031EF8228C00AEA252 /* proxy */, - C221ED521EA6849D00471C65 /* data-and-storage */, - C25C132B1E8A403100AE26A1 /* stickers */, - C24D9FC81E250328002CD3F3 /* privacy */, - C2B0722C1DFEDE290082939D /* elements */, - C275932F1DF6E1D200A0807A /* modal */, - C2271DD11DAF6DF5001792B6 /* EmptyChatViewController.swift */, - C2A71CE21DDB2EBD00C69F73 /* GeneralSettingsViewController.swift */, - C2A71CE61DDB2F8700C69F73 /* UsernameSettingsViewController.swift */, - C2A71CE81DDB342100C69F73 /* NotificationSettingsViewController.swift */, - C24949111E5B704900D7ED5D /* AccountsListViewController.swift */, - C21177FF1F16BB8300AC706D /* BioViewController.swift */, + 9F3EAB4920A5F90B003FE7E3 /* TGPassportMRZ.h */, + 9F3EAB4A20A5F90B003FE7E3 /* TGPassportMRZ.m */, ); - name = "settings-controllers"; + path = mrz; sourceTree = ""; }; - C2271DD51DAF80AC001792B6 /* shared media */ = { + 9F62AE7F202D85CC007FB557 /* fetch */ = { isa = PBXGroup; children = ( - C226743B1DC12742000BA9ED /* controllers */, - C2271DD61DAF80D5001792B6 /* PeerMediaController.swift */, - C21AAE331DB0F6BC007638C5 /* MediaTitleBarView.swift */, - C22674371DC125C1000BA9ED /* PeerMediaCollectionInterfaceState.swift */, + 9F62AE7D202D85B7007FB557 /* FetchManager.swift */, + 9F62AE80202D85E7007FB557 /* FetchManagerLocation.swift */, + 9F62AE82202D8759007FB557 /* FetchMediaUtils.swift */, ); - name = "shared media"; + name = fetch; sourceTree = ""; }; - C2271F521D9D41FC00424F7B /* contacts */ = { + 9F72973E20BD9C580067F815 /* video-player */ = { isa = PBXGroup; children = ( - C2271F531D9D420B00424F7B /* ContactsController.swift */, - C23BC37A1E9B8F5B00D79F92 /* AddContactTableItem.swift */, - C23BC37D1E9BB28F00D79F92 /* AddContactModalController.swift */, + 9F72973F20BD9C6A0067F815 /* VideoPlayer.swift */, ); - name = contacts; + name = "video-player"; sourceTree = ""; }; - C2271F551D9D468900424F7B /* table-elements */ = { + 9F7943AE20854E1900FEDB81 /* proxy */ = { isa = PBXGroup; children = ( - C2271DAF1DAE124C001792B6 /* general */, - C2271F561D9D46BC00424F7B /* ShortPeerRowItem.swift */, - C2271F581D9D46CA00424F7B /* ShortPeerRowView.swift */, - C2B1A0EE1D9D94CE00ACB1DD /* SeparatorRowItem.swift */, - C2B1A0F01D9D94E400ACB1DD /* SeparatorRowView.swift */, - C2271DC01DAE583E001792B6 /* TextAndLabelItem.swift */, - C230B9121DD392EB0057F596 /* SearchRowItem.swift */, - C2FF145F1E532C0A007B7B14 /* SearchEmptyRowItem.swift */, - C2016F3A1EAA4538003AF981 /* RecentPeerRowItem.swift */, - C234D4111EEDE6990017DC25 /* LoadingTableItem.swift */, + 9F7943AF20854E2F00FEDB81 /* ProxyListRowItem.swift */, + 9F7943B120855DC200FEDB81 /* ProxyListController.swift */, + 9F0367F12272108800456348 /* ProxyQRCodeRowItem.swift */, ); - name = "table-elements"; + name = proxy; sourceTree = ""; }; - C22B63531D967F3800085C19 /* external-objc */ = { + 9F7B5FC922003D8C0087D020 /* wallpapers */ = { isa = PBXGroup; children = ( - C2B6488A1EB2533800BA574B /* TGGifConverter.h */, - C2B6488B1EB2533800BA574B /* TGGifConverter.m */, - C22B63541D967F4500085C19 /* TGInputTextTag.h */, - C22B63551D967F4500085C19 /* TGInputTextTag.m */, - C22B63561D967F4500085C19 /* TGModernGrowingTextView.h */, - C22B63571D967F4500085C19 /* TGModernGrowingTextView.m */, - C27A6F6F1ECF604A00C65577 /* TGCurrencyFormatter.h */, - C27A6F701ECF604A00C65577 /* TGCurrencyFormatter.m */, + 9F14CBF22007DEB300F22DA9 /* ChatWallpaperModalController.swift */, + 9F14CBF42007DFD400F22DA9 /* Wallpapers.swift */, + 9F1668B72007E3BC00DD39FB /* ThemeGridControllerItem.swift */, + 9F153D1321F0C7F800B95D82 /* WallpaperPreviewController.swift */, + 9F7B5FCA22003DC70087D020 /* WallpaperColorPicker.swift */, + 9F7B5FCC220099220087D020 /* WallpaperPatternPreviewView.swift */, + 273274E526A818AE00E04289 /* WallpaperColorPickerContainerView.swift */, + 273274E726A8190F00E04289 /* WallpaperAdditionColorView.swift */, + 278680AC26A982DC005FDBB9 /* WallpaperPatternPreviewController.swift */, + 2746926B26A99DD800B67817 /* WallpaperCheckboxView.swift */, ); - name = "external-objc"; + name = wallpapers; sourceTree = ""; }; - C22B635A1D969D7E00085C19 /* input */ = { + 9F7D422822240235007B68BB /* account */ = { isa = PBXGroup; children = ( - C2777B5C1DCE48F4008B69DD /* panels */, - C29B5F3F1DC7858100D13E65 /* context */, - C22B635B1D96A14E00085C19 /* ChatInputView.swift */, - C2303E721D9966BD00098E12 /* ChatInputActionsView.swift */, - C2303E771D997C3100098E12 /* ChatInputAttachView.swift */, - C2B1A1261DA3D84900ACB1DD /* ChatInputAccessory.swift */, - C26A719A1DC9FB3600F69385 /* InputPasteboardParser.swift */, - C25911361DF1A68200671E72 /* ChatInputRecordingView.swift */, + 9F7D422922240403007B68BB /* AccountContext.swift */, + 9F7D422B222415C9007B68BB /* SharedAccountContext.swift */, + 9F7B74022227F492006610E4 /* ManageSharedAccountInfo.swift */, + 9F7B74042227F4F2006610E4 /* SharedAccountInfo.swift */, + 9F7B740E2229618E006610E4 /* SharedWakeupManager.swift */, + 9F7B741022296FD7006610E4 /* SharedNotificationManager.swift */, + 9FF5A1CA2232A2FF00BC1359 /* UpgradedAccount.swift */, + 9F1BC1A8223FDE6D00F21815 /* InputSources.swift */, + 9F0F8E81226DCD1C00A97F6A /* OpmizeDatabaseView.swift */, + A7393D432408F84300CE44CA /* DiceCache.swift */, + 27200F1A257C256E00574365 /* DevicesContext.swift */, ); - name = input; + name = account; sourceTree = ""; }; - C22E062F1D80439800A11C88 /* ui */ = { + 9FA0E52720519DF8001E5649 /* Video Encoder */ = { isa = PBXGroup; children = ( - C22552491F7BE6BB0007944D /* video-record */, - C235A47A1EFBDC6400D463FD /* appearance */, - C205DB991EE800E9003711DF /* ui-controls */, - C2BB12081ED87C3800BDE46A /* parent-controllers */, - C234A7FC1ED724D900EBBECE /* localization */, - C27A8E971EC9D45C00C8E7E7 /* quick-switcher */, - C205FEAA1EB74F7A00455808 /* ex-kit */, - C2B648871EB0C47000BA574B /* pip */, - C2016F661EAE0A49003AF981 /* calls */, - C2B4D0BF1E34E03C00CBC4E6 /* pretty-grid-inline */, - C2EA177D1E2FD4F900887153 /* image */, - C2A315AA1E2E639500D89000 /* calendar */, - C24D9FC21E24FFCD002CD3F3 /* passcode */, - C2AC9C161E1E68490085C7DE /* controls */, - C232EA111E16551000C4D38C /* games */, - C2DF47EA1DE9A17A003AA6C0 /* auth */, - C253E2321DE36B8F0022A29F /* inline-audio-player */, - C230B9201DD4C23E0057F596 /* video */, - C230B8F21DD368A10057F596 /* compose */, - C29B5F381DC668A200D13E65 /* dragging */, - C2FBC1E51DC631620063A23B /* overlay */, - C250B02F1DB7884A004E9FBE /* quick-look */, - C2844AD51DA90775009308DC /* entertainment */, - C2B1A1331DA6580C00ACB1DD /* media */, - C2B1A12A1DA410A000ACB1DD /* models */, - C2271F551D9D468900424F7B /* table-elements */, - C2303E881D9A767E00098E12 /* right */, - C2303E871D9A766900098E12 /* left */, + 9FA0E52820519E33001E5649 /* MP4Atom.h */, + 9FA0E52920519E33001E5649 /* MP4Atom.m */, + 9FA0E52B20519FCC001E5649 /* LiveUploadingHelper.h */, + 9FA0E52C20519FCC001E5649 /* LiveUploadingHelper.m */, ); - name = ui; + path = "Video Encoder"; sourceTree = ""; }; - C22E06301D8043A000A11C88 /* chatlist */ = { + 9FC4DA9421DD0B21003E2A62 /* channel-members */ = { isa = PBXGroup; children = ( - C22E06311D8044CC00A11C88 /* ChatListController.swift */, - C22E063C1D80929400A11C88 /* ChatListRowItem.swift */, - C2CBCAC41D81649E00142EC0 /* ChatListRowView.swift */, - C25FC7FA1D86FEA90041E303 /* ChatListHoleRowItem.swift */, - C25FC7FC1D86FEE00041E303 /* ChatListHoleRowView.swift */, - C2A506761DF5664F00971A93 /* ForwardChatListController.swift */, - C20B8F3F1DFD9999008A354E /* ChatListNothingItem.swift */, - C2AC9C141E1E627F0085C7DE /* TabBadgeItem.swift */, + 9FC4DA9521DD0B35003E2A62 /* PeerChannelMemberCategoriesContextsManager.swift */, + 9FC4DA9721DD0B86003E2A62 /* ChannelMemberCategoryListContext.swift */, + 9FC4DA9921DD0BE6003E2A62 /* CachedChannelAdmins.swift */, + 9FC4DAA721DE0626003E2A62 /* ChannelPermissionsController.swift */, + A731925425DA7AA300218E0E /* GigagroupLanding.swift */, ); - name = chatlist; + name = "channel-members"; sourceTree = ""; }; - C2303E871D9A766900098E12 /* left */ = { + 9FDA713020E64541001ED8ED /* media */ = { isa = PBXGroup; children = ( - C2FB2FA91EBF73AE0093C8BA /* calls */, + C296AF851D8DB178001DBB59 /* MediaUtils.swift */, + 9F72973320B878B00067F815 /* MapResources.swift */, + C2B6488F1EB25B1700BA574B /* MediaResources.swift */, + 9FDA713120E6456A001ED8ED /* ExternalMusicAlbumArtResources.swift */, + 9F4EEF8121D4F584002C3B33 /* ImageTransparency.swift */, + D00746C9257691760000DF74 /* StickerShimmerEffectView.swift */, + ); + name = media; + sourceTree = ""; + }; + 9FDA713720EA9101001ED8ED /* read-articles */ = { + isa = PBXGroup; + children = ( + ); + name = "read-articles"; + sourceTree = ""; + }; + 9FFAE4DB205A8C76000C028E /* modern */ = { + isa = PBXGroup; + children = ( + 27949108260A41C60003BFA0 /* ffmpeg */, + 9FFAE505205A928C000C028E /* RingByteBuffer.swift */, + 9FFAE503205A916B000C028E /* VideoPlayerProxy.swift */, + 9FFAE501205A9153000C028E /* ManagedAudioSession.swift */, + 9FFAE4EA205A8C87000C028E /* MediaPlayer.swift */, + 9FFAE4E2205A8C83000C028E /* MediaPlayerAudioRenderer.swift */, + D02F09FF22E8875800553411 /* SoftwareVideoSource.swift */, + D02F0A0122E88C6E00553411 /* MediaPlayerFramePreview.swift */, + D02F0A0322E88C9900553411 /* UniversalSoftwareVideoSource.swift */, + ); + name = modern; + sourceTree = ""; + }; + A71DC8332386D0C4000EEDE2 /* api-credentials */ = { + isa = PBXGroup; + children = ( + A71DC82E2386AADF000EEDE2 /* Signature.swift */, + C2BB2DAF1F8BDF6700520255 /* Config.swift */, + ); + name = "api-credentials"; + sourceTree = ""; + }; + A72D7AE723C4718C005BAC59 /* polls */ = { + isa = PBXGroup; + children = ( + A72D7AE823C471A7005BAC59 /* PollResultController.swift */, + D0D3CE6B23D465FA00864F3C /* PollResultStickItem.swift */, + ); + name = polls; + sourceTree = ""; + }; + A76C8AB124235F8900FDB071 /* modern */ = { + isa = PBXGroup; + children = ( + A76C8AB2242366EC00FDB071 /* PeerMediaBlockRowItem.swift */, + ); + name = modern; + sourceTree = ""; + }; + A778DC2C23C77ACB00DD307B /* sounds */ = { + isa = PBXGroup; + children = ( + 27E969E5258B417200CB9F64 /* reconnecting.mp3 */, + 27E969DF2589172D00CB9F64 /* call down.mp3 */, + 27A7D37C2600F58A00A67737 /* voip_group_recording_started.mp3 */, + 27A7D37B2600F58900A67737 /* voip_group_unmuted.mp3 */, + 27E969DE2589172B00CB9F64 /* call up.mp3 */, + 27A9904C257A8044009044DB /* Pop.wav */, + 27A9904D257A8046009044DB /* Purr.wav */, + A778DC3223C8988300DD307B /* quiz-correct.mp3 */, + A778DC3123C8988100DD307B /* quiz-incorrect.mp3 */, + A778DC2D23C77AD800DD307B /* confetti.mp3 */, + ); + path = sounds; + sourceTree = ""; + }; + A7831B1A2403CFD00056AEAC /* stats */ = { + isa = PBXGroup; + children = ( + A7831B1B2403CFDD0056AEAC /* ChannelStatsViewController.swift */, + A7831B2924040B6B0056AEAC /* StatisticRowItem.swift */, + A7393D452409044C00CE44CA /* ChannelOverviewStatsRowItem.swift */, + A742CE44241A517800C6B69B /* ChannelRecentPostRowItem.swift */, + A76C8AA324221F5400FDB071 /* StatisticsLoadingRowItem.swift */, + D0A2764F249CE588005E3C77 /* GroupsStatsController.swift */, + D0ABA3F225499C5A00031678 /* MessageStatsController.swift */, + D0ABA3F62549C37E00031678 /* MessageSharedRowItem.swift */, + ); + name = stats; + sourceTree = ""; + }; + A789E08223E05E9A00AEB34A /* filters */ = { + isa = PBXGroup; + children = ( + A789E08323E05EAE00AEB34A /* ChatListFiltersListController.swift */, + D0FFC4AB23E184BA0044D305 /* ChatListFilterController.swift */, + A7919137240D1077002011CA /* ChatListFilterPredicate.swift */, + A7029EF7240E3A5400A89ABD /* ChatListFiltersHeaderItem.swift */, + A742CDCC240FB32F00C6B69B /* ChatListFilterRecommendedItem.swift */, + A76C8A9F2422132400FDB071 /* VerticalTabsView.swift */, + D0D087E1243DF4F100E05317 /* ChatlistFilterVisibilityItem.swift */, + D0D81AED243E2D6F00CB9D20 /* ChatListFilterFolderIconController.swift */, + ); + name = filters; + sourceTree = ""; + }; + A7B5030D23B62DF200C9838E /* svg */ = { + isa = PBXGroup; + children = ( + A7B5031223B62E1A00C9838E /* Svg.h */, + A7B5031123B62E1700C9838E /* Svg.m */, + A7B5030E23B62E0000C9838E /* nanosvg.c */, + A7B5030F23B62E0400C9838E /* nanosvg.h */, + ); + path = svg; + sourceTree = ""; + }; + A7C41DAF2358622F00CF9402 /* modern */ = { + isa = PBXGroup; + children = ( + A7C41DB3235862BB00CF9402 /* PeerMediaPhotosController.swift */, + A7C41DB523586DC100CF9402 /* PeerPhotosMonthItem.swift */, + ); + name = modern; + sourceTree = ""; + }; + A7E83210255E8A6F0095A167 /* hover */ = { + isa = PBXGroup; + children = ( + A7E83211255E8A8E0095A167 /* LinkHoverController.swift */, + ); + name = hover; + sourceTree = ""; + }; + A7F282FE2395166D00742C20 /* settings-search */ = { + isa = PBXGroup; + children = ( + A7F282FF2395168300742C20 /* SettingsSearchRecentQueries.swift */, + A7F283012395185B00742C20 /* SettingsSearchableItems.swift */, + A7F2830523954E1B00742C20 /* CachedFaqInstantPage.swift */, + A7F283092395570400742C20 /* SearchSettingsController.swift */, + A778DC4123CD9F3C00DD307B /* SearchSettingsEmptyItem.swift */, + ); + name = "settings-search"; + sourceTree = ""; + }; + C2016F661EAE0A49003AF981 /* calls */ = { + isa = PBXGroup; + children = ( + D0E7BD18256A7D1C0068644D /* group-calls */, + D00B191A24E54B83006CCB87 /* ui */, + C2E064611ECCB20800387BB8 /* settings */, + C271EBEA1EBA3BC90034792D /* PCallSession.swift */, + C2CFCABF1EBB9C8100843F6A /* CallAudioPlayer.swift */, + C22A899E1EBCC2AA005B963D /* CallNavigationHeaderView.swift */, + C2F4ED1A1EC5AE1D005F2696 /* CallRatingModalViewController.swift */, + D00EECD0252B47B1001EB99F /* CallFeedbackController.swift */, + D074A56424A1DE7700E92F8A /* OngoingCallContext.swift */, + D074A56624A1DF5200E92F8A /* VoipDerivedState.swift */, + D0E3685724DAB066009896D4 /* VideoCallsConfiguration.swift */, + D00EECD2252C703C001EB99F /* CallSettingsController.swift */, + D00EECD4252C9192001EB99F /* CameraPreviewRowItem.swift */, + D00EECD6252CD2EF001EB99F /* MicrophonePreviewRowItem.swift */, + ); + name = calls; + sourceTree = ""; + }; + C20232A61D81D178007C9ADE /* chat */ = { + isa = PBXGroup; + children = ( + 27FE779826F4941200E1C90B /* themes */, + 2773A53825FA1D8C00AB45E9 /* alerts */, + 2775101F25E790E7003D58D1 /* payments */, + A7E83210255E8A6F0095A167 /* hover */, + A72D7AE723C4718C005BAC59 /* polls */, + 9F13EE152100AFDC00562E53 /* contacts */, + C22467591FA8544200F03E27 /* photo-grouping */, + C21656E61EE576650041A6BA /* popover */, + C24616161ED33F850026D5BC /* instant-pip */, + C2379D281DDCCBD00063AD30 /* bot */, + C250B02C1DB78041004E9FBE /* utils */, + C2B1A10F1D9FD09000ACB1DD /* interface */, + C22B635A1D969D7E00085C19 /* input */, + C219E1E61D8AC84B0042F0C8 /* items */, + C219E1E51D8AC83A0042F0C8 /* views */, + C20232A71D81D189007C9ADE /* ChatController.swift */, + 274B831726F0C0A700E6E474 /* EmojiScreenEffect.swift */, + C219E1DA1D8884290042F0C8 /* ChatHistoryEntry.swift */, + C2256D971DAB9D5A00494CF4 /* ChatHistoryViewForLocation.swift */, + C2D187F11E28D0840038961D /* ChatSwitchInlineController.swift */, + C295C65E1F75808600BA309D /* ChatAdditionController.swift */, + 9F153D1521F3662700B95D82 /* StoredMessageFromSearchPeer.swift */, + D0B6D72922AA65D3008E36FE /* NewContactController.swift */, + D00C73B52302C196004B1E2B /* ChatScheduleController.swift */, + D0BDD50023A38660002010A5 /* InactiveChannelsController.swift */, + D0906CFB254BFE1D000E6961 /* ModalPreviews.swift */, + ); + name = chat; + sourceTree = ""; + }; + C205DB991EE800E9003711DF /* ui-controls */ = { + isa = PBXGroup; + children = ( + C205DB9C1EE88762003711DF /* Views.swift */, + 27C4089325B081B500372302 /* FireTimerControl.swift */, + ); + name = "ui-controls"; + sourceTree = ""; + }; + C205FEAA1EB74F7A00455808 /* ex-kit */ = { + isa = PBXGroup; + children = ( + C224A72C1EB75A3100F43F3F /* ExMajorNavigationController.swift */, + ); + name = "ex-kit"; + sourceTree = ""; + }; + C209C36E1F262520009231FE /* emoji */ = { + isa = PBXGroup; + children = ( + C209C36F1F262537009231FE /* emoji_suggestions_data.cpp */, + C209C3701F262537009231FE /* emoji_suggestions_data.h */, + C209C3711F262537009231FE /* emoji_suggestions.cpp */, + C209C3721F262537009231FE /* emoji_suggestions.h */, + C209C3751F26271B009231FE /* EmojiSuggestionBridge.h */, + C209C3761F26271B009231FE /* EmojiSuggestionBridge.mm */, + ); + name = emoji; + sourceTree = ""; + }; + C20CAD131FE436C600EFF8BF /* appearance */ = { + isa = PBXGroup; + children = ( + D02BD7C1232D14CC00D1814A /* items */, + D014AA272317DA0E00CE5362 /* themes */, + 9F7B5FC922003D8C0087D020 /* wallpapers */, + C20CAD141FE436E300EFF8BF /* SelectSizeRowItem.swift */, + 9F0B8F161FFB7F1A00073D3F /* AccentColorRowItem.swift */, + 9F17E5B8212F173900C25A65 /* AutoNightViewController.swift */, + D04D213E230D68B800609388 /* CustomAccentColorModalController.swift */, + D02BD7BF232D0FB800D1814A /* AppAppearanceViewController.swift */, + ); + name = appearance; + sourceTree = ""; + }; + C210742A1E780CA8006EE5EF /* cache */ = { + isa = PBXGroup; + children = ( + C210742B1E780CC1006EE5EF /* PhotoCache.swift */, + ); + name = cache; + sourceTree = ""; + }; + C21656E61EE576650041A6BA /* popover */ = { + isa = PBXGroup; + children = ( + ); + name = popover; + sourceTree = ""; + }; + C2167E541DC220F900F98E03 /* content */ = { + isa = PBXGroup; + children = ( + C2167E4E1DC220D800F98E03 /* PeerMediaWebpageRowContent.swift */, + C2167E501DC220E900F98E03 /* PeerMediaMusicRowContent.swift */, + C2167E521DC220F600F98E03 /* PeerMediaFileRowContent.swift */, + C2167E561DC221E600F98E03 /* PeerMediaRowContent.swift */, + C2E40A1A1E37ADAF0099AC7D /* PeerMediaEmptyRowItem.swift */, + C23EEC881FCC47C1001371CD /* PeerMediaDateItem.swift */, + 9FDA713320E65D49001ED8ED /* PeerMediaPlayerAnimationView.swift */, + 9F127E14210B1F540080D709 /* PeerMediaVoiceRowItem.swift */, + ); + name = content; + sourceTree = ""; + }; + C219E1E51D8AC83A0042F0C8 /* views */ = { + isa = PBXGroup; + children = ( + C2A506731DF438A700971A93 /* elements */, + C2777B561DCE1174008B69DD /* controls */, + C250B01D1DB67934004E9FBE /* webpages */, + C296AF8D1D8EACD3001DBB59 /* content */, + C20232AB1D81D1AE007C9ADE /* ChatMessageView.swift */, + C219E1E71D8AC90B0042F0C8 /* ChatUnreadRowView.swift */, + C296AF7E1D8D38E5001DBB59 /* ChatRowView.swift */, + C253A9731D94182500CDC850 /* ChatRightView.swift */, + C259ED1B1DB8DC78008E6712 /* ChatNavigateScroller.swift */, + C2777B631DCFB559008B69DD /* ChatServiceRowView.swift */, + C2777B671DD20DD2008B69DD /* ChatTitleBarView.swift */, + C2A71CEC1DDB3C1000C69F73 /* ChatHeaderController.swift */, + C2DF47E81DE892E3003AA6C0 /* ChatToasterView.swift */, + C2E8694C1F43500D00BDD0A2 /* ChatNavigationMention.swift */, + A7377E1A23ACC79100AD3ADD /* ChatNavigateFailed.swift */, + A7565EAD23BE02AF0031EADE /* ChatGradientModel.swift */, + D0E82DFF2500F10E00E09A20 /* MergedAvatarsView.swift */, + D048EDDD24FFEC5600977606 /* ChannelCommentsControls.swift */, + 270F020626F0B0830099D2AA /* EmojiAnimationEffectView.swift */, + ); + name = views; + sourceTree = ""; + }; + C219E1E61D8AC84B0042F0C8 /* items */ = { + isa = PBXGroup; + children = ( + C219E1E31D8AC8370042F0C8 /* ChatUnreadRowItem.swift */, + C20232A91D81D19C007C9ADE /* ChatRowItem.swift */, + C219E1D61D8869F20042F0C8 /* ChatHoleRowItem.swift */, + C219E1F51D8C09130042F0C8 /* ChatMessageItem.swift */, + C219E1FB1D8D35FA0042F0C8 /* ChatMediaItem.swift */, + C253A9621D91A90100CDC850 /* ChatFileMediaItem.swift */, + C2777B611DCFB4C9008B69DD /* ChatServiceItem.swift */, + C230B9231DD4C78E0057F596 /* ChatGIFMediaItem.swift */, + C232234C1DE20E610078D738 /* ChatMessageDateHeader.swift */, + C253E22D1DE33A7C0022A29F /* ChatMusicRowItem.swift */, + C2DF47E31DE84A67003AA6C0 /* ChatVoiceRowItem.swift */, + C22EBCB71DFAB36A0034C435 /* ChatMapRowItem.swift */, + C2A12FE51E0C503900EC2239 /* ChatContactRowItem.swift */, + C20B8F3D1DFC52EE008A354E /* ChatEmptyPeerItem.swift */, + C26546CB1EA0AC3C00E3969A /* ChatVideoMessageItem.swift */, + C2057FA91EBC6A3C000423DC /* ChatCallRowItem.swift */, + C2E0646A1ECF137300387BB8 /* ChatInvoiceItem.swift */, + C224675C1FA884E300F03E27 /* ChatGroupedItem.swift */, + 9FDD78D021C8F0CC00F1B4EF /* ChatPollItem.swift */, + D00CE50E2289CE87008C1B4F /* ChatAnimatedStickerItem.swift */, + A7393D342407CAE100CE44CA /* ChatMediaDice.swift */, + D0D81AF7243F69AD00CB9D20 /* PollTimerView.swift */, + D0C550C3251127DA00B64966 /* ChatCommentsHeaderItem.swift */, + D0D139242524A31E005FCF35 /* RepliesHeaderRowItem.swift */, + 2764A5B025E0173900F9A20D /* AutoDeleteContextMenuView.swift */, + ); + name = items; + sourceTree = ""; + }; + C21BE3AD1FD0980200C1C849 /* developer */ = { + isa = PBXGroup; + children = ( + C21BE3AE1FD099AA00C1C849 /* DeveloperViewController.swift */, + C21BE3B01FD14CDB00C1C849 /* ParseAppearanceColors.swift */, + ); + name = developer; + sourceTree = ""; + }; + C221ED521EA6849D00471C65 /* data-and-storage */ = { + isa = PBXGroup; + children = ( + C221ED531EA684BE00471C65 /* DataAndStorageViewController.swift */, + C221ED5B1EA69AA300471C65 /* StorageUsageController.swift */, + C221ED5D1EA6B36600471C65 /* ChatStorageManagmentModalController.swift */, + 9FBE0EE0201FBEFC0060FD1C /* DownloadSettingsViewController.swift */, + 9F3EAB3A20A5A1EC003FE7E3 /* NetworkUsageStatsController.swift */, + D0E3684524D80A0D009896D4 /* ClearCache.swift */, + D0E3684724D842F7009896D4 /* StorageUsageCleanProgressRowItem.swift */, + ); + name = "data-and-storage"; + sourceTree = ""; + }; + C22467591FA8544200F03E27 /* photo-grouping */ = { + isa = PBXGroup; + children = ( + C224675A1FA8546200F03E27 /* GroupedLayout.swift */, + ); + name = "photo-grouping"; + sourceTree = ""; + }; + C22552491F7BE6BB0007944D /* video-record */ = { + isa = PBXGroup; + children = ( + C225524A1F7BE7000007944D /* VideoRecorderModalController.swift */, + C225524C1F7BE8E40007944D /* VideoRecorderModalView.swift */, + C225524E1F7C03B50007944D /* VideoRecorderPipeline.swift */, + C22338441F823F8C004AD57C /* VideoCameraStructures.swift */, + ); + name = "video-record"; + sourceTree = ""; + }; + C22674361DC0E270000BA9ED /* table */ = { + isa = PBXGroup; + children = ( + C2271F3D1DB4D4240045E719 /* ETabRowItem.swift */, + C2271F3F1DB4D5850045E719 /* ETabRowView.swift */, + ); + name = table; + sourceTree = ""; + }; + C226743B1DC12742000BA9ED /* controllers */ = { + isa = PBXGroup; + children = ( + C22674431DC20384000BA9ED /* list */, + C22674421DC2037C000BA9ED /* grid */, + ); + name = controllers; + sourceTree = ""; + }; + C22674421DC2037C000BA9ED /* grid */ = { + isa = PBXGroup; + children = ( + A7C41DAF2358622F00CF9402 /* modern */, + ); + name = grid; + sourceTree = ""; + }; + C22674431DC20384000BA9ED /* list */ = { + isa = PBXGroup; + children = ( + C2167E541DC220F900F98E03 /* content */, + C22674441DC20664000BA9ED /* PeerMediaListController.swift */, + D093474D242DCFC4000ECA88 /* PeerMediaGroupPeersController.swift */, + D0DD91FD246A8A380039D83D /* PeerMediaGifsController.swift */, + ); + name = list; + sourceTree = ""; + }; + C2271D9A1DACC011001792B6 /* search */ = { + isa = PBXGroup; + children = ( + C2271D9B1DACC027001792B6 /* SearchController.swift */, + C2271D9D1DACC796001792B6 /* ChatListMessageRowItem.swift */, + C2271D9F1DACC7F7001792B6 /* ChatListMessageRowView.swift */, + 9FDA713E20EE2D49001ED8ED /* PopularPeersRowItem.swift */, + ); + name = search; + sourceTree = ""; + }; + C2271DA81DAD867E001792B6 /* PeerInfo */ = { + isa = PBXGroup; + children = ( + A76C8AB124235F8900FDB071 /* modern */, + C2AF3B801E5CD77800DFDD81 /* controllers */, + C2271DC21DAE5E32001792B6 /* table */, + C2271DA91DAD8716001792B6 /* PeerInfoController.swift */, + C2271DAB1DAE1116001792B6 /* PeerInfoEntries.swift */, + C2271DAD1DAE1172001792B6 /* UserInfoEntries.swift */, + C2271DB81DAE2125001792B6 /* GroupInfoEntries.swift */, + C2271DBA1DAE213D001792B6 /* ChannelInfoEntries.swift */, + C2B1B1251E5F840B00895E0D /* PeerInfoUtils.swift */, + ); + name = PeerInfo; + sourceTree = ""; + }; + C2271DAF1DAE124C001792B6 /* general */ = { + isa = PBXGroup; + children = ( + C2271DB01DAE126B001792B6 /* GeneralRowItem.swift */, + C2271DB21DAE127A001792B6 /* GeneralRowView.swift */, + C2271DB41DAE131C001792B6 /* GeneralInteractedRowItem.swift */, + C2271DB61DAE132C001792B6 /* GeneralInteractedRowView.swift */, + C2FD38311DCA215F009DC28C /* GeneralInputRow.swift */, + C2A5067A1DF5BE6900971A93 /* GeneralTextRowItem.swift */, + 9F10CE912061BE19002DD61A /* InputDataController.swift */, + 9F10CE932061C8C8002DD61A /* InputDataControllerEntries.swift */, + 9F10CE952061C98E002DD61A /* InputDataRowItem.swift */, + 9F10CE99206284F8002DD61A /* InputDataDateRowItem.swift */, + 9FC8AD992062A5610094F7B4 /* InputDataDataSelectorRowItem.swift */, + 27C4089925B0BD4E00372302 /* NumberSelectorController.swift */, + 9FC8AD9B2062AA630094F7B4 /* ValuesSelectorModalController.swift */, + D014193D22AE9A90008667CB /* GeneralLineSeparatorRowItem.swift */, + D0C39EC4233A6077003CD402 /* GeneralBlockTextRowItem.swift */, + D07C6D7C234698C600468B1A /* DynamicHeightRowItem.swift */, + 27C4088D25B0603700372302 /* GeneralLoadingRowItem.swift */, + D05F392222FB45450040F341 /* DateSelectorModalController.swift */, + 2763A158261C91B000C12762 /* DatePickerRowItem.swift */, + ); + name = general; + sourceTree = ""; + }; + C2271DC21DAE5E32001792B6 /* table */ = { + isa = PBXGroup; + children = ( + C2271DBE1DAE563D001792B6 /* PeerInfoHeaderItem.swift */, + D0FCA7612434867400B72F18 /* PeerInfoHeadItem.swift */, + ); + name = table; + sourceTree = ""; + }; + C2271DD01DAF6DD9001792B6 /* settings-controllers */ = { + isa = PBXGroup; + children = ( + 2728991A26CBCD8F00F4D288 /* items */, + 27FFCEC32694BA2C006CA024 /* widget-cards */, + 9F10CE862060FFB7002DD61A /* secure-id */, + C20CAD131FE436C600EFF8BF /* appearance */, + C21BE3AD1FD0980200C1C849 /* developer */, + C275E9ED1F8FC99600D3D8C0 /* change-phone-number */, + C2C5C2031EF8228C00AEA252 /* proxy */, + C221ED521EA6849D00471C65 /* data-and-storage */, + C25C132B1E8A403100AE26A1 /* stickers */, + C24D9FC81E250328002CD3F3 /* privacy */, + C2B0722C1DFEDE290082939D /* elements */, + C275932F1DF6E1D200A0807A /* modal */, + C2271DD11DAF6DF5001792B6 /* EmptyChatViewController.swift */, + C2A71CE21DDB2EBD00C69F73 /* GeneralSettingsViewController.swift */, + C2A71CE61DDB2F8700C69F73 /* UsernameSettingsViewController.swift */, + D01CBE2C22A5384600F6A971 /* NotificationPreferencesController.swift */, + A767DD4023F2BB3200366F76 /* ShortcutListController.swift */, + ); + name = "settings-controllers"; + sourceTree = ""; + }; + C2271DD51DAF80AC001792B6 /* shared media */ = { + isa = PBXGroup; + children = ( + C226743B1DC12742000BA9ED /* controllers */, + C2271DD61DAF80D5001792B6 /* PeerMediaController.swift */, + C22674371DC125C1000BA9ED /* PeerMediaCollectionInterfaceState.swift */, + ); + name = "shared media"; + sourceTree = ""; + }; + C2271F521D9D41FC00424F7B /* contacts */ = { + isa = PBXGroup; + children = ( + C2271F531D9D420B00424F7B /* ContactsController.swift */, + C23BC37A1E9B8F5B00D79F92 /* AddContactTableItem.swift */, + C23BC37D1E9BB28F00D79F92 /* AddContactModalController.swift */, + ); + name = contacts; + sourceTree = ""; + }; + C2271F551D9D468900424F7B /* table-elements */ = { + isa = PBXGroup; + children = ( + C2271DAF1DAE124C001792B6 /* general */, + C2271F561D9D46BC00424F7B /* ShortPeerRowItem.swift */, + C2271F581D9D46CA00424F7B /* ShortPeerRowView.swift */, + C2B1A0EE1D9D94CE00ACB1DD /* SeparatorRowItem.swift */, + C2B1A0F01D9D94E400ACB1DD /* SeparatorRowView.swift */, + C2271DC01DAE583E001792B6 /* TextAndLabelItem.swift */, + C230B9121DD392EB0057F596 /* SearchRowItem.swift */, + C2FF145F1E532C0A007B7B14 /* SearchEmptyRowItem.swift */, + C2016F3A1EAA4538003AF981 /* RecentPeerRowItem.swift */, + C234D4111EEDE6990017DC25 /* LoadingTableItem.swift */, + A7C1377C23D1A62700803ED3 /* PeerEmptyHolderItem.swift */, + ); + name = "table-elements"; + sourceTree = ""; + }; + C22B63531D967F3800085C19 /* external-objc */ = { + isa = PBXGroup; + children = ( + 9FB14FBD2098896200688EF9 /* EDSunriseSet.h */, + 9FB14FBE209889A500688EF9 /* EDSunriseSet.m */, + C2B6488A1EB2533800BA574B /* TGGifConverter.h */, + C2B6488B1EB2533800BA574B /* TGGifConverter.m */, + C22B63541D967F4500085C19 /* TGInputTextTag.h */, + C22B63551D967F4500085C19 /* TGInputTextTag.m */, + C22B63561D967F4500085C19 /* TGModernGrowingTextView.h */, + C22B63571D967F4500085C19 /* TGModernGrowingTextView.m */, + C27A6F6F1ECF604A00C65577 /* TGCurrencyFormatter.h */, + C27A6F701ECF604A00C65577 /* TGCurrencyFormatter.m */, + ); + name = "external-objc"; + sourceTree = ""; + }; + C22B635A1D969D7E00085C19 /* input */ = { + isa = PBXGroup; + children = ( + 9F21F65320B5A41B00332C85 /* location */, + C2777B5C1DCE48F4008B69DD /* panels */, + C29B5F3F1DC7858100D13E65 /* context */, + C22B635B1D96A14E00085C19 /* ChatInputView.swift */, + C2303E721D9966BD00098E12 /* ChatInputActionsView.swift */, + C2303E771D997C3100098E12 /* ChatInputAttachView.swift */, + 277C34A7267B5BC100B1CAA7 /* ChatInputMenuView.swift */, + C2B1A1261DA3D84900ACB1DD /* ChatInputAccessory.swift */, + C26A719A1DC9FB3600F69385 /* InputPasteboardParser.swift */, + C25911361DF1A68200671E72 /* ChatInputRecordingView.swift */, + D076F890229823DA004F895A /* ChannelDiscussionInputView.swift */, + A7E83213255E992D0095A167 /* WaveView.swift */, + A7E832182562CFC70095A167 /* VoiceBlobView.swift */, + ); + name = input; + sourceTree = ""; + }; + C22E062F1D80439800A11C88 /* ui */ = { + isa = PBXGroup; + children = ( + D0CBB0F32492972A00620C65 /* Video-Avatar */, + 9F354E9A227062F3006F1D42 /* haptic */, + 9F3D5F6122044D3500CB0CAA /* updater */, + 9F0AE6BA2199BBA400A8B53A /* media-player */, + D071E89F21496D77001B6024 /* touchbar */, + 9F72973E20BD9C580067F815 /* video-player */, + C2A7592E1FB1C19E009FCF07 /* alert */, + C22552491F7BE6BB0007944D /* video-record */, + C235A47A1EFBDC6400D463FD /* appearance */, + C205DB991EE800E9003711DF /* ui-controls */, + C2BB12081ED87C3800BDE46A /* parent-controllers */, + C234A7FC1ED724D900EBBECE /* localization */, + C27A8E971EC9D45C00C8E7E7 /* quick-switcher */, + C205FEAA1EB74F7A00455808 /* ex-kit */, + C2B648871EB0C47000BA574B /* pip */, + C2016F661EAE0A49003AF981 /* calls */, + C2B4D0BF1E34E03C00CBC4E6 /* pretty-grid-inline */, + C2EA177D1E2FD4F900887153 /* image */, + C2A315AA1E2E639500D89000 /* calendar */, + C24D9FC21E24FFCD002CD3F3 /* passcode */, + C2AC9C161E1E68490085C7DE /* controls */, + C232EA111E16551000C4D38C /* games */, + C2DF47EA1DE9A17A003AA6C0 /* auth */, + C253E2321DE36B8F0022A29F /* inline-audio-player */, + C230B9201DD4C23E0057F596 /* video */, + C230B8F21DD368A10057F596 /* compose */, + C29B5F381DC668A200D13E65 /* dragging */, + C2FBC1E51DC631620063A23B /* overlay */, + C250B02F1DB7884A004E9FBE /* quick-look */, + C2844AD51DA90775009308DC /* entertainment */, + C2B1A1331DA6580C00ACB1DD /* media */, + C2B1A12A1DA410A000ACB1DD /* models */, + C2271F551D9D468900424F7B /* table-elements */, + C2303E881D9A767E00098E12 /* right */, + C2303E871D9A766900098E12 /* left */, + 9F0368002277091800456348 /* LAnimationButton.swift */, + ); + name = ui; + sourceTree = ""; + }; + C22E06301D8043A000A11C88 /* chatlist */ = { + isa = PBXGroup; + children = ( + A789E08223E05E9A00AEB34A /* filters */, + C22E06311D8044CC00A11C88 /* ChatListController.swift */, + C22E063C1D80929400A11C88 /* ChatListRowItem.swift */, + C2CBCAC41D81649E00142EC0 /* ChatListRowView.swift */, + C25FC7FA1D86FEA90041E303 /* ChatListHoleRowItem.swift */, + C25FC7FC1D86FEE00041E303 /* ChatListHoleRowView.swift */, + C2A506761DF5664F00971A93 /* ForwardChatListController.swift */, + C20B8F3F1DFD9999008A354E /* ChatListNothingItem.swift */, + C2AC9C141E1E627F0085C7DE /* TabBadgeItem.swift */, + D0186730223807D200A77C45 /* ChatListEmptyRowItem.swift */, + 9F0367F62273260A00456348 /* UndoTooltipController.swift */, + A7C1379D23DF21EA00803ED3 /* ChatListRevealItem.swift */, + A73884342580E501002E8424 /* CGChatListIndicator.swift */, + 2725785A259CF987001558E8 /* AnimatedBadgeView.swift */, + ); + name = chatlist; + sourceTree = ""; + }; + C2303E871D9A766900098E12 /* left */ = { + isa = PBXGroup; + children = ( + D001E972243B19C4009025F9 /* folders-sidebar */, + A7F282FE2395166D00742C20 /* settings-search */, + C2FB2FA91EBF73AE0093C8BA /* calls */, C230B8ED1DD335780057F596 /* account */, C2271D9A1DACC011001792B6 /* search */, C2271F521D9D41FC00424F7B /* contacts */, @@ -1607,12 +3388,14 @@ C2303E881D9A767E00098E12 /* right */ = { isa = PBXGroup; children = ( + D0A2764C249C9C8E005E3C77 /* user-photos */, + 9F1BABAC21E5EBFE0075C03E /* undo */, + 9FC4DA9421DD0B21003E2A62 /* channel-members */, C2423A521F2234EA0041907F /* instantpage */, C29B5F4D1DC8F37100D13E65 /* navigation actions */, C2271DD01DAF6DD9001792B6 /* settings-controllers */, C2271DA81DAD867E001792B6 /* PeerInfo */, C20232A61D81D178007C9ADE /* chat */, - C29670781F0FAAC800884DA2 /* AppearanceViewController.swift */, ); name = right; sourceTree = ""; @@ -1622,6 +3405,10 @@ children = ( C230B8EE1DD3358C0057F596 /* AccountViewController.swift */, C230B8F01DD348970057F596 /* AccountInfoItem.swift */, + 9F8DF3C7209228B000AED104 /* EditAccountInfoController.swift */, + 9F12D342209251CF0072928B /* EditAccountInfoItem.swift */, + 9F77B3A5221C1DAC003B65B8 /* LogoutViewController.swift */, + A7F283032395289B00742C20 /* AccountUtils.swift */, ); name = account; sourceTree = ""; @@ -1670,8 +3457,16 @@ C230B9201DD4C23E0057F596 /* video */ = { isa = PBXGroup; children = ( + D06C694E253887DC00DD9005 /* slots */, C230B9211DD4C24E0057F596 /* GIFPlayerView.swift */, C2DF47951DE71160003AA6C0 /* GIFContainerView.swift */, + D01E1F0322B39A4800AD6DAE /* LottiePlayer.swift */, + D0CC4ADD22BA5C930088F36D /* LottieBufferCompressor.swift */, + A7C7215A23FD4BAA00CE3F75 /* LottieLocalAnimations.swift */, + D06C7BB4247D6F4B00E67C3C /* SoftwareVideoLayerFrameManager.swift */, + D06C7BB6247D6FA900E67C3C /* SampleBufferPool.swift */, + D06C7BB8247E664900E67C3C /* SoftwareVideoThumbnailLayer.swift */, + D06C7BBA247FAE3E00E67C3C /* GifPlayerBufferView.swift */, ); name = video; sourceTree = ""; @@ -1752,6 +3547,9 @@ isa = PBXGroup; children = ( C2B9BE881EFC5E7000D6B96F /* Appearance.swift */, + D04D2143230DB55B00609388 /* TelegramIconsTheme.swift */, + A778DC2923C75F1100DD307B /* Confetti.swift */, + A778DC2F23C8985300DD307B /* SoundEffects.swift */, ); name = appearance; sourceTree = ""; @@ -1783,6 +3581,7 @@ C2423A521F2234EA0041907F /* instantpage */ = { isa = PBXGroup; children = ( + 9FDA713720EA9101001ED8ED /* read-articles */, C2423A531F2235080041907F /* InstantPageViewController.swift */, C2DE5D271F3CA5FE0081EC1E /* InstantPageItem.swift */, C2DE5D291F3CA69D0081EC1E /* InstantPageMedia.swift */, @@ -1806,6 +3605,18 @@ C29F4C771F45FBFF00DBFC00 /* InstantPageSlideshowItem.swift */, C29F4C7C1F47283600DBFC00 /* InstantPageBrowser.swift */, C2A87DE71F4C6910002D3F73 /* InstantViewWindow.swift */, + C2905E1B207E4D9E00990AD7 /* InstantPageAudioView.swift */, + C2905E1D207E545600990AD7 /* InstantPageAudioItem.swift */, + 9F21A7CE21C1552D0037784F /* InstantPageTheme.swift */, + 9F21A7D221C167000037784F /* InstantPageImageItem.swift */, + 9F21A7D421C16CB90037784F /* InstantPagePeerReferenceItem.swift */, + 9F21A7DB21C290E00037784F /* InstantPageTableItem.swift */, + 9F1C279221D38A96003CD033 /* InstantPageScrollableItem.swift */, + 9F4EEF7D21D3C3E3002C3B33 /* InstantPageDetailsItem.swift */, + 9F4EEF7F21D3C76E002C3B33 /* InstantPageContentView.swift */, + 9F4EEF8521D4FA68002C3B33 /* InstantPageArticleItem.swift */, + 9F4EEF8721D515C5002C3B33 /* InstantPageStoredState.swift */, + A7F2830723954EF800742C20 /* CachedInstantPages.swift */, ); name = instantpage; sourceTree = ""; @@ -1823,6 +3634,8 @@ children = ( C248BD251E706A05004B9106 /* RecentSessionsController.swift */, C248BD281E706DDA004B9106 /* RecentSessionRowItem.swift */, + 9FA0E53C205693DA001E5649 /* WebSessionsController.swift */, + 9FA0E53E2056E159001E5649 /* WebAuthorizationRowItem.swift */, ); name = sessions; sourceTree = ""; @@ -1831,6 +3644,8 @@ isa = PBXGroup; children = ( C24D9FC31E24FFF3002CD3F3 /* PasscodeLockController.swift */, + 9F147F6E223014EB00D71BD1 /* PasscodeControllers.swift */, + A7E831F72551A85F0095A167 /* ColdStartPasslockController.swift */, ); name = passcode; sourceTree = ""; @@ -1846,11 +3661,12 @@ C24D9FC81E250328002CD3F3 /* privacy */ = { isa = PBXGroup; children = ( + C24D9FC91E25033E002CD3F3 /* PrivacyAndSecurityViewController.swift */, + 9F7943AE20854E1900FEDB81 /* proxy */, C2F9C4461F94FFC0002B2CBF /* two-step-verification */, C2E52A0B1EB8C367009AF87D /* selective-privacy */, C248BD271E706DBC004B9106 /* sessions */, C24D9FC51E250094002CD3F3 /* passcode */, - C24D9FC91E25033E002CD3F3 /* PrivacyAndSecurityViewController.swift */, C248BD231E706104004B9106 /* BlockedPeersViewController.swift */, ); name = privacy; @@ -1859,8 +3675,12 @@ C24D9FDA1E267550002CD3F3 /* preview-sender */ = { isa = PBXGroup; children = ( + D0BEB996216BD5F90055B718 /* editor */, C2FD382D1DCA1FA3009DC28C /* PreviewSenderController.swift */, - C24D9FDB1E267932002CD3F3 /* PreviewSenderItems.swift */, + C23044821F98F8B400977C51 /* MediaPreviewRowItem.swift */, + C246D6271FAB72D4004C17FA /* MediaGroupPreviewRowItem.swift */, + D0BEB999216D10920055B718 /* MediaPreviewEditControl.swift */, + D0675D83217F1F27004900A7 /* ArchiverContext.swift */, ); name = "preview-sender"; sourceTree = ""; @@ -1908,23 +3728,26 @@ C24DAB971E082FDF005EE404 /* objc */ = { isa = PBXGroup; children = ( + 9F291CA02264E57F00C66267 /* BuildConfig.h */, + 9F291C9F2264E57F00C66267 /* BuildConfig.m */, + 9F3EAB4820A5F90B003FE7E3 /* mrz */, + 9F3EAB3C20A5ED2F003FE7E3 /* ocr */, + 9FA0E53A2052EDFF001E5649 /* HackUtils.h */, + 9FA0E5392052EDFE001E5649 /* HackUtils.m */, + C24102661FD587C400DB8625 /* RHARCSupport.h */, C2A8E1451F7D2602000FD5E3 /* opengl */, - C29F4C741F45F58B00DBFC00 /* MIHSliderView.h */, - C29F4C751F45F58B00DBFC00 /* MIHSliderView.m */, C21B24661EDB116B00FC6CDA /* NumberPluralizationForm.h */, C21B24671EDB116B00FC6CDA /* NumberPluralizationForm.m */, C26E82CF1E83EFFE0046DF2F /* TimeObserver.h */, C26E82D01E83EFFE0046DF2F /* TimeObserver.m */, C24DAB961E082EB4005EE404 /* vimeo */, C24DAB951E082971005EE404 /* youtube */, - C25253281DF03F9600ADBC98 /* TGAudioWaveform.h */, - C25253291DF03F9600ADBC98 /* TGAudioWaveform.m */, - C25253251DF03F5700ADBC98 /* TGOpusAudioRecorder.h */, - C25253261DF03F5700ADBC98 /* TGOpusAudioRecorder.m */, C2DF47CC1DE79751003AA6C0 /* ATQueue.h */, C2DF47CD1DE79751003AA6C0 /* ATQueue.m */, C2DF47C91DE79719003AA6C0 /* TGDataItem.h */, C2DF47CA1DE79719003AA6C0 /* TGDataItem.m */, + D09D9DF2229C289F00378796 /* GZip.h */, + D09D9DF3229C289F00378796 /* GZip.m */, ); path = objc; sourceTree = ""; @@ -1966,9 +3789,11 @@ C2203EA11DDE2AB8001E6AB6 /* ChatSelectText.swift */, C29C3E721E4397F300193A7E /* StickerPreviewHandler.swift */, C250BA8E1E6E1CDC0057CD96 /* ChatMessageThrottledProcessingManager.swift */, - C28149871EA7F22200BB933E /* PreparedChatHistoryViewTransition.swift */, C28149891EA7F44300BB933E /* ListViewIntermediateState.swift */, - C2084F031F5D5C6F004713C4 /* ChatReplyPreviewController.swift */, + C241025C1FD5702D00DB8625 /* ChatMessageBubbleImages.swift */, + C251FB4B1FEDCC750035E5D7 /* ChatPresentationUtils.swift */, + A7E831F2255041020095A167 /* IsEqualMessages.swift */, + 27E001B426285AC8008786D3 /* PinchToZoom.swift */, ); name = utils; sourceTree = ""; @@ -1981,16 +3806,31 @@ name = "quick-look"; sourceTree = ""; }; + C251FB511FEE300E0035E5D7 /* Products */ = { + isa = PBXGroup; + name = Products; + sourceTree = ""; + }; C252532B1DF043D800ADBC98 /* resources */ = { isa = PBXGroup; children = ( + 27672EC426F8D7A900297DB4 /* notifies */, + A778DC2C23C77ACB00DD307B /* sounds */, + D07450DB233D617B00769D7F /* tgs */, + 9F0367E622707B6400456348 /* lottie */, + 9F0AE6B42199904400A8B53A /* sound_a.caf */, + 9F3EAB3F20A5ED2F003FE7E3 /* ocr_nn.bin */, + C2786FF21FEBF088001FB044 /* palettes */, C23D0D781F1A609300AF5151 /* SFCompactRounded-Semibold.otf */, + 2740AA3926BD5CEF004B3BCE /* builtin-wallpaper-svg */, C27A6F721ECF610C00C65577 /* currencies.json */, C2CFCAB81EBB4A1600843F6A /* voip */, C23BC3671E9AA03E00D79F92 /* emoji11.txt */, + D0FFCEBD215A7CB700995AFE /* emoji14.txt */, C21074231E77F5DF006EE5EF /* dsa_pub_prod.pem */, C232EA3C1E1C06EF00C4D38C /* dsa_pub.pem */, C252532C1DF0440300ADBC98 /* begin_record.caf */, + 278C720325E67C2700F1B315 /* emoji1016.txt */, C2844ADF1DA90C8A009308DC /* emoji.txt */, C27AAFE51DE9D2EE009B9629 /* PhoneCountries.txt */, C29B5F421DC794C700D13E65 /* sent.caf */, @@ -2000,6 +3840,9 @@ C21074261E77F7A3006EE5EF /* Release.xcconfig */, C21A48B11F7D0D3F0095ADB1 /* VideoMessage.fsh */, C21A48B31F7D0D6B0095ADB1 /* VideoMessage.vsh */, + 9FC8ADA7206A77E00094F7B4 /* countries */, + 9FDE0A8E21AD41C2001546D7 /* emoji1014-1.txt */, + D01E1EFF22B261A800AD6DAE /* Alpha.xcconfig */, ); name = resources; sourceTree = ""; @@ -2007,64 +3850,23 @@ C253A9531D9164CE00CDC850 /* thrid-party */ = { isa = PBXGroup; children = ( + A7B5030D23B62DF200C9838E /* svg */, + 9FA0E52720519DF8001E5649 /* Video Encoder */, C209C36E1F262520009231FE /* emoji */, - C2C9B90B1E800F0C00380D79 /* media-key-tap */, - C2FD33F31E6C1486008D13D4 /* sskeychain */, C24DAB971E082FDF005EE404 /* objc */, - C2DF47991DE79574003AA6C0 /* ogg */, - C2DF479F1DE79574003AA6C0 /* opus */, - C2DF47A81DE79574003AA6C0 /* opusenc */, - C2DF47B31DE79574003AA6C0 /* opusfile */, - C253A9541D9165A400CDC850 /* libwebp */, ); name = "thrid-party"; path = "../thrid-party"; sourceTree = ""; }; - C253A9541D9165A400CDC850 /* libwebp */ = { - isa = PBXGroup; - children = ( - C253A9551D9165A400CDC850 /* include */, - C253A95A1D9165A400CDC850 /* lib */, - ); - path = libwebp; - sourceTree = ""; - }; - C253A9551D9165A400CDC850 /* include */ = { - isa = PBXGroup; - children = ( - C253A9561D9165A400CDC850 /* webp */, - ); - path = include; - sourceTree = ""; - }; - C253A9561D9165A400CDC850 /* webp */ = { - isa = PBXGroup; - children = ( - C253A9571D9165A400CDC850 /* decode.h */, - C253A9581D9165A400CDC850 /* encode.h */, - C253A9591D9165A400CDC850 /* types.h */, - ); - path = webp; - sourceTree = ""; - }; - C253A95A1D9165A400CDC850 /* lib */ = { - isa = PBXGroup; - children = ( - C253A95B1D9165A400CDC850 /* libwebp.a */, - ); - path = lib; - sourceTree = ""; - }; C253E22F1DE34FA20022A29F /* audio */ = { isa = PBXGroup; children = ( + 9FFAE4DB205A8C76000C028E /* modern */, C253E2301DE34FBB0022A29F /* AudioPlayerController.swift */, - C253E2351DE398580022A29F /* AudioPlayer.swift */, - C253E2371DE398980022A29F /* NativeAudioPlayer.swift */, - C2DF47CF1DE82475003AA6C0 /* OpusAudioPlayer.swift */, C28BAB261DF980DE0027CE3A /* AudioRecorder.swift */, C28BAB281DF981320027CE3A /* AudioWaveform.swift */, + 27DCF4BC267C9D680019EC49 /* AudioCommandCenter.swift */, ); name = audio; sourceTree = ""; @@ -2073,6 +3875,8 @@ isa = PBXGroup; children = ( C253E2331DE3776A0022A29F /* InlineAudioPlayerView.swift */, + C2CE43E320E2CFE700656543 /* PlayerListController.swift */, + D0BE207D24D032E400038A8B /* VolumeControllerPopover.swift */, ); name = "inline-audio-player"; sourceTree = ""; @@ -2092,6 +3896,7 @@ isa = PBXGroup; children = ( C25F71471E410DEE0046AF4E /* InAppNotificationSettings.swift */, + 9F4EC947218B459A002B3C56 /* RenderedTotalUnreadCount.swift */, C25F71491E4110C50046AF4E /* ApplicationSpecificPreferencesKeys.swift */, C2FD34141E6C9003008D13D4 /* BaseApplicationSettings.swift */, C221ED551EA6877300471C65 /* GeneratedMediaStoreSettings.swift */, @@ -2100,6 +3905,16 @@ C296707A1F0FBFB500884DA2 /* ThemeSettings.swift */, C24FD40F1F20FBFB00A97196 /* RecentUsedEmoji.swift */, C29E0EDF1F4DC43100C0C7A8 /* InstantViewAppearance.swift */, + C256A9151FB9E1490043D497 /* AdditionalSettings.swift */, + 9FDA713A20EA9532001ED8ED /* ReadArticlesListPreferences.swift */, + 9F17E5BA212F191F00C25A65 /* AutoNightThemePreferences.swift */, + 9FF32C7B21B7DF4800BF58B6 /* StickerSettings.swift */, + 9F77B3972211979B003B65B8 /* AutoplayPreferences.swift */, + 9F1890922238F3DC00665EF5 /* DownloadedFilesPaths.swift */, + D07450F02340DC8200769D7F /* WalletConfiguration.swift */, + A766493E236D6BFD00163DF4 /* PasscodeSettings.swift */, + A7F282B1238D122900742C20 /* UnauthorizedConfiguration.swift */, + A7C1379123DB00D900803ED3 /* ChatListFilterPreferences.swift */, ); name = "inapp-settings"; sourceTree = ""; @@ -2127,12 +3942,13 @@ C271EB981EB9DEDA0034792D /* calls */ = { isa = PBXGroup; children = ( + D074A56924A1EB0D00E92F8A /* webrtc */, C271EBA01EB9F04E0034792D /* TGCallUtils.h */, C271EBA11EB9F04E0034792D /* TGCallUtils.mm */, - C271EB991EB9DEF00034792D /* CallBridge.h */, - C271EB9A1EB9DEF00034792D /* CallBridge.mm */, - C271EBE71EBA22FE0034792D /* TGCallConnectionDescription.h */, - C271EBE81EBA22FE0034792D /* TGCallConnectionDescription.m */, + C271EB991EB9DEF00034792D /* OngoingCallThreadLocalContext.h */, + C271EB9A1EB9DEF00034792D /* OngoingCallThreadLocalContext.mm */, + C271EBE71EBA22FE0034792D /* OngoingCallConnectionDescription.h */, + C271EBE81EBA22FE0034792D /* OngoingCallConnectionDescription.m */, C2AF011B1F03D4C600D8AC1D /* TGCallAesCtr.h */, C2AF011C1F03D4C600D8AC1D /* TGCallAesCtr.m */, ); @@ -2153,6 +3969,7 @@ C275E9EE1F8FCA4200D3D8C0 /* PhoneNumberIntroController.swift */, C275E9F41F8FEDEB00D3D8C0 /* PhoneNumberConfirmController.swift */, C275E9F61F90CDF900D3D8C0 /* PhoneNumberInputCodeController.swift */, + 9F9206EF20727AF30054E581 /* ChangePhoneNumberContainerView.swift */, ); name = "change-phone-number"; sourceTree = ""; @@ -2177,16 +3994,16 @@ isa = PBXGroup; children = ( C29B5F441DC7DA4B00D13E65 /* MessageActionsPanelView.swift */, + C2C415E41FA33D1A00FF36F4 /* InputFormatterPopover.swift */, ); name = panels; sourceTree = ""; }; - C279824C1E72C83500262BFD /* legacy */ = { + C2786FF21FEBF088001FB044 /* palettes */ = { isa = PBXGroup; children = ( - C2FD34121E6C2503008D13D4 /* LegacyImportAuthorization.swift */, ); - name = legacy; + name = palettes; sourceTree = ""; }; C27A8E971EC9D45C00C8E7E7 /* quick-switcher */ = { @@ -2208,6 +4025,7 @@ C2844AD51DA90775009308DC /* entertainment */ = { isa = PBXGroup; children = ( + D070DB7F22D36359008A0BBE /* modern-stickers */, C2F93A2B1F3C550A00BCD48F /* tolerance */, C22674361DC0E270000BA9ED /* table */, C2844ADA1DA907F7009308DC /* gifs */, @@ -2234,11 +4052,6 @@ C2844AD91DA907F2009308DC /* stickers */ = { isa = PBXGroup; children = ( - C2271F391DB4D0540045E719 /* EStickersViewController.swift */, - C22674201DBCECCC000BA9ED /* EStickerGridItem.swift */, - C226741E1DBCEAC2000BA9ED /* EStickerGridEntries.swift */, - C22674321DBF665A000BA9ED /* EStickerPackEntries.swift */, - C22674341DBF6A85000BA9ED /* EStickerPackItem.swift */, ); name = stickers; sourceTree = ""; @@ -2248,6 +4061,7 @@ children = ( C2271F3B1DB4D0630045E719 /* GIFViewController.swift */, C2B6A9641E8519FA00A441B7 /* RecentGIFRowItem.swift */, + D076A07D248A5F890077BC0A /* GifPanelTabRowItem.swift */, ); name = gifs; sourceTree = ""; @@ -2273,6 +4087,8 @@ C22EBCB91DFAB64F0034C435 /* ChatMapContentView.swift */, C248BD201E6F09CC004B9106 /* ChatGameContentView.swift */, C21095991E9FE04700E10BDB /* ChatVideoMessageContentView.swift */, + D00CE50C2289C9B7008C1B4F /* MediaAnimatedStickerView.swift */, + A7393D362407CD7A00CE44CA /* ChatDiceContentView.swift */, ); name = content; sourceTree = ""; @@ -2294,6 +4110,9 @@ C23B3AEA1E338ADC009C162C /* ContextMediaRowItem.swift */, C29C3E6E1E4352C100193A7E /* ContextStickerRowItem.swift */, C24FD40D1F20EE8B00A97196 /* ContextClueRowItem.swift */, + C2E6F3CE1F9F85260023653D /* ContextHashtagRowItem.swift */, + 9F0AE6862191D29D00A8B53A /* ContextSearchMessageItem.swift */, + A7F2831A239A496400742C20 /* ContextShowPeersHolder.swift */, ); name = context; sourceTree = ""; @@ -2331,7 +4150,11 @@ C2A506741DF438B900971A93 /* AudioWaveformView.swift */, C24D9F901E1F8F85002CD3F3 /* MajorBackNavigationBar.swift */, C209C38C1F276D4C009231FE /* ChatSearchView.swift */, - C25BB1681F867FEE0089ED02 /* ChatVideoAccessoryView.swift */, + C25BB1681F867FEE0089ED02 /* ChatMessageAccessoryView.swift */, + C20CAD111FE291E200EFF8BF /* ChatBubbleAccessoryForward.swift */, + 9F580BE420A0AA7B00F6D56C /* ChatRecorderOverlayWindow.swift */, + 27BA045626E4F3E9008FC1A3 /* MessageReadMenuItem.swift */, + 27BA045A26E51F55008FC1A3 /* AvatarContentView.swift */, ); name = elements; sourceTree = ""; @@ -2344,6 +4167,13 @@ name = views; sourceTree = ""; }; + C2A7592E1FB1C19E009FCF07 /* alert */ = { + isa = PBXGroup; + children = ( + ); + name = alert; + sourceTree = ""; + }; C2A8E1451F7D2602000FD5E3 /* opengl */ = { isa = PBXGroup; children = ( @@ -2368,21 +4198,24 @@ C2AF3B801E5CD77800DFDD81 /* controllers */ = { isa = PBXGroup; children = ( + 27D006DD25AF3AD100EE3EB1 /* invitelinks */, + A7831B1A2403CFD00056AEAC /* stats */, + D076F88B2296FCE5004F895A /* discussion */, C29340EF1F506BC80074991E /* groupstickers */, C230BEB81EE9AE5B0029586C /* logs */, - C2AF3B811E5CD79200DFDD81 /* ConvertGroupViewController.swift */, C26A37EB1E5DE464006977AC /* ChannelAdminsViewController.swift */, - C26A37ED1E5DE48F006977AC /* ChannelBlacklistViewController.swift */, - C2B1B11D1E5F151D00895E0D /* ChannelVisibilityController.swift */, + C26A37ED1E5DE48F006977AC /* ChannelBlocklistViewController.swift */, C20CB7281E60886E00C992AC /* LinkInvationController.swift */, C22EE61D1E67506800334C38 /* ChannelMembersViewController.swift */, C2FD33E81E696A86008D13D4 /* GroupsInCommonViewController.swift */, - C2538E511E770B4600B21DF0 /* GroupAdminsController.swift */, C21795E21E795955006A2AA3 /* SecretChatKeyViewController.swift */, C205DB971EE71127003711DF /* ChannelAdminController.swift */, C230BEB31EE97B6F0029586C /* RestictedModalViewController.swift */, C258D1B31F8D385700458478 /* PreHistorySettingsController.swift */, C258D1B51F8D3A0D00458478 /* PreHistoryControllerStructures.swift */, + 9F7D421E22203DB1007B68BB /* ChannelStatisticsController.swift */, + D01C731622A9814C000DA008 /* InputPasswordController.swift */, + A731924A25CAE9DC00218E0E /* AutoremoMessagesController.swift */, ); name = controllers; sourceTree = ""; @@ -2412,6 +4245,8 @@ C2B1A1121D9FD2AE00ACB1DD /* ChatPresentationInterfaceState.swift */, C253E2391DE4D3DB0022A29F /* ChatInterfaceInputContext.swift */, C253E23B1DE4D4080022A29F /* ChatInterfaceStateContextQueries.swift */, + 9FC4DA9B21DD187C003E2A62 /* SearchPeerMembers.swift */, + A7E831EF255040B70095A167 /* ChatPresentationInputQueryResult.swift */, ); name = interface; sourceTree = ""; @@ -2442,11 +4277,12 @@ C2B1A1341DA6581500ACB1DD /* gallery */ = { isa = PBXGroup; children = ( + C2E8BA081FB5F13600DEB5E2 /* thumbs */, C26505911E02FC01001954DC /* items */, C2A1054A1E0163D500B01F48 /* GalleryPageController.swift */, + 9F6B54C721369B4000748FC1 /* GalleryModernControls.swift */, C2B1A1351DA6587100ACB1DD /* GalleryViewer.swift */, - C2412E061DA795D200588C14 /* GalleryControls.swift */, - C2EA53461F751EF300C183F7 /* GalleryMessageEntry.swift */, + A7C7215623FD45D300CE3F75 /* SaveModalController.swift */, ); name = gallery; sourceTree = ""; @@ -2471,6 +4307,7 @@ isa = PBXGroup; children = ( C2BB12091ED87C5A00BDE46A /* ControllerExtension.swift */, + 9F1668C72008F30900DD39FB /* ChatBackgroundView.swift */, ); name = "parent-controllers"; sourceTree = ""; @@ -2478,35 +4315,21 @@ C2C5C2031EF8228C00AEA252 /* proxy */ = { isa = PBXGroup; children = ( - C2C5C2041EF822B900AEA252 /* ProxySettingsViewController.swift */, C2AF01121F01543200D8AC1D /* ExportProxyModalController.swift */, ); name = proxy; sourceTree = ""; }; - C2C9B90B1E800F0C00380D79 /* media-key-tap */ = { - isa = PBXGroup; - children = ( - C2C9B9351E8016D400380D79 /* SPMediaKeyTap.h */, - C2C9B9361E8016D400380D79 /* SPMediaKeyTap.m */, - C2C9B9311E8016C400380D79 /* SPInvocationGrabbing */, - ); - name = "media-key-tap"; - sourceTree = ""; - }; - C2C9B9311E8016C400380D79 /* SPInvocationGrabbing */ = { - isa = PBXGroup; - children = ( - C2C9B9321E8016C400380D79 /* NSObject+SPInvocationGrabbing.h */, - C2C9B9331E8016C400380D79 /* NSObject+SPInvocationGrabbing.m */, - ); - name = SPInvocationGrabbing; - path = objc/SPInvocationGrabbing; - sourceTree = ""; - }; C2CBCABE1D81526A00142EC0 /* utils */ = { isa = PBXGroup; children = ( + 2711D71026CFBF6A00917305 /* MediaObjectToAvatar.swift */, + 27D4F6DE261B776200CCAE03 /* Notices.swift */, + D08E5C9722C583CE007B1C09 /* Tuple.swift */, + 9FFAE507205A92B9000C028E /* RingBuffer.h */, + 9FFAE508205A92B9000C028E /* RingBuffer.m */, + 9F0E6F79203EFE870086699C /* Preferences.swift */, + 9F62AE7F202D85CC007FB557 /* fetch */, C234A7F91ED7104500EBBECE /* localization */, C271EB981EB9DEDA0034792D /* calls */, C210742A1E780CA8006EE5EF /* cache */, @@ -2517,16 +4340,14 @@ C28BAB2B1DF9C2790027CE3A /* DateUtils.mm */, C2DF47D61DE8256F003AA6C0 /* opus */, C22B63531D967F3800085C19 /* external-objc */, - C253A95D1D9165CD00CDC850 /* webp.h */, - C253A95E1D9165CD00CDC850 /* webp.m */, C253A9431D90303200CDC850 /* FastBlur.h */, C253A9441D90303200CDC850 /* FastBlur.m */, C2CBCABF1D81528700142EC0 /* System.swift */, - C296AF851D8DB178001DBB59 /* MediaUtils.swift */, C253A94A1D9032A000CDC850 /* Telegram-Mac-Bridging-Header.h */, C276248A1D95AF7600FE5B2B /* ObjcUtils.h */, C276248B1D95AF7600FE5B2B /* ObjcUtils.m */, C276248D1D95B4F300FE5B2B /* Extensions.swift */, + 27DF650626AADB9E000753AC /* StringFormat.swift */, C2271DC91DAED681001792B6 /* PresenceStrings.swift */, C2271F2A1DB3BEB60045E719 /* GlobalHandlers.swift */, C250B0191DB66B44004E9FBE /* InAppLinks.swift */, @@ -2545,143 +4366,66 @@ C232EABD1E1D13A800C4D38C /* TextUtils.swift */, C201C2311E3B2D1C0026C21E /* FastSettings.swift */, C2F8923C1E3FA51000D98B2D /* PasteboardUtils.swift */, - C2B1B11F1E5F170A00895E0D /* ValidateAddressNameInteractive.swift */, C2FD33ED1E697B31008D13D4 /* TransformOutgoingMessageMedia.swift */, C2C98FEE1E818FB5009CBDB7 /* ClearUserNotifies.swift */, C25C13321E8A433700AE26A1 /* TableUtils.swift */, - C2B6488D1EB25A5300BA574B /* FetchVideoMediaResource.swift */, - C2B6488F1EB25B1700BA574B /* MediaResources.swift */, - C21B24621EDADC8600FC6CDA /* MMMenuItem.swift */, - C2F9C44B1F95FE58002B2CBF /* Markdown.swift */, - ); - name = utils; - sourceTree = ""; - }; - C2CFCAB81EBB4A1600843F6A /* voip */ = { - isa = PBXGroup; - children = ( - C2DDA04F1EC0C19A003531BB /* opening.m4a */, - C2DDA04D1EC0C024003531BB /* opening.mp3 */, - C2CFCABD1EBB4A4D00843F6A /* voip_connecting.mp3 */, - C2CFCAB91EBB4A2200843F6A /* voip_busy.caf */, - C2CFCABA1EBB4A2200843F6A /* voip_end.caf */, - C2CFCABB1EBB4A2200843F6A /* voip_fail.caf */, - C2CFCABC1EBB4A2200843F6A /* voip_ringback.caf */, - ); - name = voip; - sourceTree = ""; - }; - C2D2CAEE1E6486CE00939968 /* stickers */ = { - isa = PBXGroup; - children = ( - C2D2CAEC1E64579700939968 /* StickersPackPreviewModalController.swift */, - C2D2CAEF1E64874600939968 /* StickerPackGridItem.swift */, - ); - name = stickers; - sourceTree = ""; - }; - C2D70AF21F2BFB3700AE768E /* Products */ = { - isa = PBXGroup; - name = Products; - sourceTree = ""; - }; - C2DF47991DE79574003AA6C0 /* ogg */ = { - isa = PBXGroup; - children = ( - C2DF479A1DE79574003AA6C0 /* ogg */, - ); - name = ogg; - path = "thrid-party/ogg"; - sourceTree = SOURCE_ROOT; - }; - C2DF479A1DE79574003AA6C0 /* ogg */ = { - isa = PBXGroup; - children = ( - C2DF479B1DE79574003AA6C0 /* bitwise.c */, - C2DF479C1DE79574003AA6C0 /* framing.c */, - C2DF479D1DE79574003AA6C0 /* ogg.h */, - C2DF479E1DE79574003AA6C0 /* os_types.h */, + C2B6488D1EB25A5300BA574B /* FetchVideoMediaResource.swift */, + C21B24621EDADC8600FC6CDA /* MMMenuItem.swift */, + C2F9C44B1F95FE58002B2CBF /* Markdown.swift */, + C241026E1FD58EA800DB8625 /* SImageView.swift */, + 9F9483B0202AF816006E873D /* CrashHandler.swift */, + 9FA0E5332051A41A001E5649 /* PreUploadManager.swift */, + 9F52F5192130286E006FC0B5 /* LocationRequest.swift */, + 9FC4DA9D21DD1C6C003E2A62 /* ImageCompression.swift */, + 9F147F7B2231543800D71BD1 /* PeerUtils.swift */, + 9FF1DEA5225B699D009512C9 /* SearchUtils.swift */, + 9F0367EF227208E000456348 /* QRCode.swift */, + D09D9DF0229C27A700378796 /* AnimatedStickerUtils.swift */, + D0276B7C22BD6511003155D8 /* DisplayLink.swift */, + D0E52B3122FD66C4000C0306 /* MessageTimecode.swift */, + A7D2822E236C549B0000A9BF /* SyncCoreExtension.swift */, + A7D28236236C5B2C0000A9BF /* PhoneNumberUtils.swift */, + D00B318625729F0500D62056 /* DataItem.swift */, + 27F5F23225E7CACE00E8AC69 /* CurrencyFormat.swift */, + 27C4F5DD26B3F62E008123EC /* VideoMessageConfig.swift */, + 2728991826CBC0D100F4D288 /* UNUserNotifications.swift */, ); - path = ogg; + name = utils; sourceTree = ""; }; - C2DF479F1DE79574003AA6C0 /* opus */ = { - isa = PBXGroup; - children = ( - C2DF47A01DE79574003AA6C0 /* include */, - C2DF47A61DE79574003AA6C0 /* lib */, - ); - name = opus; - path = "thrid-party/opus"; - sourceTree = SOURCE_ROOT; - }; - C2DF47A01DE79574003AA6C0 /* include */ = { + C2CFCAB81EBB4A1600843F6A /* voip */ = { isa = PBXGroup; children = ( - C2DF47A11DE79574003AA6C0 /* opus */, + C2DDA04F1EC0C19A003531BB /* opening.m4a */, + C2DDA04D1EC0C024003531BB /* opening.mp3 */, + C2CFCABD1EBB4A4D00843F6A /* voip_connecting.mp3 */, + C2CFCAB91EBB4A2200843F6A /* voip_busy.caf */, + C2CFCABA1EBB4A2200843F6A /* voip_end.caf */, + C2CFCABB1EBB4A2200843F6A /* voip_fail.caf */, + C2CFCABC1EBB4A2200843F6A /* voip_ringback.caf */, ); - path = include; + name = voip; sourceTree = ""; }; - C2DF47A11DE79574003AA6C0 /* opus */ = { + C2D2CAEE1E6486CE00939968 /* stickers */ = { isa = PBXGroup; children = ( - C2DF47A21DE79574003AA6C0 /* opus.h */, - C2DF47A31DE79574003AA6C0 /* opus_defines.h */, - C2DF47A41DE79574003AA6C0 /* opus_multistream.h */, - C2DF47A51DE79574003AA6C0 /* opus_types.h */, + C2D2CAEC1E64579700939968 /* StickerPackPreviewModalController.swift */, + C2D2CAEF1E64874600939968 /* StickerPackGridItem.swift */, ); - path = opus; + name = stickers; sourceTree = ""; }; - C2DF47A61DE79574003AA6C0 /* lib */ = { + C2D70AF21F2BFB3700AE768E /* Products */ = { isa = PBXGroup; - children = ( - C2DF47A71DE79574003AA6C0 /* libopus.a */, - ); - path = lib; + name = Products; sourceTree = ""; }; - C2DF47A81DE79574003AA6C0 /* opusenc */ = { - isa = PBXGroup; - children = ( - C2DF47A91DE79574003AA6C0 /* diag_range.c */, - C2DF47AA1DE79574003AA6C0 /* diag_range.h */, - C2DF47AB1DE79574003AA6C0 /* opus_header.c */, - C2DF47AC1DE79574003AA6C0 /* opus_header.h */, - C2DF47AD1DE79574003AA6C0 /* opusenc.h */, - C2DF47AE1DE79574003AA6C0 /* opusenc.m */, - C2DF47AF1DE79574003AA6C0 /* picture.c */, - C2DF47B01DE79574003AA6C0 /* picture.h */, - C2DF47B11DE79574003AA6C0 /* wav_io.c */, - C2DF47B21DE79574003AA6C0 /* wav_io.h */, - ); - name = opusenc; - path = "thrid-party/opusenc"; - sourceTree = SOURCE_ROOT; - }; - C2DF47B31DE79574003AA6C0 /* opusfile */ = { - isa = PBXGroup; - children = ( - C2DF47B41DE79574003AA6C0 /* info.c */, - C2DF47B51DE79574003AA6C0 /* internal.c */, - C2DF47B61DE79574003AA6C0 /* internal.h */, - C2DF47B71DE79574003AA6C0 /* opusfile.c */, - C2DF47B81DE79574003AA6C0 /* opusfile.h */, - C2DF47B91DE79574003AA6C0 /* stream.c */, - ); - name = opusfile; - path = "thrid-party/opusfile"; - sourceTree = SOURCE_ROOT; - }; C2DF47D61DE8256F003AA6C0 /* opus */ = { isa = PBXGroup; children = ( C2DF47D81DE82653003AA6C0 /* NSObject+TGLock.h */, C2DF47D91DE82653003AA6C0 /* NSObject+TGLock.m */, - C2DF47D71DE82582003AA6C0 /* OpusAudioBuffer.h */, - C2DF47D11DE824FD003AA6C0 /* OpusObjcBridge.h */, - C2DF47D21DE824FD003AA6C0 /* OpusObjcBridge.mm */, ); name = opus; sourceTree = ""; @@ -2704,6 +4448,7 @@ C27AAFE71DE9DA51009B9629 /* utils */, C2DF47EC1DE9A18E003AA6C0 /* register */, C2DF47EB1DE9A18A003AA6C0 /* login */, + 9F63152521D236CB009FD379 /* ForgotPasswordController.swift */, ); name = auth; sourceTree = ""; @@ -2727,8 +4472,6 @@ C2E064611ECCB20800387BB8 /* settings */ = { isa = PBXGroup; children = ( - C2E064621ECCB24000387BB8 /* NativeCallSettingsViewController.swift */, - C2E064631ECCB24000387BB8 /* NativeCallSettingsViewController.xib */, ); name = settings; sourceTree = ""; @@ -2742,6 +4485,15 @@ name = "selective-privacy"; sourceTree = ""; }; + C2E8BA081FB5F13600DEB5E2 /* thumbs */ = { + isa = PBXGroup; + children = ( + C2E8BA061FB5EF4C00DEB5E2 /* GalleryThumbsControl.swift */, + C2E8BA091FB5F15900DEB5E2 /* GalleryThumbsControlView.swift */, + ); + name = thumbs; + sourceTree = ""; + }; C2EA177D1E2FD4F900887153 /* image */ = { isa = PBXGroup; children = ( @@ -2763,9 +4515,7 @@ isa = PBXGroup; children = ( C2F9C4471F9500B4002B2CBF /* TwoStepVerificationUnlockController.swift */, - C2F9C4491F9500C3002B2CBF /* TwoStepVerificationUnlockStructures.swift */, - C240E9511F96449E00F671FA /* TwoStepVerificationPasswordEntryController.swift */, - C20320FE1F9769BA00143395 /* TwoStepVerificationResetController.swift */, + 9F63152821D26892009FD379 /* CancelResetAccountController.swift */, ); name = "two-step-verification"; sourceTree = ""; @@ -2774,6 +4524,7 @@ isa = PBXGroup; children = ( C2FB2FAA1EBF73CF0093C8BA /* RecentCallsViewController.swift */, + 9FB7CB6C221EB22700888EA9 /* CallSettingsModalController.swift */, ); name = calls; sourceTree = ""; @@ -2781,6 +4532,7 @@ C2FBC1E51DC631620063A23B /* overlay */ = { isa = PBXGroup; children = ( + D076F86E2295B1F6004F895A /* inline-auth */, C2D2CAEE1E6486CE00939968 /* stickers */, C24D9FDA1E267550002CD3F3 /* preview-sender */, C230B8F71DD3714D0057F596 /* popover */, @@ -2788,24 +4540,238 @@ C2A71CD61DD9EEDB00C69F73 /* WebpageModalController.swift */, C29A65061E098B610071BCEF /* ShareModalController.swift */, C232E9F51E1437B100C4D38C /* SearchResultModalController.swift */, - C29C3E701E43881500193A7E /* StickerPreviewModalController.swift */, + C29C3E701E43881500193A7E /* ModalPreviewViews.swift */, C26D8A3B1E464944002FAA3F /* JoinLinkPreviewModalController.swift */, C22EE6181E66ECB200334C38 /* ReportReasonModalController.swift */, C24BA3BC1E9D30F800E8970B /* DeleteSupergroupMessagesModalController.swift */, + 9F72974720C597800067F815 /* TermsModalController.swift */, + C2CE43E820F4F74F00656543 /* UpdateModalController.swift */, + 9F262D5E21BFD5BC006817CD /* LocalizationPreviewModalController.swift */, + 9F6314E021CAA0AB009FD379 /* NewPollController.swift */, + 9F18908C2237B5A400665EF5 /* InputURLFormatterModalController.swift */, + D014193B22AE939F008667CB /* ModalOptionSetController.swift */, + 2764A5A925DFB5B300F9A20D /* ReportDetailsController.swift */, ); name = overlay; sourceTree = ""; }; - C2FD33F31E6C1486008D13D4 /* sskeychain */ = { + D001E972243B19C4009025F9 /* folders-sidebar */ = { + isa = PBXGroup; + children = ( + D001E973243B1A0A009025F9 /* LeftSidebarController.swift */, + D001E975243B385E009025F9 /* FolderIcons.swift */, + D001E977243B4639009025F9 /* LeftSidebarFolderItem.swift */, + ); + name = "folders-sidebar"; + sourceTree = ""; + }; + D00B191A24E54B83006CCB87 /* ui */ = { + isa = PBXGroup; + children = ( + C2016F671EAE0A68003AF981 /* CallWindowController.swift */, + D00B191B24E54BA3006CCB87 /* CallReceptionControl.swift */, + D00B191D24E54F20006CCB87 /* CallStatusView.swift */, + D0530D3D24E69132003273BC /* CallControl.swift */, + D0530D3F24E693A5003273BC /* CameraViews.swift */, + D0530D4124E69459003273BC /* CallTooltipView.swift */, + ); + name = ui; + sourceTree = ""; + }; + D014AA272317DA0E00CE5362 /* themes */ = { + isa = PBXGroup; + children = ( + D004BD2A23153415009A54B1 /* ThemePreviewModalController.swift */, + D014AA232316CE0700CE5362 /* NewThemeController.swift */, + D014AA252317D07D00CE5362 /* EditThemeController.swift */, + ); + name = themes; + sourceTree = ""; + }; + D02BD7C1232D14CC00D1814A /* items */ = { + isa = PBXGroup; + children = ( + D02BD7C2232D14E800D1814A /* ThemePreviewRowItem.swift */, + D02BD7C4232D204F00D1814A /* ThemeListRowItem.swift */, + D02BD7C6232D4DB200D1814A /* AppearanceThumbs.swift */, + ); + name = items; + sourceTree = ""; + }; + D0558D782151411F006B403D /* chat */ = { + isa = PBXGroup; + children = ( + D071E8A5214A805C001B6024 /* ChatTouchBar.swift */, + D071E8A7214BBE21001B6024 /* ChatStickersTouchBarPopover.swift */, + D0558D7B215141CF006B403D /* ChatInfoTouchbar.swift */, + ); + name = chat; + sourceTree = ""; + }; + D0558D792151412C006B403D /* stickers */ = { + isa = PBXGroup; + children = ( + D071E8A9214BC589001B6024 /* TouchBarStickerItemView.swift */, + ); + name = stickers; + sourceTree = ""; + }; + D0558D7A2151413A006B403D /* chat-list */ = { + isa = PBXGroup; + children = ( + D071E8A321496F38001B6024 /* ChatListTouchBar.swift */, + ); + name = "chat-list"; + sourceTree = ""; + }; + D0558D832152B4C8006B403D /* gallery */ = { + isa = PBXGroup; + children = ( + D0558D842152B4D3006B403D /* GalleryTouchBar.swift */, + D0558D862154047E006B403D /* GalleryTouchBarThumbItemView.swift */, + ); + name = gallery; + sourceTree = ""; + }; + D05F392122FB448E0040F341 /* experemental */ = { + isa = PBXGroup; + children = ( + A71DC82C23858356000EEDE2 /* Spotlight.swift */, + A7F28336239A808500742C20 /* TemplateController.swift */, + 27E434132680C57900B05CB1 /* CoreMediaVideoTest.swift */, + 27CF9608268F5B3E0086515A /* SoftwareGradientBackgroundItem.swift */, + ); + name = experemental; + sourceTree = ""; + }; + D06C694E253887DC00DD9005 /* slots */ = { + isa = PBXGroup; + children = ( + D06C694F253887F600DD9005 /* SlotMachineValue.swift */, + D06C69512538894C00DD9005 /* SlotsMediaContentView.swift */, + ); + name = slots; + sourceTree = ""; + }; + D070DB7F22D36359008A0BBE /* modern-stickers */ = { + isa = PBXGroup; + children = ( + D070DB8022D3638F008A0BBE /* StickersViewController.swift */, + D004167422D37AD00000566B /* StickerPackPanelRowItem.swift */, + D004167622D4AD3B0000566B /* StickerPackItems.swift */, + 276A6C0E26C5316F00E4FF34 /* StickerPackTrendingItem.swift */, + ); + name = "modern-stickers"; + sourceTree = ""; + }; + D071E89F21496D77001B6024 /* touchbar */ = { + isa = PBXGroup; + children = ( + D0BEB9932166AF000055B718 /* shared-media */, + D0558D832152B4C8006B403D /* gallery */, + D0558D7A2151413A006B403D /* chat-list */, + D0558D792151412C006B403D /* stickers */, + D0558D782151411F006B403D /* chat */, + D071E8B1214C15AE001B6024 /* Emoji Picker */, + D071E8AB214BE6E7001B6024 /* TouchBarScrubberHeaderItemView.swift */, + ); + name = touchbar; + sourceTree = ""; + }; + D071E8B1214C15AE001B6024 /* Emoji Picker */ = { + isa = PBXGroup; + children = ( + D071E8B2214C15C7001B6024 /* TouchBarEmojiPicker.swift */, + D071E8B4214C1935001B6024 /* TouchBarEmojiItemView.swift */, + ); + name = "Emoji Picker"; + sourceTree = ""; + }; + D07450DB233D617B00769D7F /* tgs */ = { + isa = PBXGroup; + children = ( + 275DDC39267B8702009CF884 /* bot_close_menu.tgs */, + 275DDC38267B8701009CF884 /* bot_menu_close.tgs */, + 274BB56F264013B800620D03 /* cameraoff.tgs */, + 274BB56C264013AC00620D03 /* cameraon.tgs */, + 27B8AFEA26380B4E0044C71B /* screenoff.tgs */, + 27B8AFE926380B4D0044C71B /* screenon.tgs */, + 270E2B4D2609FDBA00B0738C /* playlist_pause_play.tgs */, + 270E2B4A2609F95D00B0738C /* playlist_play_pause.tgs */, + 2763A16A261DF65700C12762 /* voice_chat_set_reminder.tgs */, + 2763A15B261DF05200C12762 /* voice_chat_start_chat_to_mute.tgs */, + 2763A170261DF6A100C12762 /* voice_chat_set_reminder_to_raise_hand.tgs */, + 2763A16D261DF68100C12762 /* voice_chat_set_reminder_to_mute.tgs */, + 2763A167261DF60A00C12762 /* voice_chat_cancel_reminder_to_raise_hand.tgs */, + 2763A161261DF5CD00C12762 /* voice_chat_cancel_reminder.tgs */, + 2763A164261DF5EF00C12762 /* voice_chat_cancel_reminder_to_mute.tgs */, + 2773A56B25FAA63E00AB45E9 /* voice_chat_hand_on_muted.tgs */, + 2773A56A25FAA63D00AB45E9 /* voice_chat_hand_on_unmuted.tgs */, + 2773A56725FA4E4B00AB45E9 /* voice_chat_raise_hand_7.tgs */, + 2773A56425FA4E3A00AB45E9 /* voice_chat_raise_hand_6.tgs */, + 2773A56125FA4E2300AB45E9 /* voice_chat_raise_hand_5.tgs */, + 2773A55E25FA4E0C00AB45E9 /* voice_chat_raise_hand_4.tgs */, + 27478F5B25FBD05F005B8B98 /* voice_chat_raise_hand_3.tgs */, + 2773A55025FA4DAC00AB45E9 /* voice_chat_raise_hand_2.tgs */, + 2773A53425F91E1500AB45E9 /* voice_chat_raise_hand_1.tgs */, + 2773A54325FA324B00AB45E9 /* voice_chat_hand_off.tgs */, + 2773A54225FA324A00AB45E9 /* voice_chat_mute.tgs */, + 2773A54025FA324900AB45E9 /* voice_chat_unmute.tgs */, + 2764A5AC25DFBB4300F9A20D /* police.tgs */, + A731925825DBA60000218E0E /* gigagroup.tgs */, + A731924E25CD5F2200218E0E /* destructor.tgs */, + 2744AE7125B58E6C00E8849F /* invitations.tgs */, + D0D1391A2521F652005FCF35 /* discussion.tgs */, + D0A75F2B2449B67D001F84A0 /* dart_idle.tgs */, + A76C8AA124221D2800FDB071 /* graph_loading.tgs */, + A76C8A9D2420FFE400FDB071 /* folder_empty.tgs */, + D0830FC324127468006198E7 /* new_folder.tgs */, + A7029EF9240E3CCF00A89ABD /* folder.tgs */, + A7393D412407FF0F00CE44CA /* dice_idle.tgs */, + A7C7215823FD473D00CE3F75 /* success_saved.tgs */, + A7B6DDD623ED8FDF00B8E01C /* think_spectacular.tgs */, + D07C6D7F2346A42F00468B1A /* monkey_see.tgs */, + D07C6D7E2346A42E00468B1A /* monkey_unsee.tgs */, + D07450F42340F28D00769D7F /* wallet_success_created.tgs */, + D07450F22340E89000769D7F /* sad_man.tgs */, + D07450DE233D61B200769D7F /* brilliant_loading.tgs */, + D07450DC233D61B000769D7F /* brilliant_static.tgs */, + D07450E3233D61B600769D7F /* chiken_born.tgs */, + D07450E1233D61B400769D7F /* fly_dollar.tgs */, + D07450DD233D61B100769D7F /* gift.tgs */, + D07450E4233D61B700769D7F /* keychain.tgs */, + D07450DF233D61B200769D7F /* keyboard_typing.tgs */, + D07450E5233D61B800769D7F /* smart_guy.tgs */, + D07450E2233D61B500769D7F /* swap_money.tgs */, + D07450E0233D61B300769D7F /* write_words.tgs */, + ); + path = tgs; + sourceTree = ""; + }; + D074A56924A1EB0D00E92F8A /* webrtc */ = { + isa = PBXGroup; + children = ( + ); + name = webrtc; + sourceTree = ""; + }; + D076F86E2295B1F6004F895A /* inline-auth */ = { + isa = PBXGroup; + children = ( + D076F86C22959958004F895A /* InlineLoginController.swift */, + D076F86F2295B24D004F895A /* InlineAuthOptionRowItem.swift */, + ); + name = "inline-auth"; + sourceTree = ""; + }; + D076F88B2296FCE5004F895A /* discussion */ = { isa = PBXGroup; children = ( - C2FD33F41E6C1486008D13D4 /* SSKeychain.h */, - C2FD33F51E6C1486008D13D4 /* SSKeychain.m */, - C2FD33F61E6C1486008D13D4 /* SSKeychainQuery.h */, - C2FD33F71E6C1486008D13D4 /* SSKeychainQuery.m */, + D076F8862296CAB2004F895A /* ChannelDisscussionGroup.swift */, + D076F88C2296FD18004F895A /* AnimtedStickerHeaderItem.swift */, + D076F88E2297285F004F895A /* DiscussionSetModalController.swift */, ); - name = sskeychain; - path = objc/sskeychain; + name = discussion; sourceTree = ""; }; D098C70C1D7E175A007784E4 = { @@ -2830,21 +4796,24 @@ D098C7171D7E175A007784E4 /* Telegram-Mac */ = { isa = PBXGroup; children = ( - C279824C1E72C83500262BFD /* legacy */, + A71DC8332386D0C4000EEDE2 /* api-credentials */, + D05F392122FB448E0040F341 /* experemental */, + 9FDA713020E64541001ED8ED /* media */, C25F71461E410DD50046AF4E /* inapp-settings */, C253E22F1DE34FA20022A29F /* audio */, C253A9531D9164CE00CDC850 /* thrid-party */, C2CBCABE1D81526A00142EC0 /* utils */, C22E062F1D80439800A11C88 /* ui */, D098C7381D7E1A62007784E4 /* Telegram-Mac.entitlements */, + D00CA6502281BB0900FFACAD /* Telegram-Sandbox.entitlements */, D098C7181D7E175A007784E4 /* AppDelegate.swift */, C2F6190C1E844DCD007A051B /* TelegramAccountAuxiliaryMethods.swift */, - C2DEC87E1DECB8C800F6544A /* TelegramApplicationContext.swift */, + 9F7D422822240235007B68BB /* account */, C24949131E5B763F00D7ED5D /* ApplicationContext.swift */, - C2BB2DAF1F8BDF6700520255 /* Config.swift */, D098C71A1D7E175A007784E4 /* Assets.xcassets */, D098C71C1D7E175A007784E4 /* MainMenu.xib */, D098C71F1D7E175A007784E4 /* Info.plist */, + 27E6DB092689C71E003D6164 /* MetalFunctions.metal */, C253A9721D92F9F100CDC850 /* Localizable.strings */, C252532B1DF043D800ADBC98 /* resources */, ); @@ -2854,13 +4823,76 @@ D098C7311D7E18CB007784E4 /* Frameworks */ = { isa = PBXGroup; children = ( + 271999E62683C04B000DE2B7 /* MetalPerformanceShaders.framework */, + 27E4343C2680D38600B05CB1 /* CoreMediaMacCapture.framework */, + 272C182F267D00350030B5FB /* CoreMediaIO.framework */, + 27D1D62D2611FB0D00684DEA /* rnnoise.framework */, + 278FA43725E8D5CD00280629 /* Stripe.framework */, + 2756EF1C25C463F10062303D /* LegacyReachability.framework */, + 273F9C0A257BC486004EC51B /* HotKey.framework */, + D00747002577A7D40000DF74 /* DDHotKey.framework */, + D0E7BD6B256AA5570068644D /* TelegramVoip.framework */, + D0076FFD25691EDE007EF588 /* FFMpegBinding.framework */, + D0076EEA25685BB5007EF588 /* libbz2.1.0.tbd */, + D0076EE725685A50007EF588 /* AppCenter.framework */, + D0076EA825685919007EF588 /* libcrypto.a */, + D0076EA925685919007EF588 /* libssl.a */, + D0076EA025685750007EF588 /* libavcodec.a */, + D0076E9E2568574F007EF588 /* libavformat.a */, + D0076EA125685750007EF588 /* libavutil.a */, + D0076E9F25685750007EF588 /* libswresample.a */, + D0076E9A256855AB007EF588 /* libmac_framework_objc_static.a */, + D0076E97256855A2007EF588 /* libopus.a */, + D0076E9125685596007EF588 /* libwebp.a */, + D0076E8A25685339007EF588 /* webrtc.framework */, + D0076E362568482E007EF588 /* OpusBinding.framework */, + D0983E1A2568375F00467703 /* ffmpeg.framework */, + D0983E1C2568376000467703 /* libopus.framework */, + D0983E1E2568376000467703 /* libwebp.framework */, + D0983E162568373E00467703 /* AppCenterAnalytics.framework */, + D0983E182568373E00467703 /* AppCenterCrashes.framework */, + D0A51D3524F7EC2100D75641 /* Mozjpeg.framework */, + D09D836D24A6205500120F73 /* libtgvoip.framework */, + D06E38AB24A5F3B700C7D03A /* Metal.framework */, + D06E38AA24A5F3B700C7D03A /* MetalKit.framework */, + D06E38A024A51FE000C7D03A /* TgVoipWebrtc.framework */, + D06E389E24A51F5C00C7D03A /* TgVoipWebrtc.framework */, + A7919134240D0869002011CA /* MurMurHash32.framework */, + A7918DB324093505002011CA /* GraphUI.framework */, + A7393D382407D0F100CE44CA /* GraphCore.framework */, + A7377E5B23B5E99500AD3ADD /* libxml2.tbd */, + A71DC82A23858311000EEDE2 /* CoreSpotlight.framework */, + A74EB06F237961A1005F55AE /* AppCenter.framework */, + A74EB070237961A1005F55AE /* AppCenterCrashes.framework */, + A7DF1B6B237415AD00ACC01F /* Zip.framework */, + A7ED5DAE236C7CE100040372 /* RLottie.framework */, + A7D2823B236C69070000A9BF /* SSignalKit.framework */, + A7D28231236C57DD0000A9BF /* libphonenumber.framework */, + A7D2822C236C51F10000A9BF /* OpenSSLEncryption.framework */, + A7D2822A236C51A50000A9BF /* OpenSSLEncryption.framework */, + A7D28210236C3D390000A9BF /* SyncCore.framework */, + A7D2820A236C3C1B0000A9BF /* MtProtoKit.framework */, + A7D28208236C3C150000A9BF /* TelegramCore.framework */, + A7D28206236C3C0F0000A9BF /* SwiftSignalKit.framework */, + A7D28204236C3C0B0000A9BF /* Postbox.framework */, + 9F6FF3212248E298004364FE /* UserNotifications.framework */, + 9F4EEF8321D4F59C002C3B33 /* Accelerate.framework */, + D0675D6A217E20B9004900A7 /* Zip.framework */, + 9F13EE182100B6DA00562E53 /* Contacts.framework */, + 9F21F65C20B5A9C900332C85 /* MapKit.framework */, + 9F0BCD9E2087BABC001D8D8A /* Sparkle.framework */, + 9FFAE513205AB50C000C028E /* libz.tbd */, + 9FFAE510205AB4AB000C028E /* libbz2.tbd */, + 9FFAE50E205AB4A3000C028E /* libiconv.tbd */, + 9F0E6F77203ED1380086699C /* AppKit.framework */, + C251FB501FEE300E0035E5D7 /* Telegram.xcodeproj */, + C256A9131FB9CBF10043D497 /* LocalAuthentication.framework */, + 9F0BCD9C2087BA81001D8D8A /* Sparkle.framework */, + C2EBBEA01FB5CA94009AD8ED /* CoreServices.framework */, C2A8E14A1F7D2690000FD5E3 /* CoreVideo.framework */, C21A48AD1F7CFBBE0095ADB1 /* OpenGL.framework */, C209C37C1F2628D7009231FE /* libc++.tbd */, C209C37D1F2628D7009231FE /* libc++abi.tbd */, - C209C37E1F2628D7009231FE /* libstdc++.6.0.9.tbd */, - C209C37F1F2628D7009231FE /* libstdc++.6.tbd */, - C209C3791F262858009231FE /* libstdc++.tbd */, C271EB961EB916870034792D /* libtgvoip.framework */, C2C9B92A1E8011CD00380D79 /* Carbon.framework */, C2C9B9281E8011B000380D79 /* IOKit.framework */, @@ -2869,25 +4901,89 @@ C2FD33EF1E697EC2008D13D4 /* QuickLook.framework */, C24DAB921E0828B6005EE404 /* JavaScriptCore.framework */, C2DF47D41DE82524003AA6C0 /* libc++.1.tbd */, - C2DF47971DE79540003AA6C0 /* libopus.a */, C2203E9F1DDE040F001E6AB6 /* libcommonCrypto.tbd */, C231992D1EE006330011BEBE /* Sparkle.framework */, - C2A71CD81DDA0FA300C69F73 /* HockeySDK.framework */, C2C738F01DD898DA00CE9D8A /* AVKit.framework */, C2C030CC1DD5097400617711 /* CoreMedia.framework */, - C2FBC1DE1DC61B580063A23B /* MtProtoKitMac.framework */, - C2FBC1D81DC61B050063A23B /* SwiftSignalKitMac.framework */, - C2FBC1D61DC61AFF0063A23B /* TelegramCoreMac.framework */, C259ED1D1DB956C1008E6712 /* QuartzCore.framework */, C250B0341DB78A0A004E9FBE /* Quartz.framework */, C250B0321DB788B3004E9FBE /* Foundation.framework */, - C234CA901D97E117003023F7 /* PostboxMac.framework */, C253A9661D92E3AE00CDC850 /* AVFoundation.framework */, C22E06251D7F16C000A11C88 /* TGUIKit.framework */, ); name = Frameworks; sourceTree = ""; }; + D0A2764C249C9C8E005E3C77 /* user-photos */ = { + isa = PBXGroup; + children = ( + D0A2764D249C9D6B005E3C77 /* PeerPhotos.swift */, + ); + name = "user-photos"; + sourceTree = ""; + }; + D0A75F2824487A05001F84A0 /* canvas */ = { + isa = PBXGroup; + children = ( + D0A75F24244843D3001F84A0 /* EditImageCanvasController.swift */, + D0A75F2624486B6D001F84A0 /* EditImageCanvasColorPicker.swift */, + D0A75F2924487A1E001F84A0 /* EditImageCanvasControls.swift */, + ); + name = canvas; + sourceTree = ""; + }; + D0BEB9932166AF000055B718 /* shared-media */ = { + isa = PBXGroup; + children = ( + D0BEB9942166AF270055B718 /* PeerMediaTouchBar.swift */, + ); + name = "shared-media"; + sourceTree = ""; + }; + D0BEB996216BD5F90055B718 /* editor */ = { + isa = PBXGroup; + children = ( + D0A75F2824487A05001F84A0 /* canvas */, + D0BEB98E21628C250055B718 /* EditImageModalController.swift */, + D0BEB997216BD8A70055B718 /* EditImageControls.swift */, + ); + name = editor; + sourceTree = ""; + }; + D0CBB0F32492972A00620C65 /* Video-Avatar */ = { + isa = PBXGroup; + children = ( + D0CBB0F42492974F00620C65 /* VideoAvatarModalController.swift */, + D0D7520824C03CD60037D73A /* VideoEditorThumbs.swift */, + D0D7520A24C04FD60037D73A /* VideoEditorScrubbler.swift */, + ); + name = "Video-Avatar"; + sourceTree = ""; + }; + D0E7BD18256A7D1C0068644D /* group-calls */ = { + isa = PBXGroup; + children = ( + 276B5978261C77C20029FD3F /* views */, + 27A0A4E525F7762F00B789AF /* profile */, + 27B15D2D259B28C700C7F280 /* capturer */, + D0E7BD19256A7D350068644D /* GroupCallWindow.swift */, + D0E7BD6F256AA6430068644D /* PresentationGroupCall.swift */, + D0E7BD72256AA6B50068644D /* PresentationGroupCallManager.swift */, + D0E7BD78256AE4B00068644D /* GroupCallController.swift */, + D0E7BECF256EA7140068644D /* GroupCallSettingsController.swift */, + D00747032577AF0B0000DF74 /* PushToTalkRowItem.swift */, + D032AFA32578172400E67215 /* PushToTalk.swift */, + 27A99049257A7435009044DB /* SoundEffectPlayQueue.swift */, + A7388431257E6E0C002E8424 /* GroupCallNavigationHeaderView.swift */, + 274A12DD2584BFF5006C6FED /* GroupCallInvitation.swift */, + A7ADC4792587B3750069737A /* VoiceChatActionButton.swift */, + 275045E425EE271D00872D20 /* GroupCallDisplayAsController.swift */, + 276B597F261C78F40029FD3F /* GroupCallUIState.swift */, + 278E86D6265D49100006685D /* MicroListenerController.swift */, + ); + name = "group-calls"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -2899,6 +4995,7 @@ C232EA8E1E1D07E700C4D38C /* Frameworks */, C232EA8F1E1D07E700C4D38C /* Resources */, C232EAA91E1D080100C4D38C /* CopyFiles */, + D0BEBD0C256147EE0048FBE0 /* ShellScript */, ); buildRules = ( ); @@ -2913,17 +5010,20 @@ isa = PBXNativeTarget; buildConfigurationList = D098C7221D7E175A007784E4 /* Build configuration list for PBXNativeTarget "Telegram" */; buildPhases = ( - D098C7121D7E175A007784E4 /* Frameworks */, C20D5AB61DAA94500042616A /* Copy Files */, + D098C7121D7E175A007784E4 /* Frameworks */, D098C7111D7E175A007784E4 /* Sources */, D098C7131D7E175A007784E4 /* Resources */, C232EA661E1D041B00C4D38C /* Embed App Extensions */, C23D0D7B1F1A648600AF5151 /* Fonts */, + C2786FF51FEC128F001FB044 /* Palettes */, C2D85FD51F25FE5400EDEA15 /* ShellScript */, + 9F193BD82130B968001C0800 /* ShellScript */, ); buildRules = ( ); dependencies = ( + D032AFA72578190600E67215 /* PBXTargetDependency */, ); name = Telegram; productName = "Telegram-Mac"; @@ -2936,29 +5036,42 @@ D098C70D1D7E175A007784E4 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 0820; - LastUpgradeCheck = 0810; + DefaultBuildSystemTypeForWorkspace = Latest; + LastSwiftUpdateCheck = 1220; + LastUpgradeCheck = 1000; ORGANIZATIONNAME = Telegram; TargetAttributes = { C232EA901E1D07E700C4D38C = { CreatedOnToolsVersion = 8.2; - DevelopmentTeam = 6N38VWS5BX; + DevelopmentTeam = N8RBWJ5X6J; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.ApplicationGroups.Mac = { enabled = 1; }; + com.apple.HardenedRuntime = { + enabled = 0; + }; }; }; D098C7141D7E175A007784E4 = { CreatedOnToolsVersion = 8.0; - DevelopmentTeam = 6N38VWS5BX; + DevelopmentTeam = N8RBWJ5X6J; LastSwiftMigration = 0800; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.ApplicationGroups.Mac = { enabled = 1; }; + com.apple.HardenedRuntime = { + enabled = 1; + }; + com.apple.Keychain = { + enabled = 1; + }; + com.apple.Maps.Mac = { + enabled = 1; + }; com.apple.Sandbox = { enabled = 1; }; @@ -2971,6 +5084,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, Base, es, @@ -2978,6 +5092,8 @@ it, nl, "pt-BR", + ru, + uk, ); mainGroup = D098C70C1D7E175A007784E4; productRefGroup = D098C7161D7E175A007784E4 /* Products */; @@ -2987,6 +5103,10 @@ ProductGroup = C2D70AF21F2BFB3700AE768E /* Products */; ProjectRef = C2D70AF11F2BFB3700AE768E /* Telegram.xcodeproj */; }, + { + ProductGroup = C251FB511FEE300E0035E5D7 /* Products */; + ProjectRef = C251FB501FEE300E0035E5D7 /* Telegram.xcodeproj */; + }, ); projectRoot = ""; targets = ( @@ -3003,7 +5123,6 @@ files = ( C299B2001F1E11AF00922882 /* Localizable.strings in Resources */, C232EA9B1E1D07E700C4D38C /* ShareViewController.xib in Resources */, - C21656D41EE4A83E0041A6BA /* MainMenu.xib in Resources */, C23D0D7A1F1A609300AF5151 /* SFCompactRounded-Semibold.otf in Resources */, C232EAC21E1D448C00C4D38C /* Assets.xcassets in Resources */, C232EA951E1D07E700C4D38C /* icon.icns in Resources */, @@ -3014,35 +5133,161 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 27672EAE26F8D62600297DB4 /* 0.m4a in Resources */, + 27672EAF26F8D62800297DB4 /* 1.m4a in Resources */, + 27672EB026F8D62A00297DB4 /* 2.m4a in Resources */, + 27672EB126F8D62C00297DB4 /* 3.m4a in Resources */, + 27672EB226F8D62E00297DB4 /* 4.m4a in Resources */, + 27672EB326F8D63100297DB4 /* 5.m4a in Resources */, + 27672EB426F8D63300297DB4 /* 6.m4a in Resources */, + 27672EB526F8D63500297DB4 /* 7.m4a in Resources */, + 27672EB626F8D63700297DB4 /* 8.m4a in Resources */, + 27672EB726F8D63900297DB4 /* 9.m4a in Resources */, + 27672EB826F8D63E00297DB4 /* 100.m4a in Resources */, + 27672EB926F8D64000297DB4 /* 101.m4a in Resources */, + 27672EBA26F8D64300297DB4 /* 102.m4a in Resources */, + 27672EBB26F8D64400297DB4 /* 103.m4a in Resources */, + 27672EBC26F8D64600297DB4 /* 104.m4a in Resources */, + 27672EBD26F8D64900297DB4 /* 105.m4a in Resources */, + 27672EBE26F8D64E00297DB4 /* 106.m4a in Resources */, + 27672EBF26F8D65100297DB4 /* 107.m4a in Resources */, + 27672EC026F8D65200297DB4 /* 108.m4a in Resources */, + 27672EC126F8D65400297DB4 /* 109.m4a in Resources */, + 27672EC226F8D65600297DB4 /* 110.m4a in Resources */, + 27672EC326F8D65800297DB4 /* 111.m4a in Resources */, + 2764A5AE25DFBDE700F9A20D /* police.tgs in Resources */, + D0D1391B2521FBD5005FCF35 /* discussion.tgs in Resources */, + D0A75F2C2449B689001F84A0 /* dart_idle.tgs in Resources */, + A76C8AA224221D3000FDB071 /* graph_loading.tgs in Resources */, + A76C8A9E2420FFE500FDB071 /* folder_empty.tgs in Resources */, + D0830FC424127473006198E7 /* new_folder.tgs in Resources */, + 2773A56225FA4E2300AB45E9 /* voice_chat_raise_hand_5.tgs in Resources */, + A7029EFA240E3CDA00A89ABD /* folder.tgs in Resources */, + A7393D422407FF1900CE44CA /* dice_idle.tgs in Resources */, + A7B6DDD723ED935100B8E01C /* think_spectacular.tgs in Resources */, + 27E969E02589172D00CB9F64 /* call up.mp3 in Resources */, C2057FA51EBBCEA7000423DC /* voip_busy.caf in Resources */, + 9F3EAB4520A5ED2F003FE7E3 /* ocr_nn.bin in Resources */, C2057FA61EBBCEA7000423DC /* voip_end.caf in Resources */, + 270E2B4E2609FDBB00B0738C /* playlist_pause_play.tgs in Resources */, + D07450EB233D61B800769D7F /* fly_dollar.tgs in Resources */, C2057FA71EBBCEA7000423DC /* voip_fail.caf in Resources */, C2DDA04E1EC0C024003531BB /* opening.mp3 in Resources */, - C2E064651ECCB24000387BB8 /* NativeCallSettingsViewController.xib in Resources */, + 9F0AE6B52199904400A8B53A /* sound_a.caf in Resources */, + A778DC2E23C77AD800DD307B /* confetti.mp3 in Resources */, + 2763A15C261DF05200C12762 /* voice_chat_start_chat_to_mute.tgs in Resources */, + D07450ED233D61B800769D7F /* chiken_born.tgs in Resources */, + 2744AE7225B58E6C00E8849F /* invitations.tgs in Resources */, + D0E7BE7C256D56DA0068644D /* group_call_member_unmute.json in Resources */, + 9F03681322771A9700456348 /* anim_delete.json in Resources */, + 9F03681822771A9700456348 /* anim_group.json in Resources */, + D07450E9233D61B800769D7F /* keyboard_typing.tgs in Resources */, + 9F03681422771A9700456348 /* anim_unread.json in Resources */, + 27B8AFEB26380B4E0044C71B /* screenon.tgs in Resources */, + 9FDE0A8F21AD41C2001546D7 /* emoji1014-1.txt in Resources */, + D07450F52340F28D00769D7F /* wallet_success_created.tgs in Resources */, + 2773A54725FA324B00AB45E9 /* voice_chat_hand_off.tgs in Resources */, + 9F03680F22771A9700456348 /* anim_unarchive.json in Resources */, + 2773A55125FA4DAC00AB45E9 /* voice_chat_raise_hand_2.tgs in Resources */, + 2773A54625FA324B00AB45E9 /* voice_chat_mute.tgs in Resources */, + 9F03681622771A9700456348 /* anim_mute.json in Resources */, C2057FA81EBBCEA7000423DC /* voip_ringback.caf in Resources */, + 27E969E12589172D00CB9F64 /* call down.mp3 in Resources */, + A731924F25CD5F2300218E0E /* destructor.tgs in Resources */, + 2773A56C25FAA63E00AB45E9 /* voice_chat_hand_on_unmuted.tgs in Resources */, C252532D1DF04AF500ADBC98 /* begin_record.caf in Resources */, + D0E7BE71256D30130068644D /* group_call_speaker_mute.json in Resources */, + 2763A162261DF5CD00C12762 /* voice_chat_cancel_reminder.tgs in Resources */, + 275DDC3A267B8702009CF884 /* bot_menu_close.tgs in Resources */, + 27A9904E257A8047009044DB /* Pop.wav in Resources */, + D07450E8233D61B800769D7F /* brilliant_loading.tgs in Resources */, + 2763A165261DF5EF00C12762 /* voice_chat_cancel_reminder_to_mute.tgs in Resources */, + A7C7215923FD473E00CE3F75 /* success_saved.tgs in Resources */, C29B5F431DC7970400D13E65 /* sent.caf in Resources */, C27AAFE61DE9D2EE009B9629 /* PhoneCountries.txt in Resources */, + 2773A54425FA324B00AB45E9 /* voice_chat_unmute.tgs in Resources */, C2DDA0501EC0C19A003531BB /* opening.m4a in Resources */, + 2763A168261DF60A00C12762 /* voice_chat_cancel_reminder_to_raise_hand.tgs in Resources */, + 2773A56525FA4E3A00AB45E9 /* voice_chat_raise_hand_6.tgs in Resources */, + 2763A171261DF6A100C12762 /* voice_chat_set_reminder_to_raise_hand.tgs in Resources */, C250B0371DB7BB09004E9FBE /* mime-types.txt in Resources */, + D07450EA233D61B800769D7F /* write_words.tgs in Resources */, + 2773A53525F91E1500AB45E9 /* voice_chat_raise_hand_1.tgs in Resources */, + 270E2B4B2609F95D00B0738C /* playlist_play_pause.tgs in Resources */, + 278C720425E67C2700F1B315 /* emoji1016.txt in Resources */, + 9F03681722771A9700456348 /* anim_unpin.json in Resources */, C253A9701D92F9F100CDC850 /* Localizable.strings in Resources */, + D07450E7233D61B800769D7F /* gift.tgs in Resources */, + 2773A56D25FAA63E00AB45E9 /* voice_chat_hand_on_muted.tgs in Resources */, + 9F03681B22771A9700456348 /* anim_pin.json in Resources */, C232EA3D1E1C06EF00C4D38C /* dsa_pub.pem in Resources */, D098C71B1D7E175A007784E4 /* Assets.xcassets in Resources */, C2CFCABE1EBB4A4D00843F6A /* voip_connecting.mp3 in Resources */, + 9F03681222771A9700456348 /* anim_read.json in Resources */, + 2773A56825FA4E4B00AB45E9 /* voice_chat_raise_hand_7.tgs in Resources */, + D07450F32340E89100769D7F /* sad_man.tgs in Resources */, + D07C6D802346A43000468B1A /* monkey_unsee.tgs in Resources */, D098C71E1D7E175A007784E4 /* MainMenu.xib in Resources */, + D07450EE233D61B800769D7F /* keychain.tgs in Resources */, + 274BB56D264013AD00620D03 /* cameraon.tgs in Resources */, + D07C6D812346A43000468B1A /* monkey_see.tgs in Resources */, + D0D71386227ADE9400EC88B1 /* maccheck.json in Resources */, + 2773A55F25FA4E0C00AB45E9 /* voice_chat_raise_hand_4.tgs in Resources */, C2844AE01DA90C8A009308DC /* emoji.txt in Resources */, + 9FC8ADA8206A77E00094F7B4 /* countries in Resources */, C21A48B21F7D0D3F0095ADB1 /* VideoMessage.fsh in Resources */, + D0FFCEBE215A7CB700995AFE /* emoji14.txt in Resources */, + 9F03681A22771A9700456348 /* anim_unmute.json in Resources */, + 274BB570264013B900620D03 /* cameraoff.tgs in Resources */, C27A6F731ECF610C00C65577 /* currencies.json in Resources */, + 2763A16E261DF68100C12762 /* voice_chat_set_reminder_to_mute.tgs in Resources */, + 27478F5C25FBD05F005B8B98 /* voice_chat_raise_hand_3.tgs in Resources */, C250B01C1DB670B4004E9FBE /* url-schemes.txt in Resources */, + D0A5586E2574F75B00B7C182 /* group_call_chatlist_typing.json in Resources */, + 9F03681122771A9700456348 /* archiveAvatar.json in Resources */, + D07450EF233D61B800769D7F /* smart_guy.tgs in Resources */, C23BC3681E9AA03E00D79F92 /* emoji11.txt in Resources */, + D07450E6233D61B800769D7F /* brilliant_static.tgs in Resources */, + 9F03681522771A9700456348 /* anim_hide.json in Resources */, + 27A9904F257A8047009044DB /* Purr.wav in Resources */, + D0E7BE72256D30130068644D /* group_call_speaker_unmute.json in Resources */, C21A48B41F7D0D6B0095ADB1 /* VideoMessage.vsh in Resources */, + 27A7D37E2600F58A00A67737 /* voip_group_recording_started.mp3 in Resources */, + A778DC3423C8988300DD307B /* quiz-correct.mp3 in Resources */, + 27E969E6258B417200CB9F64 /* reconnecting.mp3 in Resources */, + 9F03681022771A9700456348 /* anim_ungroup.json in Resources */, + D0E7BE7B256D56DA0068644D /* group_call_member_mute.json in Resources */, + 2763A16B261DF65700C12762 /* voice_chat_set_reminder.tgs in Resources */, + 27A7D37D2600F58A00A67737 /* voip_group_unmuted.mp3 in Resources */, + 275DDC3B267B8702009CF884 /* bot_close_menu.tgs in Resources */, + A778DC3323C8988300DD307B /* quiz-incorrect.mp3 in Resources */, + D07450EC233D61B800769D7F /* swap_money.tgs in Resources */, + 27B8AFEC26380B4E0044C71B /* screenoff.tgs in Resources */, + A731925925DBA60000218E0E /* gigagroup.tgs in Resources */, C21074241E77F5DF006EE5EF /* dsa_pub_prod.pem in Resources */, + 9F03681922771A9700456348 /* anim_archive.json in Resources */, + C23D0D7C1F1A649900AF5151 /* SFCompactRounded-Semibold.otf in Resources */, + 2740AA3A26BD5F7A004B3BCE /* builtin-wallpaper-svg in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 9F193BD82130B968001C0800 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = " +"; + }; C2D85FD51F25FE5400EDEA15 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -3054,7 +5299,25 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "sh ${PROJECT_DIR}/tools/bump_build_number.sh \"${PROJECT_DIR}/TelegramMac/${INFOPLIST_FILE}\"\nsh ${PROJECT_DIR}/tools/sync_share_version.sh \"${PROJECT_DIR}/${INFOPLIST_FILE}\" \"${PROJECT_DIR}/TelegramShare/Info.plist\"\n"; + shellScript = "sh ${PROJECT_DIR}/tools/bump_build_number.sh \"${PROJECT_DIR}/${INFOPLIST_FILE}\"\nsh ${PROJECT_DIR}/tools/sync_share_version.sh \"${PROJECT_DIR}/${INFOPLIST_FILE}\" \"${PROJECT_DIR}/TelegramShare/Info.plist\"\n"; + }; + D0BEBD0C256147EE0048FBE0 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = " +"; }; /* End PBXShellScriptBuildPhase section */ @@ -3063,12 +5326,38 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 273BE6C926136D4C00AAFE4E /* ImageCompression.swift in Sources */, + A7E831F1255040D60095A167 /* ChatPresentationInputQueryResult.swift in Sources */, + D06C69532539E30200DD9005 /* SlotMachineValue.swift in Sources */, + A789E09723E427CE00AEB34A /* ChatListFilterPreferences.swift in Sources */, + A7F2831923994B9700742C20 /* AutoplayPreferences.swift in Sources */, + A71DC8342386D512000EEDE2 /* Signature.swift in Sources */, + A71DC8352386D512000EEDE2 /* Config.swift in Sources */, + D06ACAF0231AC3E5002DCD81 /* ParseAppearanceColors.swift in Sources */, + D06ACAEF231AC265002DCD81 /* ChatMessageBubbleImages.swift in Sources */, + D04D2145230DB57300609388 /* TelegramIconsTheme.swift in Sources */, + D06CD4F022F22F1100E444DF /* PhotoCache.swift in Sources */, + D00CE4F82284D5E3008C1B4F /* TransformOutgoingMessageMedia.swift in Sources */, + D00CE4F72284D530008C1B4F /* BuildConfig.m in Sources */, + 9F147F7E22315EC200D71BD1 /* ManageSharedAccountInfo.swift in Sources */, + 9F147F7F22315EC200D71BD1 /* SharedAccountInfo.swift in Sources */, + 9F147F7A2231533700D71BD1 /* InAppNotificationSettings.swift in Sources */, + A7E831F4255041020095A167 /* IsEqualMessages.swift in Sources */, + 9F147F79223151D900D71BD1 /* RenderedTotalUnreadCount.swift in Sources */, + 9F147F782231515800D71BD1 /* UpdaterNotifySettings.swift in Sources */, + 9F147F7722314FBE00D71BD1 /* PanelUtils.swift in Sources */, + 9F147F7522314F2700D71BD1 /* GlobalBadgeNode.swift in Sources */, + 9F147F7322314B5500D71BD1 /* AccountContext.swift in Sources */, + 9F147F7422314B5500D71BD1 /* SharedAccountContext.swift in Sources */, + 9F88A134200FD425007B899E /* Wallpapers.swift in Sources */, C299B1FF1F1E0B9F00922882 /* NumberPluralizationForm.m in Sources */, + 9F147F7D2231543800D71BD1 /* PeerUtils.swift in Sources */, C299B1FE1F1E0AB700922882 /* ApplicationSpecificPreferencesKeys.swift in Sources */, C299B1FD1F1E0A8E00922882 /* StringPluralization.swift in Sources */, C299B1FC1F1E0A3500922882 /* ObjcUtils.m in Sources */, C299B1FB1F1E097C00922882 /* ControllerExtension.swift in Sources */, C299B1F91F1E095800922882 /* Localizable.swift in Sources */, + A7D28230236C549B0000A9BF /* SyncCoreExtension.swift in Sources */, C299B1FA1F1E095800922882 /* LocalizableExtension.swift in Sources */, C299B1F81F1E08C900922882 /* ThumbUtils.swift in Sources */, C299B1F61F1E080300922882 /* ThemeSettings.swift in Sources */, @@ -3076,7 +5365,6 @@ C23BC3711E9ACD1700D79F92 /* SearchEmptyRowItem.swift in Sources */, C23BC36C1E9ACC0800D79F92 /* MimeTypes.swift in Sources */, C23BC3701E9ACCF900D79F92 /* SEPasslockController.swift in Sources */, - C291E2751E8B051900D397BA /* PhotoCache.swift in Sources */, C291E2741E8B051100D397BA /* ImageUtils.swift in Sources */, C232EABF1E1D13F500C4D38C /* ChatListNothingItem.swift in Sources */, C232EABC1E1D134C00C4D38C /* AvatarLayer.swift in Sources */, @@ -3085,6 +5373,7 @@ C291E2731E8AFA2C00D397BA /* ShareApplicationContext.swift in Sources */, C232EAB81E1D12A300C4D38C /* ShortPeerRowItem.swift in Sources */, C232EAB91E1D12A300C4D38C /* ShortPeerRowView.swift in Sources */, + A7664940236D6BFD00163DF4 /* PasscodeSettings.swift in Sources */, C232EAC91E1D536200C4D38C /* SEModalProgressView.swift in Sources */, C232EAB71E1D123800C4D38C /* System.swift in Sources */, C232EAB61E1D11CA00C4D38C /* InterfaceObserver.swift in Sources */, @@ -3098,429 +5387,800 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9F27EFF920C6B8EE00682B76 /* PassportController.swift in Sources */, C2593FBF1F7D242E00F6D2B1 /* TGPaintShader.m in Sources */, C23BC37C1E9B8F6F00D79F92 /* AddContactTableItem.swift in Sources */, C248BD221E705B62004B9106 /* PrivacyAndSecurityViewController.swift in Sources */, - C2F952B01F8E1C840056E586 /* CachedAdminIds.swift in Sources */, C250B0211DB67DA6004E9FBE /* WPContentView.swift in Sources */, - C2DF47CB1DE79719003AA6C0 /* TGDataItem.m in Sources */, - C2538E521E770B4600B21DF0 /* GroupAdminsController.swift in Sources */, C22338451F823F8C004AD57C /* VideoCameraStructures.swift in Sources */, C27AAFED1DEB1D72009B9629 /* SignalUtils.swift in Sources */, C253A9631D91A90100CDC850 /* ChatFileMediaItem.swift in Sources */, C24FD4101F20FBFB00A97196 /* RecentUsedEmoji.swift in Sources */, C219E1D71D8869F20042F0C8 /* ChatHoleRowItem.swift in Sources */, - C253A95F1D9165CD00CDC850 /* webp.m in Sources */, + 9F1668B82007E3BC00DD39FB /* ThemeGridControllerItem.swift in Sources */, + A76C8AA424221F5400FDB071 /* StatisticsLoadingRowItem.swift in Sources */, C2DE5D2C1F3CA6E80081EC1E /* InstantPageMediaItem.swift in Sources */, - C252532A1DF03F9600ADBC98 /* TGAudioWaveform.m in Sources */, C218FF981F41FF2300DD7D35 /* InstantPageAnchorItem.swift in Sources */, + A7C41DB623586DC100CF9402 /* PeerPhotosMonthItem.swift in Sources */, + A7C7215B23FD4BAA00CE3F75 /* LottieLocalAnimations.swift in Sources */, C218FF941F3CC99200DD7D35 /* instantPageWebEmbedView.swift in Sources */, + 2713B224260A42FE00CE0EC6 /* MediaTrackFrameBuffer.swift in Sources */, + D0E7BD70256AA6430068644D /* PresentationGroupCall.swift in Sources */, + D0186731223807D200A77C45 /* ChatListEmptyRowItem.swift in Sources */, + D02BD7C7232D4DB200D1814A /* AppearanceThumbs.swift in Sources */, C250B02B1DB76BA4004E9FBE /* WPMediaLayout.swift in Sources */, + 9F7943B020854E2F00FEDB81 /* ProxyListRowItem.swift in Sources */, + 9FF32C7C21B7DF4800BF58B6 /* StickerSettings.swift in Sources */, C2271DB91DAE2125001792B6 /* GroupInfoEntries.swift in Sources */, + D00EECD3252C703C001EB99F /* CallSettingsController.swift in Sources */, C2EA177F1E2FD50000887153 /* TransformImageView.swift in Sources */, + 276B5980261C78F40029FD3F /* GroupCallUIState.swift in Sources */, C29C3E6F1E4352C100193A7E /* ContextStickerRowItem.swift in Sources */, + D0D087E2243DF4F100E05317 /* ChatlistFilterVisibilityItem.swift in Sources */, C2271F3E1DB4D4240045E719 /* ETabRowItem.swift in Sources */, - C2DF47BD1DE79574003AA6C0 /* diag_range.c in Sources */, - C25BB1691F867FEE0089ED02 /* ChatVideoAccessoryView.swift in Sources */, - C2DF47BB1DE79574003AA6C0 /* framing.c in Sources */, - C29670791F0FAAC800884DA2 /* AppearanceViewController.swift in Sources */, + 9F7B5FCB22003DC70087D020 /* WallpaperColorPicker.swift in Sources */, + D00B191C24E54BA3006CCB87 /* CallReceptionControl.swift in Sources */, + A7393D352407CAE100CE44CA /* ChatMediaDice.swift in Sources */, + D0BEB9952166AF270055B718 /* PeerMediaTouchBar.swift in Sources */, + C25BB1691F867FEE0089ED02 /* ChatMessageAccessoryView.swift in Sources */, + C21BE3AF1FD099AA00C1C849 /* DeveloperViewController.swift in Sources */, + D070DB8122D3638F008A0BBE /* StickersViewController.swift in Sources */, C2777B621DCFB4C9008B69DD /* ChatServiceItem.swift in Sources */, + 278E86D7265D49100006685D /* MicroListenerController.swift in Sources */, C232EABE1E1D13A800C4D38C /* TextUtils.swift in Sources */, C253A9741D94182500CDC850 /* ChatRightView.swift in Sources */, + 27C4089125B068C300372302 /* InviteLinkRowItem.swift in Sources */, + 9FDA713F20EE2D49001ED8ED /* PopularPeersRowItem.swift in Sources */, + 9F77B3A6221C1DAC003B65B8 /* LogoutViewController.swift in Sources */, + 27C4089A25B0BD4F00372302 /* NumberSelectorController.swift in Sources */, C248BD211E6F09CC004B9106 /* ChatGameContentView.swift in Sources */, - C253A92D1D8EE1A600CDC850 /* ChatMediaView.swift in Sources */, + A7F28337239A808500742C20 /* TemplateController.swift in Sources */, + A7F2830623954E1B00742C20 /* CachedFaqInstantPage.swift in Sources */, C226742F1DBE2CB3000BA9ED /* PanelUtils.swift in Sources */, - C2DF47C51DE79574003AA6C0 /* stream.c in Sources */, C271EBA21EB9F04E0034792D /* TGCallUtils.mm in Sources */, + D0530D3E24E69133003273BC /* CallControl.swift in Sources */, + C2CE43E920F4F74F00656543 /* UpdateModalController.swift in Sources */, C22EE6191E66ECB200334C38 /* ReportReasonModalController.swift in Sources */, C2B0722E1DFEDE430082939D /* UsernameInputRowItem.swift in Sources */, - C271EBE91EBA22FE0034792D /* TGCallConnectionDescription.m in Sources */, + 2719655425EEB89E00FC9DE5 /* GroupCallRecorderRowItem.swift in Sources */, + C271EBE91EBA22FE0034792D /* OngoingCallConnectionDescription.m in Sources */, + 277C34A8267B5BC100B1CAA7 /* ChatInputMenuView.swift in Sources */, C23B3AEB1E338ADC009C162C /* ContextMediaRowItem.swift in Sources */, + 9F0368012277091800456348 /* LAnimationButton.swift in Sources */, C26505931E0301A5001954DC /* MGalleryPhotoItem.swift in Sources */, + D00CE50D2289C9B7008C1B4F /* MediaAnimatedStickerView.swift in Sources */, C205FEA61EB39DE400455808 /* SidebarCapViewController.swift in Sources */, C2271DAE1DAE1172001792B6 /* UserInfoEntries.swift in Sources */, C230B9131DD392EB0057F596 /* SearchRowItem.swift in Sources */, C2F4ED1B1EC5AE1D005F2696 /* CallRatingModalViewController.swift in Sources */, C2DF47E91DE892E3003AA6C0 /* ChatToasterView.swift in Sources */, C2167E4F1DC220D800F98E03 /* PeerMediaWebpageRowContent.swift in Sources */, + D02F0A0022E8875800553411 /* SoftwareVideoSource.swift in Sources */, C2B1A10E1D9FD02400ACB1DD /* ChatInterfaceState.swift in Sources */, - C2412E071DA795D200588C14 /* GalleryControls.swift in Sources */, + 27DCF4BD267C9D680019EC49 /* AudioCommandCenter.swift in Sources */, C26546CC1EA0AC3C00E3969A /* ChatVideoMessageItem.swift in Sources */, + D048EDDE24FFEC5600977606 /* ChannelCommentsControls.swift in Sources */, + 9FC4DAA821DE0626003E2A62 /* ChannelPermissionsController.swift in Sources */, C2E0646B1ECF137300387BB8 /* ChatInvoiceItem.swift in Sources */, C232234D1DE20E610078D738 /* ChatMessageDateHeader.swift in Sources */, + 9FA0E52A20519E33001E5649 /* MP4Atom.m in Sources */, + A76C8AB3242366EC00FDB071 /* PeerMediaBlockRowItem.swift in Sources */, C20D5AB31DA9965B0042616A /* EBlockRowView.swift in Sources */, + C21BE3B11FD14CDB00C1C849 /* ParseAppearanceColors.swift in Sources */, + D06C695F253DC5DF00DD9005 /* LocationPreviewMapRowItem.swift in Sources */, + D0E7BD80256C1FB00068644D /* GroupCallSpeakButton.swift in Sources */, C2A71CE11DDB18FF00C69F73 /* ThumbUtils.swift in Sources */, + 9F72973420B878B00067F815 /* MapResources.swift in Sources */, + C23EEC891FCC47C1001371CD /* PeerMediaDateItem.swift in Sources */, C2F8923D1E3FA51000D98B2D /* PasteboardUtils.swift in Sources */, + D004BD2B23153415009A54B1 /* ThemePreviewModalController.swift in Sources */, + 9FFAE506205A928C000C028E /* RingByteBuffer.swift in Sources */, C24DABBE1E0834B7005EE404 /* XCDYouTubeLogger.m in Sources */, - C2DF47D31DE824FD003AA6C0 /* OpusObjcBridge.mm in Sources */, + 9F4EC948218B459A002B3C56 /* RenderedTotalUnreadCount.swift in Sources */, + C23044831F98F8B400977C51 /* MediaPreviewRowItem.swift in Sources */, + D014193C22AE939F008667CB /* ModalOptionSetController.swift in Sources */, C2B4D0C81E36048000CBC4E6 /* ChatActivitiesModel.swift in Sources */, + 9F10CE962061C98E002DD61A /* InputDataRowItem.swift in Sources */, C258D1B41F8D385700458478 /* PreHistorySettingsController.swift in Sources */, C2271DA01DACC7F7001792B6 /* ChatListMessageRowView.swift in Sources */, C219E1DB1D8884290042F0C8 /* ChatHistoryEntry.swift in Sources */, + 9F0367F0227208E000456348 /* QRCode.swift in Sources */, + 9F291CA12264E57F00C66267 /* BuildConfig.m in Sources */, C2A71CD71DD9EEDB00C69F73 /* WebpageModalController.swift in Sources */, + 9FFAE4FB205A8C89000C028E /* MediaPlayer.swift in Sources */, + 9F127E15210B1F540080D709 /* PeerMediaVoiceRowItem.swift in Sources */, C230B90F1DD383820057F596 /* ComposeViewController.swift in Sources */, C22EBCB81DFAB36A0034C435 /* ChatMapRowItem.swift in Sources */, C2AC9C151E1E627F0085C7DE /* TabBadgeItem.swift in Sources */, C25FC7FB1D86FEA90041E303 /* ChatListHoleRowItem.swift in Sources */, C2271F471DB4FC130045E719 /* EStickItem.swift in Sources */, C2844AD71DA907E8009308DC /* EntertainmentViewController.swift in Sources */, + 27FE779A26F4944800E1C90B /* ChatThemeSelectorController.swift in Sources */, + 9F17E5BB212F191F00C25A65 /* AutoNightThemePreferences.swift in Sources */, C2303E731D9966BD00098E12 /* ChatInputActionsView.swift in Sources */, + A7C41DB4235862BB00CF9402 /* PeerMediaPhotosController.swift in Sources */, + 9F0F8E82226DCD1C00A97F6A /* OpmizeDatabaseView.swift in Sources */, + D0E52B3222FD66C4000C0306 /* MessageTimecode.swift in Sources */, + 9F0367F72273260A00456348 /* UndoTooltipController.swift in Sources */, + 2764A5B125E0173900F9A20D /* AutoDeleteContextMenuView.swift in Sources */, + A731925525DA7AA400218E0E /* GigagroupLanding.swift in Sources */, + D0A75F25244843D3001F84A0 /* EditImageCanvasController.swift in Sources */, + 9F21A7D321C167000037784F /* InstantPageImageItem.swift in Sources */, + 9F12D343209251CF0072928B /* EditAccountInfoItem.swift in Sources */, C210959A1E9FE04700E10BDB /* ChatVideoMessageContentView.swift in Sources */, - C2DEC87F1DECB8C800F6544A /* TelegramApplicationContext.swift in Sources */, + C20CAD151FE436E300EFF8BF /* SelectSizeRowItem.swift in Sources */, + D01C731722A9814C000DA008 /* InputPasswordController.swift in Sources */, + 27DF650726AADB9E000753AC /* StringFormat.swift in Sources */, + 9F10CE922061BE19002DD61A /* InputDataController.swift in Sources */, + 9F10CE9820626B1B002DD61A /* PassportDocumentRowItem.swift in Sources */, C29B5F451DC7DA4B00D13E65 /* MessageActionsPanelView.swift in Sources */, + C20CAD121FE291E300EFF8BF /* ChatBubbleAccessoryForward.swift in Sources */, + 9F72974820C597800067F815 /* TermsModalController.swift in Sources */, + 9F13EE1B2100BFCA00562E53 /* VCardHeaderItem.swift in Sources */, C218FF9C1F4204C400DD7D35 /* InstantPageChannelView.swift in Sources */, + D07C6D7D234698C600468B1A /* DynamicHeightRowItem.swift in Sources */, + A73884352580E501002E8424 /* CGChatListIndicator.swift in Sources */, C2271DC11DAE583E001792B6 /* TextAndLabelItem.swift in Sources */, C218FF9A1F42030B00DD7D35 /* InstantPageChannelItem.swift in Sources */, + D01CBE2D22A5384700F6A971 /* NotificationPreferencesController.swift in Sources */, C2D187F21E28D0840038961D /* ChatSwitchInlineController.swift in Sources */, C2A12FE61E0C503900EC2239 /* ChatContactRowItem.swift in Sources */, + D014AA242316CE0700CE5362 /* NewThemeController.swift in Sources */, C29B5F3E1DC74DB400D13E65 /* SenderController.swift in Sources */, + 9FC8ADA6206925F60094F7B4 /* PassportWindowController.swift in Sources */, + A7393D372407CD7A00CE44CA /* ChatDiceContentView.swift in Sources */, C2271D9E1DACC796001792B6 /* ChatListMessageRowItem.swift in Sources */, C2DF47DA1DE82653003AA6C0 /* NSObject+TGLock.m in Sources */, + 9FC4DA9621DD0B35003E2A62 /* PeerChannelMemberCategoriesContextsManager.swift in Sources */, C250B0271DB69694004E9FBE /* WPArticleLayout.swift in Sources */, C232EA131E16552900C4D38C /* WebGameViewController.swift in Sources */, C2AF01131F01543200D8AC1D /* ExportProxyModalController.swift in Sources */, C2F6190D1E844DCD007A051B /* TelegramAccountAuxiliaryMethods.swift in Sources */, - C2DF47C01DE79574003AA6C0 /* picture.c in Sources */, C20B8F3E1DFC52EE008A354E /* ChatEmptyPeerItem.swift in Sources */, - C2016F681EAE0A68003AF981 /* PhoneCallWindowController.swift in Sources */, + 275FD30E266A20FC008EBC99 /* GroupCallTileRowItem.swift in Sources */, + C2016F681EAE0A68003AF981 /* CallWindowController.swift in Sources */, + 9FDA713220E6456A001ED8ED /* ExternalMusicAlbumArtResources.swift in Sources */, + 2713B223260A42FE00CE0EC6 /* MediaTrackFrameDecoder.swift in Sources */, C210742C1E780CC1006EE5EF /* PhotoCache.swift in Sources */, C2256D981DAB9D5A00494CF4 /* ChatHistoryViewForLocation.swift in Sources */, C20CB7291E60886F00C992AC /* LinkInvationController.swift in Sources */, - C253E2381DE398980022A29F /* NativeAudioPlayer.swift in Sources */, C221ED561EA6877300471C65 /* GeneratedMediaStoreSettings.swift in Sources */, C296AF861D8DB178001DBB59 /* MediaUtils.swift in Sources */, + 9FA0E52D20519FCC001E5649 /* LiveUploadingHelper.m in Sources */, C22EE61E1E67506800334C38 /* ChannelMembersViewController.swift in Sources */, - C240E9521F96449E00F671FA /* TwoStepVerificationPasswordEntryController.swift in Sources */, C26A719B1DC9FB3600F69385 /* InputPasteboardParser.swift in Sources */, + D02BD7C3232D14E800D1814A /* ThemePreviewRowItem.swift in Sources */, C230B8F41DD368D40057F596 /* SelectPeersController.swift in Sources */, + 2746926C26A99DD800B67817 /* WallpaperCheckboxView.swift in Sources */, C250B0391DB7BB2D004E9FBE /* MimeTypes.swift in Sources */, C250BA8F1E6E1CDC0057CD96 /* ChatMessageThrottledProcessingManager.swift in Sources */, + 271B783E25ECD94C007144D7 /* PamentsSelectMethodController.swift in Sources */, + 2760E55926970672001C59F8 /* WidgetButton.swift in Sources */, C271EBEB1EBA3BC90034792D /* PCallSession.swift in Sources */, + 9F3D5F8622085B2000CB0CAA /* UpdaterNotifySettings.swift in Sources */, C24DABC11E0834B7005EE404 /* XCDYouTubeVideoOperation.m in Sources */, + A7E83214255E992D0095A167 /* WaveView.swift in Sources */, + D0BEB998216BD8A70055B718 /* EditImageControls.swift in Sources */, C29F4C7D1F47283600DBFC00 /* InstantPageBrowser.swift in Sources */, + 2711D71126CFBF6A00917305 /* MediaObjectToAvatar.swift in Sources */, C25FC7FD1D86FEE00041E303 /* ChatListHoleRowView.swift in Sources */, + 27DBE4AF25B0424100FCEE2A /* InviteLinksController.swift in Sources */, C2A506791DF59C9700971A93 /* PicturePicker.swift in Sources */, C225524D1F7BE8E40007944D /* VideoRecorderModalView.swift in Sources */, + 27BF618026567DEE00331308 /* GroupCallContextMenuHeader.swift in Sources */, C2D1839D1E4DBF7E001CE25A /* MGalleryPeerPhotoItem.swift in Sources */, - C2D2CAED1E64579700939968 /* StickersPackPreviewModalController.swift in Sources */, + 9F63152921D26892009FD379 /* CancelResetAccountController.swift in Sources */, + C2D2CAED1E64579700939968 /* StickerPackPreviewModalController.swift in Sources */, C22B63581D967F4500085C19 /* TGInputTextTag.m in Sources */, C2DE5D341F3CA94A0081EC1E /* InstantPageMediaView.swift in Sources */, + A7F283042395289B00742C20 /* AccountUtils.swift in Sources */, + A7F283022395185B00742C20 /* SettingsSearchableItems.swift in Sources */, + D0A75F2724486B6D001F84A0 /* EditImageCanvasColorPicker.swift in Sources */, C232EA0E1E155E9400C4D38C /* PeersListController.swift in Sources */, + C2C415E51FA33D1A00FF36F4 /* InputFormatterPopover.swift in Sources */, + 9F0AE6C02199CDB500A8B53A /* SVideoView.swift in Sources */, C232EA101E156DE100C4D38C /* ContextCommandRowItem.swift in Sources */, - C20320FF1F9769BA00143395 /* TwoStepVerificationResetController.swift in Sources */, + D0BEB99A216D10920055B718 /* MediaPreviewEditControl.swift in Sources */, C234A7FB1ED7112400EBBECE /* LocalizableExtension.swift in Sources */, + 9FDA713420E65D49001ED8ED /* PeerMediaPlayerAnimationView.swift in Sources */, + 270202C3261B3D3100CFCE6D /* CurrencyUITextFieldDelegate.swift in Sources */, + 276B5986261C79940029FD3F /* GroupCallControlsView.swift in Sources */, C225524B1F7BE7000007944D /* VideoRecorderModalController.swift in Sources */, - C253E2361DE398580022A29F /* AudioPlayer.swift in Sources */, + A778DC3023C8985300DD307B /* SoundEffects.swift in Sources */, C2B648901EB25B1700BA574B /* MediaResources.swift in Sources */, C219E1E81D8AC90B0042F0C8 /* ChatUnreadRowView.swift in Sources */, C2D2CAF01E64874600939968 /* StickerPackGridItem.swift in Sources */, C2379D2A1DDCCBF10063AD30 /* ReplyMarkupNode.swift in Sources */, + D076F86D22959958004F895A /* InlineLoginController.swift in Sources */, C21B24611ED9C39F00FC6CDA /* SuggestionLocalizationViewController.swift in Sources */, + 27D006DF25AF3B1C00EE3EB1 /* ExportedInvitationRowItem.swift in Sources */, C275E9EF1F8FCA4200D3D8C0 /* PhoneNumberIntroController.swift in Sources */, C275E9F71F90CDF900D3D8C0 /* PhoneNumberInputCodeController.swift in Sources */, - C28149881EA7F22200BB933E /* PreparedChatHistoryViewTransition.swift in Sources */, + 9FC8ADA02062D5E70094F7B4 /* PassportAcceptRowItem.swift in Sources */, + D071E8AA214BC589001B6024 /* TouchBarStickerItemView.swift in Sources */, + 9F4EEF8821D515C5002C3B33 /* InstantPageStoredState.swift in Sources */, C20B8F401DFD9999008A354E /* ChatListNothingItem.swift in Sources */, + D014AA262317D07D00CE5362 /* EditThemeController.swift in Sources */, C259ED1C1DB8DC78008E6712 /* ChatNavigateScroller.swift in Sources */, C29A65031E08396F0071BCEF /* MGalleryExternalVideoItem.swift in Sources */, C24DABBF1E0834B7005EE404 /* XCDYouTubePlayerScript.m in Sources */, C24DABC01E0834B7005EE404 /* XCDYouTubeVideo.m in Sources */, C2B1A1131D9FD2AE00ACB1DD /* ChatPresentationInterfaceState.swift in Sources */, C2DE5D321F3CA8370081EC1E /* InstantPageTile.swift in Sources */, + 276A6C0F26C5316F00E4FF34 /* StickerPackTrendingItem.swift in Sources */, + A7919138240D1077002011CA /* ChatListFilterPredicate.swift in Sources */, C28BAB291DF981320027CE3A /* AudioWaveform.swift in Sources */, C22EBCBA1DFAB64F0034C435 /* ChatMapContentView.swift in Sources */, C234A7FE1ED725C300EBBECE /* LanguageViewController.swift in Sources */, C2E40A1B1E37ADAF0099AC7D /* PeerMediaEmptyRowItem.swift in Sources */, + 9F9206F020727AF30054E581 /* ChangePhoneNumberContainerView.swift in Sources */, C239BE951E61F15600C2C453 /* LoginOthersAccountModal.swift in Sources */, + D07450F12340DC8200769D7F /* WalletConfiguration.swift in Sources */, C2B6488C1EB2533800BA574B /* TGGifConverter.m in Sources */, - C2EA53471F751EF300C183F7 /* GalleryMessageEntry.swift in Sources */, - C22674411DC13165000BA9ED /* GridHoleItem.swift in Sources */, C250B01A1DB66B44004E9FBE /* InAppLinks.swift in Sources */, + 9FF1DEA6225B699D009512C9 /* SearchUtils.swift in Sources */, + A7C1379E23DF21EA00803ED3 /* ChatListRevealItem.swift in Sources */, C22A899F1EBCC2AA005B963D /* CallNavigationHeaderView.swift in Sources */, + 27949113260A41EB0003BFA0 /* FFMpegMediaFrameSourceContextHelpers.swift in Sources */, C27A6F711ECF604A00C65577 /* TGCurrencyFormatter.m in Sources */, + A742CDCD240FB32F00C6B69B /* ChatListFilterRecommendedItem.swift in Sources */, C2A506771DF5664F00971A93 /* ForwardChatListController.swift in Sources */, + 2744AE6D25B44E1500E8849F /* ExportedInvitationController.swift in Sources */, + D0BE207E24D032E400038A8B /* VolumeControllerPopover.swift in Sources */, C20232AA1D81D19C007C9ADE /* ChatRowItem.swift in Sources */, - C2271F3A1DB4D0540045E719 /* EStickersViewController.swift in Sources */, + D0E7BD79256AE4B00068644D /* GroupCallController.swift in Sources */, + A731924B25CAE9DC00218E0E /* AutoremoMessagesController.swift in Sources */, + 9FA0E53B2052EDFF001E5649 /* HackUtils.m in Sources */, + 9FC4DA9E21DD1C6C003E2A62 /* ImageCompression.swift in Sources */, C2271DAC1DAE1116001792B6 /* PeerInfoEntries.swift in Sources */, C25C132D1E8A404F00AE26A1 /* InstalledStickerPacksController.swift in Sources */, + 9F10CE9A206284F8002DD61A /* InputDataDateRowItem.swift in Sources */, C24DABA41E083185005EE404 /* YTVimeoURLParser.m in Sources */, + D0FFC4AC23E184BA0044D305 /* ChatListFilterController.swift in Sources */, C22674311DBE9B50000BA9ED /* FetchCachedRepresentations.swift in Sources */, C25C13311E8A406D00AE26A1 /* FeaturedStickerPacksController.swift in Sources */, + D0A2764E249C9D6B005E3C77 /* PeerPhotos.swift in Sources */, + 9FC8ADA22062E2DF0094F7B4 /* PassportNewPhoneNumberRowItem.swift in Sources */, + 9F0E6F7A203EFE870086699C /* Preferences.swift in Sources */, + 27A0A4EB25F7814500B789AF /* GroupCallTextAndLabelRowItem.swift in Sources */, + 9F0AE6BE2199BEBE00A8B53A /* SVideoController.swift in Sources */, + 9F580BE520A0AA7B00F6D56C /* ChatRecorderOverlayWindow.swift in Sources */, C2016F3B1EAA4538003AF981 /* RecentPeerRowItem.swift in Sources */, + 276B597A261C780F0029FD3F /* GroupCallMainVideoContainer.swift in Sources */, + 9FDD78D121C8F0CC00F1B4EF /* ChatPollItem.swift in Sources */, + D00747042577AF0B0000DF74 /* PushToTalkRowItem.swift in Sources */, C25F71481E410DEE0046AF4E /* InAppNotificationSettings.swift in Sources */, C250B0311DB78869004E9FBE /* QuickLookPreview.swift in Sources */, + D0BDD50123A38660002010A5 /* InactiveChannelsController.swift in Sources */, C2DE5D3A1F3CAD990081EC1E /* InstantPageLayout.swift in Sources */, + 9F153D1621F3662700B95D82 /* StoredMessageFromSearchPeer.swift in Sources */, + 9F7D422A22240404007B68BB /* AccountContext.swift in Sources */, + 9F21A7CF21C1552D0037784F /* InstantPageTheme.swift in Sources */, C21B24681EDB116B00FC6CDA /* NumberPluralizationForm.m in Sources */, C2EA17811E2FD50A00887153 /* ImageUtils.swift in Sources */, + 9F147F7C2231543800D71BD1 /* PeerUtils.swift in Sources */, C219E1E41D8AC8370042F0C8 /* ChatUnreadRowItem.swift in Sources */, + 274B831826F0C0A700E6E474 /* EmojiScreenEffect.swift in Sources */, + D02F0A0422E88C9900553411 /* UniversalSoftwareVideoSource.swift in Sources */, C296AF7F1D8D38E5001DBB59 /* ChatRowView.swift in Sources */, + 278680AD26A982DC005FDBB9 /* WallpaperPatternPreviewController.swift in Sources */, C2F9C4481F9500B4002B2CBF /* TwoStepVerificationUnlockController.swift in Sources */, C2B6A9651E8519FA00A441B7 /* RecentGIFRowItem.swift in Sources */, + C2E8BA071FB5EF4C00DEB5E2 /* GalleryThumbsControl.swift in Sources */, + 2713B21F260A42FE00CE0EC6 /* MediaFrameSource.swift in Sources */, C2A1054B1E0163D500B01F48 /* GalleryPageController.swift in Sources */, + D0D752C822F8A76100E2CB74 /* Geocoding.swift in Sources */, + 274A12DE2584BFF5006C6FED /* GroupCallInvitation.swift in Sources */, C271EB901EB8E6A40034792D /* SelectivePrivacySettingsPeersController.swift in Sources */, C209C3731F262537009231FE /* emoji_suggestions_data.cpp in Sources */, - C2FD34131E6C2503008D13D4 /* LegacyImportAuthorization.swift in Sources */, + D0BEB98F21628C250055B718 /* EditImageModalController.swift in Sources */, C28BAB271DF980DE0027CE3A /* AudioRecorder.swift in Sources */, + 2775102525E79233003D58D1 /* PaymentsCheckoutPreviewRowItem.swift in Sources */, C21B24651EDB115700FC6CDA /* StringPluralization.swift in Sources */, C248BD291E706DDA004B9106 /* RecentSessionRowItem.swift in Sources */, - C2DF47BE1DE79574003AA6C0 /* opus_header.c in Sources */, + 9F77B3982211979B003B65B8 /* AutoplayPreferences.swift in Sources */, + A778DC2A23C75F1100DD307B /* Confetti.swift in Sources */, C2B9BE891EFC5E7000D6B96F /* Appearance.swift in Sources */, + C256A9161FB9E1490043D497 /* AdditionalSettings.swift in Sources */, C29F4C781F45FBFF00DBFC00 /* InstantPageSlideshowItem.swift in Sources */, - C2DF47D01DE82475003AA6C0 /* OpusAudioPlayer.swift in Sources */, C2B1A1111D9FD0A100ACB1DD /* ChatInterfaceInteraction.swift in Sources */, C2B1A0EF1D9D94CE00ACB1DD /* SeparatorRowItem.swift in Sources */, C29340F11F506C310074991E /* EmptyGroupstickerSearchRowItem.swift in Sources */, + 27F5F23325E7CACE00E8AC69 /* CurrencyFormat.swift in Sources */, + D06C6950253887F600DD9005 /* SlotMachineValue.swift in Sources */, + D09D9DF4229C289F00378796 /* GZip.m in Sources */, + D0906CFC254BFE1D000E6961 /* ModalPreviews.swift in Sources */, C2FD33F21E69CF6D008D13D4 /* ChatUrlPreviewModel.swift in Sources */, C225524F1F7C03B50007944D /* VideoRecorderPipeline.swift in Sources */, + A7E83212255E8A8E0095A167 /* LinkHoverController.swift in Sources */, + D001E976243B385E009025F9 /* FolderIcons.swift in Sources */, + 27F023D425A5C19F008F1C81 /* DesktopCapturerWindow.swift in Sources */, C2B648891EB0C48600BA574B /* PIPVideoWindow.swift in Sources */, + 9F3EAB4B20A5F90B003FE7E3 /* TGPassportMRZ.m in Sources */, C2B6488E1EB25A5400BA574B /* FetchVideoMediaResource.swift in Sources */, + D06C7BB7247D6FA900E67C3C /* SampleBufferPool.swift in Sources */, C2A72D901DEC66F300C3B945 /* LoginErrorStateView.swift in Sources */, + 9F0AE6872191D29D00A8B53A /* ContextSearchMessageItem.swift in Sources */, + A7565EAE23BE02AF0031EADE /* ChatGradientModel.swift in Sources */, + 9F0AE6BC2199BBB900A8B53A /* MediaPlayerView.swift in Sources */, + 9F7D421F22203DB1007B68BB /* ChannelStatisticsController.swift in Sources */, C2271F491DB4FC220045E719 /* EStickView.swift in Sources */, + 9F153D1421F0C7F800B95D82 /* WallpaperPreviewController.swift in Sources */, + 2713B222260A42FE00CE0EC6 /* MediaPlaybackData.swift in Sources */, + 27A0A4E325F76F4600B789AF /* GroupCallPeerController.swift in Sources */, + 9F6B54C821369B4000748FC1 /* GalleryModernControls.swift in Sources */, C2271F541D9D420B00424F7B /* ContactsController.swift in Sources */, - C24D9FDC1E267932002CD3F3 /* PreviewSenderItems.swift in Sources */, - C2271F511D9C392400424F7B /* SPopoverRowView.swift in Sources */, C258D1B61F8D3A0D00458478 /* PreHistoryControllerStructures.swift in Sources */, C2C98FEF1E818FB5009CBDB7 /* ClearUserNotifies.swift in Sources */, - C2B1B1201E5F170A00895E0D /* ValidateAddressNameInteractive.swift in Sources */, + A7831B1C2403CFDD0056AEAC /* ChannelStatsViewController.swift in Sources */, + A7C1377D23D1A62800803ED3 /* PeerEmptyHolderItem.swift in Sources */, + A778DC4223CD9F3C00DD307B /* SearchSettingsEmptyItem.swift in Sources */, C2DE5D381F3CAD010081EC1E /* InstantPageTextStyleStack.swift in Sources */, C29E0EE21F4EFB5100C0C7A8 /* GroupStickerSetController.swift in Sources */, C29B5F4B1DC8ED5900D13E65 /* ForwardPanelModel.swift in Sources */, + 27FE779C26F4AE4400E1C90B /* ChatThemeRowItem.swift in Sources */, + 9FFAE4F3205A8C89000C028E /* MediaPlayerAudioRenderer.swift in Sources */, + D0D81AEE243E2D6F00CB9D20 /* ChatListFilterFolderIconController.swift in Sources */, + D0E7BD1A256A7D350068644D /* GroupCallWindow.swift in Sources */, + D0D7520B24C04FD60037D73A /* VideoEditorScrubbler.swift in Sources */, + 2744018526E6344E0035C55D /* WidgetRecentPeersController.swift in Sources */, + D074A56524A1DE7700E92F8A /* OngoingCallContext.swift in Sources */, C25911371DF1A68200671E72 /* ChatInputRecordingView.swift in Sources */, C2271DB31DAE127A001792B6 /* GeneralRowView.swift in Sources */, C2167E511DC220E900F98E03 /* PeerMediaMusicRowContent.swift in Sources */, + 27949110260A41EB0003BFA0 /* FFMpegMediaFrameSource.swift in Sources */, + 9F62AE83202D8759007FB557 /* FetchMediaUtils.swift in Sources */, + D032AFA42578172400E67215 /* PushToTalk.swift in Sources */, C2A506751DF438B900971A93 /* AudioWaveformView.swift in Sources */, + D00EECD5252C9192001EB99F /* CameraPreviewRowItem.swift in Sources */, C2271DB51DAE131C001792B6 /* GeneralInteractedRowItem.swift in Sources */, + 276C1EBF261B451500FE41C9 /* Currency.swift in Sources */, + 9FDA713B20EA9532001ED8ED /* ReadArticlesListPreferences.swift in Sources */, + D093474E242DCFC4000ECA88 /* PeerMediaGroupPeersController.swift in Sources */, C25C13331E8A433700AE26A1 /* TableUtils.swift in Sources */, C24949141E5B763F00D7ED5D /* ApplicationContext.swift in Sources */, C219E1F61D8C09130042F0C8 /* ChatMessageItem.swift in Sources */, C209C3741F262537009231FE /* emoji_suggestions.cpp in Sources */, C2A71CE31DDB2EBD00C69F73 /* GeneralSettingsViewController.swift in Sources */, + A7C1379223DB00D900803ED3 /* ChatListFilterPreferences.swift in Sources */, C24DABBD1E0834B7005EE404 /* XCDYouTubeClient.m in Sources */, C2271D9C1DACC027001792B6 /* SearchController.swift in Sources */, + D071E8B3214C15C7001B6024 /* TouchBarEmojiPicker.swift in Sources */, + 276B597D261C78AD0029FD3F /* GroupCallView.swift in Sources */, C234A8001ED73A3300EBBECE /* LanguageRowItem.swift in Sources */, - C226743F1DC12E4E000BA9ED /* GridMessageItem.swift in Sources */, + 27949115260A41EB0003BFA0 /* FFMpegMediaVideoFrameDecoder.swift in Sources */, + D02F0A0222E88C6E00553411 /* MediaPlayerFramePreview.swift in Sources */, + 9F7B5FCD220099220087D020 /* WallpaperPatternPreviewView.swift in Sources */, C250B02E1DB7805E004E9FBE /* ChatLayoutUtils.swift in Sources */, + 27200F1B257C256E00574365 /* DevicesContext.swift in Sources */, + D0FCA7622434867400B72F18 /* PeerInfoHeadItem.swift in Sources */, + 27E001B526285AC8008786D3 /* PinchToZoom.swift in Sources */, C2271DBF1DAE563D001792B6 /* PeerInfoHeaderItem.swift in Sources */, + 27CF9609268F5B3E0086515A /* SoftwareGradientBackgroundItem.swift in Sources */, + A7F2831B239A496400742C20 /* ContextShowPeersHolder.swift in Sources */, C2A71CED1DDB3C1000C69F73 /* ChatHeaderController.swift in Sources */, - C2C5C2051EF822B900AEA252 /* ProxySettingsViewController.swift in Sources */, C2203EA21DDE2AB8001E6AB6 /* ChatSelectText.swift in Sources */, + 9F4EEF8021D3C76E002C3B33 /* InstantPageContentView.swift in Sources */, + 9FFAE504205A916B000C028E /* VideoPlayerProxy.swift in Sources */, C2CBCAC01D81528700142EC0 /* System.swift in Sources */, + A7E831F82551A85F0095A167 /* ColdStartPasslockController.swift in Sources */, C221ED5E1EA6B36600471C65 /* ChatStorageManagmentModalController.swift in Sources */, + 9FB14FBF209889A500688EF9 /* EDSunriseSet.m in Sources */, C21AAE361DB22CA5007638C5 /* AvatarLayer.swift in Sources */, C2DF47E01DE846AB003AA6C0 /* ChatAudioContentView.swift in Sources */, + 270F020726F0B0830099D2AA /* EmojiAnimationEffectView.swift in Sources */, + 27AAFCBF2649288E000B1053 /* GroupCallTileView.swift in Sources */, C29B5F4F1DC8F39A00D13E65 /* FWDNavigationAction.swift in Sources */, - C2DF47C41DE79574003AA6C0 /* opusfile.c in Sources */, C24DABA51E083185005EE404 /* YTVimeoVideo.m in Sources */, C2FD33E91E696A86008D13D4 /* GroupsInCommonViewController.swift in Sources */, C21795E31E795955006A2AA3 /* SecretChatKeyViewController.swift in Sources */, - C2DF47C21DE79574003AA6C0 /* info.c in Sources */, + D0E3684824D842F7009896D4 /* StorageUsageCleanProgressRowItem.swift in Sources */, + C2CE43E420E2CFE800656543 /* PlayerListController.swift in Sources */, + 9F21A7DC21C290E00037784F /* InstantPageTableItem.swift in Sources */, D098C73A1D7E1C40007784E4 /* AuthController.swift in Sources */, + C2905E1C207E4D9E00990AD7 /* InstantPageAudioView.swift in Sources */, + D014193E22AE9A90008667CB /* GeneralLineSeparatorRowItem.swift in Sources */, C29A65011E0837A70071BCEF /* XCDYouTubeVideoWebpage.m in Sources */, C26505971E041B91001954DC /* MGalleryGIFItem.swift in Sources */, - C2A71CE91DDB342100C69F73 /* NotificationSettingsViewController.swift in Sources */, + 278FA2E625E813F100280629 /* PaymentsShippingMethodController.swift in Sources */, + 9FF5A1CB2232A2FF00BC1359 /* UpgradedAccount.swift in Sources */, + 9FA0E5342051A41A001E5649 /* PreUploadManager.swift in Sources */, + D076F88D2296FD18004F895A /* AnimtedStickerHeaderItem.swift in Sources */, + D0A27650249CE588005E3C77 /* GroupsStatsController.swift in Sources */, C2CFCAC01EBB9C8100843F6A /* CallAudioPlayer.swift in Sources */, C230B9241DD4C78E0057F596 /* ChatGIFMediaItem.swift in Sources */, C230B9161DD39E780057F596 /* CreateGroupViewController.swift in Sources */, C276383E1E8A9A86009E7839 /* StickerSetTableRowItem.swift in Sources */, + A7393D442408F84300CE44CA /* DiceCache.swift in Sources */, + D0B6D72A22AA65D3008E36FE /* NewContactController.swift in Sources */, + D076F88F2297285F004F895A /* DiscussionSetModalController.swift in Sources */, C2DF47CE1DE79751003AA6C0 /* ATQueue.m in Sources */, - C2C9B9341E8016C400380D79 /* NSObject+SPInvocationGrabbing.m in Sources */, C224A72D1EB75A3100F43F3F /* ExMajorNavigationController.swift in Sources */, - C271EB9B1EB9DEF00034792D /* CallBridge.mm in Sources */, + C271EB9B1EB9DEF00034792D /* OngoingCallThreadLocalContext.mm in Sources */, + A7831B2A24040B6B0056AEAC /* StatisticRowItem.swift in Sources */, C2B1B1261E5F840B00895E0D /* PeerInfoUtils.swift in Sources */, C2D187F01E28C58C0038961D /* ContextSwitchPeerRowItem.swift in Sources */, C24DABA21E083185005EE404 /* YTVimeoExtractor.m in Sources */, + 278E86D4265D0F330006685D /* GroupCallAvatarView.swift in Sources */, C26A71991DC9FA5100F69385 /* EditMessageModel.swift in Sources */, C2AF011D1F03D4C600D8AC1D /* TGCallAesCtr.m in Sources */, C250B0231DB6927C004E9FBE /* WPArticleContentView.swift in Sources */, + 9FFAE509205A92B9000C028E /* RingBuffer.m in Sources */, C2271F401DB4D5850045E719 /* ETabRowView.swift in Sources */, + 273274E826A8190F00E04289 /* WallpaperAdditionColorView.swift in Sources */, C23C5AE11E1136D1005903E1 /* GroupNameRowItem.swift in Sources */, + D04D2144230DB55B00609388 /* TelegramIconsTheme.swift in Sources */, + 2773A53E25FA220C00AB45E9 /* JoinVoiceChatAlertRowItem.swift in Sources */, C25C132F1E8A405E00AE26A1 /* ArchivedStickerPacksController.swift in Sources */, - C29F4C761F45F58B00DBFC00 /* MIHSliderView.m in Sources */, - C2DF47C31DE79574003AA6C0 /* internal.c in Sources */, + D01E1F0422B39A4800AD6DAE /* LottiePlayer.swift in Sources */, + 2728991926CBC0D100F4D288 /* UNUserNotifications.swift in Sources */, + D004167522D37AD00000566B /* StickerPackPanelRowItem.swift in Sources */, C295C65F1F75808600BA309D /* ChatAdditionController.swift in Sources */, + D001E974243B1A0A009025F9 /* LeftSidebarController.swift in Sources */, C2271F571D9D46BC00424F7B /* ShortPeerRowItem.swift in Sources */, C291942F1DCC6E2200359491 /* DeclareEncodables.swift in Sources */, + 27B15D32259B373800C7F280 /* DesktopCapturePreviewItem.swift in Sources */, + 27E6DB0A2689C71E003D6164 /* MetalFunctions.metal in Sources */, + A7D2822F236C549B0000A9BF /* SyncCoreExtension.swift in Sources */, C2A315BB1E2E697200D89000 /* TwoStepVerification.swift in Sources */, C296AF881D8DB2A7001DBB59 /* ChatMediaContentView.swift in Sources */, C2DE5D401F3CB0380081EC1E /* InstantPageTileView.swift in Sources */, C2271DBB1DAE213D001792B6 /* ChannelInfoEntries.swift in Sources */, + 9FC8AD9C2062AA630094F7B4 /* ValuesSelectorModalController.swift in Sources */, + A7393D462409044C00CE44CA /* ChannelOverviewStatsRowItem.swift in Sources */, + 27E5997D261B3FA800228411 /* String.swift in Sources */, C221ED581EA687B600471C65 /* AutomaticMediaDownloadCategoryPeers.swift in Sources */, + 27BA045726E4F3E9008FC1A3 /* MessageReadMenuItem.swift in Sources */, + 9F1BABB021E60DCC0075C03E /* UndoOverlayHeaderView.swift in Sources */, C24BA3BD1E9D30F800E8970B /* DeleteSupergroupMessagesModalController.swift in Sources */, C20232AC1D81D1AE007C9ADE /* ChatMessageView.swift in Sources */, C209C3771F26271B009231FE /* EmojiSuggestionBridge.mm in Sources */, + D071E8B5214C1935001B6024 /* TouchBarEmojiItemView.swift in Sources */, C250B0291DB769EE004E9FBE /* WPMediaContentView.swift in Sources */, + D06C69522538894C00DD9005 /* SlotsMediaContentView.swift in Sources */, + 275045E525EE271D00872D20 /* GroupCallDisplayAsController.swift in Sources */, + D071E8A8214BBE21001B6024 /* ChatStickersTouchBarPopover.swift in Sources */, C2B1A0F11D9D94E400ACB1DD /* SeparatorRowView.swift in Sources */, C232E9F61E1437B100C4D38C /* SearchResultModalController.swift in Sources */, + 9F21A7D521C16CB90037784F /* InstantPagePeerReferenceItem.swift in Sources */, + A7D28237236C5B2C0000A9BF /* PhoneNumberUtils.swift in Sources */, C2A16E551E117BD000585A85 /* CreateChannelViewController.swift in Sources */, + 9F7D422C222415C9007B68BB /* SharedAccountContext.swift in Sources */, + 27D4F6DF261B776200CCAE03 /* Notices.swift in Sources */, + 27BA045B26E51F55008FC1A3 /* AvatarContentView.swift in Sources */, + 9F1AE5E420B6D7AA002A9D8D /* LocationPlaceSuggestionRowItem.swift in Sources */, + A789E08423E05EAE00AEB34A /* ChatListFiltersListController.swift in Sources */, C2D187EE1E28B9CB0038961D /* ShareInlineResultNavigationAction.swift in Sources */, C226742B1DBE16B9000BA9ED /* CachedResourceRepresentations.swift in Sources */, + 9F354E9C2270630A006F1D42 /* HapticEngine.swift in Sources */, + 27A0A4E725F7764600B789AF /* GroupCallPeerAvatarRowItem.swift in Sources */, C2167E571DC221E600F98E03 /* PeerMediaRowContent.swift in Sources */, + D08E5C9822C583CE007B1C09 /* Tuple.swift in Sources */, + D0DD91FE246A8A380039D83D /* PeerMediaGifsController.swift in Sources */, + D0675D84217F1F27004900A7 /* ArchiverContext.swift in Sources */, C248BD261E706A05004B9106 /* RecentSessionsController.swift in Sources */, - C2DF47BA1DE79574003AA6C0 /* bitwise.c in Sources */, + 9FA0E53D205693DA001E5649 /* WebSessionsController.swift in Sources */, C250B01F1DB67B42004E9FBE /* WPLayout.swift in Sources */, + D0D7520924C03CD60037D73A /* VideoEditorThumbs.swift in Sources */, + 9F0367F22272108800456348 /* ProxyQRCodeRowItem.swift in Sources */, D098C7191D7E175A007784E4 /* AppDelegate.swift in Sources */, C2C73A5B1EEAF3AE00DB8420 /* ChannelEventFilterModalController.swift in Sources */, C2271DD21DAF6DF5001792B6 /* EmptyChatViewController.swift in Sources */, C23BC37E1E9BB28F00D79F92 /* AddContactModalController.swift in Sources */, + 9F3EAB4420A5ED2F003FE7E3 /* ocr.mm in Sources */, C2BB120A1ED87C5A00BDE46A /* ControllerExtension.swift in Sources */, + A7B5031323B62E1A00C9838E /* Svg.m in Sources */, + A767DD4123F2BB3200366F76 /* ShortcutListController.swift in Sources */, + 9F62AE81202D85E7007FB557 /* FetchManagerLocation.swift in Sources */, C230B91F1DD4BEBC0057F596 /* ChatGIFContentView.swift in Sources */, + A7E832192562CFC70095A167 /* VoiceBlobView.swift in Sources */, + 2760E55626970513001C59F8 /* WidgetStorageController.swift in Sources */, + D0D139252524A31E005FCF35 /* RepliesHeaderRowItem.swift in Sources */, C276248C1D95AF7600FE5B2B /* ObjcUtils.m in Sources */, + 276B5983261C79250029FD3F /* GroupCallTitleView.swift in Sources */, + 9F1668C82008F30900DD39FB /* ChatBackgroundView.swift in Sources */, + 9FA0E53F2056E159001E5649 /* WebAuthorizationRowItem.swift in Sources */, + D0276B7D22BD6511003155D8 /* DisplayLink.swift in Sources */, + 9F0B8F171FFB7F1A00073D3F /* AccentColorRowItem.swift in Sources */, C2DF47EE1DE9A1F3003AA6C0 /* LoginViewController.swift in Sources */, C2DE5D301F3CA7C00081EC1E /* InstantPageLinkSelectionView.swift in Sources */, + A742CE45241A517800C6B69B /* ChannelRecentPostRowItem.swift in Sources */, C2271DCA1DAED681001792B6 /* PresenceStrings.swift in Sources */, + D02BD7C5232D204F00D1814A /* ThemeListRowItem.swift in Sources */, + C224675D1FA884E300F03E27 /* ChatGroupedItem.swift in Sources */, C2271DD71DAF80D5001792B6 /* PeerMediaController.swift in Sources */, + 2764A5AA25DFB5B300F9A20D /* ReportDetailsController.swift in Sources */, + 9F1BABAE21E5ECE70075C03E /* ChatUndoManager.swift in Sources */, + A7F2830823954EF800742C20 /* CachedInstantPages.swift in Sources */, C2DF47E41DE84A67003AA6C0 /* ChatVoiceRowItem.swift in Sources */, C28BAB2C1DF9C2790027CE3A /* DateUtils.mm in Sources */, C2A5067B1DF5BE6900971A93 /* GeneralTextRowItem.swift in Sources */, C2A8E1481F7D2612000FD5E3 /* TGVideoCameraGLRenderer.m in Sources */, + D05F392322FB45450040F341 /* DateSelectorModalController.swift in Sources */, + D00746CA257691760000DF74 /* StickerShimmerEffectView.swift in Sources */, C209C38D1F276D4C009231FE /* ChatSearchView.swift in Sources */, + 2763A159261C91B000C12762 /* DatePickerRowItem.swift in Sources */, C2777B601DCF4766008B69DD /* CoreExtension.swift in Sources */, + A7029EF8240E3A5400A89ABD /* ChatListFiltersHeaderItem.swift in Sources */, C2A16E3E1E11674F00585A85 /* CreateGroupSignals.swift in Sources */, - C2C9B9371E8016D400380D79 /* SPMediaKeyTap.m in Sources */, + D06C7BB9247E664900E67C3C /* SoftwareVideoThumbnailLayer.swift in Sources */, C21B24631EDADC8600FC6CDA /* MMMenuItem.swift in Sources */, C2A71CE71DDB2F8700C69F73 /* UsernameSettingsViewController.swift in Sources */, - C2E064641ECCB24000387BB8 /* NativeCallSettingsViewController.swift in Sources */, - C21AAE341DB0F6BC007638C5 /* MediaTitleBarView.swift in Sources */, + 9F3EAB4620A5ED2F003FE7E3 /* genann.c in Sources */, + D0E3684624D80A0D009896D4 /* ClearCache.swift in Sources */, + D00CE50F2289CE87008C1B4F /* ChatAnimatedStickerItem.swift in Sources */, C230BEB41EE97B6F0029586C /* RestictedModalViewController.swift in Sources */, C22E063D1D80929400A11C88 /* ChatListRowItem.swift in Sources */, C20D5AB11DA996480042616A /* EBlockItem.swift in Sources */, + 9F17E5B9212F173900C25A65 /* AutoNightViewController.swift in Sources */, C2167E531DC220F600F98E03 /* PeerMediaFileRowContent.swift in Sources */, C22674451DC20664000BA9ED /* PeerMediaListController.swift in Sources */, + D076A07E248A5F890077BC0A /* GifPanelTabRowItem.swift in Sources */, + 9FB7CB6D221EB22700888EA9 /* CallSettingsModalController.swift in Sources */, C230B8F11DD348970057F596 /* AccountInfoItem.swift in Sources */, C2FBC1E71DC631980063A23B /* SPPreviewController.swift in Sources */, + D0ABA3F72549C37E00031678 /* MessageSharedRowItem.swift in Sources */, + 27FFCEC52694BA58006CA024 /* WidgetAppearance.swift in Sources */, C230B8EF1DD3358C0057F596 /* AccountViewController.swift in Sources */, + 9F4EEF8621D4FA68002C3B33 /* InstantPageArticleItem.swift in Sources */, + 9F18DD94206D8FFD00A2AAD0 /* SecureIdVerificationDocument.swift in Sources */, + 270202BF261A1BF500CFCE6D /* PaymentsTipsRowItem.swift in Sources */, + 9F7B741122296FD7006610E4 /* SharedNotificationManager.swift in Sources */, C25F714A1E4110C50046AF4E /* ApplicationSpecificPreferencesKeys.swift in Sources */, - C226743A1DC1273E000BA9ED /* PeerMediaGridController.swift in Sources */, + 9F7B740F2229618E006610E4 /* SharedWakeupManager.swift in Sources */, C24DABA31E083185005EE404 /* YTVimeoExtractorOperation.m in Sources */, + 9F10CE8E20617C36002DD61A /* PassportInsertPasswordItem.swift in Sources */, + 2725785B259CF987001558E8 /* AnimatedBadgeView.swift in Sources */, + D071E8AC214BE6E7001B6024 /* TouchBarScrubberHeaderItemView.swift in Sources */, C253E23E1DE61CB50022A29F /* ContextListRowItem.swift in Sources */, C2271DB11DAE126B001792B6 /* GeneralRowItem.swift in Sources */, C2A87DE81F4C6910002D3F73 /* InstantViewWindow.swift in Sources */, + 9F21F65520B5A72800332C85 /* LocationModalController.swift in Sources */, + 2783BA9526E24795004A1591 /* GroupCallVideoOrientationRowItem.swift in Sources */, + D00EECD1252B47B1001EB99F /* CallFeedbackController.swift in Sources */, + D0E7BED0256EA7140068644D /* GroupCallSettingsController.swift in Sources */, C2777B681DD20DD2008B69DD /* ChatTitleBarView.swift in Sources */, C246161B1ED33FFE0026D5BC /* InstantVideoPIP.swift in Sources */, + D0CBB0F52492974F00620C65 /* VideoAvatarModalController.swift in Sources */, + 2729712126135C94006D38E2 /* GroupCallInviteRowItem.swift in Sources */, + D0E3685824DAB066009896D4 /* VideoCallsConfiguration.swift in Sources */, + D0D81AF8243F69AD00CB9D20 /* PollTimerView.swift in Sources */, C26A37EC1E5DE465006977AC /* ChannelAdminsViewController.swift in Sources */, C29C3E5B1E421F1700193A7E /* ChatAccessoryModel.swift in Sources */, C2BB2DB01F8BDF6700520255 /* Config.swift in Sources */, + 9FC4DA9A21DD0BE6003E2A62 /* CachedChannelAdmins.swift in Sources */, + 9F4EEF7E21D3C3E3002C3B33 /* InstantPageDetailsItem.swift in Sources */, C2A315B11E2E656100D89000 /* CalendarMonthController.swift in Sources */, C275932E1DF6E1CE00A0807A /* AboutModalController.swift in Sources */, + 9F1C279321D38A96003CD033 /* InstantPageScrollableItem.swift in Sources */, + 279A1E2825E945A2007D48E7 /* PaymentsReceiptController.swift in Sources */, C2271F591D9D46CA00424F7B /* ShortPeerRowView.swift in Sources */, + D0530D4224E69459003273BC /* CallTooltipView.swift in Sources */, + C251FB4C1FEDCC750035E5D7 /* ChatPresentationUtils.swift in Sources */, C29B5F411DC7859800D13E65 /* InputContextHelper.swift in Sources */, + 9F7B74052227F4F2006610E4 /* SharedAccountInfo.swift in Sources */, C24FD40E1F20EE8B00A97196 /* ContextClueRowItem.swift in Sources */, C29A65051E0845B80071BCEF /* ExternalVideoLoader.swift in Sources */, + 2713B221260A42FE00CE0EC6 /* MediaTrackFrame.swift in Sources */, C2FD33EE1E697B31008D13D4 /* TransformOutgoingMessageMedia.swift in Sources */, + D0558D852152B4D3006B403D /* GalleryTouchBar.swift in Sources */, C253A9451D90303200CDC850 /* FastBlur.m in Sources */, + 27335F0325C8295200FD040C /* GroupVideoView.swift in Sources */, + D0D3CE6C23D465FA00864F3C /* PollResultStickItem.swift in Sources */, C2057FAA1EBC6A3C000423DC /* ChatCallRowItem.swift in Sources */, C253E23A1DE4D3DB0022A29F /* ChatInterfaceInputContext.swift in Sources */, C24D9FC41E24FFF3002CD3F3 /* PasscodeLockController.swift in Sources */, + 9F10CE942061C8C8002DD61A /* InputDataControllerEntries.swift in Sources */, + A71DC82F2386AADF000EEDE2 /* Signature.swift in Sources */, + D02BD7C0232D0FB800D1814A /* AppAppearanceViewController.swift in Sources */, + 27C4089425B081B500372302 /* FireTimerControl.swift in Sources */, C2A315AF1E2E64BE00D89000 /* CalendarController.swift in Sources */, C296AF8A1D8E9F94001DBB59 /* ChatInteractiveContentView.swift in Sources */, C2271F2B1DB3BEB60045E719 /* GlobalHandlers.swift in Sources */, + C224675B1FA8546200F03E27 /* GroupedLayout.swift in Sources */, C230BEBA1EE9AEB80029586C /* ChannelEventLogItem.swift in Sources */, - C26A37EE1E5DE48F006977AC /* ChannelBlacklistViewController.swift in Sources */, + C26A37EE1E5DE48F006977AC /* ChannelBlocklistViewController.swift in Sources */, C205DB981EE71127003711DF /* ChannelAdminController.swift in Sources */, + D0E7BD73256AA6B50068644D /* PresentationGroupCallManager.swift in Sources */, C22674381DC125C1000BA9ED /* PeerMediaCollectionInterfaceState.swift in Sources */, C2271F3C1DB4D0630045E719 /* GIFViewController.swift in Sources */, C2777B6A1DD211E5008B69DD /* PeerPresenceStatusManager.swift in Sources */, C2303E781D997C3100098E12 /* ChatInputAttachView.swift in Sources */, C296707B1F0FBFB500884DA2 /* ThemeSettings.swift in Sources */, C248BD241E706104004B9106 /* BlockedPeersViewController.swift in Sources */, + D0558D7C215141CF006B403D /* ChatInfoTouchbar.swift in Sources */, + D0558D872154047E006B403D /* GalleryTouchBarThumbItemView.swift in Sources */, C230B9111DD3866A0057F596 /* ComposeActions.swift in Sources */, + 9F3D5F6322044D8700CB0CAA /* AppUpdateViewController.swift in Sources */, C24DAB861E08026C005EE404 /* MGalleryVideoItem.swift in Sources */, + 9F9483B1202AF816006E873D /* CrashHandler.swift in Sources */, C2DE5D3C1F3CAE120081EC1E /* InstantPageTextItem.swift in Sources */, + D0A75F2A24487A1E001F84A0 /* EditImageCanvasControls.swift in Sources */, C221ED5A1EA6896400471C65 /* VoiceCallSettings.swift in Sources */, C253E23C1DE4D4080022A29F /* ChatInterfaceStateContextQueries.swift in Sources */, + D0E7BD7D256BC9460068644D /* GroupCallParticipantRowItem.swift in Sources */, C253E22E1DE33A7C0022A29F /* ChatMusicRowItem.swift in Sources */, + 27F5F23C25E7CCD700E8AC69 /* PaymentsCheckoutPriceItem.swift in Sources */, + D09D9DF1229C27A700378796 /* AnimatedStickerUtils.swift in Sources */, + 9F7943B220855DC200FEDB81 /* ProxyListController.swift in Sources */, + 27E5997A261B3F5800228411 /* CurrencyFormatter.swift in Sources */, + 27C4089725B0A1F400372302 /* ClosureInviteLinkController.swift in Sources */, + 273274E626A818AE00E04289 /* WallpaperColorPickerContainerView.swift in Sources */, C296AF8C1D8EA7F3001DBB59 /* ChatFileContentView.swift in Sources */, C219E1FC1D8D35FA0042F0C8 /* ChatMediaItem.swift in Sources */, - C21178001F16BB8300AC706D /* BioViewController.swift in Sources */, + 278FA2E925E8C36A00280629 /* PaymentsPaymentMethodController.swift in Sources */, + D0E82E002500F10E00E09A20 /* MergedAvatarsView.swift in Sources */, C234D4121EEDE6990017DC25 /* LoadingTableItem.swift in Sources */, - C230B8F61DD371430057F596 /* SPopoverViewController.swift in Sources */, - C22674331DBF665A000BA9ED /* EStickerPackEntries.swift in Sources */, - C2FD33F81E6C1486008D13D4 /* SSKeychain.m in Sources */, + 9F1962D82101458C00FFF048 /* VCardLocationRowItem.swift in Sources */, + 27C4F5DE26B3F62E008123EC /* VideoMessageConfig.swift in Sources */, + 9FC8ADA42063B6450094F7B4 /* PassportTwoStepVerificationIntroItem.swift in Sources */, + 9F4EEF8221D4F584002C3B33 /* ImageTransparency.swift in Sources */, + D00EECD7252CD2EF001EB99F /* MicrophonePreviewRowItem.swift in Sources */, C230B9221DD4C24E0057F596 /* GIFPlayerView.swift in Sources */, C2CBCAC51D81649E00142EC0 /* ChatListRowView.swift in Sources */, + 27949114260A41EB0003BFA0 /* FFMpegMediaPassthroughVideoFrameDecoder.swift in Sources */, C29E0EE01F4DC43100C0C7A8 /* InstantViewAppearance.swift in Sources */, + 270A262A269D8F1000B31B2B /* WidgetStickersController.swift in Sources */, C20232A81D81D189007C9ADE /* ChatController.swift in Sources */, + D00C73B62302C196004B1E2B /* ChatScheduleController.swift in Sources */, + D0C39EC5233A6077003CD402 /* GeneralBlockTextRowItem.swift in Sources */, C26E82D11E83EFFE0046DF2F /* TimeObserver.m in Sources */, C253A96D1D92F69C00CDC850 /* ReplyModel.swift in Sources */, + D076F891229823DA004F895A /* ChannelDiscussionInputView.swift in Sources */, + 9F72974020BD9C6A0067F815 /* VideoPlayer.swift in Sources */, + A7F283002395168300742C20 /* SettingsSearchRecentQueries.swift in Sources */, + 9F10CE8A20611536002DD61A /* PassportHeaderItem.swift in Sources */, C29C3E731E4397F300193A7E /* StickerPreviewHandler.swift in Sources */, + D04D213F230D68B800609388 /* CustomAccentColorModalController.swift in Sources */, C29A65071E098B610071BCEF /* ShareModalController.swift in Sources */, - C2DF47BF1DE79574003AA6C0 /* opusenc.m in Sources */, + C2E8BA0A1FB5F15900DEB5E2 /* GalleryThumbsControlView.swift in Sources */, C2DF47E21DE846B8003AA6C0 /* ChatMusicContentView.swift in Sources */, + 279A1E0D25E90D13007D48E7 /* PaymentWebInteractionController.swift in Sources */, C2271F381DB4D0490045E719 /* EmojiViewController.swift in Sources */, + D071E8A421496F38001B6024 /* ChatListTouchBar.swift in Sources */, + 2775102125E7917F003D58D1 /* PaymentsCheckoutController.swift in Sources */, + A7C7215723FD45D300CE3F75 /* SaveModalController.swift in Sources */, + 9F147F6F223014EB00D71BD1 /* PasscodeControllers.swift in Sources */, C2271DAA1DAD8716001792B6 /* PeerInfoController.swift in Sources */, + D001E978243B4639009025F9 /* LeftSidebarFolderItem.swift in Sources */, C22B635C1D96A14E00085C19 /* ChatInputView.swift in Sources */, + 2728991C26CBCDA900F4D288 /* TurnOnNotificationsRowItem.swift in Sources */, + D076F8872296CAB2004F895A /* ChannelDisscussionGroup.swift in Sources */, + 27F5F23F25E7DD8300E8AC69 /* PaymentsShippingInfoController.swift in Sources */, + 27949112260A41EB0003BFA0 /* FFMpegAudioFrameDecoder.swift in Sources */, + A7F2830A2395570400742C20 /* SearchSettingsController.swift in Sources */, C253E2341DE3776A0022A29F /* InlineAudioPlayerView.swift in Sources */, C26D8A3C1E464944002FAA3F /* JoinLinkPreviewModalController.swift in Sources */, C2E52A0D1EB8C386009AF87D /* SelectivePrivacySettingsController.swift in Sources */, - C21656E81EE576FA0041A6BA /* ChatUserPopover.swift in Sources */, + 9F13EE172100B05300562E53 /* VCardContactController.swift in Sources */, + 9FFAE502205A9154000C028E /* ManagedAudioSession.swift in Sources */, C2AC9C181E1E687E0085C7DE /* GlobalBadgeNode.swift in Sources */, + 270202C6261B3D7C00CFCE6D /* UITextField.swift in Sources */, + 27FFCEC72694BC95006CA024 /* WidgetController.swift in Sources */, C275E9F51F8FEDEB00D3D8C0 /* PhoneNumberConfirmController.swift in Sources */, C2DE5D281F3CA5FE0081EC1E /* InstantPageItem.swift in Sources */, - C2084F041F5D5C6F004713C4 /* ChatReplyPreviewController.swift in Sources */, - C226741F1DBCEAC2000BA9ED /* EStickerGridEntries.swift in Sources */, - C2FD33F91E6C1486008D13D4 /* SSKeychainQuery.m in Sources */, C2DF47961DE71160003AA6C0 /* GIFContainerView.swift in Sources */, - C2271F4F1D9C38F500424F7B /* SPopoverRowItem.swift in Sources */, C2FD34151E6C9003008D13D4 /* BaseApplicationSettings.swift in Sources */, + C27BAC7A20CFCE68007A7508 /* PassportSettingsHeaderItem.swift in Sources */, + D076F8702295B24D004F895A /* InlineAuthOptionRowItem.swift in Sources */, C253E2261DE335C10022A29F /* ChatVoiceContentView.swift in Sources */, + A7377E1B23ACC79100AD3ADD /* ChatNavigateFailed.swift in Sources */, C2271DB71DAE132C001792B6 /* GeneralInteractedRowView.swift in Sources */, C218FF921F3CC8C700DD7D35 /* InstantPageWebEmbedItem.swift in Sources */, C2F9C44C1F95FE58002B2CBF /* Markdown.swift in Sources */, - C2F9C44A1F9500C3002B2CBF /* TwoStepVerificationUnlockStructures.swift in Sources */, + 9F62AE7E202D85B7007FB557 /* FetchManager.swift in Sources */, C2777B661DCFC649008B69DD /* Localizable.swift in Sources */, C24D9F911E1F8F85002CD3F3 /* MajorBackNavigationBar.swift in Sources */, C2A16E411E1167DA00585A85 /* ChannelIntroViewController.swift in Sources */, C201C2321E3B2D1C0026C21E /* FastSettings.swift in Sources */, C2A315B41E2E65C600D89000 /* CalendarUtils.m in Sources */, + A72D7AE923C471A7005BAC59 /* PollResultController.swift in Sources */, C2DE5D361F3CACCF0081EC1E /* InstantPageLayoutSpacings.swift in Sources */, C2423A541F2235080041907F /* InstantPageViewController.swift in Sources */, C221ED541EA684BE00471C65 /* DataAndStorageViewController.swift in Sources */, - C2AF3B821E5CD79200DFDD81 /* ConvertGroupViewController.swift in Sources */, + 9F3EAB3B20A5A1EC003FE7E3 /* NetworkUsageStatsController.swift in Sources */, + A766493F236D6BFD00163DF4 /* PasscodeSettings.swift in Sources */, + 9FBE0EE1201FBEFC0060FD1C /* DownloadSettingsViewController.swift in Sources */, + 9F52F51A2130286E006FC0B5 /* LocationRequest.swift in Sources */, + A7ADC47A2587B3750069737A /* VoiceChatActionButton.swift in Sources */, C2B1B11E1E5F151D00895E0D /* ChannelVisibilityController.swift in Sources */, + A71DC82D23858356000EEDE2 /* Spotlight.swift in Sources */, C205DB9D1EE88765003711DF /* Views.swift in Sources */, C27AAFE91DE9DA61009B9629 /* CountryManager.swift in Sources */, C253A9611D917ACD00CDC850 /* ChatStickerContentView.swift in Sources */, + A7B5031023B62E0400C9838E /* nanosvg.c in Sources */, + C2E6F3CF1F9F85260023653D /* ContextHashtagRowItem.swift in Sources */, C276248E1D95B4F300FE5B2B /* Extensions.swift in Sources */, + D06C69552539F6BD00DD9005 /* LiveLocationViewController.swift in Sources */, C27AAFEF1DEB2EA9009B9629 /* EmptyComposeController.swift in Sources */, + 9F1890932238F3DC00665EF5 /* DownloadedFilesPaths.swift in Sources */, + 2760E55B269706D7001C59F8 /* WidgetView.swift in Sources */, C22B63591D967F4500085C19 /* TGModernGrowingTextView.m in Sources */, + 27A9904A257A7435009044DB /* SoundEffectPlayQueue.swift in Sources */, C2B4D0C11E34E07100CBC4E6 /* PrettyGridUtils.swift in Sources */, C27A8E991EC9D48000C8E7E7 /* QuickSwitcherModalController.swift in Sources */, C230BEB21EE97B290029586C /* ChannelEventLogController.swift in Sources */, C2777B5B1DCE11A5008B69DD /* SendingClockProgress.swift in Sources */, + C2905E1E207E545600990AD7 /* InstantPageAudioItem.swift in Sources */, + D074A56724A1DF5200E92F8A /* VoipDerivedState.swift in Sources */, C2F93A2D1F3C55C500BCD48F /* EmojiToleranceController.swift in Sources */, + D071E8A6214A805C001B6024 /* ChatTouchBar.swift in Sources */, + D0530D4024E693A5003273BC /* CameraViews.swift in Sources */, C2B1A1271DA3D84900ACB1DD /* ChatInputAccessory.swift in Sources */, + 27E434142680C57900B05CB1 /* CoreMediaVideoTest.swift in Sources */, + 27C4088E25B0603700372302 /* GeneralLoadingRowItem.swift in Sources */, + A76C8AA02422132400FDB071 /* VerticalTabsView.swift in Sources */, + 276A1D59265D0E600083D6E7 /* GroupCallSpeakingTooltipView.swift in Sources */, + 27B15D2F259B2DA700C7F280 /* DesktopCaptureListUI.swift in Sources */, C218FF961F3DAD7200DD7D35 /* InstantPageSelectText.swift in Sources */, - C25253271DF03F5700ADBC98 /* TGOpusAudioRecorder.m in Sources */, C236ADDE1F7D318700E8C71A /* TGVideoCameraMovieRecorder.m in Sources */, C2DE5D3E1F3CAF2B0081EC1E /* InstantPageShapeItem.swift in Sources */, + 9F18908D2237B5A400665EF5 /* InputURLFormatterModalController.swift in Sources */, C221ED5C1EA69AA300471C65 /* StorageUsageController.swift in Sources */, + 27E59980261B404000228411 /* CurrencyLocale.swift in Sources */, + D0ABA3F325499C5A00031678 /* MessageStatsController.swift in Sources */, C2FD382E1DCA1FA3009DC28C /* PreviewSenderController.swift in Sources */, C281498A1EA7F44300BB933E /* ListViewIntermediateState.swift in Sources */, + D00B191E24E54F20006CCB87 /* CallStatusView.swift in Sources */, + 9F14CBF32007DEB300F22DA9 /* ChatWallpaperModalController.swift in Sources */, + 9F262D5F21BFD5BC006817CD /* LocalizationPreviewModalController.swift in Sources */, + 276B5989261C7AC60029FD3F /* GroupCallSchedule.swift in Sources */, C2FD38321DCA215F009DC28C /* GeneralInputRow.swift in Sources */, C2FF14601E532C0A007B7B14 /* SearchEmptyRowItem.swift in Sources */, C2167E5F1DC25C6900F98E03 /* InterfaceObserver.swift in Sources */, - C29C3E711E43881500193A7E /* StickerPreviewModalController.swift in Sources */, + C29C3E711E43881500193A7E /* ModalPreviewViews.swift in Sources */, + 9F7B74032227F492006610E4 /* ManageSharedAccountInfo.swift in Sources */, + D0C550C4251127DA00B64966 /* ChatCommentsHeaderItem.swift in Sources */, + A7F282B2238D122900742C20 /* UnauthorizedConfiguration.swift in Sources */, + 9FC4DA9C21DD187C003E2A62 /* SearchPeerMembers.swift in Sources */, + 2713B220260A42FE00CE0EC6 /* MediaTrackDecodableFrame.swift in Sources */, C22E06321D8044CC00A11C88 /* ChatListController.swift in Sources */, - C2DF47C11DE79574003AA6C0 /* wav_io.c in Sources */, - C24949121E5B704900D7ED5D /* AccountsListViewController.swift in Sources */, + 2773A53A25FA1D9D00AB45E9 /* JoinVoiceChatAlertController.swift in Sources */, + C246D6281FAB72D4004C17FA /* MediaGroupPreviewRowItem.swift in Sources */, + 9F18DD93206D8FFD00A2AAD0 /* SecureIdVerificationDocumentsContext.swift in Sources */, + 9F8DF3C8209228B000AED104 /* EditAccountInfoController.swift in Sources */, + C241025D1FD5702D00DB8625 /* ChatMessageBubbleImages.swift in Sources */, + 9FC8AD9A2062A5610094F7B4 /* InputDataDataSelectorRowItem.swift in Sources */, + D004167722D4AD3B0000566B /* StickerPackItems.swift in Sources */, + 27949111260A41EB0003BFA0 /* FFMpegMediaFrameSourceContext.swift in Sources */, + 9F14CBF52007DFD400F22DA9 /* Wallpapers.swift in Sources */, + A7E831F3255041020095A167 /* IsEqualMessages.swift in Sources */, + D06C7BB5247D6F4B00E67C3C /* SoftwareVideoLayerFrameManager.swift in Sources */, + D0CC4ADE22BA5C930088F36D /* LottieBufferCompressor.swift in Sources */, + C241026F1FD58EA900DB8625 /* SImageView.swift in Sources */, + D06C7BBB247FAE3E00E67C3C /* GifPlayerBufferView.swift in Sources */, C24D9FC71E2500AC002CD3F3 /* PasscodeSettingsViewController.swift in Sources */, + D00B318725729F0500D62056 /* DataItem.swift in Sources */, + 9F3EAB4720A5ED2F003FE7E3 /* fast-edge.cpp in Sources */, + A7E831F0255040B70095A167 /* ChatPresentationInputQueryResult.swift in Sources */, + 9F6314E121CAA0AB009FD379 /* NewPollController.swift in Sources */, C2DE5D2A1F3CA69D0081EC1E /* InstantPageMedia.swift in Sources */, C2DE5D2E1F3CA72E0081EC1E /* InstantPageView.swift in Sources */, C2FB2FAB1EBF73D00093C8BA /* RecentCallsViewController.swift in Sources */, - C22674211DBCECCC000BA9ED /* EStickerGridItem.swift in Sources */, - C219E1D91D886A160042F0C8 /* ChatHoleRowView.swift in Sources */, C26505951E0301B4001954DC /* MGalleryItem.swift in Sources */, C2777B641DCFB559008B69DD /* ChatServiceRowView.swift in Sources */, C2303E8A1D9A76D800098E12 /* MainViewController.swift in Sources */, - C22674351DBF6A85000BA9ED /* EStickerPackItem.swift in Sources */, - C2271DC41DAE5E46001792B6 /* PeerInfoHeaderView.swift in Sources */, + 9F1BC1A9223FDE6D00F21815 /* InputSources.swift in Sources */, C2E8694D1F43500D00BDD0A2 /* ChatNavigationMention.swift in Sources */, + 9F63152621D236CB009FD379 /* ForgotPasswordController.swift in Sources */, C2B1A1361DA6587100ACB1DD /* GalleryViewer.swift in Sources */, + 9FC4DA9821DD0B86003E2A62 /* ChannelMemberCategoryListContext.swift in Sources */, + 9F1AE5E620B70328002A9D8D /* LocationSendCurrentItem.swift in Sources */, + A7388432257E6E0C002E8424 /* GroupCallNavigationHeaderView.swift in Sources */, C253E2311DE34FBB0022A29F /* AudioPlayerController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + D032AFA72578190600E67215 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = C232EA901E1D07E700C4D38C /* TelegramShare */; + targetProxy = D032AFA62578190600E67215 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ C232EA991E1D07E700C4D38C /* ShareViewController.xib */ = { isa = PBXVariantGroup; @@ -3557,27 +6217,194 @@ C21656CC1EE1CC760041A6BA /* it */, C21656CD1EE1CC7C0041A6BA /* de */, C21656CE1EE1CC830041A6BA /* es */, + 9F13CCE22006719400ECF301 /* ru */, + 9F13CCE3200671AF00ECF301 /* uk */, ); name = Localizable.strings; sourceTree = ""; }; - D098C71C1D7E175A007784E4 /* MainMenu.xib */ = { - isa = PBXVariantGroup; - children = ( - C25692971EDD85FB009EE421 /* en */, - C21656D31EE4A83C0041A6BA /* es */, - C21656D51EE4A8400041A6BA /* de */, - C21656D61EE4A8430041A6BA /* it */, - C21656D81EE4A8470041A6BA /* nl */, - C21656DA1EE4A8490041A6BA /* pt-BR */, - ); - name = MainMenu.xib; - sourceTree = ""; + D098C71C1D7E175A007784E4 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + C25692971EDD85FB009EE421 /* en */, + C21656D31EE4A83C0041A6BA /* es */, + C21656D51EE4A8400041A6BA /* de */, + C21656D61EE4A8430041A6BA /* it */, + C21656D81EE4A8470041A6BA /* nl */, + C21656DA1EE4A8490041A6BA /* pt-BR */, + 9F52E74E2074C12B0048791C /* Base */, + 9F52E7502074C16D0048791C /* ru */, + 9F52E7522074C1710048791C /* uk */, + ); + name = MainMenu.xib; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + A71DC8392386D6C2000EEDE2 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + 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_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_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + }; + name = Github; + }; + A71DC83A2386D6C2000EEDE2 /* Github */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C21074251E77F79A006EE5EF /* Beta.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ARCHS = "$(ARCHS_STANDARD)"; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = NO; + CLANG_WARN_SUSPICIOUS_MOVE = NO; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = "Telegram-Mac/Telegram-Mac.entitlements"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_INJECT_BASE_ENTITLEMENTS = YES; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 220805; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = N8RBWJ5X6J; + ENABLE_BITCODE = NO; + ENABLE_HARDENED_RUNTIME = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + "$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks", + "$(PROJECT_DIR)/HockeySDK-Mac", + "$(PROJECT_DIR)/submodules", + "$(PROJECT_DIR)/submodules/AppCenter", + ); + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "GITHUB=1", + "TGVOIP_USE_CUSTOM_CRYPTO=1", + "WEBRTC_MAC=1", + "HAVE_SCTP=1", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = NO; + GCC_WARN_ABOUT_INVALID_OFFSETOF_MACRO = NO; + GCC_WARN_ABOUT_POINTER_SIGNEDNESS = NO; + HEADERMAP_USES_VFS = YES; + HEADER_SEARCH_PATHS = ( + "$(PROJECT_DIR)/submodules/libtgvoip", + "$(PROJECT_DIR)/thrid-party", + ); + INFOPLIST_FILE = "Telegram-Mac/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/thrid-party/libwebp/lib", + "$(SDKROOT)/usr/lib/system", + "$(PROJECT_DIR)/thrid-party/opus/lib", + "$(PROJECT_DIR)/thrid-party/ffmpeg/lib", + "$(PROJECT_DIR)/thrid-party/libjpeg-turbo", + "$(PROJECT_DIR)", + "$(PROJECT_DIR)/thrid-party/openssl/lib", + "$(PROJECT_DIR)/core-xprojects/libwebp/build/libwebp/lib", + "$(PROJECT_DIR)/core-xprojects/libopus/build/libopus/lib", + "$(PROJECT_DIR)/core-xprojects/webrtc/build/webrtc", + "$(PROJECT_DIR)/core-xprojects/ffmpeg/build/ffmpeg/lib", + "$(PROJECT_DIR)/core-xprojects/OpenSSLEncryption/build/openssl/lib", + ); + MACOSX_DEPLOYMENT_TARGET = 10.11; + MARKETING_VERSION = 8.1.1; + ONLY_ACTIVE_ARCH = NO; + OTHER_CODE_SIGN_FLAGS = ""; + OTHER_LDFLAGS = "-ObjC"; + OTHER_SWIFT_FLAGS = "-Xfrontend -debug-time-function-bodies -D GITHUB"; + PRODUCT_BUNDLE_IDENTIFIER = ru.keepcoder.Telegram; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_COMPILATION_MODE = singlefile; + SWIFT_ENFORCE_EXCLUSIVE_ACCESS = off; + SWIFT_OBJC_BRIDGING_HEADER = "Telegram-Mac/Telegram-Mac-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_REFLECTION_METADATA_LEVEL = none; + SWIFT_VERSION = 5.0; + }; + name = Github; + }; + A71DC83B2386D6C2000EEDE2 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = TelegramShare/TelegramShare.entitlements; + CODE_SIGN_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 209499; + DEVELOPMENT_TEAM = N8RBWJ5X6J; + ENABLE_HARDENED_RUNTIME = YES; + FRAMEWORK_SEARCH_PATHS = ""; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "SHARE=1", + "BETA=1", + ); + INFOPLIST_FILE = TelegramShare/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MARKETING_VERSION = 8.1.1; + ONLY_ACTIVE_ARCH = NO; + OTHER_CODE_SIGN_FLAGS = ""; + OTHER_SWIFT_FLAGS = "-D DEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = ru.keepcoder.Telegram.TelegramShare; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = SHARE; + SWIFT_OBJC_BRIDGING_HEADER = "Telegram-Mac/Telegram-Mac-Bridging-Header.h"; + SWIFT_VERSION = 4.2; + VALID_ARCHS = "arm64 x86_64"; + }; + name = Github; }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - C232EA251E1BC1A100C4D38C /* Release AppStore */ = { + C232EA251E1BC1A100C4D38C /* ReleaseAppStore */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -3613,136 +6440,196 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.10; + MACOSX_DEPLOYMENT_TARGET = 10.11; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; }; - name = "Release AppStore"; + name = ReleaseAppStore; }; - C232EA261E1BC1A100C4D38C /* Release AppStore */ = { + C232EA261E1BC1A100C4D38C /* ReleaseAppStore */ = { isa = XCBuildConfiguration; baseConfigurationReference = C21074261E77F7A3006EE5EF /* Release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ARCHS = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; CLANG_ENABLE_MODULES = YES; + CLANG_USE_OPTIMIZATION_PROFILE = NO; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = NO; CLANG_WARN_SUSPICIOUS_MOVE = NO; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_ENTITLEMENTS = "Telegram-Mac/Telegram-Mac.entitlements"; - CODE_SIGN_IDENTITY = "Mac Developer"; + CODE_SIGN_ENTITLEMENTS = "Telegram-Mac/Telegram-Sandbox.entitlements"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 6N38VWS5BX; + CURRENT_PROJECT_VERSION = 220805; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = N8RBWJ5X6J; + ENABLE_BITCODE = NO; + ENABLE_HARDENED_RUNTIME = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)", + "$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks", + "$(PROJECT_DIR)/HockeySDK-Mac", + "$(PROJECT_DIR)/submodules", + "$(PROJECT_DIR)/submodules/AppCenter", + ); + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_OPTIMIZATION_LEVEL = s; + GCC_PREPROCESSOR_DEFINITIONS = ( + "APP_STORE=1", + "TGVOIP_USE_CUSTOM_CRYPTO=1", + TGVOIP_NO_OSX_PRIVATE_API, + "WEBRTC_MAC=1", + "HAVE_SCTP=1", ); - GCC_PREPROCESSOR_DEFINITIONS = "BETA=1"; GCC_WARN_64_TO_32_BIT_CONVERSION = NO; GCC_WARN_ABOUT_INVALID_OFFSETOF_MACRO = NO; GCC_WARN_ABOUT_POINTER_SIGNEDNESS = NO; HEADERMAP_USES_VFS = YES; HEADER_SEARCH_PATHS = ( - "$(PROJECT_DIR)/thrid-party/ogg", "$(PROJECT_DIR)/submodules/libtgvoip", + "$(PROJECT_DIR)/thrid-party", ); INFOPLIST_FILE = "Telegram-Mac/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Telegram-Mac/libwebp/lib", + "$(PROJECT_DIR)/thrid-party/libwebp/lib", "$(SDKROOT)/usr/lib/system", "$(PROJECT_DIR)/thrid-party/opus/lib", + "$(PROJECT_DIR)/thrid-party/ffmpeg/lib", + "$(PROJECT_DIR)/thrid-party/libjpeg-turbo", + "$(PROJECT_DIR)", + "$(PROJECT_DIR)/thrid-party/openssl/lib", + "$(PROJECT_DIR)/core-xprojects/libwebp/build/libwebp/lib", + "$(PROJECT_DIR)/core-xprojects/libopus/build/libopus/lib", + "$(PROJECT_DIR)/core-xprojects/webrtc/build/webrtc", + "$(PROJECT_DIR)/core-xprojects/ffmpeg/build/ffmpeg/lib", + "$(PROJECT_DIR)/core-xprojects/OpenSSLEncryption/build/openssl/lib", ); MACOSX_DEPLOYMENT_TARGET = 10.11; + MARKETING_VERSION = 8.1.1; + ONLY_ACTIVE_ARCH = NO; OTHER_CODE_SIGN_FLAGS = ""; + OTHER_LDFLAGS = "-ObjC"; OTHER_SWIFT_FLAGS = "-Xfrontend -debug-time-function-bodies -D APP_STORE"; PRODUCT_BUNDLE_IDENTIFIER = ru.keepcoder.Telegram; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = ""; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_ENFORCE_EXCLUSIVE_ACCESS = off; SWIFT_OBJC_BRIDGING_HEADER = "Telegram-Mac/Telegram-Mac-Bridging-Header.h"; - SWIFT_VERSION = 4.0; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_REFLECTION_METADATA_LEVEL = none; + SWIFT_VERSION = 5.0; }; - name = "Release AppStore"; + name = ReleaseAppStore; }; - C232EAA11E1D07E700C4D38C /* Debug Hockeyapp */ = { + C232EAA11E1D07E700C4D38C /* DebugHockeyapp */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = TelegramShare/TelegramShare.entitlements; CODE_SIGN_IDENTITY = ""; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 6N38VWS5BX; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/submodules/libtgvoip/build/Debug-iphoneos", - "$(PROJECT_DIR)", + CURRENT_PROJECT_VERSION = 209499; + DEVELOPMENT_TEAM = N8RBWJ5X6J; + ENABLE_HARDENED_RUNTIME = YES; + FRAMEWORK_SEARCH_PATHS = ""; + GCC_OPTIMIZATION_LEVEL = s; + GCC_PREPROCESSOR_DEFINITIONS = ( + "SHARE=1", + "BETA=1", ); INFOPLIST_FILE = TelegramShare/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.11; + MARKETING_VERSION = 8.1.1; + ONLY_ACTIVE_ARCH = NO; + OTHER_CODE_SIGN_FLAGS = ""; + OTHER_SWIFT_FLAGS = "-D BETA"; PRODUCT_BUNDLE_IDENTIFIER = ru.keepcoder.Telegram.TelegramShare; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; - SKIP_INSTALL = YES; + SKIP_INSTALL = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = SHARE; SWIFT_OBJC_BRIDGING_HEADER = "Telegram-Mac/Telegram-Mac-Bridging-Header.h"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; + VALID_ARCHS = "arm64 x86_64"; }; - name = "Debug Hockeyapp"; + name = DebugHockeyapp; }; - C232EAA21E1D07E700C4D38C /* Debug AppStore */ = { + C232EAA21E1D07E700C4D38C /* DebugAppStore */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = TelegramShare/TelegramShare.entitlements; - CODE_SIGN_IDENTITY = ""; + CODE_SIGN_IDENTITY = "Apple Development"; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 6N38VWS5BX; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/submodules/libtgvoip/build/Debug-iphoneos", - "$(PROJECT_DIR)", + CURRENT_PROJECT_VERSION = 209499; + DEVELOPMENT_TEAM = N8RBWJ5X6J; + ENABLE_HARDENED_RUNTIME = YES; + FRAMEWORK_SEARCH_PATHS = ""; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "SHARE=1", + "DEBUG=1", ); INFOPLIST_FILE = TelegramShare/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.11; + MARKETING_VERSION = 8.1.1; + ONLY_ACTIVE_ARCH = YES; + OTHER_CODE_SIGN_FLAGS = ""; + OTHER_SWIFT_FLAGS = "-D DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = ru.keepcoder.Telegram.TelegramShare; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - SKIP_INSTALL = YES; + SKIP_INSTALL = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = SHARE; SWIFT_OBJC_BRIDGING_HEADER = "Telegram-Mac/Telegram-Mac-Bridging-Header.h"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; + VALID_ARCHS = "arm64 x86_64"; }; - name = "Debug AppStore"; + name = DebugAppStore; }; - C232EAA31E1D07E700C4D38C /* Release AppStore */ = { + C232EAA31E1D07E700C4D38C /* ReleaseAppStore */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = TelegramShare/TelegramShare.entitlements; - CODE_SIGN_IDENTITY = "Mac Developer"; + CODE_SIGN_IDENTITY = ""; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 6N38VWS5BX; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/submodules/libtgvoip/build/Debug-iphoneos", - "$(PROJECT_DIR)", + CURRENT_PROJECT_VERSION = 209499; + DEVELOPMENT_TEAM = N8RBWJ5X6J; + ENABLE_HARDENED_RUNTIME = YES; + FRAMEWORK_SEARCH_PATHS = ""; + GCC_OPTIMIZATION_LEVEL = s; + GCC_PREPROCESSOR_DEFINITIONS = ( + "SHARE=1", + "APPSTORE=1", ); INFOPLIST_FILE = TelegramShare/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.11; + MARKETING_VERSION = 8.1.1; + ONLY_ACTIVE_ARCH = NO; + OTHER_CODE_SIGN_FLAGS = ""; OTHER_SWIFT_FLAGS = "-D APP_STORE"; PRODUCT_BUNDLE_IDENTIFIER = ru.keepcoder.Telegram.TelegramShare; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - SKIP_INSTALL = YES; + SKIP_INSTALL = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = SHARE; SWIFT_OBJC_BRIDGING_HEADER = "Telegram-Mac/Telegram-Mac-Bridging-Header.h"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; + VALID_ARCHS = "arm64 x86_64"; }; - name = "Release AppStore"; + name = ReleaseAppStore; }; - C248BD1D1E6ECB24004B9106 /* Release Hockeyapp */ = { + C248BD1D1E6ECB24004B9106 /* ReleaseHockeyapp */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -3778,20 +6665,21 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.10; + MACOSX_DEPLOYMENT_TARGET = 10.11; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; }; - name = "Release Hockeyapp"; + name = ReleaseHockeyapp; }; - C248BD1E1E6ECB24004B9106 /* Release Hockeyapp */ = { + C248BD1E1E6ECB24004B9106 /* ReleaseHockeyapp */ = { isa = XCBuildConfiguration; baseConfigurationReference = C21074261E77F7A3006EE5EF /* Release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ARCHS = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; CLANG_ENABLE_MODULES = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = NO; @@ -3799,64 +6687,273 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "Telegram-Mac/Telegram-Mac.entitlements"; CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 6N38VWS5BX; + CURRENT_PROJECT_VERSION = 220805; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = N8RBWJ5X6J; + ENABLE_BITCODE = NO; + ENABLE_HARDENED_RUNTIME = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)", + "$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks", + "$(PROJECT_DIR)/HockeySDK-Mac", + "$(PROJECT_DIR)/submodules", + "$(PROJECT_DIR)/submodules/AppCenter", + ); + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_OPTIMIZATION_LEVEL = s; + GCC_PREPROCESSOR_DEFINITIONS = ( + "STABLE=1", + "TGVOIP_USE_CUSTOM_CRYPTO=1", + "WEBRTC_MAC=1", + "HAVE_SCTP=1", ); - GCC_PREPROCESSOR_DEFINITIONS = "APP_STORE=1"; GCC_WARN_64_TO_32_BIT_CONVERSION = NO; GCC_WARN_ABOUT_INVALID_OFFSETOF_MACRO = NO; GCC_WARN_ABOUT_POINTER_SIGNEDNESS = NO; HEADERMAP_USES_VFS = YES; HEADER_SEARCH_PATHS = ( - "$(PROJECT_DIR)/thrid-party/ogg", "$(PROJECT_DIR)/submodules/libtgvoip", + "$(PROJECT_DIR)/thrid-party", ); INFOPLIST_FILE = "Telegram-Mac/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Telegram-Mac/libwebp/lib", + "$(PROJECT_DIR)/thrid-party/libwebp/lib", "$(SDKROOT)/usr/lib/system", "$(PROJECT_DIR)/thrid-party/opus/lib", + "$(PROJECT_DIR)/thrid-party/ffmpeg/lib", + "$(PROJECT_DIR)/thrid-party/libjpeg-turbo", + "$(PROJECT_DIR)", + "$(PROJECT_DIR)/thrid-party/openssl/lib", + "$(PROJECT_DIR)/core-xprojects/libwebp/build/libwebp/lib", + "$(PROJECT_DIR)/core-xprojects/libopus/build/libopus/lib", + "$(PROJECT_DIR)/core-xprojects/webrtc/build/webrtc", + "$(PROJECT_DIR)/core-xprojects/ffmpeg/build/ffmpeg/lib", + "$(PROJECT_DIR)/core-xprojects/OpenSSLEncryption/build/openssl/lib", ); MACOSX_DEPLOYMENT_TARGET = 10.11; + MARKETING_VERSION = 8.1.1; + ONLY_ACTIVE_ARCH = NO; + OTHER_CODE_SIGN_FLAGS = ""; + OTHER_LDFLAGS = "-ObjC"; OTHER_SWIFT_FLAGS = "-Xfrontend -debug-time-function-bodies -D STABLE"; PRODUCT_BUNDLE_IDENTIFIER = ru.keepcoder.Telegram; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = ""; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_ENFORCE_EXCLUSIVE_ACCESS = off; SWIFT_OBJC_BRIDGING_HEADER = "Telegram-Mac/Telegram-Mac-Bridging-Header.h"; - SWIFT_VERSION = 4.0; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_REFLECTION_METADATA_LEVEL = none; + SWIFT_VERSION = 5.0; }; - name = "Release Hockeyapp"; + name = ReleaseHockeyapp; }; - C248BD1F1E6ECB24004B9106 /* Release Hockeyapp */ = { + C248BD1F1E6ECB24004B9106 /* ReleaseHockeyapp */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = TelegramShare/TelegramShare.entitlements; CODE_SIGN_IDENTITY = ""; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 6N38VWS5BX; + CURRENT_PROJECT_VERSION = 209499; + DEVELOPMENT_TEAM = N8RBWJ5X6J; + ENABLE_HARDENED_RUNTIME = YES; + FRAMEWORK_SEARCH_PATHS = ""; + GCC_PREPROCESSOR_DEFINITIONS = ( + "SHARE=1", + "STABLE=1", + ); + INFOPLIST_FILE = TelegramShare/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MARKETING_VERSION = 8.1.1; + ONLY_ACTIVE_ARCH = NO; + OTHER_CODE_SIGN_FLAGS = ""; + OTHER_SWIFT_FLAGS = "-D STABLE"; + PRODUCT_BUNDLE_IDENTIFIER = ru.keepcoder.Telegram.TelegramShare; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = SHARE; + SWIFT_OBJC_BRIDGING_HEADER = "Telegram-Mac/Telegram-Mac-Bridging-Header.h"; + SWIFT_VERSION = 4.2; + VALID_ARCHS = "arm64 x86_64"; + }; + name = ReleaseHockeyapp; + }; + D01E1EC922B2608F00AD6DAE /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + 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_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_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + 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_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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = HockeyappMacAlpha; + }; + D01E1ECA22B2608F00AD6DAE /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D01E1EFF22B261A800AD6DAE /* Alpha.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ARCHS = "$(ARCHS_STANDARD)"; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = NO; + CLANG_WARN_SUSPICIOUS_MOVE = NO; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = "Telegram-Mac/Telegram-Mac.entitlements"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 220805; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = N8RBWJ5X6J; + ENABLE_BITCODE = NO; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_NS_ASSERTIONS = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/submodules/libtgvoip/build/Debug-iphoneos", "$(PROJECT_DIR)", + "$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks", + "$(PROJECT_DIR)/HockeySDK-Mac", + "$(PROJECT_DIR)/submodules", + "$(PROJECT_DIR)/submodules/AppCenter", + ); + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_OPTIMIZATION_LEVEL = s; + GCC_PREPROCESSOR_DEFINITIONS = ( + "ALPHA=1", + "TGVOIP_USE_CUSTOM_CRYPTO=1", + "WEBRTC_MAC=1", + "HAVE_SCTP=1", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = NO; + GCC_WARN_ABOUT_INVALID_OFFSETOF_MACRO = NO; + GCC_WARN_ABOUT_POINTER_SIGNEDNESS = NO; + HEADERMAP_USES_VFS = YES; + HEADER_SEARCH_PATHS = ( + "$(PROJECT_DIR)/submodules/libtgvoip", + "$(PROJECT_DIR)/thrid-party", + ); + INFOPLIST_FILE = "Telegram-Mac/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/thrid-party/libwebp/lib", + "$(SDKROOT)/usr/lib/system", + "$(PROJECT_DIR)/thrid-party/opus/lib", + "$(PROJECT_DIR)/thrid-party/ffmpeg/lib", + "$(PROJECT_DIR)/thrid-party/libjpeg-turbo", + "$(PROJECT_DIR)", + "$(PROJECT_DIR)/thrid-party/openssl/lib", + "$(PROJECT_DIR)/core-xprojects/libwebp/build/libwebp/lib", + "$(PROJECT_DIR)/core-xprojects/libopus/build/libopus/lib", + "$(PROJECT_DIR)/core-xprojects/webrtc/build/webrtc", + "$(PROJECT_DIR)/core-xprojects/ffmpeg/build/ffmpeg/lib", + "$(PROJECT_DIR)/core-xprojects/OpenSSLEncryption/build/openssl/lib", + ); + MACOSX_DEPLOYMENT_TARGET = 10.11; + MARKETING_VERSION = 8.1.1; + MTL_ENABLE_DEBUG_INFO = NO; + ONLY_ACTIVE_ARCH = NO; + OTHER_CODE_SIGN_FLAGS = ""; + OTHER_LDFLAGS = "-ObjC"; + OTHER_SWIFT_FLAGS = "-Xfrontend -debug-time-function-bodies -D ALPHA"; + PRODUCT_BUNDLE_IDENTIFIER = ru.keepcoder.Telegram; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; + SWIFT_ENFORCE_EXCLUSIVE_ACCESS = off; + SWIFT_OBJC_BRIDGING_HEADER = "Telegram-Mac/Telegram-Mac-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_REFLECTION_METADATA_LEVEL = none; + SWIFT_VERSION = 5.0; + }; + name = HockeyappMacAlpha; + }; + D01E1ECB22B2608F00AD6DAE /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = TelegramShare/TelegramShare.entitlements; + CODE_SIGN_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 209499; + DEVELOPMENT_TEAM = N8RBWJ5X6J; + ENABLE_HARDENED_RUNTIME = YES; + FRAMEWORK_SEARCH_PATHS = ""; + GCC_OPTIMIZATION_LEVEL = s; + GCC_PREPROCESSOR_DEFINITIONS = ( + "SHARE=1", + "ALPHA=1", ); INFOPLIST_FILE = TelegramShare/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @executable_path/../../../../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.11; + MARKETING_VERSION = 8.1.1; + ONLY_ACTIVE_ARCH = NO; + OTHER_CODE_SIGN_FLAGS = ""; + OTHER_SWIFT_FLAGS = "-D ALPHA"; PRODUCT_BUNDLE_IDENTIFIER = ru.keepcoder.Telegram.TelegramShare; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; - SKIP_INSTALL = YES; + SKIP_INSTALL = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = SHARE; SWIFT_OBJC_BRIDGING_HEADER = "Telegram-Mac/Telegram-Mac-Bridging-Header.h"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; + VALID_ARCHS = "arm64 x86_64"; }; - name = "Release Hockeyapp"; + name = HockeyappMacAlpha; }; - D098C7201D7E175A007784E4 /* Debug Hockeyapp */ = { + D098C7201D7E175A007784E4 /* DebugHockeyapp */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -3898,16 +6995,16 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.10; + MACOSX_DEPLOYMENT_TARGET = 10.11; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; - name = "Debug Hockeyapp"; + name = DebugHockeyapp; }; - D098C7211D7E175A007784E4 /* Debug AppStore */ = { + D098C7211D7E175A007784E4 /* DebugAppStore */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -3943,20 +7040,22 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.10; + MACOSX_DEPLOYMENT_TARGET = 10.11; MTL_ENABLE_DEBUG_INFO = NO; + ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; }; - name = "Debug AppStore"; + name = DebugAppStore; }; - D098C7231D7E175A007784E4 /* Debug Hockeyapp */ = { + D098C7231D7E175A007784E4 /* DebugHockeyapp */ = { isa = XCBuildConfiguration; baseConfigurationReference = C21074251E77F79A006EE5EF /* Beta.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ARCHS = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; CLANG_ENABLE_MODULES = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = NO; @@ -3964,90 +7063,158 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "Telegram-Mac/Telegram-Mac.entitlements"; CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 6N38VWS5BX; + CURRENT_PROJECT_VERSION = 220805; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = N8RBWJ5X6J; + ENABLE_BITCODE = NO; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_NS_ASSERTIONS = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)", + "$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks", + "$(PROJECT_DIR)/HockeySDK-Mac", + "$(PROJECT_DIR)/submodules", + "$(PROJECT_DIR)/submodules/AppCenter", ); + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_OPTIMIZATION_LEVEL = s; GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", "BETA=1", + "TGVOIP_USE_CUSTOM_CRYPTO=1", + "WEBRTC_MAC=1", + "HAVE_SCTP=1", ); GCC_WARN_64_TO_32_BIT_CONVERSION = NO; GCC_WARN_ABOUT_INVALID_OFFSETOF_MACRO = NO; GCC_WARN_ABOUT_POINTER_SIGNEDNESS = NO; HEADERMAP_USES_VFS = YES; HEADER_SEARCH_PATHS = ( - "$(PROJECT_DIR)/thrid-party/ogg", "$(PROJECT_DIR)/submodules/libtgvoip", + "$(PROJECT_DIR)/thrid-party", ); INFOPLIST_FILE = "Telegram-Mac/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Telegram-Mac/libwebp/lib", + "$(PROJECT_DIR)/thrid-party/libwebp/lib", "$(SDKROOT)/usr/lib/system", "$(PROJECT_DIR)/thrid-party/opus/lib", + "$(PROJECT_DIR)/thrid-party/ffmpeg/lib", + "$(PROJECT_DIR)/thrid-party/libjpeg-turbo", + "$(PROJECT_DIR)", + "$(PROJECT_DIR)/thrid-party/openssl/lib", + "$(PROJECT_DIR)/core-xprojects/libwebp/build/libwebp/lib", + "$(PROJECT_DIR)/core-xprojects/libopus/build/libopus/lib", + "$(PROJECT_DIR)/core-xprojects/webrtc/build/webrtc", + "$(PROJECT_DIR)/core-xprojects/ffmpeg/build/ffmpeg/lib", + "$(PROJECT_DIR)/core-xprojects/OpenSSLEncryption/build/openssl/lib", ); MACOSX_DEPLOYMENT_TARGET = 10.11; + MARKETING_VERSION = 8.1.1; + MTL_ENABLE_DEBUG_INFO = NO; + ONLY_ACTIVE_ARCH = NO; + OTHER_CODE_SIGN_FLAGS = ""; + OTHER_LDFLAGS = "-ObjC"; OTHER_SWIFT_FLAGS = "-Xfrontend -debug-time-function-bodies -D BETA"; PRODUCT_BUNDLE_IDENTIFIER = ru.keepcoder.Telegram; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = ""; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; + SWIFT_ENFORCE_EXCLUSIVE_ACCESS = off; SWIFT_OBJC_BRIDGING_HEADER = "Telegram-Mac/Telegram-Mac-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.0; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_REFLECTION_METADATA_LEVEL = none; + SWIFT_VERSION = 5.0; }; - name = "Debug Hockeyapp"; + name = DebugHockeyapp; }; - D098C7241D7E175A007784E4 /* Debug AppStore */ = { + D098C7241D7E175A007784E4 /* DebugAppStore */ = { isa = XCBuildConfiguration; baseConfigurationReference = C21074251E77F79A006EE5EF /* Beta.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ARCHS = "$(ARCHS_STANDARD)"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; CLANG_ENABLE_MODULES = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_INT_CONVERSION = NO; CLANG_WARN_SUSPICIOUS_MOVE = NO; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = "Telegram-Mac/Telegram-Mac.entitlements"; - CODE_SIGN_IDENTITY = ""; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_INJECT_BASE_ENTITLEMENTS = YES; + CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 6N38VWS5BX; + CURRENT_PROJECT_VERSION = 220805; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = N8RBWJ5X6J; + ENABLE_BITCODE = NO; + ENABLE_HARDENED_RUNTIME = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)", + "$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks", + "$(PROJECT_DIR)/HockeySDK-Mac", + "$(PROJECT_DIR)/submodules", + "$(PROJECT_DIR)/submodules/AppCenter", + ); + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "TGVOIP_USE_CUSTOM_CRYPTO=1", + "WEBRTC_MAC=1", + "HAVE_SCTP=1", ); - GCC_PREPROCESSOR_DEFINITIONS = "STABLE=1"; GCC_WARN_64_TO_32_BIT_CONVERSION = NO; GCC_WARN_ABOUT_INVALID_OFFSETOF_MACRO = NO; GCC_WARN_ABOUT_POINTER_SIGNEDNESS = NO; HEADERMAP_USES_VFS = YES; HEADER_SEARCH_PATHS = ( - "$(PROJECT_DIR)/thrid-party/ogg", "$(PROJECT_DIR)/submodules/libtgvoip", + "$(PROJECT_DIR)/thrid-party", ); INFOPLIST_FILE = "Telegram-Mac/Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Telegram-Mac/libwebp/lib", + "$(PROJECT_DIR)/thrid-party/libwebp/lib", "$(SDKROOT)/usr/lib/system", "$(PROJECT_DIR)/thrid-party/opus/lib", + "$(PROJECT_DIR)/thrid-party/ffmpeg/lib", + "$(PROJECT_DIR)/thrid-party/libjpeg-turbo", + "$(PROJECT_DIR)", + "$(PROJECT_DIR)/thrid-party/openssl/lib", + "$(PROJECT_DIR)/core-xprojects/libwebp/build/libwebp/lib", + "$(PROJECT_DIR)/core-xprojects/libopus/build/libopus/lib", + "$(PROJECT_DIR)/core-xprojects/webrtc/build/webrtc", + "$(PROJECT_DIR)/core-xprojects/ffmpeg/build/ffmpeg/lib", + "$(PROJECT_DIR)/core-xprojects/OpenSSLEncryption/build/openssl/lib", ); MACOSX_DEPLOYMENT_TARGET = 10.11; - OTHER_SWIFT_FLAGS = "-Xfrontend -debug-time-function-bodies -D BETA"; + MARKETING_VERSION = 8.1.1; + ONLY_ACTIVE_ARCH = YES; + OTHER_CODE_SIGN_FLAGS = ""; + OTHER_LDFLAGS = "-ObjC"; + OTHER_SWIFT_FLAGS = "-Xfrontend -debug-time-function-bodies -D DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = ru.keepcoder.Telegram; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = ""; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_COMPILATION_MODE = singlefile; + SWIFT_ENFORCE_EXCLUSIVE_ACCESS = off; SWIFT_OBJC_BRIDGING_HEADER = "Telegram-Mac/Telegram-Mac-Bridging-Header.h"; - SWIFT_VERSION = 4.0; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_REFLECTION_METADATA_LEVEL = none; + SWIFT_VERSION = 5.0; }; - name = "Debug AppStore"; + name = DebugAppStore; }; /* End XCBuildConfiguration section */ @@ -4055,35 +7222,41 @@ C232EAA01E1D07E700C4D38C /* Build configuration list for PBXNativeTarget "TelegramShare" */ = { isa = XCConfigurationList; buildConfigurations = ( - C232EAA11E1D07E700C4D38C /* Debug Hockeyapp */, - C232EAA21E1D07E700C4D38C /* Debug AppStore */, - C248BD1F1E6ECB24004B9106 /* Release Hockeyapp */, - C232EAA31E1D07E700C4D38C /* Release AppStore */, + C232EAA11E1D07E700C4D38C /* DebugHockeyapp */, + D01E1ECB22B2608F00AD6DAE /* HockeyappMacAlpha */, + C232EAA21E1D07E700C4D38C /* DebugAppStore */, + A71DC83B2386D6C2000EEDE2 /* Github */, + C248BD1F1E6ECB24004B9106 /* ReleaseHockeyapp */, + C232EAA31E1D07E700C4D38C /* ReleaseAppStore */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = "Debug AppStore"; + defaultConfigurationName = DebugAppStore; }; D098C7101D7E175A007784E4 /* Build configuration list for PBXProject "Telegram" */ = { isa = XCConfigurationList; buildConfigurations = ( - D098C7201D7E175A007784E4 /* Debug Hockeyapp */, - D098C7211D7E175A007784E4 /* Debug AppStore */, - C248BD1D1E6ECB24004B9106 /* Release Hockeyapp */, - C232EA251E1BC1A100C4D38C /* Release AppStore */, + D098C7201D7E175A007784E4 /* DebugHockeyapp */, + D01E1EC922B2608F00AD6DAE /* HockeyappMacAlpha */, + D098C7211D7E175A007784E4 /* DebugAppStore */, + A71DC8392386D6C2000EEDE2 /* Github */, + C248BD1D1E6ECB24004B9106 /* ReleaseHockeyapp */, + C232EA251E1BC1A100C4D38C /* ReleaseAppStore */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = "Debug AppStore"; + defaultConfigurationName = DebugAppStore; }; D098C7221D7E175A007784E4 /* Build configuration list for PBXNativeTarget "Telegram" */ = { isa = XCConfigurationList; buildConfigurations = ( - D098C7231D7E175A007784E4 /* Debug Hockeyapp */, - D098C7241D7E175A007784E4 /* Debug AppStore */, - C248BD1E1E6ECB24004B9106 /* Release Hockeyapp */, - C232EA261E1BC1A100C4D38C /* Release AppStore */, + D098C7231D7E175A007784E4 /* DebugHockeyapp */, + D01E1ECA22B2608F00AD6DAE /* HockeyappMacAlpha */, + D098C7241D7E175A007784E4 /* DebugAppStore */, + A71DC83A2386D6C2000EEDE2 /* Github */, + C248BD1E1E6ECB24004B9106 /* ReleaseHockeyapp */, + C232EA261E1BC1A100C4D38C /* ReleaseAppStore */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = "Debug AppStore"; + defaultConfigurationName = DebugAppStore; }; /* End XCConfigurationList section */ }; diff --git a/Telegram.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Telegram.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/Telegram.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Telegram.xcodeproj/xcshareddata/xcschemes/Github.xcscheme b/Telegram.xcodeproj/xcshareddata/xcschemes/Github.xcscheme new file mode 100644 index 0000000000..a2f8efa164 --- /dev/null +++ b/Telegram.xcodeproj/xcshareddata/xcschemes/Github.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Telegram.xcodeproj/xcshareddata/xcschemes/Hockeyapp.xcscheme b/Telegram.xcodeproj/xcshareddata/xcschemes/Hockeyapp.xcscheme new file mode 100644 index 0000000000..e282d3ed37 --- /dev/null +++ b/Telegram.xcodeproj/xcshareddata/xcschemes/Hockeyapp.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Telegram.xcodeproj/xcshareddata/xcschemes/HockeyappAlpha.xcscheme b/Telegram.xcodeproj/xcshareddata/xcschemes/HockeyappAlpha.xcscheme new file mode 100644 index 0000000000..dc02169a0b --- /dev/null +++ b/Telegram.xcodeproj/xcshareddata/xcschemes/HockeyappAlpha.xcscheme @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Telegram.xcodeproj/xcshareddata/xcschemes/Release.xcscheme b/Telegram.xcodeproj/xcshareddata/xcschemes/Release.xcscheme new file mode 100644 index 0000000000..a26ae09f9b --- /dev/null +++ b/Telegram.xcodeproj/xcshareddata/xcschemes/Release.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Telegram.xcodeproj/xcshareddata/xcschemes/Telegram.xcscheme b/Telegram.xcodeproj/xcshareddata/xcschemes/Telegram.xcscheme new file mode 100644 index 0000000000..79bbd93789 --- /dev/null +++ b/Telegram.xcodeproj/xcshareddata/xcschemes/Telegram.xcscheme @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Telegram.xcodeproj/xcshareddata/xcschemes/store.xcscheme b/Telegram.xcodeproj/xcshareddata/xcschemes/store.xcscheme new file mode 100644 index 0000000000..ee5a14d035 --- /dev/null +++ b/Telegram.xcodeproj/xcshareddata/xcschemes/store.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Telegram.xcodeproj/xcuserdata/mikhailfilimonov.xcuserdatad/xcschemes/Telegram.xcscheme b/Telegram.xcodeproj/xcuserdata/mikhailfilimonov.xcuserdatad/xcschemes/Telegram.xcscheme old mode 100644 new mode 100755 index a4517fc909..fe5b2f6800 --- a/Telegram.xcodeproj/xcuserdata/mikhailfilimonov.xcuserdatad/xcschemes/Telegram.xcscheme +++ b/Telegram.xcodeproj/xcuserdata/mikhailfilimonov.xcuserdatad/xcschemes/Telegram.xcscheme @@ -26,7 +26,6 @@ buildConfiguration = "Debug Hockeyapp" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - language = "" shouldUseLaunchSchemeArgsEnv = "YES"> @@ -43,10 +42,9 @@ SchemeUserState AppStore.xcscheme + + orderHint + 4 + + AppStore.xcscheme_^#shared#^_ orderHint 14 Hockeyapp.xcscheme + + orderHint + 2 + + Hockeyapp.xcscheme_^#shared#^_ orderHint 12 + HockeyappAlpha.xcscheme_^#shared#^_ + + orderHint + 16 + Release.xcscheme + + orderHint + 3 + + Release.xcscheme_^#shared#^_ orderHint 13 @@ -24,10 +44,25 @@ orderHint 1 + Telegram.xcscheme_^#shared#^_ + + orderHint + 11 + + TelegramShare 2.xcscheme_^#shared#^_ + + orderHint + 22 + TelegramShare.xcscheme orderHint - 15 + 5 + + TelegramShare.xcscheme_^#shared#^_ + + orderHint + 24 SuppressBuildableAutocreation @@ -35,22 +70,22 @@ C232EA561E1D041A00C4D38C primary - + C232EA741E1D058700C4D38C primary - + C232EA901E1D07E700C4D38C primary - + D098C7141D7E175A007784E4 primary - + diff --git a/Telegram.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist b/Telegram.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000000..9ec16478f3 --- /dev/null +++ b/Telegram.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,71 @@ + + + + + SchemeUserState + + AppStore.xcscheme_^#shared#^_ + + orderHint + 4 + + Github.xcscheme_^#shared#^_ + + orderHint + 24 + + Hockeyapp.xcscheme_^#shared#^_ + + orderHint + 2 + + HockeyappAlpha.xcscheme_^#shared#^_ + + orderHint + 3 + + Release.xcscheme_^#shared#^_ + + isShown + + orderHint + 4 + + Spotlight.xcscheme_^#shared#^_ + + orderHint + 29 + + Telegram.xcscheme + + orderHint + 1 + + Telegram.xcscheme_^#shared#^_ + + orderHint + 30 + + TelegramShare.xcscheme_^#shared#^_ + + isShown + + orderHint + 65 + + store.xcscheme_^#shared#^_ + + orderHint + 27 + + + SuppressBuildableAutocreation + + D098C7141D7E175A007784E4 + + primary + + + + + diff --git a/Telegram.xcodeproj/xcuserdata/telegram.xcuserdatad/xcschemes/AppStore.xcscheme b/Telegram.xcodeproj/xcuserdata/telegram.xcuserdatad/xcschemes/AppStore.xcscheme new file mode 100644 index 0000000000..c1d2575aad --- /dev/null +++ b/Telegram.xcodeproj/xcuserdata/telegram.xcuserdatad/xcschemes/AppStore.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Telegram.xcodeproj/xcuserdata/telegram.xcuserdatad/xcschemes/TelegramShare.xcscheme b/Telegram.xcodeproj/xcuserdata/telegram.xcuserdatad/xcschemes/TelegramShare.xcscheme new file mode 100644 index 0000000000..e96e18ddc6 --- /dev/null +++ b/Telegram.xcodeproj/xcuserdata/telegram.xcuserdatad/xcschemes/TelegramShare.xcscheme @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TelegramShare/Base.lproj/ShareViewController.xib b/TelegramShare/Base.lproj/ShareViewController.xib index c84fefa2da..ceec19abe2 100644 --- a/TelegramShare/Base.lproj/ShareViewController.xib +++ b/TelegramShare/Base.lproj/ShareViewController.xib @@ -1,8 +1,8 @@ - + - + @@ -15,7 +15,7 @@ - + diff --git a/TelegramShare/Info.plist b/TelegramShare/Info.plist index 83f33aafb8..e7df9ef7b4 100644 --- a/TelegramShare/Info.plist +++ b/TelegramShare/Info.plist @@ -19,9 +19,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 3.5.2 + $(MARKETING_VERSION) CFBundleVersion - 107976 + 221856 LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSExtension diff --git a/TelegramShare/SEModalProgressView.swift b/TelegramShare/SEModalProgressView.swift index 2c87233f33..c7916c823d 100644 --- a/TelegramShare/SEModalProgressView.swift +++ b/TelegramShare/SEModalProgressView.swift @@ -25,21 +25,21 @@ class SEModalProgressView: View { addSubview(containerView) self.backgroundColor = theme.colors.blackTransparent self.containerView.backgroundColor = theme.colors.grayBackground - let layout = TextViewLayout(.initialize(string: tr(.shareExtensionShare), color: theme.colors.text, font: .normal(.title))) + let layout = TextViewLayout(.initialize(string: tr(L10n.shareExtensionShare), color: theme.colors.text, font: .normal(.title))) layout.measure(width: .greatestFiniteMagnitude) header.update(layout) header.backgroundColor = theme.colors.grayBackground containerView.setFrameSize(250, 80) containerView.layer?.cornerRadius = .cornerRadius - progress.style = ControlStyle(foregroundColor: theme.colors.blueUI, backgroundColor: theme.colors.grayBackground) + progress.style = ControlStyle(foregroundColor: theme.colors.accent, backgroundColor: theme.colors.grayBackground) progress.setFrameSize(250, 4) cancel.set(font: .medium(.title), for: .Normal) - cancel.set(color: theme.colors.blueUI, for: .Normal) - cancel.set(text: tr(.shareExtensionCancel), for: .Normal) - cancel.sizeToFit() + cancel.set(color: theme.colors.accent, for: .Normal) + cancel.set(text: tr(L10n.shareExtensionCancel), for: .Normal) + _ = cancel.sizeToFit() cancel.set(handler: { [weak self] _ in self?.cancelImpl?() diff --git a/TelegramShare/SEPasslockController.swift b/TelegramShare/SEPasslockController.swift index f1f38504b8..c4a0856338 100644 --- a/TelegramShare/SEPasslockController.swift +++ b/TelegramShare/SEPasslockController.swift @@ -19,41 +19,37 @@ import TGUIKit import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore + +import Postbox +import SwiftSignalKit + -enum PasscodeViewState { - case login -} private class PasscodeLockView : Control, NSTextFieldDelegate { - fileprivate let photoView:AvatarControl = AvatarControl(font: .avatar(.custom(23))) fileprivate let nameView:TextView = TextView() fileprivate let input:NSSecureTextField private let nextButton:TitleButton = TitleButton() - private var state:PasscodeViewState? fileprivate var cancel:ImageButton = ImageButton() fileprivate let value:ValuePromise = ValuePromise(ignoreRepeated: false) required init(frame frameRect: NSRect) { input = NSSecureTextField(frame: NSZeroRect) + input.stringValue = "" super.init(frame: frameRect) - photoView.setFrameSize(NSMakeSize(80, 80)) self.backgroundColor = theme.colors.background - nextButton.set(color: theme.colors.blueUI, for: .Normal) + nextButton.set(color: theme.colors.accent, for: .Normal) nextButton.set(font: .normal(.title), for: .Normal) - nextButton.set(text: tr(.shareExtensionPasscodeNext), for: .Normal) - nextButton.sizeToFit() + nextButton.set(text: tr(L10n.shareExtensionPasscodeNext), for: .Normal) + _ = nextButton.sizeToFit() cancel.set(image: theme.icons.chatInlineDismiss, for: .Normal) - cancel.sizeToFit() + _ = cancel.sizeToFit() nameView.backgroundColor = theme.colors.background - addSubview(photoView) addSubview(nameView) addSubview(input) addSubview(nextButton) @@ -66,7 +62,7 @@ private class PasscodeLockView : Control, NSTextFieldDelegate { input.delegate = self let attr = NSMutableAttributedString() - _ = attr.append(string: tr(.shareExtensionPasscodePlaceholder), color: theme.colors.grayText, font: NSFont.normal(FontSize.text)) + _ = attr.append(string: tr(L10n.shareExtensionPasscodePlaceholder), color: theme.colors.grayText, font: NSFont.normal(FontSize.text)) attr.setAlignment(.center, range: attr.range) input.placeholderAttributedString = attr input.backgroundColor = theme.colors.background @@ -93,22 +89,15 @@ private class PasscodeLockView : Control, NSTextFieldDelegate { value.set(input.stringValue) } - func update(with state:PasscodeViewState, account:Account, peer:Peer) { - self.state = state - - photoView.setPeer(account: account, peer: peer) - let layout = TextViewLayout(.initialize(string:peer.displayTitle, color: theme.colors.text, font:.normal(.title))) + func update() { + let layout = TextViewLayout(.initialize(string: L10n.passlockEnterYourPasscode, color: theme.colors.text, font:.normal(.title))) layout.measure(width: frame.width - 40) nameView.update(layout) needsLayout = true - changeInput(state) } - fileprivate func changeInput(_ state:PasscodeViewState) { - - } override func draw(_ layer: CALayer, in ctx: CGContext) { super.draw(layer, in: ctx) @@ -119,10 +108,9 @@ private class PasscodeLockView : Control, NSTextFieldDelegate { override func layout() { super.layout() - photoView.center() - photoView.setFrameOrigin(photoView.frame.minX, photoView.frame.minY - floorToScreenPixels((20 + input.frame.height + 60)/2.0) - 20) + nameView.center() + nameView.centerX(y: nameView.frame.minY - floorToScreenPixels(backingScaleFactor, (20 + input.frame.height + 60)/2.0) - 20) input.setFrameSize(200, input.frame.height) - nameView.centerX(y: photoView.frame.maxY + 20) input.centerX(y: nameView.frame.minY + 30 + 20) input.setFrameOrigin(input.frame.minX, input.frame.minY) setNeedsDisplayLayer() @@ -138,81 +126,42 @@ private class PasscodeLockView : Control, NSTextFieldDelegate { } class SEPasslockController: ModalViewController { - private let account:Account - private var state: PasscodeViewState { - didSet { - self.genericView.changeInput(state) - } - } - private let disposable:MetaDisposable = MetaDisposable() + private let valueDisposable = MetaDisposable() - private let logoutDisposable = MetaDisposable() - private var passcodeValues:[String] = [] - private let _doneValue:Promise = Promise() - - var doneValue:Signal { - return _doneValue.get() - } private let cancelImpl:()->Void - init(_ account:Account, _ state: PasscodeViewState, cancelImpl:@escaping()->Void) { - self.account = account - self.state = state + private let checkNextValue: (String)->Bool + init(checkNextValue: @escaping(String)->Bool, cancelImpl:@escaping()->Void) { self.cancelImpl = cancelImpl + self.checkNextValue = checkNextValue super.init(frame: NSMakeRect(0, 0, 340, 310)) } override var isFullScreen: Bool { - switch state { - case .login: - return true - } + return true } private var genericView:PasscodeLockView { return self.view as! PasscodeLockView } - private func checkNextValue(_ passcode: String, _ current:String?) { - switch state { - case .login: - if current == passcode { - _doneValue.set(.single(true)) - close() - } else { - genericView.input.shake() - } - } - } - - override func viewDidLoad() { super.viewDidLoad() genericView.cancel.set(handler: { [weak self] _ in self?.cancelImpl() }, for: .Click) - valueDisposable.set((genericView.value.get() |> mapToSignal { [weak self] value in - if let strongSelf = self { - return strongSelf.account.postbox.modify { modifier -> (String, String?) in - switch modifier.getAccessChallengeData() { - case .none: - return (value, nil) - case let .plaintextPassword(passcode, _, _), let .numericalPassword(passcode, _, _): - return (value, passcode) - } - } + + valueDisposable.set((genericView.value.get() |> deliverOnMainQueue).start(next: { [weak self] value in + guard let `self` = self else { + return } - return .single(("", nil)) - } |> deliverOnMainQueue).start(next: { [weak self] value, current in - self?.checkNextValue(value, current) - })) - - disposable.set((account.postbox.loadedPeerWithId(account.peerId) |> deliverOnMainQueue).start(next: { [weak self] peer in - if let strongSelf = self { - strongSelf.genericView.update(with: strongSelf.state, account: strongSelf.account, peer: peer) - strongSelf.readyOnce() + if !self.checkNextValue(value) { + self.genericView.input.shake() } })) + genericView.update() + readyOnce() + } @@ -237,8 +186,6 @@ class SEPasslockController: ModalViewController { } deinit { - disposable.dispose() - logoutDisposable.dispose() valueDisposable.dispose() self.window?.removeAllHandlers(for: self) } diff --git a/TelegramShare/SESelectController.swift b/TelegramShare/SESelectController.swift index 814260658b..bd28e12400 100644 --- a/TelegramShare/SESelectController.swift +++ b/TelegramShare/SESelectController.swift @@ -8,34 +8,92 @@ import Cocoa import TGUIKit -import SwiftSignalKitMac -import TelegramCoreMac -import PostboxMac +import SwiftSignalKit +import TelegramCore +import Postbox -extension Peer { - - var canSendMessage: Bool { - if let channel = self as? TelegramChannel { - if case .broadcast(_) = channel.info { - return channel.hasAdminRights(.canPostMessages) - } else if case .group(_) = channel.info { - return !channel.hasBannedRights(.banSendMessages) - } - } else if let group = self as? TelegramGroup { - return group.membership == .Member - } else if let secret = self as? TelegramSecretChat { - switch secret.embeddedState { - case .terminated: - return false - case .handshake: - return false - default: - return true +class SelectAccountView: Control { + + init(_ accounts: [AccountWithInfo], primary: AccountRecordId, switchAccount: @escaping(AccountRecordId) -> Void, frame: NSRect) { + super.init(frame: frame) + backgroundColor = NSColor.black.withAlphaComponent(0.85) + + if let current = accounts.first(where: {$0.account.id == primary}) { + + let currentControl = AvatarControl(font: .avatar(12)) + currentControl.frame = NSMakeRect(frame.width - 30 - 10, 10, 30, 30) + currentControl.setPeer(account: current.account, peer: current.peer) + addSubview(currentControl) + + + var y: CGFloat = currentControl.frame.maxY + 10 + for current in accounts { + if current.account.id != primary { + let container = Button() + + container.autohighlight = true + + container.backgroundColor = .white + let nameView = TextView() + nameView.userInteractionEnabled = false + nameView.isSelectable = false + + let layout = TextViewLayout(.initialize(string: current.peer.compactDisplayTitle, color: .text, font: .medium(.text)), maximumNumberOfLines: 1) + layout.measure(width: 150) + + nameView.background = .white + nameView.update(layout) + + let control = AvatarControl(font: .avatar(12)) + control.setFrameSize(30, 30) + control.setPeer(account: current.account, peer: current.peer) + control.userInteractionEnabled = false + + container.addSubview(control) + + container.addSubview(nameView) + + container.setFrameSize(NSMakeSize(5 + nameView.frame.width + 5 + control.frame.width, 30)) + container.layer?.cornerRadius = container.frame.height / 2 + + container.frame = NSMakeRect(frame.width - container.frame.width - 10, 10, container.frame.width, container.frame.height) + + control.centerY(x: container.frame.width - control.frame.width) + nameView.centerY(x: 5) + + addSubview(container) + + container.set(handler: { [weak self] _ in + self?.change(opacity: 0, animated: true, removeOnCompletion: false, duration: 0.2, timingFunction: .spring, completion: { _ in + switchAccount(current.account.id) + }) + + }, for: .Click) + + container._change(pos: NSMakePoint(container.frame.minX, y), animated: true, timingFunction: .spring) + container.layer?.animateAlpha(from: 0, to: 1, duration: 0.2, timingFunction: .spring) + y += container.frame.height + 10 + } } + + set(handler: { [weak self] _ in + self?.change(opacity: 0, animated: true, removeOnCompletion: false, duration: 0.2, timingFunction: .spring, completion: { [weak self] completed in + self?.removeFromSuperview() + }) + }, for: .SingleClick) } - return true + } + + + required init(frame frameRect: NSRect) { + fatalError("init(frame:) has not been implemented") + } + + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } } @@ -45,34 +103,72 @@ class ShareModalView : View { let tableView:TableView = TableView() let acceptView:TitleButton = TitleButton() let cancelView:TitleButton = TitleButton() + private var photoView: AvatarControl? + private var control: Control = Control() let borderView:View = View() required init(frame frameRect: NSRect) { super.init(frame: frameRect) self.backgroundColor = theme.colors.background borderView.backgroundColor = theme.colors.border - acceptView.style = ControlStyle(font: .medium(.text),foregroundColor: theme.colors.blueUI) - acceptView.set(text: tr(.shareExtensionShare), for: .Normal) - acceptView.sizeToFit() + acceptView.style = ControlStyle(font: .medium(.text),foregroundColor: theme.colors.accent) + acceptView.set(text: L10n.shareExtensionShare, for: .Normal) + _ = acceptView.sizeToFit() - cancelView.style = ControlStyle(font:.medium(.text),foregroundColor: theme.colors.blueUI) - cancelView.set(text: tr(.shareExtensionCancel), for: .Normal) - cancelView.sizeToFit() + cancelView.style = ControlStyle(font:.medium(.text),foregroundColor: theme.colors.accent) + cancelView.set(text: L10n.shareExtensionCancel, for: .Normal) + _ = cancelView.sizeToFit() addSubview(acceptView) addSubview(cancelView) addSubview(searchView) addSubview(tableView) addSubview(borderView) + addSubview(control) + } + + func updateWithAccounts(_ accounts: (primary: AccountRecordId?, accounts: [AccountWithInfo]), context: AccountContext) -> Void { + if accounts.accounts.count > 1, let primary = accounts.primary { + if photoView == nil { + photoView = AvatarControl(font: .avatar(12)) + photoView?.setFrameSize(NSMakeSize(30, 30)) + addSubview(photoView!) + } + if let account = accounts.accounts.first(where: {$0.account.id == primary}) { + photoView?.setPeer(account: account.account, peer: account.peer) + } + photoView?.removeAllHandlers() + + + + photoView?.set(handler: { [weak self] _ in + guard let `self` = self else {return} + let view = SelectAccountView(accounts.accounts, primary: primary, switchAccount: { recordId in + context.sharedContext.switchToAccount(id: recordId, action: nil) + }, frame: self.bounds) + self.addSubview(view) + view.layer?.animateAlpha(from: 0, to: 1, duration: 0.2) + }, for: .Click) + } else { + photoView?.removeFromSuperview() + photoView = nil + } + needsLayout = true } override func layout() { super.layout() - searchView.frame = NSMakeRect(10, 10, frame.width - 20, 30) + if let photoView = photoView { + photoView.setFrameOrigin(frame.width - photoView.frame.width - 10, 10) + searchView.frame = NSMakeRect(10, 10, frame.width - 20 - photoView.frame.width - 10, 30) + } else { + searchView.frame = NSMakeRect(10, 10, frame.width - 20, 30) + } + control.frame = NSMakeRect(frame.width - 30 - 30, 10, 30, 30) tableView.frame = NSMakeRect(0, 50, frame.width, frame.height - 50 - 40) borderView.frame = NSMakeRect(0, tableView.frame.maxY, frame.width, .borderSize) - acceptView.setFrameOrigin(frame.width - acceptView.frame.width - 30, floorToScreenPixels(tableView.frame.maxY + (40 - acceptView.frame.height) / 2.0)) - cancelView.setFrameOrigin(acceptView.frame.minX - cancelView.frame.width - 30, floorToScreenPixels(tableView.frame.maxY + (40 - cancelView.frame.height) / 2.0)) + acceptView.setFrameOrigin(frame.width - acceptView.frame.width - 30, floorToScreenPixels(backingScaleFactor, tableView.frame.maxY + (40 - acceptView.frame.height) / 2.0)) + cancelView.setFrameOrigin(acceptView.frame.minX - cancelView.frame.width - 30, floorToScreenPixels(backingScaleFactor, tableView.frame.maxY + (40 - cancelView.frame.height) / 2.0)) } required init?(coder: NSCoder) { @@ -83,28 +179,26 @@ class ShareModalView : View { class ShareObject { - let account:Account - let context:NSExtensionContext - init(_ account:Account, _ context:NSExtensionContext) { - self.account = account + let context: AccountContext + let shareContext:NSExtensionContext + init(_ context: AccountContext, _ shareContext:NSExtensionContext) { self.context = context + self.shareContext = shareContext } private let progressView = SEModalProgressView() func perform(to entries:[PeerId], view: NSView) { - var signals:[Signal] = [] - - - + var signals:[Signal] = [] + var needWaitAsync = false var k:Int = 0 - let total = context.inputItems.reduce(0) { (current, item) -> Int in + let total = shareContext.inputItems.reduce(0) { (current, item) -> Int in if let item = item as? NSExtensionItem { if let _ = item.attributedContentText?.string { return current + 1 - } else if let attachments = item.attachments as? [NSItemProvider] { + } else if let attachments = item.attachments { return current + attachments.count } } @@ -128,7 +222,7 @@ class ShareObject { self.progressView.set(progress: CGFloat(min(progress / Float(total), 1))) }, completed: { - self.context.completeRequest(returningItems: nil, completionHandler: nil) + self.shareContext.completeRequest(returningItems: nil, completionHandler: nil) }) self.progressView.cancelImpl = { @@ -140,14 +234,16 @@ class ShareObject { } } + NSLog("\(entries), \(shareContext.inputItems)") + for peerId in entries { - for j in 0 ..< context.inputItems.count { - if let item = context.inputItems[j] as? NSExtensionItem { + for j in 0 ..< shareContext.inputItems.count { + if let item = shareContext.inputItems[j] as? NSExtensionItem { if let text = item.attributedContentText?.string { signals.append(sendText(text, to:peerId)) k += 1 requestIfNeeded() - } else if let attachments = item.attachments as? [NSItemProvider] { + } else if let attachments = item.attachments { for i in 0 ..< attachments.count { attachments[i].loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil, completionHandler: { (coding, error) in @@ -157,10 +253,26 @@ class ShareObject { } else { signals.append(self.sendMedia(url, to:peerId)) } + k += 1 + } else if let data = coding as? Data, let string = String(data: data, encoding: .utf8), let url = URL(string: string) { + if !url.isFileURL { + signals.append(self.sendText(url.absoluteString, to:peerId)) + } else { + signals.append(self.sendMedia(url, to:peerId)) + } + k += 1 } - k += 1 requestIfNeeded() }) + if k != total { + attachments[i].loadItem(forTypeIdentifier: kUTTypeImage as String, options: nil, completionHandler: { (coding, error) in + if let data = (coding as? NSImage)?.tiffRepresentation { + signals.append(self.sendMedia(nil, data, to:peerId)) + k += 1 + requestIfNeeded() + } + }) + } } } } @@ -169,38 +281,57 @@ class ShareObject { } - private func sendText(_ text:String, to peerId:PeerId) -> Signal { - return Signal.single(0) |> then(standaloneSendMessage(account: self.account, peerId: peerId, text: text, attributes: [], media: nil, replyToMessageId: nil) |> mapError {_ in} |> map {_ in return 1}) + private func sendText(_ text:String, to peerId:PeerId) -> Signal { + return Signal.single(0) |> then(standaloneSendMessage(account: context.account, peerId: peerId, text: text, attributes: [], media: nil, replyToMessageId: nil) |> `catch` {_ in return .complete()} |> map {_ in return 1}) } - private let queue:Queue = Queue(name: "proccessShareFilesQueue", target: nil) + private let queue:Queue = Queue(name: "proccessShareFilesQueue") - private func prepareMedia(_ path: URL) -> Signal { + private func prepareMedia(_ path: URL?, _ pasteData: Data? = nil) -> Signal { return Signal { subscriber in - if let data = try? Data(contentsOf: path) { + + let data = pasteData ?? (path != nil ? try? Data(contentsOf: path!) : nil) + + if let data = data { + var forceImage: Bool = false + if let _ = NSImage(data: data) { + if let path = path { + let mimeType = MIMEType(path.path) + if mimeType.hasPrefix("image/") && !mimeType.hasSuffix("gif") { + forceImage = true + } + } else { + forceImage = true + } + } - let mimeType = MIMEType(path.absoluteString.nsstring.pathExtension.lowercased()) - if mimeType.hasPrefix("image/") && !mimeType.hasSuffix("gif") { - + if forceImage { let options = NSMutableDictionary() options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailWithTransform as String) options.setValue(1280 as NSNumber, forKey: kCGImageSourceThumbnailMaxPixelSize as String) options.setValue(true as NSNumber, forKey: kCGImageSourceCreateThumbnailFromImageAlways as String) - + if let imageSource = CGImageSourceCreateWithData(data as CFData, nil) { let image = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options) - if let image = image { - let imageRep = NSBitmapImageRep(cgImage: image) - let options: [NSBitmapImageRep.PropertyKey: Any] = [.compressionFactor: 0.83] - let data = imageRep.representation(using: .jpeg, properties: options) - if let data = data { + if let image = image, let data = NSImage(cgImage: image, size: image.backingSize).tiffRepresentation(using: .jpeg, factor: 0.83) { + let imageRep = NSBitmapImageRep(data: data) + if let data = imageRep?.representation(using: .jpeg, properties: [:]) { subscriber.putNext(StandaloneMedia.image(data)) } } } } else { - subscriber.putNext(StandaloneMedia.file(data: data, mimeType: mimeType, attributes: [])) + var mimeType: String = "application/octet-stream" + let fileName: String + if let path = path { + mimeType = MIMEType(path.path) + fileName = path.path.nsstring.lastPathComponent + } else { + fileName = "Unnamed.file" + } + + subscriber.putNext(StandaloneMedia.file(data: data, mimeType: mimeType, attributes: [.FileName(fileName: fileName)])) } } @@ -212,15 +343,15 @@ class ShareObject { - private func sendMedia(_ path:URL, to peerId:PeerId) -> Signal { - return Signal.single(0) |> then(prepareMedia(path) |> mapToSignal { media -> Signal in - return standaloneSendMessage(account: self.account, peerId: peerId, text: "", attributes: [], media: media, replyToMessageId: nil) |> mapError {_ in} + private func sendMedia(_ path:URL?, _ data: Data? = nil, to peerId:PeerId) -> Signal { + return Signal.single(0) |> then(prepareMedia(path, data) |> mapToSignal { media -> Signal in + return standaloneSendMessage(account: self.context.account, peerId: peerId, text: "", attributes: [], media: media, replyToMessageId: nil) |> `catch` {_ in return .complete()} }) } func cancel() { let cancelError = NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError, userInfo: nil) - context.cancelRequest(withError: cancelError) + shareContext.cancelRequest(withError: cancelError) } } @@ -302,22 +433,19 @@ func ==(lhs:SelectablePeersEntry, rhs:SelectablePeersEntry) -> Bool { -fileprivate func prepareEntries(from:[SelectablePeersEntry]?, to:[SelectablePeersEntry], account:Account, initialSize:NSSize, animated:Bool, selectInteraction:SelectPeerInteraction) -> Signal,Void> { +fileprivate func prepareEntries(from:[SelectablePeersEntry]?, to:[SelectablePeersEntry], account:Account, initialSize:NSSize, animated:Bool, selectInteraction:SelectPeerInteraction) -> Signal, NoError> { return Signal {subscriber in - let (deleted,inserted,updated) = proccessEntries(from, right: to, { (entry) -> TableRowItem in - + let (deleted,inserted,updated) = proccessEntries(from, right: to, { entry -> TableRowItem in switch entry { case let .plain(peer, _): - return ShortPeerRowItem(initialSize, peer: peer, account:account, height:40, photoSize:NSMakeSize(30,30), inset:NSEdgeInsets(left: 10, right:10), interactionType:.selectable(selectInteraction)) + return ShortPeerRowItem(initialSize, peer: peer, account:account, height:40, photoSize:NSMakeSize(30,30), isLookSavedMessage: true, inset:NSEdgeInsets(left: 10, right:10), interactionType:.selectable(selectInteraction)) case .emptySearch: return SearchEmptyRowItem(initialSize, stableId: SelectablePeersEntryStableId.emptySearch) } - - }) - let transition = TableEntriesTransition<[SelectablePeersEntry]>(deleted: deleted, inserted: inserted, updated:updated, entries:to, animated:animated, state: animated ? .none(nil) : .saveVisible(.lower)) + let transition = TableEntriesTransition<[SelectablePeersEntry]>(deleted: deleted, inserted: inserted, updated:updated, entries:to, animated:animated, state: .none(nil)) subscriber.putNext(transition) subscriber.putCompletion() @@ -346,7 +474,7 @@ class SESelectController: GenericViewController, Notifable { private let search:ValuePromise = ValuePromise(ignoreRepeated: true) private let inSearchSelected:Atomic<[PeerId]> = Atomic(value:[]) private let disposable:MetaDisposable = MetaDisposable() - + private let accountsDisposable = MetaDisposable() func notify(with value: Any, oldValue: Any, animated: Bool) { if let value = value as? SelectPeerPresentation, let oldValue = oldValue as? SelectPeerPresentation { @@ -361,6 +489,8 @@ class SESelectController: GenericViewController, Notifable { } } + + func isEqual(to other: Notifable) -> Bool { return false } @@ -368,23 +498,29 @@ class SESelectController: GenericViewController, Notifable { override func viewDidLoad() { super.viewDidLoad() + let context = self.share.context + + accountsDisposable.set((self.share.context.sharedContext.activeAccountsWithInfo |> deliverOnMainQueue).start(next: { [weak self] accounts in + self?.genericView.updateWithAccounts(accounts, context: context) + })) + search.set(SearchState(state: .None, request: nil)) let previous:Atomic<[SelectablePeersEntry]?> = Atomic(value: nil) let initialSize = self.atomicSize.modify({$0}) - let account = share.account + let account = share.context.account let table = genericView.tableView let selectInteraction = self.selectInteractions selectInteraction.add(observer: self) - let list:Signal,Void> = search.get() |> distinctUntilChanged |> mapToSignal { [weak self] search -> Signal,Void> in + let list:Signal, NoError> = search.get() |> distinctUntilChanged |> mapToSignal { [weak self] search -> Signal, NoError> in if search.state == .None { - let signal:Signal<(ChatListView,ViewUpdateType),Void> = account.viewTracker.tailChatListView(count: 100) + let signal:Signal<(ChatListView,ViewUpdateType), NoError> = account.viewTracker.tailChatListView(groupId: .root, count: 100) |> take(1) - return signal |> deliverOn(prepareQueue) |> mapToQueue { [weak self] (value) -> Signal, Void> in + return combineLatest(signal, account.postbox.loadedPeerWithId(account.peerId)) |> deliverOn(prepareQueue) |> mapToQueue { [weak self] value, mainPeer -> Signal, NoError> in if let strongSelf = self { var entries:[SelectablePeersEntry] = [] @@ -393,12 +529,15 @@ class SESelectController: GenericViewController, Notifable { var fromPeers:[PeerId:Peer] = [:] var contains:[PeerId:Peer] = [:] + entries.append(.plain(mainPeer, ChatListIndex.init(pinningIndex: 0, messageIndex: MessageIndex.absoluteUpperBound()))) + contains[mainPeer.id] = mainPeer + for entry in value.0.entries { switch entry { - case let .MessageEntry(id, _, _, _, _, renderedPeer, _): + case let .MessageEntry(id, _, _, _, _, renderedPeer, _, _, _, _): if let peer = renderedPeer.chatMainPeer { if !fromSetIds.contains(peer.id), contains[peer.id] == nil { - if peer.canSendMessage { + if peer.canSendMessage(false) { entries.append(.plain(peer,id)) contains[peer.id] = peer } @@ -422,41 +561,79 @@ class SESelectController: GenericViewController, Notifable { } entries.sort(by: <) - return prepareEntries(from: previous.modify({$0}), to: entries, account: account, initialSize: initialSize, animated: true, selectInteraction:selectInteraction) |> deliverOnMainQueue + return prepareEntries(from: previous.swap(entries), to: entries, account: account, initialSize: initialSize, animated: true, selectInteraction:selectInteraction) } return .never() } } else { - return ( search.request.isEmpty ? recentPeers(account: account) : account.postbox.searchPeers(query: search.request.lowercased()) |> map { - return $0.flatMap({$0.chatMainPeer}).filter({!($0 is TelegramSecretChat)}) }) |> deliverOn(prepareQueue) |> mapToSignal { peers -> Signal, Void> in + + let signal: Signal<([Peer], Peer), NoError> + + if search.request.isEmpty { + signal = combineLatest(context.engine.peers.recentPeers() |> map { recent -> [Peer] in + switch recent { + case .disabled: + return [] + case let .peers(peers): + return peers + } + }, account.postbox.loadedPeerWithId(account.peerId)) + |> deliverOn(prepareQueue) + } else { + let foundLocalPeers = account.postbox.searchPeers(query: search.request.lowercased()) |> map {$0.compactMap { $0.chatMainPeer} } + + + + let foundRemotePeers:Signal<[Peer], NoError> = .single([]) |> then ( context.engine.peers.searchPeers(query: search.request.lowercased()) |> map { $0.map{$0.peer} + $1.map{$0.peer} } ) + + signal = combineLatest(combineLatest(foundLocalPeers, foundRemotePeers) |> map {$0 + $1}, account.postbox.loadedPeerWithId(account.peerId)) + + } + + let assignSavedMessages:Bool + if search.request.isEmpty { + assignSavedMessages = true + } else if L10n.peerSavedMessages.lowercased().hasPrefix(search.request.lowercased()) || "Saved Messages".lowercased().hasPrefix(search.request.lowercased()) { + assignSavedMessages = true + } else { + assignSavedMessages = false + } + + + return signal |> mapToSignal { peers, mainPeer in var entries:[SelectablePeersEntry] = [] var i:Int32 = Int32.max + + var contains: Set = Set() + if assignSavedMessages { + entries.append(.plain(mainPeer, ChatListIndex(pinningIndex: 0, messageIndex: MessageIndex.absoluteUpperBound()))) + contains.insert(mainPeer.id) + } + + for peer in peers { - if peer.canSendMessage { + if peer.canSendMessage(false), !contains.contains(peer.id) { let index = MessageIndex(id: MessageId(peerId: peer.id, namespace: 1, id: i), timestamp: i) entries.append(.plain(peer, ChatListIndex(pinningIndex: nil, messageIndex: index))) + contains.insert(peer.id) i -= 1 } } - if entries.isEmpty { - entries.append(.emptySearch) - } entries.sort(by: <) - return prepareEntries(from: previous.modify({$0}), to: entries, account: account, initialSize: initialSize, animated: true, selectInteraction:selectInteraction) |> deliverOnMainQueue + return prepareEntries(from: previous.swap(entries), to: entries, account: account, initialSize: initialSize, animated: true, selectInteraction:selectInteraction) } + } - } - disposable.set(list.start(next: { [weak self] (transition) in + disposable.set((list |> deliverOnMainQueue).start(next: { [weak self] (transition) in table.resetScrollNotifies() - _ = previous.swap(transition.entries) table.merge(with:transition) self?.readyOnce() })) - self.genericView.searchView.searchInteractions = SearchInteractions({ state in + self.genericView.searchView.searchInteractions = SearchInteractions({ state, _ in self.search.set(SearchState(state: state.state, request: state.request)) }, { state in self.search.set(SearchState(state: state.state, request: state.request)) @@ -505,6 +682,7 @@ class SESelectController: GenericViewController, Notifable { deinit { disposable.dispose() + accountsDisposable.dispose() } } diff --git a/TelegramShare/SEUnauthorizedViewController.swift b/TelegramShare/SEUnauthorizedViewController.swift index e36ff3552e..dce4ce9a2c 100644 --- a/TelegramShare/SEUnauthorizedViewController.swift +++ b/TelegramShare/SEUnauthorizedViewController.swift @@ -21,10 +21,10 @@ class SEUnauthorizedView : View { imageView.sizeToFit() self.backgroundColor = theme.colors.background cancel.set(font: .medium(.title), for: .Normal) - cancel.set(color: theme.colors.blueUI, for: .Normal) - cancel.set(text: tr(.shareExtensionUnauthorizedOK), for: .Normal) + cancel.set(color: theme.colors.accent, for: .Normal) + cancel.set(text: tr(L10n.shareExtensionUnauthorizedOK), for: .Normal) - let layout = TextViewLayout(.initialize(string: tr(.shareExtensionUnauthorizedDescription), color: theme.colors.text, font: .normal(.text)), alignment: .center) + let layout = TextViewLayout(.initialize(string: tr(L10n.shareExtensionUnauthorizedDescription), color: theme.colors.text, font: .normal(.text)), alignment: .center) textView.backgroundColor = theme.colors.background textView.update(layout) diff --git a/TelegramShare/ShareApplicationContext.swift b/TelegramShare/ShareApplicationContext.swift index 5b9321d19f..4b06ef1343 100644 --- a/TelegramShare/ShareApplicationContext.swift +++ b/TelegramShare/ShareApplicationContext.swift @@ -8,77 +8,30 @@ import Cocoa import TGUIKit -import TelegramCoreMac -import PostboxMac -import SwiftSignalKitMac +import TelegramCore +import Postbox +import SwiftSignalKit -private let telegramAccountAuxiliaryMethods = AccountAuxiliaryMethods(updatePeerChatInputState: { interfaceState, inputState -> PeerChatInterfaceState? in + +let telegramAccountAuxiliaryMethods = AccountAuxiliaryMethods(fetchResource: { account, resource, range, _ in return nil -}, fetchResource: { account, resource, range, _ in +}, fetchResourceMediaReferenceHash: { resource in + return .single(nil) +}, prepareSecretThumbnailData: { _ in return nil }) -func applicationContext(accountManager: AccountManager, appGroupPath: String, extensionContext: NSExtensionContext) -> Signal { - - return currentAccount(networkArguments: NetworkInitializationArguments(apiId: 2834, languagesCategory: "macos"), supplementary: true, manager: accountManager, appGroupPath: appGroupPath, testingEnvironment: false, auxiliaryMethods: telegramAccountAuxiliaryMethods) |> mapToSignal { result -> Signal in - if let result = result { - switch result { - case .unauthorized(let account): - return account.postbox.preferencesView(keys: [PreferencesKeys.localizationSettings]) |> take(1) |> deliverOnMainQueue |> map { value in - return .unauthorized(UnauthorizedApplicationContext(account: account, context: extensionContext, localization: value.values[PreferencesKeys.localizationSettings] as? LocalizationSettings, theme: value.values[ApplicationSpecificPreferencesKeys.themeSettings] as? ThemePalleteSettings)) - } - case let .authorized(account): - let paslock:Signal = account.postbox.modify { modifier -> PostboxAccessChallengeData in - return modifier.getAccessChallengeData() - } |> deliverOnMainQueue - - return paslock |> mapToSignal { access -> Signal in - let promise:Promise = Promise() - let auth: Signal = combineLatest(promise.get(), account.postbox.preferencesView(keys: [PreferencesKeys.localizationSettings, ApplicationSpecificPreferencesKeys.themeSettings]) |> take(1)) |> deliverOnMainQueue |> map { _, value in - return .authorized(AuthorizedApplicationContext(account: account, context: extensionContext, localization: value.values[PreferencesKeys.localizationSettings] as? LocalizationSettings, theme: value.values[ApplicationSpecificPreferencesKeys.themeSettings] as? ThemePalleteSettings)) - } - switch access { - case .none: - promise.set(.single(Void())) - return auth - default: - return account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.themeSettings, PreferencesKeys.localizationSettings]) |> take(1) |> deliverOnMainQueue |> map { value in - return .postboxAccess(PasscodeAccessContext(promise: promise, account: account, context: extensionContext, localization: value.values[PreferencesKeys.localizationSettings] as? LocalizationSettings, theme: value.values[ApplicationSpecificPreferencesKeys.themeSettings] as? ThemePalleteSettings)) - } |> then(auth) - - } - } - default: - return .complete() - } - - } - - return .single(nil) - } |> deliverOnMainQueue - -} - - final class UnauthorizedApplicationContext { let account: UnauthorizedAccount let rootController: SEUnauthorizedViewController - init( account: UnauthorizedAccount, context: NSExtensionContext, localization:LocalizationSettings?, theme:ThemePalleteSettings?) { + init( account: UnauthorizedAccount, context: NSExtensionContext) { self.account = account - if let localization = localization { - applyShareUILocalization(localization) - } - if let themeSettings = theme { - updateTheme(with: themeSettings) - } else { - setDefaultTheme(for: nil) - } - + self.rootController = SEUnauthorizedViewController(cancelImpl: { let cancelError = NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError, userInfo: nil) context.cancelRequest(withError: cancelError) @@ -88,75 +41,13 @@ final class UnauthorizedApplicationContext { } class AuthorizedApplicationContext { - let account: Account + let context: AccountContext let rootController: SESelectController - init(account: Account, context: NSExtensionContext, localization:LocalizationSettings?, theme:ThemePalleteSettings?) { - self.account = account - - if let localization = localization { - applyShareUILocalization(localization) - } - - if let themeSettings = theme { - updateTheme(with: themeSettings) - } else { - setDefaultTheme() - } + init(context: AccountContext, shareContext: NSExtensionContext) { + self.context = context - self.rootController = SESelectController(ShareObject(account, context)) - account.network.shouldKeepConnection.set(.single(true)) + self.rootController = SESelectController(ShareObject(context, shareContext)) + context.account.network.shouldKeepConnection.set(.single(true)) } } -class PasscodeAccessContext { - let account: Account - let promise:Promise - let rootController: SEPasslockController - init(promise:Promise, account: Account, context:NSExtensionContext, localization:LocalizationSettings?, theme:ThemePalleteSettings?) { - self.account = account - self.promise = promise - if let localization = localization { - applyShareUILocalization(localization) - } - if let themeSettings = theme { - updateTheme(with: themeSettings) - } else { - setDefaultTheme() - } - - self.rootController = SEPasslockController(account, .login, cancelImpl: { - let cancelError = NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError, userInfo: nil) - context.cancelRequest(withError: cancelError) - }) - promise.set(rootController.doneValue |> filter {$0} |> map {_ in}) - } -} - -enum ShareApplicationContext { - case unauthorized(UnauthorizedApplicationContext) - case authorized(AuthorizedApplicationContext) - case postboxAccess(PasscodeAccessContext) - - func showRoot(for window:Window) { - if let content = window.contentView { - switch self { - case let .postboxAccess(context): - showModal(with: context.rootController, for: window) - default: - content.addSubview(rootView) - rootView.frame = content.bounds - } - } - } - - var rootView: NSView { - switch self { - case let .unauthorized(context): - return context.rootController.view - case let .authorized(context): - return context.rootController.view - case let .postboxAccess(context): - return context.rootController.view - } - } -} diff --git a/TelegramShare/ShareViewController.swift b/TelegramShare/ShareViewController.swift index 3bff0efc6c..24e27d7f7d 100644 --- a/TelegramShare/ShareViewController.swift +++ b/TelegramShare/ShareViewController.swift @@ -8,54 +8,174 @@ import Cocoa import TGUIKit -import PostboxMac -import TelegramCoreMac -import SwiftSignalKitMac +import Postbox +import TelegramCore +import SwiftSignalKit + +import OpenSSLEncryption class ShareViewController: NSViewController { override var nibName: NSNib.Name? { - return NSNib.Name(rawValue: "ShareViewController") + return "ShareViewController" } - private let accountManagerPromise = Promise() - private var contextValue: ShareApplicationContext? - private let context = Promise() + + + + private var contextValue: AuthorizedApplicationContext? + private let context = Promise() private let contextDisposable = MetaDisposable() + private var passlock: SEPasslockController? = nil - override func loadView() { - super.loadView() + override func viewDidLoad() { + super.viewDidLoad() - declareEncodable(ThemePalleteSettings.self, f: { ThemePalleteSettings(decoder: $0) }) - - let appGroupName = "6N38VWS5BX.ru.keepcoder.Telegram" - guard let containerUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName) else { + + + declareEncodable(ThemePaletteSettings.self, f: { ThemePaletteSettings(decoder: $0) }) + declareEncodable(InAppNotificationSettings.self, f: { InAppNotificationSettings(decoder: $0) }) + + guard let containerUrl = ApiEnvironment.containerURL else { return } + initializeAccountManagement() + + - let logger = Logger(basePath: containerUrl.path + "/sharelogs") + let rootPath = containerUrl.path + let accountManager = AccountManager(basePath: containerUrl.path + "/accounts-metadata", isTemporary: false, isReadOnly: true) + + let logger = Logger(rootPath: containerUrl.path, basePath: containerUrl.path + "/logs") logger.logToConsole = false logger.logToFile = false Logger.setSharedLogger(logger) + let themeSemaphore = DispatchSemaphore(value: 0) + var themeSettings: ThemePaletteSettings = ThemePaletteSettings.defaultTheme + _ = (themeSettingsView(accountManager: accountManager) |> take(1)).start(next: { settings in + themeSettings = settings + themeSemaphore.signal() + }) + themeSemaphore.wait() + + var localization: LocalizationSettings? = nil + let localizationSemaphore = DispatchSemaphore(value: 0) + _ = (accountManager.transaction { transaction in + localization = transaction.getSharedData(SharedDataKeys.localizationSettings) as? LocalizationSettings + localizationSemaphore.signal() + }).start() + localizationSemaphore.wait() + + if let localization = localization { + applyShareUILocalization(localization) + } + + updateTheme(with: themeSettings) + + + let appEncryption = AppEncryptionParameters(path: rootPath) + + if let deviceSpecificEncryptionParameters = appEncryption.decrypt() { + let parameters = ValueBoxEncryptionParameters(forceEncryptionIfNoSet: true, key: ValueBoxEncryptionParameters.Key(data: deviceSpecificEncryptionParameters.key)!, salt: ValueBoxEncryptionParameters.Salt(data: deviceSpecificEncryptionParameters.salt)!) + launchExtension(accountManager: accountManager, encryptionParameters: parameters, appEncryption: appEncryption) + } else { + let extensionContext = self.extensionContext! + + self.passlock = SEPasslockController(checkNextValue: { passcode in + appEncryption.applyPasscode(passcode) + if let params = appEncryption.decrypt() { + let parameters = ValueBoxEncryptionParameters(forceEncryptionIfNoSet: true, key: ValueBoxEncryptionParameters.Key(data: params.key)!, salt: ValueBoxEncryptionParameters.Salt(data: params.salt)!) + self.launchExtension(accountManager: accountManager, encryptionParameters: parameters, appEncryption: appEncryption) + return true + } else { + return false + } + }, cancelImpl: { + let cancelError = NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError, userInfo: nil) + extensionContext.cancelRequest(withError: cancelError) + }) + + self.passlock!.view.frame = self.view.bounds + self.view.addSubview(self.passlock!.view) + } + } + + + private func launchExtension(accountManager: AccountManager, encryptionParameters: ValueBoxEncryptionParameters, appEncryption: AppEncryptionParameters) { + let extensionContext = self.extensionContext! + + let containerUrl = ApiEnvironment.containerURL! - self.accountManagerPromise.set(accountManager(basePath: containerUrl.path + "/accounts-metadata")) - self.context.set(self.accountManagerPromise.get() |> deliverOnMainQueue |> mapToSignal { accountManager -> Signal in - return applicationContext(accountManager: accountManager, appGroupPath: containerUrl.path, extensionContext: extensionContext) - }) + let rootPath = containerUrl.path + - self.contextDisposable.set(self.context.get().start(next: { context in - assert(Queue.mainQueue().isCurrent()) - self.contextValue = context - self.view.removeAllSubviews() - if let rootView = context?.rootView { - rootView.frame = self.view.bounds - self.view.addSubview(rootView) - } - })) + let networkArguments = NetworkInitializationArguments(apiId: ApiEnvironment.apiId, apiHash: ApiEnvironment.apiHash, languagesCategory: ApiEnvironment.language, appVersion: ApiEnvironment.version, voipMaxLayer: 90, voipVersions: [], appData: .single(ApiEnvironment.appData), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider()) + let sharedContext = SharedAccountContext(accountManager: accountManager, networkArguments: networkArguments, rootPath: rootPath, encryptionParameters: encryptionParameters, appEncryption: appEncryption, displayUpgradeProgress: { _ in }) + + + + + let rawAccounts = sharedContext.activeAccounts + |> map { _, accounts, _ -> [Account] in + return accounts.map({ $0.1 }) + } + let _ = (sharedAccountInfos(accountManager: sharedContext.accountManager, accounts: rawAccounts) + |> deliverOn(Queue())).start(next: { infos in + storeAccountsData(rootPath: rootPath, accounts: infos) + }) + + + + let readyDisposable = MetaDisposable() + _ = (self.context.get() |> mapToSignal { context -> Signal in + return .single(context) + + } |> deliverOnMainQueue).start(next: { context in + assert(Queue.mainQueue().isCurrent()) + + if let context = context { + context.rootController.view.frame = self.view.bounds + + readyDisposable.set((context.rootController.ready.get() |> take(1)).start(next: { [weak context] _ in + guard let context = context else { return } + if let contextValue = self.contextValue { + contextValue.rootController.view.removeFromSuperview() + } + self.contextValue = context + if let passlock = self.passlock, passlock.isLoaded() { + self.passlock?.view.removeFromSuperview() + } + self.view.addSubview(context.rootController.view, positioned: .below, relativeTo: self.view.subviews.first) + + })) + } + }) + + + self.context.set(sharedContext.activeAccounts + |> map { primary, _, _ -> Account? in + return primary + } + |> distinctUntilChanged(isEqual: { lhs, rhs in + if lhs !== rhs { + return false + } + return true + }) + |> map { account in + if let account = account { + let context = AccountContext(sharedContext: sharedContext, window: Window(contentRect: NSZeroRect, styleMask: [], backing: NSWindow.BackingStoreType.buffered, defer: true), account: account) + return AuthorizedApplicationContext(context: context, shareContext: extensionContext) + + } else { + return nil + } + }) } + } diff --git a/TelegramShare/TelegramShare.entitlements b/TelegramShare/TelegramShare.entitlements index 1e99545733..c0559cf67f 100644 --- a/TelegramShare/TelegramShare.entitlements +++ b/TelegramShare/TelegramShare.entitlements @@ -6,7 +6,8 @@ com.apple.security.application-groups - $(TeamIdentifierPrefix)ru.keepcoder.Telegram + 6N38VWS5BX.ru.keepcoder.Telegram.TelegramShare + 6N38VWS5BX.ru.keepcoder.Telegram com.apple.security.files.user-selected.read-only diff --git a/buildbox/appdiff.py b/buildbox/appdiff.py new file mode 100644 index 0000000000..f5faa3d1a8 --- /dev/null +++ b/buildbox/appdiff.py @@ -0,0 +1,275 @@ +import sys +import os +import glob +import tempfile +import re +import filecmp +import subprocess + + +def get_file_list(dir): + result_files = [] + result_dirs = [] + for root, dirs, files in os.walk(dir, topdown=False): + for name in files: + result_files.append(os.path.relpath(os.path.join(root, name), dir)) + for name in dirs: + result_dirs.append(os.path.relpath(os.path.join(root, name), dir)) + return set(result_dirs), set(result_files) + + +def remove_codesign_dirs(dirs): + result = set() + for dir in dirs: + if dir == '_CodeSignature': + continue + if re.match('PlugIns/.*\\.appex/SC_Info', dir): + continue + if re.match('Frameworks/.*\\.framework/SC_Info', dir): + continue + result.add(dir) + return result + + +def remove_codesign_files(files): + result = set() + for f in files: + if f == 'Contents/embedded.provisionprofile': + continue + if re.match('Contents/.*/.*\\.appex/embedded.provisionprofile', f): + continue + if re.match('Contents/_CodeSignature/CodeResources', f): + continue + if f == 'Contents/CodeResources': + continue + if re.match('Contents/PlugIns/.*\\.appex/Contents/_CodeSignature', f): + continue + if re.match('Contents/Frameworks/.*\\.framework/Versions/A/_CodeSignature', f): + continue + result.add(f) + return result + + + +def remove_plugin_files(files): + result = set() + excluded = set() + for f in files: + if False and re.match('PlugIns/.*', f): + excluded.add(f) + else: + result.add(f) + return (result, excluded) + + +def remove_asset_files(files): + result = set() + excluded = set() + for f in files: + if re.match('.*\\.car', f): + excluded.add(f) + else: + result.add(f) + return (result, excluded) + + +def remove_nib_files(files): + result = set() + excluded = set() + for f in files: + result.add(f) + return (result, excluded) + + +def diff_dirs(app1, dir1, app2, dir2): + only_in_app1 = dir1.difference(dir2) + only_in_app2 = dir2.difference(dir1) + if len(only_in_app1) == 0 and len(only_in_app2) == 0: + return + print('Directory structure doesn\'t match in ' + app1 + ' and ' + app2) + if len(only_in_app1) != 0: + print('Directories not present in ' + app2) + for dir in only_in_app1: + print(' ' + dir) + if len(only_in_app2) != 0: + print('Directories not present in ' + app1) + for dir in only_in_app2: + print(' ' + dir) + + sys.exit(1) + + +def is_binary(file): + out = os.popen('file "' + file + '"').read() + if out.find('Mach-O') == -1: + return False + return True + + +def is_xcconfig(file): + if re.match('.*\\.xcconfig', file): + return True + else: + return False + + +def diff_binaries(tempdir, self_base_path, file1, file2): + diff_app = tempdir + '/main' + if not os.path.isfile(diff_app): + if not os.path.isfile(self_base_path + '/main.cpp'): + print('Could not find ' + self_base_path + '/main.cpp') + sys.exit(1) + subprocess.call(['clang', self_base_path + '/main.cpp', '-lc++', '-o', diff_app]) + if not os.path.isfile(diff_app): + print('Could not compile ' + self_base_path + '/main.cpp') + sys.exit(1) + + result = os.popen(diff_app + ' ' + file1 + ' ' + file2).read().strip() + if result == 'Encrypted': + return 'binary_encrypted' + elif result == 'Equal': + return 'equal' + elif result == 'Not Equal': + return 'not_equal' + else: + print('Unexpected data from binary diff code: ' + result) + sys.exit(1) + + +def is_plist(file1): + if file1.find('.plist') == -1: + return False + return True + + +def diff_plists(file1, file2): + remove_properties = ['UISupportedDevices', 'DTAppStoreToolsBuild', 'MinimumOSVersion', 'BuildMachineOSBuild'] + + clean1_properties = '' + clean2_properties = '' + + with open(os.devnull, 'w') as devnull: + for property in remove_properties: + if not subprocess.call(['plutil', '-extract', property, 'xml1', '-o', '-', file1], stderr=devnull, stdout=devnull): + clean1_properties += ' | plutil -remove ' + property + ' -r -o - -- -' + if not subprocess.call(['plutil', '-extract', property, 'xml1', '-o', '-', file2], stderr=devnull, stdout=devnull): + clean2_properties += ' | plutil -remove ' + property + ' -r -o - -- -' + + data1 = os.popen('plutil -convert xml1 "' + file1 + '" -o -' + clean1_properties).read() + data2 = os.popen('plutil -convert xml1 "' + file2 + '" -o -' + clean2_properties).read() + + if data1 == data2: + return 'equal' + else: + with open('lhs.plist', 'wb') as f: + f.write(str.encode(data1)) + with open('rhs.plist', 'wb') as f: + f.write(str.encode(data2)) + sys.exit(1) + return 'not_equal' + + +def diff_xcconfigs(file1, file2): + with open(file1, 'rb') as f: + data1 = f.read().strip() + with open(file2, 'rb') as f: + data2 = f.read().strip() + if data1 != data2: + return 'not_equal' + return 'equal' + + +def diff_files(app1, files1, app2, files2): + only_in_app1 = files1.difference(files2) + only_in_app2 = files2.difference(files1) + if len(only_in_app1) == 0 and len(only_in_app2) == 0: + return + if len(only_in_app1) != 0: + print('Files not present in ' + app2) + for f in only_in_app1: + print(' ' + f) + if len(only_in_app2) != 0: + print('Files not present in ' + app1) + for f in only_in_app2: + print(' ' + f) + + sys.exit(1) + + +def base_app_dir(path): + return path + + +def diff_file(tempdir, self_base_path, path1, path2): + if is_plist(path1): + return diff_plists(path1, path2) + elif is_binary(path1): + return diff_binaries(tempdir, self_base_path, path1, path2) + elif is_xcconfig(path1): + return diff_xcconfigs(path1, path2) + else: + if filecmp.cmp(path1, path2): + return 'equal' + return 'not_equal' + + +def appdiff(self_base_path, app1, app2): + tempdir = tempfile.mkdtemp() + + app1_dir = app1 + app2_dir = app2 + + (app1_dirs, app1_files) = get_file_list(base_app_dir(app1_dir)) + (app2_dirs, app2_files) = get_file_list(base_app_dir(app2_dir)) + + clean_app1_dirs = remove_codesign_dirs(app1_dirs) + clean_app2_dirs = remove_codesign_dirs(app2_dirs) + + clean_app1_files = remove_codesign_files(app1_files) + clean_app2_files = remove_codesign_files(app2_files) + + diff_dirs(app1, clean_app1_dirs, app2, clean_app2_dirs) + diff_files(app1, clean_app1_files, app2, clean_app2_files) + + clean_app1_files, plugin_app1_files = remove_plugin_files(clean_app1_files) + clean_app2_files, plugin_app2_files = remove_plugin_files(clean_app2_files) + + clean_app1_files, plugin_app1_files = remove_asset_files(clean_app1_files) + clean_app2_files, plugin_app2_files = remove_asset_files(clean_app2_files) + + clean_app1_files, nib_app1_files = remove_nib_files(clean_app1_files) + clean_app2_files, nib_app2_files = remove_nib_files(clean_app2_files) + + different_files = [] + encrypted_files = [] + for relative_file_path in clean_app1_files: + file_result = diff_file(tempdir, self_base_path, base_app_dir(app1_dir) + '/' + relative_file_path, base_app_dir(app2_dir) + '/' + relative_file_path) + if file_result == 'equal': + pass + elif file_result == 'binary_encrypted': + encrypted_files.append(relative_file_path) + else: + different_files.append(relative_file_path) + + if len(different_files) != 0: + print('Different files in ' + app1 + ' and ' + app2) + for relative_file_path in different_files: + print(' ' + relative_file_path) + else: + if len(encrypted_files) != 0: + print(' Excluded files that couldn\'t be checked due to being encrypted:') + for relative_file_path in encrypted_files: + print(' ' + relative_file_path) + if len(plugin_app1_files) != 0: + print(' APPs contain PlugIns directory with app extensions. Extensions can\'t currently be checked.') + if len(nib_app1_files) != 0: + print(' APPs contain .nib (compiled Interface Builder) files that are compiled by the App Store and can\'t currently be checked:') + for relative_file_path in nib_app1_files: + print(' ' + relative_file_path) + + +if len(sys.argv) != 3: + print('Usage: appdiff app1 app2') + sys.exit(1) + +appdiff(os.path.dirname(sys.argv[0]), sys.argv[1], sys.argv[2]) diff --git a/buildbox/build-vm.sh b/buildbox/build-vm.sh new file mode 100644 index 0000000000..6dac5233ae --- /dev/null +++ b/buildbox/build-vm.sh @@ -0,0 +1,10 @@ +#!/bin/sh + + +export PATH="$PATH:$HOME/.fastlane/bin" + +BUILD_CONFIGURATION=$1 + +cd ~/build +fastlane $BUILD_CONFIGURATION +tar cf "./output/Telegram.tar" -C "./output" . diff --git a/buildbox/build.sh b/buildbox/build.sh new file mode 100644 index 0000000000..aac24c4906 --- /dev/null +++ b/buildbox/build.sh @@ -0,0 +1,70 @@ +#!/bin/bash +set -x +set -e + + +#if [ "$1" = "alpha" ] || [ "$1" = "beta" ] ; then +MACOS_VERSION="10.16" +XCODE_VERSION="12.2" +#else +#MACOS_VERSION="10.15" +#XCODE_VERSION="10.3" +#fi + + +GUEST_SHELL="bash" + + +VM_BASE_NAME="macos$(echo $MACOS_VERSION | sed -e 's/\.'/_/g)_Xcode$(echo $XCODE_VERSION | sed -e 's/\.'/_/g)" + +BUILD_MACHINE="macOS"; + + +BUILDBOX_DIR="buildbox" +BUILD_CONFIGURATION="$1" + +rm -rf "$HOME/build-$BUILD_CONFIGURATION" +mkdir -p "$HOME/build-$BUILD_CONFIGURATION" + +PROCESS_ID="$$" +VM_NAME="$VM_BASE_NAME-$(openssl rand -hex 10)-build-telegram-$PROCESS_ID" + +prlctl clone "$VM_BASE_NAME" --linked --name "$VM_NAME" +prlctl start "$VM_NAME" + + +rm -f "$HOME/build-$BUILD_CONFIGURATION/Telegram.tar" +tar cf "$HOME/build-$BUILD_CONFIGURATION/Telegram.tar" --exclude "$BUILDBOX_DIR" --exclude ".git" --exclude "./submodules/telegram-ios/.git" --exclude "./submodules/rlottie/.git" --exclude "./submodules/Sparkle/.git" --exclude "./submodules/ton/.git" --exclude "./submodules/Zip/.git" --exclude "./submodules/libtgvoip/.git" "." + + + +while [ 1 ]; do + TEST_IP=$(prlctl exec "$VM_NAME" "ifconfig | grep inet | grep broadcast | grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -1 | tr '\n' '\0'" || echo "") + if [ ! -z "$TEST_IP" ]; then + RESPONSE=$(ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null telegram@"$TEST_IP" -o ServerAliveInterval=60 -t "echo -n 1") + if [ "$RESPONSE" == "1" ]; then + VM_IP="$TEST_IP" + break + fi + fi +sleep 1 +done +# +ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null telegram@"$VM_IP" -o ServerAliveInterval=60 -t "rm -rf build/" +ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null telegram@"$VM_IP" -o ServerAliveInterval=60 -t "mkdir -p build;" +scp -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -pr "$HOME/build-$BUILD_CONFIGURATION/Telegram.tar" telegram@"$VM_IP":build + +ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null telegram@"$VM_IP" -o ServerAliveInterval=60 -t "tar -xf build/Telegram.tar -C ./build" +ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null telegram@"$VM_IP" -o ServerAliveInterval=60 -t "mkdir -p build/buildbox" +scp -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -pr "$BUILDBOX_DIR/build-vm.sh" telegram@"$VM_IP":build/buildbox +ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null telegram@"$VM_IP" -o ServerAliveInterval=60 -t "$GUEST_SHELL -l build/buildbox/build-vm.sh $BUILD_CONFIGURATION" || true + +scp -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -pr telegram@"$VM_IP":build/output/Telegram.tar "$HOME/build-$BUILD_CONFIGURATION/Telegram.tar" + + +tar -xf "$HOME/build-$BUILD_CONFIGURATION/Telegram.tar" -C "$HOME/build-$BUILD_CONFIGURATION" +rm -f "$HOME/build-$BUILD_CONFIGURATION/Telegram.tar" + +prlctl stop "$VM_NAME" --kill +prlctl delete "$VM_NAME" + diff --git a/buildbox/cleanup-telegram-build-vms.sh b/buildbox/cleanup-telegram-build-vms.sh new file mode 100644 index 0000000000..6e79d27856 --- /dev/null +++ b/buildbox/cleanup-telegram-build-vms.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +case "$(uname -s)" in +Linux*) BUILD_MACHINE=linux;; +Darwin*) BUILD_MACHINE=macOS;; +*) BUILD_MACHINE="" +esac + +function list_include_item { +local list="$1" +local item="$2" +if [[ $list =~ (^|[[:space:]])"$item"($|[[:space:]]) ]] ; then +result=0 +else +result=1 +fi +return $result +} + +function clean_once { +RUNNING_PIDS=$(pgrep -f buildbox/build-telegram.sh) + +if [ "$BUILD_MACHINE" == "linux" ]; then +virsh list --all --name | grep build-telegram | while read vm ; do +VM_PID=$(echo $vm | egrep -o 'build-telegram-[0-9]+' | egrep -o '[0-9]+') +if [ ! -z "$VM_PID" ] && [ ! -z "$vm" ]; then +if `list_include_item "$RUNNING_PIDS" "$VM_PID"` ; then +echo "$vm:$VM_PID is still valid" +else +virsh destroy "$vm" || true +virsh undefine "$vm" --remove-all-storage --nvram || true +fi +else +echo "Can't parse VM string $vm" +fi +done +elif [ "$BUILD_MACHINE" == "macOS" ]; then +prlctl list -a | grep build-telegram | while read vm ; do +VM_PID=$(echo $vm | grep -Eo 'build-telegram-\d+' | grep -Eo '\d+') +VM_UUID=$(echo $vm | grep -Eo '\{(\d|[a-f]|-)*\}') +if [ ! -z "$VM_PID" ] && [ ! -z "$VM_UUID" ]; then +if `list_include_item "$RUNNING_PIDS" "$VM_PID"` ; then +echo "$VM_UUID:$VM_PID is still valid" +else +prlctl stop "$VM_UUID" --kill || true +prlctl delete "$VM_UUID" || true +fi +else +echo "Can't parse VM string $vm" +fi +done +else +echo "Unknown build machine $(uname -s)" +fi +} + +if [ "$1" == "loop" ]; then +while [ 1 ]; do +clean_once +sleep 10 +done +else +clean_once +fi diff --git a/buildbox/internal.sh b/buildbox/internal.sh new file mode 100644 index 0000000000..566a0d8936 --- /dev/null +++ b/buildbox/internal.sh @@ -0,0 +1,12 @@ +#/bin/sh +set -x +set -e + +export PATH="$PATH:$PWD/../deploy" +export PATH="$PATH:$HOME/.fastlane/bin" + +tag="$1" +sh ./buildbox/cleanup-telegram-build-vms.sh +sh ./buildbox/build.sh $tag +sh deploy-$tag.sh ~/build-$tag $PWD Telegram.app ~/.credentials/dsa-$tag +rm -rf ~/build-$tag diff --git a/buildbox/main.cpp b/buildbox/main.cpp new file mode 100644 index 0000000000..99c513eda5 --- /dev/null +++ b/buildbox/main.cpp @@ -0,0 +1,262 @@ +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +static uint32_t funcSwap32(uint32_t input) { + return OSSwapBigToHostInt32(input); +} + +static uint32_t funcNoSwap32(uint32_t input) { + return OSSwapLittleToHostInt32(input); +} + +static bool cleanArch(std::vector &archData, bool &isEncrypted) { + uint32_t (*swap32)(uint32_t) = funcNoSwap32; + + uint32_t offset = 0; + + const struct mach_header* header = (struct mach_header*)(archData.data() + offset); + + switch (header->magic) { + case MH_CIGAM: + swap32 = funcSwap32; + case MH_MAGIC: + offset += sizeof(struct mach_header); + break; + case MH_CIGAM_64: + swap32 = funcSwap32; + case MH_MAGIC_64: + offset += sizeof(struct mach_header_64); + break; + default: + return nullptr; + } + + uint32_t commandCount = swap32(header->ncmds); + + for (uint32_t i = 0; i < commandCount; i++) { + const struct load_command* loadCommand = (const struct load_command*)(archData.data() + offset); + uint32_t commandSize = swap32(loadCommand->cmdsize); + + uint32_t commandType = swap32(loadCommand->cmd); + if (commandType == LC_CODE_SIGNATURE) { + const struct linkedit_data_command *dataCommand = (const struct linkedit_data_command *)(archData.data() + offset); + uint32_t dataOffset = swap32(dataCommand->dataoff); + uint32_t dataSize = swap32(dataCommand->datasize); + + // account for different signature size + memset(archData.data() + offset + offsetof(linkedit_data_command, datasize), 0, sizeof(uint32_t)); + + // remove signature + archData.erase(archData.begin() + dataOffset, archData.begin() + dataOffset + dataSize); + } else if (commandType == LC_SEGMENT_64) { + const struct segment_command_64 *segmentCommand = (const struct segment_command_64 *)(archData.data() + offset); + std::string segmentName = std::string(segmentCommand->segname); + if (segmentName == "__LINKEDIT") { + // account for different signature size + memset(archData.data() + offset + offsetof(segment_command_64, vmsize), 0, sizeof(uint32_t)); + // account for different file size because of signatures + memset(archData.data() + offset + offsetof(segment_command_64, filesize), 0, sizeof(uint32_t)); + } + } else if (commandType == LC_ID_DYLIB) { + // account for dylib timestamp + memset(archData.data() + offset + offsetof(dylib_command, dylib) + offsetof(struct dylib, timestamp), 0, sizeof(uint32_t)); + } else if (commandType == LC_UUID) { + // account for dylib uuid + memset(archData.data() + offset + offsetof(uuid_command, uuid), 0, 16); + } else if (commandType == LC_ENCRYPTION_INFO_64) { + const struct encryption_info_command_64 *encryptionInfoCommand = (const struct encryption_info_command_64 *)(archData.data() + offset); + if (encryptionInfoCommand->cryptid != 0) { + isEncrypted = true; + } + } + + offset += commandSize; + } + + return true; +} + +static std::vector parseFat(std::vector const &fileData) { + size_t offset = 0; + + const struct fat_header *fatHeader = (const struct fat_header *)fileData.data(); + offset += sizeof(*fatHeader); + + size_t initialOffset = offset; + + uint32_t archCount = OSSwapBigToHostInt32(fatHeader->nfat_arch); + + for (uint32_t i = 0; i < archCount; i++) { + const struct fat_arch *arch = (const struct fat_arch *)(fileData.data() + offset); + offset += sizeof(*arch); + + uint32_t archOffset = OSSwapBigToHostInt32(arch->offset); + uint32_t archSize = OSSwapBigToHostInt32(arch->size); + cpu_type_t cputype = OSSwapBigToHostInt32(arch->cputype); + + if (cputype == CPU_TYPE_ARM64) { + std::vector archData; + archData.resize(archSize); + memcpy(archData.data(), fileData.data() + archOffset, archSize); + return archData; + } + } + + offset = initialOffset; + + for (uint32_t i = 0; i < archCount; i++) { + const struct fat_arch *arch = (const struct fat_arch *)(fileData.data() + offset); + offset += sizeof(*arch); + + uint32_t archOffset = OSSwapBigToHostInt32(arch->offset); + uint32_t archSize = OSSwapBigToHostInt32(arch->size); + cpu_type_t cputype = OSSwapBigToHostInt32(arch->cputype); + cpu_type_t cpusubtype = OSSwapBigToHostInt32(arch->cpusubtype); + + if (cputype == CPU_TYPE_ARM && cpusubtype == CPU_SUBTYPE_ARM_V7K) { + std::vector archData; + archData.resize(archSize); + memcpy(archData.data(), fileData.data() + archOffset, archSize); + return archData; + } + } + + return std::vector(); +} + +static std::vector parseMachO(std::vector const &fileData) { + const uint32_t *magic = (const uint32_t *)fileData.data(); + + if (*magic == FAT_CIGAM || *magic == FAT_MAGIC) { + return parseFat(fileData); + } else { + return fileData; + } +} + +static std::vector readFile(std::string const &file) { + int fd = open(file.c_str(), O_RDONLY); + + if (fd == -1) { + return std::vector(); + } + + struct stat st; + fstat(fd, &st); + + std::vector fileData; + fileData.resize((size_t)st.st_size); + read(fd, fileData.data(), (size_t)st.st_size); + close(fd); + + return fileData; +} + +static void writeDataToFile(std::vector const &data, std::string const &path) { + int fd = open(path.c_str(), O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR); + if (fd == -1) { + return; + } + + write(fd, data.data(), data.size()); + + close(fd); +} + +static std::vector stripSwiftSymbols(std::string const &file) { + std::string command; + command += "xcrun strip -ST -o /dev/stdout \""; + command += file; + command += "\" 2> /dev/null"; + + uint8_t buffer[128]; + std::vector result; + FILE *pipe = popen(command.c_str(), "r"); + if (!pipe) { + throw std::runtime_error("popen() failed!"); + } + while (true) { + size_t readBytes = fread(buffer, 1, 128, pipe); + if (readBytes <= 0) { + break; + } + result.insert(result.end(), buffer, buffer + readBytes); + } + pclose(pipe); + + return result; +} + +static bool endsWith(std::string const &mainStr, std::string const &toMatch) { + if(mainStr.size() >= toMatch.size() && mainStr.compare(mainStr.size() - toMatch.size(), toMatch.size(), toMatch) == 0) { + return true; + } else { + return false; + } +} + +int main(int argc, const char *argv[]) { + if (argc != 3) { + printf("Usage: machofilediff file1 file2\n"); + return 1; + } + + std::string file1 = argv[1]; + std::string file2 = argv[2]; + + std::vector fileData1; + if (endsWith(file1, ".dylib")) { + fileData1 = stripSwiftSymbols(file1); + } else { + fileData1 = readFile(file1); + } + + std::vector fileData2; + if (endsWith(file2, ".dylib")) { + fileData2 = stripSwiftSymbols(file2); + } else { + fileData2 = readFile(file2); + } + + std::vector arch1 = parseMachO(fileData1); + if (arch1.size() == 0) { + printf("Couldn't parse %s\n", file1.c_str()); + return 1; + } + + std::vector arch2 = parseMachO(fileData2); + if (arch2.size() == 0) { + printf("Couldn't parse %s\n", file2.c_str()); + return 1; + } + + bool arch1Encrypted = false; + bool arch2Encrypted = false; + cleanArch(arch1, arch1Encrypted); + cleanArch(arch2, arch2Encrypted); + + if (arch1 == arch2) { + printf("Equal\n"); + return 0; + } else { + if (arch1Encrypted || arch2Encrypted) { + printf("Encrypted\n"); + } else { + printf("Not Equal\n"); + } + + return 1; + } + + return 0; +} diff --git a/buildbox/sync-toolbox.sh b/buildbox/sync-toolbox.sh new file mode 100644 index 0000000000..73f7316af3 --- /dev/null +++ b/buildbox/sync-toolbox.sh @@ -0,0 +1,17 @@ +#/bin/sh +set -x +set -e + +export PATH="$PATH:$HOME/.credentials" +source variables.sh +MAIN_REPOSITORY=$PWD +cd .. +if [ ! -d "./deploy" ]; then + mkdir -p ./deploy + git clone "$deploy_repository" deploy +else + cd ./deploy + git reset --hard + git pull origin master + cd .. +fi diff --git a/core-xprojects/CoreMediaMacCapture/CoreMediaMacCapture/Capturer.h b/core-xprojects/CoreMediaMacCapture/CoreMediaMacCapture/Capturer.h new file mode 100644 index 0000000000..d230870648 --- /dev/null +++ b/core-xprojects/CoreMediaMacCapture/CoreMediaMacCapture/Capturer.h @@ -0,0 +1,28 @@ +// +// Capturer.h +// CoreMediaMacCapture +// +// Created by Mikhail Filimonov on 21.06.2021. +// + +#import +#import +NS_ASSUME_NONNULL_BEGIN + + +typedef void(^renderBlock)(CMSampleBufferRef); + + +@interface CoreMediaCapturer : NSObject + + +-(id)initWithDeviceId:(NSString *)deviceId; + +-(void)start:(renderBlock)renderBlock; +-(void)stop; + + + +@end + +NS_ASSUME_NONNULL_END diff --git a/core-xprojects/CoreMediaMacCapture/CoreMediaMacCapture/Capturer.m b/core-xprojects/CoreMediaMacCapture/CoreMediaMacCapture/Capturer.m new file mode 100644 index 0000000000..7dbee4a04b --- /dev/null +++ b/core-xprojects/CoreMediaMacCapture/CoreMediaMacCapture/Capturer.m @@ -0,0 +1,45 @@ +// +// Capturer.m +// CoreMediaMacCapture +// +// Created by Mikhail Filimonov on 21.06.2021. +// + +#import "Capturer.h" +#import "CoreMediaVideoHAL.h" + +@interface CoreMediaCapturer () + +@end + +@implementation CoreMediaCapturer +{ + NSString * _deviceId; + Device * _device; +} +-(id)initWithDeviceId:(NSString *)deviceId { + if (self = [super init]) { + _deviceId = deviceId; + + } + return self; +} + +-(void)start:(renderBlock)renderBlock { + _device = [Device FindDeviceByUniqueId:_deviceId]; + + [_device run:^(CMSampleBufferRef sampleBuffer) { + renderBlock(sampleBuffer); + }]; + + +} +-(void)stop { + [_device stop]; +} + +-(void)dealloc { + +} + +@end diff --git a/core-xprojects/CoreMediaMacCapture/CoreMediaMacCapture/CoreMediaMacCapture.h b/core-xprojects/CoreMediaMacCapture/CoreMediaMacCapture/CoreMediaMacCapture.h new file mode 100644 index 0000000000..dac0312474 --- /dev/null +++ b/core-xprojects/CoreMediaMacCapture/CoreMediaMacCapture/CoreMediaMacCapture.h @@ -0,0 +1,19 @@ +// +// CoreMediaMacCapture.h +// CoreMediaMacCapture +// +// Created by Mikhail Filimonov on 21.06.2021. +// + +#import + +//! Project version number for CoreMediaMacCapture. +FOUNDATION_EXPORT double CoreMediaMacCaptureVersionNumber; + +//! Project version string for CoreMediaMacCapture. +FOUNDATION_EXPORT const unsigned char CoreMediaMacCaptureVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + +#import diff --git a/core-xprojects/CoreMediaMacCapture/CoreMediaMacCapture/CoreMediaVideoHAL.h b/core-xprojects/CoreMediaMacCapture/CoreMediaMacCapture/CoreMediaVideoHAL.h new file mode 100644 index 0000000000..2e68c85340 --- /dev/null +++ b/core-xprojects/CoreMediaMacCapture/CoreMediaMacCapture/CoreMediaVideoHAL.h @@ -0,0 +1,25 @@ +// +// CoreMediaVideoHAL.h +// CoreMediaMacCapture +// +// Created by Mikhail Filimonov on 21.06.2021. +// + +#import +#import +NS_ASSUME_NONNULL_BEGIN + +typedef void(^RenderBlock)(CMSampleBufferRef); + +@interface Device : NSObject + ++(Device * __nullable)FindDeviceByUniqueId:(NSString *)pUID; + +-(void)run:(RenderBlock)render; +-(void)stop; +@end + + + + +NS_ASSUME_NONNULL_END diff --git a/core-xprojects/CoreMediaMacCapture/CoreMediaMacCapture/CoreMediaVideoHAL.mm b/core-xprojects/CoreMediaMacCapture/CoreMediaMacCapture/CoreMediaVideoHAL.mm new file mode 100644 index 0000000000..315518ec06 --- /dev/null +++ b/core-xprojects/CoreMediaMacCapture/CoreMediaMacCapture/CoreMediaVideoHAL.mm @@ -0,0 +1,215 @@ +// +// CoreMediaVideoHAL.m +// CoreMediaMacCapture +// +// Created by Mikhail Filimonov on 21.06.2021. +// + +#import "CoreMediaVideoHAL.h" + +#import +#import + + + +@interface Device () +{ + CMIODeviceID _deviceId; + CMIOStreamID _streamId; + CMSimpleQueueRef _queueRef; + RenderBlock _renderBlock; +} + +-(CMSimpleQueueRef)queue; +-(RenderBlock)block; + +@end + +static void handleStreamQueueAltered(CMIOStreamID streamID, void* token, void* refCon) { + CMSampleBufferRef sb = 0; + + Device *renderBlock = (__bridge Device *)refCon; + CMSimpleQueueRef queueRef = [renderBlock queue]; + + while(0 != (sb = (CMSampleBufferRef)CMSimpleQueueDequeue(queueRef))) { + renderBlock.block(sb); + CFRelease(sb); + } +} + +OSStatus GetPropertyData(CMIOObjectID objID, int32_t sel, CMIOObjectPropertyScope scope, + UInt32 qualifierDataSize, const void* qualifierData, UInt32 dataSize, + UInt32& dataUsed, void* data) { + CMIOObjectPropertyAddress addr={ (CMIOObjectPropertySelector)sel, scope, + kCMIOObjectPropertyElementMaster }; + return CMIOObjectGetPropertyData(objID, &addr, qualifierDataSize, qualifierData, + dataSize, &dataUsed, data); +} +OSStatus GetPropertyData(CMIOObjectID objID, int32_t selector, UInt32 qualifierDataSize, + const void* qualifierData, UInt32 dataSize, UInt32& dataUsed, + void* data) { + return GetPropertyData(objID, selector, 0, qualifierDataSize, + qualifierData, dataSize, dataUsed, data); +} + +OSStatus GetPropertyDataSize(CMIOObjectID objID, int32_t sel, + CMIOObjectPropertyScope scope, uint32_t& size) { + CMIOObjectPropertyAddress addr={ (CMIOObjectPropertySelector)sel, scope, + kCMIOObjectPropertyElementMaster }; + return CMIOObjectGetPropertyDataSize(objID, &addr, 0, 0, &size); +} + +OSStatus GetPropertyDataSize(CMIOObjectID objID, int32_t selector, uint32_t& size) { + return GetPropertyDataSize(objID, selector, 0, size); +} + +OSStatus GetNumberDevices(uint32_t& cnt) { + if(0 != GetPropertyDataSize(kCMIOObjectSystemObject, kCMIOHardwarePropertyDevices, cnt)) + return -1; + cnt /= sizeof(CMIODeviceID); + return 0; +} + +OSStatus GetDevices(uint32_t& cnt, CMIODeviceID* pDevs) { + OSStatus status; + uint32_t numberDevices = 0, used = 0; + if((status = GetNumberDevices(numberDevices)) < 0) + return status; + if(numberDevices > (cnt = numberDevices)) + return -1; + uint32_t size = numberDevices * sizeof(CMIODeviceID); + return GetPropertyData(kCMIOObjectSystemObject, kCMIOHardwarePropertyDevices, + 0, NULL, size, used, pDevs); +} + +template< const int C_Size > +OSStatus GetDeviceStrProp(CMIOObjectID objID, CMIOObjectPropertySelector sel, + char (&pValue)[C_Size]) { + CFStringRef answer = NULL; + UInt32 dataUsed= 0; + OSStatus status = GetPropertyData(objID, sel, 0, NULL, sizeof(answer), + dataUsed, &answer); + if(0 == status)// SUCCESS + CFStringCopyUTF8String(answer, pValue); + return status; +} + +template< const int C_Size > +Boolean CFStringCopyUTF8String(CFStringRef aString, char (&pText)[C_Size]) { + CFIndex length = CFStringGetLength(aString); + if(sizeof(pText) < (length + 1)) + return false; + CFIndex maxSize = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8); + return CFStringGetCString(aString, pText, maxSize, kCFStringEncodingUTF8); +} + + + +uint32_t GetNumberInputStreams(CMIODeviceID devID) +{ + uint32 size = 0; + GetPropertyDataSize(devID, kCMIODevicePropertyStreams, + kCMIODevicePropertyScopeInput, size); + return size / sizeof(CMIOStreamID); +} +OSStatus GetInputStreams(CMIODeviceID devID, uint32_t& + ioNumberStreams, CMIOStreamID* streamList) +{ + ioNumberStreams = MIN(GetNumberInputStreams(devID), ioNumberStreams); + uint32_t size = ioNumberStreams * sizeof(CMIOStreamID); + uint32_t dataUsed = 0; + OSStatus err = GetPropertyData(devID, kCMIODevicePropertyStreams, + kCMIODevicePropertyScopeInput, 0, + NULL, size, dataUsed, streamList); + if(0 != err) + return err; + ioNumberStreams = size / sizeof(CMIOStreamID); + CMIOStreamID* firstItem = &(streamList[0]); + CMIOStreamID* lastItem = firstItem + ioNumberStreams; + + //std::sort(firstItem, lastItem); + return 0; +} + + + +@implementation Device + +-(id)initWithDeviceId:(CMIODeviceID)deviceId streamId:(CMIOStreamID)streamId { + if (self = [super init]) { + _deviceId = deviceId; + _streamId = streamId; + } + return self; +} + +-(CMIODeviceID)cmioDevice { + return _deviceId; +} + ++(Device *)FindDeviceByUniqueId:(NSString *)pUID { + OSStatus status = 0; + uint32_t numDev = 0; + if(((status = GetNumberDevices(numDev)) < 0) || (0 == numDev)) + return nil; + // Allocate memory on the stack + CMIODeviceID* pDevs = (CMIODeviceID*)alloca(numDev * sizeof(*pDevs)); + if((status = GetDevices(numDev, pDevs)) < 0) + return nil; + for(uint32_t i = 0; i < numDev; i++) { + char pUniqueID[64]; + if((status = GetDeviceStrProp(pDevs[i], kCMIODevicePropertyDeviceUID, pUniqueID)) < 0) + break; + status = afpObjectNotFound;// Not Found… + if(0 != strcmp([pUID UTF8String], pUniqueID)) + continue; + + uint32_t numStreams = GetNumberInputStreams(pDevs[i]); + CMIOStreamID* pStreams = (CMIOStreamID*)alloca(numStreams * sizeof(CMIOStreamID)); + GetInputStreams(pDevs[i], numStreams, pStreams); + if (numStreams <= 0) + return nil; + + + CMIOStreamID streamId = pStreams[0]; +// + return [[Device alloc] initWithDeviceId:pDevs[i] streamId:streamId]; + } + + return nil; +} + +-(CMSimpleQueueRef)queue { + return _queueRef; +} +-(RenderBlock)block { + return _renderBlock; +} + +-(void)run:(RenderBlock)render { + _renderBlock = render; + + + + CMIOStreamCopyBufferQueue(_streamId, handleStreamQueueAltered, (void*)CFBridgingRetain(self), &_queueRef); + + CMIODeviceStartStream(_deviceId, _streamId); + +} + +/* + + */ + +-(void)stop { + CMIODeviceStopStream(_deviceId, _streamId); + CMIOStreamCopyBufferQueue(_streamId, nil, nil, &_queueRef); + if (_queueRef) + CFRelease(_queueRef); +} + +-(void)dealloc { + [self stop]; +} + +@end diff --git a/core-xprojects/CoreMediaMacCapture/CoreMediaMacCapture/Info.plist b/core-xprojects/CoreMediaMacCapture/CoreMediaMacCapture/Info.plist new file mode 100644 index 0000000000..9bcb244429 --- /dev/null +++ b/core-xprojects/CoreMediaMacCapture/CoreMediaMacCapture/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/core-xprojects/CryptoUtils/CryptoUtils.xcodeproj/project.pbxproj b/core-xprojects/CryptoUtils/CryptoUtils.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..fe63587036 --- /dev/null +++ b/core-xprojects/CryptoUtils/CryptoUtils.xcodeproj/project.pbxproj @@ -0,0 +1,739 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + A7918FEB240CF659002011CA /* CryptoUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = A7918FE9240CF659002011CA /* CryptoUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7918FF9240CF6C8002011CA /* Crypto.m in Sources */ = {isa = PBXBuildFile; fileRef = A7918FF8240CF6C8002011CA /* Crypto.m */; }; + A7918FFC240CF6D9002011CA /* Crypto.h in Headers */ = {isa = PBXBuildFile; fileRef = A7918FFB240CF6D9002011CA /* Crypto.h */; settings = {ATTRIBUTES = (Public, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A7918FE6240CF659002011CA /* CryptoUtils.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CryptoUtils.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7918FE9240CF659002011CA /* CryptoUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CryptoUtils.h; sourceTree = ""; }; + A7918FEA240CF659002011CA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A7918FF8240CF6C8002011CA /* Crypto.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Crypto.m; path = "../../../../submodules/telegram-ios/submodules/CryptoUtils/Sources/Crypto.m"; sourceTree = ""; }; + A7918FFB240CF6D9002011CA /* Crypto.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Crypto.h; path = "../../../../submodules/telegram-ios/submodules/CryptoUtils/PublicHeaders/CryptoUtils/Crypto.h"; sourceTree = ""; }; + A791903A240CF9DB002011CA /* MtProtoKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MtProtoKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A7918FE3240CF659002011CA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A7918FDC240CF659002011CA = { + isa = PBXGroup; + children = ( + A7918FE8240CF659002011CA /* CryptoUtils */, + A7918FE7240CF659002011CA /* Products */, + A7919039240CF9DB002011CA /* Frameworks */, + ); + sourceTree = ""; + }; + A7918FE7240CF659002011CA /* Products */ = { + isa = PBXGroup; + children = ( + A7918FE6240CF659002011CA /* CryptoUtils.framework */, + ); + name = Products; + sourceTree = ""; + }; + A7918FE8240CF659002011CA /* CryptoUtils */ = { + isa = PBXGroup; + children = ( + A7918FFA240CF6CF002011CA /* Headers */, + A7918FF7240CF6BD002011CA /* Sources */, + A7918FE9240CF659002011CA /* CryptoUtils.h */, + A7918FEA240CF659002011CA /* Info.plist */, + ); + path = CryptoUtils; + sourceTree = ""; + }; + A7918FF7240CF6BD002011CA /* Sources */ = { + isa = PBXGroup; + children = ( + A7918FF8240CF6C8002011CA /* Crypto.m */, + ); + path = Sources; + sourceTree = ""; + }; + A7918FFA240CF6CF002011CA /* Headers */ = { + isa = PBXGroup; + children = ( + A7918FFB240CF6D9002011CA /* Crypto.h */, + ); + path = Headers; + sourceTree = ""; + }; + A7919039240CF9DB002011CA /* Frameworks */ = { + isa = PBXGroup; + children = ( + A791903A240CF9DB002011CA /* MtProtoKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + A7918FE1240CF659002011CA /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + A7918FEB240CF659002011CA /* CryptoUtils.h in Headers */, + A7918FFC240CF6D9002011CA /* Crypto.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + A7918FE5240CF659002011CA /* CryptoUtils */ = { + isa = PBXNativeTarget; + buildConfigurationList = A7918FEE240CF659002011CA /* Build configuration list for PBXNativeTarget "CryptoUtils" */; + buildPhases = ( + A7918FE1240CF659002011CA /* Headers */, + A7918FE2240CF659002011CA /* Sources */, + A7918FE3240CF659002011CA /* Frameworks */, + A7918FE4240CF659002011CA /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = CryptoUtils; + productName = CryptoUtils; + productReference = A7918FE6240CF659002011CA /* CryptoUtils.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A7918FDD240CF659002011CA /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1030; + ORGANIZATIONNAME = Telegram; + TargetAttributes = { + A7918FE5240CF659002011CA = { + CreatedOnToolsVersion = 10.3; + }; + }; + }; + buildConfigurationList = A7918FE0240CF659002011CA /* Build configuration list for PBXProject "CryptoUtils" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = A7918FDC240CF659002011CA; + productRefGroup = A7918FE7240CF659002011CA /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A7918FE5240CF659002011CA /* CryptoUtils */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A7918FE4240CF659002011CA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A7918FE2240CF659002011CA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A7918FF9240CF6C8002011CA /* Crypto.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A7918FEC240CF659002011CA /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + A7918FED240CF659002011CA /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + A7918FEF240CF659002011CA /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = CryptoUtils/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.CryptoUtils; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = DebugAppStore; + }; + A7918FF0240CF659002011CA /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = CryptoUtils/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.CryptoUtils; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = ReleaseAppStore; + }; + A7918FF1240CF691002011CA /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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 = s; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + A7918FF2240CF691002011CA /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = CryptoUtils/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.CryptoUtils; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = DebugHockeyapp; + }; + A7918FF3240CF69A002011CA /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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 = s; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + A7918FF4240CF69A002011CA /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = CryptoUtils/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.CryptoUtils; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = HockeyappMacAlpha; + }; + A7918FF5240CF6A7002011CA /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + A7918FF6240CF6A7002011CA /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = CryptoUtils/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.CryptoUtils; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = ReleaseHockeyapp; + }; + D0B4941224F3F5D900E0A9B3 /* Github */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + D0B4941324F3F5D900E0A9B3 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = CryptoUtils/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.CryptoUtils; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = Github; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A7918FE0240CF659002011CA /* Build configuration list for PBXProject "CryptoUtils" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A7918FEC240CF659002011CA /* DebugAppStore */, + D0B4941224F3F5D900E0A9B3 /* Github */, + A7918FF3240CF69A002011CA /* HockeyappMacAlpha */, + A7918FF1240CF691002011CA /* DebugHockeyapp */, + A7918FED240CF659002011CA /* ReleaseAppStore */, + A7918FF5240CF6A7002011CA /* ReleaseHockeyapp */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; + A7918FEE240CF659002011CA /* Build configuration list for PBXNativeTarget "CryptoUtils" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A7918FEF240CF659002011CA /* DebugAppStore */, + D0B4941324F3F5D900E0A9B3 /* Github */, + A7918FF4240CF69A002011CA /* HockeyappMacAlpha */, + A7918FF2240CF691002011CA /* DebugHockeyapp */, + A7918FF0240CF659002011CA /* ReleaseAppStore */, + A7918FF6240CF6A7002011CA /* ReleaseHockeyapp */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; +/* End XCConfigurationList section */ + }; + rootObject = A7918FDD240CF659002011CA /* Project object */; +} diff --git a/core-xprojects/CryptoUtils/CryptoUtils.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/CryptoUtils/CryptoUtils.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..84c209f5ea --- /dev/null +++ b/core-xprojects/CryptoUtils/CryptoUtils.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/CryptoUtils/CryptoUtils.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/CryptoUtils/CryptoUtils.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/CryptoUtils/CryptoUtils.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/CryptoUtils/CryptoUtils/CryptoUtils.h b/core-xprojects/CryptoUtils/CryptoUtils/CryptoUtils.h new file mode 100644 index 0000000000..76b67bce53 --- /dev/null +++ b/core-xprojects/CryptoUtils/CryptoUtils/CryptoUtils.h @@ -0,0 +1,19 @@ +// +// CryptoUtils.h +// CryptoUtils +// +// Created by Mikhail Filimonov on 02.03.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +#import + +//! Project version number for CryptoUtils. +FOUNDATION_EXPORT double CryptoUtilsVersionNumber; + +//! Project version string for CryptoUtils. +FOUNDATION_EXPORT const unsigned char CryptoUtilsVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + +#import diff --git a/core-xprojects/CryptoUtils/CryptoUtils/Info.plist b/core-xprojects/CryptoUtils/CryptoUtils/Info.plist new file mode 100644 index 0000000000..861c829fcb --- /dev/null +++ b/core-xprojects/CryptoUtils/CryptoUtils/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2020 Telegram. All rights reserved. + + diff --git a/core-xprojects/EncryptionProviderMac/EncryptionProviderHeader.h b/core-xprojects/EncryptionProviderMac/EncryptionProviderHeader.h new file mode 100644 index 0000000000..9401626d52 --- /dev/null +++ b/core-xprojects/EncryptionProviderMac/EncryptionProviderHeader.h @@ -0,0 +1,19 @@ +// +// EncryptionProvider.h +// EncryptionProvider +// +// Created by Mikhail Filimonov on 31.10.2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +#import + +//! Project version number for EncryptionProvider. +FOUNDATION_EXPORT double EncryptionProviderVersionNumber; + +//! Project version string for EncryptionProvider. +FOUNDATION_EXPORT const unsigned char EncryptionProviderVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + +#import diff --git a/core-xprojects/EncryptionProviderMac/EncryptionProvider_Xcode.xcodeproj/project.pbxproj b/core-xprojects/EncryptionProviderMac/EncryptionProvider_Xcode.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..7739ddfa6b --- /dev/null +++ b/core-xprojects/EncryptionProviderMac/EncryptionProvider_Xcode.xcodeproj/project.pbxproj @@ -0,0 +1,1027 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + A790A30C236B072F000451B5 /* EncryptionProviderHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = A790A2A0236AFA72000451B5 /* EncryptionProviderHeader.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7918DB6240CED79002011CA /* EncryptionProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = A7918DB5240CED78002011CA /* EncryptionProvider.h */; settings = {ATTRIBUTES = (Public, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A72D452A236AF6770052FA81 /* EncryptionProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EncryptionProvider.h; sourceTree = ""; }; + A72D4534236AF6F40052FA81 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + A790A29E236AFA72000451B5 /* EncryptionProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = EncryptionProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A790A2A0236AFA72000451B5 /* EncryptionProviderHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EncryptionProviderHeader.h; sourceTree = ""; }; + A790A2A1236AFA72000451B5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A7918DB5240CED78002011CA /* EncryptionProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = EncryptionProvider.h; path = "../../submodules/telegram-ios/submodules/EncryptionProvider/PublicHeaders/EncryptionProvider/EncryptionProvider.h"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A790A29B236AFA72000451B5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A72D451E236AF6770052FA81 = { + isa = PBXGroup; + children = ( + A7918DB5240CED78002011CA /* EncryptionProvider.h */, + A72D4529236AF6770052FA81 /* Sources */, + A790A29F236AFA72000451B5 /* EncryptionProvider */, + A72D4528236AF6770052FA81 /* Products */, + A72D4533236AF6F40052FA81 /* Frameworks */, + ); + sourceTree = ""; + }; + A72D4528236AF6770052FA81 /* Products */ = { + isa = PBXGroup; + children = ( + A790A29E236AFA72000451B5 /* EncryptionProvider.framework */, + ); + name = Products; + sourceTree = ""; + }; + A72D4529236AF6770052FA81 /* Sources */ = { + isa = PBXGroup; + children = ( + A72D452A236AF6770052FA81 /* EncryptionProvider.h */, + ); + name = Sources; + path = "../../submodules/telegram-ios/submodules/EncryptionProvider/Sources"; + sourceTree = ""; + }; + A72D4533236AF6F40052FA81 /* Frameworks */ = { + isa = PBXGroup; + children = ( + A72D4534236AF6F40052FA81 /* Foundation.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + A790A29F236AFA72000451B5 /* EncryptionProvider */ = { + isa = PBXGroup; + children = ( + A790A2A0236AFA72000451B5 /* EncryptionProviderHeader.h */, + A790A2A1236AFA72000451B5 /* Info.plist */, + ); + name = EncryptionProvider; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + A790A299236AFA72000451B5 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + A7918DB6240CED79002011CA /* EncryptionProvider.h in Headers */, + A790A30C236B072F000451B5 /* EncryptionProviderHeader.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + A790A29D236AFA72000451B5 /* EncryptionProvider */ = { + isa = PBXNativeTarget; + buildConfigurationList = A790A2AF236AFA72000451B5 /* Build configuration list for PBXNativeTarget "EncryptionProvider" */; + buildPhases = ( + A790A299236AFA72000451B5 /* Headers */, + A790A29A236AFA72000451B5 /* Sources */, + A790A29B236AFA72000451B5 /* Frameworks */, + A790A29C236AFA72000451B5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = EncryptionProvider; + productName = EncryptionProvider; + productReference = A790A29E236AFA72000451B5 /* EncryptionProvider.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A72D451F236AF6770052FA81 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1030; + ORGANIZATIONNAME = Telegram; + TargetAttributes = { + A790A29D236AFA72000451B5 = { + CreatedOnToolsVersion = 10.3; + }; + }; + }; + buildConfigurationList = A72D4522236AF6770052FA81 /* Build configuration list for PBXProject "EncryptionProvider_Xcode" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = A72D451E236AF6770052FA81; + productRefGroup = A72D4528236AF6770052FA81 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A790A29D236AFA72000451B5 /* EncryptionProvider */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A790A29C236AFA72000451B5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A790A29A236AFA72000451B5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A72D4536236AF7540052FA81 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + }; + name = DebugHockeyapp; + }; + A72D4538236AF75E0052FA81 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + }; + name = HockeyappMacAlpha; + }; + A72D453A236AF7670052FA81 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + }; + name = DebugAppStore; + }; + A72D453E236AF7780052FA81 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + }; + name = ReleaseAppStore; + }; + A72D4540236AF77E0052FA81 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + }; + name = ReleaseHockeyapp; + }; + A790A2B1236AFA72000451B5 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD)"; + 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 = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + FRAMEWORK_VERSION = A; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = s; + 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 = Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.EncryptionProvider; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + A790A2B2236AFA72000451B5 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD)"; + 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 = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + FRAMEWORK_VERSION = A; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = s; + 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 = Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.EncryptionProvider; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + A790A2B3236AFA72000451B5 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD)"; + 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 = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + FRAMEWORK_VERSION = A; + 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 = Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.EncryptionProvider; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + A790A2B5236AFA72000451B5 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD)"; + 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 = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + FRAMEWORK_VERSION = A; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = s; + 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 = Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.EncryptionProvider; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + A790A2B6236AFA72000451B5 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD)"; + 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 = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + FRAMEWORK_VERSION = A; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = s; + 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 = Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.EncryptionProvider; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + A7F282D0238EAB0500742C20 /* Github */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + }; + name = Github; + }; + A7F282D1238EAB0500742C20 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD)"; + 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 = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + FRAMEWORK_VERSION = A; + 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 = Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.EncryptionProvider; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A72D4522236AF6770052FA81 /* Build configuration list for PBXProject "EncryptionProvider_Xcode" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A72D4536236AF7540052FA81 /* DebugHockeyapp */, + A72D4538236AF75E0052FA81 /* HockeyappMacAlpha */, + A72D453A236AF7670052FA81 /* DebugAppStore */, + A7F282D0238EAB0500742C20 /* Github */, + A72D453E236AF7780052FA81 /* ReleaseAppStore */, + A72D4540236AF77E0052FA81 /* ReleaseHockeyapp */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = DebugHockeyapp; + }; + A790A2AF236AFA72000451B5 /* Build configuration list for PBXNativeTarget "EncryptionProvider" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A790A2B1236AFA72000451B5 /* DebugHockeyapp */, + A790A2B2236AFA72000451B5 /* HockeyappMacAlpha */, + A790A2B3236AFA72000451B5 /* DebugAppStore */, + A7F282D1238EAB0500742C20 /* Github */, + A790A2B5236AFA72000451B5 /* ReleaseAppStore */, + A790A2B6236AFA72000451B5 /* ReleaseHockeyapp */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = DebugHockeyapp; + }; +/* End XCConfigurationList section */ + }; + rootObject = A72D451F236AF6770052FA81 /* Project object */; +} diff --git a/core-xprojects/EncryptionProviderMac/EncryptionProvider_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/EncryptionProviderMac/EncryptionProvider_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..61bd5ca3da --- /dev/null +++ b/core-xprojects/EncryptionProviderMac/EncryptionProvider_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/EncryptionProviderMac/EncryptionProvider_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/EncryptionProviderMac/EncryptionProvider_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/EncryptionProviderMac/EncryptionProvider_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/EncryptionProviderMac/EncryptionProvider_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate b/core-xprojects/EncryptionProviderMac/EncryptionProvider_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000..9550516b36 Binary files /dev/null and b/core-xprojects/EncryptionProviderMac/EncryptionProvider_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/core-xprojects/EncryptionProviderMac/EncryptionProvider_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/EncryptionProvider.xcscheme b/core-xprojects/EncryptionProviderMac/EncryptionProvider_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/EncryptionProvider.xcscheme new file mode 100644 index 0000000000..834be1769c --- /dev/null +++ b/core-xprojects/EncryptionProviderMac/EncryptionProvider_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/EncryptionProvider.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/EncryptionProviderMac/EncryptionProvider_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist b/core-xprojects/EncryptionProviderMac/EncryptionProvider_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000000..b5e1deb6e9 --- /dev/null +++ b/core-xprojects/EncryptionProviderMac/EncryptionProvider_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,29 @@ + + + + + SchemeUserState + + EncryptionProvider.xcscheme + + isShown + + orderHint + 15 + + EncryptionProviderTests.xcscheme_^#shared#^_ + + orderHint + 1 + + + SuppressBuildableAutocreation + + A790A29D236AFA72000451B5 + + primary + + + + + diff --git a/core-xprojects/EncryptionProviderMac/Info.plist b/core-xprojects/EncryptionProviderMac/Info.plist new file mode 100644 index 0000000000..0c4389ac7e --- /dev/null +++ b/core-xprojects/EncryptionProviderMac/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2019 Telegram. All rights reserved. + + diff --git a/core-xprojects/FFMpegBinding/FFMpegBinding.xcodeproj/project.pbxproj b/core-xprojects/FFMpegBinding/FFMpegBinding.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..24161a0175 --- /dev/null +++ b/core-xprojects/FFMpegBinding/FFMpegBinding.xcodeproj/project.pbxproj @@ -0,0 +1,815 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + D0076FBC25691D2D007EF588 /* FFMpegRemuxer.m in Sources */ = {isa = PBXBuildFile; fileRef = D0076FB325691D2D007EF588 /* FFMpegRemuxer.m */; }; + D0076FBD25691D2D007EF588 /* FFMpegAVFrame.m in Sources */ = {isa = PBXBuildFile; fileRef = D0076FB425691D2D007EF588 /* FFMpegAVFrame.m */; }; + D0076FBE25691D2D007EF588 /* FFMpegAVFormatContext.m in Sources */ = {isa = PBXBuildFile; fileRef = D0076FB525691D2D007EF588 /* FFMpegAVFormatContext.m */; }; + D0076FBF25691D2D007EF588 /* FFMpegAVIOContext.m in Sources */ = {isa = PBXBuildFile; fileRef = D0076FB625691D2D007EF588 /* FFMpegAVIOContext.m */; }; + D0076FC025691D2D007EF588 /* FFMpegSWResample.m in Sources */ = {isa = PBXBuildFile; fileRef = D0076FB725691D2D007EF588 /* FFMpegSWResample.m */; }; + D0076FC125691D2D007EF588 /* FFMpegGlobals.m in Sources */ = {isa = PBXBuildFile; fileRef = D0076FB825691D2D007EF588 /* FFMpegGlobals.m */; }; + D0076FC225691D2D007EF588 /* FFMpegAVCodec.m in Sources */ = {isa = PBXBuildFile; fileRef = D0076FB925691D2D007EF588 /* FFMpegAVCodec.m */; }; + D0076FC325691D2D007EF588 /* FFMpegPacket.m in Sources */ = {isa = PBXBuildFile; fileRef = D0076FBA25691D2D007EF588 /* FFMpegPacket.m */; }; + D0076FC425691D2D007EF588 /* FFMpegAVCodecContext.m in Sources */ = {isa = PBXBuildFile; fileRef = D0076FBB25691D2D007EF588 /* FFMpegAVCodecContext.m */; }; + D0076FD325691D3C007EF588 /* FFMpegGlobals.h in Headers */ = {isa = PBXBuildFile; fileRef = D0076FC825691D3C007EF588 /* FFMpegGlobals.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0076FD425691D3C007EF588 /* FFMpegAVCodec.h in Headers */ = {isa = PBXBuildFile; fileRef = D0076FC925691D3C007EF588 /* FFMpegAVCodec.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0076FD525691D3C007EF588 /* FFMpegAVCodecContext.h in Headers */ = {isa = PBXBuildFile; fileRef = D0076FCA25691D3C007EF588 /* FFMpegAVCodecContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0076FD625691D3C007EF588 /* FFMpegPacket.h in Headers */ = {isa = PBXBuildFile; fileRef = D0076FCB25691D3C007EF588 /* FFMpegPacket.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0076FD725691D3C007EF588 /* FFMpegAVFormatContext.h in Headers */ = {isa = PBXBuildFile; fileRef = D0076FCC25691D3C007EF588 /* FFMpegAVFormatContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0076FD825691D3C007EF588 /* FFMpegBinding.h in Headers */ = {isa = PBXBuildFile; fileRef = D0076FCD25691D3C007EF588 /* FFMpegBinding.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0076FD925691D3C007EF588 /* FFMpegAVFrame.h in Headers */ = {isa = PBXBuildFile; fileRef = D0076FCE25691D3C007EF588 /* FFMpegAVFrame.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0076FDA25691D3C007EF588 /* FFMpegRemuxer.h in Headers */ = {isa = PBXBuildFile; fileRef = D0076FCF25691D3C007EF588 /* FFMpegRemuxer.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0076FDB25691D3C007EF588 /* FFMpegAVIOContext.h in Headers */ = {isa = PBXBuildFile; fileRef = D0076FD025691D3C007EF588 /* FFMpegAVIOContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0076FDC25691D3C007EF588 /* FFMpegAVSampleFormat.h in Headers */ = {isa = PBXBuildFile; fileRef = D0076FD125691D3C007EF588 /* FFMpegAVSampleFormat.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0076FDD25691D3C007EF588 /* FFMpegSWResample.h in Headers */ = {isa = PBXBuildFile; fileRef = D0076FD225691D3C007EF588 /* FFMpegSWResample.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0076FF425691E9B007EF588 /* ffmpeg.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0076FF325691E9B007EF588 /* ffmpeg.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + D0076FA625691C89007EF588 /* FFMpegBinding.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FFMpegBinding.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0076FAA25691C89007EF588 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D0076FB325691D2D007EF588 /* FFMpegRemuxer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FFMpegRemuxer.m; sourceTree = ""; }; + D0076FB425691D2D007EF588 /* FFMpegAVFrame.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FFMpegAVFrame.m; sourceTree = ""; }; + D0076FB525691D2D007EF588 /* FFMpegAVFormatContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FFMpegAVFormatContext.m; sourceTree = ""; }; + D0076FB625691D2D007EF588 /* FFMpegAVIOContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FFMpegAVIOContext.m; sourceTree = ""; }; + D0076FB725691D2D007EF588 /* FFMpegSWResample.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FFMpegSWResample.m; sourceTree = ""; }; + D0076FB825691D2D007EF588 /* FFMpegGlobals.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FFMpegGlobals.m; sourceTree = ""; }; + D0076FB925691D2D007EF588 /* FFMpegAVCodec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FFMpegAVCodec.m; sourceTree = ""; }; + D0076FBA25691D2D007EF588 /* FFMpegPacket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FFMpegPacket.m; sourceTree = ""; }; + D0076FBB25691D2D007EF588 /* FFMpegAVCodecContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FFMpegAVCodecContext.m; sourceTree = ""; }; + D0076FC825691D3C007EF588 /* FFMpegGlobals.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FFMpegGlobals.h; sourceTree = ""; }; + D0076FC925691D3C007EF588 /* FFMpegAVCodec.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FFMpegAVCodec.h; sourceTree = ""; }; + D0076FCA25691D3C007EF588 /* FFMpegAVCodecContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FFMpegAVCodecContext.h; sourceTree = ""; }; + D0076FCB25691D3C007EF588 /* FFMpegPacket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FFMpegPacket.h; sourceTree = ""; }; + D0076FCC25691D3C007EF588 /* FFMpegAVFormatContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FFMpegAVFormatContext.h; sourceTree = ""; }; + D0076FCD25691D3C007EF588 /* FFMpegBinding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FFMpegBinding.h; sourceTree = ""; }; + D0076FCE25691D3C007EF588 /* FFMpegAVFrame.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FFMpegAVFrame.h; sourceTree = ""; }; + D0076FCF25691D3C007EF588 /* FFMpegRemuxer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FFMpegRemuxer.h; sourceTree = ""; }; + D0076FD025691D3C007EF588 /* FFMpegAVIOContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FFMpegAVIOContext.h; sourceTree = ""; }; + D0076FD125691D3C007EF588 /* FFMpegAVSampleFormat.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FFMpegAVSampleFormat.h; sourceTree = ""; }; + D0076FD225691D3C007EF588 /* FFMpegSWResample.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FFMpegSWResample.h; sourceTree = ""; }; + D0076FF325691E9B007EF588 /* ffmpeg.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ffmpeg.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0076FF525691EAB007EF588 /* libavcodec.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavcodec.a; path = ../ffmpeg/build/ffmpeg/lib/libavcodec.a; sourceTree = ""; }; + D0076FF625691EAB007EF588 /* libswresample.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libswresample.a; path = ../ffmpeg/build/ffmpeg/lib/libswresample.a; sourceTree = ""; }; + D0076FF725691EAB007EF588 /* libavformat.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavformat.a; path = ../ffmpeg/build/ffmpeg/lib/libavformat.a; sourceTree = ""; }; + D0076FF825691EAB007EF588 /* libavutil.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavutil.a; path = ../ffmpeg/build/ffmpeg/lib/libavutil.a; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D0076FA325691C89007EF588 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D0076FF425691E9B007EF588 /* ffmpeg.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + D0076F9C25691C89007EF588 = { + isa = PBXGroup; + children = ( + D0076FC725691D3C007EF588 /* FFMpegBinding */, + D0076FB225691D2D007EF588 /* Sources */, + D0076FA825691C89007EF588 /* FFMpegBinding */, + D0076FA725691C89007EF588 /* Products */, + D0076FF225691E9B007EF588 /* Frameworks */, + ); + sourceTree = ""; + }; + D0076FA725691C89007EF588 /* Products */ = { + isa = PBXGroup; + children = ( + D0076FA625691C89007EF588 /* FFMpegBinding.framework */, + ); + name = Products; + sourceTree = ""; + }; + D0076FA825691C89007EF588 /* FFMpegBinding */ = { + isa = PBXGroup; + children = ( + D0076FAA25691C89007EF588 /* Info.plist */, + ); + path = FFMpegBinding; + sourceTree = ""; + }; + D0076FB225691D2D007EF588 /* Sources */ = { + isa = PBXGroup; + children = ( + D0076FB325691D2D007EF588 /* FFMpegRemuxer.m */, + D0076FB425691D2D007EF588 /* FFMpegAVFrame.m */, + D0076FB525691D2D007EF588 /* FFMpegAVFormatContext.m */, + D0076FB625691D2D007EF588 /* FFMpegAVIOContext.m */, + D0076FB725691D2D007EF588 /* FFMpegSWResample.m */, + D0076FB825691D2D007EF588 /* FFMpegGlobals.m */, + D0076FB925691D2D007EF588 /* FFMpegAVCodec.m */, + D0076FBA25691D2D007EF588 /* FFMpegPacket.m */, + D0076FBB25691D2D007EF588 /* FFMpegAVCodecContext.m */, + ); + name = Sources; + path = "../../submodules/telegram-ios/submodules/FFMpegBinding/Sources"; + sourceTree = ""; + }; + D0076FC725691D3C007EF588 /* FFMpegBinding */ = { + isa = PBXGroup; + children = ( + D0076FC825691D3C007EF588 /* FFMpegGlobals.h */, + D0076FC925691D3C007EF588 /* FFMpegAVCodec.h */, + D0076FCA25691D3C007EF588 /* FFMpegAVCodecContext.h */, + D0076FCB25691D3C007EF588 /* FFMpegPacket.h */, + D0076FCC25691D3C007EF588 /* FFMpegAVFormatContext.h */, + D0076FCD25691D3C007EF588 /* FFMpegBinding.h */, + D0076FCE25691D3C007EF588 /* FFMpegAVFrame.h */, + D0076FCF25691D3C007EF588 /* FFMpegRemuxer.h */, + D0076FD025691D3C007EF588 /* FFMpegAVIOContext.h */, + D0076FD125691D3C007EF588 /* FFMpegAVSampleFormat.h */, + D0076FD225691D3C007EF588 /* FFMpegSWResample.h */, + ); + name = FFMpegBinding; + path = "../../submodules/telegram-ios/submodules/FFMpegBinding/Public/FFMpegBinding"; + sourceTree = ""; + }; + D0076FF225691E9B007EF588 /* Frameworks */ = { + isa = PBXGroup; + children = ( + D0076FF525691EAB007EF588 /* libavcodec.a */, + D0076FF725691EAB007EF588 /* libavformat.a */, + D0076FF825691EAB007EF588 /* libavutil.a */, + D0076FF625691EAB007EF588 /* libswresample.a */, + D0076FF325691E9B007EF588 /* ffmpeg.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + D0076FA125691C89007EF588 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D0076FD825691D3C007EF588 /* FFMpegBinding.h in Headers */, + D0076FD325691D3C007EF588 /* FFMpegGlobals.h in Headers */, + D0076FD525691D3C007EF588 /* FFMpegAVCodecContext.h in Headers */, + D0076FD725691D3C007EF588 /* FFMpegAVFormatContext.h in Headers */, + D0076FDD25691D3C007EF588 /* FFMpegSWResample.h in Headers */, + D0076FDA25691D3C007EF588 /* FFMpegRemuxer.h in Headers */, + D0076FD925691D3C007EF588 /* FFMpegAVFrame.h in Headers */, + D0076FDB25691D3C007EF588 /* FFMpegAVIOContext.h in Headers */, + D0076FD625691D3C007EF588 /* FFMpegPacket.h in Headers */, + D0076FDC25691D3C007EF588 /* FFMpegAVSampleFormat.h in Headers */, + D0076FD425691D3C007EF588 /* FFMpegAVCodec.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + D0076FA525691C89007EF588 /* FFMpegBinding */ = { + isa = PBXNativeTarget; + buildConfigurationList = D0076FAE25691C89007EF588 /* Build configuration list for PBXNativeTarget "FFMpegBinding" */; + buildPhases = ( + D0076FA125691C89007EF588 /* Headers */, + D0076FA225691C89007EF588 /* Sources */, + D0076FA325691C89007EF588 /* Frameworks */, + D0076FA425691C89007EF588 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = FFMpegBinding; + productName = FFMpegBinding; + productReference = D0076FA625691C89007EF588 /* FFMpegBinding.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D0076F9D25691C89007EF588 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1220; + TargetAttributes = { + D0076FA525691C89007EF588 = { + CreatedOnToolsVersion = 12.2; + }; + }; + }; + buildConfigurationList = D0076FA025691C89007EF588 /* Build configuration list for PBXProject "FFMpegBinding" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = D0076F9C25691C89007EF588; + productRefGroup = D0076FA725691C89007EF588 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D0076FA525691C89007EF588 /* FFMpegBinding */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D0076FA425691C89007EF588 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D0076FA225691C89007EF588 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D0076FC425691D2D007EF588 /* FFMpegAVCodecContext.m in Sources */, + D0076FBC25691D2D007EF588 /* FFMpegRemuxer.m in Sources */, + D0076FC125691D2D007EF588 /* FFMpegGlobals.m in Sources */, + D0076FBD25691D2D007EF588 /* FFMpegAVFrame.m in Sources */, + D0076FBE25691D2D007EF588 /* FFMpegAVFormatContext.m in Sources */, + D0076FC025691D2D007EF588 /* FFMpegSWResample.m in Sources */, + D0076FBF25691D2D007EF588 /* FFMpegAVIOContext.m in Sources */, + D0076FC325691D2D007EF588 /* FFMpegPacket.m in Sources */, + D0076FC225691D2D007EF588 /* FFMpegAVCodec.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + D0076FAC25691C89007EF588 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + D0076FAD25691C89007EF588 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + D0076FAF25691C89007EF588 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = $PROJECT_DIR/../ffmpeg/build/ffmpeg/include; + INFOPLIST_FILE = FFMpegBinding/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.FFMpegBinding; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = DebugAppStore; + }; + D0076FB025691C89007EF588 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = $PROJECT_DIR/../ffmpeg/build/ffmpeg/include; + INFOPLIST_FILE = FFMpegBinding/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.FFMpegBinding; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = ReleaseAppStore; + }; + D0076FDF25691D75007EF588 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + D0076FE025691D75007EF588 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = $PROJECT_DIR/../ffmpeg/build/ffmpeg/include; + INFOPLIST_FILE = FFMpegBinding/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.FFMpegBinding; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = ReleaseHockeyapp; + }; + D0076FE125691D7A007EF588 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + D0076FE225691D7A007EF588 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = $PROJECT_DIR/../ffmpeg/build/ffmpeg/include; + INFOPLIST_FILE = FFMpegBinding/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.FFMpegBinding; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = DebugHockeyapp; + }; + D0076FE325691D81007EF588 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + D0076FE425691D81007EF588 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = $PROJECT_DIR/../ffmpeg/build/ffmpeg/include; + INFOPLIST_FILE = FFMpegBinding/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.FFMpegBinding; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = HockeyappMacAlpha; + }; + D0076FE525691D87007EF588 /* Github */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + D0076FE625691D87007EF588 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = $PROJECT_DIR/../ffmpeg/build/ffmpeg/include; + INFOPLIST_FILE = FFMpegBinding/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.FFMpegBinding; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = Github; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D0076FA025691C89007EF588 /* Build configuration list for PBXProject "FFMpegBinding" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0076FAC25691C89007EF588 /* DebugAppStore */, + D0076FE525691D87007EF588 /* Github */, + D0076FAD25691C89007EF588 /* ReleaseAppStore */, + D0076FDF25691D75007EF588 /* ReleaseHockeyapp */, + D0076FE125691D7A007EF588 /* DebugHockeyapp */, + D0076FE325691D81007EF588 /* HockeyappMacAlpha */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; + D0076FAE25691C89007EF588 /* Build configuration list for PBXNativeTarget "FFMpegBinding" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0076FAF25691C89007EF588 /* DebugAppStore */, + D0076FE625691D87007EF588 /* Github */, + D0076FB025691C89007EF588 /* ReleaseAppStore */, + D0076FE025691D75007EF588 /* ReleaseHockeyapp */, + D0076FE225691D7A007EF588 /* DebugHockeyapp */, + D0076FE425691D81007EF588 /* HockeyappMacAlpha */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; +/* End XCConfigurationList section */ + }; + rootObject = D0076F9D25691C89007EF588 /* Project object */; +} diff --git a/core-xprojects/FFMpegBinding/FFMpegBinding.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/FFMpegBinding/FFMpegBinding.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/core-xprojects/FFMpegBinding/FFMpegBinding.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/FFMpegBinding/FFMpegBinding.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/FFMpegBinding/FFMpegBinding.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/FFMpegBinding/FFMpegBinding.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/FFMpegBinding/FFMpegBinding/Info.plist b/core-xprojects/FFMpegBinding/FFMpegBinding/Info.plist new file mode 100644 index 0000000000..9bcb244429 --- /dev/null +++ b/core-xprojects/FFMpegBinding/FFMpegBinding/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/core-xprojects/GraphCore/Graph/Info.plist b/core-xprojects/GraphCore/Graph/Info.plist new file mode 100644 index 0000000000..861c829fcb --- /dev/null +++ b/core-xprojects/GraphCore/Graph/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2020 Telegram. All rights reserved. + + diff --git a/core-xprojects/GraphCore/GraphCore.xcodeproj/project.pbxproj b/core-xprojects/GraphCore/GraphCore.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..f82c3505be --- /dev/null +++ b/core-xprojects/GraphCore/GraphCore.xcodeproj/project.pbxproj @@ -0,0 +1,1017 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + D0A9B733241CEFAC007DF938 /* HorizontalScalesRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B6F9241CEFAC007DF938 /* HorizontalScalesRenderer.swift */; }; + D0A9B734241CEFAC007DF938 /* BarChartRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B6FA241CEFAC007DF938 /* BarChartRenderer.swift */; }; + D0A9B735241CEFAC007DF938 /* LinesChartRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B6FB241CEFAC007DF938 /* LinesChartRenderer.swift */; }; + D0A9B736241CEFAC007DF938 /* PecentChartRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B6FC241CEFAC007DF938 /* PecentChartRenderer.swift */; }; + D0A9B737241CEFAC007DF938 /* PerformanceRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B6FD241CEFAC007DF938 /* PerformanceRenderer.swift */; }; + D0A9B738241CEFAC007DF938 /* VerticalLinesRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B6FE241CEFAC007DF938 /* VerticalLinesRenderer.swift */; }; + D0A9B73A241CEFAC007DF938 /* VerticalScalesRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B700241CEFAC007DF938 /* VerticalScalesRenderer.swift */; }; + D0A9B73B241CEFAC007DF938 /* BaseChartRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B701241CEFAC007DF938 /* BaseChartRenderer.swift */; }; + D0A9B73C241CEFAC007DF938 /* PieChartRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B702241CEFAC007DF938 /* PieChartRenderer.swift */; }; + D0A9B73D241CEFAC007DF938 /* ChartDetailsRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B703241CEFAC007DF938 /* ChartDetailsRenderer.swift */; }; + D0A9B73E241CEFAC007DF938 /* PercentPieAnimationRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B704241CEFAC007DF938 /* PercentPieAnimationRenderer.swift */; }; + D0A9B73F241CEFAC007DF938 /* PieChartComponentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B707241CEFAC007DF938 /* PieChartComponentController.swift */; }; + D0A9B740241CEFAC007DF938 /* PercentPieChartController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B708241CEFAC007DF938 /* PercentPieChartController.swift */; }; + D0A9B741241CEFAC007DF938 /* PercentChartComponentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B709241CEFAC007DF938 /* PercentChartComponentController.swift */; }; + D0A9B742241CEFAC007DF938 /* BaseChartController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B70A241CEFAC007DF938 /* BaseChartController.swift */; }; + D0A9B743241CEFAC007DF938 /* BarsComponentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B70C241CEFAC007DF938 /* BarsComponentController.swift */; }; + D0A9B744241CEFAC007DF938 /* StackedBarsChartController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B70D241CEFAC007DF938 /* StackedBarsChartController.swift */; }; + D0A9B745241CEFAC007DF938 /* LinesComponentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B70E241CEFAC007DF938 /* LinesComponentController.swift */; }; + D0A9B746241CEFAC007DF938 /* DailyBarsChartController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B70F241CEFAC007DF938 /* DailyBarsChartController.swift */; }; + D0A9B747241CEFAC007DF938 /* StepBarsChartController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B710241CEFAC007DF938 /* StepBarsChartController.swift */; }; + D0A9B749241CEFAC007DF938 /* GeneralChartComponentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B712241CEFAC007DF938 /* GeneralChartComponentController.swift */; }; + D0A9B74A241CEFAC007DF938 /* GeneralLinesChartController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B714241CEFAC007DF938 /* GeneralLinesChartController.swift */; }; + D0A9B74B241CEFAC007DF938 /* TwoAxisLinesChartController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B715241CEFAC007DF938 /* TwoAxisLinesChartController.swift */; }; + D0A9B74C241CEFAC007DF938 /* BaseLinesChartController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B716241CEFAC007DF938 /* BaseLinesChartController.swift */; }; + D0A9B74D241CEFAC007DF938 /* ColorMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B718241CEFAC007DF938 /* ColorMode.swift */; }; + D0A9B74E241CEFAC007DF938 /* ChartLineData.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B719241CEFAC007DF938 /* ChartLineData.swift */; }; + D0A9B74F241CEFAC007DF938 /* LinesSelectionLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B71A241CEFAC007DF938 /* LinesSelectionLabel.swift */; }; + D0A9B750241CEFAC007DF938 /* LinesChartLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B71B241CEFAC007DF938 /* LinesChartLabel.swift */; }; + D0A9B751241CEFAC007DF938 /* TimeInterval+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B71D241CEFAC007DF938 /* TimeInterval+Utils.swift */; }; + D0A9B752241CEFAC007DF938 /* AnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B71E241CEFAC007DF938 /* AnimationController.swift */; }; + D0A9B753241CEFAC007DF938 /* TextUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B71F241CEFAC007DF938 /* TextUtils.swift */; }; + D0A9B754241CEFAC007DF938 /* UIView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B720241CEFAC007DF938 /* UIView+Extensions.swift */; }; + D0A9B755241CEFAC007DF938 /* UIColor+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B721241CEFAC007DF938 /* UIColor+Utils.swift */; }; + D0A9B756241CEFAC007DF938 /* DisplayLinkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B722241CEFAC007DF938 /* DisplayLinkService.swift */; }; + D0A9B757241CEFAC007DF938 /* GlobalHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B723241CEFAC007DF938 /* GlobalHelpers.swift */; }; + D0A9B758241CEFAC007DF938 /* TimeZone.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B724241CEFAC007DF938 /* TimeZone.swift */; }; + D0A9B759241CEFAC007DF938 /* ScalesNumberFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B725241CEFAC007DF938 /* ScalesNumberFormatter.swift */; }; + D0A9B75A241CEFAC007DF938 /* CGPoint+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B726241CEFAC007DF938 /* CGPoint+Extensions.swift */; }; + D0A9B75B241CEFAC007DF938 /* Array+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B727241CEFAC007DF938 /* Array+Utils.swift */; }; + D0A9B75C241CEFAC007DF938 /* UIImage+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B728241CEFAC007DF938 /* UIImage+Utils.swift */; }; + D0A9B75D241CEFAC007DF938 /* ClosedRange+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B729241CEFAC007DF938 /* ClosedRange+Utils.swift */; }; + D0A9B75E241CEFAC007DF938 /* CGFloat.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B72A241CEFAC007DF938 /* CGFloat.swift */; }; + D0A9B75F241CEFAC007DF938 /* NumberFormatter+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B72B241CEFAC007DF938 /* NumberFormatter+Utils.swift */; }; + D0A9B760241CEFAC007DF938 /* GraphCore.h in Headers */ = {isa = PBXBuildFile; fileRef = D0A9B72C241CEFAC007DF938 /* GraphCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0A9B761241CEFAC007DF938 /* ChartVisibilityItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B72E241CEFAC007DF938 /* ChartVisibilityItem.swift */; }; + D0A9B762241CEFAC007DF938 /* ChartsDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B72F241CEFAC007DF938 /* ChartsDataManager.swift */; }; + D0A9B763241CEFAC007DF938 /* Convert.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B730241CEFAC007DF938 /* Convert.swift */; }; + D0A9B764241CEFAC007DF938 /* ChartsCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B731241CEFAC007DF938 /* ChartsCollection.swift */; }; + D0A9B765241CEFAC007DF938 /* ChartsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A9B732241CEFAC007DF938 /* ChartsError.swift */; }; + D0DEF74B242BBCE200A34A30 /* LineBulletsRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DEF74A242BBCE200A34A30 /* LineBulletsRenderer.swift */; }; + D0DEF74C242BBDBE00A34A30 /* TwoAxisStepBarsChartController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DEF746242BB79900A34A30 /* TwoAxisStepBarsChartController.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A7831B152403BFE30056AEAC /* TGUIKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TGUIKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A789E0C223E841B600AEB34A /* GraphCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = GraphCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A789E0C623E841B600AEB34A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D0A9B6F9241CEFAC007DF938 /* HorizontalScalesRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalScalesRenderer.swift; sourceTree = ""; }; + D0A9B6FA241CEFAC007DF938 /* BarChartRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BarChartRenderer.swift; sourceTree = ""; }; + D0A9B6FB241CEFAC007DF938 /* LinesChartRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinesChartRenderer.swift; sourceTree = ""; }; + D0A9B6FC241CEFAC007DF938 /* PecentChartRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PecentChartRenderer.swift; sourceTree = ""; }; + D0A9B6FD241CEFAC007DF938 /* PerformanceRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PerformanceRenderer.swift; sourceTree = ""; }; + D0A9B6FE241CEFAC007DF938 /* VerticalLinesRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerticalLinesRenderer.swift; sourceTree = ""; }; + D0A9B6FF241CEFAC007DF938 /* LineBulletsRenerer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LineBulletsRenerer.swift; sourceTree = ""; }; + D0A9B700241CEFAC007DF938 /* VerticalScalesRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerticalScalesRenderer.swift; sourceTree = ""; }; + D0A9B701241CEFAC007DF938 /* BaseChartRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseChartRenderer.swift; sourceTree = ""; }; + D0A9B702241CEFAC007DF938 /* PieChartRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PieChartRenderer.swift; sourceTree = ""; }; + D0A9B703241CEFAC007DF938 /* ChartDetailsRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartDetailsRenderer.swift; sourceTree = ""; }; + D0A9B704241CEFAC007DF938 /* PercentPieAnimationRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PercentPieAnimationRenderer.swift; sourceTree = ""; }; + D0A9B707241CEFAC007DF938 /* PieChartComponentController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PieChartComponentController.swift; sourceTree = ""; }; + D0A9B708241CEFAC007DF938 /* PercentPieChartController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PercentPieChartController.swift; sourceTree = ""; }; + D0A9B709241CEFAC007DF938 /* PercentChartComponentController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PercentChartComponentController.swift; sourceTree = ""; }; + D0A9B70A241CEFAC007DF938 /* BaseChartController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseChartController.swift; sourceTree = ""; }; + D0A9B70C241CEFAC007DF938 /* BarsComponentController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BarsComponentController.swift; sourceTree = ""; }; + D0A9B70D241CEFAC007DF938 /* StackedBarsChartController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackedBarsChartController.swift; sourceTree = ""; }; + D0A9B70E241CEFAC007DF938 /* LinesComponentController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinesComponentController.swift; sourceTree = ""; }; + D0A9B70F241CEFAC007DF938 /* DailyBarsChartController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DailyBarsChartController.swift; sourceTree = ""; }; + D0A9B710241CEFAC007DF938 /* StepBarsChartController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StepBarsChartController.swift; sourceTree = ""; }; + D0A9B712241CEFAC007DF938 /* GeneralChartComponentController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneralChartComponentController.swift; sourceTree = ""; }; + D0A9B714241CEFAC007DF938 /* GeneralLinesChartController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneralLinesChartController.swift; sourceTree = ""; }; + D0A9B715241CEFAC007DF938 /* TwoAxisLinesChartController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TwoAxisLinesChartController.swift; sourceTree = ""; }; + D0A9B716241CEFAC007DF938 /* BaseLinesChartController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseLinesChartController.swift; sourceTree = ""; }; + D0A9B718241CEFAC007DF938 /* ColorMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorMode.swift; sourceTree = ""; }; + D0A9B719241CEFAC007DF938 /* ChartLineData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartLineData.swift; sourceTree = ""; }; + D0A9B71A241CEFAC007DF938 /* LinesSelectionLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinesSelectionLabel.swift; sourceTree = ""; }; + D0A9B71B241CEFAC007DF938 /* LinesChartLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinesChartLabel.swift; sourceTree = ""; }; + D0A9B71D241CEFAC007DF938 /* TimeInterval+Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Utils.swift"; sourceTree = ""; }; + D0A9B71E241CEFAC007DF938 /* AnimationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimationController.swift; sourceTree = ""; }; + D0A9B71F241CEFAC007DF938 /* TextUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextUtils.swift; sourceTree = ""; }; + D0A9B720241CEFAC007DF938 /* UIView+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Extensions.swift"; sourceTree = ""; }; + D0A9B721241CEFAC007DF938 /* UIColor+Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Utils.swift"; sourceTree = ""; }; + D0A9B722241CEFAC007DF938 /* DisplayLinkService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayLinkService.swift; sourceTree = ""; }; + D0A9B723241CEFAC007DF938 /* GlobalHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalHelpers.swift; sourceTree = ""; }; + D0A9B724241CEFAC007DF938 /* TimeZone.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimeZone.swift; sourceTree = ""; }; + D0A9B725241CEFAC007DF938 /* ScalesNumberFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScalesNumberFormatter.swift; sourceTree = ""; }; + D0A9B726241CEFAC007DF938 /* CGPoint+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGPoint+Extensions.swift"; sourceTree = ""; }; + D0A9B727241CEFAC007DF938 /* Array+Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+Utils.swift"; sourceTree = ""; }; + D0A9B728241CEFAC007DF938 /* UIImage+Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+Utils.swift"; sourceTree = ""; }; + D0A9B729241CEFAC007DF938 /* ClosedRange+Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ClosedRange+Utils.swift"; sourceTree = ""; }; + D0A9B72A241CEFAC007DF938 /* CGFloat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGFloat.swift; sourceTree = ""; }; + D0A9B72B241CEFAC007DF938 /* NumberFormatter+Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NumberFormatter+Utils.swift"; sourceTree = ""; }; + D0A9B72C241CEFAC007DF938 /* GraphCore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GraphCore.h; sourceTree = ""; }; + D0A9B72E241CEFAC007DF938 /* ChartVisibilityItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartVisibilityItem.swift; sourceTree = ""; }; + D0A9B72F241CEFAC007DF938 /* ChartsDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartsDataManager.swift; sourceTree = ""; }; + D0A9B730241CEFAC007DF938 /* Convert.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Convert.swift; sourceTree = ""; }; + D0A9B731241CEFAC007DF938 /* ChartsCollection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartsCollection.swift; sourceTree = ""; }; + D0A9B732241CEFAC007DF938 /* ChartsError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartsError.swift; sourceTree = ""; }; + D0DEF746242BB79900A34A30 /* TwoAxisStepBarsChartController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TwoAxisStepBarsChartController.swift; sourceTree = ""; }; + D0DEF74A242BBCE200A34A30 /* LineBulletsRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LineBulletsRenderer.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A789E0BF23E841B600AEB34A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A7831B142403BFE30056AEAC /* Frameworks */ = { + isa = PBXGroup; + children = ( + A7831B152403BFE30056AEAC /* TGUIKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + A789E0B823E841B600AEB34A = { + isa = PBXGroup; + children = ( + A789E0C423E841B600AEB34A /* Graph */, + A789E0C323E841B600AEB34A /* Products */, + A7831B142403BFE30056AEAC /* Frameworks */, + ); + sourceTree = ""; + }; + A789E0C323E841B600AEB34A /* Products */ = { + isa = PBXGroup; + children = ( + A789E0C223E841B600AEB34A /* GraphCore.framework */, + ); + name = Products; + sourceTree = ""; + }; + A789E0C423E841B600AEB34A /* Graph */ = { + isa = PBXGroup; + children = ( + D0A9B6F6241CEFAC007DF938 /* Sources */, + A789E0C623E841B600AEB34A /* Info.plist */, + ); + path = Graph; + sourceTree = ""; + }; + D0A9B6F6241CEFAC007DF938 /* Sources */ = { + isa = PBXGroup; + children = ( + D0A9B6F7241CEFAC007DF938 /* Charts */, + D0A9B717241CEFAC007DF938 /* Models */, + D0A9B71C241CEFAC007DF938 /* Helpers */, + D0A9B72C241CEFAC007DF938 /* GraphCore.h */, + D0A9B72D241CEFAC007DF938 /* Charts Reader */, + ); + name = Sources; + path = "../../../submodules/telegram-ios/submodules/GraphCore/Sources"; + sourceTree = ""; + }; + D0A9B6F7241CEFAC007DF938 /* Charts */ = { + isa = PBXGroup; + children = ( + D0A9B6F8241CEFAC007DF938 /* Renderes */, + D0A9B705241CEFAC007DF938 /* Controllers */, + ); + path = Charts; + sourceTree = ""; + }; + D0A9B6F8241CEFAC007DF938 /* Renderes */ = { + isa = PBXGroup; + children = ( + D0DEF74A242BBCE200A34A30 /* LineBulletsRenderer.swift */, + D0A9B6F9241CEFAC007DF938 /* HorizontalScalesRenderer.swift */, + D0A9B6FA241CEFAC007DF938 /* BarChartRenderer.swift */, + D0A9B6FB241CEFAC007DF938 /* LinesChartRenderer.swift */, + D0A9B6FC241CEFAC007DF938 /* PecentChartRenderer.swift */, + D0A9B6FD241CEFAC007DF938 /* PerformanceRenderer.swift */, + D0A9B6FE241CEFAC007DF938 /* VerticalLinesRenderer.swift */, + D0A9B6FF241CEFAC007DF938 /* LineBulletsRenerer.swift */, + D0A9B700241CEFAC007DF938 /* VerticalScalesRenderer.swift */, + D0A9B701241CEFAC007DF938 /* BaseChartRenderer.swift */, + D0A9B702241CEFAC007DF938 /* PieChartRenderer.swift */, + D0A9B703241CEFAC007DF938 /* ChartDetailsRenderer.swift */, + D0A9B704241CEFAC007DF938 /* PercentPieAnimationRenderer.swift */, + ); + path = Renderes; + sourceTree = ""; + }; + D0A9B705241CEFAC007DF938 /* Controllers */ = { + isa = PBXGroup; + children = ( + D0A9B706241CEFAC007DF938 /* Percent And Pie */, + D0A9B70A241CEFAC007DF938 /* BaseChartController.swift */, + D0A9B70B241CEFAC007DF938 /* Stacked Bars */, + D0A9B712241CEFAC007DF938 /* GeneralChartComponentController.swift */, + D0A9B713241CEFAC007DF938 /* Lines */, + ); + path = Controllers; + sourceTree = ""; + }; + D0A9B706241CEFAC007DF938 /* Percent And Pie */ = { + isa = PBXGroup; + children = ( + D0A9B707241CEFAC007DF938 /* PieChartComponentController.swift */, + D0A9B708241CEFAC007DF938 /* PercentPieChartController.swift */, + D0A9B709241CEFAC007DF938 /* PercentChartComponentController.swift */, + ); + path = "Percent And Pie"; + sourceTree = ""; + }; + D0A9B70B241CEFAC007DF938 /* Stacked Bars */ = { + isa = PBXGroup; + children = ( + D0DEF746242BB79900A34A30 /* TwoAxisStepBarsChartController.swift */, + D0A9B70C241CEFAC007DF938 /* BarsComponentController.swift */, + D0A9B70D241CEFAC007DF938 /* StackedBarsChartController.swift */, + D0A9B70E241CEFAC007DF938 /* LinesComponentController.swift */, + D0A9B70F241CEFAC007DF938 /* DailyBarsChartController.swift */, + D0A9B710241CEFAC007DF938 /* StepBarsChartController.swift */, + ); + path = "Stacked Bars"; + sourceTree = ""; + }; + D0A9B713241CEFAC007DF938 /* Lines */ = { + isa = PBXGroup; + children = ( + D0A9B714241CEFAC007DF938 /* GeneralLinesChartController.swift */, + D0A9B715241CEFAC007DF938 /* TwoAxisLinesChartController.swift */, + D0A9B716241CEFAC007DF938 /* BaseLinesChartController.swift */, + ); + path = Lines; + sourceTree = ""; + }; + D0A9B717241CEFAC007DF938 /* Models */ = { + isa = PBXGroup; + children = ( + D0A9B718241CEFAC007DF938 /* ColorMode.swift */, + D0A9B719241CEFAC007DF938 /* ChartLineData.swift */, + D0A9B71A241CEFAC007DF938 /* LinesSelectionLabel.swift */, + D0A9B71B241CEFAC007DF938 /* LinesChartLabel.swift */, + ); + path = Models; + sourceTree = ""; + }; + D0A9B71C241CEFAC007DF938 /* Helpers */ = { + isa = PBXGroup; + children = ( + D0A9B71D241CEFAC007DF938 /* TimeInterval+Utils.swift */, + D0A9B71E241CEFAC007DF938 /* AnimationController.swift */, + D0A9B71F241CEFAC007DF938 /* TextUtils.swift */, + D0A9B720241CEFAC007DF938 /* UIView+Extensions.swift */, + D0A9B721241CEFAC007DF938 /* UIColor+Utils.swift */, + D0A9B722241CEFAC007DF938 /* DisplayLinkService.swift */, + D0A9B723241CEFAC007DF938 /* GlobalHelpers.swift */, + D0A9B724241CEFAC007DF938 /* TimeZone.swift */, + D0A9B725241CEFAC007DF938 /* ScalesNumberFormatter.swift */, + D0A9B726241CEFAC007DF938 /* CGPoint+Extensions.swift */, + D0A9B727241CEFAC007DF938 /* Array+Utils.swift */, + D0A9B728241CEFAC007DF938 /* UIImage+Utils.swift */, + D0A9B729241CEFAC007DF938 /* ClosedRange+Utils.swift */, + D0A9B72A241CEFAC007DF938 /* CGFloat.swift */, + D0A9B72B241CEFAC007DF938 /* NumberFormatter+Utils.swift */, + ); + path = Helpers; + sourceTree = ""; + }; + D0A9B72D241CEFAC007DF938 /* Charts Reader */ = { + isa = PBXGroup; + children = ( + D0A9B72E241CEFAC007DF938 /* ChartVisibilityItem.swift */, + D0A9B72F241CEFAC007DF938 /* ChartsDataManager.swift */, + D0A9B730241CEFAC007DF938 /* Convert.swift */, + D0A9B731241CEFAC007DF938 /* ChartsCollection.swift */, + D0A9B732241CEFAC007DF938 /* ChartsError.swift */, + ); + path = "Charts Reader"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + A789E0BD23E841B600AEB34A /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D0A9B760241CEFAC007DF938 /* GraphCore.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + A789E0C123E841B600AEB34A /* GraphCore */ = { + isa = PBXNativeTarget; + buildConfigurationList = A789E0CA23E841B600AEB34A /* Build configuration list for PBXNativeTarget "GraphCore" */; + buildPhases = ( + A789E0BD23E841B600AEB34A /* Headers */, + A789E0BE23E841B600AEB34A /* Sources */, + A789E0BF23E841B600AEB34A /* Frameworks */, + A789E0C023E841B600AEB34A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = GraphCore; + productName = Graph; + productReference = A789E0C223E841B600AEB34A /* GraphCore.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A789E0B923E841B600AEB34A /* Project object */ = { + isa = PBXProject; + attributes = { + DefaultBuildSystemTypeForWorkspace = Latest; + LastUpgradeCheck = 1030; + ORGANIZATIONNAME = Telegram; + TargetAttributes = { + A789E0C123E841B600AEB34A = { + CreatedOnToolsVersion = 10.3; + }; + }; + }; + buildConfigurationList = A789E0BC23E841B600AEB34A /* Build configuration list for PBXProject "GraphCore" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = A789E0B823E841B600AEB34A; + productRefGroup = A789E0C323E841B600AEB34A /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A789E0C123E841B600AEB34A /* GraphCore */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A789E0C023E841B600AEB34A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A789E0BE23E841B600AEB34A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D0A9B756241CEFAC007DF938 /* DisplayLinkService.swift in Sources */, + D0A9B759241CEFAC007DF938 /* ScalesNumberFormatter.swift in Sources */, + D0A9B744241CEFAC007DF938 /* StackedBarsChartController.swift in Sources */, + D0A9B745241CEFAC007DF938 /* LinesComponentController.swift in Sources */, + D0A9B737241CEFAC007DF938 /* PerformanceRenderer.swift in Sources */, + D0A9B75D241CEFAC007DF938 /* ClosedRange+Utils.swift in Sources */, + D0A9B736241CEFAC007DF938 /* PecentChartRenderer.swift in Sources */, + D0A9B73E241CEFAC007DF938 /* PercentPieAnimationRenderer.swift in Sources */, + D0A9B754241CEFAC007DF938 /* UIView+Extensions.swift in Sources */, + D0A9B749241CEFAC007DF938 /* GeneralChartComponentController.swift in Sources */, + D0A9B73C241CEFAC007DF938 /* PieChartRenderer.swift in Sources */, + D0A9B75C241CEFAC007DF938 /* UIImage+Utils.swift in Sources */, + D0A9B758241CEFAC007DF938 /* TimeZone.swift in Sources */, + D0A9B74D241CEFAC007DF938 /* ColorMode.swift in Sources */, + D0A9B738241CEFAC007DF938 /* VerticalLinesRenderer.swift in Sources */, + D0A9B74A241CEFAC007DF938 /* GeneralLinesChartController.swift in Sources */, + D0A9B742241CEFAC007DF938 /* BaseChartController.swift in Sources */, + D0A9B757241CEFAC007DF938 /* GlobalHelpers.swift in Sources */, + D0A9B73A241CEFAC007DF938 /* VerticalScalesRenderer.swift in Sources */, + D0A9B750241CEFAC007DF938 /* LinesChartLabel.swift in Sources */, + D0A9B75B241CEFAC007DF938 /* Array+Utils.swift in Sources */, + D0A9B75F241CEFAC007DF938 /* NumberFormatter+Utils.swift in Sources */, + D0A9B741241CEFAC007DF938 /* PercentChartComponentController.swift in Sources */, + D0A9B751241CEFAC007DF938 /* TimeInterval+Utils.swift in Sources */, + D0A9B763241CEFAC007DF938 /* Convert.swift in Sources */, + D0DEF74B242BBCE200A34A30 /* LineBulletsRenderer.swift in Sources */, + D0A9B75E241CEFAC007DF938 /* CGFloat.swift in Sources */, + D0A9B74E241CEFAC007DF938 /* ChartLineData.swift in Sources */, + D0A9B753241CEFAC007DF938 /* TextUtils.swift in Sources */, + D0A9B73B241CEFAC007DF938 /* BaseChartRenderer.swift in Sources */, + D0A9B755241CEFAC007DF938 /* UIColor+Utils.swift in Sources */, + D0A9B73F241CEFAC007DF938 /* PieChartComponentController.swift in Sources */, + D0A9B75A241CEFAC007DF938 /* CGPoint+Extensions.swift in Sources */, + D0A9B74B241CEFAC007DF938 /* TwoAxisLinesChartController.swift in Sources */, + D0A9B733241CEFAC007DF938 /* HorizontalScalesRenderer.swift in Sources */, + D0A9B762241CEFAC007DF938 /* ChartsDataManager.swift in Sources */, + D0A9B74F241CEFAC007DF938 /* LinesSelectionLabel.swift in Sources */, + D0A9B734241CEFAC007DF938 /* BarChartRenderer.swift in Sources */, + D0A9B752241CEFAC007DF938 /* AnimationController.swift in Sources */, + D0A9B735241CEFAC007DF938 /* LinesChartRenderer.swift in Sources */, + D0A9B764241CEFAC007DF938 /* ChartsCollection.swift in Sources */, + D0A9B761241CEFAC007DF938 /* ChartVisibilityItem.swift in Sources */, + D0A9B765241CEFAC007DF938 /* ChartsError.swift in Sources */, + D0A9B73D241CEFAC007DF938 /* ChartDetailsRenderer.swift in Sources */, + D0A9B740241CEFAC007DF938 /* PercentPieChartController.swift in Sources */, + D0A9B746241CEFAC007DF938 /* DailyBarsChartController.swift in Sources */, + D0A9B747241CEFAC007DF938 /* StepBarsChartController.swift in Sources */, + D0A9B743241CEFAC007DF938 /* BarsComponentController.swift in Sources */, + D0A9B74C241CEFAC007DF938 /* BaseLinesChartController.swift in Sources */, + D0DEF74C242BBDBE00A34A30 /* TwoAxisStepBarsChartController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A7393CB92406859300CE44CA /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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 = s; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + A7393CBA2406859300CE44CA /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + FRAMEWORK_VERSION = A; + GCC_PREPROCESSOR_DEFINITIONS = ""; + INFOPLIST_FILE = Graph/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Graph; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_VERSION = 5.0; + }; + name = DebugHockeyapp; + }; + A7393CBC2406859800CE44CA /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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 = s; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + A7393CBD2406859800CE44CA /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + FRAMEWORK_VERSION = A; + GCC_PREPROCESSOR_DEFINITIONS = ""; + INFOPLIST_FILE = Graph/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Graph; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_VERSION = 5.0; + }; + name = HockeyappMacAlpha; + }; + A7393CBF240685A600CE44CA /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + A7393CC0240685A600CE44CA /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = Graph/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Graph; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = ReleaseAppStore; + }; + A789E0C823E841B600AEB34A /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + A789E0C923E841B600AEB34A /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + A789E0CB23E841B600AEB34A /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = "Mac Developer"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = Graph/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Graph; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = DebugAppStore; + }; + A789E0CC23E841B600AEB34A /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = Graph/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Graph; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = ReleaseHockeyapp; + }; + D0B4940E24F3F5C800E0A9B3 /* Github */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + D0B4940F24F3F5C800E0A9B3 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = "Mac Developer"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = Graph/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Graph; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = Github; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A789E0BC23E841B600AEB34A /* Build configuration list for PBXProject "GraphCore" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A789E0C823E841B600AEB34A /* DebugAppStore */, + D0B4940E24F3F5C800E0A9B3 /* Github */, + A7393CBC2406859800CE44CA /* HockeyappMacAlpha */, + A7393CB92406859300CE44CA /* DebugHockeyapp */, + A789E0C923E841B600AEB34A /* ReleaseHockeyapp */, + A7393CBF240685A600CE44CA /* ReleaseAppStore */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseHockeyapp; + }; + A789E0CA23E841B600AEB34A /* Build configuration list for PBXNativeTarget "GraphCore" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A789E0CB23E841B600AEB34A /* DebugAppStore */, + D0B4940F24F3F5C800E0A9B3 /* Github */, + A7393CBD2406859800CE44CA /* HockeyappMacAlpha */, + A7393CBA2406859300CE44CA /* DebugHockeyapp */, + A789E0CC23E841B600AEB34A /* ReleaseHockeyapp */, + A7393CC0240685A600CE44CA /* ReleaseAppStore */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseHockeyapp; + }; +/* End XCConfigurationList section */ + }; + rootObject = A789E0B923E841B600AEB34A /* Project object */; +} diff --git a/core-xprojects/GraphCore/GraphCore.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/GraphCore/GraphCore.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..8f31ea705e --- /dev/null +++ b/core-xprojects/GraphCore/GraphCore.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/GraphCore/GraphCore.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/GraphCore/GraphCore.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/GraphCore/GraphCore.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/HotKey/HotKey.xcodeproj/project.pbxproj b/core-xprojects/HotKey/HotKey.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..87f09fb615 --- /dev/null +++ b/core-xprojects/HotKey/HotKey.xcodeproj/project.pbxproj @@ -0,0 +1,689 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + D00746DD2577A41A0000DF74 /* HotKey.h in Headers */ = {isa = PBXBuildFile; fileRef = D00746DB2577A41A0000DF74 /* HotKey.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D00746E92577A68E0000DF74 /* HotKeyUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = D00746E52577A6850000DF74 /* HotKeyUtilities.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D00746EC2577A68E0000DF74 /* HotKeyUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = D00746E82577A68E0000DF74 /* HotKeyUtilities.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + D00746D82577A41A0000DF74 /* HotKey.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = HotKey.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D00746DB2577A41A0000DF74 /* HotKey.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HotKey.h; sourceTree = ""; }; + D00746DC2577A41A0000DF74 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D00746E52577A6850000DF74 /* HotKeyUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HotKeyUtilities.h; sourceTree = ""; }; + D00746E82577A68E0000DF74 /* HotKeyUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HotKeyUtilities.m; sourceTree = ""; }; + D00746F12577A6C90000DF74 /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D00746D52577A41A0000DF74 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + D00746CE2577A41A0000DF74 = { + isa = PBXGroup; + children = ( + D00746DA2577A41A0000DF74 /* HotKey */, + D00746D92577A41A0000DF74 /* Products */, + D00746F02577A6C90000DF74 /* Frameworks */, + ); + sourceTree = ""; + }; + D00746D92577A41A0000DF74 /* Products */ = { + isa = PBXGroup; + children = ( + D00746D82577A41A0000DF74 /* HotKey.framework */, + ); + name = Products; + sourceTree = ""; + }; + D00746DA2577A41A0000DF74 /* HotKey */ = { + isa = PBXGroup; + children = ( + D00746E52577A6850000DF74 /* HotKeyUtilities.h */, + D00746E82577A68E0000DF74 /* HotKeyUtilities.m */, + D00746DB2577A41A0000DF74 /* HotKey.h */, + D00746DC2577A41A0000DF74 /* Info.plist */, + ); + path = HotKey; + sourceTree = ""; + }; + D00746F02577A6C90000DF74 /* Frameworks */ = { + isa = PBXGroup; + children = ( + D00746F12577A6C90000DF74 /* Carbon.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + D00746D32577A41A0000DF74 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D00746DD2577A41A0000DF74 /* HotKey.h in Headers */, + D00746E92577A68E0000DF74 /* HotKeyUtilities.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + D00746D72577A41A0000DF74 /* HotKey */ = { + isa = PBXNativeTarget; + buildConfigurationList = D00746E02577A41A0000DF74 /* Build configuration list for PBXNativeTarget "HotKey" */; + buildPhases = ( + D00746D32577A41A0000DF74 /* Headers */, + D00746D42577A41A0000DF74 /* Sources */, + D00746D52577A41A0000DF74 /* Frameworks */, + D00746D62577A41A0000DF74 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = HotKey; + productName = DDHotKey; + productReference = D00746D82577A41A0000DF74 /* HotKey.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D00746CF2577A41A0000DF74 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1220; + TargetAttributes = { + D00746D72577A41A0000DF74 = { + CreatedOnToolsVersion = 12.2; + }; + }; + }; + buildConfigurationList = D00746D22577A41A0000DF74 /* Build configuration list for PBXProject "HotKey" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = D00746CE2577A41A0000DF74; + productRefGroup = D00746D92577A41A0000DF74 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D00746D72577A41A0000DF74 /* HotKey */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D00746D62577A41A0000DF74 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D00746D42577A41A0000DF74 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D00746EC2577A68E0000DF74 /* HotKeyUtilities.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + D00746DE2577A41A0000DF74 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + D00746DF2577A41A0000DF74 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + D00746E12577A41A0000DF74 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = HotKey/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.HotKey; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = DebugAppStore; + }; + D00746E22577A41A0000DF74 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = HotKey/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.HotKey; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = ReleaseAppStore; + }; + D00746F62577A76F0000DF74 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + D00746F72577A76F0000DF74 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = HotKey/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.HotKey; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = ReleaseHockeyapp; + }; + D00746F82577A7750000DF74 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + D00746F92577A7750000DF74 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = HotKey/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.HotKey; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = DebugHockeyapp; + }; + D00746FA2577A77E0000DF74 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + D00746FB2577A77E0000DF74 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = HotKey/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.HotKey; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = HockeyappMacAlpha; + }; + D00746FC2577A7820000DF74 /* Github */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + D00746FD2577A7820000DF74 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = HotKey/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.HotKey; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = Github; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D00746D22577A41A0000DF74 /* Build configuration list for PBXProject "HotKey" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D00746DE2577A41A0000DF74 /* DebugAppStore */, + D00746FC2577A7820000DF74 /* Github */, + D00746DF2577A41A0000DF74 /* ReleaseAppStore */, + D00746F62577A76F0000DF74 /* ReleaseHockeyapp */, + D00746F82577A7750000DF74 /* DebugHockeyapp */, + D00746FA2577A77E0000DF74 /* HockeyappMacAlpha */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; + D00746E02577A41A0000DF74 /* Build configuration list for PBXNativeTarget "HotKey" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D00746E12577A41A0000DF74 /* DebugAppStore */, + D00746FD2577A7820000DF74 /* Github */, + D00746E22577A41A0000DF74 /* ReleaseAppStore */, + D00746F72577A76F0000DF74 /* ReleaseHockeyapp */, + D00746F92577A7750000DF74 /* DebugHockeyapp */, + D00746FB2577A77E0000DF74 /* HockeyappMacAlpha */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; +/* End XCConfigurationList section */ + }; + rootObject = D00746CF2577A41A0000DF74 /* Project object */; +} diff --git a/core-xprojects/HotKey/HotKey.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/HotKey/HotKey.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/core-xprojects/HotKey/HotKey.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/HotKey/HotKey.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/HotKey/HotKey.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/HotKey/HotKey.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/HotKey/HotKey/HotKey.h b/core-xprojects/HotKey/HotKey/HotKey.h new file mode 100644 index 0000000000..87804d19e4 --- /dev/null +++ b/core-xprojects/HotKey/HotKey/HotKey.h @@ -0,0 +1,9 @@ + +#import + +FOUNDATION_EXPORT double DDHotKeyVersionNumber; + +FOUNDATION_EXPORT const unsigned char DDHotKeyVersionString[]; + + +#import diff --git a/core-xprojects/HotKey/HotKey/HotKeyUtilities.h b/core-xprojects/HotKey/HotKey/HotKeyUtilities.h new file mode 100644 index 0000000000..534413e99a --- /dev/null +++ b/core-xprojects/HotKey/HotKey/HotKeyUtilities.h @@ -0,0 +1,15 @@ +#import + +extern NSString *StringFromKeyCode(unsigned short keyCode, NSUInteger modifiers); +extern UInt32 CarbonModifierFlagsFromCocoaModifiers(NSUInteger flags); + + + +@interface PermissionsManager : NSObject + ++ (void)requestInputMonitoringPermission; ++ (void)openInputMonitoringPrefs; + ++ (BOOL)checkInputMonitoringWithPrompt:(BOOL)prompt; ++ (BOOL)checkAccessibilityWithPrompt:(BOOL)prompt; +@end diff --git a/core-xprojects/HotKey/HotKey/HotKeyUtilities.m b/core-xprojects/HotKey/HotKey/HotKeyUtilities.m new file mode 100644 index 0000000000..33896c014e --- /dev/null +++ b/core-xprojects/HotKey/HotKey/HotKeyUtilities.m @@ -0,0 +1,206 @@ + +#import "HotKeyUtilities.h" +#import +#import + +static NSDictionary *_KeyCodeToCharacterMap(void); +static NSDictionary *_KeyCodeToCharacterMap(void) { + static NSDictionary *keyCodeMap = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + keyCodeMap = @{ + @(kVK_Return) : @"↩", + @(kVK_Tab) : @"⇥", + @(kVK_Space) : @"⎵", + @(kVK_Delete) : @"⌫", + @(kVK_Escape) : @"⎋", + @(kVK_Command) : @"⌘", + @(kVK_Shift) : @"⇧", + @(kVK_CapsLock) : @"⇪", + @(kVK_Option) : @"⌥", + @(kVK_Control) : @"⌃", + @(kVK_RightShift) : @"⇧", + @(kVK_RightOption) : @"⌥", + @(kVK_RightControl) : @"⌃", + @(kVK_VolumeUp) : @"🔊", + @(kVK_VolumeDown) : @"🔈", + @(kVK_Mute) : @"🔇", + @(kVK_Function) : @"\u2318", + @(kVK_F1) : @"F1", + @(kVK_F2) : @"F2", + @(kVK_F3) : @"F3", + @(kVK_F4) : @"F4", + @(kVK_F5) : @"F5", + @(kVK_F6) : @"F6", + @(kVK_F7) : @"F7", + @(kVK_F8) : @"F8", + @(kVK_F9) : @"F9", + @(kVK_F10) : @"F10", + @(kVK_F11) : @"F11", + @(kVK_F12) : @"F12", + @(kVK_F13) : @"F13", + @(kVK_F14) : @"F14", + @(kVK_F15) : @"F15", + @(kVK_F16) : @"F16", + @(kVK_F17) : @"F17", + @(kVK_F18) : @"F18", + @(kVK_F19) : @"F19", + @(kVK_F20) : @"F20", + // @(kVK_Help) : @"", + @(kVK_ForwardDelete) : @"⌦", + @(kVK_Home) : @"↖", + @(kVK_End) : @"↘", + @(kVK_PageUp) : @"⇞", + @(kVK_PageDown) : @"⇟", + @(kVK_LeftArrow) : @"←", + @(kVK_RightArrow) : @"→", + @(kVK_DownArrow) : @"↓", + @(kVK_UpArrow) : @"↑", + }; + }); + return keyCodeMap; +} + +NSString *StringFromKeyCode(unsigned short keyCode, NSUInteger modifiers) { + NSMutableString *final = [NSMutableString stringWithString:@""]; + NSDictionary *characterMap = _KeyCodeToCharacterMap(); + + if (modifiers & NSControlKeyMask) { + [final appendString:[characterMap objectForKey:@(kVK_Control)]]; + } + if (modifiers & NSAlternateKeyMask) { + [final appendString:[characterMap objectForKey:@(kVK_Option)]]; + } + if (modifiers & NSShiftKeyMask) { + [final appendString:[characterMap objectForKey:@(kVK_Shift)]]; + } + if (modifiers & NSCommandKeyMask) { + [final appendString:[characterMap objectForKey:@(kVK_Command)]]; + } + + if (keyCode == kVK_Control || keyCode == kVK_Option || keyCode == kVK_Shift || keyCode == kVK_Command) { + return final; + } + + NSString *mapped = [characterMap objectForKey:@(keyCode)]; + if (mapped != nil) { + [final appendString:mapped]; + } else { + + TISInputSourceRef currentKeyboard = TISCopyCurrentASCIICapableKeyboardLayoutInputSource(); + CFDataRef uchr = (CFDataRef)TISGetInputSourceProperty(currentKeyboard, kTISPropertyUnicodeKeyLayoutData); + + // Fix crash using non-unicode layouts, such as Chinese or Japanese. + if (!uchr) { + CFRelease(currentKeyboard); + currentKeyboard = TISCopyCurrentASCIICapableKeyboardLayoutInputSource(); + uchr = (CFDataRef)TISGetInputSourceProperty(currentKeyboard, kTISPropertyUnicodeKeyLayoutData); + } + + const UCKeyboardLayout *keyboardLayout = (const UCKeyboardLayout*)CFDataGetBytePtr(uchr); + + if (keyboardLayout) { + UInt32 deadKeyState = 0; + UniCharCount maxStringLength = 255; + UniCharCount actualStringLength = 0; + UniChar unicodeString[maxStringLength]; + + UInt32 keyModifiers = CarbonModifierFlagsFromCocoaModifiers(modifiers); + + OSStatus status = UCKeyTranslate(keyboardLayout, + keyCode, kUCKeyActionDown, keyModifiers, + LMGetKbdType(), 0, + &deadKeyState, + maxStringLength, + &actualStringLength, unicodeString); + + if (actualStringLength > 0 && status == noErr) { + NSString *characterString = [NSString stringWithCharacters:unicodeString length:(NSUInteger)actualStringLength]; + + [final appendString:characterString]; + } + } + } + + return final; +} + +UInt32 CarbonModifierFlagsFromCocoaModifiers(NSUInteger flags) { + UInt32 newFlags = 0; + if ((flags & NSControlKeyMask) > 0) { newFlags |= controlKey; } + if ((flags & NSCommandKeyMask) > 0) { newFlags |= cmdKey; } + if ((flags & NSShiftKeyMask) > 0) { newFlags |= shiftKey; } + if ((flags & NSAlternateKeyMask) > 0) { newFlags |= optionKey; } + if ((flags & NSAlphaShiftKeyMask) > 0) { newFlags |= alphaLock; } + return newFlags; +} + +#import + +NSString *const PermissionsManagerKeyAccessibilityEnabled=@"accessibilityEnabled"; +NSString *const PermissionsManagerKeyInputMonitoringEnabled=@"inputMonitoringEnabled"; +NSString *const PermissionsManagerKeyHasAllRequiredPermissions=@"hasAllRequiredPermissions"; + +static NSString *const PrefsHasRequestedInputMonitoringPermission=@"HasRequestedInputMonitoringPermission"; +static NSString *const PrefsHasRequestedAccessibilityPermission=@"HasRequestedAccessibilityPermission"; + +@interface PermissionsManager () +@property (getter=isAccessibilityEnabled) BOOL accessibilityEnabled; +@property (getter=isInputMonitoringEnabled) BOOL inputMonitoringEnabled; +@property NSTimer *refreshTimer; +@property NSDate *refreshStarted; +@end + +@implementation PermissionsManager + + ++ (BOOL)checkAccessibilityWithPrompt:(BOOL)prompt +{ + // this is a 10.9 API but is only needed on 10.14. + // with no prompt, this check is very fast. otherwise it blocks. + NSDictionary *const options=@{(__bridge NSString *)kAXTrustedCheckOptionPrompt: @(prompt)}; + return AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options); +} + ++ (BOOL)checkInputMonitoringWithPrompt:(BOOL)prompt +{ + if (@available(macOS 10.15, *)) { + static const IOHIDRequestType accessType=kIOHIDRequestTypeListenEvent; + if (prompt) { + // this will block + dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ + IOHIDRequestAccess(accessType); + }); + return NO; + } + else { + // this check is very fast + return kIOHIDAccessTypeGranted==IOHIDCheckAccess(accessType); + } + } + else { + return [PermissionsManager checkAccessibilityWithPrompt:prompt]; + } +} + ++ (NSURL *)securitySettingsUrlForKey:(NSString *)key +{ + return [NSURL URLWithString:[NSString stringWithFormat:@"x-apple.systempreferences:com.apple.preference.security?%@", key]]; +} + ++ (void)openAccessibilityPrefs +{ + [[NSWorkspace sharedWorkspace] openURL:[PermissionsManager securitySettingsUrlForKey:@"Privacy_Accessibility"]]; +} + ++ (void)openInputMonitoringPrefs +{ + if (@available(macOS 10.15, *)) { + [[NSWorkspace sharedWorkspace] openURL:[PermissionsManager securitySettingsUrlForKey:@"Privacy_ListenEvent"]]; + } else { + [self openAccessibilityPrefs]; + } +} + + +@end diff --git a/core-xprojects/HotKey/HotKey/Info.plist b/core-xprojects/HotKey/HotKey/Info.plist new file mode 100644 index 0000000000..9bcb244429 --- /dev/null +++ b/core-xprojects/HotKey/HotKey/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/core-xprojects/LegacyReachability/LegacyReachability.xcodeproj/project.pbxproj b/core-xprojects/LegacyReachability/LegacyReachability.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..b4969cacea --- /dev/null +++ b/core-xprojects/LegacyReachability/LegacyReachability.xcodeproj/project.pbxproj @@ -0,0 +1,715 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + A76D02C625C4037E00FCE1B5 /* LegacyReachability.h in Headers */ = {isa = PBXBuildFile; fileRef = A76D02C525C4037E00FCE1B5 /* LegacyReachability.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A76D02C825C4038300FCE1B5 /* LegacyReachability.m in Sources */ = {isa = PBXBuildFile; fileRef = A76D02C725C4038300FCE1B5 /* LegacyReachability.m */; }; + A7919027240CF904002011CA /* LegacyReachabilityPublic.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919025240CF904002011CA /* LegacyReachabilityPublic.h */; settings = {ATTRIBUTES = (Public, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A76D02C525C4037E00FCE1B5 /* LegacyReachability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = LegacyReachability.h; path = "../../../submodules/telegram-ios/submodules/Reachability/LegacyReachability/PublicHeaders/LegacyReachability/LegacyReachability.h"; sourceTree = ""; }; + A76D02C725C4038300FCE1B5 /* LegacyReachability.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = LegacyReachability.m; path = "../../../submodules/telegram-ios/submodules/Reachability/LegacyReachability/Sources/LegacyReachability.m"; sourceTree = ""; }; + A7919022240CF904002011CA /* LegacyReachability.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = LegacyReachability.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7919025240CF904002011CA /* LegacyReachabilityPublic.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LegacyReachabilityPublic.h; sourceTree = ""; }; + A7919026240CF904002011CA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A791901F240CF904002011CA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A7919018240CF904002011CA = { + isa = PBXGroup; + children = ( + A7919024240CF904002011CA /* LegacyReachability */, + A7919023240CF904002011CA /* Products */, + ); + sourceTree = ""; + }; + A7919023240CF904002011CA /* Products */ = { + isa = PBXGroup; + children = ( + A7919022240CF904002011CA /* LegacyReachability.framework */, + ); + name = Products; + sourceTree = ""; + }; + A7919024240CF904002011CA /* LegacyReachability */ = { + isa = PBXGroup; + children = ( + A76D02C725C4038300FCE1B5 /* LegacyReachability.m */, + A76D02C525C4037E00FCE1B5 /* LegacyReachability.h */, + A7919025240CF904002011CA /* LegacyReachabilityPublic.h */, + A7919026240CF904002011CA /* Info.plist */, + ); + path = LegacyReachability; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + A791901D240CF904002011CA /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + A76D02C625C4037E00FCE1B5 /* LegacyReachability.h in Headers */, + A7919027240CF904002011CA /* LegacyReachabilityPublic.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + A7919021240CF904002011CA /* LegacyReachability */ = { + isa = PBXNativeTarget; + buildConfigurationList = A791902A240CF904002011CA /* Build configuration list for PBXNativeTarget "LegacyReachability" */; + buildPhases = ( + A791901D240CF904002011CA /* Headers */, + A791901E240CF904002011CA /* Sources */, + A791901F240CF904002011CA /* Frameworks */, + A7919020240CF904002011CA /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = LegacyReachability; + productName = Reachability; + productReference = A7919022240CF904002011CA /* LegacyReachability.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A7919019240CF904002011CA /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1030; + ORGANIZATIONNAME = Telegram; + TargetAttributes = { + A7919021240CF904002011CA = { + CreatedOnToolsVersion = 10.3; + }; + }; + }; + buildConfigurationList = A791901C240CF904002011CA /* Build configuration list for PBXProject "LegacyReachability" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = A7919018240CF904002011CA; + productRefGroup = A7919023240CF904002011CA /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A7919021240CF904002011CA /* LegacyReachability */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A7919020240CF904002011CA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A791901E240CF904002011CA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A76D02C825C4038300FCE1B5 /* LegacyReachability.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A7919028240CF904002011CA /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + A7919029240CF904002011CA /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + A791902B240CF904002011CA /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = LegacyReachability/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.LegacyReachability; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = DebugAppStore; + }; + A791902C240CF904002011CA /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = LegacyReachability/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.LegacyReachability; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = ReleaseHockeyapp; + }; + A791903C240CFA4D002011CA /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + A791903D240CFA4D002011CA /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_OPTIMIZATION_LEVEL = s; + INFOPLIST_FILE = LegacyReachability/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.LegacyReachability; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = DebugHockeyapp; + }; + A791903E240CFA55002011CA /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + A791903F240CFA55002011CA /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_OPTIMIZATION_LEVEL = s; + INFOPLIST_FILE = LegacyReachability/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.LegacyReachability; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = HockeyappMacAlpha; + }; + A7919040240CFA5D002011CA /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + A7919041240CFA5D002011CA /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = LegacyReachability/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.LegacyReachability; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = ReleaseAppStore; + }; + D0B4941624F3F60B00E0A9B3 /* Github */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + D0B4941724F3F60B00E0A9B3 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = LegacyReachability/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.LegacyReachability; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = Github; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A791901C240CF904002011CA /* Build configuration list for PBXProject "LegacyReachability" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A7919028240CF904002011CA /* DebugAppStore */, + D0B4941624F3F60B00E0A9B3 /* Github */, + A791903E240CFA55002011CA /* HockeyappMacAlpha */, + A791903C240CFA4D002011CA /* DebugHockeyapp */, + A7919029240CF904002011CA /* ReleaseHockeyapp */, + A7919040240CFA5D002011CA /* ReleaseAppStore */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseHockeyapp; + }; + A791902A240CF904002011CA /* Build configuration list for PBXNativeTarget "LegacyReachability" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A791902B240CF904002011CA /* DebugAppStore */, + D0B4941724F3F60B00E0A9B3 /* Github */, + A791903F240CFA55002011CA /* HockeyappMacAlpha */, + A791903D240CFA4D002011CA /* DebugHockeyapp */, + A791902C240CF904002011CA /* ReleaseHockeyapp */, + A7919041240CFA5D002011CA /* ReleaseAppStore */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseHockeyapp; + }; +/* End XCConfigurationList section */ + }; + rootObject = A7919019240CF904002011CA /* Project object */; +} diff --git a/core-xprojects/LegacyReachability/LegacyReachability.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/LegacyReachability/LegacyReachability.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..87066cc83f --- /dev/null +++ b/core-xprojects/LegacyReachability/LegacyReachability.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/LegacyReachability/LegacyReachability.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/LegacyReachability/LegacyReachability.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/LegacyReachability/LegacyReachability.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/LegacyReachability/LegacyReachability/Info.plist b/core-xprojects/LegacyReachability/LegacyReachability/Info.plist new file mode 100644 index 0000000000..861c829fcb --- /dev/null +++ b/core-xprojects/LegacyReachability/LegacyReachability/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2020 Telegram. All rights reserved. + + diff --git a/core-xprojects/LegacyReachability/LegacyReachability/LegacyReachabilityPublic.h b/core-xprojects/LegacyReachability/LegacyReachability/LegacyReachabilityPublic.h new file mode 100644 index 0000000000..23c9c0cc9f --- /dev/null +++ b/core-xprojects/LegacyReachability/LegacyReachability/LegacyReachabilityPublic.h @@ -0,0 +1,19 @@ +// +// Reachability.h +// Reachability +// +// Created by Mikhail Filimonov on 02.03.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +#import + +//! Project version number for Reachability. +FOUNDATION_EXPORT double ReachabilityVersionNumber; + +//! Project version string for Reachability. +FOUNDATION_EXPORT const unsigned char ReachabilityVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + +#import diff --git a/core-xprojects/Mozjpeg/Mozjpeg.xcodeproj/project.pbxproj b/core-xprojects/Mozjpeg/Mozjpeg.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..2d7c3a6a37 --- /dev/null +++ b/core-xprojects/Mozjpeg/Mozjpeg.xcodeproj/project.pbxproj @@ -0,0 +1,772 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + D0A51CF924F7CF9200D75641 /* Mozjpeg.h in Headers */ = {isa = PBXBuildFile; fileRef = D0A51CEB24F7CF9200D75641 /* Mozjpeg.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0A51D3724F7F48400D75641 /* Mozjpeg.m in Sources */ = {isa = PBXBuildFile; fileRef = D0A51CDD24F7CF6800D75641 /* Mozjpeg.m */; }; + D0A51D3C24F7F62C00D75641 /* libjpeg.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0A51D3A24F7F62C00D75641 /* libjpeg.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + D0A51CDB24F7CF6800D75641 /* Mozjpeg.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Mozjpeg.h; sourceTree = ""; }; + D0A51CDD24F7CF6800D75641 /* Mozjpeg.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Mozjpeg.m; sourceTree = ""; }; + D0A51CE924F7CF9200D75641 /* Mozjpeg.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Mozjpeg.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0A51CEB24F7CF9200D75641 /* Mozjpeg.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Mozjpeg.h; sourceTree = ""; }; + D0A51CEC24F7CF9200D75641 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D0A51D3924F7F62C00D75641 /* libturbojpeg.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libturbojpeg.a; path = build/libturbojpeg.a; sourceTree = ""; }; + D0A51D3A24F7F62C00D75641 /* libjpeg.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libjpeg.a; path = build/libjpeg.a; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D0A51CE624F7CF9200D75641 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D0A51D3C24F7F62C00D75641 /* libjpeg.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + D0A51CCF24F7CF6800D75641 = { + isa = PBXGroup; + children = ( + D0A51CDA24F7CF6800D75641 /* Mozjpeg */, + D0A51CEA24F7CF9200D75641 /* Mozjpeg */, + D0A51CD924F7CF6800D75641 /* Products */, + D0A51D3824F7F62C00D75641 /* Frameworks */, + ); + sourceTree = ""; + }; + D0A51CD924F7CF6800D75641 /* Products */ = { + isa = PBXGroup; + children = ( + D0A51CE924F7CF9200D75641 /* Mozjpeg.framework */, + ); + name = Products; + sourceTree = ""; + }; + D0A51CDA24F7CF6800D75641 /* Mozjpeg */ = { + isa = PBXGroup; + children = ( + D0A51CDB24F7CF6800D75641 /* Mozjpeg.h */, + D0A51CDD24F7CF6800D75641 /* Mozjpeg.m */, + ); + path = Mozjpeg; + sourceTree = ""; + }; + D0A51CEA24F7CF9200D75641 /* Mozjpeg */ = { + isa = PBXGroup; + children = ( + D0A51CEB24F7CF9200D75641 /* Mozjpeg.h */, + D0A51CEC24F7CF9200D75641 /* Info.plist */, + ); + path = Mozjpeg; + sourceTree = ""; + }; + D0A51D3824F7F62C00D75641 /* Frameworks */ = { + isa = PBXGroup; + children = ( + D0A51D3A24F7F62C00D75641 /* libjpeg.a */, + D0A51D3924F7F62C00D75641 /* libturbojpeg.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + D0A51CE424F7CF9200D75641 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D0A51CF924F7CF9200D75641 /* Mozjpeg.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + D0A51CE824F7CF9200D75641 /* Mozjpeg */ = { + isa = PBXNativeTarget; + buildConfigurationList = D0A51CFA24F7CF9200D75641 /* Build configuration list for PBXNativeTarget "Mozjpeg" */; + buildPhases = ( + D0A51D0824F7DCFD00D75641 /* Build Script */, + D0A51CE424F7CF9200D75641 /* Headers */, + D0A51CE524F7CF9200D75641 /* Sources */, + D0A51CE624F7CF9200D75641 /* Frameworks */, + D0A51CE724F7CF9200D75641 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Mozjpeg; + productName = Mozjpeg; + productReference = D0A51CE924F7CF9200D75641 /* Mozjpeg.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D0A51CD024F7CF6800D75641 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1160; + ORGANIZATIONNAME = "Mikhail Filimonov"; + TargetAttributes = { + D0A51CE824F7CF9200D75641 = { + CreatedOnToolsVersion = 11.6; + }; + }; + }; + buildConfigurationList = D0A51CD324F7CF6800D75641 /* Build configuration list for PBXProject "Mozjpeg" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = D0A51CCF24F7CF6800D75641; + productRefGroup = D0A51CD924F7CF6800D75641 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D0A51CE824F7CF9200D75641 /* Mozjpeg */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D0A51CE724F7CF9200D75641 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + D0A51D0824F7DCFD00D75641 /* Build Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Build Script"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nif [ ! -d \"$PROJECT_DIR/build\" ]; then\n sh $PROJECT_DIR/mozjpeg/build.sh ../../submodules/telegram-ios/third-party/mozjpeg/mozjpeg\nfi\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D0A51CE524F7CF9200D75641 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D0A51D3724F7F48400D75641 /* Mozjpeg.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + D0A51CDF24F7CF6800D75641 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + }; + name = DebugAppStore; + }; + D0A51CE024F7CF6800D75641 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + }; + name = ReleaseHockeyapp; + }; + D0A51CFB24F7CF9200D75641 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_STYLE = Manual; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = ( + $PROJECT_DIR/build, + "$PROJECT_DIR/../../submodules/telegram-ios/third-party/mozjpeg/mozjpeg", + ); + INFOPLIST_FILE = Mozjpeg/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + $PROJECT_DIR/build/, + "$(PROJECT_DIR)/build", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Mozjpeg; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + D0A51CFC24F7CF9200D75641 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_STYLE = Manual; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = ( + $PROJECT_DIR/build, + "$PROJECT_DIR/../../submodules/telegram-ios/third-party/mozjpeg/mozjpeg", + ); + INFOPLIST_FILE = Mozjpeg/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + $PROJECT_DIR/build/, + "$(PROJECT_DIR)/build", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Mozjpeg; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + D0A51D0024F7DB8A00D75641 /* Github */ = { + isa = XCBuildConfiguration; + 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; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + }; + name = Github; + }; + D0A51D0124F7DB8A00D75641 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_STYLE = Manual; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = ( + $PROJECT_DIR/build, + "$PROJECT_DIR/../../submodules/telegram-ios/third-party/mozjpeg/mozjpeg", + ); + INFOPLIST_FILE = Mozjpeg/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + $PROJECT_DIR/build/, + "$(PROJECT_DIR)/build", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Mozjpeg; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + D0A51D0224F7DB9900D75641 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + }; + name = ReleaseAppStore; + }; + D0A51D0324F7DB9900D75641 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_STYLE = Manual; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = ( + $PROJECT_DIR/build, + "$PROJECT_DIR/../../submodules/telegram-ios/third-party/mozjpeg/mozjpeg", + ); + INFOPLIST_FILE = Mozjpeg/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + $PROJECT_DIR/build/, + "$(PROJECT_DIR)/build", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Mozjpeg; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + D0A51D0424F7DBA000D75641 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + }; + name = DebugHockeyapp; + }; + D0A51D0524F7DBA000D75641 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_STYLE = Manual; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = ( + $PROJECT_DIR/build, + "$PROJECT_DIR/../../submodules/telegram-ios/third-party/mozjpeg/mozjpeg", + ); + INFOPLIST_FILE = Mozjpeg/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + $PROJECT_DIR/build/, + "$(PROJECT_DIR)/build", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Mozjpeg; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + D0A51D0624F7DBB600D75641 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + }; + name = HockeyappMacAlpha; + }; + D0A51D0724F7DBB600D75641 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_STYLE = Manual; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = ( + $PROJECT_DIR/build, + "$PROJECT_DIR/../../submodules/telegram-ios/third-party/mozjpeg/mozjpeg", + ); + INFOPLIST_FILE = Mozjpeg/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + $PROJECT_DIR/build/, + "$(PROJECT_DIR)/build", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Mozjpeg; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D0A51CD324F7CF6800D75641 /* Build configuration list for PBXProject "Mozjpeg" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0A51CDF24F7CF6800D75641 /* DebugAppStore */, + D0A51D0024F7DB8A00D75641 /* Github */, + D0A51CE024F7CF6800D75641 /* ReleaseHockeyapp */, + D0A51D0224F7DB9900D75641 /* ReleaseAppStore */, + D0A51D0624F7DBB600D75641 /* HockeyappMacAlpha */, + D0A51D0424F7DBA000D75641 /* DebugHockeyapp */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseHockeyapp; + }; + D0A51CFA24F7CF9200D75641 /* Build configuration list for PBXNativeTarget "Mozjpeg" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0A51CFB24F7CF9200D75641 /* DebugAppStore */, + D0A51D0124F7DB8A00D75641 /* Github */, + D0A51CFC24F7CF9200D75641 /* ReleaseHockeyapp */, + D0A51D0324F7DB9900D75641 /* ReleaseAppStore */, + D0A51D0724F7DBB600D75641 /* HockeyappMacAlpha */, + D0A51D0524F7DBA000D75641 /* DebugHockeyapp */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseHockeyapp; + }; +/* End XCConfigurationList section */ + }; + rootObject = D0A51CD024F7CF6800D75641 /* Project object */; +} diff --git a/core-xprojects/Mozjpeg/Mozjpeg.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/Mozjpeg/Mozjpeg.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..4a701496d2 --- /dev/null +++ b/core-xprojects/Mozjpeg/Mozjpeg.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/Mozjpeg/Mozjpeg.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/Mozjpeg/Mozjpeg.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/Mozjpeg/Mozjpeg.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/Mozjpeg/Mozjpeg.xcodeproj/xcshareddata/xcschemes/Mozjpeg.xcscheme b/core-xprojects/Mozjpeg/Mozjpeg.xcodeproj/xcshareddata/xcschemes/Mozjpeg.xcscheme new file mode 100644 index 0000000000..1c942696b1 --- /dev/null +++ b/core-xprojects/Mozjpeg/Mozjpeg.xcodeproj/xcshareddata/xcschemes/Mozjpeg.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/Mozjpeg/Mozjpeg/Info.plist b/core-xprojects/Mozjpeg/Mozjpeg/Info.plist new file mode 100644 index 0000000000..89edd11cbe --- /dev/null +++ b/core-xprojects/Mozjpeg/Mozjpeg/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2020 Mikhail Filimonov. All rights reserved. + + diff --git a/core-xprojects/Mozjpeg/Mozjpeg/Mozjpeg.h b/core-xprojects/Mozjpeg/Mozjpeg/Mozjpeg.h new file mode 100644 index 0000000000..0dfc725a56 --- /dev/null +++ b/core-xprojects/Mozjpeg/Mozjpeg/Mozjpeg.h @@ -0,0 +1,22 @@ +// +// Mozjpeg.h +// Mozjpeg +// +// Created by Mikhail Filimonov on 27/08/2020. +// Copyright © 2020 Mikhail Filimonov. All rights reserved. +// + +#import + +//! Project version number for Mozjpeg. +FOUNDATION_EXPORT double MozjpegVersionNumber; + +//! Project version string for Mozjpeg. +FOUNDATION_EXPORT const unsigned char MozjpegVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + +NSData * _Nullable compressJPEGData(CGImageRef _Nonnull sourceImage); +NSArray * _Nonnull extractJPEGDataScans(NSData * _Nonnull data); +NSData * _Nullable compressMiniThumbnail(CGImageRef _Nonnull image); diff --git a/core-xprojects/Mozjpeg/Mozjpeg/Mozjpeg.m b/core-xprojects/Mozjpeg/Mozjpeg/Mozjpeg.m new file mode 100644 index 0000000000..2f917288e4 --- /dev/null +++ b/core-xprojects/Mozjpeg/Mozjpeg/Mozjpeg.m @@ -0,0 +1,263 @@ +// +// Mozjpeg.m +// Mozjpeg +// +// Created by Mikhail Filimonov on 27/08/2020. +// Copyright © 2020 Mikhail Filimonov. All rights reserved. +// + + + +#import "Mozjpeg.h" +#import "turbojpeg.h" +#import "jpeglib.h" +#import + +static NSData *getHeaderPattern() { + static NSData *value = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + value = [[NSData alloc] initWithBase64EncodedString:@"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDACgcHiMeGSgjISMtKygwPGRBPDc3PHtYXUlkkYCZlo+AjIqgtObDoKrarYqMyP/L2u71////m8H////6/+b9//j/2wBDASstLTw1PHZBQXb4pYyl+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj4+Pj/wAARCAAAAAADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwA=" options:0]; + }); + return value; +} + +static NSData *getFooterPattern() { + static NSData *value = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + value = [[NSData alloc] initWithBase64EncodedString:@"/9k=" options:0]; + }); + return value; +} + +NSArray * _Nonnull extractJPEGDataScans(NSData * _Nonnull data) { + NSMutableArray *result = [[NSMutableArray alloc] init]; + + const uint8_t *dataBytes = data.bytes; + int offset = 0; + while (offset < data.length) { + bool found = false; + for (int i = offset + 2; i < data.length - 1; i++) { + if (dataBytes[i] == 0xffU && dataBytes[i + 1] == 0xdaU) { + if (offset != 0) { + [result addObject:@(i)]; + } + offset = i; + found = true; + } + } + if (!found) { + break; + } + } + +#if DEBUG + static NSString *sessionPrefix = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sessionPrefix = [NSString stringWithFormat:@"%u", arc4random()]; + }); + + NSString *randomId = [NSString stringWithFormat:@"%u", arc4random()]; + NSString *dirPath = [[NSTemporaryDirectory() stringByAppendingPathComponent:sessionPrefix] stringByAppendingPathComponent:randomId]; + [[NSFileManager defaultManager] createDirectoryAtPath:dirPath withIntermediateDirectories:true attributes:nil error:nil]; + for (int i = 0; i < result.count + 1; i++) { + NSString *filePath = [dirPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%d.jpg", i]]; + if (i == result.count) { + [data writeToFile:filePath atomically:true]; + } else { + [[data subdataWithRange:NSMakeRange(0, [result[i] intValue])] writeToFile:filePath atomically:true]; + } + } + NSLog(@"Path: %@", dirPath); +#endif + + return result; +} + +NSData * _Nullable compressJPEGData(CGImageRef _Nonnull sourceImage) { + int width = (int)(CGImageGetWidth(sourceImage)); + int height = (int)(CGImageGetHeight(sourceImage)); + + int targetBytesPerRow = ((4 * (int)width) + 15) & (~15); + uint8_t *targetMemory = malloc((int)(targetBytesPerRow * height)); + + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGBitmapInfo bitmapInfo = kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host; + + CGContextRef targetContext = CGBitmapContextCreate(targetMemory, width, height, 8, targetBytesPerRow, colorSpace, bitmapInfo); + + + CGColorSpaceRelease(colorSpace); + + CGContextSetFillColorWithColor(targetContext, [NSColor whiteColor].CGColor); + CGContextFillRect(targetContext, CGRectMake(0, 0, width, height)); + + CGContextDrawImage(targetContext, CGRectMake(0, 0, width, height), sourceImage); + + + int bufferBytesPerRow = ((3 * (int)width) + 15) & (~15); + uint8_t *buffer = malloc(bufferBytesPerRow * height); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + uint32_t *color = ((uint32_t *)&targetMemory[y * targetBytesPerRow + x * 4]); + + uint32_t r = ((*color >> 16) & 0xff); + uint32_t g = ((*color >> 8) & 0xff); + uint32_t b = (*color & 0xff); + + buffer[y * bufferBytesPerRow + x * 3 + 0] = r; + buffer[y * bufferBytesPerRow + x * 3 + 1] = g; + buffer[y * bufferBytesPerRow + x * 3 + 2] = b; + } + } + + CGContextRelease(targetContext); + + free(targetMemory); + + struct jpeg_compress_struct cinfo; + struct jpeg_error_mgr jerr; + cinfo.err = jpeg_std_error(&jerr); + jpeg_create_compress(&cinfo); + + uint8_t *outBuffer = NULL; + unsigned long outSize = 0; + jpeg_mem_dest(&cinfo, &outBuffer, &outSize); + + cinfo.image_width = (uint32_t)width; + cinfo.image_height = (uint32_t)height; + cinfo.input_components = 3; + cinfo.in_color_space = JCS_RGB; + jpeg_c_set_int_param(&cinfo, JINT_COMPRESS_PROFILE, JCP_FASTEST); + jpeg_set_defaults(&cinfo); + cinfo.arith_code = FALSE; + cinfo.dct_method = JDCT_ISLOW; + cinfo.optimize_coding = TRUE; + jpeg_set_quality(&cinfo, 78, 1); + jpeg_simple_progression(&cinfo); + jpeg_start_compress(&cinfo, 1); + + JSAMPROW rowPointer[1]; + while (cinfo.next_scanline < cinfo.image_height) { + rowPointer[0] = (JSAMPROW)(buffer + cinfo.next_scanline * bufferBytesPerRow); + jpeg_write_scanlines(&cinfo, rowPointer, 1); + } + + jpeg_finish_compress(&cinfo); + + NSData *result = [[NSData alloc] initWithBytes:outBuffer length:outSize]; + + jpeg_destroy_compress(&cinfo); + + free(buffer); + + return result; +} + +NSData * _Nullable compressMiniThumbnail(CGImageRef _Nonnull image) { + CGSize size = CGSizeMake(40.0f, 40.0f); + CGSize fittedSize = CGSizeMake(CGImageGetWidth(image), CGImageGetHeight(image)); + if (fittedSize.width > size.width) { + fittedSize = CGSizeMake(size.width, (int)((fittedSize.height * size.width / MAX(fittedSize.width, 1.0f)))); + } + if (fittedSize.height > size.height) { + fittedSize = CGSizeMake((int)((fittedSize.width * size.height / MAX(fittedSize.height, 1.0f))), size.height); + } + + int width = (int)fittedSize.width; + int height = (int)fittedSize.height; + + int targetBytesPerRow = ((4 * (int)width) + 15) & (~15); + uint8_t *targetMemory = malloc((int)(targetBytesPerRow * height)); + + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGBitmapInfo bitmapInfo = kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host; + + CGContextRef targetContext = CGBitmapContextCreate(targetMemory, width, height, 8, targetBytesPerRow, colorSpace, bitmapInfo); + + CGColorSpaceRelease(colorSpace); + + CGContextDrawImage(targetContext, CGRectMake(0, 0, width, height), image); + + int bufferBytesPerRow = ((3 * (int)width) + 15) & (~15); + uint8_t *buffer = malloc(bufferBytesPerRow * height); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + uint32_t *color = ((uint32_t *)&targetMemory[y * targetBytesPerRow + x * 4]); + + uint32_t r = ((*color >> 16) & 0xff); + uint32_t g = ((*color >> 8) & 0xff); + uint32_t b = (*color & 0xff); + + buffer[y * bufferBytesPerRow + x * 3 + 0] = r; + buffer[y * bufferBytesPerRow + x * 3 + 1] = g; + buffer[y * bufferBytesPerRow + x * 3 + 2] = b; + } + } + + CGContextRelease(targetContext); + + free(targetMemory); + + struct jpeg_compress_struct cinfo; + struct jpeg_error_mgr jerr; + cinfo.err = jpeg_std_error(&jerr); + jpeg_create_compress(&cinfo); + + uint8_t *outBuffer = NULL; + unsigned long outSize = 0; + jpeg_mem_dest(&cinfo, &outBuffer, &outSize); + + cinfo.image_width = (uint32_t)width; + cinfo.image_height = (uint32_t)height; + cinfo.input_components = 3; + cinfo.in_color_space = JCS_RGB; + jpeg_c_set_int_param(&cinfo, JINT_COMPRESS_PROFILE, JCP_FASTEST); + jpeg_set_defaults(&cinfo); + cinfo.arith_code = FALSE; + cinfo.dct_method = JDCT_ISLOW; + cinfo.optimize_coding = FALSE; + jpeg_set_quality(&cinfo, 20, 1); + jpeg_start_compress(&cinfo, 1); + + JSAMPROW rowPointer[1]; + while (cinfo.next_scanline < cinfo.image_height) { + rowPointer[0] = (JSAMPROW)(buffer + cinfo.next_scanline * bufferBytesPerRow); + jpeg_write_scanlines(&cinfo, rowPointer, 1); + } + + jpeg_finish_compress(&cinfo); + + NSMutableData *serializedData = nil; + + NSData *headerPattern = getHeaderPattern(); + NSData *footerPattern = getFooterPattern(); + if (outBuffer[164] == height && outBuffer[166] == width && headerPattern != nil && footerPattern != nil) { + outBuffer[164] = 0; + outBuffer[166] = 0; + + if (memcmp(headerPattern.bytes, outBuffer, headerPattern.length) == 0) { + if (memcmp(footerPattern.bytes, outBuffer + outSize - footerPattern.length, footerPattern.length) == 0) { + serializedData = [[NSMutableData alloc] init]; + uint8_t version = 1; + [serializedData appendBytes:&version length:1]; + uint8_t outWidth = (uint8_t)width; + uint8_t outHeight = (uint8_t)height; + [serializedData appendBytes:&outHeight length:1]; + [serializedData appendBytes:&outWidth length:1]; + unsigned long contentSize = outSize - headerPattern.length - footerPattern.length; + [serializedData appendBytes:outBuffer + headerPattern.length length:contentSize]; + } + } + } + + jpeg_destroy_compress(&cinfo); + + free(buffer); + + return serializedData; +} diff --git a/core-xprojects/Mozjpeg/Mozjpeg/build.sh b/core-xprojects/Mozjpeg/Mozjpeg/build.sh new file mode 100755 index 0000000000..d0fe866429 --- /dev/null +++ b/core-xprojects/Mozjpeg/Mozjpeg/build.sh @@ -0,0 +1,62 @@ +#! /bin/sh + +set -e +set -x + + + +SOURCE_DIR="$1" +BUILD_DIR=$(echo "$(cd "$(dirname "$3")"; pwd -P)/$(basename "$3")") +OUTPUTNAME="libjpeg.a" + +MACOS_PLATFORMDIR="$PLATFORM_DIR" +MACOS_SYSROOT=($SDK_DIR) + +cd "$BUILD_DIR" +mkdir build +cd build + +for ARCH in $ARCHS +do + +export CFLAGS="-Wall -arch $ARCH -mmacosx-version-min=10.11 -funwind-tables" + +mkdir $ARCH +cd $ARCH + +touch toolchain.cmake +echo "set(CMAKE_SYSTEM_NAME Darwin)" >> toolchain.cmake +if [ $ARCH = "arm64" ]; then +echo "set(CMAKE_SYSTEM_PROCESSOR aarch64)" >> toolchain.cmake +else +echo "set(CMAKE_SYSTEM_PROCESSOR AMD64)" >> toolchain.cmake +fi +echo "set(CMAKE_C_COMPILER $(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang)" >> toolchain.cmake + +cmake -G"Unix Makefiles" -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake -DCMAKE_OSX_SYSROOT=${MACOS_SYSROOT[0]} -DPNG_SUPPORTED=FALSE -DENABLE_SHARED=FALSE -DWITH_JPEG8=1 ../../$SOURCE_DIR +make + +cd .. + +done + + +#lipo -create -output universal_app x86_app arm_app +cd "$BUILD_DIR" +cd build + +ARCH_COUNT=( $ARCHS ) +ARCH_COUNT=${#ARCH_COUNT[@]} +if [[ $ARCH_COUNT -gt 1 ]] ; then +LIBRARIES="" +for ARCH in $ARCHS +do +LIBRARIES="$LIBRARIES ${BUILD_DIR}build/$ARCH/$OUTPUTNAME" +done +lipo -create -output $OUTPUTNAME $LIBRARIES +else +mv "${BUILD_DIR}build/$ARCHS/$OUTPUTNAME" "${BUILD_DIR}build/$OUTPUTNAME" +fi + +mv "${BUILD_DIR}build/x86_64/jconfigint.h" "${BUILD_DIR}build/jconfigint.h" +mv "${BUILD_DIR}build/x86_64/jconfig.h" "${BUILD_DIR}build/jconfig.h" diff --git a/core-xprojects/MtProtoKit/MtProtoKit/Info.plist b/core-xprojects/MtProtoKit/MtProtoKit/Info.plist new file mode 100644 index 0000000000..fb8e650960 --- /dev/null +++ b/core-xprojects/MtProtoKit/MtProtoKit/Info.plist @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2015 Telegram. All rights reserved. + NSPrincipalClass + + + diff --git a/core-xprojects/MtProtoKit/MtProtoKit/MtProtoKit.h b/core-xprojects/MtProtoKit/MtProtoKit/MtProtoKit.h new file mode 100644 index 0000000000..7dc61a88ac --- /dev/null +++ b/core-xprojects/MtProtoKit/MtProtoKit/MtProtoKit.h @@ -0,0 +1,77 @@ +// +// MtProtoKit.h +// MtProtoKit +// +// Created by Peter on 01/05/15. +// Copyright (c) 2015 Telegram. All rights reserved. +// + +#import + +//! Project version number for MtProtoKit. +FOUNDATION_EXPORT double MtProtoKitVersionNumber; + +//! Project version string for MtProtoKit. +FOUNDATION_EXPORT const unsigned char MtProtoKitVersionString[]; + +#ifndef MtProtoKitFramework +# define MtProtoKitFramework 1 +#endif + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import diff --git a/core-xprojects/MtProtoKit/MtProtoKit_Xcode.xcodeproj/project.pbxproj b/core-xprojects/MtProtoKit/MtProtoKit_Xcode.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..89705619a6 --- /dev/null +++ b/core-xprojects/MtProtoKit/MtProtoKit_Xcode.xcodeproj/project.pbxproj @@ -0,0 +1,1486 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + A790A319236B0F6C000451B5 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A790A318236B0F6C000451B5 /* Foundation.framework */; }; + A790A31B236B1199000451B5 /* EncryptionProvider.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A790A31A236B1199000451B5 /* EncryptionProvider.framework */; }; + D0179DC6250BC54600599140 /* MTPreparedMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D43250BC54600599140 /* MTPreparedMessage.m */; }; + D0179DC7250BC54600599140 /* MTGzip.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D44250BC54600599140 /* MTGzip.m */; }; + D0179DC8250BC54600599140 /* MTTime.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D45250BC54600599140 /* MTTime.m */; }; + D0179DC9250BC54600599140 /* MTBag.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D46250BC54600599140 /* MTBag.m */; }; + D0179DCA250BC54600599140 /* MTBuffer.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D47250BC54600599140 /* MTBuffer.m */; }; + D0179DCB250BC54600599140 /* MTBackupAddressSignals.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D48250BC54600599140 /* MTBackupAddressSignals.m */; }; + D0179DCD250BC54600599140 /* MTPongMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D4A250BC54600599140 /* MTPongMessage.m */; }; + D0179DCE250BC54600599140 /* MTIncomingMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D4B250BC54600599140 /* MTIncomingMessage.m */; }; + D0179DD0250BC54600599140 /* MTBindKeyMessageService.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D4D250BC54600599140 /* MTBindKeyMessageService.m */; }; + D0179DD2250BC54600599140 /* MTDatacenterAuthAction.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D4F250BC54600599140 /* MTDatacenterAuthAction.m */; }; + D0179DD3250BC54600599140 /* GCDAsyncSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D50250BC54600599140 /* GCDAsyncSocket.m */; }; + D0179DD4250BC54600599140 /* MTTransportTransaction.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D51250BC54600599140 /* MTTransportTransaction.m */; }; + D0179DD6250BC54600599140 /* MTRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D53250BC54600599140 /* MTRequest.m */; }; + D0179DD9250BC54600599140 /* MTResPqMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D56250BC54600599140 /* MTResPqMessage.m */; }; + D0179DDC250BC54600599140 /* MTQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D59250BC54600599140 /* MTQueue.m */; }; + D0179DDD250BC54600599140 /* MTOutgoingMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D5A250BC54600599140 /* MTOutgoingMessage.m */; }; + D0179DDE250BC54600599140 /* MTConnectionProbing.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D5B250BC54600599140 /* MTConnectionProbing.m */; }; + D0179DDF250BC54600599140 /* MTDestroySessionResponseMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D5C250BC54600599140 /* MTDestroySessionResponseMessage.m */; }; + D0179DE0250BC54600599140 /* MTBindingTempAuthKeyContext.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D5D250BC54600599140 /* MTBindingTempAuthKeyContext.m */; }; + D0179DE1250BC54600599140 /* MTMsgResendReqMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D5E250BC54600599140 /* MTMsgResendReqMessage.m */; }; + D0179DE2250BC54600599140 /* MTTcpTransport.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D5F250BC54600599140 /* MTTcpTransport.m */; }; + D0179DE3250BC54600599140 /* MTNetworkUsageManager.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D60250BC54600599140 /* MTNetworkUsageManager.m */; }; + D0179DE4250BC54600599140 /* MTMessageEncryptionKey.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D61250BC54600599140 /* MTMessageEncryptionKey.m */; }; + D0179DE5250BC54600599140 /* MTEncryption.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D62250BC54600599140 /* MTEncryption.m */; }; + D0179DE6250BC54600599140 /* MTHttpRequestOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D63250BC54600599140 /* MTHttpRequestOperation.m */; }; + D0179DE7250BC54600599140 /* MTTimer.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D64250BC54600599140 /* MTTimer.m */; }; + D0179DE8250BC54600599140 /* MTTcpConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D65250BC54600599140 /* MTTcpConnection.m */; }; + D0179DE9250BC54600599140 /* MTDatacenterSaltInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D66250BC54600599140 /* MTDatacenterSaltInfo.m */; }; + D0179DEC250BC54600599140 /* MTTimeFixContext.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D69250BC54600599140 /* MTTimeFixContext.m */; }; + D0179DEE250BC54600599140 /* MTMsgAllInfoMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D6B250BC54600599140 /* MTMsgAllInfoMessage.m */; }; + D0179DEF250BC54600599140 /* MTServerDhInnerDataMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D6C250BC54600599140 /* MTServerDhInnerDataMessage.m */; }; + D0179DF0250BC54600599140 /* MTDatacenterVerificationData.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D6D250BC54600599140 /* MTDatacenterVerificationData.m */; }; + D0179DF2250BC54600599140 /* MTDatacenterAuthInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D6F250BC54600599140 /* MTDatacenterAuthInfo.m */; }; + D0179DF3250BC54600599140 /* MTRequestContext.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D70250BC54600599140 /* MTRequestContext.m */; }; + D0179DF4250BC54600599140 /* MTExportedAuthorizationData.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D71250BC54600599140 /* MTExportedAuthorizationData.m */; }; + D0179DF5250BC54600599140 /* MTRequestErrorContext.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D72250BC54600599140 /* MTRequestErrorContext.m */; }; + D0179DF6250BC54600599140 /* MTSessionInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D73250BC54600599140 /* MTSessionInfo.m */; }; + D0179DF7250BC54600599140 /* MTMsgsAckMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D74250BC54600599140 /* MTMsgsAckMessage.m */; }; + D0179DFA250BC54600599140 /* MTDatacenterAddressSet.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D77250BC54600599140 /* MTDatacenterAddressSet.m */; }; + D0179DFB250BC54600599140 /* MTInternalMessageParser.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D78250BC54600599140 /* MTInternalMessageParser.m */; }; + D0179DFD250BC54600599140 /* MTInputStream.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D7A250BC54600599140 /* MTInputStream.m */; }; + D0179DFF250BC54600599140 /* MTDatacenterAddress.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D7C250BC54600599140 /* MTDatacenterAddress.m */; }; + D0179E00250BC54600599140 /* MTTransportSchemeStats.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D7D250BC54600599140 /* MTTransportSchemeStats.m */; }; + D0179E03250BC54600599140 /* MTTransportScheme.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D80250BC54600599140 /* MTTransportScheme.m */; }; + D0179E04250BC54600599140 /* MTQueueLocalObject.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D82250BC54600599140 /* MTQueueLocalObject.m */; }; + D0179E07250BC54600599140 /* MTFileBasedKeychain.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D85250BC54600599140 /* MTFileBasedKeychain.m */; }; + D0179E08250BC54600599140 /* MTAtomic.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D86250BC54600599140 /* MTAtomic.m */; }; + D0179E0A250BC54600599140 /* MTTcpConnectionBehaviour.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D88250BC54600599140 /* MTTcpConnectionBehaviour.m */; }; + D0179E0B250BC54600599140 /* MTTimeSyncMessageService.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D89250BC54600599140 /* MTTimeSyncMessageService.m */; }; + D0179E0C250BC54600599140 /* AFURLConnectionOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D8A250BC54600599140 /* AFURLConnectionOperation.m */; }; + D0179E0D250BC54600599140 /* MTFutureSaltsMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D8B250BC54600599140 /* MTFutureSaltsMessage.m */; }; + D0179E0E250BC54600599140 /* MTServerDhParamsMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D8C250BC54600599140 /* MTServerDhParamsMessage.m */; }; + D0179E10250BC54600599140 /* MTSubscriber.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D8E250BC54600599140 /* MTSubscriber.m */; }; + D0179E11250BC54600599140 /* AFHTTPRequestOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D8F250BC54600599140 /* AFHTTPRequestOperation.m */; }; + D0179E13250BC54600599140 /* PingFoundation.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D91250BC54600599140 /* PingFoundation.m */; }; + D0179E14250BC54600599140 /* MTDisposable.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D92250BC54600599140 /* MTDisposable.m */; }; + D0179E15250BC54600599140 /* MTRequestMessageService.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D93250BC54600599140 /* MTRequestMessageService.m */; }; + D0179E16250BC54600599140 /* MTDropResponseContext.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D94250BC54600599140 /* MTDropResponseContext.m */; }; + D0179E18250BC54600599140 /* MTProtoInstance.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D96250BC54600599140 /* MTProtoInstance.m */; }; + D0179E1A250BC54600599140 /* MTRpcError.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D98250BC54600599140 /* MTRpcError.m */; }; + D0179E1B250BC54600599140 /* MTResendMessageService.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D99250BC54600599140 /* MTResendMessageService.m */; }; + D0179E1C250BC54600599140 /* MTMsgDetailedInfoMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D9A250BC54600599140 /* MTMsgDetailedInfoMessage.m */; }; + D0179E1D250BC54600599140 /* MTDiscoverDatacenterAddressAction.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D9B250BC54600599140 /* MTDiscoverDatacenterAddressAction.m */; }; + D0179E1F250BC54600599140 /* MTBadMsgNotificationMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D9D250BC54600599140 /* MTBadMsgNotificationMessage.m */; }; + D0179E20250BC54600599140 /* MTAes.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D9E250BC54600599140 /* MTAes.m */; }; + D0179E21250BC54600599140 /* MTNewSessionCreatedMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179D9F250BC54600599140 /* MTNewSessionCreatedMessage.m */; }; + D0179E22250BC54600599140 /* MTDatacenterAddressListData.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DA0250BC54600599140 /* MTDatacenterAddressListData.m */; }; + D0179E23250BC54600599140 /* MTNetworkUsageCalculationInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DA1250BC54600599140 /* MTNetworkUsageCalculationInfo.m */; }; + D0179E25250BC54600599140 /* MTKeychain.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DA3250BC54600599140 /* MTKeychain.m */; }; + D0179E26250BC54600599140 /* MTApiEnvironment.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DA4250BC54600599140 /* MTApiEnvironment.m */; }; + D0179E28250BC54600599140 /* MTContext.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DA6250BC54600599140 /* MTContext.m */; }; + D0179E29250BC54600599140 /* MTPingMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DA7250BC54600599140 /* MTPingMessage.m */; }; + D0179E2B250BC54600599140 /* MTDiscoverConnectionSignals.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DA9250BC54600599140 /* MTDiscoverConnectionSignals.m */; }; + D0179E2D250BC54600599140 /* MTSetClientDhParamsResponseMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DAB250BC54600599140 /* MTSetClientDhParamsResponseMessage.m */; }; + D0179E2E250BC54600599140 /* MTDropRpcResultMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DAC250BC54600599140 /* MTDropRpcResultMessage.m */; }; + D0179E30250BC54600599140 /* MTOutputStream.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DAE250BC54600599140 /* MTOutputStream.m */; }; + D0179E31250BC54600599140 /* MTNetworkAvailability.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DAF250BC54600599140 /* MTNetworkAvailability.m */; }; + D0179E32250BC54600599140 /* MTSignal.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DB0250BC54600599140 /* MTSignal.m */; }; + D0179E35250BC54600599140 /* MTRpcResultMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DB3250BC54600599140 /* MTRpcResultMessage.m */; }; + D0179E36250BC54600599140 /* MTProtoEngine.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DB4250BC54600599140 /* MTProtoEngine.m */; }; + D0179E37250BC54600599140 /* MTProxyConnectivity.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DB5250BC54600599140 /* MTProxyConnectivity.m */; }; + D0179E38250BC54600599140 /* MTProto.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DB6250BC54600599140 /* MTProto.m */; }; + D0179E39250BC54600599140 /* MTMsgsStateInfoMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DB7250BC54600599140 /* MTMsgsStateInfoMessage.m */; }; + D0179E3A250BC54600599140 /* MTDatacenterTransferAuthAction.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DB8250BC54600599140 /* MTDatacenterTransferAuthAction.m */; }; + D0179E3B250BC54600599140 /* MTTransport.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DB9250BC54600599140 /* MTTransport.m */; }; + D0179E3C250BC54600599140 /* MTMessageTransaction.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DBA250BC54600599140 /* MTMessageTransaction.m */; }; + D0179E3D250BC54600599140 /* MTDatacenterAuthMessageService.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DBB250BC54600599140 /* MTDatacenterAuthMessageService.m */; }; + D0179E3E250BC54600599140 /* MTMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DBC250BC54600599140 /* MTMessage.m */; }; + D0179E3F250BC54600599140 /* MTDNS.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DBD250BC54600599140 /* MTDNS.m */; }; + D0179E41250BC54600599140 /* MTBufferReader.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DBF250BC54600599140 /* MTBufferReader.m */; }; + D0179E42250BC54600599140 /* MTLogging.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DC0250BC54600599140 /* MTLogging.m */; }; + D0179E43250BC54600599140 /* MTMsgsStateReqMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DC1250BC54600599140 /* MTMsgsStateReqMessage.m */; }; + D0179E45250BC54600599140 /* MTRsa.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DC3250BC54600599140 /* MTRsa.m */; }; + D0179E46250BC54600599140 /* MTMsgContainerMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179DC4250BC54600599140 /* MTMsgContainerMessage.m */; }; + D0179F0E250BCB7700599140 /* MTDisposable.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179ECF250BCB7600599140 /* MTDisposable.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F0F250BCB7700599140 /* MTRequestMessageService.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179ED0250BCB7600599140 /* MTRequestMessageService.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F10250BCB7700599140 /* MtProtoKit.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179ED1250BCB7600599140 /* MtProtoKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F11250BCB7700599140 /* AFHTTPRequestOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179ED2250BCB7600599140 /* AFHTTPRequestOperation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F12250BCB7700599140 /* MTSubscriber.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179ED3250BCB7600599140 /* MTSubscriber.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F13250BCB7700599140 /* AFURLConnectionOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179ED4250BCB7600599140 /* AFURLConnectionOperation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F14250BCB7700599140 /* MTTimeSyncMessageService.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179ED5250BCB7600599140 /* MTTimeSyncMessageService.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F15250BCB7700599140 /* MTNetworkUsageCalculationInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179ED6250BCB7600599140 /* MTNetworkUsageCalculationInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F16250BCB7700599140 /* MTDatacenterAddressListData.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179ED7250BCB7600599140 /* MTDatacenterAddressListData.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F17250BCB7700599140 /* MTResendMessageService.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179ED8250BCB7600599140 /* MTResendMessageService.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F18250BCB7700599140 /* MTProtoInstance.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179ED9250BCB7600599140 /* MTProtoInstance.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F19250BCB7700599140 /* MTRpcError.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EDA250BCB7600599140 /* MTRpcError.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F1A250BCB7700599140 /* MTDropResponseContext.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EDB250BCB7600599140 /* MTDropResponseContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F1B250BCB7700599140 /* MTSignal.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EDC250BCB7600599140 /* MTSignal.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F1C250BCB7700599140 /* MTNetworkAvailability.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EDD250BCB7600599140 /* MTNetworkAvailability.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F1D250BCB7700599140 /* MTProtoPersistenceInterface.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EDE250BCB7600599140 /* MTProtoPersistenceInterface.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F1E250BCB7700599140 /* MTOutputStream.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EDF250BCB7600599140 /* MTOutputStream.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F1F250BCB7700599140 /* MTSerialization.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EE0250BCB7600599140 /* MTSerialization.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F20250BCB7700599140 /* MTContext.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EE1250BCB7600599140 /* MTContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F21250BCB7700599140 /* MTApiEnvironment.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EE2250BCB7600599140 /* MTApiEnvironment.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F22250BCB7700599140 /* MTKeychain.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EE3250BCB7600599140 /* MTKeychain.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F23250BCB7700599140 /* MTLogging.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EE4250BCB7600599140 /* MTLogging.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F24250BCB7700599140 /* MTMessageTransaction.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EE5250BCB7600599140 /* MTMessageTransaction.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F25250BCB7700599140 /* MTDatacenterAuthMessageService.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EE6250BCB7600599140 /* MTDatacenterAuthMessageService.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F26250BCB7700599140 /* MTTransport.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EE7250BCB7600599140 /* MTTransport.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F27250BCB7700599140 /* MTDatacenterTransferAuthAction.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EE8250BCB7600599140 /* MTDatacenterTransferAuthAction.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F28250BCB7700599140 /* MTProto.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EE9250BCB7600599140 /* MTProto.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F29250BCB7700599140 /* MTProxyConnectivity.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EEA250BCB7600599140 /* MTProxyConnectivity.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F2A250BCB7700599140 /* MTProtoEngine.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EEB250BCB7600599140 /* MTProtoEngine.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F2B250BCB7700599140 /* MTDatacenterAuthAction.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EEC250BCB7600599140 /* MTDatacenterAuthAction.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F2C250BCB7700599140 /* MTBindKeyMessageService.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EED250BCB7600599140 /* MTBindKeyMessageService.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F2D250BCB7700599140 /* MTIncomingMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EEE250BCB7600599140 /* MTIncomingMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F2E250BCB7700599140 /* MTBackupAddressSignals.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EEF250BCB7600599140 /* MTBackupAddressSignals.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F2F250BCB7700599140 /* MTTime.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EF0250BCB7600599140 /* MTTime.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F30250BCB7700599140 /* MTBag.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EF1250BCB7600599140 /* MTBag.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F31250BCB7700599140 /* MTMessageService.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EF2250BCB7600599140 /* MTMessageService.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F32250BCB7700599140 /* MTGzip.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EF3250BCB7600599140 /* MTGzip.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F33250BCB7700599140 /* MTPreparedMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EF4250BCB7600599140 /* MTPreparedMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F34250BCB7700599140 /* MTOutgoingMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EF5250BCB7600599140 /* MTOutgoingMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F35250BCB7700599140 /* MTQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EF6250BCB7600599140 /* MTQueue.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F36250BCB7700599140 /* MTRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EF7250BCB7600599140 /* MTRequest.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F37250BCB7700599140 /* MTTransportTransaction.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EF8250BCB7600599140 /* MTTransportTransaction.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F38250BCB7700599140 /* MTSessionInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EF9250BCB7600599140 /* MTSessionInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F39250BCB7700599140 /* MTRequestErrorContext.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EFA250BCB7600599140 /* MTRequestErrorContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F3A250BCB7700599140 /* MTExportedAuthorizationData.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EFB250BCB7600599140 /* MTExportedAuthorizationData.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F3B250BCB7700599140 /* MTDatacenterVerificationData.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EFC250BCB7600599140 /* MTDatacenterVerificationData.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F3C250BCB7700599140 /* MTDatacenterAuthInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EFD250BCB7600599140 /* MTDatacenterAuthInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F3D250BCB7700599140 /* MTRequestContext.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EFE250BCB7600599140 /* MTRequestContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F3E250BCB7700599140 /* MTInternalId.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179EFF250BCB7600599140 /* MTInternalId.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F3F250BCB7700599140 /* MTDatacenterSaltInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179F00250BCB7600599140 /* MTDatacenterSaltInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F40250BCB7700599140 /* MTTimeFixContext.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179F01250BCB7600599140 /* MTTimeFixContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F41250BCB7700599140 /* MTTimer.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179F02250BCB7600599140 /* MTTimer.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F42250BCB7700599140 /* MTHttpRequestOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179F03250BCB7600599140 /* MTHttpRequestOperation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F43250BCB7700599140 /* MTMessageEncryptionKey.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179F04250BCB7600599140 /* MTMessageEncryptionKey.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F44250BCB7700599140 /* MTEncryption.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179F05250BCB7600599140 /* MTEncryption.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F45250BCB7700599140 /* MTNetworkUsageManager.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179F06250BCB7600599140 /* MTNetworkUsageManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F46250BCB7700599140 /* MTTcpTransport.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179F07250BCB7600599140 /* MTTcpTransport.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F47250BCB7700599140 /* MTAtomic.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179F08250BCB7600599140 /* MTAtomic.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F48250BCB7700599140 /* MTFileBasedKeychain.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179F09250BCB7600599140 /* MTFileBasedKeychain.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F49250BCB7700599140 /* MTTransportScheme.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179F0A250BCB7600599140 /* MTTransportScheme.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F4A250BCB7700599140 /* MTDatacenterAddress.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179F0B250BCB7600599140 /* MTDatacenterAddress.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F4B250BCB7700599140 /* MTInputStream.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179F0C250BCB7600599140 /* MTInputStream.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0179F4C250BCB7700599140 /* MTDatacenterAddressSet.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179F0D250BCB7600599140 /* MTDatacenterAddressSet.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D038789E2332500A00DB441C /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D038789D2332500A00DB441C /* libc++.tbd */; }; + D03878A02332503300DB441C /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D038789F2332503300DB441C /* Security.framework */; }; + D03878A22332503E00DB441C /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D03878A12332503E00DB441C /* SystemConfiguration.framework */; }; + D03878A42332504600DB441C /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D03878A32332504600DB441C /* CFNetwork.framework */; }; + D0B4187B1D7E04CF004562A4 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D0B4187A1D7E04CF004562A4 /* libz.tbd */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A790A318236B0F6C000451B5 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; + A790A31A236B1199000451B5 /* EncryptionProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = EncryptionProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7918DD6240CEF8A002011CA /* MurMurHash32.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MurMurHash32.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7D281E5236C26400000A9BF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D00354711C173CD0006610DA /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; + D00354731C173CD9006610DA /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; + D0179D42250BC54600599140 /* PingFoundation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PingFoundation.h; sourceTree = ""; }; + D0179D43250BC54600599140 /* MTPreparedMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTPreparedMessage.m; sourceTree = ""; }; + D0179D44250BC54600599140 /* MTGzip.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTGzip.m; sourceTree = ""; }; + D0179D45250BC54600599140 /* MTTime.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTTime.m; sourceTree = ""; }; + D0179D46250BC54600599140 /* MTBag.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTBag.m; sourceTree = ""; }; + D0179D47250BC54600599140 /* MTBuffer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTBuffer.m; sourceTree = ""; }; + D0179D48250BC54600599140 /* MTBackupAddressSignals.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTBackupAddressSignals.m; sourceTree = ""; }; + D0179D49250BC54600599140 /* MTServerDhParamsMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTServerDhParamsMessage.h; sourceTree = ""; }; + D0179D4A250BC54600599140 /* MTPongMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTPongMessage.m; sourceTree = ""; }; + D0179D4B250BC54600599140 /* MTIncomingMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTIncomingMessage.m; sourceTree = ""; }; + D0179D4C250BC54600599140 /* MTFutureSaltsMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTFutureSaltsMessage.h; sourceTree = ""; }; + D0179D4D250BC54600599140 /* MTBindKeyMessageService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTBindKeyMessageService.m; sourceTree = ""; }; + D0179D4E250BC54600599140 /* MTTcpConnectionBehaviour.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTTcpConnectionBehaviour.h; sourceTree = ""; }; + D0179D4F250BC54600599140 /* MTDatacenterAuthAction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTDatacenterAuthAction.m; sourceTree = ""; }; + D0179D50250BC54600599140 /* GCDAsyncSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDAsyncSocket.m; sourceTree = ""; }; + D0179D51250BC54600599140 /* MTTransportTransaction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTTransportTransaction.m; sourceTree = ""; }; + D0179D52250BC54600599140 /* MTNewSessionCreatedMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTNewSessionCreatedMessage.h; sourceTree = ""; }; + D0179D53250BC54600599140 /* MTRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRequest.m; sourceTree = ""; }; + D0179D54250BC54600599140 /* MTBadMsgNotificationMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTBadMsgNotificationMessage.h; sourceTree = ""; }; + D0179D55250BC54600599140 /* MTAes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTAes.h; sourceTree = ""; }; + D0179D56250BC54600599140 /* MTResPqMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTResPqMessage.m; sourceTree = ""; }; + D0179D57250BC54600599140 /* MTDiscoverDatacenterAddressAction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTDiscoverDatacenterAddressAction.h; sourceTree = ""; }; + D0179D58250BC54600599140 /* MTMsgDetailedInfoMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTMsgDetailedInfoMessage.h; sourceTree = ""; }; + D0179D59250BC54600599140 /* MTQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTQueue.m; sourceTree = ""; }; + D0179D5A250BC54600599140 /* MTOutgoingMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTOutgoingMessage.m; sourceTree = ""; }; + D0179D5B250BC54600599140 /* MTConnectionProbing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTConnectionProbing.m; sourceTree = ""; }; + D0179D5C250BC54600599140 /* MTDestroySessionResponseMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTDestroySessionResponseMessage.m; sourceTree = ""; }; + D0179D5D250BC54600599140 /* MTBindingTempAuthKeyContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTBindingTempAuthKeyContext.m; sourceTree = ""; }; + D0179D5E250BC54600599140 /* MTMsgResendReqMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTMsgResendReqMessage.m; sourceTree = ""; }; + D0179D5F250BC54600599140 /* MTTcpTransport.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTTcpTransport.m; sourceTree = ""; }; + D0179D60250BC54600599140 /* MTNetworkUsageManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTNetworkUsageManager.m; sourceTree = ""; }; + D0179D61250BC54600599140 /* MTMessageEncryptionKey.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTMessageEncryptionKey.m; sourceTree = ""; }; + D0179D62250BC54600599140 /* MTEncryption.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTEncryption.m; sourceTree = ""; }; + D0179D63250BC54600599140 /* MTHttpRequestOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTHttpRequestOperation.m; sourceTree = ""; }; + D0179D64250BC54600599140 /* MTTimer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTTimer.m; sourceTree = ""; }; + D0179D65250BC54600599140 /* MTTcpConnection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTTcpConnection.m; sourceTree = ""; }; + D0179D66250BC54600599140 /* MTDatacenterSaltInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTDatacenterSaltInfo.m; sourceTree = ""; }; + D0179D67250BC54600599140 /* MTSetClientDhParamsResponseMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTSetClientDhParamsResponseMessage.h; sourceTree = ""; }; + D0179D68250BC54600599140 /* MTDropRpcResultMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTDropRpcResultMessage.h; sourceTree = ""; }; + D0179D69250BC54600599140 /* MTTimeFixContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTTimeFixContext.m; sourceTree = ""; }; + D0179D6A250BC54600599140 /* MTDiscoverConnectionSignals.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTDiscoverConnectionSignals.h; sourceTree = ""; }; + D0179D6B250BC54600599140 /* MTMsgAllInfoMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTMsgAllInfoMessage.m; sourceTree = ""; }; + D0179D6C250BC54600599140 /* MTServerDhInnerDataMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTServerDhInnerDataMessage.m; sourceTree = ""; }; + D0179D6D250BC54600599140 /* MTDatacenterVerificationData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTDatacenterVerificationData.m; sourceTree = ""; }; + D0179D6E250BC54600599140 /* MTPingMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTPingMessage.h; sourceTree = ""; }; + D0179D6F250BC54600599140 /* MTDatacenterAuthInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTDatacenterAuthInfo.m; sourceTree = ""; }; + D0179D70250BC54600599140 /* MTRequestContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRequestContext.m; sourceTree = ""; }; + D0179D71250BC54600599140 /* MTExportedAuthorizationData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTExportedAuthorizationData.m; sourceTree = ""; }; + D0179D72250BC54600599140 /* MTRequestErrorContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRequestErrorContext.m; sourceTree = ""; }; + D0179D73250BC54600599140 /* MTSessionInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTSessionInfo.m; sourceTree = ""; }; + D0179D74250BC54600599140 /* MTMsgsAckMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTMsgsAckMessage.m; sourceTree = ""; }; + D0179D75250BC54600599140 /* MTRsa.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRsa.h; sourceTree = ""; }; + D0179D76250BC54600599140 /* MTMsgContainerMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTMsgContainerMessage.h; sourceTree = ""; }; + D0179D77250BC54600599140 /* MTDatacenterAddressSet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTDatacenterAddressSet.m; sourceTree = ""; }; + D0179D78250BC54600599140 /* MTInternalMessageParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTInternalMessageParser.m; sourceTree = ""; }; + D0179D79250BC54600599140 /* MTMsgsStateReqMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTMsgsStateReqMessage.h; sourceTree = ""; }; + D0179D7A250BC54600599140 /* MTInputStream.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTInputStream.m; sourceTree = ""; }; + D0179D7B250BC54600599140 /* MTBufferReader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTBufferReader.h; sourceTree = ""; }; + D0179D7C250BC54600599140 /* MTDatacenterAddress.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTDatacenterAddress.m; sourceTree = ""; }; + D0179D7D250BC54600599140 /* MTTransportSchemeStats.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTTransportSchemeStats.m; sourceTree = ""; }; + D0179D7E250BC54600599140 /* MTDNS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTDNS.h; sourceTree = ""; }; + D0179D7F250BC54600599140 /* MTMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTMessage.h; sourceTree = ""; }; + D0179D80250BC54600599140 /* MTTransportScheme.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTTransportScheme.m; sourceTree = ""; }; + D0179D82250BC54600599140 /* MTQueueLocalObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTQueueLocalObject.m; sourceTree = ""; }; + D0179D83250BC54600599140 /* MTQueueLocalObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTQueueLocalObject.h; sourceTree = ""; }; + D0179D84250BC54600599140 /* MTMsgsStateInfoMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTMsgsStateInfoMessage.h; sourceTree = ""; }; + D0179D85250BC54600599140 /* MTFileBasedKeychain.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTFileBasedKeychain.m; sourceTree = ""; }; + D0179D86250BC54600599140 /* MTAtomic.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTAtomic.m; sourceTree = ""; }; + D0179D87250BC54600599140 /* MTRpcResultMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRpcResultMessage.h; sourceTree = ""; }; + D0179D88250BC54600599140 /* MTTcpConnectionBehaviour.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTTcpConnectionBehaviour.m; sourceTree = ""; }; + D0179D89250BC54600599140 /* MTTimeSyncMessageService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTTimeSyncMessageService.m; sourceTree = ""; }; + D0179D8A250BC54600599140 /* AFURLConnectionOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AFURLConnectionOperation.m; sourceTree = ""; }; + D0179D8B250BC54600599140 /* MTFutureSaltsMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTFutureSaltsMessage.m; sourceTree = ""; }; + D0179D8C250BC54600599140 /* MTServerDhParamsMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTServerDhParamsMessage.m; sourceTree = ""; }; + D0179D8D250BC54600599140 /* MTPongMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTPongMessage.h; sourceTree = ""; }; + D0179D8E250BC54600599140 /* MTSubscriber.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTSubscriber.m; sourceTree = ""; }; + D0179D8F250BC54600599140 /* AFHTTPRequestOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AFHTTPRequestOperation.m; sourceTree = ""; }; + D0179D90250BC54600599140 /* MTBuffer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTBuffer.h; sourceTree = ""; }; + D0179D91250BC54600599140 /* PingFoundation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PingFoundation.m; sourceTree = ""; }; + D0179D92250BC54600599140 /* MTDisposable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTDisposable.m; sourceTree = ""; }; + D0179D93250BC54600599140 /* MTRequestMessageService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRequestMessageService.m; sourceTree = ""; }; + D0179D94250BC54600599140 /* MTDropResponseContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTDropResponseContext.m; sourceTree = ""; }; + D0179D95250BC54600599140 /* MTDestroySessionResponseMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTDestroySessionResponseMessage.h; sourceTree = ""; }; + D0179D96250BC54600599140 /* MTProtoInstance.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTProtoInstance.m; sourceTree = ""; }; + D0179D97250BC54600599140 /* MTConnectionProbing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTConnectionProbing.h; sourceTree = ""; }; + D0179D98250BC54600599140 /* MTRpcError.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRpcError.m; sourceTree = ""; }; + D0179D99250BC54600599140 /* MTResendMessageService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTResendMessageService.m; sourceTree = ""; }; + D0179D9A250BC54600599140 /* MTMsgDetailedInfoMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTMsgDetailedInfoMessage.m; sourceTree = ""; }; + D0179D9B250BC54600599140 /* MTDiscoverDatacenterAddressAction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTDiscoverDatacenterAddressAction.m; sourceTree = ""; }; + D0179D9C250BC54600599140 /* MTResPqMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTResPqMessage.h; sourceTree = ""; }; + D0179D9D250BC54600599140 /* MTBadMsgNotificationMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTBadMsgNotificationMessage.m; sourceTree = ""; }; + D0179D9E250BC54600599140 /* MTAes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTAes.m; sourceTree = ""; }; + D0179D9F250BC54600599140 /* MTNewSessionCreatedMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTNewSessionCreatedMessage.m; sourceTree = ""; }; + D0179DA0250BC54600599140 /* MTDatacenterAddressListData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTDatacenterAddressListData.m; sourceTree = ""; }; + D0179DA1250BC54600599140 /* MTNetworkUsageCalculationInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTNetworkUsageCalculationInfo.m; sourceTree = ""; }; + D0179DA2250BC54600599140 /* GCDAsyncSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDAsyncSocket.h; sourceTree = ""; }; + D0179DA3250BC54600599140 /* MTKeychain.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTKeychain.m; sourceTree = ""; }; + D0179DA4250BC54600599140 /* MTApiEnvironment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTApiEnvironment.m; sourceTree = ""; }; + D0179DA5250BC54600599140 /* MTMsgsAckMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTMsgsAckMessage.h; sourceTree = ""; }; + D0179DA6250BC54600599140 /* MTContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTContext.m; sourceTree = ""; }; + D0179DA7250BC54600599140 /* MTPingMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTPingMessage.m; sourceTree = ""; }; + D0179DA8250BC54600599140 /* MTServerDhInnerDataMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTServerDhInnerDataMessage.h; sourceTree = ""; }; + D0179DA9250BC54600599140 /* MTDiscoverConnectionSignals.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTDiscoverConnectionSignals.m; sourceTree = ""; }; + D0179DAA250BC54600599140 /* MTMsgAllInfoMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTMsgAllInfoMessage.h; sourceTree = ""; }; + D0179DAB250BC54600599140 /* MTSetClientDhParamsResponseMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTSetClientDhParamsResponseMessage.m; sourceTree = ""; }; + D0179DAC250BC54600599140 /* MTDropRpcResultMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTDropRpcResultMessage.m; sourceTree = ""; }; + D0179DAD250BC54600599140 /* MTTcpConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTTcpConnection.h; sourceTree = ""; }; + D0179DAE250BC54600599140 /* MTOutputStream.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTOutputStream.m; sourceTree = ""; }; + D0179DAF250BC54600599140 /* MTNetworkAvailability.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTNetworkAvailability.m; sourceTree = ""; }; + D0179DB0250BC54600599140 /* MTSignal.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTSignal.m; sourceTree = ""; }; + D0179DB1250BC54600599140 /* MTBindingTempAuthKeyContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTBindingTempAuthKeyContext.h; sourceTree = ""; }; + D0179DB2250BC54600599140 /* MTMsgResendReqMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTMsgResendReqMessage.h; sourceTree = ""; }; + D0179DB3250BC54600599140 /* MTRpcResultMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRpcResultMessage.m; sourceTree = ""; }; + D0179DB4250BC54600599140 /* MTProtoEngine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTProtoEngine.m; sourceTree = ""; }; + D0179DB5250BC54600599140 /* MTProxyConnectivity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTProxyConnectivity.m; sourceTree = ""; }; + D0179DB6250BC54600599140 /* MTProto.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTProto.m; sourceTree = ""; }; + D0179DB7250BC54600599140 /* MTMsgsStateInfoMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTMsgsStateInfoMessage.m; sourceTree = ""; }; + D0179DB8250BC54600599140 /* MTDatacenterTransferAuthAction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTDatacenterTransferAuthAction.m; sourceTree = ""; }; + D0179DB9250BC54600599140 /* MTTransport.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTTransport.m; sourceTree = ""; }; + D0179DBA250BC54600599140 /* MTMessageTransaction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTMessageTransaction.m; sourceTree = ""; }; + D0179DBB250BC54600599140 /* MTDatacenterAuthMessageService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTDatacenterAuthMessageService.m; sourceTree = ""; }; + D0179DBC250BC54600599140 /* MTMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTMessage.m; sourceTree = ""; }; + D0179DBD250BC54600599140 /* MTDNS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTDNS.m; sourceTree = ""; }; + D0179DBE250BC54600599140 /* MTTransportSchemeStats.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTTransportSchemeStats.h; sourceTree = ""; }; + D0179DBF250BC54600599140 /* MTBufferReader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTBufferReader.m; sourceTree = ""; }; + D0179DC0250BC54600599140 /* MTLogging.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTLogging.m; sourceTree = ""; }; + D0179DC1250BC54600599140 /* MTMsgsStateReqMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTMsgsStateReqMessage.m; sourceTree = ""; }; + D0179DC2250BC54600599140 /* MTInternalMessageParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTInternalMessageParser.h; sourceTree = ""; }; + D0179DC3250BC54600599140 /* MTRsa.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRsa.m; sourceTree = ""; }; + D0179DC4250BC54600599140 /* MTMsgContainerMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTMsgContainerMessage.m; sourceTree = ""; }; + D0179ECF250BCB7600599140 /* MTDisposable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTDisposable.h; sourceTree = ""; }; + D0179ED0250BCB7600599140 /* MTRequestMessageService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRequestMessageService.h; sourceTree = ""; }; + D0179ED1250BCB7600599140 /* MtProtoKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MtProtoKit.h; sourceTree = ""; }; + D0179ED2250BCB7600599140 /* AFHTTPRequestOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AFHTTPRequestOperation.h; sourceTree = ""; }; + D0179ED3250BCB7600599140 /* MTSubscriber.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTSubscriber.h; sourceTree = ""; }; + D0179ED4250BCB7600599140 /* AFURLConnectionOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AFURLConnectionOperation.h; sourceTree = ""; }; + D0179ED5250BCB7600599140 /* MTTimeSyncMessageService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTTimeSyncMessageService.h; sourceTree = ""; }; + D0179ED6250BCB7600599140 /* MTNetworkUsageCalculationInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTNetworkUsageCalculationInfo.h; sourceTree = ""; }; + D0179ED7250BCB7600599140 /* MTDatacenterAddressListData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTDatacenterAddressListData.h; sourceTree = ""; }; + D0179ED8250BCB7600599140 /* MTResendMessageService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTResendMessageService.h; sourceTree = ""; }; + D0179ED9250BCB7600599140 /* MTProtoInstance.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTProtoInstance.h; sourceTree = ""; }; + D0179EDA250BCB7600599140 /* MTRpcError.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRpcError.h; sourceTree = ""; }; + D0179EDB250BCB7600599140 /* MTDropResponseContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTDropResponseContext.h; sourceTree = ""; }; + D0179EDC250BCB7600599140 /* MTSignal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTSignal.h; sourceTree = ""; }; + D0179EDD250BCB7600599140 /* MTNetworkAvailability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTNetworkAvailability.h; sourceTree = ""; }; + D0179EDE250BCB7600599140 /* MTProtoPersistenceInterface.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTProtoPersistenceInterface.h; sourceTree = ""; }; + D0179EDF250BCB7600599140 /* MTOutputStream.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTOutputStream.h; sourceTree = ""; }; + D0179EE0250BCB7600599140 /* MTSerialization.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTSerialization.h; sourceTree = ""; }; + D0179EE1250BCB7600599140 /* MTContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTContext.h; sourceTree = ""; }; + D0179EE2250BCB7600599140 /* MTApiEnvironment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTApiEnvironment.h; sourceTree = ""; }; + D0179EE3250BCB7600599140 /* MTKeychain.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTKeychain.h; sourceTree = ""; }; + D0179EE4250BCB7600599140 /* MTLogging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTLogging.h; sourceTree = ""; }; + D0179EE5250BCB7600599140 /* MTMessageTransaction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTMessageTransaction.h; sourceTree = ""; }; + D0179EE6250BCB7600599140 /* MTDatacenterAuthMessageService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTDatacenterAuthMessageService.h; sourceTree = ""; }; + D0179EE7250BCB7600599140 /* MTTransport.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTTransport.h; sourceTree = ""; }; + D0179EE8250BCB7600599140 /* MTDatacenterTransferAuthAction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTDatacenterTransferAuthAction.h; sourceTree = ""; }; + D0179EE9250BCB7600599140 /* MTProto.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTProto.h; sourceTree = ""; }; + D0179EEA250BCB7600599140 /* MTProxyConnectivity.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTProxyConnectivity.h; sourceTree = ""; }; + D0179EEB250BCB7600599140 /* MTProtoEngine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTProtoEngine.h; sourceTree = ""; }; + D0179EEC250BCB7600599140 /* MTDatacenterAuthAction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTDatacenterAuthAction.h; sourceTree = ""; }; + D0179EED250BCB7600599140 /* MTBindKeyMessageService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTBindKeyMessageService.h; sourceTree = ""; }; + D0179EEE250BCB7600599140 /* MTIncomingMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTIncomingMessage.h; sourceTree = ""; }; + D0179EEF250BCB7600599140 /* MTBackupAddressSignals.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTBackupAddressSignals.h; sourceTree = ""; }; + D0179EF0250BCB7600599140 /* MTTime.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTTime.h; sourceTree = ""; }; + D0179EF1250BCB7600599140 /* MTBag.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTBag.h; sourceTree = ""; }; + D0179EF2250BCB7600599140 /* MTMessageService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTMessageService.h; sourceTree = ""; }; + D0179EF3250BCB7600599140 /* MTGzip.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTGzip.h; sourceTree = ""; }; + D0179EF4250BCB7600599140 /* MTPreparedMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTPreparedMessage.h; sourceTree = ""; }; + D0179EF5250BCB7600599140 /* MTOutgoingMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTOutgoingMessage.h; sourceTree = ""; }; + D0179EF6250BCB7600599140 /* MTQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTQueue.h; sourceTree = ""; }; + D0179EF7250BCB7600599140 /* MTRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRequest.h; sourceTree = ""; }; + D0179EF8250BCB7600599140 /* MTTransportTransaction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTTransportTransaction.h; sourceTree = ""; }; + D0179EF9250BCB7600599140 /* MTSessionInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTSessionInfo.h; sourceTree = ""; }; + D0179EFA250BCB7600599140 /* MTRequestErrorContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRequestErrorContext.h; sourceTree = ""; }; + D0179EFB250BCB7600599140 /* MTExportedAuthorizationData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTExportedAuthorizationData.h; sourceTree = ""; }; + D0179EFC250BCB7600599140 /* MTDatacenterVerificationData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTDatacenterVerificationData.h; sourceTree = ""; }; + D0179EFD250BCB7600599140 /* MTDatacenterAuthInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTDatacenterAuthInfo.h; sourceTree = ""; }; + D0179EFE250BCB7600599140 /* MTRequestContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRequestContext.h; sourceTree = ""; }; + D0179EFF250BCB7600599140 /* MTInternalId.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTInternalId.h; sourceTree = ""; }; + D0179F00250BCB7600599140 /* MTDatacenterSaltInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTDatacenterSaltInfo.h; sourceTree = ""; }; + D0179F01250BCB7600599140 /* MTTimeFixContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTTimeFixContext.h; sourceTree = ""; }; + D0179F02250BCB7600599140 /* MTTimer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTTimer.h; sourceTree = ""; }; + D0179F03250BCB7600599140 /* MTHttpRequestOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTHttpRequestOperation.h; sourceTree = ""; }; + D0179F04250BCB7600599140 /* MTMessageEncryptionKey.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTMessageEncryptionKey.h; sourceTree = ""; }; + D0179F05250BCB7600599140 /* MTEncryption.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTEncryption.h; sourceTree = ""; }; + D0179F06250BCB7600599140 /* MTNetworkUsageManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTNetworkUsageManager.h; sourceTree = ""; }; + D0179F07250BCB7600599140 /* MTTcpTransport.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTTcpTransport.h; sourceTree = ""; }; + D0179F08250BCB7600599140 /* MTAtomic.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTAtomic.h; sourceTree = ""; }; + D0179F09250BCB7600599140 /* MTFileBasedKeychain.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTFileBasedKeychain.h; sourceTree = ""; }; + D0179F0A250BCB7600599140 /* MTTransportScheme.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTTransportScheme.h; sourceTree = ""; }; + D0179F0B250BCB7600599140 /* MTDatacenterAddress.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTDatacenterAddress.h; sourceTree = ""; }; + D0179F0C250BCB7600599140 /* MTInputStream.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTInputStream.h; sourceTree = ""; }; + D0179F0D250BCB7600599140 /* MTDatacenterAddressSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTDatacenterAddressSet.h; sourceTree = ""; }; + D038789D2332500A00DB441C /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/lib/libc++.tbd"; sourceTree = DEVELOPER_DIR; }; + D038789F2332503300DB441C /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/System/Library/Frameworks/Security.framework; sourceTree = DEVELOPER_DIR; }; + D03878A12332503E00DB441C /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/System/Library/Frameworks/SystemConfiguration.framework; sourceTree = DEVELOPER_DIR; }; + D03878A32332504600DB441C /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/System/Library/Frameworks/CFNetwork.framework; sourceTree = DEVELOPER_DIR; }; + D05A831718AFB3F9007F1076 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + D063A2F918B14AB500C65116 /* libcrypto.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libcrypto.dylib; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.9.sdk/usr/lib/libcrypto.dylib; sourceTree = DEVELOPER_DIR; }; + D079AB971AF39B8000076F59 /* MtProtoKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MtProtoKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0B4187A1D7E04CF004562A4 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.12.sdk/usr/lib/libz.tbd; sourceTree = DEVELOPER_DIR; }; + D0CAF2EB1D75F4520011F558 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + D0CAF2EE1D75F4E20011F558 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + D0CAF2F01D75F4EA0011F558 /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; }; + D0CD990F1D75C16100F41187 /* libcrypto.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libcrypto.a; path = openssl/iOS/libcrypto.a; sourceTree = ""; }; + D0D1A0711ADDE2FC007D9ED6 /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = usr/lib/libz.dylib; sourceTree = SDKROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D079AB931AF39B8000076F59 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A790A31B236B1199000451B5 /* EncryptionProvider.framework in Frameworks */, + A790A319236B0F6C000451B5 /* Foundation.framework in Frameworks */, + D03878A42332504600DB441C /* CFNetwork.framework in Frameworks */, + D03878A22332503E00DB441C /* SystemConfiguration.framework in Frameworks */, + D03878A02332503300DB441C /* Security.framework in Frameworks */, + D038789E2332500A00DB441C /* libc++.tbd in Frameworks */, + D0B4187B1D7E04CF004562A4 /* libz.tbd in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + D0179D41250BC54600599140 /* Sources */ = { + isa = PBXGroup; + children = ( + D0179D42250BC54600599140 /* PingFoundation.h */, + D0179D43250BC54600599140 /* MTPreparedMessage.m */, + D0179D44250BC54600599140 /* MTGzip.m */, + D0179D45250BC54600599140 /* MTTime.m */, + D0179D46250BC54600599140 /* MTBag.m */, + D0179D47250BC54600599140 /* MTBuffer.m */, + D0179D48250BC54600599140 /* MTBackupAddressSignals.m */, + D0179D49250BC54600599140 /* MTServerDhParamsMessage.h */, + D0179D4A250BC54600599140 /* MTPongMessage.m */, + D0179D4B250BC54600599140 /* MTIncomingMessage.m */, + D0179D4C250BC54600599140 /* MTFutureSaltsMessage.h */, + D0179D4D250BC54600599140 /* MTBindKeyMessageService.m */, + D0179D4E250BC54600599140 /* MTTcpConnectionBehaviour.h */, + D0179D4F250BC54600599140 /* MTDatacenterAuthAction.m */, + D0179D50250BC54600599140 /* GCDAsyncSocket.m */, + D0179D51250BC54600599140 /* MTTransportTransaction.m */, + D0179D52250BC54600599140 /* MTNewSessionCreatedMessage.h */, + D0179D53250BC54600599140 /* MTRequest.m */, + D0179D54250BC54600599140 /* MTBadMsgNotificationMessage.h */, + D0179D55250BC54600599140 /* MTAes.h */, + D0179D56250BC54600599140 /* MTResPqMessage.m */, + D0179D57250BC54600599140 /* MTDiscoverDatacenterAddressAction.h */, + D0179D58250BC54600599140 /* MTMsgDetailedInfoMessage.h */, + D0179D59250BC54600599140 /* MTQueue.m */, + D0179D5A250BC54600599140 /* MTOutgoingMessage.m */, + D0179D5B250BC54600599140 /* MTConnectionProbing.m */, + D0179D5C250BC54600599140 /* MTDestroySessionResponseMessage.m */, + D0179D5D250BC54600599140 /* MTBindingTempAuthKeyContext.m */, + D0179D5E250BC54600599140 /* MTMsgResendReqMessage.m */, + D0179D5F250BC54600599140 /* MTTcpTransport.m */, + D0179D60250BC54600599140 /* MTNetworkUsageManager.m */, + D0179D61250BC54600599140 /* MTMessageEncryptionKey.m */, + D0179D62250BC54600599140 /* MTEncryption.m */, + D0179D63250BC54600599140 /* MTHttpRequestOperation.m */, + D0179D64250BC54600599140 /* MTTimer.m */, + D0179D65250BC54600599140 /* MTTcpConnection.m */, + D0179D66250BC54600599140 /* MTDatacenterSaltInfo.m */, + D0179D67250BC54600599140 /* MTSetClientDhParamsResponseMessage.h */, + D0179D68250BC54600599140 /* MTDropRpcResultMessage.h */, + D0179D69250BC54600599140 /* MTTimeFixContext.m */, + D0179D6A250BC54600599140 /* MTDiscoverConnectionSignals.h */, + D0179D6B250BC54600599140 /* MTMsgAllInfoMessage.m */, + D0179D6C250BC54600599140 /* MTServerDhInnerDataMessage.m */, + D0179D6D250BC54600599140 /* MTDatacenterVerificationData.m */, + D0179D6E250BC54600599140 /* MTPingMessage.h */, + D0179D6F250BC54600599140 /* MTDatacenterAuthInfo.m */, + D0179D70250BC54600599140 /* MTRequestContext.m */, + D0179D71250BC54600599140 /* MTExportedAuthorizationData.m */, + D0179D72250BC54600599140 /* MTRequestErrorContext.m */, + D0179D73250BC54600599140 /* MTSessionInfo.m */, + D0179D74250BC54600599140 /* MTMsgsAckMessage.m */, + D0179D75250BC54600599140 /* MTRsa.h */, + D0179D76250BC54600599140 /* MTMsgContainerMessage.h */, + D0179D77250BC54600599140 /* MTDatacenterAddressSet.m */, + D0179D78250BC54600599140 /* MTInternalMessageParser.m */, + D0179D79250BC54600599140 /* MTMsgsStateReqMessage.h */, + D0179D7A250BC54600599140 /* MTInputStream.m */, + D0179D7B250BC54600599140 /* MTBufferReader.h */, + D0179D7C250BC54600599140 /* MTDatacenterAddress.m */, + D0179D7D250BC54600599140 /* MTTransportSchemeStats.m */, + D0179D7E250BC54600599140 /* MTDNS.h */, + D0179D7F250BC54600599140 /* MTMessage.h */, + D0179D80250BC54600599140 /* MTTransportScheme.m */, + D0179D81250BC54600599140 /* Utils */, + D0179D84250BC54600599140 /* MTMsgsStateInfoMessage.h */, + D0179D85250BC54600599140 /* MTFileBasedKeychain.m */, + D0179D86250BC54600599140 /* MTAtomic.m */, + D0179D87250BC54600599140 /* MTRpcResultMessage.h */, + D0179D88250BC54600599140 /* MTTcpConnectionBehaviour.m */, + D0179D89250BC54600599140 /* MTTimeSyncMessageService.m */, + D0179D8A250BC54600599140 /* AFURLConnectionOperation.m */, + D0179D8B250BC54600599140 /* MTFutureSaltsMessage.m */, + D0179D8C250BC54600599140 /* MTServerDhParamsMessage.m */, + D0179D8D250BC54600599140 /* MTPongMessage.h */, + D0179D8E250BC54600599140 /* MTSubscriber.m */, + D0179D8F250BC54600599140 /* AFHTTPRequestOperation.m */, + D0179D90250BC54600599140 /* MTBuffer.h */, + D0179D91250BC54600599140 /* PingFoundation.m */, + D0179D92250BC54600599140 /* MTDisposable.m */, + D0179D93250BC54600599140 /* MTRequestMessageService.m */, + D0179D94250BC54600599140 /* MTDropResponseContext.m */, + D0179D95250BC54600599140 /* MTDestroySessionResponseMessage.h */, + D0179D96250BC54600599140 /* MTProtoInstance.m */, + D0179D97250BC54600599140 /* MTConnectionProbing.h */, + D0179D98250BC54600599140 /* MTRpcError.m */, + D0179D99250BC54600599140 /* MTResendMessageService.m */, + D0179D9A250BC54600599140 /* MTMsgDetailedInfoMessage.m */, + D0179D9B250BC54600599140 /* MTDiscoverDatacenterAddressAction.m */, + D0179D9C250BC54600599140 /* MTResPqMessage.h */, + D0179D9D250BC54600599140 /* MTBadMsgNotificationMessage.m */, + D0179D9E250BC54600599140 /* MTAes.m */, + D0179D9F250BC54600599140 /* MTNewSessionCreatedMessage.m */, + D0179DA0250BC54600599140 /* MTDatacenterAddressListData.m */, + D0179DA1250BC54600599140 /* MTNetworkUsageCalculationInfo.m */, + D0179DA2250BC54600599140 /* GCDAsyncSocket.h */, + D0179DA3250BC54600599140 /* MTKeychain.m */, + D0179DA4250BC54600599140 /* MTApiEnvironment.m */, + D0179DA5250BC54600599140 /* MTMsgsAckMessage.h */, + D0179DA6250BC54600599140 /* MTContext.m */, + D0179DA7250BC54600599140 /* MTPingMessage.m */, + D0179DA8250BC54600599140 /* MTServerDhInnerDataMessage.h */, + D0179DA9250BC54600599140 /* MTDiscoverConnectionSignals.m */, + D0179DAA250BC54600599140 /* MTMsgAllInfoMessage.h */, + D0179DAB250BC54600599140 /* MTSetClientDhParamsResponseMessage.m */, + D0179DAC250BC54600599140 /* MTDropRpcResultMessage.m */, + D0179DAD250BC54600599140 /* MTTcpConnection.h */, + D0179DAE250BC54600599140 /* MTOutputStream.m */, + D0179DAF250BC54600599140 /* MTNetworkAvailability.m */, + D0179DB0250BC54600599140 /* MTSignal.m */, + D0179DB1250BC54600599140 /* MTBindingTempAuthKeyContext.h */, + D0179DB2250BC54600599140 /* MTMsgResendReqMessage.h */, + D0179DB3250BC54600599140 /* MTRpcResultMessage.m */, + D0179DB4250BC54600599140 /* MTProtoEngine.m */, + D0179DB5250BC54600599140 /* MTProxyConnectivity.m */, + D0179DB6250BC54600599140 /* MTProto.m */, + D0179DB7250BC54600599140 /* MTMsgsStateInfoMessage.m */, + D0179DB8250BC54600599140 /* MTDatacenterTransferAuthAction.m */, + D0179DB9250BC54600599140 /* MTTransport.m */, + D0179DBA250BC54600599140 /* MTMessageTransaction.m */, + D0179DBB250BC54600599140 /* MTDatacenterAuthMessageService.m */, + D0179DBC250BC54600599140 /* MTMessage.m */, + D0179DBD250BC54600599140 /* MTDNS.m */, + D0179DBE250BC54600599140 /* MTTransportSchemeStats.h */, + D0179DBF250BC54600599140 /* MTBufferReader.m */, + D0179DC0250BC54600599140 /* MTLogging.m */, + D0179DC1250BC54600599140 /* MTMsgsStateReqMessage.m */, + D0179DC2250BC54600599140 /* MTInternalMessageParser.h */, + D0179DC3250BC54600599140 /* MTRsa.m */, + D0179DC4250BC54600599140 /* MTMsgContainerMessage.m */, + ); + path = Sources; + sourceTree = ""; + }; + D0179D81250BC54600599140 /* Utils */ = { + isa = PBXGroup; + children = ( + D0179D82250BC54600599140 /* MTQueueLocalObject.m */, + D0179D83250BC54600599140 /* MTQueueLocalObject.h */, + ); + path = Utils; + sourceTree = ""; + }; + D0179ECD250BCB7600599140 /* PublicHeaders */ = { + isa = PBXGroup; + children = ( + D0179ECE250BCB7600599140 /* MtProtoKit */, + ); + name = PublicHeaders; + path = "../../submodules/telegram-ios/submodules/MtProtoKit/PublicHeaders"; + sourceTree = ""; + }; + D0179ECE250BCB7600599140 /* MtProtoKit */ = { + isa = PBXGroup; + children = ( + D0179ECF250BCB7600599140 /* MTDisposable.h */, + D0179ED0250BCB7600599140 /* MTRequestMessageService.h */, + D0179ED1250BCB7600599140 /* MtProtoKit.h */, + D0179ED2250BCB7600599140 /* AFHTTPRequestOperation.h */, + D0179ED3250BCB7600599140 /* MTSubscriber.h */, + D0179ED4250BCB7600599140 /* AFURLConnectionOperation.h */, + D0179ED5250BCB7600599140 /* MTTimeSyncMessageService.h */, + D0179ED6250BCB7600599140 /* MTNetworkUsageCalculationInfo.h */, + D0179ED7250BCB7600599140 /* MTDatacenterAddressListData.h */, + D0179ED8250BCB7600599140 /* MTResendMessageService.h */, + D0179ED9250BCB7600599140 /* MTProtoInstance.h */, + D0179EDA250BCB7600599140 /* MTRpcError.h */, + D0179EDB250BCB7600599140 /* MTDropResponseContext.h */, + D0179EDC250BCB7600599140 /* MTSignal.h */, + D0179EDD250BCB7600599140 /* MTNetworkAvailability.h */, + D0179EDE250BCB7600599140 /* MTProtoPersistenceInterface.h */, + D0179EDF250BCB7600599140 /* MTOutputStream.h */, + D0179EE0250BCB7600599140 /* MTSerialization.h */, + D0179EE1250BCB7600599140 /* MTContext.h */, + D0179EE2250BCB7600599140 /* MTApiEnvironment.h */, + D0179EE3250BCB7600599140 /* MTKeychain.h */, + D0179EE4250BCB7600599140 /* MTLogging.h */, + D0179EE5250BCB7600599140 /* MTMessageTransaction.h */, + D0179EE6250BCB7600599140 /* MTDatacenterAuthMessageService.h */, + D0179EE7250BCB7600599140 /* MTTransport.h */, + D0179EE8250BCB7600599140 /* MTDatacenterTransferAuthAction.h */, + D0179EE9250BCB7600599140 /* MTProto.h */, + D0179EEA250BCB7600599140 /* MTProxyConnectivity.h */, + D0179EEB250BCB7600599140 /* MTProtoEngine.h */, + D0179EEC250BCB7600599140 /* MTDatacenterAuthAction.h */, + D0179EED250BCB7600599140 /* MTBindKeyMessageService.h */, + D0179EEE250BCB7600599140 /* MTIncomingMessage.h */, + D0179EEF250BCB7600599140 /* MTBackupAddressSignals.h */, + D0179EF0250BCB7600599140 /* MTTime.h */, + D0179EF1250BCB7600599140 /* MTBag.h */, + D0179EF2250BCB7600599140 /* MTMessageService.h */, + D0179EF3250BCB7600599140 /* MTGzip.h */, + D0179EF4250BCB7600599140 /* MTPreparedMessage.h */, + D0179EF5250BCB7600599140 /* MTOutgoingMessage.h */, + D0179EF6250BCB7600599140 /* MTQueue.h */, + D0179EF7250BCB7600599140 /* MTRequest.h */, + D0179EF8250BCB7600599140 /* MTTransportTransaction.h */, + D0179EF9250BCB7600599140 /* MTSessionInfo.h */, + D0179EFA250BCB7600599140 /* MTRequestErrorContext.h */, + D0179EFB250BCB7600599140 /* MTExportedAuthorizationData.h */, + D0179EFC250BCB7600599140 /* MTDatacenterVerificationData.h */, + D0179EFD250BCB7600599140 /* MTDatacenterAuthInfo.h */, + D0179EFE250BCB7600599140 /* MTRequestContext.h */, + D0179EFF250BCB7600599140 /* MTInternalId.h */, + D0179F00250BCB7600599140 /* MTDatacenterSaltInfo.h */, + D0179F01250BCB7600599140 /* MTTimeFixContext.h */, + D0179F02250BCB7600599140 /* MTTimer.h */, + D0179F03250BCB7600599140 /* MTHttpRequestOperation.h */, + D0179F04250BCB7600599140 /* MTMessageEncryptionKey.h */, + D0179F05250BCB7600599140 /* MTEncryption.h */, + D0179F06250BCB7600599140 /* MTNetworkUsageManager.h */, + D0179F07250BCB7600599140 /* MTTcpTransport.h */, + D0179F08250BCB7600599140 /* MTAtomic.h */, + D0179F09250BCB7600599140 /* MTFileBasedKeychain.h */, + D0179F0A250BCB7600599140 /* MTTransportScheme.h */, + D0179F0B250BCB7600599140 /* MTDatacenterAddress.h */, + D0179F0C250BCB7600599140 /* MTInputStream.h */, + D0179F0D250BCB7600599140 /* MTDatacenterAddressSet.h */, + ); + path = MtProtoKit; + sourceTree = ""; + }; + D05A830918AFB3F9007F1076 = { + isa = PBXGroup; + children = ( + D0179ECD250BCB7600599140 /* PublicHeaders */, + D05A849B18AFCA3D007F1076 /* MtProtoKit */, + D079AB981AF39B8000076F59 /* MtProtoKit */, + D05A831618AFB3F9007F1076 /* Frameworks */, + D05A831518AFB3F9007F1076 /* Products */, + ); + sourceTree = ""; + }; + D05A831518AFB3F9007F1076 /* Products */ = { + isa = PBXGroup; + children = ( + D079AB971AF39B8000076F59 /* MtProtoKit.framework */, + ); + name = Products; + sourceTree = ""; + }; + D05A831618AFB3F9007F1076 /* Frameworks */ = { + isa = PBXGroup; + children = ( + A7918DD6240CEF8A002011CA /* MurMurHash32.framework */, + A790A31A236B1199000451B5 /* EncryptionProvider.framework */, + A790A318236B0F6C000451B5 /* Foundation.framework */, + D03878A32332504600DB441C /* CFNetwork.framework */, + D03878A12332503E00DB441C /* SystemConfiguration.framework */, + D038789F2332503300DB441C /* Security.framework */, + D038789D2332500A00DB441C /* libc++.tbd */, + D0B4187A1D7E04CF004562A4 /* libz.tbd */, + D0CAF2F01D75F4EA0011F558 /* CFNetwork.framework */, + D0CAF2EE1D75F4E20011F558 /* UIKit.framework */, + D0CAF2EB1D75F4520011F558 /* Security.framework */, + D0CD990F1D75C16100F41187 /* libcrypto.a */, + D00354731C173CD9006610DA /* SystemConfiguration.framework */, + D00354711C173CD0006610DA /* libz.tbd */, + D0D1A0711ADDE2FC007D9ED6 /* libz.dylib */, + D063A2F918B14AB500C65116 /* libcrypto.dylib */, + D05A831718AFB3F9007F1076 /* Foundation.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + D05A849B18AFCA3D007F1076 /* MtProtoKit */ = { + isa = PBXGroup; + children = ( + D0179D41250BC54600599140 /* Sources */, + ); + name = MtProtoKit; + path = "../../submodules/telegram-ios/submodules/MtProtoKit"; + sourceTree = ""; + }; + D079AB981AF39B8000076F59 /* MtProtoKit */ = { + isa = PBXGroup; + children = ( + A7D281E5236C26400000A9BF /* Info.plist */, + ); + path = MtProtoKit; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + D079AB941AF39B8000076F59 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D0179F30250BCB7700599140 /* MTBag.h in Headers */, + D0179F4A250BCB7700599140 /* MTDatacenterAddress.h in Headers */, + D0179F37250BCB7700599140 /* MTTransportTransaction.h in Headers */, + D0179F1F250BCB7700599140 /* MTSerialization.h in Headers */, + D0179F29250BCB7700599140 /* MTProxyConnectivity.h in Headers */, + D0179F0F250BCB7700599140 /* MTRequestMessageService.h in Headers */, + D0179F1D250BCB7700599140 /* MTProtoPersistenceInterface.h in Headers */, + D0179F11250BCB7700599140 /* AFHTTPRequestOperation.h in Headers */, + D0179F2E250BCB7700599140 /* MTBackupAddressSignals.h in Headers */, + D0179F21250BCB7700599140 /* MTApiEnvironment.h in Headers */, + D0179F27250BCB7700599140 /* MTDatacenterTransferAuthAction.h in Headers */, + D0179F2A250BCB7700599140 /* MTProtoEngine.h in Headers */, + D0179F34250BCB7700599140 /* MTOutgoingMessage.h in Headers */, + D0179F45250BCB7700599140 /* MTNetworkUsageManager.h in Headers */, + D0179F1E250BCB7700599140 /* MTOutputStream.h in Headers */, + D0179F48250BCB7700599140 /* MTFileBasedKeychain.h in Headers */, + D0179F3A250BCB7700599140 /* MTExportedAuthorizationData.h in Headers */, + D0179F41250BCB7700599140 /* MTTimer.h in Headers */, + D0179F26250BCB7700599140 /* MTTransport.h in Headers */, + D0179F43250BCB7700599140 /* MTMessageEncryptionKey.h in Headers */, + D0179F22250BCB7700599140 /* MTKeychain.h in Headers */, + D0179F2F250BCB7700599140 /* MTTime.h in Headers */, + D0179F2C250BCB7700599140 /* MTBindKeyMessageService.h in Headers */, + D0179F1C250BCB7700599140 /* MTNetworkAvailability.h in Headers */, + D0179F23250BCB7700599140 /* MTLogging.h in Headers */, + D0179F14250BCB7700599140 /* MTTimeSyncMessageService.h in Headers */, + D0179F44250BCB7700599140 /* MTEncryption.h in Headers */, + D0179F13250BCB7700599140 /* AFURLConnectionOperation.h in Headers */, + D0179F38250BCB7700599140 /* MTSessionInfo.h in Headers */, + D0179F18250BCB7700599140 /* MTProtoInstance.h in Headers */, + D0179F17250BCB7700599140 /* MTResendMessageService.h in Headers */, + D0179F12250BCB7700599140 /* MTSubscriber.h in Headers */, + D0179F3E250BCB7700599140 /* MTInternalId.h in Headers */, + D0179F19250BCB7700599140 /* MTRpcError.h in Headers */, + D0179F36250BCB7700599140 /* MTRequest.h in Headers */, + D0179F25250BCB7700599140 /* MTDatacenterAuthMessageService.h in Headers */, + D0179F24250BCB7700599140 /* MTMessageTransaction.h in Headers */, + D0179F28250BCB7700599140 /* MTProto.h in Headers */, + D0179F3F250BCB7700599140 /* MTDatacenterSaltInfo.h in Headers */, + D0179F39250BCB7700599140 /* MTRequestErrorContext.h in Headers */, + D0179F4B250BCB7700599140 /* MTInputStream.h in Headers */, + D0179F1B250BCB7700599140 /* MTSignal.h in Headers */, + D0179F0E250BCB7700599140 /* MTDisposable.h in Headers */, + D0179F20250BCB7700599140 /* MTContext.h in Headers */, + D0179F42250BCB7700599140 /* MTHttpRequestOperation.h in Headers */, + D0179F10250BCB7700599140 /* MtProtoKit.h in Headers */, + D0179F15250BCB7700599140 /* MTNetworkUsageCalculationInfo.h in Headers */, + D0179F40250BCB7700599140 /* MTTimeFixContext.h in Headers */, + D0179F3C250BCB7700599140 /* MTDatacenterAuthInfo.h in Headers */, + D0179F31250BCB7700599140 /* MTMessageService.h in Headers */, + D0179F16250BCB7700599140 /* MTDatacenterAddressListData.h in Headers */, + D0179F46250BCB7700599140 /* MTTcpTransport.h in Headers */, + D0179F2B250BCB7700599140 /* MTDatacenterAuthAction.h in Headers */, + D0179F33250BCB7700599140 /* MTPreparedMessage.h in Headers */, + D0179F32250BCB7700599140 /* MTGzip.h in Headers */, + D0179F35250BCB7700599140 /* MTQueue.h in Headers */, + D0179F49250BCB7700599140 /* MTTransportScheme.h in Headers */, + D0179F1A250BCB7700599140 /* MTDropResponseContext.h in Headers */, + D0179F47250BCB7700599140 /* MTAtomic.h in Headers */, + D0179F4C250BCB7700599140 /* MTDatacenterAddressSet.h in Headers */, + D0179F3B250BCB7700599140 /* MTDatacenterVerificationData.h in Headers */, + D0179F2D250BCB7700599140 /* MTIncomingMessage.h in Headers */, + D0179F3D250BCB7700599140 /* MTRequestContext.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + D079AB961AF39B8000076F59 /* MtProtoKit */ = { + isa = PBXNativeTarget; + buildConfigurationList = D079ABAE1AF39B8000076F59 /* Build configuration list for PBXNativeTarget "MtProtoKit" */; + buildPhases = ( + D079AB921AF39B8000076F59 /* Sources */, + D079AB931AF39B8000076F59 /* Frameworks */, + D079AB941AF39B8000076F59 /* Headers */, + D079AB951AF39B8000076F59 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MtProtoKit; + productName = MtProtoKitMac; + productReference = D079AB971AF39B8000076F59 /* MtProtoKit.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D05A830A18AFB3F9007F1076 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = Telegram; + TargetAttributes = { + D079AB961AF39B8000076F59 = { + CreatedOnToolsVersion = 6.3.1; + LastSwiftMigration = 1010; + }; + }; + }; + buildConfigurationList = D05A830D18AFB3F9007F1076 /* Build configuration list for PBXProject "MtProtoKit_Xcode" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + English, + en, + ); + mainGroup = D05A830918AFB3F9007F1076; + productRefGroup = D05A831518AFB3F9007F1076 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D079AB961AF39B8000076F59 /* MtProtoKit */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D079AB951AF39B8000076F59 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D079AB921AF39B8000076F59 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D0179E45250BC54600599140 /* MTRsa.m in Sources */, + D0179E11250BC54600599140 /* AFHTTPRequestOperation.m in Sources */, + D0179E3B250BC54600599140 /* MTTransport.m in Sources */, + D0179DDD250BC54600599140 /* MTOutgoingMessage.m in Sources */, + D0179DCE250BC54600599140 /* MTIncomingMessage.m in Sources */, + D0179E0A250BC54600599140 /* MTTcpConnectionBehaviour.m in Sources */, + D0179DCA250BC54600599140 /* MTBuffer.m in Sources */, + D0179E20250BC54600599140 /* MTAes.m in Sources */, + D0179E0C250BC54600599140 /* AFURLConnectionOperation.m in Sources */, + D0179DF4250BC54600599140 /* MTExportedAuthorizationData.m in Sources */, + D0179E46250BC54600599140 /* MTMsgContainerMessage.m in Sources */, + D0179DCB250BC54600599140 /* MTBackupAddressSignals.m in Sources */, + D0179DF3250BC54600599140 /* MTRequestContext.m in Sources */, + D0179DE1250BC54600599140 /* MTMsgResendReqMessage.m in Sources */, + D0179E00250BC54600599140 /* MTTransportSchemeStats.m in Sources */, + D0179DF5250BC54600599140 /* MTRequestErrorContext.m in Sources */, + D0179DE9250BC54600599140 /* MTDatacenterSaltInfo.m in Sources */, + D0179DE5250BC54600599140 /* MTEncryption.m in Sources */, + D0179E0E250BC54600599140 /* MTServerDhParamsMessage.m in Sources */, + D0179E3A250BC54600599140 /* MTDatacenterTransferAuthAction.m in Sources */, + D0179E35250BC54600599140 /* MTRpcResultMessage.m in Sources */, + D0179E39250BC54600599140 /* MTMsgsStateInfoMessage.m in Sources */, + D0179DD4250BC54600599140 /* MTTransportTransaction.m in Sources */, + D0179DD9250BC54600599140 /* MTResPqMessage.m in Sources */, + D0179E37250BC54600599140 /* MTProxyConnectivity.m in Sources */, + D0179DDE250BC54600599140 /* MTConnectionProbing.m in Sources */, + D0179E28250BC54600599140 /* MTContext.m in Sources */, + D0179DE2250BC54600599140 /* MTTcpTransport.m in Sources */, + D0179E08250BC54600599140 /* MTAtomic.m in Sources */, + D0179DD2250BC54600599140 /* MTDatacenterAuthAction.m in Sources */, + D0179E18250BC54600599140 /* MTProtoInstance.m in Sources */, + D0179DC7250BC54600599140 /* MTGzip.m in Sources */, + D0179E03250BC54600599140 /* MTTransportScheme.m in Sources */, + D0179DFA250BC54600599140 /* MTDatacenterAddressSet.m in Sources */, + D0179E22250BC54600599140 /* MTDatacenterAddressListData.m in Sources */, + D0179DF7250BC54600599140 /* MTMsgsAckMessage.m in Sources */, + D0179DF6250BC54600599140 /* MTSessionInfo.m in Sources */, + D0179E29250BC54600599140 /* MTPingMessage.m in Sources */, + D0179E26250BC54600599140 /* MTApiEnvironment.m in Sources */, + D0179E42250BC54600599140 /* MTLogging.m in Sources */, + D0179DD0250BC54600599140 /* MTBindKeyMessageService.m in Sources */, + D0179DE0250BC54600599140 /* MTBindingTempAuthKeyContext.m in Sources */, + D0179DFD250BC54600599140 /* MTInputStream.m in Sources */, + D0179DE4250BC54600599140 /* MTMessageEncryptionKey.m in Sources */, + D0179E1A250BC54600599140 /* MTRpcError.m in Sources */, + D0179DC9250BC54600599140 /* MTBag.m in Sources */, + D0179E0B250BC54600599140 /* MTTimeSyncMessageService.m in Sources */, + D0179E14250BC54600599140 /* MTDisposable.m in Sources */, + D0179E2B250BC54600599140 /* MTDiscoverConnectionSignals.m in Sources */, + D0179DD6250BC54600599140 /* MTRequest.m in Sources */, + D0179DF2250BC54600599140 /* MTDatacenterAuthInfo.m in Sources */, + D0179DF0250BC54600599140 /* MTDatacenterVerificationData.m in Sources */, + D0179E31250BC54600599140 /* MTNetworkAvailability.m in Sources */, + D0179DE3250BC54600599140 /* MTNetworkUsageManager.m in Sources */, + D0179E07250BC54600599140 /* MTFileBasedKeychain.m in Sources */, + D0179E3E250BC54600599140 /* MTMessage.m in Sources */, + D0179E16250BC54600599140 /* MTDropResponseContext.m in Sources */, + D0179E3C250BC54600599140 /* MTMessageTransaction.m in Sources */, + D0179E25250BC54600599140 /* MTKeychain.m in Sources */, + D0179DEF250BC54600599140 /* MTServerDhInnerDataMessage.m in Sources */, + D0179E13250BC54600599140 /* PingFoundation.m in Sources */, + D0179DFB250BC54600599140 /* MTInternalMessageParser.m in Sources */, + D0179E36250BC54600599140 /* MTProtoEngine.m in Sources */, + D0179DDC250BC54600599140 /* MTQueue.m in Sources */, + D0179DC8250BC54600599140 /* MTTime.m in Sources */, + D0179DEC250BC54600599140 /* MTTimeFixContext.m in Sources */, + D0179E23250BC54600599140 /* MTNetworkUsageCalculationInfo.m in Sources */, + D0179E10250BC54600599140 /* MTSubscriber.m in Sources */, + D0179DE6250BC54600599140 /* MTHttpRequestOperation.m in Sources */, + D0179E30250BC54600599140 /* MTOutputStream.m in Sources */, + D0179DC6250BC54600599140 /* MTPreparedMessage.m in Sources */, + D0179E3D250BC54600599140 /* MTDatacenterAuthMessageService.m in Sources */, + D0179E1C250BC54600599140 /* MTMsgDetailedInfoMessage.m in Sources */, + D0179E1F250BC54600599140 /* MTBadMsgNotificationMessage.m in Sources */, + D0179DEE250BC54600599140 /* MTMsgAllInfoMessage.m in Sources */, + D0179E1B250BC54600599140 /* MTResendMessageService.m in Sources */, + D0179E1D250BC54600599140 /* MTDiscoverDatacenterAddressAction.m in Sources */, + D0179DFF250BC54600599140 /* MTDatacenterAddress.m in Sources */, + D0179E15250BC54600599140 /* MTRequestMessageService.m in Sources */, + D0179DE8250BC54600599140 /* MTTcpConnection.m in Sources */, + D0179E04250BC54600599140 /* MTQueueLocalObject.m in Sources */, + D0179E2E250BC54600599140 /* MTDropRpcResultMessage.m in Sources */, + D0179E32250BC54600599140 /* MTSignal.m in Sources */, + D0179E2D250BC54600599140 /* MTSetClientDhParamsResponseMessage.m in Sources */, + D0179DDF250BC54600599140 /* MTDestroySessionResponseMessage.m in Sources */, + D0179E43250BC54600599140 /* MTMsgsStateReqMessage.m in Sources */, + D0179DD3250BC54600599140 /* GCDAsyncSocket.m in Sources */, + D0179E3F250BC54600599140 /* MTDNS.m in Sources */, + D0179E0D250BC54600599140 /* MTFutureSaltsMessage.m in Sources */, + D0179DE7250BC54600599140 /* MTTimer.m in Sources */, + D0179DCD250BC54600599140 /* MTPongMessage.m in Sources */, + D0179E21250BC54600599140 /* MTNewSessionCreatedMessage.m in Sources */, + D0179E38250BC54600599140 /* MTProto.m in Sources */, + D0179E41250BC54600599140 /* MTBufferReader.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A7F282E7238EAB7100742C20 /* Github */ = { + isa = XCBuildConfiguration; + 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_MODULES_AUTOLINK = NO; + 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; + COPY_PHASE_STRIP = NO; + 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 = 7.1; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + }; + name = Github; + }; + A7F282E8238EAB7100742C20 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_CXX_LANGUAGE_STANDARD = "c++14"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + FRAMEWORK_SEARCH_PATHS = ""; + FRAMEWORK_VERSION = A; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + "BETA=1", + ); + HEADER_SEARCH_PATHS = "$PROJECT_DIR/../../submodules/telegram-ios/submodules/MtProtoKit/PublicHeaders"; + INFOPLIST_FILE = MtProtoKit/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ""; + MACH_O_TYPE = mh_dylib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = NO; + OTHER_CFLAGS = "-DMtProtoKitMacFramework=1"; + PRODUCT_BUNDLE_IDENTIFIER = "org.telegram.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + D0364D4722B3E35B002A6EF0 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_MODULES_AUTOLINK = NO; + 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; + COPY_PHASE_STRIP = NO; + 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 = 7.1; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + }; + name = HockeyappMacAlpha; + }; + D0364D4922B3E35B002A6EF0 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_CXX_LANGUAGE_STANDARD = "c++14"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + FRAMEWORK_SEARCH_PATHS = ""; + FRAMEWORK_VERSION = A; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = s; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + "BETA=1", + ); + HEADER_SEARCH_PATHS = "$PROJECT_DIR/../../submodules/telegram-ios/submodules/MtProtoKit/PublicHeaders"; + INFOPLIST_FILE = MtProtoKit/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ""; + MACH_O_TYPE = mh_dylib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = NO; + OTHER_CFLAGS = "-DMtProtoKitMacFramework=1"; + PRODUCT_BUNDLE_IDENTIFIER = "org.telegram.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 3.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + D079FD101F06BE440038FADE /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_MODULES_AUTOLINK = NO; + 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; + COPY_PHASE_STRIP = NO; + 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 = 7.1; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + }; + name = DebugHockeyapp; + }; + D079FD121F06BE440038FADE /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_CXX_LANGUAGE_STANDARD = "c++14"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + FRAMEWORK_SEARCH_PATHS = ""; + FRAMEWORK_VERSION = A; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = s; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + "BETA=1", + ); + HEADER_SEARCH_PATHS = "$PROJECT_DIR/../../submodules/telegram-ios/submodules/MtProtoKit/PublicHeaders"; + INFOPLIST_FILE = MtProtoKit/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ""; + MACH_O_TYPE = mh_dylib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = NO; + OTHER_CFLAGS = "-DMtProtoKitMacFramework=1"; + PRODUCT_BUNDLE_IDENTIFIER = "org.telegram.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 3.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + D079FD161F06BE4D0038FADE /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_MODULES_AUTOLINK = NO; + 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; + COPY_PHASE_STRIP = YES; + 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 = 7.1; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = ReleaseAppStore; + }; + D079FD181F06BE4D0038FADE /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_CXX_LANGUAGE_STANDARD = "c++14"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + FRAMEWORK_SEARCH_PATHS = ""; + FRAMEWORK_VERSION = A; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = APPSTORE; + HEADER_SEARCH_PATHS = "$PROJECT_DIR/../../submodules/telegram-ios/submodules/MtProtoKit/PublicHeaders"; + INFOPLIST_FILE = MtProtoKit/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ""; + MACH_O_TYPE = mh_dylib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + ONLY_ACTIVE_ARCH = NO; + OTHER_CFLAGS = "-DMtProtoKitMacFramework=1"; + PRODUCT_BUNDLE_IDENTIFIER = "org.telegram.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + D079FD1C1F06BE540038FADE /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_MODULES_AUTOLINK = NO; + 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; + COPY_PHASE_STRIP = YES; + 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 = 7.1; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = ReleaseHockeyapp; + }; + D079FD1E1F06BE540038FADE /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_CXX_LANGUAGE_STANDARD = "c++14"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + FRAMEWORK_SEARCH_PATHS = ""; + FRAMEWORK_VERSION = A; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = STABLE; + HEADER_SEARCH_PATHS = "$PROJECT_DIR/../../submodules/telegram-ios/submodules/MtProtoKit/PublicHeaders"; + INFOPLIST_FILE = MtProtoKit/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ""; + MACH_O_TYPE = mh_dylib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + ONLY_ACTIVE_ARCH = NO; + OTHER_CFLAGS = "-DMtProtoKitMacFramework=1"; + PRODUCT_BUNDLE_IDENTIFIER = "org.telegram.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + D0DB57B01E5C4B470071854C /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_MODULES_AUTOLINK = NO; + 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; + COPY_PHASE_STRIP = NO; + 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 = 7.1; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + }; + name = DebugAppStore; + }; + D0DB57B21E5C4B470071854C /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_CXX_LANGUAGE_STANDARD = "c++14"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + FRAMEWORK_SEARCH_PATHS = ""; + FRAMEWORK_VERSION = A; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "BETA=1", + ); + HEADER_SEARCH_PATHS = "$PROJECT_DIR/../../submodules/telegram-ios/submodules/MtProtoKit/PublicHeaders"; + INFOPLIST_FILE = MtProtoKit/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ""; + MACH_O_TYPE = mh_dylib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = "-DMtProtoKitMacFramework=1"; + PRODUCT_BUNDLE_IDENTIFIER = "org.telegram.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D05A830D18AFB3F9007F1076 /* Build configuration list for PBXProject "MtProtoKit_Xcode" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D079FD101F06BE440038FADE /* DebugHockeyapp */, + D0364D4722B3E35B002A6EF0 /* HockeyappMacAlpha */, + D0DB57B01E5C4B470071854C /* DebugAppStore */, + A7F282E7238EAB7100742C20 /* Github */, + D079FD161F06BE4D0038FADE /* ReleaseAppStore */, + D079FD1C1F06BE540038FADE /* ReleaseHockeyapp */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = DebugHockeyapp; + }; + D079ABAE1AF39B8000076F59 /* Build configuration list for PBXNativeTarget "MtProtoKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D079FD121F06BE440038FADE /* DebugHockeyapp */, + D0364D4922B3E35B002A6EF0 /* HockeyappMacAlpha */, + D0DB57B21E5C4B470071854C /* DebugAppStore */, + A7F282E8238EAB7100742C20 /* Github */, + D079FD181F06BE4D0038FADE /* ReleaseAppStore */, + D079FD1E1F06BE540038FADE /* ReleaseHockeyapp */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = DebugHockeyapp; + }; +/* End XCConfigurationList section */ + }; + rootObject = D05A830A18AFB3F9007F1076 /* Project object */; +} diff --git a/core-xprojects/MtProtoKit/MtProtoKit_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/MtProtoKit/MtProtoKit_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..02630510de --- /dev/null +++ b/core-xprojects/MtProtoKit/MtProtoKit_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/MtProtoKit/MtProtoKit_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/MtProtoKit/MtProtoKit_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/MtProtoKit/MtProtoKit_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/MtProtoKit/MtProtoKit_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate b/core-xprojects/MtProtoKit/MtProtoKit_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000..5568046175 Binary files /dev/null and b/core-xprojects/MtProtoKit/MtProtoKit_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/core-xprojects/MtProtoKit/MtProtoKit_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/MtProtoKit.xcscheme b/core-xprojects/MtProtoKit/MtProtoKit_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/MtProtoKit.xcscheme new file mode 100644 index 0000000000..60d9295c4e --- /dev/null +++ b/core-xprojects/MtProtoKit/MtProtoKit_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/MtProtoKit.xcscheme @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/MtProtoKit/MtProtoKit_Xcode.xcodeproj/xcuserdata/mikhailfilimonov.xcuserdatad/xcschemes/xcschememanagement.plist b/core-xprojects/MtProtoKit/MtProtoKit_Xcode.xcodeproj/xcuserdata/mikhailfilimonov.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100755 index 0000000000..064640e4e1 --- /dev/null +++ b/core-xprojects/MtProtoKit/MtProtoKit_Xcode.xcodeproj/xcuserdata/mikhailfilimonov.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,24 @@ + + + + + SchemeUserState + + MtProtoKit.xcscheme_^#shared#^_ + + orderHint + 33 + + MtProtoKitDynamic.xcscheme_^#shared#^_ + + orderHint + 35 + + MtProtoKitMac.xcscheme_^#shared#^_ + + orderHint + 34 + + + + diff --git a/core-xprojects/MtProtoKit/MtProtoKit_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/MtProtoKit.xcscheme b/core-xprojects/MtProtoKit/MtProtoKit_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/MtProtoKit.xcscheme new file mode 100644 index 0000000000..8b16bd31c9 --- /dev/null +++ b/core-xprojects/MtProtoKit/MtProtoKit_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/MtProtoKit.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/MtProtoKit/MtProtoKit_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist b/core-xprojects/MtProtoKit/MtProtoKit_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000000..068d96fc7a --- /dev/null +++ b/core-xprojects/MtProtoKit/MtProtoKit_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,21 @@ + + + + + SchemeUserState + + MtProtoKit.xcscheme + + isShown + + orderHint + 10 + + MtProtoKitDynamic.xcscheme_^#shared#^_ + + orderHint + 2 + + + + diff --git a/core-xprojects/MtProtoKit/MtProtoKit_Xcode.xcodeproj/xcuserdata/telegram.xcuserdatad/xcschemes/xcschememanagement.plist b/core-xprojects/MtProtoKit/MtProtoKit_Xcode.xcodeproj/xcuserdata/telegram.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000000..a43f65e87d --- /dev/null +++ b/core-xprojects/MtProtoKit/MtProtoKit_Xcode.xcodeproj/xcuserdata/telegram.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,24 @@ + + + + + SchemeUserState + + MtProtoKit.xcscheme_^#shared#^_ + + orderHint + 39 + + MtProtoKitDynamic.xcscheme_^#shared#^_ + + orderHint + 40 + + MtProtoKitMac.xcscheme_^#shared#^_ + + orderHint + 41 + + + + diff --git a/core-xprojects/MurMurHash32/MurMurHash32.xcodeproj/project.pbxproj b/core-xprojects/MurMurHash32/MurMurHash32.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..5571d26c28 --- /dev/null +++ b/core-xprojects/MurMurHash32/MurMurHash32.xcodeproj/project.pbxproj @@ -0,0 +1,715 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + A7918DCC240CEEBD002011CA /* MurMurHash32Public.h in Headers */ = {isa = PBXBuildFile; fileRef = A7918DCA240CEEBD002011CA /* MurMurHash32Public.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7918DD3240CEEFC002011CA /* MurMurHash32.h in Headers */ = {isa = PBXBuildFile; fileRef = A7918DD2240CEEFC002011CA /* MurMurHash32.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7918DD5240CEF0D002011CA /* MurMurHash32.m in Sources */ = {isa = PBXBuildFile; fileRef = A7918DD4240CEF0D002011CA /* MurMurHash32.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A7918DC7240CEEBD002011CA /* MurMurHash32.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MurMurHash32.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7918DCA240CEEBD002011CA /* MurMurHash32Public.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MurMurHash32Public.h; sourceTree = ""; }; + A7918DCB240CEEBD002011CA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A7918DD2240CEEFC002011CA /* MurMurHash32.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = MurMurHash32.h; path = "../../submodules/telegram-ios/submodules/MurMurHash32/PublicHeaders/MurMurHash32/MurMurHash32.h"; sourceTree = ""; }; + A7918DD4240CEF0D002011CA /* MurMurHash32.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = MurMurHash32.m; path = "../../submodules/telegram-ios/submodules/MurMurHash32/Sources/MurMurHash32.m"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A7918DC4240CEEBD002011CA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A7918DBD240CEEBD002011CA = { + isa = PBXGroup; + children = ( + A7918DD4240CEF0D002011CA /* MurMurHash32.m */, + A7918DD2240CEEFC002011CA /* MurMurHash32.h */, + A7918DC9240CEEBD002011CA /* MurMurHash32 */, + A7918DC8240CEEBD002011CA /* Products */, + ); + sourceTree = ""; + }; + A7918DC8240CEEBD002011CA /* Products */ = { + isa = PBXGroup; + children = ( + A7918DC7240CEEBD002011CA /* MurMurHash32.framework */, + ); + name = Products; + sourceTree = ""; + }; + A7918DC9240CEEBD002011CA /* MurMurHash32 */ = { + isa = PBXGroup; + children = ( + A7918DCA240CEEBD002011CA /* MurMurHash32Public.h */, + A7918DCB240CEEBD002011CA /* Info.plist */, + ); + path = MurMurHash32; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + A7918DC2240CEEBD002011CA /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + A7918DCC240CEEBD002011CA /* MurMurHash32Public.h in Headers */, + A7918DD3240CEEFC002011CA /* MurMurHash32.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + A7918DC6240CEEBD002011CA /* MurMurHash32 */ = { + isa = PBXNativeTarget; + buildConfigurationList = A7918DCF240CEEBD002011CA /* Build configuration list for PBXNativeTarget "MurMurHash32" */; + buildPhases = ( + A7918DC2240CEEBD002011CA /* Headers */, + A7918DC3240CEEBD002011CA /* Sources */, + A7918DC4240CEEBD002011CA /* Frameworks */, + A7918DC5240CEEBD002011CA /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MurMurHash32; + productName = MurMurHash32; + productReference = A7918DC7240CEEBD002011CA /* MurMurHash32.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A7918DBE240CEEBD002011CA /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1030; + ORGANIZATIONNAME = Telegram; + TargetAttributes = { + A7918DC6240CEEBD002011CA = { + CreatedOnToolsVersion = 10.3; + }; + }; + }; + buildConfigurationList = A7918DC1240CEEBD002011CA /* Build configuration list for PBXProject "MurMurHash32" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = A7918DBD240CEEBD002011CA; + productRefGroup = A7918DC8240CEEBD002011CA /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A7918DC6240CEEBD002011CA /* MurMurHash32 */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A7918DC5240CEEBD002011CA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A7918DC3240CEEBD002011CA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A7918DD5240CEF0D002011CA /* MurMurHash32.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A7918DCD240CEEBD002011CA /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + A7918DCE240CEEBD002011CA /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + A7918DD0240CEEBD002011CA /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = MurMurHash32/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.MurMurHash32; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = DebugAppStore; + }; + A7918DD1240CEEBD002011CA /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = MurMurHash32/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.MurMurHash32; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = ReleaseHockeyapp; + }; + A7918DD8240CEFA5002011CA /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + A7918DD9240CEFA5002011CA /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_OPTIMIZATION_LEVEL = s; + INFOPLIST_FILE = MurMurHash32/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.MurMurHash32; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = DebugHockeyapp; + }; + A7918DDA240CEFAB002011CA /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + A7918DDB240CEFAB002011CA /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_OPTIMIZATION_LEVEL = s; + INFOPLIST_FILE = MurMurHash32/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.MurMurHash32; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = HockeyappMacAlpha; + }; + A7918DDC240CEFC1002011CA /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + A7918DDD240CEFC1002011CA /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = MurMurHash32/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.MurMurHash32; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = ReleaseAppStore; + }; + D0B4940824F3F5A600E0A9B3 /* Github */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + D0B4940924F3F5A600E0A9B3 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = MurMurHash32/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.MurMurHash32; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = Github; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A7918DC1240CEEBD002011CA /* Build configuration list for PBXProject "MurMurHash32" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A7918DCD240CEEBD002011CA /* DebugAppStore */, + D0B4940824F3F5A600E0A9B3 /* Github */, + A7918DDA240CEFAB002011CA /* HockeyappMacAlpha */, + A7918DD8240CEFA5002011CA /* DebugHockeyapp */, + A7918DCE240CEEBD002011CA /* ReleaseHockeyapp */, + A7918DDC240CEFC1002011CA /* ReleaseAppStore */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseHockeyapp; + }; + A7918DCF240CEEBD002011CA /* Build configuration list for PBXNativeTarget "MurMurHash32" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A7918DD0240CEEBD002011CA /* DebugAppStore */, + D0B4940924F3F5A600E0A9B3 /* Github */, + A7918DDB240CEFAB002011CA /* HockeyappMacAlpha */, + A7918DD9240CEFA5002011CA /* DebugHockeyapp */, + A7918DD1240CEEBD002011CA /* ReleaseHockeyapp */, + A7918DDD240CEFC1002011CA /* ReleaseAppStore */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseHockeyapp; + }; +/* End XCConfigurationList section */ + }; + rootObject = A7918DBE240CEEBD002011CA /* Project object */; +} diff --git a/core-xprojects/MurMurHash32/MurMurHash32.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/MurMurHash32/MurMurHash32.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..80145310c2 --- /dev/null +++ b/core-xprojects/MurMurHash32/MurMurHash32.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/MurMurHash32/MurMurHash32.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/MurMurHash32/MurMurHash32.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/MurMurHash32/MurMurHash32.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/MurMurHash32/MurMurHash32/Info.plist b/core-xprojects/MurMurHash32/MurMurHash32/Info.plist new file mode 100644 index 0000000000..861c829fcb --- /dev/null +++ b/core-xprojects/MurMurHash32/MurMurHash32/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2020 Telegram. All rights reserved. + + diff --git a/core-xprojects/MurMurHash32/MurMurHash32/MurMurHash32Public.h b/core-xprojects/MurMurHash32/MurMurHash32/MurMurHash32Public.h new file mode 100644 index 0000000000..5b8ae4cf7b --- /dev/null +++ b/core-xprojects/MurMurHash32/MurMurHash32/MurMurHash32Public.h @@ -0,0 +1,20 @@ +// +// MurMurHash32.h +// MurMurHash32 +// +// Created by Mikhail Filimonov on 02.03.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +#import + +//! Project version number for MurMurHash32. +FOUNDATION_EXPORT double MurMurHash32VersionNumber; + +//! Project version string for MurMurHash32. +FOUNDATION_EXPORT const unsigned char MurMurHash32VersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + +#import diff --git a/core-xprojects/NetworkLogging/NetworkLogging.xcodeproj/project.pbxproj b/core-xprojects/NetworkLogging/NetworkLogging.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..39bfdb4361 --- /dev/null +++ b/core-xprojects/NetworkLogging/NetworkLogging.xcodeproj/project.pbxproj @@ -0,0 +1,771 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + A791900C240CF76A002011CA /* NetworkLoggingPublic.h in Headers */ = {isa = PBXBuildFile; fileRef = A791900A240CF76A002011CA /* NetworkLoggingPublic.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7919133240CFF3F002011CA /* MtProtoKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7919132240CFF3F002011CA /* MtProtoKit.framework */; }; + D0179ECC250BC7EA00599140 /* NetworkLogging.m in Sources */ = {isa = PBXBuildFile; fileRef = D0179ECB250BC7EA00599140 /* NetworkLogging.m */; }; + D0179F50250BCBAD00599140 /* NetworkLogging.h in Headers */ = {isa = PBXBuildFile; fileRef = D0179F4F250BCBAD00599140 /* NetworkLogging.h */; settings = {ATTRIBUTES = (Public, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A7919007240CF76A002011CA /* NetworkLogging.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = NetworkLogging.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A791900A240CF76A002011CA /* NetworkLoggingPublic.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NetworkLoggingPublic.h; sourceTree = ""; }; + A791900B240CF76A002011CA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A7919132240CFF3F002011CA /* MtProtoKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MtProtoKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0179ECB250BC7EA00599140 /* NetworkLogging.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NetworkLogging.m; sourceTree = ""; }; + D0179F4F250BCBAD00599140 /* NetworkLogging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NetworkLogging.h; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A7919004240CF76A002011CA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A7919133240CFF3F002011CA /* MtProtoKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A7918FFD240CF76A002011CA = { + isa = PBXGroup; + children = ( + A7919009240CF76A002011CA /* NetworkLogging */, + A7919008240CF76A002011CA /* Products */, + A7919131240CFF3F002011CA /* Frameworks */, + ); + sourceTree = ""; + }; + A7919008240CF76A002011CA /* Products */ = { + isa = PBXGroup; + children = ( + A7919007240CF76A002011CA /* NetworkLogging.framework */, + ); + name = Products; + sourceTree = ""; + }; + A7919009240CF76A002011CA /* NetworkLogging */ = { + isa = PBXGroup; + children = ( + D0179F4D250BCBAD00599140 /* PublicHeaders */, + D0179ECA250BC7EA00599140 /* Sources */, + A791900A240CF76A002011CA /* NetworkLoggingPublic.h */, + A791900B240CF76A002011CA /* Info.plist */, + ); + path = NetworkLogging; + sourceTree = ""; + }; + A7919131240CFF3F002011CA /* Frameworks */ = { + isa = PBXGroup; + children = ( + A7919132240CFF3F002011CA /* MtProtoKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + D0179ECA250BC7EA00599140 /* Sources */ = { + isa = PBXGroup; + children = ( + D0179ECB250BC7EA00599140 /* NetworkLogging.m */, + ); + name = Sources; + path = "../../../submodules/telegram-ios/submodules/NetworkLogging/Sources"; + sourceTree = ""; + }; + D0179F4D250BCBAD00599140 /* PublicHeaders */ = { + isa = PBXGroup; + children = ( + D0179F4E250BCBAD00599140 /* NetworkLogging */, + ); + name = PublicHeaders; + path = "../../../submodules/telegram-ios/submodules/NetworkLogging/PublicHeaders"; + sourceTree = ""; + }; + D0179F4E250BCBAD00599140 /* NetworkLogging */ = { + isa = PBXGroup; + children = ( + D0179F4F250BCBAD00599140 /* NetworkLogging.h */, + ); + path = NetworkLogging; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + A7919002240CF76A002011CA /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + A791900C240CF76A002011CA /* NetworkLoggingPublic.h in Headers */, + D0179F50250BCBAD00599140 /* NetworkLogging.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + A7919006240CF76A002011CA /* NetworkLogging */ = { + isa = PBXNativeTarget; + buildConfigurationList = A791900F240CF76A002011CA /* Build configuration list for PBXNativeTarget "NetworkLogging" */; + buildPhases = ( + A7919002240CF76A002011CA /* Headers */, + A7919003240CF76A002011CA /* Sources */, + A7919004240CF76A002011CA /* Frameworks */, + A7919005240CF76A002011CA /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = NetworkLogging; + productName = NetworkLogging; + productReference = A7919007240CF76A002011CA /* NetworkLogging.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A7918FFE240CF76A002011CA /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1030; + ORGANIZATIONNAME = Telegram; + TargetAttributes = { + A7919006240CF76A002011CA = { + CreatedOnToolsVersion = 10.3; + }; + }; + }; + buildConfigurationList = A7919001240CF76A002011CA /* Build configuration list for PBXProject "NetworkLogging" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = A7918FFD240CF76A002011CA; + productRefGroup = A7919008240CF76A002011CA /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A7919006240CF76A002011CA /* NetworkLogging */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A7919005240CF76A002011CA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A7919003240CF76A002011CA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D0179ECC250BC7EA00599140 /* NetworkLogging.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A791900D240CF76A002011CA /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + A791900E240CF76A002011CA /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + A7919010240CF76A002011CA /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + HEADER_SEARCH_PATHS = ( + "$PROJECT_DIR/../../submodules/telegram-ios/submodules/NetworkLogging/PublicHeaders", + "$PROJECT_DIR/../../submodules/telegram-ios/submodules/MtProtoKit/PublicHeaders", + ); + INFOPLIST_FILE = NetworkLogging/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.NetworkLogging; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = DebugAppStore; + }; + A7919011240CF76A002011CA /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + HEADER_SEARCH_PATHS = ( + "$PROJECT_DIR/../../submodules/telegram-ios/submodules/NetworkLogging/PublicHeaders", + "$PROJECT_DIR/../../submodules/telegram-ios/submodules/MtProtoKit/PublicHeaders", + ); + INFOPLIST_FILE = NetworkLogging/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.NetworkLogging; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = ReleaseAppStore; + }; + A7919042240CFA70002011CA /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + A7919043240CFA70002011CA /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_OPTIMIZATION_LEVEL = s; + HEADER_SEARCH_PATHS = ( + "$PROJECT_DIR/../../submodules/telegram-ios/submodules/NetworkLogging/PublicHeaders", + "$PROJECT_DIR/../../submodules/telegram-ios/submodules/MtProtoKit/PublicHeaders", + ); + INFOPLIST_FILE = NetworkLogging/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.NetworkLogging; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = DebugHockeyapp; + }; + A7919044240CFA76002011CA /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + A7919045240CFA76002011CA /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_OPTIMIZATION_LEVEL = s; + HEADER_SEARCH_PATHS = ( + "$PROJECT_DIR/../../submodules/telegram-ios/submodules/NetworkLogging/PublicHeaders", + "$PROJECT_DIR/../../submodules/telegram-ios/submodules/MtProtoKit/PublicHeaders", + ); + INFOPLIST_FILE = NetworkLogging/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.NetworkLogging; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = HockeyappMacAlpha; + }; + A7919046240CFA86002011CA /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + A7919047240CFA86002011CA /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + HEADER_SEARCH_PATHS = ( + "$PROJECT_DIR/../../submodules/telegram-ios/submodules/NetworkLogging/PublicHeaders", + "$PROJECT_DIR/../../submodules/telegram-ios/submodules/MtProtoKit/PublicHeaders", + ); + INFOPLIST_FILE = NetworkLogging/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.NetworkLogging; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = ReleaseHockeyapp; + }; + D0B4941424F3F60400E0A9B3 /* Github */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + D0B4941524F3F60400E0A9B3 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + HEADER_SEARCH_PATHS = ( + "$PROJECT_DIR/../../submodules/telegram-ios/submodules/NetworkLogging/PublicHeaders", + "$PROJECT_DIR/../../submodules/telegram-ios/submodules/MtProtoKit/PublicHeaders", + ); + INFOPLIST_FILE = NetworkLogging/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.NetworkLogging; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = Github; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A7919001240CF76A002011CA /* Build configuration list for PBXProject "NetworkLogging" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A791900D240CF76A002011CA /* DebugAppStore */, + D0B4941424F3F60400E0A9B3 /* Github */, + A7919044240CFA76002011CA /* HockeyappMacAlpha */, + A7919042240CFA70002011CA /* DebugHockeyapp */, + A791900E240CF76A002011CA /* ReleaseAppStore */, + A7919046240CFA86002011CA /* ReleaseHockeyapp */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; + A791900F240CF76A002011CA /* Build configuration list for PBXNativeTarget "NetworkLogging" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A7919010240CF76A002011CA /* DebugAppStore */, + D0B4941524F3F60400E0A9B3 /* Github */, + A7919045240CFA76002011CA /* HockeyappMacAlpha */, + A7919043240CFA70002011CA /* DebugHockeyapp */, + A7919011240CF76A002011CA /* ReleaseAppStore */, + A7919047240CFA86002011CA /* ReleaseHockeyapp */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; +/* End XCConfigurationList section */ + }; + rootObject = A7918FFE240CF76A002011CA /* Project object */; +} diff --git a/core-xprojects/NetworkLogging/NetworkLogging.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/NetworkLogging/NetworkLogging.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..57d5f7e94e --- /dev/null +++ b/core-xprojects/NetworkLogging/NetworkLogging.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/NetworkLogging/NetworkLogging.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/NetworkLogging/NetworkLogging.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/NetworkLogging/NetworkLogging.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/NetworkLogging/NetworkLogging/Info.plist b/core-xprojects/NetworkLogging/NetworkLogging/Info.plist new file mode 100644 index 0000000000..861c829fcb --- /dev/null +++ b/core-xprojects/NetworkLogging/NetworkLogging/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2020 Telegram. All rights reserved. + + diff --git a/core-xprojects/NetworkLogging/NetworkLogging/NetworkLoggingPublic.h b/core-xprojects/NetworkLogging/NetworkLogging/NetworkLoggingPublic.h new file mode 100644 index 0000000000..efa455195b --- /dev/null +++ b/core-xprojects/NetworkLogging/NetworkLogging/NetworkLoggingPublic.h @@ -0,0 +1,20 @@ +// +// NetworkLogging.h +// NetworkLogging +// +// Created by Mikhail Filimonov on 02.03.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +#import + +//! Project version number for NetworkLogging. +FOUNDATION_EXPORT double NetworkLoggingVersionNumber; + +//! Project version string for NetworkLogging. +FOUNDATION_EXPORT const unsigned char NetworkLoggingVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + +#import diff --git a/core-xprojects/OpenSSLEncryption/OpenSSLEncryption/Info.plist b/core-xprojects/OpenSSLEncryption/OpenSSLEncryption/Info.plist new file mode 100644 index 0000000000..0c4389ac7e --- /dev/null +++ b/core-xprojects/OpenSSLEncryption/OpenSSLEncryption/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2019 Telegram. All rights reserved. + + diff --git a/core-xprojects/OpenSSLEncryption/OpenSSLEncryption/OpenSSLEncryption.h b/core-xprojects/OpenSSLEncryption/OpenSSLEncryption/OpenSSLEncryption.h new file mode 100644 index 0000000000..91d79d32b6 --- /dev/null +++ b/core-xprojects/OpenSSLEncryption/OpenSSLEncryption/OpenSSLEncryption.h @@ -0,0 +1,20 @@ +// +// OpenSSLEncryption.h +// OpenSSLEncryption +// +// Created by Mikhail Filimonov on 31.10.2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +#import + +//! Project version number for OpenSSLEncryption. +FOUNDATION_EXPORT double OpenSSLEncryptionVersionNumber; + +//! Project version string for OpenSSLEncryption. +FOUNDATION_EXPORT const unsigned char OpenSSLEncryptionVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + +#import diff --git a/core-xprojects/OpenSSLEncryption/OpenSSLEncryption/build.sh b/core-xprojects/OpenSSLEncryption/OpenSSLEncryption/build.sh new file mode 100644 index 0000000000..2413b4eb44 --- /dev/null +++ b/core-xprojects/OpenSSLEncryption/OpenSSLEncryption/build.sh @@ -0,0 +1,203 @@ +#!/bin/bash + +set -x +set -e + +SRC_DIR="$1" +BUILD_DIR=$(echo "$(cd "$(dirname "$3")"; pwd -P)/$(basename "$3")") + + + +cd $BUILD_DIR + +rm -rf build || true +mkdir build + +OUT_DIR="${BUILD_DIR}build" + + + + +CROSS_TOP_MAC="$(xcode-select -p)/Platforms/MacOSX.platform" +CROSS_SDK_MAC="MacOSX.sdk" + + +SOURCE_DIR="$OUT_DIR/openssl-1.1.1d" +SOURCE_ARCHIVE="$SRC_DIR/openssl-1.1.1d.tar.gz" + +rm -rf "$SOURCE_DIR" + +tar -xzf "$SOURCE_ARCHIVE" --directory "$OUT_DIR" + +export CROSS_COMPILE=`xcode-select --print-path`/Toolchains/XcodeDefault.xctoolchain/usr/bin/ + +function build_for () +{ + DIR="$(pwd)" + cd "$SOURCE_DIR" + + PLATFORM="$1" + ARCH="$2" + CROSS_TOP_ENV="CROSS_TOP_$3" + CROSS_SDK_ENV="CROSS_SDK_$3" + + make clean || true + + export CROSS_TOP="${!CROSS_TOP_ENV}" + export CROSS_SDK="${!CROSS_SDK_ENV}" + + MINIMAL_FLAGS=(\ + "no-shared" \ + "no-afalgeng" \ + "no-aria" \ + "no-asan" \ + "no-async" \ + "no-autoalginit" \ + "no-autoerrinit" \ + "no-autoload-config" \ + "no-bf" \ + "no-blake2" \ + "no-buildtest-c++" \ + "no-camellia" \ + "no-capieng" \ + "no-cast" \ + "no-chacha" \ + "no-cmac" \ + "no-cms" \ + "no-comp" \ + "no-crypto-mdebug" \ + "no-crypto-mdebug-backtrace" \ + "no-ct" \ + "no-deprecated" \ + "no-des" \ + "no-devcryptoeng" \ + "no-dgram" \ + "no-dh" \ + "no-dsa" \ + "no-dtls" \ + "no-dynamic-engine" \ + "no-ec" \ + "no-ec2m" \ + "no-ecdh" \ + "no-ecdsa" \ + "no-ec_nistp_64_gcc_128" \ + "no-egd" \ + "no-engine" \ + "no-err" \ + "no-external-tests" \ + "no-filenames" \ + "no-fuzz-libfuzzer" \ + "no-fuzz-afl" \ + "no-gost" \ + "no-heartbeats" \ + "no-idea" \ + "no-makedepend" \ + "no-md2" \ + "no-md4" \ + "no-mdc2" \ + "no-msan" \ + "no-multiblock" \ + "no-nextprotoneg" \ + "no-pinshared" \ + "no-ocb" \ + "no-ocsp" \ + "no-pic" \ + "no-poly1305" \ + "no-posix-io" \ + "no-psk" \ + "no-rc2" \ + "no-rc4" \ + "no-rc5" \ + "no-rfc3779" \ + "no-rmd160" \ + "no-scrypt" \ + "no-sctp" \ + "no-shared" \ + "no-siphash" \ + "no-sm2" \ + "no-sm3" \ + "no-sm4" \ + "no-sock" \ + "no-srp" \ + "no-srtp" \ + "no-sse2" \ + "no-ssl" \ + "no-ssl-trace" \ + "no-static-engine" \ + "no-stdio" \ + "no-tests" \ + "no-tls" \ + "no-ts" \ + "no-ubsan" \ + "no-ui-console" \ + "no-unit-test" \ + "no-whirlpool" \ + "no-weak-ssl-ciphers" \ + "no-zlib" \ + "no-zlib-dynamic" \ + ) + + DEFAULT_FLAGS=(\ + "no-shared" \ + "no-asm" \ + "no-ssl3" \ + "no-comp" \ + "no-hw" \ + "no-engine" \ + "no-async" \ + "no-tests" \ + ) + + ./Configure $PLATFORM "-arch $ARCH" ${DEFAULT_FLAGS[@]} --prefix="${ABS_TMP_DIR}/${ARCH}" || exit 1 + + make || exit 2 + unset CROSS_TOP + unset CROSS_SDK + + cd "$DIR" +} + +patch "$SOURCE_DIR/Configurations/10-main.conf" < "$PWD/OpenSSLEncryption/patch-conf.diff" || exit 1 + + +for ARCH in $ARCHS +do + build_for darwin64-$ARCH-cc $ARCH MAC + mkdir build/$ARCH + mv "build/openssl-1.1.1d/libssl.a" "build/$ARCH/libssl.a" + mv "build/openssl-1.1.1d/libcrypto.a" "build/$ARCH/libcrypto.a" +done + + +ARCH_COUNT=( $ARCHS ) +ARCH_COUNT=${#ARCH_COUNT[@]} +if [[ $ARCH_COUNT -gt 1 ]] ; then +LIBSSLA="" +LIBCRYPTO="" +mkdir -p ${BUILD_DIR}build/openssl/lib +mv $SOURCE_DIR/include ${BUILD_DIR}build/openssl/include +for ARCH in $ARCHS +do +LIBSSLA="$LIBSSLA ${BUILD_DIR}build/$ARCH/libssl.a" +LIBCRYPTO="$LIBCRYPTO ${BUILD_DIR}build/$ARCH/libcrypto.a" +done +lipo -create -output ${BUILD_DIR}build/openssl/lib/libssl.a $LIBSSLA +lipo -create -output ${BUILD_DIR}build/openssl/lib/libcrypto.a $LIBCRYPTO +else +mv "${BUILD_DIR}build/$ARCHS/libssl.a" "${BUILD_DIR}build/libssl.a" +mv "${BUILD_DIR}build/$ARCHS/libcrypto.a" "${BUILD_DIR}build/libcrypto.a" +fi + + +#cp -r "${TMP_DIR}/$ARCH/include" "${TMP_DIR}/" +#if [ "$ARCH" == "arm64" ]; then +# patch -p3 "${TMP_DIR}/include/openssl/opensslconf.h" < "$SRC_DIR/patch-include.patch" || exit 1 +#fi +# +#DFT_DIST_DIR="$OUT_DIR/out" +#rm -rf "$DFT_DIST_DIR" +#mkdir -p "$DFT_DIST_DIR" +# +#DIST_DIR="${DIST_DIR:-$DFT_DIST_DIR}" +#mkdir -p "${DIST_DIR}" +#cp -r "${TMP_DIR}/include" "${TMP_DIR}/$ARCH/lib" "${DIST_DIR}" diff --git a/core-xprojects/OpenSSLEncryption/OpenSSLEncryption/patch-conf.diff b/core-xprojects/OpenSSLEncryption/OpenSSLEncryption/patch-conf.diff new file mode 100644 index 0000000000..0be5f97e44 --- /dev/null +++ b/core-xprojects/OpenSSLEncryption/OpenSSLEncryption/patch-conf.diff @@ -0,0 +1,27 @@ +diff --git a/Configurations/10-main.conf b/Configurations/10-main.conf +index c9e1a46..a94d8ea 100644 +--- a/Configurations/10-main.conf ++++ b/Configurations/10-main.conf +@@ -1551,13 +1551,21 @@ my %targets = ( + perlasm_scheme => "macosx", + }, + "darwin64-x86_64-cc" => { +- inherit_from => [ "darwin-common", asm("x86_64_asm") ], ++ inherit_from => [ "darwin-common", asm("no_asm") ], + CFLAGS => add("-Wall"), + cflags => add("-arch x86_64"), + lib_cppflags => add("-DL_ENDIAN"), + bn_ops => "SIXTY_FOUR_BIT_LONG", + perlasm_scheme => "macosx", + }, ++ "darwin64-arm64-cc" => { ++ inherit_from => [ "darwin-common", asm("no_asm") ], ++ CFLAGS => add("-Wall"), ++ cflags => add("-arch arm64"), ++ lib_cppflags => add("-DL_ENDIAN"), ++ bn_ops => "SIXTY_FOUR_BIT_LONG", ++ perlasm_scheme => "macosx", ++ }, + + ##### GNU Hurd + "hurd-x86" => { diff --git a/core-xprojects/OpenSSLEncryption/OpenSSLEncryption_Xcode.xcodeproj/project.pbxproj b/core-xprojects/OpenSSLEncryption/OpenSSLEncryption_Xcode.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..a478a79508 --- /dev/null +++ b/core-xprojects/OpenSSLEncryption/OpenSSLEncryption_Xcode.xcodeproj/project.pbxproj @@ -0,0 +1,764 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + A790A2DD236AFCEF000451B5 /* OpenSSLEncryption.h in Headers */ = {isa = PBXBuildFile; fileRef = A790A2CF236AFCEF000451B5 /* OpenSSLEncryption.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A790A2E9236AFE3D000451B5 /* OpenSSLEncryptionProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = A790A2E7236AFE3D000451B5 /* OpenSSLEncryptionProvider.m */; }; + A790A2ED236AFE7C000451B5 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A790A2EC236AFE7C000451B5 /* Foundation.framework */; }; + A790A30F236B097D000451B5 /* EncryptionProvider.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A790A2FF236B0188000451B5 /* EncryptionProvider.framework */; }; + A791912C240CFDB6002011CA /* OpenSSLEncryptionProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = A791912B240CFDB6002011CA /* OpenSSLEncryptionProvider.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0076EAE25685957007EF588 /* libcrypto.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0983AD72565B0DD00467703 /* libcrypto.a */; }; + D0076EAF25685957007EF588 /* libssl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0983AD62565B0DD00467703 /* libssl.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A790A2CC236AFCEF000451B5 /* OpenSSLEncryption.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OpenSSLEncryption.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A790A2CF236AFCEF000451B5 /* OpenSSLEncryption.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OpenSSLEncryption.h; sourceTree = ""; }; + A790A2D0236AFCEF000451B5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A790A2E7236AFE3D000451B5 /* OpenSSLEncryptionProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OpenSSLEncryptionProvider.m; sourceTree = ""; }; + A790A2EC236AFE7C000451B5 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + A790A2FF236B0188000451B5 /* EncryptionProvider.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = EncryptionProvider.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A790A301236B027F000451B5 /* libcrypto.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libcrypto.a; path = "../../thrid-party/openssl/lib/libcrypto.a"; sourceTree = ""; }; + A791912B240CFDB6002011CA /* OpenSSLEncryptionProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OpenSSLEncryptionProvider.h; path = ../PublicHeaders/OpenSSLEncryptionProvider/OpenSSLEncryptionProvider.h; sourceTree = ""; }; + D06E38B424A5F6F800C7D03A /* libssl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libssl.a; path = "../../thrid-party/openssl/lib/libssl.a"; sourceTree = ""; }; + D0983AD62565B0DD00467703 /* libssl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libssl.a; path = build/openssl/lib/libssl.a; sourceTree = ""; }; + D0983AD72565B0DD00467703 /* libcrypto.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libcrypto.a; path = build/openssl/lib/libcrypto.a; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A790A2C9236AFCEF000451B5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A790A30F236B097D000451B5 /* EncryptionProvider.framework in Frameworks */, + A790A2ED236AFE7C000451B5 /* Foundation.framework in Frameworks */, + D0076EAE25685957007EF588 /* libcrypto.a in Frameworks */, + D0076EAF25685957007EF588 /* libssl.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A790A2C2236AFCEF000451B5 = { + isa = PBXGroup; + children = ( + A790A2E6236AFDFB000451B5 /* Sources */, + A790A2CE236AFCEF000451B5 /* OpenSSLEncryption */, + A790A2CD236AFCEF000451B5 /* Products */, + A790A2EB236AFE7C000451B5 /* Frameworks */, + ); + sourceTree = ""; + }; + A790A2CD236AFCEF000451B5 /* Products */ = { + isa = PBXGroup; + children = ( + A790A2CC236AFCEF000451B5 /* OpenSSLEncryption.framework */, + ); + name = Products; + sourceTree = ""; + }; + A790A2CE236AFCEF000451B5 /* OpenSSLEncryption */ = { + isa = PBXGroup; + children = ( + A790A2CF236AFCEF000451B5 /* OpenSSLEncryption.h */, + A790A2D0236AFCEF000451B5 /* Info.plist */, + ); + path = OpenSSLEncryption; + sourceTree = ""; + }; + A790A2E6236AFDFB000451B5 /* Sources */ = { + isa = PBXGroup; + children = ( + A791912B240CFDB6002011CA /* OpenSSLEncryptionProvider.h */, + A790A2E7236AFE3D000451B5 /* OpenSSLEncryptionProvider.m */, + ); + name = Sources; + path = "../../submodules/telegram-ios/submodules/OpenSSLEncryptionProvider/Sources"; + sourceTree = ""; + }; + A790A2EB236AFE7C000451B5 /* Frameworks */ = { + isa = PBXGroup; + children = ( + D0983AD72565B0DD00467703 /* libcrypto.a */, + D06E38B424A5F6F800C7D03A /* libssl.a */, + A790A301236B027F000451B5 /* libcrypto.a */, + D0983AD62565B0DD00467703 /* libssl.a */, + A790A2FF236B0188000451B5 /* EncryptionProvider.framework */, + A790A2EC236AFE7C000451B5 /* Foundation.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + A790A2C7236AFCEF000451B5 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + A790A2DD236AFCEF000451B5 /* OpenSSLEncryption.h in Headers */, + A791912C240CFDB6002011CA /* OpenSSLEncryptionProvider.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + A790A2CB236AFCEF000451B5 /* OpenSSLEncryption */ = { + isa = PBXNativeTarget; + buildConfigurationList = A790A2E0236AFCEF000451B5 /* Build configuration list for PBXNativeTarget "OpenSSLEncryption" */; + buildPhases = ( + D0983AAC2565843400467703 /* Run Script */, + A790A2C7236AFCEF000451B5 /* Headers */, + A790A2C8236AFCEF000451B5 /* Sources */, + A790A2C9236AFCEF000451B5 /* Frameworks */, + A790A2CA236AFCEF000451B5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = OpenSSLEncryption; + productName = OpenSSLEncryption; + productReference = A790A2CC236AFCEF000451B5 /* OpenSSLEncryption.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A790A2C3236AFCEF000451B5 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1030; + ORGANIZATIONNAME = Telegram; + TargetAttributes = { + A790A2CB236AFCEF000451B5 = { + CreatedOnToolsVersion = 10.3; + }; + }; + }; + buildConfigurationList = A790A2C6236AFCEF000451B5 /* Build configuration list for PBXProject "OpenSSLEncryption_Xcode" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = A790A2C2236AFCEF000451B5; + productRefGroup = A790A2CD236AFCEF000451B5 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A790A2CB236AFCEF000451B5 /* OpenSSLEncryption */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A790A2CA236AFCEF000451B5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + D0983AAC2565843400467703 /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ ! -d \"$PROJECT_DIR/build\" ]; then\n sh $PROJECT_DIR/OpenSSLEncryption/build.sh ../../submodules/telegram-ios/submodules/openssl\nfi\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A790A2C8236AFCEF000451B5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A790A2E9236AFE3D000451B5 /* OpenSSLEncryptionProvider.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A790A2F1236B00F1000451B5 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + A790A2F2236B00F1000451B5 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + HEADER_SEARCH_PATHS = ( + "$(PROJECT_DIR)/build/openssl/include", + "$(PROJECT_DIR)/../../submodules/telegram-ios/submodules/OpenSSLEncryptionProvider/PublicHeaders", + ); + INFOPLIST_FILE = OpenSSLEncryption/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/build/openssl/lib"; + MACH_O_TYPE = staticlib; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.OpenSSLEncryption; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = DebugHockeyapp; + }; + A790A2F3236B00F8000451B5 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + A790A2F4236B00F8000451B5 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_OPTIMIZATION_LEVEL = 0; + HEADER_SEARCH_PATHS = ( + "$(PROJECT_DIR)/build/openssl/include", + "$(PROJECT_DIR)/../../submodules/telegram-ios/submodules/OpenSSLEncryptionProvider/PublicHeaders", + ); + INFOPLIST_FILE = OpenSSLEncryption/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/build/openssl/lib"; + MACH_O_TYPE = staticlib; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.OpenSSLEncryption; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = DebugAppStore; + }; + A790A2F5236B00FD000451B5 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + A790A2F6236B00FD000451B5 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + HEADER_SEARCH_PATHS = ( + "$(PROJECT_DIR)/build/openssl/include", + "$(PROJECT_DIR)/../../submodules/telegram-ios/submodules/OpenSSLEncryptionProvider/PublicHeaders", + ); + INFOPLIST_FILE = OpenSSLEncryption/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/build/openssl/lib"; + MACH_O_TYPE = staticlib; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.OpenSSLEncryption; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = HockeyappMacAlpha; + }; + A790A2F9236B010A000451B5 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + A790A2FA236B010A000451B5 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + HEADER_SEARCH_PATHS = ( + "$(PROJECT_DIR)/build/openssl/include", + "$(PROJECT_DIR)/../../submodules/telegram-ios/submodules/OpenSSLEncryptionProvider/PublicHeaders", + ); + INFOPLIST_FILE = OpenSSLEncryption/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/build/openssl/lib"; + MACH_O_TYPE = staticlib; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.OpenSSLEncryption; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = ReleaseAppStore; + }; + A790A2FB236B0110000451B5 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + A790A2FC236B0110000451B5 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + HEADER_SEARCH_PATHS = ( + "$(PROJECT_DIR)/build/openssl/include", + "$(PROJECT_DIR)/../../submodules/telegram-ios/submodules/OpenSSLEncryptionProvider/PublicHeaders", + ); + INFOPLIST_FILE = OpenSSLEncryption/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/build/openssl/lib"; + MACH_O_TYPE = staticlib; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.OpenSSLEncryption; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = ReleaseHockeyapp; + }; + A7F282CE238EAAFD00742C20 /* Github */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + A7F282CF238EAAFD00742C20 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_OPTIMIZATION_LEVEL = 0; + HEADER_SEARCH_PATHS = ( + "$(PROJECT_DIR)/build/openssl/include", + "$(PROJECT_DIR)/../../submodules/telegram-ios/submodules/OpenSSLEncryptionProvider/PublicHeaders", + ); + INFOPLIST_FILE = OpenSSLEncryption/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/build/openssl/lib"; + MACH_O_TYPE = staticlib; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.OpenSSLEncryption; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = Github; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A790A2C6236AFCEF000451B5 /* Build configuration list for PBXProject "OpenSSLEncryption_Xcode" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A790A2F1236B00F1000451B5 /* DebugHockeyapp */, + A790A2F3236B00F8000451B5 /* DebugAppStore */, + A7F282CE238EAAFD00742C20 /* Github */, + A790A2F5236B00FD000451B5 /* HockeyappMacAlpha */, + A790A2F9236B010A000451B5 /* ReleaseAppStore */, + A790A2FB236B0110000451B5 /* ReleaseHockeyapp */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = DebugHockeyapp; + }; + A790A2E0236AFCEF000451B5 /* Build configuration list for PBXNativeTarget "OpenSSLEncryption" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A790A2F2236B00F1000451B5 /* DebugHockeyapp */, + A790A2F4236B00F8000451B5 /* DebugAppStore */, + A7F282CF238EAAFD00742C20 /* Github */, + A790A2F6236B00FD000451B5 /* HockeyappMacAlpha */, + A790A2FA236B010A000451B5 /* ReleaseAppStore */, + A790A2FC236B0110000451B5 /* ReleaseHockeyapp */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = DebugHockeyapp; + }; +/* End XCConfigurationList section */ + }; + rootObject = A790A2C3236AFCEF000451B5 /* Project object */; +} diff --git a/core-xprojects/OpenSSLEncryption/OpenSSLEncryption_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/OpenSSLEncryption/OpenSSLEncryption_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..20bdb16ff2 --- /dev/null +++ b/core-xprojects/OpenSSLEncryption/OpenSSLEncryption_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/OpenSSLEncryption/OpenSSLEncryption_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/OpenSSLEncryption/OpenSSLEncryption_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/OpenSSLEncryption/OpenSSLEncryption_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/OpenSSLEncryption/OpenSSLEncryption_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate b/core-xprojects/OpenSSLEncryption/OpenSSLEncryption_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000..676152aac0 Binary files /dev/null and b/core-xprojects/OpenSSLEncryption/OpenSSLEncryption_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/core-xprojects/OpenSSLEncryption/OpenSSLEncryption_Xcode.xcodeproj/xcshareddata/xcschemes/OpenSSLEncryption.xcscheme b/core-xprojects/OpenSSLEncryption/OpenSSLEncryption_Xcode.xcodeproj/xcshareddata/xcschemes/OpenSSLEncryption.xcscheme new file mode 100644 index 0000000000..e5c2be4357 --- /dev/null +++ b/core-xprojects/OpenSSLEncryption/OpenSSLEncryption_Xcode.xcodeproj/xcshareddata/xcschemes/OpenSSLEncryption.xcscheme @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/OpenSSLEncryption/OpenSSLEncryption_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/OpenSSLEncryption.xcscheme b/core-xprojects/OpenSSLEncryption/OpenSSLEncryption_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/OpenSSLEncryption.xcscheme new file mode 100644 index 0000000000..0a27458bf4 --- /dev/null +++ b/core-xprojects/OpenSSLEncryption/OpenSSLEncryption_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/OpenSSLEncryption.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/OpenSSLEncryption/OpenSSLEncryption_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist b/core-xprojects/OpenSSLEncryption/OpenSSLEncryption_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000000..108e8fd41c --- /dev/null +++ b/core-xprojects/OpenSSLEncryption/OpenSSLEncryption_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,29 @@ + + + + + SchemeUserState + + OpenSSLEncryption.xcscheme + + isShown + + orderHint + 20 + + OpenSSLEncryption.xcscheme_^#shared#^_ + + orderHint + 26 + + + SuppressBuildableAutocreation + + A790A2CB236AFCEF000451B5 + + primary + + + + + diff --git a/core-xprojects/OpusBinding/OpusBinding.xcodeproj/project.pbxproj b/core-xprojects/OpusBinding/OpusBinding.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..ab768107a8 --- /dev/null +++ b/core-xprojects/OpusBinding/OpusBinding.xcodeproj/project.pbxproj @@ -0,0 +1,873 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + D05F623E25694EB200CC7EFA /* libopus.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0983CF225681EDA00467703 /* libopus.framework */; }; + D0983C9B25681C5100467703 /* ogg.h in Headers */ = {isa = PBXBuildFile; fileRef = D0983C8325681C5000467703 /* ogg.h */; }; + D0983C9C25681C5100467703 /* framing.c in Sources */ = {isa = PBXBuildFile; fileRef = D0983C8425681C5000467703 /* framing.c */; }; + D0983C9D25681C5100467703 /* os_types.h in Headers */ = {isa = PBXBuildFile; fileRef = D0983C8525681C5000467703 /* os_types.h */; }; + D0983C9E25681C5100467703 /* bitwise.c in Sources */ = {isa = PBXBuildFile; fileRef = D0983C8625681C5000467703 /* bitwise.c */; }; + D0983CA025681C5100467703 /* picture.h in Headers */ = {isa = PBXBuildFile; fileRef = D0983C8925681C5000467703 /* picture.h */; }; + D0983CA125681C5100467703 /* opusenc.m in Sources */ = {isa = PBXBuildFile; fileRef = D0983C8A25681C5100467703 /* opusenc.m */; }; + D0983CA225681C5100467703 /* opus_header.c in Sources */ = {isa = PBXBuildFile; fileRef = D0983C8B25681C5100467703 /* opus_header.c */; }; + D0983CA325681C5100467703 /* wav_io.c in Sources */ = {isa = PBXBuildFile; fileRef = D0983C8C25681C5100467703 /* wav_io.c */; }; + D0983CA425681C5100467703 /* diag_range.h in Headers */ = {isa = PBXBuildFile; fileRef = D0983C8D25681C5100467703 /* diag_range.h */; }; + D0983CA525681C5100467703 /* picture.c in Sources */ = {isa = PBXBuildFile; fileRef = D0983C8E25681C5100467703 /* picture.c */; }; + D0983CA625681C5100467703 /* opus_header.h in Headers */ = {isa = PBXBuildFile; fileRef = D0983C8F25681C5100467703 /* opus_header.h */; }; + D0983CA725681C5100467703 /* wav_io.h in Headers */ = {isa = PBXBuildFile; fileRef = D0983C9025681C5100467703 /* wav_io.h */; }; + D0983CA825681C5100467703 /* diag_range.c in Sources */ = {isa = PBXBuildFile; fileRef = D0983C9125681C5100467703 /* diag_range.c */; }; + D0983CA925681C5100467703 /* OggOpusReader.m in Sources */ = {isa = PBXBuildFile; fileRef = D0983C9225681C5100467703 /* OggOpusReader.m */; }; + D0983CAA25681C5100467703 /* internal.h in Headers */ = {isa = PBXBuildFile; fileRef = D0983C9425681C5100467703 /* internal.h */; }; + D0983CAB25681C5100467703 /* opusfile.c in Sources */ = {isa = PBXBuildFile; fileRef = D0983C9525681C5100467703 /* opusfile.c */; }; + D0983CAC25681C5100467703 /* info.c in Sources */ = {isa = PBXBuildFile; fileRef = D0983C9625681C5100467703 /* info.c */; }; + D0983CAD25681C5100467703 /* internal.c in Sources */ = {isa = PBXBuildFile; fileRef = D0983C9725681C5100467703 /* internal.c */; }; + D0983CAE25681C5100467703 /* opusfile.h in Headers */ = {isa = PBXBuildFile; fileRef = D0983C9825681C5100467703 /* opusfile.h */; }; + D0983CAF25681C5100467703 /* stream.c in Sources */ = {isa = PBXBuildFile; fileRef = D0983C9925681C5100467703 /* stream.c */; }; + D0983CB025681C5100467703 /* TGDataItem.m in Sources */ = {isa = PBXBuildFile; fileRef = D0983C9A25681C5100467703 /* TGDataItem.m */; }; + D0983CBC25681D3700467703 /* TGOggOpusWriter.h in Headers */ = {isa = PBXBuildFile; fileRef = D0983CB825681D3700467703 /* TGOggOpusWriter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0983CBD25681D3700467703 /* OpusBinding.h in Headers */ = {isa = PBXBuildFile; fileRef = D0983CB925681D3700467703 /* OpusBinding.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0983CBE25681D3700467703 /* TGDataItem.h in Headers */ = {isa = PBXBuildFile; fileRef = D0983CBA25681D3700467703 /* TGDataItem.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0983CBF25681D3700467703 /* OggOpusReader.h in Headers */ = {isa = PBXBuildFile; fileRef = D0983CBB25681D3700467703 /* OggOpusReader.h */; settings = {ATTRIBUTES = (Public, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + D0983BE42566B2BC00467703 /* OpusBinding.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OpusBinding.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0983BE82566B2BC00467703 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D0983C8325681C5000467703 /* ogg.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ogg.h; sourceTree = ""; }; + D0983C8425681C5000467703 /* framing.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = framing.c; sourceTree = ""; }; + D0983C8525681C5000467703 /* os_types.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = os_types.h; sourceTree = ""; }; + D0983C8625681C5000467703 /* bitwise.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = bitwise.c; sourceTree = ""; }; + D0983C8925681C5000467703 /* picture.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = picture.h; sourceTree = ""; }; + D0983C8A25681C5100467703 /* opusenc.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = opusenc.m; sourceTree = ""; }; + D0983C8B25681C5100467703 /* opus_header.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = opus_header.c; sourceTree = ""; }; + D0983C8C25681C5100467703 /* wav_io.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = wav_io.c; sourceTree = ""; }; + D0983C8D25681C5100467703 /* diag_range.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = diag_range.h; sourceTree = ""; }; + D0983C8E25681C5100467703 /* picture.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = picture.c; sourceTree = ""; }; + D0983C8F25681C5100467703 /* opus_header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = opus_header.h; sourceTree = ""; }; + D0983C9025681C5100467703 /* wav_io.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = wav_io.h; sourceTree = ""; }; + D0983C9125681C5100467703 /* diag_range.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = diag_range.c; sourceTree = ""; }; + D0983C9225681C5100467703 /* OggOpusReader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OggOpusReader.m; sourceTree = ""; }; + D0983C9425681C5100467703 /* internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = internal.h; sourceTree = ""; }; + D0983C9525681C5100467703 /* opusfile.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = opusfile.c; sourceTree = ""; }; + D0983C9625681C5100467703 /* info.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = info.c; sourceTree = ""; }; + D0983C9725681C5100467703 /* internal.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = internal.c; sourceTree = ""; }; + D0983C9825681C5100467703 /* opusfile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = opusfile.h; sourceTree = ""; }; + D0983C9925681C5100467703 /* stream.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = stream.c; sourceTree = ""; }; + D0983C9A25681C5100467703 /* TGDataItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TGDataItem.m; sourceTree = ""; }; + D0983CB825681D3700467703 /* TGOggOpusWriter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGOggOpusWriter.h; sourceTree = ""; }; + D0983CB925681D3700467703 /* OpusBinding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OpusBinding.h; sourceTree = ""; }; + D0983CBA25681D3700467703 /* TGDataItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGDataItem.h; sourceTree = ""; }; + D0983CBB25681D3700467703 /* OggOpusReader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OggOpusReader.h; sourceTree = ""; }; + D0983CC525681D8E00467703 /* libopus.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libopus.a; path = ../libopus/build/libopus/lib/libopus.a; sourceTree = ""; }; + D0983CF225681EDA00467703 /* libopus.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = libopus.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D0983BE12566B2BC00467703 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D05F623E25694EB200CC7EFA /* libopus.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + D0983BDA2566B2BC00467703 = { + isa = PBXGroup; + children = ( + D0983CB625681D3700467703 /* PublicHeaders */, + D0983C8125681C5000467703 /* Sources */, + D0983BE62566B2BC00467703 /* OpusBinding */, + D0983BE52566B2BC00467703 /* Products */, + D0983CC425681D8D00467703 /* Frameworks */, + ); + sourceTree = ""; + }; + D0983BE52566B2BC00467703 /* Products */ = { + isa = PBXGroup; + children = ( + D0983BE42566B2BC00467703 /* OpusBinding.framework */, + ); + name = Products; + sourceTree = ""; + }; + D0983BE62566B2BC00467703 /* OpusBinding */ = { + isa = PBXGroup; + children = ( + D0983BE82566B2BC00467703 /* Info.plist */, + ); + path = OpusBinding; + sourceTree = ""; + }; + D0983C8125681C5000467703 /* Sources */ = { + isa = PBXGroup; + children = ( + D0983C8225681C5000467703 /* ogg */, + D0983C8825681C5000467703 /* opusenc */, + D0983C9225681C5100467703 /* OggOpusReader.m */, + D0983C9325681C5100467703 /* opusfile */, + D0983C9A25681C5100467703 /* TGDataItem.m */, + ); + name = Sources; + path = "../../submodules/telegram-ios/submodules/OpusBinding/Sources"; + sourceTree = ""; + }; + D0983C8225681C5000467703 /* ogg */ = { + isa = PBXGroup; + children = ( + D0983C8325681C5000467703 /* ogg.h */, + D0983C8425681C5000467703 /* framing.c */, + D0983C8525681C5000467703 /* os_types.h */, + D0983C8625681C5000467703 /* bitwise.c */, + ); + path = ogg; + sourceTree = ""; + }; + D0983C8825681C5000467703 /* opusenc */ = { + isa = PBXGroup; + children = ( + D0983C8925681C5000467703 /* picture.h */, + D0983C8A25681C5100467703 /* opusenc.m */, + D0983C8B25681C5100467703 /* opus_header.c */, + D0983C8C25681C5100467703 /* wav_io.c */, + D0983C8D25681C5100467703 /* diag_range.h */, + D0983C8E25681C5100467703 /* picture.c */, + D0983C8F25681C5100467703 /* opus_header.h */, + D0983C9025681C5100467703 /* wav_io.h */, + D0983C9125681C5100467703 /* diag_range.c */, + ); + path = opusenc; + sourceTree = ""; + }; + D0983C9325681C5100467703 /* opusfile */ = { + isa = PBXGroup; + children = ( + D0983C9425681C5100467703 /* internal.h */, + D0983C9525681C5100467703 /* opusfile.c */, + D0983C9625681C5100467703 /* info.c */, + D0983C9725681C5100467703 /* internal.c */, + D0983C9825681C5100467703 /* opusfile.h */, + D0983C9925681C5100467703 /* stream.c */, + ); + path = opusfile; + sourceTree = ""; + }; + D0983CB625681D3700467703 /* PublicHeaders */ = { + isa = PBXGroup; + children = ( + D0983CB725681D3700467703 /* OpusBinding */, + ); + name = PublicHeaders; + path = "../../submodules/telegram-ios/submodules/OpusBinding/PublicHeaders"; + sourceTree = ""; + }; + D0983CB725681D3700467703 /* OpusBinding */ = { + isa = PBXGroup; + children = ( + D0983CB825681D3700467703 /* TGOggOpusWriter.h */, + D0983CB925681D3700467703 /* OpusBinding.h */, + D0983CBA25681D3700467703 /* TGDataItem.h */, + D0983CBB25681D3700467703 /* OggOpusReader.h */, + ); + path = OpusBinding; + sourceTree = ""; + }; + D0983CC425681D8D00467703 /* Frameworks */ = { + isa = PBXGroup; + children = ( + D0983CF225681EDA00467703 /* libopus.framework */, + D0983CC525681D8E00467703 /* libopus.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + D0983BDF2566B2BC00467703 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D0983CBC25681D3700467703 /* TGOggOpusWriter.h in Headers */, + D0983CBD25681D3700467703 /* OpusBinding.h in Headers */, + D0983CBE25681D3700467703 /* TGDataItem.h in Headers */, + D0983CBF25681D3700467703 /* OggOpusReader.h in Headers */, + D0983C9B25681C5100467703 /* ogg.h in Headers */, + D0983CA625681C5100467703 /* opus_header.h in Headers */, + D0983CAA25681C5100467703 /* internal.h in Headers */, + D0983CA725681C5100467703 /* wav_io.h in Headers */, + D0983C9D25681C5100467703 /* os_types.h in Headers */, + D0983CA025681C5100467703 /* picture.h in Headers */, + D0983CA425681C5100467703 /* diag_range.h in Headers */, + D0983CAE25681C5100467703 /* opusfile.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + D0983BE32566B2BC00467703 /* OpusBinding */ = { + isa = PBXNativeTarget; + buildConfigurationList = D0983BEC2566B2BC00467703 /* Build configuration list for PBXNativeTarget "OpusBinding" */; + buildPhases = ( + D0983BDF2566B2BC00467703 /* Headers */, + D0983BE02566B2BC00467703 /* Sources */, + D0983BE12566B2BC00467703 /* Frameworks */, + D0983BE22566B2BC00467703 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = OpusBinding; + productName = OpusBindings; + productReference = D0983BE42566B2BC00467703 /* OpusBinding.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D0983BDB2566B2BC00467703 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1220; + TargetAttributes = { + D0983BE32566B2BC00467703 = { + CreatedOnToolsVersion = 12.2; + }; + }; + }; + buildConfigurationList = D0983BDE2566B2BC00467703 /* Build configuration list for PBXProject "OpusBinding" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = D0983BDA2566B2BC00467703; + productRefGroup = D0983BE52566B2BC00467703 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D0983BE32566B2BC00467703 /* OpusBinding */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D0983BE22566B2BC00467703 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D0983BE02566B2BC00467703 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D0983CA825681C5100467703 /* diag_range.c in Sources */, + D0983CAD25681C5100467703 /* internal.c in Sources */, + D0983CA925681C5100467703 /* OggOpusReader.m in Sources */, + D0983C9C25681C5100467703 /* framing.c in Sources */, + D0983CA225681C5100467703 /* opus_header.c in Sources */, + D0983CB025681C5100467703 /* TGDataItem.m in Sources */, + D0983CAF25681C5100467703 /* stream.c in Sources */, + D0983CA525681C5100467703 /* picture.c in Sources */, + D0983CAC25681C5100467703 /* info.c in Sources */, + D0983CA125681C5100467703 /* opusenc.m in Sources */, + D0983CA325681C5100467703 /* wav_io.c in Sources */, + D0983C9E25681C5100467703 /* bitwise.c in Sources */, + D0983CAB25681C5100467703 /* opusfile.c in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + D0983BEA2566B2BC00467703 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + D0983BEB2566B2BC00467703 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + D0983BED2566B2BC00467703 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = ( + "$(PROJECT_DIR)/../libopus/build/libopus/include", + "$(PROJECT_DIR)/../../submodules/telegram-ios/submodules/OpusBinding/Sources", + ); + INFOPLIST_FILE = OpusBinding/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.OpusBinding; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = DebugAppStore; + }; + D0983BEE2566B2BC00467703 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = ( + "$(PROJECT_DIR)/../libopus/build/libopus/include", + "$(PROJECT_DIR)/../../submodules/telegram-ios/submodules/OpusBinding/Sources", + ); + INFOPLIST_FILE = OpusBinding/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.OpusBinding; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = ReleaseAppStore; + }; + D0983C7925681B2900467703 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + D0983C7A25681B2900467703 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = ( + "$(PROJECT_DIR)/../libopus/build/libopus/include", + "$(PROJECT_DIR)/../../submodules/telegram-ios/submodules/OpusBinding/Sources", + ); + INFOPLIST_FILE = OpusBinding/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.OpusBinding; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = ReleaseHockeyapp; + }; + D0983C7B25681B2F00467703 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + D0983C7C25681B2F00467703 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = ( + "$(PROJECT_DIR)/../libopus/build/libopus/include", + "$(PROJECT_DIR)/../../submodules/telegram-ios/submodules/OpusBinding/Sources", + ); + INFOPLIST_FILE = OpusBinding/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.OpusBinding; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = DebugHockeyapp; + }; + D0983C7D25681B3500467703 /* Github */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + D0983C7E25681B3500467703 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = ( + "$(PROJECT_DIR)/../libopus/build/libopus/include", + "$(PROJECT_DIR)/../../submodules/telegram-ios/submodules/OpusBinding/Sources", + ); + INFOPLIST_FILE = OpusBinding/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.OpusBinding; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = Github; + }; + D0983C7F25681B3900467703 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + D0983C8025681B3900467703 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = ( + "$(PROJECT_DIR)/../libopus/build/libopus/include", + "$(PROJECT_DIR)/../../submodules/telegram-ios/submodules/OpusBinding/Sources", + ); + INFOPLIST_FILE = OpusBinding/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.OpusBinding; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = HockeyappMacAlpha; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D0983BDE2566B2BC00467703 /* Build configuration list for PBXProject "OpusBinding" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0983BEA2566B2BC00467703 /* DebugAppStore */, + D0983C7D25681B3500467703 /* Github */, + D0983BEB2566B2BC00467703 /* ReleaseAppStore */, + D0983C7925681B2900467703 /* ReleaseHockeyapp */, + D0983C7B25681B2F00467703 /* DebugHockeyapp */, + D0983C7F25681B3900467703 /* HockeyappMacAlpha */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; + D0983BEC2566B2BC00467703 /* Build configuration list for PBXNativeTarget "OpusBinding" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0983BED2566B2BC00467703 /* DebugAppStore */, + D0983C7E25681B3500467703 /* Github */, + D0983BEE2566B2BC00467703 /* ReleaseAppStore */, + D0983C7A25681B2900467703 /* ReleaseHockeyapp */, + D0983C7C25681B2F00467703 /* DebugHockeyapp */, + D0983C8025681B3900467703 /* HockeyappMacAlpha */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; +/* End XCConfigurationList section */ + }; + rootObject = D0983BDB2566B2BC00467703 /* Project object */; +} diff --git a/core-xprojects/OpusBinding/OpusBinding.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/OpusBinding/OpusBinding.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/core-xprojects/OpusBinding/OpusBinding.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/OpusBinding/OpusBinding.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/OpusBinding/OpusBinding.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/OpusBinding/OpusBinding.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/OpusBinding/OpusBinding/Info.plist b/core-xprojects/OpusBinding/OpusBinding/Info.plist new file mode 100644 index 0000000000..9bcb244429 --- /dev/null +++ b/core-xprojects/OpusBinding/OpusBinding/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/TGUIKit/TGUIKit/Info.plist b/core-xprojects/Postbox/Postbox/Info.plist similarity index 100% rename from TGUIKit/TGUIKit/Info.plist rename to core-xprojects/Postbox/Postbox/Info.plist diff --git a/core-xprojects/Postbox/Postbox/Postbox.h b/core-xprojects/Postbox/Postbox/Postbox.h new file mode 100644 index 0000000000..6fc344bb31 --- /dev/null +++ b/core-xprojects/Postbox/Postbox/Postbox.h @@ -0,0 +1,18 @@ +// +// PostboxMac.h +// PostboxMac +// +// Created by Peter on 9/5/16. +// Copyright © 2016 Telegram. All rights reserved. +// + +#import + +//! Project version number for PostboxMac. +FOUNDATION_EXPORT double PostboxVersionNumber; + +//! Project version string for PostboxMac. +FOUNDATION_EXPORT const unsigned char PostboxVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + diff --git a/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/project.pbxproj b/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..a13914ea78 --- /dev/null +++ b/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/project.pbxproj @@ -0,0 +1,1578 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 27E2851826BBE0C900219927 /* TimestampBasedMessageAttributesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2845026BBE0C900219927 /* TimestampBasedMessageAttributesTable.swift */; }; + 27E2851926BBE0C900219927 /* ItemCacheMetaTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2845126BBE0C900219927 /* ItemCacheMetaTable.swift */; }; + 27E2851A26BBE0C900219927 /* PeerReadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2845226BBE0C900219927 /* PeerReadState.swift */; }; + 27E2851B26BBE0C900219927 /* Upgrades.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2845326BBE0C900219927 /* Upgrades.swift */; }; + 27E2851C26BBE0C900219927 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2845426BBE0C900219927 /* Coding.swift */; }; + 27E2851D26BBE0C900219927 /* UnsentMessageIndicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2845526BBE0C900219927 /* UnsentMessageIndicesView.swift */; }; + 27E2851E26BBE0C900219927 /* ReverseAssociatedPeerTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2845626BBE0C900219927 /* ReverseAssociatedPeerTable.swift */; }; + 27E2851F26BBE0C900219927 /* PostboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2845726BBE0C900219927 /* PostboxView.swift */; }; + 27E2852026BBE0C900219927 /* InvalidatedMessageHistoryTagSummariesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2845826BBE0C900219927 /* InvalidatedMessageHistoryTagSummariesView.swift */; }; + 27E2852126BBE0C900219927 /* MessageHistoryHoleIndexTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2845926BBE0C900219927 /* MessageHistoryHoleIndexTable.swift */; }; + 27E2852226BBE0C900219927 /* LocalMessageHistoryTagsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2845A26BBE0C900219927 /* LocalMessageHistoryTagsTable.swift */; }; + 27E2852326BBE0C900219927 /* SeedConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2845B26BBE0C900219927 /* SeedConfiguration.swift */; }; + 27E2852426BBE0C900219927 /* CachedPeerData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2845C26BBE0C900219927 /* CachedPeerData.swift */; }; + 27E2852526BBE0C900219927 /* MediaResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2845D26BBE0C900219927 /* MediaResource.swift */; }; + 27E2852626BBE0C900219927 /* Hash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2845E26BBE0C900219927 /* Hash.swift */; }; + 27E2852726BBE0C900219927 /* InvalidatedGroupMessageStatsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2845F26BBE0C900219927 /* InvalidatedGroupMessageStatsTable.swift */; }; + 27E2852826BBE0C900219927 /* ViewTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2846026BBE0C900219927 /* ViewTracker.swift */; }; + 27E2852926BBE0C900219927 /* ValueBoxKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2846126BBE0C900219927 /* ValueBoxKey.swift */; }; + 27E2852A26BBE0C900219927 /* AdditionalChatListItemsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2846226BBE0C900219927 /* AdditionalChatListItemsTable.swift */; }; + 27E2852B26BBE0C900219927 /* SimpleSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2846326BBE0C900219927 /* SimpleSet.swift */; }; + 27E2852C26BBE0C900219927 /* PostboxUpgrade_20to21.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2846426BBE0C900219927 /* PostboxUpgrade_20to21.swift */; }; + 27E2852D26BBE0C900219927 /* PeerNotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2846526BBE0C900219927 /* PeerNotificationSettings.swift */; }; + 27E2852E26BBE0C900219927 /* ValueBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2846626BBE0C900219927 /* ValueBox.swift */; }; + 27E2852F26BBE0C900219927 /* ChatListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2846726BBE0C900219927 /* ChatListView.swift */; }; + 27E2853026BBE0C900219927 /* PeerOperationLogTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2846826BBE0C900219927 /* PeerOperationLogTable.swift */; }; + 27E2853126BBE0C900219927 /* MutablePeerChatInclusionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2846926BBE0C900219927 /* MutablePeerChatInclusionView.swift */; }; + 27E2853226BBE0C900219927 /* PostboxUpgrade_19to20.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2846A26BBE0C900219927 /* PostboxUpgrade_19to20.swift */; }; + 27E2853326BBE0C900219927 /* AccountManagerAtomicState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2846B26BBE0C900219927 /* AccountManagerAtomicState.swift */; }; + 27E2853426BBE0C900219927 /* CachedPeerDataTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2846C26BBE0C900219927 /* CachedPeerDataTable.swift */; }; + 27E2853526BBE0C900219927 /* PostboxUpgrade_21to22.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2846D26BBE0C900219927 /* PostboxUpgrade_21to22.swift */; }; + 27E2853626BBE0C900219927 /* PeerChatTopIndexableMessageIds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2846E26BBE0C900219927 /* PeerChatTopIndexableMessageIds.swift */; }; + 27E2853726BBE0C900219927 /* PeerChatListInclusion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2846F26BBE0C900219927 /* PeerChatListInclusion.swift */; }; + 27E2853826BBE0C900219927 /* PostboxTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2847026BBE0C900219927 /* PostboxTransaction.swift */; }; + 27E2853926BBE0C900219927 /* ItemCollectionInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2847126BBE0C900219927 /* ItemCollectionInfoView.swift */; }; + 27E2853A26BBE0C900219927 /* TimestampBasedMessageAttributesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2847226BBE0C900219927 /* TimestampBasedMessageAttributesView.swift */; }; + 27E2853B26BBE0C900219927 /* MessageHistoryFailedTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2847326BBE0C900219927 /* MessageHistoryFailedTable.swift */; }; + 27E2853C26BBE0C900219927 /* PeerNotificationSettingsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2847426BBE0C900219927 /* PeerNotificationSettingsTable.swift */; }; + 27E2853D26BBE0C900219927 /* MediaBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2847526BBE0C900219927 /* MediaBox.swift */; }; + 27E2853E26BBE0C900219927 /* LocalMessageTagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2847626BBE0C900219927 /* LocalMessageTagsView.swift */; }; + 27E2853F26BBE0C900219927 /* AccountManagerRecordTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2847726BBE0C900219927 /* AccountManagerRecordTable.swift */; }; + 27E2854026BBE0C900219927 /* TopChatMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2847826BBE0C900219927 /* TopChatMessageView.swift */; }; + 27E2854126BBE0C900219927 /* MessageHistorySynchronizeReadStateTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2847926BBE0C900219927 /* MessageHistorySynchronizeReadStateTable.swift */; }; + 27E2854226BBE0C900219927 /* PendingMessageActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2847A26BBE0C900219927 /* PendingMessageActionsView.swift */; }; + 27E2854326BBE0C900219927 /* ItemCollectionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2847B26BBE0C900219927 /* ItemCollectionsView.swift */; }; + 27E2854426BBE0C900219927 /* PeerNotificationSettingsBehaviorTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2847C26BBE0C900219927 /* PeerNotificationSettingsBehaviorTable.swift */; }; + 27E2854526BBE0C900219927 /* AccountRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2847D26BBE0C900219927 /* AccountRecord.swift */; }; + 27E2854626BBE0C900219927 /* PeerGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2847E26BBE0C900219927 /* PeerGroup.swift */; }; + 27E2854726BBE0C900219927 /* NoticeTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2847F26BBE0C900219927 /* NoticeTable.swift */; }; + 27E2854826BBE0C900219927 /* InitialMessageHistoryData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2848026BBE0C900219927 /* InitialMessageHistoryData.swift */; }; + 27E2854926BBE0C900219927 /* IntermediateMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2848126BBE0C900219927 /* IntermediateMessage.swift */; }; + 27E2854A26BBE0C900219927 /* RedBlackTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2848226BBE0C900219927 /* RedBlackTree.swift */; }; + 27E2854B26BBE0C900219927 /* ContactPeersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2848326BBE0C900219927 /* ContactPeersView.swift */; }; + 27E2854C26BBE0C900219927 /* PeerChatInterfaceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2848426BBE0C900219927 /* PeerChatInterfaceState.swift */; }; + 27E2854D26BBE0C900219927 /* PostboxUpgrade_18to19.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2848526BBE0C900219927 /* PostboxUpgrade_18to19.swift */; }; + 27E2854E26BBE0C900219927 /* OrderedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2848626BBE0C900219927 /* OrderedList.swift */; }; + 27E2854F26BBE0C900219927 /* MessageHistoryOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2848726BBE0C900219927 /* MessageHistoryOperation.swift */; }; + 27E2855026BBE0C900219927 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2848826BBE0C900219927 /* PreferencesView.swift */; }; + 27E2855126BBE0C900219927 /* KeychainTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2848926BBE0C900219927 /* KeychainTable.swift */; }; + 27E2855226BBE0C900219927 /* ItemCollectionInfoTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2848A26BBE0C900219927 /* ItemCollectionInfoTable.swift */; }; + 27E2855326BBE0C900219927 /* MessageHistoryThreadsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2848B26BBE0C900219927 /* MessageHistoryThreadsTable.swift */; }; + 27E2855426BBE0C900219927 /* MessageHistoryTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2848C26BBE0C900219927 /* MessageHistoryTable.swift */; }; + 27E2855526BBE0C900219927 /* MessageHistoryIndexTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2848D26BBE0C900219927 /* MessageHistoryIndexTable.swift */; }; + 27E2855626BBE0C900219927 /* AccessChallengeDataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2848E26BBE0C900219927 /* AccessChallengeDataView.swift */; }; + 27E2855726BBE0C900219927 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2848F26BBE0C900219927 /* Message.swift */; }; + 27E2855826BBE0C900219927 /* PeerPresencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2849026BBE0C900219927 /* PeerPresencesView.swift */; }; + 27E2855926BBE0C900219927 /* ChatListTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2849126BBE0C900219927 /* ChatListTable.swift */; }; + 27E2855A26BBE0C900219927 /* ChatLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2849226BBE0C900219927 /* ChatLocation.swift */; }; + 27E2855B26BBE0C900219927 /* StringIndexTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2849326BBE0C900219927 /* StringIndexTokens.swift */; }; + 27E2855C26BBE0C900219927 /* AccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2849426BBE0C900219927 /* AccountManager.swift */; }; + 27E2855D26BBE0C900219927 /* PostboxStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2849526BBE0C900219927 /* PostboxStateView.swift */; }; + 27E2855E26BBE0C900219927 /* PendingMessageActionsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2849626BBE0C900219927 /* PendingMessageActionsTable.swift */; }; + 27E2855F26BBE0C900219927 /* TimestampBasedMessageAttributesIndexTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2849726BBE0C900219927 /* TimestampBasedMessageAttributesIndexTable.swift */; }; + 27E2856026BBE0C900219927 /* MessageHistoryUnsentTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2849826BBE0C900219927 /* MessageHistoryUnsentTable.swift */; }; + 27E2856126BBE0C900219927 /* PostboxUpgrade_16to17.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2849926BBE0C900219927 /* PostboxUpgrade_16to17.swift */; }; + 27E2856226BBE0C900219927 /* MessageHistoryTextIndexTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2849A26BBE0C900219927 /* MessageHistoryTextIndexTable.swift */; }; + 27E2856326BBE0C900219927 /* AdaptedPostboxEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2849D26BBE0C900219927 /* AdaptedPostboxEncoder.swift */; }; + 27E2856426BBE0C900219927 /* AdaptedPostboxUnkeyedEncodingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2849E26BBE0C900219927 /* AdaptedPostboxUnkeyedEncodingContainer.swift */; }; + 27E2856526BBE0C900219927 /* AdaptedPostboxSingleValueEncodingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2849F26BBE0C900219927 /* AdaptedPostboxSingleValueEncodingContainer.swift */; }; + 27E2856626BBE0C900219927 /* AdaptedPostboxKeyedEncodingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284A026BBE0C900219927 /* AdaptedPostboxKeyedEncodingContainer.swift */; }; + 27E2856726BBE0C900219927 /* AdaptedPostboxDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284A226BBE0C900219927 /* AdaptedPostboxDecoder.swift */; }; + 27E2856826BBE0C900219927 /* AdaptedPostboxSingleValueDecodingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284A326BBE0C900219927 /* AdaptedPostboxSingleValueDecodingContainer.swift */; }; + 27E2856926BBE0C900219927 /* AdaptedPostboxKeyedDecodingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284A426BBE0C900219927 /* AdaptedPostboxKeyedDecodingContainer.swift */; }; + 27E2856A26BBE0C900219927 /* AdaptedPostboxUnkeyedDecodingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284A526BBE0C900219927 /* AdaptedPostboxUnkeyedDecodingContainer.swift */; }; + 27E2856B26BBE0C900219927 /* PostboxCodingAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284A626BBE0C900219927 /* PostboxCodingAdapter.swift */; }; + 27E2856C26BBE0C900219927 /* MediaBoxFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284A726BBE0C900219927 /* MediaBoxFile.swift */; }; + 27E2856D26BBE0C900219927 /* Table.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284A826BBE0C900219927 /* Table.swift */; }; + 27E2856E26BBE0C900219927 /* ChatListIndexTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284A926BBE0C900219927 /* ChatListIndexTable.swift */; }; + 27E2856F26BBE0C900219927 /* MessageHistoryTagSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284AA26BBE0C900219927 /* MessageHistoryTagSummaryView.swift */; }; + 27E2857026BBE0C900219927 /* ItemCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284AB26BBE0C900219927 /* ItemCollection.swift */; }; + 27E2857126BBE0C900219927 /* MessageHistoryHolesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284AC26BBE0C900219927 /* MessageHistoryHolesView.swift */; }; + 27E2857226BBE0C900219927 /* RenderedPeer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284AD26BBE0C900219927 /* RenderedPeer.swift */; }; + 27E2857326BBE0C900219927 /* ChatListHolesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284AE26BBE0C900219927 /* ChatListHolesView.swift */; }; + 27E2857426BBE0C900219927 /* MutableBasicPeerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284AF26BBE0C900219927 /* MutableBasicPeerView.swift */; }; + 27E2857526BBE0C900219927 /* PendingPeerNotificationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284B026BBE0C900219927 /* PendingPeerNotificationSettingsView.swift */; }; + 27E2857626BBE0C900219927 /* SynchronizeGroupMessageStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284B126BBE0C900219927 /* SynchronizeGroupMessageStatsView.swift */; }; + 27E2857726BBE0C900219927 /* AdditionalMessageHistoryViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284B226BBE0C900219927 /* AdditionalMessageHistoryViewData.swift */; }; + 27E2857826BBE0C900219927 /* TempBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284B326BBE0C900219927 /* TempBox.swift */; }; + 27E2857926BBE0C900219927 /* PostboxLogging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284B426BBE0C900219927 /* PostboxLogging.swift */; }; + 27E2857A26BBE0C900219927 /* PostboxUpgrade_23to24.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284B526BBE0C900219927 /* PostboxUpgrade_23to24.swift */; }; + 27E2857B26BBE0C900219927 /* HistoryTagInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284B626BBE0C900219927 /* HistoryTagInfoView.swift */; }; + 27E2857C26BBE0C900219927 /* MessageHistoryMetadataTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284B726BBE0C900219927 /* MessageHistoryMetadataTable.swift */; }; + 27E2857D26BBE0C900219927 /* ReverseIndexReferenceTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284B826BBE0C900219927 /* ReverseIndexReferenceTable.swift */; }; + 27E2857E26BBE0C900219927 /* ChatListViewState.swift.bak in Resources */ = {isa = PBXBuildFile; fileRef = 27E284B926BBE0C900219927 /* ChatListViewState.swift.bak */; }; + 27E2857F26BBE0C900219927 /* MessageHistoryViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284BA26BBE0C900219927 /* MessageHistoryViewState.swift */; }; + 27E2858026BBE0C900219927 /* GlobalMessageHistoryTagsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284BB26BBE0C900219927 /* GlobalMessageHistoryTagsTable.swift */; }; + 27E2858126BBE0C900219927 /* Postbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284BC26BBE0C900219927 /* Postbox.swift */; }; + 27E2858226BBE0C900219927 /* PeerNameIndexRepresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284BD26BBE0C900219927 /* PeerNameIndexRepresentation.swift */; }; + 27E2858326BBE0C900219927 /* AccountManagerSharedDataTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284BE26BBE0C900219927 /* AccountManagerSharedDataTable.swift */; }; + 27E2858426BBE0C900219927 /* ChatListViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284BF26BBE0C900219927 /* ChatListViewState.swift */; }; + 27E2858526BBE0C900219927 /* SimpleDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284C026BBE0C900219927 /* SimpleDictionary.swift */; }; + 27E2858626BBE0C900219927 /* PeerChatStateTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284C126BBE0C900219927 /* PeerChatStateTable.swift */; }; + 27E2858726BBE0C900219927 /* OrderedItemListIndexTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284C226BBE0C900219927 /* OrderedItemListIndexTable.swift */; }; + 27E2858826BBE0C900219927 /* ContactPeerIdsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284C326BBE0C900219927 /* ContactPeerIdsView.swift */; }; + 27E2858926BBE0C900219927 /* CachedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284C426BBE0C900219927 /* CachedItemView.swift */; }; + 27E2858A26BBE0C900219927 /* UnreadMessageCountsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284C526BBE0C900219927 /* UnreadMessageCountsView.swift */; }; + 27E2858B26BBE0C900219927 /* PeerTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284C626BBE0C900219927 /* PeerTable.swift */; }; + 27E2858C26BBE0C900219927 /* GlobalMessageIdsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284C726BBE0C900219927 /* GlobalMessageIdsTable.swift */; }; + 27E2858D26BBE0C900219927 /* ItemCollectionIdsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284C826BBE0C900219927 /* ItemCollectionIdsView.swift */; }; + 27E2858E26BBE0C900219927 /* MessageHistoryTagsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284C926BBE0C900219927 /* MessageHistoryTagsTable.swift */; }; + 27E2858F26BBE0C900219927 /* MessageHistoryThreadHoleIndexTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284CA26BBE0C900219927 /* MessageHistoryThreadHoleIndexTable.swift */; }; + 27E2859026BBE0C900219927 /* PeerMergedOperationLogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284CB26BBE0C900219927 /* PeerMergedOperationLogView.swift */; }; + 27E2859126BBE0C900219927 /* ItemCollectionItemTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284CC26BBE0C900219927 /* ItemCollectionItemTable.swift */; }; + 27E2859226BBE0C900219927 /* PeerPresence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284CD26BBE0C900219927 /* PeerPresence.swift */; }; + 27E2859326BBE0C900219927 /* PeerMergedOperationLogIndexTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284CE26BBE0C900219927 /* PeerMergedOperationLogIndexTable.swift */; }; + 27E2859426BBE0C900219927 /* UnsentMessageHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284CF26BBE0C900219927 /* UnsentMessageHistoryView.swift */; }; + 27E2859526BBE0C900219927 /* PendingMessageActionsSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284D026BBE0C900219927 /* PendingMessageActionsSummaryView.swift */; }; + 27E2859626BBE0C900219927 /* DeviceContactImportInfoTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284D126BBE0C900219927 /* DeviceContactImportInfoTable.swift */; }; + 27E2859726BBE0C900219927 /* Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284D226BBE0C900219927 /* Media.swift */; }; + 27E2859826BBE0C900219927 /* PostboxUpgrade_17to18.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284D326BBE0C900219927 /* PostboxUpgrade_17to18.swift */; }; + 27E2859926BBE0C900219927 /* UnorderedItemListTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284D426BBE0C900219927 /* UnorderedItemListTable.swift */; }; + 27E2859A26BBE0C900219927 /* MessageHistoryAnchorIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284D526BBE0C900219927 /* MessageHistoryAnchorIndex.swift */; }; + 27E2859B26BBE0C900219927 /* FailedMessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284D626BBE0C900219927 /* FailedMessagesView.swift */; }; + 27E2859C26BBE0C900219927 /* OrderedItemListEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284D726BBE0C900219927 /* OrderedItemListEntry.swift */; }; + 27E2859D26BBE0C900219927 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284D826BBE0C900219927 /* Database.swift */; }; + 27E2859E26BBE0C900219927 /* MessageHistoryTagsSummaryTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284D926BBE0C900219927 /* MessageHistoryTagsSummaryTable.swift */; }; + 27E2859F26BBE0C900219927 /* ManagedFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284DA26BBE0C900219927 /* ManagedFile.swift */; }; + 27E285A026BBE0C900219927 /* MessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284DB26BBE0C900219927 /* MessagesView.swift */; }; + 27E285A126BBE0C900219927 /* PeerNotificationSettingsBehaviorTimestampView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284DC26BBE0C900219927 /* PeerNotificationSettingsBehaviorTimestampView.swift */; }; + 27E285A226BBE0C900219927 /* PeerNameIndexTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284DD26BBE0C900219927 /* PeerNameIndexTable.swift */; }; + 27E285A326BBE0C900219927 /* AdditionalChatListItemsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284DE26BBE0C900219927 /* AdditionalChatListItemsView.swift */; }; + 27E285A426BBE0C900219927 /* SqliteValueBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284DF26BBE0C900219927 /* SqliteValueBox.swift */; }; + 27E285A526BBE0C900219927 /* MessageHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284E026BBE0C900219927 /* MessageHistoryView.swift */; }; + 27E285A626BBE0C900219927 /* PeerChatInterfaceStateTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284E126BBE0C900219927 /* PeerChatInterfaceStateTable.swift */; }; + 27E285A726BBE0C900219927 /* FileSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284E226BBE0C900219927 /* FileSize.swift */; }; + 27E285A826BBE0C900219927 /* PostboxUpgrade_22to23.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284E326BBE0C900219927 /* PostboxUpgrade_22to23.swift */; }; + 27E285A926BBE0C900219927 /* PeerNotificationSettingsBehaviorIndexTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284E426BBE0C900219927 /* PeerNotificationSettingsBehaviorIndexTable.swift */; }; + 27E285AA26BBE0C900219927 /* ItemCollectionInfosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284E526BBE0C900219927 /* ItemCollectionInfosView.swift */; }; + 27E285AB26BBE0C900219927 /* PeerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284E626BBE0C900219927 /* PeerView.swift */; }; + 27E285AC26BBE0C900219927 /* CachedPeerDataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284E726BBE0C900219927 /* CachedPeerDataView.swift */; }; + 27E285AD26BBE0C900219927 /* PeerPresenceTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284E826BBE0C900219927 /* PeerPresenceTable.swift */; }; + 27E285AE26BBE0C900219927 /* MetadataTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284E926BBE0C900219927 /* MetadataTable.swift */; }; + 27E285AF26BBE0C900219927 /* PostboxUpgrade_24to25.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284EA26BBE0C900219927 /* PostboxUpgrade_24to25.swift */; }; + 27E285B026BBE0C900219927 /* NoticeEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284EB26BBE0C900219927 /* NoticeEntryView.swift */; }; + 27E285B126BBE0C900219927 /* AllChatListHolesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284EC26BBE0C900219927 /* AllChatListHolesView.swift */; }; + 27E285B226BBE0C900219927 /* OrderedItemListTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284ED26BBE0C900219927 /* OrderedItemListTable.swift */; }; + 27E285B326BBE0C900219927 /* InvalidatedMessageHistoryTagsSummaryTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284EE26BBE0C900219927 /* InvalidatedMessageHistoryTagsSummaryTable.swift */; }; + 27E285B426BBE0C900219927 /* TimeBasedCleanup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284EF26BBE0C900219927 /* TimeBasedCleanup.swift */; }; + 27E285B526BBE0C900219927 /* GroupMessageStatsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284F026BBE0C900219927 /* GroupMessageStatsTable.swift */; }; + 27E285B626BBE0C900219927 /* BinarySearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284F126BBE0C900219927 /* BinarySearch.swift */; }; + 27E285B726BBE0C900219927 /* PostboxUpgrade_13to14.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284F226BBE0C900219927 /* PostboxUpgrade_13to14.swift */; }; + 27E285B826BBE0C900219927 /* OrderStatisticTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284F326BBE0C900219927 /* OrderStatisticTable.swift */; }; + 27E285B926BBE0C900219927 /* PendingMessageActionsMetadataTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284F426BBE0C900219927 /* PendingMessageActionsMetadataTable.swift */; }; + 27E285BA26BBE0C900219927 /* AccountSharedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284F526BBE0C900219927 /* AccountSharedData.swift */; }; + 27E285BB26BBE0C900219927 /* PinnedItemId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284F626BBE0C900219927 /* PinnedItemId.swift */; }; + 27E285BC26BBE0C900219927 /* IpcPipe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284F726BBE0C900219927 /* IpcPipe.swift */; }; + 27E285BD26BBE0C900219927 /* Views.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284F826BBE0C900219927 /* Views.swift */; }; + 27E285BE26BBE0C900219927 /* GlobalMessageTagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284F926BBE0C900219927 /* GlobalMessageTagsView.swift */; }; + 27E285BF26BBE0C900219927 /* MultiplePeersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284FA26BBE0C900219927 /* MultiplePeersView.swift */; }; + 27E285C026BBE0C900219927 /* ChatListHole.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284FB26BBE0C900219927 /* ChatListHole.swift */; }; + 27E285C126BBE0C900219927 /* MappedFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284FC26BBE0C900219927 /* MappedFile.swift */; }; + 27E285C226BBE0C900219927 /* PendingPeerNotificationSettingsIndexTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284FD26BBE0C900219927 /* PendingPeerNotificationSettingsIndexTable.swift */; }; + 27E285C326BBE0C900219927 /* PeerChatThreadInterfaceStateTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284FE26BBE0C900219927 /* PeerChatThreadInterfaceStateTable.swift */; }; + 27E285C426BBE0C900219927 /* MessageOfInterestHolesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E284FF26BBE0C900219927 /* MessageOfInterestHolesView.swift */; }; + 27E285C526BBE0C900219927 /* ContactTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2850026BBE0C900219927 /* ContactTable.swift */; }; + 27E285C626BBE0C900219927 /* SharedAccountMediaManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2850126BBE0C900219927 /* SharedAccountMediaManager.swift */; }; + 27E285C726BBE0C900219927 /* AccountRecordsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2850226BBE0C900219927 /* AccountRecordsView.swift */; }; + 27E285C826BBE0C900219927 /* PeerOperationLogMetadataTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2850326BBE0C900219927 /* PeerOperationLogMetadataTable.swift */; }; + 27E285C926BBE0C900219927 /* MessageHistoryReadStateTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2850426BBE0C900219927 /* MessageHistoryReadStateTable.swift */; }; + 27E285CA26BBE0C900219927 /* PreferencesEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2850526BBE0C900219927 /* PreferencesEntry.swift */; }; + 27E285CB26BBE0C900219927 /* AccountManagerMetadataTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2850626BBE0C900219927 /* AccountManagerMetadataTable.swift */; }; + 27E285CC26BBE0C900219927 /* MediaResourceStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2850726BBE0C900219927 /* MediaResourceStatus.swift */; }; + 27E285CD26BBE0C900219927 /* ItemCacheTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2850826BBE0C900219927 /* ItemCacheTable.swift */; }; + 27E285CE26BBE0C900219927 /* PeerNotificationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2850926BBE0C900219927 /* PeerNotificationSettingsView.swift */; }; + 27E285CF26BBE0C900219927 /* MessageMediaTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2850A26BBE0C900219927 /* MessageMediaTable.swift */; }; + 27E285D026BBE0C900219927 /* Peer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2850B26BBE0C900219927 /* Peer.swift */; }; + 27E285D126BBE0C900219927 /* PreferencesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2850C26BBE0C900219927 /* PreferencesTable.swift */; }; + 27E285D226BBE0C900219927 /* OrderedItemListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2850D26BBE0C900219927 /* OrderedItemListView.swift */; }; + 27E285D326BBE0C900219927 /* PostboxUpgrade_12to13.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2850E26BBE0C900219927 /* PostboxUpgrade_12to13.swift */; }; + 27E285D426BBE0C900219927 /* RatingTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2850F26BBE0C900219927 /* RatingTable.swift */; }; + 27E285D526BBE0C900219927 /* MessageHistoryViewEntryAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2851026BBE0C900219927 /* MessageHistoryViewEntryAttributes.swift */; }; + 27E285D626BBE0C900219927 /* PostboxUpgrade_15to16.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2851126BBE0C900219927 /* PostboxUpgrade_15to16.swift */; }; + 27E285D726BBE0C900219927 /* PostboxAccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2851226BBE0C900219927 /* PostboxAccess.swift */; }; + 27E285D826BBE0C900219927 /* MessageGloballyUniqueIdTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2851326BBE0C900219927 /* MessageGloballyUniqueIdTable.swift */; }; + 27E285D926BBE0C900219927 /* PostboxUpgrade_14to15.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2851426BBE0C900219927 /* PostboxUpgrade_14to15.swift */; }; + 27E285DA26BBE0C900219927 /* PeerChatStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2851526BBE0C900219927 /* PeerChatStateView.swift */; }; + 27E285DB26BBE0C900219927 /* SynchronizePeerReadStatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2851626BBE0C900219927 /* SynchronizePeerReadStatesView.swift */; }; + 27E285DC26BBE0C900219927 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27E2851726BBE0C900219927 /* MessageView.swift */; }; + A701F925236B3350002ABF81 /* sqlcipher.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A701F924236B3350002ABF81 /* sqlcipher.framework */; }; + A701F92C236C1B66002ABF81 /* SwiftSignalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A701F92B236C1B66002ABF81 /* SwiftSignalKit.framework */; }; + A701F935236C1DC4002ABF81 /* Crc32.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A701F934236C1DC4002ABF81 /* Crc32.framework */; }; + A7918DDF240CEFFA002011CA /* MurMurHash32.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7918DDE240CEFFA002011CA /* MurMurHash32.framework */; }; + A7918E69240CF2A3002011CA /* StringTransliteration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7918E68240CF2A3002011CA /* StringTransliteration.framework */; }; + D0B418171D7DFAF3004562A4 /* Postbox.h in Headers */ = {isa = PBXBuildFile; fileRef = D0B418151D7DFAF3004562A4 /* Postbox.h */; settings = {ATTRIBUTES = (Public, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 27E2845026BBE0C900219927 /* TimestampBasedMessageAttributesTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimestampBasedMessageAttributesTable.swift; sourceTree = ""; }; + 27E2845126BBE0C900219927 /* ItemCacheMetaTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemCacheMetaTable.swift; sourceTree = ""; }; + 27E2845226BBE0C900219927 /* PeerReadState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerReadState.swift; sourceTree = ""; }; + 27E2845326BBE0C900219927 /* Upgrades.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Upgrades.swift; sourceTree = ""; }; + 27E2845426BBE0C900219927 /* Coding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Coding.swift; sourceTree = ""; }; + 27E2845526BBE0C900219927 /* UnsentMessageIndicesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnsentMessageIndicesView.swift; sourceTree = ""; }; + 27E2845626BBE0C900219927 /* ReverseAssociatedPeerTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReverseAssociatedPeerTable.swift; sourceTree = ""; }; + 27E2845726BBE0C900219927 /* PostboxView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostboxView.swift; sourceTree = ""; }; + 27E2845826BBE0C900219927 /* InvalidatedMessageHistoryTagSummariesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InvalidatedMessageHistoryTagSummariesView.swift; sourceTree = ""; }; + 27E2845926BBE0C900219927 /* MessageHistoryHoleIndexTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryHoleIndexTable.swift; sourceTree = ""; }; + 27E2845A26BBE0C900219927 /* LocalMessageHistoryTagsTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalMessageHistoryTagsTable.swift; sourceTree = ""; }; + 27E2845B26BBE0C900219927 /* SeedConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeedConfiguration.swift; sourceTree = ""; }; + 27E2845C26BBE0C900219927 /* CachedPeerData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachedPeerData.swift; sourceTree = ""; }; + 27E2845D26BBE0C900219927 /* MediaResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaResource.swift; sourceTree = ""; }; + 27E2845E26BBE0C900219927 /* Hash.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Hash.swift; sourceTree = ""; }; + 27E2845F26BBE0C900219927 /* InvalidatedGroupMessageStatsTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InvalidatedGroupMessageStatsTable.swift; sourceTree = ""; }; + 27E2846026BBE0C900219927 /* ViewTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewTracker.swift; sourceTree = ""; }; + 27E2846126BBE0C900219927 /* ValueBoxKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueBoxKey.swift; sourceTree = ""; }; + 27E2846226BBE0C900219927 /* AdditionalChatListItemsTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdditionalChatListItemsTable.swift; sourceTree = ""; }; + 27E2846326BBE0C900219927 /* SimpleSet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleSet.swift; sourceTree = ""; }; + 27E2846426BBE0C900219927 /* PostboxUpgrade_20to21.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostboxUpgrade_20to21.swift; sourceTree = ""; }; + 27E2846526BBE0C900219927 /* PeerNotificationSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerNotificationSettings.swift; sourceTree = ""; }; + 27E2846626BBE0C900219927 /* ValueBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueBox.swift; sourceTree = ""; }; + 27E2846726BBE0C900219927 /* ChatListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListView.swift; sourceTree = ""; }; + 27E2846826BBE0C900219927 /* PeerOperationLogTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerOperationLogTable.swift; sourceTree = ""; }; + 27E2846926BBE0C900219927 /* MutablePeerChatInclusionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MutablePeerChatInclusionView.swift; sourceTree = ""; }; + 27E2846A26BBE0C900219927 /* PostboxUpgrade_19to20.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostboxUpgrade_19to20.swift; sourceTree = ""; }; + 27E2846B26BBE0C900219927 /* AccountManagerAtomicState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountManagerAtomicState.swift; sourceTree = ""; }; + 27E2846C26BBE0C900219927 /* CachedPeerDataTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachedPeerDataTable.swift; sourceTree = ""; }; + 27E2846D26BBE0C900219927 /* PostboxUpgrade_21to22.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostboxUpgrade_21to22.swift; sourceTree = ""; }; + 27E2846E26BBE0C900219927 /* PeerChatTopIndexableMessageIds.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerChatTopIndexableMessageIds.swift; sourceTree = ""; }; + 27E2846F26BBE0C900219927 /* PeerChatListInclusion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerChatListInclusion.swift; sourceTree = ""; }; + 27E2847026BBE0C900219927 /* PostboxTransaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostboxTransaction.swift; sourceTree = ""; }; + 27E2847126BBE0C900219927 /* ItemCollectionInfoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemCollectionInfoView.swift; sourceTree = ""; }; + 27E2847226BBE0C900219927 /* TimestampBasedMessageAttributesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimestampBasedMessageAttributesView.swift; sourceTree = ""; }; + 27E2847326BBE0C900219927 /* MessageHistoryFailedTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryFailedTable.swift; sourceTree = ""; }; + 27E2847426BBE0C900219927 /* PeerNotificationSettingsTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerNotificationSettingsTable.swift; sourceTree = ""; }; + 27E2847526BBE0C900219927 /* MediaBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaBox.swift; sourceTree = ""; }; + 27E2847626BBE0C900219927 /* LocalMessageTagsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalMessageTagsView.swift; sourceTree = ""; }; + 27E2847726BBE0C900219927 /* AccountManagerRecordTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountManagerRecordTable.swift; sourceTree = ""; }; + 27E2847826BBE0C900219927 /* TopChatMessageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopChatMessageView.swift; sourceTree = ""; }; + 27E2847926BBE0C900219927 /* MessageHistorySynchronizeReadStateTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistorySynchronizeReadStateTable.swift; sourceTree = ""; }; + 27E2847A26BBE0C900219927 /* PendingMessageActionsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PendingMessageActionsView.swift; sourceTree = ""; }; + 27E2847B26BBE0C900219927 /* ItemCollectionsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemCollectionsView.swift; sourceTree = ""; }; + 27E2847C26BBE0C900219927 /* PeerNotificationSettingsBehaviorTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerNotificationSettingsBehaviorTable.swift; sourceTree = ""; }; + 27E2847D26BBE0C900219927 /* AccountRecord.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountRecord.swift; sourceTree = ""; }; + 27E2847E26BBE0C900219927 /* PeerGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerGroup.swift; sourceTree = ""; }; + 27E2847F26BBE0C900219927 /* NoticeTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoticeTable.swift; sourceTree = ""; }; + 27E2848026BBE0C900219927 /* InitialMessageHistoryData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InitialMessageHistoryData.swift; sourceTree = ""; }; + 27E2848126BBE0C900219927 /* IntermediateMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntermediateMessage.swift; sourceTree = ""; }; + 27E2848226BBE0C900219927 /* RedBlackTree.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedBlackTree.swift; sourceTree = ""; }; + 27E2848326BBE0C900219927 /* ContactPeersView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactPeersView.swift; sourceTree = ""; }; + 27E2848426BBE0C900219927 /* PeerChatInterfaceState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerChatInterfaceState.swift; sourceTree = ""; }; + 27E2848526BBE0C900219927 /* PostboxUpgrade_18to19.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostboxUpgrade_18to19.swift; sourceTree = ""; }; + 27E2848626BBE0C900219927 /* OrderedList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrderedList.swift; sourceTree = ""; }; + 27E2848726BBE0C900219927 /* MessageHistoryOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryOperation.swift; sourceTree = ""; }; + 27E2848826BBE0C900219927 /* PreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = ""; }; + 27E2848926BBE0C900219927 /* KeychainTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeychainTable.swift; sourceTree = ""; }; + 27E2848A26BBE0C900219927 /* ItemCollectionInfoTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemCollectionInfoTable.swift; sourceTree = ""; }; + 27E2848B26BBE0C900219927 /* MessageHistoryThreadsTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryThreadsTable.swift; sourceTree = ""; }; + 27E2848C26BBE0C900219927 /* MessageHistoryTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryTable.swift; sourceTree = ""; }; + 27E2848D26BBE0C900219927 /* MessageHistoryIndexTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryIndexTable.swift; sourceTree = ""; }; + 27E2848E26BBE0C900219927 /* AccessChallengeDataView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessChallengeDataView.swift; sourceTree = ""; }; + 27E2848F26BBE0C900219927 /* Message.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; + 27E2849026BBE0C900219927 /* PeerPresencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerPresencesView.swift; sourceTree = ""; }; + 27E2849126BBE0C900219927 /* ChatListTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListTable.swift; sourceTree = ""; }; + 27E2849226BBE0C900219927 /* ChatLocation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatLocation.swift; sourceTree = ""; }; + 27E2849326BBE0C900219927 /* StringIndexTokens.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringIndexTokens.swift; sourceTree = ""; }; + 27E2849426BBE0C900219927 /* AccountManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountManager.swift; sourceTree = ""; }; + 27E2849526BBE0C900219927 /* PostboxStateView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostboxStateView.swift; sourceTree = ""; }; + 27E2849626BBE0C900219927 /* PendingMessageActionsTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PendingMessageActionsTable.swift; sourceTree = ""; }; + 27E2849726BBE0C900219927 /* TimestampBasedMessageAttributesIndexTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimestampBasedMessageAttributesIndexTable.swift; sourceTree = ""; }; + 27E2849826BBE0C900219927 /* MessageHistoryUnsentTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryUnsentTable.swift; sourceTree = ""; }; + 27E2849926BBE0C900219927 /* PostboxUpgrade_16to17.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostboxUpgrade_16to17.swift; sourceTree = ""; }; + 27E2849A26BBE0C900219927 /* MessageHistoryTextIndexTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryTextIndexTable.swift; sourceTree = ""; }; + 27E2849D26BBE0C900219927 /* AdaptedPostboxEncoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdaptedPostboxEncoder.swift; sourceTree = ""; }; + 27E2849E26BBE0C900219927 /* AdaptedPostboxUnkeyedEncodingContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdaptedPostboxUnkeyedEncodingContainer.swift; sourceTree = ""; }; + 27E2849F26BBE0C900219927 /* AdaptedPostboxSingleValueEncodingContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdaptedPostboxSingleValueEncodingContainer.swift; sourceTree = ""; }; + 27E284A026BBE0C900219927 /* AdaptedPostboxKeyedEncodingContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdaptedPostboxKeyedEncodingContainer.swift; sourceTree = ""; }; + 27E284A226BBE0C900219927 /* AdaptedPostboxDecoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdaptedPostboxDecoder.swift; sourceTree = ""; }; + 27E284A326BBE0C900219927 /* AdaptedPostboxSingleValueDecodingContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdaptedPostboxSingleValueDecodingContainer.swift; sourceTree = ""; }; + 27E284A426BBE0C900219927 /* AdaptedPostboxKeyedDecodingContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdaptedPostboxKeyedDecodingContainer.swift; sourceTree = ""; }; + 27E284A526BBE0C900219927 /* AdaptedPostboxUnkeyedDecodingContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdaptedPostboxUnkeyedDecodingContainer.swift; sourceTree = ""; }; + 27E284A626BBE0C900219927 /* PostboxCodingAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostboxCodingAdapter.swift; sourceTree = ""; }; + 27E284A726BBE0C900219927 /* MediaBoxFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaBoxFile.swift; sourceTree = ""; }; + 27E284A826BBE0C900219927 /* Table.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Table.swift; sourceTree = ""; }; + 27E284A926BBE0C900219927 /* ChatListIndexTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListIndexTable.swift; sourceTree = ""; }; + 27E284AA26BBE0C900219927 /* MessageHistoryTagSummaryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryTagSummaryView.swift; sourceTree = ""; }; + 27E284AB26BBE0C900219927 /* ItemCollection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemCollection.swift; sourceTree = ""; }; + 27E284AC26BBE0C900219927 /* MessageHistoryHolesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryHolesView.swift; sourceTree = ""; }; + 27E284AD26BBE0C900219927 /* RenderedPeer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RenderedPeer.swift; sourceTree = ""; }; + 27E284AE26BBE0C900219927 /* ChatListHolesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListHolesView.swift; sourceTree = ""; }; + 27E284AF26BBE0C900219927 /* MutableBasicPeerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MutableBasicPeerView.swift; sourceTree = ""; }; + 27E284B026BBE0C900219927 /* PendingPeerNotificationSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PendingPeerNotificationSettingsView.swift; sourceTree = ""; }; + 27E284B126BBE0C900219927 /* SynchronizeGroupMessageStatsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SynchronizeGroupMessageStatsView.swift; sourceTree = ""; }; + 27E284B226BBE0C900219927 /* AdditionalMessageHistoryViewData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdditionalMessageHistoryViewData.swift; sourceTree = ""; }; + 27E284B326BBE0C900219927 /* TempBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TempBox.swift; sourceTree = ""; }; + 27E284B426BBE0C900219927 /* PostboxLogging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostboxLogging.swift; sourceTree = ""; }; + 27E284B526BBE0C900219927 /* PostboxUpgrade_23to24.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostboxUpgrade_23to24.swift; sourceTree = ""; }; + 27E284B626BBE0C900219927 /* HistoryTagInfoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HistoryTagInfoView.swift; sourceTree = ""; }; + 27E284B726BBE0C900219927 /* MessageHistoryMetadataTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryMetadataTable.swift; sourceTree = ""; }; + 27E284B826BBE0C900219927 /* ReverseIndexReferenceTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReverseIndexReferenceTable.swift; sourceTree = ""; }; + 27E284B926BBE0C900219927 /* ChatListViewState.swift.bak */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ChatListViewState.swift.bak; sourceTree = ""; }; + 27E284BA26BBE0C900219927 /* MessageHistoryViewState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryViewState.swift; sourceTree = ""; }; + 27E284BB26BBE0C900219927 /* GlobalMessageHistoryTagsTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalMessageHistoryTagsTable.swift; sourceTree = ""; }; + 27E284BC26BBE0C900219927 /* Postbox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Postbox.swift; sourceTree = ""; }; + 27E284BD26BBE0C900219927 /* PeerNameIndexRepresentation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerNameIndexRepresentation.swift; sourceTree = ""; }; + 27E284BE26BBE0C900219927 /* AccountManagerSharedDataTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountManagerSharedDataTable.swift; sourceTree = ""; }; + 27E284BF26BBE0C900219927 /* ChatListViewState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListViewState.swift; sourceTree = ""; }; + 27E284C026BBE0C900219927 /* SimpleDictionary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleDictionary.swift; sourceTree = ""; }; + 27E284C126BBE0C900219927 /* PeerChatStateTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerChatStateTable.swift; sourceTree = ""; }; + 27E284C226BBE0C900219927 /* OrderedItemListIndexTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrderedItemListIndexTable.swift; sourceTree = ""; }; + 27E284C326BBE0C900219927 /* ContactPeerIdsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactPeerIdsView.swift; sourceTree = ""; }; + 27E284C426BBE0C900219927 /* CachedItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachedItemView.swift; sourceTree = ""; }; + 27E284C526BBE0C900219927 /* UnreadMessageCountsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnreadMessageCountsView.swift; sourceTree = ""; }; + 27E284C626BBE0C900219927 /* PeerTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerTable.swift; sourceTree = ""; }; + 27E284C726BBE0C900219927 /* GlobalMessageIdsTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalMessageIdsTable.swift; sourceTree = ""; }; + 27E284C826BBE0C900219927 /* ItemCollectionIdsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemCollectionIdsView.swift; sourceTree = ""; }; + 27E284C926BBE0C900219927 /* MessageHistoryTagsTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryTagsTable.swift; sourceTree = ""; }; + 27E284CA26BBE0C900219927 /* MessageHistoryThreadHoleIndexTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryThreadHoleIndexTable.swift; sourceTree = ""; }; + 27E284CB26BBE0C900219927 /* PeerMergedOperationLogView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMergedOperationLogView.swift; sourceTree = ""; }; + 27E284CC26BBE0C900219927 /* ItemCollectionItemTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemCollectionItemTable.swift; sourceTree = ""; }; + 27E284CD26BBE0C900219927 /* PeerPresence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerPresence.swift; sourceTree = ""; }; + 27E284CE26BBE0C900219927 /* PeerMergedOperationLogIndexTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMergedOperationLogIndexTable.swift; sourceTree = ""; }; + 27E284CF26BBE0C900219927 /* UnsentMessageHistoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnsentMessageHistoryView.swift; sourceTree = ""; }; + 27E284D026BBE0C900219927 /* PendingMessageActionsSummaryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PendingMessageActionsSummaryView.swift; sourceTree = ""; }; + 27E284D126BBE0C900219927 /* DeviceContactImportInfoTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceContactImportInfoTable.swift; sourceTree = ""; }; + 27E284D226BBE0C900219927 /* Media.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Media.swift; sourceTree = ""; }; + 27E284D326BBE0C900219927 /* PostboxUpgrade_17to18.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostboxUpgrade_17to18.swift; sourceTree = ""; }; + 27E284D426BBE0C900219927 /* UnorderedItemListTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnorderedItemListTable.swift; sourceTree = ""; }; + 27E284D526BBE0C900219927 /* MessageHistoryAnchorIndex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryAnchorIndex.swift; sourceTree = ""; }; + 27E284D626BBE0C900219927 /* FailedMessagesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FailedMessagesView.swift; sourceTree = ""; }; + 27E284D726BBE0C900219927 /* OrderedItemListEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrderedItemListEntry.swift; sourceTree = ""; }; + 27E284D826BBE0C900219927 /* Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = ""; }; + 27E284D926BBE0C900219927 /* MessageHistoryTagsSummaryTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryTagsSummaryTable.swift; sourceTree = ""; }; + 27E284DA26BBE0C900219927 /* ManagedFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedFile.swift; sourceTree = ""; }; + 27E284DB26BBE0C900219927 /* MessagesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessagesView.swift; sourceTree = ""; }; + 27E284DC26BBE0C900219927 /* PeerNotificationSettingsBehaviorTimestampView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerNotificationSettingsBehaviorTimestampView.swift; sourceTree = ""; }; + 27E284DD26BBE0C900219927 /* PeerNameIndexTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerNameIndexTable.swift; sourceTree = ""; }; + 27E284DE26BBE0C900219927 /* AdditionalChatListItemsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdditionalChatListItemsView.swift; sourceTree = ""; }; + 27E284DF26BBE0C900219927 /* SqliteValueBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SqliteValueBox.swift; sourceTree = ""; }; + 27E284E026BBE0C900219927 /* MessageHistoryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryView.swift; sourceTree = ""; }; + 27E284E126BBE0C900219927 /* PeerChatInterfaceStateTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerChatInterfaceStateTable.swift; sourceTree = ""; }; + 27E284E226BBE0C900219927 /* FileSize.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileSize.swift; sourceTree = ""; }; + 27E284E326BBE0C900219927 /* PostboxUpgrade_22to23.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostboxUpgrade_22to23.swift; sourceTree = ""; }; + 27E284E426BBE0C900219927 /* PeerNotificationSettingsBehaviorIndexTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerNotificationSettingsBehaviorIndexTable.swift; sourceTree = ""; }; + 27E284E526BBE0C900219927 /* ItemCollectionInfosView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemCollectionInfosView.swift; sourceTree = ""; }; + 27E284E626BBE0C900219927 /* PeerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerView.swift; sourceTree = ""; }; + 27E284E726BBE0C900219927 /* CachedPeerDataView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachedPeerDataView.swift; sourceTree = ""; }; + 27E284E826BBE0C900219927 /* PeerPresenceTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerPresenceTable.swift; sourceTree = ""; }; + 27E284E926BBE0C900219927 /* MetadataTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetadataTable.swift; sourceTree = ""; }; + 27E284EA26BBE0C900219927 /* PostboxUpgrade_24to25.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostboxUpgrade_24to25.swift; sourceTree = ""; }; + 27E284EB26BBE0C900219927 /* NoticeEntryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoticeEntryView.swift; sourceTree = ""; }; + 27E284EC26BBE0C900219927 /* AllChatListHolesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AllChatListHolesView.swift; sourceTree = ""; }; + 27E284ED26BBE0C900219927 /* OrderedItemListTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrderedItemListTable.swift; sourceTree = ""; }; + 27E284EE26BBE0C900219927 /* InvalidatedMessageHistoryTagsSummaryTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InvalidatedMessageHistoryTagsSummaryTable.swift; sourceTree = ""; }; + 27E284EF26BBE0C900219927 /* TimeBasedCleanup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimeBasedCleanup.swift; sourceTree = ""; }; + 27E284F026BBE0C900219927 /* GroupMessageStatsTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupMessageStatsTable.swift; sourceTree = ""; }; + 27E284F126BBE0C900219927 /* BinarySearch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BinarySearch.swift; sourceTree = ""; }; + 27E284F226BBE0C900219927 /* PostboxUpgrade_13to14.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostboxUpgrade_13to14.swift; sourceTree = ""; }; + 27E284F326BBE0C900219927 /* OrderStatisticTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrderStatisticTable.swift; sourceTree = ""; }; + 27E284F426BBE0C900219927 /* PendingMessageActionsMetadataTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PendingMessageActionsMetadataTable.swift; sourceTree = ""; }; + 27E284F526BBE0C900219927 /* AccountSharedData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountSharedData.swift; sourceTree = ""; }; + 27E284F626BBE0C900219927 /* PinnedItemId.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PinnedItemId.swift; sourceTree = ""; }; + 27E284F726BBE0C900219927 /* IpcPipe.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IpcPipe.swift; sourceTree = ""; }; + 27E284F826BBE0C900219927 /* Views.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Views.swift; sourceTree = ""; }; + 27E284F926BBE0C900219927 /* GlobalMessageTagsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalMessageTagsView.swift; sourceTree = ""; }; + 27E284FA26BBE0C900219927 /* MultiplePeersView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiplePeersView.swift; sourceTree = ""; }; + 27E284FB26BBE0C900219927 /* ChatListHole.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListHole.swift; sourceTree = ""; }; + 27E284FC26BBE0C900219927 /* MappedFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MappedFile.swift; sourceTree = ""; }; + 27E284FD26BBE0C900219927 /* PendingPeerNotificationSettingsIndexTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PendingPeerNotificationSettingsIndexTable.swift; sourceTree = ""; }; + 27E284FE26BBE0C900219927 /* PeerChatThreadInterfaceStateTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerChatThreadInterfaceStateTable.swift; sourceTree = ""; }; + 27E284FF26BBE0C900219927 /* MessageOfInterestHolesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageOfInterestHolesView.swift; sourceTree = ""; }; + 27E2850026BBE0C900219927 /* ContactTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactTable.swift; sourceTree = ""; }; + 27E2850126BBE0C900219927 /* SharedAccountMediaManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharedAccountMediaManager.swift; sourceTree = ""; }; + 27E2850226BBE0C900219927 /* AccountRecordsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountRecordsView.swift; sourceTree = ""; }; + 27E2850326BBE0C900219927 /* PeerOperationLogMetadataTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerOperationLogMetadataTable.swift; sourceTree = ""; }; + 27E2850426BBE0C900219927 /* MessageHistoryReadStateTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryReadStateTable.swift; sourceTree = ""; }; + 27E2850526BBE0C900219927 /* PreferencesEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesEntry.swift; sourceTree = ""; }; + 27E2850626BBE0C900219927 /* AccountManagerMetadataTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountManagerMetadataTable.swift; sourceTree = ""; }; + 27E2850726BBE0C900219927 /* MediaResourceStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaResourceStatus.swift; sourceTree = ""; }; + 27E2850826BBE0C900219927 /* ItemCacheTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemCacheTable.swift; sourceTree = ""; }; + 27E2850926BBE0C900219927 /* PeerNotificationSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerNotificationSettingsView.swift; sourceTree = ""; }; + 27E2850A26BBE0C900219927 /* MessageMediaTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageMediaTable.swift; sourceTree = ""; }; + 27E2850B26BBE0C900219927 /* Peer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Peer.swift; sourceTree = ""; }; + 27E2850C26BBE0C900219927 /* PreferencesTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesTable.swift; sourceTree = ""; }; + 27E2850D26BBE0C900219927 /* OrderedItemListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrderedItemListView.swift; sourceTree = ""; }; + 27E2850E26BBE0C900219927 /* PostboxUpgrade_12to13.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostboxUpgrade_12to13.swift; sourceTree = ""; }; + 27E2850F26BBE0C900219927 /* RatingTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RatingTable.swift; sourceTree = ""; }; + 27E2851026BBE0C900219927 /* MessageHistoryViewEntryAttributes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHistoryViewEntryAttributes.swift; sourceTree = ""; }; + 27E2851126BBE0C900219927 /* PostboxUpgrade_15to16.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostboxUpgrade_15to16.swift; sourceTree = ""; }; + 27E2851226BBE0C900219927 /* PostboxAccess.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostboxAccess.swift; sourceTree = ""; }; + 27E2851326BBE0C900219927 /* MessageGloballyUniqueIdTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageGloballyUniqueIdTable.swift; sourceTree = ""; }; + 27E2851426BBE0C900219927 /* PostboxUpgrade_14to15.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostboxUpgrade_14to15.swift; sourceTree = ""; }; + 27E2851526BBE0C900219927 /* PeerChatStateView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerChatStateView.swift; sourceTree = ""; }; + 27E2851626BBE0C900219927 /* SynchronizePeerReadStatesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SynchronizePeerReadStatesView.swift; sourceTree = ""; }; + 27E2851726BBE0C900219927 /* MessageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; }; + A701F8ED236B2E54002ABF81 /* crc32mac.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = crc32mac.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A701F8EF236B2E5A002ABF81 /* libphonenumbermac.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = libphonenumbermac.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A701F8F1236B2E61002ABF81 /* SwiftSignalKitMac.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftSignalKitMac.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A701F924236B3350002ABF81 /* sqlcipher.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = sqlcipher.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A701F92B236C1B66002ABF81 /* SwiftSignalKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftSignalKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A701F92D236C1D02002ABF81 /* crc32.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = crc32.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A701F934236C1DC4002ABF81 /* Crc32.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Crc32.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7918DDE240CEFFA002011CA /* MurMurHash32.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MurMurHash32.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7918E68240CF2A3002011CA /* StringTransliteration.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = StringTransliteration.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D03E45562305C7C90049C28B /* sqlcipher.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = sqlcipher.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D03E457A2305CD000049C28B /* Crc32.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Crc32.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D03E457C2305CD090049C28B /* Crc32.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Crc32.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D03E457E2305CD130049C28B /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + D03E462B2306E01C0049C28B /* sqlciphermac.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = sqlciphermac.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D03E463C2306E0F60049C28B /* crc32mac.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = crc32mac.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0B418131D7DFAF2004562A4 /* Postbox.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Postbox.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0B418151D7DFAF3004562A4 /* Postbox.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Postbox.h; sourceTree = ""; }; + D0B418161D7DFAF3004562A4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D0B418601D7DFE95004562A4 /* SwiftSignalKitMac.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftSignalKitMac.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug/SwiftSignalKitMac.framework"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D0B4180F1D7DFAF2004562A4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A7918E69240CF2A3002011CA /* StringTransliteration.framework in Frameworks */, + A7918DDF240CEFFA002011CA /* MurMurHash32.framework in Frameworks */, + A701F935236C1DC4002ABF81 /* Crc32.framework in Frameworks */, + A701F92C236C1B66002ABF81 /* SwiftSignalKit.framework in Frameworks */, + A701F925236B3350002ABF81 /* sqlcipher.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 27E2844F26BBE0C900219927 /* Sources */ = { + isa = PBXGroup; + children = ( + 27E2845026BBE0C900219927 /* TimestampBasedMessageAttributesTable.swift */, + 27E2845126BBE0C900219927 /* ItemCacheMetaTable.swift */, + 27E2845226BBE0C900219927 /* PeerReadState.swift */, + 27E2845326BBE0C900219927 /* Upgrades.swift */, + 27E2845426BBE0C900219927 /* Coding.swift */, + 27E2845526BBE0C900219927 /* UnsentMessageIndicesView.swift */, + 27E2845626BBE0C900219927 /* ReverseAssociatedPeerTable.swift */, + 27E2845726BBE0C900219927 /* PostboxView.swift */, + 27E2845826BBE0C900219927 /* InvalidatedMessageHistoryTagSummariesView.swift */, + 27E2845926BBE0C900219927 /* MessageHistoryHoleIndexTable.swift */, + 27E2845A26BBE0C900219927 /* LocalMessageHistoryTagsTable.swift */, + 27E2845B26BBE0C900219927 /* SeedConfiguration.swift */, + 27E2845C26BBE0C900219927 /* CachedPeerData.swift */, + 27E2845D26BBE0C900219927 /* MediaResource.swift */, + 27E2845E26BBE0C900219927 /* Hash.swift */, + 27E2845F26BBE0C900219927 /* InvalidatedGroupMessageStatsTable.swift */, + 27E2846026BBE0C900219927 /* ViewTracker.swift */, + 27E2846126BBE0C900219927 /* ValueBoxKey.swift */, + 27E2846226BBE0C900219927 /* AdditionalChatListItemsTable.swift */, + 27E2846326BBE0C900219927 /* SimpleSet.swift */, + 27E2846426BBE0C900219927 /* PostboxUpgrade_20to21.swift */, + 27E2846526BBE0C900219927 /* PeerNotificationSettings.swift */, + 27E2846626BBE0C900219927 /* ValueBox.swift */, + 27E2846726BBE0C900219927 /* ChatListView.swift */, + 27E2846826BBE0C900219927 /* PeerOperationLogTable.swift */, + 27E2846926BBE0C900219927 /* MutablePeerChatInclusionView.swift */, + 27E2846A26BBE0C900219927 /* PostboxUpgrade_19to20.swift */, + 27E2846B26BBE0C900219927 /* AccountManagerAtomicState.swift */, + 27E2846C26BBE0C900219927 /* CachedPeerDataTable.swift */, + 27E2846D26BBE0C900219927 /* PostboxUpgrade_21to22.swift */, + 27E2846E26BBE0C900219927 /* PeerChatTopIndexableMessageIds.swift */, + 27E2846F26BBE0C900219927 /* PeerChatListInclusion.swift */, + 27E2847026BBE0C900219927 /* PostboxTransaction.swift */, + 27E2847126BBE0C900219927 /* ItemCollectionInfoView.swift */, + 27E2847226BBE0C900219927 /* TimestampBasedMessageAttributesView.swift */, + 27E2847326BBE0C900219927 /* MessageHistoryFailedTable.swift */, + 27E2847426BBE0C900219927 /* PeerNotificationSettingsTable.swift */, + 27E2847526BBE0C900219927 /* MediaBox.swift */, + 27E2847626BBE0C900219927 /* LocalMessageTagsView.swift */, + 27E2847726BBE0C900219927 /* AccountManagerRecordTable.swift */, + 27E2847826BBE0C900219927 /* TopChatMessageView.swift */, + 27E2847926BBE0C900219927 /* MessageHistorySynchronizeReadStateTable.swift */, + 27E2847A26BBE0C900219927 /* PendingMessageActionsView.swift */, + 27E2847B26BBE0C900219927 /* ItemCollectionsView.swift */, + 27E2847C26BBE0C900219927 /* PeerNotificationSettingsBehaviorTable.swift */, + 27E2847D26BBE0C900219927 /* AccountRecord.swift */, + 27E2847E26BBE0C900219927 /* PeerGroup.swift */, + 27E2847F26BBE0C900219927 /* NoticeTable.swift */, + 27E2848026BBE0C900219927 /* InitialMessageHistoryData.swift */, + 27E2848126BBE0C900219927 /* IntermediateMessage.swift */, + 27E2848226BBE0C900219927 /* RedBlackTree.swift */, + 27E2848326BBE0C900219927 /* ContactPeersView.swift */, + 27E2848426BBE0C900219927 /* PeerChatInterfaceState.swift */, + 27E2848526BBE0C900219927 /* PostboxUpgrade_18to19.swift */, + 27E2848626BBE0C900219927 /* OrderedList.swift */, + 27E2848726BBE0C900219927 /* MessageHistoryOperation.swift */, + 27E2848826BBE0C900219927 /* PreferencesView.swift */, + 27E2848926BBE0C900219927 /* KeychainTable.swift */, + 27E2848A26BBE0C900219927 /* ItemCollectionInfoTable.swift */, + 27E2848B26BBE0C900219927 /* MessageHistoryThreadsTable.swift */, + 27E2848C26BBE0C900219927 /* MessageHistoryTable.swift */, + 27E2848D26BBE0C900219927 /* MessageHistoryIndexTable.swift */, + 27E2848E26BBE0C900219927 /* AccessChallengeDataView.swift */, + 27E2848F26BBE0C900219927 /* Message.swift */, + 27E2849026BBE0C900219927 /* PeerPresencesView.swift */, + 27E2849126BBE0C900219927 /* ChatListTable.swift */, + 27E2849226BBE0C900219927 /* ChatLocation.swift */, + 27E2849326BBE0C900219927 /* StringIndexTokens.swift */, + 27E2849426BBE0C900219927 /* AccountManager.swift */, + 27E2849526BBE0C900219927 /* PostboxStateView.swift */, + 27E2849626BBE0C900219927 /* PendingMessageActionsTable.swift */, + 27E2849726BBE0C900219927 /* TimestampBasedMessageAttributesIndexTable.swift */, + 27E2849826BBE0C900219927 /* MessageHistoryUnsentTable.swift */, + 27E2849926BBE0C900219927 /* PostboxUpgrade_16to17.swift */, + 27E2849A26BBE0C900219927 /* MessageHistoryTextIndexTable.swift */, + 27E2849B26BBE0C900219927 /* Utils */, + 27E284A726BBE0C900219927 /* MediaBoxFile.swift */, + 27E284A826BBE0C900219927 /* Table.swift */, + 27E284A926BBE0C900219927 /* ChatListIndexTable.swift */, + 27E284AA26BBE0C900219927 /* MessageHistoryTagSummaryView.swift */, + 27E284AB26BBE0C900219927 /* ItemCollection.swift */, + 27E284AC26BBE0C900219927 /* MessageHistoryHolesView.swift */, + 27E284AD26BBE0C900219927 /* RenderedPeer.swift */, + 27E284AE26BBE0C900219927 /* ChatListHolesView.swift */, + 27E284AF26BBE0C900219927 /* MutableBasicPeerView.swift */, + 27E284B026BBE0C900219927 /* PendingPeerNotificationSettingsView.swift */, + 27E284B126BBE0C900219927 /* SynchronizeGroupMessageStatsView.swift */, + 27E284B226BBE0C900219927 /* AdditionalMessageHistoryViewData.swift */, + 27E284B326BBE0C900219927 /* TempBox.swift */, + 27E284B426BBE0C900219927 /* PostboxLogging.swift */, + 27E284B526BBE0C900219927 /* PostboxUpgrade_23to24.swift */, + 27E284B626BBE0C900219927 /* HistoryTagInfoView.swift */, + 27E284B726BBE0C900219927 /* MessageHistoryMetadataTable.swift */, + 27E284B826BBE0C900219927 /* ReverseIndexReferenceTable.swift */, + 27E284B926BBE0C900219927 /* ChatListViewState.swift.bak */, + 27E284BA26BBE0C900219927 /* MessageHistoryViewState.swift */, + 27E284BB26BBE0C900219927 /* GlobalMessageHistoryTagsTable.swift */, + 27E284BC26BBE0C900219927 /* Postbox.swift */, + 27E284BD26BBE0C900219927 /* PeerNameIndexRepresentation.swift */, + 27E284BE26BBE0C900219927 /* AccountManagerSharedDataTable.swift */, + 27E284BF26BBE0C900219927 /* ChatListViewState.swift */, + 27E284C026BBE0C900219927 /* SimpleDictionary.swift */, + 27E284C126BBE0C900219927 /* PeerChatStateTable.swift */, + 27E284C226BBE0C900219927 /* OrderedItemListIndexTable.swift */, + 27E284C326BBE0C900219927 /* ContactPeerIdsView.swift */, + 27E284C426BBE0C900219927 /* CachedItemView.swift */, + 27E284C526BBE0C900219927 /* UnreadMessageCountsView.swift */, + 27E284C626BBE0C900219927 /* PeerTable.swift */, + 27E284C726BBE0C900219927 /* GlobalMessageIdsTable.swift */, + 27E284C826BBE0C900219927 /* ItemCollectionIdsView.swift */, + 27E284C926BBE0C900219927 /* MessageHistoryTagsTable.swift */, + 27E284CA26BBE0C900219927 /* MessageHistoryThreadHoleIndexTable.swift */, + 27E284CB26BBE0C900219927 /* PeerMergedOperationLogView.swift */, + 27E284CC26BBE0C900219927 /* ItemCollectionItemTable.swift */, + 27E284CD26BBE0C900219927 /* PeerPresence.swift */, + 27E284CE26BBE0C900219927 /* PeerMergedOperationLogIndexTable.swift */, + 27E284CF26BBE0C900219927 /* UnsentMessageHistoryView.swift */, + 27E284D026BBE0C900219927 /* PendingMessageActionsSummaryView.swift */, + 27E284D126BBE0C900219927 /* DeviceContactImportInfoTable.swift */, + 27E284D226BBE0C900219927 /* Media.swift */, + 27E284D326BBE0C900219927 /* PostboxUpgrade_17to18.swift */, + 27E284D426BBE0C900219927 /* UnorderedItemListTable.swift */, + 27E284D526BBE0C900219927 /* MessageHistoryAnchorIndex.swift */, + 27E284D626BBE0C900219927 /* FailedMessagesView.swift */, + 27E284D726BBE0C900219927 /* OrderedItemListEntry.swift */, + 27E284D826BBE0C900219927 /* Database.swift */, + 27E284D926BBE0C900219927 /* MessageHistoryTagsSummaryTable.swift */, + 27E284DA26BBE0C900219927 /* ManagedFile.swift */, + 27E284DB26BBE0C900219927 /* MessagesView.swift */, + 27E284DC26BBE0C900219927 /* PeerNotificationSettingsBehaviorTimestampView.swift */, + 27E284DD26BBE0C900219927 /* PeerNameIndexTable.swift */, + 27E284DE26BBE0C900219927 /* AdditionalChatListItemsView.swift */, + 27E284DF26BBE0C900219927 /* SqliteValueBox.swift */, + 27E284E026BBE0C900219927 /* MessageHistoryView.swift */, + 27E284E126BBE0C900219927 /* PeerChatInterfaceStateTable.swift */, + 27E284E226BBE0C900219927 /* FileSize.swift */, + 27E284E326BBE0C900219927 /* PostboxUpgrade_22to23.swift */, + 27E284E426BBE0C900219927 /* PeerNotificationSettingsBehaviorIndexTable.swift */, + 27E284E526BBE0C900219927 /* ItemCollectionInfosView.swift */, + 27E284E626BBE0C900219927 /* PeerView.swift */, + 27E284E726BBE0C900219927 /* CachedPeerDataView.swift */, + 27E284E826BBE0C900219927 /* PeerPresenceTable.swift */, + 27E284E926BBE0C900219927 /* MetadataTable.swift */, + 27E284EA26BBE0C900219927 /* PostboxUpgrade_24to25.swift */, + 27E284EB26BBE0C900219927 /* NoticeEntryView.swift */, + 27E284EC26BBE0C900219927 /* AllChatListHolesView.swift */, + 27E284ED26BBE0C900219927 /* OrderedItemListTable.swift */, + 27E284EE26BBE0C900219927 /* InvalidatedMessageHistoryTagsSummaryTable.swift */, + 27E284EF26BBE0C900219927 /* TimeBasedCleanup.swift */, + 27E284F026BBE0C900219927 /* GroupMessageStatsTable.swift */, + 27E284F126BBE0C900219927 /* BinarySearch.swift */, + 27E284F226BBE0C900219927 /* PostboxUpgrade_13to14.swift */, + 27E284F326BBE0C900219927 /* OrderStatisticTable.swift */, + 27E284F426BBE0C900219927 /* PendingMessageActionsMetadataTable.swift */, + 27E284F526BBE0C900219927 /* AccountSharedData.swift */, + 27E284F626BBE0C900219927 /* PinnedItemId.swift */, + 27E284F726BBE0C900219927 /* IpcPipe.swift */, + 27E284F826BBE0C900219927 /* Views.swift */, + 27E284F926BBE0C900219927 /* GlobalMessageTagsView.swift */, + 27E284FA26BBE0C900219927 /* MultiplePeersView.swift */, + 27E284FB26BBE0C900219927 /* ChatListHole.swift */, + 27E284FC26BBE0C900219927 /* MappedFile.swift */, + 27E284FD26BBE0C900219927 /* PendingPeerNotificationSettingsIndexTable.swift */, + 27E284FE26BBE0C900219927 /* PeerChatThreadInterfaceStateTable.swift */, + 27E284FF26BBE0C900219927 /* MessageOfInterestHolesView.swift */, + 27E2850026BBE0C900219927 /* ContactTable.swift */, + 27E2850126BBE0C900219927 /* SharedAccountMediaManager.swift */, + 27E2850226BBE0C900219927 /* AccountRecordsView.swift */, + 27E2850326BBE0C900219927 /* PeerOperationLogMetadataTable.swift */, + 27E2850426BBE0C900219927 /* MessageHistoryReadStateTable.swift */, + 27E2850526BBE0C900219927 /* PreferencesEntry.swift */, + 27E2850626BBE0C900219927 /* AccountManagerMetadataTable.swift */, + 27E2850726BBE0C900219927 /* MediaResourceStatus.swift */, + 27E2850826BBE0C900219927 /* ItemCacheTable.swift */, + 27E2850926BBE0C900219927 /* PeerNotificationSettingsView.swift */, + 27E2850A26BBE0C900219927 /* MessageMediaTable.swift */, + 27E2850B26BBE0C900219927 /* Peer.swift */, + 27E2850C26BBE0C900219927 /* PreferencesTable.swift */, + 27E2850D26BBE0C900219927 /* OrderedItemListView.swift */, + 27E2850E26BBE0C900219927 /* PostboxUpgrade_12to13.swift */, + 27E2850F26BBE0C900219927 /* RatingTable.swift */, + 27E2851026BBE0C900219927 /* MessageHistoryViewEntryAttributes.swift */, + 27E2851126BBE0C900219927 /* PostboxUpgrade_15to16.swift */, + 27E2851226BBE0C900219927 /* PostboxAccess.swift */, + 27E2851326BBE0C900219927 /* MessageGloballyUniqueIdTable.swift */, + 27E2851426BBE0C900219927 /* PostboxUpgrade_14to15.swift */, + 27E2851526BBE0C900219927 /* PeerChatStateView.swift */, + 27E2851626BBE0C900219927 /* SynchronizePeerReadStatesView.swift */, + 27E2851726BBE0C900219927 /* MessageView.swift */, + ); + name = Sources; + path = "../../submodules/telegram-ios/submodules/Postbox/Sources"; + sourceTree = ""; + }; + 27E2849B26BBE0C900219927 /* Utils */ = { + isa = PBXGroup; + children = ( + 27E2849C26BBE0C900219927 /* Encoder */, + 27E284A126BBE0C900219927 /* Decoder */, + 27E284A626BBE0C900219927 /* PostboxCodingAdapter.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 27E2849C26BBE0C900219927 /* Encoder */ = { + isa = PBXGroup; + children = ( + 27E2849D26BBE0C900219927 /* AdaptedPostboxEncoder.swift */, + 27E2849E26BBE0C900219927 /* AdaptedPostboxUnkeyedEncodingContainer.swift */, + 27E2849F26BBE0C900219927 /* AdaptedPostboxSingleValueEncodingContainer.swift */, + 27E284A026BBE0C900219927 /* AdaptedPostboxKeyedEncodingContainer.swift */, + ); + path = Encoder; + sourceTree = ""; + }; + 27E284A126BBE0C900219927 /* Decoder */ = { + isa = PBXGroup; + children = ( + 27E284A226BBE0C900219927 /* AdaptedPostboxDecoder.swift */, + 27E284A326BBE0C900219927 /* AdaptedPostboxSingleValueDecodingContainer.swift */, + 27E284A426BBE0C900219927 /* AdaptedPostboxKeyedDecodingContainer.swift */, + 27E284A526BBE0C900219927 /* AdaptedPostboxUnkeyedDecodingContainer.swift */, + ); + path = Decoder; + sourceTree = ""; + }; + D0B418141D7DFAF3004562A4 /* Postbox */ = { + isa = PBXGroup; + children = ( + D0B418151D7DFAF3004562A4 /* Postbox.h */, + D0B418161D7DFAF3004562A4 /* Info.plist */, + ); + path = Postbox; + sourceTree = ""; + }; + D0B4185F1D7DFE95004562A4 /* Frameworks */ = { + isa = PBXGroup; + children = ( + A7918E68240CF2A3002011CA /* StringTransliteration.framework */, + A7918DDE240CEFFA002011CA /* MurMurHash32.framework */, + A701F934236C1DC4002ABF81 /* Crc32.framework */, + A701F92D236C1D02002ABF81 /* crc32.framework */, + A701F92B236C1B66002ABF81 /* SwiftSignalKit.framework */, + A701F924236B3350002ABF81 /* sqlcipher.framework */, + A701F8F1236B2E61002ABF81 /* SwiftSignalKitMac.framework */, + A701F8EF236B2E5A002ABF81 /* libphonenumbermac.framework */, + A701F8ED236B2E54002ABF81 /* crc32mac.framework */, + D03E463C2306E0F60049C28B /* crc32mac.framework */, + D03E462B2306E01C0049C28B /* sqlciphermac.framework */, + D03E457E2305CD130049C28B /* Foundation.framework */, + D03E457C2305CD090049C28B /* Crc32.framework */, + D03E457A2305CD000049C28B /* Crc32.framework */, + D03E45562305C7C90049C28B /* sqlcipher.framework */, + D0B418601D7DFE95004562A4 /* SwiftSignalKitMac.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + D0E3A7401B28A7E300A402D9 = { + isa = PBXGroup; + children = ( + 27E2844F26BBE0C900219927 /* Sources */, + D0B418141D7DFAF3004562A4 /* Postbox */, + D0E3A74B1B28A7E300A402D9 /* Products */, + D0B4185F1D7DFE95004562A4 /* Frameworks */, + ); + sourceTree = ""; + }; + D0E3A74B1B28A7E300A402D9 /* Products */ = { + isa = PBXGroup; + children = ( + D0B418131D7DFAF2004562A4 /* Postbox.framework */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + D0B418101D7DFAF2004562A4 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D0B418171D7DFAF3004562A4 /* Postbox.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + D0B418121D7DFAF2004562A4 /* Postbox */ = { + isa = PBXNativeTarget; + buildConfigurationList = D0B4181B1D7DFAF3004562A4 /* Build configuration list for PBXNativeTarget "Postbox" */; + buildPhases = ( + D0B4180E1D7DFAF2004562A4 /* Sources */, + D0B4180F1D7DFAF2004562A4 /* Frameworks */, + D0B418101D7DFAF2004562A4 /* Headers */, + D0B418111D7DFAF2004562A4 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Postbox; + productName = PostboxMac; + productReference = D0B418131D7DFAF2004562A4 /* Postbox.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D0E3A7411B28A7E300A402D9 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftMigration = 0700; + LastSwiftUpdateCheck = 0700; + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = Telegram; + TargetAttributes = { + D0B418121D7DFAF2004562A4 = { + CreatedOnToolsVersion = 8.0; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = D0E3A7441B28A7E300A402D9 /* Build configuration list for PBXProject "Postbox_Xcode" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + English, + en, + ); + mainGroup = D0E3A7401B28A7E300A402D9; + productRefGroup = D0E3A74B1B28A7E300A402D9 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D0B418121D7DFAF2004562A4 /* Postbox */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D0B418111D7DFAF2004562A4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 27E2857E26BBE0C900219927 /* ChatListViewState.swift.bak in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D0B4180E1D7DFAF2004562A4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 27E285C926BBE0C900219927 /* MessageHistoryReadStateTable.swift in Sources */, + 27E2855226BBE0C900219927 /* ItemCollectionInfoTable.swift in Sources */, + 27E2856026BBE0C900219927 /* MessageHistoryUnsentTable.swift in Sources */, + 27E2852B26BBE0C900219927 /* SimpleSet.swift in Sources */, + 27E2858726BBE0C900219927 /* OrderedItemListIndexTable.swift in Sources */, + 27E2857F26BBE0C900219927 /* MessageHistoryViewState.swift in Sources */, + 27E2852D26BBE0C900219927 /* PeerNotificationSettings.swift in Sources */, + 27E285B526BBE0C900219927 /* GroupMessageStatsTable.swift in Sources */, + 27E2859A26BBE0C900219927 /* MessageHistoryAnchorIndex.swift in Sources */, + 27E2855626BBE0C900219927 /* AccessChallengeDataView.swift in Sources */, + 27E285CD26BBE0C900219927 /* ItemCacheTable.swift in Sources */, + 27E2852726BBE0C900219927 /* InvalidatedGroupMessageStatsTable.swift in Sources */, + 27E285DC26BBE0C900219927 /* MessageView.swift in Sources */, + 27E2857D26BBE0C900219927 /* ReverseIndexReferenceTable.swift in Sources */, + 27E285D426BBE0C900219927 /* RatingTable.swift in Sources */, + 27E285CC26BBE0C900219927 /* MediaResourceStatus.swift in Sources */, + 27E2858626BBE0C900219927 /* PeerChatStateTable.swift in Sources */, + 27E2854426BBE0C900219927 /* PeerNotificationSettingsBehaviorTable.swift in Sources */, + 27E2853926BBE0C900219927 /* ItemCollectionInfoView.swift in Sources */, + 27E2851B26BBE0C900219927 /* Upgrades.swift in Sources */, + 27E2854326BBE0C900219927 /* ItemCollectionsView.swift in Sources */, + 27E285D826BBE0C900219927 /* MessageGloballyUniqueIdTable.swift in Sources */, + 27E285B426BBE0C900219927 /* TimeBasedCleanup.swift in Sources */, + 27E2852F26BBE0C900219927 /* ChatListView.swift in Sources */, + 27E2856926BBE0C900219927 /* AdaptedPostboxKeyedDecodingContainer.swift in Sources */, + 27E2857426BBE0C900219927 /* MutableBasicPeerView.swift in Sources */, + 27E2856D26BBE0C900219927 /* Table.swift in Sources */, + 27E2856A26BBE0C900219927 /* AdaptedPostboxUnkeyedDecodingContainer.swift in Sources */, + 27E285C826BBE0C900219927 /* PeerOperationLogMetadataTable.swift in Sources */, + 27E2854A26BBE0C900219927 /* RedBlackTree.swift in Sources */, + 27E285BD26BBE0C900219927 /* Views.swift in Sources */, + 27E2854126BBE0C900219927 /* MessageHistorySynchronizeReadStateTable.swift in Sources */, + 27E2852826BBE0C900219927 /* ViewTracker.swift in Sources */, + 27E2859E26BBE0C900219927 /* MessageHistoryTagsSummaryTable.swift in Sources */, + 27E2858826BBE0C900219927 /* ContactPeerIdsView.swift in Sources */, + 27E2855A26BBE0C900219927 /* ChatLocation.swift in Sources */, + 27E2855526BBE0C900219927 /* MessageHistoryIndexTable.swift in Sources */, + 27E285A126BBE0C900219927 /* PeerNotificationSettingsBehaviorTimestampView.swift in Sources */, + 27E285BB26BBE0C900219927 /* PinnedItemId.swift in Sources */, + 27E285B926BBE0C900219927 /* PendingMessageActionsMetadataTable.swift in Sources */, + 27E2857826BBE0C900219927 /* TempBox.swift in Sources */, + 27E285B326BBE0C900219927 /* InvalidatedMessageHistoryTagsSummaryTable.swift in Sources */, + 27E2851826BBE0C900219927 /* TimestampBasedMessageAttributesTable.swift in Sources */, + 27E2852126BBE0C900219927 /* MessageHistoryHoleIndexTable.swift in Sources */, + 27E285B226BBE0C900219927 /* OrderedItemListTable.swift in Sources */, + 27E2856F26BBE0C900219927 /* MessageHistoryTagSummaryView.swift in Sources */, + 27E2857C26BBE0C900219927 /* MessageHistoryMetadataTable.swift in Sources */, + 27E285BE26BBE0C900219927 /* GlobalMessageTagsView.swift in Sources */, + 27E2851926BBE0C900219927 /* ItemCacheMetaTable.swift in Sources */, + 27E2857526BBE0C900219927 /* PendingPeerNotificationSettingsView.swift in Sources */, + 27E2855D26BBE0C900219927 /* PostboxStateView.swift in Sources */, + 27E2856326BBE0C900219927 /* AdaptedPostboxEncoder.swift in Sources */, + 27E285D126BBE0C900219927 /* PreferencesTable.swift in Sources */, + 27E2853D26BBE0C900219927 /* MediaBox.swift in Sources */, + 27E285AD26BBE0C900219927 /* PeerPresenceTable.swift in Sources */, + 27E2854E26BBE0C900219927 /* OrderedList.swift in Sources */, + 27E2856426BBE0C900219927 /* AdaptedPostboxUnkeyedEncodingContainer.swift in Sources */, + 27E285A226BBE0C900219927 /* PeerNameIndexTable.swift in Sources */, + 27E2851D26BBE0C900219927 /* UnsentMessageIndicesView.swift in Sources */, + 27E2858E26BBE0C900219927 /* MessageHistoryTagsTable.swift in Sources */, + 27E2858926BBE0C900219927 /* CachedItemView.swift in Sources */, + 27E285CA26BBE0C900219927 /* PreferencesEntry.swift in Sources */, + 27E2853526BBE0C900219927 /* PostboxUpgrade_21to22.swift in Sources */, + 27E2857926BBE0C900219927 /* PostboxLogging.swift in Sources */, + 27E2858026BBE0C900219927 /* GlobalMessageHistoryTagsTable.swift in Sources */, + 27E285BC26BBE0C900219927 /* IpcPipe.swift in Sources */, + 27E2853126BBE0C900219927 /* MutablePeerChatInclusionView.swift in Sources */, + 27E2857126BBE0C900219927 /* MessageHistoryHolesView.swift in Sources */, + 27E2856B26BBE0C900219927 /* PostboxCodingAdapter.swift in Sources */, + 27E2855026BBE0C900219927 /* PreferencesView.swift in Sources */, + 27E2857A26BBE0C900219927 /* PostboxUpgrade_23to24.swift in Sources */, + 27E2851C26BBE0C900219927 /* Coding.swift in Sources */, + 27E2858126BBE0C900219927 /* Postbox.swift in Sources */, + 27E2854226BBE0C900219927 /* PendingMessageActionsView.swift in Sources */, + 27E2857726BBE0C900219927 /* AdditionalMessageHistoryViewData.swift in Sources */, + 27E285A826BBE0C900219927 /* PostboxUpgrade_22to23.swift in Sources */, + 27E2855C26BBE0C900219927 /* AccountManager.swift in Sources */, + 27E2859126BBE0C900219927 /* ItemCollectionItemTable.swift in Sources */, + 27E285C726BBE0C900219927 /* AccountRecordsView.swift in Sources */, + 27E2853226BBE0C900219927 /* PostboxUpgrade_19to20.swift in Sources */, + 27E2858F26BBE0C900219927 /* MessageHistoryThreadHoleIndexTable.swift in Sources */, + 27E2857026BBE0C900219927 /* ItemCollection.swift in Sources */, + 27E285D626BBE0C900219927 /* PostboxUpgrade_15to16.swift in Sources */, + 27E2852A26BBE0C900219927 /* AdditionalChatListItemsTable.swift in Sources */, + 27E285C026BBE0C900219927 /* ChatListHole.swift in Sources */, + 27E2858A26BBE0C900219927 /* UnreadMessageCountsView.swift in Sources */, + 27E2854826BBE0C900219927 /* InitialMessageHistoryData.swift in Sources */, + 27E2854926BBE0C900219927 /* IntermediateMessage.swift in Sources */, + 27E2853F26BBE0C900219927 /* AccountManagerRecordTable.swift in Sources */, + 27E2856226BBE0C900219927 /* MessageHistoryTextIndexTable.swift in Sources */, + 27E2857226BBE0C900219927 /* RenderedPeer.swift in Sources */, + 27E2859826BBE0C900219927 /* PostboxUpgrade_17to18.swift in Sources */, + 27E2859926BBE0C900219927 /* UnorderedItemListTable.swift in Sources */, + 27E2852026BBE0C900219927 /* InvalidatedMessageHistoryTagSummariesView.swift in Sources */, + 27E285D026BBE0C900219927 /* Peer.swift in Sources */, + 27E285A326BBE0C900219927 /* AdditionalChatListItemsView.swift in Sources */, + 27E2851E26BBE0C900219927 /* ReverseAssociatedPeerTable.swift in Sources */, + 27E285C626BBE0C900219927 /* SharedAccountMediaManager.swift in Sources */, + 27E2858426BBE0C900219927 /* ChatListViewState.swift in Sources */, + 27E2853B26BBE0C900219927 /* MessageHistoryFailedTable.swift in Sources */, + 27E285B126BBE0C900219927 /* AllChatListHolesView.swift in Sources */, + 27E2859426BBE0C900219927 /* UnsentMessageHistoryView.swift in Sources */, + 27E285A626BBE0C900219927 /* PeerChatInterfaceStateTable.swift in Sources */, + 27E285D526BBE0C900219927 /* MessageHistoryViewEntryAttributes.swift in Sources */, + 27E2855126BBE0C900219927 /* KeychainTable.swift in Sources */, + 27E2856126BBE0C900219927 /* PostboxUpgrade_16to17.swift in Sources */, + 27E2853326BBE0C900219927 /* AccountManagerAtomicState.swift in Sources */, + 27E285D326BBE0C900219927 /* PostboxUpgrade_12to13.swift in Sources */, + 27E2853726BBE0C900219927 /* PeerChatListInclusion.swift in Sources */, + 27E285A926BBE0C900219927 /* PeerNotificationSettingsBehaviorIndexTable.swift in Sources */, + 27E2855926BBE0C900219927 /* ChatListTable.swift in Sources */, + 27E2855326BBE0C900219927 /* MessageHistoryThreadsTable.swift in Sources */, + 27E2856726BBE0C900219927 /* AdaptedPostboxDecoder.swift in Sources */, + 27E2855F26BBE0C900219927 /* TimestampBasedMessageAttributesIndexTable.swift in Sources */, + 27E2855426BBE0C900219927 /* MessageHistoryTable.swift in Sources */, + 27E285AC26BBE0C900219927 /* CachedPeerDataView.swift in Sources */, + 27E285B726BBE0C900219927 /* PostboxUpgrade_13to14.swift in Sources */, + 27E2859F26BBE0C900219927 /* ManagedFile.swift in Sources */, + 27E285AE26BBE0C900219927 /* MetadataTable.swift in Sources */, + 27E2856C26BBE0C900219927 /* MediaBoxFile.swift in Sources */, + 27E285AB26BBE0C900219927 /* PeerView.swift in Sources */, + 27E2856626BBE0C900219927 /* AdaptedPostboxKeyedEncodingContainer.swift in Sources */, + 27E285D926BBE0C900219927 /* PostboxUpgrade_14to15.swift in Sources */, + 27E2859726BBE0C900219927 /* Media.swift in Sources */, + 27E285C126BBE0C900219927 /* MappedFile.swift in Sources */, + 27E2853826BBE0C900219927 /* PostboxTransaction.swift in Sources */, + 27E285BF26BBE0C900219927 /* MultiplePeersView.swift in Sources */, + 27E2855B26BBE0C900219927 /* StringIndexTokens.swift in Sources */, + 27E2856526BBE0C900219927 /* AdaptedPostboxSingleValueEncodingContainer.swift in Sources */, + 27E2854626BBE0C900219927 /* PeerGroup.swift in Sources */, + 27E2854D26BBE0C900219927 /* PostboxUpgrade_18to19.swift in Sources */, + 27E285BA26BBE0C900219927 /* AccountSharedData.swift in Sources */, + 27E285C526BBE0C900219927 /* ContactTable.swift in Sources */, + 27E285C326BBE0C900219927 /* PeerChatThreadInterfaceStateTable.swift in Sources */, + 27E2857626BBE0C900219927 /* SynchronizeGroupMessageStatsView.swift in Sources */, + 27E285A426BBE0C900219927 /* SqliteValueBox.swift in Sources */, + 27E285B026BBE0C900219927 /* NoticeEntryView.swift in Sources */, + 27E2852226BBE0C900219927 /* LocalMessageHistoryTagsTable.swift in Sources */, + 27E285D726BBE0C900219927 /* PostboxAccess.swift in Sources */, + 27E2853026BBE0C900219927 /* PeerOperationLogTable.swift in Sources */, + 27E285C426BBE0C900219927 /* MessageOfInterestHolesView.swift in Sources */, + 27E2858C26BBE0C900219927 /* GlobalMessageIdsTable.swift in Sources */, + 27E2851F26BBE0C900219927 /* PostboxView.swift in Sources */, + 27E2859626BBE0C900219927 /* DeviceContactImportInfoTable.swift in Sources */, + 27E2852E26BBE0C900219927 /* ValueBox.swift in Sources */, + 27E2855726BBE0C900219927 /* Message.swift in Sources */, + 27E2859026BBE0C900219927 /* PeerMergedOperationLogView.swift in Sources */, + 27E2859526BBE0C900219927 /* PendingMessageActionsSummaryView.swift in Sources */, + 27E2859C26BBE0C900219927 /* OrderedItemListEntry.swift in Sources */, + 27E2853A26BBE0C900219927 /* TimestampBasedMessageAttributesView.swift in Sources */, + 27E2852626BBE0C900219927 /* Hash.swift in Sources */, + 27E285A026BBE0C900219927 /* MessagesView.swift in Sources */, + 27E2852526BBE0C900219927 /* MediaResource.swift in Sources */, + 27E2854B26BBE0C900219927 /* ContactPeersView.swift in Sources */, + 27E2853626BBE0C900219927 /* PeerChatTopIndexableMessageIds.swift in Sources */, + 27E2858D26BBE0C900219927 /* ItemCollectionIdsView.swift in Sources */, + 27E285CB26BBE0C900219927 /* AccountManagerMetadataTable.swift in Sources */, + 27E285A726BBE0C900219927 /* FileSize.swift in Sources */, + 27E285C226BBE0C900219927 /* PendingPeerNotificationSettingsIndexTable.swift in Sources */, + 27E2855826BBE0C900219927 /* PeerPresencesView.swift in Sources */, + 27E2851A26BBE0C900219927 /* PeerReadState.swift in Sources */, + 27E2859326BBE0C900219927 /* PeerMergedOperationLogIndexTable.swift in Sources */, + 27E2855E26BBE0C900219927 /* PendingMessageActionsTable.swift in Sources */, + 27E285D226BBE0C900219927 /* OrderedItemListView.swift in Sources */, + 27E2857B26BBE0C900219927 /* HistoryTagInfoView.swift in Sources */, + 27E285A526BBE0C900219927 /* MessageHistoryView.swift in Sources */, + 27E285AA26BBE0C900219927 /* ItemCollectionInfosView.swift in Sources */, + 27E285CE26BBE0C900219927 /* PeerNotificationSettingsView.swift in Sources */, + 27E2854726BBE0C900219927 /* NoticeTable.swift in Sources */, + 27E2853426BBE0C900219927 /* CachedPeerDataTable.swift in Sources */, + 27E2858526BBE0C900219927 /* SimpleDictionary.swift in Sources */, + 27E2854C26BBE0C900219927 /* PeerChatInterfaceState.swift in Sources */, + 27E2857326BBE0C900219927 /* ChatListHolesView.swift in Sources */, + 27E2859B26BBE0C900219927 /* FailedMessagesView.swift in Sources */, + 27E2854526BBE0C900219927 /* AccountRecord.swift in Sources */, + 27E2854F26BBE0C900219927 /* MessageHistoryOperation.swift in Sources */, + 27E2858226BBE0C900219927 /* PeerNameIndexRepresentation.swift in Sources */, + 27E285CF26BBE0C900219927 /* MessageMediaTable.swift in Sources */, + 27E285B826BBE0C900219927 /* OrderStatisticTable.swift in Sources */, + 27E2859226BBE0C900219927 /* PeerPresence.swift in Sources */, + 27E285AF26BBE0C900219927 /* PostboxUpgrade_24to25.swift in Sources */, + 27E2852926BBE0C900219927 /* ValueBoxKey.swift in Sources */, + 27E2856E26BBE0C900219927 /* ChatListIndexTable.swift in Sources */, + 27E285DA26BBE0C900219927 /* PeerChatStateView.swift in Sources */, + 27E2856826BBE0C900219927 /* AdaptedPostboxSingleValueDecodingContainer.swift in Sources */, + 27E285B626BBE0C900219927 /* BinarySearch.swift in Sources */, + 27E2852426BBE0C900219927 /* CachedPeerData.swift in Sources */, + 27E2854026BBE0C900219927 /* TopChatMessageView.swift in Sources */, + 27E2852C26BBE0C900219927 /* PostboxUpgrade_20to21.swift in Sources */, + 27E2853E26BBE0C900219927 /* LocalMessageTagsView.swift in Sources */, + 27E2858B26BBE0C900219927 /* PeerTable.swift in Sources */, + 27E2858326BBE0C900219927 /* AccountManagerSharedDataTable.swift in Sources */, + 27E2852326BBE0C900219927 /* SeedConfiguration.swift in Sources */, + 27E2859D26BBE0C900219927 /* Database.swift in Sources */, + 27E285DB26BBE0C900219927 /* SynchronizePeerReadStatesView.swift in Sources */, + 27E2853C26BBE0C900219927 /* PeerNotificationSettingsTable.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A7F282D8238EAB3C00742C20 /* Github */ = { + isa = XCBuildConfiguration; + 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_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + A7F282D9238EAB3C00742C20 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ""; + INFOPLIST_FILE = Postbox/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + OTHER_CFLAGS = ( + "-DSQLITE_HAS_CODEC=1", + "-DSQLCIPHER_CRYPTO_CC=1", + "-DSQLITE_ENABLE_FTS5", + "-DSQLITE_DEFAULT_MEMSTATUS=0", + "-DNDEBUG", + ); + OTHER_SWIFT_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Telegram.PostboxMac; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; + SWIFT_VERSION = 5.0; + }; + name = Github; + }; + C22069C81E8EB4BF00E82730 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + C22069CB1E8EB4BF00E82730 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = Postbox/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + OTHER_CFLAGS = ( + "-DSQLITE_HAS_CODEC=1", + "-DSQLCIPHER_CRYPTO_CC=1", + "-DSQLITE_ENABLE_FTS5", + "-DSQLITE_DEFAULT_MEMSTATUS=0", + "-DNDEBUG", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Telegram.PostboxMac; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 5.0; + }; + name = ReleaseHockeyapp; + }; + D0364D5122B3E385002A6EF0 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + D0364D5422B3E385002A6EF0 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_OPTIMIZATION_LEVEL = s; + GCC_PREPROCESSOR_DEFINITIONS = ""; + INFOPLIST_FILE = Postbox/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + OTHER_CFLAGS = ( + "-DSQLITE_HAS_CODEC=1", + "-DSQLCIPHER_CRYPTO_CC=1", + "-DSQLITE_ENABLE_FTS5", + "-DSQLITE_DEFAULT_MEMSTATUS=0", + "-DNDEBUG", + ); + OTHER_SWIFT_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Telegram.PostboxMac; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 5.0; + }; + name = HockeyappMacAlpha; + }; + D079FD0C1F06BE070038FADE /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + D079FD0F1F06BE070038FADE /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_IDENTITY = "Mac Developer"; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_OPTIMIZATION_LEVEL = 0; + INFOPLIST_FILE = Postbox/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = ( + "-DSQLITE_HAS_CODEC=1", + "-DSQLCIPHER_CRYPTO_CC=1", + "-DSQLITE_ENABLE_FTS5", + "-DSQLITE_DEFAULT_MEMSTATUS=0", + "-DNDEBUG", + ); + OTHER_SWIFT_FLAGS = "-DDEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Telegram.PostboxMac; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_VERSION = 5.0; + }; + name = DebugAppStore; + }; + D086A5711CC0116A00F08284 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + D0B418181D7DFAF3004562A4 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_OPTIMIZATION_LEVEL = s; + GCC_PREPROCESSOR_DEFINITIONS = ""; + INFOPLIST_FILE = Postbox/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + OTHER_CFLAGS = ( + "-DSQLITE_HAS_CODEC=1", + "-DSQLCIPHER_CRYPTO_CC=1", + "-DSQLITE_ENABLE_FTS5", + "-DSQLITE_DEFAULT_MEMSTATUS=0", + "-DNDEBUG", + ); + OTHER_SWIFT_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Telegram.PostboxMac; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 5.0; + }; + name = DebugHockeyapp; + }; + D0B4181A1D7DFAF3004562A4 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = Postbox/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + OTHER_CFLAGS = ( + "-DSQLITE_HAS_CODEC=1", + "-DSQLCIPHER_CRYPTO_CC=1", + "-DSQLITE_ENABLE_FTS5", + "-DSQLITE_DEFAULT_MEMSTATUS=0", + "-DNDEBUG", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Telegram.PostboxMac; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 5.0; + }; + name = ReleaseAppStore; + }; + D0E3A75E1B28A7E300A402D9 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D0B4181B1D7DFAF3004562A4 /* Build configuration list for PBXNativeTarget "Postbox" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0B418181D7DFAF3004562A4 /* DebugHockeyapp */, + D0364D5422B3E385002A6EF0 /* HockeyappMacAlpha */, + D079FD0F1F06BE070038FADE /* DebugAppStore */, + A7F282D9238EAB3C00742C20 /* Github */, + C22069CB1E8EB4BF00E82730 /* ReleaseHockeyapp */, + D0B4181A1D7DFAF3004562A4 /* ReleaseAppStore */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = DebugHockeyapp; + }; + D0E3A7441B28A7E300A402D9 /* Build configuration list for PBXProject "Postbox_Xcode" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0E3A75E1B28A7E300A402D9 /* DebugHockeyapp */, + D0364D5122B3E385002A6EF0 /* HockeyappMacAlpha */, + D079FD0C1F06BE070038FADE /* DebugAppStore */, + A7F282D8238EAB3C00742C20 /* Github */, + C22069C81E8EB4BF00E82730 /* ReleaseHockeyapp */, + D086A5711CC0116A00F08284 /* ReleaseAppStore */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = DebugHockeyapp; + }; +/* End XCConfigurationList section */ + }; + rootObject = D0E3A7411B28A7E300A402D9 /* Project object */; +} diff --git a/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..088de55706 --- /dev/null +++ b/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate b/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000..1f36f24a42 Binary files /dev/null and b/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/Postbox.xcscheme b/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/Postbox.xcscheme new file mode 100644 index 0000000000..2a2d0f2587 --- /dev/null +++ b/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/Postbox.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/PostboxMac.xcscheme b/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/PostboxMac.xcscheme new file mode 100644 index 0000000000..136c3391e9 --- /dev/null +++ b/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/PostboxMac.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/PostboxTests.xcscheme b/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/PostboxTests.xcscheme new file mode 100644 index 0000000000..83d158d854 --- /dev/null +++ b/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/PostboxTests.xcscheme @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/xcuserdata/mikhailfilimonov.xcuserdatad/xcschemes/xcschememanagement.plist b/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/xcuserdata/mikhailfilimonov.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100755 index 0000000000..32c3a46156 --- /dev/null +++ b/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/xcuserdata/mikhailfilimonov.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,24 @@ + + + + + SchemeUserState + + Postbox.xcscheme_^#shared#^_ + + orderHint + 20 + + PostboxMac.xcscheme_^#shared#^_ + + orderHint + 37 + + PostboxTests.xcscheme_^#shared#^_ + + orderHint + 21 + + + + diff --git a/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/Postbox.xcscheme b/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/Postbox.xcscheme new file mode 100644 index 0000000000..2a2d0f2587 --- /dev/null +++ b/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/Postbox.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist b/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000000..20613542da --- /dev/null +++ b/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,24 @@ + + + + + SchemeUserState + + Postbox.xcscheme + + isShown + + orderHint + 14 + + + SuppressBuildableAutocreation + + D0B418121D7DFAF2004562A4 + + primary + + + + + diff --git a/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/xcuserdata/telegram.xcuserdatad/xcschemes/xcschememanagement.plist b/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/xcuserdata/telegram.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000000..e737be8e3e --- /dev/null +++ b/core-xprojects/Postbox/Postbox_Xcode.xcodeproj/xcuserdata/telegram.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,24 @@ + + + + + SchemeUserState + + Postbox.xcscheme_^#shared#^_ + + orderHint + 33 + + PostboxMac.xcscheme_^#shared#^_ + + orderHint + 43 + + PostboxTests.xcscheme_^#shared#^_ + + orderHint + 34 + + + + diff --git a/core-xprojects/Reachability/Reachability.xcodeproj/project.pbxproj b/core-xprojects/Reachability/Reachability.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..6a1cde03b3 --- /dev/null +++ b/core-xprojects/Reachability/Reachability.xcodeproj/project.pbxproj @@ -0,0 +1,708 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + A76D02D925C403C500FCE1B5 /* Reachability.h in Headers */ = {isa = PBXBuildFile; fileRef = A76D02D725C403C500FCE1B5 /* Reachability.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A76D02E125C403CF00FCE1B5 /* Reachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76D02E025C403CF00FCE1B5 /* Reachability.swift */; }; + A76D02F325C4049700FCE1B5 /* LegacyReachability.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A76D02F225C4049700FCE1B5 /* LegacyReachability.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A76D02D425C403C500FCE1B5 /* Reachability.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Reachability.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A76D02D725C403C500FCE1B5 /* Reachability.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Reachability.h; sourceTree = ""; }; + A76D02D825C403C500FCE1B5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A76D02E025C403CF00FCE1B5 /* Reachability.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Reachability.swift; path = "../../../submodules/telegram-ios/submodules/Reachability/Sources/Reachability.swift"; sourceTree = ""; }; + A76D02F225C4049700FCE1B5 /* LegacyReachability.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LegacyReachability.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A76D02D125C403C500FCE1B5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A76D02F325C4049700FCE1B5 /* LegacyReachability.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A76D02CA25C403C500FCE1B5 = { + isa = PBXGroup; + children = ( + A76D02D625C403C500FCE1B5 /* Reachability */, + A76D02D525C403C500FCE1B5 /* Products */, + A76D02F125C4049700FCE1B5 /* Frameworks */, + ); + sourceTree = ""; + }; + A76D02D525C403C500FCE1B5 /* Products */ = { + isa = PBXGroup; + children = ( + A76D02D425C403C500FCE1B5 /* Reachability.framework */, + ); + name = Products; + sourceTree = ""; + }; + A76D02D625C403C500FCE1B5 /* Reachability */ = { + isa = PBXGroup; + children = ( + A76D02E025C403CF00FCE1B5 /* Reachability.swift */, + A76D02D725C403C500FCE1B5 /* Reachability.h */, + A76D02D825C403C500FCE1B5 /* Info.plist */, + ); + path = Reachability; + sourceTree = ""; + }; + A76D02F125C4049700FCE1B5 /* Frameworks */ = { + isa = PBXGroup; + children = ( + A76D02F225C4049700FCE1B5 /* LegacyReachability.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + A76D02CF25C403C500FCE1B5 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + A76D02D925C403C500FCE1B5 /* Reachability.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + A76D02D325C403C500FCE1B5 /* Reachability */ = { + isa = PBXNativeTarget; + buildConfigurationList = A76D02DC25C403C500FCE1B5 /* Build configuration list for PBXNativeTarget "Reachability" */; + buildPhases = ( + A76D02CF25C403C500FCE1B5 /* Headers */, + A76D02D025C403C500FCE1B5 /* Sources */, + A76D02D125C403C500FCE1B5 /* Frameworks */, + A76D02D225C403C500FCE1B5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Reachability; + productName = Reachability; + productReference = A76D02D425C403C500FCE1B5 /* Reachability.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A76D02CB25C403C500FCE1B5 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1240; + TargetAttributes = { + A76D02D325C403C500FCE1B5 = { + CreatedOnToolsVersion = 12.4; + LastSwiftMigration = 1240; + }; + }; + }; + buildConfigurationList = A76D02CE25C403C500FCE1B5 /* Build configuration list for PBXProject "Reachability" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = A76D02CA25C403C500FCE1B5; + productRefGroup = A76D02D525C403C500FCE1B5 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A76D02D325C403C500FCE1B5 /* Reachability */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A76D02D225C403C500FCE1B5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A76D02D025C403C500FCE1B5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A76D02E125C403CF00FCE1B5 /* Reachability.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A76D02DA25C403C500FCE1B5 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + A76D02DB25C403C500FCE1B5 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + A76D02DD25C403C500FCE1B5 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Reachability/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Reachability; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = DebugAppStore; + }; + A76D02DE25C403C500FCE1B5 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Reachability/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Reachability; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = ReleaseAppStore; + }; + A76D02E425C403F900FCE1B5 /* Github */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + A76D02E525C403F900FCE1B5 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Reachability/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Reachability; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Github; + }; + A76D02E625C4040200FCE1B5 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + A76D02E725C4040200FCE1B5 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Reachability/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Reachability; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = ReleaseHockeyapp; + }; + A76D02E825C4040700FCE1B5 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + A76D02E925C4040700FCE1B5 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Reachability/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Reachability; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = HockeyappMacAlpha; + }; + A76D02EA25C4041400FCE1B5 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + A76D02EB25C4041400FCE1B5 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Reachability/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Reachability; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = DebugHockeyapp; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A76D02CE25C403C500FCE1B5 /* Build configuration list for PBXProject "Reachability" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A76D02DA25C403C500FCE1B5 /* DebugAppStore */, + A76D02E425C403F900FCE1B5 /* Github */, + A76D02DB25C403C500FCE1B5 /* ReleaseAppStore */, + A76D02E625C4040200FCE1B5 /* ReleaseHockeyapp */, + A76D02E825C4040700FCE1B5 /* HockeyappMacAlpha */, + A76D02EA25C4041400FCE1B5 /* DebugHockeyapp */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; + A76D02DC25C403C500FCE1B5 /* Build configuration list for PBXNativeTarget "Reachability" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A76D02DD25C403C500FCE1B5 /* DebugAppStore */, + A76D02E525C403F900FCE1B5 /* Github */, + A76D02DE25C403C500FCE1B5 /* ReleaseAppStore */, + A76D02E725C4040200FCE1B5 /* ReleaseHockeyapp */, + A76D02E925C4040700FCE1B5 /* HockeyappMacAlpha */, + A76D02EB25C4041400FCE1B5 /* DebugHockeyapp */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; +/* End XCConfigurationList section */ + }; + rootObject = A76D02CB25C403C500FCE1B5 /* Project object */; +} diff --git a/core-xprojects/Reachability/Reachability.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/Reachability/Reachability.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/core-xprojects/Reachability/Reachability.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/Reachability/Reachability.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/Reachability/Reachability.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/Reachability/Reachability.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/Reachability/Reachability/Info.plist b/core-xprojects/Reachability/Reachability/Info.plist new file mode 100644 index 0000000000..9bcb244429 --- /dev/null +++ b/core-xprojects/Reachability/Reachability/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/core-xprojects/Reachability/Reachability/Reachability.h b/core-xprojects/Reachability/Reachability/Reachability.h new file mode 100644 index 0000000000..65c2557289 --- /dev/null +++ b/core-xprojects/Reachability/Reachability/Reachability.h @@ -0,0 +1,18 @@ +// +// Reachability.h +// Reachability +// +// Created by Mikhail Filimonov on 29.01.2021. +// + +#import + +//! Project version number for Reachability. +FOUNDATION_EXPORT double ReachabilityVersionNumber; + +//! Project version string for Reachability. +FOUNDATION_EXPORT const unsigned char ReachabilityVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/project.pbxproj b/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..e891701b04 --- /dev/null +++ b/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/project.pbxproj @@ -0,0 +1,1617 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 27D0067525AEFD9E00EE3EB1 /* SQueueLocalObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 27D0067325AEFD9E00EE3EB1 /* SQueueLocalObject.m */; }; + 27D0067625AEFD9E00EE3EB1 /* SQueueLocalObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 27D0067425AEFD9E00EE3EB1 /* SQueueLocalObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A791908B240CFCE4002011CA /* STimer.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919055240CFCE1002011CA /* STimer.h */; }; + A791908C240CFCE4002011CA /* SSignal+Pipe.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919056240CFCE1002011CA /* SSignal+Pipe.h */; }; + A791908D240CFCE4002011CA /* SDisposableSet.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919057240CFCE1002011CA /* SDisposableSet.h */; }; + A791908E240CFCE4002011CA /* SSignal.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919058240CFCE1002011CA /* SSignal.h */; }; + A791908F240CFCE4002011CA /* SSignal+Accumulate.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919059240CFCE1002011CA /* SSignal+Accumulate.h */; }; + A7919091240CFCE4002011CA /* SThreadPoolQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = A791905B240CFCE1002011CA /* SThreadPoolQueue.h */; }; + A7919092240CFCE4002011CA /* SSignal+Meta.h in Headers */ = {isa = PBXBuildFile; fileRef = A791905C240CFCE1002011CA /* SSignal+Meta.h */; }; + A7919093240CFCE4002011CA /* SVariable.h in Headers */ = {isa = PBXBuildFile; fileRef = A791905D240CFCE1002011CA /* SVariable.h */; }; + A7919095240CFCE4002011CA /* SThreadPoolTask.h in Headers */ = {isa = PBXBuildFile; fileRef = A791905F240CFCE1002011CA /* SThreadPoolTask.h */; }; + A7919098240CFCE4002011CA /* SSignal+SideEffects.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919062240CFCE1002011CA /* SSignal+SideEffects.h */; }; + A791909A240CFCE4002011CA /* SSignalKit.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919064240CFCE2002011CA /* SSignalKit.h */; }; + A791909D240CFCE4002011CA /* SSignal+Dispatch.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919067240CFCE2002011CA /* SSignal+Dispatch.h */; }; + A791909E240CFCE4002011CA /* SSignal+Combine.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919068240CFCE2002011CA /* SSignal+Combine.h */; }; + A79190A4240CFCE4002011CA /* SMetaDisposable.h in Headers */ = {isa = PBXBuildFile; fileRef = A791906E240CFCE2002011CA /* SMetaDisposable.h */; }; + A79190A7240CFCE4002011CA /* SBlockDisposable.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919071240CFCE2002011CA /* SBlockDisposable.h */; }; + A79190A8240CFCE4002011CA /* SAtomic.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919072240CFCE2002011CA /* SAtomic.h */; }; + A79190AA240CFCE4002011CA /* SSignal+Multicast.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919074240CFCE2002011CA /* SSignal+Multicast.h */; }; + A79190AD240CFCE4002011CA /* SSignal+Single.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919077240CFCE2002011CA /* SSignal+Single.h */; }; + A79190AE240CFCE4002011CA /* SDisposable.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919078240CFCE2002011CA /* SDisposable.h */; }; + A79190AF240CFCE4002011CA /* SSignal+Mapping.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919079240CFCE3002011CA /* SSignal+Mapping.h */; }; + A79190B0240CFCE4002011CA /* SBag.h in Headers */ = {isa = PBXBuildFile; fileRef = A791907A240CFCE3002011CA /* SBag.h */; }; + A79190B3240CFCE4002011CA /* SQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = A791907D240CFCE3002011CA /* SQueue.h */; }; + A79190B4240CFCE4002011CA /* SSignal+Timing.h in Headers */ = {isa = PBXBuildFile; fileRef = A791907E240CFCE3002011CA /* SSignal+Timing.h */; }; + A79190B6240CFCE4002011CA /* SSignal+Catch.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919080240CFCE3002011CA /* SSignal+Catch.h */; }; + A79190B7240CFCE4002011CA /* SSubscriber.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919081240CFCE3002011CA /* SSubscriber.h */; }; + A79190BA240CFCE4002011CA /* SMulticastSignalManager.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919084240CFCE3002011CA /* SMulticastSignalManager.h */; }; + A79190BC240CFCE4002011CA /* SThreadPool.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919086240CFCE4002011CA /* SThreadPool.h */; }; + A79190BD240CFCE4002011CA /* SSignal+Take.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919087240CFCE4002011CA /* SSignal+Take.h */; }; + A79190BF240CFD14002011CA /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7918E18240CF16A002011CA /* Atomic.swift */; }; + A79190C0240CFD14002011CA /* Bag.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7918E22240CF16B002011CA /* Bag.swift */; }; + A79190C1240CFD14002011CA /* Disposable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7918E23240CF16B002011CA /* Disposable.swift */; }; + A79190C2240CFD14002011CA /* Lock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7918E20240CF16A002011CA /* Lock.swift */; }; + A79190C3240CFD14002011CA /* Multicast.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7918E2E240CF16B002011CA /* Multicast.swift */; }; + A79190C4240CFD14002011CA /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7918E1C240CF16A002011CA /* Promise.swift */; }; + A79190C5240CFD14002011CA /* Queue.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7918E2B240CF16B002011CA /* Queue.swift */; }; + A79190C6240CFD14002011CA /* QueueLocalObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7918E1B240CF16A002011CA /* QueueLocalObject.swift */; }; + A79190C7240CFD14002011CA /* Signal_Catch.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7918E1A240CF16A002011CA /* Signal_Catch.swift */; }; + A79190C8240CFD14002011CA /* Signal_Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7918E24240CF16B002011CA /* Signal_Combine.swift */; }; + A79190C9240CFD14002011CA /* Signal_Dispatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7918E15240CF16A002011CA /* Signal_Dispatch.swift */; }; + A79190CA240CFD14002011CA /* Signal_Loop.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7918E17240CF16A002011CA /* Signal_Loop.swift */; }; + A79190CB240CFD14002011CA /* Signal_Mapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7918E16240CF16A002011CA /* Signal_Mapping.swift */; }; + A79190CC240CFD14002011CA /* Signal_Materialize.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7918E2A240CF16B002011CA /* Signal_Materialize.swift */; }; + A79190CD240CFD14002011CA /* Signal_Merge.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7918E28240CF16B002011CA /* Signal_Merge.swift */; }; + A79190CE240CFD14002011CA /* Signal_Meta.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7918E2D240CF16B002011CA /* Signal_Meta.swift */; }; + A79190CF240CFD14002011CA /* Signal_Reduce.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7918E27240CF16B002011CA /* Signal_Reduce.swift */; }; + A79190D0240CFD14002011CA /* Signal_SideEffects.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7918E19240CF16A002011CA /* Signal_SideEffects.swift */; }; + A79190D1240CFD14002011CA /* Signal_Single.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7918E2C240CF16B002011CA /* Signal_Single.swift */; }; + A79190D2240CFD14002011CA /* Signal_Take.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7918E1E240CF16A002011CA /* Signal_Take.swift */; }; + A79190D3240CFD14002011CA /* Signal_Timing.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7918E25240CF16B002011CA /* Signal_Timing.swift */; }; + A79190D4240CFD14002011CA /* Signal.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7918E26240CF16B002011CA /* Signal.swift */; }; + A79190D5240CFD14002011CA /* Subscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7918E1D240CF16A002011CA /* Subscriber.swift */; }; + A79190D6240CFD14002011CA /* ThreadPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7918E21240CF16B002011CA /* ThreadPool.swift */; }; + A79190D7240CFD14002011CA /* Timer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7918E29240CF16B002011CA /* Timer.swift */; }; + A79190D8240CFD14002011CA /* ValuePipe.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7918E1F240CF16A002011CA /* ValuePipe.swift */; }; + A79190DA240CFD22002011CA /* SAtomic.m in Sources */ = {isa = PBXBuildFile; fileRef = A7919082240CFCE3002011CA /* SAtomic.m */; }; + A79190DC240CFD22002011CA /* SBag.m in Sources */ = {isa = PBXBuildFile; fileRef = A7919061240CFCE1002011CA /* SBag.m */; }; + A79190DE240CFD22002011CA /* SBlockDisposable.m in Sources */ = {isa = PBXBuildFile; fileRef = A7919065240CFCE2002011CA /* SBlockDisposable.m */; }; + A79190E0240CFD22002011CA /* SDisposableSet.h in Sources */ = {isa = PBXBuildFile; fileRef = A7919057240CFCE1002011CA /* SDisposableSet.h */; }; + A79190E1240CFD22002011CA /* SDisposableSet.m in Sources */ = {isa = PBXBuildFile; fileRef = A7919053240CFCE1002011CA /* SDisposableSet.m */; }; + A79190E3240CFD22002011CA /* SMetaDisposable.m in Sources */ = {isa = PBXBuildFile; fileRef = A791907F240CFCE3002011CA /* SMetaDisposable.m */; }; + A79190E5240CFD22002011CA /* SMulticastSignalManager.m in Sources */ = {isa = PBXBuildFile; fileRef = A7919088240CFCE4002011CA /* SMulticastSignalManager.m */; }; + A79190E7240CFD22002011CA /* SQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = A7919070240CFCE2002011CA /* SQueue.m */; }; + A79190E9240CFD22002011CA /* SSignal.m in Sources */ = {isa = PBXBuildFile; fileRef = A791906C240CFCE2002011CA /* SSignal.m */; }; + A79190EB240CFD22002011CA /* SSignal+Accumulate.m in Sources */ = {isa = PBXBuildFile; fileRef = A7919060240CFCE1002011CA /* SSignal+Accumulate.m */; }; + A79190ED240CFD22002011CA /* SSignal+Catch.m in Sources */ = {isa = PBXBuildFile; fileRef = A7919054240CFCE1002011CA /* SSignal+Catch.m */; }; + A79190EF240CFD22002011CA /* SSignal+Combine.m in Sources */ = {isa = PBXBuildFile; fileRef = A791906B240CFCE2002011CA /* SSignal+Combine.m */; }; + A79190F1240CFD22002011CA /* SSignal+Dispatch.m in Sources */ = {isa = PBXBuildFile; fileRef = A7919076240CFCE2002011CA /* SSignal+Dispatch.m */; }; + A79190F3240CFD22002011CA /* SSignal+Mapping.m in Sources */ = {isa = PBXBuildFile; fileRef = A791907C240CFCE3002011CA /* SSignal+Mapping.m */; }; + A79190F5240CFD22002011CA /* SSignal+Meta.m in Sources */ = {isa = PBXBuildFile; fileRef = A791906F240CFCE2002011CA /* SSignal+Meta.m */; }; + A79190F7240CFD22002011CA /* SSignal+Multicast.m in Sources */ = {isa = PBXBuildFile; fileRef = A791906A240CFCE2002011CA /* SSignal+Multicast.m */; }; + A79190F9240CFD22002011CA /* SSignal+Pipe.m in Sources */ = {isa = PBXBuildFile; fileRef = A7919073240CFCE2002011CA /* SSignal+Pipe.m */; }; + A79190FB240CFD22002011CA /* SSignal+SideEffects.m in Sources */ = {isa = PBXBuildFile; fileRef = A7919069240CFCE2002011CA /* SSignal+SideEffects.m */; }; + A79190FD240CFD22002011CA /* SSignal+Single.m in Sources */ = {isa = PBXBuildFile; fileRef = A7919066240CFCE2002011CA /* SSignal+Single.m */; }; + A79190FF240CFD22002011CA /* SSignal+Take.m in Sources */ = {isa = PBXBuildFile; fileRef = A791905E240CFCE1002011CA /* SSignal+Take.m */; }; + A7919101240CFD22002011CA /* SSignal+Timing.m in Sources */ = {isa = PBXBuildFile; fileRef = A791907B240CFCE3002011CA /* SSignal+Timing.m */; }; + A7919104240CFD22002011CA /* SSubscriber.m in Sources */ = {isa = PBXBuildFile; fileRef = A7919063240CFCE2002011CA /* SSubscriber.m */; }; + A7919106240CFD22002011CA /* SThreadPool.m in Sources */ = {isa = PBXBuildFile; fileRef = A7919085240CFCE3002011CA /* SThreadPool.m */; }; + A7919108240CFD22002011CA /* SThreadPoolQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = A7919083240CFCE3002011CA /* SThreadPoolQueue.m */; }; + A791910A240CFD22002011CA /* SThreadPoolTask.m in Sources */ = {isa = PBXBuildFile; fileRef = A7919075240CFCE2002011CA /* SThreadPoolTask.m */; }; + A791910C240CFD22002011CA /* STimer.m in Sources */ = {isa = PBXBuildFile; fileRef = A791906D240CFCE2002011CA /* STimer.m */; }; + A791910E240CFD22002011CA /* SVariable.m in Sources */ = {isa = PBXBuildFile; fileRef = A791905A240CFCE1002011CA /* SVariable.m */; }; + A791910F240CFD60002011CA /* SAtomic.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919072240CFCE2002011CA /* SAtomic.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7919110240CFD60002011CA /* SBag.h in Headers */ = {isa = PBXBuildFile; fileRef = A791907A240CFCE3002011CA /* SBag.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7919111240CFD60002011CA /* SBlockDisposable.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919071240CFCE2002011CA /* SBlockDisposable.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7919112240CFD60002011CA /* SDisposable.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919078240CFCE2002011CA /* SDisposable.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7919113240CFD60002011CA /* SDisposableSet.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919057240CFCE1002011CA /* SDisposableSet.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7919114240CFD60002011CA /* SMetaDisposable.h in Headers */ = {isa = PBXBuildFile; fileRef = A791906E240CFCE2002011CA /* SMetaDisposable.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7919115240CFD60002011CA /* SMulticastSignalManager.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919084240CFCE3002011CA /* SMulticastSignalManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7919116240CFD60002011CA /* SQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = A791907D240CFCE3002011CA /* SQueue.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7919117240CFD60002011CA /* SSignal.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919058240CFCE1002011CA /* SSignal.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7919118240CFD60002011CA /* SSignal+Accumulate.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919059240CFCE1002011CA /* SSignal+Accumulate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7919119240CFD60002011CA /* SSignal+Catch.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919080240CFCE3002011CA /* SSignal+Catch.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A791911A240CFD60002011CA /* SSignal+Combine.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919068240CFCE2002011CA /* SSignal+Combine.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A791911B240CFD60002011CA /* SSignal+Dispatch.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919067240CFCE2002011CA /* SSignal+Dispatch.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A791911C240CFD60002011CA /* SSignal+Mapping.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919079240CFCE3002011CA /* SSignal+Mapping.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A791911D240CFD60002011CA /* SSignal+Meta.h in Headers */ = {isa = PBXBuildFile; fileRef = A791905C240CFCE1002011CA /* SSignal+Meta.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A791911E240CFD60002011CA /* SSignal+Multicast.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919074240CFCE2002011CA /* SSignal+Multicast.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A791911F240CFD60002011CA /* SSignal+Pipe.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919056240CFCE1002011CA /* SSignal+Pipe.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7919120240CFD60002011CA /* SSignal+SideEffects.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919062240CFCE1002011CA /* SSignal+SideEffects.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7919121240CFD60002011CA /* SSignal+Single.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919077240CFCE2002011CA /* SSignal+Single.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7919122240CFD60002011CA /* SSignal+Take.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919087240CFCE4002011CA /* SSignal+Take.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7919123240CFD60002011CA /* SSignal+Timing.h in Headers */ = {isa = PBXBuildFile; fileRef = A791907E240CFCE3002011CA /* SSignal+Timing.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7919124240CFD60002011CA /* SSignalKit.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919064240CFCE2002011CA /* SSignalKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7919125240CFD60002011CA /* SSubscriber.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919081240CFCE3002011CA /* SSubscriber.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7919126240CFD60002011CA /* SThreadPool.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919086240CFCE4002011CA /* SThreadPool.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7919127240CFD60002011CA /* SThreadPoolQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = A791905B240CFCE1002011CA /* SThreadPoolQueue.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7919128240CFD60002011CA /* SThreadPoolTask.h in Headers */ = {isa = PBXBuildFile; fileRef = A791905F240CFCE1002011CA /* SThreadPoolTask.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7919129240CFD60002011CA /* STimer.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919055240CFCE1002011CA /* STimer.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A791912A240CFD60002011CA /* SVariable.h in Headers */ = {isa = PBXBuildFile; fileRef = A791905D240CFCE1002011CA /* SVariable.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0B417F11D7DFA63004562A4 /* SwiftSignalKit.h in Headers */ = {isa = PBXBuildFile; fileRef = D0B417EF1D7DFA63004562A4 /* SwiftSignalKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 27D0067325AEFD9E00EE3EB1 /* SQueueLocalObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SQueueLocalObject.m; path = Source/SSignalKit/SQueueLocalObject.m; sourceTree = ""; }; + 27D0067425AEFD9E00EE3EB1 /* SQueueLocalObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SQueueLocalObject.h; path = Source/SSignalKit/SQueueLocalObject.h; sourceTree = ""; }; + A790A321236B17DF000451B5 /* SSignalKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SSignalKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A790A37C236B1C44000451B5 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; + A7918E15240CF16A002011CA /* Signal_Dispatch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Signal_Dispatch.swift; path = Source/Signal_Dispatch.swift; sourceTree = ""; }; + A7918E16240CF16A002011CA /* Signal_Mapping.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Signal_Mapping.swift; path = Source/Signal_Mapping.swift; sourceTree = ""; }; + A7918E17240CF16A002011CA /* Signal_Loop.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Signal_Loop.swift; path = Source/Signal_Loop.swift; sourceTree = ""; }; + A7918E18240CF16A002011CA /* Atomic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Atomic.swift; path = Source/Atomic.swift; sourceTree = ""; }; + A7918E19240CF16A002011CA /* Signal_SideEffects.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Signal_SideEffects.swift; path = Source/Signal_SideEffects.swift; sourceTree = ""; }; + A7918E1A240CF16A002011CA /* Signal_Catch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Signal_Catch.swift; path = Source/Signal_Catch.swift; sourceTree = ""; }; + A7918E1B240CF16A002011CA /* QueueLocalObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = QueueLocalObject.swift; path = Source/QueueLocalObject.swift; sourceTree = ""; }; + A7918E1C240CF16A002011CA /* Promise.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Promise.swift; path = Source/Promise.swift; sourceTree = ""; }; + A7918E1D240CF16A002011CA /* Subscriber.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Subscriber.swift; path = Source/Subscriber.swift; sourceTree = ""; }; + A7918E1E240CF16A002011CA /* Signal_Take.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Signal_Take.swift; path = Source/Signal_Take.swift; sourceTree = ""; }; + A7918E1F240CF16A002011CA /* ValuePipe.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ValuePipe.swift; path = Source/ValuePipe.swift; sourceTree = ""; }; + A7918E20240CF16A002011CA /* Lock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Lock.swift; path = Source/Lock.swift; sourceTree = ""; }; + A7918E21240CF16B002011CA /* ThreadPool.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ThreadPool.swift; path = Source/ThreadPool.swift; sourceTree = ""; }; + A7918E22240CF16B002011CA /* Bag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Bag.swift; path = Source/Bag.swift; sourceTree = ""; }; + A7918E23240CF16B002011CA /* Disposable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Disposable.swift; path = Source/Disposable.swift; sourceTree = ""; }; + A7918E24240CF16B002011CA /* Signal_Combine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Signal_Combine.swift; path = Source/Signal_Combine.swift; sourceTree = ""; }; + A7918E25240CF16B002011CA /* Signal_Timing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Signal_Timing.swift; path = Source/Signal_Timing.swift; sourceTree = ""; }; + A7918E26240CF16B002011CA /* Signal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Signal.swift; path = Source/Signal.swift; sourceTree = ""; }; + A7918E27240CF16B002011CA /* Signal_Reduce.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Signal_Reduce.swift; path = Source/Signal_Reduce.swift; sourceTree = ""; }; + A7918E28240CF16B002011CA /* Signal_Merge.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Signal_Merge.swift; path = Source/Signal_Merge.swift; sourceTree = ""; }; + A7918E29240CF16B002011CA /* Timer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Timer.swift; path = Source/Timer.swift; sourceTree = ""; }; + A7918E2A240CF16B002011CA /* Signal_Materialize.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Signal_Materialize.swift; path = Source/Signal_Materialize.swift; sourceTree = ""; }; + A7918E2B240CF16B002011CA /* Queue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Queue.swift; path = Source/Queue.swift; sourceTree = ""; }; + A7918E2C240CF16B002011CA /* Signal_Single.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Signal_Single.swift; path = Source/Signal_Single.swift; sourceTree = ""; }; + A7918E2D240CF16B002011CA /* Signal_Meta.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Signal_Meta.swift; path = Source/Signal_Meta.swift; sourceTree = ""; }; + A7918E2E240CF16B002011CA /* Multicast.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Multicast.swift; path = Source/Multicast.swift; sourceTree = ""; }; + A7919053240CFCE1002011CA /* SDisposableSet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SDisposableSet.m; path = Source/SSignalKit/SDisposableSet.m; sourceTree = ""; }; + A7919054240CFCE1002011CA /* SSignal+Catch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "SSignal+Catch.m"; path = "Source/SSignalKit/SSignal+Catch.m"; sourceTree = ""; }; + A7919055240CFCE1002011CA /* STimer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = STimer.h; path = Source/SSignalKit/STimer.h; sourceTree = ""; }; + A7919056240CFCE1002011CA /* SSignal+Pipe.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SSignal+Pipe.h"; path = "Source/SSignalKit/SSignal+Pipe.h"; sourceTree = ""; }; + A7919057240CFCE1002011CA /* SDisposableSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SDisposableSet.h; path = Source/SSignalKit/SDisposableSet.h; sourceTree = ""; }; + A7919058240CFCE1002011CA /* SSignal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SSignal.h; path = Source/SSignalKit/SSignal.h; sourceTree = ""; }; + A7919059240CFCE1002011CA /* SSignal+Accumulate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SSignal+Accumulate.h"; path = "Source/SSignalKit/SSignal+Accumulate.h"; sourceTree = ""; }; + A791905A240CFCE1002011CA /* SVariable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SVariable.m; path = Source/SSignalKit/SVariable.m; sourceTree = ""; }; + A791905B240CFCE1002011CA /* SThreadPoolQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SThreadPoolQueue.h; path = Source/SSignalKit/SThreadPoolQueue.h; sourceTree = ""; }; + A791905C240CFCE1002011CA /* SSignal+Meta.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SSignal+Meta.h"; path = "Source/SSignalKit/SSignal+Meta.h"; sourceTree = ""; }; + A791905D240CFCE1002011CA /* SVariable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SVariable.h; path = Source/SSignalKit/SVariable.h; sourceTree = ""; }; + A791905E240CFCE1002011CA /* SSignal+Take.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "SSignal+Take.m"; path = "Source/SSignalKit/SSignal+Take.m"; sourceTree = ""; }; + A791905F240CFCE1002011CA /* SThreadPoolTask.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SThreadPoolTask.h; path = Source/SSignalKit/SThreadPoolTask.h; sourceTree = ""; }; + A7919060240CFCE1002011CA /* SSignal+Accumulate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "SSignal+Accumulate.m"; path = "Source/SSignalKit/SSignal+Accumulate.m"; sourceTree = ""; }; + A7919061240CFCE1002011CA /* SBag.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SBag.m; path = Source/SSignalKit/SBag.m; sourceTree = ""; }; + A7919062240CFCE1002011CA /* SSignal+SideEffects.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SSignal+SideEffects.h"; path = "Source/SSignalKit/SSignal+SideEffects.h"; sourceTree = ""; }; + A7919063240CFCE2002011CA /* SSubscriber.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SSubscriber.m; path = Source/SSignalKit/SSubscriber.m; sourceTree = ""; }; + A7919064240CFCE2002011CA /* SSignalKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SSignalKit.h; path = Source/SSignalKit/SSignalKit.h; sourceTree = ""; }; + A7919065240CFCE2002011CA /* SBlockDisposable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SBlockDisposable.m; path = Source/SSignalKit/SBlockDisposable.m; sourceTree = ""; }; + A7919066240CFCE2002011CA /* SSignal+Single.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "SSignal+Single.m"; path = "Source/SSignalKit/SSignal+Single.m"; sourceTree = ""; }; + A7919067240CFCE2002011CA /* SSignal+Dispatch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SSignal+Dispatch.h"; path = "Source/SSignalKit/SSignal+Dispatch.h"; sourceTree = ""; }; + A7919068240CFCE2002011CA /* SSignal+Combine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SSignal+Combine.h"; path = "Source/SSignalKit/SSignal+Combine.h"; sourceTree = ""; }; + A7919069240CFCE2002011CA /* SSignal+SideEffects.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "SSignal+SideEffects.m"; path = "Source/SSignalKit/SSignal+SideEffects.m"; sourceTree = ""; }; + A791906A240CFCE2002011CA /* SSignal+Multicast.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "SSignal+Multicast.m"; path = "Source/SSignalKit/SSignal+Multicast.m"; sourceTree = ""; }; + A791906B240CFCE2002011CA /* SSignal+Combine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "SSignal+Combine.m"; path = "Source/SSignalKit/SSignal+Combine.m"; sourceTree = ""; }; + A791906C240CFCE2002011CA /* SSignal.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SSignal.m; path = Source/SSignalKit/SSignal.m; sourceTree = ""; }; + A791906D240CFCE2002011CA /* STimer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = STimer.m; path = Source/SSignalKit/STimer.m; sourceTree = ""; }; + A791906E240CFCE2002011CA /* SMetaDisposable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SMetaDisposable.h; path = Source/SSignalKit/SMetaDisposable.h; sourceTree = ""; }; + A791906F240CFCE2002011CA /* SSignal+Meta.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "SSignal+Meta.m"; path = "Source/SSignalKit/SSignal+Meta.m"; sourceTree = ""; }; + A7919070240CFCE2002011CA /* SQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SQueue.m; path = Source/SSignalKit/SQueue.m; sourceTree = ""; }; + A7919071240CFCE2002011CA /* SBlockDisposable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SBlockDisposable.h; path = Source/SSignalKit/SBlockDisposable.h; sourceTree = ""; }; + A7919072240CFCE2002011CA /* SAtomic.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SAtomic.h; path = Source/SSignalKit/SAtomic.h; sourceTree = ""; }; + A7919073240CFCE2002011CA /* SSignal+Pipe.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "SSignal+Pipe.m"; path = "Source/SSignalKit/SSignal+Pipe.m"; sourceTree = ""; }; + A7919074240CFCE2002011CA /* SSignal+Multicast.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SSignal+Multicast.h"; path = "Source/SSignalKit/SSignal+Multicast.h"; sourceTree = ""; }; + A7919075240CFCE2002011CA /* SThreadPoolTask.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SThreadPoolTask.m; path = Source/SSignalKit/SThreadPoolTask.m; sourceTree = ""; }; + A7919076240CFCE2002011CA /* SSignal+Dispatch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "SSignal+Dispatch.m"; path = "Source/SSignalKit/SSignal+Dispatch.m"; sourceTree = ""; }; + A7919077240CFCE2002011CA /* SSignal+Single.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SSignal+Single.h"; path = "Source/SSignalKit/SSignal+Single.h"; sourceTree = ""; }; + A7919078240CFCE2002011CA /* SDisposable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SDisposable.h; path = Source/SSignalKit/SDisposable.h; sourceTree = ""; }; + A7919079240CFCE3002011CA /* SSignal+Mapping.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SSignal+Mapping.h"; path = "Source/SSignalKit/SSignal+Mapping.h"; sourceTree = ""; }; + A791907A240CFCE3002011CA /* SBag.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SBag.h; path = Source/SSignalKit/SBag.h; sourceTree = ""; }; + A791907B240CFCE3002011CA /* SSignal+Timing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "SSignal+Timing.m"; path = "Source/SSignalKit/SSignal+Timing.m"; sourceTree = ""; }; + A791907C240CFCE3002011CA /* SSignal+Mapping.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "SSignal+Mapping.m"; path = "Source/SSignalKit/SSignal+Mapping.m"; sourceTree = ""; }; + A791907D240CFCE3002011CA /* SQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SQueue.h; path = Source/SSignalKit/SQueue.h; sourceTree = ""; }; + A791907E240CFCE3002011CA /* SSignal+Timing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SSignal+Timing.h"; path = "Source/SSignalKit/SSignal+Timing.h"; sourceTree = ""; }; + A791907F240CFCE3002011CA /* SMetaDisposable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SMetaDisposable.m; path = Source/SSignalKit/SMetaDisposable.m; sourceTree = ""; }; + A7919080240CFCE3002011CA /* SSignal+Catch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SSignal+Catch.h"; path = "Source/SSignalKit/SSignal+Catch.h"; sourceTree = ""; }; + A7919081240CFCE3002011CA /* SSubscriber.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SSubscriber.h; path = Source/SSignalKit/SSubscriber.h; sourceTree = ""; }; + A7919082240CFCE3002011CA /* SAtomic.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SAtomic.m; path = Source/SSignalKit/SAtomic.m; sourceTree = ""; }; + A7919083240CFCE3002011CA /* SThreadPoolQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SThreadPoolQueue.m; path = Source/SSignalKit/SThreadPoolQueue.m; sourceTree = ""; }; + A7919084240CFCE3002011CA /* SMulticastSignalManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SMulticastSignalManager.h; path = Source/SSignalKit/SMulticastSignalManager.h; sourceTree = ""; }; + A7919085240CFCE3002011CA /* SThreadPool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SThreadPool.m; path = Source/SSignalKit/SThreadPool.m; sourceTree = ""; }; + A7919086240CFCE4002011CA /* SThreadPool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SThreadPool.h; path = Source/SSignalKit/SThreadPool.h; sourceTree = ""; }; + A7919087240CFCE4002011CA /* SSignal+Take.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SSignal+Take.h"; path = "Source/SSignalKit/SSignal+Take.h"; sourceTree = ""; }; + A7919088240CFCE4002011CA /* SMulticastSignalManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SMulticastSignalManager.m; path = Source/SSignalKit/SMulticastSignalManager.m; sourceTree = ""; }; + D0085B251B282B9800EAF753 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D0445DDC1A7C2CA500267924 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D0B417ED1D7DFA63004562A4 /* SwiftSignalKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftSignalKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0B417EF1D7DFA63004562A4 /* SwiftSignalKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SwiftSignalKit.h; sourceTree = ""; }; + D0B417F01D7DFA63004562A4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A790A31E236B17DF000451B5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D0B417E91D7DFA63004562A4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A790A37B236B1C43000451B5 /* Frameworks */ = { + isa = PBXGroup; + children = ( + A790A37C236B1C44000451B5 /* Foundation.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + A7918E14240CF163002011CA /* Sources */ = { + isa = PBXGroup; + children = ( + A7918E18240CF16A002011CA /* Atomic.swift */, + A7918E22240CF16B002011CA /* Bag.swift */, + A7918E23240CF16B002011CA /* Disposable.swift */, + A7918E20240CF16A002011CA /* Lock.swift */, + A7918E2E240CF16B002011CA /* Multicast.swift */, + A7918E1C240CF16A002011CA /* Promise.swift */, + A7918E2B240CF16B002011CA /* Queue.swift */, + A7918E1B240CF16A002011CA /* QueueLocalObject.swift */, + A7918E1A240CF16A002011CA /* Signal_Catch.swift */, + A7918E24240CF16B002011CA /* Signal_Combine.swift */, + A7918E15240CF16A002011CA /* Signal_Dispatch.swift */, + A7918E17240CF16A002011CA /* Signal_Loop.swift */, + A7918E16240CF16A002011CA /* Signal_Mapping.swift */, + A7918E2A240CF16B002011CA /* Signal_Materialize.swift */, + A7918E28240CF16B002011CA /* Signal_Merge.swift */, + A7918E2D240CF16B002011CA /* Signal_Meta.swift */, + A7918E27240CF16B002011CA /* Signal_Reduce.swift */, + A7918E19240CF16A002011CA /* Signal_SideEffects.swift */, + A7918E2C240CF16B002011CA /* Signal_Single.swift */, + A7918E1E240CF16A002011CA /* Signal_Take.swift */, + A7918E25240CF16B002011CA /* Signal_Timing.swift */, + A7918E26240CF16B002011CA /* Signal.swift */, + A7918E1D240CF16A002011CA /* Subscriber.swift */, + A7918E21240CF16B002011CA /* ThreadPool.swift */, + A7918E29240CF16B002011CA /* Timer.swift */, + A7918E1F240CF16A002011CA /* ValuePipe.swift */, + ); + name = Sources; + sourceTree = ""; + }; + D0085B231B282B9800EAF753 /* SwiftSignalKit */ = { + isa = PBXGroup; + children = ( + A7918E14240CF163002011CA /* Sources */, + D0085B241B282B9800EAF753 /* Supporting Files */, + ); + name = SwiftSignalKit; + path = "../../submodules/telegram-ios/submodules/SSignalKit/SwiftSignalKit"; + sourceTree = ""; + }; + D0085B241B282B9800EAF753 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + D0085B251B282B9800EAF753 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + D0445DCE1A7C2CA500267924 = { + isa = PBXGroup; + children = ( + D0445DDA1A7C2CA500267924 /* SSignalKit */, + D0085B231B282B9800EAF753 /* SwiftSignalKit */, + D0B417EE1D7DFA63004562A4 /* SwiftSignalKit */, + D0445DD91A7C2CA500267924 /* Products */, + A790A37B236B1C43000451B5 /* Frameworks */, + ); + sourceTree = ""; + }; + D0445DD91A7C2CA500267924 /* Products */ = { + isa = PBXGroup; + children = ( + D0B417ED1D7DFA63004562A4 /* SwiftSignalKit.framework */, + A790A321236B17DF000451B5 /* SSignalKit.framework */, + ); + name = Products; + sourceTree = ""; + }; + D0445DDA1A7C2CA500267924 /* SSignalKit */ = { + isa = PBXGroup; + children = ( + A7919072240CFCE2002011CA /* SAtomic.h */, + A7919082240CFCE3002011CA /* SAtomic.m */, + A791907A240CFCE3002011CA /* SBag.h */, + A7919061240CFCE1002011CA /* SBag.m */, + A7919071240CFCE2002011CA /* SBlockDisposable.h */, + A7919065240CFCE2002011CA /* SBlockDisposable.m */, + A7919078240CFCE2002011CA /* SDisposable.h */, + A7919057240CFCE1002011CA /* SDisposableSet.h */, + A7919053240CFCE1002011CA /* SDisposableSet.m */, + A791906E240CFCE2002011CA /* SMetaDisposable.h */, + A791907F240CFCE3002011CA /* SMetaDisposable.m */, + A7919084240CFCE3002011CA /* SMulticastSignalManager.h */, + A7919088240CFCE4002011CA /* SMulticastSignalManager.m */, + A791907D240CFCE3002011CA /* SQueue.h */, + A7919070240CFCE2002011CA /* SQueue.m */, + A7919058240CFCE1002011CA /* SSignal.h */, + A791906C240CFCE2002011CA /* SSignal.m */, + A7919059240CFCE1002011CA /* SSignal+Accumulate.h */, + A7919060240CFCE1002011CA /* SSignal+Accumulate.m */, + A7919080240CFCE3002011CA /* SSignal+Catch.h */, + A7919054240CFCE1002011CA /* SSignal+Catch.m */, + A7919068240CFCE2002011CA /* SSignal+Combine.h */, + A791906B240CFCE2002011CA /* SSignal+Combine.m */, + A7919067240CFCE2002011CA /* SSignal+Dispatch.h */, + A7919076240CFCE2002011CA /* SSignal+Dispatch.m */, + A7919079240CFCE3002011CA /* SSignal+Mapping.h */, + A791907C240CFCE3002011CA /* SSignal+Mapping.m */, + A791905C240CFCE1002011CA /* SSignal+Meta.h */, + A791906F240CFCE2002011CA /* SSignal+Meta.m */, + A7919074240CFCE2002011CA /* SSignal+Multicast.h */, + A791906A240CFCE2002011CA /* SSignal+Multicast.m */, + A7919056240CFCE1002011CA /* SSignal+Pipe.h */, + A7919073240CFCE2002011CA /* SSignal+Pipe.m */, + A7919062240CFCE1002011CA /* SSignal+SideEffects.h */, + A7919069240CFCE2002011CA /* SSignal+SideEffects.m */, + A7919077240CFCE2002011CA /* SSignal+Single.h */, + A7919066240CFCE2002011CA /* SSignal+Single.m */, + A7919087240CFCE4002011CA /* SSignal+Take.h */, + A791905E240CFCE1002011CA /* SSignal+Take.m */, + A791907E240CFCE3002011CA /* SSignal+Timing.h */, + A791907B240CFCE3002011CA /* SSignal+Timing.m */, + A7919064240CFCE2002011CA /* SSignalKit.h */, + A7919081240CFCE3002011CA /* SSubscriber.h */, + A7919063240CFCE2002011CA /* SSubscriber.m */, + A7919086240CFCE4002011CA /* SThreadPool.h */, + A7919085240CFCE3002011CA /* SThreadPool.m */, + A791905B240CFCE1002011CA /* SThreadPoolQueue.h */, + A7919083240CFCE3002011CA /* SThreadPoolQueue.m */, + A791905F240CFCE1002011CA /* SThreadPoolTask.h */, + A7919075240CFCE2002011CA /* SThreadPoolTask.m */, + A7919055240CFCE1002011CA /* STimer.h */, + A791906D240CFCE2002011CA /* STimer.m */, + A791905D240CFCE1002011CA /* SVariable.h */, + A791905A240CFCE1002011CA /* SVariable.m */, + 27D0067425AEFD9E00EE3EB1 /* SQueueLocalObject.h */, + 27D0067325AEFD9E00EE3EB1 /* SQueueLocalObject.m */, + D0445DDB1A7C2CA500267924 /* Supporting Files */, + ); + name = SSignalKit; + path = "../../submodules/telegram-ios/submodules/SSignalKit/SSignalKit"; + sourceTree = ""; + }; + D0445DDB1A7C2CA500267924 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + D0445DDC1A7C2CA500267924 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + D0B417EE1D7DFA63004562A4 /* SwiftSignalKit */ = { + isa = PBXGroup; + children = ( + D0B417EF1D7DFA63004562A4 /* SwiftSignalKit.h */, + D0B417F01D7DFA63004562A4 /* Info.plist */, + ); + path = SwiftSignalKit; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + A790A31C236B17DF000451B5 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + A791910F240CFD60002011CA /* SAtomic.h in Headers */, + A7919110240CFD60002011CA /* SBag.h in Headers */, + A7919111240CFD60002011CA /* SBlockDisposable.h in Headers */, + A7919112240CFD60002011CA /* SDisposable.h in Headers */, + A7919113240CFD60002011CA /* SDisposableSet.h in Headers */, + A7919114240CFD60002011CA /* SMetaDisposable.h in Headers */, + A7919115240CFD60002011CA /* SMulticastSignalManager.h in Headers */, + A7919116240CFD60002011CA /* SQueue.h in Headers */, + A7919117240CFD60002011CA /* SSignal.h in Headers */, + A7919118240CFD60002011CA /* SSignal+Accumulate.h in Headers */, + A7919119240CFD60002011CA /* SSignal+Catch.h in Headers */, + A791911A240CFD60002011CA /* SSignal+Combine.h in Headers */, + A791911B240CFD60002011CA /* SSignal+Dispatch.h in Headers */, + A791911C240CFD60002011CA /* SSignal+Mapping.h in Headers */, + A791911D240CFD60002011CA /* SSignal+Meta.h in Headers */, + A791911E240CFD60002011CA /* SSignal+Multicast.h in Headers */, + A791911F240CFD60002011CA /* SSignal+Pipe.h in Headers */, + A7919120240CFD60002011CA /* SSignal+SideEffects.h in Headers */, + A7919121240CFD60002011CA /* SSignal+Single.h in Headers */, + A7919122240CFD60002011CA /* SSignal+Take.h in Headers */, + A7919123240CFD60002011CA /* SSignal+Timing.h in Headers */, + A7919124240CFD60002011CA /* SSignalKit.h in Headers */, + A7919125240CFD60002011CA /* SSubscriber.h in Headers */, + A7919126240CFD60002011CA /* SThreadPool.h in Headers */, + A7919127240CFD60002011CA /* SThreadPoolQueue.h in Headers */, + A7919128240CFD60002011CA /* SThreadPoolTask.h in Headers */, + 27D0067625AEFD9E00EE3EB1 /* SQueueLocalObject.h in Headers */, + A7919129240CFD60002011CA /* STimer.h in Headers */, + A791912A240CFD60002011CA /* SVariable.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D0B417EA1D7DFA63004562A4 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + A7919093240CFCE4002011CA /* SVariable.h in Headers */, + A79190AF240CFCE4002011CA /* SSignal+Mapping.h in Headers */, + A79190B6240CFCE4002011CA /* SSignal+Catch.h in Headers */, + A79190B4240CFCE4002011CA /* SSignal+Timing.h in Headers */, + A79190B7240CFCE4002011CA /* SSubscriber.h in Headers */, + A791908B240CFCE4002011CA /* STimer.h in Headers */, + A7919098240CFCE4002011CA /* SSignal+SideEffects.h in Headers */, + A7919091240CFCE4002011CA /* SThreadPoolQueue.h in Headers */, + A7919095240CFCE4002011CA /* SThreadPoolTask.h in Headers */, + A79190BA240CFCE4002011CA /* SMulticastSignalManager.h in Headers */, + A79190B3240CFCE4002011CA /* SQueue.h in Headers */, + A791908C240CFCE4002011CA /* SSignal+Pipe.h in Headers */, + A791909E240CFCE4002011CA /* SSignal+Combine.h in Headers */, + A791908E240CFCE4002011CA /* SSignal.h in Headers */, + A79190B0240CFCE4002011CA /* SBag.h in Headers */, + A79190BD240CFCE4002011CA /* SSignal+Take.h in Headers */, + A79190AD240CFCE4002011CA /* SSignal+Single.h in Headers */, + D0B417F11D7DFA63004562A4 /* SwiftSignalKit.h in Headers */, + A79190A8240CFCE4002011CA /* SAtomic.h in Headers */, + A79190A4240CFCE4002011CA /* SMetaDisposable.h in Headers */, + A791908F240CFCE4002011CA /* SSignal+Accumulate.h in Headers */, + A791909D240CFCE4002011CA /* SSignal+Dispatch.h in Headers */, + A79190AA240CFCE4002011CA /* SSignal+Multicast.h in Headers */, + A79190A7240CFCE4002011CA /* SBlockDisposable.h in Headers */, + A79190AE240CFCE4002011CA /* SDisposable.h in Headers */, + A79190BC240CFCE4002011CA /* SThreadPool.h in Headers */, + A791908D240CFCE4002011CA /* SDisposableSet.h in Headers */, + A7919092240CFCE4002011CA /* SSignal+Meta.h in Headers */, + A791909A240CFCE4002011CA /* SSignalKit.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + A790A320236B17DF000451B5 /* SSignalKit */ = { + isa = PBXNativeTarget; + buildConfigurationList = A790A332236B17DF000451B5 /* Build configuration list for PBXNativeTarget "SSignalKit" */; + buildPhases = ( + A790A31C236B17DF000451B5 /* Headers */, + A790A31D236B17DF000451B5 /* Sources */, + A790A31E236B17DF000451B5 /* Frameworks */, + A790A31F236B17DF000451B5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SSignalKit; + productName = SSignalKit; + productReference = A790A321236B17DF000451B5 /* SSignalKit.framework */; + productType = "com.apple.product-type.framework"; + }; + D0B417EC1D7DFA63004562A4 /* SwiftSignalKit */ = { + isa = PBXNativeTarget; + buildConfigurationList = D0B417F21D7DFA63004562A4 /* Build configuration list for PBXNativeTarget "SwiftSignalKit" */; + buildPhases = ( + D0B417E81D7DFA63004562A4 /* Sources */, + D0B417E91D7DFA63004562A4 /* Frameworks */, + D0B417EA1D7DFA63004562A4 /* Headers */, + D0B417EB1D7DFA63004562A4 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SwiftSignalKit; + productName = SwiftSignalKitMac; + productReference = D0B417ED1D7DFA63004562A4 /* SwiftSignalKit.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D0445DCF1A7C2CA500267924 /* Project object */ = { + isa = PBXProject; + attributes = { + DefaultBuildSystemTypeForWorkspace = Latest; + LastSwiftMigration = 0700; + LastSwiftUpdateCheck = 0700; + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = Telegram; + TargetAttributes = { + A790A320236B17DF000451B5 = { + CreatedOnToolsVersion = 10.3; + DevelopmentTeam = 6N38VWS5BX; + ProvisioningStyle = Automatic; + }; + D0B417EC1D7DFA63004562A4 = { + CreatedOnToolsVersion = 8.0; + LastSwiftMigration = 1030; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = D0445DD21A7C2CA500267924 /* Build configuration list for PBXProject "SSignalKit_Xcode" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + English, + en, + ); + mainGroup = D0445DCE1A7C2CA500267924; + productRefGroup = D0445DD91A7C2CA500267924 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D0B417EC1D7DFA63004562A4 /* SwiftSignalKit */, + A790A320236B17DF000451B5 /* SSignalKit */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A790A31F236B17DF000451B5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D0B417EB1D7DFA63004562A4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A790A31D236B17DF000451B5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A79190DA240CFD22002011CA /* SAtomic.m in Sources */, + A79190DC240CFD22002011CA /* SBag.m in Sources */, + A79190DE240CFD22002011CA /* SBlockDisposable.m in Sources */, + A79190E0240CFD22002011CA /* SDisposableSet.h in Sources */, + A79190E1240CFD22002011CA /* SDisposableSet.m in Sources */, + A79190E3240CFD22002011CA /* SMetaDisposable.m in Sources */, + A79190E5240CFD22002011CA /* SMulticastSignalManager.m in Sources */, + A79190E7240CFD22002011CA /* SQueue.m in Sources */, + A79190E9240CFD22002011CA /* SSignal.m in Sources */, + A79190EB240CFD22002011CA /* SSignal+Accumulate.m in Sources */, + A79190ED240CFD22002011CA /* SSignal+Catch.m in Sources */, + A79190EF240CFD22002011CA /* SSignal+Combine.m in Sources */, + A79190F1240CFD22002011CA /* SSignal+Dispatch.m in Sources */, + A79190F3240CFD22002011CA /* SSignal+Mapping.m in Sources */, + A79190F5240CFD22002011CA /* SSignal+Meta.m in Sources */, + A79190F7240CFD22002011CA /* SSignal+Multicast.m in Sources */, + A79190F9240CFD22002011CA /* SSignal+Pipe.m in Sources */, + A79190FB240CFD22002011CA /* SSignal+SideEffects.m in Sources */, + A79190FD240CFD22002011CA /* SSignal+Single.m in Sources */, + A79190FF240CFD22002011CA /* SSignal+Take.m in Sources */, + A7919101240CFD22002011CA /* SSignal+Timing.m in Sources */, + A7919104240CFD22002011CA /* SSubscriber.m in Sources */, + A7919106240CFD22002011CA /* SThreadPool.m in Sources */, + A7919108240CFD22002011CA /* SThreadPoolQueue.m in Sources */, + 27D0067525AEFD9E00EE3EB1 /* SQueueLocalObject.m in Sources */, + A791910A240CFD22002011CA /* SThreadPoolTask.m in Sources */, + A791910C240CFD22002011CA /* STimer.m in Sources */, + A791910E240CFD22002011CA /* SVariable.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D0B417E81D7DFA63004562A4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A79190BF240CFD14002011CA /* Atomic.swift in Sources */, + A79190C0240CFD14002011CA /* Bag.swift in Sources */, + A79190C1240CFD14002011CA /* Disposable.swift in Sources */, + A79190C2240CFD14002011CA /* Lock.swift in Sources */, + A79190C3240CFD14002011CA /* Multicast.swift in Sources */, + A79190C4240CFD14002011CA /* Promise.swift in Sources */, + A79190C5240CFD14002011CA /* Queue.swift in Sources */, + A79190C6240CFD14002011CA /* QueueLocalObject.swift in Sources */, + A79190C7240CFD14002011CA /* Signal_Catch.swift in Sources */, + A79190C8240CFD14002011CA /* Signal_Combine.swift in Sources */, + A79190C9240CFD14002011CA /* Signal_Dispatch.swift in Sources */, + A79190CA240CFD14002011CA /* Signal_Loop.swift in Sources */, + A79190CB240CFD14002011CA /* Signal_Mapping.swift in Sources */, + A79190CC240CFD14002011CA /* Signal_Materialize.swift in Sources */, + A79190CD240CFD14002011CA /* Signal_Merge.swift in Sources */, + A79190CE240CFD14002011CA /* Signal_Meta.swift in Sources */, + A79190CF240CFD14002011CA /* Signal_Reduce.swift in Sources */, + A79190D0240CFD14002011CA /* Signal_SideEffects.swift in Sources */, + A79190D1240CFD14002011CA /* Signal_Single.swift in Sources */, + A79190D2240CFD14002011CA /* Signal_Take.swift in Sources */, + A79190D3240CFD14002011CA /* Signal_Timing.swift in Sources */, + A79190D4240CFD14002011CA /* Signal.swift in Sources */, + A79190D5240CFD14002011CA /* Subscriber.swift in Sources */, + A79190D6240CFD14002011CA /* ThreadPool.swift in Sources */, + A79190D7240CFD14002011CA /* Timer.swift in Sources */, + A79190D8240CFD14002011CA /* ValuePipe.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A790A333236B17DF000451B5 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + FRAMEWORK_VERSION = A; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = s; + 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 = SwiftSignalKit/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = NO; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.SSignalKit; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + A790A334236B17DF000451B5 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + FRAMEWORK_VERSION = A; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = s; + 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 = SwiftSignalKit/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = NO; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.SSignalKit; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + A790A336236B17DF000451B5 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + FRAMEWORK_VERSION = A; + 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 = SwiftSignalKit/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = NO; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.SSignalKit; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + A790A338236B17DF000451B5 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + FRAMEWORK_VERSION = A; + 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 = SwiftSignalKit/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = NO; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.SSignalKit; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + A790A33A236B17DF000451B5 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + FRAMEWORK_VERSION = A; + 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 = SwiftSignalKit/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = NO; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.SSignalKit; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + A7F282DC238EAB4B00742C20 /* Github */ = { + isa = XCBuildConfiguration; + 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_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + ENABLE_BITCODE = NO; + 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.1; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + A7F282DD238EAB4B00742C20 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ARCHS = "$(ARCHS_STANDARD)"; + 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_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = SwiftSignalKit/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Telegram.SwiftSignalKit; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Github; + }; + A7F282DE238EAB4B00742C20 /* Github */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + FRAMEWORK_VERSION = A; + 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 = SwiftSignalKit/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = NO; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.SSignalKit; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + D0364D5522B3E38E002A6EF0 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + ENABLE_BITCODE = NO; + 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.1; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + D0364D5A22B3E38E002A6EF0 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ARCHS = "$(ARCHS_STANDARD)"; + 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_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_OPTIMIZATION_LEVEL = s; + INFOPLIST_FILE = SwiftSignalKit/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Telegram.SwiftSignalKit; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 5.0; + }; + name = HockeyappMacAlpha; + }; + D0445DEC1A7C2CA500267924 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + ENABLE_BITCODE = NO; + 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.1; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + D0445DED1A7C2CA500267924 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + CURRENT_PROJECT_VERSION = 1; + ENABLE_BITCODE = NO; + 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.1; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + D086A5741CC0117500F08284 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + CURRENT_PROJECT_VERSION = 1; + ENABLE_BITCODE = NO; + 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.1; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + D0B417F31D7DFA63004562A4 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ARCHS = "$(ARCHS_STANDARD)"; + 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_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_OPTIMIZATION_LEVEL = s; + INFOPLIST_FILE = SwiftSignalKit/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Telegram.SwiftSignalKit; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 5.0; + }; + name = DebugHockeyapp; + }; + D0B417F41D7DFA63004562A4 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ARCHS = "$(ARCHS_STANDARD)"; + 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_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = SwiftSignalKit/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Telegram.SwiftSignalKit; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 5.0; + }; + name = ReleaseHockeyapp; + }; + D0B417F51D7DFA63004562A4 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ARCHS = "$(ARCHS_STANDARD)"; + 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_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = SwiftSignalKit/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Telegram.SwiftSignalKit; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 5.0; + }; + name = ReleaseAppStore; + }; + D0DB57B61E5C4B7A0071854C /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + ENABLE_BITCODE = NO; + 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.1; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + D0DB57BB1E5C4B7A0071854C /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ARCHS = "$(ARCHS_STANDARD)"; + 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_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = SwiftSignalKit/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Telegram.SwiftSignalKit; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = DebugAppStore; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A790A332236B17DF000451B5 /* Build configuration list for PBXNativeTarget "SSignalKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A790A333236B17DF000451B5 /* DebugHockeyapp */, + A790A334236B17DF000451B5 /* HockeyappMacAlpha */, + A790A336236B17DF000451B5 /* DebugAppStore */, + A7F282DE238EAB4B00742C20 /* Github */, + A790A338236B17DF000451B5 /* ReleaseHockeyapp */, + A790A33A236B17DF000451B5 /* ReleaseAppStore */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseHockeyapp; + }; + D0445DD21A7C2CA500267924 /* Build configuration list for PBXProject "SSignalKit_Xcode" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0445DEC1A7C2CA500267924 /* DebugHockeyapp */, + D0364D5522B3E38E002A6EF0 /* HockeyappMacAlpha */, + D0DB57B61E5C4B7A0071854C /* DebugAppStore */, + A7F282DC238EAB4B00742C20 /* Github */, + D0445DED1A7C2CA500267924 /* ReleaseHockeyapp */, + D086A5741CC0117500F08284 /* ReleaseAppStore */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseHockeyapp; + }; + D0B417F21D7DFA63004562A4 /* Build configuration list for PBXNativeTarget "SwiftSignalKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0B417F31D7DFA63004562A4 /* DebugHockeyapp */, + D0364D5A22B3E38E002A6EF0 /* HockeyappMacAlpha */, + D0DB57BB1E5C4B7A0071854C /* DebugAppStore */, + A7F282DD238EAB4B00742C20 /* Github */, + D0B417F41D7DFA63004562A4 /* ReleaseHockeyapp */, + D0B417F51D7DFA63004562A4 /* ReleaseAppStore */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseHockeyapp; + }; +/* End XCConfigurationList section */ + }; + rootObject = D0445DCF1A7C2CA500267924 /* Project object */; +} diff --git a/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..2141ad337b --- /dev/null +++ b/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate b/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000..45f08369bd Binary files /dev/null and b/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/SSignalKit.xcscheme b/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/SSignalKit.xcscheme new file mode 100644 index 0000000000..b09b5d14f1 --- /dev/null +++ b/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/SSignalKit.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/SwiftSignalKit.xcscheme b/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/SwiftSignalKit.xcscheme new file mode 100644 index 0000000000..7684a4160e --- /dev/null +++ b/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/SwiftSignalKit.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/SwiftSignalKitMac.xcscheme b/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/SwiftSignalKitMac.xcscheme new file mode 100644 index 0000000000..38e04aebfa --- /dev/null +++ b/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/SwiftSignalKitMac.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/xcuserdata/mikhailfilimonov.xcuserdatad/xcschemes/xcschememanagement.plist b/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/xcuserdata/mikhailfilimonov.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100755 index 0000000000..1995169846 --- /dev/null +++ b/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/xcuserdata/mikhailfilimonov.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,24 @@ + + + + + SchemeUserState + + SSignalKit.xcscheme_^#shared#^_ + + orderHint + 38 + + SwiftSignalKit.xcscheme_^#shared#^_ + + orderHint + 39 + + SwiftSignalKitMac.xcscheme_^#shared#^_ + + orderHint + 40 + + + + diff --git a/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/SSignalKit.xcscheme b/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/SSignalKit.xcscheme new file mode 100644 index 0000000000..1cea9528d0 --- /dev/null +++ b/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/SSignalKit.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/SwiftSignalKit.xcscheme b/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/SwiftSignalKit.xcscheme new file mode 100644 index 0000000000..38e04aebfa --- /dev/null +++ b/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/SwiftSignalKit.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/xcuserdata/telegram.xcuserdatad/xcschemes/xcschememanagement.plist b/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/xcuserdata/telegram.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000000..9f43b65949 --- /dev/null +++ b/core-xprojects/SSignalKit/SSignalKit_Xcode.xcodeproj/xcuserdata/telegram.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,24 @@ + + + + + SchemeUserState + + SSignalKit.xcscheme_^#shared#^_ + + orderHint + 44 + + SwiftSignalKit.xcscheme_^#shared#^_ + + orderHint + 45 + + SwiftSignalKitMac.xcscheme_^#shared#^_ + + orderHint + 46 + + + + diff --git a/core-xprojects/SSignalKit/SwiftSignalKit/Info.plist b/core-xprojects/SSignalKit/SwiftSignalKit/Info.plist new file mode 100644 index 0000000000..ab41b55afe --- /dev/null +++ b/core-xprojects/SSignalKit/SwiftSignalKit/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2016 Telegram. All rights reserved. + NSPrincipalClass + + + diff --git a/core-xprojects/SSignalKit/SwiftSignalKit/SwiftSignalKit.h b/core-xprojects/SSignalKit/SwiftSignalKit/SwiftSignalKit.h new file mode 100644 index 0000000000..fe79ca3a9e --- /dev/null +++ b/core-xprojects/SSignalKit/SwiftSignalKit/SwiftSignalKit.h @@ -0,0 +1,19 @@ +// +// SwiftSignalKitMac.h +// SwiftSignalKitMac +// +// Created by Peter on 9/5/16. +// Copyright © 2016 Telegram. All rights reserved. +// + +#import + +//! Project version number for SwiftSignalKitMac. +FOUNDATION_EXPORT double SwiftSignalKitVersionNumber; + +//! Project version string for SwiftSignalKitMac. +FOUNDATION_EXPORT const unsigned char SwiftSignalKitVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/core-xprojects/StringTransliteration/StringTransliteration.xcodeproj/project.pbxproj b/core-xprojects/StringTransliteration/StringTransliteration.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..f499a72b04 --- /dev/null +++ b/core-xprojects/StringTransliteration/StringTransliteration.xcodeproj/project.pbxproj @@ -0,0 +1,711 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + A7918E58240CF21B002011CA /* StringTransliterationPublic.h in Headers */ = {isa = PBXBuildFile; fileRef = A7918E56240CF21B002011CA /* StringTransliterationPublic.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7918E5F240CF228002011CA /* StringTransliteration.h in Headers */ = {isa = PBXBuildFile; fileRef = A7918E5E240CF228002011CA /* StringTransliteration.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7918E61240CF230002011CA /* StringTransliteration.m in Sources */ = {isa = PBXBuildFile; fileRef = A7918E60240CF230002011CA /* StringTransliteration.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A7918E53240CF21B002011CA /* StringTransliteration.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StringTransliteration.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7918E56240CF21B002011CA /* StringTransliterationPublic.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = StringTransliterationPublic.h; sourceTree = ""; }; + A7918E57240CF21B002011CA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A7918E5E240CF228002011CA /* StringTransliteration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = StringTransliteration.h; path = "../../../submodules/telegram-ios/submodules/StringTransliteration/PublicHeaders/StringTransliteration/StringTransliteration.h"; sourceTree = ""; }; + A7918E60240CF230002011CA /* StringTransliteration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = StringTransliteration.m; path = "../../../submodules/telegram-ios/submodules/StringTransliteration/Sources/StringTransliteration.m"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A7918E50240CF21B002011CA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A7918E49240CF21B002011CA = { + isa = PBXGroup; + children = ( + A7918E55240CF21B002011CA /* StringTransliteration */, + A7918E54240CF21B002011CA /* Products */, + ); + sourceTree = ""; + }; + A7918E54240CF21B002011CA /* Products */ = { + isa = PBXGroup; + children = ( + A7918E53240CF21B002011CA /* StringTransliteration.framework */, + ); + name = Products; + sourceTree = ""; + }; + A7918E55240CF21B002011CA /* StringTransliteration */ = { + isa = PBXGroup; + children = ( + A7918E60240CF230002011CA /* StringTransliteration.m */, + A7918E5E240CF228002011CA /* StringTransliteration.h */, + A7918E56240CF21B002011CA /* StringTransliterationPublic.h */, + A7918E57240CF21B002011CA /* Info.plist */, + ); + path = StringTransliteration; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + A7918E4E240CF21B002011CA /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + A7918E58240CF21B002011CA /* StringTransliterationPublic.h in Headers */, + A7918E5F240CF228002011CA /* StringTransliteration.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + A7918E52240CF21B002011CA /* StringTransliteration */ = { + isa = PBXNativeTarget; + buildConfigurationList = A7918E5B240CF21B002011CA /* Build configuration list for PBXNativeTarget "StringTransliteration" */; + buildPhases = ( + A7918E4E240CF21B002011CA /* Headers */, + A7918E4F240CF21B002011CA /* Sources */, + A7918E50240CF21B002011CA /* Frameworks */, + A7918E51240CF21B002011CA /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = StringTransliteration; + productName = StringTransliteration; + productReference = A7918E53240CF21B002011CA /* StringTransliteration.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A7918E4A240CF21B002011CA /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1030; + ORGANIZATIONNAME = Telegram; + TargetAttributes = { + A7918E52240CF21B002011CA = { + CreatedOnToolsVersion = 10.3; + }; + }; + }; + buildConfigurationList = A7918E4D240CF21B002011CA /* Build configuration list for PBXProject "StringTransliteration" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = A7918E49240CF21B002011CA; + productRefGroup = A7918E54240CF21B002011CA /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A7918E52240CF21B002011CA /* StringTransliteration */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A7918E51240CF21B002011CA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A7918E4F240CF21B002011CA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A7918E61240CF230002011CA /* StringTransliteration.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A7918E59240CF21B002011CA /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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 = s; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + A7918E5A240CF21B002011CA /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + A7918E5C240CF21B002011CA /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = StringTransliteration/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.StringTransliteration; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = DebugHockeyapp; + }; + A7918E5D240CF21B002011CA /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = StringTransliteration/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.StringTransliteration; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = ReleaseHockeyapp; + }; + A7918E62240CF269002011CA /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + A7918E63240CF269002011CA /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = StringTransliteration/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.StringTransliteration; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = DebugAppStore; + }; + A7918E64240CF26D002011CA /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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 = s; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + A7918E65240CF26D002011CA /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = StringTransliteration/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.StringTransliteration; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = HockeyappMacAlpha; + }; + A7918E66240CF275002011CA /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + A7918E67240CF275002011CA /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = StringTransliteration/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.StringTransliteration; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = ReleaseAppStore; + }; + D0B4941024F3F5D000E0A9B3 /* Github */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + D0B4941124F3F5D000E0A9B3 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = StringTransliteration/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.StringTransliteration; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = Github; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A7918E4D240CF21B002011CA /* Build configuration list for PBXProject "StringTransliteration" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A7918E59240CF21B002011CA /* DebugHockeyapp */, + A7918E64240CF26D002011CA /* HockeyappMacAlpha */, + A7918E62240CF269002011CA /* DebugAppStore */, + D0B4941024F3F5D000E0A9B3 /* Github */, + A7918E5A240CF21B002011CA /* ReleaseHockeyapp */, + A7918E66240CF275002011CA /* ReleaseAppStore */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseHockeyapp; + }; + A7918E5B240CF21B002011CA /* Build configuration list for PBXNativeTarget "StringTransliteration" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A7918E5C240CF21B002011CA /* DebugHockeyapp */, + A7918E65240CF26D002011CA /* HockeyappMacAlpha */, + A7918E63240CF269002011CA /* DebugAppStore */, + D0B4941124F3F5D000E0A9B3 /* Github */, + A7918E5D240CF21B002011CA /* ReleaseHockeyapp */, + A7918E67240CF275002011CA /* ReleaseAppStore */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseHockeyapp; + }; +/* End XCConfigurationList section */ + }; + rootObject = A7918E4A240CF21B002011CA /* Project object */; +} diff --git a/core-xprojects/StringTransliteration/StringTransliteration.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/StringTransliteration/StringTransliteration.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..ab496dd063 --- /dev/null +++ b/core-xprojects/StringTransliteration/StringTransliteration.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/StringTransliteration/StringTransliteration.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/StringTransliteration/StringTransliteration.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/StringTransliteration/StringTransliteration.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/StringTransliteration/StringTransliteration/Info.plist b/core-xprojects/StringTransliteration/StringTransliteration/Info.plist new file mode 100644 index 0000000000..861c829fcb --- /dev/null +++ b/core-xprojects/StringTransliteration/StringTransliteration/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2020 Telegram. All rights reserved. + + diff --git a/core-xprojects/StringTransliteration/StringTransliteration/StringTransliterationPublic.h b/core-xprojects/StringTransliteration/StringTransliteration/StringTransliterationPublic.h new file mode 100644 index 0000000000..def56d9905 --- /dev/null +++ b/core-xprojects/StringTransliteration/StringTransliteration/StringTransliterationPublic.h @@ -0,0 +1,20 @@ +// +// StringTransliteration.h +// StringTransliteration +// +// Created by Mikhail Filimonov on 02.03.2020. +// Copyright © 2020 Telegram. All rights reserved. +// + +#import + +//! Project version number for StringTransliteration. +FOUNDATION_EXPORT double StringTransliterationVersionNumber; + +//! Project version string for StringTransliteration. +FOUNDATION_EXPORT const unsigned char StringTransliterationVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + +#import diff --git a/core-xprojects/Stripe/Images/stp_card_amex.png b/core-xprojects/Stripe/Images/stp_card_amex.png new file mode 100644 index 0000000000..c25fb10e65 Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_amex.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_amex@2x.png b/core-xprojects/Stripe/Images/stp_card_amex@2x.png new file mode 100644 index 0000000000..3e11213014 Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_amex@2x.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_amex_template.png b/core-xprojects/Stripe/Images/stp_card_amex_template.png new file mode 100644 index 0000000000..dd7d8529c8 Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_amex_template.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_amex_template@2x.png b/core-xprojects/Stripe/Images/stp_card_amex_template@2x.png new file mode 100644 index 0000000000..64eb765a5b Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_amex_template@2x.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_applepay.png b/core-xprojects/Stripe/Images/stp_card_applepay.png new file mode 100644 index 0000000000..9dea38b70e Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_applepay.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_applepay@2x.png b/core-xprojects/Stripe/Images/stp_card_applepay@2x.png new file mode 100644 index 0000000000..4ff86964c7 Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_applepay@2x.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_cvc.png b/core-xprojects/Stripe/Images/stp_card_cvc.png new file mode 100644 index 0000000000..a8cc72e863 Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_cvc.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_cvc@2x.png b/core-xprojects/Stripe/Images/stp_card_cvc@2x.png new file mode 100644 index 0000000000..e74678b466 Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_cvc@2x.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_cvc_amex.png b/core-xprojects/Stripe/Images/stp_card_cvc_amex.png new file mode 100644 index 0000000000..dc9636fad8 Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_cvc_amex.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_cvc_amex@2x.png b/core-xprojects/Stripe/Images/stp_card_cvc_amex@2x.png new file mode 100644 index 0000000000..f89464e97e Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_cvc_amex@2x.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_cvc_amex@3x.png b/core-xprojects/Stripe/Images/stp_card_cvc_amex@3x.png new file mode 100644 index 0000000000..00feb894e6 Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_cvc_amex@3x.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_diners.png b/core-xprojects/Stripe/Images/stp_card_diners.png new file mode 100644 index 0000000000..4741bb1b67 Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_diners.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_diners@2x.png b/core-xprojects/Stripe/Images/stp_card_diners@2x.png new file mode 100644 index 0000000000..d5594d180d Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_diners@2x.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_diners_template.png b/core-xprojects/Stripe/Images/stp_card_diners_template.png new file mode 100644 index 0000000000..4f91d33350 Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_diners_template.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_diners_template@2x.png b/core-xprojects/Stripe/Images/stp_card_diners_template@2x.png new file mode 100644 index 0000000000..9dd1c08d7f Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_diners_template@2x.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_discover.png b/core-xprojects/Stripe/Images/stp_card_discover.png new file mode 100644 index 0000000000..6d6a9eff84 Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_discover.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_discover@2x.png b/core-xprojects/Stripe/Images/stp_card_discover@2x.png new file mode 100644 index 0000000000..a2fb57b054 Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_discover@2x.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_discover_template.png b/core-xprojects/Stripe/Images/stp_card_discover_template.png new file mode 100644 index 0000000000..c69cad0ab3 Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_discover_template.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_discover_template@2x.png b/core-xprojects/Stripe/Images/stp_card_discover_template@2x.png new file mode 100644 index 0000000000..2861f32022 Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_discover_template@2x.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_error.png b/core-xprojects/Stripe/Images/stp_card_error.png new file mode 100644 index 0000000000..225b0a4bf6 Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_error.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_error@2x.png b/core-xprojects/Stripe/Images/stp_card_error@2x.png new file mode 100644 index 0000000000..a002c08eaa Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_error@2x.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_error_amex.png b/core-xprojects/Stripe/Images/stp_card_error_amex.png new file mode 100644 index 0000000000..27ca7503fd Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_error_amex.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_error_amex@2x.png b/core-xprojects/Stripe/Images/stp_card_error_amex@2x.png new file mode 100644 index 0000000000..509362d5e0 Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_error_amex@2x.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_form_back.png b/core-xprojects/Stripe/Images/stp_card_form_back.png new file mode 100644 index 0000000000..15e57f9405 Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_form_back.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_form_back@2x.png b/core-xprojects/Stripe/Images/stp_card_form_back@2x.png new file mode 100644 index 0000000000..cf9adcaaad Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_form_back@2x.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_form_front.png b/core-xprojects/Stripe/Images/stp_card_form_front.png new file mode 100644 index 0000000000..98ac727c00 Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_form_front.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_form_front@2x.png b/core-xprojects/Stripe/Images/stp_card_form_front@2x.png new file mode 100644 index 0000000000..69af8e63c1 Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_form_front@2x.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_jcb.png b/core-xprojects/Stripe/Images/stp_card_jcb.png new file mode 100644 index 0000000000..da91814bbb Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_jcb.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_jcb@2x.png b/core-xprojects/Stripe/Images/stp_card_jcb@2x.png new file mode 100644 index 0000000000..39ca106197 Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_jcb@2x.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_jcb_template.png b/core-xprojects/Stripe/Images/stp_card_jcb_template.png new file mode 100644 index 0000000000..af73541ecb Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_jcb_template.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_jcb_template@2x.png b/core-xprojects/Stripe/Images/stp_card_jcb_template@2x.png new file mode 100644 index 0000000000..5504c9733f Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_jcb_template@2x.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_mastercard.png b/core-xprojects/Stripe/Images/stp_card_mastercard.png new file mode 100644 index 0000000000..f7cefef59c Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_mastercard.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_mastercard@2x.png b/core-xprojects/Stripe/Images/stp_card_mastercard@2x.png new file mode 100644 index 0000000000..d754e986fd Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_mastercard@2x.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_mastercard_template.png b/core-xprojects/Stripe/Images/stp_card_mastercard_template.png new file mode 100644 index 0000000000..003d7c6281 Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_mastercard_template.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_mastercard_template@2x.png b/core-xprojects/Stripe/Images/stp_card_mastercard_template@2x.png new file mode 100644 index 0000000000..ac4b140af4 Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_mastercard_template@2x.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_unknown.png b/core-xprojects/Stripe/Images/stp_card_unknown.png new file mode 100644 index 0000000000..c2100ceb6c Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_unknown.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_unknown@2x.png b/core-xprojects/Stripe/Images/stp_card_unknown@2x.png new file mode 100644 index 0000000000..563eb18ef0 Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_unknown@2x.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_visa.png b/core-xprojects/Stripe/Images/stp_card_visa.png new file mode 100644 index 0000000000..39728b7756 Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_visa.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_visa@2x.png b/core-xprojects/Stripe/Images/stp_card_visa@2x.png new file mode 100644 index 0000000000..05997f02ef Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_visa@2x.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_visa_template.png b/core-xprojects/Stripe/Images/stp_card_visa_template.png new file mode 100644 index 0000000000..24f3081591 Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_visa_template.png differ diff --git a/core-xprojects/Stripe/Images/stp_card_visa_template@2x.png b/core-xprojects/Stripe/Images/stp_card_visa_template@2x.png new file mode 100644 index 0000000000..12ef2ce182 Binary files /dev/null and b/core-xprojects/Stripe/Images/stp_card_visa_template@2x.png differ diff --git a/core-xprojects/Stripe/PublicHeaders/Stripe/STPAPIClient.h b/core-xprojects/Stripe/PublicHeaders/Stripe/STPAPIClient.h new file mode 100755 index 0000000000..839d7db3f6 --- /dev/null +++ b/core-xprojects/Stripe/PublicHeaders/Stripe/STPAPIClient.h @@ -0,0 +1,206 @@ +// +// STPAPIClient.h +// StripeExample +// +// Created by Jack Flintermann on 12/18/14. +// Copyright (c) 2014 Stripe. All rights reserved. +// + +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +#define FAUXPAS_IGNORED_ON_LINE(...) +#define FAUXPAS_IGNORED_IN_FILE(...) +FAUXPAS_IGNORED_IN_FILE(APIAvailability) + +static NSString *const STPSDKVersion = @"9.1.0"; + +@class STPBankAccount, STPBankAccountParams, STPCard, STPCardParams, STPToken, STPPaymentConfiguration; + +/** + A top-level class that imports the rest of the Stripe SDK. This class used to contain several methods to create Stripe tokens, but those are now deprecated in + favor of STPAPIClient. + */ +@interface Stripe : NSObject FAUXPAS_IGNORED_ON_LINE(UnprefixedClass); + +/** + * Set your Stripe API key with this method. New instances of STPAPIClient will be initialized with this value. You should call this method as early as + * possible in your application's lifecycle, preferably in your AppDelegate. + * + * @param publishableKey Your publishable key, obtained from https://stripe.com/account/apikeys + * @warning Make sure not to ship your test API keys to the App Store! This will log a warning if you use your test key in a release build. + */ ++ (void)setDefaultPublishableKey:(NSString *)publishableKey; + +/// The current default publishable key. ++ (nullable NSString *)defaultPublishableKey; + +/** + * By default, Stripe collects some basic information about SDK usage. + * You can call this method to turn off analytics collection. + */ ++ (void)disableAnalytics; + +@end + +/// A client for making connections to the Stripe API. +@interface STPAPIClient : NSObject + ++ (NSString *)stringWithCardBrand:(STPCardBrand)brand; + +/** + * A shared singleton API client. Its API key will be initially equal to [Stripe defaultPublishableKey]. + */ ++ (instancetype)sharedClient; +- (instancetype)initWithConfiguration:(STPPaymentConfiguration *)configuration NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithPublishableKey:(NSString *)publishableKey; + +/** + * @see [Stripe setDefaultPublishableKey:] + */ +@property (nonatomic, copy, nullable) NSString *publishableKey; + +/** + * @see -initWithConfiguration + */ +@property (nonatomic, copy) STPPaymentConfiguration *configuration; + +- (void)createTokenWithCard:(STPCardParams *)card completion:(nullable STPTokenCompletionBlock)completion; + +@end + +#pragma mark Bank Accounts + +/** + * STPAPIClient extensions to create Stripe tokens from bank accounts. + */ +@interface STPAPIClient (BankAccounts) + +/** + * Converts an STPBankAccount object into a Stripe token using the Stripe API. + * + * @param bankAccount The user's bank account details. Cannot be nil. @see https://stripe.com/docs/api#create_bank_account_token + * @param completion The callback to run with the returned Stripe token (and any errors that may have occurred). + */ +- (void)createTokenWithBankAccount:(STPBankAccountParams *)bankAccount completion:(__nullable STPTokenCompletionBlock)completion; + +@end + +#pragma mark Credit Cards + +/** + * STPAPIClient extensions to create Stripe tokens from credit or debit cards. + */ +@interface STPAPIClient (CreditCards) + +/** + * Converts an STPCardParams object into a Stripe token using the Stripe API. + * + * @param card The user's card details. Cannot be nil. @see https://stripe.com/docs/api#create_card_token + * @param completion The callback to run with the returned Stripe token (and any errors that may have occurred). + */ + + +@end + +/** + * Convenience methods for working with Apple Pay. + */ +@interface Stripe(ApplePay) + +/** + * Whether or not this device is capable of using Apple Pay. This checks both whether the user is running an iPhone 6/6+ or later, iPad Air 2 or later, or iPad + *mini 3 or later, as well as whether or not they have stored any cards in Apple Pay on their device. + * + * @param paymentRequest The return value of this method depends on the `supportedNetworks` property of this payment request, which by default should be + *`@[PKPaymentNetworkAmex, PKPaymentNetworkMasterCard, PKPaymentNetworkVisa, PKPaymentNetworkDiscover]`. + * + * @return whether or not the user is currently able to pay with Apple Pay. + */ ++ (BOOL)canSubmitPaymentRequest:(PKPaymentRequest *)paymentRequest NS_AVAILABLE_IOS(8_0); + ++ (BOOL)deviceSupportsApplePay; + +/** + * A convenience method to return a `PKPaymentRequest` with sane default values. You will still need to configure the `paymentSummaryItems` property to indicate + *what the user is purchasing, as well as the optional `requiredShippingAddressFields`, `requiredBillingAddressFields`, and `shippingMethods` properties to indicate + *what contact information your application requires. + * + * @param merchantIdentifier Your Apple Merchant ID, as obtained at https://developer.apple.com/account/ios/identifiers/merchant/merchantCreate.action + * + * @return a `PKPaymentRequest` with proper default values. Returns nil if running on < iOS8. + */ ++ (PKPaymentRequest *)paymentRequestWithMerchantIdentifier:(NSString *)merchantIdentifier NS_AVAILABLE_IOS(8_0); + + +@end + +#pragma mark - Deprecated Methods + +/** + * A callback to be run with a token response from the Stripe API. + * + * @param token The Stripe token from the response. Will be nil if an error occurs. @see STPToken + * @param error The error returned from the response, or nil in one occurs. @see StripeError.h for possible values. + * @deprecated This has been renamed to STPTokenCompletionBlock. + */ +typedef void (^STPCompletionBlock)(STPToken * __nullable token, NSError * __nullable error) __attribute__((deprecated("STPCompletionBlock has been renamed to STPTokenCompletionBlock."))); + +// These methods are deprecated. You should instead use STPAPIClient to create tokens. +// Example: [Stripe createTokenWithCard:card completion:completion]; +// becomes [[STPAPIClient sharedClient] createTokenWithCard:card completion:completion]; +@interface Stripe (Deprecated) + +/** + * Securely convert your user's credit card details into a Stripe token, which you can then safely store on your server and use to charge the user. The URL + *connection will run on the main queue. Uses the value of [Stripe defaultPublishableKey] for authentication. + * + * @param card The user's card details. @see STPCard + * @param handler Code to run when the user's card has been turned into a Stripe token. + * @deprecated Use STPAPIClient instead. + */ ++ (void)createTokenWithCard:(STPCard *)card completion:(nullable STPCompletionBlock)handler __attribute__((deprecated)); + +/** + * Securely convert your user's credit card details into a Stripe token, which you can then safely store on your server and use to charge the user. The URL + *connection will run on the main queue. + * + * @param card The user's card details. @see STPCard + * @param publishableKey The API key to use to authenticate with Stripe. Get this at https://stripe.com/account/apikeys . + * @param handler Code to run when the user's card has been turned into a Stripe token. + * @deprecated Use STPAPIClient instead. + */ ++ (void)createTokenWithCard:(STPCard *)card publishableKey:(NSString *)publishableKey completion:(nullable STPCompletionBlock)handler __attribute__((deprecated)); + +/** + * Securely convert your user's credit card details into a Stripe token, which you can then safely store on your server and use to charge the user. The URL + *connection will run on the main queue. Uses the value of [Stripe defaultPublishableKey] for authentication. + * + * @param bankAccount The user's bank account details. @see STPBankAccount + * @param handler Code to run when the user's card has been turned into a Stripe token. + * @deprecated Use STPAPIClient instead. + */ ++ (void)createTokenWithBankAccount:(STPBankAccount *)bankAccount completion:(nullable STPCompletionBlock)handler __attribute__((deprecated)); + +/** + * Securely convert your user's credit card details into a Stripe token, which you can then safely store on your server and use to charge the user. The URL + *connection will run on the main queue. Uses the value of [Stripe defaultPublishableKey] for authentication. + * + * @param bankAccount The user's bank account details. @see STPBankAccount + * @param publishableKey The API key to use to authenticate with Stripe. Get this at https://stripe.com/account/apikeys . + * @param handler Code to run when the user's card has been turned into a Stripe token. + * @deprecated Use STPAPIClient instead. + */ ++ (void)createTokenWithBankAccount:(STPBankAccount *)bankAccount + publishableKey:(NSString *)publishableKey + completion:(nullable STPCompletionBlock)handler __attribute__((deprecated)); + ++ (BOOL)deviceSupportsApplePay; + +@end + +NS_ASSUME_NONNULL_END diff --git a/core-xprojects/Stripe/PublicHeaders/Stripe/STPAPIResponseDecodable.h b/core-xprojects/Stripe/PublicHeaders/Stripe/STPAPIResponseDecodable.h new file mode 100755 index 0000000000..d90b38517b --- /dev/null +++ b/core-xprojects/Stripe/PublicHeaders/Stripe/STPAPIResponseDecodable.h @@ -0,0 +1,28 @@ +// +// STPAPIResponseDecodable.h +// Stripe +// +// Created by Jack Flintermann on 10/14/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import + +@protocol STPAPIResponseDecodable + +/** + * These fields are required to be present in the API response. If any of them are nil, `decodedObjectFromAPIResponse` should also return nil. + */ ++ (nonnull NSArray *)requiredFields; + +/** + * Parses an response from the Stripe API (in JSON format; represented as an `NSDictionary`) into an instance of the class. Returns nil if the object could not be decoded (i.e. if one of its `requiredFields` is nil). + */ ++ (nullable instancetype)decodedObjectFromAPIResponse:(nullable NSDictionary *)response; + +/** + * The raw JSON response used to create the object. This can be useful for using beta features that haven't yet been made into properties in the SDK. + */ +@property(nonatomic, readonly, nonnull, copy)NSDictionary *allResponseFields; + +@end diff --git a/core-xprojects/Stripe/PublicHeaders/Stripe/STPAddress.h b/core-xprojects/Stripe/PublicHeaders/Stripe/STPAddress.h new file mode 100755 index 0000000000..53d19a99b6 --- /dev/null +++ b/core-xprojects/Stripe/PublicHeaders/Stripe/STPAddress.h @@ -0,0 +1,84 @@ +// +// STPAddress.h +// Stripe +// +// Created by Ben Guo on 4/13/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import + +/** + * What set of billing address information you need to collect from your user. + * + * @note If the user is from a country that does not use zip/postal codes, + * the user may not be asked for one regardless of this setting. + */ +typedef NS_ENUM(NSUInteger, STPBillingAddressFields) { + /** + * No billing address information + */ + STPBillingAddressFieldsNone, + /** + * Just request the user's billing ZIP code + */ + STPBillingAddressFieldsZip, + /** + * Request the user's full billing address + */ + STPBillingAddressFieldsFull, +}; + +/** + * STPAddress Contains an address as represented by the Stripe API. + */ +@interface STPAddress : NSObject + +/** + * The user's full name (e.g. "Jane Doe") + */ +@property (nonatomic, copy) NSString *name; + +/** + * The first line of the user's street address (e.g. "123 Fake St") + */ +@property (nonatomic, copy) NSString *line1; + +/** + * The apartment, floor number, etc of the user's street address (e.g. "Apartment 1A") + */ +@property (nonatomic, copy) NSString *line2; + +/** + * The city in which the user resides (e.g. "San Francisco") + */ +@property (nonatomic, copy) NSString *city; + +/** + * The state in which the user resides (e.g. "CA") + */ +@property (nonatomic, copy) NSString *state; + +/** + * The postal code in which the user resides (e.g. "90210") + */ +@property (nonatomic, copy) NSString *postalCode; + +/** + * The ISO country code of the address (e.g. "US") + */ +@property (nonatomic, copy) NSString *country; + +/** + * The phone number of the address (e.g. "8885551212") + */ +@property (nonatomic, copy) NSString *phone; + +/** + * The email of the address (e.g. "jane@doe.com") + */ +@property (nonatomic, copy) NSString *email; + +- (BOOL)containsRequiredFields:(STPBillingAddressFields)requiredFields; + +@end diff --git a/core-xprojects/Stripe/PublicHeaders/Stripe/STPBINRange.h b/core-xprojects/Stripe/PublicHeaders/Stripe/STPBINRange.h new file mode 100755 index 0000000000..d9d0e8c93e --- /dev/null +++ b/core-xprojects/Stripe/PublicHeaders/Stripe/STPBINRange.h @@ -0,0 +1,26 @@ +// +// STPBINRange.h +// Stripe +// +// Created by Jack Flintermann on 5/24/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface STPBINRange : NSObject + +@property(nonatomic, readonly)NSUInteger length; +@property(nonatomic, readonly)STPCardBrand brand; + ++ (NSArray *)allRanges; ++ (NSArray *)binRangesForNumber:(NSString *)number; ++ (NSArray *)binRangesForBrand:(STPCardBrand)brand; ++ (instancetype)mostSpecificBINRangeForNumber:(NSString *)number; + +@end + +NS_ASSUME_NONNULL_END diff --git a/core-xprojects/Stripe/PublicHeaders/Stripe/STPBackendAPIAdapter.h b/core-xprojects/Stripe/PublicHeaders/Stripe/STPBackendAPIAdapter.h new file mode 100755 index 0000000000..54f23226e8 --- /dev/null +++ b/core-xprojects/Stripe/PublicHeaders/Stripe/STPBackendAPIAdapter.h @@ -0,0 +1,65 @@ +// +// STPBackendAPIAdapter.h +// Stripe +// +// Created by Jack Flintermann on 1/12/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import +#import +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@class STPCard, STPToken; + +/** + * Call this block after you're done fetching a customer on your server. You can use the `STPCustomerDeserializer` class to convert a JSON response into an `STPCustomer` object. + * + * @param customer a deserialized `STPCustomer` object obtained from your backend API, or nil if an error occurred. + * @param error any error that occurred while communicating with your server, or nil if your call succeeded + */ +typedef void (^STPCustomerCompletionBlock)(STPCustomer * __nullable customer, NSError * __nullable error); + +/** + * You should make your application's API client conform to this interface in order to use it with an `STPPaymentContext`. It provides a "bridge" from the prebuilt UI we expose (such as `STPPaymentMethodsViewController`) to your backend to fetch the information it needs to power those views. To read about how to implement this protocol, see https://stripe.com/docs/mobile/ios#prepare-your-api . To see examples of implementing these APIs, see MyAPIClient.swift in our example project and https://github.com/stripe/example-ios-backend . + */ +@protocol STPBackendAPIAdapter + +/** + * Retrieve the cards to be displayed inside a payment context. On your backend, retrieve the Stripe customer associated with your currently logged-in user (see https://stripe.com/docs/api#retrieve_customer ), and return the raw JSON response from the Stripe API. (For an example Ruby implementation of this API, see https://github.com/stripe/example-ios-backend/blob/master/web.rb#L40 ). Back in your iOS app, after you've called this API, deserialize your API response into an `STPCustomer` object (you can use the `STPCustomerDeserializer` class to do this). See MyAPIClient.swift in our example project to see this in action. + * + * @see STPCard + * @param completion call this callback when you're done fetching and parsing the above information from your backend. For example, `completion(customer, nil)` (if your call succeeds) or `completion(nil, error)` if an error is returned. + * + * @note If you are on Swift 3, you must declare the completion block as `@escaping` or Xcode will give you a protocol conformance error. https://bugs.swift.org/browse/SR-2597 + */ +- (void)retrieveCustomer:(STPCustomerCompletionBlock)completion; + +/** + * Adds a payment source to a customer. On your backend, retrieve the Stripe customer associated with your logged-in user. Then, call the Update Customer method on that customer as described at https://stripe.com/docs/api#update_customer (for an example Ruby implementation of this API, see https://github.com/stripe/example-ios-backend/blob/master/web.rb#L60 ). If this API call succeeds, call `completion(nil)`. Otherwise, call `completion(error)` with the error that occurred. + * + * @param source a valid payment source, such as a card token. + * @param completion call this callback when you're done adding the token to the customer on your backend. For example, `completion(nil)` (if your call succeeds) or `completion(error)` if an error is returned. + * + * @note If you are on Swift 3, you must declare the completion block as `@escaping` or Xcode will give you a protocol conformance error. https://bugs.swift.org/browse/SR-2597 + */ +- (void)attachSourceToCustomer:(id)source completion:(STPErrorBlock)completion; + +/** + * Change a customer's `default_source` to be the provided card. On your backend, retrieve the Stripe customer associated with your logged-in user. Then, call the Customer Update method as described at https://stripe.com/docs/api#update_customer , specifying default_source to be the value of source.stripeID (for an example Ruby implementation of this API, see https://github.com/stripe/example-ios-backend/blob/master/web.rb#L82 ). If this API call succeeds, call `completion(nil)`. Otherwise, call `completion(error)` with the error that occurred. + * + * @param source The newly-selected default source for the user. + * @param completion call this callback when you're done selecting the new default source for the customer on your backend. For example, `completion(nil)` (if your call succeeds) or `completion(error)` if an error is returned. + * + * @note If you are on Swift 3, you must declare the completion block as `@escaping` or Xcode will give you a protocol conformance error. https://bugs.swift.org/browse/SR-2597 + */ +- (void)selectDefaultCustomerSource:(id)source completion:(STPErrorBlock)completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/core-xprojects/Stripe/PublicHeaders/Stripe/STPBankAccount.h b/core-xprojects/Stripe/PublicHeaders/Stripe/STPBankAccount.h new file mode 100755 index 0000000000..a01b7565c7 --- /dev/null +++ b/core-xprojects/Stripe/PublicHeaders/Stripe/STPBankAccount.h @@ -0,0 +1,98 @@ +// +// STPBankAccount.h +// Stripe +// +// Created by Charles Scalesse on 10/1/14. +// +// + +#import +#import +#import + +typedef NS_ENUM(NSInteger, STPBankAccountStatus) { + STPBankAccountStatusNew, + STPBankAccountStatusValidated, + STPBankAccountStatusVerified, + STPBankAccountStatusErrored, +}; + +/** + * Representation of a user's bank account details that have been tokenized with the Stripe API. @see https://stripe.com/docs/api#cards + */ +@interface STPBankAccount : STPBankAccountParams + +/** + * The last 4 digits of the bank account's account number. + */ +- (nonnull NSString *)last4; + +/** + * The routing number for the bank account. This should be the ACH routing number, not the wire routing number. + */ +@property (nonatomic, copy, nonnull) NSString *routingNumber; + +/** + * Two-letter ISO code representing the country the bank account is located in. + */ +@property (nonatomic, copy, nullable) NSString *country; + +/** + * The default currency for the bank account. + */ +@property (nonatomic, copy, nullable) NSString *currency; + +/** + * The Stripe ID for the bank account. + */ +@property (nonatomic, readonly, nonnull) NSString *bankAccountId; + +/** + * The last 4 digits of the account number. + */ +@property (nonatomic, readonly, nullable) NSString *last4; + +/** + * The name of the bank that owns the account. + */ +@property (nonatomic, readonly, nullable) NSString *bankName; + +/** + * The name of the person or business that owns the bank account. + */ +@property(nonatomic, copy, nullable) NSString *accountHolderName; + +/** + * The type of entity that holds the account. + */ +@property(nonatomic) STPBankAccountHolderType accountHolderType; + +/** + * A proxy for the account number, this uniquely identifies the account and can be used to compare equality of different bank accounts. + */ +@property (nonatomic, readonly, nullable) NSString *fingerprint; + +/** + * The validation status of the bank account. @see STPBankAccountStatus + */ +@property (nonatomic, readonly) STPBankAccountStatus status; + +/** + * Whether or not the bank account has been validated via microdeposits or other means. + * @deprecated Use status == STPBankAccountStatusValidated instead. + */ +@property (nonatomic, readonly) BOOL validated __attribute__((deprecated("Use status == STPBankAccountStatusValidated instead."))); + +/** + * Whether or not the bank account is currently disabled. + * @deprecated Use status == STPBankAccountStatusErrored instead. + */ +@property (nonatomic, readonly) BOOL disabled __attribute__((deprecated("Use status == STPBankAccountStatusErrored instead."))); + +#pragma mark - deprecated setters for STPBankAccountParams properties + +#define DEPRECATED_IN_FAVOR_OF_STPBANKACCOUNTPARAMS __attribute__((deprecated("For collecting your users' bank account details, you should use an STPBankAccountParams object instead of an STPBankAccount."))) + +- (void)setAccountNumber:(nullable NSString *)accountNumber DEPRECATED_IN_FAVOR_OF_STPBANKACCOUNTPARAMS; + +@end diff --git a/core-xprojects/Stripe/PublicHeaders/Stripe/STPBankAccountParams.h b/core-xprojects/Stripe/PublicHeaders/Stripe/STPBankAccountParams.h new file mode 100755 index 0000000000..ada0add4e8 --- /dev/null +++ b/core-xprojects/Stripe/PublicHeaders/Stripe/STPBankAccountParams.h @@ -0,0 +1,58 @@ +// +// STPBankAccountParams.h +// Stripe +// +// Created by Jack Flintermann on 10/4/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import +#import + +typedef NS_ENUM(NSInteger, STPBankAccountHolderType) { + STPBankAccountHolderTypeIndividual, + STPBankAccountHolderTypeCompany, +}; + +/** + * Representation of a user's bank account details. You can assemble these with information that your user enters and + * then create Stripe tokens with them using an STPAPIClient. @see https://stripe.com/docs/api#create_bank_account_token + */ +@interface STPBankAccountParams : NSObject + +/** + * The account number for the bank account. Currently must be a checking account. + */ +@property (nonatomic, copy, nullable) NSString *accountNumber; + +/** + * The last 4 digits of the bank account's account number, if it's been set, otherwise nil. + */ +- (nullable NSString *)last4; + +/** + * The routing number for the bank account. This should be the ACH routing number, not the wire routing number. + */ +@property (nonatomic, copy, nullable) NSString *routingNumber; + +/** + * Two-letter ISO code representing the country the bank account is located in. + */ +@property (nonatomic, copy, nullable) NSString *country; + +/** + * The default currency for the bank account. + */ +@property (nonatomic, copy, nullable) NSString *currency; + +/** + * The name of the person or business that owns the bank account. + */ +@property(nonatomic, copy, nullable) NSString *accountHolderName; + +/** + * The type of entity that holds the account. Defaults to STPBankAccountHolderTypeIndividual. + */ +@property(nonatomic) STPBankAccountHolderType accountHolderType; + +@end diff --git a/core-xprojects/Stripe/PublicHeaders/Stripe/STPBlocks.h b/core-xprojects/Stripe/PublicHeaders/Stripe/STPBlocks.h new file mode 100755 index 0000000000..9241997934 --- /dev/null +++ b/core-xprojects/Stripe/PublicHeaders/Stripe/STPBlocks.h @@ -0,0 +1,49 @@ +// +// STPBlocks.h +// Stripe +// +// Created by Jack Flintermann on 3/23/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import + +@class STPToken; + +/** + * An enum representing the status of a payment requested from the user. + */ +typedef NS_ENUM(NSUInteger, STPPaymentStatus) { + /** + * The payment succeeded. + */ + STPPaymentStatusSuccess, + /** + * The payment failed due to an unforeseen error, such as the user's Internet connection being offline. + */ + STPPaymentStatusError, + /** + * The user cancelled the payment (for example, by hitting "cancel" in the Apple Pay dialog). + */ + STPPaymentStatusUserCancellation, +}; + +/** + * An empty block, called with no arguments, returning nothing. + */ +typedef void (^STPVoidBlock)(); + +/** + * A block that may optionally be called with an error. + * + * @param error The error that occurred, if any. + */ +typedef void (^STPErrorBlock)(NSError * __nullable error); + +/** + * A callback to be run with a token response from the Stripe API. + * + * @param token The Stripe token from the response. Will be nil if an error occurs. @see STPToken + * @param error The error returned from the response, or nil in one occurs. @see StripeError.h for possible values. + */ +typedef void (^STPTokenCompletionBlock)(STPToken * __nullable token, NSError * __nullable error); diff --git a/core-xprojects/Stripe/PublicHeaders/Stripe/STPCard.h b/core-xprojects/Stripe/PublicHeaders/Stripe/STPCard.h new file mode 100755 index 0000000000..3db9a08e19 --- /dev/null +++ b/core-xprojects/Stripe/PublicHeaders/Stripe/STPCard.h @@ -0,0 +1,168 @@ +// +// STPCard.h +// Stripe +// +// Created by Saikat Chakrabarti on 11/2/12. +// +// + +#import + +#import +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * The various funding sources for a payment card. + */ +typedef NS_ENUM(NSInteger, STPCardFundingType) { + STPCardFundingTypeDebit, + STPCardFundingTypeCredit, + STPCardFundingTypePrepaid, + STPCardFundingTypeOther, +}; + +/** + * Representation of a user's credit card details that have been tokenized with the Stripe API. @see https://stripe.com/docs/api#cards + */ +@interface STPCard : STPCardParams + +/** + * Create an STPCard from a Stripe API response. + * + * @param cardID The Stripe ID of the card, e.g. `card_185iQx4JYtv6MPZKfcuXwkOx` + * @param brand The brand of the card (e.g. "Visa". To obtain this enum value from a string, use `[STPCardBrand brandFromString:string]`; + * @param last4 The last 4 digits of the card, e.g. 4242 + * @param expMonth The card's expiration month, 1-indexed (i.e. 1 = January) + * @param expYear The card's expiration year + * @param funding The card's funding type (credit, debit, or prepaid). To obtain this enum value from a string, use `[STPCardBrand fundingFromString:string]`. + * + * @return an STPCard instance populated with the provided values. + */ +- (instancetype)initWithID:(NSString *)cardID + brand:(STPCardBrand)brand + last4:(NSString *)last4 + expMonth:(NSUInteger)expMonth + expYear:(NSUInteger)expYear + funding:(STPCardFundingType)funding; + +/** + * This parses a string representing a card's brand into the appropriate STPCardBrand enum value, i.e. `[STPCard brandFromString:@"American Express"] == STPCardBrandAmex` + * + * @param string a string representing the card's brand as returned from the Stripe API + * + * @return an enum value mapped to that string. If the string is unrecognized, returns STPCardBrandUnknown. + */ ++ (STPCardBrand)brandFromString:(NSString *)string; + +/** + * This parses a string representing a card's funding type into the appropriate `STPCardFundingType` enum value, i.e. `[STPCard fundingFromString:@"prepaid"] == STPCardFundingTypePrepaid`. + * + * @param string a string representing the card's funding type as returned from the Stripe API + * + * @return an enum value mapped to that string. If the string is unrecognized, returns `STPCardFundingTypeOther`. + */ ++ (STPCardFundingType)fundingFromString:(NSString *)string; + +/** + * The last 4 digits of the card. + */ +@property (nonatomic, readonly) NSString *last4; + +/** + * For cards made with Apple Pay, this refers to the last 4 digits of the "Device Account Number" for the tokenized card. For regular cards, it will be nil. + */ +@property (nonatomic, readonly, nullable) NSString *dynamicLast4; + +/** + * Whether or not the card originated from Apple Pay. + */ +@property (nonatomic, readonly) BOOL isApplePayCard; + +/** + * The card's expiration month. 1-indexed (i.e. 1 == January) + */ +@property (nonatomic) NSUInteger expMonth; + +/** + * The card's expiration year. + */ +@property (nonatomic) NSUInteger expYear; + +/** + * The cardholder's name. + */ +@property (nonatomic, copy, nullable) NSString *name; + +/** + * The cardholder's address. + */ +@property (nonatomic, copy, nullable) NSString *addressLine1; +@property (nonatomic, copy, nullable) NSString *addressLine2; +@property (nonatomic, copy, nullable) NSString *addressCity; +@property (nonatomic, copy, nullable) NSString *addressState; +@property (nonatomic, copy, nullable) NSString *addressZip; +@property (nonatomic, copy, nullable) NSString *addressCountry; + +/** + * The Stripe ID for the card. + */ +@property (nonatomic, readonly, nullable) NSString *cardId; + +/** + * The issuer of the card. + */ +@property (nonatomic, readonly) STPCardBrand brand; + +/** + * The issuer of the card. + * Can be one of "Visa", "American Express", "MasterCard", "Discover", "JCB", "Diners Club", or "Unknown" + * @deprecated use `brand` instead. + */ +@property (nonatomic, readonly) NSString *type __attribute__((deprecated)); + +/** + * The funding source for the card (credit, debit, prepaid, or other) + */ +@property (nonatomic, readonly) STPCardFundingType funding; + +/** + * A proxy for the card's number, this uniquely identifies the credit card and can be used to compare different cards. + * @deprecated This field will no longer be present in responses when using your publishable key. If you want to access the value of this field, you can look it up on your backend using your secret key. + */ +@property (nonatomic, readonly, nullable) NSString *fingerprint __attribute__((deprecated("This field will no longer be present in responses when using your publishable key. If you want to access the value of this field, you can look it up on your backend using your secret key."))); + +/** + * Two-letter ISO code representing the issuing country of the card. + */ +@property (nonatomic, readonly, nullable) NSString *country; + +/** + * This is only applicable when tokenizing debit cards to issue payouts to managed accounts. You should not set it otherwise. The card can then be used as a transfer destination for funds in this currency. + */ +@property (nonatomic, copy, nullable) NSString *currency; + +#pragma mark - deprecated properties + +#define DEPRECATED_IN_FAVOR_OF_STPCARDPARAMS __attribute__((deprecated("For collecting your users' credit card details, you should use an STPCardParams object instead of an STPCard."))) + +@property (nonatomic, copy, nullable) NSString *number DEPRECATED_IN_FAVOR_OF_STPCARDPARAMS; +@property (nonatomic, copy, nullable) NSString *cvc DEPRECATED_IN_FAVOR_OF_STPCARDPARAMS; +- (void)setExpMonth:(NSUInteger)expMonth DEPRECATED_IN_FAVOR_OF_STPCARDPARAMS; +- (void)setExpYear:(NSUInteger)expYear DEPRECATED_IN_FAVOR_OF_STPCARDPARAMS; +- (void)setName:(nullable NSString *)name DEPRECATED_IN_FAVOR_OF_STPCARDPARAMS; +- (void)setAddressLine1:(nullable NSString *)addressLine1 DEPRECATED_IN_FAVOR_OF_STPCARDPARAMS; +- (void)setAddressLine2:(nullable NSString *)addressLine2 DEPRECATED_IN_FAVOR_OF_STPCARDPARAMS; +- (void)setAddressCity:(nullable NSString *)addressCity DEPRECATED_IN_FAVOR_OF_STPCARDPARAMS; +- (void)setAddressState:(nullable NSString *)addressState DEPRECATED_IN_FAVOR_OF_STPCARDPARAMS; +- (void)setAddressZip:(nullable NSString *)addressZip DEPRECATED_IN_FAVOR_OF_STPCARDPARAMS; +- (void)setAddressCountry:(nullable NSString *)addressCountry DEPRECATED_IN_FAVOR_OF_STPCARDPARAMS; + + +@end + +NS_ASSUME_NONNULL_END diff --git a/core-xprojects/Stripe/PublicHeaders/Stripe/STPCardBrand.h b/core-xprojects/Stripe/PublicHeaders/Stripe/STPCardBrand.h new file mode 100755 index 0000000000..cc4390ccd3 --- /dev/null +++ b/core-xprojects/Stripe/PublicHeaders/Stripe/STPCardBrand.h @@ -0,0 +1,22 @@ +// +// STPCardBrand.h +// Stripe +// +// Created by Jack Flintermann on 7/24/15. +// Copyright (c) 2015 Stripe, Inc. All rights reserved. +// + +#import + +/** + * The various card brands to which a payment card can belong. + */ +typedef NS_ENUM(NSInteger, STPCardBrand) { + STPCardBrandVisa, + STPCardBrandAmex, + STPCardBrandMasterCard, + STPCardBrandDiscover, + STPCardBrandJCB, + STPCardBrandDinersClub, + STPCardBrandUnknown, +}; diff --git a/core-xprojects/Stripe/PublicHeaders/Stripe/STPCardParams.h b/core-xprojects/Stripe/PublicHeaders/Stripe/STPCardParams.h new file mode 100755 index 0000000000..926fb088d4 --- /dev/null +++ b/core-xprojects/Stripe/PublicHeaders/Stripe/STPCardParams.h @@ -0,0 +1,98 @@ +// +// STPCardParams.h +// Stripe +// +// Created by Jack Flintermann on 10/4/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import +#import +#if TARGET_OS_IPHONE +#import +#endif + +/** + * Representation of a user's credit card details. You can assemble these with information that your user enters and + * then create Stripe tokens with them using an STPAPIClient. @see https://stripe.com/docs/api#cards + */ +@interface STPCardParams : NSObject + +/** + * The card's number. + */ +@property (nonatomic, copy, nullable) NSString *number; + +/** + * The last 4 digits of the card's number, if it's been set, otherwise nil. + */ +- (nullable NSString *)last4; + +/** + * The card's expiration month. + */ +@property (nonatomic) NSUInteger expMonth; + +/** + * The card's expiration year. + */ +@property (nonatomic) NSUInteger expYear; + +/** + * The card's security code, found on the back. + */ +@property (nonatomic, copy, nullable) NSString *cvc; + +/** + * The cardholder's name. + */ +@property (nonatomic, copy, nullable) NSString *name; + +/** + * The cardholder's address. + */ +#if TARGET_OS_IPHONE +@property(nonatomic, copy, nonnull) STPAddress *address; +#endif + +@property (nonatomic, copy, nullable) NSString *addressLine1; +@property (nonatomic, copy, nullable) NSString *addressLine2; +@property (nonatomic, copy, nullable) NSString *addressCity; +@property (nonatomic, copy, nullable) NSString *addressState; +@property (nonatomic, copy, nullable) NSString *addressZip; +@property (nonatomic, copy, nullable) NSString *addressCountry; + +/** + * Three-letter ISO currency code representing the currency paid out to the bank account. This is only applicable when tokenizing debit cards to issue payouts to managed accounts. You should not set it otherwise. The card can then be used as a transfer destination for funds in this currency. + */ +@property (nonatomic, copy, nullable) NSString *currency; + +/** + * Validate each field of the card. + * @return whether or not that field is valid. + * @deprecated use STPCardValidator instead. + */ +- (BOOL)validateNumber:(__nullable id * __nullable )ioValue + error:(NSError * __nullable * __nullable )outError __attribute__((deprecated("Use STPCardValidator instead."))); +- (BOOL)validateCvc:(__nullable id * __nullable )ioValue + error:(NSError * __nullable * __nullable )outError __attribute__((deprecated("Use STPCardValidator instead."))); +- (BOOL)validateExpMonth:(__nullable id * __nullable )ioValue + error:(NSError * __nullable * __nullable )outError __attribute__((deprecated("Use STPCardValidator instead."))); +- (BOOL)validateExpYear:(__nullable id * __nullable)ioValue + error:(NSError * __nullable * __nullable )outError __attribute__((deprecated("Use STPCardValidator instead."))); + +/** + * This validates a fully populated card to check for all errors, including ones that come about + * from the interaction of more than one property. It will also do all the validations on individual + * properties, so if you only want to call one method on your card to validate it after setting all the + * properties, call this one + * + * @param outError a pointer to an NSError that, after calling this method, will be populated with an error if the card is not valid. See StripeError.h for + possible values + * + * @return whether or not the card is valid. + * @deprecated use STPCardValidator instead. + */ +- (BOOL)validateCardReturningError:(NSError * __nullable * __nullable)outError __attribute__((deprecated("Use STPCardValidator instead."))); + +@end diff --git a/core-xprojects/Stripe/PublicHeaders/Stripe/STPCardValidationState.h b/core-xprojects/Stripe/PublicHeaders/Stripe/STPCardValidationState.h new file mode 100755 index 0000000000..7efc4870ca --- /dev/null +++ b/core-xprojects/Stripe/PublicHeaders/Stripe/STPCardValidationState.h @@ -0,0 +1,18 @@ +// +// STPCardValidationState.h +// Stripe +// +// Created by Jack Flintermann on 8/7/15. +// Copyright (c) 2015 Stripe, Inc. All rights reserved. +// + +#import + +/** + * These fields indicate whether a card field represents a valid value, invalid value, or incomplete value. + */ +typedef NS_ENUM(NSInteger, STPCardValidationState) { + STPCardValidationStateValid, // The field's contents are valid. For example, a valid, 16-digit card number. + STPCardValidationStateInvalid, // The field's contents are invalid. For example, an expiration date of "13/42". + STPCardValidationStateIncomplete, // The field's contents are not yet valid, but could be by typing additional characters. For example, a CVC of "1". +}; diff --git a/core-xprojects/Stripe/PublicHeaders/Stripe/STPCardValidator.h b/core-xprojects/Stripe/PublicHeaders/Stripe/STPCardValidator.h new file mode 100755 index 0000000000..f6bc671b50 --- /dev/null +++ b/core-xprojects/Stripe/PublicHeaders/Stripe/STPCardValidator.h @@ -0,0 +1,119 @@ +// +// STPCardValidator.h +// Stripe +// +// Created by Jack Flintermann on 7/15/15. +// Copyright (c) 2015 Stripe, Inc. All rights reserved. +// + +#import + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * This class contains static methods to validate card numbers, expiration dates, and CVCs. For a list of test card numbers to use with this code, see https://stripe.com/docs/testing + */ +@interface STPCardValidator : NSObject + +/** + * Returns a copy of the passed string with all non-numeric characters removed. + */ ++ (NSString *)sanitizedNumericStringForString:(NSString *)string; + +/** + * Whether or not the target string contains only numeric characters. + */ ++ (BOOL)stringIsNumeric:(NSString *)string; + +/** + * Validates a card number, passed as a string. This will return STPCardValidationStateInvalid for numbers that are too short or long, contain invalid characters, do not pass Luhn validation, or (optionally) do not match a number format issued by a major card brand. + * + * @param cardNumber The card number to validate. Ex. @"4242424242424242" + * @param validatingCardBrand Whether or not to enforce that the number appears to be issued by a major card brand (or could be). For example, no issuing card network currently issues card numbers beginning with the digit 9; if an otherwise correct-length and luhn-valid card number beginning with 9 (example: 9999999999999995) were passed to this method, it would return STPCardValidationStateInvalid if this parameter were YES and STPCardValidationStateValid if this parameter were NO. If unsure, you should use YES for this value. + * + * @return STPCardValidationStateValid if the number is valid, STPCardValidationStateInvalid if the number is invalid, or STPCardValidationStateIncomplete if the number is a substring of a valid card (e.g. @"4242"). + */ ++ (STPCardValidationState)validationStateForNumber:(NSString *)cardNumber + validatingCardBrand:(BOOL)validatingCardBrand; + +/** + * The card brand for a card number or substring thereof. + * + * @param cardNumber A card number, or partial card number. For example, @"4242", @"5555555555554444", or @"123". + * + * @return The brand for that card number. The example parameters would return STPCardBrandVisa, STPCardBrandMasterCard, and STPCardBrandUnknown, respectively. + */ ++ (STPCardBrand)brandForNumber:(NSString *)cardNumber; + +/** + * The possible number lengths for cards associated with a card brand. For example, Discover card numbers contain 16 characters, while American Express cards contain 15 characters. + */ ++ (NSSet*)lengthsForCardBrand:(STPCardBrand)brand; ++ (NSInteger)maxLengthForCardBrand:(STPCardBrand)brand; ++ (NSInteger)lengthForCardBrand:(STPCardBrand)brand __attribute__((deprecated("Card brands may have multiple lengths - use lengthsForCardBrand or maxLengthForCardBrand instead."))); + +/** + * The length of the final grouping of digits to use when formatting a card number for display. For example, Visa cards display their final 4 numbers, e.g. "4242", while American Express cards display their final 5 digits, e.g. "10005". + */ ++ (NSInteger)fragmentLengthForCardBrand:(STPCardBrand)brand; + +/** + * Validates an expiration month, passed as an (optionally 0-padded) string. Example valid values are "3", "12", and "08". Example invalid values are "99", "a", and "00". Incomplete values include "0" and "1". + * + * @param expirationMonth A string representing a 2-digit expiration month for a payment card. + * + * @return STPCardValidationStateValid if the month is valid, STPCardValidationStateInvalid if the month is invalid, or STPCardValidationStateIncomplete if the month is a substring of a valid month (e.g. @"0" or @"1"). + */ ++ (STPCardValidationState)validationStateForExpirationMonth:(NSString *)expirationMonth; + +/** + * Validates an expiration year, passed as a string representing the final 2 digits of the year. This considers the period between the current year until 2099 as valid times. An example valid value would be "16" (assuming the current year, as determined by [NSDate date], is 2015). Will return STPCardValidationStateInvalid for a month/year combination that is earlier than the current date (i.e. @"15" and @"04" in October 2015. Example invalid values are "00", "a", and "13". Any 1-digit string will return STPCardValidationStateIncomplete. + * + * @param expirationYear A string representing a 2-digit expiration year for a payment card. + * @param expirationMonth A string representing a 2-digit expiration month for a payment card. See -validationStateForExpirationMonth for the desired formatting of this string. + * + * @return STPCardValidationStateValid if the year is valid, STPCardValidationStateInvalid if the year is invalid, or STPCardValidationStateIncomplete if the year is a substring of a valid year (e.g. @"1" or @"2"). + */ ++ (STPCardValidationState)validationStateForExpirationYear:(NSString *)expirationYear + inMonth:(NSString *)expirationMonth; + +/** + * The max CVC length for a card brand (for context, American Express CVCs are 4 digits, while all others are 3). + */ ++ (NSUInteger)maxCVCLengthForCardBrand:(STPCardBrand)brand; + +/** + * Validates a card's CVC, passed as a numeric string, for the given card brand. + * + * @param cvc the CVC to validate + * @param brand the card brand (can be determined from the card's number using +brandForNumber) + * + * @return Whether the CVC represents a valid CVC for that card brand. For example, would return STPCardValidationStateValid for @"123" and STPCardBrandVisa, STPCardValidationStateValid for @"1234" and STPCardBrandAmericanExpress, STPCardValidationStateIncomplete for @"12" and STPCardBrandVisa, and STPCardValidationStateInvalid for @"12345" and any brand. + */ ++ (STPCardValidationState)validationStateForCVC:(NSString *)cvc cardBrand:(STPCardBrand)brand; + +/** + * Validates the given card details. + * + * @param card the card details to validate. + * + * @return STPCardValidationStateValid if all fields are valid, STPCardValidationStateInvalid if any field is invalid, or STPCardValidationStateIncomplete if all fields are either incomplete or valid. + */ ++ (STPCardValidationState)validationStateForCard:(STPCardParams *)card; + +// Exposed for testing only. ++ (STPCardValidationState)validationStateForExpirationYear:(NSString *)expirationYear + inMonth:(NSString *)expirationMonth + inCurrentYear:(NSInteger)currentYear + currentMonth:(NSInteger)currentMonth; ++ (STPCardValidationState)validationStateForCard:(STPCardParams *)card + inCurrentYear:(NSInteger)currentYear + currentMonth:(NSInteger)currentMonth; + +@end + +NS_ASSUME_NONNULL_END diff --git a/core-xprojects/Stripe/PublicHeaders/Stripe/STPCustomer.h b/core-xprojects/Stripe/PublicHeaders/Stripe/STPCustomer.h new file mode 100755 index 0000000000..bfc14864ee --- /dev/null +++ b/core-xprojects/Stripe/PublicHeaders/Stripe/STPCustomer.h @@ -0,0 +1,86 @@ +// +// STPCustomer.h +// Stripe +// +// Created by Jack Flintermann on 6/9/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * An `STPCustomer` represents a deserialized Customer object from the Stripe API. You can use `STPCustomerDeserializer` to convert a JSON response from the Stripe API into an `STPCustomer`. + */ +@interface STPCustomer : NSObject + +/** + * Initialize a customer object with the provided values. + * + * @param stripeID The ID of the customer, e.g. `cus_abc` + * @param defaultSource The default source of the customer, such as an `STPCard` object. Can be nil. + * @param sources All of the customer's payment sources. This might be an empty array. + * + * @return an instance of STPCustomer + */ ++ (instancetype)customerWithStripeID:(NSString *)stripeID + defaultSource:(nullable id)defaultSource + sources:(NSArray> *)sources; + +/** + * The Stripe ID of the customer, e.g. `cus_1234` + */ +@property(nonatomic, readonly, copy)NSString *stripeID; + +/** + * The default source used to charge the customer. + */ +@property(nonatomic, readonly, nullable) id defaultSource; + +/** + * The available payment sources the customer has (this may be an empty array). + */ +@property(nonatomic, readonly) NSArray> *sources; + +@end + +/** + Use `STPCustomerDeserializer` to convert a response from the Stripe API into an `STPCustomer` object. `STPCustomerDeserializer` expects the JSON response to be in the exact same format as the Stripe API. + */ +@interface STPCustomerDeserializer : NSObject + +/** + * Initialize a customer deserializer. The `data`, `urlResponse`, and `error` parameters are intended to be passed from an `NSURLSessionDataTask` callback. After it has been initialized, you can inspect the `error` and `customer` properties to see if the deserialization was successful. If `error` is nil, `customer` will be non-nil (and vice versa). + * + * @param data An `NSData` object representing encoded JSON for a Customer object + * @param urlResponse The URL response obtained from the `NSURLSessionTask` + * @param error Any error that occurred from the URL session task (if this is non-nil, the `error` property will be set to this value after initialization). + * + */ +- (instancetype)initWithData:(nullable NSData *)data + urlResponse:(nullable NSURLResponse *)urlResponse + error:(nullable NSError *)error; + +/** + * Initializes a customer deserializer with a JSON dictionary. This JSON should be in the exact same format as what the Stripe API returns. If it's successfully parsed, the `customer` parameter will be present after initialization; otherwise `error` will be present. + * + * @param json a JSON dictionary. + * + */ +- (instancetype)initWithJSONResponse:(id)json; + +/** + * If a customer was successfully parsed from the response, it will be set here. Otherwise, this value wil be nil (and the `error` property will explain what went wrong). + */ +@property(nonatomic, readonly, nullable)STPCustomer *customer; + +/** + * If the deserializer failed to parse a customer, this property will explain why (and the `customer` property will be nil). + */ +@property(nonatomic, readonly, nullable)NSError *error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/core-xprojects/Stripe/PublicHeaders/Stripe/STPFormEncodable.h b/core-xprojects/Stripe/PublicHeaders/Stripe/STPFormEncodable.h new file mode 100755 index 0000000000..2ebe38ee5d --- /dev/null +++ b/core-xprojects/Stripe/PublicHeaders/Stripe/STPFormEncodable.h @@ -0,0 +1,35 @@ +// +// STPFormEncodable.h +// Stripe +// +// Created by Jack Flintermann on 10/14/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import + +/** + * Objects conforming to STPFormEncodable can be automatically converted to a form-encoded string, which can then be used when making requests to the Stripe API. + */ +@protocol STPFormEncodable + +/** + * The root object name to be used when converting this object to a form-encoded string. For example, if this returns @"card", then the form-encoded output will resemble @"card[foo]=bar" (where 'foo' and 'bar' are specified by `propertyNamesToFormFieldNamesMapping` below. + */ ++ (nonnull NSString *)rootObjectName; + +/** + * This maps properties on an object that is being form-encoded into parameter names in the Stripe API. For example, STPCardParams has a field called `expMonth`, but the Stripe API expects a field called `exp_month`. This dictionary represents a mapping from the former to the latter (in other words, [STPCardParams propertyNamesToFormFieldNamesMapping][@"expMonth"] == @"exp_month".) + */ ++ (nonnull NSDictionary *)propertyNamesToFormFieldNamesMapping; + +/** + * You can use this property to add additional fields to an API request that are not explicitly defined by the object's interface. This can be useful when using beta features that haven't been added to the Stripe SDK yet. For example, if the /v1/tokens API began to accept a beta field called "test_field", you might do the following: + STPCardParams *cardParams = [STPCardParams new]; + // add card values + cardParams.additionalAPIParameters = @{@"test_field": @"example_value"}; + [[STPAPIClient sharedClient] createTokenWithCard:cardParams completion:...]; + */ +@property(nonatomic, readwrite, nonnull, copy)NSDictionary *additionalAPIParameters; + +@end diff --git a/core-xprojects/Stripe/PublicHeaders/Stripe/STPImageLibrary.h b/core-xprojects/Stripe/PublicHeaders/Stripe/STPImageLibrary.h new file mode 100755 index 0000000000..369523fd02 --- /dev/null +++ b/core-xprojects/Stripe/PublicHeaders/Stripe/STPImageLibrary.h @@ -0,0 +1,79 @@ +// +// STPImages.h +// Stripe +// +// Created by Jack Flintermann on 6/30/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * This class lets you access card icons used by the Stripe SDK. All icons are 32 x 20 points. + */ +@interface STPImageLibrary : NSObject + +/** + * An icon representing Apple Pay. + */ ++ (NSImage *)applePayCardImage; + +/** + * An icon representing American Express. + */ ++ (NSImage *)amexCardImage; + +/** + * An icon representing Diners Club. + */ ++ (NSImage *)dinersClubCardImage; + +/** + * An icon representing Discover. + */ ++ (NSImage *)discoverCardImage; + +/** + * An icon representing JCB. + */ ++ (NSImage *)jcbCardImage; + +/** + * An icon representing MasterCard. + */ ++ (NSImage *)masterCardCardImage; + +/** + * An icon representing Visa. + */ ++ (NSImage *)visaCardImage; + +/** + * An icon to use when the type of the card is unknown. + */ ++ (NSImage *)unknownCardCardImage; + +/** + * This returns the appropriate icon for the specified card brand. + */ ++ (NSImage *)brandImageForCardBrand:(STPCardBrand)brand; + +/** + * This returns the appropriate icon for the specified card brand as a + * single color template that can be tinted + */ ++ (NSImage *)templatedBrandImageForCardBrand:(STPCardBrand)brand; + +/** + * This returns a small icon indicating the CVC location for the given card brand. + */ ++ (NSImage *)cvcImageForCardBrand:(STPCardBrand)brand; + + +@end + +NS_ASSUME_NONNULL_END diff --git a/core-xprojects/Stripe/PublicHeaders/Stripe/STPPaymentConfiguration.h b/core-xprojects/Stripe/PublicHeaders/Stripe/STPPaymentConfiguration.h new file mode 100755 index 0000000000..252b34b57b --- /dev/null +++ b/core-xprojects/Stripe/PublicHeaders/Stripe/STPPaymentConfiguration.h @@ -0,0 +1,65 @@ +// +// STPPaymentConfiguration.h +// Stripe +// +// Created by Jack Flintermann on 5/18/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + + +/** + An `STPPaymentConfiguration` represents all the options you can set or change + around a payment. + + You provide an `STPPaymentConfiguration` object to your `STPPaymentContext` + when making a charge. The configuration generally has settings that + will not change from payment to payment and thus is reusable, while the context + is specific to a single particular payment instance. + */ +@interface STPPaymentConfiguration : NSObject + +/** + This is a convenience singleton configuration that uses the default values + for every property + */ ++ (instancetype)sharedConfiguration; + +/** + * Your Stripe publishable key. You can get this from https://dashboard.stripe.com/account/apikeys . + */ +@property(nonatomic, copy)NSString *publishableKey; + +/** + * An enum value representing which payment methods you will accept from your user in addition to credit cards. Unless you have a very specific reason not to, you should leave this at the default, `STPPaymentMethodTypeAll`. + */ +@property(nonatomic)STPPaymentMethodType additionalPaymentMethods; + +/** + * The billing address fields the user must fill out when prompted for their payment details. These fields will all be present on the returned token from Stripe. See https://stripe.com/docs/api#create_card_token for more information. + */ +@property(nonatomic)STPBillingAddressFields requiredBillingAddressFields; + +/** + * The name of your company, for displaying to the user during payment flows. For example, when using Apple Pay, the payment sheet's final line item will read "PAY {companyName}". This defaults to the name of your iOS application. + */ +@property(nonatomic, copy)NSString *companyName; + +/** + * The Apple Merchant Identifier to use during Apple Pay transactions. To create one of these, see our guide at https://stripe.com/docs/mobile/apple-pay . You must set this to a valid identifier in order to automatically enable Apple Pay. + */ +@property(nonatomic, nullable, copy)NSString *appleMerchantIdentifier; + +/** + * When entering their payment information, users who have a saved card with Stripe will be prompted to autofill it by entering an SMS code. Set this property to `YES` to disable this feature. The user won't receive an SMS code even if they have their payment information stored with Stripe, and won't be prompted to save it if they don't. + */ +@property(nonatomic)BOOL smsAutofillDisabled; + +@end + +NS_ASSUME_NONNULL_END diff --git a/core-xprojects/Stripe/PublicHeaders/Stripe/STPPaymentMethod.h b/core-xprojects/Stripe/PublicHeaders/Stripe/STPPaymentMethod.h new file mode 100755 index 0000000000..2556f53634 --- /dev/null +++ b/core-xprojects/Stripe/PublicHeaders/Stripe/STPPaymentMethod.h @@ -0,0 +1,51 @@ +// +// STPPaymentMethod.h +// Stripe +// +// Created by Ben Guo on 4/19/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import + +/** + * This represents all of the payment methods available to your user (in addition to card payments, which are always enabled) when configuring an `STPPaymentContext`. + */ +typedef NS_OPTIONS(NSUInteger, STPPaymentMethodType) { + + /** + * Don't use any payment methods except for cards. + */ + STPPaymentMethodTypeNone = 0, + + /** + * The user is allowed to pay with Apple Pay (if it's configured and available on their device). + */ + STPPaymentMethodTypeApplePay = 1 << 0, + /** + * The user can use any available payment method to pay. + */ + STPPaymentMethodTypeAll = STPPaymentMethodTypeApplePay +}; + +/** + * This protocol represents a payment method that a user can select and use to pay. Currently the only classes that conform to it are `STPCard` (which represents that the user wants to pay with a specific card) and `STPApplePayPaymentMethod` (which represents that the user wants to pay with Apple Pay). + */ +@protocol STPPaymentMethod + +/** + * A small (32 x 20 points) logo image representing the payment method. For example, the Visa logo for a Visa card, or the Apple Pay logo. + */ +@property (nonatomic, readonly) NSImage *image; + +/** + * A small (32 x 20 points) logo image representing the payment method that can be used as template for tinted icons. + */ +@property (nonatomic, readonly) NSImage *templateImage; + +/** + * A string describing the payment method, such as "Apple Pay" or "Visa 4242". + */ +@property (nonatomic, readonly) NSString *label; + +@end diff --git a/core-xprojects/Stripe/PublicHeaders/Stripe/STPPhoneNumberValidator.h b/core-xprojects/Stripe/PublicHeaders/Stripe/STPPhoneNumberValidator.h new file mode 100755 index 0000000000..9f775178ba --- /dev/null +++ b/core-xprojects/Stripe/PublicHeaders/Stripe/STPPhoneNumberValidator.h @@ -0,0 +1,31 @@ +// +// STPPhoneNumberValidator.h +// Stripe +// +// Created by Jack Flintermann on 10/16/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface STPPhoneNumberValidator : NSObject + ++ (BOOL)stringIsValidPartialPhoneNumber:(NSString *)string; ++ (BOOL)stringIsValidPhoneNumber:(NSString *)string; ++ (BOOL)stringIsValidPartialPhoneNumber:(NSString *)string + forCountryCode:(nullable NSString *)countryCode; ++ (BOOL)stringIsValidPhoneNumber:(NSString *)string + forCountryCode:(nullable NSString *)countryCode; + ++ (NSString *)formattedSanitizedPhoneNumberForString:(NSString *)string; ++ (NSString *)formattedSanitizedPhoneNumberForString:(NSString *)string + forCountryCode:(nullable NSString *)countryCode; ++ (NSString *)formattedRedactedPhoneNumberForString:(NSString *)string; ++ (NSString *)formattedRedactedPhoneNumberForString:(NSString *)string + forCountryCode:(nullable NSString *)countryCode; + +@end + +NS_ASSUME_NONNULL_END diff --git a/core-xprojects/Stripe/PublicHeaders/Stripe/STPPostalCodeValidator.h b/core-xprojects/Stripe/PublicHeaders/Stripe/STPPostalCodeValidator.h new file mode 100755 index 0000000000..fadaa0d36b --- /dev/null +++ b/core-xprojects/Stripe/PublicHeaders/Stripe/STPPostalCodeValidator.h @@ -0,0 +1,26 @@ +// +// STPPostalCodeValidator.h +// Stripe +// +// Created by Ben Guo on 4/14/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import + +typedef NS_ENUM(NSInteger, STPPostalCodeType) { + STPCountryPostalCodeTypeNumericOnly, + STPCountryPostalCodeTypeAlphanumeric, + STPCountryPostalCodeTypeNotRequired, +}; + +@interface STPPostalCodeValidator : NSObject + ++ (BOOL)stringIsValidPostalCode:(nullable NSString *)string + type:(STPPostalCodeType)postalCodeType; ++ (BOOL)stringIsValidPostalCode:(nullable NSString *)string + countryCode:(nullable NSString *)countryCode; + ++ (STPPostalCodeType)postalCodeTypeForCountryCode:(nullable NSString *)countryCode; + +@end diff --git a/core-xprojects/Stripe/PublicHeaders/Stripe/STPSource.h b/core-xprojects/Stripe/PublicHeaders/Stripe/STPSource.h new file mode 100755 index 0000000000..289a3079be --- /dev/null +++ b/core-xprojects/Stripe/PublicHeaders/Stripe/STPSource.h @@ -0,0 +1,22 @@ +// +// STPSource.h +// Stripe +// +// Created by Jack Flintermann on 1/15/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import +#import + +/** + * A source represents a source of funds for your user that you can charge - for example, a card on file. Currently, only `STPCard` implements this interface, although future payment methods will use it as well. When implementing your server backend, you should pass the `stripeID` property to the Create Charge method as the `source` parameter. + */ +@protocol STPSource + +/** + * The stripe ID of the source. When implementing your server backend, you should pass this property to the Create Charge method as the `source` parameter. + */ +@property(nonatomic, readonly, copy, nonnull)NSString *stripeID; + +@end diff --git a/core-xprojects/Stripe/PublicHeaders/Stripe/STPToken.h b/core-xprojects/Stripe/PublicHeaders/Stripe/STPToken.h new file mode 100755 index 0000000000..4f6896d968 --- /dev/null +++ b/core-xprojects/Stripe/PublicHeaders/Stripe/STPToken.h @@ -0,0 +1,53 @@ +// +// STPToken.h +// Stripe +// +// Created by Saikat Chakrabarti on 11/5/12. +// +// + +#import +#import +#import + +@class STPCard; +@class STPBankAccount; + +/** + * A token returned from submitting payment details to the Stripe API. You should not have to instantiate one of these directly. + */ +@interface STPToken : NSObject + +/** + * You cannot directly instantiate an `STPToken`. You should only use one that has been returned from an `STPAPIClient` callback. + */ +- (nonnull instancetype) init; + +/** + * The value of the token. You can store this value on your server and use it to make charges and customers. @see + * https://stripe.com/docs/mobile/ios#sending-tokens + */ +@property (nonatomic, readonly, nonnull) NSString *tokenId; + +/** + * Whether or not this token was created in livemode. Will be YES if you used your Live Publishable Key, and NO if you used your Test Publishable Key. + */ +@property (nonatomic, readonly) BOOL livemode; + +/** + * The credit card details that were used to create the token. Will only be set if the token was created via a credit card or Apple Pay, otherwise it will be + * nil. + */ +@property (nonatomic, readonly, nullable) STPCard *card; + +/** + * The bank account details that were used to create the token. Will only be set if the token was created with a bank account, otherwise it will be nil. + */ +@property (nonatomic, readonly, nullable) STPBankAccount *bankAccount; + +/** + * When the token was created. + */ +@property (nonatomic, readonly, nullable) NSDate *created; + +@end diff --git a/core-xprojects/Stripe/PublicHeaders/Stripe/Stripe.h b/core-xprojects/Stripe/PublicHeaders/Stripe/Stripe.h new file mode 100644 index 0000000000..88da3bb602 --- /dev/null +++ b/core-xprojects/Stripe/PublicHeaders/Stripe/Stripe.h @@ -0,0 +1,23 @@ +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import diff --git a/core-xprojects/Stripe/PublicHeaders/Stripe/StripeError.h b/core-xprojects/Stripe/PublicHeaders/Stripe/StripeError.h new file mode 100755 index 0000000000..9cf9ef99e5 --- /dev/null +++ b/core-xprojects/Stripe/PublicHeaders/Stripe/StripeError.h @@ -0,0 +1,74 @@ +// +// StripeError.h +// Stripe +// +// Created by Saikat Chakrabarti on 11/4/12. +// +// + +#import + +/** + * All Stripe iOS errors will be under this domain. + */ +FOUNDATION_EXPORT NSString * __nonnull const StripeDomain; + +typedef NS_ENUM(NSInteger, STPErrorCode) { + STPConnectionError = 40, // Trouble connecting to Stripe. + STPInvalidRequestError = 50, // Your request had invalid parameters. + STPAPIError = 60, // General-purpose API error (should be rare). + STPCardError = 70, // Something was wrong with the given card (most common). + STPCancellationError = 80, // The operation was cancelled. + STPCheckoutUnknownError = 5000, // Checkout failed + STPCheckoutTooManyAttemptsError = 5001, // Too many incorrect code attempts +}; + +#pragma mark userInfo keys + +// A developer-friendly error message that explains what went wrong. You probably +// shouldn't show this to your users, but might want to use it yourself. +FOUNDATION_EXPORT NSString * __nonnull const STPErrorMessageKey; + +// What went wrong with your STPCard (e.g., STPInvalidCVC. See below for full list). +FOUNDATION_EXPORT NSString * __nonnull const STPCardErrorCodeKey; + +// Which parameter on the STPCard had an error (e.g., "cvc"). Useful for marking up the +// right UI element. +FOUNDATION_EXPORT NSString * __nonnull const STPErrorParameterKey; + +#pragma mark STPCardErrorCodeKeys + +// (Usually determined locally:) +FOUNDATION_EXPORT NSString * __nonnull const STPInvalidNumber; +FOUNDATION_EXPORT NSString * __nonnull const STPInvalidExpMonth; +FOUNDATION_EXPORT NSString * __nonnull const STPInvalidExpYear; +FOUNDATION_EXPORT NSString * __nonnull const STPInvalidCVC; + +// (Usually sent from the server:) +FOUNDATION_EXPORT NSString * __nonnull const STPIncorrectNumber; +FOUNDATION_EXPORT NSString * __nonnull const STPExpiredCard; +FOUNDATION_EXPORT NSString * __nonnull const STPCardDeclined; +FOUNDATION_EXPORT NSString * __nonnull const STPProcessingError; +FOUNDATION_EXPORT NSString * __nonnull const STPIncorrectCVC; + + +@interface NSError(Stripe) + ++ (nullable NSError *)stp_errorFromStripeResponse:(nullable NSDictionary *)jsonDictionary; ++ (nonnull NSError *)stp_genericFailedToParseResponseError; +- (BOOL)stp_isUnknownCheckoutError; +- (BOOL)stp_isURLSessionCancellationError; + +#pragma mark Strings + ++ (nonnull NSString *)stp_cardErrorInvalidNumberUserMessage; ++ (nonnull NSString *)stp_cardInvalidCVCUserMessage; ++ (nonnull NSString *)stp_cardErrorInvalidExpMonthUserMessage; ++ (nonnull NSString *)stp_cardErrorInvalidExpYearUserMessage; ++ (nonnull NSString *)stp_cardErrorExpiredCardUserMessage; ++ (nonnull NSString *)stp_cardErrorDeclinedUserMessage; ++ (nonnull NSString *)stp_cardErrorProcessingErrorUserMessage; ++ (nonnull NSString *)stp_unexpectedErrorMessage; + + +@end diff --git a/core-xprojects/Stripe/Sources/NSDictionary+Stripe.h b/core-xprojects/Stripe/Sources/NSDictionary+Stripe.h new file mode 100755 index 0000000000..89294fd5fd --- /dev/null +++ b/core-xprojects/Stripe/Sources/NSDictionary+Stripe.h @@ -0,0 +1,17 @@ +// +// NSDictionary+Stripe.h +// Stripe +// +// Created by Jack Flintermann on 10/15/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import + +@interface NSDictionary (Stripe) + +- (nullable NSDictionary *)stp_dictionaryByRemovingNullsValidatingRequiredFields:(nonnull NSArray *)requiredFields; + +@end + +void linkNSDictionaryCategory(void); diff --git a/core-xprojects/Stripe/Sources/NSDictionary+Stripe.m b/core-xprojects/Stripe/Sources/NSDictionary+Stripe.m new file mode 100755 index 0000000000..343d8cdc6f --- /dev/null +++ b/core-xprojects/Stripe/Sources/NSDictionary+Stripe.m @@ -0,0 +1,30 @@ +// +// NSDictionary+Stripe.m +// Stripe +// +// Created by Jack Flintermann on 10/15/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import "NSDictionary+Stripe.h" + +@implementation NSDictionary (Stripe) + +- (nullable NSDictionary *)stp_dictionaryByRemovingNullsValidatingRequiredFields:(nonnull NSArray *)requiredFields { + NSMutableDictionary *dict = [NSMutableDictionary dictionary]; + [self enumerateKeysAndObjectsUsingBlock:^(id key, id obj, __unused BOOL *stop) { + if (obj != [NSNull null]) { + dict[key] = obj; + } + }]; + for (NSString *key in requiredFields) { + if (![[dict allKeys] containsObject:key]) { + return nil; + } + } + return [dict copy]; +} + +@end + +void linkNSDictionaryCategory(void){} diff --git a/core-xprojects/Stripe/Sources/NSString+Stripe.h b/core-xprojects/Stripe/Sources/NSString+Stripe.h new file mode 100755 index 0000000000..493a22b311 --- /dev/null +++ b/core-xprojects/Stripe/Sources/NSString+Stripe.h @@ -0,0 +1,19 @@ +// +// NSString+Stripe.h +// Stripe +// +// Created by Jack Flintermann on 10/16/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import + +@interface NSString (Stripe) + +- (NSString *)stp_safeSubstringToIndex:(NSUInteger)index; +- (NSString *)stp_safeSubstringFromIndex:(NSUInteger)index; +- (NSString *)stp_reversedString; + +@end + +void linkNSStringCategory(void); diff --git a/core-xprojects/Stripe/Sources/NSString+Stripe.m b/core-xprojects/Stripe/Sources/NSString+Stripe.m new file mode 100755 index 0000000000..732a7e1181 --- /dev/null +++ b/core-xprojects/Stripe/Sources/NSString+Stripe.m @@ -0,0 +1,33 @@ +// +// NSString+Stripe.m +// Stripe +// +// Created by Jack Flintermann on 10/16/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import "NSString+Stripe.h" + +@implementation NSString (Stripe) + +- (NSString *)stp_safeSubstringToIndex:(NSUInteger)index { + return [self substringToIndex:MIN(self.length, index)]; +} + +- (NSString *)stp_safeSubstringFromIndex:(NSUInteger)index { + return (index > self.length) ? @"" : [self substringFromIndex:index]; +} + +- (NSString *)stp_reversedString { + NSMutableString *mutableReversedString = [NSMutableString stringWithCapacity:self.length]; + [self enumerateSubstringsInRange:NSMakeRange(0, self.length) + options:NSStringEnumerationReverse | NSStringEnumerationByComposedCharacterSequences + usingBlock:^(NSString *substring, __unused NSRange substringRange, __unused NSRange enclosingRange, __unused BOOL *stop) { + [mutableReversedString appendString:substring]; + }]; + return [mutableReversedString copy]; +} + +@end + +void linkNSStringCategory(void){} diff --git a/core-xprojects/Stripe/Sources/NSString+Stripe_CardBrands.h b/core-xprojects/Stripe/Sources/NSString+Stripe_CardBrands.h new file mode 100755 index 0000000000..2fa96ef159 --- /dev/null +++ b/core-xprojects/Stripe/Sources/NSString+Stripe_CardBrands.h @@ -0,0 +1,18 @@ +// +// NSString+Stripe_CardBrands.h +// Stripe +// +// Created by Jack Flintermann on 1/15/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import +#import "STPCardBrand.h" + +@interface NSString (Stripe_CardBrands) + ++ (nonnull instancetype)stp_stringWithCardBrand:(STPCardBrand)brand; + +@end + +void linkNSStringCardBrandsCategory(void); diff --git a/core-xprojects/Stripe/Sources/NSString+Stripe_CardBrands.m b/core-xprojects/Stripe/Sources/NSString+Stripe_CardBrands.m new file mode 100755 index 0000000000..b5961d2a4c --- /dev/null +++ b/core-xprojects/Stripe/Sources/NSString+Stripe_CardBrands.m @@ -0,0 +1,27 @@ +// +// NSString+Stripe_CardBrands.m +// Stripe +// +// Created by Jack Flintermann on 1/15/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import "NSString+Stripe_CardBrands.h" + +@implementation NSString (Stripe_CardBrands) + ++ (nonnull instancetype)stp_stringWithCardBrand:(STPCardBrand)brand { + switch (brand) { + case STPCardBrandAmex: return @"American Express"; + case STPCardBrandDinersClub: return @"Diners Club"; + case STPCardBrandDiscover: return @"Discover"; + case STPCardBrandJCB: return @"JCB"; + case STPCardBrandMasterCard: return @"MasterCard"; + case STPCardBrandUnknown: return @"Unknown"; + case STPCardBrandVisa: return @"Visa"; + } +} + +@end + +void linkNSStringCardBrandsCategory(void){} diff --git a/core-xprojects/Stripe/Sources/PKPayment+Stripe.h b/core-xprojects/Stripe/Sources/PKPayment+Stripe.h new file mode 100755 index 0000000000..3aa35a77be --- /dev/null +++ b/core-xprojects/Stripe/Sources/PKPayment+Stripe.h @@ -0,0 +1,20 @@ +// +// PKPayment+Stripe.h +// Stripe +// +// Created by Ben Guo on 7/2/15. +// + +#import + +@interface PKPayment (Stripe) + +/// Returns true if the instance is a payment from the simulator. +- (BOOL)stp_isSimulated; + +/// Returns a fake transaction identifier with the expected ~-separated format. ++ (NSString *)stp_testTransactionIdentifier; + +@end + +void linkPKPaymentCategory(void); diff --git a/core-xprojects/Stripe/Sources/PKPayment+Stripe.m b/core-xprojects/Stripe/Sources/PKPayment+Stripe.m new file mode 100755 index 0000000000..36120128a4 --- /dev/null +++ b/core-xprojects/Stripe/Sources/PKPayment+Stripe.m @@ -0,0 +1,35 @@ +// +// PKPayment+Stripe.m +// Stripe +// +// Created by Ben Guo on 7/2/15. +// + +#import "PKPayment+Stripe.h" + +@implementation PKPayment (Stripe) + +- (BOOL)stp_isSimulated { + return [self.token.transactionIdentifier isEqualToString:@"Simulated Identifier"]; +} + ++ (NSString *)stp_testTransactionIdentifier { + NSString *uuid = [[NSUUID UUID] UUIDString]; + uuid = [uuid stringByReplacingOccurrencesOfString:@"~" withString:@"" + options:0 + range:NSMakeRange(0, uuid.length)]; + + // Simulated cards don't have enough info yet. For now, use a fake Visa number + NSString *number = @"4242424242424242"; + + // Without the original PKPaymentRequest, we'll need to use fake data here. + NSDecimalNumber *amount = [NSDecimalNumber decimalNumberWithString:@"0"]; + NSString *cents = [@([[amount decimalNumberByMultiplyingByPowerOf10:2] integerValue]) stringValue]; + NSString *currency = @"USD"; + NSString *identifier = [@[@"ApplePayStubs", number, cents, currency, uuid] componentsJoinedByString:@"~"]; + return identifier; +} + +@end + +void linkPKPaymentCategory(void){} diff --git a/core-xprojects/Stripe/Sources/STPAPIClient+Private.h b/core-xprojects/Stripe/Sources/STPAPIClient+Private.h new file mode 100755 index 0000000000..4073f6e8e5 --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPAPIClient+Private.h @@ -0,0 +1,26 @@ +// +// STPAPIClient+Private.h +// Stripe +// +// Created by Jack Flintermann on 10/14/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface STPAPIClient() + +- (instancetype)initWithPublishableKey:(NSString *)publishableKey + baseURL:(NSString *)baseURL; + +- (void)createTokenWithData:(NSData *)data + completion:(STPTokenCompletionBlock)completion; + +@property (nonatomic, readwrite) NSURL *apiURL; +@property (nonatomic, readwrite) NSURLSession *urlSession; + +@end + +NS_ASSUME_NONNULL_END diff --git a/core-xprojects/Stripe/Sources/STPAPIClient.m b/core-xprojects/Stripe/Sources/STPAPIClient.m new file mode 100755 index 0000000000..4b4918d79a --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPAPIClient.m @@ -0,0 +1,302 @@ +// +// STPAPIClient.m +// StripeExample +// +// Created by Jack Flintermann on 12/18/14. +// Copyright (c) 2014 Stripe. All rights reserved. +// + +#import +#import + +#import "STPAPIClient.h" +#import "STPFormEncoder.h" +#import "STPBankAccount.h" +#import "STPCard.h" +#import "STPToken.h" +#import "STPAPIPostRequest.h" +#import "STPPaymentConfiguration.h" +#import "NSString+Stripe_CardBrands.h" + +#if __has_include("Fabric.h") +#import "Fabric+FABKits.h" +#import "FABKitProtocol.h" +#endif + +#ifdef STP_STATIC_LIBRARY_BUILD +#import "STPCategoryLoader.h" +#endif + +#define FAUXPAS_IGNORED_IN_METHOD(...) +FAUXPAS_IGNORED_IN_FILE(APIAvailability) + +static NSString *const apiURLBase = @"api.stripe.com/v1"; +static NSString *const tokenEndpoint = @"tokens"; +static NSString *const stripeAPIVersion = @"2015-10-12"; + +@implementation Stripe + ++ (void)setDefaultPublishableKey:(NSString *)publishableKey { + [STPPaymentConfiguration sharedConfiguration].publishableKey = publishableKey; +} + ++ (NSString *)defaultPublishableKey { + return [STPPaymentConfiguration sharedConfiguration].publishableKey; +} + ++ (void)disableAnalytics { +} + +@end + +#if __has_include("Fabric.h") +@interface STPAPIClient () +#else +@interface STPAPIClient() +#endif +@property (nonatomic, readwrite) NSURL *apiURL; +@property (nonatomic, readwrite) NSURLSession *urlSession; +@end + +@implementation STPAPIClient + ++ (NSString *)stringWithCardBrand:(STPCardBrand)brand { + return [NSString stp_stringWithCardBrand:brand]; +} + ++ (void)initialize { +#ifdef STP_STATIC_LIBRARY_BUILD + [STPCategoryLoader loadCategories]; +#endif +} + ++ (instancetype)sharedClient { + static id sharedClient; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ sharedClient = [[self alloc] init]; }); + return sharedClient; +} + +- (instancetype)init { + return [self initWithConfiguration:[STPPaymentConfiguration sharedConfiguration]]; +} + +- (instancetype)initWithPublishableKey:(NSString *)publishableKey { + STPPaymentConfiguration *config = [[STPPaymentConfiguration alloc] init]; + config.publishableKey = [publishableKey copy]; + [self.class validateKey:publishableKey]; + return [self initWithConfiguration:config]; +} + +- (instancetype)initWithConfiguration:(STPPaymentConfiguration *)configuration { + self = [super init]; + if (self) { + _apiURL = [NSURL URLWithString:[NSString stringWithFormat:@"https://%@", apiURLBase]]; + _configuration = configuration; + NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; + NSString *auth = [@"Bearer " stringByAppendingString:self.publishableKey]; + sessionConfiguration.HTTPAdditionalHeaders = @{ + @"X-Stripe-User-Agent": [self.class stripeUserAgentDetails], + @"Stripe-Version": stripeAPIVersion, + @"Authorization": auth, + }; + _urlSession = [NSURLSession sessionWithConfiguration:sessionConfiguration]; + + } + return self; +} + +- (instancetype)initWithPublishableKey:(NSString *)publishableKey + baseURL:(NSString *)baseURL { + self = [self initWithPublishableKey:publishableKey]; + if (self) { + _apiURL = [NSURL URLWithString:baseURL]; + } + return self; +} + +- (void)setPublishableKey:(NSString *)publishableKey { + self.configuration.publishableKey = [publishableKey copy]; +} + +- (NSString *)publishableKey { + return self.configuration.publishableKey; +} + +- (void)createTokenWithData:(NSData *)data + completion:(STPTokenCompletionBlock)completion { + NSCAssert(data != nil, @"'data' is required to create a token"); + NSCAssert(completion != nil, @"'completion' is required to use the token that is created"); + [STPAPIPostRequest startWithAPIClient:self + endpoint:tokenEndpoint + postData:data + serializer:[[STPToken alloc] init] + completion:^(STPToken *object, NSHTTPURLResponse *response, NSError *error) { + completion(object, error); + }]; +} + +- (void)createTokenWithCard:(STPCard *)card completion:(STPTokenCompletionBlock)completion { + NSData *data = [STPFormEncoder formEncodedDataForObject:card]; + [self createTokenWithData:data completion:completion]; +} + +#pragma mark - private helpers + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-variable" ++ (void)validateKey:(NSString *)publishableKey { + NSCAssert(publishableKey != nil && ![publishableKey isEqualToString:@""], + @"You must use a valid publishable key to create a token. For more info, see https://stripe.com/docs/stripe.js"); + BOOL secretKey = [publishableKey hasPrefix:@"sk_"]; + NSCAssert(!secretKey, + @"You are using a secret key to create a token, instead of the publishable one. For more info, see https://stripe.com/docs/stripe.js"); +#ifndef DEBUG + if ([publishableKey.lowercaseString hasPrefix:@"pk_test"]) { + FAUXPAS_IGNORED_IN_METHOD(NSLogUsed); + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSLog(@"ℹ️ You're using your Stripe testmode key. Make sure to use your livemode key when submitting to the App Store!"); + }); + } +#endif +} +#pragma clang diagnostic pop + +#pragma mark Utility methods - + ++ (NSString *)stripeUserAgentDetails { + NSMutableDictionary *details = [@{ + @"lang": @"objective-c", + @"bindings_version": STPSDKVersion, + } mutableCopy]; + NSString *version = [NSProcessInfo processInfo].operatingSystemVersionString; + if (version) { + details[@"os_version"] = version; + } + struct utsname systemInfo; + uname(&systemInfo); + NSString *deviceType = @(systemInfo.machine); + if (deviceType) { + details[@"type"] = deviceType; + } + return [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:[details copy] options:0 error:NULL] encoding:NSUTF8StringEncoding]; +} + +#pragma mark Fabric +#if __has_include("Fabric.h") + ++ (NSString *)bundleIdentifier { + return @"com.stripe.stripe-ios"; +} + ++ (NSString *)kitDisplayVersion { + return STPSDKVersion; +} + ++ (void)initializeIfNeeded { + Class fabric = NSClassFromString(@"Fabric"); + if (fabric) { + // The app must be using Fabric, as it exists at runtime. We fetch our default publishable key from Fabric. + NSDictionary *fabricConfiguration = [fabric configurationDictionaryForKitClass:[STPAPIClient class]]; + NSString *publishableKey = fabricConfiguration[@"publishable"]; + if (!publishableKey) { + NSLog(@"Configuration dictionary returned by Fabric was nil, or doesn't have publishableKey. Can't initialize Stripe."); + return; + } + [self validateKey:publishableKey]; + [Stripe setDefaultPublishableKey:publishableKey]; + } else { + NSCAssert(fabric, @"initializeIfNeeded method called from a project that doesn't have Fabric."); + } +} + +#endif + +@end + +#pragma mark - Bank Accounts +@implementation STPAPIClient (BankAccounts) + +- (void)createTokenWithBankAccount:(STPBankAccountParams *)bankAccount + completion:(STPTokenCompletionBlock)completion { + NSData *data = [STPFormEncoder formEncodedDataForObject:bankAccount]; + [self createTokenWithData:data completion:completion]; +} + +@end + +#pragma mark - Credit Cards + +@implementation Stripe (ApplePay) + ++ (BOOL)canSubmitPaymentRequest:(PKPaymentRequest *)paymentRequest { + if (![self deviceSupportsApplePay]) { + return NO; + } + if (paymentRequest == nil) { + return NO; + } + if (paymentRequest.merchantIdentifier == nil) { + return NO; + } + return [[[paymentRequest.paymentSummaryItems lastObject] amount] floatValue] > 0; +} + ++ (NSArray *)supportedPKPaymentNetworks { + NSArray *supportedNetworks = @[PKPaymentNetworkAmex, PKPaymentNetworkMasterCard, PKPaymentNetworkVisa]; + if ((&PKPaymentNetworkDiscover) != NULL) { + supportedNetworks = [supportedNetworks arrayByAddingObject:PKPaymentNetworkDiscover]; + } + return supportedNetworks; +} + ++ (BOOL)deviceSupportsApplePay { + return [PKPaymentAuthorizationViewController class] && [PKPaymentAuthorizationViewController canMakePaymentsUsingNetworks:[self supportedPKPaymentNetworks]]; +} + ++ (PKPaymentRequest *)paymentRequestWithMerchantIdentifier:(NSString *)merchantIdentifier { + if (![PKPaymentRequest class]) { + return nil; + } + PKPaymentRequest *paymentRequest = [PKPaymentRequest new]; + [paymentRequest setMerchantIdentifier:merchantIdentifier]; + [paymentRequest setSupportedNetworks:[self supportedPKPaymentNetworks]]; + [paymentRequest setMerchantCapabilities:PKMerchantCapability3DS]; + [paymentRequest setCountryCode:@"US"]; + [paymentRequest setCurrencyCode:@"USD"]; + return paymentRequest; +} + +@end + +@implementation Stripe (Deprecated) + ++ (id)alloc { + NSCAssert(NO, @"'Stripe' is a static class and cannot be instantiated."); + return nil; +} + +#pragma mark Shorthand methods - + ++ (void)createTokenWithCard:(STPCard *)card completion:(STPCompletionBlock)handler { + [[STPAPIClient sharedClient] createTokenWithCard:card completion:handler]; +} + ++ (void)createTokenWithCard:(STPCard *)card publishableKey:(NSString *)publishableKey completion:(STPCompletionBlock)handler { + STPPaymentConfiguration *config = [STPPaymentConfiguration new]; + config.publishableKey = publishableKey; + [[[STPAPIClient alloc] initWithConfiguration:config] createTokenWithCard:card completion:handler]; +} + ++ (void)createTokenWithBankAccount:(STPBankAccount *)bankAccount completion:(STPCompletionBlock)handler { + [[STPAPIClient sharedClient] createTokenWithBankAccount:bankAccount completion:handler]; +} + ++ (void)createTokenWithBankAccount:(STPBankAccount *)bankAccount publishableKey:(NSString *)publishableKey completion:(STPCompletionBlock)handler { + STPPaymentConfiguration *config = [STPPaymentConfiguration new]; + config.publishableKey = publishableKey; + [[[STPAPIClient alloc] initWithConfiguration:config] createTokenWithBankAccount:bankAccount completion:handler]; +} + +@end diff --git a/core-xprojects/Stripe/Sources/STPAPIPostRequest.h b/core-xprojects/Stripe/Sources/STPAPIPostRequest.h new file mode 100755 index 0000000000..49ebb210d3 --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPAPIPostRequest.h @@ -0,0 +1,23 @@ +// +// STPAPIPostRequest.h +// Stripe +// +// Created by Jack Flintermann on 10/14/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import +#import "STPAPIResponseDecodable.h" +@class STPAPIClient; + +@interface STPAPIPostRequest<__covariant ResponseType:id> : NSObject + +typedef void(^STPAPIPostResponseBlock)(ResponseType object, NSHTTPURLResponse *response, NSError *error); + ++ (void)startWithAPIClient:(STPAPIClient *)apiClient + endpoint:(NSString *)endpoint + postData:(NSData *)postData + serializer:(ResponseType)serializer + completion:(STPAPIPostResponseBlock)completion; + +@end diff --git a/core-xprojects/Stripe/Sources/STPAPIPostRequest.m b/core-xprojects/Stripe/Sources/STPAPIPostRequest.m new file mode 100755 index 0000000000..733d1283a9 --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPAPIPostRequest.m @@ -0,0 +1,51 @@ +// +// STPAPIPostRequest.m +// Stripe +// +// Created by Jack Flintermann on 10/14/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import "STPAPIPostRequest.h" +#import "STPAPIClient.h" +#import "STPAPIClient+Private.h" +#import "StripeError.h" +#import "STPDispatchFunctions.h" + +@implementation STPAPIPostRequest + ++ (void)startWithAPIClient:(STPAPIClient *)apiClient + endpoint:(NSString *)endpoint + postData:(NSData *)postData + serializer:(id)serializer + completion:(STPAPIPostResponseBlock)completion { + + NSURL *url = [apiClient.apiURL URLByAppendingPathComponent:endpoint]; + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; + request.HTTPMethod = @"POST"; + request.HTTPBody = postData; + + [[apiClient.urlSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable body, NSURLResponse * _Nullable response, NSError * _Nullable error) { + NSDictionary *jsonDictionary = body ? [NSJSONSerialization JSONObjectWithData:body options:0 error:NULL] : nil; + id responseObject = [[serializer class] decodedObjectFromAPIResponse:jsonDictionary]; + NSError *returnedError = [NSError stp_errorFromStripeResponse:jsonDictionary] ?: error; + if ((!responseObject || ![response isKindOfClass:[NSHTTPURLResponse class]]) && !returnedError) { + returnedError = [NSError stp_genericFailedToParseResponseError]; + } + + NSHTTPURLResponse *httpResponse; + if ([response isKindOfClass:[NSHTTPURLResponse class]]) { + httpResponse = (NSHTTPURLResponse *)response; + } + stpDispatchToMainThreadIfNecessary(^{ + if (returnedError) { + completion(nil, httpResponse, returnedError); + } else { + completion(responseObject, httpResponse, nil); + } + }); + }] resume]; + +} + +@end diff --git a/core-xprojects/Stripe/Sources/STPAddress.m b/core-xprojects/Stripe/Sources/STPAddress.m new file mode 100755 index 0000000000..baff355640 --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPAddress.m @@ -0,0 +1,41 @@ +// +// STPAddress.m +// Stripe +// +// Created by Ben Guo on 4/13/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import "STPAddress.h" +#import "STPCardValidator.h" +#import "STPPostalCodeValidator.h" + +@implementation STPAddress + + +- (BOOL)containsRequiredFields:(STPBillingAddressFields)requiredFields { + BOOL containsFields = YES; + switch (requiredFields) { + case STPBillingAddressFieldsNone: + return YES; + case STPBillingAddressFieldsZip: + return [STPPostalCodeValidator stringIsValidPostalCode:self.postalCode + countryCode:self.country]; + case STPBillingAddressFieldsFull: + return [self hasValidPostalAddress]; + } + return containsFields; +} + +- (BOOL)hasValidPostalAddress { + return (self.line1.length > 0 + && self.city.length > 0 + && self.country.length > 0 + && (self.state.length > 0 || ![self.country isEqualToString:@"US"]) + && [STPPostalCodeValidator stringIsValidPostalCode:self.postalCode + countryCode:self.country]); +} + + +@end + diff --git a/core-xprojects/Stripe/Sources/STPBINRange.m b/core-xprojects/Stripe/Sources/STPBINRange.m new file mode 100755 index 0000000000..9998b79178 --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPBINRange.m @@ -0,0 +1,114 @@ +// +// STPBINRange.m +// Stripe +// +// Created by Jack Flintermann on 5/24/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import "STPBINRange.h" +#import "NSString+Stripe.h" + +@interface STPBINRange() + +@property(nonatomic)NSUInteger length; +@property(nonatomic)NSString *qRangeLow; +@property(nonatomic)NSString *qRangeHigh; +@property(nonatomic)STPCardBrand brand; + +- (BOOL)matchesNumber:(NSString *)number; + +@end + + +@implementation STPBINRange + ++ (NSArray *)allRanges { + + static NSArray *STPBINRangeAllRanges; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSArray *ranges = @[ + // Catch-all values + @[@"", @"", @16, @(STPCardBrandUnknown)], + @[@"34", @"34", @15, @(STPCardBrandAmex)], + @[@"37", @"37", @15, @(STPCardBrandAmex)], + @[@"30", @"30", @14, @(STPCardBrandDinersClub)], + @[@"36", @"36", @14, @(STPCardBrandDinersClub)], + @[@"38", @"39", @14, @(STPCardBrandDinersClub)], + @[@"6011", @"6011", @16, @(STPCardBrandDiscover)], + @[@"622", @"622", @16, @(STPCardBrandDiscover)], + @[@"64", @"65", @16, @(STPCardBrandDiscover)], + @[@"35", @"35", @16, @(STPCardBrandJCB)], + @[@"5", @"5", @16, @(STPCardBrandMasterCard)], + @[@"4", @"4", @16, @(STPCardBrandVisa)], + // Specific known BIN ranges + @[@"222100", @"272099", @16, @(STPCardBrandMasterCard)], + + @[@"413600", @"413600", @13, @(STPCardBrandVisa)], + @[@"444509", @"444509", @13, @(STPCardBrandVisa)], + @[@"444509", @"444509", @13, @(STPCardBrandVisa)], + @[@"444550", @"444550", @13, @(STPCardBrandVisa)], + @[@"450603", @"450603", @13, @(STPCardBrandVisa)], + @[@"450617", @"450617", @13, @(STPCardBrandVisa)], + @[@"450628", @"450629", @13, @(STPCardBrandVisa)], + @[@"450636", @"450636", @13, @(STPCardBrandVisa)], + @[@"450640", @"450641", @13, @(STPCardBrandVisa)], + @[@"450662", @"450662", @13, @(STPCardBrandVisa)], + @[@"463100", @"463100", @13, @(STPCardBrandVisa)], + @[@"476142", @"476142", @13, @(STPCardBrandVisa)], + @[@"476143", @"476143", @13, @(STPCardBrandVisa)], + @[@"492901", @"492902", @13, @(STPCardBrandVisa)], + @[@"492920", @"492920", @13, @(STPCardBrandVisa)], + @[@"492923", @"492923", @13, @(STPCardBrandVisa)], + @[@"492928", @"492930", @13, @(STPCardBrandVisa)], + @[@"492937", @"492937", @13, @(STPCardBrandVisa)], + @[@"492939", @"492939", @13, @(STPCardBrandVisa)], + @[@"492960", @"492960", @13, @(STPCardBrandVisa)], + ]; + NSMutableArray *binRanges = [NSMutableArray array]; + for (NSArray *range in ranges) { + STPBINRange *binRange = [self.class new]; + binRange.qRangeLow = range[0]; + binRange.qRangeHigh = range[1]; + binRange.length = [range[2] unsignedIntegerValue]; + binRange.brand = [range[3] integerValue]; + [binRanges addObject:binRange]; + } + STPBINRangeAllRanges = [binRanges copy]; + }); + return STPBINRangeAllRanges; +} + +- (BOOL)matchesNumber:(NSString *)number { + NSString *low = [number stringByPaddingToLength:self.qRangeLow.length withString:@"0" startingAtIndex:0]; + NSString *high = [number stringByPaddingToLength:self.qRangeHigh.length withString:@"0" startingAtIndex:0]; + + return self.qRangeLow.integerValue <= low.integerValue && self.qRangeHigh.integerValue >= high.integerValue; +} + +- (NSComparisonResult)compare:(STPBINRange *)other { + return [@(self.qRangeLow.length) compare:@(other.qRangeLow.length)]; +} + ++ (NSArray *)binRangesForNumber:(NSString *)number { + return [[self allRanges] filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(STPBINRange *range, __unused NSDictionary *bindings) { + return [range matchesNumber:number]; + }]]; +} + ++ (instancetype)mostSpecificBINRangeForNumber:(NSString *)number { + NSArray *validRanges = [[self allRanges] filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(STPBINRange *range, __unused NSDictionary *bindings) { + return [range matchesNumber:number]; + }]]; + return [[validRanges sortedArrayUsingSelector:@selector(compare:)] lastObject]; +} + ++ (NSArray *)binRangesForBrand:(STPCardBrand)brand { + return [[self allRanges] filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(STPBINRange *range, __unused NSDictionary *bindings) { + return range.brand == brand; + }]]; +} + +@end diff --git a/core-xprojects/Stripe/Sources/STPBankAccount.m b/core-xprojects/Stripe/Sources/STPBankAccount.m new file mode 100755 index 0000000000..565a244844 --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPBankAccount.m @@ -0,0 +1,113 @@ +// +// STPBankAccount.m +// Stripe +// +// Created by Charles Scalesse on 10/1/14. +// +// + +#import "STPBankAccount.h" +#import "NSDictionary+Stripe.h" + +@interface STPBankAccount () + +@property (nonatomic, readwrite) NSString *bankAccountId; +@property (nonatomic, readwrite) NSString *last4; +@property (nonatomic, readwrite) NSString *bankName; +@property (nonatomic, readwrite) NSString *fingerprint; +@property (nonatomic) STPBankAccountStatus status; +@property (nonatomic, readwrite, nonnull, copy) NSDictionary *allResponseFields; + +@end + +@implementation STPBankAccount + +@synthesize routingNumber, country, currency, accountHolderName, accountHolderType; + +- (void)setAccountNumber:(NSString *)accountNumber { + [super setAccountNumber:accountNumber]; +} + +- (NSString *)last4 { + return _last4 ?: [super last4]; +} + +#pragma mark - Equality + +- (BOOL)isEqual:(STPBankAccount *)bankAccount { + return [self isEqualToBankAccount:bankAccount]; +} + +- (NSUInteger)hash { + return [self.bankAccountId hash]; +} + +- (BOOL)isEqualToBankAccount:(STPBankAccount *)bankAccount { + if (self == bankAccount) { + return YES; + } + + if (!bankAccount || ![bankAccount isKindOfClass:self.class]) { + return NO; + } + + return [self.bankAccountId isEqualToString:bankAccount.bankAccountId]; +} + +- (BOOL)validated { + return self.status == STPBankAccountStatusValidated; +} + +- (BOOL)disabled { + return self.status == STPBankAccountStatusErrored; +} + +#pragma mark STPAPIResponseDecodable + ++ (NSArray *)requiredFields { + return @[ + @"id", + @"last4", + @"bank_name", + @"country", + @"currency", + @"status", + ]; +} + ++ (instancetype)decodedObjectFromAPIResponse:(NSDictionary *)response { + NSDictionary *dict = [response stp_dictionaryByRemovingNullsValidatingRequiredFields:[self requiredFields]]; + if (!dict) { + return nil; + } + + STPBankAccount *bankAccount = [self new]; + bankAccount.bankAccountId = dict[@"id"]; + bankAccount.last4 = dict[@"last4"]; + bankAccount.bankName = dict[@"bank_name"]; + bankAccount.country = dict[@"country"]; + bankAccount.fingerprint = dict[@"fingerprint"]; + bankAccount.currency = dict[@"currency"]; + bankAccount.accountHolderName = dict[@"account_holder_name"]; + NSString *accountHolderType = dict[@"account_holder_type"]; + if ([accountHolderType isEqualToString:@"individual"]) { + bankAccount.accountHolderType = STPBankAccountHolderTypeIndividual; + } else if ([accountHolderType isEqualToString:@"company"]) { + bankAccount.accountHolderType = STPBankAccountHolderTypeCompany; + } + NSString *status = dict[@"status"]; + if ([status isEqual: @"new"]) { + bankAccount.status = STPBankAccountStatusNew; + } else if ([status isEqual: @"validated"]) { + bankAccount.status = STPBankAccountStatusValidated; + } else if ([status isEqual: @"verified"]) { + bankAccount.status = STPBankAccountStatusVerified; + } else if ([status isEqual: @"errored"]) { + bankAccount.status = STPBankAccountStatusErrored; + } + + bankAccount.allResponseFields = dict; + return bankAccount; +} + +@end diff --git a/core-xprojects/Stripe/Sources/STPBankAccountParams.m b/core-xprojects/Stripe/Sources/STPBankAccountParams.m new file mode 100755 index 0000000000..2be5fc537e --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPBankAccountParams.m @@ -0,0 +1,61 @@ +// +// STPBankAccountParams.m +// Stripe +// +// Created by Jack Flintermann on 10/4/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import "STPBankAccountParams.h" +#define FAUXPAS_IGNORED_ON_LINE(...) + +@interface STPBankAccountParams() +@property(nonatomic, readonly)NSString *accountHolderTypeString; +@end + +@implementation STPBankAccountParams + +@synthesize additionalAPIParameters = _additionalAPIParameters; + +- (instancetype)init { + self = [super init]; + if (self) { + _additionalAPIParameters = @{}; + _accountHolderType = STPBankAccountHolderTypeIndividual; + } + return self; +} + +- (NSString *)last4 { + if (self.accountNumber && self.accountNumber.length >= 4) { + return [self.accountNumber substringFromIndex:(self.accountNumber.length - 4)]; + } else { + return nil; + } +} + +- (NSString *)accountHolderTypeString { FAUXPAS_IGNORED_ON_LINE(UnusedMethod) + switch (self.accountHolderType) { + case STPBankAccountHolderTypeCompany: + return @"company"; + case STPBankAccountHolderTypeIndividual: + return @"individual"; + } +} + ++ (NSString *)rootObjectName { + return @"bank_account"; +} + ++ (NSDictionary *)propertyNamesToFormFieldNamesMapping { + return @{ + @"accountNumber": @"account_number", + @"routingNumber": @"routing_number", + @"country": @"country", + @"currency": @"currency", + @"accountHolderName": @"account_holder_name", + @"accountHolderTypeString": @"account_holder_type", + }; +} + +@end diff --git a/core-xprojects/Stripe/Sources/STPCard.m b/core-xprojects/Stripe/Sources/STPCard.m new file mode 100755 index 0000000000..4a44fd8bf9 --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPCard.m @@ -0,0 +1,200 @@ +// +// STPCard.m +// Stripe +// +// Created by Saikat Chakrabarti on 11/2/12. +// +// + +#import "STPCard.h" +#import "NSDictionary+Stripe.h" +#import "NSString+Stripe_CardBrands.h" +#import "STPImageLibrary.h" +#import "STPImageLibrary+Private.h" + +@interface STPCard () + +@property (nonatomic, readwrite) NSString *cardId; +@property (nonatomic, readwrite) NSString *last4; +@property (nonatomic, readwrite) NSString *dynamicLast4; +@property (nonatomic, readwrite) STPCardBrand brand; +@property (nonatomic, readwrite) STPCardFundingType funding; +@property (nonatomic, readwrite) NSString *fingerprint; +@property (nonatomic, readwrite) NSString *country; +@property (nonatomic, readwrite, nonnull, copy) NSDictionary *allResponseFields; + +@end + +@implementation STPCard + +@dynamic number, cvc, expMonth, expYear, currency, name, addressLine1, addressLine2, addressCity, addressState, addressZip, addressCountry; + +- (instancetype)initWithID:(NSString *)stripeID + brand:(STPCardBrand)brand + last4:(NSString *)last4 + expMonth:(NSUInteger)expMonth + expYear:(NSUInteger)expYear + funding:(STPCardFundingType)funding { + self = [super init]; + if (self) { + _cardId = stripeID; + _brand = brand; + _last4 = last4; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated" + self.expMonth = expMonth; + self.expYear = expYear; +#pragma clang diagnostic pop + _funding = funding; + } + return self; +} + ++ (STPCardBrand)brandFromString:(NSString *)string { + NSString *brand = [string lowercaseString]; + if ([brand isEqualToString:@"visa"]) { + return STPCardBrandVisa; + } else if ([brand isEqualToString:@"american express"]) { + return STPCardBrandAmex; + } else if ([brand isEqualToString:@"mastercard"]) { + return STPCardBrandMasterCard; + } else if ([brand isEqualToString:@"discover"]) { + return STPCardBrandDiscover; + } else if ([brand isEqualToString:@"jcb"]) { + return STPCardBrandJCB; + } else if ([brand isEqualToString:@"diners club"]) { + return STPCardBrandDinersClub; + } else { + return STPCardBrandUnknown; + } +} + ++ (STPCardFundingType)fundingFromString:(NSString *)string { + NSString *funding = [string lowercaseString]; + if ([funding isEqualToString:@"credit"]) { + return STPCardFundingTypeCredit; + } else if ([funding isEqualToString:@"debit"]) { + return STPCardFundingTypeDebit; + } else if ([funding isEqualToString:@"prepaid"]) { + return STPCardFundingTypePrepaid; + } else { + return STPCardFundingTypeOther; + } +} + +- (instancetype)init { + self = [super init]; + if (self) { + _brand = STPCardBrandUnknown; + _funding = STPCardFundingTypeOther; + } + + return self; +} + +- (NSString *)last4 { + return _last4 ?: [super last4]; +} + +- (BOOL)isApplePayCard { + return [self.allResponseFields[@"tokenization_method"] isEqualToString:@"apple_pay"]; +} + +- (NSString *)type { + switch (self.brand) { + case STPCardBrandAmex: + return @"American Express"; + case STPCardBrandDinersClub: + return @"Diners Club"; + case STPCardBrandDiscover: + return @"Discover"; + case STPCardBrandJCB: + return @"JCB"; + case STPCardBrandMasterCard: + return @"MasterCard"; + case STPCardBrandVisa: + return @"Visa"; + default: + return @"Unknown"; + } +} + +- (BOOL)isEqual:(id)other { + return [self isEqualToCard:other]; +} + +- (NSUInteger)hash { + return [self.cardId hash]; +} + +- (BOOL)isEqualToCard:(STPCard *)other { + if (self == other) { + return YES; + } + + if (!other || ![other isKindOfClass:self.class]) { + return NO; + } + + return [self.cardId isEqualToString:other.cardId]; +} + +#pragma mark STPAPIResponseDecodable ++ (NSArray *)requiredFields { + return @[@"id", @"last4", @"brand", @"exp_month", @"exp_year"]; +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated" ++ (instancetype)decodedObjectFromAPIResponse:(NSDictionary *)response { + NSDictionary *dict = [response stp_dictionaryByRemovingNullsValidatingRequiredFields:[self requiredFields]]; + if (!dict) { + return nil; + } + + STPCard *card = [self new]; + card.cardId = dict[@"id"]; + card.name = dict[@"name"]; + card.last4 = dict[@"last4"]; + card.dynamicLast4 = dict[@"dynamic_last4"]; + NSString *brand = [dict[@"brand"] lowercaseString]; + card.brand = [self.class brandFromString:brand]; + NSString *funding = dict[@"funding"]; + card.funding = [self.class fundingFromString:funding]; + card.fingerprint = dict[@"fingerprint"]; + card.country = dict[@"country"]; + card.currency = dict[@"currency"]; + card.expMonth = [dict[@"exp_month"] intValue]; + card.expYear = [dict[@"exp_year"] intValue]; + card.addressLine1 = dict[@"address_line1"]; + card.addressLine2 = dict[@"address_line2"]; + card.addressCity = dict[@"address_city"]; + card.addressState = dict[@"address_state"]; + card.addressZip = dict[@"address_zip"]; + card.addressCountry = dict[@"address_country"]; + + card.allResponseFields = dict; + return card; +} +#pragma clang diagnostic pop + +#pragma mark - STPSource + +- (NSString *)stripeID { + return self.cardId; +} + +- (NSString *)label { + NSString *brand = [NSString stp_stringWithCardBrand:self.brand]; + return [NSString stringWithFormat:@"%@ %@", brand, self.last4]; +} + +- (NSImage *)image { + return [STPImageLibrary brandImageForCardBrand:self.brand]; +} + +- (NSImage *)templateImage { + return [STPImageLibrary templatedBrandImageForCardBrand:self.brand]; +} + +@end diff --git a/core-xprojects/Stripe/Sources/STPCardParams.m b/core-xprojects/Stripe/Sources/STPCardParams.m new file mode 100755 index 0000000000..329271d0b3 --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPCardParams.m @@ -0,0 +1,199 @@ +// +// STPCardParams.m +// Stripe +// +// Created by Jack Flintermann on 10/4/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import "STPCardParams.h" +#import "STPCardValidator.h" +#import "StripeError.h" + +@implementation STPCardParams + +@synthesize additionalAPIParameters = _additionalAPIParameters; + +- (instancetype)init { + self = [super init]; + if (self) { + _additionalAPIParameters = @{}; + } + return self; +} + +- (NSString *)last4 { + if (self.number && self.number.length >= 4) { + return [self.number substringFromIndex:(self.number.length - 4)]; + } else { + return nil; + } +} + +#if TARGET_OS_IPHONE + +- (STPAddress *)address { + STPAddress *address = [STPAddress new]; + address.name = self.name; + address.line1 = self.addressLine1; + address.line2 = self.addressLine2; + address.city = self.addressCity; + address.state = self.addressState; + address.postalCode = self.addressZip; + address.country = self.addressCountry; + return address; +} + +- (void)setAddress:(STPAddress *)address { + self.name = address.name; + self.addressLine1 = address.line1; + self.addressLine2 = address.line2; + self.addressCity = address.city; + self.addressState = address.state; + self.addressZip = address.postalCode; + self.addressCountry = address.country; +} + +#endif + +- (BOOL)validateNumber:(id *)ioValue error:(NSError **)outError { + if (*ioValue == nil) { + return [self.class handleValidationErrorForParameter:@"number" error:outError]; + } + NSString *ioValueString = (NSString *)*ioValue; + + if ([STPCardValidator validationStateForNumber:ioValueString validatingCardBrand:NO] != STPCardValidationStateValid) { + return [self.class handleValidationErrorForParameter:@"number" error:outError]; + } + return YES; +} + +- (BOOL)validateCvc:(id *)ioValue error:(NSError **)outError { + if (*ioValue == nil) { + return [self.class handleValidationErrorForParameter:@"number" error:outError]; + } + NSString *ioValueString = (NSString *)*ioValue; + + STPCardBrand brand = [STPCardValidator brandForNumber:self.number]; + + if ([STPCardValidator validationStateForCVC:ioValueString cardBrand:brand] != STPCardValidationStateValid) { + return [self.class handleValidationErrorForParameter:@"cvc" error:outError]; + } + return YES; +} + +- (BOOL)validateExpMonth:(id *)ioValue error:(NSError **)outError { + if (*ioValue == nil) { + return [self.class handleValidationErrorForParameter:@"expMonth" error:outError]; + } + NSString *ioValueString = [(NSString *)*ioValue stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + + if ([STPCardValidator validationStateForExpirationMonth:ioValueString] != STPCardValidationStateValid) { + return [self.class handleValidationErrorForParameter:@"expMonth" error:outError]; + } + return YES; +} + +- (BOOL)validateExpYear:(id *)ioValue error:(NSError **)outError { + if (*ioValue == nil) { + return [self.class handleValidationErrorForParameter:@"expYear" error:outError]; + } + NSString *ioValueString = [(NSString *)*ioValue stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + + NSString *monthString = [@(self.expMonth) stringValue]; + if ([STPCardValidator validationStateForExpirationYear:ioValueString inMonth:monthString] != STPCardValidationStateValid) { + return [self.class handleValidationErrorForParameter:@"expYear" error:outError]; + } + return YES; +} + +- (BOOL)validateCardReturningError:(NSError **)outError { + // Order matters here + NSString *numberRef = [self number]; + NSString *expMonthRef = [NSString stringWithFormat:@"%02lu", (unsigned long)[self expMonth]]; + NSString *expYearRef = [NSString stringWithFormat:@"%02lu", (unsigned long)[self expYear]]; + NSString *cvcRef = [self cvc]; + + // Make sure expMonth, expYear, and number are set. Validate CVC if it is provided + return [self validateNumber:&numberRef error:outError] && [self validateExpYear:&expYearRef error:outError] && + [self validateExpMonth:&expMonthRef error:outError] && (cvcRef == nil || [self validateCvc:&cvcRef error:outError]); +} + +#pragma mark Private Helpers ++ (BOOL)handleValidationErrorForParameter:(NSString *)parameter error:(NSError **)outError { + if (outError != nil) { + if ([parameter isEqualToString:@"number"]) { + *outError = [self createErrorWithMessage:[NSError stp_cardErrorInvalidNumberUserMessage] + parameter:parameter + cardErrorCode:STPInvalidNumber + devErrorMessage:@"Card number must be between 10 and 19 digits long and Luhn valid."]; + } else if ([parameter isEqualToString:@"cvc"]) { + *outError = [self createErrorWithMessage:[NSError stp_cardInvalidCVCUserMessage] + parameter:parameter + cardErrorCode:STPInvalidCVC + devErrorMessage:@"Card CVC must be numeric, 3 digits for Visa, Discover, MasterCard, JCB, and Discover cards, and 3 or 4 " + @"digits for American Express cards."]; + } else if ([parameter isEqualToString:@"expMonth"]) { + *outError = [self createErrorWithMessage:[NSError stp_cardErrorInvalidExpMonthUserMessage] + parameter:parameter + cardErrorCode:STPInvalidExpMonth + devErrorMessage:@"expMonth must be less than 13"]; + } else if ([parameter isEqualToString:@"expYear"]) { + *outError = [self createErrorWithMessage:[NSError stp_cardErrorInvalidExpYearUserMessage] + parameter:parameter + cardErrorCode:STPInvalidExpYear + devErrorMessage:@"expYear must be this year or a year in the future"]; + } else { + // This should not be possible since this is a private method so we + // know exactly how it is called. We use STPAPIError for all errors + // that are unexpected within the bindings as well. + *outError = [[NSError alloc] initWithDomain:StripeDomain + code:STPAPIError + userInfo:@{ + NSLocalizedDescriptionKey: [NSError stp_unexpectedErrorMessage], + STPErrorMessageKey: @"There was an error within the Stripe client library when trying to generate the " + @"proper validation error. Contact support@stripe.com if you see this." + }]; + } + } + return NO; +} + ++ (NSError *)createErrorWithMessage:(NSString *)userMessage + parameter:(NSString *)parameter + cardErrorCode:(NSString *)cardErrorCode + devErrorMessage:(NSString *)devMessage { + return [[NSError alloc] initWithDomain:StripeDomain + code:STPCardError + userInfo:@{ + NSLocalizedDescriptionKey: userMessage, + STPErrorParameterKey: parameter, + STPCardErrorCodeKey: cardErrorCode, + STPErrorMessageKey: devMessage + }]; +} + +#pragma mark - STPFormEncodable + ++ (NSString *)rootObjectName { + return @"card"; +} + ++ (NSDictionary *)propertyNamesToFormFieldNamesMapping { + return @{ + @"number": @"number", + @"cvc": @"cvc", + @"name": @"name", + @"addressLine1": @"address_line1", + @"addressLine2": @"address_line2", + @"addressCity": @"address_city", + @"addressState": @"address_state", + @"addressZip": @"address_zip", + @"addressCountry": @"address_country", + @"expMonth": @"exp_month", + @"expYear": @"exp_year", + @"currency": @"currency", + }; +} + +@end diff --git a/core-xprojects/Stripe/Sources/STPCardValidator.m b/core-xprojects/Stripe/Sources/STPCardValidator.m new file mode 100755 index 0000000000..2d8d5ae371 --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPCardValidator.m @@ -0,0 +1,290 @@ +// +// STPCardValidator.m +// Stripe +// +// Created by Jack Flintermann on 7/15/15. +// Copyright (c) 2015 Stripe, Inc. All rights reserved. +// + +#import "STPCardValidator.h" +#import "STPBINRange.h" + +@implementation STPCardValidator + ++ (NSString *)sanitizedNumericStringForString:(NSString *)string { + return stringByRemovingCharactersFromSet(string, invertedAsciiDigitCharacterSet()); +} + +static NSCharacterSet *invertedAsciiDigitCharacterSet() { + static NSCharacterSet *cs; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + cs = [[NSCharacterSet characterSetWithCharactersInString:@"0123456789"] invertedSet]; + }); + return cs; +} + ++ (NSString *)stringByRemovingSpacesFromString:(NSString *)string { + NSCharacterSet *set = [NSCharacterSet whitespaceCharacterSet]; + return stringByRemovingCharactersFromSet(string, set); +} + +static NSString * _Nonnull stringByRemovingCharactersFromSet(NSString * _Nonnull string, NSCharacterSet * _Nonnull cs) { + NSRange range = [string rangeOfCharacterFromSet:cs]; + if (range.location != NSNotFound) { + NSMutableString *newString = [[string substringWithRange:NSMakeRange(0, range.location)] mutableCopy]; + NSUInteger lastPosition = NSMaxRange(range); + while (lastPosition < string.length) { + range = [string rangeOfCharacterFromSet:cs options:0 range:NSMakeRange(lastPosition, string.length - lastPosition)]; + if (range.location == NSNotFound) break; + if (range.location != lastPosition) { + [newString appendString:[string substringWithRange:NSMakeRange(lastPosition, range.location - lastPosition)]]; + } + lastPosition = NSMaxRange(range); + } + if (lastPosition != string.length) { + [newString appendString:[string substringWithRange:NSMakeRange(lastPosition, string.length - lastPosition)]]; + } + return newString; + } else { + return string; + } +} + ++ (BOOL)stringIsNumeric:(NSString *)string { + return [string rangeOfCharacterFromSet:invertedAsciiDigitCharacterSet()].location == NSNotFound; +} + ++ (STPCardValidationState)validationStateForExpirationMonth:(NSString *)expirationMonth { + + NSString *sanitizedExpiration = [self stringByRemovingSpacesFromString:expirationMonth]; + + if (![self stringIsNumeric:sanitizedExpiration]) { + return STPCardValidationStateInvalid; + } + + switch (sanitizedExpiration.length) { + case 0: + return STPCardValidationStateIncomplete; + case 1: + return ([sanitizedExpiration isEqualToString:@"0"] || [sanitizedExpiration isEqualToString:@"1"]) ? STPCardValidationStateIncomplete : STPCardValidationStateValid; + case 2: + return (0 < sanitizedExpiration.integerValue && sanitizedExpiration.integerValue <= 12) ? STPCardValidationStateValid : STPCardValidationStateInvalid; + default: + return STPCardValidationStateInvalid; + } +} + ++ (STPCardValidationState)validationStateForExpirationYear:(NSString *)expirationYear inMonth:(NSString *)expirationMonth inCurrentYear:(NSInteger)currentYear currentMonth:(NSInteger)currentMonth { + + NSInteger moddedYear = currentYear % 100; + + if (![self stringIsNumeric:expirationMonth] || ![self stringIsNumeric:expirationYear]) { + return STPCardValidationStateInvalid; + } + + NSString *sanitizedMonth = [self sanitizedNumericStringForString:expirationMonth]; + NSString *sanitizedYear = [self sanitizedNumericStringForString:expirationYear]; + + switch (sanitizedYear.length) { + case 0: + case 1: + return STPCardValidationStateIncomplete; + case 2: { + if (sanitizedYear.integerValue == moddedYear) { + return sanitizedMonth.integerValue >= currentMonth ? STPCardValidationStateValid : STPCardValidationStateInvalid; + } else { + return sanitizedYear.integerValue > moddedYear ? STPCardValidationStateValid : STPCardValidationStateInvalid; + } + } + default: + return STPCardValidationStateInvalid; + } +} + + ++ (STPCardValidationState)validationStateForExpirationYear:(NSString *)expirationYear + inMonth:(NSString *)expirationMonth { + return [self validationStateForExpirationYear:expirationYear + inMonth:expirationMonth + inCurrentYear:[self currentYear] + currentMonth:[self currentMonth]]; +} + + ++ (STPCardValidationState)validationStateForCVC:(NSString *)cvc cardBrand:(STPCardBrand)brand { + + if (![self stringIsNumeric:cvc]) { + return STPCardValidationStateInvalid; + } + + NSString *sanitizedCvc = [self sanitizedNumericStringForString:cvc]; + + NSUInteger minLength = [self minCVCLength]; + NSUInteger maxLength = [self maxCVCLengthForCardBrand:brand]; + if (sanitizedCvc.length < minLength) { + return STPCardValidationStateIncomplete; + } + else if (sanitizedCvc.length > maxLength) { + return STPCardValidationStateInvalid; + } + else { + return STPCardValidationStateValid; + } +} + ++ (STPCardValidationState)validationStateForNumber:(nonnull NSString *)cardNumber + validatingCardBrand:(BOOL)validatingCardBrand { + + NSString *sanitizedNumber = [self stringByRemovingSpacesFromString:cardNumber]; + if (![self stringIsNumeric:sanitizedNumber]) { + return STPCardValidationStateInvalid; + } + if (sanitizedNumber.length == 0) { + return STPCardValidationStateIncomplete; + } + STPBINRange *binRange = [STPBINRange mostSpecificBINRangeForNumber:sanitizedNumber]; + if (binRange.brand == STPCardBrandUnknown && validatingCardBrand) { + return STPCardValidationStateInvalid; + } + if (sanitizedNumber.length == binRange.length) { + BOOL isValidLuhn = [self stringIsValidLuhn:sanitizedNumber]; + return isValidLuhn ? STPCardValidationStateValid : STPCardValidationStateInvalid; + } else if (sanitizedNumber.length > binRange.length) { + return STPCardValidationStateInvalid; + } else { + return STPCardValidationStateIncomplete; + } +} + ++ (STPCardValidationState)validationStateForCard:(nonnull STPCardParams *)card inCurrentYear:(NSInteger)currentYear currentMonth:(NSInteger)currentMonth { + STPCardValidationState numberValidation = [self validationStateForNumber:card.number validatingCardBrand:YES]; + NSString *expMonthString = [NSString stringWithFormat:@"%02lu", (unsigned long)card.expMonth]; + STPCardValidationState expMonthValidation = [self validationStateForExpirationMonth:expMonthString]; + NSString *expYearString = [NSString stringWithFormat:@"%02lu", (unsigned long)card.expYear%100]; + STPCardValidationState expYearValidation = [self validationStateForExpirationYear:expYearString + inMonth:expMonthString + inCurrentYear:currentYear + currentMonth:currentMonth]; + STPCardBrand brand = [self brandForNumber:card.number]; + STPCardValidationState cvcValidation = [self validationStateForCVC:card.cvc cardBrand:brand]; + + NSArray *states = @[@(numberValidation), + @(expMonthValidation), + @(expYearValidation), + @(cvcValidation)]; + BOOL incomplete = NO; + for (NSNumber *boxedState in states) { + STPCardValidationState state = [boxedState integerValue]; + if (state == STPCardValidationStateInvalid) { + return state; + } + else if (state == STPCardValidationStateIncomplete) { + incomplete = YES; + } + } + return incomplete ? STPCardValidationStateIncomplete : STPCardValidationStateValid; +} + ++ (STPCardValidationState)validationStateForCard:(STPCardParams *)card { + return [self validationStateForCard:card + inCurrentYear:[self currentYear] + currentMonth:[self currentMonth]]; +} + ++ (NSUInteger)minCVCLength { + return 3; +} + ++ (NSUInteger)maxCVCLengthForCardBrand:(STPCardBrand)brand { + switch (brand) { + case STPCardBrandAmex: + case STPCardBrandUnknown: + return 4; + default: + return 3; + } +} + ++ (STPCardBrand)brandForNumber:(NSString *)cardNumber { + NSString *sanitizedNumber = [self sanitizedNumericStringForString:cardNumber]; + NSSet *brands = [self possibleBrandsForNumber:sanitizedNumber]; + if (brands.count == 1) { + return (STPCardBrand)[brands.anyObject integerValue]; + } + return STPCardBrandUnknown; +} + ++ (NSSet *)possibleBrandsForNumber:(NSString *)cardNumber { + NSArray *binRanges = [STPBINRange binRangesForNumber:cardNumber]; + NSMutableSet *possibleBrands = [NSMutableSet setWithArray:[binRanges valueForKeyPath:@"brand"]]; + [possibleBrands removeObject:@(STPCardBrandUnknown)]; + return [possibleBrands copy]; +} + ++ (NSSet*)lengthsForCardBrand:(STPCardBrand)brand { + NSMutableSet *set = [NSMutableSet set]; + NSArray *binRanges = [STPBINRange binRangesForBrand:brand]; + for (STPBINRange *binRange in binRanges) { + [set addObject:@(binRange.length)]; + } + return [set copy]; +} + ++ (NSInteger)lengthForCardBrand:(STPCardBrand)brand { + return [self maxLengthForCardBrand:brand]; +} + ++ (NSInteger)maxLengthForCardBrand:(STPCardBrand)brand { + NSInteger maxLength = -1; + for (NSNumber *length in [self lengthsForCardBrand:brand]) { + if (length.integerValue > maxLength) { + maxLength = length.integerValue; + } + } + return maxLength; +} + ++ (NSInteger)fragmentLengthForCardBrand:(STPCardBrand)brand { + switch (brand) { + case STPCardBrandAmex: + return 5; + case STPCardBrandDinersClub: + return 2; + default: + return 4; + } +} + ++ (BOOL)stringIsValidLuhn:(NSString *)number { + BOOL odd = true; + int sum = 0; + NSMutableArray *digits = [NSMutableArray arrayWithCapacity:number.length]; + + for (int i = 0; i < (NSInteger)number.length; i++) { + [digits addObject:[number substringWithRange:NSMakeRange(i, 1)]]; + } + + for (NSString *digitStr in [digits reverseObjectEnumerator]) { + int digit = [digitStr intValue]; + if ((odd = !odd)) digit *= 2; + if (digit > 9) digit -= 9; + sum += digit; + } + + return sum % 10 == 0; +} + ++ (NSInteger)currentYear { + NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; + NSDateComponents *dateComponents = [calendar components:NSCalendarUnitYear fromDate:[NSDate date]]; + return dateComponents.year % 100; +} + ++ (NSInteger)currentMonth { + NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; + NSDateComponents *dateComponents = [calendar components:NSCalendarUnitMonth fromDate:[NSDate date]]; + return dateComponents.month; +} + +@end diff --git a/core-xprojects/Stripe/Sources/STPCustomer.m b/core-xprojects/Stripe/Sources/STPCustomer.m new file mode 100755 index 0000000000..ff3b7c3e6f --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPCustomer.m @@ -0,0 +1,101 @@ +// +// STPCustomer.m +// Stripe +// +// Created by Jack Flintermann on 6/9/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import "STPCustomer.h" +#import "StripeError.h" +#import "STPCard.h" + +@interface STPCustomer() + +@property(nonatomic, copy)NSString *stripeID; +@property(nonatomic) id defaultSource; +@property(nonatomic) NSArray> *sources; + +@end + +@implementation STPCustomer + ++ (instancetype)customerWithStripeID:(NSString *)stripeID + defaultSource:(id)defaultSource + sources:(NSArray> *)sources { + STPCustomer *customer = [self new]; + customer.stripeID = stripeID; + customer.defaultSource = defaultSource; + customer.sources = sources; + return customer; +} + +@end + +@interface STPCustomerDeserializer() + +@property(nonatomic, nullable)STPCustomer *customer; +@property(nonatomic, nullable)NSError *error; + +@end + +@implementation STPCustomerDeserializer + +- (instancetype)initWithData:(nullable NSData *)data + urlResponse:(nullable __unused NSURLResponse *)urlResponse + error:(nullable NSError *)error { + if (error) { + return [self initWithError:error]; + } + NSError *jsonError; + id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; + if (!json) { + return [self initWithError:jsonError]; + } + return [self initWithJSONResponse:json]; +} + +- (instancetype)initWithError:(NSError *)error { + self = [super init]; + if (self) { + _error = error; + } + return self; +} + +- (instancetype)initWithJSONResponse:(id)json { + self = [super init]; + if (self) { + if (![json isKindOfClass:[NSDictionary class]] || ![json[@"id"] isKindOfClass:[NSString class]]) { + _error = [NSError stp_genericFailedToParseResponseError]; + return self; + } + STPCustomer *customer = [STPCustomer new]; + customer.stripeID = json[@"id"]; + NSString *defaultSourceId; + if ([json[@"default_source"] isKindOfClass:[NSString class]]) { + defaultSourceId = json[@"default_source"]; + } + NSMutableArray *sources = [NSMutableArray array]; + if ([json[@"sources"] isKindOfClass:[NSDictionary class]] && [json[@"sources"][@"data"] isKindOfClass:[NSArray class]]) { + for (id contents in json[@"sources"][@"data"]) { + if ([contents isKindOfClass:[NSDictionary class]]) { + // eventually support other source types + STPCard *card = [STPCard decodedObjectFromAPIResponse:contents]; + // ignore apple pay cards from the response + if (card && !card.isApplePayCard) { + [sources addObject:card]; + if (defaultSourceId && [card.stripeID isEqualToString:defaultSourceId]) { + customer.defaultSource = card; + } + } + } + } + customer.sources = sources; + } + _customer = customer; + } + return self; +} + +@end diff --git a/core-xprojects/Stripe/Sources/STPDelegateProxy.h b/core-xprojects/Stripe/Sources/STPDelegateProxy.h new file mode 100755 index 0000000000..8fdc2fa835 --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPDelegateProxy.h @@ -0,0 +1,16 @@ +// +// STPDelegateProxy.h +// Stripe +// +// Created by Jack Flintermann on 10/20/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import + +@interface STPDelegateProxy<__covariant DelegateType:NSObject *> : NSObject + +@property(nonatomic, weak)DelegateType delegate; +- (instancetype)init; + +@end diff --git a/core-xprojects/Stripe/Sources/STPDelegateProxy.m b/core-xprojects/Stripe/Sources/STPDelegateProxy.m new file mode 100755 index 0000000000..a36a931162 --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPDelegateProxy.m @@ -0,0 +1,41 @@ +// +// STPDelegateProxy.m +// Stripe +// +// Created by Jack Flintermann on 10/20/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import "STPDelegateProxy.h" + +@implementation STPDelegateProxy + +- (instancetype)init { + return self; +} + +- (BOOL)respondsToSelector:(SEL)selector { + return [super respondsToSelector:selector] || [_delegate respondsToSelector:selector]; +} + +- (id)forwardingTargetForSelector:(SEL)selector { + if ([self respondsToSelector:selector]) { + return self; + } + if ([_delegate respondsToSelector:selector]) { + return _delegate; + } + return self; +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { + return [super methodSignatureForSelector:selector] ?: [_delegate methodSignatureForSelector:selector]; +} + +- (void)forwardInvocation:(NSInvocation *)invocation { + if ([_delegate respondsToSelector:invocation.selector]) { + [invocation invokeWithTarget:_delegate]; + } +} + +@end diff --git a/core-xprojects/Stripe/Sources/STPDispatchFunctions.h b/core-xprojects/Stripe/Sources/STPDispatchFunctions.h new file mode 100755 index 0000000000..44d8778485 --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPDispatchFunctions.h @@ -0,0 +1,11 @@ +// +// STPDispatchFunctions.h +// Stripe +// +// Created by Brian Dorfman on 10/24/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#include + +void stpDispatchToMainThreadIfNecessary(dispatch_block_t block); diff --git a/core-xprojects/Stripe/Sources/STPDispatchFunctions.m b/core-xprojects/Stripe/Sources/STPDispatchFunctions.m new file mode 100755 index 0000000000..a64f30b18d --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPDispatchFunctions.m @@ -0,0 +1,18 @@ +// +// STPDispatchFunctions.m +// Stripe +// +// Created by Brian Dorfman on 10/24/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#include "STPDispatchFunctions.h" + +void stpDispatchToMainThreadIfNecessary(dispatch_block_t block) { + if ([NSThread isMainThread]) { + block(); + } + else { + dispatch_async(dispatch_get_main_queue(), block); + } +} diff --git a/core-xprojects/Stripe/Sources/STPFormEncoder.h b/core-xprojects/Stripe/Sources/STPFormEncoder.h new file mode 100755 index 0000000000..1d4d2f089d --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPFormEncoder.h @@ -0,0 +1,24 @@ +// +// STPFormEncoder.h +// Stripe +// +// Created by Jack Flintermann on 1/8/15. +// Copyright (c) 2015 Stripe, Inc. All rights reserved. +// + +#import + +@class STPCardParams, STPBankAccountParams; +@protocol STPFormEncodable; + +@interface STPFormEncoder : NSObject + ++ (nonnull NSData *)formEncodedDataForObject:(nonnull NSObject *)object; + ++ (nonnull NSString *)stringByURLEncoding:(nonnull NSString *)string; + ++ (nonnull NSString *)stringByReplacingSnakeCaseWithCamelCase:(nonnull NSString *)input; + ++ (nonnull NSString *)queryStringFromParameters:(nonnull NSDictionary *)parameters; + +@end diff --git a/core-xprojects/Stripe/Sources/STPFormEncoder.m b/core-xprojects/Stripe/Sources/STPFormEncoder.m new file mode 100755 index 0000000000..01b15851a9 --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPFormEncoder.m @@ -0,0 +1,188 @@ +// +// STPFormEncoder.m +// Stripe +// +// Created by Jack Flintermann on 1/8/15. +// Copyright (c) 2015 Stripe, Inc. All rights reserved. +// + +#import "STPFormEncoder.h" +#import "STPCardParams.h" + +FOUNDATION_EXPORT NSString * STPPercentEscapedStringFromString(NSString *string); +FOUNDATION_EXPORT NSString * STPQueryStringFromParameters(NSDictionary *parameters); + +@implementation STPFormEncoder + ++ (NSString *)stringByReplacingSnakeCaseWithCamelCase:(NSString *)input { + NSArray *parts = [input componentsSeparatedByString:@"_"]; + NSMutableString *camelCaseParam = [NSMutableString string]; + [parts enumerateObjectsUsingBlock:^(NSString *part, NSUInteger idx, __unused BOOL *stop) { + [camelCaseParam appendString:(idx == 0 ? part : [part capitalizedString])]; + }]; + + return [camelCaseParam copy]; +} + ++ (nonnull NSData *)formEncodedDataForObject:(nonnull NSObject *)object { + NSDictionary *keyPairs = [self keyPairDictionaryForObject:object]; + NSString *rootObjectName = [object.class rootObjectName]; + NSDictionary *dict = rootObjectName != nil ? @{ rootObjectName: keyPairs } : keyPairs; + return [STPQueryStringFromParameters(dict) dataUsingEncoding:NSUTF8StringEncoding]; +} + ++ (NSDictionary *)keyPairDictionaryForObject:(nonnull NSObject *)object { + NSMutableDictionary *keyPairs = [NSMutableDictionary dictionary]; + [[object.class propertyNamesToFormFieldNamesMapping] enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull propertyName, NSString * _Nonnull formFieldName, __unused BOOL * _Nonnull stop) { + id value = [self formEncodableValueForObject:[object valueForKey:propertyName]]; + if (value) { + keyPairs[formFieldName] = value; + } + }]; + [object.additionalAPIParameters enumerateKeysAndObjectsUsingBlock:^(id _Nonnull additionalFieldName, id _Nonnull additionalFieldValue, __unused BOOL * _Nonnull stop) { + id value = [self formEncodableValueForObject:additionalFieldValue]; + if (value) { + keyPairs[additionalFieldName] = value; + } + }]; + return [keyPairs copy]; +} + ++ (id)formEncodableValueForObject:(NSObject *)object { + if ([object conformsToProtocol:@protocol(STPFormEncodable)]) { + return [self keyPairDictionaryForObject:(NSObject*)object]; + } else { + return object; + } +} + ++ (NSString *)stringByURLEncoding:(NSString *)string { + return STPPercentEscapedStringFromString(string); +} + ++ (NSString *)queryStringFromParameters:(NSDictionary *)parameters { + return STPQueryStringFromParameters(parameters); +} + +@end + + +// This code is adapted from https://github.com/AFNetworking/AFNetworking/blob/master/AFNetworking/AFURLRequestSerialization.m . The only modifications are to replace the AF namespace with the STP namespace to avoid collisions with apps that are using both Stripe and AFNetworking. +NSString * STPPercentEscapedStringFromString(NSString *string) { + static NSString * const kSTPCharactersGeneralDelimitersToEncode = @":#[]@"; // does not include "?" or "/" due to RFC 3986 - Section 3.4 + static NSString * const kSTPCharactersSubDelimitersToEncode = @"!$&'()*+,;="; + + NSMutableCharacterSet * allowedCharacterSet = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy]; + [allowedCharacterSet removeCharactersInString:[kSTPCharactersGeneralDelimitersToEncode stringByAppendingString:kSTPCharactersSubDelimitersToEncode]]; + + // FIXME: https://github.com/AFNetworking/AFNetworking/pull/3028 + // return [string stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet]; + + static NSUInteger const batchSize = 50; + + NSUInteger index = 0; + NSMutableString *escaped = @"".mutableCopy; + + while (index < string.length) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wgnu" + NSUInteger length = MIN(string.length - index, batchSize); +#pragma GCC diagnostic pop + NSRange range = NSMakeRange(index, length); + + // To avoid breaking up character sequences such as 👴🏻👮🏽 + range = [string rangeOfComposedCharacterSequencesForRange:range]; + + NSString *substring = [string substringWithRange:range]; + NSString *encoded = [substring stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet]; + [escaped appendString:encoded]; + + index += range.length; + } + + return escaped; +} + +#pragma mark - + +@interface STPQueryStringPair : NSObject +@property (readwrite, nonatomic, strong) id field; +@property (readwrite, nonatomic, strong) id value; + +- (instancetype)initWithField:(id)field value:(id)value; + +- (NSString *)URLEncodedStringValue; +@end + +@implementation STPQueryStringPair + +- (instancetype)initWithField:(id)field value:(id)value { + self = [super init]; + if (!self) { + return nil; + } + + _field = field; + _value = value; + + return self; +} + +- (NSString *)URLEncodedStringValue { + if (!self.value || [self.value isEqual:[NSNull null]]) { + return STPPercentEscapedStringFromString([self.field description]); + } else { + return [NSString stringWithFormat:@"%@=%@", STPPercentEscapedStringFromString([self.field description]), STPPercentEscapedStringFromString([self.value description])]; + } +} + +@end + +#pragma mark - + +FOUNDATION_EXPORT NSArray * STPQueryStringPairsFromDictionary(NSDictionary *dictionary); +FOUNDATION_EXPORT NSArray * STPQueryStringPairsFromKeyAndValue(NSString *key, id value); + +NSString * STPQueryStringFromParameters(NSDictionary *parameters) { + NSMutableArray *mutablePairs = [NSMutableArray array]; + for (STPQueryStringPair *pair in STPQueryStringPairsFromDictionary(parameters)) { + [mutablePairs addObject:[pair URLEncodedStringValue]]; + } + + return [mutablePairs componentsJoinedByString:@"&"]; +} + +NSArray * STPQueryStringPairsFromDictionary(NSDictionary *dictionary) { + return STPQueryStringPairsFromKeyAndValue(nil, dictionary); +} + +NSArray * STPQueryStringPairsFromKeyAndValue(NSString *key, id value) { + NSMutableArray *mutableQueryStringComponents = [NSMutableArray array]; + NSString *descriptionSelector = NSStringFromSelector(@selector(description)); + NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:descriptionSelector ascending:YES selector:@selector(compare:)]; + + if ([value isKindOfClass:[NSDictionary class]]) { + NSDictionary *dictionary = value; + // Sort dictionary keys to ensure consistent ordering in query string, which is important when deserializing potentially ambiguous sequences, such as an array of dictionaries + for (id nestedKey in [dictionary.allKeys sortedArrayUsingDescriptors:@[ sortDescriptor ]]) { + id nestedValue = dictionary[nestedKey]; + if (nestedValue) { + [mutableQueryStringComponents addObjectsFromArray:STPQueryStringPairsFromKeyAndValue((key ? [NSString stringWithFormat:@"%@[%@]", key, nestedKey] : nestedKey), nestedValue)]; + } + } + } else if ([value isKindOfClass:[NSArray class]]) { + NSArray *array = value; + for (id nestedValue in array) { + [mutableQueryStringComponents addObjectsFromArray:STPQueryStringPairsFromKeyAndValue([NSString stringWithFormat:@"%@[]", key], nestedValue)]; + } + } else if ([value isKindOfClass:[NSSet class]]) { + NSSet *set = value; + for (id obj in [set sortedArrayUsingDescriptors:@[ sortDescriptor ]]) { + [mutableQueryStringComponents addObjectsFromArray:STPQueryStringPairsFromKeyAndValue(key, obj)]; + } + } else { + [mutableQueryStringComponents addObject:[[STPQueryStringPair alloc] initWithField:key value:value]]; + } + + return mutableQueryStringComponents; +} diff --git a/core-xprojects/Stripe/Sources/STPImageLibrary+Private.h b/core-xprojects/Stripe/Sources/STPImageLibrary+Private.h new file mode 100755 index 0000000000..45e242f588 --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPImageLibrary+Private.h @@ -0,0 +1,31 @@ +// +// STPImageLibrary+Private.h +// Stripe +// +// Created by Jack Flintermann on 6/30/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import "STPImageLibrary.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface STPImageLibrary (Private) + ++ (NSImage *)addIcon; ++ (NSImage *)leftChevronIcon; ++ (NSImage *)smallRightChevronIcon; ++ (NSImage *)checkmarkIcon; ++ (NSImage *)largeCardFrontImage; ++ (NSImage *)largeCardBackImage; ++ (NSImage *)largeCardApplePayImage; + ++ (NSImage *)safeImageNamed:(NSString *)imageName + templateIfAvailable:(BOOL)templateIfAvailable; ++ (NSImage *)brandImageForCardBrand:(STPCardBrand)brand + template:(BOOL)isTemplate; + +@end + +NS_ASSUME_NONNULL_END diff --git a/core-xprojects/Stripe/Sources/STPImageLibrary.m b/core-xprojects/Stripe/Sources/STPImageLibrary.m new file mode 100755 index 0000000000..1f69be0c31 --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPImageLibrary.m @@ -0,0 +1,140 @@ +// +// STPImages.m +// Stripe +// +// Created by Jack Flintermann on 6/30/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import "STPImageLibrary.h" +#import "STPImageLibrary+Private.h" + +// Dummy class for locating the framework bundle + + +@implementation STPImageLibrary + ++ (NSImage *)applePayCardImage { + return [self safeImageNamed:@"stp_card_applepay"]; +} + ++ (NSImage *)amexCardImage { + return [self brandImageForCardBrand:STPCardBrandAmex]; +} + ++ (NSImage *)dinersClubCardImage { + return [self brandImageForCardBrand:STPCardBrandDinersClub]; +} + ++ (NSImage *)discoverCardImage { + return [self brandImageForCardBrand:STPCardBrandDiscover]; +} + ++ (NSImage *)jcbCardImage { + return [self brandImageForCardBrand:STPCardBrandJCB]; +} + ++ (NSImage *)masterCardCardImage { + return [self brandImageForCardBrand:STPCardBrandMasterCard]; +} + ++ (NSImage *)visaCardImage { + return [self brandImageForCardBrand:STPCardBrandVisa]; +} + ++ (NSImage *)unknownCardCardImage { + return [self brandImageForCardBrand:STPCardBrandUnknown]; +} + ++ (NSImage *)brandImageForCardBrand:(STPCardBrand)brand { + return [self brandImageForCardBrand:brand template:NO]; +} + ++ (NSImage *)templatedBrandImageForCardBrand:(STPCardBrand)brand { + return [self brandImageForCardBrand:brand template:YES]; +} + ++ (NSImage *)cvcImageForCardBrand:(STPCardBrand)brand { + NSString *imageName = brand == STPCardBrandAmex ? @"stp_card_cvc_amex" : @"stp_card_cvc"; + return [self safeImageNamed:imageName]; +} + ++ (NSImage *)safeImageNamed:(NSString *)imageName { + return [self safeImageNamed:imageName templateIfAvailable:NO]; +} + +@end + +@implementation STPImageLibrary (Private) + ++ (NSImage *)addIcon { + return [self safeImageNamed:@"stp_icon_add" templateIfAvailable:YES]; +} + ++ (NSImage *)leftChevronIcon { + return [self safeImageNamed:@"stp_icon_chevron_left" templateIfAvailable:YES]; +} + ++ (NSImage *)smallRightChevronIcon { + return [self safeImageNamed:@"stp_icon_chevron_right_small" templateIfAvailable:YES]; +} + ++ (NSImage *)checkmarkIcon { + return [self safeImageNamed:@"stp_icon_checkmark" templateIfAvailable:YES]; +} + ++ (NSImage *)largeCardFrontImage { + return [self safeImageNamed:@"stp_card_form_front" templateIfAvailable:YES]; +} + ++ (NSImage *)largeCardBackImage { + return [self safeImageNamed:@"stp_card_form_back" templateIfAvailable:YES]; +} + ++ (NSImage *)largeCardApplePayImage { + return [self safeImageNamed:@"stp_card_form_applepay" templateIfAvailable:YES]; +} + ++ (NSImage *)safeImageNamed:(NSString *)imageName + templateIfAvailable:(BOOL)templateIfAvailable { + + NSImage *image = [NSImage imageNamed:imageName]; +// [image setTemplate:templateIfAvailable]; + + return image; +} + ++ (NSImage *)brandImageForCardBrand:(STPCardBrand)brand + template:(BOOL)isTemplate { + BOOL shouldUseTemplate = isTemplate; + NSString *imageName; + switch (brand) { + case STPCardBrandAmex: + imageName = shouldUseTemplate ? @"stp_card_amex_template" : @"stp_card_amex"; + break; + case STPCardBrandDinersClub: + imageName = shouldUseTemplate ? @"stp_card_diners_template" : @"stp_card_diners"; + break; + case STPCardBrandDiscover: + imageName = shouldUseTemplate ? @"stp_card_discover_template" : @"stp_card_discover"; + break; + case STPCardBrandJCB: + imageName = shouldUseTemplate ? @"stp_card_jcb_template" : @"stp_card_jcb"; + break; + case STPCardBrandMasterCard: + imageName = shouldUseTemplate ? @"stp_card_mastercard_template" : @"stp_card_mastercard"; + break; + case STPCardBrandUnknown: + shouldUseTemplate = YES; + imageName = @"stp_card_unknown"; + break; + case STPCardBrandVisa: + imageName = shouldUseTemplate ? @"stp_card_visa_template" : @"stp_card_visa"; + break; + } + NSImage *image = [self safeImageNamed:imageName + templateIfAvailable:shouldUseTemplate]; + return image; +} + +@end diff --git a/core-xprojects/Stripe/Sources/STPPaymentCardTextFieldViewModel.h b/core-xprojects/Stripe/Sources/STPPaymentCardTextFieldViewModel.h new file mode 100755 index 0000000000..3a3c8c96d4 --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPPaymentCardTextFieldViewModel.h @@ -0,0 +1,37 @@ +// +// STPPaymentCardTextFieldViewModel.h +// Stripe +// +// Created by Jack Flintermann on 7/21/15. +// Copyright (c) 2015 Stripe, Inc. All rights reserved. +// + +#import +#import + +#import "STPCard.h" +#import "STPCardValidator.h" + +typedef NS_ENUM(NSInteger, STPCardFieldType) { + STPCardFieldTypeNumber, + STPCardFieldTypeExpiration, + STPCardFieldTypeCVC, +}; + +@interface STPPaymentCardTextFieldViewModel : NSObject + +@property(nonatomic, readwrite, copy, nullable)NSString *cardNumber; +@property(nonatomic, readwrite, copy, nullable)NSString *rawExpiration; +@property(nonatomic, readonly, nullable)NSString *expirationMonth; +@property(nonatomic, readonly, nullable)NSString *expirationYear; +@property(nonatomic, readwrite, copy, nullable)NSString *cvc; +@property(nonatomic, readonly) STPCardBrand brand; + +- (nonnull NSString *)defaultPlaceholder; +- (nullable NSString *)numberWithoutLastDigits; + +- (BOOL)isValid; + +- (STPCardValidationState)validationStateForField:(STPCardFieldType)fieldType; + +@end diff --git a/core-xprojects/Stripe/Sources/STPPaymentCardTextFieldViewModel.m b/core-xprojects/Stripe/Sources/STPPaymentCardTextFieldViewModel.m new file mode 100755 index 0000000000..014691eba7 --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPPaymentCardTextFieldViewModel.m @@ -0,0 +1,105 @@ +// +// STPPaymentCardTextFieldViewModel.m +// Stripe +// +// Created by Jack Flintermann on 7/21/15. +// Copyright (c) 2015 Stripe, Inc. All rights reserved. +// + +#import "STPPaymentCardTextFieldViewModel.h" +#import "NSString+Stripe.h" + +#define FAUXPAS_IGNORED_IN_METHOD(...) + +@implementation STPPaymentCardTextFieldViewModel + +- (void)setCardNumber:(NSString *)cardNumber { + NSString *sanitizedNumber = [STPCardValidator sanitizedNumericStringForString:cardNumber]; + STPCardBrand brand = [STPCardValidator brandForNumber:sanitizedNumber]; + NSInteger maxLength = [STPCardValidator maxLengthForCardBrand:brand]; + _cardNumber = [sanitizedNumber stp_safeSubstringToIndex:maxLength]; +} + +// This might contain slashes. +- (void)setRawExpiration:(NSString *)expiration { + NSString *sanitizedExpiration = [STPCardValidator sanitizedNumericStringForString:expiration]; + self.expirationMonth = [sanitizedExpiration stp_safeSubstringToIndex:2]; + self.expirationYear = [[sanitizedExpiration stp_safeSubstringFromIndex:2] stp_safeSubstringToIndex:2]; +} + +- (NSString *)rawExpiration { + NSMutableArray *array = [@[] mutableCopy]; + if (self.expirationMonth && ![self.expirationMonth isEqualToString:@""]) { + [array addObject:self.expirationMonth]; + } + + if ([STPCardValidator validationStateForExpirationMonth:self.expirationMonth] == STPCardValidationStateValid) { + [array addObject:self.expirationYear]; + } + return [array componentsJoinedByString:@"/"]; +} + +- (void)setExpirationMonth:(NSString *)expirationMonth { + NSString *sanitizedExpiration = [STPCardValidator sanitizedNumericStringForString:expirationMonth]; + if (sanitizedExpiration.length == 1 && ![sanitizedExpiration isEqualToString:@"0"] && ![sanitizedExpiration isEqualToString:@"1"]) { + sanitizedExpiration = [@"0" stringByAppendingString:sanitizedExpiration]; + } + _expirationMonth = [sanitizedExpiration stp_safeSubstringToIndex:2]; +} + +- (void)setExpirationYear:(NSString *)expirationYear { + _expirationYear = [[STPCardValidator sanitizedNumericStringForString:expirationYear] stp_safeSubstringToIndex:2]; +} + +- (void)setCvc:(NSString *)cvc { + NSInteger maxLength = [STPCardValidator maxCVCLengthForCardBrand:self.brand]; + _cvc = [[STPCardValidator sanitizedNumericStringForString:cvc] stp_safeSubstringToIndex:maxLength]; +} + +- (STPCardBrand)brand { + return [STPCardValidator brandForNumber:self.cardNumber]; +} + +- (STPCardValidationState)validationStateForField:(STPCardFieldType)fieldType { + switch (fieldType) { + case STPCardFieldTypeNumber: + return [STPCardValidator validationStateForNumber:self.cardNumber validatingCardBrand:YES]; + break; + case STPCardFieldTypeExpiration: { + STPCardValidationState monthState = [STPCardValidator validationStateForExpirationMonth:self.expirationMonth]; + STPCardValidationState yearState = [STPCardValidator validationStateForExpirationYear:self.expirationYear inMonth:self.expirationMonth]; + if (monthState == STPCardValidationStateValid && yearState == STPCardValidationStateValid) { + return STPCardValidationStateValid; + } else if (monthState == STPCardValidationStateInvalid || yearState == STPCardValidationStateInvalid) { + return STPCardValidationStateInvalid; + } else { + return STPCardValidationStateIncomplete; + } + break; + } + case STPCardFieldTypeCVC: + return [STPCardValidator validationStateForCVC:self.cvc cardBrand:self.brand]; + } +} + +- (BOOL)isValid { + return ([self validationStateForField:STPCardFieldTypeNumber] == STPCardValidationStateValid && + [self validationStateForField:STPCardFieldTypeExpiration] == STPCardValidationStateValid && + [self validationStateForField:STPCardFieldTypeCVC] == STPCardValidationStateValid); +} + +- (NSString *)defaultPlaceholder { + return @"1234567812345678"; +} + +- (NSString *)numberWithoutLastDigits { + NSUInteger length = [STPCardValidator fragmentLengthForCardBrand:[STPCardValidator brandForNumber:self.cardNumber]]; + NSUInteger toIndex = self.cardNumber.length - length; + + return (toIndex < self.cardNumber.length) ? + [self.cardNumber substringToIndex:toIndex] : + [self.defaultPlaceholder stp_safeSubstringToIndex:[self defaultPlaceholder].length - length]; + +} + +@end diff --git a/core-xprojects/Stripe/Sources/STPPaymentConfiguration+Private.h b/core-xprojects/Stripe/Sources/STPPaymentConfiguration+Private.h new file mode 100755 index 0000000000..2ced368774 --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPPaymentConfiguration+Private.h @@ -0,0 +1,16 @@ +// +// STPPaymentConfiguration+Private.h +// Stripe +// +// Created by Jack Flintermann on 6/9/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import "STPPaymentConfiguration.h" + +@interface STPPaymentConfiguration (Private) + +@property(nonatomic, readonly)BOOL applePayEnabled; + +@end + diff --git a/core-xprojects/Stripe/Sources/STPPaymentConfiguration.m b/core-xprojects/Stripe/Sources/STPPaymentConfiguration.m new file mode 100755 index 0000000000..d55e0b26dc --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPPaymentConfiguration.m @@ -0,0 +1,46 @@ +// +// STPPaymentConfiguration.m +// Stripe +// +// Created by Jack Flintermann on 5/18/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import "STPPaymentConfiguration.h" +#import "STPPaymentConfiguration+Private.h" +#import "STPAPIClient.h" + +@implementation STPPaymentConfiguration + ++ (instancetype)sharedConfiguration { + static STPPaymentConfiguration *sharedConfiguration; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedConfiguration = [self new]; + }); + return sharedConfiguration; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _additionalPaymentMethods = STPPaymentMethodTypeAll; + _requiredBillingAddressFields = STPBillingAddressFieldsNone; + _companyName = @"Telegram"; + _smsAutofillDisabled = NO; + } + return self; +} + +- (id)copyWithZone:(__unused NSZone *)zone { + STPPaymentConfiguration *copy = [self.class new]; + copy.publishableKey = self.publishableKey; + copy.additionalPaymentMethods = self.additionalPaymentMethods; + copy.requiredBillingAddressFields = self.requiredBillingAddressFields; + copy.companyName = self.companyName; + copy.appleMerchantIdentifier = self.appleMerchantIdentifier; + copy.smsAutofillDisabled = self.smsAutofillDisabled; + return copy; +} + +@end diff --git a/core-xprojects/Stripe/Sources/STPPhoneNumberValidator.m b/core-xprojects/Stripe/Sources/STPPhoneNumberValidator.m new file mode 100755 index 0000000000..09f83d0e78 --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPPhoneNumberValidator.m @@ -0,0 +1,107 @@ +// +// STPPhoneNumberValidator.m +// Stripe +// +// Created by Jack Flintermann on 10/16/15. +// Copyright © 2015 Stripe, Inc. All rights reserved. +// + +#import "STPPhoneNumberValidator.h" +#import "STPCardValidator.h" +#import "NSString+Stripe.h" + +@implementation STPPhoneNumberValidator + ++ (NSString *)countryCodeOrCurrentLocaleCountryFromString:(nullable NSString *)nillableCode { + NSString *countryCode = nillableCode; + if (!countryCode) { + countryCode = [[NSLocale autoupdatingCurrentLocale] objectForKey:NSLocaleCountryCode]; + } + return countryCode; +} + ++ (BOOL)stringIsValidPartialPhoneNumber:(NSString *)string { + return [self stringIsValidPartialPhoneNumber:string forCountryCode:nil]; +} + ++ (BOOL)stringIsValidPhoneNumber:(NSString *)string { + return [self stringIsValidPhoneNumber:string forCountryCode:nil]; +} + ++ (BOOL)stringIsValidPartialPhoneNumber:(NSString *)string + forCountryCode:(nullable NSString *)nillableCode { + NSString *countryCode = [self countryCodeOrCurrentLocaleCountryFromString:nillableCode]; + + if ([countryCode isEqualToString:@"US"]) { + return [STPCardValidator sanitizedNumericStringForString:string].length <= 10; + } + else { + return YES; + } +} + ++ (BOOL)stringIsValidPhoneNumber:(NSString *)string + forCountryCode:(nullable NSString *)nillableCode { + NSString *countryCode = [self countryCodeOrCurrentLocaleCountryFromString:nillableCode]; + + if ([countryCode isEqualToString:@"US"]) { + return [STPCardValidator sanitizedNumericStringForString:string].length == 10; + } + else { + return YES; + } +} + ++ (NSString *)formattedSanitizedPhoneNumberForString:(NSString *)string { + return [self formattedSanitizedPhoneNumberForString:string + forCountryCode:nil]; +} + ++ (NSString *)formattedSanitizedPhoneNumberForString:(NSString *)string + forCountryCode:(nullable NSString *)nillableCode { + NSString *countryCode = [self countryCodeOrCurrentLocaleCountryFromString:nillableCode]; + NSString *sanitized = [STPCardValidator sanitizedNumericStringForString:string]; + return [self formattedPhoneNumberForString:sanitized + forCountryCode:countryCode]; +} + ++ (NSString *)formattedRedactedPhoneNumberForString:(NSString *)string { + return [self formattedRedactedPhoneNumberForString:string + forCountryCode:nil]; +} + ++ (NSString *)formattedRedactedPhoneNumberForString:(NSString *)string + forCountryCode:(nullable NSString *)nillableCode { + NSString *countryCode = [self countryCodeOrCurrentLocaleCountryFromString:nillableCode]; + NSScanner *scanner = [NSScanner scannerWithString:string]; + NSMutableString *prefix = [NSMutableString stringWithCapacity:string.length]; + [scanner scanUpToString:@"*" intoString:&prefix]; + NSString *number = [string stringByReplacingOccurrencesOfString:prefix withString:@""]; + number = [number stringByReplacingOccurrencesOfString:@"*" withString:@"•"]; + number = [self formattedPhoneNumberForString:number + forCountryCode:countryCode]; + return [NSString stringWithFormat:@"%@ %@", prefix, number]; +} + ++ (NSString *)formattedPhoneNumberForString:(NSString *)string + forCountryCode:(NSString *)countryCode { + + if (![countryCode isEqualToString:@"US"]) { + return string; + } + if (string.length >= 6) { + return [NSString stringWithFormat:@"(%@) %@-%@", + [string stp_safeSubstringToIndex:3], + [[string stp_safeSubstringToIndex:6] stp_safeSubstringFromIndex:3], + [[string stp_safeSubstringToIndex:10] stp_safeSubstringFromIndex:6] + ]; + } else if (string.length >= 3) { + return [NSString stringWithFormat:@"(%@) %@", + [string stp_safeSubstringToIndex:3], + [string stp_safeSubstringFromIndex:3] + ]; + } + return string; +} + +@end diff --git a/core-xprojects/Stripe/Sources/STPPostalCodeValidator.m b/core-xprojects/Stripe/Sources/STPPostalCodeValidator.m new file mode 100755 index 0000000000..a332ba6a21 --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPPostalCodeValidator.m @@ -0,0 +1,117 @@ +// +// STPPostalCodeValidator.m +// Stripe +// +// Created by Ben Guo on 4/14/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +#import "STPPostalCodeValidator.h" +#import "STPCardValidator.h" +#import "STPPhoneNumberValidator.h" + +@implementation STPPostalCodeValidator + ++ (BOOL)stringIsValidPostalCode:(nullable NSString *)string + type:(STPPostalCodeType)postalCodeType { + switch (postalCodeType) { + case STPCountryPostalCodeTypeNumericOnly: + return [STPCardValidator sanitizedNumericStringForString:string].length > 0; + case STPCountryPostalCodeTypeAlphanumeric: + return string.length > 0; + case STPCountryPostalCodeTypeNotRequired: + return YES; + } +} + ++ (BOOL)stringIsValidPostalCode:(nullable NSString *)string + countryCode:(nullable NSString *)countryCode { + return [self stringIsValidPostalCode:string + type:[self postalCodeTypeForCountryCode:countryCode]]; +} + ++ (STPPostalCodeType)postalCodeTypeForCountryCode:(NSString *)countryCode { + if ([countryCode isEqualToString:@"US"]) { + return STPCountryPostalCodeTypeNumericOnly; + } + else if ([[self countriesWithNoPostalCodes] containsObject:countryCode]) { + return STPCountryPostalCodeTypeNotRequired; + } + else { + return STPCountryPostalCodeTypeAlphanumeric; + } +} + ++ (NSArray *)countriesWithNoPostalCodes { + return @[ @"AE", + @"AG", + @"AN", + @"AO", + @"AW", + @"BF", + @"BI", + @"BJ", + @"BO", + @"BS", + @"BW", + @"BZ", + @"CD", + @"CF", + @"CG", + @"CI", + @"CK", + @"CM", + @"DJ", + @"DM", + @"ER", + @"FJ", + @"GD", + @"GH", + @"GM", + @"GN", + @"GQ", + @"GY", + @"HK", + @"IE", + @"JM", + @"KE", + @"KI", + @"KM", + @"KN", + @"KP", + @"LC", + @"ML", + @"MO", + @"MR", + @"MS", + @"MU", + @"MW", + @"NR", + @"NU", + @"PA", + @"QA", + @"RW", + @"SA", + @"SB", + @"SC", + @"SL", + @"SO", + @"SR", + @"ST", + @"SY", + @"TF", + @"TK", + @"TL", + @"TO", + @"TT", + @"TV", + @"TZ", + @"UG", + @"VU", + @"YE", + @"ZA", + @"ZW" + ]; +} + +@end diff --git a/core-xprojects/Stripe/Sources/STPToken.m b/core-xprojects/Stripe/Sources/STPToken.m new file mode 100755 index 0000000000..1dcd8933b6 --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPToken.m @@ -0,0 +1,101 @@ +// +// STPToken.m +// Stripe +// +// Created by Saikat Chakrabarti on 11/5/12. +// +// + +#import "STPToken.h" +#import "STPCard.h" +#import "STPBankAccount.h" +#import "NSDictionary+Stripe.h" + +@interface STPToken() +@property (nonatomic, nonnull) NSString *tokenId; +@property (nonatomic) BOOL livemode; +@property (nonatomic, nullable) STPCard *card; +@property (nonatomic, nullable) STPBankAccount *bankAccount; +@property (nonatomic, nullable) NSDate *created; +@property (nonatomic, readwrite, nonnull, copy) NSDictionary *allResponseFields; +@end + +@implementation STPToken + +- (NSString *)description { + return self.tokenId ?: @"Unknown token"; +} + +- (NSString *)debugDescription { + NSString *token = self.tokenId ?: @"Unknown token"; + NSString *livemode = self.livemode ? @"live mode" : @"test mode"; + return [NSString stringWithFormat:@"%@ (%@)", token, livemode]; +} + +- (BOOL)isEqual:(id)object { + return [self isEqualToToken:object]; +} + +- (NSUInteger)hash { + return [self.tokenId hash]; +} + +- (BOOL)isEqualToToken:(STPToken *)object { + if (self == object) { + return YES; + } + + if (!object || ![object isKindOfClass:self.class]) { + return NO; + } + + if ((self.card || object.card) && (![self.card isEqual:object.card])) { + return NO; + } + + if ((self.bankAccount || object.bankAccount) && (![self.bankAccount isEqual:object.bankAccount])) { + return NO; + } + + return self.livemode == object.livemode && [self.tokenId isEqualToString:object.tokenId] && [self.created isEqualToDate:object.created] && + [self.card isEqual:object.card] && [self.tokenId isEqualToString:object.tokenId] && [self.created isEqualToDate:object.created]; +} + +#pragma mark STPSource + +- (NSString *)stripeID { + return self.tokenId; +} + +#pragma mark STPAPIResponseDecodable + ++ (NSArray *)requiredFields { + return @[@"id", @"livemode", @"created"]; +} + ++ (instancetype)decodedObjectFromAPIResponse:(NSDictionary *)response { + NSDictionary *dict = [response stp_dictionaryByRemovingNullsValidatingRequiredFields:[self requiredFields]]; + if (!dict) { + return nil; + } + + STPToken *token = [self new]; + token.tokenId = dict[@"id"]; + token.livemode = [dict[@"livemode"] boolValue]; + token.created = [NSDate dateWithTimeIntervalSince1970:[dict[@"created"] doubleValue]]; + + NSDictionary *cardDictionary = dict[@"card"]; + if (cardDictionary) { + token.card = [STPCard decodedObjectFromAPIResponse:cardDictionary]; + } + + NSDictionary *bankAccountDictionary = dict[@"bank_account"]; + if (bankAccountDictionary) { + token.bankAccount = [STPBankAccount decodedObjectFromAPIResponse:bankAccountDictionary]; + } + + token.allResponseFields = dict; + return token; +} + +@end diff --git a/core-xprojects/Stripe/Sources/STPWeakStrongMacros.h b/core-xprojects/Stripe/Sources/STPWeakStrongMacros.h new file mode 100755 index 0000000000..e090126cda --- /dev/null +++ b/core-xprojects/Stripe/Sources/STPWeakStrongMacros.h @@ -0,0 +1,19 @@ +// +// STPWeakStrongMacros.h +// Stripe +// +// Created by Brian Dorfman on 7/28/16. +// Copyright © 2016 Stripe, Inc. All rights reserved. +// + +/* + * Based on @weakify() and @strongify() from + * https://github.com/jspahrsummers/libextc + */ + +#define WEAK(var) __weak typeof(var) weak_##var = var; +#define STRONG(var) \ +_Pragma("clang diagnostic push") \ +_Pragma("clang diagnostic ignored \"-Wshadow\"") \ +__strong typeof(var) var = weak_##var; \ +_Pragma("clang diagnostic pop") \ diff --git a/core-xprojects/Stripe/Sources/StripeError.m b/core-xprojects/Stripe/Sources/StripeError.m new file mode 100755 index 0000000000..d503524da2 --- /dev/null +++ b/core-xprojects/Stripe/Sources/StripeError.m @@ -0,0 +1,137 @@ +// +// StripeError.m +// Stripe +// +// Created by Saikat Chakrabarti on 11/4/12. +// +// + +#import "StripeError.h" +#import "STPFormEncoder.h" + +NSString *const StripeDomain = @"com.stripe.lib"; +NSString *const STPCardErrorCodeKey = @"com.stripe.lib:CardErrorCodeKey"; +NSString *const STPErrorMessageKey = @"com.stripe.lib:ErrorMessageKey"; +NSString *const STPErrorParameterKey = @"com.stripe.lib:ErrorParameterKey"; +NSString *const STPInvalidNumber = @"com.stripe.lib:InvalidNumber"; +NSString *const STPInvalidExpMonth = @"com.stripe.lib:InvalidExpiryMonth"; +NSString *const STPInvalidExpYear = @"com.stripe.lib:InvalidExpiryYear"; +NSString *const STPInvalidCVC = @"com.stripe.lib:InvalidCVC"; +NSString *const STPIncorrectNumber = @"com.stripe.lib:IncorrectNumber"; +NSString *const STPExpiredCard = @"com.stripe.lib:ExpiredCard"; +NSString *const STPCardDeclined = @"com.stripe.lib:CardDeclined"; +NSString *const STPProcessingError = @"com.stripe.lib:ProcessingError"; +NSString *const STPIncorrectCVC = @"com.stripe.lib:IncorrectCVC"; + +@implementation NSError(Stripe) + ++ (NSError *)stp_errorFromStripeResponse:(NSDictionary *)jsonDictionary { + NSDictionary *errorDictionary = jsonDictionary[@"error"]; + if (!errorDictionary) { + return nil; + } + NSString *type = errorDictionary[@"type"]; + NSString *devMessage = errorDictionary[@"message"]; + NSString *parameter = errorDictionary[@"param"]; + NSInteger code = 0; + + // There should always be a message and type for the error + if (devMessage == nil || type == nil) { + NSDictionary *userInfo = @{ + NSLocalizedDescriptionKey: [self stp_unexpectedErrorMessage], + STPErrorMessageKey: @"Could not interpret the error response that was returned from Stripe." + }; + return [[self alloc] initWithDomain:StripeDomain code:STPAPIError userInfo:userInfo]; + } + + NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; + userInfo[STPErrorMessageKey] = devMessage; + + if (parameter) { + userInfo[STPErrorParameterKey] = [STPFormEncoder stringByReplacingSnakeCaseWithCamelCase:parameter]; + } + + if ([type isEqualToString:@"api_error"]) { + code = STPAPIError; + userInfo[NSLocalizedDescriptionKey] = [self stp_unexpectedErrorMessage]; + } else if ([type isEqualToString:@"invalid_request_error"]) { + code = STPInvalidRequestError; + userInfo[NSLocalizedDescriptionKey] = devMessage; + } else if ([type isEqualToString:@"card_error"]) { + code = STPCardError; + NSDictionary *errorCodes = @{ + @"incorrect_number": @{@"code": STPIncorrectNumber, @"message": [self stp_cardErrorInvalidNumberUserMessage]}, + @"invalid_number": @{@"code": STPInvalidNumber, @"message": [self stp_cardErrorInvalidNumberUserMessage]}, + @"invalid_expiry_month": @{@"code": STPInvalidExpMonth, @"message": [self stp_cardErrorInvalidExpMonthUserMessage]}, + @"invalid_expiry_year": @{@"code": STPInvalidExpYear, @"message": [self stp_cardErrorInvalidExpYearUserMessage]}, + @"invalid_cvc": @{@"code": STPInvalidCVC, @"message": [self stp_cardInvalidCVCUserMessage]}, + @"expired_card": @{@"code": STPExpiredCard, @"message": [self stp_cardErrorExpiredCardUserMessage]}, + @"incorrect_cvc": @{@"code": STPIncorrectCVC, @"message": [self stp_cardInvalidCVCUserMessage]}, + @"card_declined": @{@"code": STPCardDeclined, @"message": [self stp_cardErrorDeclinedUserMessage]}, + @"processing_error": @{@"code": STPProcessingError, @"message": [self stp_cardErrorProcessingErrorUserMessage]}, + }; + NSDictionary *codeMapEntry = errorCodes[errorDictionary[@"code"]]; + + if (codeMapEntry) { + userInfo[STPCardErrorCodeKey] = codeMapEntry[@"code"]; + userInfo[NSLocalizedDescriptionKey] = codeMapEntry[@"message"]; + } else { + userInfo[STPCardErrorCodeKey] = errorDictionary[@"code"]; + userInfo[NSLocalizedDescriptionKey] = devMessage; + } + } + + return [[self alloc] initWithDomain:StripeDomain code:code userInfo:userInfo]; +} + ++ (nonnull NSError *)stp_genericFailedToParseResponseError { + NSDictionary *userInfo = @{ + NSLocalizedDescriptionKey: [self stp_unexpectedErrorMessage], + STPErrorMessageKey: @"The response from Stripe failed to get parsed into valid JSON." + }; + return [[self alloc] initWithDomain:StripeDomain code:STPAPIError userInfo:userInfo]; +} + +- (BOOL)stp_isUnknownCheckoutError { + return self.code == STPCheckoutUnknownError; +} + +- (BOOL)stp_isURLSessionCancellationError { + return [self.domain isEqualToString:NSURLErrorDomain] && self.code == NSURLErrorCancelled; +} + +#pragma mark Strings + ++ (nonnull NSString *)stp_cardErrorInvalidNumberUserMessage { + return @"Your_cards_number_is_invalid"; +} + ++ (nonnull NSString *)stp_cardInvalidCVCUserMessage { + return @"Your_cards_security_code_is_invalid"; +} + ++ (nonnull NSString *)stp_cardErrorInvalidExpMonthUserMessage { + return @"Your_cards_expiration_month_is_invalid"; +} + ++ (nonnull NSString *)stp_cardErrorInvalidExpYearUserMessage { + return @"Your_cards_expiration_year_is_invalid"; +} + ++ (nonnull NSString *)stp_cardErrorExpiredCardUserMessage { + return @"Your_card_has_expired"; +} + ++ (nonnull NSString *)stp_cardErrorDeclinedUserMessage { + return @"Your_card_was_declined"; +} + ++ (nonnull NSString *)stp_unexpectedErrorMessage { + return @"Error.Generic"; +} + ++ (nonnull NSString *)stp_cardErrorProcessingErrorUserMessage { + return @"Error.Generic"; +} + +@end diff --git a/core-xprojects/Stripe/Stripe.xcodeproj/project.pbxproj b/core-xprojects/Stripe/Stripe.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..42b387d62e --- /dev/null +++ b/core-xprojects/Stripe/Stripe.xcodeproj/project.pbxproj @@ -0,0 +1,921 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 278FA3B825E8D47400280629 /* StripeError.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA39F25E8D47400280629 /* StripeError.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 278FA3BA25E8D47400280629 /* STPFormEncodable.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3A125E8D47400280629 /* STPFormEncodable.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 278FA3BB25E8D47400280629 /* STPBankAccount.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3A225E8D47400280629 /* STPBankAccount.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 278FA3BC25E8D47400280629 /* STPAPIResponseDecodable.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3A325E8D47400280629 /* STPAPIResponseDecodable.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 278FA3BD25E8D47400280629 /* STPCardValidationState.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3A425E8D47400280629 /* STPCardValidationState.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 278FA3BE25E8D47400280629 /* STPSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3A525E8D47400280629 /* STPSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 278FA3BF25E8D47400280629 /* STPCardBrand.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3A625E8D47400280629 /* STPCardBrand.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 278FA3C125E8D47400280629 /* STPBankAccountParams.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3A825E8D47400280629 /* STPBankAccountParams.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 278FA3C225E8D47400280629 /* STPCardParams.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3A925E8D47400280629 /* STPCardParams.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 278FA3C425E8D47400280629 /* STPCard.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3AB25E8D47400280629 /* STPCard.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 278FA3C525E8D47400280629 /* STPPhoneNumberValidator.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3AC25E8D47400280629 /* STPPhoneNumberValidator.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 278FA3C625E8D47400280629 /* STPCustomer.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3AD25E8D47400280629 /* STPCustomer.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 278FA3C725E8D47400280629 /* STPBlocks.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3AE25E8D47400280629 /* STPBlocks.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 278FA3C825E8D47400280629 /* STPBINRange.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3AF25E8D47400280629 /* STPBINRange.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 278FA3C925E8D47400280629 /* STPPaymentMethod.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3B025E8D47400280629 /* STPPaymentMethod.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 278FA3CA25E8D47400280629 /* Stripe.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3B125E8D47400280629 /* Stripe.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 278FA3CB25E8D47400280629 /* STPPostalCodeValidator.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3B225E8D47400280629 /* STPPostalCodeValidator.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 278FA3CC25E8D47400280629 /* STPCardValidator.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3B325E8D47400280629 /* STPCardValidator.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 278FA3CD25E8D47400280629 /* STPToken.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3B425E8D47400280629 /* STPToken.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 278FA3CE25E8D47400280629 /* STPAPIClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3B525E8D47400280629 /* STPAPIClient.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 278FA3CF25E8D47400280629 /* STPPaymentConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3B625E8D47400280629 /* STPPaymentConfiguration.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 278FA3D025E8D47400280629 /* STPBackendAPIAdapter.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3B725E8D47400280629 /* STPBackendAPIAdapter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 278FA3FD25E8D49C00280629 /* STPPaymentCardTextFieldViewModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3D325E8D49C00280629 /* STPPaymentCardTextFieldViewModel.h */; }; + 278FA3FE25E8D49C00280629 /* STPCustomer.m in Sources */ = {isa = PBXBuildFile; fileRef = 278FA3D425E8D49C00280629 /* STPCustomer.m */; }; + 278FA3FF25E8D49C00280629 /* STPAPIPostRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 278FA3D525E8D49C00280629 /* STPAPIPostRequest.m */; }; + 278FA40025E8D49C00280629 /* NSString+Stripe_CardBrands.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3D625E8D49C00280629 /* NSString+Stripe_CardBrands.h */; }; + 278FA40125E8D49C00280629 /* STPBINRange.m in Sources */ = {isa = PBXBuildFile; fileRef = 278FA3D725E8D49C00280629 /* STPBINRange.m */; }; + 278FA40225E8D49C00280629 /* NSString+Stripe.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3D825E8D49C00280629 /* NSString+Stripe.h */; }; + 278FA40325E8D49C00280629 /* STPImageLibrary.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3D925E8D49C00280629 /* STPImageLibrary.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 278FA40425E8D49C00280629 /* STPFormEncoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3DA25E8D49C00280629 /* STPFormEncoder.h */; }; + 278FA40525E8D49C00280629 /* STPAPIClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 278FA3DB25E8D49C00280629 /* STPAPIClient.m */; }; + 278FA40625E8D49C00280629 /* STPToken.m in Sources */ = {isa = PBXBuildFile; fileRef = 278FA3DC25E8D49C00280629 /* STPToken.m */; }; + 278FA40725E8D49C00280629 /* STPCardValidator.m in Sources */ = {isa = PBXBuildFile; fileRef = 278FA3DD25E8D49C00280629 /* STPCardValidator.m */; }; + 278FA40825E8D49C00280629 /* NSDictionary+Stripe.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3DE25E8D49C00280629 /* NSDictionary+Stripe.h */; }; + 278FA40925E8D49C00280629 /* STPPostalCodeValidator.m in Sources */ = {isa = PBXBuildFile; fileRef = 278FA3DF25E8D49C00280629 /* STPPostalCodeValidator.m */; }; + 278FA40A25E8D49C00280629 /* STPDispatchFunctions.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3E025E8D49C00280629 /* STPDispatchFunctions.h */; }; + 278FA40B25E8D49C00280629 /* STPPaymentConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 278FA3E125E8D49C00280629 /* STPPaymentConfiguration.m */; }; + 278FA40C25E8D49C00280629 /* PKPayment+Stripe.m in Sources */ = {isa = PBXBuildFile; fileRef = 278FA3E225E8D49C00280629 /* PKPayment+Stripe.m */; }; + 278FA40D25E8D49C00280629 /* STPDelegateProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = 278FA3E325E8D49C00280629 /* STPDelegateProxy.m */; }; + 278FA40F25E8D49C00280629 /* STPPaymentCardTextFieldViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 278FA3E525E8D49C00280629 /* STPPaymentCardTextFieldViewModel.m */; }; + 278FA41025E8D49C00280629 /* STPBankAccount.m in Sources */ = {isa = PBXBuildFile; fileRef = 278FA3E625E8D49C00280629 /* STPBankAccount.m */; }; + 278FA41125E8D49C00280629 /* StripeError.m in Sources */ = {isa = PBXBuildFile; fileRef = 278FA3E725E8D49C00280629 /* StripeError.m */; }; + 278FA41325E8D49C00280629 /* STPFormEncoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 278FA3E925E8D49C00280629 /* STPFormEncoder.m */; }; + 278FA41425E8D49C00280629 /* NSString+Stripe.m in Sources */ = {isa = PBXBuildFile; fileRef = 278FA3EA25E8D49C00280629 /* NSString+Stripe.m */; }; + 278FA41525E8D49C00280629 /* STPImageLibrary.m in Sources */ = {isa = PBXBuildFile; fileRef = 278FA3EB25E8D49C00280629 /* STPImageLibrary.m */; }; + 278FA41625E8D49C00280629 /* STPWeakStrongMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3EC25E8D49C00280629 /* STPWeakStrongMacros.h */; }; + 278FA41725E8D49C00280629 /* NSString+Stripe_CardBrands.m in Sources */ = {isa = PBXBuildFile; fileRef = 278FA3ED25E8D49C00280629 /* NSString+Stripe_CardBrands.m */; }; + 278FA41825E8D49C00280629 /* STPAPIClient+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3EE25E8D49C00280629 /* STPAPIClient+Private.h */; }; + 278FA41925E8D49C00280629 /* STPAPIPostRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3EF25E8D49C00280629 /* STPAPIPostRequest.h */; }; + 278FA41A25E8D49C00280629 /* STPDispatchFunctions.m in Sources */ = {isa = PBXBuildFile; fileRef = 278FA3F025E8D49C00280629 /* STPDispatchFunctions.m */; }; + 278FA41B25E8D49C00280629 /* STPCardParams.m in Sources */ = {isa = PBXBuildFile; fileRef = 278FA3F125E8D49C00280629 /* STPCardParams.m */; }; + 278FA41D25E8D49C00280629 /* STPBankAccountParams.m in Sources */ = {isa = PBXBuildFile; fileRef = 278FA3F325E8D49C00280629 /* STPBankAccountParams.m */; }; + 278FA41E25E8D49C00280629 /* NSDictionary+Stripe.m in Sources */ = {isa = PBXBuildFile; fileRef = 278FA3F425E8D49C00280629 /* NSDictionary+Stripe.m */; }; + 278FA42025E8D49C00280629 /* STPPaymentConfiguration+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3F625E8D49C00280629 /* STPPaymentConfiguration+Private.h */; }; + 278FA42125E8D49C00280629 /* STPImageLibrary+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3F725E8D49C00280629 /* STPImageLibrary+Private.h */; }; + 278FA42325E8D49C00280629 /* STPDelegateProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3F925E8D49C00280629 /* STPDelegateProxy.h */; }; + 278FA42425E8D49C00280629 /* STPPhoneNumberValidator.m in Sources */ = {isa = PBXBuildFile; fileRef = 278FA3FA25E8D49C00280629 /* STPPhoneNumberValidator.m */; }; + 278FA42525E8D49C00280629 /* STPCard.m in Sources */ = {isa = PBXBuildFile; fileRef = 278FA3FB25E8D49C00280629 /* STPCard.m */; }; + 278FA42625E8D49C00280629 /* PKPayment+Stripe.h in Headers */ = {isa = PBXBuildFile; fileRef = 278FA3FC25E8D49C00280629 /* PKPayment+Stripe.h */; }; + 279A1DFA25E8E327007D48E7 /* STPAddress.m in Sources */ = {isa = PBXBuildFile; fileRef = 279A1DF925E8E327007D48E7 /* STPAddress.m */; }; + 279A1DFE25E8E38A007D48E7 /* STPAddress.h in Headers */ = {isa = PBXBuildFile; fileRef = 279A1DFD25E8E38A007D48E7 /* STPAddress.h */; settings = {ATTRIBUTES = (Public, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 278FA2F525E8CF9F00280629 /* Stripe.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Stripe.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 278FA39F25E8D47400280629 /* StripeError.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StripeError.h; sourceTree = ""; }; + 278FA3A125E8D47400280629 /* STPFormEncodable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPFormEncodable.h; sourceTree = ""; }; + 278FA3A225E8D47400280629 /* STPBankAccount.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPBankAccount.h; sourceTree = ""; }; + 278FA3A325E8D47400280629 /* STPAPIResponseDecodable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPAPIResponseDecodable.h; sourceTree = ""; }; + 278FA3A425E8D47400280629 /* STPCardValidationState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPCardValidationState.h; sourceTree = ""; }; + 278FA3A525E8D47400280629 /* STPSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPSource.h; sourceTree = ""; }; + 278FA3A625E8D47400280629 /* STPCardBrand.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPCardBrand.h; sourceTree = ""; }; + 278FA3A825E8D47400280629 /* STPBankAccountParams.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPBankAccountParams.h; sourceTree = ""; }; + 278FA3A925E8D47400280629 /* STPCardParams.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPCardParams.h; sourceTree = ""; }; + 278FA3AB25E8D47400280629 /* STPCard.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPCard.h; sourceTree = ""; }; + 278FA3AC25E8D47400280629 /* STPPhoneNumberValidator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPPhoneNumberValidator.h; sourceTree = ""; }; + 278FA3AD25E8D47400280629 /* STPCustomer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPCustomer.h; sourceTree = ""; }; + 278FA3AE25E8D47400280629 /* STPBlocks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPBlocks.h; sourceTree = ""; }; + 278FA3AF25E8D47400280629 /* STPBINRange.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPBINRange.h; sourceTree = ""; }; + 278FA3B025E8D47400280629 /* STPPaymentMethod.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPPaymentMethod.h; sourceTree = ""; }; + 278FA3B125E8D47400280629 /* Stripe.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Stripe.h; sourceTree = ""; }; + 278FA3B225E8D47400280629 /* STPPostalCodeValidator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPPostalCodeValidator.h; sourceTree = ""; }; + 278FA3B325E8D47400280629 /* STPCardValidator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPCardValidator.h; sourceTree = ""; }; + 278FA3B425E8D47400280629 /* STPToken.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPToken.h; sourceTree = ""; }; + 278FA3B525E8D47400280629 /* STPAPIClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPAPIClient.h; sourceTree = ""; }; + 278FA3B625E8D47400280629 /* STPPaymentConfiguration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPPaymentConfiguration.h; sourceTree = ""; }; + 278FA3B725E8D47400280629 /* STPBackendAPIAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPBackendAPIAdapter.h; sourceTree = ""; }; + 278FA3D325E8D49C00280629 /* STPPaymentCardTextFieldViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPPaymentCardTextFieldViewModel.h; sourceTree = ""; }; + 278FA3D425E8D49C00280629 /* STPCustomer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPCustomer.m; sourceTree = ""; }; + 278FA3D525E8D49C00280629 /* STPAPIPostRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPAPIPostRequest.m; sourceTree = ""; }; + 278FA3D625E8D49C00280629 /* NSString+Stripe_CardBrands.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+Stripe_CardBrands.h"; sourceTree = ""; }; + 278FA3D725E8D49C00280629 /* STPBINRange.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPBINRange.m; sourceTree = ""; }; + 278FA3D825E8D49C00280629 /* NSString+Stripe.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+Stripe.h"; sourceTree = ""; }; + 278FA3D925E8D49C00280629 /* STPImageLibrary.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPImageLibrary.h; sourceTree = ""; }; + 278FA3DA25E8D49C00280629 /* STPFormEncoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPFormEncoder.h; sourceTree = ""; }; + 278FA3DB25E8D49C00280629 /* STPAPIClient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPAPIClient.m; sourceTree = ""; }; + 278FA3DC25E8D49C00280629 /* STPToken.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPToken.m; sourceTree = ""; }; + 278FA3DD25E8D49C00280629 /* STPCardValidator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPCardValidator.m; sourceTree = ""; }; + 278FA3DE25E8D49C00280629 /* NSDictionary+Stripe.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+Stripe.h"; sourceTree = ""; }; + 278FA3DF25E8D49C00280629 /* STPPostalCodeValidator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPPostalCodeValidator.m; sourceTree = ""; }; + 278FA3E025E8D49C00280629 /* STPDispatchFunctions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPDispatchFunctions.h; sourceTree = ""; }; + 278FA3E125E8D49C00280629 /* STPPaymentConfiguration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPPaymentConfiguration.m; sourceTree = ""; }; + 278FA3E225E8D49C00280629 /* PKPayment+Stripe.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "PKPayment+Stripe.m"; sourceTree = ""; }; + 278FA3E325E8D49C00280629 /* STPDelegateProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPDelegateProxy.m; sourceTree = ""; }; + 278FA3E525E8D49C00280629 /* STPPaymentCardTextFieldViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPPaymentCardTextFieldViewModel.m; sourceTree = ""; }; + 278FA3E625E8D49C00280629 /* STPBankAccount.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPBankAccount.m; sourceTree = ""; }; + 278FA3E725E8D49C00280629 /* StripeError.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StripeError.m; sourceTree = ""; }; + 278FA3E925E8D49C00280629 /* STPFormEncoder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPFormEncoder.m; sourceTree = ""; }; + 278FA3EA25E8D49C00280629 /* NSString+Stripe.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+Stripe.m"; sourceTree = ""; }; + 278FA3EB25E8D49C00280629 /* STPImageLibrary.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPImageLibrary.m; sourceTree = ""; }; + 278FA3EC25E8D49C00280629 /* STPWeakStrongMacros.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPWeakStrongMacros.h; sourceTree = ""; }; + 278FA3ED25E8D49C00280629 /* NSString+Stripe_CardBrands.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+Stripe_CardBrands.m"; sourceTree = ""; }; + 278FA3EE25E8D49C00280629 /* STPAPIClient+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "STPAPIClient+Private.h"; sourceTree = ""; }; + 278FA3EF25E8D49C00280629 /* STPAPIPostRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPAPIPostRequest.h; sourceTree = ""; }; + 278FA3F025E8D49C00280629 /* STPDispatchFunctions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPDispatchFunctions.m; sourceTree = ""; }; + 278FA3F125E8D49C00280629 /* STPCardParams.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPCardParams.m; sourceTree = ""; }; + 278FA3F325E8D49C00280629 /* STPBankAccountParams.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPBankAccountParams.m; sourceTree = ""; }; + 278FA3F425E8D49C00280629 /* NSDictionary+Stripe.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+Stripe.m"; sourceTree = ""; }; + 278FA3F625E8D49C00280629 /* STPPaymentConfiguration+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "STPPaymentConfiguration+Private.h"; sourceTree = ""; }; + 278FA3F725E8D49C00280629 /* STPImageLibrary+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "STPImageLibrary+Private.h"; sourceTree = ""; }; + 278FA3F925E8D49C00280629 /* STPDelegateProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPDelegateProxy.h; sourceTree = ""; }; + 278FA3FA25E8D49C00280629 /* STPPhoneNumberValidator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPPhoneNumberValidator.m; sourceTree = ""; }; + 278FA3FB25E8D49C00280629 /* STPCard.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = STPCard.m; sourceTree = ""; }; + 278FA3FC25E8D49C00280629 /* PKPayment+Stripe.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "PKPayment+Stripe.h"; sourceTree = ""; }; + 279A1DF325E8E307007D48E7 /* STPAddress.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = STPAddress.h; path = ../../../../../../../.Trash/STPAddress.h; sourceTree = ""; }; + 279A1DF925E8E327007D48E7 /* STPAddress.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPAddress.m; sourceTree = ""; }; + 279A1DFD25E8E38A007D48E7 /* STPAddress.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = STPAddress.h; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 278FA2F225E8CF9F00280629 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 278FA2EB25E8CF9F00280629 = { + isa = PBXGroup; + children = ( + 278FA3D225E8D49C00280629 /* Sources */, + 278FA39D25E8D47400280629 /* PublicHeaders */, + 278FA2F625E8CF9F00280629 /* Products */, + ); + sourceTree = ""; + }; + 278FA2F625E8CF9F00280629 /* Products */ = { + isa = PBXGroup; + children = ( + 278FA2F525E8CF9F00280629 /* Stripe.framework */, + ); + name = Products; + sourceTree = ""; + }; + 278FA39D25E8D47400280629 /* PublicHeaders */ = { + isa = PBXGroup; + children = ( + 278FA39E25E8D47400280629 /* Stripe */, + ); + path = PublicHeaders; + sourceTree = ""; + }; + 278FA39E25E8D47400280629 /* Stripe */ = { + isa = PBXGroup; + children = ( + 278FA39F25E8D47400280629 /* StripeError.h */, + 278FA3D925E8D49C00280629 /* STPImageLibrary.h */, + 278FA3A125E8D47400280629 /* STPFormEncodable.h */, + 278FA3A225E8D47400280629 /* STPBankAccount.h */, + 278FA3A325E8D47400280629 /* STPAPIResponseDecodable.h */, + 278FA3A425E8D47400280629 /* STPCardValidationState.h */, + 278FA3A525E8D47400280629 /* STPSource.h */, + 278FA3A625E8D47400280629 /* STPCardBrand.h */, + 278FA3A825E8D47400280629 /* STPBankAccountParams.h */, + 278FA3A925E8D47400280629 /* STPCardParams.h */, + 279A1DF325E8E307007D48E7 /* STPAddress.h */, + 278FA3AB25E8D47400280629 /* STPCard.h */, + 278FA3AC25E8D47400280629 /* STPPhoneNumberValidator.h */, + 278FA3AD25E8D47400280629 /* STPCustomer.h */, + 278FA3AE25E8D47400280629 /* STPBlocks.h */, + 278FA3AF25E8D47400280629 /* STPBINRange.h */, + 278FA3B025E8D47400280629 /* STPPaymentMethod.h */, + 278FA3B125E8D47400280629 /* Stripe.h */, + 278FA3B225E8D47400280629 /* STPPostalCodeValidator.h */, + 278FA3B325E8D47400280629 /* STPCardValidator.h */, + 278FA3B425E8D47400280629 /* STPToken.h */, + 278FA3B525E8D47400280629 /* STPAPIClient.h */, + 278FA3B625E8D47400280629 /* STPPaymentConfiguration.h */, + 278FA3B725E8D47400280629 /* STPBackendAPIAdapter.h */, + 279A1DFD25E8E38A007D48E7 /* STPAddress.h */, + ); + path = Stripe; + sourceTree = ""; + }; + 278FA3D225E8D49C00280629 /* Sources */ = { + isa = PBXGroup; + children = ( + 278FA3D325E8D49C00280629 /* STPPaymentCardTextFieldViewModel.h */, + 278FA3D425E8D49C00280629 /* STPCustomer.m */, + 278FA3D525E8D49C00280629 /* STPAPIPostRequest.m */, + 278FA3D625E8D49C00280629 /* NSString+Stripe_CardBrands.h */, + 278FA3D725E8D49C00280629 /* STPBINRange.m */, + 278FA3D825E8D49C00280629 /* NSString+Stripe.h */, + 278FA3DA25E8D49C00280629 /* STPFormEncoder.h */, + 278FA3DB25E8D49C00280629 /* STPAPIClient.m */, + 278FA3DC25E8D49C00280629 /* STPToken.m */, + 278FA3DD25E8D49C00280629 /* STPCardValidator.m */, + 278FA3DE25E8D49C00280629 /* NSDictionary+Stripe.h */, + 278FA3DF25E8D49C00280629 /* STPPostalCodeValidator.m */, + 278FA3E025E8D49C00280629 /* STPDispatchFunctions.h */, + 278FA3E125E8D49C00280629 /* STPPaymentConfiguration.m */, + 278FA3E225E8D49C00280629 /* PKPayment+Stripe.m */, + 278FA3E325E8D49C00280629 /* STPDelegateProxy.m */, + 278FA3E525E8D49C00280629 /* STPPaymentCardTextFieldViewModel.m */, + 278FA3E625E8D49C00280629 /* STPBankAccount.m */, + 278FA3E725E8D49C00280629 /* StripeError.m */, + 278FA3E925E8D49C00280629 /* STPFormEncoder.m */, + 278FA3EA25E8D49C00280629 /* NSString+Stripe.m */, + 278FA3EB25E8D49C00280629 /* STPImageLibrary.m */, + 278FA3EC25E8D49C00280629 /* STPWeakStrongMacros.h */, + 278FA3ED25E8D49C00280629 /* NSString+Stripe_CardBrands.m */, + 278FA3EE25E8D49C00280629 /* STPAPIClient+Private.h */, + 278FA3EF25E8D49C00280629 /* STPAPIPostRequest.h */, + 278FA3F025E8D49C00280629 /* STPDispatchFunctions.m */, + 278FA3F125E8D49C00280629 /* STPCardParams.m */, + 278FA3F325E8D49C00280629 /* STPBankAccountParams.m */, + 278FA3F425E8D49C00280629 /* NSDictionary+Stripe.m */, + 278FA3F625E8D49C00280629 /* STPPaymentConfiguration+Private.h */, + 278FA3F725E8D49C00280629 /* STPImageLibrary+Private.h */, + 278FA3F925E8D49C00280629 /* STPDelegateProxy.h */, + 278FA3FA25E8D49C00280629 /* STPPhoneNumberValidator.m */, + 278FA3FB25E8D49C00280629 /* STPCard.m */, + 279A1DF925E8E327007D48E7 /* STPAddress.m */, + 278FA3FC25E8D49C00280629 /* PKPayment+Stripe.h */, + ); + path = Sources; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 278FA2F025E8CF9F00280629 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 278FA3C925E8D47400280629 /* STPPaymentMethod.h in Headers */, + 278FA42025E8D49C00280629 /* STPPaymentConfiguration+Private.h in Headers */, + 278FA3CA25E8D47400280629 /* Stripe.h in Headers */, + 278FA41625E8D49C00280629 /* STPWeakStrongMacros.h in Headers */, + 278FA3C825E8D47400280629 /* STPBINRange.h in Headers */, + 278FA3CF25E8D47400280629 /* STPPaymentConfiguration.h in Headers */, + 278FA40825E8D49C00280629 /* NSDictionary+Stripe.h in Headers */, + 278FA40225E8D49C00280629 /* NSString+Stripe.h in Headers */, + 278FA3BE25E8D47400280629 /* STPSource.h in Headers */, + 278FA41925E8D49C00280629 /* STPAPIPostRequest.h in Headers */, + 278FA42625E8D49C00280629 /* PKPayment+Stripe.h in Headers */, + 278FA3CE25E8D47400280629 /* STPAPIClient.h in Headers */, + 278FA3B825E8D47400280629 /* StripeError.h in Headers */, + 278FA3CD25E8D47400280629 /* STPToken.h in Headers */, + 278FA3BD25E8D47400280629 /* STPCardValidationState.h in Headers */, + 278FA42325E8D49C00280629 /* STPDelegateProxy.h in Headers */, + 278FA40A25E8D49C00280629 /* STPDispatchFunctions.h in Headers */, + 278FA3C625E8D47400280629 /* STPCustomer.h in Headers */, + 278FA3CC25E8D47400280629 /* STPCardValidator.h in Headers */, + 278FA3FD25E8D49C00280629 /* STPPaymentCardTextFieldViewModel.h in Headers */, + 278FA3D025E8D47400280629 /* STPBackendAPIAdapter.h in Headers */, + 279A1DFE25E8E38A007D48E7 /* STPAddress.h in Headers */, + 278FA3CB25E8D47400280629 /* STPPostalCodeValidator.h in Headers */, + 278FA40325E8D49C00280629 /* STPImageLibrary.h in Headers */, + 278FA40425E8D49C00280629 /* STPFormEncoder.h in Headers */, + 278FA3C725E8D47400280629 /* STPBlocks.h in Headers */, + 278FA41825E8D49C00280629 /* STPAPIClient+Private.h in Headers */, + 278FA40025E8D49C00280629 /* NSString+Stripe_CardBrands.h in Headers */, + 278FA42125E8D49C00280629 /* STPImageLibrary+Private.h in Headers */, + 278FA3BF25E8D47400280629 /* STPCardBrand.h in Headers */, + 278FA3C125E8D47400280629 /* STPBankAccountParams.h in Headers */, + 278FA3C425E8D47400280629 /* STPCard.h in Headers */, + 278FA3C525E8D47400280629 /* STPPhoneNumberValidator.h in Headers */, + 278FA3BB25E8D47400280629 /* STPBankAccount.h in Headers */, + 278FA3C225E8D47400280629 /* STPCardParams.h in Headers */, + 278FA3BA25E8D47400280629 /* STPFormEncodable.h in Headers */, + 278FA3BC25E8D47400280629 /* STPAPIResponseDecodable.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 278FA2F425E8CF9F00280629 /* Stripe */ = { + isa = PBXNativeTarget; + buildConfigurationList = 278FA2FD25E8CF9F00280629 /* Build configuration list for PBXNativeTarget "Stripe" */; + buildPhases = ( + 278FA2F025E8CF9F00280629 /* Headers */, + 278FA2F125E8CF9F00280629 /* Sources */, + 278FA2F225E8CF9F00280629 /* Frameworks */, + 278FA2F325E8CF9F00280629 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Stripe; + productName = Stripe; + productReference = 278FA2F525E8CF9F00280629 /* Stripe.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 278FA2EC25E8CF9F00280629 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1220; + TargetAttributes = { + 278FA2F425E8CF9F00280629 = { + CreatedOnToolsVersion = 12.2; + }; + }; + }; + buildConfigurationList = 278FA2EF25E8CF9F00280629 /* Build configuration list for PBXProject "Stripe" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 278FA2EB25E8CF9F00280629; + productRefGroup = 278FA2F625E8CF9F00280629 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 278FA2F425E8CF9F00280629 /* Stripe */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 278FA2F325E8CF9F00280629 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 278FA2F125E8CF9F00280629 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 278FA40725E8D49C00280629 /* STPCardValidator.m in Sources */, + 278FA42425E8D49C00280629 /* STPPhoneNumberValidator.m in Sources */, + 278FA40B25E8D49C00280629 /* STPPaymentConfiguration.m in Sources */, + 278FA42525E8D49C00280629 /* STPCard.m in Sources */, + 278FA41125E8D49C00280629 /* StripeError.m in Sources */, + 278FA40C25E8D49C00280629 /* PKPayment+Stripe.m in Sources */, + 278FA41B25E8D49C00280629 /* STPCardParams.m in Sources */, + 278FA41325E8D49C00280629 /* STPFormEncoder.m in Sources */, + 278FA40D25E8D49C00280629 /* STPDelegateProxy.m in Sources */, + 278FA3FF25E8D49C00280629 /* STPAPIPostRequest.m in Sources */, + 278FA41525E8D49C00280629 /* STPImageLibrary.m in Sources */, + 278FA40525E8D49C00280629 /* STPAPIClient.m in Sources */, + 278FA41725E8D49C00280629 /* NSString+Stripe_CardBrands.m in Sources */, + 278FA41025E8D49C00280629 /* STPBankAccount.m in Sources */, + 278FA40625E8D49C00280629 /* STPToken.m in Sources */, + 278FA40125E8D49C00280629 /* STPBINRange.m in Sources */, + 278FA41E25E8D49C00280629 /* NSDictionary+Stripe.m in Sources */, + 278FA41425E8D49C00280629 /* NSString+Stripe.m in Sources */, + 278FA40F25E8D49C00280629 /* STPPaymentCardTextFieldViewModel.m in Sources */, + 278FA41A25E8D49C00280629 /* STPDispatchFunctions.m in Sources */, + 278FA3FE25E8D49C00280629 /* STPCustomer.m in Sources */, + 279A1DFA25E8E327007D48E7 /* STPAddress.m in Sources */, + 278FA40925E8D49C00280629 /* STPPostalCodeValidator.m in Sources */, + 278FA41D25E8D49C00280629 /* STPBankAccountParams.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 278FA2FB25E8CF9F00280629 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + 278FA2FC25E8CF9F00280629 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + 278FA2FE25E8CF9F00280629 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Stripe/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + PRODUCT_BUNDLE_IDENTIFIER = org.Telegram.Stripe; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = DebugAppStore; + }; + 278FA2FF25E8CF9F00280629 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Stripe/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + PRODUCT_BUNDLE_IDENTIFIER = org.Telegram.Stripe; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = ReleaseAppStore; + }; + 278FA42D25E8D57800280629 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + 278FA42E25E8D57800280629 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Stripe/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + PRODUCT_BUNDLE_IDENTIFIER = org.Telegram.Stripe; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = DebugHockeyapp; + }; + 278FA42F25E8D58000280629 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + 278FA43025E8D58000280629 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Stripe/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + PRODUCT_BUNDLE_IDENTIFIER = org.Telegram.Stripe; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = ReleaseHockeyapp; + }; + 278FA43125E8D59200280629 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + 278FA43225E8D59200280629 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Stripe/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + PRODUCT_BUNDLE_IDENTIFIER = org.Telegram.Stripe; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = HockeyappMacAlpha; + }; + 278FA43325E8D59900280629 /* Github */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + 278FA43425E8D59900280629 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Stripe/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + PRODUCT_BUNDLE_IDENTIFIER = org.Telegram.Stripe; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = Github; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 278FA2EF25E8CF9F00280629 /* Build configuration list for PBXProject "Stripe" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 278FA2FB25E8CF9F00280629 /* DebugAppStore */, + 278FA43325E8D59900280629 /* Github */, + 278FA2FC25E8CF9F00280629 /* ReleaseAppStore */, + 278FA42D25E8D57800280629 /* DebugHockeyapp */, + 278FA42F25E8D58000280629 /* ReleaseHockeyapp */, + 278FA43125E8D59200280629 /* HockeyappMacAlpha */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; + 278FA2FD25E8CF9F00280629 /* Build configuration list for PBXNativeTarget "Stripe" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 278FA2FE25E8CF9F00280629 /* DebugAppStore */, + 278FA43425E8D59900280629 /* Github */, + 278FA2FF25E8CF9F00280629 /* ReleaseAppStore */, + 278FA42E25E8D57800280629 /* DebugHockeyapp */, + 278FA43025E8D58000280629 /* ReleaseHockeyapp */, + 278FA43225E8D59200280629 /* HockeyappMacAlpha */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; +/* End XCConfigurationList section */ + }; + rootObject = 278FA2EC25E8CF9F00280629 /* Project object */; +} diff --git a/core-xprojects/Stripe/Stripe.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/Stripe/Stripe.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/core-xprojects/Stripe/Stripe.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/Stripe/Stripe.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/Stripe/Stripe.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/Stripe/Stripe.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/Stripe/Stripe/Info.plist b/core-xprojects/Stripe/Stripe/Info.plist new file mode 100644 index 0000000000..9bcb244429 --- /dev/null +++ b/core-xprojects/Stripe/Stripe/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/core-xprojects/Stripe/Stripe/Stripe.h b/core-xprojects/Stripe/Stripe/Stripe.h new file mode 100644 index 0000000000..71fc604aff --- /dev/null +++ b/core-xprojects/Stripe/Stripe/Stripe.h @@ -0,0 +1,18 @@ +// +// Stripe.h +// Stripe +// +// Created by Mikhail Filimonov on 26.02.2021. +// + +#import + +//! Project version number for Stripe. +FOUNDATION_EXPORT double StripeVersionNumber; + +//! Project version string for Stripe. +FOUNDATION_EXPORT const unsigned char StripeVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/core-xprojects/TelegramApi/TelegramApi/Info.plist b/core-xprojects/TelegramApi/TelegramApi/Info.plist new file mode 100644 index 0000000000..e1fe4cfb7b --- /dev/null +++ b/core-xprojects/TelegramApi/TelegramApi/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/core-xprojects/TelegramApi/TelegramApi/TelegramApi.h b/core-xprojects/TelegramApi/TelegramApi/TelegramApi.h new file mode 100644 index 0000000000..64322dc581 --- /dev/null +++ b/core-xprojects/TelegramApi/TelegramApi/TelegramApi.h @@ -0,0 +1,19 @@ +// +// TelegramApi.h +// TelegramApi +// +// Created by Peter on 6/16/19. +// Copyright © 2019 Telegram LLP. All rights reserved. +// + +#import + +//! Project version number for TelegramApi. +FOUNDATION_EXPORT double TelegramApiVersionNumber; + +//! Project version string for TelegramApi. +FOUNDATION_EXPORT const unsigned char TelegramApiVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/project.pbxproj b/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..1604c5fdbd --- /dev/null +++ b/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/project.pbxproj @@ -0,0 +1,811 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + A701F93A236C210C002ABF81 /* TelegramApi.h in Headers */ = {isa = PBXBuildFile; fileRef = A701F939236C210C002ABF81 /* TelegramApi.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A701F93B236C21AE002ABF81 /* SecretApiLayer8.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035734522B5C9BF00F0920D /* SecretApiLayer8.swift */; }; + A701F93C236C21AE002ABF81 /* SecretApiLayer46.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035734422B5C9BF00F0920D /* SecretApiLayer46.swift */; }; + A701F93D236C21AE002ABF81 /* SecretApiLayer73.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035734622B5C9BF00F0920D /* SecretApiLayer73.swift */; }; + A701F93E236C21AE002ABF81 /* SecretApiLayer101.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09E9601A22C2BE4900B13673 /* SecretApiLayer101.swift */; }; + A701F93F236C21AE002ABF81 /* DeserializeFunctionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035733C22B5C39100F0920D /* DeserializeFunctionResponse.swift */; }; + A701F940236C21AE002ABF81 /* TelegramApiLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035733A22B5C31400F0920D /* TelegramApiLogger.swift */; }; + A701F941236C21AE002ABF81 /* Buffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035733822B5C2E200F0920D /* Buffer.swift */; }; + A701F942236C21AE002ABF81 /* Api0.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035733022B5C29900F0920D /* Api0.swift */; }; + A701F943236C21AE002ABF81 /* Api1.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035733222B5C29900F0920D /* Api1.swift */; }; + A701F944236C21AE002ABF81 /* Api2.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035733122B5C29900F0920D /* Api2.swift */; }; + A701F945236C21AE002ABF81 /* Api3.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035733322B5C29900F0920D /* Api3.swift */; }; + A731925225D516C900218E0E /* Api4.swift in Sources */ = {isa = PBXBuildFile; fileRef = A731925125D516C900218E0E /* Api4.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 09E9601A22C2BE4900B13673 /* SecretApiLayer101.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecretApiLayer101.swift; sourceTree = ""; }; + A701F937236C1FFB002ABF81 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A701F939236C210C002ABF81 /* TelegramApi.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TelegramApi.h; sourceTree = ""; }; + A731925125D516C900218E0E /* Api4.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Api4.swift; sourceTree = ""; }; + D035733022B5C29900F0920D /* Api0.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Api0.swift; sourceTree = ""; }; + D035733122B5C29900F0920D /* Api2.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Api2.swift; sourceTree = ""; }; + D035733222B5C29900F0920D /* Api1.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Api1.swift; sourceTree = ""; }; + D035733322B5C29900F0920D /* Api3.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Api3.swift; sourceTree = ""; }; + D035733822B5C2E200F0920D /* Buffer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Buffer.swift; sourceTree = ""; }; + D035733A22B5C31400F0920D /* TelegramApiLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelegramApiLogger.swift; sourceTree = ""; }; + D035733C22B5C39100F0920D /* DeserializeFunctionResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeserializeFunctionResponse.swift; sourceTree = ""; }; + D035734422B5C9BF00F0920D /* SecretApiLayer46.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecretApiLayer46.swift; sourceTree = ""; }; + D035734522B5C9BF00F0920D /* SecretApiLayer8.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecretApiLayer8.swift; sourceTree = ""; }; + D035734622B5C9BF00F0920D /* SecretApiLayer73.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecretApiLayer73.swift; sourceTree = ""; }; + D0CC4AD922BA46F30088F36D /* TelegramApi.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TelegramApi.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D0CC4AD022BA46F30088F36D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A701F936236C1FFB002ABF81 /* TelegramApi */ = { + isa = PBXGroup; + children = ( + A701F939236C210C002ABF81 /* TelegramApi.h */, + A701F937236C1FFB002ABF81 /* Info.plist */, + ); + path = TelegramApi; + sourceTree = SOURCE_ROOT; + }; + D035731522B5C1FC00F0920D /* TelegramApi */ = { + isa = PBXGroup; + children = ( + A701F936236C1FFB002ABF81 /* TelegramApi */, + D035732122B5C1FC00F0920D /* Sources */, + D035732022B5C1FC00F0920D /* Products */, + ); + name = TelegramApi; + sourceTree = ""; + }; + D035732022B5C1FC00F0920D /* Products */ = { + isa = PBXGroup; + children = ( + D0CC4AD922BA46F30088F36D /* TelegramApi.framework */, + ); + name = Products; + sourceTree = ""; + }; + D035732122B5C1FC00F0920D /* Sources */ = { + isa = PBXGroup; + children = ( + D035734522B5C9BF00F0920D /* SecretApiLayer8.swift */, + D035734422B5C9BF00F0920D /* SecretApiLayer46.swift */, + D035734622B5C9BF00F0920D /* SecretApiLayer73.swift */, + 09E9601A22C2BE4900B13673 /* SecretApiLayer101.swift */, + D035733C22B5C39100F0920D /* DeserializeFunctionResponse.swift */, + D035733A22B5C31400F0920D /* TelegramApiLogger.swift */, + D035733822B5C2E200F0920D /* Buffer.swift */, + D035733022B5C29900F0920D /* Api0.swift */, + D035733222B5C29900F0920D /* Api1.swift */, + D035733122B5C29900F0920D /* Api2.swift */, + D035733322B5C29900F0920D /* Api3.swift */, + A731925125D516C900218E0E /* Api4.swift */, + ); + name = Sources; + path = "../../submodules/telegram-ios/submodules/TelegramApi/Sources"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + D0CC4AC322BA46F30088F36D /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + A701F93A236C210C002ABF81 /* TelegramApi.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + D0CC4AC222BA46F30088F36D /* TelegramApi */ = { + isa = PBXNativeTarget; + buildConfigurationList = D0CC4AD222BA46F30088F36D /* Build configuration list for PBXNativeTarget "TelegramApi" */; + buildPhases = ( + D0CC4AC322BA46F30088F36D /* Headers */, + D0CC4AC522BA46F30088F36D /* Sources */, + D0CC4AD022BA46F30088F36D /* Frameworks */, + D0CC4AD122BA46F30088F36D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = TelegramApi; + productName = TelegramApi; + productReference = D0CC4AD922BA46F30088F36D /* TelegramApi.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D035731622B5C1FC00F0920D /* Project object */ = { + isa = PBXProject; + attributes = { + DefaultBuildSystemTypeForWorkspace = Latest; + LastUpgradeCheck = 1010; + ORGANIZATIONNAME = "Telegram LLP"; + }; + buildConfigurationList = D035731922B5C1FC00F0920D /* Build configuration list for PBXProject "TelegramApi_Xcode" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = D035731522B5C1FC00F0920D /* TelegramApi */; + productRefGroup = D035732022B5C1FC00F0920D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D0CC4AC222BA46F30088F36D /* TelegramApi */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D0CC4AD122BA46F30088F36D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D0CC4AC522BA46F30088F36D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A701F93B236C21AE002ABF81 /* SecretApiLayer8.swift in Sources */, + A701F93C236C21AE002ABF81 /* SecretApiLayer46.swift in Sources */, + A701F93D236C21AE002ABF81 /* SecretApiLayer73.swift in Sources */, + A731925225D516C900218E0E /* Api4.swift in Sources */, + A701F93E236C21AE002ABF81 /* SecretApiLayer101.swift in Sources */, + A701F93F236C21AE002ABF81 /* DeserializeFunctionResponse.swift in Sources */, + A701F940236C21AE002ABF81 /* TelegramApiLogger.swift in Sources */, + A701F941236C21AE002ABF81 /* Buffer.swift in Sources */, + A701F942236C21AE002ABF81 /* Api0.swift in Sources */, + A701F943236C21AE002ABF81 /* Api1.swift in Sources */, + A701F944236C21AE002ABF81 /* Api2.swift in Sources */, + A701F945236C21AE002ABF81 /* Api3.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A7F282E3238EAB6200742C20 /* Github */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + A7F282E4238EAB6200742C20 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = TelegramApi/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramApi; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = macosx; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Github; + }; + D0276B8422C17FAA003155D8 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + D0276B8622C17FAA003155D8 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = TelegramApi/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramApi; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = macosx; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = ReleaseAppStore; + }; + D0276B8722C17FB2003155D8 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + D0276B8922C17FB2003155D8 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = TelegramApi/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramApi; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = macosx; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = ReleaseHockeyapp; + }; + D035732522B5C1FC00F0920D /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + D0CC4AA522BA44AD0088F36D /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + D0CC4AA722BA44B70088F36D /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + D0CC4AD322BA46F30088F36D /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_OPTIMIZATION_LEVEL = s; + INFOPLIST_FILE = TelegramApi/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramApi; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = DebugHockeyapp; + }; + D0CC4AD422BA46F30088F36D /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_OPTIMIZATION_LEVEL = s; + INFOPLIST_FILE = TelegramApi/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramApi; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = HockeyappMacAlpha; + }; + D0CC4AD522BA46F30088F36D /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = TelegramApi/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramApi; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = macosx; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = DebugAppStore; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D035731922B5C1FC00F0920D /* Build configuration list for PBXProject "TelegramApi_Xcode" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D035732522B5C1FC00F0920D /* DebugHockeyapp */, + D0CC4AA722BA44B70088F36D /* HockeyappMacAlpha */, + D0CC4AA522BA44AD0088F36D /* DebugAppStore */, + A7F282E3238EAB6200742C20 /* Github */, + D0276B8422C17FAA003155D8 /* ReleaseAppStore */, + D0276B8722C17FB2003155D8 /* ReleaseHockeyapp */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = DebugHockeyapp; + }; + D0CC4AD222BA46F30088F36D /* Build configuration list for PBXNativeTarget "TelegramApi" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0CC4AD322BA46F30088F36D /* DebugHockeyapp */, + D0CC4AD422BA46F30088F36D /* HockeyappMacAlpha */, + D0CC4AD522BA46F30088F36D /* DebugAppStore */, + A7F282E4238EAB6200742C20 /* Github */, + D0276B8622C17FAA003155D8 /* ReleaseAppStore */, + D0276B8922C17FB2003155D8 /* ReleaseHockeyapp */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = DebugHockeyapp; + }; +/* End XCConfigurationList section */ + }; + rootObject = D035731622B5C1FC00F0920D /* Project object */; +} diff --git a/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..94b2795e22 --- /dev/null +++ b/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,4 @@ + + + diff --git a/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate b/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000..8134c0d9ba Binary files /dev/null and b/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/TelegramApi.xcscheme b/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/TelegramApi.xcscheme new file mode 100644 index 0000000000..8274d109b7 --- /dev/null +++ b/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/TelegramApi.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/TelegramApiMac.xcscheme b/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/TelegramApiMac.xcscheme new file mode 100644 index 0000000000..8274d109b7 --- /dev/null +++ b/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/TelegramApiMac.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/xcuserdata/mikhailfilimonov.xcuserdatad/xcschemes/xcschememanagement.plist b/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/xcuserdata/mikhailfilimonov.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100755 index 0000000000..60d336eeb1 --- /dev/null +++ b/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/xcuserdata/mikhailfilimonov.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,19 @@ + + + + + SchemeUserState + + TelegramApi.xcscheme_^#shared#^_ + + orderHint + 30 + + TelegramApiMac.xcscheme_^#shared#^_ + + orderHint + 31 + + + + diff --git a/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/TelegramApi.xcscheme b/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/TelegramApi.xcscheme new file mode 100644 index 0000000000..49a8e70e5b --- /dev/null +++ b/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/TelegramApi.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist b/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000000..4eca84ac26 --- /dev/null +++ b/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,24 @@ + + + + + SchemeUserState + + TelegramApi.xcscheme + + isShown + + orderHint + 12 + + + SuppressBuildableAutocreation + + D0CC4AC222BA46F30088F36D + + primary + + + + + diff --git a/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/xcuserdata/telegram.xcuserdatad/xcschemes/xcschememanagement.plist b/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/xcuserdata/telegram.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000000..eb013ad372 --- /dev/null +++ b/core-xprojects/TelegramApi/TelegramApi_Xcode.xcodeproj/xcuserdata/telegram.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,19 @@ + + + + + SchemeUserState + + TelegramApi.xcscheme_^#shared#^_ + + orderHint + 36 + + TelegramApiMac.xcscheme_^#shared#^_ + + orderHint + 37 + + + + diff --git a/core-xprojects/TelegramCore/TelegramCore/Info.plist b/core-xprojects/TelegramCore/TelegramCore/Info.plist new file mode 100644 index 0000000000..6c6dba7bb1 --- /dev/null +++ b/core-xprojects/TelegramCore/TelegramCore/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2016 Peter. All rights reserved. + NSPrincipalClass + + + diff --git a/core-xprojects/TelegramCore/TelegramCore/TelegramCore.h b/core-xprojects/TelegramCore/TelegramCore/TelegramCore.h new file mode 100644 index 0000000000..f6bcd8339e --- /dev/null +++ b/core-xprojects/TelegramCore/TelegramCore/TelegramCore.h @@ -0,0 +1,18 @@ +// +// TelegramCoreMac.h +// TelegramCoreMac +// +// Created by Peter on 9/5/16. +// Copyright © 2016 Peter. All rights reserved. +// + +#import + +//! Project version number for TelegramCoreMac. +FOUNDATION_EXPORT double TelegramCoreVersionNumber; + +//! Project version string for TelegramCoreMac. +FOUNDATION_EXPORT const unsigned char TelegramCoreVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + diff --git a/core-xprojects/TelegramCore/TelegramCore/TelegramCore.xcconfig b/core-xprojects/TelegramCore/TelegramCore/TelegramCore.xcconfig new file mode 100644 index 0000000000..c3b3ada1b4 --- /dev/null +++ b/core-xprojects/TelegramCore/TelegramCore/TelegramCore.xcconfig @@ -0,0 +1,2 @@ +SWIFT_INCLUDE_PATHS = $(SRCROOT)/TelegramCore +MODULEMAP_PRIVATE_FILE = $(SRCROOT)/TelegramCore/module.private.modulemap diff --git a/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/project.pbxproj b/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..6f399191e3 --- /dev/null +++ b/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/project.pbxproj @@ -0,0 +1,3043 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 279881BE260DCA7400C2AF8E /* TelegramCore.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 279881BD260DCA7400C2AF8E /* TelegramCore.xcconfig */; }; + 27BA025A26E4ECD2008FC1A3 /* Wallpapers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA004126E4ECD1008FC1A3 /* Wallpapers.swift */; }; + 27BA025B26E4ECD2008FC1A3 /* Authorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA004226E4ECD1008FC1A3 /* Authorization.swift */; }; + 27BA025C26E4ECD2008FC1A3 /* ContentSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA004426E4ECD1008FC1A3 /* ContentSettings.swift */; }; + 27BA025D26E4ECD2008FC1A3 /* LoggingSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA004526E4ECD1008FC1A3 /* LoggingSettings.swift */; }; + 27BA025E26E4ECD2008FC1A3 /* LimitsConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA004626E4ECD1008FC1A3 /* LimitsConfiguration.swift */; }; + 27BA025F26E4ECD2008FC1A3 /* ProxySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA004726E4ECD1008FC1A3 /* ProxySettings.swift */; }; + 27BA026026E4ECD2008FC1A3 /* PeerContactSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA004826E4ECD1008FC1A3 /* PeerContactSettings.swift */; }; + 27BA026126E4ECD2008FC1A3 /* CacheStorageSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA004926E4ECD1008FC1A3 /* CacheStorageSettings.swift */; }; + 27BA026226E4ECD2008FC1A3 /* NetworkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA004A26E4ECD1008FC1A3 /* NetworkSettings.swift */; }; + 27BA026326E4ECD2008FC1A3 /* ContentPrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA004B26E4ECD1008FC1A3 /* ContentPrivacySettings.swift */; }; + 27BA026426E4ECD2008FC1A3 /* VoipConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA004C26E4ECD1008FC1A3 /* VoipConfiguration.swift */; }; + 27BA026526E4ECD2008FC1A3 /* AutodownloadSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA004D26E4ECD1008FC1A3 /* AutodownloadSettings.swift */; }; + 27BA026626E4ECD2008FC1A3 /* GlobalNotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA004E26E4ECD1008FC1A3 /* GlobalNotificationSettings.swift */; }; + 27BA026726E4ECD2008FC1A3 /* PrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA004F26E4ECD1008FC1A3 /* PrivacySettings.swift */; }; + 27BA026826E4ECD2008FC1A3 /* PeerStatistics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA005026E4ECD1008FC1A3 /* PeerStatistics.swift */; }; + 27BA026926E4ECD2008FC1A3 /* NotificationAutolockReportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA005226E4ECD1008FC1A3 /* NotificationAutolockReportManager.swift */; }; + 27BA026A26E4ECD2008FC1A3 /* MacInternalUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA005326E4ECD1008FC1A3 /* MacInternalUpdater.swift */; }; + 27BA026B26E4ECD2008FC1A3 /* GroupReturnAndLeft.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA005426E4ECD1008FC1A3 /* GroupReturnAndLeft.swift */; }; + 27BA026C26E4ECD2008FC1A3 /* StandaloneSendMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA005626E4ECD1008FC1A3 /* StandaloneSendMessage.swift */; }; + 27BA026D26E4ECD2008FC1A3 /* ChatUpdatingMessageMedia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA005726E4ECD1008FC1A3 /* ChatUpdatingMessageMedia.swift */; }; + 27BA026E26E4ECD2008FC1A3 /* EnqueueMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA005826E4ECD1008FC1A3 /* EnqueueMessage.swift */; }; + 27BA026F26E4ECD2008FC1A3 /* PendingMessageUploadedContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA005926E4ECD1008FC1A3 /* PendingMessageUploadedContent.swift */; }; + 27BA027026E4ECD2008FC1A3 /* PendingUpdateMessageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA005A26E4ECD1008FC1A3 /* PendingUpdateMessageManager.swift */; }; + 27BA027126E4ECD2008FC1A3 /* StandaloneUploadedMedia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA005B26E4ECD1008FC1A3 /* StandaloneUploadedMedia.swift */; }; + 27BA027226E4ECD2008FC1A3 /* RequestEditMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA005C26E4ECD1008FC1A3 /* RequestEditMessage.swift */; }; + 27BA027326E4ECD2008FC1A3 /* MessageStatistics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA005D26E4ECD1008FC1A3 /* MessageStatistics.swift */; }; + 27BA027426E4ECD2008FC1A3 /* Suggestions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA005E26E4ECD1008FC1A3 /* Suggestions.swift */; }; + 27BA027526E4ECD2008FC1A3 /* Themes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA005F26E4ECD1008FC1A3 /* Themes.swift */; }; + 27BA027626E4ECD2008FC1A3 /* NetworkType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA006126E4ECD1008FC1A3 /* NetworkType.swift */; }; + 27BA027726E4ECD2008FC1A3 /* MultipartFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA006226E4ECD1008FC1A3 /* MultipartFetch.swift */; }; + 27BA027826E4ECD2008FC1A3 /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA006326E4ECD1008FC1A3 /* Network.swift */; }; + 27BA027926E4ECD2008FC1A3 /* MultiplexedRequestManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA006426E4ECD1008FC1A3 /* MultiplexedRequestManager.swift */; }; + 27BA027A26E4ECD2008FC1A3 /* Download.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA006526E4ECD1008FC1A3 /* Download.swift */; }; + 27BA027B26E4ECD2008FC1A3 /* MultipartUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA006626E4ECD1008FC1A3 /* MultipartUpload.swift */; }; + 27BA027C26E4ECD2008FC1A3 /* FetchedMediaResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA006726E4ECD1008FC1A3 /* FetchedMediaResource.swift */; }; + 27BA027D26E4ECD2008FC1A3 /* ProxyServersStatuses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA006826E4ECD1008FC1A3 /* ProxyServersStatuses.swift */; }; + 27BA027E26E4ECD2008FC1A3 /* FetchHttpResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA006926E4ECD1008FC1A3 /* FetchHttpResource.swift */; }; + 27BA027F26E4ECD2008FC1A3 /* UpdatePeers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA006A26E4ECD1008FC1A3 /* UpdatePeers.swift */; }; + 27BA028026E4ECD2008FC1A3 /* SplitTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA006B26E4ECD1008FC1A3 /* SplitTest.swift */; }; + 27BA028126E4ECD2008FC1A3 /* ImageRepresentationsUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA006D26E4ECD1008FC1A3 /* ImageRepresentationsUtils.swift */; }; + 27BA028226E4ECD2008FC1A3 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA006E26E4ECD1008FC1A3 /* Coding.swift */; }; + 27BA028326E4ECD2008FC1A3 /* MediaResourceNetworkStatsTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA006F26E4ECD1008FC1A3 /* MediaResourceNetworkStatsTag.swift */; }; + 27BA028426E4ECD2008FC1A3 /* StringFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA007026E4ECD1008FC1A3 /* StringFormat.swift */; }; + 27BA028526E4ECD2008FC1A3 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA007126E4ECD1008FC1A3 /* Log.swift */; }; + 27BA028626E4ECD2008FC1A3 /* PeerUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA007226E4ECD1008FC1A3 /* PeerUtils.swift */; }; + 27BA028726E4ECD2008FC1A3 /* DecryptedResourceData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA007326E4ECD1008FC1A3 /* DecryptedResourceData.swift */; }; + 27BA028826E4ECD2008FC1A3 /* MD5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA007426E4ECD1008FC1A3 /* MD5.swift */; }; + 27BA028926E4ECD2008FC1A3 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA007526E4ECD1008FC1A3 /* JSON.swift */; }; + 27BA028A26E4ECD2008FC1A3 /* UpdateMessageMedia.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA007626E4ECD1008FC1A3 /* UpdateMessageMedia.swift */; }; + 27BA028B26E4ECD2008FC1A3 /* MemoryBufferExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA007726E4ECD1008FC1A3 /* MemoryBufferExtensions.swift */; }; + 27BA028C26E4ECD2008FC1A3 /* CanSendMessagesToPeer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA007826E4ECD1008FC1A3 /* CanSendMessagesToPeer.swift */; }; + 27BA028D26E4ECD2008FC1A3 /* MessageUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA007926E4ECD1008FC1A3 /* MessageUtils.swift */; }; + 27BA028E26E4ECD2008FC1A3 /* ManagedSynchronizeSavedStickersOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA007B26E4ECD1008FC1A3 /* ManagedSynchronizeSavedStickersOperations.swift */; }; + 27BA028F26E4ECD2008FC1A3 /* AccountState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA007C26E4ECD1008FC1A3 /* AccountState.swift */; }; + 27BA029026E4ECD2008FC1A3 /* ProcessSecretChatIncomingDecryptedOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA007D26E4ECD1008FC1A3 /* ProcessSecretChatIncomingDecryptedOperations.swift */; }; + 27BA029126E4ECD2008FC1A3 /* HistoryViewStateValidation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA007E26E4ECD1008FC1A3 /* HistoryViewStateValidation.swift */; }; + 27BA029226E4ECD2008FC1A3 /* SynchronizeEmojiKeywordsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA007F26E4ECD1008FC1A3 /* SynchronizeEmojiKeywordsOperation.swift */; }; + 27BA029326E4ECD2008FC1A3 /* ApplyUpdateMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA008026E4ECD1008FC1A3 /* ApplyUpdateMessage.swift */; }; + 27BA029426E4ECD2008FC1A3 /* ChatHistoryPreloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA008126E4ECD1008FC1A3 /* ChatHistoryPreloadManager.swift */; }; + 27BA029526E4ECD2008FC1A3 /* AppConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA008226E4ECD1008FC1A3 /* AppConfiguration.swift */; }; + 27BA029626E4ECD2008FC1A3 /* SynchronizeAppLogEventsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA008326E4ECD1008FC1A3 /* SynchronizeAppLogEventsOperation.swift */; }; + 27BA029726E4ECD2008FC1A3 /* SynchronizeMarkAllUnseenPersonalMessagesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA008426E4ECD1008FC1A3 /* SynchronizeMarkAllUnseenPersonalMessagesOperation.swift */; }; + 27BA029826E4ECD2008FC1A3 /* ManagedSynchronizeEmojiKeywordsOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA008526E4ECD1008FC1A3 /* ManagedSynchronizeEmojiKeywordsOperations.swift */; }; + 27BA029926E4ECD2008FC1A3 /* UpdatesApiUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA008626E4ECD1008FC1A3 /* UpdatesApiUtils.swift */; }; + 27BA029A26E4ECD2008FC1A3 /* ManagedNotificationSettingsBehaviors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA008726E4ECD1008FC1A3 /* ManagedNotificationSettingsBehaviors.swift */; }; + 27BA029B26E4ECD2008FC1A3 /* MessageMediaPreuploadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA008826E4ECD1008FC1A3 /* MessageMediaPreuploadManager.swift */; }; + 27BA029C26E4ECD2008FC1A3 /* ManagedPendingPeerNotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA008926E4ECD1008FC1A3 /* ManagedPendingPeerNotificationSettings.swift */; }; + 27BA029D26E4ECD2008FC1A3 /* SynchronizeRecentlyUsedMediaOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA008A26E4ECD1008FC1A3 /* SynchronizeRecentlyUsedMediaOperations.swift */; }; + 27BA029E26E4ECD2008FC1A3 /* ManagedMessageHistoryHoles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA008B26E4ECD1008FC1A3 /* ManagedMessageHistoryHoles.swift */; }; + 27BA029F26E4ECD2008FC1A3 /* ManagedConsumePersonalMessagesActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA008C26E4ECD1008FC1A3 /* ManagedConsumePersonalMessagesActions.swift */; }; + 27BA02A026E4ECD2008FC1A3 /* ManagedProxyInfoUpdates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA008D26E4ECD1008FC1A3 /* ManagedProxyInfoUpdates.swift */; }; + 27BA02A126E4ECD2008FC1A3 /* ManagedAppConfigurationUpdates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA008E26E4ECD2008FC1A3 /* ManagedAppConfigurationUpdates.swift */; }; + 27BA02A226E4ECD2008FC1A3 /* AccountStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA008F26E4ECD2008FC1A3 /* AccountStateManager.swift */; }; + 27BA02A326E4ECD2008FC1A3 /* CachedSentMediaReferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA009026E4ECD2008FC1A3 /* CachedSentMediaReferences.swift */; }; + 27BA02A426E4ECD2008FC1A3 /* ManagedLocalizationUpdatesOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA009126E4ECD2008FC1A3 /* ManagedLocalizationUpdatesOperations.swift */; }; + 27BA02A526E4ECD2008FC1A3 /* FetchChatList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA009226E4ECD2008FC1A3 /* FetchChatList.swift */; }; + 27BA02A626E4ECD2008FC1A3 /* ManagedGlobalNotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA009326E4ECD2008FC1A3 /* ManagedGlobalNotificationSettings.swift */; }; + 27BA02A726E4ECD2008FC1A3 /* FetchSecretFileResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA009426E4ECD2008FC1A3 /* FetchSecretFileResource.swift */; }; + 27BA02A826E4ECD2008FC1A3 /* ChannelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA009526E4ECD2008FC1A3 /* ChannelState.swift */; }; + 27BA02A926E4ECD2008FC1A3 /* ManagedSynchronizeRecentlyUsedMediaOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA009626E4ECD2008FC1A3 /* ManagedSynchronizeRecentlyUsedMediaOperations.swift */; }; + 27BA02AA26E4ECD2008FC1A3 /* ManagedRecentStickers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA009726E4ECD2008FC1A3 /* ManagedRecentStickers.swift */; }; + 27BA02AB26E4ECD2008FC1A3 /* ManagedChatListHoles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA009826E4ECD2008FC1A3 /* ManagedChatListHoles.swift */; }; + 27BA02AC26E4ECD2008FC1A3 /* ManagedSynchronizeMarkAllUnseenPersonalMessagesOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA009926E4ECD2008FC1A3 /* ManagedSynchronizeMarkAllUnseenPersonalMessagesOperations.swift */; }; + 27BA02AD26E4ECD2008FC1A3 /* ManagedServiceViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA009A26E4ECD2008FC1A3 /* ManagedServiceViews.swift */; }; + 27BA02AE26E4ECD2008FC1A3 /* PeerInputActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA009B26E4ECD2008FC1A3 /* PeerInputActivityManager.swift */; }; + 27BA02AF26E4ECD2008FC1A3 /* ManagedSynchronizePinnedChatsOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA009C26E4ECD2008FC1A3 /* ManagedSynchronizePinnedChatsOperations.swift */; }; + 27BA02B026E4ECD2008FC1A3 /* SynchronizeSavedStickersOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA009D26E4ECD2008FC1A3 /* SynchronizeSavedStickersOperation.swift */; }; + 27BA02B126E4ECD2008FC1A3 /* MessageReactions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA009E26E4ECD2008FC1A3 /* MessageReactions.swift */; }; + 27BA02B226E4ECD2008FC1A3 /* ManagedCloudChatRemoveMessagesOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA009F26E4ECD2008FC1A3 /* ManagedCloudChatRemoveMessagesOperations.swift */; }; + 27BA02B326E4ECD2008FC1A3 /* PendingMessageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00A026E4ECD2008FC1A3 /* PendingMessageManager.swift */; }; + 27BA02B426E4ECD2008FC1A3 /* StickerManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00A126E4ECD2008FC1A3 /* StickerManagement.swift */; }; + 27BA02B526E4ECD2008FC1A3 /* InitializeAccountAfterLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00A226E4ECD2008FC1A3 /* InitializeAccountAfterLogin.swift */; }; + 27BA02B626E4ECD2008FC1A3 /* UpdateGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00A326E4ECD2008FC1A3 /* UpdateGroup.swift */; }; + 27BA02B726E4ECD2008FC1A3 /* ManagedSynchronizeGroupMessageStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00A426E4ECD2008FC1A3 /* ManagedSynchronizeGroupMessageStats.swift */; }; + 27BA02B826E4ECD2008FC1A3 /* Fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00A526E4ECD2008FC1A3 /* Fetch.swift */; }; + 27BA02B926E4ECD2008FC1A3 /* ManagedSynchronizeMarkFeaturedStickerPacksAsSeenOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00A626E4ECD2008FC1A3 /* ManagedSynchronizeMarkFeaturedStickerPacksAsSeenOperations.swift */; }; + 27BA02BA26E4ECD2008FC1A3 /* SynchronizeLocalizationUpdatesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00A726E4ECD2008FC1A3 /* SynchronizeLocalizationUpdatesOperation.swift */; }; + 27BA02BB26E4ECD2008FC1A3 /* ManagedAnimatedEmojiUpdates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00A826E4ECD2008FC1A3 /* ManagedAnimatedEmojiUpdates.swift */; }; + 27BA02BC26E4ECD2008FC1A3 /* UpdateMessageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00A926E4ECD2008FC1A3 /* UpdateMessageService.swift */; }; + 27BA02BD26E4ECD2008FC1A3 /* SynchronizeInstalledStickerPacksOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00AA26E4ECD2008FC1A3 /* SynchronizeInstalledStickerPacksOperation.swift */; }; + 27BA02BE26E4ECD2008FC1A3 /* ManagedLocalInputActivities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00AB26E4ECD2008FC1A3 /* ManagedLocalInputActivities.swift */; }; + 27BA02BF26E4ECD2008FC1A3 /* ManagedSynchronizeInstalledStickerPacksOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00AC26E4ECD2008FC1A3 /* ManagedSynchronizeInstalledStickerPacksOperations.swift */; }; + 27BA02C026E4ECD2008FC1A3 /* ManagedSynchronizeGroupedPeersOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00AD26E4ECD2008FC1A3 /* ManagedSynchronizeGroupedPeersOperations.swift */; }; + 27BA02C126E4ECD2008FC1A3 /* CallSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00AE26E4ECD2008FC1A3 /* CallSessionManager.swift */; }; + 27BA02C226E4ECD2008FC1A3 /* ManagedSecretChatOutgoingOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00AF26E4ECD2008FC1A3 /* ManagedSecretChatOutgoingOperations.swift */; }; + 27BA02C326E4ECD2008FC1A3 /* ManagedSynchronizeConsumeMessageContentsOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00B026E4ECD2008FC1A3 /* ManagedSynchronizeConsumeMessageContentsOperations.swift */; }; + 27BA02C426E4ECD2008FC1A3 /* PeerInputActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00B126E4ECD2008FC1A3 /* PeerInputActivity.swift */; }; + 27BA02C526E4ECD2008FC1A3 /* SynchronizeSavedGifsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00B226E4ECD2008FC1A3 /* SynchronizeSavedGifsOperation.swift */; }; + 27BA02C626E4ECD2008FC1A3 /* ManagedAutoremoveMessageOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00B326E4ECD2008FC1A3 /* ManagedAutoremoveMessageOperations.swift */; }; + 27BA02C726E4ECD2008FC1A3 /* AppUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00B426E4ECD2008FC1A3 /* AppUpdate.swift */; }; + 27BA02C826E4ECD2008FC1A3 /* AccountViewTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00B526E4ECD2008FC1A3 /* AccountViewTracker.swift */; }; + 27BA02C926E4ECD2008FC1A3 /* SynchronizeChatInputStateOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00B626E4ECD2008FC1A3 /* SynchronizeChatInputStateOperation.swift */; }; + 27BA02CA26E4ECD2008FC1A3 /* SynchronizeGroupedPeersOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00B726E4ECD2008FC1A3 /* SynchronizeGroupedPeersOperation.swift */; }; + 27BA02CB26E4ECD2008FC1A3 /* ProcessSecretChatIncomingEncryptedOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00B826E4ECD2008FC1A3 /* ProcessSecretChatIncomingEncryptedOperations.swift */; }; + 27BA02CC26E4ECD2008FC1A3 /* UnauthorizedAccountStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00B926E4ECD2008FC1A3 /* UnauthorizedAccountStateManager.swift */; }; + 27BA02CD26E4ECD2008FC1A3 /* ContactSyncManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00BA26E4ECD2008FC1A3 /* ContactSyncManager.swift */; }; + 27BA02CE26E4ECD2008FC1A3 /* ManagedAccountPresence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00BB26E4ECD2008FC1A3 /* ManagedAccountPresence.swift */; }; + 27BA02CF26E4ECD2008FC1A3 /* ManagedSynchronizePeerReadStates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00BC26E4ECD2008FC1A3 /* ManagedSynchronizePeerReadStates.swift */; }; + 27BA02D026E4ECD2008FC1A3 /* SynchronizeConsumeMessageContentsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00BD26E4ECD2008FC1A3 /* SynchronizeConsumeMessageContentsOperation.swift */; }; + 27BA02D126E4ECD2008FC1A3 /* CloudChatRemoveMessagesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00BE26E4ECD2008FC1A3 /* CloudChatRemoveMessagesOperation.swift */; }; + 27BA02D226E4ECD2008FC1A3 /* Holes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00BF26E4ECD2008FC1A3 /* Holes.swift */; }; + 27BA02D326E4ECD2008FC1A3 /* ManagedSynchronizeChatInputStateOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00C026E4ECD2008FC1A3 /* ManagedSynchronizeChatInputStateOperations.swift */; }; + 27BA02D426E4ECD2008FC1A3 /* AppChangelogState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00C126E4ECD2008FC1A3 /* AppChangelogState.swift */; }; + 27BA02D526E4ECD2008FC1A3 /* ManagedSynchronizeAppLogEventsOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00C226E4ECD2008FC1A3 /* ManagedSynchronizeAppLogEventsOperations.swift */; }; + 27BA02D626E4ECD2008FC1A3 /* SynchronizePeerReadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00C326E4ECD2008FC1A3 /* SynchronizePeerReadState.swift */; }; + 27BA02D726E4ECD2008FC1A3 /* ManagedSynchronizeSavedGifsOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00C426E4ECD2008FC1A3 /* ManagedSynchronizeSavedGifsOperations.swift */; }; + 27BA02D826E4ECD2008FC1A3 /* ManagedConfigurationUpdates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00C526E4ECD2008FC1A3 /* ManagedConfigurationUpdates.swift */; }; + 27BA02D926E4ECD2008FC1A3 /* ManagedVoipConfigurationUpdates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00C626E4ECD2008FC1A3 /* ManagedVoipConfigurationUpdates.swift */; }; + 27BA02DA26E4ECD2008FC1A3 /* AppChangelog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00C726E4ECD2008FC1A3 /* AppChangelog.swift */; }; + 27BA02DB26E4ECD2008FC1A3 /* AccountStateManagementUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00C826E4ECD2008FC1A3 /* AccountStateManagementUtils.swift */; }; + 27BA02DC26E4ECD2008FC1A3 /* ManagedAutodownloadSettingsUpdates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00C926E4ECD2008FC1A3 /* ManagedAutodownloadSettingsUpdates.swift */; }; + 27BA02DD26E4ECD2008FC1A3 /* Serialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00CA26E4ECD2008FC1A3 /* Serialization.swift */; }; + 27BA02DE26E4ECD2008FC1A3 /* LoadedPeer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00CB26E4ECD2008FC1A3 /* LoadedPeer.swift */; }; + 27BA02DF26E4ECD2008FC1A3 /* LoadedPeerFromMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00CC26E4ECD2008FC1A3 /* LoadedPeerFromMessage.swift */; }; + 27BA02E026E4ECD2008FC1A3 /* UpdatePeerChatInterfaceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00CD26E4ECD2008FC1A3 /* UpdatePeerChatInterfaceState.swift */; }; + 27BA02E126E4ECD2008FC1A3 /* TelegramEngineCalls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00D026E4ECD2008FC1A3 /* TelegramEngineCalls.swift */; }; + 27BA02E226E4ECD2008FC1A3 /* GroupCalls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00D126E4ECD2008FC1A3 /* GroupCalls.swift */; }; + 27BA02E326E4ECD2008FC1A3 /* RateCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00D226E4ECD2008FC1A3 /* RateCall.swift */; }; + 27BA02E426E4ECD2008FC1A3 /* SecureIdPersonalDetailsValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00D426E4ECD2008FC1A3 /* SecureIdPersonalDetailsValue.swift */; }; + 27BA02E526E4ECD2008FC1A3 /* SecureIdValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00D526E4ECD2008FC1A3 /* SecureIdValue.swift */; }; + 27BA02E626E4ECD2008FC1A3 /* RequestSecureIdForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00D626E4ECD2008FC1A3 /* RequestSecureIdForm.swift */; }; + 27BA02E726E4ECD2008FC1A3 /* SecureIdAddressValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00D726E4ECD2008FC1A3 /* SecureIdAddressValue.swift */; }; + 27BA02E826E4ECD2008FC1A3 /* SecureIdEmailValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00D826E4ECD2008FC1A3 /* SecureIdEmailValue.swift */; }; + 27BA02E926E4ECD2008FC1A3 /* SecureIdUtilityBillValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00D926E4ECD2008FC1A3 /* SecureIdUtilityBillValue.swift */; }; + 27BA02EA26E4ECD2008FC1A3 /* TelegramEngineSecureId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00DA26E4ECD2008FC1A3 /* TelegramEngineSecureId.swift */; }; + 27BA02EB26E4ECD2008FC1A3 /* SecureIdValueAccessContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00DB26E4ECD2008FC1A3 /* SecureIdValueAccessContext.swift */; }; + 27BA02EC26E4ECD2008FC1A3 /* SecureIdPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00DC26E4ECD2008FC1A3 /* SecureIdPadding.swift */; }; + 27BA02ED26E4ECD2008FC1A3 /* SecureIdConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00DD26E4ECD2008FC1A3 /* SecureIdConfiguration.swift */; }; + 27BA02EE26E4ECD2008FC1A3 /* SaveSecureIdValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00DE26E4ECD2008FC1A3 /* SaveSecureIdValue.swift */; }; + 27BA02EF26E4ECD2008FC1A3 /* SecureIdIDCardValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00DF26E4ECD2008FC1A3 /* SecureIdIDCardValue.swift */; }; + 27BA02F026E4ECD2008FC1A3 /* SecureIdPassportRegistrationValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00E026E4ECD2008FC1A3 /* SecureIdPassportRegistrationValue.swift */; }; + 27BA02F126E4ECD2008FC1A3 /* VerifySecureIdValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00E126E4ECD2008FC1A3 /* VerifySecureIdValue.swift */; }; + 27BA02F226E4ECD2008FC1A3 /* SecureIdDriversLicenseValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00E226E4ECD2008FC1A3 /* SecureIdDriversLicenseValue.swift */; }; + 27BA02F326E4ECD2008FC1A3 /* SecureIdRentalAgreementValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00E326E4ECD2008FC1A3 /* SecureIdRentalAgreementValue.swift */; }; + 27BA02F426E4ECD2008FC1A3 /* GrantSecureIdAccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00E426E4ECD2008FC1A3 /* GrantSecureIdAccess.swift */; }; + 27BA02F526E4ECD2008FC1A3 /* SecureIdForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00E526E4ECD2008FC1A3 /* SecureIdForm.swift */; }; + 27BA02F626E4ECD2008FC1A3 /* SecureIdVerificationDocumentReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00E626E4ECD2008FC1A3 /* SecureIdVerificationDocumentReference.swift */; }; + 27BA02F726E4ECD2008FC1A3 /* SecureIdBankStatementValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00E726E4ECD2008FC1A3 /* SecureIdBankStatementValue.swift */; }; + 27BA02F826E4ECD2008FC1A3 /* AccessSecureId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00E826E4ECD2008FC1A3 /* AccessSecureId.swift */; }; + 27BA02F926E4ECD2008FC1A3 /* SecureFileMediaResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00E926E4ECD2008FC1A3 /* SecureFileMediaResource.swift */; }; + 27BA02FA26E4ECD2008FC1A3 /* UploadSecureIdFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00EA26E4ECD2008FC1A3 /* UploadSecureIdFile.swift */; }; + 27BA02FB26E4ECD2008FC1A3 /* SecureIdValueContentError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00EB26E4ECD2008FC1A3 /* SecureIdValueContentError.swift */; }; + 27BA02FC26E4ECD2008FC1A3 /* SecureIdDataTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00EC26E4ECD2008FC1A3 /* SecureIdDataTypes.swift */; }; + 27BA02FD26E4ECD2008FC1A3 /* SecureIdInternalPassportValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00ED26E4ECD2008FC1A3 /* SecureIdInternalPassportValue.swift */; }; + 27BA02FE26E4ECD2008FC1A3 /* SecureIdPassportValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00EE26E4ECD2008FC1A3 /* SecureIdPassportValue.swift */; }; + 27BA02FF26E4ECD2008FC1A3 /* SecureIdTemporaryRegistrationValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00EF26E4ECD2008FC1A3 /* SecureIdTemporaryRegistrationValue.swift */; }; + 27BA030026E4ECD2008FC1A3 /* SecureIdPhoneValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00F026E4ECD2008FC1A3 /* SecureIdPhoneValue.swift */; }; + 27BA030126E4ECD2008FC1A3 /* TelegramEnginePayments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00F226E4ECD2008FC1A3 /* TelegramEnginePayments.swift */; }; + 27BA030226E4ECD2008FC1A3 /* BotPaymentForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00F326E4ECD2008FC1A3 /* BotPaymentForm.swift */; }; + 27BA030326E4ECD2008FC1A3 /* BankCards.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00F426E4ECD2008FC1A3 /* BankCards.swift */; }; + 27BA030426E4ECD2008FC1A3 /* ImportStickers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00F626E4ECD2008FC1A3 /* ImportStickers.swift */; }; + 27BA030526E4ECD2008FC1A3 /* StickerPack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00F726E4ECD2008FC1A3 /* StickerPack.swift */; }; + 27BA030626E4ECD2008FC1A3 /* SearchStickers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00F826E4ECD2008FC1A3 /* SearchStickers.swift */; }; + 27BA030726E4ECD2008FC1A3 /* TelegramEngineStickers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00F926E4ECD2008FC1A3 /* TelegramEngineStickers.swift */; }; + 27BA030826E4ECD2008FC1A3 /* LoadedStickerPack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00FA26E4ECD2008FC1A3 /* LoadedStickerPack.swift */; }; + 27BA030926E4ECD2008FC1A3 /* StickerPackInteractiveOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00FB26E4ECD2008FC1A3 /* StickerPackInteractiveOperations.swift */; }; + 27BA030A26E4ECD2008FC1A3 /* EmojiKeywords.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00FC26E4ECD2008FC1A3 /* EmojiKeywords.swift */; }; + 27BA030B26E4ECD2008FC1A3 /* ArchivedStickerPacks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00FD26E4ECD2008FC1A3 /* ArchivedStickerPacks.swift */; }; + 27BA030C26E4ECD2008FC1A3 /* CachedStickerPack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00FE26E4ECD2008FC1A3 /* CachedStickerPack.swift */; }; + 27BA030D26E4ECD2008FC1A3 /* StickerSetInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA00FF26E4ECD2008FC1A3 /* StickerSetInstallation.swift */; }; + 27BA030E26E4ECD2008FC1A3 /* ClearCloudDrafts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA010126E4ECD2008FC1A3 /* ClearCloudDrafts.swift */; }; + 27BA030F26E4ECD2008FC1A3 /* InstallInteractiveReadMessagesAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA010226E4ECD2008FC1A3 /* InstallInteractiveReadMessagesAction.swift */; }; + 27BA031026E4ECD2008FC1A3 /* TelegramEngineMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA010326E4ECD2008FC1A3 /* TelegramEngineMessages.swift */; }; + 27BA031126E4ECD2008FC1A3 /* Polls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA010426E4ECD2008FC1A3 /* Polls.swift */; }; + 27BA031226E4ECD2008FC1A3 /* SearchMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA010526E4ECD2008FC1A3 /* SearchMessages.swift */; }; + 27BA031326E4ECD2008FC1A3 /* ChatList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA010626E4ECD2008FC1A3 /* ChatList.swift */; }; + 27BA031426E4ECD2008FC1A3 /* LoadMessagesIfNecessary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA010726E4ECD2008FC1A3 /* LoadMessagesIfNecessary.swift */; }; + 27BA031526E4ECD2008FC1A3 /* RequestStartBot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA010826E4ECD2008FC1A3 /* RequestStartBot.swift */; }; + 27BA031626E4ECD2008FC1A3 /* ReplyThreadHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA010926E4ECD2008FC1A3 /* ReplyThreadHistory.swift */; }; + 27BA031726E4ECD2008FC1A3 /* CallList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA010A26E4ECD2008FC1A3 /* CallList.swift */; }; + 27BA031826E4ECD2008FC1A3 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA010B26E4ECD2008FC1A3 /* Message.swift */; }; + 27BA031926E4ECD2008FC1A3 /* RecentlyUsedHashtags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA010C26E4ECD2008FC1A3 /* RecentlyUsedHashtags.swift */; }; + 27BA031A26E4ECD2008FC1A3 /* UpdatePinnedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA010D26E4ECD2008FC1A3 /* UpdatePinnedMessage.swift */; }; + 27BA031B26E4ECD2008FC1A3 /* EarliestUnseenPersonalMentionMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA010E26E4ECD2008FC1A3 /* EarliestUnseenPersonalMentionMessage.swift */; }; + 27BA031C26E4ECD2008FC1A3 /* ForwardGame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA010F26E4ECD2008FC1A3 /* ForwardGame.swift */; }; + 27BA031D26E4ECD2008FC1A3 /* ApplyMaxReadIndexInteractively.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA011026E4ECD2008FC1A3 /* ApplyMaxReadIndexInteractively.swift */; }; + 27BA031E26E4ECD2008FC1A3 /* Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA011126E4ECD2008FC1A3 /* Media.swift */; }; + 27BA031F26E4ECD2008FC1A3 /* PeerLiveLocationsContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA011226E4ECD2008FC1A3 /* PeerLiveLocationsContext.swift */; }; + 27BA032026E4ECD2008FC1A3 /* OutgoingMessageWithChatContextResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA011326E4ECD2008FC1A3 /* OutgoingMessageWithChatContextResult.swift */; }; + 27BA032126E4ECD2008FC1A3 /* EngineGroupCallDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA011426E4ECD2008FC1A3 /* EngineGroupCallDescription.swift */; }; + 27BA032226E4ECD2008FC1A3 /* MessageReadStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA011526E4ECD2008FC1A3 /* MessageReadStats.swift */; }; + 27BA032326E4ECD2008FC1A3 /* ScheduledMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA011626E4ECD2008FC1A3 /* ScheduledMessages.swift */; }; + 27BA032426E4ECD2008FC1A3 /* DeleteMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA011726E4ECD2008FC1A3 /* DeleteMessages.swift */; }; + 27BA032526E4ECD2008FC1A3 /* RequestMessageActionCallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA011826E4ECD2008FC1A3 /* RequestMessageActionCallback.swift */; }; + 27BA032626E4ECD2008FC1A3 /* MarkAllChatsAsRead.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA011926E4ECD2008FC1A3 /* MarkAllChatsAsRead.swift */; }; + 27BA032726E4ECD2008FC1A3 /* DeleteMessagesInteractively.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA011A26E4ECD2008FC1A3 /* DeleteMessagesInteractively.swift */; }; + 27BA032826E4ECD2008FC1A3 /* RequestChatContextResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA011B26E4ECD2008FC1A3 /* RequestChatContextResults.swift */; }; + 27BA032926E4ECD2008FC1A3 /* MarkMessageContentAsConsumedInteractively.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA011C26E4ECD2008FC1A3 /* MarkMessageContentAsConsumedInteractively.swift */; }; + 27BA032A26E4ECD2008FC1A3 /* ReadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA011D26E4ECD2008FC1A3 /* ReadState.swift */; }; + 27BA032B26E4ECD2008FC1A3 /* AdMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA011E26E4ECD2008FC1A3 /* AdMessages.swift */; }; + 27BA032C26E4ECD2008FC1A3 /* ExportMessageLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA011F26E4ECD2008FC1A3 /* ExportMessageLink.swift */; }; + 27BA032D26E4ECD2008FC1A3 /* BlockedPeers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA012126E4ECD2008FC1A3 /* BlockedPeers.swift */; }; + 27BA032E26E4ECD2008FC1A3 /* UpdatedAccountPrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA012226E4ECD2008FC1A3 /* UpdatedAccountPrivacySettings.swift */; }; + 27BA032F26E4ECD2008FC1A3 /* BlockedPeersContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA012326E4ECD2008FC1A3 /* BlockedPeersContext.swift */; }; + 27BA033026E4ECD2008FC1A3 /* TelegramEnginePrivacy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA012426E4ECD2008FC1A3 /* TelegramEnginePrivacy.swift */; }; + 27BA033126E4ECD2008FC1A3 /* RecentAccountSessions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA012526E4ECD2008FC1A3 /* RecentAccountSessions.swift */; }; + 27BA033226E4ECD2008FC1A3 /* RecentWebSessions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA012626E4ECD2008FC1A3 /* RecentWebSessions.swift */; }; + 27BA033326E4ECD2008FC1A3 /* RecentAccountSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA012726E4ECD2008FC1A3 /* RecentAccountSession.swift */; }; + 27BA033426E4ECD2008FC1A3 /* ActiveSessionsContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA012826E4ECD2008FC1A3 /* ActiveSessionsContext.swift */; }; + 27BA033526E4ECD2008FC1A3 /* TelegramEngineResolve.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA012A26E4ECD2008FC1A3 /* TelegramEngineResolve.swift */; }; + 27BA033626E4ECD2008FC1A3 /* DeepLinkInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA012B26E4ECD2008FC1A3 /* DeepLinkInfo.swift */; }; + 27BA033726E4ECD2008FC1A3 /* TelegramEngineAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA012D26E4ECD2008FC1A3 /* TelegramEngineAuth.swift */; }; + 27BA033826E4ECD2008FC1A3 /* CancelAccountReset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA012E26E4ECD2008FC1A3 /* CancelAccountReset.swift */; }; + 27BA033926E4ECD2008FC1A3 /* ConfirmTwoStepRecoveryEmail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA012F26E4ECD2008FC1A3 /* ConfirmTwoStepRecoveryEmail.swift */; }; + 27BA033A26E4ECD2008FC1A3 /* AuthTransfer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA013026E4ECD2008FC1A3 /* AuthTransfer.swift */; }; + 27BA033B26E4ECD2008FC1A3 /* TwoStepVerification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA013126E4ECD2008FC1A3 /* TwoStepVerification.swift */; }; + 27BA033C26E4ECD2008FC1A3 /* PhoneNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA013326E4ECD2008FC1A3 /* PhoneNumber.swift */; }; + 27BA033D26E4ECD2008FC1A3 /* TelegramEngineContacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA013426E4ECD2008FC1A3 /* TelegramEngineContacts.swift */; }; + 27BA033E26E4ECD2008FC1A3 /* ContactManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA013526E4ECD2008FC1A3 /* ContactManagement.swift */; }; + 27BA033F26E4ECD2008FC1A3 /* ImportContact.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA013626E4ECD2008FC1A3 /* ImportContact.swift */; }; + 27BA034026E4ECD2008FC1A3 /* UpdateContactName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA013726E4ECD2008FC1A3 /* UpdateContactName.swift */; }; + 27BA034126E4ECD2008FC1A3 /* DeviceContact.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA013826E4ECD2008FC1A3 /* DeviceContact.swift */; }; + 27BA034226E4ECD2008FC1A3 /* TelegramDeviceContactImportInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA013926E4ECD2008FC1A3 /* TelegramDeviceContactImportInfo.swift */; }; + 27BA034326E4ECD2008FC1A3 /* CollectCacheUsageStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA013B26E4ECD2008FC1A3 /* CollectCacheUsageStats.swift */; }; + 27BA034426E4ECD2008FC1A3 /* TelegramEngineResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA013C26E4ECD2008FC1A3 /* TelegramEngineResources.swift */; }; + 27BA034526E4ECD2008FC1A3 /* TelegramEngineHistoryImport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA013E26E4ECD2008FC1A3 /* TelegramEngineHistoryImport.swift */; }; + 27BA034626E4ECD2008FC1A3 /* StringCodingKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA014026E4ECD2008FC1A3 /* StringCodingKey.swift */; }; + 27BA034726E4ECD2008FC1A3 /* PeersNearby.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA014226E4ECD2008FC1A3 /* PeersNearby.swift */; }; + 27BA034826E4ECD2008FC1A3 /* TelegramEnginePeersNearby.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA014326E4ECD2008FC1A3 /* TelegramEnginePeersNearby.swift */; }; + 27BA034926E4ECD2008FC1A3 /* TelegramEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA014426E4ECD2008FC1A3 /* TelegramEngine.swift */; }; + 27BA034A26E4ECD2008FC1A3 /* RemovePeerChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA014626E4ECD2008FC1A3 /* RemovePeerChat.swift */; }; + 27BA034B26E4ECD2008FC1A3 /* ChannelOwnershipTransfer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA014726E4ECD2008FC1A3 /* ChannelOwnershipTransfer.swift */; }; + 27BA034C26E4ECD2008FC1A3 /* ChangePeerNotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA014826E4ECD2008FC1A3 /* ChangePeerNotificationSettings.swift */; }; + 27BA034D26E4ECD2008FC1A3 /* ManageChannelDiscussionGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA014926E4ECD2008FC1A3 /* ManageChannelDiscussionGroup.swift */; }; + 27BA034E26E4ECD2008FC1A3 /* RequestUserPhotos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA014A26E4ECD2008FC1A3 /* RequestUserPhotos.swift */; }; + 27BA034F26E4ECD2008FC1A3 /* ChatListFiltering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA014B26E4ECD2008FC1A3 /* ChatListFiltering.swift */; }; + 27BA035026E4ECD2008FC1A3 /* SupportPeerId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA014C26E4ECD2008FC1A3 /* SupportPeerId.swift */; }; + 27BA035126E4ECD2008FC1A3 /* ChannelHistoryAvailabilitySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA014D26E4ECD2008FC1A3 /* ChannelHistoryAvailabilitySettings.swift */; }; + 27BA035226E4ECD2008FC1A3 /* JoinChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA014E26E4ECD2008FC1A3 /* JoinChannel.swift */; }; + 27BA035326E4ECD2008FC1A3 /* UpdateCachedPeerData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA014F26E4ECD2008FC1A3 /* UpdateCachedPeerData.swift */; }; + 27BA035426E4ECD2008FC1A3 /* GroupsInCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA015026E4ECD2008FC1A3 /* GroupsInCommon.swift */; }; + 27BA035526E4ECD2008FC1A3 /* RecentPeers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA015126E4ECD2008FC1A3 /* RecentPeers.swift */; }; + 27BA035626E4ECD2008FC1A3 /* UpdateGroupSpecificStickerset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA015226E4ECD2008FC1A3 /* UpdateGroupSpecificStickerset.swift */; }; + 27BA035726E4ECD2008FC1A3 /* NotificationExceptionsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA015326E4ECD2008FC1A3 /* NotificationExceptionsList.swift */; }; + 27BA035826E4ECD2008FC1A3 /* ChannelAdminEventLogContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA015426E4ECD2008FC1A3 /* ChannelAdminEventLogContext.swift */; }; + 27BA035926E4ECD2008FC1A3 /* ChannelAdminEventLogs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA015526E4ECD2008FC1A3 /* ChannelAdminEventLogs.swift */; }; + 27BA035A26E4ECD2008FC1A3 /* CheckPeerChatServiceActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA015626E4ECD2008FC1A3 /* CheckPeerChatServiceActions.swift */; }; + 27BA035B26E4ECD2008FC1A3 /* SearchGroupMembers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA015726E4ECD2008FC1A3 /* SearchGroupMembers.swift */; }; + 27BA035C26E4ECD2008FC1A3 /* JoinLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA015826E4ECD2008FC1A3 /* JoinLink.swift */; }; + 27BA035D26E4ECD2008FC1A3 /* AddPeerMember.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA015926E4ECD2008FC1A3 /* AddPeerMember.swift */; }; + 27BA035E26E4ECD2008FC1A3 /* ReportPeer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA015A26E4ECD2008FC1A3 /* ReportPeer.swift */; }; + 27BA035F26E4ECD2008FC1A3 /* ChannelBlacklist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA015B26E4ECD2008FC1A3 /* ChannelBlacklist.swift */; }; + 27BA036026E4ECD2008FC1A3 /* InactiveChannels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA015C26E4ECD2008FC1A3 /* InactiveChannels.swift */; }; + 27BA036126E4ECD2008FC1A3 /* PeerPhotoUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA015D26E4ECD2008FC1A3 /* PeerPhotoUpdater.swift */; }; + 27BA036226E4ECD2008FC1A3 /* ChatOnlineMembers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA015E26E4ECD2008FC1A3 /* ChatOnlineMembers.swift */; }; + 27BA036326E4ECD2008FC1A3 /* SlowMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA015F26E4ECD2008FC1A3 /* SlowMode.swift */; }; + 27BA036426E4ECD2008FC1A3 /* PeerAdmins.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA016026E4ECD2008FC1A3 /* PeerAdmins.swift */; }; + 27BA036526E4ECD2008FC1A3 /* ToggleChannelSignatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA016126E4ECD2008FC1A3 /* ToggleChannelSignatures.swift */; }; + 27BA036626E4ECD2008FC1A3 /* ConvertGroupToSupergroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA016226E4ECD2008FC1A3 /* ConvertGroupToSupergroup.swift */; }; + 27BA036726E4ECD2008FC1A3 /* CreateSecretChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA016326E4ECD2008FC1A3 /* CreateSecretChat.swift */; }; + 27BA036826E4ECD2008FC1A3 /* CreateGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA016426E4ECD2008FC1A3 /* CreateGroup.swift */; }; + 27BA036926E4ECD2008FC1A3 /* PeerCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA016526E4ECD2008FC1A3 /* PeerCommands.swift */; }; + 27BA036A26E4ECD2008FC1A3 /* TogglePeerChatPinned.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA016626E4ECD2008FC1A3 /* TogglePeerChatPinned.swift */; }; + 27BA036B26E4ECD2008FC1A3 /* ChannelCreation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA016726E4ECD2008FC1A3 /* ChannelCreation.swift */; }; + 27BA036C26E4ECD2008FC1A3 /* AddressNames.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA016826E4ECD2008FC1A3 /* AddressNames.swift */; }; + 27BA036D26E4ECD2008FC1A3 /* PeerSpecificStickerPack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA016926E4ECD2008FC1A3 /* PeerSpecificStickerPack.swift */; }; + 27BA036E26E4ECD2008FC1A3 /* InvitationLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA016A26E4ECD2008FC1A3 /* InvitationLinks.swift */; }; + 27BA036F26E4ECD2008FC1A3 /* ResolvePeerByName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA016B26E4ECD2008FC1A3 /* ResolvePeerByName.swift */; }; + 27BA037026E4ECD2008FC1A3 /* FindChannelById.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA016C26E4ECD2008FC1A3 /* FindChannelById.swift */; }; + 27BA037126E4ECD2008FC1A3 /* SearchPeers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA016D26E4ECD2008FC1A3 /* SearchPeers.swift */; }; + 27BA037226E4ECD2008FC1A3 /* UpdatePeerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA016E26E4ECD2008FC1A3 /* UpdatePeerInfo.swift */; }; + 27BA037326E4ECD2008FC1A3 /* OpaqueChatState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA016F26E4ECD2008FC1A3 /* OpaqueChatState.swift */; }; + 27BA037426E4ECD2008FC1A3 /* TelegramEnginePeers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA017026E4ECD2008FC1A3 /* TelegramEnginePeers.swift */; }; + 27BA037526E4ECD2008FC1A3 /* ChannelMembers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA017126E4ECD2008FC1A3 /* ChannelMembers.swift */; }; + 27BA037626E4ECD2008FC1A3 /* Peer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA017226E4ECD2008FC1A3 /* Peer.swift */; }; + 27BA037726E4ECD2008FC1A3 /* ChannelParticipants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA017326E4ECD2008FC1A3 /* ChannelParticipants.swift */; }; + 27BA037826E4ECD2008FC1A3 /* RemovePeerMember.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA017426E4ECD2008FC1A3 /* RemovePeerMember.swift */; }; + 27BA037926E4ECD2008FC1A3 /* RecentlySearchedPeerIds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA017526E4ECD2008FC1A3 /* RecentlySearchedPeerIds.swift */; }; + 27BA037A26E4ECD2008FC1A3 /* TelegramEngineData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA017726E4ECD2008FC1A3 /* TelegramEngineData.swift */; }; + 27BA037B26E4ECD2008FC1A3 /* PeerSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA017826E4ECD2008FC1A3 /* PeerSummary.swift */; }; + 27BA037C26E4ECD2008FC1A3 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA017926E4ECD2008FC1A3 /* Configuration.swift */; }; + 27BA037D26E4ECD2008FC1A3 /* RegisterNotificationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA017B26E4ECD2008FC1A3 /* RegisterNotificationToken.swift */; }; + 27BA037E26E4ECD2008FC1A3 /* UpdateAccountPeerName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA017C26E4ECD2008FC1A3 /* UpdateAccountPeerName.swift */; }; + 27BA037F26E4ECD2008FC1A3 /* TelegramEngineAccountData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA017D26E4ECD2008FC1A3 /* TelegramEngineAccountData.swift */; }; + 27BA038026E4ECD2008FC1A3 /* ChangeAccountPhoneNumber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA017E26E4ECD2008FC1A3 /* ChangeAccountPhoneNumber.swift */; }; + 27BA038126E4ECD2008FC1A3 /* TermsOfService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA017F26E4ECD2008FC1A3 /* TermsOfService.swift */; }; + 27BA038226E4ECD2008FC1A3 /* TelegramEngineThemes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA018126E4ECD2008FC1A3 /* TelegramEngineThemes.swift */; }; + 27BA038326E4ECD2008FC1A3 /* ChatThemes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA018226E4ECD2008FC1A3 /* ChatThemes.swift */; }; + 27BA038426E4ECD2008FC1A3 /* TelegramEngineLocalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA018426E4ECD2008FC1A3 /* TelegramEngineLocalization.swift */; }; + 27BA038526E4ECD2008FC1A3 /* Localizations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA018526E4ECD2008FC1A3 /* Localizations.swift */; }; + 27BA038626E4ECD2008FC1A3 /* Countries.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA018626E4ECD2008FC1A3 /* Countries.swift */; }; + 27BA038726E4ECD2008FC1A3 /* LocalizationInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA018726E4ECD2008FC1A3 /* LocalizationInfo.swift */; }; + 27BA038826E4ECD2008FC1A3 /* SuggestedLocalizationEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA018826E4ECD2008FC1A3 /* SuggestedLocalizationEntry.swift */; }; + 27BA038926E4ECD2008FC1A3 /* LocalizationListState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA018926E4ECD2008FC1A3 /* LocalizationListState.swift */; }; + 27BA038A26E4ECD2008FC1A3 /* LocalizationPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA018A26E4ECD2008FC1A3 /* LocalizationPreview.swift */; }; + 27BA038B26E4ECD2008FC1A3 /* TelegramGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA018C26E4ECD2008FC1A3 /* TelegramGroup.swift */; }; + 27BA038C26E4ECD2008FC1A3 /* TelegramPeerNotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA018D26E4ECD2008FC1A3 /* TelegramPeerNotificationSettings.swift */; }; + 27BA038D26E4ECD2008FC1A3 /* TelegramUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA018E26E4ECD2008FC1A3 /* TelegramUser.swift */; }; + 27BA038E26E4ECD2008FC1A3 /* CloudMediaResourceParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA018F26E4ECD2008FC1A3 /* CloudMediaResourceParameters.swift */; }; + 27BA038F26E4ECD2008FC1A3 /* TelegramMediaWebDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA019026E4ECD2008FC1A3 /* TelegramMediaWebDocument.swift */; }; + 27BA039026E4ECD2008FC1A3 /* BotInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA019126E4ECD2008FC1A3 /* BotInfo.swift */; }; + 27BA039126E4ECD2008FC1A3 /* CachedChannelParticipants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA019226E4ECD2008FC1A3 /* CachedChannelParticipants.swift */; }; + 27BA039226E4ECD2008FC1A3 /* CachedGroupParticipants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA019326E4ECD2008FC1A3 /* CachedGroupParticipants.swift */; }; + 27BA039326E4ECD2008FC1A3 /* TelegramChannelAdminRights.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA019426E4ECD2008FC1A3 /* TelegramChannelAdminRights.swift */; }; + 27BA039426E4ECD2008FC1A3 /* TextEntitiesMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA019526E4ECD2008FC1A3 /* TextEntitiesMessageAttribute.swift */; }; + 27BA039526E4ECD2008FC1A3 /* TelegramMediaWebpage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA019626E4ECD2008FC1A3 /* TelegramMediaWebpage.swift */; }; + 27BA039626E4ECD2008FC1A3 /* TelegramMediaWebFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA019726E4ECD2008FC1A3 /* TelegramMediaWebFile.swift */; }; + 27BA039726E4ECD2008FC1A3 /* TelegramMediaGame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA019826E4ECD2008FC1A3 /* TelegramMediaGame.swift */; }; + 27BA039826E4ECD2008FC1A3 /* Wallpaper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA019926E4ECD2008FC1A3 /* Wallpaper.swift */; }; + 27BA039926E4ECD2008FC1A3 /* TelegramMediaPoll.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA019A26E4ECD2008FC1A3 /* TelegramMediaPoll.swift */; }; + 27BA039A26E4ECD2008FC1A3 /* RemoteStorageConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA019B26E4ECD2008FC1A3 /* RemoteStorageConfiguration.swift */; }; + 27BA039B26E4ECD2008FC1A3 /* TelegramMediaImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA019C26E4ECD2008FC1A3 /* TelegramMediaImage.swift */; }; + 27BA039C26E4ECD2008FC1A3 /* ReactionsMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA019D26E4ECD2008FC1A3 /* ReactionsMessageAttribute.swift */; }; + 27BA039D26E4ECD2008FC1A3 /* ChatContextResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA019E26E4ECD2008FC1A3 /* ChatContextResult.swift */; }; + 27BA039E26E4ECD2008FC1A3 /* ImageRepresentationWithReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA019F26E4ECD2008FC1A3 /* ImageRepresentationWithReference.swift */; }; + 27BA039F26E4ECD2008FC1A3 /* TelegramMediaAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01A026E4ECD2008FC1A3 /* TelegramMediaAction.swift */; }; + 27BA03A026E4ECD2008FC1A3 /* TelegramMediaMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01A126E4ECD2008FC1A3 /* TelegramMediaMap.swift */; }; + 27BA03A126E4ECD2008FC1A3 /* EncryptedMediaResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01A226E4ECD2008FC1A3 /* EncryptedMediaResource.swift */; }; + 27BA03A226E4ECD2008FC1A3 /* PeerAccessRestrictionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01A326E4ECD2008FC1A3 /* PeerAccessRestrictionInfo.swift */; }; + 27BA03A326E4ECD2008FC1A3 /* TelegramChannelBannedRights.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01A426E4ECD2008FC1A3 /* TelegramChannelBannedRights.swift */; }; + 27BA03A426E4ECD2008FC1A3 /* ExportedInvitation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01A526E4ECD2008FC1A3 /* ExportedInvitation.swift */; }; + 27BA03A526E4ECD2008FC1A3 /* StoreMessage_Telegram.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01A626E4ECD2008FC1A3 /* StoreMessage_Telegram.swift */; }; + 27BA03A626E4ECD2008FC1A3 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01A726E4ECD2008FC1A3 /* Theme.swift */; }; + 27BA03A726E4ECD2008FC1A3 /* PeerGeoLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01A826E4ECD2008FC1A3 /* PeerGeoLocation.swift */; }; + 27BA03A826E4ECD2008FC1A3 /* ApiUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01A926E4ECD2008FC1A3 /* ApiUtils.swift */; }; + 27BA03A926E4ECD2008FC1A3 /* RichText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01AA26E4ECD2008FC1A3 /* RichText.swift */; }; + 27BA03AA26E4ECD2008FC1A3 /* InstantPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01AB26E4ECD2008FC1A3 /* InstantPage.swift */; }; + 27BA03AB26E4ECD2008FC1A3 /* CloudFileMediaResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01AC26E4ECD2008FC1A3 /* CloudFileMediaResource.swift */; }; + 27BA03AC26E4ECD2008FC1A3 /* ApiGroupOrChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01AD26E4ECD2008FC1A3 /* ApiGroupOrChannel.swift */; }; + 27BA03AD26E4ECD2008FC1A3 /* TelegramChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01AE26E4ECD2008FC1A3 /* TelegramChannel.swift */; }; + 27BA03AE26E4ECD2008FC1A3 /* MediaResourceApiUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01AF26E4ECD2008FC1A3 /* MediaResourceApiUtils.swift */; }; + 27BA03AF26E4ECD2008FC1A3 /* TelegramUserPresence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01B026E4ECD2008FC1A3 /* TelegramUserPresence.swift */; }; + 27BA03B026E4ECD2008FC1A3 /* AdMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01B126E4ECD2008FC1A3 /* AdMessageAttribute.swift */; }; + 27BA03B126E4ECD2008FC1A3 /* TelegramMediaFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01B226E4ECD2008FC1A3 /* TelegramMediaFile.swift */; }; + 27BA03B226E4ECD2008FC1A3 /* ReplyMarkupMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01B326E4ECD2008FC1A3 /* ReplyMarkupMessageAttribute.swift */; }; + 27BA03B326E4ECD2008FC1A3 /* SyncCore_TelegramMediaAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01B526E4ECD2008FC1A3 /* SyncCore_TelegramMediaAction.swift */; }; + 27BA03B426E4ECD2008FC1A3 /* SyncCore_AutodownloadSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01B626E4ECD2008FC1A3 /* SyncCore_AutodownloadSettings.swift */; }; + 27BA03B526E4ECD2008FC1A3 /* SyncCore_wallapersState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01B726E4ECD2008FC1A3 /* SyncCore_wallapersState.swift */; }; + 27BA03B626E4ECD2008FC1A3 /* SyncCore_SynchronizeChatInputStateOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01B826E4ECD2008FC1A3 /* SyncCore_SynchronizeChatInputStateOperation.swift */; }; + 27BA03B726E4ECD2008FC1A3 /* SyncCore_TelegramUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01B926E4ECD2008FC1A3 /* SyncCore_TelegramUser.swift */; }; + 27BA03B826E4ECD2008FC1A3 /* SyncCore_TelegramChatAdminRights.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01BA26E4ECD2008FC1A3 /* SyncCore_TelegramChatAdminRights.swift */; }; + 27BA03B926E4ECD2008FC1A3 /* SyncCore_PeerAccessHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01BB26E4ECD2008FC1A3 /* SyncCore_PeerAccessHash.swift */; }; + 27BA03BA26E4ECD2008FC1A3 /* SyncCore_ViewCountMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01BC26E4ECD2008FC1A3 /* SyncCore_ViewCountMessageAttribute.swift */; }; + 27BA03BB26E4ECD2008FC1A3 /* SyncCore_SynchronizeAppLogEventsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01BD26E4ECD2008FC1A3 /* SyncCore_SynchronizeAppLogEventsOperation.swift */; }; + 27BA03BC26E4ECD2008FC1A3 /* SyncCore_TelegramMediaImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01BE26E4ECD2008FC1A3 /* SyncCore_TelegramMediaImage.swift */; }; + 27BA03BD26E4ECD2008FC1A3 /* SyncCore_AppConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01BF26E4ECD2008FC1A3 /* SyncCore_AppConfiguration.swift */; }; + 27BA03BE26E4ECD2008FC1A3 /* SyncCore_SecretChatFileReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01C026E4ECD2008FC1A3 /* SyncCore_SecretChatFileReference.swift */; }; + 27BA03BF26E4ECD2008FC1A3 /* SyncCore_LoggingSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01C126E4ECD2008FC1A3 /* SyncCore_LoggingSettings.swift */; }; + 27BA03C026E4ECD2008FC1A3 /* SyncCore_SynchronizeSavedStickersOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01C226E4ECD2008FC1A3 /* SyncCore_SynchronizeSavedStickersOperation.swift */; }; + 27BA03C126E4ECD2008FC1A3 /* SyncCore_SecureFileMediaResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01C326E4ECD2008FC1A3 /* SyncCore_SecureFileMediaResource.swift */; }; + 27BA03C226E4ECD2008FC1A3 /* SyncCore_UpdateMessageReactionsAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01C426E4ECD2008FC1A3 /* SyncCore_UpdateMessageReactionsAction.swift */; }; + 27BA03C326E4ECD2008FC1A3 /* SyncCore_EditedMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01C526E4ECD2008FC1A3 /* SyncCore_EditedMessageAttribute.swift */; }; + 27BA03C426E4ECD2008FC1A3 /* SyncCore_SearchBotsConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01C626E4ECD2008FC1A3 /* SyncCore_SearchBotsConfiguration.swift */; }; + 27BA03C526E4ECD2008FC1A3 /* SyncCore_TemporaryTwoStepPasswordToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01C726E4ECD2008FC1A3 /* SyncCore_TemporaryTwoStepPasswordToken.swift */; }; + 27BA03C626E4ECD2008FC1A3 /* SyncCore_RecentPeerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01C826E4ECD2008FC1A3 /* SyncCore_RecentPeerItem.swift */; }; + 27BA03C726E4ECD2008FC1A3 /* SyncCore_Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01C926E4ECD2008FC1A3 /* SyncCore_Localization.swift */; }; + 27BA03C826E4ECD2008FC1A3 /* SyncCore_GlobalNotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01CA26E4ECD2008FC1A3 /* SyncCore_GlobalNotificationSettings.swift */; }; + 27BA03C926E4ECD2008FC1A3 /* SyncCore_TelegramChatBannedRights.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01CB26E4ECD2008FC1A3 /* SyncCore_TelegramChatBannedRights.swift */; }; + 27BA03CA26E4ECD2008FC1A3 /* SyncCore_TelegramSecretChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01CC26E4ECD2008FC1A3 /* SyncCore_TelegramSecretChat.swift */; }; + 27BA03CB26E4ECD2008FC1A3 /* SyncCore_CachedStickerQueryResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01CD26E4ECD2008FC1A3 /* SyncCore_CachedStickerQueryResult.swift */; }; + 27BA03CC26E4ECD2008FC1A3 /* SyncCore_TelegramMediaGame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01CE26E4ECD2008FC1A3 /* SyncCore_TelegramMediaGame.swift */; }; + 27BA03CD26E4ECD2008FC1A3 /* SyncCore_TelegramMediaContact.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01CF26E4ECD2008FC1A3 /* SyncCore_TelegramMediaContact.swift */; }; + 27BA03CE26E4ECD2008FC1A3 /* SyncCore_StickerPackCollectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01D026E4ECD2008FC1A3 /* SyncCore_StickerPackCollectionInfo.swift */; }; + 27BA03CF26E4ECD2008FC1A3 /* SyncCore_CachedThemesConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01D126E4ECD2008FC1A3 /* SyncCore_CachedThemesConfiguration.swift */; }; + 27BA03D026E4ECD2008FC1A3 /* SyncCore_RestrictedContentMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01D226E4ECD2008FC1A3 /* SyncCore_RestrictedContentMessageAttribute.swift */; }; + 27BA03D126E4ECD2008FC1A3 /* SyncCore_ConsumableContentMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01D326E4ECD2008FC1A3 /* SyncCore_ConsumableContentMessageAttribute.swift */; }; + 27BA03D226E4ECD2008FC1A3 /* SyncCore_CachedResolvedByNamePeer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01D426E4ECD2008FC1A3 /* SyncCore_CachedResolvedByNamePeer.swift */; }; + 27BA03D326E4ECD2008FC1A3 /* SyncCore_TelegramMediaPoll.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01D526E4ECD2008FC1A3 /* SyncCore_TelegramMediaPoll.swift */; }; + 27BA03D426E4ECD2008FC1A3 /* SyncCore_SecretChatIncomingDecryptedOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01D626E4ECD2008FC1A3 /* SyncCore_SecretChatIncomingDecryptedOperation.swift */; }; + 27BA03D526E4ECD2008FC1A3 /* SyncCore_TelegramPeerNotificationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01D726E4ECD2008FC1A3 /* SyncCore_TelegramPeerNotificationSettings.swift */; }; + 27BA03D626E4ECD2008FC1A3 /* SyncCore_TelegramMediaUnsupported.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01D826E4ECD2008FC1A3 /* SyncCore_TelegramMediaUnsupported.swift */; }; + 27BA03D726E4ECD2008FC1A3 /* SyncCore_CachedChannelData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01D926E4ECD2008FC1A3 /* SyncCore_CachedChannelData.swift */; }; + 27BA03D826E4ECD2008FC1A3 /* SyncCore_AccountSortOrderAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01DA26E4ECD2008FC1A3 /* SyncCore_AccountSortOrderAttribute.swift */; }; + 27BA03D926E4ECD2008FC1A3 /* SyncCore_OutgoingScheduleInfoMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01DB26E4ECD2008FC1A3 /* SyncCore_OutgoingScheduleInfoMessageAttribute.swift */; }; + 27BA03DA26E4ECD2008FC1A3 /* SyncCore_ConsumePersonalMessageAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01DC26E4ECD2008FC1A3 /* SyncCore_ConsumePersonalMessageAction.swift */; }; + 27BA03DB26E4ECD2008FC1A3 /* SyncCore_TelegramTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01DD26E4ECD2008FC1A3 /* SyncCore_TelegramTheme.swift */; }; + 27BA03DC26E4ECD2008FC1A3 /* SyncCore_SecretFileEncryptionKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01DE26E4ECD2008FC1A3 /* SyncCore_SecretFileEncryptionKey.swift */; }; + 27BA03DD26E4ECD2008FC1A3 /* SyncCore_CachedGroupData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01DF26E4ECD2008FC1A3 /* SyncCore_CachedGroupData.swift */; }; + 27BA03DE26E4ECD2008FC1A3 /* SyncCore_CachedSecureIdConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01E026E4ECD2008FC1A3 /* SyncCore_CachedSecureIdConfiguration.swift */; }; + 27BA03DF26E4ECD2008FC1A3 /* SyncCore_ForwardHideSendersNamesMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01E126E4ECD2008FC1A3 /* SyncCore_ForwardHideSendersNamesMessageAttribute.swift */; }; + 27BA03E026E4ECD2008FC1A3 /* SyncCore_TelegramMediaExpiredContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01E226E4ECD2008FC1A3 /* SyncCore_TelegramMediaExpiredContent.swift */; }; + 27BA03E126E4ECD2008FC1A3 /* SyncCore_SavedStickerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01E326E4ECD2008FC1A3 /* SyncCore_SavedStickerItem.swift */; }; + 27BA03E226E4ECD2008FC1A3 /* SyncCore_AuthorSignatureMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01E426E4ECD2008FC1A3 /* SyncCore_AuthorSignatureMessageAttribute.swift */; }; + 27BA03E326E4ECD2008FC1A3 /* SyncCore_ChannelMessageStateVersionAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01E526E4ECD2008FC1A3 /* SyncCore_ChannelMessageStateVersionAttribute.swift */; }; + 27BA03E426E4ECD2008FC1A3 /* SyncCore_ThemeSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01E626E4ECD2008FC1A3 /* SyncCore_ThemeSettings.swift */; }; + 27BA03E526E4ECD2008FC1A3 /* SyncCore_Namespaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01E726E4ECD2008FC1A3 /* SyncCore_Namespaces.swift */; }; + 27BA03E626E4ECD2008FC1A3 /* SyncCore_PixelDimensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01E826E4ECD2008FC1A3 /* SyncCore_PixelDimensions.swift */; }; + 27BA03E726E4ECD2008FC1A3 /* SyncCore_InlineBotMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01E926E4ECD2008FC1A3 /* SyncCore_InlineBotMessageAttribute.swift */; }; + 27BA03E826E4ECD2008FC1A3 /* SyncCore_LocalizationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01EA26E4ECD2008FC1A3 /* SyncCore_LocalizationSettings.swift */; }; + 27BA03E926E4ECD2008FC1A3 /* SyncCore_SendScheduledMessageImmediatelyAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01EB26E4ECD2008FC1A3 /* SyncCore_SendScheduledMessageImmediatelyAction.swift */; }; + 27BA03EA26E4ECD2008FC1A3 /* SyncCore_ReplyMarkupMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01EC26E4ECD2008FC1A3 /* SyncCore_ReplyMarkupMessageAttribute.swift */; }; + 27BA03EB26E4ECD2008FC1A3 /* SyncCore_AutoremoveTimeoutMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01ED26E4ECD2008FC1A3 /* SyncCore_AutoremoveTimeoutMessageAttribute.swift */; }; + 27BA03EC26E4ECD2008FC1A3 /* SyncCore_JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01EE26E4ECD2008FC1A3 /* SyncCore_JSON.swift */; }; + 27BA03ED26E4ECD2008FC1A3 /* SyncCore_TelegramGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01EF26E4ECD2008FC1A3 /* SyncCore_TelegramGroup.swift */; }; + 27BA03EE26E4ECD2008FC1A3 /* SyncCore_ChannelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01F026E4ECD2008FC1A3 /* SyncCore_ChannelState.swift */; }; + 27BA03EF26E4ECD2008FC1A3 /* SyncCore_OutgoingMessageInfoAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01F126E4ECD2008FC1A3 /* SyncCore_OutgoingMessageInfoAttribute.swift */; }; + 27BA03F026E4ECD2008FC1A3 /* SyncCore_UnauthorizedAccountState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01F226E4ECD2008FC1A3 /* SyncCore_UnauthorizedAccountState.swift */; }; + 27BA03F126E4ECD2008FC1A3 /* SyncCore_SecretChatEncryptionConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01F326E4ECD2008FC1A3 /* SyncCore_SecretChatEncryptionConfig.swift */; }; + 27BA03F226E4ECD2008FC1A3 /* SyncCore_TelegramWallpaper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01F426E4ECD2008FC1A3 /* SyncCore_TelegramWallpaper.swift */; }; + 27BA03F326E4ECD2008FC1A3 /* SyncCore_ArchivedStickerPacksInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01F526E4ECD2008FC1A3 /* SyncCore_ArchivedStickerPacksInfo.swift */; }; + 27BA03F426E4ECD2008FC1A3 /* SyncCore_NetworkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01F626E4ECD2008FC1A3 /* SyncCore_NetworkSettings.swift */; }; + 27BA03F526E4ECD2008FC1A3 /* SyncCore_LocalizationInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01F726E4ECD2008FC1A3 /* SyncCore_LocalizationInfo.swift */; }; + 27BA03F626E4ECD2008FC1A3 /* SyncCore_RichText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01F826E4ECD2008FC1A3 /* SyncCore_RichText.swift */; }; + 27BA03F726E4ECD2008FC1A3 /* SyncCore_NotificationInfoMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01F926E4ECD2008FC1A3 /* SyncCore_NotificationInfoMessageAttribute.swift */; }; + 27BA03F826E4ECD2008FC1A3 /* SyncCore_InstantPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01FA26E4ECD2008FC1A3 /* SyncCore_InstantPage.swift */; }; + 27BA03F926E4ECD2008FC1A3 /* SyncCore_TextEntitiesMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01FB26E4ECD2008FC1A3 /* SyncCore_TextEntitiesMessageAttribute.swift */; }; + 27BA03FA26E4ECD2008FC1A3 /* SyncCore_TelegramUserPresence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01FC26E4ECD2008FC1A3 /* SyncCore_TelegramUserPresence.swift */; }; + 27BA03FB26E4ECD2008FC1A3 /* SyncCore_ProxySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01FD26E4ECD2008FC1A3 /* SyncCore_ProxySettings.swift */; }; + 27BA03FC26E4ECD2008FC1A3 /* SyncCore_ReactionsMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01FE26E4ECD2008FC1A3 /* SyncCore_ReactionsMessageAttribute.swift */; }; + 27BA03FD26E4ECD2008FC1A3 /* SyncCore_SecureIdFileReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA01FF26E4ECD2008FC1A3 /* SyncCore_SecureIdFileReference.swift */; }; + 27BA03FE26E4ECD2008FC1A3 /* SyncCore_SynchronizeMarkAllUnseenPersonalMessagesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA020026E4ECD2008FC1A3 /* SyncCore_SynchronizeMarkAllUnseenPersonalMessagesOperation.swift */; }; + 27BA03FF26E4ECD2008FC1A3 /* SyncCore_SynchronizeEmojiKeywordsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA020126E4ECD2008FC1A3 /* SyncCore_SynchronizeEmojiKeywordsOperation.swift */; }; + 27BA040026E4ECD2008FC1A3 /* SyncCore_TelegramDeviceContactImportedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA020226E4ECD2008FC1A3 /* SyncCore_TelegramDeviceContactImportedData.swift */; }; + 27BA040126E4ECD2008FC1A3 /* SyncCore_ForwardSourceInfoAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA020326E4ECD2008FC1A3 /* SyncCore_ForwardSourceInfoAttribute.swift */; }; + 27BA040226E4ECD2008FC1A3 /* SyncCore_SourceReferenceMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA020426E4ECD2008FC1A3 /* SyncCore_SourceReferenceMessageAttribute.swift */; }; + 27BA040326E4ECD2008FC1A3 /* SyncCore_TelegramMediaDice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA020526E4ECD2008FC1A3 /* SyncCore_TelegramMediaDice.swift */; }; + 27BA040426E4ECD2008FC1A3 /* SyncCore_ReplyMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA020626E4ECD2008FC1A3 /* SyncCore_ReplyMessageAttribute.swift */; }; + 27BA040526E4ECD2008FC1A3 /* SyncCore_WalletCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA020726E4ECD2008FC1A3 /* SyncCore_WalletCollection.swift */; }; + 27BA040626E4ECD2008FC1A3 /* SyncCore_EmojiKeywordItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA020826E4ECD2008FC1A3 /* SyncCore_EmojiKeywordItem.swift */; }; + 27BA040726E4ECD2008FC1A3 /* SyncCore_WasScheduledMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA020926E4ECD2008FC1A3 /* SyncCore_WasScheduledMessageAttribute.swift */; }; + 27BA040826E4ECD2008FC1A3 /* SyncCore_MediaReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA020A26E4ECD2008FC1A3 /* SyncCore_MediaReference.swift */; }; + 27BA040926E4ECD2008FC1A3 /* SyncCore_ImportableDeviceContactData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA020B26E4ECD2008FC1A3 /* SyncCore_ImportableDeviceContactData.swift */; }; + 27BA040A26E4ECD2008FC1A3 /* SyncCore_AppChangelogState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA020C26E4ECD2008FC1A3 /* SyncCore_AppChangelogState.swift */; }; + 27BA040B26E4ECD2008FC1A3 /* SyncCore_ContentRequiresValidationMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA020D26E4ECD2008FC1A3 /* SyncCore_ContentRequiresValidationMessageAttribute.swift */; }; + 27BA040C26E4ECD2008FC1A3 /* SyncCore_BotInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA020E26E4ECD2008FC1A3 /* SyncCore_BotInfo.swift */; }; + 27BA040D26E4ECD2008FC1A3 /* SyncCore_LimitsConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA020F26E4ECD2008FC1A3 /* SyncCore_LimitsConfiguration.swift */; }; + 27BA040E26E4ECD2008FC1A3 /* SyncCore_TelegramMediaMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA021026E4ECD2008FC1A3 /* SyncCore_TelegramMediaMap.swift */; }; + 27BA040F26E4ECD2008FC1A3 /* SyncCore_TelegramMediaResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA021126E4ECD2008FC1A3 /* SyncCore_TelegramMediaResource.swift */; }; + 27BA041026E4ECD2008FC1A3 /* SyncCore_RemoteStorageConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA021226E4ECD2008FC1A3 /* SyncCore_RemoteStorageConfiguration.swift */; }; + 27BA041126E4ECD2008FC1A3 /* SyncCore_TelegramMediaFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA021326E4ECD2008FC1A3 /* SyncCore_TelegramMediaFile.swift */; }; + 27BA041226E4ECD2008FC1A3 /* SyncCore_LocalizationListState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA021426E4ECD2008FC1A3 /* SyncCore_LocalizationListState.swift */; }; + 27BA041326E4ECD2008FC1A3 /* SyncCore_SecretChatState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA021526E4ECD2008FC1A3 /* SyncCore_SecretChatState.swift */; }; + 27BA041426E4ECD2008FC1A3 /* SyncCore_TelegramMediaWebpage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA021626E4ECD2008FC1A3 /* SyncCore_TelegramMediaWebpage.swift */; }; + 27BA041526E4ECD2008FC1A3 /* SyncCore_CachedRecentPeers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA021726E4ECD2008FC1A3 /* SyncCore_CachedRecentPeers.swift */; }; + 27BA041626E4ECD2008FC1A3 /* SyncCore_AuthorizedAccountState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA021826E4ECD2008FC1A3 /* SyncCore_AuthorizedAccountState.swift */; }; + 27BA041726E4ECD2008FC1A3 /* SyncCore_PeerReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA021926E4ECD2008FC1A3 /* SyncCore_PeerReference.swift */; }; + 27BA041826E4ECD2008FC1A3 /* SyncCore_SuggestedLocalizationEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA021A26E4ECD2008FC1A3 /* SyncCore_SuggestedLocalizationEntry.swift */; }; + 27BA041926E4ECD2008FC1A3 /* SyncCore_SecretChatOutgoingOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA021B26E4ECD2008FC1A3 /* SyncCore_SecretChatOutgoingOperation.swift */; }; + 27BA041A26E4ECD2008FC1A3 /* SyncCore_CachedGroupParticipants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA021C26E4ECD2008FC1A3 /* SyncCore_CachedGroupParticipants.swift */; }; + 27BA041B26E4ECD2008FC1A3 /* SyncCore_CachedStickerPack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA021D26E4ECD2008FC1A3 /* SyncCore_CachedStickerPack.swift */; }; + 27BA041C26E4ECD2008FC1A3 /* SyncCore_ExportedInvitation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA021E26E4ECD2008FC1A3 /* SyncCore_ExportedInvitation.swift */; }; + 27BA041D26E4ECD2008FC1A3 /* SyncCore_RecentHashtagItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA021F26E4ECD2008FC1A3 /* SyncCore_RecentHashtagItem.swift */; }; + 27BA041E26E4ECD2008FC1A3 /* SyncCore_CloudFileMediaResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA022026E4ECD2008FC1A3 /* SyncCore_CloudFileMediaResource.swift */; }; + 27BA041F26E4ECD2008FC1A3 /* SyncCore_ValidationMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA022126E4ECD2008FC1A3 /* SyncCore_ValidationMessageAttribute.swift */; }; + 27BA042026E4ECD2008FC1A3 /* SyncCore_ReplyThreadMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA022226E4ECD2008FC1A3 /* SyncCore_ReplyThreadMessageAttribute.swift */; }; + 27BA042126E4ECD2008FC1A3 /* SyncCore_PeerAccessRestrictionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA022326E4ECD2008FC1A3 /* SyncCore_PeerAccessRestrictionInfo.swift */; }; + 27BA042226E4ECD2008FC1A3 /* SyncCore_OutgoingChatContextResultMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA022426E4ECD2008FC1A3 /* SyncCore_OutgoingChatContextResultMessageAttribute.swift */; }; + 27BA042326E4ECD2008FC1A3 /* SyncCore_CachedUserData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA022526E4ECD2008FC1A3 /* SyncCore_CachedUserData.swift */; }; + 27BA042426E4ECD2008FC1A3 /* SyncCore_SynchronizeRecentlyUsedMediaOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA022626E4ECD2008FC1A3 /* SyncCore_SynchronizeRecentlyUsedMediaOperation.swift */; }; + 27BA042526E4ECD2008FC1A3 /* SyncCore_CachedLocalizationInfos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA022726E4ECD2008FC1A3 /* SyncCore_CachedLocalizationInfos.swift */; }; + 27BA042626E4ECD2008FC1A3 /* SyncCore_AccountEnvironmentAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA022826E4ECD2008FC1A3 /* SyncCore_AccountEnvironmentAttribute.swift */; }; + 27BA042726E4ECD2008FC1A3 /* SyncCore_PeerStatusSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA022926E4ECD2008FC1A3 /* SyncCore_PeerStatusSettings.swift */; }; + 27BA042826E4ECD2008FC1A3 /* SyncCore_StickerPackItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA022A26E4ECD2008FC1A3 /* SyncCore_StickerPackItem.swift */; }; + 27BA042926E4ECD2008FC1A3 /* SyncCore_SynchronizeSavedGifsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA022B26E4ECD2008FC1A3 /* SyncCore_SynchronizeSavedGifsOperation.swift */; }; + 27BA042A26E4ECD2008FC1A3 /* SyncCore_AccountBackupDataAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA022C26E4ECD2008FC1A3 /* SyncCore_AccountBackupDataAttribute.swift */; }; + 27BA042B26E4ECD2008FC1A3 /* SyncCore_RecentMediaItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA022D26E4ECD2008FC1A3 /* SyncCore_RecentMediaItem.swift */; }; + 27BA042C26E4ECD2008FC1A3 /* SyncCore_LoggedOutAccountAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA022E26E4ECD2008FC1A3 /* SyncCore_LoggedOutAccountAttribute.swift */; }; + 27BA042D26E4ECD2008FC1A3 /* SyncCore_EmojiKeywordCollectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA022F26E4ECD2008FC1A3 /* SyncCore_EmojiKeywordCollectionInfo.swift */; }; + 27BA042E26E4ECD2008FC1A3 /* SyncCore_RegularChatState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA023026E4ECD2008FC1A3 /* SyncCore_RegularChatState.swift */; }; + 27BA042F26E4ECD2008FC1A3 /* SyncCore_SynchronizePinnedChatsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA023126E4ECD2008FC1A3 /* SyncCore_SynchronizePinnedChatsOperation.swift */; }; + 27BA043026E4ECD2008FC1A3 /* SyncCore_ForwardCountMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA023226E4ECD2008FC1A3 /* SyncCore_ForwardCountMessageAttribute.swift */; }; + 27BA043126E4ECD2008FC1A3 /* SyncCore_SuggestedLocalizationUpdatesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA023326E4ECD2008FC1A3 /* SyncCore_SuggestedLocalizationUpdatesOperation.swift */; }; + 27BA043226E4ECD2008FC1A3 /* SyncCore_SynchronizeInstalledStickerPacksOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA023426E4ECD2008FC1A3 /* SyncCore_SynchronizeInstalledStickerPacksOperations.swift */; }; + 27BA043326E4ECD2008FC1A3 /* SyncCore_ContentPrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA023526E4ECD2008FC1A3 /* SyncCore_ContentPrivacySettings.swift */; }; + 27BA043426E4ECD2008FC1A3 /* SyncCore_FeaturedStickerPack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA023626E4ECD2008FC1A3 /* SyncCore_FeaturedStickerPack.swift */; }; + 27BA043526E4ECD2008FC1A3 /* SyncCore_TelegramMediaWebFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA023726E4ECD2008FC1A3 /* SyncCore_TelegramMediaWebFile.swift */; }; + 27BA043626E4ECD2008FC1A3 /* SyncCore_OutgoingContentInfoMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA023826E4ECD2008FC1A3 /* SyncCore_OutgoingContentInfoMessageAttribute.swift */; }; + 27BA043726E4ECD2008FC1A3 /* SyncCore_SecretChatKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA023926E4ECD2008FC1A3 /* SyncCore_SecretChatKeychain.swift */; }; + 27BA043826E4ECD2008FC1A3 /* SyncCore_VoipConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA023A26E4ECD2008FC1A3 /* SyncCore_VoipConfiguration.swift */; }; + 27BA043926E4ECD2008FC1A3 /* SyncCore_EmbeddedMediaStickersMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA023B26E4ECD2008FC1A3 /* SyncCore_EmbeddedMediaStickersMessageAttribute.swift */; }; + 27BA043A26E4ECD2008FC1A3 /* SyncCore_TelegramMediaInvoice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA023C26E4ECD2008FC1A3 /* SyncCore_TelegramMediaInvoice.swift */; }; + 27BA043B26E4ECD2008FC1A3 /* SyncCore_EmojiSearchQueryMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA023D26E4ECD2008FC1A3 /* SyncCore_EmojiSearchQueryMessageAttribute.swift */; }; + 27BA043C26E4ECD2008FC1A3 /* SyncCore_CloudChatRemoveMessagesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA023E26E4ECD2008FC1A3 /* SyncCore_CloudChatRemoveMessagesOperation.swift */; }; + 27BA043D26E4ECD2008FC1A3 /* SyncCore_PeerGroupMessageStateVersionAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA023F26E4ECD2008FC1A3 /* SyncCore_PeerGroupMessageStateVersionAttribute.swift */; }; + 27BA043E26E4ECD2008FC1A3 /* SyncCore_CacheStorageSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA024026E4ECD2008FC1A3 /* SyncCore_CacheStorageSettings.swift */; }; + 27BA043F26E4ECD2008FC1A3 /* SyncCore_SecretChatIncomingEncryptedOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA024126E4ECD2008FC1A3 /* SyncCore_SecretChatIncomingEncryptedOperation.swift */; }; + 27BA044026E4ECD2008FC1A3 /* SyncCore_SecretChatSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA024226E4ECD2008FC1A3 /* SyncCore_SecretChatSettings.swift */; }; + 27BA044126E4ECD2008FC1A3 /* SyncCore_ConsumablePersonalMentionMessageAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA024326E4ECD2008FC1A3 /* SyncCore_ConsumablePersonalMentionMessageAttribute.swift */; }; + 27BA044226E4ECD2008FC1A3 /* SyncCore_SynchronizeConsumeMessageContentsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA024426E4ECD2008FC1A3 /* SyncCore_SynchronizeConsumeMessageContentsOperation.swift */; }; + 27BA044326E4ECD2008FC1A3 /* SyncCore_CachedWallpapersConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA024526E4ECD2008FC1A3 /* SyncCore_CachedWallpapersConfiguration.swift */; }; + 27BA044426E4ECD2008FC1A3 /* SyncCore_SynchronizeableChatInputState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA024626E4ECD2008FC1A3 /* SyncCore_SynchronizeableChatInputState.swift */; }; + 27BA044526E4ECD2008FC1A3 /* SyncCore_ContactsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA024726E4ECD2008FC1A3 /* SyncCore_ContactsSettings.swift */; }; + 27BA044626E4ECD2008FC1A3 /* SyncCore_SynchronizeGroupedPeersOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA024826E4ECD2008FC1A3 /* SyncCore_SynchronizeGroupedPeersOperation.swift */; }; + 27BA044726E4ECD2008FC1A3 /* SyncCore_StandaloneAccountTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA024926E4ECD2008FC1A3 /* SyncCore_StandaloneAccountTransaction.swift */; }; + 27BA044826E4ECD2008FC1A3 /* SyncCore_TelegramChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA024A26E4ECD2008FC1A3 /* SyncCore_TelegramChannel.swift */; }; + 27BA044926E4ECD2008FC1A3 /* AccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA024C26E4ECD2008FC1A3 /* AccountManager.swift */; }; + 27BA044A26E4ECD2008FC1A3 /* AccountIntermediateState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA024D26E4ECD2008FC1A3 /* AccountIntermediateState.swift */; }; + 27BA044B26E4ECD2008FC1A3 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA024E26E4ECD2008FC1A3 /* Account.swift */; }; + 27BA044C26E4ECD2008FC1A3 /* WebpagePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA024F26E4ECD2008FC1A3 /* WebpagePreview.swift */; }; + 27BA044D26E4ECD2008FC1A3 /* SecretChatRekeySession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA025126E4ECD2008FC1A3 /* SecretChatRekeySession.swift */; }; + 27BA044E26E4ECD2008FC1A3 /* SecretChatOutgoingOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA025226E4ECD2008FC1A3 /* SecretChatOutgoingOperation.swift */; }; + 27BA044F26E4ECD2008FC1A3 /* SecretChatEncryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA025326E4ECD2008FC1A3 /* SecretChatEncryption.swift */; }; + 27BA045026E4ECD2008FC1A3 /* SecretChatEncryptionConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA025426E4ECD2008FC1A3 /* SecretChatEncryptionConfig.swift */; }; + 27BA045126E4ECD2008FC1A3 /* SecretChatLayerNegotiation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA025526E4ECD2008FC1A3 /* SecretChatLayerNegotiation.swift */; }; + 27BA045226E4ECD2008FC1A3 /* SecretChatFileReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA025626E4ECD2008FC1A3 /* SecretChatFileReference.swift */; }; + 27BA045326E4ECD2008FC1A3 /* SecretChatIncomingEncryptedOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA025726E4ECD2008FC1A3 /* SecretChatIncomingEncryptedOperation.swift */; }; + 27BA045426E4ECD2008FC1A3 /* UpdateSecretChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA025826E4ECD2008FC1A3 /* UpdateSecretChat.swift */; }; + 27BA045526E4ECD2008FC1A3 /* SetSecretChatMessageAutoremoveTimeoutInteractively.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27BA025926E4ECD2008FC1A3 /* SetSecretChatMessageAutoremoveTimeoutInteractively.swift */; }; + A7919034240CF9C7002011CA /* Reachability.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7919033240CF9C7002011CA /* Reachability.framework */; }; + A7919036240CF9CC002011CA /* CryptoUtils.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7919035240CF9CC002011CA /* CryptoUtils.framework */; }; + A7919038240CF9D1002011CA /* NetworkLogging.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7919037240CF9D1002011CA /* NetworkLogging.framework */; }; + A7D281DE236C25500000A9BF /* SwiftSignalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D281DD236C25500000A9BF /* SwiftSignalKit.framework */; }; + A7D281E2236C25710000A9BF /* Postbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D281E1236C25710000A9BF /* Postbox.framework */; }; + A7D281E4236C257C0000A9BF /* TelegramApi.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D281E3236C257C0000A9BF /* TelegramApi.framework */; }; + A7D281EA236C27030000A9BF /* MtProtoKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D281E9236C27030000A9BF /* MtProtoKit.framework */; }; + D0B4186B1D7E03D5004562A4 /* TelegramCore.h in Headers */ = {isa = PBXBuildFile; fileRef = D0B418691D7E03D5004562A4 /* TelegramCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 279881BD260DCA7400C2AF8E /* TelegramCore.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = TelegramCore.xcconfig; sourceTree = ""; }; + 27BA004126E4ECD1008FC1A3 /* Wallpapers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Wallpapers.swift; sourceTree = ""; }; + 27BA004226E4ECD1008FC1A3 /* Authorization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Authorization.swift; sourceTree = ""; }; + 27BA004426E4ECD1008FC1A3 /* ContentSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentSettings.swift; sourceTree = ""; }; + 27BA004526E4ECD1008FC1A3 /* LoggingSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoggingSettings.swift; sourceTree = ""; }; + 27BA004626E4ECD1008FC1A3 /* LimitsConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LimitsConfiguration.swift; sourceTree = ""; }; + 27BA004726E4ECD1008FC1A3 /* ProxySettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProxySettings.swift; sourceTree = ""; }; + 27BA004826E4ECD1008FC1A3 /* PeerContactSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerContactSettings.swift; sourceTree = ""; }; + 27BA004926E4ECD1008FC1A3 /* CacheStorageSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheStorageSettings.swift; sourceTree = ""; }; + 27BA004A26E4ECD1008FC1A3 /* NetworkSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkSettings.swift; sourceTree = ""; }; + 27BA004B26E4ECD1008FC1A3 /* ContentPrivacySettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentPrivacySettings.swift; sourceTree = ""; }; + 27BA004C26E4ECD1008FC1A3 /* VoipConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoipConfiguration.swift; sourceTree = ""; }; + 27BA004D26E4ECD1008FC1A3 /* AutodownloadSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutodownloadSettings.swift; sourceTree = ""; }; + 27BA004E26E4ECD1008FC1A3 /* GlobalNotificationSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalNotificationSettings.swift; sourceTree = ""; }; + 27BA004F26E4ECD1008FC1A3 /* PrivacySettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivacySettings.swift; sourceTree = ""; }; + 27BA005026E4ECD1008FC1A3 /* PeerStatistics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerStatistics.swift; sourceTree = ""; }; + 27BA005226E4ECD1008FC1A3 /* NotificationAutolockReportManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationAutolockReportManager.swift; sourceTree = ""; }; + 27BA005326E4ECD1008FC1A3 /* MacInternalUpdater.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MacInternalUpdater.swift; sourceTree = ""; }; + 27BA005426E4ECD1008FC1A3 /* GroupReturnAndLeft.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupReturnAndLeft.swift; sourceTree = ""; }; + 27BA005626E4ECD1008FC1A3 /* StandaloneSendMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StandaloneSendMessage.swift; sourceTree = ""; }; + 27BA005726E4ECD1008FC1A3 /* ChatUpdatingMessageMedia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatUpdatingMessageMedia.swift; sourceTree = ""; }; + 27BA005826E4ECD1008FC1A3 /* EnqueueMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnqueueMessage.swift; sourceTree = ""; }; + 27BA005926E4ECD1008FC1A3 /* PendingMessageUploadedContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PendingMessageUploadedContent.swift; sourceTree = ""; }; + 27BA005A26E4ECD1008FC1A3 /* PendingUpdateMessageManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PendingUpdateMessageManager.swift; sourceTree = ""; }; + 27BA005B26E4ECD1008FC1A3 /* StandaloneUploadedMedia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StandaloneUploadedMedia.swift; sourceTree = ""; }; + 27BA005C26E4ECD1008FC1A3 /* RequestEditMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestEditMessage.swift; sourceTree = ""; }; + 27BA005D26E4ECD1008FC1A3 /* MessageStatistics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageStatistics.swift; sourceTree = ""; }; + 27BA005E26E4ECD1008FC1A3 /* Suggestions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Suggestions.swift; sourceTree = ""; }; + 27BA005F26E4ECD1008FC1A3 /* Themes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Themes.swift; sourceTree = ""; }; + 27BA006126E4ECD1008FC1A3 /* NetworkType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkType.swift; sourceTree = ""; }; + 27BA006226E4ECD1008FC1A3 /* MultipartFetch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultipartFetch.swift; sourceTree = ""; }; + 27BA006326E4ECD1008FC1A3 /* Network.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = ""; }; + 27BA006426E4ECD1008FC1A3 /* MultiplexedRequestManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiplexedRequestManager.swift; sourceTree = ""; }; + 27BA006526E4ECD1008FC1A3 /* Download.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Download.swift; sourceTree = ""; }; + 27BA006626E4ECD1008FC1A3 /* MultipartUpload.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultipartUpload.swift; sourceTree = ""; }; + 27BA006726E4ECD1008FC1A3 /* FetchedMediaResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchedMediaResource.swift; sourceTree = ""; }; + 27BA006826E4ECD1008FC1A3 /* ProxyServersStatuses.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProxyServersStatuses.swift; sourceTree = ""; }; + 27BA006926E4ECD1008FC1A3 /* FetchHttpResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchHttpResource.swift; sourceTree = ""; }; + 27BA006A26E4ECD1008FC1A3 /* UpdatePeers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdatePeers.swift; sourceTree = ""; }; + 27BA006B26E4ECD1008FC1A3 /* SplitTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplitTest.swift; sourceTree = ""; }; + 27BA006D26E4ECD1008FC1A3 /* ImageRepresentationsUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageRepresentationsUtils.swift; sourceTree = ""; }; + 27BA006E26E4ECD1008FC1A3 /* Coding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Coding.swift; sourceTree = ""; }; + 27BA006F26E4ECD1008FC1A3 /* MediaResourceNetworkStatsTag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaResourceNetworkStatsTag.swift; sourceTree = ""; }; + 27BA007026E4ECD1008FC1A3 /* StringFormat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringFormat.swift; sourceTree = ""; }; + 27BA007126E4ECD1008FC1A3 /* Log.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; }; + 27BA007226E4ECD1008FC1A3 /* PeerUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerUtils.swift; sourceTree = ""; }; + 27BA007326E4ECD1008FC1A3 /* DecryptedResourceData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DecryptedResourceData.swift; sourceTree = ""; }; + 27BA007426E4ECD1008FC1A3 /* MD5.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MD5.swift; sourceTree = ""; }; + 27BA007526E4ECD1008FC1A3 /* JSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = ""; }; + 27BA007626E4ECD1008FC1A3 /* UpdateMessageMedia.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateMessageMedia.swift; sourceTree = ""; }; + 27BA007726E4ECD1008FC1A3 /* MemoryBufferExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MemoryBufferExtensions.swift; sourceTree = ""; }; + 27BA007826E4ECD1008FC1A3 /* CanSendMessagesToPeer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CanSendMessagesToPeer.swift; sourceTree = ""; }; + 27BA007926E4ECD1008FC1A3 /* MessageUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageUtils.swift; sourceTree = ""; }; + 27BA007B26E4ECD1008FC1A3 /* ManagedSynchronizeSavedStickersOperations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedSynchronizeSavedStickersOperations.swift; sourceTree = ""; }; + 27BA007C26E4ECD1008FC1A3 /* AccountState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountState.swift; sourceTree = ""; }; + 27BA007D26E4ECD1008FC1A3 /* ProcessSecretChatIncomingDecryptedOperations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProcessSecretChatIncomingDecryptedOperations.swift; sourceTree = ""; }; + 27BA007E26E4ECD1008FC1A3 /* HistoryViewStateValidation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HistoryViewStateValidation.swift; sourceTree = ""; }; + 27BA007F26E4ECD1008FC1A3 /* SynchronizeEmojiKeywordsOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SynchronizeEmojiKeywordsOperation.swift; sourceTree = ""; }; + 27BA008026E4ECD1008FC1A3 /* ApplyUpdateMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApplyUpdateMessage.swift; sourceTree = ""; }; + 27BA008126E4ECD1008FC1A3 /* ChatHistoryPreloadManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryPreloadManager.swift; sourceTree = ""; }; + 27BA008226E4ECD1008FC1A3 /* AppConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppConfiguration.swift; sourceTree = ""; }; + 27BA008326E4ECD1008FC1A3 /* SynchronizeAppLogEventsOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SynchronizeAppLogEventsOperation.swift; sourceTree = ""; }; + 27BA008426E4ECD1008FC1A3 /* SynchronizeMarkAllUnseenPersonalMessagesOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SynchronizeMarkAllUnseenPersonalMessagesOperation.swift; sourceTree = ""; }; + 27BA008526E4ECD1008FC1A3 /* ManagedSynchronizeEmojiKeywordsOperations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedSynchronizeEmojiKeywordsOperations.swift; sourceTree = ""; }; + 27BA008626E4ECD1008FC1A3 /* UpdatesApiUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdatesApiUtils.swift; sourceTree = ""; }; + 27BA008726E4ECD1008FC1A3 /* ManagedNotificationSettingsBehaviors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedNotificationSettingsBehaviors.swift; sourceTree = ""; }; + 27BA008826E4ECD1008FC1A3 /* MessageMediaPreuploadManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageMediaPreuploadManager.swift; sourceTree = ""; }; + 27BA008926E4ECD1008FC1A3 /* ManagedPendingPeerNotificationSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedPendingPeerNotificationSettings.swift; sourceTree = ""; }; + 27BA008A26E4ECD1008FC1A3 /* SynchronizeRecentlyUsedMediaOperations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SynchronizeRecentlyUsedMediaOperations.swift; sourceTree = ""; }; + 27BA008B26E4ECD1008FC1A3 /* ManagedMessageHistoryHoles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedMessageHistoryHoles.swift; sourceTree = ""; }; + 27BA008C26E4ECD1008FC1A3 /* ManagedConsumePersonalMessagesActions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedConsumePersonalMessagesActions.swift; sourceTree = ""; }; + 27BA008D26E4ECD1008FC1A3 /* ManagedProxyInfoUpdates.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedProxyInfoUpdates.swift; sourceTree = ""; }; + 27BA008E26E4ECD2008FC1A3 /* ManagedAppConfigurationUpdates.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedAppConfigurationUpdates.swift; sourceTree = ""; }; + 27BA008F26E4ECD2008FC1A3 /* AccountStateManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountStateManager.swift; sourceTree = ""; }; + 27BA009026E4ECD2008FC1A3 /* CachedSentMediaReferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachedSentMediaReferences.swift; sourceTree = ""; }; + 27BA009126E4ECD2008FC1A3 /* ManagedLocalizationUpdatesOperations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedLocalizationUpdatesOperations.swift; sourceTree = ""; }; + 27BA009226E4ECD2008FC1A3 /* FetchChatList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchChatList.swift; sourceTree = ""; }; + 27BA009326E4ECD2008FC1A3 /* ManagedGlobalNotificationSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedGlobalNotificationSettings.swift; sourceTree = ""; }; + 27BA009426E4ECD2008FC1A3 /* FetchSecretFileResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchSecretFileResource.swift; sourceTree = ""; }; + 27BA009526E4ECD2008FC1A3 /* ChannelState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelState.swift; sourceTree = ""; }; + 27BA009626E4ECD2008FC1A3 /* ManagedSynchronizeRecentlyUsedMediaOperations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedSynchronizeRecentlyUsedMediaOperations.swift; sourceTree = ""; }; + 27BA009726E4ECD2008FC1A3 /* ManagedRecentStickers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedRecentStickers.swift; sourceTree = ""; }; + 27BA009826E4ECD2008FC1A3 /* ManagedChatListHoles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedChatListHoles.swift; sourceTree = ""; }; + 27BA009926E4ECD2008FC1A3 /* ManagedSynchronizeMarkAllUnseenPersonalMessagesOperations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedSynchronizeMarkAllUnseenPersonalMessagesOperations.swift; sourceTree = ""; }; + 27BA009A26E4ECD2008FC1A3 /* ManagedServiceViews.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedServiceViews.swift; sourceTree = ""; }; + 27BA009B26E4ECD2008FC1A3 /* PeerInputActivityManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInputActivityManager.swift; sourceTree = ""; }; + 27BA009C26E4ECD2008FC1A3 /* ManagedSynchronizePinnedChatsOperations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedSynchronizePinnedChatsOperations.swift; sourceTree = ""; }; + 27BA009D26E4ECD2008FC1A3 /* SynchronizeSavedStickersOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SynchronizeSavedStickersOperation.swift; sourceTree = ""; }; + 27BA009E26E4ECD2008FC1A3 /* MessageReactions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageReactions.swift; sourceTree = ""; }; + 27BA009F26E4ECD2008FC1A3 /* ManagedCloudChatRemoveMessagesOperations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedCloudChatRemoveMessagesOperations.swift; sourceTree = ""; }; + 27BA00A026E4ECD2008FC1A3 /* PendingMessageManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PendingMessageManager.swift; sourceTree = ""; }; + 27BA00A126E4ECD2008FC1A3 /* StickerManagement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickerManagement.swift; sourceTree = ""; }; + 27BA00A226E4ECD2008FC1A3 /* InitializeAccountAfterLogin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InitializeAccountAfterLogin.swift; sourceTree = ""; }; + 27BA00A326E4ECD2008FC1A3 /* UpdateGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateGroup.swift; sourceTree = ""; }; + 27BA00A426E4ECD2008FC1A3 /* ManagedSynchronizeGroupMessageStats.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedSynchronizeGroupMessageStats.swift; sourceTree = ""; }; + 27BA00A526E4ECD2008FC1A3 /* Fetch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Fetch.swift; sourceTree = ""; }; + 27BA00A626E4ECD2008FC1A3 /* ManagedSynchronizeMarkFeaturedStickerPacksAsSeenOperations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedSynchronizeMarkFeaturedStickerPacksAsSeenOperations.swift; sourceTree = ""; }; + 27BA00A726E4ECD2008FC1A3 /* SynchronizeLocalizationUpdatesOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SynchronizeLocalizationUpdatesOperation.swift; sourceTree = ""; }; + 27BA00A826E4ECD2008FC1A3 /* ManagedAnimatedEmojiUpdates.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedAnimatedEmojiUpdates.swift; sourceTree = ""; }; + 27BA00A926E4ECD2008FC1A3 /* UpdateMessageService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateMessageService.swift; sourceTree = ""; }; + 27BA00AA26E4ECD2008FC1A3 /* SynchronizeInstalledStickerPacksOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SynchronizeInstalledStickerPacksOperation.swift; sourceTree = ""; }; + 27BA00AB26E4ECD2008FC1A3 /* ManagedLocalInputActivities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedLocalInputActivities.swift; sourceTree = ""; }; + 27BA00AC26E4ECD2008FC1A3 /* ManagedSynchronizeInstalledStickerPacksOperations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedSynchronizeInstalledStickerPacksOperations.swift; sourceTree = ""; }; + 27BA00AD26E4ECD2008FC1A3 /* ManagedSynchronizeGroupedPeersOperations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedSynchronizeGroupedPeersOperations.swift; sourceTree = ""; }; + 27BA00AE26E4ECD2008FC1A3 /* CallSessionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallSessionManager.swift; sourceTree = ""; }; + 27BA00AF26E4ECD2008FC1A3 /* ManagedSecretChatOutgoingOperations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedSecretChatOutgoingOperations.swift; sourceTree = ""; }; + 27BA00B026E4ECD2008FC1A3 /* ManagedSynchronizeConsumeMessageContentsOperations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedSynchronizeConsumeMessageContentsOperations.swift; sourceTree = ""; }; + 27BA00B126E4ECD2008FC1A3 /* PeerInputActivity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInputActivity.swift; sourceTree = ""; }; + 27BA00B226E4ECD2008FC1A3 /* SynchronizeSavedGifsOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SynchronizeSavedGifsOperation.swift; sourceTree = ""; }; + 27BA00B326E4ECD2008FC1A3 /* ManagedAutoremoveMessageOperations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedAutoremoveMessageOperations.swift; sourceTree = ""; }; + 27BA00B426E4ECD2008FC1A3 /* AppUpdate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppUpdate.swift; sourceTree = ""; }; + 27BA00B526E4ECD2008FC1A3 /* AccountViewTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountViewTracker.swift; sourceTree = ""; }; + 27BA00B626E4ECD2008FC1A3 /* SynchronizeChatInputStateOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SynchronizeChatInputStateOperation.swift; sourceTree = ""; }; + 27BA00B726E4ECD2008FC1A3 /* SynchronizeGroupedPeersOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SynchronizeGroupedPeersOperation.swift; sourceTree = ""; }; + 27BA00B826E4ECD2008FC1A3 /* ProcessSecretChatIncomingEncryptedOperations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProcessSecretChatIncomingEncryptedOperations.swift; sourceTree = ""; }; + 27BA00B926E4ECD2008FC1A3 /* UnauthorizedAccountStateManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnauthorizedAccountStateManager.swift; sourceTree = ""; }; + 27BA00BA26E4ECD2008FC1A3 /* ContactSyncManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactSyncManager.swift; sourceTree = ""; }; + 27BA00BB26E4ECD2008FC1A3 /* ManagedAccountPresence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedAccountPresence.swift; sourceTree = ""; }; + 27BA00BC26E4ECD2008FC1A3 /* ManagedSynchronizePeerReadStates.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedSynchronizePeerReadStates.swift; sourceTree = ""; }; + 27BA00BD26E4ECD2008FC1A3 /* SynchronizeConsumeMessageContentsOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SynchronizeConsumeMessageContentsOperation.swift; sourceTree = ""; }; + 27BA00BE26E4ECD2008FC1A3 /* CloudChatRemoveMessagesOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudChatRemoveMessagesOperation.swift; sourceTree = ""; }; + 27BA00BF26E4ECD2008FC1A3 /* Holes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Holes.swift; sourceTree = ""; }; + 27BA00C026E4ECD2008FC1A3 /* ManagedSynchronizeChatInputStateOperations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedSynchronizeChatInputStateOperations.swift; sourceTree = ""; }; + 27BA00C126E4ECD2008FC1A3 /* AppChangelogState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppChangelogState.swift; sourceTree = ""; }; + 27BA00C226E4ECD2008FC1A3 /* ManagedSynchronizeAppLogEventsOperations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedSynchronizeAppLogEventsOperations.swift; sourceTree = ""; }; + 27BA00C326E4ECD2008FC1A3 /* SynchronizePeerReadState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SynchronizePeerReadState.swift; sourceTree = ""; }; + 27BA00C426E4ECD2008FC1A3 /* ManagedSynchronizeSavedGifsOperations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedSynchronizeSavedGifsOperations.swift; sourceTree = ""; }; + 27BA00C526E4ECD2008FC1A3 /* ManagedConfigurationUpdates.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedConfigurationUpdates.swift; sourceTree = ""; }; + 27BA00C626E4ECD2008FC1A3 /* ManagedVoipConfigurationUpdates.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedVoipConfigurationUpdates.swift; sourceTree = ""; }; + 27BA00C726E4ECD2008FC1A3 /* AppChangelog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppChangelog.swift; sourceTree = ""; }; + 27BA00C826E4ECD2008FC1A3 /* AccountStateManagementUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountStateManagementUtils.swift; sourceTree = ""; }; + 27BA00C926E4ECD2008FC1A3 /* ManagedAutodownloadSettingsUpdates.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedAutodownloadSettingsUpdates.swift; sourceTree = ""; }; + 27BA00CA26E4ECD2008FC1A3 /* Serialization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Serialization.swift; sourceTree = ""; }; + 27BA00CB26E4ECD2008FC1A3 /* LoadedPeer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadedPeer.swift; sourceTree = ""; }; + 27BA00CC26E4ECD2008FC1A3 /* LoadedPeerFromMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadedPeerFromMessage.swift; sourceTree = ""; }; + 27BA00CD26E4ECD2008FC1A3 /* UpdatePeerChatInterfaceState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdatePeerChatInterfaceState.swift; sourceTree = ""; }; + 27BA00D026E4ECD2008FC1A3 /* TelegramEngineCalls.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramEngineCalls.swift; sourceTree = ""; }; + 27BA00D126E4ECD2008FC1A3 /* GroupCalls.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupCalls.swift; sourceTree = ""; }; + 27BA00D226E4ECD2008FC1A3 /* RateCall.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RateCall.swift; sourceTree = ""; }; + 27BA00D426E4ECD2008FC1A3 /* SecureIdPersonalDetailsValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureIdPersonalDetailsValue.swift; sourceTree = ""; }; + 27BA00D526E4ECD2008FC1A3 /* SecureIdValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureIdValue.swift; sourceTree = ""; }; + 27BA00D626E4ECD2008FC1A3 /* RequestSecureIdForm.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestSecureIdForm.swift; sourceTree = ""; }; + 27BA00D726E4ECD2008FC1A3 /* SecureIdAddressValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureIdAddressValue.swift; sourceTree = ""; }; + 27BA00D826E4ECD2008FC1A3 /* SecureIdEmailValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureIdEmailValue.swift; sourceTree = ""; }; + 27BA00D926E4ECD2008FC1A3 /* SecureIdUtilityBillValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureIdUtilityBillValue.swift; sourceTree = ""; }; + 27BA00DA26E4ECD2008FC1A3 /* TelegramEngineSecureId.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramEngineSecureId.swift; sourceTree = ""; }; + 27BA00DB26E4ECD2008FC1A3 /* SecureIdValueAccessContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureIdValueAccessContext.swift; sourceTree = ""; }; + 27BA00DC26E4ECD2008FC1A3 /* SecureIdPadding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureIdPadding.swift; sourceTree = ""; }; + 27BA00DD26E4ECD2008FC1A3 /* SecureIdConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureIdConfiguration.swift; sourceTree = ""; }; + 27BA00DE26E4ECD2008FC1A3 /* SaveSecureIdValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SaveSecureIdValue.swift; sourceTree = ""; }; + 27BA00DF26E4ECD2008FC1A3 /* SecureIdIDCardValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureIdIDCardValue.swift; sourceTree = ""; }; + 27BA00E026E4ECD2008FC1A3 /* SecureIdPassportRegistrationValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureIdPassportRegistrationValue.swift; sourceTree = ""; }; + 27BA00E126E4ECD2008FC1A3 /* VerifySecureIdValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerifySecureIdValue.swift; sourceTree = ""; }; + 27BA00E226E4ECD2008FC1A3 /* SecureIdDriversLicenseValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureIdDriversLicenseValue.swift; sourceTree = ""; }; + 27BA00E326E4ECD2008FC1A3 /* SecureIdRentalAgreementValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureIdRentalAgreementValue.swift; sourceTree = ""; }; + 27BA00E426E4ECD2008FC1A3 /* GrantSecureIdAccess.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GrantSecureIdAccess.swift; sourceTree = ""; }; + 27BA00E526E4ECD2008FC1A3 /* SecureIdForm.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureIdForm.swift; sourceTree = ""; }; + 27BA00E626E4ECD2008FC1A3 /* SecureIdVerificationDocumentReference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureIdVerificationDocumentReference.swift; sourceTree = ""; }; + 27BA00E726E4ECD2008FC1A3 /* SecureIdBankStatementValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureIdBankStatementValue.swift; sourceTree = ""; }; + 27BA00E826E4ECD2008FC1A3 /* AccessSecureId.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessSecureId.swift; sourceTree = ""; }; + 27BA00E926E4ECD2008FC1A3 /* SecureFileMediaResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureFileMediaResource.swift; sourceTree = ""; }; + 27BA00EA26E4ECD2008FC1A3 /* UploadSecureIdFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UploadSecureIdFile.swift; sourceTree = ""; }; + 27BA00EB26E4ECD2008FC1A3 /* SecureIdValueContentError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureIdValueContentError.swift; sourceTree = ""; }; + 27BA00EC26E4ECD2008FC1A3 /* SecureIdDataTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureIdDataTypes.swift; sourceTree = ""; }; + 27BA00ED26E4ECD2008FC1A3 /* SecureIdInternalPassportValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureIdInternalPassportValue.swift; sourceTree = ""; }; + 27BA00EE26E4ECD2008FC1A3 /* SecureIdPassportValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureIdPassportValue.swift; sourceTree = ""; }; + 27BA00EF26E4ECD2008FC1A3 /* SecureIdTemporaryRegistrationValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureIdTemporaryRegistrationValue.swift; sourceTree = ""; }; + 27BA00F026E4ECD2008FC1A3 /* SecureIdPhoneValue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureIdPhoneValue.swift; sourceTree = ""; }; + 27BA00F226E4ECD2008FC1A3 /* TelegramEnginePayments.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramEnginePayments.swift; sourceTree = ""; }; + 27BA00F326E4ECD2008FC1A3 /* BotPaymentForm.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotPaymentForm.swift; sourceTree = ""; }; + 27BA00F426E4ECD2008FC1A3 /* BankCards.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BankCards.swift; sourceTree = ""; }; + 27BA00F626E4ECD2008FC1A3 /* ImportStickers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportStickers.swift; sourceTree = ""; }; + 27BA00F726E4ECD2008FC1A3 /* StickerPack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickerPack.swift; sourceTree = ""; }; + 27BA00F826E4ECD2008FC1A3 /* SearchStickers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchStickers.swift; sourceTree = ""; }; + 27BA00F926E4ECD2008FC1A3 /* TelegramEngineStickers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramEngineStickers.swift; sourceTree = ""; }; + 27BA00FA26E4ECD2008FC1A3 /* LoadedStickerPack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadedStickerPack.swift; sourceTree = ""; }; + 27BA00FB26E4ECD2008FC1A3 /* StickerPackInteractiveOperations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickerPackInteractiveOperations.swift; sourceTree = ""; }; + 27BA00FC26E4ECD2008FC1A3 /* EmojiKeywords.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiKeywords.swift; sourceTree = ""; }; + 27BA00FD26E4ECD2008FC1A3 /* ArchivedStickerPacks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArchivedStickerPacks.swift; sourceTree = ""; }; + 27BA00FE26E4ECD2008FC1A3 /* CachedStickerPack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachedStickerPack.swift; sourceTree = ""; }; + 27BA00FF26E4ECD2008FC1A3 /* StickerSetInstallation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickerSetInstallation.swift; sourceTree = ""; }; + 27BA010126E4ECD2008FC1A3 /* ClearCloudDrafts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClearCloudDrafts.swift; sourceTree = ""; }; + 27BA010226E4ECD2008FC1A3 /* InstallInteractiveReadMessagesAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstallInteractiveReadMessagesAction.swift; sourceTree = ""; }; + 27BA010326E4ECD2008FC1A3 /* TelegramEngineMessages.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramEngineMessages.swift; sourceTree = ""; }; + 27BA010426E4ECD2008FC1A3 /* Polls.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Polls.swift; sourceTree = ""; }; + 27BA010526E4ECD2008FC1A3 /* SearchMessages.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchMessages.swift; sourceTree = ""; }; + 27BA010626E4ECD2008FC1A3 /* ChatList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatList.swift; sourceTree = ""; }; + 27BA010726E4ECD2008FC1A3 /* LoadMessagesIfNecessary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadMessagesIfNecessary.swift; sourceTree = ""; }; + 27BA010826E4ECD2008FC1A3 /* RequestStartBot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestStartBot.swift; sourceTree = ""; }; + 27BA010926E4ECD2008FC1A3 /* ReplyThreadHistory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplyThreadHistory.swift; sourceTree = ""; }; + 27BA010A26E4ECD2008FC1A3 /* CallList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallList.swift; sourceTree = ""; }; + 27BA010B26E4ECD2008FC1A3 /* Message.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; + 27BA010C26E4ECD2008FC1A3 /* RecentlyUsedHashtags.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentlyUsedHashtags.swift; sourceTree = ""; }; + 27BA010D26E4ECD2008FC1A3 /* UpdatePinnedMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdatePinnedMessage.swift; sourceTree = ""; }; + 27BA010E26E4ECD2008FC1A3 /* EarliestUnseenPersonalMentionMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EarliestUnseenPersonalMentionMessage.swift; sourceTree = ""; }; + 27BA010F26E4ECD2008FC1A3 /* ForwardGame.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForwardGame.swift; sourceTree = ""; }; + 27BA011026E4ECD2008FC1A3 /* ApplyMaxReadIndexInteractively.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApplyMaxReadIndexInteractively.swift; sourceTree = ""; }; + 27BA011126E4ECD2008FC1A3 /* Media.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Media.swift; sourceTree = ""; }; + 27BA011226E4ECD2008FC1A3 /* PeerLiveLocationsContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerLiveLocationsContext.swift; sourceTree = ""; }; + 27BA011326E4ECD2008FC1A3 /* OutgoingMessageWithChatContextResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutgoingMessageWithChatContextResult.swift; sourceTree = ""; }; + 27BA011426E4ECD2008FC1A3 /* EngineGroupCallDescription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EngineGroupCallDescription.swift; sourceTree = ""; }; + 27BA011526E4ECD2008FC1A3 /* MessageReadStats.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageReadStats.swift; sourceTree = ""; }; + 27BA011626E4ECD2008FC1A3 /* ScheduledMessages.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScheduledMessages.swift; sourceTree = ""; }; + 27BA011726E4ECD2008FC1A3 /* DeleteMessages.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteMessages.swift; sourceTree = ""; }; + 27BA011826E4ECD2008FC1A3 /* RequestMessageActionCallback.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestMessageActionCallback.swift; sourceTree = ""; }; + 27BA011926E4ECD2008FC1A3 /* MarkAllChatsAsRead.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkAllChatsAsRead.swift; sourceTree = ""; }; + 27BA011A26E4ECD2008FC1A3 /* DeleteMessagesInteractively.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteMessagesInteractively.swift; sourceTree = ""; }; + 27BA011B26E4ECD2008FC1A3 /* RequestChatContextResults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestChatContextResults.swift; sourceTree = ""; }; + 27BA011C26E4ECD2008FC1A3 /* MarkMessageContentAsConsumedInteractively.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkMessageContentAsConsumedInteractively.swift; sourceTree = ""; }; + 27BA011D26E4ECD2008FC1A3 /* ReadState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReadState.swift; sourceTree = ""; }; + 27BA011E26E4ECD2008FC1A3 /* AdMessages.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdMessages.swift; sourceTree = ""; }; + 27BA011F26E4ECD2008FC1A3 /* ExportMessageLink.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExportMessageLink.swift; sourceTree = ""; }; + 27BA012126E4ECD2008FC1A3 /* BlockedPeers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockedPeers.swift; sourceTree = ""; }; + 27BA012226E4ECD2008FC1A3 /* UpdatedAccountPrivacySettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdatedAccountPrivacySettings.swift; sourceTree = ""; }; + 27BA012326E4ECD2008FC1A3 /* BlockedPeersContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockedPeersContext.swift; sourceTree = ""; }; + 27BA012426E4ECD2008FC1A3 /* TelegramEnginePrivacy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramEnginePrivacy.swift; sourceTree = ""; }; + 27BA012526E4ECD2008FC1A3 /* RecentAccountSessions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentAccountSessions.swift; sourceTree = ""; }; + 27BA012626E4ECD2008FC1A3 /* RecentWebSessions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentWebSessions.swift; sourceTree = ""; }; + 27BA012726E4ECD2008FC1A3 /* RecentAccountSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentAccountSession.swift; sourceTree = ""; }; + 27BA012826E4ECD2008FC1A3 /* ActiveSessionsContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActiveSessionsContext.swift; sourceTree = ""; }; + 27BA012A26E4ECD2008FC1A3 /* TelegramEngineResolve.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramEngineResolve.swift; sourceTree = ""; }; + 27BA012B26E4ECD2008FC1A3 /* DeepLinkInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeepLinkInfo.swift; sourceTree = ""; }; + 27BA012D26E4ECD2008FC1A3 /* TelegramEngineAuth.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramEngineAuth.swift; sourceTree = ""; }; + 27BA012E26E4ECD2008FC1A3 /* CancelAccountReset.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CancelAccountReset.swift; sourceTree = ""; }; + 27BA012F26E4ECD2008FC1A3 /* ConfirmTwoStepRecoveryEmail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfirmTwoStepRecoveryEmail.swift; sourceTree = ""; }; + 27BA013026E4ECD2008FC1A3 /* AuthTransfer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthTransfer.swift; sourceTree = ""; }; + 27BA013126E4ECD2008FC1A3 /* TwoStepVerification.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TwoStepVerification.swift; sourceTree = ""; }; + 27BA013326E4ECD2008FC1A3 /* PhoneNumber.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhoneNumber.swift; sourceTree = ""; }; + 27BA013426E4ECD2008FC1A3 /* TelegramEngineContacts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramEngineContacts.swift; sourceTree = ""; }; + 27BA013526E4ECD2008FC1A3 /* ContactManagement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactManagement.swift; sourceTree = ""; }; + 27BA013626E4ECD2008FC1A3 /* ImportContact.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportContact.swift; sourceTree = ""; }; + 27BA013726E4ECD2008FC1A3 /* UpdateContactName.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateContactName.swift; sourceTree = ""; }; + 27BA013826E4ECD2008FC1A3 /* DeviceContact.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceContact.swift; sourceTree = ""; }; + 27BA013926E4ECD2008FC1A3 /* TelegramDeviceContactImportInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramDeviceContactImportInfo.swift; sourceTree = ""; }; + 27BA013B26E4ECD2008FC1A3 /* CollectCacheUsageStats.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectCacheUsageStats.swift; sourceTree = ""; }; + 27BA013C26E4ECD2008FC1A3 /* TelegramEngineResources.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramEngineResources.swift; sourceTree = ""; }; + 27BA013E26E4ECD2008FC1A3 /* TelegramEngineHistoryImport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramEngineHistoryImport.swift; sourceTree = ""; }; + 27BA014026E4ECD2008FC1A3 /* StringCodingKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringCodingKey.swift; sourceTree = ""; }; + 27BA014226E4ECD2008FC1A3 /* PeersNearby.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeersNearby.swift; sourceTree = ""; }; + 27BA014326E4ECD2008FC1A3 /* TelegramEnginePeersNearby.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramEnginePeersNearby.swift; sourceTree = ""; }; + 27BA014426E4ECD2008FC1A3 /* TelegramEngine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramEngine.swift; sourceTree = ""; }; + 27BA014626E4ECD2008FC1A3 /* RemovePeerChat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemovePeerChat.swift; sourceTree = ""; }; + 27BA014726E4ECD2008FC1A3 /* ChannelOwnershipTransfer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelOwnershipTransfer.swift; sourceTree = ""; }; + 27BA014826E4ECD2008FC1A3 /* ChangePeerNotificationSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangePeerNotificationSettings.swift; sourceTree = ""; }; + 27BA014926E4ECD2008FC1A3 /* ManageChannelDiscussionGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageChannelDiscussionGroup.swift; sourceTree = ""; }; + 27BA014A26E4ECD2008FC1A3 /* RequestUserPhotos.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestUserPhotos.swift; sourceTree = ""; }; + 27BA014B26E4ECD2008FC1A3 /* ChatListFiltering.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListFiltering.swift; sourceTree = ""; }; + 27BA014C26E4ECD2008FC1A3 /* SupportPeerId.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SupportPeerId.swift; sourceTree = ""; }; + 27BA014D26E4ECD2008FC1A3 /* ChannelHistoryAvailabilitySettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelHistoryAvailabilitySettings.swift; sourceTree = ""; }; + 27BA014E26E4ECD2008FC1A3 /* JoinChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JoinChannel.swift; sourceTree = ""; }; + 27BA014F26E4ECD2008FC1A3 /* UpdateCachedPeerData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateCachedPeerData.swift; sourceTree = ""; }; + 27BA015026E4ECD2008FC1A3 /* GroupsInCommon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupsInCommon.swift; sourceTree = ""; }; + 27BA015126E4ECD2008FC1A3 /* RecentPeers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentPeers.swift; sourceTree = ""; }; + 27BA015226E4ECD2008FC1A3 /* UpdateGroupSpecificStickerset.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateGroupSpecificStickerset.swift; sourceTree = ""; }; + 27BA015326E4ECD2008FC1A3 /* NotificationExceptionsList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationExceptionsList.swift; sourceTree = ""; }; + 27BA015426E4ECD2008FC1A3 /* ChannelAdminEventLogContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelAdminEventLogContext.swift; sourceTree = ""; }; + 27BA015526E4ECD2008FC1A3 /* ChannelAdminEventLogs.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelAdminEventLogs.swift; sourceTree = ""; }; + 27BA015626E4ECD2008FC1A3 /* CheckPeerChatServiceActions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheckPeerChatServiceActions.swift; sourceTree = ""; }; + 27BA015726E4ECD2008FC1A3 /* SearchGroupMembers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchGroupMembers.swift; sourceTree = ""; }; + 27BA015826E4ECD2008FC1A3 /* JoinLink.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JoinLink.swift; sourceTree = ""; }; + 27BA015926E4ECD2008FC1A3 /* AddPeerMember.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddPeerMember.swift; sourceTree = ""; }; + 27BA015A26E4ECD2008FC1A3 /* ReportPeer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReportPeer.swift; sourceTree = ""; }; + 27BA015B26E4ECD2008FC1A3 /* ChannelBlacklist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelBlacklist.swift; sourceTree = ""; }; + 27BA015C26E4ECD2008FC1A3 /* InactiveChannels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InactiveChannels.swift; sourceTree = ""; }; + 27BA015D26E4ECD2008FC1A3 /* PeerPhotoUpdater.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerPhotoUpdater.swift; sourceTree = ""; }; + 27BA015E26E4ECD2008FC1A3 /* ChatOnlineMembers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatOnlineMembers.swift; sourceTree = ""; }; + 27BA015F26E4ECD2008FC1A3 /* SlowMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SlowMode.swift; sourceTree = ""; }; + 27BA016026E4ECD2008FC1A3 /* PeerAdmins.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerAdmins.swift; sourceTree = ""; }; + 27BA016126E4ECD2008FC1A3 /* ToggleChannelSignatures.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToggleChannelSignatures.swift; sourceTree = ""; }; + 27BA016226E4ECD2008FC1A3 /* ConvertGroupToSupergroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConvertGroupToSupergroup.swift; sourceTree = ""; }; + 27BA016326E4ECD2008FC1A3 /* CreateSecretChat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateSecretChat.swift; sourceTree = ""; }; + 27BA016426E4ECD2008FC1A3 /* CreateGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateGroup.swift; sourceTree = ""; }; + 27BA016526E4ECD2008FC1A3 /* PeerCommands.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerCommands.swift; sourceTree = ""; }; + 27BA016626E4ECD2008FC1A3 /* TogglePeerChatPinned.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TogglePeerChatPinned.swift; sourceTree = ""; }; + 27BA016726E4ECD2008FC1A3 /* ChannelCreation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelCreation.swift; sourceTree = ""; }; + 27BA016826E4ECD2008FC1A3 /* AddressNames.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddressNames.swift; sourceTree = ""; }; + 27BA016926E4ECD2008FC1A3 /* PeerSpecificStickerPack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerSpecificStickerPack.swift; sourceTree = ""; }; + 27BA016A26E4ECD2008FC1A3 /* InvitationLinks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InvitationLinks.swift; sourceTree = ""; }; + 27BA016B26E4ECD2008FC1A3 /* ResolvePeerByName.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResolvePeerByName.swift; sourceTree = ""; }; + 27BA016C26E4ECD2008FC1A3 /* FindChannelById.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FindChannelById.swift; sourceTree = ""; }; + 27BA016D26E4ECD2008FC1A3 /* SearchPeers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchPeers.swift; sourceTree = ""; }; + 27BA016E26E4ECD2008FC1A3 /* UpdatePeerInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdatePeerInfo.swift; sourceTree = ""; }; + 27BA016F26E4ECD2008FC1A3 /* OpaqueChatState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpaqueChatState.swift; sourceTree = ""; }; + 27BA017026E4ECD2008FC1A3 /* TelegramEnginePeers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramEnginePeers.swift; sourceTree = ""; }; + 27BA017126E4ECD2008FC1A3 /* ChannelMembers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelMembers.swift; sourceTree = ""; }; + 27BA017226E4ECD2008FC1A3 /* Peer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Peer.swift; sourceTree = ""; }; + 27BA017326E4ECD2008FC1A3 /* ChannelParticipants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelParticipants.swift; sourceTree = ""; }; + 27BA017426E4ECD2008FC1A3 /* RemovePeerMember.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemovePeerMember.swift; sourceTree = ""; }; + 27BA017526E4ECD2008FC1A3 /* RecentlySearchedPeerIds.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentlySearchedPeerIds.swift; sourceTree = ""; }; + 27BA017726E4ECD2008FC1A3 /* TelegramEngineData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramEngineData.swift; sourceTree = ""; }; + 27BA017826E4ECD2008FC1A3 /* PeerSummary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerSummary.swift; sourceTree = ""; }; + 27BA017926E4ECD2008FC1A3 /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; + 27BA017B26E4ECD2008FC1A3 /* RegisterNotificationToken.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegisterNotificationToken.swift; sourceTree = ""; }; + 27BA017C26E4ECD2008FC1A3 /* UpdateAccountPeerName.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateAccountPeerName.swift; sourceTree = ""; }; + 27BA017D26E4ECD2008FC1A3 /* TelegramEngineAccountData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramEngineAccountData.swift; sourceTree = ""; }; + 27BA017E26E4ECD2008FC1A3 /* ChangeAccountPhoneNumber.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChangeAccountPhoneNumber.swift; sourceTree = ""; }; + 27BA017F26E4ECD2008FC1A3 /* TermsOfService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TermsOfService.swift; sourceTree = ""; }; + 27BA018126E4ECD2008FC1A3 /* TelegramEngineThemes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramEngineThemes.swift; sourceTree = ""; }; + 27BA018226E4ECD2008FC1A3 /* ChatThemes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatThemes.swift; sourceTree = ""; }; + 27BA018426E4ECD2008FC1A3 /* TelegramEngineLocalization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramEngineLocalization.swift; sourceTree = ""; }; + 27BA018526E4ECD2008FC1A3 /* Localizations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Localizations.swift; sourceTree = ""; }; + 27BA018626E4ECD2008FC1A3 /* Countries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Countries.swift; sourceTree = ""; }; + 27BA018726E4ECD2008FC1A3 /* LocalizationInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalizationInfo.swift; sourceTree = ""; }; + 27BA018826E4ECD2008FC1A3 /* SuggestedLocalizationEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SuggestedLocalizationEntry.swift; sourceTree = ""; }; + 27BA018926E4ECD2008FC1A3 /* LocalizationListState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalizationListState.swift; sourceTree = ""; }; + 27BA018A26E4ECD2008FC1A3 /* LocalizationPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalizationPreview.swift; sourceTree = ""; }; + 27BA018C26E4ECD2008FC1A3 /* TelegramGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramGroup.swift; sourceTree = ""; }; + 27BA018D26E4ECD2008FC1A3 /* TelegramPeerNotificationSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramPeerNotificationSettings.swift; sourceTree = ""; }; + 27BA018E26E4ECD2008FC1A3 /* TelegramUser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramUser.swift; sourceTree = ""; }; + 27BA018F26E4ECD2008FC1A3 /* CloudMediaResourceParameters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudMediaResourceParameters.swift; sourceTree = ""; }; + 27BA019026E4ECD2008FC1A3 /* TelegramMediaWebDocument.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramMediaWebDocument.swift; sourceTree = ""; }; + 27BA019126E4ECD2008FC1A3 /* BotInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BotInfo.swift; sourceTree = ""; }; + 27BA019226E4ECD2008FC1A3 /* CachedChannelParticipants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachedChannelParticipants.swift; sourceTree = ""; }; + 27BA019326E4ECD2008FC1A3 /* CachedGroupParticipants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachedGroupParticipants.swift; sourceTree = ""; }; + 27BA019426E4ECD2008FC1A3 /* TelegramChannelAdminRights.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramChannelAdminRights.swift; sourceTree = ""; }; + 27BA019526E4ECD2008FC1A3 /* TextEntitiesMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextEntitiesMessageAttribute.swift; sourceTree = ""; }; + 27BA019626E4ECD2008FC1A3 /* TelegramMediaWebpage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramMediaWebpage.swift; sourceTree = ""; }; + 27BA019726E4ECD2008FC1A3 /* TelegramMediaWebFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramMediaWebFile.swift; sourceTree = ""; }; + 27BA019826E4ECD2008FC1A3 /* TelegramMediaGame.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramMediaGame.swift; sourceTree = ""; }; + 27BA019926E4ECD2008FC1A3 /* Wallpaper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Wallpaper.swift; sourceTree = ""; }; + 27BA019A26E4ECD2008FC1A3 /* TelegramMediaPoll.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramMediaPoll.swift; sourceTree = ""; }; + 27BA019B26E4ECD2008FC1A3 /* RemoteStorageConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteStorageConfiguration.swift; sourceTree = ""; }; + 27BA019C26E4ECD2008FC1A3 /* TelegramMediaImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramMediaImage.swift; sourceTree = ""; }; + 27BA019D26E4ECD2008FC1A3 /* ReactionsMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactionsMessageAttribute.swift; sourceTree = ""; }; + 27BA019E26E4ECD2008FC1A3 /* ChatContextResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatContextResult.swift; sourceTree = ""; }; + 27BA019F26E4ECD2008FC1A3 /* ImageRepresentationWithReference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageRepresentationWithReference.swift; sourceTree = ""; }; + 27BA01A026E4ECD2008FC1A3 /* TelegramMediaAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramMediaAction.swift; sourceTree = ""; }; + 27BA01A126E4ECD2008FC1A3 /* TelegramMediaMap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramMediaMap.swift; sourceTree = ""; }; + 27BA01A226E4ECD2008FC1A3 /* EncryptedMediaResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EncryptedMediaResource.swift; sourceTree = ""; }; + 27BA01A326E4ECD2008FC1A3 /* PeerAccessRestrictionInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerAccessRestrictionInfo.swift; sourceTree = ""; }; + 27BA01A426E4ECD2008FC1A3 /* TelegramChannelBannedRights.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramChannelBannedRights.swift; sourceTree = ""; }; + 27BA01A526E4ECD2008FC1A3 /* ExportedInvitation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExportedInvitation.swift; sourceTree = ""; }; + 27BA01A626E4ECD2008FC1A3 /* StoreMessage_Telegram.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoreMessage_Telegram.swift; sourceTree = ""; }; + 27BA01A726E4ECD2008FC1A3 /* Theme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; + 27BA01A826E4ECD2008FC1A3 /* PeerGeoLocation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerGeoLocation.swift; sourceTree = ""; }; + 27BA01A926E4ECD2008FC1A3 /* ApiUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiUtils.swift; sourceTree = ""; }; + 27BA01AA26E4ECD2008FC1A3 /* RichText.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RichText.swift; sourceTree = ""; }; + 27BA01AB26E4ECD2008FC1A3 /* InstantPage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantPage.swift; sourceTree = ""; }; + 27BA01AC26E4ECD2008FC1A3 /* CloudFileMediaResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudFileMediaResource.swift; sourceTree = ""; }; + 27BA01AD26E4ECD2008FC1A3 /* ApiGroupOrChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiGroupOrChannel.swift; sourceTree = ""; }; + 27BA01AE26E4ECD2008FC1A3 /* TelegramChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramChannel.swift; sourceTree = ""; }; + 27BA01AF26E4ECD2008FC1A3 /* MediaResourceApiUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaResourceApiUtils.swift; sourceTree = ""; }; + 27BA01B026E4ECD2008FC1A3 /* TelegramUserPresence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramUserPresence.swift; sourceTree = ""; }; + 27BA01B126E4ECD2008FC1A3 /* AdMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdMessageAttribute.swift; sourceTree = ""; }; + 27BA01B226E4ECD2008FC1A3 /* TelegramMediaFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramMediaFile.swift; sourceTree = ""; }; + 27BA01B326E4ECD2008FC1A3 /* ReplyMarkupMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplyMarkupMessageAttribute.swift; sourceTree = ""; }; + 27BA01B526E4ECD2008FC1A3 /* SyncCore_TelegramMediaAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_TelegramMediaAction.swift; sourceTree = ""; }; + 27BA01B626E4ECD2008FC1A3 /* SyncCore_AutodownloadSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_AutodownloadSettings.swift; sourceTree = ""; }; + 27BA01B726E4ECD2008FC1A3 /* SyncCore_wallapersState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_wallapersState.swift; sourceTree = ""; }; + 27BA01B826E4ECD2008FC1A3 /* SyncCore_SynchronizeChatInputStateOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SynchronizeChatInputStateOperation.swift; sourceTree = ""; }; + 27BA01B926E4ECD2008FC1A3 /* SyncCore_TelegramUser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_TelegramUser.swift; sourceTree = ""; }; + 27BA01BA26E4ECD2008FC1A3 /* SyncCore_TelegramChatAdminRights.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_TelegramChatAdminRights.swift; sourceTree = ""; }; + 27BA01BB26E4ECD2008FC1A3 /* SyncCore_PeerAccessHash.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_PeerAccessHash.swift; sourceTree = ""; }; + 27BA01BC26E4ECD2008FC1A3 /* SyncCore_ViewCountMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_ViewCountMessageAttribute.swift; sourceTree = ""; }; + 27BA01BD26E4ECD2008FC1A3 /* SyncCore_SynchronizeAppLogEventsOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SynchronizeAppLogEventsOperation.swift; sourceTree = ""; }; + 27BA01BE26E4ECD2008FC1A3 /* SyncCore_TelegramMediaImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_TelegramMediaImage.swift; sourceTree = ""; }; + 27BA01BF26E4ECD2008FC1A3 /* SyncCore_AppConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_AppConfiguration.swift; sourceTree = ""; }; + 27BA01C026E4ECD2008FC1A3 /* SyncCore_SecretChatFileReference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SecretChatFileReference.swift; sourceTree = ""; }; + 27BA01C126E4ECD2008FC1A3 /* SyncCore_LoggingSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_LoggingSettings.swift; sourceTree = ""; }; + 27BA01C226E4ECD2008FC1A3 /* SyncCore_SynchronizeSavedStickersOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SynchronizeSavedStickersOperation.swift; sourceTree = ""; }; + 27BA01C326E4ECD2008FC1A3 /* SyncCore_SecureFileMediaResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SecureFileMediaResource.swift; sourceTree = ""; }; + 27BA01C426E4ECD2008FC1A3 /* SyncCore_UpdateMessageReactionsAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_UpdateMessageReactionsAction.swift; sourceTree = ""; }; + 27BA01C526E4ECD2008FC1A3 /* SyncCore_EditedMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_EditedMessageAttribute.swift; sourceTree = ""; }; + 27BA01C626E4ECD2008FC1A3 /* SyncCore_SearchBotsConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SearchBotsConfiguration.swift; sourceTree = ""; }; + 27BA01C726E4ECD2008FC1A3 /* SyncCore_TemporaryTwoStepPasswordToken.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_TemporaryTwoStepPasswordToken.swift; sourceTree = ""; }; + 27BA01C826E4ECD2008FC1A3 /* SyncCore_RecentPeerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_RecentPeerItem.swift; sourceTree = ""; }; + 27BA01C926E4ECD2008FC1A3 /* SyncCore_Localization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_Localization.swift; sourceTree = ""; }; + 27BA01CA26E4ECD2008FC1A3 /* SyncCore_GlobalNotificationSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_GlobalNotificationSettings.swift; sourceTree = ""; }; + 27BA01CB26E4ECD2008FC1A3 /* SyncCore_TelegramChatBannedRights.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_TelegramChatBannedRights.swift; sourceTree = ""; }; + 27BA01CC26E4ECD2008FC1A3 /* SyncCore_TelegramSecretChat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_TelegramSecretChat.swift; sourceTree = ""; }; + 27BA01CD26E4ECD2008FC1A3 /* SyncCore_CachedStickerQueryResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_CachedStickerQueryResult.swift; sourceTree = ""; }; + 27BA01CE26E4ECD2008FC1A3 /* SyncCore_TelegramMediaGame.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_TelegramMediaGame.swift; sourceTree = ""; }; + 27BA01CF26E4ECD2008FC1A3 /* SyncCore_TelegramMediaContact.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_TelegramMediaContact.swift; sourceTree = ""; }; + 27BA01D026E4ECD2008FC1A3 /* SyncCore_StickerPackCollectionInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_StickerPackCollectionInfo.swift; sourceTree = ""; }; + 27BA01D126E4ECD2008FC1A3 /* SyncCore_CachedThemesConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_CachedThemesConfiguration.swift; sourceTree = ""; }; + 27BA01D226E4ECD2008FC1A3 /* SyncCore_RestrictedContentMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_RestrictedContentMessageAttribute.swift; sourceTree = ""; }; + 27BA01D326E4ECD2008FC1A3 /* SyncCore_ConsumableContentMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_ConsumableContentMessageAttribute.swift; sourceTree = ""; }; + 27BA01D426E4ECD2008FC1A3 /* SyncCore_CachedResolvedByNamePeer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_CachedResolvedByNamePeer.swift; sourceTree = ""; }; + 27BA01D526E4ECD2008FC1A3 /* SyncCore_TelegramMediaPoll.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_TelegramMediaPoll.swift; sourceTree = ""; }; + 27BA01D626E4ECD2008FC1A3 /* SyncCore_SecretChatIncomingDecryptedOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SecretChatIncomingDecryptedOperation.swift; sourceTree = ""; }; + 27BA01D726E4ECD2008FC1A3 /* SyncCore_TelegramPeerNotificationSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_TelegramPeerNotificationSettings.swift; sourceTree = ""; }; + 27BA01D826E4ECD2008FC1A3 /* SyncCore_TelegramMediaUnsupported.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_TelegramMediaUnsupported.swift; sourceTree = ""; }; + 27BA01D926E4ECD2008FC1A3 /* SyncCore_CachedChannelData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_CachedChannelData.swift; sourceTree = ""; }; + 27BA01DA26E4ECD2008FC1A3 /* SyncCore_AccountSortOrderAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_AccountSortOrderAttribute.swift; sourceTree = ""; }; + 27BA01DB26E4ECD2008FC1A3 /* SyncCore_OutgoingScheduleInfoMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_OutgoingScheduleInfoMessageAttribute.swift; sourceTree = ""; }; + 27BA01DC26E4ECD2008FC1A3 /* SyncCore_ConsumePersonalMessageAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_ConsumePersonalMessageAction.swift; sourceTree = ""; }; + 27BA01DD26E4ECD2008FC1A3 /* SyncCore_TelegramTheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_TelegramTheme.swift; sourceTree = ""; }; + 27BA01DE26E4ECD2008FC1A3 /* SyncCore_SecretFileEncryptionKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SecretFileEncryptionKey.swift; sourceTree = ""; }; + 27BA01DF26E4ECD2008FC1A3 /* SyncCore_CachedGroupData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_CachedGroupData.swift; sourceTree = ""; }; + 27BA01E026E4ECD2008FC1A3 /* SyncCore_CachedSecureIdConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_CachedSecureIdConfiguration.swift; sourceTree = ""; }; + 27BA01E126E4ECD2008FC1A3 /* SyncCore_ForwardHideSendersNamesMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_ForwardHideSendersNamesMessageAttribute.swift; sourceTree = ""; }; + 27BA01E226E4ECD2008FC1A3 /* SyncCore_TelegramMediaExpiredContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_TelegramMediaExpiredContent.swift; sourceTree = ""; }; + 27BA01E326E4ECD2008FC1A3 /* SyncCore_SavedStickerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SavedStickerItem.swift; sourceTree = ""; }; + 27BA01E426E4ECD2008FC1A3 /* SyncCore_AuthorSignatureMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_AuthorSignatureMessageAttribute.swift; sourceTree = ""; }; + 27BA01E526E4ECD2008FC1A3 /* SyncCore_ChannelMessageStateVersionAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_ChannelMessageStateVersionAttribute.swift; sourceTree = ""; }; + 27BA01E626E4ECD2008FC1A3 /* SyncCore_ThemeSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_ThemeSettings.swift; sourceTree = ""; }; + 27BA01E726E4ECD2008FC1A3 /* SyncCore_Namespaces.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_Namespaces.swift; sourceTree = ""; }; + 27BA01E826E4ECD2008FC1A3 /* SyncCore_PixelDimensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_PixelDimensions.swift; sourceTree = ""; }; + 27BA01E926E4ECD2008FC1A3 /* SyncCore_InlineBotMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_InlineBotMessageAttribute.swift; sourceTree = ""; }; + 27BA01EA26E4ECD2008FC1A3 /* SyncCore_LocalizationSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_LocalizationSettings.swift; sourceTree = ""; }; + 27BA01EB26E4ECD2008FC1A3 /* SyncCore_SendScheduledMessageImmediatelyAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SendScheduledMessageImmediatelyAction.swift; sourceTree = ""; }; + 27BA01EC26E4ECD2008FC1A3 /* SyncCore_ReplyMarkupMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_ReplyMarkupMessageAttribute.swift; sourceTree = ""; }; + 27BA01ED26E4ECD2008FC1A3 /* SyncCore_AutoremoveTimeoutMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_AutoremoveTimeoutMessageAttribute.swift; sourceTree = ""; }; + 27BA01EE26E4ECD2008FC1A3 /* SyncCore_JSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_JSON.swift; sourceTree = ""; }; + 27BA01EF26E4ECD2008FC1A3 /* SyncCore_TelegramGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_TelegramGroup.swift; sourceTree = ""; }; + 27BA01F026E4ECD2008FC1A3 /* SyncCore_ChannelState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_ChannelState.swift; sourceTree = ""; }; + 27BA01F126E4ECD2008FC1A3 /* SyncCore_OutgoingMessageInfoAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_OutgoingMessageInfoAttribute.swift; sourceTree = ""; }; + 27BA01F226E4ECD2008FC1A3 /* SyncCore_UnauthorizedAccountState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_UnauthorizedAccountState.swift; sourceTree = ""; }; + 27BA01F326E4ECD2008FC1A3 /* SyncCore_SecretChatEncryptionConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SecretChatEncryptionConfig.swift; sourceTree = ""; }; + 27BA01F426E4ECD2008FC1A3 /* SyncCore_TelegramWallpaper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_TelegramWallpaper.swift; sourceTree = ""; }; + 27BA01F526E4ECD2008FC1A3 /* SyncCore_ArchivedStickerPacksInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_ArchivedStickerPacksInfo.swift; sourceTree = ""; }; + 27BA01F626E4ECD2008FC1A3 /* SyncCore_NetworkSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_NetworkSettings.swift; sourceTree = ""; }; + 27BA01F726E4ECD2008FC1A3 /* SyncCore_LocalizationInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_LocalizationInfo.swift; sourceTree = ""; }; + 27BA01F826E4ECD2008FC1A3 /* SyncCore_RichText.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_RichText.swift; sourceTree = ""; }; + 27BA01F926E4ECD2008FC1A3 /* SyncCore_NotificationInfoMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_NotificationInfoMessageAttribute.swift; sourceTree = ""; }; + 27BA01FA26E4ECD2008FC1A3 /* SyncCore_InstantPage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_InstantPage.swift; sourceTree = ""; }; + 27BA01FB26E4ECD2008FC1A3 /* SyncCore_TextEntitiesMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_TextEntitiesMessageAttribute.swift; sourceTree = ""; }; + 27BA01FC26E4ECD2008FC1A3 /* SyncCore_TelegramUserPresence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_TelegramUserPresence.swift; sourceTree = ""; }; + 27BA01FD26E4ECD2008FC1A3 /* SyncCore_ProxySettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_ProxySettings.swift; sourceTree = ""; }; + 27BA01FE26E4ECD2008FC1A3 /* SyncCore_ReactionsMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_ReactionsMessageAttribute.swift; sourceTree = ""; }; + 27BA01FF26E4ECD2008FC1A3 /* SyncCore_SecureIdFileReference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SecureIdFileReference.swift; sourceTree = ""; }; + 27BA020026E4ECD2008FC1A3 /* SyncCore_SynchronizeMarkAllUnseenPersonalMessagesOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SynchronizeMarkAllUnseenPersonalMessagesOperation.swift; sourceTree = ""; }; + 27BA020126E4ECD2008FC1A3 /* SyncCore_SynchronizeEmojiKeywordsOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SynchronizeEmojiKeywordsOperation.swift; sourceTree = ""; }; + 27BA020226E4ECD2008FC1A3 /* SyncCore_TelegramDeviceContactImportedData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_TelegramDeviceContactImportedData.swift; sourceTree = ""; }; + 27BA020326E4ECD2008FC1A3 /* SyncCore_ForwardSourceInfoAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_ForwardSourceInfoAttribute.swift; sourceTree = ""; }; + 27BA020426E4ECD2008FC1A3 /* SyncCore_SourceReferenceMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SourceReferenceMessageAttribute.swift; sourceTree = ""; }; + 27BA020526E4ECD2008FC1A3 /* SyncCore_TelegramMediaDice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_TelegramMediaDice.swift; sourceTree = ""; }; + 27BA020626E4ECD2008FC1A3 /* SyncCore_ReplyMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_ReplyMessageAttribute.swift; sourceTree = ""; }; + 27BA020726E4ECD2008FC1A3 /* SyncCore_WalletCollection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_WalletCollection.swift; sourceTree = ""; }; + 27BA020826E4ECD2008FC1A3 /* SyncCore_EmojiKeywordItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_EmojiKeywordItem.swift; sourceTree = ""; }; + 27BA020926E4ECD2008FC1A3 /* SyncCore_WasScheduledMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_WasScheduledMessageAttribute.swift; sourceTree = ""; }; + 27BA020A26E4ECD2008FC1A3 /* SyncCore_MediaReference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_MediaReference.swift; sourceTree = ""; }; + 27BA020B26E4ECD2008FC1A3 /* SyncCore_ImportableDeviceContactData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_ImportableDeviceContactData.swift; sourceTree = ""; }; + 27BA020C26E4ECD2008FC1A3 /* SyncCore_AppChangelogState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_AppChangelogState.swift; sourceTree = ""; }; + 27BA020D26E4ECD2008FC1A3 /* SyncCore_ContentRequiresValidationMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_ContentRequiresValidationMessageAttribute.swift; sourceTree = ""; }; + 27BA020E26E4ECD2008FC1A3 /* SyncCore_BotInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_BotInfo.swift; sourceTree = ""; }; + 27BA020F26E4ECD2008FC1A3 /* SyncCore_LimitsConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_LimitsConfiguration.swift; sourceTree = ""; }; + 27BA021026E4ECD2008FC1A3 /* SyncCore_TelegramMediaMap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_TelegramMediaMap.swift; sourceTree = ""; }; + 27BA021126E4ECD2008FC1A3 /* SyncCore_TelegramMediaResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_TelegramMediaResource.swift; sourceTree = ""; }; + 27BA021226E4ECD2008FC1A3 /* SyncCore_RemoteStorageConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_RemoteStorageConfiguration.swift; sourceTree = ""; }; + 27BA021326E4ECD2008FC1A3 /* SyncCore_TelegramMediaFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_TelegramMediaFile.swift; sourceTree = ""; }; + 27BA021426E4ECD2008FC1A3 /* SyncCore_LocalizationListState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_LocalizationListState.swift; sourceTree = ""; }; + 27BA021526E4ECD2008FC1A3 /* SyncCore_SecretChatState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SecretChatState.swift; sourceTree = ""; }; + 27BA021626E4ECD2008FC1A3 /* SyncCore_TelegramMediaWebpage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_TelegramMediaWebpage.swift; sourceTree = ""; }; + 27BA021726E4ECD2008FC1A3 /* SyncCore_CachedRecentPeers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_CachedRecentPeers.swift; sourceTree = ""; }; + 27BA021826E4ECD2008FC1A3 /* SyncCore_AuthorizedAccountState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_AuthorizedAccountState.swift; sourceTree = ""; }; + 27BA021926E4ECD2008FC1A3 /* SyncCore_PeerReference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_PeerReference.swift; sourceTree = ""; }; + 27BA021A26E4ECD2008FC1A3 /* SyncCore_SuggestedLocalizationEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SuggestedLocalizationEntry.swift; sourceTree = ""; }; + 27BA021B26E4ECD2008FC1A3 /* SyncCore_SecretChatOutgoingOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SecretChatOutgoingOperation.swift; sourceTree = ""; }; + 27BA021C26E4ECD2008FC1A3 /* SyncCore_CachedGroupParticipants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_CachedGroupParticipants.swift; sourceTree = ""; }; + 27BA021D26E4ECD2008FC1A3 /* SyncCore_CachedStickerPack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_CachedStickerPack.swift; sourceTree = ""; }; + 27BA021E26E4ECD2008FC1A3 /* SyncCore_ExportedInvitation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_ExportedInvitation.swift; sourceTree = ""; }; + 27BA021F26E4ECD2008FC1A3 /* SyncCore_RecentHashtagItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_RecentHashtagItem.swift; sourceTree = ""; }; + 27BA022026E4ECD2008FC1A3 /* SyncCore_CloudFileMediaResource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_CloudFileMediaResource.swift; sourceTree = ""; }; + 27BA022126E4ECD2008FC1A3 /* SyncCore_ValidationMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_ValidationMessageAttribute.swift; sourceTree = ""; }; + 27BA022226E4ECD2008FC1A3 /* SyncCore_ReplyThreadMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_ReplyThreadMessageAttribute.swift; sourceTree = ""; }; + 27BA022326E4ECD2008FC1A3 /* SyncCore_PeerAccessRestrictionInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_PeerAccessRestrictionInfo.swift; sourceTree = ""; }; + 27BA022426E4ECD2008FC1A3 /* SyncCore_OutgoingChatContextResultMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_OutgoingChatContextResultMessageAttribute.swift; sourceTree = ""; }; + 27BA022526E4ECD2008FC1A3 /* SyncCore_CachedUserData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_CachedUserData.swift; sourceTree = ""; }; + 27BA022626E4ECD2008FC1A3 /* SyncCore_SynchronizeRecentlyUsedMediaOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SynchronizeRecentlyUsedMediaOperation.swift; sourceTree = ""; }; + 27BA022726E4ECD2008FC1A3 /* SyncCore_CachedLocalizationInfos.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_CachedLocalizationInfos.swift; sourceTree = ""; }; + 27BA022826E4ECD2008FC1A3 /* SyncCore_AccountEnvironmentAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_AccountEnvironmentAttribute.swift; sourceTree = ""; }; + 27BA022926E4ECD2008FC1A3 /* SyncCore_PeerStatusSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_PeerStatusSettings.swift; sourceTree = ""; }; + 27BA022A26E4ECD2008FC1A3 /* SyncCore_StickerPackItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_StickerPackItem.swift; sourceTree = ""; }; + 27BA022B26E4ECD2008FC1A3 /* SyncCore_SynchronizeSavedGifsOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SynchronizeSavedGifsOperation.swift; sourceTree = ""; }; + 27BA022C26E4ECD2008FC1A3 /* SyncCore_AccountBackupDataAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_AccountBackupDataAttribute.swift; sourceTree = ""; }; + 27BA022D26E4ECD2008FC1A3 /* SyncCore_RecentMediaItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_RecentMediaItem.swift; sourceTree = ""; }; + 27BA022E26E4ECD2008FC1A3 /* SyncCore_LoggedOutAccountAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_LoggedOutAccountAttribute.swift; sourceTree = ""; }; + 27BA022F26E4ECD2008FC1A3 /* SyncCore_EmojiKeywordCollectionInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_EmojiKeywordCollectionInfo.swift; sourceTree = ""; }; + 27BA023026E4ECD2008FC1A3 /* SyncCore_RegularChatState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_RegularChatState.swift; sourceTree = ""; }; + 27BA023126E4ECD2008FC1A3 /* SyncCore_SynchronizePinnedChatsOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SynchronizePinnedChatsOperation.swift; sourceTree = ""; }; + 27BA023226E4ECD2008FC1A3 /* SyncCore_ForwardCountMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_ForwardCountMessageAttribute.swift; sourceTree = ""; }; + 27BA023326E4ECD2008FC1A3 /* SyncCore_SuggestedLocalizationUpdatesOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SuggestedLocalizationUpdatesOperation.swift; sourceTree = ""; }; + 27BA023426E4ECD2008FC1A3 /* SyncCore_SynchronizeInstalledStickerPacksOperations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SynchronizeInstalledStickerPacksOperations.swift; sourceTree = ""; }; + 27BA023526E4ECD2008FC1A3 /* SyncCore_ContentPrivacySettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_ContentPrivacySettings.swift; sourceTree = ""; }; + 27BA023626E4ECD2008FC1A3 /* SyncCore_FeaturedStickerPack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_FeaturedStickerPack.swift; sourceTree = ""; }; + 27BA023726E4ECD2008FC1A3 /* SyncCore_TelegramMediaWebFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_TelegramMediaWebFile.swift; sourceTree = ""; }; + 27BA023826E4ECD2008FC1A3 /* SyncCore_OutgoingContentInfoMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_OutgoingContentInfoMessageAttribute.swift; sourceTree = ""; }; + 27BA023926E4ECD2008FC1A3 /* SyncCore_SecretChatKeychain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SecretChatKeychain.swift; sourceTree = ""; }; + 27BA023A26E4ECD2008FC1A3 /* SyncCore_VoipConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_VoipConfiguration.swift; sourceTree = ""; }; + 27BA023B26E4ECD2008FC1A3 /* SyncCore_EmbeddedMediaStickersMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_EmbeddedMediaStickersMessageAttribute.swift; sourceTree = ""; }; + 27BA023C26E4ECD2008FC1A3 /* SyncCore_TelegramMediaInvoice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_TelegramMediaInvoice.swift; sourceTree = ""; }; + 27BA023D26E4ECD2008FC1A3 /* SyncCore_EmojiSearchQueryMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_EmojiSearchQueryMessageAttribute.swift; sourceTree = ""; }; + 27BA023E26E4ECD2008FC1A3 /* SyncCore_CloudChatRemoveMessagesOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_CloudChatRemoveMessagesOperation.swift; sourceTree = ""; }; + 27BA023F26E4ECD2008FC1A3 /* SyncCore_PeerGroupMessageStateVersionAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_PeerGroupMessageStateVersionAttribute.swift; sourceTree = ""; }; + 27BA024026E4ECD2008FC1A3 /* SyncCore_CacheStorageSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_CacheStorageSettings.swift; sourceTree = ""; }; + 27BA024126E4ECD2008FC1A3 /* SyncCore_SecretChatIncomingEncryptedOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SecretChatIncomingEncryptedOperation.swift; sourceTree = ""; }; + 27BA024226E4ECD2008FC1A3 /* SyncCore_SecretChatSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SecretChatSettings.swift; sourceTree = ""; }; + 27BA024326E4ECD2008FC1A3 /* SyncCore_ConsumablePersonalMentionMessageAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_ConsumablePersonalMentionMessageAttribute.swift; sourceTree = ""; }; + 27BA024426E4ECD2008FC1A3 /* SyncCore_SynchronizeConsumeMessageContentsOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SynchronizeConsumeMessageContentsOperation.swift; sourceTree = ""; }; + 27BA024526E4ECD2008FC1A3 /* SyncCore_CachedWallpapersConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_CachedWallpapersConfiguration.swift; sourceTree = ""; }; + 27BA024626E4ECD2008FC1A3 /* SyncCore_SynchronizeableChatInputState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SynchronizeableChatInputState.swift; sourceTree = ""; }; + 27BA024726E4ECD2008FC1A3 /* SyncCore_ContactsSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_ContactsSettings.swift; sourceTree = ""; }; + 27BA024826E4ECD2008FC1A3 /* SyncCore_SynchronizeGroupedPeersOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_SynchronizeGroupedPeersOperation.swift; sourceTree = ""; }; + 27BA024926E4ECD2008FC1A3 /* SyncCore_StandaloneAccountTransaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_StandaloneAccountTransaction.swift; sourceTree = ""; }; + 27BA024A26E4ECD2008FC1A3 /* SyncCore_TelegramChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCore_TelegramChannel.swift; sourceTree = ""; }; + 27BA024C26E4ECD2008FC1A3 /* AccountManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountManager.swift; sourceTree = ""; }; + 27BA024D26E4ECD2008FC1A3 /* AccountIntermediateState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountIntermediateState.swift; sourceTree = ""; }; + 27BA024E26E4ECD2008FC1A3 /* Account.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = ""; }; + 27BA024F26E4ECD2008FC1A3 /* WebpagePreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebpagePreview.swift; sourceTree = ""; }; + 27BA025126E4ECD2008FC1A3 /* SecretChatRekeySession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecretChatRekeySession.swift; sourceTree = ""; }; + 27BA025226E4ECD2008FC1A3 /* SecretChatOutgoingOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecretChatOutgoingOperation.swift; sourceTree = ""; }; + 27BA025326E4ECD2008FC1A3 /* SecretChatEncryption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecretChatEncryption.swift; sourceTree = ""; }; + 27BA025426E4ECD2008FC1A3 /* SecretChatEncryptionConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecretChatEncryptionConfig.swift; sourceTree = ""; }; + 27BA025526E4ECD2008FC1A3 /* SecretChatLayerNegotiation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecretChatLayerNegotiation.swift; sourceTree = ""; }; + 27BA025626E4ECD2008FC1A3 /* SecretChatFileReference.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecretChatFileReference.swift; sourceTree = ""; }; + 27BA025726E4ECD2008FC1A3 /* SecretChatIncomingEncryptedOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecretChatIncomingEncryptedOperation.swift; sourceTree = ""; }; + 27BA025826E4ECD2008FC1A3 /* UpdateSecretChat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateSecretChat.swift; sourceTree = ""; }; + 27BA025926E4ECD2008FC1A3 /* SetSecretChatMessageAutoremoveTimeoutInteractively.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetSecretChatMessageAutoremoveTimeoutInteractively.swift; sourceTree = ""; }; + A76D02EE25C4048500FCE1B5 /* LegacyReachability.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LegacyReachability.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7919033240CF9C7002011CA /* Reachability.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Reachability.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7919035240CF9CC002011CA /* CryptoUtils.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CryptoUtils.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7919037240CF9D1002011CA /* NetworkLogging.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = NetworkLogging.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7D281D7236C25370000A9BF /* libphonenumber.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = libphonenumber.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7D281D9236C253C0000A9BF /* TelegramApi.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TelegramApi.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7D281DB236C25440000A9BF /* MtProtoKitMac.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MtProtoKitMac.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7D281DD236C25500000A9BF /* SwiftSignalKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftSignalKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7D281DF236C25580000A9BF /* libphonenumber.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = libphonenumber.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7D281E1236C25710000A9BF /* Postbox.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Postbox.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7D281E3236C257C0000A9BF /* TelegramApi.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TelegramApi.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7D281E9236C27030000A9BF /* MtProtoKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MtProtoKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A7D281EB236C2FAD0000A9BF /* SyncCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SyncCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0208AF32306E92B00A23503 /* libphonenumbermac.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = libphonenumbermac.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D035732E22B5C24F00F0920D /* TelegramApi.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TelegramApi.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D03B0E571D631EB900955575 /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; + D03B0E5D1D6327F600955575 /* SSignalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = SSignalKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D03B0E5F1D6327FF00955575 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; + D03B0E611D63281A00955575 /* libavcodec.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavcodec.a; path = "third-party/FFmpeg-iOS/lib/libavcodec.a"; sourceTree = ""; }; + D03B0E621D63281A00955575 /* libavformat.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavformat.a; path = "third-party/FFmpeg-iOS/lib/libavformat.a"; sourceTree = ""; }; + D03B0E631D63281A00955575 /* libavutil.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavutil.a; path = "third-party/FFmpeg-iOS/lib/libavutil.a"; sourceTree = ""; }; + D03B0E641D63281A00955575 /* libswresample.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libswresample.a; path = "third-party/FFmpeg-iOS/lib/libswresample.a"; sourceTree = ""; }; + D03B0E691D63283000955575 /* libwebp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libwebp.a; path = "third-party/libwebp/lib/libwebp.a"; sourceTree = ""; }; + D03B0E6B1D63283C00955575 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = usr/lib/libiconv.tbd; sourceTree = SDKROOT; }; + D03E45D02305D34C0049C28B /* libphonenumber_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = libphonenumber_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D03E45D32305D44A0049C28B /* libphonenumber.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = libphonenumber.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D06706641D512ADB00DED3E3 /* AsyncDisplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = AsyncDisplayKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D06706651D512ADB00DED3E3 /* Display.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Display.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D06706671D512ADB00DED3E3 /* Postbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Postbox.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D06706681D512ADB00DED3E3 /* SwiftSignalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = SwiftSignalKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D067066E1D512AEB00DED3E3 /* MtProtoKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = MtProtoKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0AC49491D7097A400AA55DA /* SSignalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = SSignalKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0AF32371FAE8C910097362B /* MultipeerConnectivity.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MultipeerConnectivity.framework; path = System/Library/Frameworks/MultipeerConnectivity.framework; sourceTree = SDKROOT; }; + D0B418671D7E03D5004562A4 /* TelegramCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TelegramCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0B418691D7E03D5004562A4 /* TelegramCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TelegramCore.h; sourceTree = ""; }; + D0B4186A1D7E03D5004562A4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D0B418701D7E0409004562A4 /* PostboxMac.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = PostboxMac.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0B418711D7E0409004562A4 /* SwiftSignalKitMac.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = SwiftSignalKitMac.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0B4187E1D7E054E004562A4 /* MtProtoKitMac.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = MtProtoKitMac.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0CAF2E91D75EC600011F558 /* MtProtoKitDynamic.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = MtProtoKitDynamic.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0CC4AA322BA44960088F36D /* TelegramApi.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TelegramApi.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0CC4ADB22BA47280088F36D /* TelegramApiMac.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TelegramApiMac.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D0B418631D7E03D5004562A4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A7919038240CF9D1002011CA /* NetworkLogging.framework in Frameworks */, + A7919036240CF9CC002011CA /* CryptoUtils.framework in Frameworks */, + A7919034240CF9C7002011CA /* Reachability.framework in Frameworks */, + A7D281EA236C27030000A9BF /* MtProtoKit.framework in Frameworks */, + A7D281E4236C257C0000A9BF /* TelegramApi.framework in Frameworks */, + A7D281E2236C25710000A9BF /* Postbox.framework in Frameworks */, + A7D281DE236C25500000A9BF /* SwiftSignalKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 27BA004026E4ECD1008FC1A3 /* Sources */ = { + isa = PBXGroup; + children = ( + 27BA004126E4ECD1008FC1A3 /* Wallpapers.swift */, + 27BA004226E4ECD1008FC1A3 /* Authorization.swift */, + 27BA004326E4ECD1008FC1A3 /* Settings */, + 27BA005026E4ECD1008FC1A3 /* PeerStatistics.swift */, + 27BA005126E4ECD1008FC1A3 /* MacOS */, + 27BA005526E4ECD1008FC1A3 /* PendingMessages */, + 27BA005D26E4ECD1008FC1A3 /* MessageStatistics.swift */, + 27BA005E26E4ECD1008FC1A3 /* Suggestions.swift */, + 27BA005F26E4ECD1008FC1A3 /* Themes.swift */, + 27BA006026E4ECD1008FC1A3 /* Network */, + 27BA006A26E4ECD1008FC1A3 /* UpdatePeers.swift */, + 27BA006B26E4ECD1008FC1A3 /* SplitTest.swift */, + 27BA006C26E4ECD1008FC1A3 /* Utils */, + 27BA007A26E4ECD1008FC1A3 /* State */, + 27BA00CB26E4ECD2008FC1A3 /* LoadedPeer.swift */, + 27BA00CC26E4ECD2008FC1A3 /* LoadedPeerFromMessage.swift */, + 27BA00CD26E4ECD2008FC1A3 /* UpdatePeerChatInterfaceState.swift */, + 27BA00CE26E4ECD2008FC1A3 /* TelegramEngine */, + 27BA018B26E4ECD2008FC1A3 /* ApiUtils */, + 27BA01B426E4ECD2008FC1A3 /* SyncCore */, + 27BA024B26E4ECD2008FC1A3 /* Account */, + 27BA024F26E4ECD2008FC1A3 /* WebpagePreview.swift */, + 27BA025026E4ECD2008FC1A3 /* SecretChats */, + ); + name = Sources; + path = "../../../submodules/telegram-ios/submodules/TelegramCore/Sources"; + sourceTree = ""; + }; + 27BA004326E4ECD1008FC1A3 /* Settings */ = { + isa = PBXGroup; + children = ( + 27BA004426E4ECD1008FC1A3 /* ContentSettings.swift */, + 27BA004526E4ECD1008FC1A3 /* LoggingSettings.swift */, + 27BA004626E4ECD1008FC1A3 /* LimitsConfiguration.swift */, + 27BA004726E4ECD1008FC1A3 /* ProxySettings.swift */, + 27BA004826E4ECD1008FC1A3 /* PeerContactSettings.swift */, + 27BA004926E4ECD1008FC1A3 /* CacheStorageSettings.swift */, + 27BA004A26E4ECD1008FC1A3 /* NetworkSettings.swift */, + 27BA004B26E4ECD1008FC1A3 /* ContentPrivacySettings.swift */, + 27BA004C26E4ECD1008FC1A3 /* VoipConfiguration.swift */, + 27BA004D26E4ECD1008FC1A3 /* AutodownloadSettings.swift */, + 27BA004E26E4ECD1008FC1A3 /* GlobalNotificationSettings.swift */, + 27BA004F26E4ECD1008FC1A3 /* PrivacySettings.swift */, + ); + path = Settings; + sourceTree = ""; + }; + 27BA005126E4ECD1008FC1A3 /* MacOS */ = { + isa = PBXGroup; + children = ( + 27BA005226E4ECD1008FC1A3 /* NotificationAutolockReportManager.swift */, + 27BA005326E4ECD1008FC1A3 /* MacInternalUpdater.swift */, + 27BA005426E4ECD1008FC1A3 /* GroupReturnAndLeft.swift */, + ); + path = MacOS; + sourceTree = ""; + }; + 27BA005526E4ECD1008FC1A3 /* PendingMessages */ = { + isa = PBXGroup; + children = ( + 27BA005626E4ECD1008FC1A3 /* StandaloneSendMessage.swift */, + 27BA005726E4ECD1008FC1A3 /* ChatUpdatingMessageMedia.swift */, + 27BA005826E4ECD1008FC1A3 /* EnqueueMessage.swift */, + 27BA005926E4ECD1008FC1A3 /* PendingMessageUploadedContent.swift */, + 27BA005A26E4ECD1008FC1A3 /* PendingUpdateMessageManager.swift */, + 27BA005B26E4ECD1008FC1A3 /* StandaloneUploadedMedia.swift */, + 27BA005C26E4ECD1008FC1A3 /* RequestEditMessage.swift */, + ); + path = PendingMessages; + sourceTree = ""; + }; + 27BA006026E4ECD1008FC1A3 /* Network */ = { + isa = PBXGroup; + children = ( + 27BA006126E4ECD1008FC1A3 /* NetworkType.swift */, + 27BA006226E4ECD1008FC1A3 /* MultipartFetch.swift */, + 27BA006326E4ECD1008FC1A3 /* Network.swift */, + 27BA006426E4ECD1008FC1A3 /* MultiplexedRequestManager.swift */, + 27BA006526E4ECD1008FC1A3 /* Download.swift */, + 27BA006626E4ECD1008FC1A3 /* MultipartUpload.swift */, + 27BA006726E4ECD1008FC1A3 /* FetchedMediaResource.swift */, + 27BA006826E4ECD1008FC1A3 /* ProxyServersStatuses.swift */, + 27BA006926E4ECD1008FC1A3 /* FetchHttpResource.swift */, + ); + path = Network; + sourceTree = ""; + }; + 27BA006C26E4ECD1008FC1A3 /* Utils */ = { + isa = PBXGroup; + children = ( + 27BA006D26E4ECD1008FC1A3 /* ImageRepresentationsUtils.swift */, + 27BA006E26E4ECD1008FC1A3 /* Coding.swift */, + 27BA006F26E4ECD1008FC1A3 /* MediaResourceNetworkStatsTag.swift */, + 27BA007026E4ECD1008FC1A3 /* StringFormat.swift */, + 27BA007126E4ECD1008FC1A3 /* Log.swift */, + 27BA007226E4ECD1008FC1A3 /* PeerUtils.swift */, + 27BA007326E4ECD1008FC1A3 /* DecryptedResourceData.swift */, + 27BA007426E4ECD1008FC1A3 /* MD5.swift */, + 27BA007526E4ECD1008FC1A3 /* JSON.swift */, + 27BA007626E4ECD1008FC1A3 /* UpdateMessageMedia.swift */, + 27BA007726E4ECD1008FC1A3 /* MemoryBufferExtensions.swift */, + 27BA007826E4ECD1008FC1A3 /* CanSendMessagesToPeer.swift */, + 27BA007926E4ECD1008FC1A3 /* MessageUtils.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 27BA007A26E4ECD1008FC1A3 /* State */ = { + isa = PBXGroup; + children = ( + 27BA007B26E4ECD1008FC1A3 /* ManagedSynchronizeSavedStickersOperations.swift */, + 27BA007C26E4ECD1008FC1A3 /* AccountState.swift */, + 27BA007D26E4ECD1008FC1A3 /* ProcessSecretChatIncomingDecryptedOperations.swift */, + 27BA007E26E4ECD1008FC1A3 /* HistoryViewStateValidation.swift */, + 27BA007F26E4ECD1008FC1A3 /* SynchronizeEmojiKeywordsOperation.swift */, + 27BA008026E4ECD1008FC1A3 /* ApplyUpdateMessage.swift */, + 27BA008126E4ECD1008FC1A3 /* ChatHistoryPreloadManager.swift */, + 27BA008226E4ECD1008FC1A3 /* AppConfiguration.swift */, + 27BA008326E4ECD1008FC1A3 /* SynchronizeAppLogEventsOperation.swift */, + 27BA008426E4ECD1008FC1A3 /* SynchronizeMarkAllUnseenPersonalMessagesOperation.swift */, + 27BA008526E4ECD1008FC1A3 /* ManagedSynchronizeEmojiKeywordsOperations.swift */, + 27BA008626E4ECD1008FC1A3 /* UpdatesApiUtils.swift */, + 27BA008726E4ECD1008FC1A3 /* ManagedNotificationSettingsBehaviors.swift */, + 27BA008826E4ECD1008FC1A3 /* MessageMediaPreuploadManager.swift */, + 27BA008926E4ECD1008FC1A3 /* ManagedPendingPeerNotificationSettings.swift */, + 27BA008A26E4ECD1008FC1A3 /* SynchronizeRecentlyUsedMediaOperations.swift */, + 27BA008B26E4ECD1008FC1A3 /* ManagedMessageHistoryHoles.swift */, + 27BA008C26E4ECD1008FC1A3 /* ManagedConsumePersonalMessagesActions.swift */, + 27BA008D26E4ECD1008FC1A3 /* ManagedProxyInfoUpdates.swift */, + 27BA008E26E4ECD2008FC1A3 /* ManagedAppConfigurationUpdates.swift */, + 27BA008F26E4ECD2008FC1A3 /* AccountStateManager.swift */, + 27BA009026E4ECD2008FC1A3 /* CachedSentMediaReferences.swift */, + 27BA009126E4ECD2008FC1A3 /* ManagedLocalizationUpdatesOperations.swift */, + 27BA009226E4ECD2008FC1A3 /* FetchChatList.swift */, + 27BA009326E4ECD2008FC1A3 /* ManagedGlobalNotificationSettings.swift */, + 27BA009426E4ECD2008FC1A3 /* FetchSecretFileResource.swift */, + 27BA009526E4ECD2008FC1A3 /* ChannelState.swift */, + 27BA009626E4ECD2008FC1A3 /* ManagedSynchronizeRecentlyUsedMediaOperations.swift */, + 27BA009726E4ECD2008FC1A3 /* ManagedRecentStickers.swift */, + 27BA009826E4ECD2008FC1A3 /* ManagedChatListHoles.swift */, + 27BA009926E4ECD2008FC1A3 /* ManagedSynchronizeMarkAllUnseenPersonalMessagesOperations.swift */, + 27BA009A26E4ECD2008FC1A3 /* ManagedServiceViews.swift */, + 27BA009B26E4ECD2008FC1A3 /* PeerInputActivityManager.swift */, + 27BA009C26E4ECD2008FC1A3 /* ManagedSynchronizePinnedChatsOperations.swift */, + 27BA009D26E4ECD2008FC1A3 /* SynchronizeSavedStickersOperation.swift */, + 27BA009E26E4ECD2008FC1A3 /* MessageReactions.swift */, + 27BA009F26E4ECD2008FC1A3 /* ManagedCloudChatRemoveMessagesOperations.swift */, + 27BA00A026E4ECD2008FC1A3 /* PendingMessageManager.swift */, + 27BA00A126E4ECD2008FC1A3 /* StickerManagement.swift */, + 27BA00A226E4ECD2008FC1A3 /* InitializeAccountAfterLogin.swift */, + 27BA00A326E4ECD2008FC1A3 /* UpdateGroup.swift */, + 27BA00A426E4ECD2008FC1A3 /* ManagedSynchronizeGroupMessageStats.swift */, + 27BA00A526E4ECD2008FC1A3 /* Fetch.swift */, + 27BA00A626E4ECD2008FC1A3 /* ManagedSynchronizeMarkFeaturedStickerPacksAsSeenOperations.swift */, + 27BA00A726E4ECD2008FC1A3 /* SynchronizeLocalizationUpdatesOperation.swift */, + 27BA00A826E4ECD2008FC1A3 /* ManagedAnimatedEmojiUpdates.swift */, + 27BA00A926E4ECD2008FC1A3 /* UpdateMessageService.swift */, + 27BA00AA26E4ECD2008FC1A3 /* SynchronizeInstalledStickerPacksOperation.swift */, + 27BA00AB26E4ECD2008FC1A3 /* ManagedLocalInputActivities.swift */, + 27BA00AC26E4ECD2008FC1A3 /* ManagedSynchronizeInstalledStickerPacksOperations.swift */, + 27BA00AD26E4ECD2008FC1A3 /* ManagedSynchronizeGroupedPeersOperations.swift */, + 27BA00AE26E4ECD2008FC1A3 /* CallSessionManager.swift */, + 27BA00AF26E4ECD2008FC1A3 /* ManagedSecretChatOutgoingOperations.swift */, + 27BA00B026E4ECD2008FC1A3 /* ManagedSynchronizeConsumeMessageContentsOperations.swift */, + 27BA00B126E4ECD2008FC1A3 /* PeerInputActivity.swift */, + 27BA00B226E4ECD2008FC1A3 /* SynchronizeSavedGifsOperation.swift */, + 27BA00B326E4ECD2008FC1A3 /* ManagedAutoremoveMessageOperations.swift */, + 27BA00B426E4ECD2008FC1A3 /* AppUpdate.swift */, + 27BA00B526E4ECD2008FC1A3 /* AccountViewTracker.swift */, + 27BA00B626E4ECD2008FC1A3 /* SynchronizeChatInputStateOperation.swift */, + 27BA00B726E4ECD2008FC1A3 /* SynchronizeGroupedPeersOperation.swift */, + 27BA00B826E4ECD2008FC1A3 /* ProcessSecretChatIncomingEncryptedOperations.swift */, + 27BA00B926E4ECD2008FC1A3 /* UnauthorizedAccountStateManager.swift */, + 27BA00BA26E4ECD2008FC1A3 /* ContactSyncManager.swift */, + 27BA00BB26E4ECD2008FC1A3 /* ManagedAccountPresence.swift */, + 27BA00BC26E4ECD2008FC1A3 /* ManagedSynchronizePeerReadStates.swift */, + 27BA00BD26E4ECD2008FC1A3 /* SynchronizeConsumeMessageContentsOperation.swift */, + 27BA00BE26E4ECD2008FC1A3 /* CloudChatRemoveMessagesOperation.swift */, + 27BA00BF26E4ECD2008FC1A3 /* Holes.swift */, + 27BA00C026E4ECD2008FC1A3 /* ManagedSynchronizeChatInputStateOperations.swift */, + 27BA00C126E4ECD2008FC1A3 /* AppChangelogState.swift */, + 27BA00C226E4ECD2008FC1A3 /* ManagedSynchronizeAppLogEventsOperations.swift */, + 27BA00C326E4ECD2008FC1A3 /* SynchronizePeerReadState.swift */, + 27BA00C426E4ECD2008FC1A3 /* ManagedSynchronizeSavedGifsOperations.swift */, + 27BA00C526E4ECD2008FC1A3 /* ManagedConfigurationUpdates.swift */, + 27BA00C626E4ECD2008FC1A3 /* ManagedVoipConfigurationUpdates.swift */, + 27BA00C726E4ECD2008FC1A3 /* AppChangelog.swift */, + 27BA00C826E4ECD2008FC1A3 /* AccountStateManagementUtils.swift */, + 27BA00C926E4ECD2008FC1A3 /* ManagedAutodownloadSettingsUpdates.swift */, + 27BA00CA26E4ECD2008FC1A3 /* Serialization.swift */, + ); + path = State; + sourceTree = ""; + }; + 27BA00CE26E4ECD2008FC1A3 /* TelegramEngine */ = { + isa = PBXGroup; + children = ( + 27BA00CF26E4ECD2008FC1A3 /* Calls */, + 27BA00D326E4ECD2008FC1A3 /* SecureId */, + 27BA00F126E4ECD2008FC1A3 /* Payments */, + 27BA00F526E4ECD2008FC1A3 /* Stickers */, + 27BA010026E4ECD2008FC1A3 /* Messages */, + 27BA012026E4ECD2008FC1A3 /* Privacy */, + 27BA012926E4ECD2008FC1A3 /* Resolve */, + 27BA012C26E4ECD2008FC1A3 /* Auth */, + 27BA013226E4ECD2008FC1A3 /* Contacts */, + 27BA013A26E4ECD2008FC1A3 /* Resources */, + 27BA013D26E4ECD2008FC1A3 /* HistoryImport */, + 27BA013F26E4ECD2008FC1A3 /* Utils */, + 27BA014126E4ECD2008FC1A3 /* PeersNearby */, + 27BA014426E4ECD2008FC1A3 /* TelegramEngine.swift */, + 27BA014526E4ECD2008FC1A3 /* Peers */, + 27BA017626E4ECD2008FC1A3 /* Data */, + 27BA017A26E4ECD2008FC1A3 /* AccountData */, + 27BA018026E4ECD2008FC1A3 /* Themes */, + 27BA018326E4ECD2008FC1A3 /* Localization */, + ); + path = TelegramEngine; + sourceTree = ""; + }; + 27BA00CF26E4ECD2008FC1A3 /* Calls */ = { + isa = PBXGroup; + children = ( + 27BA00D026E4ECD2008FC1A3 /* TelegramEngineCalls.swift */, + 27BA00D126E4ECD2008FC1A3 /* GroupCalls.swift */, + 27BA00D226E4ECD2008FC1A3 /* RateCall.swift */, + ); + path = Calls; + sourceTree = ""; + }; + 27BA00D326E4ECD2008FC1A3 /* SecureId */ = { + isa = PBXGroup; + children = ( + 27BA00D426E4ECD2008FC1A3 /* SecureIdPersonalDetailsValue.swift */, + 27BA00D526E4ECD2008FC1A3 /* SecureIdValue.swift */, + 27BA00D626E4ECD2008FC1A3 /* RequestSecureIdForm.swift */, + 27BA00D726E4ECD2008FC1A3 /* SecureIdAddressValue.swift */, + 27BA00D826E4ECD2008FC1A3 /* SecureIdEmailValue.swift */, + 27BA00D926E4ECD2008FC1A3 /* SecureIdUtilityBillValue.swift */, + 27BA00DA26E4ECD2008FC1A3 /* TelegramEngineSecureId.swift */, + 27BA00DB26E4ECD2008FC1A3 /* SecureIdValueAccessContext.swift */, + 27BA00DC26E4ECD2008FC1A3 /* SecureIdPadding.swift */, + 27BA00DD26E4ECD2008FC1A3 /* SecureIdConfiguration.swift */, + 27BA00DE26E4ECD2008FC1A3 /* SaveSecureIdValue.swift */, + 27BA00DF26E4ECD2008FC1A3 /* SecureIdIDCardValue.swift */, + 27BA00E026E4ECD2008FC1A3 /* SecureIdPassportRegistrationValue.swift */, + 27BA00E126E4ECD2008FC1A3 /* VerifySecureIdValue.swift */, + 27BA00E226E4ECD2008FC1A3 /* SecureIdDriversLicenseValue.swift */, + 27BA00E326E4ECD2008FC1A3 /* SecureIdRentalAgreementValue.swift */, + 27BA00E426E4ECD2008FC1A3 /* GrantSecureIdAccess.swift */, + 27BA00E526E4ECD2008FC1A3 /* SecureIdForm.swift */, + 27BA00E626E4ECD2008FC1A3 /* SecureIdVerificationDocumentReference.swift */, + 27BA00E726E4ECD2008FC1A3 /* SecureIdBankStatementValue.swift */, + 27BA00E826E4ECD2008FC1A3 /* AccessSecureId.swift */, + 27BA00E926E4ECD2008FC1A3 /* SecureFileMediaResource.swift */, + 27BA00EA26E4ECD2008FC1A3 /* UploadSecureIdFile.swift */, + 27BA00EB26E4ECD2008FC1A3 /* SecureIdValueContentError.swift */, + 27BA00EC26E4ECD2008FC1A3 /* SecureIdDataTypes.swift */, + 27BA00ED26E4ECD2008FC1A3 /* SecureIdInternalPassportValue.swift */, + 27BA00EE26E4ECD2008FC1A3 /* SecureIdPassportValue.swift */, + 27BA00EF26E4ECD2008FC1A3 /* SecureIdTemporaryRegistrationValue.swift */, + 27BA00F026E4ECD2008FC1A3 /* SecureIdPhoneValue.swift */, + ); + path = SecureId; + sourceTree = ""; + }; + 27BA00F126E4ECD2008FC1A3 /* Payments */ = { + isa = PBXGroup; + children = ( + 27BA00F226E4ECD2008FC1A3 /* TelegramEnginePayments.swift */, + 27BA00F326E4ECD2008FC1A3 /* BotPaymentForm.swift */, + 27BA00F426E4ECD2008FC1A3 /* BankCards.swift */, + ); + path = Payments; + sourceTree = ""; + }; + 27BA00F526E4ECD2008FC1A3 /* Stickers */ = { + isa = PBXGroup; + children = ( + 27BA00F626E4ECD2008FC1A3 /* ImportStickers.swift */, + 27BA00F726E4ECD2008FC1A3 /* StickerPack.swift */, + 27BA00F826E4ECD2008FC1A3 /* SearchStickers.swift */, + 27BA00F926E4ECD2008FC1A3 /* TelegramEngineStickers.swift */, + 27BA00FA26E4ECD2008FC1A3 /* LoadedStickerPack.swift */, + 27BA00FB26E4ECD2008FC1A3 /* StickerPackInteractiveOperations.swift */, + 27BA00FC26E4ECD2008FC1A3 /* EmojiKeywords.swift */, + 27BA00FD26E4ECD2008FC1A3 /* ArchivedStickerPacks.swift */, + 27BA00FE26E4ECD2008FC1A3 /* CachedStickerPack.swift */, + 27BA00FF26E4ECD2008FC1A3 /* StickerSetInstallation.swift */, + ); + path = Stickers; + sourceTree = ""; + }; + 27BA010026E4ECD2008FC1A3 /* Messages */ = { + isa = PBXGroup; + children = ( + 27BA010126E4ECD2008FC1A3 /* ClearCloudDrafts.swift */, + 27BA010226E4ECD2008FC1A3 /* InstallInteractiveReadMessagesAction.swift */, + 27BA010326E4ECD2008FC1A3 /* TelegramEngineMessages.swift */, + 27BA010426E4ECD2008FC1A3 /* Polls.swift */, + 27BA010526E4ECD2008FC1A3 /* SearchMessages.swift */, + 27BA010626E4ECD2008FC1A3 /* ChatList.swift */, + 27BA010726E4ECD2008FC1A3 /* LoadMessagesIfNecessary.swift */, + 27BA010826E4ECD2008FC1A3 /* RequestStartBot.swift */, + 27BA010926E4ECD2008FC1A3 /* ReplyThreadHistory.swift */, + 27BA010A26E4ECD2008FC1A3 /* CallList.swift */, + 27BA010B26E4ECD2008FC1A3 /* Message.swift */, + 27BA010C26E4ECD2008FC1A3 /* RecentlyUsedHashtags.swift */, + 27BA010D26E4ECD2008FC1A3 /* UpdatePinnedMessage.swift */, + 27BA010E26E4ECD2008FC1A3 /* EarliestUnseenPersonalMentionMessage.swift */, + 27BA010F26E4ECD2008FC1A3 /* ForwardGame.swift */, + 27BA011026E4ECD2008FC1A3 /* ApplyMaxReadIndexInteractively.swift */, + 27BA011126E4ECD2008FC1A3 /* Media.swift */, + 27BA011226E4ECD2008FC1A3 /* PeerLiveLocationsContext.swift */, + 27BA011326E4ECD2008FC1A3 /* OutgoingMessageWithChatContextResult.swift */, + 27BA011426E4ECD2008FC1A3 /* EngineGroupCallDescription.swift */, + 27BA011526E4ECD2008FC1A3 /* MessageReadStats.swift */, + 27BA011626E4ECD2008FC1A3 /* ScheduledMessages.swift */, + 27BA011726E4ECD2008FC1A3 /* DeleteMessages.swift */, + 27BA011826E4ECD2008FC1A3 /* RequestMessageActionCallback.swift */, + 27BA011926E4ECD2008FC1A3 /* MarkAllChatsAsRead.swift */, + 27BA011A26E4ECD2008FC1A3 /* DeleteMessagesInteractively.swift */, + 27BA011B26E4ECD2008FC1A3 /* RequestChatContextResults.swift */, + 27BA011C26E4ECD2008FC1A3 /* MarkMessageContentAsConsumedInteractively.swift */, + 27BA011D26E4ECD2008FC1A3 /* ReadState.swift */, + 27BA011E26E4ECD2008FC1A3 /* AdMessages.swift */, + 27BA011F26E4ECD2008FC1A3 /* ExportMessageLink.swift */, + ); + path = Messages; + sourceTree = ""; + }; + 27BA012026E4ECD2008FC1A3 /* Privacy */ = { + isa = PBXGroup; + children = ( + 27BA012126E4ECD2008FC1A3 /* BlockedPeers.swift */, + 27BA012226E4ECD2008FC1A3 /* UpdatedAccountPrivacySettings.swift */, + 27BA012326E4ECD2008FC1A3 /* BlockedPeersContext.swift */, + 27BA012426E4ECD2008FC1A3 /* TelegramEnginePrivacy.swift */, + 27BA012526E4ECD2008FC1A3 /* RecentAccountSessions.swift */, + 27BA012626E4ECD2008FC1A3 /* RecentWebSessions.swift */, + 27BA012726E4ECD2008FC1A3 /* RecentAccountSession.swift */, + 27BA012826E4ECD2008FC1A3 /* ActiveSessionsContext.swift */, + ); + path = Privacy; + sourceTree = ""; + }; + 27BA012926E4ECD2008FC1A3 /* Resolve */ = { + isa = PBXGroup; + children = ( + 27BA012A26E4ECD2008FC1A3 /* TelegramEngineResolve.swift */, + 27BA012B26E4ECD2008FC1A3 /* DeepLinkInfo.swift */, + ); + path = Resolve; + sourceTree = ""; + }; + 27BA012C26E4ECD2008FC1A3 /* Auth */ = { + isa = PBXGroup; + children = ( + 27BA012D26E4ECD2008FC1A3 /* TelegramEngineAuth.swift */, + 27BA012E26E4ECD2008FC1A3 /* CancelAccountReset.swift */, + 27BA012F26E4ECD2008FC1A3 /* ConfirmTwoStepRecoveryEmail.swift */, + 27BA013026E4ECD2008FC1A3 /* AuthTransfer.swift */, + 27BA013126E4ECD2008FC1A3 /* TwoStepVerification.swift */, + ); + path = Auth; + sourceTree = ""; + }; + 27BA013226E4ECD2008FC1A3 /* Contacts */ = { + isa = PBXGroup; + children = ( + 27BA013326E4ECD2008FC1A3 /* PhoneNumber.swift */, + 27BA013426E4ECD2008FC1A3 /* TelegramEngineContacts.swift */, + 27BA013526E4ECD2008FC1A3 /* ContactManagement.swift */, + 27BA013626E4ECD2008FC1A3 /* ImportContact.swift */, + 27BA013726E4ECD2008FC1A3 /* UpdateContactName.swift */, + 27BA013826E4ECD2008FC1A3 /* DeviceContact.swift */, + 27BA013926E4ECD2008FC1A3 /* TelegramDeviceContactImportInfo.swift */, + ); + path = Contacts; + sourceTree = ""; + }; + 27BA013A26E4ECD2008FC1A3 /* Resources */ = { + isa = PBXGroup; + children = ( + 27BA013B26E4ECD2008FC1A3 /* CollectCacheUsageStats.swift */, + 27BA013C26E4ECD2008FC1A3 /* TelegramEngineResources.swift */, + ); + path = Resources; + sourceTree = ""; + }; + 27BA013D26E4ECD2008FC1A3 /* HistoryImport */ = { + isa = PBXGroup; + children = ( + 27BA013E26E4ECD2008FC1A3 /* TelegramEngineHistoryImport.swift */, + ); + path = HistoryImport; + sourceTree = ""; + }; + 27BA013F26E4ECD2008FC1A3 /* Utils */ = { + isa = PBXGroup; + children = ( + 27BA014026E4ECD2008FC1A3 /* StringCodingKey.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 27BA014126E4ECD2008FC1A3 /* PeersNearby */ = { + isa = PBXGroup; + children = ( + 27BA014226E4ECD2008FC1A3 /* PeersNearby.swift */, + 27BA014326E4ECD2008FC1A3 /* TelegramEnginePeersNearby.swift */, + ); + path = PeersNearby; + sourceTree = ""; + }; + 27BA014526E4ECD2008FC1A3 /* Peers */ = { + isa = PBXGroup; + children = ( + 27BA014626E4ECD2008FC1A3 /* RemovePeerChat.swift */, + 27BA014726E4ECD2008FC1A3 /* ChannelOwnershipTransfer.swift */, + 27BA014826E4ECD2008FC1A3 /* ChangePeerNotificationSettings.swift */, + 27BA014926E4ECD2008FC1A3 /* ManageChannelDiscussionGroup.swift */, + 27BA014A26E4ECD2008FC1A3 /* RequestUserPhotos.swift */, + 27BA014B26E4ECD2008FC1A3 /* ChatListFiltering.swift */, + 27BA014C26E4ECD2008FC1A3 /* SupportPeerId.swift */, + 27BA014D26E4ECD2008FC1A3 /* ChannelHistoryAvailabilitySettings.swift */, + 27BA014E26E4ECD2008FC1A3 /* JoinChannel.swift */, + 27BA014F26E4ECD2008FC1A3 /* UpdateCachedPeerData.swift */, + 27BA015026E4ECD2008FC1A3 /* GroupsInCommon.swift */, + 27BA015126E4ECD2008FC1A3 /* RecentPeers.swift */, + 27BA015226E4ECD2008FC1A3 /* UpdateGroupSpecificStickerset.swift */, + 27BA015326E4ECD2008FC1A3 /* NotificationExceptionsList.swift */, + 27BA015426E4ECD2008FC1A3 /* ChannelAdminEventLogContext.swift */, + 27BA015526E4ECD2008FC1A3 /* ChannelAdminEventLogs.swift */, + 27BA015626E4ECD2008FC1A3 /* CheckPeerChatServiceActions.swift */, + 27BA015726E4ECD2008FC1A3 /* SearchGroupMembers.swift */, + 27BA015826E4ECD2008FC1A3 /* JoinLink.swift */, + 27BA015926E4ECD2008FC1A3 /* AddPeerMember.swift */, + 27BA015A26E4ECD2008FC1A3 /* ReportPeer.swift */, + 27BA015B26E4ECD2008FC1A3 /* ChannelBlacklist.swift */, + 27BA015C26E4ECD2008FC1A3 /* InactiveChannels.swift */, + 27BA015D26E4ECD2008FC1A3 /* PeerPhotoUpdater.swift */, + 27BA015E26E4ECD2008FC1A3 /* ChatOnlineMembers.swift */, + 27BA015F26E4ECD2008FC1A3 /* SlowMode.swift */, + 27BA016026E4ECD2008FC1A3 /* PeerAdmins.swift */, + 27BA016126E4ECD2008FC1A3 /* ToggleChannelSignatures.swift */, + 27BA016226E4ECD2008FC1A3 /* ConvertGroupToSupergroup.swift */, + 27BA016326E4ECD2008FC1A3 /* CreateSecretChat.swift */, + 27BA016426E4ECD2008FC1A3 /* CreateGroup.swift */, + 27BA016526E4ECD2008FC1A3 /* PeerCommands.swift */, + 27BA016626E4ECD2008FC1A3 /* TogglePeerChatPinned.swift */, + 27BA016726E4ECD2008FC1A3 /* ChannelCreation.swift */, + 27BA016826E4ECD2008FC1A3 /* AddressNames.swift */, + 27BA016926E4ECD2008FC1A3 /* PeerSpecificStickerPack.swift */, + 27BA016A26E4ECD2008FC1A3 /* InvitationLinks.swift */, + 27BA016B26E4ECD2008FC1A3 /* ResolvePeerByName.swift */, + 27BA016C26E4ECD2008FC1A3 /* FindChannelById.swift */, + 27BA016D26E4ECD2008FC1A3 /* SearchPeers.swift */, + 27BA016E26E4ECD2008FC1A3 /* UpdatePeerInfo.swift */, + 27BA016F26E4ECD2008FC1A3 /* OpaqueChatState.swift */, + 27BA017026E4ECD2008FC1A3 /* TelegramEnginePeers.swift */, + 27BA017126E4ECD2008FC1A3 /* ChannelMembers.swift */, + 27BA017226E4ECD2008FC1A3 /* Peer.swift */, + 27BA017326E4ECD2008FC1A3 /* ChannelParticipants.swift */, + 27BA017426E4ECD2008FC1A3 /* RemovePeerMember.swift */, + 27BA017526E4ECD2008FC1A3 /* RecentlySearchedPeerIds.swift */, + ); + path = Peers; + sourceTree = ""; + }; + 27BA017626E4ECD2008FC1A3 /* Data */ = { + isa = PBXGroup; + children = ( + 27BA017726E4ECD2008FC1A3 /* TelegramEngineData.swift */, + 27BA017826E4ECD2008FC1A3 /* PeerSummary.swift */, + 27BA017926E4ECD2008FC1A3 /* Configuration.swift */, + ); + path = Data; + sourceTree = ""; + }; + 27BA017A26E4ECD2008FC1A3 /* AccountData */ = { + isa = PBXGroup; + children = ( + 27BA017B26E4ECD2008FC1A3 /* RegisterNotificationToken.swift */, + 27BA017C26E4ECD2008FC1A3 /* UpdateAccountPeerName.swift */, + 27BA017D26E4ECD2008FC1A3 /* TelegramEngineAccountData.swift */, + 27BA017E26E4ECD2008FC1A3 /* ChangeAccountPhoneNumber.swift */, + 27BA017F26E4ECD2008FC1A3 /* TermsOfService.swift */, + ); + path = AccountData; + sourceTree = ""; + }; + 27BA018026E4ECD2008FC1A3 /* Themes */ = { + isa = PBXGroup; + children = ( + 27BA018126E4ECD2008FC1A3 /* TelegramEngineThemes.swift */, + 27BA018226E4ECD2008FC1A3 /* ChatThemes.swift */, + ); + path = Themes; + sourceTree = ""; + }; + 27BA018326E4ECD2008FC1A3 /* Localization */ = { + isa = PBXGroup; + children = ( + 27BA018426E4ECD2008FC1A3 /* TelegramEngineLocalization.swift */, + 27BA018526E4ECD2008FC1A3 /* Localizations.swift */, + 27BA018626E4ECD2008FC1A3 /* Countries.swift */, + 27BA018726E4ECD2008FC1A3 /* LocalizationInfo.swift */, + 27BA018826E4ECD2008FC1A3 /* SuggestedLocalizationEntry.swift */, + 27BA018926E4ECD2008FC1A3 /* LocalizationListState.swift */, + 27BA018A26E4ECD2008FC1A3 /* LocalizationPreview.swift */, + ); + path = Localization; + sourceTree = ""; + }; + 27BA018B26E4ECD2008FC1A3 /* ApiUtils */ = { + isa = PBXGroup; + children = ( + 27BA018C26E4ECD2008FC1A3 /* TelegramGroup.swift */, + 27BA018D26E4ECD2008FC1A3 /* TelegramPeerNotificationSettings.swift */, + 27BA018E26E4ECD2008FC1A3 /* TelegramUser.swift */, + 27BA018F26E4ECD2008FC1A3 /* CloudMediaResourceParameters.swift */, + 27BA019026E4ECD2008FC1A3 /* TelegramMediaWebDocument.swift */, + 27BA019126E4ECD2008FC1A3 /* BotInfo.swift */, + 27BA019226E4ECD2008FC1A3 /* CachedChannelParticipants.swift */, + 27BA019326E4ECD2008FC1A3 /* CachedGroupParticipants.swift */, + 27BA019426E4ECD2008FC1A3 /* TelegramChannelAdminRights.swift */, + 27BA019526E4ECD2008FC1A3 /* TextEntitiesMessageAttribute.swift */, + 27BA019626E4ECD2008FC1A3 /* TelegramMediaWebpage.swift */, + 27BA019726E4ECD2008FC1A3 /* TelegramMediaWebFile.swift */, + 27BA019826E4ECD2008FC1A3 /* TelegramMediaGame.swift */, + 27BA019926E4ECD2008FC1A3 /* Wallpaper.swift */, + 27BA019A26E4ECD2008FC1A3 /* TelegramMediaPoll.swift */, + 27BA019B26E4ECD2008FC1A3 /* RemoteStorageConfiguration.swift */, + 27BA019C26E4ECD2008FC1A3 /* TelegramMediaImage.swift */, + 27BA019D26E4ECD2008FC1A3 /* ReactionsMessageAttribute.swift */, + 27BA019E26E4ECD2008FC1A3 /* ChatContextResult.swift */, + 27BA019F26E4ECD2008FC1A3 /* ImageRepresentationWithReference.swift */, + 27BA01A026E4ECD2008FC1A3 /* TelegramMediaAction.swift */, + 27BA01A126E4ECD2008FC1A3 /* TelegramMediaMap.swift */, + 27BA01A226E4ECD2008FC1A3 /* EncryptedMediaResource.swift */, + 27BA01A326E4ECD2008FC1A3 /* PeerAccessRestrictionInfo.swift */, + 27BA01A426E4ECD2008FC1A3 /* TelegramChannelBannedRights.swift */, + 27BA01A526E4ECD2008FC1A3 /* ExportedInvitation.swift */, + 27BA01A626E4ECD2008FC1A3 /* StoreMessage_Telegram.swift */, + 27BA01A726E4ECD2008FC1A3 /* Theme.swift */, + 27BA01A826E4ECD2008FC1A3 /* PeerGeoLocation.swift */, + 27BA01A926E4ECD2008FC1A3 /* ApiUtils.swift */, + 27BA01AA26E4ECD2008FC1A3 /* RichText.swift */, + 27BA01AB26E4ECD2008FC1A3 /* InstantPage.swift */, + 27BA01AC26E4ECD2008FC1A3 /* CloudFileMediaResource.swift */, + 27BA01AD26E4ECD2008FC1A3 /* ApiGroupOrChannel.swift */, + 27BA01AE26E4ECD2008FC1A3 /* TelegramChannel.swift */, + 27BA01AF26E4ECD2008FC1A3 /* MediaResourceApiUtils.swift */, + 27BA01B026E4ECD2008FC1A3 /* TelegramUserPresence.swift */, + 27BA01B126E4ECD2008FC1A3 /* AdMessageAttribute.swift */, + 27BA01B226E4ECD2008FC1A3 /* TelegramMediaFile.swift */, + 27BA01B326E4ECD2008FC1A3 /* ReplyMarkupMessageAttribute.swift */, + ); + path = ApiUtils; + sourceTree = ""; + }; + 27BA01B426E4ECD2008FC1A3 /* SyncCore */ = { + isa = PBXGroup; + children = ( + 27BA01B526E4ECD2008FC1A3 /* SyncCore_TelegramMediaAction.swift */, + 27BA01B626E4ECD2008FC1A3 /* SyncCore_AutodownloadSettings.swift */, + 27BA01B726E4ECD2008FC1A3 /* SyncCore_wallapersState.swift */, + 27BA01B826E4ECD2008FC1A3 /* SyncCore_SynchronizeChatInputStateOperation.swift */, + 27BA01B926E4ECD2008FC1A3 /* SyncCore_TelegramUser.swift */, + 27BA01BA26E4ECD2008FC1A3 /* SyncCore_TelegramChatAdminRights.swift */, + 27BA01BB26E4ECD2008FC1A3 /* SyncCore_PeerAccessHash.swift */, + 27BA01BC26E4ECD2008FC1A3 /* SyncCore_ViewCountMessageAttribute.swift */, + 27BA01BD26E4ECD2008FC1A3 /* SyncCore_SynchronizeAppLogEventsOperation.swift */, + 27BA01BE26E4ECD2008FC1A3 /* SyncCore_TelegramMediaImage.swift */, + 27BA01BF26E4ECD2008FC1A3 /* SyncCore_AppConfiguration.swift */, + 27BA01C026E4ECD2008FC1A3 /* SyncCore_SecretChatFileReference.swift */, + 27BA01C126E4ECD2008FC1A3 /* SyncCore_LoggingSettings.swift */, + 27BA01C226E4ECD2008FC1A3 /* SyncCore_SynchronizeSavedStickersOperation.swift */, + 27BA01C326E4ECD2008FC1A3 /* SyncCore_SecureFileMediaResource.swift */, + 27BA01C426E4ECD2008FC1A3 /* SyncCore_UpdateMessageReactionsAction.swift */, + 27BA01C526E4ECD2008FC1A3 /* SyncCore_EditedMessageAttribute.swift */, + 27BA01C626E4ECD2008FC1A3 /* SyncCore_SearchBotsConfiguration.swift */, + 27BA01C726E4ECD2008FC1A3 /* SyncCore_TemporaryTwoStepPasswordToken.swift */, + 27BA01C826E4ECD2008FC1A3 /* SyncCore_RecentPeerItem.swift */, + 27BA01C926E4ECD2008FC1A3 /* SyncCore_Localization.swift */, + 27BA01CA26E4ECD2008FC1A3 /* SyncCore_GlobalNotificationSettings.swift */, + 27BA01CB26E4ECD2008FC1A3 /* SyncCore_TelegramChatBannedRights.swift */, + 27BA01CC26E4ECD2008FC1A3 /* SyncCore_TelegramSecretChat.swift */, + 27BA01CD26E4ECD2008FC1A3 /* SyncCore_CachedStickerQueryResult.swift */, + 27BA01CE26E4ECD2008FC1A3 /* SyncCore_TelegramMediaGame.swift */, + 27BA01CF26E4ECD2008FC1A3 /* SyncCore_TelegramMediaContact.swift */, + 27BA01D026E4ECD2008FC1A3 /* SyncCore_StickerPackCollectionInfo.swift */, + 27BA01D126E4ECD2008FC1A3 /* SyncCore_CachedThemesConfiguration.swift */, + 27BA01D226E4ECD2008FC1A3 /* SyncCore_RestrictedContentMessageAttribute.swift */, + 27BA01D326E4ECD2008FC1A3 /* SyncCore_ConsumableContentMessageAttribute.swift */, + 27BA01D426E4ECD2008FC1A3 /* SyncCore_CachedResolvedByNamePeer.swift */, + 27BA01D526E4ECD2008FC1A3 /* SyncCore_TelegramMediaPoll.swift */, + 27BA01D626E4ECD2008FC1A3 /* SyncCore_SecretChatIncomingDecryptedOperation.swift */, + 27BA01D726E4ECD2008FC1A3 /* SyncCore_TelegramPeerNotificationSettings.swift */, + 27BA01D826E4ECD2008FC1A3 /* SyncCore_TelegramMediaUnsupported.swift */, + 27BA01D926E4ECD2008FC1A3 /* SyncCore_CachedChannelData.swift */, + 27BA01DA26E4ECD2008FC1A3 /* SyncCore_AccountSortOrderAttribute.swift */, + 27BA01DB26E4ECD2008FC1A3 /* SyncCore_OutgoingScheduleInfoMessageAttribute.swift */, + 27BA01DC26E4ECD2008FC1A3 /* SyncCore_ConsumePersonalMessageAction.swift */, + 27BA01DD26E4ECD2008FC1A3 /* SyncCore_TelegramTheme.swift */, + 27BA01DE26E4ECD2008FC1A3 /* SyncCore_SecretFileEncryptionKey.swift */, + 27BA01DF26E4ECD2008FC1A3 /* SyncCore_CachedGroupData.swift */, + 27BA01E026E4ECD2008FC1A3 /* SyncCore_CachedSecureIdConfiguration.swift */, + 27BA01E126E4ECD2008FC1A3 /* SyncCore_ForwardHideSendersNamesMessageAttribute.swift */, + 27BA01E226E4ECD2008FC1A3 /* SyncCore_TelegramMediaExpiredContent.swift */, + 27BA01E326E4ECD2008FC1A3 /* SyncCore_SavedStickerItem.swift */, + 27BA01E426E4ECD2008FC1A3 /* SyncCore_AuthorSignatureMessageAttribute.swift */, + 27BA01E526E4ECD2008FC1A3 /* SyncCore_ChannelMessageStateVersionAttribute.swift */, + 27BA01E626E4ECD2008FC1A3 /* SyncCore_ThemeSettings.swift */, + 27BA01E726E4ECD2008FC1A3 /* SyncCore_Namespaces.swift */, + 27BA01E826E4ECD2008FC1A3 /* SyncCore_PixelDimensions.swift */, + 27BA01E926E4ECD2008FC1A3 /* SyncCore_InlineBotMessageAttribute.swift */, + 27BA01EA26E4ECD2008FC1A3 /* SyncCore_LocalizationSettings.swift */, + 27BA01EB26E4ECD2008FC1A3 /* SyncCore_SendScheduledMessageImmediatelyAction.swift */, + 27BA01EC26E4ECD2008FC1A3 /* SyncCore_ReplyMarkupMessageAttribute.swift */, + 27BA01ED26E4ECD2008FC1A3 /* SyncCore_AutoremoveTimeoutMessageAttribute.swift */, + 27BA01EE26E4ECD2008FC1A3 /* SyncCore_JSON.swift */, + 27BA01EF26E4ECD2008FC1A3 /* SyncCore_TelegramGroup.swift */, + 27BA01F026E4ECD2008FC1A3 /* SyncCore_ChannelState.swift */, + 27BA01F126E4ECD2008FC1A3 /* SyncCore_OutgoingMessageInfoAttribute.swift */, + 27BA01F226E4ECD2008FC1A3 /* SyncCore_UnauthorizedAccountState.swift */, + 27BA01F326E4ECD2008FC1A3 /* SyncCore_SecretChatEncryptionConfig.swift */, + 27BA01F426E4ECD2008FC1A3 /* SyncCore_TelegramWallpaper.swift */, + 27BA01F526E4ECD2008FC1A3 /* SyncCore_ArchivedStickerPacksInfo.swift */, + 27BA01F626E4ECD2008FC1A3 /* SyncCore_NetworkSettings.swift */, + 27BA01F726E4ECD2008FC1A3 /* SyncCore_LocalizationInfo.swift */, + 27BA01F826E4ECD2008FC1A3 /* SyncCore_RichText.swift */, + 27BA01F926E4ECD2008FC1A3 /* SyncCore_NotificationInfoMessageAttribute.swift */, + 27BA01FA26E4ECD2008FC1A3 /* SyncCore_InstantPage.swift */, + 27BA01FB26E4ECD2008FC1A3 /* SyncCore_TextEntitiesMessageAttribute.swift */, + 27BA01FC26E4ECD2008FC1A3 /* SyncCore_TelegramUserPresence.swift */, + 27BA01FD26E4ECD2008FC1A3 /* SyncCore_ProxySettings.swift */, + 27BA01FE26E4ECD2008FC1A3 /* SyncCore_ReactionsMessageAttribute.swift */, + 27BA01FF26E4ECD2008FC1A3 /* SyncCore_SecureIdFileReference.swift */, + 27BA020026E4ECD2008FC1A3 /* SyncCore_SynchronizeMarkAllUnseenPersonalMessagesOperation.swift */, + 27BA020126E4ECD2008FC1A3 /* SyncCore_SynchronizeEmojiKeywordsOperation.swift */, + 27BA020226E4ECD2008FC1A3 /* SyncCore_TelegramDeviceContactImportedData.swift */, + 27BA020326E4ECD2008FC1A3 /* SyncCore_ForwardSourceInfoAttribute.swift */, + 27BA020426E4ECD2008FC1A3 /* SyncCore_SourceReferenceMessageAttribute.swift */, + 27BA020526E4ECD2008FC1A3 /* SyncCore_TelegramMediaDice.swift */, + 27BA020626E4ECD2008FC1A3 /* SyncCore_ReplyMessageAttribute.swift */, + 27BA020726E4ECD2008FC1A3 /* SyncCore_WalletCollection.swift */, + 27BA020826E4ECD2008FC1A3 /* SyncCore_EmojiKeywordItem.swift */, + 27BA020926E4ECD2008FC1A3 /* SyncCore_WasScheduledMessageAttribute.swift */, + 27BA020A26E4ECD2008FC1A3 /* SyncCore_MediaReference.swift */, + 27BA020B26E4ECD2008FC1A3 /* SyncCore_ImportableDeviceContactData.swift */, + 27BA020C26E4ECD2008FC1A3 /* SyncCore_AppChangelogState.swift */, + 27BA020D26E4ECD2008FC1A3 /* SyncCore_ContentRequiresValidationMessageAttribute.swift */, + 27BA020E26E4ECD2008FC1A3 /* SyncCore_BotInfo.swift */, + 27BA020F26E4ECD2008FC1A3 /* SyncCore_LimitsConfiguration.swift */, + 27BA021026E4ECD2008FC1A3 /* SyncCore_TelegramMediaMap.swift */, + 27BA021126E4ECD2008FC1A3 /* SyncCore_TelegramMediaResource.swift */, + 27BA021226E4ECD2008FC1A3 /* SyncCore_RemoteStorageConfiguration.swift */, + 27BA021326E4ECD2008FC1A3 /* SyncCore_TelegramMediaFile.swift */, + 27BA021426E4ECD2008FC1A3 /* SyncCore_LocalizationListState.swift */, + 27BA021526E4ECD2008FC1A3 /* SyncCore_SecretChatState.swift */, + 27BA021626E4ECD2008FC1A3 /* SyncCore_TelegramMediaWebpage.swift */, + 27BA021726E4ECD2008FC1A3 /* SyncCore_CachedRecentPeers.swift */, + 27BA021826E4ECD2008FC1A3 /* SyncCore_AuthorizedAccountState.swift */, + 27BA021926E4ECD2008FC1A3 /* SyncCore_PeerReference.swift */, + 27BA021A26E4ECD2008FC1A3 /* SyncCore_SuggestedLocalizationEntry.swift */, + 27BA021B26E4ECD2008FC1A3 /* SyncCore_SecretChatOutgoingOperation.swift */, + 27BA021C26E4ECD2008FC1A3 /* SyncCore_CachedGroupParticipants.swift */, + 27BA021D26E4ECD2008FC1A3 /* SyncCore_CachedStickerPack.swift */, + 27BA021E26E4ECD2008FC1A3 /* SyncCore_ExportedInvitation.swift */, + 27BA021F26E4ECD2008FC1A3 /* SyncCore_RecentHashtagItem.swift */, + 27BA022026E4ECD2008FC1A3 /* SyncCore_CloudFileMediaResource.swift */, + 27BA022126E4ECD2008FC1A3 /* SyncCore_ValidationMessageAttribute.swift */, + 27BA022226E4ECD2008FC1A3 /* SyncCore_ReplyThreadMessageAttribute.swift */, + 27BA022326E4ECD2008FC1A3 /* SyncCore_PeerAccessRestrictionInfo.swift */, + 27BA022426E4ECD2008FC1A3 /* SyncCore_OutgoingChatContextResultMessageAttribute.swift */, + 27BA022526E4ECD2008FC1A3 /* SyncCore_CachedUserData.swift */, + 27BA022626E4ECD2008FC1A3 /* SyncCore_SynchronizeRecentlyUsedMediaOperation.swift */, + 27BA022726E4ECD2008FC1A3 /* SyncCore_CachedLocalizationInfos.swift */, + 27BA022826E4ECD2008FC1A3 /* SyncCore_AccountEnvironmentAttribute.swift */, + 27BA022926E4ECD2008FC1A3 /* SyncCore_PeerStatusSettings.swift */, + 27BA022A26E4ECD2008FC1A3 /* SyncCore_StickerPackItem.swift */, + 27BA022B26E4ECD2008FC1A3 /* SyncCore_SynchronizeSavedGifsOperation.swift */, + 27BA022C26E4ECD2008FC1A3 /* SyncCore_AccountBackupDataAttribute.swift */, + 27BA022D26E4ECD2008FC1A3 /* SyncCore_RecentMediaItem.swift */, + 27BA022E26E4ECD2008FC1A3 /* SyncCore_LoggedOutAccountAttribute.swift */, + 27BA022F26E4ECD2008FC1A3 /* SyncCore_EmojiKeywordCollectionInfo.swift */, + 27BA023026E4ECD2008FC1A3 /* SyncCore_RegularChatState.swift */, + 27BA023126E4ECD2008FC1A3 /* SyncCore_SynchronizePinnedChatsOperation.swift */, + 27BA023226E4ECD2008FC1A3 /* SyncCore_ForwardCountMessageAttribute.swift */, + 27BA023326E4ECD2008FC1A3 /* SyncCore_SuggestedLocalizationUpdatesOperation.swift */, + 27BA023426E4ECD2008FC1A3 /* SyncCore_SynchronizeInstalledStickerPacksOperations.swift */, + 27BA023526E4ECD2008FC1A3 /* SyncCore_ContentPrivacySettings.swift */, + 27BA023626E4ECD2008FC1A3 /* SyncCore_FeaturedStickerPack.swift */, + 27BA023726E4ECD2008FC1A3 /* SyncCore_TelegramMediaWebFile.swift */, + 27BA023826E4ECD2008FC1A3 /* SyncCore_OutgoingContentInfoMessageAttribute.swift */, + 27BA023926E4ECD2008FC1A3 /* SyncCore_SecretChatKeychain.swift */, + 27BA023A26E4ECD2008FC1A3 /* SyncCore_VoipConfiguration.swift */, + 27BA023B26E4ECD2008FC1A3 /* SyncCore_EmbeddedMediaStickersMessageAttribute.swift */, + 27BA023C26E4ECD2008FC1A3 /* SyncCore_TelegramMediaInvoice.swift */, + 27BA023D26E4ECD2008FC1A3 /* SyncCore_EmojiSearchQueryMessageAttribute.swift */, + 27BA023E26E4ECD2008FC1A3 /* SyncCore_CloudChatRemoveMessagesOperation.swift */, + 27BA023F26E4ECD2008FC1A3 /* SyncCore_PeerGroupMessageStateVersionAttribute.swift */, + 27BA024026E4ECD2008FC1A3 /* SyncCore_CacheStorageSettings.swift */, + 27BA024126E4ECD2008FC1A3 /* SyncCore_SecretChatIncomingEncryptedOperation.swift */, + 27BA024226E4ECD2008FC1A3 /* SyncCore_SecretChatSettings.swift */, + 27BA024326E4ECD2008FC1A3 /* SyncCore_ConsumablePersonalMentionMessageAttribute.swift */, + 27BA024426E4ECD2008FC1A3 /* SyncCore_SynchronizeConsumeMessageContentsOperation.swift */, + 27BA024526E4ECD2008FC1A3 /* SyncCore_CachedWallpapersConfiguration.swift */, + 27BA024626E4ECD2008FC1A3 /* SyncCore_SynchronizeableChatInputState.swift */, + 27BA024726E4ECD2008FC1A3 /* SyncCore_ContactsSettings.swift */, + 27BA024826E4ECD2008FC1A3 /* SyncCore_SynchronizeGroupedPeersOperation.swift */, + 27BA024926E4ECD2008FC1A3 /* SyncCore_StandaloneAccountTransaction.swift */, + 27BA024A26E4ECD2008FC1A3 /* SyncCore_TelegramChannel.swift */, + ); + path = SyncCore; + sourceTree = ""; + }; + 27BA024B26E4ECD2008FC1A3 /* Account */ = { + isa = PBXGroup; + children = ( + 27BA024C26E4ECD2008FC1A3 /* AccountManager.swift */, + 27BA024D26E4ECD2008FC1A3 /* AccountIntermediateState.swift */, + 27BA024E26E4ECD2008FC1A3 /* Account.swift */, + ); + path = Account; + sourceTree = ""; + }; + 27BA025026E4ECD2008FC1A3 /* SecretChats */ = { + isa = PBXGroup; + children = ( + 27BA025126E4ECD2008FC1A3 /* SecretChatRekeySession.swift */, + 27BA025226E4ECD2008FC1A3 /* SecretChatOutgoingOperation.swift */, + 27BA025326E4ECD2008FC1A3 /* SecretChatEncryption.swift */, + 27BA025426E4ECD2008FC1A3 /* SecretChatEncryptionConfig.swift */, + 27BA025526E4ECD2008FC1A3 /* SecretChatLayerNegotiation.swift */, + 27BA025626E4ECD2008FC1A3 /* SecretChatFileReference.swift */, + 27BA025726E4ECD2008FC1A3 /* SecretChatIncomingEncryptedOperation.swift */, + 27BA025826E4ECD2008FC1A3 /* UpdateSecretChat.swift */, + 27BA025926E4ECD2008FC1A3 /* SetSecretChatMessageAutoremoveTimeoutInteractively.swift */, + ); + path = SecretChats; + sourceTree = ""; + }; + D06706631D512ADA00DED3E3 /* Frameworks */ = { + isa = PBXGroup; + children = ( + A76D02EE25C4048500FCE1B5 /* LegacyReachability.framework */, + A7919037240CF9D1002011CA /* NetworkLogging.framework */, + A7919035240CF9CC002011CA /* CryptoUtils.framework */, + A7919033240CF9C7002011CA /* Reachability.framework */, + A7D281EB236C2FAD0000A9BF /* SyncCore.framework */, + A7D281E9236C27030000A9BF /* MtProtoKit.framework */, + A7D281E3236C257C0000A9BF /* TelegramApi.framework */, + A7D281E1236C25710000A9BF /* Postbox.framework */, + A7D281DF236C25580000A9BF /* libphonenumber.framework */, + A7D281DD236C25500000A9BF /* SwiftSignalKit.framework */, + A7D281DB236C25440000A9BF /* MtProtoKitMac.framework */, + A7D281D9236C253C0000A9BF /* TelegramApi.framework */, + A7D281D7236C25370000A9BF /* libphonenumber.framework */, + D0208AF32306E92B00A23503 /* libphonenumbermac.framework */, + D03E45D32305D44A0049C28B /* libphonenumber.framework */, + D03E45D02305D34C0049C28B /* libphonenumber_iOS.framework */, + D0CC4ADB22BA47280088F36D /* TelegramApiMac.framework */, + D0CC4AA322BA44960088F36D /* TelegramApi.framework */, + D035732E22B5C24F00F0920D /* TelegramApi.framework */, + D0AF32371FAE8C910097362B /* MultipeerConnectivity.framework */, + D0B4187E1D7E054E004562A4 /* MtProtoKitMac.framework */, + D0B418701D7E0409004562A4 /* PostboxMac.framework */, + D0B418711D7E0409004562A4 /* SwiftSignalKitMac.framework */, + D0CAF2E91D75EC600011F558 /* MtProtoKitDynamic.framework */, + D0AC49491D7097A400AA55DA /* SSignalKit.framework */, + D03B0E6B1D63283C00955575 /* libiconv.tbd */, + D03B0E691D63283000955575 /* libwebp.a */, + D03B0E611D63281A00955575 /* libavcodec.a */, + D03B0E621D63281A00955575 /* libavformat.a */, + D03B0E631D63281A00955575 /* libavutil.a */, + D03B0E641D63281A00955575 /* libswresample.a */, + D03B0E5F1D6327FF00955575 /* libz.tbd */, + D03B0E5D1D6327F600955575 /* SSignalKit.framework */, + D03B0E571D631EB900955575 /* CoreMedia.framework */, + D067066E1D512AEB00DED3E3 /* MtProtoKit.framework */, + D06706641D512ADB00DED3E3 /* AsyncDisplayKit.framework */, + D06706651D512ADB00DED3E3 /* Display.framework */, + D06706671D512ADB00DED3E3 /* Postbox.framework */, + D06706681D512ADB00DED3E3 /* SwiftSignalKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + D09D8BF71D4FAB1D0081DBEC = { + isa = PBXGroup; + children = ( + D0B418681D7E03D5004562A4 /* TelegramCore */, + D09D8C021D4FAB1D0081DBEC /* Products */, + D06706631D512ADA00DED3E3 /* Frameworks */, + ); + sourceTree = ""; + }; + D09D8C021D4FAB1D0081DBEC /* Products */ = { + isa = PBXGroup; + children = ( + D0B418671D7E03D5004562A4 /* TelegramCore.framework */, + ); + name = Products; + sourceTree = ""; + }; + D0B418681D7E03D5004562A4 /* TelegramCore */ = { + isa = PBXGroup; + children = ( + 27BA004026E4ECD1008FC1A3 /* Sources */, + 279881BD260DCA7400C2AF8E /* TelegramCore.xcconfig */, + D0B418691D7E03D5004562A4 /* TelegramCore.h */, + D0B4186A1D7E03D5004562A4 /* Info.plist */, + ); + path = TelegramCore; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + D0B418641D7E03D5004562A4 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D0B4186B1D7E03D5004562A4 /* TelegramCore.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + D0B418661D7E03D5004562A4 /* TelegramCore */ = { + isa = PBXNativeTarget; + buildConfigurationList = D0B4186C1D7E03D5004562A4 /* Build configuration list for PBXNativeTarget "TelegramCore" */; + buildPhases = ( + D0B418621D7E03D5004562A4 /* Sources */, + D0B418631D7E03D5004562A4 /* Frameworks */, + D0B418641D7E03D5004562A4 /* Headers */, + D0B418651D7E03D5004562A4 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = TelegramCore; + productName = TelegramCoreMac; + productReference = D0B418671D7E03D5004562A4 /* TelegramCore.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D09D8BF81D4FAB1D0081DBEC /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0800; + ORGANIZATIONNAME = Peter; + TargetAttributes = { + D0B418661D7E03D5004562A4 = { + CreatedOnToolsVersion = 8.0; + LastSwiftMigration = 1030; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = D09D8BFB1D4FAB1D0081DBEC /* Build configuration list for PBXProject "TelegramCore_Xcode" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + English, + en, + ); + mainGroup = D09D8BF71D4FAB1D0081DBEC; + productRefGroup = D09D8C021D4FAB1D0081DBEC /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D0B418661D7E03D5004562A4 /* TelegramCore */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D0B418651D7E03D5004562A4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 279881BE260DCA7400C2AF8E /* TelegramCore.xcconfig in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D0B418621D7E03D5004562A4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 27BA033226E4ECD2008FC1A3 /* RecentWebSessions.swift in Sources */, + 27BA03A326E4ECD2008FC1A3 /* TelegramChannelBannedRights.swift in Sources */, + 27BA035B26E4ECD2008FC1A3 /* SearchGroupMembers.swift in Sources */, + 27BA02B726E4ECD2008FC1A3 /* ManagedSynchronizeGroupMessageStats.swift in Sources */, + 27BA034026E4ECD2008FC1A3 /* UpdateContactName.swift in Sources */, + 27BA03C126E4ECD2008FC1A3 /* SyncCore_SecureFileMediaResource.swift in Sources */, + 27BA03B526E4ECD2008FC1A3 /* SyncCore_wallapersState.swift in Sources */, + 27BA037326E4ECD2008FC1A3 /* OpaqueChatState.swift in Sources */, + 27BA040526E4ECD2008FC1A3 /* SyncCore_WalletCollection.swift in Sources */, + 27BA031326E4ECD2008FC1A3 /* ChatList.swift in Sources */, + 27BA03E326E4ECD2008FC1A3 /* SyncCore_ChannelMessageStateVersionAttribute.swift in Sources */, + 27BA027F26E4ECD2008FC1A3 /* UpdatePeers.swift in Sources */, + 27BA029726E4ECD2008FC1A3 /* SynchronizeMarkAllUnseenPersonalMessagesOperation.swift in Sources */, + 27BA034726E4ECD2008FC1A3 /* PeersNearby.swift in Sources */, + 27BA037626E4ECD2008FC1A3 /* Peer.swift in Sources */, + 27BA029F26E4ECD2008FC1A3 /* ManagedConsumePersonalMessagesActions.swift in Sources */, + 27BA041D26E4ECD2008FC1A3 /* SyncCore_RecentHashtagItem.swift in Sources */, + 27BA03F426E4ECD2008FC1A3 /* SyncCore_NetworkSettings.swift in Sources */, + 27BA029D26E4ECD2008FC1A3 /* SynchronizeRecentlyUsedMediaOperations.swift in Sources */, + 27BA02B626E4ECD2008FC1A3 /* UpdateGroup.swift in Sources */, + 27BA039526E4ECD2008FC1A3 /* TelegramMediaWebpage.swift in Sources */, + 27BA039126E4ECD2008FC1A3 /* CachedChannelParticipants.swift in Sources */, + 27BA02F426E4ECD2008FC1A3 /* GrantSecureIdAccess.swift in Sources */, + 27BA035026E4ECD2008FC1A3 /* SupportPeerId.swift in Sources */, + 27BA042126E4ECD2008FC1A3 /* SyncCore_PeerAccessRestrictionInfo.swift in Sources */, + 27BA03C726E4ECD2008FC1A3 /* SyncCore_Localization.swift in Sources */, + 27BA02F926E4ECD2008FC1A3 /* SecureFileMediaResource.swift in Sources */, + 27BA031126E4ECD2008FC1A3 /* Polls.swift in Sources */, + 27BA03E426E4ECD2008FC1A3 /* SyncCore_ThemeSettings.swift in Sources */, + 27BA03B426E4ECD2008FC1A3 /* SyncCore_AutodownloadSettings.swift in Sources */, + 27BA043526E4ECD2008FC1A3 /* SyncCore_TelegramMediaWebFile.swift in Sources */, + 27BA027626E4ECD2008FC1A3 /* NetworkType.swift in Sources */, + 27BA031A26E4ECD2008FC1A3 /* UpdatePinnedMessage.swift in Sources */, + 27BA028526E4ECD2008FC1A3 /* Log.swift in Sources */, + 27BA02E726E4ECD2008FC1A3 /* SecureIdAddressValue.swift in Sources */, + 27BA044B26E4ECD2008FC1A3 /* Account.swift in Sources */, + 27BA030826E4ECD2008FC1A3 /* LoadedStickerPack.swift in Sources */, + 27BA02D826E4ECD2008FC1A3 /* ManagedConfigurationUpdates.swift in Sources */, + 27BA03DF26E4ECD2008FC1A3 /* SyncCore_ForwardHideSendersNamesMessageAttribute.swift in Sources */, + 27BA029826E4ECD2008FC1A3 /* ManagedSynchronizeEmojiKeywordsOperations.swift in Sources */, + 27BA03F726E4ECD2008FC1A3 /* SyncCore_NotificationInfoMessageAttribute.swift in Sources */, + 27BA03B726E4ECD2008FC1A3 /* SyncCore_TelegramUser.swift in Sources */, + 27BA040726E4ECD2008FC1A3 /* SyncCore_WasScheduledMessageAttribute.swift in Sources */, + 27BA041626E4ECD2008FC1A3 /* SyncCore_AuthorizedAccountState.swift in Sources */, + 27BA035826E4ECD2008FC1A3 /* ChannelAdminEventLogContext.swift in Sources */, + 27BA044226E4ECD2008FC1A3 /* SyncCore_SynchronizeConsumeMessageContentsOperation.swift in Sources */, + 27BA036026E4ECD2008FC1A3 /* InactiveChannels.swift in Sources */, + 27BA037F26E4ECD2008FC1A3 /* TelegramEngineAccountData.swift in Sources */, + 27BA03D226E4ECD2008FC1A3 /* SyncCore_CachedResolvedByNamePeer.swift in Sources */, + 27BA032E26E4ECD2008FC1A3 /* UpdatedAccountPrivacySettings.swift in Sources */, + 27BA03AA26E4ECD2008FC1A3 /* InstantPage.swift in Sources */, + 27BA039D26E4ECD2008FC1A3 /* ChatContextResult.swift in Sources */, + 27BA034E26E4ECD2008FC1A3 /* RequestUserPhotos.swift in Sources */, + 27BA02AC26E4ECD2008FC1A3 /* ManagedSynchronizeMarkAllUnseenPersonalMessagesOperations.swift in Sources */, + 27BA02BD26E4ECD2008FC1A3 /* SynchronizeInstalledStickerPacksOperation.swift in Sources */, + 27BA026426E4ECD2008FC1A3 /* VoipConfiguration.swift in Sources */, + 27BA042426E4ECD2008FC1A3 /* SyncCore_SynchronizeRecentlyUsedMediaOperation.swift in Sources */, + 27BA034426E4ECD2008FC1A3 /* TelegramEngineResources.swift in Sources */, + 27BA026626E4ECD2008FC1A3 /* GlobalNotificationSettings.swift in Sources */, + 27BA043026E4ECD2008FC1A3 /* SyncCore_ForwardCountMessageAttribute.swift in Sources */, + 27BA035726E4ECD2008FC1A3 /* NotificationExceptionsList.swift in Sources */, + 27BA038E26E4ECD2008FC1A3 /* CloudMediaResourceParameters.swift in Sources */, + 27BA026D26E4ECD2008FC1A3 /* ChatUpdatingMessageMedia.swift in Sources */, + 27BA042226E4ECD2008FC1A3 /* SyncCore_OutgoingChatContextResultMessageAttribute.swift in Sources */, + 27BA039826E4ECD2008FC1A3 /* Wallpaper.swift in Sources */, + 27BA044D26E4ECD2008FC1A3 /* SecretChatRekeySession.swift in Sources */, + 27BA03EF26E4ECD2008FC1A3 /* SyncCore_OutgoingMessageInfoAttribute.swift in Sources */, + 27BA031D26E4ECD2008FC1A3 /* ApplyMaxReadIndexInteractively.swift in Sources */, + 27BA042E26E4ECD2008FC1A3 /* SyncCore_RegularChatState.swift in Sources */, + 27BA02A326E4ECD2008FC1A3 /* CachedSentMediaReferences.swift in Sources */, + 27BA034F26E4ECD2008FC1A3 /* ChatListFiltering.swift in Sources */, + 27BA02E926E4ECD2008FC1A3 /* SecureIdUtilityBillValue.swift in Sources */, + 27BA032A26E4ECD2008FC1A3 /* ReadState.swift in Sources */, + 27BA043B26E4ECD2008FC1A3 /* SyncCore_EmojiSearchQueryMessageAttribute.swift in Sources */, + 27BA02C226E4ECD2008FC1A3 /* ManagedSecretChatOutgoingOperations.swift in Sources */, + 27BA034C26E4ECD2008FC1A3 /* ChangePeerNotificationSettings.swift in Sources */, + 27BA02AD26E4ECD2008FC1A3 /* ManagedServiceViews.swift in Sources */, + 27BA02DB26E4ECD2008FC1A3 /* AccountStateManagementUtils.swift in Sources */, + 27BA02D326E4ECD2008FC1A3 /* ManagedSynchronizeChatInputStateOperations.swift in Sources */, + 27BA03ED26E4ECD2008FC1A3 /* SyncCore_TelegramGroup.swift in Sources */, + 27BA041A26E4ECD2008FC1A3 /* SyncCore_CachedGroupParticipants.swift in Sources */, + 27BA029626E4ECD2008FC1A3 /* SynchronizeAppLogEventsOperation.swift in Sources */, + 27BA036B26E4ECD2008FC1A3 /* ChannelCreation.swift in Sources */, + 27BA030D26E4ECD2008FC1A3 /* StickerSetInstallation.swift in Sources */, + 27BA043D26E4ECD2008FC1A3 /* SyncCore_PeerGroupMessageStateVersionAttribute.swift in Sources */, + 27BA033C26E4ECD2008FC1A3 /* PhoneNumber.swift in Sources */, + 27BA037826E4ECD2008FC1A3 /* RemovePeerMember.swift in Sources */, + 27BA033626E4ECD2008FC1A3 /* DeepLinkInfo.swift in Sources */, + 27BA029526E4ECD2008FC1A3 /* AppConfiguration.swift in Sources */, + 27BA026E26E4ECD2008FC1A3 /* EnqueueMessage.swift in Sources */, + 27BA032C26E4ECD2008FC1A3 /* ExportMessageLink.swift in Sources */, + 27BA031726E4ECD2008FC1A3 /* CallList.swift in Sources */, + 27BA02B926E4ECD2008FC1A3 /* ManagedSynchronizeMarkFeaturedStickerPacksAsSeenOperations.swift in Sources */, + 27BA026926E4ECD2008FC1A3 /* NotificationAutolockReportManager.swift in Sources */, + 27BA02BB26E4ECD2008FC1A3 /* ManagedAnimatedEmojiUpdates.swift in Sources */, + 27BA035F26E4ECD2008FC1A3 /* ChannelBlacklist.swift in Sources */, + 27BA02D926E4ECD2008FC1A3 /* ManagedVoipConfigurationUpdates.swift in Sources */, + 27BA034D26E4ECD2008FC1A3 /* ManageChannelDiscussionGroup.swift in Sources */, + 27BA031426E4ECD2008FC1A3 /* LoadMessagesIfNecessary.swift in Sources */, + 27BA033F26E4ECD2008FC1A3 /* ImportContact.swift in Sources */, + 27BA042626E4ECD2008FC1A3 /* SyncCore_AccountEnvironmentAttribute.swift in Sources */, + 27BA02F726E4ECD2008FC1A3 /* SecureIdBankStatementValue.swift in Sources */, + 27BA03D126E4ECD2008FC1A3 /* SyncCore_ConsumableContentMessageAttribute.swift in Sources */, + 27BA03C426E4ECD2008FC1A3 /* SyncCore_SearchBotsConfiguration.swift in Sources */, + 27BA03AB26E4ECD2008FC1A3 /* CloudFileMediaResource.swift in Sources */, + 27BA028726E4ECD2008FC1A3 /* DecryptedResourceData.swift in Sources */, + 27BA043426E4ECD2008FC1A3 /* SyncCore_FeaturedStickerPack.swift in Sources */, + 27BA036E26E4ECD2008FC1A3 /* InvitationLinks.swift in Sources */, + 27BA034326E4ECD2008FC1A3 /* CollectCacheUsageStats.swift in Sources */, + 27BA02B526E4ECD2008FC1A3 /* InitializeAccountAfterLogin.swift in Sources */, + 27BA032726E4ECD2008FC1A3 /* DeleteMessagesInteractively.swift in Sources */, + 27BA044826E4ECD2008FC1A3 /* SyncCore_TelegramChannel.swift in Sources */, + 27BA042026E4ECD2008FC1A3 /* SyncCore_ReplyThreadMessageAttribute.swift in Sources */, + 27BA03A526E4ECD2008FC1A3 /* StoreMessage_Telegram.swift in Sources */, + 27BA025D26E4ECD2008FC1A3 /* LoggingSettings.swift in Sources */, + 27BA03CC26E4ECD2008FC1A3 /* SyncCore_TelegramMediaGame.swift in Sources */, + 27BA039226E4ECD2008FC1A3 /* CachedGroupParticipants.swift in Sources */, + 27BA042826E4ECD2008FC1A3 /* SyncCore_StickerPackItem.swift in Sources */, + 27BA02F526E4ECD2008FC1A3 /* SecureIdForm.swift in Sources */, + 27BA028A26E4ECD2008FC1A3 /* UpdateMessageMedia.swift in Sources */, + 27BA030C26E4ECD2008FC1A3 /* CachedStickerPack.swift in Sources */, + 27BA036826E4ECD2008FC1A3 /* CreateGroup.swift in Sources */, + 27BA044726E4ECD2008FC1A3 /* SyncCore_StandaloneAccountTransaction.swift in Sources */, + 27BA044C26E4ECD2008FC1A3 /* WebpagePreview.swift in Sources */, + 27BA03B226E4ECD2008FC1A3 /* ReplyMarkupMessageAttribute.swift in Sources */, + 27BA036D26E4ECD2008FC1A3 /* PeerSpecificStickerPack.swift in Sources */, + 27BA036126E4ECD2008FC1A3 /* PeerPhotoUpdater.swift in Sources */, + 27BA02D426E4ECD2008FC1A3 /* AppChangelogState.swift in Sources */, + 27BA033826E4ECD2008FC1A3 /* CancelAccountReset.swift in Sources */, + 27BA043126E4ECD2008FC1A3 /* SyncCore_SuggestedLocalizationUpdatesOperation.swift in Sources */, + 27BA02C326E4ECD2008FC1A3 /* ManagedSynchronizeConsumeMessageContentsOperations.swift in Sources */, + 27BA03D726E4ECD2008FC1A3 /* SyncCore_CachedChannelData.swift in Sources */, + 27BA03C926E4ECD2008FC1A3 /* SyncCore_TelegramChatBannedRights.swift in Sources */, + 27BA03B326E4ECD2008FC1A3 /* SyncCore_TelegramMediaAction.swift in Sources */, + 27BA03BE26E4ECD2008FC1A3 /* SyncCore_SecretChatFileReference.swift in Sources */, + 27BA03B926E4ECD2008FC1A3 /* SyncCore_PeerAccessHash.swift in Sources */, + 27BA02E826E4ECD2008FC1A3 /* SecureIdEmailValue.swift in Sources */, + 27BA040E26E4ECD2008FC1A3 /* SyncCore_TelegramMediaMap.swift in Sources */, + 27BA032426E4ECD2008FC1A3 /* DeleteMessages.swift in Sources */, + 27BA033A26E4ECD2008FC1A3 /* AuthTransfer.swift in Sources */, + 27BA032226E4ECD2008FC1A3 /* MessageReadStats.swift in Sources */, + 27BA040426E4ECD2008FC1A3 /* SyncCore_ReplyMessageAttribute.swift in Sources */, + 27BA044626E4ECD2008FC1A3 /* SyncCore_SynchronizeGroupedPeersOperation.swift in Sources */, + 27BA027326E4ECD2008FC1A3 /* MessageStatistics.swift in Sources */, + 27BA031B26E4ECD2008FC1A3 /* EarliestUnseenPersonalMentionMessage.swift in Sources */, + 27BA03E626E4ECD2008FC1A3 /* SyncCore_PixelDimensions.swift in Sources */, + 27BA02CC26E4ECD2008FC1A3 /* UnauthorizedAccountStateManager.swift in Sources */, + 27BA03CE26E4ECD2008FC1A3 /* SyncCore_StickerPackCollectionInfo.swift in Sources */, + 27BA044426E4ECD2008FC1A3 /* SyncCore_SynchronizeableChatInputState.swift in Sources */, + 27BA031F26E4ECD2008FC1A3 /* PeerLiveLocationsContext.swift in Sources */, + 27BA03A826E4ECD2008FC1A3 /* ApiUtils.swift in Sources */, + 27BA027826E4ECD2008FC1A3 /* Network.swift in Sources */, + 27BA02A526E4ECD2008FC1A3 /* FetchChatList.swift in Sources */, + 27BA026B26E4ECD2008FC1A3 /* GroupReturnAndLeft.swift in Sources */, + 27BA03A626E4ECD2008FC1A3 /* Theme.swift in Sources */, + 27BA02CB26E4ECD2008FC1A3 /* ProcessSecretChatIncomingEncryptedOperations.swift in Sources */, + 27BA03AF26E4ECD2008FC1A3 /* TelegramUserPresence.swift in Sources */, + 27BA027126E4ECD2008FC1A3 /* StandaloneUploadedMedia.swift in Sources */, + 27BA02A226E4ECD2008FC1A3 /* AccountStateManager.swift in Sources */, + 27BA03C626E4ECD2008FC1A3 /* SyncCore_RecentPeerItem.swift in Sources */, + 27BA029326E4ECD2008FC1A3 /* ApplyUpdateMessage.swift in Sources */, + 27BA033726E4ECD2008FC1A3 /* TelegramEngineAuth.swift in Sources */, + 27BA02E126E4ECD2008FC1A3 /* TelegramEngineCalls.swift in Sources */, + 27BA02B826E4ECD2008FC1A3 /* Fetch.swift in Sources */, + 27BA038A26E4ECD2008FC1A3 /* LocalizationPreview.swift in Sources */, + 27BA03AC26E4ECD2008FC1A3 /* ApiGroupOrChannel.swift in Sources */, + 27BA02BE26E4ECD2008FC1A3 /* ManagedLocalInputActivities.swift in Sources */, + 27BA038F26E4ECD2008FC1A3 /* TelegramMediaWebDocument.swift in Sources */, + 27BA02F026E4ECD2008FC1A3 /* SecureIdPassportRegistrationValue.swift in Sources */, + 27BA03E826E4ECD2008FC1A3 /* SyncCore_LocalizationSettings.swift in Sources */, + 27BA034826E4ECD2008FC1A3 /* TelegramEnginePeersNearby.swift in Sources */, + 27BA027926E4ECD2008FC1A3 /* MultiplexedRequestManager.swift in Sources */, + 27BA038726E4ECD2008FC1A3 /* LocalizationInfo.swift in Sources */, + 27BA042D26E4ECD2008FC1A3 /* SyncCore_EmojiKeywordCollectionInfo.swift in Sources */, + 27BA030726E4ECD2008FC1A3 /* TelegramEngineStickers.swift in Sources */, + 27BA038D26E4ECD2008FC1A3 /* TelegramUser.swift in Sources */, + 27BA040226E4ECD2008FC1A3 /* SyncCore_SourceReferenceMessageAttribute.swift in Sources */, + 27BA037726E4ECD2008FC1A3 /* ChannelParticipants.swift in Sources */, + 27BA028B26E4ECD2008FC1A3 /* MemoryBufferExtensions.swift in Sources */, + 27BA029A26E4ECD2008FC1A3 /* ManagedNotificationSettingsBehaviors.swift in Sources */, + 27BA034B26E4ECD2008FC1A3 /* ChannelOwnershipTransfer.swift in Sources */, + 27BA03A726E4ECD2008FC1A3 /* PeerGeoLocation.swift in Sources */, + 27BA040A26E4ECD2008FC1A3 /* SyncCore_AppChangelogState.swift in Sources */, + 27BA028E26E4ECD2008FC1A3 /* ManagedSynchronizeSavedStickersOperations.swift in Sources */, + 27BA042526E4ECD2008FC1A3 /* SyncCore_CachedLocalizationInfos.swift in Sources */, + 27BA03AD26E4ECD2008FC1A3 /* TelegramChannel.swift in Sources */, + 27BA03D926E4ECD2008FC1A3 /* SyncCore_OutgoingScheduleInfoMessageAttribute.swift in Sources */, + 27BA028926E4ECD2008FC1A3 /* JSON.swift in Sources */, + 27BA041B26E4ECD2008FC1A3 /* SyncCore_CachedStickerPack.swift in Sources */, + 27BA030426E4ECD2008FC1A3 /* ImportStickers.swift in Sources */, + 27BA03F026E4ECD2008FC1A3 /* SyncCore_UnauthorizedAccountState.swift in Sources */, + 27BA03D326E4ECD2008FC1A3 /* SyncCore_TelegramMediaPoll.swift in Sources */, + 27BA031E26E4ECD2008FC1A3 /* Media.swift in Sources */, + 27BA043326E4ECD2008FC1A3 /* SyncCore_ContentPrivacySettings.swift in Sources */, + 27BA039726E4ECD2008FC1A3 /* TelegramMediaGame.swift in Sources */, + 27BA036C26E4ECD2008FC1A3 /* AddressNames.swift in Sources */, + 27BA026826E4ECD2008FC1A3 /* PeerStatistics.swift in Sources */, + 27BA041426E4ECD2008FC1A3 /* SyncCore_TelegramMediaWebpage.swift in Sources */, + 27BA028026E4ECD2008FC1A3 /* SplitTest.swift in Sources */, + 27BA042326E4ECD2008FC1A3 /* SyncCore_CachedUserData.swift in Sources */, + 27BA028826E4ECD2008FC1A3 /* MD5.swift in Sources */, + 27BA03FE26E4ECD2008FC1A3 /* SyncCore_SynchronizeMarkAllUnseenPersonalMessagesOperation.swift in Sources */, + 27BA02F626E4ECD2008FC1A3 /* SecureIdVerificationDocumentReference.swift in Sources */, + 27BA032326E4ECD2008FC1A3 /* ScheduledMessages.swift in Sources */, + 27BA025F26E4ECD2008FC1A3 /* ProxySettings.swift in Sources */, + 27BA02F126E4ECD2008FC1A3 /* VerifySecureIdValue.swift in Sources */, + 27BA028326E4ECD2008FC1A3 /* MediaResourceNetworkStatsTag.swift in Sources */, + 27BA02E626E4ECD2008FC1A3 /* RequestSecureIdForm.swift in Sources */, + 27BA03E926E4ECD2008FC1A3 /* SyncCore_SendScheduledMessageImmediatelyAction.swift in Sources */, + 27BA040626E4ECD2008FC1A3 /* SyncCore_EmojiKeywordItem.swift in Sources */, + 27BA029126E4ECD2008FC1A3 /* HistoryViewStateValidation.swift in Sources */, + 27BA038C26E4ECD2008FC1A3 /* TelegramPeerNotificationSettings.swift in Sources */, + 27BA02BA26E4ECD2008FC1A3 /* SynchronizeLocalizationUpdatesOperation.swift in Sources */, + 27BA043C26E4ECD2008FC1A3 /* SyncCore_CloudChatRemoveMessagesOperation.swift in Sources */, + 27BA03F826E4ECD2008FC1A3 /* SyncCore_InstantPage.swift in Sources */, + 27BA02FD26E4ECD2008FC1A3 /* SecureIdInternalPassportValue.swift in Sources */, + 27BA034926E4ECD2008FC1A3 /* TelegramEngine.swift in Sources */, + 27BA031826E4ECD2008FC1A3 /* Message.swift in Sources */, + 27BA030626E4ECD2008FC1A3 /* SearchStickers.swift in Sources */, + 27BA043626E4ECD2008FC1A3 /* SyncCore_OutgoingContentInfoMessageAttribute.swift in Sources */, + 27BA02AE26E4ECD2008FC1A3 /* PeerInputActivityManager.swift in Sources */, + 27BA039C26E4ECD2008FC1A3 /* ReactionsMessageAttribute.swift in Sources */, + 27BA02EA26E4ECD2008FC1A3 /* TelegramEngineSecureId.swift in Sources */, + 27BA042B26E4ECD2008FC1A3 /* SyncCore_RecentMediaItem.swift in Sources */, + 27BA034526E4ECD2008FC1A3 /* TelegramEngineHistoryImport.swift in Sources */, + 27BA041F26E4ECD2008FC1A3 /* SyncCore_ValidationMessageAttribute.swift in Sources */, + 27BA031526E4ECD2008FC1A3 /* RequestStartBot.swift in Sources */, + 27BA037B26E4ECD2008FC1A3 /* PeerSummary.swift in Sources */, + 27BA02C626E4ECD2008FC1A3 /* ManagedAutoremoveMessageOperations.swift in Sources */, + 27BA02DA26E4ECD2008FC1A3 /* AppChangelog.swift in Sources */, + 27BA036F26E4ECD2008FC1A3 /* ResolvePeerByName.swift in Sources */, + 27BA037226E4ECD2008FC1A3 /* UpdatePeerInfo.swift in Sources */, + 27BA044F26E4ECD2008FC1A3 /* SecretChatEncryption.swift in Sources */, + 27BA033926E4ECD2008FC1A3 /* ConfirmTwoStepRecoveryEmail.swift in Sources */, + 27BA03CA26E4ECD2008FC1A3 /* SyncCore_TelegramSecretChat.swift in Sources */, + 27BA026F26E4ECD2008FC1A3 /* PendingMessageUploadedContent.swift in Sources */, + 27BA02AB26E4ECD2008FC1A3 /* ManagedChatListHoles.swift in Sources */, + 27BA03DB26E4ECD2008FC1A3 /* SyncCore_TelegramTheme.swift in Sources */, + 27BA037926E4ECD2008FC1A3 /* RecentlySearchedPeerIds.swift in Sources */, + 27BA02D726E4ECD2008FC1A3 /* ManagedSynchronizeSavedGifsOperations.swift in Sources */, + 27BA02AA26E4ECD2008FC1A3 /* ManagedRecentStickers.swift in Sources */, + 27BA03F526E4ECD2008FC1A3 /* SyncCore_LocalizationInfo.swift in Sources */, + 27BA03E226E4ECD2008FC1A3 /* SyncCore_AuthorSignatureMessageAttribute.swift in Sources */, + 27BA027426E4ECD2008FC1A3 /* Suggestions.swift in Sources */, + 27BA043826E4ECD2008FC1A3 /* SyncCore_VoipConfiguration.swift in Sources */, + 27BA03D526E4ECD2008FC1A3 /* SyncCore_TelegramPeerNotificationSettings.swift in Sources */, + 27BA041126E4ECD2008FC1A3 /* SyncCore_TelegramMediaFile.swift in Sources */, + 27BA02DD26E4ECD2008FC1A3 /* Serialization.swift in Sources */, + 27BA039E26E4ECD2008FC1A3 /* ImageRepresentationWithReference.swift in Sources */, + 27BA03D826E4ECD2008FC1A3 /* SyncCore_AccountSortOrderAttribute.swift in Sources */, + 27BA026A26E4ECD2008FC1A3 /* MacInternalUpdater.swift in Sources */, + 27BA03B026E4ECD2008FC1A3 /* AdMessageAttribute.swift in Sources */, + 27BA033E26E4ECD2008FC1A3 /* ContactManagement.swift in Sources */, + 27BA045226E4ECD2008FC1A3 /* SecretChatFileReference.swift in Sources */, + 27BA03B126E4ECD2008FC1A3 /* TelegramMediaFile.swift in Sources */, + 27BA03CB26E4ECD2008FC1A3 /* SyncCore_CachedStickerQueryResult.swift in Sources */, + 27BA033526E4ECD2008FC1A3 /* TelegramEngineResolve.swift in Sources */, + 27BA03CD26E4ECD2008FC1A3 /* SyncCore_TelegramMediaContact.swift in Sources */, + 27BA02C526E4ECD2008FC1A3 /* SynchronizeSavedGifsOperation.swift in Sources */, + 27BA02FC26E4ECD2008FC1A3 /* SecureIdDataTypes.swift in Sources */, + 27BA025A26E4ECD2008FC1A3 /* Wallpapers.swift in Sources */, + 27BA03F626E4ECD2008FC1A3 /* SyncCore_RichText.swift in Sources */, + 27BA039326E4ECD2008FC1A3 /* TelegramChannelAdminRights.swift in Sources */, + 27BA03EE26E4ECD2008FC1A3 /* SyncCore_ChannelState.swift in Sources */, + 27BA02CF26E4ECD2008FC1A3 /* ManagedSynchronizePeerReadStates.swift in Sources */, + 27BA025C26E4ECD2008FC1A3 /* ContentSettings.swift in Sources */, + 27BA03C226E4ECD2008FC1A3 /* SyncCore_UpdateMessageReactionsAction.swift in Sources */, + 27BA038626E4ECD2008FC1A3 /* Countries.swift in Sources */, + 27BA042926E4ECD2008FC1A3 /* SyncCore_SynchronizeSavedGifsOperation.swift in Sources */, + 27BA041026E4ECD2008FC1A3 /* SyncCore_RemoteStorageConfiguration.swift in Sources */, + 27BA040126E4ECD2008FC1A3 /* SyncCore_ForwardSourceInfoAttribute.swift in Sources */, + 27BA030326E4ECD2008FC1A3 /* BankCards.swift in Sources */, + 27BA02C026E4ECD2008FC1A3 /* ManagedSynchronizeGroupedPeersOperations.swift in Sources */, + 27BA027C26E4ECD2008FC1A3 /* FetchedMediaResource.swift in Sources */, + 27BA035426E4ECD2008FC1A3 /* GroupsInCommon.swift in Sources */, + 27BA040C26E4ECD2008FC1A3 /* SyncCore_BotInfo.swift in Sources */, + 27BA028C26E4ECD2008FC1A3 /* CanSendMessagesToPeer.swift in Sources */, + 27BA02AF26E4ECD2008FC1A3 /* ManagedSynchronizePinnedChatsOperations.swift in Sources */, + 27BA02EF26E4ECD2008FC1A3 /* SecureIdIDCardValue.swift in Sources */, + 27BA02E526E4ECD2008FC1A3 /* SecureIdValue.swift in Sources */, + 27BA029C26E4ECD2008FC1A3 /* ManagedPendingPeerNotificationSettings.swift in Sources */, + 27BA035326E4ECD2008FC1A3 /* UpdateCachedPeerData.swift in Sources */, + 27BA03CF26E4ECD2008FC1A3 /* SyncCore_CachedThemesConfiguration.swift in Sources */, + 27BA02C726E4ECD2008FC1A3 /* AppUpdate.swift in Sources */, + 27BA032526E4ECD2008FC1A3 /* RequestMessageActionCallback.swift in Sources */, + 27BA040D26E4ECD2008FC1A3 /* SyncCore_LimitsConfiguration.swift in Sources */, + 27BA03B826E4ECD2008FC1A3 /* SyncCore_TelegramChatAdminRights.swift in Sources */, + 27BA02FE26E4ECD2008FC1A3 /* SecureIdPassportValue.swift in Sources */, + 27BA02C926E4ECD2008FC1A3 /* SynchronizeChatInputStateOperation.swift in Sources */, + 27BA033D26E4ECD2008FC1A3 /* TelegramEngineContacts.swift in Sources */, + 27BA02D026E4ECD2008FC1A3 /* SynchronizeConsumeMessageContentsOperation.swift in Sources */, + 27BA029426E4ECD2008FC1A3 /* ChatHistoryPreloadManager.swift in Sources */, + 27BA036226E4ECD2008FC1A3 /* ChatOnlineMembers.swift in Sources */, + 27BA033126E4ECD2008FC1A3 /* RecentAccountSessions.swift in Sources */, + 27BA042F26E4ECD2008FC1A3 /* SyncCore_SynchronizePinnedChatsOperation.swift in Sources */, + 27BA027026E4ECD2008FC1A3 /* PendingUpdateMessageManager.swift in Sources */, + 27BA031C26E4ECD2008FC1A3 /* ForwardGame.swift in Sources */, + 27BA037026E4ECD2008FC1A3 /* FindChannelById.swift in Sources */, + 27BA025E26E4ECD2008FC1A3 /* LimitsConfiguration.swift in Sources */, + 27BA036A26E4ECD2008FC1A3 /* TogglePeerChatPinned.swift in Sources */, + 27BA040926E4ECD2008FC1A3 /* SyncCore_ImportableDeviceContactData.swift in Sources */, + 27BA030026E4ECD2008FC1A3 /* SecureIdPhoneValue.swift in Sources */, + 27BA02B326E4ECD2008FC1A3 /* PendingMessageManager.swift in Sources */, + 27BA028626E4ECD2008FC1A3 /* PeerUtils.swift in Sources */, + 27BA02B126E4ECD2008FC1A3 /* MessageReactions.swift in Sources */, + 27BA027E26E4ECD2008FC1A3 /* FetchHttpResource.swift in Sources */, + 27BA038426E4ECD2008FC1A3 /* TelegramEngineLocalization.swift in Sources */, + 27BA043226E4ECD2008FC1A3 /* SyncCore_SynchronizeInstalledStickerPacksOperations.swift in Sources */, + 27BA041E26E4ECD2008FC1A3 /* SyncCore_CloudFileMediaResource.swift in Sources */, + 27BA027726E4ECD2008FC1A3 /* MultipartFetch.swift in Sources */, + 27BA032626E4ECD2008FC1A3 /* MarkAllChatsAsRead.swift in Sources */, + 27BA02CE26E4ECD2008FC1A3 /* ManagedAccountPresence.swift in Sources */, + 27BA038126E4ECD2008FC1A3 /* TermsOfService.swift in Sources */, + 27BA02A926E4ECD2008FC1A3 /* ManagedSynchronizeRecentlyUsedMediaOperations.swift in Sources */, + 27BA02A726E4ECD2008FC1A3 /* FetchSecretFileResource.swift in Sources */, + 27BA039B26E4ECD2008FC1A3 /* TelegramMediaImage.swift in Sources */, + 27BA039426E4ECD2008FC1A3 /* TextEntitiesMessageAttribute.swift in Sources */, + 27BA026326E4ECD2008FC1A3 /* ContentPrivacySettings.swift in Sources */, + 27BA035126E4ECD2008FC1A3 /* ChannelHistoryAvailabilitySettings.swift in Sources */, + 27BA038326E4ECD2008FC1A3 /* ChatThemes.swift in Sources */, + 27BA033326E4ECD2008FC1A3 /* RecentAccountSession.swift in Sources */, + 27BA03C826E4ECD2008FC1A3 /* SyncCore_GlobalNotificationSettings.swift in Sources */, + 27BA045326E4ECD2008FC1A3 /* SecretChatIncomingEncryptedOperation.swift in Sources */, + 27BA03C026E4ECD2008FC1A3 /* SyncCore_SynchronizeSavedStickersOperation.swift in Sources */, + 27BA043E26E4ECD2008FC1A3 /* SyncCore_CacheStorageSettings.swift in Sources */, + 27BA02A426E4ECD2008FC1A3 /* ManagedLocalizationUpdatesOperations.swift in Sources */, + 27BA042A26E4ECD2008FC1A3 /* SyncCore_AccountBackupDataAttribute.swift in Sources */, + 27BA03DD26E4ECD2008FC1A3 /* SyncCore_CachedGroupData.swift in Sources */, + 27BA030526E4ECD2008FC1A3 /* StickerPack.swift in Sources */, + 27BA02BC26E4ECD2008FC1A3 /* UpdateMessageService.swift in Sources */, + 27BA031026E4ECD2008FC1A3 /* TelegramEngineMessages.swift in Sources */, + 27BA030A26E4ECD2008FC1A3 /* EmojiKeywords.swift in Sources */, + 27BA02E226E4ECD2008FC1A3 /* GroupCalls.swift in Sources */, + 27BA035526E4ECD2008FC1A3 /* RecentPeers.swift in Sources */, + 27BA036526E4ECD2008FC1A3 /* ToggleChannelSignatures.swift in Sources */, + 27BA037426E4ECD2008FC1A3 /* TelegramEnginePeers.swift in Sources */, + 27BA036726E4ECD2008FC1A3 /* CreateSecretChat.swift in Sources */, + 27BA03B626E4ECD2008FC1A3 /* SyncCore_SynchronizeChatInputStateOperation.swift in Sources */, + 27BA03FA26E4ECD2008FC1A3 /* SyncCore_TelegramUserPresence.swift in Sources */, + 27BA035926E4ECD2008FC1A3 /* ChannelAdminEventLogs.swift in Sources */, + 27BA037D26E4ECD2008FC1A3 /* RegisterNotificationToken.swift in Sources */, + 27BA039926E4ECD2008FC1A3 /* TelegramMediaPoll.swift in Sources */, + 27BA02BF26E4ECD2008FC1A3 /* ManagedSynchronizeInstalledStickerPacksOperations.swift in Sources */, + 27BA043F26E4ECD2008FC1A3 /* SyncCore_SecretChatIncomingEncryptedOperation.swift in Sources */, + 27BA041926E4ECD2008FC1A3 /* SyncCore_SecretChatOutgoingOperation.swift in Sources */, + 27BA02F326E4ECD2008FC1A3 /* SecureIdRentalAgreementValue.swift in Sources */, + 27BA03E126E4ECD2008FC1A3 /* SyncCore_SavedStickerItem.swift in Sources */, + 27BA028226E4ECD2008FC1A3 /* Coding.swift in Sources */, + 27BA02CA26E4ECD2008FC1A3 /* SynchronizeGroupedPeersOperation.swift in Sources */, + 27BA02F226E4ECD2008FC1A3 /* SecureIdDriversLicenseValue.swift in Sources */, + 27BA037A26E4ECD2008FC1A3 /* TelegramEngineData.swift in Sources */, + 27BA045526E4ECD2008FC1A3 /* SetSecretChatMessageAutoremoveTimeoutInteractively.swift in Sources */, + 27BA03A226E4ECD2008FC1A3 /* PeerAccessRestrictionInfo.swift in Sources */, + 27BA025B26E4ECD2008FC1A3 /* Authorization.swift in Sources */, + 27BA042726E4ECD2008FC1A3 /* SyncCore_PeerStatusSettings.swift in Sources */, + 27BA032126E4ECD2008FC1A3 /* EngineGroupCallDescription.swift in Sources */, + 27BA044326E4ECD2008FC1A3 /* SyncCore_CachedWallpapersConfiguration.swift in Sources */, + 27BA044026E4ECD2008FC1A3 /* SyncCore_SecretChatSettings.swift in Sources */, + 27BA040826E4ECD2008FC1A3 /* SyncCore_MediaReference.swift in Sources */, + 27BA03D626E4ECD2008FC1A3 /* SyncCore_TelegramMediaUnsupported.swift in Sources */, + 27BA027A26E4ECD2008FC1A3 /* Download.swift in Sources */, + 27BA040F26E4ECD2008FC1A3 /* SyncCore_TelegramMediaResource.swift in Sources */, + 27BA02D626E4ECD2008FC1A3 /* SynchronizePeerReadState.swift in Sources */, + 27BA02EC26E4ECD2008FC1A3 /* SecureIdPadding.swift in Sources */, + 27BA035A26E4ECD2008FC1A3 /* CheckPeerChatServiceActions.swift in Sources */, + 27BA027B26E4ECD2008FC1A3 /* MultipartUpload.swift in Sources */, + 27BA02E326E4ECD2008FC1A3 /* RateCall.swift in Sources */, + 27BA03A126E4ECD2008FC1A3 /* EncryptedMediaResource.swift in Sources */, + 27BA038926E4ECD2008FC1A3 /* LocalizationListState.swift in Sources */, + 27BA02EB26E4ECD2008FC1A3 /* SecureIdValueAccessContext.swift in Sources */, + 27BA038026E4ECD2008FC1A3 /* ChangeAccountPhoneNumber.swift in Sources */, + 27BA045426E4ECD2008FC1A3 /* UpdateSecretChat.swift in Sources */, + 27BA033B26E4ECD2008FC1A3 /* TwoStepVerification.swift in Sources */, + 27BA031626E4ECD2008FC1A3 /* ReplyThreadHistory.swift in Sources */, + 27BA044526E4ECD2008FC1A3 /* SyncCore_ContactsSettings.swift in Sources */, + 27BA03C526E4ECD2008FC1A3 /* SyncCore_TemporaryTwoStepPasswordToken.swift in Sources */, + 27BA042C26E4ECD2008FC1A3 /* SyncCore_LoggedOutAccountAttribute.swift in Sources */, + 27BA026026E4ECD2008FC1A3 /* PeerContactSettings.swift in Sources */, + 27BA041826E4ECD2008FC1A3 /* SyncCore_SuggestedLocalizationEntry.swift in Sources */, + 27BA02FF26E4ECD2008FC1A3 /* SecureIdTemporaryRegistrationValue.swift in Sources */, + 27BA02A626E4ECD2008FC1A3 /* ManagedGlobalNotificationSettings.swift in Sources */, + 27BA03D026E4ECD2008FC1A3 /* SyncCore_RestrictedContentMessageAttribute.swift in Sources */, + 27BA036926E4ECD2008FC1A3 /* PeerCommands.swift in Sources */, + 27BA044926E4ECD2008FC1A3 /* AccountManager.swift in Sources */, + 27BA03E726E4ECD2008FC1A3 /* SyncCore_InlineBotMessageAttribute.swift in Sources */, + 27BA03A026E4ECD2008FC1A3 /* TelegramMediaMap.swift in Sources */, + 27BA034A26E4ECD2008FC1A3 /* RemovePeerChat.swift in Sources */, + 27BA034226E4ECD2008FC1A3 /* TelegramDeviceContactImportInfo.swift in Sources */, + 27BA035C26E4ECD2008FC1A3 /* JoinLink.swift in Sources */, + 27BA03BD26E4ECD2008FC1A3 /* SyncCore_AppConfiguration.swift in Sources */, + 27BA029926E4ECD2008FC1A3 /* UpdatesApiUtils.swift in Sources */, + 27BA03A426E4ECD2008FC1A3 /* ExportedInvitation.swift in Sources */, + 27BA038B26E4ECD2008FC1A3 /* TelegramGroup.swift in Sources */, + 27BA03DE26E4ECD2008FC1A3 /* SyncCore_CachedSecureIdConfiguration.swift in Sources */, + 27BA038526E4ECD2008FC1A3 /* Localizations.swift in Sources */, + 27BA033426E4ECD2008FC1A3 /* ActiveSessionsContext.swift in Sources */, + 27BA02DF26E4ECD2008FC1A3 /* LoadedPeerFromMessage.swift in Sources */, + 27BA034126E4ECD2008FC1A3 /* DeviceContact.swift in Sources */, + 27BA030926E4ECD2008FC1A3 /* StickerPackInteractiveOperations.swift in Sources */, + 27BA028426E4ECD2008FC1A3 /* StringFormat.swift in Sources */, + 27BA027526E4ECD2008FC1A3 /* Themes.swift in Sources */, + 27BA03DC26E4ECD2008FC1A3 /* SyncCore_SecretFileEncryptionKey.swift in Sources */, + 27BA036426E4ECD2008FC1A3 /* PeerAdmins.swift in Sources */, + 27BA027D26E4ECD2008FC1A3 /* ProxyServersStatuses.swift in Sources */, + 27BA02A826E4ECD2008FC1A3 /* ChannelState.swift in Sources */, + 27BA043926E4ECD2008FC1A3 /* SyncCore_EmbeddedMediaStickersMessageAttribute.swift in Sources */, + 27BA044E26E4ECD2008FC1A3 /* SecretChatOutgoingOperation.swift in Sources */, + 27BA03FF26E4ECD2008FC1A3 /* SyncCore_SynchronizeEmojiKeywordsOperation.swift in Sources */, + 27BA034626E4ECD2008FC1A3 /* StringCodingKey.swift in Sources */, + 27BA03F926E4ECD2008FC1A3 /* SyncCore_TextEntitiesMessageAttribute.swift in Sources */, + 27BA045126E4ECD2008FC1A3 /* SecretChatLayerNegotiation.swift in Sources */, + 27BA040B26E4ECD2008FC1A3 /* SyncCore_ContentRequiresValidationMessageAttribute.swift in Sources */, + 27BA02B226E4ECD2008FC1A3 /* ManagedCloudChatRemoveMessagesOperations.swift in Sources */, + 27BA041326E4ECD2008FC1A3 /* SyncCore_SecretChatState.swift in Sources */, + 27BA031226E4ECD2008FC1A3 /* SearchMessages.swift in Sources */, + 27BA02CD26E4ECD2008FC1A3 /* ContactSyncManager.swift in Sources */, + 27BA039F26E4ECD2008FC1A3 /* TelegramMediaAction.swift in Sources */, + 27BA033026E4ECD2008FC1A3 /* TelegramEnginePrivacy.swift in Sources */, + 27BA03A926E4ECD2008FC1A3 /* RichText.swift in Sources */, + 27BA03DA26E4ECD2008FC1A3 /* SyncCore_ConsumePersonalMessageAction.swift in Sources */, + 27BA029026E4ECD2008FC1A3 /* ProcessSecretChatIncomingDecryptedOperations.swift in Sources */, + 27BA041C26E4ECD2008FC1A3 /* SyncCore_ExportedInvitation.swift in Sources */, + 27BA029E26E4ECD2008FC1A3 /* ManagedMessageHistoryHoles.swift in Sources */, + 27BA02FA26E4ECD2008FC1A3 /* UploadSecureIdFile.swift in Sources */, + 27BA02ED26E4ECD2008FC1A3 /* SecureIdConfiguration.swift in Sources */, + 27BA026126E4ECD2008FC1A3 /* CacheStorageSettings.swift in Sources */, + 27BA044126E4ECD2008FC1A3 /* SyncCore_ConsumablePersonalMentionMessageAttribute.swift in Sources */, + 27BA026226E4ECD2008FC1A3 /* NetworkSettings.swift in Sources */, + 27BA02D126E4ECD2008FC1A3 /* CloudChatRemoveMessagesOperation.swift in Sources */, + 27BA029B26E4ECD2008FC1A3 /* MessageMediaPreuploadManager.swift in Sources */, + 27BA026C26E4ECD2008FC1A3 /* StandaloneSendMessage.swift in Sources */, + 27BA030E26E4ECD2008FC1A3 /* ClearCloudDrafts.swift in Sources */, + 27BA03C326E4ECD2008FC1A3 /* SyncCore_EditedMessageAttribute.swift in Sources */, + 27BA044A26E4ECD2008FC1A3 /* AccountIntermediateState.swift in Sources */, + 27BA03EC26E4ECD2008FC1A3 /* SyncCore_JSON.swift in Sources */, + 27BA030B26E4ECD2008FC1A3 /* ArchivedStickerPacks.swift in Sources */, + 27BA037526E4ECD2008FC1A3 /* ChannelMembers.swift in Sources */, + 27BA035626E4ECD2008FC1A3 /* UpdateGroupSpecificStickerset.swift in Sources */, + 27BA032D26E4ECD2008FC1A3 /* BlockedPeers.swift in Sources */, + 27BA03F126E4ECD2008FC1A3 /* SyncCore_SecretChatEncryptionConfig.swift in Sources */, + 27BA03BA26E4ECD2008FC1A3 /* SyncCore_ViewCountMessageAttribute.swift in Sources */, + 27BA035226E4ECD2008FC1A3 /* JoinChannel.swift in Sources */, + 27BA03BF26E4ECD2008FC1A3 /* SyncCore_LoggingSettings.swift in Sources */, + 27BA02B426E4ECD2008FC1A3 /* StickerManagement.swift in Sources */, + 27BA02FB26E4ECD2008FC1A3 /* SecureIdValueContentError.swift in Sources */, + 27BA02D226E4ECD2008FC1A3 /* Holes.swift in Sources */, + 27BA045026E4ECD2008FC1A3 /* SecretChatEncryptionConfig.swift in Sources */, + 27BA03BC26E4ECD2008FC1A3 /* SyncCore_TelegramMediaImage.swift in Sources */, + 27BA040026E4ECD2008FC1A3 /* SyncCore_TelegramDeviceContactImportedData.swift in Sources */, + 27BA02D526E4ECD2008FC1A3 /* ManagedSynchronizeAppLogEventsOperations.swift in Sources */, + 27BA03E026E4ECD2008FC1A3 /* SyncCore_TelegramMediaExpiredContent.swift in Sources */, + 27BA03F226E4ECD2008FC1A3 /* SyncCore_TelegramWallpaper.swift in Sources */, + 27BA032026E4ECD2008FC1A3 /* OutgoingMessageWithChatContextResult.swift in Sources */, + 27BA035E26E4ECD2008FC1A3 /* ReportPeer.swift in Sources */, + 27BA039626E4ECD2008FC1A3 /* TelegramMediaWebFile.swift in Sources */, + 27BA03FB26E4ECD2008FC1A3 /* SyncCore_ProxySettings.swift in Sources */, + 27BA036626E4ECD2008FC1A3 /* ConvertGroupToSupergroup.swift in Sources */, + 27BA041726E4ECD2008FC1A3 /* SyncCore_PeerReference.swift in Sources */, + 27BA02EE26E4ECD2008FC1A3 /* SaveSecureIdValue.swift in Sources */, + 27BA02E426E4ECD2008FC1A3 /* SecureIdPersonalDetailsValue.swift in Sources */, + 27BA030226E4ECD2008FC1A3 /* BotPaymentForm.swift in Sources */, + 27BA037E26E4ECD2008FC1A3 /* UpdateAccountPeerName.swift in Sources */, + 27BA035D26E4ECD2008FC1A3 /* AddPeerMember.swift in Sources */, + 27BA037C26E4ECD2008FC1A3 /* Configuration.swift in Sources */, + 27BA032926E4ECD2008FC1A3 /* MarkMessageContentAsConsumedInteractively.swift in Sources */, + 27BA028126E4ECD2008FC1A3 /* ImageRepresentationsUtils.swift in Sources */, + 27BA030F26E4ECD2008FC1A3 /* InstallInteractiveReadMessagesAction.swift in Sources */, + 27BA02DE26E4ECD2008FC1A3 /* LoadedPeer.swift in Sources */, + 27BA03EA26E4ECD2008FC1A3 /* SyncCore_ReplyMarkupMessageAttribute.swift in Sources */, + 27BA03FC26E4ECD2008FC1A3 /* SyncCore_ReactionsMessageAttribute.swift in Sources */, + 27BA02C426E4ECD2008FC1A3 /* PeerInputActivity.swift in Sources */, + 27BA043A26E4ECD2008FC1A3 /* SyncCore_TelegramMediaInvoice.swift in Sources */, + 27BA02F826E4ECD2008FC1A3 /* AccessSecureId.swift in Sources */, + 27BA043726E4ECD2008FC1A3 /* SyncCore_SecretChatKeychain.swift in Sources */, + 27BA03EB26E4ECD2008FC1A3 /* SyncCore_AutoremoveTimeoutMessageAttribute.swift in Sources */, + 27BA03BB26E4ECD2008FC1A3 /* SyncCore_SynchronizeAppLogEventsOperation.swift in Sources */, + 27BA03D426E4ECD2008FC1A3 /* SyncCore_SecretChatIncomingDecryptedOperation.swift in Sources */, + 27BA032F26E4ECD2008FC1A3 /* BlockedPeersContext.swift in Sources */, + 27BA02DC26E4ECD2008FC1A3 /* ManagedAutodownloadSettingsUpdates.swift in Sources */, + 27BA028F26E4ECD2008FC1A3 /* AccountState.swift in Sources */, + 27BA040326E4ECD2008FC1A3 /* SyncCore_TelegramMediaDice.swift in Sources */, + 27BA03FD26E4ECD2008FC1A3 /* SyncCore_SecureIdFileReference.swift in Sources */, + 27BA026526E4ECD2008FC1A3 /* AutodownloadSettings.swift in Sources */, + 27BA03AE26E4ECD2008FC1A3 /* MediaResourceApiUtils.swift in Sources */, + 27BA032B26E4ECD2008FC1A3 /* AdMessages.swift in Sources */, + 27BA029226E4ECD2008FC1A3 /* SynchronizeEmojiKeywordsOperation.swift in Sources */, + 27BA036326E4ECD2008FC1A3 /* SlowMode.swift in Sources */, + 27BA039A26E4ECD2008FC1A3 /* RemoteStorageConfiguration.swift in Sources */, + 27BA02A126E4ECD2008FC1A3 /* ManagedAppConfigurationUpdates.swift in Sources */, + 27BA030126E4ECD2008FC1A3 /* TelegramEnginePayments.swift in Sources */, + 27BA038826E4ECD2008FC1A3 /* SuggestedLocalizationEntry.swift in Sources */, + 27BA038226E4ECD2008FC1A3 /* TelegramEngineThemes.swift in Sources */, + 27BA02C826E4ECD2008FC1A3 /* AccountViewTracker.swift in Sources */, + 27BA026726E4ECD2008FC1A3 /* PrivacySettings.swift in Sources */, + 27BA03F326E4ECD2008FC1A3 /* SyncCore_ArchivedStickerPacksInfo.swift in Sources */, + 27BA039026E4ECD2008FC1A3 /* BotInfo.swift in Sources */, + 27BA027226E4ECD2008FC1A3 /* RequestEditMessage.swift in Sources */, + 27BA028D26E4ECD2008FC1A3 /* MessageUtils.swift in Sources */, + 27BA041226E4ECD2008FC1A3 /* SyncCore_LocalizationListState.swift in Sources */, + 27BA037126E4ECD2008FC1A3 /* SearchPeers.swift in Sources */, + 27BA02E026E4ECD2008FC1A3 /* UpdatePeerChatInterfaceState.swift in Sources */, + 27BA032826E4ECD2008FC1A3 /* RequestChatContextResults.swift in Sources */, + 27BA02A026E4ECD2008FC1A3 /* ManagedProxyInfoUpdates.swift in Sources */, + 27BA02C126E4ECD2008FC1A3 /* CallSessionManager.swift in Sources */, + 27BA041526E4ECD2008FC1A3 /* SyncCore_CachedRecentPeers.swift in Sources */, + 27BA03E526E4ECD2008FC1A3 /* SyncCore_Namespaces.swift in Sources */, + 27BA02B026E4ECD2008FC1A3 /* SynchronizeSavedStickersOperation.swift in Sources */, + 27BA031926E4ECD2008FC1A3 /* RecentlyUsedHashtags.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A7F282E1238EAB5B00742C20 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + 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_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = NO; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/third-party/FFmpeg-iOS/include"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + A7F282E2238EAB5B00742C20 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = "DEBUG=1"; + INFOPLIST_FILE = TelegramCore/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MODULEMAP_PRIVATE_FILE = ""; + ONLY_ACTIVE_ARCH = NO; + OTHER_SWIFT_FLAGS = "-DDEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Telegram.TelegramCore; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Github; + }; + C22069BE1E8EB4A200E82730 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + 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_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/third-party/FFmpeg-iOS/include"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + C22069C11E8EB4A200E82730 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = TelegramCore/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MODULEMAP_PRIVATE_FILE = ""; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Telegram.TelegramCore; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 5.0; + }; + name = ReleaseHockeyapp; + }; + D0364D4D22B3E37C002A6EF0 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + 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_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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_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; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/third-party/FFmpeg-iOS/include"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + D0364D5022B3E37C002A6EF0 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_NS_ASSERTIONS = NO; + FRAMEWORK_VERSION = A; + GCC_OPTIMIZATION_LEVEL = s; + GCC_PREPROCESSOR_DEFINITIONS = ""; + INFOPLIST_FILE = TelegramCore/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MODULEMAP_PRIVATE_FILE = ""; + ONLY_ACTIVE_ARCH = NO; + OTHER_SWIFT_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Telegram.TelegramCore; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 5.0; + }; + name = HockeyappMacAlpha; + }; + D06706551D51162400DED3E3 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + 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_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/third-party/FFmpeg-iOS/include"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + D09D8C131D4FAB1D0081DBEC /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + 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_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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_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; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/third-party/FFmpeg-iOS/include"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + D09D8C141D4FAB1D0081DBEC /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + 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_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = NO; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/third-party/FFmpeg-iOS/include"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + D0B4186D1D7E03D5004562A4 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_NS_ASSERTIONS = NO; + FRAMEWORK_VERSION = A; + GCC_OPTIMIZATION_LEVEL = s; + GCC_PREPROCESSOR_DEFINITIONS = ""; + INFOPLIST_FILE = TelegramCore/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MODULEMAP_PRIVATE_FILE = ""; + ONLY_ACTIVE_ARCH = NO; + OTHER_SWIFT_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Telegram.TelegramCore; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 5.0; + }; + name = DebugHockeyapp; + }; + D0B4186E1D7E03D5004562A4 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = "DEBUG=1"; + INFOPLIST_FILE = TelegramCore/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MODULEMAP_PRIVATE_FILE = ""; + ONLY_ACTIVE_ARCH = YES; + OTHER_SWIFT_FLAGS = "-DDEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Telegram.TelegramCore; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = DebugAppStore; + }; + D0B4186F1D7E03D5004562A4 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_SUSPICIOUS_MOVES = YES; + CODE_SIGN_IDENTITY = ""; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = TelegramCore/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MODULEMAP_PRIVATE_FILE = ""; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.Telegram.TelegramCore; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 5.0; + }; + name = ReleaseAppStore; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D09D8BFB1D4FAB1D0081DBEC /* Build configuration list for PBXProject "TelegramCore_Xcode" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D09D8C131D4FAB1D0081DBEC /* DebugHockeyapp */, + D0364D4D22B3E37C002A6EF0 /* HockeyappMacAlpha */, + D09D8C141D4FAB1D0081DBEC /* DebugAppStore */, + A7F282E1238EAB5B00742C20 /* Github */, + C22069BE1E8EB4A200E82730 /* ReleaseHockeyapp */, + D06706551D51162400DED3E3 /* ReleaseAppStore */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = DebugAppStore; + }; + D0B4186C1D7E03D5004562A4 /* Build configuration list for PBXNativeTarget "TelegramCore" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0B4186D1D7E03D5004562A4 /* DebugHockeyapp */, + D0364D5022B3E37C002A6EF0 /* HockeyappMacAlpha */, + D0B4186E1D7E03D5004562A4 /* DebugAppStore */, + A7F282E2238EAB5B00742C20 /* Github */, + C22069C11E8EB4A200E82730 /* ReleaseHockeyapp */, + D0B4186F1D7E03D5004562A4 /* ReleaseAppStore */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = DebugAppStore; + }; +/* End XCConfigurationList section */ + }; + rootObject = D09D8BF81D4FAB1D0081DBEC /* Project object */; +} diff --git a/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..48eb199cdd --- /dev/null +++ b/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate b/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000..f648f7bf5d Binary files /dev/null and b/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/TelegramCore.xcscheme b/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/TelegramCore.xcscheme new file mode 100644 index 0000000000..b6b42b380c --- /dev/null +++ b/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/TelegramCore.xcscheme @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/TelegramCoreMac.xcscheme b/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/TelegramCoreMac.xcscheme new file mode 100644 index 0000000000..b655b1740f --- /dev/null +++ b/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/TelegramCoreMac.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/xcuserdata/mikhailfilimonov.xcuserdatad/xcschemes/xcschememanagement.plist b/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/xcuserdata/mikhailfilimonov.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100755 index 0000000000..249f7c30c4 --- /dev/null +++ b/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/xcuserdata/mikhailfilimonov.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,19 @@ + + + + + SchemeUserState + + TelegramCore.xcscheme_^#shared#^_ + + orderHint + 23 + + TelegramCoreMac.xcscheme_^#shared#^_ + + orderHint + 36 + + + + diff --git a/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/TelegramCore.xcscheme b/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/TelegramCore.xcscheme new file mode 100644 index 0000000000..b655b1740f --- /dev/null +++ b/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/TelegramCore.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist b/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000000..ac05244e98 --- /dev/null +++ b/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,16 @@ + + + + + SchemeUserState + + TelegramCore.xcscheme + + isShown + + orderHint + 7 + + + + diff --git a/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/xcuserdata/telegram.xcuserdatad/xcschemes/xcschememanagement.plist b/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/xcuserdata/telegram.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000000..c051690e6f --- /dev/null +++ b/core-xprojects/TelegramCore/TelegramCore_Xcode.xcodeproj/xcuserdata/telegram.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,19 @@ + + + + + SchemeUserState + + TelegramCore.xcscheme_^#shared#^_ + + orderHint + 35 + + TelegramCoreMac.xcscheme_^#shared#^_ + + orderHint + 42 + + + + diff --git a/core-xprojects/TelegramVoip/TelegramVoip.xcodeproj/project.pbxproj b/core-xprojects/TelegramVoip/TelegramVoip.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..3ca6e1cf98 --- /dev/null +++ b/core-xprojects/TelegramVoip/TelegramVoip.xcodeproj/project.pbxproj @@ -0,0 +1,750 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 272899F725A72C5500831C79 /* OngoingCallVideoCapturer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 272899F625A72C5500831C79 /* OngoingCallVideoCapturer.swift */; }; + D0E7BD4D256AA3380068644D /* TelegramVoip.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E7BD4B256AA3380068644D /* TelegramVoip.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0E7BD61256AA5040068644D /* GroupCallContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E7BD5F256AA5040068644D /* GroupCallContext.swift */; }; + D0E7BD68256AA5300068644D /* SwiftSignalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0E7BD67256AA5300068644D /* SwiftSignalKit.framework */; }; + D0E7BD6A256AA5340068644D /* TgVoipWebrtc.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0E7BD69256AA5340068644D /* TgVoipWebrtc.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 272899F625A72C5500831C79 /* OngoingCallVideoCapturer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OngoingCallVideoCapturer.swift; sourceTree = ""; }; + D0E7BD48256AA3380068644D /* TelegramVoip.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TelegramVoip.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0E7BD4B256AA3380068644D /* TelegramVoip.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TelegramVoip.h; sourceTree = ""; }; + D0E7BD4C256AA3380068644D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D0E7BD5F256AA5040068644D /* GroupCallContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupCallContext.swift; sourceTree = ""; }; + D0E7BD67256AA5300068644D /* SwiftSignalKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftSignalKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0E7BD69256AA5340068644D /* TgVoipWebrtc.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TgVoipWebrtc.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D0E7BD45256AA3380068644D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D0E7BD6A256AA5340068644D /* TgVoipWebrtc.framework in Frameworks */, + D0E7BD68256AA5300068644D /* SwiftSignalKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 272899F525A72C3A00831C79 /* camera */ = { + isa = PBXGroup; + children = ( + 272899F625A72C5500831C79 /* OngoingCallVideoCapturer.swift */, + ); + path = camera; + sourceTree = ""; + }; + D0E7BD3E256AA3380068644D = { + isa = PBXGroup; + children = ( + D0E7BD4A256AA3380068644D /* TelegramVoip */, + D0E7BD49256AA3380068644D /* Products */, + D0E7BD66256AA5300068644D /* Frameworks */, + ); + sourceTree = ""; + }; + D0E7BD49256AA3380068644D /* Products */ = { + isa = PBXGroup; + children = ( + D0E7BD48256AA3380068644D /* TelegramVoip.framework */, + ); + name = Products; + sourceTree = ""; + }; + D0E7BD4A256AA3380068644D /* TelegramVoip */ = { + isa = PBXGroup; + children = ( + 272899F525A72C3A00831C79 /* camera */, + D0E7BD5E256AA5040068644D /* Sources */, + D0E7BD4B256AA3380068644D /* TelegramVoip.h */, + D0E7BD4C256AA3380068644D /* Info.plist */, + ); + path = TelegramVoip; + sourceTree = ""; + }; + D0E7BD5E256AA5040068644D /* Sources */ = { + isa = PBXGroup; + children = ( + D0E7BD5F256AA5040068644D /* GroupCallContext.swift */, + ); + name = Sources; + path = "../../../submodules/telegram-ios/submodules/TelegramVoip/Sources"; + sourceTree = ""; + }; + D0E7BD66256AA5300068644D /* Frameworks */ = { + isa = PBXGroup; + children = ( + D0E7BD69256AA5340068644D /* TgVoipWebrtc.framework */, + D0E7BD67256AA5300068644D /* SwiftSignalKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + D0E7BD43256AA3380068644D /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D0E7BD4D256AA3380068644D /* TelegramVoip.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + D0E7BD47256AA3380068644D /* TelegramVoip */ = { + isa = PBXNativeTarget; + buildConfigurationList = D0E7BD50256AA3380068644D /* Build configuration list for PBXNativeTarget "TelegramVoip" */; + buildPhases = ( + D0E7BD43256AA3380068644D /* Headers */, + D0E7BD44256AA3380068644D /* Sources */, + D0E7BD45256AA3380068644D /* Frameworks */, + D0E7BD46256AA3380068644D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = TelegramVoip; + productName = TelegramVoip; + productReference = D0E7BD48256AA3380068644D /* TelegramVoip.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D0E7BD3F256AA3380068644D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1220; + TargetAttributes = { + D0E7BD47256AA3380068644D = { + CreatedOnToolsVersion = 12.2; + }; + }; + }; + buildConfigurationList = D0E7BD42256AA3380068644D /* Build configuration list for PBXProject "TelegramVoip" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = D0E7BD3E256AA3380068644D; + productRefGroup = D0E7BD49256AA3380068644D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D0E7BD47256AA3380068644D /* TelegramVoip */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D0E7BD46256AA3380068644D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D0E7BD44256AA3380068644D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D0E7BD61256AA5040068644D /* GroupCallContext.swift in Sources */, + 272899F725A72C5500831C79 /* OngoingCallVideoCapturer.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + D0E7BD4E256AA3380068644D /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + D0E7BD4F256AA3380068644D /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + D0E7BD51256AA3380068644D /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + WEBRTC_MAC, + ); + INFOPLIST_FILE = TelegramVoip/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramVoip; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = DebugAppStore; + }; + D0E7BD52256AA3380068644D /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREPROCESSOR_DEFINITIONS = WEBRTC_MAC; + INFOPLIST_FILE = TelegramVoip/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramVoip; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = ReleaseAppStore; + }; + D0E7BD54256AA47A0068644D /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + D0E7BD55256AA47A0068644D /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREPROCESSOR_DEFINITIONS = WEBRTC_MAC; + INFOPLIST_FILE = TelegramVoip/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramVoip; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = ReleaseHockeyapp; + }; + D0E7BD56256AA4820068644D /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + D0E7BD57256AA4820068644D /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREPROCESSOR_DEFINITIONS = WEBRTC_MAC; + INFOPLIST_FILE = TelegramVoip/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramVoip; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = DebugHockeyapp; + }; + D0E7BD58256AA4870068644D /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + D0E7BD59256AA4870068644D /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREPROCESSOR_DEFINITIONS = WEBRTC_MAC; + INFOPLIST_FILE = TelegramVoip/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramVoip; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = HockeyappMacAlpha; + }; + D0E7BD5C256AA4980068644D /* Github */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + D0E7BD5D256AA4980068644D /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + WEBRTC_MAC, + ); + INFOPLIST_FILE = TelegramVoip/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramVoip; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = Github; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D0E7BD42256AA3380068644D /* Build configuration list for PBXProject "TelegramVoip" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0E7BD4E256AA3380068644D /* DebugAppStore */, + D0E7BD5C256AA4980068644D /* Github */, + D0E7BD4F256AA3380068644D /* ReleaseAppStore */, + D0E7BD54256AA47A0068644D /* ReleaseHockeyapp */, + D0E7BD56256AA4820068644D /* DebugHockeyapp */, + D0E7BD58256AA4870068644D /* HockeyappMacAlpha */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; + D0E7BD50256AA3380068644D /* Build configuration list for PBXNativeTarget "TelegramVoip" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0E7BD51256AA3380068644D /* DebugAppStore */, + D0E7BD5D256AA4980068644D /* Github */, + D0E7BD52256AA3380068644D /* ReleaseAppStore */, + D0E7BD55256AA47A0068644D /* ReleaseHockeyapp */, + D0E7BD57256AA4820068644D /* DebugHockeyapp */, + D0E7BD59256AA4870068644D /* HockeyappMacAlpha */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; +/* End XCConfigurationList section */ + }; + rootObject = D0E7BD3F256AA3380068644D /* Project object */; +} diff --git a/core-xprojects/TelegramVoip/TelegramVoip.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/TelegramVoip/TelegramVoip.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/core-xprojects/TelegramVoip/TelegramVoip.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/TelegramVoip/TelegramVoip.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/TelegramVoip/TelegramVoip.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/TelegramVoip/TelegramVoip.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/TelegramVoip/TelegramVoip/Info.plist b/core-xprojects/TelegramVoip/TelegramVoip/Info.plist new file mode 100644 index 0000000000..9bcb244429 --- /dev/null +++ b/core-xprojects/TelegramVoip/TelegramVoip/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/core-xprojects/TelegramVoip/TelegramVoip/TelegramVoip.h b/core-xprojects/TelegramVoip/TelegramVoip/TelegramVoip.h new file mode 100644 index 0000000000..ab23b57973 --- /dev/null +++ b/core-xprojects/TelegramVoip/TelegramVoip/TelegramVoip.h @@ -0,0 +1,18 @@ +// +// TelegramVoip.h +// TelegramVoip +// +// Created by Mikhail Filimonov on 22/11/2020. +// + +#import + +//! Project version number for TelegramVoip. +FOUNDATION_EXPORT double TelegramVoipVersionNumber; + +//! Project version string for TelegramVoip. +FOUNDATION_EXPORT const unsigned char TelegramVoipVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/core-xprojects/TelegramVoip/TelegramVoip/camera/OngoingCallVideoCapturer.swift b/core-xprojects/TelegramVoip/TelegramVoip/camera/OngoingCallVideoCapturer.swift new file mode 100644 index 0000000000..2bf532a9db --- /dev/null +++ b/core-xprojects/TelegramVoip/TelegramVoip/camera/OngoingCallVideoCapturer.swift @@ -0,0 +1,140 @@ +// +// OngoingCallVideoCapturer.swift +// TelegramVoip +// +// Created by Mikhail Filimonov on 07.01.2021. +// + +import Foundation +import Cocoa +import TgVoipWebrtc + + + +public enum OngoingCallVideoOrientation { + case rotation0 + case rotation90 + case rotation180 + case rotation270 +} + +public extension OngoingCallVideoOrientation { + init(_ orientation: OngoingCallVideoOrientationWebrtc) { + switch orientation { + case .orientation0: + self = .rotation0 + case .orientation90: + self = .rotation90 + case .orientation180: + self = .rotation180 + case .orientation270: + self = .rotation270 + @unknown default: + self = .rotation0 + } + } +} + + + +public final class OngoingCallContextPresentationCallVideoView { + public let view: NSView + public let setOnFirstFrameReceived: (((Float) -> Void)?) -> Void + public let getOrientation: () -> OngoingCallVideoOrientation + public let getAspect: () -> CGFloat + public let setOnOrientationUpdated: (((OngoingCallVideoOrientation, CGFloat) -> Void)?) -> Void + public let setVideoContentMode: (CALayerContentsGravity) -> Void + public let setOnIsMirroredUpdated: (((Bool) -> Void)?) -> Void + public let setIsPaused: (Bool) -> Void + public let renderToSize:(NSSize, Bool)->Void + public init( + view: NSView, + setOnFirstFrameReceived: @escaping (((Float) -> Void)?) -> Void, + getOrientation: @escaping () -> OngoingCallVideoOrientation, + getAspect: @escaping () -> CGFloat, + setOnOrientationUpdated: @escaping (((OngoingCallVideoOrientation, CGFloat) -> Void)?) -> Void, + setVideoContentMode: @escaping(CALayerContentsGravity) -> Void, + setOnIsMirroredUpdated: @escaping (((Bool) -> Void)?) -> Void, + setIsPaused: @escaping(Bool) -> Void, + renderToSize: @escaping(NSSize, Bool) -> Void + ) { + self.view = view + self.setOnFirstFrameReceived = setOnFirstFrameReceived + self.getOrientation = getOrientation + self.getAspect = getAspect + self.setOnOrientationUpdated = setOnOrientationUpdated + self.setVideoContentMode = setVideoContentMode + self.setOnIsMirroredUpdated = setOnIsMirroredUpdated + self.setIsPaused = setIsPaused + self.renderToSize = renderToSize + } +} + + + +public final class OngoingCallVideoCapturer { + public let impl: OngoingCallThreadLocalContextVideoCapturer + + public init(_ deviceId: String = "", keepLandscape: Bool = true) { + self.impl = OngoingCallThreadLocalContextVideoCapturer(deviceId: deviceId, keepLandscape: keepLandscape) + } + + public func makeOutgoingVideoView(completion: @escaping (OngoingCallContextPresentationCallVideoView?) -> Void) { + self.impl.makeOutgoingVideoView(false, completion: { view, _ in + if let view = view { + completion(OngoingCallContextPresentationCallVideoView( + view: view, setOnFirstFrameReceived: { [weak view] f in + view?.setOnFirstFrameReceived(f) + }, getOrientation: { + return .rotation90 + }, + getAspect: { [weak view] in + if let view = view { + return view.aspect + } else { + return 0.0 + } + }, + setOnOrientationUpdated: { [weak view] f in + + }, + setVideoContentMode: { [weak view] mode in + view?.setVideoContentMode(mode) + }, setOnIsMirroredUpdated: { [weak view] f in + view?.setOnIsMirroredUpdated { value in + f?(value) + } + }, setIsPaused: { [weak view] paused in + view?.setIsPaused(paused) + }, + renderToSize: { [weak view] size, animated in + view?.render(to: size, animated: animated) + } + )) + } else { + completion(nil) + } + }) + } + + public func setIsVideoEnabled(_ value: Bool) { + self.impl.setIsVideoEnabled(value) + } + + public func setOnFatalError(_ onError: @escaping()->Void) { + self.impl.setOnFatalError({ + DispatchQueue.main.async(execute: onError) + }) + } + public func setOnPause(_ onPause: @escaping(Bool)->Void) { + self.impl.setOnPause({ pause in + DispatchQueue.main.async(execute: { + onPause(pause) + }) + }) + } + + public func switchVideoInput(_ deviceId: String) { + self.impl.switchVideoInput(deviceId) + } +} diff --git a/core-xprojects/TgVoipWebrtc/TgVoipWebrtc.xcodeproj/project.pbxproj b/core-xprojects/TgVoipWebrtc/TgVoipWebrtc.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..5f08d3d561 --- /dev/null +++ b/core-xprojects/TgVoipWebrtc/TgVoipWebrtc.xcodeproj/project.pbxproj @@ -0,0 +1,1397 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 2702A835259BD7F6007F1DDD /* SSignalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2702A834259BD7F6007F1DDD /* SSignalKit.framework */; }; + 270C398426CE57730013E395 /* StreamingMediaContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 270C398226CE57730013E395 /* StreamingMediaContext.h */; }; + 270C398526CE57730013E395 /* StreamingMediaContext.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 270C398326CE57730013E395 /* StreamingMediaContext.cpp */; }; + 271503EC2631677B004FB0E0 /* DesktopCaptureSourceManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 271503E92631677B004FB0E0 /* DesktopCaptureSourceManager.cpp */; }; + 271503ED2631677B004FB0E0 /* DesktopCaptureSourceHelper.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 271503EA2631677B004FB0E0 /* DesktopCaptureSourceHelper.cpp */; }; + 271503EE2631677B004FB0E0 /* DesktopCaptureSource.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 271503EB2631677B004FB0E0 /* DesktopCaptureSource.cpp */; }; + 271503F42631679B004FB0E0 /* DesktopSharingCapturer.mm in Sources */ = {isa = PBXBuildFile; fileRef = 271503F02631679B004FB0E0 /* DesktopSharingCapturer.mm */; }; + 271503F52631679B004FB0E0 /* DesktopSharingCapturer.h in Headers */ = {isa = PBXBuildFile; fileRef = 271503F12631679B004FB0E0 /* DesktopSharingCapturer.h */; }; + 271504102631799A004FB0E0 /* DesktopCaptureSourceViewMac.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2715040E2631799A004FB0E0 /* DesktopCaptureSourceViewMac.mm */; }; + 271504112631799A004FB0E0 /* DesktopCaptureSourceViewMac.h in Headers */ = {isa = PBXBuildFile; fileRef = 2715040F2631799A004FB0E0 /* DesktopCaptureSourceViewMac.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 271999D62681F1E2000DE2B7 /* TGCMIOCapturer.m in Sources */ = {isa = PBXBuildFile; fileRef = 271999D02681F1E2000DE2B7 /* TGCMIOCapturer.m */; }; + 271999D72681F1E2000DE2B7 /* TGCMIOCapturer.h in Headers */ = {isa = PBXBuildFile; fileRef = 271999D12681F1E2000DE2B7 /* TGCMIOCapturer.h */; }; + 271999D82681F1E2000DE2B7 /* VideoCMIOCapture.mm in Sources */ = {isa = PBXBuildFile; fileRef = 271999D22681F1E2000DE2B7 /* VideoCMIOCapture.mm */; }; + 271999D92681F1E2000DE2B7 /* TGCMIODevice.h in Headers */ = {isa = PBXBuildFile; fileRef = 271999D32681F1E2000DE2B7 /* TGCMIODevice.h */; }; + 271999DA2681F1E2000DE2B7 /* TGCMIODevice.mm in Sources */ = {isa = PBXBuildFile; fileRef = 271999D42681F1E2000DE2B7 /* TGCMIODevice.mm */; }; + 271999DB2681F1E2000DE2B7 /* VideoCMIOCapture.h in Headers */ = {isa = PBXBuildFile; fileRef = 271999D52681F1E2000DE2B7 /* VideoCMIOCapture.h */; }; + 27237EC2269C70A400DF4C44 /* SctpDataChannelProviderInterfaceImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 27237EC0269C70A400DF4C44 /* SctpDataChannelProviderInterfaceImpl.h */; }; + 27237EC3269C70A400DF4C44 /* SctpDataChannelProviderInterfaceImpl.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 27237EC1269C70A400DF4C44 /* SctpDataChannelProviderInterfaceImpl.cpp */; }; + 2728991426CBB77600F4D288 /* VideoStreamingPart.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2728991026CBB77600F4D288 /* VideoStreamingPart.cpp */; }; + 2728991526CBB77600F4D288 /* AudioStreamingPart.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2728991126CBB77600F4D288 /* AudioStreamingPart.cpp */; }; + 2728991626CBB77600F4D288 /* VideoStreamingPart.h in Headers */ = {isa = PBXBuildFile; fileRef = 2728991226CBB77600F4D288 /* VideoStreamingPart.h */; }; + 2728991726CBB77600F4D288 /* AudioStreamingPart.h in Headers */ = {isa = PBXBuildFile; fileRef = 2728991326CBB77600F4D288 /* AudioStreamingPart.h */; }; + 27328D8C2660E8AD00E3B7D8 /* DarwinVideoSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 27328D8A2660E8AD00E3B7D8 /* DarwinVideoSource.h */; }; + 27328D8D2660E8AD00E3B7D8 /* DarwinVideoSource.mm in Sources */ = {isa = PBXBuildFile; fileRef = 27328D8B2660E8AD00E3B7D8 /* DarwinVideoSource.mm */; }; + 27335F0825C833D400FD040C /* GroupInstanceCustomImpl.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 27335F0625C833D300FD040C /* GroupInstanceCustomImpl.cpp */; }; + 27335F0925C833D400FD040C /* GroupInstanceCustomImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 27335F0725C833D300FD040C /* GroupInstanceCustomImpl.h */; }; + 273F3237265B97F2005D918E /* VideoSampleBufferViewMac.mm in Sources */ = {isa = PBXBuildFile; fileRef = 273F3235265B97F2005D918E /* VideoSampleBufferViewMac.mm */; }; + 273F3238265B97F2005D918E /* VideoSampleBufferViewMac.h in Headers */ = {isa = PBXBuildFile; fileRef = 273F3236265B97F2005D918E /* VideoSampleBufferViewMac.h */; }; + 274340FA25A5BADE00A71071 /* AudioDeviceHelper.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 274340F925A5BADE00A71071 /* AudioDeviceHelper.cpp */; }; + 274BB568263AE0AA00620D03 /* GroupJoinPayloadInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 274BB565263AE0AA00620D03 /* GroupJoinPayloadInternal.h */; }; + 274BB569263AE0AA00620D03 /* GroupJoinPayloadInternal.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 274BB566263AE0AA00620D03 /* GroupJoinPayloadInternal.cpp */; }; + 274BB56A263AE0AA00620D03 /* GroupJoinPayload.h in Headers */ = {isa = PBXBuildFile; fileRef = 274BB567263AE0AA00620D03 /* GroupJoinPayload.h */; }; + 274CF52C26987D26009139CC /* FakeAudioDeviceModule.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 274CF52B26987D26009139CC /* FakeAudioDeviceModule.cpp */; }; + 2767ADF625D6DDDC00717AA1 /* DesktopCaptureSourceHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = 2767ADEC25D6DDDC00717AA1 /* DesktopCaptureSourceHelper.h */; }; + 2767ADF725D6DDDC00717AA1 /* DesktopCaptureSourceManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 2767ADED25D6DDDC00717AA1 /* DesktopCaptureSourceManager.h */; }; + 2767ADFA25D6DDDC00717AA1 /* DesktopCaptureSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 2767ADF025D6DDDC00717AA1 /* DesktopCaptureSource.h */; }; + 2774C565262F19F4006E6F26 /* SignalingEncryption.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 2774C563262F19F4006E6F26 /* SignalingEncryption.cpp */; }; + 2774C566262F19F4006E6F26 /* SignalingEncryption.h in Headers */ = {isa = PBXBuildFile; fileRef = 2774C564262F19F4006E6F26 /* SignalingEncryption.h */; }; + 277CBFAB268B4E9F0082ACA2 /* objc_video_encoder_factory.mm in Sources */ = {isa = PBXBuildFile; fileRef = 277CBFA9268B4E9F0082ACA2 /* objc_video_encoder_factory.mm */; }; + 277CBFAC268B4E9F0082ACA2 /* objc_video_encoder_factory.h in Headers */ = {isa = PBXBuildFile; fileRef = 277CBFAA268B4E9F0082ACA2 /* objc_video_encoder_factory.h */; }; + 27863665269C6ECF006D98C6 /* TurnCustomizerImpl.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 27863663269C6ECF006D98C6 /* TurnCustomizerImpl.cpp */; }; + 27863666269C6ECF006D98C6 /* TurnCustomizerImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 27863664269C6ECF006D98C6 /* TurnCustomizerImpl.h */; }; + 27A0A4E025F764FA00B789AF /* ffmpeg.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27A0A4DF25F764FA00B789AF /* ffmpeg.framework */; }; + 27C167FF26203A56000B8449 /* NativeNetworkingImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 27C167F926203A56000B8449 /* NativeNetworkingImpl.h */; }; + 27C1680026203A56000B8449 /* Signaling.h in Headers */ = {isa = PBXBuildFile; fileRef = 27C167FA26203A56000B8449 /* Signaling.h */; }; + 27C1680126203A56000B8449 /* NativeNetworkingImpl.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 27C167FB26203A56000B8449 /* NativeNetworkingImpl.cpp */; }; + 27C1680226203A56000B8449 /* InstanceV2Impl.h in Headers */ = {isa = PBXBuildFile; fileRef = 27C167FC26203A56000B8449 /* InstanceV2Impl.h */; }; + 27C1680326203A56000B8449 /* InstanceV2Impl.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 27C167FD26203A56000B8449 /* InstanceV2Impl.cpp */; }; + 27C1680426203A56000B8449 /* Signaling.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 27C167FE26203A56000B8449 /* Signaling.cpp */; }; + 27D0067D25AF266400EE3EB1 /* GroupNetworkManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 27D0067B25AF266400EE3EB1 /* GroupNetworkManager.cpp */; }; + 27D0067E25AF266400EE3EB1 /* GroupNetworkManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 27D0067C25AF266400EE3EB1 /* GroupNetworkManager.h */; }; + 27D0068325AF26AE00EE3EB1 /* StaticThreads.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 27D0068125AF26AE00EE3EB1 /* StaticThreads.cpp */; }; + 27D7ABA525EFCF7B008DB916 /* TelegramCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 27D7ABA425EFCF7B008DB916 /* TelegramCore.framework */; }; + 27DF650A26AADFB2000753AC /* objc_video_decoder_factory.mm in Sources */ = {isa = PBXBuildFile; fileRef = 27DF650826AADFB2000753AC /* objc_video_decoder_factory.mm */; }; + 27DF650B26AADFB2000753AC /* objc_video_decoder_factory.h in Headers */ = {isa = PBXBuildFile; fileRef = 27DF650926AADFB2000753AC /* objc_video_decoder_factory.h */; }; + 27E6DAF926860663003D6164 /* TGRTCMTLRenderer.h in Headers */ = {isa = PBXBuildFile; fileRef = 27E6DAF426860663003D6164 /* TGRTCMTLRenderer.h */; }; + 27E6DAFA26860663003D6164 /* TGRTCMTLI420Renderer.mm in Sources */ = {isa = PBXBuildFile; fileRef = 27E6DAF526860663003D6164 /* TGRTCMTLI420Renderer.mm */; }; + 27E6DAFB26860663003D6164 /* TGRTCMTLRenderer+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 27E6DAF626860663003D6164 /* TGRTCMTLRenderer+Private.h */; }; + 27E6DAFC26860663003D6164 /* TGRTCMTLI420Renderer.h in Headers */ = {isa = PBXBuildFile; fileRef = 27E6DAF726860663003D6164 /* TGRTCMTLI420Renderer.h */; }; + 27E6DAFD26860663003D6164 /* TGRTCMTLRenderer.mm in Sources */ = {isa = PBXBuildFile; fileRef = 27E6DAF826860663003D6164 /* TGRTCMTLRenderer.mm */; }; + 27E6DB11268A10D5003D6164 /* TGRTCMetalContextHolder.h in Headers */ = {isa = PBXBuildFile; fileRef = 27E6DB0F268A10D5003D6164 /* TGRTCMetalContextHolder.h */; }; + 27E6DB12268A10D5003D6164 /* TGRTCMetalContextHolder.m in Sources */ = {isa = PBXBuildFile; fileRef = 27E6DB10268A10D5003D6164 /* TGRTCMetalContextHolder.m */; }; + D00602F924CF3D4F00206226 /* EncryptedConnection.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D00602F824CF3D4F00206226 /* EncryptedConnection.cpp */; }; + D00602FB24CF3D5C00206226 /* LogSinkImpl.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D00602FA24CF3D5C00206226 /* LogSinkImpl.cpp */; }; + D00602FD24CF3D6600206226 /* CryptoHelper.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D00602FC24CF3D6600206226 /* CryptoHelper.cpp */; }; + D00602FF24CF3D7500206226 /* Message.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D00602FE24CF3D7500206226 /* Message.cpp */; }; + D0076E90256854E7007EF588 /* webrtc.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0076E3C25685056007EF588 /* webrtc.framework */; }; + D00B192024E56FE2006CCB87 /* GLVideoViewMac.mm in Sources */ = {isa = PBXBuildFile; fileRef = D00B191F24E56FE2006CCB87 /* GLVideoViewMac.mm */; }; + D00B192224E5722A006CCB87 /* GLKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D00B192124E5722A006CCB87 /* GLKit.framework */; }; + D06E369A24A4FD4A00C7D03A /* TgVoipWebrtc.h in Headers */ = {isa = PBXBuildFile; fileRef = D06E368C24A4FD4900C7D03A /* TgVoipWebrtc.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D06E36DD24A5000100C7D03A /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D06E36DC24A5000100C7D03A /* AudioToolbox.framework */; }; + D06E36DF24A5000700C7D03A /* VideoToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D06E36DE24A5000700C7D03A /* VideoToolbox.framework */; }; + D06E36E124A5001300C7D03A /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D06E36E024A5001300C7D03A /* CoreMedia.framework */; }; + D06E36E324A5001800C7D03A /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D06E36E224A5001800C7D03A /* AVFoundation.framework */; }; + D0D751A024BE34AB0037D73A /* OngoingCallThreadLocalContext.mm in Sources */ = {isa = PBXBuildFile; fileRef = D0D7519F24BE34AB0037D73A /* OngoingCallThreadLocalContext.mm */; }; + D0D751A224BE34B40037D73A /* OngoingCallThreadLocalContext.h in Headers */ = {isa = PBXBuildFile; fileRef = D0D751A124BE34B40037D73A /* OngoingCallThreadLocalContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0D751A424BE372F0037D73A /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0D751A324BE372F0037D73A /* AppKit.framework */; }; + D0D751AE24BE3A140037D73A /* VideoMetalViewMac.mm in Sources */ = {isa = PBXBuildFile; fileRef = D0D751A524BE3A130037D73A /* VideoMetalViewMac.mm */; }; + D0D751AF24BE3A140037D73A /* TGRTCDefaultVideoEncoderFactory.mm in Sources */ = {isa = PBXBuildFile; fileRef = D0D751A624BE3A130037D73A /* TGRTCDefaultVideoEncoderFactory.mm */; }; + D0D751B124BE3A140037D73A /* VideoCapturerInterfaceImpl.mm in Sources */ = {isa = PBXBuildFile; fileRef = D0D751A824BE3A140037D73A /* VideoCapturerInterfaceImpl.mm */; }; + D0D751B224BE3A140037D73A /* TGRTCVideoEncoderH265.mm in Sources */ = {isa = PBXBuildFile; fileRef = D0D751A924BE3A140037D73A /* TGRTCVideoEncoderH265.mm */; }; + D0D751B324BE3A140037D73A /* TGRTCVideoDecoderH265.mm in Sources */ = {isa = PBXBuildFile; fileRef = D0D751AA24BE3A140037D73A /* TGRTCVideoDecoderH265.mm */; }; + D0D751B424BE3A140037D73A /* TGRTCDefaultVideoDecoderFactory.mm in Sources */ = {isa = PBXBuildFile; fileRef = D0D751AB24BE3A140037D73A /* TGRTCDefaultVideoDecoderFactory.mm */; }; + D0D751B524BE3A140037D73A /* VideoCameraCapturerMac.mm in Sources */ = {isa = PBXBuildFile; fileRef = D0D751AC24BE3A140037D73A /* VideoCameraCapturerMac.mm */; }; + D0D751B624BE3A140037D73A /* DarwinInterface.mm in Sources */ = {isa = PBXBuildFile; fileRef = D0D751AD24BE3A140037D73A /* DarwinInterface.mm */; }; + D0D751C124BE3AC70037D73A /* NetworkManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D0D751B724BE3AC60037D73A /* NetworkManager.cpp */; }; + D0D751C224BE3AC70037D73A /* MediaManager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D0D751B824BE3AC60037D73A /* MediaManager.cpp */; }; + D0D751C324BE3AC70037D73A /* ThreadLocalObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D0D751B924BE3AC60037D73A /* ThreadLocalObject.cpp */; }; + D0D751C424BE3AC70037D73A /* VideoCaptureInterface.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D0D751BA24BE3AC70037D73A /* VideoCaptureInterface.cpp */; }; + D0D751C524BE3AC70037D73A /* CodecSelectHelper.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D0D751BB24BE3AC70037D73A /* CodecSelectHelper.cpp */; }; + D0D751C624BE3AC70037D73A /* Manager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D0D751BC24BE3AC70037D73A /* Manager.cpp */; }; + D0D751C724BE3AC70037D73A /* InstanceImpl.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D0D751BD24BE3AC70037D73A /* InstanceImpl.cpp */; }; + D0D751C924BE3AC70037D73A /* VideoCaptureInterfaceImpl.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D0D751BF24BE3AC70037D73A /* VideoCaptureInterfaceImpl.cpp */; }; + D0D751CA24BE3AC70037D73A /* Instance.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D0D751C024BE3AC70037D73A /* Instance.cpp */; }; + D0D751CC24BE3AD40037D73A /* InstanceImplLegacy.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D0D751CB24BE3AD30037D73A /* InstanceImplLegacy.cpp */; }; + D0E3685224DAA0E2009896D4 /* TGRTCCVPixelBuffer.mm in Sources */ = {isa = PBXBuildFile; fileRef = D0E3685124DAA0E2009896D4 /* TGRTCCVPixelBuffer.mm */; }; + D0E3685524DAADF5009896D4 /* TGRTCVideoDecoderH264.mm in Sources */ = {isa = PBXBuildFile; fileRef = D0E3685324DAADF5009896D4 /* TGRTCVideoDecoderH264.mm */; }; + D0E3685624DAADF5009896D4 /* TGRTCVideoEncoderH264.mm in Sources */ = {isa = PBXBuildFile; fileRef = D0E3685424DAADF5009896D4 /* TGRTCVideoEncoderH264.mm */; }; + D0E7BD20256A7E0D0068644D /* GroupInstanceImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = D0E7BD1E256A7E0D0068644D /* GroupInstanceImpl.h */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 2702A834259BD7F6007F1DDD /* SSignalKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SSignalKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 270C398226CE57730013E395 /* StreamingMediaContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StreamingMediaContext.h; sourceTree = ""; }; + 270C398326CE57730013E395 /* StreamingMediaContext.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = StreamingMediaContext.cpp; sourceTree = ""; }; + 271503E92631677B004FB0E0 /* DesktopCaptureSourceManager.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = DesktopCaptureSourceManager.cpp; path = ../../submodules/tgcalls/tgcalls/desktop_capturer/DesktopCaptureSourceManager.cpp; sourceTree = ""; }; + 271503EA2631677B004FB0E0 /* DesktopCaptureSourceHelper.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = DesktopCaptureSourceHelper.cpp; path = ../../submodules/tgcalls/tgcalls/desktop_capturer/DesktopCaptureSourceHelper.cpp; sourceTree = ""; }; + 271503EB2631677B004FB0E0 /* DesktopCaptureSource.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = DesktopCaptureSource.cpp; path = ../../submodules/tgcalls/tgcalls/desktop_capturer/DesktopCaptureSource.cpp; sourceTree = ""; }; + 271503F02631679B004FB0E0 /* DesktopSharingCapturer.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = DesktopSharingCapturer.mm; path = ../../submodules/tgcalls/tgcalls/platform/darwin/DesktopSharingCapturer.mm; sourceTree = ""; }; + 271503F12631679B004FB0E0 /* DesktopSharingCapturer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DesktopSharingCapturer.h; path = ../../submodules/tgcalls/tgcalls/platform/darwin/DesktopSharingCapturer.h; sourceTree = ""; }; + 2715040E2631799A004FB0E0 /* DesktopCaptureSourceViewMac.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = DesktopCaptureSourceViewMac.mm; path = ../../submodules/tgcalls/tgcalls/platform/darwin/DesktopCaptureSourceViewMac.mm; sourceTree = ""; }; + 2715040F2631799A004FB0E0 /* DesktopCaptureSourceViewMac.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DesktopCaptureSourceViewMac.h; path = ../../submodules/tgcalls/tgcalls/platform/darwin/DesktopCaptureSourceViewMac.h; sourceTree = ""; }; + 271999D02681F1E2000DE2B7 /* TGCMIOCapturer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TGCMIOCapturer.m; path = ../../submodules/tgcalls/tgcalls/platform/darwin/TGCMIOCapturer.m; sourceTree = ""; }; + 271999D12681F1E2000DE2B7 /* TGCMIOCapturer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGCMIOCapturer.h; path = ../../submodules/tgcalls/tgcalls/platform/darwin/TGCMIOCapturer.h; sourceTree = ""; }; + 271999D22681F1E2000DE2B7 /* VideoCMIOCapture.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = VideoCMIOCapture.mm; path = ../../submodules/tgcalls/tgcalls/platform/darwin/VideoCMIOCapture.mm; sourceTree = ""; }; + 271999D32681F1E2000DE2B7 /* TGCMIODevice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TGCMIODevice.h; path = ../../submodules/tgcalls/tgcalls/platform/darwin/TGCMIODevice.h; sourceTree = ""; }; + 271999D42681F1E2000DE2B7 /* TGCMIODevice.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = TGCMIODevice.mm; path = ../../submodules/tgcalls/tgcalls/platform/darwin/TGCMIODevice.mm; sourceTree = ""; }; + 271999D52681F1E2000DE2B7 /* VideoCMIOCapture.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = VideoCMIOCapture.h; path = ../../submodules/tgcalls/tgcalls/platform/darwin/VideoCMIOCapture.h; sourceTree = ""; }; + 27237EC0269C70A400DF4C44 /* SctpDataChannelProviderInterfaceImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SctpDataChannelProviderInterfaceImpl.h; path = ../../submodules/tgcalls/tgcalls/SctpDataChannelProviderInterfaceImpl.h; sourceTree = ""; }; + 27237EC1269C70A400DF4C44 /* SctpDataChannelProviderInterfaceImpl.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = SctpDataChannelProviderInterfaceImpl.cpp; path = ../../submodules/tgcalls/tgcalls/SctpDataChannelProviderInterfaceImpl.cpp; sourceTree = ""; }; + 2728991026CBB77600F4D288 /* VideoStreamingPart.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = VideoStreamingPart.cpp; sourceTree = ""; }; + 2728991126CBB77600F4D288 /* AudioStreamingPart.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = AudioStreamingPart.cpp; sourceTree = ""; }; + 2728991226CBB77600F4D288 /* VideoStreamingPart.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VideoStreamingPart.h; sourceTree = ""; }; + 2728991326CBB77600F4D288 /* AudioStreamingPart.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AudioStreamingPart.h; sourceTree = ""; }; + 27328D8A2660E8AD00E3B7D8 /* DarwinVideoSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DarwinVideoSource.h; path = ../../submodules/tgcalls/tgcalls/platform/darwin/DarwinVideoSource.h; sourceTree = ""; }; + 27328D8B2660E8AD00E3B7D8 /* DarwinVideoSource.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = DarwinVideoSource.mm; path = ../../submodules/tgcalls/tgcalls/platform/darwin/DarwinVideoSource.mm; sourceTree = ""; }; + 27335F0625C833D300FD040C /* GroupInstanceCustomImpl.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = GroupInstanceCustomImpl.cpp; sourceTree = ""; }; + 27335F0725C833D300FD040C /* GroupInstanceCustomImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GroupInstanceCustomImpl.h; sourceTree = ""; }; + 273F3235265B97F2005D918E /* VideoSampleBufferViewMac.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = VideoSampleBufferViewMac.mm; path = ../../submodules/tgcalls/tgcalls/platform/darwin/VideoSampleBufferViewMac.mm; sourceTree = ""; }; + 273F3236265B97F2005D918E /* VideoSampleBufferViewMac.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = VideoSampleBufferViewMac.h; path = ../../submodules/tgcalls/tgcalls/platform/darwin/VideoSampleBufferViewMac.h; sourceTree = ""; }; + 274340F925A5BADE00A71071 /* AudioDeviceHelper.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = AudioDeviceHelper.cpp; path = ../../submodules/tgcalls/tgcalls/AudioDeviceHelper.cpp; sourceTree = ""; }; + 274BB565263AE0AA00620D03 /* GroupJoinPayloadInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GroupJoinPayloadInternal.h; sourceTree = ""; }; + 274BB566263AE0AA00620D03 /* GroupJoinPayloadInternal.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = GroupJoinPayloadInternal.cpp; sourceTree = ""; }; + 274BB567263AE0AA00620D03 /* GroupJoinPayload.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GroupJoinPayload.h; sourceTree = ""; }; + 274CF52B26987D26009139CC /* FakeAudioDeviceModule.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = FakeAudioDeviceModule.cpp; path = ../../submodules/tgcalls/tgcalls/FakeAudioDeviceModule.cpp; sourceTree = ""; }; + 2767ADEC25D6DDDC00717AA1 /* DesktopCaptureSourceHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DesktopCaptureSourceHelper.h; path = ../../submodules/tgcalls/tgcalls/desktop_capturer/DesktopCaptureSourceHelper.h; sourceTree = ""; }; + 2767ADED25D6DDDC00717AA1 /* DesktopCaptureSourceManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DesktopCaptureSourceManager.h; path = ../../submodules/tgcalls/tgcalls/desktop_capturer/DesktopCaptureSourceManager.h; sourceTree = ""; }; + 2767ADF025D6DDDC00717AA1 /* DesktopCaptureSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DesktopCaptureSource.h; path = ../../submodules/tgcalls/tgcalls/desktop_capturer/DesktopCaptureSource.h; sourceTree = ""; }; + 2774C563262F19F4006E6F26 /* SignalingEncryption.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SignalingEncryption.cpp; sourceTree = ""; }; + 2774C564262F19F4006E6F26 /* SignalingEncryption.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SignalingEncryption.h; sourceTree = ""; }; + 277CBFA9268B4E9F0082ACA2 /* objc_video_encoder_factory.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = objc_video_encoder_factory.mm; path = ../../submodules/tgcalls/tgcalls/platform/darwin/objc_video_encoder_factory.mm; sourceTree = ""; }; + 277CBFAA268B4E9F0082ACA2 /* objc_video_encoder_factory.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = objc_video_encoder_factory.h; path = ../../submodules/tgcalls/tgcalls/platform/darwin/objc_video_encoder_factory.h; sourceTree = ""; }; + 27863663269C6ECF006D98C6 /* TurnCustomizerImpl.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = TurnCustomizerImpl.cpp; path = ../../submodules/tgcalls/tgcalls/TurnCustomizerImpl.cpp; sourceTree = ""; }; + 27863664269C6ECF006D98C6 /* TurnCustomizerImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TurnCustomizerImpl.h; path = ../../submodules/tgcalls/tgcalls/TurnCustomizerImpl.h; sourceTree = ""; }; + 27A0A4DF25F764FA00B789AF /* ffmpeg.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ffmpeg.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 27C167F926203A56000B8449 /* NativeNetworkingImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NativeNetworkingImpl.h; sourceTree = ""; }; + 27C167FA26203A56000B8449 /* Signaling.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Signaling.h; sourceTree = ""; }; + 27C167FB26203A56000B8449 /* NativeNetworkingImpl.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = NativeNetworkingImpl.cpp; sourceTree = ""; }; + 27C167FC26203A56000B8449 /* InstanceV2Impl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InstanceV2Impl.h; sourceTree = ""; }; + 27C167FD26203A56000B8449 /* InstanceV2Impl.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = InstanceV2Impl.cpp; sourceTree = ""; }; + 27C167FE26203A56000B8449 /* Signaling.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Signaling.cpp; sourceTree = ""; }; + 27D0067B25AF266400EE3EB1 /* GroupNetworkManager.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = GroupNetworkManager.cpp; sourceTree = ""; }; + 27D0067C25AF266400EE3EB1 /* GroupNetworkManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GroupNetworkManager.h; sourceTree = ""; }; + 27D0068125AF26AE00EE3EB1 /* StaticThreads.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = StaticThreads.cpp; path = ../../submodules/tgcalls/tgcalls/StaticThreads.cpp; sourceTree = ""; }; + 27D7ABA425EFCF7B008DB916 /* TelegramCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TelegramCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 27DF650826AADFB2000753AC /* objc_video_decoder_factory.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = objc_video_decoder_factory.mm; path = ../../submodules/tgcalls/tgcalls/platform/darwin/objc_video_decoder_factory.mm; sourceTree = ""; }; + 27DF650926AADFB2000753AC /* objc_video_decoder_factory.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = objc_video_decoder_factory.h; path = ../../submodules/tgcalls/tgcalls/platform/darwin/objc_video_decoder_factory.h; sourceTree = ""; }; + 27E4344C2680DF1700B05CB1 /* CoreMediaMacCapture.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CoreMediaMacCapture.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 27E6DAF426860663003D6164 /* TGRTCMTLRenderer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGRTCMTLRenderer.h; sourceTree = ""; }; + 27E6DAF526860663003D6164 /* TGRTCMTLI420Renderer.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = TGRTCMTLI420Renderer.mm; sourceTree = ""; }; + 27E6DAF626860663003D6164 /* TGRTCMTLRenderer+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "TGRTCMTLRenderer+Private.h"; sourceTree = ""; }; + 27E6DAF726860663003D6164 /* TGRTCMTLI420Renderer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TGRTCMTLI420Renderer.h; sourceTree = ""; }; + 27E6DAF826860663003D6164 /* TGRTCMTLRenderer.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = TGRTCMTLRenderer.mm; sourceTree = ""; }; + 27E6DB0F268A10D5003D6164 /* TGRTCMetalContextHolder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TGRTCMetalContextHolder.h; sourceTree = ""; }; + 27E6DB10268A10D5003D6164 /* TGRTCMetalContextHolder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TGRTCMetalContextHolder.m; sourceTree = ""; }; + D00602F824CF3D4F00206226 /* EncryptedConnection.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = EncryptedConnection.cpp; path = ../../submodules/tgcalls/tgcalls/EncryptedConnection.cpp; sourceTree = ""; }; + D00602FA24CF3D5C00206226 /* LogSinkImpl.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = LogSinkImpl.cpp; path = ../../submodules/tgcalls/tgcalls/LogSinkImpl.cpp; sourceTree = ""; }; + D00602FC24CF3D6600206226 /* CryptoHelper.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CryptoHelper.cpp; path = ../../submodules/tgcalls/tgcalls/CryptoHelper.cpp; sourceTree = ""; }; + D00602FE24CF3D7500206226 /* Message.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Message.cpp; path = ../../submodules/tgcalls/tgcalls/Message.cpp; sourceTree = ""; }; + D0076E3C25685056007EF588 /* webrtc.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = webrtc.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D00B191F24E56FE2006CCB87 /* GLVideoViewMac.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = GLVideoViewMac.mm; path = ../../submodules/tgcalls/tgcalls/platform/darwin/GLVideoViewMac.mm; sourceTree = ""; }; + D00B192124E5722A006CCB87 /* GLKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GLKit.framework; path = System/Library/Frameworks/GLKit.framework; sourceTree = SDKROOT; }; + D06E368924A4FD4900C7D03A /* TgVoipWebrtc.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TgVoipWebrtc.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D06E368C24A4FD4900C7D03A /* TgVoipWebrtc.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TgVoipWebrtc.h; sourceTree = ""; }; + D06E368D24A4FD4900C7D03A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D06E369724A4FD4A00C7D03A /* TgVoipWebrtcTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TgVoipWebrtcTests.swift; sourceTree = ""; }; + D06E369924A4FD4A00C7D03A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D06E36A624A4FDAC00C7D03A /* libmac_framework_objc_static.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libmac_framework_objc_static.a; sourceTree = ""; }; + D06E36DC24A5000100C7D03A /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; }; + D06E36DE24A5000700C7D03A /* VideoToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = VideoToolbox.framework; path = System/Library/Frameworks/VideoToolbox.framework; sourceTree = SDKROOT; }; + D06E36E024A5001300C7D03A /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; + D06E36E224A5001800C7D03A /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; + D06E38A624A5EEAD00C7D03A /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; }; + D06E38A724A5EEAD00C7D03A /* MetalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetalKit.framework; path = System/Library/Frameworks/MetalKit.framework; sourceTree = SDKROOT; }; + D0D7519F24BE34AB0037D73A /* OngoingCallThreadLocalContext.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = OngoingCallThreadLocalContext.mm; path = "../../../submodules/telegram-ios/submodules/TgVoipWebrtc/Sources/OngoingCallThreadLocalContext.mm"; sourceTree = ""; }; + D0D751A124BE34B40037D73A /* OngoingCallThreadLocalContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OngoingCallThreadLocalContext.h; path = "../../../submodules/telegram-ios/submodules/TgVoipWebrtc/PublicHeaders/TgVoipWebrtc/OngoingCallThreadLocalContext.h"; sourceTree = ""; }; + D0D751A324BE372F0037D73A /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; + D0D751A524BE3A130037D73A /* VideoMetalViewMac.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = VideoMetalViewMac.mm; path = ../../submodules/tgcalls/tgcalls/platform/darwin/VideoMetalViewMac.mm; sourceTree = ""; }; + D0D751A624BE3A130037D73A /* TGRTCDefaultVideoEncoderFactory.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = TGRTCDefaultVideoEncoderFactory.mm; path = ../../submodules/tgcalls/tgcalls/platform/darwin/TGRTCDefaultVideoEncoderFactory.mm; sourceTree = ""; }; + D0D751A824BE3A140037D73A /* VideoCapturerInterfaceImpl.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = VideoCapturerInterfaceImpl.mm; path = ../../submodules/tgcalls/tgcalls/platform/darwin/VideoCapturerInterfaceImpl.mm; sourceTree = ""; }; + D0D751A924BE3A140037D73A /* TGRTCVideoEncoderH265.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = TGRTCVideoEncoderH265.mm; path = ../../submodules/tgcalls/tgcalls/platform/darwin/TGRTCVideoEncoderH265.mm; sourceTree = ""; }; + D0D751AA24BE3A140037D73A /* TGRTCVideoDecoderH265.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = TGRTCVideoDecoderH265.mm; path = ../../submodules/tgcalls/tgcalls/platform/darwin/TGRTCVideoDecoderH265.mm; sourceTree = ""; }; + D0D751AB24BE3A140037D73A /* TGRTCDefaultVideoDecoderFactory.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = TGRTCDefaultVideoDecoderFactory.mm; path = ../../submodules/tgcalls/tgcalls/platform/darwin/TGRTCDefaultVideoDecoderFactory.mm; sourceTree = ""; }; + D0D751AC24BE3A140037D73A /* VideoCameraCapturerMac.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = VideoCameraCapturerMac.mm; path = ../../submodules/tgcalls/tgcalls/platform/darwin/VideoCameraCapturerMac.mm; sourceTree = ""; }; + D0D751AD24BE3A140037D73A /* DarwinInterface.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = DarwinInterface.mm; path = ../../submodules/tgcalls/tgcalls/platform/darwin/DarwinInterface.mm; sourceTree = ""; }; + D0D751B724BE3AC60037D73A /* NetworkManager.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = NetworkManager.cpp; path = ../../submodules/tgcalls/tgcalls/NetworkManager.cpp; sourceTree = ""; }; + D0D751B824BE3AC60037D73A /* MediaManager.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = MediaManager.cpp; path = ../../submodules/tgcalls/tgcalls/MediaManager.cpp; sourceTree = ""; }; + D0D751B924BE3AC60037D73A /* ThreadLocalObject.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = ThreadLocalObject.cpp; path = ../../submodules/tgcalls/tgcalls/ThreadLocalObject.cpp; sourceTree = ""; }; + D0D751BA24BE3AC70037D73A /* VideoCaptureInterface.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = VideoCaptureInterface.cpp; path = ../../submodules/tgcalls/tgcalls/VideoCaptureInterface.cpp; sourceTree = ""; }; + D0D751BB24BE3AC70037D73A /* CodecSelectHelper.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = CodecSelectHelper.cpp; path = ../../submodules/tgcalls/tgcalls/CodecSelectHelper.cpp; sourceTree = ""; }; + D0D751BC24BE3AC70037D73A /* Manager.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = Manager.cpp; path = ../../submodules/tgcalls/tgcalls/Manager.cpp; sourceTree = ""; }; + D0D751BD24BE3AC70037D73A /* InstanceImpl.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = InstanceImpl.cpp; path = ../../submodules/tgcalls/tgcalls/InstanceImpl.cpp; sourceTree = ""; }; + D0D751BF24BE3AC70037D73A /* VideoCaptureInterfaceImpl.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = VideoCaptureInterfaceImpl.cpp; path = ../../submodules/tgcalls/tgcalls/VideoCaptureInterfaceImpl.cpp; sourceTree = ""; }; + D0D751C024BE3AC70037D73A /* Instance.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = Instance.cpp; path = ../../submodules/tgcalls/tgcalls/Instance.cpp; sourceTree = ""; }; + D0D751CB24BE3AD30037D73A /* InstanceImplLegacy.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = InstanceImplLegacy.cpp; path = ../../submodules/tgcalls/tgcalls/legacy/InstanceImplLegacy.cpp; sourceTree = ""; }; + D0E3685124DAA0E2009896D4 /* TGRTCCVPixelBuffer.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = TGRTCCVPixelBuffer.mm; path = ../../submodules/tgcalls/tgcalls/platform/darwin/TGRTCCVPixelBuffer.mm; sourceTree = ""; }; + D0E3685324DAADF5009896D4 /* TGRTCVideoDecoderH264.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = TGRTCVideoDecoderH264.mm; path = ../../submodules/tgcalls/tgcalls/platform/darwin/TGRTCVideoDecoderH264.mm; sourceTree = ""; }; + D0E3685424DAADF5009896D4 /* TGRTCVideoEncoderH264.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = TGRTCVideoEncoderH264.mm; path = ../../submodules/tgcalls/tgcalls/platform/darwin/TGRTCVideoEncoderH264.mm; sourceTree = ""; }; + D0E7BD1E256A7E0D0068644D /* GroupInstanceImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GroupInstanceImpl.h; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D06E368624A4FD4900C7D03A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 27A0A4E025F764FA00B789AF /* ffmpeg.framework in Frameworks */, + 27D7ABA525EFCF7B008DB916 /* TelegramCore.framework in Frameworks */, + 2702A835259BD7F6007F1DDD /* SSignalKit.framework in Frameworks */, + D0076E90256854E7007EF588 /* webrtc.framework in Frameworks */, + D00B192224E5722A006CCB87 /* GLKit.framework in Frameworks */, + D0D751A424BE372F0037D73A /* AppKit.framework in Frameworks */, + D06E36E324A5001800C7D03A /* AVFoundation.framework in Frameworks */, + D06E36E124A5001300C7D03A /* CoreMedia.framework in Frameworks */, + D06E36DF24A5000700C7D03A /* VideoToolbox.framework in Frameworks */, + D06E36DD24A5000100C7D03A /* AudioToolbox.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2771ADBF2590E63E000B29FF /* capturer */ = { + isa = PBXGroup; + children = ( + 2715040F2631799A004FB0E0 /* DesktopCaptureSourceViewMac.h */, + 2715040E2631799A004FB0E0 /* DesktopCaptureSourceViewMac.mm */, + 271503F12631679B004FB0E0 /* DesktopSharingCapturer.h */, + 271503F02631679B004FB0E0 /* DesktopSharingCapturer.mm */, + 271503EB2631677B004FB0E0 /* DesktopCaptureSource.cpp */, + 2767ADEC25D6DDDC00717AA1 /* DesktopCaptureSourceHelper.h */, + 271503EA2631677B004FB0E0 /* DesktopCaptureSourceHelper.cpp */, + 271503E92631677B004FB0E0 /* DesktopCaptureSourceManager.cpp */, + 2767ADF025D6DDDC00717AA1 /* DesktopCaptureSource.h */, + 2767ADED25D6DDDC00717AA1 /* DesktopCaptureSourceManager.h */, + ); + name = capturer; + sourceTree = ""; + }; + 27C167F826203A56000B8449 /* v2 */ = { + isa = PBXGroup; + children = ( + 2774C563262F19F4006E6F26 /* SignalingEncryption.cpp */, + 2774C564262F19F4006E6F26 /* SignalingEncryption.h */, + 27C167F926203A56000B8449 /* NativeNetworkingImpl.h */, + 27C167FA26203A56000B8449 /* Signaling.h */, + 27C167FB26203A56000B8449 /* NativeNetworkingImpl.cpp */, + 27C167FC26203A56000B8449 /* InstanceV2Impl.h */, + 27C167FD26203A56000B8449 /* InstanceV2Impl.cpp */, + 27C167FE26203A56000B8449 /* Signaling.cpp */, + ); + name = v2; + path = ../../submodules/tgcalls/tgcalls/v2; + sourceTree = ""; + }; + 27DCF4C0267CE3230019EC49 /* macOS */ = { + isa = PBXGroup; + children = ( + 27E6DAF726860663003D6164 /* TGRTCMTLI420Renderer.h */, + 27E6DAF526860663003D6164 /* TGRTCMTLI420Renderer.mm */, + 27E6DAF426860663003D6164 /* TGRTCMTLRenderer.h */, + 27E6DAF826860663003D6164 /* TGRTCMTLRenderer.mm */, + 27E6DAF626860663003D6164 /* TGRTCMTLRenderer+Private.h */, + 27E6DB0F268A10D5003D6164 /* TGRTCMetalContextHolder.h */, + 27E6DB10268A10D5003D6164 /* TGRTCMetalContextHolder.m */, + ); + name = macOS; + path = ../platform/darwin/macOS; + sourceTree = ""; + }; + D06E367F24A4FD4900C7D03A = { + isa = PBXGroup; + children = ( + 27C167F826203A56000B8449 /* v2 */, + 2771ADBF2590E63E000B29FF /* capturer */, + D0E7BD1C256A7E0D0068644D /* group */, + D0D751CB24BE3AD30037D73A /* InstanceImplLegacy.cpp */, + D0D751BB24BE3AC70037D73A /* CodecSelectHelper.cpp */, + 274340F925A5BADE00A71071 /* AudioDeviceHelper.cpp */, + D0D751C024BE3AC70037D73A /* Instance.cpp */, + D0D751BD24BE3AC70037D73A /* InstanceImpl.cpp */, + D0D751BC24BE3AC70037D73A /* Manager.cpp */, + D0D751B824BE3AC60037D73A /* MediaManager.cpp */, + 27863663269C6ECF006D98C6 /* TurnCustomizerImpl.cpp */, + 27863664269C6ECF006D98C6 /* TurnCustomizerImpl.h */, + 27237EC1269C70A400DF4C44 /* SctpDataChannelProviderInterfaceImpl.cpp */, + 27237EC0269C70A400DF4C44 /* SctpDataChannelProviderInterfaceImpl.h */, + D0D751B724BE3AC60037D73A /* NetworkManager.cpp */, + D0D751B924BE3AC60037D73A /* ThreadLocalObject.cpp */, + D0D751BA24BE3AC70037D73A /* VideoCaptureInterface.cpp */, + D0D751BF24BE3AC70037D73A /* VideoCaptureInterfaceImpl.cpp */, + 277CBFAA268B4E9F0082ACA2 /* objc_video_encoder_factory.h */, + 277CBFA9268B4E9F0082ACA2 /* objc_video_encoder_factory.mm */, + 27DF650926AADFB2000753AC /* objc_video_decoder_factory.h */, + 27DF650826AADFB2000753AC /* objc_video_decoder_factory.mm */, + D0D751AD24BE3A140037D73A /* DarwinInterface.mm */, + 27328D8A2660E8AD00E3B7D8 /* DarwinVideoSource.h */, + 27328D8B2660E8AD00E3B7D8 /* DarwinVideoSource.mm */, + D0D751AB24BE3A140037D73A /* TGRTCDefaultVideoDecoderFactory.mm */, + D0D751A624BE3A130037D73A /* TGRTCDefaultVideoEncoderFactory.mm */, + D0D751AA24BE3A140037D73A /* TGRTCVideoDecoderH265.mm */, + D0D751A924BE3A140037D73A /* TGRTCVideoEncoderH265.mm */, + D0E3685324DAADF5009896D4 /* TGRTCVideoDecoderH264.mm */, + D0E3685424DAADF5009896D4 /* TGRTCVideoEncoderH264.mm */, + D0E3685124DAA0E2009896D4 /* TGRTCCVPixelBuffer.mm */, + D0D751AC24BE3A140037D73A /* VideoCameraCapturerMac.mm */, + D0D751A824BE3A140037D73A /* VideoCapturerInterfaceImpl.mm */, + 271999D12681F1E2000DE2B7 /* TGCMIOCapturer.h */, + 271999D02681F1E2000DE2B7 /* TGCMIOCapturer.m */, + 271999D32681F1E2000DE2B7 /* TGCMIODevice.h */, + 271999D42681F1E2000DE2B7 /* TGCMIODevice.mm */, + 271999D52681F1E2000DE2B7 /* VideoCMIOCapture.h */, + 271999D22681F1E2000DE2B7 /* VideoCMIOCapture.mm */, + 273F3236265B97F2005D918E /* VideoSampleBufferViewMac.h */, + 273F3235265B97F2005D918E /* VideoSampleBufferViewMac.mm */, + D0D751A524BE3A130037D73A /* VideoMetalViewMac.mm */, + 274CF52B26987D26009139CC /* FakeAudioDeviceModule.cpp */, + D00B191F24E56FE2006CCB87 /* GLVideoViewMac.mm */, + D00602F824CF3D4F00206226 /* EncryptedConnection.cpp */, + D00602FA24CF3D5C00206226 /* LogSinkImpl.cpp */, + D00602FC24CF3D6600206226 /* CryptoHelper.cpp */, + D00602FE24CF3D7500206226 /* Message.cpp */, + 27D0068125AF26AE00EE3EB1 /* StaticThreads.cpp */, + D06E368B24A4FD4900C7D03A /* TgVoipWebrtc */, + D06E369624A4FD4A00C7D03A /* TgVoipWebrtcTests */, + D06E368A24A4FD4900C7D03A /* Products */, + D06E36A324A4FDA000C7D03A /* Frameworks */, + ); + sourceTree = ""; + }; + D06E368A24A4FD4900C7D03A /* Products */ = { + isa = PBXGroup; + children = ( + D06E368924A4FD4900C7D03A /* TgVoipWebrtc.framework */, + ); + name = Products; + sourceTree = ""; + }; + D06E368B24A4FD4900C7D03A /* TgVoipWebrtc */ = { + isa = PBXGroup; + children = ( + D0D751A124BE34B40037D73A /* OngoingCallThreadLocalContext.h */, + D0D7519F24BE34AB0037D73A /* OngoingCallThreadLocalContext.mm */, + D06E368C24A4FD4900C7D03A /* TgVoipWebrtc.h */, + D06E368D24A4FD4900C7D03A /* Info.plist */, + ); + path = TgVoipWebrtc; + sourceTree = ""; + }; + D06E369624A4FD4A00C7D03A /* TgVoipWebrtcTests */ = { + isa = PBXGroup; + children = ( + D06E369724A4FD4A00C7D03A /* TgVoipWebrtcTests.swift */, + D06E369924A4FD4A00C7D03A /* Info.plist */, + ); + path = TgVoipWebrtcTests; + sourceTree = ""; + }; + D06E36A324A4FDA000C7D03A /* Frameworks */ = { + isa = PBXGroup; + children = ( + 27E4344C2680DF1700B05CB1 /* CoreMediaMacCapture.framework */, + 27A0A4DF25F764FA00B789AF /* ffmpeg.framework */, + 27D7ABA425EFCF7B008DB916 /* TelegramCore.framework */, + 2702A834259BD7F6007F1DDD /* SSignalKit.framework */, + D0076E3C25685056007EF588 /* webrtc.framework */, + D00B192124E5722A006CCB87 /* GLKit.framework */, + D0D751A324BE372F0037D73A /* AppKit.framework */, + D06E38A624A5EEAD00C7D03A /* Metal.framework */, + D06E38A724A5EEAD00C7D03A /* MetalKit.framework */, + D06E36E224A5001800C7D03A /* AVFoundation.framework */, + D06E36E024A5001300C7D03A /* CoreMedia.framework */, + D06E36DE24A5000700C7D03A /* VideoToolbox.framework */, + D06E36DC24A5000100C7D03A /* AudioToolbox.framework */, + D06E36A624A4FDAC00C7D03A /* libmac_framework_objc_static.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + D0E7BD1C256A7E0D0068644D /* group */ = { + isa = PBXGroup; + children = ( + 27DCF4C0267CE3230019EC49 /* macOS */, + 274BB567263AE0AA00620D03 /* GroupJoinPayload.h */, + 274BB566263AE0AA00620D03 /* GroupJoinPayloadInternal.cpp */, + 274BB565263AE0AA00620D03 /* GroupJoinPayloadInternal.h */, + 27335F0625C833D300FD040C /* GroupInstanceCustomImpl.cpp */, + 27335F0725C833D300FD040C /* GroupInstanceCustomImpl.h */, + 27D0067B25AF266400EE3EB1 /* GroupNetworkManager.cpp */, + 27D0067C25AF266400EE3EB1 /* GroupNetworkManager.h */, + D0E7BD1E256A7E0D0068644D /* GroupInstanceImpl.h */, + 270C398326CE57730013E395 /* StreamingMediaContext.cpp */, + 270C398226CE57730013E395 /* StreamingMediaContext.h */, + 2728991126CBB77600F4D288 /* AudioStreamingPart.cpp */, + 2728991326CBB77600F4D288 /* AudioStreamingPart.h */, + 2728991026CBB77600F4D288 /* VideoStreamingPart.cpp */, + 2728991226CBB77600F4D288 /* VideoStreamingPart.h */, + ); + name = group; + path = ../../submodules/tgcalls/tgcalls/group; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + D06E368424A4FD4900C7D03A /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D0E7BD20256A7E0D0068644D /* GroupInstanceImpl.h in Headers */, + 271999D72681F1E2000DE2B7 /* TGCMIOCapturer.h in Headers */, + 2728991726CBB77600F4D288 /* AudioStreamingPart.h in Headers */, + D0D751A224BE34B40037D73A /* OngoingCallThreadLocalContext.h in Headers */, + 271504112631799A004FB0E0 /* DesktopCaptureSourceViewMac.h in Headers */, + 277CBFAC268B4E9F0082ACA2 /* objc_video_encoder_factory.h in Headers */, + 27237EC2269C70A400DF4C44 /* SctpDataChannelProviderInterfaceImpl.h in Headers */, + 271999DB2681F1E2000DE2B7 /* VideoCMIOCapture.h in Headers */, + 2774C566262F19F4006E6F26 /* SignalingEncryption.h in Headers */, + 27DF650B26AADFB2000753AC /* objc_video_decoder_factory.h in Headers */, + 274BB568263AE0AA00620D03 /* GroupJoinPayloadInternal.h in Headers */, + 271999D92681F1E2000DE2B7 /* TGCMIODevice.h in Headers */, + 2767ADFA25D6DDDC00717AA1 /* DesktopCaptureSource.h in Headers */, + 2728991626CBB77600F4D288 /* VideoStreamingPart.h in Headers */, + 27863666269C6ECF006D98C6 /* TurnCustomizerImpl.h in Headers */, + 2767ADF725D6DDDC00717AA1 /* DesktopCaptureSourceManager.h in Headers */, + 27C1680226203A56000B8449 /* InstanceV2Impl.h in Headers */, + 27C167FF26203A56000B8449 /* NativeNetworkingImpl.h in Headers */, + 273F3238265B97F2005D918E /* VideoSampleBufferViewMac.h in Headers */, + 27E6DAFB26860663003D6164 /* TGRTCMTLRenderer+Private.h in Headers */, + 2767ADF625D6DDDC00717AA1 /* DesktopCaptureSourceHelper.h in Headers */, + 274BB56A263AE0AA00620D03 /* GroupJoinPayload.h in Headers */, + 27D0067E25AF266400EE3EB1 /* GroupNetworkManager.h in Headers */, + 27E6DAFC26860663003D6164 /* TGRTCMTLI420Renderer.h in Headers */, + 27328D8C2660E8AD00E3B7D8 /* DarwinVideoSource.h in Headers */, + 271503F52631679B004FB0E0 /* DesktopSharingCapturer.h in Headers */, + 27E6DAF926860663003D6164 /* TGRTCMTLRenderer.h in Headers */, + 27335F0925C833D400FD040C /* GroupInstanceCustomImpl.h in Headers */, + 270C398426CE57730013E395 /* StreamingMediaContext.h in Headers */, + 27E6DB11268A10D5003D6164 /* TGRTCMetalContextHolder.h in Headers */, + D06E369A24A4FD4A00C7D03A /* TgVoipWebrtc.h in Headers */, + 27C1680026203A56000B8449 /* Signaling.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + D06E368824A4FD4900C7D03A /* TgVoipWebrtc */ = { + isa = PBXNativeTarget; + buildConfigurationList = D06E369D24A4FD4A00C7D03A /* Build configuration list for PBXNativeTarget "TgVoipWebrtc" */; + buildPhases = ( + D06E368424A4FD4900C7D03A /* Headers */, + D06E368524A4FD4900C7D03A /* Sources */, + D06E368624A4FD4900C7D03A /* Frameworks */, + D06E368724A4FD4900C7D03A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = TgVoipWebrtc; + productName = TgVoipWebrtc; + productReference = D06E368924A4FD4900C7D03A /* TgVoipWebrtc.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D06E368024A4FD4900C7D03A /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1030; + LastUpgradeCheck = 1030; + ORGANIZATIONNAME = "Mikhail Filimonov"; + TargetAttributes = { + D06E368824A4FD4900C7D03A = { + CreatedOnToolsVersion = 10.3; + LastSwiftMigration = 1250; + }; + }; + }; + buildConfigurationList = D06E368324A4FD4900C7D03A /* Build configuration list for PBXProject "TgVoipWebrtc" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = D06E367F24A4FD4900C7D03A; + productRefGroup = D06E368A24A4FD4900C7D03A /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D06E368824A4FD4900C7D03A /* TgVoipWebrtc */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D06E368724A4FD4900C7D03A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D06E368524A4FD4900C7D03A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 27D0068325AF26AE00EE3EB1 /* StaticThreads.cpp in Sources */, + 27328D8D2660E8AD00E3B7D8 /* DarwinVideoSource.mm in Sources */, + 2728991526CBB77600F4D288 /* AudioStreamingPart.cpp in Sources */, + 2728991426CBB77600F4D288 /* VideoStreamingPart.cpp in Sources */, + 27E6DB12268A10D5003D6164 /* TGRTCMetalContextHolder.m in Sources */, + 271503F42631679B004FB0E0 /* DesktopSharingCapturer.mm in Sources */, + D0D751C124BE3AC70037D73A /* NetworkManager.cpp in Sources */, + D0D751C224BE3AC70037D73A /* MediaManager.cpp in Sources */, + 270C398526CE57730013E395 /* StreamingMediaContext.cpp in Sources */, + 27237EC3269C70A400DF4C44 /* SctpDataChannelProviderInterfaceImpl.cpp in Sources */, + D00602FD24CF3D6600206226 /* CryptoHelper.cpp in Sources */, + 277CBFAB268B4E9F0082ACA2 /* objc_video_encoder_factory.mm in Sources */, + 27E6DAFA26860663003D6164 /* TGRTCMTLI420Renderer.mm in Sources */, + D0D751C324BE3AC70037D73A /* ThreadLocalObject.cpp in Sources */, + D00602FF24CF3D7500206226 /* Message.cpp in Sources */, + D0D751C424BE3AC70037D73A /* VideoCaptureInterface.cpp in Sources */, + 271503ED2631677B004FB0E0 /* DesktopCaptureSourceHelper.cpp in Sources */, + 271999D82681F1E2000DE2B7 /* VideoCMIOCapture.mm in Sources */, + 271504102631799A004FB0E0 /* DesktopCaptureSourceViewMac.mm in Sources */, + 271503EE2631677B004FB0E0 /* DesktopCaptureSource.cpp in Sources */, + D0D751C524BE3AC70037D73A /* CodecSelectHelper.cpp in Sources */, + 274CF52C26987D26009139CC /* FakeAudioDeviceModule.cpp in Sources */, + 27C1680326203A56000B8449 /* InstanceV2Impl.cpp in Sources */, + 2774C565262F19F4006E6F26 /* SignalingEncryption.cpp in Sources */, + D0D751C624BE3AC70037D73A /* Manager.cpp in Sources */, + 27863665269C6ECF006D98C6 /* TurnCustomizerImpl.cpp in Sources */, + D0D751C724BE3AC70037D73A /* InstanceImpl.cpp in Sources */, + 271999D62681F1E2000DE2B7 /* TGCMIOCapturer.m in Sources */, + D0D751C924BE3AC70037D73A /* VideoCaptureInterfaceImpl.cpp in Sources */, + 27DF650A26AADFB2000753AC /* objc_video_decoder_factory.mm in Sources */, + D0D751CA24BE3AC70037D73A /* Instance.cpp in Sources */, + 27E6DAFD26860663003D6164 /* TGRTCMTLRenderer.mm in Sources */, + 274340FA25A5BADE00A71071 /* AudioDeviceHelper.cpp in Sources */, + D0D751A024BE34AB0037D73A /* OngoingCallThreadLocalContext.mm in Sources */, + D0D751AE24BE3A140037D73A /* VideoMetalViewMac.mm in Sources */, + D0D751AF24BE3A140037D73A /* TGRTCDefaultVideoEncoderFactory.mm in Sources */, + D00B192024E56FE2006CCB87 /* GLVideoViewMac.mm in Sources */, + 271503EC2631677B004FB0E0 /* DesktopCaptureSourceManager.cpp in Sources */, + D0E3685224DAA0E2009896D4 /* TGRTCCVPixelBuffer.mm in Sources */, + D0D751B124BE3A140037D73A /* VideoCapturerInterfaceImpl.mm in Sources */, + D0D751B224BE3A140037D73A /* TGRTCVideoEncoderH265.mm in Sources */, + D0D751B324BE3A140037D73A /* TGRTCVideoDecoderH265.mm in Sources */, + 27335F0825C833D400FD040C /* GroupInstanceCustomImpl.cpp in Sources */, + 27C1680126203A56000B8449 /* NativeNetworkingImpl.cpp in Sources */, + D0D751B424BE3A140037D73A /* TGRTCDefaultVideoDecoderFactory.mm in Sources */, + D0D751B524BE3A140037D73A /* VideoCameraCapturerMac.mm in Sources */, + D0E3685524DAADF5009896D4 /* TGRTCVideoDecoderH264.mm in Sources */, + 27D0067D25AF266400EE3EB1 /* GroupNetworkManager.cpp in Sources */, + 274BB569263AE0AA00620D03 /* GroupJoinPayloadInternal.cpp in Sources */, + D0E3685624DAADF5009896D4 /* TGRTCVideoEncoderH264.mm in Sources */, + 27C1680426203A56000B8449 /* Signaling.cpp in Sources */, + 273F3237265B97F2005D918E /* VideoSampleBufferViewMac.mm in Sources */, + D00602FB24CF3D5C00206226 /* LogSinkImpl.cpp in Sources */, + D0D751B624BE3A140037D73A /* DarwinInterface.mm in Sources */, + D00602F924CF3D4F00206226 /* EncryptedConnection.cpp in Sources */, + 271999DA2681F1E2000DE2B7 /* TGCMIODevice.mm in Sources */, + D0D751CC24BE3AD40037D73A /* InstanceImplLegacy.cpp in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + D06E369B24A4FD4A00C7D03A /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + D06E369C24A4FD4A00C7D03A /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + D06E369E24A4FD4A00C7D03A /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = N8RBWJ5X6J; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_PREPROCESSOR_DEFINITIONS = ( + WEBRTC_POSIX, + WEBRTC_MAC, + RTC_ENABLE_VP9, + "TGVOIP_NAMESPACE=tgvoip_webrtc", + ); + HEADER_SEARCH_PATHS = ( + "$(PROJECT_DIR)/../webrtc/build/src/third_party/abseil-cpp", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/components/renderer/metal", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/components/video_codec", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/base", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/api/video_codec", + "$(PROJECT_DIR)/../webrtc/build/src/third_party/libyuv/include", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/components/renderer/opengl", + "$(PROJECT_DIR)/../webrtc/build/src/", + "$(PROJECT_DIR)/", + "$(PROJECT_DIR)/../../submodules/libtgvoip", + "$(PROJECT_DIR)/../OpenSSLEncryption/build/openssl/include", + "$(PROJECT_DIR)/../../submodules/tgcalls/tgcalls", + "$(PROJECT_DIR)/../libopus/build/libopus/include", + "$(PROJECT_DIR)/../../submodules/telegram-ios/submodules/OpusBinding/Sources", + $PROJECT_DIR/../ffmpeg/build/ffmpeg/include, + "$(PROJECT_DIR)/../../submodules/telegram-ios/third-party/rnnoise/PublicHeaders", + "$(PROJECT_DIR)/../../submodules/tgcalls", + ); + INFOPLIST_FILE = TgVoipWebrtc/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + ); + LLVM_LTO = YES_THIN; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TgVoipWebrtc; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = DebugAppStore; + }; + D06E369F24A4FD4A00C7D03A /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = N8RBWJ5X6J; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_PREPROCESSOR_DEFINITIONS = ( + "TGVOIP_NAMESPACE=tgvoip_webrtc", + WEBRTC_POSIX, + WEBRTC_MAC, + RTC_ENABLE_VP9, + ); + HEADER_SEARCH_PATHS = ( + "$(PROJECT_DIR)/../webrtc/build/src/third_party/abseil-cpp", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/components/renderer/metal", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/components/video_codec", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/base", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/api/video_codec", + "$(PROJECT_DIR)/../webrtc/build/src/third_party/libyuv/include", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/components/renderer/opengl", + "$(PROJECT_DIR)/../webrtc/build/src/", + "$(PROJECT_DIR)/", + "$(PROJECT_DIR)/../../submodules/libtgvoip", + "$(PROJECT_DIR)/../OpenSSLEncryption/build/openssl/include", + "$(PROJECT_DIR)/../../submodules/tgcalls/tgcalls", + "$(PROJECT_DIR)/../libopus/build/libopus/include", + "$(PROJECT_DIR)/../../submodules/telegram-ios/submodules/OpusBinding/Sources", + $PROJECT_DIR/../ffmpeg/build/ffmpeg/include, + "$(PROJECT_DIR)/../../submodules/telegram-ios/third-party/rnnoise/PublicHeaders", + "$(PROJECT_DIR)/../../submodules/tgcalls", + ); + INFOPLIST_FILE = TgVoipWebrtc/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + ); + LLVM_LTO = YES_THIN; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + OTHER_CFLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TgVoipWebrtc; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = ReleaseAppStore; + }; + D0D751CD24BF0A950037D73A /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + D0D751CE24BF0A950037D73A /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = N8RBWJ5X6J; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_OPTIMIZATION_LEVEL = s; + GCC_PREPROCESSOR_DEFINITIONS = ( + WEBRTC_POSIX, + WEBRTC_MAC, + RTC_ENABLE_VP9, + "TGVOIP_NAMESPACE=tgvoip_webrtc", + ); + HEADER_SEARCH_PATHS = ( + "$(PROJECT_DIR)/../webrtc/build/src/third_party/abseil-cpp", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/components/renderer/metal", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/components/video_codec", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/base", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/api/video_codec", + "$(PROJECT_DIR)/../webrtc/build/src/third_party/libyuv/include", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/components/renderer/opengl", + "$(PROJECT_DIR)/../webrtc/build/src/", + "$(PROJECT_DIR)/", + "$(PROJECT_DIR)/../../submodules/libtgvoip", + "$(PROJECT_DIR)/../OpenSSLEncryption/build/openssl/include", + "$(PROJECT_DIR)/../../submodules/tgcalls/tgcalls", + "$(PROJECT_DIR)/../libopus/build/libopus/include", + "$(PROJECT_DIR)/../../submodules/telegram-ios/submodules/OpusBinding/Sources", + $PROJECT_DIR/../ffmpeg/build/ffmpeg/include, + "$(PROJECT_DIR)/../../submodules/telegram-ios/third-party/rnnoise/PublicHeaders", + "$(PROJECT_DIR)/../../submodules/tgcalls", + ); + INFOPLIST_FILE = TgVoipWebrtc/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + ); + LLVM_LTO = YES_THIN; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + OTHER_CFLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TgVoipWebrtc; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_VERSION = 5.0; + }; + name = DebugHockeyapp; + }; + D0D751CF24BF0AB50037D73A /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + D0D751D024BF0AB50037D73A /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = N8RBWJ5X6J; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_OPTIMIZATION_LEVEL = s; + GCC_PREPROCESSOR_DEFINITIONS = ( + WEBRTC_POSIX, + WEBRTC_MAC, + RTC_ENABLE_VP9, + "TGVOIP_NAMESPACE=tgvoip_webrtc", + ); + HEADER_SEARCH_PATHS = ( + "$(PROJECT_DIR)/../webrtc/build/src/third_party/abseil-cpp", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/components/renderer/metal", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/components/video_codec", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/base", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/api/video_codec", + "$(PROJECT_DIR)/../webrtc/build/src/third_party/libyuv/include", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/components/renderer/opengl", + "$(PROJECT_DIR)/../webrtc/build/src/", + "$(PROJECT_DIR)/", + "$(PROJECT_DIR)/../../submodules/libtgvoip", + "$(PROJECT_DIR)/../OpenSSLEncryption/build/openssl/include", + "$(PROJECT_DIR)/../../submodules/tgcalls/tgcalls", + "$(PROJECT_DIR)/../libopus/build/libopus/include", + "$(PROJECT_DIR)/../../submodules/telegram-ios/submodules/OpusBinding/Sources", + $PROJECT_DIR/../ffmpeg/build/ffmpeg/include, + "$(PROJECT_DIR)/../../submodules/telegram-ios/third-party/rnnoise/PublicHeaders", + "$(PROJECT_DIR)/../../submodules/tgcalls", + ); + INFOPLIST_FILE = TgVoipWebrtc/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + ); + LLVM_LTO = YES_THIN; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + OTHER_CFLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TgVoipWebrtc; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_VERSION = 5.0; + }; + name = HockeyappMacAlpha; + }; + D0D751D124BF0ABA0037D73A /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + D0D751D224BF0ABA0037D73A /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = N8RBWJ5X6J; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_PREPROCESSOR_DEFINITIONS = ( + "TGVOIP_NAMESPACE=tgvoip_webrtc", + WEBRTC_POSIX, + WEBRTC_MAC, + RTC_ENABLE_VP9, + ); + HEADER_SEARCH_PATHS = ( + "$(PROJECT_DIR)/../webrtc/build/src/third_party/abseil-cpp", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/components/renderer/metal", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/components/video_codec", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/base", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/api/video_codec", + "$(PROJECT_DIR)/../webrtc/build/src/third_party/libyuv/include", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/components/renderer/opengl", + "$(PROJECT_DIR)/../webrtc/build/src/", + "$(PROJECT_DIR)/", + "$(PROJECT_DIR)/../../submodules/libtgvoip", + "$(PROJECT_DIR)/../OpenSSLEncryption/build/openssl/include", + "$(PROJECT_DIR)/../../submodules/tgcalls/tgcalls", + "$(PROJECT_DIR)/../libopus/build/libopus/include", + "$(PROJECT_DIR)/../../submodules/telegram-ios/submodules/OpusBinding/Sources", + $PROJECT_DIR/../ffmpeg/build/ffmpeg/include, + "$(PROJECT_DIR)/../../submodules/telegram-ios/third-party/rnnoise/PublicHeaders", + "$(PROJECT_DIR)/../../submodules/tgcalls", + ); + INFOPLIST_FILE = TgVoipWebrtc/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + ); + LLVM_LTO = YES_THIN; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + OTHER_CFLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TgVoipWebrtc; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = ReleaseHockeyapp; + }; + D0D751D324BF0ACE0037D73A /* Github */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + D0D751D424BF0ACE0037D73A /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = N8RBWJ5X6J; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_PREPROCESSOR_DEFINITIONS = ( + WEBRTC_POSIX, + WEBRTC_MAC, + RTC_ENABLE_VP9, + "TGVOIP_NAMESPACE=tgvoip_webrtc", + ); + HEADER_SEARCH_PATHS = ( + "$(PROJECT_DIR)/../webrtc/build/src/third_party/abseil-cpp", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/components/renderer/metal", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/components/video_codec", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/base", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/api/video_codec", + "$(PROJECT_DIR)/../webrtc/build/src/third_party/libyuv/include", + "$(PROJECT_DIR)/../webrtc/build/src/sdk/objc/components/renderer/opengl", + "$(PROJECT_DIR)/../webrtc/build/src/", + "$(PROJECT_DIR)/", + "$(PROJECT_DIR)/../../submodules/libtgvoip", + "$(PROJECT_DIR)/../OpenSSLEncryption/build/openssl/include", + "$(PROJECT_DIR)/../../submodules/tgcalls/tgcalls", + "$(PROJECT_DIR)/../libopus/build/libopus/include", + "$(PROJECT_DIR)/../../submodules/telegram-ios/submodules/OpusBinding/Sources", + $PROJECT_DIR/../ffmpeg/build/ffmpeg/include, + "$(PROJECT_DIR)/../../submodules/telegram-ios/third-party/rnnoise/PublicHeaders", + "$(PROJECT_DIR)/../../submodules/tgcalls", + ); + INFOPLIST_FILE = TgVoipWebrtc/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + ); + LLVM_LTO = YES_THIN; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + OTHER_CFLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TgVoipWebrtc; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Github; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D06E368324A4FD4900C7D03A /* Build configuration list for PBXProject "TgVoipWebrtc" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D06E369B24A4FD4A00C7D03A /* DebugAppStore */, + D0D751D324BF0ACE0037D73A /* Github */, + D0D751CD24BF0A950037D73A /* DebugHockeyapp */, + D0D751CF24BF0AB50037D73A /* HockeyappMacAlpha */, + D06E369C24A4FD4A00C7D03A /* ReleaseAppStore */, + D0D751D124BF0ABA0037D73A /* ReleaseHockeyapp */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; + D06E369D24A4FD4A00C7D03A /* Build configuration list for PBXNativeTarget "TgVoipWebrtc" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D06E369E24A4FD4A00C7D03A /* DebugAppStore */, + D0D751D424BF0ACE0037D73A /* Github */, + D0D751CE24BF0A950037D73A /* DebugHockeyapp */, + D0D751D024BF0AB50037D73A /* HockeyappMacAlpha */, + D06E369F24A4FD4A00C7D03A /* ReleaseAppStore */, + D0D751D224BF0ABA0037D73A /* ReleaseHockeyapp */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; +/* End XCConfigurationList section */ + }; + rootObject = D06E368024A4FD4900C7D03A /* Project object */; +} diff --git a/core-xprojects/TgVoipWebrtc/TgVoipWebrtc.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/TgVoipWebrtc/TgVoipWebrtc.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..a7a466f55f --- /dev/null +++ b/core-xprojects/TgVoipWebrtc/TgVoipWebrtc.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/TgVoipWebrtc/TgVoipWebrtc.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/TgVoipWebrtc/TgVoipWebrtc.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/TgVoipWebrtc/TgVoipWebrtc.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/TgVoipWebrtc/TgVoipWebrtc.xcodeproj/xcshareddata/xcschemes/TgVoipWebrtc.xcscheme b/core-xprojects/TgVoipWebrtc/TgVoipWebrtc.xcodeproj/xcshareddata/xcschemes/TgVoipWebrtc.xcscheme new file mode 100644 index 0000000000..7114119354 --- /dev/null +++ b/core-xprojects/TgVoipWebrtc/TgVoipWebrtc.xcodeproj/xcshareddata/xcschemes/TgVoipWebrtc.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/TgVoipWebrtc/TgVoipWebrtc/Info.plist b/core-xprojects/TgVoipWebrtc/TgVoipWebrtc/Info.plist new file mode 100644 index 0000000000..d74d13d71b --- /dev/null +++ b/core-xprojects/TgVoipWebrtc/TgVoipWebrtc/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2020 Mikhail Filimonov. All rights reserved. + + diff --git a/core-xprojects/TgVoipWebrtc/TgVoipWebrtc/OngoingCallThreadLocalContextWebrtc.h b/core-xprojects/TgVoipWebrtc/TgVoipWebrtc/OngoingCallThreadLocalContextWebrtc.h new file mode 100644 index 0000000000..d301adeebe --- /dev/null +++ b/core-xprojects/TgVoipWebrtc/TgVoipWebrtc/OngoingCallThreadLocalContextWebrtc.h @@ -0,0 +1,120 @@ +#ifndef OngoingCallContext_h +#define OngoingCallContext_h + +#import + +@interface OngoingCallConnectionDescriptionWebrtc : NSObject + +@property (nonatomic, readonly) int64_t connectionId; +@property (nonatomic, strong, readonly) NSString * _Nonnull ip; +@property (nonatomic, strong, readonly) NSString * _Nonnull ipv6; +@property (nonatomic, readonly) int32_t port; +@property (nonatomic, strong, readonly) NSData * _Nonnull peerTag; + +- (instancetype _Nonnull)initWithConnectionId:(int64_t)connectionId ip:(NSString * _Nonnull)ip ipv6:(NSString * _Nonnull)ipv6 port:(int32_t)port peerTag:(NSData * _Nonnull)peerTag; + +@end + +typedef NS_ENUM(int32_t, OngoingCallStateWebrtc) { + OngoingCallStateInitializingWebrtc, + OngoingCallStateConnectedWebrtc, + OngoingCallStateFailedWebrtc, + OngoingCallStateReconnectingWebrtc +}; + +typedef NS_ENUM(int32_t, OngoingCallVideoStateWebrtc) { + OngoingCallVideoStatePossibleWebrtc, + OngoingCallVideoStateOutgoingRequestedWebrtc, + OngoingCallVideoStateIncomingRequestedWebrtc, + OngoingCallVideoStateActiveWebrtc +}; + + +typedef NS_ENUM(int32_t, OngoingCallRemoteVideoStateWebrtc) { + OngoingCallRemoteVideoStateInactiveWebrtc, + OngoingCallRemoteVideoStateActiveWebrtc +}; + +typedef NS_ENUM(int32_t, OngoingCallNetworkTypeWebrtc) { + OngoingCallNetworkTypeWifiWebrtc, +}; + +typedef NS_ENUM(int32_t, OngoingCallDataSavingWebrtc) { + OngoingCallDataSavingNeverWebrtc, + OngoingCallDataSavingCellularWebrtc, + OngoingCallDataSavingAlwaysWebrtc +}; + +@protocol OngoingCallThreadLocalContextQueueWebrtc + +- (void)dispatch:(void (^ _Nonnull)())f; +- (bool)isCurrent; + +@end + +@interface VoipProxyServerWebrtc : NSObject + +@property (nonatomic, strong, readonly) NSString * _Nonnull host; +@property (nonatomic, readonly) int32_t port; +@property (nonatomic, strong, readonly) NSString * _Nullable username; +@property (nonatomic, strong, readonly) NSString * _Nullable password; + +- (instancetype _Nonnull)initWithHost:(NSString * _Nonnull)host port:(int32_t)port username:(NSString * _Nullable)username password:(NSString * _Nullable)password; + +@end + +@interface VoipRtcServerWebrtc : NSObject + +@property (nonatomic, strong, readonly) NSString * _Nonnull host; +@property (nonatomic, readonly) int32_t port; +@property (nonatomic, strong, readonly) NSString * _Nullable username; +@property (nonatomic, strong, readonly) NSString * _Nullable password; +@property (nonatomic, readonly) bool isTurn; + +- (instancetype _Nonnull)initWithHost:(NSString * _Nonnull)host port:(int32_t)port username:(NSString * _Nullable)username password:(NSString * _Nullable)password isTurn:(bool)isTurn; + +@end + +@interface OngoingCallThreadLocalContextVideoCapturer : NSObject + +- (instancetype _Nonnull)init; + +- (void)switchVideoCamera; +- (void)setIsVideoEnabled:(bool)isVideoEnabled; + +- (void)makeOutgoingVideoView:(void (^_Nonnull)(NSView * _Nullable))completion; + +@end + + +@interface OngoingCallThreadLocalContextWebrtc : NSObject + ++ (void)setupLoggingFunction:(void (* _Nullable)(NSString * _Nullable))loggingFunction; ++ (void)applyServerConfig:(NSString * _Nullable)data; ++ (int32_t)maxLayer; ++ (NSString * _Nonnull)version; + +@property (nonatomic, copy) void (^ _Nullable stateChanged)(OngoingCallStateWebrtc, OngoingCallVideoStateWebrtc, OngoingCallRemoteVideoStateWebrtc); +@property (nonatomic, copy) void (^ _Nullable signalBarsChanged)(int32_t); + +- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue proxy:(VoipProxyServerWebrtc * _Nullable)proxy rtcServers:(NSArray * _Nonnull)rtcServers networkType:(OngoingCallNetworkTypeWebrtc)networkType dataSaving:(OngoingCallDataSavingWebrtc)dataSaving derivedState:(NSData * _Nonnull)derivedState key:(NSData * _Nonnull)key isOutgoing:(bool)isOutgoing primaryConnection:(OngoingCallConnectionDescriptionWebrtc * _Nonnull)primaryConnection alternativeConnections:(NSArray * _Nonnull)alternativeConnections maxLayer:(int32_t)maxLayer allowP2P:(BOOL)allowP2P logPath:(NSString * _Nonnull)logPath sendSignalingData:(void (^ _Nonnull)(NSData * _Nonnull))sendSignalingData videoCapturer:(OngoingCallThreadLocalContextVideoCapturer * _Nullable)videoCapturer; + +- (void)stop:(void (^_Nullable)(NSString * _Nullable debugLog, int64_t bytesSentWifi, int64_t bytesReceivedWifi, int64_t bytesSentMobile, int64_t bytesReceivedMobile))completion; + +- (bool)needRate; + +- (NSString * _Nullable)debugInfo; +- (NSString * _Nullable)version; +- (NSData * _Nonnull)getDerivedState; + +- (void)setIsMuted:(bool)isMuted; +- (void)setVideoEnabled:(bool)videoEnabled; +- (void)switchVideoCamera; +- (void)setNetworkType:(OngoingCallNetworkTypeWebrtc)networkType; +- (void)makeIncomingVideoView:(void (^_Nonnull)(NSView * _Nullable))completion; +- (void)makeOutgoingVideoView:(void (^_Nonnull)(NSView * _Nullable))completion; +- (void)addSignalingData:(NSData * _Nonnull)data; + +@end + +#endif diff --git a/core-xprojects/TgVoipWebrtc/TgVoipWebrtc/OngoingCallThreadLocalContextWebrtc.mm b/core-xprojects/TgVoipWebrtc/TgVoipWebrtc/OngoingCallThreadLocalContextWebrtc.mm new file mode 100644 index 0000000000..5e14f6be3d --- /dev/null +++ b/core-xprojects/TgVoipWebrtc/TgVoipWebrtc/OngoingCallThreadLocalContextWebrtc.mm @@ -0,0 +1,499 @@ +#import "OngoingCallThreadLocalContextWebrtc.h" + +#import "TgVoip.h" +#import +#import "VideoMetalView.h" +using namespace TGVOIP_NAMESPACE; + + +@implementation OngoingCallConnectionDescriptionWebrtc + +- (instancetype _Nonnull)initWithConnectionId:(int64_t)connectionId ip:(NSString * _Nonnull)ip ipv6:(NSString * _Nonnull)ipv6 port:(int32_t)port peerTag:(NSData * _Nonnull)peerTag { + self = [super init]; + if (self != nil) { + _connectionId = connectionId; + _ip = ip; + _ipv6 = ipv6; + _port = port; + _peerTag = peerTag; + } + return self; +} + +@end + + +@interface OngoingCallThreadLocalContextVideoCapturer () { + std::shared_ptr _interface; +} + + @end + +@implementation OngoingCallThreadLocalContextVideoCapturer + +- (instancetype _Nonnull)init { + self = [super init]; + if (self != nil) { + _interface = TgVoipVideoCaptureInterface::makeInstance(); + } + return self; +} + +- (void)switchVideoCamera { + _interface->switchCamera(); +} + +- (void)setIsVideoEnabled:(bool)isVideoEnabled { + _interface->setIsVideoEnabled(isVideoEnabled); +} + +- (std::shared_ptr)getInterface { + return _interface; +} + + + + +- (void)makeOutgoingVideoView:(void (^_Nonnull)(NSView * _Nullable))completion { + std::shared_ptr interface = _interface; + dispatch_async(dispatch_get_main_queue(), ^{ + VideoMetalView *remoteRenderer = [[VideoMetalView alloc] initWithFrame:CGRectZero]; + remoteRenderer.videoContentMode = kCAGravityResizeAspectFill; + + std::shared_ptr> sink = [remoteRenderer getSink]; + interface->setVideoOutput(sink); + + completion(remoteRenderer); + }); +} + + @end + + +@interface OngoingCallThreadLocalContextWebrtc () { + id _queue; + int32_t _contextId; + + OngoingCallNetworkTypeWebrtc _networkType; + NSTimeInterval _callReceiveTimeout; + NSTimeInterval _callRingTimeout; + NSTimeInterval _callConnectTimeout; + NSTimeInterval _callPacketTimeout; + + TgVoip *_tgVoip; + + OngoingCallStateWebrtc _state; + OngoingCallVideoStateWebrtc _videoState; + OngoingCallRemoteVideoStateWebrtc _remoteVideoState; + OngoingCallThreadLocalContextVideoCapturer *_videoCapturer; + + + int32_t _signalBars; + NSData *_lastDerivedState; + + void (^_sendSignalingData)(NSData *); +} + +- (void)controllerStateChanged:(TgVoipState)state; +- (void)signalBarsChanged:(int32_t)signalBars; + +@end + +@implementation VoipProxyServerWebrtc + +- (instancetype _Nonnull)initWithHost:(NSString * _Nonnull)host port:(int32_t)port username:(NSString * _Nullable)username password:(NSString * _Nullable)password { + self = [super init]; + if (self != nil) { + _host = host; + _port = port; + _username = username; + _password = password; + } + return self; +} + +@end + +@implementation VoipRtcServerWebrtc + +- (instancetype _Nonnull)initWithHost:(NSString * _Nonnull)host port:(int32_t)port username:(NSString * _Nullable)username password:(NSString * _Nullable)password isTurn:(bool)isTurn { + self = [super init]; + if (self != nil) { + _host = host; + _port = port; + _username = username; + _password = password; + _isTurn = isTurn; + } + return self; +} + +@end + +static TgVoipNetworkType callControllerNetworkTypeForType(OngoingCallNetworkTypeWebrtc type) { + switch (type) { + case OngoingCallNetworkTypeWifiWebrtc: + return TgVoipNetworkType::WiFi; + default: + return TgVoipNetworkType::ThirdGeneration; + } +} + +static TgVoipDataSaving callControllerDataSavingForType(OngoingCallDataSavingWebrtc type) { + switch (type) { + case OngoingCallDataSavingNeverWebrtc: + return TgVoipDataSaving::Never; + case OngoingCallDataSavingCellularWebrtc: + return TgVoipDataSaving::Mobile; + case OngoingCallDataSavingAlwaysWebrtc: + return TgVoipDataSaving::Always; + default: + return TgVoipDataSaving::Never; + } +} + +@implementation OngoingCallThreadLocalContextWebrtc + +static void (*InternalVoipLoggingFunction)(NSString *) = NULL; + ++ (void)setupLoggingFunction:(void (*)(NSString *))loggingFunction { + InternalVoipLoggingFunction = loggingFunction; + TgVoip::setLoggingFunction([](std::string const &string) { + if (InternalVoipLoggingFunction) { + InternalVoipLoggingFunction([[NSString alloc] initWithUTF8String:string.c_str()]); + } + }); +} + ++ (void)applyServerConfig:(NSString *)string { + if (string.length != 0) { + TgVoip::setGlobalServerConfig(std::string(string.UTF8String)); + } +} + ++ (int32_t)maxLayer { + return 92; +} + ++ (NSString *)version { + return @"2.7.7"; +} + +- (instancetype _Nonnull)initWithQueue:(id _Nonnull)queue proxy:(VoipProxyServerWebrtc * _Nullable)proxy rtcServers:(NSArray * _Nonnull)rtcServers networkType:(OngoingCallNetworkTypeWebrtc)networkType dataSaving:(OngoingCallDataSavingWebrtc)dataSaving derivedState:(NSData * _Nonnull)derivedState key:(NSData * _Nonnull)key isOutgoing:(bool)isOutgoing primaryConnection:(OngoingCallConnectionDescriptionWebrtc * _Nonnull)primaryConnection alternativeConnections:(NSArray * _Nonnull)alternativeConnections maxLayer:(int32_t)maxLayer allowP2P:(BOOL)allowP2P logPath:(NSString * _Nonnull)logPath sendSignalingData:(void (^)(NSData * _Nonnull))sendSignalingData videoCapturer:(OngoingCallThreadLocalContextVideoCapturer * _Nullable)videoCapturer { + self = [super init]; + if (self != nil) { + _queue = queue; + assert([queue isCurrent]); + + _callReceiveTimeout = 20.0; + _callRingTimeout = 90.0; + _callConnectTimeout = 30.0; + _callPacketTimeout = 10.0; + _networkType = networkType; + _sendSignalingData = [sendSignalingData copy]; + _videoCapturer = videoCapturer; + if (videoCapturer != nil) { + _videoState = OngoingCallVideoStateOutgoingRequestedWebrtc; + _remoteVideoState = OngoingCallRemoteVideoStateActiveWebrtc; + } else { + _videoState = OngoingCallVideoStatePossibleWebrtc; + _remoteVideoState = OngoingCallRemoteVideoStateInactiveWebrtc; + } + + + + std::vector derivedStateValue; + derivedStateValue.resize(derivedState.length); + [derivedState getBytes:derivedStateValue.data() length:derivedState.length]; + + std::unique_ptr proxyValue = nullptr; + if (proxy != nil) { + TgVoipProxy *proxyObject = new TgVoipProxy(); + proxyObject->host = proxy.host.UTF8String; + proxyObject->port = (uint16_t)proxy.port; + proxyObject->login = proxy.username.UTF8String ?: ""; + proxyObject->password = proxy.password.UTF8String ?: ""; + proxyValue = std::unique_ptr(proxyObject); + } + + std::vector parsedRtcServers; + for (VoipRtcServerWebrtc *server in rtcServers) { + parsedRtcServers.push_back((TgVoipRtcServer){ + .host = server.host.UTF8String, + .port = (uint16_t)server.port, + .login = server.username.UTF8String, + .password = server.password.UTF8String, + .isTurn = server.isTurn + }); + } +// + /*TgVoipCrypto crypto; + crypto.sha1 = &TGCallSha1; + crypto.sha256 = &TGCallSha256; + crypto.rand_bytes = &TGCallRandomBytes; + crypto.aes_ige_encrypt = &TGCallAesIgeEncrypt; + crypto.aes_ige_decrypt = &TGCallAesIgeDecrypt; + crypto.aes_ctr_encrypt = &TGCallAesCtrEncrypt;*/ + + std::vector endpoints; + NSArray *connections = [@[primaryConnection] arrayByAddingObjectsFromArray:alternativeConnections]; + for (OngoingCallConnectionDescriptionWebrtc *connection in connections) { + unsigned char peerTag[16]; + [connection.peerTag getBytes:peerTag length:16]; + + TgVoipEndpoint endpoint; + endpoint.endpointId = connection.connectionId; + endpoint.host = { + .ipv4 = std::string(connection.ip.UTF8String), + .ipv6 = std::string(connection.ipv6.UTF8String) + }; + endpoint.port = (uint16_t)connection.port; + endpoint.type = TgVoipEndpointType::UdpRelay; + memcpy(endpoint.peerTag, peerTag, 16); + endpoints.push_back(endpoint); + } + + TgVoipConfig config = { + .initializationTimeout = _callConnectTimeout, + .receiveTimeout = _callPacketTimeout, + .dataSaving = callControllerDataSavingForType(dataSaving), + .enableP2P = (bool)allowP2P, + .enableAEC = false, + .enableNS = true, + .enableAGC = true, + .enableCallUpgrade = false, + .logPath = logPath.length == 0 ? "" : std::string(logPath.UTF8String), + .maxApiLayer = [OngoingCallThreadLocalContextWebrtc maxLayer] + }; + + std::vector encryptionKeyValue; + encryptionKeyValue.resize(key.length); + memcpy(encryptionKeyValue.data(), key.bytes, key.length); + + TgVoipEncryptionKey encryptionKey = { + .value = encryptionKeyValue, + .isOutgoing = isOutgoing, + }; + + __weak OngoingCallThreadLocalContextWebrtc *weakSelf = self; + _tgVoip = TgVoip::makeInstance( + config, + { derivedStateValue }, + endpoints, + proxyValue, + parsedRtcServers, + callControllerNetworkTypeForType(networkType), + encryptionKey, + [_videoCapturer getInterface], + [weakSelf, queue](TgVoipState state, TgVoip::VideoState videoState) { + [queue dispatch:^{ + __strong OngoingCallThreadLocalContextWebrtc *strongSelf = weakSelf; + if (strongSelf) { + OngoingCallVideoStateWebrtc mappedVideoState; + switch (videoState) { + case TgVoip::VideoState::possible: + mappedVideoState = OngoingCallVideoStatePossibleWebrtc; + break; + case TgVoip::VideoState::outgoingRequested: + mappedVideoState = OngoingCallVideoStateOutgoingRequestedWebrtc; + break; + case TgVoip::VideoState::incomingRequested: + mappedVideoState = OngoingCallVideoStateIncomingRequestedWebrtc; + break; + case TgVoip::VideoState::active: + mappedVideoState = OngoingCallVideoStateActiveWebrtc; + break; + } + + [strongSelf controllerStateChanged:state videoState:mappedVideoState]; + } + }]; + }, + [weakSelf, queue](bool isActive) { + [queue dispatch:^{ + __strong OngoingCallThreadLocalContextWebrtc *strongSelf = weakSelf; + if (strongSelf) { + OngoingCallRemoteVideoStateWebrtc remoteVideoState; + if (isActive) { + remoteVideoState = OngoingCallRemoteVideoStateActiveWebrtc; + } else { + remoteVideoState = OngoingCallRemoteVideoStateInactiveWebrtc; + } + if (strongSelf->_remoteVideoState != remoteVideoState) { + strongSelf->_remoteVideoState = remoteVideoState; + if (strongSelf->_stateChanged) { + strongSelf->_stateChanged(strongSelf->_state, strongSelf->_videoState, strongSelf->_remoteVideoState); + } + } + } + }]; + }, + [weakSelf, queue](const std::vector &data) { + NSData *mappedData = [[NSData alloc] initWithBytes:data.data() length:data.size()]; + [queue dispatch:^{ + __strong OngoingCallThreadLocalContextWebrtc *strongSelf = weakSelf; + if (strongSelf) { + [strongSelf signalingDataEmitted:mappedData]; + } + }]; + } + ); +// + _state = OngoingCallStateInitializingWebrtc; + _signalBars = -1; + } + return self; +} + +- (void)dealloc { + assert([_queue isCurrent]); + if (_tgVoip != NULL) { + [self stop:nil]; + } +} + +- (bool)needRate { + return false; +} + +- (void)controllerStateChanged:(TgVoipState)state videoState:(OngoingCallVideoStateWebrtc)videoState { + OngoingCallStateWebrtc callState = OngoingCallStateInitializingWebrtc; + switch (state) { + case TgVoipState::Estabilished: + callState = OngoingCallStateConnectedWebrtc; + break; + case TgVoipState::Failed: + callState = OngoingCallStateFailedWebrtc; + break; + case TgVoipState::Reconnecting: + callState = OngoingCallStateReconnectingWebrtc; + break; + default: + break; + } + + if (_state != callState || _videoState != videoState) { + _state = callState; + _videoState = videoState; + + if (_stateChanged) { + _stateChanged(_state, _videoState, _remoteVideoState); + } + } +} + +- (void)stop:(void (^)(NSString *, int64_t, int64_t, int64_t, int64_t))completion { + if (_tgVoip) { + TgVoipFinalState finalState = _tgVoip->stop(); + + NSString *debugLog = [NSString stringWithUTF8String:finalState.debugLog.c_str()]; + _lastDerivedState = [[NSData alloc] initWithBytes:finalState.persistentState.value.data() length:finalState.persistentState.value.size()]; + + delete _tgVoip; + _tgVoip = NULL; + + if (completion) { + completion(debugLog, finalState.trafficStats.bytesSentWifi, finalState.trafficStats.bytesReceivedWifi, finalState.trafficStats.bytesSentMobile, finalState.trafficStats.bytesReceivedMobile); + } + } +} + +- (NSString *)debugInfo { + if (_tgVoip != nil) { + NSString *version = [self version]; + auto rawDebugString = _tgVoip->getDebugInfo(); + return [NSString stringWithUTF8String:rawDebugString.c_str()]; + } else { + return nil; + } +} + +- (NSString *)version { + if (_tgVoip != nil) { + return [NSString stringWithUTF8String:_tgVoip->getVersion().c_str()]; + } else { + return nil; + } +} + +- (NSData * _Nonnull)getDerivedState { + if (_tgVoip) { + auto persistentState = _tgVoip->getPersistentState(); + return [[NSData alloc] initWithBytes:persistentState.value.data() length:persistentState.value.size()]; + } else if (_lastDerivedState != nil) { + return _lastDerivedState; + } else { + return [NSData data]; + } +} + + + +- (void)signalBarsChanged:(int32_t)signalBars { + if (signalBars != _signalBars) { + _signalBars = signalBars; + + if (_signalBarsChanged) { + _signalBarsChanged(signalBars); + } + } +} + +- (void)signalingDataEmitted:(NSData *)data { + if (_sendSignalingData) { + _sendSignalingData(data); + } +} + +- (void)addSignalingData:(NSData *)data { + if (_tgVoip) { + std::vector mappedData; + mappedData.resize(data.length); + [data getBytes:mappedData.data() length:data.length]; + _tgVoip->receiveSignalingData(mappedData); + } +} + +- (void)setIsMuted:(bool)isMuted { + if (_tgVoip) { + _tgVoip->setMuteMicrophone(isMuted); + } +} + + + +- (void)switchVideoCamera { + if (_tgVoip) { + // _tgVoip->switchVideoCamera(); + } +} + +- (void)setNetworkType:(OngoingCallNetworkTypeWebrtc)networkType { + if (_networkType != networkType) { + _networkType = networkType; + if (_tgVoip) { + _tgVoip->setNetworkType(callControllerNetworkTypeForType(networkType)); + } + } +} + +- (void)makeIncomingVideoView:(void (^_Nonnull)(NSView * _Nullable))completion { + if (_tgVoip) { + __weak OngoingCallThreadLocalContextWebrtc *weakSelf = self; + dispatch_async(dispatch_get_main_queue(), ^{ + VideoMetalView *remoteRenderer = [[VideoMetalView alloc] initWithFrame:CGRectZero]; + remoteRenderer.videoContentMode = kCAGravityResizeAspectFill; + + std::shared_ptr> sink = [remoteRenderer getSink]; + __strong OngoingCallThreadLocalContextWebrtc *strongSelf = weakSelf; + if (strongSelf) { + strongSelf->_tgVoip->setIncomingVideoOutput(sink); + } + + completion(remoteRenderer); + }); + } +} + +@end + diff --git a/core-xprojects/TgVoipWebrtc/TgVoipWebrtc/TgVoipWebrtc.h b/core-xprojects/TgVoipWebrtc/TgVoipWebrtc/TgVoipWebrtc.h new file mode 100644 index 0000000000..a0c203b143 --- /dev/null +++ b/core-xprojects/TgVoipWebrtc/TgVoipWebrtc/TgVoipWebrtc.h @@ -0,0 +1,22 @@ +// +// TgVoipWebrtc.h +// TgVoipWebrtc +// +// Created by Mikhail Filimonov on 25/06/2020. +// Copyright © 2020 Mikhail Filimonov. All rights reserved. +// + +#import + +//! Project version number for TgVoipWebrtc. +FOUNDATION_EXPORT double TgVoipWebrtcVersionNumber; + +//! Project version string for TgVoipWebrtc. +FOUNDATION_EXPORT const unsigned char TgVoipWebrtcVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + +#import "OngoingCallThreadLocalContext.h" +//#import "DesktopCaptureSourceManager.h" +#import "DesktopCaptureSourceViewMac.h" diff --git a/core-xprojects/TgVoipWebrtc/TgVoipWebrtcTests/Info.plist b/core-xprojects/TgVoipWebrtc/TgVoipWebrtcTests/Info.plist new file mode 100644 index 0000000000..6c40a6cd0c --- /dev/null +++ b/core-xprojects/TgVoipWebrtc/TgVoipWebrtcTests/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 + 1 + + diff --git a/core-xprojects/TgVoipWebrtc/TgVoipWebrtcTests/TgVoipWebrtcTests.swift b/core-xprojects/TgVoipWebrtc/TgVoipWebrtcTests/TgVoipWebrtcTests.swift new file mode 100644 index 0000000000..725f463aee --- /dev/null +++ b/core-xprojects/TgVoipWebrtc/TgVoipWebrtcTests/TgVoipWebrtcTests.swift @@ -0,0 +1,34 @@ +// +// TgVoipWebrtcTests.swift +// TgVoipWebrtcTests +// +// Created by Mikhail Filimonov on 25/06/2020. +// Copyright © 2020 Mikhail Filimonov. All rights reserved. +// + +import XCTest +@testable import TgVoipWebrtc + +class TgVoipWebrtcTests: XCTestCase { + + override func 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. + } + + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + 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/core-xprojects/crc32/Crc32/Crc32 b/core-xprojects/crc32/Crc32/Crc32 new file mode 100644 index 0000000000..b9b4f486c8 --- /dev/null +++ b/core-xprojects/crc32/Crc32/Crc32 @@ -0,0 +1,9 @@ +#import + +//! Project version number for crc32mac. +FOUNDATION_EXPORT double crc32VersionNumber; + +//! Project version string for crc32mac. +FOUNDATION_EXPORT const unsigned char crc32VersionString[]; + +uint32_t Crc32(const void *bytes, int length); diff --git a/core-xprojects/crc32/Crc32/Crc32.h b/core-xprojects/crc32/Crc32/Crc32.h new file mode 100644 index 0000000000..f890c2c16a --- /dev/null +++ b/core-xprojects/crc32/Crc32/Crc32.h @@ -0,0 +1,9 @@ +#import + +//! Project version number for crc32mac. +FOUNDATION_EXPORT double Crc32VersionNumber; + +//! Project version string for crc32mac. +FOUNDATION_EXPORT const unsigned char Crc32VersionString[]; + +uint32_t Crc32(const void *bytes, int length); diff --git a/core-xprojects/crc32/Crc32/Info.plist b/core-xprojects/crc32/Crc32/Info.plist new file mode 100644 index 0000000000..5371a6e108 --- /dev/null +++ b/core-xprojects/crc32/Crc32/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2019 Telegram Messenger LLP. All rights reserved. + + diff --git a/core-xprojects/crc32/Crc32_Xcode.xcodeproj/project.pbxproj b/core-xprojects/crc32/Crc32_Xcode.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..09a6baacd8 --- /dev/null +++ b/core-xprojects/crc32/Crc32_Xcode.xcodeproj/project.pbxproj @@ -0,0 +1,1049 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + A701F930236C1D9E002ABF81 /* Crc32.h in Headers */ = {isa = PBXBuildFile; fileRef = A701F92F236C1D9E002ABF81 /* Crc32.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D03E463E2306E22E0049C28B /* Crc32.m in Sources */ = {isa = PBXBuildFile; fileRef = D03E45732305CCD20049C28B /* Crc32.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A701F92F236C1D9E002ABF81 /* Crc32.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Crc32.h; sourceTree = ""; }; + D03E45732305CCD20049C28B /* Crc32.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Crc32.m; sourceTree = ""; }; + D03E45762305CCEB0049C28B /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + D03E45782305CCF00049C28B /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; + D03E46322306E0BB0049C28B /* Crc32.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Crc32.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D03E46352306E0BB0049C28B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D03E462F2306E0BB0049C28B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + D03E455A2305CC310049C28B = { + isa = PBXGroup; + children = ( + D03E45662305CC310049C28B /* Sources */, + D03E46332306E0BB0049C28B /* Crc32 */, + D03E45652305CC310049C28B /* Products */, + D03E45752305CCEB0049C28B /* Frameworks */, + ); + sourceTree = ""; + }; + D03E45652305CC310049C28B /* Products */ = { + isa = PBXGroup; + children = ( + D03E46322306E0BB0049C28B /* Crc32.framework */, + ); + name = Products; + sourceTree = ""; + }; + D03E45662305CC310049C28B /* Sources */ = { + isa = PBXGroup; + children = ( + D03E45732305CCD20049C28B /* Crc32.m */, + ); + name = Sources; + path = "../../submodules/telegram-ios/submodules/Crc32/Sources"; + sourceTree = ""; + }; + D03E45752305CCEB0049C28B /* Frameworks */ = { + isa = PBXGroup; + children = ( + D03E45782305CCF00049C28B /* libz.tbd */, + D03E45762305CCEB0049C28B /* Foundation.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + D03E46332306E0BB0049C28B /* Crc32 */ = { + isa = PBXGroup; + children = ( + A701F92F236C1D9E002ABF81 /* Crc32.h */, + D03E46352306E0BB0049C28B /* Info.plist */, + ); + path = Crc32; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + D03E462D2306E0BB0049C28B /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + A701F930236C1D9E002ABF81 /* Crc32.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + D03E46312306E0BB0049C28B /* Crc32 */ = { + isa = PBXNativeTarget; + buildConfigurationList = D03E463B2306E0BB0049C28B /* Build configuration list for PBXNativeTarget "Crc32" */; + buildPhases = ( + D03E462D2306E0BB0049C28B /* Headers */, + D03E462E2306E0BB0049C28B /* Sources */, + D03E462F2306E0BB0049C28B /* Frameworks */, + D03E46302306E0BB0049C28B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Crc32; + productName = crc32mac; + productReference = D03E46322306E0BB0049C28B /* Crc32.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D03E455B2305CC310049C28B /* Project object */ = { + isa = PBXProject; + attributes = { + DefaultBuildSystemTypeForWorkspace = Latest; + LastUpgradeCheck = 1030; + ORGANIZATIONNAME = "Telegram Messenger LLP"; + TargetAttributes = { + D03E46312306E0BB0049C28B = { + CreatedOnToolsVersion = 10.3; + }; + }; + }; + buildConfigurationList = D03E455E2305CC310049C28B /* Build configuration list for PBXProject "Crc32_Xcode" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = D03E455A2305CC310049C28B; + productRefGroup = D03E45652305CC310049C28B /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D03E46312306E0BB0049C28B /* Crc32 */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D03E46302306E0BB0049C28B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D03E462E2306E0BB0049C28B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D03E463E2306E22E0049C28B /* Crc32.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A7F282DF238EAB5400742C20 /* Github */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + A7F282E0238EAB5400742C20 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD)"; + 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 = ""; + CODE_SIGN_STYLE = Manual; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + FRAMEWORK_VERSION = A; + 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 = crc32/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.crc32; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + D0208AAC2306E7EB00A23503 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + D0208AAE2306E7EB00A23503 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD)"; + 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 = ""; + CODE_SIGN_STYLE = Manual; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + FRAMEWORK_VERSION = A; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = s; + 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 = crc32/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.crc32; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + D0208AAF2306E7F700A23503 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + D0208AB12306E7F700A23503 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD)"; + 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 = ""; + CODE_SIGN_STYLE = Manual; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + FRAMEWORK_VERSION = A; + 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 = crc32/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.crc32; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + D0208AB22306E7FD00A23503 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + D0208AB42306E7FD00A23503 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD)"; + 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 = ""; + CODE_SIGN_STYLE = Manual; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + FRAMEWORK_VERSION = A; + 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 = crc32/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.crc32; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + D0208AB52306E80300A23503 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + D0208AB72306E80300A23503 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD)"; + 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 = ""; + CODE_SIGN_STYLE = Manual; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + FRAMEWORK_VERSION = A; + 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 = crc32/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.crc32; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + D03E456F2305CC4E0049C28B /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + D03E46382306E0BB0049C28B /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD)"; + 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 = ""; + CODE_SIGN_STYLE = Manual; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + FRAMEWORK_VERSION = A; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = s; + 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 = crc32/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.crc32; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D03E455E2305CC310049C28B /* Build configuration list for PBXProject "Crc32_Xcode" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D03E456F2305CC4E0049C28B /* DebugHockeyapp */, + D0208AAF2306E7F700A23503 /* DebugAppStore */, + A7F282DF238EAB5400742C20 /* Github */, + D0208AAC2306E7EB00A23503 /* HockeyappMacAlpha */, + D0208AB22306E7FD00A23503 /* ReleaseAppStore */, + D0208AB52306E80300A23503 /* ReleaseHockeyapp */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = DebugHockeyapp; + }; + D03E463B2306E0BB0049C28B /* Build configuration list for PBXNativeTarget "Crc32" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D03E46382306E0BB0049C28B /* DebugHockeyapp */, + D0208AB12306E7F700A23503 /* DebugAppStore */, + A7F282E0238EAB5400742C20 /* Github */, + D0208AAE2306E7EB00A23503 /* HockeyappMacAlpha */, + D0208AB42306E7FD00A23503 /* ReleaseAppStore */, + D0208AB72306E80300A23503 /* ReleaseHockeyapp */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = DebugHockeyapp; + }; +/* End XCConfigurationList section */ + }; + rootObject = D03E455B2305CC310049C28B /* Project object */; +} diff --git a/core-xprojects/crc32/Crc32_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/crc32/Crc32_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/core-xprojects/crc32/Crc32_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/crc32/Crc32_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/crc32/Crc32_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/crc32/Crc32_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/crc32/Crc32_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate b/core-xprojects/crc32/Crc32_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000..37cfdeee1b Binary files /dev/null and b/core-xprojects/crc32/Crc32_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/core-xprojects/crc32/Crc32_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/Crc32.xcscheme b/core-xprojects/crc32/Crc32_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/Crc32.xcscheme new file mode 100644 index 0000000000..422caaa163 --- /dev/null +++ b/core-xprojects/crc32/Crc32_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/Crc32.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/crc32/Crc32_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/crc32mac.xcscheme b/core-xprojects/crc32/Crc32_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/crc32mac.xcscheme new file mode 100644 index 0000000000..422caaa163 --- /dev/null +++ b/core-xprojects/crc32/Crc32_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/crc32mac.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/crc32/Crc32_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist b/core-xprojects/crc32/Crc32_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000000..ac50275bec --- /dev/null +++ b/core-xprojects/crc32/Crc32_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,22 @@ + + + + + SchemeUserState + + Crc32.xcscheme_^#shared#^_ + + orderHint + 0 + + + SuppressBuildableAutocreation + + D03E46312306E0BB0049C28B + + primary + + + + + diff --git a/core-xprojects/ffmpeg/ffmpeg.xcodeproj/project.pbxproj b/core-xprojects/ffmpeg/ffmpeg.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..8c6878df1f --- /dev/null +++ b/core-xprojects/ffmpeg/ffmpeg.xcodeproj/project.pbxproj @@ -0,0 +1,756 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + D0983BA425669C9700467703 /* ffmpeg.h in Headers */ = {isa = PBXBuildFile; fileRef = D0983BA225669C9700467703 /* ffmpeg.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0983BC925669FEB00467703 /* libopus.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0983BAD25669CB900467703 /* libopus.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + D0983B9F25669C9700467703 /* ffmpeg.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ffmpeg.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0983BA225669C9700467703 /* ffmpeg.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ffmpeg.h; sourceTree = ""; }; + D0983BA325669C9700467703 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D0983BAD25669CB900467703 /* libopus.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = libopus.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0983CF725682D4600467703 /* libswresample.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libswresample.a; path = build/ffmpeg/lib/libswresample.a; sourceTree = ""; }; + D0983CF825682D4600467703 /* libavutil.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavutil.a; path = build/ffmpeg/lib/libavutil.a; sourceTree = ""; }; + D0983CF925682D4600467703 /* libavformat.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavformat.a; path = build/ffmpeg/lib/libavformat.a; sourceTree = ""; }; + D0983CFA25682D4600467703 /* libavcodec.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavcodec.a; path = build/ffmpeg/lib/libavcodec.a; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D0983B9C25669C9700467703 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D0983BC925669FEB00467703 /* libopus.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + D0983B9525669C9700467703 = { + isa = PBXGroup; + children = ( + D0983BA125669C9700467703 /* ffmpeg */, + D0983BA025669C9700467703 /* Products */, + D0983BAC25669CB900467703 /* Frameworks */, + ); + sourceTree = ""; + }; + D0983BA025669C9700467703 /* Products */ = { + isa = PBXGroup; + children = ( + D0983B9F25669C9700467703 /* ffmpeg.framework */, + ); + name = Products; + sourceTree = ""; + }; + D0983BA125669C9700467703 /* ffmpeg */ = { + isa = PBXGroup; + children = ( + D0983BA225669C9700467703 /* ffmpeg.h */, + D0983BA325669C9700467703 /* Info.plist */, + ); + path = ffmpeg; + sourceTree = ""; + }; + D0983BAC25669CB900467703 /* Frameworks */ = { + isa = PBXGroup; + children = ( + D0983CFA25682D4600467703 /* libavcodec.a */, + D0983CF925682D4600467703 /* libavformat.a */, + D0983CF825682D4600467703 /* libavutil.a */, + D0983CF725682D4600467703 /* libswresample.a */, + D0983BAD25669CB900467703 /* libopus.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + D0983B9A25669C9700467703 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D0983BA425669C9700467703 /* ffmpeg.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + D0983B9E25669C9700467703 /* ffmpeg */ = { + isa = PBXNativeTarget; + buildConfigurationList = D0983BA725669C9700467703 /* Build configuration list for PBXNativeTarget "ffmpeg" */; + buildPhases = ( + D0983BBD25669D9F00467703 /* ShellScript */, + D0983B9A25669C9700467703 /* Headers */, + D0983B9B25669C9700467703 /* Sources */, + D0983B9C25669C9700467703 /* Frameworks */, + D0983B9D25669C9700467703 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ffmpeg; + productName = ffmpeg; + productReference = D0983B9F25669C9700467703 /* ffmpeg.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D0983B9625669C9700467703 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1220; + TargetAttributes = { + D0983B9E25669C9700467703 = { + CreatedOnToolsVersion = 12.2; + }; + }; + }; + buildConfigurationList = D0983B9925669C9700467703 /* Build configuration list for PBXProject "ffmpeg" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = D0983B9525669C9700467703; + productRefGroup = D0983BA025669C9700467703 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D0983B9E25669C9700467703 /* ffmpeg */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D0983B9D25669C9700467703 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + D0983BBD25669D9F00467703 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ ! -d \"$PROJECT_DIR/build\" ]; then\nsh $PROJECT_DIR/ffmpeg/build.sh $PROJECT_DIR/../../submodules/telegram-ios/submodules/ffmpeg/Sources/FFMpeg\nfi\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D0983B9B25669C9700467703 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + D0983BA525669C9700467703 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + D0983BA625669C9700467703 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + D0983BA825669C9700467703 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = ffmpeg/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/build/ffmpeg/lib", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.ffmpeg; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = DebugAppStore; + }; + D0983BA925669C9700467703 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = ffmpeg/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/build/ffmpeg/lib", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.ffmpeg; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = ReleaseHockeyapp; + }; + D0983BBF25669FB100467703 /* Github */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + D0983BC025669FB100467703 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = ffmpeg/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/build/ffmpeg/lib", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.ffmpeg; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = Github; + }; + D0983BC125669FB500467703 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + D0983BC225669FB500467703 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = ffmpeg/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/build/ffmpeg/lib", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.ffmpeg; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = ReleaseAppStore; + }; + D0983BC325669FC600467703 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + D0983BC425669FC600467703 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = ffmpeg/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/build/ffmpeg/lib", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.ffmpeg; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = HockeyappMacAlpha; + }; + D0983BC525669FCF00467703 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + D0983BC625669FCF00467703 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = ffmpeg/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/build/ffmpeg/lib", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.ffmpeg; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = DebugHockeyapp; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D0983B9925669C9700467703 /* Build configuration list for PBXProject "ffmpeg" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0983BA525669C9700467703 /* DebugAppStore */, + D0983BBF25669FB100467703 /* Github */, + D0983BA625669C9700467703 /* ReleaseHockeyapp */, + D0983BC125669FB500467703 /* ReleaseAppStore */, + D0983BC325669FC600467703 /* HockeyappMacAlpha */, + D0983BC525669FCF00467703 /* DebugHockeyapp */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseHockeyapp; + }; + D0983BA725669C9700467703 /* Build configuration list for PBXNativeTarget "ffmpeg" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0983BA825669C9700467703 /* DebugAppStore */, + D0983BC025669FB100467703 /* Github */, + D0983BA925669C9700467703 /* ReleaseHockeyapp */, + D0983BC225669FB500467703 /* ReleaseAppStore */, + D0983BC425669FC600467703 /* HockeyappMacAlpha */, + D0983BC625669FCF00467703 /* DebugHockeyapp */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseHockeyapp; + }; +/* End XCConfigurationList section */ + }; + rootObject = D0983B9625669C9700467703 /* Project object */; +} diff --git a/core-xprojects/ffmpeg/ffmpeg.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/ffmpeg/ffmpeg.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/core-xprojects/ffmpeg/ffmpeg.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/ffmpeg/ffmpeg.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/ffmpeg/ffmpeg.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/ffmpeg/ffmpeg.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/ffmpeg/ffmpeg.xcodeproj/xcshareddata/xcschemes/ffmpeg.xcscheme b/core-xprojects/ffmpeg/ffmpeg.xcodeproj/xcshareddata/xcschemes/ffmpeg.xcscheme new file mode 100644 index 0000000000..e3aab383ca --- /dev/null +++ b/core-xprojects/ffmpeg/ffmpeg.xcodeproj/xcshareddata/xcschemes/ffmpeg.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/ffmpeg/ffmpeg/Info.plist b/core-xprojects/ffmpeg/ffmpeg/Info.plist new file mode 100644 index 0000000000..9bcb244429 --- /dev/null +++ b/core-xprojects/ffmpeg/ffmpeg/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/core-xprojects/ffmpeg/ffmpeg/build.sh b/core-xprojects/ffmpeg/ffmpeg/build.sh new file mode 100755 index 0000000000..d9c91e7a0f --- /dev/null +++ b/core-xprojects/ffmpeg/ffmpeg/build.sh @@ -0,0 +1,169 @@ +#!/bin/sh + +set -e +set -x + + +SOURCE_DIR=$1 + +BUILD_DIR=$(echo "$(cd "$(dirname "$3")"; pwd -P)/$(basename "$3")") +BUILD_DIR="${BUILD_DIR}build/" + +FAT="${BUILD_DIR}ffmpeg" + +SCRATCH="${BUILD_DIR}scratch" +THIN="${BUILD_DIR}thin" + +PKG_CONFIG="$SOURCE_DIR/pkg-config" + + + + + + +rm -rf $BUILD_DIR || true +mkdir -p $BUILD_DIR || true + + +LIBOPUS_PATH="${BUILD_DIR}../../libopus/build/libopus" + +FF_VERSION="4.1" +SOURCE="$SOURCE_DIR/ffmpeg-$FF_VERSION" + +GAS_PREPROCESSOR_PATH="$SOURCE_DIR/gas-preprocessor.pl" + + + + +export PATH="$SOURCE_DIR:$PATH" + +LIB_NAMES="libavcodec libavformat libavutil libswresample" + + +CONFIGURE_FLAGS="--enable-cross-compile --disable-programs \ + --disable-armv5te --disable-armv6 --disable-armv6t2 \ + --disable-doc --enable-pic --disable-all --disable-everything \ + --enable-avcodec \ + --enable-swresample \ + --enable-avformat \ + --disable-xlib \ + --enable-libopus \ + --enable-audiotoolbox \ + --enable-bsf=aac_adtstoasc \ + --enable-decoder=h264,hevc,libopus,mp3_at,aac,flac,alac_at,pcm_s16le,pcm_s24le,gsm_ms_at \ + --enable-demuxer=aac,mov,m4v,mp3,ogg,libopus,flac,wav,aiff,matroska \ + --enable-parser=aac,h264,mp3,libopus \ + --enable-protocol=file \ + --enable-muxer=mp4 \ + " + + +CONFIGURE_FLAGS="$CONFIGURE_FLAGS --disable-debug" + +COMPILE="y" + +DEPLOYMENT_TARGET=$MACOSX_DEPLOYMENT_TARGET + + +if [ "$COMPILE" ] +then + if [ ! `which yasm` ]; then + echo 'Yasm not found' + exit 1 + fi + if [ ! `which pkg-config` ]; then + echo 'pkg-config not found' + exit 1 + else + echo "PATH=$PATH" + echo "pkg-config=$(which pkg-config)" + fi + if [ ! `which "$GAS_PREPROCESSOR_PATH"` ]; then + echo '$GAS_PREPROCESSOR_PATH not found.' + exit 1 + fi + + if [ ! -r $SOURCE ]; then + echo "FFmpeg source not found at $SOURCE" + exit 1 + fi + + for ARCH in $ARCHS + do + echo "building $ARCH..." + mkdir -p "$SCRATCH/$ARCH" + pushd "$SCRATCH/$ARCH" + + + + CFLAGS="-arch $ARCH" + if [ "$ARCH" = "x86_64" ] + then + PLATFORM="MacOSX" + CFLAGS="$CFLAGS -mmacosx-version-min=$DEPLOYMENT_TARGET" + else + PLATFORM="MacOSX" + CFLAGS="$CFLAGS -mmacosx-version-min=$DEPLOYMENT_TARGET" + if [ "$ARCH" = "arm64" ] + then + EXPORT="GASPP_FIX_XCODE5=1" + fi + fi + + XCRUN_SDK=`echo $PLATFORM | tr '[:upper:]' '[:lower:]'` + CC="xcrun -sdk $XCRUN_SDK clang" + + if [ "$ARCH" = "arm64" ] + then + AS="$GAS_PREPROCESSOR_PATH -arch aarch64 -- $CC" + else + AS="$GAS_PREPROCESSOR_PATH -- $CC" + fi + + CXXFLAGS="$CFLAGS" + LDFLAGS="$CFLAGS" + + CONFIGURED_MARKER="$THIN/$ARCH/configured_marker" + CONFIGURED_MARKER_CONTENTS="" + if [ -r "$CONFIGURED_MARKER" ] + then + CONFIGURED_MARKER_CONTENTS=`cat "$CONFIGURED_MARKER"` + fi + if [ "$CONFIGURED_MARKER_CONTENTS" = "$CONFIGURE_FLAGS" ] + then + echo "1" >/dev/null + else + mkdir -p "$THIN/$ARCH" + TMPDIR=${TMPDIR/%\/} "$SOURCE/configure" \ + --target-os=darwin \ + --arch=$ARCH \ + --cc="$CC" \ + --as="$AS" \ + $CONFIGURE_FLAGS \ + --extra-cflags="$CFLAGS" \ + --extra-ldflags="$LDFLAGS" \ + --prefix="$THIN/$ARCH" \ + --pkg-config="$PKG_CONFIG" \ + --pkg-config-flags="--libopus_path $LIBOPUS_PATH" \ + || exit 1 + echo "$CONFIGURE_FLAGS" > "$CONFIGURED_MARKER" + fi + + CORE_COUNT=`sysctl -n hw.logicalcpu` + make -j$CORE_COUNT install $EXPORT || exit 1 + + popd + done +fi + + +mkdir -p "$FAT"/lib +set - $ARCHS +for LIB in "$THIN/$1/lib/"*.a +do + LIB_NAME="$(basename $LIB)" + echo "LIPO_INPUT command find \"$THIN\" -name \"$LIB_NAME\"" + LIPO_INPUT=`find "$THIN" -name "$LIB_NAME"` + lipo -create $LIPO_INPUT -output "$FAT/lib/$LIB_NAME" || exit 1 +done +cp -rf "$THIN/$1/include" "$FAT" diff --git a/core-xprojects/ffmpeg/ffmpeg/ffmpeg.h b/core-xprojects/ffmpeg/ffmpeg/ffmpeg.h new file mode 100644 index 0000000000..643ed6a942 --- /dev/null +++ b/core-xprojects/ffmpeg/ffmpeg/ffmpeg.h @@ -0,0 +1,17 @@ +// +// ffmpeg.h +// ffmpeg +// +// Created by Mikhail Filimonov on 19/11/2020. +// + +#import + +//! Project version number for ffmpeg. +FOUNDATION_EXPORT double ffmpegVersionNumber; + +//! Project version string for ffmpeg. +FOUNDATION_EXPORT const unsigned char ffmpegVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + diff --git a/core-xprojects/libopus/libopus.xcodeproj/project.pbxproj b/core-xprojects/libopus/libopus.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..1abca93406 --- /dev/null +++ b/core-xprojects/libopus/libopus.xcodeproj/project.pbxproj @@ -0,0 +1,746 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + D0983B8925668FAA00467703 /* libopus.h in Headers */ = {isa = PBXBuildFile; fileRef = D0983B8725668FAA00467703 /* libopus.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0983BCE2566A00F00467703 /* libopus.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0983BCD2566A00F00467703 /* libopus.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + D0983B8425668FAA00467703 /* libopus.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = libopus.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0983B8725668FAA00467703 /* libopus.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = libopus.h; sourceTree = ""; }; + D0983B8825668FAA00467703 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D0983BCD2566A00F00467703 /* libopus.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libopus.a; path = build/libopus/lib/libopus.a; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D0983B8125668FAA00467703 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D0983BCE2566A00F00467703 /* libopus.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + D0983B7A25668FAA00467703 = { + isa = PBXGroup; + children = ( + D0983B8625668FAA00467703 /* libopus */, + D0983B8525668FAA00467703 /* Products */, + D0983BCC2566A00F00467703 /* Frameworks */, + ); + sourceTree = ""; + }; + D0983B8525668FAA00467703 /* Products */ = { + isa = PBXGroup; + children = ( + D0983B8425668FAA00467703 /* libopus.framework */, + ); + name = Products; + sourceTree = ""; + }; + D0983B8625668FAA00467703 /* libopus */ = { + isa = PBXGroup; + children = ( + D0983B8725668FAA00467703 /* libopus.h */, + D0983B8825668FAA00467703 /* Info.plist */, + ); + path = libopus; + sourceTree = ""; + }; + D0983BCC2566A00F00467703 /* Frameworks */ = { + isa = PBXGroup; + children = ( + D0983BCD2566A00F00467703 /* libopus.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + D0983B7F25668FAA00467703 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D0983B8925668FAA00467703 /* libopus.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + D0983B8325668FAA00467703 /* libopus */ = { + isa = PBXNativeTarget; + buildConfigurationList = D0983B8C25668FAA00467703 /* Build configuration list for PBXNativeTarget "libopus" */; + buildPhases = ( + D0983B912566916800467703 /* ShellScript */, + D0983B7F25668FAA00467703 /* Headers */, + D0983B8025668FAA00467703 /* Sources */, + D0983B8125668FAA00467703 /* Frameworks */, + D0983B8225668FAA00467703 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = libopus; + productName = libopus; + productReference = D0983B8425668FAA00467703 /* libopus.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D0983B7B25668FAA00467703 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1220; + TargetAttributes = { + D0983B8325668FAA00467703 = { + CreatedOnToolsVersion = 12.2; + }; + }; + }; + buildConfigurationList = D0983B7E25668FAA00467703 /* Build configuration list for PBXProject "libopus" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = D0983B7A25668FAA00467703; + productRefGroup = D0983B8525668FAA00467703 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D0983B8325668FAA00467703 /* libopus */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D0983B8225668FAA00467703 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + D0983B912566916800467703 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nif [ ! -d \"$PROJECT_DIR/build\" ]; then\nsh $PROJECT_DIR/libopus/build.sh .\nfi\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D0983B8025668FAA00467703 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + D0983B8A25668FAA00467703 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + D0983B8B25668FAA00467703 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + D0983B8D25668FAA00467703 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = libopus/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/build/libopus/lib", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.libopus; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = DebugAppStore; + }; + D0983B8E25668FAA00467703 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = libopus/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/build/libopus/lib", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.libopus; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = ReleaseAppStore; + }; + D0983BB225669D4000467703 /* Github */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + D0983BB325669D4000467703 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = libopus/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/build/libopus/lib", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.libopus; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = Github; + }; + D0983BB425669D4700467703 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + D0983BB525669D4700467703 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = libopus/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/build/libopus/lib", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.libopus; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = ReleaseHockeyapp; + }; + D0983BB625669D4D00467703 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + D0983BB725669D4D00467703 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = libopus/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/build/libopus/lib", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.libopus; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = DebugHockeyapp; + }; + D0983BB825669D5400467703 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + D0983BB925669D5400467703 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = libopus/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/build/libopus/lib", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.libopus; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = HockeyappMacAlpha; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D0983B7E25668FAA00467703 /* Build configuration list for PBXProject "libopus" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0983B8A25668FAA00467703 /* DebugAppStore */, + D0983BB225669D4000467703 /* Github */, + D0983B8B25668FAA00467703 /* ReleaseAppStore */, + D0983BB425669D4700467703 /* ReleaseHockeyapp */, + D0983BB625669D4D00467703 /* DebugHockeyapp */, + D0983BB825669D5400467703 /* HockeyappMacAlpha */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; + D0983B8C25668FAA00467703 /* Build configuration list for PBXNativeTarget "libopus" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0983B8D25668FAA00467703 /* DebugAppStore */, + D0983BB325669D4000467703 /* Github */, + D0983B8E25668FAA00467703 /* ReleaseAppStore */, + D0983BB525669D4700467703 /* ReleaseHockeyapp */, + D0983BB725669D4D00467703 /* DebugHockeyapp */, + D0983BB925669D5400467703 /* HockeyappMacAlpha */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; +/* End XCConfigurationList section */ + }; + rootObject = D0983B7B25668FAA00467703 /* Project object */; +} diff --git a/core-xprojects/libopus/libopus.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/libopus/libopus.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/core-xprojects/libopus/libopus.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/libopus/libopus.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/libopus/libopus.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/libopus/libopus.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/libopus/libopus.xcodeproj/xcshareddata/xcschemes/libopus.xcscheme b/core-xprojects/libopus/libopus.xcodeproj/xcshareddata/xcschemes/libopus.xcscheme new file mode 100644 index 0000000000..589387223e --- /dev/null +++ b/core-xprojects/libopus/libopus.xcodeproj/xcshareddata/xcschemes/libopus.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/libopus/libopus/Info.plist b/core-xprojects/libopus/libopus/Info.plist new file mode 100644 index 0000000000..9bcb244429 --- /dev/null +++ b/core-xprojects/libopus/libopus/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/core-xprojects/libopus/libopus/build.sh b/core-xprojects/libopus/libopus/build.sh new file mode 100644 index 0000000000..db4d111617 --- /dev/null +++ b/core-xprojects/libopus/libopus/build.sh @@ -0,0 +1,88 @@ + +set -e +set -x + + +SRC_DIR="$1" +BUILD_DIR=$(echo "$(cd "$(dirname "$3")"; pwd -P)/$(basename "$3")") + + + +cd $BUILD_DIR + +rm -rf build || true +mkdir build + +OUT_DIR="${BUILD_DIR}build" +mkdir -p $OUT_DIR + + + +CROSS_TOP_MAC="$(xcode-select -p)/Platforms/MacOSX.platform" +CROSS_SDK_MAC="MacOSX.sdk" + + +SOURCE_DIR="$OUT_DIR/opus-1.3.1" +SOURCE_ARCHIVE="$SRC_DIR/opus-1.3.1.tar.gz" + +rm -rf "$SOURCE_DIR" + +tar -xzf "$SOURCE_ARCHIVE" --directory "$OUT_DIR" + + + +CROSS_TOP_MAC="$(xcode-select -p)/Platforms/MacOSX.platform" +CROSS_SDK_MAC="MacOSX.sdk" + + +DEVROOT=`xcode-select --print-path`/Toolchains/XcodeDefault.xctoolchain +export PATH="${DEVROOT}/usr/bin:${PATH}" + +for ARCH in $ARCHS +do + ARCH1=$ARCH + ARCH2="" + if [[ "${ARCH}" == "arm64" ]]; then + ARCH1="aarch64" + ARCH2="arm64" + fi + + + DIR="$(pwd)" + cd "$SOURCE_DIR" + + ROOTDIR=$OUT_DIR/$ARCH + mkdir -p "${ROOTDIR}" + + SDKROOT=$CROSS_TOP_MAC/Developer/SDKs/$CROSS_SDK_MAC + CFLAGS="-arch ${ARCH2:-${ARCH1}} -pipe -isysroot ${SDKROOT} -Os -DNDEBUG" + CFLAGS+=" -mmacosx-version-min=10.11 ${EXTRA_CFLAGS}" + + + ./configure --host=${ARCH1}-apple-darwin \ + --enable-fixed-point \ + --disable-doc \ + --disable-extra-programs \ + --disable-asm \ + --build=$(./config.guess) \ + CFLAGS="${CFLAGS}" \ + + + + # run make only in the src/ directory to create libwebp.a/libwebpdecoder.a + make + + mv "${SOURCE_DIR}/.libs/libopus.a" ${ROOTDIR}/libopus.a + + LIBLIST+=" ${ROOTDIR}/libopus.a" + + make clean + +done +# +mkdir -p ${OUT_DIR}/libopus/lib +mkdir -p ${OUT_DIR}/libopus/include/opus +# + +cp -a ${SOURCE_DIR}/include/ ${OUT_DIR}/libopus/include/opus +lipo -create ${LIBLIST} -output ${OUT_DIR}/libopus/lib/libopus.a diff --git a/core-xprojects/libopus/libopus/libopus.h b/core-xprojects/libopus/libopus/libopus.h new file mode 100644 index 0000000000..3bbd7d392f --- /dev/null +++ b/core-xprojects/libopus/libopus/libopus.h @@ -0,0 +1,18 @@ +// +// libopus.h +// libopus +// +// Created by Mikhail Filimonov on 19/11/2020. +// + +#import + +//! Project version number for libopus. +FOUNDATION_EXPORT double libopusVersionNumber; + +//! Project version string for libopus. +FOUNDATION_EXPORT const unsigned char libopusVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/core-xprojects/libopus/opus-1.3.1.tar.gz b/core-xprojects/libopus/opus-1.3.1.tar.gz new file mode 100644 index 0000000000..1119371fc2 Binary files /dev/null and b/core-xprojects/libopus/opus-1.3.1.tar.gz differ diff --git a/core-xprojects/libphonenumber/libphonenumber/Info.plist b/core-xprojects/libphonenumber/libphonenumber/Info.plist new file mode 100644 index 0000000000..5371a6e108 --- /dev/null +++ b/core-xprojects/libphonenumber/libphonenumber/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2019 Telegram Messenger LLP. All rights reserved. + + diff --git a/core-xprojects/libphonenumber/libphonenumber/libphonenumber.h b/core-xprojects/libphonenumber/libphonenumber/libphonenumber.h new file mode 100644 index 0000000000..798e6a43ec --- /dev/null +++ b/core-xprojects/libphonenumber/libphonenumber/libphonenumber.h @@ -0,0 +1,12 @@ +#import + +//! Project version number for libphonenumber_Mac. +FOUNDATION_EXPORT double libphonenumber_VersionNumber; + +//! Project version string for libphonenumber_Mac. +FOUNDATION_EXPORT const unsigned char libphonenumber_VersionString[]; + +#import +#import +#import +#import diff --git a/core-xprojects/libphonenumber/libphonenumber_Xcode.xcodeproj/project.pbxproj b/core-xprojects/libphonenumber/libphonenumber_Xcode.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..07751616ab --- /dev/null +++ b/core-xprojects/libphonenumber/libphonenumber_Xcode.xcodeproj/project.pbxproj @@ -0,0 +1,848 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + A791904E240CFC49002011CA /* NBPhoneNumber.h in Headers */ = {isa = PBXBuildFile; fileRef = A7919049240CFC49002011CA /* NBPhoneNumber.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A791904F240CFC49002011CA /* NBPhoneNumberUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = A791904A240CFC49002011CA /* NBPhoneNumberUtil.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7919051240CFC49002011CA /* NBAsYouTypeFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = A791904C240CFC49002011CA /* NBAsYouTypeFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7919052240CFC49002011CA /* NBPhoneNumberDefines.h in Headers */ = {isa = PBXBuildFile; fileRef = A791904D240CFC49002011CA /* NBPhoneNumberDefines.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0208ABF2306E85800A23503 /* NBMetadataCoreTest.h in Headers */ = {isa = PBXBuildFile; fileRef = D03E459B2305D1EF0049C28B /* NBMetadataCoreTest.h */; }; + D0208AC02306E85800A23503 /* NBPhoneMetaDataGenerator.h in Headers */ = {isa = PBXBuildFile; fileRef = D03E45A22305D1EF0049C28B /* NBPhoneMetaDataGenerator.h */; }; + D0208AC42306E85800A23503 /* libphonenumber.h in Headers */ = {isa = PBXBuildFile; fileRef = D0208AB92306E84F00A23503 /* libphonenumber.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0208AC52306E85800A23503 /* NBMetadataCoreMapper.h in Headers */ = {isa = PBXBuildFile; fileRef = D03E45A02305D1EF0049C28B /* NBMetadataCoreMapper.h */; }; + D0208AC92306E85800A23503 /* NBMetadataCore.h in Headers */ = {isa = PBXBuildFile; fileRef = D03E459E2305D1EF0049C28B /* NBMetadataCore.h */; }; + D0208ACA2306E85800A23503 /* NBPhoneMetaData.h in Headers */ = {isa = PBXBuildFile; fileRef = D03E45B02305D1F10049C28B /* NBPhoneMetaData.h */; }; + D0208ACB2306E85800A23503 /* NBPhoneNumberDesc.h in Headers */ = {isa = PBXBuildFile; fileRef = D03E45A72305D1F00049C28B /* NBPhoneNumberDesc.h */; }; + D0208ACC2306E85800A23503 /* NBMetadataHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = D03E459A2305D1EF0049C28B /* NBMetadataHelper.h */; }; + D0208ACD2306E85800A23503 /* NBMetadataCoreTestMapper.h in Headers */ = {isa = PBXBuildFile; fileRef = D03E45A82305D1F00049C28B /* NBMetadataCoreTestMapper.h */; }; + D0208ACF2306E85800A23503 /* NBMetadataCoreMapper.m in Sources */ = {isa = PBXBuildFile; fileRef = D03E45A42305D1F00049C28B /* NBMetadataCoreMapper.m */; }; + D0208AD02306E85800A23503 /* NBMetadataCoreTestMapper.m in Sources */ = {isa = PBXBuildFile; fileRef = D03E45A32305D1F00049C28B /* NBMetadataCoreTestMapper.m */; }; + D0208AD12306E85800A23503 /* NBPhoneNumberDefines.m in Sources */ = {isa = PBXBuildFile; fileRef = D03E45A12305D1EF0049C28B /* NBPhoneNumberDefines.m */; }; + D0208AD22306E85800A23503 /* NBNumberFormat.m in Sources */ = {isa = PBXBuildFile; fileRef = D03E45AE2305D1F10049C28B /* NBNumberFormat.m */; }; + D0208AD32306E85800A23503 /* NBAsYouTypeFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = D03E45B12305D1F10049C28B /* NBAsYouTypeFormatter.m */; }; + D0208AD42306E85800A23503 /* NBMetadataCore.m in Sources */ = {isa = PBXBuildFile; fileRef = D03E45A92305D1F00049C28B /* NBMetadataCore.m */; }; + D0208AD52306E85800A23503 /* NBPhoneNumberDesc.m in Sources */ = {isa = PBXBuildFile; fileRef = D03E459F2305D1EF0049C28B /* NBPhoneNumberDesc.m */; }; + D0208AD62306E85800A23503 /* NBPhoneNumber.m in Sources */ = {isa = PBXBuildFile; fileRef = D03E45AD2305D1F10049C28B /* NBPhoneNumber.m */; }; + D0208AD72306E85800A23503 /* NBPhoneMetaData.m in Sources */ = {isa = PBXBuildFile; fileRef = D03E45AF2305D1F10049C28B /* NBPhoneMetaData.m */; }; + D0208AD82306E85800A23503 /* NBMetadataCoreTest.m in Sources */ = {isa = PBXBuildFile; fileRef = D03E45B22305D1F10049C28B /* NBMetadataCoreTest.m */; }; + D0208AD92306E85800A23503 /* NBPhoneMetaDataGenerator.m in Sources */ = {isa = PBXBuildFile; fileRef = D03E45AB2305D1F10049C28B /* NBPhoneMetaDataGenerator.m */; }; + D0208ADA2306E85800A23503 /* NBMetadataHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = D03E45AC2305D1F10049C28B /* NBMetadataHelper.m */; }; + D0208ADB2306E85800A23503 /* NBPhoneNumberUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = D03E45A52305D1F00049C28B /* NBPhoneNumberUtil.m */; }; + D0208ADD2306E85800A23503 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D03E45CE2305D32D0049C28B /* Foundation.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A7919049240CFC49002011CA /* NBPhoneNumber.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NBPhoneNumber.h; path = "../../../submodules/telegram-ios/submodules/libphonenumber/PublicHeaders/libphonenumber/NBPhoneNumber.h"; sourceTree = ""; }; + A791904A240CFC49002011CA /* NBPhoneNumberUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NBPhoneNumberUtil.h; path = "../../../submodules/telegram-ios/submodules/libphonenumber/PublicHeaders/libphonenumber/NBPhoneNumberUtil.h"; sourceTree = ""; }; + A791904C240CFC49002011CA /* NBAsYouTypeFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NBAsYouTypeFormatter.h; path = "../../../submodules/telegram-ios/submodules/libphonenumber/PublicHeaders/libphonenumber/NBAsYouTypeFormatter.h"; sourceTree = ""; }; + A791904D240CFC49002011CA /* NBPhoneNumberDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NBPhoneNumberDefines.h; path = "../../../submodules/telegram-ios/submodules/libphonenumber/PublicHeaders/libphonenumber/NBPhoneNumberDefines.h"; sourceTree = ""; }; + D0208AB92306E84F00A23503 /* libphonenumber.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = libphonenumber.h; sourceTree = ""; }; + D0208ABA2306E84F00A23503 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D0208AE52306E85800A23503 /* libphonenumber.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = libphonenumber.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D03E45992305D1EF0049C28B /* NBNumberFormat.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NBNumberFormat.h; sourceTree = ""; }; + D03E459A2305D1EF0049C28B /* NBMetadataHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NBMetadataHelper.h; sourceTree = ""; }; + D03E459B2305D1EF0049C28B /* NBMetadataCoreTest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NBMetadataCoreTest.h; sourceTree = ""; }; + D03E459E2305D1EF0049C28B /* NBMetadataCore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NBMetadataCore.h; sourceTree = ""; }; + D03E459F2305D1EF0049C28B /* NBPhoneNumberDesc.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NBPhoneNumberDesc.m; sourceTree = ""; }; + D03E45A02305D1EF0049C28B /* NBMetadataCoreMapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NBMetadataCoreMapper.h; sourceTree = ""; }; + D03E45A12305D1EF0049C28B /* NBPhoneNumberDefines.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NBPhoneNumberDefines.m; sourceTree = ""; }; + D03E45A22305D1EF0049C28B /* NBPhoneMetaDataGenerator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NBPhoneMetaDataGenerator.h; sourceTree = ""; }; + D03E45A32305D1F00049C28B /* NBMetadataCoreTestMapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NBMetadataCoreTestMapper.m; sourceTree = ""; }; + D03E45A42305D1F00049C28B /* NBMetadataCoreMapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NBMetadataCoreMapper.m; sourceTree = ""; }; + D03E45A52305D1F00049C28B /* NBPhoneNumberUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NBPhoneNumberUtil.m; sourceTree = ""; }; + D03E45A72305D1F00049C28B /* NBPhoneNumberDesc.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NBPhoneNumberDesc.h; sourceTree = ""; }; + D03E45A82305D1F00049C28B /* NBMetadataCoreTestMapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NBMetadataCoreTestMapper.h; sourceTree = ""; }; + D03E45A92305D1F00049C28B /* NBMetadataCore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NBMetadataCore.m; sourceTree = ""; }; + D03E45AB2305D1F10049C28B /* NBPhoneMetaDataGenerator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NBPhoneMetaDataGenerator.m; sourceTree = ""; }; + D03E45AC2305D1F10049C28B /* NBMetadataHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NBMetadataHelper.m; sourceTree = ""; }; + D03E45AD2305D1F10049C28B /* NBPhoneNumber.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NBPhoneNumber.m; sourceTree = ""; }; + D03E45AE2305D1F10049C28B /* NBNumberFormat.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NBNumberFormat.m; sourceTree = ""; }; + D03E45AF2305D1F10049C28B /* NBPhoneMetaData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NBPhoneMetaData.m; sourceTree = ""; }; + D03E45B02305D1F10049C28B /* NBPhoneMetaData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NBPhoneMetaData.h; sourceTree = ""; }; + D03E45B12305D1F10049C28B /* NBAsYouTypeFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NBAsYouTypeFormatter.m; sourceTree = ""; }; + D03E45B22305D1F10049C28B /* NBMetadataCoreTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NBMetadataCoreTest.m; sourceTree = ""; }; + D03E45CE2305D32D0049C28B /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D0208ADC2306E85800A23503 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D0208ADD2306E85800A23503 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A7919048240CFC40002011CA /* headers */ = { + isa = PBXGroup; + children = ( + A791904C240CFC49002011CA /* NBAsYouTypeFormatter.h */, + A7919049240CFC49002011CA /* NBPhoneNumber.h */, + A791904D240CFC49002011CA /* NBPhoneNumberDefines.h */, + A791904A240CFC49002011CA /* NBPhoneNumberUtil.h */, + ); + path = headers; + sourceTree = ""; + }; + D0208AB82306E84F00A23503 /* libphonenumber */ = { + isa = PBXGroup; + children = ( + D0208AB92306E84F00A23503 /* libphonenumber.h */, + D0208ABA2306E84F00A23503 /* Info.plist */, + ); + path = libphonenumber; + sourceTree = ""; + }; + D03E45802305CE830049C28B = { + isa = PBXGroup; + children = ( + A7919048240CFC40002011CA /* headers */, + D0208AB82306E84F00A23503 /* libphonenumber */, + D03E458C2305CE840049C28B /* Sources */, + D03E458B2305CE830049C28B /* Products */, + D03E45CD2305D32D0049C28B /* Frameworks */, + ); + sourceTree = ""; + }; + D03E458B2305CE830049C28B /* Products */ = { + isa = PBXGroup; + children = ( + D0208AE52306E85800A23503 /* libphonenumber.framework */, + ); + name = Products; + sourceTree = ""; + }; + D03E458C2305CE840049C28B /* Sources */ = { + isa = PBXGroup; + children = ( + D03E45B12305D1F10049C28B /* NBAsYouTypeFormatter.m */, + D03E459E2305D1EF0049C28B /* NBMetadataCore.h */, + D03E45A92305D1F00049C28B /* NBMetadataCore.m */, + D03E45A02305D1EF0049C28B /* NBMetadataCoreMapper.h */, + D03E45A42305D1F00049C28B /* NBMetadataCoreMapper.m */, + D03E459B2305D1EF0049C28B /* NBMetadataCoreTest.h */, + D03E45B22305D1F10049C28B /* NBMetadataCoreTest.m */, + D03E45A82305D1F00049C28B /* NBMetadataCoreTestMapper.h */, + D03E45A32305D1F00049C28B /* NBMetadataCoreTestMapper.m */, + D03E459A2305D1EF0049C28B /* NBMetadataHelper.h */, + D03E45AC2305D1F10049C28B /* NBMetadataHelper.m */, + D03E45992305D1EF0049C28B /* NBNumberFormat.h */, + D03E45AE2305D1F10049C28B /* NBNumberFormat.m */, + D03E45B02305D1F10049C28B /* NBPhoneMetaData.h */, + D03E45AF2305D1F10049C28B /* NBPhoneMetaData.m */, + D03E45A22305D1EF0049C28B /* NBPhoneMetaDataGenerator.h */, + D03E45AB2305D1F10049C28B /* NBPhoneMetaDataGenerator.m */, + D03E45AD2305D1F10049C28B /* NBPhoneNumber.m */, + D03E45A12305D1EF0049C28B /* NBPhoneNumberDefines.m */, + D03E45A72305D1F00049C28B /* NBPhoneNumberDesc.h */, + D03E459F2305D1EF0049C28B /* NBPhoneNumberDesc.m */, + D03E45A52305D1F00049C28B /* NBPhoneNumberUtil.m */, + ); + name = Sources; + path = "../../submodules/telegram-ios/submodules/libphonenumber/Sources"; + sourceTree = ""; + }; + D03E45CD2305D32D0049C28B /* Frameworks */ = { + isa = PBXGroup; + children = ( + D03E45CE2305D32D0049C28B /* Foundation.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + D0208ABE2306E85800A23503 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D0208AC42306E85800A23503 /* libphonenumber.h in Headers */, + A791904E240CFC49002011CA /* NBPhoneNumber.h in Headers */, + A7919051240CFC49002011CA /* NBAsYouTypeFormatter.h in Headers */, + A791904F240CFC49002011CA /* NBPhoneNumberUtil.h in Headers */, + A7919052240CFC49002011CA /* NBPhoneNumberDefines.h in Headers */, + D0208ABF2306E85800A23503 /* NBMetadataCoreTest.h in Headers */, + D0208AC02306E85800A23503 /* NBPhoneMetaDataGenerator.h in Headers */, + D0208AC52306E85800A23503 /* NBMetadataCoreMapper.h in Headers */, + D0208AC92306E85800A23503 /* NBMetadataCore.h in Headers */, + D0208ACA2306E85800A23503 /* NBPhoneMetaData.h in Headers */, + D0208ACB2306E85800A23503 /* NBPhoneNumberDesc.h in Headers */, + D0208ACC2306E85800A23503 /* NBMetadataHelper.h in Headers */, + D0208ACD2306E85800A23503 /* NBMetadataCoreTestMapper.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + D0208ABD2306E85800A23503 /* libphonenumber */ = { + isa = PBXNativeTarget; + buildConfigurationList = D0208AE02306E85800A23503 /* Build configuration list for PBXNativeTarget "libphonenumber" */; + buildPhases = ( + D0208ABE2306E85800A23503 /* Headers */, + D0208ACE2306E85800A23503 /* Sources */, + D0208ADC2306E85800A23503 /* Frameworks */, + D0208ADE2306E85800A23503 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = libphonenumber; + productName = "libphonenumber-iOS"; + productReference = D0208AE52306E85800A23503 /* libphonenumber.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D03E45812305CE830049C28B /* Project object */ = { + isa = PBXProject; + attributes = { + DefaultBuildSystemTypeForWorkspace = Latest; + LastUpgradeCheck = 1030; + ORGANIZATIONNAME = "Telegram Messenger LLP"; + }; + buildConfigurationList = D03E45842305CE830049C28B /* Build configuration list for PBXProject "libphonenumber_Xcode" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = D03E45802305CE830049C28B; + productRefGroup = D03E458B2305CE830049C28B /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D0208ABD2306E85800A23503 /* libphonenumber */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D0208ADE2306E85800A23503 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D0208ACE2306E85800A23503 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D0208ACF2306E85800A23503 /* NBMetadataCoreMapper.m in Sources */, + D0208AD02306E85800A23503 /* NBMetadataCoreTestMapper.m in Sources */, + D0208AD12306E85800A23503 /* NBPhoneNumberDefines.m in Sources */, + D0208AD22306E85800A23503 /* NBNumberFormat.m in Sources */, + D0208AD32306E85800A23503 /* NBAsYouTypeFormatter.m in Sources */, + D0208AD42306E85800A23503 /* NBMetadataCore.m in Sources */, + D0208AD52306E85800A23503 /* NBPhoneNumberDesc.m in Sources */, + D0208AD62306E85800A23503 /* NBPhoneNumber.m in Sources */, + D0208AD72306E85800A23503 /* NBPhoneMetaData.m in Sources */, + D0208AD82306E85800A23503 /* NBMetadataCoreTest.m in Sources */, + D0208AD92306E85800A23503 /* NBPhoneMetaDataGenerator.m in Sources */, + D0208ADA2306E85800A23503 /* NBMetadataHelper.m in Sources */, + D0208ADB2306E85800A23503 /* NBPhoneNumberUtil.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A7F282E5238EAB6800742C20 /* Github */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + A7F282E6238EAB6800742C20 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = libphonenumber/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.libphonenumbermac; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Github; + }; + D0208AE22306E85800A23503 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_OPTIMIZATION_LEVEL = s; + INFOPLIST_FILE = libphonenumber/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.libphonenumbermac; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = DebugHockeyapp; + }; + D0208AE72306E86800A23503 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + D0208AE92306E86800A23503 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = libphonenumber/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.libphonenumbermac; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = DebugAppStore; + }; + D0208AEA2306E87100A23503 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + D0208AEC2306E87100A23503 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_OPTIMIZATION_LEVEL = s; + INFOPLIST_FILE = libphonenumber/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.libphonenumbermac; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = HockeyappMacAlpha; + }; + D0208AED2306E87700A23503 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + D0208AEF2306E87700A23503 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = libphonenumber/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.libphonenumbermac; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = ReleaseAppStore; + }; + D0208AF02306E87E00A23503 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + D0208AF22306E87E00A23503 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = libphonenumber/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.libphonenumbermac; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = ReleaseHockeyapp; + }; + D03E45952305CE9A0049C28B /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D0208AE02306E85800A23503 /* Build configuration list for PBXNativeTarget "libphonenumber" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0208AE22306E85800A23503 /* DebugHockeyapp */, + D0208AEC2306E87100A23503 /* HockeyappMacAlpha */, + D0208AE92306E86800A23503 /* DebugAppStore */, + A7F282E6238EAB6800742C20 /* Github */, + D0208AEF2306E87700A23503 /* ReleaseAppStore */, + D0208AF22306E87E00A23503 /* ReleaseHockeyapp */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = DebugHockeyapp; + }; + D03E45842305CE830049C28B /* Build configuration list for PBXProject "libphonenumber_Xcode" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D03E45952305CE9A0049C28B /* DebugHockeyapp */, + D0208AEA2306E87100A23503 /* HockeyappMacAlpha */, + D0208AE72306E86800A23503 /* DebugAppStore */, + A7F282E5238EAB6800742C20 /* Github */, + D0208AED2306E87700A23503 /* ReleaseAppStore */, + D0208AF02306E87E00A23503 /* ReleaseHockeyapp */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = DebugHockeyapp; + }; +/* End XCConfigurationList section */ + }; + rootObject = D03E45812305CE830049C28B /* Project object */; +} diff --git a/core-xprojects/libphonenumber/libphonenumber_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/libphonenumber/libphonenumber_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/core-xprojects/libphonenumber/libphonenumber_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/libphonenumber/libphonenumber_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/libphonenumber/libphonenumber_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/libphonenumber/libphonenumber_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/libphonenumber/libphonenumber_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate b/core-xprojects/libphonenumber/libphonenumber_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000..0630bef85d Binary files /dev/null and b/core-xprojects/libphonenumber/libphonenumber_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/core-xprojects/libphonenumber/libphonenumber_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/libphonenumber.xcscheme b/core-xprojects/libphonenumber/libphonenumber_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/libphonenumber.xcscheme new file mode 100644 index 0000000000..04c54132b1 --- /dev/null +++ b/core-xprojects/libphonenumber/libphonenumber_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/libphonenumber.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/libphonenumber/libphonenumber_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/libphonenumbermac.xcscheme b/core-xprojects/libphonenumber/libphonenumber_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/libphonenumbermac.xcscheme new file mode 100644 index 0000000000..04c54132b1 --- /dev/null +++ b/core-xprojects/libphonenumber/libphonenumber_Xcode.xcodeproj/xcuserdata/keepcoder.xcuserdatad/xcschemes/libphonenumbermac.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/libphonenumber/libphonenumber_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/libphonenumber.xcscheme b/core-xprojects/libphonenumber/libphonenumber_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/libphonenumber.xcscheme new file mode 100644 index 0000000000..04c54132b1 --- /dev/null +++ b/core-xprojects/libphonenumber/libphonenumber_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/libphonenumber.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/libphonenumber/libphonenumber_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist b/core-xprojects/libphonenumber/libphonenumber_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000000..685a0cde7a --- /dev/null +++ b/core-xprojects/libphonenumber/libphonenumber_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,16 @@ + + + + + SchemeUserState + + libphonenumber.xcscheme + + isShown + + orderHint + 6 + + + + diff --git a/core-xprojects/libwebp/libwebp-master.zip b/core-xprojects/libwebp/libwebp-master.zip new file mode 100644 index 0000000000..142ec4e88f Binary files /dev/null and b/core-xprojects/libwebp/libwebp-master.zip differ diff --git a/core-xprojects/libwebp/libwebp.xcodeproj/project.pbxproj b/core-xprojects/libwebp/libwebp.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..01daad1f03 --- /dev/null +++ b/core-xprojects/libwebp/libwebp.xcodeproj/project.pbxproj @@ -0,0 +1,783 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 2759F3B52624C68B00E72ED0 /* WebPImageCoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 2759F3B32624C68A00E72ED0 /* WebPImageCoder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2759F3B62624C68B00E72ED0 /* WebPImageCoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 2759F3B42624C68B00E72ED0 /* WebPImageCoder.m */; }; + 27CD096D2624CF9C00ECB8EF /* libwebpdecoder.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0983B69256689B300467703 /* libwebpdecoder.a */; }; + 27CD096E2624CF9C00ECB8EF /* libwebpdemux.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0983B6B256689B300467703 /* libwebpdemux.a */; }; + 27CD096F2624CF9C00ECB8EF /* libwebpmux.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0983B6C256689B300467703 /* libwebpmux.a */; }; + D0983B552566606600467703 /* libwebp.h in Headers */ = {isa = PBXBuildFile; fileRef = D0983B532566606600467703 /* libwebp.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D0983B6E256689B300467703 /* libwebp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0983B6A256689B300467703 /* libwebp.a */; }; + D0983B7425668BEA00467703 /* libwebp.m in Sources */ = {isa = PBXBuildFile; fileRef = D0983B7225668BEA00467703 /* libwebp.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 2759F3B32624C68A00E72ED0 /* WebPImageCoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebPImageCoder.h; sourceTree = ""; }; + 2759F3B42624C68B00E72ED0 /* WebPImageCoder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WebPImageCoder.m; sourceTree = ""; }; + D0983B502566606600467703 /* libwebp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = libwebp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0983B532566606600467703 /* libwebp.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = libwebp.h; sourceTree = ""; }; + D0983B542566606600467703 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D0983B69256689B300467703 /* libwebpdecoder.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libwebpdecoder.a; path = build/libwebp/lib/libwebpdecoder.a; sourceTree = ""; }; + D0983B6A256689B300467703 /* libwebp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libwebp.a; path = build/libwebp/lib/libwebp.a; sourceTree = ""; }; + D0983B6B256689B300467703 /* libwebpdemux.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libwebpdemux.a; path = build/libwebp/lib/libwebpdemux.a; sourceTree = ""; }; + D0983B6C256689B300467703 /* libwebpmux.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libwebpmux.a; path = build/libwebp/lib/libwebpmux.a; sourceTree = ""; }; + D0983B7225668BEA00467703 /* libwebp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = libwebp.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D0983B4D2566606600467703 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D0983B6E256689B300467703 /* libwebp.a in Frameworks */, + 27CD096D2624CF9C00ECB8EF /* libwebpdecoder.a in Frameworks */, + 27CD096E2624CF9C00ECB8EF /* libwebpdemux.a in Frameworks */, + 27CD096F2624CF9C00ECB8EF /* libwebpmux.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + D0983B462566606500467703 = { + isa = PBXGroup; + children = ( + D0983B522566606600467703 /* libwebp */, + D0983B512566606600467703 /* Products */, + D0983B68256689B300467703 /* Frameworks */, + ); + sourceTree = ""; + }; + D0983B512566606600467703 /* Products */ = { + isa = PBXGroup; + children = ( + D0983B502566606600467703 /* libwebp.framework */, + ); + name = Products; + sourceTree = ""; + }; + D0983B522566606600467703 /* libwebp */ = { + isa = PBXGroup; + children = ( + 2759F3B32624C68A00E72ED0 /* WebPImageCoder.h */, + 2759F3B42624C68B00E72ED0 /* WebPImageCoder.m */, + D0983B7225668BEA00467703 /* libwebp.m */, + D0983B532566606600467703 /* libwebp.h */, + D0983B542566606600467703 /* Info.plist */, + ); + path = libwebp; + sourceTree = ""; + }; + D0983B68256689B300467703 /* Frameworks */ = { + isa = PBXGroup; + children = ( + D0983B6A256689B300467703 /* libwebp.a */, + D0983B69256689B300467703 /* libwebpdecoder.a */, + D0983B6B256689B300467703 /* libwebpdemux.a */, + D0983B6C256689B300467703 /* libwebpmux.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + D0983B4B2566606600467703 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 2759F3B52624C68B00E72ED0 /* WebPImageCoder.h in Headers */, + D0983B552566606600467703 /* libwebp.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + D0983B4F2566606600467703 /* libwebp */ = { + isa = PBXNativeTarget; + buildConfigurationList = D0983B582566606600467703 /* Build configuration list for PBXNativeTarget "libwebp" */; + buildPhases = ( + D0983B5D2566620000467703 /* Run Script */, + D0983B4B2566606600467703 /* Headers */, + D0983B4C2566606600467703 /* Sources */, + D0983B4D2566606600467703 /* Frameworks */, + D0983B4E2566606600467703 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = libwebp; + productName = libwebp; + productReference = D0983B502566606600467703 /* libwebp.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D0983B472566606500467703 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1220; + TargetAttributes = { + D0983B4F2566606600467703 = { + CreatedOnToolsVersion = 12.2; + }; + }; + }; + buildConfigurationList = D0983B4A2566606500467703 /* Build configuration list for PBXProject "libwebp" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = D0983B462566606500467703; + productRefGroup = D0983B512566606600467703 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D0983B4F2566606600467703 /* libwebp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D0983B4E2566606600467703 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + D0983B5D2566620000467703 /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nif [ ! -d \"$PROJECT_DIR/build\" ]; then\n sh $PROJECT_DIR/libwebp/build.sh .\nfi\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D0983B4C2566606600467703 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D0983B7425668BEA00467703 /* libwebp.m in Sources */, + 2759F3B62624C68B00E72ED0 /* WebPImageCoder.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + D0983B562566606600467703 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + D0983B572566606600467703 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + D0983B592566606600467703 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/build/libwebp/include"; + INFOPLIST_FILE = libwebp/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/build/libwebp/lib", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = com.google.libwebp; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = DebugAppStore; + }; + D0983B5A2566606600467703 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/build/libwebp/include"; + INFOPLIST_FILE = libwebp/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/build/libwebp/lib", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = com.google.libwebp; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = ReleaseAppStore; + }; + D0983B602566896B00467703 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + D0983B612566896B00467703 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/build/libwebp/include"; + INFOPLIST_FILE = libwebp/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/build/libwebp/lib", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = com.google.libwebp; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = ReleaseHockeyapp; + }; + D0983B622566897900467703 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + D0983B632566897900467703 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/build/libwebp/include"; + INFOPLIST_FILE = libwebp/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/build/libwebp/lib", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = com.google.libwebp; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = DebugHockeyapp; + }; + D0983B642566899B00467703 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + D0983B652566899B00467703 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/build/libwebp/include"; + INFOPLIST_FILE = libwebp/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/build/libwebp/lib", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = com.google.libwebp; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = HockeyappMacAlpha; + }; + D0983B66256689A300467703 /* Github */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + D0983B67256689A300467703 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/build/libwebp/include"; + INFOPLIST_FILE = libwebp/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/build/libwebp/lib", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_BUNDLE_IDENTIFIER = com.google.libwebp; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = Github; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D0983B4A2566606500467703 /* Build configuration list for PBXProject "libwebp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0983B562566606600467703 /* DebugAppStore */, + D0983B66256689A300467703 /* Github */, + D0983B572566606600467703 /* ReleaseAppStore */, + D0983B602566896B00467703 /* ReleaseHockeyapp */, + D0983B622566897900467703 /* DebugHockeyapp */, + D0983B642566899B00467703 /* HockeyappMacAlpha */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; + D0983B582566606600467703 /* Build configuration list for PBXNativeTarget "libwebp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0983B592566606600467703 /* DebugAppStore */, + D0983B67256689A300467703 /* Github */, + D0983B5A2566606600467703 /* ReleaseAppStore */, + D0983B612566896B00467703 /* ReleaseHockeyapp */, + D0983B632566897900467703 /* DebugHockeyapp */, + D0983B652566899B00467703 /* HockeyappMacAlpha */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; +/* End XCConfigurationList section */ + }; + rootObject = D0983B472566606500467703 /* Project object */; +} diff --git a/core-xprojects/libwebp/libwebp.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/libwebp/libwebp.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/core-xprojects/libwebp/libwebp.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/libwebp/libwebp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/libwebp/libwebp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/libwebp/libwebp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/libwebp/libwebp.xcodeproj/xcshareddata/xcschemes/libwebp.xcscheme b/core-xprojects/libwebp/libwebp.xcodeproj/xcshareddata/xcschemes/libwebp.xcscheme new file mode 100644 index 0000000000..bf75e296eb --- /dev/null +++ b/core-xprojects/libwebp/libwebp.xcodeproj/xcshareddata/xcschemes/libwebp.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/libwebp/libwebp/Info.plist b/core-xprojects/libwebp/libwebp/Info.plist new file mode 100644 index 0000000000..9bcb244429 --- /dev/null +++ b/core-xprojects/libwebp/libwebp/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/core-xprojects/libwebp/libwebp/WebPImageCoder.h b/core-xprojects/libwebp/libwebp/WebPImageCoder.h new file mode 100644 index 0000000000..82443e5a31 --- /dev/null +++ b/core-xprojects/libwebp/libwebp/WebPImageCoder.h @@ -0,0 +1,68 @@ +// +// WebPImageCoder.h +// WebPImage +// +// Created by ibireme on 15/5/13. +// Copyright (c) 2015 ibireme. +// +// This source code is licensed under the MIT-style license found in the +// LICENSE file in the root directory of this source tree. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + + +typedef NS_ENUM(NSUInteger, WebPImageType) { + WebPImageTypeUnknown = 0, ///< unknown + WebPImageTypeWebP +}; + +typedef NS_ENUM(NSUInteger, WebPImageDisposeMethod) { + WebPImageDisposeNone = 0, + WebPImageDisposeBackground, + WebPImageDisposePrevious, +}; + +typedef NS_ENUM(NSUInteger, WebPImageBlendOperation) { + WebPImageBlendNone = 0, + WebPImageBlendOver, +}; + +@interface WebPImageFrame : NSObject +@property (nonatomic) NSUInteger index; ///< Frame index (zero based) +@property (nonatomic) NSUInteger width; ///< Frame width +@property (nonatomic) NSUInteger height; ///< Frame height +@property (nonatomic) NSUInteger offsetX; ///< Frame origin.x in canvas (left-bottom based) +@property (nonatomic) NSUInteger offsetY; ///< Frame origin.y in canvas (left-bottom based) +@property (nonatomic) NSTimeInterval duration; ///< Frame duration in seconds +@property (nonatomic) WebPImageDisposeMethod dispose; ///< Frame dispose method. +@property (nonatomic) WebPImageBlendOperation blend; ///< Frame blend operation. +@property (nullable, nonatomic, strong) NSImage *image; ///< The image. +@end + + +@interface WebPImageDecoder : NSObject + +@property (nullable, nonatomic, readonly) NSData *data; ///< Image data. +@property (nonatomic, readonly) WebPImageType type; ///< Image data type. +@property (nonatomic, readonly) CGFloat scale; ///< Image scale. +@property (nonatomic, readonly) NSUInteger frameCount; ///< Image frame count. +@property (nonatomic, readonly) NSUInteger loopCount; ///< Image loop count, 0 means infinite. +@property (nonatomic, readonly) NSUInteger width; ///< Image canvas width. +@property (nonatomic, readonly) NSUInteger height; ///< Image canvas height. +@property (nonatomic, readonly, getter=isFinalized) BOOL finalized; +- (instancetype)initWithScale:(CGFloat)scale NS_DESIGNATED_INITIALIZER; +- (BOOL)updateData:(nullable NSData *)data final:(BOOL)final; ++ (nullable instancetype)decoderWithData:(NSData *)data scale:(CGFloat)scale; +- (nullable WebPImageFrame *)frameAtIndex:(NSUInteger)index decodeForDisplay:(BOOL)decodeForDisplay; +- (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index; + +@end + + + +CG_EXTERN CGImageRef _Nullable WebPCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay); + +NS_ASSUME_NONNULL_END diff --git a/core-xprojects/libwebp/libwebp/WebPImageCoder.m b/core-xprojects/libwebp/libwebp/WebPImageCoder.m new file mode 100644 index 0000000000..b04b6d66c4 --- /dev/null +++ b/core-xprojects/libwebp/libwebp/WebPImageCoder.m @@ -0,0 +1,596 @@ +// +// WebPImageCoder.m +// WebPImage +// +// Created by ibireme on 15/5/13. +// Copyright (c) 2015 ibireme. +// +// This source code is licensed under the MIT-style license found in the +// LICENSE file in the root directory of this source tree. +// + +#import "WebPImageCoder.h" +#import +#import +#import +#import +#import + +#import "decode.h" +#import "encode.h" +#import "demux.h" +#import "mux.h" + + + +static void WebPCGDataProviderReleaseDataCallback(void *info, const void *data, size_t size) { + if (info) free(info); +} +static inline size_t WebPImageByteAlign(size_t size, size_t alignment) { + return ((size + (alignment - 1)) / alignment) * alignment; +} + +CGColorSpaceRef WebPCGColorSpaceGetDeviceRGB() { + static CGColorSpaceRef space; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + space = CGColorSpaceCreateDeviceRGB(); + }); + return space; +} + +CGColorSpaceRef WebPCGColorSpaceGetDeviceGray() { + static CGColorSpaceRef space; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + space = CGColorSpaceCreateDeviceGray(); + }); + return space; +} + +BOOL WebPCGColorSpaceIsDeviceRGB(CGColorSpaceRef space) { + return space && CFEqual(space, WebPCGColorSpaceGetDeviceRGB()); +} + +BOOL WebPCGColorSpaceIsDeviceGray(CGColorSpaceRef space) { + return space && CFEqual(space, WebPCGColorSpaceGetDeviceGray()); +} + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Decoder + +@implementation WebPImageFrame + +- (id)copyWithZone:(NSZone *)zone { + WebPImageFrame *frame = [self.class new]; + frame.index = _index; + frame.width = _width; + frame.height = _height; + frame.offsetX = _offsetX; + frame.offsetY = _offsetY; + frame.duration = _duration; + frame.dispose = _dispose; + frame.blend = _blend; + frame.image = _image.copy; + return frame; +} +@end + +// Internal frame object. +@interface _WebPImageDecoderFrame : WebPImageFrame +@property (nonatomic, assign) BOOL hasAlpha; ///< Whether frame has alpha. +@property (nonatomic, assign) BOOL isFullSize; ///< Whether frame fill the canvas. +@property (nonatomic, assign) NSUInteger blendFromIndex; ///< Blend from frame index to current frame. +@end + +@implementation _WebPImageDecoderFrame +- (id)copyWithZone:(NSZone *)zone { + _WebPImageDecoderFrame *frame = [super copyWithZone:zone]; + frame.hasAlpha = _hasAlpha; + frame.isFullSize = _isFullSize; + frame.blendFromIndex = _blendFromIndex; + return frame; +} +@end + + +@implementation WebPImageDecoder { + pthread_mutex_t _lock; // recursive lock + + BOOL _sourceTypeDetected; + WebPDemuxer *_webpSource; + + dispatch_semaphore_t _framesLock; + NSArray *_frames; + BOOL _needBlend; + NSUInteger _blendFrameIndex; + CGContextRef _blendCanvas; +} + +- (void)dealloc { + if (_webpSource) WebPDemuxDelete(_webpSource); + if (_blendCanvas) CFRelease(_blendCanvas); + pthread_mutex_destroy(&_lock); +} + ++ (instancetype)decoderWithData:(NSData *)data scale:(CGFloat)scale { + if (!data) return nil; + WebPImageDecoder *decoder = [[WebPImageDecoder alloc] initWithScale:scale]; + [decoder updateData:data final:YES]; + if (decoder.frameCount == 0) return nil; + return decoder; +} + +- (instancetype)init { + return [self initWithScale:[NSScreen mainScreen].backingScaleFactor]; +} + +- (instancetype)initWithScale:(CGFloat)scale { + self = [super init]; + if (scale <= 0) scale = 1; + _scale = scale; + _framesLock = dispatch_semaphore_create(1); + + pthread_mutexattr_t attr; + pthread_mutexattr_init (&attr); + pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE); + pthread_mutex_init (&_lock, &attr); + pthread_mutexattr_destroy (&attr); + + return self; +} + +- (BOOL)updateData:(NSData *)data final:(BOOL)final { + BOOL result = NO; + pthread_mutex_lock(&_lock); + result = [self _updateData:data final:final]; + pthread_mutex_unlock(&_lock); + return result; +} + +- (WebPImageFrame *)frameAtIndex:(NSUInteger)index decodeForDisplay:(BOOL)decodeForDisplay { + WebPImageFrame *result = nil; + pthread_mutex_lock(&_lock); + result = [self _frameAtIndex:index decodeForDisplay:decodeForDisplay]; + pthread_mutex_unlock(&_lock); + return result; +} + +- (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index { + NSTimeInterval result = 0; + dispatch_semaphore_wait(_framesLock, DISPATCH_TIME_FOREVER); + if (index < _frames.count) { + result = ((_WebPImageDecoderFrame *)_frames[index]).duration; + } + dispatch_semaphore_signal(_framesLock); + return result; +} +- (BOOL)_updateData:(NSData *)data final:(BOOL)final { + if (_finalized) return NO; + if (data.length < _data.length) return NO; + _finalized = final; + _data = data; + + _type = WebPImageTypeWebP; + [self _updateSource]; + _sourceTypeDetected = YES; + return YES; +} + +- (WebPImageFrame *)_frameAtIndex:(NSUInteger)index decodeForDisplay:(BOOL)decodeForDisplay { + if (index >= _frames.count) return 0; + _WebPImageDecoderFrame *frame = [(_WebPImageDecoderFrame *)_frames[index] copy]; + BOOL decoded = NO; + BOOL extendToCanvas = NO; + if (decodeForDisplay) { // ICO contains multi-size frame and should not extend to canvas. + extendToCanvas = YES; + } + + if (!_needBlend) { + CGImageRef imageRef = [self _newUnblendedImageAtIndex:index extendToCanvas:extendToCanvas decoded:&decoded]; + if (!imageRef) return nil; + if (decodeForDisplay && !decoded) { + CGImageRef imageRefDecoded = WebPCGImageCreateDecodedCopy(imageRef, YES); + if (imageRefDecoded) { + CFRelease(imageRef); + imageRef = imageRefDecoded; + decoded = YES; + } + } + + NSImage *image = [[NSImage alloc] initWithCGImage:imageRef size:NSMakeSize(CGImageGetWidth(imageRef), CGImageGetHeight(imageRef))]; + CFRelease(imageRef); + if (!image) return nil; + frame.image = image; + return frame; + } + + // blend + if (![self _createBlendContextIfNeeded]) return nil; + CGImageRef imageRef = NULL; + + if (_blendFrameIndex + 1 == frame.index) { + imageRef = [self _newBlendedImageWithFrame:frame]; + _blendFrameIndex = index; + } else { // should draw canvas from previous frame + _blendFrameIndex = NSNotFound; + CGContextClearRect(_blendCanvas, CGRectMake(0, 0, _width, _height)); + + if (frame.blendFromIndex == frame.index) { + CGImageRef unblendedImage = [self _newUnblendedImageAtIndex:index extendToCanvas:NO decoded:NULL]; + if (unblendedImage) { + CGContextDrawImage(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height), unblendedImage); + CFRelease(unblendedImage); + } + imageRef = CGBitmapContextCreateImage(_blendCanvas); + if (frame.dispose == WebPImageDisposeBackground) { + CGContextClearRect(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height)); + } + _blendFrameIndex = index; + } else { // canvas is not ready + for (uint32_t i = (uint32_t)frame.blendFromIndex; i <= (uint32_t)frame.index; i++) { + if (i == frame.index) { + if (!imageRef) imageRef = [self _newBlendedImageWithFrame:frame]; + } else { + [self _blendImageWithFrame:_frames[i]]; + } + } + _blendFrameIndex = index; + } + } + + if (!imageRef) return nil; + NSImage *image = [[NSImage alloc] initWithCGImage:imageRef size:NSMakeSize(CGImageGetWidth(imageRef), CGImageGetHeight(imageRef))]; + CFRelease(imageRef); + if (!image) return nil; + + frame.image = image; + if (extendToCanvas) { + frame.width = _width; + frame.height = _height; + frame.offsetX = 0; + frame.offsetY = 0; + frame.dispose = WebPImageDisposeNone; + frame.blend = WebPImageBlendNone; + } + return frame; +} + + +#pragma private + +- (void)_updateSource { + [self _updateSourceWebP]; +} + +- (void)_updateSourceWebP { + _width = 0; + _height = 0; + _loopCount = 0; + if (_webpSource) WebPDemuxDelete(_webpSource); + _webpSource = NULL; + dispatch_semaphore_wait(_framesLock, DISPATCH_TIME_FOREVER); + _frames = nil; + dispatch_semaphore_signal(_framesLock); + + WebPData webPData = {0}; + webPData.bytes = _data.bytes; + webPData.size = _data.length; + WebPDemuxer *demuxer = WebPDemux(&webPData); + if (!demuxer) return; + + uint32_t webpFrameCount = WebPDemuxGetI(demuxer, WEBP_FF_FRAME_COUNT); + uint32_t webpLoopCount = WebPDemuxGetI(demuxer, WEBP_FF_LOOP_COUNT); + uint32_t canvasWidth = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_WIDTH); + uint32_t canvasHeight = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT); + if (webpFrameCount == 0 || canvasWidth < 1 || canvasHeight < 1) { + WebPDemuxDelete(demuxer); + return; + } + + NSMutableArray *frames = [NSMutableArray new]; + BOOL needBlend = NO; + uint32_t iterIndex = 0; + uint32_t lastBlendIndex = 0; + WebPIterator iter = {0}; + if (WebPDemuxGetFrame(demuxer, 1, &iter)) { // one-based index... + do { + _WebPImageDecoderFrame *frame = [_WebPImageDecoderFrame new]; + [frames addObject:frame]; + if (iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) { + frame.dispose = WebPImageDisposeBackground; + } + if (iter.blend_method == WEBP_MUX_BLEND) { + frame.blend = WebPImageBlendOver; + } + + int canvasWidth = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_WIDTH); + int canvasHeight = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT); + frame.index = iterIndex; + frame.duration = iter.duration / 1000.0; + frame.width = iter.width; + frame.height = iter.height; + frame.hasAlpha = iter.has_alpha; + frame.blend = iter.blend_method == WEBP_MUX_BLEND; + frame.offsetX = iter.x_offset; + frame.offsetY = canvasHeight - iter.y_offset - iter.height; + + BOOL sizeEqualsToCanvas = (iter.width == canvasWidth && iter.height == canvasHeight); + BOOL offsetIsZero = (iter.x_offset == 0 && iter.y_offset == 0); + frame.isFullSize = (sizeEqualsToCanvas && offsetIsZero); + + if ((!frame.blend || !frame.hasAlpha) && frame.isFullSize) { + frame.blendFromIndex = lastBlendIndex = iterIndex; + } else { + if (frame.dispose && frame.isFullSize) { + frame.blendFromIndex = lastBlendIndex; + lastBlendIndex = iterIndex + 1; + } else { + frame.blendFromIndex = lastBlendIndex; + } + } + if (frame.index != frame.blendFromIndex) needBlend = YES; + iterIndex++; + } while (WebPDemuxNextFrame(&iter)); + WebPDemuxReleaseIterator(&iter); + } + if (frames.count != webpFrameCount) { + WebPDemuxDelete(demuxer); + return; + } + + _width = canvasWidth; + _height = canvasHeight; + _frameCount = frames.count; + _loopCount = webpLoopCount; + _needBlend = needBlend; + _webpSource = demuxer; + dispatch_semaphore_wait(_framesLock, DISPATCH_TIME_FOREVER); + _frames = frames; + dispatch_semaphore_signal(_framesLock); +} + +- (CGImageRef)_newUnblendedImageAtIndex:(NSUInteger)index + extendToCanvas:(BOOL)extendToCanvas + decoded:(BOOL *)decoded CF_RETURNS_RETAINED { + + if (!_finalized && index > 0) return NULL; + if (_frames.count <= index) return NULL; + + if (_webpSource) { + WebPIterator iter; + if (!WebPDemuxGetFrame(_webpSource, (int)(index + 1), &iter)) return NULL; // demux webp frame data + // frame numbers are one-based in webp -----------^ + + int frameWidth = iter.width; + int frameHeight = iter.height; + if (frameWidth < 1 || frameHeight < 1) return NULL; + + int width = extendToCanvas ? (int)_width : frameWidth; + int height = extendToCanvas ? (int)_height : frameHeight; + if (width > _width || height > _height) return NULL; + + const uint8_t *payload = iter.fragment.bytes; + size_t payloadSize = iter.fragment.size; + + WebPDecoderConfig config; + if (!WebPInitDecoderConfig(&config)) { + WebPDemuxReleaseIterator(&iter); + return NULL; + } + if (WebPGetFeatures(payload , payloadSize, &config.input) != VP8_STATUS_OK) { + WebPDemuxReleaseIterator(&iter); + return NULL; + } + + size_t bitsPerComponent = 8; + size_t bitsPerPixel = 32; + size_t bytesPerRow = WebPImageByteAlign(bitsPerPixel / 8 * width, 32); + size_t length = bytesPerRow * height; + CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst; //bgrA + + void *pixels = calloc(1, length); + if (!pixels) { + WebPDemuxReleaseIterator(&iter); + return NULL; + } + + config.output.colorspace = MODE_bgrA; + config.output.is_external_memory = 1; + config.output.u.RGBA.rgba = pixels; + config.output.u.RGBA.stride = (int)bytesPerRow; + config.output.u.RGBA.size = length; + VP8StatusCode result = WebPDecode(payload, payloadSize, &config); // decode + if ((result != VP8_STATUS_OK) && (result != VP8_STATUS_NOT_ENOUGH_DATA)) { + WebPDemuxReleaseIterator(&iter); + free(pixels); + return NULL; + } + WebPDemuxReleaseIterator(&iter); + + if (extendToCanvas && (iter.x_offset != 0 || iter.y_offset != 0)) { + void *tmp = calloc(1, length); + if (tmp) { + vImage_Buffer src = {pixels, height, width, bytesPerRow}; + vImage_Buffer dest = {tmp, height, width, bytesPerRow}; + vImage_CGAffineTransform transform = {1, 0, 0, 1, iter.x_offset, -iter.y_offset}; + uint8_t backColor[4] = {0}; + vImage_Error error = vImageAffineWarpCG_ARGB8888(&src, &dest, NULL, &transform, backColor, kvImageBackgroundColorFill); + if (error == kvImageNoError) { + memcpy(pixels, tmp, length); + } + free(tmp); + } + } + + CGDataProviderRef provider = CGDataProviderCreateWithData(pixels, pixels, length, WebPCGDataProviderReleaseDataCallback); + if (!provider) { + free(pixels); + return NULL; + } + pixels = NULL; // hold by provider + + CGImageRef image = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, WebPCGColorSpaceGetDeviceRGB(), bitmapInfo, provider, NULL, false, kCGRenderingIntentDefault); + CFRelease(provider); + if (decoded) *decoded = YES; + return image; + } + + return NULL; +} + +- (BOOL)_createBlendContextIfNeeded { + if (!_blendCanvas) { + _blendFrameIndex = NSNotFound; + _blendCanvas = CGBitmapContextCreate(NULL, _width, _height, 8, 0, WebPCGColorSpaceGetDeviceRGB(), kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst); + } + BOOL suc = _blendCanvas != NULL; + return suc; +} + +- (void)_blendImageWithFrame:(_WebPImageDecoderFrame *)frame { + if (frame.dispose == WebPImageDisposePrevious) { + // nothing + } else if (frame.dispose == WebPImageDisposeBackground) { + CGContextClearRect(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height)); + } else { // no dispose + if (frame.blend == WebPImageBlendOver) { + CGImageRef unblendImage = [self _newUnblendedImageAtIndex:frame.index extendToCanvas:NO decoded:NULL]; + if (unblendImage) { + CGContextDrawImage(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height), unblendImage); + CFRelease(unblendImage); + } + } else { + CGContextClearRect(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height)); + CGImageRef unblendImage = [self _newUnblendedImageAtIndex:frame.index extendToCanvas:NO decoded:NULL]; + if (unblendImage) { + CGContextDrawImage(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height), unblendImage); + CFRelease(unblendImage); + } + } + } +} + +- (CGImageRef)_newBlendedImageWithFrame:(_WebPImageDecoderFrame *)frame CF_RETURNS_RETAINED{ + CGImageRef imageRef = NULL; + if (frame.dispose == WebPImageDisposePrevious) { + if (frame.blend == WebPImageBlendOver) { + CGImageRef previousImage = CGBitmapContextCreateImage(_blendCanvas); + CGImageRef unblendImage = [self _newUnblendedImageAtIndex:frame.index extendToCanvas:NO decoded:NULL]; + if (unblendImage) { + CGContextDrawImage(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height), unblendImage); + CFRelease(unblendImage); + } + imageRef = CGBitmapContextCreateImage(_blendCanvas); + CGContextClearRect(_blendCanvas, CGRectMake(0, 0, _width, _height)); + if (previousImage) { + CGContextDrawImage(_blendCanvas, CGRectMake(0, 0, _width, _height), previousImage); + CFRelease(previousImage); + } + } else { + CGImageRef previousImage = CGBitmapContextCreateImage(_blendCanvas); + CGImageRef unblendImage = [self _newUnblendedImageAtIndex:frame.index extendToCanvas:NO decoded:NULL]; + if (unblendImage) { + CGContextClearRect(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height)); + CGContextDrawImage(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height), unblendImage); + CFRelease(unblendImage); + } + imageRef = CGBitmapContextCreateImage(_blendCanvas); + CGContextClearRect(_blendCanvas, CGRectMake(0, 0, _width, _height)); + if (previousImage) { + CGContextDrawImage(_blendCanvas, CGRectMake(0, 0, _width, _height), previousImage); + CFRelease(previousImage); + } + } + } else if (frame.dispose == WebPImageDisposeBackground) { + if (frame.blend == WebPImageBlendOver) { + CGImageRef unblendImage = [self _newUnblendedImageAtIndex:frame.index extendToCanvas:NO decoded:NULL]; + if (unblendImage) { + CGContextDrawImage(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height), unblendImage); + CFRelease(unblendImage); + } + imageRef = CGBitmapContextCreateImage(_blendCanvas); + CGContextClearRect(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height)); + } else { + CGImageRef unblendImage = [self _newUnblendedImageAtIndex:frame.index extendToCanvas:NO decoded:NULL]; + if (unblendImage) { + CGContextClearRect(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height)); + CGContextDrawImage(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height), unblendImage); + CFRelease(unblendImage); + } + imageRef = CGBitmapContextCreateImage(_blendCanvas); + CGContextClearRect(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height)); + } + } else { // no dispose + if (frame.blend == WebPImageBlendOver) { + CGImageRef unblendImage = [self _newUnblendedImageAtIndex:frame.index extendToCanvas:NO decoded:NULL]; + if (unblendImage) { + CGContextDrawImage(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height), unblendImage); + CFRelease(unblendImage); + } + imageRef = CGBitmapContextCreateImage(_blendCanvas); + } else { + CGImageRef unblendImage = [self _newUnblendedImageAtIndex:frame.index extendToCanvas:NO decoded:NULL]; + if (unblendImage) { + CGContextClearRect(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height)); + CGContextDrawImage(_blendCanvas, CGRectMake(frame.offsetX, frame.offsetY, frame.width, frame.height), unblendImage); + CFRelease(unblendImage); + } + imageRef = CGBitmapContextCreateImage(_blendCanvas); + } + } + return imageRef; +} + +@end + + + +CGImageRef WebPCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay) { + if (!imageRef) return NULL; + size_t width = CGImageGetWidth(imageRef); + size_t height = CGImageGetHeight(imageRef); + if (width == 0 || height == 0) return NULL; + + if (decodeForDisplay) { //decode with redraw (may lose some precision) + CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask; + BOOL hasAlpha = NO; + if (alphaInfo == kCGImageAlphaPremultipliedLast || + alphaInfo == kCGImageAlphaPremultipliedFirst || + alphaInfo == kCGImageAlphaLast || + alphaInfo == kCGImageAlphaFirst) { + hasAlpha = YES; + } + // BGRA8888 (premultiplied) or BGRX8888 + // same as UIGraphicsBeginImageContext() and -[UIView drawRect:] + CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host; + bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst; + CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, WebPCGColorSpaceGetDeviceRGB(), bitmapInfo); + if (!context) return NULL; + CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); // decode + CGImageRef newImage = CGBitmapContextCreateImage(context); + CFRelease(context); + return newImage; + + } else { + CGColorSpaceRef space = CGImageGetColorSpace(imageRef); + size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef); + size_t bitsPerPixel = CGImageGetBitsPerPixel(imageRef); + size_t bytesPerRow = CGImageGetBytesPerRow(imageRef); + CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef); + if (bytesPerRow == 0 || width == 0 || height == 0) return NULL; + + CGDataProviderRef dataProvider = CGImageGetDataProvider(imageRef); + if (!dataProvider) return NULL; + CFDataRef data = CGDataProviderCopyData(dataProvider); // decode + if (!data) return NULL; + + CGDataProviderRef newProvider = CGDataProviderCreateWithCFData(data); + CFRelease(data); + if (!newProvider) return NULL; + + CGImageRef newImage = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, space, bitmapInfo, newProvider, NULL, false, kCGRenderingIntentDefault); + CFRelease(newProvider); + return newImage; + } +} diff --git a/core-xprojects/libwebp/libwebp/build.sh b/core-xprojects/libwebp/libwebp/build.sh new file mode 100644 index 0000000000..1fbbf69d1c --- /dev/null +++ b/core-xprojects/libwebp/libwebp/build.sh @@ -0,0 +1,108 @@ + +set -e +set -x + + +SRC_DIR="$1" +BUILD_DIR=$(echo "$(cd "$(dirname "$3")"; pwd -P)/$(basename "$3")") + + + +cd $BUILD_DIR + +rm -rf build || true +mkdir build + +OUT_DIR="${BUILD_DIR}build" +mkdir -p $OUT_DIR + + + +CROSS_TOP_MAC="$(xcode-select -p)/Platforms/MacOSX.platform" +CROSS_SDK_MAC="MacOSX.sdk" + + +SOURCE_DIR="$OUT_DIR/libwebp-master" +SOURCE_ARCHIVE="$SRC_DIR/libwebp-master.zip" + +rm -rf "$SOURCE_DIR" + +tar -xzf "$SOURCE_ARCHIVE" --directory "$OUT_DIR" + + +cd $SOURCE_DIR +sh autogen.sh + + +CROSS_TOP_MAC="$(xcode-select -p)/Platforms/MacOSX.platform" +CROSS_SDK_MAC="MacOSX.sdk" + + +DEVROOT=`xcode-select --print-path`/Toolchains/XcodeDefault.xctoolchain +export PATH="${DEVROOT}/usr/bin:${PATH}" + +for ARCH in $ARCHS +do + ARCH1=$ARCH + ARCH2="" + if [[ "${ARCH}" == "arm64" ]]; then + ARCH1="aarch64" + ARCH2="arm64" + fi + + + DIR="$(pwd)" + cd "$SOURCE_DIR" + + ROOTDIR=$OUT_DIR/$ARCH + mkdir -p "${ROOTDIR}" + + SDKROOT=$CROSS_TOP_MAC/Developer/SDKs/$CROSS_SDK_MAC + CFLAGS="-arch ${ARCH2:-${ARCH1}} -pipe -isysroot ${SDKROOT} -Os -DNDEBUG" + CFLAGS+=" -mmacosx-version-min=10.11 ${EXTRA_CFLAGS}" + ./configure --host=${ARCH1}-apple-darwin --prefix=${ROOTDIR} \ + --build=$(./config.guess) \ + --disable-shared --enable-static \ + --enable-libwebpdecoder --enable-swap-16bit-csp \ + --enable-libwebpmux \ + CFLAGS="${CFLAGS}" + + # run make only in the src/ directory to create libwebp.a/libwebpdecoder.a + cd src/ + make V=0 + + mv "${SOURCE_DIR}/src/.libs/libwebp.a" ${ROOTDIR}/libwebp.a + mv "${SOURCE_DIR}/src/.libs/libwebpdecoder.a" ${ROOTDIR}/libwebpdecoder.a + mv "${SOURCE_DIR}/src/mux/.libs/libwebpmux.a" ${ROOTDIR}/libwebpmux.a + mv "${SOURCE_DIR}/src/demux/.libs/libwebpdemux.a" ${ROOTDIR}/libwebpdemux.a + + LIBLIST+=" ${ROOTDIR}/libwebp.a" + DECLIBLIST+=" ${ROOTDIR}/libwebpdecoder.a" + MUXLIBLIST+=" ${ROOTDIR}/libwebpmux.a" + DEMUXLIBLIST+=" ${ROOTDIR}/libwebpdemux.a" + + make clean + +done + +mkdir -p ${OUT_DIR}/libwebp/lib +mkdir -p ${OUT_DIR}/libwebp/include + +echo "LIBLIST = ${LIBLIST}" +cp -a ${SOURCE_DIR}/src/webp/{decode,encode,types}.h ${OUT_DIR}/libwebp/include/ +lipo -create ${LIBLIST} -output ${OUT_DIR}/libwebp/lib/libwebp.a + + +echo "DECLIBLIST = ${DECLIBLIST}" +cp -a ${SOURCE_DIR}/src/webp/{decode,types}.h ${OUT_DIR}/libwebp/include/ +lipo -create ${DECLIBLIST} -output ${OUT_DIR}/libwebp/lib/libwebpdecoder.a + + +echo "MUXLIBLIST = ${MUXLIBLIST}" +cp -a ${SOURCE_DIR}/src/webp/{types,mux,mux_types}.h ${OUT_DIR}/libwebp/include/ +lipo -create ${MUXLIBLIST} -output ${OUT_DIR}/libwebp/lib/libwebpmux.a + +echo "DEMUXLIBLIST = ${DEMUXLIBLIST}" +cp -a ${SOURCE_DIR}/src/webp/{decode,types,mux_types,demux}.h ${OUT_DIR}/libwebp/include/ +lipo -create ${DEMUXLIBLIST} -output ${OUT_DIR}/libwebp/lib/libwebpdemux.a + diff --git a/core-xprojects/libwebp/libwebp/libwebp.h b/core-xprojects/libwebp/libwebp/libwebp.h new file mode 100644 index 0000000000..644780875f --- /dev/null +++ b/core-xprojects/libwebp/libwebp/libwebp.h @@ -0,0 +1,21 @@ +// +// libwebp.h +// libwebp +// +// Created by Mikhail Filimonov on 19/11/2020. +// + +#import + +//! Project version number for libwebp. +FOUNDATION_EXPORT double libwebpVersionNumber; + +//! Project version string for libwebp. +FOUNDATION_EXPORT const unsigned char libwebpVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + +NSImage * _Nullable convertFromWebP(NSData *data); + +#import diff --git a/core-xprojects/libwebp/libwebp/libwebp.m b/core-xprojects/libwebp/libwebp/libwebp.m new file mode 100755 index 0000000000..dbfa82fab8 --- /dev/null +++ b/core-xprojects/libwebp/libwebp/libwebp.m @@ -0,0 +1,17 @@ +#import "libwebp.h" +#import +#import "decode.h" +#import "encode.h" +#import "demux.h" +#import "mux.h" + +NSImage * _Nullable convertFromWebP(NSData *imgData) { + + if (imgData == nil) { + return nil; + } + WebPImageDecoder *decoder = [WebPImageDecoder decoderWithData:imgData scale: 2]; + + return [decoder frameAtIndex:0 decodeForDisplay:YES].image; + +} diff --git a/core-xprojects/rnnoise/rnnoise.xcodeproj/project.pbxproj b/core-xprojects/rnnoise/rnnoise.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..fc5a669c37 --- /dev/null +++ b/core-xprojects/rnnoise/rnnoise.xcodeproj/project.pbxproj @@ -0,0 +1,927 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 27D1D5FA2611F6EF00684DEA /* _rnnoise.h in Headers */ = {isa = PBXBuildFile; fileRef = 27D1D5F82611F6EF00684DEA /* _rnnoise.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 27D1D6162611F78F00684DEA /* rnnoise.h in Headers */ = {isa = PBXBuildFile; fileRef = 27D1D6022611F78F00684DEA /* rnnoise.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 27D1D6172611F78F00684DEA /* celt_lpc.h in Headers */ = {isa = PBXBuildFile; fileRef = 27D1D6042611F78F00684DEA /* celt_lpc.h */; }; + 27D1D6182611F78F00684DEA /* kiss_fft.c in Sources */ = {isa = PBXBuildFile; fileRef = 27D1D6052611F78F00684DEA /* kiss_fft.c */; }; + 27D1D6192611F78F00684DEA /* rnn.h in Headers */ = {isa = PBXBuildFile; fileRef = 27D1D6062611F78F00684DEA /* rnn.h */; }; + 27D1D61A2611F78F00684DEA /* denoise.c in Sources */ = {isa = PBXBuildFile; fileRef = 27D1D6072611F78F00684DEA /* denoise.c */; }; + 27D1D61B2611F78F00684DEA /* compile.sh in Resources */ = {isa = PBXBuildFile; fileRef = 27D1D6082611F78F00684DEA /* compile.sh */; }; + 27D1D61C2611F78F00684DEA /* pitch.h in Headers */ = {isa = PBXBuildFile; fileRef = 27D1D6092611F78F00684DEA /* pitch.h */; }; + 27D1D61D2611F78F00684DEA /* tansig_table.h in Headers */ = {isa = PBXBuildFile; fileRef = 27D1D60A2611F78F00684DEA /* tansig_table.h */; }; + 27D1D61E2611F78F00684DEA /* _kiss_fft_guts.h in Headers */ = {isa = PBXBuildFile; fileRef = 27D1D60B2611F78F00684DEA /* _kiss_fft_guts.h */; }; + 27D1D61F2611F78F00684DEA /* rnn_data.h in Headers */ = {isa = PBXBuildFile; fileRef = 27D1D60C2611F78F00684DEA /* rnn_data.h */; }; + 27D1D6202611F78F00684DEA /* kiss_fft.h in Headers */ = {isa = PBXBuildFile; fileRef = 27D1D60D2611F78F00684DEA /* kiss_fft.h */; }; + 27D1D6212611F78F00684DEA /* common.h in Headers */ = {isa = PBXBuildFile; fileRef = 27D1D60E2611F78F00684DEA /* common.h */; }; + 27D1D6222611F78F00684DEA /* celt_lpc.c in Sources */ = {isa = PBXBuildFile; fileRef = 27D1D60F2611F78F00684DEA /* celt_lpc.c */; }; + 27D1D6232611F78F00684DEA /* pitch.c in Sources */ = {isa = PBXBuildFile; fileRef = 27D1D6102611F78F00684DEA /* pitch.c */; }; + 27D1D6242611F78F00684DEA /* rnn.c in Sources */ = {isa = PBXBuildFile; fileRef = 27D1D6112611F78F00684DEA /* rnn.c */; }; + 27D1D6252611F78F00684DEA /* rnn_reader.c in Sources */ = {isa = PBXBuildFile; fileRef = 27D1D6122611F78F00684DEA /* rnn_reader.c */; }; + 27D1D6262611F78F00684DEA /* opus_types.h in Headers */ = {isa = PBXBuildFile; fileRef = 27D1D6132611F78F00684DEA /* opus_types.h */; }; + 27D1D6272611F78F00684DEA /* rnn_data.c in Sources */ = {isa = PBXBuildFile; fileRef = 27D1D6142611F78F00684DEA /* rnn_data.c */; }; + 27D1D6282611F78F00684DEA /* arch.h in Headers */ = {isa = PBXBuildFile; fileRef = 27D1D6152611F78F00684DEA /* arch.h */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 27D1D5F52611F6EF00684DEA /* rnnoise.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = rnnoise.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 27D1D5F82611F6EF00684DEA /* _rnnoise.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = _rnnoise.h; sourceTree = ""; }; + 27D1D5F92611F6EF00684DEA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 27D1D6022611F78F00684DEA /* rnnoise.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = rnnoise.h; sourceTree = ""; }; + 27D1D6042611F78F00684DEA /* celt_lpc.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = celt_lpc.h; sourceTree = ""; }; + 27D1D6052611F78F00684DEA /* kiss_fft.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = kiss_fft.c; sourceTree = ""; }; + 27D1D6062611F78F00684DEA /* rnn.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = rnn.h; sourceTree = ""; }; + 27D1D6072611F78F00684DEA /* denoise.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = denoise.c; sourceTree = ""; }; + 27D1D6082611F78F00684DEA /* compile.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = compile.sh; sourceTree = ""; }; + 27D1D6092611F78F00684DEA /* pitch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = pitch.h; sourceTree = ""; }; + 27D1D60A2611F78F00684DEA /* tansig_table.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = tansig_table.h; sourceTree = ""; }; + 27D1D60B2611F78F00684DEA /* _kiss_fft_guts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _kiss_fft_guts.h; sourceTree = ""; }; + 27D1D60C2611F78F00684DEA /* rnn_data.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = rnn_data.h; sourceTree = ""; }; + 27D1D60D2611F78F00684DEA /* kiss_fft.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = kiss_fft.h; sourceTree = ""; }; + 27D1D60E2611F78F00684DEA /* common.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = common.h; sourceTree = ""; }; + 27D1D60F2611F78F00684DEA /* celt_lpc.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = celt_lpc.c; sourceTree = ""; }; + 27D1D6102611F78F00684DEA /* pitch.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = pitch.c; sourceTree = ""; }; + 27D1D6112611F78F00684DEA /* rnn.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = rnn.c; sourceTree = ""; }; + 27D1D6122611F78F00684DEA /* rnn_reader.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = rnn_reader.c; sourceTree = ""; }; + 27D1D6132611F78F00684DEA /* opus_types.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = opus_types.h; sourceTree = ""; }; + 27D1D6142611F78F00684DEA /* rnn_data.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = rnn_data.c; sourceTree = ""; }; + 27D1D6152611F78F00684DEA /* arch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = arch.h; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 27D1D5F22611F6EF00684DEA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 27D1D5EB2611F6EF00684DEA = { + isa = PBXGroup; + children = ( + 27D1D6012611F78F00684DEA /* PublicHeaders */, + 27D1D6032611F78F00684DEA /* Sources */, + 27D1D5F72611F6EF00684DEA /* rnnoise */, + 27D1D5F62611F6EF00684DEA /* Products */, + ); + sourceTree = ""; + }; + 27D1D5F62611F6EF00684DEA /* Products */ = { + isa = PBXGroup; + children = ( + 27D1D5F52611F6EF00684DEA /* rnnoise.framework */, + ); + name = Products; + sourceTree = ""; + }; + 27D1D5F72611F6EF00684DEA /* rnnoise */ = { + isa = PBXGroup; + children = ( + 27D1D5F82611F6EF00684DEA /* _rnnoise.h */, + 27D1D5F92611F6EF00684DEA /* Info.plist */, + ); + path = rnnoise; + sourceTree = ""; + }; + 27D1D6012611F78F00684DEA /* PublicHeaders */ = { + isa = PBXGroup; + children = ( + 27D1D6022611F78F00684DEA /* rnnoise.h */, + ); + name = PublicHeaders; + path = "../../submodules/telegram-ios/third-party/rnnoise/PublicHeaders"; + sourceTree = ""; + }; + 27D1D6032611F78F00684DEA /* Sources */ = { + isa = PBXGroup; + children = ( + 27D1D6042611F78F00684DEA /* celt_lpc.h */, + 27D1D6052611F78F00684DEA /* kiss_fft.c */, + 27D1D6062611F78F00684DEA /* rnn.h */, + 27D1D6072611F78F00684DEA /* denoise.c */, + 27D1D6082611F78F00684DEA /* compile.sh */, + 27D1D6092611F78F00684DEA /* pitch.h */, + 27D1D60A2611F78F00684DEA /* tansig_table.h */, + 27D1D60B2611F78F00684DEA /* _kiss_fft_guts.h */, + 27D1D60C2611F78F00684DEA /* rnn_data.h */, + 27D1D60D2611F78F00684DEA /* kiss_fft.h */, + 27D1D60E2611F78F00684DEA /* common.h */, + 27D1D60F2611F78F00684DEA /* celt_lpc.c */, + 27D1D6102611F78F00684DEA /* pitch.c */, + 27D1D6112611F78F00684DEA /* rnn.c */, + 27D1D6122611F78F00684DEA /* rnn_reader.c */, + 27D1D6132611F78F00684DEA /* opus_types.h */, + 27D1D6142611F78F00684DEA /* rnn_data.c */, + 27D1D6152611F78F00684DEA /* arch.h */, + ); + name = Sources; + path = "../../submodules/telegram-ios/third-party/rnnoise/Sources"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 27D1D5F02611F6EF00684DEA /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 27D1D6172611F78F00684DEA /* celt_lpc.h in Headers */, + 27D1D6162611F78F00684DEA /* rnnoise.h in Headers */, + 27D1D6212611F78F00684DEA /* common.h in Headers */, + 27D1D6192611F78F00684DEA /* rnn.h in Headers */, + 27D1D61C2611F78F00684DEA /* pitch.h in Headers */, + 27D1D6202611F78F00684DEA /* kiss_fft.h in Headers */, + 27D1D5FA2611F6EF00684DEA /* _rnnoise.h in Headers */, + 27D1D61D2611F78F00684DEA /* tansig_table.h in Headers */, + 27D1D61E2611F78F00684DEA /* _kiss_fft_guts.h in Headers */, + 27D1D6262611F78F00684DEA /* opus_types.h in Headers */, + 27D1D6282611F78F00684DEA /* arch.h in Headers */, + 27D1D61F2611F78F00684DEA /* rnn_data.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 27D1D5F42611F6EF00684DEA /* rnnoise */ = { + isa = PBXNativeTarget; + buildConfigurationList = 27D1D5FD2611F6EF00684DEA /* Build configuration list for PBXNativeTarget "rnnoise" */; + buildPhases = ( + 27D1D5F02611F6EF00684DEA /* Headers */, + 27D1D5F12611F6EF00684DEA /* Sources */, + 27D1D5F22611F6EF00684DEA /* Frameworks */, + 27D1D5F32611F6EF00684DEA /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = rnnoise; + productName = rnnoise; + productReference = 27D1D5F52611F6EF00684DEA /* rnnoise.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 27D1D5EC2611F6EF00684DEA /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1220; + TargetAttributes = { + 27D1D5F42611F6EF00684DEA = { + CreatedOnToolsVersion = 12.2; + }; + }; + }; + buildConfigurationList = 27D1D5EF2611F6EF00684DEA /* Build configuration list for PBXProject "rnnoise" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 27D1D5EB2611F6EF00684DEA; + productRefGroup = 27D1D5F62611F6EF00684DEA /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 27D1D5F42611F6EF00684DEA /* rnnoise */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 27D1D5F32611F6EF00684DEA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 27D1D61B2611F78F00684DEA /* compile.sh in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 27D1D5F12611F6EF00684DEA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 27D1D6242611F78F00684DEA /* rnn.c in Sources */, + 27D1D6232611F78F00684DEA /* pitch.c in Sources */, + 27D1D6182611F78F00684DEA /* kiss_fft.c in Sources */, + 27D1D61A2611F78F00684DEA /* denoise.c in Sources */, + 27D1D6222611F78F00684DEA /* celt_lpc.c in Sources */, + 27D1D6272611F78F00684DEA /* rnn_data.c in Sources */, + 27D1D6252611F78F00684DEA /* rnn_reader.c in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 27D1D5FB2611F6EF00684DEA /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + 27D1D5FC2611F6EF00684DEA /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + 27D1D5FE2611F6EF00684DEA /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = rnnoise/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + OTHER_CFLAGS = ( + "-Dcelt_iir=rnnoise_celt_iir", + "-D_celt_autocorr=rnnoise__celt_autocorr", + "-D_celt_lpc=rnnoise__celt_lpc", + "-Dcelt_pitch_xcorr=rnnoise_celt_pitch_xcorr", + "-Dremove_doubling=rnnoise_remove_doubling", + "-Dpitch_search=rnnoise_pitch_search", + "-Dpitch_filter=rnnoise_pitch_filter", + "-Dpitch_downsample=rnnoise_pitch_downsample", + "-Dopus_ifft_c=rnnoise_opus_ifft_c", + "-Dopus_fft_impl=rnnoise_opus_fft_impl", + "-Dopus_fft_free_arch_c=rnnoise_opus_fft_free_arch_c", + "-Dopus_fft_free=rnnoise_opus_fft_free", + "-Dopus_fft_c=rnnoise_opus_fft_c", + "-Dopus_fft_alloc_twiddles=rnnoise_opus_fft_alloc_twiddles", + "-Dopus_fft_alloc_arch_c=rnnoise_opus_fft_alloc_arch_c", + "-Dopus_fft_alloc=rnnoise_opus_fft_alloc", + "-Dinterp_band_gain=rnnoise_interp_band_gain", + "-Dcompute_rnn=rnnoise_compute_rnn", + "-Dcompute_gru=rnnoise_compute_gru", + "-Dcompute_dense=rnnoise_compute_dense", + "-Dcompute_band_energy=rnnoise_compute_band_energy", + "-Dcompute_band_corr=rnnoise_compute_band_corr", + "-D_celt_lpc=rnnoise__celt_lpc", + "-Dcelt_iir=rnnoise_celt_iir", + "-Dcelt_fir=rnnoise_celt_fir", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.Telegram.rnnoise; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = DebugAppStore; + }; + 27D1D5FF2611F6EF00684DEA /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = rnnoise/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + OTHER_CFLAGS = ( + "-Dcelt_iir=rnnoise_celt_iir", + "-D_celt_autocorr=rnnoise__celt_autocorr", + "-D_celt_lpc=rnnoise__celt_lpc", + "-Dcelt_pitch_xcorr=rnnoise_celt_pitch_xcorr", + "-Dremove_doubling=rnnoise_remove_doubling", + "-Dpitch_search=rnnoise_pitch_search", + "-Dpitch_filter=rnnoise_pitch_filter", + "-Dpitch_downsample=rnnoise_pitch_downsample", + "-Dopus_ifft_c=rnnoise_opus_ifft_c", + "-Dopus_fft_impl=rnnoise_opus_fft_impl", + "-Dopus_fft_free_arch_c=rnnoise_opus_fft_free_arch_c", + "-Dopus_fft_free=rnnoise_opus_fft_free", + "-Dopus_fft_c=rnnoise_opus_fft_c", + "-Dopus_fft_alloc_twiddles=rnnoise_opus_fft_alloc_twiddles", + "-Dopus_fft_alloc_arch_c=rnnoise_opus_fft_alloc_arch_c", + "-Dopus_fft_alloc=rnnoise_opus_fft_alloc", + "-Dinterp_band_gain=rnnoise_interp_band_gain", + "-Dcompute_rnn=rnnoise_compute_rnn", + "-Dcompute_gru=rnnoise_compute_gru", + "-Dcompute_dense=rnnoise_compute_dense", + "-Dcompute_band_energy=rnnoise_compute_band_energy", + "-Dcompute_band_corr=rnnoise_compute_band_corr", + "-D_celt_lpc=rnnoise__celt_lpc", + "-Dcelt_iir=rnnoise_celt_iir", + "-Dcelt_fir=rnnoise_celt_fir", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.Telegram.rnnoise; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = ReleaseAppStore; + }; + 27D1D6322611FB2500684DEA /* Github */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + 27D1D6332611FB2500684DEA /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = rnnoise/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + OTHER_CFLAGS = ( + "-Dcelt_iir=rnnoise_celt_iir", + "-D_celt_autocorr=rnnoise__celt_autocorr", + "-D_celt_lpc=rnnoise__celt_lpc", + "-Dcelt_pitch_xcorr=rnnoise_celt_pitch_xcorr", + "-Dremove_doubling=rnnoise_remove_doubling", + "-Dpitch_search=rnnoise_pitch_search", + "-Dpitch_filter=rnnoise_pitch_filter", + "-Dpitch_downsample=rnnoise_pitch_downsample", + "-Dopus_ifft_c=rnnoise_opus_ifft_c", + "-Dopus_fft_impl=rnnoise_opus_fft_impl", + "-Dopus_fft_free_arch_c=rnnoise_opus_fft_free_arch_c", + "-Dopus_fft_free=rnnoise_opus_fft_free", + "-Dopus_fft_c=rnnoise_opus_fft_c", + "-Dopus_fft_alloc_twiddles=rnnoise_opus_fft_alloc_twiddles", + "-Dopus_fft_alloc_arch_c=rnnoise_opus_fft_alloc_arch_c", + "-Dopus_fft_alloc=rnnoise_opus_fft_alloc", + "-Dinterp_band_gain=rnnoise_interp_band_gain", + "-Dcompute_rnn=rnnoise_compute_rnn", + "-Dcompute_gru=rnnoise_compute_gru", + "-Dcompute_dense=rnnoise_compute_dense", + "-Dcompute_band_energy=rnnoise_compute_band_energy", + "-Dcompute_band_corr=rnnoise_compute_band_corr", + "-D_celt_lpc=rnnoise__celt_lpc", + "-Dcelt_iir=rnnoise_celt_iir", + "-Dcelt_fir=rnnoise_celt_fir", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.Telegram.rnnoise; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = Github; + }; + 27D1D6342611FB3300684DEA /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + 27D1D6352611FB3300684DEA /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = rnnoise/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + OTHER_CFLAGS = ( + "-Dcelt_iir=rnnoise_celt_iir", + "-D_celt_autocorr=rnnoise__celt_autocorr", + "-D_celt_lpc=rnnoise__celt_lpc", + "-Dcelt_pitch_xcorr=rnnoise_celt_pitch_xcorr", + "-Dremove_doubling=rnnoise_remove_doubling", + "-Dpitch_search=rnnoise_pitch_search", + "-Dpitch_filter=rnnoise_pitch_filter", + "-Dpitch_downsample=rnnoise_pitch_downsample", + "-Dopus_ifft_c=rnnoise_opus_ifft_c", + "-Dopus_fft_impl=rnnoise_opus_fft_impl", + "-Dopus_fft_free_arch_c=rnnoise_opus_fft_free_arch_c", + "-Dopus_fft_free=rnnoise_opus_fft_free", + "-Dopus_fft_c=rnnoise_opus_fft_c", + "-Dopus_fft_alloc_twiddles=rnnoise_opus_fft_alloc_twiddles", + "-Dopus_fft_alloc_arch_c=rnnoise_opus_fft_alloc_arch_c", + "-Dopus_fft_alloc=rnnoise_opus_fft_alloc", + "-Dinterp_band_gain=rnnoise_interp_band_gain", + "-Dcompute_rnn=rnnoise_compute_rnn", + "-Dcompute_gru=rnnoise_compute_gru", + "-Dcompute_dense=rnnoise_compute_dense", + "-Dcompute_band_energy=rnnoise_compute_band_energy", + "-Dcompute_band_corr=rnnoise_compute_band_corr", + "-D_celt_lpc=rnnoise__celt_lpc", + "-Dcelt_iir=rnnoise_celt_iir", + "-Dcelt_fir=rnnoise_celt_fir", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.Telegram.rnnoise; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = ReleaseHockeyapp; + }; + 27D1D6362611FB3A00684DEA /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + 27D1D6372611FB3A00684DEA /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = rnnoise/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + OTHER_CFLAGS = ( + "-Dcelt_iir=rnnoise_celt_iir", + "-D_celt_autocorr=rnnoise__celt_autocorr", + "-D_celt_lpc=rnnoise__celt_lpc", + "-Dcelt_pitch_xcorr=rnnoise_celt_pitch_xcorr", + "-Dremove_doubling=rnnoise_remove_doubling", + "-Dpitch_search=rnnoise_pitch_search", + "-Dpitch_filter=rnnoise_pitch_filter", + "-Dpitch_downsample=rnnoise_pitch_downsample", + "-Dopus_ifft_c=rnnoise_opus_ifft_c", + "-Dopus_fft_impl=rnnoise_opus_fft_impl", + "-Dopus_fft_free_arch_c=rnnoise_opus_fft_free_arch_c", + "-Dopus_fft_free=rnnoise_opus_fft_free", + "-Dopus_fft_c=rnnoise_opus_fft_c", + "-Dopus_fft_alloc_twiddles=rnnoise_opus_fft_alloc_twiddles", + "-Dopus_fft_alloc_arch_c=rnnoise_opus_fft_alloc_arch_c", + "-Dopus_fft_alloc=rnnoise_opus_fft_alloc", + "-Dinterp_band_gain=rnnoise_interp_band_gain", + "-Dcompute_rnn=rnnoise_compute_rnn", + "-Dcompute_gru=rnnoise_compute_gru", + "-Dcompute_dense=rnnoise_compute_dense", + "-Dcompute_band_energy=rnnoise_compute_band_energy", + "-Dcompute_band_corr=rnnoise_compute_band_corr", + "-D_celt_lpc=rnnoise__celt_lpc", + "-Dcelt_iir=rnnoise_celt_iir", + "-Dcelt_fir=rnnoise_celt_fir", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.Telegram.rnnoise; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = DebugHockeyapp; + }; + 27D1D6382611FB4000684DEA /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + 27D1D6392611FB4000684DEA /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = rnnoise/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.11; + OTHER_CFLAGS = ( + "-Dcelt_iir=rnnoise_celt_iir", + "-D_celt_autocorr=rnnoise__celt_autocorr", + "-D_celt_lpc=rnnoise__celt_lpc", + "-Dcelt_pitch_xcorr=rnnoise_celt_pitch_xcorr", + "-Dremove_doubling=rnnoise_remove_doubling", + "-Dpitch_search=rnnoise_pitch_search", + "-Dpitch_filter=rnnoise_pitch_filter", + "-Dpitch_downsample=rnnoise_pitch_downsample", + "-Dopus_ifft_c=rnnoise_opus_ifft_c", + "-Dopus_fft_impl=rnnoise_opus_fft_impl", + "-Dopus_fft_free_arch_c=rnnoise_opus_fft_free_arch_c", + "-Dopus_fft_free=rnnoise_opus_fft_free", + "-Dopus_fft_c=rnnoise_opus_fft_c", + "-Dopus_fft_alloc_twiddles=rnnoise_opus_fft_alloc_twiddles", + "-Dopus_fft_alloc_arch_c=rnnoise_opus_fft_alloc_arch_c", + "-Dopus_fft_alloc=rnnoise_opus_fft_alloc", + "-Dinterp_band_gain=rnnoise_interp_band_gain", + "-Dcompute_rnn=rnnoise_compute_rnn", + "-Dcompute_gru=rnnoise_compute_gru", + "-Dcompute_dense=rnnoise_compute_dense", + "-Dcompute_band_energy=rnnoise_compute_band_energy", + "-Dcompute_band_corr=rnnoise_compute_band_corr", + "-D_celt_lpc=rnnoise__celt_lpc", + "-Dcelt_iir=rnnoise_celt_iir", + "-Dcelt_fir=rnnoise_celt_fir", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.Telegram.rnnoise; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = HockeyappMacAlpha; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 27D1D5EF2611F6EF00684DEA /* Build configuration list for PBXProject "rnnoise" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 27D1D5FB2611F6EF00684DEA /* DebugAppStore */, + 27D1D6322611FB2500684DEA /* Github */, + 27D1D5FC2611F6EF00684DEA /* ReleaseAppStore */, + 27D1D6342611FB3300684DEA /* ReleaseHockeyapp */, + 27D1D6362611FB3A00684DEA /* DebugHockeyapp */, + 27D1D6382611FB4000684DEA /* HockeyappMacAlpha */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; + 27D1D5FD2611F6EF00684DEA /* Build configuration list for PBXNativeTarget "rnnoise" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 27D1D5FE2611F6EF00684DEA /* DebugAppStore */, + 27D1D6332611FB2500684DEA /* Github */, + 27D1D5FF2611F6EF00684DEA /* ReleaseAppStore */, + 27D1D6352611FB3300684DEA /* ReleaseHockeyapp */, + 27D1D6372611FB3A00684DEA /* DebugHockeyapp */, + 27D1D6392611FB4000684DEA /* HockeyappMacAlpha */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; +/* End XCConfigurationList section */ + }; + rootObject = 27D1D5EC2611F6EF00684DEA /* Project object */; +} diff --git a/core-xprojects/rnnoise/rnnoise.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/rnnoise/rnnoise.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/core-xprojects/rnnoise/rnnoise.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/rnnoise/rnnoise.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/rnnoise/rnnoise.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/rnnoise/rnnoise.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/rnnoise/rnnoise/Info.plist b/core-xprojects/rnnoise/rnnoise/Info.plist new file mode 100644 index 0000000000..9bcb244429 --- /dev/null +++ b/core-xprojects/rnnoise/rnnoise/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/core-xprojects/rnnoise/rnnoise/_rnnoise.h b/core-xprojects/rnnoise/rnnoise/_rnnoise.h new file mode 100644 index 0000000000..3cc84089ee --- /dev/null +++ b/core-xprojects/rnnoise/rnnoise/_rnnoise.h @@ -0,0 +1,19 @@ +// +// rnnoise.h +// rnnoise +// +// Created by Mikhail Filimonov on 29.03.2021. +// + +#import + +//! Project version number for rnnoise. +FOUNDATION_EXPORT double rnnoiseVersionNumber; + +//! Project version string for rnnoise. +FOUNDATION_EXPORT const unsigned char rnnoiseVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + +#import + diff --git a/core-xprojects/sqlcipher/sqlcipher/Info.plist b/core-xprojects/sqlcipher/sqlcipher/Info.plist new file mode 100644 index 0000000000..0c4389ac7e --- /dev/null +++ b/core-xprojects/sqlcipher/sqlcipher/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2019 Telegram. All rights reserved. + + diff --git a/core-xprojects/sqlcipher/sqlcipher/sqlcipher.h b/core-xprojects/sqlcipher/sqlcipher/sqlcipher.h new file mode 100644 index 0000000000..3ce7d554c8 --- /dev/null +++ b/core-xprojects/sqlcipher/sqlcipher/sqlcipher.h @@ -0,0 +1,22 @@ +// +// sqlcipher.h +// sqlcipher +// +// Created by Mikhail Filimonov on 31.10.2019. +// Copyright © 2019 Telegram. All rights reserved. +// + +#import + +//! Project version number for sqlcipher. +FOUNDATION_EXPORT double sqlcipherVersionNumber; + +//! Project version string for sqlcipher. +FOUNDATION_EXPORT const unsigned char sqlcipherVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + +#import +#import + diff --git a/core-xprojects/sqlcipher/sqlcipher_Xcode.xcodeproj/project.pbxproj b/core-xprojects/sqlcipher/sqlcipher_Xcode.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..22b8923dca --- /dev/null +++ b/core-xprojects/sqlcipher/sqlcipher_Xcode.xcodeproj/project.pbxproj @@ -0,0 +1,774 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + A701F902236B2EF8002ABF81 /* sqlcipher.h in Headers */ = {isa = PBXBuildFile; fileRef = A701F900236B2EF8002ABF81 /* sqlcipher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A701F911236B2F01002ABF81 /* sqlite3.c in Sources */ = {isa = PBXBuildFile; fileRef = A701F909236B2F01002ABF81 /* sqlite3.c */; }; + A7918DBA240CEDE5002011CA /* sqlite3.h in Headers */ = {isa = PBXBuildFile; fileRef = A7918DB7240CEDE5002011CA /* sqlite3.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A7918DBB240CEDE5002011CA /* sqlite3ext.h in Headers */ = {isa = PBXBuildFile; fileRef = A7918DB8240CEDE5002011CA /* sqlite3ext.h */; settings = {ATTRIBUTES = (Public, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A701F8FD236B2EF8002ABF81 /* sqlcipher.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = sqlcipher.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A701F900236B2EF8002ABF81 /* sqlcipher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = sqlcipher.h; sourceTree = ""; }; + A701F901236B2EF8002ABF81 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A701F909236B2F01002ABF81 /* sqlite3.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = sqlite3.c; sourceTree = ""; }; + A701F90A236B2F01002ABF81 /* SQLite-Bridging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SQLite-Bridging.h"; sourceTree = ""; }; + A701F90B236B2F01002ABF81 /* sqlcipher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = sqlcipher.h; sourceTree = ""; }; + A701F90C236B2F01002ABF81 /* sqlcipher_config.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = sqlcipher_config.h; sourceTree = ""; }; + A701F90D236B2F01002ABF81 /* sqlite3.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = sqlite3.h; sourceTree = ""; }; + A701F90E236B2F01002ABF81 /* sqlite3ext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = sqlite3ext.h; sourceTree = ""; }; + A701F90F236B2F01002ABF81 /* SQLite-Bridging.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "SQLite-Bridging.m"; sourceTree = ""; }; + A701F910236B2F01002ABF81 /* fts3_tokenizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = fts3_tokenizer.h; sourceTree = ""; }; + A701F922236B3172002ABF81 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; }; + A7918DB7240CEDE5002011CA /* sqlite3.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = sqlite3.h; path = "../../submodules/telegram-ios/submodules/sqlcipher/PublicHeaders/sqlcipher/sqlite3.h"; sourceTree = ""; }; + A7918DB8240CEDE5002011CA /* sqlite3ext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = sqlite3ext.h; path = "../../submodules/telegram-ios/submodules/sqlcipher/PublicHeaders/sqlcipher/sqlite3ext.h"; sourceTree = ""; }; + A7918DB9240CEDE5002011CA /* sqlcipher_config.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = sqlcipher_config.h; path = "../../submodules/telegram-ios/submodules/sqlcipher/PublicHeaders/sqlcipher/sqlcipher_config.h"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A701F8FA236B2EF8002ABF81 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A701F8F3236B2EF8002ABF81 = { + isa = PBXGroup; + children = ( + A7918DB9240CEDE5002011CA /* sqlcipher_config.h */, + A7918DB7240CEDE5002011CA /* sqlite3.h */, + A7918DB8240CEDE5002011CA /* sqlite3ext.h */, + A701F908236B2F01002ABF81 /* Sources */, + A701F8FF236B2EF8002ABF81 /* sqlcipher */, + A701F8FE236B2EF8002ABF81 /* Products */, + A701F921236B3171002ABF81 /* Frameworks */, + ); + sourceTree = ""; + }; + A701F8FE236B2EF8002ABF81 /* Products */ = { + isa = PBXGroup; + children = ( + A701F8FD236B2EF8002ABF81 /* sqlcipher.framework */, + ); + name = Products; + sourceTree = ""; + }; + A701F8FF236B2EF8002ABF81 /* sqlcipher */ = { + isa = PBXGroup; + children = ( + A701F900236B2EF8002ABF81 /* sqlcipher.h */, + A701F901236B2EF8002ABF81 /* Info.plist */, + ); + path = sqlcipher; + sourceTree = ""; + }; + A701F908236B2F01002ABF81 /* Sources */ = { + isa = PBXGroup; + children = ( + A701F909236B2F01002ABF81 /* sqlite3.c */, + A701F90A236B2F01002ABF81 /* SQLite-Bridging.h */, + A701F90B236B2F01002ABF81 /* sqlcipher.h */, + A701F90C236B2F01002ABF81 /* sqlcipher_config.h */, + A701F90D236B2F01002ABF81 /* sqlite3.h */, + A701F90E236B2F01002ABF81 /* sqlite3ext.h */, + A701F90F236B2F01002ABF81 /* SQLite-Bridging.m */, + A701F910236B2F01002ABF81 /* fts3_tokenizer.h */, + ); + name = Sources; + path = "../../submodules/telegram-ios/submodules/sqlcipher/Sources"; + sourceTree = ""; + }; + A701F921236B3171002ABF81 /* Frameworks */ = { + isa = PBXGroup; + children = ( + A701F922236B3172002ABF81 /* libsqlite3.tbd */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + A701F8F8236B2EF8002ABF81 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + A7918DBA240CEDE5002011CA /* sqlite3.h in Headers */, + A7918DBB240CEDE5002011CA /* sqlite3ext.h in Headers */, + A701F902236B2EF8002ABF81 /* sqlcipher.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + A701F8FC236B2EF8002ABF81 /* sqlcipher */ = { + isa = PBXNativeTarget; + buildConfigurationList = A701F905236B2EF8002ABF81 /* Build configuration list for PBXNativeTarget "sqlcipher" */; + buildPhases = ( + A701F8F8236B2EF8002ABF81 /* Headers */, + A701F8F9236B2EF8002ABF81 /* Sources */, + A701F8FA236B2EF8002ABF81 /* Frameworks */, + A701F8FB236B2EF8002ABF81 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = sqlcipher; + productName = sqlcipher; + productReference = A701F8FD236B2EF8002ABF81 /* sqlcipher.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A701F8F4236B2EF8002ABF81 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1030; + ORGANIZATIONNAME = Telegram; + TargetAttributes = { + A701F8FC236B2EF8002ABF81 = { + CreatedOnToolsVersion = 10.3; + }; + }; + }; + buildConfigurationList = A701F8F7236B2EF8002ABF81 /* Build configuration list for PBXProject "sqlcipher_Xcode" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = A701F8F3236B2EF8002ABF81; + productRefGroup = A701F8FE236B2EF8002ABF81 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A701F8FC236B2EF8002ABF81 /* sqlcipher */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A701F8FB236B2EF8002ABF81 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A701F8F9236B2EF8002ABF81 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A701F911236B2F01002ABF81 /* sqlite3.c in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A701F903236B2EF8002ABF81 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + A701F904236B2EF8002ABF81 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + A701F906236B2EF8002ABF81 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_PREPROCESSOR_DEFINITIONS = ""; + INFOPLIST_FILE = sqlcipher/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = ( + "-DSQLITE_HAS_CODEC=1", + "-DSQLCIPHER_CRYPTO_CC=1", + "-DSQLITE_ENABLE_FTS5", + "-DSQLITE_DEFAULT_MEMSTATUS=0", + "-DNDEBUG", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.sqlcipher; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = DebugAppStore; + }; + A701F907236B2EF8002ABF81 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = sqlcipher/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + OTHER_CFLAGS = ( + "-DSQLITE_HAS_CODEC=1", + "-DSQLCIPHER_CRYPTO_CC=1", + "-DSQLITE_ENABLE_FTS5", + "-DSQLITE_DEFAULT_MEMSTATUS=0", + "-DNDEBUG", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.sqlcipher; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = ReleaseAppStore; + }; + A701F91A236B2F84002ABF81 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + A701F91B236B2F84002ABF81 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = sqlcipher/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + OTHER_CFLAGS = ( + "-DSQLITE_HAS_CODEC=1", + "-DSQLCIPHER_CRYPTO_CC=1", + "-DSQLITE_ENABLE_FTS5", + "-DSQLITE_DEFAULT_MEMSTATUS=0", + "-DNDEBUG", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.sqlcipher; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = ReleaseHockeyapp; + }; + A701F91C236B2F89002ABF81 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + A701F91D236B2F89002ABF81 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = sqlcipher/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + OTHER_CFLAGS = ( + "-DSQLITE_HAS_CODEC=1", + "-DSQLCIPHER_CRYPTO_CC=1", + "-DSQLITE_ENABLE_FTS5", + "-DSQLITE_DEFAULT_MEMSTATUS=0", + "-DNDEBUG", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.sqlcipher; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = DebugHockeyapp; + }; + A701F91E236B2F90002ABF81 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + A701F91F236B2F90002ABF81 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = sqlcipher/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + OTHER_CFLAGS = ( + "-DSQLITE_HAS_CODEC=1", + "-DSQLCIPHER_CRYPTO_CC=1", + "-DSQLITE_ENABLE_FTS5", + "-DSQLITE_DEFAULT_MEMSTATUS=0", + "-DNDEBUG", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.sqlcipher; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = HockeyappMacAlpha; + }; + A7F282DA238EAB4300742C20 /* Github */ = { + isa = XCBuildConfiguration; + 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_IDENTITY = "Mac Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + A7F282DB238EAB4300742C20 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_PREPROCESSOR_DEFINITIONS = ""; + INFOPLIST_FILE = sqlcipher/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + OTHER_CFLAGS = ( + "-DSQLITE_HAS_CODEC=1", + "-DSQLCIPHER_CRYPTO_CC=1", + "-DSQLITE_ENABLE_FTS5", + "-DSQLITE_DEFAULT_MEMSTATUS=0", + "-DNDEBUG", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.sqlcipher; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + }; + name = Github; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A701F8F7236B2EF8002ABF81 /* Build configuration list for PBXProject "sqlcipher_Xcode" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A701F903236B2EF8002ABF81 /* DebugAppStore */, + A7F282DA238EAB4300742C20 /* Github */, + A701F904236B2EF8002ABF81 /* ReleaseAppStore */, + A701F91A236B2F84002ABF81 /* ReleaseHockeyapp */, + A701F91C236B2F89002ABF81 /* DebugHockeyapp */, + A701F91E236B2F90002ABF81 /* HockeyappMacAlpha */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; + A701F905236B2EF8002ABF81 /* Build configuration list for PBXNativeTarget "sqlcipher" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A701F906236B2EF8002ABF81 /* DebugAppStore */, + A7F282DB238EAB4300742C20 /* Github */, + A701F907236B2EF8002ABF81 /* ReleaseAppStore */, + A701F91B236B2F84002ABF81 /* ReleaseHockeyapp */, + A701F91D236B2F89002ABF81 /* DebugHockeyapp */, + A701F91F236B2F90002ABF81 /* HockeyappMacAlpha */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; +/* End XCConfigurationList section */ + }; + rootObject = A701F8F4236B2EF8002ABF81 /* Project object */; +} diff --git a/core-xprojects/sqlcipher/sqlcipher_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/sqlcipher/sqlcipher_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..9f308709d3 --- /dev/null +++ b/core-xprojects/sqlcipher/sqlcipher_Xcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/sqlcipher/sqlcipher_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/sqlcipher/sqlcipher_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/sqlcipher/sqlcipher_Xcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/sqlcipher/sqlcipher_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate b/core-xprojects/sqlcipher/sqlcipher_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000..83ccf0d58e Binary files /dev/null and b/core-xprojects/sqlcipher/sqlcipher_Xcode.xcodeproj/project.xcworkspace/xcuserdata/overtake.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/core-xprojects/sqlcipher/sqlcipher_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/sqlcipher.xcscheme b/core-xprojects/sqlcipher/sqlcipher_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/sqlcipher.xcscheme new file mode 100644 index 0000000000..5d56c02aed --- /dev/null +++ b/core-xprojects/sqlcipher/sqlcipher_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/sqlcipher.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/sqlcipher/sqlcipher_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist b/core-xprojects/sqlcipher/sqlcipher_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000000..90ffb252fb --- /dev/null +++ b/core-xprojects/sqlcipher/sqlcipher_Xcode.xcodeproj/xcuserdata/overtake.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,24 @@ + + + + + SchemeUserState + + sqlcipher.xcscheme + + isShown + + orderHint + 9 + + + SuppressBuildableAutocreation + + A701F8FC236B2EF8002ABF81 + + primary + + + + + diff --git a/core-xprojects/webrtc/webrtc.xcodeproj/project.pbxproj b/core-xprojects/webrtc/webrtc.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..5dbc4405ac --- /dev/null +++ b/core-xprojects/webrtc/webrtc.xcodeproj/project.pbxproj @@ -0,0 +1,760 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + D0076E8D256853F7007EF588 /* OpenSSLEncryption.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0983C122566CCD900467703 /* OpenSSLEncryption.framework */; }; + D0983C002566C86800467703 /* webrtc.h in Headers */ = {isa = PBXBuildFile; fileRef = D0983BFE2566C86800467703 /* webrtc.h */; settings = {ATTRIBUTES = (Public, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + D0983BFB2566C86800467703 /* webrtc.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = webrtc.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0983BFE2566C86800467703 /* webrtc.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = webrtc.h; sourceTree = ""; }; + D0983BFF2566C86800467703 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D0983C122566CCD900467703 /* OpenSSLEncryption.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OpenSSLEncryption.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0983C41256815B200467703 /* libmac_framework_objc_static.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libmac_framework_objc_static.a; path = build/webrtc/libmac_framework_objc_static.a; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D0983BF82566C86800467703 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D0076E8D256853F7007EF588 /* OpenSSLEncryption.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + D0983BF12566C86800467703 = { + isa = PBXGroup; + children = ( + D0983BFD2566C86800467703 /* webrtc */, + D0983BFC2566C86800467703 /* Products */, + D0983C112566CCD900467703 /* Frameworks */, + ); + sourceTree = ""; + }; + D0983BFC2566C86800467703 /* Products */ = { + isa = PBXGroup; + children = ( + D0983BFB2566C86800467703 /* webrtc.framework */, + ); + name = Products; + sourceTree = ""; + }; + D0983BFD2566C86800467703 /* webrtc */ = { + isa = PBXGroup; + children = ( + D0983BFE2566C86800467703 /* webrtc.h */, + D0983BFF2566C86800467703 /* Info.plist */, + ); + path = webrtc; + sourceTree = ""; + }; + D0983C112566CCD900467703 /* Frameworks */ = { + isa = PBXGroup; + children = ( + D0983C41256815B200467703 /* libmac_framework_objc_static.a */, + D0983C122566CCD900467703 /* OpenSSLEncryption.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + D0983BF62566C86800467703 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D0983C002566C86800467703 /* webrtc.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + D0983BFA2566C86800467703 /* webrtc */ = { + isa = PBXNativeTarget; + buildConfigurationList = D0983C032566C86800467703 /* Build configuration list for PBXNativeTarget "webrtc" */; + buildPhases = ( + D0983C102566C9DF00467703 /* ShellScript */, + D0983BF62566C86800467703 /* Headers */, + D0983BF72566C86800467703 /* Sources */, + D0983BF82566C86800467703 /* Frameworks */, + D0983BF92566C86800467703 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = webrtc; + productName = webrtc; + productReference = D0983BFB2566C86800467703 /* webrtc.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D0983BF22566C86800467703 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1220; + TargetAttributes = { + D0983BFA2566C86800467703 = { + CreatedOnToolsVersion = 12.2; + }; + }; + }; + buildConfigurationList = D0983BF52566C86800467703 /* Build configuration list for PBXProject "webrtc" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = D0983BF12566C86800467703; + productRefGroup = D0983BFC2566C86800467703 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D0983BFA2566C86800467703 /* webrtc */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D0983BF92566C86800467703 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + D0983C102566C9DF00467703 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ ! -d \"$PROJECT_DIR/build\" ]; then\nsh $PROJECT_DIR/webrtc/build.sh $PROJECT_DIR/../../submodules/tg_owt/ $PROJECT_DIR/../OpenSSLEncryption/build/openssl/include $PROJECT_DIR/../../submodules/telegram-ios/third-party/mozjpeg/mozjpeg $PROJECT_DIR/../libopus/build/libopus/include/opus $PROJECT_DIR/../ffmpeg/build/ffmpeg/include\nfi\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D0983BF72566C86800467703 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + D0983C012566C86800467703 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStore; + }; + D0983C022566C86800467703 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStore; + }; + D0983C042566C86800467703 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREPROCESSOR_DEFINITIONS = ""; + INFOPLIST_FILE = webrtc/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/build/webrtc", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.webrtc; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = DebugAppStore; + }; + D0983C052566C86800467703 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREPROCESSOR_DEFINITIONS = ""; + INFOPLIST_FILE = webrtc/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/build/webrtc", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + OTHER_CFLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.webrtc; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = ReleaseAppStore; + }; + D0983C082566C93F00467703 /* Github */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Github; + }; + D0983C092566C93F00467703 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREPROCESSOR_DEFINITIONS = ""; + INFOPLIST_FILE = webrtc/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/build/webrtc", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + OTHER_CFLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.webrtc; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = Github; + }; + D0983C0A2566C94800467703 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyapp; + }; + D0983C0B2566C94800467703 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREPROCESSOR_DEFINITIONS = ""; + INFOPLIST_FILE = webrtc/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/build/webrtc", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + OTHER_CFLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.webrtc; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = ReleaseHockeyapp; + }; + D0983C0C2566C95100467703 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + D0983C0D2566C95100467703 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREPROCESSOR_DEFINITIONS = ""; + INFOPLIST_FILE = webrtc/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/build/webrtc", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + OTHER_CFLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.webrtc; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = DebugHockeyapp; + }; + D0983C0E2566C95800467703 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = HockeyappMacAlpha; + }; + D0983C0F2566C95800467703 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 6N38VWS5BX; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREPROCESSOR_DEFINITIONS = ""; + INFOPLIST_FILE = webrtc/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/build/webrtc", + ); + MACH_O_TYPE = staticlib; + ONLY_ACTIVE_ARCH = NO; + OTHER_CFLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.webrtc; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + }; + name = HockeyappMacAlpha; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D0983BF52566C86800467703 /* Build configuration list for PBXProject "webrtc" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0983C012566C86800467703 /* DebugAppStore */, + D0983C082566C93F00467703 /* Github */, + D0983C022566C86800467703 /* ReleaseAppStore */, + D0983C0A2566C94800467703 /* ReleaseHockeyapp */, + D0983C0C2566C95100467703 /* DebugHockeyapp */, + D0983C0E2566C95800467703 /* HockeyappMacAlpha */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; + D0983C032566C86800467703 /* Build configuration list for PBXNativeTarget "webrtc" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D0983C042566C86800467703 /* DebugAppStore */, + D0983C092566C93F00467703 /* Github */, + D0983C052566C86800467703 /* ReleaseAppStore */, + D0983C0B2566C94800467703 /* ReleaseHockeyapp */, + D0983C0D2566C95100467703 /* DebugHockeyapp */, + D0983C0F2566C95800467703 /* HockeyappMacAlpha */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; +/* End XCConfigurationList section */ + }; + rootObject = D0983BF22566C86800467703 /* Project object */; +} diff --git a/core-xprojects/webrtc/webrtc.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/core-xprojects/webrtc/webrtc.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/core-xprojects/webrtc/webrtc.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/core-xprojects/webrtc/webrtc.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/core-xprojects/webrtc/webrtc.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/core-xprojects/webrtc/webrtc.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/core-xprojects/webrtc/webrtc.xcodeproj/xcshareddata/xcschemes/webrtc.xcscheme b/core-xprojects/webrtc/webrtc.xcodeproj/xcshareddata/xcschemes/webrtc.xcscheme new file mode 100644 index 0000000000..5b9497dba4 --- /dev/null +++ b/core-xprojects/webrtc/webrtc.xcodeproj/xcshareddata/xcschemes/webrtc.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core-xprojects/webrtc/webrtc/Info.plist b/core-xprojects/webrtc/webrtc/Info.plist new file mode 100644 index 0000000000..9bcb244429 --- /dev/null +++ b/core-xprojects/webrtc/webrtc/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/core-xprojects/webrtc/webrtc/build-gn.sh b/core-xprojects/webrtc/webrtc/build-gn.sh new file mode 100755 index 0000000000..316d187830 --- /dev/null +++ b/core-xprojects/webrtc/webrtc/build-gn.sh @@ -0,0 +1,67 @@ +#/bin/sh + +set -x +set -e + +SOURCE_DIR="$1" +DEPOT_DIR="$2" +OPENSSL_DIR="$3" + +BUILD_DIR="${PROJECT_DIR}/build/" + +rm -rf $BUILD_DIR || true +mkdir -p $BUILD_DIR || true + +export PATH="$PATH:$DEPOT_DIR" + + +cp -R $SOURCE_DIR $BUILD_DIR + + + +rm -rf "${BUILD_DIR}src/openssl" +cp -R $OPENSSL_DIR/lib "${BUILD_DIR}src/openssl" +cp -R $OPENSSL_DIR/include "${BUILD_DIR}src/openssl/include" +pushd "${BUILD_DIR}src" + + + +# + +#if [ "$ARCH" == "x64" ]; then +# OUT_DIR="ios_sim" +#fi +# + +LIBS="" +for ARCH in $ARCHS +do + + +CURRENT_ARCH=$ARCH +if [ "$ARCH" == "x86_64" ]; then + CURRENT_ARCH="x64" +fi + +if [ "$ARCH" == "arm64" ]; then + CURRENT_ARCH="arm64" +fi + + +OUT_DIR=$CURRENT_ARCH +buildtools/mac/gn gen out/$OUT_DIR --args="use_xcode_clang=false target_cpu=\"$CURRENT_ARCH\""' target_os="mac" is_debug=false is_component_build=false rtc_include_tests=false use_rtti=true rtc_use_x11=false use_custom_libcxx=false use_custom_libcxx_for_host=false rtc_build_ssl=false rtc_build_examples=false rtc_build_tools=false mac_deployment_target="10.11" is_unsafe_developer_build=false rtc_enable_protobuf=false rtc_include_builtin_video_codecs=true rtc_build_libvpx=true rtc_libvpx_build_vp9=true rtc_use_gtk=false rtc_use_metal_rendering=true mac_sdk_min="11.0" rtc_desktop_capture_supported=false strip_debug_info=true symbol_level=0 ios_enable_code_signing=false desktop_capture=true' +ninja -C out/$OUT_DIR mac_framework_objc_static + +LIBS="$LIBS ${BUILD_DIR}src/out/$OUT_DIR/obj/sdk/libmac_framework_objc_static.a" + +done + +LIB_PATH=${BUILD_DIR}webrtc +mkdir -p $LIB_PATH +lipo -create $LIBS -output "$LIB_PATH/libmac_framework_objc_static.a" || exit 1 +rm -rf ${BUILD_DIR}src/out +# +#popd + + +#--developer_dir diff --git a/core-xprojects/webrtc/webrtc/build.sh b/core-xprojects/webrtc/webrtc/build.sh new file mode 100755 index 0000000000..3c5bd5fb8e --- /dev/null +++ b/core-xprojects/webrtc/webrtc/build.sh @@ -0,0 +1,63 @@ +#/bin/sh + +set -x +set -e + +SOURCE_DIR="$1" + +OPENSSL_DIR="$2" +JPEG_DIR="$3" +OPUS_DIR="$4" +FFMPEG_DIR="$5" + + +BUILD_DIR="${PROJECT_DIR}/build/" + +rm -rf $BUILD_DIR || true +mkdir -p $BUILD_DIR || true + + +cp -R $SOURCE_DIR $BUILD_DIR + + + +LIBS="" +for ARCH in $ARCHS +do + +pushd $BUILD_DIR + +CURRENT_ARCH=$ARCH + + +OUT_DIR=$CURRENT_ARCH +mkdir -p $OUT_DIR || true +cd $OUT_DIR + +cmake -G Ninja \ + -DCMAKE_OSX_ARCHITECTURES=$CURRENT_ARCH \ + -DTG_OWT_SPECIAL_TARGET=mac \ + -DCMAKE_BUILD_TYPE=Release \ + -DTG_OWT_LIBJPEG_INCLUDE_PATH=$JPEG_DIR \ + -DTG_OWT_OPENSSL_INCLUDE_PATH=$OPENSSL_DIR \ + -DTG_OWT_OPUS_INCLUDE_PATH=$OPUS_DIR \ + -DTG_OWT_FFMPEG_INCLUDE_PATH=$FFMPEG_DIR .. + +ninja +LIBS="$LIBS ${BUILD_DIR}$OUT_DIR/libtg_owt.a" + # -DCMAKE_BUILD_TYPE=Debug + +cd .. + +done +# +LIB_PATH=${BUILD_DIR}webrtc +rm -rf $LIB_PATH || true +mkdir -p $LIB_PATH +lipo -create $LIBS -output "$LIB_PATH/libmac_framework_objc_static.a" || exit 1 + + +#popd + + +#--developer_dir diff --git a/core-xprojects/webrtc/webrtc/webrtc.h b/core-xprojects/webrtc/webrtc/webrtc.h new file mode 100644 index 0000000000..1104c3ee4d --- /dev/null +++ b/core-xprojects/webrtc/webrtc/webrtc.h @@ -0,0 +1,18 @@ +// +// webrtc.h +// webrtc +// +// Created by Mikhail Filimonov on 19/11/2020. +// + +#import + +//! Project version number for webrtc. +FOUNDATION_EXPORT double webrtcVersionNumber; + +//! Project version string for webrtc. +FOUNDATION_EXPORT const unsigned char webrtcVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 0000000000..4c182ad2fd --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,64 @@ +# Customise this file, documentation can be found here: +# https://github.com/fastlane/fastlane/tree/master/fastlane/docs +# All available actions: https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Actions.md +# can also be listed using the `fastlane actions` command + +# Change the syntax highlighting to Ruby +# All lines starting with a # are ignored when running `fastlane` + +# If you want to automatically update fastlane if a new version is available: +# update_fastlane + +# This is the minimum version number required. +# Update this, if you use features of a newer version +fastlane_version "2.3.1" + +default_platform :mac + +# Fastfile actions accept additional configuration, but +# don't worry, fastlane will prompt you for required +# info which you can add here later + +lane :beta do + sh("rm -rf ./output") + sh("mkdir ./output") + gym( + scheme: "Hockeyapp", + clean: true, + silent: true, + output_directory: "./output" + ) +end + +lane :alpha do + sh("rm -rf ./output") + sh("mkdir ./output") + gym( + scheme: "HockeyappAlpha", + clean: true, + silent: true, + output_directory: "./output" + ) +end + +lane :appstore do + sh("rm -rf ./output") + sh("mkdir ./output") + gym( + scheme: "store", + clean: true, + silent: true, + output_directory: "./output" + ) +end + +lane :release do + sh("rm -rf ./output") + sh("mkdir ./output") + gym( + scheme: "Release", + clean: true, + silent: true, + output_directory: "./output" + ) +end diff --git a/images/mas_badge.png b/images/mas_badge.png new file mode 100644 index 0000000000..cf1aa85b98 Binary files /dev/null and b/images/mas_badge.png differ diff --git a/images/tg.png b/images/tg.png new file mode 100644 index 0000000000..30b2ebf4b2 Binary files /dev/null and b/images/tg.png differ diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter.xcodeproj/project.pbxproj b/submodules/AppCenter-sdk/AppCenter/AppCenter.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..df3d7fb7e6 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter.xcodeproj/project.pbxproj @@ -0,0 +1,1862 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXAggregateTarget section */ + C229D8AB2546E7E5001909F3 /* AppCenter */ = { + isa = PBXAggregateTarget; + buildConfigurationList = C229D8AC2546E7E5001909F3 /* Build configuration list for PBXAggregateTarget "AppCenter" */; + buildPhases = ( + C26F4E7C2546F406006E0EB7 /* Copy Products */, + ); + dependencies = ( + ); + name = AppCenter; + productName = AppCenter; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 04237DC223A31617009BB406 /* MSACHttpClientDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 04237DC023A31617009BB406 /* MSACHttpClientDelegate.h */; }; + 244CC1F92399CA5A00A58F51 /* MSACDependencyConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = 244CC1F62399CA5A00A58F51 /* MSACDependencyConfiguration.h */; }; + 244CC1FC2399CA5A00A58F51 /* MSACDependencyConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 244CC1F72399CA5A00A58F51 /* MSACDependencyConfiguration.m */; }; + C9A920E6230C61820068070D /* MSACAppCenter.m in Sources */ = {isa = PBXBuildFile; fileRef = 6E0401561D1C9AAA0051BCFA /* MSACAppCenter.m */; }; + C9A920E7230C61820068070D /* MSACLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 3592ABA61DC90E3600EF4592 /* MSACLogger.m */; }; + C9A920E8230C61820068070D /* MSACServiceAbstract.m in Sources */ = {isa = PBXBuildFile; fileRef = D38024051E7126F500466558 /* MSACServiceAbstract.m */; }; + C9A920E9230C61820068070D /* MSACWrapperLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 35D0B7521DDFABFD003EACCD /* MSACWrapperLogger.m */; }; + C9A920EA230C61820068070D /* MSACAppDelegateForwarder.m in Sources */ = {isa = PBXBuildFile; fileRef = 38C4471F1ECE5352002E1B11 /* MSACAppDelegateForwarder.m */; }; + C9A920EB230C61820068070D /* MSACDelegateForwarder.m in Sources */ = {isa = PBXBuildFile; fileRef = 38032086217E85A90089772A /* MSACDelegateForwarder.m */; }; + C9A920EC230C61820068070D /* MSACChannelGroupDefault.m in Sources */ = {isa = PBXBuildFile; fileRef = E84B8E2D1D2351DB006FD231 /* MSACChannelGroupDefault.m */; }; + C9A920ED230C61820068070D /* MSACChannelUnitConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 6EF628F31D371B1600CAFF64 /* MSACChannelUnitConfiguration.m */; }; + C9A920EE230C61820068070D /* MSACChannelUnitDefault.m in Sources */ = {isa = PBXBuildFile; fileRef = 6E0684621D36BC8D00A8CC6C /* MSACChannelUnitDefault.m */; }; + C9A920EF230C61820068070D /* MSACOneCollectorChannelDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 805275C020A1A5F400704115 /* MSACOneCollectorChannelDelegate.m */; }; + C9A920F0230C61820068070D /* MSACCSEpochAndSeq.m in Sources */ = {isa = PBXBuildFile; fileRef = 3814A8E520BF5FA00093AF45 /* MSACCSEpochAndSeq.m */; }; + C9A920F4230C61820068070D /* MSACDeviceHistoryInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = B2FD53611E56501B0050F909 /* MSACDeviceHistoryInfo.m */; }; + C9A920F5230C61820068070D /* MSACDeviceTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = 04754F3C1EA980FD002CBA46 /* MSACDeviceTracker.m */; }; + C9A920F6230C61820068070D /* MSACSessionContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 045C85131FE0B31600089540 /* MSACSessionContext.m */; }; + C9A920F7230C61820068070D /* MSACSessionHistoryInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 047EBB9B1FE30842009BB1C8 /* MSACSessionHistoryInfo.m */; }; + C9A920F8230C61820068070D /* MSACUserIdContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 047FEE1021A48CC200ED77CD /* MSACUserIdContext.m */; }; + C9A920F9230C61820068070D /* MSACUserIdHistoryInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 047FEE1321A48CC200ED77CD /* MSACUserIdHistoryInfo.m */; }; + C9A920FA230C61820068070D /* MSACHttpUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 3571F659224962180052406C /* MSACHttpUtil.m */; }; + C9A920FB230C61820068070D /* MSACHttpClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 2DA0350241FEC3A6A4919BFE /* MSACHttpClient.m */; }; + C9A920FC230C61820068070D /* MSACHttpCall.m in Sources */ = {isa = PBXBuildFile; fileRef = 3571F65322457FCC0052406C /* MSACHttpCall.m */; }; + C9A920FD230C61820068070D /* MSACTicketCache.m in Sources */ = {isa = PBXBuildFile; fileRef = B23507B32118D22800F98D4F /* MSACTicketCache.m */; }; + C9A920FF230C61820068070D /* MSACHttpIngestion.m in Sources */ = {isa = PBXBuildFile; fileRef = E84B8E341D235226006FD231 /* MSACHttpIngestion.m */; }; + C9A92100230C61820068070D /* MSACAppCenterIngestion.m in Sources */ = {isa = PBXBuildFile; fileRef = BA6822A89D38F04D8D95CE83 /* MSACAppCenterIngestion.m */; }; + C9A92101230C61820068070D /* MSACOneCollectorIngestion.m in Sources */ = {isa = PBXBuildFile; fileRef = B2B7D50A20C5E562001D31B8 /* MSACOneCollectorIngestion.m */; }; + C9A92102230C61820068070D /* MSACAppExtension.m in Sources */ = {isa = PBXBuildFile; fileRef = E74B14D820B37042002C0183 /* MSACAppExtension.m */; }; + C9A92103230C61820068070D /* MSACCommonSchemaLog.m in Sources */ = {isa = PBXBuildFile; fileRef = E74B149820B364EE002C0183 /* MSACCommonSchemaLog.m */; }; + C9A92104230C61820068070D /* MSACCSData.m in Sources */ = {isa = PBXBuildFile; fileRef = E7D23C5520B4E38B00A47D62 /* MSACCSData.m */; }; + C9A92105230C61820068070D /* MSACCSExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = E7D23C6520B6391700A47D62 /* MSACCSExtensions.m */; }; + C9A92106230C61820068070D /* MSACDeviceExtension.m in Sources */ = {isa = PBXBuildFile; fileRef = 359C38DC214079D90066C509 /* MSACDeviceExtension.m */; }; + C9A92107230C61820068070D /* MSACLocExtension.m in Sources */ = {isa = PBXBuildFile; fileRef = E74B14A920B36B9B002C0183 /* MSACLocExtension.m */; }; + C9A92108230C61820068070D /* MSACMetadataExtension.m in Sources */ = {isa = PBXBuildFile; fileRef = 2DA030746930EE3DCB6A21B3 /* MSACMetadataExtension.m */; }; + C9A92109230C61820068070D /* MSACNetExtension.m in Sources */ = {isa = PBXBuildFile; fileRef = E74B14D120B36EC5002C0183 /* MSACNetExtension.m */; }; + C9A9210A230C61820068070D /* MSACOSExtension.m in Sources */ = {isa = PBXBuildFile; fileRef = E74B14D920B37042002C0183 /* MSACOSExtension.m */; }; + C9A9210B230C61820068070D /* MSACProtocolExtension.m in Sources */ = {isa = PBXBuildFile; fileRef = E74B14C920B36DF1002C0183 /* MSACProtocolExtension.m */; }; + C9A9210C230C61820068070D /* MSACSDKExtension.m in Sources */ = {isa = PBXBuildFile; fileRef = E74B14E920B3711F002C0183 /* MSACSDKExtension.m */; }; + C9A9210D230C61820068070D /* MSACUserExtension.m in Sources */ = {isa = PBXBuildFile; fileRef = E74B14A120B36AAB002C0183 /* MSACUserExtension.m */; }; + C9A9210E230C61820068070D /* MSACAbstractLog.m in Sources */ = {isa = PBXBuildFile; fileRef = E88EBBEB1D2C612E007E7785 /* MSACAbstractLog.m */; }; + C9A9210F230C61820068070D /* MSACCustomPropertiesLog.m in Sources */ = {isa = PBXBuildFile; fileRef = F803BBF81E8E3989004B1E7A /* MSACCustomPropertiesLog.m */; }; + C9A92110230C61820068070D /* MSACLogContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = 6E171B591D234717000DC480 /* MSACLogContainer.m */; }; + C9A92111230C61820068070D /* MSACLogWithProperties.m in Sources */ = {isa = PBXBuildFile; fileRef = E8010E671D2DD4EF0035196F /* MSACLogWithProperties.m */; }; + C9A92112230C61820068070D /* MSACStartServiceLog.m in Sources */ = {isa = PBXBuildFile; fileRef = D38023E61E6EFC7C00466558 /* MSACStartServiceLog.m */; }; + C9A92113230C61820068070D /* MSACBooleanTypedProperty.m in Sources */ = {isa = PBXBuildFile; fileRef = 35DFC1C22170030C00455589 /* MSACBooleanTypedProperty.m */; }; + C9A92114230C61820068070D /* MSACDateTimeTypedProperty.m in Sources */ = {isa = PBXBuildFile; fileRef = 35DFC1C42170030C00455589 /* MSACDateTimeTypedProperty.m */; }; + C9A92115230C61820068070D /* MSACDoubleTypedProperty.m in Sources */ = {isa = PBXBuildFile; fileRef = 35DFC1C62170030C00455589 /* MSACDoubleTypedProperty.m */; }; + C9A92116230C61820068070D /* MSACLongTypedProperty.m in Sources */ = {isa = PBXBuildFile; fileRef = 35DFC1C82170030C00455589 /* MSACLongTypedProperty.m */; }; + C9A92117230C61820068070D /* MSACStringTypedProperty.m in Sources */ = {isa = PBXBuildFile; fileRef = 35DFC1BE2170030C00455589 /* MSACStringTypedProperty.m */; }; + C9A92118230C61820068070D /* MSACTypedProperty.m in Sources */ = {isa = PBXBuildFile; fileRef = 35DFC1C32170030C00455589 /* MSACTypedProperty.m */; }; + C9A92119230C61820068070D /* MSACDBStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 3849BA7B1EF3145A0072E3E0 /* MSACDBStorage.m */; }; + C9A9211A230C61820068070D /* MSACAppCenterUserDefaults.m in Sources */ = {isa = PBXBuildFile; fileRef = E8A8D1EA1D3057A90022931E /* MSACAppCenterUserDefaults.m */; }; + C9A9211B230C61820068070D /* MSACLogDBStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = B2CD74841F22BD910070E7DF /* MSACLogDBStorage.m */; }; + C9A9211C230C61820068070D /* MSACOrderedDictionary.m in Sources */ = {isa = PBXBuildFile; fileRef = B29D883C21E925A400EAF084 /* MSACOrderedDictionary.m */; }; + C9A9211D230C61820068070D /* MSACEncrypter.m in Sources */ = {isa = PBXBuildFile; fileRef = 8087362820C134AC004C4157 /* MSACEncrypter.m */; }; + C9A9211E230C61820068070D /* MSACHistoryInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 047FEE0721A4884600ED77CD /* MSACHistoryInfo.m */; }; + C9A9211F230C61820068070D /* MSACKeychainUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 045BC3161E3FD88600B6C960 /* MSACKeychainUtil.m */; }; + C9A92120230C61820068070D /* MSACUtility.m in Sources */ = {isa = PBXBuildFile; fileRef = B2CD74911F22BE270070E7DF /* MSACUtility.m */; }; + C9A92121230C61820068070D /* MSACUtility+Application.m in Sources */ = {isa = PBXBuildFile; fileRef = B2CD74931F22BE270070E7DF /* MSACUtility+Application.m */; }; + C9A92122230C61820068070D /* MSACUtility+Date.m in Sources */ = {isa = PBXBuildFile; fileRef = B2CD74961F22BE270070E7DF /* MSACUtility+Date.m */; }; + C9A92123230C61820068070D /* MSACUtility+Environment.m in Sources */ = {isa = PBXBuildFile; fileRef = B2CD74981F22BE270070E7DF /* MSACUtility+Environment.m */; }; + C9A92124230C61820068070D /* MSACUtility+File.m in Sources */ = {isa = PBXBuildFile; fileRef = B2CD749A1F22BE270070E7DF /* MSACUtility+File.m */; }; + C9A92125230C61820068070D /* MSACUtility+PropertyValidation.m in Sources */ = {isa = PBXBuildFile; fileRef = B28E41D42076ECFE00CC6AD8 /* MSACUtility+PropertyValidation.m */; }; + C9A92126230C61820068070D /* MSACUtility+StringFormatting.m in Sources */ = {isa = PBXBuildFile; fileRef = B2CD749C1F22BE270070E7DF /* MSACUtility+StringFormatting.m */; }; + C9A92127230C61820068070D /* MSACCompression.m in Sources */ = {isa = PBXBuildFile; fileRef = 38148D8520D07FB70046257E /* MSACCompression.m */; }; + C9A92128230C61820068070D /* MSAC_Reachability.m in Sources */ = {isa = PBXBuildFile; fileRef = E83283C51D46C62E000B029E /* MSAC_Reachability.m */; }; + C9A92129230C61820068070D /* MSACCustomProperties.m in Sources */ = {isa = PBXBuildFile; fileRef = F803BBF21E8E3677004B1E7A /* MSACCustomProperties.m */; }; + C9A9212A230C61820068070D /* MSACDevice.m in Sources */ = {isa = PBXBuildFile; fileRef = 3844FF1A1E8C2716003E9194 /* MSACDevice.m */; }; + C9A9212B230C61820068070D /* MSACWrapperSdk.m in Sources */ = {isa = PBXBuildFile; fileRef = B2CD74561F22BB710070E7DF /* MSACWrapperSdk.m */; }; + D55E7085252F5A1000AB994D /* MSACTestSessionInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = D55E7082252F5A1000AB994D /* MSACTestSessionInfo.m */; }; + D55E7088252F5A1000AB994D /* MSACTestSessionInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = D55E7083252F5A1000AB994D /* MSACTestSessionInfo.h */; }; + DF5DA1FA23A0E55500DE695C /* MSACDispatcherUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = DF5DA1F823A0E55500DE695C /* MSACDispatcherUtil.h */; }; + DF5DA1FE23A0E57B00DE695C /* MSACDispatcherUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = DF5DA1FC23A0E57B00DE695C /* MSACDispatcherUtil.m */; }; + DFE95545244D96540061E3FA /* HTTPStubsMethodSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = DFE95522244D96170061E3FA /* HTTPStubsMethodSwizzling.h */; }; + DFE9554D244D965A0061E3FA /* Compatibility.h in Headers */ = {isa = PBXBuildFile; fileRef = DFE95524244D96170061E3FA /* Compatibility.h */; }; + DFE9554E244D965A0061E3FA /* HTTPStubsResponse+JSON.h in Headers */ = {isa = PBXBuildFile; fileRef = DFE95525244D96170061E3FA /* HTTPStubsResponse+JSON.h */; }; + DFE9554F244D965A0061E3FA /* HTTPStubsPathHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = DFE95526244D96170061E3FA /* HTTPStubsPathHelpers.h */; }; + DFE95550244D965A0061E3FA /* HTTPStubsResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = DFE95527244D96170061E3FA /* HTTPStubsResponse.h */; }; + DFE95551244D965A0061E3FA /* NSURLRequest+HTTPBodyTesting.h in Headers */ = {isa = PBXBuildFile; fileRef = DFE95528244D96170061E3FA /* NSURLRequest+HTTPBodyTesting.h */; }; + DFE95552244D965A0061E3FA /* HTTPStubs.h in Headers */ = {isa = PBXBuildFile; fileRef = DFE95529244D96170061E3FA /* HTTPStubs.h */; }; + F8936CBE230C24D9006A330F /* AppCenter.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E0401401D1C99AC0051BCFA /* AppCenter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F8936CBF230C24D9006A330F /* MSACAppCenter.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E0401551D1C9AAA0051BCFA /* MSACAppCenter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F8936CC0230C24D9006A330F /* MSACAppCenterErrors.h in Headers */ = {isa = PBXBuildFile; fileRef = 3889932E1E29829700C27B36 /* MSACAppCenterErrors.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F8936CC1230C24D9006A330F /* MSACChannelGroupProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 3542741A2012AF0500BE766F /* MSACChannelGroupProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F8936CC2230C24D9006A330F /* MSACChannelProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 354274162012AE4000BE766F /* MSACChannelProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F8936CC3230C24D9006A330F /* MSACConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E04013F1D1C99AC0051BCFA /* MSACConstants.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F8936CC4230C24D9006A330F /* MSACConstants+Flags.h in Headers */ = {isa = PBXBuildFile; fileRef = 04B525B82194D44E00FA37FD /* MSACConstants+Flags.h */; settings = {ATTRIBUTES = (Private, ); }; }; + F8936CC5230C24D9006A330F /* MSACEnable.h in Headers */ = {isa = PBXBuildFile; fileRef = 3542742220167DA400BE766F /* MSACEnable.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F8936CC6230C24D9006A330F /* MSACLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = 358F9BC22019531F00B9E22C /* MSACLogger.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F8936CC7230C24DA006A330F /* MSACService.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E0401451D1C99AC0051BCFA /* MSACService.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F8936CC8230C24DA006A330F /* MSACServiceAbstract.h in Headers */ = {isa = PBXBuildFile; fileRef = 387C76FC1D6C9CF100D68CC1 /* MSACServiceAbstract.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F8936CC9230C24DA006A330F /* MSACWrapperLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = 35D0B7511DDFABFD003EACCD /* MSACWrapperLogger.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F8936CDC230C25A4006A330F /* MSACCustomProperties.h in Headers */ = {isa = PBXBuildFile; fileRef = F803BBF11E8E3677004B1E7A /* MSACCustomProperties.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F8936CDD230C25A4006A330F /* MSACDevice.h in Headers */ = {isa = PBXBuildFile; fileRef = 3844FF1B1E8C2716003E9194 /* MSACDevice.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F8936CDE230C25A4006A330F /* MSACLog.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E3E2C9D1D35701000B1EE50 /* MSACLog.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F8936CDF230C25A4006A330F /* MSACLogWithProperties.h in Headers */ = {isa = PBXBuildFile; fileRef = 3844FF191E8C2716003E9194 /* MSACLogWithProperties.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F8936CE0230C25A4006A330F /* MSACAbstractLog.h in Headers */ = {isa = PBXBuildFile; fileRef = 3844FF1C1E8C2716003E9194 /* MSACAbstractLog.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F8936CE1230C25A4006A330F /* MSACWrapperSdk.h in Headers */ = {isa = PBXBuildFile; fileRef = B2CD74551F22BB710070E7DF /* MSACWrapperSdk.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F8936CFC230C2604006A330F /* MSACAppCenterPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 38E1B6791DDE3FDF000EFED1 /* MSACAppCenterPrivate.h */; }; + F8936CFD230C2604006A330F /* MSACCustomPropertiesPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 266ED9D2BA796BA0329F4FC7 /* MSACCustomPropertiesPrivate.h */; }; + F8936CFE230C2604006A330F /* MSACDelegateForwarderPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 3803208D217E8BD40089772A /* MSACDelegateForwarderPrivate.h */; }; + F8936CFF230C2604006A330F /* MSACChannelGroupDefaultPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 04AB676220E18A74002828AA /* MSACChannelGroupDefaultPrivate.h */; }; + F8936D00230C2604006A330F /* MSACChannelUnitDefaultPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 2DA030B94CD3D38725671A79 /* MSACChannelUnitDefaultPrivate.h */; }; + F8936D01230C2604006A330F /* MSACOneCollectorChannelDelegatePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = E79750E620A4F41400E3EAE8 /* MSACOneCollectorChannelDelegatePrivate.h */; }; + F8936D03230C2604006A330F /* MSACDeviceTrackerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 04754F3D1EA980FD002CBA46 /* MSACDeviceTrackerPrivate.h */; }; + F8936D04230C2604006A330F /* MSACSessionContextPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 049378381FE44539000ADBAF /* MSACSessionContextPrivate.h */; }; + F8936D05230C2604006A330F /* MSACUserIdContextPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 047FEE2521A4968A00ED77CD /* MSACUserIdContextPrivate.h */; }; + F8936D06230C2604006A330F /* MSACHttpClientPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 3571F64B22454EDF0052406C /* MSACHttpClientPrivate.h */; }; + F8936D07230C2604006A330F /* MSACHttpIngestionPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 38F1944D1DADB93100D3E0FE /* MSACHttpIngestionPrivate.h */; }; + F8936D08230C2604006A330F /* MSACOneCollectorIngestionPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = B2ADB1412124C13300D0D7D9 /* MSACOneCollectorIngestionPrivate.h */; }; + F8936D09230C2604006A330F /* MSACAbstractLogPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 3513432D2056FF9000E6DC7D /* MSACAbstractLogPrivate.h */; }; + F8936D0A230C2604006A330F /* MSACDBStoragePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 3849BA841EF35D830072E3E0 /* MSACDBStoragePrivate.h */; }; + F8936D0B230C2604006A330F /* MSACLogDBStoragePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 383481721EA7FF6100787F56 /* MSACLogDBStoragePrivate.h */; }; + F8936D0C230C2604006A330F /* MSACOrderedDictionaryPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = B29D883A21E925A400EAF084 /* MSACOrderedDictionaryPrivate.h */; }; + F8936D0D230C2604006A330F /* MSACEncrypterPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 80B7EA2020CA9C9C00DF524C /* MSACEncrypterPrivate.h */; }; + F8936D0E230C2604006A330F /* MSACKeychainUtilPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 386A69EC1FD8843D0057B316 /* MSACKeychainUtilPrivate.h */; }; + F8936D0F230C2604006A330F /* MSACUtility+ApplicationPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = B2CD74941F22BE270070E7DF /* MSACUtility+ApplicationPrivate.h */; }; + F8936D7C230C2805006A330F /* AppCenter+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E0401581D1C9CFB0051BCFA /* AppCenter+Internal.h */; }; + F8936D7D230C2805006A330F /* MSACAppCenterInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E0401441D1C99AC0051BCFA /* MSACAppCenterInternal.h */; }; + F8936D7E230C2805006A330F /* MSACCustomPropertiesInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = F803BBF51E8E383E004B1E7A /* MSACCustomPropertiesInternal.h */; }; + F8936D7F230C2805006A330F /* MSACLoggerInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 3592ABA51DC90E3600EF4592 /* MSACLoggerInternal.h */; }; + F8936D80230C2805006A330F /* MSACServiceAbstractInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 387C757F1D6270A300D68CC1 /* MSACServiceAbstractInternal.h */; }; + F8936D81230C2805006A330F /* MSACServiceCommon.h in Headers */ = {isa = PBXBuildFile; fileRef = 387C758D1D64DF2500D68CC1 /* MSACServiceCommon.h */; }; + F8936D82230C2805006A330F /* MSACServiceInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E0401471D1C99AC0051BCFA /* MSACServiceInternal.h */; }; + F8936D83230C2805006A330F /* MSACAppDelegateUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = 38C633C81FDF163D005F40C9 /* MSACAppDelegateUtil.h */; }; + F8936D84230C2805006A330F /* MSACAppDelegateForwarder.h in Headers */ = {isa = PBXBuildFile; fileRef = 38C4471E1ECE5352002E1B11 /* MSACAppDelegateForwarder.h */; }; + F8936D85230C2805006A330F /* MSACCustomApplicationDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 38C633C61FDF15DD005F40C9 /* MSACCustomApplicationDelegate.h */; }; + F8936D86230C2805006A330F /* MSACDelegateForwarder.h in Headers */ = {isa = PBXBuildFile; fileRef = 38032085217E85A90089772A /* MSACDelegateForwarder.h */; }; + F8936D87230C2805006A330F /* MSACCustomDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 38032091217E9DC50089772A /* MSACCustomDelegate.h */; }; + F8936D88230C2805006A330F /* MSACChannelGroupDefault.h in Headers */ = {isa = PBXBuildFile; fileRef = E84B8E2C1D2351DB006FD231 /* MSACChannelGroupDefault.h */; }; + F8936D89230C2805006A330F /* MSACChannelUnitConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = 6EF628F21D371B1600CAFF64 /* MSACChannelUnitConfiguration.h */; }; + F8936D8A230C2805006A330F /* MSACChannelUnitDefault.h in Headers */ = {isa = PBXBuildFile; fileRef = 3542741C2012B53B00BE766F /* MSACChannelUnitDefault.h */; }; + F8936D8B230C2805006A330F /* MSACChannelDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 3542741B2012B02600BE766F /* MSACChannelDelegate.h */; settings = {ATTRIBUTES = (Private, ); }; }; + F8936D8C230C2805006A330F /* MSACOneCollectorChannelDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 805275BF20A19F7B00704115 /* MSACOneCollectorChannelDelegate.h */; }; + F8936D8D230C2805006A330F /* MSACChannelUnitProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 354274192012AEFC00BE766F /* MSACChannelUnitProtocol.h */; }; + F8936D8E230C2805006A330F /* MSACCSEpochAndSeq.h in Headers */ = {isa = PBXBuildFile; fileRef = 3814A8E120BF5E790093AF45 /* MSACCSEpochAndSeq.h */; }; + F8936D93230C2805006A330F /* MSACDeviceHistoryInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = B2FD53601E56501B0050F909 /* MSACDeviceHistoryInfo.h */; }; + F8936D94230C2805006A330F /* MSACDeviceTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 381C91E51D3DB65D004512F1 /* MSACDeviceTracker.h */; }; + F8936D95230C2805006A330F /* MSACSessionContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 045C850F1FE0B1D900089540 /* MSACSessionContext.h */; }; + F8936D96230C2805006A330F /* MSACSessionHistoryInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 047EBB9C1FE30842009BB1C8 /* MSACSessionHistoryInfo.h */; }; + F8936D97230C2805006A330F /* MSACUserIdContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 047FEE1221A48CC200ED77CD /* MSACUserIdContext.h */; }; + F8936D98230C2805006A330F /* MSACUserIdHistoryInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 047FEE1121A48CC200ED77CD /* MSACUserIdHistoryInfo.h */; }; + F8936D99230C2805006A330F /* MSACHttpUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = 3571F658224962180052406C /* MSACHttpUtil.h */; }; + F8936D9A230C2805006A330F /* MSACHttpClientProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 2DA0373E842A41C8413D1722 /* MSACHttpClientProtocol.h */; }; + F8936D9B230C2805006A330F /* MSACHttpClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 2DA03260792C64C0290E6E41 /* MSACHttpClient.h */; }; + F8936D9C230C2805006A330F /* MSACHttpCall.h in Headers */ = {isa = PBXBuildFile; fileRef = 3571F64F22457E220052406C /* MSACHttpCall.h */; }; + F8936D9D230C2805006A330F /* MSACTicketCache.h in Headers */ = {isa = PBXBuildFile; fileRef = B23507B42118D22800F98D4F /* MSACTicketCache.h */; }; + F8936D9E230C2805006A330F /* MSACIngestionProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = E84B8E311D235209006FD231 /* MSACIngestionProtocol.h */; }; + F8936DA2230C2805006A330F /* MSACHttpIngestion.h in Headers */ = {isa = PBXBuildFile; fileRef = E84B8E331D235226006FD231 /* MSACHttpIngestion.h */; }; + F8936DA3230C2805006A330F /* MSACAppCenterIngestion.h in Headers */ = {isa = PBXBuildFile; fileRef = BA6824A001520825F18DFC42 /* MSACAppCenterIngestion.h */; }; + F8936DA4230C2805006A330F /* MSACOneCollectorIngestion.h in Headers */ = {isa = PBXBuildFile; fileRef = B2B7D50920C5E562001D31B8 /* MSACOneCollectorIngestion.h */; }; + F8936DA5230C2805006A330F /* MSACAppExtension.h in Headers */ = {isa = PBXBuildFile; fileRef = E74B14C020B36C66002C0183 /* MSACAppExtension.h */; }; + F8936DA6230C2805006A330F /* MSACCommonSchemaLog.h in Headers */ = {isa = PBXBuildFile; fileRef = E74B149720B364EE002C0183 /* MSACCommonSchemaLog.h */; }; + F8936DA7230C2805006A330F /* MSACCSData.h in Headers */ = {isa = PBXBuildFile; fileRef = E7D23C5420B4E38B00A47D62 /* MSACCSData.h */; }; + F8936DA8230C2805006A330F /* MSACCSExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = E7D23C6420B6391700A47D62 /* MSACCSExtensions.h */; }; + F8936DA9230C2805006A330F /* MSACDeviceExtension.h in Headers */ = {isa = PBXBuildFile; fileRef = 359C38DB214079D90066C509 /* MSACDeviceExtension.h */; }; + F8936DAA230C2805006A330F /* MSACLocExtension.h in Headers */ = {isa = PBXBuildFile; fileRef = E74B14A820B36B9B002C0183 /* MSACLocExtension.h */; }; + F8936DAB230C2805006A330F /* MSACMetadataExtension.h in Headers */ = {isa = PBXBuildFile; fileRef = 2DA038EE68EEE1414A270ADA /* MSACMetadataExtension.h */; }; + F8936DAC230C2805006A330F /* MSACNetExtension.h in Headers */ = {isa = PBXBuildFile; fileRef = E74B14D020B36EC5002C0183 /* MSACNetExtension.h */; }; + F8936DAD230C2805006A330F /* MSACOSExtension.h in Headers */ = {isa = PBXBuildFile; fileRef = E74B14B820B36C2B002C0183 /* MSACOSExtension.h */; }; + F8936DAE230C2805006A330F /* MSACProtocolExtension.h in Headers */ = {isa = PBXBuildFile; fileRef = E74B14C820B36DF1002C0183 /* MSACProtocolExtension.h */; }; + F8936DAF230C2805006A330F /* MSACSDKExtension.h in Headers */ = {isa = PBXBuildFile; fileRef = E74B14E820B3711F002C0183 /* MSACSDKExtension.h */; }; + F8936DB0230C2805006A330F /* MSACUserExtension.h in Headers */ = {isa = PBXBuildFile; fileRef = E74B14E420B370A5002C0183 /* MSACUserExtension.h */; }; + F8936DB1230C2805006A330F /* MSACAbstractLogInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 3844FF121E8C22CC003E9194 /* MSACAbstractLogInternal.h */; }; + F8936DB2230C2805006A330F /* MSACCustomPropertiesLog.h in Headers */ = {isa = PBXBuildFile; fileRef = F803BBFA1E8E39AB004B1E7A /* MSACCustomPropertiesLog.h */; }; + F8936DB3230C2805006A330F /* MSACDeviceInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = B2C3C1831DB83A3E00CB83F7 /* MSACDeviceInternal.h */; }; + F8936DB4230C2805006A330F /* MSACLogContainer.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E171B581D234717000DC480 /* MSACLogContainer.h */; }; + F8936DB5230C2805006A330F /* MSACModel.h in Headers */ = {isa = PBXBuildFile; fileRef = E7D23C6020B4EED000A47D62 /* MSACModel.h */; }; + F8936DB6230C2805006A330F /* MSACNoAutoAssignSessionIdLog.h in Headers */ = {isa = PBXBuildFile; fileRef = 04F3613C1FE1A92000A5A6D2 /* MSACNoAutoAssignSessionIdLog.h */; }; + F8936DB7230C2805006A330F /* MSACSerializableObject.h in Headers */ = {isa = PBXBuildFile; fileRef = E7D23C5C20B4EC3700A47D62 /* MSACSerializableObject.h */; }; + F8936DB8230C2805006A330F /* MSACStartServiceLog.h in Headers */ = {isa = PBXBuildFile; fileRef = D38023E71E6EFC7C00466558 /* MSACStartServiceLog.h */; }; + F8936DB9230C2805006A330F /* MSACWrapperSdkInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = B2CD745D1F22BBBB0070E7DF /* MSACWrapperSdkInternal.h */; }; + F8936DBA230C2805006A330F /* MSACLogConversion.h in Headers */ = {isa = PBXBuildFile; fileRef = 38BC346C20B8CB0C00119C05 /* MSACLogConversion.h */; }; + F8936DBB230C2805006A330F /* MSACBooleanTypedProperty.h in Headers */ = {isa = PBXBuildFile; fileRef = 35DFC1C72170030C00455589 /* MSACBooleanTypedProperty.h */; }; + F8936DBC230C2805006A330F /* MSACDateTimeTypedProperty.h in Headers */ = {isa = PBXBuildFile; fileRef = 35DFC1C52170030C00455589 /* MSACDateTimeTypedProperty.h */; }; + F8936DBD230C2805006A330F /* MSACDoubleTypedProperty.h in Headers */ = {isa = PBXBuildFile; fileRef = 35DFC1C02170030C00455589 /* MSACDoubleTypedProperty.h */; }; + F8936DBE230C2805006A330F /* MSACLongTypedProperty.h in Headers */ = {isa = PBXBuildFile; fileRef = 35DFC1C12170030C00455589 /* MSACLongTypedProperty.h */; }; + F8936DBF230C2805006A330F /* MSACStringTypedProperty.h in Headers */ = {isa = PBXBuildFile; fileRef = 35DFC1BD2170030C00455589 /* MSACStringTypedProperty.h */; }; + F8936DC0230C2805006A330F /* MSACTypedProperty.h in Headers */ = {isa = PBXBuildFile; fileRef = 35DFC1BF2170030C00455589 /* MSACTypedProperty.h */; }; + F8936DC1230C2805006A330F /* MSACStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = E84B8E381D235246006FD231 /* MSACStorage.h */; }; + F8936DC2230C2805006A330F /* MSACDBStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = 3849BA791EF314440072E3E0 /* MSACDBStorage.h */; }; + F8936DC3230C2805006A330F /* MSACAppCenterUserDefaults.h in Headers */ = {isa = PBXBuildFile; fileRef = E8A8D1E91D3057A90022931E /* MSACAppCenterUserDefaults.h */; }; + F8936DC4230C2805006A330F /* MSACLogDBStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = B2CD74831F22BD910070E7DF /* MSACLogDBStorage.h */; }; + F8936DC5230C2805006A330F /* MSACLogDBStorageVersion.h in Headers */ = {isa = PBXBuildFile; fileRef = 046658B7215AD59D0079DCC7 /* MSACLogDBStorageVersion.h */; }; + F8936DC6230C2805006A330F /* MSACOrderedDictionary.h in Headers */ = {isa = PBXBuildFile; fileRef = B29D883B21E925A400EAF084 /* MSACOrderedDictionary.h */; }; + F8936DC7230C2805006A330F /* MSACConstants+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = B2CD748F1F22BE270070E7DF /* MSACConstants+Internal.h */; }; + F8936DC8230C2805006A330F /* MSACEncrypter.h in Headers */ = {isa = PBXBuildFile; fileRef = 8087362720C1348B004C4157 /* MSACEncrypter.h */; }; + F8936DC9230C2805006A330F /* MSACHistoryInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 047FEE0621A4884600ED77CD /* MSACHistoryInfo.h */; }; + F8936DCA230C2805006A330F /* MSACKeychainUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = 045BC3181E3FD8AC00B6C960 /* MSACKeychainUtil.h */; }; + F8936DCB230C2805006A330F /* MSACUtility.h in Headers */ = {isa = PBXBuildFile; fileRef = B2CD74901F22BE270070E7DF /* MSACUtility.h */; }; + F8936DCC230C2805006A330F /* MSACUtility+Application.h in Headers */ = {isa = PBXBuildFile; fileRef = B2CD74921F22BE270070E7DF /* MSACUtility+Application.h */; }; + F8936DCD230C2805006A330F /* MSACUtility+Date.h in Headers */ = {isa = PBXBuildFile; fileRef = B2CD74951F22BE270070E7DF /* MSACUtility+Date.h */; }; + F8936DCE230C2805006A330F /* MSACUtility+Environment.h in Headers */ = {isa = PBXBuildFile; fileRef = B2CD74971F22BE270070E7DF /* MSACUtility+Environment.h */; }; + F8936DCF230C2805006A330F /* MSACUtility+File.h in Headers */ = {isa = PBXBuildFile; fileRef = B2CD74991F22BE270070E7DF /* MSACUtility+File.h */; }; + F8936DD0230C2805006A330F /* MSACUtility+PropertyValidation.h in Headers */ = {isa = PBXBuildFile; fileRef = B28E41D32076EC4000CC6AD8 /* MSACUtility+PropertyValidation.h */; }; + F8936DD1230C2805006A330F /* MSACUtility+StringFormatting.h in Headers */ = {isa = PBXBuildFile; fileRef = B2CD749B1F22BE270070E7DF /* MSACUtility+StringFormatting.h */; }; + F8936DD2230C2805006A330F /* MSACCompression.h in Headers */ = {isa = PBXBuildFile; fileRef = 38148D8420D07FB70046257E /* MSACCompression.h */; }; + F8936DD3230C2805006A330F /* MSAC_Reachability.h in Headers */ = {isa = PBXBuildFile; fileRef = E83283C41D46C62E000B029E /* MSAC_Reachability.h */; }; + F8BA7A2A23AA8A26009FBCCF /* MSACStorageBindableArray.h in Headers */ = {isa = PBXBuildFile; fileRef = F8BA7A2823AA8A26009FBCCF /* MSACStorageBindableArray.h */; }; + F8BA7A2E23AA8B84009FBCCF /* MSACStorageBindableArray.m in Sources */ = {isa = PBXBuildFile; fileRef = F8BA7A2C23AA8B84009FBCCF /* MSACStorageBindableArray.m */; }; + F8DC50DD23AA828E00BF8839 /* MSACStorageBindableType.h in Headers */ = {isa = PBXBuildFile; fileRef = F8DC50C823AA75FF00BF8839 /* MSACStorageBindableType.h */; }; + F8DC50DE23AA828E00BF8839 /* MSACStorageNumberType.h in Headers */ = {isa = PBXBuildFile; fileRef = F8DC50C923AA777400BF8839 /* MSACStorageNumberType.h */; }; + F8DC50DF23AA828E00BF8839 /* MSACStorageTextType.h in Headers */ = {isa = PBXBuildFile; fileRef = F8DC50CA23AA77CB00BF8839 /* MSACStorageTextType.h */; }; + F8DC50E123AA828E00BF8839 /* MSACStorageTextType.m in Sources */ = {isa = PBXBuildFile; fileRef = F8DC50CC23AA77F700BF8839 /* MSACStorageTextType.m */; }; + F8DC50E223AA828E00BF8839 /* MSACStorageNumberType.m in Sources */ = {isa = PBXBuildFile; fileRef = F8DC50CE23AA7A3900BF8839 /* MSACStorageNumberType.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + C2392EE42464279400425640 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C2392EDA2464279400425640 /* OCMock.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 030EF0A814632FD000B04273; + remoteInfo = OCMock; + }; + C2392EE62464279400425640 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C2392EDA2464279400425640 /* OCMock.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 03565A3118F0566E003AE91E; + remoteInfo = OCMockTests; + }; + C2392EE82464279400425640 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C2392EDA2464279400425640 /* OCMock.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 030EF0DC14632FF700B04273; + remoteInfo = OCMockLib; + }; + C2392EEA2464279400425640 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C2392EDA2464279400425640 /* OCMock.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = D31108AD1828DB8700737925; + remoteInfo = OCMockLibTests; + }; + C2392EEC2464279400425640 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C2392EDA2464279400425640 /* OCMock.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = F0B950F11B0080BE00942C38; + remoteInfo = "OCMock iOS"; + }; + C2392EEE2464279400425640 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C2392EDA2464279400425640 /* OCMock.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 817EB1621BD765130047E85A; + remoteInfo = "OCMock tvOS"; + }; + C2392EF02464279400425640 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C2392EDA2464279400425640 /* OCMock.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 8DE97CA022B43EE60098C63F; + remoteInfo = "OCMock watchOS"; + }; + DFCB80072472C1BD0058D292 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DFCB7FFD2472C1BD0058D292 /* OCHamcrest.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 087601E213440806001B439B; + remoteInfo = OCHamcrest; + }; + DFCB80092472C1BD0058D292 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DFCB7FFD2472C1BD0058D292 /* OCHamcrest.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 087601F713440807001B439B; + remoteInfo = OCHamcrestTests; + }; + DFCB800B2472C1BD0058D292 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DFCB7FFD2472C1BD0058D292 /* OCHamcrest.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 081BEE621345979F003F846A; + remoteInfo = libochamcrest; + }; + DFCB800D2472C1BD0058D292 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DFCB7FFD2472C1BD0058D292 /* OCHamcrest.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 081BEE6C1345979F003F846A; + remoteInfo = libochamcrestTests; + }; + DFCB800F2472C1BD0058D292 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DFCB7FFD2472C1BD0058D292 /* OCHamcrest.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 24C5BFE91C1777D900C2BAFD; + remoteInfo = "OCHamcrest-iOS"; + }; + DFCB80112472C1BD0058D292 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DFCB7FFD2472C1BD0058D292 /* OCHamcrest.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 24C5BFF61C17781400C2BAFD; + remoteInfo = "OCHamcrest-tvOS"; + }; + DFCB80132472C1BD0058D292 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DFCB7FFD2472C1BD0058D292 /* OCHamcrest.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 24C5C0031C17782500C2BAFD; + remoteInfo = "OCHamcrest-watchOS"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 041CFF8D1ECCFFD200B4654B /* Tests macOS.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = "Tests macOS.xcconfig"; path = "../../Config/Tests macOS.xcconfig"; sourceTree = ""; }; + 04237DC023A31617009BB406 /* MSACHttpClientDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACHttpClientDelegate.h; sourceTree = ""; }; + 04311FEE1EE083C2007054C5 /* MSACTestFrameworks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACTestFrameworks.h; sourceTree = ""; }; + 043120701EE0BCC4007054C5 /* tvOS.modulemap */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = "sourcecode.module-map"; path = tvOS.modulemap; sourceTree = ""; }; + 043120711EE0BCC4007054C5 /* tvOS.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = tvOS.xcconfig; sourceTree = ""; }; + 04545DC8227B603D00A49E06 /* AppCenter.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppCenter.xcconfig; sourceTree = ""; }; + 045660FA1D99EEEB002F7055 /* MSACLogWithPropertiesTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACLogWithPropertiesTests.m; sourceTree = ""; }; + 045BC3161E3FD88600B6C960 /* MSACKeychainUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACKeychainUtil.m; sourceTree = ""; }; + 045BC3181E3FD8AC00B6C960 /* MSACKeychainUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACKeychainUtil.h; sourceTree = ""; }; + 045C850F1FE0B1D900089540 /* MSACSessionContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACSessionContext.h; sourceTree = ""; }; + 045C85131FE0B31600089540 /* MSACSessionContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACSessionContext.m; sourceTree = ""; }; + 046658B7215AD59D0079DCC7 /* MSACLogDBStorageVersion.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACLogDBStorageVersion.h; sourceTree = ""; }; + 0469D1B11F4DF89A00A43A8E /* AppCenter Release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "AppCenter Release.xcconfig"; sourceTree = ""; }; + 04754F3C1EA980FD002CBA46 /* MSACDeviceTracker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACDeviceTracker.m; sourceTree = ""; }; + 04754F3D1EA980FD002CBA46 /* MSACDeviceTrackerPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACDeviceTrackerPrivate.h; sourceTree = ""; }; + 047EBB9B1FE30842009BB1C8 /* MSACSessionHistoryInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACSessionHistoryInfo.m; sourceTree = ""; }; + 047EBB9C1FE30842009BB1C8 /* MSACSessionHistoryInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACSessionHistoryInfo.h; sourceTree = ""; }; + 047FEE0621A4884600ED77CD /* MSACHistoryInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACHistoryInfo.h; sourceTree = ""; }; + 047FEE0721A4884600ED77CD /* MSACHistoryInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACHistoryInfo.m; sourceTree = ""; }; + 047FEE1021A48CC200ED77CD /* MSACUserIdContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACUserIdContext.m; sourceTree = ""; }; + 047FEE1121A48CC200ED77CD /* MSACUserIdHistoryInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACUserIdHistoryInfo.h; sourceTree = ""; }; + 047FEE1221A48CC200ED77CD /* MSACUserIdContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACUserIdContext.h; sourceTree = ""; }; + 047FEE1321A48CC200ED77CD /* MSACUserIdHistoryInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACUserIdHistoryInfo.m; sourceTree = ""; }; + 047FEE2521A4968A00ED77CD /* MSACUserIdContextPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACUserIdContextPrivate.h; sourceTree = ""; }; + 04840CD71ECCF6560020B0FC /* Tests iOS.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = "Tests iOS.xcconfig"; path = "../../Config/Tests iOS.xcconfig"; sourceTree = ""; }; + 0484DD5E1F3910DF0092B777 /* Tests tvOS.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = "Tests tvOS.xcconfig"; path = "../../Config/Tests tvOS.xcconfig"; sourceTree = ""; }; + 0485AFEC1EAAAC3B00C10CAF /* macOS.modulemap */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = "sourcecode.module-map"; path = macOS.modulemap; sourceTree = ""; }; + 0485AFEE1EAAAD0000C10CAF /* iOS.modulemap */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = "sourcecode.module-map"; path = iOS.modulemap; sourceTree = ""; }; + 049378381FE44539000ADBAF /* MSACSessionContextPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACSessionContextPrivate.h; sourceTree = ""; }; + 0493783F1FE4913C000ADBAF /* MSACSessionContextTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACSessionContextTests.m; sourceTree = ""; }; + 049BC8281ECE3A5200FB6719 /* iOS.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = iOS.xcconfig; sourceTree = ""; }; + 049BC8291ECE3A5A00FB6719 /* macOS.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = macOS.xcconfig; sourceTree = ""; }; + 04A20D70217660D50096723C /* MSACWrapperLoggerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACWrapperLoggerTests.m; sourceTree = ""; }; + 04A79A4720C6F635006D0072 /* MSACOneCollectorIngestionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACOneCollectorIngestionTests.m; sourceTree = ""; }; + 04AB676220E18A74002828AA /* MSACChannelGroupDefaultPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACChannelGroupDefaultPrivate.h; sourceTree = ""; }; + 04B525B82194D44E00FA37FD /* MSACConstants+Flags.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MSACConstants+Flags.h"; sourceTree = ""; }; + 04B59A4022050370008DA079 /* MSACHttpIngestionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACHttpIngestionTests.m; sourceTree = ""; }; + 04B7BBEE1E5FAD4D001A0CE1 /* MSACHttpUtilTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACHttpUtilTests.m; sourceTree = ""; }; + 04F3613C1FE1A92000A5A6D2 /* MSACNoAutoAssignSessionIdLog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACNoAutoAssignSessionIdLog.h; sourceTree = ""; }; + 04FD126A1E4103CC007ABFE7 /* MSACKeychainUtilTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACKeychainUtilTests.m; sourceTree = ""; }; + 2434213123A456C900EDFA83 /* MSACDependencyConfigurationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACDependencyConfigurationTests.m; sourceTree = ""; }; + 244CC1F62399CA5A00A58F51 /* MSACDependencyConfiguration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACDependencyConfiguration.h; sourceTree = ""; }; + 244CC1F72399CA5A00A58F51 /* MSACDependencyConfiguration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACDependencyConfiguration.m; sourceTree = ""; }; + 24D44B402395DBBE003CC224 /* MSACTestUtil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACTestUtil.h; sourceTree = ""; }; + 24D44B412395DBBE003CC224 /* MSACTestUtil.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACTestUtil.m; sourceTree = ""; }; + 266ED9D2BA796BA0329F4FC7 /* MSACCustomPropertiesPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACCustomPropertiesPrivate.h; sourceTree = ""; }; + 2DA030746930EE3DCB6A21B3 /* MSACMetadataExtension.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACMetadataExtension.m; sourceTree = ""; }; + 2DA030B94CD3D38725671A79 /* MSACChannelUnitDefaultPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACChannelUnitDefaultPrivate.h; sourceTree = ""; }; + 2DA03260792C64C0290E6E41 /* MSACHttpClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACHttpClient.h; sourceTree = ""; }; + 2DA0350241FEC3A6A4919BFE /* MSACHttpClient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACHttpClient.m; sourceTree = ""; }; + 2DA0373E842A41C8413D1722 /* MSACHttpClientProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACHttpClientProtocol.h; sourceTree = ""; }; + 2DA038EE68EEE1414A270ADA /* MSACMetadataExtension.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACMetadataExtension.h; sourceTree = ""; }; + 3511AE7C20C5FF150056A739 /* MSACMockLogWithConversion.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACMockLogWithConversion.h; sourceTree = ""; }; + 3513432D2056FF9000E6DC7D /* MSACAbstractLogPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACAbstractLogPrivate.h; sourceTree = ""; }; + 354274162012AE4000BE766F /* MSACChannelProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACChannelProtocol.h; sourceTree = ""; }; + 354274192012AEFC00BE766F /* MSACChannelUnitProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACChannelUnitProtocol.h; sourceTree = ""; }; + 3542741A2012AF0500BE766F /* MSACChannelGroupProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACChannelGroupProtocol.h; sourceTree = ""; }; + 3542741B2012B02600BE766F /* MSACChannelDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACChannelDelegate.h; sourceTree = ""; }; + 3542741C2012B53B00BE766F /* MSACChannelUnitDefault.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACChannelUnitDefault.h; sourceTree = ""; }; + 3542742220167DA400BE766F /* MSACEnable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACEnable.h; sourceTree = ""; }; + 3571F64B22454EDF0052406C /* MSACHttpClientPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACHttpClientPrivate.h; sourceTree = ""; }; + 3571F64F22457E220052406C /* MSACHttpCall.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACHttpCall.h; sourceTree = ""; }; + 3571F65322457FCC0052406C /* MSACHttpCall.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACHttpCall.m; sourceTree = ""; }; + 3571F658224962180052406C /* MSACHttpUtil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACHttpUtil.h; sourceTree = ""; }; + 3571F659224962180052406C /* MSACHttpUtil.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACHttpUtil.m; sourceTree = ""; }; + 358F9BC22019531F00B9E22C /* MSACLogger.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACLogger.h; sourceTree = ""; }; + 3592ABA51DC90E3600EF4592 /* MSACLoggerInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACLoggerInternal.h; sourceTree = ""; }; + 3592ABA61DC90E3600EF4592 /* MSACLogger.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACLogger.m; sourceTree = ""; }; + 359C38DB214079D90066C509 /* MSACDeviceExtension.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACDeviceExtension.h; sourceTree = ""; }; + 359C38DC214079D90066C509 /* MSACDeviceExtension.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACDeviceExtension.m; sourceTree = ""; }; + 359E898F224BF70400795CF5 /* MSACHttpCallTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACHttpCallTests.m; sourceTree = ""; }; + 35B80F9220C1FE1A00CDFA55 /* MSACMockLogObject.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACMockLogObject.h; sourceTree = ""; }; + 35C0E3C91FD6146A004E841E /* MSACMockSecondService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACMockSecondService.m; sourceTree = ""; }; + 35C0E3CA1FD6146A004E841E /* MSACMockSecondService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACMockSecondService.h; sourceTree = ""; }; + 35D0B7511DDFABFD003EACCD /* MSACWrapperLogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACWrapperLogger.h; sourceTree = ""; }; + 35D0B7521DDFABFD003EACCD /* MSACWrapperLogger.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACWrapperLogger.m; sourceTree = ""; }; + 35DFC1BD2170030C00455589 /* MSACStringTypedProperty.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSACStringTypedProperty.h; path = AppCenter/Internals/Model/Properties/MSACStringTypedProperty.h; sourceTree = SOURCE_ROOT; }; + 35DFC1BE2170030C00455589 /* MSACStringTypedProperty.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MSACStringTypedProperty.m; path = AppCenter/Internals/Model/Properties/MSACStringTypedProperty.m; sourceTree = SOURCE_ROOT; }; + 35DFC1BF2170030C00455589 /* MSACTypedProperty.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSACTypedProperty.h; path = AppCenter/Internals/Model/Properties/MSACTypedProperty.h; sourceTree = SOURCE_ROOT; }; + 35DFC1C02170030C00455589 /* MSACDoubleTypedProperty.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSACDoubleTypedProperty.h; path = AppCenter/Internals/Model/Properties/MSACDoubleTypedProperty.h; sourceTree = SOURCE_ROOT; }; + 35DFC1C12170030C00455589 /* MSACLongTypedProperty.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSACLongTypedProperty.h; path = AppCenter/Internals/Model/Properties/MSACLongTypedProperty.h; sourceTree = SOURCE_ROOT; }; + 35DFC1C22170030C00455589 /* MSACBooleanTypedProperty.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MSACBooleanTypedProperty.m; path = AppCenter/Internals/Model/Properties/MSACBooleanTypedProperty.m; sourceTree = SOURCE_ROOT; }; + 35DFC1C32170030C00455589 /* MSACTypedProperty.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MSACTypedProperty.m; path = AppCenter/Internals/Model/Properties/MSACTypedProperty.m; sourceTree = SOURCE_ROOT; }; + 35DFC1C42170030C00455589 /* MSACDateTimeTypedProperty.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MSACDateTimeTypedProperty.m; path = AppCenter/Internals/Model/Properties/MSACDateTimeTypedProperty.m; sourceTree = SOURCE_ROOT; }; + 35DFC1C52170030C00455589 /* MSACDateTimeTypedProperty.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSACDateTimeTypedProperty.h; path = AppCenter/Internals/Model/Properties/MSACDateTimeTypedProperty.h; sourceTree = SOURCE_ROOT; }; + 35DFC1C62170030C00455589 /* MSACDoubleTypedProperty.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MSACDoubleTypedProperty.m; path = AppCenter/Internals/Model/Properties/MSACDoubleTypedProperty.m; sourceTree = SOURCE_ROOT; }; + 35DFC1C72170030C00455589 /* MSACBooleanTypedProperty.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSACBooleanTypedProperty.h; path = AppCenter/Internals/Model/Properties/MSACBooleanTypedProperty.h; sourceTree = SOURCE_ROOT; }; + 35DFC1C82170030C00455589 /* MSACLongTypedProperty.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MSACLongTypedProperty.m; path = AppCenter/Internals/Model/Properties/MSACLongTypedProperty.m; sourceTree = SOURCE_ROOT; }; + 35DFC2172170044500455589 /* MSACBooleanTypedPropertyTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACBooleanTypedPropertyTests.m; sourceTree = ""; }; + 35DFC2182170044600455589 /* MSACDoubleTypedPropertyTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACDoubleTypedPropertyTests.m; sourceTree = ""; }; + 35DFC2192170044600455589 /* MSACDateTimeTypedPropertyTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACDateTimeTypedPropertyTests.m; sourceTree = ""; }; + 35DFC21A2170044600455589 /* MSACStringTypedPropertyTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACStringTypedPropertyTests.m; sourceTree = ""; }; + 35DFC21B2170044600455589 /* MSACLongTypedPropertyTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACLongTypedPropertyTests.m; sourceTree = ""; }; + 35DFC21C2170044600455589 /* MSACTypedPropertyTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACTypedPropertyTests.m; sourceTree = ""; }; + 38032085217E85A90089772A /* MSACDelegateForwarder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACDelegateForwarder.h; sourceTree = ""; }; + 38032086217E85A90089772A /* MSACDelegateForwarder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACDelegateForwarder.m; sourceTree = ""; }; + 3803208D217E8BD40089772A /* MSACDelegateForwarderPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACDelegateForwarderPrivate.h; sourceTree = ""; }; + 38032091217E9DC50089772A /* MSACCustomDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACCustomDelegate.h; sourceTree = ""; }; + 380A4DCA1DD6908A00E99219 /* MSACUtilityTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACUtilityTests.m; sourceTree = ""; }; + 38148D8420D07FB70046257E /* MSACCompression.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACCompression.h; sourceTree = ""; }; + 38148D8520D07FB70046257E /* MSACCompression.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACCompression.m; sourceTree = ""; }; + 3814A8E120BF5E790093AF45 /* MSACCSEpochAndSeq.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACCSEpochAndSeq.h; sourceTree = ""; }; + 3814A8E520BF5FA00093AF45 /* MSACCSEpochAndSeq.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACCSEpochAndSeq.m; sourceTree = ""; }; + 381C91E51D3DB65D004512F1 /* MSACDeviceTracker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACDeviceTracker.h; sourceTree = ""; }; + 383481721EA7FF6100787F56 /* MSACLogDBStoragePrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACLogDBStoragePrivate.h; sourceTree = ""; }; + 3844FF121E8C22CC003E9194 /* MSACAbstractLogInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACAbstractLogInternal.h; sourceTree = ""; }; + 3844FF191E8C2716003E9194 /* MSACLogWithProperties.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSACLogWithProperties.h; path = Model/MSACLogWithProperties.h; sourceTree = ""; }; + 3844FF1A1E8C2716003E9194 /* MSACDevice.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MSACDevice.m; path = Model/MSACDevice.m; sourceTree = ""; }; + 3844FF1B1E8C2716003E9194 /* MSACDevice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSACDevice.h; path = Model/MSACDevice.h; sourceTree = ""; }; + 3844FF1C1E8C2716003E9194 /* MSACAbstractLog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSACAbstractLog.h; path = Model/MSACAbstractLog.h; sourceTree = ""; }; + 384959D41D491D4F008F6B3A /* MSACAppCenterTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACAppCenterTests.m; sourceTree = ""; }; + 3849BA791EF314440072E3E0 /* MSACDBStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACDBStorage.h; sourceTree = ""; }; + 3849BA7B1EF3145A0072E3E0 /* MSACDBStorage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACDBStorage.m; sourceTree = ""; }; + 3849BA7D1EF3489D0072E3E0 /* MSACDBStorageTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACDBStorageTests.m; sourceTree = ""; }; + 3849BA841EF35D830072E3E0 /* MSACDBStoragePrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACDBStoragePrivate.h; sourceTree = ""; }; + 384A925F2188BE400099BE70 /* MSACDelegateForwarderTestUtil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACDelegateForwarderTestUtil.h; sourceTree = ""; }; + 384A92602188BE400099BE70 /* MSACDelegateForwarderTestUtil.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACDelegateForwarderTestUtil.m; sourceTree = ""; }; + 385FC0541D37EBD700A1799F /* MSACDeviceTrackerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACDeviceTrackerTests.m; sourceTree = ""; }; + 38641B041EB0F40800B2CE73 /* MSACAppDelegateForwarderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACAppDelegateForwarderTests.m; sourceTree = ""; }; + 386A69EC1FD8843D0057B316 /* MSACKeychainUtilPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACKeychainUtilPrivate.h; sourceTree = ""; }; + 386E8D911E25932100EECF0F /* MSACHttpTestUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACHttpTestUtil.m; sourceTree = ""; }; + 386E8D921E25932100EECF0F /* MSACHttpTestUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACHttpTestUtil.h; sourceTree = ""; }; + 387A7FCA22178E91008A5587 /* MSACReachabilityTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACReachabilityTests.m; sourceTree = ""; }; + 387C757F1D6270A300D68CC1 /* MSACServiceAbstractInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = MSACServiceAbstractInternal.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 387C758D1D64DF2500D68CC1 /* MSACServiceCommon.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = MSACServiceCommon.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 387C75951D64EE1900D68CC1 /* MSACServiceAbstractTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = MSACServiceAbstractTests.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 387C76FC1D6C9CF100D68CC1 /* MSACServiceAbstract.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACServiceAbstract.h; sourceTree = ""; }; + 3889932E1E29829700C27B36 /* MSACAppCenterErrors.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACAppCenterErrors.h; sourceTree = ""; }; + 38A3891B212B6E3C00F1C0D8 /* MSACDeadLockTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACDeadLockTests.m; sourceTree = ""; }; + 38AE130F20C1B54000FAB2AD /* MSACModelTestsUtililty.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACModelTestsUtililty.h; sourceTree = ""; }; + 38AE131020C1B54000FAB2AD /* MSACModelTestsUtililty.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACModelTestsUtililty.m; sourceTree = ""; }; + 38BC346C20B8CB0C00119C05 /* MSACLogConversion.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACLogConversion.h; sourceTree = ""; }; + 38C4471E1ECE5352002E1B11 /* MSACAppDelegateForwarder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACAppDelegateForwarder.h; sourceTree = ""; }; + 38C4471F1ECE5352002E1B11 /* MSACAppDelegateForwarder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACAppDelegateForwarder.m; sourceTree = ""; }; + 38C633C61FDF15DD005F40C9 /* MSACCustomApplicationDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACCustomApplicationDelegate.h; sourceTree = ""; }; + 38C633C81FDF163D005F40C9 /* MSACAppDelegateUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACAppDelegateUtil.h; sourceTree = ""; }; + 38E1B6791DDE3FDF000EFED1 /* MSACAppCenterPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACAppCenterPrivate.h; sourceTree = ""; }; + 38F1944D1DADB93100D3E0FE /* MSACHttpIngestionPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACHttpIngestionPrivate.h; sourceTree = ""; }; + 38FDFF682109409900E17269 /* MSACMockKeychainUtil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACMockKeychainUtil.h; sourceTree = ""; }; + 38FDFF692109409900E17269 /* MSACMockKeychainUtil.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACMockKeychainUtil.m; sourceTree = ""; }; + 58603EA088552D6FBB8EBE6C /* MSACAbstractLogTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACAbstractLogTests.m; sourceTree = ""; }; + 5C7877911EA0CFF3002263CC /* MSACLogDBStorageTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACLogDBStorageTests.m; sourceTree = ""; }; + 6E04013F1D1C99AC0051BCFA /* MSACConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACConstants.h; sourceTree = ""; }; + 6E0401401D1C99AC0051BCFA /* AppCenter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppCenter.h; sourceTree = ""; }; + 6E0401441D1C99AC0051BCFA /* MSACAppCenterInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACAppCenterInternal.h; sourceTree = ""; }; + 6E0401451D1C99AC0051BCFA /* MSACService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACService.h; sourceTree = ""; }; + 6E0401471D1C99AC0051BCFA /* MSACServiceInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACServiceInternal.h; sourceTree = ""; }; + 6E0401551D1C9AAA0051BCFA /* MSACAppCenter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACAppCenter.h; sourceTree = ""; }; + 6E0401561D1C9AAA0051BCFA /* MSACAppCenter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACAppCenter.m; sourceTree = ""; }; + 6E0401581D1C9CFB0051BCFA /* AppCenter+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "AppCenter+Internal.h"; sourceTree = ""; }; + 6E0401841D1CAD810051BCFA /* AppCenter Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "AppCenter Debug.xcconfig"; sourceTree = ""; }; + 6E0684621D36BC8D00A8CC6C /* MSACChannelUnitDefault.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = MSACChannelUnitDefault.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 6E171B581D234717000DC480 /* MSACLogContainer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACLogContainer.h; sourceTree = ""; }; + 6E171B591D234717000DC480 /* MSACLogContainer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACLogContainer.m; sourceTree = ""; }; + 6E23957C1D22EF4F00E543C8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 6E3E2C9D1D35701000B1EE50 /* MSACLog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSACLog.h; path = Model/MSACLog.h; sourceTree = ""; }; + 6E3E2CC01D3596AE00B1EE50 /* MSACDeviceLogTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACDeviceLogTests.m; sourceTree = ""; }; + 6E48A5A31D3831FE006E8B5F /* MSACChannelUnitConfigurationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACChannelUnitConfigurationTests.m; sourceTree = ""; }; + 6E48A5A61D383893006E8B5F /* MSACChannelGroupDefaultTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACChannelGroupDefaultTests.m; sourceTree = ""; }; + 6EB1F40D1D2443B7005F9F99 /* MSACChannelUnitDefaultTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = MSACChannelUnitDefaultTests.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 6EF628F21D371B1600CAFF64 /* MSACChannelUnitConfiguration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACChannelUnitConfiguration.h; sourceTree = ""; }; + 6EF628F31D371B1600CAFF64 /* MSACChannelUnitConfiguration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACChannelUnitConfiguration.m; sourceTree = ""; }; + 7FB70548222E58FB00D93258 /* MSACMockReachability.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACMockReachability.m; sourceTree = ""; }; + 7FB7054A222E593400D93258 /* MSACMockReachability.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACMockReachability.h; sourceTree = ""; }; + 805275BF20A19F7B00704115 /* MSACOneCollectorChannelDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACOneCollectorChannelDelegate.h; sourceTree = ""; }; + 805275C020A1A5F400704115 /* MSACOneCollectorChannelDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACOneCollectorChannelDelegate.m; sourceTree = ""; }; + 805F3F691F209C8A00B489E4 /* MSACMockService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACMockService.h; sourceTree = ""; }; + 805F3F6A1F209C9D00B489E4 /* MSACMockService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACMockService.m; sourceTree = ""; }; + 8087362720C1348B004C4157 /* MSACEncrypter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACEncrypter.h; sourceTree = ""; }; + 8087362820C134AC004C4157 /* MSACEncrypter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACEncrypter.m; sourceTree = ""; }; + 8087362A20C1DCCF004C4157 /* MSACEncrypterTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACEncrypterTests.m; sourceTree = ""; }; + 80B7EA2020CA9C9C00DF524C /* MSACEncrypterPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACEncrypterPrivate.h; sourceTree = ""; }; + 9237B60B2244407000C273D8 /* MSACHttpClientTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACHttpClientTests.m; sourceTree = ""; }; + 9C02498021A4BF3800C7B887 /* MSACUserIdContextTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACUserIdContextTests.m; sourceTree = ""; }; + B23507B32118D22800F98D4F /* MSACTicketCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACTicketCache.m; sourceTree = ""; }; + B23507B42118D22800F98D4F /* MSACTicketCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACTicketCache.h; sourceTree = ""; }; + B24F3F161D93A3FF00827213 /* MSACLoggerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACLoggerTests.m; sourceTree = ""; }; + B26D4DB8211B5BE300AB4E28 /* MSACMockCommonSchemaLog.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACMockCommonSchemaLog.h; sourceTree = ""; }; + B26D4DB9211B5BE300AB4E28 /* MSACMockCommonSchemaLog.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACMockCommonSchemaLog.m; sourceTree = ""; }; + B26D4DD6211B9B5D00AB4E28 /* MSACTicketCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACTicketCacheTests.m; sourceTree = ""; }; + B27B4125214C939D007CAE9C /* MSACStorageTestUtil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACStorageTestUtil.h; sourceTree = ""; }; + B27B4126214C939D007CAE9C /* MSACStorageTestUtil.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACStorageTestUtil.m; sourceTree = ""; }; + B28E41D32076EC4000CC6AD8 /* MSACUtility+PropertyValidation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MSACUtility+PropertyValidation.h"; sourceTree = ""; }; + B28E41D42076ECFE00CC6AD8 /* MSACUtility+PropertyValidation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MSACUtility+PropertyValidation.m"; sourceTree = ""; }; + B29D883A21E925A400EAF084 /* MSACOrderedDictionaryPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACOrderedDictionaryPrivate.h; sourceTree = ""; }; + B29D883B21E925A400EAF084 /* MSACOrderedDictionary.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACOrderedDictionary.h; sourceTree = ""; }; + B29D883C21E925A400EAF084 /* MSACOrderedDictionary.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACOrderedDictionary.m; sourceTree = ""; }; + B29D884621E9286100EAF084 /* MSACOrderedDictionaryTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACOrderedDictionaryTest.m; sourceTree = ""; }; + B2ADB1412124C13300D0D7D9 /* MSACOneCollectorIngestionPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACOneCollectorIngestionPrivate.h; sourceTree = ""; }; + B2B7D50920C5E562001D31B8 /* MSACOneCollectorIngestion.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACOneCollectorIngestion.h; sourceTree = ""; }; + B2B7D50A20C5E562001D31B8 /* MSACOneCollectorIngestion.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACOneCollectorIngestion.m; sourceTree = ""; }; + B2C3C1831DB83A3E00CB83F7 /* MSACDeviceInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACDeviceInternal.h; sourceTree = ""; }; + B2CD74551F22BB710070E7DF /* MSACWrapperSdk.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSACWrapperSdk.h; path = Model/MSACWrapperSdk.h; sourceTree = ""; }; + B2CD74561F22BB710070E7DF /* MSACWrapperSdk.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MSACWrapperSdk.m; path = Model/MSACWrapperSdk.m; sourceTree = ""; }; + B2CD745D1F22BBBB0070E7DF /* MSACWrapperSdkInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACWrapperSdkInternal.h; sourceTree = ""; }; + B2CD74831F22BD910070E7DF /* MSACLogDBStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACLogDBStorage.h; sourceTree = ""; }; + B2CD74841F22BD910070E7DF /* MSACLogDBStorage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACLogDBStorage.m; sourceTree = ""; }; + B2CD748F1F22BE270070E7DF /* MSACConstants+Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MSACConstants+Internal.h"; sourceTree = ""; }; + B2CD74901F22BE270070E7DF /* MSACUtility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACUtility.h; sourceTree = ""; }; + B2CD74911F22BE270070E7DF /* MSACUtility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACUtility.m; sourceTree = ""; }; + B2CD74921F22BE270070E7DF /* MSACUtility+Application.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MSACUtility+Application.h"; sourceTree = ""; }; + B2CD74931F22BE270070E7DF /* MSACUtility+Application.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MSACUtility+Application.m"; sourceTree = ""; }; + B2CD74941F22BE270070E7DF /* MSACUtility+ApplicationPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MSACUtility+ApplicationPrivate.h"; sourceTree = ""; }; + B2CD74951F22BE270070E7DF /* MSACUtility+Date.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MSACUtility+Date.h"; sourceTree = ""; }; + B2CD74961F22BE270070E7DF /* MSACUtility+Date.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MSACUtility+Date.m"; sourceTree = ""; }; + B2CD74971F22BE270070E7DF /* MSACUtility+Environment.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MSACUtility+Environment.h"; sourceTree = ""; }; + B2CD74981F22BE270070E7DF /* MSACUtility+Environment.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MSACUtility+Environment.m"; sourceTree = ""; }; + B2CD74991F22BE270070E7DF /* MSACUtility+File.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MSACUtility+File.h"; sourceTree = ""; }; + B2CD749A1F22BE270070E7DF /* MSACUtility+File.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MSACUtility+File.m"; sourceTree = ""; }; + B2CD749B1F22BE270070E7DF /* MSACUtility+StringFormatting.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MSACUtility+StringFormatting.h"; sourceTree = ""; }; + B2CD749C1F22BE270070E7DF /* MSACUtility+StringFormatting.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MSACUtility+StringFormatting.m"; sourceTree = ""; }; + B2FD53601E56501B0050F909 /* MSACDeviceHistoryInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACDeviceHistoryInfo.h; sourceTree = ""; }; + B2FD53611E56501B0050F909 /* MSACDeviceHistoryInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACDeviceHistoryInfo.m; sourceTree = ""; }; + B2FD53641E567BCF0050F909 /* MSACDeviceHistoryInfoTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACDeviceHistoryInfoTests.m; sourceTree = ""; }; + BA6822A89D38F04D8D95CE83 /* MSACAppCenterIngestion.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACAppCenterIngestion.m; sourceTree = ""; }; + BA6824A001520825F18DFC42 /* MSACAppCenterIngestion.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACAppCenterIngestion.h; sourceTree = ""; }; + C2392EDA2464279400425640 /* OCMock.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = OCMock.xcodeproj; path = ../../Vendor/OCMock/Source/OCMock.xcodeproj; sourceTree = ""; }; + C26F4DE42546EB33006E0EB7 /* XCFramework.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = XCFramework.xcconfig; path = ../../../Config/XCFramework.xcconfig; sourceTree = ""; }; + C26F4DED2546EB34006E0EB7 /* iOS Universal.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "iOS Universal.xcconfig"; path = "../../../Config/iOS Universal.xcconfig"; sourceTree = ""; }; + C26F4DEE2546EB34006E0EB7 /* tvOS Universal.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "tvOS Universal.xcconfig"; path = "../../../Config/tvOS Universal.xcconfig"; sourceTree = ""; }; + C9A92007230C05D70068070D /* AppCenter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AppCenter.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C9A92026230C08540068070D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D36136821E7BB338004AE043 /* MSACStoragePerformanceTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACStoragePerformanceTests.m; sourceTree = ""; }; + D377A30B1E83A04600B2C97A /* MSACMockUserDefaults.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACMockUserDefaults.h; sourceTree = ""; }; + D377A30C1E83A05900B2C97A /* MSACMockUserDefaults.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACMockUserDefaults.m; sourceTree = ""; }; + D38023E61E6EFC7C00466558 /* MSACStartServiceLog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACStartServiceLog.m; sourceTree = ""; }; + D38023E71E6EFC7C00466558 /* MSACStartServiceLog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACStartServiceLog.h; sourceTree = ""; }; + D38024051E7126F500466558 /* MSACServiceAbstract.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACServiceAbstract.m; sourceTree = ""; }; + D38024111E7130C700466558 /* MSACStartServiceLogTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACStartServiceLogTests.m; sourceTree = ""; }; + D55E7082252F5A1000AB994D /* MSACTestSessionInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACTestSessionInfo.m; sourceTree = ""; }; + D55E7083252F5A1000AB994D /* MSACTestSessionInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACTestSessionInfo.h; sourceTree = ""; }; + D5812F312423C2FA00C5F5C5 /* MSACUserDefaultsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACUserDefaultsTests.m; sourceTree = ""; }; + DF5DA1F823A0E55500DE695C /* MSACDispatcherUtil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACDispatcherUtil.h; sourceTree = ""; }; + DF5DA1FC23A0E57B00DE695C /* MSACDispatcherUtil.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACDispatcherUtil.m; sourceTree = ""; }; + DFCB7FFD2472C1BD0058D292 /* OCHamcrest.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = OCHamcrest.xcodeproj; path = ../../Vendor/OCHamcrest/Source/OCHamcrest.xcodeproj; sourceTree = ""; }; + DFE95520244D96170061E3FA /* HTTPStubs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTTPStubs.m; sourceTree = ""; }; + DFE95521244D96170061E3FA /* NSURLRequest+HTTPBodyTesting.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSURLRequest+HTTPBodyTesting.m"; sourceTree = ""; }; + DFE95522244D96170061E3FA /* HTTPStubsMethodSwizzling.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTTPStubsMethodSwizzling.h; sourceTree = ""; }; + DFE95524244D96170061E3FA /* Compatibility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Compatibility.h; sourceTree = ""; }; + DFE95525244D96170061E3FA /* HTTPStubsResponse+JSON.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "HTTPStubsResponse+JSON.h"; sourceTree = ""; }; + DFE95526244D96170061E3FA /* HTTPStubsPathHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTTPStubsPathHelpers.h; sourceTree = ""; }; + DFE95527244D96170061E3FA /* HTTPStubsResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTTPStubsResponse.h; sourceTree = ""; }; + DFE95528244D96170061E3FA /* NSURLRequest+HTTPBodyTesting.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSURLRequest+HTTPBodyTesting.h"; sourceTree = ""; }; + DFE95529244D96170061E3FA /* HTTPStubs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTTPStubs.h; sourceTree = ""; }; + DFE9552A244D96170061E3FA /* HTTPStubsResponse+JSON.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "HTTPStubsResponse+JSON.m"; sourceTree = ""; }; + DFE9552B244D96170061E3FA /* HTTPStubsMethodSwizzling.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTTPStubsMethodSwizzling.m; sourceTree = ""; }; + DFE9552C244D96170061E3FA /* HTTPStubsResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTTPStubsResponse.m; sourceTree = ""; }; + DFE9552D244D96170061E3FA /* HTTPStubsPathHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTTPStubsPathHelpers.m; sourceTree = ""; }; + DFE9552E244D96170061E3FA /* HTTPStubs+NSURLSessionConfiguration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "HTTPStubs+NSURLSessionConfiguration.m"; sourceTree = ""; }; + E74B149720B364EE002C0183 /* MSACCommonSchemaLog.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACCommonSchemaLog.h; sourceTree = ""; }; + E74B149820B364EE002C0183 /* MSACCommonSchemaLog.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACCommonSchemaLog.m; sourceTree = ""; }; + E74B14A120B36AAB002C0183 /* MSACUserExtension.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACUserExtension.m; sourceTree = ""; }; + E74B14A820B36B9B002C0183 /* MSACLocExtension.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACLocExtension.h; sourceTree = ""; }; + E74B14A920B36B9B002C0183 /* MSACLocExtension.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACLocExtension.m; sourceTree = ""; }; + E74B14B820B36C2B002C0183 /* MSACOSExtension.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACOSExtension.h; sourceTree = ""; }; + E74B14C020B36C66002C0183 /* MSACAppExtension.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACAppExtension.h; sourceTree = ""; }; + E74B14C820B36DF1002C0183 /* MSACProtocolExtension.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACProtocolExtension.h; sourceTree = ""; }; + E74B14C920B36DF1002C0183 /* MSACProtocolExtension.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACProtocolExtension.m; sourceTree = ""; }; + E74B14D020B36EC5002C0183 /* MSACNetExtension.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACNetExtension.h; sourceTree = ""; }; + E74B14D120B36EC5002C0183 /* MSACNetExtension.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACNetExtension.m; sourceTree = ""; }; + E74B14D820B37042002C0183 /* MSACAppExtension.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACAppExtension.m; sourceTree = ""; }; + E74B14D920B37042002C0183 /* MSACOSExtension.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACOSExtension.m; sourceTree = ""; }; + E74B14E420B370A5002C0183 /* MSACUserExtension.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACUserExtension.h; sourceTree = ""; }; + E74B14E820B3711F002C0183 /* MSACSDKExtension.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACSDKExtension.h; sourceTree = ""; }; + E74B14E920B3711F002C0183 /* MSACSDKExtension.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACSDKExtension.m; sourceTree = ""; }; + E79750E620A4F41400E3EAE8 /* MSACOneCollectorChannelDelegatePrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACOneCollectorChannelDelegatePrivate.h; sourceTree = ""; }; + E79750EA20A50BF400E3EAE8 /* MSACOneCollectorChannelDelegateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACOneCollectorChannelDelegateTests.m; sourceTree = ""; }; + E7D23C5020B4E0CA00A47D62 /* MSACCommonSchemaLogTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACCommonSchemaLogTests.m; sourceTree = ""; }; + E7D23C5420B4E38B00A47D62 /* MSACCSData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACCSData.h; sourceTree = ""; }; + E7D23C5520B4E38B00A47D62 /* MSACCSData.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACCSData.m; sourceTree = ""; }; + E7D23C5C20B4EC3700A47D62 /* MSACSerializableObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACSerializableObject.h; sourceTree = ""; }; + E7D23C6020B4EED000A47D62 /* MSACModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACModel.h; sourceTree = ""; }; + E7D23C6420B6391700A47D62 /* MSACCSExtensions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACCSExtensions.h; sourceTree = ""; }; + E7D23C6520B6391700A47D62 /* MSACCSExtensions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACCSExtensions.m; sourceTree = ""; }; + E7D23C6E20B6412300A47D62 /* MSACCSExtensionsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACCSExtensionsTests.m; sourceTree = ""; }; + E8010E671D2DD4EF0035196F /* MSACLogWithProperties.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACLogWithProperties.m; sourceTree = ""; }; + E829E4221D25C8BA00F19DA1 /* MSACAppCenterIngestionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = MSACAppCenterIngestionTests.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + E83283C41D46C62E000B029E /* MSAC_Reachability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSAC_Reachability.h; sourceTree = ""; }; + E83283C51D46C62E000B029E /* MSAC_Reachability.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSAC_Reachability.m; sourceTree = ""; }; + E84B8E2C1D2351DB006FD231 /* MSACChannelGroupDefault.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACChannelGroupDefault.h; sourceTree = ""; }; + E84B8E2D1D2351DB006FD231 /* MSACChannelGroupDefault.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = MSACChannelGroupDefault.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + E84B8E311D235209006FD231 /* MSACIngestionProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = MSACIngestionProtocol.h; sourceTree = ""; }; + E84B8E331D235226006FD231 /* MSACHttpIngestion.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACHttpIngestion.h; sourceTree = ""; }; + E84B8E341D235226006FD231 /* MSACHttpIngestion.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = MSACHttpIngestion.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + E84B8E381D235246006FD231 /* MSACStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACStorage.h; sourceTree = ""; }; + E88D17041D35B6B500A5EA57 /* MSACMockLog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACMockLog.h; sourceTree = ""; }; + E88D17051D35B6B500A5EA57 /* MSACMockLog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACMockLog.m; sourceTree = ""; }; + E88EBBEB1D2C612E007E7785 /* MSACAbstractLog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACAbstractLog.m; sourceTree = ""; }; + E88EBBFA1D2C8CC7007E7785 /* MSACLogContainerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = MSACLogContainerTests.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + E8A8D1E91D3057A90022931E /* MSACAppCenterUserDefaults.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSACAppCenterUserDefaults.h; path = ../Model/Util/MSACAppCenterUserDefaults.h; sourceTree = ""; }; + E8A8D1EA1D3057A90022931E /* MSACAppCenterUserDefaults.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MSACAppCenterUserDefaults.m; path = ../Model/Util/MSACAppCenterUserDefaults.m; sourceTree = ""; }; + F803BBF11E8E3677004B1E7A /* MSACCustomProperties.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACCustomProperties.h; sourceTree = ""; }; + F803BBF21E8E3677004B1E7A /* MSACCustomProperties.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACCustomProperties.m; sourceTree = ""; }; + F803BBF51E8E383E004B1E7A /* MSACCustomPropertiesInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACCustomPropertiesInternal.h; sourceTree = ""; }; + F803BBF81E8E3989004B1E7A /* MSACCustomPropertiesLog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACCustomPropertiesLog.m; sourceTree = ""; }; + F803BBFA1E8E39AB004B1E7A /* MSACCustomPropertiesLog.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACCustomPropertiesLog.h; sourceTree = ""; }; + F803BC371E8E6927004B1E7A /* MSACCustomPropertiesTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACCustomPropertiesTests.m; sourceTree = ""; }; + F803BC391E8E6963004B1E7A /* MSACCustomPropertiesLogTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACCustomPropertiesLogTests.m; sourceTree = ""; }; + F82E4C6C217F159A00EDAB34 /* sqlite3.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = sqlite3.c; path = ../../Vendor/SQLite3/sqlite3.c; sourceTree = ""; }; + F898179E2433327C008D92E1 /* MSACAppCenterUserDefaultsPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACAppCenterUserDefaultsPrivate.h; sourceTree = ""; }; + F8BA7A2823AA8A26009FBCCF /* MSACStorageBindableArray.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACStorageBindableArray.h; sourceTree = ""; }; + F8BA7A2C23AA8B84009FBCCF /* MSACStorageBindableArray.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACStorageBindableArray.m; sourceTree = ""; }; + F8DC50C823AA75FF00BF8839 /* MSACStorageBindableType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACStorageBindableType.h; sourceTree = ""; }; + F8DC50C923AA777400BF8839 /* MSACStorageNumberType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACStorageNumberType.h; sourceTree = ""; }; + F8DC50CA23AA77CB00BF8839 /* MSACStorageTextType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACStorageTextType.h; sourceTree = ""; }; + F8DC50CC23AA77F700BF8839 /* MSACStorageTextType.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACStorageTextType.m; sourceTree = ""; }; + F8DC50CE23AA7A3900BF8839 /* MSACStorageNumberType.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACStorageNumberType.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + C9A92001230C05D70068070D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 045C850E1FE0B19C00089540 /* Session */ = { + isa = PBXGroup; + children = ( + 045C850F1FE0B1D900089540 /* MSACSessionContext.h */, + 049378381FE44539000ADBAF /* MSACSessionContextPrivate.h */, + 045C85131FE0B31600089540 /* MSACSessionContext.m */, + 047EBB9C1FE30842009BB1C8 /* MSACSessionHistoryInfo.h */, + 047EBB9B1FE30842009BB1C8 /* MSACSessionHistoryInfo.m */, + ); + path = Session; + sourceTree = ""; + }; + 047FEE0E21A48BDE00ED77CD /* Context */ = { + isa = PBXGroup; + children = ( + 381C91E21D3DB65D004512F1 /* Device */, + 045C850E1FE0B19C00089540 /* Session */, + 047FEE0F21A48CC200ED77CD /* UserId */, + ); + path = Context; + sourceTree = ""; + }; + 047FEE0F21A48CC200ED77CD /* UserId */ = { + isa = PBXGroup; + children = ( + 047FEE1221A48CC200ED77CD /* MSACUserIdContext.h */, + 047FEE2521A4968A00ED77CD /* MSACUserIdContextPrivate.h */, + 047FEE1021A48CC200ED77CD /* MSACUserIdContext.m */, + 047FEE1121A48CC200ED77CD /* MSACUserIdHistoryInfo.h */, + 047FEE1321A48CC200ED77CD /* MSACUserIdHistoryInfo.m */, + ); + path = UserId; + sourceTree = ""; + }; + 04A140AB1ECE7F6F001CEE94 /* Support */ = { + isa = PBXGroup; + children = ( + 04840CD71ECCF6560020B0FC /* Tests iOS.xcconfig */, + 041CFF8D1ECCFFD200B4654B /* Tests macOS.xcconfig */, + 0484DD5E1F3910DF0092B777 /* Tests tvOS.xcconfig */, + ); + name = Support; + sourceTree = ""; + }; + 2DA0385B8A682DEEC59324CE /* HttpClient */ = { + isa = PBXGroup; + children = ( + 3571F657224962090052406C /* Util */, + 2DA0373E842A41C8413D1722 /* MSACHttpClientProtocol.h */, + 2DA0350241FEC3A6A4919BFE /* MSACHttpClient.m */, + 2DA03260792C64C0290E6E41 /* MSACHttpClient.h */, + 3571F64B22454EDF0052406C /* MSACHttpClientPrivate.h */, + 3571F64F22457E220052406C /* MSACHttpCall.h */, + 3571F65322457FCC0052406C /* MSACHttpCall.m */, + ); + path = HttpClient; + sourceTree = ""; + }; + 354B79E0217109CE002183B5 /* Properties */ = { + isa = PBXGroup; + children = ( + 35DFC1C72170030C00455589 /* MSACBooleanTypedProperty.h */, + 35DFC1C22170030C00455589 /* MSACBooleanTypedProperty.m */, + 35DFC1C52170030C00455589 /* MSACDateTimeTypedProperty.h */, + 35DFC1C42170030C00455589 /* MSACDateTimeTypedProperty.m */, + 35DFC1C02170030C00455589 /* MSACDoubleTypedProperty.h */, + 35DFC1C62170030C00455589 /* MSACDoubleTypedProperty.m */, + 35DFC1C12170030C00455589 /* MSACLongTypedProperty.h */, + 35DFC1C82170030C00455589 /* MSACLongTypedProperty.m */, + 35DFC1BD2170030C00455589 /* MSACStringTypedProperty.h */, + 35DFC1BE2170030C00455589 /* MSACStringTypedProperty.m */, + 35DFC1BF2170030C00455589 /* MSACTypedProperty.h */, + 35DFC1C32170030C00455589 /* MSACTypedProperty.m */, + ); + path = Properties; + sourceTree = ""; + }; + 3571F657224962090052406C /* Util */ = { + isa = PBXGroup; + children = ( + 3571F658224962180052406C /* MSACHttpUtil.h */, + 3571F659224962180052406C /* MSACHttpUtil.m */, + ); + path = Util; + sourceTree = ""; + }; + 381C91E21D3DB65D004512F1 /* Device */ = { + isa = PBXGroup; + children = ( + B2FD53601E56501B0050F909 /* MSACDeviceHistoryInfo.h */, + B2FD53611E56501B0050F909 /* MSACDeviceHistoryInfo.m */, + 381C91E51D3DB65D004512F1 /* MSACDeviceTracker.h */, + 04754F3D1EA980FD002CBA46 /* MSACDeviceTrackerPrivate.h */, + 04754F3C1EA980FD002CBA46 /* MSACDeviceTracker.m */, + ); + path = Device; + sourceTree = ""; + }; + 3844FF161E8C261D003E9194 /* Model */ = { + isa = PBXGroup; + children = ( + F803BBF11E8E3677004B1E7A /* MSACCustomProperties.h */, + F803BBF21E8E3677004B1E7A /* MSACCustomProperties.m */, + 3844FF1B1E8C2716003E9194 /* MSACDevice.h */, + 3844FF1A1E8C2716003E9194 /* MSACDevice.m */, + 6E3E2C9D1D35701000B1EE50 /* MSACLog.h */, + 3844FF191E8C2716003E9194 /* MSACLogWithProperties.h */, + 3844FF1C1E8C2716003E9194 /* MSACAbstractLog.h */, + B2CD74551F22BB710070E7DF /* MSACWrapperSdk.h */, + B2CD74561F22BB710070E7DF /* MSACWrapperSdk.m */, + ); + name = Model; + sourceTree = ""; + }; + 38C4471C1ECE5352002E1B11 /* DelegateForwarder */ = { + isa = PBXGroup; + children = ( + 38C633C81FDF163D005F40C9 /* MSACAppDelegateUtil.h */, + 38C4471E1ECE5352002E1B11 /* MSACAppDelegateForwarder.h */, + 38C4471F1ECE5352002E1B11 /* MSACAppDelegateForwarder.m */, + 38C633C61FDF15DD005F40C9 /* MSACCustomApplicationDelegate.h */, + 3803208D217E8BD40089772A /* MSACDelegateForwarderPrivate.h */, + 38032085217E85A90089772A /* MSACDelegateForwarder.h */, + 38032086217E85A90089772A /* MSACDelegateForwarder.m */, + 38032091217E9DC50089772A /* MSACCustomDelegate.h */, + ); + path = DelegateForwarder; + sourceTree = ""; + }; + 6E0400FA1D1C98220051BCFA = { + isa = PBXGroup; + children = ( + 6E0401051D1C98220051BCFA /* AppCenter */, + 6E2395791D22EF4F00E543C8 /* AppCenterTests */, + 6E0401041D1C98220051BCFA /* Products */, + ); + sourceTree = ""; + wrapsLines = 0; + }; + 6E0401041D1C98220051BCFA /* Products */ = { + isa = PBXGroup; + children = ( + C9A92007230C05D70068070D /* AppCenter.framework */, + ); + name = Products; + sourceTree = ""; + }; + 6E0401051D1C98220051BCFA /* AppCenter */ = { + isa = PBXGroup; + children = ( + 6E0401401D1C99AC0051BCFA /* AppCenter.h */, + 6E0401551D1C9AAA0051BCFA /* MSACAppCenter.h */, + 6E0401561D1C9AAA0051BCFA /* MSACAppCenter.m */, + 3889932E1E29829700C27B36 /* MSACAppCenterErrors.h */, + 3542741A2012AF0500BE766F /* MSACChannelGroupProtocol.h */, + 354274162012AE4000BE766F /* MSACChannelProtocol.h */, + 6E04013F1D1C99AC0051BCFA /* MSACConstants.h */, + 04B525B82194D44E00FA37FD /* MSACConstants+Flags.h */, + 3542742220167DA400BE766F /* MSACEnable.h */, + 358F9BC22019531F00B9E22C /* MSACLogger.h */, + 3592ABA61DC90E3600EF4592 /* MSACLogger.m */, + 6E0401451D1C99AC0051BCFA /* MSACService.h */, + 387C76FC1D6C9CF100D68CC1 /* MSACServiceAbstract.h */, + D38024051E7126F500466558 /* MSACServiceAbstract.m */, + 35D0B7511DDFABFD003EACCD /* MSACWrapperLogger.h */, + 35D0B7521DDFABFD003EACCD /* MSACWrapperLogger.m */, + 6E0401421D1C99AC0051BCFA /* Internals */, + 3844FF161E8C261D003E9194 /* Model */, + 6E0401821D1CAD810051BCFA /* Support */, + ); + path = AppCenter; + sourceTree = ""; + }; + 6E0401421D1C99AC0051BCFA /* Internals */ = { + isa = PBXGroup; + children = ( + 6E0401581D1C9CFB0051BCFA /* AppCenter+Internal.h */, + 6E0401441D1C99AC0051BCFA /* MSACAppCenterInternal.h */, + 38E1B6791DDE3FDF000EFED1 /* MSACAppCenterPrivate.h */, + F803BBF51E8E383E004B1E7A /* MSACCustomPropertiesInternal.h */, + 266ED9D2BA796BA0329F4FC7 /* MSACCustomPropertiesPrivate.h */, + 244CC1F62399CA5A00A58F51 /* MSACDependencyConfiguration.h */, + 244CC1F72399CA5A00A58F51 /* MSACDependencyConfiguration.m */, + 3592ABA51DC90E3600EF4592 /* MSACLoggerInternal.h */, + 387C757F1D6270A300D68CC1 /* MSACServiceAbstractInternal.h */, + 387C758D1D64DF2500D68CC1 /* MSACServiceCommon.h */, + 6E0401471D1C99AC0051BCFA /* MSACServiceInternal.h */, + 38C4471C1ECE5352002E1B11 /* DelegateForwarder */, + E84B8E241D234EC7006FD231 /* Channel */, + 047FEE0E21A48BDE00ED77CD /* Context */, + 2DA0385B8A682DEEC59324CE /* HttpClient */, + E84B8E221D234EBB006FD231 /* Ingestion */, + 6E171B511D234717000DC480 /* Model */, + E84B8E231D234EC1006FD231 /* Storage */, + 6E0401481D1C99AC0051BCFA /* Util */, + E83283C21D46C62E000B029E /* Vendor */, + ); + path = Internals; + sourceTree = ""; + }; + 6E0401481D1C99AC0051BCFA /* Util */ = { + isa = PBXGroup; + children = ( + B29D883B21E925A400EAF084 /* MSACOrderedDictionary.h */, + B29D883C21E925A400EAF084 /* MSACOrderedDictionary.m */, + B29D883A21E925A400EAF084 /* MSACOrderedDictionaryPrivate.h */, + B2CD748F1F22BE270070E7DF /* MSACConstants+Internal.h */, + 8087362720C1348B004C4157 /* MSACEncrypter.h */, + 80B7EA2020CA9C9C00DF524C /* MSACEncrypterPrivate.h */, + 8087362820C134AC004C4157 /* MSACEncrypter.m */, + 047FEE0621A4884600ED77CD /* MSACHistoryInfo.h */, + 047FEE0721A4884600ED77CD /* MSACHistoryInfo.m */, + 045BC3181E3FD8AC00B6C960 /* MSACKeychainUtil.h */, + 386A69EC1FD8843D0057B316 /* MSACKeychainUtilPrivate.h */, + 045BC3161E3FD88600B6C960 /* MSACKeychainUtil.m */, + B2CD74901F22BE270070E7DF /* MSACUtility.h */, + B2CD74911F22BE270070E7DF /* MSACUtility.m */, + B2CD74921F22BE270070E7DF /* MSACUtility+Application.h */, + B2CD74931F22BE270070E7DF /* MSACUtility+Application.m */, + B2CD74941F22BE270070E7DF /* MSACUtility+ApplicationPrivate.h */, + B2CD74951F22BE270070E7DF /* MSACUtility+Date.h */, + B2CD74961F22BE270070E7DF /* MSACUtility+Date.m */, + B2CD74971F22BE270070E7DF /* MSACUtility+Environment.h */, + B2CD74981F22BE270070E7DF /* MSACUtility+Environment.m */, + B2CD74991F22BE270070E7DF /* MSACUtility+File.h */, + B2CD749A1F22BE270070E7DF /* MSACUtility+File.m */, + B28E41D32076EC4000CC6AD8 /* MSACUtility+PropertyValidation.h */, + B28E41D42076ECFE00CC6AD8 /* MSACUtility+PropertyValidation.m */, + B2CD749B1F22BE270070E7DF /* MSACUtility+StringFormatting.h */, + B2CD749C1F22BE270070E7DF /* MSACUtility+StringFormatting.m */, + 38148D8420D07FB70046257E /* MSACCompression.h */, + 38148D8520D07FB70046257E /* MSACCompression.m */, + DF5DA1F823A0E55500DE695C /* MSACDispatcherUtil.h */, + DF5DA1FC23A0E57B00DE695C /* MSACDispatcherUtil.m */, + ); + path = Util; + sourceTree = ""; + }; + 6E0401821D1CAD810051BCFA /* Support */ = { + isa = PBXGroup; + children = ( + 6E0401841D1CAD810051BCFA /* AppCenter Debug.xcconfig */, + 0469D1B11F4DF89A00A43A8E /* AppCenter Release.xcconfig */, + 04545DC8227B603D00A49E06 /* AppCenter.xcconfig */, + C9A92026230C08540068070D /* Info.plist */, + C26F4DED2546EB34006E0EB7 /* iOS Universal.xcconfig */, + 0485AFEE1EAAAD0000C10CAF /* iOS.modulemap */, + 049BC8281ECE3A5200FB6719 /* iOS.xcconfig */, + 0485AFEC1EAAAC3B00C10CAF /* macOS.modulemap */, + 049BC8291ECE3A5A00FB6719 /* macOS.xcconfig */, + C26F4DEE2546EB34006E0EB7 /* tvOS Universal.xcconfig */, + 043120701EE0BCC4007054C5 /* tvOS.modulemap */, + 043120711EE0BCC4007054C5 /* tvOS.xcconfig */, + C26F4DE42546EB33006E0EB7 /* XCFramework.xcconfig */, + ); + path = Support; + sourceTree = ""; + }; + 6E171B511D234717000DC480 /* Model */ = { + isa = PBXGroup; + children = ( + E74B149F20B369BF002C0183 /* CommonSchema */, + 3844FF121E8C22CC003E9194 /* MSACAbstractLogInternal.h */, + E88EBBEB1D2C612E007E7785 /* MSACAbstractLog.m */, + 3513432D2056FF9000E6DC7D /* MSACAbstractLogPrivate.h */, + F803BBFA1E8E39AB004B1E7A /* MSACCustomPropertiesLog.h */, + F803BBF81E8E3989004B1E7A /* MSACCustomPropertiesLog.m */, + B2C3C1831DB83A3E00CB83F7 /* MSACDeviceInternal.h */, + 6E171B581D234717000DC480 /* MSACLogContainer.h */, + 6E171B591D234717000DC480 /* MSACLogContainer.m */, + E8010E671D2DD4EF0035196F /* MSACLogWithProperties.m */, + E7D23C6020B4EED000A47D62 /* MSACModel.h */, + 04F3613C1FE1A92000A5A6D2 /* MSACNoAutoAssignSessionIdLog.h */, + E7D23C5C20B4EC3700A47D62 /* MSACSerializableObject.h */, + D38023E71E6EFC7C00466558 /* MSACStartServiceLog.h */, + D38023E61E6EFC7C00466558 /* MSACStartServiceLog.m */, + B2CD745D1F22BBBB0070E7DF /* MSACWrapperSdkInternal.h */, + 38BC346C20B8CB0C00119C05 /* MSACLogConversion.h */, + 354B79E0217109CE002183B5 /* Properties */, + ); + path = Model; + sourceTree = ""; + }; + 6E2395791D22EF4F00E543C8 /* AppCenterTests */ = { + isa = PBXGroup; + children = ( + E7D23C6C20B6407E00A47D62 /* Model */, + 6E23957C1D22EF4F00E543C8 /* Info.plist */, + E829E4221D25C8BA00F19DA1 /* MSACAppCenterIngestionTests.m */, + 384959D41D491D4F008F6B3A /* MSACAppCenterTests.m */, + 38641B041EB0F40800B2CE73 /* MSACAppDelegateForwarderTests.m */, + 6E48A5A61D383893006E8B5F /* MSACChannelGroupDefaultTests.m */, + 6E48A5A31D3831FE006E8B5F /* MSACChannelUnitConfigurationTests.m */, + 6EB1F40D1D2443B7005F9F99 /* MSACChannelUnitDefaultTests.m */, + F803BC371E8E6927004B1E7A /* MSACCustomPropertiesTests.m */, + 3849BA7D1EF3489D0072E3E0 /* MSACDBStorageTests.m */, + 38A3891B212B6E3C00F1C0D8 /* MSACDeadLockTests.m */, + 2434213123A456C900EDFA83 /* MSACDependencyConfigurationTests.m */, + B2FD53641E567BCF0050F909 /* MSACDeviceHistoryInfoTests.m */, + 385FC0541D37EBD700A1799F /* MSACDeviceTrackerTests.m */, + 8087362A20C1DCCF004C4157 /* MSACEncrypterTests.m */, + 9237B60B2244407000C273D8 /* MSACHttpClientTests.m */, + 359E898F224BF70400795CF5 /* MSACHttpCallTests.m */, + 04B59A4022050370008DA079 /* MSACHttpIngestionTests.m */, + 04B7BBEE1E5FAD4D001A0CE1 /* MSACHttpUtilTests.m */, + 04FD126A1E4103CC007ABFE7 /* MSACKeychainUtilTests.m */, + 5C7877911EA0CFF3002263CC /* MSACLogDBStorageTests.m */, + D5812F312423C2FA00C5F5C5 /* MSACUserDefaultsTests.m */, + B24F3F161D93A3FF00827213 /* MSACLoggerTests.m */, + 045660FA1D99EEEB002F7055 /* MSACLogWithPropertiesTests.m */, + E79750EA20A50BF400E3EAE8 /* MSACOneCollectorChannelDelegateTests.m */, + 04A79A4720C6F635006D0072 /* MSACOneCollectorIngestionTests.m */, + B29D884621E9286100EAF084 /* MSACOrderedDictionaryTest.m */, + 387A7FCA22178E91008A5587 /* MSACReachabilityTests.m */, + 387C75951D64EE1900D68CC1 /* MSACServiceAbstractTests.m */, + 0493783F1FE4913C000ADBAF /* MSACSessionContextTests.m */, + D36136821E7BB338004AE043 /* MSACStoragePerformanceTests.m */, + B26D4DD6211B9B5D00AB4E28 /* MSACTicketCacheTests.m */, + 9C02498021A4BF3800C7B887 /* MSACUserIdContextTests.m */, + 380A4DCA1DD6908A00E99219 /* MSACUtilityTests.m */, + 04A20D70217660D50096723C /* MSACWrapperLoggerTests.m */, + 6E2395831D22EF5F00E543C8 /* Frameworks */, + 04A140AB1ECE7F6F001CEE94 /* Support */, + 6E363AEF1D2C84D60079043D /* Util */, + ); + path = AppCenterTests; + sourceTree = ""; + }; + 6E2395831D22EF5F00E543C8 /* Frameworks */ = { + isa = PBXGroup; + children = ( + DFCB7FFD2472C1BD0058D292 /* OCHamcrest.xcodeproj */, + C2392EDA2464279400425640 /* OCMock.xcodeproj */, + DFE9551F244D96170061E3FA /* OHHTTPStubs */, + F82E4C6C217F159A00EDAB34 /* sqlite3.c */, + ); + name = Frameworks; + sourceTree = ""; + }; + 6E363AEF1D2C84D60079043D /* Util */ = { + isa = PBXGroup; + children = ( + D55E7083252F5A1000AB994D /* MSACTestSessionInfo.h */, + D55E7082252F5A1000AB994D /* MSACTestSessionInfo.m */, + 386E8D921E25932100EECF0F /* MSACHttpTestUtil.h */, + 386E8D911E25932100EECF0F /* MSACHttpTestUtil.m */, + B26D4DB8211B5BE300AB4E28 /* MSACMockCommonSchemaLog.h */, + B26D4DB9211B5BE300AB4E28 /* MSACMockCommonSchemaLog.m */, + E88D17041D35B6B500A5EA57 /* MSACMockLog.h */, + E88D17051D35B6B500A5EA57 /* MSACMockLog.m */, + 38FDFF682109409900E17269 /* MSACMockKeychainUtil.h */, + 38FDFF692109409900E17269 /* MSACMockKeychainUtil.m */, + D377A30B1E83A04600B2C97A /* MSACMockUserDefaults.h */, + D377A30C1E83A05900B2C97A /* MSACMockUserDefaults.m */, + 805F3F691F209C8A00B489E4 /* MSACMockService.h */, + 805F3F6A1F209C9D00B489E4 /* MSACMockService.m */, + 35C0E3CA1FD6146A004E841E /* MSACMockSecondService.h */, + 35C0E3C91FD6146A004E841E /* MSACMockSecondService.m */, + 04311FEE1EE083C2007054C5 /* MSACTestFrameworks.h */, + 35B80F9220C1FE1A00CDFA55 /* MSACMockLogObject.h */, + 3511AE7C20C5FF150056A739 /* MSACMockLogWithConversion.h */, + B27B4125214C939D007CAE9C /* MSACStorageTestUtil.h */, + B27B4126214C939D007CAE9C /* MSACStorageTestUtil.m */, + 384A925F2188BE400099BE70 /* MSACDelegateForwarderTestUtil.h */, + 384A92602188BE400099BE70 /* MSACDelegateForwarderTestUtil.m */, + 7FB70548222E58FB00D93258 /* MSACMockReachability.m */, + 7FB7054A222E593400D93258 /* MSACMockReachability.h */, + 24D44B402395DBBE003CC224 /* MSACTestUtil.h */, + 24D44B412395DBBE003CC224 /* MSACTestUtil.m */, + ); + path = Util; + sourceTree = ""; + }; + C2392EDB2464279400425640 /* Products */ = { + isa = PBXGroup; + children = ( + C2392EE52464279400425640 /* OCMock.framework */, + C2392EE72464279400425640 /* OCMockTests.xctest */, + C2392EE92464279400425640 /* libOCMock.a */, + C2392EEB2464279400425640 /* OCMockLibTests.xctest */, + C2392EED2464279400425640 /* OCMock.framework */, + C2392EEF2464279400425640 /* OCMock.framework */, + C2392EF12464279400425640 /* OCMock.framework */, + ); + name = Products; + sourceTree = ""; + }; + DFCB7FFE2472C1BD0058D292 /* Products */ = { + isa = PBXGroup; + children = ( + DFCB80082472C1BD0058D292 /* OCHamcrest.framework */, + DFCB800A2472C1BD0058D292 /* OCHamcrestTests.xctest */, + DFCB800C2472C1BD0058D292 /* libochamcrest.a */, + DFCB800E2472C1BD0058D292 /* libochamcrestTests.xctest */, + DFCB80102472C1BD0058D292 /* OCHamcrest.framework */, + DFCB80122472C1BD0058D292 /* OCHamcrest.framework */, + DFCB80142472C1BD0058D292 /* OCHamcrest.framework */, + ); + name = Products; + sourceTree = ""; + }; + DFE9551F244D96170061E3FA /* OHHTTPStubs */ = { + isa = PBXGroup; + children = ( + DFE95520244D96170061E3FA /* HTTPStubs.m */, + DFE95521244D96170061E3FA /* NSURLRequest+HTTPBodyTesting.m */, + DFE95522244D96170061E3FA /* HTTPStubsMethodSwizzling.h */, + DFE95523244D96170061E3FA /* include */, + DFE9552A244D96170061E3FA /* HTTPStubsResponse+JSON.m */, + DFE9552B244D96170061E3FA /* HTTPStubsMethodSwizzling.m */, + DFE9552C244D96170061E3FA /* HTTPStubsResponse.m */, + DFE9552D244D96170061E3FA /* HTTPStubsPathHelpers.m */, + DFE9552E244D96170061E3FA /* HTTPStubs+NSURLSessionConfiguration.m */, + ); + name = OHHTTPStubs; + path = ../../Vendor/OHHTTPStubs/Sources/OHHTTPStubs; + sourceTree = ""; + }; + DFE95523244D96170061E3FA /* include */ = { + isa = PBXGroup; + children = ( + DFE95524244D96170061E3FA /* Compatibility.h */, + DFE95525244D96170061E3FA /* HTTPStubsResponse+JSON.h */, + DFE95526244D96170061E3FA /* HTTPStubsPathHelpers.h */, + DFE95527244D96170061E3FA /* HTTPStubsResponse.h */, + DFE95528244D96170061E3FA /* NSURLRequest+HTTPBodyTesting.h */, + DFE95529244D96170061E3FA /* HTTPStubs.h */, + ); + path = include; + sourceTree = ""; + }; + E74B149F20B369BF002C0183 /* CommonSchema */ = { + isa = PBXGroup; + children = ( + E74B14C020B36C66002C0183 /* MSACAppExtension.h */, + E74B14D820B37042002C0183 /* MSACAppExtension.m */, + E74B149720B364EE002C0183 /* MSACCommonSchemaLog.h */, + E74B149820B364EE002C0183 /* MSACCommonSchemaLog.m */, + E7D23C5420B4E38B00A47D62 /* MSACCSData.h */, + E7D23C5520B4E38B00A47D62 /* MSACCSData.m */, + E7D23C6420B6391700A47D62 /* MSACCSExtensions.h */, + E7D23C6520B6391700A47D62 /* MSACCSExtensions.m */, + 359C38DB214079D90066C509 /* MSACDeviceExtension.h */, + 359C38DC214079D90066C509 /* MSACDeviceExtension.m */, + E74B14A820B36B9B002C0183 /* MSACLocExtension.h */, + E74B14A920B36B9B002C0183 /* MSACLocExtension.m */, + 2DA038EE68EEE1414A270ADA /* MSACMetadataExtension.h */, + 2DA030746930EE3DCB6A21B3 /* MSACMetadataExtension.m */, + E74B14D020B36EC5002C0183 /* MSACNetExtension.h */, + E74B14D120B36EC5002C0183 /* MSACNetExtension.m */, + E74B14B820B36C2B002C0183 /* MSACOSExtension.h */, + E74B14D920B37042002C0183 /* MSACOSExtension.m */, + E74B14C820B36DF1002C0183 /* MSACProtocolExtension.h */, + E74B14C920B36DF1002C0183 /* MSACProtocolExtension.m */, + E74B14E820B3711F002C0183 /* MSACSDKExtension.h */, + E74B14E920B3711F002C0183 /* MSACSDKExtension.m */, + E74B14E420B370A5002C0183 /* MSACUserExtension.h */, + E74B14A120B36AAB002C0183 /* MSACUserExtension.m */, + ); + path = CommonSchema; + sourceTree = ""; + }; + E7D23C6C20B6407E00A47D62 /* Model */ = { + isa = PBXGroup; + children = ( + E7D23C6D20B640A000A47D62 /* CommonSchema */, + 58603EA088552D6FBB8EBE6C /* MSACAbstractLogTests.m */, + 35DFC2172170044500455589 /* MSACBooleanTypedPropertyTests.m */, + F803BC391E8E6963004B1E7A /* MSACCustomPropertiesLogTests.m */, + 35DFC2192170044600455589 /* MSACDateTimeTypedPropertyTests.m */, + 6E3E2CC01D3596AE00B1EE50 /* MSACDeviceLogTests.m */, + 35DFC2182170044600455589 /* MSACDoubleTypedPropertyTests.m */, + E88EBBFA1D2C8CC7007E7785 /* MSACLogContainerTests.m */, + 35DFC21B2170044600455589 /* MSACLongTypedPropertyTests.m */, + 38AE130F20C1B54000FAB2AD /* MSACModelTestsUtililty.h */, + 38AE131020C1B54000FAB2AD /* MSACModelTestsUtililty.m */, + D38024111E7130C700466558 /* MSACStartServiceLogTests.m */, + 35DFC21A2170044600455589 /* MSACStringTypedPropertyTests.m */, + 35DFC21C2170044600455589 /* MSACTypedPropertyTests.m */, + ); + name = Model; + sourceTree = ""; + }; + E7D23C6D20B640A000A47D62 /* CommonSchema */ = { + isa = PBXGroup; + children = ( + E7D23C5020B4E0CA00A47D62 /* MSACCommonSchemaLogTests.m */, + E7D23C6E20B6412300A47D62 /* MSACCSExtensionsTests.m */, + ); + name = CommonSchema; + sourceTree = ""; + }; + E83283C21D46C62E000B029E /* Vendor */ = { + isa = PBXGroup; + children = ( + E83283C31D46C62E000B029E /* Reachability */, + ); + path = Vendor; + sourceTree = ""; + }; + E83283C31D46C62E000B029E /* Reachability */ = { + isa = PBXGroup; + children = ( + E83283C41D46C62E000B029E /* MSAC_Reachability.h */, + E83283C51D46C62E000B029E /* MSAC_Reachability.m */, + ); + path = Reachability; + sourceTree = ""; + }; + E84B8E221D234EBB006FD231 /* Ingestion */ = { + isa = PBXGroup; + children = ( + 04237DC023A31617009BB406 /* MSACHttpClientDelegate.h */, + E8753D6B1D4BE4FC00241513 /* Util */, + E84B8E311D235209006FD231 /* MSACIngestionProtocol.h */, + E84B8E331D235226006FD231 /* MSACHttpIngestion.h */, + 38F1944D1DADB93100D3E0FE /* MSACHttpIngestionPrivate.h */, + E84B8E341D235226006FD231 /* MSACHttpIngestion.m */, + BA6824A001520825F18DFC42 /* MSACAppCenterIngestion.h */, + BA6822A89D38F04D8D95CE83 /* MSACAppCenterIngestion.m */, + B2B7D50920C5E562001D31B8 /* MSACOneCollectorIngestion.h */, + B2ADB1412124C13300D0D7D9 /* MSACOneCollectorIngestionPrivate.h */, + B2B7D50A20C5E562001D31B8 /* MSACOneCollectorIngestion.m */, + ); + path = Ingestion; + sourceTree = ""; + }; + E84B8E231D234EC1006FD231 /* Storage */ = { + isa = PBXGroup; + children = ( + E84B8E381D235246006FD231 /* MSACStorage.h */, + 3849BA791EF314440072E3E0 /* MSACDBStorage.h */, + 3849BA841EF35D830072E3E0 /* MSACDBStoragePrivate.h */, + 3849BA7B1EF3145A0072E3E0 /* MSACDBStorage.m */, + E8A8D1E91D3057A90022931E /* MSACAppCenterUserDefaults.h */, + E8A8D1EA1D3057A90022931E /* MSACAppCenterUserDefaults.m */, + B2CD74831F22BD910070E7DF /* MSACLogDBStorage.h */, + 383481721EA7FF6100787F56 /* MSACLogDBStoragePrivate.h */, + B2CD74841F22BD910070E7DF /* MSACLogDBStorage.m */, + 046658B7215AD59D0079DCC7 /* MSACLogDBStorageVersion.h */, + F8DC50C823AA75FF00BF8839 /* MSACStorageBindableType.h */, + F8DC50C923AA777400BF8839 /* MSACStorageNumberType.h */, + F8DC50CA23AA77CB00BF8839 /* MSACStorageTextType.h */, + F8DC50CC23AA77F700BF8839 /* MSACStorageTextType.m */, + F8DC50CE23AA7A3900BF8839 /* MSACStorageNumberType.m */, + F8BA7A2823AA8A26009FBCCF /* MSACStorageBindableArray.h */, + F8BA7A2C23AA8B84009FBCCF /* MSACStorageBindableArray.m */, + F898179E2433327C008D92E1 /* MSACAppCenterUserDefaultsPrivate.h */, + ); + path = Storage; + sourceTree = ""; + }; + E84B8E241D234EC7006FD231 /* Channel */ = { + isa = PBXGroup; + children = ( + E84B8E2C1D2351DB006FD231 /* MSACChannelGroupDefault.h */, + 04AB676220E18A74002828AA /* MSACChannelGroupDefaultPrivate.h */, + E84B8E2D1D2351DB006FD231 /* MSACChannelGroupDefault.m */, + 6EF628F21D371B1600CAFF64 /* MSACChannelUnitConfiguration.h */, + 6EF628F31D371B1600CAFF64 /* MSACChannelUnitConfiguration.m */, + 3542741C2012B53B00BE766F /* MSACChannelUnitDefault.h */, + 2DA030B94CD3D38725671A79 /* MSACChannelUnitDefaultPrivate.h */, + 6E0684621D36BC8D00A8CC6C /* MSACChannelUnitDefault.m */, + 3542741B2012B02600BE766F /* MSACChannelDelegate.h */, + 805275BF20A19F7B00704115 /* MSACOneCollectorChannelDelegate.h */, + E79750E620A4F41400E3EAE8 /* MSACOneCollectorChannelDelegatePrivate.h */, + 805275C020A1A5F400704115 /* MSACOneCollectorChannelDelegate.m */, + 354274192012AEFC00BE766F /* MSACChannelUnitProtocol.h */, + 3814A8E120BF5E790093AF45 /* MSACCSEpochAndSeq.h */, + 3814A8E520BF5FA00093AF45 /* MSACCSEpochAndSeq.m */, + ); + path = Channel; + sourceTree = ""; + }; + E8753D6B1D4BE4FC00241513 /* Util */ = { + isa = PBXGroup; + children = ( + B23507B42118D22800F98D4F /* MSACTicketCache.h */, + B23507B32118D22800F98D4F /* MSACTicketCache.m */, + ); + path = Util; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + C9A91FFF230C05D70068070D /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + F8936CE0230C25A4006A330F /* MSACAbstractLog.h in Headers */, + F8936D81230C2805006A330F /* MSACServiceCommon.h in Headers */, + F8936CC3230C24D9006A330F /* MSACConstants.h in Headers */, + F8936DA2230C2805006A330F /* MSACHttpIngestion.h in Headers */, + F8936CBE230C24D9006A330F /* AppCenter.h in Headers */, + F8936D06230C2604006A330F /* MSACHttpClientPrivate.h in Headers */, + DF5DA1FA23A0E55500DE695C /* MSACDispatcherUtil.h in Headers */, + F8936DBB230C2805006A330F /* MSACBooleanTypedProperty.h in Headers */, + F8936D04230C2604006A330F /* MSACSessionContextPrivate.h in Headers */, + F8936D7C230C2805006A330F /* AppCenter+Internal.h in Headers */, + F8936CC0230C24D9006A330F /* MSACAppCenterErrors.h in Headers */, + F8936DBD230C2805006A330F /* MSACDoubleTypedProperty.h in Headers */, + F8936DA9230C2805006A330F /* MSACDeviceExtension.h in Headers */, + F8936D7F230C2805006A330F /* MSACLoggerInternal.h in Headers */, + F8936DA7230C2805006A330F /* MSACCSData.h in Headers */, + F8936DA3230C2805006A330F /* MSACAppCenterIngestion.h in Headers */, + F8936D01230C2604006A330F /* MSACOneCollectorChannelDelegatePrivate.h in Headers */, + F8936DB5230C2805006A330F /* MSACModel.h in Headers */, + F8936DCE230C2805006A330F /* MSACUtility+Environment.h in Headers */, + F8936D88230C2805006A330F /* MSACChannelGroupDefault.h in Headers */, + F8936DA6230C2805006A330F /* MSACCommonSchemaLog.h in Headers */, + F8936D9D230C2805006A330F /* MSACTicketCache.h in Headers */, + F8936DC0230C2805006A330F /* MSACTypedProperty.h in Headers */, + F8936DC3230C2805006A330F /* MSACAppCenterUserDefaults.h in Headers */, + F8936D8E230C2805006A330F /* MSACCSEpochAndSeq.h in Headers */, + F8936DC7230C2805006A330F /* MSACConstants+Internal.h in Headers */, + F8936DB1230C2805006A330F /* MSACAbstractLogInternal.h in Headers */, + F8936D0C230C2604006A330F /* MSACOrderedDictionaryPrivate.h in Headers */, + F8936CC4230C24D9006A330F /* MSACConstants+Flags.h in Headers */, + F8936D0D230C2604006A330F /* MSACEncrypterPrivate.h in Headers */, + F8936D95230C2805006A330F /* MSACSessionContext.h in Headers */, + F8936DC9230C2805006A330F /* MSACHistoryInfo.h in Headers */, + F8936D85230C2805006A330F /* MSACCustomApplicationDelegate.h in Headers */, + F8936D98230C2805006A330F /* MSACUserIdHistoryInfo.h in Headers */, + DFE9554F244D965A0061E3FA /* HTTPStubsPathHelpers.h in Headers */, + DFE9554D244D965A0061E3FA /* Compatibility.h in Headers */, + F8936DD0230C2805006A330F /* MSACUtility+PropertyValidation.h in Headers */, + F8936D8C230C2805006A330F /* MSACOneCollectorChannelDelegate.h in Headers */, + F8936DBE230C2805006A330F /* MSACLongTypedProperty.h in Headers */, + F8936DCF230C2805006A330F /* MSACUtility+File.h in Headers */, + F8936CC9230C24DA006A330F /* MSACWrapperLogger.h in Headers */, + F8936D0B230C2604006A330F /* MSACLogDBStoragePrivate.h in Headers */, + F8936D94230C2805006A330F /* MSACDeviceTracker.h in Headers */, + F8936D83230C2805006A330F /* MSACAppDelegateUtil.h in Headers */, + F8936DD2230C2805006A330F /* MSACCompression.h in Headers */, + F8936CFE230C2604006A330F /* MSACDelegateForwarderPrivate.h in Headers */, + D55E7088252F5A1000AB994D /* MSACTestSessionInfo.h in Headers */, + F8936CC1230C24D9006A330F /* MSACChannelGroupProtocol.h in Headers */, + F8936DAC230C2805006A330F /* MSACNetExtension.h in Headers */, + F8936D05230C2604006A330F /* MSACUserIdContextPrivate.h in Headers */, + F8936D96230C2805006A330F /* MSACSessionHistoryInfo.h in Headers */, + F8936DB0230C2805006A330F /* MSACUserExtension.h in Headers */, + F8936DAD230C2805006A330F /* MSACOSExtension.h in Headers */, + DFE9554E244D965A0061E3FA /* HTTPStubsResponse+JSON.h in Headers */, + F8936DB9230C2805006A330F /* MSACWrapperSdkInternal.h in Headers */, + F8936D9E230C2805006A330F /* MSACIngestionProtocol.h in Headers */, + F8936D03230C2604006A330F /* MSACDeviceTrackerPrivate.h in Headers */, + F8936DC5230C2805006A330F /* MSACLogDBStorageVersion.h in Headers */, + F8936DB7230C2805006A330F /* MSACSerializableObject.h in Headers */, + F8936CDF230C25A4006A330F /* MSACLogWithProperties.h in Headers */, + F8936D93230C2805006A330F /* MSACDeviceHistoryInfo.h in Headers */, + F8936DAA230C2805006A330F /* MSACLocExtension.h in Headers */, + F8936CBF230C24D9006A330F /* MSACAppCenter.h in Headers */, + F8936DB4230C2805006A330F /* MSACLogContainer.h in Headers */, + F8936CFC230C2604006A330F /* MSACAppCenterPrivate.h in Headers */, + F8936DD3230C2805006A330F /* MSAC_Reachability.h in Headers */, + F8936DB2230C2805006A330F /* MSACCustomPropertiesLog.h in Headers */, + F8936D82230C2805006A330F /* MSACServiceInternal.h in Headers */, + F8DC50DE23AA828E00BF8839 /* MSACStorageNumberType.h in Headers */, + F8936CC8230C24DA006A330F /* MSACServiceAbstract.h in Headers */, + F8936CFF230C2604006A330F /* MSACChannelGroupDefaultPrivate.h in Headers */, + F8936D08230C2604006A330F /* MSACOneCollectorIngestionPrivate.h in Headers */, + F8936DCA230C2805006A330F /* MSACKeychainUtil.h in Headers */, + F8936D97230C2805006A330F /* MSACUserIdContext.h in Headers */, + F8936DC8230C2805006A330F /* MSACEncrypter.h in Headers */, + F8936D9B230C2805006A330F /* MSACHttpClient.h in Headers */, + F8936DA4230C2805006A330F /* MSACOneCollectorIngestion.h in Headers */, + F8936CDC230C25A4006A330F /* MSACCustomProperties.h in Headers */, + 244CC1F92399CA5A00A58F51 /* MSACDependencyConfiguration.h in Headers */, + F8936DCB230C2805006A330F /* MSACUtility.h in Headers */, + F8936D87230C2805006A330F /* MSACCustomDelegate.h in Headers */, + F8936D7E230C2805006A330F /* MSACCustomPropertiesInternal.h in Headers */, + F8936DCC230C2805006A330F /* MSACUtility+Application.h in Headers */, + DFE95551244D965A0061E3FA /* NSURLRequest+HTTPBodyTesting.h in Headers */, + DFE95545244D96540061E3FA /* HTTPStubsMethodSwizzling.h in Headers */, + F8936D8A230C2805006A330F /* MSACChannelUnitDefault.h in Headers */, + F8936D84230C2805006A330F /* MSACAppDelegateForwarder.h in Headers */, + F8936CFD230C2604006A330F /* MSACCustomPropertiesPrivate.h in Headers */, + F8936DBF230C2805006A330F /* MSACStringTypedProperty.h in Headers */, + F8936D7D230C2805006A330F /* MSACAppCenterInternal.h in Headers */, + F8936DC1230C2805006A330F /* MSACStorage.h in Headers */, + F8936D99230C2805006A330F /* MSACHttpUtil.h in Headers */, + F8936DBC230C2805006A330F /* MSACDateTimeTypedProperty.h in Headers */, + F8936D0A230C2604006A330F /* MSACDBStoragePrivate.h in Headers */, + F8936D80230C2805006A330F /* MSACServiceAbstractInternal.h in Headers */, + F8936DA8230C2805006A330F /* MSACCSExtensions.h in Headers */, + F8936D8D230C2805006A330F /* MSACChannelUnitProtocol.h in Headers */, + F8936CDE230C25A4006A330F /* MSACLog.h in Headers */, + F8936DCD230C2805006A330F /* MSACUtility+Date.h in Headers */, + F8936DAF230C2805006A330F /* MSACSDKExtension.h in Headers */, + F8936DC2230C2805006A330F /* MSACDBStorage.h in Headers */, + DFE95550244D965A0061E3FA /* HTTPStubsResponse.h in Headers */, + F8936DB3230C2805006A330F /* MSACDeviceInternal.h in Headers */, + F8936D00230C2604006A330F /* MSACChannelUnitDefaultPrivate.h in Headers */, + F8936DD1230C2805006A330F /* MSACUtility+StringFormatting.h in Headers */, + F8DC50DF23AA828E00BF8839 /* MSACStorageTextType.h in Headers */, + F8936DC6230C2805006A330F /* MSACOrderedDictionary.h in Headers */, + F8936DAB230C2805006A330F /* MSACMetadataExtension.h in Headers */, + F8936CC2230C24D9006A330F /* MSACChannelProtocol.h in Headers */, + F8936D9C230C2805006A330F /* MSACHttpCall.h in Headers */, + F8936CDD230C25A4006A330F /* MSACDevice.h in Headers */, + DFE95552244D965A0061E3FA /* HTTPStubs.h in Headers */, + F8BA7A2A23AA8A26009FBCCF /* MSACStorageBindableArray.h in Headers */, + F8936D8B230C2805006A330F /* MSACChannelDelegate.h in Headers */, + F8DC50DD23AA828E00BF8839 /* MSACStorageBindableType.h in Headers */, + F8936DA5230C2805006A330F /* MSACAppExtension.h in Headers */, + F8936DAE230C2805006A330F /* MSACProtocolExtension.h in Headers */, + F8936D07230C2604006A330F /* MSACHttpIngestionPrivate.h in Headers */, + F8936DC4230C2805006A330F /* MSACLogDBStorage.h in Headers */, + F8936D89230C2805006A330F /* MSACChannelUnitConfiguration.h in Headers */, + F8936CC6230C24D9006A330F /* MSACLogger.h in Headers */, + F8936DBA230C2805006A330F /* MSACLogConversion.h in Headers */, + F8936D86230C2805006A330F /* MSACDelegateForwarder.h in Headers */, + 04237DC223A31617009BB406 /* MSACHttpClientDelegate.h in Headers */, + F8936D0F230C2604006A330F /* MSACUtility+ApplicationPrivate.h in Headers */, + F8936CC7230C24DA006A330F /* MSACService.h in Headers */, + F8936DB6230C2805006A330F /* MSACNoAutoAssignSessionIdLog.h in Headers */, + F8936DB8230C2805006A330F /* MSACStartServiceLog.h in Headers */, + F8936D09230C2604006A330F /* MSACAbstractLogPrivate.h in Headers */, + F8936CC5230C24D9006A330F /* MSACEnable.h in Headers */, + F8936D9A230C2805006A330F /* MSACHttpClientProtocol.h in Headers */, + F8936CE1230C25A4006A330F /* MSACWrapperSdk.h in Headers */, + F8936D0E230C2604006A330F /* MSACKeychainUtilPrivate.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + C9A91FFE230C05D70068070D /* AppCenter macOS Framework */ = { + isa = PBXNativeTarget; + buildConfigurationList = C9A92004230C05D70068070D /* Build configuration list for PBXNativeTarget "AppCenter macOS Framework" */; + buildPhases = ( + C2CB276B230EDEC30076A4E0 /* Verify No Build Settings */, + C9A91FFF230C05D70068070D /* Headers */, + C9A92000230C05D70068070D /* Sources */, + C9A92001230C05D70068070D /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "AppCenter macOS Framework"; + productName = AppCenterStatic; + productReference = C9A92007230C05D70068070D /* AppCenter.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 6E0400FB1D1C98220051BCFA /* Project object */ = { + isa = PBXProject; + attributes = { + CLASSPREFIX = MS; + LastUpgradeCheck = 1220; + ORGANIZATIONNAME = Microsoft; + TargetAttributes = { + C229D8AB2546E7E5001909F3 = { + CreatedOnToolsVersion = 12.2; + ProvisioningStyle = Automatic; + }; + C9A91FFE230C05D70068070D = { + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 6E0400FE1D1C98220051BCFA /* Build configuration list for PBXProject "AppCenter" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 6E0400FA1D1C98220051BCFA; + productRefGroup = 6E0401041D1C98220051BCFA /* Products */; + projectDirPath = ""; + projectReferences = ( + { + ProductGroup = DFCB7FFE2472C1BD0058D292 /* Products */; + ProjectRef = DFCB7FFD2472C1BD0058D292 /* OCHamcrest.xcodeproj */; + }, + { + ProductGroup = C2392EDB2464279400425640 /* Products */; + ProjectRef = C2392EDA2464279400425640 /* OCMock.xcodeproj */; + }, + ); + projectRoot = ""; + targets = ( + C229D8AB2546E7E5001909F3 /* AppCenter */, + C9A91FFE230C05D70068070D /* AppCenter macOS Framework */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXReferenceProxy section */ + C2392EE52464279400425640 /* OCMock.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = OCMock.framework; + remoteRef = C2392EE42464279400425640 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C2392EE72464279400425640 /* OCMockTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = OCMockTests.xctest; + remoteRef = C2392EE62464279400425640 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C2392EE92464279400425640 /* libOCMock.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libOCMock.a; + remoteRef = C2392EE82464279400425640 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C2392EEB2464279400425640 /* OCMockLibTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = OCMockLibTests.xctest; + remoteRef = C2392EEA2464279400425640 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C2392EED2464279400425640 /* OCMock.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = OCMock.framework; + remoteRef = C2392EEC2464279400425640 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C2392EEF2464279400425640 /* OCMock.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = OCMock.framework; + remoteRef = C2392EEE2464279400425640 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C2392EF12464279400425640 /* OCMock.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = OCMock.framework; + remoteRef = C2392EF02464279400425640 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + DFCB80082472C1BD0058D292 /* OCHamcrest.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = OCHamcrest.framework; + remoteRef = DFCB80072472C1BD0058D292 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + DFCB800A2472C1BD0058D292 /* OCHamcrestTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = OCHamcrestTests.xctest; + remoteRef = DFCB80092472C1BD0058D292 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + DFCB800C2472C1BD0058D292 /* libochamcrest.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libochamcrest.a; + remoteRef = DFCB800B2472C1BD0058D292 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + DFCB800E2472C1BD0058D292 /* libochamcrestTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = libochamcrestTests.xctest; + remoteRef = DFCB800D2472C1BD0058D292 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + DFCB80102472C1BD0058D292 /* OCHamcrest.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = OCHamcrest.framework; + remoteRef = DFCB800F2472C1BD0058D292 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + DFCB80122472C1BD0058D292 /* OCHamcrest.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = OCHamcrest.framework; + remoteRef = DFCB80112472C1BD0058D292 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + DFCB80142472C1BD0058D292 /* OCHamcrest.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = OCHamcrest.framework; + remoteRef = DFCB80132472C1BD0058D292 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + +/* Begin PBXShellScriptBuildPhase section */ + C26F4E7C2546F406006E0EB7 /* Copy Products */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Copy Products"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$SRCROOT/../Scripts/copy-products.sh\"\n"; + showEnvVarsInLog = 0; + }; + C2CB276B230EDEC30076A4E0 /* Verify No Build Settings */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Verify No Build Settings"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#\"${SRCROOT}/../Scripts/VerifyNoBuildSettings.swift\" ${PROJECT_NAME}.xcodeproj/project.pbxproj\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + C9A92000230C05D70068070D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C9A92103230C61820068070D /* MSACCommonSchemaLog.m in Sources */, + C9A920FC230C61820068070D /* MSACHttpCall.m in Sources */, + C9A92118230C61820068070D /* MSACTypedProperty.m in Sources */, + C9A92101230C61820068070D /* MSACOneCollectorIngestion.m in Sources */, + D55E7085252F5A1000AB994D /* MSACTestSessionInfo.m in Sources */, + C9A92126230C61820068070D /* MSACUtility+StringFormatting.m in Sources */, + C9A92114230C61820068070D /* MSACDateTimeTypedProperty.m in Sources */, + C9A920ED230C61820068070D /* MSACChannelUnitConfiguration.m in Sources */, + C9A9211E230C61820068070D /* MSACHistoryInfo.m in Sources */, + F8DC50E223AA828E00BF8839 /* MSACStorageNumberType.m in Sources */, + C9A920EA230C61820068070D /* MSACAppDelegateForwarder.m in Sources */, + C9A92123230C61820068070D /* MSACUtility+Environment.m in Sources */, + C9A920EE230C61820068070D /* MSACChannelUnitDefault.m in Sources */, + C9A920E8230C61820068070D /* MSACServiceAbstract.m in Sources */, + C9A92109230C61820068070D /* MSACNetExtension.m in Sources */, + C9A9211C230C61820068070D /* MSACOrderedDictionary.m in Sources */, + C9A92108230C61820068070D /* MSACMetadataExtension.m in Sources */, + C9A92112230C61820068070D /* MSACStartServiceLog.m in Sources */, + C9A920FA230C61820068070D /* MSACHttpUtil.m in Sources */, + C9A92113230C61820068070D /* MSACBooleanTypedProperty.m in Sources */, + 244CC1FC2399CA5A00A58F51 /* MSACDependencyConfiguration.m in Sources */, + C9A920E9230C61820068070D /* MSACWrapperLogger.m in Sources */, + C9A920EF230C61820068070D /* MSACOneCollectorChannelDelegate.m in Sources */, + C9A920FB230C61820068070D /* MSACHttpClient.m in Sources */, + C9A920FD230C61820068070D /* MSACTicketCache.m in Sources */, + C9A920F7230C61820068070D /* MSACSessionHistoryInfo.m in Sources */, + F8BA7A2E23AA8B84009FBCCF /* MSACStorageBindableArray.m in Sources */, + C9A920EB230C61820068070D /* MSACDelegateForwarder.m in Sources */, + C9A9210B230C61820068070D /* MSACProtocolExtension.m in Sources */, + C9A920F5230C61820068070D /* MSACDeviceTracker.m in Sources */, + C9A9211F230C61820068070D /* MSACKeychainUtil.m in Sources */, + C9A920EC230C61820068070D /* MSACChannelGroupDefault.m in Sources */, + C9A9210F230C61820068070D /* MSACCustomPropertiesLog.m in Sources */, + C9A92105230C61820068070D /* MSACCSExtensions.m in Sources */, + C9A92129230C61820068070D /* MSACCustomProperties.m in Sources */, + C9A92121230C61820068070D /* MSACUtility+Application.m in Sources */, + C9A92117230C61820068070D /* MSACStringTypedProperty.m in Sources */, + C9A92111230C61820068070D /* MSACLogWithProperties.m in Sources */, + C9A920F0230C61820068070D /* MSACCSEpochAndSeq.m in Sources */, + C9A92128230C61820068070D /* MSAC_Reachability.m in Sources */, + C9A92102230C61820068070D /* MSACAppExtension.m in Sources */, + C9A92122230C61820068070D /* MSACUtility+Date.m in Sources */, + C9A92119230C61820068070D /* MSACDBStorage.m in Sources */, + C9A92110230C61820068070D /* MSACLogContainer.m in Sources */, + C9A92124230C61820068070D /* MSACUtility+File.m in Sources */, + C9A92125230C61820068070D /* MSACUtility+PropertyValidation.m in Sources */, + DF5DA1FE23A0E57B00DE695C /* MSACDispatcherUtil.m in Sources */, + C9A92127230C61820068070D /* MSACCompression.m in Sources */, + C9A9210D230C61820068070D /* MSACUserExtension.m in Sources */, + C9A9210E230C61820068070D /* MSACAbstractLog.m in Sources */, + C9A920F6230C61820068070D /* MSACSessionContext.m in Sources */, + C9A92106230C61820068070D /* MSACDeviceExtension.m in Sources */, + C9A920F4230C61820068070D /* MSACDeviceHistoryInfo.m in Sources */, + C9A920E6230C61820068070D /* MSACAppCenter.m in Sources */, + C9A9211A230C61820068070D /* MSACAppCenterUserDefaults.m in Sources */, + C9A9210C230C61820068070D /* MSACSDKExtension.m in Sources */, + C9A920E7230C61820068070D /* MSACLogger.m in Sources */, + C9A9211D230C61820068070D /* MSACEncrypter.m in Sources */, + C9A9212B230C61820068070D /* MSACWrapperSdk.m in Sources */, + C9A9211B230C61820068070D /* MSACLogDBStorage.m in Sources */, + C9A92115230C61820068070D /* MSACDoubleTypedProperty.m in Sources */, + C9A92116230C61820068070D /* MSACLongTypedProperty.m in Sources */, + C9A920F9230C61820068070D /* MSACUserIdHistoryInfo.m in Sources */, + C9A92120230C61820068070D /* MSACUtility.m in Sources */, + F8DC50E123AA828E00BF8839 /* MSACStorageTextType.m in Sources */, + C9A920FF230C61820068070D /* MSACHttpIngestion.m in Sources */, + C9A92100230C61820068070D /* MSACAppCenterIngestion.m in Sources */, + C9A920F8230C61820068070D /* MSACUserIdContext.m in Sources */, + C9A92107230C61820068070D /* MSACLocExtension.m in Sources */, + C9A92104230C61820068070D /* MSACCSData.m in Sources */, + C9A9210A230C61820068070D /* MSACOSExtension.m in Sources */, + C9A9212A230C61820068070D /* MSACDevice.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 6E04010A1D1C98220051BCFA /* DebugAppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6E0401841D1CAD810051BCFA /* AppCenter Debug.xcconfig */; + buildSettings = { + }; + name = DebugAppStore; + }; + 6E04010B1D1C98220051BCFA /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0469D1B11F4DF89A00A43A8E /* AppCenter Release.xcconfig */; + buildSettings = { + }; + name = ReleaseAppStore; + }; + C229D8AD2546E7E5001909F3 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + }; + name = DebugAppStore; + }; + C229D8AE2546E7E5001909F3 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + }; + name = ReleaseAppStore; + }; + C9A92005230C05D70068070D /* DebugAppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 049BC8291ECE3A5A00FB6719 /* macOS.xcconfig */; + buildSettings = { + DEVELOPMENT_TEAM = ""; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = YES; + }; + name = DebugAppStore; + }; + C9A92006230C05D70068070D /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 049BC8291ECE3A5A00FB6719 /* macOS.xcconfig */; + buildSettings = { + DEVELOPMENT_TEAM = ""; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + }; + name = ReleaseAppStore; + }; + D0076ED7256859DC007EF588 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0469D1B11F4DF89A00A43A8E /* AppCenter Release.xcconfig */; + buildSettings = { + }; + name = ReleaseHockeyapp; + }; + D0076ED8256859DC007EF588 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + }; + name = ReleaseHockeyapp; + }; + D0076ED9256859DC007EF588 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 049BC8291ECE3A5A00FB6719 /* macOS.xcconfig */; + buildSettings = { + DEVELOPMENT_TEAM = ""; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + }; + name = ReleaseHockeyapp; + }; + D0076EDA256859E2007EF588 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0469D1B11F4DF89A00A43A8E /* AppCenter Release.xcconfig */; + buildSettings = { + }; + name = DebugHockeyapp; + }; + D0076EDB256859E2007EF588 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + }; + name = DebugHockeyapp; + }; + D0076EDC256859E2007EF588 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 049BC8291ECE3A5A00FB6719 /* macOS.xcconfig */; + buildSettings = { + DEVELOPMENT_TEAM = ""; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + }; + name = DebugHockeyapp; + }; + D0076EDD256859E7007EF588 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0469D1B11F4DF89A00A43A8E /* AppCenter Release.xcconfig */; + buildSettings = { + }; + name = HockeyappMacAlpha; + }; + D0076EDE256859E7007EF588 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + }; + name = HockeyappMacAlpha; + }; + D0076EDF256859E7007EF588 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 049BC8291ECE3A5A00FB6719 /* macOS.xcconfig */; + buildSettings = { + DEVELOPMENT_TEAM = ""; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + }; + name = HockeyappMacAlpha; + }; + D0076EE0256859ED007EF588 /* Github */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6E0401841D1CAD810051BCFA /* AppCenter Debug.xcconfig */; + buildSettings = { + }; + name = Github; + }; + D0076EE1256859ED007EF588 /* Github */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + }; + name = Github; + }; + D0076EE2256859ED007EF588 /* Github */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 049BC8291ECE3A5A00FB6719 /* macOS.xcconfig */; + buildSettings = { + DEVELOPMENT_TEAM = ""; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + }; + name = Github; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 6E0400FE1D1C98220051BCFA /* Build configuration list for PBXProject "AppCenter" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6E04010A1D1C98220051BCFA /* DebugAppStore */, + D0076EE0256859ED007EF588 /* Github */, + 6E04010B1D1C98220051BCFA /* ReleaseAppStore */, + D0076ED7256859DC007EF588 /* ReleaseHockeyapp */, + D0076EDA256859E2007EF588 /* DebugHockeyapp */, + D0076EDD256859E7007EF588 /* HockeyappMacAlpha */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; + C229D8AC2546E7E5001909F3 /* Build configuration list for PBXAggregateTarget "AppCenter" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C229D8AD2546E7E5001909F3 /* DebugAppStore */, + D0076EE1256859ED007EF588 /* Github */, + C229D8AE2546E7E5001909F3 /* ReleaseAppStore */, + D0076ED8256859DC007EF588 /* ReleaseHockeyapp */, + D0076EDB256859E2007EF588 /* DebugHockeyapp */, + D0076EDE256859E7007EF588 /* HockeyappMacAlpha */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; + C9A92004230C05D70068070D /* Build configuration list for PBXNativeTarget "AppCenter macOS Framework" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C9A92005230C05D70068070D /* DebugAppStore */, + D0076EE2256859ED007EF588 /* Github */, + C9A92006230C05D70068070D /* ReleaseAppStore */, + D0076ED9256859DC007EF588 /* ReleaseHockeyapp */, + D0076EDC256859E2007EF588 /* DebugHockeyapp */, + D0076EDF256859E7007EF588 /* HockeyappMacAlpha */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; +/* End XCConfigurationList section */ + }; + rootObject = 6E0400FB1D1C98220051BCFA /* Project object */; +} diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/AppCenter.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/AppCenter.h new file mode 100644 index 0000000000..c156eec452 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/AppCenter.h @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAbstractLog.h" +#import "MSACAppCenter.h" +#import "MSACAppCenterErrors.h" +#import "MSACChannelGroupProtocol.h" +#import "MSACChannelProtocol.h" +#import "MSACConstants.h" +#import "MSACCustomProperties.h" +#import "MSACDevice.h" +#import "MSACEnable.h" +#import "MSACLog.h" +#import "MSACLogWithProperties.h" +#import "MSACLogger.h" +#import "MSACService.h" +#import "MSACServiceAbstract.h" +#import "MSACWrapperLogger.h" +#import "MSACWrapperSdk.h" diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/AppCenter+Internal.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/AppCenter+Internal.h new file mode 100644 index 0000000000..f10cca7b12 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/AppCenter+Internal.h @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACDevice.h" +#import "MSACLogger.h" +#import "MSACServiceAbstractInternal.h" +#import "MSACServiceInternal.h" +#import "MSACUtility+Application.h" +#import "MSACUtility+Date.h" +#import "MSACUtility+Environment.h" +#import "MSACUtility+PropertyValidation.h" +#import "MSACWrapperSdk.h" + +// Channel +#import "Channel/MSACChannelDelegate.h" + +// Model +#import "MSACLog.h" +#import "Model/MSACAbstractLogInternal.h" +#import "Model/MSACLogContainer.h" +#import "Model/Util/MSACAppCenterUserDefaults.h" diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACCSEpochAndSeq.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACCSEpochAndSeq.h new file mode 100644 index 0000000000..a179a2cdd5 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACCSEpochAndSeq.h @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +@interface MSACCSEpochAndSeq : NSObject + +@property(nonatomic) NSUInteger seq; +@property(nonatomic) NSString *epoch; + +/** + * Create a MSACCSEpochAndSeq with the given epoch. + * + * @param epoch The random unique UUID. + * + * @return A MSACCSEpochAndSeq with the given epoch and default seq to 0. + */ +- (instancetype)initWithEpoch:(NSString *)epoch; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACCSEpochAndSeq.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACCSEpochAndSeq.m new file mode 100644 index 0000000000..fb60a904df --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACCSEpochAndSeq.m @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACCSEpochAndSeq.h" + +@implementation MSACCSEpochAndSeq + +- (instancetype)initWithEpoch:(NSString *)epoch { + if ((self = [super init])) { + _epoch = epoch; + _seq = 0; + } + return self; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelDelegate.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelDelegate.h new file mode 100644 index 0000000000..59fb0bfcff --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelDelegate.h @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACConstants+Flags.h" + +@protocol MSACChannelUnitProtocol; +@protocol MSACChannelGroupProtocol; +@protocol MSACChannelProtocol; +@protocol MSACLog; + +@protocol MSACChannelDelegate + +@optional + +/** + * A callback that is called when a channel unit is added to the channel group. + * + * @param channelGroup The channel group. + * @param channel The newly added channel. + */ +- (void)channelGroup:(id)channelGroup didAddChannelUnit:(id)channel; + +/** + * A callback that is called when a log is just enqueued. Delegates may want to prepare the log a little more before further processing. + * + * @param log The log to prepare. + */ +- (void)channel:(id)channel prepareLog:(id)log; + +/** + * A callback that is called after a log is definitely prepared. + * + * @param log The log. + * @param internalId An internal Id to keep track of logs. + * @param flags Options for the log. + */ +- (void)channel:(id)channel didPrepareLog:(id)log internalId:(NSString *)internalId flags:(MSACFlags)flags; + +/** + * A callback that is called after a log completed the enqueueing process whether it was successful or not. + * + * @param log The log. + * @param internalId An internal Id to keep track of logs. + */ +- (void)channel:(id)channel didCompleteEnqueueingLog:(id)log internalId:(NSString *)internalId; + +/** + * Callback method that will be called before each log will be send to the server. + * + * @param channel The channel object. + * @param log The log to be sent. + */ +- (void)channel:(id)channel willSendLog:(id)log; + +/** + * Callback method that will be called in case the SDK was able to send a log. + * + * @param channel The channel object. + * @param log The log to be sent. + */ +- (void)channel:(id)channel didSucceedSendingLog:(id)log; + +/** + * Callback method that will be called in case the SDK was unable to send a log. + * + * @param channel The channel object. + * @param log The log to be sent. + * @param error The error that occured. + */ +- (void)channel:(id)channel didFailSendingLog:(id)log withError:(NSError *)error; + +/** + * A callback that is called when setEnabled has been invoked. + * + * @param channel The channel. + * @param isEnabled The boolean that indicates enabled. + * @param deletedData The boolean that indicates deleting data on disabled. + */ +- (void)channel:(id)channel didSetEnabled:(BOOL)isEnabled andDeleteDataOnDisabled:(BOOL)deletedData; + +/** + * A callback that is called when pause has been invoked. + * + * @param channel The channel. + * @param identifyingObject The identifying object used to pause the channel. + */ +- (void)channel:(id)channel didPauseWithIdentifyingObject:(id)identifyingObject; + +/** + * A callback that is called when resume has been invoked. + * + * @param channel The channel. + * @param identifyingObject The identifying object used to resume the channel. + */ +- (void)channel:(id)channel didResumeWithIdentifyingObject:(id)identifyingObject; + +/** + * Callback method that will determine if a log should be filtered out from the usual processing pipeline. If any delegate returns true, the + * log is filtered. + * + * @param channelUnit The channel unit that is going to send the log. + * @param log The log to be filtered or not. + * + * @return `true` if the log should be filtered out. + */ +- (BOOL)channelUnit:(id)channelUnit shouldFilterLog:(id)log; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelGroupDefault.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelGroupDefault.h new file mode 100644 index 0000000000..174abcf097 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelGroupDefault.h @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACChannelGroupProtocol.h" +#import "MSACDeviceTracker.h" + +NS_ASSUME_NONNULL_BEGIN + +@class MSACAppCenterIngestion; + +@protocol MSACHttpClientProtocol; +@protocol MSACStorage; + +/** + * A channel group which triggers and manages the processing of log items on different channels. All items will be immediately passed to the + * persistence layer in order to make the queue crash safe. Once a maximum number of items have been enqueued or the internal timer finished + * running, events will be forwarded to the ingestion. Furthermore, its responsibility is to tell the persistence layer what to do with a + * pending batch based on the status code returned by the ingestion + */ +@interface MSACChannelGroupDefault : NSObject + +/** + * Initializes a new `MSACChannelGroupDefault` instance. + * + * @param httpClient The HTTP client. + * @param installId A unique installation identifier. + * @param logUrl A base URL to use for backend communication. + * + * @return A new `MSACChannelGroupDefault` instance. + */ +- (instancetype)initWithHttpClient:(id)httpClient installId:(NSUUID *)installId logUrl:(NSString *)logUrl; + +/** + * Collection of channel delegates. + */ +@property(nonatomic) NSHashTable> *delegates; + +/** + * An ingestion instance that is used to send batches of log items to the backend. + */ +@property(nonatomic, strong, nullable) MSACAppCenterIngestion *ingestion; + +/** + * A storage instance to store and read enqueued log items. + */ +@property(nonatomic, strong) id storage; + +/** + * A queue which makes adding new items thread safe. + */ +@property(nonatomic, strong) dispatch_queue_t logsDispatchQueue; + +/** + * An array containing all channels that are a part of this channel group. + */ +@property(nonatomic, strong) NSMutableArray *channels; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelGroupDefault.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelGroupDefault.m new file mode 100644 index 0000000000..d2946602f1 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelGroupDefault.m @@ -0,0 +1,273 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACChannelGroupDefault.h" +#import "AppCenter+Internal.h" +#import "MSACAppCenterIngestion.h" +#import "MSACChannelGroupDefaultPrivate.h" +#import "MSACChannelUnitConfiguration.h" +#import "MSACChannelUnitDefault.h" +#import "MSACDispatcherUtil.h" +#import "MSACLogDBStorage.h" + +static char *const kMSACLogsDispatchQueue = "com.microsoft.appcenter.ChannelGroupQueue"; + +@implementation MSACChannelGroupDefault + +#pragma mark - Initialization + +- (instancetype)initWithHttpClient:(id)httpClient installId:(NSUUID *)installId logUrl:(NSString *)logUrl { + self = [self initWithIngestion:[[MSACAppCenterIngestion alloc] initWithHttpClient:httpClient + baseUrl:logUrl + installId:[installId UUIDString]]]; + return self; +} + +- (instancetype)initWithIngestion:(nullable MSACAppCenterIngestion *)ingestion { + if ((self = [self init])) { + dispatch_queue_t serialQueue = dispatch_queue_create(kMSACLogsDispatchQueue, DISPATCH_QUEUE_SERIAL); + _logsDispatchQueue = serialQueue; + _channels = [NSMutableArray> new]; + _delegates = [NSHashTable weakObjectsHashTable]; + _storage = [MSACLogDBStorage new]; + if (ingestion) { + _ingestion = ingestion; + } + } + return self; +} + +- (id)addChannelUnitWithConfiguration:(MSACChannelUnitConfiguration *)configuration { + return [self addChannelUnitWithConfiguration:configuration withIngestion:self.ingestion]; +} + +- (id)addChannelUnitWithConfiguration:(MSACChannelUnitConfiguration *)configuration + withIngestion:(nullable id)ingestion { + MSACChannelUnitDefault *channel; + if (configuration) { + channel = [[MSACChannelUnitDefault alloc] initWithIngestion:(ingestion ? ingestion : self.ingestion) + storage:self.storage + configuration:configuration + logsDispatchQueue:self.logsDispatchQueue]; + [channel addDelegate:self]; + dispatch_async(self.logsDispatchQueue, ^{ + // Schedule sending any pending log. + [channel checkPendingLogs]; + }); + [self.channels addObject:channel]; + [self enumerateDelegatesForSelector:@selector(channelGroup:didAddChannelUnit:) + withBlock:^(id channelDelegate) { + [channelDelegate channelGroup:self didAddChannelUnit:channel]; + }]; + } + return channel; +} + +- (id)channelUnitForGroupId:(NSString *)groupId { + for (MSACChannelUnitDefault *channel in self.channels) { + if ([channel.configuration.groupId isEqualToString:groupId]) { + return channel; + } + } + return nil; +} + +#pragma mark - Delegate + +- (void)addDelegate:(id)delegate { + @synchronized(self) { + [self.delegates addObject:delegate]; + } +} + +- (void)removeDelegate:(id)delegate { + @synchronized(self) { + [self.delegates removeObject:delegate]; + } +} + +- (void)enumerateDelegatesForSelector:(SEL)selector withBlock:(void (^)(id delegate))block { + NSArray *synchronizedDelegates; + @synchronized(self) { + + // Don't execute the block while locking; it might be locking too and deadlock ourselves. + synchronizedDelegates = [self.delegates allObjects]; + } + for (id delegate in synchronizedDelegates) { + if ([delegate respondsToSelector:selector]) { + block(delegate); + } + } +} + +#pragma mark - Channel Delegate + +- (void)channel:(id)channel prepareLog:(id)log { + [self enumerateDelegatesForSelector:@selector(channel:prepareLog:) + withBlock:^(id delegate) { + [delegate channel:channel prepareLog:log]; + }]; +} + +- (void)channel:(id)channel didPrepareLog:(id)log internalId:(NSString *)internalId flags:(MSACFlags)flags { + [self enumerateDelegatesForSelector:@selector(channel:didPrepareLog:internalId:flags:) + withBlock:^(id delegate) { + [delegate channel:channel didPrepareLog:log internalId:internalId flags:flags]; + }]; +} + +- (void)channel:(id)channel didCompleteEnqueueingLog:(id)log internalId:(NSString *)internalId { + [self enumerateDelegatesForSelector:@selector(channel:didCompleteEnqueueingLog:internalId:) + withBlock:^(id delegate) { + [delegate channel:channel didCompleteEnqueueingLog:log internalId:internalId]; + }]; +} + +- (void)channel:(id)channel willSendLog:(id)log { + [self enumerateDelegatesForSelector:@selector(channel:willSendLog:) + withBlock:^(id delegate) { + [delegate channel:channel willSendLog:log]; + }]; +} + +- (void)channel:(id)channel didSucceedSendingLog:(id)log { + [self enumerateDelegatesForSelector:@selector(channel:didSucceedSendingLog:) + withBlock:^(id delegate) { + [delegate channel:channel didSucceedSendingLog:log]; + }]; +} + +- (void)channel:(id)channel didSetEnabled:(BOOL)isEnabled andDeleteDataOnDisabled:(BOOL)deletedData { + [self enumerateDelegatesForSelector:@selector(channel:didSetEnabled:andDeleteDataOnDisabled:) + withBlock:^(id delegate) { + [delegate channel:channel didSetEnabled:isEnabled andDeleteDataOnDisabled:deletedData]; + }]; +} + +- (void)channel:(id)channel didFailSendingLog:(id)log withError:(NSError *)error { + [self enumerateDelegatesForSelector:@selector(channel:didFailSendingLog:withError:) + withBlock:^(id delegate) { + [delegate channel:channel didFailSendingLog:log withError:error]; + }]; +} + +- (BOOL)channelUnit:(id)channelUnit shouldFilterLog:(id)log { + __block BOOL shouldFilter = NO; + [self enumerateDelegatesForSelector:@selector(channelUnit:shouldFilterLog:) + withBlock:^(id delegate) { + shouldFilter = shouldFilter || [delegate channelUnit:channelUnit shouldFilterLog:log]; + }]; + return shouldFilter; +} + +- (void)channel:(id)channel didPauseWithIdentifyingObject:(id)identifyingObject { + [self enumerateDelegatesForSelector:@selector(channel:didPauseWithIdentifyingObject:) + withBlock:^(id delegate) { + [delegate channel:channel didPauseWithIdentifyingObject:identifyingObject]; + }]; +} + +- (void)channel:(id)channel didResumeWithIdentifyingObject:(id)identifyingObject { + [self enumerateDelegatesForSelector:@selector(channel:didResumeWithIdentifyingObject:) + withBlock:^(id delegate) { + [delegate channel:channel didResumeWithIdentifyingObject:identifyingObject]; + }]; +} + +#pragma mark - Enable / Disable + +- (void)setEnabled:(BOOL)isEnabled andDeleteDataOnDisabled:(BOOL)deleteData { + +#if !TARGET_OS_OSX + if (isEnabled) { + [MSAC_NOTIFICATION_CENTER addObserver:self + selector:@selector(applicationWillTerminate:) + name:UIApplicationWillTerminateNotification + object:nil]; + } else { + [MSAC_NOTIFICATION_CENTER removeObserver:self]; + } +#endif + + // Propagate to ingestion. + [self.ingestion setEnabled:isEnabled andDeleteDataOnDisabled:deleteData]; + + // Propagate to initialized channels. + for (id channel in self.channels) { + [channel setEnabled:isEnabled andDeleteDataOnDisabled:deleteData]; + } + + // Notify delegates. + [self enumerateDelegatesForSelector:@selector(channel:didSetEnabled:andDeleteDataOnDisabled:) + withBlock:^(id delegate) { + [delegate channel:self didSetEnabled:isEnabled andDeleteDataOnDisabled:deleteData]; + }]; + + /** + * TODO: There should be some concept of logs on disk expiring to avoid leaks when a channel is disabled with lingering logs but never + * enabled again. + * + * Note that this is an unlikely scenario. Solving this issue is more of a proactive measure. + */ +} + +#if !TARGET_OS_OSX +- (void)applicationWillTerminate:(__unused UIApplication *)application { + + // Block logs queue so that it isn't killed before app termination. + [MSACDispatcherUtil dispatchSyncWithTimeout:1 + onQueue:self.logsDispatchQueue + withBlock:^{ + }]; +} +#endif + +#pragma mark - Pause / Resume + +- (void)pauseWithIdentifyingObject:(id)identifyingObject { + + // Disable ingestion, sending log will not be possible but they'll still be stored. + [self.ingestion setEnabled:NO andDeleteDataOnDisabled:NO]; + + // Pause each channel asynchronously. + for (id channel in self.channels) { + [channel pauseWithIdentifyingObject:identifyingObject]; + } +} + +- (void)resumeWithIdentifyingObject:(id)identifyingObject { + + // Resume ingestion, logs can be sent again. Pending logs are sent. + [self.ingestion setEnabled:YES andDeleteDataOnDisabled:NO]; + + // Resume each channel asynchronously. + for (id channel in self.channels) { + [channel resumeWithIdentifyingObject:identifyingObject]; + } +} + +#pragma mark - Other public methods + +- (void)setLogUrl:(NSString *)logUrl { + self.ingestion.baseURL = logUrl; +} + +- (void)setAppSecret:(NSString *)appSecret { + self.ingestion.appSecret = appSecret; +} + +- (NSString *)appSecret { + return self.ingestion.appSecret; +} + +- (NSString *)logUrl { + return self.ingestion.baseURL; +} + +- (void)setMaxStorageSize:(long)sizeInBytes completionHandler:(nullable void (^)(BOOL))completionHandler { + dispatch_async(self.logsDispatchQueue, ^{ + [self.storage setMaxStorageSize:sizeInBytes completionHandler:completionHandler]; + }); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelGroupDefaultPrivate.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelGroupDefaultPrivate.h new file mode 100644 index 0000000000..cbbccf4968 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelGroupDefaultPrivate.h @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACChannelDelegate.h" + +NS_ASSUME_NONNULL_BEGIN + +@class MSACAppCenterIngestion; +@class UIApplication; + +@interface MSACChannelGroupDefault () + +/** + * Initializes a new `MSACChannelGroupDefault` instance. + * + * @param ingestion An HTTP ingestion instance that is used to send batches of log items to the backend. + * + * @return A new `MSACChannelGroupDefault` instance. + */ +- (instancetype)initWithIngestion:(nullable MSACAppCenterIngestion *)ingestion; + +#if !TARGET_OS_OSX + +/** + * Called when applciation is terminating. + */ +- (void)applicationWillTerminate:(UIApplication *)application; + +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelUnitConfiguration.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelUnitConfiguration.h new file mode 100644 index 0000000000..9ec5368301 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelUnitConfiguration.h @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACConstants+Internal.h" +#import "MSACConstants.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface MSACChannelUnitConfiguration : NSObject + +/** + * The groupId that will be used for storage by this channel. + */ +@property(nonatomic, copy, readonly) NSString *groupId; + +/** + * The priority of logs for this channel + */ +@property(nonatomic, assign, readonly) MSACPriority priority; + +/** + * Threshold after which the queue will be flushed. + */ +@property(nonatomic, readonly) NSUInteger batchSizeLimit; + +/** + * Maximum number of batches forwarded to the ingestion at the same time. + */ +@property(nonatomic, readonly) NSUInteger pendingBatchesLimit; + +/** + * Interval for flushing the queue. + */ +@property(nonatomic, readonly) NSUInteger flushInterval; + +/** + * Initializes a new instance based on given settings. + * + * @param groupId The id used by the channel to determine a group of logs. + * @param priority The priority of logs being sent by the channel. + * @param flushInterval The interval in seconds after which a new batch will be finished. Must be between 3 and 86400 (1 day). + * @param batchSizeLimit The maximum number of logs after which a new batch will be finished. + * @param pendingBatchesLimit The maximum number of batches that have currently been forwarded to another component. + * + * @return a fully configured `MSACChannelUnitConfiguration` instance. + */ +- (instancetype)initWithGroupId:(NSString *)groupId + priority:(MSACPriority)priority + flushInterval:(NSUInteger)flushInterval + batchSizeLimit:(NSUInteger)batchSizeLimit + pendingBatchesLimit:(NSUInteger)pendingBatchesLimit; + +/** + * Initializes a new instance with default settings. + * + * @param groupId The id used by the channel to determine a group of logs. + * + * @return a fully configured `MSACChannelConfiguration` instance with default settings. + */ +- (instancetype)initDefaultConfigurationWithGroupId:(NSString *)groupId; + +/** + * Initializes a new instance with flushInterval. + * + * @param groupId The id used by the channel to determine a group of logs. + * @param flushInterval The interval in seconds after which a new batch will be finished. Must be between 3 and 86400 (1 day). + * + * @return a fully configured `MSACChannelConfiguration` instance with flushInterval. + */ +- (instancetype)initDefaultConfigurationWithGroupId:(NSString *)groupId flushInterval:(NSUInteger)flushInterval; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelUnitConfiguration.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelUnitConfiguration.m new file mode 100644 index 0000000000..da6f66df4c --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelUnitConfiguration.m @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACChannelUnitConfiguration.h" + +@implementation MSACChannelUnitConfiguration + +- (instancetype)initWithGroupId:(NSString *)groupId + priority:(MSACPriority)priority + flushInterval:(NSUInteger)flushInterval + batchSizeLimit:(NSUInteger)batchSizeLimit + pendingBatchesLimit:(NSUInteger)pendingBatchesLimit { + if ((self = [super init])) { + _groupId = groupId; + _priority = priority; + _flushInterval = flushInterval; + _batchSizeLimit = batchSizeLimit; + _pendingBatchesLimit = pendingBatchesLimit; + } + return self; +} + +- (instancetype)initDefaultConfigurationWithGroupId:(NSString *)groupId flushInterval:(NSUInteger)flushInterval { + return [self initWithGroupId:groupId priority:MSACPriorityDefault flushInterval:flushInterval batchSizeLimit:50 pendingBatchesLimit:3]; +} + +- (instancetype)initDefaultConfigurationWithGroupId:(NSString *)groupId { + return [self initDefaultConfigurationWithGroupId:groupId flushInterval:kMSACFlushIntervalDefault]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelUnitDefault.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelUnitDefault.h new file mode 100644 index 0000000000..5898bdac70 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelUnitDefault.h @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACChannelUnitProtocol.h" + +NS_ASSUME_NONNULL_BEGIN + +@class MSACChannelUnitConfiguration; + +@protocol MSACIngestionProtocol; +@protocol MSACStorage; + +@interface MSACChannelUnitDefault : NSObject + +/** + * Initializes a new `MSACChannelUnitDefault` instance. + * + * @param ingestion An ingestion instance that is used to send batches of log items to the backend. + * @param storage A storage instance to store and read enqueued log items. + * @param configuration The configuration used by this channel. + * @param logsDispatchQueue Queue used to process logs. + * + * @return A new `MSACChannelUnitDefault` instance. + */ +- (instancetype)initWithIngestion:(nullable id)ingestion + storage:(id)storage + configuration:(MSACChannelUnitConfiguration *)configuration + logsDispatchQueue:(dispatch_queue_t)logsDispatchQueue; + +/** + * Hash table of channel delegate. + */ +@property(nonatomic) NSHashTable> *delegates; + +/** + * An ingestion instance that is used to send batches of log items to the + * backend. + */ +@property(nonatomic, nullable) id ingestion; + +/** + * A storage instance to store and read enqueued log items. + */ +@property(nonatomic) id storage; + +/** + * A timer source which is used to flush the queue after a certain amount of time. + */ +@property(nonatomic) dispatch_source_t timerSource; + +/** + * A counter that keeps tracks of the number of logs added to the queue. + */ +@property(nonatomic, assign) NSUInteger itemsCount; + +/** + * A list used to keep track of batches that have been forwarded to the ingestion component. + */ +@property(nonatomic, strong) NSMutableArray *pendingBatchIds; + +/** + * A boolean value set to YES if there is at least one available batch from the storage. + */ +@property(nonatomic) BOOL availableBatchFromStorage; + +/** + * A boolean value set to YES if the pending batch queue is full. + */ +@property(nonatomic) BOOL pendingBatchQueueFull; + +/** + * A boolean value set to YES if the channel is enabled or NO otherwise. + * Enable/disable does resume/pause the channel as needed under the hood. When a channel is disabled with data deletion it deletes persisted + * logs and discards incoming logs. + */ +@property(nonatomic, getter=isEnabled) BOOL enabled; + +/** + * A boolean value set to YES if the channel is paused or NO otherwise. A paused channel doesn't forward logs to the ingestion. A paused + * state doesn't impact the current enabled state. + */ +@property(nonatomic, getter=isPaused) BOOL paused; + +/** + * A boolean value set to YES if logs are discarded (not persisted) or NO otherwise. Logs are discarded when the related service is disabled + * or an unrecoverable error happened. + */ +@property(nonatomic) BOOL discardLogs; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelUnitDefault.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelUnitDefault.m new file mode 100644 index 0000000000..d50eb45b7c --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelUnitDefault.m @@ -0,0 +1,590 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACChannelUnitDefault.h" +#import "MSACAbstractLogInternal.h" +#import "MSACAppCenterErrors.h" +#import "MSACAppCenterIngestion.h" +#import "MSACAppCenterInternal.h" +#import "MSACChannelUnitConfiguration.h" +#import "MSACChannelUnitDefaultPrivate.h" +#import "MSACDeviceTracker.h" +#import "MSACStorage.h" +#import "MSACUtility+StringFormatting.h" + +/** + * Key for the start timestamp. + */ +static NSString *const kMSACStartTimestampPrefix = @"ChannelStartTimer"; + +@implementation MSACChannelUnitDefault + +@synthesize configuration = _configuration; +@synthesize logsDispatchQueue = _logsDispatchQueue; + +#pragma mark - Initialization + +- (instancetype)init { + if ((self = [super init])) { + _itemsCount = 0; + _pendingBatchIds = [NSMutableArray new]; + _pendingBatchQueueFull = NO; + _availableBatchFromStorage = NO; + _enabled = YES; + _paused = NO; + _discardLogs = NO; + _delegates = [NSHashTable weakObjectsHashTable]; + _pausedIdentifyingObjects = [NSHashTable weakObjectsHashTable]; + _pausedTargetKeys = [NSMutableSet new]; + } + return self; +} + +- (instancetype)initWithIngestion:(nullable id)ingestion + storage:(id)storage + configuration:(MSACChannelUnitConfiguration *)configuration + logsDispatchQueue:(dispatch_queue_t)logsDispatchQueue { + if ((self = [self init])) { + _ingestion = ingestion; + _storage = storage; + _configuration = configuration; + _logsDispatchQueue = logsDispatchQueue; + } + return self; +} + +#pragma mark - MSACChannelDelegate + +- (void)addDelegate:(id)delegate { + dispatch_async(self.logsDispatchQueue, ^{ + @synchronized(self.delegates) { + [self.delegates addObject:delegate]; + } + }); +} + +- (void)removeDelegate:(id)delegate { + dispatch_async(self.logsDispatchQueue, ^{ + @synchronized(self.delegates) { + [self.delegates removeObject:delegate]; + } + }); +} + +#pragma mark - Managing queue + +- (void)enqueueItem:(id)item flags:(MSACFlags)flags { + + /* + * Set common log info. + * Only add timestamp and device info in case the log doesn't have one. In case the log is restored after a crash or for crashes, we don't + * want the timestamp and the device information to be updated but want the old one preserved. + */ + if (item && !item.timestamp) { + item.timestamp = [NSDate date]; + } + if (item && !item.device) { + item.device = [[MSACDeviceTracker sharedInstance] device]; + } + if (!item || ![item isValid]) { + MSACLogWarning([MSACAppCenter logTag], @"Log is not valid."); + return; + } + + // Internal ID to keep track of logs between modules. + NSString *internalLogId = MSAC_UUID_STRING; + + @autoreleasepool { + + // Additional preparations for the log. Used to specify the session id and distribution group id. + [self enumerateDelegatesForSelector:@selector(channel:prepareLog:) + withBlock:^(id delegate) { + [delegate channel:self prepareLog:item]; + }]; + + // Notify delegate about enqueuing as fast as possible on the current thread. + [self enumerateDelegatesForSelector:@selector(channel:didPrepareLog:internalId:flags:) + withBlock:^(id delegate) { + [delegate channel:self didPrepareLog:item internalId:internalLogId flags:flags]; + }]; + } + + // Return fast in case our item is empty or we are discarding logs right now. + dispatch_async(self.logsDispatchQueue, ^{ + // Use separate autorelease pool for enqueuing logs. + @autoreleasepool { + + // Check if the log should be filtered out. If so, don't enqueue it. + __block BOOL shouldFilter = NO; + [self enumerateDelegatesForSelector:@selector(channelUnit:shouldFilterLog:) + withBlock:^(id delegate) { + shouldFilter = shouldFilter || [delegate channelUnit:self shouldFilterLog:item]; + }]; + if (shouldFilter) { + MSACLogDebug([MSACAppCenter logTag], @"Log of type '%@' was filtered out by delegate(s)", item.type); + [self enumerateDelegatesForSelector:@selector(channel:didCompleteEnqueueingLog:internalId:) + withBlock:^(id delegate) { + [delegate channel:self didCompleteEnqueueingLog:item internalId:internalLogId]; + }]; + return; + } + if (!self.ingestion.isReadyToSend) { + MSACLogDebug([MSACAppCenter logTag], @"Log of type '%@' was not filtered out by delegate(s) but ingestion is not ready to send it.", + item.type); + [self enumerateDelegatesForSelector:@selector(channel:didCompleteEnqueueingLog:internalId:) + withBlock:^(id delegate) { + [delegate channel:self didCompleteEnqueueingLog:item internalId:internalLogId]; + }]; + return; + } + if (self.discardLogs) { + MSACLogWarning([MSACAppCenter logTag], @"Channel %@ disabled in log discarding mode, discard this log.", + self.configuration.groupId); + NSError *error = [NSError errorWithDomain:kMSACACErrorDomain + code:MSACACConnectionPausedErrorCode + userInfo:@{NSLocalizedDescriptionKey : kMSACACConnectionPausedErrorDesc}]; + [self notifyFailureBeforeSendingForItem:item withError:error]; + [self enumerateDelegatesForSelector:@selector(channel:didCompleteEnqueueingLog:internalId:) + withBlock:^(id delegate) { + [delegate channel:self didCompleteEnqueueingLog:item internalId:internalLogId]; + }]; + return; + } + + // Save the log first. + MSACLogDebug([MSACAppCenter logTag], @"Saving log, type: %@, flags: %u.", item.type, (unsigned int)flags); + bool success = [self.storage saveLog:item withGroupId:self.configuration.groupId flags:flags]; + + // Notify delegates of completion (whatever the result is). + [self enumerateDelegatesForSelector:@selector(channel:didCompleteEnqueueingLog:internalId:) + withBlock:^(id delegate) { + [delegate channel:self didCompleteEnqueueingLog:item internalId:internalLogId]; + }]; + + // If successful, check if logs can be sent now. + if (success) { + self.itemsCount += 1; + [self checkPendingLogs]; + } + } + }); +} + +- (void)sendLogContainer:(MSACLogContainer *__nonnull)container { + + // Add to pending batches. + [self.pendingBatchIds addObject:container.batchId]; + if (self.pendingBatchIds.count >= self.configuration.pendingBatchesLimit) { + + // The maximum number of batches forwarded to the ingestion at the same time has been reached. + self.pendingBatchQueueFull = YES; + } + + // Optimization. If the current log level is greater than + // MSACLogLevelDebug, we can skip it. + if ([MSACAppCenter logLevel] <= MSACLogLevelDebug) { + NSUInteger count = [container.logs count]; + for (NSUInteger i = 0; i < count; i++) { + MSACLogDebug([MSACAppCenter logTag], @"Sending %tu/%tu log, group Id: %@, batch Id: %@, session Id: %@, payload:\n%@", (i + 1), count, + self.configuration.groupId, container.batchId, container.logs[i].sid, + [(MSACAbstractLog *)container.logs[i] serializeLogWithPrettyPrinting:YES]); + } + } + + // Notify delegates. + [self enumerateDelegatesForSelector:@selector(channel:willSendLog:) + withBlock:^(id delegate) { + for (id aLog in container.logs) { + [delegate channel:self willSendLog:aLog]; + } + }]; + + // Forward logs to the ingestion. + [self.ingestion sendAsync:container + completionHandler:^(NSString *ingestionBatchId, NSHTTPURLResponse *response, __unused NSData *data, NSError *error) { + dispatch_async(self.logsDispatchQueue, ^{ + if (![self.pendingBatchIds containsObject:ingestionBatchId]) { + MSACLogWarning([MSACAppCenter logTag], @"Batch Id %@ not expected, ignore.", ingestionBatchId); + return; + } + BOOL succeeded = response.statusCode == MSACHTTPCodesNo200OK; + if (succeeded) { + MSACLogDebug([MSACAppCenter logTag], @"Log(s) sent with success, batch Id:%@.", ingestionBatchId); + + // Notify delegates. + [self enumerateDelegatesForSelector:@selector(channel:didSucceedSendingLog:) + withBlock:^(id delegate) { + for (id aLog in container.logs) { + [delegate channel:self didSucceedSendingLog:aLog]; + } + }]; + + // Remove the logs from storage. + [self.storage deleteLogsWithBatchId:ingestionBatchId groupId:self.configuration.groupId]; + } + + // Failure. + else { + MSACLogError([MSACAppCenter logTag], @"Log(s) sent with failure, batch Id:%@, status code:%tu", ingestionBatchId, + response.statusCode); + + // Notify delegates. + [self enumerateDelegatesForSelector:@selector(channel:didFailSendingLog:withError:) + withBlock:^(id delegate) { + for (id aLog in container.logs) { + [delegate channel:self didFailSendingLog:aLog withError:error]; + } + }]; + + // Disable and delete all data on fatal error. + if (![MSACHttpUtil isRecoverableError:response.statusCode]) { + MSACLogError([MSACAppCenter logTag], @"Fatal error encountered; shutting down channel unit with group ID %@", + self.configuration.groupId); + [self setEnabled:NO andDeleteDataOnDisabled:YES]; + return; + } + } + + // Remove from pending batches. + [self.pendingBatchIds removeObject:ingestionBatchId]; + + // Update pending batch queue state. + if (self.pendingBatchQueueFull && self.pendingBatchIds.count < self.configuration.pendingBatchesLimit) { + self.pendingBatchQueueFull = NO; + + if (succeeded && self.availableBatchFromStorage) { + [self flushQueue]; + } + } + }); + }]; +} + +- (void)flushQueue { + + // Nothing to flush if there is no ingestion. + if (!self.ingestion) { + return; + } + + // Don't flush while disabled. + if (!self.enabled) { + return; + } + + // Ingestion is not ready. + if (!self.ingestion.isReadyToSend) { + return; + } + + // Cancel any timer. + [self resetTimer]; + + // Don't flush while paused or if pending bach queue is full. + if (self.paused || self.pendingBatchQueueFull) { + + // Still close the current batch it will be flushed later. + if (self.itemsCount >= self.configuration.batchSizeLimit) { + + // That batch becomes available. + self.availableBatchFromStorage = YES; + self.itemsCount = 0; + } + return; + } + + // Reset item count and load data from the storage. + self.itemsCount = 0; + + // NOTE: It isn't async operation, completion handler will be called immediately. + self.availableBatchFromStorage = [self.storage loadLogsWithGroupId:self.configuration.groupId + limit:self.configuration.batchSizeLimit + excludedTargetKeys:[self.pausedTargetKeys allObjects] + completionHandler:^(NSArray> *_Nonnull logArray, NSString *batchId) { + // Check if there is data to send. Logs may be deleted from storage before this flush. + if (logArray.count > 0) { + MSACLogContainer *container = [[MSACLogContainer alloc] initWithBatchId:batchId + andLogs:logArray]; + [self sendLogContainer:container]; + } + }]; + + // Flush again if there is another batch to send. + if (self.availableBatchFromStorage && !self.pendingBatchQueueFull) { + [self flushQueue]; + } +} + +- (void)checkPendingLogs { + + // If the interval is default and we reached batchSizeLimit flush logs now. + if (!self.paused && self.configuration.flushInterval == kMSACFlushIntervalDefault && + self.itemsCount >= self.configuration.batchSizeLimit) { + [self flushQueue]; + } else if (self.itemsCount > 0) { + NSUInteger flushInterval = [self resolveFlushInterval]; + + // Skip sending logs if the channel is paused. + if (self.paused) { + return; + } + + // If the interval is over, send all logs without any additional timers. + if (flushInterval == 0) { + [self flushQueue]; + } + + // Postpone sending logs. + else { + [self startTimer:flushInterval]; + } + } +} + +#pragma mark - Timer + +- (void)startTimer:(NSUInteger)flushInterval { + + // Don't start timer while disabled. + if (!self.enabled) { + return; + } + + // Cancel any timer. + [self resetTimer]; + + // Create new timer. + self.timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.logsDispatchQueue); + + /** + * Cast (NSEC_PER_SEC * flushInterval) to (int64_t) silence warning. The compiler otherwise complains that we're using + * a float param (flushInterval) and implicitly downcast to int64_t. + */ + dispatch_source_set_timer(self.timerSource, dispatch_walltime(NULL, (int64_t)(NSEC_PER_SEC * flushInterval)), 1ull * NSEC_PER_SEC, + 1ull * NSEC_PER_SEC); + __weak typeof(self) weakSelf = self; + dispatch_source_set_event_handler(self.timerSource, ^{ + typeof(self) strongSelf = weakSelf; + + // Flush the queue as needed. + if (strongSelf) { + if (strongSelf.itemsCount > 0) { + [strongSelf flushQueue]; + } + [strongSelf resetTimer]; + + // Remove the current timestamp. All pending logs will be sent in flushQueue call. + [MSAC_APP_CENTER_USER_DEFAULTS removeObjectForKey:[strongSelf oldestPendingLogTimestampKey]]; + } + }); + dispatch_resume(self.timerSource); +} + +- (NSUInteger)resolveFlushInterval { + NSUInteger flushInterval = self.configuration.flushInterval; + + // If the interval is custom. + if (flushInterval > kMSACFlushIntervalDefault) { + NSDate *now = [NSDate date]; + NSDate *oldestPendingLogTimestamp = [MSAC_APP_CENTER_USER_DEFAULTS objectForKey:[self oldestPendingLogTimestampKey]]; + + // The timer isn't started or has invalid value (start time in the future), so start it and store the current time. + if (oldestPendingLogTimestamp == nil || [now compare:oldestPendingLogTimestamp] == NSOrderedAscending) { + [MSAC_APP_CENTER_USER_DEFAULTS setObject:now forKey:[self oldestPendingLogTimestampKey]]; + } + + // If the interval is over. + else if ([now compare:[oldestPendingLogTimestamp dateByAddingTimeInterval:flushInterval]] == NSOrderedDescending) { + [MSAC_APP_CENTER_USER_DEFAULTS removeObjectForKey:[self oldestPendingLogTimestampKey]]; + return 0; + } + + // We still have to wait for the rest of the interval. + else { + flushInterval -= (NSUInteger)[now timeIntervalSinceDate:oldestPendingLogTimestamp]; + } + } + return flushInterval; +} + +- (NSString *)oldestPendingLogTimestampKey { + return [NSString stringWithFormat:@"%@:%@", kMSACStartTimestampPrefix, self.configuration.groupId]; +} + +- (void)resetTimer { + if (self.timerSource) { + dispatch_source_cancel(self.timerSource); + } +} + +#pragma mark - Life cycle + +- (void)setEnabled:(BOOL)isEnabled andDeleteDataOnDisabled:(BOOL)deleteData { + dispatch_async(self.logsDispatchQueue, ^{ + if (self.enabled != isEnabled) { + self.enabled = isEnabled; + if (isEnabled) { + [self resumeWithIdentifyingObjectSync:self]; + } else { + [self pauseWithIdentifyingObjectSync:self]; + } + } + + // Even if it's already disabled we might also want to delete logs this time. + if (!isEnabled && deleteData) { + MSACLogDebug([MSACAppCenter logTag], @"Delete all logs for group Id %@", self.configuration.groupId); + NSError *error = [NSError errorWithDomain:kMSACACErrorDomain + code:MSACACConnectionPausedErrorCode + userInfo:@{NSLocalizedDescriptionKey : kMSACACConnectionPausedErrorDesc}]; + [self deleteAllLogsWithErrorSync:error]; + + // Reset states. + self.itemsCount = 0; + self.availableBatchFromStorage = NO; + self.pendingBatchQueueFull = NO; + [MSAC_APP_CENTER_USER_DEFAULTS removeObjectForKey:[self oldestPendingLogTimestampKey]]; + + // Prevent further logs from being persisted. + self.discardLogs = YES; + } else { + + // Allow logs to be persisted. + self.discardLogs = NO; + } + + // Notify delegates. + [self enumerateDelegatesForSelector:@selector(channel:didSetEnabled:andDeleteDataOnDisabled:) + withBlock:^(id delegate) { + [delegate channel:self didSetEnabled:isEnabled andDeleteDataOnDisabled:deleteData]; + }]; + }); +} + +- (void)pauseWithIdentifyingObject:(id)identifyingObject { + dispatch_async(self.logsDispatchQueue, ^{ + [self pauseWithIdentifyingObjectSync:identifyingObject]; + }); +} + +- (void)resumeWithIdentifyingObject:(id)identifyingObject { + dispatch_async(self.logsDispatchQueue, ^{ + [self resumeWithIdentifyingObjectSync:identifyingObject]; + }); +} + +- (void)pauseWithIdentifyingObjectSync:(id)identifyingObject { + [self.pausedIdentifyingObjects addObject:identifyingObject]; + MSACLogVerbose([MSACAppCenter logTag], @"Identifying object %@ added to pause lane for channel %@.", identifyingObject, + self.configuration.groupId); + if (!self.paused) { + MSACLogDebug([MSACAppCenter logTag], @"Pause channel %@.", self.configuration.groupId); + self.paused = YES; + [self resetTimer]; + } + [self enumerateDelegatesForSelector:@selector(channel:didPauseWithIdentifyingObject:) + withBlock:^(id delegate) { + [delegate channel:self didPauseWithIdentifyingObject:identifyingObject]; + }]; +} + +- (void)resumeWithIdentifyingObjectSync:(id)identifyingObject { + [self.pausedIdentifyingObjects removeObject:identifyingObject]; + MSACLogVerbose([MSACAppCenter logTag], @"Identifying object %@ removed from pause lane for channel %@.", identifyingObject, + self.configuration.groupId); + if ([self.pausedIdentifyingObjects count] == 0) { + MSACLogDebug([MSACAppCenter logTag], @"Resume channel %@.", self.configuration.groupId); + self.paused = NO; + [self checkPendingLogs]; + } + [self enumerateDelegatesForSelector:@selector(channel:didResumeWithIdentifyingObject:) + withBlock:^(id delegate) { + [delegate channel:self didResumeWithIdentifyingObject:identifyingObject]; + }]; +} + +- (void)pauseSendingLogsWithToken:(NSString *)token { + NSString *targetKey = [MSACUtility targetKeyFromTargetToken:token]; + dispatch_async(self.logsDispatchQueue, ^{ + MSACLogDebug([MSACAppCenter logTag], @"Pause channel for target key %@.", targetKey); + [self.pausedTargetKeys addObject:targetKey]; + }); +} + +- (void)resumeSendingLogsWithToken:(NSString *)token { + NSString *targetKey = [MSACUtility targetKeyFromTargetToken:token]; + dispatch_async(self.logsDispatchQueue, ^{ + MSACLogDebug([MSACAppCenter logTag], @"Resume channel for target key %@.", targetKey); + [self.pausedTargetKeys removeObject:targetKey]; + + // Update item count and check logs if it meets the conditions to send logs. + // This solution is not ideal since it might create a batch with fewer logs than expected as the log count contains logs with paused + // keys, this would be an optimization that doesn't seem necessary for now. Aligned with Android implementation. + self.itemsCount = [self.storage countLogs]; + [self checkPendingLogs]; + }); +} + +#pragma mark - Storage + +- (void)deleteAllLogsWithError:(NSError *)error { + dispatch_async(self.logsDispatchQueue, ^{ + [self deleteAllLogsWithErrorSync:error]; + }); +} + +- (void)deleteAllLogsWithErrorSync:(NSError *)error { + NSArray> *deletedLogs; + + // Delete pending batches first. + for (NSString *batchId in self.pendingBatchIds) { + [self.storage deleteLogsWithBatchId:batchId groupId:self.configuration.groupId]; + } + [self.pendingBatchIds removeAllObjects]; + + // Delete remaining logs. + deletedLogs = [self.storage deleteLogsWithGroupId:self.configuration.groupId]; + + // Notify failure of remaining logs. + for (id log in deletedLogs) { + [self notifyFailureBeforeSendingForItem:log withError:error]; + } +} + +#pragma mark - Helper + +- (void)enumerateDelegatesForSelector:(SEL)selector withBlock:(void (^)(id delegate))block { + NSArray *synchronizedDelegates; + @synchronized(self.delegates) { + + // Don't execute the block while locking; it might be locking too and deadlock ourselves. + synchronizedDelegates = [self.delegates allObjects]; + } + for (id delegate in synchronizedDelegates) { + if ([delegate respondsToSelector:selector]) { + block(delegate); + } + } +} + +- (void)notifyFailureBeforeSendingForItem:(id)item withError:(NSError *)error { + NSArray *synchronizedDelegates; + @synchronized(self.delegates) { + + // Don't execute the block while locking; it might be locking too and deadlock ourselves. + synchronizedDelegates = [self.delegates allObjects]; + } + for (id delegate in synchronizedDelegates) { + + // Call willSendLog before didFailSendingLog + if ([delegate respondsToSelector:@selector(channel:willSendLog:)]) { + [delegate channel:self willSendLog:item]; + } + + // Call didFailSendingLog + if ([delegate respondsToSelector:@selector(channel:didFailSendingLog:withError:)]) { + [delegate channel:self didFailSendingLog:item withError:error]; + } + } +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelUnitDefaultPrivate.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelUnitDefaultPrivate.h new file mode 100644 index 0000000000..5c4dbc0fd8 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelUnitDefaultPrivate.h @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACChannelUnitDefault.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface MSACChannelUnitDefault () + +@property(nonatomic) NSHashTable *pausedIdentifyingObjects; + +@property(nonatomic) NSMutableSet *pausedTargetKeys; + +/** + * Flush pending logs. + */ +- (void)flushQueue; + +/** + * Synchronously pause operations, logs will be stored but not sent. + * + * @param identifyingObject Object used to identify the pause request. + * + * @discussion The same identifying object must be used to call resume. + * + * @see resumeWithIdentifyingObject: + */ +- (void)pauseWithIdentifyingObjectSync:(id)identifyingObject; + +/** + * Synchronously resume operations, logs can be sent again. + * + * @param identifyingObject Object used to passed to the pause method. + * + * @discussion The channel only resume when all the outstanding identifying objects have been resumed. + * + * @see pauseWithIdentifyingObject: + */ +- (void)resumeWithIdentifyingObjectSync:(id)identifyingObject; + +/** + * If we have flushInterval bigger than 3 seconds, we should subtract an oldest log's timestamp from it. + * It is required to avoid situations when the logs are not being sent to server because time interval is too big + * for a typical user session. + * + * @return Remaining interval to trigger flush. + */ +- (NSUInteger)resolveFlushInterval; + +/** + * Get a key for NSUserDefaults where the oldest pending log timestamp is stored for the channel. + * + * @return A key for the oldest pending log timestamp. + */ +- (NSString *)oldestPendingLogTimestampKey; + +/** + * Start timer to send logs. + * + * @param flushInterval delay in seconds. + */ +- (void)startTimer:(NSUInteger)flushInterval; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelUnitProtocol.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelUnitProtocol.h new file mode 100644 index 0000000000..88884e8bc3 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACChannelUnitProtocol.h @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACChannelProtocol.h" +#import "MSACConstants+Flags.h" + +NS_ASSUME_NONNULL_BEGIN + +@class MSACChannelUnitConfiguration; +@protocol MSACLog; + +/** + * `MSACChannelUnitProtocol` represents a kind of channel that is able to actually store/send logs (as opposed to a channel group, which + * simply contains a collection of channel units). + */ +@protocol MSACChannelUnitProtocol + +/** + * The configuration used by this channel unit. + */ +@property(nonatomic) MSACChannelUnitConfiguration *configuration; + +/** + * Queue used to process logs. + */ +@property(nonatomic) dispatch_queue_t logsDispatchQueue; + +/** + * Enqueue a new log item. + * + * @param item The log item that should be enqueued. + * @param flags Options for the item being enqueued. + */ +- (void)enqueueItem:(id)item flags:(MSACFlags)flags; + +/** + * Pause sending logs with the given transmission target token. + * + * @param token The transmission target token. + * + * @discussion The logs with the given token will continue to be persisted in the storage but they will only be sent once it resumes sending + * logs. + * + * @see resumeSendingLogsWithToken: + */ +- (void)pauseSendingLogsWithToken:(NSString *)token; + +/** + * Resume sending logs with the given transmission target token. + * + * @param token The transmission target token. + * + * @see pauseSendingLogsWithToken: + */ +- (void)resumeSendingLogsWithToken:(NSString *)token; + +/** + * Check for enqueued logs to send to ingestion. + */ +- (void)checkPendingLogs; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACOneCollectorChannelDelegate.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACOneCollectorChannelDelegate.h new file mode 100644 index 0000000000..5f06d74ee3 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACOneCollectorChannelDelegate.h @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACChannelDelegate.h" + +@protocol MSACHttpClientProtocol; + +NS_ASSUME_NONNULL_BEGIN + +/** + * One Collector channel delegate that is used to redirect selected traffic to One Collector. + */ +@interface MSACOneCollectorChannelDelegate : NSObject + +/** + * Init a `MSACOneCollectorChannelDelegate` with an install Id. + * + * @param httpClient HTTP client instance. + * @param installId A device install Id. + * @param baseUrl base url to use for backend communication. + * + * @return A `MSACOneCollectorChannelDelegate` instance. + */ +- (instancetype)initWithHttpClient:(id)httpClient + installId:(NSUUID *)installId + baseUrl:(nullable NSString *)baseUrl; + +/** + * Change the base URL (schema + authority + port only) that is used to communicate with the backend. + * + * @param logUrl base URL to use for backend communication. + */ +- (void)setLogUrl:(NSString *)logUrl; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACOneCollectorChannelDelegate.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACOneCollectorChannelDelegate.m new file mode 100644 index 0000000000..e5231f981d --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACOneCollectorChannelDelegate.m @@ -0,0 +1,193 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAbstractLogInternal.h" +#import "MSACAppCenterInternal.h" +#import "MSACCSEpochAndSeq.h" +#import "MSACCSExtensions.h" +#import "MSACChannelUnitConfiguration.h" +#import "MSACChannelUnitProtocol.h" +#import "MSACOneCollectorChannelDelegatePrivate.h" +#import "MSACOneCollectorIngestion.h" +#import "MSACSDKExtension.h" + +static NSString *const kMSACOneCollectorBaseUrl = @"https://mobile.events.data.microsoft.com"; // TODO: move to constants? +static NSString *const kMSACBaseErrorMsg = @"Log validation failed."; + +/** + * Log name regex. alnum characters, no heading or trailing periods, no heading underscores, min length of 4, max length of 100. + */ + +@implementation MSACOneCollectorChannelDelegate + +- (instancetype)initWithHttpClient:(id)httpClient installId:(NSUUID *)installId baseUrl:(NSString *)baseUrl { + self = [self init]; + if (self) { + _installId = installId; + _baseUrl = baseUrl ?: kMSACOneCollectorBaseUrl; + _oneCollectorChannels = [NSMutableDictionary new]; + _oneCollectorIngestion = [[MSACOneCollectorIngestion alloc] initWithHttpClient:httpClient baseUrl:_baseUrl]; + _epochsAndSeqsByIKey = [NSMutableDictionary new]; + } + return self; +} + +- (void)channelGroup:(id)channelGroup didAddChannelUnit:(id)channel { + + // Add OneCollector group based on the given channel's group id. + NSString *groupId = channel.configuration.groupId; + if (![self isOneCollectorGroup:groupId]) { + NSString *oneCollectorGroupId = [NSString stringWithFormat:@"%@%@", channel.configuration.groupId, kMSACOneCollectorGroupIdSuffix]; + MSACChannelUnitConfiguration *channelUnitConfiguration = + [[MSACChannelUnitConfiguration alloc] initDefaultConfigurationWithGroupId:oneCollectorGroupId + flushInterval:channel.configuration.flushInterval]; + id channelUnit = [channelGroup addChannelUnitWithConfiguration:channelUnitConfiguration + withIngestion:self.oneCollectorIngestion]; + self.oneCollectorChannels[groupId] = channelUnit; + } +} + +- (void)channel:(id)__unused channel prepareLog:(id)log { + + // Prepare Common Schema logs. + if ([log isKindOfClass:[MSACCommonSchemaLog class]]) { + MSACCommonSchemaLog *csLog = (MSACCommonSchemaLog *)log; + + // Set SDK extension values. + MSACCSEpochAndSeq *epochAndSeq = self.epochsAndSeqsByIKey[csLog.iKey]; + if (!epochAndSeq) { + epochAndSeq = [[MSACCSEpochAndSeq alloc] initWithEpoch:MSAC_UUID_STRING]; + } + csLog.ext.sdkExt.epoch = epochAndSeq.epoch; + csLog.ext.sdkExt.seq = ++epochAndSeq.seq; + csLog.ext.sdkExt.installId = self.installId; + self.epochsAndSeqsByIKey[csLog.iKey] = epochAndSeq; + + // Set install ID to SDK. + csLog.ext.sdkExt.installId = self.installId; + } +} + +- (void)channel:(id)channel + didPrepareLog:(id)log + internalId:(NSString *)__unused internalId + flags:(MSACFlags)flags { + id channelUnit = (id)channel; + id oneCollectorChannelUnit = nil; + NSString *groupId = channelUnit.configuration.groupId; + + /* + * Reroute Custom Schema logs to their One Collector channel if they were enqueued to a non One Collector channel. Happens to logs from + * the log buffer after a crash. + */ + if ([(NSObject *)log isKindOfClass:[MSACCommonSchemaLog class]] && ![self isOneCollectorGroup:groupId]) { + oneCollectorChannelUnit = self.oneCollectorChannels[groupId]; + if (oneCollectorChannelUnit) { + [oneCollectorChannelUnit enqueueItem:log flags:flags]; + } + return; + } + if (![self shouldSendLogToOneCollector:log] || ![channel conformsToProtocol:@protocol(MSACChannelUnitProtocol)]) { + return; + } + oneCollectorChannelUnit = self.oneCollectorChannels[groupId]; + if (!oneCollectorChannelUnit) { + return; + } + id logConversion = (id)log; + NSArray *commonSchemaLogs = [logConversion toCommonSchemaLogsWithFlags:flags]; + for (MSACCommonSchemaLog *commonSchemaLog in commonSchemaLogs) { + [oneCollectorChannelUnit enqueueItem:commonSchemaLog flags:flags]; + } +} + +- (BOOL)channelUnit:(id)channelUnit shouldFilterLog:(id)log { + + // Validate Custom Schema logs, filter out invalid logs. + if ([log isKindOfClass:[MSACCommonSchemaLog class]]) { + if (![self isOneCollectorGroup:channelUnit.configuration.groupId]) { + return true; + } + return ![self validateLog:(MSACCommonSchemaLog *)log]; + } + + // It's an App Center log. Filter out if it contains token(s) since it's already re-enqueued as CS log(s). + return [[log transmissionTargetTokens] count] > 0; +} + +- (void)channel:(id)channel didSetEnabled:(BOOL)isEnabled andDeleteDataOnDisabled:(BOOL)deletedData { + if ([channel conformsToProtocol:@protocol(MSACChannelUnitProtocol)]) { + NSString *groupId = ((id)channel).configuration.groupId; + if (![self isOneCollectorGroup:groupId]) { + + // Mirror disabling state to OneCollector channels. + [self.oneCollectorChannels[groupId] setEnabled:isEnabled andDeleteDataOnDisabled:deletedData]; + } + } else if ([channel conformsToProtocol:@protocol(MSACChannelGroupProtocol)] && !isEnabled && deletedData) { + + // Reset epoch and seq values when SDK is disabled as a whole. + [self.epochsAndSeqsByIKey removeAllObjects]; + } +} + +- (void)channel:(id)channel didPauseWithIdentifyingObject:(id)identifyingObject { + if ([channel conformsToProtocol:@protocol(MSACChannelUnitProtocol)]) { + NSString *groupId = ((id)channel).configuration.groupId; + id oneCollectorChannel = self.oneCollectorChannels[groupId]; + [oneCollectorChannel pauseWithIdentifyingObject:identifyingObject]; + } +} + +- (void)channel:(id)channel didResumeWithIdentifyingObject:(id)identifyingObject { + if ([channel conformsToProtocol:@protocol(MSACChannelUnitProtocol)]) { + NSString *groupId = ((id)channel).configuration.groupId; + id oneCollectorChannel = self.oneCollectorChannels[groupId]; + [oneCollectorChannel resumeWithIdentifyingObject:identifyingObject]; + } +} + +#pragma mark - Helper + +- (BOOL)isOneCollectorGroup:(NSString *)groupId { + return [groupId hasSuffix:kMSACOneCollectorGroupIdSuffix]; +} + +- (BOOL)shouldSendLogToOneCollector:(id)log { + NSObject *logObject = (NSObject *)log; + return [[log transmissionTargetTokens] count] > 0 && [log conformsToProtocol:@protocol(MSACLogConversion)] && + ![logObject isKindOfClass:[MSACCommonSchemaLog class]]; +} + +- (BOOL)validateLog:(MSACCommonSchemaLog *)log { + if (![self validateLogName:log.name]) { + return NO; + } + + // Property values are valid strings already. + return YES; +} + +- (BOOL)validateLogName:(NSString *)name { + + // Name mustn't be nil. + if (!name.length) { + MSACLogError([MSACAppCenter logTag], @"%@ Name must not be nil or empty.", kMSACBaseErrorMsg); + return NO; + } + + // The Common Schema event name must conform to a regex. + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:kMSACLogNameRegex options:0 error:nil]; + NSRange range = NSMakeRange(0, name.length); + NSUInteger count = [regex numberOfMatchesInString:name options:0 range:range]; + if (!count) { + MSACLogError([MSACAppCenter logTag], @"%@ Name must match '%@' but was '%@'", kMSACBaseErrorMsg, kMSACLogNameRegex, name); + return NO; + } + return YES; +} + +- (void)setLogUrl:(NSString *)logUrl { + self.oneCollectorIngestion.baseURL = logUrl; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACOneCollectorChannelDelegatePrivate.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACOneCollectorChannelDelegatePrivate.h new file mode 100644 index 0000000000..d5e17908d6 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Channel/MSACOneCollectorChannelDelegatePrivate.h @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACOneCollectorChannelDelegate.h" + +@class MSACOneCollectorIngestion; + +@protocol MSACChannelUnitProtocol; +@protocol MSACLog; + +@class MSACCSEpochAndSeq; + +/** + * Regex for Custom Schema log name validation. + */ +static NSString *const kMSACLogNameRegex = @"^[a-zA-Z0-9]((\\.(?!(\\.|$)))|[_a-zA-Z0-9]){3,99}$"; + +@interface MSACOneCollectorChannelDelegate () + +/** + * Collection of channel unit protocols per group Id. + */ +@property(nonatomic) NSMutableDictionary> *oneCollectorChannels; + +/** + * Http ingestion to send logs to One Collector endpoint. + */ +@property(nonatomic) MSACOneCollectorIngestion *oneCollectorIngestion; + +/** + * Base Url for One Collector endpoint. + */ +@property(nonatomic, copy) NSString *baseUrl; + +/** + * Keep track of epoch and sequence per tenant token. + */ +@property(nonatomic) NSMutableDictionary *epochsAndSeqsByIKey; + +/** + * UUID created on first-time SDK initialization. + */ +@property(nonatomic) NSUUID *installId; + +/** + * Returns 'YES' if the log should be sent to one collector. + */ +- (BOOL)shouldSendLogToOneCollector:(id)log; + +/** + * Validate Common Schema 3.0 Log. + * + * @param log The Common Schema log. + * + * @return YES if Common Schema log is valid; NO otherwise. + */ +- (BOOL)validateLog:(MSACCommonSchemaLog *)log; + +/** + * Validate Common Schema log name. + * + * @param name The log name. + * + * @return YES if name is valid, NO otherwise. + */ +- (BOOL)validateLogName:(NSString *)name; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Device/MSACDeviceHistoryInfo.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Device/MSACDeviceHistoryInfo.h new file mode 100644 index 0000000000..5751a57fed --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Device/MSACDeviceHistoryInfo.h @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACHistoryInfo.h" + +@class MSACDevice; + +/** + * Model class that correlates MSACDevice to a crash at app relaunch. + */ +@interface MSACDeviceHistoryInfo : MSACHistoryInfo + +/** + * Instance of MSACDevice. + */ +@property(nonatomic) MSACDevice *device; + +/** + * Initializes a new `MSACDeviceHistoryInfo` instance. + * + * @param timestamp Timestamp. + * @param device Device instance. + */ +- (instancetype)initWithTimestamp:(NSDate *)timestamp andDevice:(MSACDevice *)device; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Device/MSACDeviceHistoryInfo.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Device/MSACDeviceHistoryInfo.m new file mode 100644 index 0000000000..d4a9c2d666 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Device/MSACDeviceHistoryInfo.m @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACDeviceHistoryInfo.h" + +static NSString *const kMSACDeviceKey = @"deviceKey"; + +/** + * This class is used to associate device properties with the timestamp that it was created with. + */ +@implementation MSACDeviceHistoryInfo + +- (instancetype)initWithTimestamp:(NSDate *)timestamp andDevice:(MSACDevice *)device { + self = [super initWithTimestamp:timestamp]; + if (self) { + _device = device; + } + return self; +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if (self) { + self.device = [coder decodeObjectForKey:kMSACDeviceKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.device forKey:kMSACDeviceKey]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Device/MSACDeviceTracker.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Device/MSACDeviceTracker.h new file mode 100644 index 0000000000..2e1e7709e0 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Device/MSACDeviceTracker.h @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +@class MSACDevice; + +/** + * Provide and keep track of device log based on collected properties. + */ +@interface MSACDeviceTracker : NSObject + +/** + * Current device log. This will be updated on app launch. + */ +@property(nonatomic, readonly) MSACDevice *device; + +/** + * Returns singleton instance of MSACDeviceTracker. + * + * @return an instance of MSACDeviceTracker. + */ ++ (instancetype)sharedInstance; + +/** + * Clears the device history in memory and in NSUserDefaults keeping the current device. + */ +- (void)clearDevices; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Device/MSACDeviceTracker.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Device/MSACDeviceTracker.m new file mode 100644 index 0000000000..b6b2b03e7e --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Device/MSACDeviceTracker.m @@ -0,0 +1,450 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACDeviceTracker.h" +#import "MSACAppCenterUserDefaults.h" +#import "MSACConstants+Internal.h" +#import "MSACDeviceHistoryInfo.h" +#import "MSACDeviceTrackerPrivate.h" +#import "MSACUtility+Application.h" +#import "MSACUtility+Date.h" +#import "MSACUtility.h" +#import "MSACWrapperSdkInternal.h" + +static NSUInteger const kMSACMaxDevicesHistoryCount = 5; + +@interface MSACDeviceTracker () + +// We need a private setter for the device to avoid the warning that is related to direct access of ivars. +@property(nonatomic) MSACDevice *device; + +@end + +@implementation MSACDeviceTracker : NSObject + +static BOOL needRefresh = YES; +static MSACWrapperSdk *wrapperSdkInformation = nil; +static NSString *overriddenCountryCode = nil; + +/** + * Singleton. + */ +static dispatch_once_t onceToken; +static MSACDeviceTracker *sharedInstance = nil; + +#pragma mark - Initialisation + ++ (instancetype)sharedInstance { + dispatch_once(&onceToken, ^{ + sharedInstance = [[MSACDeviceTracker alloc] init]; + }); + return sharedInstance; +} + ++ (void)resetSharedInstance { + onceToken = 0; + sharedInstance = nil; + + // Reset state of global variables. + // FIXME: move it to shared instance. + needRefresh = YES; + wrapperSdkInformation = nil; + overriddenCountryCode = nil; +} + +- (instancetype)init { + if ((self = [super init])) { + + // Restore past sessions from NSUserDefaults. + NSData *devices = [MSAC_APP_CENTER_USER_DEFAULTS objectForKey:kMSACPastDevicesKey]; + if (devices != nil) { + NSArray *arrayFromData = (NSArray *)[[MSACUtility unarchiveKeyedData:devices] mutableCopy]; + + // If array is not nil, create a mutable version. + if (arrayFromData != nil) { + _deviceHistory = [NSMutableArray arrayWithArray:arrayFromData]; + } + } + + // Create new array and create device info in case we don't have any from disk. + if (_deviceHistory == nil) { + _deviceHistory = [NSMutableArray new]; + } + + // This will instantiate the device property to make sure we have a history. + [self device]; + } + return self; +} + +- (void)setWrapperSdk:(MSACWrapperSdk *)wrapperSdk { + @synchronized(self) { + wrapperSdkInformation = wrapperSdk; + needRefresh = YES; + + // Replace the last device without wrapperSdk in the UserDefaults with an updated info. + [self.deviceHistory removeLastObject]; + [self device]; + } +} + +- (void)setCountryCode:(NSString *)countryCode { + @synchronized(self) { + overriddenCountryCode = countryCode; + needRefresh = YES; + } +} + +- (NSString *)countryCode { + @synchronized(self) { + return overriddenCountryCode; + } +} + +- (MSACWrapperSdk *)wrapperSdk { + @synchronized(self) { + return wrapperSdkInformation; + } +} + ++ (void)refreshDeviceNextTime { + @synchronized([MSACDeviceTracker sharedInstance]) { + needRefresh = YES; + } +} + +/** + * Get the current device log. + */ +- (MSACDevice *)device { + @synchronized(self) { + + // Lazy creation in case the property hasn't been set yet. + if (!_device || needRefresh) { + + // Get new device info. + _device = [self updatedDevice]; + + // Create new MSACDeviceHistoryInfo. + MSACDeviceHistoryInfo *deviceHistoryInfo = [[MSACDeviceHistoryInfo alloc] initWithTimestamp:[NSDate date] andDevice:_device]; + + // Insert new MSACDeviceHistoryInfo at the proper index to keep self.deviceHistory sorted. + NSUInteger newIndex = [self.deviceHistory indexOfObject:deviceHistoryInfo + inSortedRange:(NSRange){0, [self.deviceHistory count]} + options:NSBinarySearchingInsertionIndex + usingComparator:^(MSACDeviceHistoryInfo *a, MSACDeviceHistoryInfo *b) { + return [a.timestamp compare:b.timestamp]; + }]; + [self.deviceHistory insertObject:deviceHistoryInfo atIndex:newIndex]; + + // Remove first (the oldest) item if reached max limit. + if ([self.deviceHistory count] > kMSACMaxDevicesHistoryCount) { + [self.deviceHistory removeObjectAtIndex:0]; + } + + // Persist the device history in NSData format. + [MSAC_APP_CENTER_USER_DEFAULTS setObject:[MSACUtility archiveKeyedData:self.deviceHistory] forKey:kMSACPastDevicesKey]; + } + return _device; + } +} + +/** + * Refresh device properties. + */ +- (MSACDevice *)updatedDevice { + @synchronized(self) { + MSACDevice *newDevice = [MSACDevice new]; +#if TARGET_OS_IOS + CTTelephonyNetworkInfo *telephonyNetworkInfo = [CTTelephonyNetworkInfo new]; + CTCarrier *carrier; + + // The CTTelephonyNetworkInfo.serviceSubscriberCellularProviders method crash because of an issue in iOS 12.0 + // It was fixed in iOS 12.1 + if (@available(iOS 12.1, *)) { + NSDictionary *carriers = [telephonyNetworkInfo serviceSubscriberCellularProviders]; + carrier = [self firstCarrier:carriers]; + } else if (@available(iOS 12, *)) { + NSDictionary *carriers = [telephonyNetworkInfo valueForKey:@"serviceSubscriberCellularProvider"]; + carrier = [self firstCarrier:carriers]; + } + + // Use the old API as fallback if new one doesn't work. + if (carrier == nil) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + carrier = [telephonyNetworkInfo subscriberCellularProvider]; +#pragma clang diagnostic pop + } +#endif + + // Collect device properties. + newDevice.sdkName = [MSACUtility sdkName]; + newDevice.sdkVersion = [MSACUtility sdkVersion]; + newDevice.model = [self deviceModel]; + newDevice.oemName = kMSACDeviceManufacturer; +#if TARGET_OS_OSX || TARGET_OS_MACCATALYST + newDevice.osName = [self osName]; +#else + newDevice.osName = [self osName:MSAC_DEVICE]; +#endif +#if TARGET_OS_OSX + newDevice.osVersion = [self osVersion]; +#else + newDevice.osVersion = [self osVersion:MSAC_DEVICE]; +#endif + newDevice.osBuild = [self osBuild]; + newDevice.locale = [self locale:MSAC_LOCALE]; + newDevice.timeZoneOffset = [self timeZoneOffset:[NSTimeZone localTimeZone]]; + newDevice.screenSize = [self screenSize]; + newDevice.appVersion = [self appVersion:MSAC_APP_MAIN_BUNDLE]; +#if TARGET_OS_IOS + newDevice.carrierCountry = [self carrierCountry:carrier] ?: overriddenCountryCode; + newDevice.carrierName = [self carrierName:carrier]; +#else + + // Carrier information is not available on macOS/tvOS, but if we have an override country code, use it. + newDevice.carrierCountry = overriddenCountryCode; + newDevice.carrierName = nil; +#endif + newDevice.appBuild = [self appBuild:MSAC_APP_MAIN_BUNDLE]; + newDevice.appNamespace = [self appNamespace:MSAC_APP_MAIN_BUNDLE]; + + // Add wrapper SDK information + [self refreshWrapperSdk:newDevice]; + + // Make sure we set the flag to indicate we don't need to update our device info. + needRefresh = NO; + + // Return new device. + return newDevice; + } +} + +/** + * Refresh wrapper SDK properties. + */ +- (void)refreshWrapperSdk:(MSACDevice *)device { + if (wrapperSdkInformation) { + device.wrapperSdkVersion = wrapperSdkInformation.wrapperSdkVersion; + device.wrapperSdkName = wrapperSdkInformation.wrapperSdkName; + device.wrapperRuntimeVersion = wrapperSdkInformation.wrapperRuntimeVersion; + device.liveUpdateReleaseLabel = wrapperSdkInformation.liveUpdateReleaseLabel; + device.liveUpdateDeploymentKey = wrapperSdkInformation.liveUpdateDeploymentKey; + device.liveUpdatePackageHash = wrapperSdkInformation.liveUpdatePackageHash; + } +} + +- (MSACDevice *)deviceForTimestamp:(NSDate *)timestamp { + if (!timestamp || self.deviceHistory.count == 0) { + + // Return a new device in case we don't have a device in our history or timestamp is nil. + return [self device]; + } else { + + // This implements a binary search with complexity O(log n). + MSACDeviceHistoryInfo *find = [[MSACDeviceHistoryInfo alloc] initWithTimestamp:timestamp andDevice:nil]; + NSUInteger index = [self.deviceHistory indexOfObject:find + inSortedRange:NSMakeRange(0, self.deviceHistory.count) + options:NSBinarySearchingFirstEqual | NSBinarySearchingInsertionIndex + usingComparator:^(MSACDeviceHistoryInfo *a, MSACDeviceHistoryInfo *b) { + return [a.timestamp compare:b.timestamp]; + }]; + + /* + * All timestamps are larger. + * For now, the SDK picks up the oldest which is closer to the device info at the crash time. + */ + if (index == 0) { + return self.deviceHistory[0].device; + } + + // All timestamps are smaller. + else if (index == self.deviceHistory.count) { + return [self.deviceHistory lastObject].device; + } + + // [index - 1] should be the right index for the timestamp. + else { + return self.deviceHistory[index - 1].device; + } + } +} + +- (void)clearDevices { + @synchronized(self) { + + // Clear information about the entire history, except for the current device. + if (self.deviceHistory.count > 1) { + [self.deviceHistory removeObjectsInRange:NSMakeRange(0, self.deviceHistory.count - 1)]; + } + + // Clear persistence, but keep the latest information about the device. + [MSAC_APP_CENTER_USER_DEFAULTS setObject:[MSACUtility archiveKeyedData:self.deviceHistory] forKey:kMSACPastDevicesKey]; + } +} + +#pragma mark - Helpers + +- (NSString *)deviceModel { + size_t size; +#if TARGET_OS_OSX || TARGET_OS_MACCATALYST + const char *name = "hw.model"; +#else + const char *name = "hw.machine"; +#endif + sysctlbyname(name, NULL, &size, NULL, 0); + char *answer = (char *)malloc(size); + if (answer == NULL) { + return @"Unknown"; + } + sysctlbyname(name, answer, &size, NULL, 0); + NSString *model = [NSString stringWithCString:answer encoding:NSUTF8StringEncoding]; + free(answer); + return model ? model : @"Unknown"; +} + +#if TARGET_OS_OSX || TARGET_OS_MACCATALYST +- (NSString *)osName { + return @"macOS"; +} +#else +- (NSString *)osName:(UIDevice *)device { + return device.systemName; +} +#endif + +#if TARGET_OS_OSX +- (NSString *)osVersion { + NSString *osVersion = nil; + + if (@available(macOS 10.10, *)) { + NSOperatingSystemVersion osSystemVersion = [[NSProcessInfo processInfo] operatingSystemVersion]; + osVersion = [NSString stringWithFormat:@"%ld.%ld.%ld", (long)osSystemVersion.majorVersion, (long)osSystemVersion.minorVersion, + (long)osSystemVersion.patchVersion]; + } else { + SInt32 major, minor, bugfix; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated" + OSErr err1 = Gestalt(gestaltSystemVersionMajor, &major); + OSErr err2 = Gestalt(gestaltSystemVersionMinor, &minor); + OSErr err3 = Gestalt(gestaltSystemVersionBugFix, &bugfix); + if ((!err1) && (!err2) && (!err3)) { + osVersion = [NSString stringWithFormat:@"%ld.%ld.%ld", (long)major, (long)minor, (long)bugfix]; + } +#pragma clang diagnostic pop + } + return osVersion; +} +#else +- (NSString *)osVersion:(UIDevice *)device { + return device.systemVersion; +} +#endif + +- (NSString *)osBuild { + size_t size; + sysctlbyname("kern.osversion", NULL, &size, NULL, 0); + char *answer = (char *)malloc(size); + if (answer == NULL) { + return @"Unknown"; + } + sysctlbyname("kern.osversion", answer, &size, NULL, 0); + NSString *osBuild = [NSString stringWithCString:answer encoding:NSUTF8StringEncoding]; + free(answer); + return osBuild ? osBuild : @"Unknown"; +} + +- (NSString *)locale:(NSLocale *)currentLocale { + + /* + * [currentLocale objectForKey:NSLocaleIdentifier] will return an alternate language if a language set in system is not supported by + * applications. If system language is set to en_US but an application doesn't support en_US, for example, the OS will return the next + * application supported language in Preferred Language Order list unless there is only one language in the list. The method will return + * the first language in the list to prevent from the above scenario. + * + * In addition to that: + * 1. preferred language returns "-" instead of "_" as a delimiter of language code and country code, the method will concatenate language + * code and country code with "_" and return it. + * 2. some languages can be set without country code so region code can be returned in this case. + * 3. some langugaes have script code which differentiate languages. E.g. zh-Hans and zh-Hant. This is a possible scenario in Apple + * platforms that a locale can be zh_CN for Traditional Chinese. The method will return zh-Hant_CN in this case to make sure system + * language is Traditional Chinese even though region is set to China. + */ + NSLocale *preferredLanguage = [[NSLocale alloc] initWithLocaleIdentifier:[NSLocale preferredLanguages][0]]; + NSString *languageCode = [preferredLanguage objectForKey:NSLocaleLanguageCode]; + NSString *scriptCode = [preferredLanguage objectForKey:NSLocaleScriptCode]; + NSString *countryCode = [preferredLanguage objectForKey:NSLocaleCountryCode]; + NSString *locale = + [NSString stringWithFormat:@"%@%@_%@", languageCode, (scriptCode ? [NSString stringWithFormat:@"-%@", scriptCode] : @""), + countryCode ?: [currentLocale objectForKey:NSLocaleCountryCode]]; + return locale; +} + +- (NSNumber *)timeZoneOffset:(NSTimeZone *)timeZone { + return @([timeZone secondsFromGMT] / 60); +} + +- (NSString *)screenSize { +#if TARGET_OS_OSX + + // Report screen resolution as shown in display settings ('Looks like' field in scaling tab). + NSSize screenSize = [NSScreen mainScreen].frame.size; + return [NSString stringWithFormat:@"%dx%d", (int)screenSize.width, (int)screenSize.height]; +#elif TARGET_OS_MACCATALYST + + // macOS API is not directly avaliable on Mac Catalyst. + NSObject *screen = [NSClassFromString(@"NSScreen") valueForKey:@"mainScreen"]; + if (screen == nil) { + CGSize screenSize = [UIScreen mainScreen].nativeBounds.size; + return [NSString stringWithFormat:@"%dx%d", (int)screenSize.width, (int)screenSize.height]; + } + SEL selector = NSSelectorFromString(@"frame"); + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[[screen class] instanceMethodSignatureForSelector:selector]]; + [invocation setSelector:selector]; + [invocation setTarget:screen]; + [invocation invoke]; + CGRect frame; + [invocation getReturnValue:&frame]; + return [NSString stringWithFormat:@"%dx%d", (int)frame.size.width, (int)frame.size.height]; +#else + CGFloat scale = [UIScreen mainScreen].scale; + CGSize screenSize = [UIScreen mainScreen].bounds.size; + return [NSString stringWithFormat:@"%dx%d", (int)(screenSize.height * scale), (int)(screenSize.width * scale)]; +#endif +} + +#if TARGET_OS_IOS +- (NSString *)carrierName:(CTCarrier *)carrier { + return [self isValidCarrierName:carrier.carrierName] ? carrier.carrierName : nil; +} + +- (NSString *)carrierCountry:(CTCarrier *)carrier { + return ([carrier.isoCountryCode length] > 0) ? carrier.isoCountryCode : nil; +} + +- (BOOL)isValidCarrierName:(NSString *)carrier { + return [carrier length] > 0 && [@"carrier" caseInsensitiveCompare:carrier] != NSOrderedSame; +} + +- (CTCarrier *)firstCarrier:(NSDictionary *)carriers { + for (NSString *key in carriers) { + return carriers[key]; + } + return nil; +} +#endif + +- (NSString *)appVersion:(NSBundle *)appBundle { + return [appBundle infoDictionary][@"CFBundleShortVersionString"]; +} + +- (NSString *)appBuild:(NSBundle *)appBundle { + return [appBundle infoDictionary][@"CFBundleVersion"]; +} + +- (NSString *)appNamespace:(NSBundle *)appBundle { + return [appBundle bundleIdentifier]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Device/MSACDeviceTrackerPrivate.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Device/MSACDeviceTrackerPrivate.h new file mode 100644 index 0000000000..0a86a3180a --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Device/MSACDeviceTrackerPrivate.h @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#ifndef MSAC_DEVICE_TRACKER_PRIVATE_H +#define MSAC_DEVICE_TRACKER_PRIVATE_H + +#if TARGET_OS_IOS +#import +#import +#endif + +#if TARGET_OS_OSX +#import +#else +#import +#endif + +#import + +#import "MSACDeviceInternal.h" +#import "MSACDeviceTracker.h" +#import "MSACWrapperSdk.h" + +/** + * Key for device history. + */ +static NSString *const kMSACPastDevicesKey = @"PastDevices"; + +@class MSACDeviceHistoryInfo; + +@interface MSACDeviceTracker () + +/** + * History of past devices. + */ +@property(nonatomic) NSMutableArray *deviceHistory; + +/** + * Reset singleton instance. + */ ++ (void)resetSharedInstance; + +/** + * Sets a flag that will cause MSACDeviceTracker to update it's device info the next time the device property is accessed. Mostly intended + * for Unit Testing. + */ ++ (void)refreshDeviceNextTime; + +/** + * Get device model. + * + * @return The device model as an NSString. + */ +- (NSString *)deviceModel; + +#if TARGET_OS_OSX || TARGET_OS_MACCATALYST +/** + * Get the OS name. + * + * @return The OS name as an NSString. + */ +- (NSString *)osName; +#else +/** + * Get the OS name. + * + * @param device Current UIDevice. + * + * @return The OS name as an NSString. + */ +- (NSString *)osName:(UIDevice *)device; +#endif + +#if TARGET_OS_OSX +/** + * Get the OS version. + * + * @return The OS version as an NSString. + */ +- (NSString *)osVersion; +#else +/** + * Get the OS version. + * + * @param device Current UIDevice. + * + * @return The OS version as an NSString. + */ +- (NSString *)osVersion:(UIDevice *)device; +#endif + +/** + * Get the device current locale. + * + * @param deviceLocale Device current locale. + * + * @return The device current locale as an NSString. + */ +- (NSString *)locale:(NSLocale *)deviceLocale; + +/** + * Get the device current timezone offset (UTC as reference). + * + * @param timeZone Device timezone. + * + * @return The device current timezone offset as an NSNumber. + */ +- (NSNumber *)timeZoneOffset:(NSTimeZone *)timeZone; + +/** + * Get the rendered screen size. + * + * @return The size of the screen as an NSString with format "HEIGHTxWIDTH". + */ +- (NSString *)screenSize; + +#if TARGET_OS_IOS +/** + * Get the network carrier name. + * + * @param carrier Network carrier. + * + * @return The network carrier name as an NSString. + */ +- (NSString *)carrierName:(CTCarrier *)carrier; + +/** + * Get the network carrier country. + * + * @param carrier Network carrier. + * + * @return The network carrier country as an NSString. + */ +- (NSString *)carrierCountry:(CTCarrier *)carrier; +#endif + +/** + * Get the application version. + * + * @param appBundle Application main bundle. + * + * @return The application version as an NSString. + */ +- (NSString *)appVersion:(NSBundle *)appBundle; + +/** + * Get the application build. + * + * @param appBundle Application main bundle. + * + * @return The application build as an NSString. + */ +- (NSString *)appBuild:(NSBundle *)appBundle; + +/** + * Get the application bundle ID. + * + * @param appBundle Application main bundle. + * + * @return The application bundle ID as an NSString. + */ +- (NSString *)appNamespace:(NSBundle *)appBundle; + +/** + * Set wrapper SDK information to use when building device properties. + * + * @param wrapperSdk wrapper SDK information. + */ +- (void)setWrapperSdk:(MSACWrapperSdk *)wrapperSdk; + +/** + * Set country code to use when building device properties. + * + * @param countryCode The two-letter ISO country code. @see https://www.iso.org/obp/ui/#search for more information. + */ +- (void)setCountryCode:(NSString *)countryCode; + +/** + * Get country code. + * + * @return country code. + */ +- (NSString *)countryCode; + +/** + * Get wrapper SDK. + * + * @return wrapper sdk. + */ +- (MSACWrapperSdk *)wrapperSdk; + +/** + * Return a new Instance of MSACDevice. + * + * @returns A new Instance of MSACDevice. @see MSACDevice + * + * @discussion Intended to be used to update the device-property of MSACDeviceTracker @see MSACDeviceTracker. + */ +- (MSACDevice *)updatedDevice; + +/** + * Return a device from the history of past devices. This will be used e.g. for Crashes after relaunch. + * + * @param timestamp Timestamp that will be used to find a matching MSACDevice in history. + * + * @return Instance of MSACDevice that's closest to timestamp. + * + * @discussion If we cannot find a device that's within the range of the timestamp, the latest device from history will be returned. If + * there is no history, we return the current MSACDevice. + */ +- (MSACDevice *)deviceForTimestamp:(NSDate *)timestamp; + +@end + +#endif diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Session/MSACSessionContext.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Session/MSACSessionContext.h new file mode 100644 index 0000000000..3ebb50b379 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Session/MSACSessionContext.h @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACSessionHistoryInfo.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface MSACSessionContext : NSObject + +/** + * The current session info. + */ +@property(nonatomic) MSACSessionHistoryInfo *currentSessionInfo; + +/** + * The session history that contains session Id and timestamp as an item. + */ +@property(nonatomic) NSMutableArray *sessionHistory; + +/** + * Get singleton instance. + */ ++ (instancetype)sharedInstance; + +/** + * Set current session Id. + * + * @param sessionId The session Id. + */ +- (void)setSessionId:(nullable NSString *)sessionId; + +/** + * Get current session Id. + * + * @return The current session Id. + */ +- (NSString *)sessionId; + +/** + * Get session Id at specific time. + * + * @param date The timestamp for the session. + * + * @return The session Id at the given time. + */ +- (nullable NSString *)sessionIdAt:(NSDate *)date; + +/** + * Clear all session Id history. + * + * @param keepCurrentSession YES to keep current session, NO to delete every entry. + */ +- (void)clearSessionHistoryAndKeepCurrentSession:(BOOL)keepCurrentSession; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Session/MSACSessionContext.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Session/MSACSessionContext.m new file mode 100644 index 0000000000..d75dce9e4f --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Session/MSACSessionContext.m @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAppCenterInternal.h" +#import "MSACLogger.h" +#import "MSACSessionContextPrivate.h" +#import "MSACUtility.h" + +/** + * Storage key for history data. + */ +static NSString *const kMSACSessionIdHistoryKey = @"SessionIdHistory"; + +/** + * Singleton. + */ +static MSACSessionContext *sharedInstance; +static dispatch_once_t onceToken; + +@implementation MSACSessionContext + ++ (instancetype)sharedInstance { + dispatch_once(&onceToken, ^{ + if (sharedInstance == nil) { + sharedInstance = [[MSACSessionContext alloc] init]; + } + }); + return sharedInstance; +} + +- (instancetype)init { + self = [super init]; + if (self) { + NSData *data = [MSAC_APP_CENTER_USER_DEFAULTS objectForKey:kMSACSessionIdHistoryKey]; + if (data != nil) { + _sessionHistory = (NSMutableArray *)[[MSACUtility unarchiveKeyedData:data] mutableCopy]; + } + if (!_sessionHistory) { + _sessionHistory = [NSMutableArray new]; + } + NSUInteger count = [_sessionHistory count]; + MSACLogDebug([MSACAppCenter logTag], @"%tu session(s) in the history.", count); + _currentSessionInfo = [[MSACSessionHistoryInfo alloc] initWithTimestamp:[NSDate date] andSessionId:nil]; + [_sessionHistory addObject:_currentSessionInfo]; + } + return self; +} + ++ (void)resetSharedInstance { + onceToken = 0; + sharedInstance = nil; +} + +- (NSString *)sessionId { + return [self currentSessionInfo].sessionId; +} + +- (void)setSessionId:(nullable NSString *)sessionId { + @synchronized(self) { + [self.sessionHistory removeLastObject]; + self.currentSessionInfo.sessionId = sessionId; + self.currentSessionInfo.timestamp = [NSDate date]; + [self.sessionHistory addObject:self.currentSessionInfo]; + [MSAC_APP_CENTER_USER_DEFAULTS setObject:[MSACUtility archiveKeyedData:self.sessionHistory] forKey:kMSACSessionIdHistoryKey]; + MSACLogVerbose([MSACAppCenter logTag], @"Stored new session with id:%@ and timestamp: %@.", self.currentSessionInfo.sessionId, + self.currentSessionInfo.timestamp); + } +} + +- (nullable NSString *)sessionIdAt:(NSDate *)date { + @synchronized(self) { + for (MSACSessionHistoryInfo *info in [self.sessionHistory reverseObjectEnumerator]) { + if ([info.timestamp compare:date] == NSOrderedAscending) { + return info.sessionId; + } + } + return nil; + } +} + +- (void)clearSessionHistoryAndKeepCurrentSession:(BOOL)keepCurrentSession { + @synchronized(self) { + [self.sessionHistory removeAllObjects]; + if (keepCurrentSession) { + [self.sessionHistory addObject:self.currentSessionInfo]; + } + [MSAC_APP_CENTER_USER_DEFAULTS setObject:[MSACUtility archiveKeyedData:self.sessionHistory] forKey:kMSACSessionIdHistoryKey]; + MSACLogVerbose([MSACAppCenter logTag], @"Cleared old sessions."); + } +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Session/MSACSessionContextPrivate.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Session/MSACSessionContextPrivate.h new file mode 100644 index 0000000000..9aa031be7a --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Session/MSACSessionContextPrivate.h @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACSessionContext.h" + +@interface MSACSessionContext () + +/** + * Reset singleton instance. + */ ++ (void)resetSharedInstance; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Session/MSACSessionHistoryInfo.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Session/MSACSessionHistoryInfo.h new file mode 100644 index 0000000000..25cb7f8dfd --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Session/MSACSessionHistoryInfo.h @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACHistoryInfo.h" + +/** + * Model class that is intended to be used to correlate sessionId to a crash at app relaunch. + */ +@interface MSACSessionHistoryInfo : MSACHistoryInfo + +/** + * Session Id. + */ +@property(nonatomic, copy) NSString *sessionId; + +/** + * Initializes a new `MSACSessionHistoryInfo` instance. + * + * @param timestamp Timestamp. + * @param sessionId Session Id. + */ +- (instancetype)initWithTimestamp:(NSDate *)timestamp andSessionId:(NSString *)sessionId; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Session/MSACSessionHistoryInfo.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Session/MSACSessionHistoryInfo.m new file mode 100644 index 0000000000..0f5a6530df --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/Session/MSACSessionHistoryInfo.m @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACSessionHistoryInfo.h" + +static NSString *const kMSACSessionIdKey = @"sessionIdKey"; + +/** + * This class is used to associate session id with the timestamp that it was created. + */ +@implementation MSACSessionHistoryInfo + +- (instancetype)initWithTimestamp:(NSDate *)timestamp andSessionId:(NSString *)sessionId { + self = [super initWithTimestamp:timestamp]; + if (self) { + _sessionId = sessionId; + } + return self; +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if (self) { + _sessionId = [coder decodeObjectForKey:kMSACSessionIdKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.sessionId forKey:kMSACSessionIdKey]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/UserId/MSACUserIdContext.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/UserId/MSACUserIdContext.h new file mode 100644 index 0000000000..c8094bb5a9 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/UserId/MSACUserIdContext.h @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACUserIdHistoryInfo.h" + +NS_ASSUME_NONNULL_BEGIN + +@protocol MSACUserIdContextDelegate; + +@interface MSACUserIdContext : NSObject + +/** + * The current userId info. + */ +@property(nonatomic) MSACUserIdHistoryInfo *currentUserIdInfo; + +/** + * The user Id history that contains user Id and timestamp as an item. + */ +@property(nonatomic) NSMutableArray *userIdHistory; + +/** + * Hash table containing all the delegates as weak references. + */ +@property(nonatomic) NSHashTable> *delegates; + +/** + * Get singleton instance. + */ ++ (instancetype)sharedInstance; + +/** + * Add a delegate. This method is thread safe. + * + * @param delegate A delegate. + */ +- (void)addDelegate:(id)delegate; + +/** + * Remove a delegate. This method is thread safe. + * + * @param delegate A delegate. + */ +- (void)removeDelegate:(id)delegate; + +/** + * Set current user Id. + * + * @param userId The user Id. + */ +- (void)setUserId:(nullable NSString *)userId; + +/** + * Get current user Id. + * + * @return The current user Id. + */ +- (NSString *)userId; + +/** + * Get user Id at specific time. + * + * @param date The timestamp for the user Id. + * + * @return The user Id at the given time. + */ +- (nullable NSString *)userIdAt:(NSDate *)date; + +/** + * Clear all user Id history. + */ +- (void)clearUserIdHistory; + +/** + * Check if userId is valid for App Center. + * + * @param userId The user Id. + * + * @return YES if valid, NO otherwise. + */ ++ (BOOL)isUserIdValidForAppCenter:(nullable NSString *)userId; + +/** + * Check if userId is valid for One Collector. + * + * @param userId The user Id. + * + * @return YES if valid, NO otherwise. + */ ++ (BOOL)isUserIdValidForOneCollector:(nullable NSString *)userId; + +/** + * Add 'c:' prefix to userId if the userId has no prefix. + * + * @param userId userId. + * + * @return prefixed userId or null if the userId was null. + */ ++ (nullable NSString *)prefixedUserIdFromUserId:(nullable NSString *)userId; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/UserId/MSACUserIdContext.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/UserId/MSACUserIdContext.m new file mode 100644 index 0000000000..2bf200c86e --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/UserId/MSACUserIdContext.m @@ -0,0 +1,178 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAppCenterInternal.h" +#import "MSACConstants+Internal.h" +#import "MSACLogger.h" +#import "MSACUserIdContextDelegate.h" +#import "MSACUserIdContextPrivate.h" +#import "MSACUtility.h" + +/** + * Maximum allowed length for user identifier for App Center server. + */ +static const int kMSACMaxUserIdLength = 256; + +/* + * Custom User ID prefix for Common Schema. + */ +static NSString *const kMSACUserIdCustomPrefix = @"c"; + +/** + * User Id history key. + */ +static NSString *const kMSACUserIdHistoryKey = @"UserIdHistory"; + +/** + * Singleton. + */ +static MSACUserIdContext *sharedInstance; +static dispatch_once_t onceToken; + +@implementation MSACUserIdContext + ++ (instancetype)sharedInstance { + dispatch_once(&onceToken, ^{ + if (sharedInstance == nil) { + sharedInstance = [[MSACUserIdContext alloc] init]; + } + }); + return sharedInstance; +} + +- (instancetype)init { + self = [super init]; + if (self) { + NSData *data = [MSAC_APP_CENTER_USER_DEFAULTS objectForKey:kMSACUserIdHistoryKey]; + if (data != nil) { + _userIdHistory = (NSMutableArray *)[[MSACUtility unarchiveKeyedData:data] mutableCopy]; + } + if (!_userIdHistory) { + _userIdHistory = [NSMutableArray new]; + } + NSUInteger count = [_userIdHistory count]; + MSACLogDebug([MSACAppCenter logTag], @"%tu userId(s) in the history.", count); + + // Set nil to current userId so that it can return nil for the userId between App Center start and setUserId call. + _currentUserIdInfo = [[MSACUserIdHistoryInfo alloc] initWithTimestamp:[NSDate date] andUserId:nil]; + [_userIdHistory addObject:_currentUserIdInfo]; + + /* + * Persist nil userId as a current userId to NSUserDefaults so that Crashes can retrieve a correct userId when apps crash between App + * Center start and setUserId call. + */ + [MSAC_APP_CENTER_USER_DEFAULTS setObject:[MSACUtility archiveKeyedData:self.userIdHistory] forKey:kMSACUserIdHistoryKey]; + _delegates = [NSHashTable weakObjectsHashTable]; + } + return self; +} + ++ (void)resetSharedInstance { + onceToken = 0; + sharedInstance = nil; +} + +- (NSString *)userId { + return [self currentUserIdInfo].userId; +} + +- (void)setUserId:(nullable NSString *)userId { + NSArray *synchronizedDelegates; + @synchronized(self) { + BOOL sameUserId = + (!userId && !self.currentUserIdInfo.userId) || (userId && [self.currentUserIdInfo.userId isEqualToString:(NSString *)userId]); + if (sameUserId) { + return; + } + self.currentUserIdInfo.timestamp = [NSDate date]; + self.currentUserIdInfo.userId = userId; + + /* + * Replacing the last userId from history because the userId has changed within a same lifecycle without crashes. + * The userId history is only used to correlate a crashes log with a userId, previous userId won't be used at all since there is no + * crashes on apps between previous userId and current userId. + */ + [self.userIdHistory removeLastObject]; + [self.userIdHistory addObject:self.currentUserIdInfo]; + [MSAC_APP_CENTER_USER_DEFAULTS setObject:[MSACUtility archiveKeyedData:self.userIdHistory] forKey:kMSACUserIdHistoryKey]; + MSACLogVerbose([MSACAppCenter logTag], @"Stored new userId:%@ and timestamp: %@.", self.currentUserIdInfo.userId, + self.currentUserIdInfo.timestamp); + synchronizedDelegates = [self.delegates allObjects]; + } + for (id delegate in synchronizedDelegates) { + if ([delegate respondsToSelector:@selector(userIdContext:didUpdateUserId:)]) { + [delegate userIdContext:self didUpdateUserId:userId]; + } + } +} + +- (nullable NSString *)userIdAt:(NSDate *)date { + @synchronized(self) { + for (MSACUserIdHistoryInfo *info in [self.userIdHistory reverseObjectEnumerator]) { + if ([info.timestamp compare:date] == NSOrderedAscending) { + return info.userId; + } + } + return nil; + } +} + +- (void)clearUserIdHistory { + @synchronized(self) { + [self.userIdHistory removeAllObjects]; + [self.userIdHistory addObject:self.currentUserIdInfo]; + [MSAC_APP_CENTER_USER_DEFAULTS setObject:[MSACUtility archiveKeyedData:self.userIdHistory] forKey:kMSACUserIdHistoryKey]; + MSACLogVerbose([MSACAppCenter logTag], @"Cleared old userIds while keeping current userId."); + } +} + ++ (BOOL)isUserIdValidForAppCenter:(nullable NSString *)userId { + if (userId && userId.length > kMSACMaxUserIdLength) { + MSACLogError([MSACAppCenter logTag], @"userId is limited to %d characters.", kMSACMaxUserIdLength); + return NO; + } + return YES; +} + ++ (BOOL)isUserIdValidForOneCollector:(nullable NSString *)userId { + if (!userId) { + return YES; + } + NSRange separator = [userId rangeOfString:kMSACCommonSchemaPrefixSeparator]; + if (userId.length == 0) { + MSACLogError([MSACAppCenter logTag], @"userId must not be empty."); + return NO; + } + if (separator.location != NSNotFound) { + NSString *prefix = [userId substringToIndex:separator.location]; + if (![prefix isEqualToString:kMSACUserIdCustomPrefix]) { + MSACLogError([MSACAppCenter logTag], @"userId prefix must be '%@', '%@' is not supported.", kMSACUserIdCustomPrefix, prefix); + return NO; + } else if (separator.location == userId.length - 1) { + MSACLogError([MSACAppCenter logTag], @"userId must not be empty."); + return NO; + } + } + return YES; +} + ++ (nullable NSString *)prefixedUserIdFromUserId:(nullable NSString *)userId { + if (userId && [userId rangeOfString:kMSACCommonSchemaPrefixSeparator].location == NSNotFound) { + return [NSString stringWithFormat:@"%@%@%@", kMSACUserIdCustomPrefix, kMSACCommonSchemaPrefixSeparator, userId]; + } + return userId; +} + +- (void)addDelegate:(id)delegate { + @synchronized(self) { + [self.delegates addObject:delegate]; + } +} + +- (void)removeDelegate:(id)delegate { + @synchronized(self) { + [self.delegates removeObject:delegate]; + } +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/UserId/MSACUserIdContextDelegate.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/UserId/MSACUserIdContextDelegate.h new file mode 100644 index 0000000000..06177dbcfd --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/UserId/MSACUserIdContextDelegate.h @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +NS_ASSUME_NONNULL_BEGIN + +@class MSACUserIdContext; + +@protocol MSACUserIdContextDelegate + +/** + * A callback that is called after a new userId is set. + * + * @param userIdContext userId context. + * @param userId userId which was set. + */ +- (void)userIdContext:(MSACUserIdContext *)userIdContext didUpdateUserId:(nullable NSString *)userId; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/UserId/MSACUserIdContextPrivate.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/UserId/MSACUserIdContextPrivate.h new file mode 100644 index 0000000000..49daf8302f --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/UserId/MSACUserIdContextPrivate.h @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACUserIdContext.h" + +@interface MSACUserIdContext () + +/** + * Reset singleton instance. + */ ++ (void)resetSharedInstance; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/UserId/MSACUserIdHistoryInfo.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/UserId/MSACUserIdHistoryInfo.h new file mode 100644 index 0000000000..b538846769 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/UserId/MSACUserIdHistoryInfo.h @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACHistoryInfo.h" + +/** + * Model class that is intended to be used to correlate userId to a crash at app relaunch. + */ +@interface MSACUserIdHistoryInfo : MSACHistoryInfo + +/** + * User Id. + */ +@property(nonatomic, copy) NSString *userId; + +/** + * Initializes a new `MSACUserIdHistoryInfo` instance. + * + * @param timestamp Timestamp. + * @param userId User Id. + */ +- (instancetype)initWithTimestamp:(NSDate *)timestamp andUserId:(NSString *)userId; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/UserId/MSACUserIdHistoryInfo.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/UserId/MSACUserIdHistoryInfo.m new file mode 100644 index 0000000000..f92efdd21e --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Context/UserId/MSACUserIdHistoryInfo.m @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACUserIdHistoryInfo.h" + +static NSString *const kMSACUserIdKey = @"userIdKey"; + +/** + * This class is used to associate user id with the timestamp that it was created. + */ +@implementation MSACUserIdHistoryInfo + +- (instancetype)initWithTimestamp:(NSDate *)timestamp andUserId:(NSString *)userId { + self = [super initWithTimestamp:timestamp]; + if (self) { + _userId = userId; + } + return self; +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if (self) { + _userId = [coder decodeObjectForKey:kMSACUserIdKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.userId forKey:kMSACUserIdKey]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/DelegateForwarder/MSACAppDelegateForwarder.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/DelegateForwarder/MSACAppDelegateForwarder.h new file mode 100644 index 0000000000..5a035ae7c8 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/DelegateForwarder/MSACAppDelegateForwarder.h @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACCustomApplicationDelegate.h" +#import "MSACDelegateForwarder.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const kMSACAppDelegateForwarderEnabledKey = @"AppCenterAppDelegateForwarderEnabled"; + +@interface MSACAppDelegateForwarder : MSACDelegateForwarder + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/DelegateForwarder/MSACAppDelegateForwarder.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/DelegateForwarder/MSACAppDelegateForwarder.m new file mode 100644 index 0000000000..852ccdc87d --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/DelegateForwarder/MSACAppDelegateForwarder.m @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAppDelegateForwarder.h" +#import "MSACCustomApplicationDelegate.h" +#import "MSACUtility+Application.h" + +// Original selectors with special handling. +static NSString *const kMSACOpenURLSourceApplicationAnnotation = @"application:openURL:sourceApplication:annotation:"; +static NSString *const kMSACOpenURLOptions = @"application:openURL:options:"; + +// Singleton instance. +static MSACAppDelegateForwarder *sharedInstance = nil; +static dispatch_once_t swizzlingOnceToken; + +@implementation MSACAppDelegateForwarder + ++ (void)load { + + /* + * The application starts querying its delegate for its implementation as soon as it is set then may never query again. It means that if + * the application delegate doesn't implement an optional method of the `UIApplicationDelegate` protocol at that time then that method may + * never be called even if added later via swizzling. This is why the application delegate swizzling should happen at the time it is set + * to the application object. + */ + [[MSACAppDelegateForwarder sharedInstance] setEnabledFromPlistForKey:kMSACAppDelegateForwarderEnabledKey]; +} + +- (instancetype)init { + if ((self = [super init])) { +#if !TARGET_OS_OSX && !TARGET_OS_MACCATALYST + self.deprecatedSelectors = @{kMSACOpenURLOptions : kMSACOpenURLSourceApplicationAnnotation}; +#endif + } + return self; +} + ++ (instancetype)sharedInstance { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [self new]; + }); + return sharedInstance; +} + ++ (void)resetSharedInstance { + sharedInstance = [self new]; +} + +- (Class)originalClassForSetDelegate { + return [MSACApplication class]; +} + +- (dispatch_once_t *)swizzlingOnceToken { + return &swizzlingOnceToken; +} + +#pragma mark - Custom Application + +- (void)custom_setDelegate:(id)delegate { + + // Swizzle only once. + static dispatch_once_t delegateSwizzleOnceToken; + dispatch_once(&delegateSwizzleOnceToken, ^{ + // Swizzle the delegate object before it's actually set. + [[MSACAppDelegateForwarder sharedInstance] swizzleOriginalDelegate:delegate]; + }); + + // Forward to the original `setDelegate:` implementation. + IMP originalImp = [MSACAppDelegateForwarder sharedInstance].originalSetDelegateImp; + if (originalImp) { + ((void (*)(id, SEL, id))originalImp)(self, _cmd, delegate); + } +} + +#pragma mark - Custom UIApplicationDelegate + +#if !TARGET_OS_OSX && !TARGET_OS_MACCATALYST + +/* + * Those methods will never get called but their implementation will be used by swizzling. Those implementations will run within the + * delegate context. Meaning that `self` will point to the original app delegate and not this forwarder. + */ +- (BOOL)custom_application:(UIApplication *)application + openURL:(NSURL *)url + sourceApplication:(nullable NSString *)sourceApplication + annotation:(id)annotation { + BOOL result = NO; + IMP originalImp = NULL; + + // Forward to the original delegate. + [[MSACAppDelegateForwarder sharedInstance].originalImplementations[NSStringFromSelector(_cmd)] getValue:&originalImp]; + if (originalImp) { + result = ((BOOL(*)(id, SEL, UIApplication *, NSURL *, NSString *, id))originalImp)(self, _cmd, application, url, sourceApplication, + annotation); + } + + // Forward to custom delegates. + return [[MSACAppDelegateForwarder sharedInstance] application:application + openURL:url + sourceApplication:sourceApplication + annotation:annotation + returnedValue:result]; +} + +- (BOOL)custom_application:(UIApplication *)application + openURL:(nonnull NSURL *)url + options:(nonnull NSDictionary *)options { + BOOL result = NO; + IMP originalImp = NULL; + + // Forward to the original delegate. + [[MSACAppDelegateForwarder sharedInstance].originalImplementations[NSStringFromSelector(_cmd)] getValue:&originalImp]; + if (originalImp) { + result = ((BOOL(*)(id, SEL, UIApplication *, NSURL *, NSDictionary *))originalImp)( + self, _cmd, application, url, options); + } + + // Forward to custom delegates. + return [[MSACAppDelegateForwarder sharedInstance] application:application openURL:url options:options returnedValue:result]; +} +#endif + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/DelegateForwarder/MSACAppDelegateUtil.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/DelegateForwarder/MSACAppDelegateUtil.h new file mode 100644 index 0000000000..f6d44a4a41 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/DelegateForwarder/MSACAppDelegateUtil.h @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#if TARGET_OS_OSX +#import + +#ifndef MSACApplicationDelegate +#define MSACApplicationDelegate NSApplicationDelegate +#endif + +#ifndef MSACApplication +#define MSACApplication NSApplication +#endif +#else +#import + +#ifndef MSACApplicationDelegate +#define MSACApplicationDelegate UIApplicationDelegate +#endif + +#ifndef MSACApplication +#define MSACApplication UIApplication +#endif + +#endif diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/DelegateForwarder/MSACCustomApplicationDelegate.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/DelegateForwarder/MSACCustomApplicationDelegate.h new file mode 100644 index 0000000000..c801c9ccdc --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/DelegateForwarder/MSACCustomApplicationDelegate.h @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAppDelegateUtil.h" +#import "MSACCustomDelegate.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Custom delegate matching @c UIApplicationDelegate. + * + * @discussion Delegates here are using swizzling. Any delegate that can be registered through the notification center should not be + * registered through swizzling. Due to the early registration of swizzling on the original app delegate each custom delegate must sign up + * for selectors to swizzle within the `load` method of a category over the @c MSACAppDelegateForwarder class. + */ +@protocol MSACCustomApplicationDelegate + +@optional + +#if !TARGET_OS_OSX && !TARGET_OS_MACCATALYST + +/** + * Asks the delegate to open a resource specified by a URL, and provides a dictionary of launch options. + * + * @param application The singleton app object. + * @param url The URL resource to open. This resource can be a network resource or a file. + * @param sourceApplication The bundle ID of the app that is requesting your app to open the URL (url). + * @param annotation A Property list supplied by the source app to communicate information to the receiving app. + * @param returnedValue Value returned by the original delegate implementation. + * + * @return `YES` if the delegate successfully handled the request or `NO` if the attempt to open the URL resource failed. + */ +- (BOOL)application:(UIApplication *)application + openURL:(NSURL *)url + sourceApplication:(nullable NSString *)sourceApplication + annotation:(id)annotation + returnedValue:(BOOL)returnedValue; + +/** + * Asks the delegate to open a resource specified by a URL, and provides a dictionary of launch options. + * + * @param application The singleton app object. + * @param url The URL resource to open. This resource can be a network resource or a file. + * @param options A dictionary of URL handling options. For information about the possible keys in this dictionary and how to handle them, + * @see UIApplicationOpenURLOptionsKey. By default, the value of this parameter is an empty dictionary. + * @param returnedValue Value returned by the original delegate implementation. + * + * @return `YES` if the delegate successfully handled the request or `NO` if the attempt to open the URL resource failed. + */ +- (BOOL)application:(UIApplication *)application + openURL:(NSURL *)url + options:(NSDictionary *)options + returnedValue:(BOOL)returnedValue; + +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/DelegateForwarder/MSACCustomDelegate.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/DelegateForwarder/MSACCustomDelegate.h new file mode 100644 index 0000000000..795593f7aa --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/DelegateForwarder/MSACCustomDelegate.h @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +NS_ASSUME_NONNULL_BEGIN + +/** + * Custom delegate. + * + * @discussion Delegates here are using swizzling. Any delegate that can be registered through the notification center should not be + * registered through swizzling. Due to the early registration of swizzling on the original app delegate each custom delegate must sign up + * for selectors to swizzle within the `load` method of a category over the @see MSACDelegateForwarder class. + */ +@protocol MSACCustomDelegate +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/DelegateForwarder/MSACDelegateForwarder.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/DelegateForwarder/MSACDelegateForwarder.h new file mode 100644 index 0000000000..7a4071b740 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/DelegateForwarder/MSACDelegateForwarder.h @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol MSACCustomDelegate; + +/** + * Enum used to represent all kind of executors running a completion handler. + */ +typedef NS_OPTIONS(NSUInteger, MSACCompletionExecutor) { + MSACCompletionExecutorNone = (1 << 0), + MSACCompletionExecutorOriginal = (1 << 1), + MSACCompletionExecutorCustom = (1 << 2), + MSACCompletionExecutorForwarder = (1 << 3) +}; + +@interface MSACDelegateForwarder : NSObject + +/** + * Enable/Disable Application forwarding. + */ +@property(atomic, getter=isEnabled) BOOL enabled; + +/** + * Hash table containing all the delegates as weak references. + */ +@property(nonatomic) NSHashTable> *delegates; + +/** + * Hold the original setDelegate implementation. + */ +@property(nonatomic) IMP originalSetDelegateImp; + +// TODO SEL can be stored as NSValue in dictionaries for a better efficiency. +/** + * Keep track of the original delegate's method implementations. + */ +@property(nonatomic, readonly) NSMutableDictionary *originalImplementations; + +/** + * Dictionary of deprecated original selectors indexed by their new equivalent. + */ +@property(nonatomic) NSDictionary *deprecatedSelectors; + +/** + * Return the singleton instance of a delegate forwarder. + * + * @return The delegate forwarder instance. + * + * @discussion This method is abstract and needs to be overwritten by subclasses. + */ ++ (nullable instancetype)sharedInstance; + +/** + * Register swizzling for the given original application delegate. + * + * @param originalDelegate The original application delegate. + */ +- (void)swizzleOriginalDelegate:(NSObject *)originalDelegate; + +/** + * Add a delegate. This method is thread safe. + * + * @param delegate A delegate. + */ +- (void)addDelegate:(id)delegate; + +/** + * Remove a delegate. This method is thread safe. + * + * @param delegate A delegate. + */ +- (void)removeDelegate:(id)delegate; + +/** + * Add an app delegate selector to swizzle. + * + * @param selector An app delegate selector to swizzle. + * + * @discussion Due to the early registration of swizzling on the original app delegate each custom delegate must sign up for selectors to + * swizzle within the @c load method of a category over the @see MSACAppDelegateForwarder class. + */ +- (void)addDelegateSelectorToSwizzle:(SEL)selector; + +/** + * Flush debugging traces accumulated until now. + */ ++ (void)flushTraceBuffer; + +/** + * Set the enabled state from the application plist file. + * + * @param plistKey Plist key for the forwarder enabled state. + */ +- (void)setEnabledFromPlistForKey:(NSString *)plistKey; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/DelegateForwarder/MSACDelegateForwarder.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/DelegateForwarder/MSACDelegateForwarder.m new file mode 100644 index 0000000000..8757be23ba --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/DelegateForwarder/MSACDelegateForwarder.m @@ -0,0 +1,300 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAppCenterInternal.h" +#import "MSACCustomDelegate.h" +#import "MSACDelegateForwarder.h" +#import "MSACDelegateForwarderPrivate.h" +#import "MSACLogger.h" + +static NSString *const kMSACCustomSelectorPrefix = @"custom_"; +static NSString *const kMSACReturnedValueSelectorPart = @"returnedValue:"; + +// A buffer containing all the console logs that couldn't be printed yet. +static NSMutableArray *traceBuffer = nil; + +@implementation MSACDelegateForwarder + +@synthesize enabled = _enabled; + ++ (void)load { + traceBuffer = [NSMutableArray new]; +} + +- (instancetype)init { + if ((self = [super init])) { + _delegates = [NSHashTable weakObjectsHashTable]; + _selectorsToSwizzle = [NSMutableSet new]; + _originalImplementations = [NSMutableDictionary new]; + _enabled = YES; + } + return self; +} + ++ (instancetype)sharedInstance { + + // This is an empty method expected to be overridden in sub classes. + return nil; +} + ++ (void)resetSharedInstance { + + // This is an empty method expected to be overridden in sub classes. +} + ++ (NSString *)enabledKey { + + // This is an empty method expected to be overridden in sub classes. + return nil; +} + +- (Class)originalClassForSetDelegate { + + // This is an empty method expected to be overridden in sub classes. + return nil; +} + +- (dispatch_once_t *)swizzlingOnceToken { + + // This is an empty method expected to be overridden in sub classes. + return nil; +} + +#pragma mark - Custom Application + +/** + * Custom implementation of the setDelegate: method. + * + * @param delegate The delegate to be swizzled, its type here is @c id to be generic but your implementation will have to declare + * the exact type of the expected delegate (i.e.: @c MSACApplicationDelegate). + * + * @discussion Beware, @c self in this method is not the current class but the swizzled class. + */ +- (void)custom_setDelegate:(__unused id)delegate { + + // This is an empty method expected to be overridden in sub classes. +} + +#pragma mark - Logging + +- (void)addTraceBlock:(void (^)(void))block { + @synchronized(traceBuffer) { + if (traceBuffer) { + static dispatch_once_t onceToken = 0; + dispatch_once(&onceToken, ^{ + [traceBuffer addObject:^{ + MSACLogVerbose([MSACAppCenter logTag], @"Start buffering traces."); + }]; + }); + [traceBuffer addObject:block]; + } else { + block(); + } + } +} + ++ (void)flushTraceBuffer { + if (traceBuffer) { + @synchronized(traceBuffer) { + for (dispatch_block_t traceBlock in traceBuffer) { + traceBlock(); + } + [traceBuffer removeAllObjects]; + traceBuffer = nil; + MSACLogVerbose([MSACAppCenter logTag], @"Stop buffering traces, flushed."); + } + } +} + +#pragma mark - Swizzling + +- (void)addDelegateSelectorToSwizzle:(SEL)selector { + if (self.enabled) { + + // Swizzle only once and only if needed. No selector to swizzle then no swizzling at all. + dispatch_once([self swizzlingOnceToken], ^{ + self.originalSetDelegateImp = [self swizzleOriginalSelector:@selector(setDelegate:) + withCustomSelector:@selector(custom_setDelegate:) + originalClass:[self originalClassForSetDelegate]]; + }); + [self.selectorsToSwizzle addObject:NSStringFromSelector(selector)]; + } +} + +- (void)swizzleOriginalDelegate:(NSObject *)originalDelegate { + IMP originalImp = NULL; + Class delegateClass = [originalDelegate class]; + SEL originalSelector, customSelector; + + // Swizzle all registered selectors. + for (NSString *selectorString in self.selectorsToSwizzle) { + originalSelector = NSSelectorFromString(selectorString); + customSelector = NSSelectorFromString([kMSACCustomSelectorPrefix stringByAppendingString:selectorString]); + originalImp = [self swizzleOriginalSelector:originalSelector withCustomSelector:customSelector originalClass:delegateClass]; + if (originalImp) { + + // Save the original implementation for later use. + self.originalImplementations[selectorString] = [NSValue valueWithBytes:&originalImp objCType:@encode(IMP)]; + } + } + [self.selectorsToSwizzle removeAllObjects]; +} + +- (IMP)swizzleOriginalSelector:(SEL)originalSelector withCustomSelector:(SEL)customSelector originalClass:(Class)originalClass { + + // Replace original implementation + NSString *originalSelectorStr = NSStringFromSelector(originalSelector); + Method originalMethod = class_getInstanceMethod(originalClass, originalSelector); + IMP customImp = class_getMethodImplementation([self class], customSelector); + IMP originalImp = NULL; + BOOL methodAdded = NO; + BOOL skipped = NO; + NSString *warningMsg; + NSString *remediationMsg = @"You need to explicitly call the App Center API from your app delegate implementation."; + + // Replace original implementation by the custom one. + if (originalMethod) { + originalImp = method_setImplementation(originalMethod, customImp); + } else if (![originalClass instancesRespondToSelector:originalSelector]) { + + // Check for deprecation. + NSString *deprecatedSelectorStr = self.deprecatedSelectors[originalSelectorStr]; + if (deprecatedSelectorStr && [originalClass instancesRespondToSelector:NSSelectorFromString(deprecatedSelectorStr)]) { + + // An implementation for the deprecated selector exists. Don't add the new method, it might eclipse the original implementation. + warningMsg = [NSString + stringWithFormat:@"No implementation found for this selector, though an implementation of its deprecated API '%@' exists.", + deprecatedSelectorStr]; + } else { + + // Skip this selector if it's deprecated and doesn't have an implementation. + if ([self.deprecatedSelectors.allValues containsObject:originalSelectorStr]) { + skipped = YES; + } else { + + /* + * The original class may not implement the selector (e.g.: optional method from protocol), add the method to the original class and + * associate it with the custom implementation. + */ + Method customMethod = class_getInstanceMethod([self class], customSelector); + methodAdded = class_addMethod(originalClass, originalSelector, customImp, method_getTypeEncoding(customMethod)); + } + } + } + + /* + * If class instances respond to the selector but no implementation is found it's likely that the original class is doing message + * forwarding, in this case we can't add our implementation to the class or we will break the forwarding. + */ + + // Validate swizzling. + if (!skipped) { + if (!originalImp && !methodAdded) { + [self addTraceBlock:^{ + NSString *message = [NSString stringWithFormat:@"Cannot swizzle selector '%@' of class '%@'.", originalSelectorStr, originalClass]; + if (warningMsg) { + MSACLogWarning([MSACAppCenter logTag], @"%@ %@", message, warningMsg); + } else { + MSACLogError([MSACAppCenter logTag], @"%@ %@", message, remediationMsg); + } + }]; + } else { + [self addTraceBlock:^{ + MSACLogDebug([MSACAppCenter logTag], @"Selector '%@' of class '%@' is swizzled.", originalSelectorStr, originalClass); + }]; + } + } + return originalImp; +} + +#pragma mark - Forwarding + +- (void)forwardInvocation:(NSInvocation *)invocation { + @synchronized([self class]) { + BOOL forwarded = NO; + BOOL hasReturnedValue = ([NSStringFromSelector(invocation.selector) hasSuffix:kMSACReturnedValueSelectorPart]); + NSUInteger returnedValueIdx = 0; + void *returnedValuePtr = NULL; + + // Prepare returned value if any. + if (hasReturnedValue) { + + // Returned value argument is always the last one. + returnedValueIdx = invocation.methodSignature.numberOfArguments - 1; + returnedValuePtr = malloc(invocation.methodSignature.methodReturnLength); + } + + // Forward to delegates executing a custom method. + for (id delegate in self.delegates) { + if ([delegate respondsToSelector:invocation.selector]) { + [invocation invokeWithTarget:delegate]; + + // Chaining return values. + if (hasReturnedValue) { + [invocation getReturnValue:returnedValuePtr]; + [invocation setArgument:returnedValuePtr atIndex:returnedValueIdx]; + } + forwarded = YES; + } + } + + // Forward back the original return value if no delegates to receive the message. + if (hasReturnedValue && !forwarded) { + [invocation getArgument:returnedValuePtr atIndex:returnedValueIdx]; + [invocation setReturnValue:returnedValuePtr]; + } + free(returnedValuePtr); + } +} + +#pragma mark - Delegates + +- (void)addDelegate:(id)delegate { + @synchronized(self) { + if (self.enabled) { + [self.delegates addObject:delegate]; + } + } +} + +- (void)removeDelegate:(id)delegate { + @synchronized(self) { + if (self.enabled) { + [self.delegates removeObject:delegate]; + } + } +} + +#pragma mark - Other + +- (void)setEnabledFromPlistForKey:(NSString *)plistKey { + NSNumber *forwarderEnabledNum = [NSBundle.mainBundle objectForInfoDictionaryKey:plistKey]; + BOOL forwarderEnabled = forwarderEnabledNum ? [forwarderEnabledNum boolValue] : YES; + self.enabled = forwarderEnabled; + if (self.enabled) { + [self addTraceBlock:^{ + MSACLogDebug([MSACAppCenter logTag], @"Delegate forwarder for info.plist key '%@' enabled. It may use swizzling.", plistKey); + }]; + } else { + [self addTraceBlock:^{ + MSACLogDebug([MSACAppCenter logTag], @"Delegate forwarder for info.plist key '%@' disabled. It won't use swizzling.", plistKey); + }]; + } +} + +- (BOOL)isEnabled { + return _enabled; +} + +- (void)setEnabled:(BOOL)enabled { + @synchronized(self) { + _enabled = enabled; + if (!enabled) { + [self.delegates removeAllObjects]; + } + } +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/DelegateForwarder/MSACDelegateForwarderPrivate.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/DelegateForwarder/MSACDelegateForwarderPrivate.h new file mode 100644 index 0000000000..b0a8b950c4 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/DelegateForwarder/MSACDelegateForwarderPrivate.h @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACDelegateForwarder.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface MSACDelegateForwarder () + +/** + * Keep track of original selectors to swizzle. + */ +@property(nonatomic, readonly) NSMutableSet *selectorsToSwizzle; + +/** + * Only used by tests to reset the singleton instance. + */ ++ (void)resetSharedInstance; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/HttpClient/MSACHttpCall.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/HttpClient/MSACHttpCall.h new file mode 100644 index 0000000000..cc241f191c --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/HttpClient/MSACHttpCall.h @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACHttpClientProtocol.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface MSACHttpCall : NSObject + +/** + * Request body. + */ +@property(nonatomic, nullable) NSData *data; + +/** + * Request headers. + */ +@property(nonatomic) NSDictionary *headers; + +/** + * Request URL. + */ +@property(nonatomic) NSURL *url; + +/** + * HTTP method. + */ +@property(nonatomic, copy) NSString *method; + +/** + * Call completion handler used for communicating with calling component. + */ +@property(nonatomic) MSACHttpRequestCompletionHandler completionHandler; + +/** + * A timer source which is used to flush the queue after a certain amount of time. + */ +@property(nonatomic) dispatch_source_t timerSource; + +/** + * Number of retries performed for this call. + */ +@property(nonatomic) int retryCount; + +/** + * Retry intervals for each retry. + */ +@property(nonatomic) NSArray *retryIntervals; + +/** + * Indicates if the call is currently being sent or awaiting a response. + */ +@property(atomic, getter=isInProgress) BOOL inProgress; + +/** + * Initialize a call with specified retry intervals. + * + * @param url The endpoint to use in the HTTP request. + * @param method The HTTP method (verb) to use for the HTTP request (e.g. GET, POST, etc.). + * @param headers HTTP headers. + * @param data A data instance that will be transformed request body. + * @param retryIntervals Retry intervals used in case of recoverable errors. + * @param compressionEnabled Enable or disable payload compression. + * @param completionHandler Completion handler. + * + * @return A retriable call instance. + */ +- (instancetype)initWithUrl:(NSURL *)url + method:(NSString *)method + headers:(nullable NSDictionary *)headers + data:(nullable NSData *)data + retryIntervals:(NSArray *)retryIntervals + compressionEnabled:(BOOL)compressionEnabled + completionHandler:(MSACHttpRequestCompletionHandler)completionHandler; + +/** + * Start the retry timer and invoke a callback after. + * + * @param statusCode The status code that the call received. + * @param retryAfter If this is not nil, the retry intervals will be ignored the next time and the value passed will be used instead. Unit + * is milliseconds. + * @param event The callback to be invoked after the timer. + */ +- (void)startRetryTimerWithStatusCode:(NSUInteger)statusCode retryAfter:(nullable NSNumber *)retryAfter event:(dispatch_block_t)event; + +/** + * Indicate if the limit of maximum retries has been reached. + * + * @return YES if the limit of maximum retries has been reached, NO otherwise. + */ +- (BOOL)hasReachedMaxRetries; + +/** + * Reset and stop retrying. + */ +- (void)resetRetry; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/HttpClient/MSACHttpCall.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/HttpClient/MSACHttpCall.m new file mode 100644 index 0000000000..3cac0c9029 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/HttpClient/MSACHttpCall.m @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACHttpCall.h" +#import "MSACAppCenterInternal.h" +#import "MSACCompression.h" +#import "MSACConstants+Internal.h" + +@implementation MSACHttpCall + +- (instancetype)initWithUrl:(NSURL *)url + method:(NSString *)method + headers:(NSDictionary *)headers + data:(NSData *)data + retryIntervals:(NSArray *)retryIntervals + compressionEnabled:(BOOL)compressionEnabled + completionHandler:(MSACHttpRequestCompletionHandler)completionHandler { + if ((self = [super init])) { + _url = url; + _method = method; + _retryIntervals = retryIntervals; + _completionHandler = completionHandler; + _retryCount = 0; + _inProgress = NO; + + // Create copy of given headers. Mutable in case compression header must be added. + NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionaryWithDictionary:headers]; + + // Zip data if it is long enough. + if (compressionEnabled && data.length >= kMSACHTTPMinGZipLength) { + data = [MSACCompression compressData:data]; + mutableHeaders[kMSACHeaderContentEncodingKey] = kMSACHeaderContentEncoding; + } + if (data && ![mutableHeaders objectForKey:kMSACHeaderContentTypeKey]) { + mutableHeaders[kMSACHeaderContentTypeKey] = kMSACAppCenterContentType; + } + _data = data; + _headers = mutableHeaders; + } + return self; +} + +- (BOOL)hasReachedMaxRetries { + @synchronized(self) { + return self.retryCount >= (int)[self.retryIntervals count]; + } +} + +- (void)resetRetry { + @synchronized(self) { + if (self.timerSource) { + dispatch_source_cancel(self.timerSource); + } + self.retryCount = 0; + } +} + +- (void)startRetryTimerWithStatusCode:(NSUInteger)statusCode retryAfter:(NSNumber *)retryAfter event:(dispatch_block_t)event { + @synchronized(self) { + + // Create queue. + self.timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, DISPATCH_TARGET_QUEUE_DEFAULT); + uint32_t millisecondsDelta = retryAfter ? [retryAfter unsignedIntValue] : [self delayForRetryCount:self.retryCount]; + MSACLogWarning([MSACAppCenter logTag], @"Call attempt #%d failed with status code: %tu, it will be retried in %d ms.", self.retryCount, + statusCode, millisecondsDelta); + uint64_t nanosecondsDelta = NSEC_PER_MSEC * millisecondsDelta; + self.retryCount++; + dispatch_source_set_timer(self.timerSource, dispatch_walltime(NULL, nanosecondsDelta), DISPATCH_TIME_FOREVER, 1ull * NSEC_PER_SEC); + dispatch_source_set_event_handler(self.timerSource, event); + dispatch_resume(self.timerSource); + } +} + +- (uint32_t)delayForRetryCount:(NSUInteger)retryCount { + + // Create a random delay. + uint32_t millisecondsDelay = + (uint32_t)((NSEC_PER_SEC * [(NSNumber *)self.retryIntervals[retryCount] doubleValue] / 2.0) / (double)NSEC_PER_MSEC); + millisecondsDelay += arc4random_uniform(millisecondsDelay); + return millisecondsDelay; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/HttpClient/MSACHttpClient.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/HttpClient/MSACHttpClient.h new file mode 100644 index 0000000000..50e9d37eda --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/HttpClient/MSACHttpClient.h @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACHttpClientProtocol.h" + +@interface MSACHttpClient : NSObject + +/** + * Creates an instance of MSACHttpClient. + * + * @return A new instance of MSACHttpClient. + */ +- (instancetype)init; + +/** + * Creates an instance of MSACHttpClient. + * + * @param maxHttpConnectionsPerHost The maximum number of connections that can be open for a single host at once. + * + * @return A new instance of MSACHttpClient. + */ +- (instancetype)initWithMaxHttpConnectionsPerHost:(NSInteger)maxHttpConnectionsPerHost; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/HttpClient/MSACHttpClient.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/HttpClient/MSACHttpClient.m new file mode 100644 index 0000000000..32e7d0c06a --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/HttpClient/MSACHttpClient.m @@ -0,0 +1,279 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACHttpClient.h" +#import "MSACAppCenterErrors.h" +#import "MSACAppCenterInternal.h" +#import "MSACConstants+Internal.h" +#import "MSACHttpCall.h" +#import "MSACHttpClientDelegate.h" +#import "MSACHttpClientPrivate.h" +#import "MSACHttpUtil.h" +#import "MSAC_Reachability.h" + +#define DEFAULT_RETRY_INTERVALS @[ @10, @(5 * 60), @(20 * 60) ] + +@implementation MSACHttpClient + +@synthesize delegate = _delegate; + +- (instancetype)init { + return [self initWithMaxHttpConnectionsPerHost:nil reachability:[MSAC_Reachability reachabilityForInternetConnection]]; +} + +- (instancetype)initWithMaxHttpConnectionsPerHost:(NSInteger)maxHttpConnectionsPerHost { + return [self initWithMaxHttpConnectionsPerHost:@(maxHttpConnectionsPerHost) + reachability:[MSAC_Reachability reachabilityForInternetConnection]]; +} + +- (instancetype)initWithMaxHttpConnectionsPerHost:(NSNumber *)maxHttpConnectionsPerHost reachability:(MSAC_Reachability *)reachability { + if ((self = [super init])) { + _sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; + if (maxHttpConnectionsPerHost) { + _sessionConfiguration.HTTPMaximumConnectionsPerHost = [maxHttpConnectionsPerHost integerValue]; + } + _session = [NSURLSession sessionWithConfiguration:_sessionConfiguration]; + _pendingCalls = [NSMutableSet new]; + _enabled = YES; + _paused = NO; + _reachability = reachability; + _delegate = nil; + + // Add listener to reachability. + [MSAC_NOTIFICATION_CENTER addObserver:self + selector:@selector(networkStateChanged:) + name:kMSACReachabilityChangedNotification + object:nil]; + [self.reachability startNotifier]; + } + return self; +} + +- (void)sendAsync:(NSURL *)url + method:(NSString *)method + headers:(nullable NSDictionary *)headers + data:(nullable NSData *)data + completionHandler:(MSACHttpRequestCompletionHandler)completionHandler { + [self sendAsync:url + method:method + headers:headers + data:data + retryIntervals:DEFAULT_RETRY_INTERVALS + compressionEnabled:YES + completionHandler:completionHandler]; +} + +- (void)sendAsync:(NSURL *)url + method:(NSString *)method + headers:(nullable NSDictionary *)headers + data:(nullable NSData *)data + retryIntervals:(NSArray *)retryIntervals + compressionEnabled:(BOOL)compressionEnabled + completionHandler:(MSACHttpRequestCompletionHandler)completionHandler { + @synchronized(self) { + if (!self.enabled) { + NSError *error = [NSError errorWithDomain:kMSACACErrorDomain + code:MSACACDisabledErrorCode + userInfo:@{NSLocalizedDescriptionKey : kMSACACDisabledErrorDesc}]; + completionHandler(nil, nil, error); + return; + } + MSACHttpCall *call = [[MSACHttpCall alloc] initWithUrl:url + method:method + headers:headers + data:data + retryIntervals:retryIntervals + compressionEnabled:compressionEnabled + completionHandler:completionHandler]; + [self sendCallAsync:call]; + } +} + +- (void)sendCallAsync:(MSACHttpCall *)call { + @synchronized(self) { + if (![self.pendingCalls containsObject:call]) { + [self.pendingCalls addObject:call]; + } + if (self.paused) { + return; + } + + // Call delegate before sending HTTP request. + id strongDelegate = self.delegate; + if ([strongDelegate respondsToSelector:@selector(willSendHTTPRequestToURL:withHeaders:)]) { + [strongDelegate willSendHTTPRequestToURL:call.url withHeaders:call.headers]; + } + + // Send HTTP request. + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:call.url + cachePolicy:NSURLRequestReloadIgnoringLocalCacheData + timeoutInterval:0]; + request.HTTPBody = call.data; + request.HTTPMethod = call.method; + request.allHTTPHeaderFields = call.headers; + + // Always disable cookies. + [request setHTTPShouldHandleCookies:NO]; + call.inProgress = YES; + NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + [self requestCompletedWithHttpCall:call data:data response:response error:error]; + }]; + [task resume]; + } +} + +- (void)requestCompletedWithHttpCall:(MSACHttpCall *)httpCall + data:(NSData *)data + response:(NSURLResponse *)response + error:(NSError *)error { + NSHTTPURLResponse *httpResponse; + @synchronized(self) { + httpCall.inProgress = NO; + + // If the call was removed, do not invoke the completion handler as that will have been done already by set enabled. + if (![self.pendingCalls containsObject:httpCall]) { + MSACLogDebug([MSACAppCenter logTag], @"HTTP call was canceled; do not process further."); + return; + } + + // Handle NSError (low level error where we don't even get a HTTP response). + BOOL internetIsDown = [MSACHttpUtil isNoInternetConnectionError:error]; + BOOL couldNotEstablishSecureConnection = [MSACHttpUtil isSSLConnectionError:error]; + if (error) { + if (internetIsDown || couldNotEstablishSecureConnection) { + + // Reset the retry count, will retry once the (secure) connection is established again. + [httpCall resetRetry]; + NSString *logMessage = internetIsDown ? @"Internet connection is down." : @"Could not establish secure connection."; + MSACLogInfo([MSACAppCenter logTag], @"HTTP call failed with error: %@", logMessage); + return; + } else { + MSACLogError([MSACAppCenter logTag], @"HTTP request error with code: %td, domain: %@, description: %@", error.code, error.domain, + error.localizedDescription); + } + } + + // Handle HTTP error. + else { + httpResponse = (NSHTTPURLResponse *)response; + if ([MSACHttpUtil isRecoverableError:httpResponse.statusCode]) { + if ([httpCall hasReachedMaxRetries]) { + [self pause]; + } else { + + // Check if there is a "retry after" header in the response + NSString *retryAfter = httpResponse.allHeaderFields[kMSACRetryHeaderKey]; + NSNumber *retryAfterMilliseconds; + if (retryAfter) { + NSNumberFormatter *formatter = [NSNumberFormatter new]; + retryAfterMilliseconds = [formatter numberFromString:retryAfter]; + } + [httpCall startRetryTimerWithStatusCode:httpResponse.statusCode + retryAfter:retryAfterMilliseconds + event:^{ + [self sendCallAsync:httpCall]; + }]; + return; + } + } else if (![MSACHttpUtil isSuccessStatusCode:httpResponse.statusCode]) { + + // Removing the call from pendingCalls and invoking completion handler must be done before disabling to avoid duplicate invocations. + [self.pendingCalls removeObject:httpCall]; + + // Unblock the caller now with the outcome of the call. + httpCall.completionHandler(data, httpResponse, error); + [self setEnabled:NO andDeleteDataOnDisabled:YES]; + + // Return so as not to re-invoke completion handler. + return; + } + } + [self.pendingCalls removeObject:httpCall]; + } + + // Unblock the caller now with the outcome of the call. + httpCall.completionHandler(data, httpResponse, error); +} + +- (void)networkStateChanged:(__unused NSNotificationCenter *)notification { + if ([self.reachability currentReachabilityStatus] == NotReachable) { + MSACLogInfo([MSACAppCenter logTag], @"Internet connection is down."); + [self pause]; + } else { + MSACLogInfo([MSACAppCenter logTag], @"Internet connection is up."); + [self resume]; + } +} + +- (void)pause { + @synchronized(self) { + if (self.paused) { + return; + } + MSACLogInfo([MSACAppCenter logTag], @"Pause HTTP client."); + self.paused = YES; + + // Reset retry for all calls. + for (MSACHttpCall *call in self.pendingCalls) { + [call resetRetry]; + } + } +} + +- (void)resume { + @synchronized(self) { + + // Resume only while enabled. + if (self.paused && self.enabled) { + MSACLogInfo([MSACAppCenter logTag], @"Resume HTTP client."); + self.paused = NO; + + // Resume calls. + for (MSACHttpCall *call in self.pendingCalls) { + if (!call.inProgress) { + [self sendCallAsync:call]; + } + } + } + } +} + +- (void)setEnabled:(BOOL)isEnabled andDeleteDataOnDisabled:(BOOL)deleteData { + @synchronized(self) { + if (self.enabled != isEnabled) { + self.enabled = isEnabled; + if (isEnabled) { + self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration]; + [self.reachability startNotifier]; + [self resume]; + } else { + [self.reachability stopNotifier]; + [self pause]; + if (deleteData) { + + // Cancel all the tasks and invalidate current session to free resources. + [self.session invalidateAndCancel]; + self.session = nil; + + // Remove pending calls and invoke their completion handler. + for (MSACHttpCall *call in self.pendingCalls) { + NSError *error = [NSError errorWithDomain:kMSACACErrorDomain + code:MSACACCanceledErrorCode + userInfo:@{NSLocalizedDescriptionKey : kMSACACCanceledErrorDesc}]; + call.completionHandler(nil, nil, error); + } + [self.pendingCalls removeAllObjects]; + } + } + } + } +} + +- (void)dealloc { + [self.reachability stopNotifier]; + [MSAC_NOTIFICATION_CENTER removeObserver:self name:kMSACReachabilityChangedNotification object:nil]; + [self.session finishTasksAndInvalidate]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/HttpClient/MSACHttpClientPrivate.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/HttpClient/MSACHttpClientPrivate.h new file mode 100644 index 0000000000..f49539166e --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/HttpClient/MSACHttpClientPrivate.h @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACHttpCall.h" +#import "MSACHttpClient.h" + +@class MSAC_Reachability; + +@interface MSACHttpClient () + +/** + * The HTTP session object. + */ +@property(nonatomic) NSURLSession *session; + +/** + * Reachability library object, which listens for changes in the network state. + */ +@property(nonatomic) MSAC_Reachability *reachability; + +/** + * Pending http calls. + */ +@property(nonatomic) NSMutableSet *pendingCalls; + +/** + * A boolean value set to YES if the client is paused or NO otherwise. While paused, the client will store new calls but not send them until + * resumed. + */ +@property(nonatomic, getter=isPaused) BOOL paused; + +/** + * A boolean value set to YES if the client is enabled or NO otherwise. While disabled, the client will not store any calls. + */ +@property(nonatomic, getter=isEnabled) BOOL enabled; + +/** + * Configuration object for the NSURLSession. Need to store this because the session will need to be re-created after re-enabling the + * client. + */ +@property(nonatomic) NSURLSessionConfiguration *sessionConfiguration; + +/** + * Disables the client, deletes data, and cancels any calls. + * + * @param isEnabled Whether to enable or disable the client. + * @param deleteData Whether to delete data on disabled. + */ +- (void)setEnabled:(BOOL)isEnabled andDeleteDataOnDisabled:(BOOL)deleteData; + +/** + * The actual send call. + * + * @param call The HTTP call to send. + */ +- (void)sendCallAsync:(MSACHttpCall *)call; + +/** + * The completion handler for the HTTP call completion. + * + * @param httpCall The HTTP call object. + * @param data The data being sent. + * @param response The HTTP response. + * @param error The HTTP error. + */ +- (void)requestCompletedWithHttpCall:(MSACHttpCall *)httpCall data:(NSData *)data response:(NSURLResponse *)response error:(NSError *)error; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/HttpClient/MSACHttpClientProtocol.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/HttpClient/MSACHttpClientProtocol.h new file mode 100644 index 0000000000..1e2bd5b39f --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/HttpClient/MSACHttpClientProtocol.h @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +@protocol MSACHttpClientDelegate; + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^MSACHttpRequestCompletionHandler)(NSData *_Nullable responseBody, NSHTTPURLResponse *_Nullable response, + NSError *_Nullable error); + +@protocol MSACHttpClientProtocol + +/** + * HTTP client delegates. + */ +@property(nonatomic, weak, nullable) id delegate; + +@required + +/** + * Make an HTTP call. + * + * @param url The endpoint to use in the HTTP request. + * @param method The HTTP method (verb) to use for the HTTP request (e.g. GET, POST, etc.). + * @param headers HTTP headers. + * @param data A data instance that will be transformed request body. + * @param completionHandler Completion handler. + */ +- (void)sendAsync:(NSURL *)url + method:(NSString *)method + headers:(nullable NSDictionary *)headers + data:(nullable NSData *)data + completionHandler:(nullable MSACHttpRequestCompletionHandler)completionHandler; + +/** + * Make an HTTP call. + * + * @param url The endpoint to use in the HTTP request. + * @param method The HTTP method (verb) to use for the HTTP request (e.g. GET, POST, etc.). + * @param headers HTTP headers. + * @param data A data instance that will be transformed request body. + * @param retryIntervals The retry intervals for the request. + * @param compressionEnabled Whether to compress the request data when it exceeds a certain size. + * @param completionHandler Completion handler. + */ +- (void)sendAsync:(NSURL *)url + method:(NSString *)method + headers:(nullable NSDictionary *)headers + data:(nullable NSData *)data + retryIntervals:(NSArray *)retryIntervals + compressionEnabled:(BOOL)compressionEnabled + completionHandler:(nullable MSACHttpRequestCompletionHandler)completionHandler; + +/** + * Pause the HTTP client. + * The client is automatically paused when it becomes disabled or on network issues. A paused state doesn't impact the current enabled + * state. + * + * @see resume. + */ +- (void)pause; + +/** + * Resume the HTTP client. + * + * @see pause. + */ +- (void)resume; + +/** + * Enables or disables the client. All pending requests are canceled and discarded upon disabling. + * + * @param isEnabled The desired enabled state of the client - pass `YES` to enable, `NO` to disable. + */ +- (void)setEnabled:(BOOL)isEnabled; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/HttpClient/Util/MSACHttpUtil.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/HttpClient/Util/MSACHttpUtil.h new file mode 100644 index 0000000000..e5066b2ef3 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/HttpClient/Util/MSACHttpUtil.h @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +static short const kMSACMaxCharactersDisplayedForAppSecret = 8; +static NSString *const kMSACHidingStringForAppSecret = @"*"; + +@interface MSACHttpUtil : NSObject + +/** + * Indicate if the http response is recoverable. + * + * @param statusCode Http status code. + * + * @return YES if it is recoverable. + */ ++ (BOOL)isRecoverableError:(NSInteger)statusCode; + +/** + * Indicate if the http response is a success response. + * + * @param statusCode Http status code. + * + * @return YES if it is a success code. + */ ++ (BOOL)isSuccessStatusCode:(NSInteger)statusCode; + +/** + * Indicate if error is due to no internet connection. + * + * @param error http error. + * + * @return YES if it is a no network connection error, NO otherwise. + */ ++ (BOOL)isNoInternetConnectionError:(NSError *)error; + +/** + * Indicate if error is because a secure connection could not be established, e.g. when using a public network that * is open but requires + * accepting terms and conditions, and the user hasn't done that, yet. + * + * @param error http error. + * + * @return YES if it is an SSL connection error, NO otherwise. + */ ++ (BOOL)isSSLConnectionError:(NSError *)error; + +/** + * Hide a secret replacing the first N characters by a hiding character. + * + * @param secret the secret string. + * + * @return secret by hiding some characters. + */ ++ (NSString *)hideSecret:(NSString *)secret; + +/** + * Hide a secret in the string. + * + * @param string the string with secret part. + * @param secret the secret string. + * + * @return string with the hiding secret. + */ ++ (NSString *)hideSecretInString:(NSString *)string secret:(NSString *)secret; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/HttpClient/Util/MSACHttpUtil.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/HttpClient/Util/MSACHttpUtil.m new file mode 100644 index 0000000000..0c7e7910fa --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/HttpClient/Util/MSACHttpUtil.m @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACHttpUtil.h" + +@implementation MSACHttpUtil + ++ (BOOL)isRecoverableError:(NSInteger)statusCode { + + // There are some cases when statusCode is 0, e.g., when server is unreachable. If so, the error will contain more details. + return statusCode >= 500 || statusCode == 408 || statusCode == 429 || statusCode == 0; +} + ++ (BOOL)isSuccessStatusCode:(NSInteger)statusCode { + return statusCode >= 200 && statusCode < 300; +} + ++ (BOOL)isNoInternetConnectionError:(NSError *)error { + return ([error.domain isEqualToString:NSURLErrorDomain] && + ((error.code == NSURLErrorNotConnectedToInternet) || (error.code == NSURLErrorNetworkConnectionLost))); +} + ++ (BOOL)isSSLConnectionError:(NSError *)error { + + // Check for error domain and if the error.code falls in the range of SSL connection errors (between -2000 and -1200). + return ([error.domain isEqualToString:NSURLErrorDomain] && + ((error.code >= NSURLErrorCannotLoadFromNetwork) && (error.code <= NSURLErrorSecureConnectionFailed))); +} + ++ (NSString *)hideSecret:(NSString *)secret { + + // Hide everything if secret is shorter than the max number of displayed characters. + NSUInteger appSecretHiddenPartLength = + (secret.length > kMSACMaxCharactersDisplayedForAppSecret ? secret.length - kMSACMaxCharactersDisplayedForAppSecret : secret.length); + NSString *appSecretHiddenPart = [@"" stringByPaddingToLength:appSecretHiddenPartLength + withString:kMSACHidingStringForAppSecret + startingAtIndex:0]; + return [secret stringByReplacingCharactersInRange:NSMakeRange(0, appSecretHiddenPart.length) withString:appSecretHiddenPart]; +} + ++ (NSString *)hideSecretInString:(NSString *)string secret:(NSString *)secret { + NSString *encodedSecret = [self hideSecret:secret]; + return [string stringByReplacingOccurrencesOfString:secret withString:encodedSecret]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACAppCenterIngestion.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACAppCenterIngestion.h new file mode 100644 index 0000000000..c0bede6bc1 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACAppCenterIngestion.h @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACHttpClientProtocol.h" +#import "MSACHttpIngestion.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface MSACAppCenterIngestion : MSACHttpIngestion + +/** + * The app secret. + */ +@property(nonatomic, copy) NSString *appSecret; + +/** + * Initialize the Ingestion. + * + * @param baseUrl Base url. + * @param installId A unique installation identifier. + * @param httpClient The underlying HTTP client. + * + * @return An ingestion instance. + */ +- (id)initWithHttpClient:(id)httpClient baseUrl:(NSString *)baseUrl installId:(NSString *)installId; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACAppCenterIngestion.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACAppCenterIngestion.m new file mode 100644 index 0000000000..d85806ba08 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACAppCenterIngestion.m @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAppCenterIngestion.h" +#import "MSACAppCenterErrors.h" +#import "MSACAppCenterInternal.h" +#import "MSACConstants+Internal.h" +#import "MSACHttpIngestionPrivate.h" +#import "MSACLoggerInternal.h" + +@implementation MSACAppCenterIngestion + +static NSString *const kMSACAPIVersion = @"1.0.0"; +static NSString *const kMSACAPIVersionKey = @"api-version"; +static NSString *const kMSACApiPath = @"/logs"; + +// URL components' name within a partial URL. +static NSString *const kMSACPartialURLComponentsName[] = {@"scheme", @"user", @"password", @"host", @"port", @"path"}; + +- (id)initWithHttpClient:(id)httpClient baseUrl:(NSString *)baseUrl installId:(NSString *)installId { + self = [super initWithHttpClient:httpClient + baseUrl:baseUrl + apiPath:kMSACApiPath + headers:@{kMSACHeaderContentTypeKey : kMSACAppCenterContentType, kMSACHeaderInstallIDKey : installId} + queryStrings:@{kMSACAPIVersionKey : kMSACAPIVersion}]; + return self; +} + +- (BOOL)isReadyToSend { + return self.appSecret != nil; +} + +- (void)sendAsync:(NSObject *)data completionHandler:(MSACSendAsyncCompletionHandler)handler { + MSACLogContainer *container = (MSACLogContainer *)data; + NSString *batchId = container.batchId; + + /* + * FIXME: All logs are already validated at the time the logs are enqueued to Channel. It is not necessary but it can still protect + * against invalid logs being sent to server that are messed up somehow in Storage. If we see performance issues due to this validation, + * we will remove `[container isValid]` call below. + */ + // Verify container. + if (!container || ![container isValid]) { + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : kMSACACLogInvalidContainerErrorDesc}; + NSError *error = [NSError errorWithDomain:kMSACACErrorDomain code:MSACACLogInvalidContainerErrorCode userInfo:userInfo]; + MSACLogError([MSACAppCenter logTag], @"%@", [error localizedDescription]); + handler(batchId, 0, nil, error); + return; + } + if (!self.appSecret) { + MSACLogError([MSACAppCenter logTag], @"AppCenter ingestion is used without app secret."); + return; + } + [super sendAsync:data + completionHandler:^(NSString *_Nonnull __unused callId, NSHTTPURLResponse *_Nullable response, NSData *_Nullable responseBody, + NSError *_Nullable error) { + // Ignore the given call ID so that the container's batch ID can be used instead. + handler(batchId, response, responseBody, error); + }]; +} + +- (NSDictionary *)getHeadersWithData:(nullable NSObject *__unused)data eTag:(nullable NSString *__unused)eTag { + NSMutableDictionary *httpHeaders = [self.httpHeaders mutableCopy]; + [httpHeaders setValue:self.appSecret forKey:kMSACHeaderAppSecretKey]; + return httpHeaders; +} + +- (NSData *)getPayloadWithData:(nullable NSObject *)data { + MSACLogContainer *container = (MSACLogContainer *)data; + NSString *jsonString = [container serializeLog]; + return [jsonString dataUsingEncoding:NSUTF8StringEncoding]; +} + +- (NSString *)obfuscateResponsePayload:(NSString *)payload { + return payload; +} + +#pragma mark - MSACHttpClientDelegate + +- (void)willSendHTTPRequestToURL:(NSURL *)url withHeaders:(nullable NSDictionary *)headers { + + // Don't lose time pretty printing headers if not going to be printed. + if ([MSACLogger currentLogLevel] <= MSACLogLevelVerbose) { + + // Obfuscate secrets. + NSMutableArray *flattenedHeaders = [NSMutableArray new]; + [headers enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, BOOL *stop __unused) { + if ([key isEqualToString:kMSACHeaderAppSecretKey]) { + value = [MSACHttpUtil hideSecret:value]; + } + [flattenedHeaders addObject:[NSString stringWithFormat:@"%@ = %@", key, value]]; + }]; + + // Log URL and headers. + MSACLogVerbose([MSACAppCenter logTag], @"URL: %@", url); + MSACLogVerbose([MSACAppCenter logTag], @"Headers: %@", [flattenedHeaders componentsJoinedByString:@", "]); + } +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACHttpClientDelegate.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACHttpClientDelegate.h new file mode 100644 index 0000000000..e82773333f --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACHttpClientDelegate.h @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol MSACHttpClientDelegate + +@optional + +/** + * A method is called right before sending HTTP request. + * + * @param url A URL. + * @param headers A collection of headers. + */ +- (void)willSendHTTPRequestToURL:(NSURL *)url withHeaders:(nullable NSDictionary *)headers; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACHttpIngestion.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACHttpIngestion.h new file mode 100644 index 0000000000..82d08d8085 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACHttpIngestion.h @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACHttpClientDelegate.h" +#import "MSACIngestionProtocol.h" + +NS_ASSUME_NONNULL_BEGIN + +// HTTP request/response headers for eTag. +static NSString *const kMSACETagResponseHeader = @"etag"; +static NSString *const kMSACETagRequestHeader = @"If-None-Match"; + +@interface MSACHttpIngestion : NSObject + +/** + * Base URL (schema + authority + port only) used to communicate with the server. + */ +@property(nonatomic, copy) NSString *baseURL; + +/** + * API URL path used to identify an API from the server. + */ +@property(nonatomic, copy) NSString *apiPath; + +/** + * Send Url. + */ +@property(nonatomic) NSURL *sendURL; + +/** + * Request header parameters. + */ +@property(nonatomic) NSDictionary *httpHeaders; + +/** + * The HTTP Client. + */ +@property(nonatomic) id httpClient; + +/** + * Retrieve data payload. + * + * @param data The request data. + * @param eTag The ETag. + */ +- (nullable NSDictionary *)getHeadersWithData:(nullable NSObject *)data eTag:(nullable NSString *)eTag; + +/** + * Retrieve data payload as http request body. + * + * @param data The request body data. + */ +- (nullable NSData *)getPayloadWithData:(nullable NSObject *)data; + +/** + * Send data to backend + * + * @param data A data instance that will be transformed request body. + * @param eTag HTTP entity tag. + * @param handler Completion handler. + */ +- (void)sendAsync:(nullable NSObject *)data eTag:(nullable NSString *)eTag completionHandler:(MSACSendAsyncCompletionHandler)handler; + +/** + * Get eTag from the given response. + * + * @param response HTTP response with eTag header. + * + * @return An eTag or `nil` if not found. + */ ++ (nullable NSString *)eTagFromResponse:(NSHTTPURLResponse *)response; + +/** + * Get the Http method to use. + * + * @return The http method. Defaults to POST if not overridden. + */ +- (NSString *)getHttpMethod; + +/** + * Build a new URL with the given values. + * + * @param baseURL Base URL for Ingestion endpoint. + * @param apiPath A path for an API. + * @param queryStrings An array of query strings. + * + * @return A complete URL with the given values. + */ +- (NSURL *)buildURLWithBaseURL:(NSString *)baseURL apiPath:(NSString *)apiPath queryStrings:(NSDictionary *)queryStrings; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACHttpIngestion.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACHttpIngestion.m new file mode 100644 index 0000000000..5e26ba5f39 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACHttpIngestion.m @@ -0,0 +1,234 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACHttpIngestion.h" +#import "MSACAppCenterInternal.h" +#import "MSACConstants+Internal.h" +#import "MSACHttpClientPrivate.h" +#import "MSACHttpIngestionPrivate.h" +#import "MSACLoggerInternal.h" +#import "MSACUtility+StringFormatting.h" + +// URL components' name within a partial URL. +static NSString *const kMSACPartialURLComponentsName[] = {@"scheme", @"user", @"password", @"host", @"port", @"path"}; + +@implementation MSACHttpIngestion + +@synthesize baseURL = _baseURL; +@synthesize apiPath = _apiPath; + +#pragma mark - Initialize + +- (id)initWithHttpClient:(id)httpClient + baseUrl:(NSString *)baseUrl + apiPath:(NSString *)apiPath + headers:(NSDictionary *)headers + queryStrings:(NSDictionary *)queryStrings { + return [self initWithHttpClient:httpClient + baseUrl:baseUrl + apiPath:apiPath + headers:headers + queryStrings:queryStrings + retryIntervals:@[ @(10), @(5 * 60), @(20 * 60) ]]; +} + +- (id)initWithHttpClient:(id)httpClient + baseUrl:(NSString *)baseUrl + apiPath:(NSString *)apiPath + headers:(NSDictionary *)headers + queryStrings:(NSDictionary *)queryStrings + retryIntervals:(NSArray *)retryIntervals { + return [self initWithHttpClient:httpClient + baseUrl:baseUrl + apiPath:apiPath + headers:headers + queryStrings:queryStrings + retryIntervals:retryIntervals + maxNumberOfConnections:4]; +} + +- (id)initWithHttpClient:(id)httpClient + baseUrl:(NSString *)baseUrl + apiPath:(NSString *)apiPath + headers:(NSDictionary *)headers + queryStrings:(NSDictionary *)queryStrings + retryIntervals:(NSArray *)retryIntervals + maxNumberOfConnections:(NSInteger)maxNumberOfConnections { + if ((self = [super init])) { + _httpHeaders = headers; + _httpClient = httpClient; + _enabled = YES; + _callsRetryIntervals = retryIntervals; + _apiPath = apiPath; + _maxNumberOfConnections = maxNumberOfConnections; + _baseURL = baseUrl; + + // Set HTTP client delegate. + httpClient.delegate = self; + + // Set send URL which can't be null + _sendURL = [self buildURLWithBaseURL:baseUrl apiPath:apiPath queryStrings:queryStrings]; + } + return self; +} + +#pragma mark - MSACIngestion + +- (BOOL)isReadyToSend { + return YES; +} + +- (void)sendAsync:(NSObject *)data completionHandler:(MSACSendAsyncCompletionHandler)handler { + [self sendAsync:data eTag:nil callId:MSAC_UUID_STRING completionHandler:handler]; +} + +- (void)sendAsync:(NSObject *)data eTag:(nullable NSString *)eTag completionHandler:(MSACSendAsyncCompletionHandler)handler { + [self sendAsync:data eTag:eTag callId:MSAC_UUID_STRING completionHandler:handler]; +} + +#pragma mark - Life cycle + +- (void)setEnabled:(BOOL)isEnabled andDeleteDataOnDisabled:(BOOL __unused)deleteData { + @synchronized(self) { + self.enabled = isEnabled; + } +} + +#pragma mark - MSACHttpIngestion + +- (NSURL *)buildURLWithBaseURL:(NSString *)baseURL apiPath:(NSString *)apiPath queryStrings:(NSDictionary *)queryStrings { + + // Construct the URL string with the query string. + NSMutableString *urlString = [NSMutableString stringWithFormat:@"%@%@", baseURL, apiPath]; + __block NSMutableString *queryStringForEncoding = [NSMutableString new]; + + // Set query parameter. + [queryStrings enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull queryString, __unused BOOL *_Nonnull stop) { + [queryStringForEncoding + appendString:[NSString stringWithFormat:@"%@%@=%@", [queryStringForEncoding length] > 0 ? @"&" : @"", key, queryString]]; + }]; + if ([queryStringForEncoding length] > 0) { + [urlString appendFormat:@"?%@", [queryStringForEncoding + stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]]; + } + + return (NSURL * _Nonnull)[NSURL URLWithString:urlString]; +} + +// This method will be overridden by subclasses. +- (NSDictionary *)getHeadersWithData:(NSObject *__unused)data eTag:(NSString *__unused)eTag { + return nil; +} + +// This method will be overridden by subclasses. +- (NSData *)getPayloadWithData:(NSObject *__unused)data { + return nil; +} + +// This method will be overridden by subclasses. +- (NSString *)obfuscateResponsePayload:(NSString *__unused)payload { + return nil; +} + +- (NSString *)getHttpMethod { + return kMSACHttpMethodPost; +}; + +#pragma mark - Private + +- (void)setBaseURL:(NSString *)baseURL { + @synchronized(self) { + BOOL success = false; + NSURLComponents *components; + _baseURL = baseURL; + NSURL *partialURL = [NSURL URLWithString:[baseURL stringByAppendingString:self.apiPath]]; + + // Merge new parial URL and current full URL. + if (partialURL) { + components = [NSURLComponents componentsWithURL:self.sendURL resolvingAgainstBaseURL:NO]; + @try { + for (u_long i = 0; i < sizeof(kMSACPartialURLComponentsName) / sizeof(*kMSACPartialURLComponentsName); i++) { + NSString *propertyName = kMSACPartialURLComponentsName[i]; + [components setValue:[partialURL valueForKey:propertyName] forKey:propertyName]; + } + } @catch (NSException *ex) { + MSACLogInfo([MSACAppCenter logTag], @"Error while updating HTTP URL %@ with %@: \n%@", self.sendURL.absoluteString, baseURL, ex); + } + + // Update full URL. + if (components.URL) { + self.sendURL = (NSURL * _Nonnull) components.URL; + success = true; + } + } + + // Notify failure. + if (!success) { + MSACLogInfo([MSACAppCenter logTag], @"Failed to update HTTP URL %@ with %@", self.sendURL.absoluteString, baseURL); + } + } +} + +- (void)sendAsync:(NSObject *)data + eTag:(nullable NSString *)eTag + callId:(NSString *)callId + completionHandler:(MSACSendAsyncCompletionHandler)handler { + @synchronized(self) { + if (!self.enabled) { + return; + } + NSDictionary *httpHeaders = [self getHeadersWithData:data eTag:eTag]; + NSData *payload = [self getPayloadWithData:data]; + [self.httpClient sendAsync:self.sendURL + method:[self getHttpMethod] + headers:httpHeaders + data:payload + retryIntervals:self.callsRetryIntervals + compressionEnabled:YES + completionHandler:^(NSData *_Nullable responseBody, NSHTTPURLResponse *_Nullable response, NSError *_Nullable error) { + [self printResponse:response body:responseBody error:error]; + handler(callId, response, responseBody, error); + }]; + } +} + +#pragma mark - Printing + +- (void)printResponse:(NSHTTPURLResponse *)response body:(NSData *)responseBody error:(NSError *)error { + + // Don't lose time pretty printing if not going to be printed. + if (error) { + MSACLogDebug([MSACAppCenter logTag], @"HTTP request error with code: %td, domain: %@, description: %@", error.code, error.domain, + error.localizedDescription); + } else if ([MSACAppCenter logLevel] <= MSACLogLevelVerbose) { + NSString *contentType = response.allHeaderFields[kMSACHeaderContentTypeKey]; + NSString *payload; + + // Obfuscate payload. + if (responseBody.length > 0) { + if ([contentType hasPrefix:@"application/json"]) { + payload = [self obfuscateResponsePayload:[MSACUtility prettyPrintJson:responseBody]]; + } else if (!contentType.length || [contentType hasPrefix:@"text/"] || [contentType hasPrefix:@"application/"]) { + payload = [self obfuscateResponsePayload:[[NSString alloc] initWithData:responseBody encoding:NSUTF8StringEncoding]]; + } else { + payload = @""; + } + } + MSACLogVerbose([MSACAppCenter logTag], @"HTTP response received with status code: %tu, payload:\n%@", response.statusCode, payload); + } +} + +#pragma mark - Helper + ++ (nullable NSString *)eTagFromResponse:(NSHTTPURLResponse *)response { + + // Response header keys are case-insensitive but NSHTTPURLResponse contains case-sensitive keys in Dictionary. + for (NSString *key in response.allHeaderFields.allKeys) { + if ([[key lowercaseString] isEqualToString:kMSACETagResponseHeader]) { + return response.allHeaderFields[key]; + } + } + return nil; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACHttpIngestionPrivate.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACHttpIngestionPrivate.h new file mode 100644 index 0000000000..34f534e10a --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACHttpIngestionPrivate.h @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACHttpIngestion.h" + +@interface MSACHttpIngestion () + +/** + * The maximum number of connections for the session. The one collector endpoint only allows for two connections while the app center + * endpoint doesn't impose a limit, using the iOS default value of 4 connections for this. + */ +@property(nonatomic, readonly) NSInteger maxNumberOfConnections; + +/** + * Retry intervals used by calls in case of recoverable errors. + */ +@property(nonatomic) NSArray *callsRetryIntervals; + +/** + * A boolean value set to YES if the ingestion is enabled or NO otherwise. + * Enable/disable does resume/pause the ingestion as needed under the hood. + */ +@property(nonatomic, getter=isEnabled) BOOL enabled; + +/** + * Initialize the Ingestion with default retry intervals. + * + * @param httpClient The HTTP client. + * @param baseUrl Base url. + * @param apiPath Base API path. + * @param headers HTTP headers. + * @param queryStrings An array of query strings. + */ +- (id)initWithHttpClient:(id)httpClient + baseUrl:(NSString *)baseUrl + apiPath:(NSString *)apiPath + headers:(NSDictionary *)headers + queryStrings:(NSDictionary *)queryStrings; + +/** + * Initialize the Ingestion. + * + * @param httpClient The HTTP client. + * @param baseUrl Base url. + * @param apiPath Base API path. + * @param headers Http headers. + * @param queryStrings An array of query strings. + * @param retryIntervals An array for retry intervals in second. + */ +- (id)initWithHttpClient:(id)httpClient + baseUrl:(NSString *)baseUrl + apiPath:(NSString *)apiPath + headers:(NSDictionary *)headers + queryStrings:(NSDictionary *)queryStrings + retryIntervals:(NSArray *)retryIntervals; + +/** + * Initialize the Ingestion. + * + * @param httpClient The HTTP client. + * @param baseUrl Base url. + * @param apiPath Base API path. + * @param headers Http headers. + * @param queryStrings An array of query strings. + * @param retryIntervals An array for retry intervals in second. + * @param maxNumberOfConnections The maximum number of connections per host. + */ +- (id)initWithHttpClient:(id)httpClient + baseUrl:(NSString *)baseUrl + apiPath:(NSString *)apiPath + headers:(NSDictionary *)headers + queryStrings:(NSDictionary *)queryStrings + retryIntervals:(NSArray *)retryIntervals + maxNumberOfConnections:(NSInteger)maxNumberOfConnections; + +/** + * Hide a part of sensitive value for payload. + * + * @param payload The response payload to be obfuscated. + * + * @return An obfuscated value. + */ +- (NSString *)obfuscateResponsePayload:(NSString *)payload; + +/** + * Gets the HTTP payload for the given data. + * + * @param data The data object. + * + * @return The serialized HTTP data. + */ +- (NSData *)getPayloadWithData:(NSObject *)data; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACIngestionProtocol.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACIngestionProtocol.h new file mode 100644 index 0000000000..f80285179d --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACIngestionProtocol.h @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACEnable.h" +#import "MSACHttpClientProtocol.h" +#import "MSACHttpUtil.h" +#import "MSAC_Reachability.h" + +NS_ASSUME_NONNULL_BEGIN + +@protocol MSACIngestionDelegate; + +typedef void (^MSACSendAsyncCompletionHandler)(NSString *callId, NSHTTPURLResponse *_Nullable response, NSData *_Nullable data, + NSError *_Nullable error); + +@protocol MSACIngestionProtocol + +/** + * The indicator of readiness to send data. + */ +@property(nonatomic, readonly, getter=isReadyToSend) BOOL readyToSend; + +/** + * Send data. + * + * @param data Instance that will be transformed to request body. + * @param handler Completion handler. + */ +- (void)sendAsync:(nullable NSObject *)data completionHandler:(MSACSendAsyncCompletionHandler)handler; + +/** + * Send data. + * + * @param data Instance that will be transformed to request body. + * @param eTag HTTP entity tag. + * @param handler Completion handler. + */ +- (void)sendAsync:(nullable NSObject *)data eTag:(nullable NSString *)eTag completionHandler:(MSACSendAsyncCompletionHandler)handler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACOneCollectorIngestion.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACOneCollectorIngestion.h new file mode 100644 index 0000000000..27cb1e14b1 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACOneCollectorIngestion.h @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACHttpIngestion.h" + +static NSString *const kMSACOneCollectorApiKey = @"apikey"; +static NSString *const kMSACOneCollectorApiPath = @"/OneCollector"; +static NSString *const kMSACOneCollectorApiVersion = @"1.0"; + +/** + * Assign value in header to avoid "format is not a string literal" warning. + * The convention for this format string is -----. + */ +static NSString *const kMSACOneCollectorClientVersionFormat = @"ACS-iOS-ObjectiveC-no-%@-no"; +static NSString *const kMSACOneCollectorClientVersionKey = @"Client-Version"; +static NSString *const kMSACOneCollectorContentType = @"application/x-json-stream; charset=utf-8"; +static NSString *const kMSACOneCollectorLogSeparator = @"\n"; +static NSString *const kMSACOneCollectorTicketsKey = @"Tickets"; +static NSString *const kMSACOneCollectorUploadTimeKey = @"Upload-Time"; + +@interface MSACOneCollectorIngestion : MSACHttpIngestion + +/** + * Initialize the ingestion. + * + * @param baseUrl Base url. + * + * @return An ingestion instance. + */ +- (id)initWithHttpClient:(id)httpClient baseUrl:(NSString *)baseUrl; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACOneCollectorIngestion.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACOneCollectorIngestion.m new file mode 100644 index 0000000000..13161a814a --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACOneCollectorIngestion.m @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAbstractLogInternal.h" +#import "MSACAppCenterErrors.h" +#import "MSACAppCenterInternal.h" +#import "MSACCSExtensions.h" +#import "MSACConstants+Internal.h" +#import "MSACHttpIngestionPrivate.h" +#import "MSACLoggerInternal.h" +#import "MSACOneCollectorIngestionPrivate.h" +#import "MSACProtocolExtension.h" +#import "MSACTicketCache.h" +#import "MSACUtility+StringFormatting.h" + +@implementation MSACOneCollectorIngestion + +- (id)initWithHttpClient:(id)httpClient baseUrl:(NSString *)baseUrl { + self = [super initWithHttpClient:httpClient + baseUrl:baseUrl + apiPath:[NSString stringWithFormat:@"%@/%@", kMSACOneCollectorApiPath, kMSACOneCollectorApiVersion] + headers:@{ + kMSACHeaderContentTypeKey : kMSACOneCollectorContentType, + kMSACOneCollectorClientVersionKey : + [NSString stringWithFormat:kMSACOneCollectorClientVersionFormat, [MSACUtility sdkVersion]] + } + queryStrings:nil + retryIntervals:@[ @(10), @(5 * 60), @(20 * 60) ] + maxNumberOfConnections:2]; + return self; +} + +- (void)sendAsync:(NSObject *)data completionHandler:(MSACSendAsyncCompletionHandler)handler { + MSACLogContainer *container = (MSACLogContainer *)data; + NSString *batchId = container.batchId; + + /* + * FIXME: All logs are already validated at the time the logs are enqueued to Channel. It is not necessary but it can still protect + * against invalid logs being sent to server that are messed up somehow in Storage. If we see performance issues due to this validation, + * we will remove `[container isValid]` call below. + */ + + // Verify container. + if (!container || ![container isValid]) { + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : kMSACACLogInvalidContainerErrorDesc}; + NSError *error = [NSError errorWithDomain:kMSACACErrorDomain code:MSACACLogInvalidContainerErrorCode userInfo:userInfo]; + MSACLogError([MSACAppCenter logTag], @"%@", [error localizedDescription]); + handler(batchId, 0, nil, error); + return; + } + [super sendAsync:container + completionHandler:^(NSString *_Nonnull __unused callId, NSHTTPURLResponse *_Nullable response, NSData *_Nullable responseBody, + NSError *_Nullable error) { + // Ignore the given call ID so that the container's batch ID can be used instead. + handler(batchId, response, responseBody, error); + }]; +} + +- (NSDictionary *)getHeadersWithData:(nullable NSObject *)data eTag:(nullable NSString *__unused)eTag { + MSACLogContainer *container = (MSACLogContainer *)data; + NSMutableDictionary *headers = [self.httpHeaders mutableCopy]; + NSMutableSet *apiKeys = [NSMutableSet new]; + for (id log in container.logs) { + [apiKeys addObjectsFromArray:[log.transmissionTargetTokens allObjects]]; + } + headers[kMSACOneCollectorApiKey] = [[apiKeys allObjects] componentsJoinedByString:@","]; + headers[kMSACOneCollectorUploadTimeKey] = [NSString stringWithFormat:@"%lld", (long long)[MSACUtility nowInMilliseconds]]; + + // Gather tokens from logs. + NSMutableDictionary *ticketsAndKeys = [NSMutableDictionary new]; + for (id log in container.logs) { + MSACCommonSchemaLog *csLog = (MSACCommonSchemaLog *)log; + if (csLog.ext.protocolExt) { + NSArray *ticketKeys = [[[csLog ext] protocolExt] ticketKeys]; + for (NSString *ticketKey in ticketKeys) { + NSString *authenticationToken = [[MSACTicketCache sharedInstance] ticketFor:ticketKey]; + if (authenticationToken) { + [ticketsAndKeys setValue:authenticationToken forKey:ticketKey]; + } + } + } + } + if (ticketsAndKeys.count > 0) { + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:ticketsAndKeys options:0 error:nil]; + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + [headers setValue:jsonString forKey:kMSACOneCollectorTicketsKey]; + } + + return headers; +} + +- (NSData *)getPayloadWithData:(nullable NSObject *)data { + MSACLogContainer *container = (MSACLogContainer *)data; + NSMutableString *jsonString = [NSMutableString new]; + for (id log in container.logs) { + MSACAbstractLog *abstractLog = (MSACAbstractLog *)log; + [jsonString appendString:[abstractLog serializeLogWithPrettyPrinting:NO]]; + + // Separator for one collector logs. + [jsonString appendString:kMSACOneCollectorLogSeparator]; + } + NSData *httpBody = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; + return httpBody; +} + +- (NSString *)obfuscateResponsePayload:(NSString *)payload { + return [MSACUtility obfuscateString:payload + searchingForPattern:kMSACTokenKeyValuePattern + toReplaceWithTemplate:kMSACTokenKeyValueObfuscatedTemplate]; +} + +- (NSString *)obfuscateTargetTokens:(NSString *)tokenString { + NSArray *tokens = [tokenString componentsSeparatedByString:@","]; + NSMutableArray *obfuscatedTokens = [NSMutableArray new]; + for (NSString *token in tokens) { + [obfuscatedTokens addObject:[MSACHttpUtil hideSecret:token]]; + } + return [obfuscatedTokens componentsJoinedByString:@","]; +} + +- (NSString *)obfuscateTickets:(NSString *)ticketString { + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@":[^\"]+" options:0 error:nil]; + return [regex stringByReplacingMatchesInString:ticketString options:0 range:NSMakeRange(0, ticketString.length) withTemplate:@":***"]; +} + +- (void)willSendHTTPRequestToURL:(NSURL *)url withHeaders:(nullable NSDictionary *)headers { + + // Don't lose time pretty printing headers if not going to be printed. + if ([MSACLogger currentLogLevel] <= MSACLogLevelVerbose) { + + // Obfuscate secrets. + NSMutableArray *flattenedHeaders = [NSMutableArray new]; + [headers enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *value, BOOL *stop __unused) { + if ([key isEqualToString:kMSACOneCollectorApiKey]) { + value = [self obfuscateTargetTokens:value]; + } else if ([key isEqualToString:kMSACOneCollectorTicketsKey]) { + value = [self obfuscateTickets:value]; + } + [flattenedHeaders addObject:[NSString stringWithFormat:@"%@ = %@", key, value]]; + }]; + + // Log URL and headers. + MSACLogVerbose([MSACAppCenter logTag], @"URL: %@", url); + MSACLogVerbose([MSACAppCenter logTag], @"Headers: %@", [flattenedHeaders componentsJoinedByString:@", "]); + } +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACOneCollectorIngestionPrivate.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACOneCollectorIngestionPrivate.h new file mode 100644 index 0000000000..4064a1a64e --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/MSACOneCollectorIngestionPrivate.h @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACOneCollectorIngestion.h" + +@interface MSACOneCollectorIngestion () + +/** + * Hide secret from the given token string. + * + * @param tokenString A token string. + * + * @return A obfuscated token string. + */ +- (NSString *)obfuscateTargetTokens:(NSString *)tokenString; + +/** + * Hide secret from the given ticket string. + * + * @param ticketString A ticket string. + * + * @return A obfuscated ticket string. + */ +- (NSString *)obfuscateTickets:(NSString *)ticketString; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/Util/MSACTicketCache.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/Util/MSACTicketCache.h new file mode 100644 index 0000000000..4040a8b135 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/Util/MSACTicketCache.h @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface MSACTicketCache : NSObject + +/** + * Dictionary to hold tickets. + */ +@property(nonatomic) NSMutableDictionary *tickets; + +/** + * Return singleton instance of MSACTicketCache. + * + * @return the instance. + */ ++ (instancetype)sharedInstance; + +/** + * Retrieve a ticket from the ticket cache. + * + * @param key The key for the ticket. + * + * @return The ticket or nil. + */ +- (NSString *_Nullable)ticketFor:(NSString *)key; + +/** + * Add a ticket to the cache. + * + * @param value The ticket to cache. + * @param key The key for the ticket to be cached. + */ +- (void)setTicket:(NSString *)value forKey:(NSString *)key; + +/** + * Clear the cache. This will be used in tests. + */ +- (void)clearCache; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/Util/MSACTicketCache.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/Util/MSACTicketCache.m new file mode 100644 index 0000000000..e03c1d682b --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Ingestion/Util/MSACTicketCache.m @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACTicketCache.h" + +@implementation MSACTicketCache + +/** + * Singleton. + */ +static MSACTicketCache *sharedInstance = nil; +static dispatch_once_t onceToken; + +- (instancetype)init { + if ((self = [super init])) { + _tickets = [NSMutableDictionary new]; + } + return self; +} + ++ (instancetype)sharedInstance { + dispatch_once(&onceToken, ^{ + if (sharedInstance == nil) { + sharedInstance = [[MSACTicketCache alloc] init]; + } + }); + return sharedInstance; +} + +- (NSString *_Nullable)ticketFor:(NSString *)key { + return [self.tickets valueForKey:key]; +} + +- (void)setTicket:(NSString *)ticket forKey:(NSString *)key { + [self.tickets setValue:ticket forKey:key]; +} + +- (void)clearCache { + self.tickets = [NSMutableDictionary new]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACAppCenterInternal.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACAppCenterInternal.h new file mode 100644 index 0000000000..7f04478810 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACAppCenterInternal.h @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "AppCenter+Internal.h" +#import "MSACAppCenter.h" +#import "MSACChannelGroupProtocol.h" +#import "MSACServiceInternal.h" + +/* + * Persisted storage keys. + */ +static NSString *const kMSACInstallIdKey = @"InstallId"; +static NSString *const kMSACAppCenterIsEnabledKey = @"AppCenterIsEnabled"; + +/* + * Name of the environment variable to check for which services should be disabled. + */ +static NSString *const kMSACDisableVariable = @"APP_CENTER_DISABLE"; + +/* + * Value that would cause all services to be disabled. + */ +static NSString *const kMSACDisableAll = @"All"; + +/** + * Name of the environment variable to check to see if we're running in App Center Test. + */ +static NSString *const kMSACRunningInAppCenter = @"RUNNING_IN_APP_CENTER"; + +/** + * A string value for environment variables denoting `true`. + */ +static NSString *const kMSACTrueEnvironmentString = @"1"; + +@interface MSACAppCenter () + +@property(nonatomic) id channelGroup; + +@property(nonatomic) NSMutableArray *> *services; + +@property(nonatomic) NSMutableArray *startedServiceNames; + +@property(nonatomic, copy) NSString *appSecret; + +@property(nonatomic, copy) NSString *defaultTransmissionTargetToken; + +@property(atomic, copy) NSString *logUrl; + +@property(nonatomic, readonly) NSUUID *installId; + +@property(nonatomic) NSNumber *requestedMaxStorageSizeInBytes; + +/** + * Flag indicating if the SDK is enabled or not as a whole. + */ +@property(nonatomic, getter=isEnabled) BOOL enabled; + +/** + * Flag indicating if the SDK is configured or not. + */ +@property(nonatomic, getter=isSdkConfigured) BOOL sdkConfigured; + +/** + * Flag indicating if the SDK is configured From Application or not. + */ +@property(nonatomic, getter=isConfiguredFromApplication) BOOL configuredFromApplication; + +/** + * Flag indicating if the SDK is enabled state updating or not. + */ +@property(nonatomic, getter=isEnabledStateUpdating) BOOL enabledStateUpdating; + +@property(nonatomic, copy) void (^maxStorageSizeCompletionHandler)(BOOL); + +@property BOOL setMaxStorageSizeHasBeenCalled; + +/** + * Returns the singleton instance of App Center. + * + * @return The singleton instance. + */ ++ (instancetype)sharedInstance; + +/** + * Get the log tag for the AppCenter service. + * + * @return A name of logger tag for the AppCenter service. + */ ++ (NSString *)logTag; + +/** + * Get the group ID for the AppCenter service. + * + * @return A storage identifier for the AppCenter service. + */ ++ (NSString *)groupId; + +/** + * Get the log URL. + * + * @return The log URL. + */ +- (NSString *)logUrl; + +/** + * Get the app secret. + * + * @return The app secret. + */ +- (NSString *)appSecret; + +/** + * Sort the array of services in descending order based on their priority. + * + * @return The array of services in descending order. + */ +- (NSArray *)sortServices:(NSArray *)services; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACAppCenterPrivate.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACAppCenterPrivate.h new file mode 100644 index 0000000000..237bd7cb45 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACAppCenterPrivate.h @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAppCenter.h" +#import "MSACChannelUnitProtocol.h" + +@class MSACOneCollectorChannelDelegate; + +@interface MSACAppCenter () + +@property(nonatomic) id channelUnit; +@property(nonatomic) MSACOneCollectorChannelDelegate *oneCollectorChannelDelegate; + +/** + * Method to reset the singleton when running unit tests only. So calling sharedInstance returns a fresh instance. + */ ++ (void)resetSharedInstance; + +/** + * Configure the SDK. + * + * @param appSecret A unique and secret key used to identify the application for App Center ingestion. + * @param transmissionTargetToken A unique and secret key used to identify the application for One Collector ingestion. + * @param fromApplication A flag indicating that the sdk is configured from an application. + * + * @return `YES` if configured successfully, otherwise `NO`. + */ +- (BOOL)configureWithAppSecret:(NSString *)appSecret + transmissionTargetToken:(NSString *)transmissionTargetToken + fromApplication:(BOOL)fromApplication; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACCustomPropertiesInternal.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACCustomPropertiesInternal.h new file mode 100644 index 0000000000..b68614f5ca --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACCustomPropertiesInternal.h @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#ifndef MSAC_CUSTOM_PROPERTIES_INTERNAL_H +#define MSAC_CUSTOM_PROPERTIES_INTERNAL_H + +#import "MSACCustomProperties.h" + +/** + * Private declarations for MSACCustomProperties. + */ +@interface MSACCustomProperties () + +/** + * Create an immutable copy of the properties dictionary to use in synchronized scenarios. + * + * @return An immutable copy of properties. + */ +- (NSDictionary *)propertiesImmutableCopy; + +@end + +#endif diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACCustomPropertiesPrivate.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACCustomPropertiesPrivate.h new file mode 100644 index 0000000000..cdbd47c627 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACCustomPropertiesPrivate.h @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACCustomProperties.h" + +/** + * Private declarations for MSACCustomProperties. + */ +@interface MSACCustomProperties () + +@property(nonatomic, strong) NSMutableDictionary *properties; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACDependencyConfiguration.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACDependencyConfiguration.h new file mode 100644 index 0000000000..126cb1bc54 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACDependencyConfiguration.h @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +@protocol MSACHttpClientProtocol; + +NS_ASSUME_NONNULL_BEGIN + +@interface MSACDependencyConfiguration : NSObject + +@property(class, nonatomic, nullable) id httpClient; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACDependencyConfiguration.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACDependencyConfiguration.m new file mode 100644 index 0000000000..947630b363 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACDependencyConfiguration.m @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACDependencyConfiguration.h" +#import "MSACHttpClientProtocol.h" + +static id _httpClient; + +@implementation MSACDependencyConfiguration + ++ (id)httpClient { + @synchronized(self) { + return _httpClient; + } +} + ++ (void)setHttpClient:(nullable id)httpClient { + @synchronized(self) { + _httpClient = httpClient; + } +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACLoggerInternal.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACLoggerInternal.h new file mode 100644 index 0000000000..41b45e2dbb --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACLoggerInternal.h @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACLogger.h" + +FOUNDATION_EXPORT MSACLogHandler const msDefaultLogHandler; + +@interface MSACLogger () + ++ (BOOL)isUserDefinedLogLevel; + +/* + * For testing only. + */ ++ (void)setIsUserDefinedLogLevel:(BOOL)isUserDefinedLogLevel; + ++ (MSACLogLevel)currentLogLevel; + ++ (MSACLogHandler)logHandler; + ++ (void)setCurrentLogLevel:(MSACLogLevel)currentLogLevel; + ++ (void)setLogHandler:(MSACLogHandler)logHandler; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACServiceAbstractInternal.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACServiceAbstractInternal.h new file mode 100644 index 0000000000..8f38efaa8c --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACServiceAbstractInternal.h @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACService.h" +#import "MSACServiceAbstract.h" +#import "MSACServiceCommon.h" +#import "MSACServiceInternal.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Abstraction of services internal common logic. + * This class is intended to be subclassed only not instantiated directly. + * + * @see MSACServiceInternal protocol, any service subclassing this class must also conform to this protocol. + */ +@interface MSACServiceAbstract () + +/** + * isEnabled value storage key. + */ +@property(nonatomic, copy, readonly) NSString *isEnabledKey; + +/** + * Flag indicating if a service has been started or not. + */ +@property(nonatomic, getter=isStarted) BOOL started; + +#pragma mark - Service initialization + +/** + * Create a service. + * + * @return A service with common logic already implemented. + */ +- (instancetype)init; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACServiceCommon.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACServiceCommon.h new file mode 100644 index 0000000000..f4bfe0d447 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACServiceCommon.h @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACConstants.h" + +NS_ASSUME_NONNULL_BEGIN + +@class MSACChannelUnitConfiguration; + +@protocol MSACChannelGroupProtocol; +@protocol MSACChannelUnitProtocol; + +/** + * Protocol declaring public common logic for services. + */ +@protocol MSACServiceCommon + +@required + +/** + * Flag indicating if a service is available or not. It means that the service is started and enabled. + */ +@property(nonatomic, readonly, getter=isAvailable) BOOL available; + +/** + * Flag indicating if a service is enabled or not. + */ +@property(nonatomic, getter=isEnabled) BOOL enabled; + +/** + * Channel group. + */ +@property(nonatomic) id channelGroup; + +/** + * Channel unit. + */ +@property(nonatomic) id channelUnit; + +/** + * The app secret for the SDK. + */ +@property(nonatomic, nonnull) NSString *appSecret; + +/** + * The default transmission target token. + */ +@property(nonatomic, nonnull) NSString *defaultTransmissionTargetToken; + +@optional + +/** + * Service unique key for storage purpose. + * + * @discussion: IMPORTANT, This string is used to point to the right storage value for this service. Changing this string results in data + * lost if previous data is not migrated. + */ +@property(nonatomic, copy, readonly) NSString *groupId; + +/** + * The channel configuration for this service. + */ +@property(nonatomic) MSACChannelUnitConfiguration *channelUnitConfiguration; + +@required + +/** + * Apply the enabled state to the service. + * + * @param isEnabled A boolean value set to YES to enable the service or NO otherwise. + */ +- (void)applyEnabledState:(BOOL)isEnabled; + +/** + * The initialization priority for this service. + */ +@property(nonatomic, readonly) MSACInitializationPriority initializationPriority; + +/** + * Check if the SDK has been properly initialized and the service can be used. Logs an error in case it wasn't. + * + * @return a BOOL to indicate proper initialization of the SDK. + */ +- (BOOL)canBeUsed; + +/** + * Start this service with a channel group. Also sets the flag that indicates that a service has been started. + * + * @param channelGroup channel group used to persist and send logs. + * @param appSecret app secret for the SDK. + * @param token default transmission target token. + * @param fromApplication indicates whether the service started from an application or not. + * + * @discussion Note that this is defined both here and in MSACServiceAbstract.h. This is intentional, and due to the way the classes are + * factored. + */ +- (void)startWithChannelGroup:(id)channelGroup + appSecret:(nullable NSString *)appSecret + transmissionTargetToken:(nullable NSString *)token + fromApplication:(BOOL)fromApplication; + +/** + * Update configuration when the service requires to start again. This method should only be called if the service is started from libraries + * and then is being started from an application. + * + * @param appSecret app secret for the SDK. + * @param token default transmission target token for this service. + * + * @discussion Note that this is defined both here and in MSACServiceAbstract.h. This is intentional, and due to the way the classes are + * factored. + */ +- (void)updateConfigurationWithAppSecret:(NSString *)appSecret transmissionTargetToken:(NSString *)token; + +/** + * Checks if the service needs the application secret. + * + * @return `YES` if the application secret is required, `NO` otherwise. + */ +- (BOOL)isAppSecretRequired; + +/** + * Checks if the service is started from an application. + * + * @return `YES` if the service is started from an application, `NO` otherwise. + */ +- (BOOL)isStartedFromApplication; + +@optional + +/** + * Get the unique instance. + * + * @return unique instance. + */ ++ (instancetype)sharedInstance; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACServiceInternal.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACServiceInternal.h new file mode 100644 index 0000000000..aac6733d14 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/MSACServiceInternal.h @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACService.h" +#import "MSACServiceCommon.h" + +/** + * Protocol declaring all the logic of a service. This is what concrete services needs to conform to. The difference is that + * MSACServiceCommon is public, while MSACServiceInternal is private. Some properties are present in both, which is counter-intuitive but + * the way we implemented this to achieve abstraction and not have empty implementations in MSACServiceAbstract. + */ +@protocol MSACServiceInternal + +/** + * The initialization priority for this service. Defined here as well as in MSACServiceCommon to achieve abstraction. + */ +@property(nonatomic, readonly) MSACInitializationPriority initializationPriority; + +/** + * The app secret for the SDK. + */ +@property(nonatomic) NSString *appSecret; + +/** + * Service unique key for storage purpose. + * + * @discussion: IMPORTANT, This string is used to point to the right storage value for this service. Changing this string results in data + * lost if previous data is not migrated. + */ +@property(nonatomic, copy, readonly) NSString *groupId; + +/** + * Get the unique instance. + * + * @return The unique instance. + */ ++ (instancetype)sharedInstance; + +/** + * Get a service name. + * + * @return the service name. + * + * @discussion This is used to initialize each service. + */ ++ (NSString *)serviceName; + +/** + * Get the log tag for this service. + * + * @return A name of logger tag for this service. + */ ++ (NSString *)logTag; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACAppExtension.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACAppExtension.h new file mode 100644 index 0000000000..525a1c37ad --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACAppExtension.h @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACModel.h" +#import "MSACSerializableObject.h" + +static NSString *const kMSACAppId = @"id"; +static NSString *const kMSACAppLocale = @"locale"; +static NSString *const kMSACAppName = @"name"; +static NSString *const kMSACAppVer = @"ver"; +static NSString *const kMSACAppUserId = @"userId"; + +/** + * The App extension contains data specified by the application. + */ +@interface MSACAppExtension : NSObject + +/** + * The application's bundle identifier. + */ +@property(nonatomic, copy) NSString *appId; + +/** + * The application's version. + */ +@property(nonatomic, copy) NSString *ver; + +/** + * The application's name. + */ +@property(nonatomic, copy) NSString *name; + +/** + * The application's locale. + */ +@property(nonatomic, copy) NSString *locale; + +/** + * The application's userId. + */ +@property(nonatomic, copy) NSString *userId; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACAppExtension.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACAppExtension.m new file mode 100644 index 0000000000..3fe7146c59 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACAppExtension.m @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAppExtension.h" + +@implementation MSACAppExtension + +#pragma mark - MSACSerializableObject + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [NSMutableDictionary new]; + if (self.appId) { + dict[kMSACAppId] = self.appId; + } + if (self.ver) { + dict[kMSACAppVer] = self.ver; + } + if (self.name) { + dict[kMSACAppName] = self.name; + } + if (self.locale) { + dict[kMSACAppLocale] = self.locale; + } + if (self.userId) { + dict[kMSACAppUserId] = self.userId; + } + return dict.count == 0 ? nil : dict; +} + +#pragma mark - MSACModel + +- (BOOL)isValid { + + // All attributes are optional. + return YES; +} + +#pragma mark - NSObject + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACAppExtension class]]) { + return NO; + } + MSACAppExtension *appExt = (MSACAppExtension *)object; + return ((!self.appId && !appExt.appId) || [self.appId isEqualToString:appExt.appId]) && + ((!self.ver && !appExt.ver) || [self.ver isEqualToString:appExt.ver]) && + ((!self.name && !appExt.name) || [self.name isEqualToString:appExt.name]) && + ((!self.locale && !appExt.locale) || [self.locale isEqualToString:appExt.locale]) && + ((!self.userId && !appExt.userId) || [self.userId isEqualToString:appExt.userId]); +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + if ((self = [super init])) { + _appId = [coder decodeObjectForKey:kMSACAppId]; + _ver = [coder decodeObjectForKey:kMSACAppVer]; + _name = [coder decodeObjectForKey:kMSACAppName]; + _locale = [coder decodeObjectForKey:kMSACAppLocale]; + _userId = [coder decodeObjectForKey:kMSACAppUserId]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:self.appId forKey:kMSACAppId]; + [coder encodeObject:self.ver forKey:kMSACAppVer]; + [coder encodeObject:self.name forKey:kMSACAppName]; + [coder encodeObject:self.locale forKey:kMSACAppLocale]; + [coder encodeObject:self.userId forKey:kMSACAppUserId]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACCSData.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACCSData.h new file mode 100644 index 0000000000..f6771fc353 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACCSData.h @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACModel.h" +#import "MSACOrderedDictionary.h" +#import "MSACSerializableObject.h" + +static NSString *const kMSACDataBaseData = @"baseData"; +static NSString *const kMSACDataBaseType = @"baseType"; + +/** + * The data object contains Part B and Part C properties. + */ +@interface MSACCSData : NSObject + +@property(atomic, copy) NSDictionary *properties; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACCSData.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACCSData.m new file mode 100644 index 0000000000..69a7ebe79c --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACCSData.m @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACCSData.h" +#import "MSACOrderedDictionary.h" + +@implementation MSACCSData + +#pragma mark - MSACSerializableObject + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict; + if (self.properties) { + dict = [MSACOrderedDictionary new]; + + // ORDER MATTERS: Make sure baseType and baseData appear first in part B + if (self.properties[kMSACDataBaseType]) { + dict[kMSACDataBaseType] = self.properties[kMSACDataBaseType]; + } + if (self.properties[kMSACDataBaseData]) { + dict[kMSACDataBaseData] = self.properties[kMSACDataBaseData]; + } + [dict addEntriesFromDictionary:self.properties]; + } + return dict; +} + +#pragma mark - MSACModel + +- (BOOL)isValid { + + // All attributes are optional. + return YES; +} + +#pragma mark - NSObject + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACCSData class]]) { + return NO; + } + MSACCSData *csData = (MSACCSData *)object; + return (!self.properties && !csData.properties) || [self.properties isEqualToDictionary:csData.properties]; +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + if ((self = [super init])) { + _properties = [coder decodeObject]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeRootObject:self.properties]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACCSExtensions.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACCSExtensions.h new file mode 100644 index 0000000000..8df51a4991 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACCSExtensions.h @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACModel.h" +#import "MSACSerializableObject.h" + +@class MSACAppExtension; +@class MSACDeviceExtension; +@class MSACLocExtension; +@class MSACMetadataExtension; +@class MSACNetExtension; +@class MSACOSExtension; +@class MSACProtocolExtension; +@class MSACSDKExtension; +@class MSACUserExtension; + +static NSString *const kMSACCSAppExt = @"app"; +static NSString *const kMSACCSDeviceExt = @"device"; +static NSString *const kMSACCSLocExt = @"loc"; +static NSString *const kMSACCSMetadataExt = @"metadata"; +static NSString *const kMSACCSNetExt = @"net"; +static NSString *const kMSACCSOSExt = @"os"; +static NSString *const kMSACCSProtocolExt = @"protocol"; +static NSString *const kMSACCSUserExt = @"user"; +static NSString *const kMSACCSSDKExt = @"sdk"; + +/** + * Part A extensions. + */ +@interface MSACCSExtensions : NSObject + +/** + * The Metadata extension. + */ +@property(nonatomic) MSACMetadataExtension *metadataExt; + +/** + * The Protocol extension. + */ +@property(nonatomic) MSACProtocolExtension *protocolExt; + +/** + * The User extension. + */ +@property(nonatomic) MSACUserExtension *userExt; + +/** + * The Device extension. + */ +@property(nonatomic) MSACDeviceExtension *deviceExt; + +/** + * The OS extension. + */ +@property(nonatomic) MSACOSExtension *osExt; + +/** + * The App extension. + */ +@property(nonatomic) MSACAppExtension *appExt; + +/** + * The network extension. + */ +@property(nonatomic) MSACNetExtension *netExt; + +/** + * The SDK extension. + */ +@property(nonatomic) MSACSDKExtension *sdkExt; + +/** + * The Loc extension. + */ +@property(nonatomic) MSACLocExtension *locExt; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACCSExtensions.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACCSExtensions.m new file mode 100644 index 0000000000..88d033dddc --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACCSExtensions.m @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACCSExtensions.h" +#import "MSACAbstractLogInternal.h" +#import "MSACAppExtension.h" +#import "MSACDeviceExtension.h" +#import "MSACLocExtension.h" +#import "MSACMetadataExtension.h" +#import "MSACNetExtension.h" +#import "MSACOSExtension.h" +#import "MSACProtocolExtension.h" +#import "MSACSDKExtension.h" +#import "MSACUserExtension.h" + +@implementation MSACCSExtensions + +#pragma mark - MSACSerializableObject + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [NSMutableDictionary new]; + if (self.metadataExt) { + dict[kMSACCSMetadataExt] = [self.metadataExt serializeToDictionary]; + } + if (self.protocolExt) { + dict[kMSACCSProtocolExt] = [self.protocolExt serializeToDictionary]; + } + if (self.userExt) { + dict[kMSACCSUserExt] = [self.userExt serializeToDictionary]; + } + if (self.deviceExt) { + dict[kMSACCSDeviceExt] = [self.deviceExt serializeToDictionary]; + } + if (self.osExt) { + dict[kMSACCSOSExt] = [self.osExt serializeToDictionary]; + } + if (self.appExt) { + dict[kMSACCSAppExt] = [self.appExt serializeToDictionary]; + } + if (self.netExt) { + dict[kMSACCSNetExt] = [self.netExt serializeToDictionary]; + } + if (self.sdkExt) { + dict[kMSACCSSDKExt] = [self.sdkExt serializeToDictionary]; + } + if (self.locExt) { + dict[kMSACCSLocExt] = [self.locExt serializeToDictionary]; + } + return dict; +} + +#pragma mark - MSACModel + +- (BOOL)isValid { +#define MSACLOG_VALIDATE_OPTIONAL_OBJECT(fieldName) MSACLOG_VALIDATE(fieldName, self.fieldName == nil || [self.fieldName isValid]) + return MSACLOG_VALIDATE_OPTIONAL_OBJECT(metadataExt) && MSACLOG_VALIDATE_OPTIONAL_OBJECT(protocolExt) && + MSACLOG_VALIDATE_OPTIONAL_OBJECT(userExt) && MSACLOG_VALIDATE_OPTIONAL_OBJECT(deviceExt) && + MSACLOG_VALIDATE_OPTIONAL_OBJECT(osExt) && MSACLOG_VALIDATE_OPTIONAL_OBJECT(appExt) && MSACLOG_VALIDATE_OPTIONAL_OBJECT(netExt) && + MSACLOG_VALIDATE_OPTIONAL_OBJECT(sdkExt) && MSACLOG_VALIDATE_OPTIONAL_OBJECT(locExt); +} + +#pragma mark - NSObject + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACCSExtensions class]]) { + return NO; + } + MSACCSExtensions *csExt = (MSACCSExtensions *)object; + return ((!self.protocolExt && !csExt.protocolExt) || [self.protocolExt isEqual:csExt.protocolExt]) && + ((!self.metadataExt && !csExt.metadataExt) || [self.metadataExt isEqual:csExt.metadataExt]) && + ((!self.userExt && !csExt.userExt) || [self.userExt isEqual:csExt.userExt]) && + ((!self.deviceExt && !csExt.deviceExt) || [self.deviceExt isEqual:csExt.deviceExt]) && + ((!self.osExt && !csExt.osExt) || [self.osExt isEqual:csExt.osExt]) && + ((!self.appExt && !csExt.appExt) || [self.appExt isEqual:csExt.appExt]) && + ((!self.netExt && !csExt.netExt) || [self.netExt isEqual:csExt.netExt]) && + ((!self.sdkExt && !csExt.sdkExt) || [self.sdkExt isEqual:csExt.sdkExt]) && + ((!self.locExt && !csExt.locExt) || [self.locExt isEqual:csExt.locExt]); +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + if ((self = [super init])) { + _metadataExt = [coder decodeObjectForKey:kMSACCSMetadataExt]; + _protocolExt = [coder decodeObjectForKey:kMSACCSProtocolExt]; + _userExt = [coder decodeObjectForKey:kMSACCSUserExt]; + _deviceExt = [coder decodeObjectForKey:kMSACCSDeviceExt]; + _osExt = [coder decodeObjectForKey:kMSACCSOSExt]; + _appExt = [coder decodeObjectForKey:kMSACCSAppExt]; + _netExt = [coder decodeObjectForKey:kMSACCSNetExt]; + _sdkExt = [coder decodeObjectForKey:kMSACCSSDKExt]; + _locExt = [coder decodeObjectForKey:kMSACCSLocExt]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:self.metadataExt forKey:kMSACCSMetadataExt]; + [coder encodeObject:self.protocolExt forKey:kMSACCSProtocolExt]; + [coder encodeObject:self.userExt forKey:kMSACCSUserExt]; + [coder encodeObject:self.deviceExt forKey:kMSACCSDeviceExt]; + [coder encodeObject:self.osExt forKey:kMSACCSOSExt]; + [coder encodeObject:self.appExt forKey:kMSACCSAppExt]; + [coder encodeObject:self.netExt forKey:kMSACCSNetExt]; + [coder encodeObject:self.sdkExt forKey:kMSACCSSDKExt]; + [coder encodeObject:self.locExt forKey:kMSACCSLocExt]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACCommonSchemaLog.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACCommonSchemaLog.h new file mode 100644 index 0000000000..ddf717b0e8 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACCommonSchemaLog.h @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAbstractLogInternal.h" +#import "MSACModel.h" +#import "MSACSerializableObject.h" + +@class MSACCSExtensions; +@class MSACCSData; + +// Keys +static NSString *const kMSACCSCV = @"cV"; +static NSString *const kMSACCSData = @"data"; +static NSString *const kMSACCSExt = @"ext"; +static NSString *const kMSACCSFlags = @"flags"; +static NSString *const kMSACCSIKey = @"iKey"; +static NSString *const kMSACCSName = @"name"; +static NSString *const kMSACCSPopSample = @"popSample"; +static NSString *const kMSACCSTime = @"time"; +static NSString *const kMSACCSVer = @"ver"; + +// Values +static NSString *const kMSACCSVerValue = @"3.0"; + +/** + * Common schema has one event type with extensions, everything is called an event. + */ +@interface MSACCommonSchemaLog : MSACAbstractLog + +/** + * The version of the schema. The format is a string with major and minor such as 3.0. + */ +@property(nonatomic, copy) NSString *ver; + +/** + * The event name. + */ +@property(nonatomic, copy) NSString *name; + +/** + * The effective sample rate for this event at the time it was generated by a client. The valid range is from a minimum value of one out of + * every 100 million devices which is "0.000001", all the way up to all devices which is "100". The default value is 100. + */ +@property(nonatomic) double popSample; + +/** + * An identifier used to identify applications or other logical groupings of events. + */ +@property(nonatomic, copy) NSString *iKey; + +/** + * Event Property flags contain a collection of bits that describe how the event should be processed by the One Collector pipeline. + */ +@property(nonatomic) int64_t flags; + +/** + * Correlation Vector: A single field for tracking partial order of related telemetry events across component boundaries. + */ +@property(nonatomic, copy) NSString *cV; + +/** + * Part A extensions. + */ +@property(nonatomic) MSACCSExtensions *ext; + +/** + * Part C + */ +@property(nonatomic) MSACCSData *data; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACCommonSchemaLog.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACCommonSchemaLog.m new file mode 100644 index 0000000000..3d4a99c9f9 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACCommonSchemaLog.m @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACCommonSchemaLog.h" +#import "MSACCSData.h" +#import "MSACCSExtensions.h" +#import "MSACModel.h" +#import "MSACOrderedDictionary.h" +#import "MSACUtility+Date.h" + +@implementation MSACCommonSchemaLog + +#pragma mark - MSACSerializableObject + +- (NSMutableDictionary *)serializeToDictionary { + + // ORDER MATTERS: Make sure ver, name, timestamp, popSample, iKey and flags appear first in part A. + // No call to super here, it already contains everything needed for CS JSON serialization. + NSMutableDictionary *dict = [MSACOrderedDictionary new]; + if (self.ver) { + dict[kMSACCSVer] = self.ver; + } + if (self.name) { + dict[kMSACCSName] = self.name; + } + + // Timestamp already exists in the parent implementation but the serialized key is different. + if (self.timestamp) { + dict[kMSACCSTime] = [MSACUtility dateToISO8601:self.timestamp]; + } + + // TODO: Not supporting popSample and cV today. When added, popSample needs to be ordered between timestamp and iKey. + if (self.iKey) { + dict[kMSACCSIKey] = self.iKey; + } + if (self.flags) { + dict[kMSACCSFlags] = @(self.flags); + } + if (self.ext) { + dict[kMSACCSExt] = [self.ext serializeToDictionary]; + } + if (self.data) { + dict[kMSACCSData] = [self.data serializeToDictionary]; + } + return dict; +} + +#pragma mark - MSACModel + +- (BOOL)isValid { + + // Do not call [super isValid] here as CS logs don't require the same validation as AC logs except for timestamp. + return MSACLOG_VALIDATE_NOT_NIL(timestamp) && MSACLOG_VALIDATE_NOT_NIL(ver) && MSACLOG_VALIDATE_NOT_NIL(name); +} + +#pragma mark - NSObject + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACCommonSchemaLog class]] || ![super isEqual:object]) { + return NO; + } + + MSACCommonSchemaLog *csLog = (MSACCommonSchemaLog *)object; + return ((!self.ver && !csLog.ver) || [self.ver isEqualToString:csLog.ver]) && + ((!self.name && !csLog.name) || [self.name isEqualToString:csLog.name]) && self.popSample == csLog.popSample && + ((!self.iKey && !csLog.iKey) || [self.iKey isEqualToString:csLog.iKey]) && self.flags == csLog.flags && + ((!self.cV && !csLog.cV) || [self.cV isEqualToString:csLog.cV]) && ((!self.ext && !csLog.ext) || [self.ext isEqual:csLog.ext]) && + ((!self.data && !csLog.data) || [self.data isEqual:csLog.data]); +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + if ((self = [super initWithCoder:coder])) { + _ver = [coder decodeObjectForKey:kMSACCSVer]; + _name = [coder decodeObjectForKey:kMSACCSName]; + _popSample = [coder decodeDoubleForKey:kMSACCSPopSample]; + _iKey = [coder decodeObjectForKey:kMSACCSIKey]; + _flags = [coder decodeInt64ForKey:kMSACCSFlags]; + _cV = [coder decodeObjectForKey:kMSACCSCV]; + _ext = [coder decodeObjectForKey:kMSACCSExt]; + _data = [coder decodeObjectForKey:kMSACCSData]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.ver forKey:kMSACCSVer]; + [coder encodeObject:self.name forKey:kMSACCSName]; + [coder encodeDouble:self.popSample forKey:kMSACCSPopSample]; + [coder encodeObject:self.iKey forKey:kMSACCSIKey]; + [coder encodeInt64:self.flags forKey:kMSACCSFlags]; + [coder encodeObject:self.cV forKey:kMSACCSCV]; + [coder encodeObject:self.ext forKey:kMSACCSExt]; + [coder encodeObject:self.data forKey:kMSACCSData]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACDeviceExtension.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACDeviceExtension.h new file mode 100644 index 0000000000..77894a3273 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACDeviceExtension.h @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACModel.h" +#import "MSACSerializableObject.h" + +static NSString *const kMSACDeviceLocalId = @"localId"; + +/** + * Device extension contains device information. + */ +@interface MSACDeviceExtension : NSObject + +@property(nonatomic, copy) NSString *localId; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACDeviceExtension.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACDeviceExtension.m new file mode 100644 index 0000000000..9240cad26d --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACDeviceExtension.m @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACDeviceExtension.h" + +@implementation MSACDeviceExtension + +#pragma mark - MSACSerializableObject + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict; + if (self.localId) { + dict = [NSMutableDictionary new]; + dict[kMSACDeviceLocalId] = self.localId; + } + return dict; +} + +#pragma mark - MSACModel + +- (BOOL)isValid { + + // All attributes are optional. + return YES; +} + +#pragma mark - NSObject + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACDeviceExtension class]]) { + return NO; + } + MSACDeviceExtension *deviceExt = (MSACDeviceExtension *)object; + return (!self.localId && !deviceExt.localId) || [self.localId isEqualToString:deviceExt.localId]; +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + if ((self = [super init])) { + _localId = [coder decodeObjectForKey:kMSACDeviceLocalId]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:self.localId forKey:kMSACDeviceLocalId]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACLocExtension.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACLocExtension.h new file mode 100644 index 0000000000..99a2244d9d --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACLocExtension.h @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACModel.h" +#import "MSACSerializableObject.h" + +static NSString *const kMSACTimezone = @"tz"; + +/** + * Describes the location from which the event was logged. + */ +@interface MSACLocExtension : NSObject + +/** + * Time zone on the device. + */ +@property(nonatomic, copy) NSString *tz; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACLocExtension.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACLocExtension.m new file mode 100644 index 0000000000..e5e2c5e9b9 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACLocExtension.m @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACLocExtension.h" + +@implementation MSACLocExtension + +#pragma mark - MSACSerializableObject + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict; + if (self.tz) { + dict = [NSMutableDictionary new]; + dict[kMSACTimezone] = self.tz; + } + return dict; +} + +#pragma mark - MSACModel + +- (BOOL)isValid { + + // All attributes are optional. + return YES; +} +#pragma mark - NSObject + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACLocExtension class]]) { + return NO; + } + + MSACLocExtension *locExt = (MSACLocExtension *)object; + return (!self.tz && !locExt.tz) || [self.tz isEqualToString:locExt.tz]; +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + if ((self = [super init])) { + _tz = [coder decodeObjectForKey:kMSACTimezone]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:self.tz forKey:kMSACTimezone]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACMetadataExtension.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACMetadataExtension.h new file mode 100644 index 0000000000..0c78c3de73 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACMetadataExtension.h @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACModel.h" +#import "MSACSerializableObject.h" +#import + +static NSString *const kMSACFieldDelimiter = @"f"; + +/** + * The metadata section contains additional typing/schema-related information for each field in the Part B or Part C payload. + */ +@interface MSACMetadataExtension : NSObject + +/** + * Additional typing/schema-related information for each field in the Part B or Part C payload. + */ +@property(atomic, copy) NSDictionary *metadata; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACMetadataExtension.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACMetadataExtension.m new file mode 100644 index 0000000000..b60ccfcf02 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACMetadataExtension.m @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACMetadataExtension.h" + +@implementation MSACMetadataExtension + +#pragma mark - MSACSerializableObject + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict; + if (self.metadata) { + dict = [NSMutableDictionary new]; + [dict addEntriesFromDictionary:self.metadata]; + } + return dict; +} + +#pragma mark - MSACModel + +- (BOOL)isValid { + + // All attributes are optional. + return YES; +} + +#pragma mark - NSObject + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACMetadataExtension class]]) { + return NO; + } + MSACMetadataExtension *csMetadata = (MSACMetadataExtension *)object; + return (!self.metadata && !csMetadata) || [self.metadata isEqualToDictionary:csMetadata.metadata]; +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + if ((self = [super init])) { + _metadata = [coder decodeObject]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeRootObject:self.metadata]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACNetExtension.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACNetExtension.h new file mode 100644 index 0000000000..2f63bee32e --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACNetExtension.h @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACModel.h" +#import "MSACSerializableObject.h" + +static NSString *const kMSACNetProvider = @"provider"; + +/** + * The network extension contains network properties. + */ +@interface MSACNetExtension : NSObject + +/** + * The network provider. + */ +@property(nonatomic, copy) NSString *provider; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACNetExtension.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACNetExtension.m new file mode 100644 index 0000000000..b949c445f6 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACNetExtension.m @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACNetExtension.h" + +@implementation MSACNetExtension + +#pragma mark - MSACSerializableObject + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict; + if (self.provider) { + dict = [NSMutableDictionary new]; + dict[kMSACNetProvider] = self.provider; + } + return dict; +} + +#pragma mark - MSACModel + +- (BOOL)isValid { + + // All attributes are optional. + return YES; +} + +#pragma mark - NSObject + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACNetExtension class]]) { + return NO; + } + MSACNetExtension *netExt = (MSACNetExtension *)object; + return ((!self.provider && !netExt.provider) || [self.provider isEqualToString:netExt.provider]); +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + if ((self = [super init])) { + _provider = [coder decodeObjectForKey:kMSACNetProvider]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:self.provider forKey:kMSACNetProvider]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACOSExtension.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACOSExtension.h new file mode 100644 index 0000000000..a715f6b705 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACOSExtension.h @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACModel.h" +#import "MSACSerializableObject.h" + +static NSString *const kMSACOSName = @"name"; +static NSString *const kMSACOSVer = @"ver"; + +/** + * The OS extension tracks common os elements that are not available in the core envelope. + */ +@interface MSACOSExtension : NSObject + +/** + * The OS name. + */ +@property(nonatomic, copy) NSString *name; + +/** + * The OS version. + */ +@property(nonatomic, copy) NSString *ver; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACOSExtension.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACOSExtension.m new file mode 100644 index 0000000000..9629605734 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACOSExtension.m @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACOSExtension.h" + +@implementation MSACOSExtension + +#pragma mark - MSACSerializableObject + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [NSMutableDictionary new]; + if (self.ver) { + dict[kMSACOSVer] = self.ver; + } + if (self.name) { + dict[kMSACOSName] = self.name; + } + return dict.count == 0 ? nil : dict; +} + +#pragma mark - MSACModel + +- (BOOL)isValid { + + // All attributes are optional. + return YES; +} + +#pragma mark - NSObject + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACOSExtension class]]) { + return NO; + } + MSACOSExtension *osExt = (MSACOSExtension *)object; + return ((!self.ver && !osExt.ver) || [self.ver isEqualToString:osExt.ver]) && + ((!self.name && !osExt.name) || [self.name isEqualToString:osExt.name]); +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + if ((self = [super init])) { + _ver = [coder decodeObjectForKey:kMSACOSVer]; + _name = [coder decodeObjectForKey:kMSACOSName]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:self.ver forKey:kMSACOSVer]; + [coder encodeObject:self.name forKey:kMSACOSName]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACProtocolExtension.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACProtocolExtension.h new file mode 100644 index 0000000000..dbeb5c12f9 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACProtocolExtension.h @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACModel.h" +#import "MSACSerializableObject.h" + +static NSString *const kMSACDevMake = @"devMake"; +static NSString *const kMSACDevModel = @"devModel"; +static NSString *const kMSACTicketKeys = @"ticketKeys"; + +/** + * The Protocol extension contains device specific information. + */ +@interface MSACProtocolExtension : NSObject + +/** + * Ticket keys. + */ +@property(nonatomic) NSArray *ticketKeys; + +/** + * The device's manufacturer. + */ +@property(nonatomic, copy) NSString *devMake; + +/** + * The device's model. + */ +@property(nonatomic, copy) NSString *devModel; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACProtocolExtension.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACProtocolExtension.m new file mode 100644 index 0000000000..83c9273d80 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACProtocolExtension.m @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACProtocolExtension.h" + +@implementation MSACProtocolExtension + +#pragma mark - MSACSerializableObject + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [NSMutableDictionary new]; + if (self.ticketKeys) { + dict[kMSACTicketKeys] = self.ticketKeys; + } + if (self.devMake) { + dict[kMSACDevMake] = self.devMake; + } + if (self.devModel) { + dict[kMSACDevModel] = self.devModel; + } + return dict.count == 0 ? nil : dict; +} + +#pragma mark - MSACModel + +- (BOOL)isValid { + + // All attributes are optional. + return YES; +} + +#pragma mark - NSObject + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACProtocolExtension class]]) { + return NO; + } + MSACProtocolExtension *protocolExt = (MSACProtocolExtension *)object; + return ((!self.ticketKeys && !protocolExt.ticketKeys) || [self.ticketKeys isEqualToArray:protocolExt.ticketKeys]) && + ((!self.devMake && !protocolExt.devMake) || [self.devMake isEqualToString:protocolExt.devMake]) && + ((!self.devModel && !protocolExt.devModel) || [self.devModel isEqualToString:protocolExt.devModel]); +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + if ((self = [super init])) { + _ticketKeys = [coder decodeObjectForKey:kMSACTicketKeys]; + _devMake = [coder decodeObjectForKey:kMSACDevMake]; + _devModel = [coder decodeObjectForKey:kMSACDevModel]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:self.ticketKeys forKey:kMSACTicketKeys]; + [coder encodeObject:self.devMake forKey:kMSACDevMake]; + [coder encodeObject:self.devModel forKey:kMSACDevModel]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACSDKExtension.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACSDKExtension.h new file mode 100644 index 0000000000..d287caafe0 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACSDKExtension.h @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACModel.h" +#import "MSACSerializableObject.h" + +static NSString *const kMSACSDKEpoch = @"epoch"; +static NSString *const kMSACSDKInstallId = @"installId"; +static NSString *const kMSACSDKLibVer = @"libVer"; +static NSString *const kMSACSDKSeq = @"seq"; + +/** + * The SDK extension is used by platform specific library to record field that are specifically required for a specific SDK. + */ +@interface MSACSDKExtension : NSObject + +/** + * The SDK version. + */ +@property(nonatomic, copy) NSString *libVer; + +/** + * ID incremented for each SDK initialization. + */ +@property(nonatomic, copy) NSString *epoch; + +/** + * ID incremented for each event. + */ +@property(nonatomic) int64_t seq; + +/** + * ID created on first-time SDK initialization. It may serves as the device.localId. + */ +@property(nonatomic) NSUUID *installId; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACSDKExtension.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACSDKExtension.m new file mode 100644 index 0000000000..cf1dc2248c --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACSDKExtension.m @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACSDKExtension.h" + +@implementation MSACSDKExtension + +#pragma mark - MSACSerializableObject + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [NSMutableDictionary new]; + if (self.libVer) { + dict[kMSACSDKLibVer] = self.libVer; + } + if (self.epoch) { + dict[kMSACSDKEpoch] = self.epoch; + } + if (self.installId) { + dict[kMSACSDKInstallId] = [self.installId UUIDString]; + } + + // The initial value corresponding to an epoch on a device should be 1, 0 means no seq attributes. + if (self.seq) { + dict[kMSACSDKSeq] = @(self.seq); + } + return dict.count == 0 ? nil : dict; +} + +#pragma mark - MSACModel + +- (BOOL)isValid { + + // All attributes are optional. + return YES; +} + +#pragma mark - NSObject + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACSDKExtension class]]) { + return NO; + } + MSACSDKExtension *sdkExt = (MSACSDKExtension *)object; + return ((!self.libVer && !sdkExt.libVer) || [self.libVer isEqualToString:sdkExt.libVer]) && + ((!self.epoch && !sdkExt.epoch) || [self.epoch isEqualToString:sdkExt.epoch]) && (self.seq == sdkExt.seq) && + ((!self.installId && !sdkExt.installId) || [self.installId isEqual:sdkExt.installId]); +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + if ((self = [super init])) { + _libVer = [coder decodeObjectForKey:kMSACSDKLibVer]; + _epoch = [coder decodeObjectForKey:kMSACSDKEpoch]; + _seq = [coder decodeInt64ForKey:kMSACSDKSeq]; + _installId = [coder decodeObjectForKey:kMSACSDKInstallId]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:self.libVer forKey:kMSACSDKLibVer]; + [coder encodeObject:self.epoch forKey:kMSACSDKEpoch]; + [coder encodeInt64:self.seq forKey:kMSACSDKSeq]; + [coder encodeObject:self.installId forKey:kMSACSDKInstallId]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACUserExtension.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACUserExtension.h new file mode 100644 index 0000000000..4c8cf9d794 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACUserExtension.h @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACModel.h" +#import "MSACSerializableObject.h" + +static NSString *const kMSACUserLocale = @"locale"; +static NSString *const kMSACUserLocalId = @"localId"; + +/** + * The “user” extension tracks common user elements that are not available in the core envelope. + */ +@interface MSACUserExtension : NSObject + +/** + * Local Id. + */ +@property(nonatomic, copy) NSString *localId; + +/** + * User's locale. + */ +@property(nonatomic, copy) NSString *locale; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACUserExtension.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACUserExtension.m new file mode 100644 index 0000000000..2898405677 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/CommonSchema/MSACUserExtension.m @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACUserExtension.h" + +@implementation MSACUserExtension + +#pragma mark - MSACSerializableObject + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [NSMutableDictionary new]; + if (self.localId) { + dict[kMSACUserLocalId] = self.localId; + } + if (self.locale) { + dict[kMSACUserLocale] = self.locale; + } + return dict.count == 0 ? nil : dict; +} + +#pragma mark - MSACModel + +- (BOOL)isValid { + + // All attributes are optional. + return YES; +} + +#pragma mark - NSObject + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACUserExtension class]]) { + return NO; + } + MSACUserExtension *userExt = (MSACUserExtension *)object; + return ((!self.localId && !userExt.localId) || [self.localId isEqualToString:userExt.localId]) && + ((!self.locale && !userExt.locale) || [self.locale isEqualToString:userExt.locale]); +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + if ((self = [super init])) { + _localId = [coder decodeObjectForKey:kMSACUserLocalId]; + _locale = [coder decodeObjectForKey:kMSACUserLocale]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:self.localId forKey:kMSACUserLocalId]; + [coder encodeObject:self.locale forKey:kMSACUserLocale]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACAbstractLog.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACAbstractLog.m new file mode 100644 index 0000000000..db91a70d84 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACAbstractLog.m @@ -0,0 +1,246 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAbstractLogInternal.h" +#import "MSACAbstractLogPrivate.h" +#import "MSACAppExtension.h" +#import "MSACCSExtensions.h" +#import "MSACConstants+Internal.h" +#import "MSACDevice.h" +#import "MSACDeviceExtension.h" +#import "MSACDeviceInternal.h" +#import "MSACLocExtension.h" +#import "MSACNetExtension.h" +#import "MSACOSExtension.h" +#import "MSACProtocolExtension.h" +#import "MSACSDKExtension.h" +#import "MSACUserExtension.h" +#import "MSACUserIdContext.h" +#import "MSACUtility+Date.h" +#import "MSACUtility+StringFormatting.h" + +/** + * App namespace prefix for common schema. + */ +static NSString *const kMSACAppNamespacePrefix = @"I"; + +@implementation MSACAbstractLog + +@synthesize type = _type; +@synthesize timestamp = _timestamp; +@synthesize sid = _sid; +@synthesize distributionGroupId = _distributionGroupId; +@synthesize userId = _userId; +@synthesize device = _device; +@synthesize tag = _tag; + +- (instancetype)init { + self = [super init]; + if (self) { + } + return self; +} + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [NSMutableDictionary new]; + + if (self.type) { + dict[kMSACType] = self.type; + } + if (self.timestamp) { + dict[kMSACTimestamp] = [MSACUtility dateToISO8601:self.timestamp]; + } + if (self.sid) { + dict[kMSACSId] = self.sid; + } + if (self.distributionGroupId) { + dict[kMSACDistributionGroupId] = self.distributionGroupId; + } + if (self.userId) { + dict[kMSACUserId] = self.userId; + } + if (self.device) { + dict[kMSACDevice] = [self.device serializeToDictionary]; + } + return dict; +} + +- (BOOL)isValid { + return MSACLOG_VALIDATE_NOT_NIL(type) && MSACLOG_VALIDATE_NOT_NIL(timestamp) && + MSACLOG_VALIDATE(device, self.device != nil && [self.device isValid]); +} + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACAbstractLog class]]) { + return NO; + } + MSACAbstractLog *log = (MSACAbstractLog *)object; + return ((!self.tag && !log.tag) || [self.tag isEqual:log.tag]) && ((!self.type && !log.type) || [self.type isEqualToString:log.type]) && + ((!self.timestamp && !log.timestamp) || [self.timestamp isEqualToDate:log.timestamp]) && + ((!self.sid && !log.sid) || [self.sid isEqualToString:log.sid]) && + ((!self.distributionGroupId && !log.distributionGroupId) || [self.distributionGroupId isEqualToString:log.distributionGroupId]) && + ((!self.userId && !log.userId) || [self.userId isEqualToString:log.userId]) && + ((!self.device && !log.device) || [self.device isEqual:log.device]); +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super init]; + if (self) { + _type = [coder decodeObjectForKey:kMSACType]; + _timestamp = [coder decodeObjectForKey:kMSACTimestamp]; + _sid = [coder decodeObjectForKey:kMSACSId]; + _distributionGroupId = [coder decodeObjectForKey:kMSACDistributionGroupId]; + _userId = [coder decodeObjectForKey:kMSACUserId]; + _device = [coder decodeObjectForKey:kMSACDevice]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:self.type forKey:kMSACType]; + [coder encodeObject:self.timestamp forKey:kMSACTimestamp]; + [coder encodeObject:self.sid forKey:kMSACSId]; + [coder encodeObject:self.distributionGroupId forKey:kMSACDistributionGroupId]; + [coder encodeObject:self.userId forKey:kMSACUserId]; + [coder encodeObject:self.device forKey:kMSACDevice]; +} + +#pragma mark - Utility + +- (NSString *)serializeLogWithPrettyPrinting:(BOOL)prettyPrint { + NSString *jsonString; + NSJSONWritingOptions printOptions = prettyPrint ? NSJSONWritingPrettyPrinted : (NSJSONWritingOptions)0; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:[self serializeToDictionary] options:printOptions error:nil]; + if (jsonData) { + jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + jsonString = [jsonString stringByReplacingOccurrencesOfString:@"\\/" withString:@"/"]; + } + return jsonString; +} + +#pragma mark - Transmission Target logic + +- (NSSet *)transmissionTargetTokens { + @synchronized(self) { + return _transmissionTargetTokens; + } +} + +- (void)addTransmissionTargetToken:(NSString *)token { + @synchronized(self) { + if (self.transmissionTargetTokens == nil) { + self.transmissionTargetTokens = [NSSet new]; + } + NSMutableSet *mutableSet = [self.transmissionTargetTokens mutableCopy]; + [mutableSet addObject:token]; + self.transmissionTargetTokens = mutableSet; + } +} + +#pragma mark - MSACLogConversion + +- (NSArray *)toCommonSchemaLogsWithFlags:(MSACFlags)flags { + NSMutableArray *csLogs = [NSMutableArray new]; + for (NSString *token in self.transmissionTargetTokens) { + MSACCommonSchemaLog *csLog = [self toCommonSchemaLogForTargetToken:token flags:(MSACFlags)flags]; + if (csLog) { + [csLogs addObject:csLog]; + } + } + + // Return nil if none are converted. + return (csLogs.count > 0) ? csLogs : nil; +} + +#pragma mark - Helper + +- (MSACCommonSchemaLog *)toCommonSchemaLogForTargetToken:(NSString *)token flags:(MSACFlags)flags { + MSACCommonSchemaLog *csLog = [MSACCommonSchemaLog new]; + csLog.transmissionTargetTokens = [NSSet setWithObject:token]; + csLog.ver = kMSACCSVerValue; + csLog.timestamp = self.timestamp; + + // TODO popSample not supported at this time. + + // Calculate iKey based on the target token. + csLog.iKey = [MSACUtility iKeyFromTargetToken:token]; + csLog.flags = flags; + + // TODO cV not supported at this time. + + // Setup extensions. + csLog.ext = [MSACCSExtensions new]; + + // Protocol extension. + csLog.ext.protocolExt = [MSACProtocolExtension new]; + csLog.ext.protocolExt.devMake = self.device.oemName; + csLog.ext.protocolExt.devModel = self.device.model; + + // User extension. + csLog.ext.userExt = [MSACUserExtension new]; + csLog.ext.userExt.localId = [MSACUserIdContext prefixedUserIdFromUserId:self.userId]; + + // FIXME Country code can be wrong if the locale doesn't correspond to the region in the setting (i.e.:fr_US). Convert user local to use + // dash (-) as the separator as described in RFC 4646. E.g., zh-Hans-CN. + csLog.ext.userExt.locale = [self.device.locale stringByReplacingOccurrencesOfString:@"_" withString:@"-"]; + + // OS extension. + csLog.ext.osExt = [MSACOSExtension new]; + csLog.ext.osExt.name = self.device.osName; + csLog.ext.osExt.ver = [self combineOsVersion:self.device.osVersion withBuild:self.device.osBuild]; + + // App extension. + csLog.ext.appExt = [MSACAppExtension new]; + csLog.ext.appExt.appId = + [NSString stringWithFormat:@"%@%@%@", kMSACAppNamespacePrefix, kMSACCommonSchemaPrefixSeparator, self.device.appNamespace]; + csLog.ext.appExt.ver = self.device.appVersion; + csLog.ext.appExt.locale = [[[NSBundle mainBundle] preferredLocalizations] firstObject]; + + // Network extension. + csLog.ext.netExt = [MSACNetExtension new]; + csLog.ext.netExt.provider = self.device.carrierName; + + // SDK extension. + csLog.ext.sdkExt = [MSACSDKExtension new]; + csLog.ext.sdkExt.libVer = [self combineSDKLibVer:self.device.sdkName withVersion:self.device.sdkVersion]; + + // Loc extension. + csLog.ext.locExt = [MSACLocExtension new]; + csLog.ext.locExt.tz = [self convertTimeZoneOffsetToISO8601:[self.device.timeZoneOffset integerValue]]; + + // Device extension. + csLog.ext.deviceExt = [MSACDeviceExtension new]; + + return csLog; +} + +- (NSString *)combineOsVersion:(NSString *)version withBuild:(NSString *)build { + NSString *combinedVersionAndBuild; + if (version && version.length) { + combinedVersionAndBuild = [NSString stringWithFormat:@"Version %@", version]; + } + if (build && build.length) { + combinedVersionAndBuild = [NSString stringWithFormat:@"%@ (Build %@)", combinedVersionAndBuild, build]; + } + return combinedVersionAndBuild; +} + +- (NSString *)combineSDKLibVer:(NSString *)name withVersion:(NSString *)version { + NSString *combinedVersion; + if (name && name.length && version && version.length) { + combinedVersion = [NSString stringWithFormat:@"%@-%@", name, version]; + } + return combinedVersion; +} + +- (NSString *)convertTimeZoneOffsetToISO8601:(NSInteger)timeZoneOffset { + NSInteger offsetInHour = timeZoneOffset / 60; + NSInteger remainingMinutes = labs(timeZoneOffset) % 60; + + // This will look like this: +hhh:mm. + return [NSString stringWithFormat:@"%+03ld:%02ld", (long)offsetInHour, (long)remainingMinutes]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACAbstractLogInternal.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACAbstractLogInternal.h new file mode 100644 index 0000000000..6491c9604c --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACAbstractLogInternal.h @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAbstractLog.h" +#import "MSACAppCenterInternal.h" +#import "MSACCommonSchemaLog.h" +#import "MSACConstants.h" +#import "MSACLog.h" +#import "MSACLogConversion.h" +#import "MSACSerializableObject.h" + +@interface MSACAbstractLog () + +/** + * Serialize logs into a JSON string. + * + * @param prettyPrint boolean indicates pretty printing. + * + * @return A serialized string. + */ +- (NSString *)serializeLogWithPrettyPrinting:(BOOL)prettyPrint; + +/** + * Convert an AppCenter log to the Common Schema 3.0 event log per tenant token. + * + * @param token The tenant token. + * @param flags Flags to set for the common schema log. + * + * @return A common schema log. + */ +- (MSACCommonSchemaLog *)toCommonSchemaLogForTargetToken:(NSString *)token flags:(MSACFlags)flags; + +@end + +#define MSACLOG_VALIDATE(fieldName, rule) \ + ({ \ + BOOL isValid = rule; \ + if (!isValid) { \ + MSACLogVerbose([MSACAppCenter logTag], @"%@: \"%@\" is not valid.", NSStringFromClass([self class]), @ #fieldName); \ + } \ + isValid; \ + }) + +#define MSACLOG_VALIDATE_NOT_NIL(fieldName) MSACLOG_VALIDATE(fieldName, self.fieldName != nil) diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACAbstractLogPrivate.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACAbstractLogPrivate.h new file mode 100644 index 0000000000..c9e4db59dd --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACAbstractLogPrivate.h @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +static NSString *const kMSACDevice = @"device"; +static NSString *const kMSACDistributionGroupId = @"distributionGroupId"; +static NSString *const kMSACSId = @"sid"; +static NSString *const kMSACType = @"type"; +static NSString *const kMSACTimestamp = @"timestamp"; +static NSString *const kMSACUserId = @"userId"; + +@interface MSACAbstractLog () + +/** + * List of transmission target tokens that this log should be sent to. + */ +@property(nonatomic) NSSet *transmissionTargetTokens; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACCustomPropertiesLog.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACCustomPropertiesLog.h new file mode 100644 index 0000000000..e3cf723859 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACCustomPropertiesLog.h @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAbstractLogInternal.h" + +@interface MSACCustomPropertiesLog : MSACAbstractLog + +/** + * Key/value pair properties. + */ +@property(nonatomic) NSDictionary *properties; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACCustomPropertiesLog.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACCustomPropertiesLog.m new file mode 100644 index 0000000000..ace141463e --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACCustomPropertiesLog.m @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACCustomPropertiesLog.h" +#import "MSACUtility+Date.h" + +static NSString *const kMSACCustomProperties = @"customProperties"; +static NSString *const kMSACProperties = @"properties"; +static NSString *const kMSACPropertyType = @"type"; +static NSString *const kMSACPropertyName = @"name"; +static NSString *const kMSACPropertyValue = @"value"; +static NSString *const kMSACPropertyTypeClear = @"clear"; +static NSString *const kMSACPropertyTypeBoolean = @"boolean"; +static NSString *const kMSACPropertyTypeNumber = @"number"; +static NSString *const kMSACPropertyTypeDateTime = @"dateTime"; +static NSString *const kMSACPropertyTypeString = @"string"; + +@implementation MSACCustomPropertiesLog + +@synthesize type = _type; +@synthesize properties = _properties; + +- (instancetype)init { + self = [super init]; + if (self) { + self.type = kMSACCustomProperties; + } + return self; +} + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACCustomPropertiesLog class]] || ![super isEqual:object]) { + return NO; + } + MSACCustomPropertiesLog *log = (MSACCustomPropertiesLog *)object; + return ((!self.properties && !log.properties) || [self.properties isEqualToDictionary:log.properties]); +} + +- (BOOL)isValid { + return [super isValid] && MSACLOG_VALIDATE(properties, self.properties && self.properties.count > 0); +} + +#pragma mark - MSACSerializableObject + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary]; + if (self.properties) { + NSMutableArray *propertiesArray = [NSMutableArray array]; + for (NSString *key in self.properties) { + NSObject *value = [self.properties objectForKey:key]; + NSMutableDictionary *property = [MSACCustomPropertiesLog serializeProperty:value]; + if (property) { + [property setObject:key forKey:kMSACPropertyName]; + [propertiesArray addObject:property]; + } + } + dict[kMSACProperties] = propertiesArray; + } + return dict; +} + +/** + * Serialize the value as custom property. + */ ++ (NSMutableDictionary *)serializeProperty:(NSObject *)value { + NSMutableDictionary *property = [NSMutableDictionary new]; + if ([value isKindOfClass:[NSNull class]]) { + [property setObject:kMSACPropertyTypeClear forKey:kMSACPropertyType]; + } else if ([value isKindOfClass:[NSNumber class]]) { + + /** + * NSNumber is “toll-free bridged” with its Core Foundation counterparts: + * CFNumber for integer and floating point values, and CFBoolean for Boolean values. + * + * NSCFBoolean is a private class in the NSNumber class cluster. + */ + if ([NSStringFromClass([value class]) isEqualToString:@"__NSCFBoolean"]) { + [property setObject:kMSACPropertyTypeBoolean forKey:kMSACPropertyType]; + [property setObject:value forKey:kMSACPropertyValue]; + } else { + [property setObject:kMSACPropertyTypeNumber forKey:kMSACPropertyType]; + [property setObject:value forKey:kMSACPropertyValue]; + } + } else if ([value isKindOfClass:[NSDate class]]) { + [property setObject:kMSACPropertyTypeDateTime forKey:kMSACPropertyType]; + [property setObject:[MSACUtility dateToISO8601:(NSDate *)value] forKey:kMSACPropertyValue]; + } else if ([value isKindOfClass:[NSString class]]) { + [property setObject:kMSACPropertyTypeString forKey:kMSACPropertyType]; + [property setObject:value forKey:kMSACPropertyValue]; + } else { + return nil; + } + return property; +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if (self) { + self.type = [coder decodeObjectForKey:kMSACCustomProperties]; + self.properties = [coder decodeObjectForKey:kMSACProperties]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.type forKey:kMSACCustomProperties]; + [coder encodeObject:self.properties forKey:kMSACProperties]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACDeviceInternal.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACDeviceInternal.h new file mode 100644 index 0000000000..e5496daeb2 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACDeviceInternal.h @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#ifndef MSAC_DEVICE_INTERNAL_H +#define MSAC_DEVICE_INTERNAL_H + +#import + +#import "MSACAbstractLogInternal.h" +#import "MSACDevice.h" + +static NSString *const kMSACSDKName = @"sdkName"; +static NSString *const kMSACSDKVersion = @"sdkVersion"; +static NSString *const kMSACModel = @"model"; +static NSString *const kMSACOEMName = @"oemName"; +static NSString *const kMSACACOSName = @"osName"; +static NSString *const kMSACOSVersion = @"osVersion"; +static NSString *const kMSACOSBuild = @"osBuild"; +static NSString *const kMSACOSAPILevel = @"osApiLevel"; +static NSString *const kMSACLocale = @"locale"; +static NSString *const kMSACTimeZoneOffset = @"timeZoneOffset"; +static NSString *const kMSACScreenSize = @"screenSize"; +static NSString *const kMSACAppVersion = @"appVersion"; +static NSString *const kMSACCarrierName = @"carrierName"; +static NSString *const kMSACCarrierCountry = @"carrierCountry"; +static NSString *const kMSACAppBuild = @"appBuild"; +static NSString *const kMSACAppNamespace = @"appNamespace"; + +@interface MSACDevice () + +/* + * Name of the SDK. Consists of the name of the SDK and the platform, e.g. "appcenter.ios", "appcenter.android" + */ +@property(nonatomic, copy) NSString *sdkName; + +/* + * Version of the SDK in semver format, e.g. "1.2.0" or "0.12.3-alpha.1". + */ +@property(nonatomic, copy) NSString *sdkVersion; + +/* + * Device model (example: iPad2,3). + */ +@property(nonatomic, copy) NSString *model; + +/* + * Device manufacturer (example: HTC). + */ +@property(nonatomic, copy) NSString *oemName; + +/* + * OS name (example: iOS). + */ +@property(nonatomic, copy) NSString *osName; + +/* + * OS version (example: 9.3.0). + */ +@property(nonatomic, copy) NSString *osVersion; + +/* + * OS build code (example: LMY47X). [optional] + */ +@property(nonatomic, copy) NSString *osBuild; + +/* + * API level when applicable like in Android (example: 15). [optional] + */ +@property(nonatomic, copy) NSNumber *osApiLevel; + +/* + * Language code (example: en_US). + */ +@property(nonatomic, copy) NSString *locale; + +/* + * The offset in minutes from UTC for the device time zone, including daylight savings time. + */ +@property(nonatomic) NSNumber *timeZoneOffset; + +/* + * Screen size of the device in pixels (example: 640x480). + */ +@property(nonatomic, copy) NSString *screenSize; + +/* + * Application version name, e.g. 1.1.0 + */ +@property(nonatomic, copy) NSString *appVersion; + +/* + * Carrier name (for mobile devices). [optional] + */ +@property(nonatomic, copy) NSString *carrierName; + +/* + * Carrier country code (for mobile devices). [optional] + */ +@property(nonatomic, copy) NSString *carrierCountry; + +/* + * The app's build number, e.g. 42. + */ +@property(nonatomic, copy) NSString *appBuild; + +/* + * The bundle identifier, package identifier, or namespace, depending on what the individual plattforms use, .e.g com.microsoft.example. + * [optional] + */ +@property(nonatomic, copy) NSString *appNamespace; + +@end + +#endif diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACLogContainer.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACLogContainer.h new file mode 100644 index 0000000000..78e748adb1 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACLogContainer.h @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACLog.h" + +@interface MSACLogContainer : NSObject + +/** + * Unique batch Id. + */ +@property(nonatomic, copy) NSString *batchId; + +/** + * The list of logs. + */ +@property(nonatomic) NSArray> *logs; + +/** + * Initializer. + * + * @param batchId Unique batch ID. + * @param logs Array of logs. + * + * @return A log container instance for the given batch ID. + */ +- (id)initWithBatchId:(NSString *)batchId andLogs:(NSArray> *)logs; + +/** + * Serialize logs into a JSON string. + * + * @return A JSON string. + */ +- (NSString *)serializeLog; + +/** + * Serialize logs into a JSON string. + * + * @param prettyPrint boolean indicates pretty printing. + * + * @return A serialized string. + */ +- (NSString *)serializeLogWithPrettyPrinting:(BOOL)prettyPrint; + +/** + * Checks if the object's values are valid. + * + * @return YES, if the object is valid. + */ +- (BOOL)isValid; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACLogContainer.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACLogContainer.m new file mode 100644 index 0000000000..bd350f1f17 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACLogContainer.m @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACLogContainer.h" +#import "MSACAppCenterInternal.h" +#import "MSACLogger.h" +#import "MSACSerializableObject.h" + +static NSString *const kMSACLogs = @"logs"; + +@implementation MSACLogContainer + +- (id)initWithBatchId:(NSString *)batchId andLogs:(NSArray> *)logs { + if ((self = [super init])) { + self.batchId = batchId; + self.logs = logs; + } + return self; +} + +- (NSString *)serializeLog { + return [self serializeLogWithPrettyPrinting:NO]; +} + +- (NSString *)serializeLogWithPrettyPrinting:(BOOL)prettyPrint { + NSString *jsonString; + NSMutableArray *jsonArray = [NSMutableArray array]; + [self.logs enumerateObjectsUsingBlock:^(id _Nonnull obj, __attribute__((unused)) NSUInteger idx, + __attribute__((unused)) BOOL *_Nonnull stop) { + NSMutableDictionary *dict = [(id)obj serializeToDictionary]; + if (dict) { + [jsonArray addObject:dict]; + } + }]; + + NSMutableDictionary *logContainer = [[NSMutableDictionary alloc] init]; + [logContainer setValue:jsonArray forKey:kMSACLogs]; + + NSError *error; + NSJSONWritingOptions printOptions = prettyPrint ? NSJSONWritingPrettyPrinted : (NSJSONWritingOptions)0; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:logContainer options:printOptions error:&error]; + + if (!jsonData) { + MSACLogError([MSACAppCenter logTag], @"Couldn't serialize log container to json: %@", error.localizedDescription); + } else { + jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + /* + * NSJSONSerialization escapes paths by default. + * We don't need that extra bytes going over the wire, so we replace them. + */ + jsonString = [jsonString stringByReplacingOccurrencesOfString:@"\\/" withString:@"/"]; + } + return jsonString; +} + +- (BOOL)isValid { + + // Check for empty container + if ([self.logs count] == 0) { + MSACLogVerbose([MSACAppCenter logTag], @"%@: there are no logs in container.", NSStringFromClass([self class])); + return NO; + } + + __block BOOL isValid = YES; + [self.logs enumerateObjectsUsingBlock:^(id _Nonnull obj, __unused NSUInteger idx, BOOL *_Nonnull stop) { + if (![obj isValid]) { + *stop = YES; + isValid = NO; + return; + } + }]; + return isValid; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACLogConversion.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACLogConversion.h new file mode 100644 index 0000000000..b3404e2243 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACLogConversion.h @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACConstants+Flags.h" + +@class MSACCommonSchemaLog; + +@protocol MSACLogConversion + +/** + * Method to transform a log into one or several common schema logs. If the log has multiple transmission target tokens, the conversion will + * produce one log per token. + * + * @param flags The Common Schema flags for the log. + * + * @return An array of MCSCommonSchemaLog objects. + */ +- (NSArray *)toCommonSchemaLogsWithFlags:(MSACFlags)flags; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACLogWithProperties.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACLogWithProperties.m new file mode 100644 index 0000000000..6ba95da0ad --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACLogWithProperties.m @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACLogWithProperties.h" +#import "MSACAbstractLogInternal.h" + +static NSString *const kMSACProperties = @"properties"; + +@implementation MSACLogWithProperties + +@synthesize properties = _properties; + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary]; + + if (self.properties && [self.properties count] != 0) { + dict[kMSACProperties] = self.properties; + } + return dict; +} + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACLogWithProperties class]] || ![super isEqual:object]) { + return NO; + } + MSACLogWithProperties *log = (MSACLogWithProperties *)object; + return ((!self.properties && !log.properties) || [self.properties isEqualToDictionary:log.properties]); +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if (self) { + _properties = [coder decodeObjectForKey:kMSACProperties]; + } + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.properties forKey:kMSACProperties]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACModel.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACModel.h new file mode 100644 index 0000000000..80e9b8c721 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACModel.h @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +@protocol MSACModel + +/** + * Checks if the object's values are valid. + * + * @return YES, if the object is valid. + */ +- (BOOL)isValid; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACNoAutoAssignSessionIdLog.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACNoAutoAssignSessionIdLog.h new file mode 100644 index 0000000000..b025986f7e --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACNoAutoAssignSessionIdLog.h @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +@protocol MSACNoAutoAssignSessionIdLog +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACSerializableObject.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACSerializableObject.h new file mode 100644 index 0000000000..6c96b64fb4 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACSerializableObject.h @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +@protocol MSACSerializableObject + +/** + * Serialize this object to a dictionary. + * + * @return A dictionary representing this object. + */ +- (NSMutableDictionary *)serializeToDictionary; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACStartServiceLog.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACStartServiceLog.h new file mode 100644 index 0000000000..e0175e4ab7 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACStartServiceLog.h @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAbstractLogInternal.h" +#import "MSACNoAutoAssignSessionIdLog.h" + +@interface MSACStartServiceLog : MSACAbstractLog + +/** + * Services which started with SDK + */ +@property(nonatomic) NSArray *services; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACStartServiceLog.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACStartServiceLog.m new file mode 100644 index 0000000000..641fc4b831 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACStartServiceLog.m @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACStartServiceLog.h" + +static NSString *const kMSACStartService = @"startService"; +static NSString *const kMSACServices = @"services"; + +@implementation MSACStartServiceLog + +@synthesize services = _services; + +- (instancetype)init { + if ((self = [super init])) { + self.type = kMSACStartService; + } + return self; +} + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACStartServiceLog class]] || ![super isEqual:object]) { + return NO; + } + MSACStartServiceLog *log = (MSACStartServiceLog *)object; + return ((!self.services && !log.services) || [self.services isEqualToArray:log.services]); +} + +#pragma mark - MSACSerializableObject + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary]; + if (self.services) { + dict[kMSACServices] = self.services; + } + return dict; +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + if ((self = [super initWithCoder:coder])) { + self.services = [coder decodeObjectForKey:kMSACServices]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.services forKey:kMSACServices]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACWrapperSdkInternal.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACWrapperSdkInternal.h new file mode 100644 index 0000000000..fa30ace21e --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/MSACWrapperSdkInternal.h @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#ifndef MSAC_WRAPPER_SDK_INTERNAL_H +#define MSAC_WRAPPER_SDK_INTERNAL_H + +#import + +#import "MSACAbstractLogInternal.h" + +static NSString *const kMSACWrapperSDKVersion = @"wrapperSdkVersion"; +static NSString *const kMSACWrapperSDKName = @"wrapperSdkName"; +static NSString *const kMSACWrapperRuntimeVersion = @"wrapperRuntimeVersion"; +static NSString *const kMSACLiveUpdateReleaseLabel = @"liveUpdateReleaseLabel"; +static NSString *const kMSACLiveUpdateDeploymentKey = @"liveUpdateDeploymentKey"; +static NSString *const kMSACLiveUpdatePackageHash = @"liveUpdatePackageHash"; + +@interface MSACWrapperSdk () + +/* + * Version of the wrapper SDK. When the SDK is embedding another base SDK (for example Xamarin.Android wraps Android), the Xamarin specific + * version is populated into this field while sdkVersion refers to the original Android SDK. [optional] + */ +@property(nonatomic, copy) NSString *wrapperSdkVersion; + +/* + * Name of the wrapper SDK (examples: Xamarin, Cordova). [optional] + */ +@property(nonatomic, copy) NSString *wrapperSdkName; + +/* + * Version of the wrapper technology framework (Xamarin runtime version or ReactNative or Cordova etc...). [optional] + */ +@property(nonatomic, copy) NSString *wrapperRuntimeVersion; + +/* + * Label that is used to identify application code 'version' released via Live Update beacon running on device + */ +@property(nonatomic, copy) NSString *liveUpdateReleaseLabel; + +/* + * Identifier of environment that current application release belongs to, deployment key then maps to environment like Production, Staging. + */ +@property(nonatomic, copy) NSString *liveUpdateDeploymentKey; + +/* + * Hash of all files (ReactNative or Cordova) deployed to device via LiveUpdate beacon. Helps identify the Release version on device or need + * to download updates in future + */ +@property(nonatomic, copy) NSString *liveUpdatePackageHash; + +@end + +#endif diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACBooleanTypedProperty.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACBooleanTypedProperty.h new file mode 100644 index 0000000000..1be0adf6f9 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACBooleanTypedProperty.h @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACTypedProperty.h" + +static NSString *const kMSACBooleanTypedPropertyType = @"boolean"; + +@interface MSACBooleanTypedProperty : MSACTypedProperty + +/** + * Boolean property value. + */ +@property(nonatomic) BOOL value; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACBooleanTypedProperty.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACBooleanTypedProperty.m new file mode 100644 index 0000000000..1b9b34f2fe --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACBooleanTypedProperty.m @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACBooleanTypedProperty.h" + +@implementation MSACBooleanTypedProperty + +- (instancetype)init { + if ((self = [super init])) { + self.type = kMSACBooleanTypedPropertyType; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if (self) { + _value = [coder decodeBoolForKey:kMSACTypedPropertyValue]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeBool:self.value forKey:kMSACTypedPropertyValue]; +} + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary]; + dict[kMSACTypedPropertyValue] = @(self.value); + return dict; +} + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACBooleanTypedProperty class]] || ![super isEqual:object]) { + return NO; + } + MSACBooleanTypedProperty *property = (MSACBooleanTypedProperty *)object; + return (self.value == property.value); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACDateTimeTypedProperty.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACDateTimeTypedProperty.h new file mode 100644 index 0000000000..53ba3c76a3 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACDateTimeTypedProperty.h @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACTypedProperty.h" + +static NSString *const kMSACDateTimeTypedPropertyType = @"dateTime"; + +@interface MSACDateTimeTypedProperty : MSACTypedProperty + +/** + * Date and time property value. + */ +@property(nonatomic) NSDate *value; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACDateTimeTypedProperty.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACDateTimeTypedProperty.m new file mode 100644 index 0000000000..c5d0d61478 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACDateTimeTypedProperty.m @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACDateTimeTypedProperty.h" +#import "MSACSerializableObject.h" +#import "MSACUtility+Date.h" + +@implementation MSACDateTimeTypedProperty + +- (instancetype)init { + if ((self = [super init])) { + self.type = kMSACDateTimeTypedPropertyType; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if (self) { + _value = [coder decodeObjectForKey:kMSACTypedPropertyValue]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.value forKey:kMSACTypedPropertyValue]; +} + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary]; + dict[kMSACTypedPropertyValue] = [MSACUtility dateToISO8601:self.value]; + return dict; +} + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACDateTimeTypedProperty class]] || ![super isEqual:object]) { + return NO; + } + MSACDateTimeTypedProperty *property = (MSACDateTimeTypedProperty *)object; + return ((!self.value && !property.value) || [self.value isEqualToDate:property.value]); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACDoubleTypedProperty.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACDoubleTypedProperty.h new file mode 100644 index 0000000000..e71870ee5c --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACDoubleTypedProperty.h @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACTypedProperty.h" + +static NSString *const kMSACDoubleTypedPropertyType = @"double"; + +@interface MSACDoubleTypedProperty : MSACTypedProperty + +/** + * Double property value. + */ +@property(nonatomic) double value; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACDoubleTypedProperty.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACDoubleTypedProperty.m new file mode 100644 index 0000000000..69b684ad03 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACDoubleTypedProperty.m @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACDoubleTypedProperty.h" + +@implementation MSACDoubleTypedProperty + +- (instancetype)init { + if ((self = [super init])) { + self.type = kMSACDoubleTypedPropertyType; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if (self) { + _value = [coder decodeDoubleForKey:kMSACTypedPropertyValue]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeDouble:self.value forKey:kMSACTypedPropertyValue]; +} + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary]; + dict[kMSACTypedPropertyValue] = @(self.value); + return dict; +} + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACDoubleTypedProperty class]] || ![super isEqual:object]) { + return NO; + } + MSACDoubleTypedProperty *property = (MSACDoubleTypedProperty *)object; + return (self.value == property.value); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACLongTypedProperty.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACLongTypedProperty.h new file mode 100644 index 0000000000..a1d6c4f424 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACLongTypedProperty.h @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACTypedProperty.h" + +static NSString *const kMSACLongTypedPropertyType = @"long"; + +@interface MSACLongTypedProperty : MSACTypedProperty + +/** + * Long property value (64-bit signed integer). + */ +@property(nonatomic) int64_t value; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACLongTypedProperty.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACLongTypedProperty.m new file mode 100644 index 0000000000..0530f5af52 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACLongTypedProperty.m @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACLongTypedProperty.h" + +@implementation MSACLongTypedProperty + +- (instancetype)init { + if ((self = [super init])) { + self.type = kMSACLongTypedPropertyType; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if (self) { + _value = [coder decodeInt64ForKey:kMSACTypedPropertyValue]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeInt64:self.value forKey:kMSACTypedPropertyValue]; +} + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary]; + dict[kMSACTypedPropertyValue] = @(self.value); + return dict; +} + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACLongTypedProperty class]] || ![super isEqual:object]) { + return NO; + } + MSACLongTypedProperty *property = (MSACLongTypedProperty *)object; + return (self.value == property.value); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACStringTypedProperty.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACStringTypedProperty.h new file mode 100644 index 0000000000..d5451fda74 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACStringTypedProperty.h @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACTypedProperty.h" + +static NSString *const kMSACStringTypedPropertyType = @"string"; + +@interface MSACStringTypedProperty : MSACTypedProperty + +/** + * String property value. + */ +@property(nonatomic, copy) NSString *value; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACStringTypedProperty.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACStringTypedProperty.m new file mode 100644 index 0000000000..bce1757e61 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACStringTypedProperty.m @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACStringTypedProperty.h" + +@implementation MSACStringTypedProperty + +- (instancetype)init { + if ((self = [super init])) { + self.type = kMSACStringTypedPropertyType; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if (self) { + _value = [coder decodeObjectForKey:kMSACTypedPropertyValue]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.value forKey:kMSACTypedPropertyValue]; +} + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary]; + dict[kMSACTypedPropertyValue] = self.value; + return dict; +} + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACStringTypedProperty class]] || ![super isEqual:object]) { + return NO; + } + MSACStringTypedProperty *property = (MSACStringTypedProperty *)object; + return ((!self.value && !property.value) || [self.value isEqualToString:property.value]); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACTypedProperty.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACTypedProperty.h new file mode 100644 index 0000000000..fded13aebe --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACTypedProperty.h @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACSerializableObject.h" + +static NSString *const kMSACTypedPropertyValue = @"value"; + +@interface MSACTypedProperty : NSObject + +/** + * Property type. + */ +@property(nonatomic, copy) NSString *type; + +/** + * Property name. + */ +@property(nonatomic, copy) NSString *name; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACTypedProperty.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACTypedProperty.m new file mode 100644 index 0000000000..3a86d7f1d5 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Properties/MSACTypedProperty.m @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACTypedProperty.h" +#import "MSACSerializableObject.h" + +static NSString *const kMSACTypedPropertyType = @"type"; +static NSString *const kMSACTypedPropertyName = @"name"; + +@implementation MSACTypedProperty + +// Subclasses need to decode "value" since the type might be saved as a primitive. +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super init]; + if (self) { + _type = [coder decodeObjectForKey:kMSACTypedPropertyType]; + _name = [coder decodeObjectForKey:kMSACTypedPropertyName]; + } + return self; +} + +// Subclasses need to encode "value" since the type might be saved as a primitive. +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:self.type forKey:kMSACTypedPropertyType]; + [coder encodeObject:self.name forKey:kMSACTypedPropertyName]; +} + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [NSMutableDictionary new]; + dict[kMSACTypedPropertyType] = self.type; + dict[kMSACTypedPropertyName] = self.name; + return dict; +} + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACTypedProperty class]]) { + return NO; + } + MSACTypedProperty *property = (MSACTypedProperty *)object; + return ((!self.type && !property.type) || [self.type isEqualToString:property.type]); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Util/MSACAppCenterUserDefaults.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Util/MSACAppCenterUserDefaults.h new file mode 100644 index 0000000000..44691d632d --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Util/MSACAppCenterUserDefaults.h @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#define MSACPrefixKeyFrom(_key) [[MSACUserDefaultsPrefixKey alloc] initWithPrefix:_key] + +NS_ASSUME_NONNULL_BEGIN + +/** + * Persistent settings, a wrapper around NSUserDefaults capable of updating object or dictionary (including expiration). + */ +@interface MSACAppCenterUserDefaults : NSObject + +/** + * @return the shared settings. + */ ++ (instancetype)shared; + +/** + * Get an object in the settings, returning object if key was found, nil otherwise. + * + * @param key a unique key to identify the value. + */ +- (nullable id)objectForKey:(NSString *)key; + +/** + * Sets the value of the specified key in the settings. + * + * @param value the object to set. + * @param key a unique key to identify the value. + */ +- (void)setObject:(id)value forKey:(NSString *)key; + +/** + * Removes a value from the settings. + * + * @param key the key to remove. + */ +- (void)removeObjectForKey:(NSString *)key; + +/** + * Migrates values for the old keys to new keys. + * @param migratedKeys a dictionary for keys that contains new key as a key of dictionary and old key as a value. + * @param service service name. + */ +- (void)migrateKeys:(NSDictionary *)migratedKeys forService:(NSString *)service; + +@end + +#pragma mark - Prefix key + +/** + * A class defining that the instance of this class needs wildcard migration. + * This means that for instances of this class, MSACAppCenterUserDefautls will + * search for the old keys starting with this key and migrate all of them. + */ +@interface MSACUserDefaultsPrefixKey : NSObject + +@property(nonatomic) NSString *keyPrefix; + +- (instancetype)initWithPrefix:(NSString *)prefix; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Util/MSACAppCenterUserDefaults.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Util/MSACAppCenterUserDefaults.m new file mode 100644 index 0000000000..368dc3c0a2 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Model/Util/MSACAppCenterUserDefaults.m @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAppCenterUserDefaults.h" +#import "MSACAppCenterInternal.h" +#import "MSACAppCenterUserDefaultsPrivate.h" +#import "MSACLogger.h" + +static NSString *const kMSACAppCenterUserDefaultsMigratedKeyFormat = @"310%@UserDefaultsMigratedKey"; + +static MSACAppCenterUserDefaults *sharedInstance = nil; +static dispatch_once_t onceToken; + +@implementation MSACAppCenterUserDefaults + ++ (instancetype)shared { + dispatch_once(&onceToken, ^{ + sharedInstance = [[MSACAppCenterUserDefaults alloc] init]; + }); + return sharedInstance; +} + ++ (void)resetSharedInstance { + + // Reset the once_token so dispatch_once will run again. + onceToken = 0; + sharedInstance = nil; +} + +- (void)migrateKeys:(NSDictionary *)migratedKeys forService:(NSString *)service { + NSNumber *serviceMigrated = [self objectForKey:[NSString stringWithFormat:kMSACAppCenterUserDefaultsMigratedKeyFormat, service]]; + if (serviceMigrated) { + return; + } + MSACLogVerbose([MSACAppCenter logTag], @"Migrating the old NSDefaults keys to new ones."); + for (NSObject *newKey in migratedKeys) { + NSAssert([newKey isKindOfClass:[NSString class]], @"Unsupported type"); + id oldKey = migratedKeys[newKey]; + NSString *newKeyString = (NSString *)newKey; + if ([oldKey isKindOfClass:[NSString class]]) { + id value = [[NSUserDefaults standardUserDefaults] objectForKey:(NSString *)oldKey]; + [self swapKeys:(NSString *)oldKey newKey:newKeyString value:value]; + } else { + NSAssert([oldKey isKindOfClass:[MSACUserDefaultsPrefixKey class]], @"Unsupported type"); + + // List all the keys starting with oldKey. + NSString *oldKeyPrefix = ((MSACUserDefaultsPrefixKey *)oldKey).keyPrefix; + NSArray *userDefaultsDictionary = [[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys]; + for (NSString *userDefaultsKey in userDefaultsDictionary) { + if ([userDefaultsKey isKindOfClass:[NSString class]] && [userDefaultsKey hasPrefix:oldKeyPrefix]) { + NSString *suffix = [userDefaultsKey substringFromIndex:[oldKeyPrefix length]]; + NSString *newKeyWithSuffix = [newKeyString stringByAppendingString:suffix]; + id value = [[NSUserDefaults standardUserDefaults] objectForKey:userDefaultsKey]; + [self swapKeys:userDefaultsKey newKey:newKeyWithSuffix value:value]; + } + } + } + } + [self setObject:@YES forKey:[NSString stringWithFormat:kMSACAppCenterUserDefaultsMigratedKeyFormat, service]]; +} + +- (void)swapKeys:(NSString *)oldKey newKey:(NSString *)newKey value:(id)value { + if (value == nil) { + return; + } + [[NSUserDefaults standardUserDefaults] setObject:value forKey:newKey]; + [[NSUserDefaults standardUserDefaults] removeObjectForKey:oldKey]; + MSACLogVerbose([MSACAppCenter logTag], @"Migrating key %@ -> %@", oldKey, newKey); +} + +- (NSString *)getAppCenterKeyFrom:(NSString *)key { + NSAssert(![key hasPrefix:kMSACUserDefaultsPrefix], @"Please do not prepend the key with 'MSAppCenter'. It's done automatically."); + return [kMSACUserDefaultsPrefix stringByAppendingString:key]; +} + +- (id)objectForKey:(NSString *)key { + NSString *keyPrefixed = [self getAppCenterKeyFrom:key]; + return [[NSUserDefaults standardUserDefaults] objectForKey:keyPrefixed]; +} + +- (void)setObject:(id)value forKey:(NSString *)key { + NSString *keyPrefixed = [self getAppCenterKeyFrom:key]; + [[NSUserDefaults standardUserDefaults] setObject:value forKey:keyPrefixed]; +} + +- (void)removeObjectForKey:(NSString *)key { + NSString *keyPrefixed = [self getAppCenterKeyFrom:key]; + [[NSUserDefaults standardUserDefaults] removeObjectForKey:keyPrefixed]; +} + +@end + +#pragma mark - Prefix key + +@implementation MSACUserDefaultsPrefixKey + +- (instancetype)initWithPrefix:(NSString *)prefix { + if ((self = [super init])) { + _keyPrefix = prefix; + } + return self; +} + +- (id)copyWithZone:(nullable __unused NSZone *)zone { + return [[MSACUserDefaultsPrefixKey alloc] initWithPrefix:self.keyPrefix]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACAppCenterUserDefaultsPrivate.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACAppCenterUserDefaultsPrivate.h new file mode 100644 index 0000000000..d6c97b9daa --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACAppCenterUserDefaultsPrivate.h @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const kMSACUserDefaultsPrefix = @"MSAppCenter"; + +@interface MSACAppCenterUserDefaults () + +/** + * Resets the shared instance of the class. + */ ++ (void)resetSharedInstance; + +NS_ASSUME_NONNULL_END + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACDBStorage.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACDBStorage.h new file mode 100644 index 0000000000..5d2b89a590 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACDBStorage.h @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACStorageBindableArray.h" + +NS_ASSUME_NONNULL_BEGIN + +/* + * FIXME: We need ordered columns so we can't just use an `NSDictionary` to store them. A workaround is to use an array of dictionaries + * instead, still works fine as literals. But, we should use an array of tuples when we'll switch to Swift. + * + * Database schema example: + * + * @{ + * table_name : @[ + * @{ column_name: @[ column_type, column_constraints, ... ]}, + * ... + * ], + * ... + * }; + */ +typedef NSArray *> *> MSACDBColumnsSchema; +typedef NSDictionary MSACDBSchema; + +// SQLite types +static NSString *const kMSACSQLiteTypeText = @"TEXT"; +static NSString *const kMSACSQLiteTypeInteger = @"INTEGER"; + +// SQLite column constraints. +static NSString *const kMSACSQLiteConstraintNotNull = @"NOT NULL"; +static NSString *const kMSACSQLiteConstraintPrimaryKey = @"PRIMARY KEY"; +static NSString *const kMSACSQLiteConstraintAutoincrement = @"AUTOINCREMENT"; + +@interface MSACDBStorage : NSObject + +/** + * Initialize this database with a schema and a filename for its creation. + * + * @param schema Schema describing the database. + * @param version Version of the database. + * @param filename Database filename in the file system. + * + * @return An instance of a database. + */ +- (instancetype)initWithSchema:(nullable MSACDBSchema *)schema version:(NSUInteger)version filename:(NSString *)filename; + +/** + * Initialize this database with a version and a filename for its creation. + * + * @param version Version of the database. + * @param filename Database filename in the file system. + * + * @return An instance of a database. + */ +- (instancetype)initWithVersion:(NSUInteger)version filename:(NSString *)filename; + +/** + * Count entries on a given table using the given SQLite "WHERE" clause's condition. + * + * @param tableName Name of the table to inspect. + * @param condition The SQLite "WHERE" clause's condition. + * + * @return The count of entries for this query. + */ +- (NSUInteger)countEntriesForTable:(NSString *)tableName + condition:(nullable NSString *)condition + withValues:(nullable MSACStorageBindableArray *)values; + +/** + * Execute a non selection SQLite query on the database (i.e.: "CREATE", "INSERT", "UPDATE"... but not "SELECT"). + * + * @param query An SQLite query to execute. + * + * @return The SQLite return code. + */ +- (int)executeNonSelectionQuery:(NSString *)query; + +/** + * Execute a non selection SQLite query on the database (i.e.: "CREATE", "INSERT", "UPDATE"... but not "SELECT"). + * + * @param query A SQLite query to execute. + * @param values An array of query parameters to be substituted using `sqlite3_bind`. + * + * @return A result code for the query execution. + */ +- (int)executeNonSelectionQuery:(NSString *)query withValues:(nullable MSACStorageBindableArray *)values; + +/** + * Execute a "SELECT" SQLite query on the database. + * + * @param query A SQLite "SELECT" query to execute. + * @param values An array of query parameters to be substituted using `sqlite3_bind`. + * + * @return The selected entries. + */ +- (NSArray *)executeSelectionQuery:(NSString *)query withValues:(nullable MSACStorageBindableArray *)values; + +/** + * Get columns indexes from schema. + * + * @param schema Schema describing the database. + * + * @return Database tables columns indexes. + */ ++ (NSDictionary *)columnsIndexes:(MSACDBSchema *)schema; + +/** + * Deletes database. + * + */ +- (void)dropDatabase; + +/** + * Deletes table within an existing database. + * + * @param tableName Name of the table to delete. + * + * @return operation status. + */ +- (BOOL)dropTable:(NSString *)tableName; + +/** + * Set the maximum size of the internal storage. This method must be called before App Center is started. + * + * @param sizeInBytes Maximum size of the internal storage in bytes. This will be rounded up to the nearest multiple of a SQLite page size + * (default is 4096 bytes). Values below 20,480 bytes (20 KiB) will be ignored. + * @param completionHandler Callback that is invoked when the database size has been set. The `BOOL` parameter is `YES` if changing the size + * is successful, and `NO` otherwise. + * + * @discussion This only sets the maximum size of the database, but App Center modules might store additional data. + * The value passed to this method is not persisted on disk. The default maximum database size is 10485760 bytes (10 MiB). + */ +- (void)setMaxStorageSize:(long)sizeInBytes completionHandler:(nullable void (^)(BOOL))completionHandler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACDBStorage.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACDBStorage.m new file mode 100644 index 0000000000..9fb8745812 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACDBStorage.m @@ -0,0 +1,519 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAppCenterInternal.h" +#import "MSACDBStoragePrivate.h" +#import "MSACStorageBindableArray.h" +#import "MSACUtility+File.h" + +static dispatch_once_t sqliteConfigurationResultOnceToken; +static int sqliteConfigurationResult = SQLITE_ERROR; + +@implementation MSACDBStorage + ++ (void)load { + + /* + * Configure SQLite at load time to invoke configuration only once and before opening a DB. + * If it is custom SQLite library we need to turn on URI filename capability. + */ + sqliteConfigurationResult = [self configureSQLite]; +} + +- (instancetype)initWithSchema:(MSACDBSchema *)schema version:(NSUInteger)version filename:(NSString *)filename { + _schema = schema; + + // Log SQLite configuration result only once at init time because log level won't be set at load time. + dispatch_once(&sqliteConfigurationResultOnceToken, ^{ + if (sqliteConfigurationResult == SQLITE_OK) { + MSACLogDebug([MSACAppCenter logTag], @"SQLite global configuration successfully updated."); + } else { + NSString *errorString; + if (@available(macOS 10.10, *)) { + errorString = [NSString stringWithUTF8String:sqlite3_errstr(sqliteConfigurationResult)]; + } else { + errorString = @(sqliteConfigurationResult).stringValue; + } + MSACLogError([MSACAppCenter logTag], @"Failed to update SQLite global configuration. Error: %@.", errorString); + } + }); + if ((self = [super init])) { + int result = [self configureDatabaseWithSchema:schema version:version filename:filename]; + if (result == SQLITE_CORRUPT || result == SQLITE_NOTADB) { + [self dropDatabase]; + result = [self configureDatabaseWithSchema:schema version:version filename:filename]; + } + if (result != SQLITE_OK) { + MSACLogError([MSACAppCenter logTag], @"Failed to initialize database."); + } + } + return self; +} + +- (instancetype)initWithVersion:(NSUInteger)version filename:(NSString *)filename { + return [self initWithSchema:nil version:version filename:filename]; +} + +- (int)configureDatabaseWithSchema:(MSACDBSchema *)schema version:(NSUInteger)version filename:(NSString *)filename { + BOOL newDatabase = ![MSACUtility fileExistsForPathComponent:filename]; + self.dbFileURL = [MSACUtility createFileAtPathComponent:filename withData:nil atomically:NO forceOverwrite:NO]; + self.maxSizeInBytes = kMSACDefaultDatabaseSizeInBytes; + int result; + sqlite3 *db = [MSACDBStorage openDatabaseAtFileURL:self.dbFileURL withResult:&result]; + if (result != SQLITE_OK) { + return result; + } + self.pageSize = [MSACDBStorage getPageSizeInOpenedDatabase:db]; + NSUInteger databaseVersion = [MSACDBStorage versionInOpenedDatabase:db result:&result]; + if (result != SQLITE_OK) { + sqlite3_close(db); + return result; + } + + // Create table. + if (schema) { + result = [MSACDBStorage createTablesWithSchema:schema inOpenedDatabase:db]; + if (result != SQLITE_OK) { + MSACLogError([MSACAppCenter logTag], @"Failed to create tables with schema with error \"%d\".", result); + sqlite3_close(db); + return result; + } + } + if (newDatabase) { + MSACLogInfo([MSACAppCenter logTag], @"Created \"%@\" database with %lu version.", filename, (unsigned long)version); + [self customizeDatabase:db]; + } else if (databaseVersion < version) { + MSACLogInfo([MSACAppCenter logTag], @"Migrating \"%@\" database from version %lu to %lu.", filename, (unsigned long)databaseVersion, + (unsigned long)version); + [self migrateDatabase:db fromVersion:databaseVersion]; + } + [MSACDBStorage enableAutoVacuumInOpenedDatabase:db]; + [MSACDBStorage setVersion:version inOpenedDatabase:db]; + sqlite3_close(db); + return result; +} + +- (int)executeQueryUsingBlock:(MSACDBStorageQueryBlock)callback { + int result; + sqlite3 *db = [MSACDBStorage openDatabaseAtFileURL:self.dbFileURL withResult:&result]; + if (!db) { + return result; + } + + // The value is stored as part of the database connection and must be reset every time the database is opened. + long maxPageCount = self.maxSizeInBytes / self.pageSize; + result = [MSACDBStorage setMaxPageCount:maxPageCount inOpenedDatabase:db]; + + // Do not proceed with the query if the database is corrupted. + if (result == SQLITE_CORRUPT || result == SQLITE_NOTADB) { + sqlite3_close(db); + return result; + } + + // Log a warning if max page count can't be set. + if (result != SQLITE_OK) { + MSACLogError([MSACAppCenter logTag], @"Failed to open database with specified maximum size constraint."); + } + result = callback(db); + sqlite3_close(db); + return result; +} + +- (void)dropDatabase { + BOOL result = [MSACUtility deleteFileAtURL:self.dbFileURL]; + if (result) { + MSACLogVerbose([MSACAppCenter logTag], @"Database %@ has been deleted.", (NSString * _Nonnull) self.dbFileURL.absoluteString); + } else { + MSACLogError([MSACAppCenter logTag], @"Failed to delete database."); + } +} + +- (BOOL)dropTable:(NSString *)tableName { + return [self executeQueryUsingBlock:^int(void *db) { + if ([MSACDBStorage tableExists:tableName inOpenedDatabase:db]) { + NSString *deleteQuery = [NSString stringWithFormat:@"DROP TABLE \"%@\";", tableName]; + int result = [MSACDBStorage executeNonSelectionQuery:deleteQuery inOpenedDatabase:db]; + if (result == SQLITE_OK) { + MSACLogVerbose([MSACAppCenter logTag], @"Table %@ has been deleted", tableName); + } else { + MSACLogError([MSACAppCenter logTag], @"Failed to delete table %@", tableName); + } + return result; + } + return SQLITE_OK; + }] == SQLITE_OK; +} + +- (BOOL)createTable:(NSString *)tableName columnsSchema:(MSACDBColumnsSchema *)columnsSchema { + return [self executeQueryUsingBlock:^int(void *db) { + if (![MSACDBStorage tableExists:tableName inOpenedDatabase:db]) { + NSString *createQuery = [NSString + stringWithFormat:@"CREATE TABLE \"%@\" (%@);", tableName, [MSACDBStorage columnsQueryFromColumnsSchema:columnsSchema]]; + int result = [MSACDBStorage executeNonSelectionQuery:createQuery inOpenedDatabase:db]; + if (result == SQLITE_OK) { + MSACLogVerbose([MSACAppCenter logTag], @"Table %@ has been created", tableName); + } else { + MSACLogError([MSACAppCenter logTag], @"Failed to create table %@", tableName); + } + return result; + } + return SQLITE_OK; + }] == SQLITE_OK; +} + ++ (NSString *)columnsQueryFromColumnsSchema:(MSACDBColumnsSchema *)columnsSchema { + NSMutableArray *columnQueries = [NSMutableArray new]; + + // Browse columns. + for (NSUInteger i = 0; i < columnsSchema.count; i++) { + NSString *columnName = columnsSchema[i].allKeys[0]; + + // Compute column query. + [columnQueries + addObject:[NSString stringWithFormat:@"\"%@\" %@", columnName, [columnsSchema[i][columnName] componentsJoinedByString:@" "]]]; + } + return [columnQueries componentsJoinedByString:@", "]; +} + ++ (int)createTablesWithSchema:(MSACDBSchema *)schema inOpenedDatabase:(void *)db { + int result = SQLITE_OK; + NSMutableArray *tableQueries = [NSMutableArray new]; + + // Browse tables. + for (NSString *tableName in schema) { + + // Optimization, don't even compute the query if the table already exists. + if ([self tableExists:tableName inOpenedDatabase:db result:&result]) { + if (result != SQLITE_OK) { + return result; + } + continue; + } + + // Compute table query. + [tableQueries addObject:[NSString stringWithFormat:@"CREATE TABLE \"%@\" (%@);", tableName, + [MSACDBStorage columnsQueryFromColumnsSchema:schema[tableName]]]]; + } + + // Create the tables. + if (tableQueries.count > 0) { + + /* + * We do not join queries with ';' because we do not execute a non-selection query using `exec`. + * We are using `step`, and `step` can only handle one-line statements. + */ + for (NSString *tableQuery in tableQueries) { + result = [self executeNonSelectionQuery:tableQuery inOpenedDatabase:db]; + if (result != SQLITE_OK) { + return result; + } + } + } + return result; +} + ++ (NSDictionary *)columnsIndexes:(MSACDBSchema *)schema { + NSMutableDictionary *dbColumnsIndexes = [NSMutableDictionary new]; + for (NSString *tableName in schema) { + NSMutableDictionary *tableColumnsIndexes = [NSMutableDictionary new]; + NSArray *columns = schema[tableName]; + for (NSUInteger i = 0; i < columns.count; i++) { + NSString *columnName = columns[i].allKeys[0]; + tableColumnsIndexes[columnName] = @(i); + } + dbColumnsIndexes[tableName] = tableColumnsIndexes; + } + return dbColumnsIndexes; +} + ++ (BOOL)tableExists:(NSString *)tableName inOpenedDatabase:(void *)db { + return [MSACDBStorage tableExists:tableName inOpenedDatabase:db result:nil]; +} + ++ (BOOL)tableExists:(NSString *)tableName inOpenedDatabase:(void *)db result:(int *)result { + MSACStorageBindableArray *values = [MSACStorageBindableArray new]; + [values addString:tableName]; + NSString *query = [NSString stringWithFormat:@"SELECT COUNT(*) FROM \"sqlite_master\" WHERE \"type\"='table' AND \"name\"=?;"]; + NSArray *entries = [MSACDBStorage executeSelectionQuery:query inOpenedDatabase:db result:result withValues:values]; + return entries.count > 0 && entries[0].count > 0 ? [(NSNumber *)entries[0][0] boolValue] : NO; +} + ++ (NSUInteger)versionInOpenedDatabase:(void *)db result:(int *)result { + NSArray *entries = [MSACDBStorage executeSelectionQuery:@"PRAGMA user_version" + inOpenedDatabase:db + result:result + withValues:nil]; + return entries.count > 0 && entries[0].count > 0 ? [(NSNumber *)entries[0][0] unsignedIntegerValue] : 0; +} + ++ (void)setVersion:(NSUInteger)version inOpenedDatabase:(void *)db { + NSString *query = [NSString stringWithFormat:@"PRAGMA user_version = %lu", (unsigned long)version]; + + // We use a selection query here because pragma set returns a value. + [MSACDBStorage executeSelectionQuery:query inOpenedDatabase:db withValues:nil]; +} + ++ (void)enableAutoVacuumInOpenedDatabase:(void *)db { + NSArray *result = [MSACDBStorage executeSelectionQuery:@"PRAGMA auto_vacuum" inOpenedDatabase:db withValues:nil]; + int vacuumMode = 0; + if (result.count > 0 && result[0].count > 0) { + vacuumMode = [(NSNumber *)result[0][0] intValue]; + } + BOOL autoVacuumDisabled = vacuumMode != 1; + + /* + * If `auto_vacuum` is disabled, change it to `FULL` and then manually `VACUUM` the database. Per the SQLite docs, changing the state of + * `auto_vacuum` must be followed by a manual `VACUUM` before the change can take effect (for more information, + * see https://www.sqlite.org/pragma.html#pragma_auto_vacuum). + */ + if (autoVacuumDisabled) { + MSACLogDebug([MSACAppCenter logTag], @"Vacuuming database and enabling auto_vacuum"); + + // We use a selection query here because pragma set returns a value. + [MSACDBStorage executeSelectionQuery:@"PRAGMA auto_vacuum = FULL;" inOpenedDatabase:db withValues:nil]; + [MSACDBStorage executeSelectionQuery:@"VACUUM;" inOpenedDatabase:db withValues:nil]; + } +} + +- (NSUInteger)countEntriesForTable:(NSString *)tableName + condition:(nullable NSString *)condition + withValues:(nullable MSACStorageBindableArray *)values { + NSMutableString *countLogQuery = [NSMutableString stringWithFormat:@"SELECT COUNT(*) FROM \"%@\" ", tableName]; + if (condition.length > 0) { + [countLogQuery appendFormat:@"WHERE %@", condition]; + } + NSArray *> *result = [self executeSelectionQuery:countLogQuery withValues:values]; + return (result.count > 0) ? result[0][0].unsignedIntegerValue : 0; +} + ++ (int)executeNonSelectionQuery:(NSString *)query inOpenedDatabase:(void *)db { + return [self executeNonSelectionQuery:query inOpenedDatabase:db withValues:nil]; +} + +- (int)executeNonSelectionQuery:(NSString *)query { + return [self executeNonSelectionQuery:query withValues:nil]; +} + +- (int)executeNonSelectionQuery:(NSString *)query withValues:(nullable MSACStorageBindableArray *)values { + return [self executeQueryUsingBlock:^int(void *db) { + return [MSACDBStorage executeNonSelectionQuery:query inOpenedDatabase:db withValues:values]; + }]; +} + ++ (int)executeNonSelectionQuery:(NSString *)query inOpenedDatabase:(void *)db withValues:(nullable MSACStorageBindableArray *)values { + return [MSACDBStorage + executeQuery:query + inOpenedDatabase:db + withValues:values + usingBlock:^(void *statement) { + int stepResult = sqlite3_step(statement); + if (stepResult == SQLITE_DONE) { + return SQLITE_OK; + } + NSString *errorMessage = [NSString stringWithUTF8String:sqlite3_errmsg(db)]; + if (stepResult == SQLITE_CORRUPT || stepResult == SQLITE_NOTADB) { + MSACLogError([MSACAppCenter logTag], @"A database file is corrupted, result=%d\n\t%@", stepResult, errorMessage); + } else if (stepResult == SQLITE_FULL) { + MSACLogDebug([MSACAppCenter logTag], @"Query failed with error: %d\n\t%@", stepResult, errorMessage); + } else { + MSACLogError([MSACAppCenter logTag], @"Could not execute the statement, result=%d\n\t%@", stepResult, errorMessage); + } + return stepResult; + }]; +} + ++ (int)executeQuery:(NSString *)query + inOpenedDatabase:(void *)db + withValues:(nullable MSACStorageBindableArray *)values + usingBlock:(MSACDBStorageQueryBlock)block { + sqlite3_stmt *statement = NULL; + int result = sqlite3_prepare_v2(db, [query UTF8String], -1, &statement, NULL); + if (result != SQLITE_OK) { + NSString *errorMessage = [NSString stringWithUTF8String:sqlite3_errmsg(db)]; + MSACLogError([MSACAppCenter logTag], @"Failed to prepare SQLite statement, result=%d\n\t%@", result, errorMessage); + return result; + } + result = [values bindAllValuesWithStatement:statement inOpenedDatabase:db]; + if (result == SQLITE_OK) { + result = block(statement); + } + int finalizeResult = sqlite3_finalize(statement); + if (finalizeResult != SQLITE_OK) { + NSString *errorMessage = [NSString stringWithUTF8String:sqlite3_errmsg(db)]; + MSACLogError([MSACAppCenter logTag], @"Failed to finalize SQLite statement, result=%d\n\t%@", finalizeResult, errorMessage); + } + return result; +} + +- (NSArray *)executeSelectionQuery:(NSString *)query withValues:(nullable MSACStorageBindableArray *)values { + __block NSArray *entries = nil; + [self executeQueryUsingBlock:^int(void *db) { + entries = [MSACDBStorage executeSelectionQuery:query inOpenedDatabase:db withValues:values]; + return SQLITE_OK; + }]; + return entries ?: [NSArray new]; +} + ++ (NSArray *)executeSelectionQuery:(NSString *)query + inOpenedDatabase:(void *)db + withValues:(nullable MSACStorageBindableArray *)values { + return [self executeSelectionQuery:query inOpenedDatabase:db result:nil withValues:values]; +} + ++ (NSArray *)executeSelectionQuery:(NSString *)query + inOpenedDatabase:(void *)db + result:(int *)result + withValues:(nullable MSACStorageBindableArray *)values { + NSMutableArray *entries = [NSMutableArray new]; + int queryResult = + [MSACDBStorage executeQuery:query + inOpenedDatabase:db + withValues:values + usingBlock:^(void *statement) { + int stepResult; + + // Loop on rows. + while ((stepResult = sqlite3_step(statement)) == SQLITE_ROW) { + NSMutableArray *entry = [NSMutableArray new]; + + // Loop on columns. + for (int i = 0; i < sqlite3_column_count(statement); i++) { + NSObject *value = [MSACDBStorage columnValueFromStatement:statement atIndex:i]; + [entry addObject:value]; + } + if (entry.count > 0) { + [entries addObject:entry]; + } + } + if (stepResult != SQLITE_DONE) { + NSString *errorMessage = [NSString stringWithUTF8String:sqlite3_errmsg(db)]; + MSACLogError([MSACAppCenter logTag], @"Query failed with error: %d\n\t%@", stepResult, errorMessage); + return stepResult; + } + return SQLITE_OK; + }]; + if (result) { + *result = queryResult; + } + return entries; +} + ++ (NSObject *)columnValueFromStatement:(sqlite3_stmt *)statement atIndex:(int)index { + + /* + * Convert values. + */ + int columnType = sqlite3_column_type(statement, index); + switch (columnType) { + case SQLITE_INTEGER: + return @(sqlite3_column_int(statement, index)); + case SQLITE_TEXT: + return [NSString stringWithUTF8String:(const char *)sqlite3_column_text(statement, index)]; + case SQLITE_NULL: + return [NSNull null]; + default: + MSACLogError([MSACAppCenter logTag], @"Could not retrieve column value at index %d from statement: unknown type %d.", index, + columnType); + return [NSNull null]; + } +} + +- (void)customizeDatabase:(void *)__unused db { +} + +- (void)migrateDatabase:(void *)__unused db fromVersion:(NSUInteger)__unused version { +} + +- (void)setMaxStorageSize:(long)sizeInBytes completionHandler:(nullable void (^)(BOOL))completionHandler { + int result; + BOOL success; + sqlite3 *db = [MSACDBStorage openDatabaseAtFileURL:self.dbFileURL withResult:&result]; + if (!db) { + return; + } + + // Check the current number of pages in the database to determine whether the requested size will shrink the database. + long currentPageCount = [MSACDBStorage getPageCountInOpenedDatabase:db]; + MSACLogDebug([MSACAppCenter logTag], @"Found %ld pages in the database.", currentPageCount); + long requestedMaxPageCount = sizeInBytes % self.pageSize ? sizeInBytes / self.pageSize + 1 : sizeInBytes / self.pageSize; + if (currentPageCount > requestedMaxPageCount) { + MSACLogWarning([MSACAppCenter logTag], + @"Cannot change database size to %ld bytes as it would cause a loss of data. " + "Maximum database size will not be changed.", + sizeInBytes); + success = NO; + } else { + + // Attempt to set the limit and check the page count to make sure the given limit works. + result = [MSACDBStorage setMaxPageCount:requestedMaxPageCount inOpenedDatabase:db]; + if (result != SQLITE_OK) { + MSACLogError([MSACAppCenter logTag], @"Could not change maximum database size to %ld bytes. SQLite error code: %i", sizeInBytes, + result); + success = NO; + } else { + long currentMaxPageCount = [MSACDBStorage getMaxPageCountInOpenedDatabase:db]; + long actualMaxSize = currentMaxPageCount * self.pageSize; + if (requestedMaxPageCount != currentMaxPageCount) { + MSACLogError([MSACAppCenter logTag], @"Could not change maximum database size to %ld bytes, current maximum size is %ld bytes.", + sizeInBytes, actualMaxSize); + success = NO; + } else { + if (sizeInBytes == actualMaxSize) { + MSACLogInfo([MSACAppCenter logTag], @"Changed maximum database size to %ld bytes.", actualMaxSize); + } else { + MSACLogInfo([MSACAppCenter logTag], @"Changed maximum database size to %ld bytes (next multiple of 4KiB).", actualMaxSize); + } + self.maxSizeInBytes = actualMaxSize; + success = YES; + } + } + } + sqlite3_close(db); + if (completionHandler) { + completionHandler(success); + } +} + ++ (sqlite3 *)openDatabaseAtFileURL:(NSURL *)fileURL withResult:(int *)result { + sqlite3 *db = NULL; + *result = sqlite3_open_v2([[fileURL absoluteString] UTF8String], &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_URI, NULL); + if (*result != SQLITE_OK) { + MSACLogError([MSACAppCenter logTag], @"Failed to open database with result: %d.", *result); + return NULL; + } + return db; +} + ++ (long)getPageSizeInOpenedDatabase:(void *)db { + return [MSACDBStorage querySingleValue:@"PRAGMA page_size;" inOpenedDatabase:db]; +} + ++ (long)getPageCountInOpenedDatabase:(void *)db { + return [MSACDBStorage querySingleValue:@"PRAGMA page_count;" inOpenedDatabase:db]; +} + ++ (long)getMaxPageCountInOpenedDatabase:(void *)db { + return [MSACDBStorage querySingleValue:@"PRAGMA max_page_count;" inOpenedDatabase:db]; +} + ++ (long)querySingleValue:(NSString *)query inOpenedDatabase:(void *)db { + NSArray *rows = [MSACDBStorage executeSelectionQuery:query inOpenedDatabase:db withValues:nil]; + return rows.count > 0 && rows[0].count > 0 ? [(NSNumber *)rows[0][0] longValue] : 0; +} + ++ (int)setMaxPageCount:(long)maxPageCount inOpenedDatabase:(void *)db { + int result; + NSString *statement = [NSString stringWithFormat:@"PRAGMA max_page_count = %ld", maxPageCount]; + + // We use a selection query here because pragma set returns a value. + [MSACDBStorage executeSelectionQuery:statement inOpenedDatabase:db result:&result withValues:nil]; + return result; +} + ++ (int)configureSQLite { + return sqlite3_config(SQLITE_CONFIG_URI, 1); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACDBStoragePrivate.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACDBStoragePrivate.h new file mode 100644 index 0000000000..557bd06848 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACDBStoragePrivate.h @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACDBStorage.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef int (^MSACDBStorageQueryBlock)(void *); + +// 10 MiB. +static const long kMSACDefaultDatabaseSizeInBytes = 10 * 1024 * 1024; + +@interface MSACDBStorage () + +/** + * Database file name. + */ +@property(nonatomic, nullable) NSURL *dbFileURL; + +/** + * Maximum size of the database. + */ +@property(nonatomic) long maxSizeInBytes; + +/** + * Page size for database. + */ +@property(nonatomic) long pageSize; + +/** + * Schema for the table. + */ +@property(nonatomic, readonly, nullable) MSACDBSchema *schema; + +/** + * Called after the database is created. Override to customize the database. + * + * @param db Database handle. + */ +- (void)customizeDatabase:(void *)db; + +/** + * Called when migration is needed. Override to customize. + * + * @param db Database handle. + * @param version Current database version. + */ +- (void)migrateDatabase:(void *)db fromVersion:(NSUInteger)version; + +/** + * Open database to prepare actions in callback. + * + * @param block Actions to perform in query. + */ +- (int)executeQueryUsingBlock:(MSACDBStorageQueryBlock)block; + +/** + * Creates a table within an existing database. + * + * @param tableName Table name. + * @param columnsSchema Schema describing the columns structure. + * + * @return YES if table is created or already exists, NO otherwise. + */ +- (BOOL)createTable:(NSString *)tableName columnsSchema:(MSACDBColumnsSchema *)columnsSchema; + +/** + * Create table with schema. + * + * @param schema Database schema. + * @param db Database handle. + * + * @return result `SQLITE_OK` or an error code. + */ ++ (int)createTablesWithSchema:(nullable MSACDBSchema *)schema inOpenedDatabase:(void *)db; + +/** + * Query the number of pages (i.e.: SQLite "page_count") of the database. + * + * @param db Database handle. + * + * @return The number of pages. + */ ++ (long)getPageCountInOpenedDatabase:(void *)db; + +/** + * Query the size of pages (i.e.: SQLite "page_size") of the database. + * + * @param db Database handle. + * + * @return The size of pages. + */ ++ (long)getPageSizeInOpenedDatabase:(void *)db; + +/** + * Set the auto vacuum (i.e.: SQLite "auto_vacuum") of the database. + * + * @param db Database handle. + */ ++ (void)enableAutoVacuumInOpenedDatabase:(void *)db; + +/** + * Check if a table exists in this database. + * + * @param tableName Table name. + * @param db Database handle. + * + * @return `YES` if the table exists in the database, otherwise `NO`. + */ ++ (BOOL)tableExists:(NSString *)tableName inOpenedDatabase:(void *)db; + +/** + * Get current database version. + * + * @param db Database handle. + * @param result `SQLITE_OK` or an error code. + */ ++ (NSUInteger)versionInOpenedDatabase:(void *)db result:(int *)result; + +/** + * Set current database version. + * + * @param db Database handle. + */ ++ (void)setVersion:(NSUInteger)version inOpenedDatabase:(void *)db; + +/** + * Execute a non selection SQLite query on the database (i.e.: "CREATE", + * "INSERT", "UPDATE"... but not "SELECT"). + * + * @param query An SQLite query to execute. + * @param db Database handle. + * + * @return `YES` if the query executed successfully, otherwise `NO`. + */ ++ (int)executeNonSelectionQuery:(NSString *)query inOpenedDatabase:(void *)db; + +/** + * Execute a non selection SQLite query on the database (i.e.: "CREATE", "INSERT", "UPDATE"... but not "SELECT"). + * + * @param query A SQLite statement to execute. + * @param db Database handle. + * @param values An array of query parameters to be substituted using `sqlite3_bind`. + * + * @return `YES` if the query executed successfully, otherwise `NO`. + */ ++ (int)executeNonSelectionQuery:(NSString *)query inOpenedDatabase:(void *)db withValues:(nullable MSACStorageBindableArray *)values; + +/** + * Execute a "SELECT" SQLite query on the database. + * + * @param query A SQLite "SELECT" query to execute. + * @param db Database handle. + * @param values An array of query parameters to be substituted using `sqlite3_bind`. + * + * @return The selected entries. + */ ++ (NSArray *)executeSelectionQuery:(NSString *)query + inOpenedDatabase:(void *)db + withValues:(nullable MSACStorageBindableArray *)values; + +/** + * Execute a "SELECT" SQLite query on the database. + * + * @param query An SQLite "SELECT" query to execute. + * @param db Database handle. + * @param result A reference of result code. + * + * @return The selected entries. + */ ++ (NSArray *)executeSelectionQuery:(NSString *)query + inOpenedDatabase:(void *)db + result:(nullable int *)result + withValues:(nullable MSACStorageBindableArray *)values; + +/** + * Query the maximum number of pages (i.e.: SQLite "max_page_count") of the database. + * + * @param db Database handle. + * + * @return The maximum number of pages. + */ ++ (long)getMaxPageCountInOpenedDatabase:(void *)db; + +/** + * Set global SQLite configuration. + * + * @return `SQLITE_OK` if SQLite configured successfully, otherwise an error code. + * + * @discussion SQLite global configuration must be set before any database is opened. + */ ++ (int)configureSQLite; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACLogDBStorage.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACLogDBStorage.h new file mode 100644 index 0000000000..07872faa98 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACLogDBStorage.h @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACDBStorage.h" +#import "MSACStorage.h" + +@interface MSACLogDBStorage : MSACDBStorage + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACLogDBStorage.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACLogDBStorage.m new file mode 100644 index 0000000000..87c7d0bd36 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACLogDBStorage.m @@ -0,0 +1,399 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAppCenterInternal.h" +#import "MSACConstants+Internal.h" +#import "MSACDBStoragePrivate.h" +#import "MSACLogDBStoragePrivate.h" +#import "MSACLogDBStorageVersion.h" +#import "MSACStorageNumberType.h" +#import "MSACStorageTextType.h" +#import "MSACUtility+StringFormatting.h" + +static const NSUInteger kMSACSchemaVersion = 5; + +@implementation MSACLogDBStorage + +#pragma mark - Initialization + +- (instancetype)init { + + /* + * DO NOT modify schema without a migration plan and bumping database version. + */ + MSACDBSchema *schema = @{ + kMSACLogTableName : @[ + @{kMSACIdColumnName : @[ kMSACSQLiteTypeInteger, kMSACSQLiteConstraintPrimaryKey, kMSACSQLiteConstraintAutoincrement ]}, + @{kMSACGroupIdColumnName : @[ kMSACSQLiteTypeText, kMSACSQLiteConstraintNotNull ]}, + @{kMSACLogColumnName : @[ kMSACSQLiteTypeText, kMSACSQLiteConstraintNotNull ]}, + @{kMSACTargetTokenColumnName : @[ kMSACSQLiteTypeText ]}, @{kMSACTargetKeyColumnName : @[ kMSACSQLiteTypeText ]}, + @{kMSACPriorityColumnName : @[ kMSACSQLiteTypeInteger ]} + ] + }; + self = [self initWithSchema:schema version:kMSACSchemaVersion filename:kMSACDBFileName]; + if (self) { + NSDictionary *columnIndexes = [MSACDBStorage columnsIndexes:schema]; + _idColumnIndex = ((NSNumber *)columnIndexes[kMSACLogTableName][kMSACIdColumnName]).unsignedIntegerValue; + _groupIdColumnIndex = ((NSNumber *)columnIndexes[kMSACLogTableName][kMSACGroupIdColumnName]).unsignedIntegerValue; + _logColumnIndex = ((NSNumber *)columnIndexes[kMSACLogTableName][kMSACLogColumnName]).unsignedIntegerValue; + _targetTokenColumnIndex = ((NSNumber *)columnIndexes[kMSACLogTableName][kMSACTargetTokenColumnName]).unsignedIntegerValue; + _batches = [NSMutableDictionary *> new]; + _targetTokenEncrypter = [MSACEncrypter new]; + } + return self; +} + +#pragma mark - Save logs + +- (BOOL)saveLog:(id)log withGroupId:(NSString *)groupId flags:(MSACFlags)flags { + if (!log) { + return NO; + } + MSACFlags persistenceFlags = flags & kMSACPersistenceFlagsMask; + + // Insert this log to the DB. + NSString *base64Data = [[MSACUtility archiveKeyedData:log] base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed]; + + MSACStorageBindableArray *addLogValues = [MSACStorageBindableArray new]; + [addLogValues addString:groupId]; + [addLogValues addString:base64Data]; + [addLogValues addNumber:@(persistenceFlags)]; + NSString *addLogQuery = [NSString stringWithFormat:@"INSERT INTO \"%@\" (\"%@\", \"%@\", \"%@\") VALUES (?, ?, ?)", kMSACLogTableName, + kMSACGroupIdColumnName, kMSACLogColumnName, kMSACPriorityColumnName]; + + // Serialize target token. + if ([(NSObject *)log isKindOfClass:[MSACCommonSchemaLog class]]) { + NSString *targetToken = [[log transmissionTargetTokens] anyObject]; + NSString *encryptedToken = [self.targetTokenEncrypter encryptString:targetToken]; + NSString *targetKey = [MSACUtility targetKeyFromTargetToken:targetToken]; + + addLogValues = [MSACStorageBindableArray new]; + [addLogValues addString:groupId]; + [addLogValues addString:base64Data]; + [addLogValues addString:encryptedToken]; + [addLogValues addString:targetKey]; + [addLogValues addNumber:@(persistenceFlags)]; + addLogQuery = [NSString stringWithFormat:@"INSERT INTO \"%@\" (\"%@\", \"%@\", \"%@\", \"%@\", \"%@\") VALUES (?, ?, ?, ?, ?)", + kMSACLogTableName, kMSACGroupIdColumnName, kMSACLogColumnName, kMSACTargetTokenColumnName, + kMSACTargetKeyColumnName, kMSACPriorityColumnName]; + } + return [self executeQueryUsingBlock:^int(void *db) { + // Check maximum size. + NSUInteger maxSize = [MSACDBStorage getMaxPageCountInOpenedDatabase:db] * self.pageSize; + if (base64Data.length >= maxSize) { + MSACLogError([MSACAppCenter logTag], + @"Log is too large (%tu bytes) to store in database. Current maximum database size is %tu bytes.", + base64Data.length, maxSize); + return SQLITE_ERROR; + } + + // Try to insert. + int result = [MSACDBStorage executeNonSelectionQuery:addLogQuery inOpenedDatabase:db withValues:addLogValues]; + NSMutableArray *logsCanBeDeleted = nil; + if (result == SQLITE_FULL) { + + // Selecting logs with equal or lower priority and ordering by priority then age. + NSString *query = [NSString stringWithFormat:@"SELECT \"%@\" FROM \"%@\" WHERE \"%@\" <= ? ORDER BY \"%@\" ASC, \"%@\" ASC", + kMSACIdColumnName, kMSACLogTableName, kMSACPriorityColumnName, + kMSACPriorityColumnName, kMSACIdColumnName]; + MSACStorageBindableArray *values = [MSACStorageBindableArray new]; + [values addNumber:@(flags)]; + NSArray *entries = [MSACDBStorage executeSelectionQuery:query inOpenedDatabase:db withValues:values]; + logsCanBeDeleted = [NSMutableArray new]; + for (NSMutableArray *row in entries) { + [logsCanBeDeleted addObject:row[0]]; + } + } + + // If the database is full, delete logs until there is room to add the log. + long countOfLogsDeleted = 0; + NSUInteger index = 0; + while (result == SQLITE_FULL && index < [logsCanBeDeleted count]) { + result = [MSACLogDBStorage deleteLogsFromDBWithColumnValues:@[ logsCanBeDeleted[index] ] + columnName:kMSACIdColumnName + inOpenedDatabase:db]; + if (result != SQLITE_OK) { + break; + } + MSACLogDebug([MSACAppCenter logTag], @"Deleted a log with id %@ to store a new log.", logsCanBeDeleted[index]); + ++countOfLogsDeleted; + ++index; + result = [MSACDBStorage executeNonSelectionQuery:addLogQuery inOpenedDatabase:db withValues:addLogValues]; + } + if (countOfLogsDeleted > 0) { + MSACLogDebug([MSACAppCenter logTag], @"Log storage was over capacity, %ld oldest log(s) with equal or lower priority deleted.", + (long)countOfLogsDeleted); + } + if (result == SQLITE_OK) { + MSACLogVerbose([MSACAppCenter logTag], @"Log is stored with id: '%ld'", (long)sqlite3_last_insert_rowid(db)); + } else if (result == SQLITE_FULL && index == [logsCanBeDeleted count]) { + MSACLogError([MSACAppCenter logTag], @"Storage is full and no logs with equal or lower priority exist; discarding the log."); + } + return result; + }] == SQLITE_OK; +} + +#pragma mark - Load logs + +- (NSString *)buildKeyFormatWithCount:(NSUInteger)count { + NSString *keyFormat = [@"(" stringByPaddingToLength:count * 2 withString:@"?," startingAtIndex:0]; + keyFormat = [keyFormat stringByAppendingString:@")"]; + return keyFormat; +} + +- (BOOL)loadLogsWithGroupId:(NSString *)groupId + limit:(NSUInteger)limit + excludedTargetKeys:(nullable NSArray *)excludedTargetKeys + completionHandler:(nullable MSACLoadDataCompletionHandler)completionHandler { + BOOL logsAvailable; + BOOL moreLogsAvailable = NO; + NSString *batchId; + NSMutableArray *logEntries; + NSMutableArray *dbIds = [NSMutableArray new]; + NSMutableArray> *logs = [NSMutableArray> new]; + + // Get ids from batches. + NSMutableArray *idsInBatches = [NSMutableArray new]; + for (NSString *batchKey in [self.batches allKeys]) { + if ([batchKey hasPrefix:groupId]) { + [idsInBatches addObjectsFromArray:(NSArray * _Nonnull) self.batches[batchKey]]; + } + } + + // Build the "WHERE" clause's conditions. + NSMutableString *condition = [NSMutableString stringWithFormat:@"\"%@\" = ?", kMSACGroupIdColumnName]; + MSACStorageBindableArray *values = [MSACStorageBindableArray new]; + [values addString:groupId]; + + // Filter out paused target keys. + if (excludedTargetKeys != nil && excludedTargetKeys.count > 0) { + NSString *keyFormat = [self buildKeyFormatWithCount:excludedTargetKeys.count]; + [condition appendFormat:@" AND \"%@\" NOT IN %@", kMSACTargetKeyColumnName, keyFormat]; + for (NSString *item in excludedTargetKeys) { + [values addString:item]; + } + } + + // Take only logs that are not already part of a batch. + if (idsInBatches.count > 0) { + NSString *keyFormat = [self buildKeyFormatWithCount:idsInBatches.count]; + [condition appendFormat:@" AND \"%@\" NOT IN %@", kMSACIdColumnName, keyFormat]; + for (NSNumber *item in idsInBatches) { + [values addNumber:item]; + } + } + + // Build the "ORDER BY" clause's conditions. + [condition appendFormat:@" ORDER BY \"%@\" DESC, \"%@\" ASC", kMSACPriorityColumnName, kMSACIdColumnName]; + + /* + * There is a need to determine if there will be more logs available than those under the limit. This is just about knowing if there is at + * least 1 log above the limit. + * + * FIXME: We should simply use a count API from the consumer object instead of the "limit + 1" technique, it only requires 1 SQL request + * instead of 2 for the count but it is a bit confusing and doesn't really fit a database storage. + */ + [condition appendFormat:@" LIMIT %lu", (unsigned long)((limit < NSUIntegerMax) ? limit + 1 : limit)]; + + // Get log entries from DB. + logEntries = [[self logsWithCondition:condition andValues:values] mutableCopy]; + + // More logs available for the next batch, remove the log in excess for this batch. + if (logEntries.count > 0 && logEntries.count > limit) { + [logEntries removeLastObject]; + moreLogsAvailable = YES; + } + + // Get lists of logs and DB ids. + for (NSArray *logEntry in logEntries) { + [dbIds addObject:logEntry[self.idColumnIndex]]; + [logs addObject:logEntry[self.logColumnIndex]]; + } + + // Generate batch Id. + logsAvailable = logEntries.count > 0; + if (logsAvailable) { + batchId = MSAC_UUID_STRING; + self.batches[[groupId stringByAppendingString:batchId]] = dbIds; + MSACLogVerbose([MSACAppCenter logTag], @"Load log(s) with id(s) '%@' as batch Id:%@", [dbIds componentsJoinedByString:@"','"], batchId); + } + + // Load completed. + if (completionHandler) { + completionHandler(logs, batchId); + } + + // Return YES if more logs available. + return moreLogsAvailable; +} + +#pragma mark - Delete logs + +- (NSArray> *)deleteLogsWithGroupId:(NSString *)groupId { + NSArray> *logs = [self logsFromDBWithGroupId:groupId]; + + // Delete logs. + [self deleteLogsFromDBWithColumnValue:groupId columnName:kMSACGroupIdColumnName]; + + // Delete related batches. + for (NSString *batchKey in [self.batches allKeys]) { + if ([batchKey hasPrefix:groupId]) { + [self.batches removeObjectForKey:batchKey]; + } + } + return logs; +} + +- (void)deleteLogsWithBatchId:(NSString *)batchId groupId:(NSString *)groupId { + + // Get log Ids. + NSString *batchIdKey = [groupId stringByAppendingString:batchId]; + NSArray *ids = self.batches[batchIdKey]; + + // Delete logs and associated batch. + if (ids.count > 0) { + [self deleteLogsFromDBWithColumnValues:ids columnName:kMSACIdColumnName]; + [self.batches removeObjectForKey:batchIdKey]; + } +} + +#pragma mark - DB selection + +- (NSArray> *)logsFromDBWithGroupId:(NSString *)groupId { + + // Get log entries for the given group Id. + NSString *condition = [NSString stringWithFormat:@"\"%@\" = ?", kMSACGroupIdColumnName]; + MSACStorageBindableArray *values = [MSACStorageBindableArray new]; + [values addString:groupId]; + NSArray *logEntries = [self logsWithCondition:condition andValues:values]; + + // Get logs only. + NSMutableArray> *logs = [NSMutableArray> new]; + for (NSArray *logEntry in logEntries) { + [logs addObject:logEntry[self.logColumnIndex]]; + } + return logs; +} + +- (NSArray *)logsWithCondition:(NSString *_Nullable)condition andValues:(nullable MSACStorageBindableArray *)values { + NSMutableArray *logEntries = [NSMutableArray new]; + NSMutableString *query = [NSMutableString stringWithFormat:@"SELECT * FROM \"%@\"", kMSACLogTableName]; + if (condition.length > 0) { + [query appendFormat:@" WHERE %@", condition]; + } + NSArray *entries = [self executeSelectionQuery:query withValues:values]; + + // Get logs from DB. + for (NSMutableArray *row in entries) { + NSNumber *dbId = row[self.idColumnIndex]; + NSData *logData = [[NSData alloc] initWithBase64EncodedString:row[self.logColumnIndex] + options:NSDataBase64DecodingIgnoreUnknownCharacters]; + id log; + + // Deserialize the log. + log = (id)[MSACUtility unarchiveKeyedData:logData]; + if (!log) { + + // The archived log is not valid. + MSACLogError([MSACAppCenter logTag], @"Deserialization failed for log with Id %@", dbId); + [self deleteLogFromDBWithId:dbId]; + continue; + } + + // Deserialize target token. A token value from the row dictionary can't be `nil` but can be of class `NSNull`. + NSString *encryptedToken = row[self.targetTokenColumnIndex]; + if ([encryptedToken isKindOfClass:[NSString class]]) { + if (encryptedToken.length > 0) { + NSString *targetToken = [self.targetTokenEncrypter decryptString:encryptedToken]; + if (targetToken) { + [log addTransmissionTargetToken:targetToken]; + } else { + MSACLogError([MSACAppCenter logTag], @"Failed to decrypt the target token for log with Id %@.", dbId); + } + } else { + MSACLogError([MSACAppCenter logTag], @"Unexpected empty target token for log with Id %@.", dbId); + } + } + + // Update with deserialized log. + row[self.logColumnIndex] = log; + [logEntries addObject:row]; + } + return logEntries; +} + +#pragma mark - DB deletion + +- (void)deleteLogFromDBWithId:(NSNumber *)dbId { + [self deleteLogsFromDBWithColumnValue:dbId columnName:kMSACIdColumnName]; +} + +- (void)deleteLogsFromDBWithColumnValue:(id)columnValue columnName:(NSString *)columnName { + [self deleteLogsFromDBWithColumnValues:@[ columnValue ] columnName:columnName]; +} + +- (void)deleteLogsFromDBWithColumnValues:(NSArray *)columnValues columnName:(NSString *)columnName { + [self executeQueryUsingBlock:^int(void *db) { + return [MSACLogDBStorage deleteLogsFromDBWithColumnValues:columnValues columnName:columnName inOpenedDatabase:db]; + }]; +} + ++ (int)deleteLogsFromDBWithColumnValues:(NSArray *)columnValues columnName:(NSString *)columnName inOpenedDatabase:(void *)db { + NSString *deletionTrace = [NSString + stringWithFormat:@"Deletion of log(s) by %@ with value(s) '%@'", columnName, [columnValues componentsJoinedByString:@"','"]]; + + // Build up delete query. + char surroundingChar = (char)(([(NSObject *)[columnValues firstObject] isKindOfClass:[NSString class]]) ? '\'' : '\0'); + NSString *valuesSeparation = [NSString stringWithFormat:@"%c, %c", surroundingChar, surroundingChar]; + NSString *whereCondition = [NSString stringWithFormat:@"\"%@\" IN (%c%@%c)", columnName, surroundingChar, + [columnValues componentsJoinedByString:valuesSeparation], surroundingChar]; + NSString *deleteLogsQuery = [NSString stringWithFormat:@"DELETE FROM \"%@\" WHERE %@", kMSACLogTableName, whereCondition]; + + // Execute. + int result = [MSACDBStorage executeNonSelectionQuery:deleteLogsQuery inOpenedDatabase:db]; + if (result == SQLITE_OK) { + MSACLogVerbose([MSACAppCenter logTag], @"%@ succeeded.", deletionTrace); + } else { + MSACLogError([MSACAppCenter logTag], @"%@ failed.", deletionTrace); + } + return result; +} + +#pragma mark - DB count + +- (NSUInteger)countLogs { + return [self countEntriesForTable:kMSACLogTableName condition:nil withValues:nil]; +} + +#pragma mark - DB migration + +- (void)createPriorityIndex:(void *)db { + NSString *indexStatement = [NSString stringWithFormat:@"CREATE INDEX \"ix_%@_%@\" ON \"%@\" (\"%@\")", kMSACLogTableName, + kMSACPriorityColumnName, kMSACLogTableName, kMSACPriorityColumnName]; + [MSACDBStorage executeNonSelectionQuery:indexStatement inOpenedDatabase:db]; +} + +- (void)customizeDatabase:(void *)db { + [self createPriorityIndex:db]; +} + +/* + * Migration process is implemented through database versioning. + * After altering current schema, database version should be bumped and actions for migration should be implemented in this method. + */ +- (void)migrateDatabase:(void *)db fromVersion:(NSUInteger __unused)version { + + /* + * With version 3.0 of the SDK we decided to remove timestamp column and as + * it's a major SDK version and SQLite does not support removing column we just start over. + * When adding a new column in a future version, update this code by something like + * if (version <= kMSACDropTableVersion) {drop/create} else {add missing columns} + */ + [self dropTable:kMSACLogTableName]; + [MSACDBStorage createTablesWithSchema:self.schema inOpenedDatabase:db]; + [self customizeDatabase:db]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACLogDBStoragePrivate.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACLogDBStoragePrivate.h new file mode 100644 index 0000000000..00a590e883 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACLogDBStoragePrivate.h @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACEncrypter.h" +#import "MSACLogDBStorage.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const kMSACDBFileName = @"Logs.sqlite"; +static NSString *const kMSACLogTableName = @"logs"; +static NSString *const kMSACIdColumnName = @"id"; +static NSString *const kMSACGroupIdColumnName = @"groupId"; +static NSString *const kMSACLogColumnName = @"log"; +static NSString *const kMSACTargetTokenColumnName = @"targetToken"; +static NSString *const kMSACTargetKeyColumnName = @"targetKey"; +static NSString *const kMSACPriorityColumnName = @"priority"; + +@protocol MSACDatabaseConnection; + +@interface MSACLogDBStorage () + +/** + * Keep track of logs batches per group Id associated with their logs Ids. + */ +@property(nonatomic) NSMutableDictionary *> *batches; + +/** + * "id" database column index. + */ +@property(nonatomic, readonly) NSUInteger idColumnIndex; + +/** + * "groupId" database column index. + */ +@property(nonatomic, readonly) NSUInteger groupIdColumnIndex; + +/** + * "log" database column index. + */ +@property(nonatomic, readonly) NSUInteger logColumnIndex; + +/** + * "targetToken" database column index. + */ +@property(nonatomic, readonly) NSUInteger targetTokenColumnIndex; + +/** + * Encrypter for target tokens. + */ +@property(nonatomic, readonly) MSACEncrypter *targetTokenEncrypter; + +/** + * Get all logs with the given group Id from the storage. + * + * @param groupId The key used for grouping logs. + * + * @return Logs and their ids corresponding to the given group Id from the storage. + */ +- (NSArray> *)logsFromDBWithGroupId:(NSString *)groupId; + +/** + * Builds a string for sqlite values binding: for example, (?, ?, ?). + */ +- (NSString *)buildKeyFormatWithCount:(NSUInteger)count; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACLogDBStorageVersion.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACLogDBStorageVersion.h new file mode 100644 index 0000000000..962fd346c0 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACLogDBStorageVersion.h @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +NS_ASSUME_NONNULL_BEGIN + +static NSUInteger const kMSACDropTableVersion = 5; + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACStorage.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACStorage.h new file mode 100644 index 0000000000..ce1b86e4e3 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACStorage.h @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACConstants+Flags.h" +#import "MSACLog.h" +#import "MSACLogContainer.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Completion handler triggered when data is loaded from the storage. + * + * @param logArray Array of logs loaded from the storage. + * @param batchId Batch Id associated with the logs, `nil` if no logs available. + */ +typedef void (^MSACLoadDataCompletionHandler)(NSArray> *_Nullable logArray, NSString *_Nullable batchId); + +/** + * Defines the storage component which is responsible for persisting logs. + */ +@protocol MSACStorage + +@required + +/** + * Store a log. + * + * @param log The log to be stored. + * @param groupId The key used for grouping logs. + * @param flags A flag that indicates if the log has critical persistence priority. + * + * @return BOOL that indicates if the log was saved successfully. + */ +- (BOOL)saveLog:(id)log withGroupId:(NSString *)groupId flags:(MSACFlags)flags; + +/** + * Get the number of logs stored in the storage. + * + * @return The number of logs. + */ +- (NSUInteger)countLogs; + +/** + * Delete logs related to given group from the storage. + * + * @param groupId The key used for grouping logs. + * + * @return The list of deleted logs. + */ +- (NSArray> *)deleteLogsWithGroupId:(NSString *)groupId; + +/** + * Delete a log from the storage. + * + * @param batchId Id of the log to be deleted from storage. + * @param groupId The key used for grouping logs. + */ +- (void)deleteLogsWithBatchId:(NSString *)batchId groupId:(NSString *)groupId; + +/** + * Return the most recent logs for a Group Id. + * + * @param groupId The key used for grouping. + * @param limit Limit the maximum number of logs to be loaded from disk. + * @param excludedTargetKeys The array of target keys to exclude for the logs. + * @param completionHandler The completion handler for loading the logs. + * + * @return a list of logs. + */ +- (BOOL)loadLogsWithGroupId:(NSString *)groupId + limit:(NSUInteger)limit + excludedTargetKeys:(nullable NSArray *)excludedTargetKeys + completionHandler:(nullable MSACLoadDataCompletionHandler)completionHandler; + +/** + * Set the maximum size of the internal storage. This method must be called before App Center is started. + * + * @param sizeInBytes Maximum size of the internal storage in bytes. This will be rounded up to the nearest multiple of a SQLite page size + * (default is 4096 bytes). Values below 24576 bytes (24 KiB) will be ignored. + * @param completionHandler Callback that is invoked when the database size has been set. The `BOOL` parameter is `YES` if changing the size + * is successful, and `NO` otherwise. + * + * @discussion The value passed to this method is not persisted on disk. The default maximum database size is 10485760 bytes (10 MiB). + * + */ +- (void)setMaxStorageSize:(long)sizeInBytes completionHandler:(nullable void (^)(BOOL))completionHandler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACStorageBindableArray.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACStorageBindableArray.h new file mode 100644 index 0000000000..2c515edbae --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACStorageBindableArray.h @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACStorageBindableType.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface MSACStorageBindableArray : NSObject + +/** + * Custom array for storing values to be bound in an sqlite statement. + * Accepts only supported types. + */ +@property(nonatomic) NSMutableArray> *array; + +/** + * Adds an NSString object into array. + * + * @param value A string value to be added to the array. + */ +- (void)addString:(nullable NSString *)value; + +/** + * Adds a number object into array. + * + * @param value NSNumber value to be added to the array. + * Can not be nil since it means it's an error. + */ +- (void)addNumber:(NSNumber *)value; + +/** + * Binds all values in an array with given sqlite statement. + * + * @param query A SQLite statement. + * @param db A reference to database. + */ +- (int)bindAllValuesWithStatement:(void *)query inOpenedDatabase:(void *)db; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACStorageBindableArray.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACStorageBindableArray.m new file mode 100644 index 0000000000..cf9ab07f5f --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACStorageBindableArray.m @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAppCenterInternal.h" +#import "MSACStorageBindableArray.h" +#import "MSACStorageNumberType.h" +#import "MSACStorageTextType.h" + +@implementation MSACStorageBindableArray + +- (instancetype)init { + if ((self = [super init])) { + _array = [NSMutableArray new]; + } + return self; +} + +- (void)addString:(nullable NSString *)value { + [self.array addObject:[[MSACStorageTextType alloc] initWithValue:value]]; +} + +- (void)addNumber:(nonnull NSNumber *)value { + [self.array addObject:[[MSACStorageNumberType alloc] initWithValue:value]]; +} + +- (int)bindAllValuesWithStatement:(void *)query inOpenedDatabase:(void *)db { + for (int i = 0; i < (int)self.array.count; i++) { + id value = self.array[i]; + int result = [value bindWithStatement:query atIndex:i + 1]; + if (result != SQLITE_OK) { + MSACLogError([MSACAppCenter logTag], @"Binding query parameter %d failed with error: %d. Message: %@", i + 1, result, + [NSString stringWithUTF8String:sqlite3_errmsg(db)]); + return result; + } + } + return SQLITE_OK; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACStorageBindableType.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACStorageBindableType.h new file mode 100644 index 0000000000..b2b01a1024 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACStorageBindableType.h @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Defines the storage type to be bound in an sql statement. + */ +@protocol MSACStorageBindableType + +@required + +/** + * Binds itself with a statement. + * + * @param query SQLite statement. + * @param index Position of the parameter. + * + * @return int result code. + */ +- (int)bindWithStatement:(void *)query atIndex:(int)index; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACStorageNumberType.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACStorageNumberType.h new file mode 100644 index 0000000000..84b1f03f89 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACStorageNumberType.h @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACStorageBindableType.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface MSACStorageNumberType : NSObject + +@property(nonatomic) NSNumber *value; + +/** + * Initializer with a value represented as NSNumber. + */ +- (instancetype)initWithValue:(NSNumber *)value; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACStorageNumberType.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACStorageNumberType.m new file mode 100644 index 0000000000..3148298450 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACStorageNumberType.m @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACStorageNumberType.h" + +@implementation MSACStorageNumberType + +- (instancetype)initWithValue:(NSNumber *)value { + if ((self = [super init])) { + _value = value; + } + return self; +} + +- (int)bindWithStatement:(void *)query atIndex:(int)index { + return sqlite3_bind_int64(query, index, [self.value longLongValue]); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACStorageTextType.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACStorageTextType.h new file mode 100644 index 0000000000..366c028f91 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACStorageTextType.h @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACStorageBindableType.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface MSACStorageTextType : NSObject + +@property(nonatomic, nullable) NSString *value; + +/** + * Initializer with a value represented as NSString. + */ +- (instancetype __nonnull)initWithValue:(nullable NSString *)value; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACStorageTextType.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACStorageTextType.m new file mode 100644 index 0000000000..0b45f969b5 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Storage/MSACStorageTextType.m @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACStorageTextType.h" + +@implementation MSACStorageTextType + +- (instancetype)initWithValue:(nullable NSString *)value { + if ((self = [super init])) { + _value = value; + } + return self; +} + +- (int)bindWithStatement:(void *)query atIndex:(int)index { + if (self.value) { + return sqlite3_bind_text(query, index, [self.value UTF8String], -1, SQLITE_TRANSIENT); + } else { + return sqlite3_bind_null(query, index); + } +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACCompression.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACCompression.h new file mode 100644 index 0000000000..c941211089 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACCompression.h @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +@interface MSACCompression : NSObject + +/** + * Compress given data using zlib. + * + * @param data Data to compress. + * + * @return Compressed data. + */ ++ (NSData *)compressData:(NSData *)data; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACCompression.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACCompression.m new file mode 100644 index 0000000000..2765ffdf63 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACCompression.m @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACCompression.h" +#import "MSACAppCenterInternal.h" +#import "MSACLogger.h" +#import "zlib.h" + +@implementation MSACCompression + +// See https://www.zlib.net/manual.html for more details on zlib usage. ++ (NSData *)compressData:(NSData *)data { + if (data == nil || data.length < 1) { + return nil; + } + + // set struct values + z_stream zStreamStruct; + zStreamStruct.zalloc = NULL; // use default values for these 3 + zStreamStruct.zfree = NULL; + zStreamStruct.opaque = NULL; + zStreamStruct.total_out = 0; // # of bytes written out +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wcast-qual" + zStreamStruct.next_in = (Bytef *)[data bytes]; +#pragma clang diagnostic pop + zStreamStruct.avail_in = (unsigned int)data.length; + + // Init zlib. windowBits is 31: (15 max compression rate + 16 gzip header and trailer), memLevel is 8: default memory allocation. + int initError = deflateInit2(&zStreamStruct, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY); + if (initError != Z_OK) { + NSString *errorMsg = nil; + switch (initError) { + case Z_STREAM_ERROR: + errorMsg = @"Invalid parameter passed in to function."; + break; + case Z_MEM_ERROR: + errorMsg = @"Not enough memory."; + break; + case Z_VERSION_ERROR: + errorMsg = @"Version of zlib.h & libz library do not match!"; + break; + default: + errorMsg = @"Unknown error!"; + break; + } + MSACLogError(MSACAppCenter.logTag, @"Compression failed with error: %@", errorMsg); + return nil; + } + + // zlib documentation says it should be this size. + NSMutableData *compressedData = [NSMutableData dataWithLength:(NSUInteger)([data length] * 1.01 + 12)]; + + // Deflate data. + int deflateStatus; + do { + + // Set next_out to be beginning of mutable data + the total already output. + zStreamStruct.next_out = (unsigned char *)[compressedData mutableBytes] + zStreamStruct.total_out; + + // Set avail_out to total length - the total already output. + zStreamStruct.avail_out = (unsigned int)([compressedData length] - zStreamStruct.total_out); + + // Call deflate, which will update total_out & return Z_STREAM_END if done, Z_OK if more to do, or an error message. + deflateStatus = deflate(&zStreamStruct, Z_FINISH); + } while (deflateStatus == Z_OK); + if (deflateStatus != Z_STREAM_END) { + NSString *errorMsg = nil; + switch (deflateStatus) { + case Z_BUF_ERROR: + errorMsg = @"No progress is possible."; + break; + case Z_STREAM_ERROR: + errorMsg = @"Inconsistent stream state."; + break; + default: + errorMsg = @"Unknown error!"; + break; + } + MSACLogError(MSACAppCenter.logTag, @"Deflate failed with error: %@", errorMsg); + deflateEnd(&zStreamStruct); + return nil; + } + deflateEnd(&zStreamStruct); + [compressedData setLength:zStreamStruct.total_out]; + return compressedData; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACConstants+Internal.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACConstants+Internal.h new file mode 100644 index 0000000000..b5820556ea --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACConstants+Internal.h @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +// Device manufacturer +static NSString *const kMSACDeviceManufacturer = @"Apple"; + +// HTTP method names. +static NSString *const kMSACHttpMethodGet = @"GET"; +static NSString *const kMSACHttpMethodPost = @"POST"; +static NSString *const kMSACHttpMethodDelete = @"DELETE"; + +// HTTP Headers + Query string. +static NSString *const kMSACHeaderAppSecretKey = @"App-Secret"; +static NSString *const kMSACHeaderInstallIDKey = @"Install-ID"; +static NSString *const kMSACHeaderContentTypeKey = @"Content-Type"; +static NSString *const kMSACAppCenterContentType = @"application/json"; +static NSString *const kMSACHeaderContentEncodingKey = @"Content-Encoding"; +static NSString *const kMSACHeaderContentEncoding = @"gzip"; +static NSString *const kMSACRetryHeaderKey = @"x-ms-retry-after-ms"; + +// Token obfuscation. +static NSString *const kMSACTokenKeyValuePattern = @"\"token\"\\s*:\\s*\"[^\"]+\""; +static NSString *const kMSACTokenKeyValueObfuscatedTemplate = @"\"token\" : \"***\""; + +// Redirect URI obfuscation. +static NSString *const kMSACRedirectUriPattern = @"\"redirect_uri\"\\s*:\\s*\"[^\"]+\""; +static NSString *const kMSACRedirectUriObfuscatedTemplate = @"\"redirect_uri\" : \"***\""; + +// Info.plist key names. +static NSString *const kMSACCFBundleURLTypes = @"CFBundleURLTypes"; +static NSString *const kMSACCFBundleURLSchemes = @"CFBundleURLSchemes"; +static NSString *const kMSACCFBundleTypeRole = @"CFBundleTypeRole"; + +// Other HTTP constants. +static short const kMSACHTTPMinGZipLength = 1400; + +/** + * Enum indicating result of a MSACIngestionCall. + */ +typedef NS_ENUM(NSInteger, MSACIngestionCallResult) { + MSACIngestionCallResultSuccess = 100, + MSACIngestionCallResultRecoverableError = 500, + MSACIngestionCallResultFatalError = 999 +}; + +/** + * Constants for maximum number and length of log properties. + */ +/** + * Maximum properties per log. + */ +static const int kMSACMaxPropertiesPerLog = 20; + +/** + * Minimum properties key length. + */ +static const int kMSACMinPropertyKeyLength = 1; + +/** + * Maximum properties key length. + */ +static const int kMSACMaxPropertyKeyLength = 125; + +/** + * Maximum properties value length. + */ +static const int kMSACMaxPropertyValueLength = 125; + +/** + * Maximum allowable size of a common schema log in bytes. + */ +static const long kMSACMaximumCommonSchemaLogSizeInBytes = 2 * 1024 * 1024; + +/** + * Suffix for One Collector group ID. + */ +static NSString *const kMSACOneCollectorGroupIdSuffix = @"/one"; + +/** + * Bit mask for persistence flags. + */ +static const NSUInteger kMSACPersistenceFlagsMask = 0xFF; + +/** + * Common schema prefix separator used in various field values. + */ +static NSString *const kMSACCommonSchemaPrefixSeparator = @":"; + +/** + * Default flush interval for channel. + */ +static NSUInteger const kMSACFlushIntervalDefault = 3; diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACDispatcherUtil.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACDispatcherUtil.h new file mode 100644 index 0000000000..b359de8541 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACDispatcherUtil.h @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#define MSAC_DISPATCH_SELECTOR(declaration, object, selectorName, ...) \ + ({ \ + SEL selector = NSSelectorFromString(@ #selectorName); \ + IMP impl = [object methodForSelector:selector]; \ + (declaration impl)(object, selector, ##__VA_ARGS__); \ + }) + +@interface MSACDispatcherUtil : NSObject + ++ (void)performBlockOnMainThread:(void (^)(void))block; + +/** + * Adds a dispatch_async block to the provided queue and waits for its execution. + * @param timeout Timeout for waiting in seconds. + * @param dispatchQueue The queue to perform a block on. + * @param block The block to be executed. + */ ++ (void)dispatchSyncWithTimeout:(NSTimeInterval)timeout onQueue:(dispatch_queue_t)dispatchQueue withBlock:(dispatch_block_t)block; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACDispatcherUtil.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACDispatcherUtil.m new file mode 100644 index 0000000000..a77ccddbcf --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACDispatcherUtil.m @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACDispatcherUtil.h" +#import "MSACAppCenterInternal.h" + +@implementation MSACDispatcherUtil + ++ (void)performBlockOnMainThread:(void (^)(void))block { + +#if TARGET_OS_OSX + [self performSelectorOnMainThread:@selector(runBlock:) withObject:block waitUntilDone:NO]; +#else + if ([NSThread isMainThread]) { + block(); + } else { + dispatch_async(dispatch_get_main_queue(), block); + } +#endif +} + ++ (void)runBlock:(void (^)(void))block { + block(); +} + ++ (void)dispatchSyncWithTimeout:(NSTimeInterval)timeout onQueue:(dispatch_queue_t)dispatchQueue withBlock:(dispatch_block_t)block { + dispatch_semaphore_t delayedProcessingSemaphore = dispatch_semaphore_create(0); + dispatch_async(dispatchQueue, ^{ + block(); + dispatch_semaphore_signal(delayedProcessingSemaphore); + }); + dispatch_semaphore_wait(delayedProcessingSemaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC))); +} +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACEncrypter.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACEncrypter.h new file mode 100644 index 0000000000..7b12a13775 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACEncrypter.h @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Class for Encryption. Uses RSA algorithm with key size 256. If no key pair is specified, generates new key pair and stores it in + * Keychain. Key pair is loaded if it is present in Keychain. + */ +@interface MSACEncrypter : NSObject + +- (NSString *_Nullable)encryptString:(NSString *)string; + +- (NSString *_Nullable)decryptString:(NSString *)string; + +- (NSData *_Nullable)encryptData:(NSData *)data; + +- (NSData *_Nullable)decryptData:(NSData *)data; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACEncrypter.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACEncrypter.m new file mode 100644 index 0000000000..bf1c27ae08 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACEncrypter.m @@ -0,0 +1,232 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAppCenterInternal.h" +#import "MSACConstants+Internal.h" +#import "MSACEncrypterPrivate.h" +#import "MSACKeychainUtil.h" +#import "MSACLogger.h" + +static NSObject *const classLock; + +@interface MSACEncrypter () + +@property(atomic) NSData *originalKeyData; +@property(atomic) NSData *alternateKeyData; + +@end + +@implementation MSACEncrypter + +- (NSString *_Nullable)encryptString:(NSString *)string { + NSData *dataToEncrypt = [string dataUsingEncoding:NSUTF8StringEncoding]; + NSData *encryptedData = [self encryptData:dataToEncrypt]; + return [encryptedData base64EncodedStringWithOptions:0]; +} + +- (NSData *_Nullable)encryptData:(NSData *)data { + NSString *keyTag = [MSACEncrypter getCurrentKeyTag]; + NSData *key = [self getKeyWithKeyTag:keyTag]; + NSData *initializationVector = [MSACEncrypter generateInitializationVector]; + NSData *result = [MSACEncrypter performCryptoOperation:kCCEncrypt input:data initializationVector:initializationVector key:key]; + if (result) { + NSData *metadata = [MSACEncrypter getMetadataStringWithKeyTag:keyTag]; + NSMutableData *mutableData = [NSMutableData new]; + [mutableData appendData:metadata]; + [mutableData appendBytes:(const void *)[kMSACEncryptionMetadataSeparator UTF8String] length:1]; + [mutableData appendData:initializationVector]; + [mutableData appendData:result]; + result = mutableData; + } + return result; +} + +- (NSString *_Nullable)decryptString:(NSString *)string { + NSString *result = nil; + NSData *dataToDecrypt = [[NSData alloc] initWithBase64EncodedString:string options:0]; + if (dataToDecrypt) { + NSData *decryptedBytes = [self decryptData:dataToDecrypt]; + result = [[NSString alloc] initWithData:decryptedBytes encoding:NSUTF8StringEncoding]; + if (!result) { + MSACLogWarning([MSACAppCenter logTag], @"Converting decrypted NSData to NSString failed."); + } + } else { + MSACLogWarning([MSACAppCenter logTag], @"Conversion of encrypted string to NSData failed."); + } + return result; +} + +- (NSData *_Nullable)decryptData:(NSData *)data { + + // Separate cipher prefix from cipher. + NSRange dataRange = NSMakeRange(0, [data length]); + NSData *separatorAsData = [kMSACEncryptionMetadataSeparator dataUsingEncoding:NSUTF8StringEncoding]; + size_t metadataLocation = [data rangeOfData:separatorAsData options:0 range:dataRange].location; + NSString *metadata; + if (metadataLocation != NSNotFound) { + NSData *subdata = [data subdataWithRange:NSMakeRange(0, metadataLocation)]; + metadata = [[NSString alloc] initWithData:subdata encoding:NSUTF8StringEncoding]; + } + NSData *key; + NSData *initializationVector; + NSData *cipherText; + if (metadata) { + + // Extract key from metadata. + NSString *keyTag = [metadata componentsSeparatedByString:kMSACEncryptionMetadataInternalSeparator][0]; + + // Metadata, separator, and initialization vector. + size_t cipherTextPrefixLength = metadataLocation + 1 + kCCBlockSizeAES128; + NSRange cipherTextRange = NSMakeRange(cipherTextPrefixLength, [data length] - cipherTextPrefixLength); + NSRange ivRange = NSMakeRange(metadataLocation + 1, kCCBlockSizeAES128); + initializationVector = [data subdataWithRange:ivRange]; + cipherText = [data subdataWithRange:cipherTextRange]; + key = [self getKeyWithKeyTag:keyTag]; + } else { + + // If there is no metadata, this is old data, so use the old key and an empty initialization vector. + key = [self getKeyWithKeyTag:kMSACEncryptionKeyTagOriginal]; + cipherText = data; + } + return [MSACEncrypter performCryptoOperation:kCCDecrypt input:cipherText initializationVector:initializationVector key:key]; +} + ++ (NSString *)getCurrentKeyTag { + @synchronized(classLock) { + NSString *keyMetadata = [MSAC_APP_CENTER_USER_DEFAULTS objectForKey:kMSACEncryptionKeyMetadataKey]; + if (!keyMetadata) { + [self rotateToNewKeyTag:kMSACEncryptionKeyTagAlternate]; + return kMSACEncryptionKeyTagAlternate; + } + + // Format is {keyTag}/{expiration as iso}. + NSArray *keyMetadataComponents = [keyMetadata componentsSeparatedByString:kMSACEncryptionMetadataInternalSeparator]; + NSString *keyTag = keyMetadataComponents[0]; + NSString *expirationIso = keyMetadataComponents[1]; + NSDate *expiration = [MSACUtility dateFromISO8601:expirationIso]; + BOOL isNotExpired = [[expiration laterDate:[NSDate date]] isEqualToDate:expiration]; + if (isNotExpired) { + return keyTag; + } + + // Key is expired and must be rotated. + if ([keyTag isEqualToString:kMSACEncryptionKeyTagOriginal]) { + keyTag = kMSACEncryptionKeyTagAlternate; + } else { + keyTag = kMSACEncryptionKeyTagOriginal; + } + [self rotateToNewKeyTag:keyTag]; + return keyTag; + } +} + ++ (void)rotateToNewKeyTag:(NSString *)newKeyTag { + NSDate *expiration = [[NSDate date] dateByAddingTimeInterval:kMSACEncryptionKeyLifetimeInSeconds]; + NSString *expirationIso = [MSACUtility dateToISO8601:expiration]; + + // Format is {keyTag}/{expiration as iso}. + NSString *keyMetadata = [@[ newKeyTag, expirationIso ] componentsJoinedByString:kMSACEncryptionMetadataInternalSeparator]; + [MSAC_APP_CENTER_USER_DEFAULTS setObject:keyMetadata forKey:kMSACEncryptionKeyMetadataKey]; +} + +- (NSData *)getKeyWithKeyTag:(NSString *)keyTag { + NSData *keyData; + BOOL isOriginalKeyTag = [keyTag isEqualToString:kMSACEncryptionKeyTagOriginal]; + keyData = isOriginalKeyTag ? self.originalKeyData : self.alternateKeyData; + + // Key was found in memory. + if (keyData) { + return keyData; + } + + // If key is not in memory; try loading it from Keychain. + NSString *stringKey = [MSACKeychainUtil stringForKey:keyTag statusCode:nil]; + if (stringKey) { + keyData = [[NSData alloc] initWithBase64EncodedString:stringKey options:0]; + } else { + + // If key is not saved in Keychain, create one and save it. This will only happen at most twice after an app is installed. + @synchronized(classLock) { + + // Recheck if the key has been written from another thread. + stringKey = [MSACKeychainUtil stringForKey:keyTag statusCode:nil]; + if (!stringKey) { + keyData = [MSACEncrypter generateAndSaveKeyWithTag:keyTag]; + } + } + if (isOriginalKeyTag) { + self.originalKeyData = keyData; + } else { + self.alternateKeyData = keyData; + } + } + return keyData; +} + ++ (NSData *_Nullable)performCryptoOperation:(CCOperation)operation + input:(NSData *)input + initializationVector:(NSData *)initializationVector + key:(NSData *)key { + NSData *result; + + // Create a buffer whose size is at least one block plus 1. This is not needed for decryption, but it works. + size_t outputBufferSize = [input length] + kCCBlockSizeAES128 + 1; + uint8_t *outputBuffer = malloc(outputBufferSize * sizeof(uint8_t)); + size_t numBytesNeeded = 0; + CCCryptorStatus status = + CCCrypt(operation, kMSACEncryptionAlgorithm, kCCOptionPKCS7Padding, [key bytes], kMSACEncryptionKeySize, [initializationVector bytes], + [input bytes], input.length, outputBuffer, outputBufferSize, &numBytesNeeded); + if (status != kCCSuccess) { + + // Do not print the status; it is a security requirement that specific crypto errors are not printed. + MSACLogError([MSACAppCenter logTag], @"Error performing encryption or decryption."); + } else { + result = [NSData dataWithBytes:outputBuffer length:numBytesNeeded]; + if (!result) { + MSACLogError([MSACAppCenter logTag], @"Could not create NSData object from encrypted or decrypted bytes."); + } + } + free(outputBuffer); + return result; +} + ++ (NSData *)generateAndSaveKeyWithTag:(NSString *)keyTag { + NSData *resultKey = nil; + uint8_t *keyBytes = nil; + keyBytes = malloc(kMSACEncryptionKeySize * sizeof(uint8_t)); + OSStatus status = SecRandomCopyBytes(kSecRandomDefault, kMSACEncryptionKeySize, keyBytes); + if (status != errSecSuccess) { + MSACLogError([MSACAppCenter logTag], @"Error generating encryption key. Error code: %d", (int)status); + } + resultKey = [[NSData alloc] initWithBytes:keyBytes length:kMSACEncryptionKeySize]; + free(keyBytes); + + // Save key to the Keychain. + NSString *stringKey = [resultKey base64EncodedStringWithOptions:0]; + [MSACKeychainUtil storeString:stringKey forKey:keyTag]; + return resultKey; +} + ++ (NSData *)generateInitializationVector { + uint8_t *ivBytes = malloc(kCCBlockSizeAES128 * sizeof(uint8_t)); + OSStatus status = SecRandomCopyBytes(kSecRandomDefault, kCCBlockSizeAES128, ivBytes); + if (status != errSecSuccess) { + MSACLogError([MSACAppCenter logTag], @"Error generating initialization vector. Error code: %d", (int)status); + } + NSData *initializationVector = [NSData dataWithBytes:ivBytes length:kCCBlockSizeAES128]; + free(ivBytes); + return initializationVector; +} + ++ (NSData *)getMetadataStringWithKeyTag:(NSString *)keyTag { + + // Format is {key tag}/{algorithm}/{cipher mode}/{padding mode}/{key length} + NSArray *metadata = + @[ keyTag, kMSACEncryptionAlgorithmName, kMSACEncryptionCipherMode, kMSACEncryptionPaddingMode, @(kMSACEncryptionKeySize) ]; + NSString *metadataString = [metadata componentsJoinedByString:kMSACEncryptionMetadataInternalSeparator]; + return [metadataString dataUsingEncoding:NSUTF8StringEncoding]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACEncrypterPrivate.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACEncrypterPrivate.h new file mode 100644 index 0000000000..8e23792aff --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACEncrypterPrivate.h @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import +#import + +#import "MSACEncrypter.h" + +NS_ASSUME_NONNULL_BEGIN + +static int const kMSACEncryptionAlgorithm = kCCAlgorithmAES; +static NSString *const kMSACEncryptionAlgorithmName = @"AES"; +static NSString *const kMSACEncryptionCipherMode = @"CBC"; + +// One year. +static NSTimeInterval const kMSACEncryptionKeyLifetimeInSeconds = 365 * 24 * 60 * 60; +static int const kMSACEncryptionKeySize = kCCKeySizeAES256; +static NSString *const kMSACEncryptionKeyMetadataKey = @"EncryptionKeyMetadata"; +static NSString *const kMSACEncryptionKeyTagAlternate = @"kMSEncryptionKeyTagAlternate"; +static NSString *const kMSACEncryptionKeyTagOriginal = @"kMSEncryptionKeyTag"; + +// This separator is used for key metadata, as well as between metadata that is prepended to the cipher text. +static NSString *const kMSACEncryptionMetadataInternalSeparator = @"/"; + +// This separator is only used between the metadata and cipher text of the encryption result. +static NSString *const kMSACEncryptionMetadataSeparator = @":"; +static NSString *const kMSACEncryptionPaddingMode = @"PKCS7"; + +@interface MSACEncrypter () + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACHistoryInfo.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACHistoryInfo.h new file mode 100644 index 0000000000..8d7f510fce --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACHistoryInfo.h @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +@interface MSACHistoryInfo : NSObject + +/** + * Timestamp. + */ +@property(nonatomic) NSDate *timestamp; + +/** + * Initializes a new `MSACHistoryInfo` instance. + * + * @param timestamp Timestamp + */ +- (instancetype)initWithTimestamp:(NSDate *)timestamp; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACHistoryInfo.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACHistoryInfo.m new file mode 100644 index 0000000000..b7bb34a4bf --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACHistoryInfo.m @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACHistoryInfo.h" + +static NSString *const kMSACTimestampKey = @"timestampKey"; + +/** + * This class is a base class for maintaining history of data in time order. + */ +@implementation MSACHistoryInfo + +- (instancetype)initWithTimestamp:(NSDate *)timestamp { + self = [super init]; + if (self) { + _timestamp = timestamp; + } + return self; +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super init]; + if (self) { + _timestamp = [coder decodeObjectForKey:kMSACTimestampKey]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:self.timestamp forKey:kMSACTimestampKey]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACKeychainUtil.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACKeychainUtil.h new file mode 100644 index 0000000000..39fff64e3f --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACKeychainUtil.h @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Utility class for Keychain. + */ +@interface MSACKeychainUtil : NSObject + +/** + * Store a string to Keychain with the given key. + * + * @param string A string data to be placed in Keychain. + * @param key A unique key for the data. + * + * @return YES if stored successfully, NO otherwise. + */ ++ (BOOL)storeString:(NSString *)string forKey:(NSString *)key; + +/** + * Delete a string from Keychain with the given key. + * + * @param key A unique key for the data. + * + * @return A string data that was deleted. + */ ++ (NSString *_Nullable)deleteStringForKey:(NSString *)key; + +/** + * Get a string from Keychain with the given key. + * + * @param key A unique key for the data. + * + * @return A string data if exists. + */ ++ (NSString *_Nullable)stringForKey:(NSString *)key statusCode:(OSStatus *_Nullable)statusCode; + +/** + * Clear all keys and strings. + * + * @return YES if cleared successfully, NO otherwise. + */ ++ (BOOL)clear; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACKeychainUtil.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACKeychainUtil.m new file mode 100644 index 0000000000..9aaad089e9 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACKeychainUtil.m @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAppCenterInternal.h" +#import "MSACKeychainUtilPrivate.h" +#import "MSACLogger.h" +#import "MSACUtility.h" + +@implementation MSACKeychainUtil + +static NSString *AppCenterKeychainServiceName(NSString *suffix) { + static NSString *serviceName = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + serviceName = [NSString stringWithFormat:@"%@.%@", [MSAC_APP_MAIN_BUNDLE bundleIdentifier], suffix]; + }); + return serviceName; +} + ++ (BOOL)storeString:(NSString *)string forKey:(NSString *)key withServiceName:(NSString *)serviceName { + NSMutableDictionary *attributes = [MSACKeychainUtil generateItem:key withServiceName:serviceName]; + + // By default the keychain is not accessible when the device is locked, this will make it accessible after the first unlock. + attributes[(__bridge id)kSecAttrAccessible] = (__bridge id)(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly); + attributes[(__bridge id)kSecValueData] = [string dataUsingEncoding:NSUTF8StringEncoding]; + OSStatus status = [self addSecItem:attributes]; + + // Delete item if already exists. + if (status == errSecDuplicateItem) { + [self deleteSecItem:attributes]; + status = [self addSecItem:attributes]; + } + if (status == noErr) { + MSACLogVerbose([MSACAppCenter logTag], @"Stored a string with key='%@', service='%@' to keychain.", key, serviceName); + return YES; + } + MSACLogWarning([MSACAppCenter logTag], @"Failed to store item with key='%@', service='%@' to keychain. OS Status code %i", key, + serviceName, (int)status); + return NO; +} + ++ (BOOL)storeString:(NSString *)string forKey:(NSString *)key { + return [MSACKeychainUtil storeString:string forKey:key withServiceName:AppCenterKeychainServiceName(kMSACServiceSuffix)]; +} + ++ (NSString *)deleteStringForKey:(NSString *)key withServiceName:(NSString *)serviceName { + NSString *string = [MSACKeychainUtil stringForKey:key statusCode:nil]; + if (string) { + NSMutableDictionary *query = [MSACKeychainUtil generateItem:key withServiceName:serviceName]; + OSStatus status = [self deleteSecItem:query]; + if (status == noErr) { + MSACLogVerbose([MSACAppCenter logTag], @"Deleted a string with key='%@', service='%@' from keychain.", key, serviceName); + return string; + } + MSACLogWarning([MSACAppCenter logTag], @"Failed to delete item with key='%@', service='%@' from keychain. OS Status code %i", key, + serviceName, (int)status); + } + return nil; +} + ++ (NSString *)deleteStringForKey:(NSString *)key { + return [MSACKeychainUtil deleteStringForKey:key withServiceName:AppCenterKeychainServiceName(kMSACServiceSuffix)]; +} + ++ (NSString *)stringForKey:(NSString *)key withServiceName:(NSString *)serviceName statusCode:(OSStatus *)statusCode { + NSMutableDictionary *query = [MSACKeychainUtil generateItem:key withServiceName:serviceName]; + query[(__bridge id)kSecReturnData] = (__bridge id)kCFBooleanTrue; + query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne; + CFTypeRef result = nil; + + // Create placeholder to use in case given status code pointer is NULL. Can't put it inside the if statement or it can get deallocated too + // early. + OSStatus statusPlaceholder; + if (!statusCode) { + statusCode = &statusPlaceholder; + } + *statusCode = [self secItemCopyMatchingQuery:query result:&result]; + if (*statusCode == noErr) { + MSACLogVerbose([MSACAppCenter logTag], @"Retrieved a string with key='%@', service='%@' from keychain.", key, serviceName); + return [[NSString alloc] initWithData:(__bridge_transfer NSData *)result encoding:NSUTF8StringEncoding]; + } + MSACLogWarning([MSACAppCenter logTag], @"Failed to retrieve item with key='%@', service='%@' from keychain. OS Status code %i", key, + serviceName, (int)*statusCode); + return nil; +} + ++ (NSString *)stringForKey:(NSString *)key statusCode:(OSStatus *)statusCode { + return [MSACKeychainUtil stringForKey:key withServiceName:AppCenterKeychainServiceName(kMSACServiceSuffix) statusCode:statusCode]; +} + ++ (BOOL)clear { + NSMutableDictionary *query = [NSMutableDictionary new]; + query[(__bridge id)kSecClass] = (__bridge id)kSecClassGenericPassword; + query[(__bridge id)kSecAttrService] = AppCenterKeychainServiceName(kMSACServiceSuffix); + OSStatus status = [self deleteSecItem:query]; + return status == noErr; +} + ++ (NSMutableDictionary *)generateItem:(NSString *)key withServiceName:(NSString *)serviceName { + NSMutableDictionary *item = [NSMutableDictionary new]; + item[(__bridge id)kSecClass] = (__bridge id)kSecClassGenericPassword; + item[(__bridge id)kSecAttrService] = serviceName; + item[(__bridge id)kSecAttrAccount] = key; + return item; +} + +#pragma mark - Keychain wrapper + ++ (OSStatus)deleteSecItem:(NSMutableDictionary *)query { + return SecItemDelete((__bridge CFDictionaryRef)query); +} + ++ (OSStatus)addSecItem:(NSMutableDictionary *)attributes { + return SecItemAdd((__bridge CFDictionaryRef)attributes, nil); +} + ++ (OSStatus)secItemCopyMatchingQuery:(NSMutableDictionary *)query result:(CFTypeRef *__nullable CF_RETURNS_RETAINED)result { + return SecItemCopyMatching((__bridge CFDictionaryRef)query, result); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACKeychainUtilPrivate.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACKeychainUtilPrivate.h new file mode 100644 index 0000000000..71849aec74 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACKeychainUtilPrivate.h @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACKeychainUtil.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Keychain service name suffix. + */ +static NSString *const kMSACServiceSuffix = @"AppCenter"; + +/** + * Utility class for Keychain. + */ +@interface MSACKeychainUtil () + +/** + * Store a string to Keychain with the given key. + * + * @param string A string data to be placed in Keychain. + * @param key A unique key for the data. + * @param serviceName Keychain service name. + * + * @return YES if stored successfully, NO otherwise. + */ ++ (BOOL)storeString:(NSString *)string forKey:(NSString *)key withServiceName:(NSString *)serviceName; + +/** + * Delete a string from Keychain with the given key. + * + * @param key A unique key for the data. + * @param serviceName Keychain service name. + * + * @return A string data that was deleted. + */ ++ (NSString *_Nullable)deleteStringForKey:(NSString *)key withServiceName:(NSString *)serviceName; + +/** + * Get a string from Keychain with the given key. + * + * @param key A unique key for the data. + * @param serviceName Keychain service name. + * + * @return A string data if exists. + */ ++ (NSString *_Nullable)stringForKey:(NSString *)key withServiceName:(NSString *)serviceName statusCode:(OSStatus *_Nullable)statusCode; + +/** + * Deletes items that match a search query. + * + * @param query A dictionary that describes the search for the keychain items you want to delete. + * + * @return A result code for the deletion. + */ ++ (OSStatus)deleteSecItem:(NSMutableDictionary *)query; + +/** + * Adds one or more items to a keychain. + * + * @param attributes A dictionary that describes the item to add. + * + * @return A result code for the addition. + */ ++ (OSStatus)addSecItem:(NSMutableDictionary *)attributes; + +/** + * Returns one or more keychain items that match a search query, or copies attributes of specific keychain items. + * + * @param query A dictionary that describes the search. + * @param result A reference to the found items. + * + * @return A result code for the addition. + */ ++ (OSStatus)secItemCopyMatchingQuery:(NSMutableDictionary *)query result:(CFTypeRef *__nullable CF_RETURNS_RETAINED)result; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACOrderedDictionary.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACOrderedDictionary.h new file mode 100644 index 0000000000..d9470824a0 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACOrderedDictionary.h @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +/** + * A simple ordered dictionary implementation that orders its content only when inserting. + */ +@interface MSACOrderedDictionary : NSMutableDictionary + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACOrderedDictionary.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACOrderedDictionary.m new file mode 100644 index 0000000000..88b80b5ce1 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACOrderedDictionary.m @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACOrderedDictionaryPrivate.h" + +@implementation MSACOrderedDictionary + +/* + * Why are we implementing methods that are defined in our parent class? + * From Apple's documentation at + * https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/ClassCluster.html + * "You create and interact with instances of the cluster just as you would any other class. Behind the scenes, though, when you + * create an instance of the public class, the class returns an object of the appropriate subclass based on the creation method that + * you invoke. (You don’t, and can’t, choose the actual class of the instance.)" + */ + +- (instancetype)init { + if ((self = [super init])) { + _dictionary = [NSMutableDictionary new]; + _order = [NSMutableArray new]; + } + return self; +} + +- (instancetype)initWithCapacity:(NSUInteger)numItems { + self = [super init]; + if (self != nil) { + _dictionary = [[NSMutableDictionary alloc] initWithCapacity:numItems]; + _order = [[NSMutableArray alloc] initWithCapacity:numItems]; + } + return self; +} + +- (void)setObject:(id)anObject forKey:(id)aKey { + if (!self.dictionary[aKey]) { + [self.order addObject:aKey]; + } + self.dictionary[aKey] = anObject; +} + +- (NSEnumerator *)keyEnumerator { + return [self.order objectEnumerator]; +} + +- (id)objectForKey:(id)key { + return self.dictionary[key]; +} + +- (NSUInteger)count { + return [self.dictionary count]; +} + +- (void)removeAllObjects { + [self.dictionary removeAllObjects]; +} + +- (NSMutableDictionary *)mutableCopy { + MSACOrderedDictionary *copy = [MSACOrderedDictionary new]; + copy.dictionary = [self.dictionary mutableCopy]; + copy.order = [self.order mutableCopy]; + return copy; +} + +- (BOOL)isEqualToDictionary:(NSDictionary *)otherDictionary { + if (![(NSObject *)otherDictionary isKindOfClass:[MSACOrderedDictionary class]] || ! + [self.dictionary isEqualToDictionary:otherDictionary]) { + return NO; + } + return [self.order isEqualToArray:((MSACOrderedDictionary *)otherDictionary).order]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACOrderedDictionaryPrivate.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACOrderedDictionaryPrivate.h new file mode 100644 index 0000000000..00a6404113 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACOrderedDictionaryPrivate.h @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACOrderedDictionary.h" + +@interface MSACOrderedDictionary () + +/** + * An array containing the keys that are used to maintain the order. + */ +@property(nonatomic) NSMutableArray *order; + +/** + * The backing store for our ordered dictionary. + */ +@property(nonatomic) NSMutableDictionary *dictionary; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+Application.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+Application.h new file mode 100644 index 0000000000..369d50d55c --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+Application.h @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#if TARGET_OS_OSX +#import +#else +#import +#endif + +#import "MSACUtility.h" + +#if !TARGET_OS_OSX +#define MSAC_DEVICE [UIDevice currentDevice] +#endif + +/* + * Workaround for exporting symbols from category object files. + */ +extern NSString *MSACUtilityApplicationCategory; + +/** + * App states + */ +typedef NS_ENUM(NSInteger, MSACApplicationState) { + +/** + * Application is active. + */ +#if TARGET_OS_OSX + MSACApplicationStateActive, +#else + MSACApplicationStateActive = UIApplicationStateActive, +#endif + +/** + * Application is inactive. + */ +#if TARGET_OS_OSX + MSACApplicationStateInactive, +#else + MSACApplicationStateInactive = UIApplicationStateInactive, +#endif + +/** + * Application is in background. + */ +#if TARGET_OS_OSX + MSACApplicationStateBackground, +#else + MSACApplicationStateBackground = UIApplicationStateBackground, +#endif + + /** + * Application state can't be determined. + */ + MSACApplicationStateUnknown +}; + +typedef NS_ENUM(NSInteger, MSACOpenURLState) { + + /** + * Not being able to determine whether a URL has been processed or not. + */ + MSACOpenURLStateUnknown, + + /** + * A URL has been processed successfully. + */ + MSACOpenURLStateSucceed, + + /** + * A URL could not be processed. + */ + MSACOpenURLStateFailed +}; + +/** + * Utility class that is used throughout the SDK. + * Application part. + */ +@interface MSACUtility (Application) + +/** + * Get the Shared Application from either NSApplication (MacOS) or UIApplication. + * + * @return The shared application. + */ +#if TARGET_OS_OSX ++ (NSApplication *)sharedApp; +#else ++ (UIApplication *)sharedApp; +#endif + +/** + * Get the App Delegate. + * + * @return The delegate of the app object or nil if not accessible. + */ +#if TARGET_OS_OSX ++ (id)sharedAppDelegate; +#else ++ (id)sharedAppDelegate; +#endif + +/** + * Get current application state. + * + * @return Current state of the application or MSACApplicationStateUnknown while the state can't be determined. + * + * @discussion The application state may not be available anywhere. Application extensions doesn't have it for instance, in that case the + * MSACApplicationStateUnknown value is returned. + */ ++ (MSACApplicationState)applicationState; + +/** + * Attempt to open the URL asynchronously. + * + * @param url The URL to open. + * @param options A dictionary of options to use when opening the URL. + * @param completion The block to execute with the results. + */ ++ (void)sharedAppOpenUrl:(NSURL *)url + options:(NSDictionary *)options + completionHandler:(void (^)(MSACOpenURLState state))completion; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+Application.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+Application.m new file mode 100644 index 0000000000..4708981b42 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+Application.m @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACUtility+ApplicationPrivate.h" + +/* + * Workaround for exporting symbols from category object files. + */ +NSString *MSACUtilityApplicationCategory; + +@implementation MSACUtility (Application) + ++ (MSACApplicationState)applicationState { + + // App extensions must not access sharedApplication. + if (!MSAC_IS_APP_EXTENSION) { + return (MSACApplicationState)[[self class] sharedAppState]; + } + return MSACApplicationStateUnknown; +} + +#if TARGET_OS_OSX ++ (NSApplication *)sharedApp { + + // Compute selector at runtime for more discretion. + SEL sharedAppSel = NSSelectorFromString(@"sharedApplication"); + return ((NSApplication * (*)(id, SEL))[[NSApplication class] methodForSelector:sharedAppSel])([NSApplication class], sharedAppSel); +} +#else ++ (UIApplication *)sharedApp { + + // Compute selector at runtime for more discretion. + SEL sharedAppSel = NSSelectorFromString(@"sharedApplication"); + return ((UIApplication * (*)(id, SEL))[[UIApplication class] methodForSelector:sharedAppSel])([UIApplication class], sharedAppSel); +} +#endif + +#if TARGET_OS_OSX ++ (id)sharedAppDelegate { + return [self sharedApp].delegate; +} +#else ++ (id)sharedAppDelegate { + return [self sharedApp].delegate; +} +#endif + +#if TARGET_OS_OSX ++ (MSACApplicationState)sharedAppState { + + // UI API (isHidden) cannot be called from a background thread. + if ([NSThread isMainThread]) { + return [[MSACUtility sharedApp] isHidden] ? MSACApplicationStateBackground : MSACApplicationStateActive; + } + return MSACApplicationStateUnknown; +} +#else ++ (UIApplicationState)sharedAppState { + return [(NSNumber *)[[MSACUtility sharedApp] valueForKey:@"applicationState"] longValue]; +} +#endif + ++ (void)sharedAppOpenUrl:(NSURL *)url + options:(NSDictionary *)options + completionHandler:(void (^)(MSACOpenURLState state))completion { +#if TARGET_OS_OSX + (void)options; + + /* + * TODO: iOS SDK has an issue that openURL returns NO even though it was able to open a browser. Need to make sure openURL returns YES/NO + * on macOS properly. + */ + // Dispatch the open url call to the next loop to avoid freezing the App new instance start up. + dispatch_async(dispatch_get_main_queue(), ^{ + completion([[NSWorkspace sharedWorkspace] openURL:url]); + }); +#else + UIApplication *sharedApp = [[self class] sharedApp]; + + // FIXME: App extensions does support openURL through NSExtensionContest, we may use this somehow. + if (MSAC_IS_APP_EXTENSION || ![sharedApp canOpenURL:url]) { + if (completion) { + completion(MSACOpenURLStateFailed); + } + return; + } + + // Dispatch the open url call to the next loop to avoid freezing the App new instance start up. + dispatch_async(dispatch_get_main_queue(), ^{ + SEL selector = NSSelectorFromString(@"openURL:options:completionHandler:"); + if ([sharedApp respondsToSelector:selector]) { + id resourceUrl = url; + id urlOptions = options; + id completionHandler = ^(BOOL success) { + if (completion) { + completion(success ? MSACOpenURLStateSucceed : MSACOpenURLStateUnknown); + } + }; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[sharedApp methodSignatureForSelector:selector]]; + [invocation setSelector:selector]; + [invocation setTarget:sharedApp]; + [invocation setArgument:&resourceUrl atIndex:2]; + [invocation setArgument:&urlOptions atIndex:3]; + [invocation setArgument:&completionHandler atIndex:4]; + [invocation invoke]; + } else { + BOOL success = [sharedApp performSelector:@selector(openURL:) withObject:url]; + if (completion) { + completion(success ? MSACOpenURLStateSucceed : MSACOpenURLStateFailed); + } + } + }); +#endif +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+ApplicationPrivate.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+ApplicationPrivate.h new file mode 100644 index 0000000000..ca0834ffaf --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+ApplicationPrivate.h @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACUtility+Application.h" + +/** + * Utility class that is used throughout the SDK. + * Application private part. + */ +@interface MSACUtility (ApplicationPrivate) + +/** + * Get the shared app state. + * + * @return The shared app state. + * + * @discussion This method is exposed for testing purposes. The shared app state is resolved at runtime by this method which makes the + * UIApplication not mockable. This method is meant to be stubbed in tests to inject the desired application states. + */ +#if TARGET_OS_OSX ++ (MSACApplicationState)sharedAppState; +#else ++ (UIApplicationState)sharedAppState; +#endif + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+Date.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+Date.h new file mode 100644 index 0000000000..f2ddb91f0a --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+Date.h @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACUtility.h" + +/* + * Workaround for exporting symbols from category object files. + */ +extern NSString *MSACUtilityDateCategory; + +/** + * Utility class that is used throughout the SDK. + * Date part. + */ +@interface MSACUtility (Date) + +/** + * Return the current date (aka NOW) in ms. + * + * @return current time in ms with sub-ms precision if necessary + * + * @discussion Utility function that returns NOW as a NSTimeInterval but in ms instead of seconds with sub-ms precision. We're using + * NSTimeInterval here instead of long long because we might be interested in sub-millisecond precision which we keep with NSTimeInterval as + * NSTimeInterval is actually NSDouble. + */ ++ (NSTimeInterval)nowInMilliseconds; + +/** + * Convert a date object to an ISO 8601 formatted string. + * + * @return an ISO 8601 string representation of the date. + */ ++ (NSString *)dateToISO8601:(NSDate *)date; + +/** + * Convert an ISO 8601 formatted string to a date object. + * + * @return a date object. + */ ++ (NSDate *)dateFromISO8601:(NSString *)string; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+Date.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+Date.m new file mode 100644 index 0000000000..0ede311d9d --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+Date.m @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACUtility+Date.h" + +/** + * Cached date formatter instance. + */ +static NSDateFormatter *dateFormatter = nil; + +/* + * Workaround for exporting symbols from category object files. + */ +NSString *MSACUtilityDateCategory; + +@implementation MSACUtility (Date) + ++ (NSTimeInterval)nowInMilliseconds { + return ([[NSDate date] timeIntervalSince1970] * 1000); +} + ++ (NSString *)dateToISO8601:(NSDate *)date { + return [[MSACUtility ISO8601DateFormatter] stringFromDate:date]; +} + ++ (NSDate *)dateFromISO8601:(NSString *)string { + return [[MSACUtility ISO8601DateFormatter] dateFromString:string]; +} + ++ (NSDateFormatter *)ISO8601DateFormatter { + if (!dateFormatter) { + @synchronized(self) { + if (!dateFormatter) { + dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setLocale:[NSLocale systemLocale]]; + [dateFormatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]]; + [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"]; + } + } + } + return dateFormatter; +} + ++ (void)resetDateFormatterInstance { + dateFormatter = nil; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+Environment.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+Environment.h new file mode 100644 index 0000000000..929e025a7b --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+Environment.h @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACUtility.h" + +/* + * Workaround for exporting symbols from category object files. + */ +extern NSString *MSACUtilityEnvironmentCategory; + +/** + * App environment + */ +typedef NS_ENUM(NSInteger, MSACEnvironment) { + + /** + * App has been downloaded from the AppStore. + */ + MSACEnvironmentAppStore = 0, + + /** + * App has been downloaded from TestFlight. + */ + MSACEnvironmentTestFlight = 1, + + /** + * App has been installed by some other mechanism. + * This could be Ad-Hoc, Enterprise, etc. + */ + MSACEnvironmentOther = 99 +}; + +/** + * Utility class that is used throughout the SDK. + * Environment part. + */ +@interface MSACUtility (Environment) + +/** + * Detect the environment that the app is running in. + * + * @return the MSACEnvironment of the app. + */ ++ (MSACEnvironment)currentAppEnvironment; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+Environment.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+Environment.m new file mode 100644 index 0000000000..acd550b8cb --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+Environment.m @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACUtility+Environment.h" + +/* + * Workaround for exporting symbols from category object files. + */ +NSString *MSACUtilityEnvironmentCategory; + +@implementation MSACUtility (Environment) + ++ (MSACEnvironment)currentAppEnvironment { +#if TARGET_OS_SIMULATOR || TARGET_OS_OSX || TARGET_OS_MACCATALYST + return MSACEnvironmentOther; +#else + + // MobilePovision profiles are a clear indicator for Ad-Hoc distribution. + if ([self hasEmbeddedMobileProvision]) { + return MSACEnvironmentOther; + } + + /** + * TestFlight is only supported from iOS 8 onwards and as our deployment target is iOS 8, we don't have to do any checks for + * floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_6_1). + */ + if ([self isAppStoreReceiptSandbox]) { + return MSACEnvironmentTestFlight; + } + + return MSACEnvironmentAppStore; +#endif +} + ++ (BOOL)hasEmbeddedMobileProvision { + BOOL hasEmbeddedMobileProvision = !![[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision"]; + return hasEmbeddedMobileProvision; +} + ++ (BOOL)isAppStoreReceiptSandbox { +#if TARGET_OS_SIMULATOR + return NO; +#else + if (![NSBundle.mainBundle respondsToSelector:@selector(appStoreReceiptURL)]) { + return NO; + } + NSURL *appStoreReceiptURL = NSBundle.mainBundle.appStoreReceiptURL; + NSString *appStoreReceiptLastComponent = appStoreReceiptURL.lastPathComponent; + + BOOL isSandboxReceipt = [appStoreReceiptLastComponent isEqualToString:@"sandboxReceipt"]; + return isSandboxReceipt; +#endif +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+File.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+File.h new file mode 100644 index 0000000000..ce9b44aae0 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+File.h @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACUtility.h" + +/* + * Workaround for exporting symbols from category object files. + */ +extern NSString *MSACUtilityFileCategory; + +/** + * Utility class that is used throughout the SDK. + * File part. + */ +@interface MSACUtility (File) + +/** + * Creates a file inside the app center sdk's file directory, intermediate directories are also create if nonexistent. + * + * @param filePathComponent A string representing the path of the file to create. + * @param data The data to write to the file. + * @param atomically Flag to indicate atomic write or not. + * @param forceOverwrite Flag to make this method overwrite existing files. + * + * @return The URL of the file that was created. Necessary for e.g. crash buffer. + * + * @discussion SDK files should not be backed up in iCloud. Thus, iCloud backup is explicitely deactivated on every folder created. + */ ++ (NSURL *)createFileAtPathComponent:(NSString *)filePathComponent + withData:(NSData *)data + atomically:(BOOL)atomically + forceOverwrite:(BOOL)forceOverwrite; + +/** + * Removes the file or directory specified inside the app center sdk directory. + * + * @param itemPathComponent A string representing the path of the file to delete. + * + * @return YES if the item was removed successfully or if URL was nil. Returns NO if an error occurred. + */ ++ (BOOL)deleteItemForPathComponent:(NSString *)itemPathComponent; + +/** + * Creates a directory inside the app center sdk's file directory, intermediate directories are also created if nonexistent. + * + * @param directoryPathComponent A string representing the path of the directory to create. + * + * @return `YES` if the operation was successful or if the item already exists, otherwise `NO`. + * + * @discussion SDK files should not be backed up in iCloud. Thus, iCloud backup is explicitely deactivated on every folder created. + */ ++ (NSURL *)createDirectoryForPathComponent:(NSString *)directoryPathComponent; + +/** + * Load a data at a filePathComponent, e.g. load data at "/Crashes/foo.bar". + * + * @param filePathComponent A string representing the pathComponent of the file to read. + * + * @return The data of the file or `nil` if the file does not exist. + */ ++ (NSData *)loadDataForPathComponent:(NSString *)filePathComponent; + +/** + * Returns the NSURLs of the contents of a directory. + * + * @param directory A string representing the path of the directory to look for content. + * + * @return An array of NSURL* of each file or directory in a directory. + */ ++ (NSArray *)contentsOfDirectory:(NSString *)directory propertiesForKeys:(NSArray *)propertiesForKeys; + +/** + * Checks for existence of a path component. + * + * @param filePathComponent The path component to check. + * + * @return `YES` if a file or existence exists at the specified location. Otherwiese `NO`. + */ ++ (BOOL)fileExistsForPathComponent:(NSString *)filePathComponent; + +/** + * Removes a file at the given URL if it exists. + * + * @param fileURL The URL of the file to delete. + * + * @return A flag indicating success or fail. + */ ++ (BOOL)deleteFileAtURL:(NSURL *)fileURL; + +/** + * Get the full path for a component if it exists. + * + * @param filePathComponent A string representing the path component of the file or directory to check. + * + * @return The URL for the given path component or `nil`. + */ ++ (NSURL *)fullURLForPathComponent:(NSString *)filePathComponent; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+File.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+File.m new file mode 100644 index 0000000000..57ff02182d --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+File.m @@ -0,0 +1,210 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAppCenterInternal.h" +#import "MSACLogger.h" +#import "MSACUtility+File.h" + +/* + * Workaround for exporting symbols from category object files. + */ +NSString *MSACUtilityFileCategory; + +/** + * Bundle identifier, used for storage directories. + */ +static NSString *const kMSACAppCenterBundleIdentifier = @"com.microsoft.appcenter"; + +@implementation MSACUtility (File) + ++ (NSURL *)createFileAtPathComponent:(NSString *)filePathComponent + withData:(NSData *)data + atomically:(BOOL)atomically + forceOverwrite:(BOOL)forceOverwrite { + @synchronized(self) { + if (filePathComponent) { + NSURL *fileURL = [[self appCenterDirectoryURL] URLByAppendingPathComponent:filePathComponent]; + + // Check if item already exists. We need to check this as writeToURL:atomically: can override an existing file. + if (!forceOverwrite && [fileURL checkResourceIsReachableAndReturnError:nil]) { + return fileURL; + } + + // Create parent directories as needed. + NSURL *directoryURL = [fileURL URLByDeletingLastPathComponent]; + [self createDirectoryAtURL:directoryURL]; + + // Create the file. + NSData *theData = (data != nil) ? data : [NSData data]; + if ([theData writeToURL:fileURL atomically:atomically]) { + return fileURL; + } else { + MSACLogError([MSACAppCenter logTag], @"Couldn't create new file at path %@", fileURL); + } + } + return nil; + } +} + ++ (BOOL)deleteItemForPathComponent:(NSString *)itemPathComponent { + @synchronized(self) { + if (itemPathComponent) { + NSURL *itemURL = [[self appCenterDirectoryURL] URLByAppendingPathComponent:itemPathComponent]; + NSError *error = nil; + BOOL succeeded; + succeeded = [[NSFileManager defaultManager] removeItemAtURL:itemURL error:&error]; + if (error) { + MSACLogDebug([MSACAppCenter logTag], @"Couldn't remove item at %@: %@", itemURL, error.localizedDescription); + } + return succeeded; + } + return NO; + } +} + +// TODO: We should remove this and just expose the method taking a pathComponent. ++ (BOOL)deleteFileAtURL:(NSURL *)fileURL { + @synchronized(self) { + if (fileURL) { + + /* + * No need to check existence of directory as checkResourceIsReachableAndReturnError: is synchronous. From it's docs: "If your app + * must perform operations on the file, such as opening it or copying resource properties, it is more efficient to attempt the + * operation and handle any failure that may occur." + */ + NSError *error = nil; + BOOL succeeded; + succeeded = [[NSFileManager defaultManager] removeItemAtURL:fileURL error:&error]; + if (error) { + MSACLogDebug([MSACAppCenter logTag], @"Couldn't remove item at %@: %@", fileURL, error.localizedDescription); + } + return succeeded; + } + return NO; + } +} + ++ (NSURL *)createDirectoryForPathComponent:(NSString *)directoryPathComponent { + @synchronized(self) { + if (directoryPathComponent) { + NSURL *subDirURL = [[self appCenterDirectoryURL] URLByAppendingPathComponent:directoryPathComponent]; + BOOL success = [self createDirectoryAtURL:subDirURL]; + return success ? subDirURL : nil; + } + return nil; + } +} + ++ (NSData *)loadDataForPathComponent:(NSString *)filePathComponent { + @synchronized(self) { + if (filePathComponent) { + NSURL *fileURL = [[self appCenterDirectoryURL] URLByAppendingPathComponent:filePathComponent]; + return [NSData dataWithContentsOfURL:fileURL]; + } + return nil; + } +} + +// TODO candidate for refactoring. Should return pathComponents and not full URLs. Has big impact on crashes logic. ++ (NSArray *)contentsOfDirectory:(NSString *)directory propertiesForKeys:(NSArray *)propertiesForKeys { + @synchronized(self) { + if (directory && directory.length > 0) { + NSFileManager *fileManager = [NSFileManager new]; + NSError *error = nil; + NSURL *dirURL = [[self appCenterDirectoryURL] URLByAppendingPathComponent:directory isDirectory:YES]; + NSArray *files = [fileManager contentsOfDirectoryAtURL:dirURL + includingPropertiesForKeys:propertiesForKeys + options:(NSDirectoryEnumerationOptions)0 + error:&error]; + if (!files) { + MSACLogDebug([MSACAppCenter logTag], @"Couldn't get files in the directory \"%@\": %@", directory, error.localizedDescription); + } + return files; + } + return nil; + } +} + ++ (BOOL)fileExistsForPathComponent:(NSString *)filePathComponent { + { + NSURL *fileURL = [[self appCenterDirectoryURL] URLByAppendingPathComponent:filePathComponent]; + return [fileURL checkResourceIsReachableAndReturnError:nil]; + } +} + ++ (NSURL *)fullURLForPathComponent:(NSString *)filePathComponent { + { + if (filePathComponent) { + return [[self appCenterDirectoryURL] URLByAppendingPathComponent:filePathComponent]; + } + return nil; + } +} + +#pragma mark - Private methods. + ++ (NSURL *)appCenterDirectoryURL { + static NSURL *dirURL = nil; + static dispatch_once_t predFilesDir; + dispatch_once(&predFilesDir, ^{ + +#if TARGET_OS_TV + NSSearchPathDirectory directory = NSCachesDirectory; +#else + NSSearchPathDirectory directory = NSApplicationSupportDirectory; +#endif + + NSFileManager *fileManager = [[NSFileManager alloc] init]; + NSArray *urls = [fileManager URLsForDirectory:directory inDomains:NSUserDomainMask]; + NSURL *baseDirUrl = [urls objectAtIndex:0]; + +#if TARGET_OS_OSX || TARGET_OS_MACCATALYST + + // Use the application's bundle identifier for macOS to make sure to use separate directories for each app. + NSString *bundleIdentifier = [NSString stringWithFormat:@"%@/", [MSAC_APP_MAIN_BUNDLE bundleIdentifier]]; + dirURL = [[baseDirUrl URLByAppendingPathComponent:bundleIdentifier] URLByAppendingPathComponent:kMSACAppCenterBundleIdentifier]; +#else + dirURL = [baseDirUrl URLByAppendingPathComponent:kMSACAppCenterBundleIdentifier]; +#endif + [self createDirectoryAtURL:dirURL]; + }); + + return dirURL; +} + ++ (BOOL)createDirectoryAtURL:(NSURL *)fullDirURL { + if (fullDirURL) { + + /* + * No need to check existence of directory: + * + * 1. createDirectoryAtURL:withIntermediateDirectories:attributes:error: returns YES if the directory already exists. + * 2. checkResourceIsReachableAndReturnError: is synchronous. From it's docs: "If your app must perform operations on the file, such as + * opening it or copying resource properties, it is more efficient to attempt the operation and handle any failure that may occur." + */ + NSError *error = nil; + if ([[NSFileManager defaultManager] createDirectoryAtURL:fullDirURL withIntermediateDirectories:YES attributes:nil error:&error]) { + [self disableBackupForDirectoryURL:fullDirURL]; + return YES; + } else { + MSACLogError([MSACAppCenter logTag], @"Couldn't create directory at %@: %@", fullDirURL, error.localizedDescription); + } + } + return NO; +} + ++ (BOOL)disableBackupForDirectoryURL:(nonnull NSURL *)directoryURL { + NSError *error = nil; + + // SDK files shouldn't be backed up in iCloud. + if (!directoryURL || ![directoryURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:&error]) { + MSACLogError([MSACAppCenter logTag], @"Error excluding %@ from iCloud backup %@", directoryURL, error.localizedDescription); + return NO; + } else { + return YES; + } +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+PropertyValidation.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+PropertyValidation.h new file mode 100644 index 0000000000..b39b94911f --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+PropertyValidation.h @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACUtility.h" + +NS_ASSUME_NONNULL_BEGIN + +/* + * Workaround for exporting symbols from category object files. + */ +extern NSString *MSACUtilityPropertyValidationCategory; + +/** + * Utility class that is used throughout the SDK. + * Property validation part. + */ +@interface MSACUtility (PropertyValidation) + ++ (NSDictionary *)validateProperties:(NSDictionary *)properties + forLogName:(NSString *)logName + type:(NSString *)logType; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+PropertyValidation.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+PropertyValidation.m new file mode 100644 index 0000000000..d6e614e5bb --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+PropertyValidation.m @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACUtility+PropertyValidation.h" + +#import "MSACAppCenterInternal.h" +#import "MSACConstants+Internal.h" +#import "MSACLogger.h" + +/* + * Workaround for exporting symbols from category object files. + */ +NSString *MSACUtilityPropertyValidationCategory; + +@implementation NSObject (MSACUtility_PropertyValidation) + ++ (NSDictionary *)validateProperties:(NSDictionary *)properties + forLogName:(NSString *)logName + type:(NSString *)logType { + NSMutableDictionary *validProperties = [NSMutableDictionary new]; + for (id key in properties) { + + // Don't send more properties than we can. + if ([validProperties count] >= kMSACMaxPropertiesPerLog) { + MSACLogWarning([MSACAppCenter logTag], @"%@ '%@' : properties cannot contain more than %d items. Skipping other properties.", logType, + logName, kMSACMaxPropertiesPerLog); + break; + } + if (![(NSObject *)key isKindOfClass:[NSString class]] || ![properties[key] isKindOfClass:[NSString class]]) { + continue; + } + + // Validate key. + NSString *strKey = key; + if ([strKey length] < kMSACMinPropertyKeyLength) { + MSACLogWarning([MSACAppCenter logTag], @"%@ '%@' : a property key cannot be null or empty. Property will be skipped.", logType, + logName); + continue; + } + if ([strKey length] > kMSACMaxPropertyKeyLength) { + MSACLogWarning([MSACAppCenter logTag], + @"%@ '%@' : property %@ : property key length cannot be longer than %d characters. Property key will be truncated.", + logType, logName, strKey, kMSACMaxPropertyKeyLength); + strKey = [strKey substringToIndex:kMSACMaxPropertyKeyLength]; + } + + // Validate value. + NSString *value = properties[key]; + if ([value length] > kMSACMaxPropertyValueLength) { + MSACLogWarning([MSACAppCenter logTag], + @"%@ '%@' : property '%@' : property value cannot be longer than %d characters. Property value will be truncated.", + logType, logName, strKey, kMSACMaxPropertyValueLength); + value = [value substringToIndex:kMSACMaxPropertyValueLength]; + } + + // Save valid properties. + [validProperties setObject:value forKey:strKey]; + } + return validProperties; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+StringFormatting.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+StringFormatting.h new file mode 100644 index 0000000000..d5a52cbd39 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+StringFormatting.h @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACUtility.h" + +NS_ASSUME_NONNULL_BEGIN + +/* + * Workaround for exporting symbols from category object files. + */ +extern NSString *MSACUtilityStringFormattingCategory; + +/** + * Utility class that is used throughout the SDK. + * StringFormatting part. + */ +@interface MSACUtility (StringFormatting) + +/** + * Create SHA256 of a string. + * + * @param string A string. + * + * @returns The SHA256 of given string. + */ ++ (NSString *)sha256:(NSString *)string; + +/** + * Extract app secret from a string. + * + * @param string A string. + * + * @returns The app secret or nil if none was found. + */ ++ (NSString *)appSecretFrom:(nullable NSString *)string; + +/** + * Extract transmission target token from a string. + * + * @param string A string. + * + * @returns The tenant id or nil if none was found. + */ ++ (NSString *)transmissionTargetTokenFrom:(nullable NSString *)string; + +/** + * Extract iKey from a transmission target token string. + * + * @param token The transmission target token as a string. + * + * @returns The iKey or nil if none was found. + */ ++ (nullable NSString *)iKeyFromTargetToken:(nullable NSString *)token; + +/** + * Extract target key from a transmission target token string. + * + * @param token The transmission target token as a string. + * + * @returns The target key or nil if none was found. + */ ++ (nullable NSString *)targetKeyFromTargetToken:(NSString *)token; + +/** + * Pretty print json data payload. + * + * @param data A data payload. + * + * @returns The pretty printed json data payload. + */ ++ (nullable NSString *)prettyPrintJson:(nullable NSData *)data; + +/** + * Hide sensitive values included in string. + * + * @param unObfuscatedString String to obfuscate. + * @param pattern Pattern to search for. + * @param aTemplate Template applied to any found pattern. + * + * @return Obfuscated string or nil if obfuscation failed. + */ ++ (nullable NSString *)obfuscateString:(nullable NSString *)unObfuscatedString + searchingForPattern:(NSString *)pattern + toReplaceWithTemplate:(NSString *)aTemplate; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+StringFormatting.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+StringFormatting.m new file mode 100644 index 0000000000..c28195bcbe --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility+StringFormatting.m @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAppCenterInternal.h" +#import "MSACLogger.h" +#import "MSACUtility+StringFormatting.h" + +/* + * Workaround for exporting symbols from category object files. + */ +NSString *MSACUtilityStringFormattingCategory; + +/* + * We support the following formats: + * target=<..> + * appsecret=<..> + * target=<..>;appsecret=<..> + * ios=<..>;macos=<..> + */ + +static NSString *kMSACTransmissionTargetKey = @"target="; +static NSString *kMSACAppSecretKey = @"appsecret="; +static NSString *kMSACSecretSeparator = @"="; + +#if TARGET_OS_OSX || TARGET_OS_MACCATALYST +static NSString *kMSACAppSecretOSKey = @"macos="; +#elif TARGET_OS_IOS +static NSString *kMSACAppSecretOSKey = @"ios="; +#elif TARGET_OS_TV +static NSString *kMSACAppSecretOSKey = @"appsecret="; +#endif + +@implementation NSObject (MSACUtility_StringFormatting) + ++ (NSString *)sha256:(NSString *)string { + + // Hash string with SHA256. + const char *encodedString = [string cStringUsingEncoding:NSASCIIStringEncoding]; + unsigned char hashedData[CC_SHA256_DIGEST_LENGTH]; + CC_SHA256(encodedString, (CC_LONG)strlen(encodedString), hashedData); + + // Convert hashed data to NSString. + NSData *data = [NSData dataWithBytes:hashedData length:sizeof(hashedData)]; + NSMutableString *stringBuffer = [NSMutableString stringWithCapacity:([data length] * 2)]; + const unsigned char *dataBuffer = [data bytes]; + for (NSUInteger i = 0; i < [data length]; i++) { + [stringBuffer appendFormat:@"%02x", dataBuffer[i]]; + } + return [stringBuffer copy]; +} + ++ (NSString *)appSecretFrom:(NSString *)string { + NSArray *components = [string componentsSeparatedByString:@";"]; + if (components == nil || components.count == 0) { + return nil; + } else { + for (NSString *component in components) { + BOOL transmissionTokenIsNotPresent = [component rangeOfString:kMSACTransmissionTargetKey].location == NSNotFound; + + // Component is app secret, return the component. Check for length > 0 as "foo;" will be parsed as 2 components. + if (transmissionTokenIsNotPresent && component.length > 0) { + NSString *secretString = @""; + if ([string rangeOfString:kMSACAppSecretOSKey].location != NSNotFound) { + + // If we know the whole string contains OSKey somewhere, we start looking for it. + if ([component rangeOfString:kMSACAppSecretOSKey].location != NSNotFound) { + secretString = [component stringByReplacingOccurrencesOfString:kMSACAppSecretOSKey withString:@""]; + } + } else { + + // If the whole string does not contain OSKey, we either use its value + // or search for "appsecret" components. + if ([component rangeOfString:kMSACAppSecretKey].location == NSNotFound && + [component rangeOfString:kMSACSecretSeparator].location == NSNotFound) { + + // Make sure the string is "clean" and without keys at this point. + secretString = component; + } else { + secretString = [component stringByReplacingOccurrencesOfString:kMSACAppSecretKey withString:@""]; + } + } + + // Check for string length to avoid returning empty string. + if ((secretString != nil) && (secretString.length > 0)) { + return secretString; + } + } + } + + // String does not contain an app secret. + return nil; + } +} + ++ (NSString *)transmissionTargetTokenFrom:(NSString *)string { + NSArray *components = [string componentsSeparatedByString:@";"]; + if (components == nil || components.count == 0) { + return nil; + } else { + for (NSString *component in components) { + + // Component is transmission target token, return the component. + if (([component rangeOfString:kMSACTransmissionTargetKey].location != NSNotFound) && (component.length > 0)) { + NSString *transmissionTarget = [component stringByReplacingOccurrencesOfString:kMSACTransmissionTargetKey withString:@""]; + + // Check for string length to avoid returning empty string. + if (transmissionTarget.length > 0) { + return transmissionTarget; + } + } + } + + // String does not contain a transmission target token. + return nil; + } +} + ++ (nullable NSString *)iKeyFromTargetToken:(NSString *)token { + NSString *targetKey = [self targetKeyFromTargetToken:token]; + return targetKey.length ? [NSString stringWithFormat:@"o:%@", targetKey] : nil; +} + ++ (nullable NSString *)targetKeyFromTargetToken:(NSString *)token { + NSString *targetKey = [token componentsSeparatedByString:@"-"][0]; + return targetKey.length ? targetKey : nil; +} + ++ (nullable NSString *)prettyPrintJson:(nullable NSData *)data { + if (!data) { + return nil; + } + + // Error instance for JSON parsing. Trying to format json for log. Don't need to log json error here. + NSError *jsonError = nil; + NSString *result = nil; + id dictionary = [NSJSONSerialization JSONObjectWithData:(NSData *)data options:NSJSONReadingMutableContainers error:&jsonError]; + if (jsonError) { + result = [[NSString alloc] initWithData:(NSData *)data encoding:NSUTF8StringEncoding]; + } else { + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dictionary options:NSJSONWritingPrettyPrinted error:&jsonError]; + if (!jsonData || jsonError) { + result = [[NSString alloc] initWithData:(NSData *)data encoding:NSUTF8StringEncoding]; + } else { + + // NSJSONSerialization escapes paths by default so we replace them. + result = [[[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding] stringByReplacingOccurrencesOfString:@"\\/" + withString:@"/"]; + } + } + return result; +} + +- (NSString *)obfuscateString:(NSString *)unObfuscatedString + searchingForPattern:(NSString *)pattern + toReplaceWithTemplate:(NSString *)aTemplate { + NSString *obfuscatedString; + NSError *error = nil; + if (unObfuscatedString) { + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern + options:NSRegularExpressionCaseInsensitive + error:&error]; + if (!regex) { + MSACLogError([MSACAppCenter logTag], @"Couldn't create regular expression with pattern\"%@\": %@", pattern, + error.localizedDescription); + return nil; + } + obfuscatedString = [regex stringByReplacingMatchesInString:unObfuscatedString + options:0 + range:NSMakeRange(0, [unObfuscatedString length]) + withTemplate:aTemplate]; + } + return obfuscatedString; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility.h new file mode 100644 index 0000000000..d26c14c6ea --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility.h @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAppCenterUserDefaults.h" + +#if TARGET_OS_MACCATALYST +#define APP_CENTER_C_NAME "appcenter.maccatalyst" +#elif TARGET_OS_IOS +#define APP_CENTER_C_NAME "appcenter.ios" +#elif TARGET_OS_OSX +#define APP_CENTER_C_NAME "appcenter.macos" +#elif TARGET_OS_TV +#define APP_CENTER_C_NAME "appcenter.tvos" +#endif + +#define MSAC_APP_CENTER_USER_DEFAULTS [MSACAppCenterUserDefaults shared] +#define MSAC_NOTIFICATION_CENTER [NSNotificationCenter defaultCenter] +#define MSAC_UUID_STRING [[NSUUID UUID] UUIDString] +#define MSAC_UUID_FROM_STRING(uuidString) [[NSUUID alloc] initWithUUIDString:uuidString] +#define MSAC_LOCALE [NSLocale currentLocale] +#define MSAC_CLASS_NAME_WITHOUT_PREFIX [NSStringFromClass([self class]) substringFromIndex:4] +#define MSAC_IS_APP_EXTENSION ([[[NSBundle mainBundle] executablePath] rangeOfString:@".appex/"].length > 0) +#define MSAC_APP_MAIN_BUNDLE [NSBundle mainBundle] + +/** + * Utility class that is used throughout the SDK. + * Basic part. + */ +@interface MSACUtility : NSObject + +/** + * Get the name of AppCenter SDK. + */ ++ (NSString *)sdkName; + +/** + * Get the current version of AppCenter SDK. + */ ++ (NSString *)sdkVersion; + +/** + * Unarchive data. + * + * @param data The data for unarchiving in NSData type. + * + * @return The unarchived data as an NSObject. + */ ++ (NSObject *)unarchiveKeyedData:(NSData *)data; + +/** + * Archive data. + * + * @param data The data for archiving. + * + * @return The archived data as an NSData. + */ ++ (NSData *)archiveKeyedData:(id)data; + +/** + * Add models for migration. + * + * @param data Dictionary for migration classes, where key - old class name, value - new class type. + * + */ ++ (void)addMigrationClasses:(NSDictionary *)data; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility.m new file mode 100644 index 0000000000..ea33c563b6 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Util/MSACUtility.m @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAppCenterInternal.h" +#import "MSACLoggerInternal.h" +#import "MSACUtility+Application.h" +#import "MSACUtility+Date.h" +#import "MSACUtility+Environment.h" +#import "MSACUtility+File.h" +#import "MSACUtility+PropertyValidation.h" +#import "MSACUtility+StringFormatting.h" + +// SDK versioning struct. Needs to be big enough to hold the info. +typedef struct { + uint8_t info_version; + const char ms_name[32]; + const char ms_version[32]; + const char ms_build[32]; +} ms_info_t; + +// SDK versioning. +static ms_info_t appcenter_library_info __attribute__((section("__TEXT,__ms_ios,regular,no_dead_strip"))) = { + .info_version = 1, .ms_name = APP_CENTER_C_NAME, .ms_version = APP_CENTER_C_VERSION, .ms_build = APP_CENTER_C_BUILD}; + +@implementation MSACUtility + +/** + * Dictionary for migration classes, where key - old class name, value - new class type. + */ +static NSMutableDictionary *targetClasses; + +/** + * @discussion Workaround for exporting symbols from category object files. See article + * https://medium.com/ios-os-x-development/categories-in-static-libraries-78e41f8ddb96#.aedfl1kl0 + */ +__attribute__((used)) static void importCategories() { + [NSString stringWithFormat:@"%@ %@ %@ %@ %@ %@", MSACUtilityApplicationCategory, MSACUtilityEnvironmentCategory, MSACUtilityDateCategory, + MSACUtilityStringFormattingCategory, MSACUtilityFileCategory, MSACUtilityPropertyValidationCategory]; +} + ++ (NSString *)sdkName { + return [NSString stringWithUTF8String:appcenter_library_info.ms_name]; +} + ++ (NSString *)sdkVersion { + return [NSString stringWithUTF8String:appcenter_library_info.ms_version]; +} + ++ (NSObject *)unarchiveKeyedData:(NSData *)data { + if (!data) { + return nil; + } + NSError *error; + NSObject *unarchivedData; + NSException *exception; + @try { + if (@available(iOS 11.0, macOS 10.13, watchOS 4.0, *)) { + NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingFromData:data error:&error]; + for (NSString *key in targetClasses) { + [unarchiver setClass:targetClasses[key] forClassName:key]; + } + unarchiver.requiresSecureCoding = NO; + unarchivedData = [unarchiver decodeTopLevelObjectForKey:NSKeyedArchiveRootObjectKey error:&error]; + } else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated" + unarchivedData = [NSKeyedUnarchiver unarchiveObjectWithData:data]; +#pragma clang diagnostic pop + } + } @catch (NSException *ex) { + exception = ex; + } + if (!unarchivedData || exception) { + + // Unarchiving process failed. + MSACLogError([MSACAppCenter logTag], @"Unarchiving NSData failed with error: %@", + exception ? exception.reason : error.localizedDescription); + } + return unarchivedData; +} + ++ (NSData *)archiveKeyedData:(id)data { + if (!data) { + return nil; + } + NSError *error; + NSData *archivedData; + NSException *exception; + @try { + if (@available(iOS 11.0, macOS 10.13, watchOS 4.0, *)) { + archivedData = [NSKeyedArchiver archivedDataWithRootObject:data requiringSecureCoding:NO error:&error]; + } else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated" + archivedData = [NSKeyedArchiver archivedDataWithRootObject:data]; +#pragma clang diagnostic pop + } + } @catch (NSException *ex) { + exception = ex; + } + if (!archivedData || exception) { + + // Unarchiving process failed. + MSACLogError([MSACAppCenter logTag], @"Archiving NSData failed with error: %@", + exception ? exception.reason : error.localizedDescription); + } + return archivedData; +} + ++ (void)addMigrationClasses:(NSDictionary *)data { + if (targetClasses == nil) { + targetClasses = [NSMutableDictionary new]; + } + [targetClasses addEntriesFromDictionary:data]; +} +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Vendor/Reachability/MSAC_Reachability.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Vendor/Reachability/MSAC_Reachability.h new file mode 100644 index 0000000000..95a77b5a7f --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Vendor/Reachability/MSAC_Reachability.h @@ -0,0 +1,56 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Basic demonstration of how to use the SystemConfiguration Reachablity APIs. + */ + +#import +#import +#import + +typedef enum : NSInteger { + NotReachable = 0, + ReachableViaWiFi, + ReachableViaWWAN +} NetworkStatus; + +#pragma mark IPv6 Support +// Reachability fully support IPv6. For full details, see ReadMe.md. + +extern NSString *kMSACReachabilityChangedNotification; + +@interface MSAC_Reachability : NSObject + +/*! + * Use to check the reachability of a given host name. + */ ++ (instancetype)reachabilityWithHostName:(NSString *)hostName; + +/*! + * Use to check the reachability of a given IP address. + */ ++ (instancetype)reachabilityWithAddress:(const struct sockaddr *)hostAddress; + +/*! + * Checks whether the default route is available. Should be used by applications + * that do not connect to a particular host. + */ ++ (instancetype)reachabilityForInternetConnection; + +/*! + * Start listening for reachability notifications on the current run loop. + */ +- (void)startNotifier; +- (void)stopNotifier; + +- (NetworkStatus)currentReachabilityStatus; + +/*! + * WWAN may be available, but not active until a connection has been + * established. WiFi may require a connection for VPN on Demand. + */ +- (BOOL)connectionRequired; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Vendor/Reachability/MSAC_Reachability.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Vendor/Reachability/MSAC_Reachability.m new file mode 100644 index 0000000000..0a8f593de9 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Internals/Vendor/Reachability/MSAC_Reachability.m @@ -0,0 +1,235 @@ +/* + Copyright (C) 2016 Apple Inc. All Rights Reserved. + See LICENSE.txt for this sample’s licensing information + + Abstract: + Basic demonstration of how to use the SystemConfiguration Reachablity APIs. + */ + +#import +#import +#import "MSACDispatcherUtil.h" + +#import "MSAC_Reachability.h" + +#pragma mark IPv6 Support + +NSString *kMSACReachabilityChangedNotification = + @"kMSNetworkReachabilityChangedNotification"; + +#pragma mark - Supporting functions + +#define kShouldPrintReachabilityFlags 0 + +static void PrintReachabilityFlags(__unused SCNetworkReachabilityFlags flags, + __unused const char *comment) { +#if kShouldPrintReachabilityFlags + + NSLog(@"Reachability Flag Status: %c%c %c%c%c%c%c%c%c %s\n", + (flags & kSCNetworkReachabilityFlagsIsWWAN) ? 'W' : '-', + (flags & kSCNetworkReachabilityFlagsReachable) ? 'R' : '-', + (flags & kSCNetworkReachabilityFlagsTransientConnection) ? 't' : '-', + (flags & kSCNetworkReachabilityFlagsConnectionRequired) ? 'c' : '-', + (flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) ? 'C' : '-', + (flags & kSCNetworkReachabilityFlagsInterventionRequired) ? 'i' : '-', + (flags & kSCNetworkReachabilityFlagsConnectionOnDemand) ? 'D' : '-', + (flags & kSCNetworkReachabilityFlagsIsLocalAddress) ? 'l' : '-', + (flags & kSCNetworkReachabilityFlagsIsDirect) ? 'd' : '-', comment); +#endif +} + +static void ReachabilityCallback(SCNetworkReachabilityRef target, + SCNetworkReachabilityFlags flags, void *info) { +#pragma unused(target, flags) + NSCAssert(info != NULL, @"info was NULL in ReachabilityCallback"); + NSCAssert([(__bridge NSObject *)info isKindOfClass:[MSAC_Reachability class]], + @"info was wrong class in ReachabilityCallback"); + + MSAC_Reachability *noteObject = (__bridge MSAC_Reachability *)info; + // Post a notification to notify the client that the network reachability + // changed. + [[NSNotificationCenter defaultCenter] + postNotificationName:kMSACReachabilityChangedNotification + object:noteObject]; +} + +#pragma mark - Reachability extension + +@interface MSAC_Reachability () + +@property(nonatomic) SCNetworkReachabilityRef reachabilityRef; + +@end + +#pragma mark - Reachability implementation + +/* + * Starting and stopping notifier for reachability + * instance are enforced to run in main thread. MSAC_Reachability is not + * thread-safe so stopNotifier doesn't properly unschedule jobs from the loop + * when it is called from a different thread, and this generates unexpected + * crashes that are caused by accessing a disposed instance especially when + * reachability is used for local variables. + */ +@implementation MSAC_Reachability { +} + +// It's based on Apple's sample code. Disable an one warning type for this +// function +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnullable-to-nonnull-conversion" + ++ (instancetype)reachabilityWithHostName:(NSString *)hostName { + MSAC_Reachability *returnValue = NULL; + SCNetworkReachabilityRef reachability = + SCNetworkReachabilityCreateWithName(NULL, [hostName UTF8String]); + if (reachability != NULL) { + returnValue = [[MSAC_Reachability alloc] init]; + if (returnValue != NULL) { + returnValue.reachabilityRef = reachability; + } else { + CFRelease(reachability); + } + } + return returnValue; +} + +#pragma clang diagnostic pop + ++ (instancetype)reachabilityWithAddress:(const struct sockaddr *)hostAddress { + MSAC_Reachability *returnValue = NULL; + SCNetworkReachabilityRef reachability = + SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, hostAddress); + if (reachability != NULL) { + returnValue = [[MSAC_Reachability alloc] init]; + if (returnValue != NULL) { + returnValue.reachabilityRef = reachability; + } else { + CFRelease(reachability); + } + } + return returnValue; +} + ++ (instancetype)reachabilityForInternetConnection { + struct sockaddr_in zeroAddress; + bzero(&zeroAddress, sizeof(zeroAddress)); + zeroAddress.sin_len = sizeof(zeroAddress); + zeroAddress.sin_family = AF_INET; + + return [self reachabilityWithAddress:(const struct sockaddr *)&zeroAddress]; +} + +#pragma mark - Start and stop notifier + +- (void)startNotifier { + [MSACDispatcherUtil performBlockOnMainThread:^{ + SCNetworkReachabilityContext context = {0, (__bridge void *)(self), NULL, + NULL, NULL}; + if (SCNetworkReachabilitySetCallback(self.reachabilityRef, + ReachabilityCallback, &context)) { + if (SCNetworkReachabilityScheduleWithRunLoop(self.reachabilityRef, + CFRunLoopGetCurrent(), + kCFRunLoopDefaultMode)) { + [[NSNotificationCenter defaultCenter] postNotificationName:kMSACReachabilityChangedNotification + object:self]; + } + } + }]; +} + +- (void)stopNotifier { + [MSACDispatcherUtil performBlockOnMainThread:^{ + if (self.reachabilityRef != NULL) { + SCNetworkReachabilityUnscheduleFromRunLoop( + self.reachabilityRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); + } + }]; +} + +- (void)dealloc { + __block SCNetworkReachabilityRef reachabilityRef = self.reachabilityRef; + if (reachabilityRef != NULL) { + [MSACDispatcherUtil performBlockOnMainThread:^{ + SCNetworkReachabilityUnscheduleFromRunLoop(reachabilityRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); + CFRelease(reachabilityRef); + }]; + } +} + +#pragma mark - Network Flag Handling + +- (NetworkStatus)networkStatusForFlags:(SCNetworkReachabilityFlags)flags { + PrintReachabilityFlags(flags, "networkStatusForFlags"); + if ((flags & kSCNetworkReachabilityFlagsReachable) == 0) { + // The target host is not reachable. + return NotReachable; + } + + NetworkStatus returnValue = NotReachable; + + if ((flags & kSCNetworkReachabilityFlagsConnectionRequired) == 0) { + + // If the target host is reachable and no connection is required then we'll + // assume (for now) that you're on Wi-Fi... + returnValue = ReachableViaWiFi; + } + + if ((((flags & kSCNetworkReachabilityFlagsConnectionOnDemand) != 0) || + (flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) != 0)) { + /* + ... and the connection is on-demand (or on-traffic) if the calling + application is using the CFSocketStream or higher APIs... + */ + + if ((flags & kSCNetworkReachabilityFlagsInterventionRequired) == 0) { + /* + ... and no [user] intervention is needed... + */ + returnValue = ReachableViaWiFi; + } + } + +/* + * This flag indicates that the specified nodename or address can be reached via + * an EDGE, GPRS, or other "cell" connection. Not available on macOS. + */ +#if !TARGET_OS_OSX && !TARGET_OS_MACCATALYST + if ((flags & kSCNetworkReachabilityFlagsIsWWAN) == + kSCNetworkReachabilityFlagsIsWWAN) { + + // ... but WWAN connections are OK if the calling application is using the + // CFNetwork APIs. + returnValue = ReachableViaWWAN; + } +#endif + + return returnValue; +} + +- (BOOL)connectionRequired { + NSAssert(self.reachabilityRef != NULL, + @"connectionRequired called with NULL reachabilityRef"); + SCNetworkReachabilityFlags flags; + + if (SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) { + return (flags & kSCNetworkReachabilityFlagsConnectionRequired) != 0; + } + + return NO; +} + +- (NetworkStatus)currentReachabilityStatus { + NSAssert(self.reachabilityRef != NULL, + @"currentNetworkStatus called with NULL SCNetworkReachabilityRef"); + NetworkStatus returnValue = NotReachable; + SCNetworkReachabilityFlags flags; + + if (SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) { + returnValue = [self networkStatusForFlags:flags]; + } + + return returnValue; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACAppCenter.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACAppCenter.h new file mode 100644 index 0000000000..304c2bd797 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACAppCenter.h @@ -0,0 +1,186 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACConstants.h" + +@class MSACWrapperSdk; + +#if !TARGET_OS_TV +@class MSACCustomProperties; +#endif + +NS_SWIFT_NAME(AppCenter) +@interface MSACAppCenter : NSObject + +/** + * Returns the singleton instance of MSACAppCenter. + */ ++ (instancetype)sharedInstance; + +/** + * Configure the SDK with an application secret. + * + * @param appSecret A unique and secret key used to identify the application. + * + * @discussion This may be called only once per application process lifetime. + */ ++ (void)configureWithAppSecret:(NSString *)appSecret NS_SWIFT_NAME(configure(withAppSecret:)); + +/** + * Configure the SDK. + * + * @discussion This may be called only once per application process lifetime. + */ ++ (void)configure; + +/** + * Configure the SDK with an application secret and an array of services to start. + * + * @param appSecret A unique and secret key used to identify the application. + * @param services Array of services to start. + * + * @discussion This may be called only once per application process lifetime. + */ ++ (void)start:(NSString *)appSecret withServices:(NSArray *)services NS_SWIFT_NAME(start(withAppSecret:services:)); + +/** + * Start the SDK with an array of services. + * + * @param services Array of services to start. + * + * @discussion This may be called only once per application process lifetime. + */ ++ (void)startWithServices:(NSArray *)services NS_SWIFT_NAME(start(services:)); + +/** + * Start a service. + * + * @param service A service to start. + * + * @discussion This may be called only once per service per application process lifetime. + */ ++ (void)startService:(Class)service; + +/** + * Configure the SDK with an array of services to start from a library. This will not start the service at application level, it will enable + * the service only for the library. + * + * @param services Array of services to start. + */ ++ (void)startFromLibraryWithServices:(NSArray *)services NS_SWIFT_NAME(startFromLibrary(services:)); + +/** + * The flag indicates whether the SDK has already been configured or not. + */ +@property(class, atomic, readonly, getter=isConfigured) BOOL configured; + +/** + * The flag indicates whether app is running in App Center Test Cloud. + */ +@property(class, atomic, readonly, getter=isRunningInAppCenterTestCloud) BOOL runningInAppCenterTestCloud; + +/** + * The flag indicates whether or not the SDK was enabled as a whole + * + * The state is persisted in the device's storage across application launches. + */ +@property(class, nonatomic, getter=isEnabled, setter=setEnabled:) BOOL enabled NS_SWIFT_NAME(enabled); + +/** + * The SDK's log level. + */ +@property(class, nonatomic) MSACLogLevel logLevel; + +/** + * Base URL to use for backend communication. + */ +@property(class, nonatomic) NSString *logUrl; + +/** + * Set log handler. + */ +@property(class, nonatomic) MSACLogHandler logHandler; + +/** + * Set wrapper SDK information to use when building device properties. This is intended in case you are building a SDK that uses the App + * Center SDK under the hood, e.g. our Xamarin SDK or ReactNative SDk. + */ +@property(class, nonatomic) MSACWrapperSdk *wrapperSdk; + +#if !TARGET_OS_TV +/** + * Set the custom properties. + * + * @param customProperties Custom properties object. + */ ++ (void)setCustomProperties:(MSACCustomProperties *)customProperties; +#endif + +/** + * Check whether the application delegate forwarder is enabled or not. + * + * @discussion The application delegate forwarder forwards messages that target your application delegate methods via swizzling to the SDK. + * It simplifies the SDK integration but may not be suitable to any situations. For + * instance it should be disabled if you or one of your third party SDK is doing message forwarding on the application delegate. Message + * forwarding usually implies the implementation of @see NSObject#forwardingTargetForSelector: or @see NSObject#forwardInvocation: methods. + * To disable the application delegate forwarder just add the `AppCenterAppDelegateForwarderEnabled` tag to your Info .plist file and set it + * to `0`. Then you will have to forward any application delegate needed by the SDK manually. + */ +@property(class, readonly, nonatomic, getter=isAppDelegateForwarderEnabled) BOOL appDelegateForwarderEnabled; + +/** + * Unique installation identifier. + * + */ +@property(class, readonly, nonatomic) NSUUID *installId; + +/** + * Detect if a debugger is attached to the app process. This is only invoked once on app startup and can not detect + * if the debugger is being attached during runtime! + * + */ +@property(class, readonly, nonatomic, getter=isDebuggerAttached) BOOL debuggerAttached; + +/** + * Current version of AppCenter SDK. + * + */ +@property(class, readonly, nonatomic) NSString *sdkVersion; + +/** + * Set the maximum size of the internal storage. This method must be called before App Center is started. This method is only intended for + * applications. + * + * @param sizeInBytes Maximum size of the internal storage in bytes. This will be rounded up to the nearest multiple of a SQLite page size + * (default is 4096 bytes). Values below 20,480 bytes (20 KiB) will be ignored. + * + * @param completionHandler Callback that is invoked when the database size has been set. The `BOOL` parameter is `YES` if changing the size + * is successful, and `NO` otherwise. This parameter can be null. + * + * @discussion This only sets the maximum size of the database, but App Center modules might store additional data. + * The value passed to this method is not persisted on disk. The default maximum database size is 10485760 bytes (10 MiB). + */ ++ (void)setMaxStorageSize:(long)sizeInBytes completionHandler:(void (^)(BOOL))completionHandler; + +/** + * Set the user identifier. + * + * @discussion Set the user identifier for logs sent for the default target token when the secret passed in @c + * MSACAppCenter:start:withServices: contains "target={targetToken}". + * + * For App Center backend the user identifier maximum length is 256 characters. + * + * AppCenter must be configured or started before this API can be used. + */ +@property(class, nonatomic) NSString *userId; + +/** + * Set country code to use when building device properties. + * + * @see https://www.iso.org/obp/ui/#search for more information. + */ +@property(class, nonatomic) NSString *countryCode; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACAppCenter.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACAppCenter.m new file mode 100644 index 0000000000..8232404aaa --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACAppCenter.m @@ -0,0 +1,794 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAppCenterIngestion.h" +#import "MSACAppCenterInternal.h" +#import "MSACAppCenterPrivate.h" +#import "MSACAppDelegateForwarder.h" +#import "MSACChannelGroupDefault.h" +#import "MSACChannelGroupDefaultPrivate.h" +#import "MSACChannelUnitConfiguration.h" +#import "MSACDependencyConfiguration.h" +#import "MSACDeviceHistoryInfo.h" +#import "MSACDeviceTrackerPrivate.h" +#import "MSACHttpClient.h" +#import "MSACLogWithProperties.h" +#import "MSACLoggerInternal.h" +#import "MSACOneCollectorChannelDelegate.h" +#import "MSACSessionContext.h" +#import "MSACStartServiceLog.h" +#import "MSACUserIdContext.h" +#import "MSACUtility+StringFormatting.h" + +#if !TARGET_OS_TV +#import "MSACCustomPropertiesInternal.h" +#import "MSACCustomPropertiesLog.h" +#endif + +/** + * Singleton. + */ +static MSACAppCenter *sharedInstance = nil; +static dispatch_once_t onceToken; + +/** + * Base URL for HTTP Ingestion backend API calls. + */ +static NSString *const kMSACAppCenterBaseUrl = @"https://in.appcenter.ms"; + +/** + * Service name for initialization. + */ +static NSString *const kMSACServiceName = @"AppCenter"; + +/** + * The group Id for storage. + */ +static NSString *const kMSACGroupId = @"AppCenter"; + +/** + * The minimum storage size, limited by SQLite. + * 24 KiB to be able to send the default logs (start service, start session, push installation). + */ +static const long kMSACMinUpperSizeLimitInBytes = 24 * 1024; + +@implementation MSACAppCenter + +@synthesize installId = _installId; + +@synthesize logUrl = _logUrl; + ++ (instancetype)sharedInstance { + dispatch_once(&onceToken, ^{ + if (sharedInstance == nil) { + sharedInstance = [[MSACAppCenter alloc] init]; + } + }); + return sharedInstance; +} + +#pragma mark - public + ++ (void)configureWithAppSecret:(NSString *)appSecret { + + // 'appSecret' is actually a secret string + NSString *appSecretOnly = [MSACUtility appSecretFrom:appSecret]; + NSString *transmissionTargetToken = [MSACUtility transmissionTargetTokenFrom:appSecret]; + [[MSACAppCenter sharedInstance] configureWithAppSecret:appSecretOnly transmissionTargetToken:transmissionTargetToken fromApplication:YES]; +} + ++ (void)configure { + [[MSACAppCenter sharedInstance] configureWithAppSecret:nil transmissionTargetToken:nil fromApplication:YES]; +} + ++ (void)start:(NSString *)appSecret withServices:(NSArray *)services { + + // 'appSecret' is actually a secret string + [[MSACAppCenter sharedInstance] start:appSecret withServices:services fromApplication:YES]; +} + ++ (void)startWithServices:(NSArray *)services { + [[MSACAppCenter sharedInstance] start:nil withServices:services fromApplication:YES]; +} + ++ (void)startService:(Class)service { + if (!service) { + return; + } + [[MSACAppCenter sharedInstance] startServices:@[ service ] + withAppSecret:[[MSACAppCenter sharedInstance] appSecret] + transmissionTargetToken:[[MSACAppCenter sharedInstance] defaultTransmissionTargetToken] + fromApplication:YES]; +} + ++ (void)startFromLibraryWithServices:(NSArray *)services { + [[MSACAppCenter sharedInstance] start:nil withServices:services fromApplication:NO]; +} + ++ (BOOL)isConfigured { + return [MSACAppCenter sharedInstance].sdkConfigured && [MSACAppCenter sharedInstance].configuredFromApplication; +} + ++ (BOOL)isRunningInAppCenterTestCloud { + NSDictionary *environmentVariables = [[NSProcessInfo processInfo] environment]; + NSString *runningInAppCenter = environmentVariables[kMSACRunningInAppCenter]; + if ([runningInAppCenter isEqualToString:kMSACTrueEnvironmentString]) { + return YES; + } + return NO; +} + ++ (void)setLogUrl:(NSString *)logUrl { + [[MSACAppCenter sharedInstance] setLogUrl:logUrl]; +} + ++ (void)setEnabled:(BOOL)isEnabled { + [[MSACAppCenter sharedInstance] setEnabled:isEnabled]; +} + +/** + * Checks if SDK is enabled and initialized. + * + * @discussion This method is different from the instance one and in addition checks canBeUsed. + * + * @return `YES` if SDK is enabled and initialized, `NO` otherwise + */ ++ (BOOL)isEnabled { + @synchronized([MSACAppCenter sharedInstance]) { + if ([[MSACAppCenter sharedInstance] canBeUsed]) { + return [[MSACAppCenter sharedInstance] isEnabled]; + } + } + return NO; +} + ++ (BOOL)isAppDelegateForwarderEnabled { + return [MSACAppDelegateForwarder sharedInstance].enabled; +} + ++ (NSUUID *)installId { + return [[MSACAppCenter sharedInstance] installId]; +} + ++ (MSACLogLevel)logLevel { + return MSACLogger.currentLogLevel; +} + ++ (void)setLogLevel:(MSACLogLevel)logLevel { + MSACLogger.currentLogLevel = logLevel; + + // The logger is not set at the time of swizzling but now may be a good time to flush the traces. + [MSACDelegateForwarder flushTraceBuffer]; +} + ++ (void)setLogHandler:(MSACLogHandler)logHandler { + [MSACLogger setLogHandler:logHandler]; +} + ++ (void)setWrapperSdk:(MSACWrapperSdk *)wrapperSdk { + [[MSACDeviceTracker sharedInstance] setWrapperSdk:wrapperSdk]; +} + ++ (NSString *)countryCode { + return [MSACDeviceTracker sharedInstance].countryCode; +} + ++ (MSACWrapperSdk *)wrapperSdk { + return [MSACDeviceTracker sharedInstance].wrapperSdk; +} + ++ (NSString *)userId { + return [MSACUserIdContext sharedInstance].userId; +} + ++ (MSACLogHandler)logHandler { + return MSACLogger.logHandler; +} + +#if !TARGET_OS_TV ++ (void)setCustomProperties:(MSACCustomProperties *)customProperties { + [[MSACAppCenter sharedInstance] setCustomProperties:customProperties]; +} +#endif + +/** + * Check if the debugger is attached + * + * Taken from + * https://github.com/plausiblelabs/plcrashreporter/blob/2dd862ce049e6f43feb355308dfc710f3af54c4d/Source/Crash%20Demo/main.m#L96 + * + * @return `YES` if the debugger is attached to the current process, `NO` otherwise + */ ++ (BOOL)isDebuggerAttached { + static BOOL debuggerIsAttached = NO; + + static dispatch_once_t debuggerPredicate; + dispatch_once(&debuggerPredicate, ^{ + struct kinfo_proc info; + size_t info_size = sizeof(info); + int name[4]; + + name[0] = CTL_KERN; + name[1] = KERN_PROC; + name[2] = KERN_PROC_PID; + name[3] = getpid(); + + if (sysctl(name, 4, &info, &info_size, NULL, 0) == -1) { + NSLog(@"[MSACCrashes] ERROR: Checking for a running debugger via sysctl() " + @"failed."); + debuggerIsAttached = false; + } + + if (!debuggerIsAttached && (info.kp_proc.p_flag & P_TRACED) != 0) { + debuggerIsAttached = true; + } + }); + + return debuggerIsAttached; +} + ++ (NSString *)sdkVersion { + return [MSACUtility sdkVersion]; +} + ++ (NSString *)logTag { + return kMSACServiceName; +} + ++ (NSString *)groupId { + return kMSACGroupId; +} + ++ (void)setMaxStorageSize:(long)sizeInBytes completionHandler:(void (^)(BOOL))completionHandler { + [[MSACAppCenter sharedInstance] setMaxStorageSize:sizeInBytes completionHandler:completionHandler]; +} + ++ (void)setUserId:(NSString *)userId { + [[MSACAppCenter sharedInstance] setUserId:userId]; +} + ++ (void)setCountryCode:(NSString *)countryCode { + [[MSACDeviceTracker sharedInstance] setCountryCode:countryCode]; +} + +#pragma mark - private + +- (instancetype)init { + if ((self = [super init])) { + _services = [NSMutableArray new]; + _enabledStateUpdating = NO; + NSDictionary *changedKeys = @{ + @"MSAppCenterChannelStartTimer" : MSACPrefixKeyFrom(@"MSChannelStartTimer"), + // [MSACChannelUnitDefault oldestPendingLogTimestampKey] + @"MSAppCenterPastDevices" : @"pastDevicesKey", + // [MSACDeviceTracker init], + // [MSACDeviceTracker device], + // [MSACDeviceTracker clearDevices] + @"MSAppCenterInstallId" : @"MSInstallId", + // [MSACAppCenter installId] + @"MSAppCenterAppCenterIsEnabled" : @"MSAppCenterIsEnabled", + // [MSACAppCenter isEnabled] + @"MSAppCenterEncryptionKeyMetadata" : @"MSEncryptionKeyMetadata", + // [MSACEncrypter getCurrentKeyTag], + // [MSACEncrypter rotateToNewKeyTag] + @"MSAppCenterSessionIdHistory" : @"SessionIdHistory", + // [MSACSessionContext init], + // [MSACSessionContext setSessionId], + // [MSACSessionContext clearSessionHistoryAndKeepCurrentSession] + @"MSAppCenterUserIdHistory" : @"UserIdHistory" + // [MSACUserIdContext init], + // [MSACUserIdContext setUserId], + // [MSACUserIdContext clearUserIdHistory] + }; + [MSAC_APP_CENTER_USER_DEFAULTS migrateKeys:changedKeys forService:kMSACServiceName]; + } + [MSACUtility addMigrationClasses:@{ + @"MSDeviceHistoryInfo" : MSACDeviceHistoryInfo.self, + @"MSDevice" : MSACDevice.self, + @"MSStartServiceLog" : MSACStartServiceLog.self, + @"MSSessionHistoryInfo" : MSACSessionHistoryInfo.self, + @"MSUserIdHistoryInfo" : MSACUserIdHistoryInfo.self, + @"MSLogWithProperties" : MSACLogWithProperties.self, + @"MSLogContainer" : MSACLogContainer.self, + @"MSWrapperSdk" : MSACWrapperSdk.self, + @"MSAbstractLog" : MSACAbstractLog.self, + }]; +#if !TARGET_OS_TV + [MSACUtility addMigrationClasses:@{@"MSCustomProperties" : MSACCustomProperties.self}]; +#endif + return self; +} + +/** + * Configuring without an app secret is valid. If that is the case, the app secret will not be set. + */ +- (BOOL)configureWithAppSecret:(NSString *)appSecret + transmissionTargetToken:(NSString *)transmissionTargetToken + fromApplication:(BOOL)fromApplication { + @synchronized(self) { + BOOL success = false; + if (self.configuredFromApplication && fromApplication) { + MSACLogAssert([MSACAppCenter logTag], @"App Center SDK has already been configured."); + } else { + if (!self.appSecret) { + self.appSecret = appSecret; + + // Initialize session context. + // FIXME: It would be better to have obvious way to initialize session context instead of calling setSessionId. + [[MSACSessionContext sharedInstance] setSessionId:nil]; + } + if (!self.defaultTransmissionTargetToken) { + self.defaultTransmissionTargetToken = transmissionTargetToken; + } + + /* + * Instantiate MSACUserIdContext as early as possible to prevent Crashes from using older userId when a newer version of app removes + * setUserId call from older version of app. MSACUserIdContext will handle this one in intializer so we need to make sure + * MSACUserIdContext is initialized before Crashes service processes logs. + */ + [MSACUserIdContext sharedInstance]; + + // Init the main pipeline. + [self initializeChannelGroup]; + [self applyPipelineEnabledState:self.isEnabled]; + self.sdkConfigured = YES; + self.configuredFromApplication = self.configuredFromApplication || fromApplication; + + /* + * If the log level hasn't been customized before and we are not running in an app store environment, we set the default log level to + * MSACLogLevelWarning. + */ + if ((![MSACLogger isUserDefinedLogLevel]) && ([MSACUtility currentAppEnvironment] == MSACEnvironmentOther)) { + [MSACAppCenter setLogLevel:MSACLogLevelWarning]; + } + success = true; + } + if (success) { + MSACLogInfo([MSACAppCenter logTag], @"App Center SDK configured %@successfully.", fromApplication ? @"" : @"from a library "); + } else { + MSACLogAssert([MSACAppCenter logTag], @"App Center SDK configuration %@failed.", fromApplication ? @"" : @"from a library "); + } + return success; + } +} + +- (void)start:(NSString *)secretString withServices:(NSArray *)services fromApplication:(BOOL)fromApplication { + NSString *appSecret = [MSACUtility appSecretFrom:secretString]; + NSString *transmissionTargetToken = [MSACUtility transmissionTargetTokenFrom:secretString]; + BOOL configured = [self configureWithAppSecret:appSecret transmissionTargetToken:transmissionTargetToken fromApplication:fromApplication]; + if (configured && services) { + [self startServices:services withAppSecret:appSecret transmissionTargetToken:transmissionTargetToken fromApplication:fromApplication]; + } +} + +- (void)startServices:(NSArray *)services + withAppSecret:(NSString *)appSecret + transmissionTargetToken:(NSString *)transmissionTargetToken + fromApplication:(BOOL)fromApplication { + if (!self.sdkConfigured || !services) { + return; + } + NSArray *sortedServices = [self sortServices:services]; + MSACLogVerbose([MSACAppCenter logTag], @"Start services %@ from %@", [sortedServices componentsJoinedByString:@", "], + (fromApplication ? @"an application" : @"a library")); + NSMutableArray *servicesNames = [NSMutableArray arrayWithCapacity:sortedServices.count]; + for (Class service in sortedServices) { + if ([self startService:service + withAppSecret:appSecret + transmissionTargetToken:transmissionTargetToken + fromApplication:fromApplication]) { + [servicesNames addObject:[service serviceName]]; + } + } + if ([servicesNames count] > 0) { + if (fromApplication) { + [self sendStartServiceLog:servicesNames]; + } + } else { + MSACLogDebug([MSACAppCenter logTag], @"No services have been started."); + } +} + +/** + * Sort services in descending order to make sure the service with the highest priority gets initialized first. This is intended to make + * sure Crashes gets initialized first. + * + * @param services An array of services. + */ +- (NSArray *)sortServices:(NSArray *)services { + if (services && services.count > 1) { + return [services sortedArrayUsingComparator:^NSComparisonResult(id clazzA, id clazzB) { +#pragma clang diagnostic push + +// Ignore "Unknown warning group '-Wobjc-messaging-id'" for old XCode +#pragma clang diagnostic ignored "-Wunknown-pragmas" +#pragma clang diagnostic ignored "-Wunknown-warning-option" + +// Ignore "Messaging unqualified id" for XCode 10 +#pragma clang diagnostic ignored "-Wobjc-messaging-id" + id serviceA = [clazzA sharedInstance]; + id serviceB = [clazzB sharedInstance]; +#pragma clang diagnostic pop + if (serviceA.initializationPriority < serviceB.initializationPriority) { + return NSOrderedDescending; + } else { + return NSOrderedAscending; + } + }]; + } else { + return services; + } +} + +- (BOOL)startService:(Class)clazz + withAppSecret:(NSString *)appSecret + transmissionTargetToken:(NSString *)transmissionTargetToken + fromApplication:(BOOL)fromApplication { + @synchronized(self) { + + // Check if clazz is valid class. + if (![clazz conformsToProtocol:@protocol(MSACServiceCommon)]) { + MSACLogError([MSACAppCenter logTag], @"Cannot start service %@. Provided value is nil or invalid.", clazz); + return NO; + } + + // Check if App Center is not configured to start service. + if (!self.sdkConfigured || (!self.configuredFromApplication && fromApplication)) { + MSACLogError([MSACAppCenter logTag], @"App Center has not been configured so it couldn't start the service."); + return NO; + } + id service = [clazz sharedInstance]; + if (service.isAvailable && fromApplication && service.isStartedFromApplication) { + + // Service already works, we shouldn't send log with this service name + return NO; + } + if (service.isAppSecretRequired && ![appSecret length]) { + + // Service requires an app secret but none is provided. + MSACLogError([MSACAppCenter logTag], + @"Cannot start service %@. App Center was started without app secret, but the service requires it.", clazz); + return NO; + } + + // Check if service should be disabled. + if ([self shouldDisable:[clazz serviceName]]) { + MSACLogDebug([MSACAppCenter logTag], @"Environment variable to disable service has been set; not starting service %@", clazz); + return NO; + } + + if (!service.isAvailable) { + + // Set appCenterDelegate. + [self.services addObject:service]; + + // Start service with channel group. + [service startWithChannelGroup:self.channelGroup + appSecret:appSecret + transmissionTargetToken:transmissionTargetToken + fromApplication:fromApplication]; + + // Disable service if AppCenter is disabled. + if ([clazz isEnabled] && !self.isEnabled) { + self.enabledStateUpdating = YES; + [clazz setEnabled:NO]; + self.enabledStateUpdating = NO; + } + } else if (fromApplication) { + [service updateConfigurationWithAppSecret:appSecret transmissionTargetToken:transmissionTargetToken]; + } + + // Service started. + return YES; + } +} + +- (NSString *)logUrl { + return _logUrl; +} + ++ (NSString *)logUrl { + return [MSACAppCenter sharedInstance].logUrl; +} + +- (void)setLogUrl:(NSString *)logUrl { + @synchronized(self) { + _logUrl = logUrl; + id localChannelGroup = self.channelGroup; + if (localChannelGroup) { + if (self.appSecret) { + MSACLogInfo([MSACAppCenter logTag], @"The log url of App Center endpoint was changed to %@", self.logUrl); + [localChannelGroup setLogUrl:logUrl]; + } else { + MSACLogInfo([MSACAppCenter logTag], @"The log url of One Collector endpoint was changed to %@", self.logUrl); + [self.oneCollectorChannelDelegate setLogUrl:logUrl]; + } + } + } +} + +- (void)setMaxStorageSize:(long)sizeInBytes completionHandler:(void (^)(BOOL))completionHandler { + + // Check if sizeInBytes is greater than minimum size. + if (sizeInBytes < kMSACMinUpperSizeLimitInBytes) { + if (completionHandler) { + completionHandler(NO); + } + MSACLogWarning([MSACAppCenter logTag], @"Cannot set storage size to %ld bytes, minimum value is %ld bytes", sizeInBytes, + kMSACMinUpperSizeLimitInBytes); + return; + } + + // Change the max storage size. + BOOL setMaxSizeFailed = NO; + @synchronized(self) { + if (self.setMaxStorageSizeHasBeenCalled) { + MSACLogWarning([MSACAppCenter logTag], @"setMaxStorageSize:completionHandler: may only be called once per app launch"); + setMaxSizeFailed = YES; + } else { + self.setMaxStorageSizeHasBeenCalled = YES; + if (self.configuredFromApplication) { + MSACLogWarning([MSACAppCenter logTag], @"Unable to set storage size after the application has configured App Center"); + setMaxSizeFailed = YES; + } else { + self.requestedMaxStorageSizeInBytes = @(sizeInBytes); + self.maxStorageSizeCompletionHandler = completionHandler; + if (self.channelGroup) { + [self.channelGroup setMaxStorageSize:sizeInBytes completionHandler:self.maxStorageSizeCompletionHandler]; + } + } + } + } + if (setMaxSizeFailed && completionHandler) { + completionHandler(NO); + } +} + +- (void)setUserId:(NSString *)userId { + if (!self.configuredFromApplication) { + MSACLogError([MSACAppCenter logTag], @"AppCenter must be configured from application, libraries cannot call setUserId."); + return; + } + if (!self.appSecret && !self.defaultTransmissionTargetToken) { + MSACLogError([MSACAppCenter logTag], @"AppCenter must be configured with a secret from application to call setUserId."); + return; + } + if (userId) { + if (self.appSecret && ![MSACUserIdContext isUserIdValidForAppCenter:userId]) { + return; + } + if (self.defaultTransmissionTargetToken && ![MSACUserIdContext isUserIdValidForOneCollector:userId]) { + return; + } + } + [[MSACUserIdContext sharedInstance] setUserId:userId]; +} + +#if !TARGET_OS_TV +- (void)setCustomProperties:(MSACCustomProperties *)customProperties { + NSDictionary *propertiesCopy = [customProperties propertiesImmutableCopy]; + if (!customProperties || (propertiesCopy.count == 0)) { + MSACLogError([MSACAppCenter logTag], @"Custom properties may not be null or empty"); + return; + } + [self sendCustomPropertiesLog:propertiesCopy]; +} +#endif + +- (void)setEnabled:(BOOL)isEnabled { + @synchronized(self) { + if (![self canBeUsed]) { + return; + } + self.enabledStateUpdating = YES; + if ([self isEnabled] != isEnabled) { + + // Persist the enabled status. + [MSAC_APP_CENTER_USER_DEFAULTS setObject:@(isEnabled) forKey:kMSACAppCenterIsEnabledKey]; + + // Enable/disable pipeline. + [self applyPipelineEnabledState:isEnabled]; + } + + // Propagate enable/disable on all services. + for (id service in self.services) { + [[service class] setEnabled:isEnabled]; + } + self.enabledStateUpdating = NO; + MSACLogInfo([MSACAppCenter logTag], @"App Center SDK %@.", isEnabled ? @"enabled" : @"disabled"); + } +} + +- (BOOL)isEnabled { + + /* + * Get isEnabled value from persistence. + * No need to cache the value in a property, user settings already have their cache mechanism. + */ + NSNumber *isEnabledNumber = [MSAC_APP_CENTER_USER_DEFAULTS objectForKey:kMSACAppCenterIsEnabledKey]; + + // Return the persisted value otherwise it's enabled by default. + return (isEnabledNumber) ? [isEnabledNumber boolValue] : YES; +} + +- (void)applyPipelineEnabledState:(BOOL)isEnabled { + + // Remove all notification handlers. + [MSAC_NOTIFICATION_CENTER removeObserver:self]; + + // Hookup to application life-cycle events. + if (isEnabled) { +#if !TARGET_OS_OSX + [MSAC_NOTIFICATION_CENTER addObserver:self + selector:@selector(applicationDidEnterBackground) + name:UIApplicationDidEnterBackgroundNotification + object:nil]; + [MSAC_NOTIFICATION_CENTER addObserver:self + selector:@selector(applicationWillEnterForeground) + name:UIApplicationWillEnterForegroundNotification + object:nil]; +#endif + } else { + + // Clean session, device and userId history in case we are disabled. + [[MSACDeviceTracker sharedInstance] clearDevices]; + [[MSACSessionContext sharedInstance] clearSessionHistoryAndKeepCurrentSession:NO]; + [[MSACUserIdContext sharedInstance] clearUserIdHistory]; + } + + @synchronized(self) { + + // Propagate to channel group. + [self.channelGroup setEnabled:isEnabled andDeleteDataOnDisabled:YES]; + + // Send started services. + if (self.startedServiceNames && isEnabled) { + [self sendStartServiceLog:self.startedServiceNames]; + self.startedServiceNames = nil; + } + } +} + +- (void)initializeChannelGroup { + @synchronized(self) { + + // Construct channel group. + if (!self.oneCollectorChannelDelegate) { + self.oneCollectorChannelDelegate = [[MSACOneCollectorChannelDelegate alloc] initWithHttpClient:[MSACHttpClient new] + installId:self.installId + baseUrl:self.appSecret ? nil : self.logUrl]; + } + if (!self.channelGroup) { + id httpClient = [MSACDependencyConfiguration httpClient]; + if (!httpClient) { + httpClient = [MSACHttpClient new]; + } + self.channelGroup = [[MSACChannelGroupDefault alloc] initWithHttpClient:httpClient + installId:self.installId + logUrl:self.logUrl ?: kMSACAppCenterBaseUrl]; + [self.channelGroup addDelegate:self.oneCollectorChannelDelegate]; + if (self.requestedMaxStorageSizeInBytes) { + long storageSize = [self.requestedMaxStorageSizeInBytes longValue]; + [self.channelGroup setMaxStorageSize:storageSize completionHandler:self.maxStorageSizeCompletionHandler]; + } + } + [self.channelGroup setAppSecret:self.appSecret]; + + // Initialize a channel unit for start service logs. + self.channelUnit = + self.channelUnit + ?: [self.channelGroup addChannelUnitWithConfiguration:[[MSACChannelUnitConfiguration alloc] + initDefaultConfigurationWithGroupId:[MSACAppCenter groupId]]]; + } +} + +- (NSString *)appSecret { + return _appSecret; +} + +- (NSUUID *)installId { + @synchronized(self) { + if (!_installId) { + + // Check if install Id has already been persisted. + NSString *savedInstallId = [MSAC_APP_CENTER_USER_DEFAULTS objectForKey:kMSACInstallIdKey]; + if (savedInstallId) { + _installId = MSAC_UUID_FROM_STRING(savedInstallId); + } + + // Create a new random install Id if persistence failed. + if (!_installId) { + _installId = [NSUUID UUID]; + + // Persist the install Id string. + [MSAC_APP_CENTER_USER_DEFAULTS setObject:[_installId UUIDString] forKey:kMSACInstallIdKey]; + } + } + return _installId; + } +} + +- (BOOL)canBeUsed { + BOOL canBeUsed = self.sdkConfigured; + if (!canBeUsed) { + MSACLogError([MSACAppCenter logTag], @"App Center SDK hasn't been configured. You need to call [MSACAppCenter start:YOUR_APP_SECRET " + @"withServices:LIST_OF_SERVICES] first."); + } + return canBeUsed; +} + +- (void)sendStartServiceLog:(NSArray *)servicesNames { + @synchronized(self) { + if (self.isEnabled) { + MSACStartServiceLog *serviceLog = [MSACStartServiceLog new]; + serviceLog.services = servicesNames; + [self.channelUnit enqueueItem:serviceLog flags:MSACFlagsDefault]; + } else { + if (self.startedServiceNames == nil) { + self.startedServiceNames = [NSMutableArray new]; + } + [self.startedServiceNames addObjectsFromArray:servicesNames]; + } + } +} + +#if !TARGET_OS_TV +- (void)sendCustomPropertiesLog:(NSDictionary *)properties { + MSACCustomPropertiesLog *customPropertiesLog = [MSACCustomPropertiesLog new]; + customPropertiesLog.properties = properties; + [self.channelUnit enqueueItem:customPropertiesLog flags:MSACFlagsDefault]; +} +#endif + ++ (void)resetSharedInstance { + onceToken = 0; // resets the once_token so dispatch_once will run again + sharedInstance = nil; +} + +#pragma mark - Application life cycle + +#if !TARGET_OS_OSX +/** + * The application will go to the foreground. + */ +- (void)applicationWillEnterForeground { + [self.channelGroup resumeWithIdentifyingObject:self]; +} + +/** + * The application will go to the background. + */ +- (void)applicationDidEnterBackground { + [self.channelGroup pauseWithIdentifyingObject:self]; +} +#endif + +#pragma mark - Disable services for test cloud + +/** + * Determines whether a service should be disabled. + * + * @param serviceName The service name to consider for disabling. + * + * @return YES if the service should be disabled. + */ +- (BOOL)shouldDisable:(NSString *)serviceName { + NSDictionary *environmentVariables = [[NSProcessInfo processInfo] environment]; + NSString *disabledServices = environmentVariables[kMSACDisableVariable]; + if (!disabledServices) { + return NO; + } + NSMutableArray *disabledServicesList = [NSMutableArray arrayWithArray:[disabledServices componentsSeparatedByString:@","]]; + + // Trim whitespace characters. + for (NSUInteger i = 0; i < [disabledServicesList count]; ++i) { + NSString *service = disabledServicesList[i]; + service = [service stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + disabledServicesList[i] = service; + } + return [disabledServicesList containsObject:serviceName] || [disabledServicesList containsObject:kMSACDisableAll]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACAppCenterErrors.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACAppCenterErrors.h new file mode 100644 index 0000000000..8e77d77c62 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACAppCenterErrors.h @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#ifndef MSAC_APP_CENTER_ERRORS_H +#define MSAC_APP_CENTER_ERRORS_H + +#import + +#define MSAC_APP_CENTER_BASE_DOMAIN @"com.Microsoft.AppCenter." + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - Domain + +static NSString *const kMSACACErrorDomain = MSAC_APP_CENTER_BASE_DOMAIN @"ErrorDomain"; + +#pragma mark - General + +// Error codes. +NS_ENUM(NSInteger){MSACACLogInvalidContainerErrorCode = 1, MSACACCanceledErrorCode = 2, MSACACDisabledErrorCode = 3}; + +// Error descriptions. +static NSString const *kMSACACLogInvalidContainerErrorDesc = @"Invalid log container."; +static NSString const *kMSACACCanceledErrorDesc = @"The operation was canceled."; +static NSString const *kMSACACDisabledErrorDesc = @"The service is disabled."; + +#pragma mark - Connection + +// Error codes. +NS_ENUM(NSInteger){MSACACConnectionPausedErrorCode = 100, MSACACConnectionHttpErrorCode = 101}; + +// Error descriptions. +static NSString const *kMSACACConnectionHttpErrorDesc = @"An HTTP error occured."; +static NSString const *kMSACACConnectionPausedErrorDesc = @"Canceled, connection paused with log deletion."; + +// Error user info keys. +static NSString const *kMSACACConnectionHttpCodeErrorKey = @"MSConnectionHttpCode"; + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACChannelGroupProtocol.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACChannelGroupProtocol.h new file mode 100644 index 0000000000..67c5722879 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACChannelGroupProtocol.h @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#ifndef MSAC_CHANNEL_GROUP_PROTOCOL_H +#define MSAC_CHANNEL_GROUP_PROTOCOL_H + +#import + +#import "MSACChannelProtocol.h" + +NS_ASSUME_NONNULL_BEGIN + +@class MSACChannelUnitConfiguration; + +@protocol MSACIngestionProtocol; +@protocol MSACChannelUnitProtocol; + +/** + * `MSACChannelGroupProtocol` represents a kind of channel that contains constituent MSACChannelUnit objects. When an operation from the + * `MSACChannelProtocol` is performed on the group, that operation should be propagated to its constituent MSACChannelUnit objects. + */ +NS_SWIFT_NAME(ChannelGroupProtocol) +@protocol MSACChannelGroupProtocol + +/** + * Initialize a channel unit with the given configuration. + * + * @param configuration channel configuration. + * + * @return The added `MSACChannelUnitProtocol`. Use this object to enqueue logs. + */ +- (id)addChannelUnitWithConfiguration:(MSACChannelUnitConfiguration *)configuration + NS_SWIFT_NAME(addChannelUnit(withConfiguration:)); + +/** + * Initialize a channel unit with the given configuration. + * + * @param configuration channel configuration. + * @param ingestion The alternative ingestion object + * + * @return The added `MSACChannelUnitProtocol`. Use this object to enqueue logs. + */ +- (id)addChannelUnitWithConfiguration:(MSACChannelUnitConfiguration *)configuration + withIngestion:(nullable id)ingestion + NS_SWIFT_NAME(addChannelUnit(_:ingestion:)); + +/** + * Change the base URL (schema + authority + port only) used to communicate with the backend. + */ +@property(nonatomic) NSString *_Nullable logUrl; + +/** + * Set the app secret. + */ +@property(nonatomic) NSString *_Nullable appSecret; + +/** + * Set the maximum size of the internal storage. This method must be called before App Center is started. + * + * @discussion The default maximum database size is 10485760 bytes (10 MiB). + * + * @param sizeInBytes Maximum size of the internal storage in bytes. This will be rounded up to the nearest multiple of a SQLite page size + * (default is 4096 bytes). Values below 24576 bytes (24 KiB) will be ignored. + * @param completionHandler Callback that is invoked when the database size has been set. The `BOOL` parameter is `YES` if changing the size + * is successful, and `NO` otherwise. + */ +- (void)setMaxStorageSize:(long)sizeInBytes + completionHandler:(nullable void (^)(BOOL))completionHandler NS_SWIFT_NAME(setMaxStorageSize(_:completionHandler:)); + +/** + * Return a channel unit instance for the given groupId. + * + * @param groupId The group ID for a channel unit. + * + * @return A channel unit instance or `nil`. + */ +- (nullable id)channelUnitForGroupId:(NSString *)groupId; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACChannelProtocol.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACChannelProtocol.h new file mode 100644 index 0000000000..0c4b4d523d --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACChannelProtocol.h @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#ifndef MSAC_CHANNEL_PROTOCOL_H +#define MSAC_CHANNEL_PROTOCOL_H + +#import + +#import "MSACEnable.h" + +NS_ASSUME_NONNULL_BEGIN + +@protocol MSACChannelDelegate; + +/** + * `MSACChannelProtocol` contains the essential operations of a channel. Channels are broadly responsible for enqueuing logs to be sent to + * the backend and/or stored on disk. + */ +NS_SWIFT_NAME(ChannelProtocol) +@protocol MSACChannelProtocol + +/** + * Add delegate. + * + * @param delegate delegate. + */ +- (void)addDelegate:(id)delegate; + +/** + * Remove delegate. + * + * @param delegate delegate. + */ +- (void)removeDelegate:(id)delegate; + +/** + * Pause operations, logs will be stored but not sent. + * + * @param identifyingObject Object used to identify the pause request. + * + * @discussion A paused channel doesn't forward logs to the ingestion. The identifying object used to pause the channel can be any unique + * object. The same identifying object must be used to call resume. For simplicity if the caller is the one owning the channel then @c self + * can be used as identifying object. + * + * @see resumeWithIdentifyingObject: + */ +- (void)pauseWithIdentifyingObject:(id)identifyingObject; + +/** + * Resume operations, logs can be sent again. + * + * @param identifyingObject Object used to passed to the pause method. + * + * @discussion The channel only resume when all the outstanding identifying objects have been resumed. + * + * @see pauseWithIdentifyingObject: + */ +- (void)resumeWithIdentifyingObject:(id)identifyingObject; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACConstants+Flags.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACConstants+Flags.h new file mode 100644 index 0000000000..5408e550e5 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACConstants+Flags.h @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#ifndef MSAC_CONSTANTS_FLAGS_H +#define MSAC_CONSTANTS_FLAGS_H + +#import + +typedef NS_OPTIONS(NSUInteger, MSACFlags) { + MSACFlagsNone = (0 << 0), // => 00000000 + MSACFlagsNormal = (1 << 0), // => 00000001 + MSACFlagsCritical = (1 << 1), // => 00000010 + MSACFlagsPersistenceNormal DEPRECATED_MSG_ATTRIBUTE("please use MSACFlagsNormal") = MSACFlagsNormal, + MSACFlagsPersistenceCritical DEPRECATED_MSG_ATTRIBUTE("please use MSACFlagsCritical") = MSACFlagsCritical, + MSACFlagsDefault = MSACFlagsNormal +} NS_SWIFT_NAME(Flags); + +#endif diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACConstants.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACConstants.h new file mode 100644 index 0000000000..545e9ea70a --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACConstants.h @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +/** + * Log Levels + */ +typedef NS_ENUM(NSUInteger, MSACLogLevel) { + + /** + * Logging will be very chatty + */ + MSACLogLevelVerbose = 2, + + /** + * Debug information will be logged + */ + MSACLogLevelDebug = 3, + + /** + * Information will be logged + */ + MSACLogLevelInfo = 4, + + /** + * Errors and warnings will be logged + */ + MSACLogLevelWarning = 5, + + /** + * Errors will be logged + */ + MSACLogLevelError = 6, + + /** + * Only critical errors will be logged + */ + MSACLogLevelAssert = 7, + + /** + * Logging is disabled + */ + MSACLogLevelNone = 99 +} NS_SWIFT_NAME(LogLevel); + +typedef NSString * (^MSACLogMessageProvider)(void)NS_SWIFT_NAME(LogMessageProvider); +typedef void (^MSACLogHandler)(MSACLogMessageProvider messageProvider, MSACLogLevel logLevel, NSString *tag, const char *file, + const char *function, uint line) NS_SWIFT_NAME(LogHandler); + +/** + * Channel priorities, check the kMSACPriorityCount if you add a new value. + * The order matters here! Values NEED to range from low priority to high priority. + */ +typedef NS_ENUM(NSInteger, MSACPriority) { MSACPriorityBackground, MSACPriorityDefault, MSACPriorityHigh } NS_SWIFT_NAME(Priority); +static short const kMSACPriorityCount = MSACPriorityHigh + 1; + +/** + * The priority by which the modules are initialized. + * MSACPriorityMax is reserved for only 1 module and this needs to be Crashes. + * Crashes needs to be initialized first to catch crashes in our other SDK Modules (which will hopefully never happen) and to avoid losing + * any log at crash time. + */ +typedef NS_ENUM(NSInteger, MSACInitializationPriority) { + MSACInitializationPriorityDefault = 500, + MSACInitializationPriorityHigh = 750, + MSACInitializationPriorityMax = 999 +} NS_SWIFT_NAME(InitializationPriority); + +/** + * Enum with the different HTTP status codes. + */ +typedef NS_ENUM(NSInteger, MSACHTTPCodesNo) { + + // Invalid + MSACHTTPCodesNo0XXInvalidUnknown = 0, + + // Informational + MSACHTTPCodesNo1XXInformationalUnknown = 1, + MSACHTTPCodesNo100Continue = 100, + MSACHTTPCodesNo101SwitchingProtocols = 101, + MSACHTTPCodesNo102Processing = 102, + + // Success + MSACHTTPCodesNo2XXSuccessUnknown = 2, + MSACHTTPCodesNo200OK = 200, + MSACHTTPCodesNo201Created = 201, + MSACHTTPCodesNo202Accepted = 202, + MSACHTTPCodesNo203NonAuthoritativeInformation = 203, + MSACHTTPCodesNo204NoContent = 204, + MSACHTTPCodesNo205ResetContent = 205, + MSACHTTPCodesNo206PartialContent = 206, + MSACHTTPCodesNo207MultiStatus = 207, + MSACHTTPCodesNo208AlreadyReported = 208, + MSACHTTPCodesNo209IMUsed = 209, + + // Redirection + MSACHTTPCodesNo3XXSuccessUnknown = 3, + MSACHTTPCodesNo300MultipleChoices = 300, + MSACHTTPCodesNo301MovedPermanently = 301, + MSACHTTPCodesNo302Found = 302, + MSACHTTPCodesNo303SeeOther = 303, + MSACHTTPCodesNo304NotModified = 304, + MSACHTTPCodesNo305UseProxy = 305, + MSACHTTPCodesNo306SwitchProxy = 306, + MSACHTTPCodesNo307TemporaryRedirect = 307, + MSACHTTPCodesNo308PermanentRedirect = 308, + + // Client error + MSACHTTPCodesNo4XXSuccessUnknown = 4, + MSACHTTPCodesNo400BadRequest = 400, + MSACHTTPCodesNo401Unauthorised = 401, + MSACHTTPCodesNo402PaymentRequired = 402, + MSACHTTPCodesNo403Forbidden = 403, + MSACHTTPCodesNo404NotFound = 404, + MSACHTTPCodesNo405MethodNotAllowed = 405, + MSACHTTPCodesNo406NotAcceptable = 406, + MSACHTTPCodesNo407ProxyAuthenticationRequired = 407, + MSACHTTPCodesNo408RequestTimeout = 408, + MSACHTTPCodesNo409Conflict = 409, + MSACHTTPCodesNo410Gone = 410, + MSACHTTPCodesNo411LengthRequired = 411, + MSACHTTPCodesNo412PreconditionFailed = 412, + MSACHTTPCodesNo413RequestEntityTooLarge = 413, + MSACHTTPCodesNo414RequestURITooLong = 414, + MSACHTTPCodesNo415UnsupportedMediaType = 415, + MSACHTTPCodesNo416RequestedRangeNotSatisfiable = 416, + MSACHTTPCodesNo417ExpectationFailed = 417, + MSACHTTPCodesNo418IamATeapot = 418, + MSACHTTPCodesNo419AuthenticationTimeout = 419, + MSACHTTPCodesNo420MethodFailureSpringFramework = 420, + MSACHTTPCodesNo420EnhanceYourCalmTwitter = 4200, + MSACHTTPCodesNo422UnprocessableEntity = 422, + MSACHTTPCodesNo423Locked = 423, + MSACHTTPCodesNo424FailedDependency = 424, + MSACHTTPCodesNo424MethodFailureWebDaw = 4240, + MSACHTTPCodesNo425UnorderedCollection = 425, + MSACHTTPCodesNo426UpgradeRequired = 426, + MSACHTTPCodesNo428PreconditionRequired = 428, + MSACHTTPCodesNo429TooManyRequests = 429, + MSACHTTPCodesNo431RequestHeaderFieldsTooLarge = 431, + MSACHTTPCodesNo444NoResponseNginx = 444, + MSACHTTPCodesNo449RetryWithMicrosoft = 449, + MSACHTTPCodesNo450BlockedByWindowsParentalControls = 450, + MSACHTTPCodesNo451RedirectMicrosoft = 451, + MSACHTTPCodesNo451UnavailableForLegalReasons = 4510, + MSACHTTPCodesNo494RequestHeaderTooLargeNginx = 494, + MSACHTTPCodesNo495CertErrorNginx = 495, + MSACHTTPCodesNo496NoCertNginx = 496, + MSACHTTPCodesNo497HTTPToHTTPSNginx = 497, + MSACHTTPCodesNo499ClientClosedRequestNginx = 499, + + // Server error + MSACHTTPCodesNo5XXSuccessUnknown = 5, + MSACHTTPCodesNo500InternalServerError = 500, + MSACHTTPCodesNo501NotImplemented = 501, + MSACHTTPCodesNo502BadGateway = 502, + MSACHTTPCodesNo503ServiceUnavailable = 503, + MSACHTTPCodesNo504GatewayTimeout = 504, + MSACHTTPCodesNo505HTTPVersionNotSupported = 505, + MSACHTTPCodesNo506VariantAlsoNegotiates = 506, + MSACHTTPCodesNo507InsufficientStorage = 507, + MSACHTTPCodesNo508LoopDetected = 508, + MSACHTTPCodesNo509BandwidthLimitExceeded = 509, + MSACHTTPCodesNo510NotExtended = 510, + MSACHTTPCodesNo511NetworkAuthenticationRequired = 511, + MSACHTTPCodesNo522ConnectionTimedOut = 522, + MSACHTTPCodesNo598NetworkReadTimeoutErrorUnknown = 598, + MSACHTTPCodesNo599NetworkConnectTimeoutErrorUnknown = 599 +} NS_SWIFT_NAME(HTTPCodesNo); diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACCustomProperties.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACCustomProperties.h new file mode 100644 index 0000000000..28f1cf77dc --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACCustomProperties.h @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#ifndef MSAC_CUSTOM_PROPERTIES_H +#define MSAC_CUSTOM_PROPERTIES_H + +#import + +/** + * Custom properties builder. + * Collects multiple properties to send in one log. + */ +NS_SWIFT_NAME(CustomProperties) +@interface MSACCustomProperties : NSObject + +/** + * Set the specified property value with the specified key. + * If the properties previously contained a property for the key, the old value is replaced. + * + * @param key Key with which the specified value is to be set. + * @param value Value to be set with the specified key. + * + * @return This instance. + */ +- (instancetype)setString:(NSString *)value forKey:(NSString *)key NS_SWIFT_NAME(set(_:forKey:)); + +/** + * Set the specified property value with the specified key. + * If the properties previously contained a property for the key, the old value is replaced. + * + * @param key Key with which the specified value is to be set. + * @param value Value to be set with the specified key. + * + * @return This instance. + */ +- (instancetype)setNumber:(NSNumber *)value forKey:(NSString *)key NS_SWIFT_NAME(set(_:forKey:)); + +/** + * Set the specified property value with the specified key. + * If the properties previously contained a property for the key, the old value is replaced. + * + * @param key Key with which the specified value is to be set. + * @param value Value to be set with the specified key. + * + * @return This instance. + */ +- (instancetype)setBool:(BOOL)value forKey:(NSString *)key NS_SWIFT_NAME(set(_:forKey:)); + +/** + * Set the specified property value with the specified key. + * If the properties previously contained a property for the key, the old value is replaced. + * + * @param key Key with which the specified value is to be set. + * @param value Value to be set with the specified key. + * + * @return This instance. + */ +- (instancetype)setDate:(NSDate *)value forKey:(NSString *)key NS_SWIFT_NAME(set(_:forKey:)); + +/** + * Clear the property for the specified key. + * + * @param key Key whose mapping is to be cleared. + * + * @return This instance. + */ +- (instancetype)clearPropertyForKey:(NSString *)key NS_SWIFT_NAME(clearProperty(forKey:)); + +@end + +#endif diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACCustomProperties.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACCustomProperties.m new file mode 100644 index 0000000000..7ca33ec553 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACCustomProperties.m @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACCustomProperties.h" +#import "MSACAppCenterInternal.h" +#import "MSACCustomPropertiesPrivate.h" + +static NSString *const kKeyPattern = @"^[a-zA-Z][a-zA-Z0-9]*$"; +static const int maxPropertiesCount = 60; +static const int maxPropertyKeyLength = 128; +static const int maxPropertyValueLength = 128; + +@implementation MSACCustomProperties + +@synthesize properties = _properties; + +- (instancetype)init { + if ((self = [super init])) { + _properties = [NSMutableDictionary new]; + } + return self; +} + +- (instancetype)setString:(NSString *)value forKey:(NSString *)key { + return [self setObject:value forKey:key]; +} + +- (instancetype)setNumber:(NSNumber *)value forKey:(NSString *)key { + return [self setObject:value forKey:key]; +} + +- (instancetype)setBool:(BOOL)value forKey:(NSString *)key { + return [self setObject:[NSNumber numberWithBool:value] forKey:key]; +} + +- (instancetype)setDate:(NSDate *)value forKey:(NSString *)key { + return [self setObject:value forKey:key]; +} + +- (instancetype)setObject:(NSObject *)value forKey:(NSString *)key { + @synchronized(self.properties) { + if ([self isValidKey:key] && [self isValidValue:value]) { + [self.properties setObject:value forKey:key]; + } + } + return self; +} + +- (instancetype)clearPropertyForKey:(NSString *)key { + @synchronized(self.properties) { + if ([self isValidKey:key]) { + [self.properties setObject:[NSNull null] forKey:key]; + } + } + return self; +} + +- (BOOL)isValidKey:(NSString *)key { + static NSRegularExpression *regex = nil; + if (!regex) { + NSError *error = nil; + regex = [NSRegularExpression regularExpressionWithPattern:kKeyPattern options:(NSRegularExpressionOptions)0 error:&error]; + if (!regex) { + MSACLogError([MSACAppCenter logTag], @"Couldn't create regular expression with pattern\"%@\": %@", kKeyPattern, + error.localizedDescription); + return NO; + } + } + if (!key || ![regex matchesInString:key options:(NSMatchingOptions)0 range:NSMakeRange(0, key.length)].count) { + MSACLogError([MSACAppCenter logTag], @"Custom property \"%@\" must match \"%@\"", key, kKeyPattern); + return NO; + } + if (key.length > maxPropertyKeyLength) { + MSACLogError([MSACAppCenter logTag], @"Custom property \"%@\" length cannot be longer than \"%d\" characters.", key, + maxPropertyKeyLength); + return NO; + } + if ([self.properties objectForKey:key]) { + MSACLogWarning([MSACAppCenter logTag], @"Custom property \"%@\" is already set or cleared and will be overridden.", key); + } else if ([self properties].count >= maxPropertiesCount) { + MSACLogError([MSACAppCenter logTag], @"Custom properties cannot contain more than \"%d\" items.", maxPropertiesCount); + return NO; + } + return YES; +} + +- (BOOL)isValidValue:(NSObject *)value { + if (value) { + if ([value isKindOfClass:[NSString class]]) { + NSString *stringValue = (NSString *)value; + if (stringValue.length > maxPropertyValueLength) { + MSACLogError([MSACAppCenter logTag], @"Custom property value length cannot be longer than \"%d\" characters.", + maxPropertyValueLength); + return NO; + } + } else if ([value isKindOfClass:[NSNumber class]]) { + double number = [(NSNumber *)value doubleValue]; + if (number == (double)INFINITY || number == -(double)INFINITY || number != number) { + MSACLogError([MSACAppCenter logTag], @"Custom property value cannot be NaN or infinite."); + return NO; + } + } + } else { + MSACLogError([MSACAppCenter logTag], @"Custom property value cannot be null, did you mean to call clear?"); + return NO; + } + return YES; +} + +- (NSDictionary *)propertiesImmutableCopy { + @synchronized(self.properties) { + return [[NSDictionary alloc] initWithDictionary:self.properties]; + } +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACEnable.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACEnable.h new file mode 100644 index 0000000000..3feff5b5e5 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACEnable.h @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#ifndef MSAC_ENABLE_H +#define MSAC_ENABLE_H + +#import + +/** + * Protocol to define an instance that can be enabled/disabled. + */ +NS_SWIFT_NAME(Enable) +@protocol MSACEnable + +@required + +/** + * Enable/disable this instance and delete data on disabled state. + * + * @param isEnabled A boolean value set to YES to enable the instance or NO to disable it. + * @param deleteData A boolean value set to YES to delete data or NO to keep it. + */ +- (void)setEnabled:(BOOL)isEnabled andDeleteDataOnDisabled:(BOOL)deleteData NS_SWIFT_NAME(setEnabled(_:deleteDataOnDisabled:)); + +@end + +#endif diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACLogger.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACLogger.h new file mode 100644 index 0000000000..fcf792d6aa --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACLogger.h @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACConstants.h" + +#define MSACLog(_level, _tag, _message) \ + [MSACLogger logMessage:_message level:_level tag:_tag file:__FILE__ function:__PRETTY_FUNCTION__ line:__LINE__] +#define MSACLogAssert(tag, format, ...) \ + MSACLog(MSACLogLevelAssert, tag, (^{ \ + return [NSString stringWithFormat:(format), ##__VA_ARGS__]; \ + })) +#define MSACLogError(tag, format, ...) \ + MSACLog(MSACLogLevelError, tag, (^{ \ + return [NSString stringWithFormat:(format), ##__VA_ARGS__]; \ + })) +#define MSACLogWarning(tag, format, ...) \ + MSACLog(MSACLogLevelWarning, tag, (^{ \ + return [NSString stringWithFormat:(format), ##__VA_ARGS__]; \ + })) +#define MSACLogInfo(tag, format, ...) \ + MSACLog(MSACLogLevelInfo, tag, (^{ \ + return [NSString stringWithFormat:(format), ##__VA_ARGS__]; \ + })) +#define MSACLogDebug(tag, format, ...) \ + MSACLog(MSACLogLevelDebug, tag, (^{ \ + return [NSString stringWithFormat:(format), ##__VA_ARGS__]; \ + })) +#define MSACLogVerbose(tag, format, ...) \ + MSACLog(MSACLogLevelVerbose, tag, (^{ \ + return [NSString stringWithFormat:(format), ##__VA_ARGS__]; \ + })) + +NS_SWIFT_NAME(Logger) +@interface MSACLogger : NSObject + ++ (void)logMessage:(MSACLogMessageProvider)messageProvider + level:(MSACLogLevel)loglevel + tag:(NSString *)tag + file:(const char *)file + function:(const char *)function + line:(uint)line; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACLogger.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACLogger.m new file mode 100644 index 0000000000..12a457a7ec --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACLogger.m @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACLoggerInternal.h" + +@implementation MSACLogger + +static MSACLogLevel _currentLogLevel = MSACLogLevelAssert; +static MSACLogHandler currentLogHandler; +static BOOL _isUserDefinedLogLevel = NO; + +MSACLogHandler const msDefaultLogHandler = ^(MSACLogMessageProvider messageProvider, MSACLogLevel logLevel, NSString *tag, + __attribute__((unused)) const char *file, const char *function, uint line) { + if (messageProvider) { + if (_currentLogLevel > logLevel) { + return; + } + NSString *level; + switch (logLevel) { + case MSACLogLevelVerbose: + level = @"VERBOSE"; + break; + case MSACLogLevelDebug: + level = @"DEBUG"; + break; + case MSACLogLevelInfo: + level = @"INFO"; + break; + case MSACLogLevelWarning: + level = @"WARNING"; + break; + case MSACLogLevelError: + level = @"ERROR"; + break; + case MSACLogLevelAssert: + level = @"ASSERT"; + break; + case MSACLogLevelNone: + return; + } + NSLog(@"[%@] %@: %@/%d %@", tag, level, [NSString stringWithCString:function encoding:NSUTF8StringEncoding], line, messageProvider()); + } +}; + ++ (void)initialize { + currentLogHandler = msDefaultLogHandler; +} + ++ (MSACLogLevel)currentLogLevel { + @synchronized(self) { + return _currentLogLevel; + } +} + ++ (MSACLogHandler)logHandler { + @synchronized(self) { + return currentLogHandler; + } +} + ++ (void)setCurrentLogLevel:(MSACLogLevel)currentLogLevel { + @synchronized(self) { + _isUserDefinedLogLevel = YES; + _currentLogLevel = currentLogLevel; + } +} + ++ (void)setLogHandler:(MSACLogHandler)logHandler { + @synchronized(self) { + _isUserDefinedLogLevel = YES; + currentLogHandler = logHandler; + } +} + ++ (void)logMessage:(MSACLogMessageProvider)messageProvider + level:(MSACLogLevel)loglevel + tag:(NSString *)tag + file:(const char *)file + function:(const char *)function + line:(uint)line { + if (currentLogHandler) { + currentLogHandler(messageProvider, loglevel, tag, file, function, line); + } +} + ++ (BOOL)isUserDefinedLogLevel { + return _isUserDefinedLogLevel; +} + ++ (void)setIsUserDefinedLogLevel:(BOOL)isUserDefinedLogLevel { + _isUserDefinedLogLevel = isUserDefinedLogLevel; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACService.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACService.h new file mode 100644 index 0000000000..b9fafff912 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACService.h @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#ifndef MSAC_SERVICE_H +#define MSAC_SERVICE_H + +#import + +/** + * Protocol declaring service logic. + */ +NS_SWIFT_NAME(Service) +@protocol MSACService + +/** + * Indicates whether this service is enabled. + * The state is persisted in the device's storage across application launches. + */ +@property(class, nonatomic, getter=isEnabled, setter=setEnabled:) BOOL enabled NS_SWIFT_NAME(enabled); + +@end + +#endif diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACServiceAbstract.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACServiceAbstract.h new file mode 100644 index 0000000000..398e177f09 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACServiceAbstract.h @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#ifndef MSAC_SERVICE_ABSTRACT_H +#define MSAC_SERVICE_ABSTRACT_H + +#import + +#import "MSACService.h" + +@protocol MSACChannelGroupProtocol; + +/** + * Abstraction of services common logic. + * This class is intended to be subclassed only not instantiated directly. + */ +NS_SWIFT_NAME(ServiceAbstract) +@interface MSACServiceAbstract : NSObject + +/** + * The flag indicates whether the service is started from application or not. + */ +@property(nonatomic, assign) BOOL startedFromApplication; + +/** + * Start this service with a channel group. Also sets the flag that indicates that a service has been started. + * + * @param channelGroup channel group used to persist and send logs. + * @param appSecret app secret for the SDK. + * @param token default transmission target token for this service. + * @param fromApplication indicates whether the service started from an application or not. + */ +- (void)startWithChannelGroup:(id)channelGroup + appSecret:(NSString *)appSecret + transmissionTargetToken:(NSString *)token + fromApplication:(BOOL)fromApplication; + +/** + * Update configuration when the service requires to start again. This method should only be called if the service is started from libraries + * and then is being started from an application. + * + * @param appSecret app secret for the SDK. + * @param token default transmission target token for this service. + */ +- (void)updateConfigurationWithAppSecret:(NSString *)appSecret transmissionTargetToken:(NSString *)token; + +/** + * The flag indicate whether the service needs the application secret or not. + */ +@property(atomic, readonly) BOOL isAppSecretRequired; + +@end + +#endif diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACServiceAbstract.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACServiceAbstract.m new file mode 100644 index 0000000000..c03b5a5b80 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACServiceAbstract.m @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAppCenterInternal.h" +#import "MSACChannelGroupProtocol.h" +#import "MSACChannelUnitProtocol.h" + +@implementation MSACServiceAbstract + +@synthesize channelGroup = _channelGroup; +@synthesize channelUnit = _channelUnit; +@synthesize appSecret = _appSecret; +@synthesize defaultTransmissionTargetToken = _defaultTransmissionTargetToken; + +- (instancetype)init { + if ((self = [super init])) { + _started = NO; + _isEnabledKey = [NSString stringWithFormat:@"%@IsEnabled", self.groupId]; + } + return self; +} + +#pragma mark : - MSACServiceCommon + +- (BOOL)isEnabled { + + // Get isEnabled value from persistence. + // No need to cache the value in a property, user settings already have their cache mechanism. + NSNumber *isEnabledNumber = [MSAC_APP_CENTER_USER_DEFAULTS objectForKey:self.isEnabledKey]; + + // Return the persisted value otherwise it's enabled by default. + return (isEnabledNumber) ? [isEnabledNumber boolValue] : YES; +} + +- (void)setEnabled:(BOOL)isEnabled { + if (self.isEnabled != isEnabled) { + + // Apply enabled state. + [self applyEnabledState:isEnabled]; + + // Persist the enabled status. + [MSAC_APP_CENTER_USER_DEFAULTS setObject:@(isEnabled) forKey:self.isEnabledKey]; + } +} + +- (void)applyEnabledState:(BOOL)isEnabled { + + // Propagate isEnabled and delete logs on disabled. + [self.channelUnit setEnabled:isEnabled andDeleteDataOnDisabled:YES]; +} + +- (BOOL)canBeUsed { + BOOL canBeUsed = [MSACAppCenter sharedInstance].sdkConfigured && self.started; + if (!canBeUsed) { + MSACLogError( + [MSACAppCenter logTag], + @"%@ service hasn't been started. You need to call [MSACAppCenter start:YOUR_APP_SECRET withServices:LIST_OF_SERVICES] first.", + MSAC_CLASS_NAME_WITHOUT_PREFIX); + } + return canBeUsed; +} + +- (BOOL)isAvailable { + return self.isEnabled && self.started; +} + +- (MSACInitializationPriority)initializationPriority { + return MSACInitializationPriorityDefault; +} + +- (BOOL)isAppSecretRequired { + return YES; +} + +- (BOOL)isStartedFromApplication { + return self.startedFromApplication; +} + +#pragma mark : - MSACService + +- (void)startWithChannelGroup:(id)channelGroup + appSecret:(NSString *)appSecret + transmissionTargetToken:(NSString *)token + fromApplication:(BOOL)fromApplication { + self.startedFromApplication = fromApplication; + self.channelGroup = channelGroup; + self.appSecret = appSecret; + self.defaultTransmissionTargetToken = token; + self.started = YES; + if ([self respondsToSelector:@selector(channelUnitConfiguration)]) { + + // Initialize channel unit for the service in channel group. + self.channelUnit = [self.channelGroup addChannelUnitWithConfiguration:self.channelUnitConfiguration]; + } + + // Enable this service as needed. + if (self.isEnabled) { + [self applyEnabledState:self.isEnabled]; + } +} + +- (void)updateConfigurationWithAppSecret:(NSString *)appSecret transmissionTargetToken:(NSString *)token { + self.startedFromApplication = YES; + self.appSecret = appSecret; + self.defaultTransmissionTargetToken = token; + + // Enable this service as needed. + if (self.isEnabled) { + [self applyEnabledState:self.isEnabled]; + } +} + +#pragma clang diagnostic push + +// Ignore "Unknown warning group '-Wobjc-messaging-id'" for old XCode +#pragma clang diagnostic ignored "-Wunknown-pragmas" +#pragma clang diagnostic ignored "-Wunknown-warning-option" + +// Ignore "Messaging unqualified id" for XCode 10 +#pragma clang diagnostic ignored "-Wobjc-messaging-id" ++ (void)setEnabled:(BOOL)isEnabled { + @synchronized([self sharedInstance]) { + if ([[self sharedInstance] canBeUsed]) { + if (![MSACAppCenter isEnabled] && ![MSACAppCenter sharedInstance].enabledStateUpdating) { + MSACLogError([MSACAppCenter logTag], + @"The SDK is disabled. Re-enable the whole SDK from AppCenter first before enabling %@ service.", + MSAC_CLASS_NAME_WITHOUT_PREFIX); + } else { + [[self sharedInstance] setEnabled:isEnabled]; + } + } + } +} + ++ (BOOL)isEnabled { + @synchronized([self sharedInstance]) { + return [[self sharedInstance] canBeUsed] && [[self sharedInstance] isEnabled]; + } +} +#pragma clang diagnostic pop + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACWrapperLogger.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACWrapperLogger.h new file mode 100644 index 0000000000..f38e855fee --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACWrapperLogger.h @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACConstants.h" + +/** + * This is a utility for producing App Center style log messages. It is only intended for use by App Center services and wrapper SDKs of App + * Center. + */ +NS_SWIFT_NAME(WrapperLogger) +@interface MSACWrapperLogger : NSObject + ++ (void)MSACWrapperLog:(MSACLogMessageProvider)message tag:(NSString *)tag level:(MSACLogLevel)level; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACWrapperLogger.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACWrapperLogger.m new file mode 100644 index 0000000000..07d901a604 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/MSACWrapperLogger.m @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACWrapperLogger.h" +#import "MSACLogger.h" + +@implementation MSACWrapperLogger + ++ (void)MSACWrapperLog:(MSACLogMessageProvider)message tag:(NSString *)tag level:(MSACLogLevel)level { + MSACLog(level, tag, message); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Model/MSACAbstractLog.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Model/MSACAbstractLog.h new file mode 100644 index 0000000000..2d6db269aa --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Model/MSACAbstractLog.h @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#ifndef MSAC_ABSTRACT_LOG_H +#define MSAC_ABSTRACT_LOG_H + +#import + +NS_SWIFT_NAME(AbstractLog) +@interface MSACAbstractLog : NSObject + +@end + +#endif diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Model/MSACDevice.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Model/MSACDevice.h new file mode 100644 index 0000000000..18160ba330 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Model/MSACDevice.h @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#ifndef MSAC_DEVICE_H +#define MSAC_DEVICE_H + +#import + +#import "MSACWrapperSdk.h" + +NS_SWIFT_NAME(Device) +@interface MSACDevice : MSACWrapperSdk + +/* + * Name of the SDK. Consists of the name of the SDK and the platform, e.g. "appcenter.ios", "appcenter.android" + */ +@property(nonatomic, copy, readonly) NSString *sdkName; + +/* + * Version of the SDK in semver format, e.g. "1.2.0" or "0.12.3-alpha.1". + */ +@property(nonatomic, copy, readonly) NSString *sdkVersion; + +/* + * Device model (example: iPad2,3). + */ +@property(nonatomic, copy, readonly) NSString *model; + +/* + * Device manufacturer (example: HTC). + */ +@property(nonatomic, copy, readonly) NSString *oemName; + +/* + * OS name (example: iOS). + */ +@property(nonatomic, copy, readonly) NSString *osName; + +/* + * OS version (example: 9.3.0). + */ +@property(nonatomic, copy, readonly) NSString *osVersion; + +/* + * OS build code (example: LMY47X). [optional] + */ +@property(nonatomic, copy, readonly) NSString *osBuild; + +/* + * API level when applicable like in Android (example: 15). [optional] + */ +@property(nonatomic, copy, readonly) NSNumber *osApiLevel; + +/* + * Language code (example: en_US). + */ +@property(nonatomic, copy, readonly) NSString *locale; + +/* + * The offset in minutes from UTC for the device time zone, including daylight savings time. + */ +@property(nonatomic, readonly, strong) NSNumber *timeZoneOffset; + +/* + * Screen size of the device in pixels (example: 640x480). + */ +@property(nonatomic, copy, readonly) NSString *screenSize; + +/* + * Application version name, e.g. 1.1.0 + */ +@property(nonatomic, copy, readonly) NSString *appVersion; + +/* + * Carrier name (for mobile devices). [optional] + */ +@property(nonatomic, copy, readonly) NSString *carrierName; + +/* + * Carrier country code (for mobile devices). [optional] + */ +@property(nonatomic, copy, readonly) NSString *carrierCountry; + +/* + * The app's build number, e.g. 42. + */ +@property(nonatomic, copy, readonly) NSString *appBuild; + +/* + * The bundle identifier, package identifier, or namespace, depending on what the individual plattforms use, .e.g com.microsoft.example. + * [optional] + */ +@property(nonatomic, copy, readonly) NSString *appNamespace; + +@end + +#endif diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Model/MSACDevice.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Model/MSACDevice.m new file mode 100644 index 0000000000..42ed279eb2 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Model/MSACDevice.m @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACDevice.h" +#import "MSACDeviceInternal.h" +#import "MSACWrapperSdkInternal.h" + +@implementation MSACDevice + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary]; + + if (self.sdkName) { + dict[kMSACSDKName] = self.sdkName; + } + if (self.sdkVersion) { + dict[kMSACSDKVersion] = self.sdkVersion; + } + if (self.model) { + dict[kMSACModel] = self.model; + } + if (self.oemName) { + dict[kMSACOEMName] = self.oemName; + } + if (self.osName) { + dict[kMSACACOSName] = self.osName; + } + if (self.osVersion) { + dict[kMSACOSVersion] = self.osVersion; + } + if (self.osBuild) { + dict[kMSACOSBuild] = self.osBuild; + } + if (self.osApiLevel) { + dict[kMSACOSAPILevel] = self.osApiLevel; + } + if (self.locale) { + dict[kMSACLocale] = self.locale; + } + if (self.timeZoneOffset) { + dict[kMSACTimeZoneOffset] = self.timeZoneOffset; + } + if (self.screenSize) { + dict[kMSACScreenSize] = self.screenSize; + } + if (self.appVersion) { + dict[kMSACAppVersion] = self.appVersion; + } + if (self.carrierName) { + dict[kMSACCarrierName] = self.carrierName; + } + if (self.carrierCountry) { + dict[kMSACCarrierCountry] = self.carrierCountry; + } + if (self.appBuild) { + dict[kMSACAppBuild] = self.appBuild; + } + if (self.appNamespace) { + dict[kMSACAppNamespace] = self.appNamespace; + } + return dict; +} + +- (BOOL)isValid { + return [super isValid] && MSACLOG_VALIDATE_NOT_NIL(sdkName) && MSACLOG_VALIDATE_NOT_NIL(sdkVersion) && MSACLOG_VALIDATE_NOT_NIL(osName) && + MSACLOG_VALIDATE_NOT_NIL(osVersion) && MSACLOG_VALIDATE_NOT_NIL(locale) && MSACLOG_VALIDATE_NOT_NIL(timeZoneOffset) && + MSACLOG_VALIDATE_NOT_NIL(appVersion) && MSACLOG_VALIDATE_NOT_NIL(appBuild); +} + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACDevice class]] || ![super isEqual:object]) { + return NO; + } + MSACDevice *device = (MSACDevice *)object; + return ((!self.sdkName && !device.sdkName) || [self.sdkName isEqualToString:device.sdkName]) && + ((!self.sdkVersion && !device.sdkVersion) || [self.sdkVersion isEqualToString:device.sdkVersion]) && + ((!self.model && !device.model) || [self.model isEqualToString:device.model]) && + ((!self.oemName && !device.oemName) || [self.oemName isEqualToString:device.oemName]) && + ((!self.osName && !device.osName) || [self.osName isEqualToString:device.osName]) && + ((!self.osVersion && !device.osVersion) || [self.osVersion isEqualToString:device.osVersion]) && + ((!self.osBuild && !device.osBuild) || [self.osBuild isEqualToString:device.osBuild]) && + ((!self.osApiLevel && !device.osApiLevel) || [self.osApiLevel isEqualToNumber:device.osApiLevel]) && + ((!self.locale && !device.locale) || [self.locale isEqualToString:device.locale]) && + ((!self.timeZoneOffset && !device.timeZoneOffset) || [self.timeZoneOffset isEqualToNumber:device.timeZoneOffset]) && + ((!self.screenSize && !device.screenSize) || [self.screenSize isEqualToString:device.screenSize]) && + ((!self.appVersion && !device.appVersion) || [self.appVersion isEqualToString:device.appVersion]) && + ((!self.carrierName && !device.carrierName) || [self.carrierName isEqualToString:device.carrierName]) && + ((!self.carrierCountry && !device.carrierCountry) || [self.carrierCountry isEqualToString:device.carrierCountry]) && + ((!self.appBuild && !device.appBuild) || [self.appBuild isEqualToString:device.appBuild]) && + ((!self.appNamespace && !device.appNamespace) || [self.appNamespace isEqualToString:device.appNamespace]); +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if (self) { + _sdkName = [coder decodeObjectForKey:kMSACSDKName]; + _sdkVersion = [coder decodeObjectForKey:kMSACSDKVersion]; + _model = [coder decodeObjectForKey:kMSACModel]; + _oemName = [coder decodeObjectForKey:kMSACOEMName]; + _osName = [coder decodeObjectForKey:kMSACACOSName]; + _osVersion = [coder decodeObjectForKey:kMSACOSVersion]; + _osBuild = [coder decodeObjectForKey:kMSACOSBuild]; + _osApiLevel = [coder decodeObjectForKey:kMSACOSAPILevel]; + _locale = [coder decodeObjectForKey:kMSACLocale]; + _timeZoneOffset = [coder decodeObjectForKey:kMSACTimeZoneOffset]; + _screenSize = [coder decodeObjectForKey:kMSACScreenSize]; + _appVersion = [coder decodeObjectForKey:kMSACAppVersion]; + _carrierName = [coder decodeObjectForKey:kMSACCarrierName]; + _carrierCountry = [coder decodeObjectForKey:kMSACCarrierCountry]; + _appBuild = [coder decodeObjectForKey:kMSACAppBuild]; + _appNamespace = [coder decodeObjectForKey:kMSACAppNamespace]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.sdkName forKey:kMSACSDKName]; + [coder encodeObject:self.sdkVersion forKey:kMSACSDKVersion]; + [coder encodeObject:self.model forKey:kMSACModel]; + [coder encodeObject:self.oemName forKey:kMSACOEMName]; + [coder encodeObject:self.osName forKey:kMSACACOSName]; + [coder encodeObject:self.osVersion forKey:kMSACOSVersion]; + [coder encodeObject:self.osBuild forKey:kMSACOSBuild]; + [coder encodeObject:self.osApiLevel forKey:kMSACOSAPILevel]; + [coder encodeObject:self.locale forKey:kMSACLocale]; + [coder encodeObject:self.timeZoneOffset forKey:kMSACTimeZoneOffset]; + [coder encodeObject:self.screenSize forKey:kMSACScreenSize]; + [coder encodeObject:self.appVersion forKey:kMSACAppVersion]; + [coder encodeObject:self.carrierName forKey:kMSACCarrierName]; + [coder encodeObject:self.carrierCountry forKey:kMSACCarrierCountry]; + [coder encodeObject:self.appBuild forKey:kMSACAppBuild]; + [coder encodeObject:self.appNamespace forKey:kMSACAppNamespace]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Model/MSACLog.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Model/MSACLog.h new file mode 100644 index 0000000000..d46b377fd1 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Model/MSACLog.h @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#ifndef MSAC_LOG_H +#define MSAC_LOG_H + +#import + +@class MSACDevice; + +NS_SWIFT_NAME(Log) +@protocol MSACLog + +/** + * Log type. + */ +@property(nonatomic, copy) NSString *type; + +/** + * Log timestamp. + */ +@property(nonatomic, strong) NSDate *timestamp; + +/** + * A session identifier is used to correlate logs together. A session is an abstract concept in the API and is not necessarily an analytics + * session, it can be used to only track crashes. + */ +@property(nonatomic, copy) NSString *sid; + +/** + * Optional distribution group ID value. + */ +@property(nonatomic, copy) NSString *distributionGroupId; + +/** + * Optional user identifier. + */ +@property(nonatomic, copy) NSString *userId; + +/** + * Device properties associated to this log. + */ +@property(nonatomic, strong) MSACDevice *device; + +/** + * Transient object tag. For example, a log can be tagged with a transmission target. We do this currently to prevent properties being + * applied retroactively to previous logs by comparing their tags. + */ +@property(nonatomic, strong) NSObject *tag; + +/** + * Checks if the object's values are valid. + * + * @return YES, if the object is valid. + */ +- (BOOL)isValid; + +/** + * Adds a transmission target token that this log should be sent to. + * + * @param token The transmission target token. + */ +- (void)addTransmissionTargetToken:(NSString *)token; + +/** + * Gets all transmission target tokens that this log should be sent to. + * + * @returns Collection of transmission target tokens that this log should be sent to. + */ +- (NSSet *)transmissionTargetTokens; + +@end + +#endif diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Model/MSACLogWithProperties.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Model/MSACLogWithProperties.h new file mode 100644 index 0000000000..5c3019f0eb --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Model/MSACLogWithProperties.h @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#ifndef MSAC_LOG_WITH_PROPERTIES_H +#define MSAC_LOG_WITH_PROPERTIES_H + +#import + +#import "MSACAbstractLog.h" + +NS_SWIFT_NAME(LogWithProperties) +@interface MSACLogWithProperties : MSACAbstractLog + +/** + * Additional key/value pair parameters. [optional] + */ +@property(nonatomic, strong) NSDictionary *properties; + +@end + +#endif diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Model/MSACWrapperSdk.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/Model/MSACWrapperSdk.h new file mode 100644 index 0000000000..20410721d5 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Model/MSACWrapperSdk.h @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#ifndef MSAC_WRAPPER_SDK_H +#define MSAC_WRAPPER_SDK_H + +#import + +NS_SWIFT_NAME(WrapperSdk) +@interface MSACWrapperSdk : NSObject + +/* + * Version of the wrapper SDK. When the SDK is embedding another base SDK (for example Xamarin.Android wraps Android), the Xamarin specific + * version is populated into this field while sdkVersion refers to the original Android SDK. [optional] + */ +@property(nonatomic, copy, readonly) NSString *wrapperSdkVersion; + +/* + * Name of the wrapper SDK (examples: Xamarin, Cordova). [optional] + */ +@property(nonatomic, copy, readonly) NSString *wrapperSdkName; + +/* + * Version of the wrapper technology framework (Xamarin runtime version or ReactNative or Cordova etc...). [optional] + */ +@property(nonatomic, copy, readonly) NSString *wrapperRuntimeVersion; + +/* + * Label that is used to identify application code 'version' released via Live Update beacon running on device. + */ +@property(nonatomic, copy, readonly) NSString *liveUpdateReleaseLabel; + +/* + * Identifier of environment that current application release belongs to, deployment key then maps to environment like Production, Staging. + */ +@property(nonatomic, copy, readonly) NSString *liveUpdateDeploymentKey; + +/* + * Hash of all files (ReactNative or Cordova) deployed to device via LiveUpdate beacon. Helps identify the Release version on device or need + * to download updates in future + */ +@property(nonatomic, copy, readonly) NSString *liveUpdatePackageHash; + +- (instancetype)initWithWrapperSdkVersion:(NSString *)wrapperSdkVersion + wrapperSdkName:(NSString *)wrapperSdkName + wrapperRuntimeVersion:(NSString *)wrapperRuntimeVersion + liveUpdateReleaseLabel:(NSString *)liveUpdateReleaseLabel + liveUpdateDeploymentKey:(NSString *)liveUpdateDeploymentKey + liveUpdatePackageHash:(NSString *)liveUpdatePackageHash; + +/** + * Checks if the object's values are valid. + * + * @return YES, if the object is valid. + */ +- (BOOL)isValid; + +@end + +#endif diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Model/MSACWrapperSdk.m b/submodules/AppCenter-sdk/AppCenter/AppCenter/Model/MSACWrapperSdk.m new file mode 100644 index 0000000000..e9205f3aec --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Model/MSACWrapperSdk.m @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACWrapperSdk.h" +#import "MSACWrapperSdkInternal.h" + +@implementation MSACWrapperSdk + +- (instancetype)initWithWrapperSdkVersion:(NSString *)wrapperSdkVersion + wrapperSdkName:(NSString *)wrapperSdkName + wrapperRuntimeVersion:(NSString *)wrapperRuntimeVersion + liveUpdateReleaseLabel:(NSString *)liveUpdateReleaseLabel + liveUpdateDeploymentKey:(NSString *)liveUpdateDeploymentKey + liveUpdatePackageHash:(NSString *)liveUpdatePackageHash { + self = [super init]; + if (self) { + _wrapperSdkVersion = wrapperSdkVersion; + _wrapperSdkName = wrapperSdkName; + _wrapperRuntimeVersion = wrapperRuntimeVersion; + _liveUpdateReleaseLabel = liveUpdateReleaseLabel; + _liveUpdateDeploymentKey = liveUpdateDeploymentKey; + _liveUpdatePackageHash = liveUpdatePackageHash; + } + return self; +} + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [NSMutableDictionary new]; + + if (self.wrapperSdkVersion) { + dict[kMSACWrapperSDKVersion] = self.wrapperSdkVersion; + } + if (self.wrapperSdkName) { + dict[kMSACWrapperSDKName] = self.wrapperSdkName; + } + if (self.wrapperRuntimeVersion) { + dict[kMSACWrapperRuntimeVersion] = self.wrapperRuntimeVersion; + } + if (self.liveUpdateReleaseLabel) { + dict[kMSACLiveUpdateReleaseLabel] = self.liveUpdateReleaseLabel; + } + if (self.liveUpdateDeploymentKey) { + dict[kMSACLiveUpdateDeploymentKey] = self.liveUpdateDeploymentKey; + } + if (self.liveUpdatePackageHash) { + dict[kMSACLiveUpdatePackageHash] = self.liveUpdatePackageHash; + } + return dict; +} + +- (BOOL)isValid { + return YES; +} + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACWrapperSdk class]]) { + return NO; + } + MSACWrapperSdk *wrapperSdk = (MSACWrapperSdk *)object; + return ((!self.wrapperSdkVersion && !wrapperSdk.wrapperSdkVersion) || + [self.wrapperSdkVersion isEqualToString:wrapperSdk.wrapperSdkVersion]) && + ((!self.wrapperSdkName && !wrapperSdk.wrapperSdkName) || [self.wrapperSdkName isEqualToString:wrapperSdk.wrapperSdkName]) && + ((!self.wrapperRuntimeVersion && !wrapperSdk.wrapperRuntimeVersion) || + [self.wrapperRuntimeVersion isEqualToString:wrapperSdk.wrapperRuntimeVersion]) && + ((!self.liveUpdateReleaseLabel && !wrapperSdk.liveUpdateReleaseLabel) || + [self.liveUpdateReleaseLabel isEqualToString:wrapperSdk.liveUpdateReleaseLabel]) && + ((!self.liveUpdateDeploymentKey && !wrapperSdk.liveUpdateDeploymentKey) || + [self.liveUpdateDeploymentKey isEqualToString:wrapperSdk.liveUpdateDeploymentKey]) && + ((!self.liveUpdatePackageHash && !wrapperSdk.liveUpdatePackageHash) || + [self.liveUpdatePackageHash isEqualToString:wrapperSdk.liveUpdatePackageHash]); +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super init]; + if (self) { + _wrapperSdkVersion = [coder decodeObjectForKey:kMSACWrapperSDKVersion]; + _wrapperSdkName = [coder decodeObjectForKey:kMSACWrapperSDKName]; + _wrapperRuntimeVersion = [coder decodeObjectForKey:kMSACWrapperRuntimeVersion]; + _liveUpdateReleaseLabel = [coder decodeObjectForKey:kMSACLiveUpdateReleaseLabel]; + _liveUpdateDeploymentKey = [coder decodeObjectForKey:kMSACLiveUpdateDeploymentKey]; + _liveUpdatePackageHash = [coder decodeObjectForKey:kMSACLiveUpdatePackageHash]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:self.wrapperSdkVersion forKey:kMSACWrapperSDKVersion]; + [coder encodeObject:self.wrapperSdkName forKey:kMSACWrapperSDKName]; + [coder encodeObject:self.wrapperRuntimeVersion forKey:kMSACWrapperRuntimeVersion]; + [coder encodeObject:self.liveUpdateReleaseLabel forKey:kMSACLiveUpdateReleaseLabel]; + [coder encodeObject:self.liveUpdateDeploymentKey forKey:kMSACLiveUpdateDeploymentKey]; + [coder encodeObject:self.liveUpdatePackageHash forKey:kMSACLiveUpdatePackageHash]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/AppCenter Debug.xcconfig b/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/AppCenter Debug.xcconfig new file mode 100644 index 0000000000..0729deb4cc --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/AppCenter Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../../Config/Global Debug.xcconfig" +#include "./AppCenter.xcconfig" diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/AppCenter Release.xcconfig b/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/AppCenter Release.xcconfig new file mode 100644 index 0000000000..93891947bb --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/AppCenter Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../../Config/Global Release.xcconfig" +#include "./AppCenter.xcconfig" diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/AppCenter.xcconfig b/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/AppCenter.xcconfig new file mode 100644 index 0000000000..d0297bda57 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/AppCenter.xcconfig @@ -0,0 +1,5 @@ +PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.appcenter + +OTHER_LDFLAGS = $(inherited) -framework Foundation -framework SystemConfiguration -lsqlite3 + +CLANG_WARN_INT_CONVERSION = NO diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/Info.plist b/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/Info.plist new file mode 100644 index 0000000000..e30a31ca6c --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + $(CURRENT_PROJECT_VERSION) + CFBundleVersion + 1.0 + + diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/iOS.modulemap b/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/iOS.modulemap new file mode 100644 index 0000000000..f15d734dce --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/iOS.modulemap @@ -0,0 +1,13 @@ +framework module AppCenter { + umbrella header "AppCenter.h" + + export * + module * { export * } + + link framework "Foundation" + link framework "CoreTelephony" + link framework "SystemConfiguration" + link framework "UIKit" + link "sqlite3" + link "z" +} diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/iOS.xcconfig b/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/iOS.xcconfig new file mode 100644 index 0000000000..a760d57d3e --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/iOS.xcconfig @@ -0,0 +1,4 @@ +#include "../../../Config/iOS.xcconfig" +#include "../../../Config/Framework.xcconfig" + +OTHER_LDFLAGS = $(inherited) -framework CoreTelephony -framework UIKit -lsqlite3 -lz diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/macOS.modulemap b/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/macOS.modulemap new file mode 100644 index 0000000000..32c35bdba8 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/macOS.modulemap @@ -0,0 +1,12 @@ +framework module AppCenter { + umbrella header "AppCenter.h" + + export * + module * { export * } + + link framework "Foundation" + link framework "SystemConfiguration" + link framework "AppKit" + link "sqlite3" + link "z" +} diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/macOS.xcconfig b/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/macOS.xcconfig new file mode 100644 index 0000000000..2df0c34f32 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/macOS.xcconfig @@ -0,0 +1,4 @@ +#include "../../../Config/macOS.xcconfig" +#include "../../../Config/Framework.xcconfig" + +OTHER_LDFLAGS = $(inherited) -framework AppKit -lsqlite3 -lz diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/tvOS.modulemap b/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/tvOS.modulemap new file mode 100644 index 0000000000..ec02d0fe3e --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/tvOS.modulemap @@ -0,0 +1,12 @@ +framework module AppCenter { + umbrella header "AppCenter.h" + + export * + module * { export * } + + link framework "Foundation" + link framework "SystemConfiguration" + link framework "UIKit" + link "sqlite3" + link "z" +} diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/tvOS.xcconfig b/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/tvOS.xcconfig new file mode 100644 index 0000000000..8e405fa16d --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/Support/tvOS.xcconfig @@ -0,0 +1,4 @@ +#include "../../../Config/tvOS.xcconfig" +#include "../../../Config/Framework.xcconfig" + +OTHER_LDFLAGS = $(inherited) -framework UIKit -lsqlite3 -lz diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/include/AppCenter.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/AppCenter.h new file mode 120000 index 0000000000..87e196dc78 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/AppCenter.h @@ -0,0 +1 @@ +../AppCenter.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACAbstractLog.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACAbstractLog.h new file mode 120000 index 0000000000..160694007b --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACAbstractLog.h @@ -0,0 +1 @@ +../Model/MSACAbstractLog.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACAppCenter.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACAppCenter.h new file mode 120000 index 0000000000..a401e639c6 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACAppCenter.h @@ -0,0 +1 @@ +../MSACAppCenter.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACAppCenterErrors.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACAppCenterErrors.h new file mode 120000 index 0000000000..5ce69c3b0b --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACAppCenterErrors.h @@ -0,0 +1 @@ +../MSACAppCenterErrors.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACChannelGroupProtocol.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACChannelGroupProtocol.h new file mode 120000 index 0000000000..238859b0b9 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACChannelGroupProtocol.h @@ -0,0 +1 @@ +../MSACChannelGroupProtocol.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACChannelProtocol.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACChannelProtocol.h new file mode 120000 index 0000000000..7ddba8e466 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACChannelProtocol.h @@ -0,0 +1 @@ +../MSACChannelProtocol.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACConstants+Flags.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACConstants+Flags.h new file mode 120000 index 0000000000..8047644c95 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACConstants+Flags.h @@ -0,0 +1 @@ +../MSACConstants+Flags.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACConstants.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACConstants.h new file mode 120000 index 0000000000..24f23ff603 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACConstants.h @@ -0,0 +1 @@ +../MSACConstants.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACCustomProperties.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACCustomProperties.h new file mode 120000 index 0000000000..34f069a5e1 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACCustomProperties.h @@ -0,0 +1 @@ +../MSACCustomProperties.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACDevice.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACDevice.h new file mode 120000 index 0000000000..39cae415aa --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACDevice.h @@ -0,0 +1 @@ +../Model/MSACDevice.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACEnable.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACEnable.h new file mode 120000 index 0000000000..04ee93c6f6 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACEnable.h @@ -0,0 +1 @@ +../MSACEnable.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACLog.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACLog.h new file mode 120000 index 0000000000..2e578655ae --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACLog.h @@ -0,0 +1 @@ +../Model/MSACLog.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACLogWithProperties.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACLogWithProperties.h new file mode 120000 index 0000000000..23e0edee7d --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACLogWithProperties.h @@ -0,0 +1 @@ +../Model/MSACLogWithProperties.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACLogger.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACLogger.h new file mode 120000 index 0000000000..e481fc67ad --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACLogger.h @@ -0,0 +1 @@ +../MSACLogger.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACService.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACService.h new file mode 120000 index 0000000000..7e7158b09c --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACService.h @@ -0,0 +1 @@ +../MSACService.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACServiceAbstract.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACServiceAbstract.h new file mode 120000 index 0000000000..92e2439c8c --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACServiceAbstract.h @@ -0,0 +1 @@ +../MSACServiceAbstract.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACWrapperLogger.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACWrapperLogger.h new file mode 120000 index 0000000000..cd412f0d5e --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACWrapperLogger.h @@ -0,0 +1 @@ +../MSACWrapperLogger.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACWrapperSdk.h b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACWrapperSdk.h new file mode 120000 index 0000000000..7447d95f7e --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenter/include/MSACWrapperSdk.h @@ -0,0 +1 @@ +../Model/MSACWrapperSdk.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Info.plist b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Info.plist new file mode 100644 index 0000000000..ba72822e87 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/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 + 1 + + diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACAbstractLogTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACAbstractLogTests.m new file mode 100644 index 0000000000..db8e91d844 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACAbstractLogTests.m @@ -0,0 +1,309 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAbstractLogInternal.h" +#import "MSACAbstractLogPrivate.h" +#import "MSACAppExtension.h" +#import "MSACCSExtensions.h" +#import "MSACDevice.h" +#import "MSACLocExtension.h" +#import "MSACNetExtension.h" +#import "MSACOSExtension.h" +#import "MSACProtocolExtension.h" +#import "MSACSDKExtension.h" +#import "MSACTestFrameworks.h" +#import "MSACUserExtension.h" +#import "MSACUtility.h" + +@interface MSACAbstractLogTests : XCTestCase + +@property(nonatomic, strong) MSACAbstractLog *sut; + +@end + +@implementation MSACAbstractLogTests + +#pragma mark - Setup + +- (void)setUp { + [super setUp]; + self.sut = [MSACAbstractLog new]; + self.sut.type = @"fake"; + self.sut.timestamp = [NSDate dateWithTimeIntervalSince1970:0]; + self.sut.sid = @"FAKE-SESSION-ID"; + self.sut.distributionGroupId = @"FAKE-GROUP-ID"; + self.sut.userId = @"FAKE-USER-ID"; + self.sut.device = OCMPartialMock([MSACDevice new]); +} + +#pragma mark - Tests + +- (void)testInitializationWorks { + XCTAssertNotNil(self.sut); +} + +- (void)testSerializingToDictionaryWorks { + + // When + NSMutableDictionary *actual = [self.sut serializeToDictionary]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual[@"type"], equalTo(@"fake")); + assertThat(actual[@"timestamp"], equalTo(@"1970-01-01T00:00:00.000Z")); + assertThat(actual[@"sid"], equalTo(@"FAKE-SESSION-ID")); + assertThat(actual[@"distributionGroupId"], equalTo(@"FAKE-GROUP-ID")); + assertThat(actual[@"userId"], equalTo(@"FAKE-USER-ID")); + assertThat(actual[@"device"], equalTo(@{})); +} + +- (void)testNSCodingSerializationAndDeserializationWorks { + + // When + NSData *serializedLog = [MSACUtility archiveKeyedData:self.sut]; + id actual = [MSACUtility unarchiveKeyedData:serializedLog]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual, instanceOf([MSACAbstractLog class])); + + MSACAbstractLog *actualLog = actual; + assertThat(actualLog.type, equalTo(self.sut.type)); + assertThat(actualLog.timestamp, equalTo(self.sut.timestamp)); + assertThat(actualLog.sid, equalTo(self.sut.sid)); + assertThat(actualLog.distributionGroupId, equalTo(self.sut.distributionGroupId)); + assertThat(actualLog.userId, equalTo(self.sut.userId)); + assertThat(actualLog.device, equalTo(self.sut.device)); +} + +- (void)testIsValid { + + // If + id device = OCMClassMock([MSACDevice class]); + OCMStub([device isValid]).andReturn(YES); + self.sut.type = @"fake"; + self.sut.timestamp = [NSDate dateWithTimeIntervalSince1970:42]; + self.sut.device = device; + + // Then + XCTAssertTrue([self.sut isValid]); + + // When + self.sut.type = nil; + self.sut.timestamp = [NSDate dateWithTimeIntervalSince1970:42]; + self.sut.device = device; + + // Then + XCTAssertFalse([self.sut isValid]); + + // When + self.sut.type = @"fake"; + self.sut.timestamp = nil; + self.sut.device = device; + + // Then + XCTAssertFalse([self.sut isValid]); + + // When + self.sut.type = @"fake"; + self.sut.timestamp = [NSDate dateWithTimeIntervalSince1970:42]; + self.sut.device = nil; + + // Then + XCTAssertFalse([self.sut isValid]); +} + +- (void)testIsEqual { + + // If + self.sut.tag = [NSObject new]; + MSACAbstractLog *log = [MSACAbstractLog new]; + log.type = self.sut.type; + log.timestamp = self.sut.timestamp; + log.sid = self.sut.sid; + log.distributionGroupId = self.sut.distributionGroupId; + log.userId = self.sut.userId; + log.device = self.sut.device; + log.tag = self.sut.tag; + + // Then + XCTAssertTrue([self.sut isEqual:log]); + + // When + self.sut.type = @"new-fake"; + + // Then + XCTAssertFalse([self.sut isEqual:log]); + + // When + self.sut.tag = [NSObject new]; + + // Then + XCTAssertFalse([self.sut isEqual:log]); + + // When + self.sut.type = @"fake"; + self.sut.distributionGroupId = @"FAKE-NEW-GROUP-ID"; + self.sut.tag = [NSObject new]; + + // Then + XCTAssertFalse([self.sut isEqual:log]); + + // When + self.sut.distributionGroupId = @"FAKE-GROUP-ID"; + self.sut.userId = @"FAKE-NEW-USER-ID"; + + // Then + XCTAssertFalse([self.sut isEqual:log]); +} + +- (void)testSerializingToJsonWorks { + + // When + NSString *actual = [self.sut serializeLogWithPrettyPrinting:false]; + NSData *actualData = [actual dataUsingEncoding:NSUTF8StringEncoding]; + id actualDict = [NSJSONSerialization JSONObjectWithData:actualData options:0 error:nil]; + + // Then + assertThat(actualDict, instanceOf([NSDictionary class])); + assertThat([actualDict objectForKey:@"type"], equalTo(@"fake")); + assertThat([actualDict objectForKey:@"timestamp"], equalTo(@"1970-01-01T00:00:00.000Z")); + assertThat([actualDict objectForKey:@"sid"], equalTo(@"FAKE-SESSION-ID")); + assertThat([actualDict objectForKey:@"distributionGroupId"], equalTo(@"FAKE-GROUP-ID")); + assertThat([actualDict objectForKey:@"userId"], equalTo(@"FAKE-USER-ID")); + assertThat([actualDict objectForKey:@"device"], equalTo(@{})); +} + +- (void)testTransmissionTargetsWork { + + // If + NSString *transmissionTargetToken1 = @"t1"; + NSString *transmissionTargetToken = @"t2"; + + // When + [self.sut addTransmissionTargetToken:transmissionTargetToken1]; + [self.sut addTransmissionTargetToken:transmissionTargetToken1]; + [self.sut addTransmissionTargetToken:transmissionTargetToken]; + NSSet *transmissionTargets = [self.sut transmissionTargetTokens]; + + // Then + XCTAssertEqual([transmissionTargets count], (uint)2); + XCTAssertTrue([transmissionTargets containsObject:transmissionTargetToken1]); + XCTAssertTrue([transmissionTargets containsObject:transmissionTargetToken]); +} + +- (void)testNoCommonSchemaLogCreatedWhenNilTargetTokenArray { + + // If + self.sut.transmissionTargetTokens = nil; + + // When + NSArray *csLogs = [self.sut toCommonSchemaLogsWithFlags:MSACFlagsDefault]; + + // Then + XCTAssertNil(csLogs); +} + +- (void)testNoCommonSchemaLogCreatedWhenEmptyTargetTokenArray { + + // If + self.sut.transmissionTargetTokens = [@[] mutableCopy]; + + // When + NSArray *csLogs = [self.sut toCommonSchemaLogsWithFlags:MSACFlagsDefault]; + + // Then + XCTAssertNil(csLogs); +} + +- (void)testCommonSchemaLogsCorrectWhenConverted { + + // If + NSArray *expectedIKeys = @[ @"o:iKey1", @"o:iKey2" ]; + NSSet *expectedTokens = [NSSet setWithArray:@[ @"iKey1-dummytoken", @"iKey2-dummytoken" ]]; + self.sut.transmissionTargetTokens = expectedTokens; + OCMStub(self.sut.device.oemName).andReturn(@"fakeOem"); + OCMStub(self.sut.device.model).andReturn(@"fakeModel"); + OCMStub(self.sut.device.locale).andReturn(@"en_US"); + NSString *expectedLocale = @"en-US"; + OCMStub(self.sut.device.osVersion).andReturn(@"12.0.0"); + OCMStub(self.sut.device.osBuild).andReturn(@"F12332"); + NSString *expectedVersion = @"Version 12.0.0 (Build F12332)"; + OCMStub(self.sut.device.osName).andReturn(@"fakeOS"); + OCMStub(self.sut.device.appVersion).andReturn(@"1234"); + OCMStub(self.sut.device.appNamespace).andReturn(@"com.microsoft.tests"); + NSString *expectedAppId = @"I:com.microsoft.tests"; + OCMStub(self.sut.device.carrierName).andReturn(@"testCarrier"); + OCMStub(self.sut.device.sdkName).andReturn(@"AppCenter"); + OCMStub(self.sut.device.sdkVersion).andReturn(@"1.0.0"); + NSString *expectedLibVersion = @"AppCenter-1.0.0"; + OCMStub(self.sut.device.timeZoneOffset).andReturn(@100); + NSString *expectedTimeZoneOffset = @"+01:40"; + id bundleMock = OCMClassMock([NSBundle class]); + NSString *expectedAppLocale = @"fr_DE"; + OCMStub([bundleMock mainBundle]).andReturn(bundleMock); + OCMStub([bundleMock preferredLocalizations]).andReturn(@[ expectedAppLocale ]); + MSACFlags expectedFlags = MSACFlagsNormal; + NSString *prefixedUserId = [NSString stringWithFormat:@"c:%@", self.sut.userId]; + + // When + NSArray *csLogs = [self.sut toCommonSchemaLogsWithFlags:MSACFlagsNormal]; + + // Then + XCTAssertEqual(csLogs.count, expectedTokens.count); + for (MSACCommonSchemaLog *log in csLogs) { + + // Root. + for (NSString *token in log.transmissionTargetTokens) { + XCTAssertTrue([expectedTokens containsObject:token]); + } + XCTAssertEqualObjects(log.ver, @"3.0"); + XCTAssertEqualObjects(self.sut.timestamp, log.timestamp); + XCTAssertTrue([expectedIKeys containsObject:log.iKey]); + XCTAssertEqual(expectedFlags, log.flags); + + // Extension. + XCTAssertNotNil(log.ext); + + // Protocol extension. + XCTAssertNotNil(log.ext.protocolExt); + XCTAssertEqualObjects(log.ext.protocolExt.devMake, self.sut.device.oemName); + XCTAssertEqualObjects(log.ext.protocolExt.devModel, self.sut.device.model); + + // User extension. + XCTAssertNotNil(log.ext.userExt); + XCTAssertEqualObjects(log.ext.userExt.localId, prefixedUserId); + XCTAssertEqualObjects(log.ext.userExt.locale, expectedLocale); + + // OS extension. + XCTAssertNotNil(log.ext.osExt); + XCTAssertEqualObjects(log.ext.osExt.name, self.sut.device.osName); + XCTAssertEqualObjects(log.ext.osExt.ver, expectedVersion); + + // App extension. + XCTAssertNotNil(log.ext.appExt); + XCTAssertEqualObjects(log.ext.appExt.appId, expectedAppId); + XCTAssertEqualObjects(log.ext.appExt.ver, self.sut.device.appVersion); + XCTAssertEqualObjects(log.ext.appExt.locale, expectedAppLocale); + + // Network extension. + XCTAssertNotNil(log.ext.netExt); + XCTAssertEqualObjects(log.ext.netExt.provider, self.sut.device.carrierName); + + // SDK extension. + XCTAssertNotNil(log.ext.sdkExt); + XCTAssertEqualObjects(log.ext.sdkExt.libVer, expectedLibVersion); + + // Loc extension. + XCTAssertNotNil(log.ext.locExt); + XCTAssertEqualObjects(log.ext.locExt.tz, expectedTimeZoneOffset); + + // Device extension. + XCTAssertNotNil(log.ext.deviceExt); + + // Clean up. + [bundleMock stopMocking]; + } +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACAppCenterIngestionTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACAppCenterIngestionTests.m new file mode 100644 index 0000000000..1db515853c --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACAppCenterIngestionTests.m @@ -0,0 +1,223 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "AppCenter+Internal.h" +#import "MSACAppCenterErrors.h" +#import "MSACAppCenterIngestion.h" +#import "MSACConstants+Internal.h" +#import "MSACDeviceInternal.h" +#import "MSACHttpClient.h" +#import "MSACHttpIngestionPrivate.h" +#import "MSACHttpTestUtil.h" +#import "MSACHttpUtil.h" +#import "MSACLoggerInternal.h" +#import "MSACMockLog.h" +#import "MSACTestFrameworks.h" +#import "MSACTestUtil.h" + +static NSTimeInterval const kMSACTestTimeout = 5.0; +static NSString *const kMSACBaseUrl = @"https://test.com"; +static NSString *const kMSACTestAppSecret = @"TestAppSecret"; + +@interface MSACAppCenterIngestionTests : XCTestCase + +@property(nonatomic) MSACAppCenterIngestion *sut; +@property(nonatomic) id deviceMock; +@property(nonatomic) id reachabilityMock; +@property(nonatomic) NetworkStatus currentNetworkStatus; +@property(nonatomic) id httpClientMock; + +@end + +/* + * TODO: Separate base MSACHttpIngestion tests from this test and instantiate MSACAppCenterIngestion with initWithBaseUrl:, not the one with + * multiple parameters. Look at comments in each method. Add testHeaders to verify headers are populated properly. Look at testHeaders in + * MSACOneCollectorIngestionTests. + */ +@implementation MSACAppCenterIngestionTests + +- (void)setUp { + [super setUp]; + + NSDictionary *headers = @{@"Content-Type" : @"application/json", @"App-Secret" : kMSACTestAppSecret, @"Install-ID" : MSAC_UUID_STRING}; + NSDictionary *queryStrings = @{@"api-version" : @"1.0.0"}; + self.httpClientMock = OCMPartialMock([MSACHttpClient new]); + self.deviceMock = OCMPartialMock([MSACDevice new]); + OCMStub([self.deviceMock isValid]).andReturn(YES); + + // Mock reachability. + self.reachabilityMock = OCMClassMock([MSAC_Reachability class]); + self.currentNetworkStatus = ReachableViaWiFi; + OCMStub([self.reachabilityMock currentReachabilityStatus]).andDo(^(NSInvocation *invocation) { + NetworkStatus test = self.currentNetworkStatus; + [invocation setReturnValue:&test]; + }); + + // sut: System under test + self.sut = [[MSACAppCenterIngestion alloc] initWithHttpClient:self.httpClientMock + baseUrl:kMSACBaseUrl + apiPath:@"/test-path" + headers:headers + queryStrings:queryStrings + retryIntervals:@[ @(0.5), @(1), @(1.5) ]]; + [self.sut setAppSecret:kMSACTestAppSecret]; +} + +- (void)tearDown { + [MSACHttpTestUtil removeAllStubs]; + + /* + * Setting the variable to nil. We are experiencing test failure on Xcode 9 beta because the instance that was used for previous test + * method is not disposed and still listening to network changes in other tests. + */ + [MSAC_NOTIFICATION_CENTER removeObserver:self.sut name:kMSACReachabilityChangedNotification object:nil]; + self.sut = nil; + + // Stop mock. + [self.deviceMock stopMocking]; + [self.httpClientMock stopMocking]; + [self.reachabilityMock stopMocking]; + [super tearDown]; +} + +- (void)testSendBatchLogs { + + // Stub http response + [MSACHttpTestUtil stubHttp200Response]; + NSString *containerId = @"1"; + MSACLogContainer *container = [MSACTestUtil createLogContainerWithId:containerId device:self.deviceMock]; + __weak XCTestExpectation *expectation = [self expectationWithDescription:@"HTTP Response 200"]; + [self.sut sendAsync:container + completionHandler:^(NSString *batchId, NSHTTPURLResponse *response, __unused NSData *data, NSError *error) { + XCTAssertNil(error); + XCTAssertEqual(containerId, batchId); + XCTAssertEqual((MSACHTTPCodesNo)response.statusCode, MSACHTTPCodesNo200OK); + + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testInvalidContainer { + __weak XCTestExpectation *expectation = [self expectationWithDescription:@"Http call complete."]; + MSACAbstractLog *log = [MSACAbstractLog new]; + log.sid = MSAC_UUID_STRING; + log.timestamp = [NSDate date]; + + // Log does not have device info, therefore, it's an invalid log + MSACLogContainer *container = [[MSACLogContainer alloc] initWithBatchId:@"1" andLogs:(NSArray> *)@[ log ]]; + + // Then + OCMReject([self.httpClientMock sendAsync:OCMOCK_ANY + method:OCMOCK_ANY + headers:OCMOCK_ANY + data:OCMOCK_ANY + retryIntervals:OCMOCK_ANY + compressionEnabled:OCMOCK_ANY + completionHandler:OCMOCK_ANY]); + + // When + [self.sut sendAsync:container + completionHandler:^(__unused NSString *batchId, __unused NSHTTPURLResponse *response, __unused NSData *data, NSError *error) { + XCTAssertEqual(error.domain, kMSACACErrorDomain); + XCTAssertEqual(error.code, MSACACLogInvalidContainerErrorCode); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testNilContainer { + + MSACLogContainer *container = nil; + + __weak XCTestExpectation *expectation = [self expectationWithDescription:@"HTTP Network Down"]; + [self.sut sendAsync:container + completionHandler:^(__unused NSString *batchId, __unused NSHTTPURLResponse *response, __unused NSData *data, NSError *error) { + XCTAssertNotNil(error); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testHttpClientDelegateObfuscateHeaderValue { + + // If + id mockLogger = OCMClassMock([MSACLogger class]); + id mockHttpUtil = OCMClassMock([MSACHttpUtil class]); + OCMStub([mockLogger currentLogLevel]).andReturn(MSACLogLevelVerbose); + OCMStub(ClassMethod([mockHttpUtil hideSecret:OCMOCK_ANY])).andDo(nil); + NSDictionary *headers = @{kMSACHeaderAppSecretKey : kMSACTestAppSecret}; + NSURL *url = [NSURL new]; + + // When + [self.sut willSendHTTPRequestToURL:url withHeaders:headers]; + + // Then + OCMVerify([mockHttpUtil hideSecret:kMSACTestAppSecret]); + + [mockLogger stopMocking]; + [mockHttpUtil stopMocking]; +} + +- (void)testSetBaseURL { + + // If + NSString *path = @"path"; + NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@%@", @"https://www.contoso.com/", path]]; + self.sut.apiPath = path; + + // Query should be the same. + NSString *query = self.sut.sendURL.query; + + // When + [self.sut setBaseURL:(NSString * _Nonnull)[url.URLByDeletingLastPathComponent absoluteString]]; + + // Then + XCTAssertNotNil(query); + NSString *expectedURLString = [NSString stringWithFormat:@"%@?%@", url.absoluteString, query]; + XCTAssertTrue([[self.sut.sendURL absoluteString] isEqualToString:expectedURLString]); +} + +- (void)testSetInvalidBaseURL { + + // If + NSURL *expected = self.sut.sendURL; + NSString *invalidURL = @"\notGood"; + + // When + [self.sut setBaseURL:invalidURL]; + + // Then + assertThat(self.sut.sendURL, is(expected)); +} + +- (void)testObfuscateResponsePayload { + + // If + NSString *payload = @"I am the payload for testing"; + + // When + NSString *actual = [self.sut obfuscateResponsePayload:payload]; + + // Then + XCTAssertEqual(actual, payload); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACAppCenterTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACAppCenterTests.m new file mode 100644 index 0000000000..6ef83d3f64 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACAppCenterTests.m @@ -0,0 +1,1020 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include + +#if !TARGET_OS_TV +#import "MSACCustomProperties.h" +#import "MSACCustomPropertiesLog.h" +#endif + +#import "MSACAppCenter.h" +#import "MSACAppCenterIngestion.h" +#import "MSACAppCenterInternal.h" +#import "MSACAppCenterPrivate.h" +#import "MSACChannelGroupDefault.h" +#import "MSACDeviceTrackerPrivate.h" +#import "MSACHttpIngestionPrivate.h" +#import "MSACMockSecondService.h" +#import "MSACMockService.h" +#import "MSACMockUserDefaults.h" +#import "MSACOneCollectorChannelDelegate.h" +#import "MSACOneCollectorChannelDelegatePrivate.h" +#import "MSACOneCollectorIngestion.h" +#import "MSACSessionContextPrivate.h" +#import "MSACStartServiceLog.h" +#import "MSACTestFrameworks.h" +#import "MSACUserIdContextPrivate.h" + +static NSString *const kMSACInstallIdStringExample = @"F18499DA-5C3D-4F05-B4E8-D8C9C06A6F09"; + +// NSUUID can return this nullified InstallId while creating a UUID from a nil string, we want to avoid this. +static NSString *const kMSACNullifiedInstallIdString = @"00000000-0000-0000-0000-000000000000"; + +@interface MSACAppCenterTest : XCTestCase + +@property(nonatomic) MSACAppCenter *sut; +@property(nonatomic) MSACMockUserDefaults *settingsMock; +@property(nonatomic) NSString *installId; +@property(nonatomic) id deviceTrackerMock; +@property(nonatomic) id sessionContextMock; +@property(nonatomic) id channelGroupMock; +@property(nonatomic) id channelUnitMock; + +@end + +@implementation MSACAppCenterTest + +- (void)setUp { + [super setUp]; + [MSACAppCenter resetSharedInstance]; + [MSACUserIdContext resetSharedInstance]; + + // System Under Test. + self.sut = [[MSACAppCenter alloc] init]; + self.settingsMock = [MSACMockUserDefaults new]; + self.channelGroupMock = OCMClassMock([MSACChannelGroupDefault class]); + self.channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([self.channelGroupMock alloc]).andReturn(self.channelGroupMock); + OCMStub([self.channelGroupMock initWithHttpClient:OCMOCK_ANY installId:OCMOCK_ANY logUrl:OCMOCK_ANY]).andReturn(self.channelGroupMock); + OCMStub([self.channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY]).andReturn(self.channelUnitMock); + + // Device tracker. + [MSACDeviceTracker resetSharedInstance]; + self.deviceTrackerMock = OCMClassMock([MSACDeviceTracker class]); + OCMStub([self.deviceTrackerMock sharedInstance]).andReturn(self.deviceTrackerMock); + + // Session context. + [MSACSessionContext resetSharedInstance]; + self.sessionContextMock = OCMClassMock([MSACSessionContext class]); + OCMStub([self.sessionContextMock sharedInstance]).andReturn(self.sessionContextMock); +} + +- (void)tearDown { + [self.settingsMock stopMocking]; + [self.channelGroupMock stopMocking]; + [self.deviceTrackerMock stopMocking]; + [self.sessionContextMock stopMocking]; + [MSACMockService resetSharedInstance]; + [MSACMockSecondService resetSharedInstance]; + [MSACDeviceTracker resetSharedInstance]; + [MSACSessionContext resetSharedInstance]; + [super tearDown]; +} + +#pragma mark - install Id + +- (void)testGetInstallIdFromEmptyStorage { + + // If + // InstallId is removed from the storage. + [self.settingsMock removeObjectForKey:kMSACInstallIdKey]; + + // When + NSUUID *installId = self.sut.installId; + NSString *installIdString = [installId UUIDString]; + + // Then + assertThat(installId, notNilValue()); + assertThat(installIdString, notNilValue()); + assertThatInteger([installIdString length], greaterThan(@(0))); + assertThat(installIdString, isNot(kMSACNullifiedInstallIdString)); +} + +- (void)testStartWithAppSecretOnly { + + // When + NSString *appSecret = MSAC_UUID_STRING; + [MSACAppCenter start:appSecret withServices:@[ MSACMockService.class, MSACMockSecondService.class ]]; + + // Then + XCTAssertNil([[MSACAppCenter sharedInstance] defaultTransmissionTargetToken]); + XCTAssertTrue([[[MSACAppCenter sharedInstance] appSecret] isEqualToString:appSecret]); + XCTAssertTrue([MSACMockService sharedInstance].started); + XCTAssertTrue([MSACMockSecondService sharedInstance].started); + OCMVerify([self.channelUnitMock enqueueItem:[OCMArg isKindOfClass:MSACStartServiceLog.class] flags:MSACFlagsDefault]); +} + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST +- (void)testStartWithAppSecretAndTransmissionTokenForIos { + + // If + NSString *appSecret = MSAC_UUID_STRING; + NSString *transmissionTargetKey = @"target="; + NSString *appSecretKey = @"ios="; + NSString *transmissionTargetString = @"transmissionTargetToken"; + NSString *secret = [NSString stringWithFormat:@"%@%@;%@%@", appSecretKey, appSecret, transmissionTargetKey, transmissionTargetString]; + + // When + [MSACAppCenter start:secret withServices:@[ MSACMockService.class ]]; + [MSACAppCenter startService:MSACMockSecondService.class]; + + // Then + XCTAssertTrue([[[MSACAppCenter sharedInstance] appSecret] isEqualToString:appSecret]); + XCTAssertTrue([[[MSACAppCenter sharedInstance] defaultTransmissionTargetToken] isEqualToString:transmissionTargetString]); + XCTAssertTrue([MSACMockService sharedInstance].started); + XCTAssertTrue([[[MSACMockService sharedInstance] defaultTransmissionTargetToken] isEqualToString:transmissionTargetString]); + XCTAssertTrue([MSACMockSecondService sharedInstance].started); + XCTAssertTrue([[[MSACMockSecondService sharedInstance] defaultTransmissionTargetToken] isEqualToString:transmissionTargetString]); + OCMVerify([self.channelUnitMock enqueueItem:[OCMArg isKindOfClass:MSACStartServiceLog.class] flags:MSACFlagsDefault]); +} +#endif + +#if TARGET_OS_MACCATALYST +- (void)testStartWithAppSecretAndTransmissionTokenForMacCatalyst { + + // If + NSString *appSecret = MSAC_UUID_STRING; + NSString *transmissionTargetKey = @"target="; + NSString *appSecretKey = @"macos="; + NSString *transmissionTargetString = @"transmissionTargetToken"; + NSString *secret = [NSString stringWithFormat:@"%@%@;%@%@", appSecretKey, appSecret, transmissionTargetKey, transmissionTargetString]; + + // When + [MSACAppCenter start:secret withServices:@[ MSACMockService.class ]]; + [MSACAppCenter startService:MSACMockSecondService.class]; + + // Then + XCTAssertTrue([[[MSACAppCenter sharedInstance] appSecret] isEqualToString:appSecret]); + XCTAssertTrue([[[MSACAppCenter sharedInstance] defaultTransmissionTargetToken] isEqualToString:transmissionTargetString]); + XCTAssertTrue([MSACMockService sharedInstance].started); + XCTAssertTrue([[[MSACMockService sharedInstance] defaultTransmissionTargetToken] isEqualToString:transmissionTargetString]); + XCTAssertTrue([MSACMockSecondService sharedInstance].started); + XCTAssertTrue([[[MSACMockSecondService sharedInstance] defaultTransmissionTargetToken] isEqualToString:transmissionTargetString]); + OCMVerify([self.channelUnitMock enqueueItem:[OCMArg isKindOfClass:MSACStartServiceLog.class] flags:MSACFlagsDefault]); +} +#endif + +- (void)testStartWithAppSecretAndTransmissionToken { + + // If + NSString *appSecret = MSAC_UUID_STRING; + NSString *transmissionTargetKey = @"target="; + NSString *transmissionTargetString = @"transmissionTargetToken"; + NSString *secret = [NSString stringWithFormat:@"%@;%@%@", appSecret, transmissionTargetKey, transmissionTargetString]; + + // When + [MSACAppCenter start:secret withServices:@[ MSACMockService.class ]]; + [MSACAppCenter startService:MSACMockSecondService.class]; + + // Then + XCTAssertTrue([[[MSACAppCenter sharedInstance] appSecret] isEqualToString:appSecret]); + XCTAssertTrue([[[MSACAppCenter sharedInstance] defaultTransmissionTargetToken] isEqualToString:transmissionTargetString]); + XCTAssertTrue([MSACMockService sharedInstance].started); + XCTAssertTrue([[[MSACMockService sharedInstance] defaultTransmissionTargetToken] isEqualToString:transmissionTargetString]); + XCTAssertTrue([MSACMockSecondService sharedInstance].started); + XCTAssertTrue([[[MSACMockSecondService sharedInstance] defaultTransmissionTargetToken] isEqualToString:transmissionTargetString]); + OCMVerify([self.channelUnitMock enqueueItem:[OCMArg isKindOfClass:MSACStartServiceLog.class] flags:MSACFlagsDefault]); +} + +- (void)testStartWithNoAppSecret { + + // If + NSArray *services = @[ MSACMockService.class, MSACMockSecondService.class ]; + + // When + [MSACAppCenter startWithServices:services]; + + // Then + XCTAssertNil([[MSACAppCenter sharedInstance] appSecret]); + XCTAssertFalse([MSACMockService sharedInstance].started); + XCTAssertTrue([MSACMockSecondService sharedInstance].started); +} + +- (void)testStartWithTransmissionTokenOnly { + + // If + NSString *transmissionTargetKey = @"target="; + NSString *transmissionTargetString = @"transmissionTargetToken"; + NSString *secret = [NSString stringWithFormat:@"%@%@", transmissionTargetKey, transmissionTargetString]; + + // When + [MSACAppCenter start:secret withServices:@[ MSACMockService.class, MSACMockSecondService.class ]]; + + // Then + XCTAssertNil([[MSACAppCenter sharedInstance] appSecret]); + XCTAssertTrue([[[MSACAppCenter sharedInstance] defaultTransmissionTargetToken] isEqualToString:transmissionTargetString]); + XCTAssertFalse([MSACMockService sharedInstance].started); + XCTAssertTrue([MSACMockSecondService sharedInstance].started); +} + +- (void)testStartSameServiceFromLibraryAndThenApplication { + + // When + [MSACAppCenter startFromLibraryWithServices:@[ MSACMockSecondService.class ]]; + + // Then + XCTAssertNil([[MSACAppCenter sharedInstance] appSecret]); + XCTAssertFalse([MSACAppCenter isConfigured]); + XCTAssertNil([MSACMockSecondService sharedInstance].appSecret); + XCTAssertTrue([MSACMockSecondService sharedInstance].started); + + // When + [MSACAppCenter start:MSAC_UUID_STRING withServices:@[ MSACMockSecondService.class ]]; + + // Then + XCTAssertNotNil([[MSACAppCenter sharedInstance] appSecret]); + XCTAssertTrue([MSACAppCenter isConfigured]); + XCTAssertNotNil([MSACMockSecondService sharedInstance].appSecret); + XCTAssertTrue([MSACMockSecondService sharedInstance].started); +} + +- (void)testStartServicesFromLibraryAndThenApplication { + + // When + [MSACAppCenter startFromLibraryWithServices:@[ MSACMockSecondService.class ]]; + [MSACAppCenter start:MSAC_UUID_STRING withServices:@[ MSACMockService.class ]]; + + // Then + XCTAssertNotNil([[MSACAppCenter sharedInstance] appSecret]); + XCTAssertNotNil([MSACMockService sharedInstance].appSecret); + XCTAssertNil([MSACMockSecondService sharedInstance].appSecret); + XCTAssertTrue([MSACMockService sharedInstance].started); + XCTAssertTrue([MSACMockSecondService sharedInstance].started); +} + +- (void)testStartSameServiceFromApplicationAndThenLibrary { + + // When + [MSACAppCenter start:MSAC_UUID_STRING withServices:@[ MSACMockSecondService.class ]]; + + // Then + XCTAssertNotNil([[MSACAppCenter sharedInstance] appSecret]); + XCTAssertTrue([MSACAppCenter isConfigured]); + XCTAssertNotNil([MSACMockSecondService sharedInstance].appSecret); + XCTAssertTrue([MSACMockSecondService sharedInstance].started); + + // When + [MSACAppCenter startFromLibraryWithServices:@[ MSACMockSecondService.class ]]; + + // Then + XCTAssertNotNil([[MSACAppCenter sharedInstance] appSecret]); + XCTAssertTrue([MSACAppCenter isConfigured]); + XCTAssertNotNil([MSACMockSecondService sharedInstance].appSecret); + XCTAssertTrue([MSACMockSecondService sharedInstance].started); +} + +- (void)testStartServicesFromApplicationAndThenLibrary { + + // When + [MSACAppCenter start:MSAC_UUID_STRING withServices:@[ MSACMockService.class ]]; + [MSACAppCenter startFromLibraryWithServices:@[ MSACMockSecondService.class ]]; + + // Then + XCTAssertNotNil([[MSACAppCenter sharedInstance] appSecret]); + XCTAssertNotNil([MSACMockService sharedInstance].appSecret); + XCTAssertNil([MSACMockSecondService sharedInstance].appSecret); + XCTAssertTrue([MSACMockService sharedInstance].started); + XCTAssertTrue([MSACMockSecondService sharedInstance].started); +} + +- (void)testConfigureWithNoAppSecret { + + // When + [MSACAppCenter configure]; + + // Then + XCTAssertTrue([MSACAppCenter isConfigured]); +} + +- (void)testGetInstallIdFroMSACtorage { + + // If + // Expected installId is added to the storage. + [self.settingsMock setObject:kMSACInstallIdStringExample forKey:kMSACInstallIdKey]; + + // When + NSUUID *installId = self.sut.installId; + + // Then + assertThat(installId, is(MSAC_UUID_FROM_STRING(kMSACInstallIdStringExample))); + assertThat([installId UUIDString], is(kMSACInstallIdStringExample)); +} + +- (void)testGetInstallIdFromBadStorage { + + // If + // Unexpected installId is added to the storage. + [self.settingsMock setObject:MSAC_UUID_FROM_STRING(@"42") forKey:kMSACInstallIdKey]; + + // When + NSUUID *installId = self.sut.installId; + NSString *installIdString = [installId UUIDString]; + + // Then + assertThat(installId, notNilValue()); + assertThat(installIdString, notNilValue()); + assertThatInteger([installIdString length], greaterThan(@(0))); + assertThat(installIdString, isNot(kMSACNullifiedInstallIdString)); + assertThat([installId UUIDString], isNot(@"42")); +} + +- (void)testGetInstallIdTwice { + + // If + // InstallId is removed from the storage. + [self.settingsMock removeObjectForKey:kMSACInstallIdKey]; + + // When + NSUUID *installId1 = self.sut.installId; + NSString *installId1String = [installId1 UUIDString]; + + // Then + assertThat(installId1, notNilValue()); + assertThat(installId1String, notNilValue()); + assertThatInteger([installId1String length], greaterThan(@(0))); + assertThat(installId1String, isNot(kMSACNullifiedInstallIdString)); + + // When + // Second pick + NSUUID *installId2 = self.sut.installId; + + // Then + assertThat(installId1, is(installId2)); + assertThat([installId1 UUIDString], is([installId2 UUIDString])); +} + +- (void)testInstallIdPersistency { + + // If + // InstallId is removed from the storage. + [self.settingsMock removeObjectForKey:kMSACInstallIdKey]; + + // When + NSUUID *installId1 = self.sut.installId; + self.sut = [[MSACAppCenter alloc] init]; + NSUUID *installId2 = self.sut.installId; + + // Then + assertThat(installId1, is(installId2)); + assertThat([installId1 UUIDString], is([installId2 UUIDString])); +} + +- (void)testSetEnabled { + + // If + [MSACAppCenter start:MSAC_UUID_STRING withServices:@[ MSACMockService.class ]]; + + // When + [self.settingsMock setObject:@NO forKey:kMSACAppCenterIsEnabledKey]; + + // Then + XCTAssertFalse([MSACAppCenter isEnabled]); + + // When + [self.settingsMock setObject:@YES forKey:kMSACAppCenterIsEnabledKey]; + + // Then + XCTAssertTrue([MSACAppCenter isEnabled]); + + // When + [MSACAppCenter setEnabled:NO]; + + // Then + XCTAssertFalse([MSACAppCenter isEnabled]); + XCTAssertFalse([MSACMockService isEnabled]); + XCTAssertFalse(((NSNumber *)[self.settingsMock objectForKey:kMSACAppCenterIsEnabledKey]).boolValue); + OCMVerify([self.deviceTrackerMock clearDevices]); + OCMVerify([self.sessionContextMock clearSessionHistoryAndKeepCurrentSession:NO]); + + // When + [MSACAppCenter setEnabled:YES]; + + // Then + XCTAssertTrue([MSACAppCenter isEnabled]); + XCTAssertTrue([MSACMockService isEnabled]); + XCTAssertTrue(((NSNumber *)[self.settingsMock objectForKey:kMSACAppCenterIsEnabledKey]).boolValue); +} + +- (void)testClearUserIdHistoryWhenAppCenterIsDisabled { + + // If + [MSACAppCenter start:MSAC_UUID_STRING withServices:@[ MSACMockService.class ]]; + [[MSACUserIdContext sharedInstance] setUserId:@"alice"]; + [MSACUserIdContext resetSharedInstance]; + [[MSACUserIdContext sharedInstance] setUserId:@"bob"]; + + // Then + XCTAssertEqual(2, [[MSACUserIdContext sharedInstance].userIdHistory count]); + + // When + [MSACAppCenter setEnabled:NO]; + + // Then + XCTAssertFalse([MSACAppCenter isEnabled]); + + // Clearing history won't remove the most recent userId. + XCTAssertEqual(1, [[MSACUserIdContext sharedInstance].userIdHistory count]); +} + +- (void)testSetLogUrl { + + // If + NSString *fakeUrl = @"http://testUrl:1234"; + NSString *updateUrl = @"http://testUrlUpdate:1234"; + + // When + [MSACAppCenter setLogUrl:fakeUrl]; + [MSACAppCenter start:MSAC_UUID_STRING withServices:nil]; + + // Then + XCTAssertTrue([[[MSACAppCenter sharedInstance] logUrl] isEqualToString:fakeUrl]); + + // Cast to void to get rid of warning that says "Expression result unused". + OCMVerify((void)[self.channelGroupMock initWithHttpClient:OCMOCK_ANY installId:OCMOCK_ANY logUrl:equalTo(fakeUrl)]); + + // When + [MSACAppCenter setLogUrl:updateUrl]; + + // Then + OCMVerify([self.channelGroupMock setLogUrl:equalTo(updateUrl)]); +} + +- (void)testDefaultLogUrl { + + // If + NSString *defaultUrl = @"https://in.appcenter.ms"; + + // When + [MSACAppCenter start:MSAC_UUID_STRING withServices:nil]; + + // Then + XCTAssertNil([[MSACAppCenter sharedInstance] logUrl]); + + // Cast to void to get rid of warning that says "Expression result unused". + OCMVerify((void)[self.channelGroupMock initWithHttpClient:OCMOCK_ANY installId:OCMOCK_ANY logUrl:equalTo(defaultUrl)]); +} + +- (void)testDefaultLogUrlWithNoAppsecret { + NSString *defaultUrl = @"https://mobile.events.data.microsoft.com"; + + [MSACAppCenter startWithServices:nil]; + NSURL *endPointLogUrl = [[[[MSACAppCenter sharedInstance] oneCollectorChannelDelegate] oneCollectorIngestion] sendURL]; + XCTAssertTrue([[endPointLogUrl absoluteString] containsString:defaultUrl]); +} + +- (void)testSetLogUrlWithNoAppsecret { + NSString *fakeUrl = @"http://testUrl:1234"; + NSString *updateUrl = @"http://testUrlUpdate:1234"; + + [MSACAppCenter setLogUrl:fakeUrl]; + [MSACAppCenter startWithServices:nil]; + XCTAssertTrue([[[MSACAppCenter sharedInstance] logUrl] isEqualToString:fakeUrl]); + NSURL *endPointLogUrl = [[[[MSACAppCenter sharedInstance] oneCollectorChannelDelegate] oneCollectorIngestion] sendURL]; + XCTAssertTrue([[endPointLogUrl absoluteString] containsString:fakeUrl]); + + [MSACAppCenter setLogUrl:updateUrl]; + XCTAssertTrue([[[MSACAppCenter sharedInstance] logUrl] isEqualToString:updateUrl]); + endPointLogUrl = [[[[MSACAppCenter sharedInstance] oneCollectorChannelDelegate] oneCollectorIngestion] sendURL]; + XCTAssertTrue([[endPointLogUrl absoluteString] containsString:updateUrl]); +} + +- (void)testSdkVersion { + NSString *version = [NSString stringWithUTF8String:APP_CENTER_C_VERSION]; + XCTAssertTrue([[MSACAppCenter sdkVersion] isEqualToString:version]); +} + +- (void)testDisableServicesWithEnvironmentVariable { + const char *disableVariableCstr = [kMSACDisableVariable UTF8String]; + const char *disableAllCstr = [kMSACDisableAll UTF8String]; + + // If + setenv(disableVariableCstr, disableAllCstr, 1); + [[MSACMockService sharedInstance] setStarted:NO]; + [[MSACMockSecondService sharedInstance] setStarted:NO]; + + // When + [MSACAppCenter start:@"AppSecret" withServices:@[ MSACMockService.class, MSACMockSecondService.class ]]; + + // Then + XCTAssertFalse([MSACMockService sharedInstance].started); + XCTAssertFalse([MSACMockSecondService sharedInstance].started); + + // If + setenv(disableVariableCstr, [[MSACMockService serviceName] UTF8String], 1); + [[MSACMockService sharedInstance] setStarted:NO]; + [[MSACMockSecondService sharedInstance] setStarted:NO]; + [MSACAppCenter resetSharedInstance]; + + // When + [MSACAppCenter start:@"AppSecret" withServices:@[ MSACMockService.class, MSACMockSecondService.class ]]; + + // Then + XCTAssertFalse([MSACMockService sharedInstance].started); + XCTAssertTrue([MSACMockSecondService sharedInstance].started); + + // If + NSString *disableList = + [NSString stringWithFormat:@"%@,SomeService,%@", [MSACMockService serviceName], [MSACMockSecondService serviceName]]; + setenv(disableVariableCstr, [disableList UTF8String], 1); + [[MSACMockService sharedInstance] setStarted:NO]; + [[MSACMockSecondService sharedInstance] setStarted:NO]; + [MSACAppCenter resetSharedInstance]; + + // When + [MSACAppCenter start:@"AppSecret" withServices:@[ MSACMockService.class, MSACMockSecondService.class ]]; + + // Then + XCTAssertFalse([MSACMockService sharedInstance].started); + XCTAssertFalse([MSACMockSecondService sharedInstance].started); + + // Repeat previous test but with some whitespace. + // If + disableList = [NSString stringWithFormat:@" %@ , SomeService,%@ ", [MSACMockService serviceName], [MSACMockSecondService serviceName]]; + setenv(disableVariableCstr, [disableList UTF8String], 1); + [[MSACMockService sharedInstance] setStarted:NO]; + [[MSACMockSecondService sharedInstance] setStarted:NO]; + [MSACAppCenter resetSharedInstance]; + + // When + [MSACAppCenter start:@"AppSecret" withServices:@[ MSACMockService.class, MSACMockSecondService.class ]]; + + // Then + XCTAssertFalse([MSACMockService sharedInstance].started); + XCTAssertFalse([MSACMockSecondService sharedInstance].started); + + // Special tear down. + setenv(disableVariableCstr, "", 1); +} + +- (void)testIsRunningInAppCenterTestCloudWithEnvironmentVariable { + + // If + const char *isRunningVariableCstr = [kMSACRunningInAppCenter UTF8String]; + const char *isRunningCstr = [kMSACTrueEnvironmentString UTF8String]; + setenv(isRunningVariableCstr, isRunningCstr, 1); + + // Then + XCTAssertTrue([MSACAppCenter isRunningInAppCenterTestCloud]); + + // If + setenv(isRunningVariableCstr, "", 1); + + // Then + XCTAssertFalse([MSACAppCenter isRunningInAppCenterTestCloud]); +} + +#if !TARGET_OS_TV +- (void)testSetCustomPropertiesWithEmptyPropertiesDoesNotEnqueueCustomPropertiesLog { + + // If + [MSACAppCenter start:MSAC_UUID_STRING withServices:nil]; + id channelUnit = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([channelUnit enqueueItem:[OCMArg isKindOfClass:[MSACCustomPropertiesLog class]] flags:MSACFlagsDefault]).andDo(nil); + [MSACAppCenter sharedInstance].channelUnit = channelUnit; + + // When + OCMReject([channelUnit enqueueItem:[OCMArg isKindOfClass:[MSACCustomPropertiesLog class]] flags:MSACFlagsDefault]); + MSACCustomProperties *customProperties = [MSACCustomProperties new]; + [MSACAppCenter setCustomProperties:customProperties]; + + // Then + OCMVerifyAll(channelUnit); +} + +- (void)testSetCustomProperties { + + // If + [MSACAppCenter start:MSAC_UUID_STRING withServices:nil]; + id channelUnit = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([channelUnit enqueueItem:[OCMArg isKindOfClass:[MSACCustomPropertiesLog class]] flags:MSACFlagsDefault]).andDo(nil); + [MSACAppCenter sharedInstance].channelUnit = channelUnit; + + // When + MSACCustomProperties *customProperties = [MSACCustomProperties new]; + [customProperties setString:@"test" forKey:@"test"]; + [MSACAppCenter setCustomProperties:customProperties]; + + // Then + OCMVerify([channelUnit enqueueItem:[OCMArg isKindOfClass:[MSACCustomPropertiesLog class]] flags:MSACFlagsDefault]); + + // When + // Not allow processLog more + OCMReject([channelUnit enqueueItem:[OCMArg isKindOfClass:[MSACCustomPropertiesLog class]] flags:MSACFlagsDefault]); + [MSACAppCenter setCustomProperties:nil]; + [MSACAppCenter setCustomProperties:[MSACCustomProperties new]]; + + // Then + OCMVerifyAll(channelUnit); +} +#endif + +- (void)testConfigureWithAppSecret { + [MSACAppCenter configureWithAppSecret:@"App-Secret"]; + XCTAssertTrue([MSACAppCenter isConfigured]); +} + +- (void)testConfigureWithAppSecretAndTransmissionToken { + + // If + NSString *appSecret = MSAC_UUID_STRING; + NSString *transmissionTargetKey = @"target="; + NSString *transmissionTargetString = @"transmissionTargetToken"; + NSString *secret = [NSString stringWithFormat:@"%@;%@%@", appSecret, transmissionTargetKey, transmissionTargetString]; + + // When + [MSACAppCenter configureWithAppSecret:secret]; + + // Then + XCTAssertTrue([MSACAppCenter isConfigured]); + XCTAssertTrue([[[MSACAppCenter sharedInstance] appSecret] isEqualToString:appSecret]); + XCTAssertTrue([[[MSACAppCenter sharedInstance] defaultTransmissionTargetToken] isEqualToString:transmissionTargetString]); +} + +- (void)testStartServiceWithInvalidValues { + NSUInteger servicesCount = [[MSACAppCenter sharedInstance] services].count; + [MSACAppCenter startService:[MSACAppCenter class]]; + [MSACAppCenter startService:[NSString class]]; + [MSACAppCenter startService:nil]; + XCTAssertEqual(servicesCount, [[MSACAppCenter sharedInstance] services].count); +} + +- (void)testStartServiceWithoutAppSecret { + [MSACAppCenter startService:[MSACMockService class]]; + XCTAssertEqual((uint)0, [[MSACAppCenter sharedInstance] services].count); + [MSACAppCenter startService:[MSACMockSecondService class]]; + XCTAssertEqual((uint)0, [[MSACAppCenter sharedInstance] services].count); +} + +- (void)testStartWithoutServices { + + // Not allow processLog. + OCMReject([self.channelUnitMock enqueueItem:[OCMArg isKindOfClass:[MSACStartServiceLog class]] flags:MSACFlagsDefault]); + + // When + [MSACAppCenter start:MSAC_UUID_STRING withServices:nil]; + + // Then + OCMVerifyAll(self.channelUnitMock); +} + +- (void)testStartServiceLogIsSentAfterStartService { + + // If + [MSACAppCenter configureWithAppSecret:MSAC_UUID_STRING]; + id channelUnit = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([channelUnit enqueueItem:[OCMArg isKindOfClass:[MSACStartServiceLog class]] flags:MSACFlagsDefault]).andDo(nil); + [MSACAppCenter sharedInstance].channelUnit = channelUnit; + + // When + [MSACAppCenter startService:MSACMockService.class]; + + // Then + OCMVerify([channelUnit enqueueItem:[OCMArg isKindOfClass:[MSACStartServiceLog class]] flags:MSACFlagsDefault]); +} + +- (void)testDisabledCoreStatus { + + // When + [MSACAppCenter start:MSAC_UUID_STRING withServices:@[ MSACMockService.class ]]; + [MSACAppCenter setEnabled:NO]; + + // Then + XCTAssertFalse([MSACMockService isEnabled]); + OCMVerify([self.channelGroupMock setEnabled:NO andDeleteDataOnDisabled:YES]); +} + +- (void)testDisabledCorePersistedStatus { + + // If + [self.settingsMock setObject:@NO forKey:kMSACAppCenterIsEnabledKey]; + + // When + [MSACAppCenter start:MSAC_UUID_STRING withServices:@[ MSACMockService.class ]]; + + // Then + XCTAssertFalse([MSACMockService isEnabled]); + OCMVerify([self.channelGroupMock setEnabled:NO andDeleteDataOnDisabled:YES]); +} + +- (void)testStartServiceLogWithDisabledCore { + + // If + __block NSInteger logsProcessed = 0; + __block MSACStartServiceLog *log = nil; + OCMStub([self.channelUnitMock enqueueItem:[OCMArg isKindOfClass:[MSACStartServiceLog class]] flags:MSACFlagsDefault]) + .andDo(^(NSInvocation *invocation) { + [invocation getArgument:&log atIndex:2]; + logsProcessed++; + }); + + // When + [MSACAppCenter start:MSAC_UUID_STRING withServices:nil]; + [MSACAppCenter setEnabled:NO]; + [MSACAppCenter startService:MSACMockService.class]; + [MSACAppCenter startService:MSACMockSecondService.class]; + + // Then + assertThatInteger(logsProcessed, equalToInteger(0)); + XCTAssertFalse([MSACMockService isEnabled]); + XCTAssertFalse([MSACMockSecondService isEnabled]); + XCTAssertNil(log); + + // When + [MSACAppCenter setEnabled:YES]; + + // Then + assertThatInteger(logsProcessed, equalToInteger(1)); + XCTAssertNotNil(log); + NSArray *expected = @[ @"MSMockService", @"MSMockSecondService" ]; + XCTAssertTrue([log.services isEqual:expected]); +} + +- (void)testSortingServicesWorks { + + // If + id mockServiceMaxPrio = OCMProtocolMock(@protocol(MSACServiceCommon)); + OCMStub([mockServiceMaxPrio sharedInstance]).andReturn(mockServiceMaxPrio); + OCMStub([mockServiceMaxPrio initializationPriority]).andReturn(MSACInitializationPriorityMax); + + id mockServiceDefaultPrio = OCMProtocolMock(@protocol(MSACServiceCommon)); + OCMStub([mockServiceDefaultPrio sharedInstance]).andReturn(mockServiceDefaultPrio); + OCMStub([mockServiceDefaultPrio initializationPriority]).andReturn(MSACInitializationPriorityDefault); + + // When + NSArray *sorted = [self.sut sortServices:@[ (Class)mockServiceDefaultPrio, (Class)mockServiceMaxPrio ]]; + + // Then + XCTAssertTrue([sorted[0] initializationPriority] == MSACInitializationPriorityMax); + XCTAssertTrue([sorted[1] initializationPriority] == MSACInitializationPriorityDefault); +} + +- (void)testChannelOneCollectorDelegateSet { + + // When + [MSACAppCenter start:MSAC_UUID_STRING withServices:nil]; + + // Then + OCMVerify([self.channelGroupMock addDelegate:[OCMArg isKindOfClass:[MSACOneCollectorChannelDelegate class]]]); +} + +#if !TARGET_OS_OSX && !TARGET_OS_MACCATALYST +- (void)testAppIsBackgrounded { + + // If + id channelGroup = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + [self.sut configureWithAppSecret:@"AppSecret" transmissionTargetToken:nil fromApplication:YES]; + self.sut.channelGroup = channelGroup; + + // When + [[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationDidEnterBackgroundNotification object:self.sut]; + // Then + OCMVerify([channelGroup pauseWithIdentifyingObject:self.sut]); +} + +- (void)testAppIsForegrounded { + + // If + id channelGroup = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + [self.sut configureWithAppSecret:@"AppSecret" transmissionTargetToken:nil fromApplication:YES]; + self.sut.channelGroup = channelGroup; + + // When + [[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationWillEnterForegroundNotification + + object:self.sut]; + // Then + OCMVerify([channelGroup resumeWithIdentifyingObject:self.sut]); +} +#endif + +- (void)testSetStorageSizeSetsProperties { + + // If + long dbSize = 2 * 1024 * 1024; + void (^completionBlock)(BOOL) = ^(__unused BOOL success) { + }; + + // When + [MSACAppCenter setMaxStorageSize:dbSize completionHandler:completionBlock]; + + // Then + XCTAssertNotNil([MSACAppCenter sharedInstance].requestedMaxStorageSizeInBytes); + XCTAssertEqualObjects(@(dbSize), [MSACAppCenter sharedInstance].requestedMaxStorageSizeInBytes); + XCTAssertNotNil([MSACAppCenter sharedInstance].maxStorageSizeCompletionHandler); + XCTAssertEqual(completionBlock, [MSACAppCenter sharedInstance].maxStorageSizeCompletionHandler); +} + +- (void)testSetStorageHandlerCannotBeCalledAfterStart { + + // If + [MSACAppCenter start:MSAC_UUID_STRING withServices:nil]; + long dbSize = 2 * 1024 * 1024; + + // When + [MSACAppCenter setMaxStorageSize:dbSize + completionHandler:^(BOOL success) { + // Then + XCTAssertFalse(success); + }]; +} + +- (void)testSetStorageHandlerCanOnlyBeCalledOnce { + + // If + long dbSize = 2 * 1024 * 1024; + + // When + [MSACAppCenter setMaxStorageSize:dbSize + completionHandler:^(__unused BOOL success){ + }]; + [MSACAppCenter setMaxStorageSize:dbSize + 1 + completionHandler:^(__unused BOOL success){ + }]; + + // Then + XCTAssertEqual(dbSize, [[MSACAppCenter sharedInstance].requestedMaxStorageSizeInBytes longValue]); +} + +- (void)testSetStorageSizeBelowMaximumLogSizeFails { + + // If + XCTestExpectation *expectation = [self expectationWithDescription:@"Completion handler invoked."]; + + // When + [MSACAppCenter setMaxStorageSize:10 + completionHandler:^(BOOL success) { + // Then + XCTAssertFalse(success); + [expectation fulfill]; + }]; + + // Then + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testSetValidUserIdForAppCenter { + + // If + NSString *userId = @"user123"; + + // When + [MSACAppCenter setUserId:userId]; + + // Then + XCTAssertNil([[MSACUserIdContext sharedInstance] userId]); + + // When + [MSACAppCenter startFromLibraryWithServices:@[ MSACMockService.class ]]; + [MSACAppCenter setUserId:userId]; + + // Then + XCTAssertNil([[MSACUserIdContext sharedInstance] userId]); + + // When + [MSACAppCenter configureWithAppSecret:@"AppSecret"]; + [MSACAppCenter setUserId:userId]; + + // Then + XCTAssertEqual([[MSACUserIdContext sharedInstance] userId], userId); + + // When + [MSACAppCenter setUserId:nil]; + + // Then + XCTAssertNil([[MSACUserIdContext sharedInstance] userId]); +} + +- (void)testSetUserIdWithoutSecret { + + // If + NSString *userId = @"user123"; + + // When + [MSACAppCenter configure]; + [MSACAppCenter setUserId:userId]; + + // Then + XCTAssertNil([[MSACUserIdContext sharedInstance] userId]); +} + +- (void)testSetInvalidUserIdForAppCenter { + + // If + NSString *userId = @""; + for (int i = 0; i < 257; i++) { + userId = [userId stringByAppendingString:@"x"]; + } + [MSACAppCenter configureWithAppSecret:@"AppSecret"]; + + // When + [MSACAppCenter setUserId:userId]; + + // Then + XCTAssertNil([[MSACUserIdContext sharedInstance] userId]); +} + +- (void)testSetInvalidUserIdForTransmissionTarget { + + // If + [MSACAppCenter configureWithAppSecret:@"target=transmissionTargetToken"]; + + // When + // Set an empty userId + [MSACAppCenter setUserId:@""]; + + // Then + XCTAssertNil([[MSACUserIdContext sharedInstance] userId]); + + // When + // Set another empty userId + [MSACAppCenter setUserId:@"c:"]; + + // Then + XCTAssertNil([[MSACUserIdContext sharedInstance] userId]); + + // When + // Set a userId with invalid prefix + [MSACAppCenter setUserId:@"foobar:alice"]; + + // Then + XCTAssertNil([[MSACUserIdContext sharedInstance] userId]); + + // When + // Set a valid userId without prefix + [MSACAppCenter setUserId:@"alice"]; + + // Then + XCTAssertEqual([[MSACUserIdContext sharedInstance] userId], @"alice"); + + // When + // Set a valid userId with prefix c: + [MSACAppCenter setUserId:@"c:alice"]; + + // Then + XCTAssertEqual([[MSACUserIdContext sharedInstance] userId], @"c:alice"); + + // When + // Set a userId with invalid prefix again + [MSACAppCenter setUserId:@"foobar:alice"]; + + // Then + // Current userId shouldn't be overridden by the invalid one. + XCTAssertEqual([[MSACUserIdContext sharedInstance] userId], @"c:alice"); +} + +- (void)testNoUserIdWhenSetUserIdIsNotCalledInNextVersion { + + // If + // An app calls setUserId in version 1. + __block NSDate *date; + NSMutableArray *history = [NSMutableArray new]; + [history addObject:[[MSACUserIdHistoryInfo alloc] initWithTimestamp:[NSDate dateWithTimeIntervalSince1970:0] andUserId:@"alice"]]; + [history addObject:[[MSACUserIdHistoryInfo alloc] initWithTimestamp:[NSDate dateWithTimeIntervalSince1970:3000] andUserId:@"bob"]]; + [self.settingsMock setObject:[MSACUtility archiveKeyedData:history] forKey:@"UserIdHistory"]; + [MSACUserIdContext resetSharedInstance]; + + // When + // setUserId call is removed in version 2. + id dateMock = OCMClassMock([NSDate class]); + OCMStub(ClassMethod([dateMock date])).andDo(^(NSInvocation *invocation) { + date = [[NSDate alloc] initWithTimeIntervalSince1970:4000]; + [invocation setReturnValue:&date]; + }); + [MSACAppCenter configureWithAppSecret:@"AppSecret"]; + [dateMock stopMocking]; + + // Then + XCTAssertNil([[MSACUserIdContext sharedInstance] userIdAt:[NSDate dateWithTimeIntervalSince1970:5000]]); + + // When + // Version 2 app launched again. + [MSACUserIdContext resetSharedInstance]; + dateMock = OCMClassMock([NSDate class]); + OCMStub(ClassMethod([dateMock date])).andDo(^(NSInvocation *invocation) { + date = [[NSDate alloc] initWithTimeIntervalSince1970:7000]; + [invocation setReturnValue:&date]; + }); + [MSACAppCenter configureWithAppSecret:@"AppSecret"]; + [dateMock stopMocking]; + + // Then + XCTAssertNil([[MSACUserIdContext sharedInstance] userIdAt:[NSDate dateWithTimeIntervalSince1970:5000]]); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACAppDelegateForwarderTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACAppDelegateForwarderTests.m new file mode 100644 index 0000000000..5a5e76f8ff --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACAppDelegateForwarderTests.m @@ -0,0 +1,792 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAppDelegateForwarder.h" +#import "MSACAppDelegateUtil.h" +#import "MSACDelegateForwarderPrivate.h" +#import "MSACDelegateForwarderTestUtil.h" +#import "MSACTestFrameworks.h" +#import "MSACUtility+Application.h" + +@interface MSACAppDelegateForwarderTest : XCTestCase + +@property(nonatomic) MSACApplication *appMock; +@property(nonatomic) MSACAppDelegateForwarder *sut; + +@end + +/* + * We use of blocks for test validition but test frameworks contain macro capturing self that we can't avoid. Ignoring retain cycle warning + * for this test code. + */ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-retain-cycles" + +// Silence application:openURL:options: availability warning (iOS 9) for the whole test. +#pragma clang diagnostic ignored "-Wpartial-availability" + +// Silence application:openURL:sourceApplication:annotation: deprecation warning (iOS 9) for the whole test. +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +@implementation MSACAppDelegateForwarderTest + +- (void)setUp { + [super setUp]; + + // The app delegate forwarder is already set via the load method, reset it for testing. + [MSACAppDelegateForwarder resetSharedInstance]; + self.sut = [MSACAppDelegateForwarder sharedInstance]; + + // Mock app delegate. + self.appMock = OCMClassMock([MSACApplication class]); +} + +- (void)tearDown { + [super tearDown]; + [MSACAppDelegateForwarder resetSharedInstance]; +} + +- (void)testSetEnabledYesFromPlist { + + // If + id bundleMock = OCMClassMock([NSBundle class]); + OCMStub([bundleMock objectForInfoDictionaryKey:kMSACAppDelegateForwarderEnabledKey]).andReturn(@YES); + OCMStub([bundleMock mainBundle]).andReturn(bundleMock); + + // When + [[self.sut class] load]; + + // Then + assertThatBool(self.sut.enabled, isTrue()); +} + +- (void)testSetEnabledNoFromPlist { + + // If + id bundleMock = OCMClassMock([NSBundle class]); + OCMStub([bundleMock objectForInfoDictionaryKey:kMSACAppDelegateForwarderEnabledKey]).andReturn(@NO); + OCMStub([bundleMock mainBundle]).andReturn(bundleMock); + + // When + [[self.sut class] load]; + + // Then + assertThatBool(self.sut.enabled, isFalse()); +} + +- (void)testSetEnabledNoneFromPlist { + + // If + id bundleMock = OCMClassMock([NSBundle class]); + OCMStub([bundleMock objectForInfoDictionaryKey:kMSACAppDelegateForwarderEnabledKey]).andReturn(nil); + OCMStub([bundleMock mainBundle]).andReturn(bundleMock); + + // When + [[self.sut class] load]; + + // Then + assertThatBool(self.sut.enabled, isTrue()); +} + +- (void)testAddAppDelegateSelectorToSwizzle { + + // If + NSUInteger currentCount = self.sut.selectorsToSwizzle.count; + SEL expectedSelector = @selector(testAddAppDelegateSelectorToSwizzle); + NSString *expectedSelectorStr = NSStringFromSelector(expectedSelector); + + // Then + assertThatBool([self.sut.selectorsToSwizzle containsObject:expectedSelectorStr], isFalse()); + + // When + [self.sut addDelegateSelectorToSwizzle:expectedSelector]; + + // Then + assertThatInteger(self.sut.selectorsToSwizzle.count, equalToUnsignedInteger(currentCount + 1)); + assertThatBool([self.sut.selectorsToSwizzle containsObject:expectedSelectorStr], isTrue()); + + // When + [self.sut addDelegateSelectorToSwizzle:expectedSelector]; + + // Then + assertThatInteger(self.sut.selectorsToSwizzle.count, equalToUnsignedInteger(currentCount + 1)); + assertThatBool([self.sut.selectorsToSwizzle containsObject:expectedSelectorStr], isTrue()); + [self.sut.selectorsToSwizzle removeObject:expectedSelectorStr]; +} + +#if !TARGET_OS_OSX && !TARGET_OS_MACCATALYST +- (void)testSwizzleOriginalOpenURLDelegate { + + // If + + // Mock a custom app delegate. + id customDelegate = OCMProtocolMock(@protocol(MSACCustomApplicationDelegate)); + [self.sut addDelegate:customDelegate]; + NSURL *expectedURL = [NSURL URLWithString:@"https://www.contoso.com/sending-positive-waves"]; + NSDictionary *expectedOptions = @{}; + + // App delegate not implementing any selector. + id originalAppDelegate = [self createOriginalAppDelegateInstance]; + SEL selectorToSwizzle = @selector(application:openURL:options:); + [self.sut addDelegateSelectorToSwizzle:selectorToSwizzle]; + + // When + [self.sut swizzleOriginalDelegate:originalAppDelegate]; + [originalAppDelegate application:self.appMock openURL:expectedURL options:expectedOptions]; + + // Then + assertThatBool([originalAppDelegate respondsToSelector:selectorToSwizzle], isTrue()); + OCMVerify([customDelegate application:self.appMock openURL:expectedURL options:expectedOptions returnedValue:NO]); + + // If + // App delegate implementing the selector directly. + originalAppDelegate = [self createOriginalAppDelegateInstance]; + __block BOOL wasCalled = NO; + id selectorImp = ^{ + wasCalled = YES; + return YES; + }; + [MSACDelegateForwarderTestUtil addSelector:selectorToSwizzle implementation:selectorImp toInstance:originalAppDelegate]; + [self.sut addDelegateSelectorToSwizzle:selectorToSwizzle]; + + // When + [self.sut swizzleOriginalDelegate:originalAppDelegate]; + [originalAppDelegate application:self.appMock openURL:expectedURL options:expectedOptions]; + + // Then + assertThatBool([originalAppDelegate respondsToSelector:selectorToSwizzle], isTrue()); + assertThatBool(wasCalled, isTrue()); + OCMVerify([customDelegate application:self.appMock openURL:expectedURL options:expectedOptions returnedValue:YES]); + + // If + // App delegate implementing the selector indirectly. + id originalBaseAppDelegate = [self createOriginalAppDelegateInstance]; + [MSACDelegateForwarderTestUtil addSelector:selectorToSwizzle implementation:selectorImp toInstance:originalBaseAppDelegate]; + originalAppDelegate = [MSACDelegateForwarderTestUtil createInstanceWithBaseClass:[originalBaseAppDelegate class] + andConformItToProtocol:nil]; + wasCalled = NO; + [self.sut addDelegateSelectorToSwizzle:selectorToSwizzle]; + + // When + [self.sut swizzleOriginalDelegate:originalAppDelegate]; + [originalAppDelegate application:self.appMock openURL:expectedURL options:expectedOptions]; + + // Then + assertThatBool([originalAppDelegate respondsToSelector:selectorToSwizzle], isTrue()); + assertThatBool(wasCalled, isTrue()); + OCMVerify([customDelegate application:self.appMock openURL:expectedURL options:expectedOptions returnedValue:YES]); + + // If + // App delegate implementing the selector directly and indirectly. + wasCalled = NO; + __block BOOL baseWasCalled = NO; + id baseSelectorImp = ^{ + baseWasCalled = YES; + }; + originalBaseAppDelegate = [self createOriginalAppDelegateInstance]; + [MSACDelegateForwarderTestUtil addSelector:selectorToSwizzle implementation:baseSelectorImp toInstance:originalBaseAppDelegate]; + originalAppDelegate = [MSACDelegateForwarderTestUtil createInstanceWithBaseClass:[originalBaseAppDelegate class] + andConformItToProtocol:nil]; + [MSACDelegateForwarderTestUtil addSelector:selectorToSwizzle implementation:selectorImp toInstance:originalAppDelegate]; + [self.sut addDelegateSelectorToSwizzle:selectorToSwizzle]; + + // When + [self.sut swizzleOriginalDelegate:originalAppDelegate]; + [originalAppDelegate application:self.appMock openURL:expectedURL options:expectedOptions]; + + // Then + assertThatBool([originalAppDelegate respondsToSelector:selectorToSwizzle], isTrue()); + assertThatBool(wasCalled, isTrue()); + assertThatBool(baseWasCalled, isFalse()); + OCMVerify([customDelegate application:self.appMock openURL:expectedURL options:expectedOptions returnedValue:YES]); + + // If + // App delegate not implementing any selector still responds to selector. + originalAppDelegate = [self createOriginalAppDelegateInstance]; + SEL instancesRespondToSelector = @selector(instancesRespondToSelector:); + id instancesRespondToSelectorImp = ^{ + return YES; + }; + + // Adding a class method to a class requires its meta class. + [MSACDelegateForwarderTestUtil addSelector:instancesRespondToSelector + implementation:instancesRespondToSelectorImp + toClass:object_getClass([originalAppDelegate class])]; + [self.sut addDelegateSelectorToSwizzle:selectorToSwizzle]; + + // When + [self.sut swizzleOriginalDelegate:originalAppDelegate]; + + // Then + // Original delegate still responding to selector. + assertThatBool([[originalAppDelegate class] instancesRespondToSelector:selectorToSwizzle], isTrue()); + + // Swizzling did not happened so no method added/replaced for this selector. + assertThatBool(class_getInstanceMethod([originalAppDelegate class], selectorToSwizzle) == NULL, isTrue()); +} +#endif + +- (void)testForwardUnknownSelector { + + // If + // Calling an unknown selector on the forwarder must still throw an exception. + XCTestExpectation *exceptionCaughtExpectation = [self expectationWithDescription:@"Caught!! That exception will go nowhere."]; + + // When + @try { + [self.sut performSelector:@selector(testForwardUnknownSelector)]; + } @catch (NSException *ex) { + + // Then + assertThat(ex.name, is(NSInvalidArgumentException)); + assertThatBool([ex.reason containsString:@"unrecognized selector sent"], isTrue()); + [exceptionCaughtExpectation fulfill]; + } + [self waitForExpectations:@[ exceptionCaughtExpectation ] timeout:1]; +} + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST +- (void)testWithoutCustomDelegate { + + // If + NSURL *expectedURL = [NSURL URLWithString:@"https://www.contoso.com/sending-positive-waves"]; + NSDictionary *expectedOptions = @{}; + BOOL expectedReturnedValue = YES; + MSACApplication *appMock = self.appMock; + XCTestExpectation *originalCalledExpectation = [self expectationWithDescription:@"Original delegate called."]; + SEL originalOpenURLSel = @selector(application:openURL:options:); + [self.sut addDelegateSelectorToSwizzle:originalOpenURLSel]; + id originalAppDelegate = [self createOriginalAppDelegateInstance]; + id originalOpenURLImp = ^(__attribute__((unused)) id itSelf, MSACApplication *application, NSURL *url, id options) { + // Then + assertThat(application, is(appMock)); + assertThat(url, is(expectedURL)); + assertThat(options, is(expectedOptions)); + [originalCalledExpectation fulfill]; + return expectedReturnedValue; + }; + [MSACDelegateForwarderTestUtil addSelector:originalOpenURLSel implementation:originalOpenURLImp toInstance:originalAppDelegate]; + + // When + BOOL returnedValue = [originalAppDelegate application:self.appMock openURL:expectedURL options:expectedOptions]; + + // Then + assertThatUnsignedLong(self.sut.delegates.count, equalToUnsignedLong(0)); + assertThatBool(returnedValue, is(@(expectedReturnedValue))); + [self waitForExpectations:@[ originalCalledExpectation ] timeout:1]; +} +#endif + +- (void)testWithoutCustomDelegateNotReturningValue { + + // If + NSData *expectedToken = [@"Device token" dataUsingEncoding:NSUTF8StringEncoding]; + MSACApplication *appMock = self.appMock; + XCTestExpectation *originalCalledExpectation = [self expectationWithDescription:@"Original delegate called."]; + SEL originalDidRegisterForRemoteNotificationsWithDeviceTokenSel = @selector(application: + didRegisterForRemoteNotificationsWithDeviceToken:); + [self.sut addDelegateSelectorToSwizzle:originalDidRegisterForRemoteNotificationsWithDeviceTokenSel]; + id originalAppDelegate = [self createOriginalAppDelegateInstance]; + id originalDidRegisterForRemoteNotificationsWithDeviceTokenImp = + ^(__attribute__((unused)) id itSelf, MSACApplication *application, NSData *deviceToken) { + // Then + assertThat(application, is(appMock)); + assertThat(deviceToken, is(expectedToken)); + [originalCalledExpectation fulfill]; + }; + [MSACDelegateForwarderTestUtil addSelector:originalDidRegisterForRemoteNotificationsWithDeviceTokenSel + implementation:originalDidRegisterForRemoteNotificationsWithDeviceTokenImp + toInstance:originalAppDelegate]; + + // When + [originalAppDelegate application:self.appMock didRegisterForRemoteNotificationsWithDeviceToken:expectedToken]; + + // Then + assertThatUnsignedLong(self.sut.delegates.count, equalToUnsignedLong(0)); + [self waitForExpectations:@[ originalCalledExpectation ] timeout:1]; +} + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST +- (void)testWithOneCustomDelegate { + + // If + NSURL *expectedURL = [NSURL URLWithString:@"https://www.contoso.com/sending-positive-waves"]; + NSDictionary *expectedOptions = @{}; + BOOL expectedReturnedValue = YES; + MSACApplication *appMock = self.appMock; + XCTestExpectation *originalCalledExpectation = [self expectationWithDescription:@"Original delegate called."]; + XCTestExpectation *customCalledExpectation = [self expectationWithDescription:@"Custom delegate called."]; + SEL originalOpenURLiOS90Sel = @selector(application:openURL:options:); + [self.sut addDelegateSelectorToSwizzle:originalOpenURLiOS90Sel]; + id originalAppDelegate = [self createOriginalAppDelegateInstance]; + id originalOpenURLiOS90Imp = ^(__attribute__((unused)) id itSelf, MSACApplication *application, NSURL *url, id options) { + // Then + assertThat(application, is(appMock)); + assertThat(url, is(expectedURL)); + assertThat(options, is(expectedOptions)); + [originalCalledExpectation fulfill]; + return expectedReturnedValue; + }; + [MSACDelegateForwarderTestUtil addSelector:originalOpenURLiOS90Sel implementation:originalOpenURLiOS90Imp toInstance:originalAppDelegate]; + SEL customOpenURLiOS90Sel = @selector(application:openURL:options:returnedValue:); + id customAppDelegate = [self createCustomAppDelegateInstance]; + id customOpenURLiOS90Imp = + ^(__attribute__((unused)) id itSelf, MSACApplication *application, NSURL *url, id options, BOOL returnedValue) { + // Then + assertThat(application, is(appMock)); + assertThat(url, is(expectedURL)); + assertThat(options, is(expectedOptions)); + assertThatBool(returnedValue, is(@(expectedReturnedValue))); + [customCalledExpectation fulfill]; + return expectedReturnedValue; + }; + [MSACDelegateForwarderTestUtil addSelector:customOpenURLiOS90Sel implementation:customOpenURLiOS90Imp toInstance:customAppDelegate]; + [self.sut addDelegate:customAppDelegate]; + [self.sut swizzleOriginalDelegate:originalAppDelegate]; + + // When + BOOL returnedValue = [originalAppDelegate application:self.appMock openURL:expectedURL options:expectedOptions]; + + // Then + assertThatBool(returnedValue, is(@(expectedReturnedValue))); + [self waitForExpectations:@[ originalCalledExpectation, customCalledExpectation ] timeout:1]; +} +#endif + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST +- (void)testWithMultipleCustomOpenURLDelegates { + + // If + NSURL *expectedURL = [NSURL URLWithString:@"https://www.contoso.com/sending-positive-waves"]; + NSDictionary *expectedOptions = @{}; + BOOL expectedReturnedValue = YES; + XCTestExpectation *originalCalledExpectation = [self expectationWithDescription:@"Original delegate called."]; + XCTestExpectation *customCalledExpectation1 = [self expectationWithDescription:@"Custom delegate 1 called."]; + XCTestExpectation *customCalledExpectation2 = [self expectationWithDescription:@"Custom delegate 2 called."]; + MSACApplication *appMock = self.appMock; + SEL originalOpenURLiOS90Sel = @selector(application:openURL:options:); + [self.sut addDelegateSelectorToSwizzle:originalOpenURLiOS90Sel]; + id originalAppDelegate = [self createOriginalAppDelegateInstance]; + id originalOpenURLiOS90Imp = ^(__attribute__((unused)) id itSelf, MSACApplication *application, NSURL *url, id options) { + // Then + assertThat(application, is(appMock)); + assertThat(url, is(expectedURL)); + assertThat(options, is(expectedOptions)); + [originalCalledExpectation fulfill]; + return expectedReturnedValue; + }; + [MSACDelegateForwarderTestUtil addSelector:originalOpenURLiOS90Sel implementation:originalOpenURLiOS90Imp toInstance:originalAppDelegate]; + SEL customOpenURLiOS90Sel = @selector(application:openURL:options:returnedValue:); + id customAppDelegate1 = [self createCustomAppDelegateInstance]; + id customOpenURLiOS90Imp1 = + ^(__attribute__((unused)) id itSelf, MSACApplication *application, NSURL *url, id options, BOOL returnedValue) { + // Then + assertThat(application, is(appMock)); + assertThat(url, is(expectedURL)); + assertThat(options, is(expectedOptions)); + assertThatBool(returnedValue, is(@(expectedReturnedValue))); + [customCalledExpectation1 fulfill]; + return expectedReturnedValue; + }; + [MSACDelegateForwarderTestUtil addSelector:customOpenURLiOS90Sel implementation:customOpenURLiOS90Imp1 toInstance:customAppDelegate1]; + id customAppDelegate2 = [self createCustomAppDelegateInstance]; + id customOpenURLiOS90Imp2 = + ^(__attribute__((unused)) id itSelf, MSACApplication *application, NSURL *url, id options, BOOL returnedValue) { + // Then + assertThat(application, is(appMock)); + assertThat(url, is(expectedURL)); + assertThat(options, is(expectedOptions)); + assertThatBool(returnedValue, is(@(expectedReturnedValue))); + [customCalledExpectation2 fulfill]; + return expectedReturnedValue; + }; + [MSACDelegateForwarderTestUtil addSelector:customOpenURLiOS90Sel implementation:customOpenURLiOS90Imp2 toInstance:customAppDelegate2]; + [self.sut addDelegate:customAppDelegate1]; + [self.sut addDelegate:customAppDelegate2]; + [self.sut swizzleOriginalDelegate:originalAppDelegate]; + + // When + BOOL returnedValue = [originalAppDelegate application:self.appMock openURL:expectedURL options:expectedOptions]; + + // Then + assertThatBool(returnedValue, is(@(expectedReturnedValue))); + [self waitForExpectations:@[ originalCalledExpectation, customCalledExpectation1, customCalledExpectation2 ] timeout:1]; +} +#endif + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST +- (void)testWithRemovedCustomOpenURLDelegate { + + // If + NSURL *expectedURL = [NSURL URLWithString:@"https://www.contoso.com/sending-positive-waves"]; + NSDictionary *expectedAnnotation = @{}; + BOOL expectedReturnedValue = YES; + MSACApplication *appMock = self.appMock; + XCTestExpectation *originalCalledExpectation = [self expectationWithDescription:@"Original delegate called."]; + SEL originalOpenURLiOS42Sel = @selector(application:openURL:sourceApplication:annotation:); + [self.sut addDelegateSelectorToSwizzle:originalOpenURLiOS42Sel]; + id originalAppDelegate = [self createOriginalAppDelegateInstance]; + id originalOpenURLiOS42Imp = + ^(__attribute__((unused)) id itSelf, MSACApplication *application, NSURL *url, NSString *sApplication, id annotation) { + // Then + assertThat(application, is(appMock)); + assertThat(url, is(expectedURL)); + assertThat(sApplication, nilValue()); + assertThat(annotation, is(expectedAnnotation)); + [originalCalledExpectation fulfill]; + return expectedReturnedValue; + }; + [MSACDelegateForwarderTestUtil addSelector:originalOpenURLiOS42Sel implementation:originalOpenURLiOS42Imp toInstance:originalAppDelegate]; + SEL customOpenURLiOS42Sel = @selector(application:openURL:sourceApplication:annotation:returnedValue:); + id customAppDelegate = [self createCustomAppDelegateInstance]; + id customOpenURLiOS42Imp = + ^(__attribute__((unused)) id itSelf, __attribute__((unused)) MSACApplication *application, __attribute__((unused)) NSURL *url, + __attribute__((unused)) NSString *sApplication, __attribute__((unused)) id annotation, __attribute__((unused)) BOOL returnedValue) { + // Then + XCTFail(@"Custom delegate got called but is removed."); + return expectedReturnedValue; + }; + [MSACDelegateForwarderTestUtil addSelector:customOpenURLiOS42Sel implementation:customOpenURLiOS42Imp toInstance:customAppDelegate]; + [self.sut addDelegate:customAppDelegate]; + [self.sut removeDelegate:customAppDelegate]; + + // When + BOOL returnedValue = [originalAppDelegate application:self.appMock + openURL:expectedURL + sourceApplication:nil + annotation:expectedAnnotation]; + + // Then + assertThatBool(returnedValue, is(@(expectedReturnedValue))); + [self waitForExpectations:@[ originalCalledExpectation ] timeout:1]; +} +#endif + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST +- (void)testDontForwardOpenURLOnDisable { + + // If + NSURL *expectedURL = [NSURL URLWithString:@"https://www.contoso.com/sending-positive-waves"]; + NSDictionary *expectedAnnotation = @{}; + BOOL expectedReturnedValue = YES; + MSACApplication *appMock = self.appMock; + XCTestExpectation *originalCalledExpectation = [self expectationWithDescription:@"Original delegate called."]; + SEL originalOpenURLiOS42Sel = @selector(application:openURL:sourceApplication:annotation:); + [self.sut addDelegateSelectorToSwizzle:originalOpenURLiOS42Sel]; + id originalAppDelegate = [self createOriginalAppDelegateInstance]; + id originalOpenURLiOS42Imp = + ^(__attribute__((unused)) id itSelf, MSACApplication *application, NSURL *url, NSString *sApplication, id annotation) { + // Then + assertThat(application, is(appMock)); + assertThat(url, is(expectedURL)); + assertThat(sApplication, nilValue()); + assertThat(annotation, is(expectedAnnotation)); + [originalCalledExpectation fulfill]; + return expectedReturnedValue; + }; + [MSACDelegateForwarderTestUtil addSelector:originalOpenURLiOS42Sel implementation:originalOpenURLiOS42Imp toInstance:originalAppDelegate]; + SEL customOpenURLiOS42Sel = @selector(application:openURL:sourceApplication:annotation:returnedValue:); + id customAppDelegate = [self createCustomAppDelegateInstance]; + id customOpenURLiOS42Imp = + ^(__attribute__((unused)) id itSelf, __attribute__((unused)) MSACApplication *application, __attribute__((unused)) NSURL *url, + __attribute__((unused)) NSString *sApplication, __attribute__((unused)) id annotation, __attribute__((unused)) BOOL returnedValue) { + // Then + XCTFail(@"Custom delegate got called but is removed."); + return expectedReturnedValue; + }; + [MSACDelegateForwarderTestUtil addSelector:customOpenURLiOS42Sel implementation:customOpenURLiOS42Imp toInstance:customAppDelegate]; + [self.sut addDelegate:customAppDelegate]; + self.sut.enabled = NO; + + // When + BOOL returnedValue = [originalAppDelegate application:self.appMock + openURL:expectedURL + sourceApplication:nil + annotation:expectedAnnotation]; + + // Then + assertThatBool(returnedValue, is(@(expectedReturnedValue))); + [self waitForExpectations:@[ originalCalledExpectation ] timeout:1]; + self.sut.enabled = YES; +} +#endif + +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST +- (void)testReturnValueChaining { + + // If + NSURL *expectedURL = [NSURL URLWithString:@"https://www.contoso.com/sending-positive-waves"]; + NSDictionary *expectedAnnotation = @{}; + BOOL initialReturnValue = YES; + __block BOOL expectedReturnedValue; + XCTestExpectation *originalCalledExpectation = [self expectationWithDescription:@"Original delegate called."]; + XCTestExpectation *customCalledExpectation1 = [self expectationWithDescription:@"Custom delegate 1 called."]; + XCTestExpectation *customCalledExpectation2 = [self expectationWithDescription:@"Custom delegate 2 called."]; + MSACApplication *appMock = self.appMock; + SEL originalOpenURLiOS42Sel = @selector(application:openURL:sourceApplication:annotation:); + [self.sut addDelegateSelectorToSwizzle:originalOpenURLiOS42Sel]; + id originalAppDelegate = [self createOriginalAppDelegateInstance]; + id originalOpenURLiOS42Imp = + ^(__attribute__((unused)) id itSelf, MSACApplication *application, NSURL *url, NSString *sApplication, id annotation) { + // Then + assertThat(application, is(appMock)); + assertThat(url, is(expectedURL)); + assertThat(sApplication, nilValue()); + assertThat(annotation, is(expectedAnnotation)); + [originalCalledExpectation fulfill]; + expectedReturnedValue = initialReturnValue; + return expectedReturnedValue; + }; + [MSACDelegateForwarderTestUtil addSelector:originalOpenURLiOS42Sel implementation:originalOpenURLiOS42Imp toInstance:originalAppDelegate]; + SEL customOpenURLiOS42Sel = @selector(application:openURL:sourceApplication:annotation:returnedValue:); + id customAppDelegate1 = [self createCustomAppDelegateInstance]; + id customOpenURLiOS42Imp1 = ^(__attribute__((unused)) id itSelf, MSACApplication *application, NSURL *url, NSString *sApplication, + id annotation, BOOL returnedValue) { + // Then + assertThat(application, is(appMock)); + assertThat(url, is(expectedURL)); + assertThat(sApplication, nilValue()); + assertThat(annotation, is(expectedAnnotation)); + assertThatBool(returnedValue, is(@(expectedReturnedValue))); + expectedReturnedValue = !returnedValue; + [customCalledExpectation1 fulfill]; + return expectedReturnedValue; + }; + [MSACDelegateForwarderTestUtil addSelector:customOpenURLiOS42Sel implementation:customOpenURLiOS42Imp1 toInstance:customAppDelegate1]; + id customAppDelegate2 = [self createCustomAppDelegateInstance]; + id customOpenURLiOS42Imp2 = ^(__attribute__((unused)) id itSelf, MSACApplication *application, NSURL *url, NSString *sApplication, + id annotation, BOOL returnedValue) { + // Then + assertThat(application, is(appMock)); + assertThat(url, is(expectedURL)); + assertThat(sApplication, nilValue()); + assertThat(annotation, is(expectedAnnotation)); + assertThatBool(returnedValue, is(@(expectedReturnedValue))); + expectedReturnedValue = !returnedValue; + [customCalledExpectation2 fulfill]; + return expectedReturnedValue; + }; + [MSACDelegateForwarderTestUtil addSelector:customOpenURLiOS42Sel implementation:customOpenURLiOS42Imp2 toInstance:customAppDelegate2]; + [self.sut addDelegate:customAppDelegate1]; + [self.sut addDelegate:customAppDelegate2]; + [self.sut swizzleOriginalDelegate:originalAppDelegate]; + + // When + BOOL returnedValue = [originalAppDelegate application:self.appMock + openURL:expectedURL + sourceApplication:nil + annotation:expectedAnnotation]; + + // Then + assertThatBool(returnedValue, is(@(expectedReturnedValue))); + [self waitForExpectations:@[ originalCalledExpectation, customCalledExpectation1, customCalledExpectation2 ] timeout:1]; +} + +- (void)testOpenURLMethodNotImplementedByOriginalDelegate { + + // If + NSURL *expectedURL = [NSURL URLWithString:@"https://www.contoso.com/sending-positive-waves"]; + NSDictionary *expectedOptions = @{}; + BOOL expectedReturnedValue = YES; + MSACApplication *appMock = self.appMock; + XCTestExpectation *customCalledExpectation = [self expectationWithDescription:@"Custom delegate called."]; + SEL originalOpenURLiOS90Sel = @selector(application:openURL:options:); + [self.sut addDelegateSelectorToSwizzle:originalOpenURLiOS90Sel]; + id originalAppDelegate = [self createOriginalAppDelegateInstance]; + SEL customOpenURLiOS90Sel = @selector(application:openURL:options:returnedValue:); + id customAppDelegate = [self createCustomAppDelegateInstance]; + id customOpenURLiOS90Imp = + ^(__attribute__((unused)) id itSelf, MSACApplication *application, NSURL *url, id options, BOOL returnedValue) { + // Then + assertThat(application, is(appMock)); + assertThat(url, is(expectedURL)); + assertThat(options, is(expectedOptions)); + assertThatBool(returnedValue, is(@(NO))); + [customCalledExpectation fulfill]; + return expectedReturnedValue; + }; + [MSACDelegateForwarderTestUtil addSelector:customOpenURLiOS90Sel implementation:customOpenURLiOS90Imp toInstance:customAppDelegate]; + [self.sut addDelegate:customAppDelegate]; + [self.sut swizzleOriginalDelegate:originalAppDelegate]; + + // When + BOOL returnedValue = [originalAppDelegate application:self.appMock openURL:expectedURL options:expectedOptions]; + + // Then + [self waitForExpectations:@[ customCalledExpectation ] timeout:1]; + assertThatBool(returnedValue, is(@(expectedReturnedValue))); +} + +- (void)testDontSwizzleDeprecatedAPIIfNoAPIImplemented { + + // If + // Mock a custom app delegate. + id customDelegate = OCMProtocolMock(@protocol(MSACCustomApplicationDelegate)); + [self.sut addDelegate:customDelegate]; + NSURL *expectedURL = [NSURL URLWithString:@"https://www.contoso.com/sending-positive-waves"]; + id expectedOptions = @{}; + OCMExpect([customDelegate application:self.appMock openURL:expectedURL options:expectedOptions returnedValue:NO]); + + // App delegate not implementing any API. + SEL deprecatedSelector = @selector(application:openURL:sourceApplication:annotation:); + SEL newSelector = @selector(application:openURL:options:); + id originalAppDelegate = [self createOriginalAppDelegateInstance]; + [self.sut addDelegateSelectorToSwizzle:deprecatedSelector]; + [self.sut addDelegateSelectorToSwizzle:newSelector]; + + // When + [self.sut swizzleOriginalDelegate:originalAppDelegate]; + [originalAppDelegate application:self.appMock openURL:expectedURL options:expectedOptions]; + + // Then + assertThatBool([originalAppDelegate respondsToSelector:newSelector], isTrue()); + assertThatBool([originalAppDelegate respondsToSelector:deprecatedSelector], isFalse()); + OCMVerify([customDelegate application:self.appMock openURL:expectedURL options:expectedOptions returnedValue:NO]); +} + +- (void)testSwizzleDeprecatedAPIIfNoNewAPIImplemented { + + // If + // Mock a custom app delegate. + id customDelegate = OCMProtocolMock(@protocol(MSACCustomApplicationDelegate)); + [self.sut addDelegate:customDelegate]; + NSURL *expectedURL = [NSURL URLWithString:@"https://www.contoso.com/sending-positive-waves"]; + id expectedAnotation = @{}; + OCMExpect([customDelegate application:self.appMock + openURL:expectedURL + sourceApplication:nil + annotation:expectedAnotation + returnedValue:YES]); + + // App delegate implementing just the deprecated API. + SEL deprecatedSelector = @selector(application:openURL:sourceApplication:annotation:); + SEL newSelector = @selector(application:openURL:options:); + id originalAppDelegate = [self createOriginalAppDelegateInstance]; + __block short nbCalls = 0; + id selectorImp = ^{ + nbCalls++; + return YES; + }; + [MSACDelegateForwarderTestUtil addSelector:deprecatedSelector implementation:selectorImp toInstance:originalAppDelegate]; + [self.sut addDelegateSelectorToSwizzle:deprecatedSelector]; + [self.sut addDelegateSelectorToSwizzle:newSelector]; + + // When + [self.sut swizzleOriginalDelegate:originalAppDelegate]; + [originalAppDelegate application:self.appMock openURL:expectedURL sourceApplication:nil annotation:expectedAnotation]; + + // Then + assertThatBool([originalAppDelegate respondsToSelector:newSelector], isFalse()); + assertThatBool([originalAppDelegate respondsToSelector:deprecatedSelector], isTrue()); + assertThatShort(nbCalls, equalToShort(1)); + OCMVerify([customDelegate application:self.appMock + openURL:expectedURL + sourceApplication:nil + annotation:expectedAnotation + returnedValue:YES]); +} + +- (void)testSwizzleDeprecatedAPIIfJustNewAPIImplemented { + + // If + // Mock a custom app delegate. + id customDelegate = OCMProtocolMock(@protocol(MSACCustomApplicationDelegate)); + [self.sut addDelegate:customDelegate]; + NSURL *expectedURL = [NSURL URLWithString:@"https://www.contoso.com/sending-positive-waves"]; + id expectedOptions = @{}; + OCMExpect([customDelegate application:self.appMock openURL:expectedURL options:expectedOptions returnedValue:YES]); + + // App delegate implementing just the new API. + SEL deprecatedSelector = @selector(application:openURL:sourceApplication:annotation:); + SEL newSelector = @selector(application:openURL:options:); + id originalAppDelegate = [self createOriginalAppDelegateInstance]; + __block short nbCalls = 0; + id selectorImp = ^{ + nbCalls++; + return YES; + }; + [MSACDelegateForwarderTestUtil addSelector:newSelector implementation:selectorImp toInstance:originalAppDelegate]; + [self.sut addDelegateSelectorToSwizzle:deprecatedSelector]; + [self.sut addDelegateSelectorToSwizzle:newSelector]; + + // When + [self.sut swizzleOriginalDelegate:originalAppDelegate]; + [originalAppDelegate application:self.appMock openURL:expectedURL options:expectedOptions]; + + // Then + assertThatBool([originalAppDelegate respondsToSelector:deprecatedSelector], isFalse()); + assertThatBool([originalAppDelegate respondsToSelector:newSelector], isTrue()); + assertThatShort(nbCalls, equalToShort(1)); + OCMVerify([customDelegate application:self.appMock openURL:expectedURL options:expectedOptions returnedValue:YES]); +} + +- (void)testSwizzleDeprecatedAPIIfAllAPIsImplemented { + + // If + // Mock a custom app delegate. + id customDelegate = OCMProtocolMock(@protocol(MSACCustomApplicationDelegate)); + [self.sut addDelegate:customDelegate]; + NSURL *expectedURL = [NSURL URLWithString:@"https://www.contoso.com/sending-positive-waves"]; + id expectedAnotation = @{}; + id expectedOptions = @{}; + OCMExpect([customDelegate application:self.appMock openURL:expectedURL options:expectedOptions returnedValue:YES]); + OCMExpect([customDelegate application:self.appMock + openURL:expectedURL + sourceApplication:nil + annotation:expectedAnotation + returnedValue:YES]); + + // App delegate implementing all the APIs. + SEL deprecatedSelector = @selector(application:openURL:sourceApplication:annotation:); + SEL newSelector = @selector(application:openURL:options:); + id originalAppDelegate = [self createOriginalAppDelegateInstance]; + __block short deprecatedSelectorNbCalls = 0; + __block short newSelectorNbCalls = 0; + id deprecatedSelectorImp = ^{ + deprecatedSelectorNbCalls++; + return YES; + }; + id newSelectorImp = ^{ + newSelectorNbCalls++; + return YES; + }; + [MSACDelegateForwarderTestUtil addSelector:deprecatedSelector implementation:deprecatedSelectorImp toInstance:originalAppDelegate]; + [MSACDelegateForwarderTestUtil addSelector:newSelector implementation:newSelectorImp toInstance:originalAppDelegate]; + [self.sut addDelegateSelectorToSwizzle:deprecatedSelector]; + [self.sut addDelegateSelectorToSwizzle:newSelector]; + + // When + [self.sut swizzleOriginalDelegate:originalAppDelegate]; + [originalAppDelegate application:self.appMock openURL:expectedURL sourceApplication:nil annotation:expectedAnotation]; + [originalAppDelegate application:self.appMock openURL:expectedURL options:expectedOptions]; + + // Then + assertThatBool([originalAppDelegate respondsToSelector:newSelector], isTrue()); + assertThatBool([originalAppDelegate respondsToSelector:deprecatedSelector], isTrue()); + assertThatShort(newSelectorNbCalls, equalToShort(1)); + assertThatShort(deprecatedSelectorNbCalls, equalToShort(1)); + OCMVerify([customDelegate application:self.appMock openURL:expectedURL options:expectedOptions returnedValue:YES]); + OCMVerify([customDelegate application:self.appMock + openURL:expectedURL + sourceApplication:nil + annotation:expectedAnotation + returnedValue:YES]); +} + +#endif + +#pragma mark - helper + +- (id)createOriginalAppDelegateInstance { + return [MSACDelegateForwarderTestUtil createInstanceConformingToProtocol:@protocol(MSACApplicationDelegate)]; +} + +- (id)createCustomAppDelegateInstance { + return [MSACDelegateForwarderTestUtil createInstanceConformingToProtocol:@protocol(MSACCustomApplicationDelegate)]; +} + +@end + +#pragma clang diagnostic pop diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACBooleanTypedPropertyTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACBooleanTypedPropertyTests.m new file mode 100644 index 0000000000..fb7f6a9e27 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACBooleanTypedPropertyTests.m @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACBooleanTypedProperty.h" +#import "MSACTestFrameworks.h" +#import "MSACUtility.h" + +@interface MSACBooleanTypedPropertyTests : XCTestCase + +@end + +@implementation MSACBooleanTypedPropertyTests + +- (void)testSerializeToDictionary { + + // If + MSACBooleanTypedProperty *sut = [MSACBooleanTypedProperty new]; + sut.name = @"propertyName"; + sut.value = YES; + + // When + NSDictionary *dictionary = [sut serializeToDictionary]; + + // Then + XCTAssertEqualObjects(dictionary[@"type"], sut.type); + XCTAssertEqualObjects(dictionary[@"name"], sut.name); + XCTAssertEqual([dictionary[@"value"] boolValue], sut.value); +} + +- (void)testNSCodingSerializationAndDeserialization { + + // If + MSACBooleanTypedProperty *sut = [MSACBooleanTypedProperty new]; + sut.type = @"type"; + sut.name = @"name"; + sut.value = YES; + + // When + NSData *serializedProperty = [MSACUtility archiveKeyedData:sut]; + MSACBooleanTypedProperty *actual = [MSACUtility unarchiveKeyedData:serializedProperty]; + + // Then + XCTAssertNotNil(actual); + XCTAssertTrue([actual isKindOfClass:[MSACBooleanTypedProperty class]]); + XCTAssertEqualObjects(actual.name, sut.name); + XCTAssertEqualObjects(actual.type, sut.type); + XCTAssertEqual(actual.value, sut.value); +} + +- (void)testPropertyTypeIsCorrectWhenPropertyIsInitialized { + + // If + MSACBooleanTypedProperty *sut = [MSACBooleanTypedProperty new]; + + // Then + XCTAssertEqualObjects(sut.type, @"boolean"); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACCSExtensionsTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACCSExtensionsTests.m new file mode 100644 index 0000000000..5e78e9ea4d --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACCSExtensionsTests.m @@ -0,0 +1,842 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAppExtension.h" +#import "MSACCSData.h" +#import "MSACCSExtensions.h" +#import "MSACDeviceExtension.h" +#import "MSACLocExtension.h" +#import "MSACMetadataExtension.h" +#import "MSACModelTestsUtililty.h" +#import "MSACNetExtension.h" +#import "MSACOSExtension.h" +#import "MSACOrderedDictionaryPrivate.h" +#import "MSACProtocolExtension.h" +#import "MSACSDKExtension.h" +#import "MSACTestFrameworks.h" +#import "MSACUserExtension.h" +#import "MSACUtility.h" + +@interface MSACCSExtensionsTests : XCTestCase +@property(nonatomic) MSACCSExtensions *ext; +@property(nonatomic) NSMutableDictionary *extDummyValues; +@property(nonatomic) MSACUserExtension *userExt; +@property(nonatomic) NSDictionary *userExtDummyValues; +@property(nonatomic) MSACLocExtension *locExt; +@property(nonatomic) NSDictionary *locExtDummyValues; +@property(nonatomic) MSACOSExtension *osExt; +@property(nonatomic) NSDictionary *osExtDummyValues; +@property(nonatomic) MSACAppExtension *appExt; +@property(nonatomic) NSDictionary *appExtDummyValues; +@property(nonatomic) MSACProtocolExtension *protocolExt; +@property(nonatomic) NSDictionary *protocolExtDummyValues; +@property(nonatomic) MSACNetExtension *netExt; +@property(nonatomic) NSDictionary *netExtDummyValues; +@property(nonatomic) MSACSDKExtension *sdkExt; +@property(nonatomic) NSMutableDictionary *sdkExtDummyValues; +@property(nonatomic) MSACDeviceExtension *deviceExt; +@property(nonatomic) NSMutableDictionary *deviceExtDummyValues; +@property(nonatomic) MSACMetadataExtension *metadataExt; +@property(nonatomic) NSDictionary *metadataExtDummyValues; +@property(nonatomic) MSACCSData *data; +@property(nonatomic) NSDictionary *orderedDummyValues; +@property(nonatomic) NSDictionary *unorderedDummyValues; + +@end + +@implementation MSACCSExtensionsTests + +- (void)setUp { + [super setUp]; + + // Set up all extensions with dummy values. + self.userExtDummyValues = [MSACModelTestsUtililty userExtensionDummies]; + self.userExt = [MSACModelTestsUtililty userExtensionWithDummyValues:self.userExtDummyValues]; + self.locExtDummyValues = [MSACModelTestsUtililty locExtensionDummies]; + ; + self.locExt = [MSACModelTestsUtililty locExtensionWithDummyValues:self.locExtDummyValues]; + self.osExtDummyValues = [MSACModelTestsUtililty osExtensionDummies]; + self.osExt = [MSACModelTestsUtililty osExtensionWithDummyValues:self.osExtDummyValues]; + self.appExtDummyValues = [MSACModelTestsUtililty appExtensionDummies]; + self.appExt = [MSACModelTestsUtililty appExtensionWithDummyValues:self.appExtDummyValues]; + self.protocolExtDummyValues = [MSACModelTestsUtililty protocolExtensionDummies]; + self.protocolExt = [MSACModelTestsUtililty protocolExtensionWithDummyValues:self.protocolExtDummyValues]; + self.netExtDummyValues = [MSACModelTestsUtililty netExtensionDummies]; + self.netExt = [MSACModelTestsUtililty netExtensionWithDummyValues:self.netExtDummyValues]; + self.sdkExtDummyValues = [MSACModelTestsUtililty sdkExtensionDummies]; + self.sdkExt = [MSACModelTestsUtililty sdkExtensionWithDummyValues:self.sdkExtDummyValues]; + self.deviceExtDummyValues = [MSACModelTestsUtililty deviceExtensionDummies]; + self.deviceExt = [MSACModelTestsUtililty deviceExtensionWithDummyValues:self.deviceExtDummyValues]; + self.metadataExtDummyValues = [MSACModelTestsUtililty metadataExtensionDummies]; + self.metadataExt = [MSACModelTestsUtililty metadataExtensionWithDummyValues:self.metadataExtDummyValues]; + self.orderedDummyValues = [MSACModelTestsUtililty orderedDataDummies]; + self.unorderedDummyValues = [MSACModelTestsUtililty unorderedDataDummies]; + self.data = [MSACModelTestsUtililty dataWithDummyValues:self.unorderedDummyValues]; + self.extDummyValues = [MSACModelTestsUtililty extensionDummies]; + self.ext = [MSACModelTestsUtililty extensionsWithDummyValues:self.extDummyValues]; +} + +- (void)tearDown { + [super tearDown]; +} + +#pragma mark - MSACCSExtensions + +- (void)testExtJSONSerializingToDictionary { + + // When + NSMutableDictionary *dict = [self.ext serializeToDictionary]; + + // Then + XCTAssertNotNil(dict); + XCTAssertEqualObjects(dict[kMSACCSAppExt], [self.extDummyValues[kMSACCSAppExt] serializeToDictionary]); + XCTAssertEqualObjects(dict[kMSACCSNetExt], [self.extDummyValues[kMSACCSNetExt] serializeToDictionary]); + XCTAssertEqualObjects(dict[kMSACCSLocExt], [self.extDummyValues[kMSACCSLocExt] serializeToDictionary]); + XCTAssertEqualObjects(dict[kMSACCSSDKExt], [self.extDummyValues[kMSACCSSDKExt] serializeToDictionary]); + XCTAssertEqualObjects(dict[kMSACCSUserExt], [self.extDummyValues[kMSACCSUserExt] serializeToDictionary]); + XCTAssertEqualObjects(dict[kMSACCSProtocolExt], [self.extDummyValues[kMSACCSProtocolExt] serializeToDictionary]); + XCTAssertEqualObjects(dict[kMSACCSOSExt], [self.extDummyValues[kMSACCSOSExt] serializeToDictionary]); + XCTAssertEqualObjects(dict[kMSACCSDeviceExt], [self.extDummyValues[kMSACCSDeviceExt] serializeToDictionary]); + XCTAssertEqualObjects(dict[kMSACCSMetadataExt], [self.extDummyValues[kMSACCSMetadataExt] serializeToDictionary]); +} + +- (void)testExtNSCodingSerializationAndDeserialization { + + // When + NSData *serializedExt = [MSACUtility archiveKeyedData:self.ext]; + MSACCSExtensions *actualExt = [MSACUtility unarchiveKeyedData:serializedExt]; + + // Then + XCTAssertNotNil(actualExt); + XCTAssertEqualObjects(self.ext, actualExt); + XCTAssertTrue([actualExt isMemberOfClass:[MSACCSExtensions class]]); + XCTAssertEqualObjects(actualExt.metadataExt, self.extDummyValues[kMSACCSMetadataExt]); + XCTAssertEqualObjects(actualExt.userExt, self.extDummyValues[kMSACCSUserExt]); + XCTAssertEqualObjects(actualExt.locExt, self.extDummyValues[kMSACCSLocExt]); + XCTAssertEqualObjects(actualExt.appExt, self.extDummyValues[kMSACCSAppExt]); + XCTAssertEqualObjects(actualExt.protocolExt, self.extDummyValues[kMSACCSProtocolExt]); + XCTAssertEqualObjects(actualExt.osExt, self.extDummyValues[kMSACCSOSExt]); + XCTAssertEqualObjects(actualExt.netExt, self.extDummyValues[kMSACCSNetExt]); + XCTAssertEqualObjects(actualExt.sdkExt, self.extDummyValues[kMSACCSSDKExt]); +} + +- (void)testExtIsValid { + + // If + MSACCSExtensions *ext = [MSACCSExtensions new]; + + // Then + XCTAssertTrue([ext isValid]); +} + +- (void)testExtIsEqual { + + // If + MSACCSExtensions *anotherExt = [MSACCSExtensions new]; + + // Then + XCTAssertNotEqualObjects(anotherExt, self.ext); + + // If + anotherExt = [MSACModelTestsUtililty extensionsWithDummyValues:self.extDummyValues]; + + // Then + XCTAssertEqualObjects(anotherExt, self.ext); + + // If + anotherExt.metadataExt = OCMClassMock([MSACMetadataExtension class]); + + // Then + XCTAssertNotEqualObjects(anotherExt, self.ext); + + // If + anotherExt.metadataExt = self.extDummyValues[kMSACCSMetadataExt]; + anotherExt.userExt = OCMClassMock([MSACUserExtension class]); + + // Then + XCTAssertNotEqualObjects(anotherExt, self.ext); + + // If + anotherExt.userExt = self.extDummyValues[kMSACCSUserExt]; + anotherExt.locExt = OCMClassMock([MSACLocExtension class]); + + // Then + XCTAssertNotEqualObjects(anotherExt, self.ext); + + // If + anotherExt.locExt = self.extDummyValues[kMSACCSLocExt]; + anotherExt.osExt = OCMClassMock([MSACOSExtension class]); + + // Then + XCTAssertNotEqualObjects(anotherExt, self.ext); + + // If + anotherExt.osExt = self.extDummyValues[kMSACCSOSExt]; + anotherExt.appExt = OCMClassMock([MSACAppExtension class]); + + // Then + XCTAssertNotEqualObjects(anotherExt, self.ext); + + // If + anotherExt.appExt = self.extDummyValues[kMSACCSAppExt]; + anotherExt.protocolExt = OCMClassMock([MSACProtocolExtension class]); + + // Then + XCTAssertNotEqualObjects(anotherExt, self.ext); + + // If + anotherExt.protocolExt = self.extDummyValues[kMSACCSProtocolExt]; + anotherExt.netExt = OCMClassMock([MSACNetExtension class]); + + // Then + XCTAssertNotEqualObjects(anotherExt, self.ext); + + // If + anotherExt.netExt = self.extDummyValues[kMSACCSNetExt]; + anotherExt.sdkExt = OCMClassMock([MSACSDKExtension class]); + + // Then + XCTAssertNotEqualObjects(anotherExt, self.ext); +} + +#pragma mark - MSACMetadataExtension + +- (void)testMetadataExtJSONSerializingToDictionary { + + // When + NSMutableDictionary *dict = [self.metadataExt serializeToDictionary]; + + // Then + XCTAssertNotNil(dict); + XCTAssertEqualObjects(dict, self.metadataExtDummyValues); +} + +- (void)testMetadataExtNSCodingSerializationAndDeserialization { + + // When + NSData *serializedMetadataExt = [MSACUtility archiveKeyedData:self.metadataExt]; + MSACMetadataExtension *actualMetadataExt = (MSACMetadataExtension *)[MSACUtility unarchiveKeyedData:serializedMetadataExt]; + + // Then + XCTAssertNotNil(actualMetadataExt); + XCTAssertEqualObjects(self.metadataExt, actualMetadataExt); + XCTAssertTrue([actualMetadataExt isMemberOfClass:[MSACMetadataExtension class]]); + XCTAssertEqualObjects(actualMetadataExt.metadata, self.metadataExtDummyValues); +} + +- (void)testMetadataExtIsValid { + + // If + MSACMetadataExtension *metadataExt = [MSACMetadataExtension new]; + + // Then + XCTAssertTrue([metadataExt isValid]); +} + +- (void)testMetadataExtIsEqual { + + // If + MSACMetadataExtension *anotherMetadataExt = [MSACMetadataExtension new]; + + // Then + XCTAssertNotEqualObjects(anotherMetadataExt, self.metadataExt); + + // If + anotherMetadataExt = [MSACModelTestsUtililty metadataExtensionWithDummyValues:self.metadataExtDummyValues]; + + // Then + XCTAssertEqualObjects(anotherMetadataExt, self.metadataExt); + + // If + anotherMetadataExt.metadata = @{}; + + // Then + XCTAssertNotEqualObjects(anotherMetadataExt, self.metadataExt); +} + +#pragma mark - MSACUserExtension + +- (void)testUserExtJSONSerializingToDictionary { + + // When + NSMutableDictionary *dict = [self.userExt serializeToDictionary]; + + // Then + XCTAssertNotNil(dict); + XCTAssertEqualObjects(dict[kMSACUserLocalId], self.userExtDummyValues[kMSACUserLocalId]); + XCTAssertEqualObjects(dict[kMSACUserLocale], self.userExtDummyValues[kMSACUserLocale]); +} + +- (void)testUserExtNSCodingSerializationAndDeserialization { + + // When + NSData *serializedUserExt = [MSACUtility archiveKeyedData:self.userExt]; + MSACUserExtension *actualUserExt = (MSACUserExtension *)[MSACUtility unarchiveKeyedData:serializedUserExt]; + + // Then + XCTAssertNotNil(actualUserExt); + XCTAssertEqualObjects(self.userExt, actualUserExt); + XCTAssertTrue([actualUserExt isMemberOfClass:[MSACUserExtension class]]); + XCTAssertEqualObjects(actualUserExt.localId, self.userExtDummyValues[kMSACUserLocalId]); + XCTAssertEqualObjects(actualUserExt.locale, self.userExtDummyValues[kMSACUserLocale]); +} + +- (void)testUserExtIsValid { + + // If + MSACUserExtension *userExt = [MSACUserExtension new]; + + // Then + XCTAssertTrue([userExt isValid]); +} + +- (void)testUserExtIsEqual { + + // If + MSACUserExtension *anotherUserExt = [MSACUserExtension new]; + + // Then + XCTAssertNotEqualObjects(anotherUserExt, self.userExt); + + // If + anotherUserExt = [MSACModelTestsUtililty userExtensionWithDummyValues:self.userExtDummyValues]; + + // Then + XCTAssertEqualObjects(anotherUserExt, self.userExt); + + // If + anotherUserExt.locale = @"fr-fr"; + + // Then + XCTAssertNotEqualObjects(anotherUserExt, self.userExt); + + // If + anotherUserExt.locale = self.userExtDummyValues[kMSACUserLocale]; + anotherUserExt.localId = @"42"; + + // Then + XCTAssertNotEqualObjects(anotherUserExt, self.userExt); +} + +#pragma mark - MSACLocExtension + +- (void)testLocExtJSONSerializingToDictionary { + + // When + NSMutableDictionary *dict = [self.locExt serializeToDictionary]; + + // Then + XCTAssertNotNil(dict); + XCTAssertEqualObjects(dict[kMSACTimezone], self.locExtDummyValues[kMSACTimezone]); +} + +- (void)testLocExtNSCodingSerializationAndDeserialization { + + // When + NSData *serializedlocExt = [MSACUtility archiveKeyedData:self.locExt]; + MSACLocExtension *actualLocExt = (MSACLocExtension *)[MSACUtility unarchiveKeyedData:serializedlocExt]; + + // Then + XCTAssertNotNil(actualLocExt); + XCTAssertEqualObjects(self.locExt, actualLocExt); + XCTAssertTrue([actualLocExt isMemberOfClass:[MSACLocExtension class]]); + XCTAssertEqualObjects(actualLocExt.tz, self.locExtDummyValues[kMSACTimezone]); +} + +- (void)testLocExtIsValid { + + // If + MSACLocExtension *locExt = [MSACLocExtension new]; + + // Then + XCTAssertTrue([locExt isValid]); +} + +- (void)testLocExtIsEqual { + + // If + MSACLocExtension *anotherLocExt = [MSACLocExtension new]; + + // Then + XCTAssertNotEqualObjects(anotherLocExt, self.locExt); + + // If + anotherLocExt = [MSACModelTestsUtililty locExtensionWithDummyValues:self.locExtDummyValues]; + + // Then + XCTAssertEqualObjects(anotherLocExt, self.locExt); + + // If + anotherLocExt.tz = @"+02:00"; + + // Then + XCTAssertNotEqualObjects(anotherLocExt, self.locExt); +} + +#pragma mark - MSACOSExtension + +- (void)testOSExtJSONSerializingToDictionary { + + // When + NSMutableDictionary *dict = [self.osExt serializeToDictionary]; + + // Then + XCTAssertNotNil(dict); + XCTAssertEqualObjects(dict, self.osExtDummyValues); +} + +- (void)testOSExtNSCodingSerializationAndDeserialization { + + // When + NSData *serializedOSExt = [MSACUtility archiveKeyedData:self.osExt]; + MSACOSExtension *actualOSExt = (MSACOSExtension *)[MSACUtility unarchiveKeyedData:serializedOSExt]; + + // Then + XCTAssertNotNil(actualOSExt); + XCTAssertEqualObjects(self.osExt, actualOSExt); + XCTAssertTrue([actualOSExt isMemberOfClass:[MSACOSExtension class]]); + XCTAssertEqualObjects(actualOSExt.name, self.osExtDummyValues[kMSACOSName]); + XCTAssertEqualObjects(actualOSExt.ver, self.osExtDummyValues[kMSACOSVer]); +} + +- (void)testOSExtIsValid { + + // If + MSACOSExtension *osExt = [MSACOSExtension new]; + + // Then + XCTAssertTrue([osExt isValid]); +} + +- (void)testOSExtIsEqual { + + // If + MSACOSExtension *anotherOSExt = [MSACOSExtension new]; + + // Then + XCTAssertNotEqualObjects(anotherOSExt, self.osExt); + + // If + anotherOSExt = [MSACModelTestsUtililty osExtensionWithDummyValues:self.osExtDummyValues]; + + // Then + XCTAssertEqualObjects(anotherOSExt, self.osExt); + + // If + anotherOSExt.name = @"macOS"; + + // Then + XCTAssertNotEqualObjects(anotherOSExt, self.osExt); + + // If + anotherOSExt.name = self.osExtDummyValues[kMSACOSName]; + anotherOSExt.ver = @"10.13.4"; + + // Then + XCTAssertNotEqualObjects(anotherOSExt, self.osExt); +} + +#pragma mark - MSACAppExtension + +- (void)testAppExtJSONSerializingToDictionary { + + // When + NSMutableDictionary *dict = [self.appExt serializeToDictionary]; + + // Then + XCTAssertNotNil(dict); + XCTAssertEqualObjects(dict, self.appExtDummyValues); +} + +- (void)testAppExtNSCodingSerializationAndDeserialization { + + // When + NSData *serializedAppExt = [MSACUtility archiveKeyedData:self.appExt]; + MSACAppExtension *actualAppExt = (MSACAppExtension *)[MSACUtility unarchiveKeyedData:serializedAppExt]; + + // Then + XCTAssertNotNil(actualAppExt); + XCTAssertEqualObjects(self.appExt, actualAppExt); + XCTAssertTrue([actualAppExt isMemberOfClass:[MSACAppExtension class]]); + XCTAssertEqualObjects(actualAppExt.appId, self.appExtDummyValues[kMSACAppId]); + XCTAssertEqualObjects(actualAppExt.ver, self.appExtDummyValues[kMSACAppVer]); + XCTAssertEqualObjects(actualAppExt.locale, self.appExtDummyValues[kMSACAppLocale]); + XCTAssertEqualObjects(actualAppExt.userId, self.appExtDummyValues[kMSACAppUserId]); +} + +- (void)testAppExtIsValid { + + // If + MSACAppExtension *appExt = [MSACAppExtension new]; + + // Then + XCTAssertTrue([appExt isValid]); +} + +- (void)testAppExtIsEqual { + + // If + MSACAppExtension *anotherAppExt = [MSACAppExtension new]; + + // Then + XCTAssertNotEqualObjects(anotherAppExt, self.appExt); + + // If + anotherAppExt = [MSACModelTestsUtililty appExtensionWithDummyValues:self.appExtDummyValues]; + + // Then + XCTAssertEqualObjects(anotherAppExt, self.appExt); + + // If + anotherAppExt.appId = @"com.another.bundle.id"; + + // Then + XCTAssertNotEqualObjects(anotherAppExt, self.appExt); + + // If + anotherAppExt.appId = self.appExtDummyValues[kMSACAppId]; + anotherAppExt.ver = @"10.13.4"; + + // Then + XCTAssertNotEqualObjects(anotherAppExt, self.appExt); + + // If + anotherAppExt.ver = self.appExtDummyValues[kMSACAppVer]; + anotherAppExt.locale = @"fr-ca"; + + // Then + XCTAssertNotEqualObjects(anotherAppExt, self.appExt); + + // If + anotherAppExt.locale = self.appExtDummyValues[kMSACAppLocale]; + anotherAppExt.userId = @"c:charlie"; + + // Then + XCTAssertNotEqualObjects(anotherAppExt, self.appExt); +} + +#pragma mark - MSACProtocolExtension + +- (void)testProtocolExtJSONSerializingToDictionary { + + // When + NSMutableDictionary *dict = [self.protocolExt serializeToDictionary]; + + // Then + XCTAssertNotNil(dict); + XCTAssertEqualObjects(dict, self.protocolExtDummyValues); +} + +- (void)testProtocolExtNSCodingSerializationAndDeserialization { + + // When + NSData *serializedProtocolExt = [MSACUtility archiveKeyedData:self.protocolExt]; + MSACProtocolExtension *actualProtocolExt = (MSACProtocolExtension *)[MSACUtility unarchiveKeyedData:serializedProtocolExt]; + + // Then + XCTAssertNotNil(actualProtocolExt); + XCTAssertEqualObjects(self.protocolExt, actualProtocolExt); + XCTAssertTrue([actualProtocolExt isMemberOfClass:[MSACProtocolExtension class]]); + XCTAssertEqualObjects(actualProtocolExt.ticketKeys, self.protocolExtDummyValues[kMSACTicketKeys]); + XCTAssertEqualObjects(actualProtocolExt.devMake, self.protocolExtDummyValues[kMSACDevMake]); + XCTAssertEqualObjects(actualProtocolExt.devModel, self.protocolExtDummyValues[kMSACDevModel]); +} + +- (void)testProtocolExtIsValid { + + // If + MSACProtocolExtension *protocolExt = [MSACProtocolExtension new]; + + // Then + XCTAssertTrue([protocolExt isValid]); +} + +- (void)testProtocolExtIsEqual { + + // If + MSACProtocolExtension *anotherProtocolExt = [MSACProtocolExtension new]; + + // Then + XCTAssertNotEqualObjects(anotherProtocolExt, self.protocolExt); + + // If + anotherProtocolExt = [MSACModelTestsUtililty protocolExtensionWithDummyValues:self.protocolExtDummyValues]; + + // Then + XCTAssertEqualObjects(anotherProtocolExt, self.protocolExt); + + // If + anotherProtocolExt.devMake = @"Android"; + + // Then + XCTAssertNotEqualObjects(anotherProtocolExt, self.protocolExt); + + // If + anotherProtocolExt.devMake = self.protocolExtDummyValues[kMSACDevMake]; + anotherProtocolExt.devModel = @"Samsung Galaxy 8"; + + // Then + XCTAssertNotEqualObjects(anotherProtocolExt, self.protocolExt); +} + +#pragma mark - MSACNetExtension + +- (void)testNetExtJSONSerializingToDictionary { + + // When + NSMutableDictionary *dict = [self.netExt serializeToDictionary]; + + // Then + XCTAssertNotNil(dict); + XCTAssertEqualObjects(dict, self.netExtDummyValues); +} + +- (void)testNetExtNSCodingSerializationAndDeserialization { + + // When + NSData *serializedNetExt = [MSACUtility archiveKeyedData:self.netExt]; + MSACNetExtension *actualNetExt = (MSACNetExtension *)[MSACUtility unarchiveKeyedData:serializedNetExt]; + + // Then + XCTAssertNotNil(actualNetExt); + XCTAssertEqualObjects(self.netExt, actualNetExt); + XCTAssertTrue([actualNetExt isMemberOfClass:[MSACNetExtension class]]); + XCTAssertEqualObjects(actualNetExt.provider, self.netExtDummyValues[kMSACNetProvider]); +} + +- (void)testNetExtIsValid { + + // If + MSACNetExtension *netExt = [MSACNetExtension new]; + + // Then + XCTAssertTrue([netExt isValid]); +} + +- (void)testNetExtIsEqual { + + // If + MSACNetExtension *anotherNetExt = [MSACNetExtension new]; + + // Then + XCTAssertNotEqualObjects(anotherNetExt, self.netExt); + + // If + anotherNetExt = [MSACModelTestsUtililty netExtensionWithDummyValues:self.netExtDummyValues]; + + // Then + XCTAssertEqualObjects(anotherNetExt, self.netExt); + + // If + anotherNetExt.provider = @"Sprint"; + + // Then + XCTAssertNotEqualObjects(anotherNetExt, self.netExt); +} + +#pragma mark - MSACSDKExtension + +- (void)testSDKExtJSONSerializingToDictionary { + + // When + NSMutableDictionary *dict = [self.sdkExt serializeToDictionary]; + + // Then + self.sdkExtDummyValues[kMSACSDKInstallId] = [((NSUUID *)self.sdkExtDummyValues[kMSACSDKInstallId]) UUIDString]; + XCTAssertNotNil(dict); + XCTAssertEqualObjects(dict, self.sdkExtDummyValues); +} + +- (void)testSDKExtNSCodingSerializationAndDeserialization { + + // When + NSData *serializedSDKExt = [MSACUtility archiveKeyedData:self.sdkExt]; + MSACSDKExtension *actualSDKExt = (MSACSDKExtension *)[MSACUtility unarchiveKeyedData:serializedSDKExt]; + + // Then + XCTAssertNotNil(actualSDKExt); + XCTAssertEqualObjects(self.sdkExt, actualSDKExt); + XCTAssertTrue([actualSDKExt isMemberOfClass:[MSACSDKExtension class]]); + XCTAssertEqualObjects(actualSDKExt.libVer, self.sdkExtDummyValues[kMSACSDKLibVer]); + XCTAssertEqualObjects(actualSDKExt.epoch, self.sdkExtDummyValues[kMSACSDKEpoch]); + XCTAssertTrue(actualSDKExt.seq == [self.sdkExtDummyValues[kMSACSDKSeq] longLongValue]); + XCTAssertEqualObjects(actualSDKExt.installId, self.sdkExtDummyValues[kMSACSDKInstallId]); +} + +- (void)testSDKExtIsValid { + + // If + MSACSDKExtension *sdkExt = [MSACSDKExtension new]; + + // Then + XCTAssertTrue([sdkExt isValid]); +} + +- (void)testSDKExtIsEqual { + + // If + MSACSDKExtension *anotherSDKExt = [MSACSDKExtension new]; + + // Then + XCTAssertNotEqualObjects(anotherSDKExt, self.sdkExt); + + // If + anotherSDKExt = [MSACModelTestsUtililty sdkExtensionWithDummyValues:self.sdkExtDummyValues]; + + // Then + XCTAssertEqualObjects(anotherSDKExt, self.sdkExt); + + // If + anotherSDKExt.libVer = @"2.1.0"; + + // Then + XCTAssertNotEqualObjects(anotherSDKExt, self.sdkExt); + + // If + anotherSDKExt.libVer = self.sdkExtDummyValues[kMSACSDKLibVer]; + anotherSDKExt.epoch = @"other_epoch_value"; + + // Then + XCTAssertNotEqualObjects(anotherSDKExt, self.sdkExt); + + // If + anotherSDKExt.epoch = self.sdkExtDummyValues[kMSACSDKEpoch]; + anotherSDKExt.seq = 2; + + // Then + XCTAssertNotEqualObjects(anotherSDKExt, self.sdkExt); + + // If + anotherSDKExt.seq = [self.sdkExtDummyValues[kMSACSDKSeq] longLongValue]; + anotherSDKExt.installId = [NSUUID new]; + + // Then + XCTAssertNotEqualObjects(anotherSDKExt, self.appExt); +} + +#pragma mark - MSACDeviceExtension + +- (void)testDeviceExtJSONSerializingToDictionary { + + // When + NSMutableDictionary *dict = [self.deviceExt serializeToDictionary]; + + // Then + XCTAssertNotNil(dict); + XCTAssertEqualObjects(dict, self.deviceExtDummyValues); +} + +- (void)testDeviceExtNSCodingSerializationAndDeserialization { + + // When + NSData *serializedDeviceExt = [MSACUtility archiveKeyedData:self.deviceExt]; + MSACDeviceExtension *actualDeviceExt = (MSACDeviceExtension *)[MSACUtility unarchiveKeyedData:serializedDeviceExt]; + + // Then + XCTAssertNotNil(actualDeviceExt); + XCTAssertEqualObjects(self.deviceExt, actualDeviceExt); + XCTAssertTrue([actualDeviceExt isMemberOfClass:[MSACDeviceExtension class]]); + XCTAssertEqualObjects(actualDeviceExt.localId, self.deviceExtDummyValues[kMSACDeviceLocalId]); +} + +- (void)testDeviceExtIsValid { + + // When + MSACDeviceExtension *deviceExt = [MSACDeviceExtension new]; + + // Then + XCTAssertTrue([deviceExt isValid]); +} + +- (void)testDeviceExtIsEqual { + + // When + MSACDeviceExtension *anotherDeviceExt = [MSACDeviceExtension new]; + + // Then + XCTAssertNotEqualObjects(anotherDeviceExt, self.deviceExt); + + // When + anotherDeviceExt = [MSACModelTestsUtililty deviceExtensionWithDummyValues:self.deviceExtDummyValues]; + + // Then + XCTAssertEqualObjects(anotherDeviceExt, self.deviceExt); + + // When + anotherDeviceExt.localId = [[[NSUUID alloc] initWithUUIDString:@"11111111-1111-1111-1111-11111111111"] UUIDString]; + + // Then + XCTAssertNotEqualObjects(anotherDeviceExt, self.deviceExt); +} + +#pragma mark - MSACCSData + +- (void)testDataJSONSerializingToDictionaryIsOrdered { + + // When + MSACOrderedDictionary *dict = (MSACOrderedDictionary *)[self.data serializeToDictionary]; + + // Then + XCTAssertNotNil(dict); + + // Only verify the order for baseType and baseData fields. + XCTAssertTrue([dict.order[0] isEqualToString:@"baseType"]); + XCTAssertTrue([dict.order[1] isEqualToString:@"baseData"]); + XCTAssertEqualObjects(dict[@"aKey"], @"aValue"); + XCTAssertEqualObjects(dict[@"anested.key"], @"anothervalue"); + XCTAssertEqualObjects(dict[@"anotherkey"], @"yetanothervalue"); +} + +- (void)testDataNSCodingSerializationAndDeserialization { + + // When + NSData *serializedData = [MSACUtility archiveKeyedData:self.data]; + MSACCSData *actualData = (MSACCSData *)[MSACUtility unarchiveKeyedData:serializedData]; + + // Then + XCTAssertNotNil(actualData); + XCTAssertEqualObjects(self.data, actualData); + XCTAssertTrue([actualData isMemberOfClass:[MSACCSData class]]); + XCTAssertEqualObjects(actualData.properties, self.orderedDummyValues); +} + +- (void)testInvalidDataNSCodingDeserialization { + + // When + MSACCSData *actualData = (MSACCSData *)[MSACUtility unarchiveKeyedData:@"invalid data"]; + + // Then + XCTAssertNil(nil); +} + +- (void)testDataIsValid { + + // If + MSACCSData *data = [MSACCSData new]; + + // Then + XCTAssertTrue([data isValid]); +} + +- (void)testDataIsEqual { + + // If + MSACCSData *anotherData = [MSACCSData new]; + + // Then + XCTAssertNotEqualObjects(anotherData, self.data); + + // If + anotherData = [MSACModelTestsUtililty dataWithDummyValues:self.unorderedDummyValues]; + + // Then + XCTAssertEqualObjects(anotherData, self.data); + + // If + anotherData.properties = [@{@"part.c.key" : @"part.c.value"} mutableCopy]; + + // Then + XCTAssertNotEqualObjects(anotherData, self.data); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACChannelGroupDefaultTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACChannelGroupDefaultTests.m new file mode 100644 index 0000000000..1349d07b78 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACChannelGroupDefaultTests.m @@ -0,0 +1,496 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAbstractLogInternal.h" +#import "MSACAppCenterIngestion.h" +#import "MSACChannelDelegate.h" +#import "MSACChannelGroupDefault.h" +#import "MSACChannelGroupDefaultPrivate.h" +#import "MSACChannelUnitConfiguration.h" +#import "MSACChannelUnitDefault.h" +#import "MSACChannelUnitDefaultPrivate.h" +#import "MSACHttpClient.h" +#import "MSACHttpTestUtil.h" +#import "MSACHttpUtil.h" +#import "MSACIngestionProtocol.h" +#import "MSACMockLog.h" +#import "MSACStorage.h" +#import "MSACTestFrameworks.h" + +@interface MSACChannelGroupDefaultTests : XCTestCase + +@property(nonatomic) id ingestionMock; + +@property(nonatomic) MSACChannelUnitConfiguration *validConfiguration; + +@property(nonatomic) MSACChannelGroupDefault *sut; + +@end + +@implementation MSACChannelGroupDefaultTests + +- (void)setUp { + NSString *groupId = @"AppCenter"; + MSACPriority priority = MSACPriorityDefault; + NSUInteger flushInterval = 3; + NSUInteger batchSizeLimit = 10; + NSUInteger pendingBatchesLimit = 3; + self.ingestionMock = OCMClassMock([MSACAppCenterIngestion class]); + self.validConfiguration = [[MSACChannelUnitConfiguration alloc] initWithGroupId:groupId + priority:priority + flushInterval:flushInterval + batchSizeLimit:batchSizeLimit + pendingBatchesLimit:pendingBatchesLimit]; + self.sut = [[MSACChannelGroupDefault alloc] initWithIngestion:self.ingestionMock]; + + /* + * dispatch_get_main_queue isn't good option for logsDispatchQueue because + * we can't clear pending actions from it after the test. It can cause usages of stopped mocks. + * + * Keep the serial queue that created during the initialization. + */ +} + +- (void)tearDown { + __weak dispatch_object_t dispatchQueue = self.sut.logsDispatchQueue; + self.sut = nil; + XCTAssertNil(dispatchQueue); + + // Stop mocks. + [self.ingestionMock stopMocking]; + [super tearDown]; +} + +#if !TARGET_OS_OSX +- (void)testAppIsKilled { + + // If + [self.sut setEnabled:YES andDeleteDataOnDisabled:YES]; + id sut = OCMPartialMock(self.sut); + + // When + [[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationWillTerminateNotification object:sut]; + + // Then + OCMVerify([sut applicationWillTerminate:OCMOCK_ANY]); + XCTAssertNotNil(self.sut.logsDispatchQueue); + + // If + [self.sut setEnabled:NO andDeleteDataOnDisabled:YES]; + OCMReject([sut applicationWillTerminate:OCMOCK_ANY]); + + // When + [[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationWillTerminateNotification object:sut]; + + // Then + self.sut.logsDispatchQueue = nil; + OCMVerifyAll(sut); + [sut stopMocking]; +} +#endif + +#pragma mark - Tests + +- (void)testNewInstanceWasInitialisedCorrectly { + + // Then + assertThat(self.sut, notNilValue()); + assertThat(self.sut.logsDispatchQueue, notNilValue()); + assertThat(self.sut.channels, isEmpty()); + assertThat(self.sut.ingestion, equalTo(self.ingestionMock)); + assertThat(self.sut.storage, notNilValue()); +} + +- (void)testAddNewChannel { + + // Then + assertThat(self.sut.channels, isEmpty()); + + // When + id addedChannel = [self.sut addChannelUnitWithConfiguration:self.validConfiguration]; + + // This test will use a real channel unit object which runs `checkPendingLogs` in the log dispatch queue. + // We should make sure the test method is not finished before `checkPendingLogs` method call is finished to avoid object retain issue. + [self waitForLogsDispatchQueue]; + + // Then + XCTAssertTrue([self.sut.channels containsObject:addedChannel]); + assertThat(addedChannel, notNilValue()); + XCTAssertTrue(addedChannel.configuration.priority == self.validConfiguration.priority); + assertThatFloat(addedChannel.configuration.flushInterval, equalToFloat(self.validConfiguration.flushInterval)); + assertThatUnsignedLong(addedChannel.configuration.batchSizeLimit, equalToUnsignedLong(self.validConfiguration.batchSizeLimit)); + assertThatUnsignedLong(addedChannel.configuration.pendingBatchesLimit, equalToUnsignedLong(self.validConfiguration.pendingBatchesLimit)); +} + +- (void)testAddNewChannelWithDefaultIngestion { + + // When + MSACChannelUnitDefault *channelUnit = (MSACChannelUnitDefault *)[self.sut addChannelUnitWithConfiguration:self.validConfiguration]; + + // This test will use a real channel unit object which runs `checkPendingLogs` in the log dispatch queue. + // We should make sure the test method is not finished before `checkPendingLogs` method call is finished to avoid object retain issue. + [self waitForLogsDispatchQueue]; + + // Then + XCTAssertEqual(self.ingestionMock, channelUnit.ingestion); +} + +- (void)testAddChannelWithCustomIngestion { + + // If, We can't use class mock of MSACAppCenterIngestion because it is already class-mocked in setUp. + // Using more than one class mock is not supported. + MSACAppCenterIngestion *newIngestion = [MSACAppCenterIngestion new]; + + // When + MSACChannelUnitDefault *channelUnit = + (MSACChannelUnitDefault *)[self.sut addChannelUnitWithConfiguration:[MSACChannelUnitConfiguration new] withIngestion:newIngestion]; + + // This test will use a real channel unit object which runs `checkPendingLogs` in the log dispatch queue. + // We should make sure the test method is not finished before `checkPendingLogs` method call is finished to avoid object retain issue. + [self waitForLogsDispatchQueue]; + + // Then + XCTAssertNotEqual(self.ingestionMock, channelUnit.ingestion); + XCTAssertEqual(newIngestion, channelUnit.ingestion); +} + +- (void)testDelegatesConcurrentAccess { + + // If + MSACAbstractLog *log = [MSACAbstractLog new]; + for (int j = 0; j < 10; j++) { + id mockDelegate = OCMProtocolMock(@protocol(MSACChannelDelegate)); + [self.sut addDelegate:mockDelegate]; + } + id addedChannel = [self.sut addChannelUnitWithConfiguration:self.validConfiguration]; + + // When + void (^block)(void) = ^{ + for (int i = 0; i < 10; i++) { + [addedChannel enqueueItem:log flags:MSACFlagsDefault]; + } + for (int i = 0; i < 100; i++) { + [self.sut addDelegate:OCMProtocolMock(@protocol(MSACChannelDelegate))]; + } + }; + + // This test will use a real channel unit object which runs `checkPendingLogs` in the log dispatch queue. + // We should make sure the test method is not finished before `checkPendingLogs` method call is finished to avoid object retain issue. + [self waitForLogsDispatchQueue]; + + // Then + XCTAssertNoThrow(block()); +} + +- (void)testSetEnabled { + + // If + id channelMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + id delegateMock = OCMProtocolMock(@protocol(MSACChannelDelegate)); + [self.sut addDelegate:delegateMock]; + [self.sut.channels addObject:channelMock]; + + // When + [self.sut setEnabled:NO andDeleteDataOnDisabled:YES]; + + // Then + OCMVerify([self.ingestionMock setEnabled:NO andDeleteDataOnDisabled:YES]); + OCMVerify([channelMock setEnabled:NO andDeleteDataOnDisabled:YES]); + OCMVerify([delegateMock channel:self.sut didSetEnabled:NO andDeleteDataOnDisabled:YES]); +} + +- (void)testResume { + + // If + id channelMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + [self.sut.channels addObject:channelMock]; + NSObject *token = [NSObject new]; + + // When + [self.sut resumeWithIdentifyingObject:token]; + + // Then + OCMVerify([self.ingestionMock setEnabled:YES andDeleteDataOnDisabled:NO]); + OCMVerify([channelMock resumeWithIdentifyingObject:token]); +} + +- (void)testPause { + + // If + id channelMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + [self.sut.channels addObject:channelMock]; + NSObject *identifyingObject = [NSObject new]; + + // When + [self.sut pauseWithIdentifyingObject:identifyingObject]; + + // Then + OCMVerify([self.ingestionMock setEnabled:NO andDeleteDataOnDisabled:NO]); + OCMVerify([channelMock pauseWithIdentifyingObject:identifyingObject]); +} + +- (void)testChannelUnitIsCorrectlyInitialized { + + // If + id channelUnitMock = OCMClassMock([MSACChannelUnitDefault class]); + OCMStub([channelUnitMock alloc]).andReturn(channelUnitMock); + OCMStub([channelUnitMock initWithIngestion:OCMOCK_ANY storage:OCMOCK_ANY configuration:OCMOCK_ANY logsDispatchQueue:OCMOCK_ANY]) + .andReturn(channelUnitMock); + + // When + [self.sut addChannelUnitWithConfiguration:self.validConfiguration]; + + // This test will use a real channel unit object which runs `checkPendingLogs` in the log dispatch queue. + // We should make sure the test method is not finished before `checkPendingLogs` method call is finished to avoid object retain issue. + [self waitForLogsDispatchQueue]; + + // Then + OCMVerify([channelUnitMock addDelegate:(id)self.sut]); + OCMVerify([channelUnitMock checkPendingLogs]); + + // Clear + [channelUnitMock stopMocking]; +} + +- (void)testDelegateCalledWhenAddingNewChannelUnit { + + // Test that delegates are called whenever a new channel unit is added to the + // channel group. + + // If + id channelUnitMock = OCMClassMock([MSACChannelUnitDefault class]); + OCMStub([channelUnitMock alloc]).andReturn(channelUnitMock); + OCMStub([channelUnitMock initWithIngestion:OCMOCK_ANY storage:OCMOCK_ANY configuration:OCMOCK_ANY logsDispatchQueue:OCMOCK_ANY]) + .andReturn(channelUnitMock); + id delegateMock1 = OCMProtocolMock(@protocol(MSACChannelDelegate)); + OCMExpect([delegateMock1 channelGroup:self.sut didAddChannelUnit:channelUnitMock]); + id delegateMock2 = OCMProtocolMock(@protocol(MSACChannelDelegate)); + OCMExpect([delegateMock2 channelGroup:self.sut didAddChannelUnit:channelUnitMock]); + [self.sut addDelegate:delegateMock1]; + [self.sut addDelegate:delegateMock2]; + + // When + [self.sut addChannelUnitWithConfiguration:self.validConfiguration]; + + // This test will use a real channel unit object which runs `checkPendingLogs` in the log dispatch queue. + // We should make sure the test method is not finished before `checkPendingLogs` method call is finished to avoid object retain issue. + [self waitForLogsDispatchQueue]; + + // Then + OCMVerifyAll(delegateMock1); + OCMVerifyAll(delegateMock2); + + // Clear + [channelUnitMock stopMocking]; +} + +- (void)testDelegateCalledWhenChannelUnitPaused { + + // If + NSObject *identifyingObject = [NSObject new]; + [self.sut addChannelUnitWithConfiguration:self.validConfiguration]; + id delegateMock = OCMProtocolMock(@protocol(MSACChannelDelegate)); + [self.sut addDelegate:delegateMock]; + + // When + [self.sut channel:self.sut didPauseWithIdentifyingObject:identifyingObject]; + + // This test will use a real channel unit object which runs `checkPendingLogs` in the log dispatch queue. + // We should make sure the test method is not finished before `checkPendingLogs` method call is finished to avoid object retain issue. + [self waitForLogsDispatchQueue]; + + // Then + OCMVerify([delegateMock channel:self.sut didPauseWithIdentifyingObject:identifyingObject]); +} + +- (void)testDelegateCalledWhenChannelUnitResumed { + + // If + NSObject *identifyingObject = [NSObject new]; + [self.sut addChannelUnitWithConfiguration:self.validConfiguration]; + id delegateMock = OCMProtocolMock(@protocol(MSACChannelDelegate)); + [self.sut addDelegate:delegateMock]; + + // When + [self.sut channel:self.sut didResumeWithIdentifyingObject:identifyingObject]; + + // This test will use a real channel unit object which runs `checkPendingLogs` in the log dispatch queue. + // We should make sure the test method is not finished before `checkPendingLogs` method call is finished to avoid object retain issue. + [self waitForLogsDispatchQueue]; + + // Then + OCMVerify([delegateMock channel:self.sut didResumeWithIdentifyingObject:identifyingObject]); +} + +- (void)testDelegateCalledWhenChannelUnitPreparesLog { + + // If + id mockLog = [MSACMockLog new]; + [self.sut addChannelUnitWithConfiguration:self.validConfiguration]; + id delegateMock = OCMProtocolMock(@protocol(MSACChannelDelegate)); + [self.sut addDelegate:delegateMock]; + + // When + [self.sut channel:self.sut prepareLog:mockLog]; + + // This test will use a real channel unit object which runs `checkPendingLogs` in the log dispatch queue. + // We should make sure the test method is not finished before `checkPendingLogs` method call is finished to avoid object retain issue. + [self waitForLogsDispatchQueue]; + + // Then + OCMVerify([delegateMock channel:self.sut prepareLog:mockLog]); +} + +- (void)testDelegateCalledWhenChannelUnitDidPrepareLog { + + // If + id mockLog = [MSACMockLog new]; + NSString *internalId = @"mockId"; + [self.sut addChannelUnitWithConfiguration:self.validConfiguration]; + id delegateMock = OCMProtocolMock(@protocol(MSACChannelDelegate)); + [self.sut addDelegate:delegateMock]; + + // When + [self.sut channel:self.sut didPrepareLog:mockLog internalId:internalId flags:MSACFlagsDefault]; + + // This test will use a real channel unit object which runs `checkPendingLogs` in the log dispatch queue. + // We should make sure the test method is not finished before `checkPendingLogs` method call is finished to avoid object retain issue. + [self waitForLogsDispatchQueue]; + + // Then + OCMVerify([delegateMock channel:self.sut didPrepareLog:mockLog internalId:internalId flags:MSACFlagsDefault]); +} + +- (void)testDelegateCalledWhenChannelUnitDidCompleteEnqueueingLog { + + // If + id mockLog = [MSACMockLog new]; + NSString *internalId = @"mockId"; + [self.sut addChannelUnitWithConfiguration:self.validConfiguration]; + id delegateMock = OCMProtocolMock(@protocol(MSACChannelDelegate)); + [self.sut addDelegate:delegateMock]; + + // When + [self.sut channel:self.sut didCompleteEnqueueingLog:mockLog internalId:internalId]; + + // This test will use a real channel unit object which runs `checkPendingLogs` in the log dispatch queue. + // We should make sure the test method is not finished before `checkPendingLogs` method call is finished to avoid object retain issue. + [self waitForLogsDispatchQueue]; + + // Then + OCMVerify([delegateMock channel:self.sut didCompleteEnqueueingLog:mockLog internalId:internalId]); +} + +- (void)testDelegateCalledWhenChannelUnitWillSendLog { + + // If + id mockLog = [MSACMockLog new]; + [self.sut addChannelUnitWithConfiguration:self.validConfiguration]; + id delegateMock = OCMProtocolMock(@protocol(MSACChannelDelegate)); + [self.sut addDelegate:delegateMock]; + + // When + [self.sut channel:self.sut willSendLog:mockLog]; + + // This test will use a real channel unit object which runs `checkPendingLogs` in the log dispatch queue. + // We should make sure the test method is not finished before `checkPendingLogs` method call is finished to avoid object retain issue. + [self waitForLogsDispatchQueue]; + + // Then + OCMVerify([delegateMock channel:self.sut willSendLog:mockLog]); +} + +- (void)testDelegateCalledWhenChannelUnitDidSucceedSendingLog { + + // If + id mockLog = [MSACMockLog new]; + [self.sut addChannelUnitWithConfiguration:self.validConfiguration]; + id delegateMock = OCMProtocolMock(@protocol(MSACChannelDelegate)); + [self.sut addDelegate:delegateMock]; + + // When + [self.sut channel:self.sut didSucceedSendingLog:mockLog]; + + // This test will use a real channel unit object which runs `checkPendingLogs` in the log dispatch queue. + // We should make sure the test method is not finished before `checkPendingLogs` method call is finished to avoid object retain issue. + [self waitForLogsDispatchQueue]; + + // Then + OCMVerify([delegateMock channel:self.sut didSucceedSendingLog:mockLog]); +} + +- (void)testDelegateCalledWhenChannelUnitDidSetEnabled { + + // If + [self.sut addChannelUnitWithConfiguration:self.validConfiguration]; + id delegateMock = OCMProtocolMock(@protocol(MSACChannelDelegate)); + [self.sut addDelegate:delegateMock]; + + // When + [self.sut channel:self.sut didSetEnabled:YES andDeleteDataOnDisabled:YES]; + + // This test will use a real channel unit object which runs `checkPendingLogs` in the log dispatch queue. + // We should make sure the test method is not finished before `checkPendingLogs` method call is finished to avoid object retain issue. + [self waitForLogsDispatchQueue]; + + // Then + OCMVerify([delegateMock channel:self.sut didSetEnabled:YES andDeleteDataOnDisabled:YES]); +} + +- (void)testDelegateCalledWhenChannelUnitDidFailSendingLog { + + // If + id mockLog = [MSACMockLog new]; + NSError *error = [NSError new]; + [self.sut addChannelUnitWithConfiguration:self.validConfiguration]; + id delegateMock = OCMProtocolMock(@protocol(MSACChannelDelegate)); + [self.sut addDelegate:delegateMock]; + + // When + [self.sut channel:self.sut didFailSendingLog:mockLog withError:error]; + + // This test will use a real channel unit object which runs `checkPendingLogs` in the log dispatch queue. + // We should make sure the test method is not finished before `checkPendingLogs` method call is finished to avoid object retain issue. + [self waitForLogsDispatchQueue]; + + // Then + OCMVerify([delegateMock channel:self.sut didFailSendingLog:mockLog withError:error]); +} + +- (void)testDelegateCalledWhenChannelUnitShouldFilterLog { + + // If + id mockLog = [MSACMockLog new]; + id channelUnitMock = OCMClassMock([MSACChannelUnitDefault class]); + OCMStub([channelUnitMock alloc]).andReturn(channelUnitMock); + OCMStub([channelUnitMock initWithIngestion:OCMOCK_ANY storage:OCMOCK_ANY configuration:OCMOCK_ANY logsDispatchQueue:OCMOCK_ANY]) + .andReturn(channelUnitMock); + [self.sut addChannelUnitWithConfiguration:self.validConfiguration]; + id delegateMock = OCMProtocolMock(@protocol(MSACChannelDelegate)); + [self.sut addDelegate:delegateMock]; + + // When + [self.sut channelUnit:channelUnitMock shouldFilterLog:mockLog]; + + // This test will use a real channel unit object which runs `checkPendingLogs` in the log dispatch queue. + // We should make sure the test method is not finished before `checkPendingLogs` method call is finished to avoid object retain issue. + [self waitForLogsDispatchQueue]; + + // Then + OCMVerify([delegateMock channelUnit:channelUnitMock shouldFilterLog:mockLog]); + + // Clear + [channelUnitMock stopMocking]; +} + +#pragma mark - Helper + +- (void)waitForLogsDispatchQueue { + XCTestExpectation *expectation = [self expectationWithDescription:@"Logs dispatch queue"]; + dispatch_async(self.sut.logsDispatchQueue, ^{ + [expectation fulfill]; + }); + [self waitForExpectations:@[ expectation ] timeout:1]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACChannelUnitConfigurationTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACChannelUnitConfigurationTests.m new file mode 100644 index 0000000000..1c1b26a195 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACChannelUnitConfigurationTests.m @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACChannelUnitConfiguration.h" +#import "MSACTestFrameworks.h" + +@interface MSACChannelUnitConfigurationTests : XCTestCase + +@end + +@implementation MSACChannelUnitConfigurationTests + +#pragma mark - Tests + +- (void)testNewInstanceWasInitialisedCorrectly { + + // If + NSString *groupId = @"FooBar"; + MSACPriority priority = MSACPriorityDefault; + NSUInteger batchSizeLimit = 10; + NSUInteger pendingBatchesLimit = 20; + NSUInteger flushInterval = 9; + + // When + MSACChannelUnitConfiguration *sut = [[MSACChannelUnitConfiguration alloc] initWithGroupId:groupId + priority:priority + flushInterval:flushInterval + batchSizeLimit:batchSizeLimit + pendingBatchesLimit:pendingBatchesLimit]; + + // Then + assertThat(sut, notNilValue()); + assertThat(sut.groupId, equalTo(groupId)); + XCTAssertTrue(sut.priority == priority); + assertThatUnsignedInteger(sut.batchSizeLimit, equalToUnsignedInteger(batchSizeLimit)); + assertThatUnsignedInteger(sut.pendingBatchesLimit, equalToUnsignedInteger(pendingBatchesLimit)); + assertThatUnsignedInteger(sut.flushInterval, equalToUnsignedInteger(flushInterval)); +} + +- (void)testNewInstanceWithDefaultSettings { + + // If + NSString *groupId = @"FooBar"; + + // When + MSACChannelUnitConfiguration *sut = [[MSACChannelUnitConfiguration alloc] initDefaultConfigurationWithGroupId:groupId]; + + // Then + assertThat(sut, notNilValue()); + assertThat(sut.groupId, equalTo(groupId)); + XCTAssertTrue(sut.priority == MSACPriorityDefault); + assertThatUnsignedInteger(sut.batchSizeLimit, equalToUnsignedInteger(50)); + assertThatUnsignedInteger(sut.pendingBatchesLimit, equalToUnsignedInteger(3)); + assertThatUnsignedInteger(sut.flushInterval, equalToUnsignedInteger(3)); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACChannelUnitDefaultTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACChannelUnitDefaultTests.m new file mode 100644 index 0000000000..55ee17811d --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACChannelUnitDefaultTests.m @@ -0,0 +1,1970 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import +#import + +#import "MSACAbstractLogInternal.h" +#import "MSACAppCenter.h" +#import "MSACChannelDelegate.h" +#import "MSACChannelUnitConfiguration.h" +#import "MSACChannelUnitDefault.h" +#import "MSACChannelUnitDefaultPrivate.h" +#import "MSACDevice.h" +#import "MSACHttpIngestion.h" +#import "MSACHttpTestUtil.h" +#import "MSACLogContainer.h" +#import "MSACMockUserDefaults.h" +#import "MSACServiceCommon.h" +#import "MSACStorage.h" +#import "MSACTestFrameworks.h" +#import "MSACUserIdContext.h" +#import "MSACUtility.h" + +static NSTimeInterval const kMSACTestTimeout = 1.0; +static NSString *const kMSACTestGroupId = @"GroupId"; + +@interface MSACChannelUnitDefault (Test) + +- (void)sendLogContainer:(MSACLogContainer *__nonnull)container; + +@end + +@interface MSACChannelUnitDefaultTests : XCTestCase + +@property(nonatomic) MSACChannelUnitConfiguration *configuration; +@property(nonatomic) MSACMockUserDefaults *settingsMock; + +@property(nonatomic) id storageMock; +@property(nonatomic) id ingestionMock; + +/** + * Most of the channel APIs are asynchronous, this expectation is meant to be enqueued to the data dispatch queue at the end of the test + * before any asserts. Then it will be triggered on the next queue loop right after the channel finished its job. Wrap asserts within the + * handler of a waitForExpectationsWithTimeout method. + */ +@property(nonatomic) XCTestExpectation *channelEndJobExpectation; + +@property(nonatomic, weak) dispatch_queue_t dispatchQueue; + +@end + +@implementation MSACChannelUnitDefaultTests + +#pragma mark - Housekeeping + +- (void)setUp { + [super setUp]; + self.configuration = [[MSACChannelUnitConfiguration alloc] initDefaultConfigurationWithGroupId:kMSACTestGroupId]; + self.storageMock = OCMProtocolMock(@protocol(MSACStorage)); + OCMStub([self.storageMock saveLog:OCMOCK_ANY withGroupId:OCMOCK_ANY flags:MSACFlagsNormal]).andReturn(YES); + OCMStub([self.storageMock saveLog:OCMOCK_ANY withGroupId:OCMOCK_ANY flags:MSACFlagsCritical]).andReturn(YES); + self.ingestionMock = OCMProtocolMock(@protocol(MSACIngestionProtocol)); + OCMStub([self.ingestionMock isReadyToSend]).andReturn(YES); + self.settingsMock = [MSACMockUserDefaults new]; +} + +- (void)tearDown { + + // Stop mocks. + [self.storageMock stopMocking]; + [self.ingestionMock stopMocking]; + [self.settingsMock stopMocking]; + + /* + * Make sure that dispatch queue has been deallocated. + * Note: the check should be done after `stopMocking` calls because it clears list of invocations that + * keeps references to all arguments including blocks (that implicitly keeps channel "self" reference). + */ + XCTAssertNil(self.dispatchQueue); + + [super tearDown]; +} + +#pragma mark - Tests + +- (void)testPendingLogsStoresStartTimeWhenPaused { + + // If + [self initChannelEndJobExpectation]; + NSObject *object = [NSObject new]; + __block NSDate *date = [[NSDate alloc] initWithTimeIntervalSince1970:3000]; + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + + // Configure channel with custom interval. + channel.configuration = [[MSACChannelUnitConfiguration alloc] initWithGroupId:kMSACTestGroupId + priority:MSACPriorityDefault + flushInterval:60 + batchSizeLimit:50 + pendingBatchesLimit:3]; + id dateMock = OCMClassMock([NSDate class]); + OCMStub([dateMock date]).andReturn(date); + + // When + [channel pauseWithIdentifyingObjectSync:object]; + + // Trigger checkPengingLogs. Should save timestamp now. + [channel enqueueItem:[self getValidMockLog] flags:MSACFlagsDefault]; + [self enqueueChannelEndJobExpectation]; + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + NSDate *resultDate = [self.settingsMock objectForKey:channel.oldestPendingLogTimestampKey]; + XCTAssertTrue([date isEqualToDate:resultDate]); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; + + // Clear + [dateMock stopMocking]; +} + +- (void)testCustomFlushIntervalSending200Logs { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + NSUInteger flushInterval = 600; + NSUInteger batchSizeLimit = 50; + __block int currentBatchId = 1; + NSDate *date = [NSDate dateWithTimeIntervalSince1970:0]; + NSDate *date2 = [NSDate dateWithTimeIntervalSince1970:flushInterval + 100]; + __block id responseMock = [MSACHttpTestUtil createMockResponseForStatusCode:200 headers:nil]; + __block MSACSendAsyncCompletionHandler ingestionBlock; + + // Requests counter. + __block int sendCount = 0; + OCMStub([self.ingestionMock sendAsync:OCMOCK_ANY completionHandler:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + // Get ingestion block for later call. + [invocation retainArguments]; + [invocation getArgument:&ingestionBlock atIndex:3]; + sendCount++; + }); + + // Stub the storage load. + NSArray> *logs = [self getValidMockLogArrayForDate:date andCount:50]; + OCMStub([self.storageMock loadLogsWithGroupId:kMSACTestGroupId + limit:batchSizeLimit + excludedTargetKeys:OCMOCK_ANY + completionHandler:OCMOCK_ANY]) + .andDo(^(NSInvocation *invocation) { + MSACLoadDataCompletionHandler loadCallback; + + // Get ingestion block for later call. + [invocation getArgument:&loadCallback atIndex:5]; + + // Mock load with incrementing batchId. + loadCallback(logs, [@(currentBatchId++) stringValue]); + + // Return YES and exit the method. + BOOL enabled = YES; + [invocation setReturnValue:&enabled]; + }); + + // Configure channel and set custom flushInterval. + channel.configuration = [[MSACChannelUnitConfiguration alloc] initWithGroupId:kMSACTestGroupId + priority:MSACPriorityDefault + flushInterval:flushInterval + batchSizeLimit:50 + pendingBatchesLimit:3]; + + // When + channel.itemsCount = 200; + + // Timestamp saved with time == 0. + [self.settingsMock setObject:date forKey:channel.oldestPendingLogTimestampKey]; + + // Change time. Simulate time has passed. + id dateMock = OCMClassMock([NSDate class]); + OCMStub([dateMock date]).andReturn(date2); + + // Trigger checkPengingLogs. Should flush 3 batches now. + [channel checkPendingLogs]; + + // Try to release one batch. + dispatch_async(channel.logsDispatchQueue, ^{ + // Check 3 batches sent. + assertThatInt(sendCount, equalToInt(3)); + XCTAssertNotNil(ingestionBlock); + if (ingestionBlock) { + + // Release 1 batch. + ingestionBlock([@(1) stringValue], responseMock, nil, nil); + } + + // Then + dispatch_async(channel.logsDispatchQueue, ^{ + // Check 4th batch sent. + assertThatInt(sendCount, equalToInt(4)); + + [self enqueueChannelEndJobExpectation]; + }); + }); + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + assertThatUnsignedLong(channel.itemsCount, equalToInt(0)); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; + + // Clear + [dateMock stopMocking]; + [responseMock stopMocking]; +} + +- (void)testLogsFlushedImmediatelyWhenIntervalIsOver { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + channel.itemsCount = 5; + + // Configure channel. + channel.configuration = [[MSACChannelUnitConfiguration alloc] initWithGroupId:kMSACTestGroupId + priority:MSACPriorityDefault + flushInterval:600 + batchSizeLimit:1 + pendingBatchesLimit:3]; + NSDate *date = [[NSDate alloc] initWithTimeIntervalSince1970:3000]; + [self.settingsMock setObject:[[NSDate alloc] initWithTimeIntervalSince1970:500] forKey:channel.oldestPendingLogTimestampKey]; + id channelUnitMock = OCMPartialMock(channel); + OCMReject([channelUnitMock startTimer:OCMOCK_ANY]); + id dateMock = OCMClassMock([NSDate class]); + OCMStub([dateMock date]).andReturn(date); + + // When + // Trigger checkPendingLogs + [channel enqueueItem:[self getValidMockLog] flags:MSACFlagsDefault]; + [self enqueueChannelEndJobExpectation]; + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + assertThatUnsignedLong(channel.itemsCount, equalToInt(0)); + OCMVerify([channel flushQueue]); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; + + // Clear + [dateMock stopMocking]; + [channelUnitMock stopMocking]; +} + +- (void)testLogsNotFlushedImmediatelyWhenIntervalIsCustom { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + NSUInteger batchSizeLimit = 4; + int itemsToAdd = 8; + id channelUnitMock = OCMPartialMock(channel); + + // Configure channel. + channel.configuration = [[MSACChannelUnitConfiguration alloc] initWithGroupId:kMSACTestGroupId + priority:MSACPriorityDefault + flushInterval:600 + batchSizeLimit:batchSizeLimit + pendingBatchesLimit:3]; + + // When + for (NSUInteger i = 0; i < itemsToAdd; i++) { + [channel enqueueItem:[self getValidMockLog] flags:MSACFlagsDefault]; + } + [self enqueueChannelEndJobExpectation]; + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + OCMVerify([[channelUnitMock ignoringNonObjectArgs] startTimer:0]); + assertThatUnsignedLong(channel.itemsCount, equalToInt(itemsToAdd)); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; + + // Clear + [channelUnitMock stopMocking]; +} + +- (void)testResolveFlushIntervalTimestampNotSet { + + // If + NSUInteger flushInterval = 2000; + MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + + // Configure channel. + channel.configuration = [[MSACChannelUnitConfiguration alloc] initWithGroupId:kMSACTestGroupId + priority:MSACPriorityDefault + flushInterval:flushInterval + batchSizeLimit:50 + pendingBatchesLimit:1]; + NSDate *date = [[NSDate alloc] initWithTimeIntervalSince1970:1000]; + id dateMock = OCMClassMock([NSDate class]); + OCMStub([dateMock date]).andReturn(date); + + // When + NSUInteger resultFlushInterval = [channel resolveFlushInterval]; + + // Then + XCTAssertEqual(resultFlushInterval, flushInterval); + + // Clear + [dateMock stopMocking]; +} + +- (void)testResolveFlushIntervalTimeIsOut { + + // If + NSUInteger flushInterval = 2000; + MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + + // Configure channel. + channel.configuration = [[MSACChannelUnitConfiguration alloc] initWithGroupId:kMSACTestGroupId + priority:MSACPriorityDefault + flushInterval:flushInterval + batchSizeLimit:50 + pendingBatchesLimit:1]; + NSDate *date = [[NSDate alloc] initWithTimeIntervalSince1970:3000]; + [self.settingsMock setObject:[[NSDate alloc] initWithTimeIntervalSince1970:500] forKey:channel.oldestPendingLogTimestampKey]; + id dateMock = OCMClassMock([NSDate class]); + OCMStub([dateMock date]).andReturn(date); + + // When + NSUInteger resultFlushInterval = [channel resolveFlushInterval]; + + // Then + XCTAssertEqual(resultFlushInterval, 0); + + // Clear + [dateMock stopMocking]; +} + +- (void)testResolveFlushIntervalTimestampLaterThanNow { + + // If + NSUInteger flushInterval = 2000; + MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + + // Configure channel. + channel.configuration = [[MSACChannelUnitConfiguration alloc] initWithGroupId:kMSACTestGroupId + priority:MSACPriorityDefault + flushInterval:flushInterval + batchSizeLimit:50 + pendingBatchesLimit:1]; + NSDate *date = [[NSDate alloc] initWithTimeIntervalSince1970:1000]; + [self.settingsMock setObject:[[NSDate alloc] initWithTimeIntervalSince1970:2000] forKey:channel.oldestPendingLogTimestampKey]; + id dateMock = OCMClassMock([NSDate class]); + OCMStub([dateMock date]).andReturn(date); + + // When + NSUInteger resultFlushInterval = [channel resolveFlushInterval]; + + // Then + XCTAssertEqual(resultFlushInterval, flushInterval); + + // Clear + [dateMock stopMocking]; +} + +- (void)testResolveFlushIntervalNow { + + // If + MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + + // Configure channel. + channel.configuration = [[MSACChannelUnitConfiguration alloc] initWithGroupId:kMSACTestGroupId + priority:MSACPriorityDefault + flushInterval:2000 + batchSizeLimit:50 + pendingBatchesLimit:1]; + NSDate *date = [[NSDate alloc] initWithTimeIntervalSince1970:4000]; + [self.settingsMock setObject:[[NSDate alloc] initWithTimeIntervalSince1970:2000] forKey:channel.oldestPendingLogTimestampKey]; + id dateMock = OCMClassMock([NSDate class]); + OCMStub([dateMock date]).andReturn(date); + + // When + NSUInteger resultFlushInterval = [channel resolveFlushInterval]; + + // Then + XCTAssertEqual(resultFlushInterval, 0); + + // Clear + [dateMock stopMocking]; +} + +- (void)testResolveFlushInterval { + + // If + NSDate *date = [[NSDate alloc] initWithTimeIntervalSince1970:1000]; + MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + + // Configure channel. + channel.configuration = [[MSACChannelUnitConfiguration alloc] initWithGroupId:kMSACTestGroupId + priority:MSACPriorityDefault + flushInterval:2000 + batchSizeLimit:50 + pendingBatchesLimit:1]; + [self.settingsMock setObject:[[NSDate alloc] initWithTimeIntervalSince1970:500] forKey:channel.oldestPendingLogTimestampKey]; + id dateMock = OCMClassMock([NSDate class]); + OCMStub([dateMock date]).andReturn(date); + + // When + NSUInteger resultFlushInterval = [channel resolveFlushInterval]; + + // Then + XCTAssertEqual(resultFlushInterval, 1500); + + // Clear + [dateMock stopMocking]; +} + +- (void)testNewInstanceWasInitialisedCorrectly { + MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + assertThat(channel, notNilValue()); + assertThat(channel.configuration, equalTo(self.configuration)); + assertThat(channel.ingestion, equalTo(self.ingestionMock)); + assertThat(channel.storage, equalTo(self.storageMock)); + assertThatUnsignedLong(channel.itemsCount, equalToInt(0)); +} + +- (void)testLogsSentWithSuccess { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + id delegateMock = OCMProtocolMock(@protocol(MSACChannelDelegate)); + __block MSACSendAsyncCompletionHandler ingestionBlock; + __block MSACLogContainer *logContainer; + __block NSString *expectedBatchId = @"1"; + NSUInteger batchSizeLimit = 1; + id expectedLog = [MSACAbstractLog new]; + expectedLog.sid = MSAC_UUID_STRING; + + // Init mocks. + id enqueuedLog = [self getValidMockLog]; + OCMStub([self.ingestionMock sendAsync:OCMOCK_ANY completionHandler:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + // Get ingestion block for later call. + [invocation retainArguments]; + [invocation getArgument:&logContainer atIndex:2]; + [invocation getArgument:&ingestionBlock atIndex:3]; + }); + __block id responseMock = [MSACHttpTestUtil createMockResponseForStatusCode:200 headers:nil]; + + // Stub the storage load for that log. + OCMStub([self.storageMock loadLogsWithGroupId:kMSACTestGroupId + limit:batchSizeLimit + excludedTargetKeys:OCMOCK_ANY + completionHandler:OCMOCK_ANY]) + .andDo(^(NSInvocation *invocation) { + MSACLoadDataCompletionHandler loadCallback; + + // Get ingestion block for later call. + [invocation getArgument:&loadCallback atIndex:5]; + + // Mock load. + loadCallback(((NSArray> *)@[ expectedLog ]), expectedBatchId); + }); + + // Configure channel. + channel.configuration = [[MSACChannelUnitConfiguration alloc] initWithGroupId:kMSACTestGroupId + priority:MSACPriorityDefault + flushInterval:0.0 + batchSizeLimit:batchSizeLimit + pendingBatchesLimit:1]; + + [channel addDelegate:delegateMock]; + OCMReject([delegateMock channel:channel didFailSendingLog:OCMOCK_ANY withError:OCMOCK_ANY]); + OCMExpect([delegateMock channel:channel didSucceedSendingLog:expectedLog]); + OCMExpect([delegateMock channel:channel prepareLog:enqueuedLog]); + OCMExpect([delegateMock channel:channel didPrepareLog:enqueuedLog internalId:OCMOCK_ANY flags:MSACFlagsDefault]); + OCMExpect([delegateMock channel:channel didCompleteEnqueueingLog:enqueuedLog internalId:OCMOCK_ANY]); + OCMExpect([self.storageMock deleteLogsWithBatchId:expectedBatchId groupId:kMSACTestGroupId]); + + // When + dispatch_async(channel.logsDispatchQueue, ^{ + // Enqueue now that the delegate is set. + [channel enqueueItem:enqueuedLog flags:MSACFlagsDefault]; + + // Try to release one batch. + dispatch_async(channel.logsDispatchQueue, ^{ + XCTAssertNotNil(ingestionBlock); + if (ingestionBlock) { + ingestionBlock([@(1) stringValue], responseMock, nil, nil); + } + + // Then + [self enqueueChannelEndJobExpectation]; + }); + }); + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + // Get sure it has been sent. + assertThat(logContainer.batchId, is(expectedBatchId)); + assertThat(logContainer.logs, is(@[ expectedLog ])); + assertThatBool(channel.pendingBatchQueueFull, isFalse()); + assertThatUnsignedLong(channel.pendingBatchIds.count, equalToUnsignedLong(0)); + OCMVerifyAll(delegateMock); + OCMVerifyAll(self.storageMock); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; + + [responseMock stopMocking]; +} + +- (void)testDelegateDeadlock { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + __block NSObject *lock = [NSObject new], *syncCallback = [NSObject new]; + + // Needed for waiting start of background thread. + dispatch_semaphore_t syncBackground = dispatch_semaphore_create(0); + [self initChannelEndJobExpectation]; + __block id mockLog1 = [self getValidMockLog]; + __block id mockLog2 = [self getValidMockLog]; + id delegateMock = OCMProtocolMock(@protocol(MSACChannelDelegate)); + OCMStub([delegateMock channel:channel didPrepareLog:OCMOCK_ANY internalId:OCMOCK_ANY flags:MSACFlagsDefault]) + .andDo(^(__unused NSInvocation *invocation) { + // Notify that didPrepareLog has been called. + objc_sync_exit(syncCallback); + + // Do something with syncronization. + @synchronized(lock) { + } + }); + [channel addDelegate:delegateMock]; + + // When + objc_sync_enter(syncCallback); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + @synchronized(lock) { + + // Notify that backround task has been called. + dispatch_semaphore_signal(syncBackground); + + // Wait when callback will be called from main thread. + @synchronized(syncCallback) { + } + + // Enqueue item from background thread. + [channel enqueueItem:mockLog2 flags:MSACFlagsNormal]; + } + }); + + // Make sure that backround task is started. + dispatch_semaphore_wait(syncBackground, DISPATCH_TIME_FOREVER); + + // Enqueue item from main thread. + [channel enqueueItem:mockLog1 flags:MSACFlagsNormal]; + [self enqueueChannelEndJobExpectation]; + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + OCMVerify([self.storageMock saveLog:mockLog1 withGroupId:OCMOCK_ANY flags:MSACFlagsNormal]); + OCMVerify([self.storageMock saveLog:mockLog2 withGroupId:OCMOCK_ANY flags:MSACFlagsNormal]); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testLogsSentWithRecoverableError { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + id delegateMock = OCMProtocolMock(@protocol(MSACChannelDelegate)); + __block MSACSendAsyncCompletionHandler ingestionBlock; + __block MSACLogContainer *logContainer; + __block NSString *expectedBatchId = @"1"; + NSUInteger batchSizeLimit = 1; + id expectedLog = [MSACAbstractLog new]; + expectedLog.sid = MSAC_UUID_STRING; + + // Init mocks. + id enqueuedLog = [self getValidMockLog]; + OCMStub([self.ingestionMock sendAsync:OCMOCK_ANY completionHandler:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + // Get ingestion block for later call. + [invocation retainArguments]; + [invocation getArgument:&logContainer atIndex:2]; + [invocation getArgument:&ingestionBlock atIndex:3]; + }); + __block id responseMock = [MSACHttpTestUtil createMockResponseForStatusCode:500 headers:nil]; + + // Stub the storage load for that log. + OCMStub([self.storageMock loadLogsWithGroupId:kMSACTestGroupId + limit:batchSizeLimit + excludedTargetKeys:OCMOCK_ANY + completionHandler:OCMOCK_ANY]) + .andDo(^(NSInvocation *invocation) { + MSACLoadDataCompletionHandler loadCallback; + + // Get ingestion block for later call. + [invocation getArgument:&loadCallback atIndex:5]; + + // Mock load. + loadCallback(((NSArray> *)@[ expectedLog ]), expectedBatchId); + }); + + // Configure channel. + channel.configuration = [[MSACChannelUnitConfiguration alloc] initWithGroupId:kMSACTestGroupId + priority:MSACPriorityDefault + flushInterval:0.0 + batchSizeLimit:batchSizeLimit + pendingBatchesLimit:1]; + [channel addDelegate:delegateMock]; + OCMExpect([delegateMock channel:channel didFailSendingLog:expectedLog withError:OCMOCK_ANY]); + OCMReject([delegateMock channel:channel didSucceedSendingLog:OCMOCK_ANY]); + OCMExpect([delegateMock channel:channel didPrepareLog:enqueuedLog internalId:OCMOCK_ANY flags:MSACFlagsDefault]); + OCMExpect([delegateMock channel:channel didCompleteEnqueueingLog:enqueuedLog internalId:OCMOCK_ANY]); + + // The logs shouldn't be deleted after recoverable error. + OCMReject([self.storageMock deleteLogsWithBatchId:expectedBatchId groupId:kMSACTestGroupId]); + + // When + dispatch_async(channel.logsDispatchQueue, ^{ + // Enqueue now that the delegate is set. + [channel enqueueItem:enqueuedLog flags:MSACFlagsDefault]; + + // Try to release one batch. + dispatch_async(channel.logsDispatchQueue, ^{ + XCTAssertNotNil(ingestionBlock); + if (ingestionBlock) { + ingestionBlock([@(1) stringValue], responseMock, nil, nil); + } + + // Then + [self enqueueChannelEndJobExpectation]; + }); + }); + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + // Get sure it has been sent. + assertThat(logContainer.batchId, is(expectedBatchId)); + assertThat(logContainer.logs, is(@[ expectedLog ])); + assertThatBool(channel.pendingBatchQueueFull, isFalse()); + assertThatBool(channel.enabled, isTrue()); + assertThatUnsignedLong(channel.pendingBatchIds.count, equalToUnsignedLong(0)); + OCMVerifyAll(delegateMock); + OCMVerifyAll(self.storageMock); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; + + [responseMock stopMocking]; +} + +- (void)testLogsSentWithUnrecoverableError { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + self.channelEndJobExpectation.expectedFulfillmentCount = 2; + id delegateMock = OCMProtocolMock(@protocol(MSACChannelDelegate)); + __block MSACSendAsyncCompletionHandler ingestionBlock; + __block MSACLogContainer *logContainer; + __block NSString *expectedBatchId = @"1"; + NSUInteger batchSizeLimit = 1; + id expectedLog = [MSACAbstractLog new]; + expectedLog.sid = MSAC_UUID_STRING; + + // Init mocks. + id enqueuedLog = [self getValidMockLog]; + OCMStub([self.ingestionMock sendAsync:OCMOCK_ANY completionHandler:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + // Get ingestion block for later call. + [invocation retainArguments]; + [invocation getArgument:&logContainer atIndex:2]; + [invocation getArgument:&ingestionBlock atIndex:3]; + }); + __block id responseMock = [MSACHttpTestUtil createMockResponseForStatusCode:300 headers:nil]; + + // Stub the storage load for that log. + OCMStub([self.storageMock loadLogsWithGroupId:kMSACTestGroupId + limit:batchSizeLimit + excludedTargetKeys:OCMOCK_ANY + completionHandler:OCMOCK_ANY]) + .andDo(^(NSInvocation *invocation) { + MSACLoadDataCompletionHandler loadCallback; + + // Get ingestion block for later call. + [invocation getArgument:&loadCallback atIndex:5]; + + // Mock load. + loadCallback(((NSArray> *)@[ expectedLog ]), expectedBatchId); + }); + + // Configure channel. + channel.configuration = [[MSACChannelUnitConfiguration alloc] initWithGroupId:kMSACTestGroupId + priority:MSACPriorityDefault + flushInterval:0.0 + batchSizeLimit:batchSizeLimit + pendingBatchesLimit:1]; + [channel addDelegate:delegateMock]; + OCMStub([delegateMock channel:channel didSetEnabled:NO andDeleteDataOnDisabled:YES]).andDo(^(__unused NSInvocation *invocation) { + [self enqueueChannelEndJobExpectation]; + }); + OCMExpect([delegateMock channel:channel didFailSendingLog:expectedLog withError:OCMOCK_ANY]); + OCMReject([delegateMock channel:channel didSucceedSendingLog:OCMOCK_ANY]); + OCMExpect([delegateMock channel:channel didPrepareLog:enqueuedLog internalId:OCMOCK_ANY flags:MSACFlagsDefault]); + OCMExpect([delegateMock channel:channel didCompleteEnqueueingLog:enqueuedLog internalId:OCMOCK_ANY]); + + // The logs should be deleted after unrecoverable error. + OCMExpect([self.storageMock deleteLogsWithBatchId:expectedBatchId groupId:kMSACTestGroupId]); + + // When + dispatch_async(channel.logsDispatchQueue, ^{ + // Enqueue now that the delegate is set. + [channel enqueueItem:enqueuedLog flags:MSACFlagsDefault]; + + // Try to release one batch. + dispatch_async(channel.logsDispatchQueue, ^{ + XCTAssertNotNil(ingestionBlock); + if (ingestionBlock) { + ingestionBlock([@(1) stringValue], responseMock, nil, nil); + } + [self enqueueChannelEndJobExpectation]; + }); + }); + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + // Make sure it has been sent. + assertThat(logContainer.batchId, is(expectedBatchId)); + assertThat(logContainer.logs, is(@[ expectedLog ])); + + // Make sure channel is disabled and cleaned up logs. + XCTAssertFalse(channel.enabled); + OCMVerifyAll(delegateMock); + OCMVerifyAll(self.storageMock); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; + [responseMock stopMocking]; +} + +- (void)testEnqueuingItemsWillIncreaseCounter { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + MSACChannelUnitConfiguration *config = [[MSACChannelUnitConfiguration alloc] initWithGroupId:kMSACTestGroupId + priority:MSACPriorityDefault + flushInterval:5 + batchSizeLimit:10 + pendingBatchesLimit:3]; + channel.configuration = config; + int itemsToAdd = 3; + + // When + for (int i = 1; i <= itemsToAdd; i++) { + [channel enqueueItem:[self getValidMockLog] flags:MSACFlagsDefault]; + } + [self enqueueChannelEndJobExpectation]; + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + assertThatUnsignedLong(channel.itemsCount, equalToInt(itemsToAdd)); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testNotCheckingPendingLogsOnEnqueueFailure { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + channel.configuration = [[MSACChannelUnitConfiguration alloc] initWithGroupId:kMSACTestGroupId + priority:MSACPriorityDefault + flushInterval:5 + batchSizeLimit:10 + pendingBatchesLimit:3]; + channel.storage = self.storageMock = OCMProtocolMock(@protocol(MSACStorage)); + OCMStub([self.storageMock saveLog:OCMOCK_ANY withGroupId:OCMOCK_ANY flags:MSACFlagsDefault]).andReturn(NO); + id channelUnitMock = OCMPartialMock(channel); + OCMReject([channelUnitMock checkPendingLogs]); + int itemsToAdd = 3; + + // When + for (int i = 1; i <= itemsToAdd; i++) { + [channel enqueueItem:[self getValidMockLog] flags:MSACFlagsDefault]; + } + [self enqueueChannelEndJobExpectation]; + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + assertThatUnsignedLong(channel.itemsCount, equalToInt(0)); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; + [channelUnitMock stopMocking]; +} + +- (void)testEnqueueCriticalItem { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + id mockLog = [self getValidMockLog]; + + // When + [channel enqueueItem:mockLog flags:MSACFlagsCritical]; + [self enqueueChannelEndJobExpectation]; + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + OCMVerify([self.storageMock saveLog:mockLog withGroupId:OCMOCK_ANY flags:MSACFlagsCritical]); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testEnqueueNonCriticalItem { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + id mockLog = [self getValidMockLog]; + + // When + [channel enqueueItem:mockLog flags:MSACFlagsNormal]; + [self enqueueChannelEndJobExpectation]; + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + OCMVerify([self.storageMock saveLog:mockLog withGroupId:OCMOCK_ANY flags:MSACFlagsNormal]); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testEnqueueItemWithFlagsDefault { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + id mockLog = [self getValidMockLog]; + + // When + [channel enqueueItem:mockLog flags:MSACFlagsDefault]; + [self enqueueChannelEndJobExpectation]; + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + OCMVerify([self.storageMock saveLog:mockLog withGroupId:OCMOCK_ANY flags:MSACFlagsDefault]); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testQueueFlushedAfterBatchSizeReached { + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + channel.configuration = [[MSACChannelUnitConfiguration alloc] initWithGroupId:kMSACTestGroupId + priority:MSACPriorityDefault + flushInterval:0.0 + batchSizeLimit:3 + pendingBatchesLimit:3]; + int itemsToAdd = 3; + XCTestExpectation *expectation = [self expectationWithDescription:@"All items enqueued"]; + id mockLog = [self getValidMockLog]; + id delegateMock = OCMProtocolMock(@protocol(MSACChannelDelegate)); + OCMStub([delegateMock channel:channel didCompleteEnqueueingLog:mockLog internalId:OCMOCK_ANY]) + .andDo(^(__unused NSInvocation *invocation) { + static int count = 0; + count++; + if (count == itemsToAdd) { + [expectation fulfill]; + } + }); + [channel addDelegate:delegateMock]; + + // When + for (int i = 0; i < itemsToAdd; ++i) { + [channel enqueueItem:mockLog flags:MSACFlagsCritical]; + } + [self enqueueChannelEndJobExpectation]; + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + assertThatUnsignedLong(channel.itemsCount, equalToInt(0)); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testBatchQueueLimit { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + NSUInteger batchSizeLimit = 1; + __block int currentBatchId = 1; + __block NSMutableArray *sentBatchIds = [NSMutableArray new]; + __block MSACLogContainer *container; + __block MSACSendAsyncCompletionHandler ingestionBlock; + __block id responseMock = [MSACHttpTestUtil createMockResponseForStatusCode:200 headers:nil]; + NSUInteger expectedMaxPendingBatched = 2; + id expectedLog = [MSACAbstractLog new]; + expectedLog.sid = MSAC_UUID_STRING; + + // Set up mock and stubs. + OCMStub([self.ingestionMock sendAsync:OCMOCK_ANY completionHandler:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + [invocation retainArguments]; + [invocation getArgument:&container atIndex:2]; + [invocation getArgument:&ingestionBlock atIndex:3]; + if (container) { + [sentBatchIds addObject:container.batchId]; + } + }); + OCMStub([self.storageMock loadLogsWithGroupId:kMSACTestGroupId + limit:batchSizeLimit + excludedTargetKeys:OCMOCK_ANY + completionHandler:OCMOCK_ANY]) + .andDo(^(NSInvocation *invocation) { + MSACLoadDataCompletionHandler loadCallback; + + // Mock load. + [invocation getArgument:&loadCallback atIndex:5]; + loadCallback(((NSArray> *)@[ expectedLog ]), [@(currentBatchId++) stringValue]); + }); + channel.configuration = [[MSACChannelUnitConfiguration alloc] initWithGroupId:kMSACTestGroupId + priority:MSACPriorityDefault + flushInterval:0.0 + batchSizeLimit:batchSizeLimit + pendingBatchesLimit:expectedMaxPendingBatched]; + + // When + for (NSUInteger i = 1; i <= expectedMaxPendingBatched + 1; i++) { + [channel enqueueItem:[self getValidMockLog] flags:MSACFlagsDefault]; + } + + // Try to release one batch. It should trigger sending the last one. + dispatch_async(channel.logsDispatchQueue, ^{ + XCTAssertNotNil(ingestionBlock); + if (ingestionBlock) { + ingestionBlock([@(1) stringValue], responseMock, nil, nil); + } + [self enqueueChannelEndJobExpectation]; + }); + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + assertThatUnsignedLong(channel.pendingBatchIds.count, equalToUnsignedLong(expectedMaxPendingBatched)); + assertThatUnsignedLong(sentBatchIds.count, equalToUnsignedLong(expectedMaxPendingBatched + 1)); + assertThat(sentBatchIds[0], is(@"1")); + assertThat(sentBatchIds[1], is(@"2")); + assertThat(sentBatchIds[2], is(@"3")); + assertThatBool(channel.pendingBatchQueueFull, isTrue()); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; + [responseMock stopMocking]; +} + +- (void)testNextBatchSentIfPendingQueueGotRoomAgain { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + XCTestExpectation *oneLogSentExpectation = [self expectationWithDescription:@"One log sent"]; + __block MSACSendAsyncCompletionHandler ingestionBlock; + __block MSACLogContainer *lastBatchLogContainer; + __block int currentBatchId = 1; + NSUInteger batchSizeLimit = 1; + id expectedLog = [MSACAbstractLog new]; + expectedLog.sid = MSAC_UUID_STRING; + + // Init mocks. + OCMStub([self.ingestionMock sendAsync:OCMOCK_ANY completionHandler:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + // Get ingestion block for later call. + [invocation retainArguments]; + [invocation getArgument:&lastBatchLogContainer atIndex:2]; + [invocation getArgument:&ingestionBlock atIndex:3]; + }); + __block id responseMock = [MSACHttpTestUtil createMockResponseForStatusCode:200 headers:nil]; + + // Stub the storage load for that log. + OCMStub([self.storageMock loadLogsWithGroupId:kMSACTestGroupId + limit:batchSizeLimit + excludedTargetKeys:OCMOCK_ANY + completionHandler:OCMOCK_ANY]) + .andDo(^(NSInvocation *invocation) { + MSACLoadDataCompletionHandler loadCallback; + + // Get ingestion block for later call. + [invocation getArgument:&loadCallback atIndex:5]; + + // Mock load. + loadCallback(((NSArray> *)@[ expectedLog ]), [@(currentBatchId) stringValue]); + }); + + // Configure channel. + channel.configuration = [[MSACChannelUnitConfiguration alloc] initWithGroupId:kMSACTestGroupId + priority:MSACPriorityDefault + flushInterval:0.0 + batchSizeLimit:batchSizeLimit + pendingBatchesLimit:1]; + + // When + [channel enqueueItem:[self getValidMockLog] flags:MSACFlagsDefault]; + + // Try to release one batch. + dispatch_async(channel.logsDispatchQueue, ^{ + XCTAssertNotNil(ingestionBlock); + if (ingestionBlock) { + ingestionBlock([@(1) stringValue], responseMock, nil, nil); + } + + // Then + dispatch_async(channel.logsDispatchQueue, ^{ + // Batch queue should not be full; + assertThatBool(channel.pendingBatchQueueFull, isFalse()); + [oneLogSentExpectation fulfill]; + + // When + // Send another batch. + currentBatchId++; + [channel enqueueItem:[self getValidMockLog] flags:MSACFlagsDefault]; + [self enqueueChannelEndJobExpectation]; + }); + }); + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + // Get sure it has been sent. + assertThat(lastBatchLogContainer.batchId, is([@(currentBatchId) stringValue])); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; + [responseMock stopMocking]; +} + +- (void)testDontForwardLogsToIngestionOnDisabled { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + NSUInteger batchSizeLimit = 1; + id mockLog = [self getValidMockLog]; + OCMReject([self.ingestionMock sendAsync:OCMOCK_ANY completionHandler:OCMOCK_ANY]); + OCMStub([self.ingestionMock sendAsync:OCMOCK_ANY completionHandler:OCMOCK_ANY]); + OCMStub([self.storageMock loadLogsWithGroupId:kMSACTestGroupId + limit:batchSizeLimit + excludedTargetKeys:OCMOCK_ANY + completionHandler:([OCMArg invokeBlockWithArgs:((NSArray> *)@[ mockLog ]), @"1", nil])]); + channel.configuration = [[MSACChannelUnitConfiguration alloc] initWithGroupId:kMSACTestGroupId + priority:MSACPriorityDefault + flushInterval:0.0 + batchSizeLimit:batchSizeLimit + pendingBatchesLimit:10]; + + // When + [channel setEnabled:NO andDeleteDataOnDisabled:NO]; + [channel enqueueItem:mockLog flags:MSACFlagsDefault]; + [self enqueueChannelEndJobExpectation]; + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + OCMVerifyAll(self.ingestionMock); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testDeleteDataOnDisabled { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + NSUInteger batchSizeLimit = 1; + id mockLog = [self getValidMockLog]; + OCMStub([self.storageMock loadLogsWithGroupId:kMSACTestGroupId + limit:batchSizeLimit + excludedTargetKeys:OCMOCK_ANY + completionHandler:([OCMArg invokeBlockWithArgs:((NSArray> *)@[ mockLog ]), @"1", nil])]); + channel.configuration = [[MSACChannelUnitConfiguration alloc] initWithGroupId:kMSACTestGroupId + priority:MSACPriorityDefault + flushInterval:0.0 + batchSizeLimit:batchSizeLimit + pendingBatchesLimit:10]; + + // When + [channel enqueueItem:mockLog flags:MSACFlagsDefault]; + [channel setEnabled:NO andDeleteDataOnDisabled:YES]; + [self enqueueChannelEndJobExpectation]; + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + // Check that logs as been requested for + // deletion and that there is no batch left. + OCMVerify([self.storageMock deleteLogsWithGroupId:kMSACTestGroupId]); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testDontSaveLogsWhileDisabledWithDataDeletion { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + id mockLog = [self getValidMockLog]; + OCMReject([self.storageMock saveLog:OCMOCK_ANY withGroupId:OCMOCK_ANY flags:MSACFlagsDefault]); + id delegateMock = OCMProtocolMock(@protocol(MSACChannelDelegate)); + OCMStub([delegateMock channel:channel didCompleteEnqueueingLog:mockLog internalId:OCMOCK_ANY]) + .andDo(^(__unused NSInvocation *invocation) { + [self enqueueChannelEndJobExpectation]; + }); + [channel addDelegate:delegateMock]; + + // When + [channel setEnabled:NO andDeleteDataOnDisabled:YES]; + [channel enqueueItem:mockLog flags:MSACFlagsDefault]; + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + assertThatBool(channel.discardLogs, isTrue()); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testSaveLogsAfterReEnabled { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + [channel setEnabled:NO andDeleteDataOnDisabled:YES]; + id mockLog = [self getValidMockLog]; + id delegateMock = OCMProtocolMock(@protocol(MSACChannelDelegate)); + OCMStub([delegateMock channel:channel didCompleteEnqueueingLog:mockLog internalId:OCMOCK_ANY]) + .andDo(^(__unused NSInvocation *invocation) { + [self enqueueChannelEndJobExpectation]; + }); + [channel addDelegate:delegateMock]; + + // When + [channel setEnabled:YES andDeleteDataOnDisabled:NO]; + [channel enqueueItem:mockLog flags:MSACFlagsDefault]; + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + assertThatBool(channel.discardLogs, isFalse()); + OCMVerify([self.storageMock saveLog:mockLog withGroupId:OCMOCK_ANY flags:MSACFlagsDefault]); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; + + // If + [self initChannelEndJobExpectation]; + id otherMockLog = [self getValidMockLog]; + [channel setEnabled:NO andDeleteDataOnDisabled:NO]; + OCMStub([delegateMock channel:channel didCompleteEnqueueingLog:otherMockLog internalId:OCMOCK_ANY]) + .andDo(^(__unused NSInvocation *invocation) { + [self enqueueChannelEndJobExpectation]; + }); + + // When + [channel setEnabled:YES andDeleteDataOnDisabled:NO]; + [channel enqueueItem:otherMockLog flags:MSACFlagsDefault]; + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + assertThatBool(channel.discardLogs, isFalse()); + OCMVerify([self.storageMock saveLog:mockLog withGroupId:OCMOCK_ANY flags:MSACFlagsDefault]); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testPauseOnDisabled { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + [channel setEnabled:YES andDeleteDataOnDisabled:NO]; + + // When + [channel setEnabled:NO andDeleteDataOnDisabled:NO]; + + // Then + [self enqueueChannelEndJobExpectation]; + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + assertThatBool(channel.enabled, isFalse()); + assertThatBool(channel.paused, isTrue()); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testResumeOnEnabled { + + // If + __block BOOL result1, result2; + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + id ingestionMock = OCMProtocolMock(@protocol(MSACIngestionProtocol)); + channel.ingestion = ingestionMock; + + // When + [channel setEnabled:NO andDeleteDataOnDisabled:NO]; + dispatch_async(channel.logsDispatchQueue, ^{ + [channel resumeWithIdentifyingObject:self]; + }); + [channel setEnabled:YES andDeleteDataOnDisabled:NO]; + dispatch_async(channel.logsDispatchQueue, ^{ + result1 = channel.paused; + }); + [channel setEnabled:NO andDeleteDataOnDisabled:NO]; + dispatch_async(channel.logsDispatchQueue, ^{ + [channel pauseWithIdentifyingObject:self]; + dispatch_async(channel.logsDispatchQueue, ^{ + [channel setEnabled:YES andDeleteDataOnDisabled:NO]; + }); + dispatch_async(channel.logsDispatchQueue, ^{ + result2 = channel.paused; + }); + [self enqueueChannelEndJobExpectation]; + }); + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + assertThatBool(result1, isFalse()); + assertThatBool(result2, isTrue()); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testDelegateAfterChannelDisabled { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + id delegateMock = OCMProtocolMock(@protocol(MSACChannelDelegate)); + id mockLog = [self getValidMockLog]; + + // When + [channel addDelegate:delegateMock]; + [channel setEnabled:NO andDeleteDataOnDisabled:YES]; + + // Enqueue now that the delegate is set. + dispatch_async(channel.logsDispatchQueue, ^{ + [channel enqueueItem:mockLog flags:MSACFlagsDefault]; + [self enqueueChannelEndJobExpectation]; + }); + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + // Check the callbacks were invoked for logs. + OCMVerify([delegateMock channel:channel + didPrepareLog:mockLog + internalId:OCMOCK_ANY + flags:MSACFlagsDefault]); + OCMVerify([delegateMock channel:channel didCompleteEnqueueingLog:mockLog internalId:OCMOCK_ANY]); + OCMVerify([delegateMock channel:channel willSendLog:mockLog]); + OCMVerify([delegateMock channel:channel didFailSendingLog:mockLog withError:anything()]); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testDelegateAfterChannelPaused { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + NSObject *identifyingObject = [NSObject new]; + [self initChannelEndJobExpectation]; + id delegateMock = OCMProtocolMock(@protocol(MSACChannelDelegate)); + + // When + [channel addDelegate:delegateMock]; + + // Pause now that the delegate is set. + dispatch_async(channel.logsDispatchQueue, ^{ + [channel pauseWithIdentifyingObject:identifyingObject]; + [self enqueueChannelEndJobExpectation]; + }); + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + // Check the callbacks were invoked for logs. + OCMVerify([delegateMock channel:channel didPauseWithIdentifyingObject:identifyingObject]); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testDelegateAfterChannelResumed { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + NSObject *identifyingObject = [NSObject new]; + [self initChannelEndJobExpectation]; + id delegateMock = OCMProtocolMock(@protocol(MSACChannelDelegate)); + + // When + [channel addDelegate:delegateMock]; + + // Resume now that the delegate is set. + dispatch_async(channel.logsDispatchQueue, ^{ + [channel resumeWithIdentifyingObject:identifyingObject]; + [self enqueueChannelEndJobExpectation]; + }); + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + // Check the callbacks were invoked for logs. + OCMVerify([delegateMock channel:channel didResumeWithIdentifyingObject:identifyingObject]); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testDeviceAndTimestampAreAddedOnEnqueuing { + + // If + MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + id mockLog = [self getValidMockLog]; + mockLog.device = nil; + mockLog.timestamp = nil; + [self initChannelEndJobExpectation]; + + // When + [channel enqueueItem:mockLog flags:MSACFlagsDefault]; + + // Then + XCTAssertNotNil(mockLog.device); + XCTAssertNotNil(mockLog.timestamp); + [self enqueueChannelEndJobExpectation]; + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testDeviceAndTimestampAreNotOverwrittenOnEnqueuing { + + // If + MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + id mockLog = [self getValidMockLog]; + MSACDevice *device = mockLog.device = [MSACDevice new]; + NSDate *timestamp = mockLog.timestamp = [NSDate new]; + [self initChannelEndJobExpectation]; + + // When + [channel enqueueItem:mockLog flags:MSACFlagsDefault]; + + // Then + XCTAssertEqual(mockLog.device, device); + XCTAssertEqual(mockLog.timestamp, timestamp); + [self enqueueChannelEndJobExpectation]; + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testEnqueuingLogDoesNotPersistFilteredLogs { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + OCMReject([self.storageMock saveLog:OCMOCK_ANY withGroupId:OCMOCK_ANY flags:MSACFlagsDefault]); + + id log = [self getValidMockLog]; + id delegateMock = OCMProtocolMock(@protocol(MSACChannelDelegate)); + OCMStub([delegateMock channelUnit:channel shouldFilterLog:log]).andReturn(YES); + id delegateMock2 = OCMProtocolMock(@protocol(MSACChannelDelegate)); + OCMStub([delegateMock2 channelUnit:channel shouldFilterLog:log]).andReturn(NO); + OCMExpect([delegateMock channel:channel prepareLog:log]); + OCMExpect([delegateMock2 channel:channel prepareLog:log]); + OCMExpect([delegateMock channel:channel didPrepareLog:log internalId:OCMOCK_ANY flags:MSACFlagsDefault]); + OCMExpect([delegateMock2 channel:channel didPrepareLog:log internalId:OCMOCK_ANY flags:MSACFlagsDefault]); + OCMExpect([delegateMock channel:channel didCompleteEnqueueingLog:log internalId:OCMOCK_ANY]); + OCMExpect([delegateMock2 channel:channel didCompleteEnqueueingLog:log internalId:OCMOCK_ANY]); + [channel addDelegate:delegateMock]; + [channel addDelegate:delegateMock2]; + + // When + dispatch_async(channel.logsDispatchQueue, ^{ + // Enqueue now that the delegate is set. + [channel enqueueItem:log flags:MSACFlagsDefault]; + [self enqueueChannelEndJobExpectation]; + }); + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + OCMVerifyAll(delegateMock); + OCMVerifyAll(delegateMock2); + OCMVerifyAll(self.storageMock); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testEnqueuingLogPersistsUnfilteredLogs { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + id log = [self getValidMockLog]; + id delegateMock = OCMProtocolMock(@protocol(MSACChannelDelegate)); + OCMStub([delegateMock channelUnit:channel shouldFilterLog:log]).andReturn(NO); + OCMExpect([delegateMock channel:channel didPrepareLog:log internalId:OCMOCK_ANY flags:MSACFlagsDefault]); + OCMExpect([delegateMock channel:channel didCompleteEnqueueingLog:log internalId:OCMOCK_ANY]); + [channel addDelegate:delegateMock]; + + // When + dispatch_async(channel.logsDispatchQueue, ^{ + // Enqueue now that the delegate is set. + [channel enqueueItem:log flags:MSACFlagsDefault]; + [self enqueueChannelEndJobExpectation]; + }); + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + OCMVerifyAll(delegateMock); + OCMVerify([self.storageMock saveLog:log withGroupId:OCMOCK_ANY flags:MSACFlagsDefault]); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testDoesntResumeWhenNotAllPauseObjectsResumed { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + NSObject *object1 = [NSObject new]; + NSObject *object2 = [NSObject new]; + NSObject *object3 = [NSObject new]; + [channel pauseWithIdentifyingObject:object1]; + [channel pauseWithIdentifyingObject:object2]; + [channel pauseWithIdentifyingObject:object3]; + + // When + [channel resumeWithIdentifyingObject:object1]; + [channel resumeWithIdentifyingObject:object3]; + + // Then + [self enqueueChannelEndJobExpectation]; + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + XCTAssertTrue(channel.paused); + }]; +} + +- (void)testResumesWhenAllPauseObjectsResumed { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + NSObject *object1 = [NSObject new]; + NSObject *object2 = [NSObject new]; + NSObject *object3 = [NSObject new]; + [channel pauseWithIdentifyingObject:object1]; + [channel pauseWithIdentifyingObject:object2]; + [channel pauseWithIdentifyingObject:object3]; + + // When + [channel resumeWithIdentifyingObject:object1]; + [channel resumeWithIdentifyingObject:object2]; + [channel resumeWithIdentifyingObject:object3]; + + // Then + [self enqueueChannelEndJobExpectation]; + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + XCTAssertFalse(channel.paused); + }]; +} + +- (void)testResumeWhenOnlyPausedObjectIsDeallocated { + + // If + MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + __weak NSObject *weakObject = nil; + @autoreleasepool { + +// Ignore warning on weak variable usage in this scope to simulate dealloc. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-unsafe-retained-assign" + weakObject = [NSObject new]; +#pragma clang diagnostic pop +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-repeated-use-of-weak" + [channel pauseWithIdentifyingObjectSync:weakObject]; +#pragma clang diagnostic pop + } + + // Then + XCTAssertTrue(channel.paused); + + // When + [channel resumeWithIdentifyingObjectSync:[NSObject new]]; + + // Then + XCTAssertFalse(channel.paused); +} + +- (void)testResumeWithObjectThatDoesNotExistDoesNotResumeIfCurrentlyPaused { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + NSObject *object1 = [NSObject new]; + NSObject *object2 = [NSObject new]; + [channel pauseWithIdentifyingObject:object1]; + + // When + [channel resumeWithIdentifyingObject:object2]; + + // Then + [self enqueueChannelEndJobExpectation]; + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + XCTAssertTrue(channel.paused); + }]; +} + +- (void)testResumeWithObjectThatDoesNotExistDoesNotPauseIfPreviouslyResumed { + + // When + MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [channel resumeWithIdentifyingObjectSync:[NSObject new]]; + + // Then + XCTAssertFalse(channel.paused); +} + +- (void)testResumeTwiceInARowResumesWhenPaused { + + // If + MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + NSObject *object = [NSObject new]; + [channel pauseWithIdentifyingObjectSync:object]; + + // When + [channel resumeWithIdentifyingObjectSync:object]; + [channel resumeWithIdentifyingObjectSync:object]; + + // Then + XCTAssertFalse(channel.paused); +} + +- (void)testResumeOnceResumesWhenPausedTwiceWithSingleObject { + + // If + MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + NSObject *object = [NSObject new]; + [channel pauseWithIdentifyingObjectSync:object]; + [channel pauseWithIdentifyingObjectSync:object]; + + // When + [channel resumeWithIdentifyingObjectSync:object]; + + // Then + XCTAssertFalse(channel.paused); +} + +- (void)testPausedTargetKeysNotAlteredWhenChannelUnitPaused { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + NSObject *object = [NSObject new]; + NSString *targetKey = @"targetKey"; + NSString *token = [NSString stringWithFormat:@"%@-secret", targetKey]; + [channel pauseSendingLogsWithToken:token]; + + // When + [channel pauseWithIdentifyingObjectSync:object]; + + // Then + [self enqueueChannelEndJobExpectation]; + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + assertThatUnsignedLong(channel.pausedTargetKeys.count, equalToUnsignedLong(1)); + XCTAssertTrue([channel.pausedTargetKeys containsObject:targetKey]); + }]; +} + +- (void)testPausedTargetKeysNotAlteredWhenChannelUnitResumed { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + NSObject *object = [NSObject new]; + NSString *targetKey = @"targetKey"; + NSString *token = [NSString stringWithFormat:@"%@-secret", targetKey]; + [channel pauseSendingLogsWithToken:token]; + [channel pauseWithIdentifyingObject:object]; + + // When + [channel resumeWithIdentifyingObject:object]; + + // Then + [self enqueueChannelEndJobExpectation]; + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + assertThatUnsignedLong(channel.pausedTargetKeys.count, equalToUnsignedLong(1)); + XCTAssertTrue([channel.pausedTargetKeys containsObject:targetKey]); + }]; +} + +- (void)testNoLogsRetrievedFromStorageWhenTargetKeyIsPaused { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + NSString *targetKey = @"targetKey"; + NSString *token = [NSString stringWithFormat:@"%@-secret", targetKey]; + __block NSArray *excludedKeys; + OCMStub([self.storageMock loadLogsWithGroupId:channel.configuration.groupId + limit:channel.configuration.batchSizeLimit + excludedTargetKeys:OCMOCK_ANY + completionHandler:OCMOCK_ANY]) + .andDo(^(NSInvocation *invocation) { + [invocation getArgument:&excludedKeys atIndex:4]; + }); + [channel pauseSendingLogsWithToken:token]; + + // When + dispatch_async(channel.logsDispatchQueue, ^{ + [channel flushQueue]; + [self enqueueChannelEndJobExpectation]; + }); + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + assertThatUnsignedLong(excludedKeys.count, equalToUnsignedLong(1)); + XCTAssertTrue([excludedKeys containsObject:targetKey]); + }]; +} + +- (void)testLogsStoredWhenTargetKeyIsPaused { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + NSString *targetKey = @"targetKey"; + NSString *token = [NSString stringWithFormat:@"%@-secret", targetKey]; + [channel pauseSendingLogsWithToken:token]; + MSACCommonSchemaLog *log = [MSACCommonSchemaLog new]; + [log addTransmissionTargetToken:token]; + log.ver = @"3.0"; + log.name = @"test"; + log.iKey = targetKey; + + // When + [channel enqueueItem:log flags:MSACFlagsDefault]; + + // Then + [self enqueueChannelEndJobExpectation]; + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + OCMVerify([self.storageMock saveLog:log withGroupId:channel.configuration.groupId flags:MSACFlagsDefault]); + }]; +} + +- (void)testSendingPendingLogsOnResume { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + NSString *targetKey = @"targetKey"; + NSString *token = [NSString stringWithFormat:@"%@-secret", targetKey]; + id channelUnitMock = OCMPartialMock(channel); + [channel pauseSendingLogsWithToken:token]; + OCMStub([self.storageMock countLogs]).andReturn(60); + + // When + [channel resumeSendingLogsWithToken:token]; + + // Then + [self enqueueChannelEndJobExpectation]; + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + + OCMVerify([self.storageMock countLogs]); + OCMVerify([channelUnitMock checkPendingLogs]); + + // The count should be 0 since the logs were sent and not in pending state anymore. + XCTAssertEqual(channel.itemsCount, 0); + }]; + [channelUnitMock stopMocking]; +} + +- (void)testTargetKeyRemainsPausedWhenPausedASecondTime { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + NSString *targetKey = @"targetKey"; + NSString *token = [NSString stringWithFormat:@"%@-secret", targetKey]; + [channel pauseSendingLogsWithToken:token]; + + // When + [channel pauseSendingLogsWithToken:token]; + + // Then + [self enqueueChannelEndJobExpectation]; + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + assertThatUnsignedLong(channel.pausedTargetKeys.count, equalToUnsignedLong(1)); + XCTAssertTrue([channel.pausedTargetKeys containsObject:targetKey]); + }]; +} + +- (void)testTargetKeyRemainsResumedWhenResumedASecondTime { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + NSString *targetKey = @"targetKey"; + NSString *token = [NSString stringWithFormat:@"%@-secret", targetKey]; + [channel pauseSendingLogsWithToken:token]; + + // Then + [self enqueueChannelEndJobExpectation]; + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + assertThatUnsignedLong(channel.pausedTargetKeys.count, equalToUnsignedLong(1)); + XCTAssertTrue([channel.pausedTargetKeys containsObject:targetKey]); + }]; + + // If + [self initChannelEndJobExpectation]; + + // When + [channel resumeSendingLogsWithToken:token]; + [self enqueueChannelEndJobExpectation]; + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + assertThatUnsignedLong(channel.pausedTargetKeys.count, equalToUnsignedLong(0)); + }]; + + // If + [self initChannelEndJobExpectation]; + + // When + [channel resumeSendingLogsWithToken:token]; + [self enqueueChannelEndJobExpectation]; + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + assertThatUnsignedLong(channel.pausedTargetKeys.count, equalToUnsignedLong(0)); + }]; +} + +- (void)testEnqueueItemDoesNotSetUserIdWhenItAlreadyHasOne { + + // If + __block MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + [self initChannelEndJobExpectation]; + id enqueuedLog = [self getValidMockLog]; + NSString *expectedUserId = @"Fake-UserId"; + __block MSACAbstractLog *log; + id userIdContextMock = OCMClassMock([MSACUserIdContext class]); + OCMStub([userIdContextMock sharedInstance]).andReturn(userIdContextMock); + OCMStub([userIdContextMock userId]).andReturn(@"SomethingElse"); + channel.storage = self.storageMock = OCMProtocolMock(@protocol(MSACStorage)); + OCMStub([channel.storage saveLog:OCMOCK_ANY withGroupId:OCMOCK_ANY flags:MSACFlagsNormal]) + .andDo(^(NSInvocation *invocation) { + [invocation getArgument:&log atIndex:2]; + [self enqueueChannelEndJobExpectation]; + }) + .andReturn(YES); + + // When + enqueuedLog.userId = expectedUserId; + [channel enqueueItem:enqueuedLog flags:MSACFlagsDefault]; + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + XCTAssertEqual(log.userId, expectedUserId); + }]; + [userIdContextMock stopMocking]; +} + +- (void)testAddRemoveDelegate { + + // If + XCTestExpectation *addDelegateExpectation = [self expectationWithDescription:@"Add a channel delegate"]; + XCTestExpectation *removeDelegateExpectation = [self expectationWithDescription:@"Remove a channel delegate"]; + MSACChannelUnitDefault *channel = [self createChannelUnitDefault]; + id delegateMock = OCMProtocolMock(@protocol(MSACChannelDelegate)); + + // When + [channel addDelegate:delegateMock]; + dispatch_async(channel.logsDispatchQueue, ^{ + // Then + XCTAssertEqual(channel.delegates.count, 1); + XCTAssertTrue([channel.delegates containsObject:delegateMock]); + [addDelegateExpectation fulfill]; + }); + [channel removeDelegate:delegateMock]; + dispatch_async(channel.logsDispatchQueue, ^{ + // Then + XCTAssertEqual(channel.delegates.count, 0); + [removeDelegateExpectation fulfill]; + }); + + // Then + [self waitForExpectations:@[ addDelegateExpectation, removeDelegateExpectation ] timeout:kMSACTestTimeout enforceOrder:YES]; +} + +#pragma mark - Helper + +- (void)initChannelEndJobExpectation { + self.channelEndJobExpectation = [self expectationWithDescription:@"Channel job should be finished"]; +} + +- (void)enqueueChannelEndJobExpectation { + + // Enqueue end job expectation on channel's queue to detect when channel + // finished processing. + dispatch_async(self.dispatchQueue, ^{ + [self.channelEndJobExpectation fulfill]; + }); +} + +- (NSArray> *)getValidMockLogArrayForDate:(NSDate *)date andCount:(NSUInteger)count { + NSMutableArray> *logs = [NSMutableArray> new]; + for (NSUInteger i = 0; i < count; i++) { + [logs addObject:[self getValidMockLogWithDate:[date dateByAddingTimeInterval:i]]]; + } + return logs; +} + +- (id)getValidMockLog { + id mockLog = OCMPartialMock([MSACAbstractLog new]); + OCMStub([mockLog isValid]).andReturn(YES); + return mockLog; +} + +- (id)getValidMockLogWithDate:(NSDate *)date { + id mockLog = OCMPartialMock([MSACAbstractLog new]); + OCMStub([mockLog timestamp]).andReturn(date); + OCMStub([mockLog isValid]).andReturn(YES); + return mockLog; +} + +- (MSACChannelUnitDefault *)createChannelUnitDefault { + dispatch_queue_t queue = dispatch_queue_create(nil, DISPATCH_QUEUE_SERIAL); + self.dispatchQueue = queue; + return [[MSACChannelUnitDefault alloc] initWithIngestion:self.ingestionMock + storage:self.storageMock + configuration:self.configuration + logsDispatchQueue:queue]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACCommonSchemaLogTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACCommonSchemaLogTests.m new file mode 100644 index 0000000000..3a0be05053 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACCommonSchemaLogTests.m @@ -0,0 +1,317 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAppExtension.h" +#import "MSACCSData.h" +#import "MSACCSExtensions.h" +#import "MSACCommonSchemaLog.h" +#import "MSACConstants.h" +#import "MSACDevice.h" +#import "MSACLocExtension.h" +#import "MSACLogContainer.h" +#import "MSACModelTestsUtililty.h" +#import "MSACNetExtension.h" +#import "MSACOSExtension.h" +#import "MSACProtocolExtension.h" +#import "MSACSDKExtension.h" +#import "MSACTestFrameworks.h" +#import "MSACUserExtension.h" +#import "MSACUtility+Date.h" + +@interface MSACCommonSchemaLogTests : XCTestCase +@property(nonatomic) MSACCommonSchemaLog *commonSchemaLog; +@property(nonatomic) NSMutableDictionary *csLogDummyValues; +@end + +@implementation MSACCommonSchemaLogTests + +- (void)setUp { + [super setUp]; + id device = OCMClassMock([MSACDevice class]); + OCMStub([device isValid]).andReturn(YES); + NSDictionary *abstractDummies = [MSACModelTestsUtililty abstractLogDummies]; + self.csLogDummyValues = [@{ + kMSACCSVer : @"3.0", + kMSACCSName : @"1DS", + kMSACCSTime : abstractDummies[kMSACTimestamp], + kMSACCSIKey : @"o:60cd0b94-6060-11e8-9c2d-fa7ae01bbebc", + kMSACCSFlags : @(MSACFlagsNormal), + kMSACCSExt : [self extWithDummyValues], + kMSACCSData : [self dataWithDummyValues] + } mutableCopy]; + [self.csLogDummyValues addEntriesFromDictionary:abstractDummies]; + self.commonSchemaLog = [self csLogWithDummyValues:self.csLogDummyValues]; +} + +- (void)tearDown { + [super tearDown]; +} + +#pragma mark - MSACCommonSchemaLog + +- (void)testCSLogJSONSerializingToDictionary { + + // If + MSACOrderedDictionary *expectedSerializedLog = [MSACOrderedDictionary new]; + [expectedSerializedLog setObject:@"3.0" forKey:kMSACCSVer]; + [expectedSerializedLog setObject:@"1DS" forKey:kMSACCSName]; + [expectedSerializedLog setObject:[MSACUtility dateToISO8601:self.csLogDummyValues[kMSACCSTime]] forKey:kMSACCSTime]; + [expectedSerializedLog setObject:@"o:60cd0b94-6060-11e8-9c2d-fa7ae01bbebc" forKey:kMSACCSIKey]; + [expectedSerializedLog setObject:@(MSACFlagsNormal) forKey:kMSACCSFlags]; + [expectedSerializedLog setObject:[self.csLogDummyValues[kMSACCSExt] serializeToDictionary] forKey:kMSACCSExt]; + [expectedSerializedLog setObject:[self.csLogDummyValues[kMSACCSData] serializeToDictionary] forKey:kMSACCSData]; + + // When + NSMutableDictionary *serializedLog = [self.commonSchemaLog serializeToDictionary]; + + // Then + XCTAssertNotNil(serializedLog); + XCTAssertTrue([expectedSerializedLog isEqualToDictionary:serializedLog]); +} + +- (void)testCSLogNSCodingSerializationAndDeserialization { + + // When + NSData *serializedCSLog = [MSACUtility archiveKeyedData:self.commonSchemaLog]; + MSACCommonSchemaLog *actualCSLog = (MSACCommonSchemaLog *)[MSACUtility unarchiveKeyedData:serializedCSLog]; + + // Then + XCTAssertNotNil(actualCSLog); + XCTAssertEqualObjects(self.commonSchemaLog, actualCSLog); + XCTAssertTrue([actualCSLog isMemberOfClass:[MSACCommonSchemaLog class]]); + XCTAssertEqualObjects(actualCSLog.ver, self.csLogDummyValues[kMSACCSVer]); + XCTAssertEqualObjects(actualCSLog.name, self.csLogDummyValues[kMSACCSName]); + XCTAssertEqualObjects(actualCSLog.timestamp, self.csLogDummyValues[kMSACCSTime]); + XCTAssertEqual(actualCSLog.popSample, [self.csLogDummyValues[kMSACCSPopSample] doubleValue]); + XCTAssertEqualObjects(actualCSLog.iKey, self.csLogDummyValues[kMSACCSIKey]); + XCTAssertEqual(actualCSLog.flags, [self.csLogDummyValues[kMSACCSFlags] longLongValue]); + XCTAssertEqualObjects(actualCSLog.cV, self.csLogDummyValues[kMSACCSCV]); + XCTAssertEqualObjects(actualCSLog.ext, self.csLogDummyValues[kMSACCSExt]); + XCTAssertEqualObjects(actualCSLog.data, self.csLogDummyValues[kMSACCSData]); +} + +- (void)testCSLogIsValid { + + // If + MSACCommonSchemaLog *csLog = [MSACCommonSchemaLog new]; + + // Then + XCTAssertFalse([csLog isValid]); + + // If + csLog.ver = self.csLogDummyValues[kMSACCSVer]; + + // Then + XCTAssertFalse([csLog isValid]); + + // If + csLog.name = self.csLogDummyValues[kMSACCSName]; + + // Then + XCTAssertFalse([csLog isValid]); + + // If + csLog.timestamp = self.csLogDummyValues[kMSACCSTime]; + + // Then + XCTAssertTrue([csLog isValid]); + + // IF + [MSACModelTestsUtililty populateAbstractLogWithDummies:csLog]; + + // Then + XCTAssertTrue([csLog isValid]); +} + +- (void)testCSLogIsEqual { + + // If + MSACCommonSchemaLog *anotherCommonSchemaLog = [MSACCommonSchemaLog new]; + + // Then + XCTAssertNotEqualObjects(anotherCommonSchemaLog, self.commonSchemaLog); + + // If + anotherCommonSchemaLog = [self csLogWithDummyValues:self.csLogDummyValues]; + + // Then + XCTAssertEqualObjects(anotherCommonSchemaLog, self.commonSchemaLog); + + // If + anotherCommonSchemaLog.ver = @"2.0"; + + // Then + XCTAssertNotEqualObjects(anotherCommonSchemaLog, self.commonSchemaLog); + + // If + anotherCommonSchemaLog.ver = self.csLogDummyValues[kMSACCSVer]; + anotherCommonSchemaLog.name = @"Alpha SDK"; + + // Then + XCTAssertNotEqualObjects(anotherCommonSchemaLog, self.commonSchemaLog); + + // If + anotherCommonSchemaLog.name = self.csLogDummyValues[kMSACCSName]; + anotherCommonSchemaLog.timestamp = [NSDate date]; + + // Then + XCTAssertNotEqualObjects(anotherCommonSchemaLog, self.commonSchemaLog); + + // If + anotherCommonSchemaLog.timestamp = self.csLogDummyValues[kMSACCSTime]; + anotherCommonSchemaLog.popSample = 101; + + // Then + XCTAssertNotEqualObjects(anotherCommonSchemaLog, self.commonSchemaLog); + + // If + anotherCommonSchemaLog.popSample = [self.csLogDummyValues[kMSACCSPopSample] doubleValue]; + anotherCommonSchemaLog.iKey = @"o:0bcff4a2-6377-11e8-adc0-fa7ae01bbebc"; + + // Then + XCTAssertNotEqualObjects(anotherCommonSchemaLog, self.commonSchemaLog); + + // If + anotherCommonSchemaLog.iKey = self.csLogDummyValues[kMSACCSIKey]; + anotherCommonSchemaLog.flags = 31415927; + + // Then + XCTAssertNotEqualObjects(anotherCommonSchemaLog, self.commonSchemaLog); + + // If + anotherCommonSchemaLog.flags = [self.csLogDummyValues[kMSACCSFlags] longLongValue]; + anotherCommonSchemaLog.cV = @"HyCFaiQoBkyEp0L3.1.3"; + + // Then + XCTAssertNotEqualObjects(anotherCommonSchemaLog, self.commonSchemaLog); + + // If + anotherCommonSchemaLog.cV = self.csLogDummyValues[kMSACCSCV]; + anotherCommonSchemaLog.ext = OCMClassMock([MSACCSExtensions class]); + + // Then + XCTAssertNotEqualObjects(anotherCommonSchemaLog, self.commonSchemaLog); + + // If + anotherCommonSchemaLog.ext = self.csLogDummyValues[kMSACCSExt]; + anotherCommonSchemaLog.data = OCMClassMock([MSACCSData class]); + + // Then + XCTAssertNotEqualObjects(anotherCommonSchemaLog, self.commonSchemaLog); + + // If + anotherCommonSchemaLog.data = self.csLogDummyValues[kMSACCSData]; + anotherCommonSchemaLog.flags = -1; + + // Then + XCTAssertNotEqualObjects(anotherCommonSchemaLog, self.commonSchemaLog); + + // If + anotherCommonSchemaLog.flags = [self.csLogDummyValues[kMSACCSFlags] longLongValue]; + + // Then + XCTAssertEqualObjects(anotherCommonSchemaLog, self.commonSchemaLog); +} + +- (void)testOrderedDictionaryPerformance { + NSMutableArray *logs = [NSMutableArray new]; + for (int i = 0; i < 10000; i++) { + [logs addObject:self.commonSchemaLog]; + } + MSACLogContainer *logContainer = [MSACLogContainer new]; + [logContainer setLogs:logs]; + [self measureBlock:^{ + [logContainer serializeLog]; + }]; +} + +#pragma mark - Helper + +- (MSACCSExtensions *)extWithDummyValues { + MSACCSExtensions *ext = [MSACCSExtensions new]; + ext.userExt = [self userExtWithDummyValues]; + ext.locExt = [self locExtWithDummyValues]; + ext.osExt = [self osExtWithDummyValues]; + ext.appExt = [self appExtWithDummyValues]; + ext.protocolExt = [self protocolExtWithDummyValues]; + ext.netExt = [self netExtWithDummyValues]; + ext.sdkExt = [self sdkExtWithDummyValues]; + return ext; +} + +- (MSACUserExtension *)userExtWithDummyValues { + MSACUserExtension *userExt = [MSACUserExtension new]; + userExt.localId = @"c:alice"; + userExt.locale = @"en-us"; + return userExt; +} + +- (MSACLocExtension *)locExtWithDummyValues { + MSACLocExtension *locExt = [MSACLocExtension new]; + locExt.tz = @"-05:00"; + return locExt; +} + +- (MSACOSExtension *)osExtWithDummyValues { + MSACOSExtension *osExt = [MSACOSExtension new]; + osExt.name = @"Android"; + osExt.ver = @"Android P"; + return osExt; +} + +- (MSACAppExtension *)appExtWithDummyValues { + MSACAppExtension *appExt = [MSACAppExtension new]; + appExt.appId = @"com.mamamia.bundle.id"; + appExt.ver = @"1.0.0"; + appExt.locale = @"fr-ca"; + appExt.userId = @"c:alice"; + return appExt; +} + +- (MSACProtocolExtension *)protocolExtWithDummyValues { + MSACProtocolExtension *protocolExt = [MSACProtocolExtension new]; + protocolExt.devMake = @"Samsung"; + protocolExt.devModel = @"Samsung Galaxy S8"; + return protocolExt; +} + +- (MSACNetExtension *)netExtWithDummyValues { + MSACNetExtension *netExt = [MSACNetExtension new]; + netExt.provider = @"M-Telecom"; + return netExt; +} + +- (MSACSDKExtension *)sdkExtWithDummyValues { + MSACSDKExtension *sdkExt = [MSACSDKExtension new]; + sdkExt.libVer = @"3.1.4"; + sdkExt.epoch = MSAC_UUID_STRING; + sdkExt.seq = 1; + sdkExt.installId = [NSUUID new]; + return sdkExt; +} + +- (MSACCSData *)dataWithDummyValues { + MSACCSData *data = [MSACCSData new]; + data.properties = [[MSACModelTestsUtililty unorderedDataDummies] copy]; + return data; +} + +- (MSACCommonSchemaLog *)csLogWithDummyValues:(NSDictionary *)dummyValues { + MSACCommonSchemaLog *csLog = [MSACCommonSchemaLog new]; + + /* + * These are deliberately out of order to verify that they are reordered properly when serialized. + * Correct order is ver, name, timestamp, (popSample), iKey, flags. + */ + csLog.name = dummyValues[kMSACCSName]; + csLog.timestamp = dummyValues[kMSACCSTime]; + csLog.ver = dummyValues[kMSACCSVer]; + csLog.iKey = dummyValues[kMSACCSIKey]; + csLog.flags = [dummyValues[kMSACCSFlags] longLongValue]; + csLog.ext = dummyValues[kMSACCSExt]; + csLog.data = dummyValues[kMSACCSData]; + [MSACModelTestsUtililty populateAbstractLogWithDummies:csLog]; + return csLog; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACCustomPropertiesLogTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACCustomPropertiesLogTests.m new file mode 100644 index 0000000000..7488254954 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACCustomPropertiesLogTests.m @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACCustomPropertiesLog.h" +#import "MSACDevice.h" +#import "MSACTestFrameworks.h" +#import "MSACUtility.h" + +@interface MSACCustomPropertiesLogTests : XCTestCase + +@property(nonatomic, strong) MSACCustomPropertiesLog *sut; + +@end + +@implementation MSACCustomPropertiesLogTests + +@synthesize sut = _sut; + +#pragma mark - Setup + +- (void)setUp { + [super setUp]; + self.sut = [MSACCustomPropertiesLog new]; +} + +#pragma mark - Tests + +- (void)testSerializingToDictionaryWorks { + + // If + NSString *string = @"test"; + NSDate *date = [NSDate dateWithTimeIntervalSince1970:0]; + NSNumber *number = @0; + NSNumber *boolean = @NO; + NSDictionary *properties = + @{@"t1" : string, @"t2" : date, @"t3" : number, @"t4" : boolean, @"t5" : [NSNull null], @"t6" : [NSData new]}; + self.sut.properties = properties; + + // When + NSMutableDictionary *actual = [self.sut serializeToDictionary]; + + // Then + assertThat(actual, notNilValue()); + NSArray *actualProperties = actual[@"properties"]; + assertThat(actualProperties, hasCountOf(5)); + NSArray *needProperties = @[ + @{@"name" : @"t1", @"type" : @"string", @"value" : string}, + @{@"name" : @"t2", @"type" : @"dateTime", @"value" : @"1970-01-01T00:00:00.000Z"}, + @{@"name" : @"t3", @"type" : @"number", @"value" : number}, @{@"name" : @"t4", @"type" : @"boolean", @"value" : boolean}, + @{@"name" : @"t5", @"type" : @"clear"} + ]; + actualProperties = [actualProperties sortedArrayUsingDescriptors:@[ [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES] ]]; + assertThat(actualProperties, equalTo(needProperties)); +} + +- (void)testNSCodingSerializationAndDeserializationWorks { + + // If + NSString *string = @"test"; + NSDate *date = [NSDate dateWithTimeIntervalSince1970:0]; + NSNumber *number = @0; + BOOL boolean = NO; + NSDictionary *properties = + @{@"t1" : string, @"t2" : date, @"t3" : number, @"t4" : @(boolean), @"t5" : [NSNull null]}; + self.sut.properties = properties; + + // When + NSData *serializedLog = [MSACUtility archiveKeyedData:self.sut]; + id actual = [MSACUtility unarchiveKeyedData:serializedLog]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual, instanceOf([MSACCustomPropertiesLog class])); + XCTAssertTrue([self.sut isEqual:actual]); + + MSACCustomPropertiesLog *log = actual; + NSDictionary *actualProperties = log.properties; + XCTAssertEqual(actualProperties.count, properties.count); + for (NSString *key in actualProperties) { + NSObject *actualValue = [actualProperties objectForKey:key]; + NSObject *value = [properties objectForKey:key]; + assertThat(actualValue, equalTo(value)); + } +} + +- (void)testIsValid { + + // If + self.sut.device = OCMClassMock([MSACDevice class]); + OCMStub([self.sut.device isValid]).andReturn(YES); + self.sut.timestamp = [NSDate dateWithTimeIntervalSince1970:42]; + self.sut.sid = @"1234567890"; + + // When + self.sut.properties = nil; + + // Then + XCTAssertFalse([self.sut isValid]); + + // When + self.sut.properties = @{}; + + // Then + XCTAssertFalse([self.sut isValid]); + + // When + self.sut.properties = @{@"test" : @42}; + + // Then + XCTAssertTrue([self.sut isValid]); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACCustomPropertiesTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACCustomPropertiesTests.m new file mode 100644 index 0000000000..807b40e2cc --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACCustomPropertiesTests.m @@ -0,0 +1,308 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACCustomProperties.h" +#import "MSACCustomPropertiesInternal.h" +#import "MSACTestFrameworks.h" + +@interface MSACCustomPropertiesTests : XCTestCase + +@end + +@implementation MSACCustomPropertiesTests + +- (void)testKeyValidate { + + // If + NSString *string = @"test"; + NSDate *date = [NSDate dateWithTimeIntervalSince1970:0]; + NSNumber *number = @0; + BOOL boolean = NO; + + // When + MSACCustomProperties *customProperties = [MSACCustomProperties new]; + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(0)); + + // Null key. + // When + NSString *nullKey = nil; + [customProperties setString:string forKey:nullKey]; + [customProperties setDate:date forKey:nullKey]; + [customProperties setNumber:number forKey:nullKey]; + [customProperties setBool:boolean forKey:nullKey]; + [customProperties clearPropertyForKey:nullKey]; + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(0)); + + // Invalid key. + // When + NSString *invalidKey = @"!"; + [customProperties setString:string forKey:invalidKey]; + [customProperties setDate:date forKey:invalidKey]; + [customProperties setNumber:number forKey:invalidKey]; + [customProperties setBool:boolean forKey:invalidKey]; + [customProperties clearPropertyForKey:invalidKey]; + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(0)); + + // Too long key. + // When + NSString *tooLongKey = [@"" stringByPaddingToLength:129 withString:@"a" startingAtIndex:0]; + [customProperties setString:string forKey:tooLongKey]; + [customProperties setDate:date forKey:tooLongKey]; + [customProperties setNumber:number forKey:tooLongKey]; + [customProperties setBool:boolean forKey:tooLongKey]; + [customProperties clearPropertyForKey:tooLongKey]; + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(0)); + + // Normal keys. + // When + NSString *maxLongKey = [@"" stringByPaddingToLength:128 withString:@"a" startingAtIndex:0]; + [customProperties setString:string forKey:@"t1"]; + [customProperties setDate:date forKey:@"t2"]; + [customProperties setNumber:number forKey:@"t3"]; + [customProperties setBool:boolean forKey:@"t4"]; + [customProperties clearPropertyForKey:@"t5"]; + [customProperties setString:string forKey:maxLongKey]; + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(6)); + + // Already contains keys. + // When + [customProperties setString:string forKey:@"t1"]; + [customProperties setDate:date forKey:@"t2"]; + [customProperties setNumber:number forKey:@"t3"]; + [customProperties setBool:boolean forKey:@"t4"]; + [customProperties clearPropertyForKey:@"t5"]; + [customProperties setString:string forKey:maxLongKey]; + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(6)); +} + +- (void)testMaxPropertiesCount { + + // If + const int maxPropertiesCount = 60; + MSACCustomProperties *customProperties = [MSACCustomProperties new]; + + // Maximum properties count. + // When + for (int i = 0; i < maxPropertiesCount; i++) { + [customProperties setNumber:@(i) forKey:[NSString stringWithFormat:@"key%d", i]]; + } + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(maxPropertiesCount)); + + // Exceeding maximum properties count. + // When + [customProperties setNumber:@(1) forKey:@"extra1"]; + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(maxPropertiesCount)); + + // When + [customProperties setNumber:@(1) forKey:@"extra2"]; + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(maxPropertiesCount)); +} + +- (void)testSetString { + + // If + NSString *key = @"test"; + + // When + MSACCustomProperties *customProperties = [MSACCustomProperties new]; + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(0)); + + // Null value. + // When + NSString *nullValue = nil; + [customProperties setString:nullValue forKey:key]; + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(0)); + + // Too long value. + // When + NSString *tooLongValue = [@"" stringByPaddingToLength:129 withString:@"a" startingAtIndex:0]; + [customProperties setString:tooLongValue forKey:key]; + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(0)); + + // Empty value. + // When + NSString *emptyValue = @""; + [customProperties setString:emptyValue forKey:key]; + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(1)); + assertThat([customProperties propertiesImmutableCopy][key], is(emptyValue)); + + // Normal value. + // When + NSString *normalValue = @"test"; + [customProperties setString:normalValue forKey:key]; + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(1)); + assertThat([customProperties propertiesImmutableCopy][key], is(normalValue)); + + // Normal value with maximum length. + // When + NSString *maxLongValue = [@"" stringByPaddingToLength:128 withString:@"a" startingAtIndex:0]; + [customProperties setString:maxLongValue forKey:key]; + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(1)); + assertThat([customProperties propertiesImmutableCopy][key], is(maxLongValue)); +} + +- (void)testSetDate { + + // If + NSString *key = @"test"; + + // When + MSACCustomProperties *customProperties = [MSACCustomProperties new]; + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(0)); + + // Null value. + // When + NSDate *nullValue = nil; + [customProperties setDate:nullValue forKey:key]; + assertThat([customProperties propertiesImmutableCopy], hasCountOf(0)); + + // Normal value. + // When + NSDate *normalValue = [NSDate dateWithTimeIntervalSince1970:0]; + [customProperties setDate:normalValue forKey:key]; + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(1)); + assertThat([customProperties propertiesImmutableCopy][key], is(normalValue)); +} + +- (void)testSetNumber { + + // If + NSString *key = @"test"; + + // When + MSACCustomProperties *customProperties = [MSACCustomProperties new]; + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(0)); + + // Null value. + // When + NSNumber *nullValue = nil; + [customProperties setNumber:nullValue forKey:key]; + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(0)); + + // Normal value. + // When + NSNumber *normalValue = @0; + [customProperties setNumber:normalValue forKey:key]; + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(1)); + assertThat([customProperties propertiesImmutableCopy][key], is(normalValue)); +} + +- (void)testSetInvalidNumber { + + // If + NSString *key = @"test"; + + // When + MSACCustomProperties *customProperties = [MSACCustomProperties new]; + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(0)); + + // NaN value. + // When + NSNumber *nanValue = [NSNumber numberWithDouble:NAN]; + [customProperties setNumber:nanValue forKey:key]; + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(0)); + + // Positive infinite value. + // When + NSNumber *positiveInfiniteValue = [NSNumber numberWithDouble:INFINITY]; + [customProperties setNumber:positiveInfiniteValue forKey:key]; + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(0)); + + // Negative infinite value. + // When + NSNumber *negativeInfiniteValue = [NSNumber numberWithDouble:-INFINITY]; + [customProperties setNumber:negativeInfiniteValue forKey:key]; + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(0)); +} + +- (void)testSetBool { + + // If + NSString *key = @"test"; + + // When + MSACCustomProperties *customProperties = [MSACCustomProperties new]; + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(0)); + + // Normal value. + // When + BOOL normalValue = NO; + [customProperties setBool:normalValue forKey:key]; + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(1)); + assertThat([customProperties propertiesImmutableCopy][key], is(@(normalValue))); +} + +- (void)testClear { + + // If + NSString *key = @"test"; + + // When + MSACCustomProperties *customProperties = [MSACCustomProperties new]; + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(0)); + + // When + [customProperties clearPropertyForKey:key]; + + // Then + assertThat([customProperties propertiesImmutableCopy], hasCountOf(1)); + assertThat([customProperties propertiesImmutableCopy][key], is([NSNull null])); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACDBStorageTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACDBStorageTests.m new file mode 100644 index 0000000000..12e8dc8054 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACDBStorageTests.m @@ -0,0 +1,779 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACDBStoragePrivate.h" +#import "MSACStorageBindableArray.h" +#import "MSACStorageTestUtil.h" +#import "MSACTestFrameworks.h" +#import "MSACUtility+Date.h" +#import "MSACUtility+File.h" + +#define DOCUMENTS [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] + +static NSString *const kMSACTestTableName = @"table"; +static NSString *const kMSACTestPositionColName = @"position"; +static NSString *const kMSACTestPersonColName = @"person"; +static NSString *const kMSACTestHungrinessColName = @"hungriness"; +static NSString *const kMSACTestMealColName = @"meal"; +static NSString *const kMSACTestDBFileName = @"Test.sqlite"; + +// 40 KiB (10 pages by 4 KiB). +static const long kMSACTestStorageSizeMinimumUpperLimitInBytes = 40 * 1024; + +@interface MSACDBStorageTests : XCTestCase + +@property(nonatomic) MSACDBStorage *sut; +@property(nonatomic) MSACDBSchema *schema; +@property(nonatomic) MSACStorageTestUtil *storageTestUtil; + +@end + +@implementation MSACDBStorageTests + +- (void)setUp { + [super setUp]; + self.schema = @{ + kMSACTestTableName : @[ + @{kMSACTestPositionColName : @[ kMSACSQLiteTypeInteger, kMSACSQLiteConstraintPrimaryKey, kMSACSQLiteConstraintAutoincrement ]}, + @{kMSACTestPersonColName : @[ kMSACSQLiteTypeText, kMSACSQLiteConstraintNotNull ]}, + @{kMSACTestHungrinessColName : @[ kMSACSQLiteTypeInteger ]}, + @{kMSACTestMealColName : @[ kMSACSQLiteTypeText, kMSACSQLiteConstraintNotNull ]} + ] + }; + self.storageTestUtil = [[MSACStorageTestUtil alloc] initWithDbFileName:kMSACTestDBFileName]; + [self.storageTestUtil deleteDatabase]; + self.sut = [[MSACDBStorage alloc] initWithSchema:self.schema version:0 filename:kMSACTestDBFileName]; +} + +- (void)tearDown { + [self.storageTestUtil deleteDatabase]; + [super tearDown]; +} + +- (void)testInitWithSchema { + + // If + NSString *testTableName = @"test_table", *testColumnName = @"test_column", *testColumn2Name = @"test_column2"; + NSString *expectedResult = + [NSString stringWithFormat:@"CREATE TABLE \"%@\" (\"%@\" %@ %@ %@, \"%@\" %@ %@)", testTableName, testColumnName, + kMSACSQLiteTypeInteger, kMSACSQLiteConstraintPrimaryKey, kMSACSQLiteConstraintAutoincrement, + testColumn2Name, kMSACSQLiteTypeText, kMSACSQLiteConstraintNotNull]; + MSACDBSchema *testSchema = @{ + testTableName : @[ + @{testColumnName : @[ kMSACSQLiteTypeInteger, kMSACSQLiteConstraintPrimaryKey, kMSACSQLiteConstraintAutoincrement ]}, + @{testColumn2Name : @[ kMSACSQLiteTypeText, kMSACSQLiteConstraintNotNull ]} + ] + }; + id result; + + // When + self.sut = [[MSACDBStorage alloc] initWithSchema:testSchema version:0 filename:kMSACTestDBFileName]; + result = [self queryTable:testTableName]; + + // Then + assertThat(result, is(expectedResult)); + + // If + [self.storageTestUtil deleteDatabase]; + NSString *testTableName2 = @"test2_table", *testColumnName2 = @"test2_column"; + testSchema = @{ + testTableName : @[ + @{testColumnName : @[ kMSACSQLiteTypeInteger, kMSACSQLiteConstraintPrimaryKey, kMSACSQLiteConstraintAutoincrement ]}, + @{testColumn2Name : @[ kMSACSQLiteTypeText, kMSACSQLiteConstraintNotNull ]} + ], + testTableName2 : @[ @{testColumnName2 : @[ kMSACSQLiteTypeInteger, kMSACSQLiteConstraintNotNull ]} ] + }; + NSString *expectedResult2 = [NSString stringWithFormat:@"CREATE TABLE \"%@\" (\"%@\" %@ %@)", testTableName2, testColumnName2, + kMSACSQLiteTypeInteger, kMSACSQLiteConstraintNotNull]; + id result2; + + // When + self.sut = [[MSACDBStorage alloc] initWithSchema:testSchema version:0 filename:kMSACTestDBFileName]; + result = [self queryTable:testTableName]; + result2 = [self queryTable:testTableName2]; + + // Then + assertThat(result, is(expectedResult)); + assertThat(result2, is(expectedResult2)); +} + +- (void)testSqliteConfigurationErrorAfterInit { + + // when + int configResult = [MSACDBStorage configureSQLite]; + + // Then + assertThatInt(configResult, equalToInt(SQLITE_MISUSE)); +} + +- (void)testTableExists { + [self.sut executeQueryUsingBlock:^int(void *db) { + // When + BOOL tableExists = [MSACDBStorage tableExists:kMSACTestTableName inOpenedDatabase:db]; + + // Then + assertThatBool(tableExists, isTrue()); + + // If + NSString *query = [NSString stringWithFormat:@"DROP TABLE \"%@\"", kMSACTestTableName]; + [MSACDBStorage executeNonSelectionQuery:query inOpenedDatabase:db withValues:nil]; + + // When + tableExists = [MSACDBStorage tableExists:kMSACTestTableName inOpenedDatabase:db]; + + // Then + assertThatBool(tableExists, isFalse()); + + return 0; + }]; +} + +- (void)testVersion { + [self.sut executeQueryUsingBlock:^int(void *db) { + int result = 0; + + // When + NSUInteger version = [MSACDBStorage versionInOpenedDatabase:db result:&result]; + + // Then + assertThatUnsignedInteger(version, equalToUnsignedInt(0)); + + // When + [MSACDBStorage setVersion:1 inOpenedDatabase:db]; + version = [MSACDBStorage versionInOpenedDatabase:db result:&result]; + + // Then + assertThatUnsignedInteger(version, equalToUnsignedInt(1)); + + return 0; + }]; + + // After re-open. + [self.sut executeQueryUsingBlock:^int(void *db) { + // When + int result = 0; + NSUInteger version = [MSACDBStorage versionInOpenedDatabase:db result:&result]; + + // Then + assertThatUnsignedInteger(version, equalToUnsignedInt(1)); + + return 0; + }]; +} + +- (void)testMigration { + + // If + id dbStorage = OCMPartialMock(self.sut); + OCMExpect([dbStorage migrateDatabase:[OCMArg anyPointer] fromVersion:0]); + + // When + (void)[dbStorage initWithSchema:self.schema version:1 filename:kMSACTestDBFileName]; + + // Then + OCMVerifyAll(dbStorage); + + // If + // Migrate shouldn't be called in a new database. + [self.storageTestUtil deleteDatabase]; + OCMReject([[dbStorage ignoringNonObjectArgs] migrateDatabase:[OCMArg anyPointer] fromVersion:0]); + + // When + (void)[dbStorage initWithSchema:self.schema version:2 filename:kMSACTestDBFileName]; + + // Then + OCMVerifyAll(dbStorage); +} + +- (void)testGetMaxPageCountInOpenedDatabaseReturnsZeroWhenQueryFails { + + // If + // Query returns empty array. + id dbStorageMock = OCMClassMock([MSACDBStorage class]); + sqlite3 *db = [self.storageTestUtil openDatabase]; + NSMutableArray *entries = [NSMutableArray new]; + OCMStub([dbStorageMock executeSelectionQuery:[OCMArg any] inOpenedDatabase:db withValues:OCMOCK_ANY]).andReturn(entries); + + // When + long counter = [MSACDBStorage getMaxPageCountInOpenedDatabase:db]; + + // Then + assertThatLong(counter, equalToLong(0)); + + // If + // Query returns an array with empty array. + [entries addObject:[NSMutableArray new]]; + OCMStub([dbStorageMock executeSelectionQuery:[OCMArg any] inOpenedDatabase:db withValues:OCMOCK_ANY]).andReturn(entries); + + // When + counter = [MSACDBStorage getMaxPageCountInOpenedDatabase:db]; + + // Then + assertThatLong(counter, equalToLong(0)); +} + +- (void)testGetPageCountInOpenedDatabaseReturnsZeroWhenQueryFails { + + // If + // Query returns empty array. + id dbStorageMock = OCMClassMock([MSACDBStorage class]); + sqlite3 *db = [self.storageTestUtil openDatabase]; + NSMutableArray *entries = [NSMutableArray new]; + OCMStub([dbStorageMock executeSelectionQuery:[OCMArg any] inOpenedDatabase:db withValues:OCMOCK_ANY]).andReturn(entries); + + // When + long counter = [MSACDBStorage getPageCountInOpenedDatabase:db]; + + // Then + assertThatLong(counter, equalToLong(0)); + + // If + // Query returns an array with empty array. + [entries addObject:[NSMutableArray new]]; + OCMStub([dbStorageMock executeSelectionQuery:[OCMArg any] inOpenedDatabase:db withValues:OCMOCK_ANY]).andReturn(entries); + + // When + counter = [MSACDBStorage getPageCountInOpenedDatabase:db]; + + // Then + assertThatLong(counter, equalToLong(0)); +} + +- (void)testGetPageSizeInOpenedDatabaseReturnsZeroWhenQueryFails { + + // If + // Query returns empty array. + id dbStorageMock = OCMClassMock([MSACDBStorage class]); + sqlite3 *db = [self.storageTestUtil openDatabase]; + NSMutableArray *entries = [NSMutableArray new]; + OCMStub([dbStorageMock executeSelectionQuery:[OCMArg any] inOpenedDatabase:db withValues:OCMOCK_ANY]).andReturn(entries); + + // When + long counter = [MSACDBStorage getPageSizeInOpenedDatabase:db]; + + // Then + assertThatLong(counter, equalToLong(0)); + + // If + // Query returns an array with empty array. + [entries addObject:[NSMutableArray new]]; + OCMStub([dbStorageMock executeSelectionQuery:[OCMArg any] inOpenedDatabase:db withValues:OCMOCK_ANY]).andReturn(entries); + + // When + counter = [MSACDBStorage getPageSizeInOpenedDatabase:db]; + + // Then + assertThatLong(counter, equalToLong(0)); +} + +- (void)testEnableAutoVacuumInOpenedDatabaseWhenQueryFails { + + // If + // Query returns empty array. + id dbStorageMock = OCMClassMock([MSACDBStorage class]); + sqlite3 *db = [self.storageTestUtil openDatabase]; + NSMutableArray *entries = [NSMutableArray new]; + OCMStub([dbStorageMock executeSelectionQuery:[OCMArg any] inOpenedDatabase:db withValues:OCMOCK_ANY]).andReturn(entries); + OCMStub([dbStorageMock executeNonSelectionQuery:[OCMArg any] inOpenedDatabase:db withValues:OCMOCK_ANY]); + + // When + [MSACDBStorage enableAutoVacuumInOpenedDatabase:db]; + + // Then + OCMVerify([dbStorageMock executeSelectionQuery:[OCMArg any] inOpenedDatabase:db withValues:OCMOCK_ANY]); + + // If + // Query returns an array with empty array. + [entries addObject:[NSMutableArray new]]; + OCMStub([dbStorageMock executeSelectionQuery:[OCMArg any] inOpenedDatabase:db withValues:OCMOCK_ANY]).andReturn(entries); + + // When + [MSACDBStorage enableAutoVacuumInOpenedDatabase:db]; + + // Then + OCMVerify([dbStorageMock executeSelectionQuery:[OCMArg any] inOpenedDatabase:db withValues:OCMOCK_ANY]); +} + +- (void)testCreateTableWhenTableExists { + + // Then + XCTAssertTrue([self tableExists:kMSACTestTableName]); + + // When + BOOL tableExistsOrCreated = [self.sut createTable:kMSACTestTableName columnsSchema:self.schema[kMSACTestTableName]]; + + // Then + XCTAssertTrue(tableExistsOrCreated); + XCTAssertTrue([self tableExists:kMSACTestTableName]); +} + +- (void)testCreateTableWhenTableDoesntExists { + + // If + NSString *tableToCreate = @"NewTable"; + + // When + BOOL tableExistsOrCreated = [self.sut createTable:tableToCreate columnsSchema:self.schema[kMSACTestTableName]]; + + // Then + XCTAssertTrue(tableExistsOrCreated); + XCTAssertTrue([self tableExists:tableToCreate]); +} + +- (void)testDropTableWhenTableExists { + + // When + BOOL tableDropped = [self.sut dropTable:kMSACTestTableName]; + + // Then + XCTAssertTrue(tableDropped); + XCTAssertFalse([self tableExists:kMSACTestTableName]); +} + +- (void)testDropDatabase { + + // If + NSString *tableName1 = @"shortLivedTabled1"; + NSString *tableName2 = @"shortLivedTabled2"; + + // When + XCTAssertTrue([self.sut createTable:tableName1 columnsSchema:self.schema[kMSACTestTableName]]); + XCTAssertTrue([self tableExists:tableName1]); + XCTAssertTrue([self.sut createTable:tableName2 columnsSchema:self.schema[kMSACTestTableName]]); + XCTAssertTrue([self tableExists:tableName2]); + [self.sut dropDatabase]; + + // Then + XCTAssertFalse([self.sut.dbFileURL checkResourceIsReachableAndReturnError:nil]); + XCTAssertFalse([self tableExists:tableName1]); + XCTAssertFalse([self tableExists:tableName2]); +} + +- (void)testDroppedTableWhenTableDoesNotExists { + + // If + NSString *tableToDrop = @"NewTable"; + + // When + BOOL tableDropped = [self.sut dropTable:tableToDrop]; + + // Then + XCTAssertTrue(tableDropped); + XCTAssertFalse([self tableExists:tableToDrop]); +} + +- (void)testExecuteQuery { + + // If + NSString *expectedPerson = @"Hungry Guy"; + NSNumber *expectedHungriness = @(99); + NSString *expectedMeal = @"Big burger"; + NSString *query = + [NSString stringWithFormat:@"INSERT INTO \"%@\" (\"%@\", \"%@\", \"%@\") " + @"VALUES (?, ?, ?)", + kMSACTestTableName, kMSACTestPersonColName, kMSACTestHungrinessColName, kMSACTestMealColName]; + MSACStorageBindableArray *array = [MSACStorageBindableArray new]; + [array addString:expectedPerson]; + [array addString:expectedHungriness.stringValue]; + [array addString:expectedMeal]; + int result; + NSArray *entry; + + // When + result = [self.sut executeNonSelectionQuery:query withValues:array]; + + // Then + assertThatInteger(result, equalToInt(SQLITE_OK)); + + // If + query = [NSString stringWithFormat:@"SELECT * FROM \"%@\"", kMSACTestTableName]; + + // When + entry = [self.sut executeSelectionQuery:query withValues:nil]; + + // Then + assertThat(entry, is(@[ @[ @(1), expectedPerson, expectedHungriness, expectedMeal ] ])); + + // If + expectedMeal = @"Gigantic burger"; + query = [NSString stringWithFormat:@"UPDATE \"%@\" SET \"%@\" = ? WHERE \"%@\" = ?", kMSACTestTableName, kMSACTestMealColName, + kMSACTestPositionColName]; + + // When + array = [MSACStorageBindableArray new]; + [array addString:expectedMeal]; + [array addNumber:@(1)]; + result = [self.sut executeNonSelectionQuery:query withValues:array]; + + // Then + assertThatInteger(result, equalToInt(SQLITE_OK)); + + // If + query = [NSString stringWithFormat:@"SELECT * FROM \"%@\"", kMSACTestTableName]; + + // When + entry = [self.sut executeSelectionQuery:query withValues:nil]; + + // Then + assertThat(entry, is(@[ @[ @(1), expectedPerson, expectedHungriness, expectedMeal ] ])); + + // If + query = [NSString stringWithFormat:@"DELETE FROM \"%@\" WHERE \"%@\" = ?;", kMSACTestTableName, kMSACTestPositionColName]; + + // When + array = [MSACStorageBindableArray new]; + [array addNumber:@(1)]; + result = [self.sut executeNonSelectionQuery:query withValues:array]; + + // Then + assertThatInteger(result, equalToInt(SQLITE_OK)); + + // If + query = [NSString stringWithFormat:@"SELECT * FROM \"%@\"", kMSACTestTableName]; + + // When + entry = [self.sut executeSelectionQuery:query withValues:nil]; + + // Then + assertThat(entry, is(@[])); +} + +- (void)testRetrieveMultipleEntries { + + // If + id expectedGuys = [self addGuysToTheTableWithCount:20]; + + // When + id result = [self.sut executeSelectionQuery:[NSString stringWithFormat:@"SELECT * FROM \"%@\"", kMSACTestTableName] withValues:nil]; + + // Then + assertThat(result, is(expectedGuys)); +} + +- (void)testCount { + + // If + NSUInteger count; + + // When + count = [self.sut countEntriesForTable:kMSACTestTableName condition:nil withValues:nil]; + + // Then + assertThatUnsignedInteger(count, equalToInt(0)); + + // If + NSString *expectedPerson = @"Hungry Guy"; + NSNumber *expectedHungriness = @(99); + NSString *expectedMeal = @"Big burger"; + NSString *query = + [NSString stringWithFormat:@"INSERT INTO \"%@\" (\"%@\", \"%@\", \"%@\") " + @"VALUES (?, ?, ?)", + kMSACTestTableName, kMSACTestPersonColName, kMSACTestHungrinessColName, kMSACTestMealColName]; + MSACStorageBindableArray *array = [MSACStorageBindableArray new]; + [array addString:expectedPerson]; + [array addString:expectedHungriness.stringValue]; + [array addString:expectedMeal]; + [self.sut executeNonSelectionQuery:query withValues:array]; + + // When + count = [self.sut countEntriesForTable:kMSACTestTableName condition:nil withValues:nil]; + + // Then + assertThatUnsignedInteger(count, equalToInt(1)); + + // If + expectedPerson = @"Hungry Man"; + expectedMeal = @"Huge raclette"; + query = [NSString stringWithFormat:@"INSERT INTO \"%@\" (\"%@\", \"%@\", \"%@\") " + @"VALUES (?, ?, ?)", + kMSACTestTableName, kMSACTestPersonColName, kMSACTestHungrinessColName, kMSACTestMealColName]; + array = [MSACStorageBindableArray new]; + [array addString:expectedPerson]; + [array addString:expectedHungriness.stringValue]; + [array addString:expectedMeal]; + [self.sut executeNonSelectionQuery:query withValues:array]; + + // When + count = [self.sut countEntriesForTable:kMSACTestTableName condition:nil withValues:nil]; + + // Then + assertThatUnsignedInteger(count, equalToInt(2)); + + // When + array = [MSACStorageBindableArray new]; + [array addString:expectedMeal]; + count = [self.sut countEntriesForTable:kMSACTestTableName + condition:[NSString stringWithFormat:@"\"%@\" = ?", kMSACTestMealColName] + withValues:array]; + + // Then + assertThatUnsignedInteger(count, equalToInt(1)); +} + +#pragma mark - Set storage size + +- (void)testSetStorageSizeFailsWhenShrinkingDatabaseIsAttempted { + + // If + XCTestExpectation *expectation = [self expectationWithDescription:@"Completion handler invoked."]; + + // Fill the database with data to reach the desired initial size. + while ([self.storageTestUtil getDataLengthInBytes] < kMSACTestStorageSizeMinimumUpperLimitInBytes) { + [self addGuysToTheTableWithCount:1000]; + } + long bytesOfData = [self.storageTestUtil getDataLengthInBytes]; + long shrunkenSizeInBytes = bytesOfData - 12 * 1024; + + // When + __weak typeof(self) weakSelf = self; + [weakSelf.sut setMaxStorageSize:shrunkenSizeInBytes + completionHandler:^(BOOL success) { + // Then + XCTAssertFalse(success); + [expectation fulfill]; + }]; + + // Then + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testSetStorageSizePassesWhenSizeIsGreaterThanCurrentBytesOfActualData { + + // If + XCTestExpectation *expectation = [self expectationWithDescription:@"Completion handler invoked."]; + __block BOOL actualSuccess = NO; + + // Fill the database with data to reach the desired initial size. + while ([self.storageTestUtil getDataLengthInBytes] < kMSACTestStorageSizeMinimumUpperLimitInBytes) { + [self addGuysToTheTableWithCount:1000]; + } + long bytesOfData = [self.storageTestUtil getDataLengthInBytes]; + NSLog(@"bytes of data: %ld", bytesOfData); + long expandedSizeInBytes = bytesOfData + 12 * 1024; + + // When + [self.sut setMaxStorageSize:expandedSizeInBytes + completionHandler:^(BOOL success) { + actualSuccess = success; + [expectation fulfill]; + }]; + + // Open DB to trigger completion handler. + [self.sut executeQueryUsingBlock:^(__unused void *db) { + return SQLITE_OK; + }]; + + // Then + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *_Nullable error) { + // Then + XCTAssertTrue(actualSuccess); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testMaximumPageCountDoesNotChangeWhenShrinkingDatabaseIsAttempted { + + // If + const long initialMaxSize = self.sut.maxSizeInBytes; + XCTestExpectation *expectation = [self expectationWithDescription:@"Completion handler invoked."]; + + // Fill the database with data to reach the desired initial size. + while ([self.storageTestUtil getDataLengthInBytes] < kMSACTestStorageSizeMinimumUpperLimitInBytes) { + [self addGuysToTheTableWithCount:1000]; + } + long bytesOfData = [self.storageTestUtil getDataLengthInBytes]; + long shrunkenSizeInBytes = bytesOfData - 12 * 1024; + + // When + __weak typeof(self) weakSelf = self; + [weakSelf.sut setMaxStorageSize:shrunkenSizeInBytes + completionHandler:^(__unused BOOL success) { + // Then + typeof(self) strongSelf = weakSelf; + XCTAssertEqual(initialMaxSize, strongSelf.sut.maxSizeInBytes); + [expectation fulfill]; + }]; + + // Then + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testCompletionHandlerCanBeNil { + + // When + [self.sut setMaxStorageSize:4 * 1024 completionHandler:nil]; + [self addGuysToTheTableWithCount:100]; + + // Then + // Didn't crash. +} + +- (void)testNewDatabaseIsAutoVacuumed { + + // Then + XCTAssertTrue([self autoVacuumIsSetToFull]); +} + +- (void)testNonAutoVacuumingDatabaseIsManuallyVacuumedAndAutoVacuumedWhenInitialized { + + // If + + // Reset database and ensure that auto_vacuum is disabled. + [self.storageTestUtil deleteDatabase]; + sqlite3 *db = [self.storageTestUtil openDatabase]; + sqlite3_exec(db, "PRAGMA auto_vacuum = NONE; VACUUM", NULL, NULL, NULL); + sqlite3_close(db); + + // When + self.sut = [[MSACDBStorage alloc] initWithSchema:self.schema version:0 filename:kMSACTestDBFileName]; + + // Then + XCTAssertTrue([self autoVacuumIsSetToFull]); +} + +- (void)testDatabaseThatIsAutoVacuumedNotManuallyVacuumedWhenInitialized { + + // If + + // Reset database and ensure that auto_vacuum is enabled. + [self.storageTestUtil deleteDatabase]; + sqlite3 *db = [self.storageTestUtil openDatabase]; + sqlite3_exec(db, "PRAGMA auto_vacuum = FULL; VACUUM", NULL, NULL, NULL); + sqlite3_close(db); + id dbStorageMock = OCMClassMock([MSACDBStorage class]); + + // Then + OCMReject([dbStorageMock executeNonSelectionQuery:@"VACUUM" inOpenedDatabase:[OCMArg anyPointer] withValues:OCMOCK_ANY]); + + // When + self.sut = [[MSACDBStorage alloc] initWithSchema:self.schema version:0 filename:kMSACTestDBFileName]; +} + +- (void)testDatabaseThatFileWasCorrupted { + + // If + const char *validFileHeader = "SQLite format 3"; + NSURL *fileURL = [MSACUtility fullURLForPathComponent:kMSACTestDBFileName]; + NSData *data = [NSData dataWithContentsOfURL:fileURL]; + + // Check that database file is valid. + XCTAssertEqual(strcmp((const char *)data.bytes, validFileHeader), 0); + + // Corrupt the file. + NSMutableData *mutabledata = [[NSData dataWithContentsOfURL:fileURL] mutableCopy]; + *((unsigned int *)mutabledata.mutableBytes) = 0xDEADBEEF; + [mutabledata writeToURL:fileURL atomically:YES]; + + // When + self.sut = [[MSACDBStorage alloc] initWithSchema:self.schema version:1 filename:kMSACTestDBFileName]; + + // Then + // The database should be recreated. + data = [NSData dataWithContentsOfURL:fileURL]; + XCTAssertEqual(strcmp((const char *)data.bytes, validFileHeader), 0); +} + +- (void)testInitWithSchemaWithErrorResult { + + // If + id mockMSACDBStorage = OCMClassMock([MSACDBStorage class]); + OCMStub([mockMSACDBStorage versionInOpenedDatabase:[OCMArg anyPointer] result:[OCMArg anyPointer]]).andDo(^(NSInvocation *invocation) { + int *result; + [invocation getArgument:&result atIndex:3]; + *result = SQLITE_ERROR; + }); + OCMReject([mockMSACDBStorage createTablesWithSchema:OCMOCK_ANY inOpenedDatabase:[OCMArg anyPointer]]); + + // When + self.sut = [[MSACDBStorage alloc] initWithSchema:self.schema version:1 filename:kMSACTestDBFileName]; + + // Then + OCMVerifyAll(mockMSACDBStorage); + + // Clear + [mockMSACDBStorage stopMocking]; +} + +- (void)testSetMaxPageCountReturnError { + + // If + id mockMSACDBStorage = OCMClassMock([MSACDBStorage class]); + OCMStub([mockMSACDBStorage executeSelectionQuery:containsSubstring(@"PRAGMA max_page_count =") + inOpenedDatabase:[OCMArg anyPointer] + result:[OCMArg setToValue:OCMOCK_VALUE((int){SQLITE_CORRUPT})] + withValues:OCMOCK_ANY]) + .andReturn(@[]); + + // When + self.sut = [[MSACDBStorage alloc] initWithSchema:self.schema version:1 filename:kMSACTestDBFileName]; + int result = [self.sut executeQueryUsingBlock:^int(void *_Nonnull __unused db) { + return SQLITE_OK; + }]; + + // Then + XCTAssertEqual(SQLITE_CORRUPT, result); + + // Clear + [mockMSACDBStorage stopMocking]; +} + +#pragma mark - Private + +- (NSArray *)addGuysToTheTableWithCount:(short)guysCount { + NSString *insertQuery; + NSMutableArray *guys = [NSMutableArray new]; + sqlite3 *db = [self.storageTestUtil openDatabase]; + for (short i = 1; i <= guysCount; i++) { + [guys addObject:@[ + @(i), [NSString stringWithFormat:@"%@%d", kMSACTestPersonColName, i], @(arc4random_uniform(100)), + [NSString stringWithFormat:@"%@%d", kMSACTestMealColName, i] + ]]; + insertQuery = [NSString stringWithFormat:@"INSERT INTO '%@' ('%@', '%@', '%@') VALUES ('%@', '%@', '%@')", kMSACTestTableName, + kMSACTestPersonColName, kMSACTestHungrinessColName, kMSACTestMealColName, [guys lastObject][1], + [[guys lastObject][2] stringValue], [guys lastObject][3]]; + sqlite3_exec(db, [insertQuery UTF8String], NULL, NULL, NULL); + } + sqlite3_close(db); + return guys; +} + +- (NSString *)queryTable:(NSString *)tableName { + return [self.sut executeSelectionQuery:[NSString stringWithFormat:@"SELECT sql FROM sqlite_master WHERE name='%@'", tableName] + withValues:nil][0][0]; +} + +- (BOOL)tableExists:(NSString *)tableName { + NSArray *result = [self.sut + executeSelectionQuery:[NSString stringWithFormat:@"SELECT COUNT(*) FROM \"sqlite_master\" WHERE \"type\"='table' AND \"name\"='%@';", + tableName] + withValues:nil]; + return [(NSNumber *)result[0][0] boolValue]; +} + +- (BOOL)autoVacuumIsSetToFull { + int autoVacuumFullState = 1; + sqlite3 *db = [self.storageTestUtil openDatabase]; + sqlite3_stmt *statement = NULL; + sqlite3_prepare_v2(db, "PRAGMA auto_vacuum", -1, &statement, NULL); + sqlite3_step(statement); + NSNumber *autoVacuum = @(sqlite3_column_int(statement, 0)); + sqlite3_finalize(statement); + sqlite3_close(db); + return [autoVacuum intValue] == autoVacuumFullState; +} +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACDateTimeTypedPropertyTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACDateTimeTypedPropertyTests.m new file mode 100644 index 0000000000..ae40922b77 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACDateTimeTypedPropertyTests.m @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACDateTimeTypedProperty.h" +#import "MSACTestFrameworks.h" +#import "MSACUtility+Date.h" + +@interface MSACDateTimeTypedPropertyTests : XCTestCase + +@end + +@implementation MSACDateTimeTypedPropertyTests + +- (void)testNSCodingSerializationAndDeserialization { + + // If + MSACDateTimeTypedProperty *sut = [MSACDateTimeTypedProperty new]; + sut.type = @"type"; + sut.name = @"name"; + sut.value = [NSDate dateWithTimeIntervalSince1970:100000]; + + // When + NSData *serializedProperty = [MSACUtility archiveKeyedData:sut]; + MSACDateTimeTypedProperty *actual = (MSACDateTimeTypedProperty *)[MSACUtility unarchiveKeyedData:serializedProperty]; + + // Then + XCTAssertNotNil(actual); + XCTAssertTrue([actual isKindOfClass:[MSACDateTimeTypedProperty class]]); + XCTAssertEqualObjects(actual.name, sut.name); + XCTAssertEqualObjects(actual.type, sut.type); + XCTAssertEqualObjects(actual.value, sut.value); +} + +- (void)testSerializeToDictionary { + + // If + MSACDateTimeTypedProperty *sut = [MSACDateTimeTypedProperty new]; + sut.name = @"propertyName"; + sut.value = [NSDate dateWithTimeIntervalSince1970:100000]; + + // When + NSDictionary *dictionary = [sut serializeToDictionary]; + + // Then + XCTAssertEqualObjects(dictionary[@"type"], sut.type); + XCTAssertEqualObjects(dictionary[@"name"], sut.name); + XCTAssertTrue([dictionary[@"value"] isKindOfClass:[NSString class]]); + XCTAssertEqualObjects(dictionary[@"value"], [MSACUtility dateToISO8601:sut.value]); +} + +- (void)testPropertyTypeIsCorrectWhenPropertyIsInitialized { + + // If + MSACDateTimeTypedProperty *sut = [MSACDateTimeTypedProperty new]; + + // Then + XCTAssertEqualObjects(sut.type, @"dateTime"); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACDeadLockTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACDeadLockTests.m new file mode 100644 index 0000000000..021fc3ee8b --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACDeadLockTests.m @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAbstractLog.h" +#import "MSACAppCenter.h" +#import "MSACAppCenterInternal.h" +#import "MSACAppCenterPrivate.h" +#import "MSACChannelDelegate.h" +#import "MSACChannelGroupDefault.h" +#import "MSACChannelUnitProtocol.h" +#import "MSACMockService.h" +#import "MSACTestFrameworks.h" + +@interface MSACDeadLockTests : XCTestCase +@end + +@interface MSACDummyService1 : MSACMockService +@end + +@interface MSACDummyService2 : MSACMockService +@end + +static MSACDummyService1 *sharedInstanceService1 = nil; +static MSACDummyService2 *sharedInstanceService2 = nil; + +@implementation MSACDummyService1 + ++ (instancetype)sharedInstance { + if (sharedInstanceService1 == nil) { + sharedInstanceService1 = [[self alloc] init]; + } + return sharedInstanceService1; +} + +- (MSACInitializationPriority)initializationPriority { + return MSACInitializationPriorityMax; +} + +- (NSString *)serviceName { + return @"service1"; +} + +- (NSString *)groupId { + return @"service1"; +} + +- (void)channel:(__unused id)channel + didPrepareLog:(__unused id)log + internalId:(__unused NSString *)internalId + flags:(__unused MSACFlags)flags { + + // Operation locking AC while in ChannelDelegate. + NSUUID *__unused deviceId = [MSACAppCenter installId]; +} + +- (void)startWithChannelGroup:(id)channelGroup + appSecret:(nullable NSString *)appSecret + transmissionTargetToken:(nullable NSString *)token + fromApplication:(BOOL)fromApplication { + [super startWithChannelGroup:channelGroup appSecret:appSecret transmissionTargetToken:token fromApplication:fromApplication]; + id mockLog = OCMPartialMock([MSACAbstractLog new]); + OCMStub([mockLog isValid]).andReturn(YES); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + // Log enqueued from background thread (i.e. crash logs). + [self.channelUnit enqueueItem:mockLog flags:MSACFlagsDefault]; + }); +} + +@end + +@implementation MSACDummyService2 + ++ (instancetype)sharedInstance { + if (sharedInstanceService2 == nil) { + sharedInstanceService2 = [[self alloc] init]; + } + return sharedInstanceService2; +} + +- (NSString *)serviceName { + return @"service2"; +} + +- (NSString *)groupId { + return @"service2"; +} + +- (void)startWithChannelGroup:(id)channelGroup + appSecret:(nullable NSString *)appSecret + transmissionTargetToken:(nullable NSString *)token + fromApplication:(BOOL)fromApplication { + [NSThread sleepForTimeInterval:.1]; + [super startWithChannelGroup:channelGroup appSecret:appSecret transmissionTargetToken:token fromApplication:fromApplication]; +} + +@end + +@implementation MSACDeadLockTests + +- (void)setUp { + [super setUp]; + [MSACAppCenter resetSharedInstance]; +} + +- (void)testDeadLockAtStartup { + + // If + XCTestExpectation *expectation = [self expectationWithDescription:@"Not blocked."]; + + // When + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + // Start the SDK with interlocking sensible services. + [MSACAppCenter start:@"AppSecret" withServices:@ [[MSACDummyService1 class], [MSACDummyService2 class]]]; + [expectation fulfill]; + }); + + // Then + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; + + // Wait background queue. + __block MSACChannelGroupDefault *channelGroup = [MSACAppCenter sharedInstance].channelGroup; + dispatch_sync(channelGroup.logsDispatchQueue, ^{ + dispatch_suspend(channelGroup.logsDispatchQueue); + }); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACDependencyConfigurationTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACDependencyConfigurationTests.m new file mode 100644 index 0000000000..bfd1b3bdf2 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACDependencyConfigurationTests.m @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAppCenter.h" +#import "MSACAppCenterPrivate.h" +#import "MSACChannelGroupDefault.h" +#import "MSACDependencyConfiguration.h" +#import "MSACHttpClient.h" +#import "MSACTestFrameworks.h" + +@interface MSACDependencyConfigurationTests : XCTestCase + +@property id channelGroupDefaultClassMock; + +@end + +@implementation MSACDependencyConfigurationTests + +- (void)setUp { + [MSACAppCenter resetSharedInstance]; + self.channelGroupDefaultClassMock = OCMClassMock([MSACChannelGroupDefault class]); + OCMStub([self.channelGroupDefaultClassMock alloc]).andReturn(self.channelGroupDefaultClassMock); + OCMStub([self.channelGroupDefaultClassMock initWithHttpClient:OCMOCK_ANY installId:OCMOCK_ANY logUrl:OCMOCK_ANY]).andReturn(nil); +} + +- (void)tearDown { + [self.channelGroupDefaultClassMock stopMocking]; + [MSACAppCenter resetSharedInstance]; + [super tearDown]; +} + +- (void)testNotSettingDependencyCallUsesDefaultHttpClient { + + // If + id httpClientClassMock = OCMClassMock([MSACHttpClient class]); + OCMStub([httpClientClassMock new]).andReturn(httpClientClassMock); + + // When + [MSACAppCenter configureWithAppSecret:@"App-Secret"]; + + // Then + // Cast to void to get rid of warning that says "Expression result unused". + OCMVerify((void)[self.channelGroupDefaultClassMock initWithHttpClient:httpClientClassMock installId:OCMOCK_ANY logUrl:OCMOCK_ANY]); + + // Cleanup + [httpClientClassMock stopMocking]; +} + +- (void)testDependencyCallUsesInjectedHttpClient { + + // If + id httpClientClassMock = OCMClassMock([MSACHttpClient class]); + + // This stub is still required due to `oneCollectorChannelDelegate` that requires `MSACHttpClient` instantiation. + // Without this stub, `[MSACHttpClientTests testDeleteRecoverableErrorWithoutHeadersRetried]` test will fail for macOS because + // channel is paused by this `MSACHttpClient` instance somehow. + OCMStub([httpClientClassMock alloc]).andReturn(httpClientClassMock); + [MSACDependencyConfiguration setHttpClient:httpClientClassMock]; + + // When + [MSACAppCenter configureWithAppSecret:@"App-Secret"]; + + // Then + // Cast to void to get rid of warning that says "Expression result unused". + OCMVerify((void)[self.channelGroupDefaultClassMock initWithHttpClient:httpClientClassMock installId:OCMOCK_ANY logUrl:OCMOCK_ANY]); + + // Cleanup + MSACDependencyConfiguration.httpClient = nil; + [httpClientClassMock stopMocking]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACDeviceHistoryInfoTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACDeviceHistoryInfoTests.m new file mode 100644 index 0000000000..d0e22992d3 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACDeviceHistoryInfoTests.m @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACDevice.h" +#import "MSACDeviceHistoryInfo.h" +#import "MSACTestFrameworks.h" + +@interface MSACDeviceHistoryInfoTests : XCTestCase + +@end + +@implementation MSACDeviceHistoryInfoTests + +- (void)testCreationWorks { + + // When + MSACDeviceHistoryInfo *expected = [MSACDeviceHistoryInfo new]; + + // Then + XCTAssertNotNil(expected); + + // When + NSDate *timestamp = [NSDate dateWithTimeIntervalSince1970:42]; + MSACDevice *aDevice = [MSACDevice new]; + expected = [[MSACDeviceHistoryInfo alloc] initWithTimestamp:timestamp andDevice:aDevice]; + + // Then + XCTAssertNotNil(expected); + XCTAssertTrue([expected.timestamp isEqual:timestamp]); + XCTAssertTrue([expected.device isEqual:aDevice]); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACDeviceLogTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACDeviceLogTests.m new file mode 100644 index 0000000000..2ddd3e089d --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACDeviceLogTests.m @@ -0,0 +1,235 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACDevice.h" +#import "MSACDeviceInternal.h" +#import "MSACTestFrameworks.h" +#import "MSACUtility.h" +#import "MSACWrapperSdkInternal.h" + +@interface MSACDeviceLogTests : XCTestCase + +@property(nonatomic) MSACDevice *sut; + +@end + +@implementation MSACDeviceLogTests + +#pragma mark - Housekeeping + +- (void)setUp { + [super setUp]; + self.sut = [MSACDevice new]; +} + +- (void)tearDown { + [super tearDown]; +} + +#pragma mark - Tests + +- (void)testSerializingDeviceToDictionaryWorks { + + // If + NSString *sdkVersion = @"3.0.1"; + NSString *model = @"iPhone 7.2"; + NSString *oemName = @"Apple"; + NSString *osName = @"iOS"; + NSString *osVersion = @"9.3.20"; + NSNumber *osApiLevel = @(320); + NSString *locale = @"US-EN"; + NSNumber *timeZoneOffset = @(9); + NSString *screenSize = @"750x1334"; + NSString *appVersion = @"3.4.5 (34)"; + NSString *carrierName = @"T-Mobile"; + NSString *carrierCountry = @"United States"; + NSString *wrapperSdkVersion = @"6.7.8"; + NSString *wrapperSdkName = @"wrapper-sdk"; + NSString *wrapperRuntimeVersion = @"9.10"; + NSString *liveUpdateReleaseLabel = @"live-update-release"; + NSString *liveUpdateDeploymentKey = @"deployment-key"; + NSString *liveUpdatePackageHash = @"b10a8db164e0754105b7a99be72e3fe5"; + + self.sut.sdkVersion = sdkVersion; + self.sut.model = model; + self.sut.oemName = oemName; + self.sut.osName = osName; + self.sut.osVersion = osVersion; + self.sut.osApiLevel = osApiLevel; + self.sut.locale = locale; + self.sut.timeZoneOffset = timeZoneOffset; + self.sut.screenSize = screenSize; + self.sut.appVersion = appVersion; + self.sut.carrierName = carrierName; + self.sut.carrierCountry = carrierCountry; + self.sut.wrapperSdkVersion = wrapperSdkVersion; + self.sut.wrapperSdkName = wrapperSdkName; + self.sut.wrapperRuntimeVersion = wrapperRuntimeVersion; + self.sut.liveUpdateReleaseLabel = liveUpdateReleaseLabel; + self.sut.liveUpdateDeploymentKey = liveUpdateDeploymentKey; + self.sut.liveUpdatePackageHash = liveUpdatePackageHash; + + // When + NSMutableDictionary *actual = [self.sut serializeToDictionary]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual[@"sdkVersion"], equalTo(sdkVersion)); + assertThat(actual[@"model"], equalTo(model)); + assertThat(actual[@"oemName"], equalTo(oemName)); + assertThat(actual[@"osName"], equalTo(osName)); + assertThat(actual[@"osVersion"], equalTo(osVersion)); + assertThat(actual[@"osApiLevel"], equalTo(osApiLevel)); + assertThat(actual[@"locale"], equalTo(locale)); + assertThat(actual[@"timeZoneOffset"], equalTo(timeZoneOffset)); + assertThat(actual[@"screenSize"], equalTo(screenSize)); + assertThat(actual[@"appVersion"], equalTo(appVersion)); + assertThat(actual[@"carrierName"], equalTo(carrierName)); + assertThat(actual[@"carrierCountry"], equalTo(carrierCountry)); + assertThat(actual[@"wrapperSdkVersion"], equalTo(wrapperSdkVersion)); + assertThat(actual[@"wrapperSdkName"], equalTo(wrapperSdkName)); + assertThat(actual[@"liveUpdateReleaseLabel"], equalTo(liveUpdateReleaseLabel)); + assertThat(actual[@"liveUpdateDeploymentKey"], equalTo(liveUpdateDeploymentKey)); + assertThat(actual[@"liveUpdatePackageHash"], equalTo(liveUpdatePackageHash)); +} + +- (void)testNSCodingSerializationAndDeserializationWorks { + + // If + NSString *sdkVersion = @"3.0.1"; + NSString *model = @"iPhone 7.2"; + NSString *oemName = @"Apple"; + NSString *osName = @"iOS"; + NSString *osVersion = @"9.3.20"; + NSNumber *osApiLevel = @(320); + NSString *locale = @"US-EN"; + NSNumber *timeZoneOffset = @(9); + NSString *screenSize = @"750x1334"; + NSString *appVersion = @"3.4.5 (34)"; + NSString *carrierName = @"T-Mobile"; + NSString *carrierCountry = @"United States"; + NSString *wrapperSdkVersion = @"6.7.8"; + NSString *wrapperSdkName = @"wrapper-sdk"; + NSString *wrapperRuntimeVersion = @"9.10"; + NSString *liveUpdateReleaseLabel = @"live-update-release"; + NSString *liveUpdateDeploymentKey = @"deployment-key"; + NSString *liveUpdatePackageHash = @"b10a8db164e0754105b7a99be72e3fe5"; + + self.sut.sdkVersion = sdkVersion; + self.sut.model = model; + self.sut.oemName = oemName; + self.sut.osName = osName; + self.sut.osVersion = osVersion; + self.sut.osApiLevel = osApiLevel; + self.sut.locale = locale; + self.sut.timeZoneOffset = timeZoneOffset; + self.sut.screenSize = screenSize; + self.sut.appVersion = appVersion; + self.sut.carrierName = carrierName; + self.sut.carrierCountry = carrierCountry; + self.sut.wrapperSdkVersion = wrapperSdkVersion; + self.sut.wrapperSdkName = wrapperSdkName; + self.sut.wrapperRuntimeVersion = wrapperRuntimeVersion; + self.sut.liveUpdateReleaseLabel = liveUpdateReleaseLabel; + self.sut.liveUpdateDeploymentKey = liveUpdateDeploymentKey; + self.sut.liveUpdatePackageHash = liveUpdatePackageHash; + + // When + NSData *serializedEvent = [MSACUtility archiveKeyedData:self.sut]; + id actual = [MSACUtility unarchiveKeyedData:serializedEvent]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual, instanceOf([MSACDevice class])); + + MSACDevice *actualDevice = actual; + assertThat(actualDevice.sdkVersion, equalTo(sdkVersion)); + assertThat(actualDevice.model, equalTo(model)); + assertThat(actualDevice.oemName, equalTo(oemName)); + assertThat(actualDevice.osName, equalTo(osName)); + assertThat(actualDevice.osVersion, equalTo(osVersion)); + assertThat(actualDevice.osApiLevel, equalTo(osApiLevel)); + assertThat(actualDevice.locale, equalTo(locale)); + assertThat(actualDevice.timeZoneOffset, equalTo(timeZoneOffset)); + assertThat(actualDevice.screenSize, equalTo(screenSize)); + assertThat(actualDevice.appVersion, equalTo(appVersion)); + assertThat(actualDevice.carrierName, equalTo(carrierName)); + assertThat(actualDevice.carrierCountry, equalTo(carrierCountry)); + assertThat(actualDevice.wrapperSdkVersion, equalTo(wrapperSdkVersion)); + assertThat(actualDevice.wrapperSdkName, equalTo(wrapperSdkName)); + assertThat(actualDevice.wrapperRuntimeVersion, equalTo(wrapperRuntimeVersion)); + assertThat(actualDevice.liveUpdateReleaseLabel, equalTo(liveUpdateReleaseLabel)); + assertThat(actualDevice.liveUpdateDeploymentKey, equalTo(liveUpdateDeploymentKey)); + assertThat(actualDevice.liveUpdatePackageHash, equalTo(liveUpdatePackageHash)); +} + +- (void)testIsEqual { + + // If + NSString *sdkVersion = @"3.0.1"; + NSString *model = @"iPhone 7.2"; + NSString *oemName = @"Apple"; + NSString *osName = @"iOS"; + NSString *osVersion = @"9.3.20"; + NSNumber *osApiLevel = @(320); + NSString *locale = @"US-EN"; + NSNumber *timeZoneOffset = @(9); + NSString *screenSize = @"750x1334"; + NSString *appVersion = @"3.4.5 (34)"; + NSString *carrierName = @"T-Mobile"; + NSString *carrierCountry = @"United States"; + NSString *wrapperSdkVersion = @"6.7.8"; + NSString *wrapperSdkName = @"wrapper-sdk"; + NSString *wrapperRuntimeVersion = @"9.10"; + NSString *liveUpdateReleaseLabel = @"live-update-release"; + NSString *liveUpdateDeploymentKey = @"deployment-key"; + NSString *liveUpdatePackageHash = @"b10a8db164e0754105b7a99be72e3fe5"; + + self.sut.sdkVersion = sdkVersion; + self.sut.model = model; + self.sut.oemName = oemName; + self.sut.osName = osName; + self.sut.osVersion = osVersion; + self.sut.osApiLevel = osApiLevel; + self.sut.locale = locale; + self.sut.timeZoneOffset = timeZoneOffset; + self.sut.screenSize = screenSize; + self.sut.appVersion = appVersion; + self.sut.carrierName = carrierName; + self.sut.carrierCountry = carrierCountry; + self.sut.wrapperSdkVersion = wrapperSdkVersion; + self.sut.wrapperSdkName = wrapperSdkName; + self.sut.wrapperRuntimeVersion = wrapperRuntimeVersion; + self.sut.liveUpdateReleaseLabel = liveUpdateReleaseLabel; + self.sut.liveUpdateDeploymentKey = liveUpdateDeploymentKey; + self.sut.liveUpdatePackageHash = liveUpdatePackageHash; + + // When + NSData *serializedEvent = [MSACUtility archiveKeyedData:self.sut]; + id actual = [MSACUtility unarchiveKeyedData:serializedEvent]; + MSACDevice *actualDevice = actual; + + // Then + XCTAssertTrue([self.sut isEqual:actualDevice]); + + // When + self.sut.carrierCountry = @"newCarrierCountry"; + + // Then + XCTAssertFalse([self.sut isEqual:actualDevice]); + + // When + self.sut.carrierCountry = carrierCountry; + self.sut.wrapperSdkName = @"new-wrapper-sdk"; + + // Then + XCTAssertFalse([self.sut isEqual:actualDevice]); +} + +- (void)testIsNotEqualToNil { + + // Then + XCTAssertFalse([[MSACWrapperSdk new] isEqual:nil]); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACDeviceTrackerTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACDeviceTrackerTests.m new file mode 100644 index 0000000000..4f5fb18ffc --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACDeviceTrackerTests.m @@ -0,0 +1,614 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACDevice.h" +#import "MSACDeviceHistoryInfo.h" +#import "MSACDeviceInternal.h" +#import "MSACDeviceTracker.h" +#import "MSACDeviceTrackerPrivate.h" +#import "MSACMockUserDefaults.h" +#import "MSACTestFrameworks.h" +#import "MSACUtility+Date.h" +#import "MSACWrapperSdkInternal.h" + +static NSString *const kMSACDeviceManufacturerTest = @"Apple"; + +@interface MSACDeviceTrackerTests : XCTestCase + +@property(nonatomic) MSACDeviceTracker *sut; + +@end + +@implementation MSACDeviceTrackerTests + +- (void)setUp { + [super setUp]; + [MSACDeviceTracker resetSharedInstance]; + + // System Under Test. + self.sut = [MSACDeviceTracker sharedInstance]; +} + +- (void)tearDown { + [MSACDeviceTracker resetSharedInstance]; + [super tearDown]; +} + +- (void)testDeviceInfo { + + assertThat(self.sut.device.sdkVersion, notNilValue()); + assertThatInteger([self.sut.device.sdkVersion length], greaterThan(@(0))); + + assertThat(self.sut.device.model, notNilValue()); + assertThatInteger([self.sut.device.model length], greaterThan(@(0))); + + assertThat(self.sut.device.oemName, is(kMSACDeviceManufacturerTest)); + + assertThat(self.sut.device.osName, notNilValue()); + assertThatInteger([self.sut.device.osName length], greaterThan(@(0))); + + assertThat(self.sut.device.osVersion, notNilValue()); + assertThatInteger([self.sut.device.osVersion length], greaterThan(@(0))); + assertThatFloat([self.sut.device.osVersion floatValue], greaterThan(@(0.0))); + + assertThat(self.sut.device.locale, notNilValue()); + assertThatInteger([self.sut.device.locale length], greaterThan(@(0))); + + assertThat(self.sut.device.timeZoneOffset, notNilValue()); + + assertThat(self.sut.device.screenSize, notNilValue()); + + // Can't access carrier name and country in test context but it's optional and in that case it has to be nil. + assertThat(self.sut.device.carrierCountry, nilValue()); + assertThat(self.sut.device.carrierName, nilValue()); + + // Can't access a valid main bundle from test context so we can't test for App namespace (bundle ID), version and build. +} + +- (void)testDeviceModel { + + // When + NSString *model = [self.sut deviceModel]; + + // Then + assertThat(model, notNilValue()); + assertThatInteger([model length], greaterThan(@(0))); +} + +- (void)testDeviceOSName { + +// If +#if TARGET_OS_OSX || TARGET_OS_MACCATALYST + NSString *expected = @"macOS"; +#else + NSString *expected = @"iMock OS"; + id deviceMock = OCMClassMock([UIDevice class]); + OCMStub([deviceMock systemName]).andReturn(expected); +#endif + +// When +#if TARGET_OS_OSX || TARGET_OS_MACCATALYST + NSString *osName = [self.sut osName]; +#else + NSString *osName = [self.sut osName:deviceMock]; + [deviceMock stopMocking]; +#endif + + // Then + assertThat(osName, is(expected)); +} + +- (void)testDeviceOSVersion { + + // If + NSString *expected = @"4.5.6"; + +#if TARGET_OS_OSX + id processInfoMock; + if (@available(macOS 10.10, *)) { + processInfoMock = OCMClassMock([NSProcessInfo class]); + OCMStub([processInfoMock processInfo]).andReturn(processInfoMock); + NSOperatingSystemVersion osSystemVersionMock; + osSystemVersionMock.majorVersion = 4; + osSystemVersionMock.minorVersion = 5; + osSystemVersionMock.patchVersion = 6; + OCMStub([processInfoMock operatingSystemVersion]).andReturn(osSystemVersionMock); + } else { + + // TODO: No way to mock C-style functions like Gestalt. Skip the test on machine running on macOS version <= 10.9. + } +#else + id deviceMock = OCMClassMock([UIDevice class]); + OCMStub([(UIDevice *)deviceMock systemVersion]).andReturn(expected); +#endif + +// When +#if TARGET_OS_OSX + // TODO: No way to mock C-style functions like Gestalt. Skip the test on machine running on macOS version <= 10.9. + NSString *osVersion = expected; + if (@available(macOS 10.10, *)) { + osVersion = [self.sut osVersion]; + } +#else + NSString *osVersion = [self.sut osVersion:deviceMock]; + [deviceMock stopMocking]; +#endif + + // Then + assertThat(osVersion, is(expected)); + +#if (TARGET_OS_OSX || TARGET_OS_MACCATALYST) && __MAC_OS_X_VERSION_MAX_ALLOWED > 1090 + [processInfoMock stopMocking]; +#endif +} + +- (void)testDeviceLocale { + + // If + NSString *expected = @"en_US"; + id localeMock = OCMClassMock([NSLocale class]); + OCMStub([localeMock preferredLanguages]).andReturn(@[ @"en-US" ]); + + // When + NSString *locale = [self.sut locale:localeMock]; + + // Then + assertThat(locale, is(expected)); + [localeMock stopMocking]; +} + +- (void)testDeviceLocaleWithScriptCode { + + // If + NSString *expected = @"zh-Hans_CN"; + id localeMock = OCMClassMock([NSLocale class]); + OCMStub([localeMock preferredLanguages]).andReturn(@[ @"zh-Hans-CN" ]); + + // When + NSString *locale = [self.sut locale:localeMock]; + + // Then + assertThat(locale, is(expected)); + [localeMock stopMocking]; +} + +- (void)testDeviceLocaleWithoutCountryCode { + + // If + NSString *expected = @"zh-Hant_CN"; + id localeMock = OCMClassMock([NSLocale class]); + OCMStub([localeMock preferredLanguages]).andReturn(@[ @"zh-Hant" ]); + OCMStub([localeMock objectForKey:NSLocaleCountryCode]).andReturn(@"CN"); + + // When + NSString *locale = [self.sut locale:localeMock]; + + // Then + assertThat(locale, is(expected)); + [localeMock stopMocking]; +} + +- (void)testDeviceTimezoneOffset { + + // If + NSNumber *expected = @(-420); + id tzMock = OCMClassMock([NSTimeZone class]); + OCMStub([tzMock secondsFromGMT]).andReturn(-25200); + + // When + NSNumber *tz = [self.sut timeZoneOffset:tzMock]; + + // Then + assertThat(tz, is(expected)); + [tzMock stopMocking]; +} + +- (void)testDeviceScreenSize { + + // When + NSString *screenSize = [self.sut screenSize]; + + // Then + assertThat(screenSize, notNilValue()); + assertThatInteger([screenSize length], greaterThan(@(0))); +} + +#if TARGET_OS_IOS +- (void)testCarrierName { + + // If + NSString *expected = @"MobileParadise"; + id carrierMock = OCMClassMock([CTCarrier class]); + OCMStub([carrierMock carrierName]).andReturn(expected); + + // When + NSString *carrierName = [self.sut carrierName:carrierMock]; + + // Then + assertThat(carrierName, is(expected)); + [carrierMock stopMocking]; +} +#endif + +#if TARGET_OS_IOS +- (void)testNoCarrierName { + + // If + id carrierMock = OCMClassMock([CTCarrier class]); + OCMStub([carrierMock carrierName]).andReturn(nil); + + // When + NSString *carrierName = [self.sut carrierName:carrierMock]; + + // Then + assertThat(carrierName, nilValue()); + [carrierMock stopMocking]; +} +#endif + +#if TARGET_OS_IOS +- (void)testNonValidCarrierName { + + // If + id carrierMock = OCMClassMock([CTCarrier class]); + OCMStub([carrierMock carrierName]).andReturn(@"Carrier"); + + // When + NSString *carrierName = [self.sut carrierName:carrierMock]; + + // Then + assertThat(carrierName, nilValue()); + [carrierMock stopMocking]; +} +#endif + +#if TARGET_OS_IOS +- (void)testCarrierCountry { + + // If + NSString *expected = @"US"; + id carrierMock = OCMClassMock([CTCarrier class]); + OCMStub([carrierMock isoCountryCode]).andReturn(expected); + + // When + NSString *carrierCountry = [self.sut carrierCountry:carrierMock]; + + // Then + assertThat(carrierCountry, is(expected)); + [carrierMock stopMocking]; +} +#endif + +#if TARGET_OS_IOS +- (void)testNoCarrierCountry { + + // If + id carrierMock = OCMClassMock([CTCarrier class]); + OCMStub([carrierMock isoCountryCode]).andReturn(nil); + + // When + NSString *carrierCountry = [self.sut carrierCountry:carrierMock]; + + // Then + assertThat(carrierCountry, nilValue()); + [carrierMock stopMocking]; +} +#endif + +#if TARGET_OS_IOS +- (void)testCarrierCountryNotOverridden { + + // If + NSString *expected = @"US"; + id carrierMock = OCMClassMock([CTCarrier class]); + OCMStub([carrierMock isoCountryCode]).andReturn(expected); + + // When + NSString *carrierCountry = [self.sut carrierCountry:carrierMock]; + + // Then + assertThat(carrierCountry, is(expected)); + + // If + [self.sut setCountryCode:@"AU"]; + MSACDevice *device = self.sut.device; + + // Then + XCTAssertEqual(device.carrierCountry, @"AU"); + + // When + carrierCountry = [self.sut carrierCountry:carrierMock]; + + // Then + assertThat(carrierCountry, is(expected)); + [carrierMock stopMocking]; +} +#endif + +- (void)testAppVersion { + + // If + NSString *expected = @"7.8.9"; + NSDictionary *plist = @{@"CFBundleShortVersionString" : expected}; + id bundleMock = OCMClassMock([NSBundle class]); + OCMStub([bundleMock infoDictionary]).andReturn(plist); + + // When + NSString *appVersion = [self.sut appVersion:bundleMock]; + + // Then + assertThat(appVersion, is(expected)); + [bundleMock stopMocking]; +} + +- (void)testAppBuild { + + // If + NSString *expected = @"42"; + NSDictionary *plist = @{@"CFBundleVersion" : expected}; + id bundleMock = OCMClassMock([NSBundle class]); + OCMStub([bundleMock infoDictionary]).andReturn(plist); + + // When + NSString *appBuild = [self.sut appBuild:bundleMock]; + + // Then + assertThat(appBuild, is(expected)); + [bundleMock stopMocking]; +} + +- (void)testAppNamespace { + + // If + NSString *expected = @"com.microsoft.test.app"; + id bundleMock = OCMClassMock([NSBundle class]); + OCMStub([bundleMock bundleIdentifier]).andReturn(expected); + + // When + NSString *appNamespace = [self.sut appNamespace:bundleMock]; + + // Then + assertThat(appNamespace, is(expected)); + [bundleMock stopMocking]; +} + +- (void)testWrapperSdk { + + // If + MSACWrapperSdk *wrapperSdk = [[MSACWrapperSdk alloc] initWithWrapperSdkVersion:@"10.11.12" + wrapperSdkName:@"Wrapper SDK for iOS" + wrapperRuntimeVersion:@"13.14" + liveUpdateReleaseLabel:@"Release Label" + liveUpdateDeploymentKey:@"Deployment Key" + liveUpdatePackageHash:@"Package Hash"]; + + // When + [self.sut setWrapperSdk:wrapperSdk]; + MSACDevice *device = self.sut.device; + + // Then + XCTAssertEqual(device.wrapperSdkVersion, wrapperSdk.wrapperSdkVersion); + XCTAssertEqual(device.wrapperSdkName, wrapperSdk.wrapperSdkName); + XCTAssertEqual(device.wrapperRuntimeVersion, wrapperSdk.wrapperRuntimeVersion); + XCTAssertEqual(device.liveUpdateReleaseLabel, wrapperSdk.liveUpdateReleaseLabel); + XCTAssertEqual(device.liveUpdateDeploymentKey, wrapperSdk.liveUpdateDeploymentKey); + XCTAssertEqual(device.liveUpdatePackageHash, wrapperSdk.liveUpdatePackageHash); + + // Update wrapper SDK + // If + wrapperSdk.wrapperSdkVersion = @"10.11.13"; + + // When + [self.sut setWrapperSdk:wrapperSdk]; + + // Then + XCTAssertNotEqual(device.wrapperSdkVersion, wrapperSdk.wrapperSdkVersion); + + // When + device = self.sut.device; + + // Then + XCTAssertEqual(device.wrapperSdkVersion, wrapperSdk.wrapperSdkVersion); +} + +- (void)testCountryCode { + + // When + [self.sut setCountryCode:@"AU"]; + MSACDevice *device = self.sut.device; + + // Then + XCTAssertEqual(device.carrierCountry, @"AU"); + + // When + [self.sut setCountryCode:@"GB"]; + + // Then + XCTAssertNotEqual(device.carrierCountry, @"GB"); + + // When + device = self.sut.device; + + // Then + XCTAssertEqual(device.carrierCountry, @"GB"); + + // When + [self.sut setCountryCode:nil]; + + // Then + XCTAssertEqual(device.carrierCountry, @"GB"); + + // When + device = self.sut.device; + + // Then + XCTAssertNil(device.carrierCountry); +} + +- (void)testCreationOfNewDeviceWorks { + + // When + MSACDevice *expected = [self.sut updatedDevice]; + + // Then + + assertThat(expected.sdkVersion, notNilValue()); + assertThatInteger([expected.sdkVersion length], greaterThan(@(0))); + + assertThat(expected.model, notNilValue()); + assertThatInteger([expected.model length], greaterThan(@(0))); + + assertThat(expected.oemName, is(kMSACDeviceManufacturerTest)); + + assertThat(expected.osName, notNilValue()); + assertThatInteger([expected.osName length], greaterThan(@(0))); + + assertThat(expected.osVersion, notNilValue()); + assertThatInteger([expected.osVersion length], greaterThan(@(0))); + assertThatFloat([expected.osVersion floatValue], greaterThan(@(0.0))); + + assertThat(expected.locale, notNilValue()); + assertThatInteger([expected.locale length], greaterThan(@(0))); + + assertThat(expected.timeZoneOffset, notNilValue()); + + assertThat(expected.screenSize, notNilValue()); + + // Can't access carrier name and country in test context but it's optional and in that case it has to be nil. + assertThat(expected.carrierCountry, nilValue()); + assertThat(expected.carrierName, nilValue()); + + // Can't access a valid main bundle from test context so we can't test for App namespace (bundle ID), version and build. + + XCTAssertNotEqual(expected, self.sut.device); +} + +- (void)testNSUserDefaultsDeviceHistory { + MSACMockUserDefaults *defaults = [MSACMockUserDefaults new]; + + // When + [self.sut clearDevices]; + + // Restore past devices from NSUserDefaults. + NSData *devices = [defaults objectForKey:kMSACPastDevicesKey]; + NSArray *arrayFromData = (NSArray *)[[MSACUtility unarchiveKeyedData:devices] mutableCopy]; + + NSMutableArray *deviceHistory = [NSMutableArray arrayWithArray:arrayFromData]; + + // Then + XCTAssertTrue([deviceHistory count] == 1); + + [defaults stopMocking]; +} + +- (void)testClearingDeviceHistoryWorks { + + MSACMockUserDefaults *defaults = [MSACMockUserDefaults new]; + + // When + // If the storage is empty, remember the current device. + [self.sut clearDevices]; + + // Then + XCTAssertTrue([self.sut.deviceHistory count] == 1); + XCTAssertNotNil([defaults objectForKey:kMSACPastDevicesKey]); + + [defaults stopMocking]; +} + +- (void)testEnqueuingAndRefreshWorks { + + // If + [self.sut clearDevices]; + + // When + MSACDevice *first = [self.sut device]; + [MSACDeviceTracker refreshDeviceNextTime]; + MSACDevice *second = [self.sut device]; + [MSACDeviceTracker refreshDeviceNextTime]; + MSACDevice *third = [self.sut device]; + + // Then + XCTAssertTrue([[self.sut deviceHistory] count] == 3); + XCTAssertTrue([self.sut.deviceHistory[0].device isEqual:first]); + XCTAssertTrue([self.sut.deviceHistory[1].device isEqual:second]); + XCTAssertTrue([self.sut.deviceHistory[2].device isEqual:third]); + + // When + // We haven't called setNeedsRefresh: so device won't be refreshed. + MSACDevice *fourth = [self.sut device]; + + // Then + XCTAssertTrue([[self.sut deviceHistory] count] == 3); + XCTAssertTrue([fourth isEqual:third]); + + // When + [MSACDeviceTracker refreshDeviceNextTime]; + fourth = [self.sut device]; + + // Then + XCTAssertTrue([[self.sut deviceHistory] count] == 4); + XCTAssertTrue([self.sut.deviceHistory[3].device isEqual:fourth]); + + // When + [MSACDeviceTracker refreshDeviceNextTime]; + MSACDevice *fifth = [self.sut device]; + + // Then + XCTAssertTrue([[self.sut deviceHistory] count] == 5); + XCTAssertTrue([self.sut.deviceHistory[4].device isEqual:fifth]); + + // When + [MSACDeviceTracker refreshDeviceNextTime]; + MSACDevice *sixth = [self.sut device]; + + // Then + // The new device should be added at the end and the first one removed so that second is at index 0 + XCTAssertTrue([[self.sut deviceHistory] count] == 5); + XCTAssertTrue([self.sut.deviceHistory[0].device isEqual:second]); + XCTAssertTrue([self.sut.deviceHistory[4].device isEqual:sixth]); + + // When + [MSACDeviceTracker refreshDeviceNextTime]; + MSACDevice *seventh = [self.sut device]; + + // Then + // The new device should be added at the end and the first one removed so that third is at index 0 + XCTAssertTrue([[self.sut deviceHistory] count] == 5); + XCTAssertTrue([self.sut.deviceHistory[0].device isEqual:third]); + XCTAssertTrue([self.sut.deviceHistory[4].device isEqual:seventh]); +} + +- (void)testHistoryReturnsClosestDevice { + + // If + [self.sut clearDevices]; + + // When + MSACDevice *actual = [self.sut deviceForTimestamp:[NSDate dateWithTimeIntervalSince1970:1]]; + + // Then + XCTAssertTrue([actual isEqual:self.sut.device]); + XCTAssertTrue([[self.sut deviceHistory] count] == 1); + + // If + MSACDevice *first = [self.sut device]; + [MSACDeviceTracker refreshDeviceNextTime]; + [self.sut device]; // we don't need the second device history info + [MSACDeviceTracker refreshDeviceNextTime]; + MSACDevice *third = [self.sut device]; + + // When + actual = [self.sut deviceForTimestamp:[NSDate dateWithTimeIntervalSince1970:1]]; + + // Then + XCTAssertTrue([actual isEqual:first]); + + // When + actual = [self.sut deviceForTimestamp:[NSDate date]]; + + // Then + XCTAssertTrue([actual isEqual:third]); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACDoubleTypedPropertyTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACDoubleTypedPropertyTests.m new file mode 100644 index 0000000000..7803da734a --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACDoubleTypedPropertyTests.m @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACDoubleTypedProperty.h" +#import "MSACTestFrameworks.h" +#import "MSACUtility.h" + +@interface MSACDoubleTypedPropertyTests : XCTestCase + +@end + +@implementation MSACDoubleTypedPropertyTests + +- (void)testNSCodingSerializationAndDeserialization { + + // If + MSACDoubleTypedProperty *sut = [MSACDoubleTypedProperty new]; + sut.type = @"type"; + sut.name = @"name"; + sut.value = 12.23432; + + // When + NSData *serializedProperty = [MSACUtility archiveKeyedData:sut]; + MSACDoubleTypedProperty *actual = (MSACDoubleTypedProperty *)[MSACUtility unarchiveKeyedData:serializedProperty]; + + // Then + XCTAssertNotNil(actual); + XCTAssertTrue([actual isKindOfClass:[MSACDoubleTypedProperty class]]); + XCTAssertEqualObjects(actual.name, sut.name); + XCTAssertEqualObjects(actual.type, sut.type); + XCTAssertEqual(actual.value, sut.value); +} + +- (void)testSerializeToDictionary { + + // If + MSACDoubleTypedProperty *sut = [MSACDoubleTypedProperty new]; + sut.name = @"propertyName"; + sut.value = 0.123; + + // When + NSDictionary *dictionary = [sut serializeToDictionary]; + + // Then + XCTAssertEqualObjects(dictionary[@"type"], sut.type); + XCTAssertEqualObjects(dictionary[@"name"], sut.name); + XCTAssertEqual([dictionary[@"value"] doubleValue], sut.value); +} + +- (void)testPropertyTypeIsCorrectWhenPropertyIsInitialized { + + // If + MSACDoubleTypedProperty *sut = [MSACDoubleTypedProperty new]; + + // Then + XCTAssertEqualObjects(sut.type, @"double"); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACEncrypterTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACEncrypterTests.m new file mode 100644 index 0000000000..9c5b91461b --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACEncrypterTests.m @@ -0,0 +1,425 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACConstants+Internal.h" +#import "MSACEncrypterPrivate.h" +#import "MSACMockKeychainUtil.h" +#import "MSACMockUserDefaults.h" +#import "MSACTestFrameworks.h" +#import "MSACUtility+Date.h" + +@interface MSACEncrypterTests : XCTestCase + +@property(nonatomic) id keychainUtilMock; +@property(nonatomic) id settingsMock; + +@end + +@implementation MSACEncrypterTests + +- (void)setUp { + [super setUp]; + self.keychainUtilMock = [MSACMockKeychainUtil new]; + self.settingsMock = [MSACMockUserDefaults new]; +} + +- (void)tearDown { + [self.settingsMock stopMocking]; + [self.keychainUtilMock stopMocking]; + [MSACMockKeychainUtil clear]; +} + +- (void)testEncryptWithCurrentKey { + + // If + NSString *clearText = @"clear text"; + NSString *keyTag = kMSACEncryptionKeyTagAlternate; + NSString *expectedMetadata = [NSString stringWithFormat:@"%@/AES/CBC/PKCS7/32", keyTag]; + + // Save metadata to user defaults. + NSDate *expiration = [NSDate dateWithTimeIntervalSinceNow:10000000]; + NSString *expirationIso = [MSACUtility dateToISO8601:expiration]; + NSString *keyMetadataString = [NSString stringWithFormat:@"%@/%@", keyTag, expirationIso]; + [MSAC_APP_CENTER_USER_DEFAULTS setObject:keyMetadataString forKey:kMSACEncryptionKeyMetadataKey]; + + // Save key to the Keychain. + NSString *currentKey = [self generateTestEncryptionKey]; + [MSACMockKeychainUtil storeString:currentKey forKey:keyTag]; + MSACEncrypter *encrypter = [MSACEncrypter new]; + + // When + NSString *encryptedString = [encrypter encryptString:clearText]; + + // Then + + // Extract metadata. + NSData *encryptedData = [[NSData alloc] initWithBase64EncodedString:encryptedString options:0]; + NSData *separatorAsData = [kMSACEncryptionMetadataSeparator dataUsingEncoding:NSUTF8StringEncoding]; + NSRange entireRange = NSMakeRange(0, [encryptedData length]); + size_t metadataLength = [encryptedData rangeOfData:separatorAsData options:0 range:entireRange].location; + NSData *subdata = [encryptedData subdataWithRange:NSMakeRange(0, metadataLength)]; + NSString *metadata = [[NSString alloc] initWithData:subdata encoding:NSUTF8StringEncoding]; + XCTAssertEqualObjects(metadata, expectedMetadata); + + // Extract cipher text. Add 1 for the delimiter. + size_t metadataAndIvLength = metadataLength + 1 + kCCBlockSizeAES128; + NSRange cipherTextRange = NSMakeRange(metadataAndIvLength, [encryptedData length] - metadataAndIvLength); + NSData *cipherTextSubdata = [encryptedData subdataWithRange:cipherTextRange]; + NSString *cipherText = [[NSString alloc] initWithData:cipherTextSubdata encoding:NSUTF8StringEncoding]; + XCTAssertNotEqualObjects(cipherText, clearText); + + // When + NSString *decryptedString = [encrypter decryptString:encryptedString]; + + // Then + XCTAssertEqualObjects(decryptedString, clearText); +} + +- (void)testKeyRotatedOnFirstRunWithLegacyKeySaved { + + // If + NSString *clearText = @"clear text"; + NSString *expectedMetadata = [NSString stringWithFormat:@"%@/AES/CBC/PKCS7/32", kMSACEncryptionKeyTagAlternate]; + + // Mock NSDate to "freeze" time. + NSTimeInterval timeSinceReferenceDate = NSDate.timeIntervalSinceReferenceDate; + NSDate *referenceDate = [NSDate dateWithTimeIntervalSince1970:timeSinceReferenceDate]; + id nsdateMock = OCMClassMock([NSDate class]); + OCMStub(ClassMethod([nsdateMock timeIntervalSinceReferenceDate])).andReturn(timeSinceReferenceDate); + OCMStub(ClassMethod([nsdateMock date])).andReturn(referenceDate); + NSDate *expectedExpirationDate = [[NSDate date] dateByAddingTimeInterval:kMSACEncryptionKeyLifetimeInSeconds]; + NSString *expectedExpirationDateIso = [MSACUtility dateToISO8601:expectedExpirationDate]; + + // Save key to the Keychain. + NSString *currentKey = [self generateTestEncryptionKey]; + [MSACMockKeychainUtil storeString:currentKey forKey:kMSACEncryptionKeyTagOriginal]; + MSACEncrypter *encrypter = [MSACEncrypter new]; + + // When + NSString *encryptedString = [encrypter encryptString:clearText]; + + // Then + + // Extract metadata. + NSData *encryptedData = [[NSData alloc] initWithBase64EncodedString:encryptedString options:0]; + NSData *separatorAsData = [kMSACEncryptionMetadataSeparator dataUsingEncoding:NSUTF8StringEncoding]; + NSRange entireRange = NSMakeRange(0, [encryptedData length]); + size_t metadataLength = [encryptedData rangeOfData:separatorAsData options:0 range:entireRange].location; + NSData *subdata = [encryptedData subdataWithRange:NSMakeRange(0, metadataLength)]; + NSString *metadata = [[NSString alloc] initWithData:subdata encoding:NSUTF8StringEncoding]; + XCTAssertEqualObjects(metadata, expectedMetadata); + + // Extract cipher text. Add 1 for the delimiter. + size_t metadataAndIvLength = metadataLength + 1 + kCCBlockSizeAES128; + NSRange cipherTextRange = NSMakeRange(metadataAndIvLength, [encryptedData length] - metadataAndIvLength); + NSData *cipherTextSubdata = [encryptedData subdataWithRange:cipherTextRange]; + NSString *cipherText = [[NSString alloc] initWithData:cipherTextSubdata encoding:NSUTF8StringEncoding]; + XCTAssertNotEqualObjects(cipherText, clearText); + + // Ensure a new key and expiration were added to the user defaults. + NSArray *newKeyAndExpiration = [[MSAC_APP_CENTER_USER_DEFAULTS objectForKey:kMSACEncryptionKeyMetadataKey] + componentsSeparatedByString:kMSACEncryptionMetadataInternalSeparator]; + NSString *newKey = newKeyAndExpiration[0]; + XCTAssertEqualObjects(newKey, kMSACEncryptionKeyTagAlternate); + NSString *expirationIso = newKeyAndExpiration[1]; + XCTAssertEqualObjects(expirationIso, expectedExpirationDateIso); + + // When + NSString *decryptedString = [encrypter decryptString:encryptedString]; + + // Then + XCTAssertEqualObjects(decryptedString, clearText); +} + +- (void)testEncryptRotatesKeyWhenExpiredAndTwoKeysSaved { + + // If + NSString *clearText = @"clear text"; + NSDate *pastDate = [NSDate dateWithTimeIntervalSince1970:0]; + NSString *currentExpirationIso = [MSACUtility dateToISO8601:pastDate]; + NSString *currentKeyTag = kMSACEncryptionKeyTagOriginal; + NSString *expectedNewKeyTag = kMSACEncryptionKeyTagAlternate; + NSString *currentKeyMetadataString = [NSString stringWithFormat:@"%@/%@", currentKeyTag, currentExpirationIso]; + [MSAC_APP_CENTER_USER_DEFAULTS setObject:currentKeyMetadataString forKey:kMSACEncryptionKeyMetadataKey]; + NSString *expectedMetadata = [NSString stringWithFormat:@"%@/AES/CBC/PKCS7/32", expectedNewKeyTag]; + + // Mock NSDate to "freeze" time. + NSTimeInterval timeSinceReferenceDate = NSDate.timeIntervalSinceReferenceDate; + NSDate *referenceDate = [NSDate dateWithTimeIntervalSince1970:timeSinceReferenceDate]; + id nsdateMock = OCMClassMock([NSDate class]); + OCMStub(ClassMethod([nsdateMock timeIntervalSinceReferenceDate])).andReturn(timeSinceReferenceDate); + OCMStub(ClassMethod([nsdateMock date])).andReturn(referenceDate); + NSDate *expectedExpirationDate = [[NSDate date] dateByAddingTimeInterval:kMSACEncryptionKeyLifetimeInSeconds]; + NSString *expectedExpirationDateIso = [MSACUtility dateToISO8601:expectedExpirationDate]; + + // Save both keys to the Keychain. + NSString *currentKey = [self generateTestEncryptionKey]; + [MSACMockKeychainUtil storeString:currentKey forKey:currentKeyTag]; + NSString *expectedNewKey = [self generateTestEncryptionKey]; + [MSACMockKeychainUtil storeString:expectedNewKey forKey:expectedNewKeyTag]; + MSACEncrypter *encrypter = [MSACEncrypter new]; + + // When + NSString *encryptedString = [encrypter encryptString:clearText]; + + // Then + + // Extract metadata. + NSData *encryptedData = [[NSData alloc] initWithBase64EncodedString:encryptedString options:0]; + NSData *separatorAsData = [kMSACEncryptionMetadataSeparator dataUsingEncoding:NSUTF8StringEncoding]; + NSRange entireRange = NSMakeRange(0, [encryptedData length]); + size_t metadataLength = [encryptedData rangeOfData:separatorAsData options:0 range:entireRange].location; + NSData *subdata = [encryptedData subdataWithRange:NSMakeRange(0, metadataLength)]; + NSString *metadata = [[NSString alloc] initWithData:subdata encoding:NSUTF8StringEncoding]; + XCTAssertEqualObjects(metadata, expectedMetadata); + + // Extract cipher text. Add 1 for the delimiter. + size_t metadataAndIvLength = metadataLength + 1 + kCCBlockSizeAES128; + NSRange cipherTextRange = NSMakeRange(metadataAndIvLength, [encryptedData length] - metadataAndIvLength); + NSData *cipherTextSubdata = [encryptedData subdataWithRange:cipherTextRange]; + NSString *cipherText = [[NSString alloc] initWithData:cipherTextSubdata encoding:NSUTF8StringEncoding]; + XCTAssertNotEqualObjects(cipherText, clearText); + + // Ensure a new key and expiration were added to the user defaults. + NSArray *newKeyTagAndExpiration = [[MSAC_APP_CENTER_USER_DEFAULTS objectForKey:kMSACEncryptionKeyMetadataKey] + componentsSeparatedByString:kMSACEncryptionMetadataInternalSeparator]; + NSString *newKeyTag = newKeyTagAndExpiration[0]; + XCTAssertEqualObjects(newKeyTag, expectedNewKeyTag); + NSString *expirationIso = newKeyTagAndExpiration[1]; + XCTAssertEqualObjects(expirationIso, expectedExpirationDateIso); + + // When + NSString *decryptedString = [encrypter decryptString:encryptedString]; + + // Then + XCTAssertEqualObjects(decryptedString, clearText); +} + +- (void)testEncryptRotatesAndCreatesKeyWhenOnlyKeyIsExpired { + + // If + NSString *clearText = @"clear text"; + NSDate *pastDate = [NSDate dateWithTimeIntervalSince1970:0]; + NSString *oldExpirationIso = [MSACUtility dateToISO8601:pastDate]; + NSString *oldKey = kMSACEncryptionKeyTagOriginal; + NSString *expectedNewKeyTag = kMSACEncryptionKeyTagAlternate; + NSString *keyMetadataString = [NSString stringWithFormat:@"%@/%@", oldKey, oldExpirationIso]; + [MSAC_APP_CENTER_USER_DEFAULTS setObject:keyMetadataString forKey:kMSACEncryptionKeyMetadataKey]; + NSString *expectedMetadata = [NSString stringWithFormat:@"%@/AES/CBC/PKCS7/32", expectedNewKeyTag]; + + // Mock NSDate to "freeze" time. + NSTimeInterval timeSinceReferenceDate = NSDate.timeIntervalSinceReferenceDate; + NSDate *referenceDate = [NSDate dateWithTimeIntervalSince1970:timeSinceReferenceDate]; + id nsdateMock = OCMClassMock([NSDate class]); + OCMStub(ClassMethod([nsdateMock timeIntervalSinceReferenceDate])).andReturn(timeSinceReferenceDate); + OCMStub(ClassMethod([nsdateMock date])).andReturn(referenceDate); + NSDate *expectedExpirationDate = [[NSDate date] dateByAddingTimeInterval:kMSACEncryptionKeyLifetimeInSeconds]; + NSString *expectedExpirationDateIso = [MSACUtility dateToISO8601:expectedExpirationDate]; + + // Save key to the Keychain. + NSString *currentKey = [self generateTestEncryptionKey]; + [MSACMockKeychainUtil storeString:currentKey forKey:oldKey]; + MSACEncrypter *encrypter = [MSACEncrypter new]; + + // When + NSString *encryptedString = [encrypter encryptString:clearText]; + + // Then + + // Extract metadata. + NSData *encryptedData = [[NSData alloc] initWithBase64EncodedString:encryptedString options:0]; + NSData *separatorAsData = [kMSACEncryptionMetadataSeparator dataUsingEncoding:NSUTF8StringEncoding]; + NSRange entireRange = NSMakeRange(0, [encryptedData length]); + size_t metadataLength = [encryptedData rangeOfData:separatorAsData options:0 range:entireRange].location; + NSData *subdata = [encryptedData subdataWithRange:NSMakeRange(0, metadataLength)]; + NSString *metadata = [[NSString alloc] initWithData:subdata encoding:NSUTF8StringEncoding]; + XCTAssertEqualObjects(metadata, expectedMetadata); + + // Extract cipher text. Add 1 for the delimiter. + size_t metadataAndIvLength = metadataLength + 1 + kCCBlockSizeAES128; + NSRange cipherTextRange = NSMakeRange(metadataAndIvLength, [encryptedData length] - metadataAndIvLength); + NSData *cipherTextSubdata = [encryptedData subdataWithRange:cipherTextRange]; + NSString *cipherText = [[NSString alloc] initWithData:cipherTextSubdata encoding:NSUTF8StringEncoding]; + XCTAssertNotEqualObjects(cipherText, clearText); + + // Ensure a new key and expiration were added to the user defaults. + NSArray *newKeyAndExpiration = [[MSAC_APP_CENTER_USER_DEFAULTS objectForKey:kMSACEncryptionKeyMetadataKey] + componentsSeparatedByString:kMSACEncryptionMetadataInternalSeparator]; + NSString *newKey = newKeyAndExpiration[0]; + XCTAssertEqualObjects(newKey, expectedNewKeyTag); + NSString *expirationIso = newKeyAndExpiration[1]; + XCTAssertEqualObjects(expirationIso, expectedExpirationDateIso); + + // When + NSString *decryptedString = [encrypter decryptString:encryptedString]; + + // Then + XCTAssertEqualObjects(decryptedString, clearText); +} + +- (void)testDecryptLegacyItem { + + // If + NSString *clearText = @"Test string"; + + // Save the key to disk. This must not change as it was used to encrypt the text. + NSString *currentKey = @"zlIS50zXq7fm2GqassShXrjkMBsdjlTsmIT+d1D3CTI="; + [MSACMockKeychainUtil storeString:currentKey forKey:kMSACEncryptionKeyTagOriginal]; + + // This cipher text contains no metadata, and no IV was used. + NSString *cipherText = @"S6uNmq7u0eKGaU2GQPUGMQ=="; + MSACEncrypter *encrypter = [MSACEncrypter new]; + + // When + NSString *decryptedString = [encrypter decryptString:cipherText]; + + // Then + XCTAssertEqualObjects(decryptedString, clearText); +} + +- (void)testEncryptionCreatesKeyWhenNoKeyIsSaved { + + // If + NSString *clearText = @"clear text"; + NSString *expectedMetadata = [NSString stringWithFormat:@"%@/AES/CBC/PKCS7/32", kMSACEncryptionKeyTagAlternate]; + + // Mock NSDate to "freeze" time. + NSTimeInterval timeSinceReferenceDate = NSDate.timeIntervalSinceReferenceDate; + NSDate *referenceDate = [NSDate dateWithTimeIntervalSince1970:timeSinceReferenceDate]; + id nsdateMock = OCMClassMock([NSDate class]); + OCMStub(ClassMethod([nsdateMock timeIntervalSinceReferenceDate])).andReturn(timeSinceReferenceDate); + OCMStub(ClassMethod([nsdateMock date])).andReturn(referenceDate); + NSDate *expectedExpirationDate = [[NSDate date] dateByAddingTimeInterval:kMSACEncryptionKeyLifetimeInSeconds]; + NSString *expectedExpirationDateIso = [MSACUtility dateToISO8601:expectedExpirationDate]; + MSACEncrypter *encrypter = [MSACEncrypter new]; + + // When + NSString *encryptedString = [encrypter encryptString:clearText]; + + // Then + + // Extract metadata. + NSData *encryptedData = [[NSData alloc] initWithBase64EncodedString:encryptedString options:0]; + NSData *separatorAsData = [kMSACEncryptionMetadataSeparator dataUsingEncoding:NSUTF8StringEncoding]; + NSRange entireRange = NSMakeRange(0, [encryptedData length]); + size_t metadataLength = [encryptedData rangeOfData:separatorAsData options:0 range:entireRange].location; + NSData *subdata = [encryptedData subdataWithRange:NSMakeRange(0, metadataLength)]; + NSString *metadata = [[NSString alloc] initWithData:subdata encoding:NSUTF8StringEncoding]; + XCTAssertEqualObjects(metadata, expectedMetadata); + + // Extract cipher text. Add 1 for the delimiter. + size_t metadataAndIvLength = metadataLength + 1 + kCCBlockSizeAES128; + NSRange cipherTextRange = NSMakeRange(metadataAndIvLength, [encryptedData length] - metadataAndIvLength); + NSData *cipherTextSubdata = [encryptedData subdataWithRange:cipherTextRange]; + NSString *cipherText = [[NSString alloc] initWithData:cipherTextSubdata encoding:NSUTF8StringEncoding]; + XCTAssertNotEqualObjects(cipherText, clearText); + + // Ensure a new key and expiration were added to the user defaults. + NSArray *newKeyAndExpiration = [[MSAC_APP_CENTER_USER_DEFAULTS objectForKey:kMSACEncryptionKeyMetadataKey] + componentsSeparatedByString:kMSACEncryptionMetadataInternalSeparator]; + NSString *newKey = newKeyAndExpiration[0]; + XCTAssertEqualObjects(newKey, kMSACEncryptionKeyTagAlternate); + NSString *expirationIso = newKeyAndExpiration[1]; + XCTAssertEqualObjects(expirationIso, expectedExpirationDateIso); + + // When + NSString *decryptedString = [encrypter decryptString:encryptedString]; + + // Then + XCTAssertEqualObjects(decryptedString, clearText); +} + +- (void)testDecryptWithExpiredKey { + + // If + NSString *clearText = @"clear text"; + + // Save metadata to user defaults. + NSDate *expiration = [NSDate dateWithTimeIntervalSinceNow:10000000]; + NSString *keyId = kMSACEncryptionKeyTagOriginal; + NSString *expirationIso = [MSACUtility dateToISO8601:expiration]; + NSString *keyMetadataString = [NSString stringWithFormat:@"%@/%@", keyId, expirationIso]; + [MSAC_APP_CENTER_USER_DEFAULTS setObject:keyMetadataString forKey:kMSACEncryptionKeyMetadataKey]; + + // Save key to the Keychain. + NSString *currentKey = [self generateTestEncryptionKey]; + [MSACMockKeychainUtil storeString:currentKey forKey:keyId]; + MSACEncrypter *encrypter = [MSACEncrypter new]; + + // When + NSString *encryptedString = [encrypter encryptString:clearText]; + + // Alter the expiration date of the key so that it is now expired. + NSDate *pastDate = [NSDate dateWithTimeIntervalSinceNow:-1000000]; + NSString *oldExpirationIso = [MSACUtility dateToISO8601:pastDate]; + NSString *alteredKeyMetadataString = [NSString stringWithFormat:@"%@/%@", keyId, oldExpirationIso]; + [MSAC_APP_CENTER_USER_DEFAULTS setObject:alteredKeyMetadataString forKey:kMSACEncryptionKeyMetadataKey]; + + // When + NSString *decryptedString = [encrypter decryptString:encryptedString]; + + // Then + XCTAssertEqualObjects(decryptedString, clearText); +} + +- (void)testEncryptWithCurrentKeyWithEmptyClearText { + + // If + NSString *clearText = @""; + NSString *keyTag = kMSACEncryptionKeyTagAlternate; + NSString *expectedMetadata = [NSString stringWithFormat:@"%@/AES/CBC/PKCS7/32", keyTag]; + + // Save metadata to user defaults. + NSDate *expiration = [NSDate dateWithTimeIntervalSinceNow:10000000]; + NSString *expirationIso = [MSACUtility dateToISO8601:expiration]; + NSString *keyMetadataString = [NSString stringWithFormat:@"%@/%@", keyTag, expirationIso]; + [MSAC_APP_CENTER_USER_DEFAULTS setObject:keyMetadataString forKey:kMSACEncryptionKeyMetadataKey]; + + // Save key to the Keychain. + NSString *currentKey = [self generateTestEncryptionKey]; + [MSACMockKeychainUtil storeString:currentKey forKey:keyTag]; + MSACEncrypter *encrypter = [MSACEncrypter new]; + + // When + NSString *encryptedString = [encrypter encryptString:clearText]; + + // Then + + // Extract metadata. + NSData *encryptedData = [[NSData alloc] initWithBase64EncodedString:encryptedString options:0]; + NSData *separatorAsData = [kMSACEncryptionMetadataSeparator dataUsingEncoding:NSUTF8StringEncoding]; + NSRange entireRange = NSMakeRange(0, [encryptedData length]); + size_t metadataLength = [encryptedData rangeOfData:separatorAsData options:0 range:entireRange].location; + NSData *subdata = [encryptedData subdataWithRange:NSMakeRange(0, metadataLength)]; + NSString *metadata = [[NSString alloc] initWithData:subdata encoding:NSUTF8StringEncoding]; + XCTAssertEqualObjects(metadata, expectedMetadata); + + // Extract cipher text. Add 1 for the delimiter. + size_t metadataAndIvLength = metadataLength + 1 + kCCBlockSizeAES128; + NSRange cipherTextRange = NSMakeRange(metadataAndIvLength, [encryptedData length] - metadataAndIvLength); + NSData *cipherTextSubdata = [encryptedData subdataWithRange:cipherTextRange]; + NSString *cipherText = [[NSString alloc] initWithData:cipherTextSubdata encoding:NSUTF8StringEncoding]; + XCTAssertNotEqualObjects(cipherText, clearText); + + // When + NSString *decryptedString = [encrypter decryptString:encryptedString]; + + // Then + XCTAssertEqualObjects(decryptedString, clearText); +} + +- (NSString *)generateTestEncryptionKey { + NSData *resultKey = nil; + uint8_t *keyBytes = nil; + keyBytes = malloc(kMSACEncryptionKeySize * sizeof(uint8_t)); + memset((void *)keyBytes, 0x0, kMSACEncryptionKeySize); + int result = SecRandomCopyBytes(kSecRandomDefault, kMSACEncryptionKeySize, keyBytes); + if (result != errSecSuccess) { + return nil; + } + resultKey = [[NSData alloc] initWithBytes:keyBytes length:kMSACEncryptionKeySize]; + free(keyBytes); + return [resultKey base64EncodedStringWithOptions:0]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACHttpCallTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACHttpCallTests.m new file mode 100644 index 0000000000..3678c9fec8 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACHttpCallTests.m @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "AppCenter+Internal.h" +#import "HTTPStubs.h" +#import "MSACAppCenterErrors.h" +#import "MSACCompression.h" +#import "MSACConstants+Internal.h" +#import "MSACDevice.h" +#import "MSACDeviceInternal.h" +#import "MSACHttpCall.h" +#import "MSACHttpIngestionPrivate.h" +#import "MSACHttpTestUtil.h" +#import "MSACMockLog.h" +#import "MSACTestFrameworks.h" +#import "NSURLRequest+HTTPBodyTesting.h" +@interface MSACHttpCallTests : XCTestCase +@end + +@implementation MSACHttpCallTests + +- (void)testCompressHTTPBodyWhenLarge { + + // If + + // HTTP body is big enough to be compressed. + NSString *longString = [@"" stringByPaddingToLength:kMSACHTTPMinGZipLength withString:@"h" startingAtIndex:0]; + NSData *longData = [longString dataUsingEncoding:NSUTF8StringEncoding]; + NSData *expectedData = [MSACCompression compressData:longData]; + NSDictionary *expectedHeaders = + @{kMSACHeaderContentEncodingKey : kMSACHeaderContentEncoding, kMSACHeaderContentTypeKey : kMSACAppCenterContentType}; + + // When + MSACHttpCall *call = + [[MSACHttpCall alloc] initWithUrl:[NSURL new] + method:@"POST" + headers:nil + data:longData + retryIntervals:@[] + compressionEnabled:YES + completionHandler:^(__unused NSData *responseBody, __unused NSHTTPURLResponse *response, __unused NSError *error){ + }]; + + // Then + XCTAssertEqualObjects(call.data, expectedData); + XCTAssertEqualObjects(call.headers, expectedHeaders); +} + +- (void)testDoesNotCompressHTTPBodyWhenSmall { + + // If + + // HTTP body is small and will not be compressed. + NSData *shortData = [NSData dataWithBytes:"hi" length:2]; + NSDictionary *expectedHeaders = @{kMSACHeaderContentTypeKey : kMSACAppCenterContentType}; + + // When + MSACHttpCall *call = + [[MSACHttpCall alloc] initWithUrl:[NSURL new] + method:@"POST" + headers:nil + data:shortData + retryIntervals:@[] + compressionEnabled:YES + completionHandler:^(__unused NSData *responseBody, __unused NSHTTPURLResponse *response, __unused NSError *error){ + }]; + + // Then + XCTAssertEqualObjects(call.data, shortData); + XCTAssertEqualObjects(call.headers, expectedHeaders); +} + +- (void)testDoesNotCompressHTTPBodyWhenDisabled { + + // If + + // HTTP body is big enough to be compressed. + NSString *longString = [@"" stringByPaddingToLength:kMSACHTTPMinGZipLength withString:@"h" startingAtIndex:0]; + NSData *longData = [longString dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *expectedHeaders = @{kMSACHeaderContentTypeKey : kMSACAppCenterContentType}; + + // When + MSACHttpCall *call = + [[MSACHttpCall alloc] initWithUrl:[NSURL new] + method:@"POST" + headers:nil + data:longData + retryIntervals:@[] + compressionEnabled:NO + completionHandler:^(__unused NSData *responseBody, __unused NSHTTPURLResponse *response, __unused NSError *error){ + }]; + + // Then + XCTAssertEqualObjects(call.data, longData); + XCTAssertEqualObjects(call.headers, expectedHeaders); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACHttpClientTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACHttpClientTests.m new file mode 100644 index 0000000000..09bee1cbff --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACHttpClientTests.m @@ -0,0 +1,824 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "HTTPStubs.h" +#import "NSURLRequest+HTTPBodyTesting.h" + +#import "AppCenter+Internal.h" +#import "MSACAppCenterErrors.h" +#import "MSACConstants+Internal.h" +#import "MSACDevice.h" +#import "MSACDeviceInternal.h" +#import "MSACHttpCall.h" +#import "MSACHttpClientPrivate.h" +#import "MSACHttpTestUtil.h" +#import "MSACMockLog.h" +#import "MSACTestFrameworks.h" +#import "MSACTestUtil.h" +#import "MSAC_Reachability.h" + +static NSTimeInterval const kMSACTestTimeout = 5.0; + +@interface MSACHttpClientTests : XCTestCase + +@property(nonatomic) id reachabilityMock; +@property(nonatomic) NetworkStatus currentNetworkStatus; + +@end + +@interface MSACHttpClient () + +- (instancetype)initWithMaxHttpConnectionsPerHost:(NSNumber *)maxHttpConnectionsPerHost reachability:(MSAC_Reachability *)reachability; + +@end + +@implementation MSACHttpClientTests + +- (void)setUp { + [super setUp]; + + // Mock reachability. + self.reachabilityMock = OCMClassMock([MSAC_Reachability class]); + self.currentNetworkStatus = ReachableViaWiFi; + OCMStub([self.reachabilityMock currentReachabilityStatus]).andDo(^(NSInvocation *invocation) { + NetworkStatus test = self.currentNetworkStatus; + [invocation setReturnValue:&test]; + }); +} + +- (void)tearDown { + [MSACHttpTestUtil removeAllStubs]; + [self.reachabilityMock stopMocking]; + [super tearDown]; +} + +- (void)testInitWithMaxHttpConnectionsPerHost { + + // When + MSACHttpClient *httpClient = [[MSACHttpClient alloc] initWithMaxHttpConnectionsPerHost:2]; + + // Then + XCTAssertEqual(httpClient.sessionConfiguration.HTTPMaximumConnectionsPerHost, 2); +} + +- (void)testPostSuccessWithoutHeaders { + + // If + __block NSURLRequest *actualRequest; + + // Stub HTTP response. + [HTTPStubs + stubRequestsPassingTest:^BOOL(__unused NSURLRequest *request) { + return YES; + } + withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + actualRequest = request; + NSData *responsePayload = [@"OK" dataUsingEncoding:kCFStringEncodingUTF8]; + return [HTTPStubsResponse responseWithData:responsePayload statusCode:MSACHTTPCodesNo200OK headers:nil]; + }]; + __weak XCTestExpectation *expectation = [self expectationWithDescription:@"HTTP Response 200"]; + MSACHttpClient *httpClient = [MSACHttpClient new]; + NSURL *url = [NSURL URLWithString:@"https://mock/something?a=b"]; + NSString *method = @"POST"; + NSData *payload = [@"somePayload" dataUsingEncoding:kCFStringEncodingUTF8]; + + // When + [httpClient sendAsync:url + method:method + headers:nil + data:payload + completionHandler:^(NSData *responseBody, NSHTTPURLResponse *response, NSError *error) { + // Then + XCTAssertEqual(response.statusCode, MSACHTTPCodesNo200OK); + XCTAssertEqualObjects(responseBody, [@"OK" dataUsingEncoding:kCFStringEncodingUTF8]); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; + XCTAssertEqualObjects(actualRequest.URL, url); + XCTAssertEqualObjects(actualRequest.HTTPMethod, method); + XCTAssertEqualObjects(actualRequest.OHHTTPStubs_HTTPBody, payload); +} + +- (void)testSendAsyncEnablesCompressionByDefaultAndUsesDefaultRetries { + + // If + MSACHttpClient *httpClient = OCMPartialMock([MSACHttpClient new]); + NSURL *url = [NSURL URLWithString:@"https://mock/something?a=b"]; + NSString *method = @"GET"; + NSArray *defaultRetryIntervals = @[ @10, @(5 * 60), @(20 * 60) ]; + OCMStub([httpClient sendCallAsync:OCMOCK_ANY]).andDo(nil); + + // When + [httpClient sendAsync:url + method:method + headers:nil + data:nil + completionHandler:^(NSData *_Nullable responseBody __unused, NSHTTPURLResponse *_Nullable response __unused, + NSError *_Nullable error __unused){ + }]; + + // Then + OCMVerify([httpClient sendAsync:url + method:method + headers:nil + data:nil + retryIntervals:defaultRetryIntervals + compressionEnabled:YES + completionHandler:OCMOCK_ANY]); +} + +- (void)testGetWithHeadersResultInFatalNSError { + + // If + __block NSURLRequest *actualRequest; + + // Stub HTTP response. + [HTTPStubs + stubRequestsPassingTest:^BOOL(__unused NSURLRequest *request) { + return YES; + } + withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + actualRequest = request; + NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:kCFURLErrorBadURL userInfo:nil]; + return [HTTPStubsResponse responseWithError:error]; + }]; + __weak XCTestExpectation *expectation = [self expectationWithDescription:@"Network error"]; + MSACHttpClient *httpClient = [MSACHttpClient new]; + NSURL *url = [NSURL URLWithString:@"https://mock/something?a=b"]; + NSString *method = @"GET"; + + // When + [httpClient sendAsync:url + method:method + headers:nil + data:nil + completionHandler:^(NSData *responseBody, NSHTTPURLResponse *response, NSError *error) { + // Then + XCTAssertNil(response); + XCTAssertNil(responseBody); + XCTAssertNotNil(error); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; + + // Then + XCTAssertEqualObjects(actualRequest.URL, url); + XCTAssertEqualObjects(actualRequest.HTTPMethod, method); +} + +- (void)testDeleteUnrecoverableErrorWithoutHeadersNotRetried { + + // If + __block int numRequests = 0; + __block NSURLRequest *actualRequest; + [HTTPStubs + stubRequestsPassingTest:^BOOL(__unused NSURLRequest *request) { + return YES; + } + withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + ++numRequests; + actualRequest = request; + return [HTTPStubsResponse responseWithData:[NSData data] statusCode:MSACHTTPCodesNo400BadRequest headers:nil]; + }]; + __weak XCTestExpectation *expectation = [self expectationWithDescription:@""]; + MSACHttpClient *httpClient = [MSACHttpClient new]; + NSURL *url = [NSURL URLWithString:@"https://mock/something?a=b"]; + NSString *method = @"DELETE"; + + // When + [httpClient sendAsync:url + method:method + headers:nil + data:nil + completionHandler:^(NSData *responseBody, NSHTTPURLResponse *response, NSError *error) { + // Then + XCTAssertEqual(response.statusCode, MSACHTTPCodesNo400BadRequest); + XCTAssertNotNil(responseBody); + XCTAssertNil(error); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; + // Then + XCTAssertEqualObjects(actualRequest.URL, url); + XCTAssertEqualObjects(actualRequest.HTTPMethod, method); + XCTAssertEqual(numRequests, 1); +} + +- (void)testRecoverableNSErrorRetriedWhenNetworkReturns { + + /* + * The test scenario: + * 1. Request is sent. + * 2. Network goes down during request. + * 3. Test must ensure that the completion handler is not called here. + * 4. Network returns. + * 5. Test must verify that the completion handler is called now. + */ + + // If + __block BOOL completionHandlerCalled = NO; + __block BOOL firstTime = YES; + __block NSURLRequest *actualRequest; + dispatch_semaphore_t networkDownSemaphore = dispatch_semaphore_create(0); + NSArray *retryIntervals = @[ @1, @2 ]; + [HTTPStubs + stubRequestsPassingTest:^BOOL(__unused NSURLRequest *request) { + return YES; + } + withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + actualRequest = request; + if (firstTime) { + firstTime = NO; + NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCannotLoadFromNetwork userInfo:nil]; + + // Simulate network outage mid-request + [self simulateReachabilityChangedNotification:NotReachable]; + + // Network is down so it is now okay for the test to bring the network back. + dispatch_semaphore_signal(networkDownSemaphore); + return [HTTPStubsResponse responseWithError:error]; + } + return [HTTPStubsResponse responseWithData:[NSData data] statusCode:MSACHTTPCodesNo204NoContent headers:nil]; + }]; + __weak XCTestExpectation *expectation = [self expectationWithDescription:@""]; + MSACHttpClient *httpClient = [[MSACHttpClient alloc] initWithMaxHttpConnectionsPerHost:nil reachability:self.reachabilityMock]; + NSURL *url = [NSURL URLWithString:@"https://mock/something?a=b"]; + NSString *method = @"DELETE"; + + // When + [httpClient sendAsync:url + method:method + headers:nil + data:nil + retryIntervals:retryIntervals + compressionEnabled:YES + completionHandler:^(__unused NSData *responseBody, __unused NSHTTPURLResponse *response, __unused NSError *error) { + completionHandlerCalled = YES; + XCTAssertNotNil(responseBody); + XCTAssertNotNil(response); + XCTAssertEqual(response.statusCode, MSACHTTPCodesNo204NoContent); + XCTAssertNil(error); + [expectation fulfill]; + }]; + + // Wait a little to ensure that the completion handler is not invoked yet. + sleep(1); + + // Then + XCTAssertFalse(completionHandlerCalled); + + // When + + // Only bring the network back once it went down. + dispatch_semaphore_wait(networkDownSemaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kMSACTestTimeout * NSEC_PER_SEC))); + + // Restore the network and wait for completion handler to be called. + [self simulateReachabilityChangedNotification:ReachableViaWiFi]; + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testRecoverableHttpErrorThenPauseResume { + + // If + __block BOOL completionHandlerCalled = NO; + __block BOOL firstTime = YES; + __block NSURLRequest *actualRequest; + dispatch_semaphore_t networkDownSemaphore = dispatch_semaphore_create(0); + NSArray *retryIntervals = @[ @5, @2 ]; + __block MSACHttpClient *httpClient = [[MSACHttpClient alloc] initWithMaxHttpConnectionsPerHost:nil reachability:self.reachabilityMock]; + [HTTPStubs + stubRequestsPassingTest:^BOOL(__unused NSURLRequest *request) { + return YES; + } + withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + actualRequest = request; + if (firstTime) { + + // Simulate network outage while waiting for retry. + int64_t nanoseconds = (int64_t)(1 * NSEC_PER_SEC); + dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, nanoseconds); + dispatch_after(popTime, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) { + [self simulateReachabilityChangedNotification:NotReachable]; + dispatch_semaphore_signal(networkDownSemaphore); + }); + firstTime = NO; + return [HTTPStubsResponse responseWithData:[NSData data] statusCode:MSACHTTPCodesNo503ServiceUnavailable headers:nil]; + } + return [HTTPStubsResponse responseWithData:[NSData data] statusCode:MSACHTTPCodesNo204NoContent headers:nil]; + }]; + __weak XCTestExpectation *expectation = [self expectationWithDescription:@""]; + NSURL *url = [NSURL URLWithString:@"https://mock/something?a=b"]; + NSString *method = @"DELETE"; + + // When + [httpClient sendAsync:url + method:method + headers:nil + data:nil + retryIntervals:retryIntervals + compressionEnabled:YES + completionHandler:^(__unused NSData *responseBody, __unused NSHTTPURLResponse *response, __unused NSError *error) { + completionHandlerCalled = YES; + XCTAssertNotNil(responseBody); + XCTAssertNotNil(response); + XCTAssertEqual(response.statusCode, MSACHTTPCodesNo204NoContent); + XCTAssertNil(error); + [expectation fulfill]; + }]; + + // Wait a little to ensure that the completion handler is not invoked yet. + sleep(1); + + // Then + XCTAssertFalse(completionHandlerCalled); + + // When + + // Only bring the network back once it went down. + dispatch_semaphore_wait(networkDownSemaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kMSACTestTimeout * NSEC_PER_SEC))); + + // Restore the network and wait for completion handler to be called. + [self simulateReachabilityChangedNotification:ReachableViaWiFi]; + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testNetworkDownAndThenUpAgain { + + // If + __block int numRequests = 0; + __block NSURLRequest *actualRequest; + __block BOOL completionHandlerCalled = NO; + __weak XCTestExpectation *expectation = [self expectationWithDescription:@""]; + [HTTPStubs + stubRequestsPassingTest:^BOOL(__unused NSURLRequest *request) { + return YES; + } + withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + ++numRequests; + actualRequest = request; + return [HTTPStubsResponse responseWithData:[NSData data] statusCode:MSACHTTPCodesNo204NoContent headers:nil]; + }]; + MSACHttpClient *httpClient = [[MSACHttpClient alloc] initWithMaxHttpConnectionsPerHost:nil reachability:self.reachabilityMock]; + NSURL *url = [NSURL URLWithString:@"https://mock/something?a=b"]; + NSString *method = @"DELETE"; + + // When + [self simulateReachabilityChangedNotification:NotReachable]; + [httpClient sendAsync:url + method:method + headers:nil + data:nil + retryIntervals:@[ @1 ] + compressionEnabled:YES + completionHandler:^(NSData *responseBody, NSHTTPURLResponse *response, NSError *error) { + // Then + XCTAssertEqual(response.statusCode, MSACHTTPCodesNo204NoContent); + XCTAssertEqualObjects(responseBody, [NSData data]); + XCTAssertNil(error); + completionHandlerCalled = YES; + [expectation fulfill]; + }]; + + // Wait a while to make sure that the requests are not sent while the network is down. + sleep(1); + + // Then + XCTAssertFalse(completionHandlerCalled); + XCTAssertEqual(numRequests, 0); + + // When + [self simulateReachabilityChangedNotification:ReachableViaWiFi]; + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + + // Then + XCTAssertEqualObjects(actualRequest.URL, url); + XCTAssertEqualObjects(actualRequest.HTTPMethod, method); + XCTAssertEqual(numRequests, 1); + }]; +} + +- (void)testDeleteRecoverableErrorWithoutHeadersRetried { + + // If + __block int numRequests = 0; + __block NSURLRequest *actualRequest; + NSArray *retryIntervals = @[ @1, @2 ]; + [HTTPStubs + stubRequestsPassingTest:^BOOL(__unused NSURLRequest *request) { + return YES; + } + withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + actualRequest = request; + ++numRequests; + return [HTTPStubsResponse responseWithData:[NSData data] statusCode:MSACHTTPCodesNo500InternalServerError headers:nil]; + }]; + __weak XCTestExpectation *expectation = [self expectationWithDescription:@""]; + MSACHttpClient *httpClient = [[MSACHttpClient alloc] initWithMaxHttpConnectionsPerHost:nil reachability:nil]; + NSURL *url = [NSURL URLWithString:@"https://mock/something?a=b"]; + NSString *method = @"DELETE"; + + // When + [httpClient sendAsync:url + method:method + headers:nil + data:nil + retryIntervals:retryIntervals + compressionEnabled:YES + completionHandler:^(NSData *responseBody, NSHTTPURLResponse *response, NSError *error) { + // Then + XCTAssertEqual(response.statusCode, MSACHTTPCodesNo500InternalServerError); + XCTAssertNotNil(responseBody); + XCTAssertNil(error); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; + + // Then + XCTAssertEqualObjects(actualRequest.URL, url); + XCTAssertEqualObjects(actualRequest.HTTPMethod, method); + XCTAssertEqual(numRequests, 1 + [retryIntervals count]); +} + +- (void)testPauseThenResumeDoesNotResendCalls { + + // Scenario is pausing the client while there is a call that is being sent but hasn't completed yet, and then calling resume. + + // If + __block int numRequests = 0; + dispatch_semaphore_t responseSemaphore = dispatch_semaphore_create(0); + dispatch_semaphore_t pauseSemaphore = dispatch_semaphore_create(0); + [HTTPStubs + stubRequestsPassingTest:^BOOL(__unused NSURLRequest *request) { + return YES; + } + withStubResponse:^HTTPStubsResponse *(__unused NSURLRequest *request) { + ++numRequests; + + // Use this semaphore to prevent the pause from occurring before the call is enqueued. + dispatch_semaphore_signal(responseSemaphore); + + // Don't let the request finish before pausing. + dispatch_semaphore_wait(pauseSemaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kMSACTestTimeout * NSEC_PER_SEC))); + return [HTTPStubsResponse responseWithData:[NSData data] statusCode:MSACHTTPCodesNo204NoContent headers:nil]; + }]; + MSACHttpClient *httpClient = [[MSACHttpClient alloc] initWithMaxHttpConnectionsPerHost:nil reachability:self.reachabilityMock]; + NSURL *url = [NSURL URLWithString:@"https://mock/something?a=b"]; + NSString *method = @"DELETE"; + + // When + [httpClient sendAsync:url + method:method + headers:nil + data:nil + retryIntervals:@[] + compressionEnabled:YES + completionHandler:^(__unused NSData *responseBody, __unused NSHTTPURLResponse *response, __unused NSError *error){ + }]; + + // Don't pause until the call has been enqueued. + dispatch_semaphore_wait(responseSemaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kMSACTestTimeout * NSEC_PER_SEC))); + [self simulateReachabilityChangedNotification:NotReachable]; + dispatch_semaphore_signal(pauseSemaphore); + [self simulateReachabilityChangedNotification:ReachableViaWiFi]; + + // Wait a while to make sure that the request is not sent after resuming. + sleep(1); + + // Then + XCTAssertEqual(numRequests, 1); +} + +- (void)testDisablingCancelsCalls { + + // If + __weak XCTestExpectation *expectation = [self expectationWithDescription:@""]; + dispatch_semaphore_t responseSemaphore = dispatch_semaphore_create(0); + dispatch_semaphore_t testCompletedSemaphore = dispatch_semaphore_create(0); + [HTTPStubs + stubRequestsPassingTest:^BOOL(__unused NSURLRequest *request) { + return YES; + } + withStubResponse:^HTTPStubsResponse *(__unused NSURLRequest *request) { + dispatch_semaphore_signal(responseSemaphore); + + // Sleep to ensure that the call is really canceled instead of waiting for the response. + dispatch_semaphore_wait(testCompletedSemaphore, DISPATCH_TIME_FOREVER); + return [HTTPStubsResponse responseWithData:[NSData data] statusCode:MSACHTTPCodesNo204NoContent headers:nil]; + }]; + MSACHttpClient *httpClient = [[MSACHttpClient alloc] initWithMaxHttpConnectionsPerHost:nil reachability:nil]; + NSURL *url = [NSURL URLWithString:@"https://mock/something?a=b"]; + NSString *method = @"DELETE"; + + // When + [httpClient sendAsync:url + method:method + headers:nil + data:nil + retryIntervals:@[ @1 ] + compressionEnabled:YES + completionHandler:^(NSData *responseBody, NSHTTPURLResponse *response, NSError *error) { + // Then + XCTAssertNotNil(error); + XCTAssertNil(response); + XCTAssertNil(responseBody); + [expectation fulfill]; + }]; + + // Don't disable until the call has been enqueued. + dispatch_semaphore_wait(responseSemaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kMSACTestTimeout * NSEC_PER_SEC))); + [httpClient setEnabled:NO andDeleteDataOnDisabled:YES]; + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; + + // Clean up. + dispatch_semaphore_signal(testCompletedSemaphore); +} + +- (void)testDisableThenEnable { + + // If + __block NSURLRequest *actualRequest; + + // Stub HTTP response. + [HTTPStubs + stubRequestsPassingTest:^BOOL(__unused NSURLRequest *request) { + return YES; + } + withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + actualRequest = request; + NSData *responsePayload = [@"OK" dataUsingEncoding:kCFStringEncodingUTF8]; + return [HTTPStubsResponse responseWithData:responsePayload statusCode:MSACHTTPCodesNo200OK headers:nil]; + }]; + __weak XCTestExpectation *expectation = [self expectationWithDescription:@"HTTP Response 200"]; + MSACHttpClient *httpClient = [MSACHttpClient new]; + NSURL *url = [NSURL URLWithString:@"https://mock/something?a=b"]; + NSString *method = @"POST"; + NSData *payload = [@"somePayload" dataUsingEncoding:kCFStringEncodingUTF8]; + + // When + [httpClient setEnabled:NO]; + [httpClient setEnabled:YES]; + [httpClient sendAsync:url + method:method + headers:nil + data:payload + completionHandler:^(NSData *responseBody, NSHTTPURLResponse *response, NSError *error) { + // Then + XCTAssertEqual(response.statusCode, MSACHTTPCodesNo200OK); + XCTAssertEqualObjects(responseBody, [@"OK" dataUsingEncoding:kCFStringEncodingUTF8]); + XCTAssertNil(error); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; + + // Then + XCTAssertEqualObjects(actualRequest.URL, url); + XCTAssertEqualObjects(actualRequest.HTTPMethod, method); + XCTAssertEqualObjects(actualRequest.OHHTTPStubs_HTTPBody, payload); +} + +- (void)testRetryHeaderInResponse { + + // If + __block int numRequests = 0; + __block NSURLRequest *actualRequest; + NSArray *retryIntervals = @[ @1000000000, @100000000 ]; + [HTTPStubs + stubRequestsPassingTest:^BOOL(__unused NSURLRequest *request) { + return YES; + } + withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + actualRequest = request; + ++numRequests; + if (numRequests < 3) { + return [HTTPStubsResponse responseWithData:[NSData data] + statusCode:MSACHTTPCodesNo429TooManyRequests + headers:@{@"x-ms-retry-after-ms" : @"100"}]; + } + return [HTTPStubsResponse responseWithData:[NSData data] statusCode:MSACHTTPCodesNo204NoContent headers:nil]; + }]; + __weak XCTestExpectation *expectation = [self expectationWithDescription:@""]; + MSACHttpClient *httpClient = [[MSACHttpClient alloc] initWithMaxHttpConnectionsPerHost:nil reachability:nil]; + NSURL *url = [NSURL URLWithString:@"https://mock/something?a=b"]; + NSString *method = @"DELETE"; + + // When + [httpClient sendAsync:url + method:method + headers:nil + data:nil + retryIntervals:retryIntervals + compressionEnabled:YES + completionHandler:^(NSData *responseBody, NSHTTPURLResponse *response, NSError *error) { + // Then + XCTAssertEqual(response.statusCode, MSACHTTPCodesNo204NoContent); + XCTAssertNotNil(responseBody); + XCTAssertNil(error); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *_Nullable error) { + XCTAssertEqualObjects(actualRequest.URL, url); + XCTAssertEqualObjects(actualRequest.HTTPMethod, method); + XCTAssertEqual(numRequests, 1 + [retryIntervals count]); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testSendAsyncWhileDisabled { + + // If + __block NSURLRequest *actualRequest; + [HTTPStubs + stubRequestsPassingTest:^BOOL(__unused NSURLRequest *request) { + return YES; + } + withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + actualRequest = request; + return [HTTPStubsResponse responseWithData:[NSData data] statusCode:MSACHTTPCodesNo204NoContent headers:nil]; + }]; + __weak XCTestExpectation *expectation = [self expectationWithDescription:@""]; + MSACHttpClient *httpClient = [MSACHttpClient new]; + NSURL *url = [NSURL URLWithString:@"https://mock/something?a=b"]; + NSString *method = @"GET"; + + // When + [httpClient setEnabled:NO andDeleteDataOnDisabled:NO]; + [httpClient sendAsync:url + method:method + headers:nil + data:nil + completionHandler:^(NSData *responseBody, NSHTTPURLResponse *response, NSError *error) { + // Then + XCTAssertNil(response); + XCTAssertNil(responseBody); + XCTAssertNotNil(error); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; + + // Then + XCTAssertNil(actualRequest); +} + +- (void)testPausedWhenAllRetriesUsed { + + // If + XCTestExpectation *expectation = [self expectationWithDescription:@"Used all retries."]; + NSString *containerId = @"1"; + MSACLogContainer *container = OCMPartialMock([MSACLogContainer new]); + OCMStub([container isValid]).andReturn(YES); + OCMStub([container batchId]).andReturn(containerId); + MSACHttpClient *sut = [MSACHttpClient new]; + NSURL *url = [NSURL URLWithString:@"https://mock/something?a=b"]; + NSString *method = @"GET"; + + // Mock the call to intercept the retry. + NSArray *intervals = @[ @(0.5), @(1) ]; + + // Respond with a retryable error. + [MSACHttpTestUtil stubHttp500Response]; + + // Send the call. + [sut sendAsync:url + method:method + headers:nil + data:nil + retryIntervals:intervals + compressionEnabled:YES + completionHandler:^(NSData *_Nullable responseBody __unused, NSHTTPURLResponse *_Nullable response __unused, + NSError *_Nullable error __unused) { + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + XCTAssertTrue(sut.paused); + XCTAssertTrue([sut.pendingCalls count] == 0); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testRetryStoppedWhilePaused { + + // If + XCTestExpectation *responseReceivedExpectation = [self expectationWithDescription:@"Request completed."]; + MSACDevice *device = OCMPartialMock([MSACDevice new]); + OCMStub([device isValid]).andReturn(YES); + MSACHttpClient *httpClient = [MSACHttpClient new]; + + // Mock the call to intercept the retry. + NSArray *intervals = @[ @(UINT_MAX), @(UINT_MAX) ]; + MSACHttpCall *httpCall = + [[MSACHttpCall alloc] initWithUrl:[[NSURL alloc] initWithString:@""] + method:@"GET" + headers:nil + data:nil + retryIntervals:intervals + compressionEnabled:YES + completionHandler:^(NSData *_Nullable responseBody __unused, NSHTTPURLResponse *_Nullable response __unused, + NSError *_Nullable error __unused){ + }]; + + // A non-zero number that should be reset by the end. + httpCall.retryCount = 1; + id mockHttpClient = OCMPartialMock(httpClient); + [httpClient.pendingCalls addObject:httpCall]; + + OCMStub([mockHttpClient requestCompletedWithHttpCall:httpCall data:OCMOCK_ANY response:OCMOCK_ANY error:OCMOCK_ANY]) + .andForwardToRealObject() + .andDo(^(NSInvocation *invocation __unused) { + [responseReceivedExpectation fulfill]; + }); + + // Respond with a retryable error. + [MSACHttpTestUtil stubHttp500Response]; + + // Send the call. + [httpClient sendCallAsync:httpCall]; + [self waitForExpectationsWithTimeout:5 + handler:^(NSError *error) { + // When + // Pause now that the call is retrying. + [httpClient pause]; + + // Then + // Retry must be stopped. + if (@available(macOS 10.10, tvOS 9.0, watchOS 2.0, *)) { + XCTAssertNotEqual(0, dispatch_testcancel(httpCall.timerSource)); + } + XCTAssertEqual(httpCall.retryCount, 0); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)simulateReachabilityChangedNotification:(NetworkStatus)status { + self.currentNetworkStatus = status; + [[NSNotificationCenter defaultCenter] postNotificationName:kMSACReachabilityChangedNotification object:self.reachabilityMock]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACHttpIngestionTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACHttpIngestionTests.m new file mode 100644 index 0000000000..3becc427ff --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACHttpIngestionTests.m @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACDevice.h" +#import "MSACHttpClient.h" +#import "MSACHttpIngestionPrivate.h" +#import "MSACTestFrameworks.h" + +@interface MSACHttpIngestionTests : XCTestCase + +@property(nonatomic) MSACHttpIngestion *sut; +@property(nonatomic) MSACHttpClient *httpClientMock; + +@end + +@implementation MSACHttpIngestionTests + +- (void)setUp { + [super setUp]; + NSDictionary *queryStrings = @{@"api-version" : @"1.0.0"}; + self.httpClientMock = OCMPartialMock([MSACHttpClient new]); + + // sut: System under test + self.sut = [[MSACHttpIngestion alloc] initWithHttpClient:self.httpClientMock + baseUrl:@"https://www.contoso.com" + apiPath:@"/test-path" + headers:nil + queryStrings:queryStrings + retryIntervals:@[ @(0.5), @(1), @(1.5) ]]; +} + +- (void)tearDown { + [super tearDown]; + self.sut = nil; +} + +- (void)testValidETagFromResponse { + + // If + NSString *expectedETag = @"IAmAnETag"; + NSHTTPURLResponse *response = [NSHTTPURLResponse new]; + id responseMock = OCMPartialMock(response); + OCMStub([responseMock allHeaderFields]).andReturn(@{@"Etag" : expectedETag}); + + // When + NSString *eTag = [MSACHttpIngestion eTagFromResponse:responseMock]; + + // Then + XCTAssertEqualObjects(expectedETag, eTag); +} + +- (void)testInvalidETagFromResponse { + + // If + NSHTTPURLResponse *response = [NSHTTPURLResponse new]; + id responseMock = OCMPartialMock(response); + OCMStub([responseMock allHeaderFields]).andReturn(@{@"Etag1" : @"IAmAnETag"}); + + // When + NSString *eTag = [MSACHttpIngestion eTagFromResponse:responseMock]; + + // Then + XCTAssertNil(eTag); +} + +- (void)testNoETagFromResponse { + + // If + NSHTTPURLResponse *response = [NSHTTPURLResponse new]; + + // When + NSString *eTag = [MSACHttpIngestion eTagFromResponse:response]; + + // Then + XCTAssertNil(eTag); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACHttpUtilTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACHttpUtilTests.m new file mode 100644 index 0000000000..4d63aa01ae --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACHttpUtilTests.m @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACHttpUtil.h" +#import "MSACTestFrameworks.h" + +@interface MSACHttpUtilTests : XCTestCase + +@end + +@implementation MSACHttpUtilTests + +- (void)testLargeSecret { + + // If + NSString *secret = @"shhhh-its-a-secret"; + NSString *hiddenSecret; + + // When + hiddenSecret = [MSACHttpUtil hideSecret:secret]; + + // Then + NSString *fullyHiddenSecret = [@"" stringByPaddingToLength:hiddenSecret.length + withString:kMSACHidingStringForAppSecret + startingAtIndex:0]; + NSString *appSecretHiddenPart = [hiddenSecret commonPrefixWithString:fullyHiddenSecret options:0]; + NSString *appSecretVisiblePart = [hiddenSecret substringFromIndex:appSecretHiddenPart.length]; + assertThatInteger(secret.length - appSecretHiddenPart.length, equalToShort(kMSACMaxCharactersDisplayedForAppSecret)); + assertThat(appSecretVisiblePart, is([secret substringFromIndex:appSecretHiddenPart.length])); +} + +- (void)testShortSecret { + + // If + NSString *secret = @""; + for (short i = 1; i <= kMSACMaxCharactersDisplayedForAppSecret - 1; i++) + secret = [NSString stringWithFormat:@"%@%hd", secret, i]; + NSString *hiddenSecret; + + // When + hiddenSecret = [MSACHttpUtil hideSecret:secret]; + + // Then + NSString *fullyHiddenSecret = [@"" stringByPaddingToLength:hiddenSecret.length + withString:kMSACHidingStringForAppSecret + startingAtIndex:0]; + assertThatInteger(hiddenSecret.length, equalToUnsignedInteger(secret.length)); + assertThat(hiddenSecret, is(fullyHiddenSecret)); +} + +- (void)testIsNoInternetConnectionError { + + // When + NSError *error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorNotConnectedToInternet userInfo:nil]; + + // Then + XCTAssertTrue([MSACHttpUtil isNoInternetConnectionError:error]); + + // When + error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorNetworkConnectionLost userInfo:nil]; + + // Then + XCTAssertTrue([MSACHttpUtil isNoInternetConnectionError:error]); + + // When + error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorServerCertificateHasBadDate userInfo:nil]; + + // Then + XCTAssertFalse([MSACHttpUtil isNoInternetConnectionError:error]); +} + +- (void)testSSLConnectionErrorDetected { + + // When + NSError *error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorSecureConnectionFailed userInfo:nil]; + + // Then + XCTAssertTrue([MSACHttpUtil isSSLConnectionError:error]); + + // When + error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorServerCertificateHasBadDate userInfo:nil]; + + // Then + XCTAssertTrue([MSACHttpUtil isSSLConnectionError:error]); + + // When + error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorServerCertificateUntrusted userInfo:nil]; + + // Then + XCTAssertTrue([MSACHttpUtil isSSLConnectionError:error]); + + // When + error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorServerCertificateHasUnknownRoot userInfo:nil]; + + // Then + XCTAssertTrue([MSACHttpUtil isSSLConnectionError:error]); + + // When + error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorServerCertificateNotYetValid userInfo:nil]; + + // Then + XCTAssertTrue([MSACHttpUtil isSSLConnectionError:error]); + + // When + error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorClientCertificateRejected userInfo:nil]; + + // Then + XCTAssertTrue([MSACHttpUtil isSSLConnectionError:error]); + + // When + error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorClientCertificateRequired userInfo:nil]; + + // Then + XCTAssertTrue([MSACHttpUtil isSSLConnectionError:error]); + + // When + error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorCannotLoadFromNetwork userInfo:nil]; + + // Then + XCTAssertTrue([MSACHttpUtil isSSLConnectionError:error]); + + // When + error = [[NSError alloc] initWithDomain:NSURLErrorFailingURLErrorKey code:NSURLErrorCannotLoadFromNetwork userInfo:nil]; + + // Then + XCTAssertFalse([MSACHttpUtil isSSLConnectionError:error]); + + // When + error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:10 userInfo:nil]; + + // Then + XCTAssertFalse([MSACHttpUtil isSSLConnectionError:error]); +} + +- (void)testHideSecretInString { + + // If + NSString *secret = @"12345678-1234-1234-1234-123456789012"; + NSString *string = [NSString stringWithFormat:@"this-%@-should-be-encoded", secret]; + NSString *expectedEncodeString = [NSString stringWithFormat:@"this-%@56789012-should-be-encoded", [@"" stringByPaddingToLength:28 + withString:@"*" + startingAtIndex:0]]; + + // When + NSString *encodeString = [MSACHttpUtil hideSecretInString:string secret:secret]; + + // Then + XCTAssertEqualObjects(encodeString, expectedEncodeString); +} + +- (void)testIsRecoverableError { + for (int i = 0; i < 530; i++) { + + // When + BOOL result = [MSACHttpUtil isRecoverableError:i]; + + // Then + if (i >= 500) { + XCTAssertTrue(result); + } else if (i == 408) { + XCTAssertTrue(result); + } else if (i == 429) { + XCTAssertTrue(result); + } else if (i == 0) { + XCTAssertTrue(result); + } else { + XCTAssertFalse(result); + } + } +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACKeychainUtilTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACKeychainUtilTests.m new file mode 100644 index 0000000000..978b3cfa3c --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACKeychainUtilTests.m @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACKeychainUtil.h" +#import "MSACKeychainUtilPrivate.h" +#import "MSACTestFrameworks.h" + +@interface MSACKeychainUtilTests : XCTestCase +@property(nonatomic) id keychainUtilMock; +@property(nonatomic, copy) NSString *acServiceName; + +@end + +@implementation MSACKeychainUtilTests + +- (void)setUp { + [super setUp]; + self.keychainUtilMock = OCMClassMock([MSACKeychainUtil class]); + self.acServiceName = [NSString stringWithFormat:@"%@.%@", [self getBundleIdentifier], kMSACServiceSuffix]; +} + +- (void)tearDown { + [super tearDown]; + [self.keychainUtilMock stopMocking]; +} + +#if !TARGET_OS_TV +- (void)testKeychain { + + // If + NSString *key = @"Test Key"; + NSString *value = @"Test Value"; + NSDictionary *expectedAddItemQuery = @{ + (__bridge id)kSecAttrService : self.acServiceName, + (__bridge id)kSecClass : @"genp", + (__bridge id)kSecAttrAccount : key, + (__bridge id)kSecValueData : (NSData * _Nonnull)[value dataUsingEncoding:NSUTF8StringEncoding], + (__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly + }; + NSDictionary *expectedDeleteItemQuery = + @{(__bridge id)kSecAttrService : self.acServiceName, (__bridge id)kSecClass : @"genp", (__bridge id)kSecAttrAccount : key}; + NSDictionary *expectedMatchItemQuery = @{ + (__bridge id)kSecAttrService : self.acServiceName, + (__bridge id)kSecClass : @"genp", + (__bridge id)kSecAttrAccount : key, + (__bridge id)kSecReturnData : (__bridge id)kCFBooleanTrue, + (__bridge id)kSecMatchLimit : (__bridge id)kSecMatchLimitOne, + }; + + // Expect these stubbed calls. + OCMStub([self.keychainUtilMock addSecItem:[expectedAddItemQuery mutableCopy]]).andReturn(noErr); + OCMStub([self.keychainUtilMock deleteSecItem:[expectedDeleteItemQuery mutableCopy]]).andReturn(noErr); + OCMStub([self.keychainUtilMock secItemCopyMatchingQuery:[expectedMatchItemQuery mutableCopy] result:[OCMArg anyPointer]]) + .andReturn(noErr); + + // Reject any other calls. + OCMReject([self.keychainUtilMock addSecItem:[OCMArg any]]); + OCMReject([self.keychainUtilMock deleteSecItem:[OCMArg any]]); + OCMReject([self.keychainUtilMock secItemCopyMatchingQuery:[OCMArg any] result:[OCMArg anyPointer]]); + + // When + [MSACKeychainUtil storeString:value forKey:key]; + [MSACKeychainUtil stringForKey:key statusCode:nil]; + [MSACKeychainUtil deleteStringForKey:key]; + + // Then + OCMVerifyAll(self.keychainUtilMock); +} + +- (void)testKeychainGetStringSetsError { + + // If + NSString *key = @"Test Key"; + OSStatus statusExpected = errSecNoAccessForItem; + OCMStub([self.keychainUtilMock secItemCopyMatchingQuery:[OCMArg any] result:[OCMArg anyPointer]]).andReturn(statusExpected); + + // When + OSStatus statusReceived; + NSString *result = [MSACKeychainUtil stringForKey:key statusCode:&statusReceived]; + + // Then + XCTAssertNil(result); + XCTAssertEqual(statusReceived, statusExpected); +} + +- (void)testKeychainGetStringAllowsNilErrorArgument { + + // If + NSString *key = @"Test Key"; + OSStatus statusExpected = errSecNoAccessForItem; + OCMStub([self.keychainUtilMock secItemCopyMatchingQuery:[OCMArg any] result:[OCMArg anyPointer]]).andReturn(statusExpected); + + // When + NSString *result = [MSACKeychainUtil stringForKey:key statusCode:nil]; + + // Then + XCTAssertNil(result); +} + +- (void)testStoreStringHandlesDuplicateItemError { + + // If + NSString *key = @"testKey"; + NSString *value = @"testValue"; + __block int addSecItemCallsCount = 0; + OCMStub([self.keychainUtilMock addSecItem:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + ++addSecItemCallsCount; + int returnValue = addSecItemCallsCount > 1 ? noErr : errSecDuplicateItem; + [invocation setReturnValue:&returnValue]; + }); + + // When + BOOL actualResult = [MSACKeychainUtil storeString:value forKey:key]; + + // Then + XCTAssertEqual(addSecItemCallsCount, 2); + XCTAssertEqual(actualResult, YES); + OCMVerify([self.keychainUtilMock deleteSecItem:OCMOCK_ANY]); +} + +#endif + +// Before SDK 12.2 (bundled with Xcode 10.*) when running in a unit test bundle the bundle identifier is null. +// 12.2 and after the above bundle identifier is com.apple.dt.xctest.tool. +- (NSString *)getBundleIdentifier { +#if ((defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_12_2) || \ + (defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_10_14_4)) + return @"com.apple.dt.xctest.tool"; +#else + return @"(null)"; +#endif +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACLogContainerTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACLogContainerTests.m new file mode 100644 index 0000000000..054b934bd5 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACLogContainerTests.m @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "AppCenter+Internal.h" +#import "MSACAbstractLogInternal.h" +#import "MSACLogContainer.h" +#import "MSACTestFrameworks.h" + +@interface MSACLogContainerTests : XCTestCase + +@end + +@implementation MSACLogContainerTests + +- (void)testLogContainerSerialization { + + // If + MSACLogContainer *logContainer = [MSACLogContainer new]; + + MSACAbstractLog *log1 = [MSACAbstractLog new]; + log1.sid = MSAC_UUID_STRING; + log1.timestamp = [NSDate date]; + + MSACAbstractLog *log2 = [MSACAbstractLog new]; + log2.sid = MSAC_UUID_STRING; + log2.timestamp = [NSDate date]; + + logContainer.logs = (NSArray> *)@[ log1, log2 ]; + + // When + NSString *jsonString = [logContainer serializeLog]; + + // Then + XCTAssertTrue([jsonString length] > 0); +} + +- (void)testIsValidForEmptyLogs { + + // If + MSACLogContainer *logContainer = [MSACLogContainer new]; + + XCTAssertFalse([logContainer isValid]); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACLogDBStorageTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACLogDBStorageTests.m new file mode 100644 index 0000000000..c2f545b250 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACLogDBStorageTests.m @@ -0,0 +1,1193 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAbstractLogInternal.h" +#import "MSACDBStoragePrivate.h" +#import "MSACLogDBStoragePrivate.h" +#import "MSACLogDBStorageVersion.h" +#import "MSACLogWithProperties.h" +#import "MSACStorageBindableArray.h" +#import "MSACStorageBindableType.h" +#import "MSACStorageNumberType.h" +#import "MSACStorageTestUtil.h" +#import "MSACTestFrameworks.h" +#import "MSACUtility.h" + +static NSString *const kMSACTestGroupId = @"TestGroupId"; +static NSString *const kMSACAnotherTestGroupId = @"AnotherGroupId"; + +// 40 KiB (10 pages by 4 KiB). +static const long kMSACTestStorageSizeMinimumUpperLimitInBytes = 40 * 1024; + +static NSString *const kMSACLatestSchema = @"CREATE TABLE \"logs\" (" + @"\"id\" INTEGER PRIMARY KEY AUTOINCREMENT, " + @"\"groupId\" TEXT NOT NULL, " + @"\"log\" TEXT NOT NULL, " + @"\"targetToken\" TEXT, " + @"\"targetKey\" TEXT, " + @"\"priority\" INTEGER)"; + +@interface MSACLogDBStorageTests : XCTestCase + +@property(nonatomic) MSACLogDBStorage *sut; +@property(nonatomic) MSACStorageTestUtil *storageTestUtil; + +@end + +@implementation MSACLogDBStorageTests + +#pragma mark - Setup + +- (void)setUp { + [super setUp]; + self.storageTestUtil = [[MSACStorageTestUtil alloc] initWithDbFileName:kMSACDBFileName]; + [self.storageTestUtil deleteDatabase]; + XCTAssertEqual([self.storageTestUtil getDataLengthInBytes], 0); + self.sut = OCMPartialMock([MSACLogDBStorage new]); + OCMStub([self.sut executeNonSelectionQuery:OCMOCK_ANY withValues:OCMOCK_ANY]) + .andDo(^(NSInvocation *invocation) { + NSString *query; + [invocation getArgument:&query atIndex:2]; + [self validateQuerySyntax:query]; + }) + .andForwardToRealObject(); +} + +- (void)tearDown { + [self.storageTestUtil deleteDatabase]; + [super tearDown]; +} + +- (void)testLoadTooManyLogs { + + // If + NSUInteger expectedLogsCount = 5; + NSMutableArray *expectedLogs = [[self generateAndSaveLogsWithCount:expectedLogsCount + 1 + groupId:kMSACTestGroupId + flags:MSACFlagsDefault + andVerifyLogGeneration:YES] mutableCopy]; + [expectedLogs removeLastObject]; + + // When + BOOL moreLogsAvailable = [self.sut loadLogsWithGroupId:kMSACTestGroupId + limit:expectedLogsCount + excludedTargetKeys:nil + completionHandler:^(NSArray> *_Nonnull logArray, NSString *_Nonnull batchId) { + // Then + assertThat(batchId, notNilValue()); + assertThat(expectedLogs, is(logArray)); + }]; + XCTAssertTrue(moreLogsAvailable); +} + +- (void)testLoadJustEnoughNormalLogs { + + // If + NSUInteger expectedLogsCount = 5; + NSArray *expectedLogs = [self generateAndSaveLogsWithCount:expectedLogsCount + groupId:kMSACTestGroupId + flags:MSACFlagsNormal + andVerifyLogGeneration:YES]; + + // When + BOOL moreLogsAvailable = [self.sut loadLogsWithGroupId:kMSACTestGroupId + limit:expectedLogsCount + excludedTargetKeys:nil + completionHandler:^(NSArray> *_Nonnull logArray, NSString *_Nonnull batchId) { + // Then + assertThat(batchId, notNilValue()); + assertThat(expectedLogs, is(logArray)); + }]; + XCTAssertFalse(moreLogsAvailable); +} + +- (void)testLoadJustEnoughMixedPriorityLogs { + + // If + NSUInteger segmentLogCount = 2; + NSUInteger expectedLogsCount = segmentLogCount * 4; + NSMutableArray *expectedLogs = [NSMutableArray new]; + + // Create 2 normal logs. + NSMutableArray *normalLogs = [[self generateAndSaveLogsWithCount:segmentLogCount + groupId:kMSACTestGroupId + flags:MSACFlagsNormal + andVerifyLogGeneration:YES] mutableCopy]; + + // Create 2 critical logs. + expectedLogs = [[expectedLogs arrayByAddingObjectsFromArray:[self generateAndSaveLogsWithCount:segmentLogCount + groupId:kMSACTestGroupId + flags:MSACFlagsCritical + andVerifyLogGeneration:YES]] mutableCopy]; + + // Create 2 normal logs. + normalLogs = [[normalLogs arrayByAddingObjectsFromArray:[self generateAndSaveLogsWithCount:segmentLogCount + groupId:kMSACTestGroupId + flags:MSACFlagsNormal + andVerifyLogGeneration:NO]] mutableCopy]; + + // Create 2 critical logs. + expectedLogs = [[expectedLogs arrayByAddingObjectsFromArray:[self generateAndSaveLogsWithCount:segmentLogCount + groupId:kMSACTestGroupId + flags:MSACFlagsCritical + andVerifyLogGeneration:NO]] mutableCopy]; + + // Build expected logs + expectedLogs = [[expectedLogs arrayByAddingObjectsFromArray:normalLogs] mutableCopy]; + + // When + BOOL moreLogsAvailable = [self.sut loadLogsWithGroupId:kMSACTestGroupId + limit:expectedLogsCount + excludedTargetKeys:nil + completionHandler:^(NSArray> *_Nonnull logArray, NSString *_Nonnull batchId) { + // Then + assertThat(batchId, notNilValue()); + assertThat(expectedLogs, is(logArray)); + }]; + XCTAssertFalse(moreLogsAvailable); +} + +- (void)testLoadNotEnoughLogs { + + // If + NSUInteger expectedLogsCount = 2; + NSUInteger limit = 5; + NSArray *expectedLogs = [self generateAndSaveLogsWithCount:expectedLogsCount + groupId:kMSACTestGroupId + flags:MSACFlagsDefault + andVerifyLogGeneration:YES]; + + // When + BOOL moreLogsAvailable = [self.sut loadLogsWithGroupId:kMSACTestGroupId + limit:limit + excludedTargetKeys:nil + completionHandler:^(NSArray> *_Nonnull logArray, NSString *_Nonnull batchId) { + // Then + assertThat(batchId, notNilValue()); + assertThat(expectedLogs, is(logArray)); + }]; + XCTAssertFalse(moreLogsAvailable); +} + +- (void)testLoadLogsWhilePendingBatchesFromSameGroupId { + + // If + NSUInteger expectedLogsCount = 5; + __block NSArray *expectedLogs = [[self generateAndSaveLogsWithCount:expectedLogsCount + groupId:kMSACTestGroupId + flags:MSACFlagsDefault + andVerifyLogGeneration:YES] mutableCopy]; + __block NSArray *unexpectedLogs; + __block NSString *unexpectedBatchId; + + // Load some logs to trigger a new batch. + [self.sut loadLogsWithGroupId:kMSACTestGroupId + limit:2 + excludedTargetKeys:nil + completionHandler:^(NSArray> *_Nonnull logArray, NSString *_Nonnull batchId) { + // Those values shouldn't be in the next batch. + unexpectedLogs = logArray; + unexpectedBatchId = batchId; + }]; + + // When + BOOL moreLogsAvailable = [self.sut loadLogsWithGroupId:kMSACTestGroupId + limit:expectedLogsCount + excludedTargetKeys:nil + completionHandler:^(NSArray> *_Nonnull logArray, NSString *_Nonnull batchId) { + // Then + // Logs from previous batch are not expected here. + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"NOT (SELF IN %@)", unexpectedLogs]; + expectedLogs = [expectedLogs filteredArrayUsingPredicate:predicate]; + assertThat(batchId, notNilValue()); + assertThat(expectedLogs, is(logArray)); + assertThat(batchId, isNot(unexpectedBatchId)); + }]; + XCTAssertFalse(moreLogsAvailable); +} + +- (void)testLoadCommonSchemaLogsWhilePendingBatchesWithSpecificTargetKeys { + + // If + + // Key: 1, group: A. + MSACCommonSchemaLog *log1 = [MSACCommonSchemaLog new]; + [log1 addTransmissionTargetToken:@"1-t"]; + log1.iKey = @"o:1"; + [self.sut saveLog:log1 withGroupId:kMSACTestGroupId flags:MSACFlagsDefault]; + + // Key: 2, group: A. + MSACCommonSchemaLog *log2 = [MSACCommonSchemaLog new]; + [log2 addTransmissionTargetToken:@"2-t"]; + log2.iKey = @"o:2"; + [self.sut saveLog:log2 withGroupId:kMSACTestGroupId flags:MSACFlagsDefault]; + + // Key: 2, group: B. + MSACCommonSchemaLog *log3 = [MSACCommonSchemaLog new]; + [log3 addTransmissionTargetToken:@"2-t"]; + log3.iKey = @"o:2"; + [self.sut saveLog:log3 withGroupId:kMSACAnotherTestGroupId flags:MSACFlagsDefault]; + + // Key: 1, group: A. + MSACCommonSchemaLog *log4 = [MSACCommonSchemaLog new]; + [log4 addTransmissionTargetToken:@"1-t"]; + log4.iKey = @"o:1"; + [self.sut saveLog:log4 withGroupId:kMSACTestGroupId flags:MSACFlagsDefault]; + + // Key: 2, group: A. + MSACCommonSchemaLog *log5 = [MSACCommonSchemaLog new]; + [log5 addTransmissionTargetToken:@"2-t"]; + log5.iKey = @"o:2"; + [self.sut saveLog:log5 withGroupId:kMSACTestGroupId flags:MSACFlagsDefault]; + + // When + [self.sut loadLogsWithGroupId:kMSACTestGroupId + limit:10 + excludedTargetKeys:@[ @"1" ] + completionHandler:^(NSArray *_Nonnull logArray, __unused NSString *batchId) { + // Then + assertThatInt(logArray.count, equalToInt(2)); + for (MSACCommonSchemaLog *log in logArray) { + XCTAssertTrue([log.iKey isEqualToString:@"o:2"]); + } + }]; + [self.sut loadLogsWithGroupId:kMSACTestGroupId + limit:10 + excludedTargetKeys:@[ @"2" ] + completionHandler:^(NSArray *_Nonnull logArray, __unused NSString *batchId) { + // Then + assertThatInt(logArray.count, equalToInt(2)); + for (MSACCommonSchemaLog *log in logArray) { + XCTAssertTrue([log.iKey isEqualToString:@"o:1"]); + } + }]; + [self.sut loadLogsWithGroupId:kMSACTestGroupId + limit:10 + excludedTargetKeys:nil + completionHandler:^(NSArray *_Nonnull logArray, __unused NSString *batchId) { + // Then + assertThatInt(logArray.count, equalToInt(0)); + }]; +} + +- (void)testLoadCommonSchemaLogsWhilePendingBatchesWithTargetKeysForBackwardCompatibility { + + // If + NSString *targetKeyFormat = @"testTargetKey%d"; + + // When + for (int i = 0; i < 20; i++) { + MSACCommonSchemaLog *log = [MSACCommonSchemaLog new]; + if (i % 4 != 0) { + NSString *targetKey = [NSString stringWithFormat:targetKeyFormat, i % 4]; + NSString *targetToken = [targetKey stringByAppendingString:@"-secret"]; + [log addTransmissionTargetToken:targetToken]; + } + [self.sut saveLog:log withGroupId:kMSACTestGroupId flags:MSACFlagsDefault]; + } + + // Then + [self.sut loadLogsWithGroupId:kMSACTestGroupId + limit:20 + excludedTargetKeys:@[ @"testTargetKey1", @"testTargetKey2" ] + completionHandler:^(NSArray *_Nonnull logArray, __unused NSString *batchId) { + assertThatInt(logArray.count, equalToInt(5)); + }]; +} + +- (void)testLoadCommonSchemaLogsWhilePendingBatchesWithoutTargetKeysForBackwardCompatibility { + + // If + NSString *targetKey = @"testTargetKey"; + + // When + for (int i = 0; i < 10; i++) { + MSACCommonSchemaLog *log = [MSACCommonSchemaLog new]; + if (i < 5) { + NSString *targetToken = [targetKey stringByAppendingString:@"-secret"]; + [log addTransmissionTargetToken:targetToken]; + log.iKey = targetKey; + } + [self.sut saveLog:log withGroupId:kMSACTestGroupId flags:MSACFlagsDefault]; + } + + // Then + [self.sut loadLogsWithGroupId:kMSACTestGroupId + limit:10 + excludedTargetKeys:nil + completionHandler:^(NSArray *_Nonnull logArray, __unused NSString *batchId) { + int iKeyCount = 0; + for (MSACCommonSchemaLog *log in logArray) { + if ([log.iKey isEqualToString:targetKey]) { + iKeyCount++; + } + } + XCTAssertEqual(iKeyCount, 5); + XCTAssertEqual(logArray.count, 10); + }]; +} + +- (void)testLoadLogsWhilePendingBatchesFromOtherGroupId { + + // If + NSUInteger expectedLogsCount = 5; + __block NSArray *expectedLogs = [[self generateAndSaveLogsWithCount:expectedLogsCount + groupId:kMSACTestGroupId + flags:MSACFlagsDefault + andVerifyLogGeneration:YES] mutableCopy]; + __block NSArray *unexpectedLogs; + __block NSString *unexpectedBatchId; + + // Load some logs to trigger a new batch from another group Id. + [self.sut loadLogsWithGroupId:kMSACAnotherTestGroupId + limit:2 + excludedTargetKeys:nil + completionHandler:^(NSArray> *_Nonnull logArray, NSString *_Nonnull batchId) { + // Those values shouldn't be in the next batch. + unexpectedLogs = logArray; + unexpectedBatchId = batchId; + }]; + + // When + BOOL moreLogsAvailable = [self.sut loadLogsWithGroupId:kMSACTestGroupId + limit:expectedLogsCount + excludedTargetKeys:nil + completionHandler:^(NSArray> *_Nonnull logArray, NSString *_Nonnull batchId) { + // Then + // Logs from previous batch are not expected here. + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"NOT (SELF IN %@)", unexpectedLogs]; + expectedLogs = [expectedLogs filteredArrayUsingPredicate:predicate]; + assertThat(batchId, notNilValue()); + assertThat(expectedLogs, is(logArray)); + assertThat(batchId, isNot(unexpectedBatchId)); + }]; + XCTAssertFalse(moreLogsAvailable); +} + +- (void)testLoadUnlimitedLogs { + + // If + NSUInteger expectedLogsCount = 42; + NSArray *expectedLogs = [self generateAndSaveLogsWithCount:expectedLogsCount + groupId:kMSACTestGroupId + flags:MSACFlagsDefault + andVerifyLogGeneration:YES]; + + // When + NSArray *logs = [self.sut logsFromDBWithGroupId:kMSACTestGroupId]; + + // Then + assertThat(expectedLogs, is(logs)); +} + +- (void)testDeleteLogsWithGroupId { + + // Test deletion with no batch. + + // If + self.sut = [MSACLogDBStorage new]; + // [self.sut.batches removeAllObjects]; + [self generateAndSaveLogsWithCount:5 groupId:kMSACTestGroupId flags:MSACFlagsDefault andVerifyLogGeneration:YES]; + + // When + [self.sut deleteLogsWithGroupId:kMSACTestGroupId]; + + // Then + assertThatInteger([self.sut countEntriesForTable:kMSACLogTableName condition:nil withValues:nil], equalToInteger(0)); + assertThatInteger(self.sut.batches.count, equalToInteger(0)); + + // Test deletion with only the batch to delete. + + // If + // Generate logs and create one batch by loading logs. + [self generateAndSaveLogsWithCount:5 groupId:kMSACTestGroupId flags:MSACFlagsDefault andVerifyLogGeneration:YES]; + [self.sut loadLogsWithGroupId:kMSACTestGroupId limit:2 excludedTargetKeys:nil completionHandler:nil]; + + // When + [self.sut deleteLogsWithGroupId:kMSACTestGroupId]; + + // Then + assertThatInteger([self.sut countEntriesForTable:kMSACLogTableName condition:nil withValues:nil], equalToInteger(0)); + assertThatInteger(self.sut.batches.count, equalToInteger(0)); + + // Test deletion with more than one batch to delete. + + // If + // Generate logs and create two batches by loading logs twice. + [self generateAndSaveLogsWithCount:5 groupId:kMSACTestGroupId flags:MSACFlagsDefault andVerifyLogGeneration:YES]; + [self.sut loadLogsWithGroupId:kMSACTestGroupId limit:2 excludedTargetKeys:nil completionHandler:nil]; + [self.sut loadLogsWithGroupId:kMSACTestGroupId limit:2 excludedTargetKeys:nil completionHandler:nil]; + + // When + [self.sut deleteLogsWithGroupId:kMSACTestGroupId]; + + // Then + assertThatInteger([self.sut countEntriesForTable:kMSACLogTableName condition:nil withValues:nil], equalToInteger(0)); + assertThatInteger(self.sut.batches.count, equalToInteger(0)); + + // Test deletion with the batch to delete and batches from other groups. + + // If + // Generate logs and create two batches of different group Ids. + __block NSString *batchIdToDelete; + [self generateAndSaveLogsWithCount:2 groupId:kMSACTestGroupId flags:MSACFlagsDefault andVerifyLogGeneration:YES]; + NSArray *expectedLogs = [self generateAndSaveLogsWithCount:3 + groupId:kMSACAnotherTestGroupId + flags:MSACFlagsDefault + andVerifyLogGeneration:YES]; + [self.sut loadLogsWithGroupId:kMSACTestGroupId + limit:2 + excludedTargetKeys:nil + completionHandler:^(__attribute__((unused)) NSArray *_Nonnull logArray, NSString *batchId) { + batchIdToDelete = batchId; + }]; + [self.sut loadLogsWithGroupId:kMSACAnotherTestGroupId limit:2 excludedTargetKeys:nil completionHandler:nil]; + + // When + [self.sut deleteLogsWithGroupId:kMSACTestGroupId]; + + // Then + NSArray *remainingLogs = [self loadLogsWhere:nil withValues:nil]; + assertThat(remainingLogs, is(expectedLogs)); + assertThatInteger(self.sut.batches.count, equalToInteger(1)); + assertThatBool([self.sut.batches.allKeys containsObject:batchIdToDelete], isFalse()); +} + +- (void)testDeleteLogsByBatchIdWithOnlyOnePendingBatch { + + // If + __block NSString *batchIdToDelete; + __block NSArray *expectedLogs; + NSString *condition; + NSArray *remainingLogs; + [self.sut.batches removeAllObjects]; + NSArray *savedLogs = [self generateAndSaveLogsWithCount:5 groupId:kMSACTestGroupId flags:MSACFlagsDefault andVerifyLogGeneration:YES]; + [self.sut loadLogsWithGroupId:kMSACTestGroupId + limit:2 + excludedTargetKeys:nil + completionHandler:^(NSArray *_Nonnull logArray, NSString *batchId) { + batchIdToDelete = batchId; + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"NOT (self IN %@)", logArray]; + expectedLogs = [savedLogs filteredArrayUsingPredicate:predicate]; + }]; + NSArray *logIdsToDelete = self.sut.batches[batchIdToDelete]; + MSACStorageBindableArray *array = [MSACStorageBindableArray new]; + for (NSNumber *item in logIdsToDelete) { + [array addNumber:item]; + } + + // When + [self.sut deleteLogsWithBatchId:batchIdToDelete groupId:kMSACTestGroupId]; + + // Then + remainingLogs = [self loadLogsWhere:nil withValues:nil]; + NSString *keyFormat = [self.sut buildKeyFormatWithCount:logIdsToDelete.count]; + condition = [NSString stringWithFormat:@"%@ IN %@", kMSACIdColumnName, keyFormat]; + assertThatInteger([self.sut countEntriesForTable:kMSACLogTableName condition:condition withValues:array], equalToInteger(0)); + assertThat(expectedLogs, is(remainingLogs)); + assertThatInteger(self.sut.batches.count, equalToInteger(0)); +} + +- (void)testDeleteLogsByBatchIdWithMultiplePendingBatches { + + // If + __block NSString *batchIdToDelete; + __block NSArray *expectedLogs; + NSString *condition; + NSArray *remainingLogs; + [self.sut.batches removeAllObjects]; + NSArray *savedLogs = [self generateAndSaveLogsWithCount:5 groupId:kMSACTestGroupId flags:MSACFlagsDefault andVerifyLogGeneration:YES]; + [self.sut loadLogsWithGroupId:kMSACTestGroupId + limit:2 + excludedTargetKeys:nil + completionHandler:^(NSArray *_Nonnull logArray, NSString *batchId) { + batchIdToDelete = batchId; + + // Intersect arrays to build expected remaining logs. + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"NOT (self IN %@)", logArray]; + expectedLogs = [savedLogs filteredArrayUsingPredicate:predicate]; + }]; + NSArray *logIdsToDelete = self.sut.batches[batchIdToDelete]; + MSACStorageBindableArray *array = [MSACStorageBindableArray new]; + for (NSNumber *item in logIdsToDelete) { + [array addNumber:item]; + } + + // Trigger another batch. + [self.sut loadLogsWithGroupId:kMSACTestGroupId limit:2 excludedTargetKeys:nil completionHandler:nil]; + + // When + [self.sut deleteLogsWithBatchId:batchIdToDelete groupId:kMSACTestGroupId]; + + // Then + remainingLogs = [self loadLogsWhere:nil withValues:nil]; + NSString *keyFormat = [self.sut buildKeyFormatWithCount:logIdsToDelete.count]; + condition = [NSString stringWithFormat:@"%@ IN %@", kMSACIdColumnName, keyFormat]; + assertThatInteger([self.sut countEntriesForTable:kMSACLogTableName condition:condition withValues:array], equalToInteger(0)); + assertThat(expectedLogs, is(remainingLogs)); + assertThatInteger(self.sut.batches.count, equalToInteger(1)); +} + +- (void)testDeleteLogsByBatchIdWithPendingBatchesFromOtherGroups { + + // If + __block NSString *batchIdToDelete; + __block NSMutableArray *expectedLogs; + NSString *condition; + NSArray *remainingLogs; + [self.sut.batches removeAllObjects]; + NSArray *savedLogs = [self generateAndSaveLogsWithCount:5 groupId:kMSACTestGroupId flags:MSACFlagsDefault andVerifyLogGeneration:YES]; + NSArray *savedLogsFromOtherGroup = [self generateAndSaveLogsWithCount:3 + groupId:kMSACAnotherTestGroupId + flags:MSACFlagsDefault + andVerifyLogGeneration:YES]; + [self.sut loadLogsWithGroupId:kMSACTestGroupId + limit:2 + excludedTargetKeys:nil + completionHandler:^(NSArray *_Nonnull logArray, NSString *batchId) { + batchIdToDelete = batchId; + + // Intersect arrays to build expected remaining logs. + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"NOT (self IN %@)", logArray]; + expectedLogs = [[savedLogs filteredArrayUsingPredicate:predicate] mutableCopy]; + + // Remaining logs should contains logs for other groups. + [expectedLogs addObjectsFromArray:savedLogsFromOtherGroup]; + }]; + NSArray *logIdsToDelete = self.sut.batches[batchIdToDelete]; + MSACStorageBindableArray *array = [MSACStorageBindableArray new]; + for (NSNumber *item in logIdsToDelete) { + [array addNumber:item]; + } + // Trigger another batch. + [self.sut loadLogsWithGroupId:kMSACAnotherTestGroupId limit:2 excludedTargetKeys:nil completionHandler:nil]; + + // When + [self.sut deleteLogsWithBatchId:batchIdToDelete groupId:kMSACTestGroupId]; + + // Then + remainingLogs = [self loadLogsWhere:nil withValues:nil]; + NSString *keyFormat = [self.sut buildKeyFormatWithCount:logIdsToDelete.count]; + condition = [NSString stringWithFormat:@"%@ IN %@", kMSACIdColumnName, keyFormat]; + assertThatInteger([self.sut countEntriesForTable:kMSACLogTableName condition:condition withValues:array], equalToInteger(0)); + assertThat(expectedLogs, is(remainingLogs)); + assertThatInteger(self.sut.batches.count, equalToInteger(1)); +} + +- (void)testCommonSchemaLogTargetTokenIsSavedAndRestored { + + // If + NSString *testTargetToken = @"testTargetToken"; + MSACCommonSchemaLog *log = [MSACCommonSchemaLog new]; + [log addTransmissionTargetToken:testTargetToken]; + + // When + [self.sut saveLog:log withGroupId:kMSACTestGroupId flags:MSACFlagsDefault]; + + // Then + [self.sut loadLogsWithGroupId:kMSACTestGroupId + limit:1 + excludedTargetKeys:nil + completionHandler:^(NSArray *_Nonnull logArray, __unused NSString *batchId) { + id restoredLog = logArray[0]; + NSString *restoredTargetToken = [[restoredLog transmissionTargetTokens] anyObject]; + assertThatInt([restoredLog transmissionTargetTokens].count, equalToInt(1)); + XCTAssertEqualObjects(testTargetToken, restoredTargetToken); + }]; +} + +- (void)testOnlyCommonSchemaLogTargetTokenIsSavedAndRestored { + + // If + NSString *testTargetToken = @"testTargetToken"; + MSACAbstractLog *log = [MSACAbstractLog new]; + [log addTransmissionTargetToken:testTargetToken]; + + // When + [self.sut saveLog:log withGroupId:kMSACTestGroupId flags:MSACFlagsDefault]; + + // Then + [self.sut loadLogsWithGroupId:kMSACTestGroupId + limit:1 + excludedTargetKeys:nil + completionHandler:^(NSArray *_Nonnull logArray, __unused NSString *batchId) { + assertThatInt([logArray[0] transmissionTargetTokens].count, equalToInt(0)); + }]; +} + +- (void)testDeleteLogsByBatchIdWithNoPendingBatches { + + // If + [self.sut.batches removeAllObjects]; + [self generateAndSaveLogsWithCount:5 groupId:kMSACTestGroupId flags:MSACFlagsDefault andVerifyLogGeneration:YES]; + + // When + [self.sut deleteLogsWithBatchId:MSAC_UUID_STRING groupId:kMSACTestGroupId]; + + // Then + assertThatInteger(self.sut.batches.count, equalToInteger(0)); + assertThatInteger([self.sut countEntriesForTable:kMSACLogTableName condition:nil withValues:nil], equalToInteger(5)); +} + +- (void)testAddLogsWhenBelowStorageCapacity { + + // If + long maxCapacityInBytes = kMSACTestStorageSizeMinimumUpperLimitInBytes + 4 * 1024; + long initialDataLengthInBytes = maxCapacityInBytes - 12 * 1024; + MSACAbstractLog *additionalLog = [MSACAbstractLog new]; + additionalLog.sid = MSAC_UUID_STRING; + NSArray *addedDbIds = [self fillDatabaseWithLogsOfSizeInBytes:initialDataLengthInBytes ofPriority:MSACFlagsNormal]; + + // When + [self.sut setMaxStorageSize:maxCapacityInBytes + completionHandler:^(__unused BOOL success){ + }]; + + // Then + BOOL logSavedSuccessfully = [self.sut saveLog:additionalLog withGroupId:kMSACAnotherTestGroupId flags:MSACFlagsDefault]; + + // Then + XCTAssertTrue(logSavedSuccessfully); + NSString *whereCondition = [NSString stringWithFormat:@"\"%@\" = ?", kMSACGroupIdColumnName]; + MSACStorageBindableArray *values = [MSACStorageBindableArray new]; + [values addString:kMSACAnotherTestGroupId]; + NSArray> *loadedLogs = [self loadLogsWhere:whereCondition withValues:values]; + NSArray> *allLogs = [self loadLogsWhere:nil withValues:nil]; + XCTAssertEqual(loadedLogs.count, 1); + XCTAssertEqualObjects(loadedLogs[0].sid, additionalLog.sid); + XCTAssertEqual(addedDbIds.count + 1, allLogs.count); +} + +- (void)testAddCriticalLog { + + // If + MSACAbstractLog *aLog = [MSACAbstractLog new]; + aLog.sid = MSAC_UUID_STRING; + NSString *criticalLogsFilter = [NSString stringWithFormat:@"\"%@\" = ?", kMSACPriorityColumnName]; + NSString *normalLogsFilter = [NSString stringWithFormat:@"\"%@\" = ?", kMSACPriorityColumnName]; + + // When + [self.sut saveLog:aLog withGroupId:kMSACTestGroupId flags:MSACFlagsCritical]; + + // Then + MSACStorageBindableArray *values = [MSACStorageBindableArray new]; + [values addNumber:@((unsigned int)MSACFlagsCritical)]; + NSArray> *criticalLogs = [self loadLogsWhere:criticalLogsFilter withValues:values]; + + values = [MSACStorageBindableArray new]; + [values addNumber:@((unsigned int)MSACFlagsNormal)]; + NSArray> *normalLogs = [self loadLogsWhere:normalLogsFilter withValues:values]; + XCTAssertEqual(criticalLogs.count, 1); + XCTAssertEqualObjects(criticalLogs[0].sid, aLog.sid); + XCTAssertEqual(normalLogs.count, 0); +} + +- (void)testAddNormalLog { + + // If + MSACAbstractLog *aLog = [MSACAbstractLog new]; + aLog.sid = MSAC_UUID_STRING; + NSString *criticalLogsFilter = [NSString stringWithFormat:@"\"%@\" = ?", kMSACPriorityColumnName]; + NSString *normalLogsFilter = [NSString stringWithFormat:@"\"%@\" = ?", kMSACPriorityColumnName]; + + // When + [self.sut saveLog:aLog withGroupId:kMSACTestGroupId flags:MSACFlagsNormal]; + + // Then + MSACStorageBindableArray *values = [MSACStorageBindableArray new]; + [values addNumber:@((unsigned int)MSACFlagsCritical)]; + NSArray> *criticalLogs = [self loadLogsWhere:criticalLogsFilter withValues:values]; + + values = [MSACStorageBindableArray new]; + [values addNumber:@((unsigned int)MSACFlagsNormal)]; + NSArray> *normalLogs = [self loadLogsWhere:normalLogsFilter withValues:values]; + XCTAssertEqual(normalLogs.count, 1); + XCTAssertEqualObjects(normalLogs[0].sid, aLog.sid); + XCTAssertEqual(criticalLogs.count, 0); +} + +- (void)testAddLogsDoesNotExceedCapacity { + + // If + long maxCapacityInBytes = kMSACTestStorageSizeMinimumUpperLimitInBytes; + [self fillDatabaseWithLogsOfSizeInBytes:maxCapacityInBytes ofPriority:MSACFlagsNormal]; + [self.sut setMaxStorageSize:maxCapacityInBytes + completionHandler:^(__unused BOOL success){ + }]; + + // When + int additionalLogs = 0; + while (additionalLogs <= 50) { + MSACAbstractLog *additionalLog = [MSACAbstractLog new]; + BOOL logSavedSuccessfully = [self.sut saveLog:additionalLog withGroupId:kMSACTestGroupId flags:MSACFlagsDefault]; + ++additionalLogs; + + // Then + XCTAssertTrue([self.storageTestUtil getDataLengthInBytes] <= maxCapacityInBytes); + XCTAssertTrue(logSavedSuccessfully); + } +} + +- (void)testSaveNormalPriorityLogPurgesOldestNormalPriorityLogsWhenStorageFull { + + // If + long maxCapacityInBytes = kMSACTestStorageSizeMinimumUpperLimitInBytes + 4 * 1024; + NSArray *addedDbIds = [self fillDatabaseWithLogsOfSizeInBytes:maxCapacityInBytes ofPriority:MSACFlagsNormal]; + NSNumber *firstLogDbId = addedDbIds[0]; + + // When + [self.sut setMaxStorageSize:maxCapacityInBytes + completionHandler:^(__unused BOOL success){ + }]; + MSACAbstractLog *additionalLog = [MSACAbstractLog new]; + BOOL logSavedSuccessfully = [self.sut saveLog:additionalLog withGroupId:kMSACAnotherTestGroupId flags:MSACFlagsNormal]; + + // Then + XCTAssertTrue([self.storageTestUtil getDataLengthInBytes] <= maxCapacityInBytes); + XCTAssertTrue(logSavedSuccessfully); + XCTAssertFalse([self containsLogWithDbId:firstLogDbId]); + + NSString *whereCondition = [NSString stringWithFormat:@"\"%@\" = ?", kMSACGroupIdColumnName]; + MSACStorageBindableArray *values = [MSACStorageBindableArray new]; + [values addString:kMSACAnotherTestGroupId]; + NSArray> *loadedLogs = [self loadLogsWhere:whereCondition withValues:values]; + XCTAssertEqual(loadedLogs.count, 1); + XCTAssertEqualObjects(loadedLogs[0].sid, additionalLog.sid); + XCTAssertEqual(1, [self findUnknownDBIdsFromKnownIdList:addedDbIds].count); +} + +- (void)testSaveCriticalPriorityLogPurgesOldestNormalPriorityLogsWhenStorageFull { + + // If + long maxCapacityInBytes = kMSACTestStorageSizeMinimumUpperLimitInBytes + 4 * 1024; + NSArray *addedDbIds = [self fillDatabaseWithLogsOfSizeInBytes:maxCapacityInBytes ofPriority:MSACFlagsNormal]; + NSNumber *firstLogDbId = addedDbIds[0]; + + // When + [self.sut setMaxStorageSize:maxCapacityInBytes + completionHandler:^(__unused BOOL success){ + }]; + MSACAbstractLog *additionalLog = [MSACAbstractLog new]; + BOOL logSavedSuccessfully = [self.sut saveLog:additionalLog withGroupId:kMSACAnotherTestGroupId flags:MSACFlagsCritical]; + + // Then + XCTAssertTrue([self.storageTestUtil getDataLengthInBytes] <= maxCapacityInBytes); + XCTAssertTrue(logSavedSuccessfully); + XCTAssertFalse([self containsLogWithDbId:firstLogDbId]); + + NSString *whereCondition = [NSString stringWithFormat:@"\"%@\" = ?", kMSACGroupIdColumnName]; + MSACStorageBindableArray *values = [MSACStorageBindableArray new]; + [values addString:kMSACAnotherTestGroupId]; + NSArray> *loadedLogs = [self loadLogsWhere:whereCondition withValues:values]; + XCTAssertEqual(loadedLogs.count, 1); + XCTAssertEqualObjects(loadedLogs[0].sid, additionalLog.sid); + XCTAssertEqual(1, [self findUnknownDBIdsFromKnownIdList:addedDbIds].count); +} + +- (void)testSaveNormalPriorityLogDiscardsLogWhenStorageFullWithCriticalPriorityLogs { + + // If + long maxCapacityInBytes = kMSACTestStorageSizeMinimumUpperLimitInBytes + 4 * 1024; + NSArray *addedDbIds = [self fillDatabaseWithLogsOfSizeInBytes:maxCapacityInBytes ofPriority:MSACFlagsCritical]; + + // When + [self.sut setMaxStorageSize:maxCapacityInBytes + completionHandler:^(__unused BOOL success){ + }]; + MSACAbstractLog *additionalLog = [MSACAbstractLog new]; + BOOL logSavedSuccessfully = [self.sut saveLog:additionalLog withGroupId:kMSACAnotherTestGroupId flags:MSACFlagsNormal]; + + // Then + XCTAssertTrue([self.storageTestUtil getDataLengthInBytes] <= maxCapacityInBytes); + XCTAssertFalse(logSavedSuccessfully); + for (NSNumber *dbId in addedDbIds) { + XCTAssertTrue([self containsLogWithDbId:dbId]); + } + + NSString *whereCondition = [NSString stringWithFormat:@"\"%@\" = ?", kMSACGroupIdColumnName]; + MSACStorageBindableArray *values = [MSACStorageBindableArray new]; + [values addString:kMSACAnotherTestGroupId]; + NSArray> *loadedLogs = [self loadLogsWhere:whereCondition withValues:values]; + XCTAssertEqual(loadedLogs.count, 0); + XCTAssertEqual(0, [self findUnknownDBIdsFromKnownIdList:addedDbIds].count); +} + +- (void)testSaveLogPurgesNormalPriorityLogWhenStorageFullWithMixedPriorityLogs { + + // If + long maxCapacityInBytes = kMSACTestStorageSizeMinimumUpperLimitInBytes + 4 * 1024; + NSDictionary *addedDbIds = [self fillDatabaseWithMixedPriorityLogsOfSizeInBytesAndReturnDbIds:maxCapacityInBytes]; + NSNumber *oldestCriticalDbId = [((NSArray *)[addedDbIds objectForKey:[NSNumber numberWithInt:MSACFlagsCritical]]) firstObject]; + NSNumber *oldestNormalDbId = [((NSArray *)[addedDbIds objectForKey:[NSNumber numberWithInt:MSACFlagsNormal]]) firstObject]; + + // When + [self.sut setMaxStorageSize:maxCapacityInBytes + completionHandler:^(__unused BOOL success){ + }]; + MSACAbstractLog *additionalLog = [MSACAbstractLog new]; + BOOL logSavedSuccessfully = [self.sut saveLog:additionalLog withGroupId:kMSACAnotherTestGroupId flags:MSACFlagsNormal]; + + // Then + XCTAssertTrue([self.storageTestUtil getDataLengthInBytes] <= maxCapacityInBytes); + XCTAssertTrue(logSavedSuccessfully); + XCTAssertFalse([self containsLogWithDbId:oldestNormalDbId]); + XCTAssertTrue([self containsLogWithDbId:oldestCriticalDbId]); + NSString *whereCondition = [NSString stringWithFormat:@"\"%@\" = ?", kMSACGroupIdColumnName]; + MSACStorageBindableArray *values = [MSACStorageBindableArray new]; + [values addString:kMSACAnotherTestGroupId]; + NSArray> *loadedLogs = [self loadLogsWhere:whereCondition withValues:values]; + XCTAssertEqual(loadedLogs.count, 1); + XCTAssertEqualObjects(loadedLogs[0].sid, additionalLog.sid); + NSArray *knownIds = [NSArray new]; + for (NSArray *ids in [addedDbIds allValues]) { + knownIds = [knownIds arrayByAddingObjectsFromArray:ids]; + } + XCTAssertEqual(1, [self findUnknownDBIdsFromKnownIdList:knownIds].count); +} + +- (void)testSaveLargeNormalPriorityLogDoesNotPurgeOldLogs { + [self DoNotPurgeOldLogsWhenSavingLargeLogExceedsCapacityWithPriority:MSACFlagsNormal]; +} + +- (void)testSaveLargeCriticalPriorityLogDoesNotPurgeOldLogs { + [self DoNotPurgeOldLogsWhenSavingLargeLogExceedsCapacityWithPriority:MSACFlagsCritical]; +} + +- (void)DoNotPurgeOldLogsWhenSavingLargeLogExceedsCapacityWithPriority:(MSACFlags)priority { + + // If + long maxCapacityInBytes = kMSACTestStorageSizeMinimumUpperLimitInBytes + 4 * 1024; + [self.sut setMaxStorageSize:maxCapacityInBytes + completionHandler:^(__unused BOOL success){ + }]; + [self generateAndSaveLogsWithCount:1 groupId:kMSACTestGroupId flags:MSACFlagsCritical andVerifyLogGeneration:YES]; + [self generateAndSaveLogsWithCount:2 groupId:kMSACTestGroupId flags:MSACFlagsNormal andVerifyLogGeneration:YES]; + id largeLog = [self generateLogWithSize:@(maxCapacityInBytes)]; + sqlite3 *db = [self.storageTestUtil openDatabase]; + NSArray *criticalDbIds = [self dbIdsForPriority:MSACFlagsCritical inOpenedDatabase:db]; + NSArray *normalDbIds = [self dbIdsForPriority:MSACFlagsNormal inOpenedDatabase:db]; + sqlite3_close(db); + + // When + BOOL logSavedSuccessfully = [self.sut saveLog:largeLog withGroupId:kMSACAnotherTestGroupId flags:priority]; + + // Then + XCTAssertTrue([self.storageTestUtil getDataLengthInBytes] <= maxCapacityInBytes); + XCTAssertFalse(logSavedSuccessfully); + NSString *whereCondition = [NSString stringWithFormat:@"\"%@\" = ?", kMSACGroupIdColumnName]; + MSACStorageBindableArray *values = [MSACStorageBindableArray new]; + [values addString:kMSACAnotherTestGroupId]; + NSArray> *loadedLogs = [self loadLogsWhere:whereCondition withValues:values]; + XCTAssertEqual(loadedLogs.count, 0); + NSArray *knownIds = [NSArray new]; + for (NSArray *ids in @[ criticalDbIds, normalDbIds ]) { + knownIds = [knownIds arrayByAddingObjectsFromArray:ids]; + } + XCTAssertEqual(0, [self findUnknownDBIdsFromKnownIdList:knownIds].count); + for (NSNumber *dbId in normalDbIds) { + XCTAssertTrue([self containsLogWithDbId:dbId]); + } + XCTAssertTrue([self containsLogWithDbId:[criticalDbIds firstObject]]); +} + +- (void)testErrorDeletingOldestLog { + + // If + id classMock = OCMClassMock([MSACDBStorage class]); + OCMStub([classMock executeNonSelectionQuery:startsWith(@"INSERT") inOpenedDatabase:[OCMArg anyPointer] withValues:OCMOCK_ANY]) + .andReturn(SQLITE_FULL); + OCMStub([classMock executeNonSelectionQuery:startsWith(@"DELETE") inOpenedDatabase:[OCMArg anyPointer] withValues:OCMOCK_ANY]) + .andReturn(SQLITE_ERROR); + + // When + MSACAbstractLog *additionalLog = [MSACAbstractLog new]; + BOOL logSavedSuccessfully = [self.sut saveLog:additionalLog withGroupId:kMSACAnotherTestGroupId flags:MSACFlagsDefault]; + + // Then + XCTAssertFalse(logSavedSuccessfully); + [classMock stopMocking]; +} + +- (void)testCreateFromLatestSchema { + + // When + [self.storageTestUtil deleteDatabase]; + self.sut = [MSACLogDBStorage new]; + + // Then + NSString *currentTable = + [self.sut executeSelectionQuery:[NSString stringWithFormat:@"SELECT sql FROM sqlite_master WHERE name='%@'", kMSACLogTableName] + withValues:nil][0][0]; + assertThat(currentTable, is(kMSACLatestSchema)); + NSString *priorityIndex = + [self.sut executeSelectionQuery:[NSString stringWithFormat:@"SELECT sql FROM sqlite_master WHERE name='ix_%@_%@'", kMSACLogTableName, + kMSACPriorityColumnName] + withValues:nil][0][0]; + assertThat(priorityIndex, is(@"CREATE INDEX \"ix_logs_priority\" ON \"logs\" (\"priority\")")); +} + +- (void)testMigrationToLatest { + + // If + // Create old version db. + // DO NOT CHANGE. THIS IS ALREADY PUBLISHED SCHEMA. + MSACDBSchema *schema0 = @{ + kMSACLogTableName : @[ + @{kMSACIdColumnName : @[ kMSACSQLiteTypeInteger, kMSACSQLiteConstraintPrimaryKey, kMSACSQLiteConstraintAutoincrement ]}, + @{kMSACGroupIdColumnName : @[ kMSACSQLiteTypeText, kMSACSQLiteConstraintNotNull ]}, + @{kMSACLogColumnName : @[ kMSACSQLiteTypeText, kMSACSQLiteConstraintNotNull ]} + ] + }; + MSACDBStorage *storage0 = [[MSACDBStorage alloc] initWithSchema:schema0 version:0 filename:kMSACDBFileName]; + [self generateAndSaveLogsWithCount:10 + size:nil + groupId:kMSACTestGroupId + flags:MSACFlagsDefault + storage:storage0 + andVerifyLogGeneration:YES]; + + // When + self.sut = [MSACLogDBStorage new]; + + // Then + // Migration to version 5 we drop the table and re-create, so we expect 0. + assertThatInt([self loadLogsWhere:nil withValues:nil].count, equalToUnsignedInt(0)); +} + +#pragma mark - Helper methods + +- (id)generateLogWithSize:(NSNumber *)size { + MSACLogWithProperties *log = [MSACLogWithProperties new]; + if (size) { + NSString *s = [@"" stringByPaddingToLength:[size unsignedIntegerValue] withString:@"." startingAtIndex:0]; + log.properties = [NSMutableDictionary new]; + [log.properties setValue:s forKey:@"s"]; + } + log.sid = MSAC_UUID_STRING; + return log; +} + +- (NSArray> *)generateAndSaveLogsWithCount:(NSUInteger)count + groupId:(NSString *)groupId + flags:(MSACFlags)flags + andVerifyLogGeneration:(BOOL)verify { + return [self generateAndSaveLogsWithCount:count size:nil groupId:groupId flags:flags storage:self.sut andVerifyLogGeneration:verify]; +} + +- (NSArray> *)generateAndSaveLogsWithCount:(NSUInteger)count + size:(NSNumber *)size + groupId:(NSString *)groupId + flags:(MSACFlags)flags + andVerifyLogGeneration:(BOOL)verify { + return [self generateAndSaveLogsWithCount:count size:size groupId:groupId flags:flags storage:self.sut andVerifyLogGeneration:verify]; +} + +- (NSArray> *)generateAndSaveLogsWithCount:(NSUInteger)count + size:(NSNumber *)size + groupId:(NSString *)groupId + flags:(MSACFlags)flags + storage:(MSACDBStorage *)storage + andVerifyLogGeneration:(BOOL)verify { + NSMutableArray> *logs = [NSMutableArray arrayWithCapacity:count]; + NSUInteger trueLogCount; + for (NSUInteger i = 0; i < count; ++i) { + id log = [self generateLogWithSize:size]; + NSString *base64Data = [[MSACUtility archiveKeyedData:log] base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed]; + NSString *addLogQuery = [NSString stringWithFormat:@"INSERT INTO \"%@\" (\"%@\", \"%@\", \"%@\") VALUES (?, ?, ?)", kMSACLogTableName, + kMSACGroupIdColumnName, kMSACLogColumnName, kMSACPriorityColumnName]; + + MSACStorageBindableArray *values = [MSACStorageBindableArray new]; + [values addString:groupId]; + [values addString:base64Data]; + [values addNumber:@((unsigned int)flags)]; + [storage executeNonSelectionQuery:addLogQuery withValues:values]; + [logs addObject:log]; + } + + if (verify) { + + // Check the insertion worked. + MSACStorageBindableArray *values = [MSACStorageBindableArray new]; + [values addNumber:@((unsigned int)flags)]; + trueLogCount = [storage countEntriesForTable:kMSACLogTableName + condition:[NSString stringWithFormat:@"\"%@\" = '%@' AND \"%@\" = ?", kMSACGroupIdColumnName, + groupId, kMSACPriorityColumnName] + withValues:values]; + assertThatUnsignedInteger(trueLogCount, equalToUnsignedInteger(count)); + } + return logs; +} + +- (NSArray *)dbIdsForPriority:(MSACFlags)flags inOpenedDatabase:(void *)db { + NSString *selectLogQuery = [NSString stringWithFormat:@"SELECT \"%@\" FROM \"%@\" WHERE \"%@\" = ? ORDER BY \"%@\" ASC", + kMSACIdColumnName, kMSACLogTableName, kMSACPriorityColumnName, kMSACIdColumnName]; + MSACStorageBindableArray *values = [MSACStorageBindableArray new]; + [values addNumber:@((unsigned int)flags)]; + NSArray *entries = [MSACDBStorage executeSelectionQuery:selectLogQuery inOpenedDatabase:db withValues:values]; + NSMutableArray *ids = [NSMutableArray new]; + for (NSMutableArray *row in entries) { + [ids addObject:row[0]]; + } + return ids; +} + +- (NSArray> *)loadLogsWhere:(nullable NSString *)whereCondition withValues:(nullable MSACStorageBindableArray *)values { + NSMutableArray> *logs = [NSMutableArray> new]; + NSMutableArray *rows = [NSMutableArray new]; + NSMutableString *selectLogQuery = [NSMutableString stringWithFormat:@"SELECT * FROM \"%@\"", kMSACLogTableName]; + if (whereCondition.length > 0) { + [selectLogQuery appendFormat:@" WHERE %@", whereCondition]; + } + sqlite3 *db = [self.storageTestUtil openDatabase]; + sqlite3_stmt *statement = NULL; + sqlite3_prepare_v2(db, [selectLogQuery UTF8String], -1, &statement, NULL); + [values bindAllValuesWithStatement:statement inOpenedDatabase:db]; + + // Loop on rows. + while (sqlite3_step(statement) == SQLITE_ROW) { + NSMutableArray *entry = [NSMutableArray new]; + for (int i = 0; i < sqlite3_column_count(statement); i++) { + id value = nil; + switch (sqlite3_column_type(statement, i)) { + case SQLITE_INTEGER: + value = @(sqlite3_column_int(statement, i)); + break; + case SQLITE_TEXT: + value = [NSString stringWithUTF8String:(const char *)sqlite3_column_text(statement, i)]; + break; + default: + value = [NSNull null]; + break; + } + [entry addObject:value]; + } + if (entry.count > 0) { + [rows addObject:entry]; + } + } + sqlite3_finalize(statement); + for (NSArray *row in rows) { + NSString *base64Data = row[2]; + NSData *logData = [[NSData alloc] initWithBase64EncodedString:base64Data options:NSDataBase64DecodingIgnoreUnknownCharacters]; + id log = (id)[MSACUtility unarchiveKeyedData:logData]; + [logs addObject:log]; + } + sqlite3_close(db); + return logs; +} + +- (NSArray *)fillDatabaseWithLogsOfSizeInBytes:(long)sizeInBytes ofPriority:(MSACFlags)priority { + int result = 0; + sqlite3 *db = [self.storageTestUtil openDatabase]; + sqlite3_stmt *statement = NULL; + sqlite3_prepare_v2(db, "PRAGMA page_size;", -1, &statement, NULL); + sqlite3_step(statement); + int pageSize = sqlite3_column_int(statement, 0); + sqlite3_finalize(statement); + long maxPageCount = sizeInBytes / pageSize; + sqlite3_exec(db, [[NSString stringWithFormat:@"PRAGMA max_page_count = %ld;", maxPageCount] UTF8String], NULL, NULL, NULL); + do { + MSACAbstractLog *log = [MSACAbstractLog new]; + NSString *base64Data = [[MSACUtility archiveKeyedData:log] base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed]; + NSString *addLogQuery = [NSString stringWithFormat:@"INSERT INTO \"%@\" (\"%@\", \"%@\", \"%@\") VALUES ('%@', '%@', %u)", + kMSACLogTableName, kMSACGroupIdColumnName, kMSACLogColumnName, + kMSACPriorityColumnName, kMSACTestGroupId, base64Data, (unsigned int)priority]; + result = sqlite3_exec(db, [addLogQuery UTF8String], NULL, NULL, NULL); + } while (result == SQLITE_OK); + + // Get DB IDs for logs + NSString *selectLogQuery = + [NSString stringWithFormat:@"SELECT \"%@\" FROM \"%@\" ORDER BY \"%@\" ASC", kMSACIdColumnName, kMSACLogTableName, kMSACIdColumnName]; + NSArray *entries = [MSACDBStorage executeSelectionQuery:selectLogQuery inOpenedDatabase:db withValues:nil]; + NSMutableArray *ids = [NSMutableArray new]; + for (NSMutableArray *row in entries) { + [ids addObject:row[0]]; + } + sqlite3_close(db); + + return ids; +} + +- (NSDictionary *> *)fillDatabaseWithMixedPriorityLogsOfSizeInBytesAndReturnDbIds:(long)sizeInBytes { + int result = 0, count = 0; + sqlite3 *db = [self.storageTestUtil openDatabase]; + sqlite3_stmt *statement = NULL; + sqlite3_prepare_v2(db, "PRAGMA page_size;", -1, &statement, NULL); + sqlite3_step(statement); + int pageSize = sqlite3_column_int(statement, 0); + sqlite3_finalize(statement); + long maxPageCount = sizeInBytes / pageSize; + sqlite3_exec(db, [[NSString stringWithFormat:@"PRAGMA max_page_count = %ld;", maxPageCount] UTF8String], NULL, NULL, NULL); + do { + MSACAbstractLog *log = [MSACAbstractLog new]; + NSString *base64Data = [[MSACUtility archiveKeyedData:log] base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed]; + NSString *addLogQuery = + [NSString stringWithFormat:@"INSERT INTO \"%@\" (\"%@\", \"%@\", \"%@\") VALUES ('%@', '%@', %u)", kMSACLogTableName, + kMSACGroupIdColumnName, kMSACLogColumnName, kMSACPriorityColumnName, kMSACTestGroupId, base64Data, + (unsigned int)(count++ % 2 == 0 ? MSACFlagsCritical : MSACFlagsNormal)]; + result = sqlite3_exec(db, [addLogQuery UTF8String], NULL, NULL, NULL); + } while (result == SQLITE_OK); + + // Get DB IDs for logs + NSMutableDictionary *ids = [NSMutableDictionary new]; + for (NSNumber *flag in @[ [NSNumber numberWithInt:MSACFlagsNormal], [NSNumber numberWithInt:MSACFlagsCritical] ]) { + NSString *selectLogQuery = [NSString stringWithFormat:@"SELECT \"%@\" FROM \"%@\" WHERE \"%@\" = ? ORDER BY \"%@\" ASC", + kMSACIdColumnName, kMSACLogTableName, kMSACPriorityColumnName, kMSACIdColumnName]; + + MSACStorageBindableArray *values = [MSACStorageBindableArray new]; + [values addNumber:@([flag unsignedIntValue])]; + NSArray *entries = [MSACDBStorage executeSelectionQuery:selectLogQuery inOpenedDatabase:db withValues:values]; + NSMutableArray *priorityIds = [NSMutableArray new]; + for (NSMutableArray *row in entries) { + [priorityIds addObject:row[0]]; + } + [ids setObject:priorityIds forKey:flag]; + } + sqlite3_close(db); + + return ids; +} + +- (BOOL)containsLogWithDbId:(NSNumber *)dbId { + sqlite3 *db = [self.storageTestUtil openDatabase]; + NSString *selectLogQuery = + [NSString stringWithFormat:@"SELECT COUNT(*) FROM \"%@\" WHERE \"%@\" = ?", kMSACLogTableName, kMSACIdColumnName]; + MSACStorageBindableArray *values = [MSACStorageBindableArray new]; + [values addNumber:dbId]; + NSArray *> *entries = [MSACDBStorage executeSelectionQuery:selectLogQuery inOpenedDatabase:db withValues:values]; + if (entries.count > 0) { + return entries[0][0].unsignedIntegerValue > 0; + } + return NO; +} + +- (NSArray *)findUnknownDBIdsFromKnownIdList:(NSArray *)idList { + sqlite3 *db = [self.storageTestUtil openDatabase]; + NSString *keyFormat = [self.sut buildKeyFormatWithCount:idList.count]; + NSString *selectLogQuery = [NSString stringWithFormat:@"SELECT \"%@\" FROM \"%@\" WHERE \"%@\" NOT IN %@", kMSACIdColumnName, + kMSACLogTableName, kMSACIdColumnName, keyFormat]; + + MSACStorageBindableArray *values = [MSACStorageBindableArray new]; + + for (NSNumber *item in idList) { + [values addNumber:item]; + } + NSArray *> *entries = [MSACDBStorage executeSelectionQuery:selectLogQuery inOpenedDatabase:db withValues:values]; + if (entries.count > 0) { + return entries[0]; + } + return nil; +} + +- (void)validateQuerySyntax:(NSString *)query { + sqlite3 *db = [self.storageTestUtil openDatabase]; + NSString *statement = [NSString stringWithFormat:@"EXPLAIN %@", query]; + char *error; + int result = sqlite3_exec(db, [statement UTF8String], NULL, NULL, &error); + XCTAssert(result == SQLITE_OK, "%s", error); + sqlite3_close(db); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACLogWithPropertiesTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACLogWithPropertiesTests.m new file mode 100644 index 0000000000..7655b89165 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACLogWithPropertiesTests.m @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAbstractLogInternal.h" +#import "MSACLogWithProperties.h" +#import "MSACTestFrameworks.h" +#import "MSACUtility.h" + +@interface MSACLogWithPropertiesTests : XCTestCase + +@property(nonatomic) MSACLogWithProperties *sut; + +@end + +@implementation MSACLogWithPropertiesTests + +#pragma mark - Housekeeping + +- (void)setUp { + [super setUp]; + self.sut = [MSACLogWithProperties new]; +} + +- (void)tearDown { + [super tearDown]; +} + +#pragma mark - Tests + +- (void)testSerializingDeviceToDictionaryWorks { + + // If + NSDictionary *properties = @{@"key1" : @"value1", @"key2" : @"value"}; + self.sut.properties = properties; + + // When + NSMutableDictionary *actual = [self.sut serializeToDictionary]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual[@"properties"], equalTo(properties)); +} + +- (void)testNSCodingSerializationAndDeserializationWorks { + + // If + NSDictionary *properties = @{@"key1" : @"value1", @"key2" : @"value"}; + self.sut.properties = properties; + + // When + NSData *serializedEvent = [MSACUtility archiveKeyedData:self.sut]; + id actual = [MSACUtility unarchiveKeyedData:serializedEvent]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual, instanceOf([MSACLogWithProperties class])); + + MSACLogWithProperties *actualLogWithProperties = actual; + assertThat(actualLogWithProperties.properties, equalTo(properties)); +} + +- (void)testIsEqual { + + // If + NSDictionary *properties = @{@"key1" : @"value1", @"key2" : @"value"}; + self.sut.properties = properties; + + // When + NSData *serializedEvent = [MSACUtility archiveKeyedData:self.sut]; + id actual = [MSACUtility unarchiveKeyedData:serializedEvent]; + MSACLogWithProperties *actualLogWithProperties = actual; + + // then + XCTAssertTrue([self.sut.properties isEqual:actualLogWithProperties.properties]); +} + +- (void)testIsNotEqualToNil { + + // Then + XCTAssertFalse([self.sut isEqual:nil]); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACLoggerTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACLoggerTests.m new file mode 100644 index 0000000000..0a401a45d3 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACLoggerTests.m @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAppCenter.h" +#import "MSACAppCenterInternal.h" +#import "MSACAppCenterPrivate.h" +#import "MSACChannelGroupDefault.h" +#import "MSACLoggerInternal.h" +#import "MSACTestFrameworks.h" + +@interface MSACLoggerTests : XCTestCase + +@end + +@implementation MSACLoggerTests + +- (void)setUp { + [super setUp]; + + [MSACLogger setCurrentLogLevel:MSACLogLevelAssert]; + [MSACLogger setIsUserDefinedLogLevel:NO]; +} + +- (void)testDefaultLogLevels { + + // If + // Mock channels to avoid background activity. + id channelGroupMock = OCMClassMock([MSACChannelGroupDefault class]); + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([channelGroupMock alloc]).andReturn(channelGroupMock); + OCMStub([channelGroupMock initWithHttpClient:OCMOCK_ANY installId:OCMOCK_ANY logUrl:OCMOCK_ANY]).andReturn(channelGroupMock); + OCMStub([channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY]).andReturn(channelUnitMock); + + // Check default loglevel before MSACAppCenter was started. + XCTAssertTrue([MSACLogger currentLogLevel] == MSACLogLevelAssert); + + // Need to set sdkConfigured to NO to make sure the start-logic goes through once, otherwise this test will fail randomly as other tests + // might call start:withServices, too. + [MSACAppCenter resetSharedInstance]; + [MSACAppCenter sharedInstance].sdkConfigured = NO; + [MSACAppCenter start:MSAC_UUID_STRING withServices:nil]; + + // Then + XCTAssertTrue([MSACLogger currentLogLevel] == MSACLogLevelWarning); + + // Clear + [channelGroupMock stopMocking]; +} + +- (void)testSetLoglevels { + + // Check isUserDefinedLogLevel + XCTAssertFalse([MSACLogger isUserDefinedLogLevel]); + [MSACLogger setCurrentLogLevel:MSACLogLevelVerbose]; + XCTAssertTrue([MSACLogger isUserDefinedLogLevel]); +} + +- (void)testSetCurrentLoglevelWorks { + [MSACLogger setCurrentLogLevel:MSACLogLevelWarning]; + XCTAssertTrue([MSACLogger currentLogLevel] == MSACLogLevelWarning); +} + +- (void)testLoglevelNoneDoesNotLogMessages { + + // If + MSACLogMessageProvider messageProvider = ^() { + // Then + XCTFail(@"Log shouldn't be printed."); + return @""; + }; + + // When + [MSACLogger setCurrentLogLevel:MSACLogLevelNone]; + [MSACLogger logMessage:messageProvider level:MSACLogLevelNone tag:@"TAG" file:nil function:nil line:0]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACLongTypedPropertyTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACLongTypedPropertyTests.m new file mode 100644 index 0000000000..114a439f92 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACLongTypedPropertyTests.m @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACLongTypedProperty.h" +#import "MSACTestFrameworks.h" +#import "MSACUtility.h" + +@interface MSACLongTypedPropertyTests : XCTestCase + +@end + +@implementation MSACLongTypedPropertyTests + +- (void)testNSCodingSerializationAndDeserialization { + + // If + MSACLongTypedProperty *sut = [MSACLongTypedProperty new]; + sut.type = @"type"; + sut.name = @"name"; + sut.value = 12; + + // When + NSData *serializedProperty = [MSACUtility archiveKeyedData:sut]; + MSACLongTypedProperty *actual = (MSACLongTypedProperty *)[MSACUtility unarchiveKeyedData:serializedProperty]; + + // Then + XCTAssertNotNil(actual); + XCTAssertTrue([actual isKindOfClass:[MSACLongTypedProperty class]]); + XCTAssertEqualObjects(actual.name, sut.name); + XCTAssertEqualObjects(actual.type, sut.type); + XCTAssertEqual(actual.value, sut.value); +} + +- (void)testSerializeToDictionary { + + // If + MSACLongTypedProperty *sut = [MSACLongTypedProperty new]; + sut.name = @"propertyName"; + sut.value = 12; + + // When + NSDictionary *dictionary = [sut serializeToDictionary]; + + // Then + XCTAssertEqualObjects(dictionary[@"type"], sut.type); + XCTAssertEqualObjects(dictionary[@"name"], sut.name); + XCTAssertEqual([dictionary[@"value"] longLongValue], sut.value); +} + +- (void)testPropertyTypeIsCorrectWhenPropertyIsInitialized { + + // If + MSACLongTypedProperty *sut = [MSACLongTypedProperty new]; + + // Then + XCTAssertEqualObjects(sut.type, @"long"); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACModelTestsUtililty.h b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACModelTestsUtililty.h new file mode 100644 index 0000000000..c741a8915a --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACModelTestsUtililty.h @@ -0,0 +1,232 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAbstractLogInternal.h" +#import "MSACAbstractLogPrivate.h" +#import "MSACDevice.h" + +@class MSACMetadataExtension; +@class MSACUserExtension; +@class MSACLocExtension; +@class MSACOSExtension; +@class MSACAppExtension; +@class MSACProtocolExtension; +@class MSACNetExtension; +@class MSACSDKExtension; +@class MSACDeviceExtension; + +@interface MSACModelTestsUtililty : NSObject + +/** + * Get dummy values for device model. + * + * @return Dummy values for device model. + */ ++ (NSDictionary *)deviceDummies; + +/** + * Get dummy values for common schema extensions. + * + * @return Dummy values for common schema extensions. + */ ++ (NSMutableDictionary *)extensionDummies; + +/** + * Get dummy values for common schema metadata extensions. + * + * @return Dummy values for common schema metadata extensions. + */ ++ (NSDictionary *)metadataExtensionDummies; + +/** + * Get dummy values for common schema user extensions. + * + * @return Dummy values for common schema user extensions. + */ ++ (NSDictionary *)userExtensionDummies; + +/** + * Get dummy values for common schema location extensions. + * + * @return Dummy values for common schema location extensions. + */ ++ (NSDictionary *)locExtensionDummies; + +/** + * Get dummy values for common schema os extensions. + * + * @return Dummy values for common schema os extensions. + */ ++ (NSDictionary *)osExtensionDummies; + +/** + * Get dummy values for common schema app extensions. + * + * @return Dummy values for common schema app extensions. + */ ++ (NSDictionary *)appExtensionDummies; + +/** + * Get dummy values for common schema protocol extensions. + * + * @return Dummy values for common schema protocol extensions. + */ ++ (NSDictionary *)protocolExtensionDummies; + +/** + * Get dummy values for common schema network extensions. + * + * @return Dummy values for common schema network extensions. + */ ++ (NSDictionary *)netExtensionDummies; + +/** + * Get dummy values for common schema sdk extensions. + * + * @return Dummy values for common schema sdk extensions. + */ ++ (NSMutableDictionary *)sdkExtensionDummies; + +/** + * Get dummy values for common schema sdk extensions. + * + * @return Dummy values for common schema device extensions. + */ ++ (NSMutableDictionary *)deviceExtensionDummies; + +/** + * Get ordered dummy values data, e.g. properties. + * + * @return Ordered dummy values data, e.g. properties. + */ ++ (NSDictionary *)orderedDataDummies; + +/** + * Get unordered dummy values data, e.g. properties. + * + * @return Unordered dummy values data, e.g. properties. + */ ++ (NSDictionary *)unorderedDataDummies; + +/** + * Get dummy values for abstract log. + * + * @return Dummy values for abstract log. + */ ++ (NSDictionary *)abstractLogDummies; + +/** + * Get a dummy device model. + * + * @return A dummy device model. + */ ++ (MSACDevice *)dummyDevice; + +/** + * Populate dummy common schema extensions. + * + * @param dummyValues Dummy values to create the extension. + * + * @return The dummy common schema extensions. + */ ++ (MSACCSExtensions *)extensionsWithDummyValues:(NSDictionary *)dummyValues; + +/** + * Populate a dummy common schema user extension. + * + * @param dummyValues Dummy values to create the extension. + * + * @return A dummy common schema user extension. + */ ++ (MSACMetadataExtension *)metadataExtensionWithDummyValues:(NSDictionary *)dummyValues; + +/** + * Populate a dummy common schema user extension. + * + * @param dummyValues Dummy values to create the extension. + * + * @return A dummy common schema user extension. + */ ++ (MSACUserExtension *)userExtensionWithDummyValues:(NSDictionary *)dummyValues; + +/** + * Populate a dummy common schema location extension. + * + * @param dummyValues Dummy values to create the extension. + * + * @return A dummy common schema location extension. + */ ++ (MSACLocExtension *)locExtensionWithDummyValues:(NSDictionary *)dummyValues; + +/** + * Populate a dummy common schema os extension. + * + * @param dummyValues Dummy values to create the extension. + * + * @return A dummy common schema os extension. + */ ++ (MSACOSExtension *)osExtensionWithDummyValues:(NSDictionary *)dummyValues; +/** + * Populate a dummy common schema app extension. + * + * @param dummyValues Dummy values to create the extension. + * + * @return A dummy common schema app extension. + */ ++ (MSACAppExtension *)appExtensionWithDummyValues:(NSDictionary *)dummyValues; + +/** + * Populate a dummy common schema protocol extension. + * + * @param dummyValues Dummy values to create the extension. + * + * @return A dummy common schema protocol extension. + */ ++ (MSACProtocolExtension *)protocolExtensionWithDummyValues:(NSDictionary *)dummyValues; + +/** + * Populate a dummy common schema network extension. + * + * @param dummyValues Dummy values to create the extension. + * + * @return A dummy common schema network extension. + */ ++ (MSACNetExtension *)netExtensionWithDummyValues:(NSDictionary *)dummyValues; + +/** + * Populate a dummy common schema sdk extension. + * + * @param dummyValues Dummy values to create the extension. + * + * @return A dummy common schema sdk extension. + */ ++ (MSACSDKExtension *)sdkExtensionWithDummyValues:(NSDictionary *)dummyValues; + +/** + * Populate a dummy common schema device extension. + * + * @param dummyValues Dummy values to create the extension. + * + * @return A dummy common schema device extension. + */ ++ (MSACDeviceExtension *)deviceExtensionWithDummyValues:(NSDictionary *)dummyValues; + +/** + * Populate a dummy common schema data. + * + * @param dummyValues Dummy values to create the data. + * + * @return A dummy common schema data. + */ ++ (MSACCSData *)dataWithDummyValues:(NSDictionary *)dummyValues; + +/** + * Populate an abstract log with dummy values. + * + * @param log An abstract log to be filled with dummy values. + */ ++ (void)populateAbstractLogWithDummies:(MSACAbstractLog *)log; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACModelTestsUtililty.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACModelTestsUtililty.m new file mode 100644 index 0000000000..e071fed040 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACModelTestsUtililty.m @@ -0,0 +1,273 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACModelTestsUtililty.h" +#import "MSACAppExtension.h" +#import "MSACCSData.h" +#import "MSACCSExtensions.h" +#import "MSACDeviceExtension.h" +#import "MSACDeviceInternal.h" +#import "MSACLocExtension.h" +#import "MSACMetadataExtension.h" +#import "MSACNetExtension.h" +#import "MSACOSExtension.h" +#import "MSACProtocolExtension.h" +#import "MSACSDKExtension.h" +#import "MSACUserExtension.h" +#import "MSACUtility.h" +#import "MSACWrapperSdkInternal.h" + +@implementation MSACModelTestsUtililty + +#pragma mark - MSACDevice + ++ (NSDictionary *)deviceDummies { + return @{ + kMSACSDKVersion : @"3.0.1", + kMSACSDKName : @"appcenter-ios", + kMSACModel : @"iPhone 7.2", + kMSACOEMName : @"Apple", + kMSACACOSName : @"iOS", + kMSACOSVersion : @"9.3.20", + kMSACOSBuild : @"320", + kMSACLocale : @"US-EN", + kMSACTimeZoneOffset : @(9), + kMSACScreenSize : @"750x1334", + kMSACAppVersion : @"3.4.5", + kMSACAppBuild : @"178", + kMSACAppNamespace : @"com.contoso.apple.app", + kMSACCarrierName : @"Some-Telecom", + kMSACCarrierCountry : @"US", + kMSACWrapperSDKName : @"wrapper-sdk", + kMSACWrapperSDKVersion : @"6.7.8", + kMSACWrapperRuntimeVersion : @"9.10", + kMSACLiveUpdatePackageHash : @"b10a8db164e0754105b7a99be72e3fe5", + kMSACLiveUpdateReleaseLabel : @"live-update-release", + kMSACLiveUpdateDeploymentKey : @"deployment-key" + }; +} + ++ (NSMutableDictionary *)extensionDummies { + + // Set up all extensions with dummy values. + NSDictionary *userExtDummyValues = [MSACModelTestsUtililty userExtensionDummies]; + MSACUserExtension *userExt = [MSACModelTestsUtililty userExtensionWithDummyValues:userExtDummyValues]; + NSDictionary *locExtDummyValues = [MSACModelTestsUtililty locExtensionDummies]; + MSACLocExtension *locExt = [MSACModelTestsUtililty locExtensionWithDummyValues:locExtDummyValues]; + NSDictionary *osExtDummyValues = [MSACModelTestsUtililty osExtensionDummies]; + MSACOSExtension *osExt = [MSACModelTestsUtililty osExtensionWithDummyValues:osExtDummyValues]; + NSDictionary *appExtDummyValues = [MSACModelTestsUtililty appExtensionDummies]; + MSACAppExtension *appExt = [MSACModelTestsUtililty appExtensionWithDummyValues:appExtDummyValues]; + NSDictionary *protocolExtDummyValues = [MSACModelTestsUtililty protocolExtensionDummies]; + MSACProtocolExtension *protocolExt = [MSACModelTestsUtililty protocolExtensionWithDummyValues:protocolExtDummyValues]; + NSDictionary *netExtDummyValues = [MSACModelTestsUtililty netExtensionDummies]; + MSACNetExtension *netExt = [MSACModelTestsUtililty netExtensionWithDummyValues:netExtDummyValues]; + NSDictionary *sdkExtDummyValues = [MSACModelTestsUtililty sdkExtensionDummies]; + MSACSDKExtension *sdkExt = [MSACModelTestsUtililty sdkExtensionWithDummyValues:sdkExtDummyValues]; + NSDictionary *deviceExtDummyValues = [MSACModelTestsUtililty deviceExtensionDummies]; + MSACDeviceExtension *deviceExt = [MSACModelTestsUtililty deviceExtensionWithDummyValues:deviceExtDummyValues]; + + return [@{ + kMSACCSUserExt : userExt, + kMSACCSLocExt : locExt, + kMSACCSOSExt : osExt, + kMSACCSAppExt : appExt, + kMSACCSProtocolExt : protocolExt, + kMSACCSNetExt : netExt, + kMSACCSSDKExt : sdkExt, + kMSACCSDeviceExt : deviceExt + } mutableCopy]; +} + ++ (NSDictionary *)metadataExtensionDummies { + return @{kMSACFieldDelimiter : @{@"baseData" : @{kMSACFieldDelimiter : @{@"screenSize" : @2}}}}; +} + ++ (NSDictionary *)userExtensionDummies { + return @{kMSACUserLocalId : @"c:bob", kMSACUserLocale : @"en-us"}; +} + ++ (NSDictionary *)locExtensionDummies { + return @{kMSACTimezone : @"-03:00"}; +} + ++ (NSDictionary *)osExtensionDummies { + return @{kMSACOSName : @"iOS", kMSACOSVer : @"9.0"}; +} + ++ (NSDictionary *)appExtensionDummies { + return @{kMSACAppId : @"com.some.bundle.id", kMSACAppVer : @"3.4.1", kMSACAppLocale : @"en-us", kMSACAppUserId : @"c:alice"}; +} + ++ (NSDictionary *)protocolExtensionDummies { + return @{kMSACTicketKeys : @[ @"ticketKey1", @"ticketKey2" ], kMSACDevMake : @"Apple", kMSACDevModel : @"iPhone X"}; +} + ++ (NSDictionary *)netExtensionDummies { + return @{kMSACNetProvider : @"Verizon"}; +} + ++ (NSMutableDictionary *)sdkExtensionDummies { + return [@{kMSACSDKLibVer : @"1.2.0", kMSACSDKEpoch : MSAC_UUID_STRING, kMSACSDKSeq : @1, kMSACSDKInstallId : [NSUUID new]} mutableCopy]; +} + ++ (NSMutableDictionary *)deviceExtensionDummies { + return [@{kMSACDeviceLocalId : @"00000000-0000-0000-0000-000000000000"} mutableCopy]; +} + ++ (MSACOrderedDictionary *)orderedDataDummies { + MSACOrderedDictionary *data = [MSACOrderedDictionary new]; + [data setObject:@"aBaseType" forKey:@"baseType"]; + [data setObject:@"someValue" forKey:@"baseData"]; + [data setObject:@"anothervalue" forKey:@"anested.key"]; + [data setObject:@"aValue" forKey:@"aKey"]; + [data setObject:@"yetanothervalue" forKey:@"anotherkey"]; + return data; +} + ++ (NSDictionary *)unorderedDataDummies { + NSDictionary *data = @{ + @"baseType" : @"aBaseType", + @"baseData" : @"someValue", + @"anested.key" : @"anothervalue", + @"aKey" : @"aValue", + @"anotherkey" : @"yetanothervalue" + }; + + return data; +} + ++ (MSACDevice *)dummyDevice { + NSDictionary *dummyValues = [self deviceDummies]; + MSACDevice *device = [MSACDevice new]; + device.sdkVersion = dummyValues[kMSACSDKVersion]; + device.sdkName = dummyValues[kMSACSDKName]; + device.model = dummyValues[kMSACModel]; + device.oemName = dummyValues[kMSACOEMName]; + device.osName = dummyValues[kMSACACOSName]; + device.osVersion = dummyValues[kMSACOSVersion]; + device.osBuild = dummyValues[kMSACOSBuild]; + device.locale = dummyValues[kMSACLocale]; + device.timeZoneOffset = dummyValues[kMSACTimeZoneOffset]; + device.screenSize = dummyValues[kMSACScreenSize]; + device.appVersion = dummyValues[kMSACAppVersion]; + device.appBuild = dummyValues[kMSACAppBuild]; + device.appNamespace = dummyValues[kMSACAppNamespace]; + device.carrierName = dummyValues[kMSACCarrierName]; + device.carrierCountry = dummyValues[kMSACCarrierCountry]; + device.wrapperSdkVersion = dummyValues[kMSACWrapperSDKVersion]; + device.wrapperSdkName = dummyValues[kMSACWrapperSDKName]; + device.wrapperRuntimeVersion = dummyValues[kMSACWrapperRuntimeVersion]; + device.liveUpdateReleaseLabel = dummyValues[kMSACLiveUpdateReleaseLabel]; + device.liveUpdateDeploymentKey = dummyValues[kMSACLiveUpdateDeploymentKey]; + device.liveUpdatePackageHash = dummyValues[kMSACLiveUpdatePackageHash]; + return device; +} + +#pragma mark - MSACAbstractLog + ++ (NSDictionary *)abstractLogDummies { + return @{ + kMSACType : @"fakeLogType", + kMSACTimestamp : [NSDate dateWithTimeIntervalSince1970:42], + kMSACSId : @"FAKE-SESSION-ID", + kMSACDistributionGroupId : @"FAKE-GROUP-ID", + kMSACDevice : [self dummyDevice] + }; +} + ++ (void)populateAbstractLogWithDummies:(MSACAbstractLog *)log { + NSDictionary *dummyValues = [self abstractLogDummies]; + log.type = dummyValues[kMSACType]; + log.timestamp = dummyValues[kMSACTimestamp]; + log.sid = dummyValues[kMSACSId]; + log.distributionGroupId = dummyValues[kMSACDistributionGroupId]; + log.device = dummyValues[kMSACDevice]; +} + +#pragma mark - Extensions + ++ (MSACCSExtensions *)extensionsWithDummyValues:(NSDictionary *)dummyValues { + MSACCSExtensions *ext = [MSACCSExtensions new]; + ext.userExt = dummyValues[kMSACCSUserExt]; + ext.locExt = dummyValues[kMSACCSLocExt]; + ext.osExt = dummyValues[kMSACCSOSExt]; + ext.appExt = dummyValues[kMSACCSAppExt]; + ext.protocolExt = dummyValues[kMSACCSProtocolExt]; + ext.netExt = dummyValues[kMSACCSNetExt]; + ext.sdkExt = dummyValues[kMSACCSSDKExt]; + ext.deviceExt = dummyValues[kMSACCSDeviceExt]; + return ext; +} + ++ (MSACUserExtension *)userExtensionWithDummyValues:(NSDictionary *)dummyValues { + MSACUserExtension *userExt = [MSACUserExtension new]; + userExt.localId = dummyValues[kMSACUserLocalId]; + userExt.locale = dummyValues[kMSACUserLocale]; + return userExt; +} + ++ (MSACLocExtension *)locExtensionWithDummyValues:(NSDictionary *)dummyValues { + MSACLocExtension *locExt = [MSACLocExtension new]; + locExt.tz = dummyValues[kMSACTimezone]; + return locExt; +} + ++ (MSACOSExtension *)osExtensionWithDummyValues:(NSDictionary *)dummyValues { + MSACOSExtension *osExt = [MSACOSExtension new]; + osExt.name = dummyValues[kMSACOSName]; + osExt.ver = dummyValues[kMSACOSVer]; + return osExt; +} + ++ (MSACAppExtension *)appExtensionWithDummyValues:(NSDictionary *)dummyValues { + MSACAppExtension *appExt = [MSACAppExtension new]; + appExt.appId = dummyValues[kMSACAppId]; + appExt.ver = dummyValues[kMSACAppVer]; + appExt.locale = dummyValues[kMSACAppLocale]; + appExt.userId = dummyValues[kMSACAppUserId]; + return appExt; +} + ++ (MSACProtocolExtension *)protocolExtensionWithDummyValues:(NSDictionary *)dummyValues { + MSACProtocolExtension *protocolExt = [MSACProtocolExtension new]; + protocolExt.ticketKeys = dummyValues[kMSACTicketKeys]; + protocolExt.devMake = dummyValues[kMSACDevMake]; + protocolExt.devModel = dummyValues[kMSACDevModel]; + return protocolExt; +} + ++ (MSACNetExtension *)netExtensionWithDummyValues:(NSDictionary *)dummyValues { + MSACNetExtension *netExt = [MSACNetExtension new]; + netExt.provider = dummyValues[kMSACNetProvider]; + return netExt; +} + ++ (MSACSDKExtension *)sdkExtensionWithDummyValues:(NSDictionary *)dummyValues { + MSACSDKExtension *sdkExt = [MSACSDKExtension new]; + sdkExt.libVer = dummyValues[kMSACSDKLibVer]; + sdkExt.epoch = dummyValues[kMSACSDKEpoch]; + sdkExt.seq = [dummyValues[kMSACSDKSeq] longLongValue]; + sdkExt.installId = dummyValues[kMSACSDKInstallId]; + return sdkExt; +} + ++ (MSACDeviceExtension *)deviceExtensionWithDummyValues:(NSDictionary *)dummyValues { + MSACDeviceExtension *deviceExt = [MSACDeviceExtension new]; + deviceExt.localId = dummyValues[kMSACDeviceLocalId]; + return deviceExt; +} + ++ (MSACMetadataExtension *)metadataExtensionWithDummyValues:(NSDictionary *)dummyValues { + MSACMetadataExtension *metadataExt = [MSACMetadataExtension new]; + metadataExt.metadata = dummyValues; + return metadataExt; +} + ++ (MSACCSData *)dataWithDummyValues:(NSDictionary *)dummyValues { + MSACCSData *data = [MSACCSData new]; + data.properties = [dummyValues mutableCopy]; + return data; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACOneCollectorChannelDelegateTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACOneCollectorChannelDelegateTests.m new file mode 100644 index 0000000000..ad0c363c52 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACOneCollectorChannelDelegateTests.m @@ -0,0 +1,604 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACCSData.h" +#import "MSACCSExtensions.h" +#import "MSACChannelGroupProtocol.h" +#import "MSACChannelUnitConfiguration.h" +#import "MSACChannelUnitDefault.h" +#import "MSACCommonSchemaLog.h" +#import "MSACHttpClient.h" +#import "MSACIngestionProtocol.h" +#import "MSACMockLogObject.h" +#import "MSACMockLogWithConversion.h" +#import "MSACOneCollectorChannelDelegatePrivate.h" +#import "MSACOneCollectorIngestion.h" +#import "MSACSDKExtension.h" +#import "MSACStorage.h" +#import "MSACTestFrameworks.h" + +static NSString *const kMSACBaseGroupId = @"baseGroupId"; +static NSString *const kMSACOneCollectorGroupId = @"baseGroupId/one"; + +// This is to get rid of warnings in the test that a method takes `nil` as its parameter even though it is marked as `nonnull`. +// Do not convert it to const. +static NSString *kMSACNilString = nil; + +@interface MSACOneCollectorChannelDelegateTests : XCTestCase + +@property(nonatomic) MSACOneCollectorChannelDelegate *sut; +@property(nonatomic) id ingestionMock; +@property(nonatomic) id storageMock; +@property(nonatomic) dispatch_queue_t logsDispatchQueue; +@property(nonatomic) MSACChannelUnitConfiguration *baseUnitConfig; +@property(nonatomic) MSACChannelUnitConfiguration *oneCollectorUnitConfig; + +@end + +@implementation MSACOneCollectorChannelDelegateTests + +- (void)setUp { + [super setUp]; + self.sut = [[MSACOneCollectorChannelDelegate alloc] initWithHttpClient:[MSACHttpClient new] + installId:[NSUUID new] + baseUrl:kMSACNilString]; + self.ingestionMock = OCMProtocolMock(@protocol(MSACIngestionProtocol)); + self.storageMock = OCMProtocolMock(@protocol(MSACStorage)); + self.logsDispatchQueue = dispatch_get_main_queue(); + self.baseUnitConfig = [[MSACChannelUnitConfiguration alloc] initWithGroupId:kMSACBaseGroupId + priority:MSACPriorityDefault + flushInterval:3.0 + batchSizeLimit:1024 + pendingBatchesLimit:60]; + self.oneCollectorUnitConfig = [[MSACChannelUnitConfiguration alloc] initWithGroupId:kMSACOneCollectorGroupId + priority:MSACPriorityDefault + flushInterval:3.0 + batchSizeLimit:1024 + pendingBatchesLimit:60]; +} + +- (void)testDidAddChannelUnitWithBaseGroupId { + + // Test adding a base channel unit on MSACChannelGroupDefault will also add a One Collector channel unit. + + // If + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([channelUnitMock configuration]).andReturn(self.baseUnitConfig); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + __block id expectedChannelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + __block MSACChannelUnitConfiguration *oneCollectorChannelConfig = nil; + OCMStub([channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY withIngestion:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + [invocation retainArguments]; + [invocation getArgument:&oneCollectorChannelConfig atIndex:2]; + [invocation setReturnValue:&expectedChannelUnitMock]; + }); + + // When + [self.sut channelGroup:channelGroupMock didAddChannelUnit:channelUnitMock]; + + // Then + XCTAssertNotNil(self.sut.oneCollectorChannels[kMSACBaseGroupId]); + XCTAssertTrue([self.sut.oneCollectorChannels count] == 1); + XCTAssertEqual(expectedChannelUnitMock, self.sut.oneCollectorChannels[kMSACBaseGroupId]); + XCTAssertTrue([oneCollectorChannelConfig.groupId isEqualToString:kMSACOneCollectorGroupId]); + OCMVerifyAll(channelGroupMock); +} + +- (void)testDidAddChannelUnitWithOneCollectorGroupId { + + /* + * Test adding an One Collector channel unit on MSACChannelGroupDefault won't do anything on MSACOneCollectorChannelDelegate because it's + * already an One Collector group Id. + */ + + // If + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([channelUnitMock configuration]).andReturn(self.oneCollectorUnitConfig); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + OCMReject([channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY]); + + // When + [self.sut channelGroup:channelGroupMock didAddChannelUnit:channelUnitMock]; + + // Then + XCTAssertNotNil(self.sut.oneCollectorChannels); + XCTAssertTrue([self.sut.oneCollectorChannels count] == 0); + OCMVerifyAll(channelGroupMock); +} + +- (void)testOneCollectorChannelUnitIsPausedWhenBaseChannelUnitIsPaused { + + // If + NSObject *token = [NSObject new]; + MSACChannelUnitDefault *channelUnitMock = [[MSACChannelUnitDefault alloc] initWithIngestion:self.ingestionMock + storage:self.storageMock + configuration:self.baseUnitConfig + logsDispatchQueue:self.logsDispatchQueue]; + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + id oneCollectorChannelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY withIngestion:OCMOCK_ANY]).andReturn(oneCollectorChannelUnitMock); + + // When + [self.sut channelGroup:channelGroupMock didAddChannelUnit:channelUnitMock]; + [self.sut channel:channelUnitMock didPauseWithIdentifyingObject:token]; + + // Then + OCMVerify([oneCollectorChannelUnitMock pauseWithIdentifyingObject:token]); +} + +- (void)testOneCollectorChannelUnitIsNotPausedWhenNonBaseChannelUnitIsPaused { + + // If + NSObject *token = [NSObject new]; + MSACChannelUnitDefault *channelUnitMock = [[MSACChannelUnitDefault alloc] initWithIngestion:self.ingestionMock + storage:self.storageMock + configuration:self.baseUnitConfig + logsDispatchQueue:self.logsDispatchQueue]; + id oneCollectorChannelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + id otherOneCollectorChannelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + self.sut.oneCollectorChannels[kMSACBaseGroupId] = oneCollectorChannelUnitMock; + self.sut.oneCollectorChannels[@"someOtherGroupId"] = otherOneCollectorChannelUnitMock; + + // Then + OCMReject([otherOneCollectorChannelUnitMock pauseWithIdentifyingObject:token]); + + // When + [self.sut channel:channelUnitMock didPauseWithIdentifyingObject:token]; +} + +- (void)testOneCollectorChannelUnitIsResumedWhenBaseChannelUnitIsResumed { + + // If + NSObject *token = [NSObject new]; + MSACChannelUnitDefault *channelUnitMock = [[MSACChannelUnitDefault alloc] initWithIngestion:self.ingestionMock + storage:self.storageMock + configuration:self.baseUnitConfig + logsDispatchQueue:self.logsDispatchQueue]; + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + id oneCollectorChannelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY withIngestion:OCMOCK_ANY]).andReturn(oneCollectorChannelUnitMock); + + // When + [self.sut channelGroup:channelGroupMock didAddChannelUnit:channelUnitMock]; + [self.sut channel:channelUnitMock didResumeWithIdentifyingObject:token]; + + // Then + OCMVerify([oneCollectorChannelUnitMock resumeWithIdentifyingObject:token]); +} + +- (void)testOneCollectorChannelUnitIsNotResumedWhenNonBaseChannelUnitIsResumed { + + // If + NSObject *token = [NSObject new]; + MSACChannelUnitDefault *channelUnitMock = [[MSACChannelUnitDefault alloc] initWithIngestion:self.ingestionMock + storage:self.storageMock + configuration:self.baseUnitConfig + logsDispatchQueue:self.logsDispatchQueue]; + id oneCollectorChannelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + id otherOneCollectorChannelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + self.sut.oneCollectorChannels[kMSACBaseGroupId] = oneCollectorChannelUnitMock; + self.sut.oneCollectorChannels[@"someOtherGroupId"] = otherOneCollectorChannelUnitMock; + + // Then + OCMReject([otherOneCollectorChannelUnitMock resumeWithIdentifyingObject:token]); + + // When + [self.sut channel:channelUnitMock didResumeWithIdentifyingObject:token]; +} + +- (void)testDidSetEnabledAndDeleteDataOnDisabled { + + /* + * Test base channel unit's logs are cleared when the base channel unit is disabled. First, add a base channel unit to the channel group. + * Then, disable the base channel unit. Lastly, verify the storage deletion is called for the base channel group id. + */ + + // If + MSACChannelUnitDefault *channelUnit = [[MSACChannelUnitDefault alloc] initWithIngestion:self.ingestionMock + storage:self.storageMock + configuration:self.baseUnitConfig + logsDispatchQueue:self.logsDispatchQueue]; + MSACChannelUnitDefault *oneCollectorChannelUnit = [[MSACChannelUnitDefault alloc] initWithIngestion:self.sut.oneCollectorIngestion + storage:self.storageMock + configuration:self.oneCollectorUnitConfig + logsDispatchQueue:self.logsDispatchQueue]; + [channelUnit addDelegate:self.sut]; + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + OCMStub([channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY withIngestion:self.sut.oneCollectorIngestion]) + .andReturn(oneCollectorChannelUnit); + + // When + [self.sut channelGroup:channelGroupMock didAddChannelUnit:channelUnit]; + [channelUnit setEnabled:NO andDeleteDataOnDisabled:YES]; + + // Then + [self enqueueChannelEndJobExpectation]; + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *error) { + OCMVerify([self.storageMock deleteLogsWithGroupId:kMSACBaseGroupId]); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; + [self enqueueChannelEndJobExpectation]; + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *error) { + OCMVerify([self.storageMock deleteLogsWithGroupId:kMSACOneCollectorGroupId]); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testDidEnqueueLogToOneCollectorChannelWhenLogHasTargetTokensAndLogIsNotCommonSchemaLog { + + // If + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([channelUnitMock configuration]).andReturn(self.baseUnitConfig); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + id oneCollectorChannelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub(oneCollectorChannelUnitMock.logsDispatchQueue).andReturn(self.logsDispatchQueue); + OCMStub([channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY withIngestion:OCMOCK_ANY]).andReturn(oneCollectorChannelUnitMock); + NSMutableSet *transmissionTargetTokens = [NSMutableSet new]; + [transmissionTargetTokens addObject:@"fake-transmission-target-token"]; + MSACCommonSchemaLog *commonSchemaLog = [MSACCommonSchemaLog new]; + id mockLog = OCMProtocolMock(@protocol(MSACMockLogWithConversion)); + OCMStub([mockLog toCommonSchemaLogsWithFlags:MSACFlagsDefault]).andReturn(@[ commonSchemaLog ]); + OCMStub(mockLog.transmissionTargetTokens).andReturn(transmissionTargetTokens); + + // When + [self.sut channelGroup:channelGroupMock didAddChannelUnit:channelUnitMock]; + [self.sut channel:channelUnitMock didPrepareLog:mockLog internalId:@"fake-id" flags:MSACFlagsDefault]; + + // Then + [self enqueueChannelEndJobExpectation]; + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *error) { + OCMVerify([oneCollectorChannelUnitMock enqueueItem:commonSchemaLog flags:MSACFlagsDefault]); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testDidEnqueueLogToOneCollectorChannelSynchronously { + + // If + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([channelUnitMock configuration]).andReturn(self.baseUnitConfig); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + id oneCollectorChannelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub(oneCollectorChannelUnitMock.logsDispatchQueue).andReturn(self.logsDispatchQueue); + OCMStub([channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY withIngestion:OCMOCK_ANY]).andReturn(oneCollectorChannelUnitMock); + NSMutableSet *transmissionTargetTokens = [NSMutableSet new]; + [transmissionTargetTokens addObject:@"fake-transmission-target-token"]; + MSACCommonSchemaLog *commonSchemaLog = [MSACCommonSchemaLog new]; + id mockLog = OCMProtocolMock(@protocol(MSACMockLogWithConversion)); + OCMStub([mockLog toCommonSchemaLogsWithFlags:MSACFlagsDefault]).andReturn(@[ commonSchemaLog ]); + OCMStub(mockLog.transmissionTargetTokens).andReturn(transmissionTargetTokens); + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + + /* + * Make sure that the common schema log is enqueued synchronously by putting a task on the log queue that won't return + * by the time verify is called. + */ + dispatch_async(oneCollectorChannelUnitMock.logsDispatchQueue, ^{ + dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); + }); + + // When + [self.sut channelGroup:channelGroupMock didAddChannelUnit:channelUnitMock]; + [self.sut channel:channelUnitMock didPrepareLog:mockLog internalId:@"fake-id" flags:MSACFlagsDefault]; + + // Then + OCMVerify([oneCollectorChannelUnitMock enqueueItem:commonSchemaLog flags:MSACFlagsDefault]); + dispatch_semaphore_signal(sem); +} + +- (void)testDidNotEnqueueLogToOneCollectorChannelWhenLogDoesNotConformToMSACLogConversionProtocol { + + // If + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([channelUnitMock configuration]).andReturn(self.baseUnitConfig); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + id oneCollectorChannelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY withIngestion:OCMOCK_ANY]).andReturn(oneCollectorChannelUnitMock); + NSMutableSet *transmissionTargetTokens = [NSMutableSet new]; + [transmissionTargetTokens addObject:@"fake-transmission-target-token"]; + MSACCommonSchemaLog *commonSchemaLog = [MSACCommonSchemaLog new]; + id mockLog = OCMProtocolMock(@protocol(MSACMockLogObject)); + OCMStub(mockLog.transmissionTargetTokens).andReturn(transmissionTargetTokens); + + // Then + OCMReject([oneCollectorChannelUnitMock enqueueItem:commonSchemaLog flags:MSACFlagsDefault]); + + // When + [self.sut channelGroup:channelGroupMock didAddChannelUnit:channelUnitMock]; + [self.sut channel:channelUnitMock didPrepareLog:mockLog internalId:@"fake-id" flags:MSACFlagsDefault]; +} + +- (void)testReEnqueueLogWhenCommonSchemaLogIsPrepared { + + // If + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([channelUnitMock configuration]).andReturn(self.baseUnitConfig); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + id oneCollectorChannelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub(oneCollectorChannelUnitMock.logsDispatchQueue).andReturn(self.logsDispatchQueue); + OCMStub([channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY withIngestion:OCMOCK_ANY]).andReturn(oneCollectorChannelUnitMock); + NSMutableSet *transmissionTargetTokens = [NSMutableSet new]; + [transmissionTargetTokens addObject:@"fake-transmission-target-token"]; + id commonSchemaLog = OCMPartialMock([MSACCommonSchemaLog new]); + OCMStub([commonSchemaLog transmissionTargetTokens]).andReturn(transmissionTargetTokens); + + // When + [self.sut channelGroup:channelGroupMock didAddChannelUnit:channelUnitMock]; + [self.sut channel:channelUnitMock didPrepareLog:commonSchemaLog internalId:@"fake-id" flags:MSACFlagsDefault]; + + // Then + [self enqueueChannelEndJobExpectation]; + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *error) { + OCMVerify([oneCollectorChannelUnitMock enqueueItem:commonSchemaLog flags:MSACFlagsDefault]); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testDidNotEnqueueLogWhenLogHasNoTargetTokens { + + // If + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([channelUnitMock configuration]).andReturn(self.baseUnitConfig); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + id oneCollectorChannelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY withIngestion:OCMOCK_ANY]).andReturn(oneCollectorChannelUnitMock); + NSMutableSet *transmissionTargetTokens = [NSMutableSet new]; + id mockLog = OCMProtocolMock(@protocol(MSACMockLogWithConversion)); + OCMStub(mockLog.transmissionTargetTokens).andReturn(transmissionTargetTokens); + OCMStub([mockLog toCommonSchemaLogsWithFlags:MSACFlagsDefault]).andReturn(@ [[MSACCommonSchemaLog new]]); + + // Then + OCMReject([oneCollectorChannelUnitMock enqueueItem:OCMOCK_ANY flags:MSACFlagsDefault]); + + // When + [self.sut channelGroup:channelGroupMock didAddChannelUnit:channelUnitMock]; + [self.sut channel:channelUnitMock didPrepareLog:mockLog internalId:@"fake-id" flags:MSACFlagsDefault]; +} + +- (void)testDidNotEnqueueLogWhenLogHasNilTargetTokens { + + // If + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([channelUnitMock configuration]).andReturn(self.baseUnitConfig); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + id oneCollectorChannelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY withIngestion:OCMOCK_ANY]).andReturn(oneCollectorChannelUnitMock); + id mockLog = OCMProtocolMock(@protocol(MSACMockLogWithConversion)); + OCMStub(mockLog.transmissionTargetTokens).andReturn(nil); + OCMStub([mockLog toCommonSchemaLogsWithFlags:MSACFlagsDefault]).andReturn(@ [[MSACCommonSchemaLog new]]); + + // Then + OCMReject([oneCollectorChannelUnitMock enqueueItem:OCMOCK_ANY flags:MSACFlagsDefault]); + + // When + [self.sut channelGroup:channelGroupMock didAddChannelUnit:channelUnitMock]; + [self.sut channel:channelUnitMock didPrepareLog:mockLog internalId:@"fake-id" flags:MSACFlagsDefault]; +} + +- (void)testDoesNotFilterValidCommonSchemaLogs { + + // If + id oneCollectorChannelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([oneCollectorChannelUnitMock configuration]).andReturn(self.oneCollectorUnitConfig); + MSACCommonSchemaLog *log = [MSACCommonSchemaLog new]; + log.name = @"avalidname"; + + // When + BOOL shouldFilter = [self.sut channelUnit:oneCollectorChannelUnitMock shouldFilterLog:log]; + + // Then + XCTAssertFalse(shouldFilter); +} + +- (void)testFiltersInvalidCommonSchemaLogs { + + // If + id oneCollectorChannelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([oneCollectorChannelUnitMock configuration]).andReturn(self.oneCollectorUnitConfig); + MSACCommonSchemaLog *log = [MSACCommonSchemaLog new]; + log.name = nil; + + // When + BOOL shouldFilter = [self.sut channelUnit:oneCollectorChannelUnitMock shouldFilterLog:log]; + + // Then + XCTAssertTrue(shouldFilter); +} + +- (void)testDoesNotFilterLogFromNonOneCollectorChannelWhenLogHasNoTargetTokens { + + // If + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([channelUnitMock configuration]).andReturn(self.baseUnitConfig); + NSMutableSet *transmissionTargetTokens = [NSMutableSet new]; + id mockLog = OCMProtocolMock(@protocol(MSACLog)); + OCMStub(mockLog.transmissionTargetTokens).andReturn(transmissionTargetTokens); + + // When + BOOL shouldFilter = [self.sut channelUnit:channelUnitMock shouldFilterLog:mockLog]; + + // Then + XCTAssertFalse(shouldFilter); +} + +- (void)testDoesNotFilterLogFromNonOneCollectorChannelWhenLogHasNilTargetTokenSet { + + // If + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([channelUnitMock configuration]).andReturn(self.baseUnitConfig); + id mockLog = OCMProtocolMock(@protocol(MSACLog)); + OCMStub(mockLog.transmissionTargetTokens).andReturn(nil); + + // When + BOOL shouldFilter = [self.sut channelUnit:channelUnitMock shouldFilterLog:mockLog]; + + // Then + XCTAssertFalse(shouldFilter); +} + +- (void)testFiltersNonOneCollectorLogWhenLogHasTargetTokens { + + // If + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([channelUnitMock configuration]).andReturn(self.baseUnitConfig); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + id oneCollectorChannelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY withIngestion:OCMOCK_ANY]).andReturn(oneCollectorChannelUnitMock); + NSMutableSet *transmissionTargetTokens = [NSMutableSet new]; + [transmissionTargetTokens addObject:@"fake-transmission-target-token"]; + MSACCommonSchemaLog *commonSchemaLog = [MSACCommonSchemaLog new]; + id mockLog = OCMProtocolMock(@protocol(MSACMockLogWithConversion)); + OCMStub([mockLog toCommonSchemaLogsWithFlags:MSACFlagsDefault]).andReturn(@[ commonSchemaLog ]); + OCMStub(mockLog.transmissionTargetTokens).andReturn(transmissionTargetTokens); + + // When + BOOL shouldFilter = [self.sut channelUnit:channelUnitMock shouldFilterLog:mockLog]; + + // Then + XCTAssertTrue(shouldFilter); +} + +- (void)testValidateLog { + + // If + // Valid name. + MSACCommonSchemaLog *log = [MSACCommonSchemaLog new]; + log.name = @"valid.CS.event.name"; + + // Then + XCTAssertTrue([self.sut validateLog:log]); + + // If + // Invalid name. + log.name = nil; + + // Then + XCTAssertFalse([self.sut validateLog:log]); + + // If + // Valid data. + log.name = @"valid.CS.event.name"; + log.data = [MSACCSData new]; + log.data.properties = @{@"validkey" : @"validvalue"}; + + // Then + XCTAssertTrue([self.sut validateLog:log]); +} + +- (void)testValidateLogName { + const int maxNameLength = 100; + + // If + NSString *validName = @"valid.CS.event.name"; + NSString *shortName = @"e"; + NSString *name100 = [@"" stringByPaddingToLength:maxNameLength withString:@"logName100" startingAtIndex:0]; + NSString *nilLogName = nil; + NSString *emptyName = @""; + NSString *tooLongName = [@"" stringByPaddingToLength:(maxNameLength + 1) withString:@"tooLongLogName" startingAtIndex:0]; + NSString *periodAndUnderscoreName = @"hello.world_mamamia"; + NSString *leadingPeriodName = @".hello.world"; + NSString *trailingPeriodName = @"hello.world."; + NSString *consecutivePeriodName = @"hello..world"; + NSString *headingUnderscoreName = @"_hello.world"; + NSString *specialCharactersOtherThanPeriodAndUnderscore = @"hello%^&world"; + + // Then + XCTAssertTrue([self.sut validateLogName:validName]); + XCTAssertFalse([self.sut validateLogName:shortName]); + XCTAssertTrue([self.sut validateLogName:name100]); + XCTAssertFalse([self.sut validateLogName:nilLogName]); + XCTAssertFalse([self.sut validateLogName:emptyName]); + XCTAssertFalse([self.sut validateLogName:tooLongName]); + XCTAssertTrue([self.sut validateLogName:periodAndUnderscoreName]); + XCTAssertFalse([self.sut validateLogName:leadingPeriodName]); + XCTAssertFalse([self.sut validateLogName:trailingPeriodName]); + XCTAssertFalse([self.sut validateLogName:consecutivePeriodName]); + XCTAssertFalse([self.sut validateLogName:headingUnderscoreName]); + XCTAssertFalse([self.sut validateLogName:specialCharactersOtherThanPeriodAndUnderscore]); +} + +- (void)testLogNameRegex { + + // If + NSError *error = nil; + + // When + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:kMSACLogNameRegex options:0 error:&error]; + + // Then + XCTAssertNotNil(regex); + XCTAssertNil(error); +} + +- (void)testPrepareLogForSDKExtension { + + // If + NSUUID *installId = [NSUUID new]; + self.sut = [[MSACOneCollectorChannelDelegate alloc] initWithHttpClient:[MSACHttpClient new] installId:installId baseUrl:kMSACNilString]; + id channelMock = OCMProtocolMock(@protocol(MSACChannelProtocol)); + MSACCommonSchemaLog *csLogMock = OCMPartialMock([MSACCommonSchemaLog new]); + csLogMock.iKey = @"o:81439696f7164d7599d543f9bf37abb7"; + MSACCSExtensions *ext = OCMPartialMock([MSACCSExtensions new]); + MSACSDKExtension *sdkExt = OCMPartialMock([MSACSDKExtension new]); + ext.sdkExt = sdkExt; + csLogMock.ext = ext; + OCMStub([csLogMock isValid]).andReturn(YES); + + // When + [self.sut channel:channelMock prepareLog:csLogMock]; + + // Then + XCTAssertEqualObjects(installId, csLogMock.ext.sdkExt.installId); + XCTAssertNotNil(csLogMock.ext.sdkExt.epoch); + XCTAssertEqual(csLogMock.ext.sdkExt.seq, 1); + XCTAssertNotNil(self.sut.epochsAndSeqsByIKey); + XCTAssertTrue(self.sut.epochsAndSeqsByIKey.count == 1); +} + +- (void)testResetEpochAndSeq { + + // If + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + MSACCommonSchemaLog *csLogMock = OCMPartialMock([MSACCommonSchemaLog new]); + csLogMock.iKey = @"o:81439696f7164d7599d543f9bf37abb7"; + MSACCSExtensions *ext = OCMPartialMock([MSACCSExtensions new]); + MSACSDKExtension *sdkExt = OCMPartialMock([MSACSDKExtension new]); + ext.sdkExt = sdkExt; + csLogMock.ext = ext; + OCMStub([csLogMock isValid]).andReturn(YES); + + // When + [self.sut channel:channelGroupMock prepareLog:csLogMock]; + + // Then + XCTAssertNotNil(self.sut.epochsAndSeqsByIKey); + XCTAssertTrue(self.sut.epochsAndSeqsByIKey.count == 1); + + // When + [self.sut channel:channelGroupMock didSetEnabled:NO andDeleteDataOnDisabled:YES]; + + // Then + XCTAssertTrue(self.sut.epochsAndSeqsByIKey.count == 0); +} + +// A helper method to initialize the test expectation +- (void)enqueueChannelEndJobExpectation { + XCTestExpectation *channelEndJobExpectation = [self expectationWithDescription:@"Channel job should be finished"]; + dispatch_async(self.logsDispatchQueue, ^{ + [channelEndJobExpectation fulfill]; + }); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACOneCollectorIngestionTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACOneCollectorIngestionTests.m new file mode 100644 index 0000000000..4049cb96a7 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACOneCollectorIngestionTests.m @@ -0,0 +1,366 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "AppCenter+Internal.h" +#import "MSACAppCenterErrors.h" +#import "MSACConstants+Internal.h" +#import "MSACDeviceInternal.h" +#import "MSACHttpClient.h" +#import "MSACHttpIngestionPrivate.h" +#import "MSACHttpTestUtil.h" +#import "MSACLoggerInternal.h" +#import "MSACMockCommonSchemaLog.h" +#import "MSACModelTestsUtililty.h" +#import "MSACOneCollectorIngestion.h" +#import "MSACOneCollectorIngestionPrivate.h" +#import "MSACTestFrameworks.h" +#import "MSACTicketCache.h" +#import "MSACUtility+StringFormatting.h" + +static NSTimeInterval const kMSACTestTimeout = 5.0; +static NSString *const kMSACBaseUrl = @"https://test.com"; + +@interface MSACOneCollectorIngestionTests : XCTestCase + +@property(nonatomic) MSACOneCollectorIngestion *sut; +@property(nonatomic) id reachabilityMock; +@property(nonatomic) NetworkStatus currentNetworkStatus; +@property(nonatomic) MSACHttpClient *httpClientMock; +@end + +@implementation MSACOneCollectorIngestionTests + +- (void)setUp { + [super setUp]; + + self.httpClientMock = OCMPartialMock([MSACHttpClient new]); + self.reachabilityMock = OCMClassMock([MSAC_Reachability class]); + self.currentNetworkStatus = ReachableViaWiFi; + OCMStub([self.reachabilityMock currentReachabilityStatus]).andDo(^(NSInvocation *invocation) { + NetworkStatus test = self.currentNetworkStatus; + [invocation setReturnValue:&test]; + }); + + // sut: System under test + self.sut = [[MSACOneCollectorIngestion alloc] initWithHttpClient:self.httpClientMock baseUrl:kMSACBaseUrl]; +} + +- (void)tearDown { + [super tearDown]; + [self.reachabilityMock stopMocking]; + [MSACHttpTestUtil removeAllStubs]; + + /* + * Setting the variable to nil. We are experiencing test failure on Xcode 9 beta because the instance that was used for previous test + * method is not disposed and still listening to network changes in other tests. + */ + self.sut = nil; +} + +- (void)testHeaders { + + // If + id ticketCacheMock = OCMPartialMock([MSACTicketCache sharedInstance]); + OCMStub([ticketCacheMock ticketFor:@"ticketKey1"]).andReturn(@"ticketKey1Token"); + OCMStub([ticketCacheMock ticketFor:@"ticketKey2"]).andReturn(@"ticketKey2Token"); + + // When + NSString *containerId = @"1"; + MSACLogContainer *container = [self createLogContainerWithId:containerId]; + NSDictionary *headers = [self.sut getHeadersWithData:container eTag:nil]; + NSArray *keys = [headers allKeys]; + + // Then + XCTAssertTrue([keys containsObject:kMSACHeaderContentTypeKey]); + XCTAssertTrue([[headers objectForKey:kMSACHeaderContentTypeKey] isEqualToString:kMSACOneCollectorContentType]); + XCTAssertTrue([keys containsObject:kMSACOneCollectorClientVersionKey]); + NSString *expectedClientVersion = [NSString stringWithFormat:kMSACOneCollectorClientVersionFormat, [MSACUtility sdkVersion]]; + XCTAssertTrue([[headers objectForKey:kMSACOneCollectorClientVersionKey] isEqualToString:expectedClientVersion]); + XCTAssertNil([headers objectForKey:kMSACHeaderAppSecretKey]); + XCTAssertTrue([keys containsObject:kMSACOneCollectorApiKey]); + NSArray *tokens = [[headers objectForKey:kMSACOneCollectorApiKey] componentsSeparatedByString:@","]; + XCTAssertTrue([tokens count] == 3); + for (NSString *token in @[ @"token1", @"token2", @"token3" ]) { + XCTAssertTrue([tokens containsObject:token]); + } + XCTAssertTrue([keys containsObject:kMSACOneCollectorUploadTimeKey]); + NSString *uploadTimeString = [headers objectForKey:kMSACOneCollectorUploadTimeKey]; + NSNumberFormatter *formatter = [NSNumberFormatter new]; + [formatter setNumberStyle:NSNumberFormatterDecimalStyle]; + XCTAssertNotNil([formatter numberFromString:uploadTimeString]); + XCTAssertTrue([keys containsObject:kMSACOneCollectorTicketsKey]); + NSString *ticketsHeader = [headers objectForKey:kMSACOneCollectorTicketsKey]; + XCTAssertTrue([ticketsHeader isEqualToString:@"{\"ticketKey2\":\"ticketKey2Token\",\"ticketKey1\":\"ticketKey1Token\"}"]); +} + +- (void)testHttpClientDelegateObfuscateHeaderValue { + + // If + id mockLogger = OCMClassMock([MSACLogger class]); + id ingestionMock = OCMPartialMock(self.sut); + OCMStub([mockLogger currentLogLevel]).andReturn(MSACLogLevelVerbose); + OCMStub([ingestionMock obfuscateTargetTokens:OCMOCK_ANY]).andDo(nil); + OCMStub([ingestionMock obfuscateTickets:OCMOCK_ANY]).andDo(nil); + NSString *tokenValue = @"12345678"; + NSString *ticketValue = @"something"; + NSDictionary *headers = @{kMSACOneCollectorApiKey : tokenValue, kMSACOneCollectorTicketsKey : ticketValue}; + NSURL *url = [NSURL new]; + + // When + [ingestionMock willSendHTTPRequestToURL:url withHeaders:headers]; + + // Then + OCMVerify([ingestionMock obfuscateTargetTokens:tokenValue]); + OCMVerify([ingestionMock obfuscateTickets:ticketValue]); + + [mockLogger stopMocking]; + [ingestionMock stopMocking]; +} + +- (void)testObfuscateTargetTokens { + + // If + NSString *testString = @"12345678"; + + // When + NSString *result = [self.sut obfuscateTargetTokens:testString]; + + // Then + XCTAssertTrue([result isEqualToString:@"********"]); + + // If + testString = @"ThisWillBeObfuscated, ThisWillBeObfuscated, ThisWillBeObfuscated"; + + // When + result = [self.sut obfuscateTargetTokens:testString]; + + // Then + XCTAssertTrue([result isEqualToString:@"************fuscated,*************fuscated,*************fuscated"]); +} + +- (void)testObfuscateTickets { + + // If + NSString *testString = @"something"; + + // When + NSString *result = [self.sut obfuscateTickets:testString]; + + // Then + XCTAssertTrue([result isEqualToString:testString]); + + // If + testString = @"{\"ticketKey1\":\"p:AuthorizationValue1\",\"ticketKey2\":\"d:AuthorizationValue2\"}"; + + // When + result = [self.sut obfuscateTickets:testString]; + + // Then + XCTAssertTrue([result isEqualToString:@"{\"ticketKey1\":\"p:***\",\"ticketKey2\":\"d:***\"}"]); +} + +- (void)testGetPayload { + + // If + NSString *containerId = @"1"; + MSACMockCommonSchemaLog *log1 = [[MSACMockCommonSchemaLog alloc] init]; + [log1 addTransmissionTargetToken:@"token1"]; + MSACMockCommonSchemaLog *log2 = [[MSACMockCommonSchemaLog alloc] init]; + [log2 addTransmissionTargetToken:@"token2"]; + MSACLogContainer *logContainer = [[MSACLogContainer alloc] initWithBatchId:containerId andLogs:(NSArray> *)@[ log1, log2 ]]; + + // When + NSData *payload = [self.sut getPayloadWithData:logContainer]; + + // Then + XCTAssertNotNil(payload); + NSString *containerString = + [NSString stringWithFormat:@"%@%@%@%@", [log1 serializeLogWithPrettyPrinting:NO], kMSACOneCollectorLogSeparator, + [log2 serializeLogWithPrettyPrinting:NO], kMSACOneCollectorLogSeparator]; + NSData *httpBodyData = [containerString dataUsingEncoding:NSUTF8StringEncoding]; + XCTAssertEqualObjects(httpBodyData, payload); +} + +- (void)testSendBatchLogs { + + // When + + // Stub http response + [MSACHttpTestUtil stubHttp200Response]; + NSString *containerId = @"1"; + MSACLogContainer *container = [self createLogContainerWithId:containerId]; + __weak XCTestExpectation *expectation = [self expectationWithDescription:@"HTTP Response 200"]; + [self.sut sendAsync:container + completionHandler:^(NSString *batchId, NSHTTPURLResponse *response, __attribute__((unused)) NSData *data, NSError *error) { + XCTAssertNil(error); + XCTAssertEqual(containerId, batchId); + XCTAssertEqual((MSACHTTPCodesNo)response.statusCode, MSACHTTPCodesNo200OK); + [expectation fulfill]; + }]; + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testInvalidContainer { + + // If + __weak XCTestExpectation *expectation = [self expectationWithDescription:@"HTTP Response 200"]; + MSACAbstractLog *log = [MSACAbstractLog new]; + log.sid = MSAC_UUID_STRING; + log.timestamp = [NSDate date]; + + // Log does not have device info, therefore, it's an invalid log. + MSACLogContainer *container = [[MSACLogContainer alloc] initWithBatchId:@"1" andLogs:(NSArray> *)@[ log ]]; + OCMReject([self.httpClientMock sendAsync:OCMOCK_ANY + method:OCMOCK_ANY + headers:OCMOCK_ANY + data:OCMOCK_ANY + retryIntervals:OCMOCK_ANY + compressionEnabled:OCMOCK_ANY + completionHandler:OCMOCK_ANY]); + + // When + [self.sut sendAsync:container + completionHandler:^(__attribute__((unused)) NSString *batchId, __attribute__((unused)) NSHTTPURLResponse *response, + __attribute__((unused)) NSData *data, NSError *error) { + // Then + XCTAssertEqual(error.domain, kMSACACErrorDomain); + XCTAssertEqual(error.code, MSACACLogInvalidContainerErrorCode); + [expectation fulfill]; + }]; + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testNilContainer { + + // If + MSACLogContainer *container = nil; + + // When + __weak XCTestExpectation *expectation = [self expectationWithDescription:@"HTTP Network Down"]; + [self.sut sendAsync:container + completionHandler:^(__attribute__((unused)) NSString *batchId, __attribute__((unused)) NSHTTPURLResponse *response, + __attribute__((unused)) NSData *data, NSError *error) { + // Then + XCTAssertNotNil(error); + [expectation fulfill]; + }]; + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testSetBaseURL { + + // If + NSString *path = @"path"; + NSURL *expectedURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@%@", @"https://www.contoso.com/", path]]; + self.sut.apiPath = path; + + // Query should be the same. + NSString *query = self.sut.sendURL.query; + + // When + [self.sut setBaseURL:(NSString * _Nonnull)[expectedURL.URLByDeletingLastPathComponent absoluteString]]; + + // Then + XCTAssertNil(query); + XCTAssertTrue([[self.sut.sendURL absoluteString] isEqualToString:(NSString * _Nonnull) expectedURL.absoluteString]); +} + +- (void)testSetInvalidBaseURL { + + // If + NSURL *expected = self.sut.sendURL; + NSString *invalidURL = @"\notGood"; + + // When + [self.sut setBaseURL:invalidURL]; + + // Then + assertThat(self.sut.sendURL, is(expected)); +} + +#pragma mark - Test Helpers + +- (MSACLogContainer *)createLogContainerWithId:(NSString *)batchId { + id deviceMock = OCMPartialMock([MSACDevice new]); + OCMStub([deviceMock isValid]).andReturn(YES); + MSACMockCommonSchemaLog *log1 = [[MSACMockCommonSchemaLog alloc] init]; + log1.name = @"log1"; + log1.ver = @"3.0"; + log1.sid = MSAC_UUID_STRING; + log1.timestamp = [NSDate date]; + log1.device = deviceMock; + [log1 addTransmissionTargetToken:@"token1"]; + [log1 addTransmissionTargetToken:@"token2"]; + log1.ext = [MSACModelTestsUtililty extensionsWithDummyValues:[MSACModelTestsUtililty extensionDummies]]; + MSACMockCommonSchemaLog *log2 = [[MSACMockCommonSchemaLog alloc] init]; + log2.name = @"log2"; + log2.ver = @"3.0"; + log2.sid = MSAC_UUID_STRING; + log2.timestamp = [NSDate date]; + log2.device = deviceMock; + [log2 addTransmissionTargetToken:@"token2"]; + [log2 addTransmissionTargetToken:@"token3"]; + log2.ext = [MSACModelTestsUtililty extensionsWithDummyValues:[MSACModelTestsUtililty extensionDummies]]; + MSACLogContainer *logContainer = [[MSACLogContainer alloc] initWithBatchId:batchId andLogs:(NSArray> *)@[ log1, log2 ]]; + return logContainer; +} + +- (void)testHideTokenInResponse { + + // If + id mockUtility = OCMClassMock([MSACUtility class]); + id mockLogger = OCMClassMock([MSACLogger class]); + OCMStub([mockLogger currentLogLevel]).andReturn(MSACLogLevelVerbose); + OCMStub(ClassMethod([mockUtility obfuscateString:OCMOCK_ANY + searchingForPattern:kMSACTokenKeyValuePattern + toReplaceWithTemplate:kMSACTokenKeyValueObfuscatedTemplate])); + NSData *data = [@"{\"token\":\"secrets\"}" dataUsingEncoding:NSUTF8StringEncoding]; + MSACLogContainer *logContainer = [self createLogContainerWithId:@"1"]; + XCTestExpectation *requestCompletedExpectation = [self expectationWithDescription:@"Request completed."]; + + // When + [MSACHttpTestUtil stubResponseWithData:data statusCode:MSACHTTPCodesNo200OK headers:self.sut.httpHeaders name:NSStringFromSelector(_cmd)]; + [self.sut sendAsync:logContainer + completionHandler:^(__unused NSString *batchId, __unused NSHTTPURLResponse *response, __unused NSData *responseData, + __unused NSError *error) { + [requestCompletedExpectation fulfill]; + }]; + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + OCMVerify(ClassMethod([mockUtility obfuscateString:OCMOCK_ANY + searchingForPattern:kMSACTokenKeyValuePattern + toReplaceWithTemplate:kMSACTokenKeyValueObfuscatedTemplate])); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; + + // Clear + [mockUtility stopMocking]; + [mockLogger stopMocking]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACOrderedDictionaryTest.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACOrderedDictionaryTest.m new file mode 100644 index 0000000000..324ff04625 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACOrderedDictionaryTest.m @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACOrderedDictionaryPrivate.h" +#import "MSACTestFrameworks.h" + +@interface MSACOrderedDictionaryTests : XCTestCase + +@property(nonatomic) MSACOrderedDictionary *sut; + +@end + +@implementation MSACOrderedDictionaryTests + +- (void)setUp { + [super setUp]; + + self.sut = [MSACOrderedDictionary new]; +} + +- (void)tearDown { + [super tearDown]; + + [self.sut removeAllObjects]; +} + +- (void)testInitWithCapacity { + + // When + self.sut = [[MSACOrderedDictionary alloc] initWithCapacity:10]; + + // Then + XCTAssertNotNil(self.sut.order); + XCTAssertNotNil(self.sut); +} + +- (void)testCount { + + // When + [self.sut setObject:@"value1" forKey:@"key1"]; + [self.sut setObject:@"value2" forKey:@"key2"]; + + // Then + XCTAssertTrue(self.sut.count == 2); +} + +- (void)testRemoveAll { + + // If + [self.sut setObject:@"value1" forKey:@"key1"]; + [self.sut setObject:@"value2" forKey:@"key2"]; + + // When + [self.sut removeAllObjects]; + + // Then + XCTAssertTrue(self.sut.count == 0); +} + +- (void)testAddingOrderedObjects { + + // When + [self.sut setObject:@"value1" forKey:@"key1"]; + [self.sut setObject:@"value2" forKey:@"key2"]; + + // Then + NSEnumerator *keyEnumerator = [self.sut keyEnumerator]; + XCTAssertTrue(self.sut.count == 2); + XCTAssertTrue([[keyEnumerator nextObject] isEqualToString:@"key1"]); + XCTAssertTrue([[keyEnumerator nextObject] isEqualToString:@"key2"]); + XCTAssertNil([keyEnumerator nextObject]); + XCTAssertEqual([self.sut objectForKey:@"key1"], @"value1"); + XCTAssertEqual([self.sut objectForKey:@"key2"], @"value2"); +} + +- (void)testEmptyDictionariesAreEqual { + + // If + MSACOrderedDictionary *other = [MSACOrderedDictionary new]; + + // Then + XCTAssertTrue([self.sut isEqualToDictionary:other]); +} + +- (void)testDifferentLengthDictionariesNotEqual { + + // If + MSACOrderedDictionary *other = [MSACOrderedDictionary new]; + [other setObject:@"value" forKey:@"key"]; + + // Then + XCTAssertFalse([self.sut isEqualToDictionary:other]); +} + +- (void)testDifferentKeyOrdersNotEqual { + + // If + MSACOrderedDictionary *other = [MSACOrderedDictionary new]; + [other setObject:@"value1" forKey:@"key1"]; + [other setObject:@"value2" forKey:@"key2"]; + + // When + [self.sut setObject:@"value2" forKey:@"key2"]; + [self.sut setObject:@"value1" forKey:@"key1"]; + + // Then + XCTAssertFalse([self.sut isEqualToDictionary:other]); +} + +- (void)testDifferentValuesForKeysNotEqual { + + // If + MSACOrderedDictionary *other = [MSACOrderedDictionary new]; + [other setObject:@"value1" forKey:@"key1"]; + [other setObject:@"value2" forKey:@"key2"]; + + // When + [self.sut setObject:@"value1" forKey:@"key2"]; + [self.sut setObject:@"value2" forKey:@"key1"]; + + // Then + XCTAssertFalse([self.sut isEqualToDictionary:other]); +} + +- (void)testEqualDictionaries { + + // If + MSACOrderedDictionary *other = [MSACOrderedDictionary new]; + [other setObject:@"value1" forKey:@"key1"]; + [other setObject:@"value2" forKey:@"key2"]; + + // When + [self.sut setObject:@"value1" forKey:@"key1"]; + [self.sut setObject:@"value2" forKey:@"key2"]; + + // Then + XCTAssertTrue([self.sut isEqualToDictionary:other]); +} + +- (void)testCopiedDictionariesEqual { + + // When + [self.sut setObject:@"value1" forKey:@"key1"]; + [self.sut setObject:@"value2" forKey:@"key2"]; + MSACOrderedDictionary *other = [self.sut mutableCopy]; + + // Then + XCTAssertTrue([self.sut isEqualToDictionary:other]); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACReachabilityTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACReachabilityTests.m new file mode 100644 index 0000000000..f86dfdcf57 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACReachabilityTests.m @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACTestFrameworks.h" +#import "MSAC_Reachability.h" + +@interface MSACReachabilityTests : XCTestCase +@end + +@implementation MSACReachabilityTests + +- (void)testRaceConditionOnDealloc { + + // If + XCTestExpectation *expectation = [self expectationWithDescription:@"Reachability deallocated."]; + + // When + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + MSAC_Reachability *reachability = [MSAC_Reachability reachabilityForInternetConnection]; + reachability = nil; + }); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + // Arbitrary wait for reachability dealocation so if a EXC_BAD_ACCESS happens it has a chance to happen in this test. + [NSThread sleepForTimeInterval:0.1]; + [expectation fulfill]; + }); + + // Then + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACServiceAbstractTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACServiceAbstractTests.m new file mode 100644 index 0000000000..abc8844fa6 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACServiceAbstractTests.m @@ -0,0 +1,322 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAppCenter.h" +#import "MSACAppCenterInternal.h" +#import "MSACAppCenterPrivate.h" +#import "MSACChannelGroupDefault.h" +#import "MSACChannelUnitConfiguration.h" +#import "MSACConstants+Internal.h" +#import "MSACMockUserDefaults.h" +#import "MSACSessionContextPrivate.h" +#import "MSACTestFrameworks.h" + +@interface MSACServiceAbstractImplementation : MSACServiceAbstract + +@end + +@implementation MSACServiceAbstractImplementation + +@synthesize channelUnitConfiguration = _channelUnitConfiguration; + ++ (instancetype)sharedInstance { + static id sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[self alloc] init]; + }); + return sharedInstance; +} + +- (instancetype)init { + if ((self = [super init])) { + _channelUnitConfiguration = [[MSACChannelUnitConfiguration alloc] initWithGroupId:[self groupId] + priority:MSACPriorityDefault + flushInterval:3.0 + batchSizeLimit:50 + pendingBatchesLimit:3]; + } + return self; +} + ++ (NSString *)serviceName { + return @"Service"; +} + +- (void)startWithChannelGroup:(id)channelGroup appSecret:(NSString *)appSecret { + [super startWithChannelGroup:channelGroup appSecret:appSecret transmissionTargetToken:nil fromApplication:YES]; +} + +- (MSACInitializationPriority)initializationPriority { + return MSACInitializationPriorityDefault; +} + ++ (NSString *)logTag { + return @"MSServiceAbstractTest"; +} + +- (NSString *)groupId { + return @"groupId"; +} + +@end + +@interface MSACServiceAbstractTest : XCTestCase + +@property(nonatomic) id settingsMock; +@property(nonatomic) id sessionContextMock; +@property(nonatomic) id channelGroupMock; +@property(nonatomic) id channelUnitMock; + +/** + * System Under test. + */ +@property(nonatomic) MSACServiceAbstractImplementation *abstractService; + +@end + +@implementation MSACServiceAbstractTest + +- (void)setUp { + [super setUp]; + [MSACAppCenter resetSharedInstance]; + + // Set up the mocked storage. + self.settingsMock = [MSACMockUserDefaults new]; + + // Session context. + [MSACSessionContext resetSharedInstance]; + self.sessionContextMock = OCMClassMock([MSACSessionContext class]); + OCMStub([self.sessionContextMock sharedInstance]).andReturn(self.sessionContextMock); + + // Set up the mock channel. + self.channelGroupMock = OCMClassMock([MSACChannelGroupDefault class]); + self.channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([self.channelGroupMock alloc]).andReturn(self.channelGroupMock); + OCMStub([self.channelGroupMock initWithHttpClient:OCMOCK_ANY installId:OCMOCK_ANY logUrl:OCMOCK_ANY]).andReturn(self.channelGroupMock); + OCMStub([self.channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY]).andReturn(self.channelUnitMock); + + // System Under Test. + self.abstractService = [MSACServiceAbstractImplementation new]; +} + +- (void)tearDown { + [self.channelGroupMock stopMocking]; + [self.settingsMock stopMocking]; + [self.sessionContextMock stopMocking]; + [MSACAppCenter resetSharedInstance]; + [MSACSessionContext resetSharedInstance]; + [super tearDown]; +} + +- (void)testIsEnabledTrueByDefault { + + // When + BOOL isEnabled = [self.abstractService isEnabled]; + + // Then + XCTAssertTrue(isEnabled); +} + +- (void)testDisableService { + + // If + [self.settingsMock setObject:@YES forKey:self.abstractService.isEnabledKey]; + + // When + [self.abstractService setEnabled:NO]; + + // Then + XCTAssertFalse([self.abstractService isEnabled]); +} + +- (void)testEnableService { + + // If + [self.settingsMock setObject:@NO forKey:self.abstractService.isEnabledKey]; + + // When + [self.abstractService setEnabled:YES]; + + // Then + XCTAssertTrue([self.abstractService isEnabled]); +} + +- (void)testDisableServiceOnServiceDisabled { + + // If + [self.settingsMock setObject:@NO forKey:self.abstractService.isEnabledKey]; + + // When + [self.abstractService setEnabled:NO]; + + // Then + XCTAssertFalse([self.abstractService isEnabled]); +} + +- (void)testEnableServiceOnServiceEnabled { + + // If + [self.settingsMock setObject:@YES forKey:self.abstractService.isEnabledKey]; + + // When + [self.abstractService setEnabled:YES]; + + // Then + XCTAssertTrue([self.abstractService isEnabled]); +} + +- (void)testIsEnabledToPersistence { + + // If + BOOL expected = NO; + + // When + [self.abstractService setEnabled:expected]; + + // Then + XCTAssertTrue(self.abstractService.isEnabled == expected); + + // Also check that the sut did access the persistence. + XCTAssertTrue([[self.settingsMock objectForKey:self.abstractService.isEnabledKey] boolValue] == expected); +} + +- (void)testIsEnabledFromPersistence { + + // If + [self.settingsMock setObject:@NO forKey:self.abstractService.isEnabledKey]; + + // Then + XCTAssertFalse([self.abstractService isEnabled]); + + // If + [self.settingsMock setObject:@YES forKey:self.abstractService.isEnabledKey]; + + // Then + XCTAssertTrue([self.abstractService isEnabled]); +} + +- (void)testCanBeUsed { + + // If + [MSACAppCenter resetSharedInstance]; + + // Then + XCTAssertFalse([[MSACServiceAbstractImplementation sharedInstance] canBeUsed]); + + // When + [MSACAppCenter start:MSAC_UUID_STRING withServices:@ [[MSACServiceAbstractImplementation class]]]; + + // Then + XCTAssertTrue([[MSACServiceAbstractImplementation sharedInstance] canBeUsed]); +} + +- (void)testEnableServiceOnCoreDisabled { + + // If + [MSACAppCenter resetSharedInstance]; + [self.settingsMock setObject:@NO forKey:kMSACAppCenterIsEnabledKey]; + [self.settingsMock setObject:@NO forKey:self.abstractService.isEnabledKey]; + [MSACAppCenter start:MSAC_UUID_STRING withServices:@ [[MSACServiceAbstractImplementation class]]]; + + // When + [[MSACServiceAbstractImplementation class] setEnabled:YES]; + + // Then + XCTAssertFalse([[MSACServiceAbstractImplementation class] isEnabled]); +} + +- (void)testDisableServiceOnCoreEnabled { + + // If + [MSACAppCenter resetSharedInstance]; + [self.settingsMock setObject:@YES forKey:kMSACAppCenterIsEnabledKey]; + [self.settingsMock setObject:@YES forKey:self.abstractService.isEnabledKey]; + [MSACAppCenter start:MSAC_UUID_STRING withServices:@ [[MSACServiceAbstractImplementation class]]]; + + // When + [[MSACServiceAbstractImplementation class] setEnabled:NO]; + + // Then + XCTAssertFalse([[MSACServiceAbstractImplementation class] isEnabled]); +} + +- (void)testEnableServiceOnCoreEnabled { + + // If + [MSACAppCenter resetSharedInstance]; + [self.settingsMock setObject:@YES forKey:kMSACAppCenterIsEnabledKey]; + [self.settingsMock setObject:@NO forKey:self.abstractService.isEnabledKey]; + [MSACAppCenter start:MSAC_UUID_STRING withServices:@ [[MSACServiceAbstractImplementation class]]]; + + // When + [[MSACServiceAbstractImplementation class] setEnabled:YES]; + + // Then + XCTAssertTrue([[MSACServiceAbstractImplementation class] isEnabled]); +} + +- (void)testReenableCoreOnServiceDisabled { + + // If + [self.settingsMock setObject:@YES forKey:kMSACAppCenterIsEnabledKey]; + [self.settingsMock setObject:@NO forKey:self.abstractService.isEnabledKey]; + [MSACAppCenter start:MSAC_UUID_STRING withServices:@ [[MSACServiceAbstractImplementation class]]]; + + // When + [MSACAppCenter setEnabled:YES]; + + // Then + XCTAssertTrue([[MSACServiceAbstractImplementation class] isEnabled]); +} + +- (void)testReenableCoreOnServiceEnabled { + + // If + [self.settingsMock setObject:@YES forKey:kMSACAppCenterIsEnabledKey]; + [self.settingsMock setObject:@YES forKey:self.abstractService.isEnabledKey]; + [MSACAppCenter start:MSAC_UUID_STRING withServices:@ [[MSACServiceAbstractImplementation class]]]; + + // When + [MSACAppCenter setEnabled:YES]; + + // Then + XCTAssertTrue([[MSACServiceAbstractImplementation class] isEnabled]); +} + +- (void)testLogDeletedOnDisabled { + + // If + self.abstractService.channelGroup = self.channelGroupMock; + self.abstractService.channelUnit = self.channelUnitMock; + [self.settingsMock setObject:@YES forKey:self.abstractService.isEnabledKey]; + + // When + [self.abstractService setEnabled:NO]; + + // Then + // Check that log deletion has been triggered. + OCMVerify([self.channelUnitMock setEnabled:NO andDeleteDataOnDisabled:YES]); + + // GroupId from the service must match the groupId used to delete logs. + XCTAssertTrue(self.abstractService.channelUnitConfiguration.groupId == self.abstractService.groupId); +} + +- (void)testEnableChannelUnitOnStartWithChannelGroup { + + // When + [self.abstractService startWithChannelGroup:self.channelGroupMock appSecret:@"TestAppSecret"]; + + // Then + OCMVerify([self.channelUnitMock setEnabled:YES andDeleteDataOnDisabled:YES]); +} + +- (void)testInitializationPriorityCorrect { + XCTAssertTrue([self.abstractService initializationPriority] == MSACInitializationPriorityDefault); +} + +- (void)testAppSecretRequiredByDefault { + XCTAssertTrue([self.abstractService isAppSecretRequired]); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACSessionContextTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACSessionContextTests.m new file mode 100644 index 0000000000..30ad5f7589 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACSessionContextTests.m @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACMockUserDefaults.h" +#import "MSACSessionContextPrivate.h" +#import "MSACTestFrameworks.h" +#import "MSACUtility.h" + +@interface MSACSessionContextTests : XCTestCase + +@property(nonatomic) MSACSessionContext *sut; +@property(nonatomic) MSACMockUserDefaults *settingsMock; + +@end + +@implementation MSACSessionContextTests + +#pragma mark - Houskeeping + +- (void)setUp { + [super setUp]; + [MSACSessionContext resetSharedInstance]; + + self.settingsMock = [MSACMockUserDefaults new]; + self.sut = [MSACSessionContext sharedInstance]; +} + +- (void)tearDown { + [MSACSessionContext resetSharedInstance]; + [self.settingsMock stopMocking]; + [super tearDown]; +} + +#pragma mark - Tests + +- (void)testSetSessionId { + + // If + NSString *expectedSessionId = @"Session"; + + // When + [self.sut setSessionId:expectedSessionId]; + + // Then + NSData *data = [self.settingsMock objectForKey:@"SessionIdHistory"]; + XCTAssertNotNil(data); + NSMutableArray *savedData = (NSMutableArray *)[[MSACUtility unarchiveKeyedData:data] mutableCopy]; + XCTAssertEqualObjects([savedData[0] sessionId], expectedSessionId); +} + +- (void)testClearSessionHistory { + + // When + [self.sut setSessionId:@"Session1"]; + [MSACSessionContext resetSharedInstance]; + self.sut = [MSACSessionContext sharedInstance]; + [self.sut setSessionId:@"Session2"]; + + // Then + NSData *data = [self.settingsMock objectForKey:@"SessionIdHistory"]; + XCTAssertNotNil(data); + NSMutableArray *savedData = (NSMutableArray *)[[MSACUtility unarchiveKeyedData:data] mutableCopy]; + XCTAssertEqual([savedData count], 2); + + // When + [self.sut clearSessionHistoryAndKeepCurrentSession:NO]; + + // Then + data = [self.settingsMock objectForKey:@"SessionIdHistory"]; + XCTAssertNotNil(data); + + // Should keep the current session. + savedData = (NSMutableArray *)[[MSACUtility unarchiveKeyedData:data] mutableCopy]; + XCTAssertEqual([savedData count], 0); +} + +- (void)testClearSessionHistoryExceptCurrentOne { + + // When + [self.sut setSessionId:@"Session1"]; + [MSACSessionContext resetSharedInstance]; + self.sut = [MSACSessionContext sharedInstance]; + [self.sut setSessionId:@"Session2"]; + + // Then + NSData *data = [self.settingsMock objectForKey:@"SessionIdHistory"]; + XCTAssertNotNil(data); + NSMutableArray *savedData = (NSMutableArray *)[[MSACUtility unarchiveKeyedData:data] mutableCopy]; + XCTAssertEqual([savedData count], 2); + + // When + [self.sut clearSessionHistoryAndKeepCurrentSession:YES]; + + // Then + data = [self.settingsMock objectForKey:@"SessionIdHistory"]; + XCTAssertNotNil(data); + + // Should keep the current session. + savedData = (NSMutableArray *)[[MSACUtility unarchiveKeyedData:data] mutableCopy]; + XCTAssertEqual([savedData count], 1); +} + +- (void)testSessionId { + + // If + NSString *expectedSessionId = @"Session"; + + // When + [self.sut setSessionId:expectedSessionId]; + + // Then + XCTAssertEqualObjects(expectedSessionId, [self.sut sessionId]); +} + +- (void)testSessionIdAt { + + // If + __block NSDate *date; + id dateMock = OCMClassMock([NSDate class]); + + // When + OCMStub(ClassMethod([dateMock date])).andDo(^(NSInvocation *invocation) { + date = [[NSDate alloc] initWithTimeIntervalSince1970:0]; + [invocation setReturnValue:&date]; + }); + [self.sut setSessionId:@"Session1"]; + [dateMock stopMocking]; + + [MSACSessionContext resetSharedInstance]; + self.sut = [MSACSessionContext sharedInstance]; + + dateMock = OCMClassMock([NSDate class]); + OCMStub(ClassMethod([dateMock date])).andDo(^(NSInvocation *invocation) { + date = [[NSDate alloc] initWithTimeIntervalSince1970:1000]; + [invocation setReturnValue:&date]; + }); + [self.sut setSessionId:@"Session2"]; + [dateMock stopMocking]; + + [MSACSessionContext resetSharedInstance]; + self.sut = [MSACSessionContext sharedInstance]; + + dateMock = OCMClassMock([NSDate class]); + OCMStub(ClassMethod([dateMock date])).andDo(^(NSInvocation *invocation) { + date = [[NSDate alloc] initWithTimeIntervalSince1970:2000]; + [invocation setReturnValue:&date]; + }); + [self.sut setSessionId:@"Session3"]; + [dateMock stopMocking]; + + [MSACSessionContext resetSharedInstance]; + self.sut = [MSACSessionContext sharedInstance]; + + dateMock = OCMClassMock([NSDate class]); + OCMStub(ClassMethod([dateMock date])).andDo(^(NSInvocation *invocation) { + date = [[NSDate alloc] initWithTimeIntervalSince1970:3000]; + [invocation setReturnValue:&date]; + }); + [self.sut setSessionId:@"Session4"]; + [dateMock stopMocking]; + + [MSACSessionContext resetSharedInstance]; + self.sut = [MSACSessionContext sharedInstance]; + + dateMock = OCMClassMock([NSDate class]); + OCMStub(ClassMethod([dateMock date])).andDo(^(NSInvocation *invocation) { + date = [[NSDate alloc] initWithTimeIntervalSince1970:4000]; + [invocation setReturnValue:&date]; + }); + [self.sut setSessionId:@"Session5"]; + [dateMock stopMocking]; + + // Then + XCTAssertNil([self.sut sessionIdAt:[[NSDate alloc] initWithTimeIntervalSince1970:0]]); + XCTAssertEqualObjects(@"Session3", [self.sut sessionIdAt:[[NSDate alloc] initWithTimeIntervalSince1970:2500]]); + XCTAssertEqualObjects(@"Session5", [self.sut sessionIdAt:[[NSDate alloc] initWithTimeIntervalSince1970:5000]]); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACStartServiceLogTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACStartServiceLogTests.m new file mode 100644 index 0000000000..22175982c6 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACStartServiceLogTests.m @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACStartServiceLog.h" +#import "MSACTestFrameworks.h" +#import "MSACUtility.h" + +@interface MSACStartServiceLogTests : XCTestCase + +@property(nonatomic, strong) MSACStartServiceLog *sut; + +@end + +@implementation MSACStartServiceLogTests + +@synthesize sut = _sut; + +#pragma mark - Setup + +- (void)setUp { + [super setUp]; + self.sut = [MSACStartServiceLog new]; +} + +#pragma mark - Tests + +- (void)testSerializingEventToDictionaryWorks { + + // If + NSArray *services = @[ @"Service0", @"Service1", @"Service2" ]; + self.sut.services = services; + + // When + NSMutableDictionary *actual = [self.sut serializeToDictionary]; + + // Then + assertThat(actual, notNilValue()); + NSArray *actualServices = actual[@"services"]; + XCTAssertEqual(actualServices.count, services.count); + for (NSUInteger i = 0; i < actualServices.count; ++i) { + assertThat(actualServices[i], equalTo(services[i])); + } +} + +- (void)testNSCodingSerializationAndDeserializationWorks { + + // If + NSArray *services = @[ @"Service0", @"Service1", @"Service2" ]; + self.sut.services = services; + + // When + NSData *serializedLog = [MSACUtility archiveKeyedData:self.sut]; + id actual = [MSACUtility unarchiveKeyedData:serializedLog]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual, instanceOf([MSACStartServiceLog class])); + XCTAssertTrue([actual isEqual:self.sut]); + + MSACStartServiceLog *log = actual; + NSArray *actualServices = log.services; + XCTAssertEqual(actualServices.count, services.count); + for (NSUInteger i = 0; i < actualServices.count; ++i) { + assertThat(actualServices[i], equalTo(services[i])); + } +} + +- (void)testIsNotEqual { + + // Then + XCTAssertFalse([self.sut isEqual:[MSACAbstractLog new]]); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACStoragePerformanceTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACStoragePerformanceTests.m new file mode 100644 index 0000000000..a2f2026ea3 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACStoragePerformanceTests.m @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACLogDBStorage.h" +#import "MSACStartServiceLog.h" +#import "MSACTestFrameworks.h" + +static const int kMSACNumLogs = 50; +static const int kMSACNumServices = 5; +static NSString *const kMSACTestGroupId = @"TestGroupId"; + +@interface MSACStoragePerformanceTests : XCTestCase +@end + +@interface MSACStoragePerformanceTests () + +@property(nonatomic) MSACLogDBStorage *dbStorage; + +@end + +@implementation MSACStoragePerformanceTests + +@synthesize dbStorage; + +- (void)setUp { + [super setUp]; + self.dbStorage = [MSACLogDBStorage new]; +} + +- (void)tearDown { + [self.dbStorage deleteLogsWithGroupId:kMSACTestGroupId]; + [super tearDown]; +} + +#pragma mark - Database storage tests + +- (void)testDatabaseWriteShortLogsPerformance { + NSArray *arrayOfLogs = [self generateLogsWithShortServicesNames:kMSACNumLogs withNumService:kMSACNumServices]; + [self measureBlock:^{ + for (MSACStartServiceLog *log in arrayOfLogs) { + [self.dbStorage saveLog:log withGroupId:kMSACTestGroupId flags:MSACFlagsDefault]; + } + }]; +} + +- (void)testDatabaseWriteLongLogsPerformance { + NSArray *arrayOfLogs = [self generateLogsWithLongServicesNames:kMSACNumLogs withNumService:kMSACNumServices]; + [self measureBlock:^{ + for (MSACStartServiceLog *log in arrayOfLogs) { + [self.dbStorage saveLog:log withGroupId:kMSACTestGroupId flags:MSACFlagsDefault]; + } + }]; +} + +- (void)testDatabaseWriteVeryLongLogsPerformance { + NSArray *arrayOfLogs = [self generateLogsWithVeryLongServicesNames:kMSACNumLogs withNumService:kMSACNumServices]; + [self measureBlock:^{ + for (MSACStartServiceLog *log in arrayOfLogs) { + [self.dbStorage saveLog:log withGroupId:kMSACTestGroupId flags:MSACFlagsDefault]; + } + }]; +} + +#pragma mark - File storage tests + +- (void)testFileStorageWriteShortLogsPerformance { + NSArray *arrayOfLogs = [self generateLogsWithShortServicesNames:kMSACNumLogs withNumService:kMSACNumServices]; + [self measureBlock:^{ + for (MSACStartServiceLog *log in arrayOfLogs) { + [self.dbStorage saveLog:log withGroupId:kMSACTestGroupId flags:MSACFlagsDefault]; + } + }]; +} + +- (void)testFileStorageWriteLongLogsPerformance { + NSArray *arrayOfLogs = [self generateLogsWithLongServicesNames:kMSACNumLogs withNumService:kMSACNumServices]; + [self measureBlock:^{ + for (MSACStartServiceLog *log in arrayOfLogs) { + [self.dbStorage saveLog:log withGroupId:kMSACTestGroupId flags:MSACFlagsDefault]; + } + }]; +} + +- (void)testFileStorageWriteVeryLongLogsPerformance { + NSArray *arrayOfLogs = [self generateLogsWithVeryLongServicesNames:kMSACNumLogs withNumService:kMSACNumServices]; + [self measureBlock:^{ + for (MSACStartServiceLog *log in arrayOfLogs) { + [self.dbStorage saveLog:log withGroupId:kMSACTestGroupId flags:MSACFlagsDefault]; + } + }]; +} + +#pragma mark - Private + +- (NSArray *)generateLogsWithShortServicesNames:(int)numLogs withNumService:(int)numServices { + NSMutableArray *dic = [NSMutableArray new]; + for (int i = 0; i < numLogs; ++i) { + MSACStartServiceLog *log = [MSACStartServiceLog new]; + log.services = [self generateServicesWithShortNames:numServices]; + [dic addObject:log]; + } + return dic; +} + +- (NSArray *)generateLogsWithLongServicesNames:(int)numLogs withNumService:(int)numServices { + NSMutableArray *dic = [NSMutableArray new]; + for (int i = 0; i < numLogs; ++i) { + MSACStartServiceLog *log = [MSACStartServiceLog new]; + log.services = [self generateServicesWithLongNames:numServices]; + [dic addObject:log]; + } + return dic; +} + +- (NSArray *)generateLogsWithVeryLongServicesNames:(int)numLogs withNumService:(int)numServices { + NSMutableArray *dic = [NSMutableArray new]; + for (int i = 0; i < numLogs; ++i) { + MSACStartServiceLog *log = [MSACStartServiceLog new]; + log.services = [self generateServicesWithVeryLongNames:numServices]; + [dic addObject:log]; + } + return dic; +} + +- (NSArray *)generateServicesWithShortNames:(int)numServices { + NSMutableArray *dic = [NSMutableArray new]; + for (int i = 0; i < numServices; ++i) { + [dic addObject:[[NSUUID UUID] UUIDString]]; + } + return dic; +} + +- (NSArray *)generateServicesWithLongNames:(int)numServices { + NSMutableArray *dic = [NSMutableArray new]; + for (int i = 0; i < numServices; ++i) { + NSString *value = @""; + for (int j = 0; j < 10; ++j) { + value = [value stringByAppendingString:[[NSUUID UUID] UUIDString]]; + } + [dic addObject:value]; + } + return dic; +} + +- (NSArray *)generateServicesWithVeryLongNames:(int)numServices { + NSMutableArray *dic = [NSMutableArray new]; + for (int i = 0; i < numServices; ++i) { + NSString *value = @""; + for (int j = 0; j < 50; ++j) { + value = [value stringByAppendingString:[[NSUUID UUID] UUIDString]]; + } + [dic addObject:value]; + } + return dic; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACStringTypedPropertyTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACStringTypedPropertyTests.m new file mode 100644 index 0000000000..1e8a345ad7 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACStringTypedPropertyTests.m @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACStringTypedProperty.h" +#import "MSACTestFrameworks.h" +#import "MSACUtility.h" + +@interface MSACStringTypedPropertyTests : XCTestCase + +@end + +@implementation MSACStringTypedPropertyTests + +- (void)testNSCodingSerializationAndDeserialization { + + // If + MSACStringTypedProperty *sut = [MSACStringTypedProperty new]; + sut.type = @"type"; + sut.name = @"name"; + sut.value = @"value"; + + // When + NSData *serializedProperty = [MSACUtility archiveKeyedData:sut]; + MSACStringTypedProperty *actual = (MSACStringTypedProperty *)[MSACUtility unarchiveKeyedData:serializedProperty]; + + // Then + XCTAssertNotNil(actual); + XCTAssertTrue([actual isKindOfClass:[MSACStringTypedProperty class]]); + XCTAssertEqualObjects(actual.name, sut.name); + XCTAssertEqualObjects(actual.type, sut.type); + XCTAssertEqualObjects(actual.value, sut.value); +} + +- (void)testSerializeToDictionary { + + // If + MSACStringTypedProperty *sut = [MSACStringTypedProperty new]; + sut.name = @"propertyName"; + sut.value = @"value"; + + // When + NSDictionary *dictionary = [sut serializeToDictionary]; + + // Then + XCTAssertEqualObjects(dictionary[@"type"], sut.type); + XCTAssertEqualObjects(dictionary[@"name"], sut.name); + XCTAssertEqualObjects(dictionary[@"value"], sut.value); +} + +- (void)testPropertyTypeIsCorrectWhenPropertyIsInitialized { + + // If + MSACStringTypedProperty *sut = [MSACStringTypedProperty new]; + + // Then + XCTAssertEqualObjects(sut.type, @"string"); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACTicketCacheTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACTicketCacheTests.m new file mode 100644 index 0000000000..dbf971c4c4 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACTicketCacheTests.m @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACTestFrameworks.h" +#import "MSACTicketCache.h" + +@interface MSACTicketCacheTests : XCTestCase + +@property(nonatomic) MSACTicketCache *sut; + +@end + +@implementation MSACTicketCacheTests + +- (void)setUp { + [super setUp]; + + self.sut = [MSACTicketCache sharedInstance]; +} + +- (void)tearDown { + [super tearDown]; + + [self.sut clearCache]; +} + +- (void)testInitialization { + + // When + + // Then + XCTAssertNotNil(self.sut); + XCTAssertEqual([MSACTicketCache sharedInstance], [MSACTicketCache sharedInstance]); + XCTAssertNotNil(self.sut.tickets); + XCTAssertTrue(self.sut.tickets.count == 0); +} + +- (void)testAddingTickets { + + // When + [self.sut setTicket:@"ticket1" forKey:@"ticketKey1"]; + + // Then + XCTAssertTrue(self.sut.tickets.count == 1); + + // When + [self.sut setTicket:@"ticket2" forKey:@"ticketKey2"]; + + // Then + XCTAssertTrue(self.sut.tickets.count == 2); + XCTAssertTrue([[self.sut ticketFor:@"ticketKey1"] isEqualToString:@"ticket1"]); + XCTAssertTrue([[self.sut ticketFor:@"ticketKey2"] isEqualToString:@"ticket2"]); + XCTAssertNil([self.sut ticketFor:@"foo"]); +} + +- (void)testClearingTickets { + + // If + [self.sut setTicket:@"ticket1" forKey:@"ticketKey1"]; + [self.sut setTicket:@"ticket2" forKey:@"ticketKey2"]; + [self.sut setTicket:@"ticket3" forKey:@"ticketKey3"]; + + // When + [self.sut clearCache]; + + // Then + XCTAssertTrue(self.sut.tickets.count == 0); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACTypedPropertyTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACTypedPropertyTests.m new file mode 100644 index 0000000000..37d6e18237 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACTypedPropertyTests.m @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACBooleanTypedProperty.h" +#import "MSACTestFrameworks.h" +#import "MSACUtility.h" + +@interface MSACTypedPropertyTests : XCTestCase + +@end + +@implementation MSACTypedPropertyTests + +- (void)testNSCodingSerializationAndDeserialization { + + // If + NSString *propertyType = @"propertyType"; + NSString *propertyName = @"propertyName"; + MSACTypedProperty *sut = [MSACTypedProperty new]; + sut.type = propertyType; + sut.name = propertyName; + + // When + NSData *serializedProperty = [MSACUtility archiveKeyedData:sut]; + MSACTypedProperty *actual = (MSACTypedProperty *)[MSACUtility unarchiveKeyedData:serializedProperty]; + + // Then + XCTAssertNotNil(actual); + XCTAssertTrue([actual isKindOfClass:[MSACTypedProperty class]]); + XCTAssertEqualObjects(actual.name, propertyName); + XCTAssertEqualObjects(actual.type, propertyType); +} + +- (void)testSerializingTypedPropertyToDictionary { + + // If + NSString *propertyType = @"propertyType"; + NSString *propertyName = @"propertyName"; + MSACTypedProperty *sut = [MSACTypedProperty new]; + sut.type = propertyType; + sut.name = propertyName; + + // When + NSMutableDictionary *actual = [sut serializeToDictionary]; + + // Then + XCTAssertEqualObjects(actual[@"type"], sut.type); + XCTAssertEqualObjects(actual[@"name"], sut.name); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACUserDefaultsTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACUserDefaultsTests.m new file mode 100644 index 0000000000..4c48707791 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACUserDefaultsTests.m @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAppCenterUserDefaults.h" +#import "MSACAppCenterUserDefaultsPrivate.h" +#import "MSACLoggerInternal.h" +#import "MSACTestFrameworks.h" +#import "MSACUtility.h" +#import "MSACWrapperLogger.h" + +@interface MSACUserDefaultsTests : XCTestCase + +@end + +static NSString *const kMSACAppCenterUserDefaultsMigratedKey = @"MSAppCenter310AppCenterUserDefaultsMigratedKey"; + +@implementation MSACUserDefaultsTests + +- (void)setUp { + for (NSString *key in [[NSUserDefaults standardUserDefaults] dictionaryRepresentation]) { + [[NSUserDefaults standardUserDefaults] removeObjectForKey:key]; + } + [MSACAppCenterUserDefaults resetSharedInstance]; +} + +- (void)testSettingsAlreadyMigrated { + + // If + NSString *testValue = @"testValue"; + [[NSUserDefaults standardUserDefaults] setObject:testValue forKey:@"pastDevicesKey"]; + [[NSUserDefaults standardUserDefaults] setObject:@YES forKey:kMSACAppCenterUserDefaultsMigratedKey]; + + // When + [MSACAppCenterUserDefaults shared]; + + // Then + XCTAssertNil([[NSUserDefaults standardUserDefaults] objectForKey:@"MSAppCenterPastDevices"]); +} + +- (void)testPrefixIsAppendedOnSetAndGet { + + // If + NSString *value = @"testValue"; + NSString *key = @"testKey"; + + // When + MSACAppCenterUserDefaults *userDefaults = [MSACAppCenterUserDefaults shared]; + [userDefaults setObject:value forKey:key]; + + // Then + XCTAssertEqual(value, [[NSUserDefaults standardUserDefaults] objectForKey:[kMSACUserDefaultsPrefix stringByAppendingString:key]]); + XCTAssertNil([[NSUserDefaults standardUserDefaults] objectForKey:key]); + XCTAssertEqual(value, [userDefaults objectForKey:key]); + + // When + [userDefaults removeObjectForKey:key]; + + // Then + XCTAssertNil([[NSUserDefaults standardUserDefaults] objectForKey:[kMSACUserDefaultsPrefix stringByAppendingString:key]]); +} + +- (void)testMigrateUserDefaultSettings { + NSArray *suffixes = @[ @"-suffix1", @"/suffix2", @"suffix3" ]; + NSString *wildcard = @"okeyTestWildcard"; + NSString *expectedWildcard = @"MSAppCenterOkeyTestWildcard"; + + // If + NSDictionary *keys = @{ + @"MSAppCenterKeyTest1" : @"okeyTest1", + @"MSAppCenterKeyTest2" : @"okeyTest2", + @"MSAppCenterKeyTest3" : @"okeyTest3", + @"MSAppCenterKeyTest4" : @"okeyTest4", + expectedWildcard : MSACPrefixKeyFrom(wildcard) + }; + MSACAppCenterUserDefaults *userDefaults = [MSACAppCenterUserDefaults shared]; + NSMutableArray *expectedKeysArray = [[keys allKeys] mutableCopy]; + NSMutableArray *oldKeysArray = [[keys allValues] mutableCopy]; + for (NSString *suffix in suffixes) { + [expectedKeysArray addObject:[expectedWildcard stringByAppendingString:suffix]]; + [oldKeysArray addObject:[wildcard stringByAppendingString:suffix]]; + } + for (NSUInteger i = 0; i < [keys count]; i++) { + if ([oldKeysArray[i] isKindOfClass:[MSACUserDefaultsPrefixKey class]]) { + continue; + } + [[NSUserDefaults standardUserDefaults] setObject:[NSString stringWithFormat:@"Test %tu", i] forKey:oldKeysArray[i]]; + } + for (NSString *suffix in suffixes) { + [[NSUserDefaults standardUserDefaults] setObject:[NSString stringWithFormat:@"Test %@", suffix] + forKey:[wildcard stringByAppendingString:suffix]]; + } + + // Check that in MSACUserDefaultsTest the same keys. + NSArray *userDefaultKeys = [[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys]; + for (NSString *oldKey in oldKeysArray) { + if ([oldKey isKindOfClass:[MSACUserDefaultsPrefixKey class]]) { + continue; + } + XCTAssertTrue([userDefaultKeys containsObject:oldKey]); + } + XCTAssertFalse([userDefaultKeys containsObject:expectedKeysArray]); + + // When + [[NSUserDefaults standardUserDefaults] removeObjectForKey:kMSACAppCenterUserDefaultsMigratedKey]; + [userDefaults migrateKeys:keys forService:@"AppCenter"]; + + // Then + userDefaultKeys = [[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] allKeys]; + XCTAssertFalse([userDefaultKeys containsObject:oldKeysArray]); + for (NSString *expectedKey in expectedKeysArray) { + if ([expectedKey isEqualToString:expectedWildcard]) { + continue; + } + XCTAssertTrue([userDefaultKeys containsObject:expectedKey]); + } + for (NSString *oldKey in oldKeysArray) { + if ([oldKey isKindOfClass:[MSACUserDefaultsPrefixKey class]]) { + continue; + } + XCTAssertFalse([userDefaultKeys containsObject:oldKey]); + } +} + +- (void)testUnexpectedKeyTypeInMigrateUserDefaultSettings { + + // If + NSDictionary *keys = @{@"MSAppCenterKeyTest1" : @"okeyTest1"}; + MSACAppCenterUserDefaults *userDefaults = [MSACAppCenterUserDefaults shared]; + + // When + [[NSUserDefaults standardUserDefaults] setObject:@"Test 1" forKey:@"YES"]; + + // Then + XCTAssertNoThrow([userDefaults migrateKeys:keys forService:@"AppCenter"]); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACUserIdContextTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACUserIdContextTests.m new file mode 100644 index 0000000000..2faddee9f2 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACUserIdContextTests.m @@ -0,0 +1,221 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACMockUserDefaults.h" +#import "MSACTestFrameworks.h" +#import "MSACUserIdContextDelegate.h" +#import "MSACUserIdContextPrivate.h" +#import "MSACUtility.h" + +@interface MSACUserIdContextTests : XCTestCase + +@property(nonatomic) MSACUserIdContext *sut; +@property(nonatomic) MSACMockUserDefaults *settingsMock; + +@end + +@implementation MSACUserIdContextTests + +#pragma mark - Houskeeping + +- (void)setUp { + [super setUp]; + + self.settingsMock = [MSACMockUserDefaults new]; + self.sut = [MSACUserIdContext sharedInstance]; +} + +- (void)tearDown { + [MSACUserIdContext resetSharedInstance]; + [self.settingsMock stopMocking]; + [super tearDown]; +} + +#pragma mark - Tests + +- (void)testSetUserId { + + // If + NSString *expectedUserId = @"alice"; + + // When + [[MSACUserIdContext sharedInstance] setUserId:expectedUserId]; + + // Then + NSData *data = [self.settingsMock objectForKey:@"UserIdHistory"]; + XCTAssertNotNil(data); + NSMutableArray *savedData = (NSMutableArray *)[[MSACUtility unarchiveKeyedData:data] mutableCopy]; + XCTAssertEqualObjects([savedData[0] userId], expectedUserId); +} + +- (void)testClearUserIdHistory { + + // When + [[MSACUserIdContext sharedInstance] setUserId:@"UserId1"]; + [MSACUserIdContext resetSharedInstance]; + [[MSACUserIdContext sharedInstance] setUserId:@"UserId2"]; + + // Then + NSData *data = [self.settingsMock objectForKey:@"UserIdHistory"]; + XCTAssertNotNil(data); + NSMutableArray *savedData = (NSMutableArray *)[[MSACUtility unarchiveKeyedData:data] mutableCopy]; + + XCTAssertEqual([savedData count], 2); + + // When + [[MSACUserIdContext sharedInstance] clearUserIdHistory]; + + // Then + data = [self.settingsMock objectForKey:@"UserIdHistory"]; + XCTAssertNotNil(data); + + // Should keep the current userId. + savedData = (NSMutableArray *)[[MSACUtility unarchiveKeyedData:data] mutableCopy]; + XCTAssertEqual([savedData count], 1); +} + +- (void)testUserId { + + // If + NSString *expectedUserId = @"UserId"; + + // When + [[MSACUserIdContext sharedInstance] setUserId:expectedUserId]; + + // Then + XCTAssertEqualObjects(expectedUserId, [[MSACUserIdContext sharedInstance] userId]); +} + +- (void)testUserIdAt { + + // If + __block NSDate *date; + id dateMock = OCMClassMock([NSDate class]); + + // When + OCMStub(ClassMethod([dateMock date])).andDo(^(NSInvocation *invocation) { + date = [[NSDate alloc] initWithTimeIntervalSince1970:0]; + [invocation setReturnValue:&date]; + }); + [[MSACUserIdContext sharedInstance] setUserId:@"UserId1"]; + [dateMock stopMocking]; + + [MSACUserIdContext resetSharedInstance]; + + dateMock = OCMClassMock([NSDate class]); + OCMStub(ClassMethod([dateMock date])).andDo(^(NSInvocation *invocation) { + date = [[NSDate alloc] initWithTimeIntervalSince1970:1000]; + [invocation setReturnValue:&date]; + }); + [[MSACUserIdContext sharedInstance] setUserId:@"UserId2"]; + [dateMock stopMocking]; + + [MSACUserIdContext resetSharedInstance]; + + dateMock = OCMClassMock([NSDate class]); + OCMStub(ClassMethod([dateMock date])).andDo(^(NSInvocation *invocation) { + date = [[NSDate alloc] initWithTimeIntervalSince1970:2000]; + [invocation setReturnValue:&date]; + }); + [[MSACUserIdContext sharedInstance] setUserId:@"UserId3"]; + [dateMock stopMocking]; + + [MSACUserIdContext resetSharedInstance]; + + dateMock = OCMClassMock([NSDate class]); + OCMStub(ClassMethod([dateMock date])).andDo(^(NSInvocation *invocation) { + date = [[NSDate alloc] initWithTimeIntervalSince1970:3000]; + [invocation setReturnValue:&date]; + }); + [[MSACUserIdContext sharedInstance] setUserId:@"UserId4"]; + [dateMock stopMocking]; + + [MSACUserIdContext resetSharedInstance]; + + dateMock = OCMClassMock([NSDate class]); + OCMStub(ClassMethod([dateMock date])).andDo(^(NSInvocation *invocation) { + date = [[NSDate alloc] initWithTimeIntervalSince1970:4000]; + [invocation setReturnValue:&date]; + }); + [[MSACUserIdContext sharedInstance] setUserId:@"UserId5"]; + [dateMock stopMocking]; + + // Then + XCTAssertNil([[MSACUserIdContext sharedInstance] userIdAt:[[NSDate alloc] initWithTimeIntervalSince1970:0]]); + XCTAssertEqualObjects(@"UserId3", [[MSACUserIdContext sharedInstance] userIdAt:[[NSDate alloc] initWithTimeIntervalSince1970:2500]]); + XCTAssertEqualObjects(@"UserId5", [[MSACUserIdContext sharedInstance] userIdAt:[[NSDate alloc] initWithTimeIntervalSince1970:5000]]); +} + +- (void)testPrefixedUserIdFromUserId { + + // Then + XCTAssertEqualObjects([MSACUserIdContext prefixedUserIdFromUserId:@"c:alice"], @"c:alice"); + XCTAssertEqualObjects([MSACUserIdContext prefixedUserIdFromUserId:@"alice"], @"c:alice"); + XCTAssertEqualObjects([MSACUserIdContext prefixedUserIdFromUserId:@":"], @":"); + XCTAssertNil([MSACUserIdContext prefixedUserIdFromUserId:nil]); +} + +- (void)testDelegateCalledOnUserIdChanged { + + // If + XCTAssertNil([self.sut currentUserIdInfo].userId); + NSString *expectedUserId = @"Robert"; + id delegateMock = OCMProtocolMock(@protocol(MSACUserIdContextDelegate)); + [self.sut addDelegate:delegateMock]; + OCMExpect([delegateMock userIdContext:self.sut didUpdateUserId:expectedUserId]); + + // When + [[MSACUserIdContext sharedInstance] setUserId:expectedUserId]; + + // Then + XCTAssertEqual([self.sut userId], expectedUserId); + OCMVerify([delegateMock userIdContext:self.sut didUpdateUserId:expectedUserId]); +} + +- (void)testDelegateCalledOnUserIdChangedToNil { + + // If + NSString *userId = @"Robert"; + [[MSACUserIdContext sharedInstance] setUserId:userId]; + id delegateMock = OCMProtocolMock(@protocol(MSACUserIdContextDelegate)); + [self.sut addDelegate:delegateMock]; + OCMExpect([delegateMock userIdContext:self.sut didUpdateUserId:nil]); + + // When + [[MSACUserIdContext sharedInstance] setUserId:nil]; + + // Then + XCTAssertEqual([self.sut userId], nil); + OCMVerify([delegateMock userIdContext:self.sut didUpdateUserId:nil]); +} + +- (void)testDelegateNotCalledOnUserIdSame { + + // If + NSString *expectedUserId = @"Patrick"; + [[MSACUserIdContext sharedInstance] setUserId:expectedUserId]; + id delegateMock = OCMProtocolMock(@protocol(MSACUserIdContextDelegate)); + [self.sut addDelegate:delegateMock]; + OCMReject([delegateMock userIdContext:self.sut didUpdateUserId:expectedUserId]); + + // When + [[MSACUserIdContext sharedInstance] setUserId:expectedUserId]; + + // Then + OCMVerifyAll(delegateMock); +} + +- (void)testRemoveDelegate { + + // If + id delegateMock = OCMProtocolMock(@protocol(MSACUserIdContextDelegate)); + [self.sut addDelegate:delegateMock]; + + // When + [self.sut removeDelegate:delegateMock]; + + // Then + XCTAssertEqual([[self.sut delegates] count], 0); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACUtilityTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACUtilityTests.m new file mode 100644 index 0000000000..49e108361c --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACUtilityTests.m @@ -0,0 +1,1220 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACConstants+Internal.h" +#import "MSACDispatcherUtil.h" +#import "MSACSessionHistoryInfo.h" +#import "MSACTestFrameworks.h" +#import "MSACTestSessionInfo.h" +#import "MSACUtility+ApplicationPrivate.h" +#import "MSACUtility+Date.h" +#import "MSACUtility+Environment.h" +#import "MSACUtility+File.h" +#import "MSACUtility+PropertyValidation.h" +#import "MSACUtility+StringFormatting.h" + +static NSTimeInterval const kMSACTestTimeout = 1.0; + +@interface MSACUtility (Test) + ++ (void)resetDateFormatterInstance; + +@end + +@interface MSACUtilityTests : XCTestCase + +@property(nonatomic) id utils; + +@end + +@implementation MSACUtilityTests + +- (void)setUp { + [super setUp]; + + // Set up application mock. + self.utils = OCMClassMock([MSACUtility class]); +} + +- (void)tearDown { + [super tearDown]; + [self.utils stopMocking]; + [MSACUtility deleteItemForPathComponent:@"testing"]; +} + +#pragma mark - MSACUtility.h + +- (void)testSdkName { + NSString *name = [NSString stringWithUTF8String:APP_CENTER_C_NAME]; + XCTAssertTrue([[MSACUtility sdkName] isEqualToString:name]); +} + +- (void)testSdkVersion { + NSString *version = [NSString stringWithUTF8String:APP_CENTER_C_VERSION]; + XCTAssertTrue([[MSACUtility sdkVersion] isEqualToString:version]); +} + +#pragma mark - MSACUtility+Application.h + +#if !TARGET_OS_OSX && !TARGET_OS_MACCATALYST +- (void)testMSACAppStateMatchesUIAppStateWhenAvailable { + + // Then + assertThat(@([MSACUtility applicationState]), is(@([UIApplication sharedApplication].applicationState))); +} +#endif + +- (void)testMSACAppReturnsUnknownOnAppExtensions { + + // If + // Mock the helper itself to monitor method calls. + id bundleMock = OCMClassMock([NSBundle class]); + OCMStub([bundleMock executablePath]).andReturn(@"/apath/coolappext.appex/coolappext"); + OCMStub([bundleMock mainBundle]).andReturn(bundleMock); + OCMReject([self.utils sharedAppState]); + + // Then + assertThat(@([MSACUtility applicationState]), is(@(MSACApplicationStateUnknown))); + + // Make sure the sharedApplication as not been called, it's forbidden within + // app extensions + [bundleMock stopMocking]; +} + +- (void)testAppActive { + +// If +#if TARGET_OS_OSX + MSACApplicationState expectedState = MSACApplicationStateActive; + OCMStub([self.utils sharedAppState]).andReturn(expectedState); +#else + UIApplicationState expectedState = UIApplicationStateActive; + OCMStub([self.utils sharedAppState]).andReturn(expectedState); +#endif + + // When + MSACApplicationState state = [MSACUtility applicationState]; + + // Then + assertThat(@(state), is(@(expectedState))); +} + +#if !TARGET_OS_OSX && !TARGET_OS_MACCATALYST +- (void)testAppInactive { + + // If + UIApplicationState expectedState = UIApplicationStateInactive; + OCMStub([self.utils sharedAppState]).andReturn(expectedState); + + // When + MSACApplicationState state = [MSACUtility applicationState]; + + // Then + assertThat(@(state), is(@(expectedState))); +} +#endif + +- (void)testAppInBackground { + +// If +#if TARGET_OS_OSX + MSACApplicationState expectedState = MSACApplicationStateBackground; + OCMStub([self.utils sharedAppState]).andReturn(expectedState); +#else + UIApplicationState expectedState = UIApplicationStateBackground; + OCMStub([self.utils sharedAppState]).andReturn(expectedState); +#endif + + // When + MSACApplicationState state = [MSACUtility applicationState]; + + // Then + assertThat(@(state), is(@(expectedState))); +} + +- (void)testCurrentAppEnvironment { + + // When + MSACEnvironment env = [MSACUtility currentAppEnvironment]; + + // Then + // Tests always run in simulators. + XCTAssertEqual(env, MSACEnvironmentOther); +} + +#pragma mark - MSACUtility+Date.h + +- (void)testNowInMilliseconds { + + // If + NSDate *date = [NSDate date]; + id dateMock = OCMClassMock([NSDate class]); + OCMStub([dateMock date]).andReturn(date); + + // When + long long actual = (long long)([MSACUtility nowInMilliseconds] / 10); + long long expected = (long long)([[NSDate date] timeIntervalSince1970] * 100); + + // Then + XCTAssertEqual(actual, expected); + + // Negative in case of cast issue. + XCTAssertGreaterThan(actual, 0); +} + +- (void)testDateFormatterConcurrentInitialization { + + // If + [MSACUtility resetDateFormatterInstance]; + XCTestExpectation *expectation = [self expectationWithDescription:@"queueExpectation"]; + dispatch_queue_t concurrentQueue = dispatch_queue_create("com.dateformatter.queue", DISPATCH_QUEUE_CONCURRENT); + id nsDateFormatter = OCMClassMock([NSDateFormatter class]); + OCMStub([nsDateFormatter stringFromDate:OCMOCK_ANY]).andReturn(@"stub"); + OCMStub([nsDateFormatter alloc]).andReturn(nsDateFormatter); + + // When + int dispatchTimes = 10; + __block NSObject *lock = [NSObject new]; + __block int counter = 0; + for (int i = 0; i < dispatchTimes; i++) { + dispatch_async(concurrentQueue, ^{ + @try { + [MSACUtility dateToISO8601:[NSDate dateWithTimeIntervalSince1970:i]]; + } @catch (NSException *exception) { + XCTFail(@"Expectation Failed with error: %@", exception); + } + @synchronized(lock) { + counter++; + if (counter == dispatchTimes) { + [expectation fulfill]; + return; + } + } + }); + } + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + OCMVerify(times(1), [nsDateFormatter alloc]); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +#pragma mark - MSACUtility+Environment.h + +// FIXME: This method actually opens a dialog to ask to handle the URL on Mac. +#if !TARGET_OS_OSX && !TARGET_OS_MACCATALYST +- (void)testSharedAppOpenEmptyCallCallback { + + // If + XCTestExpectation *openURLCalledExpectation = [self expectationWithDescription:@"openURL Called."]; + __block BOOL handlerHasBeenCalled = NO; + + // When + [MSACUtility sharedAppOpenUrl:[NSURL URLWithString:@""] + options:@{} + completionHandler:^(MSACOpenURLState status) { + handlerHasBeenCalled = YES; + XCTAssertEqual(status, MSACOpenURLStateFailed); + }]; + dispatch_async(dispatch_get_main_queue(), ^{ + [openURLCalledExpectation fulfill]; + }); + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + XCTAssertTrue(handlerHasBeenCalled); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} +#endif + +#pragma mark - MSACUtility+StringFormatting.h + +- (void)testCreateSha256 { + + // When + NSString *test = @"TestString"; + NSString *result = [MSACUtility sha256:test]; + + // Then + XCTAssertTrue([result isEqualToString:@"6dd79f2770a0bb38073b814a5ff000647b37be5abbde71ec9176c6ce0cb32a27"]); +} + +- (void)testPrettyPrintNil { + XCTAssertNil([MSACUtility prettyPrintJson:nil]); +} + +- (void)testPrettyPrintNotJson { + NSString *nonJson = @"[test] some non json string"; + XCTAssertTrue([[MSACUtility prettyPrintJson:[nonJson dataUsingEncoding:NSUTF8StringEncoding]] isEqualToString:nonJson]); +} + +- (void)testPrettyPrintJson { + XCTAssertTrue([[MSACUtility prettyPrintJson:[@"{\"a\":1}" dataUsingEncoding:NSUTF8StringEncoding]] isEqualToString:@"{\n \"a\" : 1\n}"]); +} + +#pragma mark - MSACUtility+PropertyValidation.h + +- (void)testAppSecretFrom { + + // When + NSString *uuidString = MSAC_UUID_STRING; + + // Then + NSString *result = [MSACUtility appSecretFrom:uuidString]; + XCTAssertEqualObjects(uuidString, result); + + // When + NSString *test = nil; + result = [MSACUtility appSecretFrom:test]; + + // Then + XCTAssertNil(result); + + // When + test = [NSString stringWithFormat:@"%@;", uuidString]; + result = [MSACUtility appSecretFrom:test]; + + // Then + XCTAssertEqualObjects(uuidString, result); + + // When + test = [NSString stringWithFormat:@"%@;target={transmissionTargetToken}", uuidString]; + result = [MSACUtility appSecretFrom:test]; + + // Then + XCTAssertEqualObjects(uuidString, result); + + // When + test = [NSString stringWithFormat:@"%@;target={transmissionTargetToken};", uuidString]; + result = [MSACUtility appSecretFrom:test]; + + // Then + XCTAssertEqualObjects(uuidString, result); + + // When + test = [NSString stringWithFormat:@"target={transmissionTargetToken};%@", uuidString]; + result = [MSACUtility appSecretFrom:test]; + + // Then + XCTAssertEqualObjects(uuidString, result); + + // When + test = [NSString stringWithFormat:@"target={transmissionTargetToken};%@;", uuidString]; + + result = [MSACUtility appSecretFrom:test]; + + // Then + XCTAssertEqualObjects(uuidString, result); + + // When + test = @"target={transmissionTargetToken}"; + result = [MSACUtility appSecretFrom:test]; + + // Then + XCTAssertNil(result); + + // When + test = @"target={transmissionTargetToken};"; + result = [MSACUtility appSecretFrom:test]; + + // Then + XCTAssertNil(result); + + // When + test = [NSString stringWithFormat:@"appsecret=%@;target={transmissionTargetToken};", uuidString]; + result = [MSACUtility appSecretFrom:test]; + + // Then + XCTAssertEqualObjects(uuidString, result); + + // When + test = [NSString stringWithFormat:@"appsecret=%@;", uuidString]; + result = [MSACUtility appSecretFrom:test]; + + // Then + XCTAssertEqualObjects(uuidString, result); + + // When + test = [NSString stringWithFormat:@"appsecret=%@", uuidString]; + result = [MSACUtility appSecretFrom:test]; + + // Then + XCTAssertEqualObjects(uuidString, result); + + // When + test = [NSString stringWithFormat:@"target={transmissionTargetToken};appsecret=%@;", uuidString]; + result = [MSACUtility appSecretFrom:test]; + + // Then + XCTAssertEqualObjects(uuidString, result); + + // When + test = [NSString stringWithFormat:@"target={transmissionTargetToken};appsecret=%@", uuidString]; + result = [MSACUtility appSecretFrom:test]; + + // Then + XCTAssertEqualObjects(uuidString, result); +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + // When + test = [NSString stringWithFormat:@"targetIos={transmissionTargetToken};ios=%@", uuidString]; + result = [MSACUtility appSecretFrom:test]; + + // Then + XCTAssertEqualObjects(uuidString, result); + + // When + test = [NSString stringWithFormat:@"macos=fake;ios=%@", uuidString]; + result = [MSACUtility appSecretFrom:test]; + + // Then + XCTAssertEqualObjects(uuidString, result); + + // When + test = @"ios={app-secret};macos={fake};appsecret=fake"; + result = [MSACUtility appSecretFrom:test]; + + // Then + XCTAssertEqualObjects(result, @"{app-secret}"); +#endif +} + +- (void)testTransmissionTokenFrom { + + // When + NSString *test = @"{app-secret}"; + + // Then + NSString *result = [MSACUtility transmissionTargetTokenFrom:test]; + XCTAssertNil(result); + + // When + test = nil; + result = [MSACUtility transmissionTargetTokenFrom:test]; + + // Then + XCTAssertNil(result); + + // When + test = @"{app-secret};"; + result = [MSACUtility transmissionTargetTokenFrom:test]; + + // Then + XCTAssertNil(result); + + // When + test = @"{app-secret};target={transmissionTargetToken}"; + result = [MSACUtility transmissionTargetTokenFrom:test]; + + // Then + XCTAssertEqualObjects(result, @"{transmissionTargetToken}"); + + // When + test = @"{app-secret};target={transmissionTargetToken};"; + result = [MSACUtility transmissionTargetTokenFrom:test]; + + // Then + XCTAssertEqualObjects(result, @"{transmissionTargetToken}"); + + // When + test = @"target={transmissionTargetToken};{app-secret}"; + result = [MSACUtility transmissionTargetTokenFrom:test]; + + // Then + XCTAssertEqualObjects(result, @"{transmissionTargetToken}"); + + // When + test = @"target={transmissionTargetToken};{app-secret};"; + result = [MSACUtility transmissionTargetTokenFrom:test]; + + // Then + XCTAssertEqualObjects(result, @"{transmissionTargetToken}"); + + // When + test = @"target={transmissionTargetToken}"; + result = [MSACUtility transmissionTargetTokenFrom:test]; + + // Then + XCTAssertEqualObjects(result, @"{transmissionTargetToken}"); + + // When + test = @"target={transmissionTargetToken};"; + result = [MSACUtility transmissionTargetTokenFrom:test]; + + // Then + XCTAssertEqualObjects(result, @"{transmissionTargetToken}"); + + // When + test = @"appsecret={app-secret};target={transmissionTargetToken};"; + result = [MSACUtility transmissionTargetTokenFrom:test]; + + // Then + XCTAssertEqualObjects(result, @"{transmissionTargetToken}"); + + // When + test = @"appsecret={app-secret};"; + result = [MSACUtility transmissionTargetTokenFrom:test]; + + // Then + XCTAssertNil(result); + + // When + test = @"appsecret={app-secret}"; + result = [MSACUtility transmissionTargetTokenFrom:test]; + + // Then + XCTAssertNil(result); + + // When + test = @"target={transmissionTargetToken};appsecret={app-secret};"; + result = [MSACUtility transmissionTargetTokenFrom:test]; + + // Then + XCTAssertEqualObjects(result, @"{transmissionTargetToken}"); + + // When + test = @"target={transmissionTargetToken};appsecret={app-secret}"; + result = [MSACUtility transmissionTargetTokenFrom:test]; + + // Then + XCTAssertEqualObjects(result, @"{transmissionTargetToken}"); +#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST + // When + test = @"target={transmissionTargetToken};ios={app-secret}"; + result = [MSACUtility transmissionTargetTokenFrom:test]; + + // Then + XCTAssertEqualObjects(result, @"{transmissionTargetToken}"); + + // When + test = @"target={transmissionTargetToken};targetMacos={fake}"; + result = [MSACUtility transmissionTargetTokenFrom:test]; + + // Then + XCTAssertEqualObjects(result, @"{transmissionTargetToken}"); +#endif +} + +#if TARGET_OS_MACCATALYST + +- (void)testAppSecretCatalystFrom { + + // When + NSString *test = @"ios={fake};macos={app-secret}"; + NSString *result = [MSACUtility appSecretFrom:test]; + + // Then + XCTAssertEqualObjects(result, @"{app-secret}"); + + // When + test = @"ios={fake};macos={app-secret};appsecret=fake"; + result = [MSACUtility appSecretFrom:test]; + + // Then + XCTAssertEqualObjects(result, @"{app-secret}"); +} +#endif + +- (void)testInvalidSecretOrTokenInput { + + // When + NSString *guidString = @"{app-secret}"; + NSString *test = [NSString stringWithFormat:@"target=;appsecret=%@", guidString]; + NSString *tokenResult = [MSACUtility transmissionTargetTokenFrom:test]; + NSString *secretResult = [MSACUtility appSecretFrom:test]; + + // Then + XCTAssertNil(tokenResult); + XCTAssertEqualObjects(guidString, secretResult); + + // When + test = @"target=;target=;appsecret=;appsecret=;"; + tokenResult = [MSACUtility transmissionTargetTokenFrom:test]; + secretResult = [MSACUtility appSecretFrom:test]; + + // Then + XCTAssertNil(tokenResult); + XCTAssertNil(secretResult); + + // When + guidString = MSAC_UUID_STRING; + test = [NSString stringWithFormat:@"target=;target={transmissionTargetToken};appsecret=;appsecret=%@;", guidString]; + tokenResult = [MSACUtility transmissionTargetTokenFrom:test]; + secretResult = [MSACUtility appSecretFrom:test]; + + // Then + XCTAssertNotNil(secretResult); + XCTAssertEqualObjects(guidString, secretResult); + XCTAssertEqualObjects(tokenResult, @"{transmissionTargetToken}"); +} + +- (void)testValidatePropertyType { + NSString *longStringValue = [@"" stringByPaddingToLength:(kMSACMaxPropertyValueLength + 1) withString:@"value" startingAtIndex:0]; + NSString *stringValue125 = [@"" stringByPaddingToLength:kMSACMaxPropertyValueLength withString:@"value" startingAtIndex:0]; + NSString *testLogTypeString = @"testLog"; + + // Test valid properties + // If + NSDictionary *validProperties = + @{@"Key1" : @"Value1", stringValue125 : @"Value2", @"Key3" : stringValue125, @"Key4" : @"Value4", @"Key5" : @""}; + + // When + NSDictionary *validatedProperties = [MSACUtility validateProperties:validProperties forLogName:testLogTypeString type:testLogTypeString]; + + // Then + XCTAssertTrue([validatedProperties count] == [validProperties count]); + + // Test too many properties in one event + // If + NSDictionary *tooManyProperties = @{ + @"Key1" : @"Value1", + @"Key2" : @"Value2", + @"Key3" : @"Value3", + @"Key4" : @"Value4", + @"Key5" : @"Value5", + @"Key6" : @"Value6", + @"Key7" : @"Value7", + @"Key8" : @"Value8", + @"Key9" : @"Value9", + @"Key10" : @"Value10", + @"Key11" : @"Value11", + @"Key12" : @"Value12", + @"Key13" : @"Value13", + @"Key14" : @"Value14", + @"Key15" : @"Value15", + @"Key16" : @"Value16", + @"Key17" : @"Value17", + @"Key18" : @"Value18", + @"Key19" : @"Value19", + @"Key20" : @"Value20", + @"Key21" : @"Value21", + @"Key22" : @"Value22" + }; + + // When + validatedProperties = [MSACUtility validateProperties:tooManyProperties forLogName:testLogTypeString type:testLogTypeString]; + + // Then + XCTAssertTrue([validatedProperties count] == kMSACMaxPropertiesPerLog); + + // Test invalid properties + // If + NSDictionary *invalidKeysInProperties = @{@"Key1" : @"Value1", @(2) : @"Value2", @"" : @"Value4"}; + + // When + validatedProperties = [MSACUtility validateProperties:invalidKeysInProperties forLogName:testLogTypeString type:testLogTypeString]; + + // Then + XCTAssertTrue([validatedProperties count] == 1); + + // Test invalid values + // If + NSDictionary *invalidValuesInProperties = @{@"Key1" : @"Value1", @"Key2" : @(2)}; + + // When + validatedProperties = [MSACUtility validateProperties:invalidValuesInProperties forLogName:testLogTypeString type:testLogTypeString]; + + // Then + XCTAssertTrue([validatedProperties count] == 1); + + // Test long keys and values are truncated. + // If + NSDictionary *tooLongKeysAndValuesInProperties = @{longStringValue : longStringValue}; + + // When + validatedProperties = [MSACUtility validateProperties:tooLongKeysAndValuesInProperties + forLogName:testLogTypeString + type:testLogTypeString]; + + // Then + NSString *truncatedKey = (NSString *)[[validatedProperties allKeys] firstObject]; + NSString *truncatedValue = (NSString *)[[validatedProperties allValues] firstObject]; + XCTAssertTrue([validatedProperties count] == 1); + XCTAssertEqual([truncatedKey length], kMSACMaxPropertyKeyLength); + XCTAssertEqual([truncatedValue length], kMSACMaxPropertyValueLength); + + // Test mixed variant + // If + NSDictionary *mixedProperties = @{ + @"Key1" : @"Value1", + @(2) : @"Value2", + stringValue125 : @"Value3", + @"Key4" : stringValue125, + @"Key5" : @"Value5", + @"Key6" : @(2), + @"Key7" : longStringValue, + @"Key8" : @"Value8", + @(2) : @"Value9", + stringValue125 : @"Value10", + @"Key11" : stringValue125, + @"Key12" : @"Value12", + @"Key13" : @(2), + @"Key14" : longStringValue, + @"Key15" : @"Value15", + @(2) : @"Value16", + stringValue125 : @"Value17", + @"Key18" : stringValue125, + @"Key19" : @"Value19", + @"Key20" : @(2), + @"Key21" : longStringValue, + @"Key22" : @"Value22", + @(2) : @"Value23", + stringValue125 : @"Value124", + @"Key25" : stringValue125, + @"Key26" : @"Value26", + @"Key27" : @(2), + @"Key28" : @"Value28", + @(2) : @"Value29", + stringValue125 : @"Value30", + @"Key31" : stringValue125, + @"Key32" : @"Value32", + @"Key33" : @(2), + @"Key34" : longStringValue, + }; + + // When + validatedProperties = [MSACUtility validateProperties:mixedProperties forLogName:testLogTypeString type:testLogTypeString]; + + // Then + XCTAssertTrue([validatedProperties count] == kMSACMaxPropertiesPerLog); + XCTAssertNotNil([validatedProperties objectForKey:@"Key1"]); + XCTAssertNotNil([validatedProperties objectForKey:stringValue125]); + XCTAssertNotNil([validatedProperties objectForKey:@"Key4"]); + XCTAssertNotNil([validatedProperties objectForKey:@"Key5"]); + XCTAssertNil([validatedProperties objectForKey:@"Key6"]); + XCTAssertNotNil([validatedProperties objectForKey:@"Key7"]); +} + +#pragma mark - MSACUtility+File.h + +- (void)testCreateFile { + + // If + NSString *expectedString = @"Something"; + NSString *pathComponent = @"testing/afile.test"; + NSData *expectedData = [expectedString dataUsingEncoding:NSUTF8StringEncoding]; + BOOL forceOverwrite = NO; + + // When + NSURL *url = [MSACUtility createFileAtPathComponent:pathComponent withData:expectedData atomically:YES forceOverwrite:forceOverwrite]; + + // Then + XCTAssertNotNil(url); + NSString *expectedFile; +#if TARGET_OS_TV + expectedFile = @"/Library/Caches/com.microsoft.appcenter/testing/afile.test"; +#else +#if TARGET_OS_OSX || TARGET_OS_MACCATALYST + expectedFile = [self getPathWithBundleIdentifier:@"/Library/Application%%20Support/%@/com.microsoft.appcenter/testing/afile.test"]; +#else + expectedFile = @"/Library/Application%20Support/com.microsoft.appcenter/testing/afile.test"; +#endif +#endif + XCTAssertTrue([[url relativeString] containsString:expectedFile]); + XCTAssertTrue([url checkResourceIsReachableAndReturnError:nil]); + NSData *actualData = [NSData dataWithContentsOfURL:url]; + XCTAssertNotNil(actualData); + NSString *actualContent = [[NSString alloc] initWithData:actualData encoding:NSUTF8StringEncoding]; + XCTAssertTrue([actualContent isEqualToString:expectedString]); + + // When + NSString *newString = @"Hello"; + NSData *newData = [newString dataUsingEncoding:NSUTF8StringEncoding]; + + // Try to create a file that already exists with forceOverwrite set to NO. This shouldn't change the file. + url = [MSACUtility createFileAtPathComponent:pathComponent withData:newData atomically:YES forceOverwrite:NO]; + + // Then + actualData = [NSData dataWithContentsOfURL:url]; + XCTAssertNotNil(actualData); + actualContent = [[NSString alloc] initWithData:actualData encoding:NSUTF8StringEncoding]; + XCTAssertTrue([actualContent isEqualToString:expectedString]); + + // When + url = [MSACUtility createFileAtPathComponent:pathComponent withData:newData atomically:YES forceOverwrite:YES]; + + // Then + actualData = [NSData dataWithContentsOfURL:url]; + XCTAssertNotNil(actualData); + actualContent = [[NSString alloc] initWithData:actualData encoding:NSUTF8StringEncoding]; + XCTAssertTrue([actualContent isEqualToString:newString]); +} + +- (void)testDeleteItemForPathComponent { + + // If + NSString *expectedString = @"Something"; + NSString *pathComponent = @"testing/anotherfile.test"; + NSData *expectedData = [expectedString dataUsingEncoding:NSUTF8StringEncoding]; + BOOL forceOverwrite = NO; + + // When + NSURL *url = [MSACUtility createFileAtPathComponent:pathComponent withData:expectedData atomically:YES forceOverwrite:forceOverwrite]; + + // Then + XCTAssertNotNil(url); + NSString *expectedFile; +#if TARGET_OS_TV + expectedFile = @"/Library/Caches/com.microsoft.appcenter/testing/anotherfile.test"; +#else +#if TARGET_OS_OSX || TARGET_OS_MACCATALYST + expectedFile = [self getPathWithBundleIdentifier:@"/Library/Application%%20Support/%@/com.microsoft.appcenter/testing/anotherfile.test"]; +#else + expectedFile = @"/Library/Application%20Support/com.microsoft.appcenter/testing/anotherfile.test"; +#endif +#endif + XCTAssertTrue([[url relativeString] containsString:expectedFile]); + XCTAssertTrue([url checkResourceIsReachableAndReturnError:nil]); + + // When + [MSACUtility deleteItemForPathComponent:pathComponent]; + XCTAssertFalse([url checkResourceIsReachableAndReturnError:nil]); +} + +- (void)testCreateDirectory { + + // If + NSString *pathComponent = @"testing"; + + // When + NSURL *url = [MSACUtility createDirectoryForPathComponent:pathComponent]; + + // Then + XCTAssertNotNil(url); + NSString *expectedFile; +#if TARGET_OS_TV + expectedFile = @"/Library/Caches/com.microsoft.appcenter/testing"; +#else +#if TARGET_OS_OSX || TARGET_OS_MACCATALYST + expectedFile = [self getPathWithBundleIdentifier:@"/Library/Application%%20Support/%@/com.microsoft.appcenter/testing"]; +#else + expectedFile = @"/Library/Application%20Support/com.microsoft.appcenter/testing"; +#endif +#endif + XCTAssertTrue([[url relativeString] containsString:expectedFile]); + XCTAssertTrue([url checkResourceIsReachableAndReturnError:nil]); +} + +- (void)testLoadDataForPathComponent { + + // If + NSString *expectedString = @"Something"; + NSString *pathComponent = @"testing/anotherfile.test"; + NSData *expectedData = [expectedString dataUsingEncoding:NSUTF8StringEncoding]; + BOOL forceOverwrite = NO; + [MSACUtility createFileAtPathComponent:pathComponent withData:expectedData atomically:YES forceOverwrite:forceOverwrite]; + + // When + NSData *actualData = [MSACUtility loadDataForPathComponent:pathComponent]; + + // Then + XCTAssertNotNil(actualData); + NSString *actualContent = [[NSString alloc] initWithData:actualData encoding:NSUTF8StringEncoding]; + XCTAssertTrue([actualContent isEqualToString:expectedString]); +} + +- (void)testContentsOfDirectory { + + // If + NSString *expectedString = @"Something"; + NSString *parentDir = @"testing"; + NSString *pathComponent = [NSString stringWithFormat:@"%@%@", parentDir, @"/testFile."]; + BOOL forceOverwrite = NO; + BOOL atomical = YES; + NSUInteger fileCount; + for (fileCount = 0; fileCount < 3; fileCount++) { + [MSACUtility createFileAtPathComponent:[NSString stringWithFormat:@"%@%lu", pathComponent, (unsigned long)fileCount] + withData:[[NSString stringWithFormat:@"%@%lu", expectedString, (unsigned long)fileCount] + dataUsingEncoding:NSUTF8StringEncoding] + atomically:atomical + forceOverwrite:forceOverwrite]; + } + + // When + NSArray *contents = [MSACUtility contentsOfDirectory:parentDir propertiesForKeys:nil]; + + // Then + XCTAssertTrue(contents.count == fileCount); + for (NSURL *fileUrl in contents) { + NSString *testNb = fileUrl.pathExtension; + NSString *content = [NSString stringWithContentsOfURL:fileUrl encoding:NSUTF8StringEncoding error:nil]; + XCTAssertTrue([fileUrl checkResourceIsReachableAndReturnError:nil]); + BOOL test = [content isEqualToString:[NSString stringWithFormat:@"%@%@", expectedString, testNb]]; + XCTAssertTrue(test); + } +} + +- (void)testFileExistsForPathComponent { + + // If + NSString *expectedString = @"Something"; + NSString *pathComponent = @"testing/anotherfile.test"; + NSData *expectedData = [expectedString dataUsingEncoding:NSUTF8StringEncoding]; + BOOL forceOverwrite = NO; + [MSACUtility createFileAtPathComponent:pathComponent withData:expectedData atomically:YES forceOverwrite:forceOverwrite]; + + // When + BOOL actual = [MSACUtility fileExistsForPathComponent:pathComponent]; + + // Then + XCTAssertTrue(actual); + + // When + actual = [MSACUtility fileExistsForPathComponent:@"thisDoesNotExist"]; + + // Then + XCTAssertFalse(actual); +} + +- (void)testDeleteFileAtURL { + + // If + NSString *expectedString = @"Something"; + NSString *pathComponent = @"testing/anotherfile.test"; + NSData *expectedData = [expectedString dataUsingEncoding:NSUTF8StringEncoding]; + BOOL forceOverwrite = NO; + + // When + NSURL *url = [MSACUtility createFileAtPathComponent:pathComponent withData:expectedData atomically:YES forceOverwrite:forceOverwrite]; + + // Then + XCTAssertNotNil(url); + NSString *expectedFile; +#if TARGET_OS_TV + expectedFile = @"/Library/Caches/com.microsoft.appcenter/testing/anotherfile.test"; +#else +#if TARGET_OS_OSX || TARGET_OS_MACCATALYST + expectedFile = [self getPathWithBundleIdentifier:@"/Library/Application%%20Support/%@/com.microsoft.appcenter/testing/anotherfile.test"]; +#else + expectedFile = @"/Library/Application%20Support/com.microsoft.appcenter/testing/anotherfile.test"; +#endif +#endif + XCTAssertTrue([[url relativeString] containsString:expectedFile]); + XCTAssertTrue([url checkResourceIsReachableAndReturnError:nil]); + + // When + [MSACUtility deleteFileAtURL:url]; + + // Then + XCTAssertFalse([url checkResourceIsReachableAndReturnError:nil]); +} + +- (void)testFullURLForPathComponent { + + // If + NSString *expectedString = @"Something"; + NSString *pathComponent = @"testing/anotherfile.test"; + NSData *expectedData = [expectedString dataUsingEncoding:NSUTF8StringEncoding]; + BOOL forceOverwrite = NO; + + // When + NSURL *url = [MSACUtility createFileAtPathComponent:pathComponent withData:expectedData atomically:YES forceOverwrite:forceOverwrite]; + NSURL *actual = [MSACUtility fullURLForPathComponent:pathComponent]; + + // Then + XCTAssertNotNil(url); + XCTAssertNotNil(url); + XCTAssertTrue([[url absoluteString] isEqualToString:([actual absoluteString]) ?: @""]); +} + +- (void)testIKeyFromTargetToken { + + // When + NSString *iKey = [MSACUtility iKeyFromTargetToken:nil]; + + // Then + XCTAssertNil(iKey); + + // When + iKey = [MSACUtility iKeyFromTargetToken:@""]; + + // Then + XCTAssertNil(iKey); + + // When + iKey = [MSACUtility iKeyFromTargetToken:@"targetId-gu-id"]; + + // Then + XCTAssertEqualObjects(iKey, @"o:targetId"); +} + +- (void)testTargetIdFromTargetToken { + + // When +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + NSString *targetId = [MSACUtility targetKeyFromTargetToken:nil]; +#pragma clang diagnostic pop + + // Then + XCTAssertNil(targetId); + + // When + targetId = [MSACUtility targetKeyFromTargetToken:@""]; + + // Then + XCTAssertNil(targetId); + + // When + targetId = [MSACUtility targetKeyFromTargetToken:@"targetId-gu-id"]; + + // Then + XCTAssertEqualObjects(targetId, @"targetId"); +} + +- (void)testObfuscateString { + + // If + NSString *pattern = @"\"token\":\"[^\"]+\""; + NSString *template = @"\"token\":\"***\""; + + // Then + XCTAssertNil([MSACUtility obfuscateString:nil searchingForPattern:pattern toReplaceWithTemplate:template]); + XCTAssertEqualObjects([MSACUtility obfuscateString:@"" searchingForPattern:pattern toReplaceWithTemplate:template], @""); + XCTAssertEqualObjects([MSACUtility obfuscateString:@"{\"something\":\"else\"}" + searchingForPattern:pattern + toReplaceWithTemplate:template], + @"{\"something\":\"else\"}"); + + // If + NSString *unObfuscatedString = @"{\"something\":\"else\",\"token\":\"atoken\"}"; + NSString *expectedString = [NSString stringWithFormat:@"{\"something\":\"else\",%@}", template]; + + // Then + XCTAssertEqualObjects([MSACUtility obfuscateString:unObfuscatedString searchingForPattern:pattern toReplaceWithTemplate:template], + expectedString); +} + +- (void)testObfuscateRedirectUri { + + // If + NSString *payload = @"{\"redirect_uri\": \"abc\"}"; + + // When + NSString *obfuscatedString = [MSACUtility obfuscateString:payload + searchingForPattern:kMSACRedirectUriPattern + toReplaceWithTemplate:kMSACRedirectUriObfuscatedTemplate]; + + // Then + XCTAssertTrue([obfuscatedString rangeOfString:@"abc"].location == NSNotFound); + XCTAssertFalse([obfuscatedString rangeOfString:kMSACRedirectUriObfuscatedTemplate].location == NSNotFound); +} + +- (void)testDispatchObjectMacro { + + // If + NSMutableArray *array = [NSMutableArray new]; + + // When + MSAC_DISPATCH_SELECTOR((void (*)(id, SEL, id)), array, addObject:, @"test"); + + // Then + XCTAssertEqual([array count], 1); + XCTAssertEqual([array firstObject], @"test"); +} + +- (void)testDispatchObjectMacroWithNil { + + // If + XCTestExpectation *expectation = [self expectationWithDescription:@"Dispatch selector executed."]; + typedef void (^block)(NSString *, NSString *); + + // When + MSAC_DISPATCH_SELECTOR((void (*)(id, SEL, NSString *, NSString *, block)), self, methodWithArgs:secondArg:completionHandler:, nil, + @"test", ^(NSString *firstArg, NSString *secondArg) { + XCTAssertNil(firstArg); + XCTAssertEqual(secondArg, @"test"); + [expectation fulfill]; + }); + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testPerformBlockOnMainThread { + + // If + XCTestExpectation *expectation = [self expectationWithDescription:@"method called."]; + NSString *str = @"expectedString"; + + // When + [MSACDispatcherUtil performBlockOnMainThread:^{ + [self methodToCall:str + completionHandler:^(NSString *string) { + XCTAssertEqual(str, string); + [expectation fulfill]; + }]; + }]; + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testDispatchSyncWithTimeoutDoesNotWait { + + // If + NSTimeInterval blockTimeout = 0.1; + XCTestExpectation *expectation = [self expectationWithDescription:@"block not called."]; + + // Should be pass if `[expectation fulfill]` will be called later than `waitForExpectationsWithTimeout`. + [expectation setInverted:YES]; + dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL); + __block dispatch_semaphore_t delayedSemaphore = dispatch_semaphore_create(0); + + // When + [MSACDispatcherUtil + dispatchSyncWithTimeout:blockTimeout + onQueue:serialQueue + withBlock:^{ + dispatch_semaphore_wait(delayedSemaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC))); + [expectation fulfill]; + }]; + + // Then + [self waitForExpectationsWithTimeout:0 + handler:^(NSError *error) { + dispatch_semaphore_signal(delayedSemaphore); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testDispatchSyncWithTimeoutWaitsForBlock { + + // If + NSTimeInterval blockTimeout = 0.5; + XCTestExpectation *expectation = [self expectationWithDescription:@"block called."]; + dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL); + + // When + [MSACDispatcherUtil dispatchSyncWithTimeout:blockTimeout + onQueue:serialQueue + withBlock:^{ + [NSThread sleepForTimeInterval:blockTimeout / 2]; + [expectation fulfill]; + }]; + + // Then + [self waitForExpectationsWithTimeout:0 + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testPerformBlockOnMainThreadFromBackground { + + // If + XCTestExpectation *expectation = [self expectationWithDescription:@"method called."]; + NSString *str = @"expectedString"; + + // When + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [MSACDispatcherUtil performBlockOnMainThread:^{ + [self methodToCall:str + completionHandler:^(NSString *string) { + XCTAssertEqual(str, string); + [expectation fulfill]; + }]; + }]; + }); + + // Then + [self waitForExpectationsWithTimeout:kMSACTestTimeout + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testArchivingData { + + // If + id value = @[ @{@"key" : @42}, @[ @1, @2, @3 ], @"value", [NSNull null] ]; + + // When + NSData *data = [MSACUtility archiveKeyedData:value]; + + // Then + XCTAssertNotNil(data); + + // When + id result = [MSACUtility unarchiveKeyedData:data]; + + // Then + XCTAssertEqualObjects(value, result); +} + +- (void)testArchivingNilData { + XCTAssertNil([MSACUtility archiveKeyedData:nil]); + XCTAssertNil([MSACUtility unarchiveKeyedData:nil]); +} + +- (void)testArchivingInvalidData { + XCTAssertNil([MSACUtility archiveKeyedData:self]); + XCTAssertNil([MSACUtility unarchiveKeyedData:[@"invalid" dataUsingEncoding:NSUTF8StringEncoding]]); +} + +- (void)methodToCall:(NSString *)str completionHandler:(void (^)(NSString *string))completion { + completion(str); +} + +- (void)methodWithArgs:(NSString *)str secondArg:(NSString *)secondStr completionHandler:(void (^)(NSString *, NSString *))completion { + completion(str, secondStr); +} + +// Before SDK 12.2 (bundled with Xcode 10.*) when running in a unit test bundle the bundle identifier is null. +// 12.2 and after the above bundle identifier is com.apple.dt.xctest.tool. +- (NSString *)getPathWithBundleIdentifier:(NSString *)path { + NSString *bundleId; +#if ((defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_12_2) || \ + (defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_10_14_4)) + bundleId = @"com.apple.dt.xctest.tool"; +#else + bundleId = @"(null)"; +#endif + return [NSString stringWithFormat:path, bundleId]; +} + +- (void)testArchivingWithClassesData { + + // If + [MSACUtility addMigrationClasses:@{@"MSACTestSessionInfo" : MSACSessionHistoryInfo.self}]; + NSString *sessionId = @"testSession"; + NSDate *pastDate = [NSDate dateWithTimeIntervalSince1970:0]; + + // When + MSACTestSessionInfo *testSessionInfo = [[MSACTestSessionInfo alloc] initWithTimestamp:pastDate andSessionId:sessionId]; + NSData *archiveSession = [MSACUtility archiveKeyedData:testSessionInfo]; + + // Then + MSACSessionHistoryInfo *unarchiveSession = (MSACSessionHistoryInfo *)[MSACUtility unarchiveKeyedData:archiveSession]; + XCTAssertTrue([unarchiveSession.timestamp isEqualToDate:pastDate]); +} + +- (void)testTrimPrefix { + NSString *expectedString = @"UtilityTests"; + XCTAssertTrue([expectedString isEqualToString:MSAC_CLASS_NAME_WITHOUT_PREFIX]); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACWrapperLoggerTests.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACWrapperLoggerTests.m new file mode 100644 index 0000000000..4f7ff2a430 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/MSACWrapperLoggerTests.m @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACLoggerInternal.h" +#import "MSACTestFrameworks.h" +#import "MSACWrapperLogger.h" + +@interface MSACWrapperLoggerTests : XCTestCase + +@end + +@implementation MSACWrapperLoggerTests + +- (void)testWrapperLogger { + + // If + __block XCTestExpectation *expectation = [self expectationWithDescription:@"Wrapper logger"]; + __block NSString *expectedMessage = @"expectedMessage"; + NSString *tag = @"TAG"; + __block NSString *message = nil; + MSACLogMessageProvider messageProvider = ^() { + message = expectedMessage; + [expectation fulfill]; + return message; + }; + + // When + [MSACLogger setCurrentLogLevel:MSACLogLevelDebug]; + [MSACWrapperLogger MSACWrapperLog:messageProvider tag:tag level:MSACLogLevelDebug]; + + // Then + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + XCTAssertEqual(expectedMessage, message); + }]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACDelegateForwarderTestUtil.h b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACDelegateForwarderTestUtil.h new file mode 100644 index 0000000000..c3d0c905b8 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACDelegateForwarderTestUtil.h @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +@interface MSACDelegateForwarderTestUtil : NSObject + +/** + * Generate a random class name. + * + * @return a class name. + */ ++ (NSString *)generateClassName; + +/** + * Create an instance of an object conforming to the given protocol. + * + * @param protocol Protocol to conform to. + * + * @return An instance of an object conforming to the given protocol. + */ ++ (id)createInstanceConformingToProtocol:(Protocol *)protocol; + +/** + * Create an instance of an object inheriting from the given base class and conforming to the given protocol. + * + * @param class Base class to inherit from. + * @param protocol Protocol to conform to. + * + * @return An instance of an object inheriting from the given base class and conforming to the given protocol. + */ ++ (id)createInstanceWithBaseClass:(Class)class andConformItToProtocol:(Protocol *)protocol; + +/** + * Add a selector with implementation to an instance. + * + * @param selector Selector. + * @param block Implementation. + * @param instance Instance to extend. + */ ++ (void)addSelector:(SEL)selector implementation:(id)block toInstance:(id)instance; + +/** + * Add a selector with implementation to a class. + * + * @param selector Selector. + * @param block Implementation. + * @param class Class to extend. + */ ++ (void)addSelector:(SEL)selector implementation:(id)block toClass:(id)class; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACDelegateForwarderTestUtil.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACDelegateForwarderTestUtil.m new file mode 100644 index 0000000000..10cb11f9d4 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACDelegateForwarderTestUtil.m @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACDelegateForwarderTestUtil.h" +#import "MSACUtility.h" + +@implementation MSACDelegateForwarderTestUtil + ++ (NSString *)generateClassName { + return [@"C" stringByAppendingString:MSAC_UUID_STRING]; +} + ++ (id)createInstanceConformingToProtocol:(Protocol *)protocol { + return [self createInstanceWithBaseClass:[NSObject class] andConformItToProtocol:protocol]; +} + ++ (id)createInstanceWithBaseClass:(Class)class andConformItToProtocol:(Protocol *)protocol { + + // Generate class name to prevent conflicts in runtime added classes. + const char *name = [[self generateClassName] UTF8String]; + Class newClass = objc_allocateClassPair(class, name, 0); + if (protocol) { + class_addProtocol(newClass, protocol); + } + objc_registerClassPair(newClass); + return [newClass new]; +} + ++ (void)addSelector:(SEL)selector implementation:(id)block toInstance:(id)instance { + [self addSelector:selector implementation:block toClass:[instance class]]; +} + ++ (void)addSelector:(SEL)selector implementation:(id)block toClass:(id)class { + Method method = class_getInstanceMethod(class, selector); + const char *types = method_getTypeEncoding(method); + IMP imp = imp_implementationWithBlock(block); + class_addMethod(class, selector, imp, types); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACHttpTestUtil.h b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACHttpTestUtil.h new file mode 100644 index 0000000000..d4f3479fa0 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACHttpTestUtil.h @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +@interface MSACHttpTestUtil : NSObject + ++ (void)stubHttp500Response; + ++ (void)stubHttp404Response; + ++ (void)stubHttp200Response; + ++ (void)stubNetworkDownResponse; + ++ (void)stubLongTimeOutResponse; + ++ (void)stubResponseWithData:(NSData *)data statusCode:(int)code headers:(NSDictionary *)headers name:(NSString *)name; + ++ (void)removeAllStubs; + ++ (NSHTTPURLResponse *)createMockResponseForStatusCode:(int)statusCode headers:(NSDictionary *)headers; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACHttpTestUtil.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACHttpTestUtil.m new file mode 100644 index 0000000000..beab836555 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACHttpTestUtil.m @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "HTTPStubs.h" + +#import "MSACConstants.h" +#import "MSACHttpTestUtil.h" +#import "MSACTestFrameworks.h" + +/* + * TODO: We need to reduce this response time from UID_MAX to 2.0 because [OHHTTPStubs removeAllStubs] is called before timeout and it + * results a crash with succeeded test. Testing on Xcode 8 doesn't have any issues on it but Xcode 9 complains. Keep in mind that 2 sec + * timeout is not somewhat we get from accurate testing, it is a heuristic number and it might fail any unit tests. + */ +static NSTimeInterval const kMSACStubbedResponseTimeout = 2.0; +static NSString *const kMSACStub500Name = @"httpStub_500"; +static NSString *const kMSACStub404Name = @"httpStub_404"; +static NSString *const kMSACStub200Name = @"httpStub_200"; +static NSString *const kMSACStubNetworkDownName = @"httpStub_NetworkDown"; +static NSString *const kMSACStubLongResponseTimeOutName = @"httpStub_LongResponseTimeOut"; + +@implementation MSACHttpTestUtil + ++ (void)stubHttp500Response { + [[self class] stubResponseWithCode:MSACHTTPCodesNo500InternalServerError name:kMSACStub500Name]; +} + ++ (void)stubHttp404Response { + [[self class] stubResponseWithCode:MSACHTTPCodesNo404NotFound name:kMSACStub404Name]; +} + ++ (void)stubHttp200Response { + [[self class] stubResponseWithCode:MSACHTTPCodesNo200OK name:kMSACStub200Name]; +} + ++ (void)removeAllStubs { + [HTTPStubs removeAllStubs]; +} + ++ (void)stubNetworkDownResponse { + NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:kCFURLErrorNotConnectedToInternet userInfo:nil]; + [[self class] stubResponseWithError:error name:kMSACStubNetworkDownName]; +} + ++ (void)stubLongTimeOutResponse { + [HTTPStubs + stubRequestsPassingTest:^BOOL(__unused NSURLRequest *request) { + return YES; + } + withStubResponse:^HTTPStubsResponse *(__unused NSURLRequest *request) { + HTTPStubsResponse *responseStub = [HTTPStubsResponse new]; + responseStub.statusCode = MSACHTTPCodesNo200OK; + return [responseStub responseTime:kMSACStubbedResponseTimeout]; + }] + .name = kMSACStubLongResponseTimeOutName; +} + ++ (void)stubResponseWithCode:(NSInteger)code name:(NSString *)name { + [HTTPStubs + stubRequestsPassingTest:^BOOL(__unused NSURLRequest *request) { + return YES; + } + withStubResponse:^HTTPStubsResponse *(__unused NSURLRequest *request) { + HTTPStubsResponse *responseStub = [HTTPStubsResponse new]; + responseStub.statusCode = (int)code; + return responseStub; + }] + .name = name; +} + ++ (void)stubResponseWithData:(NSData *)data statusCode:(int)code headers:(NSDictionary *)headers name:(NSString *)name { + [HTTPStubs + stubRequestsPassingTest:^BOOL(__unused NSURLRequest *request) { + return YES; + } + withStubResponse:^HTTPStubsResponse *(__unused NSURLRequest *request) { + return [HTTPStubsResponse responseWithData:data statusCode:code headers:headers]; + }] + .name = name; +} + ++ (void)stubResponseWithError:(NSError *)error name:(NSString *)name { + [HTTPStubs + stubRequestsPassingTest:^BOOL(__unused NSURLRequest *request) { + return YES; + } + withStubResponse:^HTTPStubsResponse *(__unused NSURLRequest *request) { + return [HTTPStubsResponse responseWithError:error]; + }] + .name = name; +} + ++ (NSHTTPURLResponse *)createMockResponseForStatusCode:(int)statusCode headers:(NSDictionary *)headers { + NSHTTPURLResponse *mockedResponse = OCMClassMock([NSHTTPURLResponse class]); + OCMStub([mockedResponse statusCode]).andReturn(statusCode); + OCMStub([mockedResponse allHeaderFields]).andReturn(headers); + return mockedResponse; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockCommonSchemaLog.h b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockCommonSchemaLog.h new file mode 100644 index 0000000000..8d5d03b505 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockCommonSchemaLog.h @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAbstractLogInternal.h" +#import "MSACCommonSchemaLog.h" + +@interface MSACMockCommonSchemaLog : MSACCommonSchemaLog + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockCommonSchemaLog.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockCommonSchemaLog.m new file mode 100644 index 0000000000..e3236dedac --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockCommonSchemaLog.m @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACMockCommonSchemaLog.h" + +static NSString *const kMSACTypeMockCommonSchemaLog = @"mockCommonSchemaLog"; + +@implementation MSACMockCommonSchemaLog + +- (instancetype)init { + if ((self = [super init])) { + self.type = kMSACTypeMockCommonSchemaLog; + } + return self; +} + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary]; + return dict; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockKeychainUtil.h b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockKeychainUtil.h new file mode 100644 index 0000000000..f25f0b149b --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockKeychainUtil.h @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACKeychainUtil.h" + +@interface MSACMockKeychainUtil : MSACKeychainUtil + ++ (void)mockStatusCode:(OSStatus)statusCode forKey:(NSString *)key; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockKeychainUtil.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockKeychainUtil.m new file mode 100644 index 0000000000..2b01a03123 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockKeychainUtil.m @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACMockKeychainUtil.h" +#import "MSACTestFrameworks.h" + +static NSMutableDictionary *> *stringsDictionary; +static NSMutableDictionary *> *arraysDictionary; +static NSMutableDictionary *> *statusCodes; +static NSString *kMSACDefaultServiceName = @"DefaultServiceName"; + +@interface MSACMockKeychainUtil () + +@property(nonatomic) id mockKeychainUtil; + +@end + +@implementation MSACMockKeychainUtil + ++ (void)load { + stringsDictionary = [NSMutableDictionary new]; + arraysDictionary = [NSMutableDictionary new]; + statusCodes = [NSMutableDictionary new]; +} + +- (instancetype)init { + self = [super init]; + if (self) { + + // Mock MSACUserDefaults shared method to return this instance. + _mockKeychainUtil = OCMClassMock([MSACKeychainUtil class]); + OCMStub([_mockKeychainUtil storeString:[OCMArg any] forKey:[OCMArg any]]).andCall([self class], @selector(storeString:forKey:)); + OCMStub([_mockKeychainUtil storeString:[OCMArg any] forKey:[OCMArg any] withServiceName:[OCMArg any]]) + .andCall([self class], @selector(storeString:forKey:withServiceName:)); + OCMStub([_mockKeychainUtil deleteStringForKey:[OCMArg any]]).andCall([self class], @selector(deleteStringForKey:)); + OCMStub([_mockKeychainUtil deleteStringForKey:[OCMArg any] withServiceName:[OCMArg any]]) + .andCall([self class], @selector(deleteStringForKey:withServiceName:)); + OCMStub([_mockKeychainUtil stringForKey:[OCMArg any] statusCode:[OCMArg anyPointer]]) + .andCall([self class], @selector(stringForKey:statusCode:)); + OCMStub([_mockKeychainUtil stringForKey:[OCMArg any] withServiceName:[OCMArg any] statusCode:[OCMArg anyPointer]]) + .andCall([self class], @selector(stringForKey:withServiceName:statusCode:)); + OCMStub([_mockKeychainUtil clear]).andCall([self class], @selector(clear)); + } + return self; +} + ++ (BOOL)storeString:(NSString *)string forKey:(NSString *)key { + return [self storeString:string forKey:key withServiceName:kMSACDefaultServiceName]; +} + ++ (BOOL)storeString:(NSString *)string forKey:(NSString *)key withServiceName:(NSString *)serviceName { + + // Don't store nil objects. + if (!string) { + return NO; + } + if (!stringsDictionary[serviceName]) { + stringsDictionary[serviceName] = [NSMutableDictionary new]; + } + stringsDictionary[serviceName][key] = string; + return YES; +} + ++ (NSString *_Nullable)deleteStringForKey:(NSString *)key { + return [self deleteStringForKey:key withServiceName:kMSACDefaultServiceName]; +} + ++ (NSString *_Nullable)deleteStringForKey:(NSString *)key withServiceName:(NSString *)serviceName { + NSString *value = stringsDictionary[serviceName][key]; + [stringsDictionary[serviceName] removeObjectForKey:key]; + return value; +} + ++ (NSString *_Nullable)stringForKey:(NSString *)key statusCode:(OSStatus *)statusCode { + return [self stringForKey:key withServiceName:kMSACDefaultServiceName statusCode:statusCode]; +} + ++ (NSString *_Nullable)stringForKey:(NSString *)key withServiceName:(NSString *)serviceName statusCode:(OSStatus *)statusCode { + OSStatus placeholderStatus = noErr; + if (statusCodes[serviceName] && statusCodes[serviceName][key]) { + placeholderStatus = [statusCodes[serviceName][key] intValue]; + } + if (statusCode) { + *statusCode = placeholderStatus; + } + if (placeholderStatus != noErr) { + return nil; + } + return stringsDictionary[serviceName][key]; +} + ++ (void)mockStatusCode:(OSStatus)statusCode forKey:(NSString *)key { + if (!statusCodes[kMSACDefaultServiceName]) { + statusCodes[kMSACDefaultServiceName] = [NSMutableDictionary new]; + } + statusCodes[kMSACDefaultServiceName][key] = @(statusCode); +} + ++ (BOOL)clear { + [stringsDictionary[kMSACDefaultServiceName] removeAllObjects]; + [arraysDictionary removeAllObjects]; + return YES; +} + +- (void)stopMocking { + [stringsDictionary removeAllObjects]; + [arraysDictionary removeAllObjects]; + [statusCodes removeAllObjects]; + [self.mockKeychainUtil stopMocking]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockLog.h b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockLog.h new file mode 100644 index 0000000000..8bf2023f2c --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockLog.h @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAbstractLogInternal.h" +#import "MSACLogWithProperties.h" + +@interface MSACMockLog : MSACLogWithProperties +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockLog.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockLog.m new file mode 100644 index 0000000000..0a70967301 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockLog.m @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACMockLog.h" + +static NSString *const kMSACTypeMockLog = @"mockLog"; + +@implementation MSACMockLog + +- (instancetype)init { + if ((self = [super init])) { + self.type = kMSACTypeMockLog; + } + return self; +} + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary]; + return dict; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockLogObject.h b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockLogObject.h new file mode 100644 index 0000000000..f169c7ea1d --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockLogObject.h @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACLog.h" + +@protocol MSACMockLogObject +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockLogWithConversion.h b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockLogWithConversion.h new file mode 100644 index 0000000000..96b210df96 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockLogWithConversion.h @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACLog.h" +#import "MSACLogConversion.h" + +@protocol MSACMockLogWithConversion +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockReachability.h b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockReachability.h new file mode 100644 index 0000000000..8dfaf4c9fe --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockReachability.h @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSAC_Reachability.h" + +@interface MSACMockReachability : NSObject + +/** + * A property indicating the current status of the network. + */ +@property(class) NetworkStatus currentNetworkStatus; + +/** + * Start to mock the MSAC_Reachability. + */ ++ (id)startMocking; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockReachability.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockReachability.m new file mode 100644 index 0000000000..a59346e30b --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockReachability.m @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACMockReachability.h" +#import "MSACTestFrameworks.h" + +static NSString *const kMSACNetworkReachabilityChangedNotificationName = @"kMSNetworkReachabilityChangedNotification"; + +@implementation MSACMockReachability + +static NetworkStatus _currentNetworkStatus; + ++ (void)setCurrentNetworkStatus:(NetworkStatus)networkStatus { + _currentNetworkStatus = networkStatus; +} + ++ (NetworkStatus)currentNetworkStatus { + return _currentNetworkStatus; +} + ++ (id)startMocking { + id mockReachability = OCMClassMock([MSAC_Reachability class]); + OCMStub([mockReachability reachabilityForInternetConnection]).andReturn(mockReachability); + OCMStub([mockReachability currentReachabilityStatus]).andDo(^(NSInvocation *invocation) { + NetworkStatus status = self.currentNetworkStatus; + [invocation setReturnValue:&status]; + }); + OCMStub([mockReachability startNotifier]).andDo(^(__unused NSInvocation *invocation) { + [[NSNotificationCenter defaultCenter] postNotificationName:kMSACNetworkReachabilityChangedNotificationName object:mockReachability]; + }); + return mockReachability; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockSecondService.h b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockSecondService.h new file mode 100644 index 0000000000..aec8cbcb12 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockSecondService.h @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACServiceAbstractInternal.h" + +@interface MSACMockSecondService : MSACServiceAbstract + ++ (void)resetSharedInstance; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockSecondService.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockSecondService.m new file mode 100644 index 0000000000..14d825089e --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockSecondService.m @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACMockSecondService.h" +#import "MSACChannelUnitConfiguration.h" + +static NSString *const kMSACServiceName = @"MSMockSecondService"; +static NSString *const kMSACGroupId = @"MSSecondMock"; +static MSACMockSecondService *sharedInstance = nil; + +@implementation MSACMockSecondService + +@synthesize channelGroup = _channelGroup; +@synthesize channelUnit = _channelUnit; +@synthesize channelUnitConfiguration = _channelUnitConfiguration; +@synthesize appSecret = _appSecret; +@synthesize defaultTransmissionTargetToken = _defaultTransmissionTargetToken; + +- (instancetype)init { + if ((self = [super init])) { + + // Init channel configuration. + _channelUnitConfiguration = [[MSACChannelUnitConfiguration alloc] initDefaultConfigurationWithGroupId:[self groupId]]; + } + return self; +} + ++ (instancetype)sharedInstance { + if (sharedInstance == nil) { + sharedInstance = [[self alloc] init]; + } + return sharedInstance; +} + ++ (void)resetSharedInstance { + sharedInstance = nil; +} + ++ (NSString *)serviceName { + return kMSACServiceName; +} + ++ (NSString *)logTag { + return @"AppCenterTest"; +} + +- (NSString *)groupId { + return kMSACGroupId; +} + +- (void)startWithChannelGroup:(id)__unused logManager appSecret:(NSString *)__unused appSecret { + self.started = YES; +} + +- (void)applyEnabledState:(BOOL)__unused isEnabled { +} + +- (BOOL)isAppSecretRequired { + return NO; +} + +- (BOOL)isAvailable { + return self.started; +} + +- (MSACInitializationPriority)initializationPriority { + return MSACInitializationPriorityDefault; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockService.h b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockService.h new file mode 100644 index 0000000000..9d54309952 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockService.h @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACServiceAbstractInternal.h" + +@interface MSACMockService : MSACServiceAbstract + ++ (void)resetSharedInstance; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockService.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockService.m new file mode 100644 index 0000000000..c6c70cf761 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockService.m @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACMockService.h" +#import "MSACChannelGroupProtocol.h" +#import "MSACChannelUnitConfiguration.h" + +static NSString *const kMSACServiceName = @"MSMockService"; +static NSString *const kMSACGroupId = @"MSMock"; +static MSACMockService *sharedInstance = nil; + +@implementation MSACMockService + +@synthesize channelGroup = _channelGroup; +@synthesize channelUnit = _channelUnit; +@synthesize channelUnitConfiguration = _channelUnitConfiguration; +@synthesize appSecret = _appSecret; +@synthesize defaultTransmissionTargetToken = _defaultTransmissionTargetToken; + +- (instancetype)init { + if ((self = [super init])) { + + // Init channel configuration. + _channelUnitConfiguration = [[MSACChannelUnitConfiguration alloc] initDefaultConfigurationWithGroupId:[self groupId]]; + } + return self; +} + ++ (instancetype)sharedInstance { + if (sharedInstance == nil) { + sharedInstance = [[self alloc] init]; + } + return sharedInstance; +} + ++ (void)resetSharedInstance { + sharedInstance = nil; +} + ++ (NSString *)serviceName { + return kMSACServiceName; +} + ++ (NSString *)logTag { + return @"AppCenterTest"; +} + +- (NSString *)groupId { + return kMSACGroupId; +} + +- (void)startWithChannelGroup:(id)channelGroup + appSecret:(nullable NSString *)appSecret + transmissionTargetToken:(nullable NSString *)token + fromApplication:(BOOL)fromApplication { + self.startedFromApplication = fromApplication; + self.channelGroup = channelGroup; + self.appSecret = appSecret; + self.defaultTransmissionTargetToken = token; + self.started = YES; + self.channelUnit = [self.channelGroup addChannelUnitWithConfiguration:self.channelUnitConfiguration]; +} + +- (void)applyEnabledState:(BOOL)__unused isEnabled { +} + +- (BOOL)isAvailable { + return self.started; +} + +- (MSACInitializationPriority)initializationPriority { + return MSACInitializationPriorityDefault; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockUserDefaults.h b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockUserDefaults.h new file mode 100644 index 0000000000..1e6896efc3 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockUserDefaults.h @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const kMSACMockMigrationKey = @"%@AppCenterMigratedKey"; + +@interface MSACMockUserDefaults : NSUserDefaults + +/** + * Clear dictionary. + */ +- (void)stopMocking; + +/** + * Migrates values for the old keys to new keys. + * @param migratedKeys a dictionary for keys that contains old key as a key of dictionary and new key as a value. + * @param service service name. + */ +- (void)migrateKeys:(NSDictionary *)migratedKeys forService:(nonnull NSString *)service; + +/** + * Get an object in the settings, returning object if key was found, NULL otherwise. + * + * @param key a unique key to identify the value. + */ +- (nullable id)objectForKey:(NSString *)aKey; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockUserDefaults.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockUserDefaults.m new file mode 100644 index 0000000000..47d55a41dc --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACMockUserDefaults.m @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACMockUserDefaults.h" +#import "MSACAppCenterUserDefaults.h" +#import "MSACAppCenterUserDefaultsPrivate.h" +#import "MSACTestFrameworks.h" + +@interface MSACMockUserDefaults () + +@property(nonatomic) NSMutableDictionary *dictionary; +@property(nonatomic) id mockMSACUserDefaults; + +@end + +@implementation MSACMockUserDefaults + +- (instancetype)init { + self = [super init]; + if (self) { + _dictionary = [NSMutableDictionary new]; + + // Mock MSACUserDefaults shared method to return this instance. + _mockMSACUserDefaults = OCMClassMock([MSACAppCenterUserDefaults class]); + OCMStub([_mockMSACUserDefaults shared]).andReturn(self); + } + return self; +} + +- (void)migrateKeys:(__unused NSDictionary *)migratedKeys forService:(nonnull NSString *)service { + [self setObject:@YES forKey:[NSString stringWithFormat:kMSACMockMigrationKey, service]]; +} + +- (void)setObject:(id)anObject forKey:(NSString *)aKey { + + // Don't store nil objects. + if (!anObject) { + return; + } + [self.dictionary setObject:anObject forKey:aKey]; +} + +- (nullable id)objectForKey:(NSString *)aKey { + return self.dictionary[aKey]; +} + +- (void)removeObjectForKey:(NSString *)aKey { + [self.dictionary removeObjectForKey:aKey]; +} + +- (void)stopMocking { + [self.dictionary removeAllObjects]; + [self.mockMSACUserDefaults stopMocking]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACStorageTestUtil.h b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACStorageTestUtil.h new file mode 100644 index 0000000000..93c3d3d695 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACStorageTestUtil.h @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +@interface MSACStorageTestUtil : NSObject + +/** + * The relative path to the DB. + */ +@property(nonatomic, copy) NSString *path; + +/** + * Custom init. + * + * @param fileName The file name of the db. + * + * @return The instance. + */ +- (instancetype)initWithDbFileName:(NSString *)fileName; + +/** + * Delete the database file, this can't be undone. Only used while testing. + */ +- (void)deleteDatabase; + +/** + * Get the size of the data in the test db. + * + * @return tThe size of the data in the db. + */ +- (long)getDataLengthInBytes; + +/** + * Open the test database. Make sure to close the handle once you're done! + * + * @return The handle to the db. + */ +- (sqlite3 *)openDatabase; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACStorageTestUtil.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACStorageTestUtil.m new file mode 100644 index 0000000000..8351b190ba --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACStorageTestUtil.m @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACStorageTestUtil.h" +#import "MSACUtility+File.h" + +@implementation MSACStorageTestUtil + +- (instancetype)initWithDbFileName:(NSString *)fileName { + if ((self = [super init])) { + _path = fileName; + } + return self; +} + +- (void)deleteDatabase { + if (self.path) { + [MSACUtility deleteItemForPathComponent:self.path]; + } +} + +- (long)getDataLengthInBytes { + sqlite3 *db = [self openDatabase]; + sqlite3_stmt *statement = NULL; + sqlite3_prepare_v2(db, "PRAGMA page_count;", -1, &statement, NULL); + sqlite3_step(statement); + int pageCount = sqlite3_column_int(statement, 0); + sqlite3_finalize(statement); + sqlite3_prepare_v2(db, "PRAGMA page_size;", -1, &statement, NULL); + sqlite3_step(statement); + int pageSize = sqlite3_column_int(statement, 0); + sqlite3_finalize(statement); + sqlite3_close(db); + return (long)pageCount * pageSize; +} + +- (sqlite3 *)openDatabase { + sqlite3 *db = NULL; + NSURL *dbURL = [MSACUtility createFileAtPathComponent:self.path withData:nil atomically:NO forceOverwrite:NO]; + sqlite3_open_v2([[dbURL absoluteString] UTF8String], &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_URI, NULL); + return db; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACTestFrameworks.h b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACTestFrameworks.h new file mode 100644 index 0000000000..e7dd120893 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACTestFrameworks.h @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdocumentation" +#pragma clang diagnostic ignored "-Wdocumentation-deprecated-sync" +#pragma clang diagnostic ignored "-Wdocumentation-unknown-command" +#pragma clang diagnostic ignored "-Wobjc-interface-ivars" + +#import +#import +#import + +#pragma clang diagnostic pop diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACTestSessionInfo.h b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACTestSessionInfo.h new file mode 100644 index 0000000000..f06f02160c --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACTestSessionInfo.h @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACHistoryInfo.h" + +@interface MSACTestSessionInfo : MSACHistoryInfo + +@property(nonatomic, copy) NSString *sessionId; + +- (instancetype)initWithTimestamp:(NSDate *)timestamp andSessionId:(NSString *)sessionId; + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACTestSessionInfo.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACTestSessionInfo.m new file mode 100644 index 0000000000..b01f0c313d --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACTestSessionInfo.m @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACTestSessionInfo.h" +#import + +@implementation MSACTestSessionInfo + +- (instancetype)initWithTimestamp:(NSDate *)timestamp andSessionId:(NSString *)sessionId { + self = [super initWithTimestamp:timestamp]; + if (self) { + _sessionId = sessionId; + } + return self; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACTestUtil.h b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACTestUtil.h new file mode 100644 index 0000000000..bf39628a15 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACTestUtil.h @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +@class MSACLogContainer; +@class MSACDevice; + +NS_ASSUME_NONNULL_BEGIN + +@interface MSACTestUtil : NSObject + ++ (MSACLogContainer *)createLogContainerWithId:(NSString *)batchId device:(MSACDevice *)device; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACTestUtil.m b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACTestUtil.m new file mode 100644 index 0000000000..63a0c3dd1b --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenter/AppCenterTests/Util/MSACTestUtil.m @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACTestUtil.h" +#import "MSACDevice.h" +#import "MSACLogContainer.h" +#import "MSACMockLog.h" +#import "MSACUtility+StringFormatting.h" + +@implementation MSACTestUtil + ++ (MSACLogContainer *)createLogContainerWithId:(NSString *)batchId device:(MSACDevice *)device { + MSACMockLog *log1 = [[MSACMockLog alloc] init]; + log1.sid = MSAC_UUID_STRING; + log1.timestamp = [NSDate date]; + log1.device = device; + + MSACMockLog *log2 = [[MSACMockLog alloc] init]; + log2.sid = MSAC_UUID_STRING; + log2.timestamp = [NSDate date]; + log2.device = device; + + MSACLogContainer *logContainer = [[MSACLogContainer alloc] initWithBatchId:batchId andLogs:(NSArray> *)@[ log1, log2 ]]; + return logContainer; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics.xcodeproj/project.pbxproj b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..77cf2d847f --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics.xcodeproj/project.pbxproj @@ -0,0 +1,891 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + C9A9208A230C5E390068070D /* MSACAbstractLog.h in Headers */ = {isa = PBXBuildFile; fileRef = 358F9BC72019596400B9E22C /* MSACAbstractLog.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C9A9208B230C5E390068070D /* MSACConstants+Flags.h in Headers */ = {isa = PBXBuildFile; fileRef = 04B525BC2194D49C00FA37FD /* MSACConstants+Flags.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C9A9208C230C5E390068070D /* MSACLogWithProperties.h in Headers */ = {isa = PBXBuildFile; fileRef = 358F9BC52019590100B9E22C /* MSACLogWithProperties.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C9A9208D230C5E390068070D /* MSACService.h in Headers */ = {isa = PBXBuildFile; fileRef = 04A082051F74BB8600DC776D /* MSACService.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C9A9208E230C5E390068070D /* MSACServiceAbstract.h in Headers */ = {isa = PBXBuildFile; fileRef = 387C77041D6CC39400D68CC1 /* MSACServiceAbstract.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C9A9208F230C5E4B0068070D /* AppCenterAnalytics.h in Headers */ = {isa = PBXBuildFile; fileRef = E85547CC1D2D63EF002DF6E2 /* AppCenterAnalytics.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C9A92090230C5E4B0068070D /* MSACAnalytics.h in Headers */ = {isa = PBXBuildFile; fileRef = E85547C31D2D6253002DF6E2 /* MSACAnalytics.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C9A920A1230C5E620068070D /* AppCenter+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = E85547CF1D2D6521002DF6E2 /* AppCenter+Internal.h */; }; + C9A920A2230C5E620068070D /* MSACAnalyticsInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 3813B9641DBFE68200831214 /* MSACAnalyticsInternal.h */; }; + C9A920A3230C5E620068070D /* MSACAnalyticsDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 35BF19E21DF9C43F00193027 /* MSACAnalyticsDelegate.h */; }; + C9A920A4230C5E620068070D /* MSACAnalyticsTransmissionTargetInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 351343352057093600E6DC7D /* MSACAnalyticsTransmissionTargetInternal.h */; }; + C9A920A5230C5E620068070D /* MSACAnalytics+Validation.h in Headers */ = {isa = PBXBuildFile; fileRef = C27452D520AE0EF100B64B68 /* MSACAnalytics+Validation.h */; }; + C9A920A6230C5E620068070D /* MSACPropertyConfiguratorInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 042E5E002175230600AFD6F9 /* MSACPropertyConfiguratorInternal.h */; }; + C9A920AD230C5E6D0068070D /* MSACAnalyticsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = E85547CE1D2D64F0002DF6E2 /* MSACAnalyticsPrivate.h */; }; + C9A920AE230C5E6D0068070D /* MSACAnalyticsTransmissionTargetPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 3855D51320E2F03000CBC499 /* MSACAnalyticsTransmissionTargetPrivate.h */; }; + C9A920AF230C5E6D0068070D /* MSACPropertyConfiguratorPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = E758FA8320FFE0E200011793 /* MSACPropertyConfiguratorPrivate.h */; }; + C9A920B4230C5E9E0068070D /* MSACEventLogPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 38337D6E20C0AB0D00CEDA17 /* MSACEventLogPrivate.h */; }; + C9A920B5230C5E9E0068070D /* MSACSessionTrackerPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 042B1D291FE3508100D6E04A /* MSACSessionTrackerPrivate.h */; }; + C9A920BC230C5EBE0068070D /* MSACEventPropertiesInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 266ED07D29C2A2CC67AD1F60 /* MSACEventPropertiesInternal.h */; }; + C9A920BD230C5EBE0068070D /* MSACPageLog.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E3E2CC71D359D5E00B1EE50 /* MSACPageLog.h */; }; + C9A920BE230C5EBE0068070D /* MSACStartSessionLog.h in Headers */ = {isa = PBXBuildFile; fileRef = E8E48FA01D51670100A8C1B0 /* MSACStartSessionLog.h */; }; + C9A920C3230C5EC50068070D /* MSACSessionTrackerDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E89B33EB1D5A8F3500FDE8FB /* MSACSessionTrackerDelegate.h */; }; + C9A920C4230C5EC50068070D /* MSACSessionTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = E8E48F9C1D515C3900A8C1B0 /* MSACSessionTracker.h */; }; + C9A920C9230C5ECB0068070D /* MSACAnalyticsCategory.h in Headers */ = {isa = PBXBuildFile; fileRef = 0485AF8D1EAA852A00C10CAF /* MSACAnalyticsCategory.h */; }; + C9A920CA230C5ECB0068070D /* MSACAnalyticsConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 2DA03D5D8EA1FEA1DC687CEF /* MSACAnalyticsConstants.h */; }; + C9A920D1230C5ED40068070D /* MSACEventProperties.h in Headers */ = {isa = PBXBuildFile; fileRef = 266ED289BE3D9562F1F61348 /* MSACEventProperties.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C9A920D2230C5ED40068070D /* MSACEventLog.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E3E2CC51D359D5E00B1EE50 /* MSACEventLog.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C9A920D3230C5ED40068070D /* MSACLogWithNameAndProperties.h in Headers */ = {isa = PBXBuildFile; fileRef = F834E37020AC8E25003CB54D /* MSACLogWithNameAndProperties.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C9A920DC230C5EE50068070D /* MSACAnalyticsAuthenticationProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = B2E2ECB72114DAB800C688C0 /* MSACAnalyticsAuthenticationProvider.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C9A920DD230C5EE50068070D /* MSACAnalyticsAuthenticationProviderDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = B2ADB12F2123A72000D0D7D9 /* MSACAnalyticsAuthenticationProviderDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C9A920DE230C5EE50068070D /* MSACAnalyticsTransmissionTarget.h in Headers */ = {isa = PBXBuildFile; fileRef = 3513432F205704A100E6DC7D /* MSACAnalyticsTransmissionTarget.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C9A920DF230C5EE50068070D /* MSACPropertyConfigurator.h in Headers */ = {isa = PBXBuildFile; fileRef = E758FA7B20FFDEE700011793 /* MSACPropertyConfigurator.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C9A920E2230C5EF00068070D /* MSACAnalyticsAuthenticationProviderInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = B2350985211A69AA00F98D4F /* MSACAnalyticsAuthenticationProviderInternal.h */; }; + C9EBAA7F230D39CA00A20F0F /* MSACAnalytics.m in Sources */ = {isa = PBXBuildFile; fileRef = E85547C51D2D6253002DF6E2 /* MSACAnalytics.m */; }; + C9EBAA80230D39CA00A20F0F /* MSACAnalytics+Validation.m in Sources */ = {isa = PBXBuildFile; fileRef = C27452D120AE0DAC00B64B68 /* MSACAnalytics+Validation.m */; }; + C9EBAA81230D39CA00A20F0F /* MSACPageLog.m in Sources */ = {isa = PBXBuildFile; fileRef = 6E3E2CC81D359D5E00B1EE50 /* MSACPageLog.m */; }; + C9EBAA82230D39CA00A20F0F /* MSACStartSessionLog.m in Sources */ = {isa = PBXBuildFile; fileRef = E8E48FA11D51670100A8C1B0 /* MSACStartSessionLog.m */; }; + C9EBAA83230D39CA00A20F0F /* MSACSessionTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = E8E48F9D1D515C3900A8C1B0 /* MSACSessionTracker.m */; }; + C9EBAA84230D39CA00A20F0F /* MSACAnalyticsCategory.m in Sources */ = {isa = PBXBuildFile; fileRef = 0485AF8E1EAA852A00C10CAF /* MSACAnalyticsCategory.m */; }; + C9EBAA85230D39CA00A20F0F /* MSACEventProperties.m in Sources */ = {isa = PBXBuildFile; fileRef = 266EDBD8487DA42DAB601DD9 /* MSACEventProperties.m */; }; + C9EBAA86230D39CA00A20F0F /* MSACEventLog.m in Sources */ = {isa = PBXBuildFile; fileRef = 6E3E2CC61D359D5E00B1EE50 /* MSACEventLog.m */; }; + C9EBAA87230D39CA00A20F0F /* MSACLogWithNameAndProperties.m in Sources */ = {isa = PBXBuildFile; fileRef = F834E37120AC8E25003CB54D /* MSACLogWithNameAndProperties.m */; }; + C9EBAA88230D39CA00A20F0F /* MSACAnalyticsAuthenticationProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = B2E2ECB82114DAB800C688C0 /* MSACAnalyticsAuthenticationProvider.m */; }; + C9EBAA89230D39CA00A20F0F /* MSACAnalyticsTransmissionTarget.m in Sources */ = {isa = PBXBuildFile; fileRef = 35134330205704C100E6DC7D /* MSACAnalyticsTransmissionTarget.m */; }; + C9EBAA8A230D39CA00A20F0F /* MSACPropertyConfigurator.m in Sources */ = {isa = PBXBuildFile; fileRef = E758FA7C20FFDEE700011793 /* MSACPropertyConfigurator.m */; }; + C9EBAB2B230D72F900A20F0F /* AppCenter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C9EBAB2A230D72F900A20F0F /* AppCenter.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + C2392F0924642C8700425640 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C2392EFF24642C8700425640 /* OCMock.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 030EF0A814632FD000B04273; + remoteInfo = OCMock; + }; + C2392F0B24642C8700425640 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C2392EFF24642C8700425640 /* OCMock.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 03565A3118F0566E003AE91E; + remoteInfo = OCMockTests; + }; + C2392F0D24642C8700425640 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C2392EFF24642C8700425640 /* OCMock.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 030EF0DC14632FF700B04273; + remoteInfo = OCMockLib; + }; + C2392F0F24642C8700425640 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C2392EFF24642C8700425640 /* OCMock.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = D31108AD1828DB8700737925; + remoteInfo = OCMockLibTests; + }; + C2392F1124642C8700425640 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C2392EFF24642C8700425640 /* OCMock.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = F0B950F11B0080BE00942C38; + remoteInfo = "OCMock iOS"; + }; + C2392F1324642C8700425640 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C2392EFF24642C8700425640 /* OCMock.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 817EB1621BD765130047E85A; + remoteInfo = "OCMock tvOS"; + }; + C2392F1524642C8700425640 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C2392EFF24642C8700425640 /* OCMock.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 8DE97CA022B43EE60098C63F; + remoteInfo = "OCMock watchOS"; + }; + DFCB802D2472D5D70058D292 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DFCB80232472D5D70058D292 /* OCHamcrest.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 087601E213440806001B439B; + remoteInfo = OCHamcrest; + }; + DFCB802F2472D5D70058D292 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DFCB80232472D5D70058D292 /* OCHamcrest.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 087601F713440807001B439B; + remoteInfo = OCHamcrestTests; + }; + DFCB80312472D5D70058D292 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DFCB80232472D5D70058D292 /* OCHamcrest.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 081BEE621345979F003F846A; + remoteInfo = libochamcrest; + }; + DFCB80332472D5D70058D292 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DFCB80232472D5D70058D292 /* OCHamcrest.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 081BEE6C1345979F003F846A; + remoteInfo = libochamcrestTests; + }; + DFCB80352472D5D70058D292 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DFCB80232472D5D70058D292 /* OCHamcrest.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 24C5BFE91C1777D900C2BAFD; + remoteInfo = "OCHamcrest-iOS"; + }; + DFCB80372472D5D70058D292 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DFCB80232472D5D70058D292 /* OCHamcrest.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 24C5BFF61C17781400C2BAFD; + remoteInfo = "OCHamcrest-tvOS"; + }; + DFCB80392472D5D70058D292 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DFCB80232472D5D70058D292 /* OCHamcrest.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 24C5C0031C17782500C2BAFD; + remoteInfo = "OCHamcrest-watchOS"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 041CFF8B1ECCFF6700B4654B /* Tests iOS.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = "Tests iOS.xcconfig"; path = "../../Config/Tests iOS.xcconfig"; sourceTree = ""; }; + 042A17A71DEFA950003BA80A /* MSACAnalyticsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACAnalyticsTests.m; sourceTree = ""; }; + 042B1D291FE3508100D6E04A /* MSACSessionTrackerPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACSessionTrackerPrivate.h; sourceTree = ""; }; + 042E5E002175230600AFD6F9 /* MSACPropertyConfiguratorInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSACPropertyConfiguratorInternal.h; path = ../TransmissionTarget/MSACPropertyConfiguratorInternal.h; sourceTree = ""; }; + 04311FF11EE0858F007054C5 /* MSACTestFrameworks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSACTestFrameworks.h; path = ../../../AppCenter/AppCenterTests/Util/MSACTestFrameworks.h; sourceTree = ""; }; + 043121721EE0C248007054C5 /* tvOS.modulemap */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = "sourcecode.module-map"; path = tvOS.modulemap; sourceTree = ""; }; + 043121731EE0C248007054C5 /* tvOS.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = tvOS.xcconfig; sourceTree = ""; }; + 04545DC9227B604700A49E06 /* AppCenterAnalytics.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppCenterAnalytics.xcconfig; sourceTree = ""; }; + 0469D1B21F4DFE4C00A43A8E /* AppCenterAnalytics Release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "AppCenterAnalytics Release.xcconfig"; sourceTree = ""; }; + 0484DD601F3910FF0092B777 /* Tests tvOS.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = "Tests tvOS.xcconfig"; path = "../../Config/Tests tvOS.xcconfig"; sourceTree = ""; }; + 0485AF8D1EAA852A00C10CAF /* MSACAnalyticsCategory.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACAnalyticsCategory.h; sourceTree = ""; }; + 0485AF8E1EAA852A00C10CAF /* MSACAnalyticsCategory.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACAnalyticsCategory.m; sourceTree = ""; }; + 049BC82A1ECE3B9400FB6719 /* iOS.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = iOS.xcconfig; sourceTree = ""; }; + 049BC82B1ECE3B9A00FB6719 /* macOS.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = macOS.xcconfig; sourceTree = ""; }; + 049BC82C1ECE3CF000FB6719 /* Tests macOS.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = "Tests macOS.xcconfig"; path = "../../Config/Tests macOS.xcconfig"; sourceTree = ""; }; + 04A082051F74BB8600DC776D /* MSACService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSACService.h; path = ../AppCenter/AppCenter/MSACService.h; sourceTree = ""; }; + 04B525BC2194D49C00FA37FD /* MSACConstants+Flags.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "MSACConstants+Flags.h"; path = "../AppCenter/AppCenter/MSACConstants+Flags.h"; sourceTree = ""; }; + 04ED31E91EAAD32B0033BAAE /* macOS.modulemap */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = "sourcecode.module-map"; path = macOS.modulemap; sourceTree = ""; }; + 04ED31EB1EAAD3390033BAAE /* iOS.modulemap */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = "sourcecode.module-map"; path = iOS.modulemap; sourceTree = ""; }; + 266ED07D29C2A2CC67AD1F60 /* MSACEventPropertiesInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACEventPropertiesInternal.h; sourceTree = ""; }; + 266ED289BE3D9562F1F61348 /* MSACEventProperties.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACEventProperties.h; sourceTree = ""; }; + 266EDBD8487DA42DAB601DD9 /* MSACEventProperties.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACEventProperties.m; sourceTree = ""; }; + 2DA03D5D8EA1FEA1DC687CEF /* MSACAnalyticsConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACAnalyticsConstants.h; sourceTree = ""; }; + 324F3BA8254789FD0006E223 /* XCFramework.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = XCFramework.xcconfig; path = ../../../Config/XCFramework.xcconfig; sourceTree = ""; }; + 324F3BA9254789FD0006E223 /* iOS Universal.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "iOS Universal.xcconfig"; path = "../../../Config/iOS Universal.xcconfig"; sourceTree = ""; }; + 324F3BAA254789FE0006E223 /* tvOS Universal.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "tvOS Universal.xcconfig"; path = "../../../Config/tvOS Universal.xcconfig"; sourceTree = ""; }; + 3513432F205704A100E6DC7D /* MSACAnalyticsTransmissionTarget.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACAnalyticsTransmissionTarget.h; sourceTree = ""; }; + 35134330205704C100E6DC7D /* MSACAnalyticsTransmissionTarget.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACAnalyticsTransmissionTarget.m; sourceTree = ""; }; + 351343352057093600E6DC7D /* MSACAnalyticsTransmissionTargetInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACAnalyticsTransmissionTargetInternal.h; sourceTree = ""; }; + 351343362058900800E6DC7D /* MSACAnalyticsTransmissionTargetTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACAnalyticsTransmissionTargetTests.m; sourceTree = ""; }; + 358F9BC52019590100B9E22C /* MSACLogWithProperties.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSACLogWithProperties.h; path = ../AppCenter/AppCenter/Model/MSACLogWithProperties.h; sourceTree = ""; }; + 358F9BC72019596400B9E22C /* MSACAbstractLog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSACAbstractLog.h; path = ../AppCenter/AppCenter/Model/MSACAbstractLog.h; sourceTree = ""; }; + 35A204BE216C1AC600FEBADA /* MSACEventPropertiesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACEventPropertiesTests.m; sourceTree = ""; }; + 35BF19E21DF9C43F00193027 /* MSACAnalyticsDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACAnalyticsDelegate.h; sourceTree = ""; }; + 35BF19E31DF9D58F00193027 /* MSACMockAnalyticsDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACMockAnalyticsDelegate.h; sourceTree = ""; }; + 35BF19E41DF9D59E00193027 /* MSACMockAnalyticsDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACMockAnalyticsDelegate.m; sourceTree = ""; }; + 3813B9641DBFE68200831214 /* MSACAnalyticsInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACAnalyticsInternal.h; sourceTree = ""; }; + 38337D6E20C0AB0D00CEDA17 /* MSACEventLogPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACEventLogPrivate.h; sourceTree = ""; }; + 3855D51320E2F03000CBC499 /* MSACAnalyticsTransmissionTargetPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACAnalyticsTransmissionTargetPrivate.h; sourceTree = ""; }; + 387C77041D6CC39400D68CC1 /* MSACServiceAbstract.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSACServiceAbstract.h; path = ../AppCenter/AppCenter/MSACServiceAbstract.h; sourceTree = ""; }; + 38C215DF20E692C700191F3C /* MSACMockUserDefaults.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MSACMockUserDefaults.m; path = ../../../AppCenter/AppCenterTests/Util/MSACMockUserDefaults.m; sourceTree = ""; }; + 38C215E020E692C700191F3C /* MSACMockUserDefaults.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSACMockUserDefaults.h; path = ../../../AppCenter/AppCenterTests/Util/MSACMockUserDefaults.h; sourceTree = ""; }; + 6E3E2CC51D359D5E00B1EE50 /* MSACEventLog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACEventLog.h; sourceTree = ""; }; + 6E3E2CC61D359D5E00B1EE50 /* MSACEventLog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACEventLog.m; sourceTree = ""; }; + 6E3E2CC71D359D5E00B1EE50 /* MSACPageLog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACPageLog.h; sourceTree = ""; }; + 6E3E2CC81D359D5E00B1EE50 /* MSACPageLog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACPageLog.m; sourceTree = ""; }; + 6E3E2CD01D359F3300B1EE50 /* MSACEventLogTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACEventLogTests.m; sourceTree = ""; }; + 6E3E2CD11D359F3300B1EE50 /* MSACPageLogTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACPageLogTests.m; sourceTree = ""; }; + B2350985211A69AA00F98D4F /* MSACAnalyticsAuthenticationProviderInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACAnalyticsAuthenticationProviderInternal.h; sourceTree = ""; }; + B25E21CC214044A400CAA156 /* MSACPropertyConfiguratorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACPropertyConfiguratorTests.m; sourceTree = ""; }; + B26D4DDE211BA99E00AB4E28 /* MSACAnalyticsAuthenticationProviderTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACAnalyticsAuthenticationProviderTests.m; sourceTree = ""; }; + B2812EF21DA3148000307DCE /* AppCenterAnalytics Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "AppCenterAnalytics Debug.xcconfig"; sourceTree = ""; }; + B2ADB12F2123A72000D0D7D9 /* MSACAnalyticsAuthenticationProviderDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACAnalyticsAuthenticationProviderDelegate.h; sourceTree = ""; }; + B2E2ECB72114DAB800C688C0 /* MSACAnalyticsAuthenticationProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACAnalyticsAuthenticationProvider.h; sourceTree = ""; }; + B2E2ECB82114DAB800C688C0 /* MSACAnalyticsAuthenticationProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACAnalyticsAuthenticationProvider.m; sourceTree = ""; }; + C2392EFF24642C8700425640 /* OCMock.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = OCMock.xcodeproj; path = ../../Vendor/OCMock/Source/OCMock.xcodeproj; sourceTree = ""; }; + C27452D120AE0DAC00B64B68 /* MSACAnalytics+Validation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "MSACAnalytics+Validation.m"; sourceTree = ""; }; + C27452D520AE0EF100B64B68 /* MSACAnalytics+Validation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MSACAnalytics+Validation.h"; sourceTree = ""; }; + C2D73CE1230FC7DC00390198 /* AppCenter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AppCenter.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C2D73CE4230FC7EF00390198 /* AppCenter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AppCenter.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C2D73CE7230FC7FC00390198 /* AppCenter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AppCenter.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C9A91F3F230BFB970068070D /* AppCenterAnalytics.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AppCenterAnalytics.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C9EBAB18230D724700A20F0F /* AppCenter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AppCenter.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C9EBAB28230D72F300A20F0F /* AppCenter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AppCenter.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C9EBAB2A230D72F900A20F0F /* AppCenter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AppCenter.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DFCB80232472D5D70058D292 /* OCHamcrest.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = OCHamcrest.xcodeproj; path = ../../Vendor/OCHamcrest/Source/OCHamcrest.xcodeproj; sourceTree = ""; }; + E758FA7B20FFDEE700011793 /* MSACPropertyConfigurator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACPropertyConfigurator.h; sourceTree = ""; }; + E758FA7C20FFDEE700011793 /* MSACPropertyConfigurator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACPropertyConfigurator.m; sourceTree = ""; }; + E758FA8320FFE0E200011793 /* MSACPropertyConfiguratorPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACPropertyConfiguratorPrivate.h; sourceTree = ""; }; + E81591291D526956003D5F3C /* MSACStartSessionLogTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACStartSessionLogTests.m; sourceTree = ""; }; + E815912B1D526A09003D5F3C /* MSACSessionTrackerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACSessionTrackerTests.m; sourceTree = ""; }; + E81591301D526C3B003D5F3C /* MSACSessionTrackerUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACSessionTrackerUtil.h; sourceTree = ""; }; + E81591311D526C3B003D5F3C /* MSACSessionTrackerUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACSessionTrackerUtil.m; sourceTree = ""; }; + E85547C31D2D6253002DF6E2 /* MSACAnalytics.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACAnalytics.h; sourceTree = ""; }; + E85547C51D2D6253002DF6E2 /* MSACAnalytics.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = MSACAnalytics.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + E85547CC1D2D63EF002DF6E2 /* AppCenterAnalytics.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppCenterAnalytics.h; sourceTree = ""; }; + E85547CE1D2D64F0002DF6E2 /* MSACAnalyticsPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACAnalyticsPrivate.h; sourceTree = ""; }; + E85547CF1D2D6521002DF6E2 /* AppCenter+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "AppCenter+Internal.h"; path = "../../../AppCenter/AppCenter/Internals/AppCenter+Internal.h"; sourceTree = ""; }; + E85547D91D2D6723002DF6E2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E89B33EB1D5A8F3500FDE8FB /* MSACSessionTrackerDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACSessionTrackerDelegate.h; sourceTree = ""; }; + E8E48F9C1D515C3900A8C1B0 /* MSACSessionTracker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACSessionTracker.h; sourceTree = ""; }; + E8E48F9D1D515C3900A8C1B0 /* MSACSessionTracker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = MSACSessionTracker.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + E8E48FA01D51670100A8C1B0 /* MSACStartSessionLog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACStartSessionLog.h; sourceTree = ""; }; + E8E48FA11D51670100A8C1B0 /* MSACStartSessionLog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACStartSessionLog.m; sourceTree = ""; }; + F82E4C70217F1FA600EDAB34 /* sqlite3.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = sqlite3.c; path = ../../Vendor/SQLite3/sqlite3.c; sourceTree = ""; }; + F834E37020AC8E25003CB54D /* MSACLogWithNameAndProperties.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACLogWithNameAndProperties.h; sourceTree = ""; }; + F834E37120AC8E25003CB54D /* MSACLogWithNameAndProperties.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACLogWithNameAndProperties.m; sourceTree = ""; }; + F8D68481230D7D74000A7CED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + C9A91F3A230BFB970068070D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C9EBAB2B230D72F900A20F0F /* AppCenter.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 04A1409C1ECE7245001CEE94 /* Util */ = { + isa = PBXGroup; + children = ( + 0485AF8D1EAA852A00C10CAF /* MSACAnalyticsCategory.h */, + 0485AF8E1EAA852A00C10CAF /* MSACAnalyticsCategory.m */, + 2DA03D5D8EA1FEA1DC687CEF /* MSACAnalyticsConstants.h */, + ); + path = Util; + sourceTree = ""; + }; + 04A140AE1ECE806B001CEE94 /* Support */ = { + isa = PBXGroup; + children = ( + 041CFF8B1ECCFF6700B4654B /* Tests iOS.xcconfig */, + 049BC82C1ECE3CF000FB6719 /* Tests macOS.xcconfig */, + 0484DD601F3910FF0092B777 /* Tests tvOS.xcconfig */, + ); + name = Support; + sourceTree = ""; + }; + 04A140B11ECE811D001CEE94 /* Model */ = { + isa = PBXGroup; + children = ( + 35BF19E31DF9D58F00193027 /* MSACMockAnalyticsDelegate.h */, + 35BF19E41DF9D59E00193027 /* MSACMockAnalyticsDelegate.m */, + ); + name = Model; + sourceTree = ""; + }; + 04A140B21ECE814E001CEE94 /* Util */ = { + isa = PBXGroup; + children = ( + 38C215E020E692C700191F3C /* MSACMockUserDefaults.h */, + 38C215DF20E692C700191F3C /* MSACMockUserDefaults.m */, + E81591301D526C3B003D5F3C /* MSACSessionTrackerUtil.h */, + E81591311D526C3B003D5F3C /* MSACSessionTrackerUtil.m */, + 04311FF11EE0858F007054C5 /* MSACTestFrameworks.h */, + ); + path = Util; + sourceTree = ""; + }; + 242A6C5D22D3F57500CB02C7 /* Model */ = { + isa = PBXGroup; + children = ( + 266ED289BE3D9562F1F61348 /* MSACEventProperties.h */, + 266EDBD8487DA42DAB601DD9 /* MSACEventProperties.m */, + 6E3E2CC51D359D5E00B1EE50 /* MSACEventLog.h */, + 6E3E2CC61D359D5E00B1EE50 /* MSACEventLog.m */, + F834E37020AC8E25003CB54D /* MSACLogWithNameAndProperties.h */, + F834E37120AC8E25003CB54D /* MSACLogWithNameAndProperties.m */, + ); + path = Model; + sourceTree = ""; + }; + 3513432E2057046700E6DC7D /* TransmissionTarget */ = { + isa = PBXGroup; + children = ( + B2E2ECB72114DAB800C688C0 /* MSACAnalyticsAuthenticationProvider.h */, + B2ADB12F2123A72000D0D7D9 /* MSACAnalyticsAuthenticationProviderDelegate.h */, + B2350985211A69AA00F98D4F /* MSACAnalyticsAuthenticationProviderInternal.h */, + B2E2ECB82114DAB800C688C0 /* MSACAnalyticsAuthenticationProvider.m */, + 3513432F205704A100E6DC7D /* MSACAnalyticsTransmissionTarget.h */, + 35134330205704C100E6DC7D /* MSACAnalyticsTransmissionTarget.m */, + E758FA7B20FFDEE700011793 /* MSACPropertyConfigurator.h */, + E758FA7C20FFDEE700011793 /* MSACPropertyConfigurator.m */, + ); + path = TransmissionTarget; + sourceTree = ""; + }; + 6E0684361D35A39300A8CC6C /* Frameworks */ = { + isa = PBXGroup; + children = ( + DFCB80232472D5D70058D292 /* OCHamcrest.xcodeproj */, + C2392EFF24642C8700425640 /* OCMock.xcodeproj */, + F82E4C70217F1FA600EDAB34 /* sqlite3.c */, + ); + name = Frameworks; + sourceTree = ""; + }; + 6E3E2CC21D359D5E00B1EE50 /* Model */ = { + isa = PBXGroup; + children = ( + 38337D6E20C0AB0D00CEDA17 /* MSACEventLogPrivate.h */, + 266ED07D29C2A2CC67AD1F60 /* MSACEventPropertiesInternal.h */, + 6E3E2CC71D359D5E00B1EE50 /* MSACPageLog.h */, + 6E3E2CC81D359D5E00B1EE50 /* MSACPageLog.m */, + E8E48FA01D51670100A8C1B0 /* MSACStartSessionLog.h */, + E8E48FA11D51670100A8C1B0 /* MSACStartSessionLog.m */, + ); + path = Model; + sourceTree = ""; + }; + C2392F0024642C8700425640 /* Products */ = { + isa = PBXGroup; + children = ( + C2392F0A24642C8700425640 /* OCMock.framework */, + C2392F0C24642C8700425640 /* OCMockTests.xctest */, + C2392F0E24642C8700425640 /* libOCMock.a */, + C2392F1024642C8700425640 /* OCMockLibTests.xctest */, + C2392F1224642C8700425640 /* OCMock.framework */, + C2392F1424642C8700425640 /* OCMock.framework */, + C2392F1624642C8700425640 /* OCMock.framework */, + ); + name = Products; + sourceTree = ""; + }; + C9A91F41230BFBE40068070D /* Frameworks */ = { + isa = PBXGroup; + children = ( + C2D73CE7230FC7FC00390198 /* AppCenter.framework */, + C2D73CE4230FC7EF00390198 /* AppCenter.framework */, + C2D73CE1230FC7DC00390198 /* AppCenter.framework */, + C9EBAB2A230D72F900A20F0F /* AppCenter.framework */, + C9EBAB28230D72F300A20F0F /* AppCenter.framework */, + C9EBAB18230D724700A20F0F /* AppCenter.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + DFCB80242472D5D70058D292 /* Products */ = { + isa = PBXGroup; + children = ( + DFCB802E2472D5D70058D292 /* OCHamcrest.framework */, + DFCB80302472D5D70058D292 /* OCHamcrestTests.xctest */, + DFCB80322472D5D70058D292 /* libochamcrest.a */, + DFCB80342472D5D70058D292 /* libochamcrestTests.xctest */, + DFCB80362472D5D70058D292 /* OCHamcrest.framework */, + DFCB80382472D5D70058D292 /* OCHamcrest.framework */, + DFCB803A2472D5D70058D292 /* OCHamcrest.framework */, + ); + name = Products; + sourceTree = ""; + }; + E85547B71D2D6253002DF6E2 = { + isa = PBXGroup; + children = ( + 358F9BC72019596400B9E22C /* MSACAbstractLog.h */, + 04B525BC2194D49C00FA37FD /* MSACConstants+Flags.h */, + 358F9BC52019590100B9E22C /* MSACLogWithProperties.h */, + 04A082051F74BB8600DC776D /* MSACService.h */, + 387C77041D6CC39400D68CC1 /* MSACServiceAbstract.h */, + E85547C21D2D6253002DF6E2 /* AppCenterAnalytics */, + E85547D61D2D6723002DF6E2 /* AppCenterAnalyticsTests */, + E85547C11D2D6253002DF6E2 /* Products */, + C9A91F41230BFBE40068070D /* Frameworks */, + ); + sourceTree = ""; + }; + E85547C11D2D6253002DF6E2 /* Products */ = { + isa = PBXGroup; + children = ( + C9A91F3F230BFB970068070D /* AppCenterAnalytics.framework */, + ); + name = Products; + sourceTree = ""; + }; + E85547C21D2D6253002DF6E2 /* AppCenterAnalytics */ = { + isa = PBXGroup; + children = ( + E85547CC1D2D63EF002DF6E2 /* AppCenterAnalytics.h */, + E85547C31D2D6253002DF6E2 /* MSACAnalytics.h */, + E85547C51D2D6253002DF6E2 /* MSACAnalytics.m */, + E85547CD1D2D64B0002DF6E2 /* Internals */, + 242A6C5D22D3F57500CB02C7 /* Model */, + E85547D01D2D65AF002DF6E2 /* Support */, + 3513432E2057046700E6DC7D /* TransmissionTarget */, + ); + path = AppCenterAnalytics; + sourceTree = ""; + }; + E85547CD1D2D64B0002DF6E2 /* Internals */ = { + isa = PBXGroup; + children = ( + E85547CF1D2D6521002DF6E2 /* AppCenter+Internal.h */, + E85547CE1D2D64F0002DF6E2 /* MSACAnalyticsPrivate.h */, + 3813B9641DBFE68200831214 /* MSACAnalyticsInternal.h */, + 35BF19E21DF9C43F00193027 /* MSACAnalyticsDelegate.h */, + 3855D51320E2F03000CBC499 /* MSACAnalyticsTransmissionTargetPrivate.h */, + 351343352057093600E6DC7D /* MSACAnalyticsTransmissionTargetInternal.h */, + C27452D520AE0EF100B64B68 /* MSACAnalytics+Validation.h */, + C27452D120AE0DAC00B64B68 /* MSACAnalytics+Validation.m */, + 042E5E002175230600AFD6F9 /* MSACPropertyConfiguratorInternal.h */, + E758FA8320FFE0E200011793 /* MSACPropertyConfiguratorPrivate.h */, + 6E3E2CC21D359D5E00B1EE50 /* Model */, + E8E48F9B1D515C1F00A8C1B0 /* Session */, + 04A1409C1ECE7245001CEE94 /* Util */, + ); + path = Internals; + sourceTree = ""; + }; + E85547D01D2D65AF002DF6E2 /* Support */ = { + isa = PBXGroup; + children = ( + 324F3BA9254789FD0006E223 /* iOS Universal.xcconfig */, + 324F3BAA254789FE0006E223 /* tvOS Universal.xcconfig */, + 324F3BA8254789FD0006E223 /* XCFramework.xcconfig */, + F8D68481230D7D74000A7CED /* Info.plist */, + 04545DC9227B604700A49E06 /* AppCenterAnalytics.xcconfig */, + B2812EF21DA3148000307DCE /* AppCenterAnalytics Debug.xcconfig */, + 0469D1B21F4DFE4C00A43A8E /* AppCenterAnalytics Release.xcconfig */, + 049BC82A1ECE3B9400FB6719 /* iOS.xcconfig */, + 04ED31EB1EAAD3390033BAAE /* iOS.modulemap */, + 049BC82B1ECE3B9A00FB6719 /* macOS.xcconfig */, + 04ED31E91EAAD32B0033BAAE /* macOS.modulemap */, + 043121731EE0C248007054C5 /* tvOS.xcconfig */, + 043121721EE0C248007054C5 /* tvOS.modulemap */, + ); + path = Support; + sourceTree = ""; + }; + E85547D61D2D6723002DF6E2 /* AppCenterAnalyticsTests */ = { + isa = PBXGroup; + children = ( + E85547D91D2D6723002DF6E2 /* Info.plist */, + B26D4DDE211BA99E00AB4E28 /* MSACAnalyticsAuthenticationProviderTests.m */, + 042A17A71DEFA950003BA80A /* MSACAnalyticsTests.m */, + 351343362058900800E6DC7D /* MSACAnalyticsTransmissionTargetTests.m */, + 6E3E2CD01D359F3300B1EE50 /* MSACEventLogTests.m */, + 35A204BE216C1AC600FEBADA /* MSACEventPropertiesTests.m */, + 6E3E2CD11D359F3300B1EE50 /* MSACPageLogTests.m */, + B25E21CC214044A400CAA156 /* MSACPropertyConfiguratorTests.m */, + E815912B1D526A09003D5F3C /* MSACSessionTrackerTests.m */, + E81591291D526956003D5F3C /* MSACStartSessionLogTests.m */, + 6E0684361D35A39300A8CC6C /* Frameworks */, + 04A140B11ECE811D001CEE94 /* Model */, + 04A140AE1ECE806B001CEE94 /* Support */, + 04A140B21ECE814E001CEE94 /* Util */, + ); + path = AppCenterAnalyticsTests; + sourceTree = ""; + }; + E8E48F9B1D515C1F00A8C1B0 /* Session */ = { + isa = PBXGroup; + children = ( + E89B33EB1D5A8F3500FDE8FB /* MSACSessionTrackerDelegate.h */, + 042B1D291FE3508100D6E04A /* MSACSessionTrackerPrivate.h */, + E8E48F9C1D515C3900A8C1B0 /* MSACSessionTracker.h */, + E8E48F9D1D515C3900A8C1B0 /* MSACSessionTracker.m */, + ); + path = Session; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + C9A91F37230BFB970068070D /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + C9A920CA230C5ECB0068070D /* MSACAnalyticsConstants.h in Headers */, + C9A920BD230C5EBE0068070D /* MSACPageLog.h in Headers */, + C9A920C9230C5ECB0068070D /* MSACAnalyticsCategory.h in Headers */, + C9A9208C230C5E390068070D /* MSACLogWithProperties.h in Headers */, + C9A920E2230C5EF00068070D /* MSACAnalyticsAuthenticationProviderInternal.h in Headers */, + C9A920DF230C5EE50068070D /* MSACPropertyConfigurator.h in Headers */, + C9A920A2230C5E620068070D /* MSACAnalyticsInternal.h in Headers */, + C9A920DE230C5EE50068070D /* MSACAnalyticsTransmissionTarget.h in Headers */, + C9A920DC230C5EE50068070D /* MSACAnalyticsAuthenticationProvider.h in Headers */, + C9A920C4230C5EC50068070D /* MSACSessionTracker.h in Headers */, + C9A920BC230C5EBE0068070D /* MSACEventPropertiesInternal.h in Headers */, + C9A9208B230C5E390068070D /* MSACConstants+Flags.h in Headers */, + C9A92090230C5E4B0068070D /* MSACAnalytics.h in Headers */, + C9A920D2230C5ED40068070D /* MSACEventLog.h in Headers */, + C9A920A5230C5E620068070D /* MSACAnalytics+Validation.h in Headers */, + C9A920A3230C5E620068070D /* MSACAnalyticsDelegate.h in Headers */, + C9A9208D230C5E390068070D /* MSACService.h in Headers */, + C9A920AF230C5E6D0068070D /* MSACPropertyConfiguratorPrivate.h in Headers */, + C9A920AE230C5E6D0068070D /* MSACAnalyticsTransmissionTargetPrivate.h in Headers */, + C9A920A1230C5E620068070D /* AppCenter+Internal.h in Headers */, + C9A920AD230C5E6D0068070D /* MSACAnalyticsPrivate.h in Headers */, + C9A920B4230C5E9E0068070D /* MSACEventLogPrivate.h in Headers */, + C9A920B5230C5E9E0068070D /* MSACSessionTrackerPrivate.h in Headers */, + C9A920A4230C5E620068070D /* MSACAnalyticsTransmissionTargetInternal.h in Headers */, + C9A9208E230C5E390068070D /* MSACServiceAbstract.h in Headers */, + C9A920BE230C5EBE0068070D /* MSACStartSessionLog.h in Headers */, + C9A9208F230C5E4B0068070D /* AppCenterAnalytics.h in Headers */, + C9A920D1230C5ED40068070D /* MSACEventProperties.h in Headers */, + C9A920C3230C5EC50068070D /* MSACSessionTrackerDelegate.h in Headers */, + C9A9208A230C5E390068070D /* MSACAbstractLog.h in Headers */, + C9A920D3230C5ED40068070D /* MSACLogWithNameAndProperties.h in Headers */, + C9A920A6230C5E620068070D /* MSACPropertyConfiguratorInternal.h in Headers */, + C9A920DD230C5EE50068070D /* MSACAnalyticsAuthenticationProviderDelegate.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + C9A91F36230BFB970068070D /* AppCenterAnalytics macOS Framework */ = { + isa = PBXNativeTarget; + buildConfigurationList = C9A91F3C230BFB970068070D /* Build configuration list for PBXNativeTarget "AppCenterAnalytics macOS Framework" */; + buildPhases = ( + C2D73CD8230FC75800390198 /* Verify No Build Settings */, + C9A91F37230BFB970068070D /* Headers */, + C9A91F39230BFB970068070D /* Sources */, + C9A91F3A230BFB970068070D /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "AppCenterAnalytics macOS Framework"; + productName = appcenteranalytics; + productReference = C9A91F3F230BFB970068070D /* AppCenterAnalytics.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + E85547B81D2D6253002DF6E2 /* Project object */ = { + isa = PBXProject; + attributes = { + CLASSPREFIX = MSAC; + LastUpgradeCheck = 0820; + ORGANIZATIONNAME = Microsoft; + TargetAttributes = { + C9A91F36230BFB970068070D = { + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = E85547BB1D2D6253002DF6E2 /* Build configuration list for PBXProject "AppCenterAnalytics" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + English, + en, + ); + mainGroup = E85547B71D2D6253002DF6E2; + productRefGroup = E85547C11D2D6253002DF6E2 /* Products */; + projectDirPath = ""; + projectReferences = ( + { + ProductGroup = DFCB80242472D5D70058D292 /* Products */; + ProjectRef = DFCB80232472D5D70058D292 /* OCHamcrest.xcodeproj */; + }, + { + ProductGroup = C2392F0024642C8700425640 /* Products */; + ProjectRef = C2392EFF24642C8700425640 /* OCMock.xcodeproj */; + }, + ); + projectRoot = ""; + targets = ( + C9A91F36230BFB970068070D /* AppCenterAnalytics macOS Framework */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXReferenceProxy section */ + C2392F0A24642C8700425640 /* OCMock.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = OCMock.framework; + remoteRef = C2392F0924642C8700425640 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C2392F0C24642C8700425640 /* OCMockTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = OCMockTests.xctest; + remoteRef = C2392F0B24642C8700425640 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C2392F0E24642C8700425640 /* libOCMock.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libOCMock.a; + remoteRef = C2392F0D24642C8700425640 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C2392F1024642C8700425640 /* OCMockLibTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = OCMockLibTests.xctest; + remoteRef = C2392F0F24642C8700425640 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C2392F1224642C8700425640 /* OCMock.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = OCMock.framework; + remoteRef = C2392F1124642C8700425640 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C2392F1424642C8700425640 /* OCMock.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = OCMock.framework; + remoteRef = C2392F1324642C8700425640 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C2392F1624642C8700425640 /* OCMock.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = OCMock.framework; + remoteRef = C2392F1524642C8700425640 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + DFCB802E2472D5D70058D292 /* OCHamcrest.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = OCHamcrest.framework; + remoteRef = DFCB802D2472D5D70058D292 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + DFCB80302472D5D70058D292 /* OCHamcrestTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = OCHamcrestTests.xctest; + remoteRef = DFCB802F2472D5D70058D292 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + DFCB80322472D5D70058D292 /* libochamcrest.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libochamcrest.a; + remoteRef = DFCB80312472D5D70058D292 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + DFCB80342472D5D70058D292 /* libochamcrestTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = libochamcrestTests.xctest; + remoteRef = DFCB80332472D5D70058D292 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + DFCB80362472D5D70058D292 /* OCHamcrest.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = OCHamcrest.framework; + remoteRef = DFCB80352472D5D70058D292 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + DFCB80382472D5D70058D292 /* OCHamcrest.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = OCHamcrest.framework; + remoteRef = DFCB80372472D5D70058D292 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + DFCB803A2472D5D70058D292 /* OCHamcrest.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = OCHamcrest.framework; + remoteRef = DFCB80392472D5D70058D292 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + +/* Begin PBXShellScriptBuildPhase section */ + C2D73CD8230FC75800390198 /* Verify No Build Settings */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Verify No Build Settings"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#\"${SRCROOT}/../Scripts/VerifyNoBuildSettings.swift\" ${PROJECT_NAME}.xcodeproj/project.pbxproj\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + C9A91F39230BFB970068070D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C9EBAA7F230D39CA00A20F0F /* MSACAnalytics.m in Sources */, + C9EBAA81230D39CA00A20F0F /* MSACPageLog.m in Sources */, + C9EBAA82230D39CA00A20F0F /* MSACStartSessionLog.m in Sources */, + C9EBAA8A230D39CA00A20F0F /* MSACPropertyConfigurator.m in Sources */, + C9EBAA89230D39CA00A20F0F /* MSACAnalyticsTransmissionTarget.m in Sources */, + C9EBAA88230D39CA00A20F0F /* MSACAnalyticsAuthenticationProvider.m in Sources */, + C9EBAA86230D39CA00A20F0F /* MSACEventLog.m in Sources */, + C9EBAA85230D39CA00A20F0F /* MSACEventProperties.m in Sources */, + C9EBAA87230D39CA00A20F0F /* MSACLogWithNameAndProperties.m in Sources */, + C9EBAA83230D39CA00A20F0F /* MSACSessionTracker.m in Sources */, + C9EBAA84230D39CA00A20F0F /* MSACAnalyticsCategory.m in Sources */, + C9EBAA80230D39CA00A20F0F /* MSACAnalytics+Validation.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + C9A91F3D230BFB970068070D /* DebugAppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 049BC82B1ECE3B9A00FB6719 /* macOS.xcconfig */; + buildSettings = { + DEVELOPMENT_TEAM = ""; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = YES; + }; + name = DebugAppStore; + }; + C9A91F3E230BFB970068070D /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 049BC82B1ECE3B9A00FB6719 /* macOS.xcconfig */; + buildSettings = { + DEVELOPMENT_TEAM = ""; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + }; + name = ReleaseHockeyapp; + }; + D0983E3F25683D6200467703 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0469D1B21F4DFE4C00A43A8E /* AppCenterAnalytics Release.xcconfig */; + buildSettings = { + }; + name = ReleaseAppStore; + }; + D0983E4025683D6200467703 /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 049BC82B1ECE3B9A00FB6719 /* macOS.xcconfig */; + buildSettings = { + DEVELOPMENT_TEAM = ""; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + }; + name = ReleaseAppStore; + }; + D0983E4125683D6B00467703 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0469D1B21F4DFE4C00A43A8E /* AppCenterAnalytics Release.xcconfig */; + buildSettings = { + }; + name = DebugHockeyapp; + }; + D0983E4225683D6B00467703 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 049BC82B1ECE3B9A00FB6719 /* macOS.xcconfig */; + buildSettings = { + DEVELOPMENT_TEAM = ""; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + }; + name = DebugHockeyapp; + }; + D0983E4325683D7700467703 /* Github */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B2812EF21DA3148000307DCE /* AppCenterAnalytics Debug.xcconfig */; + buildSettings = { + }; + name = Github; + }; + D0983E4425683D7700467703 /* Github */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 049BC82B1ECE3B9A00FB6719 /* macOS.xcconfig */; + buildSettings = { + DEVELOPMENT_TEAM = ""; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + }; + name = Github; + }; + D0983E4525683D7C00467703 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0469D1B21F4DFE4C00A43A8E /* AppCenterAnalytics Release.xcconfig */; + buildSettings = { + }; + name = HockeyappMacAlpha; + }; + D0983E4625683D7C00467703 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 049BC82B1ECE3B9A00FB6719 /* macOS.xcconfig */; + buildSettings = { + DEVELOPMENT_TEAM = ""; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + }; + name = HockeyappMacAlpha; + }; + E85547C71D2D6253002DF6E2 /* DebugAppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B2812EF21DA3148000307DCE /* AppCenterAnalytics Debug.xcconfig */; + buildSettings = { + }; + name = DebugAppStore; + }; + E85547C81D2D6253002DF6E2 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0469D1B21F4DFE4C00A43A8E /* AppCenterAnalytics Release.xcconfig */; + buildSettings = { + }; + name = ReleaseHockeyapp; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + C9A91F3C230BFB970068070D /* Build configuration list for PBXNativeTarget "AppCenterAnalytics macOS Framework" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C9A91F3D230BFB970068070D /* DebugAppStore */, + D0983E4425683D7700467703 /* Github */, + C9A91F3E230BFB970068070D /* ReleaseHockeyapp */, + D0983E4025683D6200467703 /* ReleaseAppStore */, + D0983E4225683D6B00467703 /* DebugHockeyapp */, + D0983E4625683D7C00467703 /* HockeyappMacAlpha */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseHockeyapp; + }; + E85547BB1D2D6253002DF6E2 /* Build configuration list for PBXProject "AppCenterAnalytics" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E85547C71D2D6253002DF6E2 /* DebugAppStore */, + D0983E4325683D7700467703 /* Github */, + E85547C81D2D6253002DF6E2 /* ReleaseHockeyapp */, + D0983E3F25683D6200467703 /* ReleaseAppStore */, + D0983E4125683D6B00467703 /* DebugHockeyapp */, + D0983E4525683D7C00467703 /* HockeyappMacAlpha */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseHockeyapp; + }; +/* End XCConfigurationList section */ + }; + rootObject = E85547B81D2D6253002DF6E2 /* Project object */; +} diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/AppCenterAnalytics.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/AppCenterAnalytics.h new file mode 100644 index 0000000000..e452e0460c --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/AppCenterAnalytics.h @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAbstractLog.h" +#import "MSACAnalytics.h" +#import "MSACAnalyticsAuthenticationProvider.h" +#import "MSACAnalyticsAuthenticationProviderDelegate.h" +#import "MSACAnalyticsTransmissionTarget.h" +#import "MSACConstants+Flags.h" +#import "MSACEventLog.h" +#import "MSACEventProperties.h" diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/MSACAnalytics+Validation.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/MSACAnalytics+Validation.h new file mode 100644 index 0000000000..85f7e1794b --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/MSACAnalytics+Validation.h @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAnalyticsInternal.h" + +@class MSACCommonSchemaLog; +@class MSACLogWithNameAndProperties; + +NS_ASSUME_NONNULL_BEGIN + +/* + * Workaround for exporting symbols from category object files. + */ +extern NSString *MSACAnalyticsValidationCategory; + +@interface MSACAnalytics (Validation) + +/** + * Validate AppCenter log. + * + * @param log The AppCenter log. + * + * @return YES if AppCenter log is valid; NO otherwise. + */ +- (BOOL)validateLog:(MSACLogWithNameAndProperties *)log; + +/** + * Validate event name + * + * @return YES if event name is valid; NO otherwise. + */ +- (nullable NSString *)validateEventName:(NSString *)eventName forLogType:(NSString *)logType; + +/** + * Validate keys and values of properties. Intended for testing. Uses MSACUtility+PropertyValidation internally. + * + * @return dictionary which contains only valid properties. + */ +- (NSDictionary *)validateProperties:(NSDictionary *)properties + forLogName:(NSString *)logName + andType:(NSString *)logType; + +/** + * Validate MSACEventProperties for App Center's ingestion. + * + * @return MSACEventProperties object which contains only valid properties. + */ +- (MSACEventProperties *)validateAppCenterEventProperties:(MSACEventProperties *)properties; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/MSACAnalytics+Validation.m b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/MSACAnalytics+Validation.m new file mode 100644 index 0000000000..0bfe3f9541 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/MSACAnalytics+Validation.m @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "AppCenter+Internal.h" +#import "MSACAnalytics+Validation.h" +#import "MSACBooleanTypedProperty.h" +#import "MSACConstants+Internal.h" +#import "MSACDateTimeTypedProperty.h" +#import "MSACDoubleTypedProperty.h" +#import "MSACEventLog.h" +#import "MSACEventPropertiesInternal.h" +#import "MSACLongTypedProperty.h" +#import "MSACPageLog.h" +#import "MSACStringTypedProperty.h" + +// Events values limitations +static const int kMSACMinEventNameLength = 1; +static const int kMSACMaxEventNameLength = 256; + +/* + * Workaround for exporting symbols from category object files. + */ +NSString *MSACAnalyticsValidationCategory; + +@implementation MSACAnalytics (Validation) + +- (BOOL)channelUnit:(id)__unused channelUnit shouldFilterLog:(id)log { + NSObject *logObject = (NSObject *)log; + if ([logObject isKindOfClass:[MSACEventLog class]]) { + return ![self validateLog:(MSACEventLog *)log]; + } else if ([logObject isKindOfClass:[MSACPageLog class]]) { + return ![self validateLog:(MSACPageLog *)log]; + } + return NO; +} + +- (BOOL)validateLog:(MSACLogWithNameAndProperties *)log { + + // Validate event name. + NSString *validName = [self validateEventName:log.name forLogType:log.type]; + if (!validName) { + return NO; + } + log.name = validName; + + // Send only valid properties. + log.properties = [self validateProperties:log.properties forLogName:log.name andType:log.type]; + return YES; +} + +- (nullable NSString *)validateEventName:(NSString *)eventName forLogType:(NSString *)logType { + if (!eventName || [eventName length] < kMSACMinEventNameLength) { + MSACLogError([MSACAnalytics logTag], @"%@ name cannot be null or empty", logType); + return nil; + } + if ([eventName length] > kMSACMaxEventNameLength) { + MSACLogWarning([MSACAnalytics logTag], + @"%@ '%@' : name length cannot be longer than %d characters. " + @"Name will be truncated.", + logType, eventName, kMSACMaxEventNameLength); + eventName = [eventName substringToIndex:kMSACMaxEventNameLength]; + } + return eventName; +} + +- (NSDictionary *)validateProperties:(NSDictionary *)properties + forLogName:(NSString *)logName + andType:(NSString *)logType { + + // Keeping this method body in MSACAnalytics to use it in unit tests. + return [MSACUtility validateProperties:properties forLogName:logName type:logType]; +} + +- (MSACEventProperties *)validateAppCenterEventProperties:(MSACEventProperties *)eventProperties { + MSACEventProperties *validCopy = [MSACEventProperties new]; + for (NSString *propertyKey in eventProperties.properties) { + if ([validCopy.properties count] == kMSACMaxPropertiesPerLog) { + MSACLogWarning([MSACAnalytics logTag], @"Typed properties cannot contain more than %d items. Skipping other properties.", + kMSACMaxPropertiesPerLog); + break; + } + MSACTypedProperty *property = eventProperties.properties[propertyKey]; + MSACTypedProperty *validProperty = [self validateAppCenterTypedProperty:property]; + if (validProperty) { + validCopy.properties[validProperty.name] = validProperty; + } + } + return validCopy; +} + +- (MSACTypedProperty *)validateAppCenterTypedProperty:(MSACTypedProperty *)typedProperty { + MSACTypedProperty *validProperty; + if ([typedProperty isKindOfClass:[MSACStringTypedProperty class]]) { + MSACStringTypedProperty *originalStringProperty = (MSACStringTypedProperty *)typedProperty; + MSACStringTypedProperty *validStringProperty = [MSACStringTypedProperty new]; + validStringProperty.value = [self validateAppCenterStringTypedPropertyValue:originalStringProperty.value]; + validProperty = validStringProperty; + } else if ([typedProperty isKindOfClass:[MSACBooleanTypedProperty class]]) { + validProperty = [MSACBooleanTypedProperty new]; + ((MSACBooleanTypedProperty *)validProperty).value = ((MSACBooleanTypedProperty *)typedProperty).value; + } else if ([typedProperty isKindOfClass:[MSACLongTypedProperty class]]) { + validProperty = [MSACLongTypedProperty new]; + ((MSACLongTypedProperty *)validProperty).value = ((MSACLongTypedProperty *)typedProperty).value; + } else if ([typedProperty isKindOfClass:[MSACDoubleTypedProperty class]]) { + validProperty = [MSACDoubleTypedProperty new]; + ((MSACDoubleTypedProperty *)validProperty).value = ((MSACDoubleTypedProperty *)typedProperty).value; + } else if ([typedProperty isKindOfClass:[MSACDateTimeTypedProperty class]]) { + validProperty = [MSACDateTimeTypedProperty new]; + ((MSACDateTimeTypedProperty *)validProperty).value = ((MSACDateTimeTypedProperty *)typedProperty).value; + } + validProperty.name = [self validateAppCenterPropertyName:typedProperty.name]; + return validProperty; +} + +- (NSString *)validateAppCenterPropertyName:(NSString *)propertyKey { + if ([propertyKey length] > kMSACMaxPropertyKeyLength) { + MSACLogWarning([MSACAnalytics logTag], + @"Typed property '%@': key length cannot exceed %d characters. Property value will be truncated.", propertyKey, + kMSACMaxPropertyKeyLength); + return [propertyKey substringToIndex:(kMSACMaxPropertyKeyLength - 1)]; + } + return propertyKey; +} + +- (NSString *)validateAppCenterStringTypedPropertyValue:(NSString *)value { + if ([value length] > kMSACMaxPropertyValueLength) { + MSACLogWarning([MSACAnalytics logTag], @"Typed property value length cannot exceed %d characters. Property value will be truncated.", + kMSACMaxPropertyValueLength); + return [value substringToIndex:(kMSACMaxPropertyValueLength - 1)]; + } + return value; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/MSACAnalyticsDelegate.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/MSACAnalyticsDelegate.h new file mode 100644 index 0000000000..16ad9b46cb --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/MSACAnalyticsDelegate.h @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +@class MSACAnalytics; +@class MSACEventLog; +@class MSACPageLog; + +@protocol MSACAnalyticsDelegate + +@optional + +/** + * Callback method that will be called before each event log is sent to the server. + * + * @param analytics The instance of MSACAnalytics. + * @param eventLog The event log that will be sent. + */ +- (void)analytics:(MSACAnalytics *)analytics willSendEventLog:(MSACEventLog *)eventLog; + +/** + * Callback method that will be called in case the SDK was able to send an event log to the server. Use this method to provide custom + * behavior. + * + * @param analytics The instance of MSACAnalytics. + * @param eventLog The event log that App Center sent. + */ +- (void)analytics:(MSACAnalytics *)analytics didSucceedSendingEventLog:(MSACEventLog *)eventLog; + +/** + * Callback method that will be called in case the SDK was unable to send an event log to the server. + * + * @param analytics The instance of MSACAnalytics. + * @param eventLog The event log that App Center tried to send. + * @param error The error that occurred. + */ +- (void)analytics:(MSACAnalytics *)analytics didFailSendingEventLog:(MSACEventLog *)eventLog withError:(NSError *)error; + +/** + * Callback method that will be called before each page log is sent to the server. + * + * @param analytics The instance of MSACAnalytics. + * @param pageLog The page log that will be sent. + */ +- (void)analytics:(MSACAnalytics *)analytics willSendPageLog:(MSACPageLog *)pageLog; + +/** + * Callback method that will be called in case the SDK was able to send a page log to the server. Use this method to provide custom + * behavior. + * + * @param analytics The instance of MSACAnalytics. + * @param pageLog The page log that App Center sent. + */ +- (void)analytics:(MSACAnalytics *)analytics didSucceedSendingPageLog:(MSACPageLog *)pageLog; + +/** + * Callback method that will be called in case the SDK was unable to send a page log to the server. + * + * @param analytics The instance of MSACAnalytics. + * @param pageLog The page log that App Center tried to send. + * @param error The error that occurred. + */ +- (void)analytics:(MSACAnalytics *)analytics didFailSendingPageLog:(MSACPageLog *)pageLog withError:(NSError *)error; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/MSACAnalyticsInternal.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/MSACAnalyticsInternal.h new file mode 100644 index 0000000000..f1ad3b6965 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/MSACAnalyticsInternal.h @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAnalytics.h" +#import "MSACAnalyticsDelegate.h" +#import "MSACAnalyticsTransmissionTarget.h" +#import "MSACChannelDelegate.h" +#import "MSACServiceInternal.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface MSACAnalytics () + +/** + * Track an event with typed properties. + * + * @param eventName Event name. + * @param properties The typed event properties. + * @param transmissionTarget The transmission target to associate to this event. + * @param flags Optional flags. Events tracked with the MSACFlagsCritical flag will take precedence over all other events in + * storage. An event tracked with this option will only be dropped if storage must make room for a newer event that is also marked with the + * MSACFlagsCritical flag. + */ ++ (void)trackEvent:(NSString *)eventName + withTypedProperties:(nullable MSACEventProperties *)properties + forTransmissionTarget:(nullable MSACAnalyticsTransmissionTarget *)transmissionTarget + flags:(MSACFlags)flags; + +// Temporarily hiding tracking page feature. +/** + * Track a page. + * + * @param pageName page name. + */ ++ (void)trackPage:(NSString *)pageName; + +/** + * Track a page. + * + * @param pageName page name. + * @param properties dictionary of properties. + */ ++ (void)trackPage:(NSString *)pageName withProperties:(nullable NSDictionary *)properties; + +/** + * Set the page auto-tracking property. + * + * @param isEnabled is page tracking enabled or disabled. + */ ++ (void)setAutoPageTrackingEnabled:(BOOL)isEnabled; + +/** + * Indicate if auto page tracking is enabled or not. + * + * @return YES if page tracking is enabled and NO if disabled. + */ ++ (BOOL)isAutoPageTrackingEnabled; + +/** + * Set the MSACAnalyticsDelegate object. + * + * @param delegate The delegate to be set. + */ ++ (void)setDelegate:(nullable id)delegate; + +/** + * Pause transmission target for the given token. + * + * @param token The token of the transmission target. + */ ++ (void)pauseTransmissionTargetForToken:(NSString *)token; + +/** + * Resume transmission target for the given token. + * + * @param token The token of the transmission target. + */ ++ (void)resumeTransmissionTargetForToken:(NSString *)token; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/MSACAnalyticsPrivate.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/MSACAnalyticsPrivate.h new file mode 100644 index 0000000000..3c54cab257 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/MSACAnalyticsPrivate.h @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAnalytics.h" +#import "MSACAnalyticsDelegate.h" +#import "MSACAnalyticsTransmissionTarget.h" +#import "MSACServiceInternal.h" +#import "MSACSessionTracker.h" +#import "MSACSessionTrackerDelegate.h" + +NS_ASSUME_NONNULL_BEGIN + +// The Id suffix for critical events. +static NSString *const kMSACCriticalChannelSuffix = @"critical"; + +@interface MSACAnalytics () + +/** + * Session tracking component. + */ +@property(nonatomic) MSACSessionTracker *sessionTracker; + +@property(atomic, getter=isAutoPageTrackingEnabled) BOOL autoPageTrackingEnabled; + +@property(nonatomic, nullable) id delegate; + +@property(nonatomic) NSUInteger flushInterval; + +/** + * Transmission targets. + */ +@property(nonatomic) NSMutableDictionary *transmissionTargets; + +/** + * Default transmission target. + */ +@property(nonatomic) MSACAnalyticsTransmissionTarget *defaultTransmissionTarget; + +/** + * The channel unit for common schema logs. + */ +@property(nonatomic, nullable) id oneCollectorChannelUnit; + +/** + * The channel unit for critical common schema logs. + */ +@property(nonatomic, nullable) id oneCollectorCriticalChannelUnit; + +/** + * Critical events channel unit. + */ +@property(nonatomic) id criticalChannelUnit; + +/** + * Track an event. + * + * @param eventName Event name. + * @param properties Dictionary of properties. + * @param transmissionTarget Transmission target to associate with the event. + * @param flags Optional flags. Events tracked with the MSACFlagsCritical flag will take precedence over all other events in + * storage. An event tracked with this option will only be dropped if storage must make room for a newer event that is also marked with the + * MSACFlagsCritical flag. + */ +- (void)trackEvent:(NSString *)eventName + withProperties:(nullable NSDictionary *)properties + forTransmissionTarget:(nullable MSACAnalyticsTransmissionTarget *)transmissionTarget + flags:(MSACFlags)flags; + +/** + * Track an event with typed properties. + * + * @param eventName Event name. + * @param properties Typed properties. + * @param transmissionTarget Transmission target to associate with the event. + * @param flags Optional flags. Events tracked with the MSACFlagsCritical flag will take precedence over all other events in + * storage. An event tracked with this option will only be dropped if storage must make room for a newer event that is also marked with the + * MSACFlagsCritical flag. + */ +- (void)trackEvent:(NSString *)eventName + withTypedProperties:(nullable MSACEventProperties *)properties + forTransmissionTarget:(nullable MSACAnalyticsTransmissionTarget *)transmissionTarget + flags:(MSACFlags)flags; + +/** + * Track a page. + * + * @param pageName Page name. + * @param properties Dictionary of properties. + */ +- (void)trackPage:(NSString *)pageName withProperties:(nullable NSDictionary *)properties; + +/** + * Get a transmissionTarget. + * + * @param token The token of the transmission target to retrieve. + * + * @returns The transmission target object. + */ +- (MSACAnalyticsTransmissionTarget *)transmissionTargetForToken:(NSString *)token; + +/** + * Method to reset the singleton when running unit tests only. So calling sharedInstance returns a fresh instance. + */ ++ (void)resetSharedInstance; + +/** + * Removes properties with keys that are not a string or that have non-string values. + * + * @param properties A dictionary of properties. + * + * @returns A dictionary of valid properties or an empty dictionay. + */ +- (NSDictionary *)removeInvalidProperties:(NSDictionary *)properties; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/MSACAnalyticsTransmissionTargetInternal.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/MSACAnalyticsTransmissionTargetInternal.h new file mode 100644 index 0000000000..844d141e92 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/MSACAnalyticsTransmissionTargetInternal.h @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAnalyticsTransmissionTarget.h" + +@protocol MSACChannelGroupProtocol; + +NS_ASSUME_NONNULL_BEGIN + +@interface MSACAnalyticsTransmissionTarget () + +/** + * The transmission target token corresponding to this transmission target. + */ +@property(nonatomic, copy, readonly) NSString *transmissionTargetToken; + +/** + * Initialize a transmission target with token and parent target. + * + * @param token A transmission target token. + * @param parentTarget Nested parent transmission target. + * @param channelGroup The Channel group. + * + * @return A transmission target instance. + */ +- (instancetype)initWithTransmissionTargetToken:(NSString *)token + parentTarget:(nullable MSACAnalyticsTransmissionTarget *)parentTarget + channelGroup:(nonnull id)channelGroup; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/MSACAnalyticsTransmissionTargetPrivate.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/MSACAnalyticsTransmissionTargetPrivate.h new file mode 100644 index 0000000000..418b639f50 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/MSACAnalyticsTransmissionTargetPrivate.h @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAnalyticsTransmissionTarget.h" +#import "MSACChannelDelegate.h" +#import "MSACChannelGroupProtocol.h" +#import "MSACUtility.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface MSACAnalyticsTransmissionTarget () + +/** + * Parent transmission target of this target. + */ +@property(nonatomic, nullable) MSACAnalyticsTransmissionTarget *parentTarget; + +/** + * Child transmission targets nested to this transmission target. + */ +@property(nonatomic) NSMutableDictionary *childTransmissionTargets; + +/** + * isEnabled value storage key. + */ +@property(nonatomic, readonly) NSString *isEnabledKey; + +/** + * The channel group. + */ +@property(nonatomic, readonly) id channelGroup; + +/** + * Authentication provider. + */ +@property(class, nonatomic) MSACAnalyticsAuthenticationProvider *authenticationProvider; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/MSACPropertyConfiguratorPrivate.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/MSACPropertyConfiguratorPrivate.h new file mode 100644 index 0000000000..3db08d6c50 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/MSACPropertyConfiguratorPrivate.h @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAnalyticsTransmissionTarget.h" + +@class MSACTypedProperty; + +NS_ASSUME_NONNULL_BEGIN + +@interface MSACPropertyConfigurator () + +/** + * The transmission target which will have overwritten properties. + */ +@property(nonatomic, weak) MSACAnalyticsTransmissionTarget *transmissionTarget; + +/** + * Event properties attached to events tracked by this target. + */ +@property(nonatomic) MSACEventProperties *eventProperties; + +/** + * The device id to send with common schema logs. If nil, nothing is sent. + */ +@property(nonatomic, copy) NSString *deviceId; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Model/MSACEventLogPrivate.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Model/MSACEventLogPrivate.h new file mode 100644 index 0000000000..d17bee47ad --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Model/MSACEventLogPrivate.h @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACEventLog.h" + +@interface MSACEventLog () + +/** + * Maps each typed property string identifier to a CS type identifier. + */ +@property(nonatomic) NSDictionary *metadataTypeIdMapping; + +/** + * Convert AppCenter properties to Common Schema 3.0 Part C properties. + */ +- (void)setPropertiesAndMetadataForCSLog:(MSACCommonSchemaLog *)csLog; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Model/MSACEventPropertiesInternal.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Model/MSACEventPropertiesInternal.h new file mode 100644 index 0000000000..0437ea00aa --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Model/MSACEventPropertiesInternal.h @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACEventProperties.h" + +@class MSACTypedProperty; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Typed event properties. + */ +@interface MSACEventProperties () + +/** + * String and date properties. + */ +@property(nonatomic) NSMutableDictionary *properties; + +/** + * Creates an instance of EventProperties with a string-string properties dictionary. + * + * @param properties A dictionary of properties with string keys and string values. + * + * @return An instance of EventProperties. + */ +- (instancetype)initWithStringDictionary:(NSDictionary *)properties; + +/** + * Serialize this object to an array. + * + * @return An array representing this object. + */ +- (NSMutableArray *)serializeToArray; + +/** + * Indicates whether there are any properties in the collection. + * + * @return `YES` if there are no properties in the collection, `NO` otherwise. + */ +- (BOOL)isEmpty; + +/** + * Merge event properties. + * + * @param eventProperties The new properites to be merged. + */ +- (void)mergeEventProperties:(MSACEventProperties *__nonnull)eventProperties; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Model/MSACPageLog.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Model/MSACPageLog.h new file mode 100644 index 0000000000..38f54772d7 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Model/MSACPageLog.h @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACLogWithNameAndProperties.h" + +@interface MSACPageLog : MSACLogWithNameAndProperties + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Model/MSACPageLog.m b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Model/MSACPageLog.m new file mode 100644 index 0000000000..68b3afc619 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Model/MSACPageLog.m @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACPageLog.h" +#import "AppCenter+Internal.h" + +static NSString *const kMSACTypePage = @"page"; + +@implementation MSACPageLog + +- (instancetype)init { + if ((self = [super init])) { + self.type = kMSACTypePage; + } + return self; +} + +- (BOOL)isEqual:(id)object { + return [(NSObject *)object isKindOfClass:[MSACPageLog class]] && [super isEqual:object]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Model/MSACStartSessionLog.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Model/MSACStartSessionLog.h new file mode 100644 index 0000000000..532301a6c1 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Model/MSACStartSessionLog.h @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "AppCenter+Internal.h" +#import "MSACNoAutoAssignSessionIdLog.h" + +@interface MSACStartSessionLog : MSACAbstractLog + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Model/MSACStartSessionLog.m b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Model/MSACStartSessionLog.m new file mode 100644 index 0000000000..8386f8103d --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Model/MSACStartSessionLog.m @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACStartSessionLog.h" + +static NSString *const kMSACTypeEndSession = @"startSession"; + +@implementation MSACStartSessionLog + +- (instancetype)init { + if ((self = [super init])) { + self.type = kMSACTypeEndSession; + } + return self; +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Session/MSACSessionTracker.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Session/MSACSessionTracker.h new file mode 100644 index 0000000000..8f447fbd33 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Session/MSACSessionTracker.h @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "AppCenter+Internal.h" +#import "MSACSessionHistoryInfo.h" +#import "MSACSessionTrackerDelegate.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface MSACSessionTracker : NSObject + +/** + * Session tracker delegate. + */ +@property(nonatomic) id delegate; + +/** + * Session timeout time. + */ +@property(nonatomic) NSTimeInterval sessionTimeout; + +/** + * Timestamp of the last created log. + */ +@property(nonatomic) NSDate *lastCreatedLogTime; + +/** + * Timestamp of the last time that the app entered foreground. + */ +@property(nonatomic) NSDate *lastEnteredForegroundTime; + +/** + * Timestamp of the last time that the app entered background. + */ +@property(nonatomic) NSDate *lastEnteredBackgroundTime; + +/** + * Start session tracking. + */ +- (void)start; + +/** + * Stop session tracking. + */ +- (void)stop; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Session/MSACSessionTracker.m b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Session/MSACSessionTracker.m new file mode 100644 index 0000000000..509b0de627 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Session/MSACSessionTracker.m @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACSessionTracker.h" +#import "MSACAnalyticsInternal.h" +#import "MSACSessionContext.h" +#import "MSACSessionTrackerPrivate.h" +#import "MSACStartServiceLog.h" +#import "MSACStartSessionLog.h" + +static NSTimeInterval const kMSACSessionTimeOut = 20; +static NSString *const kMSACPastSessionsKey = @"PastSessions"; + +@interface MSACSessionTracker () + +/** + * Check if current session has timed out. + * + * @return YES if current session has timed out, NO otherwise. + */ +- (BOOL)hasSessionTimedOut; + +@end + +@implementation MSACSessionTracker + +- (instancetype)init { + if ((self = [super init])) { + _sessionTimeout = kMSACSessionTimeOut; + _context = [MSACSessionContext sharedInstance]; + + // Remove old session history from previous SDK versions. + [MSAC_APP_CENTER_USER_DEFAULTS removeObjectForKey:kMSACPastSessionsKey]; + + // Session tracking is not started by default. + _started = NO; + } + return self; +} + +- (void)renewSessionId { + @synchronized(self) { + if (self.started) { + + // Check if new session id is required. + if ([self.context sessionId] == nil || [self hasSessionTimedOut]) { + NSString *sessionId = MSAC_UUID_STRING; + [self.context setSessionId:sessionId]; + MSACLogInfo([MSACAnalytics logTag], @"New session ID: %@", sessionId); + + // Create a start session log. + MSACStartSessionLog *log = [[MSACStartSessionLog alloc] init]; + log.sid = sessionId; + [self.delegate sessionTracker:self processLog:log]; + } + } + } +} + +- (void)start { + if (!self.started) { + self.started = YES; + + // Request a new session id depending on the application state. + MSACApplicationState state = [MSACUtility applicationState]; + if (state == MSACApplicationStateInactive || state == MSACApplicationStateActive) { + [self renewSessionId]; + } + + // Hookup to application events. + [MSAC_NOTIFICATION_CENTER addObserver:self + selector:@selector(applicationDidEnterBackground) +#if TARGET_OS_OSX + name:NSApplicationDidResignActiveNotification +#else + name:UIApplicationDidEnterBackgroundNotification +#endif + object:nil]; + [MSAC_NOTIFICATION_CENTER addObserver:self + selector:@selector(applicationWillEnterForeground) +#if TARGET_OS_OSX + name:NSApplicationWillBecomeActiveNotification +#else + name:UIApplicationWillEnterForegroundNotification +#endif + object:nil]; + } +} + +- (void)stop { + if (self.started) { + [MSAC_NOTIFICATION_CENTER removeObserver:self]; + self.started = NO; + [self.context setSessionId:nil]; + } +} + +- (void)dealloc { + [MSAC_NOTIFICATION_CENTER removeObserver:self]; +} + +#pragma mark - private methods + +- (BOOL)hasSessionTimedOut { + + @synchronized(self) { + NSDate *now = [NSDate date]; + + // Verify if a log has already been sent and if it was sent a longer time ago than the session timeout. + BOOL noLogSentForLong = !self.lastCreatedLogTime || [now timeIntervalSinceDate:self.lastCreatedLogTime] >= self.sessionTimeout; + + // FIXME: There is no life cycle for app extensions yet so ignoring the background tests for now. + if (MSAC_IS_APP_EXTENSION) + return noLogSentForLong; + + // Verify if app is currently in the background for a longer time than the session timeout. + BOOL isBackgroundForLong = (self.lastEnteredBackgroundTime && self.lastEnteredForegroundTime) && + ([self.lastEnteredBackgroundTime compare:self.lastEnteredForegroundTime] == NSOrderedDescending) && + ([now timeIntervalSinceDate:self.lastEnteredBackgroundTime] >= self.sessionTimeout); + + // Verify if app was in the background for a longer time than the session timeout time. + BOOL wasBackgroundForLong = + (self.lastEnteredBackgroundTime) + ? [self.lastEnteredForegroundTime timeIntervalSinceDate:self.lastEnteredBackgroundTime] >= self.sessionTimeout + : false; + return noLogSentForLong && (isBackgroundForLong || wasBackgroundForLong); + } +} + +- (void)applicationDidEnterBackground { + self.lastEnteredBackgroundTime = [NSDate date]; +} + +- (void)applicationWillEnterForeground { + self.lastEnteredForegroundTime = [NSDate date]; + + // Trigger session renewal. + [self renewSessionId]; +} + +#pragma mark - MSACChannelDelegate + +- (void)channel:(id)__unused channel prepareLog:(id)log { + + /* + * Start session log is created in this method, therefore, skip in order to avoid infinite loop. Also skip start service log as it's + * always sent and should not trigger a session. + */ + if ([((NSObject *)log) isKindOfClass:[MSACStartSessionLog class]] || [((NSObject *)log) isKindOfClass:[MSACStartServiceLog class]]) + return; + + // If the log requires session Id. + if (![(NSObject *)log conformsToProtocol:@protocol(MSACNoAutoAssignSessionIdLog)]) { + log.sid = [self.context sessionId]; + } + + // Update last created log time stamp. + self.lastCreatedLogTime = [NSDate date]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Session/MSACSessionTrackerDelegate.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Session/MSACSessionTrackerDelegate.h new file mode 100644 index 0000000000..fac5531f3c --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Session/MSACSessionTrackerDelegate.h @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "AppCenter+Internal.h" + +@protocol MSACSessionTrackerDelegate + +@required + +- (void)sessionTracker:(id)sessionTracker processLog:(id)log; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Session/MSACSessionTrackerPrivate.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Session/MSACSessionTrackerPrivate.h new file mode 100644 index 0000000000..6fbd30f0ac --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Session/MSACSessionTrackerPrivate.h @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACSessionContext.h" +#import "MSACSessionTracker.h" + +@interface MSACSessionTracker () + +/** + * Session context. This should be the shared instance, unless tests need to override. + */ +@property(nonatomic) MSACSessionContext *context; + +/** + * Flag to indicate if session tracking has started or not. + */ +@property(nonatomic, getter=isStarted) BOOL started; + +/** + * Renew session Id. + */ +- (void)renewSessionId; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Util/MSACAnalyticsCategory.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Util/MSACAnalyticsCategory.h new file mode 100644 index 0000000000..a0d71e40e6 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Util/MSACAnalyticsCategory.h @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface MSACAnalyticsCategory : NSObject + +/** + * Activate category for UIViewController. + */ ++ (void)activateCategory; + +/** + * Get the last missed page view name while available. + * + * @return the last page view name. Can be nil if no name available or the page has already been tracked. + */ ++ (nullable NSString *)missedPageViewName; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Util/MSACAnalyticsCategory.m b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Util/MSACAnalyticsCategory.m new file mode 100644 index 0000000000..581fc27cf8 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Util/MSACAnalyticsCategory.m @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import +#if TARGET_OS_OSX +#import +#define MSViewController NSViewController +#else +#import +#define MSViewController UIViewController +#endif + +#import "MSACAnalyticsCategory.h" +#import "MSACAnalyticsInternal.h" + +static NSString *const kMSACViewControllerSuffix = @"ViewController"; +static NSString *MSMissedPageViewName; +static IMP viewWillAppearOriginalImp; + +/** + * Should track page. + * + * @param viewController The current view controller. + * + * @return YES if should track page, NO otherwise. + */ +static BOOL ms_shouldTrackPageView(MSViewController *viewController) { + + // For container view controllers, auto page tracking is disabled(to avoid noise). + NSSet *viewControllerSet = [NSSet setWithArray:@[ +#if TARGET_OS_OSX + @"NSTabViewController", @"NSSplitViewController", @"NSPageController" +#else + @"UINavigationController", @"UITabBarController", @"UISplitViewController", @"UIInputWindowController", @"UIPageViewController" +#endif + ]]; + NSString *className = NSStringFromClass([viewController class]); + return ![viewControllerSet containsObject:className]; +} + +@implementation MSViewController (PageViewLogging) + ++ (void)swizzleViewWillAppear { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + Class class = [self class]; + +// Get selectors. +#if TARGET_OS_OSX + SEL originalSelector = NSSelectorFromString(@"viewWillAppear"); +#else + SEL originalSelector = NSSelectorFromString(@"viewWillAppear:"); +#endif + + SEL swizzledSelector = @selector(ms_viewWillAppear:); + Method originalMethod = class_getInstanceMethod(class, originalSelector); + IMP swizzledImp = class_getMethodImplementation(class, swizzledSelector); + viewWillAppearOriginalImp = method_setImplementation(originalMethod, swizzledImp); + }); +} + +#pragma mark - Method Swizzling + +- (void)ms_viewWillAppear:(BOOL)animated { + + // Forward to the original implementation. + ((void (*)(id, SEL, BOOL))viewWillAppearOriginalImp)(self, _cmd, animated); + + if ([MSACAnalytics isAutoPageTrackingEnabled]) { + + if (!ms_shouldTrackPageView(self)) { + return; + } + + // By default, use class name for the page name. + NSString *pageViewName = NSStringFromClass([self class]); + + // Remove module name on swift classes. + pageViewName = [[pageViewName componentsSeparatedByString:@"."] lastObject]; + + // Remove suffix if any. + if ([pageViewName hasSuffix:kMSACViewControllerSuffix] && [pageViewName length] > [kMSACViewControllerSuffix length]) { + pageViewName = [pageViewName substringToIndex:[pageViewName length] - [kMSACViewControllerSuffix length]]; + } + + // Track page if ready. + if ([MSACAnalytics sharedInstance].available) { + + // Reset cached page. + MSMissedPageViewName = nil; + + // Track page. + [MSACAnalytics trackPage:pageViewName]; + } else { + + // Store the page name for retroactive tracking. + // For instance if the service becomes enabled after the view appeared. + MSMissedPageViewName = pageViewName; + } + } +} + +@end + +@implementation MSACAnalyticsCategory + ++ (void)activateCategory { + [MSViewController swizzleViewWillAppear]; +} + ++ (NSString *)missedPageViewName { + return MSMissedPageViewName; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Util/MSACAnalyticsConstants.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Util/MSACAnalyticsConstants.h new file mode 100644 index 0000000000..6e68f92495 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Internals/Util/MSACAnalyticsConstants.h @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Common schema metadata type identifiers. + */ +static const int kMSACLongMetadataTypeId = 4; +static const int kMSACDoubleMetadataTypeId = 6; +static const int kMSACDateTimeMetadataTypeId = 9; + +/** + * Minimum flush interval for channel. + */ +static NSUInteger const kMSACFlushIntervalMinimum = 3; + +/** + * Maximum flush interval for channel. + */ +static NSUInteger const kMSACFlushIntervalMaximum = 24 * 60 * 60; diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/MSACAnalytics.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/MSACAnalytics.h new file mode 100644 index 0000000000..cbabc287af --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/MSACAnalytics.h @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAnalyticsTransmissionTarget.h" +#import "MSACServiceAbstract.h" + +@class MSACEventProperties; + +NS_ASSUME_NONNULL_BEGIN + +/** + * App Center analytics service. + */ +NS_SWIFT_NAME(Analytics) +@interface MSACAnalytics : MSACServiceAbstract + +/** + * Track an event. + * + * @param eventName Event name. Cannot be `nil` or empty. + * + * @discussion Validation rules apply depending on the configured secret. + * + * For App Center, the name cannot be longer than 256 and is truncated otherwise. + * + * For One Collector, the name needs to match the `[a-zA-Z0-9]((\.(?!(\.|$)))|[_a-zA-Z0-9]){3,99}` regular expression. + */ ++ (void)trackEvent:(NSString *)eventName; + +/** + * Track a custom event with optional string properties. + * + * @param eventName Event name. Cannot be `nil` or empty. + * @param properties Dictionary of properties. Keys and values must not be `nil`. + * + * @discussion Additional validation rules apply depending on the configured secret. + * + * For App Center: + * + * - The event name cannot be longer than 256 and is truncated otherwise. + * + * - The property names cannot be empty. + * + * - The property names and values are limited to 125 characters each (truncated). + * + * - The number of properties per event is limited to 20 (truncated). + * + * + * For One Collector: + * + * - The event name needs to match the `[a-zA-Z0-9]((\.(?!(\.|$)))|[_a-zA-Z0-9]){3,99}` regular expression. + * + * - The `baseData` and `baseDataType` properties are reserved and thus discarded. + * + * - The full event size when encoded as a JSON string cannot be larger than 1.9MB. + */ ++ (void)trackEvent:(NSString *)eventName withProperties:(nullable NSDictionary *)properties; + +/** + * Track a custom event with optional string properties. + * + * @param eventName Event name. Cannot be `nil` or empty. + * @param properties Dictionary of properties. Keys and values must not be `nil`. + * @param flags Optional flags. Events tracked with the MSACFlagsCritical flag will take precedence over all other events in + * storage. An event tracked with this option will only be dropped if storage must make room for a newer event that is also marked with the + * MSACFlagsCritical flag. + * + * @discussion Additional validation rules apply depending on the configured secret. + * + * For App Center: + * + * - The event name cannot be longer than 256 and is truncated otherwise. + * + * - The property names cannot be empty. + * + * - The property names and values are limited to 125 characters each (truncated). + * + * - The number of properties per event is limited to 20 (truncated). + * + * + * For One Collector: + * + * - The event name needs to match the `[a-zA-Z0-9]((\.(?!(\.|$)))|[_a-zA-Z0-9]){3,99}` regular expression. + * + * - The `baseData` and `baseDataType` properties are reserved and thus discarded. + * + * - The full event size when encoded as a JSON string cannot be larger than 1.9MB. + */ ++ (void)trackEvent:(NSString *)eventName withProperties:(nullable NSDictionary *)properties flags:(MSACFlags)flags; + +/** + * Track a custom event with name and optional typed properties. + * + * @param eventName Event name. + * @param properties Typed properties. + * + * @discussion The following validation rules are applied: + * + * The name cannot be null or empty. + * + * The property names or values cannot be null. + * + * Double values must be finite (NaN or Infinite values are discarded). + * + * Additional validation rules apply depending on the configured secret. + * + * + * For App Center: + * + * - The event name cannot be longer than 256 and is truncated otherwise. + * + * - The property names cannot be empty. + * + * - The property names and values are limited to 125 characters each (truncated). + * + * - The number of properties per event is limited to 20 (truncated). + * + * + * For One Collector: + * + * - The event name needs to match the `[a-zA-Z0-9]((\.(?!(\.|$)))|[_a-zA-Z0-9]){3,99}` regular expression. + * + * - The `baseData` and `baseDataType` properties are reserved and thus discarded. + * + * - The full event size when encoded as a JSON string cannot be larger than 1.9MB. + */ ++ (void)trackEvent:(NSString *)eventName + withTypedProperties:(nullable MSACEventProperties *)properties NS_SWIFT_NAME(trackEvent(_:withProperties:)); + +/** + * Track a custom event with name and optional typed properties. + * + * @param eventName Event name. + * @param properties Typed properties. + * @param flags Optional flags. Events tracked with the MSACFlagsCritical flag will take precedence over all other events in + * storage. An event tracked with this option will only be dropped if storage must make room for a newer event that is also marked with the + * MSACFlagsCritical flag. + * + * @discussion The following validation rules are applied: + * + * The name cannot be null or empty. + * + * The property names or values cannot be null. + * + * Double values must be finite (NaN or Infinite values are discarded). + * + * Additional validation rules apply depending on the configured secret. + * + * + * For App Center: + * + * - The event name cannot be longer than 256 and is truncated otherwise. + * + * - The property names cannot be empty. + * + * - The property names and values are limited to 125 characters each (truncated). + * + * - The number of properties per event is limited to 20 (truncated). + * + * + * For One Collector: + * + * - The event name needs to match the `[a-zA-Z0-9]((\.(?!(\.|$)))|[_a-zA-Z0-9]){3,99}` regular expression. + * + * - The `baseData` and `baseDataType` properties are reserved and thus discarded. + * + * - The full event size when encoded as a JSON string cannot be larger than 1.9MB. + */ ++ (void)trackEvent:(NSString *)eventName + withTypedProperties:(nullable MSACEventProperties *)properties + flags:(MSACFlags)flags NS_SWIFT_NAME(trackEvent(_:withProperties:flags:)); + +/** + * Pause transmission of Analytics logs. While paused, Analytics logs are saved to disk. + * + * @see resume + */ ++ (void)pause; + +/** + * Resume transmission of Analytics logs. Any Analytics logs that accumulated on disk while paused are sent to the + * server. + * + * @see pause + */ ++ (void)resume; + +/** + * Get a transmission target. + * + * @param token The token of the transmission target to retrieve. + * + * @returns The transmission target object. + * + * @discussion This method does not need to be annotated with + * NS_SWIFT_NAME(transmissionTarget(forToken:)) as this is a static method that + * doesn't get translated like a setter in Swift. + * + * @see MSACAnalyticsTransmissionTarget for comparison. + */ ++ (MSACAnalyticsTransmissionTarget *)transmissionTargetForToken:(NSString *)token NS_SWIFT_NAME(transmissionTarget(forToken:)); + +/** + * Send time interval for non-critical logs. + * Must be between 3 seconds and 86400 seconds (1 day). + * Must be called before Analytics service start. + */ +@property(class, atomic) NSUInteger transmissionInterval; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/MSACAnalytics.m b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/MSACAnalytics.m new file mode 100644 index 0000000000..a06ed2bf27 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/MSACAnalytics.m @@ -0,0 +1,567 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAnalytics+Validation.h" +#import "MSACAnalyticsCategory.h" +#import "MSACAnalyticsConstants.h" +#import "MSACAnalyticsPrivate.h" +#import "MSACAnalyticsTransmissionTargetInternal.h" +#import "MSACBooleanTypedProperty.h" +#import "MSACChannelGroupProtocol.h" +#import "MSACChannelUnitConfiguration.h" +#import "MSACChannelUnitProtocol.h" +#import "MSACConstants+Internal.h" +#import "MSACDateTimeTypedProperty.h" +#import "MSACDeviceHistoryInfo.h" +#import "MSACDoubleTypedProperty.h" +#import "MSACEventLog.h" +#import "MSACEventProperties.h" +#import "MSACEventPropertiesInternal.h" +#import "MSACLongTypedProperty.h" +#import "MSACPageLog.h" +#import "MSACSessionContext.h" +#import "MSACStartSessionLog.h" +#import "MSACStringTypedProperty.h" +#import "MSACTypedProperty.h" +#import "MSACUserIdContext.h" +#import "MSACUtility+StringFormatting.h" + +// Service name for initialization. +static NSString *const kMSACServiceName = @"Analytics"; + +// The group Id for Analytics. +static NSString *const kMSACGroupId = @"Analytics"; + +// Singleton +static MSACAnalytics *sharedInstance = nil; +static dispatch_once_t onceToken; + +@implementation MSACAnalytics + +/** + * @discussion + * Workaround for exporting symbols from category object files. + * See article + * https://medium.com/ios-os-x-development/categories-in-static-libraries-78e41f8ddb96#.aedfl1kl0 + */ +__attribute__((used)) static void importCategories() { [NSString stringWithFormat:@"%@", MSACAnalyticsValidationCategory]; } + +@synthesize autoPageTrackingEnabled = _autoPageTrackingEnabled; +@synthesize channelUnitConfiguration = _channelUnitConfiguration; + +#pragma mark - Service initialization + +- (instancetype)init { + if ((self = [super init])) { + [MSAC_APP_CENTER_USER_DEFAULTS migrateKeys:@{ + @"MSAppCenterAnalyticsIsEnabled" : MSACPrefixKeyFrom(@"kMSAnalyticsIsEnabledKey"), // [MSACAnalytics isEnabled] + @"MSAppCenterPastSessions" : @"pastSessionsKey" // [MSACSessionTracker init] + } + forService:kMSACServiceName]; + [MSACUtility addMigrationClasses:@{ + @"MSSessionHistoryInfo" : MSACSessionHistoryInfo.self, + @"MSAbstractLog" : MSACAbstractLog.self, + @"MSEventLog" : MSACEventLog.self, + @"MSPageLog" : MSACPageLog.self, + @"MSEventProperties" : MSACEventProperties.self, + @"MSLogWithNameAndProperties" : MSACLogWithNameAndProperties.self, + @"MSBooleanTypedProperty" : MSACBooleanTypedProperty.self, + @"MSDateTimeTypedProperty" : MSACDateTimeTypedProperty.self, + @"MSDoubleTypedProperty" : MSACDoubleTypedProperty.self, + @"MSLongTypedProperty" : MSACLongTypedProperty.self, + @"MSStringTypedProperty" : MSACStringTypedProperty.self, + @"MSTypedProperty" : MSACTypedProperty.self, + @"MSStartSessionLog" : MSACStartSessionLog.self + }]; + + // Set defaults. + _autoPageTrackingEnabled = NO; + _flushInterval = kMSACFlushIntervalDefault; + + // Init session tracker. + _sessionTracker = [[MSACSessionTracker alloc] init]; + _sessionTracker.delegate = self; + + // Set up transmission target dictionary. + _transmissionTargets = [NSMutableDictionary new]; + } + return self; +} + +#pragma mark - MSACServiceInternal + ++ (instancetype)sharedInstance { + dispatch_once(&onceToken, ^{ + if (sharedInstance == nil) { + sharedInstance = [[MSACAnalytics alloc] init]; + } + }); + return sharedInstance; +} + ++ (NSString *)serviceName { + return kMSACServiceName; +} + +- (void)startWithChannelGroup:(id)channelGroup + appSecret:(nullable NSString *)appSecret + transmissionTargetToken:(nullable NSString *)token + fromApplication:(BOOL)fromApplication { + + // Init channel configuration. + self.channelUnitConfiguration = [[MSACChannelUnitConfiguration alloc] initDefaultConfigurationWithGroupId:[self groupId] + flushInterval:self.flushInterval]; + [super startWithChannelGroup:channelGroup appSecret:appSecret transmissionTargetToken:token fromApplication:fromApplication]; + if (token) { + + /* + * Don't use [self transmissionTargetForToken] because that will add the default transmission target to the cache, but it should be + * separate. + */ + self.defaultTransmissionTarget = [self createTransmissionTargetForToken:token]; + } + + // Add extra channel for critical events. + NSString *criticalGroupId = [NSString stringWithFormat:@"%@_%@", kMSACGroupId, kMSACCriticalChannelSuffix]; + MSACChannelUnitConfiguration *channelUnitConfiguration = + [[MSACChannelUnitConfiguration alloc] initDefaultConfigurationWithGroupId:criticalGroupId]; + self.criticalChannelUnit = [self.channelGroup addChannelUnitWithConfiguration:channelUnitConfiguration]; + + // TODO: Uncomment when auto page tracking will be supported. + // Set up swizzling for auto page tracking. + // [MSACAnalyticsCategory activateCategory]; + MSACLogVerbose([MSACAnalytics logTag], @"Started Analytics service."); +} + ++ (NSString *)logTag { + return @"AppCenterAnalytics"; +} + +- (NSString *)groupId { + return kMSACGroupId; +} + +#pragma mark - MSACServiceAbstract + +- (void)setEnabled:(BOOL)isEnabled { + [super setEnabled:isEnabled]; + + // Propagate to transmission targets. + for (NSString *token in self.transmissionTargets) { + [self.transmissionTargets[token] setEnabled:isEnabled]; + } + [self.defaultTransmissionTarget setEnabled:isEnabled]; +} + +- (void)applyEnabledState:(BOOL)isEnabled { + [super applyEnabledState:isEnabled]; + [self.criticalChannelUnit setEnabled:isEnabled andDeleteDataOnDisabled:YES]; + if (isEnabled) { + if (self.startedFromApplication) { + [self resume]; + + // Start session tracker. + [self.sessionTracker start]; + + // Add delegates to log manager. + [self.channelGroup addDelegate:self.sessionTracker]; + [self.channelGroup addDelegate:self]; + + // Report current page while auto page tracking is on. + if (self.autoPageTrackingEnabled) { + + // Track on the main queue to avoid race condition with page swizzling. + dispatch_async(dispatch_get_main_queue(), ^{ + if ([[MSACAnalyticsCategory missedPageViewName] length] > 0) { + [[self class] trackPage:(NSString *)[MSACAnalyticsCategory missedPageViewName]]; + } + }); + } + } + + MSACLogInfo([MSACAnalytics logTag], @"Analytics service has been enabled."); + } else { + if (self.startedFromApplication) { + [self.channelGroup removeDelegate:self.sessionTracker]; + [self.channelGroup removeDelegate:self]; + [self.sessionTracker stop]; + [[MSACSessionContext sharedInstance] clearSessionHistoryAndKeepCurrentSession:NO]; + } + MSACLogInfo([MSACAnalytics logTag], @"Analytics service has been disabled."); + } +} + +- (BOOL)isAppSecretRequired { + return NO; +} + +- (void)updateConfigurationWithAppSecret:(NSString *)appSecret transmissionTargetToken:(NSString *)token { + [super updateConfigurationWithAppSecret:appSecret transmissionTargetToken:token]; + + // Create the default target if not already created in start. + if (token && !self.defaultTransmissionTarget) { + + /* + * Don't use [self transmissionTargetForToken] because that will add the default transmission target to the cache, but it should be + * separate. + */ + self.defaultTransmissionTarget = [self createTransmissionTargetForToken:token]; + } +} + +#pragma mark - Service methods + ++ (void)trackEvent:(NSString *)eventName { + [self trackEvent:eventName withProperties:nil]; +} + ++ (void)trackEvent:(NSString *)eventName withProperties:(nullable NSDictionary *)properties { + [self trackEvent:eventName withProperties:properties flags:MSACFlagsDefault]; +} + ++ (void)trackEvent:(NSString *)eventName withProperties:(nullable NSDictionary *)properties flags:(MSACFlags)flags { + [self trackEvent:eventName withProperties:properties forTransmissionTarget:nil flags:flags]; +} + ++ (void)trackEvent:(NSString *)eventName withTypedProperties:(nullable MSACEventProperties *)properties { + [self trackEvent:eventName withTypedProperties:properties flags:MSACFlagsDefault]; +} + ++ (void)trackEvent:(NSString *)eventName withTypedProperties:(nullable MSACEventProperties *)properties flags:(MSACFlags)flags { + [self trackEvent:eventName withTypedProperties:properties forTransmissionTarget:nil flags:flags]; +} + ++ (void)trackEvent:(NSString *)eventName + withProperties:(nullable NSDictionary *)properties + forTransmissionTarget:(nullable MSACAnalyticsTransmissionTarget *)transmissionTarget + flags:(MSACFlags)flags { + [[MSACAnalytics sharedInstance] trackEvent:eventName withProperties:properties forTransmissionTarget:transmissionTarget flags:flags]; +} + ++ (void)trackEvent:(NSString *)eventName + withTypedProperties:(nullable MSACEventProperties *)properties + forTransmissionTarget:(nullable MSACAnalyticsTransmissionTarget *)transmissionTarget + flags:(MSACFlags)flags { + [[MSACAnalytics sharedInstance] trackEvent:eventName withTypedProperties:properties forTransmissionTarget:transmissionTarget flags:flags]; +} + ++ (void)trackPage:(NSString *)pageName { + [self trackPage:pageName withProperties:nil]; +} + ++ (void)trackPage:(NSString *)pageName withProperties:(nullable NSDictionary *)properties { + [[MSACAnalytics sharedInstance] trackPage:pageName withProperties:properties]; +} + ++ (void)pause { + [[MSACAnalytics sharedInstance] pause]; +} + ++ (void)resume { + [[MSACAnalytics sharedInstance] resume]; +} + ++ (void)setAutoPageTrackingEnabled:(BOOL)isEnabled { + [MSACAnalytics sharedInstance].autoPageTrackingEnabled = isEnabled; +} + ++ (BOOL)isAutoPageTrackingEnabled { + return [MSACAnalytics sharedInstance].autoPageTrackingEnabled; +} + ++ (NSUInteger)transmissionInterval { + return [MSACAnalytics sharedInstance].flushInterval; +} + ++ (void)setTransmissionInterval:(NSUInteger)interval { + [[MSACAnalytics sharedInstance] setTransmissionInterval:interval]; +} + +#pragma mark - Transmission Target + ++ (MSACAnalyticsTransmissionTarget *)transmissionTargetForToken:(NSString *)token { + return [[MSACAnalytics sharedInstance] transmissionTargetForToken:token]; +} + ++ (void)pauseTransmissionTargetForToken:(NSString *)token { + [[MSACAnalytics sharedInstance] pauseTransmissionTargetForToken:token]; +} + ++ (void)resumeTransmissionTargetForToken:(NSString *)token { + [[MSACAnalytics sharedInstance] resumeTransmissionTargetForToken:token]; +} + +#pragma mark - Private methods + +- (void)trackEvent:(NSString *)eventName + withProperties:(NSDictionary *)properties + forTransmissionTarget:(MSACAnalyticsTransmissionTarget *)transmissionTarget + flags:(MSACFlags)flags { + NSDictionary *validProperties = [self removeInvalidProperties:properties]; + MSACEventProperties *eventProperties = [[MSACEventProperties alloc] initWithStringDictionary:validProperties]; + [self trackEvent:eventName withTypedProperties:eventProperties forTransmissionTarget:transmissionTarget flags:flags]; +} + +- (void)trackEvent:(NSString *)eventName + withTypedProperties:(MSACEventProperties *)properties + forTransmissionTarget:(MSACAnalyticsTransmissionTarget *)transmissionTarget + flags:(MSACFlags)flags { + @synchronized(self) { + if (![self canBeUsed] || ![self isEnabled]) { + return; + } + + // Use default transmission target if no transmission target was provided. + if (transmissionTarget == nil) { + transmissionTarget = self.defaultTransmissionTarget; + } + + // Validate flags. + MSACFlags persistenceFlag = flags & kMSACPersistenceFlagsMask; + if (persistenceFlag != MSACFlagsNormal && persistenceFlag != MSACFlagsCritical) { + MSACLogWarning([MSACAnalytics logTag], @"Invalid flags (%u) received, using normal as a default.", (unsigned int)persistenceFlag); + persistenceFlag = MSACFlagsNormal; + } + + // Create an event log. + MSACEventLog *log = [MSACEventLog new]; + + // Add transmission target token. + if (transmissionTarget) { + if (transmissionTarget.isEnabled) { + [log addTransmissionTargetToken:[transmissionTarget transmissionTargetToken]]; + log.tag = transmissionTarget; + if (transmissionTarget == self.defaultTransmissionTarget) { + log.userId = [[MSACUserIdContext sharedInstance] userId]; + } + } else { + MSACLogError([MSACAnalytics logTag], @"This transmission target is disabled."); + return; + } + } else { + properties = [self validateAppCenterEventProperties:properties]; + } + + // Set properties of the event log. + log.name = eventName; + log.eventId = MSAC_UUID_STRING; + log.typedProperties = [properties isEmpty] ? nil : properties; + + // Send log to channel. + [self sendLog:log flags:persistenceFlag]; + } +} + +- (void)pause { + @synchronized(self) { + if ([self canBeUsed]) { + [self.channelUnit pauseWithIdentifyingObject:self]; + [self.criticalChannelUnit pauseWithIdentifyingObject:self]; + } + } +} + +- (void)resume { + @synchronized(self) { + if ([self canBeUsed]) { + [self.channelUnit resumeWithIdentifyingObject:self]; + [self.criticalChannelUnit resumeWithIdentifyingObject:self]; + } + } +} + +- (NSDictionary *)removeInvalidProperties:(NSDictionary *)properties { + NSMutableDictionary *validProperties = [NSMutableDictionary new]; + for (NSString *key in properties) { + if (![key isKindOfClass:[NSString class]]) { + MSACLogWarning([MSACAnalytics logTag], @"Event property contains an invalid key, dropping the property."); + continue; + } + + // We have a valid key, so let's validate the value. + id value = properties[key]; + if (value) { + + // Not checking for empty string, as values can be empty strings. + if ([(NSObject *)value isKindOfClass:[NSString class]]) { + [validProperties setValue:value forKey:key]; + } + } else { + MSACLogWarning([MSACAnalytics logTag], @"Event property contains an invalid value for key %@, dropping the property.", key); + } + } + + return validProperties; +} + +- (void)trackPage:(NSString *)pageName withProperties:(NSDictionary *)properties { + @synchronized(self) { + if (![self canBeUsed] || ![self isEnabled]) { + return; + } + + // Create an event log. + MSACPageLog *log = [MSACPageLog new]; + + // Set properties of the event log. + log.name = pageName; + if (properties && properties.count > 0) { + log.properties = [self removeInvalidProperties:properties]; + } + + // Send log to log manager. + [self sendLog:log flags:MSACFlagsDefault]; + } +} + +- (void)sendLog:(id)log flags:(MSACFlags)flags { + if ((flags & MSACFlagsCritical) != 0) { + [self.criticalChannelUnit enqueueItem:log flags:flags]; + } else { + [self.channelUnit enqueueItem:log flags:flags]; + } +} + +- (void)setTransmissionInterval:(NSUInteger)interval { + if (self.started) { + MSACLogError([MSACAnalytics logTag], @"The transmission interval should be set before the MSACAnalytics service is started."); + return; + } + if (interval > kMSACFlushIntervalMaximum || interval < kMSACFlushIntervalMinimum) { + MSACLogError([MSACAnalytics logTag], + @"The transmission interval is not valid, it should be between %u second(s) and %u second(s) (%u day).", + (unsigned int)kMSACFlushIntervalMinimum, (unsigned int)kMSACFlushIntervalMaximum, + (unsigned int)(kMSACFlushIntervalMaximum / 86400)); + return; + } + self.flushInterval = interval; + MSACLogDebug([MSACAnalytics logTag], @"Transmission interval set to %u second(s)", (unsigned int)interval); +} + +- (MSACAnalyticsTransmissionTarget *)transmissionTargetForToken:(NSString *)transmissionTargetToken { + MSACAnalyticsTransmissionTarget *transmissionTarget = self.transmissionTargets[transmissionTargetToken]; + if (transmissionTarget) { + MSACLogDebug([MSACAnalytics logTag], @"Returning transmission target found with id %@.", + [MSACUtility targetKeyFromTargetToken:transmissionTargetToken]); + return transmissionTarget; + } + transmissionTarget = [self createTransmissionTargetForToken:transmissionTargetToken]; + self.transmissionTargets[transmissionTargetToken] = transmissionTarget; + + // TODO: Start service if not already. + // Scenario: getTransmissionTarget gets called before App Center has an app + // secret or transmission target but start has been called for this service. + return transmissionTarget; +} + +- (MSACAnalyticsTransmissionTarget *)createTransmissionTargetForToken:(NSString *)transmissionTargetToken { + MSACAnalyticsTransmissionTarget *target = [[MSACAnalyticsTransmissionTarget alloc] initWithTransmissionTargetToken:transmissionTargetToken + parentTarget:nil + channelGroup:self.channelGroup]; + MSACLogDebug([MSACAnalytics logTag], @"Created transmission target with target key %@.", + [MSACUtility targetKeyFromTargetToken:transmissionTargetToken]); + return target; +} + +- (void)pauseTransmissionTargetForToken:(NSString *)token { + [self.oneCollectorChannelUnit pauseSendingLogsWithToken:token]; + [self.oneCollectorCriticalChannelUnit pauseSendingLogsWithToken:token]; +} + +- (void)resumeTransmissionTargetForToken:(NSString *)token { + [self.oneCollectorChannelUnit resumeSendingLogsWithToken:token]; + [self.oneCollectorCriticalChannelUnit resumeSendingLogsWithToken:token]; +} + +- (id)oneCollectorChannelUnit { + if (!_oneCollectorChannelUnit) { + NSString *oneCollectorGroupId = [NSString stringWithFormat:@"%@%@", self.groupId, kMSACOneCollectorGroupIdSuffix]; + self.oneCollectorChannelUnit = [self.channelGroup channelUnitForGroupId:oneCollectorGroupId]; + } + return _oneCollectorChannelUnit; +} + +- (id)oneCollectorCriticalChannelUnit { + if (!_oneCollectorCriticalChannelUnit) { + NSString *oneCollectorCriticalGroupId = + [NSString stringWithFormat:@"%@_%@%@", self.groupId, kMSACCriticalChannelSuffix, kMSACOneCollectorGroupIdSuffix]; + self.oneCollectorCriticalChannelUnit = [self.channelGroup channelUnitForGroupId:oneCollectorCriticalGroupId]; + } + return _oneCollectorCriticalChannelUnit; +} + ++ (void)resetSharedInstance { + + // Clean existing instance by stopping session tracker, it'll remove its observers. + [sharedInstance.sessionTracker stop]; + + // Resets the once_token so dispatch_once will run again. + onceToken = 0; + sharedInstance = nil; +} + +#pragma mark - MSACSessionTracker + +- (void)sessionTracker:(id)sessionTracker processLog:(id)log { + (void)sessionTracker; + [self sendLog:log flags:MSACFlagsDefault]; +} + ++ (void)setDelegate:(nullable id)delegate { + [[MSACAnalytics sharedInstance] setDelegate:delegate]; +} + +#pragma mark - MSACChannelDelegate + +- (void)channel:(id)channel willSendLog:(id)log { + (void)channel; + if (!self.delegate) { + return; + } + NSObject *logObject = (NSObject *)log; + id delegate = self.delegate; + if ([logObject isKindOfClass:[MSACEventLog class]] && [delegate respondsToSelector:@selector(analytics:willSendEventLog:)]) { + MSACEventLog *eventLog = (MSACEventLog *)log; + [delegate analytics:self willSendEventLog:eventLog]; + } else if ([logObject isKindOfClass:[MSACPageLog class]] && [delegate respondsToSelector:@selector(analytics:willSendPageLog:)]) { + MSACPageLog *pageLog = (MSACPageLog *)log; + [delegate analytics:self willSendPageLog:pageLog]; + } +} + +- (void)channel:(id)channel didSucceedSendingLog:(id)log { + (void)channel; + if (!self.delegate) { + return; + } + NSObject *logObject = (NSObject *)log; + if ([logObject isKindOfClass:[MSACEventLog class]] && [self.delegate respondsToSelector:@selector(analytics: + didSucceedSendingEventLog:)]) { + MSACEventLog *eventLog = (MSACEventLog *)log; + [self.delegate analytics:self didSucceedSendingEventLog:eventLog]; + } else if ([logObject isKindOfClass:[MSACPageLog class]] && [self.delegate respondsToSelector:@selector(analytics: + didSucceedSendingPageLog:)]) { + MSACPageLog *pageLog = (MSACPageLog *)log; + [self.delegate analytics:self didSucceedSendingPageLog:pageLog]; + } +} + +- (void)channel:(id)channel didFailSendingLog:(id)log withError:(NSError *)error { + (void)channel; + if (!self.delegate) { + return; + } + NSObject *logObject = (NSObject *)log; + id delegate = self.delegate; + if ([logObject isKindOfClass:[MSACEventLog class]] && [delegate respondsToSelector:@selector(analytics: + didFailSendingEventLog:withError:)]) { + MSACEventLog *eventLog = (MSACEventLog *)log; + [delegate analytics:self didFailSendingEventLog:eventLog withError:error]; + } else if ([logObject isKindOfClass:[MSACPageLog class]] && [delegate respondsToSelector:@selector(analytics: + didFailSendingPageLog:withError:)]) { + MSACPageLog *pageLog = (MSACPageLog *)log; + [delegate analytics:self didFailSendingPageLog:pageLog withError:error]; + } +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Model/MSACEventLog.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Model/MSACEventLog.h new file mode 100644 index 0000000000..7fa6b0959b --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Model/MSACEventLog.h @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACLogWithNameAndProperties.h" + +@class MSACEventProperties; +@class MSACMetadataExtension; + +NS_SWIFT_NAME(EventLog) +@interface MSACEventLog : MSACLogWithNameAndProperties + +/** + * Unique identifier for this event. + */ +@property(nonatomic, copy) NSString *eventId; + +/** + * Event properties. + */ +@property(nonatomic, strong) MSACEventProperties *typedProperties; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Model/MSACEventLog.m b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Model/MSACEventLog.m new file mode 100644 index 0000000000..0e88e837b6 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Model/MSACEventLog.m @@ -0,0 +1,239 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "AppCenter+Internal.h" +#import "MSACAnalyticsConstants.h" +#import "MSACAnalyticsInternal.h" +#import "MSACBooleanTypedProperty.h" +#import "MSACCSData.h" +#import "MSACCSExtensions.h" +#import "MSACConstants+Internal.h" +#import "MSACDateTimeTypedProperty.h" +#import "MSACDoubleTypedProperty.h" +#import "MSACEventLogPrivate.h" +#import "MSACEventPropertiesInternal.h" +#import "MSACLongTypedProperty.h" +#import "MSACMetadataExtension.h" +#import "MSACStringTypedProperty.h" + +static NSString *const kMSACTypeEvent = @"event"; + +static NSString *const kMSACId = @"id"; + +static NSString *const kMSACTypedProperties = @"typedProperties"; + +@implementation MSACEventLog + +- (instancetype)init { + if ((self = [super init])) { + self.type = kMSACTypeEvent; + _metadataTypeIdMapping = @{ + kMSACLongTypedPropertyType : @(kMSACLongMetadataTypeId), + kMSACDoubleTypedPropertyType : @(kMSACDoubleMetadataTypeId), + kMSACDateTimeTypedPropertyType : @(kMSACDateTimeMetadataTypeId) + }; + } + return self; +} + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary]; + if (self.eventId) { + dict[kMSACId] = self.eventId; + } + if (self.typedProperties) { + dict[kMSACTypedProperties] = [self.typedProperties serializeToArray]; + } + return dict; +} + +- (BOOL)isValid { + return [super isValid] && MSACLOG_VALIDATE_NOT_NIL(eventId); +} + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACEventLog class]] || ![super isEqual:object]) { + return NO; + } + MSACEventLog *eventLog = (MSACEventLog *)object; + return ((!self.eventId && !eventLog.eventId) || [self.eventId isEqualToString:eventLog.eventId]); +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if (self) { + _eventId = [coder decodeObjectForKey:kMSACId]; + _typedProperties = [coder decodeObjectForKey:kMSACTypedProperties]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.eventId forKey:kMSACId]; + [coder encodeObject:self.typedProperties forKey:kMSACTypedProperties]; +} + +#pragma mark - MSACAbstractLog + +- (MSACCommonSchemaLog *)toCommonSchemaLogForTargetToken:(NSString *)token flags:(MSACFlags)flags { + MSACCommonSchemaLog *csLog = [super toCommonSchemaLogForTargetToken:token flags:flags]; + + // Event name goes to part A. + csLog.name = self.name; + + // Metadata extension must accompany data. + // Event properties goes to part C. + [self setPropertiesAndMetadataForCSLog:csLog]; + csLog.tag = self.tag; + return csLog; +} + +#pragma mark - Helper + +- (void)setPropertiesAndMetadataForCSLog:(MSACCommonSchemaLog *)csLog { + NSMutableDictionary *csProperties; + NSMutableDictionary *metadata; + if (self.typedProperties) { + csProperties = [NSMutableDictionary new]; + metadata = [NSMutableDictionary new]; + NSString *baseTypePrefix = [NSString stringWithFormat:@"%@.", kMSACDataBaseType]; + NSString *baseDataPrefix = [NSString stringWithFormat:@"%@.", kMSACDataBaseData]; + + // If baseType is set and valid, make sure it's paired with at least 1 "baseData.*" property. + if ([self.typedProperties.properties[kMSACDataBaseType] isKindOfClass:[MSACStringTypedProperty class]]) { + BOOL foundBaseData = NO; + for (NSString *key in [self.typedProperties.properties allKeys]) { + if ([key hasPrefix:baseDataPrefix]) { + foundBaseData = YES; + break; + } + } + if (!foundBaseData) { + MSACLogWarning([MSACAnalytics logTag], @"baseType was set but baseData is missing."); + [self.typedProperties.properties removeObjectForKey:kMSACDataBaseType]; + } + } + + // If there is no valid "baseType" property, there must not be any "baseData.*" property. + else { + BOOL removedBaseData = NO; + for (NSString *key in [self.typedProperties.properties allKeys]) { + if ([key hasPrefix:baseDataPrefix]) { + [self.typedProperties.properties removeObjectForKey:key]; + removedBaseData = YES; + } + } + if (removedBaseData) { + MSACLogWarning([MSACAnalytics logTag], @"baseData was set but baseType is missing or invalid."); + } + + // Base type might be set but invalid, so remove it. + [self.typedProperties.properties removeObjectForKey:kMSACDataBaseType]; + } + + // Add typed properties and metadata to the common schema log fields. + for (MSACTypedProperty *typedProperty in [self.typedProperties.properties objectEnumerator]) { + + // Validate baseType is not an object, meaning it should not have dot. + if ([[typedProperty name] hasPrefix:baseTypePrefix]) { + MSACLogWarning([MSACAnalytics logTag], @"baseType must not be an object."); + continue; + } + + // Validate baseData is an object, meaning it has at least 1 dot. + if ([[typedProperty name] isEqualToString:kMSACDataBaseData]) { + MSACLogWarning([MSACAnalytics logTag], @"baseData must be an object."); + continue; + } + + // Convert property. + [self addTypedProperty:typedProperty toCSMetadata:metadata andCSProperties:csProperties]; + } + } + if (csProperties.count != 0) { + csLog.data = [MSACCSData new]; + csLog.data.properties = csProperties; + } + if (metadata.count != 0) { + csLog.ext.metadataExt = [MSACMetadataExtension new]; + csLog.ext.metadataExt.metadata = metadata; + } +} + +- (void)addTypedProperty:(MSACTypedProperty *)typedProperty + toCSMetadata:(NSMutableDictionary *)csMetadata + andCSProperties:(NSMutableDictionary *)csProperties { + NSNumber *typeId = self.metadataTypeIdMapping[typedProperty.type]; + + // If the key contains a '.' then it's nested objects (i.e: "a.b":"value" => {"a":{"b":"value"}}). + NSArray *csKeys = [typedProperty.name componentsSeparatedByString:@"."]; + NSMutableDictionary *propertyTree = csProperties; + NSMutableDictionary *metadataTree = csMetadata; + + /* + * Keep track of the subtree that contains all the metadata levels added in the for loop. + * Thus if it needs to be removed, a second traversal is not needed. + * The metadata should be cleaned up if the property is not added due to a key collision. + */ + NSMutableDictionary *metadataSubtreeParent = nil; + for (NSUInteger i = 0; i < csKeys.count - 1; i++) { + id key = csKeys[i]; + if (![(NSObject *)propertyTree[key] isKindOfClass:[NSMutableDictionary class]]) { + if (propertyTree[key]) { + propertyTree = nil; + break; + } + propertyTree[key] = [NSMutableDictionary new]; + } + propertyTree = propertyTree[key]; + if (typeId) { + if (!metadataTree[kMSACFieldDelimiter]) { + metadataTree[kMSACFieldDelimiter] = [NSMutableDictionary new]; + metadataSubtreeParent = metadataSubtreeParent ?: metadataTree; + } + if (!metadataTree[kMSACFieldDelimiter][key]) { + metadataTree[kMSACFieldDelimiter][key] = [NSMutableDictionary new]; + } + metadataTree = metadataTree[kMSACFieldDelimiter][key]; + } + } + id lastKey = csKeys.lastObject; + BOOL didAddTypedProperty = [self addTypedProperty:typedProperty toPropertyTree:propertyTree withKey:lastKey]; + if (typeId && didAddTypedProperty) { + if (!metadataTree[kMSACFieldDelimiter]) { + metadataTree[kMSACFieldDelimiter] = [NSMutableDictionary new]; + } + metadataTree[kMSACFieldDelimiter][lastKey] = typeId; + } else if (metadataSubtreeParent) { + [metadataSubtreeParent removeObjectForKey:kMSACFieldDelimiter]; + } +} + +- (BOOL)addTypedProperty:(MSACTypedProperty *)typedProperty toPropertyTree:(NSMutableDictionary *)propertyTree withKey:(NSString *)key { + if (!propertyTree || propertyTree[key]) { + MSACLogWarning(MSACAnalytics.logTag, @"Property key '%@' already has a value, choosing one.", key); + return NO; + } + if ([typedProperty isKindOfClass:[MSACStringTypedProperty class]]) { + MSACStringTypedProperty *stringProperty = (MSACStringTypedProperty *)typedProperty; + propertyTree[key] = stringProperty.value; + } else if ([typedProperty isKindOfClass:[MSACBooleanTypedProperty class]]) { + MSACBooleanTypedProperty *boolProperty = (MSACBooleanTypedProperty *)typedProperty; + propertyTree[key] = @(boolProperty.value); + } else if ([typedProperty isKindOfClass:[MSACLongTypedProperty class]]) { + MSACLongTypedProperty *longProperty = (MSACLongTypedProperty *)typedProperty; + propertyTree[key] = @(longProperty.value); + } else if ([typedProperty isKindOfClass:[MSACDoubleTypedProperty class]]) { + MSACDoubleTypedProperty *doubleProperty = (MSACDoubleTypedProperty *)typedProperty; + propertyTree[key] = @(doubleProperty.value); + } else if ([typedProperty isKindOfClass:[MSACDateTimeTypedProperty class]]) { + MSACDateTimeTypedProperty *dateProperty = (MSACDateTimeTypedProperty *)typedProperty; + propertyTree[key] = [MSACUtility dateToISO8601:dateProperty.value]; + } + return YES; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Model/MSACEventProperties.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Model/MSACEventProperties.h new file mode 100644 index 0000000000..c59015a999 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Model/MSACEventProperties.h @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Contains typed event properties. + */ +NS_SWIFT_NAME(EventProperties) +@interface MSACEventProperties : NSObject + +/** + * Set a string property. + * + * @param value Property value. + * @param key Property key. + */ +- (instancetype)setString:(NSString *)value forKey:(NSString *)key NS_SWIFT_NAME(setEventProperty(_:forKey:)); + +/** + * Set a double property. + * + * @param value Property value. Must be finite (`NAN` and `INFINITY` not allowed). + * @param key Property key. + */ +- (instancetype)setDouble:(double)value forKey:(NSString *)key NS_SWIFT_NAME(setEventProperty(_:forKey:)); + +/** + * Set a 64-bit integer property. + * + * @param value Property value. + * @param key Property key. + */ +- (instancetype)setInt64:(int64_t)value forKey:(NSString *)key NS_SWIFT_NAME(setEventProperty(_:forKey:)); + +/** + * Set a boolean property. + * + * @param value Property value. + * @param key Property key. + */ +- (instancetype)setBool:(BOOL)value forKey:(NSString *)key NS_SWIFT_NAME(setEventProperty(_:forKey:)); + +/** + * Set a date property. + * + * @param value Property value. + * @param key Property key. + */ +- (instancetype)setDate:(NSDate *)value forKey:(NSString *)key NS_SWIFT_NAME(setEventProperty(_:forKey:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Model/MSACEventProperties.m b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Model/MSACEventProperties.m new file mode 100644 index 0000000000..b0b2afa5fb --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Model/MSACEventProperties.m @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACEventProperties.h" +#import "MSACAnalyticsInternal.h" +#import "MSACBooleanTypedProperty.h" +#import "MSACDateTimeTypedProperty.h" +#import "MSACDoubleTypedProperty.h" +#import "MSACEventPropertiesInternal.h" +#import "MSACLogger.h" +#import "MSACLongTypedProperty.h" +#import "MSACStringTypedProperty.h" + +@implementation MSACEventProperties + +- (instancetype)init { + if ((self = [super init])) { + _properties = [NSMutableDictionary new]; + } + return self; +} + +- (instancetype)initWithStringDictionary:(NSDictionary *)properties { + if ((self = [self init])) { + for (NSString *propertyKey in properties) { + MSACStringTypedProperty *stringProperty = [MSACStringTypedProperty new]; + stringProperty.name = propertyKey; + stringProperty.value = properties[propertyKey]; + _properties[propertyKey] = stringProperty; + } + } + return self; +} + +#pragma mark - NSCoding + +- (void)encodeWithCoder:(NSCoder *)coder { + @synchronized(self.properties) { + [coder encodeObject:self.properties]; + } +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + if ((self = [self init])) { + _properties = (NSMutableDictionary *)[coder decodeObject]; + } + return self; +} + +#pragma mark - Public methods + +- (instancetype)setString:(NSString *)value forKey:(NSString *)key { + if ([MSACEventProperties validateKey:key] && [MSACEventProperties validateValue:value]) { + MSACStringTypedProperty *stringProperty = [MSACStringTypedProperty new]; + stringProperty.name = key; + stringProperty.value = value; + @synchronized(self.properties) { + self.properties[key] = stringProperty; + } + } + return self; +} + +- (instancetype)setDouble:(double)value forKey:(NSString *)key { + if ([MSACEventProperties validateKey:key]) { + + // NaN returns false for all statements, so the only way to check if value is NaN is by value != value. + if (value == (double)INFINITY || value == -(double)INFINITY || value != value) { + MSACLogError([MSACAnalytics logTag], @"Double value for property '%@' must be finite (cannot be INFINITY or NAN).", key); + return self; + } + MSACDoubleTypedProperty *doubleProperty = [MSACDoubleTypedProperty new]; + doubleProperty.name = key; + doubleProperty.value = value; + @synchronized(self.properties) { + self.properties[key] = doubleProperty; + } + } + return self; +} + +- (instancetype)setInt64:(int64_t)value forKey:(NSString *)key { + if ([MSACEventProperties validateKey:key]) { + MSACLongTypedProperty *longProperty = [MSACLongTypedProperty new]; + longProperty.name = key; + longProperty.value = value; + @synchronized(self.properties) { + self.properties[key] = longProperty; + } + } + return self; +} + +- (instancetype)setBool:(BOOL)value forKey:(NSString *)key { + if ([MSACEventProperties validateKey:key]) { + MSACBooleanTypedProperty *boolProperty = [MSACBooleanTypedProperty new]; + boolProperty.name = key; + boolProperty.value = value; + @synchronized(self.properties) { + self.properties[key] = boolProperty; + } + } + return self; +} + +- (instancetype)setDate:(NSDate *)value forKey:(NSString *)key { + if ([MSACEventProperties validateKey:key] && [MSACEventProperties validateValue:value]) { + MSACDateTimeTypedProperty *dateTimeProperty = [MSACDateTimeTypedProperty new]; + dateTimeProperty.name = key; + dateTimeProperty.value = value; + @synchronized(self.properties) { + self.properties[key] = dateTimeProperty; + } + } + return self; +} + +#pragma mark - Internal methods + +- (NSMutableArray *)serializeToArray { + NSMutableArray *propertiesArray = [NSMutableArray new]; + @synchronized(self.properties) { + for (MSACTypedProperty *typedProperty in [self.properties objectEnumerator]) { + [propertiesArray addObject:[typedProperty serializeToDictionary]]; + } + } + return propertiesArray; +} + +- (BOOL)isEmpty { + return [self.properties count] == 0; +} + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACEventProperties class]]) { + return NO; + } + MSACEventProperties *properties = (MSACEventProperties *)object; + return ((!self.properties && !properties.properties) || [self.properties isEqualToDictionary:properties.properties]); +} + +#pragma mark - Helper methods + ++ (BOOL)validateKey:(NSString *)key { + if (!key) { + MSACLogError([MSACAnalytics logTag], @"Key cannot be null. Property will not be added."); + return NO; + } + return YES; +} + ++ (BOOL)validateValue:(NSObject *)value { + if (!value) { + MSACLogError([MSACAnalytics logTag], @"Value cannot be null. Property will not be added."); + return NO; + } + return YES; +} + +- (void)mergeEventProperties:(MSACEventProperties *__nonnull)eventProperties { + [self.properties addEntriesFromDictionary:(NSDictionary *)eventProperties.properties]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Model/MSACLogWithNameAndProperties.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Model/MSACLogWithNameAndProperties.h new file mode 100644 index 0000000000..8d1c4718af --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Model/MSACLogWithNameAndProperties.h @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACLogWithProperties.h" + +NS_SWIFT_NAME(LogWithNameAndProperties) +@interface MSACLogWithNameAndProperties : MSACLogWithProperties + +/** + * Name of the event. + */ +@property(nonatomic, copy) NSString *name; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Model/MSACLogWithNameAndProperties.m b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Model/MSACLogWithNameAndProperties.m new file mode 100644 index 0000000000..06d422c979 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Model/MSACLogWithNameAndProperties.m @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACLogWithNameAndProperties.h" +#import "AppCenter+Internal.h" + +static NSString *const kMSName = @"name"; + +@implementation MSACLogWithNameAndProperties + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary]; + + if (self.name) { + dict[kMSName] = self.name; + } + return dict; +} + +- (BOOL)isValid { + return [super isValid] && MSACLOG_VALIDATE_NOT_NIL(name); +} + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACLogWithNameAndProperties class]] || ![super isEqual:object]) { + return NO; + } + MSACLogWithNameAndProperties *analyticsLog = (MSACLogWithNameAndProperties *)object; + return ((!self.name && !analyticsLog.name) || [self.name isEqualToString:analyticsLog.name]); +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if (self) { + _name = [coder decodeObjectForKey:kMSName]; + } + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.name forKey:kMSName]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/AppCenterAnalytics Debug.xcconfig b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/AppCenterAnalytics Debug.xcconfig new file mode 100644 index 0000000000..126c82fc69 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/AppCenterAnalytics Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../../Config/Global Debug.xcconfig" +#include "./AppCenterAnalytics.xcconfig" diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/AppCenterAnalytics Release.xcconfig b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/AppCenterAnalytics Release.xcconfig new file mode 100644 index 0000000000..c713bb3362 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/AppCenterAnalytics Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../../Config/Global Release.xcconfig" +#include "./AppCenterAnalytics.xcconfig" diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/AppCenterAnalytics.xcconfig b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/AppCenterAnalytics.xcconfig new file mode 100644 index 0000000000..c090965701 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/AppCenterAnalytics.xcconfig @@ -0,0 +1,3 @@ +PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.appcenter.analytics + +OTHER_LDFLAGS=$(inherited) -framework Foundation; diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/Info.plist b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/Info.plist new file mode 100644 index 0000000000..e30a31ca6c --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + $(CURRENT_PROJECT_VERSION) + CFBundleVersion + 1.0 + + diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/iOS.modulemap b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/iOS.modulemap new file mode 100644 index 0000000000..ea370ea5a7 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/iOS.modulemap @@ -0,0 +1,9 @@ +framework module AppCenterAnalytics { + umbrella header "AppCenterAnalytics.h" + + export * + module * { export * } + + link framework "Foundation" + link framework "UIKit" +} diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/iOS.xcconfig b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/iOS.xcconfig new file mode 100644 index 0000000000..0adf181b18 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/iOS.xcconfig @@ -0,0 +1,4 @@ +#include "../../../Config/iOS.xcconfig" +#include "../../../Config/Framework.xcconfig" + +OTHER_LDFLAGS=$(inherited) -framework UIKit diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/macOS.modulemap b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/macOS.modulemap new file mode 100644 index 0000000000..e8726e3794 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/macOS.modulemap @@ -0,0 +1,9 @@ +framework module AppCenterAnalytics { + umbrella header "AppCenterAnalytics.h" + + export * + module * { export * } + + link framework "Foundation" + link framework "AppKit" +} diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/macOS.xcconfig b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/macOS.xcconfig new file mode 100644 index 0000000000..87127a88a4 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/macOS.xcconfig @@ -0,0 +1,4 @@ +#include "../../../Config/macOS.xcconfig" +#include "../../../Config/Framework.xcconfig" + +OTHER_LDFLAGS=$(inherited) -framework AppKit diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/tvOS.modulemap b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/tvOS.modulemap new file mode 100644 index 0000000000..ea370ea5a7 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/tvOS.modulemap @@ -0,0 +1,9 @@ +framework module AppCenterAnalytics { + umbrella header "AppCenterAnalytics.h" + + export * + module * { export * } + + link framework "Foundation" + link framework "UIKit" +} diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/tvOS.xcconfig b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/tvOS.xcconfig new file mode 100644 index 0000000000..0bce0881fb --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/Support/tvOS.xcconfig @@ -0,0 +1,4 @@ +#include "../../../Config/tvOS.xcconfig" +#include "../../../Config/Framework.xcconfig" + +OTHER_LDFLAGS=$(inherited) -framework UIKit diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/TransmissionTarget/MSACAnalyticsAuthenticationProvider.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/TransmissionTarget/MSACAnalyticsAuthenticationProvider.h new file mode 100644 index 0000000000..49bba9ec4a --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/TransmissionTarget/MSACAnalyticsAuthenticationProvider.h @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAnalyticsAuthenticationProviderDelegate.h" + +/** + * Different authentication types, e.g. MSA Compact, MSA Delegate, AAD,... . + */ +typedef NS_ENUM(NSUInteger, MSACAnalyticsAuthenticationType) { + + /** + * AuthenticationType MSA Compact. + */ + MSACAnalyticsAuthenticationTypeMsaCompact, + + /** + * AuthenticationType MSA Delegate. + */ + MSACAnalyticsAuthenticationTypeMsaDelegate +} NS_SWIFT_NAME(AnalyticsAuthenticationType); + +NS_ASSUME_NONNULL_BEGIN + +NS_SWIFT_NAME(AnalyticsAuthenticationProvider) +@interface MSACAnalyticsAuthenticationProvider : NSObject + +/** + * The type. + */ +@property(nonatomic, readonly, assign) MSACAnalyticsAuthenticationType type; + +/** + * The ticket key for this authentication provider. + */ +@property(nonatomic, readonly, copy) NSString *ticketKey; + +/** + * The ticket key as hash. + */ +@property(nonatomic, readonly, copy) NSString *ticketKeyHash; + +@property(nonatomic, readonly, weak) id delegate; + +/** + * Create a new authentication provider. + * + * @param type The type for the provider, e.g. MSA. + * @param ticketKey The ticket key for the provider. + * @param delegate The delegate. + * + * @return A new authentication provider. + */ +- (instancetype)initWithAuthenticationType:(MSACAnalyticsAuthenticationType)type + ticketKey:(NSString *)ticketKey + delegate:(id)delegate; + +/** + * Check expiration. + */ +- (void)checkTokenExpiry; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/TransmissionTarget/MSACAnalyticsAuthenticationProvider.m b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/TransmissionTarget/MSACAnalyticsAuthenticationProvider.m new file mode 100644 index 0000000000..b2a709a3a0 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/TransmissionTarget/MSACAnalyticsAuthenticationProvider.m @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAnalyticsAuthenticationProvider.h" +#import "MSACAnalyticsAuthenticationProviderDelegate.h" +#import "MSACAnalyticsInternal.h" +#import "MSACLogger.h" +#import "MSACTicketCache.h" +#import "MSACUtility+StringFormatting.h" + +// Number of seconds to refresh token before it expires. +static int kMSRefreshThreshold = 10 * 60; + +@interface MSACAnalyticsAuthenticationProvider () + +@property(nonatomic) NSDate *expiryDate; + +/** + * Completion block that will be used to get an updated authentication token. + */ +@property(nonatomic, copy) MSACAnalyticsAuthenticationProviderCompletionBlock completionHandler; + +@end + +@implementation MSACAnalyticsAuthenticationProvider + +- (instancetype)initWithAuthenticationType:(MSACAnalyticsAuthenticationType)type + ticketKey:(NSString *)ticketKey + delegate:(id)delegate { + if ((self = [super init])) { + _type = type; + _ticketKey = ticketKey; + if (ticketKey) { + _ticketKeyHash = [MSACUtility sha256:ticketKey]; + } + _delegate = delegate; + } + return self; +} + +- (void)acquireTokenAsync { + id strongDelegate = self.delegate; + if (strongDelegate) { + if (!self.completionHandler) { + MSACAnalyticsAuthenticationProvider *__weak weakSelf = self; + self.completionHandler = ^void(NSString *token, NSDate *expiryDate) { + MSACAnalyticsAuthenticationProvider *strongSelf = weakSelf; + [strongSelf handleTokenUpdateWithToken:token expiryDate:expiryDate withCompletionHandler:strongSelf.completionHandler]; + }; + [strongDelegate authenticationProvider:self acquireTokenWithCompletionHandler:self.completionHandler]; + } + } else { + MSACLogError([MSACAnalytics logTag], @"No completionhandler to acquire token has been set."); + } +} + +- (void)handleTokenUpdateWithToken:(NSString *)token + expiryDate:(NSDate *)expiryDate + withCompletionHandler:(MSACAnalyticsAuthenticationProviderCompletionBlock)completionHandler { + @synchronized(self) { + if (self.completionHandler == completionHandler) { + self.completionHandler = nil; + MSACLogDebug([MSACAnalytics logTag], @"Got result back from MSAcquireTokenCompletionBlock."); + if (!token) { + MSACLogError([MSACAnalytics logTag], @"Token must not be null"); + return; + } + if (!expiryDate) { + MSACLogError([MSACAnalytics logTag], @"Date must not be null"); + return; + } + NSString *tokenPrefix; + switch (self.type) { + case MSACAnalyticsAuthenticationTypeMsaCompact: + tokenPrefix = @"p"; + break; + case MSACAnalyticsAuthenticationTypeMsaDelegate: + tokenPrefix = @"d"; + break; + } + [[MSACTicketCache sharedInstance] setTicket:[NSString stringWithFormat:@"%@:%@", tokenPrefix, token] forKey:self.ticketKeyHash]; + self.expiryDate = expiryDate; + } + } +} + +- (void)checkTokenExpiry { + @synchronized(self) { + if (self.expiryDate && + (long long)[self.expiryDate timeIntervalSince1970] <= ((long long)[[NSDate date] timeIntervalSince1970] + kMSRefreshThreshold)) { + [self acquireTokenAsync]; + } + } +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/TransmissionTarget/MSACAnalyticsAuthenticationProviderDelegate.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/TransmissionTarget/MSACAnalyticsAuthenticationProviderDelegate.h new file mode 100644 index 0000000000..9f7be7c65c --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/TransmissionTarget/MSACAnalyticsAuthenticationProviderDelegate.h @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +@class MSACAnalyticsAuthenticationProvider; + +/** + * Completion handler that returns the authentication token and the expiry date. + */ +typedef void (^MSACAnalyticsAuthenticationProviderCompletionBlock)(NSString *token, NSDate *expiryDate) + NS_SWIFT_NAME(AnalyticsAuthenticationProviderCompletionBlock); + +NS_SWIFT_NAME(AnalyticsAuthenticationProviderDelegate) +@protocol MSACAnalyticsAuthenticationProviderDelegate + +/** + * Required method that needs to be called from within your authentication flow to provide the authentication token and expiry date. + * + * @param authenticationProvider The authentication provider. + * @param completionHandler The completion handler. + */ +- (void)authenticationProvider:(MSACAnalyticsAuthenticationProvider *)authenticationProvider + acquireTokenWithCompletionHandler:(MSACAnalyticsAuthenticationProviderCompletionBlock)completionHandler; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/TransmissionTarget/MSACAnalyticsAuthenticationProviderInternal.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/TransmissionTarget/MSACAnalyticsAuthenticationProviderInternal.h new file mode 100644 index 0000000000..f6b9ab06eb --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/TransmissionTarget/MSACAnalyticsAuthenticationProviderInternal.h @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAnalyticsAuthenticationProvider.h" + +@interface MSACAnalyticsAuthenticationProvider () + +@property(nonatomic) signed char isAlreadyAcquiringToken; + +@property(nonatomic, strong) NSDate *expiryDate; + +/** + * Request a new token from the app. + */ +- (void)acquireTokenAsync; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/TransmissionTarget/MSACAnalyticsTransmissionTarget.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/TransmissionTarget/MSACAnalyticsTransmissionTarget.h new file mode 100644 index 0000000000..3d47515fde --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/TransmissionTarget/MSACAnalyticsTransmissionTarget.h @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAnalyticsAuthenticationProvider.h" +#import "MSACConstants+Flags.h" +#import "MSACPropertyConfigurator.h" + +@class MSACEventProperties; + +NS_ASSUME_NONNULL_BEGIN + +NS_SWIFT_NAME(AnalyticsTransmissionTarget) +@interface MSACAnalyticsTransmissionTarget : NSObject + +/** + * Property configurator. + */ +@property(nonatomic, readonly, strong) MSACPropertyConfigurator *propertyConfigurator; + ++ (void)addAuthenticationProvider:(MSACAnalyticsAuthenticationProvider *)authenticationProvider + NS_SWIFT_NAME(addAuthenticationProvider(authenticationProvider:)); + +/** + * Track an event. + * + * @param eventName event name. + */ +- (void)trackEvent:(NSString *)eventName; + +/** + * Track an event. + * + * @param eventName event name. + * @param properties dictionary of properties. + */ +- (void)trackEvent:(NSString *)eventName withProperties:(nullable NSDictionary *)properties; + +/** + * Track an event. + * + * @param eventName event name. + * @param properties dictionary of properties. + * @param flags Optional flags. Events tracked with the MSACFlagsCritical flag will take precedence over all other events in + * storage. An event tracked with this option will only be dropped if storage must make room for a newer event that is also marked with the + * MSACFlagsCritical flag. + */ +- (void)trackEvent:(NSString *)eventName withProperties:(nullable NSDictionary *)properties flags:(MSACFlags)flags; + +/** + * Track a custom event with name and optional typed properties. + * + * @param eventName Event name. + * @param properties Typed properties. + * + * @discussion The following validation rules are applied: + * + * The name cannot be null or empty. + * + * The property names or values cannot be null. + * + * Double values must be finite (NaN or Infinite values are discarded). + * + * Additional validation rules apply depending on the configured secret. + * + * - The event name needs to match the `[a-zA-Z0-9]((\.(?!(\.|$)))|[_a-zA-Z0-9]){3,99}` regular expression. + * + * - The `baseData` and `baseDataType` properties are reserved and thus discarded. + * + * - The full event size when encoded as a JSON string cannot be larger than 1.9MB. + */ +- (void)trackEvent:(NSString *)eventName + withTypedProperties:(nullable MSACEventProperties *)properties NS_SWIFT_NAME(trackEvent(_:withProperties:)); + +/** + * Track a custom event with name and optional typed properties. + * + * @param eventName Event name. + * @param properties Typed properties. + * @param flags Optional flags. Events tracked with the MSACFlagsCritical flag will take precedence over all other events in + * storage. An event tracked with this option will only be dropped if storage must make room for a newer event that is also marked with the + * MSACFlagsCritical flag. + * + * @discussion The following validation rules are applied: + * + * The name cannot be null or empty. + * + * The property names or values cannot be null. + * + * Double values must be finite (NaN or Infinite values are discarded). + * + * Additional validation rules apply depending on the configured secret. + * + * - The event name needs to match the `[a-zA-Z0-9]((\.(?!(\.|$)))|[_a-zA-Z0-9]){3,99}` regular expression. + * + * - The `baseData` and `baseDataType` properties are reserved and thus discarded. + * + * - The full event size when encoded as a JSON string cannot be larger than 1.9MB. + */ +- (void)trackEvent:(NSString *)eventName + withTypedProperties:(nullable MSACEventProperties *)properties + flags:(MSACFlags)flags NS_SWIFT_NAME(trackEvent(_:withProperties:flags:)); + +/** + * Get a nested transmission target. + * + * @param token The token of the transmission target to retrieve. + * + * @returns A transmission target object nested to this parent transmission target. + */ +- (MSACAnalyticsTransmissionTarget *)transmissionTargetForToken:(NSString *)token NS_SWIFT_NAME(transmissionTarget(forToken:)); + +/** + * The flag indicates whether or not this transmission target is enabled. Changing its state will also change states of nested transmission + * targets. + */ +@property(nonatomic, getter=isEnabled, setter=setEnabled:) BOOL enabled NS_SWIFT_NAME(enabled); + +/** + * Pause sending logs for the transmission target. It doesn't pause any of its decendants. + * + * @see resume + */ +- (void)pause; + +/** + * Resume sending logs for the transmission target. + * + * @see pause + */ +- (void)resume; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/TransmissionTarget/MSACAnalyticsTransmissionTarget.m b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/TransmissionTarget/MSACAnalyticsTransmissionTarget.m new file mode 100644 index 0000000000..dfae9cb3fe --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/TransmissionTarget/MSACAnalyticsTransmissionTarget.m @@ -0,0 +1,216 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAnalyticsAuthenticationProviderInternal.h" +#import "MSACAnalyticsInternal.h" +#import "MSACAnalyticsTransmissionTargetInternal.h" +#import "MSACAnalyticsTransmissionTargetPrivate.h" +#import "MSACCSExtensions.h" +#import "MSACCommonSchemaLog.h" +#import "MSACEventPropertiesInternal.h" +#import "MSACLogger.h" +#import "MSACPropertyConfiguratorInternal.h" +#import "MSACProtocolExtension.h" +#import "MSACServiceAbstractInternal.h" +#import "MSACUtility+StringFormatting.h" + +@implementation MSACAnalyticsTransmissionTarget + +static MSACAnalyticsAuthenticationProvider *_authenticationProvider; + +- (instancetype)initWithTransmissionTargetToken:(NSString *)token + parentTarget:(MSACAnalyticsTransmissionTarget *)parentTarget + channelGroup:(id)channelGroup { + if ((self = [super init])) { + _propertyConfigurator = [[MSACPropertyConfigurator alloc] initWithTransmissionTarget:self]; + _channelGroup = channelGroup; + _parentTarget = parentTarget; + _childTransmissionTargets = [NSMutableDictionary new]; + _transmissionTargetToken = token; + _isEnabledKey = + [NSString stringWithFormat:@"%@/%@", [MSACAnalytics sharedInstance].isEnabledKey, [MSACUtility targetKeyFromTargetToken:token]]; + + // Disable if ancestor is disabled. + if (![self isImmediateParent]) { + [MSAC_APP_CENTER_USER_DEFAULTS setObject:@NO forKey:self.isEnabledKey]; + } + + // Add property configurator to the channel group as a delegate. + [_channelGroup addDelegate:_propertyConfigurator]; + + // Add self to channel group as delegate to decorate logs with tickets. + [_channelGroup addDelegate:self]; + } + return self; +} + ++ (void)addAuthenticationProvider:(MSACAnalyticsAuthenticationProvider *)authenticationProvider { + @synchronized(self) { + if (!authenticationProvider) { + MSACLogError([MSACAnalytics logTag], @"Authentication provider may not be null."); + return; + } + + // No need to validate the authentication provider's properties as they are required for initialization and can't be null. + self.authenticationProvider = authenticationProvider; + + // Request token now. + [self.authenticationProvider acquireTokenAsync]; + } +} + +- (void)trackEvent:(NSString *)eventName { + [self trackEvent:eventName withProperties:nil]; +} + +- (void)trackEvent:(NSString *)eventName withProperties:(nullable NSDictionary *)properties { + [self trackEvent:eventName withProperties:properties flags:MSACFlagsDefault]; +} + +- (void)trackEvent:(NSString *)eventName withProperties:(nullable NSDictionary *)properties flags:(MSACFlags)flags { + MSACEventProperties *eventProperties; + if (properties) { + eventProperties = [MSACEventProperties new]; + for (NSString *key in properties.allKeys) { + NSString *value = properties[key]; + [eventProperties setString:value forKey:key]; + } + } + [self trackEvent:eventName withTypedProperties:eventProperties flags:flags]; +} + +- (void)trackEvent:(NSString *)eventName withTypedProperties:(nullable MSACEventProperties *)properties { + [self trackEvent:eventName withTypedProperties:properties flags:MSACFlagsDefault]; +} + +- (void)trackEvent:(NSString *)eventName withTypedProperties:(nullable MSACEventProperties *)properties flags:(MSACFlags)flags { + MSACEventProperties *mergedProperties = [MSACEventProperties new]; + + // Merge properties in its ancestors. + MSACAnalyticsTransmissionTarget *target = self; + while (target != nil) { + [target.propertyConfigurator mergeTypedPropertiesWith:mergedProperties]; + target = target.parentTarget; + } + + // Override properties. + if (properties) { + [mergedProperties mergeEventProperties:(MSACEventProperties * __nonnull) properties]; + } else if ([mergedProperties isEmpty]) { + + // Set nil for the properties to pass nil to trackEvent. + mergedProperties = nil; + } + [MSACAnalytics trackEvent:eventName withTypedProperties:mergedProperties forTransmissionTarget:self flags:flags]; +} + +- (MSACAnalyticsTransmissionTarget *)transmissionTargetForToken:(NSString *)token { + + // Look up for the token in the dictionary, create a new transmission target if doesn't exist. + MSACAnalyticsTransmissionTarget *target = self.childTransmissionTargets[token]; + if (!target) { + target = [[MSACAnalyticsTransmissionTarget alloc] initWithTransmissionTargetToken:token + parentTarget:self + channelGroup:self.channelGroup]; + self.childTransmissionTargets[token] = target; + } + return target; +} + +- (BOOL)isEnabled { + @synchronized([MSACAnalytics sharedInstance]) { + + // Get isEnabled value from persistence. No need to cache the value in a property, user settings already have their cache mechanism. + NSNumber *isEnabledNumber = [MSAC_APP_CENTER_USER_DEFAULTS objectForKey:self.isEnabledKey]; + + // Return the persisted value otherwise it's enabled by default. + return (isEnabledNumber) ? [isEnabledNumber boolValue] : YES; + } +} + +- (void)setEnabled:(BOOL)isEnabled { + @synchronized([MSACAnalytics sharedInstance]) { + if (self.isEnabled != isEnabled) { + + // Don't enable if the immediate parent is disabled. + if (isEnabled && ![self isImmediateParent]) { + MSACLogWarning([MSACAnalytics logTag], @"Can't enable; parent transmission " + @"target and/or Analytics service " + @"is disabled."); + return; + } + + // Persist the enabled status. + [MSAC_APP_CENTER_USER_DEFAULTS setObject:@(isEnabled) forKey:self.isEnabledKey]; + + if (isEnabled) { + + // Resume the target on enable + [self resume]; + } + } + + // Propagate to nested transmission targets. + for (NSString *token in self.childTransmissionTargets) { + [self.childTransmissionTargets[token] setEnabled:isEnabled]; + } + } +} + +- (void)pause { + if (self.isEnabled) { + [MSACAnalytics pauseTransmissionTargetForToken:self.transmissionTargetToken]; + } else { + MSACLogError([MSACAnalytics logTag], @"This transmission target is disabled."); + } +} + +- (void)resume { + if (self.isEnabled) { + [MSACAnalytics resumeTransmissionTargetForToken:self.transmissionTargetToken]; + } else { + MSACLogError([MSACAnalytics logTag], @"This transmission target is disabled."); + } +} + +#pragma mark - ChannelDelegate callbacks + +- (void)channel:(id)__unused channel prepareLog:(id)log { + + // Only set ticketKey for owned target. Not strictly necessary but this avoids setting the ticketKeyHash multiple times for a log. + if (![log.transmissionTargetTokens containsObject:self.transmissionTargetToken]) { + return; + } + if ([log isKindOfClass:[MSACCommonSchemaLog class]] && [self isEnabled]) { + if (MSACAnalyticsTransmissionTarget.authenticationProvider) { + NSString *ticketKeyHash = MSACAnalyticsTransmissionTarget.authenticationProvider.ticketKeyHash; + ((MSACCommonSchemaLog *)log).ext.protocolExt.ticketKeys = @[ ticketKeyHash ]; + [MSACAnalyticsTransmissionTarget.authenticationProvider checkTokenExpiry]; + } + } +} + +#pragma mark - Private methods + ++ (MSACAnalyticsAuthenticationProvider *)authenticationProvider { + @synchronized(self) { + return _authenticationProvider; + } +} + ++ (void)setAuthenticationProvider:(MSACAnalyticsAuthenticationProvider *)authenticationProvider { + @synchronized(self) { + _authenticationProvider = authenticationProvider; + } +} + +/** + * Check ancestor enabled state, the ancestor is either the immediate target parent if there is one or Analytics. + * + * @return YES if the immediate ancestor is enabled. + */ +- (BOOL)isImmediateParent { + return self.parentTarget ? self.parentTarget.isEnabled : [MSACAnalytics isEnabled]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/TransmissionTarget/MSACPropertyConfigurator.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/TransmissionTarget/MSACPropertyConfigurator.h new file mode 100644 index 0000000000..18da67261d --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/TransmissionTarget/MSACPropertyConfigurator.h @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +NS_ASSUME_NONNULL_BEGIN + +NS_SWIFT_NAME(PropertyConfigurator) +@interface MSACPropertyConfigurator : NSObject + +/** + * Override the application version. + * + */ +@property(nonatomic, copy) NSString *_Nullable appVersion; + +/** + * Override the application name. + * + */ +@property(nonatomic, copy) NSString *_Nullable appName; + +/** + * Override the application locale. + * + */ +@property(nonatomic, copy) NSString *_Nullable appLocale; + +/** + * User identifier. + * The identifier needs to start with c: or i: or d: or w: prefixes. + * + */ +@property(nonatomic, copy) NSString *_Nullable userId; + +/** + * Set a string event property to be attached to events tracked by this transmission target and its child transmission targets. + * + * @param propertyValue Property value. + * @param propertyKey Property key. + * + * @discussion A property set in a child transmission target overrides a property with the same key inherited from its parents. Also, the + * properties passed to the `trackEvent:withProperties:` or `trackEvent:withTypedProperties:` override any property with the same key from + * the transmission target itself or its parents. + */ +- (void)setEventPropertyString:(NSString *)propertyValue forKey:(NSString *)propertyKey NS_SWIFT_NAME(setEventProperty(_:forKey:)); + +/** + * Set a double event property to be attached to events tracked by this transmission target and its child transmission targets. + * + * @param propertyValue Property value. Must be finite (`NAN` and `INFINITY` not allowed). + * @param propertyKey Property key. + * + * @discussion A property set in a child transmission target overrides a property with the same key inherited from its parents. Also, the + * properties passed to the `trackEvent:withProperties:` or `trackEvent:withTypedProperties:` override any property with the same key from + * the transmission target itself or its parents. + */ +- (void)setEventPropertyDouble:(double)propertyValue forKey:(NSString *)propertyKey NS_SWIFT_NAME(setEventProperty(_:forKey:)); + +/** + * Set a 64-bit integer event property to be attached to events tracked by this transmission target and its child transmission targets. + * + * @param propertyValue Property value. + * @param propertyKey Property key. + * + * @discussion A property set in a child transmission target overrides a property with the same key inherited from its parents. Also, the + * properties passed to the `trackEvent:withProperties:` or `trackEvent:withTypedProperties:` override any property with the same key from + * the transmission target itself or its parents. + */ +- (void)setEventPropertyInt64:(int64_t)propertyValue forKey:(NSString *)propertyKey NS_SWIFT_NAME(setEventProperty(_:forKey:)); + +/** + * Set a boolean event property to be attached to events tracked by this transmission target and its child transmission targets. + * + * @param propertyValue Property value. + * @param propertyKey Property key. + * + * @discussion A property set in a child transmission target overrides a property with the same key inherited from its parents. Also, the + * properties passed to the `trackEvent:withProperties:` or `trackEvent:withTypedProperties:` override any property with the same key from + * the transmission target itself or its parents. + */ +- (void)setEventPropertyBool:(BOOL)propertyValue forKey:(NSString *)propertyKey NS_SWIFT_NAME(setEventProperty(_:forKey:)); + +/** + * Set a date event property to be attached to events tracked by this transmission target and its child transmission targets. + * + * @param propertyValue Property value. + * @param propertyKey Property key. + * + * @discussion A property set in a child transmission target overrides a property with the same key inherited from its parents. Also, the + * properties passed to the `trackEvent:withProperties:` or `trackEvent:withTypedProperties:` override any property with the same key from + * the transmission target itself or its parents. + */ +- (void)setEventPropertyDate:(NSDate *)propertyValue forKey:(NSString *)propertyKey NS_SWIFT_NAME(setEventProperty(_:forKey:)); + +/** + * Remove an event property from this transmission target. + * + * @param propertyKey Property key. + * + * @discussion This won't remove properties with the same name declared in other nested transmission targets. + */ +- (void)removeEventPropertyForKey:(NSString *)propertyKey NS_SWIFT_NAME(removeEventProperty(forKey:)); + +/** + * Once called, the App Center SDK will automatically add UIDevice.identifierForVendor to common schema logs. + * + * @discussion Call this before starting the SDK. This setting is not persisted, so you need to call this when setting up the SDK every + * time. If you want to provide a way for users to opt-in or opt-out of this setting, it is on you to persist their choice and configure the + * App Center SDK accordingly. + */ +- (void)collectDeviceId; + +NS_ASSUME_NONNULL_END + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/TransmissionTarget/MSACPropertyConfigurator.m b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/TransmissionTarget/MSACPropertyConfigurator.m new file mode 100644 index 0000000000..7e93cb406b --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/TransmissionTarget/MSACPropertyConfigurator.m @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#if TARGET_OS_OSX +#import +#else +#import +#endif + +#import "MSACAnalyticsInternal.h" +#import "MSACAnalyticsTransmissionTargetInternal.h" +#import "MSACAnalyticsTransmissionTargetPrivate.h" +#import "MSACAppExtension.h" +#import "MSACCSExtensions.h" +#import "MSACCommonSchemaLog.h" +#import "MSACConstants+Internal.h" +#import "MSACDeviceExtension.h" +#import "MSACEventPropertiesInternal.h" +#import "MSACLogger.h" +#import "MSACPropertyConfiguratorPrivate.h" +#import "MSACStringTypedProperty.h" +#import "MSACUserExtension.h" +#import "MSACUserIdContext.h" + +@implementation MSACPropertyConfigurator + +#if TARGET_OS_OSX +static const char deviceIdPrefix = 'u'; +#else +static const char deviceIdPrefix = 'i'; +#endif + +- (instancetype)initWithTransmissionTarget:(MSACAnalyticsTransmissionTarget *)transmissionTarget { + if ((self = [super init])) { + _transmissionTarget = transmissionTarget; + _eventProperties = [MSACEventProperties new]; + } + return self; +} + +- (void)setAppVersion:(NSString *)appVersion { + _appVersion = appVersion; +} + +- (void)setAppName:(NSString *)appName { + _appName = appName; +} + +- (void)setAppLocale:(NSString *)appLocale { + _appLocale = appLocale; +} + +- (void)setUserId:(NSString *)userId { + if ([MSACUserIdContext isUserIdValidForOneCollector:userId]) { + NSString *prefixedUserId = [MSACUserIdContext prefixedUserIdFromUserId:userId]; + _userId = prefixedUserId; + } +} + +- (void)setEventPropertyString:(NSString *)propertyValue forKey:(NSString *)propertyKey { + @synchronized([MSACAnalytics sharedInstance]) { + [self.eventProperties setString:propertyValue forKey:propertyKey]; + } +} + +- (void)setEventPropertyDouble:(double)propertyValue forKey:(NSString *)propertyKey { + @synchronized([MSACAnalytics sharedInstance]) { + [self.eventProperties setDouble:propertyValue forKey:propertyKey]; + } +} + +- (void)setEventPropertyInt64:(int64_t)propertyValue forKey:(NSString *)propertyKey { + @synchronized([MSACAnalytics sharedInstance]) { + [self.eventProperties setInt64:propertyValue forKey:propertyKey]; + } +} + +- (void)setEventPropertyBool:(BOOL)propertyValue forKey:(NSString *)propertyKey { + @synchronized([MSACAnalytics sharedInstance]) { + [self.eventProperties setBool:propertyValue forKey:propertyKey]; + } +} + +- (void)setEventPropertyDate:(NSDate *)propertyValue forKey:(NSString *)propertyKey { + @synchronized([MSACAnalytics sharedInstance]) { + [self.eventProperties setDate:propertyValue forKey:propertyKey]; + } +} + +- (void)removeEventPropertyForKey:(NSString *)propertyKey { + @synchronized([MSACAnalytics sharedInstance]) { + if (!propertyKey) { + MSACLogError([MSACAnalytics logTag], @"Event property key to remove cannot be nil."); + return; + } + [self.eventProperties.properties removeObjectForKey:propertyKey]; + } +} + +- (void)collectDeviceId { + self.deviceId = [MSACPropertyConfigurator getDeviceIdentifier]; +} + +#pragma mark - MSACChannelDelegate + +- (void)channel:(id)__unused channel prepareLog:(id)log { + MSACAnalyticsTransmissionTarget *target = self.transmissionTarget; + if (target && [log isKindOfClass:[MSACCommonSchemaLog class]] && target.enabled && [log.tag isEqual:target]) { + + // Override the application version. + while (target) { + if (target.propertyConfigurator.appVersion) { + ((MSACCommonSchemaLog *)log).ext.appExt.ver = target.propertyConfigurator.appVersion; + break; + } + target = target.parentTarget; + } + + // Override the application name. + target = self.transmissionTarget; + while (target) { + if (target.propertyConfigurator.appName) { + ((MSACCommonSchemaLog *)log).ext.appExt.name = target.propertyConfigurator.appName; + break; + } + target = target.parentTarget; + } + + // Override the application locale. + target = self.transmissionTarget; + while (target) { + if (target.propertyConfigurator.appLocale) { + ((MSACCommonSchemaLog *)log).ext.appExt.locale = target.propertyConfigurator.appLocale; + break; + } + target = target.parentTarget; + } + + // Override the userId. + target = self.transmissionTarget; + while (target) { + if (target.propertyConfigurator.userId) { + ((MSACCommonSchemaLog *)log).ext.userExt.localId = target.propertyConfigurator.userId; + break; + } + target = target.parentTarget; + } + + // The device ID must not be inherited from parent transmission targets. + [((MSACCommonSchemaLog *)log) ext].deviceExt.localId = self.deviceId; + } +} + +#pragma mark - Helper methods + ++ (NSString *)getDeviceIdentifier { + NSString *baseIdentifier; +#if TARGET_OS_OSX + + io_service_t platformExpert = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice")); + CFStringRef platformUUIDAsCFString = NULL; + if (platformExpert) { + platformUUIDAsCFString = + (CFStringRef)IORegistryEntryCreateCFProperty(platformExpert, CFSTR(kIOPlatformUUIDKey), kCFAllocatorDefault, 0); + IOObjectRelease(platformExpert); + } + NSString *platformUUIDAsNSString = nil; + if (platformUUIDAsCFString) { + platformUUIDAsNSString = [NSString stringWithString:(__bridge NSString *)platformUUIDAsCFString]; + CFRelease(platformUUIDAsCFString); + } + baseIdentifier = platformUUIDAsNSString; + +#else + baseIdentifier = [[[UIDevice currentDevice] identifierForVendor] UUIDString]; +#endif + return [NSString stringWithFormat:@"%c:%@", deviceIdPrefix, baseIdentifier]; +} + +- (void)mergeTypedPropertiesWith:(MSACEventProperties *)mergedEventProperties { + @synchronized([MSACAnalytics sharedInstance]) { + for (NSString *key in self.eventProperties.properties) { + if (!mergedEventProperties.properties[key]) { + mergedEventProperties.properties[key] = self.eventProperties.properties[key]; + } + } + } +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/TransmissionTarget/MSACPropertyConfiguratorInternal.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/TransmissionTarget/MSACPropertyConfiguratorInternal.h new file mode 100644 index 0000000000..da2dea694e --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/TransmissionTarget/MSACPropertyConfiguratorInternal.h @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACChannelDelegate.h" +#import "MSACEventPropertiesInternal.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface MSACPropertyConfigurator () + +/** + * Initialize property configurator with a transmission target. + */ +- (instancetype)initWithTransmissionTarget:(MSACAnalyticsTransmissionTarget *)transmissionTarget; + +/** + * Merge typed properties. + * + * @param mergedEventProperties The destination event properties that merges current event properties to. + */ +- (void)mergeTypedPropertiesWith:(MSACEventProperties *)mergedEventProperties; + +NS_ASSUME_NONNULL_END + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/AppCenterAnalytics.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/AppCenterAnalytics.h new file mode 120000 index 0000000000..383607ed6f --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/AppCenterAnalytics.h @@ -0,0 +1 @@ +../AppCenterAnalytics.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACAbstractLog.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACAbstractLog.h new file mode 120000 index 0000000000..8abdb4eaf8 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACAbstractLog.h @@ -0,0 +1 @@ +../../../AppCenter/AppCenter/Model/MSACAbstractLog.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACAnalytics.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACAnalytics.h new file mode 120000 index 0000000000..f2f3c2db09 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACAnalytics.h @@ -0,0 +1 @@ +../MSACAnalytics.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACAnalyticsAuthenticationProvider.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACAnalyticsAuthenticationProvider.h new file mode 120000 index 0000000000..35853a43ff --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACAnalyticsAuthenticationProvider.h @@ -0,0 +1 @@ +../TransmissionTarget/MSACAnalyticsAuthenticationProvider.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACAnalyticsAuthenticationProviderDelegate.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACAnalyticsAuthenticationProviderDelegate.h new file mode 120000 index 0000000000..a44cd9f3a0 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACAnalyticsAuthenticationProviderDelegate.h @@ -0,0 +1 @@ +../TransmissionTarget/MSACAnalyticsAuthenticationProviderDelegate.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACAnalyticsTransmissionTarget.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACAnalyticsTransmissionTarget.h new file mode 120000 index 0000000000..3e4173eb97 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACAnalyticsTransmissionTarget.h @@ -0,0 +1 @@ +../TransmissionTarget/MSACAnalyticsTransmissionTarget.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACConstants+Flags.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACConstants+Flags.h new file mode 120000 index 0000000000..29731caa3f --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACConstants+Flags.h @@ -0,0 +1 @@ +../../../AppCenter/AppCenter/MSACConstants+Flags.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACEventLog.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACEventLog.h new file mode 120000 index 0000000000..d9b302d28d --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACEventLog.h @@ -0,0 +1 @@ +../Model/MSACEventLog.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACEventProperties.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACEventProperties.h new file mode 120000 index 0000000000..3f0c4125c5 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACEventProperties.h @@ -0,0 +1 @@ +../Model/MSACEventProperties.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACLogWithNameAndProperties.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACLogWithNameAndProperties.h new file mode 120000 index 0000000000..97d28ad94c --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACLogWithNameAndProperties.h @@ -0,0 +1 @@ +../Model/MSACLogWithNameAndProperties.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACLogWithProperties.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACLogWithProperties.h new file mode 120000 index 0000000000..5d9badcfa8 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACLogWithProperties.h @@ -0,0 +1 @@ +../../../AppCenter/AppCenter/Model/MSACLogWithProperties.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACPropertyConfigurator.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACPropertyConfigurator.h new file mode 120000 index 0000000000..c2061be492 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACPropertyConfigurator.h @@ -0,0 +1 @@ +../TransmissionTarget/MSACPropertyConfigurator.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACService.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACService.h new file mode 120000 index 0000000000..b6ea0006fb --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACService.h @@ -0,0 +1 @@ +../../../AppCenter/AppCenter/MSACService.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACServiceAbstract.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACServiceAbstract.h new file mode 120000 index 0000000000..0f8d778f1e --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalytics/include/MSACServiceAbstract.h @@ -0,0 +1 @@ +../../../AppCenter/AppCenter/MSACServiceAbstract.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/Info.plist b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/Info.plist new file mode 100644 index 0000000000..ba72822e87 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/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 + 1 + + diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACAnalyticsAuthenticationProviderTests.m b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACAnalyticsAuthenticationProviderTests.m new file mode 100644 index 0000000000..05d1d9c17f --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACAnalyticsAuthenticationProviderTests.m @@ -0,0 +1,244 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAnalyticsAuthenticationProviderInternal.h" +#import "MSACTestFrameworks.h" +#import "MSACTicketCache.h" +#import "MSACUtility+StringFormatting.h" + +@interface MSACAnalyticsAuthenticationProviderTests : XCTestCase + +@property(nonatomic) MSACAnalyticsAuthenticationProvider *sut; + +@property(nonatomic) NSDate *today; + +@property(nonatomic) NSString *ticketKey; + +@property(nonatomic) NSString *token; + +@end + +@implementation MSACAnalyticsAuthenticationProviderTests + +- (void)setUp { + [super setUp]; + + self.today = [NSDate date]; + self.ticketKey = @"ticketKey1"; + self.token = @"authenticationToken"; + id mockDelegate = OCMProtocolMock(@protocol(MSACAnalyticsAuthenticationProviderDelegate)); + OCMStub([mockDelegate authenticationProvider:OCMOCK_ANY + acquireTokenWithCompletionHandler:([OCMArg invokeBlockWithArgs:self.token, self.today, nil])]); + self.sut = [[MSACAnalyticsAuthenticationProvider alloc] initWithAuthenticationType:MSACAnalyticsAuthenticationTypeMsaDelegate + ticketKey:self.ticketKey + delegate:mockDelegate]; + self.sut = [self createAuthenticationProviderWithTicketKey:self.ticketKey delegate:mockDelegate]; +} + +- (void)tearDown { + [super tearDown]; + + self.sut = nil; + [[MSACTicketCache sharedInstance] clearCache]; +} + +- (MSACAnalyticsAuthenticationProvider *)createAuthenticationProviderWithTicketKey:(NSString *)ticketKey + delegate: + (id)delegate { + + return [[MSACAnalyticsAuthenticationProvider alloc] initWithAuthenticationType:MSACAnalyticsAuthenticationTypeMsaCompact + ticketKey:ticketKey + delegate:delegate]; +} + +- (void)testInitialization { + + // Then + XCTAssertNotNil(self.sut); + XCTAssertEqual(self.sut.type, MSACAnalyticsAuthenticationTypeMsaCompact); + XCTAssertNotNil(self.sut.ticketKey); + XCTAssertNotNil(self.sut.ticketKeyHash); + XCTAssertTrue([self.sut.ticketKeyHash isEqualToString:[MSACUtility sha256:@"ticketKey1"]]); + XCTAssertNotNil(self.sut.delegate); +} + +- (void)testExpiryDateIsValid { + + // If + id mockDelegate = OCMProtocolMock(@protocol(MSACAnalyticsAuthenticationProviderDelegate)); + NSTimeInterval plusDay = (24 * 60 * 60); + OCMStub([mockDelegate + authenticationProvider:OCMOCK_ANY + acquireTokenWithCompletionHandler:([OCMArg invokeBlockWithArgs:self.token, [self.today dateByAddingTimeInterval:plusDay], nil])]); + self.sut = [self createAuthenticationProviderWithTicketKey:self.ticketKey delegate:mockDelegate]; + id sutMock = OCMPartialMock(self.sut); + + // When + XCTestExpectation *expectation = [self expectationWithDescription:@"Expiry date is valid"]; + [self.sut acquireTokenAsync]; + dispatch_async(dispatch_get_main_queue(), ^{ + [expectation fulfill]; + }); + + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + + // Then + OCMReject([sutMock acquireTokenAsync]); + [self.sut checkTokenExpiry]; + OCMVerifyAll(sutMock); + }]; +} + +- (void)testExpiryDateIsExpired { + + // If + id mockDelegate = OCMProtocolMock(@protocol(MSACAnalyticsAuthenticationProviderDelegate)); + NSTimeInterval minusDay = -(24 * 60 * 60); + OCMStub([mockDelegate + authenticationProvider:OCMOCK_ANY + acquireTokenWithCompletionHandler:([OCMArg invokeBlockWithArgs:self.token, [self.today dateByAddingTimeInterval:minusDay], nil])]); + self.sut = [self createAuthenticationProviderWithTicketKey:self.ticketKey delegate:mockDelegate]; + id sutMock = OCMPartialMock(self.sut); + + // When + XCTestExpectation *expectation = [self expectationWithDescription:@"Expiry date is expired"]; + [self.sut acquireTokenAsync]; + dispatch_async(dispatch_get_main_queue(), ^{ + [expectation fulfill]; + }); + + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + + // Then + [self.sut checkTokenExpiry]; + OCMVerify([sutMock acquireTokenAsync]); + }]; +} + +- (void)testCompletionHandlerIsCalled { + + // If + id mockDelegate = OCMProtocolMock(@protocol(MSACAnalyticsAuthenticationProviderDelegate)); + OCMStub([mockDelegate authenticationProvider:OCMOCK_ANY + acquireTokenWithCompletionHandler:([OCMArg invokeBlockWithArgs:self.token, self.today, nil])]); + self.sut = [[MSACAnalyticsAuthenticationProvider alloc] initWithAuthenticationType:MSACAnalyticsAuthenticationTypeMsaCompact + ticketKey:self.ticketKey + delegate:mockDelegate]; + + // When + XCTestExpectation *expectation = [self expectationWithDescription:@"Completion handler is called"]; + [self.sut acquireTokenAsync]; + dispatch_async(dispatch_get_main_queue(), ^{ + [expectation fulfill]; + }); + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + + // Then + XCTAssertTrue([self.sut.expiryDate isEqualToDate:self.today]); + NSString *savedToken = [[MSACTicketCache sharedInstance] ticketFor:self.sut.ticketKeyHash]; + NSString *tokenWithPrefixString = [NSString stringWithFormat:@"p:%@", self.token]; + XCTAssertTrue([savedToken isEqualToString:tokenWithPrefixString]); + }]; +} + +- (void)testCompletionHandlerIsCalledForMSADelegateType { + + // If + id mockDelegate = OCMProtocolMock(@protocol(MSACAnalyticsAuthenticationProviderDelegate)); + OCMStub([mockDelegate authenticationProvider:OCMOCK_ANY + acquireTokenWithCompletionHandler:([OCMArg invokeBlockWithArgs:self.token, self.today, nil])]); + self.sut = [[MSACAnalyticsAuthenticationProvider alloc] initWithAuthenticationType:MSACAnalyticsAuthenticationTypeMsaDelegate + ticketKey:self.ticketKey + delegate:mockDelegate]; + + // When + XCTestExpectation *expectation = [self expectationWithDescription:@"Completion handler is called"]; + [self.sut acquireTokenAsync]; + dispatch_async(dispatch_get_main_queue(), ^{ + [expectation fulfill]; + }); + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + + // Then + XCTAssertTrue([self.sut.expiryDate isEqualToDate:self.today]); + NSString *savedToken = [[MSACTicketCache sharedInstance] ticketFor:self.sut.ticketKeyHash]; + NSString *tokenWithPrefixString = [NSString stringWithFormat:@"d:%@", self.token]; + XCTAssertTrue([savedToken isEqualToString:tokenWithPrefixString]); + }]; +} + +- (void)testDelegateReturnsNullToken { + + // If + id mockDelegate = OCMProtocolMock(@protocol(MSACAnalyticsAuthenticationProviderDelegate)); + OCMStub([mockDelegate authenticationProvider:OCMOCK_ANY + acquireTokenWithCompletionHandler:([OCMArg invokeBlockWithArgs:[NSNull null], self.today, nil])]); + self.sut = [[MSACAnalyticsAuthenticationProvider alloc] initWithAuthenticationType:MSACAnalyticsAuthenticationTypeMsaDelegate + ticketKey:self.ticketKey + delegate:mockDelegate]; + + // When + XCTestExpectation *expectation = [self expectationWithDescription:@"Completion handler is called"]; + [self.sut acquireTokenAsync]; + dispatch_async(dispatch_get_main_queue(), ^{ + [expectation fulfill]; + }); + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + + // Then + XCTAssertNil(self.sut.expiryDate); + NSString *savedToken = [[MSACTicketCache sharedInstance] ticketFor:self.sut.ticketKeyHash]; + XCTAssertNil(savedToken); + }]; +} + +- (void)testDelegateReturnsNullExpiryDate { + + // If + id mockDelegate = OCMProtocolMock(@protocol(MSACAnalyticsAuthenticationProviderDelegate)); + OCMStub([mockDelegate authenticationProvider:OCMOCK_ANY + acquireTokenWithCompletionHandler:([OCMArg invokeBlockWithArgs:self.token, [NSNull null], nil])]); + self.sut = [[MSACAnalyticsAuthenticationProvider alloc] initWithAuthenticationType:MSACAnalyticsAuthenticationTypeMsaDelegate + ticketKey:self.ticketKey + delegate:mockDelegate]; + + // When + XCTestExpectation *expectation = [self expectationWithDescription:@"Completion handler is called"]; + [self.sut acquireTokenAsync]; + dispatch_async(dispatch_get_main_queue(), ^{ + [expectation fulfill]; + }); + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + + // Then + XCTAssertNil(self.sut.expiryDate); + NSString *savedToken = [[MSACTicketCache sharedInstance] ticketFor:self.sut.ticketKeyHash]; + XCTAssertNil(savedToken); + }]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACAnalyticsTests.m b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACAnalyticsTests.m new file mode 100644 index 0000000000..6c42cf78ec --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACAnalyticsTests.m @@ -0,0 +1,1670 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAnalytics+Validation.h" +#import "MSACAnalyticsCategory.h" +#import "MSACAnalyticsPrivate.h" +#import "MSACAnalyticsTransmissionTargetPrivate.h" +#import "MSACAppCenter.h" +#import "MSACAppCenterInternal.h" +#import "MSACAppCenterPrivate.h" +#import "MSACAppCenterUserDefaultsPrivate.h" +#import "MSACBooleanTypedProperty.h" +#import "MSACChannelGroupDefault.h" +#import "MSACChannelUnitConfiguration.h" +#import "MSACChannelUnitDefault.h" +#import "MSACConstants+Internal.h" +#import "MSACDateTimeTypedProperty.h" +#import "MSACDoubleTypedProperty.h" +#import "MSACEventLog.h" +#import "MSACEventPropertiesInternal.h" +#import "MSACLongTypedProperty.h" +#import "MSACMockUserDefaults.h" +#import "MSACPageLog.h" +#import "MSACSessionContextPrivate.h" +#import "MSACSessionTrackerPrivate.h" +#import "MSACStringTypedProperty.h" +#import "MSACTestFrameworks.h" + +static NSString *const kMSACAnalyticsGroupId = @"Analytics"; +static NSString *const kMSACTypeEvent = @"event"; +static NSString *const kMSACTypePage = @"page"; +static NSString *const kMSACTestAppSecret = @"TestAppSecret"; +static NSString *const kMSACTestTransmissionToken = @"AnalyticsTestTransmissionToken"; +static NSString *const kMSACTestTransmissionToken2 = @"AnalyticsTestTransmissionToken2"; +static NSString *const kMSACAnalyticsServiceName = @"Analytics"; + +@class MSACMockAnalyticsDelegate; + +@interface MSACAnalyticsTests : XCTestCase + +@property(nonatomic) MSACMockUserDefaults *settingsMock; +@property(nonatomic) id sessionContextMock; +@property(nonatomic) id channelGroupMock; +@property(nonatomic) id channelUnitMock; +@property(nonatomic) id channelUnitCriticalMock; + +@end + +@interface MSACServiceAbstract () + +- (BOOL)isEnabled; + +- (void)setEnabled:(BOOL)enabled; + +@end + +@interface MSACAnalytics () + +- (BOOL)channelUnit:(id)channelUnit shouldFilterLog:(id)log; + +@end + +/* + * FIXME: Log manager mock is holding sessionTracker instance even after dealloc and this causes session tracker test failures. There is a + * PR in OCMock that seems a related issue. https://github.com/erikdoe/ocmock/pull/348 Stopping session tracker after applyEnabledState + * calls for hack to avoid failures. + */ +@implementation MSACAnalyticsTests + +- (void)setUp { + [super setUp]; + [MSACAppCenter resetSharedInstance]; + + // Mock NSUserDefaults. + self.settingsMock = [MSACMockUserDefaults new]; + + // Mock session context. + [MSACSessionContext resetSharedInstance]; + self.sessionContextMock = OCMClassMock([MSACSessionContext class]); + OCMStub([self.sessionContextMock sharedInstance]).andReturn(self.sessionContextMock); + + // Mock channel. + self.channelGroupMock = OCMClassMock([MSACChannelGroupDefault class]); + self.channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + self.channelUnitCriticalMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + [MSACAnalytics sharedInstance].criticalChannelUnit = self.channelUnitCriticalMock; + OCMStub([self.channelGroupMock alloc]).andReturn(self.channelGroupMock); + OCMStub([self.channelGroupMock initWithHttpClient:OCMOCK_ANY installId:OCMOCK_ANY logUrl:OCMOCK_ANY]).andReturn(self.channelGroupMock); + OCMStub([self.channelGroupMock addChannelUnitWithConfiguration:hasProperty(@"groupId", endsWith(kMSACCriticalChannelSuffix))]) + .andReturn(self.channelUnitCriticalMock); + OCMStub([self.channelGroupMock addChannelUnitWithConfiguration:hasProperty(@"groupId", equalTo(kMSACAnalyticsGroupId))]) + .andReturn(self.channelUnitMock); +} + +- (void)tearDown { + [MSACSessionContext resetSharedInstance]; + [MSACAnalytics resetSharedInstance]; + [self.settingsMock stopMocking]; + [self.sessionContextMock stopMocking]; + [super tearDown]; +} + +#pragma mark - Tests + +- (void)testMigrateOnInit { + NSString *key = [NSString stringWithFormat:kMSACMockMigrationKey, @"Analytics"]; + XCTAssertNotNil([self.settingsMock objectForKey:key]); +} + +- (void)testValidateEventName { + const int maxEventNameLength = 256; + + // If + NSString *validEventName = @"validEventName"; + NSString *shortEventName = @"e"; + NSString *eventName256 = [@"" stringByPaddingToLength:maxEventNameLength withString:@"eventName256" startingAtIndex:0]; + NSString *nullableEventName = nil; + NSString *emptyEventName = @""; + NSString *tooLongEventName = [@"" stringByPaddingToLength:(maxEventNameLength + 1) withString:@"tooLongEventName" startingAtIndex:0]; + + // When + NSString *valid = [[MSACAnalytics sharedInstance] validateEventName:validEventName forLogType:kMSACTypeEvent]; + NSString *validShortEventName = [[MSACAnalytics sharedInstance] validateEventName:shortEventName forLogType:kMSACTypeEvent]; + NSString *validEventName256 = [[MSACAnalytics sharedInstance] validateEventName:eventName256 forLogType:kMSACTypeEvent]; + NSString *validNullableEventName = [[MSACAnalytics sharedInstance] validateEventName:nullableEventName forLogType:kMSACTypeEvent]; + NSString *validEmptyEventName = [[MSACAnalytics sharedInstance] validateEventName:emptyEventName forLogType:kMSACTypeEvent]; + NSString *validTooLongEventName = [[MSACAnalytics sharedInstance] validateEventName:tooLongEventName forLogType:kMSACTypeEvent]; + + // Then + XCTAssertNotNil(valid); + XCTAssertNotNil(validShortEventName); + XCTAssertNotNil(validEventName256); + XCTAssertNil(validNullableEventName); + XCTAssertNil(validEmptyEventName); + XCTAssertNotNil(validTooLongEventName); + XCTAssertEqual([validTooLongEventName length], maxEventNameLength); +} + +- (void)testApplyEnabledStateWorks { + [[MSACAnalytics sharedInstance] startWithChannelGroup:OCMProtocolMock(@protocol(MSACChannelGroupProtocol)) + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + MSACServiceAbstract *service = [MSACAnalytics sharedInstance]; + + [service setEnabled:YES]; + XCTAssertTrue([service isEnabled]); + + [service setEnabled:NO]; + XCTAssertFalse([service isEnabled]); + + [service setEnabled:YES]; + XCTAssertTrue([service isEnabled]); + + // FIXME: logManager holds session tracker somehow and it causes other test failures. Stop it for hack. + [[MSACAnalytics sharedInstance].sessionTracker stop]; +} + +- (void)testSetTransmissionIntervalApplied { + + // If + NSUInteger testInterval = 5; + + // When + [MSACAnalytics setTransmissionInterval:testInterval]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // Then + OCMVerify( + [self.channelGroupMock addChannelUnitWithConfiguration:allOf(hasProperty(@"flushInterval", equalToUnsignedInteger(testInterval)), + hasProperty(@"groupId", equalTo(kMSACAnalyticsGroupId)), nil)]); + + // FIXME: logManager holds session tracker somehow and it causes other test failures. Stop it for hack. + [[MSACAnalytics sharedInstance].sessionTracker stop]; +} + +- (void)testSetTransmissionIntervalNotApplied { + + // If + NSUInteger testInterval = 2; + + // When + [MSACAnalytics setTransmissionInterval:testInterval]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // Then + OCMVerify([self.channelGroupMock addChannelUnitWithConfiguration:allOf(hasProperty(@"flushInterval", equalToUnsignedInteger(3)), + hasProperty(@"groupId", equalTo(kMSACAnalyticsGroupId)), nil)]); + + // FIXME: logManager holds session tracker somehow and it causes other test failures. Stop it for hack. + [[MSACAnalytics sharedInstance].sessionTracker stop]; +} + +- (void)testSetTransmissionIntervalNotAppliedIfHigherThanDay { + + // If + NSUInteger testInterval = 25 * 60 * 60; + + // When + [MSACAnalytics setTransmissionInterval:testInterval]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // Then + OCMVerify([self.channelGroupMock addChannelUnitWithConfiguration:allOf(hasProperty(@"flushInterval", equalToUnsignedInteger(3)), + hasProperty(@"groupId", equalTo(kMSACAnalyticsGroupId)), nil)]); + + // FIXME: logManager holds session tracker somehow and it causes other test failures. Stop it for hack. + [[MSACAnalytics sharedInstance].sessionTracker stop]; +} + +- (void)testSetTransmissionIntervalNotAppliedAfterStart { + + // If + NSUInteger testInterval = 5; + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + + // When + [[MSACAnalytics sharedInstance] startWithChannelGroup:channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // Make sure that interval is not set after service start. + [MSACAnalytics setTransmissionInterval:testInterval]; + + // Then + // FIXME: logManager holds session tracker somehow and it causes other test failures. Stop it for hack. + [[MSACAnalytics sharedInstance].sessionTracker stop]; + XCTAssertNotEqual([MSACAnalytics sharedInstance].flushInterval, testInterval); +} + +- (void)testDisablingAnalyticsClearsSessionHistory { + [[MSACAnalytics sharedInstance] startWithChannelGroup:OCMProtocolMock(@protocol(MSACChannelGroupProtocol)) + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + MSACServiceAbstract *service = [MSACAnalytics sharedInstance]; + + [service setEnabled:NO]; + XCTAssertFalse([service isEnabled]); + + OCMVerify([self.sessionContextMock clearSessionHistoryAndKeepCurrentSession:NO]); +} + +- (void)testTrackPageCalledWhenAutoPageTrackingEnabled { + + // If + id analyticsMock = OCMPartialMock([MSACAnalytics sharedInstance]); + id analyticsCategoryMock = OCMClassMock([MSACAnalyticsCategory class]); + NSString *testPageName = @"TestPage"; + OCMStub([analyticsCategoryMock missedPageViewName]).andReturn(testPageName); + [MSACAnalytics setAutoPageTrackingEnabled:YES]; + MSACServiceAbstract *service = [MSACAnalytics sharedInstance]; + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + + // When + [[MSACAnalytics sharedInstance] startWithChannelGroup:OCMProtocolMock(@protocol(MSACChannelGroupProtocol)) + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // FIXME: logManager holds session tracker somehow and it causes other test failures. Stop it for hack. + [[MSACAnalytics sharedInstance].sessionTracker stop]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for block in applyEnabledState to be dispatched"]; + dispatch_async(dispatch_get_main_queue(), ^{ + [expectation fulfill]; + }); + + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + + // Then + XCTAssertTrue([service isEnabled]); + OCMVerify([analyticsMock trackPage:testPageName withProperties:nil]); + }]; +} + +- (void)testSettingDelegateWorks { + id delegateMock = OCMProtocolMock(@protocol(MSACAnalyticsDelegate)); + [MSACAnalytics setDelegate:delegateMock]; + XCTAssertNotNil([MSACAnalytics sharedInstance].delegate); + XCTAssertEqual([MSACAnalytics sharedInstance].delegate, delegateMock); +} + +- (void)testAnalyticsDelegateWithoutImplementations { + + // If + [MSACAnalytics setDelegate:self]; + + // When + MSACEventLog *eventLog = [MSACEventLog new]; + [[MSACAnalytics sharedInstance] channel:self.channelUnitMock willSendLog:eventLog]; + [[MSACAnalytics sharedInstance] channel:self.channelUnitMock didSucceedSendingLog:eventLog]; + [[MSACAnalytics sharedInstance] channel:self.channelUnitMock didFailSendingLog:eventLog withError:nil]; + + // Then - no crashes +} + +- (void)testAnalyticsDelegateMethodsAreCalled { + + // If + id delegateMock = OCMProtocolMock(@protocol(MSACAnalyticsDelegate)); + [MSACAnalytics setDelegate:delegateMock]; + + // When + MSACEventLog *eventLog = [MSACEventLog new]; + [[MSACAnalytics sharedInstance] channel:self.channelUnitMock willSendLog:eventLog]; + [[MSACAnalytics sharedInstance] channel:self.channelUnitMock didSucceedSendingLog:eventLog]; + [[MSACAnalytics sharedInstance] channel:self.channelUnitMock didFailSendingLog:eventLog withError:nil]; + + // Then + OCMVerify([delegateMock analytics:[MSACAnalytics sharedInstance] willSendEventLog:eventLog]); + OCMVerify([delegateMock analytics:[MSACAnalytics sharedInstance] didSucceedSendingEventLog:eventLog]); + OCMVerify([delegateMock analytics:[MSACAnalytics sharedInstance] didFailSendingEventLog:eventLog withError:nil]); +} + +- (void)testAnalyticsLogsVerificationIsCalled { + + // If + MSACEventLog *eventLog = [MSACEventLog new]; + eventLog.name = @"test"; + eventLog.properties = @{@"test" : @"test"}; + MSACPageLog *pageLog = [MSACPageLog new]; + MSACLogWithNameAndProperties *analyticsLog = [MSACLogWithNameAndProperties new]; + id analyticsMock = OCMPartialMock([MSACAnalytics sharedInstance]); + OCMExpect([analyticsMock validateLog:eventLog]).andForwardToRealObject(); + OCMExpect([analyticsMock validateEventName:@"test" forLogType:@"event"]).andForwardToRealObject(); + OCMExpect([analyticsMock validateProperties:OCMOCK_ANY forLogName:@"test" andType:@"event"]).andForwardToRealObject(); + OCMExpect([analyticsMock validateLog:pageLog]).andForwardToRealObject(); + OCMExpect([analyticsMock validateEventName:OCMOCK_ANY forLogType:@"page"]).andForwardToRealObject(); + OCMReject([analyticsMock validateProperties:OCMOCK_ANY forLogName:OCMOCK_ANY andType:@"page"]); + OCMReject([analyticsMock validateLog:analyticsLog]); + + // When + [[MSACAnalytics sharedInstance] channelUnit:nil shouldFilterLog:eventLog]; + [[MSACAnalytics sharedInstance] channelUnit:nil shouldFilterLog:pageLog]; + [[MSACAnalytics sharedInstance] channelUnit:nil shouldFilterLog:analyticsLog]; + + // Then + OCMVerifyAll(analyticsMock); +} + +- (void)testAnalyticsLogsVerificationIsCalledWithWrongClass { + + // If + NSObject *notAnalyticsLog = [NSObject new]; + + // When + BOOL wrongClass = [MSACLogWithNameAndProperties isEqual:notAnalyticsLog]; + BOOL wrongType = [MSACLogWithNameAndProperties isEqual:@"invalid equal test"]; + + // Then + XCTAssertFalse(wrongClass); + XCTAssertFalse(wrongType); +} + +- (void)testTrackEventWithoutProperties { + + // If + __block NSString *name; + __block NSString *type; + NSString *expectedName = @"gotACoffee"; + OCMStub([self.channelUnitMock enqueueItem:[OCMArg isKindOfClass:[MSACLogWithProperties class]] flags:MSACFlagsDefault]) + .andDo(^(NSInvocation *invocation) { + MSACEventLog *log; + [invocation getArgument:&log atIndex:2]; + type = log.type; + name = log.name; + }); + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + [MSACAnalytics trackEvent:expectedName]; + + // Then + assertThat(type, is(kMSACTypeEvent)); + assertThat(name, is(expectedName)); +} + +- (void)testTrackEventWithPropertiesNilWhenAnalyticsDisabled { + + // If + id analyticsMock = OCMPartialMock([MSACAnalytics sharedInstance]); + OCMStub([analyticsMock isEnabled]).andReturn(NO); + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + OCMReject([self.channelUnitMock enqueueItem:OCMOCK_ANY flags:MSACFlagsDefault]); + [[MSACAnalytics sharedInstance] trackEvent:@"Some event" withProperties:nil forTransmissionTarget:nil flags:MSACFlagsDefault]; + + // Then + OCMVerifyAll(self.channelUnitMock); +} + +- (void)testTrackEventWithTypedPropertiesNilWhenAnalyticsDisabled { + + // If + id analyticsMock = OCMPartialMock([MSACAnalytics sharedInstance]); + OCMStub([analyticsMock isEnabled]).andReturn(NO); + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + OCMReject([self.channelUnitMock enqueueItem:OCMOCK_ANY flags:MSACFlagsDefault]); + [[MSACAnalytics sharedInstance] trackEvent:@"Some event" withTypedProperties:nil forTransmissionTarget:nil flags:MSACFlagsDefault]; + + // Then + OCMVerifyAll(self.channelUnitMock); +} + +- (void)testTrackEventWithPropertiesNilWhenTransmissionTargetDisabled { + + // If + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + OCMReject([self.channelUnitMock enqueueItem:OCMOCK_ANY flags:MSACFlagsDefault]); + MSACAnalyticsTransmissionTarget *target = [MSACAnalytics transmissionTargetForToken:@"test"]; + [target setEnabled:NO]; + [[MSACAnalytics sharedInstance] trackEvent:@"Some event" withProperties:nil forTransmissionTarget:target flags:MSACFlagsDefault]; + + // Then + OCMVerifyAll(self.channelUnitMock); + + // FIXME: logManager holds session tracker somehow and it causes other test failures. Stop it for hack. + [[MSACAnalytics sharedInstance].sessionTracker stop]; +} + +- (void)testTrackEventWithPropertiesWhenTransmissionTargetProvided { + + // If + __block NSUInteger propertiesCount = 0; + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + OCMStub([self.channelUnitMock enqueueItem:[OCMArg isKindOfClass:[MSACEventLog class]] flags:MSACFlagsDefault]) + .andDo(^(NSInvocation *invocation) { + MSACEventLog *log; + [invocation getArgument:&log atIndex:2]; + propertiesCount = log.typedProperties.properties.count; + }); + + // When + NSMutableDictionary *properties = [NSMutableDictionary new]; + for (int i = 0; i < 100; i++) { + properties[[@"prop" stringByAppendingFormat:@"%d", i]] = [@"val" stringByAppendingFormat:@"%d", i]; + } + MSACAnalyticsTransmissionTarget *target = [MSACAnalytics transmissionTargetForToken:@"test"]; + [[MSACAnalytics sharedInstance] trackEvent:@"Some event" withProperties:properties forTransmissionTarget:target flags:MSACFlagsDefault]; + + // Then + XCTAssertEqual(properties.count, propertiesCount); +} + +- (void)testTrackEventSetsTagWhenTransmissionTargetProvided { + + // If + __block NSObject *tag; + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + OCMStub([self.channelUnitMock enqueueItem:[OCMArg isKindOfClass:[MSACEventLog class]] flags:MSACFlagsDefault]) + .andDo(^(NSInvocation *invocation) { + MSACEventLog *log; + [invocation getArgument:&log atIndex:2]; + tag = log.tag; + }); + + // When + MSACAnalyticsTransmissionTarget *target = [MSACAnalytics transmissionTargetForToken:@"test"]; + [[MSACAnalytics sharedInstance] trackEvent:@"Some event" withProperties:nil forTransmissionTarget:target flags:MSACFlagsDefault]; + + // Then + XCTAssertEqualObjects(tag, target); +} + +- (void)testTrackEventDoesNotSetUserIdForAppCenter { + + // If + __block MSACEventLog *log; + [MSACAppCenter setUserId:@"c:test"]; + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + OCMStub([self.channelUnitMock enqueueItem:[OCMArg isKindOfClass:[MSACEventLog class]] flags:MSACFlagsDefault]) + .andDo(^(NSInvocation *invocation) { + [invocation getArgument:&log atIndex:2]; + }); + + // When + [MSACAnalytics trackEvent:@"Some event"]; + + // Then + XCTAssertNotNil(log); + XCTAssertNil(log.userId); +} + +- (void)testTrackEventWithTypedPropertiesNilWhenTransmissionTargetDisabled { + + // If + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + OCMReject([self.channelUnitMock enqueueItem:OCMOCK_ANY flags:MSACFlagsDefault]); + MSACAnalyticsTransmissionTarget *target = [MSACAnalytics transmissionTargetForToken:@"test"]; + [target setEnabled:NO]; + [[MSACAnalytics sharedInstance] trackEvent:@"Some event" withTypedProperties:nil forTransmissionTarget:target flags:MSACFlagsDefault]; + + // Then + OCMVerifyAll(self.channelUnitMock); + + // FIXME: logManager holds session tracker somehow and it causes other test failures. Stop it for hack. + [[MSACAnalytics sharedInstance].sessionTracker stop]; +} + +- (void)testTrackEventWithPropertiesNilAndInvalidName { + + // If + NSString *invalidEventName = nil; + id analyticsMock = OCMPartialMock([MSACAnalytics sharedInstance]); + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + OCMExpect([self.channelUnitMock enqueueItem:OCMOCK_ANY flags:MSACFlagsDefault]); + + // Will be validated in shouldFilterLog callback instead. + OCMReject([analyticsMock validateEventName:OCMOCK_ANY forLogType:OCMOCK_ANY]); + OCMReject([analyticsMock validateProperties:OCMOCK_ANY forLogName:OCMOCK_ANY andType:OCMOCK_ANY]); + [[MSACAnalytics sharedInstance] trackEvent:invalidEventName withProperties:nil forTransmissionTarget:nil flags:MSACFlagsDefault]; + + // Then + OCMVerifyAll(self.channelUnitMock); + OCMVerifyAll(analyticsMock); +} + +- (void)testTrackEventWithTypedPropertiesNilAndInvalidName { + + // If + NSString *invalidEventName = nil; + id analyticsMock = OCMPartialMock([MSACAnalytics sharedInstance]); + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + OCMExpect([self.channelUnitMock enqueueItem:OCMOCK_ANY flags:MSACFlagsDefault]); + + // Will be validated in shouldFilterLog callback instead. + OCMReject([analyticsMock validateEventName:OCMOCK_ANY forLogType:OCMOCK_ANY]); + OCMReject([analyticsMock validateProperties:OCMOCK_ANY forLogName:OCMOCK_ANY andType:OCMOCK_ANY]); + [[MSACAnalytics sharedInstance] trackEvent:invalidEventName withTypedProperties:nil forTransmissionTarget:nil flags:MSACFlagsDefault]; + + // Then + OCMVerifyAll(self.channelUnitMock); + OCMVerifyAll(analyticsMock); +} + +- (void)testTrackEventWithProperties { + + // If + __block NSString *type; + __block NSString *name; + __block MSACEventProperties *eventProperties; + NSString *expectedName = @"gotACoffee"; + NSDictionary *expectedProperties = @{@"milk" : @"yes", @"cookie" : @"of course"}; + OCMStub([self.channelUnitMock enqueueItem:[OCMArg isKindOfClass:[MSACEventLog class]] flags:MSACFlagsDefault]) + .andDo(^(NSInvocation *invocation) { + MSACEventLog *log; + [invocation getArgument:&log atIndex:2]; + type = log.type; + name = log.name; + eventProperties = log.typedProperties; + }); + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + [MSACAnalytics trackEvent:expectedName withProperties:expectedProperties]; + + // Then + assertThat(type, is(kMSACTypeEvent)); + assertThat(name, is(expectedName)); + for (MSACTypedProperty *typedProperty in [eventProperties.properties objectEnumerator]) { + assertThat(typedProperty, isA([MSACStringTypedProperty class])); + MSACStringTypedProperty *stringTypedProperty = (MSACStringTypedProperty *)typedProperty; + assertThat(stringTypedProperty.value, equalTo(expectedProperties[stringTypedProperty.name])); + } + XCTAssertEqual([expectedProperties count], [eventProperties.properties count]); +} + +- (void)testTrackEventWithTypedProperties { + + // If + __block NSString *type; + __block NSString *name; + __block MSACEventProperties *eventProperties; + MSACEventProperties *expectedProperties = [MSACEventProperties new]; + [expectedProperties setString:@"string" forKey:@"stringKey"]; + [expectedProperties setBool:YES forKey:@"boolKey"]; + [expectedProperties setDate:[NSDate new] forKey:@"dateKey"]; + [expectedProperties setInt64:123 forKey:@"longKey"]; + [expectedProperties setDouble:1.23e2 forKey:@"doubleKey"]; + NSString *expectedName = @"gotACoffee"; + OCMStub([self.channelUnitMock enqueueItem:[OCMArg isKindOfClass:[MSACEventLog class]] flags:MSACFlagsDefault]) + .andDo(^(NSInvocation *invocation) { + MSACEventLog *log; + [invocation getArgument:&log atIndex:2]; + type = log.type; + name = log.name; + eventProperties = log.typedProperties; + }); + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + [MSACAnalytics trackEvent:expectedName withTypedProperties:expectedProperties]; + + // Then + assertThat(type, is(kMSACTypeEvent)); + assertThat(name, is(expectedName)); + + for (NSString *propertyKey in eventProperties.properties) { + MSACTypedProperty *typedProperty = eventProperties.properties[propertyKey]; + XCTAssertEqual(typedProperty.name, propertyKey); + if ([typedProperty isKindOfClass:[MSACBooleanTypedProperty class]]) { + MSACBooleanTypedProperty *expectedProperty = (MSACBooleanTypedProperty *)expectedProperties.properties[propertyKey]; + MSACBooleanTypedProperty *property = (MSACBooleanTypedProperty *)eventProperties.properties[propertyKey]; + XCTAssertEqual(property.value, expectedProperty.value); + } else if ([typedProperty isKindOfClass:[MSACDoubleTypedProperty class]]) { + MSACDoubleTypedProperty *expectedProperty = (MSACDoubleTypedProperty *)expectedProperties.properties[propertyKey]; + MSACDoubleTypedProperty *property = (MSACDoubleTypedProperty *)eventProperties.properties[propertyKey]; + XCTAssertEqual(property.value, expectedProperty.value); + } else if ([typedProperty isKindOfClass:[MSACLongTypedProperty class]]) { + MSACLongTypedProperty *expectedProperty = (MSACLongTypedProperty *)expectedProperties.properties[propertyKey]; + MSACLongTypedProperty *property = (MSACLongTypedProperty *)eventProperties.properties[propertyKey]; + XCTAssertEqual(property.value, expectedProperty.value); + } else if ([typedProperty isKindOfClass:[MSACStringTypedProperty class]]) { + MSACStringTypedProperty *expectedProperty = (MSACStringTypedProperty *)expectedProperties.properties[propertyKey]; + MSACStringTypedProperty *property = (MSACStringTypedProperty *)eventProperties.properties[propertyKey]; + XCTAssertEqualObjects(property.value, expectedProperty.value); + } else if ([typedProperty isKindOfClass:[MSACDateTimeTypedProperty class]]) { + MSACDateTimeTypedProperty *expectedProperty = (MSACDateTimeTypedProperty *)expectedProperties.properties[propertyKey]; + MSACDateTimeTypedProperty *property = (MSACDateTimeTypedProperty *)eventProperties.properties[propertyKey]; + XCTAssertEqual(property.value, expectedProperty.value); + } + [expectedProperties.properties removeObjectForKey:propertyKey]; + } + XCTAssertEqual([expectedProperties.properties count], 0); +} + +- (void)testTrackEventWithPropertiesWithNormalPersistenceFlag { + + // If + __block NSString *actualType; + __block NSString *actualName; + __block MSACFlags actualFlags; + NSString *expectedName = @"gotACoffee"; + OCMStub([[self.channelUnitMock ignoringNonObjectArgs] enqueueItem:[OCMArg isKindOfClass:[MSACEventLog class]] flags:(MSACFlags)0]) + .andDo(^(NSInvocation *invocation) { + MSACEventLog *log; + [invocation getArgument:&log atIndex:2]; + actualType = log.type; + actualName = log.name; + MSACFlags flags; + [invocation getArgument:&flags atIndex:3]; + actualFlags = flags; + }); + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + [MSACAnalytics trackEvent:expectedName withProperties:nil flags:MSACFlagsNormal]; + + // Then + XCTAssertEqual(actualType, kMSACTypeEvent); + XCTAssertEqual(actualName, expectedName); + XCTAssertEqual(actualFlags, MSACFlagsNormal); +} + +- (void)testTrackEventWithPropertiesWithCriticalPersistenceFlag { + + // If + __block NSString *actualType; + __block NSString *actualName; + __block MSACFlags actualFlags; + NSString *expectedName = @"gotACoffee"; + OCMReject([[self.channelUnitMock ignoringNonObjectArgs] enqueueItem:[OCMArg isKindOfClass:[MSACEventLog class]] flags:(MSACFlags)0]); + OCMStub([[self.channelUnitCriticalMock ignoringNonObjectArgs] enqueueItem:[OCMArg isKindOfClass:[MSACEventLog class]] flags:(MSACFlags)0]) + .andDo(^(NSInvocation *invocation) { + MSACEventLog *log; + [invocation getArgument:&log atIndex:2]; + actualType = log.type; + actualName = log.name; + MSACFlags flags; + [invocation getArgument:&flags atIndex:3]; + actualFlags = flags; + }); + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + [MSACAnalytics trackEvent:expectedName withProperties:nil flags:MSACFlagsCritical]; + + // Then + XCTAssertEqual(actualType, kMSACTypeEvent); + XCTAssertEqual(actualName, expectedName); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + XCTAssertEqual(actualFlags, MSACFlagsPersistenceCritical); + OCMVerifyAll(self.channelUnitMock); +#pragma clang diagnostic pop +} + +- (void)testTrackEventWithPropertiesWithInvalidFlag { + + // If + __block NSString *actualType; + __block NSString *actualName; + __block MSACFlags actualFlags; + NSString *expectedName = @"gotACoffee"; + OCMStub([[self.channelUnitMock ignoringNonObjectArgs] enqueueItem:[OCMArg isKindOfClass:[MSACEventLog class]] flags:(MSACFlags)0]) + .andDo(^(NSInvocation *invocation) { + MSACEventLog *log; + [invocation getArgument:&log atIndex:2]; + actualType = log.type; + actualName = log.name; + MSACFlags flags; + [invocation getArgument:&flags atIndex:3]; + actualFlags = flags; + }); + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + [MSACAnalytics trackEvent:expectedName withProperties:nil flags:42]; + + // Then + XCTAssertEqual(actualType, kMSACTypeEvent); + XCTAssertEqual(actualName, expectedName); + XCTAssertEqual(actualFlags, MSACFlagsNormal); +} + +- (void)testPersistanceFlagsSeparateChannels { + + // If + NSString *expectedCriticalEvent = @"Having a cup of coffee"; + NSString *expectedEvent = @"Washing a cup after having a coffee"; + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + OCMExpect([self.channelUnitCriticalMock enqueueItem:OCMOCK_ANY flags:MSACFlagsPersistenceCritical]); + OCMExpect([self.channelUnitMock enqueueItem:OCMOCK_ANY flags:MSACFlagsPersistenceNormal]); + + // When + [[MSACAnalytics sharedInstance] trackEvent:expectedCriticalEvent + withTypedProperties:nil + forTransmissionTarget:nil + flags:MSACFlagsPersistenceCritical]; + [[MSACAnalytics sharedInstance] trackEvent:expectedEvent + withTypedProperties:nil + forTransmissionTarget:nil + flags:MSACFlagsPersistenceNormal]; +#pragma clang diagnostic pop + + // Then + OCMVerifyAll(self.channelUnitCriticalMock); + OCMVerifyAll(self.channelUnitMock); +} + +- (void)testTrackEventWithTypedPropertiesWithNormalPersistenceFlag { + + // If + __block NSString *actualType; + __block NSString *actualName; + __block MSACFlags actualFlags; + NSString *expectedName = @"gotACoffee"; + OCMStub([[self.channelUnitMock ignoringNonObjectArgs] enqueueItem:[OCMArg isKindOfClass:[MSACEventLog class]] flags:(MSACFlags)0]) + .andDo(^(NSInvocation *invocation) { + MSACEventLog *log; + [invocation getArgument:&log atIndex:2]; + actualType = log.type; + actualName = log.name; + MSACFlags flags; + [invocation getArgument:&flags atIndex:3]; + actualFlags = flags; + }); + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + [MSACAnalytics trackEvent:expectedName withTypedProperties:nil flags:MSACFlagsNormal]; + + // Then + XCTAssertEqual(actualType, kMSACTypeEvent); + XCTAssertEqual(actualName, expectedName); + XCTAssertEqual(actualFlags, MSACFlagsNormal); +} + +- (void)testTrackEventWithTypedPropertiesWithCriticalPersistenceFlag { + + // If + __block NSString *actualType; + __block NSString *actualName; + __block MSACFlags actualFlags; + NSString *expectedName = @"gotACoffee"; + OCMStub([[self.channelUnitCriticalMock ignoringNonObjectArgs] enqueueItem:[OCMArg isKindOfClass:[MSACEventLog class]] flags:(MSACFlags)0]) + .andDo(^(NSInvocation *invocation) { + MSACEventLog *log; + [invocation getArgument:&log atIndex:2]; + actualType = log.type; + actualName = log.name; + MSACFlags flags; + [invocation getArgument:&flags atIndex:3]; + actualFlags = flags; + }); + OCMReject([[self.channelUnitMock ignoringNonObjectArgs] enqueueItem:[OCMArg isKindOfClass:[MSACEventLog class]] flags:(MSACFlags)0]); + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + [MSACAnalytics trackEvent:expectedName withTypedProperties:nil flags:MSACFlagsCritical]; + + // Then + XCTAssertEqual(actualType, kMSACTypeEvent); + XCTAssertEqual(actualName, expectedName); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + XCTAssertEqual(actualFlags, MSACFlagsPersistenceCritical); +#pragma clang diagnostic pop + OCMVerifyAll(self.channelUnitMock); +} + +- (void)testTrackEventWithTypedPropertiesWithInvalidFlag { + + // If + __block NSString *actualType; + __block NSString *actualName; + __block MSACFlags actualFlags; + NSString *expectedName = @"gotACoffee"; + OCMStub([[self.channelUnitMock ignoringNonObjectArgs] enqueueItem:[OCMArg isKindOfClass:[MSACEventLog class]] flags:(MSACFlags)0]) + .andDo(^(NSInvocation *invocation) { + MSACEventLog *log; + [invocation getArgument:&log atIndex:2]; + actualType = log.type; + actualName = log.name; + MSACFlags flags; + [invocation getArgument:&flags atIndex:3]; + actualFlags = flags; + }); + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + [MSACAnalytics trackEvent:expectedName withTypedProperties:nil flags:42]; + + // Then + XCTAssertEqual(actualType, kMSACTypeEvent); + XCTAssertEqual(actualName, expectedName); + XCTAssertEqual(actualFlags, MSACFlagsNormal); +} + +- (void)testTrackPageWithoutProperties { + + // If + __block NSString *name; + __block NSString *type; + NSString *expectedName = @"HomeSweetHome"; + OCMStub([self.channelUnitMock enqueueItem:[OCMArg isKindOfClass:[MSACLogWithProperties class]] flags:MSACFlagsDefault]) + .andDo(^(NSInvocation *invocation) { + MSACEventLog *log; + [invocation getArgument:&log atIndex:2]; + type = log.type; + name = log.name; + }); + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + [MSACAnalytics trackPage:expectedName]; + + // Then + assertThat(type, is(kMSACTypePage)); + assertThat(name, is(expectedName)); +} + +- (void)testTrackPageWithProperties { + + // If + __block NSString *type; + __block NSString *name; + __block NSDictionary *properties; + NSString *expectedName = @"HomeSweetHome"; + NSDictionary *expectedProperties = @{@"Sofa" : @"yes", @"TV" : @"of course"}; + OCMStub([self.channelUnitMock enqueueItem:[OCMArg isKindOfClass:[MSACLogWithProperties class]] flags:MSACFlagsDefault]) + .andDo(^(NSInvocation *invocation) { + MSACEventLog *log; + [invocation getArgument:&log atIndex:2]; + type = log.type; + name = log.name; + properties = log.properties; + }); + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + [MSACAnalytics trackPage:expectedName withProperties:expectedProperties]; + + // Then + assertThat(type, is(kMSACTypePage)); + assertThat(name, is(expectedName)); + assertThat(properties, is(expectedProperties)); +} + +- (void)testTrackPageWhenAnalyticsDisabled { + + // If + id analyticsMock = OCMPartialMock([MSACAnalytics sharedInstance]); + OCMStub([analyticsMock isEnabled]).andReturn(NO); + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + OCMReject([self.channelUnitMock enqueueItem:OCMOCK_ANY flags:MSACFlagsDefault]); + [[MSACAnalytics sharedInstance] trackPage:@"Some page" withProperties:nil]; + + // Then + OCMVerifyAll(self.channelUnitMock); +} + +- (void)testTrackPageWithInvalidName { + + // If + NSString *invalidPageName = nil; + id analyticsMock = OCMPartialMock([MSACAnalytics sharedInstance]); + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + OCMExpect([self.channelUnitMock enqueueItem:OCMOCK_ANY flags:MSACFlagsDefault]); + + // Will be validated in shouldFilterLog callback instead. + OCMReject([analyticsMock validateEventName:OCMOCK_ANY forLogType:OCMOCK_ANY]); + OCMReject([analyticsMock validateProperties:OCMOCK_ANY forLogName:OCMOCK_ANY andType:OCMOCK_ANY]); + [[MSACAnalytics sharedInstance] trackPage:invalidPageName withProperties:nil]; + + // Then + OCMVerifyAll(self.channelUnitMock); + OCMVerifyAll(analyticsMock); +} + +- (void)testAutoPageTracking { + + // For now auto page tracking is disabled by default + XCTAssertFalse([MSACAnalytics isAutoPageTrackingEnabled]); + + // When + [MSACAnalytics setAutoPageTrackingEnabled:YES]; + + // Then + XCTAssertTrue([MSACAnalytics isAutoPageTrackingEnabled]); + + // When + [MSACAnalytics setAutoPageTrackingEnabled:NO]; + + // Then + XCTAssertFalse([MSACAnalytics isAutoPageTrackingEnabled]); +} + +- (void)testInitializationPriorityCorrect { + XCTAssertTrue([[MSACAnalytics sharedInstance] initializationPriority] == MSACInitializationPriorityDefault); +} + +- (void)testServiceNameIsCorrect { + XCTAssertEqual([MSACAnalytics serviceName], kMSACAnalyticsServiceName); +} + +#if TARGET_OS_IOS + +// TODO: Modify for testing each platform when page tracking will be supported on each platform. +- (void)testViewWillAppearSwizzlingWithAnalyticsAvailable { + + // If + id analyticsMock = OCMPartialMock([MSACAnalytics sharedInstance]); + OCMStub([analyticsMock isAutoPageTrackingEnabled]).andReturn(YES); + OCMStub([analyticsMock isAvailable]).andReturn(YES); + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:OCMProtocolMock(@protocol(MSACChannelGroupProtocol)) + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + +// When +#if TARGET_OS_OSX + NSViewController *viewController = [[NSViewController alloc] init]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability" + if ([viewController respondsToSelector:@selector(viewWillAppear)]) { + [viewController viewWillAppear]; + } +#pragma clang diagnostic pop +#else + UIViewController *viewController = [[UIViewController alloc] init]; + [viewController viewWillAppear:NO]; +#endif + + // Then + OCMVerify([analyticsMock isAutoPageTrackingEnabled]); + XCTAssertNil([MSACAnalyticsCategory missedPageViewName]); +} + +- (void)testViewWillAppearSwizzlingWithAnalyticsNotAvailable { + + // If + id analyticsMock = OCMPartialMock([MSACAnalytics sharedInstance]); + OCMStub([analyticsMock isAutoPageTrackingEnabled]).andReturn(YES); + OCMStub([analyticsMock isAvailable]).andReturn(NO); + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:OCMProtocolMock(@protocol(MSACChannelGroupProtocol)) + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + +// When +#if TARGET_OS_OSX + NSViewController *viewController = [[NSViewController alloc] init]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability" + if ([viewController respondsToSelector:@selector(viewWillAppear)]) { + [viewController viewWillAppear]; + } +#pragma clang diagnostic pop +#else + UIViewController *viewController = [[UIViewController alloc] init]; + [viewController viewWillAppear:NO]; +#endif + + // Then + OCMVerify([analyticsMock isAutoPageTrackingEnabled]); + XCTAssertNotNil([MSACAnalyticsCategory missedPageViewName]); +} + +- (void)testViewWillAppearSwizzlingWithShouldTrackPageDisabled { + + // If + id analyticsMock = OCMPartialMock([MSACAnalytics sharedInstance]); + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:OCMProtocolMock(@protocol(MSACChannelGroupProtocol)) + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + OCMExpect([analyticsMock isAutoPageTrackingEnabled]).andReturn(YES); + OCMReject([analyticsMock isAvailable]); +#if TARGET_OS_OSX + NSPageController *containerController = [[NSPageController alloc] init]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability" + if ([containerController respondsToSelector:@selector(viewWillAppear)]) { + [containerController viewWillAppear]; + } +#pragma clang diagnostic pop +#else + UIPageViewController *containerController = [[UIPageViewController alloc] init]; + [containerController viewWillAppear:NO]; +#endif + + // Then + OCMVerifyAll(analyticsMock); +} + +#endif + +- (void)testStartWithTransmissionTargetAndAppSecretUsesTransmissionTarget { + + // If + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + __block MSACEventLog *log; + __block int invocations = 0; + OCMStub([self.channelUnitMock enqueueItem:[OCMArg isKindOfClass:[MSACLogWithProperties class]] flags:MSACFlagsDefault]) + .andDo(^(NSInvocation *invocation) { + ++invocations; + [invocation getArgument:&log atIndex:2]; + }); + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:kMSACTestTransmissionToken + fromApplication:YES]; + + // When + [MSACAnalytics trackEvent:@"eventName"]; + + // Then + OCMVerify([self.channelUnitMock enqueueItem:log flags:MSACFlagsDefault]); + XCTAssertTrue([[log transmissionTargetTokens] containsObject:kMSACTestTransmissionToken]); + XCTAssertEqual([[log transmissionTargetTokens] count], (unsigned long)1); + XCTAssertEqual(invocations, 1); +} + +- (void)testStartWithTransmissionTargetWithoutAppSecretUsesTransmissionTarget { + + // If + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + __block MSACEventLog *log; + __block int invocations = 0; + OCMStub([self.channelUnitMock enqueueItem:[OCMArg isKindOfClass:[MSACLogWithProperties class]] flags:MSACFlagsDefault]) + .andDo(^(NSInvocation *invocation) { + ++invocations; + [invocation getArgument:&log atIndex:2]; + }); + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:nil + transmissionTargetToken:kMSACTestTransmissionToken + fromApplication:YES]; + + // When + [MSACAnalytics trackEvent:@"eventName"]; + + // Then + OCMVerify([self.channelUnitMock enqueueItem:log flags:MSACFlagsDefault]); + XCTAssertTrue([[log transmissionTargetTokens] containsObject:kMSACTestTransmissionToken]); + XCTAssertEqual([[log transmissionTargetTokens] count], (unsigned long)1); + XCTAssertEqual(invocations, 1); +} + +- (void)testGetTransmissionTargetCreatesTransmissionTargetOnce { + + // When + MSACAnalyticsTransmissionTarget *transmissionTarget1 = [MSACAnalytics transmissionTargetForToken:kMSACTestTransmissionToken]; + MSACAnalyticsTransmissionTarget *transmissionTarget2 = [MSACAnalytics transmissionTargetForToken:kMSACTestTransmissionToken]; + + // Then + XCTAssertNotNil(transmissionTarget1); + XCTAssertEqual(transmissionTarget1, transmissionTarget2); +} + +- (void)testGetTransmissionTargetNeverReturnsDefault { + + // If + [[MSACAnalytics sharedInstance] startWithChannelGroup:OCMProtocolMock(@protocol(MSACChannelGroupProtocol)) + appSecret:nil + transmissionTargetToken:kMSACTestTransmissionToken + fromApplication:NO]; + + // When + MSACAnalyticsTransmissionTarget *transmissionTarget = [MSACAnalytics transmissionTargetForToken:kMSACTestTransmissionToken]; + + // Then + XCTAssertNotNil([MSACAnalytics sharedInstance].defaultTransmissionTarget); + XCTAssertNotNil(transmissionTarget); + XCTAssertNotEqual([MSACAnalytics sharedInstance].defaultTransmissionTarget, transmissionTarget); +} + +- (void)testDefaultTransmissionTargetMirrorAnalyticsEnableState { + + // If + MSACAnalytics *service = [MSACAnalytics sharedInstance]; + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + + // When + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:kMSACTestTransmissionToken + fromApplication:YES]; + + // Then + XCTAssertNotNil([MSACAnalytics sharedInstance].defaultTransmissionTarget); + XCTAssertTrue([service isEnabled]); + XCTAssertTrue([service.defaultTransmissionTarget isEnabled]); + + // When + [service setEnabled:NO]; + + // Then + XCTAssertFalse([service isEnabled]); + XCTAssertFalse([service.defaultTransmissionTarget isEnabled]); + + // When + [service setEnabled:YES]; + + // Then + XCTAssertTrue([service isEnabled]); + XCTAssertTrue([service.defaultTransmissionTarget isEnabled]); +} + +- (void)testEnableStatePropagateToTransmissionTargets { + + // If + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:OCMProtocolMock(@protocol(MSACChannelGroupProtocol)) + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:NO]; + MSACServiceAbstract *analytics = [MSACAnalytics sharedInstance]; + [analytics setEnabled:NO]; + + // When + + // Analytics is disabled, targets must match Analytics enabled state. + MSACAnalyticsTransmissionTarget *transmissionTarget = [MSACAnalytics transmissionTargetForToken:kMSACTestTransmissionToken]; + MSACAnalyticsTransmissionTarget *transmissionTarget2 = [MSACAnalytics transmissionTargetForToken:kMSACTestTransmissionToken2]; + + // Then + XCTAssertFalse([transmissionTarget isEnabled]); + XCTAssertFalse([transmissionTarget2 isEnabled]); + + // When + + // Trying re-enabling will fail since Analytics is still disabled. + [transmissionTarget setEnabled:YES]; + + // Then + XCTAssertFalse([transmissionTarget isEnabled]); + XCTAssertFalse([transmissionTarget2 isEnabled]); + + // When + + // Enabling Analytics will enable all targets. + [analytics setEnabled:YES]; + + // Then + XCTAssertTrue([transmissionTarget isEnabled]); + XCTAssertTrue([transmissionTarget2 isEnabled]); + + // Disabling Analytics will disable all targets. + [analytics setEnabled:NO]; + + // Then + XCTAssertFalse([transmissionTarget isEnabled]); + XCTAssertFalse([transmissionTarget2 isEnabled]); +} + +- (void)testAppSecretNotRequired { + XCTAssertFalse([[MSACAnalytics sharedInstance] isAppSecretRequired]); +} + +- (void)testSessionTrackerStarted { + + // When + [MSACAppCenter startFromLibraryWithServices:@ [[MSACAnalytics class]]]; + + // Then + XCTAssertFalse([MSACAnalytics sharedInstance].sessionTracker.started); + + // When + [MSACAppCenter start:MSAC_UUID_STRING withServices:@ [[MSACAnalytics class]]]; + + // Then + XCTAssertTrue([MSACAnalytics sharedInstance].sessionTracker.started); +} + +- (void)testSessionTrackerStartedWithToken { + + // When + [MSACAppCenter startFromLibraryWithServices:@ [[MSACAnalytics class]]]; + + // Then + XCTAssertNil([MSACAnalytics sharedInstance].defaultTransmissionTarget); + + // When + [[MSACAnalytics sharedInstance] updateConfigurationWithAppSecret:kMSACTestAppSecret transmissionTargetToken:kMSACTestTransmissionToken]; + + // Then + XCTAssertNotNil([MSACAnalytics sharedInstance].defaultTransmissionTarget); +} + +- (void)testAutoPageTrackingWhenStartedFromLibrary { + + // If + id analyticsMock = OCMPartialMock([MSACAnalytics sharedInstance]); + id analyticsCategoryMock = OCMClassMock([MSACAnalyticsCategory class]); + NSString *testPageName = @"TestPage"; + OCMStub([analyticsCategoryMock missedPageViewName]).andReturn(testPageName); + [MSACAnalytics setAutoPageTrackingEnabled:YES]; + MSACServiceAbstract *service = [MSACAnalytics sharedInstance]; + + // When + [[MSACAnalytics sharedInstance] startWithChannelGroup:OCMProtocolMock(@protocol(MSACChannelGroupProtocol)) + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:NO]; + + // Then + XCTestExpectation *expectation = [self expectationWithDescription:@"Wait for block in applyEnabledState to be dispatched"]; + dispatch_async(dispatch_get_main_queue(), ^{ + [expectation fulfill]; + }); + + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + + // Then + XCTAssertTrue([service isEnabled]); + OCMReject([analyticsMock trackPage:testPageName withProperties:nil]); + }]; +} + +#pragma mark - Property validation tests + +- (void)testRemoveInvalidPropertiesWithEmptyValue { + + // If + NSDictionary *emptyValueProperties = @{@"aValidKey" : @""}; + + // When + NSDictionary *result = [[MSACAnalytics sharedInstance] removeInvalidProperties:emptyValueProperties]; + + // Then + XCTAssertTrue(result.count == 1); + XCTAssertEqualObjects(result, emptyValueProperties); +} + +- (void)testRemoveInvalidPropertiesWithEmptyKey { + + // If + NSDictionary *emptyKeyProperties = @{@"" : @"aValidValue"}; + + // When + NSDictionary *result = [[MSACAnalytics sharedInstance] removeInvalidProperties:emptyKeyProperties]; + + // Then + XCTAssertTrue(result.count == 1); +} + +- (void)testremoveInvalidPropertiesWithNonStringKey { + + // If + NSDictionary *numberAsKeyProperties = @{@(42) : @"aValidValue"}; + + // When + NSDictionary *result = [[MSACAnalytics sharedInstance] removeInvalidProperties:numberAsKeyProperties]; + + // Then + XCTAssertTrue(result.count == 0); +} + +- (void)testValidateLogDataWithNonStringValue { + + // If + NSDictionary *numberAsValueProperties = @{@"aValidKey" : @(42)}; + + // When + NSDictionary *result = [[MSACAnalytics sharedInstance] removeInvalidProperties:numberAsValueProperties]; + + // Then + XCTAssertTrue(result.count == 0); +} + +- (void)testValidateLogDataWithCorrectNestedProperties { + + // If + NSDictionary *correctlyNestedProperties = @{@"aValidKey1" : @"aValidValue1", @"aValidKey2.aValidKey2" : @"aValidValue3"}; + + // When + NSDictionary *result = [[MSACAnalytics sharedInstance] removeInvalidProperties:correctlyNestedProperties]; + + // Then + XCTAssertTrue(result.count == 2); + XCTAssertEqualObjects(result, correctlyNestedProperties); +} + +- (void)testValidateLogDataWithIncorrectNestedProperties { + + // If + NSDictionary *incorrectNestedProperties = @{ + @"aValidKey1" : @"aValidValue1", + @"aValidKey2" : @1, + }; + + // When + NSDictionary *result = [[MSACAnalytics sharedInstance] removeInvalidProperties:incorrectNestedProperties]; + + // Then + XCTAssertTrue(result.count == 1); + XCTAssertNil(result[@"aValidKey2"]); + XCTAssertNotNil(result[@"aValidKey1"]); + XCTAssertEqualObjects(result[@"aValidKey1"], @"aValidValue1"); + XCTAssertNotEqualObjects(result, incorrectNestedProperties); +} + +- (void)testDictionaryContainsInvalidPropertiesKey { + + // If + NSDictionary *incorrectNestedProperties = @{@1 : @"aValidValue1", @"aValidKey2" : @"aValidValue2"}; + + // When + NSDictionary *result = [[MSACAnalytics sharedInstance] removeInvalidProperties:incorrectNestedProperties]; + + // Then + XCTAssertTrue(result.count == 1); + XCTAssertNotNil(result[@"aValidKey2"]); +} + +- (void)testDictionaryContainsValidNestedProperties { + NSDictionary *properties = @{@"aValidKey2" : @"aValidValue1", @"aValidKey1.avalidKey2" : @"aValidValue1"}; + // When + NSDictionary *result = [[MSACAnalytics sharedInstance] removeInvalidProperties:properties]; + + // Then + XCTAssertEqualObjects(result, properties); +} + +- (void)testPropertyNameIsTruncatedInACopyWhenValidatingForAppCenter { + + // If + MSACEventProperties *properties = [MSACEventProperties new]; + NSString *longKey = [@"" stringByPaddingToLength:kMSACMaxPropertyKeyLength + 2 withString:@"hi" startingAtIndex:0]; + NSString *truncatedKey = [longKey substringToIndex:kMSACMaxPropertyKeyLength - 1]; + [properties setString:@"test" forKey:longKey]; + MSACStringTypedProperty *originalProperty = (MSACStringTypedProperty *)properties.properties[longKey]; + + // When + MSACEventProperties *validProperties = [[MSACAnalytics sharedInstance] validateAppCenterEventProperties:properties]; + + // Then + MSACStringTypedProperty *validProperty = (MSACStringTypedProperty *)validProperties.properties[truncatedKey]; + XCTAssertNotNil(validProperty); + XCTAssertEqualObjects(validProperty.name, truncatedKey); + XCTAssertNotEqual(originalProperty, validProperty); + XCTAssertEqualObjects(originalProperty.name, longKey); +} + +- (void)testPropertyValueIsTruncatedInACopyWhenValidatingForAppCenter { + + // If + MSACEventProperties *properties = [MSACEventProperties new]; + NSString *key = @"key"; + NSString *longValue = [@"" stringByPaddingToLength:kMSACMaxPropertyValueLength + 2 withString:@"hi" startingAtIndex:0]; + NSString *truncatedValue = [longValue substringToIndex:kMSACMaxPropertyValueLength - 1]; + [properties setString:longValue forKey:key]; + MSACStringTypedProperty *originalProperty = (MSACStringTypedProperty *)properties.properties[key]; + + // When + MSACEventProperties *validProperties = [[MSACAnalytics sharedInstance] validateAppCenterEventProperties:properties]; + + // Then + MSACStringTypedProperty *validProperty = (MSACStringTypedProperty *)validProperties.properties[key]; + XCTAssertEqualObjects(validProperty.value, truncatedValue); + XCTAssertNotEqual(originalProperty, validProperty); + XCTAssertEqualObjects(originalProperty.value, longValue); +} + +- (void)testAppCenterCopyHas20PropertiesWhenSelfHasMoreThan20 { + + // If + MSACEventProperties *properties = [MSACEventProperties new]; + + // When + for (int i = 0; i < kMSACMaxPropertiesPerLog + 5; i++) { + [properties setBool:YES forKey:[@(i) stringValue]]; + } + MSACEventProperties *validProperties = [[MSACAnalytics sharedInstance] validateAppCenterEventProperties:properties]; + + // Then + XCTAssertEqual([validProperties.properties count], kMSACMaxPropertiesPerLog); +} + +- (void)testPause { + + // If + id appCenterMock = OCMClassMock([MSACAppCenter class]); + OCMStub([appCenterMock sharedInstance]).andReturn(appCenterMock); + OCMStub([appCenterMock isSdkConfigured]).andReturn(YES); + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + [MSACAnalytics pause]; + + // Then + OCMVerify([self.channelUnitMock pauseWithIdentifyingObject:[MSACAnalytics sharedInstance]]); + [appCenterMock stopMocking]; +} + +- (void)testResume { + + // If + id appCenterMock = OCMClassMock([MSACAppCenter class]); + OCMStub([appCenterMock sharedInstance]).andReturn(appCenterMock); + OCMStub([appCenterMock isSdkConfigured]).andReturn(YES); + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + [MSACAnalytics resume]; + + // Then + OCMVerify([self.channelUnitMock resumeWithIdentifyingObject:[MSACAnalytics sharedInstance]]); + [appCenterMock stopMocking]; +} + +- (void)testEnablingAnalyticsResumesIt { + + // If + id appCenterMock = OCMClassMock([MSACAppCenter class]); + OCMStub([appCenterMock sharedInstance]).andReturn(appCenterMock); + OCMStub([appCenterMock isSdkConfigured]).andReturn(YES); + OCMStub(ClassMethod([appCenterMock isEnabled])).andReturn(YES); + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + [MSACAnalytics setEnabled:NO]; + + // Reset ChannelUnitMock since it's already called at startup and we want to + // verify at enabling time. + [MSACAnalytics sharedInstance].channelUnit = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + + // When + [MSACAnalytics setEnabled:YES]; + + // Then + OCMVerify([[MSACAnalytics sharedInstance].channelUnit resumeWithIdentifyingObject:[MSACAnalytics sharedInstance]]); + [appCenterMock stopMocking]; +} + +- (void)testPauseTransmissionTargetInOneCollectorChannelUnitWhenPausedWithTargetKey { + + // If + id appCenterMock = OCMClassMock([MSACAppCenter class]); + OCMStub([appCenterMock sharedInstance]).andReturn(appCenterMock); + OCMStub([appCenterMock isSdkConfigured]).andReturn(YES); + OCMStub(ClassMethod([appCenterMock isEnabled])).andReturn(YES); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + id oneCollectorChannelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([channelGroupMock channelUnitForGroupId:@"Analytics/one"]).andReturn(oneCollectorChannelUnitMock); + [[MSACAnalytics sharedInstance] startWithChannelGroup:channelGroupMock appSecret:nil transmissionTargetToken:nil fromApplication:YES]; + // When + [MSACAnalytics pauseTransmissionTargetForToken:kMSACTestTransmissionToken]; + + // Then + OCMVerify([oneCollectorChannelUnitMock pauseSendingLogsWithToken:kMSACTestTransmissionToken]); + [appCenterMock stopMocking]; +} + +- (void)testResumeTransmissionTargetInOneCollectorChannelUnitWhenResumedWithTargetKey { + + // If + id appCenterMock = OCMClassMock([MSACAppCenter class]); + OCMStub([appCenterMock sharedInstance]).andReturn(appCenterMock); + OCMStub([appCenterMock isSdkConfigured]).andReturn(YES); + OCMStub(ClassMethod([appCenterMock isEnabled])).andReturn(YES); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + id oneCollectorChannelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([channelGroupMock channelUnitForGroupId:@"Analytics/one"]).andReturn(oneCollectorChannelUnitMock); + [[MSACAnalytics sharedInstance] startWithChannelGroup:channelGroupMock appSecret:nil transmissionTargetToken:nil fromApplication:YES]; + // When + [MSACAnalytics resumeTransmissionTargetForToken:kMSACTestTransmissionToken]; + + // Then + OCMVerify([oneCollectorChannelUnitMock resumeSendingLogsWithToken:kMSACTestTransmissionToken]); + [appCenterMock stopMocking]; +} + +#if TARGET_OS_IOS + +// TODO: Modify for testing each platform when page tracking will be supported on each platform. +- (void)testViewWillAppearSwizzling { + + // If + id analyticsMock = OCMPartialMock([MSACAnalytics sharedInstance]); + UIViewController *viewController = [[UIViewController alloc] init]; + + // When + [MSACAnalyticsCategory activateCategory]; + [viewController viewWillAppear:NO]; + + // Then + OCMVerify([analyticsMock isAutoPageTrackingEnabled]); + + // Clear + [analyticsMock stopMocking]; +} + +#endif + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACAnalyticsTransmissionTargetTests.m b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACAnalyticsTransmissionTargetTests.m new file mode 100644 index 0000000000..37e843b851 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACAnalyticsTransmissionTargetTests.m @@ -0,0 +1,1451 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAnalyticsAuthenticationProviderInternal.h" +#import "MSACAnalyticsInternal.h" +#import "MSACAnalyticsPrivate.h" +#import "MSACAnalyticsTransmissionTargetInternal.h" +#import "MSACAnalyticsTransmissionTargetPrivate.h" +#import "MSACAppCenterInternal.h" +#import "MSACAppExtension.h" +#import "MSACBooleanTypedProperty.h" +#import "MSACCSExtensions.h" +#import "MSACChannelUnitDefault.h" +#import "MSACDateTimeTypedProperty.h" +#import "MSACDoubleTypedProperty.h" +#import "MSACEventLog.h" +#import "MSACEventPropertiesInternal.h" +#import "MSACLongTypedProperty.h" +#import "MSACMockUserDefaults.h" +#import "MSACPropertyConfiguratorInternal.h" +#import "MSACPropertyConfiguratorPrivate.h" +#import "MSACStringTypedProperty.h" +#import "MSACTestFrameworks.h" +#import "MSACUserExtension.h" +#import "MSACUserIdContextPrivate.h" + +static NSString *const kMSACTypeEvent = @"event"; +static NSString *const kMSACTestTransmissionToken = @"TestTransmissionToken"; +static NSString *const kMSACTestTransmissionToken2 = @"TestTransmissionToken2"; + +@interface MSACAnalyticsTransmissionTargetTests : XCTestCase + +@property(nonatomic) MSACMockUserDefaults *settingsMock; +@property(nonatomic) id analyticsClassMock; +@property(nonatomic) id channelGroupMock; + +@end + +@implementation MSACAnalyticsTransmissionTargetTests + +- (void)setUp { + [super setUp]; + [MSACUserIdContext resetSharedInstance]; + + // Mock NSUserDefaults + self.settingsMock = [MSACMockUserDefaults new]; + + // Analytics enabled state can prevent targets from tracking events. + id analyticsClassMock = OCMClassMock([MSACAnalytics class]); + self.analyticsClassMock = OCMPartialMock([MSACAnalytics sharedInstance]); + OCMStub([analyticsClassMock sharedInstance]).andReturn(self.analyticsClassMock); + self.channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + [MSACAppCenter sharedInstance].sdkConfigured = YES; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:@"appsecret" + transmissionTargetToken:@"token" + fromApplication:YES]; +} + +- (void)tearDown { + [self.settingsMock stopMocking]; + [self.analyticsClassMock stopMocking]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + MSACAnalyticsTransmissionTarget.authenticationProvider = nil; +#pragma clang diagnostic pop + [super tearDown]; +} + +#pragma mark - Tests + +- (void)testInitialization { + + // When + MSACAnalyticsTransmissionTarget *sut = [[MSACAnalyticsTransmissionTarget alloc] initWithTransmissionTargetToken:kMSACTestTransmissionToken + parentTarget:nil + channelGroup:self.channelGroupMock]; + + // Then + XCTAssertNotNil(sut); + XCTAssertEqual(kMSACTestTransmissionToken, sut.transmissionTargetToken); + XCTAssertTrue([sut.propertyConfigurator.eventProperties isEmpty]); + XCTAssertNil(MSACAnalyticsTransmissionTarget.authenticationProvider); +} + +- (void)testTrackEvent { + + // If + MSACAnalyticsTransmissionTarget *sut = [[MSACAnalyticsTransmissionTarget alloc] initWithTransmissionTargetToken:kMSACTestTransmissionToken + parentTarget:nil + channelGroup:self.channelGroupMock]; + NSString *eventName = @"event"; + + // When + [sut trackEvent:eventName]; + + // Then + XCTAssertTrue(sut.propertyConfigurator.eventProperties.properties.count == 0); + OCMVerify([self.analyticsClassMock trackEvent:eventName withTypedProperties:nil forTransmissionTarget:sut flags:MSACFlagsDefault]); +} + +- (void)testTrackEventWithProperties { + + // If + MSACAnalyticsTransmissionTarget *sut = [[MSACAnalyticsTransmissionTarget alloc] initWithTransmissionTargetToken:kMSACTestTransmissionToken + parentTarget:nil + channelGroup:self.channelGroupMock]; + NSString *eventName = @"event"; + NSDictionary *properties = @{@"prop1" : @"val1", @"prop2" : @"val2"}; + MSACEventProperties *expectedProperties = [MSACEventProperties new]; + for (NSString *key in properties.allKeys) { + [expectedProperties setString:properties[key] forKey:key]; + } + + // When + [sut trackEvent:eventName withProperties:properties]; + + // Then + XCTAssertTrue(sut.propertyConfigurator.eventProperties.properties.count == 0); + OCMVerify([self.analyticsClassMock trackEvent:eventName + withTypedProperties:expectedProperties + forTransmissionTarget:sut + flags:MSACFlagsDefault]); +} + +- (void)testTrackEventWithNilDictionaryProperties { + + // If + MSACAnalyticsTransmissionTarget *sut = [[MSACAnalyticsTransmissionTarget alloc] initWithTransmissionTargetToken:kMSACTestTransmissionToken + parentTarget:nil + channelGroup:self.channelGroupMock]; + NSString *eventName = @"event"; + OCMStub([self.analyticsClassMock canBeUsed]).andReturn(YES); + + // When + [sut trackEvent:eventName withProperties:nil]; + + // Then + XCTAssertTrue(sut.propertyConfigurator.eventProperties.properties.count == 0); + OCMVerify([self.analyticsClassMock trackEvent:eventName withTypedProperties:nil forTransmissionTarget:sut flags:MSACFlagsDefault]); +} + +- (void)testTrackEventWithNilEventProperties { + + // If + MSACAnalyticsTransmissionTarget *sut = [[MSACAnalyticsTransmissionTarget alloc] initWithTransmissionTargetToken:kMSACTestTransmissionToken + parentTarget:nil + channelGroup:self.channelGroupMock]; + NSString *eventName = @"event"; + OCMStub([self.analyticsClassMock canBeUsed]).andReturn(YES); + + // When + [sut trackEvent:eventName withTypedProperties:nil]; + + // Then + XCTAssertTrue(sut.propertyConfigurator.eventProperties.properties.count == 0); + OCMVerify([self.analyticsClassMock trackEvent:eventName withTypedProperties:nil forTransmissionTarget:sut flags:MSACFlagsDefault]); +} + +- (void)testTrackEventWithPropertiesWithNormalPersistenceFlag { + + // If + __block NSString *actualType; + __block NSString *actualName; + __block MSACFlags actualFlags; + NSString *expectedName = @"event"; + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([self.channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY]).andReturn(channelUnitMock); + OCMStub([[channelUnitMock ignoringNonObjectArgs] enqueueItem:[OCMArg isKindOfClass:[MSACEventLog class]] flags:(MSACFlags)0]) + .andDo(^(NSInvocation *invocation) { + MSACEventLog *log; + [invocation getArgument:&log atIndex:2]; + actualType = log.type; + actualName = log.name; + MSACFlags flags; + [invocation getArgument:&flags atIndex:3]; + actualFlags = flags; + }); + MSACAnalyticsTransmissionTarget *sut = [[MSACAnalyticsTransmissionTarget alloc] initWithTransmissionTargetToken:kMSACTestTransmissionToken + parentTarget:nil + channelGroup:self.channelGroupMock]; + NSString *appSecret = MSAC_UUID_STRING; + [MSACAppCenter configureWithAppSecret:appSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:appSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + [sut trackEvent:expectedName withProperties:nil flags:MSACFlagsNormal]; + + // Then + XCTAssertEqual(actualType, kMSACTypeEvent); + XCTAssertEqual(actualName, expectedName); + XCTAssertEqual(actualFlags, MSACFlagsNormal); +} + +- (void)testTrackEventWithPropertiesWithCriticalPersistenceFlag { + + // If + __block NSString *actualType; + __block NSString *actualName; + __block MSACFlags actualFlags; + NSString *expectedName = @"event"; + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([self.channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY]).andReturn(channelUnitMock); + OCMStub([[channelUnitMock ignoringNonObjectArgs] enqueueItem:[OCMArg isKindOfClass:[MSACEventLog class]] flags:(MSACFlags)0]) + .andDo(^(NSInvocation *invocation) { + MSACEventLog *log; + [invocation getArgument:&log atIndex:2]; + actualType = log.type; + actualName = log.name; + MSACFlags flags; + [invocation getArgument:&flags atIndex:3]; + actualFlags = flags; + }); + MSACAnalyticsTransmissionTarget *sut = [[MSACAnalyticsTransmissionTarget alloc] initWithTransmissionTargetToken:kMSACTestTransmissionToken + parentTarget:nil + channelGroup:self.channelGroupMock]; + NSString *appSecret = MSAC_UUID_STRING; + [MSACAppCenter configureWithAppSecret:appSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:appSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + [sut trackEvent:expectedName withProperties:nil flags:MSACFlagsCritical]; + + // Then + XCTAssertEqual(actualType, kMSACTypeEvent); + XCTAssertEqual(actualName, expectedName); + XCTAssertEqual(actualFlags, MSACFlagsCritical); +} + +- (void)testTrackEventWithPropertiesWithInvalidFlag { + + // If + __block NSString *actualType; + __block NSString *actualName; + __block MSACFlags actualFlags; + NSString *expectedName = @"event"; + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([self.channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY]).andReturn(channelUnitMock); + OCMStub([[channelUnitMock ignoringNonObjectArgs] enqueueItem:[OCMArg isKindOfClass:[MSACEventLog class]] flags:(MSACFlags)0]) + .andDo(^(NSInvocation *invocation) { + MSACEventLog *log; + [invocation getArgument:&log atIndex:2]; + actualType = log.type; + actualName = log.name; + MSACFlags flags; + [invocation getArgument:&flags atIndex:3]; + actualFlags = flags; + }); + MSACAnalyticsTransmissionTarget *sut = [[MSACAnalyticsTransmissionTarget alloc] initWithTransmissionTargetToken:kMSACTestTransmissionToken + parentTarget:nil + channelGroup:self.channelGroupMock]; + NSString *appSecret = MSAC_UUID_STRING; + [MSACAppCenter configureWithAppSecret:appSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:appSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + [sut trackEvent:expectedName withProperties:nil flags:42]; + + // Then + XCTAssertEqual(actualType, kMSACTypeEvent); + XCTAssertEqual(actualName, expectedName); + XCTAssertEqual(actualFlags, MSACFlagsNormal); +} + +- (void)testTrackEventWithTypedPropertiesWithNormalPersistenceFlag { + + // If + __block NSString *actualType; + __block NSString *actualName; + __block MSACFlags actualFlags; + NSString *expectedName = @"event"; + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([self.channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY]).andReturn(channelUnitMock); + OCMStub([[channelUnitMock ignoringNonObjectArgs] enqueueItem:[OCMArg isKindOfClass:[MSACEventLog class]] flags:(MSACFlags)0]) + .andDo(^(NSInvocation *invocation) { + MSACEventLog *log; + [invocation getArgument:&log atIndex:2]; + actualType = log.type; + actualName = log.name; + MSACFlags flags; + [invocation getArgument:&flags atIndex:3]; + actualFlags = flags; + }); + MSACAnalyticsTransmissionTarget *sut = [[MSACAnalyticsTransmissionTarget alloc] initWithTransmissionTargetToken:kMSACTestTransmissionToken + parentTarget:nil + channelGroup:self.channelGroupMock]; + NSString *appSecret = MSAC_UUID_STRING; + [MSACAppCenter configureWithAppSecret:appSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:appSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + [sut trackEvent:expectedName withTypedProperties:nil flags:MSACFlagsNormal]; + + // Then + XCTAssertEqual(actualType, kMSACTypeEvent); + XCTAssertEqual(actualName, expectedName); + XCTAssertEqual(actualFlags, MSACFlagsNormal); +} + +- (void)testTrackEventWithTypedPropertiesWithCriticalPersistenceFlag { + + // If + __block NSString *actualType; + __block NSString *actualName; + __block MSACFlags actualFlags; + NSString *expectedName = @"event"; + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([self.channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY]).andReturn(channelUnitMock); + OCMStub([[channelUnitMock ignoringNonObjectArgs] enqueueItem:[OCMArg isKindOfClass:[MSACEventLog class]] flags:(MSACFlags)0]) + .andDo(^(NSInvocation *invocation) { + MSACEventLog *log; + [invocation getArgument:&log atIndex:2]; + actualType = log.type; + actualName = log.name; + MSACFlags flags; + [invocation getArgument:&flags atIndex:3]; + actualFlags = flags; + }); + MSACAnalyticsTransmissionTarget *sut = [[MSACAnalyticsTransmissionTarget alloc] initWithTransmissionTargetToken:kMSACTestTransmissionToken + parentTarget:nil + channelGroup:self.channelGroupMock]; + NSString *appSecret = MSAC_UUID_STRING; + [MSACAppCenter configureWithAppSecret:appSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:appSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + [sut trackEvent:expectedName withTypedProperties:nil flags:MSACFlagsCritical]; + + // Then + XCTAssertEqual(actualType, kMSACTypeEvent); + XCTAssertEqual(actualName, expectedName); + XCTAssertEqual(actualFlags, MSACFlagsCritical); +} + +- (void)testTrackEventWithTypedPropertiesWithInvalidFlag { + + // If + __block NSString *actualType; + __block NSString *actualName; + __block MSACFlags actualFlags; + NSString *expectedName = @"event"; + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([self.channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY]).andReturn(channelUnitMock); + OCMStub([[channelUnitMock ignoringNonObjectArgs] enqueueItem:[OCMArg isKindOfClass:[MSACEventLog class]] flags:(MSACFlags)0]) + .andDo(^(NSInvocation *invocation) { + MSACEventLog *log; + [invocation getArgument:&log atIndex:2]; + actualType = log.type; + actualName = log.name; + MSACFlags flags; + [invocation getArgument:&flags atIndex:3]; + actualFlags = flags; + }); + MSACAnalyticsTransmissionTarget *sut = [[MSACAnalyticsTransmissionTarget alloc] initWithTransmissionTargetToken:kMSACTestTransmissionToken + parentTarget:nil + channelGroup:self.channelGroupMock]; + NSString *appSecret = MSAC_UUID_STRING; + [MSACAppCenter configureWithAppSecret:appSecret]; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:appSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + [sut trackEvent:expectedName withTypedProperties:nil flags:42]; + + // Then + XCTAssertEqual(actualType, kMSACTypeEvent); + XCTAssertEqual(actualName, expectedName); + XCTAssertEqual(actualFlags, MSACFlagsNormal); +} + +- (void)testTrackEventSetsUserIdForDefaultTransmissionTarget { + + // If + __block MSACEventLog *log; + [[MSACUserIdContext sharedInstance] setUserId:@"c:test"]; + [MSACAnalytics resetSharedInstance]; + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([self.channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY]).andReturn(channelUnitMock); + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:@"appsecret" + transmissionTargetToken:@"token" + fromApplication:YES]; + OCMStub([channelUnitMock enqueueItem:[OCMArg isKindOfClass:[MSACEventLog class]] flags:MSACFlagsDefault]) + .andDo(^(NSInvocation *invocation) { + [invocation getArgument:&log atIndex:2]; + }); + + // When + [MSACAnalytics trackEvent:@"Some event"]; + + // Then + XCTAssertNotNil(log); + XCTAssertEqual(log.userId, @"c:test"); +} + +- (void)testTrackEventDoesNotOverrideUserIdOfDefaultTransmissionTarget { + + // If + __block MSACEventLog *log; + [[MSACUserIdContext sharedInstance] setUserId:@"c:alice"]; + [MSACAnalytics resetSharedInstance]; + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([self.channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY]).andReturn(channelUnitMock); + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:@"appsecret" + transmissionTargetToken:@"defaultToken" + fromApplication:YES]; + MSACAnalyticsTransmissionTarget *target = [MSACAnalytics transmissionTargetForToken:@"anotherToken"]; + OCMStub([channelUnitMock enqueueItem:[OCMArg isKindOfClass:[MSACEventLog class]] flags:MSACFlagsDefault]) + .andDo(^(NSInvocation *invocation) { + [invocation getArgument:&log atIndex:2]; + }); + + // When + [MSACAnalytics trackEvent:@"Some event" withTypedProperties:nil forTransmissionTarget:target flags:MSACFlagsDefault]; + + // Then + XCTAssertNotNil(log); + XCTAssertNil(log.userId); +} + +- (void)testTransmissionTargetForToken { + + // If + NSDictionary *properties = [NSDictionary new]; + MSACEventProperties *emptyProperties = [MSACEventProperties new]; + NSString *event1 = @"event1"; + NSString *event2 = @"event2"; + NSString *event3 = @"event3"; + + MSACAnalyticsTransmissionTarget *parentTransmissionTarget = + [[MSACAnalyticsTransmissionTarget alloc] initWithTransmissionTargetToken:kMSACTestTransmissionToken + parentTarget:nil + channelGroup:self.channelGroupMock]; + MSACAnalyticsTransmissionTarget *childTransmissionTarget; + + // When + childTransmissionTarget = [parentTransmissionTarget transmissionTargetForToken:kMSACTestTransmissionToken2]; + [childTransmissionTarget trackEvent:event1 withProperties:properties]; + + // Then + XCTAssertEqualObjects(kMSACTestTransmissionToken2, childTransmissionTarget.transmissionTargetToken); + XCTAssertEqualObjects(childTransmissionTarget, parentTransmissionTarget.childTransmissionTargets[kMSACTestTransmissionToken2]); + + // When + MSACAnalyticsTransmissionTarget *childTransmissionTarget2 = + [parentTransmissionTarget transmissionTargetForToken:kMSACTestTransmissionToken2]; + [childTransmissionTarget2 trackEvent:event2 withProperties:properties]; + + // Then + XCTAssertEqualObjects(childTransmissionTarget, childTransmissionTarget2); + XCTAssertEqualObjects(childTransmissionTarget2, parentTransmissionTarget.childTransmissionTargets[kMSACTestTransmissionToken2]); + + // When + MSACAnalyticsTransmissionTarget *childTransmissionTarget3 = + [parentTransmissionTarget transmissionTargetForToken:kMSACTestTransmissionToken]; + [childTransmissionTarget3 trackEvent:event3 withProperties:properties]; + + // Then + XCTAssertNotEqualObjects(parentTransmissionTarget, childTransmissionTarget3); + XCTAssertEqualObjects(childTransmissionTarget3, parentTransmissionTarget.childTransmissionTargets[kMSACTestTransmissionToken]); + OCMVerify([self.analyticsClassMock trackEvent:event1 + withTypedProperties:emptyProperties + forTransmissionTarget:childTransmissionTarget + flags:MSACFlagsDefault]); + OCMVerify([self.analyticsClassMock trackEvent:event2 + withTypedProperties:emptyProperties + forTransmissionTarget:childTransmissionTarget2 + flags:MSACFlagsDefault]); + OCMVerify([self.analyticsClassMock trackEvent:event3 + withTypedProperties:emptyProperties + forTransmissionTarget:childTransmissionTarget3 + flags:MSACFlagsDefault]); +} + +- (void)testTransmissionTargetEnabledState { + + // If + NSDictionary *properties = @{@"prop1" : @"val1", @"prop2" : @"val2"}; + MSACEventProperties *expectedProperties = [MSACEventProperties new]; + for (NSString *key in properties.allKeys) { + [expectedProperties setString:properties[key] forKey:key]; + } + NSString *event1 = @"event1"; + NSString *event2 = @"event2"; + NSString *event3 = @"event3"; + NSString *event4 = @"event4"; + MSACAnalyticsTransmissionTarget *transmissionTarget, *transmissionTarget2; + OCMStub([self.analyticsClassMock canBeUsed]).andReturn(YES); + + // Events tracked when disabled mustn't be sent. + OCMReject([self.analyticsClassMock trackEvent:event2 + withProperties:properties + forTransmissionTarget:transmissionTarget + flags:MSACFlagsDefault]); + OCMReject([self.analyticsClassMock trackEvent:event3 + withProperties:properties + forTransmissionTarget:transmissionTarget2 + flags:MSACFlagsDefault]); + + // When + + // Target enabled by default. + transmissionTarget = [[MSACAnalyticsTransmissionTarget alloc] initWithTransmissionTargetToken:kMSACTestTransmissionToken + parentTarget:nil + channelGroup:self.channelGroupMock]; + [transmissionTarget setEnabled:YES]; + + // Then + XCTAssertTrue([transmissionTarget isEnabled]); + [transmissionTarget trackEvent:event1 withProperties:properties]; + + // When + + // Disabling, track event won't work. + [transmissionTarget setEnabled:NO]; + [transmissionTarget trackEvent:event2 withProperties:properties]; + + // Then + XCTAssertFalse([transmissionTarget isEnabled]); + + // When + + // Allocating a new object with the same token should return the enabled state + // for this token. + transmissionTarget2 = [[MSACAnalyticsTransmissionTarget alloc] initWithTransmissionTargetToken:kMSACTestTransmissionToken + parentTarget:nil + channelGroup:self.channelGroupMock]; + [transmissionTarget2 trackEvent:event3 withProperties:properties]; + + // Then + XCTAssertFalse([transmissionTarget2 isEnabled]); + + // When + + // Re-enabling + [transmissionTarget2 setEnabled:YES]; + [transmissionTarget2 trackEvent:event4 withProperties:properties]; + + // Then + XCTAssertTrue([transmissionTarget2 isEnabled]); + OCMVerify([self.analyticsClassMock trackEvent:event1 + withTypedProperties:expectedProperties + forTransmissionTarget:transmissionTarget + flags:MSACFlagsDefault]); + OCMVerify([self.analyticsClassMock trackEvent:event4 + withTypedProperties:expectedProperties + forTransmissionTarget:transmissionTarget2 + flags:MSACFlagsDefault]); +} + +- (void)testTransmissionTargetNestedEnabledState { + + // If + MSACAnalyticsTransmissionTarget *target = + [[MSACAnalyticsTransmissionTarget alloc] initWithTransmissionTargetToken:kMSACTestTransmissionToken + parentTarget:nil + channelGroup:self.channelGroupMock]; + + // When + + // Create a child while parent is enabled, child also enabled. + MSACAnalyticsTransmissionTarget *childTarget = [target transmissionTargetForToken:@"childTarget1-guid"]; + + // Then + XCTAssertTrue([childTarget isEnabled]); + + // If + MSACAnalyticsTransmissionTarget *subChildTarget = [childTarget transmissionTargetForToken:@"subChildTarget1-guid"]; + + // When + + // Disabling the parent disables its children. + [target setEnabled:NO]; + + // Then + XCTAssertFalse([target isEnabled]); + XCTAssertFalse([childTarget isEnabled]); + XCTAssertFalse([subChildTarget isEnabled]); + + // When + + // Enabling a child while parent is disabled won't work. + [childTarget setEnabled:YES]; + + // Then + XCTAssertFalse([target isEnabled]); + XCTAssertFalse([childTarget isEnabled]); + XCTAssertFalse([subChildTarget isEnabled]); + + // When + + // Adding another child, it's state should reflect its parent. + MSACAnalyticsTransmissionTarget *childTarget2 = [target transmissionTargetForToken:@"childTarget2-guid"]; + + // Then + XCTAssertFalse([target isEnabled]); + XCTAssertFalse([childTarget isEnabled]); + XCTAssertFalse([subChildTarget isEnabled]); + XCTAssertFalse([childTarget2 isEnabled]); + + // When + + // Enabling a parent enables its children. + [target setEnabled:YES]; + + // Then + XCTAssertTrue([target isEnabled]); + XCTAssertTrue([childTarget isEnabled]); + XCTAssertTrue([subChildTarget isEnabled]); + XCTAssertTrue([childTarget2 isEnabled]); + + // When + + // Disabling a child only disables its children. + [childTarget setEnabled:NO]; + + // Then + XCTAssertTrue([target isEnabled]); + XCTAssertFalse([childTarget isEnabled]); + XCTAssertFalse([subChildTarget isEnabled]); + XCTAssertTrue([childTarget2 isEnabled]); +} + +- (void)testLongListOfImmediateChildren { + + // If + short maxChildren = 50; + NSMutableArray *childrenTargets; + MSACAnalyticsTransmissionTarget *parentTarget = + [[MSACAnalyticsTransmissionTarget alloc] initWithTransmissionTargetToken:kMSACTestTransmissionToken + parentTarget:nil + channelGroup:self.channelGroupMock]; + for (short i = 1; i <= maxChildren; i++) { + [childrenTargets addObject:[parentTarget transmissionTargetForToken:[NSString stringWithFormat:@"Child%d-guid", i]]]; + } + + // When + [self measureBlock:^{ + [parentTarget setEnabled:NO]; + }]; + + // Then + XCTAssertFalse(parentTarget.isEnabled); + for (MSACAnalyticsTransmissionTarget *child in childrenTargets) { + XCTAssertFalse(child.isEnabled); + } +} + +- (void)testLongListOfSubChildren { + + // If + short maxSubChildren = 50; + NSMutableArray *childrenTargets; + MSACAnalyticsTransmissionTarget *parentTarget = + [[MSACAnalyticsTransmissionTarget alloc] initWithTransmissionTargetToken:kMSACTestTransmissionToken + parentTarget:nil + channelGroup:self.channelGroupMock]; + MSACAnalyticsTransmissionTarget *currentChildren = [parentTarget transmissionTargetForToken:@"Child1-guid"]; + [childrenTargets addObject:currentChildren]; + for (short i = 2; i <= maxSubChildren; i++) { + currentChildren = [currentChildren transmissionTargetForToken:[NSString stringWithFormat:@"SubChild%d-guid", i]]; + [childrenTargets addObject:currentChildren]; + } + + // When + [self measureBlock:^{ + [parentTarget setEnabled:NO]; + }]; + + // Then + XCTAssertFalse(parentTarget.isEnabled); + for (MSACAnalyticsTransmissionTarget *child in childrenTargets) { + XCTAssertFalse(child.isEnabled); + } +} + +- (void)testMergingEventPropertiesWithCommonPropertiesOnly { + + // If + + // Common properties only. + MSACAnalyticsTransmissionTarget *target = + [[MSACAnalyticsTransmissionTarget alloc] initWithTransmissionTargetToken:kMSACTestTransmissionToken + parentTarget:nil + channelGroup:self.channelGroupMock]; + NSString *eventName = @"event"; + NSString *propCommonKey = @"propCommonKey"; + NSString *propCommonValue = @"propCommonValue"; + NSString *propCommonDoubleKey = @"propCommonDoubleKey"; + double propCommonDoubleValue = 298374; + NSString *propCommonKey2 = @"sharedPropKey"; + NSDate *propCommonValue2 = [NSDate date]; + + [target.propertyConfigurator setEventPropertyString:propCommonValue forKey:propCommonKey]; + [target.propertyConfigurator setEventPropertyDouble:propCommonDoubleValue forKey:propCommonDoubleKey]; + [target.propertyConfigurator setEventPropertyDate:propCommonValue2 forKey:propCommonKey2]; + MSACEventProperties *expectedProperties = [MSACEventProperties new]; + [expectedProperties setString:propCommonValue forKey:propCommonKey]; + [expectedProperties setDate:propCommonValue2 forKey:propCommonKey2]; + [expectedProperties setDouble:propCommonDoubleValue forKey:propCommonDoubleKey]; + + // When + [target trackEvent:eventName]; + + // Then + OCMVerify([self.analyticsClassMock trackEvent:eventName + withTypedProperties:expectedProperties + forTransmissionTarget:target + flags:MSACFlagsDefault]); +} + +- (void)testMergingEventPropertiesWithCommonAndTrackEventProperties { + + // If + MSACAnalyticsTransmissionTarget *target = + [[MSACAnalyticsTransmissionTarget alloc] initWithTransmissionTargetToken:kMSACTestTransmissionToken + parentTarget:nil + channelGroup:self.channelGroupMock]; + + // Common properties. + NSString *eventName = @"event"; + NSString *propCommonKey = @"propCommonKey"; + NSString *propCommonValue = @"propCommonValue"; + NSString *propCommonDoubleKey = @"propCommonDoubleKey"; + double propCommonDoubleValue = 298374; + NSString *propCommonKey2 = @"sharedPropKey"; + NSDate *propCommonValue2 = [NSDate date]; + [target.propertyConfigurator setEventPropertyString:propCommonValue forKey:propCommonKey]; + [target.propertyConfigurator setEventPropertyDouble:propCommonDoubleValue forKey:propCommonDoubleKey]; + [target.propertyConfigurator setEventPropertyDate:propCommonValue2 forKey:propCommonKey2]; + + // Track event properties. + NSString *propTrackKey = @"propTrackKey"; + NSString *propTrackValue = @"propTrackValue"; + NSString *propTrackKey2 = @"sharedPropKey"; + NSString *propTrackValue2 = @"propTrackValue2"; + MSACEventProperties *expectedProperties = [MSACEventProperties new]; + [expectedProperties setString:propCommonValue forKey:propCommonKey]; + [expectedProperties setDate:propCommonValue2 forKey:propCommonKey2]; + [expectedProperties setDouble:propCommonDoubleValue forKey:propCommonDoubleKey]; + [expectedProperties setString:propTrackValue forKey:propTrackKey]; + [expectedProperties setString:propTrackValue2 forKey:propTrackKey2]; + + // When + [target trackEvent:eventName withProperties:@{propTrackKey : propTrackValue, propTrackKey2 : propTrackValue2}]; + + // Then + OCMVerify([self.analyticsClassMock trackEvent:eventName + withTypedProperties:expectedProperties + forTransmissionTarget:target + flags:MSACFlagsDefault]); +} + +- (void)testMergingEventPropertiesWithCommonAndTrackEventTypedProperties { + + // If + MSACAnalyticsTransmissionTarget *target = + [[MSACAnalyticsTransmissionTarget alloc] initWithTransmissionTargetToken:kMSACTestTransmissionToken + parentTarget:nil + channelGroup:self.channelGroupMock]; + + // Common properties. + NSString *eventName = @"event"; + NSString *propCommonKey = @"propCommonKey"; + NSString *propCommonValue = @"propCommonValue"; + NSString *propCommonDoubleKey = @"propCommonDoubleKey"; + double propCommonDoubleValue = 298374; + NSString *propCommonKey2 = @"sharedPropKey"; + NSDate *propCommonValue2 = [NSDate date]; + [target.propertyConfigurator setEventPropertyString:propCommonValue forKey:propCommonKey]; + [target.propertyConfigurator setEventPropertyDouble:propCommonDoubleValue forKey:propCommonDoubleKey]; + [target.propertyConfigurator setEventPropertyDate:propCommonValue2 forKey:propCommonKey2]; + + // Track event properties. + NSString *propTrackKey = @"propTrackKey"; + NSString *propTrackValue = @"propTrackValue"; + NSString *propTrackKey2 = @"sharedPropKey"; + BOOL propTrackValue2 = YES; + MSACEventProperties *expectedProperties = [MSACEventProperties new]; + [expectedProperties setString:propCommonValue forKey:propCommonKey]; + [expectedProperties setDate:propCommonValue2 forKey:propCommonKey2]; + [expectedProperties setDouble:propCommonDoubleValue forKey:propCommonDoubleKey]; + [expectedProperties setString:propTrackValue forKey:propTrackKey]; + [expectedProperties setBool:propTrackValue2 forKey:propTrackKey2]; + MSACEventProperties *trackEventProperties = [MSACEventProperties new]; + [trackEventProperties setString:propTrackValue forKey:propTrackKey]; + [trackEventProperties setBool:propTrackValue2 forKey:propTrackKey2]; + + // When + [target trackEvent:eventName withTypedProperties:trackEventProperties]; + + // Then + OCMVerify([self.analyticsClassMock trackEvent:eventName + withTypedProperties:expectedProperties + forTransmissionTarget:target + flags:MSACFlagsDefault]); +} + +- (void)testEventPropertiesCascading { + + // If + [MSACAnalytics resetSharedInstance]; + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([self.channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY]).andReturn(channelUnitMock); + [MSACAppCenter sharedInstance].sdkConfigured = YES; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:@"appsecret" + transmissionTargetToken:@"token" + fromApplication:YES]; + + // Prepare target instances. + MSACAnalyticsTransmissionTarget *grandParent = [MSACAnalytics transmissionTargetForToken:@"grand-parent"]; + MSACAnalyticsTransmissionTarget *parent = [grandParent transmissionTargetForToken:@"parent"]; + MSACAnalyticsTransmissionTarget *child = [parent transmissionTargetForToken:@"child"]; + + // Set properties to grand parent. + [grandParent.propertyConfigurator setEventPropertyString:@"1" forKey:@"a"]; + [grandParent.propertyConfigurator setEventPropertyString:@"2" forKey:@"b"]; + [grandParent.propertyConfigurator setEventPropertyString:@"3" forKey:@"c"]; + + // Override some properties. + [parent.propertyConfigurator setEventPropertyString:@"11" forKey:@"a"]; + [parent.propertyConfigurator setEventPropertyString:@"22" forKey:@"b"]; + + // Set a new property in parent. + [parent.propertyConfigurator setEventPropertyString:@"44" forKey:@"d"]; + + // Just to show we still get value from parent which is inherited from grand parent, if we remove an override. + [parent.propertyConfigurator setEventPropertyString:@"33" forKey:@"c"]; + [parent.propertyConfigurator removeEventPropertyForKey:@"c"]; + + // Override a property. + [child.propertyConfigurator setEventPropertyString:@"444" forKey:@"d"]; + + // Set new properties in child. + [child.propertyConfigurator setEventPropertyString:@"555" forKey:@"e"]; + [child.propertyConfigurator setEventPropertyString:@"666" forKey:@"f"]; + + // Track event in child. Override some properties in trackEvent. + NSMutableDictionary *properties = [NSMutableDictionary new]; + [properties setValue:@"6666" forKey:@"f"]; + [properties setValue:@"7777" forKey:@"g"]; + + // Mock channel group. + __block MSACEventLog *eventLog; + OCMStub([channelUnitMock enqueueItem:OCMOCK_ANY flags:MSACFlagsDefault]).andDo(^(NSInvocation *invocation) { + id log = nil; + [invocation getArgument:&log atIndex:2]; + eventLog = (MSACEventLog *)log; + }); + + // When + [child trackEvent:@"eventName" withProperties:properties]; + + // Then + XCTAssertNotNil(eventLog); + XCTAssertEqual([eventLog.typedProperties.properties count], 7); + XCTAssertEqualObjects(((MSACStringTypedProperty *)eventLog.typedProperties.properties[@"a"]).value, @"11"); + XCTAssertEqualObjects(((MSACStringTypedProperty *)eventLog.typedProperties.properties[@"b"]).value, @"22"); + XCTAssertEqualObjects(((MSACStringTypedProperty *)eventLog.typedProperties.properties[@"c"]).value, @"3"); + XCTAssertEqualObjects(((MSACStringTypedProperty *)eventLog.typedProperties.properties[@"d"]).value, @"444"); + XCTAssertEqualObjects(((MSACStringTypedProperty *)eventLog.typedProperties.properties[@"e"]).value, @"555"); + XCTAssertEqualObjects(((MSACStringTypedProperty *)eventLog.typedProperties.properties[@"f"]).value, @"6666"); + XCTAssertEqualObjects(((MSACStringTypedProperty *)eventLog.typedProperties.properties[@"g"]).value, @"7777"); +} + +- (void)testEventPropertiesCascadingWithTypes { + + // If + [MSACAnalytics resetSharedInstance]; + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([self.channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY]).andReturn(channelUnitMock); + [MSACAppCenter sharedInstance].sdkConfigured = YES; + [[MSACAnalytics sharedInstance] startWithChannelGroup:self.channelGroupMock + appSecret:@"appsecret" + transmissionTargetToken:@"token" + fromApplication:YES]; + + // Prepare target instances. + MSACAnalyticsTransmissionTarget *grandParent = [MSACAnalytics transmissionTargetForToken:@"grand-parent"]; + MSACAnalyticsTransmissionTarget *parent = [grandParent transmissionTargetForToken:@"parent"]; + MSACAnalyticsTransmissionTarget *child = [parent transmissionTargetForToken:@"child"]; + + // Set properties to grand parent. + [grandParent.propertyConfigurator setEventPropertyString:@"1" forKey:@"a"]; + [grandParent.propertyConfigurator setEventPropertyDouble:2.0 forKey:@"b"]; + [grandParent.propertyConfigurator setEventPropertyString:@"3" forKey:@"c"]; + + // Override some properties. + [parent.propertyConfigurator setEventPropertyInt64:11 forKey:@"a"]; + [parent.propertyConfigurator setEventPropertyString:@"22" forKey:@"b"]; + + // Set a new property in parent. + [parent.propertyConfigurator setEventPropertyInt64:44 forKey:@"d"]; + + // Just to show we still get value from parent which is inherited from grand parent, if we remove an override. + [parent.propertyConfigurator setEventPropertyString:@"33" forKey:@"c"]; + [parent.propertyConfigurator removeEventPropertyForKey:@"c"]; + + // Override a property. + [child.propertyConfigurator setEventPropertyBool:YES forKey:@"d"]; + + // Set new properties in child. + [child.propertyConfigurator setEventPropertyDouble:55.5 forKey:@"e"]; + [child.propertyConfigurator setEventPropertyString:@"666" forKey:@"f"]; + + // Track event in child. Override some properties in trackEvent. + MSACEventProperties *properties = [MSACEventProperties new]; + [properties setDate:[NSDate dateWithTimeIntervalSince1970:6666] forKey:@"f"]; + [properties setString:@"7777" forKey:@"g"]; + + // Mock channel group. + __block MSACEventLog *eventLog; + OCMStub([channelUnitMock enqueueItem:OCMOCK_ANY flags:MSACFlagsDefault]).andDo(^(NSInvocation *invocation) { + id log = nil; + [invocation getArgument:&log atIndex:2]; + eventLog = (MSACEventLog *)log; + }); + + // When + [child trackEvent:@"eventName" withTypedProperties:properties]; + + // Then + XCTAssertNotNil(eventLog); + XCTAssertEqual([eventLog.typedProperties.properties count], 7); + XCTAssertEqual(((MSACLongTypedProperty *)eventLog.typedProperties.properties[@"a"]).value, 11); + XCTAssertEqualObjects(((MSACStringTypedProperty *)eventLog.typedProperties.properties[@"b"]).value, @"22"); + XCTAssertEqualObjects(((MSACStringTypedProperty *)eventLog.typedProperties.properties[@"c"]).value, @"3"); + XCTAssertEqual(((MSACBooleanTypedProperty *)eventLog.typedProperties.properties[@"d"]).value, YES); + XCTAssertEqual(((MSACDoubleTypedProperty *)eventLog.typedProperties.properties[@"e"]).value, 55.5); + XCTAssertEqualObjects(((MSACDateTimeTypedProperty *)eventLog.typedProperties.properties[@"f"]).value, + [NSDate dateWithTimeIntervalSince1970:6666]); + XCTAssertEqualObjects(((MSACStringTypedProperty *)eventLog.typedProperties.properties[@"g"]).value, @"7777"); +} + +- (void)testAppExtensionCommonSchemaPropertiesWithoutOverriding { + + // If + + // Prepare target instance. + MSACAnalyticsTransmissionTarget *target = [MSACAnalytics transmissionTargetForToken:@"target"]; + + // Set a log. + MSACCommonSchemaLog *log = [MSACCommonSchemaLog new]; + log.ext = [MSACCSExtensions new]; + log.ext.appExt = [MSACAppExtension new]; + log.ext.appExt.ver = @"0.0.1"; + log.ext.appExt.name = @"baseAppName"; + log.ext.appExt.locale = @"en-us"; + + // When + [target.propertyConfigurator channel:nil prepareLog:log]; + + // Then + XCTAssertNil(target.propertyConfigurator.appVersion); + XCTAssertNil(target.propertyConfigurator.appName); + XCTAssertNil(target.propertyConfigurator.appLocale); + XCTAssertEqual(log.ext.appExt.ver, @"0.0.1"); + XCTAssertEqual(log.ext.appExt.name, @"baseAppName"); + XCTAssertEqual(log.ext.appExt.locale, @"en-us"); +} + +- (void)testOverridingDefaultCommonSchemaProperties { + + // If + + // Prepare target instances. + MSACAnalyticsTransmissionTarget *parent = [MSACAnalytics transmissionTargetForToken:@"parent"]; + MSACAnalyticsTransmissionTarget *child = [parent transmissionTargetForToken:@"child"]; + + // Set properties to grand parent. + [parent.propertyConfigurator setAppVersion:@"8.4.1"]; + [parent.propertyConfigurator setAppName:@"ParentAppName"]; + [parent.propertyConfigurator setAppLocale:@"en-us"]; + [parent.propertyConfigurator setUserId:@"c:bob"]; + + // Set a log with default values. + MSACCommonSchemaLog *log = [MSACCommonSchemaLog new]; + log.tag = child; + log.ext = [MSACCSExtensions new]; + log.ext.appExt = [MSACAppExtension new]; + log.ext.userExt = [MSACUserExtension new]; + [log addTransmissionTargetToken:@"parent"]; + [log addTransmissionTargetToken:@"child"]; + + // When + [child.propertyConfigurator channel:nil prepareLog:log]; + + // Then + XCTAssertEqual(log.ext.appExt.ver, parent.propertyConfigurator.appVersion); + XCTAssertEqual(log.ext.appExt.name, parent.propertyConfigurator.appName); + XCTAssertEqual(log.ext.appExt.locale, parent.propertyConfigurator.appLocale); + XCTAssertEqual(log.ext.userExt.localId, parent.propertyConfigurator.userId); +} + +- (void)testOverridingCommonSchemaProperties { + + // If + + // Prepare target instance. + MSACAnalyticsTransmissionTarget *target = [MSACAnalytics transmissionTargetForToken:@"target"]; + + // Set properties to the target. + [target.propertyConfigurator setAppVersion:@"8.4.1"]; + [target.propertyConfigurator setAppName:@"NewAppName"]; + [target.propertyConfigurator setAppLocale:@"en-us"]; + [target.propertyConfigurator setUserId:@"c:bob"]; + + // Set a log. + MSACCommonSchemaLog *log = [MSACCommonSchemaLog new]; + log.tag = target; + [log addTransmissionTargetToken:@"target"]; + log.ext = [MSACCSExtensions new]; + log.ext.appExt = [MSACAppExtension new]; + log.ext.appExt.ver = @"0.0.1"; + log.ext.appExt.name = @"baseAppName"; + log.ext.appExt.locale = @"zh-cn"; + log.ext.userExt = [MSACUserExtension new]; + log.ext.userExt.localId = @"c:alice"; + + // When + [target.propertyConfigurator channel:nil prepareLog:log]; + + // Then + XCTAssertEqual(log.ext.appExt.ver, target.propertyConfigurator.appVersion); + XCTAssertEqual(log.ext.appExt.name, target.propertyConfigurator.appName); + XCTAssertEqual(log.ext.appExt.locale, target.propertyConfigurator.appLocale); + XCTAssertEqual(log.ext.userExt.localId, target.propertyConfigurator.userId); +} + +- (void)testOverridingCommonSchemaPropertiesFromParent { + + // If + + // Prepare target instances. + MSACAnalyticsTransmissionTarget *parent = [MSACAnalytics transmissionTargetForToken:@"parent"]; + MSACAnalyticsTransmissionTarget *child = [parent transmissionTargetForToken:@"child"]; + + // Set properties to grand parent. + [parent.propertyConfigurator setAppVersion:@"8.4.1"]; + [parent.propertyConfigurator setAppName:@"ParentAppName"]; + [parent.propertyConfigurator setAppLocale:@"en-us"]; + [parent.propertyConfigurator setUserId:@"c:bob"]; + + // Set a log. + MSACCommonSchemaLog *log = [MSACCommonSchemaLog new]; + log.tag = child; + log.ext = [MSACCSExtensions new]; + log.ext.appExt = [MSACAppExtension new]; + log.ext.appExt.ver = @"0.0.1"; + log.ext.appExt.name = @"baseAppName"; + log.ext.appExt.locale = @"zh-cn"; + log.ext.userExt = [MSACUserExtension new]; + log.ext.userExt.localId = @"c:alice"; + [log addTransmissionTargetToken:@"parent"]; + [log addTransmissionTargetToken:@"child"]; + + // When + [child.propertyConfigurator channel:nil prepareLog:log]; + + // Then + XCTAssertEqualObjects(log.ext.appExt.ver, parent.propertyConfigurator.appVersion); + XCTAssertEqualObjects(log.ext.appExt.name, parent.propertyConfigurator.appName); + XCTAssertEqualObjects(log.ext.appExt.locale, parent.propertyConfigurator.appLocale); + XCTAssertEqualObjects(log.ext.userExt.localId, parent.propertyConfigurator.userId); +} + +- (void)testOverridingCommonSchemaPropertiesDoNothingWhenTargetIsDisabled { + + // If + + // Prepare target instances. + MSACAnalyticsTransmissionTarget *grandParent = [MSACAnalytics transmissionTargetForToken:@"grand-parent"]; + MSACAnalyticsTransmissionTarget *parent = [grandParent transmissionTargetForToken:@"parent"]; + MSACAnalyticsTransmissionTarget *child = [parent transmissionTargetForToken:@"child"]; + + // Set properties to grand parent. + [grandParent.propertyConfigurator setAppVersion:@"8.4.1"]; + [grandParent.propertyConfigurator setAppName:@"GrandParentAppName"]; + [grandParent.propertyConfigurator setAppLocale:@"en-us"]; + [grandParent.propertyConfigurator setUserId:@"c:alice"]; + + // Set common properties to child. + [child.propertyConfigurator setAppVersion:@"1.4.8"]; + [child.propertyConfigurator setAppName:@"ChildAppName"]; + [child.propertyConfigurator setAppLocale:@"fr-ca"]; + [child.propertyConfigurator setUserId:@"c:bob"]; + + // Set a log. + MSACCommonSchemaLog *log = [MSACCommonSchemaLog new]; + log.tag = child; + log.ext = [MSACCSExtensions new]; + log.ext.appExt = [MSACAppExtension new]; + log.ext.appExt.ver = @"0.0.1"; + log.ext.appExt.name = @"baseAppName"; + log.ext.appExt.locale = @"zh-cn"; + log.ext.userExt = [MSACUserExtension new]; + log.ext.userExt.localId = @"c:charlie"; + [log addTransmissionTargetToken:@"parent"]; + [log addTransmissionTargetToken:@"child"]; + [log addTransmissionTargetToken:@"grand-parent"]; + + [grandParent setEnabled:NO]; + + // When + [grandParent.propertyConfigurator channel:nil prepareLog:log]; + + // Then + XCTAssertEqual(log.ext.appExt.ver, @"0.0.1"); + XCTAssertEqual(log.ext.appExt.name, @"baseAppName"); + XCTAssertEqual(log.ext.appExt.locale, @"zh-cn"); + XCTAssertEqual(log.ext.userExt.localId, @"c:charlie"); + + // If + [child setEnabled:NO]; + + // When + [child.propertyConfigurator channel:nil prepareLog:log]; + + // Then + XCTAssertNotEqual(log.ext.appExt.ver, child.propertyConfigurator.appVersion); + XCTAssertNotEqual(log.ext.appExt.name, child.propertyConfigurator.appName); + XCTAssertNotEqual(log.ext.appExt.locale, child.propertyConfigurator.appLocale); + XCTAssertNotEqual(log.ext.userExt.localId, child.propertyConfigurator.userId); + + // If + + // Reset a log. + log = [MSACCommonSchemaLog new]; + log.tag = child; + log.ext = [MSACCSExtensions new]; + log.ext.appExt = [MSACAppExtension new]; + log.ext.appExt.ver = @"0.0.1"; + log.ext.appExt.name = @"baseAppName"; + log.ext.appExt.locale = @"zh-cn"; + log.ext.userExt = [MSACUserExtension new]; + log.ext.userExt.localId = @"c:charlie"; + [log addTransmissionTargetToken:@"parent"]; + [log addTransmissionTargetToken:@"child"]; + [log addTransmissionTargetToken:@"grand-parent"]; + + // When + [parent.propertyConfigurator channel:nil prepareLog:log]; + + // Then + XCTAssertEqual(log.ext.appExt.ver, @"0.0.1"); + XCTAssertEqual(log.ext.appExt.name, @"baseAppName"); + XCTAssertEqual(log.ext.appExt.locale, @"zh-cn"); + XCTAssertEqual(log.ext.userExt.localId, @"c:charlie"); +} + +- (void)testOverridingCommonSchemaPropertiesWithTwoChildrenUnderTheSameParent { + + // If + MSACAnalyticsTransmissionTarget *parent = [MSACAnalytics transmissionTargetForToken:@"parent"]; + MSACAnalyticsTransmissionTarget *child1 = [parent transmissionTargetForToken:@"child1"]; + MSACAnalyticsTransmissionTarget *child2 = [parent transmissionTargetForToken:@"child2"]; + + // Set properties to grand parent. + [parent.propertyConfigurator setAppVersion:@"8.4.1"]; + [parent.propertyConfigurator setAppName:@"ParentAppName"]; + [parent.propertyConfigurator setAppLocale:@"en-us"]; + [parent.propertyConfigurator setUserId:@"c:alice"]; + + // Set common properties to child1. + [child1.propertyConfigurator setAppVersion:@"1.4.8"]; + [child1.propertyConfigurator setAppName:@"Child1AppName"]; + [child1.propertyConfigurator setAppLocale:@"fr-ca"]; + [child1.propertyConfigurator setUserId:@"c:bob"]; + + // Parent log. + MSACCommonSchemaLog *parentLog = [MSACCommonSchemaLog new]; + parentLog.tag = parent; + parentLog.ext = [MSACCSExtensions new]; + parentLog.ext.appExt = [MSACAppExtension new]; + parentLog.ext.appExt.ver = @"0.0.1"; + parentLog.ext.appExt.name = @"base1AppName"; + parentLog.ext.appExt.locale = @"zh-cn"; + parentLog.ext.userExt = [MSACUserExtension new]; + parentLog.ext.userExt.localId = @"c:charlie"; + [parentLog addTransmissionTargetToken:@"parent"]; + + // Child1 log. + MSACCommonSchemaLog *child1Log = [MSACCommonSchemaLog new]; + child1Log.tag = child1; + child1Log.ext = [MSACCSExtensions new]; + child1Log.ext.appExt = [MSACAppExtension new]; + child1Log.ext.appExt.ver = @"0.0.1"; + child1Log.ext.appExt.name = @"base1AppName"; + child1Log.ext.appExt.locale = @"zh-cn"; + child1Log.ext.userExt = [MSACUserExtension new]; + child1Log.ext.userExt.localId = @"c:charlie"; + [child1Log addTransmissionTargetToken:@"child1"]; + + // Child2 log. + MSACCommonSchemaLog *child2Log = [MSACCommonSchemaLog new]; + child2Log.tag = child2; + child2Log.ext = [MSACCSExtensions new]; + child2Log.ext.appExt = [MSACAppExtension new]; + child2Log.ext.appExt.ver = @"0.0.2"; + child2Log.ext.appExt.name = @"base2AppName"; + child2Log.ext.appExt.locale = @"en-us"; + child2Log.ext.userExt = [MSACUserExtension new]; + child2Log.ext.userExt.localId = @"c:charlie"; + [child2Log addTransmissionTargetToken:@"child2"]; + + // When + [parent.propertyConfigurator channel:nil prepareLog:parentLog]; + [child1.propertyConfigurator channel:nil prepareLog:child1Log]; + [child2.propertyConfigurator channel:nil prepareLog:child2Log]; + + // Then + XCTAssertEqualObjects(parentLog.ext.appExt.ver, parent.propertyConfigurator.appVersion); + XCTAssertEqualObjects(parentLog.ext.appExt.name, parent.propertyConfigurator.appName); + XCTAssertEqualObjects(parentLog.ext.appExt.locale, parent.propertyConfigurator.appLocale); + XCTAssertEqualObjects(parentLog.ext.userExt.localId, parent.propertyConfigurator.userId); + XCTAssertEqualObjects(child1Log.ext.appExt.ver, child1.propertyConfigurator.appVersion); + XCTAssertEqualObjects(child1Log.ext.appExt.name, child1.propertyConfigurator.appName); + XCTAssertEqualObjects(child1Log.ext.appExt.locale, child1.propertyConfigurator.appLocale); + XCTAssertEqualObjects(child1Log.ext.userExt.localId, child1.propertyConfigurator.userId); + XCTAssertEqualObjects(child2Log.ext.appExt.ver, parent.propertyConfigurator.appVersion); + XCTAssertEqualObjects(child2Log.ext.appExt.name, parent.propertyConfigurator.appName); + XCTAssertEqualObjects(child2Log.ext.appExt.locale, parent.propertyConfigurator.appLocale); + XCTAssertEqualObjects(child2Log.ext.userExt.localId, parent.propertyConfigurator.userId); + XCTAssertNil(child2.propertyConfigurator.appVersion); + XCTAssertNil(child2.propertyConfigurator.appName); + XCTAssertNil(child2.propertyConfigurator.appLocale); + XCTAssertNil(child2.propertyConfigurator.userId); +} + +- (void)testOverridingInvalidUserId { + + // If + MSACAnalyticsTransmissionTarget *target = [MSACAnalytics transmissionTargetForToken:@"invalidUserAppIdTest"]; + + // Set invalid user identifier. + [target.propertyConfigurator setUserId:@"invalid:invalid"]; + + // Set a log. + MSACCommonSchemaLog *log = [MSACCommonSchemaLog new]; + log.tag = target; + [log addTransmissionTargetToken:@"invalidUserAppIdTest"]; + log.ext = [MSACCSExtensions new]; + log.ext.userExt = [MSACUserExtension new]; + + // When + [target.propertyConfigurator channel:nil prepareLog:log]; + + // Then + XCTAssertNil(log.ext.userExt.localId); +} + +- (void)testOverridingValidUserIdThenUnset { + + // If + MSACAnalyticsTransmissionTarget *target = [MSACAnalytics transmissionTargetForToken:@"unsetUserIdTest"]; + + // Set properties to the target. + [target.propertyConfigurator setUserId:@"c:alice"]; + + // Set a log. + MSACCommonSchemaLog *log = [MSACCommonSchemaLog new]; + log.tag = target; + [log addTransmissionTargetToken:@"unsetUserIdTest"]; + log.ext = [MSACCSExtensions new]; + log.ext.userExt = [MSACUserExtension new]; + + // When + [target.propertyConfigurator channel:nil prepareLog:log]; + + // Then + XCTAssertEqual(log.ext.userExt.localId, @"c:alice"); + + // If + + // Unset userId. + [target.propertyConfigurator setUserId:nil]; + + // Reset a log. + log = [MSACCommonSchemaLog new]; + log.tag = target; + [log addTransmissionTargetToken:@"target"]; + log.ext = [MSACCSExtensions new]; + log.ext.userExt = [MSACUserExtension new]; + + // When + [target.propertyConfigurator channel:nil prepareLog:log]; + + // Then + XCTAssertNil(log.ext.userExt.localId); +} + +- (void)testOverridingValidUserIdWithInvalidOne { + + // If + MSACAnalyticsTransmissionTarget *target = [MSACAnalytics transmissionTargetForToken:@"target"]; + + // Set valid userId. + [target.propertyConfigurator setUserId:@"c:alice"]; + + // Set a log. + MSACCommonSchemaLog *log = [MSACCommonSchemaLog new]; + log.tag = target; + [log addTransmissionTargetToken:@"target"]; + log.ext = [MSACCSExtensions new]; + log.ext.userExt = [MSACUserExtension new]; + + // When + [target.propertyConfigurator channel:nil prepareLog:log]; + + // Then + XCTAssertEqual(log.ext.userExt.localId, @"c:alice"); + + // If + + // Set invalid userId on existing target having a valid userId. + [target.propertyConfigurator setUserId:@"invalid:invalid"]; + + // Reset a log. + log = [MSACCommonSchemaLog new]; + log.tag = target; + [log addTransmissionTargetToken:@"target"]; + log.ext = [MSACCSExtensions new]; + log.ext.userExt = [MSACUserExtension new]; + + // When + [target.propertyConfigurator channel:nil prepareLog:log]; + + // Then the value did not change. + XCTAssertEqual(log.ext.userExt.localId, @"c:alice"); +} + +- (void)testAddAuthenticationProvider { + + // If + MSACAnalyticsAuthenticationProvider *provider = [[MSACAnalyticsAuthenticationProvider alloc] + initWithAuthenticationType:MSACAnalyticsAuthenticationTypeMsaCompact + ticketKey:@"ticketKey" + delegate:OCMProtocolMock(@protocol(MSACAnalyticsAuthenticationProviderDelegate))]; + + // When + [MSACAnalyticsTransmissionTarget addAuthenticationProvider:provider]; + + // Then + XCTAssertNotNil(MSACAnalyticsTransmissionTarget.authenticationProvider); + XCTAssertEqual(provider, MSACAnalyticsTransmissionTarget.authenticationProvider); + + // If + MSACAnalyticsAuthenticationProvider *provider2 = + [[MSACAnalyticsAuthenticationProvider alloc] initWithAuthenticationType:MSACAnalyticsAuthenticationTypeMsaDelegate + ticketKey:@"ticketKey2" + delegate:OCMOCK_ANY]; + + // When + dispatch_async(dispatch_get_main_queue(), ^{ + [MSACAnalyticsTransmissionTarget addAuthenticationProvider:provider2]; + }); + [MSACAnalyticsTransmissionTarget addAuthenticationProvider:provider]; + dispatch_async(dispatch_get_main_queue(), ^{ + [MSACAnalyticsTransmissionTarget addAuthenticationProvider:provider2]; + }); + + // Then + XCTAssertEqual(provider, MSACAnalyticsTransmissionTarget.authenticationProvider); +} + +- (void)testPauseSucceedsWhenTargetIsEnabled { + + // If + MSACAnalyticsTransmissionTarget *sut = [MSACAnalytics transmissionTargetForToken:kMSACTestTransmissionToken]; + + // When + [sut pause]; + + // Then + OCMVerify([self.analyticsClassMock pauseTransmissionTargetForToken:kMSACTestTransmissionToken]); +} + +- (void)testResumeSucceedsWhenTargetIsEnabled { + + // If + MSACAnalyticsTransmissionTarget *sut = [MSACAnalytics transmissionTargetForToken:kMSACTestTransmissionToken]; + + // When + [sut resume]; + + // Then + OCMVerify([self.analyticsClassMock resumeTransmissionTargetForToken:kMSACTestTransmissionToken]); +} + +- (void)testPauseDoesNotPauseWhenTargetIsDisabled { + + // If + MSACAnalyticsTransmissionTarget *sut = [MSACAnalytics transmissionTargetForToken:kMSACTestTransmissionToken]; + OCMStub([self.analyticsClassMock canBeUsed]).andReturn(YES); + + // Then + OCMReject([self.analyticsClassMock pauseTransmissionTargetForToken:kMSACTestTransmissionToken]); + + // When + [MSACAnalytics setEnabled:NO]; + [sut pause]; +} + +- (void)testResumeDoesNotResumeWhenTargetIsDisabled { + + // If + MSACAnalyticsTransmissionTarget *sut = [MSACAnalytics transmissionTargetForToken:kMSACTestTransmissionToken]; + OCMStub([self.analyticsClassMock canBeUsed]).andReturn(YES); + + // Then + OCMReject([self.analyticsClassMock resumeTransmissionTargetForToken:kMSACTestTransmissionToken]); + + // When + [sut setEnabled:NO]; + [sut resume]; +} + +- (void)testPausedAndDisabledTargetIsResumedWhenEnabled { + + // If + MSACAnalyticsTransmissionTarget *sut = [MSACAnalytics transmissionTargetForToken:kMSACTestTransmissionToken]; + [sut pause]; + [sut setEnabled:NO]; + + // When + [sut setEnabled:YES]; + + // Then + OCMVerify([self.analyticsClassMock resumeTransmissionTargetForToken:kMSACTestTransmissionToken]); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACEventLogTests.m b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACEventLogTests.m new file mode 100644 index 0000000000..79505be7b2 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACEventLogTests.m @@ -0,0 +1,782 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAbstractLogInternal.h" +#import "MSACAnalyticsConstants.h" +#import "MSACAppExtension.h" +#import "MSACCSData.h" +#import "MSACCSExtensions.h" +#import "MSACDeviceInternal.h" +#import "MSACEventLogPrivate.h" +#import "MSACEventPropertiesInternal.h" +#import "MSACLocExtension.h" +#import "MSACMetadataExtension.h" +#import "MSACNetExtension.h" +#import "MSACOSExtension.h" +#import "MSACProtocolExtension.h" +#import "MSACSDKExtension.h" +#import "MSACStringTypedProperty.h" +#import "MSACTestFrameworks.h" +#import "MSACUserExtension.h" +#import "MSACUserIdContext.h" +#import "MSACUtility+Date.h" +#import "MSACUtility.h" + +@interface MSACEventLogTests : XCTestCase + +@property(nonatomic) MSACEventLog *sut; + +@end + +@implementation MSACEventLogTests + +#pragma mark - Houskeeping + +- (void)setUp { + [super setUp]; + self.sut = [MSACEventLog new]; +} + +- (void)tearDown { + [super tearDown]; +} + +#pragma mark - Tests + +- (void)testSerializingEventToDictionaryWorks { + + // If + NSString *typeName = @"event"; + NSString *eventId = MSAC_UUID_STRING; + NSString *eventName = @"eventName"; + MSACDevice *device = [MSACDevice new]; + NSString *sessionId = @"1234567890"; + NSDictionary *properties = @{@"Key" : @"Value"}; + NSDate *timestamp = [NSDate date]; + + self.sut.eventId = eventId; + self.sut.name = eventName; + self.sut.device = device; + self.sut.timestamp = timestamp; + self.sut.sid = sessionId; + self.sut.properties = properties; + + // When + NSMutableDictionary *actual = [self.sut serializeToDictionary]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual[@"id"], equalTo(eventId)); + assertThat(actual[@"name"], equalTo(eventName)); + assertThat(actual[@"device"], notNilValue()); + assertThat(actual[@"sid"], equalTo(sessionId)); + assertThat(actual[@"type"], equalTo(typeName)); + assertThat(actual[@"properties"], equalTo(properties)); + assertThat(actual[@"device"], notNilValue()); + assertThat(actual[@"timestamp"], equalTo([MSACUtility dateToISO8601:timestamp])); +} + +- (void)testNSCodingSerializationAndDeserializationWorks { + + // If + NSString *typeName = @"event"; + NSString *eventId = MSAC_UUID_STRING; + NSString *eventName = @"eventName"; + MSACDevice *device = [MSACDevice new]; + NSString *sessionId = @"1234567890"; + NSDate *timestamp = [NSDate date]; + NSDictionary *properties = @{@"Key" : @"Value"}; + + self.sut.eventId = eventId; + self.sut.name = eventName; + self.sut.device = device; + self.sut.timestamp = timestamp; + self.sut.sid = sessionId; + self.sut.properties = properties; + + // When + NSData *serializedEvent = [MSACUtility archiveKeyedData:self.sut]; + id actual = [MSACUtility unarchiveKeyedData:serializedEvent]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual, instanceOf([MSACEventLog class])); + MSACEventLog *actualEvent = actual; + assertThat(actualEvent.name, equalTo(eventName)); + assertThat(actualEvent.eventId, equalTo(eventId)); + assertThat(actualEvent.device, notNilValue()); + assertThat(actualEvent.timestamp, equalTo(timestamp)); + assertThat(actualEvent.type, equalTo(typeName)); + assertThat(actualEvent.sid, equalTo(sessionId)); + assertThat(actualEvent.properties, equalTo(properties)); + XCTAssertTrue([self.sut isEqual:actualEvent]); +} + +- (void)testIsValid { + + // If + self.sut.device = OCMClassMock([MSACDevice class]); + OCMStub([self.sut.device isValid]).andReturn(YES); + self.sut.timestamp = [NSDate date]; + self.sut.sid = @"1234567890"; + + // Then + XCTAssertFalse([self.sut isValid]); + + // When + self.sut.eventId = MSAC_UUID_STRING; + + // Then + XCTAssertFalse([self.sut isValid]); + + // When + self.sut.name = @"eventName"; + + // Then + XCTAssertTrue([self.sut isValid]); +} + +- (void)testIsNotEqualToNil { + + // Then + XCTAssertFalse([self.sut isEqual:nil]); +} + +- (void)testConvertACPropertiesToCSPropertiesWhenACPropertiesNil { + + // If + MSACCommonSchemaLog *csLog = [MSACCommonSchemaLog new]; + csLog.data = [MSACCSData new]; + csLog.ext = [MSACCSExtensions new]; + csLog.ext.metadataExt = [MSACMetadataExtension new]; + + // When + [self.sut setPropertiesAndMetadataForCSLog:csLog]; + + // Then + XCTAssertNil(csLog.data.properties); + XCTAssertNil(csLog.ext.metadataExt.metadata); +} + +- (void)testConvertDateTimePropertyToCSProperty { + + // If + MSACCommonSchemaLog *csLog = [MSACCommonSchemaLog new]; + csLog.data = [MSACCSData new]; + csLog.ext = [MSACCSExtensions new]; + csLog.ext.metadataExt = [MSACMetadataExtension new]; + MSACEventProperties *acProperties = [MSACEventProperties new]; + NSDate *date = [NSDate dateWithTimeIntervalSince1970:10000]; + [acProperties setDate:date forKey:@"time"]; + self.sut.typedProperties = acProperties; + + // When + [self.sut setPropertiesAndMetadataForCSLog:csLog]; + + // Then + XCTAssertEqualObjects(csLog.data.properties[@"time"], [MSACUtility dateToISO8601:date]); + XCTAssertEqualObjects(csLog.ext.metadataExt.metadata[kMSACFieldDelimiter][@"time"], @(kMSACDateTimeMetadataTypeId)); +} + +- (void)testConvertLongPropertyToCSProperty { + + // If + MSACCommonSchemaLog *csLog = [MSACCommonSchemaLog new]; + csLog.data = [MSACCSData new]; + csLog.ext = [MSACCSExtensions new]; + csLog.ext.metadataExt = [MSACMetadataExtension new]; + MSACEventProperties *acProperties = [MSACEventProperties new]; + int64_t largeNumber = 1234567890; + [acProperties setInt64:largeNumber forKey:@"largeNumber"]; + self.sut.typedProperties = acProperties; + + // When + [self.sut setPropertiesAndMetadataForCSLog:csLog]; + + // Then + XCTAssertEqualObjects(csLog.data.properties[@"largeNumber"], @(largeNumber)); + XCTAssertEqualObjects(csLog.ext.metadataExt.metadata[kMSACFieldDelimiter][@"largeNumber"], @(kMSACLongMetadataTypeId)); +} + +- (void)testConvertDoublePropertyToCSProperty { + + // If + MSACCommonSchemaLog *csLog = [MSACCommonSchemaLog new]; + csLog.data = [MSACCSData new]; + csLog.ext = [MSACCSExtensions new]; + csLog.ext.metadataExt = [MSACMetadataExtension new]; + MSACEventProperties *acProperties = [MSACEventProperties new]; + double pi = 3.1415926; + [acProperties setDouble:pi forKey:@"pi"]; + self.sut.typedProperties = acProperties; + + // When + [self.sut setPropertiesAndMetadataForCSLog:csLog]; + + // Then + XCTAssertEqualObjects(csLog.data.properties[@"pi"], @(pi)); + XCTAssertEqualObjects(csLog.ext.metadataExt.metadata[kMSACFieldDelimiter][@"pi"], @(kMSACDoubleMetadataTypeId)); +} + +- (void)testConvertStringPropertyToCSProperty { + + // If + MSACCommonSchemaLog *csLog = [MSACCommonSchemaLog new]; + csLog.data = [MSACCSData new]; + csLog.ext = [MSACCSExtensions new]; + csLog.ext.metadataExt = [MSACMetadataExtension new]; + MSACEventProperties *acProperties = [MSACEventProperties new]; + NSString *stringValue = @"hello"; + [acProperties setString:stringValue forKey:@"text"]; + self.sut.typedProperties = acProperties; + + // When + [self.sut setPropertiesAndMetadataForCSLog:csLog]; + + // Then + XCTAssertEqualObjects(csLog.data.properties[@"text"], stringValue); + XCTAssertNil(csLog.ext.metadataExt.metadata); +} + +- (void)testConvertBooleanPropertyToCSProperty { + + // If + MSACCommonSchemaLog *csLog = [MSACCommonSchemaLog new]; + csLog.data = [MSACCSData new]; + csLog.ext = [MSACCSExtensions new]; + csLog.ext.metadataExt = [MSACMetadataExtension new]; + MSACEventProperties *acProperties = [MSACEventProperties new]; + BOOL boolValue = YES; + [acProperties setBool:boolValue forKey:@"BoolKey"]; + self.sut.typedProperties = acProperties; + + // When + [self.sut setPropertiesAndMetadataForCSLog:csLog]; + + // Then + XCTAssertEqualObjects(csLog.data.properties[@"BoolKey"], @(boolValue)); + XCTAssertNil(csLog.ext.metadataExt.metadata); +} + +- (void)testConvertACPropertiesToCSPropertiesWhenPropertiesAreNotNested { + + // If + MSACCommonSchemaLog *csLog = [MSACCommonSchemaLog new]; + csLog.data = [MSACCSData new]; + csLog.ext = [MSACCSExtensions new]; + csLog.ext.metadataExt = [MSACMetadataExtension new]; + MSACEventProperties *acProperties = [MSACEventProperties new]; + [acProperties setString:@"value" forKey:@"key"]; + [acProperties setString:@"value2" forKey:@"key2"]; + self.sut.typedProperties = acProperties; + NSDictionary *expectedProperties = @{@"key" : @"value", @"key2" : @"value2"}; + + // When + [self.sut setPropertiesAndMetadataForCSLog:csLog]; + + // Then + XCTAssertEqualObjects(csLog.data.properties, expectedProperties); +} + +- (void)testConvertACPropertiesToCSPropertiesWhenPropertiesAreNested { + + // If + MSACCommonSchemaLog *csLog = [MSACCommonSchemaLog new]; + csLog.data = [MSACCSData new]; + csLog.ext = [MSACCSExtensions new]; + csLog.ext.metadataExt = [MSACMetadataExtension new]; + MSACEventProperties *acProperties = [MSACEventProperties new]; + [acProperties setInt64:1 forKey:@"p.a"]; + [acProperties setDouble:2.0 forKey:@"p.b"]; + [acProperties setBool:YES forKey:@"p.c"]; + self.sut.typedProperties = acProperties; + NSDictionary *expectedProperties = @{@"p" : @{@"a" : @1, @"b" : @2.0, @"c" : @YES}}; + NSDictionary *expectedMetadata = @{@"f" : @{@"p" : @{@"f" : @{@"a" : @(kMSACLongMetadataTypeId), @"b" : @(kMSACDoubleMetadataTypeId)}}}}; + + // When + [self.sut setPropertiesAndMetadataForCSLog:csLog]; + + // Then + XCTAssertEqualObjects(csLog.data.properties, expectedProperties); + XCTAssertEqualObjects(csLog.ext.metadataExt.metadata, expectedMetadata); +} + +- (void)testConvertACPropertiesToCSPropertiesWhenPropertiesAreNestedWithSiblings { + + // If + MSACCommonSchemaLog *csLog = [MSACCommonSchemaLog new]; + csLog.data = [MSACCSData new]; + csLog.ext = [MSACCSExtensions new]; + csLog.ext.metadataExt = [MSACMetadataExtension new]; + MSACEventProperties *acProperties = [MSACEventProperties new]; + [acProperties setString:@"value" forKey:@"key"]; + [acProperties setString:@"1" forKey:@"nes.a"]; + [acProperties setString:@"2" forKey:@"nes.t.ed"]; + [acProperties setString:@"3" forKey:@"nes.t.ed2"]; + [acProperties setString:@"value2" forKey:@"key2"]; + self.sut.typedProperties = acProperties; + NSDictionary *expectedResult = @{@"key" : @"value", @"nes" : @{@"a" : @"1", @"t" : @{@"ed" : @"2", @"ed2" : @"3"}}, @"key2" : @"value2"}; + + // When + [self.sut setPropertiesAndMetadataForCSLog:csLog]; + + // Then + XCTAssertEqualObjects(csLog.data.properties, expectedResult); +} + +- (void)testPropertiesAreNotNestedWhenAtTheSameDepth { + + // If + MSACCommonSchemaLog *csLog = [MSACCommonSchemaLog new]; + csLog.data = [MSACCSData new]; + csLog.ext = [MSACCSExtensions new]; + csLog.ext.metadataExt = [MSACMetadataExtension new]; + MSACEventProperties *acProperties = [MSACEventProperties new]; + [acProperties setInt64:1 forKey:@"a.b"]; + [acProperties setDouble:2.2 forKey:@"b.c"]; + self.sut.typedProperties = acProperties; + NSDictionary *expectedProperties = @{@"a" : @{@"b" : @1}, @"b" : @{@"c" : @2.2}}; + NSDictionary *expectedMetadata = + @{@"f" : @{@"a" : @{@"f" : @{@"b" : @(kMSACLongMetadataTypeId)}}, @"b" : @{@"f" : @{@"c" : @(kMSACDoubleMetadataTypeId)}}}}; + + // When + [self.sut setPropertiesAndMetadataForCSLog:csLog]; + + // Then + XCTAssertEqualObjects(csLog.data.properties, expectedProperties); + XCTAssertEqualObjects(csLog.ext.metadataExt.metadata, expectedMetadata); +} + +- (void)testMetadataDoesNotCreateLevelsForPropertyWhenPropertyIsString { + + // If + MSACCommonSchemaLog *csLog = [MSACCommonSchemaLog new]; + csLog.data = [MSACCSData new]; + csLog.ext = [MSACCSExtensions new]; + csLog.ext.metadataExt = [MSACMetadataExtension new]; + MSACEventProperties *acProperties = [MSACEventProperties new]; + [acProperties setInt64:1 forKey:@"a.b"]; + [acProperties setString:@"2.2" forKey:@"b.c"]; + self.sut.typedProperties = acProperties; + NSDictionary *expectedProperties = @{@"a" : @{@"b" : @1}, @"b" : @{@"c" : @"2.2"}}; + NSDictionary *expectedMetadata = @{ + @"f" : @{ + @"a" : @{@"f" : @{@"b" : @(kMSACLongMetadataTypeId)}}, + } + }; + + // When + [self.sut setPropertiesAndMetadataForCSLog:csLog]; + + // Then + XCTAssertEqualObjects(csLog.data.properties, expectedProperties); + XCTAssertEqualObjects(csLog.ext.metadataExt.metadata, expectedMetadata); +} + +- (void)testOverridePropertiesWhenStringPropertyIsDeeper { + MSACCommonSchemaLog *csLog = [MSACCommonSchemaLog new]; + csLog.data = [MSACCSData new]; + csLog.ext = [MSACCSExtensions new]; + csLog.ext.metadataExt = [MSACMetadataExtension new]; + MSACEventProperties *acProperties = [MSACEventProperties new]; + + // set "a.b" property first, "a.b.c.d" property later. + [acProperties setDouble:1.4 forKey:@"a.b"]; + [acProperties setString:@"hello" forKey:@"a.b.c.d"]; + self.sut.typedProperties = acProperties; + [self setupAndAssert:csLog]; + + // reset + csLog = [MSACCommonSchemaLog new]; + csLog.data = [MSACCSData new]; + csLog.ext = [MSACCSExtensions new]; + csLog.ext.metadataExt = [MSACMetadataExtension new]; + acProperties = [MSACEventProperties new]; + + // set "a.b.c.d" property first, "a.b" property later. + [acProperties setString:@"hello" forKey:@"a.b.c.d"]; + [acProperties setDouble:1.4 forKey:@"a.b"]; + self.sut.typedProperties = acProperties; + [self setupAndAssert:csLog]; +} + +- (void)testConvertACPropertiesToCSPropertiesWithPartBData { + + // If + MSACCommonSchemaLog *csLog = [MSACCommonSchemaLog new]; + csLog.data = [MSACCSData new]; + csLog.ext = [MSACCSExtensions new]; + csLog.ext.metadataExt = [MSACMetadataExtension new]; + MSACEventProperties *acProperties = [MSACEventProperties new]; + [acProperties setInt64:1 forKey:@"p.a"]; + [acProperties setDouble:2.0 forKey:@"p.b"]; + [acProperties setBool:YES forKey:@"p.c"]; + [acProperties setInt64:6 forKey:@"baseData.long"]; + [acProperties setString:@"hello" forKey:@"baseData.string"]; + [acProperties setString:@"type" forKey:@"baseType"]; + self.sut.typedProperties = acProperties; + NSDictionary *expectedProperties = + @{@"p" : @{@"a" : @1, @"b" : @2.0, @"c" : @YES}, @"baseData" : @{@"long" : @6, @"string" : @"hello"}, @"baseType" : @"type"}; + NSDictionary *expectedMetadata = @{ + @"f" : @{ + @"p" : @{@"f" : @{@"a" : @(kMSACLongMetadataTypeId), @"b" : @(kMSACDoubleMetadataTypeId)}}, + @"baseData" : @{@"f" : @{@"long" : @(kMSACLongMetadataTypeId)}} + } + }; + + // When + [self.sut setPropertiesAndMetadataForCSLog:csLog]; + + // Then + XCTAssertEqualObjects(csLog.data.properties, expectedProperties); + XCTAssertEqualObjects(csLog.ext.metadataExt.metadata, expectedMetadata); +} + +- (void)setupAndAssert:(MSACCommonSchemaLog *)csLog { + + // If + NSDictionary *possibleProperties1 = @{@"a" : @{@"b" : @1.4}}; + NSDictionary *possibleProperties2 = @{@"a" : @{@"b" : @{@"c" : @{@"d" : @"hello"}}}}; + NSDictionary *possibleMetadata1 = @{@"f" : @{@"a" : @{@"f" : @{@"b" : @(kMSACDoubleMetadataTypeId)}}}}; + NSDictionary *possibleMetadata2 = nil; + + // When + [self.sut setPropertiesAndMetadataForCSLog:csLog]; + + // Then + XCTAssertEqual([csLog.data.properties count], 1); + + // Since there is no guarantee which property will overwrite the other, test both possibilities. + if ([csLog.data.properties isEqualToDictionary:possibleProperties1]) { + XCTAssertEqualObjects(csLog.ext.metadataExt.metadata, possibleMetadata1); + } else if ([csLog.data.properties isEqualToDictionary:possibleProperties2]) { + XCTAssertEqualObjects(csLog.ext.metadataExt.metadata, possibleMetadata2); + } else { + XCTFail(@"csLog.data.properties did not equal either expectation"); + } +} + +- (void)testOverridePropertiesWhenLongPropertyIsDeeper { + MSACCommonSchemaLog *csLog = [MSACCommonSchemaLog new]; + csLog.data = [MSACCSData new]; + csLog.ext = [MSACCSExtensions new]; + csLog.ext.metadataExt = [MSACMetadataExtension new]; + MSACEventProperties *acProperties = [MSACEventProperties new]; + + // set "a.b" property first, "a.b.c.d" property later. + [acProperties setString:@"hello" forKey:@"a.b"]; + [acProperties setInt64:249 forKey:@"a.b.c.d"]; + self.sut.typedProperties = acProperties; + [self setupPropertiesAndAssert:csLog]; + + // reset + csLog = [MSACCommonSchemaLog new]; + csLog.data = [MSACCSData new]; + csLog.ext = [MSACCSExtensions new]; + csLog.ext.metadataExt = [MSACMetadataExtension new]; + acProperties = [MSACEventProperties new]; + + // set "a.b.c.d" property first, "a.b" property later. + [acProperties setInt64:249 forKey:@"a.b.c.d"]; + [acProperties setString:@"hello" forKey:@"a.b"]; + self.sut.typedProperties = acProperties; + [self setupPropertiesAndAssert:csLog]; +} + +- (void)setupPropertiesAndAssert:(MSACCommonSchemaLog *)csLog { + + // If + NSDictionary *possibleProperties1 = @{@"a" : @{@"b" : @"hello"}}; + NSDictionary *possibleProperties2 = @{@"a" : @{@"b" : @{@"c" : @{@"d" : @249}}}}; + NSDictionary *possibleMetadata1 = nil; + NSDictionary *possibleMetadata2 = + @{@"f" : @{@"a" : @{@"f" : @{@"b" : @{@"f" : @{@"c" : @{@"f" : @{@"d" : @(kMSACLongMetadataTypeId)}}}}}}}}; + + // When + [self.sut setPropertiesAndMetadataForCSLog:csLog]; + + // Then + XCTAssertEqual([csLog.data.properties count], 1); + + // Since there is no guarantee which property will overwrite the other, test both possibilities. + if ([csLog.data.properties isEqualToDictionary:possibleProperties1]) { + XCTAssertEqualObjects(csLog.ext.metadataExt.metadata, possibleMetadata1); + } else if ([csLog.data.properties isEqualToDictionary:possibleProperties2]) { + XCTAssertEqualObjects(csLog.ext.metadataExt.metadata, possibleMetadata2); + } else { + XCTFail(@"csLog.data.properties did not equal either expectation"); + } +} + +- (void)testOverrideValueToValueProperties { + + // If + MSACCommonSchemaLog *csLog = [MSACCommonSchemaLog new]; + csLog.data = [MSACCSData new]; + csLog.ext = [MSACCSExtensions new]; + csLog.ext.metadataExt = [MSACMetadataExtension new]; + MSACEventProperties *acProperties = [MSACEventProperties new]; + [acProperties setInt64:123 forKey:@"a.b"]; + [acProperties setDouble:2.43 forKey:@"a.b"]; + self.sut.typedProperties = acProperties; + NSDictionary *possibleProperties1 = @{@"a" : @{@"b" : @123}}; + NSDictionary *possibleProperties2 = @{@"a" : @{@"b" : @2.43}}; + NSDictionary *possibleMetadata1 = @{@"f" : @{@"a" : @{@"f" : @{@"b" : @(kMSACLongMetadataTypeId)}}}}; + NSDictionary *possibleMetadata2 = @{@"f" : @{@"a" : @{@"f" : @{@"b" : @(kMSACDoubleMetadataTypeId)}}}}; + + // When + [self.sut setPropertiesAndMetadataForCSLog:csLog]; + + // Then + XCTAssertEqual([csLog.data.properties count], 1); + + // Since there is no guarantee which property will overwrite the other, test both possibilities. + if ([csLog.data.properties isEqualToDictionary:possibleProperties1]) { + XCTAssertEqualObjects(csLog.ext.metadataExt.metadata, possibleMetadata1); + } else if ([csLog.data.properties isEqualToDictionary:possibleProperties2]) { + XCTAssertEqualObjects(csLog.ext.metadataExt.metadata, possibleMetadata2); + } else { + XCTFail(@"csLog.data.properties did not equal either expectation"); + } +} + +- (void)testConversionWhenSiblingsHaveDifferentTypesAndOnlyOneNeedsMetadataAndTheyAreOneLevelDeep { + + // If + MSACCommonSchemaLog *csLog = [MSACCommonSchemaLog new]; + csLog.data = [MSACCSData new]; + csLog.ext = [MSACCSExtensions new]; + csLog.ext.metadataExt = [MSACMetadataExtension new]; + MSACEventProperties *acProperties = [MSACEventProperties new]; + [acProperties setString:@"hello" forKey:@"a.b"]; + [acProperties setInt64:123 forKey:@"a.c"]; + [acProperties setString:@"hello" forKey:@"a.e"]; + self.sut.typedProperties = acProperties; + NSDictionary *expectedProperties = @{@"a" : @{@"c" : @123, @"b" : @"hello", @"e" : @"hello"}}; + NSDictionary *expectedMetadata = @{@"f" : @{@"a" : @{@"f" : @{@"c" : @(kMSACLongMetadataTypeId)}}}}; + + // When + [self.sut setPropertiesAndMetadataForCSLog:csLog]; + + // Then + XCTAssertEqual([csLog.data.properties count], 1); + XCTAssertEqualObjects(csLog.data.properties, expectedProperties); + XCTAssertEqualObjects(csLog.ext.metadataExt.metadata, expectedMetadata); +} + +- (void)testToCommonSchemaLogForTargetToken { + + // If + NSString *targetToken = @"aTarget-Token"; + NSString *name = @"SolarEclipse"; + MSACEventProperties *eventProperties = [MSACEventProperties new]; + [eventProperties setString:@"hello" forKey:@"aStringValue"]; + [eventProperties setInt64:1234567890l forKey:@"aLongValue"]; + [eventProperties setDouble:3.14 forKey:@"aDoubleValue"]; + NSDate *date = [NSDate dateWithTimeIntervalSince1970:10000]; + NSString *dateString = [MSACUtility dateToISO8601:date]; + [eventProperties setDate:date forKey:@"aDateTimeValue"]; + [eventProperties setBool:YES forKey:@"aBooleanValue"]; + NSDictionary *expectedProperties = @{ + @"aStringValue" : @"hello", + @"aLongValue" : @1234567890, + @"aDoubleValue" : @3.14, + @"aDateTimeValue" : dateString, + @"aBooleanValue" : @YES + }; + NSDictionary *expectedMetadata = @{@"f" : @{@"aLongValue" : @4, @"aDoubleValue" : @6, @"aDateTimeValue" : @9}}; + NSDate *timestamp = [NSDate date]; + NSString *userId = @"alice"; + MSACDevice *device = [MSACDevice new]; + NSString *oemName = @"Peach"; + NSString *model = @"pPhone1,6"; + NSString *locale = @"en_US"; + NSString *osName = @"pOS"; + NSString *osVer = @"1.2.4"; + NSString *osBuild = @"2342EEWF"; + NSString *appNamespace = @"com.contoso.peach.app"; + NSString *appVersion = @"3.1.2"; + NSString *carrierName = @"P-Telecom"; + NSString *sdkVersion = @"1.0.0"; + device.oemName = oemName; + device.model = model; + device.locale = locale; + device.osName = osName; + device.osVersion = osVer; + device.osBuild = osBuild; + device.appNamespace = appNamespace; + device.appVersion = appVersion; + device.carrierName = carrierName; + device.sdkName = @"appcenter.ios"; + device.sdkVersion = sdkVersion; + device.timeZoneOffset = @(-420); + self.sut.device = device; + self.sut.timestamp = timestamp; + self.sut.name = name; + self.sut.typedProperties = eventProperties; + self.sut.tag = [NSObject new]; + self.sut.userId = userId; + + // When + MSACCommonSchemaLog *csLog = [self.sut toCommonSchemaLogForTargetToken:targetToken flags:MSACFlagsDefault]; + + // Then + XCTAssertEqualObjects(csLog.ver, kMSACCSVerValue); + XCTAssertEqualObjects(csLog.name, name); + XCTAssertEqualObjects(csLog.timestamp, timestamp); + XCTAssertEqualObjects(csLog.iKey, @"o:aTarget"); + XCTAssertEqualObjects(csLog.ext.protocolExt.devMake, oemName); + XCTAssertEqualObjects(csLog.ext.protocolExt.devModel, model); + XCTAssertEqualObjects(csLog.ext.userExt.localId, [MSACUserIdContext prefixedUserIdFromUserId:userId]); + XCTAssertEqualObjects(csLog.ext.appExt.locale, [[[NSBundle mainBundle] preferredLocalizations] firstObject]); + XCTAssertEqualObjects(csLog.ext.osExt.name, osName); + XCTAssertEqualObjects(csLog.ext.osExt.ver, @"Version 1.2.4 (Build 2342EEWF)"); + XCTAssertEqualObjects(csLog.ext.appExt.appId, @"I:com.contoso.peach.app"); + XCTAssertEqualObjects(csLog.ext.appExt.ver, device.appVersion); + XCTAssertEqualObjects(csLog.ext.netExt.provider, carrierName); + XCTAssertEqualObjects(csLog.ext.sdkExt.libVer, @"appcenter.ios-1.0.0"); + XCTAssertEqualObjects(csLog.ext.locExt.tz, @"-07:00"); + XCTAssertEqualObjects(csLog.data.properties, expectedProperties); + XCTAssertEqualObjects(csLog.ext.metadataExt.metadata, expectedMetadata); + XCTAssertEqualObjects(csLog.tag, self.sut.tag); +} + +- (void)testInvalidBaseType { + + // If + MSACCommonSchemaLog *csLog = [MSACCommonSchemaLog new]; + csLog.data = [MSACCSData new]; + csLog.ext = [MSACCSExtensions new]; + csLog.ext.metadataExt = [MSACMetadataExtension new]; + MSACEventProperties *properties = [MSACEventProperties new]; + [properties setInt64:1 forKey:@"baseType"]; + self.sut.typedProperties = properties; + + // When + [self.sut setPropertiesAndMetadataForCSLog:csLog]; + + // Then + XCTAssertEqual(0, csLog.data.properties.count); + XCTAssertNil(csLog.ext.metadataExt.metadata); +} + +- (void)testInvalidBaseData { + + // If + MSACCommonSchemaLog *csLog = [MSACCommonSchemaLog new]; + csLog.data = [MSACCSData new]; + csLog.ext = [MSACCSExtensions new]; + csLog.ext.metadataExt = [MSACMetadataExtension new]; + MSACEventProperties *properties = [MSACEventProperties new]; + [properties setInt64:1 forKey:@"baseData"]; + self.sut.typedProperties = properties; + + // When + [self.sut setPropertiesAndMetadataForCSLog:csLog]; + + // Then + XCTAssertEqual(0, csLog.data.properties.count); + XCTAssertNil(csLog.ext.metadataExt.metadata); +} + +- (void)testBaseDataMissing { + + // If + MSACCommonSchemaLog *csLog = [MSACCommonSchemaLog new]; + csLog.data = [MSACCSData new]; + csLog.ext = [MSACCSExtensions new]; + csLog.ext.metadataExt = [MSACMetadataExtension new]; + MSACEventProperties *properties = [MSACEventProperties new]; + [properties setString:@"type" forKey:@"baseType"]; + self.sut.typedProperties = properties; + + // When + [self.sut setPropertiesAndMetadataForCSLog:csLog]; + + // Then + XCTAssertEqual(0, csLog.data.properties.count); + XCTAssertNil(csLog.ext.metadataExt.metadata); +} + +- (void)testBaseTypeMissing { + + // If + MSACCommonSchemaLog *csLog = [MSACCommonSchemaLog new]; + csLog.data = [MSACCSData new]; + csLog.ext = [MSACCSExtensions new]; + csLog.ext.metadataExt = [MSACMetadataExtension new]; + MSACEventProperties *properties = [MSACEventProperties new]; + [properties setString:@"test" forKey:@"baseData.test"]; + self.sut.typedProperties = properties; + + // When + [self.sut setPropertiesAndMetadataForCSLog:csLog]; + + // Then + XCTAssertEqual(0, csLog.data.properties.count); + XCTAssertNil(csLog.ext.metadataExt.metadata); +} + +- (void)testInvalidBaseTypeRemovesBaseData { + + // If + MSACCommonSchemaLog *csLog = [MSACCommonSchemaLog new]; + csLog.data = [MSACCSData new]; + csLog.ext = [MSACCSExtensions new]; + csLog.ext.metadataExt = [MSACMetadataExtension new]; + MSACEventProperties *properties = [MSACEventProperties new]; + [properties setString:@"test" forKey:@"baseData.test"]; + [properties setInt64:23 forKey:@"baseType"]; + self.sut.typedProperties = properties; + + // When + [self.sut setPropertiesAndMetadataForCSLog:csLog]; + + // Then + XCTAssertEqual(0, csLog.data.properties.count); + XCTAssertNil(csLog.ext.metadataExt.metadata); +} + +- (void)testInvalidBaseDataRemovesBaseType { + + // If + MSACCommonSchemaLog *csLog = [MSACCommonSchemaLog new]; + csLog.data = [MSACCSData new]; + csLog.ext = [MSACCSExtensions new]; + csLog.ext.metadataExt = [MSACMetadataExtension new]; + MSACEventProperties *properties = [MSACEventProperties new]; + [properties setString:@"test" forKey:@"baseData"]; + [properties setString:@"type" forKey:@"baseType"]; + self.sut.typedProperties = properties; + + // When + [self.sut setPropertiesAndMetadataForCSLog:csLog]; + + // Then + XCTAssertEqual(0, csLog.data.properties.count); + XCTAssertNil(csLog.ext.metadataExt.metadata); +} + +- (void)testInvalidBaseTypeAsObjectOverridingValidBaseType { + + // If + MSACCommonSchemaLog *csLog = [MSACCommonSchemaLog new]; + csLog.data = [MSACCSData new]; + csLog.ext = [MSACCSExtensions new]; + csLog.ext.metadataExt = [MSACMetadataExtension new]; + MSACEventProperties *properties = [MSACEventProperties new]; + [properties setString:@"Some.Type" forKey:@"baseType"]; + [properties setString:@"invalid" forKey:@"baseType.something"]; + [properties setString:@"valid" forKey:@"baseData.something"]; + self.sut.typedProperties = properties; + NSDictionary *expectedProperties = @{@"baseType" : @"Some.Type", @"baseData" : @{@"something" : @"valid"}}; + + // When + [self.sut setPropertiesAndMetadataForCSLog:csLog]; + + // Then + XCTAssertEqualObjects(csLog.data.properties, expectedProperties); + XCTAssertNil(csLog.ext.metadataExt.metadata); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACEventPropertiesTests.m b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACEventPropertiesTests.m new file mode 100644 index 0000000000..240eeb8255 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACEventPropertiesTests.m @@ -0,0 +1,251 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACBooleanTypedProperty.h" +#import "MSACDateTimeTypedProperty.h" +#import "MSACDoubleTypedProperty.h" +#import "MSACEventProperties.h" +#import "MSACEventPropertiesInternal.h" +#import "MSACLongTypedProperty.h" +#import "MSACStringTypedProperty.h" +#import "MSACTestFrameworks.h" +#import "MSACUtility+Date.h" +#import "MSACUtility.h" + +@interface MSACEventPropertiesTests : XCTestCase + +@end + +@implementation MSACEventPropertiesTests + +- (void)testInitWithStringDictionaryWhenStringDictionaryHasValues { + + // If + NSDictionary *stringProperties = @{@"key1" : @"val1", @"key2" : @"val2"}; + + // When + MSACEventProperties *sut = [[MSACEventProperties alloc] initWithStringDictionary:stringProperties]; + + // Then + XCTAssertEqual([sut.properties count], 2); + for (NSString *propertyKey in stringProperties) { + XCTAssertTrue([sut.properties[propertyKey] isKindOfClass:[MSACStringTypedProperty class]]); + XCTAssertEqualObjects(stringProperties[propertyKey], ((MSACStringTypedProperty *)sut.properties[propertyKey]).value); + XCTAssertEqualObjects(propertyKey, sut.properties[propertyKey].name); + } +} + +- (void)testSetBoolForKey { + + // If + MSACEventProperties *sut = [MSACEventProperties new]; + BOOL value = YES; + NSString *key = @"key"; + + // When + [sut setBool:value forKey:key]; + + // Then + MSACBooleanTypedProperty *property = (MSACBooleanTypedProperty *)sut.properties[key]; + XCTAssertEqual(property.name, key); + XCTAssertEqual(property.value, value); +} + +- (void)testSetInt64ForKey { + + // If + MSACEventProperties *sut = [MSACEventProperties new]; + int64_t value = 10; + NSString *key = @"key"; + + // When + [sut setInt64:value forKey:key]; + + // Then + MSACLongTypedProperty *property = (MSACLongTypedProperty *)sut.properties[key]; + XCTAssertEqual(property.name, key); + XCTAssertEqual(property.value, value); +} + +- (void)testSetDoubleForKey { + + // If + MSACEventProperties *sut = [MSACEventProperties new]; + double value = 10.43e3; + NSString *key = @"key"; + + // When + [sut setDouble:value forKey:key]; + + // Then + MSACDoubleTypedProperty *property = (MSACDoubleTypedProperty *)sut.properties[key]; + XCTAssertEqual(property.name, key); + XCTAssertEqual(property.value, value); +} + +- (void)testSetDoubleForKeyWhenValueIsInfinity { + + // If + MSACEventProperties *sut = [MSACEventProperties new]; + + // When + [sut setDouble:INFINITY forKey:@"key"]; + + // Then + XCTAssertEqual([sut.properties count], 0); + + // When + [sut setDouble:-INFINITY forKey:@"key"]; + + // Then + XCTAssertEqual([sut.properties count], 0); +} + +- (void)testSetDoubleForKeyWhenValueIsNaN { + + // If + MSACEventProperties *sut = [MSACEventProperties new]; + + // When + [sut setDouble:NAN forKey:@"key"]; + + // Then + XCTAssertEqual([sut.properties count], 0); +} + +- (void)testSetStringForKey { + + // If + MSACEventProperties *sut = [MSACEventProperties new]; + NSString *value = @"value"; + NSString *key = @"key"; + + // When + [sut setString:value forKey:key]; + + // Then + MSACStringTypedProperty *property = (MSACStringTypedProperty *)sut.properties[key]; + XCTAssertEqual(property.name, key); + XCTAssertEqual(property.value, value); +} + +- (void)testSetDateForKey { + + // If + MSACEventProperties *sut = [MSACEventProperties new]; + NSDate *value = [NSDate new]; + NSString *key = @"key"; + + // When + [sut setDate:value forKey:key]; + + // Then + MSACDateTimeTypedProperty *property = (MSACDateTimeTypedProperty *)sut.properties[key]; + XCTAssertEqual(property.name, key); + XCTAssertEqual(property.value, value); +} + +- (void)testSerializeToArray { + + // If + MSACEventProperties *sut = [MSACEventProperties new]; + MSACTypedProperty *property = OCMPartialMock([MSACTypedProperty new]); + NSDictionary *serializedProperty = [NSDictionary new]; + OCMStub([property serializeToDictionary]).andReturn(serializedProperty); + NSString *propertyKey = @"key"; + sut.properties[propertyKey] = property; + + // When + NSArray *propertiesArray = [sut serializeToArray]; + + // Then + XCTAssertEqual([propertiesArray count], 1); + XCTAssertEqualObjects(propertiesArray[0], serializedProperty); +} + +- (void)testNSCodingSerializationAndDeserialization { + + // If + MSACEventProperties *sut = [MSACEventProperties new]; + [sut setString:@"stringVal" forKey:@"stringKey"]; + [sut setBool:YES forKey:@"boolKey"]; + [sut setDouble:1.4 forKey:@"doubleKey"]; + [sut setInt64:8589934592ll forKey:@"intKey"]; + [sut setDate:[NSDate new] forKey:@"dateKey"]; + + // When + NSData *serializedSut = [MSACUtility archiveKeyedData:sut]; + MSACEventProperties *deserializedSut = (MSACEventProperties *)[MSACUtility unarchiveKeyedData:serializedSut]; + + // Then + XCTAssertNotNil(deserializedSut); + XCTAssertTrue([deserializedSut isKindOfClass:[MSACEventProperties class]]); + for (NSString *key in sut.properties) { + MSACTypedProperty *sutProperty = sut.properties[key]; + MSACTypedProperty *deserializedSutProperty = deserializedSut.properties[key]; + XCTAssertEqualObjects(sutProperty.name, deserializedSutProperty.name); + XCTAssertEqualObjects(sutProperty.type, deserializedSutProperty.type); + if ([deserializedSutProperty isKindOfClass:[MSACStringTypedProperty class]]) { + MSACStringTypedProperty *deserializedProperty = (MSACStringTypedProperty *)deserializedSutProperty; + MSACStringTypedProperty *originalProperty = (MSACStringTypedProperty *)sutProperty; + XCTAssertEqualObjects(originalProperty.value, deserializedProperty.value); + } else if ([deserializedSutProperty isKindOfClass:[MSACBooleanTypedProperty class]]) { + MSACBooleanTypedProperty *deserializedProperty = (MSACBooleanTypedProperty *)deserializedSutProperty; + MSACBooleanTypedProperty *originalProperty = (MSACBooleanTypedProperty *)sutProperty; + XCTAssertEqual(originalProperty.value, deserializedProperty.value); + } else if ([deserializedSutProperty isKindOfClass:[MSACLongTypedProperty class]]) { + MSACLongTypedProperty *deserializedProperty = (MSACLongTypedProperty *)deserializedSutProperty; + MSACLongTypedProperty *originalProperty = (MSACLongTypedProperty *)sutProperty; + XCTAssertEqual(originalProperty.value, deserializedProperty.value); + } else if ([deserializedSutProperty isKindOfClass:[MSACDoubleTypedProperty class]]) { + MSACDoubleTypedProperty *deserializedProperty = (MSACDoubleTypedProperty *)deserializedSutProperty; + MSACDoubleTypedProperty *originalProperty = (MSACDoubleTypedProperty *)sutProperty; + XCTAssertEqual(originalProperty.value, deserializedProperty.value); + } else if ([deserializedSutProperty isKindOfClass:[MSACDateTimeTypedProperty class]]) { + MSACDateTimeTypedProperty *deserializedProperty = (MSACDateTimeTypedProperty *)deserializedSutProperty; + MSACDateTimeTypedProperty *originalProperty = (MSACDateTimeTypedProperty *)sutProperty; + NSString *originalDateString = [MSACUtility dateToISO8601:originalProperty.value]; + NSString *deserializedDateString = [MSACUtility dateToISO8601:deserializedProperty.value]; + XCTAssertEqualObjects(originalDateString, deserializedDateString); + } + } +} + +- (void)testIsEmptyReturnsTrueWhenContainsNoProperties { + + // If + MSACEventProperties *sut = [MSACEventProperties new]; + + // When + BOOL isEmpty = [sut isEmpty]; + + // Then + XCTAssertTrue(isEmpty); +} + +- (void)testIsNoWhenEqualsWrongClass { + + // If + NSObject *invalidEvent = [NSObject new]; + + // When + BOOL result = [MSACEventProperties isEqual:invalidEvent]; + + // Then + XCTAssertFalse(result); +} + +- (void)testIsEmptyReturnsFalseWhenContainsProperties { + + // If + MSACEventProperties *sut = [MSACEventProperties new]; + [sut setBool:YES forKey:@"key"]; + + // When + BOOL isEmpty = [sut isEmpty]; + + // Then + XCTAssertFalse(isEmpty); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACMockAnalyticsDelegate.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACMockAnalyticsDelegate.h new file mode 100644 index 0000000000..e28cc6d5c6 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACMockAnalyticsDelegate.h @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAnalyticsDelegate.h" + +@interface MSACMockAnalyticsDelegate : NSObject +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACMockAnalyticsDelegate.m b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACMockAnalyticsDelegate.m new file mode 100644 index 0000000000..ecf3f9a237 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACMockAnalyticsDelegate.m @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACMockAnalyticsDelegate.h" + +@implementation MSACMockAnalyticsDelegate { +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACPageLogTests.m b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACPageLogTests.m new file mode 100644 index 0000000000..5f9282900a --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACPageLogTests.m @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "AppCenter+Internal.h" +#import "MSACPageLog.h" +#import "MSACTestFrameworks.h" +#import "MSACUtility.h" + +@interface MSACPageLogTests : XCTestCase + +@property(nonatomic) MSACPageLog *sut; + +@end + +@implementation MSACPageLogTests + +#pragma mark - Houskeeping + +- (void)setUp { + [super setUp]; + self.sut = [MSACPageLog new]; +} + +- (void)tearDown { + [super tearDown]; +} + +#pragma mark - Tests + +- (void)testSerializingPageToDictionaryWorks { + + // If + NSString *typeName = @"page"; + NSString *pageName = @"pageName"; + MSACDevice *device = [MSACDevice new]; + NSString *sessionId = @"1234567890"; + NSDictionary *properties = @{@"Key" : @"Value"}; + NSDate *timestamp = [NSDate dateWithTimeIntervalSince1970:42]; + + self.sut.name = pageName; + self.sut.device = device; + self.sut.timestamp = timestamp; + self.sut.sid = sessionId; + self.sut.properties = properties; + + // When + NSMutableDictionary *actual = [self.sut serializeToDictionary]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual[@"name"], equalTo(pageName)); + assertThat(actual[@"device"], notNilValue()); + assertThat(actual[@"sid"], equalTo(sessionId)); + assertThat(actual[@"type"], equalTo(typeName)); + assertThat(actual[@"properties"], equalTo(properties)); + assertThat(actual[@"device"], notNilValue()); + assertThat(actual[@"timestamp"], equalTo(@"1970-01-01T00:00:42.000Z")); +} + +- (void)testNSCodingSerializationAndDeserializationWorks { + + // If + NSString *typeName = @"page"; + NSString *pageName = @"pageName"; + MSACDevice *device = [MSACDevice new]; + NSString *sessionId = @"1234567890"; + NSDate *timestamp = [NSDate dateWithTimeIntervalSince1970:42]; + NSDictionary *properties = @{@"Key" : @"Value"}; + + self.sut.name = pageName; + self.sut.device = device; + self.sut.timestamp = timestamp; + self.sut.sid = sessionId; + self.sut.properties = properties; + + // When + NSData *serializedEvent = [MSACUtility archiveKeyedData:self.sut]; + id actual = [MSACUtility unarchiveKeyedData:serializedEvent]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual, instanceOf([MSACPageLog class])); + MSACPageLog *actualPage = actual; + assertThat(actualPage.name, equalTo(pageName)); + assertThat(actualPage.device, notNilValue()); + assertThat(actualPage.timestamp, equalTo(timestamp)); + assertThat(actualPage.type, equalTo(typeName)); + assertThat(actualPage.sid, equalTo(sessionId)); + assertThat(actualPage.properties, equalTo(properties)); + XCTAssertTrue([self.sut isEqual:actualPage]); +} + +- (void)testIsValid { + + // If + self.sut.device = OCMClassMock([MSACDevice class]); + OCMStub([self.sut.device isValid]).andReturn(YES); + self.sut.timestamp = [NSDate dateWithTimeIntervalSince1970:42]; + self.sut.sid = @"1234567890"; + + // Then + XCTAssertFalse([self.sut isValid]); + + // When + self.sut.name = @"pageName"; + + // Then + XCTAssertTrue([self.sut isValid]); +} + +- (void)testIsNotEqualToNil { + + // Then + XCTAssertFalse([self.sut isEqual:nil]); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACPropertyConfiguratorTests.m b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACPropertyConfiguratorTests.m new file mode 100644 index 0000000000..911262cc56 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACPropertyConfiguratorTests.m @@ -0,0 +1,241 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAnalyticsTransmissionTargetInternal.h" +#import "MSACAppExtension.h" +#import "MSACBooleanTypedProperty.h" +#import "MSACCSExtensions.h" +#import "MSACChannelGroupProtocol.h" +#import "MSACCommonSchemaLog.h" +#import "MSACDateTimeTypedProperty.h" +#import "MSACDeviceExtension.h" +#import "MSACDoubleTypedProperty.h" +#import "MSACLongTypedProperty.h" +#import "MSACPropertyConfiguratorInternal.h" +#import "MSACPropertyConfiguratorPrivate.h" +#import "MSACStringTypedProperty.h" +#import "MSACTestFrameworks.h" +#import "MSACUserExtension.h" + +@interface MSACPropertyConfiguratorTests : XCTestCase + +@property(nonatomic) MSACPropertyConfigurator *sut; +@property(nonatomic) MSACAnalyticsTransmissionTarget *transmissionTarget; +@property(nonatomic) MSACAnalyticsTransmissionTarget *parentTarget; +@property(nonatomic) NSString *targetToken; + +@end + +@implementation MSACPropertyConfiguratorTests + +- (void)setUp { + [super setUp]; + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + self.targetToken = @"123"; + self.parentTarget = OCMPartialMock([[MSACAnalyticsTransmissionTarget alloc] initWithTransmissionTargetToken:@"456" + parentTarget:nil + channelGroup:channelGroupMock]); + self.transmissionTarget = OCMPartialMock([[MSACAnalyticsTransmissionTarget alloc] initWithTransmissionTargetToken:self.targetToken + parentTarget:self.parentTarget + channelGroup:channelGroupMock]); + OCMStub([self.transmissionTarget isEnabled]).andReturn(YES); + self.sut = [[MSACPropertyConfigurator alloc] initWithTransmissionTarget:self.transmissionTarget]; + OCMStub(self.transmissionTarget.propertyConfigurator).andReturn(self.sut); +} + +- (void)tearDown { + [super tearDown]; + self.sut = nil; +} + +- (void)testInitializationWorks { + + // Then + XCTAssertNotNil(self.sut); + XCTAssertNil(self.sut.deviceId); +} + +- (void)testCollectsDeviceIdWhenShouldCollectDeviceIdIsTrue { +#if !TARGET_OS_OSX && !TARGET_OS_MACCATALYST + + // If + NSUUID *fakeIdentifier = [[NSUUID alloc] initWithUUIDString:@"00000000-0000-0000-0000-000000000000"]; + NSString *expectedLocalId = [NSString stringWithFormat:@"i:%@", [fakeIdentifier UUIDString]]; + id deviceMock = OCMClassMock([UIDevice class]); + OCMStub([deviceMock identifierForVendor]).andReturn(fakeIdentifier); + OCMStub([deviceMock currentDevice]).andReturn(deviceMock); + MSACCSExtensions *extensions = [MSACCSExtensions new]; + extensions.deviceExt = OCMPartialMock([MSACDeviceExtension new]); + MSACCommonSchemaLog *mockLog = OCMPartialMock([MSACCommonSchemaLog new]); + mockLog.ext = extensions; + mockLog.tag = self.transmissionTarget; + [mockLog addTransmissionTargetToken:self.transmissionTarget.transmissionTargetToken]; + + // When + [self.sut collectDeviceId]; + [self.sut channel:OCMOCK_ANY prepareLog:mockLog]; + + // Then + OCMVerify([extensions.deviceExt setLocalId:expectedLocalId]); + + // Clean up. + [deviceMock stopMocking]; +#endif +} + +- (void)testDeviceIdDoesNotPropagate { + + // If + MSACCommonSchemaLog *mockLog = OCMPartialMock([MSACCommonSchemaLog new]); + mockLog.ext = [MSACCSExtensions new]; + mockLog.ext.deviceExt = OCMPartialMock([MSACDeviceExtension new]); + [mockLog addTransmissionTargetToken:self.transmissionTarget.transmissionTargetToken]; + [self.parentTarget.propertyConfigurator collectDeviceId]; + + // When + [self.sut channel:OCMOCK_ANY prepareLog:mockLog]; + + // Then + XCTAssertNil(self.sut.deviceId); +} + +- (void)testRemoveNonExistingEventProperty { + + // When + [self.sut removeEventPropertyForKey:@"APropKey"]; + + // Then + XCTAssertTrue([self.sut.eventProperties isEmpty]); +} + +- (void)testSetAndRemoveEventPropertiesWithNilKeys { + +// When +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + [self.sut removeEventPropertyForKey:nil]; + + // Then + XCTAssertTrue([self.sut.eventProperties isEmpty]); + + // When + [self.sut setEventPropertyString:@"val1" forKey:nil]; + [self.sut setEventPropertyDouble:234 forKey:nil]; + [self.sut setEventPropertyInt64:23 forKey:nil]; + [self.sut setEventPropertyBool:YES forKey:nil]; + [self.sut setEventPropertyDate:[NSDate new] forKey:nil]; +#pragma clang diagnostic pop + + // Then + XCTAssertTrue([self.sut.eventProperties isEmpty]); +} + +- (void)testSetEventPropertiesWithInvalidValues { + + // If + NSString *propStringKey = @"propString"; + NSString *propDateKey = @"propDate"; + NSString *propNanKey = @"propNan"; + NSString *propInfinityKey = @"propInfinity"; + +// When +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + [self.sut removeEventPropertyForKey:nil]; + + // Then + XCTAssertTrue([self.sut.eventProperties isEmpty]); + + // When + [self.sut setEventPropertyString:nil forKey:propStringKey]; + [self.sut setEventPropertyDate:nil forKey:propDateKey]; +#pragma clang diagnostic pop + [self.sut setEventPropertyDouble:INFINITY forKey:propInfinityKey]; + [self.sut setEventPropertyDouble:-INFINITY forKey:propInfinityKey]; + [self.sut setEventPropertyDouble:NAN forKey:propNanKey]; + + // Then + XCTAssertTrue([self.sut.eventProperties isEmpty]); +} + +- (void)testSetAndRemoveEventProperty { + + // If + NSString *propStringKey = @"propString"; + NSString *propStringValue = @"val1"; + NSString *propDateKey = @"propDate"; + NSDate *propDateValue = [NSDate date]; + NSString *propDoubleKey = @"propDouble"; + double propDoubleValue = 927398.82939; + NSString *propInt64Key = @"propInt64"; + int64_t propInt64Value = 5000000000; + NSString *propBoolKey = @"propBool"; + BOOL propBoolValue = YES; + + // When + // Set properties of all types. + [self.sut setEventPropertyString:propStringValue forKey:propStringKey]; + [self.sut setEventPropertyDate:propDateValue forKey:propDateKey]; + [self.sut setEventPropertyDouble:propDoubleValue forKey:propDoubleKey]; + [self.sut setEventPropertyInt64:propInt64Value forKey:propInt64Key]; + [self.sut setEventPropertyBool:propBoolValue forKey:propBoolKey]; + + // Then + XCTAssertEqual([self.sut.eventProperties.properties count], 5); + XCTAssertEqualObjects(((MSACStringTypedProperty *)(self.sut.eventProperties.properties[propStringKey])).value, propStringValue); + XCTAssertEqualObjects(((MSACDateTimeTypedProperty *)(self.sut.eventProperties.properties[propDateKey])).value, propDateValue); + XCTAssertEqual(((MSACDoubleTypedProperty *)(self.sut.eventProperties.properties[propDoubleKey])).value, propDoubleValue); + XCTAssertEqual(((MSACLongTypedProperty *)(self.sut.eventProperties.properties[propInt64Key])).value, propInt64Value); + XCTAssertEqual(((MSACBooleanTypedProperty *)(self.sut.eventProperties.properties[propBoolKey])).value, propBoolValue); + + // When + [self.sut removeEventPropertyForKey:propStringKey]; + + // Then + XCTAssertEqual([self.sut.eventProperties.properties count], 4); + XCTAssertEqualObjects(((MSACDateTimeTypedProperty *)(self.sut.eventProperties.properties[propDateKey])).value, propDateValue); + XCTAssertEqual(((MSACDoubleTypedProperty *)(self.sut.eventProperties.properties[propDoubleKey])).value, propDoubleValue); + XCTAssertEqual(((MSACLongTypedProperty *)(self.sut.eventProperties.properties[propInt64Key])).value, propInt64Value); + XCTAssertEqual(((MSACBooleanTypedProperty *)(self.sut.eventProperties.properties[propBoolKey])).value, propBoolValue); +} + +- (void)testPropertiesAreNotAppliedToLogsOfDifferentTagWithSameToken { + + // If + id channelMock = OCMProtocolMock(@protocol(MSACChannelProtocol)); + MSACCommonSchemaLog *csLog = [MSACCommonSchemaLog new]; + csLog.ext = [MSACCSExtensions new]; + csLog.ext.appExt = [MSACAppExtension new]; + csLog.ext.userExt = [MSACUserExtension new]; + [csLog addTransmissionTargetToken:self.targetToken]; + [self.sut setAppLocale:@"en-US"]; + [self.sut setAppVersion:@"1.0.0"]; + [self.sut setAppName:@"tim"]; + [self.sut setUserId:@"c:alice"]; + + // When + [self.sut channel:channelMock prepareLog:csLog]; + + // Then + XCTAssertNil(csLog.ext.appExt.ver); + XCTAssertNil(csLog.ext.appExt.locale); + XCTAssertNil(csLog.ext.appExt.name); + XCTAssertNil(csLog.ext.userExt.localId); +} + +- (void)testSetUserId { + + // When + [self.sut setUserId:@"alice"]; + + // Then + XCTAssertEqualObjects(self.sut.userId, @"c:alice"); + + // When + [self.sut setUserId:@"c:bob"]; + + // Then + XCTAssertEqualObjects(self.sut.userId, @"c:bob"); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACSessionTrackerTests.m b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACSessionTrackerTests.m new file mode 100644 index 0000000000..2c94a89056 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACSessionTrackerTests.m @@ -0,0 +1,321 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAnalytics.h" +#import "MSACLogWithProperties.h" +#import "MSACSessionContextPrivate.h" +#import "MSACSessionTrackerPrivate.h" +#import "MSACSessionTrackerUtil.h" +#import "MSACStartServiceLog.h" +#import "MSACStartSessionLog.h" +#import "MSACTestFrameworks.h" + +static NSTimeInterval const kMSACTestSessionTimeout = 1.5; + +@interface MSACSessionTrackerTests : XCTestCase + +@property(nonatomic) MSACSessionTracker *sut; +@property(nonatomic) id context; + +@end + +@implementation MSACSessionTrackerTests + +- (void)setUp { + [super setUp]; + + self.sut = [[MSACSessionTracker alloc] init]; + [self.sut setSessionTimeout:kMSACTestSessionTimeout]; + [self.sut start]; +} + +- (void)tearDown { + [super tearDown]; + [MSACSessionContext resetSharedInstance]; + + // This is required to remove observers in dealloc. + self.sut = nil; +} + +- (void)testSession { + + // When + [self.sut renewSessionId]; + NSString *expectedSid = [self.sut.context sessionId]; + + // Then + XCTAssertNotNil(expectedSid); + + // When + [self.sut renewSessionId]; + NSString *sid = [self.sut.context sessionId]; + + // Then + XCTAssertEqual(expectedSid, sid); +} + +// Apps is in foreground for longer than the timeout time, still same session +- (void)testLongForegroundSession { + + // If + [self.sut renewSessionId]; + NSString *expectedSid = [self.sut.context sessionId]; + + // Then + XCTAssertNotNil(expectedSid); + + // When + + // Mock a log creation + self.sut.lastCreatedLogTime = [NSDate date]; + + // Wait for longer than timeout in foreground + [NSThread sleepForTimeInterval:kMSACTestSessionTimeout + 1]; + + // Get a session + [self.sut renewSessionId]; + NSString *sid = [self.sut.context sessionId]; + + // Then + XCTAssertEqual(expectedSid, sid); +} + +- (void)testShortBackgroundSession { + + // If + [self.sut renewSessionId]; + NSString *expectedSid = [self.sut.context sessionId]; + + // Then + XCTAssertNotNil(expectedSid); + + // When + + // Mock a log creation + self.sut.lastCreatedLogTime = [NSDate date]; + + // Enter background + [MSACSessionTrackerUtil simulateDidEnterBackgroundNotification]; + + // Wait for shorter than the timeout time in background + [NSThread sleepForTimeInterval:kMSACTestSessionTimeout - 1]; + + // Enter foreground + [MSACSessionTrackerUtil simulateWillEnterForegroundNotification]; + + // Get a session + [self.sut renewSessionId]; + NSString *sid = [self.sut.context sessionId]; + + // Then + XCTAssertEqual(expectedSid, sid); +} + +- (void)testLongBackgroundSession { + + // If + [self.sut renewSessionId]; + NSString *expectedSid = [self.sut.context sessionId]; + + // Then + XCTAssertNotNil(expectedSid); + + // When + + // Mock a log creation + self.sut.lastCreatedLogTime = [NSDate date]; + + // Enter background + [MSACSessionTrackerUtil simulateDidEnterBackgroundNotification]; + + // Wait for longer than the timeout time in background + [NSThread sleepForTimeInterval:kMSACTestSessionTimeout + 1]; + + // Enter foreground + [MSACSessionTrackerUtil simulateWillEnterForegroundNotification]; + + // Get a session + [self.sut renewSessionId]; + NSString *sid = [self.sut.context sessionId]; + + // Then + XCTAssertNotEqual(expectedSid, sid); +} + +- (void)testLongBackgroundSessionWithSessionTrackingStopped { + + // If + [self.sut stop]; + + // When + + // Mock a log creation + self.sut.lastCreatedLogTime = [NSDate date]; + + // Get a session + [self.sut renewSessionId]; + NSString *expectedSid = [self.sut.context sessionId]; + + // Then + XCTAssertNil(expectedSid); + + // When + + // Enter background + [MSACSessionTrackerUtil simulateDidEnterBackgroundNotification]; + + // Wait for longer than the timeout time in background + [NSThread sleepForTimeInterval:kMSACTestSessionTimeout + 1]; + + [[NSNotificationCenter defaultCenter] +#if TARGET_OS_OSX + postNotificationName:NSApplicationWillBecomeActiveNotification +#else + postNotificationName:UIApplicationWillEnterForegroundNotification +#endif + object:self]; + + // Get a session + [self.sut renewSessionId]; + NSString *sid = [self.sut.context sessionId]; + + // Then + XCTAssertNil(sid); +} + +- (void)testTooLongInBackground { + + // If + [self.sut renewSessionId]; + NSString *expectedSid = [self.sut.context sessionId]; + + // Then + XCTAssertNotNil(expectedSid); + + // When + [MSACSessionTrackerUtil simulateWillEnterForegroundNotification]; + [NSThread sleepForTimeInterval:1]; + + // Enter background + [MSACSessionTrackerUtil simulateDidEnterBackgroundNotification]; + + // Mock a log creation while app is in background + self.sut.lastCreatedLogTime = [NSDate date]; + + // Wait for longer than timeout in background + [NSThread sleepForTimeInterval:kMSACTestSessionTimeout + 1]; + + // Get a session + [self.sut renewSessionId]; + NSString *sid = [self.sut.context sessionId]; + + // Then + XCTAssertNotNil(sid); + XCTAssertNotEqual(expectedSid, sid); +} + +- (void)testStartSessionOnStart { + + // Clean up session context and stop session tracker which is initialized in setUp. + [MSACSessionContext resetSharedInstance]; + [self.sut stop]; + + // If + id analyticsMock = OCMClassMock([MSACAnalytics class]); + OCMStub([analyticsMock isAvailable]).andReturn(YES); + OCMStub([analyticsMock sharedInstance]).andReturn(analyticsMock); + [self.sut setSessionTimeout:kMSACTestSessionTimeout]; + id delegateMock = OCMProtocolMock(@protocol(MSACSessionTrackerDelegate)); + self.sut.delegate = delegateMock; + + // When + [self.sut start]; + + // Then + OCMVerify([delegateMock sessionTracker:self.sut processLog:[OCMArg isKindOfClass:[MSACStartSessionLog class]]]); +} + +- (void)testStartSessionOnAppForegrounded { + + // If + id analyticsMock = OCMClassMock([MSACAnalytics class]); + OCMStub([analyticsMock isAvailable]).andReturn(YES); + OCMStub([analyticsMock sharedInstance]).andReturn(analyticsMock); + MSACSessionTracker *sut = [[MSACSessionTracker alloc] init]; + [sut setSessionTimeout:0]; + id delegateMock = OCMProtocolMock(@protocol(MSACSessionTrackerDelegate)); + [sut start]; + + // When + [MSACSessionTrackerUtil simulateDidEnterBackgroundNotification]; + [NSThread sleepForTimeInterval:0.1]; + sut.delegate = delegateMock; + [MSACSessionTrackerUtil simulateWillEnterForegroundNotification]; + + // Then + OCMVerify([delegateMock sessionTracker:sut processLog:[OCMArg isKindOfClass:[MSACStartSessionLog class]]]); +} + +- (void)testDidEnqueueLog { + + // When + MSACLogWithProperties *log = [MSACLogWithProperties new]; + + // Then + XCTAssertNil(log.sid); + XCTAssertNil(log.timestamp); + + // When + [self.sut channel:nil prepareLog:log]; + + // Then + XCTAssertNil(log.timestamp); + XCTAssertEqual(log.sid, [self.sut.context sessionId]); +} + +- (void)testNoStartSessionWithStartSessionLog { + + // When + MSACLogWithProperties *log = [MSACLogWithProperties new]; + + // Then + XCTAssertNil(log.sid); + XCTAssertNil(log.timestamp); + + // When + [self.sut channel:nil prepareLog:log]; + + // Then + XCTAssertNil(log.timestamp); + XCTAssertEqual(log.sid, [self.sut.context sessionId]); + + // If + MSACStartSessionLog *sessionLog = [MSACStartSessionLog new]; + + // Then + XCTAssertNil(sessionLog.sid); + XCTAssertNil(sessionLog.timestamp); + + // When + [self.sut channel:nil prepareLog:sessionLog]; + + // Then + XCTAssertNil(sessionLog.timestamp); + XCTAssertNil(sessionLog.sid); + + // If + MSACStartServiceLog *serviceLog = [MSACStartServiceLog new]; + + // Then + XCTAssertNil(serviceLog.sid); + XCTAssertNil(serviceLog.timestamp); + + // When + [self.sut channel:nil prepareLog:serviceLog]; + + // Then + XCTAssertNil(serviceLog.timestamp); + XCTAssertNil(serviceLog.sid); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACStartSessionLogTests.m b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACStartSessionLogTests.m new file mode 100644 index 0000000000..9273cef692 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/MSACStartSessionLogTests.m @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACStartSessionLog.h" +#import "MSACTestFrameworks.h" +#import "MSACUtility.h" + +@interface MSACStartSessionLogTests : XCTestCase + +@property(nonatomic) MSACStartSessionLog *sut; + +@end + +@implementation MSACStartSessionLogTests + +#pragma mark - Houskeeping + +- (void)setUp { + [super setUp]; + self.sut = [MSACStartSessionLog new]; +} + +- (void)tearDown { + [super tearDown]; +} + +#pragma mark - Tests + +- (void)testSerializingSessionToDictionaryWorks { + + // If + MSACDevice *device = [MSACDevice new]; + NSString *typeName = @"startSession"; + NSString *sessionId = @"1234567890"; + NSDate *timestamp = [NSDate dateWithTimeIntervalSince1970:42]; + + self.sut.device = device; + self.sut.timestamp = timestamp; + self.sut.sid = sessionId; + + // When + NSMutableDictionary *actual = [self.sut serializeToDictionary]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual[@"type"], equalTo(typeName)); + assertThat(actual[@"device"], notNilValue()); + assertThat(actual[@"timestamp"], equalTo(@"1970-01-01T00:00:42.000Z")); +} + +- (void)testNSCodingSerializationAndDeserializationWorks { + + // If + MSACDevice *device = [MSACDevice new]; + NSString *typeName = @"startSession"; + NSString *sessionId = @"1234567890"; + NSDate *timestamp = [NSDate dateWithTimeIntervalSince1970:42]; + + self.sut.device = device; + self.sut.timestamp = timestamp; + self.sut.sid = sessionId; + + // When + NSData *serializedEvent = [MSACUtility archiveKeyedData:self.sut]; + id actual = [MSACUtility unarchiveKeyedData:serializedEvent]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual, instanceOf([MSACStartSessionLog class])); + + MSACStartSessionLog *actualSession = actual; + assertThat(actualSession.device, notNilValue()); + assertThat(actualSession.timestamp, equalTo(timestamp)); + assertThat(actualSession.type, equalTo(typeName)); + assertThat(actualSession.sid, equalTo(sessionId)); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/Util/MSACSessionTrackerUtil.h b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/Util/MSACSessionTrackerUtil.h new file mode 100644 index 0000000000..1600392525 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/Util/MSACSessionTrackerUtil.h @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +@interface MSACSessionTrackerUtil : NSObject + ++ (void)simulateDidEnterBackgroundNotification; + ++ (void)simulateWillEnterForegroundNotification; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/Util/MSACSessionTrackerUtil.m b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/Util/MSACSessionTrackerUtil.m new file mode 100644 index 0000000000..ad0029bfec --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterAnalytics/AppCenterAnalyticsTests/Util/MSACSessionTrackerUtil.m @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#if TARGET_OS_OSX +#import +#else +#import +#endif + +#import "MSACSessionTrackerUtil.h" + +@implementation MSACSessionTrackerUtil + ++ (void)simulateDidEnterBackgroundNotification { + [[NSNotificationCenter defaultCenter] +#if TARGET_OS_OSX + postNotificationName:NSApplicationDidResignActiveNotification +#else + postNotificationName:UIApplicationDidEnterBackgroundNotification +#endif + object:self]; +} + ++ (void)simulateWillEnterForegroundNotification { + [[NSNotificationCenter defaultCenter] +#if TARGET_OS_OSX + postNotificationName:NSApplicationWillBecomeActiveNotification +#else + postNotificationName:UIApplicationWillEnterForegroundNotification +#endif + object:self]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes.xcodeproj/project.pbxproj b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..cd44657ef4 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes.xcodeproj/project.pbxproj @@ -0,0 +1,1325 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + C24C9D1E2472B4AA0072C35D /* libCrashReporter.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C24C9CEF2472B3C30072C35D /* libCrashReporter.a */; }; + C2FE827D23757CB7007DCD28 /* MSACApplicationForwarder.h in Headers */ = {isa = PBXBuildFile; fileRef = C2FE827A23757CB7007DCD28 /* MSACApplicationForwarder.h */; }; + C2FE828023757CB7007DCD28 /* MSACApplicationForwarder.m in Sources */ = {isa = PBXBuildFile; fileRef = C2FE827B23757CB7007DCD28 /* MSACApplicationForwarder.m */; }; + C9EBA96C230D35A800A20F0F /* MSACAbstractLog.h in Headers */ = {isa = PBXBuildFile; fileRef = 380B81331E8C565E001C76C9 /* MSACAbstractLog.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C9EBA96D230D35A800A20F0F /* MSACService.h in Headers */ = {isa = PBXBuildFile; fileRef = 04A082011F74BB6100DC776D /* MSACService.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C9EBA96E230D35A800A20F0F /* MSACServiceAbstract.h in Headers */ = {isa = PBXBuildFile; fileRef = 387C77061D6CC41100D68CC1 /* MSACServiceAbstract.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C9EBA971230D35BD00A20F0F /* MSACLogWithProperties.h in Headers */ = {isa = PBXBuildFile; fileRef = 380B81311E8C540E001C76C9 /* MSACLogWithProperties.h */; }; + C9EBA978230D35CF00A20F0F /* AppCenterCrashes.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E0401361D1C98690051BCFA /* AppCenterCrashes.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C9EBA979230D35CF00A20F0F /* MSACCrashes.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E04014F1D1C9A4F0051BCFA /* MSACCrashes.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C9EBA97A230D35CF00A20F0F /* MSACCrashesDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = B2DCB1351DAC03C400120F87 /* MSACCrashesDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C9EBA981230D35DD00A20F0F /* MSACCrashHandlerSetupDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 350B29E71F192554009B91CF /* MSACCrashHandlerSetupDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C9EBA982230D35DD00A20F0F /* MSACWrapperCrashesHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = 350B29E81F1929A1009B91CF /* MSACWrapperCrashesHelper.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C9EBA98F230D35ED00A20F0F /* MSACWrapperExceptionManagerInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 35B7D8791DE4CB6D00C846CD /* MSACWrapperExceptionManagerInternal.h */; }; + C9EBA990230D35ED00A20F0F /* MSACWrapperExceptionManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 35D504CD1DDD140500D58B40 /* MSACWrapperExceptionManager.h */; }; + C9EBA991230D35ED00A20F0F /* MSACCrashesInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 3515F9C01DD63EC9005E6E27 /* MSACCrashesInternal.h */; }; + C9EBA992230D35ED00A20F0F /* MSACCrashesCXXExceptionWrapperException.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E73FE711D4059E7008CDC15 /* MSACCrashesCXXExceptionWrapperException.h */; }; + C9EBA993230D35ED00A20F0F /* MSACCrashesCXXExceptionHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E73FE681D402F79008CDC15 /* MSACCrashesCXXExceptionHandler.h */; }; + C9EBA994230D35ED00A20F0F /* AppCenter+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E0401591D1C9D7F0051BCFA /* AppCenter+Internal.h */; }; + C9EBA997230D35F000A20F0F /* MSACCrashesPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E0401521D1C9A4F0051BCFA /* MSACCrashesPrivate.h */; }; + C9EBA998230D35FE00A20F0F /* MSACWrapperException.h in Headers */ = {isa = PBXBuildFile; fileRef = 353FD15E1F29209000E1DF78 /* MSACWrapperException.h */; }; + C9EBA999230D35FE00A20F0F /* MSACWrapperExceptionInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 353FD1601F29209000E1DF78 /* MSACWrapperExceptionInternal.h */; }; + C9EBA99A230D35FE00A20F0F /* MSACErrorAttachmentLogInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 382346411E8B3DAD001C3A76 /* MSACErrorAttachmentLogInternal.h */; }; + C9EBA99B230D35FE00A20F0F /* MSACException.h in Headers */ = {isa = PBXBuildFile; fileRef = 3507AE3A1DD14C240030878F /* MSACException.h */; }; + C9EBA99C230D35FE00A20F0F /* MSACStackFrame.h in Headers */ = {isa = PBXBuildFile; fileRef = 3507AE3B1DD14C240030878F /* MSACStackFrame.h */; }; + C9EBA99D230D35FE00A20F0F /* MSACAbstractErrorLog.h in Headers */ = {isa = PBXBuildFile; fileRef = B2CD3BF71D80EE49000A8A91 /* MSACAbstractErrorLog.h */; }; + C9EBA99E230D35FE00A20F0F /* MSACBinary.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E7D5C651D3E9321009EC9AC /* MSACBinary.h */; }; + C9EBA99F230D35FE00A20F0F /* MSACAppleErrorLog.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E7D5C691D3E9332009EC9AC /* MSACAppleErrorLog.h */; }; + C9EBA9A0230D35FE00A20F0F /* MSACHandledErrorLog.h in Headers */ = {isa = PBXBuildFile; fileRef = 922446821F621D4500E4034A /* MSACHandledErrorLog.h */; }; + C9EBA9A1230D35FE00A20F0F /* MSACThread.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E7D5C711D3E9381009EC9AC /* MSACThread.h */; }; + C9EBA9C3230D360700A20F0F /* MSACErrorReportPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = B2F120E61D657F4F0060DED7 /* MSACErrorReportPrivate.h */; }; + C9EBA9CD230D361800A20F0F /* MSACCrashesUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E73FE6D1D4032AB008CDC15 /* MSACCrashesUtil.h */; }; + C9EBA9CE230D361800A20F0F /* MSACCrashReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = 04311FFB1EE08885007054C5 /* MSACCrashReporter.h */; }; + C9EBA9CF230D361800A20F0F /* MSACErrorLogFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = B2F375071D41AD5100F07032 /* MSACErrorLogFormatter.h */; }; + C9EBA9D4230D361F00A20F0F /* MSACCrashesUtilPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 049553051EF19F9F0097E071 /* MSACCrashesUtilPrivate.h */; }; + C9EBA9D5230D361F00A20F0F /* MSACErrorLogFormatterPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = B24F3F101D93417B00827213 /* MSACErrorLogFormatterPrivate.h */; }; + C9EBA9DC230D362A00A20F0F /* MSACErrorAttachmentLog.h in Headers */ = {isa = PBXBuildFile; fileRef = 38BD86511E8499EF004E8D7A /* MSACErrorAttachmentLog.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C9EBA9DD230D362A00A20F0F /* MSACErrorAttachmentLog+Utility.h in Headers */ = {isa = PBXBuildFile; fileRef = 3858A2171E93F37E00535A69 /* MSACErrorAttachmentLog+Utility.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C9EBA9DE230D362A00A20F0F /* MSACErrorReport.h in Headers */ = {isa = PBXBuildFile; fileRef = B2F120DE1D657CF10060DED7 /* MSACErrorReport.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C9EBAAAF230D3A1D00A20F0F /* MSACCrashes.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6E0401501D1C9A4F0051BCFA /* MSACCrashes.mm */; }; + C9EBAAB0230D3A1D00A20F0F /* MSACWrapperCrashesHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 350B29F11F1D67EE009B91CF /* MSACWrapperCrashesHelper.m */; }; + C9EBAAB1230D3A1D00A20F0F /* MSACWrapperExceptionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 35D504CE1DDD140500D58B40 /* MSACWrapperExceptionManager.m */; }; + C9EBAAB2230D3A1D00A20F0F /* MSACCrashesCXXExceptionWrapperException.m in Sources */ = {isa = PBXBuildFile; fileRef = 6E73FE721D4059E7008CDC15 /* MSACCrashesCXXExceptionWrapperException.m */; }; + C9EBAAB3230D3A1D00A20F0F /* MSACCrashesCXXExceptionHandler.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6E73FE691D402F79008CDC15 /* MSACCrashesCXXExceptionHandler.mm */; }; + C9EBAAB4230D3A1D00A20F0F /* MSACWrapperException.m in Sources */ = {isa = PBXBuildFile; fileRef = 353FD15F1F29209000E1DF78 /* MSACWrapperException.m */; }; + C9EBAAB5230D3A1D00A20F0F /* MSACException.m in Sources */ = {isa = PBXBuildFile; fileRef = 6E7D5C6E1D3E9346009EC9AC /* MSACException.m */; }; + C9EBAAB6230D3A1D00A20F0F /* MSACStackFrame.m in Sources */ = {isa = PBXBuildFile; fileRef = 6E7D5C761D3E9395009EC9AC /* MSACStackFrame.m */; }; + C9EBAAB7230D3A1D00A20F0F /* MSACAbstractErrorLog.m in Sources */ = {isa = PBXBuildFile; fileRef = B2CD3BF81D80EE49000A8A91 /* MSACAbstractErrorLog.m */; }; + C9EBAAB8230D3A1D00A20F0F /* MSACBinary.m in Sources */ = {isa = PBXBuildFile; fileRef = 6E7D5C661D3E9321009EC9AC /* MSACBinary.m */; }; + C9EBAAB9230D3A1D00A20F0F /* MSACAppleErrorLog.m in Sources */ = {isa = PBXBuildFile; fileRef = 6E7D5C6A1D3E9332009EC9AC /* MSACAppleErrorLog.m */; }; + C9EBAABA230D3A1D00A20F0F /* MSACHandledErrorLog.m in Sources */ = {isa = PBXBuildFile; fileRef = 922446831F621F3A00E4034A /* MSACHandledErrorLog.m */; }; + C9EBAABB230D3A1D00A20F0F /* MSACThread.m in Sources */ = {isa = PBXBuildFile; fileRef = 6E7D5C721D3E9381009EC9AC /* MSACThread.m */; }; + C9EBAABC230D3A1D00A20F0F /* MSACCrashesUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 6E73FE6E1D4032AB008CDC15 /* MSACCrashesUtil.m */; }; + C9EBAABD230D3A1D00A20F0F /* MSACErrorLogFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = B2F375081D41AD5100F07032 /* MSACErrorLogFormatter.m */; }; + C9EBAABE230D3A1D00A20F0F /* MSACErrorAttachmentLog.m in Sources */ = {isa = PBXBuildFile; fileRef = 8024743A1EAE077800AEC284 /* MSACErrorAttachmentLog.m */; }; + C9EBAABF230D3A1D00A20F0F /* MSACErrorAttachmentLog+Utility.m in Sources */ = {isa = PBXBuildFile; fileRef = 3858A2191E93F3B400535A69 /* MSACErrorAttachmentLog+Utility.m */; }; + C9EBAAC0230D3A1D00A20F0F /* MSACErrorReport.m in Sources */ = {isa = PBXBuildFile; fileRef = B2F120DF1D657CF10060DED7 /* MSACErrorReport.m */; }; + C9EBAB31230D736F00A20F0F /* AppCenter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C9EBAB30230D736F00A20F0F /* AppCenter.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + C2392F5124642CEA00425640 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C2392F4724642CEA00425640 /* OCMock.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 030EF0A814632FD000B04273; + remoteInfo = OCMock; + }; + C2392F5324642CEA00425640 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C2392F4724642CEA00425640 /* OCMock.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 03565A3118F0566E003AE91E; + remoteInfo = OCMockTests; + }; + C2392F5524642CEA00425640 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C2392F4724642CEA00425640 /* OCMock.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 030EF0DC14632FF700B04273; + remoteInfo = OCMockLib; + }; + C2392F5724642CEA00425640 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C2392F4724642CEA00425640 /* OCMock.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = D31108AD1828DB8700737925; + remoteInfo = OCMockLibTests; + }; + C2392F5924642CEA00425640 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C2392F4724642CEA00425640 /* OCMock.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = F0B950F11B0080BE00942C38; + remoteInfo = "OCMock iOS"; + }; + C2392F5B24642CEA00425640 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C2392F4724642CEA00425640 /* OCMock.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 817EB1621BD765130047E85A; + remoteInfo = "OCMock tvOS"; + }; + C2392F5D24642CEA00425640 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C2392F4724642CEA00425640 /* OCMock.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 8DE97CA022B43EE60098C63F; + remoteInfo = "OCMock watchOS"; + }; + C24C9CEC2472B3C30072C35D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C24C9CD12472B3C30072C35D /* CrashReporter.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 8DC2EF5B0486A6940098B216; + remoteInfo = "CrashReporter-MacOSX"; + }; + C24C9CEE2472B3C30072C35D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C24C9CD12472B3C30072C35D /* CrashReporter.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 05E731F30EFA1AAB005EDFB7; + remoteInfo = "CrashReporter-MacOSX-Static"; + }; + C24C9CF02472B3C30072C35D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C24C9CD12472B3C30072C35D /* CrashReporter.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 058812B91040582D009128FB; + remoteInfo = "CrashReporter-iOS"; + }; + C24C9CF22472B3C30072C35D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C24C9CD12472B3C30072C35D /* CrashReporter.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 05CD31520EE936A9000FDE88; + remoteInfo = "CrashReporter-iOS-Device"; + }; + C24C9CF62472B3C30072C35D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C24C9CD12472B3C30072C35D /* CrashReporter.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 8064D8BB1C4D22E5005A8B4C; + remoteInfo = "CrashReporter-tvOS"; + }; + C24C9CF82472B3C30072C35D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C24C9CD12472B3C30072C35D /* CrashReporter.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 8064D81B1C4D22D8005A8B4C; + remoteInfo = "CrashReporter-tvOS-Device"; + }; + C24C9CFC2472B3C30072C35D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C24C9CD12472B3C30072C35D /* CrashReporter.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 05E731E30EFA1A3E005EDFB7; + remoteInfo = plcrashutil; + }; + C24C9CFE2472B3C30072C35D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C24C9CD12472B3C30072C35D /* CrashReporter.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 05CD32690EE93DC3000FDE88; + remoteInfo = "Tests-MacOSX"; + }; + C24C9D022472B3C30072C35D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C24C9CD12472B3C30072C35D /* CrashReporter.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 05CD33240EE94439000FDE88; + remoteInfo = "Tests-iOS-Device"; + }; + C24C9D062472B3C30072C35D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C24C9CD12472B3C30072C35D /* CrashReporter.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 8064D9951C4D27E2005A8B4C; + remoteInfo = "Tests-tvOS-Device"; + }; + C24C9D082472B3C30072C35D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C24C9CD12472B3C30072C35D /* CrashReporter.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 05F40CE70EF7AB80008050CF; + remoteInfo = DemoCrash; + }; + C24C9D0A2472B3C30072C35D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C24C9CD12472B3C30072C35D /* CrashReporter.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 052A45CF136353FB00987004; + remoteInfo = "DemoCrash-iOS-Device"; + }; + C24C9D0E2472B3C30072C35D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C24C9CD12472B3C30072C35D /* CrashReporter.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 8064D9A31C4D27E9005A8B4C; + remoteInfo = "DemoCrash-tvOS-Device"; + }; + C24C9D122472B3C30072C35D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C24C9CD12472B3C30072C35D /* CrashReporter.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 050DE24D0F61B80B00152ED3; + remoteInfo = "Fuzz Testing"; + }; + C24C9D192472B4870072C35D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C24C9CD12472B3C30072C35D /* CrashReporter.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = 05E731F20EFA1AAB005EDFB7; + remoteInfo = "CrashReporter macOS"; + }; + DFCB80522472D6770058D292 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DFCB80482472D6770058D292 /* OCHamcrest.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 087601E213440806001B439B; + remoteInfo = OCHamcrest; + }; + DFCB80542472D6770058D292 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DFCB80482472D6770058D292 /* OCHamcrest.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 087601F713440807001B439B; + remoteInfo = OCHamcrestTests; + }; + DFCB80562472D6770058D292 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DFCB80482472D6770058D292 /* OCHamcrest.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 081BEE621345979F003F846A; + remoteInfo = libochamcrest; + }; + DFCB80582472D6770058D292 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DFCB80482472D6770058D292 /* OCHamcrest.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 081BEE6C1345979F003F846A; + remoteInfo = libochamcrestTests; + }; + DFCB805A2472D6770058D292 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DFCB80482472D6770058D292 /* OCHamcrest.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 24C5BFE91C1777D900C2BAFD; + remoteInfo = "OCHamcrest-iOS"; + }; + DFCB805C2472D6770058D292 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DFCB80482472D6770058D292 /* OCHamcrest.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 24C5BFF61C17781400C2BAFD; + remoteInfo = "OCHamcrest-tvOS"; + }; + DFCB805E2472D6770058D292 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DFCB80482472D6770058D292 /* OCHamcrest.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 24C5C0031C17782500C2BAFD; + remoteInfo = "OCHamcrest-watchOS"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 041CFF911ECD00DC00B4654B /* Tests iOS.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = "Tests iOS.xcconfig"; path = "../../Config/Tests iOS.xcconfig"; sourceTree = ""; }; + 041CFF981ECD010000B4654B /* Tests macOS.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = "Tests macOS.xcconfig"; path = "../../Config/Tests macOS.xcconfig"; sourceTree = ""; }; + 04311FF41EE08697007054C5 /* MSACTestFrameworks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSACTestFrameworks.h; path = ../../AppCenter/AppCenterTests/Util/MSACTestFrameworks.h; sourceTree = ""; }; + 04311FFB1EE08885007054C5 /* MSACCrashReporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACCrashReporter.h; sourceTree = ""; }; + 043281611F74A665002F7205 /* MSACMockUserDefaults.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSACMockUserDefaults.h; path = ../../AppCenter/AppCenterTests/Util/MSACMockUserDefaults.h; sourceTree = ""; }; + 043281621F74A665002F7205 /* MSACMockUserDefaults.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MSACMockUserDefaults.m; path = ../../AppCenter/AppCenterTests/Util/MSACMockUserDefaults.m; sourceTree = ""; }; + 04545DCA227B605200A49E06 /* AppCenterCrashes.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppCenterCrashes.xcconfig; sourceTree = ""; }; + 0469D1B31F4E013C00A43A8E /* AppCenterCrashes Release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "AppCenterCrashes Release.xcconfig"; sourceTree = ""; }; + 0484DD5F1F3910EF0092B777 /* Tests tvOS.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = "Tests tvOS.xcconfig"; path = "../../Config/Tests tvOS.xcconfig"; sourceTree = ""; }; + 049553051EF19F9F0097E071 /* MSACCrashesUtilPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACCrashesUtilPrivate.h; sourceTree = ""; }; + 049BC8251ECE36D400FB6719 /* iOS.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = iOS.xcconfig; sourceTree = ""; }; + 049BC8261ECE370100FB6719 /* macOS.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = macOS.xcconfig; sourceTree = ""; }; + 04A082011F74BB6100DC776D /* MSACService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSACService.h; path = ../AppCenter/AppCenter/MSACService.h; sourceTree = ""; }; + 04EBBEEE1F01C1420006B8AE /* tvOS.modulemap */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = "sourcecode.module-map"; path = tvOS.modulemap; sourceTree = ""; }; + 04EBBEEF1F01C1420006B8AE /* tvOS.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = tvOS.xcconfig; sourceTree = ""; }; + 04ED31F21EAAD6070033BAAE /* iOS.modulemap */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = "sourcecode.module-map"; path = iOS.modulemap; sourceTree = ""; }; + 04ED31F51EAAD6070033BAAE /* macOS.modulemap */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = "sourcecode.module-map"; path = macOS.modulemap; sourceTree = ""; }; + 324F3BAB25478A210006E223 /* tvOS Universal.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "tvOS Universal.xcconfig"; path = "../../../Config/tvOS Universal.xcconfig"; sourceTree = ""; }; + 324F3BAC25478A220006E223 /* iOS Universal.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "iOS Universal.xcconfig"; path = "../../../Config/iOS Universal.xcconfig"; sourceTree = ""; }; + 324F3BAD25478A220006E223 /* XCFramework.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = XCFramework.xcconfig; path = ../../../Config/XCFramework.xcconfig; sourceTree = ""; }; + 3507AE3A1DD14C240030878F /* MSACException.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACException.h; sourceTree = ""; }; + 3507AE3B1DD14C240030878F /* MSACStackFrame.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACStackFrame.h; sourceTree = ""; }; + 350B29E71F192554009B91CF /* MSACCrashHandlerSetupDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACCrashHandlerSetupDelegate.h; sourceTree = ""; }; + 350B29E81F1929A1009B91CF /* MSACWrapperCrashesHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACWrapperCrashesHelper.h; sourceTree = ""; }; + 350B29F11F1D67EE009B91CF /* MSACWrapperCrashesHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACWrapperCrashesHelper.m; sourceTree = ""; }; + 350B29F31F1E6F1D009B91CF /* MSACWrapperExceptionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACWrapperExceptionTests.m; sourceTree = ""; }; + 3515F9C01DD63EC9005E6E27 /* MSACCrashesInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACCrashesInternal.h; sourceTree = ""; }; + 352B1D6E1F27C36300684A7F /* MSACWrapperCrashesHelperTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MSACWrapperCrashesHelperTests.mm; sourceTree = ""; }; + 353FD15E1F29209000E1DF78 /* MSACWrapperException.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACWrapperException.h; sourceTree = ""; }; + 353FD15F1F29209000E1DF78 /* MSACWrapperException.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACWrapperException.m; sourceTree = ""; }; + 353FD1601F29209000E1DF78 /* MSACWrapperExceptionInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACWrapperExceptionInternal.h; sourceTree = ""; }; + 35B7D8791DE4CB6D00C846CD /* MSACWrapperExceptionManagerInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACWrapperExceptionManagerInternal.h; sourceTree = ""; }; + 35D504CD1DDD140500D58B40 /* MSACWrapperExceptionManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACWrapperExceptionManager.h; sourceTree = ""; }; + 35D504CE1DDD140500D58B40 /* MSACWrapperExceptionManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACWrapperExceptionManager.m; sourceTree = ""; }; + 35EF18DF1DDBCF6C00731CA8 /* MSACWrapperExceptionManagerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACWrapperExceptionManagerTests.m; sourceTree = ""; }; + 380B81311E8C540E001C76C9 /* MSACLogWithProperties.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSACLogWithProperties.h; path = ../AppCenter/AppCenter/Model/MSACLogWithProperties.h; sourceTree = ""; }; + 380B81331E8C565E001C76C9 /* MSACAbstractLog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSACAbstractLog.h; path = ../AppCenter/AppCenter/Model/MSACAbstractLog.h; sourceTree = ""; }; + 382346411E8B3DAD001C3A76 /* MSACErrorAttachmentLogInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACErrorAttachmentLogInternal.h; sourceTree = ""; }; + 3858A2171E93F37E00535A69 /* MSACErrorAttachmentLog+Utility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MSACErrorAttachmentLog+Utility.h"; sourceTree = ""; }; + 3858A2191E93F3B400535A69 /* MSACErrorAttachmentLog+Utility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MSACErrorAttachmentLog+Utility.m"; sourceTree = ""; }; + 387C77061D6CC41100D68CC1 /* MSACServiceAbstract.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSACServiceAbstract.h; path = ../AppCenter/AppCenter/MSACServiceAbstract.h; sourceTree = ""; }; + 38BD86511E8499EF004E8D7A /* MSACErrorAttachmentLog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACErrorAttachmentLog.h; sourceTree = ""; }; + 59493B275715F01438B2E6FD /* MSACCrashesUtilTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACCrashesUtilTests.m; sourceTree = ""; }; + 6E0401361D1C98690051BCFA /* AppCenterCrashes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppCenterCrashes.h; sourceTree = ""; }; + 6E04014F1D1C9A4F0051BCFA /* MSACCrashes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACCrashes.h; sourceTree = ""; }; + 6E0401501D1C9A4F0051BCFA /* MSACCrashes.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = MSACCrashes.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 6E0401521D1C9A4F0051BCFA /* MSACCrashesPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACCrashesPrivate.h; sourceTree = ""; }; + 6E0401591D1C9D7F0051BCFA /* AppCenter+Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "AppCenter+Internal.h"; path = "../../../AppCenter/AppCenter/Internals/AppCenter+Internal.h"; sourceTree = ""; }; + 6E171AE51D22F781000DC480 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 6E37297F1D1DE93800F1E4AE /* AppCenterCrashes Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "AppCenterCrashes Debug.xcconfig"; sourceTree = ""; }; + 6E73FE681D402F79008CDC15 /* MSACCrashesCXXExceptionHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACCrashesCXXExceptionHandler.h; sourceTree = ""; }; + 6E73FE691D402F79008CDC15 /* MSACCrashesCXXExceptionHandler.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MSACCrashesCXXExceptionHandler.mm; sourceTree = ""; }; + 6E73FE6D1D4032AB008CDC15 /* MSACCrashesUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACCrashesUtil.h; sourceTree = ""; }; + 6E73FE6E1D4032AB008CDC15 /* MSACCrashesUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACCrashesUtil.m; sourceTree = ""; }; + 6E73FE711D4059E7008CDC15 /* MSACCrashesCXXExceptionWrapperException.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACCrashesCXXExceptionWrapperException.h; sourceTree = ""; }; + 6E73FE721D4059E7008CDC15 /* MSACCrashesCXXExceptionWrapperException.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACCrashesCXXExceptionWrapperException.m; sourceTree = ""; }; + 6E7D5C651D3E9321009EC9AC /* MSACBinary.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACBinary.h; sourceTree = ""; }; + 6E7D5C661D3E9321009EC9AC /* MSACBinary.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACBinary.m; sourceTree = ""; }; + 6E7D5C691D3E9332009EC9AC /* MSACAppleErrorLog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACAppleErrorLog.h; sourceTree = ""; }; + 6E7D5C6A1D3E9332009EC9AC /* MSACAppleErrorLog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACAppleErrorLog.m; sourceTree = ""; }; + 6E7D5C6E1D3E9346009EC9AC /* MSACException.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACException.m; sourceTree = ""; }; + 6E7D5C711D3E9381009EC9AC /* MSACThread.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACThread.h; sourceTree = ""; }; + 6E7D5C721D3E9381009EC9AC /* MSACThread.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACThread.m; sourceTree = ""; }; + 6E7D5C761D3E9395009EC9AC /* MSACStackFrame.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACStackFrame.m; sourceTree = ""; }; + 6E7D5C7F1D3EAEB5009EC9AC /* MSACBinaryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACBinaryTests.m; sourceTree = ""; }; + 6E7D5C811D3EC06C009EC9AC /* MSACConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSACConstants.h; path = ../../AppCenter/AppCenter/MSACConstants.h; sourceTree = ""; }; + 6E7D5C831D3EC0F7009EC9AC /* MSACThreadTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACThreadTests.m; sourceTree = ""; }; + 6E7D5C861D3EC332009EC9AC /* MSACStackFrameTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACStackFrameTests.m; sourceTree = ""; }; + 6E7D5C891D3EC504009EC9AC /* MSACExceptionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACExceptionTests.m; sourceTree = ""; }; + 6EC99A2D1D4166C50016C325 /* MSACCrashesTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = MSACCrashesTests.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 6EC99A301D416CCF0016C325 /* live_report_empty.plcrash */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = live_report_empty.plcrash; sourceTree = ""; }; + 6EC99A311D416CCF0016C325 /* live_report_exception.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = live_report_exception.plcrash; sourceTree = ""; }; + 6EC99A321D416CCF0016C325 /* live_report_exception_marketing.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = live_report_exception_marketing.plcrash; sourceTree = ""; }; + 6EC99A331D416CCF0016C325 /* live_report_signal.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = live_report_signal.plcrash; sourceTree = ""; }; + 6EC99A341D416CCF0016C325 /* live_report_signal_marketing.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = live_report_signal_marketing.plcrash; sourceTree = ""; }; + 6EC99A351D416CCF0016C325 /* live_report_xamarin.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = live_report_xamarin.plcrash; sourceTree = ""; }; + 8024743A1EAE077800AEC284 /* MSACErrorAttachmentLog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACErrorAttachmentLog.m; sourceTree = ""; }; + 80955E17218B497200CD59A1 /* macOS_report_write_to_readonly_page.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = macOS_report_write_to_readonly_page.plcrash; sourceTree = ""; }; + 80955E18218B497200CD59A1 /* macOS_report_objc_access_non_object_as_object.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = macOS_report_objc_access_non_object_as_object.plcrash; sourceTree = ""; }; + 80955E19218B497300CD59A1 /* macOS_report_execute_privileged_instruction.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = macOS_report_execute_privileged_instruction.plcrash; sourceTree = ""; }; + 80955E1A218B497300CD59A1 /* macOS_report_corrupt_objc_runtime_structure.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = macOS_report_corrupt_objc_runtime_structure.plcrash; sourceTree = ""; }; + 80955E1B218B497300CD59A1 /* macOS_report_throw_cpp_exception.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = macOS_report_throw_cpp_exception.plcrash; sourceTree = ""; }; + 80955E1C218B497300CD59A1 /* macOS_report_jump_into_nx_page.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = macOS_report_jump_into_nx_page.plcrash; sourceTree = ""; }; + 80955E1D218B497300CD59A1 /* macOS_report_objc_crash_inside_msgsend.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = macOS_report_objc_crash_inside_msgsend.plcrash; sourceTree = ""; }; + 80955E1E218B497300CD59A1 /* macOS_report_builtin_trap.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = macOS_report_builtin_trap.plcrash; sourceTree = ""; }; + 80955E1F218B497300CD59A1 /* macOS_report_dwarf_unwinding.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = macOS_report_dwarf_unwinding.plcrash; sourceTree = ""; }; + 80955E20218B497300CD59A1 /* macOS_report_dereference_bad_pointer.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = macOS_report_dereference_bad_pointer.plcrash; sourceTree = ""; }; + 80955E21218B497300CD59A1 /* macOS_report_dereference_null_pointer.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = macOS_report_dereference_null_pointer.plcrash; sourceTree = ""; }; + 80955E22218B497300CD59A1 /* macOS_report_abort.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = macOS_report_abort.plcrash; sourceTree = ""; }; + 80955E23218B497300CD59A1 /* macOS_report_smash_the_bottom_of_the_stack.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = macOS_report_smash_the_bottom_of_the_stack.plcrash; sourceTree = ""; }; + 80955E24218B497400CD59A1 /* macOS_report_corrupt_malloc_internal_info.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = macOS_report_corrupt_malloc_internal_info.plcrash; sourceTree = ""; }; + 80955E25218B497400CD59A1 /* macOS_report_execute_undefined_instruction.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = macOS_report_execute_undefined_instruction.plcrash; sourceTree = ""; }; + 80955E26218B497400CD59A1 /* macOS_report_smash_the_top_of_the_stack.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = macOS_report_smash_the_top_of_the_stack.plcrash; sourceTree = ""; }; + 80955E27218B497400CD59A1 /* macOS_report_stack_overflow.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = macOS_report_stack_overflow.plcrash; sourceTree = ""; }; + 80955E28218B497400CD59A1 /* macOS_report_overwrite_link_register.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = macOS_report_overwrite_link_register.plcrash; sourceTree = ""; }; + 80955E29218B497400CD59A1 /* macOS_report_objc_message_released_object.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = macOS_report_objc_message_released_object.plcrash; sourceTree = ""; }; + 80955E2A218B497400CD59A1 /* macOS_report_swift.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = macOS_report_swift.plcrash; sourceTree = ""; }; + 922446821F621D4500E4034A /* MSACHandledErrorLog.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACHandledErrorLog.h; sourceTree = ""; }; + 922446831F621F3A00E4034A /* MSACHandledErrorLog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACHandledErrorLog.m; sourceTree = ""; }; + 922446851F6222EC00E4034A /* MSACHandledErrorLogTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACHandledErrorLogTests.m; sourceTree = ""; }; + B24F3F0E1D93368F00827213 /* MSACErrorLogFormatterTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MSACErrorLogFormatterTests.mm; sourceTree = ""; }; + B24F3F101D93417B00827213 /* MSACErrorLogFormatterPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACErrorLogFormatterPrivate.h; sourceTree = ""; }; + B266B74F216FEC8E00C9E322 /* live_report_arm64e.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = live_report_arm64e.plcrash; sourceTree = ""; }; + B26A310B21891A7800F09AE1 /* macOS_report_pthread_lock.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = macOS_report_pthread_lock.plcrash; sourceTree = ""; }; + B2A0A71A1D9C2AE700729A58 /* MSACCrashesTestUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACCrashesTestUtil.h; sourceTree = ""; }; + B2A0A71B1D9C2AE700729A58 /* MSACCrashesTestUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACCrashesTestUtil.m; sourceTree = ""; }; + B2CD3BF71D80EE49000A8A91 /* MSACAbstractErrorLog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACAbstractErrorLog.h; sourceTree = ""; }; + B2CD3BF81D80EE49000A8A91 /* MSACAbstractErrorLog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACAbstractErrorLog.m; sourceTree = ""; }; + B2DCB1351DAC03C400120F87 /* MSACCrashesDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACCrashesDelegate.h; sourceTree = ""; }; + B2F120D41D6546740060DED7 /* MSACErrorAttachmentLogTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACErrorAttachmentLogTests.m; sourceTree = ""; }; + B2F120D61D65469D0060DED7 /* MSACErrorReportTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACErrorReportTests.m; sourceTree = ""; }; + B2F120DE1D657CF10060DED7 /* MSACErrorReport.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACErrorReport.h; sourceTree = ""; }; + B2F120DF1D657CF10060DED7 /* MSACErrorReport.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACErrorReport.m; sourceTree = ""; }; + B2F120E61D657F4F0060DED7 /* MSACErrorReportPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACErrorReportPrivate.h; sourceTree = ""; }; + B2F375071D41AD5100F07032 /* MSACErrorLogFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACErrorLogFormatter.h; sourceTree = ""; }; + B2F375081D41AD5100F07032 /* MSACErrorLogFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACErrorLogFormatter.m; sourceTree = ""; }; + B2FF130B1DD12F61003DC677 /* MSACAppleErrorLogTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACAppleErrorLogTests.m; sourceTree = ""; }; + BA68266A68B7F21A86A093B0 /* MSACMockCrashesDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSACMockCrashesDelegate.m; sourceTree = ""; }; + BA6829F386854E39B92F77C4 /* MSACMockCrashesDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSACMockCrashesDelegate.h; sourceTree = ""; }; + C2392F4724642CEA00425640 /* OCMock.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = OCMock.xcodeproj; path = ../../Vendor/OCMock/Source/OCMock.xcodeproj; sourceTree = ""; }; + C24C9CD12472B3C30072C35D /* CrashReporter.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = CrashReporter.xcodeproj; sourceTree = ""; }; + C291A047237955AA0051A846 /* MSACApplicationForwarderTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACApplicationForwarderTests.m; sourceTree = ""; }; + C2B25A26230F040400511D49 /* AppCenter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AppCenter.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C2B25A2B230F079800511D49 /* AppCenter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AppCenter.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C2B25A31230F07B500511D49 /* AppCenter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AppCenter.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C2FE827A23757CB7007DCD28 /* MSACApplicationForwarder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACApplicationForwarder.h; sourceTree = ""; }; + C2FE827B23757CB7007DCD28 /* MSACApplicationForwarder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACApplicationForwarder.m; sourceTree = ""; }; + C2FE8286237594F4007DCD28 /* MSACCrashesBufferedLog.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = MSACCrashesBufferedLog.hpp; sourceTree = ""; }; + C9A91F66230BFDB70068070D /* AppCenterCrashes.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AppCenterCrashes.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C9EBAB2C230D735E00A20F0F /* AppCenter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AppCenter.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C9EBAB2E230D736A00A20F0F /* AppCenter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AppCenter.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C9EBAB30230D736F00A20F0F /* AppCenter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AppCenter.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DFCB80482472D6770058D292 /* OCHamcrest.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = OCHamcrest.xcodeproj; path = ../../Vendor/OCHamcrest/Source/OCHamcrest.xcodeproj; sourceTree = ""; }; + F82E4C74217F1FD600EDAB34 /* sqlite3.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = sqlite3.c; path = ../../Vendor/SQLite3/sqlite3.c; sourceTree = ""; }; + F851DAEC1E81867D00525570 /* MSACCrashesCXXExceptionTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MSACCrashesCXXExceptionTests.mm; sourceTree = ""; }; + F859D0F41E549B45008B2D8E /* live_report_bad_ptr.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = live_report_bad_ptr.plcrash; sourceTree = ""; }; + F859D0F51E549B45008B2D8E /* live_report_call_abort.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = live_report_call_abort.plcrash; sourceTree = ""; }; + F859D0F61E549B45008B2D8E /* live_report_call_trap.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = live_report_call_trap.plcrash; sourceTree = ""; }; + F859D0F71E549B45008B2D8E /* live_report_corrupt_objc.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = live_report_corrupt_objc.plcrash; sourceTree = ""; }; + F859D0F81E549B45008B2D8E /* live_report_cpp_exception.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = live_report_cpp_exception.plcrash; sourceTree = ""; }; + F859D0F91E549B45008B2D8E /* live_report_jump_into_nx.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = live_report_jump_into_nx.plcrash; sourceTree = ""; }; + F859D0FA1E549B45008B2D8E /* live_report_null_ptr.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = live_report_null_ptr.plcrash; sourceTree = ""; }; + F859D0FB1E549B45008B2D8E /* live_report_objc_exception.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = live_report_objc_exception.plcrash; sourceTree = ""; }; + F859D0FC1E549B45008B2D8E /* live_report_objc_msgsend.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = live_report_objc_msgsend.plcrash; sourceTree = ""; }; + F859D0FD1E549B45008B2D8E /* live_report_objc_released.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = live_report_objc_released.plcrash; sourceTree = ""; }; + F859D0FE1E549B45008B2D8E /* live_report_overwrite_link.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = live_report_overwrite_link.plcrash; sourceTree = ""; }; + F859D0FF1E549B45008B2D8E /* live_report_pthread_lock.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = live_report_pthread_lock.plcrash; sourceTree = ""; }; + F859D1001E549B45008B2D8E /* live_report_smash_bottom.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = live_report_smash_bottom.plcrash; sourceTree = ""; }; + F859D1011E549B45008B2D8E /* live_report_smash_top.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = live_report_smash_top.plcrash; sourceTree = ""; }; + F859D1021E549B45008B2D8E /* live_report_swift_crash.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = live_report_swift_crash.plcrash; sourceTree = ""; }; + F859D1031E549B45008B2D8E /* live_report_undefined_instr.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = live_report_undefined_instr.plcrash; sourceTree = ""; }; + F859D1041E549B45008B2D8E /* live_report_write_readonly.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = live_report_write_readonly.plcrash; sourceTree = ""; }; + F8DD7527230D7EC400B095B6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F8FAFDF8242CABA700C7864B /* MSACMockNSUserDefaults.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSACMockNSUserDefaults.h; sourceTree = ""; }; + F8FAFDF9242CABC300C7864B /* MSACMockNSUserDefaults.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSACMockNSUserDefaults.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + C9A91F61230BFDB70068070D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C9EBAB31230D736F00A20F0F /* AppCenter.framework in Frameworks */, + C24C9D1E2472B4AA0072C35D /* libCrashReporter.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 04A140B31ECE8249001CEE94 /* Model */ = { + isa = PBXGroup; + children = ( + BA6829F386854E39B92F77C4 /* MSACMockCrashesDelegate.h */, + BA68266A68B7F21A86A093B0 /* MSACMockCrashesDelegate.m */, + ); + name = Model; + sourceTree = ""; + }; + 04A140B41ECE8253001CEE94 /* Support */ = { + isa = PBXGroup; + children = ( + 041CFF911ECD00DC00B4654B /* Tests iOS.xcconfig */, + 041CFF981ECD010000B4654B /* Tests macOS.xcconfig */, + 0484DD5F1F3910EF0092B777 /* Tests tvOS.xcconfig */, + ); + name = Support; + sourceTree = ""; + }; + 04A140B51ECE825C001CEE94 /* Util */ = { + isa = PBXGroup; + children = ( + B2A0A71A1D9C2AE700729A58 /* MSACCrashesTestUtil.h */, + B2A0A71B1D9C2AE700729A58 /* MSACCrashesTestUtil.m */, + 043281611F74A665002F7205 /* MSACMockUserDefaults.h */, + 043281621F74A665002F7205 /* MSACMockUserDefaults.m */, + 04311FF41EE08697007054C5 /* MSACTestFrameworks.h */, + F8FAFDF8242CABA700C7864B /* MSACMockNSUserDefaults.h */, + F8FAFDF9242CABC300C7864B /* MSACMockNSUserDefaults.m */, + ); + name = Util; + sourceTree = ""; + }; + 04A140B81ECE8310001CEE94 /* Util */ = { + isa = PBXGroup; + children = ( + C2FE827A23757CB7007DCD28 /* MSACApplicationForwarder.h */, + C2FE827B23757CB7007DCD28 /* MSACApplicationForwarder.m */, + 6E73FE6D1D4032AB008CDC15 /* MSACCrashesUtil.h */, + 6E73FE6E1D4032AB008CDC15 /* MSACCrashesUtil.m */, + 049553051EF19F9F0097E071 /* MSACCrashesUtilPrivate.h */, + 04311FFB1EE08885007054C5 /* MSACCrashReporter.h */, + B2F375071D41AD5100F07032 /* MSACErrorLogFormatter.h */, + B2F375081D41AD5100F07032 /* MSACErrorLogFormatter.m */, + B24F3F101D93417B00827213 /* MSACErrorLogFormatterPrivate.h */, + ); + path = Util; + sourceTree = ""; + }; + 350B29E91F192A5C009B91CF /* WrapperSDKUtilities */ = { + isa = PBXGroup; + children = ( + 35B7D8791DE4CB6D00C846CD /* MSACWrapperExceptionManagerInternal.h */, + 35D504CE1DDD140500D58B40 /* MSACWrapperExceptionManager.m */, + 35D504CD1DDD140500D58B40 /* MSACWrapperExceptionManager.h */, + ); + name = WrapperSDKUtilities; + sourceTree = ""; + }; + 380609CC1FA2848D00211C22 /* WrapperSDKUtilities */ = { + isa = PBXGroup; + children = ( + 350B29E71F192554009B91CF /* MSACCrashHandlerSetupDelegate.h */, + 350B29E81F1929A1009B91CF /* MSACWrapperCrashesHelper.h */, + 350B29F11F1D67EE009B91CF /* MSACWrapperCrashesHelper.m */, + ); + path = WrapperSDKUtilities; + sourceTree = ""; + }; + 6E04012A1D1C98690051BCFA = { + isa = PBXGroup; + children = ( + 380B81331E8C565E001C76C9 /* MSACAbstractLog.h */, + 380B81311E8C540E001C76C9 /* MSACLogWithProperties.h */, + 04A082011F74BB6100DC776D /* MSACService.h */, + 387C77061D6CC41100D68CC1 /* MSACServiceAbstract.h */, + 6E0401351D1C98690051BCFA /* AppCenterCrashes */, + 6E171AE21D22F781000DC480 /* AppCenterCrashesTests */, + 6EC99A221D4151C60016C325 /* Vendor */, + 6E0401341D1C98690051BCFA /* Products */, + C9A91F68230BFDFD0068070D /* Frameworks */, + ); + sourceTree = ""; + }; + 6E0401341D1C98690051BCFA /* Products */ = { + isa = PBXGroup; + children = ( + C9A91F66230BFDB70068070D /* AppCenterCrashes.framework */, + ); + name = Products; + sourceTree = ""; + }; + 6E0401351D1C98690051BCFA /* AppCenterCrashes */ = { + isa = PBXGroup; + children = ( + 6E0401361D1C98690051BCFA /* AppCenterCrashes.h */, + 6E04014F1D1C9A4F0051BCFA /* MSACCrashes.h */, + 6E0401501D1C9A4F0051BCFA /* MSACCrashes.mm */, + B2DCB1351DAC03C400120F87 /* MSACCrashesDelegate.h */, + 380609CC1FA2848D00211C22 /* WrapperSDKUtilities */, + 6E0401511D1C9A4F0051BCFA /* Internals */, + D38024001E7125F500466558 /* Model */, + 6E37297E1D1DE93800F1E4AE /* Support */, + ); + path = AppCenterCrashes; + sourceTree = ""; + }; + 6E0401511D1C9A4F0051BCFA /* Internals */ = { + isa = PBXGroup; + children = ( + 6E0401591D1C9D7F0051BCFA /* AppCenter+Internal.h */, + 6E7D5C641D3E92B1009EC9AC /* Model */, + 6E73FE681D402F79008CDC15 /* MSACCrashesCXXExceptionHandler.h */, + 6E73FE691D402F79008CDC15 /* MSACCrashesCXXExceptionHandler.mm */, + 6E73FE711D4059E7008CDC15 /* MSACCrashesCXXExceptionWrapperException.h */, + 6E73FE721D4059E7008CDC15 /* MSACCrashesCXXExceptionWrapperException.m */, + C2FE8286237594F4007DCD28 /* MSACCrashesBufferedLog.hpp */, + 3515F9C01DD63EC9005E6E27 /* MSACCrashesInternal.h */, + 6E0401521D1C9A4F0051BCFA /* MSACCrashesPrivate.h */, + 04A140B81ECE8310001CEE94 /* Util */, + 350B29E91F192A5C009B91CF /* WrapperSDKUtilities */, + ); + path = Internals; + sourceTree = ""; + }; + 6E171AE21D22F781000DC480 /* AppCenterCrashesTests */ = { + isa = PBXGroup; + children = ( + 6E171AE51D22F781000DC480 /* Info.plist */, + B2FF130B1DD12F61003DC677 /* MSACAppleErrorLogTests.m */, + 6E7D5C7F1D3EAEB5009EC9AC /* MSACBinaryTests.m */, + 6E7D5C811D3EC06C009EC9AC /* MSACConstants.h */, + C291A047237955AA0051A846 /* MSACApplicationForwarderTests.m */, + F851DAEC1E81867D00525570 /* MSACCrashesCXXExceptionTests.mm */, + 6EC99A2D1D4166C50016C325 /* MSACCrashesTests.mm */, + 59493B275715F01438B2E6FD /* MSACCrashesUtilTests.m */, + B2F120D41D6546740060DED7 /* MSACErrorAttachmentLogTests.m */, + B24F3F0E1D93368F00827213 /* MSACErrorLogFormatterTests.mm */, + B2F120D61D65469D0060DED7 /* MSACErrorReportTests.m */, + 6E7D5C891D3EC504009EC9AC /* MSACExceptionTests.m */, + 922446851F6222EC00E4034A /* MSACHandledErrorLogTests.m */, + 6E7D5C861D3EC332009EC9AC /* MSACStackFrameTests.m */, + 6E7D5C831D3EC0F7009EC9AC /* MSACThreadTests.m */, + 352B1D6E1F27C36300684A7F /* MSACWrapperCrashesHelperTests.mm */, + 35EF18DF1DDBCF6C00731CA8 /* MSACWrapperExceptionManagerTests.m */, + 350B29F31F1E6F1D009B91CF /* MSACWrapperExceptionTests.m */, + 6EC99A2F1D416CCF0016C325 /* Fixtures */, + 6E171AEC1D22F7AB000DC480 /* Frameworks */, + 04A140B31ECE8249001CEE94 /* Model */, + 04A140B41ECE8253001CEE94 /* Support */, + 04A140B51ECE825C001CEE94 /* Util */, + ); + path = AppCenterCrashesTests; + sourceTree = ""; + }; + 6E171AEC1D22F7AB000DC480 /* Frameworks */ = { + isa = PBXGroup; + children = ( + DFCB80482472D6770058D292 /* OCHamcrest.xcodeproj */, + C2392F4724642CEA00425640 /* OCMock.xcodeproj */, + F82E4C74217F1FD600EDAB34 /* sqlite3.c */, + ); + name = Frameworks; + sourceTree = ""; + }; + 6E37297E1D1DE93800F1E4AE /* Support */ = { + isa = PBXGroup; + children = ( + 324F3BAC25478A220006E223 /* iOS Universal.xcconfig */, + 324F3BAB25478A210006E223 /* tvOS Universal.xcconfig */, + 324F3BAD25478A220006E223 /* XCFramework.xcconfig */, + F8DD7527230D7EC400B095B6 /* Info.plist */, + 04545DCA227B605200A49E06 /* AppCenterCrashes.xcconfig */, + 6E37297F1D1DE93800F1E4AE /* AppCenterCrashes Debug.xcconfig */, + 0469D1B31F4E013C00A43A8E /* AppCenterCrashes Release.xcconfig */, + 049BC8251ECE36D400FB6719 /* iOS.xcconfig */, + 04ED31F21EAAD6070033BAAE /* iOS.modulemap */, + 049BC8261ECE370100FB6719 /* macOS.xcconfig */, + 04ED31F51EAAD6070033BAAE /* macOS.modulemap */, + 04EBBEEF1F01C1420006B8AE /* tvOS.xcconfig */, + 04EBBEEE1F01C1420006B8AE /* tvOS.modulemap */, + ); + path = Support; + sourceTree = ""; + }; + 6E7D5C641D3E92B1009EC9AC /* Model */ = { + isa = PBXGroup; + children = ( + 353FD15E1F29209000E1DF78 /* MSACWrapperException.h */, + 353FD15F1F29209000E1DF78 /* MSACWrapperException.m */, + 353FD1601F29209000E1DF78 /* MSACWrapperExceptionInternal.h */, + 382346411E8B3DAD001C3A76 /* MSACErrorAttachmentLogInternal.h */, + 3507AE3A1DD14C240030878F /* MSACException.h */, + 6E7D5C6E1D3E9346009EC9AC /* MSACException.m */, + 3507AE3B1DD14C240030878F /* MSACStackFrame.h */, + 6E7D5C761D3E9395009EC9AC /* MSACStackFrame.m */, + B2CD3BF71D80EE49000A8A91 /* MSACAbstractErrorLog.h */, + B2CD3BF81D80EE49000A8A91 /* MSACAbstractErrorLog.m */, + 6E7D5C651D3E9321009EC9AC /* MSACBinary.h */, + 6E7D5C661D3E9321009EC9AC /* MSACBinary.m */, + 6E7D5C691D3E9332009EC9AC /* MSACAppleErrorLog.h */, + 6E7D5C6A1D3E9332009EC9AC /* MSACAppleErrorLog.m */, + 922446821F621D4500E4034A /* MSACHandledErrorLog.h */, + 922446831F621F3A00E4034A /* MSACHandledErrorLog.m */, + B2F120E61D657F4F0060DED7 /* MSACErrorReportPrivate.h */, + 6E7D5C711D3E9381009EC9AC /* MSACThread.h */, + 6E7D5C721D3E9381009EC9AC /* MSACThread.m */, + ); + path = Model; + sourceTree = ""; + }; + 6EC99A221D4151C60016C325 /* Vendor */ = { + isa = PBXGroup; + children = ( + C24C9CD02472B3AB0072C35D /* PLCrashReporter */, + ); + name = Vendor; + sourceTree = ""; + }; + 6EC99A2F1D416CCF0016C325 /* Fixtures */ = { + isa = PBXGroup; + children = ( + B26A310A2189185B00F09AE1 /* CrashProbeMacOS */, + F859D0F31E549B45008B2D8E /* CrashProbe */, + B266B74F216FEC8E00C9E322 /* live_report_arm64e.plcrash */, + 6EC99A301D416CCF0016C325 /* live_report_empty.plcrash */, + 6EC99A311D416CCF0016C325 /* live_report_exception.plcrash */, + 6EC99A321D416CCF0016C325 /* live_report_exception_marketing.plcrash */, + 6EC99A331D416CCF0016C325 /* live_report_signal.plcrash */, + 6EC99A341D416CCF0016C325 /* live_report_signal_marketing.plcrash */, + 6EC99A351D416CCF0016C325 /* live_report_xamarin.plcrash */, + ); + path = Fixtures; + sourceTree = ""; + }; + B26A310A2189185B00F09AE1 /* CrashProbeMacOS */ = { + isa = PBXGroup; + children = ( + 80955E22218B497300CD59A1 /* macOS_report_abort.plcrash */, + 80955E1E218B497300CD59A1 /* macOS_report_builtin_trap.plcrash */, + 80955E24218B497400CD59A1 /* macOS_report_corrupt_malloc_internal_info.plcrash */, + 80955E1A218B497300CD59A1 /* macOS_report_corrupt_objc_runtime_structure.plcrash */, + 80955E20218B497300CD59A1 /* macOS_report_dereference_bad_pointer.plcrash */, + 80955E21218B497300CD59A1 /* macOS_report_dereference_null_pointer.plcrash */, + 80955E1F218B497300CD59A1 /* macOS_report_dwarf_unwinding.plcrash */, + 80955E19218B497300CD59A1 /* macOS_report_execute_privileged_instruction.plcrash */, + 80955E25218B497400CD59A1 /* macOS_report_execute_undefined_instruction.plcrash */, + 80955E1C218B497300CD59A1 /* macOS_report_jump_into_nx_page.plcrash */, + 80955E18218B497200CD59A1 /* macOS_report_objc_access_non_object_as_object.plcrash */, + 80955E1D218B497300CD59A1 /* macOS_report_objc_crash_inside_msgsend.plcrash */, + 80955E29218B497400CD59A1 /* macOS_report_objc_message_released_object.plcrash */, + 80955E28218B497400CD59A1 /* macOS_report_overwrite_link_register.plcrash */, + 80955E23218B497300CD59A1 /* macOS_report_smash_the_bottom_of_the_stack.plcrash */, + 80955E26218B497400CD59A1 /* macOS_report_smash_the_top_of_the_stack.plcrash */, + 80955E27218B497400CD59A1 /* macOS_report_stack_overflow.plcrash */, + 80955E2A218B497400CD59A1 /* macOS_report_swift.plcrash */, + 80955E1B218B497300CD59A1 /* macOS_report_throw_cpp_exception.plcrash */, + 80955E17218B497200CD59A1 /* macOS_report_write_to_readonly_page.plcrash */, + B26A310B21891A7800F09AE1 /* macOS_report_pthread_lock.plcrash */, + ); + path = CrashProbeMacOS; + sourceTree = ""; + }; + C2392F4824642CEA00425640 /* Products */ = { + isa = PBXGroup; + children = ( + C2392F5224642CEA00425640 /* OCMock.framework */, + C2392F5424642CEA00425640 /* OCMockTests.xctest */, + C2392F5624642CEA00425640 /* libOCMock.a */, + C2392F5824642CEA00425640 /* OCMockLibTests.xctest */, + C2392F5A24642CEA00425640 /* OCMock.framework */, + C2392F5C24642CEA00425640 /* OCMock.framework */, + C2392F5E24642CEA00425640 /* OCMock.framework */, + ); + name = Products; + sourceTree = ""; + }; + C24C9CD02472B3AB0072C35D /* PLCrashReporter */ = { + isa = PBXGroup; + children = ( + C24C9CD12472B3C30072C35D /* CrashReporter.xcodeproj */, + ); + name = PLCrashReporter; + path = ../Vendor/PLCrashReporter; + sourceTree = ""; + }; + C24C9CD22472B3C30072C35D /* Products */ = { + isa = PBXGroup; + children = ( + C24C9CED2472B3C30072C35D /* CrashReporter.framework */, + C24C9CF12472B3C30072C35D /* CrashReporter.framework */, + C24C9CF72472B3C30072C35D /* CrashReporter.framework */, + C24C9CEF2472B3C30072C35D /* libCrashReporter.a */, + C24C9CF32472B3C30072C35D /* libCrashReporter.a */, + C24C9CF92472B3C30072C35D /* libCrashReporter.a */, + C24C9CFF2472B3C30072C35D /* Tests macOS.xctest */, + C24C9D032472B3C30072C35D /* Tests iOS.xctest */, + C24C9D072472B3C30072C35D /* CrashReporter tvOS Tests.xctest */, + C24C9D092472B3C30072C35D /* DemoCrash macOS.app */, + C24C9D0B2472B3C30072C35D /* DemoCrash iOS.app */, + C24C9D0F2472B3C30072C35D /* DemoCrash tvOS.app */, + C24C9CFD2472B3C30072C35D /* plcrashutil */, + C24C9D132472B3C30072C35D /* Fuzz Testing */, + ); + name = Products; + sourceTree = ""; + }; + C9A91F68230BFDFD0068070D /* Frameworks */ = { + isa = PBXGroup; + children = ( + C2B25A31230F07B500511D49 /* AppCenter.framework */, + C2B25A2B230F079800511D49 /* AppCenter.framework */, + C2B25A26230F040400511D49 /* AppCenter.framework */, + C9EBAB30230D736F00A20F0F /* AppCenter.framework */, + C9EBAB2E230D736A00A20F0F /* AppCenter.framework */, + C9EBAB2C230D735E00A20F0F /* AppCenter.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + D38024001E7125F500466558 /* Model */ = { + isa = PBXGroup; + children = ( + 38BD86511E8499EF004E8D7A /* MSACErrorAttachmentLog.h */, + 8024743A1EAE077800AEC284 /* MSACErrorAttachmentLog.m */, + 3858A2171E93F37E00535A69 /* MSACErrorAttachmentLog+Utility.h */, + 3858A2191E93F3B400535A69 /* MSACErrorAttachmentLog+Utility.m */, + B2F120DE1D657CF10060DED7 /* MSACErrorReport.h */, + B2F120DF1D657CF10060DED7 /* MSACErrorReport.m */, + ); + path = Model; + sourceTree = ""; + }; + DFCB80492472D6770058D292 /* Products */ = { + isa = PBXGroup; + children = ( + DFCB80532472D6770058D292 /* OCHamcrest.framework */, + DFCB80552472D6770058D292 /* OCHamcrestTests.xctest */, + DFCB80572472D6770058D292 /* libochamcrest.a */, + DFCB80592472D6770058D292 /* libochamcrestTests.xctest */, + DFCB805B2472D6770058D292 /* OCHamcrest.framework */, + DFCB805D2472D6770058D292 /* OCHamcrest.framework */, + DFCB805F2472D6770058D292 /* OCHamcrest.framework */, + ); + name = Products; + sourceTree = ""; + }; + F859D0F31E549B45008B2D8E /* CrashProbe */ = { + isa = PBXGroup; + children = ( + F859D0F41E549B45008B2D8E /* live_report_bad_ptr.plcrash */, + F859D0F51E549B45008B2D8E /* live_report_call_abort.plcrash */, + F859D0F61E549B45008B2D8E /* live_report_call_trap.plcrash */, + F859D0F71E549B45008B2D8E /* live_report_corrupt_objc.plcrash */, + F859D0F81E549B45008B2D8E /* live_report_cpp_exception.plcrash */, + F859D0F91E549B45008B2D8E /* live_report_jump_into_nx.plcrash */, + F859D0FA1E549B45008B2D8E /* live_report_null_ptr.plcrash */, + F859D0FB1E549B45008B2D8E /* live_report_objc_exception.plcrash */, + F859D0FC1E549B45008B2D8E /* live_report_objc_msgsend.plcrash */, + F859D0FD1E549B45008B2D8E /* live_report_objc_released.plcrash */, + F859D0FE1E549B45008B2D8E /* live_report_overwrite_link.plcrash */, + F859D0FF1E549B45008B2D8E /* live_report_pthread_lock.plcrash */, + F859D1001E549B45008B2D8E /* live_report_smash_bottom.plcrash */, + F859D1011E549B45008B2D8E /* live_report_smash_top.plcrash */, + F859D1021E549B45008B2D8E /* live_report_swift_crash.plcrash */, + F859D1031E549B45008B2D8E /* live_report_undefined_instr.plcrash */, + F859D1041E549B45008B2D8E /* live_report_write_readonly.plcrash */, + ); + path = CrashProbe; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + C9A91F5E230BFDB70068070D /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + C9EBA96E230D35A800A20F0F /* MSACServiceAbstract.h in Headers */, + C9EBA96D230D35A800A20F0F /* MSACService.h in Headers */, + C9EBA99D230D35FE00A20F0F /* MSACAbstractErrorLog.h in Headers */, + C9EBA982230D35DD00A20F0F /* MSACWrapperCrashesHelper.h in Headers */, + C9EBA9A1230D35FE00A20F0F /* MSACThread.h in Headers */, + C9EBA979230D35CF00A20F0F /* MSACCrashes.h in Headers */, + C9EBA99F230D35FE00A20F0F /* MSACAppleErrorLog.h in Headers */, + C9EBA9A0230D35FE00A20F0F /* MSACHandledErrorLog.h in Headers */, + C9EBA9CF230D361800A20F0F /* MSACErrorLogFormatter.h in Headers */, + C9EBA978230D35CF00A20F0F /* AppCenterCrashes.h in Headers */, + C9EBA990230D35ED00A20F0F /* MSACWrapperExceptionManager.h in Headers */, + C9EBA99E230D35FE00A20F0F /* MSACBinary.h in Headers */, + C2FE827D23757CB7007DCD28 /* MSACApplicationForwarder.h in Headers */, + C9EBA971230D35BD00A20F0F /* MSACLogWithProperties.h in Headers */, + C9EBA991230D35ED00A20F0F /* MSACCrashesInternal.h in Headers */, + C9EBA99C230D35FE00A20F0F /* MSACStackFrame.h in Headers */, + C9EBA99A230D35FE00A20F0F /* MSACErrorAttachmentLogInternal.h in Headers */, + C9EBA9C3230D360700A20F0F /* MSACErrorReportPrivate.h in Headers */, + C9EBA994230D35ED00A20F0F /* AppCenter+Internal.h in Headers */, + C9EBA99B230D35FE00A20F0F /* MSACException.h in Headers */, + C9EBA9D5230D361F00A20F0F /* MSACErrorLogFormatterPrivate.h in Headers */, + C9EBA997230D35F000A20F0F /* MSACCrashesPrivate.h in Headers */, + C9EBA9D4230D361F00A20F0F /* MSACCrashesUtilPrivate.h in Headers */, + C9EBA9DD230D362A00A20F0F /* MSACErrorAttachmentLog+Utility.h in Headers */, + C9EBA992230D35ED00A20F0F /* MSACCrashesCXXExceptionWrapperException.h in Headers */, + C9EBA993230D35ED00A20F0F /* MSACCrashesCXXExceptionHandler.h in Headers */, + C9EBA9DC230D362A00A20F0F /* MSACErrorAttachmentLog.h in Headers */, + C9EBA998230D35FE00A20F0F /* MSACWrapperException.h in Headers */, + C9EBA9CD230D361800A20F0F /* MSACCrashesUtil.h in Headers */, + C9EBA96C230D35A800A20F0F /* MSACAbstractLog.h in Headers */, + C9EBA999230D35FE00A20F0F /* MSACWrapperExceptionInternal.h in Headers */, + C9EBA9CE230D361800A20F0F /* MSACCrashReporter.h in Headers */, + C9EBA981230D35DD00A20F0F /* MSACCrashHandlerSetupDelegate.h in Headers */, + C9EBA97A230D35CF00A20F0F /* MSACCrashesDelegate.h in Headers */, + C9EBA9DE230D362A00A20F0F /* MSACErrorReport.h in Headers */, + C9EBA98F230D35ED00A20F0F /* MSACWrapperExceptionManagerInternal.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + C9A91F5D230BFDB70068070D /* AppCenterCrashes macOS Framework */ = { + isa = PBXNativeTarget; + buildConfigurationList = C9A91F63230BFDB70068070D /* Build configuration list for PBXNativeTarget "AppCenterCrashes macOS Framework" */; + buildPhases = ( + C2B25A22230F03A900511D49 /* Verify No Build Settings */, + C9A91F5E230BFDB70068070D /* Headers */, + C9A91F60230BFDB70068070D /* Sources */, + C9A91F61230BFDB70068070D /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + C24C9D1A2472B4870072C35D /* PBXTargetDependency */, + ); + name = "AppCenterCrashes macOS Framework"; + productName = AppCenterCrashesStaticIOS; + productReference = C9A91F66230BFDB70068070D /* AppCenterCrashes.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 6E04012B1D1C98690051BCFA /* Project object */ = { + isa = PBXProject; + attributes = { + CLASSPREFIX = MSAC; + LastUpgradeCheck = 0830; + ORGANIZATIONNAME = Microsoft; + TargetAttributes = { + C9A91F5D230BFDB70068070D = { + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 6E04012E1D1C98690051BCFA /* Build configuration list for PBXProject "AppCenterCrashes" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + English, + en, + ); + mainGroup = 6E04012A1D1C98690051BCFA; + productRefGroup = 6E0401341D1C98690051BCFA /* Products */; + projectDirPath = ""; + projectReferences = ( + { + ProductGroup = C24C9CD22472B3C30072C35D /* Products */; + ProjectRef = C24C9CD12472B3C30072C35D /* CrashReporter.xcodeproj */; + }, + { + ProductGroup = DFCB80492472D6770058D292 /* Products */; + ProjectRef = DFCB80482472D6770058D292 /* OCHamcrest.xcodeproj */; + }, + { + ProductGroup = C2392F4824642CEA00425640 /* Products */; + ProjectRef = C2392F4724642CEA00425640 /* OCMock.xcodeproj */; + }, + ); + projectRoot = ""; + targets = ( + C9A91F5D230BFDB70068070D /* AppCenterCrashes macOS Framework */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXReferenceProxy section */ + C2392F5224642CEA00425640 /* OCMock.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = OCMock.framework; + remoteRef = C2392F5124642CEA00425640 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C2392F5424642CEA00425640 /* OCMockTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = OCMockTests.xctest; + remoteRef = C2392F5324642CEA00425640 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C2392F5624642CEA00425640 /* libOCMock.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libOCMock.a; + remoteRef = C2392F5524642CEA00425640 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C2392F5824642CEA00425640 /* OCMockLibTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = OCMockLibTests.xctest; + remoteRef = C2392F5724642CEA00425640 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C2392F5A24642CEA00425640 /* OCMock.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = OCMock.framework; + remoteRef = C2392F5924642CEA00425640 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C2392F5C24642CEA00425640 /* OCMock.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = OCMock.framework; + remoteRef = C2392F5B24642CEA00425640 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C2392F5E24642CEA00425640 /* OCMock.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = OCMock.framework; + remoteRef = C2392F5D24642CEA00425640 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C24C9CED2472B3C30072C35D /* CrashReporter.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = CrashReporter.framework; + remoteRef = C24C9CEC2472B3C30072C35D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C24C9CEF2472B3C30072C35D /* libCrashReporter.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libCrashReporter.a; + remoteRef = C24C9CEE2472B3C30072C35D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C24C9CF12472B3C30072C35D /* CrashReporter.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = CrashReporter.framework; + remoteRef = C24C9CF02472B3C30072C35D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C24C9CF32472B3C30072C35D /* libCrashReporter.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libCrashReporter.a; + remoteRef = C24C9CF22472B3C30072C35D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C24C9CF72472B3C30072C35D /* CrashReporter.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = CrashReporter.framework; + remoteRef = C24C9CF62472B3C30072C35D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C24C9CF92472B3C30072C35D /* libCrashReporter.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libCrashReporter.a; + remoteRef = C24C9CF82472B3C30072C35D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C24C9CFD2472B3C30072C35D /* plcrashutil */ = { + isa = PBXReferenceProxy; + fileType = "compiled.mach-o.executable"; + path = plcrashutil; + remoteRef = C24C9CFC2472B3C30072C35D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C24C9CFF2472B3C30072C35D /* Tests macOS.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = "Tests macOS.xctest"; + remoteRef = C24C9CFE2472B3C30072C35D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C24C9D032472B3C30072C35D /* Tests iOS.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = "Tests iOS.xctest"; + remoteRef = C24C9D022472B3C30072C35D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C24C9D072472B3C30072C35D /* CrashReporter tvOS Tests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = "CrashReporter tvOS Tests.xctest"; + remoteRef = C24C9D062472B3C30072C35D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C24C9D092472B3C30072C35D /* DemoCrash macOS.app */ = { + isa = PBXReferenceProxy; + fileType = wrapper.application; + path = "DemoCrash macOS.app"; + remoteRef = C24C9D082472B3C30072C35D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C24C9D0B2472B3C30072C35D /* DemoCrash iOS.app */ = { + isa = PBXReferenceProxy; + fileType = wrapper.application; + path = "DemoCrash iOS.app"; + remoteRef = C24C9D0A2472B3C30072C35D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C24C9D0F2472B3C30072C35D /* DemoCrash tvOS.app */ = { + isa = PBXReferenceProxy; + fileType = wrapper.application; + path = "DemoCrash tvOS.app"; + remoteRef = C24C9D0E2472B3C30072C35D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + C24C9D132472B3C30072C35D /* Fuzz Testing */ = { + isa = PBXReferenceProxy; + fileType = "compiled.mach-o.executable"; + path = "Fuzz Testing"; + remoteRef = C24C9D122472B3C30072C35D /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + DFCB80532472D6770058D292 /* OCHamcrest.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = OCHamcrest.framework; + remoteRef = DFCB80522472D6770058D292 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + DFCB80552472D6770058D292 /* OCHamcrestTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = OCHamcrestTests.xctest; + remoteRef = DFCB80542472D6770058D292 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + DFCB80572472D6770058D292 /* libochamcrest.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libochamcrest.a; + remoteRef = DFCB80562472D6770058D292 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + DFCB80592472D6770058D292 /* libochamcrestTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = libochamcrestTests.xctest; + remoteRef = DFCB80582472D6770058D292 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + DFCB805B2472D6770058D292 /* OCHamcrest.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = OCHamcrest.framework; + remoteRef = DFCB805A2472D6770058D292 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + DFCB805D2472D6770058D292 /* OCHamcrest.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = OCHamcrest.framework; + remoteRef = DFCB805C2472D6770058D292 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + DFCB805F2472D6770058D292 /* OCHamcrest.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = OCHamcrest.framework; + remoteRef = DFCB805E2472D6770058D292 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + +/* Begin PBXShellScriptBuildPhase section */ + C2B25A22230F03A900511D49 /* Verify No Build Settings */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Verify No Build Settings"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#\"${SRCROOT}/../Scripts/VerifyNoBuildSettings.swift\" ${PROJECT_NAME}.xcodeproj/project.pbxproj\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + C9A91F60230BFDB70068070D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C9EBAAC0230D3A1D00A20F0F /* MSACErrorReport.m in Sources */, + C9EBAAB7230D3A1D00A20F0F /* MSACAbstractErrorLog.m in Sources */, + C9EBAAB1230D3A1D00A20F0F /* MSACWrapperExceptionManager.m in Sources */, + C9EBAABA230D3A1D00A20F0F /* MSACHandledErrorLog.m in Sources */, + C9EBAAB2230D3A1D00A20F0F /* MSACCrashesCXXExceptionWrapperException.m in Sources */, + C9EBAAB6230D3A1D00A20F0F /* MSACStackFrame.m in Sources */, + C9EBAABF230D3A1D00A20F0F /* MSACErrorAttachmentLog+Utility.m in Sources */, + C9EBAAAF230D3A1D00A20F0F /* MSACCrashes.mm in Sources */, + C2FE828023757CB7007DCD28 /* MSACApplicationForwarder.m in Sources */, + C9EBAABE230D3A1D00A20F0F /* MSACErrorAttachmentLog.m in Sources */, + C9EBAAB9230D3A1D00A20F0F /* MSACAppleErrorLog.m in Sources */, + C9EBAABB230D3A1D00A20F0F /* MSACThread.m in Sources */, + C9EBAAB3230D3A1D00A20F0F /* MSACCrashesCXXExceptionHandler.mm in Sources */, + C9EBAABD230D3A1D00A20F0F /* MSACErrorLogFormatter.m in Sources */, + C9EBAABC230D3A1D00A20F0F /* MSACCrashesUtil.m in Sources */, + C9EBAAB8230D3A1D00A20F0F /* MSACBinary.m in Sources */, + C9EBAAB0230D3A1D00A20F0F /* MSACWrapperCrashesHelper.m in Sources */, + C9EBAAB5230D3A1D00A20F0F /* MSACException.m in Sources */, + C9EBAAB4230D3A1D00A20F0F /* MSACWrapperException.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + C24C9D1A2472B4870072C35D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = "CrashReporter macOS"; + targetProxy = C24C9D192472B4870072C35D /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 6E04013A1D1C98690051BCFA /* DebugAppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6E37297F1D1DE93800F1E4AE /* AppCenterCrashes Debug.xcconfig */; + buildSettings = { + }; + name = DebugAppStore; + }; + 6E04013B1D1C98690051BCFA /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0469D1B31F4E013C00A43A8E /* AppCenterCrashes Release.xcconfig */; + buildSettings = { + }; + name = ReleaseAppStore; + }; + C9A91F64230BFDB70068070D /* DebugAppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 049BC8261ECE370100FB6719 /* macOS.xcconfig */; + buildSettings = { + DEVELOPMENT_TEAM = ""; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = YES; + }; + name = DebugAppStore; + }; + C9A91F65230BFDB70068070D /* ReleaseAppStore */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 049BC8261ECE370100FB6719 /* macOS.xcconfig */; + buildSettings = { + DEVELOPMENT_TEAM = ""; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + }; + name = ReleaseAppStore; + }; + D0983E3725683D3C00467703 /* Github */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6E37297F1D1DE93800F1E4AE /* AppCenterCrashes Debug.xcconfig */; + buildSettings = { + }; + name = Github; + }; + D0983E3825683D3C00467703 /* Github */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 049BC8261ECE370100FB6719 /* macOS.xcconfig */; + buildSettings = { + DEVELOPMENT_TEAM = ""; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + }; + name = Github; + }; + D0983E3925683D4300467703 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0469D1B31F4E013C00A43A8E /* AppCenterCrashes Release.xcconfig */; + buildSettings = { + }; + name = ReleaseHockeyapp; + }; + D0983E3A25683D4300467703 /* ReleaseHockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 049BC8261ECE370100FB6719 /* macOS.xcconfig */; + buildSettings = { + DEVELOPMENT_TEAM = ""; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + }; + name = ReleaseHockeyapp; + }; + D0983E3B25683D4800467703 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0469D1B31F4E013C00A43A8E /* AppCenterCrashes Release.xcconfig */; + buildSettings = { + }; + name = DebugHockeyapp; + }; + D0983E3C25683D4800467703 /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 049BC8261ECE370100FB6719 /* macOS.xcconfig */; + buildSettings = { + DEVELOPMENT_TEAM = ""; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + }; + name = DebugHockeyapp; + }; + D0983E3D25683D5300467703 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0469D1B31F4E013C00A43A8E /* AppCenterCrashes Release.xcconfig */; + buildSettings = { + }; + name = HockeyappMacAlpha; + }; + D0983E3E25683D5300467703 /* HockeyappMacAlpha */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 049BC8261ECE370100FB6719 /* macOS.xcconfig */; + buildSettings = { + DEVELOPMENT_TEAM = ""; + MACOSX_DEPLOYMENT_TARGET = 10.11; + ONLY_ACTIVE_ARCH = NO; + }; + name = HockeyappMacAlpha; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 6E04012E1D1C98690051BCFA /* Build configuration list for PBXProject "AppCenterCrashes" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6E04013A1D1C98690051BCFA /* DebugAppStore */, + D0983E3725683D3C00467703 /* Github */, + 6E04013B1D1C98690051BCFA /* ReleaseAppStore */, + D0983E3925683D4300467703 /* ReleaseHockeyapp */, + D0983E3B25683D4800467703 /* DebugHockeyapp */, + D0983E3D25683D5300467703 /* HockeyappMacAlpha */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; + C9A91F63230BFDB70068070D /* Build configuration list for PBXNativeTarget "AppCenterCrashes macOS Framework" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C9A91F64230BFDB70068070D /* DebugAppStore */, + D0983E3825683D3C00467703 /* Github */, + C9A91F65230BFDB70068070D /* ReleaseAppStore */, + D0983E3A25683D4300467703 /* ReleaseHockeyapp */, + D0983E3C25683D4800467703 /* DebugHockeyapp */, + D0983E3E25683D5300467703 /* HockeyappMacAlpha */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStore; + }; +/* End XCConfigurationList section */ + }; + rootObject = 6E04012B1D1C98690051BCFA /* Project object */; +} diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/AppCenterCrashes.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/AppCenterCrashes.h new file mode 100644 index 0000000000..8146d6dff2 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/AppCenterCrashes.h @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACCrashHandlerSetupDelegate.h" +#import "MSACCrashes.h" +#import "MSACCrashesDelegate.h" +#import "MSACErrorAttachmentLog+Utility.h" +#import "MSACErrorAttachmentLog.h" +#import "MSACWrapperCrashesHelper.h" diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACCrashesBufferedLog.hpp b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACCrashesBufferedLog.hpp new file mode 100644 index 0000000000..acff40cd02 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACCrashesBufferedLog.hpp @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import +#import +#import + +/** + * Data structure for logs that need to be flushed at crash time to make sure no + * log is lost at crash time. + * + * @property bufferPath The path where the buffered log should be persisted. + * @property buffer The actual buffered data. It comes in the form of a + * std::string but actually contains an NSData object which is a serialized log. + * @property internalId An internal id that helps keep track of logs. + * @property timestamp A timestamp that is used to determine which bufferedLog + * to delete in case the buffer is full. + */ +struct MSACCrashesBufferedLog { + std::string bufferPath; + std::string buffer; + std::string targetTokenPath; + std::string targetToken; + std::string internalId; + NSTimeInterval timestamp; + + MSACCrashesBufferedLog() = default; + + MSACCrashesBufferedLog(NSString *path, NSData *data) + : bufferPath(path.UTF8String), + buffer(&reinterpret_cast(data.bytes)[0], &reinterpret_cast(data.bytes)[data.length]) {} +}; + +/** + * Constant for size of our log buffer. + */ +const int ms_crashes_log_buffer_size = 60; + +/** + * The log buffer object where we keep out BUFFERED_LOGs which will be written + * to disk in case of a crash. + */ +extern std::array msACCrashesLogBuffer; + +/** + * Save the log buffer to files. + */ +extern void ms_save_log_buffer(); diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACCrashesCXXExceptionHandler.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACCrashesCXXExceptionHandler.h new file mode 100644 index 0000000000..eb2d4fe820 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACCrashesCXXExceptionHandler.h @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +/** + * Struct to describe CXXException information. + */ +typedef struct { + const void *__nullable exception; + const char *__nullable exception_type_name; + const char *__nullable exception_message; + uint32_t exception_frames_count; + const uintptr_t *__nonnull exception_frames; +} MSACCrashesUncaughtCXXExceptionInfo; + +typedef void (*MSACCrashesUncaughtCXXExceptionHandler)(const MSACCrashesUncaughtCXXExceptionInfo *__nonnull info); + +@interface MSACCrashesUncaughtCXXExceptionHandlerManager : NSObject + +/** + * Add a XCXX exceptionHandler. + * + * @param handler The MSACCrashesUncaughtCXXExceptionHandler that should be added. + */ ++ (void)addCXXExceptionHandler:(nonnull MSACCrashesUncaughtCXXExceptionHandler)handler; + +/** + * Remove a XCXX exceptionHandler. + * + * @param handler The MSACCrashesUncaughtCXXExceptionHandler that should be removed. + */ ++ (void)removeCXXExceptionHandler:(nonnull MSACCrashesUncaughtCXXExceptionHandler)handler; + +/** + * Handlers count + */ ++ (NSUInteger)countCXXExceptionHandler; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACCrashesCXXExceptionHandler.mm b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACCrashesCXXExceptionHandler.mm new file mode 100644 index 0000000000..1da2688441 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACCrashesCXXExceptionHandler.mm @@ -0,0 +1,235 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#import "MSACCrashesCXXExceptionHandler.h" + +// FIXME: Temporarily disable deprecated warning. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + +typedef std::vector MSACCrashesUncaughtCXXExceptionHandlerList; +typedef struct { + void *exception_object; + uintptr_t call_stack[128]; + uint32_t num_frames; +} MSACCrashesCXXExceptionTSInfo; + +static bool _MSACCrashesIsOurTerminateHandlerInstalled = false; +static std::terminate_handler _MSACCrashesOriginalTerminateHandler = nullptr; +static MSACCrashesUncaughtCXXExceptionHandlerList _MSACCrashesUncaughtExceptionHandlerList; +static OSSpinLock _MSACCrashesCXXExceptionHandlingLock = OS_SPINLOCK_INIT; +static pthread_key_t _MSACCrashesCXXExceptionInfoTSDKey = 0; + +@implementation MSACCrashesUncaughtCXXExceptionHandlerManager + +extern "C" void __attribute__((noreturn)) __cxa_throw(void *exception_object, std::type_info *tinfo, void (*dest)(void *)) { + + /* + * Purposely do not take a lock in this function. The aim is to be as fast as possible. While we could really use some of the info set up + * by the real __cxa_throw, if we call through we never get control back - the function is noreturn and jumps to landing pads. Most of the + * stuff in __cxxabiv1 also won't work yet. We therefore have to do these checks by hand. + * + * The technique for distinguishing Objective-C exceptions is based on the implementation of objc_exception_throw(). It's weird, but it's + * fast. The explicit symbol load and NULL checks should guard against the implementation changing in a future version. (Or not existing + * in an earlier version). + */ + typedef void (*cxa_throw_func)(void *, std::type_info *, void (*)(void *)) __attribute__((noreturn)); + static dispatch_once_t predicate = 0; + static cxa_throw_func __original__cxa_throw = nullptr; + static const void **__real_objc_ehtype_vtable = nullptr; + + dispatch_once(&predicate, ^{ + __original__cxa_throw = reinterpret_cast(dlsym(RTLD_NEXT, "__cxa_throw")); + __real_objc_ehtype_vtable = reinterpret_cast(dlsym(RTLD_DEFAULT, "objc_ehtype_vtable")); + }); + + // Actually check for Objective-C exceptions. + if (tinfo && __real_objc_ehtype_vtable && // Guard from an ABI change + *reinterpret_cast(tinfo) == __real_objc_ehtype_vtable + 2) { + goto callthrough; + } + + /* + * Any other exception that came here has to be C++, since Objective-C is the only (known) runtime that hijacks the C++ ABI this way. We + * need to save off a backtrace. + * Invariant: If the terminate handler is installed, the TSD key must also be initialized. + */ + if (_MSACCrashesIsOurTerminateHandlerInstalled) { + MSACCrashesCXXExceptionTSInfo *info = + static_cast(pthread_getspecific(_MSACCrashesCXXExceptionInfoTSDKey)); + + if (!info) { + info = reinterpret_cast(calloc(1, sizeof(MSACCrashesCXXExceptionTSInfo))); + pthread_setspecific(_MSACCrashesCXXExceptionInfoTSDKey, info); + } + info->exception_object = exception_object; + // XXX: All significant time in this call is spent right here. + info->num_frames = static_cast( + backtrace(reinterpret_cast(&info->call_stack[0]), sizeof(info->call_stack) / sizeof(info->call_stack[0]))); + } + +callthrough: + if (__original__cxa_throw) { + __original__cxa_throw(exception_object, tinfo, dest); + } else { + abort(); + } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunreachable-code" + __builtin_unreachable(); +#pragma clang diagnostic pop +} + +__attribute__((always_inline)) static inline void +MSACCrashesIterateExceptionHandlers_unlocked(const MSACCrashesUncaughtCXXExceptionInfo &info) { + for (const auto &handler : _MSACCrashesUncaughtExceptionHandlerList) { + handler(&info); + } +} + +static void MSACCrashesUncaughtCXXTerminateHandler(void) { + MSACCrashesUncaughtCXXExceptionInfo info = { + .exception = nullptr, + .exception_type_name = nullptr, + .exception_message = nullptr, + .exception_frames_count = 0, + .exception_frames = nullptr, + }; + auto p = std::current_exception(); + + OSSpinLockLock(&_MSACCrashesCXXExceptionHandlingLock); + { + if (p) { // explicit operator bool + info.exception = reinterpret_cast(&p); + info.exception_type_name = __cxxabiv1::__cxa_current_exception_type()->name(); + + MSACCrashesCXXExceptionTSInfo *recorded_info = + reinterpret_cast(pthread_getspecific(_MSACCrashesCXXExceptionInfoTSDKey)); + + if (recorded_info) { + info.exception_frames_count = recorded_info->num_frames - 1; + info.exception_frames = &recorded_info->call_stack[1]; + } else { + + // There's no backtrace, grab this function's trace instead. Probably means the exception came from a dynamically loaded library. + void *frames[128] = {nullptr}; + + info.exception_frames_count = static_cast(backtrace(&frames[0], sizeof(frames) / sizeof(frames[0])) - 1); + info.exception_frames = reinterpret_cast(&frames[1]); + } + + try { + std::rethrow_exception(p); + } catch (const std::exception &e) { + + // C++ exception. + info.exception_message = e.what(); + MSACCrashesIterateExceptionHandlers_unlocked(info); + } catch (const std::exception *e) { + + // C++ exception by pointer. + info.exception_message = e->what(); + MSACCrashesIterateExceptionHandlers_unlocked(info); + } catch (const std::string &e) { + + // C++ string as exception. + info.exception_message = e.c_str(); + MSACCrashesIterateExceptionHandlers_unlocked(info); + } catch (const std::string *e) { + + // C++ string pointer as exception. + info.exception_message = e->c_str(); + MSACCrashesIterateExceptionHandlers_unlocked(info); + } catch (const char *e) { // Plain string as exception. + info.exception_message = e; + MSACCrashesIterateExceptionHandlers_unlocked(info); + } catch (__attribute__((unused)) id e) { + + // Objective-C exception. Pass it on to Foundation. + OSSpinLockUnlock(&_MSACCrashesCXXExceptionHandlingLock); + if (_MSACCrashesOriginalTerminateHandler != nullptr) { + _MSACCrashesOriginalTerminateHandler(); + } + return; + } catch (...) { + + // Any other kind of exception. No message. + MSACCrashesIterateExceptionHandlers_unlocked(info); + } + } + } + OSSpinLockUnlock(&_MSACCrashesCXXExceptionHandlingLock); + + // In case terminate is called reentrantly by passing it on. + if (_MSACCrashesOriginalTerminateHandler != nullptr) { + _MSACCrashesOriginalTerminateHandler(); + } else { + abort(); + } +} + ++ (void)addCXXExceptionHandler:(MSACCrashesUncaughtCXXExceptionHandler)handler { + static dispatch_once_t key_predicate = 0; + + // This only EVER has to be done once, since we don't delete the TSD later (there's no reason to delete it). + dispatch_once(&key_predicate, ^{ + pthread_key_create(&_MSACCrashesCXXExceptionInfoTSDKey, free); + }); + + OSSpinLockLock(&_MSACCrashesCXXExceptionHandlingLock); + { + if (!_MSACCrashesIsOurTerminateHandlerInstalled) { + _MSACCrashesOriginalTerminateHandler = std::set_terminate(MSACCrashesUncaughtCXXTerminateHandler); + _MSACCrashesIsOurTerminateHandlerInstalled = true; + } + _MSACCrashesUncaughtExceptionHandlerList.push_back(handler); + } + OSSpinLockUnlock(&_MSACCrashesCXXExceptionHandlingLock); +} + ++ (void)removeCXXExceptionHandler:(MSACCrashesUncaughtCXXExceptionHandler)handler { + OSSpinLockLock(&_MSACCrashesCXXExceptionHandlingLock); + { + auto i = std::find(_MSACCrashesUncaughtExceptionHandlerList.begin(), _MSACCrashesUncaughtExceptionHandlerList.end(), handler); + + if (i != _MSACCrashesUncaughtExceptionHandlerList.end()) { + _MSACCrashesUncaughtExceptionHandlerList.erase(i); + } + + if (_MSACCrashesIsOurTerminateHandlerInstalled) { + if (_MSACCrashesUncaughtExceptionHandlerList.empty()) { + std::terminate_handler previous_handler = std::set_terminate(_MSACCrashesOriginalTerminateHandler); + + if (previous_handler != MSACCrashesUncaughtCXXTerminateHandler) { + std::set_terminate(previous_handler); + } else { + _MSACCrashesIsOurTerminateHandlerInstalled = false; + _MSACCrashesOriginalTerminateHandler = nullptr; + } + } + } + } + OSSpinLockUnlock(&_MSACCrashesCXXExceptionHandlingLock); +} + ++ (NSUInteger)countCXXExceptionHandler { + NSUInteger count = 0; + OSSpinLockLock(&_MSACCrashesCXXExceptionHandlingLock); + { count = _MSACCrashesUncaughtExceptionHandlerList.size(); } + OSSpinLockUnlock(&_MSACCrashesCXXExceptionHandlingLock); + return count; +} + +#pragma GCC diagnostic pop + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACCrashesCXXExceptionWrapperException.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACCrashesCXXExceptionWrapperException.h new file mode 100644 index 0000000000..590f296008 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACCrashesCXXExceptionWrapperException.h @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACCrashesCXXExceptionHandler.h" + +/** + * Temporary class until PLCR catches up. We trick PLCR with an Objective-C exception. This code provides us access to the C++ exception + * message, including a correct stack trace. + */ +@interface MSACCrashesCXXExceptionWrapperException : NSException + +- (instancetype)initWithCXXExceptionInfo:(const MSACCrashesUncaughtCXXExceptionInfo *)info; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACCrashesCXXExceptionWrapperException.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACCrashesCXXExceptionWrapperException.m new file mode 100644 index 0000000000..205229d49e --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACCrashesCXXExceptionWrapperException.m @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACCrashesCXXExceptionWrapperException.h" + +@interface MSACCrashesCXXExceptionWrapperException () + +@property(readonly, nonatomic) const MSACCrashesUncaughtCXXExceptionInfo *info; + +@end + +@implementation MSACCrashesCXXExceptionWrapperException + +- (instancetype)initWithCXXExceptionInfo:(const MSACCrashesUncaughtCXXExceptionInfo *)info { + extern char *__cxa_demangle(const char *mangled_name, char *output_buffer, size_t *length, int *status); + char *demangled_name = &__cxa_demangle ? __cxa_demangle(info->exception_type_name ?: "", NULL, NULL, NULL) : NULL; + + // stringWithUTF8String never returns null for us because we always send a correct string + if ((self = [super initWithName:(NSString * _Nonnull)[NSString stringWithUTF8String:demangled_name ?: info->exception_type_name ?: ""] + reason:[NSString stringWithUTF8String:info->exception_message ?: ""] + userInfo:nil])) { + _info = info; + } + return self; +} + +/** + * This method overrides [NSThread callStackReturnAddresses] and is crucial to report CXX exceptions. This is one of the "sneaky" things + * that require knowledge of how PLCrashReporter works internally. + */ +- (NSArray *)callStackReturnAddresses { + + NSMutableArray *cxxFrames = [NSMutableArray arrayWithCapacity:self.info->exception_frames_count]; + + for (uint32_t i = 0; i < self.info->exception_frames_count; ++i) { + [cxxFrames addObject:@(self.info->exception_frames[i])]; + } + + return cxxFrames; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACCrashesInternal.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACCrashesInternal.h new file mode 100644 index 0000000000..33baddac64 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACCrashesInternal.h @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACCrashes.h" +#import "MSACServiceInternal.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Key for a low memory warning in the last session. + */ +static NSString *const kMSACAppDidReceiveMemoryWarningKey = @"AppDidReceiveMemoryWarning"; + +@class MSACException; +@class MSACErrorAttachmentLog; + +@interface MSACCrashes () + +/** + * A flag that indicates whether automatic processing is enabled. + */ +@property(nonatomic, getter=isAutomaticProcessingEnabled) BOOL automaticProcessingEnabled; + +/** + * Gets a list of unprocessed crash reports. Will block until the service starts. + * + * @return An array of unprocessed error reports. + */ +- (NSArray *)unprocessedCrashReports; + +/** + * Resumes processing for a given subset of the unprocessed reports. + * + * @param filteredIds An array containing the errorId/incidentIdentifier of each report that should be sent. + * + * @return YES if should "Always Send" is true. + */ +- (BOOL)sendCrashReportsOrAwaitUserConfirmationForFilteredIds:(NSArray *)filteredIds; + +/** + * Sends error attachments for a particular error report. + * + * @param errorAttachments An array of error attachments that should be sent. + * @param incidentIdentifier The identifier of the error report that the attachments will be associated with. + */ +- (void)sendErrorAttachments:(nullable NSArray *)errorAttachments + withIncidentIdentifier:(NSString *)incidentIdentifier; + +/** + * Configure PLCrashreporter. + * + * @param enableUncaughtExceptionHandler Flag that indicates if PLCrashReporter should register an uncaught exception handler. + * + * @discussion The parameter that is passed in here should be `YES` for the "regular" iOS SDK. This property was * introduced to provide + * proper behavior in case the native iOS SDK was wrapped by the Xamarin SDK. You must not * register an UncaughtExceptionHandler for + * Xamarin as we rely on the xamarin runtime to report NSExceptions. * Registering our own UncaughtExceptionHandler will cause the Xamarin + * debugger to not work properly: The debugger will * not stop for NSExceptions and it's impossible to handle them in a C# try-catch block. + * On Xamarin runtime, if we don't * register our own exception handler, the Xamarin runtime will catch NSExceptions and re-throw them as + * .Net-exceptions * which can be handled and are then reported by App Center Crashes properly. Just as a reminder: this doesn't mean * that + * we are not using PLCrashReporter to catch crashes, it just means that we disable its ability to catch * crashes caused by NSExceptions, + * only those for the reasons mentioned in this paragraph. + */ +- (void)configureCrashReporterWithUncaughtExceptionHandlerEnabled:(BOOL)enableUncaughtExceptionHandler; + +/** + * Track handled exception directly as model form. + * This API is used by wrapper SDKs. + * + * @param exception model form exception. + * @param properties dictionary of properties. + * @param attachments a list of attachments. + * + * @return handled error ID. + */ +- (nullable NSString *)trackModelException:(MSACException *)exception + withProperties:(nullable NSDictionary *)properties + withAttachments:(nullable NSArray *)attachments; + +/** + * Get a generic error report representation for an handled exception. + * This API is used by wrapper SDKs. + * + * @param errorID handled error ID. + * + * @return an error report. + */ +- (MSACErrorReport *)buildHandledErrorReportWithErrorID:(NSString *)errorID; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACCrashesPrivate.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACCrashesPrivate.h new file mode 100644 index 0000000000..cb4fc54e1e --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACCrashesPrivate.h @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACChannelDelegate.h" +#import "MSACCrashes.h" + +@class PLCrashReporter; + +@interface MSACCrashes () + +/** + * Prototype of a callback function used to execute additional user code. Called + * upon completion of crash handling, after the crash report has been written to + * disk. + * + * @param context The API client's supplied context value. + * + * @see `MSACCrashesCallbacks` + * @see `[MSACCrashes setCrashCallbacks:]` + */ +typedef void (*MSACCrashesPostCrashSignalCallback)(void *context); + +/** + * This structure contains callbacks supported by `MSACCrashes` to allow the host + * application to perform additional tasks prior to program termination after a + * crash has occurred. + * + * @see `MSACCrashesPostCrashSignalCallback` + * @see `[MSACCrashes setCrashCallbacks:]` + */ +typedef struct MSACCrashesCallbacks { + + /** + * An arbitrary user-supplied context value. This value may be NULL. + */ + void *context; + + /** + * The callback used to report caught signal information. + */ + MSACCrashesPostCrashSignalCallback handleSignal; +} MSACCrashesCallbacks; + +@property(nonatomic, assign, getter=isMachExceptionHandlerEnabled) BOOL enableMachExceptionHandler; + +/** + * A list containing all crash files that currently stored on disk for this app. + */ +@property(nonatomic, copy) NSMutableArray *crashFiles; + +/** + * The path component directory where all crash reports are stored. + */ +@property(nonatomic, copy) NSString *crashesPathComponent; + +/** + * The directory where all buffered logs are stored. + */ +@property(nonatomic, copy) NSString *logBufferPathComponent; + +/** + * A path component that's used to indicate that a crash which occurred in the + * last session is currently written to disk. + */ +@property(nonatomic, copy) NSString *analyzerInProgressFilePathComponent; + +/** + * The object implements the protocol defined in `MSACCrashesDelegate`. + * @see MSACCrashesDelegate + */ +@property(nonatomic, weak) id delegate; + +/** + * The `PLCrashReporter` instance used for crash detection. + */ +@property(nonatomic) PLCrashReporter *plCrashReporter; + +/** + * The exception handler used by the crashes service. + */ +@property(nonatomic) NSUncaughtExceptionHandler *exceptionHandler; + +/** + * Temporary storage for crashes logs to handle user confirmation and callbacks. + */ +@property NSMutableArray *unprocessedLogs; +@property NSMutableArray *unprocessedReports; +@property NSMutableArray *unprocessedFilePaths; + +/** + * Custom user confirmation handler. + */ +@property MSACUserConfirmationHandler userConfirmationHandler; + +/** + * The start time of the application. + */ +@property(nonatomic) NSDate *appStartTime; + +/** + * Delete all data in crashes directory. + */ +- (void)deleteAllFromCrashesDirectory; + +/** + * Determine whether the error report should be processed or not. + * + * @param errorReport An error report. + * @return YES if it should process, otherwise NO. + */ +- (BOOL)shouldProcessErrorReport:(MSACErrorReport *)errorReport; + +/** + * Creates log buffer to buffer logs which will be saved in an async-safe manner + * at crash time. The buffer makes sure we don't lose any logs at crash time. + * This method creates 20 files that will be used to buffer 20 logs. + * The files will only be created once and not recreated from scratch every time + * MSACCrashes is initialized. + */ +- (void)setupLogBuffer; + +/** + * Sends crashes when given MSACUserConfirmationSend. + */ +- (void)notifyWithUserConfirmation:(MSACUserConfirmation)userConfirmation; + +/** + * Does not delete the files for our log buffer but "resets" them to be empty. + * For this, it actually overwrites the old file with an empty copy of the + * original one. The reason why we are not truly deleting the files is that they + * need to exist at crash time. + */ +- (void)emptyLogBufferFiles; + +/** + * Method to reset the singleton when running unit tests only. So calling + * sharedInstance returns a fresh instance. + */ ++ (void)resetSharedInstance; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACWrapperExceptionManager.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACWrapperExceptionManager.h new file mode 100644 index 0000000000..a1a888c361 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACWrapperExceptionManager.h @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +@class MSACWrapperException; + +/** + * This class helps wrapper SDKs augment crash reports with additional data. + * + * HOW IT WORKS: + * 1. Application is crashing from a wrapper SDK, but before propagating crash to the native code, it calls "saveWrapperException" which + * saves the MSACWrapperException to a file called "last_saved_wrapper_exception". + * 2. On startup, the native SDK must find that file if it exists, and use the MSACWrapperException's "pid" to correlate the file to a + * PLCrashReport on disk. If a match is found, the file is renamed to the UUID of the PLCrashReport. + * 3. When an MSACAppleErrorLog must be generated, a corresponding MSACWrapperException file is searched for, and if found, its + * MSACException property is added to the MSACAppleErrorLog. The file is not deleted because there is an additional "data" property that + * contains information that the wrapper SDK may want. + * 4. When an MSACErrorReport equivalent needs to be generated by a wrapper SDK, it is identical to the actual MSACErrorReport, but also + * includes the additional data that was saved. This data is accessed by "loadWrapperExceptionWithUUID". + * Thus, the file on disk can only be deleted after all crash callbacks with an MSACErrorReport parameter have completed. + */ +@interface MSACWrapperExceptionManager : NSObject + +/** + * Save the MSACWrapperException to the file "last_saved_wrapper_exception". This should only be used by a wrapper SDK; native code has no + * use for it. + */ ++ (void)saveWrapperException:(MSACWrapperException *)wrapperException; + +/** + * Load a wrapper exception from disk with a given UUID. + */ ++ (MSACWrapperException *)loadWrapperExceptionWithUUIDString:(NSString *)uuidString; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACWrapperExceptionManager.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACWrapperExceptionManager.m new file mode 100644 index 0000000000..6b9bfa079e --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACWrapperExceptionManager.m @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAppCenterInternal.h" +#import "MSACCrashesInternal.h" +#import "MSACCrashesUtil.h" +#import "MSACException.h" +#import "MSACLoggerInternal.h" +#import "MSACUtility+File.h" +#import "MSACWrapperExceptionInternal.h" +#import "MSACWrapperExceptionManagerInternal.h" + +@implementation MSACWrapperExceptionManager : NSObject + +static NSString *const kMSACLastWrapperExceptionFileName = @"last_saved_wrapper_exception"; +static NSMutableDictionary *unprocessedWrapperExceptions; + ++ (void)load { + unprocessedWrapperExceptions = [NSMutableDictionary new]; +} + +#pragma mark Public Methods + +/** + * Gets a wrapper exception with a given UUID. + */ ++ (MSACWrapperException *)loadWrapperExceptionWithUUIDString:(NSString *)uuidString { + MSACWrapperException *foundException = unprocessedWrapperExceptions[uuidString]; + return foundException ? foundException : [self loadWrapperExceptionWithBaseFilename:uuidString]; +} + +/** + * Saves a wrapper exception to disk. Should only be used by wrapper SDK. + */ ++ (void)saveWrapperException:(MSACWrapperException *)wrapperException { + [self saveWrapperException:wrapperException withBaseFilename:kMSACLastWrapperExceptionFileName]; +} + +#pragma mark Internal Methods + +/** + * Deletes a wrapper exception with a given UUID. + */ ++ (void)deleteWrapperExceptionWithUUIDString:(NSString *)uuidString { + [self deleteWrapperExceptionWithBaseFilename:uuidString]; +} + +/** + * Deletes all wrapper exceptions on disk. + */ ++ (void)deleteAllWrapperExceptions { + [MSACUtility deleteItemForPathComponent:[MSACCrashesUtil wrapperExceptionsDir]]; +} + +/** + * Renames the last saved wrapper exception with the error ID of the corresponding report in the given array. Pairing is based on the + * process id of the error report. + */ ++ (void)correlateLastSavedWrapperExceptionToReport:(NSArray *)reports { + MSACWrapperException *lastSavedWrapperException = [self loadWrapperExceptionWithBaseFilename:kMSACLastWrapperExceptionFileName]; + + // Delete the last saved exception from disk if it exists. + if (lastSavedWrapperException) { + [self deleteWrapperExceptionWithBaseFilename:kMSACLastWrapperExceptionFileName]; + } + MSACErrorReport *correspondingReport = nil; + for (MSACErrorReport *report in reports) { + if ([lastSavedWrapperException.processId unsignedLongValue] == report.appProcessIdentifier) { + correspondingReport = report; + break; + } + } + if (correspondingReport) { + + // As soon as the wrapper exception is correlated, store it in memory and save it to disk + unprocessedWrapperExceptions[correspondingReport.incidentIdentifier] = lastSavedWrapperException; + [self saveWrapperException:lastSavedWrapperException withBaseFilename:correspondingReport.incidentIdentifier]; + } +} + +#pragma mark Helper methods + +/** + * Saves a wrapper exception to disk with the given file name. + */ ++ (void)saveWrapperException:(MSACWrapperException *)wrapperException withBaseFilename:(NSString *)baseFilename { + + // For some reason, archiving directly to a file fails in some cases, so archive to NSData and write that to the file + NSData *data = [MSACUtility archiveKeyedData:wrapperException]; + NSString *pathComponent = [NSString stringWithFormat:@"%@/%@", [MSACCrashesUtil wrapperExceptionsDir], baseFilename]; + [MSACUtility createFileAtPathComponent:pathComponent withData:data atomically:YES forceOverwrite:YES]; +} + +/** + * Deletes a wrapper exception with a given file name. + */ ++ (void)deleteWrapperExceptionWithBaseFilename:(NSString *)baseFilename { + NSString *pathComponent = [NSString stringWithFormat:@"%@/%@", [MSACCrashesUtil wrapperExceptionsDir], baseFilename]; + [MSACUtility deleteItemForPathComponent:pathComponent]; +} + +/** + * Loads a wrapper exception with a given filename. + */ ++ (MSACWrapperException *)loadWrapperExceptionWithBaseFilename:(NSString *)baseFilename { + + // For some reason, unarchiving directly from a file fails in some cases, so load data from a file and unarchive it after. + NSString *pathComponent = [NSString stringWithFormat:@"%@/%@", [MSACCrashesUtil wrapperExceptionsDir], baseFilename]; + if (![MSACUtility fileExistsForPathComponent:pathComponent]) { + return nil; + } + NSData *data = [MSACUtility loadDataForPathComponent:pathComponent]; + MSACWrapperException *wrapperException = nil; + wrapperException = (MSACWrapperException *)[MSACUtility unarchiveKeyedData:data]; + if (!wrapperException) { + MSACLogError([MSACCrashes logTag], @"Could not read exception data stored on disk with file name %@", baseFilename); + [self deleteWrapperExceptionWithBaseFilename:baseFilename]; + } + return wrapperException; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACWrapperExceptionManagerInternal.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACWrapperExceptionManagerInternal.h new file mode 100644 index 0000000000..d651d00779 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/MSACWrapperExceptionManagerInternal.h @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACWrapperExceptionManager.h" + +@class MSACErrorReport; +@class MSACWrapperException; + +@interface MSACWrapperExceptionManager () + +/** + * Delete all wrapper exception files on disk. + */ ++ (void)deleteAllWrapperExceptions; + +/** + * Find the PLCrashReport with a matching process id to the MSACWrapperException that was last saved on disk, and update the filename to the + * report's UUID. + */ ++ (void)correlateLastSavedWrapperExceptionToReport:(NSArray *)reports; + +/** + * Delete a wrapper exception with a given UUID. + */ ++ (void)deleteWrapperExceptionWithUUIDString:(NSString *)uuidString; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACAbstractErrorLog.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACAbstractErrorLog.h new file mode 100644 index 0000000000..fc41e9643a --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACAbstractErrorLog.h @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "AppCenter+Internal.h" +#import "MSACLogWithProperties.h" + +@class MSACErrorAttachment; + +@interface MSACAbstractErrorLog : MSACLogWithProperties + +/** + * Error identifier. + */ +@property(nonatomic, copy) NSString *errorId; + +/** + * Process identifier. + */ +@property(nonatomic) NSNumber *processId; + +/** + * Process name. + */ +@property(nonatomic, copy) NSString *processName; + +/** + * Parent's process identifier. [optional] + */ +@property(nonatomic) NSNumber *parentProcessId; + +/** + * Name of the parent's process. [optional] + */ +@property(nonatomic, copy) NSString *parentProcessName; + +/** + * Error thread identifier. [optional] + */ +@property(nonatomic) NSNumber *errorThreadId; + +/** + * Error thread name. [optional] + */ +@property(nonatomic, copy) NSString *errorThreadName; + +/** + * If YES, this error report is an application crash. + */ +@property(nonatomic, getter=isFatal) BOOL fatal; + +/** + * Timestamp when the app was launched. + */ +@property(nonatomic) NSDate *appLaunchTimestamp; + +/** + * CPU Architecture. [optional] + */ +@property(nonatomic, copy) NSString *architecture; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACAbstractErrorLog.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACAbstractErrorLog.m new file mode 100644 index 0000000000..839a49c9a1 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACAbstractErrorLog.m @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAbstractErrorLog.h" + +static NSString *const kMSACId = @"id"; +static NSString *const kMSACProcessId = @"processId"; +static NSString *const kMSACProcessName = @"processName"; +static NSString *const kMSACParentProcessId = @"parentProcessId"; +static NSString *const kMSACParentProcessName = @"parentProcessName"; +static NSString *const kMSACErrorThreadId = @"errorThreadId"; +static NSString *const kMSACErrorThreadName = @"errorThreadName"; +static NSString *const kMSACFatal = @"fatal"; +static NSString *const kMSACAppLaunchTimestamp = @"appLaunchTimestamp"; +static NSString *const kMSACArchitecture = @"architecture"; + +@implementation MSACAbstractErrorLog + +@synthesize errorId = _id; +@synthesize processId = _processId; +@synthesize processName = _processName; +@synthesize parentProcessId = _parentProcessId; +@synthesize parentProcessName = _parentProcessName; +@synthesize errorThreadId = _errorThreadId; +@synthesize errorThreadName = _errorThreadName; +@synthesize fatal = _fatal; +@synthesize appLaunchTimestamp = _appLaunchTimestamp; +@synthesize architecture = _architecture; + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary]; + + if (self.errorId) { + dict[kMSACId] = self.errorId; + } + if (self.processId) { + dict[kMSACProcessId] = self.processId; + } + if (self.processName) { + dict[kMSACProcessName] = self.processName; + } + if (self.parentProcessId) { + dict[kMSACParentProcessId] = self.parentProcessId; + } + if (self.parentProcessName) { + dict[kMSACParentProcessName] = self.parentProcessName; + } + if (self.errorThreadId) { + dict[kMSACErrorThreadId] = self.errorThreadId; + } + if (self.errorThreadName) { + dict[kMSACErrorThreadName] = self.errorThreadName; + } + dict[kMSACFatal] = self.fatal ? @YES : @NO; + if (self.appLaunchTimestamp) { + dict[kMSACAppLaunchTimestamp] = [MSACUtility dateToISO8601:self.appLaunchTimestamp]; + } + if (self.architecture) { + dict[kMSACArchitecture] = self.architecture; + } + + return dict; +} + +- (BOOL)isValid { + return + [super isValid] && MSACLOG_VALIDATE_NOT_NIL(errorId) && MSACLOG_VALIDATE_NOT_NIL(processId) && MSACLOG_VALIDATE_NOT_NIL(processName); +} + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACAbstractErrorLog class]] || ![super isEqual:object]) { + return NO; + } + MSACAbstractErrorLog *errorLog = (MSACAbstractErrorLog *)object; + return ((!self.errorId && !errorLog.errorId) || [self.errorId isEqualToString:errorLog.errorId]) && + ((!self.processId && !errorLog.processId) || [self.processId isEqual:errorLog.processId]) && + ((!self.processName && !errorLog.processName) || [self.processName isEqualToString:errorLog.processName]) && + ((!self.parentProcessId && !errorLog.parentProcessId) || [self.parentProcessId isEqual:errorLog.parentProcessId]) && + ((!self.parentProcessName && !errorLog.parentProcessName) || + [self.parentProcessName isEqualToString:errorLog.parentProcessName]) && + ((!self.errorThreadId && !errorLog.errorThreadId) || [self.errorThreadId isEqual:errorLog.errorThreadId]) && + ((!self.errorThreadName && !errorLog.errorThreadName) || [self.errorThreadName isEqualToString:errorLog.errorThreadName]) && + (self.fatal == errorLog.fatal) && + ((!self.appLaunchTimestamp && !errorLog.appLaunchTimestamp) || [self.appLaunchTimestamp isEqual:errorLog.appLaunchTimestamp]) && + ((!self.architecture && !errorLog.architecture) || [self.architecture isEqualToString:errorLog.architecture]); +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if (self) { + _id = [coder decodeObjectForKey:kMSACId]; + _processId = [coder decodeObjectForKey:kMSACProcessId]; + _processName = [coder decodeObjectForKey:kMSACProcessName]; + _parentProcessId = [coder decodeObjectForKey:kMSACParentProcessId]; + _parentProcessName = [coder decodeObjectForKey:kMSACParentProcessName]; + _errorThreadId = [coder decodeObjectForKey:kMSACErrorThreadId]; + _errorThreadName = [coder decodeObjectForKey:kMSACErrorThreadName]; + _fatal = [coder decodeBoolForKey:kMSACFatal]; + _appLaunchTimestamp = [coder decodeObjectForKey:kMSACAppLaunchTimestamp]; + _architecture = [coder decodeObjectForKey:kMSACArchitecture]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.errorId forKey:kMSACId]; + [coder encodeObject:self.processId forKey:kMSACProcessId]; + [coder encodeObject:self.processName forKey:kMSACProcessName]; + [coder encodeObject:self.parentProcessId forKey:kMSACParentProcessId]; + [coder encodeObject:self.parentProcessName forKey:kMSACParentProcessName]; + [coder encodeObject:self.errorThreadId forKey:kMSACErrorThreadId]; + [coder encodeObject:self.errorThreadName forKey:kMSACErrorThreadName]; + [coder encodeBool:self.fatal forKey:kMSACFatal]; + [coder encodeObject:self.appLaunchTimestamp forKey:kMSACAppLaunchTimestamp]; + [coder encodeObject:self.architecture forKey:kMSACArchitecture]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACAppleErrorLog.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACAppleErrorLog.h new file mode 100644 index 0000000000..34a0f1b4ed --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACAppleErrorLog.h @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "AppCenter+Internal.h" +#import "MSACAbstractErrorLog.h" +#import "MSACNoAutoAssignSessionIdLog.h" + +@class MSACThread, MSACBinary, MSACException; + +/** + * Error log for Apple platforms. + */ +@interface MSACAppleErrorLog : MSACAbstractErrorLog + +/** + * CPU primary architecture. + * Expected values are as follows: + * public static primary_i386 = 0x00000007; + * public static primary_x86_64 = 0x01000007; + * public static primary_arm = 0x0000000C; + * public static primary_arm64 = 0x0100000C; + */ +@property(nonatomic) NSNumber *primaryArchitectureId; + +/** + * CPU architecture variant [optional]. + * + * If primary is arm64, the possible variants are + * public static variant_arm64_1 = 0x00000000; + * public static variant_arm64_2 = 0x0000000D; + * public static variant_arm64_3 = 0x00000001; + * + * If primary is arm, the possible variants are + * public static variant_armv6 = 0x00000006; + * public static variant_armv7 = 0x00000009; + * public static variant_armv7s = 0x0000000B; + * public static variant_armv7k = 0x0000000C; + */ +@property(nonatomic) NSNumber *architectureVariantId; + +/** + * Path to the application. + */ +@property(nonatomic, copy) NSString *applicationPath; + +/** + * OS exception type. + */ +@property(nonatomic, copy) NSString *osExceptionType; + +/** + * OS exception code. + */ +@property(nonatomic, copy) NSString *osExceptionCode; + +/** + * OS exception address. + */ +@property(nonatomic, copy) NSString *osExceptionAddress; + +/** + * Exception type [optional]. + */ +@property(nonatomic, copy) NSString *exceptionType; + +/** + * Exception reason [optional]. + */ +@property(nonatomic, copy) NSString *exceptionReason; + +/** + * Content of register that might contain last method call [optional]. + */ +@property(nonatomic, copy) NSString *selectorRegisterValue; + +/** + * Thread stack frames associated to the error [optional]. + */ +@property(nonatomic) NSArray *threads; + +/** + * Binaries associated to the error [optional]. + */ +@property(nonatomic) NSArray *binaries; + +/** + * Registers. [optional] + */ +@property(nonatomic) NSDictionary *registers; + +/** + * The last exception backtrace. + */ +@property(nonatomic) MSACException *exception; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACAppleErrorLog.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACAppleErrorLog.m new file mode 100644 index 0000000000..80d017561d --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACAppleErrorLog.m @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAppleErrorLog.h" +#import "MSACBinary.h" +#import "MSACException.h" +#import "MSACThread.h" + +static NSString *const kMSACTypeError = @"appleError"; +static NSString *const kMSACPrimaryArchitectureId = @"primaryArchitectureId"; +static NSString *const kMSACArchitectureVariantId = @"architectureVariantId"; +static NSString *const kMSACApplicationPath = @"applicationPath"; +static NSString *const kMSACOsExceptionType = @"osExceptionType"; +static NSString *const kMSACOsExceptionCode = @"osExceptionCode"; +static NSString *const kMSACOsExceptionAddress = @"osExceptionAddress"; +static NSString *const kMSACExceptionType = @"exceptionType"; +static NSString *const kMSACExceptionReason = @"exceptionReason"; +static NSString *const kMSACSelectorRegisterValue = @"selectorRegisterValue"; +static NSString *const kMSACThreads = @"threads"; +static NSString *const kMSACBinaries = @"binaries"; +static NSString *const kMSACRegisters = @"registers"; +static NSString *const kMSACException = @"exception"; + +@implementation MSACAppleErrorLog + +- (instancetype)init { + if ((self = [super init])) { + self.type = kMSACTypeError; + } + return self; +} + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary]; + + if (self.primaryArchitectureId) { + dict[kMSACPrimaryArchitectureId] = self.primaryArchitectureId; + } + if (self.architectureVariantId) { + dict[kMSACArchitectureVariantId] = self.architectureVariantId; + } + if (self.applicationPath) { + dict[kMSACApplicationPath] = self.applicationPath; + } + if (self.osExceptionType) { + dict[kMSACOsExceptionType] = self.osExceptionType; + } + if (self.osExceptionCode) { + dict[kMSACOsExceptionCode] = self.osExceptionCode; + } + if (self.osExceptionAddress) { + dict[kMSACOsExceptionAddress] = self.osExceptionAddress; + } + if (self.exceptionType) { + dict[kMSACExceptionType] = self.exceptionType; + } + if (self.exceptionReason) { + dict[kMSACExceptionReason] = self.exceptionReason; + } + if (self.selectorRegisterValue) { + dict[kMSACSelectorRegisterValue] = self.selectorRegisterValue; + } + if (self.threads) { + NSMutableArray *threadsArray = [NSMutableArray array]; + for (MSACThread *thread in self.threads) { + [threadsArray addObject:[thread serializeToDictionary]]; + } + dict[kMSACThreads] = threadsArray; + } + if (self.binaries) { + NSMutableArray *binariesArray = [NSMutableArray array]; + for (MSACBinary *binary in self.binaries) { + [binariesArray addObject:[binary serializeToDictionary]]; + } + dict[kMSACBinaries] = binariesArray; + } + if (self.registers) { + dict[kMSACRegisters] = self.registers; + } + if (self.exception) { + dict[kMSACException] = [self.exception serializeToDictionary]; + } + + return dict; +} + +- (BOOL)isValid { + return [super isValid] && MSACLOG_VALIDATE_NOT_NIL(primaryArchitectureId) && MSACLOG_VALIDATE_NOT_NIL(applicationPath) && + MSACLOG_VALIDATE_NOT_NIL(osExceptionType) && MSACLOG_VALIDATE_NOT_NIL(osExceptionCode) && + MSACLOG_VALIDATE_NOT_NIL(osExceptionAddress); +} + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACAppleErrorLog class]] || ![super isEqual:object]) { + return NO; + } + MSACAppleErrorLog *errorLog = (MSACAppleErrorLog *)object; + return ((!self.primaryArchitectureId && !errorLog.primaryArchitectureId) || + [self.primaryArchitectureId isEqual:errorLog.primaryArchitectureId]) && + ((!self.architectureVariantId && !errorLog.architectureVariantId) || + [self.architectureVariantId isEqual:errorLog.architectureVariantId]) && + ((!self.applicationPath && !errorLog.applicationPath) || [self.applicationPath isEqualToString:errorLog.applicationPath]) && + ((!self.osExceptionType && !errorLog.osExceptionType) || [self.osExceptionType isEqualToString:errorLog.osExceptionType]) && + ((!self.osExceptionCode && !errorLog.osExceptionCode) || [self.osExceptionCode isEqualToString:errorLog.osExceptionCode]) && + ((!self.osExceptionAddress && !errorLog.osExceptionAddress) || + [self.osExceptionAddress isEqualToString:errorLog.osExceptionAddress]) && + ((!self.exceptionType && !errorLog.exceptionType) || [self.exceptionType isEqualToString:errorLog.exceptionType]) && + ((!self.exceptionReason && !errorLog.exceptionReason) || [self.exceptionReason isEqualToString:errorLog.exceptionReason]) && + ((!self.selectorRegisterValue && !errorLog.selectorRegisterValue) || + ([self.selectorRegisterValue isEqualToString:errorLog.selectorRegisterValue])) && + ((!self.threads && !errorLog.threads) || [self.threads isEqualToArray:errorLog.threads]) && + ((!self.binaries && !errorLog.binaries) || [self.binaries isEqualToArray:errorLog.binaries]) && + ((!self.registers && !errorLog.registers) || [self.registers isEqualToDictionary:errorLog.registers]) && + ((!self.exception && !errorLog.exception) || [self.exception isEqual:errorLog.exception]); +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if (self) { + _primaryArchitectureId = [coder decodeObjectForKey:kMSACPrimaryArchitectureId]; + _architectureVariantId = [coder decodeObjectForKey:kMSACArchitectureVariantId]; + _applicationPath = [coder decodeObjectForKey:kMSACApplicationPath]; + _osExceptionType = [coder decodeObjectForKey:kMSACOsExceptionType]; + _osExceptionCode = [coder decodeObjectForKey:kMSACOsExceptionCode]; + _osExceptionAddress = [coder decodeObjectForKey:kMSACOsExceptionAddress]; + _exceptionType = [coder decodeObjectForKey:kMSACExceptionType]; + _exceptionReason = [coder decodeObjectForKey:kMSACExceptionReason]; + _selectorRegisterValue = [coder decodeObjectForKey:kMSACSelectorRegisterValue]; + _threads = [coder decodeObjectForKey:kMSACThreads]; + _binaries = [coder decodeObjectForKey:kMSACBinaries]; + _registers = [coder decodeObjectForKey:kMSACRegisters]; + _exception = [coder decodeObjectForKey:kMSACException]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.primaryArchitectureId forKey:kMSACPrimaryArchitectureId]; + [coder encodeObject:self.architectureVariantId forKey:kMSACArchitectureVariantId]; + [coder encodeObject:self.applicationPath forKey:kMSACApplicationPath]; + [coder encodeObject:self.osExceptionType forKey:kMSACOsExceptionType]; + [coder encodeObject:self.osExceptionCode forKey:kMSACOsExceptionCode]; + [coder encodeObject:self.osExceptionAddress forKey:kMSACOsExceptionAddress]; + [coder encodeObject:self.exceptionType forKey:kMSACExceptionType]; + [coder encodeObject:self.exceptionReason forKey:kMSACExceptionReason]; + [coder encodeObject:self.selectorRegisterValue forKey:kMSACSelectorRegisterValue]; + [coder encodeObject:self.threads forKey:kMSACThreads]; + [coder encodeObject:self.binaries forKey:kMSACBinaries]; + [coder encodeObject:self.registers forKey:kMSACRegisters]; + [coder encodeObject:self.exception forKey:kMSACException]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACBinary.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACBinary.h new file mode 100644 index 0000000000..4065c80bf9 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACBinary.h @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "AppCenter+Internal.h" +#import "MSACSerializableObject.h" + +/** + * Binary (library) definition for any platform. + */ +@interface MSACBinary : NSObject + +/** + * The binary id as UUID string. + */ +@property(nonatomic, copy) NSString *binaryId; + +/** + * The binary's start address. + */ +@property(nonatomic, copy) NSString *startAddress; + +/** + * The binary's end address. + */ +@property(nonatomic, copy) NSString *endAddress; + +/** + * The binary's name. + */ +@property(nonatomic, copy) NSString *name; + +/** + * The path to the binary. + */ +@property(nonatomic, copy) NSString *path; + +/** + * The architecture. + */ +@property(nonatomic, copy) NSString *architecture; + +/** + * CPU primary architecture [optional]. + */ +@property(nonatomic) NSNumber *primaryArchitectureId; + +/** + * CPU architecture variant [optional]. + */ +@property(nonatomic) NSNumber *architectureVariantId; + +/** + * Checks if the object's values are valid. + * + * @return YES, if the object is valid. + */ +- (BOOL)isValid; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACBinary.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACBinary.m new file mode 100644 index 0000000000..576229624e --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACBinary.m @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACBinary.h" + +static NSString *const kMSACId = @"id"; +static NSString *const kMSACStartAddress = @"startAddress"; +static NSString *const kMSACEndAddress = @"endAddress"; +static NSString *const kMSACName = @"name"; +static NSString *const kMSACPath = @"path"; +static NSString *const kMSACArchitecture = @"architecture"; +static NSString *const kMSACPrimaryArchitectureId = @"primaryArchitectureId"; +static NSString *const kMSACArchitectureVariantId = @"architectureVariantId"; + +@implementation MSACBinary + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [NSMutableDictionary new]; + + if (self.binaryId) { + dict[kMSACId] = self.binaryId; + } + if (self.startAddress) { + dict[kMSACStartAddress] = self.startAddress; + } + if (self.endAddress) { + dict[kMSACEndAddress] = self.endAddress; + } + if (self.name) { + dict[kMSACName] = self.name; + } + if (self.path) { + dict[kMSACPath] = self.path; + } + if (self.architecture) { + dict[kMSACArchitecture] = self.architecture; + } + if (self.primaryArchitectureId) { + dict[kMSACPrimaryArchitectureId] = self.primaryArchitectureId; + } + if (self.architectureVariantId) { + dict[kMSACArchitectureVariantId] = self.architectureVariantId; + } + + return dict; +} + +- (BOOL)isValid { + return MSACLOG_VALIDATE_NOT_NIL(binaryId) && MSACLOG_VALIDATE_NOT_NIL(startAddress) && MSACLOG_VALIDATE_NOT_NIL(endAddress) && + MSACLOG_VALIDATE_NOT_NIL(name) && MSACLOG_VALIDATE_NOT_NIL(path); +} + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACBinary class]]) { + return NO; + } + MSACBinary *binary = (MSACBinary *)object; + return ((!self.binaryId && !binary.binaryId) || [self.binaryId isEqualToString:binary.binaryId]) && + ((!self.startAddress && !binary.startAddress) || [self.startAddress isEqualToString:binary.startAddress]) && + ((!self.endAddress && !binary.endAddress) || [self.endAddress isEqualToString:binary.endAddress]) && + ((!self.name && !binary.name) || [self.name isEqualToString:binary.name]) && + ((!self.path && !binary.path) || [self.path isEqualToString:binary.path]) && + ((!self.architecture && !binary.architecture) || [self.architecture isEqualToString:binary.architecture]) && + ((!self.primaryArchitectureId && !binary.primaryArchitectureId) || + [self.primaryArchitectureId isEqual:binary.primaryArchitectureId]) && + ((!self.architectureVariantId && !binary.architectureVariantId) || + [self.architectureVariantId isEqual:binary.architectureVariantId]); +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super init]; + if (self) { + _binaryId = [coder decodeObjectForKey:kMSACId]; + _startAddress = [coder decodeObjectForKey:kMSACStartAddress]; + _endAddress = [coder decodeObjectForKey:kMSACEndAddress]; + _name = [coder decodeObjectForKey:kMSACName]; + _path = [coder decodeObjectForKey:kMSACPath]; + _architecture = [coder decodeObjectForKey:kMSACArchitecture]; + _primaryArchitectureId = [coder decodeObjectForKey:kMSACPrimaryArchitectureId]; + _architectureVariantId = [coder decodeObjectForKey:kMSACArchitectureVariantId]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:self.binaryId forKey:kMSACId]; + [coder encodeObject:self.startAddress forKey:kMSACStartAddress]; + [coder encodeObject:self.endAddress forKey:kMSACEndAddress]; + [coder encodeObject:self.name forKey:kMSACName]; + [coder encodeObject:self.path forKey:kMSACPath]; + [coder encodeObject:self.architecture forKey:kMSACArchitecture]; + [coder encodeObject:self.primaryArchitectureId forKey:kMSACPrimaryArchitectureId]; + [coder encodeObject:self.architectureVariantId forKey:kMSACArchitectureVariantId]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACErrorAttachmentLogInternal.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACErrorAttachmentLogInternal.h new file mode 100644 index 0000000000..37abba13c4 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACErrorAttachmentLogInternal.h @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAbstractLogInternal.h" +#import "MSACErrorAttachmentLog.h" + +/** + * Error attachment log. + */ +@interface MSACErrorAttachmentLog () + +/** + * Error attachment identifier. + */ +@property(nonatomic, copy) NSString *attachmentId; + +/** + * Error log identifier to attach this log to. + */ +@property(nonatomic, copy) NSString *errorId; + +/** + * Checks if the object's values are valid. + * + * @return YES, if the object is valid. + */ +- (BOOL)isValid; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACErrorReportPrivate.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACErrorReportPrivate.h new file mode 100644 index 0000000000..84cc6e1c62 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACErrorReportPrivate.h @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACErrorReport.h" + +static NSString *const kMSACErrorReportKillSignal = @"SIGKILL"; + +@interface MSACErrorReport () + +- (instancetype)initWithErrorId:(NSString *)errorId + reporterKey:(NSString *)reporterKey + signal:(NSString *)signal + exceptionName:(NSString *)exceptionName + exceptionReason:(NSString *)exceptionReason + appStartTime:(NSDate *)appStartTime + appErrorTime:(NSDate *)appErrorTime + device:(MSACDevice *)device + appProcessIdentifier:(NSUInteger)appProcessIdentifier; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACException.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACException.h new file mode 100644 index 0000000000..8ae0b40867 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACException.h @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "AppCenter+Internal.h" +#import "MSACSerializableObject.h" + +@class MSACStackFrame; + +@interface MSACException : NSObject + +/* + * Exception type. + */ +@property(nonatomic, copy) NSString *type; + +/* + * Exception reason. + */ +@property(nonatomic, copy) NSString *message; + +/* + * Raw stack trace. Sent when the frames property is either missing or unreliable. + */ +@property(nonatomic, copy) NSString *stackTrace; + +/* + * Stack frames [optional]. + */ +@property(nonatomic) NSArray *frames; + +/* + * Inner exceptions of this exception [optional]. + */ +@property(nonatomic) NSArray *innerExceptions; + +/* + * Name of the wrapper SDK that emitted this exception. + * Consists of the name of the SDK and the wrapper platform, e.g. "appcenter.xamarin", "hockeysdk.cordova". + */ +@property(nonatomic, copy) NSString *wrapperSdkName; + +/** + * Checks if the object's values are valid. + * + * @return YES, if the object is valid. + */ +- (BOOL)isValid; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACException.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACException.m new file mode 100644 index 0000000000..ab803754fc --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACException.m @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACException.h" +#import "MSACStackFrame.h" + +static NSString *const kMSACExceptionType = @"type"; +static NSString *const kMSACMessage = @"message"; +static NSString *const kMSACFrames = @"frames"; +static NSString *const kMSACStackTrace = @"stackTrace"; +static NSString *const kMSACInnerExceptions = @"innerExceptions"; +static NSString *const kMSACWrapperSDKName = @"wrapperSdkName"; + +@implementation MSACException + +- (NSMutableDictionary *)serializeToDictionary { + + NSMutableDictionary *dict = [NSMutableDictionary new]; + + if (self.type) { + dict[kMSACExceptionType] = self.type; + } + if (self.message) { + dict[kMSACMessage] = self.message; + } + if (self.stackTrace) { + dict[kMSACStackTrace] = self.stackTrace; + } + if (self.wrapperSdkName) { + dict[kMSACWrapperSDKName] = self.wrapperSdkName; + } + if (self.frames) { + NSMutableArray *framesArray = [NSMutableArray array]; + for (MSACStackFrame *frame in self.frames) { + [framesArray addObject:[frame serializeToDictionary]]; + } + dict[kMSACFrames] = framesArray; + } + if (self.innerExceptions) { + NSMutableArray *exceptionsArray = [NSMutableArray array]; + for (MSACException *exception in self.innerExceptions) { + [exceptionsArray addObject:[exception serializeToDictionary]]; + } + dict[kMSACInnerExceptions] = exceptionsArray; + } + + return dict; +} + +- (BOOL)isValid { + return MSACLOG_VALIDATE_NOT_NIL(type) && MSACLOG_VALIDATE(frames, [self.frames count] > 0); +} + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACException class]]) { + return NO; + } + MSACException *exception = (MSACException *)object; + return ((!self.type && !exception.type) || [self.type isEqualToString:exception.type]) && + ((!self.wrapperSdkName && !exception.wrapperSdkName) || [self.wrapperSdkName isEqualToString:exception.wrapperSdkName]) && + ((!self.message && !exception.message) || [self.message isEqualToString:exception.message]) && + ((!self.frames && !exception.frames) || [self.frames isEqualToArray:exception.frames]) && + ((!self.innerExceptions && !exception.innerExceptions) || [self.innerExceptions isEqualToArray:exception.innerExceptions]) && + ((!self.stackTrace && !exception.stackTrace) || [self.stackTrace isEqualToString:exception.stackTrace]); +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super init]; + if (self) { + _type = [coder decodeObjectForKey:kMSACExceptionType]; + _message = [coder decodeObjectForKey:kMSACMessage]; + _stackTrace = [coder decodeObjectForKey:kMSACStackTrace]; + _frames = [coder decodeObjectForKey:kMSACFrames]; + _innerExceptions = [coder decodeObjectForKey:kMSACInnerExceptions]; + _wrapperSdkName = [coder decodeObjectForKey:kMSACWrapperSDKName]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:self.type forKey:kMSACExceptionType]; + [coder encodeObject:self.message forKey:kMSACMessage]; + [coder encodeObject:self.stackTrace forKey:kMSACStackTrace]; + [coder encodeObject:self.frames forKey:kMSACFrames]; + [coder encodeObject:self.innerExceptions forKey:kMSACInnerExceptions]; + [coder encodeObject:self.wrapperSdkName forKey:kMSACWrapperSDKName]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACHandledErrorLog.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACHandledErrorLog.h new file mode 100644 index 0000000000..859c90d302 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACHandledErrorLog.h @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAbstractErrorLog.h" + +@class MSACException; + +/** + * Handled Error log for managed platforms (such as Xamarin, Unity, Android Dalvik/ART). + */ +@interface MSACHandledErrorLog : MSACLogWithProperties + +/** + * Unique identifier for this error. + */ +@property(nonatomic, copy) NSString *errorId; + +/** + * The exception. + */ +@property(nonatomic) MSACException *exception; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACHandledErrorLog.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACHandledErrorLog.m new file mode 100644 index 0000000000..44774f2ef4 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACHandledErrorLog.m @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACHandledErrorLog.h" +#import "MSACException.h" + +static NSString *const kMSACTypeError = @"handledError"; +static NSString *const kMSACId = @"id"; +static NSString *const kMSACException = @"exception"; + +@implementation MSACHandledErrorLog + +- (instancetype)init { + if ((self = [super init])) { + self.type = kMSACTypeError; + } + return self; +} + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary]; + + if (self.errorId) { + dict[kMSACId] = self.errorId; + } + if (self.exception) { + dict[kMSACException] = [self.exception serializeToDictionary]; + } + return dict; +} + +- (BOOL)isValid { + return [super isValid] && MSACLOG_VALIDATE_NOT_NIL(errorId) && MSACLOG_VALIDATE_NOT_NIL(exception); +} + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACHandledErrorLog class]] || ![super isEqual:object]) { + return NO; + } + MSACHandledErrorLog *errorLog = (MSACHandledErrorLog *)object; + return ((!self.errorId && !errorLog.errorId) || [self.errorId isEqual:errorLog.errorId]) && + ((!self.exception && !errorLog.exception) || [self.exception isEqual:errorLog.exception]); +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if (self) { + _errorId = [coder decodeObjectForKey:kMSACId]; + _exception = [coder decodeObjectForKey:kMSACException]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.errorId forKey:kMSACId]; + [coder encodeObject:self.exception forKey:kMSACException]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACStackFrame.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACStackFrame.h new file mode 100644 index 0000000000..d5433b79ce --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACStackFrame.h @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "AppCenter+Internal.h" +#import "MSACSerializableObject.h" + +@interface MSACStackFrame : NSObject + +/* + * Frame address [optional]. + */ +@property(nonatomic, copy) NSString *address; + +/* + * Symbolized code line [optional]. + */ +@property(nonatomic, copy) NSString *code; + +/* + * The fully qualified name of the Class containing the execution point represented by this stack trace element [optional]. + */ +@property(nonatomic, copy) NSString *className; + +/* + * The name of the method containing the execution point represented by this stack trace element [optional]. + */ +@property(nonatomic, copy) NSString *methodName; + +/* + * The line number of the source line containing the execution point represented by this stack trace element [optional]. + */ +@property(nonatomic, copy) NSNumber *lineNumber; + +/* + * The name of the file containing the execution point represented by this stack trace element [optional]. + */ +@property(nonatomic, copy) NSString *fileName; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACStackFrame.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACStackFrame.m new file mode 100644 index 0000000000..1f6f614aa0 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACStackFrame.m @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACStackFrame.h" + +static NSString *const kMSACAddress = @"address"; +static NSString *const kMSACCode = @"code"; +static NSString *const kMSACClassName = @"className"; +static NSString *const kMSACMethodName = @"methodName"; +static NSString *const kMSACLineNumber = @"lineNumber"; +static NSString *const kMSACFileName = @"fileName"; + +@implementation MSACStackFrame + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [NSMutableDictionary new]; + + if (self.address) { + dict[kMSACAddress] = self.address; + } + if (self.code) { + dict[kMSACCode] = self.code; + } + if (self.className) { + dict[kMSACClassName] = self.className; + } + if (self.methodName) { + dict[kMSACMethodName] = self.methodName; + } + if (self.lineNumber) { + dict[kMSACLineNumber] = self.lineNumber; + } + if (self.fileName) { + dict[kMSACFileName] = self.fileName; + } + return dict; +} + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACStackFrame class]]) { + return NO; + } + MSACStackFrame *frame = (MSACStackFrame *)object; + return ((!self.address && !frame.address) || [self.address isEqualToString:frame.address]) && + ((!self.code && !frame.code) || [self.code isEqualToString:frame.code]) && + ((!self.className && !frame.className) || [self.className isEqualToString:frame.className]) && + ((!self.methodName && !frame.methodName) || [self.methodName isEqualToString:frame.methodName]) && + ((!self.lineNumber && !frame.lineNumber) || [self.lineNumber isEqual:frame.lineNumber]) && + ((!self.fileName && !frame.fileName) || [self.fileName isEqualToString:frame.fileName]); +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super init]; + if (self) { + _address = [coder decodeObjectForKey:kMSACAddress]; + _code = [coder decodeObjectForKey:kMSACCode]; + _className = [coder decodeObjectForKey:kMSACClassName]; + _methodName = [coder decodeObjectForKey:kMSACMethodName]; + _lineNumber = [coder decodeObjectForKey:kMSACLineNumber]; + _fileName = [coder decodeObjectForKey:kMSACFileName]; + } + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:self.address forKey:kMSACAddress]; + [coder encodeObject:self.code forKey:kMSACCode]; + [coder encodeObject:self.className forKey:kMSACClassName]; + [coder encodeObject:self.methodName forKey:kMSACMethodName]; + [coder encodeObject:self.lineNumber forKey:kMSACLineNumber]; + [coder encodeObject:self.fileName forKey:kMSACFileName]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACThread.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACThread.h new file mode 100644 index 0000000000..db3c2def46 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACThread.h @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "AppCenter+Internal.h" + +@class MSACException; +@class MSACStackFrame; + +@interface MSACThread : NSObject + +/** + * Thread identifier. + */ +@property(nonatomic) NSNumber *threadId; + +/** + * Thread name. [optional] + */ +@property(nonatomic, copy) NSString *name; + +/** + * Stack frames. + */ +@property(nonatomic) NSMutableArray *frames; + +/** + * The last exception backtrace. + */ +@property(nonatomic) MSACException *exception; + +/** + * Checks if the object's values are valid. + * + * @return YES, if the object is valid. + */ +- (BOOL)isValid; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACThread.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACThread.m new file mode 100644 index 0000000000..ca78a6f3ec --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACThread.m @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACThread.h" +#import "MSACException.h" +#import "MSACStackFrame.h" + +static NSString *const kMSACThreadId = @"id"; +static NSString *const kMSACName = @"name"; +static NSString *const kMSACStackFrames = @"frames"; +static NSString *const kMSACException = @"exception"; + +@implementation MSACThread + +// Initializes a new instance of the class. +- (instancetype)init { + if ((self = [super init])) { + _frames = [NSMutableArray array]; + } + return self; +} + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [NSMutableDictionary new]; + + if (self.threadId) { + dict[kMSACThreadId] = self.threadId; + } + if (self.name) { + dict[kMSACName] = self.name; + } + + if (self.frames) { + NSMutableArray *framesArray = [NSMutableArray array]; + for (MSACStackFrame *frame in self.frames) { + [framesArray addObject:[frame serializeToDictionary]]; + } + dict[kMSACStackFrames] = framesArray; + } + + if (self.exception) { + dict[kMSACException] = [self.exception serializeToDictionary]; + } + + return dict; +} + +- (BOOL)isValid { + return MSACLOG_VALIDATE_NOT_NIL(threadId) && MSACLOG_VALIDATE(frames, [self.frames count] > 0); +} + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACThread class]]) { + return NO; + } + MSACThread *thread = (MSACThread *)object; + return ((!self.threadId && !thread.threadId) || [self.threadId isEqual:thread.threadId]) && + ((!self.name && !thread.name) || [self.name isEqualToString:thread.name]) && + ((!self.frames && !thread.frames) || [self.frames isEqualToArray:thread.frames]) && + ((!self.exception && !thread.exception) || [self.exception isEqual:thread.exception]); +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super init]; + if (self) { + _threadId = [coder decodeObjectForKey:kMSACThreadId]; + _name = [coder decodeObjectForKey:kMSACName]; + _frames = [coder decodeObjectForKey:kMSACStackFrames]; + _exception = [coder decodeObjectForKey:kMSACException]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:self.threadId forKey:kMSACThreadId]; + [coder encodeObject:self.name forKey:kMSACName]; + [coder encodeObject:self.frames forKey:kMSACStackFrames]; + [coder encodeObject:self.exception forKey:kMSACException]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACWrapperException.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACWrapperException.h new file mode 100644 index 0000000000..ee4cefa1d0 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACWrapperException.h @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +@class MSACException; + +/** + * This class represents a wrapper exception that augments the data recorded when the application crashes. + */ +@interface MSACWrapperException : NSObject + +/** + * The model exception for the corresponding crash. + */ +@property(nonatomic) MSACException *modelException; + +/** + * Additional data that the wrapper SDK needs to save. + */ +@property(nonatomic) NSData *exceptionData; + +/** + * Id of the crashed process; used for correlation to a PLCrashReport. + */ +@property(nonatomic, copy) NSNumber *processId; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACWrapperException.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACWrapperException.m new file mode 100644 index 0000000000..ee5b5b5266 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACWrapperException.m @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACException.h" +#import "MSACWrapperExceptionInternal.h" + +@implementation MSACWrapperException + +static NSString *const kMSACModelException = @"modelException"; +static NSString *const kMSACExceptionData = @"exceptionData"; +static NSString *const KMSACProcessId = @"processId"; + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [NSMutableDictionary new]; + if (self.modelException) { + dict[kMSACModelException] = [self.modelException serializeToDictionary]; + } + if (self.processId) { + dict[KMSACProcessId] = self.processId; + } + if (self.exceptionData) { + dict[kMSACExceptionData] = self.exceptionData; + } + return dict; +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super init]; + if (self) { + self.modelException = [coder decodeObjectForKey:kMSACModelException]; + self.exceptionData = [coder decodeObjectForKey:kMSACExceptionData]; + self.processId = [coder decodeObjectForKey:KMSACProcessId]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:self.modelException forKey:kMSACModelException]; + [coder encodeObject:self.exceptionData forKey:kMSACExceptionData]; + [coder encodeObject:self.processId forKey:KMSACProcessId]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACWrapperExceptionInternal.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACWrapperExceptionInternal.h new file mode 100644 index 0000000000..8e057426a1 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Model/MSACWrapperExceptionInternal.h @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACSerializableObject.h" +#import "MSACWrapperException.h" + +/** + * MSACWrapperException must be serializable, but only internally (so that MSACSerializableObject does not need to be bound for wrapper + * SDKs) + */ +@interface MSACWrapperException () +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Util/MSACApplicationForwarder.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Util/MSACApplicationForwarder.h new file mode 100644 index 0000000000..b8c55ae94f --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Util/MSACApplicationForwarder.h @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface MSACApplicationForwarder : NSObject + +/** + * Register forwarding on `NSApplication` via swizzling. + */ ++ (void)registerForwarding; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Util/MSACApplicationForwarder.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Util/MSACApplicationForwarder.m new file mode 100644 index 0000000000..6c719bb74b --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Util/MSACApplicationForwarder.m @@ -0,0 +1,228 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAppCenterInternal.h" +#import "MSACApplicationForwarder.h" +#import "MSACCrashesInternal.h" +#import "MSACCrashesPrivate.h" +#import "MSACUtility+Application.h" + +static NSString *const kMSACAppCenterApplicationForwarderEnabledKey = @"AppCenterApplicationForwarderEnabled"; + +static BOOL isApplicationForwarderEnabled() { + NSNumber *forwarderEnabled = [NSBundle.mainBundle objectForInfoDictionaryKey:kMSACAppCenterApplicationForwarderEnabledKey]; + return forwarderEnabled ? [forwarderEnabled boolValue] : YES; +} + +#if TARGET_OS_OSX + +/** + * The flag to allow crashing on uncaught exceptions thrown on the main thread. + */ +static NSString *const kMSACCrashOnExceptionsKey = @"NSApplicationCrashOnExceptions"; + +static BOOL isCrashOnExceptionsEnabled() { + + // We use NSUserDefaults here instead of MSACAppCenterUserDefaults, because + // we should use system user defaults for system keys. + // MSACAppCenterUserDefaults prepends all the keys with "MSAppCenter" prefix. + NSNumber *crashOnExceptions = [[NSUserDefaults standardUserDefaults] objectForKey:kMSACCrashOnExceptionsKey]; + return [crashOnExceptions boolValue]; +} + +/* + * On OS X runtime, not all uncaught exceptions end in a custom `NSUncaughtExceptionHandler`. + * In addition "sometimes" exceptions don't even cause the app to crash, depending on where and + * when they happen. + * + * Here are the known scenarios: + * + * 1. Custom `NSUncaughtExceptionHandler` don't start working until after `NSApplication` has finished + * calling all of its delegate methods! + * + * Example: + * - (void)applicationDidFinishLaunching:(NSNotification *)note { + * ... + * [NSException raise:@"ExceptionAtStartup" format:@"This will not be recognized!"]; + * ... + * } + * + * + * 2. The default `NSUncaughtExceptionHandler` in `NSApplication` only logs exceptions to the console and + * ends their processing. Resulting in exceptions that occur in the `NSApplication` "scope" not + * occurring in a registered custom `NSUncaughtExceptionHandler`. + * + * Example: + * - (void)applicationDidFinishLaunching:(NSNotification *)note { + * ... + * [self performSelector:@selector(delayedException) withObject:nil afterDelay:5]; + * ... + * } + * + * - (void)delayedException { + * NSArray *array = [NSArray array]; + * [array objectAtIndex:23]; + * } + * + * 3. Any exceptions occurring in IBAction or other GUI does not even reach the NSApplication default + * UncaughtExceptionHandler. + * + * Example: + * - (IBAction)doExceptionCrash:(id)sender { + * NSArray *array = [NSArray array]; + * [array objectAtIndex:23]; + * } + * + * + * Solution A: + * + * Implement `NSExceptionHandler` and set the `ExceptionHandlingMask` to `NSLogAndHandleEveryExceptionMask` + * + * Benefits: + * + * 1. Solves all of the above scenarios. + * + * 2. Clean solution using a standard Cocoa System specifically meant for this purpose. + * + * 3. Safe. Doesn't use private API. + * + * Problems: + * + * 1. To catch all exceptions the `NSExceptionHandlers` mask has to include `NSLogOtherExceptionMask` and + * `NSHandleOtherExceptionMask`. But this will result in @catch blocks to be called after the exception + * handler processed the exception and likely lets the app crash and create a crash report. + * This makes the @catch block basically not work at all. + * + * 2. If anywhere in the app a custom `NSUncaughtExceptionHandler` will be registered, e.g. in a closed source + * library the developer has to use, the complete mechanism will stop working. + * + * 3. Not clear if this solves all scenarios there can be. + * + * 4. Requires to adjust PLCrashReporter not to register its `NSUncaughtExceptionHandler` which is not a good idea, + * since it would require the `NSExceptionHandler` would catch *all* exceptions and that would cause + * PLCrashReporter to stop all running threads every time an exception occurs even if it will be handled right + * away, e.g. by a system framework. + * + * + * Solution B: + * + * Overwrite and extend specific methods of `NSApplication`. Can be implemented via subclassing NSApplication or + * by using a category. + * + * Benefits: + * + * 1. Solves scenarios 2 (by overwriting `reportException:`) and 3 (by overwriting `sendEvent:`). + * + * 2. Subclassing approach isn't enforcing the mechanism onto apps and lets developers opt-in. + * (Category approach would enforce it and rather be a problem of this solution.) + * + * 3. Safe. Doesn't use private API. + * + * Problems: + * + * 1. Does not automatically solve scenario 1. Developer would have to put all that code into @try @catch blocks. + * + * 2. Not a clean implementation, rather feels like a workaround. + * + * 3. Not clear if this solves all scenarios there can be. + * + * + * References: + * https://developer.apple.com/library/mac/documentation/cocoa/Conceptual/Exceptions/Tasks/ControllingAppResponse.html#//apple_ref/doc/uid/20000473-BBCHGJIJ + * http://stackoverflow.com/a/4199717/474794 + * http://stackoverflow.com/a/3419073/474794 + * http://macdevcenter.com/pub/a/mac/2007/07/31/understanding-exceptions-and-handlers-in-cocoa.html + * + */ + +#pragma mark Report Exception + +typedef void (*MSACReportExceptionImp)(id, SEL, NSException *); +static MSACReportExceptionImp reportExceptionOriginalImp; + +static void ms_reportException(id self, SEL _cmd, NSException *exception) { + [MSACCrashes applicationDidReportException:exception]; + + // Forward to the original implementation. + reportExceptionOriginalImp(self, _cmd, exception); +} + +static void swizzleReportException() { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + Method originalMethod = class_getInstanceMethod([NSApplication class], @selector(reportException:)); + reportExceptionOriginalImp = (MSACReportExceptionImp)method_setImplementation(originalMethod, (IMP)ms_reportException); + MSACLogDebug([MSACCrashes logTag], @"Selector 'reportException:' of class 'NSApplication' is swizzled."); + }); +} + +#pragma mark Send Event + +typedef void (*MSACSendEventImp)(id, SEL, NSEvent *); +static MSACSendEventImp sendEventOriginalImp; + +static void ms_sendEvent(id self, SEL _cmd, NSEvent *event) { + @try { + + // Forward to the original implementation. + sendEventOriginalImp(self, _cmd, event); + } @catch (NSException *exception) { + ms_reportException(self, @selector(reportException:), exception); + } +} + +static void swizzleSendEvent() { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + Method originalMethod = class_getInstanceMethod([NSApplication class], @selector(sendEvent:)); + sendEventOriginalImp = (MSACSendEventImp)method_setImplementation(originalMethod, (IMP)ms_sendEvent); + MSACLogDebug([MSACCrashes logTag], @"Selector 'sendEvent:' of class 'NSApplication' is swizzled."); + }); +} + +#endif + +@implementation MSACApplicationForwarder + ++ (void)registerForwarding { + if (isApplicationForwarderEnabled()) { + MSACLogDebug([MSACCrashes logTag], @"Application forwarder for info.plist key '%@' enabled. It may use swizzling.", + kMSACAppCenterApplicationForwarderEnabledKey); + } else { + MSACLogDebug([MSACCrashes logTag], @"Application forwarder for info.plist key '%@' disabled. It won't use swizzling.", + kMSACAppCenterApplicationForwarderEnabledKey); + return; + } +#if TARGET_OS_OSX + if (isCrashOnExceptionsEnabled()) { + + /* + * Solution for Scenario 2: + * + * Catch all exceptions that are being logged to the console and forward them to our + * custom UncaughtExceptionHandler. + */ + swizzleReportException(); + + /* + * Solution for Scenario 3: + * + * Exceptions that happen inside an IBAction implementation do not trigger a call to + * [NSApplication reportException:] and it does not trigger a registered UncaughtExceptionHandler + * Hence we need to catch these ourselves, e.g. by overwriting sendEvent: as done right here. + * + * On 64bit systems the @try @catch block doesn't even cost any performance. + */ + swizzleSendEvent(); + } else { + MSACLogInfo([MSACCrashes logTag], + @"Catching uncaught exceptions thrown on the main thread disabled. " + @"Set `%@` flag before SDK initialization, to allow crash on uncaught exceptions and the SDK can report them.", + kMSACCrashOnExceptionsKey); + } +#endif +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Util/MSACCrashReporter.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Util/MSACCrashReporter.h new file mode 100644 index 0000000000..91f923a5ff --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Util/MSACCrashReporter.h @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// -reserved-id-macro +// PLCrashReporter uses lots of _-prefixed macros and it's not an issue but +// enabling -Weveryting being too pedantic. +// https://stackoverflow.com/questions/228783/what-are-the-rules-about-using-an-underscore-in-a-c-identifier +// explains rules about using an underscore macro pretty well. + +// -disabled-macro-expansion +// This silences warnings when consuming PLCrashReporter for macOS. The warning +// here actually just complains about regular Preprocessor behavior. + +// -objc-interface-ivars +// This causes warnings when consuming PLCrashReporter for macOS. It complains +// about the old way of defining private ivars. PLCrashReporter doesn't use ARC, +// so we cannot just remove the old ivars and be done. Changing PLCrashReporter +// just because of this warning doesn't make any sense. + +// -documentation-unknown-command +// This causes 1 warning when consuming PLCrashReporter for macOS. The reason +// for the warning is that PLCRashReporter exposes Doxygen's @internal (it uses +// Doxygen to generate it's header docs) in a public header. There's no problem +// not knowing @internal, so we just ignore the warning. + +// MSAC prefix for PLCrashReporter API is defined in PLCrashNamespace.h and handled +// implicitly by preprocessor, so all API calls can be done without explicit prefix usage. + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreserved-id-macro" +#pragma clang diagnostic ignored "-Wdisabled-macro-expansion" +#pragma clang diagnostic ignored "-Wobjc-interface-ivars" +#pragma clang diagnostic ignored "-Wdocumentation-unknown-command" +#import "CrashReporter.h" +#pragma clang diagnostic pop diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Util/MSACCrashesUtil.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Util/MSACCrashesUtil.h new file mode 100644 index 0000000000..aedf29a3f3 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Util/MSACCrashesUtil.h @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +static NSString *const kMSACCrashesDirectory = @"crashes"; +static NSString *const kMSACLogBufferDirectory = @"crasheslogbuffer"; +static NSString *const kMSACWrapperExceptionsDirectory = @"crasheswrapperexceptions"; + +@interface MSACCrashesUtil : NSObject + +/** + * Returns the directory for storing and reading crash reports for this app. + * + * @return The directory containing crash reports for this app. + */ ++ (NSString *)crashesDir; + +/** + * Returns the directory for storing and reading buffered logs. It will be used in case we crash to make sure we don't lose any data. + * + * @return The directory containing buffered events for an app + */ ++ (NSString *)logBufferDir; + +/** + * Returns the directory for storing and reading wrapper exception data. + * + * @return The directory containing wrapper exception data. + */ ++ (NSString *)wrapperExceptionsDir; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Util/MSACCrashesUtil.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Util/MSACCrashesUtil.m new file mode 100644 index 0000000000..5afb0c7203 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Util/MSACCrashesUtil.m @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACCrashesUtil.h" +#import "MSACUtility+File.h" + +@implementation MSACCrashesUtil + +static dispatch_once_t crashesDirectoryOnceToken; +static dispatch_once_t logBufferDirectoryOnceToken; +static dispatch_once_t wrapperExceptionsDirectoryOnceToken; + +#pragma mark - Public + ++ (NSString *)crashesDir { + dispatch_once(&crashesDirectoryOnceToken, ^{ + [MSACUtility createDirectoryForPathComponent:kMSACCrashesDirectory]; + }); + + return kMSACCrashesDirectory; +} + ++ (NSString *)logBufferDir { + dispatch_once(&logBufferDirectoryOnceToken, ^{ + [MSACUtility createDirectoryForPathComponent:kMSACLogBufferDirectory]; + }); + + return kMSACLogBufferDirectory; +} + ++ (NSString *)wrapperExceptionsDir { + dispatch_once(&wrapperExceptionsDirectoryOnceToken, ^{ + [MSACUtility createDirectoryForPathComponent:kMSACWrapperExceptionsDirectory]; + }); + + return kMSACWrapperExceptionsDirectory; +} + +#pragma mark - Private + ++ (void)resetDirectory { + crashesDirectoryOnceToken = 0; + logBufferDirectoryOnceToken = 0; + wrapperExceptionsDirectoryOnceToken = 0; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Util/MSACCrashesUtilPrivate.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Util/MSACCrashesUtilPrivate.h new file mode 100644 index 0000000000..a86c993dd2 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Util/MSACCrashesUtilPrivate.h @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +@interface MSACCrashesUtil () + +/** + * Method to reset directories when running unit tests only. So calling methods re-generates directories. + */ ++ (void)resetDirectory; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Util/MSACErrorLogFormatter.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Util/MSACErrorLogFormatter.h new file mode 100644 index 0000000000..42e3d356db --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Util/MSACErrorLogFormatter.h @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +@class MSACAppleErrorLog; +@class MSACErrorReport; +@class PLCrashReport; + +/** + * Error logging error domain + */ +typedef NS_ENUM(NSInteger, MSACBinaryImageType) { + + /** + * App binary + */ + MSACBinaryImageTypeAppBinary, + + /** + * App provided framework + */ + MSACBinaryImageTypeAppFramework, + + /** + * Image not related to the app + */ + MSACBinaryImageTypeOther +}; + +@interface MSACErrorLogFormatter : NSObject + ++ (MSACAppleErrorLog *)errorLogFromCrashReport:(PLCrashReport *)report; + ++ (MSACErrorReport *)errorReportFromCrashReport:(PLCrashReport *)report; + ++ (MSACErrorReport *)errorReportFromLog:(MSACAppleErrorLog *)errorLog; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Util/MSACErrorLogFormatter.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Util/MSACErrorLogFormatter.m new file mode 100644 index 0000000000..9ed6dcd7d0 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Util/MSACErrorLogFormatter.m @@ -0,0 +1,786 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/* + * Authors: + * Landon Fuller + * Damian Morris + * Andreas Linde + * + * Copyright (c) 2008-2013 Plausible Labs Cooperative, Inc. + * Copyright (c) 2010 MOSO Corporation, Pty Ltd. + * Copyright (c) 2012-2014 HockeyApp, Bit Stadium GmbH. + * Copyright (c) 2015-16 Microsoft Corporation. + * + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +#import +#import +#import + +#if defined(__OBJC2__) +#define SEL_NAME_SECT "__objc_methname" +#else +#define SEL_NAME_SECT "__cstring" +#endif + +#import "MSACAppCenterInternal.h" +#import "MSACAppleErrorLog.h" +#import "MSACBinary.h" +#import "MSACCrashReporter.h" +#import "MSACCrashesInternal.h" +#import "MSACDeviceTrackerPrivate.h" +#import "MSACErrorLogFormatterPrivate.h" +#import "MSACErrorReportPrivate.h" +#import "MSACException.h" +#import "MSACStackFrame.h" +#import "MSACThread.h" +#import "MSACWrapperException.h" +#import "MSACWrapperExceptionManagerInternal.h" + +static NSString *unknownString = @"???"; + +/** + * Sort PLCrashReportBinaryImageInfo instances by their starting address. + */ +static NSInteger bit_binaryImageSort(id binary1, id binary2, void *__unused context) { + uint64_t addr1 = [(PLCrashReportBinaryImageInfo *)binary1 imageBaseAddress]; + uint64_t addr2 = [(PLCrashReportBinaryImageInfo *)binary2 imageBaseAddress]; + + if (addr1 < addr2) + return NSOrderedAscending; + else if (addr1 > addr2) + return NSOrderedDescending; + else + return NSOrderedSame; +} + +/** + * Validates that the given @a string terminates prior to @a limit. + */ +static const char *safer_string_read(const char *string, const char *limit) { + const char *p = string; + do { + if (p >= limit || p + 1 >= limit) { + return NULL; + } + p++; + } while (*p != '\0'); + + return string; +} + +/** + * The relativeAddress should be ` - `, extracted + * from the crash report's thread + * and binary image list. + * + * For the (architecture-specific) registers to attempt, see: + * http://sealiesoftware.com/blog/archive/2008/09/22/objc_explain_So_you_crashed_in_objc_msgSend.html + */ +static const char *findSEL(const char *imageName, NSString *imageUUID, uint64_t relativeAddress) { + unsigned int images_count = _dyld_image_count(); + for (unsigned int i = 0; i < images_count; ++i) { + intptr_t slide = _dyld_get_image_vmaddr_slide(i); + const struct mach_header *header = _dyld_get_image_header(i); + const struct mach_header_64 *header64 = (const struct mach_header_64 *)header; + const char *name = _dyld_get_image_name(i); + + // Image disappeared?. + if (name == NULL || header == NULL) + continue; + + // Check if this is the correct image. If we were being even more careful, + // we'd check the LC_UUID. + if (strcmp(name, imageName) != 0) + continue; + + // Determine whether this is a 64-bit or 32-bit Mach-O file. + BOOL m64 = NO; + if (header->magic == MH_MAGIC_64) + m64 = YES; + + NSString *uuidString = nil; + const uint8_t *command; + uint32_t ncmds; + + if (m64) { + command = (const uint8_t *)(header64 + 1); + ncmds = header64->ncmds; + } else { + command = (const uint8_t *)(header + 1); + ncmds = header->ncmds; + } + for (uint32_t idx = 0; idx < ncmds; ++idx) { + const struct load_command *load_command = (const struct load_command *)command; + if (load_command->cmd == LC_UUID) { + const struct uuid_command *uuid_command = (const struct uuid_command *)command; + const uint8_t *uuid = uuid_command->uuid; + uuidString = [[NSString stringWithFormat:@"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%" + @"02X%02X%02X%02X%02X", + uuid[0], uuid[1], uuid[2], uuid[3], uuid[4], uuid[5], uuid[6], uuid[7], uuid[8], uuid[9], + uuid[10], uuid[11], uuid[12], uuid[13], uuid[14], uuid[15]] lowercaseString]; + break; + } else { + command += load_command->cmdsize; + } + } + + // Check if this is the correct image by comparing the UUIDs. + if (!uuidString || ![uuidString isEqualToString:imageUUID]) + continue; + + // Fetch the __objc_methname section. + const char *methname_sect; + uint64_t methname_sect_size; + if (m64) { + methname_sect = getsectdatafromheader_64(header64, SEG_TEXT, SEL_NAME_SECT, &methname_sect_size); + } else { + uint32_t meth_size_32; + methname_sect = getsectdatafromheader(header, SEG_TEXT, SEL_NAME_SECT, &meth_size_32); + methname_sect_size = meth_size_32; + } + + // Apply the slide, as per getsectdatafromheader(3) + methname_sect += slide; + if (methname_sect == NULL) { + return NULL; + } + + // Calculate the target address within this image, and verify that it is + // within __objc_methname. + const char *target = ((const char *)header) + relativeAddress; + const char *limit = methname_sect + methname_sect_size; + if (target < methname_sect || target >= limit) { + return NULL; + } + + // Read the actual method name. + return safer_string_read(target, limit); + } + + return NULL; +} + +@implementation MSACErrorLogFormatter + +/** + * Formats the provided report as human-readable text in the given @a + * textFormat, and return the formatted result as a string. + * + * @param report The report to format. + * + * @return Returns the formatted result on success, or nil if an error occurs. + */ ++ (MSACAppleErrorLog *)errorLogFromCrashReport:(PLCrashReport *)report { + MSACAppleErrorLog *errorLog = [MSACAppleErrorLog new]; + + // errorId – Used for de-duplication in case we sent the same crashreport twice. + errorLog.errorId = [self errorIdForCrashReport:report]; + + // Set applicationpath and process info. + errorLog = [self addProcessInfoAndApplicationPathTo:errorLog fromCrashReport:report]; + + // Find the crashed thread. + PLCrashReportThreadInfo *crashedThread = [self findCrashedThreadInReport:report]; + + // Error Thread Id from the crashed thread. + errorLog.errorThreadId = @(crashedThread.threadNumber); + + // errorLog.errorThreadName won't be used on iOS right now, this will be + // relevant for handled exceptions. + + // All errors are fatal for now, until we add support for handled exceptions. + errorLog.fatal = YES; + + // Application launch and crash timestamps + errorLog.appLaunchTimestamp = [self getAppLaunchTimeFromReport:report]; + errorLog.timestamp = [self getCrashTimeFromReport:report]; + + // FIXME: PLCrashReporter doesn't support millisecond precision, here is a + // workaround to fill 999 for its millisecond. + double timestampInSeconds = [errorLog.timestamp timeIntervalSince1970]; + if (timestampInSeconds - (int)timestampInSeconds == 0) { + errorLog.timestamp = [NSDate dateWithTimeIntervalSince1970:(timestampInSeconds + 0.999)]; + } + + // CPU Type and Subtype for the crash. We need to query the binary images for that. + uint64_t type = report.machineInfo.processorInfo.type; + uint64_t subtype = report.machineInfo.processorInfo.subtype; + for (PLCrashReportBinaryImageInfo *image in report.images) { + if (image.codeType != nil && image.codeType.typeEncoding == PLCrashReportProcessorTypeEncodingMach) { + type = image.codeType.type; + subtype = image.codeType.subtype; + break; + } + } + BOOL is64bit = [self isCodeType64bit:type]; + errorLog.primaryArchitectureId = @(type); + errorLog.architectureVariantId = @(subtype); + + /* + * errorLog.architecture is an optional. The Android SDK will set it while for + * iOS, the file will be set on the server using primaryArchitectureId and + * architectureVariantId. + */ + + /* + * HockeyApp didn't use report.exceptionInfo for this field but exception.name + * in case of an unhandled exception or the report.signalInfo.name. More so, + * for BITCrashDetails, we used the exceptionInfo.exceptionName for a field + * called exceptionName. + */ + errorLog.osExceptionType = report.signalInfo.name; + errorLog.osExceptionCode = report.signalInfo.code; + errorLog.osExceptionAddress = [NSString stringWithFormat:@"0x%" PRIx64, report.signalInfo.address]; + + // We need the architecture of the system and the crashed thread to get the + // exceptionReason, threads and registers. + errorLog.exceptionReason = [self extractExceptionReasonFromReport:report]; + errorLog.exceptionType = report.hasExceptionInfo ? report.exceptionInfo.exceptionName : nil; + + // The registers of the crashed thread might contain the last method call, + // this can be very helpful. + errorLog.selectorRegisterValue = [self selectorRegisterValueFromReport:report ofCrashedThread:crashedThread codeType:type]; + + // Extract all threads and registers. + errorLog.threads = [self extractThreadsFromReport:report crashedThread:crashedThread is64bit:is64bit]; + errorLog.registers = [self extractRegistersFromCrashedThread:crashedThread is64bit:is64bit]; + errorLog.binaries = [self extractBinaryImagesFromReport:report is64bit:is64bit]; + + /* + * Set the device here to make sure we don't use the current device + * information but the one from history that matches the time of our crash. + */ + errorLog.device = [[MSACDeviceTracker sharedInstance] deviceForTimestamp:errorLog.timestamp]; + + // Set the exception from the wrapper SDK. + MSACWrapperException *wrapperException = + [MSACWrapperExceptionManager loadWrapperExceptionWithUUIDString:[self uuidRefToString:report.uuidRef]]; + if (wrapperException) { + errorLog.exception = wrapperException.modelException; + } + return errorLog; +} + ++ (MSACErrorReport *)errorReportFromCrashReport:(PLCrashReport *)report { + if (!report) { + return nil; + } + + MSACAppleErrorLog *errorLog = [self errorLogFromCrashReport:report]; + MSACErrorReport *errorReport = [self errorReportFromLog:errorLog]; + return errorReport; +} + ++ (MSACErrorReport *)errorReportFromLog:(MSACAppleErrorLog *)errorLog { + MSACErrorReport *errorReport = nil; + NSString *errorId = errorLog.errorId; + + /* + * There should always be an installId. Leaving the empty string out of + * paranoia as [UUID UUID] – used in [MSACAppCenter installId] – might, in + * theory, return nil. + */ + NSString *reporterKey = [[MSACAppCenter installId] UUIDString] ?: @""; + + NSString *signal = errorLog.osExceptionType; + NSString *exceptionReason = errorLog.exceptionReason; + NSString *exceptionName = errorLog.exceptionType; + NSDate *appStartTime = errorLog.appLaunchTimestamp; + NSDate *appErrorTime = errorLog.timestamp; + + // Retrieve the process' id. + NSUInteger processId = [errorLog.processId unsignedIntegerValue]; + + // Retrieve the device that correlates with the time of a crash. + MSACDevice *device = [[MSACDeviceTracker sharedInstance] deviceForTimestamp:errorLog.timestamp]; + + // Finally create the MSACErrorReport instance. + errorReport = [[MSACErrorReport alloc] initWithErrorId:errorId + reporterKey:reporterKey + signal:signal + exceptionName:exceptionName + exceptionReason:exceptionReason + appStartTime:appStartTime + appErrorTime:appErrorTime + device:device + appProcessIdentifier:processId]; + + return errorReport; +} + +#pragma mark - Private + +#pragma mark - Parse PLCrashReport + ++ (NSString *)errorIdForCrashReport:(PLCrashReport *)report { + NSString *errorId = report.uuidRef ? (NSString *)CFBridgingRelease(CFUUIDCreateString(NULL, report.uuidRef)) : MSAC_UUID_STRING; + return errorId; +} + ++ (MSACAppleErrorLog *)addProcessInfoAndApplicationPathTo:(MSACAppleErrorLog *)errorLog fromCrashReport:(PLCrashReport *)crashReport { + // Set the defaults first. + errorLog.processId = @(0); + errorLog.processName = unknownString; + errorLog.parentProcessName = unknownString; + errorLog.parentProcessId = nil; + errorLog.applicationPath = unknownString; + + // Convert PLCrashReport process information. + if (crashReport.hasProcessInfo) { + errorLog.processId = @(crashReport.processInfo.processID); + errorLog.processName = crashReport.processInfo.processName ?: errorLog.processName; + + // Process Path. + if (crashReport.processInfo.processPath != nil) { + NSString *processPath = crashReport.processInfo.processPath; + +// Remove username from the path +#if TARGET_OS_SIMULATOR || TARGET_OS_OSX || TARGET_OS_MACCATALYST + processPath = [self anonymizedPathFromPath:processPath]; +#endif + errorLog.applicationPath = processPath; + } + + // Parent Process Name. + if (crashReport.processInfo.parentProcessName != nil) { + errorLog.parentProcessName = crashReport.processInfo.parentProcessName; + } + // Parent Process ID. + errorLog.parentProcessId = @(crashReport.processInfo.parentProcessID); + } + return errorLog; +} + ++ (NSDate *)getAppLaunchTimeFromReport:(PLCrashReport *)report { + return report.processInfo ? report.processInfo.processStartTime : report.systemInfo.timestamp; +} + ++ (NSDate *)getCrashTimeFromReport:(PLCrashReport *)report { + return report.systemInfo.timestamp; +} + ++ (NSArray *)extractThreadsFromReport:(PLCrashReport *)report + crashedThread:(PLCrashReportThreadInfo *)crashedThread + is64bit:(BOOL)is64bit { + NSMutableArray *formattedThreads = [NSMutableArray array]; + MSACException *lastException = nil; + + // If CrashReport contains Exception, add the threads that belong to the + // exception to the list of threads. + if (report.exceptionInfo != nil && report.exceptionInfo.stackFrames != nil && [report.exceptionInfo.stackFrames count] > 0) { + PLCrashReportExceptionInfo *exception = report.exceptionInfo; + + MSACThread *exceptionThread = [MSACThread new]; + exceptionThread.threadId = @(-1); + + // Gather frames from the thread's exception. + for (PLCrashReportStackFrameInfo *frameInfo in exception.stackFrames) { + MSACStackFrame *frame = [MSACStackFrame new]; + frame.address = [MSACErrorLogFormatter formatAddress:frameInfo.instructionPointer is64bit:is64bit]; + [exceptionThread.frames addObject:frame]; + } + + lastException = [MSACException new]; + lastException.message = exception.exceptionReason; + lastException.frames = exceptionThread.frames; + lastException.type = report.exceptionInfo.exceptionName ?: report.signalInfo.name; + + /* + * Don't add the thread to the array of threads (as in HockeyApp), the + * exception will be added to the crashed thread instead. + */ + } + + // Get all threads from the report (as opposed to the threads from the + // exception). + for (PLCrashReportThreadInfo *plCrashReporterThread in report.threads) { + MSACThread *thread = [MSACThread new]; + thread.threadId = @(plCrashReporterThread.threadNumber); + + if ((lastException != nil) && (crashedThread != nil) && [thread.threadId isEqualToNumber:@(crashedThread.threadNumber)]) { + thread.exception = lastException; + } + + /* + * Write out the frames. In raw reports, Apple writes this out as a simple + * list of PCs. In the minimally post-processed report, Apple writes this + * out as full frame entries. We use the latter format. + */ + for (PLCrashReportStackFrameInfo *plCrashReporterFrameInfo in plCrashReporterThread.stackFrames) { + MSACStackFrame *frame = [MSACStackFrame new]; + frame.address = [MSACErrorLogFormatter formatAddress:plCrashReporterFrameInfo.instructionPointer is64bit:is64bit]; + frame.code = [self formatStackFrame:plCrashReporterFrameInfo report:report]; + [thread.frames addObject:frame]; + } + + [formattedThreads addObject:thread]; + } + + return formattedThreads; +} + +/** + * Format a stack frame for display in a thread backtrace. + * + * @param frameInfo The stack frame to format + * @param report The report from which this frame was acquired. + * + * @return Returns a formatted frame line. + */ ++ (NSString *)formatStackFrame:(PLCrashReportStackFrameInfo *)frameInfo report:(PLCrashReport *)report { + + /* + * Base image address containing instrumentation pointer, offset of the IP + * from that base address, and the associated image name. + */ + uint64_t baseAddress = 0x0; + uint64_t pcOffset = 0x0; + NSString *symbolString = nil; + + PLCrashReportBinaryImageInfo *imageInfo = [report imageForAddress:frameInfo.instructionPointer]; + if (imageInfo != nil) { + baseAddress = imageInfo.imageBaseAddress; + pcOffset = frameInfo.instructionPointer - imageInfo.imageBaseAddress; + } else if (frameInfo.instructionPointer) { + MSACLogWarning([MSACCrashes logTag], @"Cannot find image for 0x%" PRIx64, frameInfo.instructionPointer); + } + + /* + * If symbol info is available, the format used in Apple's reports is Sym + + * OffsetFromSym. Otherwise, the format used is imageBaseAddress + offsetToIP. + */ + MSACBinaryImageType imageType = [self imageTypeForImagePath:imageInfo.imageName processPath:report.processInfo.processPath]; + if (frameInfo.symbolInfo != nil && imageType == MSACBinaryImageTypeOther) { + NSString *symbolName = frameInfo.symbolInfo.symbolName; + + // Apple strips the _ symbol prefix in their reports. + if ([symbolName rangeOfString:@"_"].location == 0 && [symbolName length] > 1) { + switch (report.systemInfo.operatingSystem) { + case PLCrashReportOperatingSystemMacOSX: + case PLCrashReportOperatingSystemiPhoneOS: + case PLCrashReportOperatingSystemAppleTVOS: + case PLCrashReportOperatingSystemiPhoneSimulator: + symbolName = [symbolName substringFromIndex:1]; + break; + + case PLCrashReportOperatingSystemUnknown: + MSACLogWarning([MSACCrashes logTag], @"Symbol \"%@\" prefix rules are unknown for this OS!", symbolName); + break; + } + } + + uint64_t symOffset = frameInfo.instructionPointer - frameInfo.symbolInfo.startAddress; + symbolString = [NSString stringWithFormat:@"%@ + %" PRId64, symbolName, symOffset]; + } else { + symbolString = [NSString stringWithFormat:@"0x%" PRIx64 " + %" PRId64, baseAddress, pcOffset]; + } + + /* + * Note that width specifiers are ignored for %@, but work for C strings. + * UTF-8 is not correctly handled with %s (it depends on the system encoding), + * but UTF-16 is supported via %S, so we use it here. + */ + return symbolString; +} + ++ (NSDictionary *)extractRegistersFromCrashedThread:(PLCrashReportThreadInfo *)crashedThread is64bit:(BOOL)is64bit { + NSMutableDictionary *registers = [NSMutableDictionary new]; + + for (PLCrashReportRegisterInfo *registerInfo in crashedThread.registers) { + + // No need to format the register's name but, we need to format the value. + NSString *registerName = registerInfo.registerName; + NSString *formattedRegisterValue = [MSACErrorLogFormatter formatAddress:registerInfo.registerValue is64bit:is64bit]; + registers[registerName] = formattedRegisterValue; + } + + return registers; +} + ++ (NSString *)extractExceptionReasonFromReport:(PLCrashReport *)report { + NSString *exceptionReason = nil; + + // Uncaught Exception. + if (report.hasExceptionInfo) { + exceptionReason = [NSString stringWithString:report.exceptionInfo.exceptionReason]; + } + return exceptionReason; +} + ++ (NSString *)selectorRegisterValueFromReport:(PLCrashReport *)report + ofCrashedThread:(PLCrashReportThreadInfo *)crashedThread + codeType:(uint64_t)codeType { + + /* + * Try to find the selector in case this was a crash in obj_msgSend. + * We search this whether the crash happened in objc_msgSend or not since we + * don't have the symbol! + */ + NSString *foundSelector = nil; + + // Search the registers value for the current architecture. + switch (codeType) { + case CPU_TYPE_ARM: + foundSelector = [[self class] selectorForRegisterWithName:@"r1" ofThread:crashedThread report:report]; + if (foundSelector == NULL) { + foundSelector = [[self class] selectorForRegisterWithName:@"r2" ofThread:crashedThread report:report]; + } + break; + + case CPU_TYPE_ARM64: + foundSelector = [[self class] selectorForRegisterWithName:@"x1" ofThread:crashedThread report:report]; + break; + + case CPU_TYPE_X86: + foundSelector = [[self class] selectorForRegisterWithName:@"ecx" ofThread:crashedThread report:report]; + break; + + case CPU_TYPE_X86_64: + foundSelector = [[self class] selectorForRegisterWithName:@"rsi" ofThread:crashedThread report:report]; + if (foundSelector == NULL) { + foundSelector = [[self class] selectorForRegisterWithName:@"rdx" ofThread:crashedThread report:report]; + } + break; + } + return foundSelector; +} + ++ (NSArray *)extractBinaryImagesFromReport:(PLCrashReport *)report is64bit:(BOOL)is64bit { + + // Gather all addresses for which we need to preserve the binary images. + NSArray *addresses = [self addressesFromReport:report]; + + NSMutableArray *binaryImages = [NSMutableArray array]; + + // Images. The iPhone crash report format sorts these in ascending order, by the base address. + for (PLCrashReportBinaryImageInfo *imageInfo in [report.images sortedArrayUsingFunction:bit_binaryImageSort context:nil]) { + MSACBinary *binary = [MSACBinary new]; + binary.binaryId = (imageInfo.hasImageUUID) ? imageInfo.imageUUID : unknownString; + uint64_t startAddress = imageInfo.imageBaseAddress; + binary.startAddress = [MSACErrorLogFormatter formatAddress:startAddress is64bit:is64bit]; + uint64_t endAddress = imageInfo.imageBaseAddress + (MAX((uint64_t)1, imageInfo.imageSize) - 1); + binary.endAddress = [MSACErrorLogFormatter formatAddress:endAddress is64bit:is64bit]; + BOOL binaryIsInAddresses = [self isBinaryWithStart:startAddress end:endAddress inAddresses:addresses]; + MSACBinaryImageType imageType = [self imageTypeForImagePath:imageInfo.imageName processPath:report.processInfo.processPath]; + + // Remove username from the image path. + if (binaryIsInAddresses || (imageType != MSACBinaryImageTypeOther)) { + NSString *imagePath = @""; + if (imageInfo.imageName && [imageInfo.imageName length] > 0) { +#if TARGET_OS_SIMULATOR + imagePath = [imageInfo.imageName stringByAbbreviatingWithTildeInPath]; +#else + imagePath = imageInfo.imageName; +#endif + } +#if TARGET_OS_SIMULATOR || TARGET_OS_OSX || TARGET_OS_MACCATALYST + imagePath = [self anonymizedPathFromPath:imagePath]; +#endif + + binary.path = imagePath; + + NSString *imageName = [imageInfo.imageName lastPathComponent] ?: @"\?\?\?"; + binary.name = imageName; + + // Fetch the UUID if it exists. + binary.binaryId = (imageInfo.hasImageUUID) ? imageInfo.imageUUID : unknownString; + + // Determine the architecture string. + binary.primaryArchitectureId = @(imageInfo.codeType.type); + binary.architectureVariantId = @(imageInfo.codeType.subtype); + + [binaryImages addObject:binary]; + } + } + return binaryImages; +} + ++ (BOOL)isBinaryWithStart:(uint64_t)start end:(uint64_t)end inAddresses:(NSArray *)addresses { + for (NSNumber *address in addresses) { + + if ([address unsignedLongLongValue] >= start && [address unsignedLongLongValue] <= end) { + return YES; + } + } + return NO; +} + +/** + * Remove the user's name from a crash's process path. + * This is only necessary when sending crashes from the simulator as the path + * then contains the username of the Mac the simulator is running on. + * + * @param path A string containing the username + * + * @return An anonymized string where the real username is replaced by "USER" + */ ++ (NSString *)anonymizedPathFromPath:(NSString *)path { + NSString *anonymizedProcessPath = [NSString string]; + if (([path length] > 0) && [path hasPrefix:@"/Users/"]) { + NSError *error = nil; + NSString *regexPattern = @"(/Users/[^/]+/)"; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:regexPattern options:0 error:&error]; + if (!regex) { + MSACLogError([MSACCrashes logTag], @"Couldn't create regular expression with pattern\"%@\": %@", regexPattern, + error.localizedDescription); + return anonymizedProcessPath; + } + anonymizedProcessPath = [regex stringByReplacingMatchesInString:path + options:0 + range:NSMakeRange(0, [path length]) + withTemplate:@"/Users/USER/"]; + } else if (([path length] > 0) && ([path rangeOfString:@"Users"].length == 0)) { + return path; + } + return anonymizedProcessPath; +} + +/** + * Return the selector string of a given register name + * + * @param regName The name of the register to use for getting the address + * @param thread The crashed thread + * @param report The crash report created by PLCrashReporter. + * + * @return The selector as a C string or NULL if no selector was found + */ ++ (NSString *)selectorForRegisterWithName:(NSString *)regName ofThread:(PLCrashReportThreadInfo *)thread report:(PLCrashReport *)report { + + // Get the address for the register. + uint64_t regAddress = 0; + for (PLCrashReportRegisterInfo *reg in thread.registers) { + if ([reg.registerName isEqualToString:regName]) { + regAddress = reg.registerValue; + break; + } + } + + // Return nil if we couldn't find an address. + if (regAddress == 0) { + return nil; + } + + // Get the selector. + PLCrashReportBinaryImageInfo *imageForRegAddress = [report imageForAddress:regAddress]; + if (imageForRegAddress) { + const char *foundSelector = findSEL([imageForRegAddress.imageName UTF8String], imageForRegAddress.imageUUID, + regAddress - (uint64_t)imageForRegAddress.imageBaseAddress); + if (foundSelector != NULL) { + return [NSString stringWithUTF8String:foundSelector]; + } + } + + return nil; +} + +// Determine if in binary image is the app executable or app specific framework. ++ (MSACBinaryImageType)imageTypeForImagePath:(NSString *)imagePath processPath:(NSString *)processPath { + MSACBinaryImageType imageType = MSACBinaryImageTypeOther; + + if (!imagePath || !processPath) { + return imageType; + } + + NSString *standardizedImagePath = [[imagePath stringByStandardizingPath] lowercaseString]; + imagePath = [imagePath lowercaseString]; + processPath = [processPath lowercaseString]; + + NSRange appRange = [standardizedImagePath rangeOfString:@".app/"]; + + /* + * Exclude iOS swift dylibs. These are provided as part of the app binary by + * Xcode for now, but we never get a dSYM for those. + */ + NSRange swiftLibRange = [standardizedImagePath rangeOfString:@"frameworks/libswift"]; + BOOL dylibSuffix = [standardizedImagePath hasSuffix:@".dylib"]; + if (appRange.location != NSNotFound && !(swiftLibRange.location != NSNotFound && dylibSuffix)) { + NSString *appBundleContentsPath = [standardizedImagePath substringToIndex:appRange.location + 5]; + + /* + * Fix issue with iOS 8 `stringByStandardizingPath` removing leading + * `/private` path (when not running in the debugger or simulator only). + */ + if ([standardizedImagePath isEqual:processPath] || [imagePath hasPrefix:processPath]) { + imageType = MSACBinaryImageTypeAppBinary; + } else if ([standardizedImagePath hasPrefix:appBundleContentsPath] || [imagePath hasPrefix:appBundleContentsPath]) { + imageType = MSACBinaryImageTypeAppFramework; + } + } + + return imageType; +} + +#pragma mark - Helpers + ++ (BOOL)isCodeType64bit:(uint64_t)type { + return type == CPU_TYPE_ARM64 || type == CPU_TYPE_X86_64; +} + ++ (PLCrashReportThreadInfo *)findCrashedThreadInReport:(PLCrashReport *)report { + PLCrashReportThreadInfo *crashedThread; + for (PLCrashReportThreadInfo *thread in report.threads) { + if (thread.crashed) { + crashedThread = thread; + break; + } + } + return crashedThread; +} + ++ (NSArray *)addressesFromReport:(PLCrashReport *)report { + NSMutableArray *addresses = [NSMutableArray new]; + if (report.exceptionInfo != nil && report.exceptionInfo.stackFrames != nil && [report.exceptionInfo.stackFrames count] > 0) { + PLCrashReportExceptionInfo *exception = report.exceptionInfo; + for (PLCrashReportStackFrameInfo *frameInfo in exception.stackFrames) { + [addresses addObject:@(frameInfo.instructionPointer)]; + } + } + for (PLCrashReportThreadInfo *plCrashReporterThread in report.threads) { + for (PLCrashReportStackFrameInfo *frameInfo in plCrashReporterThread.stackFrames) { + [addresses addObject:@(frameInfo.instructionPointer)]; + } + for (PLCrashReportRegisterInfo *registerInfo in plCrashReporterThread.registers) { + [addresses addObject:@(registerInfo.registerValue)]; + } + } + + return addresses; +} + ++ (NSString *)uuidRefToString:(CFUUIDRef)uuidRef { + if (!uuidRef) { + return nil; + } + CFStringRef uuidStringRef = CFUUIDCreateString(kCFAllocatorDefault, uuidRef); + return (__bridge_transfer NSString *)uuidStringRef; +} + ++ (NSString *)formatAddress:(uint64_t)address is64bit:(BOOL)is64bit { + return [NSString stringWithFormat:@"0x%0*" PRIx64, 8 << is64bit, address]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Util/MSACErrorLogFormatterPrivate.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Util/MSACErrorLogFormatterPrivate.h new file mode 100644 index 0000000000..2546d3f2b6 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Internals/Util/MSACErrorLogFormatterPrivate.h @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACErrorLogFormatter.h" + +@class PLCrashReportThreadInfo; + +@interface MSACErrorLogFormatter () + +/** + * Remove user information from a path when the crash happened in the simulator. + * + * @param path The path that needs to be anonymized. + * + * @return The anonymized path. + */ ++ (NSString *)anonymizedPathFromPath:(NSString *)path; + +/** + * Determines the MSACBinaryImageType. + * + * @param imagePath The path to a binary image. + * @param processPath The process path. + * + * @return The type of the binary image. + */ ++ (MSACBinaryImageType)imageTypeForImagePath:(NSString *)imagePath processPath:(NSString *)processPath; + +/** + * Create an id for a crash report. Uses the one generated by PLCrashReporter or generates a new UUID. + * + * @param report The crash report. + * + * @return an error id as a NSString. + */ ++ (NSString *)errorIdForCrashReport:(PLCrashReport *)report; + +/** + * Convenience method to add process information and application path to an error log. For simulator builds, it will anonymize the path and + * remove user information from it. It was not split into two methods because separating into a method to add the processInfo and one for + * the applicationPath would mean duplicating logic. + * + * @param errorLog The error log object that will be returned. + * @param crashReport The crash + * + * @return The error log with process information and the application path. + */ ++ (MSACAppleErrorLog *)addProcessInfoAndApplicationPathTo:(MSACAppleErrorLog *)errorLog fromCrashReport:(PLCrashReport *)crashReport; + +/** + * Convenience method to find the crashed thread in a crash report. + * + * @param report The crash report. + * + * @return The crashed thread info. + */ ++ (PLCrashReportThreadInfo *)findCrashedThreadInReport:(PLCrashReport *)report; + +/** + * Extract binary images from a crash report. This only extracts the binary images that we "care" about, meaning those that are contained + * in the crash's stack frames. + * + * @param report The crash report. + * @param is64bit A flag that indicates if this is a 64bit architecture. + * + * @return An array of binary images. + */ ++ (NSArray *)extractBinaryImagesFromReport:(PLCrashReport *)report is64bit:(BOOL)is64bit; + +/** + * Format a memory address into a string. This normalizes arm64 addresses. + * + * @param address The address that will be formatted. + * @param is64bit A flag that indicates if this is a 64bit architecture. + * + * @return A formatted memory address as a string. + */ ++ (NSString *)formatAddress:(uint64_t)address is64bit:(BOOL)is64bit; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/MSACCrashes.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/MSACCrashes.h new file mode 100644 index 0000000000..a8a132f757 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/MSACCrashes.h @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACErrorReport.h" +#import "MSACServiceAbstract.h" + +@class MSACCrashesDelegate; + +/** + * Custom block that handles the alert that prompts the user whether crash reports need to be processed or not. + * + * @return Returns YES to discard crash reports, otherwise NO. + */ +typedef BOOL (^MSACUserConfirmationHandler)(NSArray *_Nonnull errorReports) NS_SWIFT_NAME(UserConfirmationHandler); + +/** + * Error Logging status. + */ +typedef NS_ENUM(NSUInteger, MSACErrorLogSetting) { + + /** + * Crash reporting is disabled. + */ + MSACErrorLogSettingDisabled = 0, + + /** + * User is asked each time before sending error logs. + */ + MSACErrorLogSettingAlwaysAsk = 1, + + /** + * Each error log is send automatically. + */ + MSACErrorLogSettingAutoSend = 2 +} NS_SWIFT_NAME(ErrorLogSetting); + +/** + * Crash Manager alert user input. + */ +typedef NS_ENUM(NSUInteger, MSACUserConfirmation) { + + /** + * User chose not to send the crash report. + */ + MSACUserConfirmationDontSend = 0, + + /** + * User wants the crash report to be sent. + */ + MSACUserConfirmationSend = 1, + + /** + * User wants to send all error logs. + */ + MSACUserConfirmationAlways = 2 +} NS_SWIFT_NAME(UserConfirmation); + +@protocol MSACCrashesDelegate; + +NS_SWIFT_NAME(Crashes) +@interface MSACCrashes : MSACServiceAbstract + +///----------------------------------------------------------------------------- +/// @name Testing Crashes Feature +///----------------------------------------------------------------------------- + +/** + * Lets the app crash for easy testing of the SDK. + * + * The best way to use this is to trigger the crash with a button action. + * + * Make sure not to let the app crash in `applicationDidFinishLaunching` or any other startup method! Since otherwise the app would crash + * before the SDK could process it. + * + * Note that our SDK provides support for handling crashes that happen early on startup. Check the documentation for more information on how + * to use this. + * + * If the SDK detects an App Store environment, it will _NOT_ cause the app to crash! + */ ++ (void)generateTestCrash; + +///----------------------------------------------------------------------------- +/// @name Helpers +///----------------------------------------------------------------------------- + +/** + * Check if the app has crashed in the last session. + * + * @return Returns YES is the app has crashed in the last session. + */ +@property(class, readonly, nonatomic) BOOL hasCrashedInLastSession; + +/** + * Check if the app received memory warning in the last session. + * + * @return Returns YES is the app received memory warning in the last session. + */ +@property(class, readonly, nonatomic) BOOL hasReceivedMemoryWarningInLastSession; + +/** + * Provides details about the crash that occurred in the last app session + */ +@property(class, nullable, readonly, nonatomic) MSACErrorReport *lastSessionCrashReport; + +#if TARGET_OS_OSX || TARGET_OS_MACCATALYST +/** + * Callback for report exception. + * + * NOTE: This method should be called only if you explicitly disabled swizzling for it. + * + * On OS X runtime, not all uncaught exceptions end in a custom `NSUncaughtExceptionHandler`. + * Forward exception from overrided `[NSApplication reportException:]` to catch additional exceptions. + */ ++ (void)applicationDidReportException:(NSException *_Nonnull)exception; +#endif + +///----------------------------------------------------------------------------- +/// @name Configuration +///----------------------------------------------------------------------------- + +#if !TARGET_OS_TV +/** + * Disable the Mach exception server. + * + * By default, the SDK uses the Mach exception handler to catch fatal signals, e.g. stack overflows, via a Mach exception server. If you + * want to disable the Mach exception handler, you should call this method _BEFORE_ starting the SDK. Your typical setup code would look + * like this: + * + * `[MSACCrashes disableMachExceptionHandler]`; + * `[MSACAppCenter start:@"YOUR_APP_ID" withServices:@[[MSACCrashes class]]];` + * + * or if you are using Swift: + * + * `MSACCrashes.disableMachExceptionHandler()` + * `MSACAppCenter.start("YOUR_APP_ID", withServices: [MSACAnalytics.self, MSACCrashes.self])` + * + * tvOS does not support the Mach exception handler, thus crashes that are caused by stack overflows cannot be detected. As a result, + * disabling the Mach exception server is not available in the tvOS SDK. + * + * @discussion It can be useful to disable the Mach exception handler when you are debugging the Crashes service while developing, + * especially when you attach the debugger to your application after launch. + */ ++ (void)disableMachExceptionHandler; +#endif + +/** + * Set the delegate + * Defines the class that implements the optional protocol `MSACCrashesDelegate`. + * + * @see MSACCrashesDelegate + */ +@property(class, nonatomic, weak) id _Nullable delegate; + +/** + * Set a user confirmation handler that is invoked right before processing crash reports to determine whether sending crash reports or not. + * + * @see MSACUserConfirmationHandler + */ +@property(class, nonatomic) MSACUserConfirmationHandler _Nullable userConfirmationHandler; + +/** + * Notify SDK with a confirmation to handle the crash report. + * + * @param userConfirmation A user confirmation. + * + * @see MSACUserConfirmation. + */ ++ (void)notifyWithUserConfirmation:(MSACUserConfirmation)userConfirmation; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/MSACCrashes.mm b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/MSACCrashes.mm new file mode 100644 index 0000000000..1bbf0f83e9 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/MSACCrashes.mm @@ -0,0 +1,1404 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#if !TARGET_OS_OSX +#import +#endif + +#import "MSACAbstractErrorLog.h" +#import "MSACAppCenterInternal.h" +#import "MSACAppleErrorLog.h" +#import "MSACApplicationForwarder.h" +#import "MSACBinary.h" +#import "MSACChannelUnitConfiguration.h" +#import "MSACChannelUnitProtocol.h" +#import "MSACCrashHandlerSetupDelegate.h" +#import "MSACCrashReporter.h" +#import "MSACCrashesBufferedLog.hpp" +#import "MSACCrashesCXXExceptionWrapperException.h" +#import "MSACCrashesDelegate.h" +#import "MSACCrashesInternal.h" +#import "MSACCrashesPrivate.h" +#import "MSACCrashesUtil.h" +#import "MSACDeviceTracker.h" +#import "MSACDispatcherUtil.h" +#import "MSACEncrypter.h" +#import "MSACErrorAttachmentLog.h" +#import "MSACErrorAttachmentLogInternal.h" +#import "MSACErrorLogFormatter.h" +#import "MSACErrorReportPrivate.h" +#import "MSACException.h" +#import "MSACHandledErrorLog.h" +#import "MSACLoggerInternal.h" +#import "MSACSessionContext.h" +#import "MSACStackFrame.h" +#import "MSACThread.h" +#import "MSACUserIdContext.h" +#import "MSACUtility+File.h" +#import "MSACWrapperCrashesHelper.h" +#import "MSACWrapperException.h" +#import "MSACWrapperExceptionManagerInternal.h" + +/** + * Service name for initialization. + */ +static NSString *const kMSACServiceName = @"Crashes"; + +/** + * The group Id for storage. + */ +static NSString *const kMSACGroupId = @"Crashes"; + +/** + * The group Id for log buffer. + */ +static NSString *const kMSACBufferGroupId = @"CrashesBuffer"; + +/** + * Name for the AnalyzerInProgress file. Some background info here: writing the file to signal that we are processing crashes proved to be + * faster and more reliable as e.g. storing a flag in the NSUserDefaults. + */ +static NSString *const kMSACAnalyzerFilename = @"MSCrashes.analyzer"; + +/** + * File extension for buffer files. Files will have a GUID as the file name and a .mscrasheslogbuffer as file extension. + */ +static NSString *const kMSACLogBufferFileExtension = @"mscrasheslogbuffer"; + +static NSString *const kMSACTargetTokenFileExtension = @"targettoken"; + +static unsigned int kMaxAttachmentSize = 7 * 1024 * 1024; + +/** + * Delay in nanoseconds before processing crashes. + */ +static int64_t kMSACCrashProcessingDelay = 1 * NSEC_PER_SEC; + +std::array msACCrashesLogBuffer; + +/** + * Singleton. + */ +static MSACCrashes *sharedInstance = nil; +static dispatch_once_t onceToken; + +/** + * Delayed processing token. + */ +static dispatch_once_t delayedProcessingToken; + +#pragma mark - Callbacks Setup + +static MSACCrashesCallbacks msCrashesCallbacks = {.context = nullptr, .handleSignal = nullptr}; +static NSString *const kMSACUserConfirmationKey = @"CrashesUserConfirmation"; +static volatile BOOL writeBufferTaskStarted = NO; + +static void ms_save_log_buffer(const std::string &data, const std::string &path) { + int fd = open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd < 0) { + return; + } + write(fd, data.data(), data.size()); + close(fd); +} + +void ms_save_log_buffer() { + + // Iterate over the buffered logs and write them to disk. + writeBufferTaskStarted = YES; + for (int i = 0; i < ms_crashes_log_buffer_size; i++) { + + // Make sure not to allocate any memory (e.g. copy). + ms_save_log_buffer(msACCrashesLogBuffer[i].buffer, msACCrashesLogBuffer[i].bufferPath); + if (!msACCrashesLogBuffer[i].targetToken.empty()) { + ms_save_log_buffer(msACCrashesLogBuffer[i].targetToken, msACCrashesLogBuffer[i].targetTokenPath); + } + } +} + +/** + * Proxy implementation for PLCrashReporter to keep our interface stable while this can change. + */ +static void plcr_post_crash_callback(__unused siginfo_t *info, __unused ucontext_t *uap, void *context) { + ms_save_log_buffer(); + if (msCrashesCallbacks.handleSignal != nullptr) { + msCrashesCallbacks.handleSignal(context); + } +} + +static PLCrashReporterCallbacks plCrashCallbacks = {.version = 0, .context = nullptr, .handleSignal = plcr_post_crash_callback}; + +/** + * C++ Exception Handler. + */ +__attribute__((noreturn)) static void uncaught_cxx_exception_handler(const MSACCrashesUncaughtCXXExceptionInfo *info) { + + /* + * This relies on a LOT of sneaky internal knowledge of how PLCR works and should not be considered a long-term solution. + */ + NSGetUncaughtExceptionHandler()([[MSACCrashesCXXExceptionWrapperException alloc] initWithCXXExceptionInfo:info]); + abort(); +} + +@interface MSACCrashes () + +/** + * Indicates if the app crashed in the previous session. + * Use this on startup, to check if the app starts the first time after it crashed previously. You can use this also to disable specific + * events, like asking the user to rate your app. + * + * @warning This property only has a correct value, once the sdk has been properly initialized! + * + * @see lastSessionCrashReport + */ +@property BOOL didCrashInLastSession; + +/** + * Indicates that the app received a low memory warning in the last session. + * It is possible that a low memory warning was sent but couldn't be logged if iOS killed the app before updating the flag in + * the filesystem. Apps can also be killed without receiving a low memory warning, or receive the warning, but crash for another reason. + * + * @warning This property only has an updated value once the SDK has been properly initialized! + */ +@property BOOL didReceiveMemoryWarningInLastSession; + +/** + * Detail information about the last crash. + */ +@property(getter=getLastSessionCrashReport) MSACErrorReport *lastSessionCrashReport; + +/** + * Queue with high priority that will be used to create the log buffer files. The default main queue is too slow. + */ +@property(nonatomic) dispatch_queue_t bufferFileQueue; + +/** + * A group to wait for creation of buffers in the test. + */ +@property(nonatomic) dispatch_group_t bufferFileGroup; + +/** + * Semaphore for exclusion with "startDelayedCrashProcessing" method. + */ +@property dispatch_semaphore_t delayedProcessingSemaphore; + +/** + * Channel unit for log buffer. + */ +@property(nonatomic) id bufferChannelUnit; + +/* + * Encrypter for target tokens. + */ +@property(nonatomic, readonly) MSACEncrypter *targetTokenEncrypter; + +/** + * A dispatch source that monitors the memory pressure of the system. + */ +@property dispatch_source_t memoryPressureSource; + +@end + +@implementation MSACCrashes + +@synthesize channelGroup = _channelGroup; +@synthesize channelUnitConfiguration = _channelUnitConfiguration; + +#pragma mark - Public Methods + ++ (void)generateTestCrash { + @synchronized([MSACCrashes sharedInstance]) { + if ([[MSACCrashes sharedInstance] canBeUsed]) { + if ([MSACUtility currentAppEnvironment] == MSACEnvironmentAppStore) { + MSACLogWarning([MSACCrashes logTag], @"GenerateTestCrash was just called in an App Store environment. The call will be ignored."); + } else { + if ([MSACAppCenter isDebuggerAttached]) { + MSACLogWarning([MSACCrashes logTag], @"The debugger is attached. The following crash cannot be detected by the SDK!"); + } + + // Crashing the app here! + __builtin_trap(); + } + } + } +} + ++ (BOOL)hasCrashedInLastSession { + return [[MSACCrashes sharedInstance] didCrashInLastSession]; +} + ++ (MSACUserConfirmationHandler)userConfirmationHandler { + return [MSACCrashes sharedInstance].userConfirmationHandler; +} + ++ (BOOL)hasReceivedMemoryWarningInLastSession { + return [[MSACCrashes sharedInstance] didReceiveMemoryWarningInLastSession]; +} + ++ (void)setUserConfirmationHandler:(MSACUserConfirmationHandler)userConfirmationHandler { + [[MSACCrashes sharedInstance] setUserConfirmationHandler:userConfirmationHandler]; +} + ++ (void)notifyWithUserConfirmation:(MSACUserConfirmation)userConfirmation { + [[MSACCrashes sharedInstance] notifyWithUserConfirmation:userConfirmation]; +} + ++ (MSACErrorReport *_Nullable)lastSessionCrashReport { + return [[MSACCrashes sharedInstance] getLastSessionCrashReport]; +} + ++ (void)applicationDidReportException:(NSException *_Nonnull)exception { + + // Don't invoke the registered UncaughtExceptionHandler if we are currently debugging this app! + if (![MSACAppCenter isDebuggerAttached]) { + + /* + * We forward this exception to PLCrashReporters UncaughtExceptionHandler. + * If the developer has implemented their own exception handler and that one is invoked before PLCrashReporters exception handler and + * the developers exception handler is invoking this method it will not finish its tasks after this call but directly jump into + * PLCrashReporters exception handler. If we wouldn't do this, this call would lead to an infinite loop. + */ + NSUncaughtExceptionHandler *plcrExceptionHandler = [MSACCrashes sharedInstance].exceptionHandler; + if (plcrExceptionHandler) { + plcrExceptionHandler(exception); + } + } +} + +/** + * This can never be bound to Xamarin. + * This method is not part of the publicly available APIs on tvOS as Mach exception handling is not possible on tvOS. + * The property is NO by default there. + */ ++ (void)disableMachExceptionHandler { + [[MSACCrashes sharedInstance] setEnableMachExceptionHandler:NO]; +} + ++ (id)delegate { + return [MSACCrashes sharedInstance].delegate; +} + ++ (void)setDelegate:(id)delegate { + [[MSACCrashes sharedInstance] setDelegate:delegate]; +} + +#pragma mark - Service initialization + +- (instancetype)init { + if ((self = [super init])) { + [MSAC_APP_CENTER_USER_DEFAULTS migrateKeys:@{ + @"MSAppCenterCrashesIsEnabled" : @"kMSCrashesIsEnabledKey", // [MSACCrashes isEnabled] + @"MSAppCenterAppDidReceiveMemoryWarning" : @"MSAppDidReceiveMemoryWarning", // [MSACCrashes processMemoryWarningInLastSession] + @"MSAppCenterCrashesUserConfirmation" : + @"MSUserConfirmation" // [MSACCrashes shouldAlwaysSend], [MSACCrashes notifyWithUserConfirmation] + } + forService:kMSACServiceName]; + [MSACUtility addMigrationClasses:@{ + @"MSAppleErrorLog" : MSACAppleErrorLog.self, + @"MSThread" : MSACThread.self, + @"MSWrapperException" : MSACWrapperException.self, + @"MSAbstractErrorLog" : MSACAbstractErrorLog.self, + @"MSHandledErrorLog" : MSACHandledErrorLog.self, + @"MSException" : MSACException.self, + @"MSStackFrame" : MSACStackFrame.self, + @"MSBinary" : MSACBinary.self, + @"MSErrorAttachmentLog" : MSACErrorAttachmentLog.self, + @"MSErrorReport" : MSACErrorReport.self + }]; + _appStartTime = [NSDate date]; + _crashFiles = [NSMutableArray new]; + _crashesPathComponent = [MSACCrashesUtil crashesDir]; + _logBufferPathComponent = [MSACCrashesUtil logBufferDir]; + _analyzerInProgressFilePathComponent = [NSString stringWithFormat:@"%@/%@", [MSACCrashesUtil crashesDir], kMSACAnalyzerFilename]; + + _didCrashInLastSession = NO; + _didReceiveMemoryWarningInLastSession = NO; + _delayedProcessingSemaphore = dispatch_semaphore_create(0); + _automaticProcessingEnabled = YES; +#if !TARGET_OS_TV + _enableMachExceptionHandler = YES; +#endif + _channelUnitConfiguration = [[MSACChannelUnitConfiguration alloc] initWithGroupId:[self groupId] + priority:MSACPriorityHigh + flushInterval:1.0 + batchSizeLimit:1 + pendingBatchesLimit:3]; + _targetTokenEncrypter = [MSACEncrypter new]; + + /* + * Using our own queue with high priority as the default main queue is slower and we want the files to be created as quickly as possible + * in case the app is crashing fast. + */ + _bufferFileQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); + _bufferFileGroup = dispatch_group_create(); + [self setupLogBuffer]; + } + return self; +} + +#pragma mark - MSACServiceAbstract + +- (void)applyEnabledState:(BOOL)isEnabled { + [super applyEnabledState:isEnabled]; + +#if !TARGET_OS_OSX + + // Remove all notification handlers. + [MSAC_NOTIFICATION_CENTER removeObserver:self]; +#endif + + // Enabling. + if (isEnabled) { + id crashSetupDelegate = [MSACWrapperCrashesHelper crashHandlerSetupDelegate]; + + // Check if a wrapper SDK has a preference for uncaught exception handling. + BOOL enableUncaughtExceptionHandler = YES; + if ([crashSetupDelegate respondsToSelector:@selector(shouldEnableUncaughtExceptionHandler)]) { + enableUncaughtExceptionHandler = [crashSetupDelegate shouldEnableUncaughtExceptionHandler]; + } + + // Allow a wrapper SDK to perform custom behavior before setting up crash handlers. + if ([crashSetupDelegate respondsToSelector:@selector(willSetUpCrashHandlers)]) { + [crashSetupDelegate willSetUpCrashHandlers]; + } + + // Set up crash handlers. + [self configureCrashReporterWithUncaughtExceptionHandlerEnabled:enableUncaughtExceptionHandler]; + + // Allow a wrapper SDK to perform custom behavior after setting up crash handlers. + if ([crashSetupDelegate respondsToSelector:@selector(didSetUpCrashHandlers)]) { + [crashSetupDelegate didSetUpCrashHandlers]; + } + + // Set up lifecycle event handler. +#if !TARGET_OS_OSX + [MSAC_NOTIFICATION_CENTER addObserver:self + selector:@selector(applicationWillEnterForeground) + name:UIApplicationWillEnterForegroundNotification + object:nil]; +#endif + + // Set up memory warning handler. +#if !TARGET_OS_OSX && !TARGET_OS_MACCATALYST + if (MSAC_IS_APP_EXTENSION) { +#endif + self.memoryPressureSource = + dispatch_source_create(DISPATCH_SOURCE_TYPE_MEMORYPRESSURE, 0, DISPATCH_MEMORYPRESSURE_WARN | DISPATCH_MEMORYPRESSURE_CRITICAL, + dispatch_get_main_queue()); + __weak typeof(self) weakSelf = self; + dispatch_source_set_event_handler(self.memoryPressureSource, ^{ + typeof(self) strongSelf = weakSelf; + [strongSelf didReceiveMemoryWarning:nil]; + }); + dispatch_resume(self.memoryPressureSource); +#if !TARGET_OS_OSX && !TARGET_OS_MACCATALYST + } else { + [MSAC_NOTIFICATION_CENTER addObserver:self + selector:@selector(didReceiveMemoryWarning:) + name:UIApplicationDidReceiveMemoryWarningNotification + object:nil]; + } +#endif + + /* + * PLCrashReporter keeps collecting crash reports even when the SDK is disabled, delete them only if current state is disabled. + */ + if (!self.isEnabled) { + [self.plCrashReporter purgePendingCrashReport]; + } + + // Get pending crashes from PLCrashReporter and persist them in the intermediate format. + if ([self.plCrashReporter hasPendingCrashReport]) { + self.didCrashInLastSession = YES; + MSACLogDebug([MSACCrashes logTag], @"The application crashed in the last session."); + [self handleLatestCrashReport]; + } + + // Get persisted crash reports. + self.crashFiles = [self persistedCrashReports]; + + /* + * Process PLCrashReports, this will format the PLCrashReport into our schema and then trigger sending. This mostly happens on the start + * of the service. + */ + if (self.crashFiles.count > 0) { + [self startDelayedCrashProcessing]; + } else { + dispatch_semaphore_signal(self.delayedProcessingSemaphore); + [self clearContextHistoryAndKeepCurrentSession]; + } + + // More details on log if a debugger is attached. + if ([MSACAppCenter isDebuggerAttached]) { + MSACLogInfo([MSACCrashes logTag], @"Crashes service has been enabled but the service cannot detect crashes due to running the " + @"application with a debugger attached."); + } else { + MSACLogInfo([MSACCrashes logTag], @"Crashes service has been enabled."); + } + } else { + if (self.memoryPressureSource) { + dispatch_source_cancel(self.memoryPressureSource); + self.memoryPressureSource = nil; + } + + // Don't set PLCrashReporter to nil! + MSACLogDebug([MSACCrashes logTag], @"Cleaning up all crash files."); + [MSACWrapperExceptionManager deleteAllWrapperExceptions]; + [self deleteAllFromCrashesDirectory]; + [self emptyLogBufferFiles]; + [self removeAnalyzerFile]; + [self.plCrashReporter purgePendingCrashReport]; + [self clearUnprocessedReports]; + [self clearContextHistoryAndKeepCurrentSession]; + [MSAC_APP_CENTER_USER_DEFAULTS removeObjectForKey:kMSACAppDidReceiveMemoryWarningKey]; + MSACLogInfo([MSACCrashes logTag], @"Crashes service has been disabled."); + } +} + +#pragma mark - MSACServiceInternal + ++ (instancetype)sharedInstance { + dispatch_once(&onceToken, ^{ + if (sharedInstance == nil) { + sharedInstance = [MSACCrashes new]; + } + }); + return sharedInstance; +} + ++ (NSString *)serviceName { + return kMSACServiceName; +} + +- (void)startWithChannelGroup:(id)channelGroup + appSecret:(nullable NSString *)appSecret + transmissionTargetToken:(nullable NSString *)token + fromApplication:(BOOL)fromApplication { + [super startWithChannelGroup:channelGroup appSecret:appSecret transmissionTargetToken:token fromApplication:fromApplication]; + [self.channelGroup addDelegate:self]; + [self processLogBufferAfterCrash]; + [self processMemoryWarningInLastSession]; + MSACLogVerbose([MSACCrashes logTag], @"Started crash service."); +} + +- (void)updateConfigurationWithAppSecret:(NSString *)appSecret transmissionTargetToken:(NSString *)token { + [self processLogBufferAfterCrash]; + + /* + * updateConfigurationWithAppSecret:transmissionTargetToken: will apply enabled state at the end so all update for the service should be + * done prior to call super method. + */ + [super updateConfigurationWithAppSecret:appSecret transmissionTargetToken:token]; +} + ++ (NSString *)logTag { + return @"AppCenterCrashes"; +} + +- (NSString *)groupId { + return kMSACGroupId; +} + +- (MSACInitializationPriority)initializationPriority { + return MSACInitializationPriorityMax; +} + +- (void)setEnableMachExceptionHandler:(BOOL)enableMachExceptionHandler { + _enableMachExceptionHandler = enableMachExceptionHandler; +} + +- (void)clearContextHistoryAndKeepCurrentSession { + [[MSACDeviceTracker sharedInstance] clearDevices]; + [[MSACSessionContext sharedInstance] clearSessionHistoryAndKeepCurrentSession:YES]; + [[MSACUserIdContext sharedInstance] clearUserIdHistory]; +} + +#pragma mark - Application life cycle + +- (void)applicationWillEnterForeground { + if (self.crashFiles.count > 0) { + [self startDelayedCrashProcessing]; + } +} + +- (void)didReceiveMemoryWarning:(NSNotification *)__unused notification { + MSACLogDebug([MSACCrashes logTag], @"The application received a low memory warning in the last session."); + [MSAC_APP_CENTER_USER_DEFAULTS setObject:@YES forKey:kMSACAppDidReceiveMemoryWarningKey]; +} + +#pragma mark - Channel Delegate + +/** + * Why are we doing the event-buffering inside crashes? + * The reason is, only Crashes has the chance to execute code at crash time and only with the following constraints: + * 1. Don't execute any Objective-C code when crashing. + * 2. Don't allocate new memory when crashing. + * 3. Only use async-safe C/C++ methods. + * This means the Crashes module can't message any other module. All logic related to the buffer needs to happen before the crash and then, + * at crash time, crashes has all info in place to save the buffer safely from the main thread (other threads are killed at crash time). + */ +- (void)channel:(id)__unused channel + didPrepareLog:(id)log + internalId:(NSString *)internalId + flags:(MSACFlags)__unused flags { + + // Don't buffer event if log is empty, crashes module is disabled or the log is related to crash. + NSObject *logObject = static_cast(log); + if (!log || ![self isEnabled] || [logObject isKindOfClass:[MSACAppleErrorLog class]] || + [logObject isKindOfClass:[MSACErrorAttachmentLog class]]) { + return; + } + + // The callback can be called from any thread, making sure we make this thread-safe. + @synchronized(self) { + NSData *serializedLog = [MSACUtility archiveKeyedData:log]; + if (serializedLog && (serializedLog.length > 0)) { + + // Serialize target token. + NSString *targetToken = log.transmissionTargetTokens != nil ? log.transmissionTargetTokens.anyObject : nil; + targetToken = targetToken != nil ? [self.targetTokenEncrypter encryptString:targetToken] : @""; + + // Storing a log. + NSTimeInterval oldestTimestamp = DBL_MAX; + long indexToDelete = 0; + MSACLogVerbose([MSACCrashes logTag], @"Storing a log to Crashes Buffer: (sid: %@, type: %@)", log.sid, log.type); + for (auto it = msACCrashesLogBuffer.begin(), end = msACCrashesLogBuffer.end(); it != end; ++it) { + + // We've found an empty element, buffer our log. + if (it->buffer.empty()) { + it->buffer = std::string(&reinterpret_cast(serializedLog.bytes)[0], + &reinterpret_cast(serializedLog.bytes)[serializedLog.length]); + it->targetToken = targetToken.UTF8String; + it->internalId = internalId.UTF8String; + it->timestamp = [[NSDate date] timeIntervalSince1970]; + + MSACLogVerbose([MSACCrashes logTag], @"Found an empty buffer position."); + + // We're done, no need to iterate any more and leave the method. + return; + } else { + + // The current element is full. Save the timestamp if applicable and continue iterating unless we have reached the last element. + + // Remember the timestamp if the log is older than the previous one or the initial one. + if (oldestTimestamp > it->timestamp) { + oldestTimestamp = it->timestamp; + indexToDelete = it - msACCrashesLogBuffer.begin(); + MSACLogVerbose([MSACCrashes logTag], @"Remembering index %ld for oldest timestamp %f.", indexToDelete, oldestTimestamp); + } + } + + /* + * Continue to iterate until we reach en empty element, in which case we store the log in it and stop, or until we reach the end of + * the buffer. In the later case, we will replace the oldest log with the current one. + */ + } + + // We've reached the last element in our buffer and we now go ahead and replace the oldest element. + MSACLogVerbose([MSACCrashes logTag], @"Reached end of buffer. Next step is overwriting the oldest one."); + + // Overwrite the oldest buffered log. + msACCrashesLogBuffer[indexToDelete].buffer = std::string(&reinterpret_cast(serializedLog.bytes)[0], + &reinterpret_cast(serializedLog.bytes)[serializedLog.length]); + msACCrashesLogBuffer[indexToDelete].internalId = internalId.UTF8String; + msACCrashesLogBuffer[indexToDelete].timestamp = [[NSDate date] timeIntervalSince1970]; + MSACLogVerbose([MSACCrashes logTag], @"Overwrote buffered log at index %ld.", indexToDelete); + + // We're done, no need to iterate any more. But no need to `return;` as we're at the end of the buffer. + } + } +} + +- (void)channel:(id)__unused channel didCompleteEnqueueingLog:(id)log internalId:(NSString *)internalId { + @synchronized(self) { + for (auto it = msACCrashesLogBuffer.begin(), end = msACCrashesLogBuffer.end(); it != end; ++it) { + NSString *bufferId = [NSString stringWithCString:it->internalId.c_str() encoding:NSUTF8StringEncoding]; + if (bufferId && bufferId.length > 0 && [bufferId isEqualToString:internalId]) { + MSACLogVerbose([MSACCrashes logTag], @"Deleting a log from buffer with id %@", internalId); + it->buffer = ""; + it->targetToken = ""; + it->timestamp = 0; + it->internalId = ""; + if (writeBufferTaskStarted) { + + /* + * Crashes already started writing buffer to files. + * To prevent sending duplicate logs after relaunch, it will delete the buffer file. + */ + unlink(it->bufferPath.c_str()); + MSACLogVerbose([MSACCrashes logTag], @"Deleted a log from Crashes Buffer (sid: %@, type: %@)", log.sid, log.type); + MSACLogVerbose([MSACCrashes logTag], @"Deleted crash buffer file: %@.", + [NSString stringWithCString:it->bufferPath.c_str() encoding:[NSString defaultCStringEncoding]]); + } + } + } + } +} + +- (void)channel:(id)__unused channel willSendLog:(id)log { + id delegate = self.delegate; + if ([delegate respondsToSelector:@selector(crashes:willSendErrorReport:)]) { + NSObject *logObject = static_cast(log); + if ([logObject isKindOfClass:[MSACAppleErrorLog class]]) { + MSACAppleErrorLog *appleErrorLog = static_cast(log); + MSACErrorReport *report = [MSACErrorLogFormatter errorReportFromLog:appleErrorLog]; + [MSACDispatcherUtil performBlockOnMainThread:^{ + [delegate crashes:self willSendErrorReport:report]; + }]; + } + } +} + +- (void)channel:(id)__unused channel didSucceedSendingLog:(id)log { + id delegate = self.delegate; + if ([delegate respondsToSelector:@selector(crashes:didSucceedSendingErrorReport:)]) { + NSObject *logObject = static_cast(log); + if ([logObject isKindOfClass:[MSACAppleErrorLog class]]) { + MSACAppleErrorLog *appleErrorLog = static_cast(log); + MSACErrorReport *report = [MSACErrorLogFormatter errorReportFromLog:appleErrorLog]; + [MSACDispatcherUtil performBlockOnMainThread:^{ + [delegate crashes:self didSucceedSendingErrorReport:report]; + }]; + } + } +} + +- (void)channel:(id)__unused channel didFailSendingLog:(id)log withError:(NSError *)error { + id delegate = self.delegate; + if ([delegate respondsToSelector:@selector(crashes:didFailSendingErrorReport:withError:)]) { + NSObject *logObject = static_cast(log); + if ([logObject isKindOfClass:[MSACAppleErrorLog class]]) { + MSACAppleErrorLog *appleErrorLog = static_cast(log); + MSACErrorReport *report = [MSACErrorLogFormatter errorReportFromLog:appleErrorLog]; + [MSACDispatcherUtil performBlockOnMainThread:^{ + [delegate crashes:self didFailSendingErrorReport:report withError:error]; + }]; + } + } +} + +#pragma mark - Crash reporter configuration + +- (void)configureCrashReporterWithUncaughtExceptionHandlerEnabled:(BOOL)enableUncaughtExceptionHandler { + if (self.plCrashReporter) { + MSACLogDebug([MSACCrashes logTag], @"Already configured PLCrashReporter."); + return; + } + + if (enableUncaughtExceptionHandler) { + MSACLogDebug([MSACCrashes logTag], @"EnableUncaughtExceptionHandler is set to YES"); + } else { + MSACLogDebug([MSACCrashes logTag], @"EnableUncaughtExceptionHandler is set to NO, we're running in a Xamarin runtime."); + } + + PLCrashReporterSignalHandlerType signalHandlerType = PLCrashReporterSignalHandlerTypeBSD; + +#if !TARGET_OS_TV + if (self.isMachExceptionHandlerEnabled) { + signalHandlerType = PLCrashReporterSignalHandlerTypeMach; + MSACLogVerbose([MSACCrashes logTag], @"Enabled Mach exception handler."); + } +#endif + PLCrashReporterSymbolicationStrategy symbolicationStrategy = PLCrashReporterSymbolicationStrategyNone; + PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc] initWithSignalHandlerType:signalHandlerType + symbolicationStrategy:symbolicationStrategy + shouldRegisterUncaughtExceptionHandler:enableUncaughtExceptionHandler]; + self.plCrashReporter = [[PLCrashReporter alloc] initWithConfiguration:config]; + + /* + * The actual signal and mach handlers are only registered when invoking `enableCrashReporterAndReturnError`, so it is safe enough to only + * disable the following part when a debugger is attached no matter which signal handler type is set. + */ + if ([MSACAppCenter isDebuggerAttached]) { + MSACLogWarning([MSACCrashes logTag], @"Detecting crashes is NOT enabled due to running the app with a debugger attached."); + } else { + + /* + * Multiple exception handlers can be set, but we can only query the top level error handler (uncaught exception handler). To check if + * PLCrashReporter's error handler is successfully added, we compare the top level one that is set before and the one after + * PLCrashReporter sets up its own. With delayed processing we can then check if another error handler was set up afterwards and can + * show a debug warning log message, that the dev has to make sure the "newer" error handler doesn't exit the process itself, because + * then all subsequent handlers would never be invoked. Note: ANY error handler setup BEFORE SDK initialization will not be processed! + */ + NSUncaughtExceptionHandler *initialHandler = NSGetUncaughtExceptionHandler(); + NSError *error = nil; + [self.plCrashReporter setCrashCallbacks:&plCrashCallbacks]; + if (![self.plCrashReporter enableCrashReporterAndReturnError:&error]) + MSACLogError([MSACCrashes logTag], @"Could not enable crash reporter: %@", [error localizedDescription]); + NSUncaughtExceptionHandler *currentHandler = NSGetUncaughtExceptionHandler(); + if (currentHandler && currentHandler != initialHandler) { + self.exceptionHandler = currentHandler; + MSACLogDebug([MSACCrashes logTag], @"Exception handler successfully initialized."); + } else if (currentHandler && !enableUncaughtExceptionHandler) { + self.exceptionHandler = currentHandler; + MSACLogDebug([MSACCrashes logTag], + @"Exception handler successfully initialized but it has not been registered due to the wrapper SDK."); + } else { + MSACLogError([MSACCrashes logTag], @"Exception handler could not be set. Make sure there is no other exception handler set up!"); + } + + // Add a handler for C++-Exceptions. + [MSACCrashesUncaughtCXXExceptionHandlerManager addCXXExceptionHandler:uncaught_cxx_exception_handler]; + + // Activate application class methods forwarding to handle additional crash details. + [MSACApplicationForwarder registerForwarding]; + } +} + +#pragma mark - Crash processing + +- (void)startDelayedCrashProcessing { + + /* + * FIXME: If application is crashed and relaunched from multitasking view, the SDK starts faster than normal launch and application state + * is not updated from inactive to active at this time. Give more delay here for a workaround but we need to fix it eventually. This can + * also happen if the application is launched from Xcode and stopped by clicking the stop button on Xcode. + * In addition to that, we also need it to be delayed because + * 1. it sometimes needs to "warm up" internet connection on iOS 8, + * 2. giving some time to start and let all Crashes initialization happen before processing crashes. + */ + + // This must be performed asynchronously to prevent a deadlock with 'unprocessedCrashReports'. + dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, kMSACCrashProcessingDelay); + dispatch_after(delay, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + /* + * FIXME: There is no life cycle for app extensions yet so force start crash processing until then. + * Note that macOS cannot access the application state from a background thread, so crash processing will start without this check. + * + * Also force-start crash processing when automatic processing is disabled. Though it sounds counterintuitive, this is important because + * there are scenarios in some wrappers (i.e. ReactNative) where the application state is not ready by the time crash processing needs + * to happen. + */ + if (self.automaticProcessingEnabled && [MSACUtility applicationState] == MSACApplicationStateBackground) { + MSACLogWarning([MSACCrashes logTag], @"Crashes will not be processed because the application is in the background."); + return; + } + + // Process and release only once. + dispatch_once(&delayedProcessingToken, ^{ + [self startCrashProcessing]; + dispatch_semaphore_signal(self.delayedProcessingSemaphore); + }); + }); +} + +- (void)startCrashProcessing { + MSACLogDebug([MSACCrashes logTag], @"Start delayed CrashManager processing"); + + // Was our own exception handler successfully added? + if (self.exceptionHandler) { + + // Get the current top level error handler. + NSUncaughtExceptionHandler *currentHandler = NSGetUncaughtExceptionHandler(); + + /* + * If the top level error handler differs from our own, at least another one was added. This could cause exception crashes not to be + * reported to App Center. Print out log message for details. + */ + if (self.exceptionHandler != currentHandler) { + MSACLogWarning([MSACCrashes logTag], @"Another exception handler was added. If " + @"this invokes any kind of exit() after processing the " + @"exception, which causes any subsequent error handler " + @"not to be invoked, these crashes will NOT be reported " + @"to App Center!"); + } + } + [self processCrashReports]; +} + +- (void)processCrashReports { + + // Handle 'disabled' state all at once to simplify the logic that follows. + if (!self.isEnabled) { + MSACLogDebug([MSACCrashes logTag], @"Crashes service is disabled; discard all crash reports"); + [self deleteAllFromCrashesDirectory]; + [MSACWrapperExceptionManager deleteAllWrapperExceptions]; + return; + } + NSError *error = nil; + self.unprocessedReports = [NSMutableArray new]; + self.unprocessedLogs = [NSMutableArray new]; + self.unprocessedFilePaths = [NSMutableArray new]; + + // First save all found crash reports for use in correlation step. + NSMutableDictionary *foundCrashReports = [NSMutableDictionary new]; + NSMutableDictionary *foundErrorReports = [NSMutableDictionary new]; + for (NSURL *fileURL in self.crashFiles) { + NSData *crashFileData = [NSData dataWithContentsOfURL:fileURL]; + if ([crashFileData length] > 0) { + PLCrashReport *report = [[PLCrashReport alloc] initWithData:crashFileData error:&error]; + if (report) { + foundCrashReports[fileURL] = report; + foundErrorReports[fileURL] = [MSACErrorLogFormatter errorReportFromCrashReport:report]; + } else { + MSACLogWarning([MSACCrashes logTag], @"Crash report found but couldn't parse it, discard the crash report: %@", + error.localizedDescription); + } + } + } + + // Correlation step. + [MSACWrapperExceptionManager correlateLastSavedWrapperExceptionToReport:[foundErrorReports allValues]]; + + // Processing step. + for (NSURL *fileURL in [foundCrashReports allKeys]) { + MSACLogVerbose([MSACCrashes logTag], @"Crash reports found"); + PLCrashReport *report = foundCrashReports[fileURL]; + MSACErrorReport *errorReport = foundErrorReports[fileURL]; + MSACAppleErrorLog *log = [MSACErrorLogFormatter errorLogFromCrashReport:report]; + if (!self.automaticProcessingEnabled || [self shouldProcessErrorReport:errorReport]) { + if (!self.automaticProcessingEnabled) { + MSACLogDebug([MSACCrashes logTag], @"Automatic crash processing is disabled, storing the crash report for later processing: %@", + report.debugDescription); + } else { + MSACLogDebug([MSACCrashes logTag], @"shouldProcessErrorReport is not implemented or returned YES, processing the crash report: %@", + report.debugDescription); + } + + // Put the log to temporary space for next callbacks. + [self.unprocessedLogs addObject:log]; + [self.unprocessedReports addObject:errorReport]; + [self.unprocessedFilePaths addObject:fileURL]; + } else { + MSACLogDebug([MSACCrashes logTag], @"shouldProcessErrorReport returned NO, discard the crash report: %@", report.debugDescription); + + // Discard the crash report. + [MSACWrapperExceptionManager deleteWrapperExceptionWithUUIDString:errorReport.incidentIdentifier]; + [self deleteCrashReportWithFileURL:fileURL]; + [self.crashFiles removeObject:fileURL]; + } + } + + // Send reports or await user confirmation if automatic processing is enabled. + if (self.automaticProcessingEnabled) { + [self sendCrashReportsOrAwaitUserConfirmation]; + } +} + +- (void)processLogBufferAfterCrash { + + // Initialize a dedicated channel for log buffer. + self.bufferChannelUnit = + [self.channelGroup addChannelUnitWithConfiguration:[[MSACChannelUnitConfiguration alloc] initWithGroupId:kMSACBufferGroupId + priority:MSACPriorityHigh + flushInterval:1.0 + batchSizeLimit:50 + pendingBatchesLimit:1]]; + + // Iterate over each file in it with the kMSACLogBufferFileExtension and send the log if a log can be deserialized. + NSArray *files = [MSACUtility contentsOfDirectory:[NSString stringWithFormat:@"%@", self.logBufferPathComponent] + propertiesForKeys:nil]; + for (NSURL *fileURL in files) { + if ([[fileURL pathExtension] isEqualToString:kMSACLogBufferFileExtension]) { + NSData *serializedLog = [NSData dataWithContentsOfURL:fileURL]; + if (serializedLog && serializedLog.length && serializedLog.length > 0) { + id item; + NSException *exception; + + // Deserialize the log. + item = static_cast>([MSACUtility unarchiveKeyedData:serializedLog]); + if (!item) { + + // The archived log is not valid. + MSACLogError([MSACAppCenter logTag], @"Deserialization failed for log: %@", + exception ? exception.reason : @"The log deserialized to NULL."); + + continue; + } + if (item) { + + // Try to set target token. + NSString *targetTokenFilePath = [fileURL.path stringByReplacingOccurrencesOfString:kMSACLogBufferFileExtension + withString:kMSACTargetTokenFileExtension]; + NSURL *targetTokenFileURL = [NSURL fileURLWithPath:targetTokenFilePath]; + NSString *targetToken = [NSString stringWithContentsOfURL:targetTokenFileURL encoding:NSUTF8StringEncoding error:nil]; + if (targetToken) { + targetToken = [self.targetTokenEncrypter decryptString:targetToken]; + if (targetToken) { + [item addTransmissionTargetToken:targetToken]; + } else { + MSACLogError([MSACAppCenter logTag], @"Failed to decrypt the target token."); + } + + // Delete target token file. + [MSACUtility deleteFileAtURL:targetTokenFileURL]; + } + + // Buffered logs are used sending their own channel. It will never contain more than 50 logs. + MSACLogDebug([MSACCrashes logTag], @"Re-enqueueing buffered log, type: %@.", item.type); + // TODO Must read log priority and serialize to be able to enqueue with proper criticality + [self.bufferChannelUnit enqueueItem:item flags:MSACFlagsDefault]; + } + } + + // Create empty new file, overwrites the old one. + [[NSData data] writeToURL:fileURL atomically:NO]; + } + } +} + +- (void)processMemoryWarningInLastSession { + if (!self.isEnabled) { + return; + } + + // Read and reset the memory warning state. + NSNumber *didReceiveMemoryWarning = [MSAC_APP_CENTER_USER_DEFAULTS objectForKey:kMSACAppDidReceiveMemoryWarningKey]; + self.didReceiveMemoryWarningInLastSession = didReceiveMemoryWarning.boolValue; + if (self.didReceiveMemoryWarningInLastSession) { + MSACLogDebug([MSACCrashes logTag], @"The application received a low memory warning in the last session."); + } + + // Clean the flag. + [MSAC_APP_CENTER_USER_DEFAULTS removeObjectForKey:kMSACAppDidReceiveMemoryWarningKey]; +} + +/** + * Gets a list of unprocessed crashes as MSACErrorReports. + */ +- (NSArray *)unprocessedCrashReports { + dispatch_semaphore_wait(self.delayedProcessingSemaphore, DISPATCH_TIME_FOREVER); + dispatch_semaphore_signal(self.delayedProcessingSemaphore); + return self.unprocessedReports; +} + +/** + * Resumes processing for a given subset of the unprocessed reports. Returns YES if should "AlwaysSend". + */ +- (BOOL)sendCrashReportsOrAwaitUserConfirmationForFilteredIds:(NSArray *)filteredIds { + NSMutableArray *filteredOutLogs = [NSMutableArray new]; + NSMutableArray *filteredOutReports = [NSMutableArray new]; + NSMutableArray *filteredOutFilePaths = [NSMutableArray new]; + for (NSUInteger i = 0; i < [self.unprocessedReports count]; i++) { + MSACErrorReport *report = self.unprocessedReports[i]; + MSACErrorReport *foundReport = nil; + for (NSString *filteredReportId in filteredIds) { + if ([report.incidentIdentifier isEqualToString:filteredReportId]) { + foundReport = report; + break; + } + } + + // Use the report from the list in case it was modified at all. + if (foundReport) { + self.unprocessedReports[i] = foundReport; + } else { + MSACAppleErrorLog *log = self.unprocessedLogs[i]; + NSURL *filePath = self.unprocessedFilePaths[i]; + [filteredOutReports addObject:report]; + [filteredOutLogs addObject:log]; + [filteredOutFilePaths addObject:filePath]; + + // Remove the items from disk. + [MSACWrapperExceptionManager deleteWrapperExceptionWithUUIDString:report.incidentIdentifier]; + [self deleteCrashReportWithFileURL:filePath]; + [self.crashFiles removeObject:filePath]; + } + } + + // Remove filtered out items from memory. + [self.unprocessedLogs removeObjectsInArray:filteredOutLogs]; + [self.unprocessedFilePaths removeObjectsInArray:filteredOutFilePaths]; + [self.unprocessedReports removeObjectsInArray:filteredOutReports]; + + // Send or await user confirmation. + return [self sendCrashReportsOrAwaitUserConfirmation]; +} + +/** + * Sends error attachments for a particular error report. + */ +- (void)sendErrorAttachments:(NSArray *)errorAttachments withIncidentIdentifier:(NSString *)incidentIdentifier { + + // Send attachments log to log manager. + for (MSACErrorAttachmentLog *attachment in errorAttachments) { + attachment.errorId = incidentIdentifier; + if (![MSACCrashes validatePropertiesForAttachment:attachment]) { + MSACLogError([MSACCrashes logTag], @"Not all required fields are present in MSACErrorAttachmentLog."); + continue; + } + if ([attachment data].length > kMaxAttachmentSize) { + MSACLogError([MSACCrashes logTag], @"Discarding attachment with size above %u bytes: size=%tu, fileName=%@.", kMaxAttachmentSize, + [attachment data].length, [attachment filename]); + continue; + } + [self.channelUnit enqueueItem:attachment flags:MSACFlagsDefault]; + } +} + +#pragma mark - Helper + +- (void)deleteAllFromCrashesDirectory { + [MSACUtility deleteItemForPathComponent:self.crashesPathComponent]; + [self.crashFiles removeAllObjects]; +} + +- (void)deleteCrashReportWithFileURL:(NSURL *)fileURL { + [MSACUtility deleteFileAtURL:fileURL]; +} + +- (void)handleLatestCrashReport { + NSError *error = nil; + + // Check if the next call ran successfully the last time. + if (![MSACUtility fileExistsForPathComponent:self.analyzerInProgressFilePathComponent]) { + + // Mark the start of the routine. + [self createAnalyzerFile]; + + // Try loading the crash report. + NSData *crashData = [[NSData alloc] initWithData:[self.plCrashReporter loadPendingCrashReportDataAndReturnError:&error]]; + if (crashData == nil) { + MSACLogError([MSACCrashes logTag], @"Couldn't load crash report: %@", error.localizedDescription); + } else { + + // Get data of PLCrashReport and write it to SDK directory. + PLCrashReport *report = [[PLCrashReport alloc] initWithData:crashData error:&error]; + if (report) { + NSString *cacheFilename = [NSString stringWithFormat:@"%.0f", [NSDate timeIntervalSinceReferenceDate]]; + NSString *crashPath = [NSString stringWithFormat:@"%@/%@", self.crashesPathComponent, cacheFilename]; + [MSACUtility createFileAtPathComponent:crashPath withData:crashData atomically:YES forceOverwrite:NO]; + self.lastSessionCrashReport = [MSACErrorLogFormatter errorReportFromCrashReport:report]; + [MSACWrapperExceptionManager correlateLastSavedWrapperExceptionToReport:@[ self.lastSessionCrashReport ]]; + } else { + MSACLogWarning([MSACCrashes logTag], @"Couldn't parse crash report: %@", error.localizedDescription); + } + } + } else { + MSACLogError([MSACCrashes logTag], @"Error on loading the crash report, it will be purged."); + } + + // Purge the report marker at the end of the routine. + [self removeAnalyzerFile]; + [self.plCrashReporter purgePendingCrashReport]; +} + +- (NSMutableArray *)persistedCrashReports { + NSMutableArray *persistedCrashReports = [NSMutableArray new]; + NSArray *files = [MSACUtility contentsOfDirectory:self.crashesPathComponent + propertiesForKeys:@[ NSURLNameKey, NSURLFileSizeKey, NSURLIsRegularFileKey ]]; + if (!files) { + MSACLogError([MSACCrashes logTag], @"No persisted crashes found."); + return persistedCrashReports; + } + for (NSURL *fileURL in files) { + NSString *fileName = nil; + [fileURL getResourceValue:&fileName forKey:NSURLNameKey error:nil]; + NSNumber *fileSizeNumber = nil; + [fileURL getResourceValue:&fileSizeNumber forKey:NSURLFileSizeKey error:nil]; + NSNumber *isRegular = nil; + [fileURL getResourceValue:&isRegular forKey:NSURLIsRegularFileKey error:nil]; + if ([isRegular boolValue] && [fileSizeNumber intValue] > 0 && ![fileName hasSuffix:@".DS_Store"] && + ![fileName hasSuffix:@".analyzer"] && ![fileName hasSuffix:@".plist"] && ![fileName hasSuffix:@".data"] && + ![fileName hasSuffix:@".meta"] && ![fileName hasSuffix:@".desc"]) { + [persistedCrashReports addObject:fileURL]; + } + } + return persistedCrashReports; +} + +- (void)removeAnalyzerFile { + [MSACUtility deleteItemForPathComponent:self.analyzerInProgressFilePathComponent]; +} + +- (void)createAnalyzerFile { + NSURL *analyzerURL = [MSACUtility createFileAtPathComponent:self.analyzerInProgressFilePathComponent + withData:nil + atomically:NO + forceOverwrite:NO]; + if (!analyzerURL) { + MSACLogError([MSACCrashes logTag], @"Couldn't create crash analyzer file."); + } +} + +- (void)setupLogBuffer { + + // We need to make this @synchronized here as we're setting up msACCrashesLogBuffer. + @synchronized(self) { + + // Setup asynchronously. + NSMutableArray *files = [NSMutableArray arrayWithCapacity:ms_crashes_log_buffer_size]; + + /* + * Create missing buffer files if needed. We don't care about which one's are already there, we'll skip existing ones. + */ + for (NSUInteger i = 0; i < ms_crashes_log_buffer_size; i++) { + + // Files are named N.mscrasheslogbuffer where N is between 0 and ms_crashes_log_buffer_size. + NSString *logId = @(i).stringValue; + NSString *filePathComponent = + [NSString stringWithFormat:@"%@/%@.%@", self.logBufferPathComponent, logId, kMSACLogBufferFileExtension]; + [files addObject:[MSACUtility fullURLForPathComponent:filePathComponent]]; + + // Create files asynchronously. We don't really care as they are only ever used in the post-crash callback. + dispatch_group_async(self.bufferFileGroup, self.bufferFileQueue, ^{ + [MSACUtility createFileAtPathComponent:filePathComponent withData:nil atomically:NO forceOverwrite:NO]; + }); + + // We need to convert the NSURL to NSString as we cannot safe NSURL to our async-safe log buffer. + NSString *path = files[i].path; + + /* + * Some explanation into what actually happens, courtesy of Gwynne: "Passing nil does not initialize anything to nil here, what + * actually happens is an exploit of the Objective-C send-to-nil-returns-zero rule, so that the effective initialization becomes + * `buffer(&(0)[0], &(0)[0])`, and since `NULL` is zero, `[0]` is equivalent to a direct dereference, and `&(*(NULL))` cancels out to + * just `NULL`, it becomes `buffer(nullptr, nullptr)`, which is a no-op because the initializer code loops as `while(begin != end)`, + * so the `nil` pointer is never dereferenced." + */ + msACCrashesLogBuffer[i] = MSACCrashesBufferedLog(path, nil); + + // Save target token path as well to avoid memory allocation when saving. + NSString *targetTokenPath = [path stringByReplacingOccurrencesOfString:kMSACLogBufferFileExtension + withString:kMSACTargetTokenFileExtension]; + msACCrashesLogBuffer[i].targetTokenPath = targetTokenPath.UTF8String; + } + } +} + +- (void)emptyLogBufferFiles { + NSString *bufferDir = [NSString stringWithFormat:@"%@", self.logBufferPathComponent]; + NSArray *files = [MSACUtility contentsOfDirectory:bufferDir propertiesForKeys:nil]; + if (!files) { + MSACLogError([MSACCrashes logTag], @"Couldn't get files in the directory \"%@\"", bufferDir); + return; + } + for (NSURL *fileURL in files) { + if ([[fileURL pathExtension] isEqualToString:kMSACLogBufferFileExtension]) { + + // Create empty new file, overwrites the old one. + NSNumber *fileSizeNumber = nil; + [fileURL getResourceValue:&fileSizeNumber forKey:NSURLFileSizeKey error:nil]; + if ([fileSizeNumber intValue] > 0) { + NSString *fileName = [fileURL lastPathComponent]; + NSString *filePathComponent = [NSString stringWithFormat:@"%@/%@", bufferDir, fileName]; + [MSACUtility createFileAtPathComponent:filePathComponent withData:nil atomically:NO forceOverwrite:YES]; + } + } + } +} + +- (BOOL)shouldProcessErrorReport:(MSACErrorReport *)errorReport { + id delegate = self.delegate; + if ([delegate respondsToSelector:@selector(crashes:shouldProcessErrorReport:)]) { + return [delegate crashes:self shouldProcessErrorReport:errorReport]; + } + return YES; +} + +// We need to override setter, because it's default behavior creates an NSArray, and some tests fail. +- (void)setCrashFiles:(NSMutableArray *)crashFiles { + _crashFiles = [[NSMutableArray alloc] initWithArray:crashFiles]; +} + ++ (BOOL)validatePropertiesForAttachment:(MSACErrorAttachmentLog *)attachment { + BOOL errorIdValid = attachment.errorId && ([attachment.errorId length] > 0); + BOOL attachmentIdValid = attachment.attachmentId && ([attachment.attachmentId length] > 0); + BOOL attachmentDataValid = attachment.data && ([attachment.data length] > 0); + BOOL contentTypeValid = attachment.contentType && ([attachment.contentType length] > 0); + + return errorIdValid && attachmentIdValid && attachmentDataValid && contentTypeValid; +} + +- (BOOL)sendCrashReportsOrAwaitUserConfirmation { + BOOL alwaysSend = [self shouldAlwaysSend]; + + // Get a user confirmation if there are crash logs that need to be processed. + if ([self.unprocessedReports count] == 0) { + return alwaysSend; + } + if (alwaysSend) { + + // User confirmation is set to MSACUserConfirmationAlways. + MSACLogDebug([MSACCrashes logTag], @"The flag for user confirmation is set to MSACUserConfirmationAlways, continue sending logs"); + [self handleUserConfirmation:MSACUserConfirmationSend]; + return alwaysSend; + } else if (self.automaticProcessingEnabled && !(self.userConfirmationHandler && [self userPromptedForConfirmation])) { + + // User confirmation handler doesn't exist or returned NO which means 'want to process'. + MSACLogDebug([MSACCrashes logTag], @"The user confirmation handler is not implemented or returned NO, continue sending logs"); + [self handleUserConfirmation:MSACUserConfirmationSend]; + } else if (!self.automaticProcessingEnabled) { + MSACLogDebug([MSACCrashes logTag], @"Automatic crash processing is disabled and \"AlwaysSend\" is false. Awaiting user confirmation."); + } + return alwaysSend; +} + +- (BOOL)userPromptedForConfirmation { + + // User confirmation handler may contain UI so we have to run it in the main thread. + __block BOOL userPromptedForConfirmation; + if ([NSThread isMainThread]) { + userPromptedForConfirmation = self.userConfirmationHandler(self.unprocessedReports); + } else { + dispatch_sync(dispatch_get_main_queue(), ^{ + userPromptedForConfirmation = self.userConfirmationHandler(self.unprocessedReports); + }); + } + return userPromptedForConfirmation; +} + +/** + * This is an instance method to make testing easier. + */ +- (BOOL)shouldAlwaysSend { + NSNumber *flag = [MSAC_APP_CENTER_USER_DEFAULTS objectForKey:kMSACUserConfirmationKey]; + return flag.boolValue; +} + +/** + * Sends error attachments for a particular error report. + */ ++ (void)sendErrorAttachments:(NSArray *)errorAttachments withIncidentIdentifier:(NSString *)incidentIdentifier { + [[MSACCrashes sharedInstance] sendErrorAttachments:errorAttachments withIncidentIdentifier:incidentIdentifier]; +} + +- (void)notifyWithUserConfirmation:(MSACUserConfirmation)userConfirmation { + + // Check if there is no handler set and unprocessedReports are not initialized as NSMutableArray (Init occurs in correct call sequence). + if (!self.userConfirmationHandler && !self.unprocessedReports) { + MSACLogError(MSACCrashes.logTag, + @"Incorrect usage of notifyWithUserConfirmation: it should only be called from userConfirmationHandler. " + @"For more information refer to the documentation."); + return; + } + [self handleUserConfirmation:userConfirmation]; +} + +- (void)handleUserConfirmation:(MSACUserConfirmation)userConfirmation { + NSArray *attachments; + + // Check for user confirmation. + if (userConfirmation == MSACUserConfirmationDontSend) { + + // Don't send logs, clean up the files. + for (NSUInteger i = 0; i < [self.unprocessedFilePaths count]; i++) { + NSURL *fileURL = self.unprocessedFilePaths[i]; + MSACErrorReport *report = self.unprocessedReports[i]; + [self deleteCrashReportWithFileURL:fileURL]; + [MSACWrapperExceptionManager deleteWrapperExceptionWithUUIDString:report.incidentIdentifier]; + [self.crashFiles removeObject:fileURL]; + } + + // Return and do not continue with crash processing. + [self clearUnprocessedReports]; + [self clearContextHistoryAndKeepCurrentSession]; + return; + } else if (userConfirmation == MSACUserConfirmationAlways) { + + /* + * Always send logs. Set the flag YES to bypass user confirmation next time. + * Continue crash processing afterwards. + */ + [MSAC_APP_CENTER_USER_DEFAULTS setObject:@YES forKey:kMSACUserConfirmationKey]; + } + + // Process crashes logs. + for (NSUInteger i = 0; i < [self.unprocessedReports count]; i++) { + MSACAppleErrorLog *log = self.unprocessedLogs[i]; + MSACErrorReport *report = self.unprocessedReports[i]; + NSURL *fileURL = self.unprocessedFilePaths[i]; + + // Get error attachments. + id delegate = self.delegate; + if ([delegate respondsToSelector:@selector(attachmentsWithCrashes:forErrorReport:)]) { + attachments = [delegate attachmentsWithCrashes:self forErrorReport:report]; + } else { + MSACLogDebug([MSACCrashes logTag], @"attachmentsWithCrashes is not implemented"); + } + + // First, get correlated session Id. + log.sid = [[MSACSessionContext sharedInstance] sessionIdAt:log.timestamp]; + + // Second, get correlated user Id. + log.userId = [[MSACUserIdContext sharedInstance] userIdAt:log.timestamp]; + + // Then, enqueue crash log. + [self.channelUnit enqueueItem:log flags:MSACFlagsCritical]; + + // Send error attachments. + [self sendErrorAttachments:attachments withIncidentIdentifier:report.incidentIdentifier]; + + // Clean up. + [self deleteCrashReportWithFileURL:fileURL]; + [MSACWrapperExceptionManager deleteWrapperExceptionWithUUIDString:report.incidentIdentifier]; + [self.crashFiles removeObject:fileURL]; + } + [self clearUnprocessedReports]; + [self clearContextHistoryAndKeepCurrentSession]; +} + +- (void)clearUnprocessedReports { + [self.unprocessedReports removeAllObjects]; + [self.unprocessedLogs removeAllObjects]; + [self.unprocessedFilePaths removeAllObjects]; +} + ++ (void)resetSharedInstance { + + // Reset the onceToken so dispatch_once will run again. + onceToken = 0; + sharedInstance = nil; + + // Reset delayed processing token. + delayedProcessingToken = 0; +} + +#pragma mark - Handled exceptions + +- (NSString *)trackModelException:(MSACException *)exception + withProperties:(nullable NSDictionary *)properties + withAttachments:(nullable NSArray *)attachments { + @synchronized(self) { + if (![self canBeUsed] || ![self isEnabled]) { + return nil; + } + + // Create an error log. + MSACHandledErrorLog *log = [MSACHandledErrorLog new]; + + // Set userId to the error log. + log.userId = [[MSACUserIdContext sharedInstance] userId]; + + // Set properties of the error log. + log.errorId = MSAC_UUID_STRING; + log.exception = exception; + if (properties && properties.count > 0) { + + // Cast to a nonnull dictionary. + NSDictionary *nonNullProperties = properties; + + // Send only valid properties. + log.properties = [MSACUtility validateProperties:nonNullProperties + forLogName:[NSString stringWithFormat:@"ErrorLog: %@", log.errorId] + type:log.type]; + } + + // Enqueue log. + [self.channelUnit enqueueItem:log flags:MSACFlagsDefault]; + + // Send error attachment logs. + if (attachments) { + + // Cast to a nonnull array. + NSArray *nonNullAttachments = attachments; + [self sendErrorAttachments:nonNullAttachments withIncidentIdentifier:log.errorId]; + } + return log.errorId; + } +} + +- (MSACErrorReport *)buildHandledErrorReportWithErrorID:(NSString *)errorID { + return [[MSACErrorReport alloc] initWithErrorId:errorID + reporterKey:nil + signal:nil + exceptionName:nil + exceptionReason:nil + appStartTime:self.appStartTime + appErrorTime:[NSDate date] + device:[[MSACDeviceTracker sharedInstance] device] + appProcessIdentifier:0]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/MSACCrashesDelegate.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/MSACCrashesDelegate.h new file mode 100644 index 0000000000..439e25c2b3 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/MSACCrashesDelegate.h @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +@class MSACCrashes; +@class MSACErrorReport; +@class MSACErrorAttachmentLog; + +NS_ASSUME_NONNULL_BEGIN + +NS_SWIFT_NAME(CrashesDelegate) +@protocol MSACCrashesDelegate + +@optional + +/** + * Callback method that will be called before processing errors. + * + * @param crashes The instance of MSACCrashes. + * @param errorReport The errorReport that will be sent. + * + * @discussion Crashes will send logs to the server or discard/delete logs based on this method's return value. + */ +- (BOOL)crashes:(MSACCrashes *)crashes shouldProcessErrorReport:(MSACErrorReport *)errorReport NS_SWIFT_NAME(crashes(_:shouldProcess:)); + +/** + * Callback method that will be called before each error will be send to the server. + * + * @param crashes The instance of MSACCrashes. + * @param errorReport The errorReport that will be sent. + * + * @discussion Use this callback to display custom UI while crashes are sent to the server. + */ +- (void)crashes:(MSACCrashes *)crashes willSendErrorReport:(MSACErrorReport *)errorReport; + +/** + * Callback method that will be called after the SDK successfully sent an error report to the server. + * + * @param crashes The instance of MSACCrashes. + * @param errorReport The errorReport that App Center sent. + * + * @discussion Use this method to hide your custom UI. + */ +- (void)crashes:(MSACCrashes *)crashes didSucceedSendingErrorReport:(MSACErrorReport *)errorReport; + +/** + * Callback method that will be called in case the SDK was unable to send an error report to the server. + * + * @param crashes The instance of MSACCrashes. + * @param errorReport The errorReport that App Center tried to send. + * @param error The error that occurred. + */ +- (void)crashes:(MSACCrashes *)crashes didFailSendingErrorReport:(MSACErrorReport *)errorReport withError:(NSError *)error; + +/** + * Method to get the attachments associated to an error report. + * + * @param crashes The instance of MSACCrashes. + * @param errorReport The errorReport associated with the returned attachments. + * + * @return The attachments associated with the given error report or nil if the error report doesn't have any attachments. + * + * @discussion Implement this method if you want attachments to the given error report. + */ +- (NSArray *)attachmentsWithCrashes:(MSACCrashes *)crashes forErrorReport:(MSACErrorReport *)errorReport; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Model/MSACErrorAttachmentLog+Utility.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Model/MSACErrorAttachmentLog+Utility.h new file mode 100644 index 0000000000..d3e64fa468 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Model/MSACErrorAttachmentLog+Utility.h @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACErrorAttachmentLog.h" + +// Exporting symbols for category. +extern NSString *MSACMSACErrorLogAttachmentLogUtilityCategory; + +@interface MSACErrorAttachmentLog (Utility) + +/** + * Create an attachment with a given filename and text. + * + * @param filename The filename the attachment should get. If nil will get an automatically generated filename. + * @param text The attachment text. + * + * @return An instance of `MSACErrorAttachmentLog`. + */ ++ (MSACErrorAttachmentLog *)attachmentWithText:(NSString *)text filename:(NSString *)filename; + +/** + * Create an attachment with a given filename and `NSData` object. + * + * @param filename The filename the attachment should get. If nil will get an automatically generated filename. + * @param data The attachment data as NSData. + * @param contentType The content type of your data as MIME type. + * + * @return An instance of `MSACErrorAttachmentLog`. + */ ++ (MSACErrorAttachmentLog *)attachmentWithBinary:(NSData *)data filename:(NSString *)filename contentType:(NSString *)contentType; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Model/MSACErrorAttachmentLog+Utility.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Model/MSACErrorAttachmentLog+Utility.m new file mode 100644 index 0000000000..8cc05fd0d0 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Model/MSACErrorAttachmentLog+Utility.m @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACErrorAttachmentLog+Utility.h" + +// Exporting symbols for category. +NSString *MSACMSACErrorLogAttachmentLogUtilityCategory; + +// This category is used to avoid adding more logic than needed to the model implementation file. +@implementation MSACErrorAttachmentLog (Utility) + ++ (nonnull MSACErrorAttachmentLog *)attachmentWithText:(nonnull NSString *)text filename:(nullable NSString *)filename { + return [[MSACErrorAttachmentLog alloc] initWithFilename:filename attachmentText:text]; +} + ++ (nonnull MSACErrorAttachmentLog *)attachmentWithBinary:(nonnull NSData *)data + filename:(nullable NSString *)filename + contentType:(nonnull NSString *)contentType { + return [[MSACErrorAttachmentLog alloc] initWithFilename:filename attachmentBinary:data contentType:contentType]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Model/MSACErrorAttachmentLog.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Model/MSACErrorAttachmentLog.h new file mode 100644 index 0000000000..c4564226a2 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Model/MSACErrorAttachmentLog.h @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAbstractLog.h" + +/** + * Error attachment log. + */ +NS_SWIFT_NAME(ErrorAttachmentLog) +@interface MSACErrorAttachmentLog : MSACAbstractLog + +/** + * Content type (text/plain for text). + */ +@property(nonatomic, copy) NSString *contentType; + +/** + * File name. + */ +@property(nonatomic, copy) NSString *filename; + +/** + * The attachment data. + */ +@property(nonatomic, copy) NSData *data; + +/** + * Initialize an attachment with a given filename and `NSData` object. + * + * @param filename The filename the attachment should get. If nil will get an automatically generated filename. + * @param data The attachment data as `NSData`. + * @param contentType The content type of your data as MIME type. + * + * @return An instance of `MSACErrorAttachmentLog`. + */ +- (instancetype)initWithFilename:(NSString *)filename attachmentBinary:(NSData *)data contentType:(NSString *)contentType; + +/** + * Initialize an attachment with a given filename and text. + * + * @param filename The filename the attachment should get. If nil will get an automatically generated filename. + * @param text The attachment text. + * + * @return An instance of `MSACErrorAttachmentLog`. + */ +- (instancetype)initWithFilename:(NSString *)filename attachmentText:(NSString *)text; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Model/MSACErrorAttachmentLog.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Model/MSACErrorAttachmentLog.m new file mode 100644 index 0000000000..9f186eef4b --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Model/MSACErrorAttachmentLog.m @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACCrashesUtil.h" +#import "MSACErrorAttachmentLog+Utility.h" +#import "MSACErrorAttachmentLogInternal.h" +#import "MSACUtility.h" + +static NSString *const kMSACTextType = @"text/plain"; + +// API property names. +static NSString *const kMSACTypeAttachment = @"errorAttachment"; +static NSString *const kMSACId = @"id"; +static NSString *const kMSACErrorId = @"errorId"; +static NSString *const kMSACContentType = @"contentType"; +static NSString *const kMSACFileName = @"fileName"; +static NSString *const kMSACData = @"data"; + +@implementation MSACErrorAttachmentLog + +/** + * @discussion Workaround for exporting symbols from category object files. See article + * https://medium.com/ios-os-x-development/categories-in-static-libraries-78e41f8ddb96#.aedfl1kl0 + */ +__attribute__((used)) static void importCategories() { [NSString stringWithFormat:@"%@", MSACMSACErrorLogAttachmentLogUtilityCategory]; } + +- (instancetype)init { + if ((self = [super init])) { + self.type = kMSACTypeAttachment; + _attachmentId = MSAC_UUID_STRING; + } + return self; +} + +- (instancetype)initWithFilename:(nullable NSString *)filename attachmentBinary:(NSData *)data contentType:(NSString *)contentType { + if ((self = [self init])) { + _data = data; + _contentType = contentType; + _filename = filename; + } + return self; +} + +- (instancetype)initWithFilename:(nullable NSString *)filename attachmentText:(NSString *)text { + if ((self = [self init])) { + self = [self initWithFilename:filename attachmentBinary:[text dataUsingEncoding:NSUTF8StringEncoding] contentType:kMSACTextType]; + } + return self; +} + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [super serializeToDictionary]; + + // Fill in the dictionary. + if (self.attachmentId) { + dict[kMSACId] = self.attachmentId; + } + if (self.errorId) { + dict[kMSACErrorId] = self.errorId; + } + if (self.contentType) { + dict[kMSACContentType] = self.contentType; + } + if (self.filename) { + dict[kMSACFileName] = self.filename; + } + if (self.data) { + dict[kMSACData] = [self.data base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithCarriageReturn]; + } + return dict; +} + +- (BOOL)isEqual:(id)object { + if (![(NSObject *)object isKindOfClass:[MSACErrorAttachmentLog class]] && ![super isEqual:object]) + return NO; + MSACErrorAttachmentLog *attachment = (MSACErrorAttachmentLog *)object; + return ((!self.attachmentId && !attachment.attachmentId) || [self.attachmentId isEqualToString:attachment.attachmentId]) && + ((!self.errorId && !attachment.errorId) || [self.errorId isEqualToString:attachment.errorId]) && + ((!self.contentType && !attachment.contentType) || [self.contentType isEqualToString:attachment.contentType]) && + ((!self.filename && !attachment.filename) || [self.filename isEqualToString:attachment.filename]) && + ((!self.data && !attachment.data) || [self.data isEqualToData:attachment.data]); +} + +- (BOOL)isValid { + return [super isValid] && MSACLOG_VALIDATE_NOT_NIL(errorId) && MSACLOG_VALIDATE_NOT_NIL(attachmentId) && MSACLOG_VALIDATE_NOT_NIL(data) && + MSACLOG_VALIDATE_NOT_NIL(contentType); +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super initWithCoder:coder]; + if (self) { + _attachmentId = [coder decodeObjectForKey:kMSACId]; + _errorId = [coder decodeObjectForKey:kMSACErrorId]; + _contentType = [coder decodeObjectForKey:kMSACContentType]; + _filename = [coder decodeObjectForKey:kMSACFileName]; + _data = [coder decodeObjectForKey:kMSACData]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.attachmentId forKey:kMSACId]; + [coder encodeObject:self.errorId forKey:kMSACErrorId]; + [coder encodeObject:self.contentType forKey:kMSACContentType]; + [coder encodeObject:self.filename forKey:kMSACFileName]; + [coder encodeObject:self.data forKey:kMSACData]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Model/MSACErrorReport.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Model/MSACErrorReport.h new file mode 100644 index 0000000000..fb6493746c --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Model/MSACErrorReport.h @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +@class MSACDevice; + +NS_SWIFT_NAME(ErrorReport) +@interface MSACErrorReport : NSObject + +/** + * UUID for the crash report. + */ +@property(nonatomic, copy, readonly) NSString *incidentIdentifier; + +/** + * UUID for the app installation on the device. + */ +@property(nonatomic, copy, readonly) NSString *reporterKey; + +/** + * Signal that caused the crash. + */ +@property(nonatomic, copy, readonly) NSString *signal; + +/** + * Exception name that triggered the crash, nil if the crash was not caused by an exception. + */ +@property(nonatomic, copy, readonly) NSString *exceptionName; + +/** + * Exception reason, nil if the crash was not caused by an exception. + */ +@property(nonatomic, copy, readonly) NSString *exceptionReason; + +/** + * Date and time the app started, nil if unknown. + */ +@property(nonatomic, readonly, strong) NSDate *appStartTime; + +/** + * Date and time the error occurred, nil if unknown + */ +@property(nonatomic, readonly, strong) NSDate *appErrorTime; + +/** + * Device information of the app when it crashed. + */ +@property(nonatomic, readonly, strong) MSACDevice *device; + +/** + * Identifier of the app process that crashed. + */ +@property(nonatomic, readonly, assign) NSUInteger appProcessIdentifier; + +/** + * Indicates if the app was killed while being in foreground from the iOS. + * + * This can happen if it consumed too much memory or the watchdog killed the app because it took too long to startup or blocks the main + * thread for too long, or other reasons. See Apple documentation: + * https://developer.apple.com/library/ios/qa/qa1693/_index.html. + * + * @see `[MSACCrashes didReceiveMemoryWarningInLastSession]` + */ +@property(nonatomic, readonly) BOOL isAppKill; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Model/MSACErrorReport.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Model/MSACErrorReport.m new file mode 100644 index 0000000000..d100bbe976 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Model/MSACErrorReport.m @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACErrorReport.h" +#import "MSACErrorReportPrivate.h" + +@interface MSACErrorReport () + +@property(nonatomic, copy) NSString *signal; + +@end + +@implementation MSACErrorReport + +- (instancetype)initWithErrorId:(NSString *)errorId + reporterKey:(NSString *)reporterKey + signal:(NSString *)signal + exceptionName:(NSString *)exceptionName + exceptionReason:(NSString *)exceptionReason + appStartTime:(NSDate *)appStartTime + appErrorTime:(NSDate *)appErrorTime + device:(MSACDevice *)device + appProcessIdentifier:(NSUInteger)appProcessIdentifier { + + if ((self = [super init])) { + _incidentIdentifier = errorId; + _reporterKey = reporterKey; + _signal = signal; + _exceptionName = exceptionName; + _exceptionReason = exceptionReason; + _appStartTime = appStartTime; + _appErrorTime = appErrorTime; + _device = device; + _appProcessIdentifier = appProcessIdentifier; + } + return self; +} + +- (BOOL)isAppKill { + BOOL result = NO; + + if (self.signal && [[self.signal uppercaseString] isEqualToString:kMSACErrorReportKillSignal]) + result = YES; + + return result; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/AppCenterCrashes Debug.xcconfig b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/AppCenterCrashes Debug.xcconfig new file mode 100644 index 0000000000..c82d2824bf --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/AppCenterCrashes Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../../Config/Global Debug.xcconfig" +#include "./AppCenterCrashes.xcconfig" diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/AppCenterCrashes Release.xcconfig b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/AppCenterCrashes Release.xcconfig new file mode 100644 index 0000000000..7461e1da85 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/AppCenterCrashes Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../../Config/Global Release.xcconfig" +#include "./AppCenterCrashes.xcconfig" diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/AppCenterCrashes.xcconfig b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/AppCenterCrashes.xcconfig new file mode 100644 index 0000000000..47538aae30 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/AppCenterCrashes.xcconfig @@ -0,0 +1,5 @@ +PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.appcenter.crashes + +OTHER_LDFLAGS = $(inherited) -framework Foundation -lc++ -lz + +USER_HEADER_SEARCH_PATHS = $(USER_HEADER_SEARCH_PATHS) "$(SRCROOT)/../Vendor/PLCrashReporter/Source" diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/Info.plist b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/Info.plist new file mode 100644 index 0000000000..e30a31ca6c --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + $(CURRENT_PROJECT_VERSION) + CFBundleVersion + 1.0 + + diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/iOS.modulemap b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/iOS.modulemap new file mode 100644 index 0000000000..858a5299fd --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/iOS.modulemap @@ -0,0 +1,10 @@ +framework module AppCenterCrashes { + umbrella header "AppCenterCrashes.h" + + export * + module * { export * } + + link framework "Foundation" + link "c++" + link "z" +} diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/iOS.xcconfig b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/iOS.xcconfig new file mode 100644 index 0000000000..eeae341cd6 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/iOS.xcconfig @@ -0,0 +1,5 @@ +#include "../../../Config/iOS.xcconfig" +#include "../../../Config/Framework.xcconfig" + +HEADER_SEARCH_PATHS = "$(SRCROOT)/../Vendor/iOS"/** +LIBRARY_SEARCH_PATHS = "$(SRCROOT)/../Vendor/iOS"/** diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/macOS.modulemap b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/macOS.modulemap new file mode 100644 index 0000000000..58d5076e5f --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/macOS.modulemap @@ -0,0 +1,10 @@ +framework module AppCenterCrashes { + umbrella header "AppCenterCrashes.h" + + export * + module * { export * } + + link framework "Foundation" + link "c++" + link "z" +} diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/macOS.xcconfig b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/macOS.xcconfig new file mode 100644 index 0000000000..fe68d66a4d --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/macOS.xcconfig @@ -0,0 +1,5 @@ +#include "../../../Config/macOS.xcconfig" +#include "../../../Config/Framework.xcconfig" + +HEADER_SEARCH_PATHS = "$(SRCROOT)/../Vendor/macOS"/** +LIBRARY_SEARCH_PATHS = "$(SRCROOT)/../Vendor/macOS"/** diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/tvOS.modulemap b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/tvOS.modulemap new file mode 100644 index 0000000000..858a5299fd --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/tvOS.modulemap @@ -0,0 +1,10 @@ +framework module AppCenterCrashes { + umbrella header "AppCenterCrashes.h" + + export * + module * { export * } + + link framework "Foundation" + link "c++" + link "z" +} diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/tvOS.xcconfig b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/tvOS.xcconfig new file mode 100644 index 0000000000..b37a16090b --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/Support/tvOS.xcconfig @@ -0,0 +1,5 @@ +#include "../../../Config/tvOS.xcconfig" +#include "../../../Config/Framework.xcconfig" + +HEADER_SEARCH_PATHS = "$(SRCROOT)/../Vendor/tvOS"/** +LIBRARY_SEARCH_PATHS = "$(SRCROOT)/../Vendor/tvOS"/** diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/WrapperSDKUtilities/MSACCrashHandlerSetupDelegate.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/WrapperSDKUtilities/MSACCrashHandlerSetupDelegate.h new file mode 100644 index 0000000000..0a05beb859 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/WrapperSDKUtilities/MSACCrashHandlerSetupDelegate.h @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +/** + * This is required for Wrapper SDKs that need to provide custom behavior surrounding the setup of crash handlers. + */ +NS_SWIFT_NAME(CrashHandlerSetupDelegate) +@protocol MSACCrashHandlerSetupDelegate + +@optional + +/** + * Callback method that will be called immediately before crash handlers are set up. + */ +- (void)willSetUpCrashHandlers; + +/** + * Callback method that will be called immediately after crash handlers are set up. + */ +- (void)didSetUpCrashHandlers; + +/** + * Callback method that gets a value indicating whether the SDK should enable an uncaught exception handler. + * + * @return YES if SDK should enable uncaught exception handler, otherwise NO. + * + * @discussion Do not register an UncaughtExceptionHandler for Xamarin as we rely on the Xamarin runtime to report NSExceptions. Registering + * our own UncaughtExceptionHandler will cause the Xamarin debugger to not work properly (it will not stop for NSExceptions). + */ +- (BOOL)shouldEnableUncaughtExceptionHandler; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/WrapperSDKUtilities/MSACWrapperCrashesHelper.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/WrapperSDKUtilities/MSACWrapperCrashesHelper.h new file mode 100644 index 0000000000..a156559c98 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/WrapperSDKUtilities/MSACWrapperCrashesHelper.h @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACCrashHandlerSetupDelegate.h" + +NS_ASSUME_NONNULL_BEGIN + +@class MSACErrorReport; +@class MSACErrorAttachmentLog; +@class MSACException; + +/** + * This general class allows wrappers to supplement the Crashes SDK with their own behavior. + */ +NS_SWIFT_NAME(WrapperCrashesHelper) +@interface MSACWrapperCrashesHelper : NSObject + +/** + * The crash handler setup delegate. + * + */ +@property(class, nonatomic) _Nullable id crashHandlerSetupDelegate; + +/** + * Gets the crash handler setup delegate. + * + * @deprecated + * + * @return The delegate being used by Crashes. + */ ++ (id)getCrashHandlerSetupDelegate DEPRECATED_MSG_ATTRIBUTE("Use crashHandlerSetupDelegate instead"); + +/** + * Enables or disables automatic crash processing. Passing NO causes SDK not to send reports immediately, even if "Always Send" is true. + */ +@property(class, nonatomic) BOOL automaticProcessing; + +/** + * Gets a list of unprocessed crash reports. Will block until the service starts. + * + * @return An array of unprocessed error reports. + */ +@property(class, readonly, nonatomic) NSArray *unprocessedCrashReports; + +/** + * Resumes processing for a given subset of the unprocessed reports. + * + * @param filteredIds An array containing the errorId/incidentIdentifier of each report that should be sent. + * + * @return YES if should "Always Send" is true. + */ ++ (BOOL)sendCrashReportsOrAwaitUserConfirmationForFilteredIds:(NSArray *)filteredIds; + +/** + * Sends error attachments for a particular error report. + * + * @param errorAttachments An array of error attachments that should be sent. + * @param incidentIdentifier The identifier of the error report that the attachments will be associated with. + */ ++ (void)sendErrorAttachments:(NSArray *)errorAttachments withIncidentIdentifier:(NSString *)incidentIdentifier; + +/** + * Track handled exception directly as model form. + * This API is used by wrapper SDKs. + * + * @param exception model form exception. + * @param properties dictionary of properties. + * @param attachments a list of attachments. + * + * @return handled error ID. + */ ++ (nullable NSString *)trackModelException:(MSACException *)exception + withProperties:(nullable NSDictionary *)properties + withAttachments:(nullable NSArray *)attachments; + +/** + * Get a generic error report representation for an handled exception. + * This API is used by wrapper SDKs. + * + * @param errorID handled error ID. + * + * @return an error report. + */ ++ (MSACErrorReport *)buildHandledErrorReportWithErrorID:(NSString *)errorID; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/WrapperSDKUtilities/MSACWrapperCrashesHelper.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/WrapperSDKUtilities/MSACWrapperCrashesHelper.m new file mode 100644 index 0000000000..8437c9b74f --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/WrapperSDKUtilities/MSACWrapperCrashesHelper.m @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACWrapperCrashesHelper.h" +#import "MSACCrashesInternal.h" +#import "MSACErrorReportPrivate.h" + +@interface MSACWrapperCrashesHelper () + +@property(weak, nonatomic) id crashHandlerSetupDelegate; + +@end + +@implementation MSACWrapperCrashesHelper + +/** + * Gets the singleton instance. + */ ++ (instancetype)sharedInstance { + static MSACWrapperCrashesHelper *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[MSACWrapperCrashesHelper alloc] init]; + }); + return sharedInstance; +} + ++ (void)setCrashHandlerSetupDelegate:(id)delegate { + [[MSACWrapperCrashesHelper sharedInstance] setCrashHandlerSetupDelegate:delegate]; +} + ++ (id)crashHandlerSetupDelegate { + return [MSACWrapperCrashesHelper sharedInstance].crashHandlerSetupDelegate; +} + ++ (id)getCrashHandlerSetupDelegate { + return [MSACWrapperCrashesHelper sharedInstance].crashHandlerSetupDelegate; +} + +/** + * Enables or disables automatic crash processing. Setting to 'NO'causes SDK not to send reports immediately, even if ALWAYS_SEND is set. + */ ++ (void)setAutomaticProcessing:(BOOL)automaticProcessing { + [[MSACCrashes sharedInstance] setAutomaticProcessingEnabled:automaticProcessing]; +} + ++ (BOOL)automaticProcessing { + return [MSACCrashes sharedInstance].isAutomaticProcessingEnabled; +} + +/** + * Gets a list of unprocessed crash reports. + */ ++ (NSArray *)unprocessedCrashReports { + return [[MSACCrashes sharedInstance] unprocessedCrashReports]; +} + +/** + * Resumes processing for a given subset of the unprocessed reports. Returns YES if should "AlwaysSend". + */ ++ (BOOL)sendCrashReportsOrAwaitUserConfirmationForFilteredIds:(NSArray *)filteredIds { + return [[MSACCrashes sharedInstance] sendCrashReportsOrAwaitUserConfirmationForFilteredIds:filteredIds]; +} + +/** + * Sends error attachments for a particular error report. + */ ++ (void)sendErrorAttachments:(NSArray *)errorAttachments withIncidentIdentifier:(NSString *)incidentIdentifier { + [[MSACCrashes sharedInstance] sendErrorAttachments:errorAttachments withIncidentIdentifier:incidentIdentifier]; +} + +/** + * Track handled exception directly as model form with user-defined custom properties. + * This API is used by wrapper SDKs. + */ ++ (NSString *)trackModelException:(MSACException *)exception + withProperties:(nullable NSDictionary *)properties + withAttachments:(nullable NSArray *)attachments { + return [[MSACCrashes sharedInstance] trackModelException:exception withProperties:properties withAttachments:attachments]; +} + ++ (MSACErrorReport *)buildHandledErrorReportWithErrorID:(NSString *)errorID { + return [[MSACCrashes sharedInstance] buildHandledErrorReportWithErrorID:errorID]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/AppCenterCrashes.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/AppCenterCrashes.h new file mode 120000 index 0000000000..cf87c1004f --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/AppCenterCrashes.h @@ -0,0 +1 @@ +../AppCenterCrashes.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACAbstractLog.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACAbstractLog.h new file mode 120000 index 0000000000..8abdb4eaf8 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACAbstractLog.h @@ -0,0 +1 @@ +../../../AppCenter/AppCenter/Model/MSACAbstractLog.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACCrashHandlerSetupDelegate.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACCrashHandlerSetupDelegate.h new file mode 120000 index 0000000000..8cf1d99c75 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACCrashHandlerSetupDelegate.h @@ -0,0 +1 @@ +../WrapperSDKUtilities/MSACCrashHandlerSetupDelegate.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACCrashes.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACCrashes.h new file mode 120000 index 0000000000..d3d7d4b4d7 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACCrashes.h @@ -0,0 +1 @@ +../MSACCrashes.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACCrashesDelegate.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACCrashesDelegate.h new file mode 120000 index 0000000000..ef7eb31135 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACCrashesDelegate.h @@ -0,0 +1 @@ +../MSACCrashesDelegate.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACErrorAttachmentLog+Utility.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACErrorAttachmentLog+Utility.h new file mode 120000 index 0000000000..808c88d7c5 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACErrorAttachmentLog+Utility.h @@ -0,0 +1 @@ +../Model/MSACErrorAttachmentLog+Utility.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACErrorAttachmentLog.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACErrorAttachmentLog.h new file mode 120000 index 0000000000..f9e3f10370 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACErrorAttachmentLog.h @@ -0,0 +1 @@ +../Model/MSACErrorAttachmentLog.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACErrorReport.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACErrorReport.h new file mode 120000 index 0000000000..669879d864 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACErrorReport.h @@ -0,0 +1 @@ +../Model/MSACErrorReport.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACService.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACService.h new file mode 120000 index 0000000000..b6ea0006fb --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACService.h @@ -0,0 +1 @@ +../../../AppCenter/AppCenter/MSACService.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACServiceAbstract.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACServiceAbstract.h new file mode 120000 index 0000000000..0f8d778f1e --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACServiceAbstract.h @@ -0,0 +1 @@ +../../../AppCenter/AppCenter/MSACServiceAbstract.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACWrapperCrashesHelper.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACWrapperCrashesHelper.h new file mode 120000 index 0000000000..e483d1eae1 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashes/include/MSACWrapperCrashesHelper.h @@ -0,0 +1 @@ +../WrapperSDKUtilities/MSACWrapperCrashesHelper.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_bad_ptr.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_bad_ptr.plcrash new file mode 100644 index 0000000000..cfcd17ec60 Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_bad_ptr.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_call_abort.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_call_abort.plcrash new file mode 100644 index 0000000000..46c6e2ba77 Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_call_abort.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_call_trap.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_call_trap.plcrash new file mode 100644 index 0000000000..b0e9e91987 Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_call_trap.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_corrupt_objc.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_corrupt_objc.plcrash new file mode 100644 index 0000000000..e402e055b5 Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_corrupt_objc.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_cpp_exception.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_cpp_exception.plcrash new file mode 100644 index 0000000000..e9460732e6 Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_cpp_exception.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_jump_into_nx.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_jump_into_nx.plcrash new file mode 100644 index 0000000000..b8c1ac565b Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_jump_into_nx.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_null_ptr.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_null_ptr.plcrash new file mode 100644 index 0000000000..c18e90a041 Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_null_ptr.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_objc_exception.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_objc_exception.plcrash new file mode 100644 index 0000000000..4aa79288e6 Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_objc_exception.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_objc_msgsend.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_objc_msgsend.plcrash new file mode 100644 index 0000000000..8608e9b5be Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_objc_msgsend.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_objc_released.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_objc_released.plcrash new file mode 100644 index 0000000000..ef115e94d4 Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_objc_released.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_overwrite_link.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_overwrite_link.plcrash new file mode 100644 index 0000000000..175b23eb2b Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_overwrite_link.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_pthread_lock.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_pthread_lock.plcrash new file mode 100644 index 0000000000..43baecb884 Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_pthread_lock.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_smash_bottom.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_smash_bottom.plcrash new file mode 100644 index 0000000000..e0443cccdc Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_smash_bottom.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_smash_top.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_smash_top.plcrash new file mode 100644 index 0000000000..05a91bc4e1 Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_smash_top.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_swift_crash.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_swift_crash.plcrash new file mode 100644 index 0000000000..48684a36fb Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_swift_crash.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_undefined_instr.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_undefined_instr.plcrash new file mode 100644 index 0000000000..d86281ab62 Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_undefined_instr.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_write_readonly.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_write_readonly.plcrash new file mode 100644 index 0000000000..eb2f193851 Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbe/live_report_write_readonly.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_abort.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_abort.plcrash new file mode 100644 index 0000000000..2d3dcb9f8e Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_abort.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_builtin_trap.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_builtin_trap.plcrash new file mode 100644 index 0000000000..4428a9516d Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_builtin_trap.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_corrupt_malloc_internal_info.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_corrupt_malloc_internal_info.plcrash new file mode 100644 index 0000000000..a4235e1c56 Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_corrupt_malloc_internal_info.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_corrupt_objc_runtime_structure.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_corrupt_objc_runtime_structure.plcrash new file mode 100644 index 0000000000..446f700d1c Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_corrupt_objc_runtime_structure.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_dereference_bad_pointer.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_dereference_bad_pointer.plcrash new file mode 100644 index 0000000000..0e94ebdb9e Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_dereference_bad_pointer.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_dereference_null_pointer.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_dereference_null_pointer.plcrash new file mode 100644 index 0000000000..2b6bb1ca81 Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_dereference_null_pointer.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_dwarf_unwinding.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_dwarf_unwinding.plcrash new file mode 100644 index 0000000000..b494ab5d70 Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_dwarf_unwinding.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_execute_privileged_instruction.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_execute_privileged_instruction.plcrash new file mode 100644 index 0000000000..90a53961ec Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_execute_privileged_instruction.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_execute_undefined_instruction.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_execute_undefined_instruction.plcrash new file mode 100644 index 0000000000..04026538a5 Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_execute_undefined_instruction.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_jump_into_nx_page.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_jump_into_nx_page.plcrash new file mode 100644 index 0000000000..bd174fa972 Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_jump_into_nx_page.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_objc_access_non_object_as_object.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_objc_access_non_object_as_object.plcrash new file mode 100644 index 0000000000..530eb6d158 Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_objc_access_non_object_as_object.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_objc_crash_inside_msgsend.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_objc_crash_inside_msgsend.plcrash new file mode 100644 index 0000000000..f5a377077e Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_objc_crash_inside_msgsend.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_objc_message_released_object.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_objc_message_released_object.plcrash new file mode 100644 index 0000000000..0b116cd30b Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_objc_message_released_object.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_overwrite_link_register.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_overwrite_link_register.plcrash new file mode 100644 index 0000000000..13a7e08b38 Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_overwrite_link_register.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_pthread_lock.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_pthread_lock.plcrash new file mode 100644 index 0000000000..1d9a379877 Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_pthread_lock.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_smash_the_bottom_of_the_stack.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_smash_the_bottom_of_the_stack.plcrash new file mode 100644 index 0000000000..b7d07b6a56 Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_smash_the_bottom_of_the_stack.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_smash_the_top_of_the_stack.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_smash_the_top_of_the_stack.plcrash new file mode 100644 index 0000000000..5b1f37827e Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_smash_the_top_of_the_stack.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_stack_overflow.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_stack_overflow.plcrash new file mode 100644 index 0000000000..2ccd8810dd Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_stack_overflow.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_swift.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_swift.plcrash new file mode 100644 index 0000000000..8eb009d294 Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_swift.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_throw_cpp_exception.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_throw_cpp_exception.plcrash new file mode 100644 index 0000000000..7ba6ca085c Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_throw_cpp_exception.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_write_to_readonly_page.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_write_to_readonly_page.plcrash new file mode 100644 index 0000000000..253d17cb4c Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/CrashProbeMacOS/macOS_report_write_to_readonly_page.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/live_report_arm64e.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/live_report_arm64e.plcrash new file mode 100644 index 0000000000..6b290fb99d Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/live_report_arm64e.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/live_report_empty.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/live_report_empty.plcrash new file mode 100644 index 0000000000..e69de29bb2 diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/live_report_exception.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/live_report_exception.plcrash new file mode 100644 index 0000000000..ab8d7d94ea Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/live_report_exception.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/live_report_exception_marketing.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/live_report_exception_marketing.plcrash new file mode 100644 index 0000000000..fcfd6bed18 Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/live_report_exception_marketing.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/live_report_signal.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/live_report_signal.plcrash new file mode 100644 index 0000000000..d0b9c41ed7 Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/live_report_signal.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/live_report_signal_marketing.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/live_report_signal_marketing.plcrash new file mode 100644 index 0000000000..a9e9a14002 Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/live_report_signal_marketing.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/live_report_xamarin.plcrash b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/live_report_xamarin.plcrash new file mode 100644 index 0000000000..f79046941a Binary files /dev/null and b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Fixtures/live_report_xamarin.plcrash differ diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Info.plist b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Info.plist new file mode 100644 index 0000000000..6c6c23c43a --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACAppleErrorLogTests.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACAppleErrorLogTests.m new file mode 100644 index 0000000000..c9e407f067 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACAppleErrorLogTests.m @@ -0,0 +1,198 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAppleErrorLog.h" +#import "MSACBinary.h" +#import "MSACCrashesTestUtil.h" +#import "MSACException.h" +#import "MSACTestFrameworks.h" +#import "MSACThread.h" + +@interface MSACAppleErrorLogTests : XCTestCase + +@property(nonatomic) MSACAppleErrorLog *sut; + +@end + +@implementation MSACAppleErrorLogTests + +#pragma mark - Housekeeping + +- (void)setUp { + [super setUp]; + + self.sut = [self appleErrorLog]; +} + +- (void)tearDown { + [super tearDown]; +} + +#pragma mark - Helper + +- (MSACAppleErrorLog *)appleErrorLog { + + MSACAppleErrorLog *appleLog = [MSACAppleErrorLog new]; + appleLog.type = @"iOS Error"; + appleLog.primaryArchitectureId = @1; + appleLog.architectureVariantId = @123; + appleLog.applicationPath = @"user/something/something/mypath"; + appleLog.osExceptionType = @"NSSuperOSException"; + appleLog.osExceptionCode = @"0x08aeee81"; + appleLog.osExceptionAddress = @"0x124342345"; + appleLog.exceptionType = @"NSExceptionType"; + appleLog.exceptionReason = @"Trying to access array[12]"; + appleLog.selectorRegisterValue = @"release()"; + appleLog.threads = @ [[MSACThread new]]; + appleLog.binaries = @ [[MSACBinary new]]; + appleLog.exception = [MSACCrashesTestUtil exception]; + appleLog.errorId = @"123"; + appleLog.processId = @123; + appleLog.processName = @"123"; + appleLog.parentProcessId = @234; + appleLog.parentProcessName = @"234"; + appleLog.errorThreadId = @2; + appleLog.errorThreadName = @"2"; + appleLog.fatal = YES; + appleLog.appLaunchTimestamp = [NSDate dateWithTimeIntervalSince1970:42]; + appleLog.architecture = @"test"; + + return appleLog; +} + +#pragma mark - Tests + +- (void)testInitializationWorks { + XCTAssertNotNil(self.sut); +} + +- (void)testSerializationToDictionaryWorks { + NSDictionary *actual = [self.sut serializeToDictionary]; + XCTAssertNotNil(actual); + assertThat(actual[@"type"], equalTo(self.sut.type)); + assertThat(actual[@"primaryArchitectureId"], equalTo(self.sut.primaryArchitectureId)); + assertThat(actual[@"architectureVariantId"], equalTo(self.sut.architectureVariantId)); + assertThat(actual[@"applicationPath"], equalTo(self.sut.applicationPath)); + assertThat(actual[@"osExceptionType"], equalTo(self.sut.osExceptionType)); + assertThat(actual[@"osExceptionCode"], equalTo(self.sut.osExceptionCode)); + assertThat(actual[@"osExceptionAddress"], equalTo(self.sut.osExceptionAddress)); + assertThat(actual[@"exceptionType"], equalTo(self.sut.exceptionType)); + assertThat(actual[@"exceptionReason"], equalTo(self.sut.exceptionReason)); + assertThat(actual[@"selectorRegisterValue"], equalTo(self.sut.selectorRegisterValue)); + assertThat(actual[@"id"], equalTo(self.sut.errorId)); + assertThat(actual[@"processId"], equalTo(self.sut.processId)); + assertThat(actual[@"processName"], equalTo(self.sut.processName)); + assertThat(actual[@"parentProcessId"], equalTo(self.sut.parentProcessId)); + assertThat(actual[@"parentProcessName"], equalTo(self.sut.parentProcessName)); + assertThat(actual[@"errorThreadId"], equalTo(self.sut.errorThreadId)); + assertThat(actual[@"errorThreadName"], equalTo(self.sut.errorThreadName)); + XCTAssertEqual([actual[@"fatal"] boolValue], self.sut.fatal); + assertThat(actual[@"appLaunchTimestamp"], equalTo(@"1970-01-01T00:00:42.000Z")); + assertThat(actual[@"architecture"], equalTo(self.sut.architecture)); + + // Exception fields. + NSDictionary *exceptionDictionary = actual[@"exception"]; + XCTAssertNotNil(exceptionDictionary); + assertThat(exceptionDictionary[@"type"], equalTo(self.sut.exception.type)); + assertThat(exceptionDictionary[@"message"], equalTo(self.sut.exception.message)); + assertThat(exceptionDictionary[@"wrapperSdkName"], equalTo(self.sut.exception.wrapperSdkName)); +} + +- (void)testNSCodingSerializationAndDeserializationWorks { + + // When + NSData *serializedEvent = [MSACUtility archiveKeyedData:self.sut]; + id actual = [MSACUtility unarchiveKeyedData:serializedEvent]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual, instanceOf([MSACAppleErrorLog class])); + + // The MSACAppleErrorLog. + MSACAppleErrorLog *actualLog = actual; + assertThat(actualLog, equalTo(self.sut)); + XCTAssertTrue([actualLog isEqual:self.sut]); + assertThat(actualLog.type, equalTo(self.sut.type)); + assertThat(actualLog.primaryArchitectureId, equalTo(self.sut.primaryArchitectureId)); + assertThat(actualLog.architectureVariantId, equalTo(self.sut.architectureVariantId)); + assertThat(actualLog.architecture, equalTo(self.sut.architecture)); + assertThat(actualLog.applicationPath, equalTo(self.sut.applicationPath)); + assertThat(actualLog.osExceptionType, equalTo(self.sut.osExceptionType)); + assertThat(actualLog.osExceptionCode, equalTo(self.sut.osExceptionCode)); + assertThat(actualLog.osExceptionAddress, equalTo(self.sut.osExceptionAddress)); + assertThat(actualLog.exceptionType, equalTo(self.sut.exceptionType)); + assertThat(actualLog.exceptionReason, equalTo(self.sut.exceptionReason)); + assertThat(actualLog.selectorRegisterValue, equalTo(self.sut.selectorRegisterValue)); + + // The exception field. + MSACException *actualException = actualLog.exception; + assertThat(actualException.type, equalTo(self.sut.exception.type)); + assertThat(actualException.message, equalTo(self.sut.exception.message)); + assertThat(actualException.wrapperSdkName, equalTo(self.sut.exception.wrapperSdkName)); +} + +- (void)testIsEqual { + + // When + MSACAppleErrorLog *first = [self appleErrorLog]; + MSACAppleErrorLog *second = [self appleErrorLog]; + + // Then + XCTAssertTrue([first isEqual:second]); + + // When + second.processId = @345; + + // Then + XCTAssertFalse([first isEqual:second]); +} + +- (void)testIsValid { + + // When + MSACAppleErrorLog *log = [MSACAppleErrorLog new]; + log.device = OCMClassMock([MSACDevice class]); + OCMStub([log.device isValid]).andReturn(YES); + log.sid = @"sid"; + log.timestamp = [NSDate dateWithTimeIntervalSince1970:42]; + log.errorId = @"errorId"; + log.processId = @123; + log.processName = @"processName"; + log.appLaunchTimestamp = [NSDate dateWithTimeIntervalSince1970:442]; + log.sid = MSAC_UUID_STRING; + + // Then + XCTAssertFalse([log isValid]); + + // When + log.primaryArchitectureId = @456; + + // Then + XCTAssertFalse([log isValid]); + + // When + log.applicationPath = @"applicationPath"; + + // Then + XCTAssertFalse([log isValid]); + + // When + log.osExceptionType = @"exceptionType"; + + // Then + XCTAssertFalse([log isValid]); + + // When + log.osExceptionCode = @"exceptionCode"; + + // Then + XCTAssertFalse([log isValid]); + + // When + log.osExceptionAddress = @"exceptionAddress"; + + // Then + XCTAssertTrue([log isValid]); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACApplicationForwarderTests.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACApplicationForwarderTests.m new file mode 100644 index 0000000000..a4dd80d071 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACApplicationForwarderTests.m @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAppCenterInternal.h" +#import "MSACApplicationForwarder.h" +#import "MSACCrashesPrivate.h" +#import "MSACMockNSUserDefaults.h" +#import "MSACTestFrameworks.h" +#import "MSACUtility+Application.h" + +#if TARGET_OS_OSX +static NSException *lastException; +static void exceptionHandler(NSException *exception) { lastException = exception; } +#endif + +@interface MSACApplicationForwarderTests : XCTestCase + +@end + +@implementation MSACApplicationForwarderTests + +- (void)tearDown { + [super tearDown]; + [MSACCrashes resetSharedInstance]; +} + +#if TARGET_OS_OSX +- (void)testRegisterForwarding { + NSException *testException = [NSException new]; + + // If + id applicationMock = OCMPartialMock([NSApplication sharedApplication]); + id appCenterMock = OCMClassMock([MSACAppCenter class]); + OCMStub([appCenterMock isDebuggerAttached]).andReturn(NO); + id crashesMock = OCMPartialMock([MSACCrashes sharedInstance]); + OCMStub([crashesMock exceptionHandler]).andReturn((NSUncaughtExceptionHandler *)exceptionHandler); + + // When + [MSACApplicationForwarder registerForwarding]; + [applicationMock reportException:testException]; + + // Then + XCTAssertNil(lastException); + + // Disable swizzling. + id bundleMock = OCMClassMock([NSBundle class]); + OCMStub([bundleMock objectForInfoDictionaryKey:@"AppCenterApplicationForwarderEnabled"]).andReturn(@NO); + OCMStub([bundleMock mainBundle]).andReturn(bundleMock); + + // When + [MSACApplicationForwarder registerForwarding]; + [applicationMock reportException:testException]; + + // Then + XCTAssertNil(lastException); + + // Enable crash on exсeptions. + MSACMockNSUserDefaults *settings = [MSACMockNSUserDefaults new]; + [settings setObject:@YES forKey:@"NSApplicationCrashOnExceptions"]; + + // When + [MSACApplicationForwarder registerForwarding]; + [applicationMock reportException:testException]; + + // Then + XCTAssertNil(lastException); + + // Enable swizzling + [bundleMock stopMocking]; + + // When + [MSACApplicationForwarder registerForwarding]; + [applicationMock reportException:testException]; + + // Then + XCTAssertEqual(lastException, testException); + [settings stopMocking]; + [applicationMock stopMocking]; + [appCenterMock stopMocking]; + [crashesMock stopMocking]; +} +#endif + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACBinaryTests.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACBinaryTests.m new file mode 100644 index 0000000000..c061cd36a0 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACBinaryTests.m @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACBinary.h" +#import "MSACTestFrameworks.h" + +@interface MSACBinaryTests : XCTestCase + +@end + +@implementation MSACBinaryTests + +#pragma mark - Tests + +- (void)testSerializingBinaryToDictionaryWorks { + + // If + MSACBinary *sut = [self binary]; + + // When + NSMutableDictionary *actual = [sut serializeToDictionary]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual[@"id"], equalTo(sut.binaryId)); + assertThat(actual[@"startAddress"], equalTo(sut.startAddress)); + assertThat(actual[@"endAddress"], equalTo(sut.endAddress)); + assertThat(actual[@"name"], equalTo(sut.name)); + assertThat(actual[@"path"], equalTo(sut.path)); + assertThat(actual[@"architecture"], equalTo(sut.architecture)); + assertThat(actual[@"primaryArchitectureId"], equalTo(sut.primaryArchitectureId)); + assertThat(actual[@"architectureVariantId"], equalTo(sut.architectureVariantId)); +} + +- (void)testNSCodingSerializationAndDeserializationWorks { + + // If + MSACBinary *sut = [self binary]; + + // When + NSData *serializedEvent = [MSACUtility archiveKeyedData:sut]; + id actual = [MSACUtility unarchiveKeyedData:serializedEvent]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual, instanceOf([MSACBinary class])); + + MSACBinary *actualBinary = actual; + assertThat(actualBinary, equalTo(actual)); + assertThat(actualBinary.binaryId, equalTo(sut.binaryId)); + assertThat(actualBinary.startAddress, equalTo(sut.startAddress)); + assertThat(actualBinary.endAddress, equalTo(sut.endAddress)); + assertThat(actualBinary.name, equalTo(sut.name)); + assertThat(actualBinary.path, equalTo(sut.path)); + assertThat(actualBinary.architecture, equalTo(sut.architecture)); + assertThat(actualBinary.primaryArchitectureId, equalTo(sut.primaryArchitectureId)); + assertThat(actualBinary.architectureVariantId, equalTo(sut.architectureVariantId)); +} + +- (void)testIsValid { + + // If + MSACBinary *sut = [MSACBinary new]; + + // Then + XCTAssertFalse([sut isValid]); + + // When + sut.binaryId = @"binaryId"; + + // Then + XCTAssertFalse([sut isValid]); + + // When + sut.startAddress = @"startAddress"; + + // Then + XCTAssertFalse([sut isValid]); + + // When + sut.endAddress = @"endAddress"; + + // Then + XCTAssertFalse([sut isValid]); + + // When + sut.name = @"name"; + + // Then + XCTAssertFalse([sut isValid]); + + // When + sut.path = @"path"; + + // Then + XCTAssertTrue([sut isValid]); +} + +- (void)testIsNotEqualToNil { + + // Then + XCTAssertFalse([[MSACBinary new] isEqual:nil]); +} + +#pragma mark - Helper + +- (MSACBinary *)binary { + MSACBinary *binary = [MSACBinary new]; + binary.binaryId = @"binaryId"; + binary.startAddress = @"startAddress"; + binary.endAddress = @"endAddress"; + binary.name = @"name"; + binary.path = @"path"; + binary.architecture = @"architecture"; + binary.primaryArchitectureId = @12; + binary.architectureVariantId = @23; + + return binary; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACCrashesCXXExceptionTests.mm b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACCrashesCXXExceptionTests.mm new file mode 100644 index 0000000000..b6fcebe61f --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACCrashesCXXExceptionTests.mm @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import +#import +#import + +#import "MSACCrashesCXXExceptionHandler.h" +#import "MSACCrashesCXXExceptionWrapperException.h" +#import "MSACTestFrameworks.h" + +static void handler1(__attribute__((unused)) const MSACCrashesUncaughtCXXExceptionInfo *__nonnull info) {} + +static void handler2(__attribute__((unused)) const MSACCrashesUncaughtCXXExceptionInfo *__nonnull info) {} + +static int terminates = 0; +static void count_terminates() { terminates++; } + +static const MSACCrashesUncaughtCXXExceptionInfo *last_info = nullptr; +static char last_exception_message[32] = {0}; +static void info_handler(const MSACCrashesUncaughtCXXExceptionInfo *__nonnull info) { + last_info = info; + if (info->exception_message) { + std::strcpy(last_exception_message, info->exception_message); + } else { + std::memset(last_exception_message, 0, sizeof(last_exception_message)); + } +} + +@interface MSACCrashesCXXExceptionWrapperException () + +@property(readonly, nonatomic) const MSACCrashesUncaughtCXXExceptionInfo *info; + +- (NSArray *)callStackReturnAddresses; + +@end + +@interface MSACCrashesCXXExceptionTests : XCTestCase + +@end + +@implementation MSACCrashesCXXExceptionTests + +- (void)testTerminateHandler { + + // If + // Replace original terminate handler. + terminates = 0; + std::terminate_handler original_terminate = std::set_terminate(count_terminates); + + // Add some handler via SDK to initialize. + [MSACCrashesUncaughtCXXExceptionHandlerManager addCXXExceptionHandler:info_handler]; + + // When + // Throw reference to std::exception. + try { + throw std::runtime_error("test1"); + } catch (...) { + std::get_terminate()(); + } + + // Then + XCTAssertEqual(terminates, 1); + XCTAssertEqual(std::strcmp(last_exception_message, "test1"), 0); + + // When + // Throw pointer to std::exception. + try { + throw new std::runtime_error("test2"); + } catch (...) { + std::get_terminate()(); + } + + // Then + XCTAssertEqual(terminates, 2); + XCTAssertEqual(std::strcmp(last_exception_message, "test2"), 0); + + // When + // Throw reference to std::string. + try { + throw std::string("test3"); + } catch (...) { + std::get_terminate()(); + } + + // Then + XCTAssertEqual(terminates, 3); + XCTAssertEqual(std::strcmp(last_exception_message, "test3"), 0); + + // When + // Throw pointer to std::string. + try { + throw new std::string("test4"); + } catch (...) { + std::get_terminate()(); + } + + // Then + XCTAssertEqual(terminates, 4); + XCTAssertEqual(std::strcmp(last_exception_message, "test4"), 0); + + // When + // Throw pointer to chars. + try { + throw "test5"; + } catch (...) { + std::get_terminate()(); + } + + // Then + XCTAssertEqual(terminates, 5); + XCTAssertEqual(std::strcmp(last_exception_message, "test5"), 0); + + // When + // Throw Objective-C exception. + @try { + @throw [NSException exceptionWithName:NSGenericException reason:@"test6" userInfo:nil]; + } @catch (...) { + std::get_terminate()(); + } + + // Then + XCTAssertEqual(terminates, 6); + + // When + // Throw something else. + try { + throw 42; + } catch (...) { + std::get_terminate()(); + } + + // Then + XCTAssertEqual(terminates, 7); + XCTAssertEqual(last_info->exception_message, nullptr); + + // Restore original terminate handler. + std::set_terminate(original_terminate); +} + +- (void)testHandlersCount { + + // Then + XCTAssertEqual([MSACCrashesUncaughtCXXExceptionHandlerManager countCXXExceptionHandler], 0U); + + // When + [MSACCrashesUncaughtCXXExceptionHandlerManager addCXXExceptionHandler:handler1]; + + // Then + XCTAssertEqual([MSACCrashesUncaughtCXXExceptionHandlerManager countCXXExceptionHandler], 1U); + + // When + [MSACCrashesUncaughtCXXExceptionHandlerManager addCXXExceptionHandler:handler2]; + + // Then + XCTAssertEqual([MSACCrashesUncaughtCXXExceptionHandlerManager countCXXExceptionHandler], 2U); + + // When + [MSACCrashesUncaughtCXXExceptionHandlerManager removeCXXExceptionHandler:handler1]; + + // Then + XCTAssertEqual([MSACCrashesUncaughtCXXExceptionHandlerManager countCXXExceptionHandler], 1U); + + // When + [MSACCrashesUncaughtCXXExceptionHandlerManager removeCXXExceptionHandler:handler1]; + + // Then + XCTAssertEqual([MSACCrashesUncaughtCXXExceptionHandlerManager countCXXExceptionHandler], 1U); + + // When + [MSACCrashesUncaughtCXXExceptionHandlerManager removeCXXExceptionHandler:handler2]; + + // Then + XCTAssertEqual([MSACCrashesUncaughtCXXExceptionHandlerManager countCXXExceptionHandler], 0U); + + // When + [MSACCrashesUncaughtCXXExceptionHandlerManager removeCXXExceptionHandler:handler2]; +} + +- (void)testWrapperException { + // If + const uintptr_t frames[2] = {0x123, 0x234}; + MSACCrashesUncaughtCXXExceptionInfo info = { + .exception = nullptr, + .exception_type_name = nullptr, + .exception_message = nullptr, + .exception_frames_count = 2, + .exception_frames = frames, + }; + + // When + MSACCrashesCXXExceptionWrapperException *wrapperException = + [[MSACCrashesCXXExceptionWrapperException alloc] initWithCXXExceptionInfo:&info]; + + // Then + XCTAssertNotNil(wrapperException); + XCTAssertEqual(&info, wrapperException.info); + + // When + NSArray *callStackReturnAddresses = [wrapperException callStackReturnAddresses]; + + // Then + XCTAssertTrue(callStackReturnAddresses.count == 2); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACCrashesTestUtil.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACCrashesTestUtil.h new file mode 100644 index 0000000000..286128c2f6 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACCrashesTestUtil.h @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +@class MSACException; + +@interface MSACCrashesTestUtil : NSObject + ++ (BOOL)createTempDirectory:(NSString *)directory; + ++ (BOOL)copyFixtureCrashReportWithFileName:(NSString *)filename; + ++ (NSData *)dataOfFixtureCrashReportWithFileName:(NSString *)filename; + ++ (MSACException *)exception; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACCrashesTestUtil.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACCrashesTestUtil.m new file mode 100644 index 0000000000..755682fffb --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACCrashesTestUtil.m @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACCrashesTestUtil.h" +#import "MSACException.h" +#import "MSACStackFrame.h" + +@implementation MSACCrashesTestUtil + ++ (BOOL)createTempDirectory:(NSString *)directory { + NSFileManager *fm = [[NSFileManager alloc] init]; + + if (![fm fileExistsAtPath:directory]) { + NSDictionary *attributes = @{NSFilePosixPermissions : @0755}; + NSError *error; + [fm createDirectoryAtPath:directory withIntermediateDirectories:YES attributes:attributes error:&error]; + if (error) + return NO; + } + + return YES; +} + ++ (BOOL)copyFixtureCrashReportWithFileName:(NSString *)filename { + NSFileManager *fm = [[NSFileManager alloc] init]; + + NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier]; + if (!bundleIdentifier) { + const char *progname = getprogname(); + if (progname == NULL) { + return NO; + } + bundleIdentifier = [NSString stringWithUTF8String:progname]; + } + + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + + // create the PLCR cache dir + NSString *plcrRootCrashesDir = [paths[0] stringByAppendingPathComponent:@"com.plausiblelabs.crashreporter.data"]; + if (![MSACCrashesTestUtil createTempDirectory:plcrRootCrashesDir]) + return NO; + + NSString *plcrCrashesDir = [plcrRootCrashesDir stringByAppendingPathComponent:bundleIdentifier]; + if (![MSACCrashesTestUtil createTempDirectory:plcrCrashesDir]) + return NO; + + NSString *filePath = [[NSBundle bundleForClass:self.class] pathForResource:filename ofType:@"plcrash"]; + if (!filePath) + return NO; + + NSError *error = nil; + [fm copyItemAtPath:filePath toPath:[plcrCrashesDir stringByAppendingPathComponent:@"live_report.plcrash"] error:&error]; + return error == nil; +} + ++ (NSData *)dataOfFixtureCrashReportWithFileName:(NSString *)filename { + + // the bundle identifier when running with unit tets is "otest" + const char *progname = getprogname(); + if (progname == NULL) { + return nil; + } + NSString *filePath = [[NSBundle bundleForClass:self.class] pathForResource:filename ofType:@"plcrash"]; + if (!filePath) { + return nil; + } else { + NSData *data = [NSData dataWithContentsOfFile:filePath]; + return data; + } +} + ++ (MSACException *)exception { + NSString *type = @"exceptionType"; + NSString *message = @"message"; + NSString *stackTrace = @"at (wrapper managed-to-native) UIKit.UIApplication:UIApplicationMain " + @"(int,string[],intptr,intptr) \n at UIKit.UIApplication.Main " + @"(System.String[] args, " + @"System.IntPtr principal, System.IntPtr delegate) [0x00005] in " + @"/Users/builder/data/lanes/3969/44931ae8/source/xamarin-macios/src/" + @"UIKit/" + @"UIApplication.cs:79 \n at UIKit.UIApplication.Main (System.String[] " + @"args, System.String " + @"principalClassName, System.String delegateClassName) [0x00038] in " + @"/Users/builder/data/lanes/3969/44931ae8/source/xamarin-macios/src/" + @"UIKit/" + @"UIApplication.cs:63 \n at HockeySDKXamarinDemo.Application.Main " + @"(System.String[] args) " + @"[0x00008] in " + @"/Users/benny/Repositories/MSAC/HockeySDK-XamarinDemo/iOS/Main.cs:17"; + NSString *wrapperSdkName = @"appcenter.xamarin"; + MSACStackFrame *frame = [MSACStackFrame new]; + frame.address = @"frameAddress"; + frame.code = @"frameSymbol"; + NSArray *frames = @[ frame ]; + + MSACException *exception = [MSACException new]; + exception.type = type; + exception.message = message; + exception.stackTrace = stackTrace; + exception.wrapperSdkName = wrapperSdkName; + exception.frames = frames; + + return exception; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACCrashesTests.mm b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACCrashesTests.mm new file mode 100644 index 0000000000..f4eadb5950 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACCrashesTests.mm @@ -0,0 +1,1382 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAppCenterInternal.h" +#import "MSACAppCenterUserDefaultsPrivate.h" +#import "MSACAppleErrorLog.h" +#import "MSACApplicationForwarder.h" +#import "MSACChannelGroupDefault.h" +#import "MSACChannelUnitConfiguration.h" +#import "MSACChannelUnitDefault.h" +#import "MSACCrashHandlerSetupDelegate.h" +#import "MSACCrashReporter.h" +#import "MSACCrashesBufferedLog.hpp" +#import "MSACCrashesCXXExceptionHandler.h" +#import "MSACCrashesInternal.h" +#import "MSACCrashesPrivate.h" +#import "MSACCrashesTestUtil.h" +#import "MSACCrashesUtil.h" +#import "MSACDeviceTrackerPrivate.h" +#import "MSACErrorAttachmentLogInternal.h" +#import "MSACErrorLogFormatter.h" +#import "MSACException.h" +#import "MSACHandledErrorLog.h" +#import "MSACLoggerInternal.h" +#import "MSACMockCrashesDelegate.h" +#import "MSACMockUserDefaults.h" +#import "MSACSessionContextPrivate.h" +#import "MSACTestFrameworks.h" +#import "MSACUserIdContextPrivate.h" +#import "MSACUtility+File.h" +#import "MSACWrapperCrashesHelper.h" + +@class MSACMockCrashesDelegate; + +static NSString *const kMSACTestAppSecret = @"TestAppSecret"; +static NSString *const kMSACFatal = @"fatal"; +static NSString *const kMSACTypeHandledError = @"handledError"; +static unsigned int kAttachmentsPerCrashReport = 3; + +@interface MSACCrashes () + ++ (void)notifyWithUserConfirmation:(MSACUserConfirmation)userConfirmation; +- (void)startDelayedCrashProcessing; +- (void)startCrashProcessing; +- (void)shouldAlwaysSend; +- (void)emptyLogBufferFiles; +- (void)handleUserConfirmation:(MSACUserConfirmation)userConfirmation; +- (void)applicationWillEnterForeground; +- (void)didReceiveMemoryWarning:(NSNotification *)notification; + +@property(nonatomic) dispatch_group_t bufferFileGroup; + +@property dispatch_source_t memoryPressureSource; + +@end + +@interface MSACCrashesTests : XCTestCase + +@property(nonatomic) MSACCrashes *sut; + +@property(nonatomic) id deviceTrackerMock; + +@property(nonatomic) MSACMockUserDefaults *settingsMock; + +@property(nonatomic) id sessionContextMock; + +@end + +@implementation MSACCrashesTests + +#pragma mark - Housekeeping + +- (void)setUp { + [super setUp]; + self.settingsMock = [MSACMockUserDefaults new]; + self.sut = [MSACCrashes new]; + [MSACDeviceTracker resetSharedInstance]; + self.deviceTrackerMock = OCMClassMock([MSACDeviceTracker class]); + OCMStub([self.deviceTrackerMock sharedInstance]).andReturn(self.deviceTrackerMock); + [MSACSessionContext resetSharedInstance]; + self.sessionContextMock = OCMClassMock([MSACSessionContext class]); + OCMStub([self.sessionContextMock sharedInstance]).andReturn(self.sessionContextMock); +} + +- (void)tearDown { + [super tearDown]; + + // Reset mocked shared instances and stop mocking them. + [self.settingsMock stopMocking]; + [self.deviceTrackerMock stopMocking]; + [self.sessionContextMock stopMocking]; + [MSACDeviceTracker resetSharedInstance]; + [MSACSessionContext resetSharedInstance]; + + // Make sure sessionTracker removes all observers. + [MSACCrashes resetSharedInstance]; + + // Wait for creation of buffers. + dispatch_group_wait(self.sut.bufferFileGroup, DISPATCH_TIME_FOREVER); + + // Delete all files. + [self.sut deleteAllFromCrashesDirectory]; + NSString *logBufferDir = [MSACCrashesUtil logBufferDir]; + [MSACUtility deleteItemForPathComponent:logBufferDir]; +} + +#pragma mark - Tests + +- (void)testMigrateOnInit { + NSString *key = [NSString stringWithFormat:kMSACMockMigrationKey, @"Crashes"]; + XCTAssertNotNil([self.settingsMock objectForKey:key]); +} + +- (void)testNewInstanceWasInitialisedCorrectly { + + // When + // An instance of MSACCrashes is created. + + // Then + assertThat(self.sut, notNilValue()); + assertThat(self.sut.crashFiles, isEmpty()); + assertThat(self.sut.analyzerInProgressFilePathComponent, notNilValue()); + XCTAssertTrue(msACCrashesLogBuffer.size() == ms_crashes_log_buffer_size); + + // Wait for creation of buffers. + dispatch_group_wait(self.sut.bufferFileGroup, DISPATCH_TIME_FOREVER); + NSArray *files = [MSACUtility contentsOfDirectory:self.sut.logBufferPathComponent propertiesForKeys:nil]; + assertThat(files, hasCountOf(ms_crashes_log_buffer_size)); +} + +- (void)testStartingManagerInitializesPLCrashReporter { + + // When + [self.sut startWithChannelGroup:OCMProtocolMock(@protocol(MSACChannelGroupProtocol)) + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // Then + assertThat(self.sut.plCrashReporter, notNilValue()); +} + +- (void)testStartingManagerWritesLastCrashReportToCrashesDir { + + // If + self.sut = OCMPartialMock(self.sut); + OCMStub([self.sut startDelayedCrashProcessing]).andDo(nil); + assertThatBool([MSACCrashesTestUtil copyFixtureCrashReportWithFileName:@"live_report_exception"], isTrue()); + + // When + [self.sut startWithChannelGroup:OCMProtocolMock(@protocol(MSACChannelGroupProtocol)) + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // Then + assertThat(self.sut.crashFiles, hasCountOf(1)); +} + +- (void)testSettingDelegateWorks { + + // When + id delegateMock = OCMProtocolMock(@protocol(MSACCrashesDelegate)); + [MSACCrashes setDelegate:delegateMock]; + + // Then + id strongDelegate = [MSACCrashes sharedInstance].delegate; + XCTAssertNotNil(strongDelegate); + XCTAssertEqual(strongDelegate, delegateMock); +} + +- (void)testDidFailSendingErrorReportIsCalled { + + // If + id delegateMock = OCMProtocolMock(@protocol(MSACCrashesDelegate)); + XCTestExpectation *expectation = [self expectationWithDescription:@"didFailSendingErrorReportCalled"]; + MSACAppleErrorLog *errorLog = OCMClassMock([MSACAppleErrorLog class]); + MSACErrorReport *errorReport = OCMClassMock([MSACErrorReport class]); + id errorLogFormatterMock = OCMClassMock([MSACErrorLogFormatter class]); + OCMStub(ClassMethod([errorLogFormatterMock errorReportFromLog:errorLog])).andReturn(errorReport); + OCMStub([delegateMock crashes:self.sut didFailSendingErrorReport:errorReport withError:nil]).andDo(^(__unused NSInvocation *invocation) { + [expectation fulfill]; + }); + + // When + [self.sut startWithChannelGroup:OCMProtocolMock(@protocol(MSACChannelGroupProtocol)) + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + [self.sut setDelegate:delegateMock]; + id channel = [MSACCrashes sharedInstance].channelUnit; + id log = errorLog; + [self.sut channel:channel didFailSendingLog:log withError:nil]; + + // Then + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testDidSucceedSendingErrorReportIsCalled { + + // If + id delegateMock = OCMProtocolMock(@protocol(MSACCrashesDelegate)); + XCTestExpectation *expectation = [self expectationWithDescription:@"didSucceedSendingErrorReportCalled"]; + MSACAppleErrorLog *errorLog = OCMClassMock([MSACAppleErrorLog class]); + MSACErrorReport *errorReport = OCMClassMock([MSACErrorReport class]); + id errorLogFormatterMock = OCMClassMock([MSACErrorLogFormatter class]); + OCMStub(ClassMethod([errorLogFormatterMock errorReportFromLog:errorLog])).andReturn(errorReport); + OCMStub([delegateMock crashes:self.sut didSucceedSendingErrorReport:errorReport]).andDo(^(__unused NSInvocation *invocation) { + [expectation fulfill]; + }); + + // When + [self.sut startWithChannelGroup:OCMProtocolMock(@protocol(MSACChannelGroupProtocol)) + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + [self.sut setDelegate:delegateMock]; + id channel = [MSACCrashes sharedInstance].channelUnit; + id log = errorLog; + [self.sut channel:channel didSucceedSendingLog:log]; + + // Then + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testWillSendErrorReportIsCalled { + + // If + id delegateMock = OCMProtocolMock(@protocol(MSACCrashesDelegate)); + XCTestExpectation *expectation = [self expectationWithDescription:@"willSendErrorReportCalled"]; + MSACAppleErrorLog *errorLog = OCMClassMock([MSACAppleErrorLog class]); + MSACErrorReport *errorReport = OCMClassMock([MSACErrorReport class]); + id errorLogFormatterMock = OCMClassMock([MSACErrorLogFormatter class]); + OCMStub(ClassMethod([errorLogFormatterMock errorReportFromLog:errorLog])).andReturn(errorReport); + OCMStub([delegateMock crashes:self.sut willSendErrorReport:errorReport]).andDo(^(__unused NSInvocation *invocation) { + [expectation fulfill]; + }); + + // When + [self.sut startWithChannelGroup:OCMProtocolMock(@protocol(MSACChannelGroupProtocol)) + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + [self.sut setDelegate:delegateMock]; + id channel = [MSACCrashes sharedInstance].channelUnit; + id log = errorLog; + [self.sut channel:channel willSendLog:log]; + + // Then + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testCrashHandlerSetupDelegateMethodsAreCalled { + + // If + id delegateMock = OCMProtocolMock(@protocol(MSACCrashHandlerSetupDelegate)); + [MSACWrapperCrashesHelper setCrashHandlerSetupDelegate:delegateMock]; + + // When + [self.sut applyEnabledState:YES]; + + // Then + OCMVerify([delegateMock willSetUpCrashHandlers]); + OCMVerify([delegateMock didSetUpCrashHandlers]); + OCMVerify([delegateMock shouldEnableUncaughtExceptionHandler]); +} + +- (void)testSettingAdditionalHandlers { + + // If + id appCenterMock = OCMClassMock([MSACAppCenter class]); + OCMStub([appCenterMock isDebuggerAttached]).andReturn(NO); + id exceptionHandlerManagerClass = OCMClassMock([MSACCrashesUncaughtCXXExceptionHandlerManager class]); + id applicationForwarderClass = OCMClassMock([MSACApplicationForwarder class]); + + // When + [self.sut applyEnabledState:YES]; + + // Then + OCMVerify([exceptionHandlerManagerClass addCXXExceptionHandler:(MSACCrashesUncaughtCXXExceptionHandler)[OCMArg anyPointer]]); + OCMVerify([applicationForwarderClass registerForwarding]); + + // Clear + [appCenterMock stopMocking]; + [exceptionHandlerManagerClass stopMocking]; + [applicationForwarderClass stopMocking]; +} + +- (void)testSettingUserConfirmationHandler { + + // When + MSACUserConfirmationHandler userConfirmationHandler = ^BOOL(__unused NSArray *_Nonnull errorReports) { + return NO; + }; + [MSACCrashes setUserConfirmationHandler:userConfirmationHandler]; + + // Then + XCTAssertNotNil([MSACCrashes sharedInstance].userConfirmationHandler); + XCTAssertEqual([MSACCrashes sharedInstance].userConfirmationHandler, userConfirmationHandler); +} + +- (void)testCrashesDelegateWithoutImplementations { + + // When + MSACMockCrashesDelegate *delegateMock = OCMPartialMock([MSACMockCrashesDelegate new]); + [MSACCrashes setDelegate:delegateMock]; + + // Then + assertThatBool([[MSACCrashes sharedInstance] shouldProcessErrorReport:nil], isTrue()); +} + +- (void)testProcessCrashes { + + // Wait for creation of buffers to avoid corruption on OCMPartialMock. + dispatch_group_wait(self.sut.bufferFileGroup, DISPATCH_TIME_FOREVER); + + // If + self.sut = OCMPartialMock(self.sut); + OCMStub([self.sut startDelayedCrashProcessing]).andDo(nil); + + // When + assertThatBool([MSACCrashesTestUtil copyFixtureCrashReportWithFileName:@"live_report_exception"], isTrue()); + [self.sut startWithChannelGroup:OCMProtocolMock(@protocol(MSACChannelGroupProtocol)) + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // Then + assertThat(self.sut.crashFiles, hasCountOf(1)); + + // When + OCMStub([self.sut shouldAlwaysSend]).andReturn(YES); + [self.sut startCrashProcessing]; + + // Then + assertThat(self.sut.crashFiles, hasCountOf(0)); + + // When + assertThatBool([MSACCrashesTestUtil copyFixtureCrashReportWithFileName:@"live_report_exception"], isTrue()); + [self.sut startWithChannelGroup:OCMProtocolMock(@protocol(MSACChannelGroupProtocol)) + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // Then + assertThat(self.sut.crashFiles, hasCountOf(1)); + assertThatLong([MSACUtility contentsOfDirectory:self.sut.crashesPathComponent propertiesForKeys:nil].count, equalToLong(1)); + + // When + MSACUserConfirmationHandler userConfirmationHandlerYES = + ^BOOL(__attribute__((unused)) NSArray *_Nonnull errorReports) { + return YES; + }; + + self.sut.userConfirmationHandler = userConfirmationHandlerYES; + [self.sut startWithChannelGroup:OCMProtocolMock(@protocol(MSACChannelGroupProtocol)) + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + [self.sut startCrashProcessing]; + [self.sut notifyWithUserConfirmation:MSACUserConfirmationDontSend]; + self.sut.userConfirmationHandler = nil; + + // Then + assertThat(self.sut.crashFiles, hasCountOf(0)); + assertThatLong([MSACUtility contentsOfDirectory:self.sut.crashesPathComponent propertiesForKeys:nil].count, equalToLong(0)); + + // When + assertThatBool([MSACCrashesTestUtil copyFixtureCrashReportWithFileName:@"live_report_exception"], isTrue()); + [self.sut startWithChannelGroup:OCMProtocolMock(@protocol(MSACChannelGroupProtocol)) + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // Then + assertThat(self.sut.crashFiles, hasCountOf(1)); + assertThatLong([MSACUtility contentsOfDirectory:self.sut.crashesPathComponent propertiesForKeys:nil].count, equalToLong(1)); + + // When + MSACUserConfirmationHandler userConfirmationHandlerNO = ^BOOL(__attribute__((unused)) NSArray *_Nonnull errorReports) { + return NO; + }; + self.sut.userConfirmationHandler = userConfirmationHandlerNO; + [self.sut startWithChannelGroup:OCMProtocolMock(@protocol(MSACChannelGroupProtocol)) + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + [self.sut startCrashProcessing]; + + // Then + assertThat(self.sut.crashFiles, hasCountOf(0)); + assertThatLong([MSACUtility contentsOfDirectory:self.sut.crashesPathComponent propertiesForKeys:nil].count, equalToLong(0)); + OCMVerify([self.deviceTrackerMock clearDevices]); + OCMVerify([self.sessionContextMock clearSessionHistoryAndKeepCurrentSession:YES]); +} + +- (void)testProcessCrashesWithErrorAttachments { + + // Wait for creation of buffers to avoid corruption on OCMPartialMock. + dispatch_group_wait(self.sut.bufferFileGroup, DISPATCH_TIME_FOREVER); + + // If + self.sut = OCMPartialMock(self.sut); + OCMStub([self.sut startDelayedCrashProcessing]).andDo(nil); + + // When + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + assertThatBool([MSACCrashesTestUtil copyFixtureCrashReportWithFileName:@"live_report_exception"], isTrue()); + NSString *validString = @"valid"; + NSData *validData = [validString dataUsingEncoding:NSUTF8StringEncoding]; + NSData *emptyData = [@"" dataUsingEncoding:NSUTF8StringEncoding]; + NSMutableData *hugeData = [[NSMutableData alloc] initWithLength:7 * 1024 * 1024 + 1]; + NSArray *invalidLogs = @[ + [self attachmentWithAttachmentId:nil attachmentData:validData contentType:validString], + [self attachmentWithAttachmentId:@"" attachmentData:validData contentType:validString], + [self attachmentWithAttachmentId:validString attachmentData:nil contentType:validString], + [self attachmentWithAttachmentId:validString attachmentData:emptyData contentType:validString], + [self attachmentWithAttachmentId:validString attachmentData:validData contentType:nil], + [self attachmentWithAttachmentId:validString attachmentData:validData contentType:@""], + [self attachmentWithAttachmentId:validString attachmentData:hugeData contentType:validString] + ]; + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([channelGroupMock addChannelUnitWithConfiguration:[OCMArg checkWithBlock:^BOOL(MSACChannelUnitConfiguration *configuration) { + return [configuration.groupId isEqualToString:@"Crashes"]; + }]]) + .andReturn(channelUnitMock); + for (NSUInteger i = 0; i < invalidLogs.count; i++) { + OCMReject([channelUnitMock enqueueItem:invalidLogs[i] flags:MSACFlagsDefault]); + } + MSACErrorAttachmentLog *validLog = [self attachmentWithAttachmentId:validString attachmentData:validData contentType:validString]; + NSMutableArray *logs = invalidLogs.mutableCopy; + [logs addObject:validLog]; + id crashesDelegateMock = OCMProtocolMock(@protocol(MSACCrashesDelegate)); + OCMStub([crashesDelegateMock attachmentsWithCrashes:OCMOCK_ANY forErrorReport:OCMOCK_ANY]).andReturn(logs); + OCMStub([crashesDelegateMock crashes:OCMOCK_ANY shouldProcessErrorReport:OCMOCK_ANY]).andReturn(YES); + [self.sut startWithChannelGroup:channelGroupMock appSecret:kMSACTestAppSecret transmissionTargetToken:nil fromApplication:YES]; + [self.sut setDelegate:crashesDelegateMock]; + + // Then + OCMExpect([channelUnitMock enqueueItem:validLog flags:MSACFlagsDefault]); + [self.sut startCrashProcessing]; + OCMVerifyAll(channelUnitMock); + OCMVerify([self.deviceTrackerMock clearDevices]); + OCMVerify([self.sessionContextMock clearSessionHistoryAndKeepCurrentSession:YES]); +} + +- (void)testProcessCrashesOnEnterForeground { + + // Wait for creation of buffers to avoid corruption on OCMPartialMock. + dispatch_group_wait(self.sut.bufferFileGroup, DISPATCH_TIME_FOREVER); + + // If + self.sut = OCMPartialMock(self.sut); + OCMStub([self.sut startDelayedCrashProcessing]).andDo(nil); + + // When + assertThatBool([MSACCrashesTestUtil copyFixtureCrashReportWithFileName:@"live_report_exception"], isTrue()); + [self.sut startWithChannelGroup:OCMProtocolMock(@protocol(MSACChannelGroupProtocol)) + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // Then + assertThat(self.sut.crashFiles, hasCountOf(1)); + + // When + [self.sut applicationWillEnterForeground]; + + // Then + OCMVerify([self.sut startDelayedCrashProcessing]); +} + +- (void)testDeleteAllFromCrashesDirectory { + + // If + assertThatBool([MSACCrashesTestUtil copyFixtureCrashReportWithFileName:@"live_report_exception"], isTrue()); + self.sut = OCMPartialMock(self.sut); + OCMStub([self.sut startDelayedCrashProcessing]).andDo(nil); + [self.sut startWithChannelGroup:OCMProtocolMock(@protocol(MSACChannelGroupProtocol)) + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + assertThatBool([MSACCrashesTestUtil copyFixtureCrashReportWithFileName:@"live_report_signal"], isTrue()); + [self.sut startWithChannelGroup:OCMProtocolMock(@protocol(MSACChannelGroupProtocol)) + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + [self.sut deleteAllFromCrashesDirectory]; + + // Then + assertThat(self.sut.crashFiles, hasCountOf(0)); +} + +- (void)testDeleteCrashReportsOnDisabled { + + // If + MSACMockUserDefaults *settings = [MSACMockUserDefaults new]; + [settings setObject:@(YES) forKey:self.sut.isEnabledKey]; + assertThatBool([MSACCrashesTestUtil copyFixtureCrashReportWithFileName:@"live_report_exception"], isTrue()); + self.sut = OCMPartialMock(self.sut); + OCMStub([self.sut startDelayedCrashProcessing]).andDo(nil); + [self.sut startWithChannelGroup:OCMProtocolMock(@protocol(MSACChannelGroupProtocol)) + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + [self.sut setEnabled:NO]; + + // Then + assertThat(self.sut.crashFiles, hasCountOf(0)); + assertThatLong([MSACUtility contentsOfDirectory:self.sut.crashesPathComponent propertiesForKeys:nil].count, equalToLong(0)); + [settings stopMocking]; + OCMVerify([self.deviceTrackerMock clearDevices]); + OCMVerify([self.sessionContextMock clearSessionHistoryAndKeepCurrentSession:YES]); +} + +- (void)testDeleteCrashReportsFromDisabledToEnabled { + + // If + MSACMockUserDefaults *settings = [MSACMockUserDefaults new]; + [settings setObject:@(NO) forKey:self.sut.isEnabledKey]; + assertThatBool([MSACCrashesTestUtil copyFixtureCrashReportWithFileName:@"live_report_exception"], isTrue()); + [self.sut startWithChannelGroup:OCMProtocolMock(@protocol(MSACChannelGroupProtocol)) + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + [self.sut setEnabled:YES]; + + // Then + assertThat(self.sut.crashFiles, hasCountOf(0)); + assertThatLong([MSACUtility contentsOfDirectory:self.sut.crashesPathComponent propertiesForKeys:nil].count, equalToLong(0)); + [settings stopMocking]; +} + +- (void)testSetupLogBufferWorks { + + // If + // Wait for creation of buffers. + dispatch_group_wait(self.sut.bufferFileGroup, DISPATCH_TIME_FOREVER); + + // Then + NSArray *first = [MSACUtility contentsOfDirectory:self.sut.logBufferPathComponent + propertiesForKeys:@[ NSURLNameKey, NSURLFileSizeKey, NSURLIsRegularFileKey ]]; + XCTAssertTrue(first.count == ms_crashes_log_buffer_size); + for (NSURL *path in first) { + unsigned long long fileSize = + [[[NSFileManager defaultManager] attributesOfItemAtPath:([path absoluteString] ?: @"") error:nil] fileSize]; + XCTAssertTrue(fileSize == 0); + } + + // When + [self.sut setupLogBuffer]; + + // Then + NSArray *second = [MSACUtility contentsOfDirectory:self.sut.logBufferPathComponent propertiesForKeys:nil]; + for (NSUInteger i = 0; i < ms_crashes_log_buffer_size; i++) { + XCTAssertTrue([([first[i] absoluteString] ?: @"") isEqualToString:([second[i] absoluteString] ?: @"")]); + } +} + +- (void)testEmptyLogBufferFiles { + + // If + NSString *testName = @"aFilename"; + NSString *dataString = @"someBufferedData"; + NSData *someData = [dataString dataUsingEncoding:NSUTF8StringEncoding]; + NSString *filePath = + [NSString stringWithFormat:@"%@/%@", self.sut.logBufferPathComponent, [testName stringByAppendingString:@".mscrasheslogbuffer"]]; + [MSACUtility createFileAtPathComponent:filePath withData:someData atomically:YES forceOverwrite:YES]; + + // When + BOOL success = [MSACUtility fileExistsForPathComponent:filePath]; + XCTAssertTrue(success); + + // Then + NSData *data = [MSACUtility loadDataForPathComponent:filePath]; + XCTAssertTrue([data length] == 16); + + // When + [self.sut emptyLogBufferFiles]; + + // Then + data = [MSACUtility loadDataForPathComponent:filePath]; + XCTAssertTrue([data length] == 0); +} + +- (void)testBufferIndexIncrement { + + // When + MSACLogWithProperties *log = [MSACLogWithProperties new]; + [self.sut channel:nil didPrepareLog:log internalId:MSAC_UUID_STRING flags:MSACFlagsNormal]; + + // Then + XCTAssertTrue([self crashesLogBufferCount] == 1); +} + +- (void)testBufferIndexOverflow { + + // When + for (int i = 0; i < ms_crashes_log_buffer_size; i++) { + MSACLogWithProperties *log = [MSACLogWithProperties new]; + [self.sut channel:nil didPrepareLog:log internalId:MSAC_UUID_STRING flags:MSACFlagsDefault]; + } + + // Then + XCTAssertTrue([self crashesLogBufferCount] == ms_crashes_log_buffer_size); + + // When + MSACLogWithProperties *log = [MSACLogWithProperties new]; + [self.sut channel:nil didPrepareLog:log internalId:MSAC_UUID_STRING flags:MSACFlagsDefault]; + NSNumberFormatter *timestampFormatter = [[NSNumberFormatter alloc] init]; + timestampFormatter.numberStyle = NSNumberFormatterDecimalStyle; + int indexOfLatestObject = 0; + NSTimeInterval oldestTimestamp = DBL_MAX; + for (auto it = msACCrashesLogBuffer.begin(), end = msACCrashesLogBuffer.end(); it != end; ++it) { + + // Remember the timestamp if the log is older than the previous one or the initial one. + if (oldestTimestamp > it->timestamp) { + oldestTimestamp = it->timestamp; + indexOfLatestObject = static_cast(it - msACCrashesLogBuffer.begin()); + } + } + // Then + XCTAssertTrue([self crashesLogBufferCount] == ms_crashes_log_buffer_size); + XCTAssertTrue(indexOfLatestObject == 1); + + // If + int numberOfLogs = 50; + // When + for (int i = 0; i < numberOfLogs; i++) { + MSACLogWithProperties *aLog = [MSACLogWithProperties new]; + [self.sut channel:nil didPrepareLog:aLog internalId:MSAC_UUID_STRING flags:MSACFlagsDefault]; + } + + indexOfLatestObject = 0; + oldestTimestamp = DBL_MAX; + for (auto it = msACCrashesLogBuffer.begin(), end = msACCrashesLogBuffer.end(); it != end; ++it) { + + // Remember the timestamp if the log is older than the previous one or the initial one. + if (oldestTimestamp > it->timestamp) { + oldestTimestamp = it->timestamp; + indexOfLatestObject = static_cast(it - msACCrashesLogBuffer.begin()); + } + } + + // Then + XCTAssertTrue([self crashesLogBufferCount] == ms_crashes_log_buffer_size); + XCTAssertTrue(indexOfLatestObject == (1 + (numberOfLogs % ms_crashes_log_buffer_size))); +} + +- (void)testBufferIndexOnPersistingLog { + + // When + MSACCommonSchemaLog *commonSchemaLog = [MSACCommonSchemaLog new]; + [commonSchemaLog addTransmissionTargetToken:MSAC_UUID_STRING]; + NSString *uuid1 = MSAC_UUID_STRING; + NSString *uuid2 = MSAC_UUID_STRING; + NSString *uuid3 = MSAC_UUID_STRING; + [self.sut channel:nil didPrepareLog:[MSACLogWithProperties new] internalId:uuid1 flags:MSACFlagsDefault]; + [self.sut channel:nil didPrepareLog:commonSchemaLog internalId:uuid2 flags:MSACFlagsDefault]; + + // Don't buffer event if log is related to crash. + [self.sut channel:nil didPrepareLog:[MSACAppleErrorLog new] internalId:uuid3 flags:MSACFlagsDefault]; + + // Then + assertThatLong([self crashesLogBufferCount], equalToLong(2)); + + // When + [self.sut channel:nil didCompleteEnqueueingLog:nil internalId:uuid3]; + + // Then + assertThatLong([self crashesLogBufferCount], equalToLong(2)); + + // When + [self.sut channel:nil didCompleteEnqueueingLog:nil internalId:uuid2]; + + // Then + assertThatLong([self crashesLogBufferCount], equalToLong(1)); + + // When + [self.sut channel:nil didCompleteEnqueueingLog:nil internalId:uuid1]; + + // Then + assertThatLong([self crashesLogBufferCount], equalToLong(0)); +} + +- (void)testLogBufferSave { + + // Wait for creation of buffers. + dispatch_group_wait(self.sut.bufferFileGroup, DISPATCH_TIME_FOREVER); + + // If + __block NSUInteger numInvocations = 0; + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + OCMStub([channelGroupMock addChannelUnitWithConfiguration:[OCMArg checkWithBlock:^BOOL(MSACChannelUnitConfiguration *configuration) { + return [configuration.groupId isEqualToString:@"CrashesBuffer"]; + }]]) + .andReturn(channelUnitMock); + OCMStub([channelUnitMock enqueueItem:OCMOCK_ANY flags:MSACFlagsDefault]).andDo(^(__unused NSInvocation *invocation) { + numInvocations++; + }); + + // When + MSACCommonSchemaLog *commonSchemaLog = [MSACCommonSchemaLog new]; + [commonSchemaLog addTransmissionTargetToken:MSAC_UUID_STRING]; + NSString *uuid1 = MSAC_UUID_STRING; + NSString *uuid2 = MSAC_UUID_STRING; + NSString *uuid3 = MSAC_UUID_STRING; + [self.sut channel:nil didPrepareLog:[MSACLogWithProperties new] internalId:uuid1 flags:MSACFlagsDefault]; + [self.sut channel:nil didPrepareLog:commonSchemaLog internalId:uuid2 flags:MSACFlagsDefault]; + + // Don't buffer event if log is related to crash. + [self.sut channel:nil didPrepareLog:[MSACAppleErrorLog new] internalId:uuid3 flags:MSACFlagsDefault]; + + // Then + assertThatLong([self crashesLogBufferCount], equalToLong(2)); + + // When + // Save on crash. + ms_save_log_buffer(); + + // Recreate crashes. + [self.sut startWithChannelGroup:channelGroupMock appSecret:kMSACTestAppSecret transmissionTargetToken:nil fromApplication:YES]; + + // Then + XCTAssertEqual(2U, numInvocations); +} + +- (void)testInitializationPriorityCorrect { + XCTAssertTrue([[MSACCrashes sharedInstance] initializationPriority] == MSACInitializationPriorityMax); +} + +// The Mach exception handler is not supported on tvOS. +#if TARGET_OS_TV +- (void)testMachExceptionHandlerDisabledOnTvOS { + + // Then + XCTAssertFalse([[MSACCrashes sharedInstance] isMachExceptionHandlerEnabled]); +} +#else +- (void)testDisableMachExceptionWorks { + + // Then + XCTAssertTrue([[MSACCrashes sharedInstance] isMachExceptionHandlerEnabled]); + + // When + [MSACCrashes disableMachExceptionHandler]; + + // Then + XCTAssertFalse([[MSACCrashes sharedInstance] isMachExceptionHandlerEnabled]); + + // Then + XCTAssertTrue([self.sut isMachExceptionHandlerEnabled]); + + // When + [self.sut setEnableMachExceptionHandler:NO]; + + // Then + XCTAssertFalse([self.sut isMachExceptionHandlerEnabled]); +} + +#endif + +- (void)testAbstractErrorLogSerialization { + MSACAbstractErrorLog *log = [MSACAbstractErrorLog new]; + + // When + NSDictionary *serializedLog = [log serializeToDictionary]; + + // Then + XCTAssertFalse([static_cast(serializedLog[kMSACFatal]) boolValue]); + + // If + log.fatal = NO; + + // When + serializedLog = [log serializeToDictionary]; + + // Then + XCTAssertFalse([static_cast(serializedLog[kMSACFatal]) boolValue]); + + // If + log.fatal = YES; + + // When + serializedLog = [log serializeToDictionary]; + + // Then + XCTAssertTrue([static_cast(serializedLog[kMSACFatal]) boolValue]); +} + +#pragma mark - Automatic Processing Tests + +- (void)testSendOrAwaitWhenAlwaysSendIsTrue { + + // Wait for creation of buffers to avoid corruption on OCMPartialMock. + dispatch_group_wait(self.sut.bufferFileGroup, DISPATCH_TIME_FOREVER); + + // If + self.sut = OCMPartialMock(self.sut); + [self.sut setAutomaticProcessingEnabled:NO]; + OCMStub([self.sut shouldAlwaysSend]).andReturn(YES); + __block NSUInteger numInvocations = 0; + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + OCMStub([channelGroupMock addChannelUnitWithConfiguration:[OCMArg checkWithBlock:^BOOL(MSACChannelUnitConfiguration *configuration) { + return [configuration.groupId isEqualToString:@"Crashes"]; + }]]) + .andReturn(channelUnitMock); + OCMStub([channelUnitMock enqueueItem:[OCMArg isKindOfClass:[MSACLogWithProperties class]] flags:MSACFlagsCritical]) + .andDo(^(NSInvocation *invocation) { + (void)invocation; + numInvocations++; + }); + [self startCrashes:self.sut withReports:YES withChannelGroup:channelGroupMock]; + NSMutableArray *reportIds = [self idListFromReports:[self.sut unprocessedCrashReports]]; + + // When + BOOL alwaysSendVal = [self.sut sendCrashReportsOrAwaitUserConfirmationForFilteredIds:reportIds]; + + // Then + XCTAssertEqual([reportIds count], numInvocations); + XCTAssertTrue(alwaysSendVal); + OCMVerify([self.deviceTrackerMock clearDevices]); + OCMVerify([self.sessionContextMock clearSessionHistoryAndKeepCurrentSession:YES]); +} + +- (void)testSendOrAwaitWhenAlwaysSendIsFalseAndNotifyAlwaysSend { + + // Wait for creation of buffers to avoid corruption on OCMPartialMock. + dispatch_group_wait(self.sut.bufferFileGroup, DISPATCH_TIME_FOREVER); + + // If + self.sut = OCMPartialMock(self.sut); + [self.sut setAutomaticProcessingEnabled:NO]; + OCMStub([self.sut shouldAlwaysSend]).andReturn(NO); + __block NSUInteger numInvocations = 0; + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + OCMStub([channelGroupMock addChannelUnitWithConfiguration:[OCMArg checkWithBlock:^BOOL(MSACChannelUnitConfiguration *configuration) { + return [configuration.groupId isEqualToString:@"Crashes"]; + }]]) + .andReturn(channelUnitMock); + OCMStub([channelUnitMock enqueueItem:[OCMArg isKindOfClass:[MSACLogWithProperties class]] flags:MSACFlagsCritical]) + .andDo(^(NSInvocation *invocation) { + (void)invocation; + numInvocations++; + }); + [self startCrashes:self.sut withReports:YES withChannelGroup:channelGroupMock]; + NSMutableArray *reports = [self idListFromReports:[self.sut unprocessedCrashReports]]; + + // When + BOOL alwaysSendVal = [self.sut sendCrashReportsOrAwaitUserConfirmationForFilteredIds:reports]; + + // Then + XCTAssertEqual(numInvocations, 0U); + XCTAssertFalse(alwaysSendVal); + + // When + [self.sut notifyWithUserConfirmation:MSACUserConfirmationAlways]; + + // Then + XCTAssertEqual([reports count], numInvocations); + OCMVerify([self.deviceTrackerMock clearDevices]); + OCMVerify([self.sessionContextMock clearSessionHistoryAndKeepCurrentSession:YES]); +} + +- (void)testSendOrAwaitWhenAlwaysSendIsFalseAndNotifySend { + + // Wait for creation of buffers to avoid corruption on OCMPartialMock. + dispatch_group_wait(self.sut.bufferFileGroup, DISPATCH_TIME_FOREVER); + + // If + self.sut = OCMPartialMock(self.sut); + [self.sut setAutomaticProcessingEnabled:NO]; + OCMStub([self.sut shouldAlwaysSend]).andReturn(NO); + __block NSUInteger numInvocations = 0; + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + OCMStub([channelGroupMock addChannelUnitWithConfiguration:[OCMArg checkWithBlock:^BOOL(MSACChannelUnitConfiguration *configuration) { + return [configuration.groupId isEqualToString:@"Crashes"]; + }]]) + .andReturn(channelUnitMock); + OCMStub([channelUnitMock enqueueItem:[OCMArg isKindOfClass:[MSACLogWithProperties class]] flags:MSACFlagsCritical]) + .andDo(^(NSInvocation *invocation) { + (void)invocation; + numInvocations++; + }); + [self startCrashes:self.sut withReports:YES withChannelGroup:channelGroupMock]; + NSMutableArray *reportIds = [self idListFromReports:[self.sut unprocessedCrashReports]]; + + // When + BOOL alwaysSendVal = [self.sut sendCrashReportsOrAwaitUserConfirmationForFilteredIds:reportIds]; + + // Then + XCTAssertEqual(0U, numInvocations); + XCTAssertFalse(alwaysSendVal); + + // When + [self.sut notifyWithUserConfirmation:MSACUserConfirmationSend]; + + // Then + XCTAssertEqual([reportIds count], numInvocations); + OCMVerify([self.deviceTrackerMock clearDevices]); + OCMVerify([self.sessionContextMock clearSessionHistoryAndKeepCurrentSession:YES]); +} + +- (void)testSendOrAwaitWhenAlwaysSendIsFalseAndNotifyDontSend { + + // Wait for creation of buffers to avoid corruption on OCMPartialMock. + dispatch_group_wait(self.sut.bufferFileGroup, DISPATCH_TIME_FOREVER); + + // If + self.sut = OCMPartialMock(self.sut); + [self.sut setAutomaticProcessingEnabled:NO]; + [self.sut applyEnabledState:YES]; + OCMStub([self.sut shouldAlwaysSend]).andReturn(NO); + __block int numInvocations = 0; + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + OCMStub([channelGroupMock addChannelUnitWithConfiguration:[OCMArg checkWithBlock:^BOOL(MSACChannelUnitConfiguration *configuration) { + return [configuration.groupId isEqualToString:@"Crashes"]; + }]]) + .andReturn(channelUnitMock); + OCMStub([channelUnitMock enqueueItem:[OCMArg isKindOfClass:[MSACLogWithProperties class]] flags:MSACFlagsCritical]) + .andDo(^(NSInvocation *invocation) { + (void)invocation; + numInvocations++; + }); + NSMutableArray *reportIds = [self idListFromReports:[self.sut unprocessedCrashReports]]; + + // When + BOOL alwaysSendVal = [self.sut sendCrashReportsOrAwaitUserConfirmationForFilteredIds:reportIds]; + [self.sut notifyWithUserConfirmation:MSACUserConfirmationDontSend]; + + // Then + XCTAssertFalse(alwaysSendVal); + XCTAssertEqual(0, numInvocations); + OCMVerify([self.deviceTrackerMock clearDevices]); + OCMVerify([self.sessionContextMock clearSessionHistoryAndKeepCurrentSession:YES]); +} + +- (void)testGetUnprocessedCrashReportsWhenThereAreNone { + + // Wait for creation of buffers to avoid corruption on OCMPartialMock. + dispatch_group_wait(self.sut.bufferFileGroup, DISPATCH_TIME_FOREVER); + + // If + self.sut = OCMPartialMock(self.sut); + OCMStub([self.sut startDelayedCrashProcessing]).andDo(nil); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + [self.sut setAutomaticProcessingEnabled:NO]; + [self startCrashes:self.sut withReports:NO withChannelGroup:channelGroupMock]; + + // When + NSArray *reports = [self.sut unprocessedCrashReports]; + + // Then + XCTAssertEqual([reports count], 0U); +} + +- (void)testSendErrorAttachments { + + // Wait for creation of buffers to avoid corruption on OCMPartialMock. + dispatch_group_wait(self.sut.bufferFileGroup, DISPATCH_TIME_FOREVER); + + // If + self.sut = OCMPartialMock(self.sut); + [self.sut setAutomaticProcessingEnabled:NO]; + MSACErrorReport *report = OCMPartialMock([MSACErrorReport new]); + OCMStub([report incidentIdentifier]).andReturn(@"incidentId"); + __block NSUInteger numInvocations = 0; + __block NSMutableArray *enqueuedAttachments = [[NSMutableArray alloc] init]; + NSMutableArray *attachments = [[NSMutableArray alloc] init]; + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + OCMStub([channelGroupMock addChannelUnitWithConfiguration:[OCMArg checkWithBlock:^BOOL(MSACChannelUnitConfiguration *configuration) { + return [configuration.groupId isEqualToString:@"Crashes"]; + }]]) + .andReturn(channelUnitMock); + OCMStub([channelUnitMock enqueueItem:OCMOCK_ANY flags:MSACFlagsDefault]).andDo(^(NSInvocation *invocation) { + numInvocations++; + MSACErrorAttachmentLog *attachmentLog; + [invocation getArgument:&attachmentLog atIndex:2]; + [enqueuedAttachments addObject:attachmentLog]; + }); + [self startCrashes:self.sut withReports:NO withChannelGroup:channelGroupMock]; + + // When + [attachments addObject:[[MSACErrorAttachmentLog alloc] initWithFilename:@"name" attachmentText:@"text1"]]; + [attachments addObject:[[MSACErrorAttachmentLog alloc] initWithFilename:@"name" attachmentText:@"text2"]]; + [attachments addObject:[[MSACErrorAttachmentLog alloc] initWithFilename:@"name" attachmentText:@"text3"]]; + [self.sut sendErrorAttachments:attachments withIncidentIdentifier:report.incidentIdentifier]; + + // Then + XCTAssertEqual([attachments count], numInvocations); + for (MSACErrorAttachmentLog *log in enqueuedAttachments) { + XCTAssertTrue([attachments containsObject:log]); + } +} + +- (void)testGetUnprocessedCrashReports { + + // Wait for creation of buffers to avoid corruption on OCMPartialMock. + dispatch_group_wait(self.sut.bufferFileGroup, DISPATCH_TIME_FOREVER); + + // If + self.sut = OCMPartialMock(self.sut); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + [self.sut setAutomaticProcessingEnabled:NO]; + NSArray *reports = [self startCrashes:self.sut withReports:YES withChannelGroup:channelGroupMock]; + + // When + NSArray *retrievedReports = [self.sut unprocessedCrashReports]; + + // Then + XCTAssertEqual([reports count], [retrievedReports count]); + for (MSACErrorReport *retrievedReport in retrievedReports) { + BOOL foundReport = NO; + for (MSACErrorReport *report in reports) { + if ([report.incidentIdentifier isEqualToString:retrievedReport.incidentIdentifier]) { + foundReport = YES; + break; + } + } + XCTAssertTrue(foundReport); + } +} + +- (void)testStartingCrashesWithoutAutomaticProcessing { + + // Wait for creation of buffers to avoid corruption on OCMPartialMock. + dispatch_group_wait(self.sut.bufferFileGroup, DISPATCH_TIME_FOREVER); + + // If + self.sut = OCMPartialMock(self.sut); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + [self.sut setAutomaticProcessingEnabled:NO]; + NSArray *reports = [self startCrashes:self.sut withReports:YES withChannelGroup:channelGroupMock]; + + // When + NSArray *retrievedReports = [self.sut unprocessedCrashReports]; + + // Then + XCTAssertEqual([reports count], [retrievedReports count]); + for (MSACErrorReport *retrievedReport in retrievedReports) { + BOOL foundReport = NO; + for (MSACErrorReport *report in reports) { + if ([report.incidentIdentifier isEqualToString:retrievedReport.incidentIdentifier]) { + foundReport = YES; + break; + } + } + XCTAssertTrue(foundReport); + } +} + +- (void)testStartingCrashesWithAutomaticProcessing { + + // Wait for creation of buffers to avoid corruption on OCMPartialMock. + dispatch_group_wait(self.sut.bufferFileGroup, DISPATCH_TIME_FOREVER); + + // If + self.sut = OCMPartialMock(self.sut); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + [self startCrashes:self.sut withReports:YES withChannelGroup:channelGroupMock]; + + // When + NSArray *retrievedReports = [self.sut unprocessedCrashReports]; + + // Then + XCTAssertEqual([retrievedReports count], 0U); +} + +- (void)testErrorOnIncorrectNotifyWithUserConfirmationCall { + + // Wait for creation of buffers to avoid corruption on OCMPartialMock. + dispatch_group_wait(self.sut.bufferFileGroup, DISPATCH_TIME_FOREVER); + + // If + self.sut = OCMPartialMock(self.sut); + OCMStub([self.sut startDelayedCrashProcessing]).andDo(nil); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + [self startCrashes:self.sut withReports:YES withChannelGroup:channelGroupMock]; + + // Then + OCMReject([self.sut handleUserConfirmation:MSACUserConfirmationAlways]); + + // When + [self.sut notifyWithUserConfirmation:MSACUserConfirmationAlways]; +} + +- (void)testCrashesSetCorrectUserIdToLogs { + + // Wait for creation of buffers to avoid corruption on OCMPartialMock. + dispatch_group_wait(self.sut.bufferFileGroup, DISPATCH_TIME_FOREVER); + + // If + __block XCTestExpectation *expectation = [self expectationWithDescription:@"Channel received a log"]; + __block NSString *expectedUserId = @"bob"; + __block NSString *actualUserId; + self.sut = OCMPartialMock(self.sut); + OCMStub([self.sut startDelayedCrashProcessing]).andDo(nil); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + assertThatBool([MSACCrashesTestUtil copyFixtureCrashReportWithFileName:@"live_report_exception"], isTrue()); + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + OCMStub([channelGroupMock addChannelUnitWithConfiguration:[OCMArg checkWithBlock:^BOOL(MSACChannelUnitConfiguration *configuration) { + return [configuration.groupId isEqualToString:@"Crashes"]; + }]]) + .andReturn(channelUnitMock); + OCMStub([channelUnitMock enqueueItem:OCMOCK_ANY flags:MSACFlagsCritical]).andDo(^(NSInvocation *invocation) { + MSACAbstractLog *log; + [invocation getArgument:&log atIndex:2]; + actualUserId = log.userId; + [expectation fulfill]; + }); + [self.sut startWithChannelGroup:channelGroupMock appSecret:kMSACTestAppSecret transmissionTargetToken:nil fromApplication:YES]; + + // Mock history + MSACMockUserDefaults *settings = [MSACMockUserDefaults new]; + __block NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; + __block NSDate *date; + + // When + id dateMock = OCMClassMock([NSDate class]); + OCMStub(ClassMethod([dateMock date])).andDo(^(NSInvocation *invocation) { + // 5 mins ago. + NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; + [dateComponents setYear:2013]; + [dateComponents setMonth:9]; + [dateComponents setDay:25]; + [dateComponents setHour:10]; + [dateComponents setMinute:50]; + [dateComponents setTimeZone:[NSTimeZone timeZoneWithName:@"UTC"]]; + date = [calendar dateFromComponents:dateComponents]; + [invocation setReturnValue:&date]; + }); + [[MSACUserIdContext sharedInstance] setUserId:@"alice"]; + [dateMock stopMocking]; + + [MSACUserIdContext resetSharedInstance]; + dateMock = OCMClassMock([NSDate class]); + OCMStub(ClassMethod([dateMock date])).andDo(^(NSInvocation *invocation) { + // 1 mins ago. + NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; + [dateComponents setYear:2013]; + [dateComponents setMonth:9]; + [dateComponents setDay:25]; + [dateComponents setHour:10]; + [dateComponents setMinute:54]; + [dateComponents setTimeZone:[NSTimeZone timeZoneWithName:@"UTC"]]; + date = [calendar dateFromComponents:dateComponents]; + [invocation setReturnValue:&date]; + }); + [[MSACUserIdContext sharedInstance] setUserId:expectedUserId]; + [dateMock stopMocking]; + + [MSACUserIdContext resetSharedInstance]; + dateMock = OCMClassMock([NSDate class]); + OCMStub(ClassMethod([dateMock date])).andDo(^(NSInvocation *invocation) { + // 5 mins after. + NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; + [dateComponents setYear:2013]; + [dateComponents setMonth:9]; + [dateComponents setDay:25]; + [dateComponents setHour:11]; + [dateComponents setMinute:0]; + [dateComponents setTimeZone:[NSTimeZone timeZoneWithName:@"UTC"]]; + date = [calendar dateFromComponents:dateComponents]; + [invocation setReturnValue:&date]; + }); + [[MSACUserIdContext sharedInstance] setUserId:@"charlie"]; + [dateMock stopMocking]; + + // Process crash. + [self.sut startCrashProcessing]; + + // Then + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *_Nullable error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + + // The fixture's timestamp is 2013-09-25 10:55:49 + XCTAssertEqualObjects(actualUserId, expectedUserId); + }]; + + [settings stopMocking]; +} + +#if !TARGET_OS_OSX && !TARGET_OS_MACCATALYST + +- (void)testMemoryWarningObserverNotExtension { + + // If + id defaultCenterMock = OCMClassMock([NSNotificationCenter class]); + OCMStub([defaultCenterMock defaultCenter]).andReturn(defaultCenterMock); + + // When + [self.sut applyEnabledState:YES]; + + // Then + OCMVerify([defaultCenterMock addObserver:self.sut + selector:[OCMArg anySelector] + name:UIApplicationDidReceiveMemoryWarningNotification + object:nil]); + + // When + [self.sut applyEnabledState:NO]; + + // Then + OCMVerify([defaultCenterMock removeObserver:self.sut]); + + // Clear + [defaultCenterMock stopMocking]; +} + +#endif + +- (void)testMemoryPressureSourceInExtensionAndMacOS { + + // If +#if !TARGET_OS_OSX && !TARGET_OS_MACCATALYST + id bundleMock = OCMClassMock([NSBundle class]); + OCMStub([bundleMock mainBundle]).andReturn(bundleMock); + OCMStub([bundleMock executablePath]).andReturn(@"/Application/Executable/Path.appex/42"); +#endif + + // When + [self.sut applyEnabledState:YES]; + + // Then + XCTAssertNotNil(self.sut.memoryPressureSource); + + // When + [self.sut applyEnabledState:NO]; + + // Then + XCTAssertNil(self.sut.memoryPressureSource); + + // Clear +#if !TARGET_OS_OSX && !TARGET_OS_MACCATALYST + [bundleMock stopMocking]; +#endif +} + +- (void)testDidReceiveMemoryWarning { + + // If + id crashes = OCMPartialMock(self.sut); + OCMStub([crashes startDelayedCrashProcessing]).andDo(nil); + OCMStub([crashes sharedInstance]).andReturn(crashes); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + MSACMockUserDefaults *settings = [MSACMockUserDefaults new]; + + // Then + XCTAssertFalse([MSACCrashes hasReceivedMemoryWarningInLastSession]); + + // When + [crashes didReceiveMemoryWarning:nil]; + + // Then + XCTAssertFalse([MSACCrashes hasReceivedMemoryWarningInLastSession]); + XCTAssertTrue(((NSNumber *)[settings objectForKey:kMSACAppDidReceiveMemoryWarningKey]).boolValue); + + // When + [crashes startWithChannelGroup:channelGroupMock appSecret:kMSACTestAppSecret transmissionTargetToken:nil fromApplication:YES]; + + // Then + XCTAssertTrue([MSACCrashes hasReceivedMemoryWarningInLastSession]); + XCTAssertFalse(((NSNumber *)[settings objectForKey:kMSACAppDidReceiveMemoryWarningKey]).boolValue); + + // Clear + [settings stopMocking]; +} + +#pragma mark Helper + +/** + * Start Crashes (self.sut) with zero or one crash files on disk. + */ +- (NSMutableArray *)startCrashes:(MSACCrashes *)crashes + withReports:(BOOL)startWithReports + withChannelGroup:(id)channelGroup { + NSMutableArray *reports = [NSMutableArray new]; + if (startWithReports) { + for (NSString *fileName in @[ @"live_report_exception" ]) { + XCTAssertTrue([MSACCrashesTestUtil copyFixtureCrashReportWithFileName:fileName]); + NSData *data = [MSACCrashesTestUtil dataOfFixtureCrashReportWithFileName:fileName]; + NSError *error; + PLCrashReport *report = [[PLCrashReport alloc] initWithData:data error:&error]; + [reports addObject:[MSACErrorLogFormatter errorReportFromCrashReport:report]]; + } + } + + XCTestExpectation *expectation = [self expectationWithDescription:@"Start the Crashes module"]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [crashes startWithChannelGroup:channelGroup appSecret:kMSACTestAppSecret transmissionTargetToken:nil fromApplication:YES]; + [expectation fulfill]; + }); + [self waitForExpectationsWithTimeout:1.0 + handler:^(NSError *error) { + if (startWithReports) { + assertThat(crashes.crashFiles, hasCountOf(1)); + } + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; + + return reports; +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-parameter" +- (NSArray *)attachmentsWithCrashes:(MSACCrashes *)crashes forErrorReport:(MSACErrorReport *)errorReport { + id deviceMock = OCMPartialMock([MSACDevice new]); + OCMStub([deviceMock isValid]).andReturn(YES); + + NSMutableArray *logs = [NSMutableArray new]; + for (unsigned int i = 0; i < kAttachmentsPerCrashReport; ++i) { + NSString *text = [NSString stringWithFormat:@"%d", i]; + MSACErrorAttachmentLog *log = [[MSACErrorAttachmentLog alloc] initWithFilename:text attachmentText:text]; + log.timestamp = [NSDate dateWithTimeIntervalSince1970:42]; + log.device = deviceMock; + [logs addObject:log]; + } + return logs; +} +#pragma clang diagnostic pop + +- (NSInteger)crashesLogBufferCount { + NSInteger bufferCount = 0; + for (auto it = msACCrashesLogBuffer.begin(), end = msACCrashesLogBuffer.end(); it != end; ++it) { + if (!it->internalId.empty()) { + bufferCount++; + } + } + return bufferCount; +} + +- (MSACErrorAttachmentLog *)attachmentWithAttachmentId:(NSString *)attachmentId + attachmentData:(NSData *)attachmentData + contentType:(NSString *)contentType { + MSACErrorAttachmentLog *log = [MSACErrorAttachmentLog alloc]; + log.attachmentId = attachmentId; + log.data = attachmentData; + log.contentType = contentType; + return log; +} + +- (NSMutableArray *)idListFromReports:(NSArray *)reports { + NSMutableArray *ids = [NSMutableArray new]; + for (MSACErrorReport *report in reports) { + [ids addObject:report.incidentIdentifier]; + } + return ids; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACCrashesUtilTests.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACCrashesUtilTests.m new file mode 100644 index 0000000000..c48c662355 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACCrashesUtilTests.m @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACCrashesUtil.h" +#import "MSACCrashesUtilPrivate.h" +#import "MSACTestFrameworks.h" +#import "MSACUtility+File.h" + +@interface MSACCrashesUtilTests : XCTestCase + +@property(nonatomic) id bundleMock; + +@end + +@implementation MSACCrashesUtilTests + +#pragma mark - Housekeeping + +- (void)setUp { + [super setUp]; + self.bundleMock = OCMClassMock([NSBundle class]); + OCMStub([self.bundleMock mainBundle]).andReturn(self.bundleMock); + OCMStub([self.bundleMock bundleIdentifier]).andReturn(@"com.test.app"); + [MSACCrashesUtil resetDirectory]; +} + +- (void)tearDown { + [self.bundleMock stopMocking]; + [MSACCrashesUtil resetDirectory]; + [super tearDown]; +} + +#pragma mark - Tests + +- (void)testCreateCrashesDir { + + // If + NSString *expectedDir; +#if TARGET_OS_TV + expectedDir = @"/Library/Caches/com.microsoft.appcenter/crashes"; +#else +#if TARGET_OS_OSX || TARGET_OS_MACCATALYST + expectedDir = [self getPathWithBundleIdentifier:@"/Library/Application%%20Support/%@/com.microsoft.appcenter/crashes"]; +#else + expectedDir = @"/Library/Application%20Support/com.microsoft.appcenter/crashes"; +#endif +#endif + + // When + [MSACCrashesUtil crashesDir]; + + // Then + NSString *crashesDir = [[MSACUtility fullURLForPathComponent:kMSACCrashesDirectory] absoluteString]; + XCTAssertNotNil(crashesDir); + XCTAssertTrue([crashesDir rangeOfString:expectedDir].location != NSNotFound); + BOOL dirExists = [MSACUtility fileExistsForPathComponent:kMSACCrashesDirectory]; + XCTAssertTrue(dirExists); +} + +- (void)testCreateLogBufferDir { + + // If + NSString *expectedDir; +#if TARGET_OS_TV + expectedDir = @"/Library/Caches/com.microsoft.appcenter/crasheslogbuffer"; +#else +#if TARGET_OS_OSX || TARGET_OS_MACCATALYST + expectedDir = [self getPathWithBundleIdentifier:@"/Library/Application%%20Support/%@/com.microsoft.appcenter/crasheslogbuffer"]; +#else + expectedDir = @"/Library/Application%20Support/com.microsoft.appcenter/crasheslogbuffer"; +#endif +#endif + + // When + [MSACCrashesUtil logBufferDir]; + + // Then + NSString *bufferDir = [[MSACUtility fullURLForPathComponent:@"crasheslogbuffer"] absoluteString]; + XCTAssertNotNil(bufferDir); + XCTAssertTrue([bufferDir rangeOfString:expectedDir].location != NSNotFound); + BOOL dirExists = [MSACUtility fileExistsForPathComponent:@"crasheslogbuffer"]; + XCTAssertTrue(dirExists); +} + +- (void)testCreateWrapperExceptionDir { + + // If + NSString *expectedDir; +#if TARGET_OS_TV + expectedDir = @"/Library/Caches/com.microsoft.appcenter/crasheswrapperexceptions"; +#else +#if TARGET_OS_OSX || TARGET_OS_MACCATALYST + expectedDir = [self getPathWithBundleIdentifier:@"/Library/Application%%20Support/%@/com.microsoft.appcenter/crasheswrapperexceptions"]; +#else + expectedDir = @"/Library/Application%20Support/com.microsoft.appcenter/crasheswrapperexceptions"; +#endif +#endif + + // When + [MSACCrashesUtil wrapperExceptionsDir]; + + // Then + NSString *crashesWrapperExceptionDir = [[MSACUtility fullURLForPathComponent:kMSACWrapperExceptionsDirectory] absoluteString]; + XCTAssertNotNil(crashesWrapperExceptionDir); + XCTAssertTrue([crashesWrapperExceptionDir rangeOfString:expectedDir].location != NSNotFound); + BOOL dirExists = [MSACUtility fileExistsForPathComponent:kMSACWrapperExceptionsDirectory]; + XCTAssertTrue(dirExists); +} + +// Before SDK 12.2 (bundled with Xcode 10.*) when running in a unit test bundle the bundle identifier is null. +// 12.2 and after the above bundle identifier is com.apple.dt.xctest.tool. +- (NSString *)getPathWithBundleIdentifier:(NSString *)path { + NSString *bundleId; +#if ((defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_12_2) || \ + (defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_10_14_4)) + bundleId = @"com.apple.dt.xctest.tool"; +#else + bundleId = @"(null)"; +#endif + return [NSString stringWithFormat:path, bundleId]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACErrorAttachmentLogTests.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACErrorAttachmentLogTests.m new file mode 100644 index 0000000000..4fe8aca838 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACErrorAttachmentLogTests.m @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACDevice.h" +#import "MSACErrorAttachmentLog+Utility.h" +#import "MSACErrorAttachmentLogInternal.h" +#import "MSACTestFrameworks.h" +#import "MSACUtility.h" + +@interface MSACErrorAttachmentLogTests : XCTestCase + +@property(nonatomic) MSACErrorAttachmentLog *sut; + +@end + +@implementation MSACErrorAttachmentLogTests + +#pragma mark - Tests + +- (void)setUp { + [super setUp]; + NSString *expectedText = @"Please attach me, I am a nice text."; + NSString *expectedFilename = @"niceFile.txt"; + self.sut = [[MSACErrorAttachmentLog alloc] initWithFilename:expectedFilename attachmentText:expectedText]; +} + +- (void)testInitializationWorks { + + // When + self.sut = [MSACErrorAttachmentLog new]; + + // Then + assertThat(self.sut.attachmentId, notNilValue()); + + // When + NSString *expectedText = @"Please attach me, I am a nice text."; + NSString *expectedFilename = @"niceFile.txt"; + self.sut = [[MSACErrorAttachmentLog alloc] initWithFilename:expectedFilename attachmentText:expectedText]; + + // Then + assertThat(self.sut.attachmentId, notNilValue()); + assertThat(self.sut.filename, is(expectedFilename)); + assertThat(self.sut.data, is([expectedText dataUsingEncoding:NSUTF8StringEncoding])); + assertThat(self.sut.contentType, is(@"text/plain")); + + // When + NSData *expectedData = [@"Please attach meI am a nice " + @"data." dataUsingEncoding:NSUTF8StringEncoding]; + NSString *expectedMimeType = @"text/xml"; + expectedFilename = @"niceFile.xml"; + self.sut = [[MSACErrorAttachmentLog alloc] initWithFilename:expectedFilename attachmentBinary:expectedData contentType:expectedMimeType]; + + // Then + assertThat(self.sut.attachmentId, notNilValue()); + assertThat(self.sut.filename, is(expectedFilename)); + assertThat(self.sut.data, is(expectedData)); + assertThat(self.sut.contentType, is(expectedMimeType)); + + // When + self.sut = [[MSACErrorAttachmentLog alloc] initWithFilename:nil attachmentBinary:expectedData contentType:expectedMimeType]; + + // Then + assertThat(self.sut.attachmentId, notNilValue()); + assertThat(self.sut.filename, nilValue()); + assertThat(self.sut.data, is(expectedData)); + assertThat(self.sut.contentType, is(expectedMimeType)); + + // When + self.sut = [[MSACErrorAttachmentLog alloc] initWithFilename:@"" attachmentBinary:expectedData contentType:expectedMimeType]; + + // Then + assertThat(self.sut.attachmentId, notNilValue()); + assertThat(self.sut.filename, notNilValue()); + assertThat(self.sut.data, is(expectedData)); + assertThat(self.sut.contentType, is(expectedMimeType)); +} + +- (void)testEquals { + + // When + NSString *text = @"Please attach me, I am a nice text."; + NSString *filename = @"niceFile.txt"; + self.sut = [MSACErrorAttachmentLog attachmentWithText:text filename:filename]; + MSACErrorAttachmentLog *other1 = [MSACErrorAttachmentLog attachmentWithText:@"Please attach me, I am a nice text." + filename:@"niceFile.txt"]; + other1.attachmentId = self.sut.attachmentId; + + // Then + assertThat(self.sut, is(other1)); + + // When + NSData *data = [@"Please attach meI am a nice " + @"data." dataUsingEncoding:NSUTF8StringEncoding]; + NSString *mimeType = @"text/xml"; + filename = @"niceFile.xml"; + self.sut = [MSACErrorAttachmentLog attachmentWithBinary:data filename:filename contentType:mimeType]; + MSACErrorAttachmentLog *other2 = + [MSACErrorAttachmentLog attachmentWithBinary:[@"Please attach " + @"meI am a nice " + @"data." dataUsingEncoding:NSUTF8StringEncoding] + filename:@"niceFile.xml" + contentType:@"text/xml"]; + other2.attachmentId = self.sut.attachmentId; + + // Then + assertThat(self.sut, is(other2)); + assertThat(other1, isNot(other2)); + + // When + NSURL *whateverOtherObject = [NSURL new]; + + // Then + assertThat(self.sut, isNot(whateverOtherObject)); +} + +- (void)testIsValid { + + // If + NSString *text = @"Please attach me, I am a nice text."; + NSString *filename = @"niceFile.txt"; + + // When + self.sut = [MSACErrorAttachmentLog attachmentWithText:text filename:filename]; + [self setDummyParentProperties:self.sut]; + self.sut.errorId = MSAC_UUID_STRING; + BOOL validity = [self.sut isValid]; + + // Then + assertThatBool(validity, isTrue()); + + // When + self.sut = [MSACErrorAttachmentLog attachmentWithText:[text copy] filename:[filename copy]]; + [self setDummyParentProperties:self.sut]; + self.sut.errorId = MSAC_UUID_STRING; + self.sut.attachmentId = nil; + validity = [self.sut isValid]; + + // Then + assertThatBool(validity, isFalse()); + + // When + self.sut = [MSACErrorAttachmentLog attachmentWithText:[text copy] filename:[filename copy]]; + [self setDummyParentProperties:self.sut]; + self.sut.errorId = MSAC_UUID_STRING; + self.sut.data = nil; + validity = [self.sut isValid]; + + // Then + assertThatBool(validity, isFalse()); + + // When + self.sut = [MSACErrorAttachmentLog attachmentWithText:[text copy] filename:[filename copy]]; + [self setDummyParentProperties:self.sut]; + self.sut.errorId = MSAC_UUID_STRING; + self.sut.contentType = nil; + validity = [self.sut isValid]; + + // Then + assertThatBool(validity, isFalse()); +} + +- (void)testSerializingToDictionary { + + // When + NSMutableDictionary *actual = [self.sut serializeToDictionary]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual[@"fileName"], equalTo(self.sut.filename)); + assertThat(actual[@"data"], equalTo([self.sut.data base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithCarriageReturn])); + assertThat(actual[@"contentType"], equalTo(self.sut.contentType)); +} + +- (void)testNSCodingSerializationAndDeserialization { + + // When + NSData *serializedEvent = [MSACUtility archiveKeyedData:self.sut]; + MSACErrorAttachmentLog *actual = [MSACUtility unarchiveKeyedData:serializedEvent]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual, instanceOf([self.sut class])); + assertThat(actual, equalTo(self.sut)); +} + +#pragma mark - Utility + +- (void)setDummyParentProperties:(MSACErrorAttachmentLog *)attachment { + attachment.timestamp = [NSDate dateWithTimeIntervalSince1970:42]; + attachment.sid = MSAC_UUID_STRING; + attachment.device = OCMClassMock([MSACDevice class]); + OCMStub([attachment.device isValid]).andReturn(YES); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACErrorLogFormatterTests.mm b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACErrorLogFormatterTests.mm new file mode 100644 index 0000000000..35586301c8 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACErrorLogFormatterTests.mm @@ -0,0 +1,610 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSACAppCenterInternal.h" +#import "MSACAppleErrorLog.h" +#import "MSACCrashReporter.h" +#import "MSACCrashesInternal.h" +#import "MSACCrashesPrivate.h" +#import "MSACCrashesTestUtil.h" +#import "MSACDeviceTrackerPrivate.h" +#import "MSACErrorLogFormatterPrivate.h" +#import "MSACException.h" +#import "MSACMockUserDefaults.h" +#import "MSACTestFrameworks.h" +#import "MSACThread.h" +#import "MSACWrapperSdkInternal.h" + +static NSString *kFixture = @"fixtureName"; +static NSString *kThreadNumber = @"crashedThreadNumber"; +static NSString *kFramesCount = @"crashedThreadStackFrames"; +static NSString *kBinariesCount = @"binariesCount"; + +static NSArray *kMacOSCrashReportsParameters = @[ + @{kThreadNumber : @0, kFramesCount : @21, kBinariesCount : @10, kFixture : @"macOS_report_abort"}, + @{kThreadNumber : @0, kFramesCount : @19, kBinariesCount : @9, kFixture : @"macOS_report_builtin_trap"}, + @{kThreadNumber : @0, kFramesCount : @30, kBinariesCount : @11, kFixture : @"macOS_report_corrupt_malloc_internal_info"}, + @{kThreadNumber : @0, kFramesCount : @21, kBinariesCount : @10, kFixture : @"macOS_report_corrupt_objc_runtime_structure"}, + @{kThreadNumber : @0, kFramesCount : @19, kBinariesCount : @9, kFixture : @"macOS_report_dereference_bad_pointer"}, + @{kThreadNumber : @0, kFramesCount : @19, kBinariesCount : @9, kFixture : @"macOS_report_dereference_null_pointer"}, + @{kThreadNumber : @0, kFramesCount : @21, kBinariesCount : @9, kFixture : @"macOS_report_dwarf_unwinding"}, + @{kThreadNumber : @0, kFramesCount : @19, kBinariesCount : @9, kFixture : @"macOS_report_execute_privileged_instruction"}, + @{kThreadNumber : @0, kFramesCount : @19, kBinariesCount : @9, kFixture : @"macOS_report_execute_undefined_instruction"}, + @{kThreadNumber : @0, kFramesCount : @20, kBinariesCount : @9, kFixture : @"macOS_report_jump_into_nx_page"}, + @{kThreadNumber : @0, kFramesCount : @26, kBinariesCount : @13, kFixture : @"macOS_report_objc_access_non_object_as_object"}, + @{kThreadNumber : @0, kFramesCount : @20, kBinariesCount : @12, kFixture : @"macOS_report_objc_crash_inside_msgsend"}, + @{kThreadNumber : @0, kFramesCount : @21, kBinariesCount : @12, kFixture : @"macOS_report_objc_message_released_object"}, + @{kThreadNumber : @0, kFramesCount : @19, kBinariesCount : @11, kFixture : @"macOS_report_overwrite_link_register"}, + @{kThreadNumber : @0, kFramesCount : @21, kBinariesCount : @10, kFixture : @"macOS_report_pthread_lock"}, + @{kThreadNumber : @0, kFramesCount : @1, kBinariesCount : @9, kFixture : @"macOS_report_smash_the_bottom_of_the_stack"}, + @{kThreadNumber : @0, kFramesCount : @1, kBinariesCount : @10, kFixture : @"macOS_report_smash_the_top_of_the_stack"}, + @{kThreadNumber : @0, kFramesCount : @512, kBinariesCount : @8, kFixture : @"macOS_report_stack_overflow"}, + @{kThreadNumber : @0, kFramesCount : @19, kBinariesCount : @9, kFixture : @"macOS_report_swift"}, + @{kThreadNumber : @0, kFramesCount : @19, kBinariesCount : @13, kFixture : @"macOS_report_throw_cpp_exception"}, + @{kThreadNumber : @0, kFramesCount : @19, kBinariesCount : @9, kFixture : @"macOS_report_write_to_readonly_page"} +]; + +@interface MSACErrorLogFormatter () + ++ (NSString *)selectorForRegisterWithName:(NSString *)regName ofThread:(PLCrashReportThreadInfo *)thread report:(PLCrashReport *)report; + +@end + +@interface MSACErrorLogFormatterTests : XCTestCase + +@property(nonatomic) id deviceMock; +@property(nonatomic) id deviceTrackerMock; + +@end + +@implementation MSACErrorLogFormatterTests + +- (void)setUp { + [MSACDeviceTracker resetSharedInstance]; + self.deviceMock = OCMPartialMock([MSACDevice new]); + OCMStub([self.deviceMock isValid]).andReturn(YES); + self.deviceTrackerMock = OCMClassMock([MSACDeviceTracker class]); + OCMStub([self.deviceTrackerMock sharedInstance]).andReturn(self.deviceTrackerMock); + OCMStub([self.deviceTrackerMock device]).andReturn(self.deviceMock); + OCMStub([self.deviceTrackerMock deviceForTimestamp:OCMOCK_ANY]).andReturn(self.deviceMock); +} + +- (void)tearDown { + [self.deviceMock stopMocking]; + [self.deviceTrackerMock stopMocking]; + [MSACDeviceTracker resetSharedInstance]; +} + +- (void)testCreateErrorReport { + NSData *crashData = [MSACCrashesTestUtil dataOfFixtureCrashReportWithFileName:@"live_report_signal"]; + XCTAssertNotNil(crashData); + + MSACMockUserDefaults *defaults = [MSACMockUserDefaults new]; + NSError *error = nil; + PLCrashReport *crashReport = [[PLCrashReport alloc] initWithData:crashData error:&error]; + + MSACErrorReport *errorReport = [MSACErrorLogFormatter errorReportFromCrashReport:crashReport]; + XCTAssertNotNil(errorReport); + XCTAssertNotNil(errorReport.incidentIdentifier); + assertThat(errorReport.reporterKey, equalTo([[MSACAppCenter installId] UUIDString])); + XCTAssertEqual(errorReport.signal, crashReport.signalInfo.name); + XCTAssertEqual(errorReport.exceptionName, nil); + XCTAssertEqual(errorReport.exceptionReason, nil); + + // FIXME: PLCrashReporter doesn't support millisecond precision, here is a workaround to fill 999 for its millisecond. + XCTAssertEqual([errorReport.appErrorTime timeIntervalSince1970], [crashReport.systemInfo.timestamp timeIntervalSince1970] + 0.999); + assertThat(errorReport.appStartTime, equalTo(crashReport.processInfo.processStartTime)); + + XCTAssertEqualObjects(errorReport.device, self.deviceMock); + XCTAssertEqual(errorReport.appProcessIdentifier, crashReport.processInfo.processID); + + crashData = [MSACCrashesTestUtil dataOfFixtureCrashReportWithFileName:@"live_report_exception"]; + XCTAssertNotNil(crashData); + error = nil; + + crashReport = [[PLCrashReport alloc] initWithData:crashData error:&error]; + errorReport = [MSACErrorLogFormatter errorReportFromCrashReport:crashReport]; + XCTAssertNotNil(errorReport); + XCTAssertNotNil(errorReport.incidentIdentifier); + assertThat(errorReport.reporterKey, equalTo([[MSACAppCenter installId] UUIDString])); + XCTAssertEqual(errorReport.signal, crashReport.signalInfo.name); + assertThat(errorReport.exceptionName, equalTo(crashReport.exceptionInfo.exceptionName)); + assertThat(errorReport.exceptionReason, equalTo(crashReport.exceptionInfo.exceptionReason)); + + // FIXME: PLCrashReporter doesn't support millisecond precision, here is a workaround to fill 999 for its millisecond. + XCTAssertEqual([errorReport.appErrorTime timeIntervalSince1970], [crashReport.systemInfo.timestamp timeIntervalSince1970] + 0.999); + assertThat(errorReport.appStartTime, equalTo(crashReport.processInfo.processStartTime)); + XCTAssertEqualObjects(errorReport.device, self.deviceMock); + XCTAssertEqual(errorReport.appProcessIdentifier, crashReport.processInfo.processID); + [defaults stopMocking]; +} + +- (void)testErrorIdFromCrashReport { + NSData *crashData = [MSACCrashesTestUtil dataOfFixtureCrashReportWithFileName:@"live_report_signal"]; + XCTAssertNotNil(crashData); + + NSError *error = nil; + PLCrashReport *report = [[PLCrashReport alloc] initWithData:crashData error:&error]; + + NSString *expected = (__bridge NSString *)CFUUIDCreateString(NULL, report.uuidRef); + NSString *actual = [MSACErrorLogFormatter errorIdForCrashReport:report]; + assertThat(actual, equalTo(expected)); +} + +- (void)testCrashProbeReports { + + // Crash with _pthread_list_lock held + [self assertIsCrashProbeReportValidConverted:@"live_report_pthread_lock"]; + + // Throw C++ exception + [self assertIsCrashProbeReportValidConverted:@"live_report_cpp_exception"]; + + // Throw Objective-C exception + [self assertIsCrashProbeReportValidConverted:@"live_report_objc_exception"]; + + // Crash inside objc_msgSend() + [self assertIsCrashProbeReportValidConverted:@"live_report_objc_msgsend"]; + + // Message a released object + [self assertIsCrashProbeReportValidConverted:@"live_report_objc_released"]; + + // Write to a read-only page + [self assertIsCrashProbeReportValidConverted:@"live_report_write_readonly"]; + + // Execute an undefined instruction + [self assertIsCrashProbeReportValidConverted:@"live_report_undefined_instr"]; + + // Dereference a NULL pointer + [self assertIsCrashProbeReportValidConverted:@"live_report_null_ptr"]; + + // Dereference a bad pointer + [self assertIsCrashProbeReportValidConverted:@"live_report_bad_ptr"]; + + // Jump into an NX page + [self assertIsCrashProbeReportValidConverted:@"live_report_jump_into_nx"]; + + // Call __builtin_trap() + [self assertIsCrashProbeReportValidConverted:@"live_report_call_trap"]; + + // Call abort() + [self assertIsCrashProbeReportValidConverted:@"live_report_call_abort"]; + + // Corrupt the Objective-C runtime's structures + [self assertIsCrashProbeReportValidConverted:@"live_report_corrupt_objc"]; + + // Overwrite link register, then crash + [self assertIsCrashProbeReportValidConverted:@"live_report_overwrite_link"]; + + // Smash the bottom of the stack + [self assertIsCrashProbeReportValidConverted:@"live_report_smash_bottom"]; + + // Smash the top of the stack + [self assertIsCrashProbeReportValidConverted:@"live_report_smash_top"]; + + // Swift + [self assertIsCrashProbeReportValidConverted:@"live_report_swift_crash"]; +} + +- (void)testProcessIdAndExceptionForObjectiveCExceptionCrash { + NSData *crashData = [MSACCrashesTestUtil dataOfFixtureCrashReportWithFileName:@"live_report_exception"]; + XCTAssertNotNil(crashData); + NSError *error = nil; + PLCrashReport *report = [[PLCrashReport alloc] initWithData:crashData error:&error]; + PLCrashReportExceptionInfo *plExceptionInfo = report.exceptionInfo; + MSACAppleErrorLog *errorLog = [MSACErrorLogFormatter errorLogFromCrashReport:report]; + + PLCrashReportThreadInfo *crashedThread = [MSACErrorLogFormatter findCrashedThreadInReport:report]; + + for (MSACThread *thread in errorLog.threads) { + if ([thread.threadId isEqualToNumber:@(crashedThread.threadNumber)]) { + MSACException *exception = thread.exception; + XCTAssertNotNil(exception); + XCTAssertEqual(exception.message, plExceptionInfo.exceptionReason); + XCTAssertEqual(exception.type, plExceptionInfo.exceptionName); + } else { + XCTAssertNil(thread.exception); + } + } +} + +- (void)testSelectorForRegisterWithName { + + // If + NSData *crashData = [MSACCrashesTestUtil dataOfFixtureCrashReportWithFileName:@"live_report_exception"]; + XCTAssertNotNil(crashData); + + // When + NSError *error = nil; + PLCrashReport *report = [[PLCrashReport alloc] initWithData:crashData error:&error]; + PLCrashReportThreadInfo *crashedThread = [MSACErrorLogFormatter findCrashedThreadInReport:report]; + PLCrashReportRegisterInfo *reg = crashedThread.registers[0]; + [MSACErrorLogFormatter selectorForRegisterWithName:reg.registerName ofThread:crashedThread report:report]; + + // Selector may not be found here, but we are sure that its operation will not lead to an application crash + // XCTAssertNotNil(foundSelector); +} + +- (void)testAddProcessInfoAndApplicationPath { + + // If + NSData *crashData = [MSACCrashesTestUtil dataOfFixtureCrashReportWithFileName:@"live_report_exception"]; + XCTAssertNotNil(crashData); + + // When + NSError *error = nil; + PLCrashReport *report = [[PLCrashReport alloc] initWithData:crashData error:&error]; + MSACAppleErrorLog *actual = [MSACAppleErrorLog new]; + actual = [MSACErrorLogFormatter addProcessInfoAndApplicationPathTo:actual fromCrashReport:report]; + + // Then + assertThat(actual.processId, equalTo(@(report.processInfo.processID))); + XCTAssertEqual(actual.processName, report.processInfo.processName); + XCTAssertNotNil(actual.applicationPath); + + /* + * Not using the report.processInfo.processPath directly to compare. + * The path will be anonymized in the Simulator for iOS. + * The path will be exactly same as the one in the fixture for macOS. + * To cover both scenario, it will be checking with endsWith instead of equalTo. + */ + assertThat(actual.applicationPath, endsWith(@"/Library/Application Support/iPhone Simulator/7.0/Applications" + @"/E196971A-6809-48AF-BB06-FD67014A35B2/HockeySDK-iOSDemo.app/HockeySDK-iOSDemo")); + + XCTAssertEqual(actual.parentProcessName, report.processInfo.parentProcessName); + assertThat(actual.parentProcessId, equalTo(@(report.processInfo.parentProcessID))); +} + +- (void)testCreateErrorLogForException { + NSData *crashData = [MSACCrashesTestUtil dataOfFixtureCrashReportWithFileName:@"live_report_exception"]; + XCTAssertNotNil(crashData); + + NSError *error = nil; + PLCrashReport *crashReport = [[PLCrashReport alloc] initWithData:crashData error:&error]; + + MSACAppleErrorLog *errorLog = [MSACErrorLogFormatter errorLogFromCrashReport:crashReport]; + + MSACException *lastExceptionStackTrace = nil; + + for (MSACThread *thread in errorLog.threads) { + if (thread.exception) { + lastExceptionStackTrace = thread.exception; + break; + } + } + + XCTAssertNotNil(errorLog); + XCTAssertNotNil(lastExceptionStackTrace); +} + +- (void)testAnonymizedPathWorks { + NSString *testPath = @"/var/containers/Bundle/Application/2A0B0E6F-0BF2-419D-A699-FCDF8ADECD8C/Puppet.app/Puppet"; + NSString *expected = testPath; + NSString *actual = [MSACErrorLogFormatter anonymizedPathFromPath:testPath]; + assertThat(actual, equalTo(expected)); + + testPath = @"/Users/someone/Library/Developer/CoreSimulator/Devices/B8321AD0-C30B-41BD-BA54-5A7759CEC4CD/data/" + @"Containers/Bundle/Application/8CC7B5B5-7841-45C4-BAC2-6AA1B944A5E1/Puppet.app/Puppet"; + expected = @"/Users/USER/Library/Developer/CoreSimulator/Devices/B8321AD0-C30B-41BD-BA54-5A7759CEC4CD/data/" + @"Containers/Bundle/Application/8CC7B5B5-7841-45C4-BAC2-6AA1B944A5E1/Puppet.app/Puppet"; + actual = [MSACErrorLogFormatter anonymizedPathFromPath:testPath]; + assertThat(actual, equalTo(expected)); + XCTAssertFalse([actual hasPrefix:@"/Users/someone"]); + XCTAssertTrue([actual hasPrefix:@"/Users/USER/"]); +} + +- (void)testOSXImages { + NSString *processPath = nil; + NSString *appBundlePath = nil; + + appBundlePath = @"/Applications/MyTestApp.App"; + + // Test with default OS X app path + processPath = [appBundlePath stringByAppendingString:@"/Contents/MacOS/MyApp"]; + [self testOSXNonAppSpecificImagesForProcessPath:processPath]; + [self assertIsOtherWithImagePath:processPath processPath:nil]; + [self assertIsOtherWithImagePath:nil processPath:processPath]; + [self assertIsAppBinaryWithImagePath:processPath processPath:processPath]; + + // Test with OS X LoginItems app helper path + processPath = [appBundlePath stringByAppendingString:@"/Contents/Library/LoginItems/net.hockeyapp.helper.app/Contents/MacOS/Helper"]; + [self testOSXNonAppSpecificImagesForProcessPath:processPath]; + [self assertIsOtherWithImagePath:processPath processPath:nil]; + [self assertIsOtherWithImagePath:nil processPath:processPath]; + [self assertIsAppBinaryWithImagePath:processPath processPath:processPath]; + + // Test with OS X app in Resources folder + processPath = @"/Applications/MyTestApp.App/Contents/Resources/Helper"; + [self testOSXNonAppSpecificImagesForProcessPath:processPath]; + [self assertIsOtherWithImagePath:processPath processPath:nil]; + [self assertIsOtherWithImagePath:nil processPath:processPath]; + [self assertIsAppBinaryWithImagePath:processPath processPath:processPath]; +} + +- (void)testiOSImages { + NSString *processPath = nil; + NSString *appBundlePath = nil; + + appBundlePath = @"/private/var/mobile/Containers/Bundle/Application/9107B4E2-CD8C-486E-A3B2-82A5B818F2A0/MyApp.app"; + + // Test with iOS App + processPath = [appBundlePath stringByAppendingString:@"/MyApp"]; + [self testiOSNonAppSpecificImagesForProcessPath:processPath]; + [self assertIsOtherWithImagePath:processPath processPath:nil]; + [self assertIsOtherWithImagePath:nil processPath:processPath]; + [self assertIsAppBinaryWithImagePath:processPath processPath:processPath]; + [self testiOSAppFrameworkAtProcessPath:processPath appBundlePath:appBundlePath]; + + // Test with iOS App Extension + processPath = [appBundlePath stringByAppendingString:@"/Plugins/MyAppExtension.appex/MyAppExtension"]; + [self testiOSNonAppSpecificImagesForProcessPath:processPath]; + [self assertIsOtherWithImagePath:processPath processPath:nil]; + [self assertIsOtherWithImagePath:nil processPath:processPath]; + [self assertIsAppBinaryWithImagePath:processPath processPath:processPath]; + [self testiOSAppFrameworkAtProcessPath:processPath appBundlePath:appBundlePath]; +} + +#pragma mark - Helpers + +- (void)testOSXNonAppSpecificImagesForProcessPath:(NSString *)processPath { + + // system test paths + NSMutableArray *nonAppSpecificImagePaths = [NSMutableArray new]; + + // OS X frameworks + [nonAppSpecificImagePaths addObject:@"cl_kernels"]; + [nonAppSpecificImagePaths addObject:@""]; + [nonAppSpecificImagePaths addObject:@"???"]; + [nonAppSpecificImagePaths addObject:@"/System/Library/Frameworks/CFNetwork.framework/Versions/A/CFNetwork"]; + [nonAppSpecificImagePaths addObject:@"/usr/lib/system/libsystem_platform.dylib"]; + [nonAppSpecificImagePaths + addObject:@"/System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/vecLib"]; + [nonAppSpecificImagePaths addObject:@"/System/Library/PrivateFrameworks/Sharing.framework/Versions/A/Sharing"]; + [nonAppSpecificImagePaths addObject:@"/usr/lib/libbsm.0.dylib"]; + + for (NSString *imagePath in nonAppSpecificImagePaths) { + [self assertIsOtherWithImagePath:imagePath processPath:processPath]; + } +} + +- (void)testiOSAppFrameworkAtProcessPath:(NSString *)processPath appBundlePath:(NSString *)appBundlePath { + NSString *frameworkPath = [appBundlePath stringByAppendingString:@"/Frameworks/MyFrameworkLib.framework/MyFrameworkLib"]; + [self assertIsAppFrameworkWithFrameworkPath:frameworkPath processPath:processPath]; + + frameworkPath = [appBundlePath stringByAppendingString:@"/Frameworks/libSwiftMyLib.framework/libSwiftMyLib"]; + [self assertIsAppFrameworkWithFrameworkPath:frameworkPath processPath:processPath]; + + NSMutableArray *swiftFrameworkPaths = [NSMutableArray new]; + [swiftFrameworkPaths addObject:[appBundlePath stringByAppendingString:@"/Frameworks/libswiftCore.dylib"]]; + [swiftFrameworkPaths addObject:[appBundlePath stringByAppendingString:@"/Frameworks/libswiftDarwin.dylib"]]; + [swiftFrameworkPaths addObject:[appBundlePath stringByAppendingString:@"/Frameworks/libswiftDispatch.dylib"]]; + [swiftFrameworkPaths addObject:[appBundlePath stringByAppendingString:@"/Frameworks/libswiftFoundation.dylib"]]; + [swiftFrameworkPaths addObject:[appBundlePath stringByAppendingString:@"/Frameworks/libswiftObjectiveC.dylib"]]; + [swiftFrameworkPaths addObject:[appBundlePath stringByAppendingString:@"/Frameworks/libswiftSecurity.dylib"]]; + [swiftFrameworkPaths addObject:[appBundlePath stringByAppendingString:@"/Frameworks/libswiftCoreGraphics.dylib"]]; + + for (NSString *swiftFrameworkPath in swiftFrameworkPaths) { + [self assertIsSwiftFrameworkWithFrameworkPath:swiftFrameworkPath processPath:processPath]; + } +} + +- (void)testiOSNonAppSpecificImagesForProcessPath:(NSString *)processPath { + + // system test paths + NSMutableArray *nonAppSpecificImagePaths = [NSMutableArray new]; + + // iOS frameworks + [nonAppSpecificImagePaths + addObject:@"/System/Library/AccessibilityBundles/AccessibilitySettingsLoader.bundle/AccessibilitySettingsLoader"]; + [nonAppSpecificImagePaths addObject:@"/System/Library/Frameworks/AVFoundation.framework/AVFoundation"]; + [nonAppSpecificImagePaths addObject:@"/System/Library/Frameworks/AVFoundation.framework/libAVFAudio.dylib"]; + [nonAppSpecificImagePaths addObject:@"/System/Library/PrivateFrameworks/AOSNotification.framework/AOSNotification"]; + [nonAppSpecificImagePaths addObject:@"/System/Library/PrivateFrameworks/Accessibility.framework/Frameworks/" + @"AccessibilityUI.framework/AccessibilityUI"]; + [nonAppSpecificImagePaths addObject:@"/System/Library/PrivateFrameworks/Accessibility.framework/Frameworks/" + @"AccessibilityUIUtilities.framework/AccessibilityUIUtilities"]; + [nonAppSpecificImagePaths addObject:@"/usr/lib/libAXSafeCategoryBundle.dylib"]; + [nonAppSpecificImagePaths addObject:@"/usr/lib/libAXSpeechManager.dylib"]; + [nonAppSpecificImagePaths addObject:@"/usr/lib/libAccessibility.dylib"]; + [nonAppSpecificImagePaths addObject:@"/usr/lib/system/libcache.dylib"]; + [nonAppSpecificImagePaths addObject:@"/usr/lib/system/libcommonCrypto.dylib"]; + [nonAppSpecificImagePaths addObject:@"/usr/lib/system/libcompiler_rt.dylib"]; + + // iOS Jailbreak libraries + [nonAppSpecificImagePaths addObject:@"/Library/MobileSubstrate/MobileSubstrate.dylib"]; + [nonAppSpecificImagePaths addObject:@"/Library/MobileSubstrate/DynamicLibraries/WeeLoader.dylib"]; + [nonAppSpecificImagePaths addObject:@"/Library/Frameworks/CydiaSubstrate.framework/Libraries/SubstrateLoader.dylib"]; + [nonAppSpecificImagePaths addObject:@"/Library/Frameworks/CydiaSubstrate.framework/CydiaSubstrate"]; + [nonAppSpecificImagePaths addObject:@"/Library/MobileSubstrate/DynamicLibraries/WinterBoard.dylib"]; + + for (NSString *imagePath in nonAppSpecificImagePaths) { + [self assertIsOtherWithImagePath:imagePath processPath:processPath]; + } +} + +- (void)assertIsAppFrameworkWithFrameworkPath:(NSString *)frameworkPath processPath:(NSString *)processPath { + MSACBinaryImageType imageType = [MSACErrorLogFormatter imageTypeForImagePath:frameworkPath processPath:processPath]; + XCTAssertEqual(imageType, MSACBinaryImageTypeAppFramework, @"Test framework %@ with process %@", frameworkPath, processPath); +} + +- (void)assertIsAppBinaryWithImagePath:(NSString *)imagePath processPath:(NSString *)processPath { + MSACBinaryImageType imageType = [MSACErrorLogFormatter imageTypeForImagePath:imagePath processPath:processPath]; + XCTAssertEqual(imageType, MSACBinaryImageTypeAppBinary, @"Test app %@ with process %@", imagePath, processPath); +} + +- (void)assertIsSwiftFrameworkWithFrameworkPath:(NSString *)swiftFrameworkPath processPath:(NSString *)processPath { + MSACBinaryImageType imageType = [MSACErrorLogFormatter imageTypeForImagePath:swiftFrameworkPath processPath:processPath]; + XCTAssertEqual(imageType, MSACBinaryImageTypeOther, @"Test swift image %@ with process %@", swiftFrameworkPath, processPath); +} + +- (void)assertIsOtherWithImagePath:(NSString *)imagePath processPath:(NSString *)processPath { + MSACBinaryImageType imageType = [MSACErrorLogFormatter imageTypeForImagePath:imagePath processPath:processPath]; + XCTAssertEqual(imageType, MSACBinaryImageTypeOther, @"Test other image %@ with process %@", imagePath, processPath); +} + +- (void)testErrorLogFromCrashReportWithWrapper { + + // If + MSACMockUserDefaults *defaults = [MSACMockUserDefaults new]; + + // When + NSData *crashData = [MSACCrashesTestUtil dataOfFixtureCrashReportWithFileName:@"live_report_exception"]; + + // Then + XCTAssertNotNil(crashData); + + // If + NSError *error = nil; + PLCrashReport *report = [[PLCrashReport alloc] initWithData:crashData error:&error]; + MSACDevice *device = self.deviceMock; + device.wrapperSdkVersion = @"10.11.12"; + device.wrapperSdkName = @"Wrapper SDK for iOS"; + device.wrapperRuntimeVersion = @"13.14"; + device.liveUpdateReleaseLabel = @"Release Label"; + device.liveUpdateDeploymentKey = @"Deployment Key"; + device.liveUpdatePackageHash = @"Package Hash"; + + // When + MSACAppleErrorLog *errorLog = [MSACErrorLogFormatter errorLogFromCrashReport:report]; + + // Then + XCTAssertEqualObjects(errorLog.device.wrapperSdkName, @"Wrapper SDK for iOS"); + [defaults stopMocking]; +} + +- (void)assertIsCrashProbeReportValidConverted:(NSString *)filename { + NSData *crashData = [MSACCrashesTestUtil dataOfFixtureCrashReportWithFileName:filename]; + XCTAssertNotNil(crashData); + + NSError *error = nil; + PLCrashReport *crashReport = [[PLCrashReport alloc] initWithData:crashData error:&error]; + XCTAssertNotNil(crashReport); + PLCrashReportThreadInfo *crashedThread = [MSACErrorLogFormatter findCrashedThreadInReport:crashReport]; + XCTAssertNotNil(crashedThread); + MSACAppleErrorLog *errorLog = [MSACErrorLogFormatter errorLogFromCrashReport:crashReport]; + XCTAssertNotNil(errorLog); + + NSString *actualId = [MSACErrorLogFormatter errorIdForCrashReport:crashReport]; + assertThat(errorLog.errorId, equalTo(actualId)); + + assertThat(errorLog.processId, equalTo(@(crashReport.processInfo.processID))); + assertThat(errorLog.processName, equalTo(crashReport.processInfo.processName)); + assertThat(errorLog.parentProcessId, equalTo(@(crashReport.processInfo.parentProcessID))); + assertThat(errorLog.parentProcessName, equalTo(crashReport.processInfo.parentProcessName)); + assertThat(errorLog.errorThreadId, equalTo(@(crashedThread.threadNumber))); + + // FIXME: PLCrashReporter doesn't support millisecond precision, here is a workaround to fill 999 for its millisecond. + XCTAssertEqual([errorLog.timestamp timeIntervalSince1970], [crashReport.systemInfo.timestamp timeIntervalSince1970] + 0.999); + assertThat(errorLog.appLaunchTimestamp, equalTo(crashReport.processInfo.processStartTime)); + + NSArray *images = crashReport.images; + for (PLCrashReportBinaryImageInfo *image in images) { + if (image.codeType != nil && image.codeType.typeEncoding == PLCrashReportProcessorTypeEncodingMach) { + XCTAssertEqual(errorLog.primaryArchitectureId.unsignedLongLongValue, image.codeType.type, @"Report: %@, Image: %@", filename, + [image.imageName lastPathComponent]); + XCTAssertEqual(errorLog.architectureVariantId.unsignedLongLongValue, image.codeType.subtype, @"Report: %@, Image: %@", filename, + [image.imageName lastPathComponent]); + } + } + + XCTAssertNotNil(errorLog.applicationPath); + + // Not using the report.processInfo.processPath directly to compare as it will be anonymized in the Simulator. + assertThat(errorLog.applicationPath, equalTo(@"/private/var/mobile/Containers/Bundle/Application/253BCE7D-4032-4FB2-AC63-C16F5C0BCBFA/" + @"CrashProbeiOS.app/CrashProbeiOS")); + + NSString *signalAddress = [NSString stringWithFormat:@"0x%" PRIx64, crashReport.signalInfo.address]; + assertThat(errorLog.osExceptionType, equalTo(crashReport.signalInfo.name)); + assertThat(errorLog.osExceptionCode, equalTo(crashReport.signalInfo.code)); + assertThat(errorLog.osExceptionAddress, equalTo(signalAddress)); + + if (crashReport.hasExceptionInfo) { + assertThat(errorLog.exceptionType, equalTo(crashReport.exceptionInfo.exceptionName)); + assertThat(errorLog.exceptionReason, equalTo(crashReport.exceptionInfo.exceptionReason)); + } else { + XCTAssertEqual(errorLog.exceptionType, nil); + XCTAssertEqual(errorLog.exceptionReason, nil); + } + + assertThat(errorLog.threads, hasCountOf([crashReport.threads count])); + for (NSUInteger i = 0; i < [errorLog.threads count]; i++) { + MSACThread *thread = errorLog.threads[i]; + PLCrashReportThreadInfo *plThread = crashReport.threads[i]; + + assertThat(thread.threadId, equalTo(@(plThread.threadNumber))); + if (crashReport.hasExceptionInfo && [thread.threadId isEqualToNumber:@(crashedThread.threadNumber)]) { + XCTAssertNotNil(thread.exception); + } else { + XCTAssertNil(thread.exception); + } + } + assertThat(errorLog.registers, hasCountOf([crashedThread.registers count])); +} + +- (void)testFormat32BitAddress { + + // If + uint64_t address32Bit = 0x123456789; + + // When + NSString *actual = [MSACErrorLogFormatter formatAddress:address32Bit is64bit:NO]; + NSString *expected = [NSString stringWithFormat:@"0x%0*" PRIx64, 8, address32Bit]; + + // Then + XCTAssertEqualObjects(expected, actual); +} + +- (void)testFormat64BitAddress { + + // If + uint64_t address64Bit = 0x1234567890abcdef; + + // When + NSString *actual = [MSACErrorLogFormatter formatAddress:address64Bit is64bit:YES]; + NSString *expected = [NSString stringWithFormat:@"0x%0*" PRIx64, 16, address64Bit]; + + // Then + XCTAssertEqualObjects(expected, actual); +} + +- (void)testBinaryImageCountFromReportIsCorrect { + + // If + NSData *crashData = [MSACCrashesTestUtil dataOfFixtureCrashReportWithFileName:@"live_report_arm64e"]; + NSError *error = nil; + PLCrashReport *crashReport = [[PLCrashReport alloc] initWithData:crashData error:&error]; + NSUInteger expectedCount = 16; + + // When + NSArray *binaryImages = [MSACErrorLogFormatter extractBinaryImagesFromReport:crashReport is64bit:YES]; + + // Then + XCTAssertEqual(expectedCount, binaryImages.count); +} + +#if TARGET_OS_OSX && !TARGET_OS_MACCATALYST +- (void)testCrashReportsParametersFromMacOSReport { + for (unsigned long i = 0; i < kMacOSCrashReportsParameters.count; i++) { + + // If + NSData *crashData = [MSACCrashesTestUtil dataOfFixtureCrashReportWithFileName:kMacOSCrashReportsParameters[i][kFixture]]; + NSError *error = nil; + PLCrashReport *crashReport = [[PLCrashReport alloc] initWithData:crashData error:&error]; + + // When + NSArray *binaryImages = [MSACErrorLogFormatter extractBinaryImagesFromReport:crashReport is64bit:YES]; + PLCrashReportThreadInfo *crashedThread = [MSACErrorLogFormatter findCrashedThreadInReport:crashReport]; + + // Then + int expectedBinariesCount = [kMacOSCrashReportsParameters[i][kBinariesCount] intValue]; + int expectedThreadNumber = [kMacOSCrashReportsParameters[i][kThreadNumber] intValue]; + int expectedFramesCount = [kMacOSCrashReportsParameters[i][kFramesCount] intValue]; + XCTAssertEqual(expectedBinariesCount, binaryImages.count); + XCTAssertEqual(expectedThreadNumber, crashedThread.threadNumber); + XCTAssertEqual(expectedFramesCount, crashedThread.stackFrames.count); + } +} +#endif + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACErrorReportTests.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACErrorReportTests.m new file mode 100644 index 0000000000..3841fece26 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACErrorReportTests.m @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACDevice.h" +#import "MSACDeviceInternal.h" +#import "MSACErrorReportPrivate.h" +#import "MSACTestFrameworks.h" +#import "MSACWrapperSdkInternal.h" + +@interface MSACErrorReportTests : XCTestCase + +@end + +@implementation MSACErrorReportTests + +- (void)testInitializationWorks { + // If + NSString *sdkVersion = @"3.0.1"; + NSString *model = @"iPhone 7.2"; + NSString *oemName = @"Apple"; + NSString *osName = @"iOS"; + NSString *osVersion = @"9.3.20"; + NSNumber *osApiLevel = @(320); + NSString *locale = @"US-EN"; + NSNumber *timeZoneOffset = @(9); + NSString *screenSize = @"750x1334"; + NSString *appVersion = @"3.4.5 (34)"; + NSString *carrierName = @"T-Mobile"; + NSString *carrierCountry = @"United States"; + NSString *wrapperSdkVersion = @"6.7.8"; + NSString *wrapperSdkName = @"wrapper-sdk"; + NSString *wrapperRuntimeVersion = @"9.10"; + NSString *liveUpdateReleaseLabel = @"live-update-release"; + NSString *liveUpdateDeploymentKey = @"deployment-key"; + NSString *liveUpdatePackageHash = @"b10a8db164e0754105b7a99be72e3fe5"; + + MSACDevice *device = [[MSACDevice alloc] init]; + device.sdkVersion = sdkVersion; + device.model = model; + device.oemName = oemName; + device.osName = osName; + device.osVersion = osVersion; + device.osApiLevel = osApiLevel; + device.locale = locale; + device.timeZoneOffset = timeZoneOffset; + device.screenSize = screenSize; + device.appVersion = appVersion; + device.carrierName = carrierName; + device.carrierCountry = carrierCountry; + device.wrapperSdkVersion = wrapperSdkVersion; + device.wrapperSdkName = wrapperSdkName; + device.wrapperRuntimeVersion = wrapperRuntimeVersion; + device.liveUpdateReleaseLabel = liveUpdateReleaseLabel; + device.liveUpdateDeploymentKey = liveUpdateDeploymentKey; + device.liveUpdatePackageHash = liveUpdatePackageHash; + + NSString *errorId = @"errorReportId"; + NSString *reporterKey = @"reporterKey"; + NSString *signal = @"signal"; + NSString *exceptionName = @"exception_name"; + NSString *exceptionReason = @"exception_reason"; + NSDate *appStartTime = [NSDate new]; + NSDate *appErrorTime = [NSDate dateWithTimeIntervalSinceNow:20]; + NSUInteger processIdentifier = 4; + + // When + MSACErrorReport *sut = [[MSACErrorReport alloc] initWithErrorId:errorId + reporterKey:reporterKey + signal:signal + exceptionName:exceptionName + exceptionReason:exceptionReason + appStartTime:appStartTime + appErrorTime:appErrorTime + device:device + appProcessIdentifier:processIdentifier]; + + // Then + assertThat(sut, notNilValue()); + assertThat(sut.incidentIdentifier, equalTo(errorId)); + assertThat(sut.reporterKey, equalTo(reporterKey)); + assertThat(sut.signal, equalTo(signal)); + assertThat(sut.exceptionName, equalTo(exceptionName)); + assertThat(sut.exceptionReason, equalTo(exceptionReason)); + assertThat(sut.appStartTime, equalTo(appStartTime)); + assertThat(sut.appErrorTime, equalTo(appErrorTime)); + assertThat(sut.device, equalTo(device)); + assertThatUnsignedInteger(sut.appProcessIdentifier, equalToUnsignedInteger(processIdentifier)); +} + +- (void)testIsAppKill { + + // When + MSACErrorReport *sut = [MSACErrorReport new]; + + // Then + XCTAssertFalse([sut isAppKill]); + + // When + sut = [[MSACErrorReport alloc] initWithErrorId:nil + reporterKey:nil + signal:@"SIGSEGV" + exceptionName:nil + exceptionReason:nil + appStartTime:nil + appErrorTime:nil + device:nil + appProcessIdentifier:0]; + + // Then + XCTAssertFalse([sut isAppKill]); + + // When + sut = [[MSACErrorReport alloc] initWithErrorId:nil + reporterKey:nil + signal:@"SIGKILL" + exceptionName:nil + exceptionReason:nil + appStartTime:nil + appErrorTime:nil + device:nil + appProcessIdentifier:0]; + + // Then + XCTAssertTrue([sut isAppKill]); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACExceptionTests.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACExceptionTests.m new file mode 100644 index 0000000000..ad02fc7141 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACExceptionTests.m @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACCrashesTestUtil.h" +#import "MSACException.h" +#import "MSACStackFrame.h" +#import "MSACTestFrameworks.h" + +@interface MSACExceptionsTests : XCTestCase + +@end + +@implementation MSACExceptionsTests + +#pragma mark - Tests + +- (void)testSerializingBinaryToDictionaryWorks { + + // If + MSACException *sut = [MSACCrashesTestUtil exception]; + sut.innerExceptions = @[ [MSACCrashesTestUtil exception] ]; + + // When + NSMutableDictionary *actual = [sut serializeToDictionary]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual[@"type"], equalTo(sut.type)); + assertThat(actual[@"message"], equalTo(sut.message)); + assertThat(actual[@"stackTrace"], equalTo(sut.stackTrace)); + assertThat(actual[@"wrapperSdkName"], equalTo(sut.wrapperSdkName)); +} + +- (void)testNSCodingSerializationAndDeserializationWorks { + + // If + MSACException *sut = [MSACCrashesTestUtil exception]; + sut.innerExceptions = @[ [MSACCrashesTestUtil exception] ]; + + // When + NSData *serializedEvent = [MSACUtility archiveKeyedData:sut]; + id actual = [MSACUtility unarchiveKeyedData:serializedEvent]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual, instanceOf([MSACException class])); + + MSACException *actualException = actual; + + assertThat(actualException, equalTo(sut)); + assertThat(actualException.type, equalTo(sut.type)); + assertThat(actualException.message, equalTo(sut.message)); + assertThat(actualException.stackTrace, equalTo(sut.stackTrace)); + assertThat(actualException.wrapperSdkName, equalTo(sut.wrapperSdkName)); + assertThatInteger(actualException.frames.count, equalToInteger(1)); + assertThat(actualException.frames.firstObject.address, equalTo(@"frameAddress")); + assertThat(actualException.frames.firstObject.code, equalTo(@"frameSymbol")); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACHandledErrorLogTests.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACHandledErrorLogTests.m new file mode 100644 index 0000000000..a1d619a9d4 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACHandledErrorLogTests.m @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACCrashesTestUtil.h" +#import "MSACException.h" +#import "MSACHandledErrorLog.h" +#import "MSACTestFrameworks.h" + +@interface MSACHandledErrorLogTests : XCTestCase + +@property(nonatomic) MSACHandledErrorLog *sut; + +@end + +@implementation MSACHandledErrorLogTests + +#pragma mark - Housekeeping + +- (void)setUp { + [super setUp]; + self.sut = [self handledErrorLog]; +} + +- (void)tearDown { + [super tearDown]; +} + +#pragma mark - Helper + +- (MSACHandledErrorLog *)handledErrorLog { + MSACHandledErrorLog *handledErrorLog = [MSACHandledErrorLog new]; + handledErrorLog.type = @"handledError"; + handledErrorLog.exception = [MSACCrashesTestUtil exception]; + handledErrorLog.errorId = @"123"; + return handledErrorLog; +} + +#pragma mark - Tests + +- (void)testInitializationWorks { + XCTAssertNotNil(self.sut); +} + +- (void)testSerializationToDictionaryWorks { + + // When + NSDictionary *actual = [self.sut serializeToDictionary]; + + // Then + XCTAssertNotNil(actual); + assertThat(actual[@"type"], equalTo(self.sut.type)); + assertThat(actual[@"id"], equalTo(self.sut.errorId)); + NSDictionary *exceptionDictionary = actual[@"exception"]; + XCTAssertNotNil(exceptionDictionary); + assertThat(exceptionDictionary[@"type"], equalTo(self.sut.exception.type)); + assertThat(exceptionDictionary[@"message"], equalTo(self.sut.exception.message)); + assertThat(exceptionDictionary[@"wrapperSdkName"], equalTo(self.sut.exception.wrapperSdkName)); +} + +- (void)testNSCodingSerializationAndDeserializationWorks { + + // When + NSData *serializedEvent = [MSACUtility archiveKeyedData:self.sut]; + id actual = [MSACUtility unarchiveKeyedData:serializedEvent]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual, instanceOf([MSACHandledErrorLog class])); + + // The MSACHandledErrorLog. + MSACHandledErrorLog *actualLog = actual; + assertThat(actualLog, equalTo(self.sut)); + XCTAssertTrue([actualLog isEqual:self.sut]); + assertThat(actualLog.type, equalTo(self.sut.type)); + assertThat(actualLog.errorId, equalTo(self.sut.errorId)); + + // The exception field. + MSACException *actualException = actualLog.exception; + assertThat(actualException.type, equalTo(self.sut.exception.type)); + assertThat(actualException.message, equalTo(self.sut.exception.message)); + assertThat(actualException.wrapperSdkName, equalTo(self.sut.exception.wrapperSdkName)); +} + +- (void)testIsEqual { + + // When + MSACHandledErrorLog *first = [self handledErrorLog]; + MSACHandledErrorLog *second = [self handledErrorLog]; + + // Then + XCTAssertTrue([first isEqual:second]); + + // When + second.errorId = MSAC_UUID_STRING; + + // Then + XCTAssertFalse([first isEqual:second]); +} + +- (void)testIsValid { + + // When + MSACHandledErrorLog *log = [MSACHandledErrorLog new]; + log.device = OCMClassMock([MSACDevice class]); + OCMStub([log.device isValid]).andReturn(YES); + log.sid = @"sid"; + log.timestamp = [NSDate dateWithTimeIntervalSince1970:42]; + log.errorId = @"errorId"; + log.sid = MSAC_UUID_STRING; + + // Then + XCTAssertFalse([log isValid]); + + // When + log.errorId = MSAC_UUID_STRING; + + // Then + XCTAssertFalse([log isValid]); + + // When + log.exception = [MSACCrashesTestUtil exception]; + + // Then + XCTAssertTrue([log isValid]); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACMockCrashesDelegate.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACMockCrashesDelegate.h new file mode 100644 index 0000000000..d1debfe02f --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACMockCrashesDelegate.h @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACCrashesDelegate.h" +#import + +@interface MSACMockCrashesDelegate : NSObject +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACMockCrashesDelegate.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACMockCrashesDelegate.m new file mode 100644 index 0000000000..33c59294f7 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACMockCrashesDelegate.m @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACMockCrashesDelegate.h" + +@implementation MSACMockCrashesDelegate +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACMockNSUserDefaults.h b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACMockNSUserDefaults.h new file mode 100644 index 0000000000..8936154a62 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACMockNSUserDefaults.h @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +@interface MSACMockNSUserDefaults : NSUserDefaults + +/** + * Clear dictionary. + */ +- (void)stopMocking; + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACMockNSUserDefaults.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACMockNSUserDefaults.m new file mode 100644 index 0000000000..c97ad86a35 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACMockNSUserDefaults.m @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACMockNSUserDefaults.h" +#import "MSACTestFrameworks.h" + +@interface MSACMockNSUserDefaults () + +@property(nonatomic) NSMutableDictionary *dictionary; +@property(nonatomic) id mockNSUserDefaults; + +@end + +@implementation MSACMockNSUserDefaults + +- (instancetype)init { + self = [super init]; + if (self) { + _dictionary = [NSMutableDictionary new]; + + // Mock MSACUserDefaults shared method to return this instance. + _mockNSUserDefaults = OCMClassMock([NSUserDefaults class]); + OCMStub([_mockNSUserDefaults standardUserDefaults]).andReturn(self); + } + return self; +} + +- (void)setObject:(id)anObject forKey:(NSString *)aKey { + + // Don't store nil objects. + if (!anObject) { + return; + } + [self.dictionary setObject:anObject forKey:aKey]; +} + +- (nullable id)objectForKey:(NSString *)aKey { + return self.dictionary[aKey]; +} + +- (void)stopMocking { + [self.dictionary removeAllObjects]; + [self.mockNSUserDefaults stopMocking]; +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACStackFrameTests.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACStackFrameTests.m new file mode 100644 index 0000000000..871f71b0c1 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACStackFrameTests.m @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACStackFrame.h" +#import "MSACTestFrameworks.h" + +@interface MSACStackFrameTests : XCTestCase + +@end + +@implementation MSACStackFrameTests + +#pragma mark - Helper + +- (MSACStackFrame *)stackFrame { + NSString *address = @"address"; + NSString *code = @"code"; + NSString *className = @"class_name"; + NSString *methodName = @"method_name"; + NSNumber *lineNumber = @123; + NSString *fileName = @"file_name"; + + MSACStackFrame *threadFrame = [MSACStackFrame new]; + threadFrame.address = address; + threadFrame.code = code; + threadFrame.className = className; + threadFrame.methodName = methodName; + threadFrame.lineNumber = lineNumber; + threadFrame.fileName = fileName; + + return threadFrame; +} + +#pragma mark - Tests + +- (void)testSerializingBinaryToDictionaryWorks { + + // If + MSACStackFrame *sut = [self stackFrame]; + + // When + NSMutableDictionary *actual = [sut serializeToDictionary]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual[@"address"], equalTo(sut.address)); + assertThat(actual[@"code"], equalTo(sut.code)); + assertThat(actual[@"className"], equalTo(sut.className)); + assertThat(actual[@"methodName"], equalTo(sut.methodName)); + assertThat(actual[@"lineNumber"], equalTo(sut.lineNumber)); + assertThat(actual[@"fileName"], equalTo(sut.fileName)); +} + +- (void)testNSCodingSerializationAndDeserializationWorks { + + // If + MSACStackFrame *sut = [self stackFrame]; + + // When + NSData *serializedEvent = [MSACUtility archiveKeyedData:sut]; + id actual = [MSACUtility unarchiveKeyedData:serializedEvent]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual, instanceOf([MSACStackFrame class])); + + MSACStackFrame *actualThreadFrame = actual; + assertThat(actualThreadFrame, equalTo(sut)); + assertThat(actualThreadFrame.address, equalTo(sut.address)); + assertThat(actualThreadFrame.code, equalTo(sut.code)); + assertThat(actualThreadFrame.className, equalTo(sut.className)); + assertThat(actualThreadFrame.methodName, equalTo(sut.methodName)); + assertThat(actualThreadFrame.lineNumber, equalTo(sut.lineNumber)); + assertThat(actualThreadFrame.fileName, equalTo(sut.fileName)); +} + +- (void)testIsNotEqualToNil { + + // Then + XCTAssertFalse([[MSACStackFrame new] isEqual:nil]); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACThreadTests.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACThreadTests.m new file mode 100644 index 0000000000..94384d9df9 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACThreadTests.m @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACException.h" +#import "MSACStackFrame.h" +#import "MSACTestFrameworks.h" +#import "MSACThread.h" + +@interface MSACThreadTests : XCTestCase + +@end + +@implementation MSACThreadTests + +#pragma mark - Helper + +- (MSACThread *)thread { + NSNumber *threadId = @(12); + NSString *name = @"thread_name"; + + MSACException *exception = [MSACException new]; + exception.type = @"exception_type"; + exception.message = @"message"; + MSACStackFrame *frame = [self stackFrame]; + exception.frames = @[ frame ]; + + MSACThread *thread = [MSACThread new]; + thread.threadId = threadId; + thread.name = name; + thread.exception = exception; + thread.frames = [@[ frame ] mutableCopy]; + + return thread; +} + +- (MSACStackFrame *)stackFrame { + NSString *address = @"address"; + NSString *code = @"code"; + + MSACStackFrame *threadFrame = [MSACStackFrame new]; + threadFrame.address = address; + threadFrame.code = code; + + return threadFrame; +} + +#pragma mark - Tests + +- (void)testSerializingBinaryToDictionaryWorks { + + // If + MSACThread *sut = [self thread]; + + // When + NSMutableDictionary *actual = [sut serializeToDictionary]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual[@"id"], equalTo(sut.threadId)); + assertThat(actual[@"name"], equalTo(sut.name)); + assertThat([actual[@"exception"] valueForKey:@"type"], equalTo(sut.exception.type)); + assertThat([actual[@"exception"] valueForKey:@"message"], equalTo(sut.exception.message)); + + NSArray *actualFrames = [actual[@"exception"] valueForKey:@"frames"]; + XCTAssertEqual(actualFrames.count, sut.exception.frames.count); + NSDictionary *actualFrame = [actualFrames firstObject]; + MSACStackFrame *expectedFrame = [sut.exception.frames firstObject]; + assertThat([actualFrame valueForKey:@"code"], equalTo(expectedFrame.code)); + assertThat([actualFrame valueForKey:@"address"], equalTo(expectedFrame.address)); +} + +- (void)testNSCodingSerializationAndDeserializationWorks { + // If + MSACThread *sut = [self thread]; + + // When + NSData *serializedEvent = [MSACUtility archiveKeyedData:sut]; + id actual = [MSACUtility unarchiveKeyedData:serializedEvent]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual, instanceOf([MSACThread class])); + + MSACThread *actualThread = actual; + assertThat(actualThread, equalTo(actual)); + assertThat(actualThread.threadId, equalTo(sut.threadId)); + assertThat(actualThread.name, equalTo(sut.name)); + assertThat(actualThread.exception.type, equalTo(sut.exception.type)); + assertThat(actualThread.exception.message, equalTo(sut.exception.message)); + assertThatUnsignedLong(actualThread.exception.frames.count, equalToUnsignedLong(sut.exception.frames.count)); + + assertThatInteger(actualThread.frames.count, equalToInteger(1)); +} + +- (void)testIsValid { + + // When + MSACThread *thread = [MSACThread new]; + + // Then + XCTAssertFalse([thread isValid]); + + // When + thread.threadId = @123; + + // Then + XCTAssertFalse([thread isValid]); + + // When + [thread.frames addObject:[MSACStackFrame new]]; + + // Then + XCTAssertTrue([thread isValid]); +} + +- (void)testIsNotEqualToNil { + + // Then + XCTAssertFalse([[MSACThread new] isEqual:nil]); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACWrapperCrashesHelperTests.mm b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACWrapperCrashesHelperTests.mm new file mode 100644 index 0000000000..a8eb42553e --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACWrapperCrashesHelperTests.mm @@ -0,0 +1,307 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACAppCenterInternal.h" +#import "MSACChannelGroupProtocol.h" +#import "MSACChannelUnitConfiguration.h" +#import "MSACChannelUnitProtocol.h" +#import "MSACCrashesInternal.h" +#import "MSACCrashesPrivate.h" +#import "MSACCrashesTestUtil.h" +#import "MSACCrashesUtil.h" +#import "MSACDeviceTrackerPrivate.h" +#import "MSACErrorAttachmentLog.h" +#import "MSACException.h" +#import "MSACHandledErrorLog.h" +#import "MSACHttpClient.h" +#import "MSACLogWithProperties.h" +#import "MSACTestFrameworks.h" +#import "MSACWrapperCrashesHelper.h" +#import "MSAC_Reachability.h" + +@interface MSACWrapperCrashesHelperTests : XCTestCase + +@property(nonatomic) id httpClientMock; +@property(nonatomic) id reachabilityMock; +@property(nonatomic) id deviceTrackerMock; + +@end + +static NSString *const kMSACTestAppSecret = @"TestAppSecret"; +static NSString *const kMSACTypeHandledError = @"handledError"; + +@implementation MSACWrapperCrashesHelperTests + +- (void)setUp { + self.httpClientMock = OCMClassMock([MSACHttpClient class]); + OCMStub([self.httpClientMock alloc]).andReturn(self.httpClientMock); + self.reachabilityMock = OCMClassMock([MSAC_Reachability class]); + OCMStub([self.reachabilityMock reachabilityForInternetConnection]).andReturn(self.reachabilityMock); + [MSACDeviceTracker resetSharedInstance]; + self.deviceTrackerMock = OCMClassMock([MSACDeviceTracker class]); + OCMStub([self.deviceTrackerMock sharedInstance]).andReturn(self.deviceTrackerMock); +} + +- (void)tearDown { + [super tearDown]; + [self.httpClientMock stopMocking]; + [self.reachabilityMock stopMocking]; + [self.deviceTrackerMock stopMocking]; + [MSACDeviceTracker resetSharedInstance]; + [MSACCrashes resetSharedInstance]; +} + +- (void)testSettingAndGettingDelegateWorks { + + // If + id delegateMock = OCMProtocolMock(@protocol(MSACCrashHandlerSetupDelegate)); + [MSACWrapperCrashesHelper setCrashHandlerSetupDelegate:delegateMock]; + + // When + id retrievedDelegate = [MSACWrapperCrashesHelper crashHandlerSetupDelegate]; + + // Then + assertThat(delegateMock, equalTo(retrievedDelegate)); +} + +- (void)testTrackModelExceptionWithExceptionOnly { + + // If + __block NSString *type; + __block NSString *userId; + __block NSString *errorId; + __block MSACException *exception; + NSString *expectedUserId = @"alice"; + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + OCMStub([channelGroupMock addChannelUnitWithConfiguration:[OCMArg checkWithBlock:^BOOL(MSACChannelUnitConfiguration *configuration) { + return [configuration.groupId isEqualToString:@"Crashes"]; + }]]) + .andReturn(channelUnitMock); + OCMStub([channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY]).andReturn(OCMProtocolMock(@protocol(MSACChannelUnitProtocol))); + OCMStub([channelUnitMock enqueueItem:[OCMArg isKindOfClass:[MSACLogWithProperties class]] flags:MSACFlagsDefault]) + .andDo(^(NSInvocation *invocation) { + MSACHandledErrorLog *log; + [invocation getArgument:&log atIndex:2]; + type = log.type; + userId = log.userId; + errorId = log.errorId; + exception = log.exception; + }); + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [MSACAppCenter setUserId:expectedUserId]; + [[MSACCrashes sharedInstance] startWithChannelGroup:channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + MSACException *expectedException = [MSACException new]; + expectedException.message = @"Oh this is wrong..."; + expectedException.stackTrace = @"mock stacktrace"; + expectedException.type = @"Some.Exception"; + NSString *actualErrorId = [MSACWrapperCrashesHelper trackModelException:expectedException withProperties:nil withAttachments:nil]; + + // Then + assertThat(type, is(kMSACTypeHandledError)); + assertThat(userId, is(expectedUserId)); + assertThat(errorId, notNilValue()); + assertThat(exception, is(expectedException)); + + // Verify the errorId returned by trackModelException is the same one that enqueued to the channel. + assertThat(actualErrorId, is(errorId)); +} + +- (void)testTrackModelExceptionWithExceptionAndProperties { + + // If + __block NSString *type; + __block NSString *userId; + __block NSString *errorId; + __block MSACException *exception; + __block NSDictionary *properties; + NSString *expectedUserId = @"alice"; + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + OCMStub([channelGroupMock addChannelUnitWithConfiguration:[OCMArg checkWithBlock:^BOOL(MSACChannelUnitConfiguration *configuration) { + return [configuration.groupId isEqualToString:@"Crashes"]; + }]]) + .andReturn(channelUnitMock); + OCMStub([channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY]).andReturn(OCMProtocolMock(@protocol(MSACChannelUnitProtocol))); + OCMStub([channelUnitMock enqueueItem:[OCMArg isKindOfClass:[MSACLogWithProperties class]] flags:MSACFlagsDefault]) + .andDo(^(NSInvocation *invocation) { + MSACHandledErrorLog *log; + [invocation getArgument:&log atIndex:2]; + type = log.type; + userId = log.userId; + errorId = log.errorId; + exception = log.exception; + properties = log.properties; + }); + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [MSACAppCenter setUserId:expectedUserId]; + [[MSACCrashes sharedInstance] startWithChannelGroup:channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + + // When + MSACException *expectedException = [MSACException new]; + expectedException.message = @"Oh this is wrong..."; + expectedException.stackTrace = @"mock stacktrace"; + expectedException.type = @"Some.Exception"; + NSDictionary *expectedProperties = @{@"milk" : @"yes", @"cookie" : @"of course"}; + NSString *actualErrorId = [MSACWrapperCrashesHelper trackModelException:expectedException + withProperties:expectedProperties + withAttachments:nil]; + + // Then + assertThat(type, is(kMSACTypeHandledError)); + assertThat(userId, is(expectedUserId)); + assertThat(errorId, notNilValue()); + assertThat(exception, is(expectedException)); + assertThat(properties, is(expectedProperties)); + + // Verify the errorId returned by trackModelException is the same one that enqueued to the channel. + assertThat(actualErrorId, is(errorId)); +} + +- (void)testTrackModelExceptionWithExceptionAndAttachments { + + // If + __block NSString *type; + __block NSString *userId; + __block NSString *errorId; + __block MSACException *exception; + __block NSMutableArray *errorAttachmentLogs = [NSMutableArray new]; + NSString *expectedUserId = @"alice"; + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + OCMStub([channelGroupMock addChannelUnitWithConfiguration:[OCMArg checkWithBlock:^BOOL(MSACChannelUnitConfiguration *configuration) { + return [configuration.groupId isEqualToString:@"Crashes"]; + }]]) + .andReturn(channelUnitMock); + OCMStub([channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY]).andReturn(OCMProtocolMock(@protocol(MSACChannelUnitProtocol))); + OCMStub([channelUnitMock enqueueItem:[OCMArg isKindOfClass:[MSACHandledErrorLog class]] flags:MSACFlagsDefault]) + .andDo(^(NSInvocation *invocation) { + MSACHandledErrorLog *log; + [invocation getArgument:&log atIndex:2]; + type = log.type; + userId = log.userId; + errorId = log.errorId; + exception = log.exception; + }); + OCMStub([channelUnitMock enqueueItem:[OCMArg isKindOfClass:[MSACErrorAttachmentLog class]] flags:MSACFlagsDefault]) + .andDo(^(NSInvocation *invocation) { + MSACErrorAttachmentLog *log; + [invocation getArgument:&log atIndex:2]; + [errorAttachmentLogs addObject:log]; + }); + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [MSACAppCenter setUserId:expectedUserId]; + [[MSACCrashes sharedInstance] startWithChannelGroup:channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + NSData *expectedData = [@"Please attach meI am a nice " + @"data." dataUsingEncoding:NSUTF8StringEncoding]; + MSACErrorAttachmentLog *errorAttachmentLog1 = [[MSACErrorAttachmentLog alloc] initWithFilename:@"text.txt" + attachmentText:@"Please attach me, I am a nice text."]; + MSACErrorAttachmentLog *errorAttachmentLog2 = [[MSACErrorAttachmentLog alloc] initWithFilename:@"binary.xml" + attachmentBinary:expectedData + contentType:@"text/xml"]; + NSArray *attachments = @[ errorAttachmentLog1, errorAttachmentLog2 ]; + + // When + MSACException *expectedException = [MSACException new]; + expectedException.message = @"Oh this is wrong..."; + expectedException.stackTrace = @"mock stacktrace"; + expectedException.type = @"Some.Exception"; + NSString *actualErrorId = [MSACWrapperCrashesHelper trackModelException:expectedException withProperties:nil withAttachments:attachments]; + + // Then + XCTAssertEqual(type, kMSACTypeHandledError); + XCTAssertEqualObjects(userId, expectedUserId); + XCTAssertNotNil(errorId); + XCTAssertEqualObjects(exception, expectedException); + XCTAssertEqual([errorAttachmentLogs count], [attachments count]); + XCTAssertEqualObjects(errorAttachmentLogs[0], errorAttachmentLog1); + XCTAssertEqualObjects(errorAttachmentLogs[1], errorAttachmentLog2); + + // Verify the errorId returned by trackModelException is the same one that enqueued to the channel. + XCTAssertEqualObjects(actualErrorId, errorId); +} + +- (void)testTrackModelExceptionWithAllParameters { + + // If + __block NSString *type; + __block NSString *userId; + __block NSString *errorId; + __block MSACException *exception; + __block NSDictionary *properties; + __block NSMutableArray *errorAttachmentLogs = [NSMutableArray new]; + NSString *expectedUserId = @"alice"; + id channelUnitMock = OCMProtocolMock(@protocol(MSACChannelUnitProtocol)); + id channelGroupMock = OCMProtocolMock(@protocol(MSACChannelGroupProtocol)); + OCMStub([channelGroupMock addChannelUnitWithConfiguration:[OCMArg checkWithBlock:^BOOL(MSACChannelUnitConfiguration *configuration) { + return [configuration.groupId isEqualToString:@"Crashes"]; + }]]) + .andReturn(channelUnitMock); + OCMStub([channelGroupMock addChannelUnitWithConfiguration:OCMOCK_ANY]).andReturn(OCMProtocolMock(@protocol(MSACChannelUnitProtocol))); + OCMStub([channelUnitMock enqueueItem:[OCMArg isKindOfClass:[MSACHandledErrorLog class]] flags:MSACFlagsDefault]) + .andDo(^(NSInvocation *invocation) { + MSACHandledErrorLog *log; + [invocation getArgument:&log atIndex:2]; + type = log.type; + userId = log.userId; + errorId = log.errorId; + exception = log.exception; + properties = log.properties; + }); + OCMStub([channelUnitMock enqueueItem:[OCMArg isKindOfClass:[MSACErrorAttachmentLog class]] flags:MSACFlagsDefault]) + .andDo(^(NSInvocation *invocation) { + MSACErrorAttachmentLog *log; + [invocation getArgument:&log atIndex:2]; + [errorAttachmentLogs addObject:log]; + }); + [MSACAppCenter configureWithAppSecret:kMSACTestAppSecret]; + [MSACAppCenter setUserId:expectedUserId]; + [[MSACCrashes sharedInstance] startWithChannelGroup:channelGroupMock + appSecret:kMSACTestAppSecret + transmissionTargetToken:nil + fromApplication:YES]; + NSData *expectedData = [@"Please attach meI am a nice " + @"data." dataUsingEncoding:NSUTF8StringEncoding]; + MSACErrorAttachmentLog *errorAttachmentLog1 = [[MSACErrorAttachmentLog alloc] initWithFilename:@"text.txt" + attachmentText:@"Please attach me, I am a nice text."]; + MSACErrorAttachmentLog *errorAttachmentLog2 = [[MSACErrorAttachmentLog alloc] initWithFilename:@"binary.xml" + attachmentBinary:expectedData + contentType:@"text/xml"]; + NSArray *attachments = @[ errorAttachmentLog1, errorAttachmentLog2 ]; + + // When + MSACException *expectedException = [MSACException new]; + expectedException.message = @"Oh this is wrong..."; + expectedException.stackTrace = @"mock stacktrace"; + expectedException.type = @"Some.Exception"; + NSDictionary *expectedProperties = @{@"milk" : @"yes", @"cookie" : @"of course"}; + NSString *actualErrorId = [MSACWrapperCrashesHelper trackModelException:expectedException + withProperties:expectedProperties + withAttachments:attachments]; + + // Then + XCTAssertEqual(type, kMSACTypeHandledError); + XCTAssertEqualObjects(userId, expectedUserId); + XCTAssertNotNil(errorId); + XCTAssertEqualObjects(exception, expectedException); + XCTAssertEqualObjects(properties, expectedProperties); + XCTAssertEqual([errorAttachmentLogs count], [attachments count]); + XCTAssertEqualObjects(errorAttachmentLogs[0], errorAttachmentLog1); + XCTAssertEqualObjects(errorAttachmentLogs[1], errorAttachmentLog2); + + // Verify the errorId returned by trackModelException is the same one that enqueued to the channel. + XCTAssertEqualObjects(actualErrorId, errorId); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACWrapperExceptionManagerTests.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACWrapperExceptionManagerTests.m new file mode 100644 index 0000000000..33ca34d234 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACWrapperExceptionManagerTests.m @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACCrashes.h" +#import "MSACException.h" +#import "MSACTestFrameworks.h" +#import "MSACWrapperException.h" +#import "MSACWrapperExceptionManagerInternal.h" + +// Copied from MSACWrapperExceptionManager.m +static NSString *const kMSACLastWrapperExceptionFileName = @"last_saved_wrapper_exception"; + +@interface MSACWrapperExceptionManagerTests : XCTestCase +@end + +// Expose private methods for use in tests +@interface MSACWrapperExceptionManager () + ++ (MSACWrapperException *)loadWrapperExceptionWithBaseFilename:(NSString *)baseFilename; + +@end + +@implementation MSACWrapperExceptionManagerTests + +#pragma mark - Housekeeping + +- (void)tearDown { + [super tearDown]; + [MSACWrapperExceptionManager deleteAllWrapperExceptions]; +} + +#pragma mark - Helper + +- (MSACException *)getModelException { + MSACException *exception = [[MSACException alloc] init]; + exception.message = @"a message"; + exception.type = @"a type"; + return exception; +} + +- (NSData *)getData { + return [@"some string" dataUsingEncoding:NSUTF8StringEncoding]; +} + +- (MSACWrapperException *)getWrapperException { + MSACWrapperException *wrapperException = [[MSACWrapperException alloc] init]; + wrapperException.modelException = [self getModelException]; + wrapperException.exceptionData = [self getData]; + wrapperException.processId = @(rand()); + return wrapperException; +} + +- (void)assertWrapperException:(MSACWrapperException *)wrapperException isEqualToOther:(MSACWrapperException *)other { + + // Test that the exceptions are the same. + assertThat(other.processId, equalTo(wrapperException.processId)); + assertThat(other.exceptionData, equalTo(wrapperException.exceptionData)); + assertThat(other.modelException, equalTo(wrapperException.modelException)); + + // The exception field. + assertThat(other.modelException.type, equalTo(wrapperException.modelException.type)); + assertThat(other.modelException.message, equalTo(wrapperException.modelException.message)); + assertThat(other.modelException.wrapperSdkName, equalTo(wrapperException.modelException.wrapperSdkName)); +} + +#pragma mark - Test + +- (void)testSaveAndLoadWrapperExceptionWorks { + + // If + MSACWrapperException *wrapperException = [self getWrapperException]; + + // When + [MSACWrapperExceptionManager saveWrapperException:wrapperException]; + MSACWrapperException *loadedException = + [MSACWrapperExceptionManager loadWrapperExceptionWithBaseFilename:kMSACLastWrapperExceptionFileName]; + + // Then + XCTAssertNotNil(loadedException); + [self assertWrapperException:wrapperException isEqualToOther:loadedException]; +} + +- (void)testSaveCorrelateWrapperExceptionWhenExists { + + // If + int numReports = 4; + NSMutableArray *mockReports = [NSMutableArray new]; + for (int i = 0; i < numReports; ++i) { + id reportMock = OCMPartialMock([MSACErrorReport new]); + OCMStub([reportMock appProcessIdentifier]).andReturn(i); + OCMStub([reportMock incidentIdentifier]).andReturn([[NSUUID UUID] UUIDString]); + [mockReports addObject:reportMock]; + } + MSACErrorReport *report = mockReports[(NSUInteger)(rand() % numReports)]; + MSACWrapperException *wrapperException = [self getWrapperException]; + wrapperException.processId = @([report appProcessIdentifier]); + + // When + [MSACWrapperExceptionManager saveWrapperException:wrapperException]; + [MSACWrapperExceptionManager correlateLastSavedWrapperExceptionToReport:mockReports]; + MSACWrapperException *loadedException = [MSACWrapperExceptionManager loadWrapperExceptionWithUUIDString:[report incidentIdentifier]]; + + // Then + XCTAssertNotNil(loadedException); + [self assertWrapperException:wrapperException isEqualToOther:loadedException]; +} + +- (void)testSaveCorrelateWrapperExceptionWhenNotExists { + + // If + MSACWrapperException *wrapperException = [self getWrapperException]; + wrapperException.processId = @4; + NSMutableArray *mockReports = [NSMutableArray new]; + id reportMock = OCMPartialMock([MSACErrorReport new]); + OCMStub([reportMock appProcessIdentifier]).andReturn(9); + NSString *uuidString = [[NSUUID UUID] UUIDString]; + OCMStub([reportMock incidentIdentifier]).andReturn(uuidString); + [mockReports addObject:reportMock]; + + // When + [MSACWrapperExceptionManager saveWrapperException:wrapperException]; + [MSACWrapperExceptionManager correlateLastSavedWrapperExceptionToReport:mockReports]; + MSACWrapperException *loadedException = [MSACWrapperExceptionManager loadWrapperExceptionWithUUIDString:uuidString]; + + // Then + XCTAssertNil(loadedException); +} + +@end diff --git a/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACWrapperExceptionTests.m b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACWrapperExceptionTests.m new file mode 100644 index 0000000000..469fca86a3 --- /dev/null +++ b/submodules/AppCenter-sdk/AppCenterCrashes/AppCenterCrashesTests/MSACWrapperExceptionTests.m @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSACException.h" +#import "MSACTestFrameworks.h" +#import "MSACWrapperExceptionInternal.h" + +@interface MSACWrapperExceptionTests : XCTestCase + +@property(nonatomic) MSACWrapperException *sut; + +@end + +@implementation MSACWrapperExceptionTests + +#pragma mark - Housekeeping + +- (void)setUp { + [super setUp]; + self.sut = [self wrapperException]; +} + +#pragma mark - Helper + +- (MSACWrapperException *)wrapperException { + MSACWrapperException *exception = [MSACWrapperException new]; + exception.processId = @4; + exception.exceptionData = [@"data string" dataUsingEncoding:NSUTF8StringEncoding]; + exception.modelException = [[MSACException alloc] init]; + exception.modelException.type = @"type"; + exception.modelException.message = @"message"; + exception.modelException.wrapperSdkName = @"wrapper sdk name"; + return exception; +} + +#pragma mark - Tests + +- (void)testInitializationWorks { + XCTAssertNotNil(self.sut); +} + +- (void)testSerializationToDictionaryWorks { + NSDictionary *actual = [self.sut serializeToDictionary]; + XCTAssertNotNil(actual); + assertThat(actual[@"processId"], equalTo(self.sut.processId)); + assertThat(actual[@"exceptionData"], equalTo(self.sut.exceptionData)); + + // Exception fields. + NSDictionary *exceptionDictionary = actual[@"modelException"]; + XCTAssertNotNil(exceptionDictionary); + assertThat(exceptionDictionary[@"type"], equalTo(self.sut.modelException.type)); + assertThat(exceptionDictionary[@"message"], equalTo(self.sut.modelException.message)); + assertThat(exceptionDictionary[@"wrapperSdkName"], equalTo(self.sut.modelException.wrapperSdkName)); +} + +- (void)testNSCodingSerializationAndDeserializationWorks { + + // When + NSData *serializedWrapperException = [MSACUtility archiveKeyedData:self.sut]; + id actual = [MSACUtility unarchiveKeyedData:serializedWrapperException]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual, instanceOf([MSACWrapperException class])); + + // The MSACAppleErrorLog. + MSACWrapperException *actualWrapperException = actual; + assertThat(actualWrapperException.processId, equalTo(self.sut.processId)); + assertThat(actualWrapperException.exceptionData, equalTo(self.sut.exceptionData)); + + // The exception field. + assertThat(actualWrapperException.modelException.type, equalTo(self.sut.modelException.type)); + assertThat(actualWrapperException.modelException.message, equalTo(self.sut.modelException.message)); + assertThat(actualWrapperException.modelException.wrapperSdkName, equalTo(self.sut.modelException.wrapperSdkName)); +} + +@end diff --git a/submodules/AppCenter-sdk/Config/App.xcconfig b/submodules/AppCenter-sdk/Config/App.xcconfig new file mode 100644 index 0000000000..dea51e1c77 --- /dev/null +++ b/submodules/AppCenter-sdk/Config/App.xcconfig @@ -0,0 +1,48 @@ +#include "./Version.xcconfig" + +// :Mark: Deployment +IPHONEOS_DEPLOYMENT_TARGET = 9.0 +TVOS_DEPLOYMENT_TARGET = 11.0 +MACOSX_DEPLOYMENT_TARGET = 10.9 +WATCHOS_DEPLOYMENT_TARGET = 3.2 + +// :Mark: Build Options +DEBUG_INFORMATION_FORMAT_Debug = dwarf +DEBUG_INFORMATION_FORMAT_Release = dwarf-with-dsym +DEBUG_INFORMATION_FORMAT = $(DEBUG_INFORMATION_FORMAT_$(CONFIGURATION)) + +// :Mark: Architectures +ONLY_ACTIVE_ARCH_Debug = YES +ONLY_ACTIVE_ARCH_Release = NO +ONLY_ACTIVE_ARCH = $(ONLY_ACTIVE_ARCH_$(CONFIGURATION)) + +// :Mark: Configuration of warnings. +CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE +CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES +CLANG_WARN__DUPLICATE_METHOD_MATCH = 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 +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 diff --git a/submodules/AppCenter-sdk/Config/Framework.xcconfig b/submodules/AppCenter-sdk/Config/Framework.xcconfig new file mode 100644 index 0000000000..a4e013870a --- /dev/null +++ b/submodules/AppCenter-sdk/Config/Framework.xcconfig @@ -0,0 +1,10 @@ +DEFINES_MODULE = YES + +VERSION_INFO_PREFIX = "" +VERSIONING_SYSTEM = "apple-generic" +CURRENT_PROJECT_VERSION = 1 +INSTALL_PATH = $(LOCAL_LIBRARY_DIR)/Frameworks + +MODULEMAP_FILE = $(SRCROOT)/$(PROJECT_NAME)/Support/$(APPCENTER_BUILD_PLATFORM).modulemap +INFOPLIST_FILE = $(PROJECT_NAME)/Support/Info.plist +MACH_O_TYPE = staticlib diff --git a/submodules/AppCenter-sdk/Config/Global Debug.xcconfig b/submodules/AppCenter-sdk/Config/Global Debug.xcconfig new file mode 100644 index 0000000000..b6a53571fe --- /dev/null +++ b/submodules/AppCenter-sdk/Config/Global Debug.xcconfig @@ -0,0 +1,21 @@ +#include "./Global.xcconfig" + +// :Mark: Architectures +ONLY_ACTIVE_ARCH = YES + +// :Mark: Build Options +DEBUG_INFORMATION_FORMAT = dwarf +ENABLE_TESTABILITY = YES +VALIDATE_PRODUCT = NO + +// :Mark: Apple LLVM 8.1 - Code Generation +GCC_OPTIMIZATION_LEVEL = 0 + +// :Mark: Apple LLVM 8.1 - Preprocessing +ENABLE_NS_ASSERTIONS = YES + +// :Mark: User-Defined +MTL_ENABLE_DEBUG_INFO = YES + +// :Mark: GCC_PREPROCESSOR_DEFINITIONS +GCC_PREPROCESSOR_DEFINITIONS[config=Debug] = $(inherited) DEBUG diff --git a/submodules/AppCenter-sdk/Config/Global Release.xcconfig b/submodules/AppCenter-sdk/Config/Global Release.xcconfig new file mode 100644 index 0000000000..3a5e9a8d46 --- /dev/null +++ b/submodules/AppCenter-sdk/Config/Global Release.xcconfig @@ -0,0 +1,26 @@ +#include "./Global.xcconfig" + +// :Mark: Architectures +ONLY_ACTIVE_ARCH = NO + +// :Mark: Build Options +DEBUG_INFORMATION_FORMAT = dwarf-with-dsym +ENABLE_TESTABILITY = NO +VALIDATE_PRODUCT = YES +BITCODE_GENERATION_MODE = bitcode + +// :Mark: Apple LLVM 8.1 - Code Generation +GCC_OPTIMIZATION_LEVEL = s + +// :Mark: Apple LLVM 8.1 - Preprocessing +ENABLE_NS_ASSERTIONS = NO + +// :Mark: User-Defined +MTL_ENABLE_DEBUG_INFO = NO + +// :Mark: Language - Modules +// Avoid dSYM warnings during build macOS application in release mode. +CLANG_ENABLE_MODULE_DEBUGGING[sdk=macosx*] = NO + +// Build without code-coverage to avoid 'undefined symbols' issue. +CLANG_ENABLE_CODE_COVERAGE = NO diff --git a/submodules/AppCenter-sdk/Config/Global.xcconfig b/submodules/AppCenter-sdk/Config/Global.xcconfig new file mode 100644 index 0000000000..afffd4665b --- /dev/null +++ b/submodules/AppCenter-sdk/Config/Global.xcconfig @@ -0,0 +1,159 @@ +#include "./Version.xcconfig" + +// :Mark: Architectures +SDKROOT = iphoneos + +// :Mark: Deployment +IPHONEOS_DEPLOYMENT_TARGET = 9.0 +TVOS_DEPLOYMENT_TARGET = 11.0 +MACOSX_DEPLOYMENT_TARGET = 10.9 +COPY_PHASE_STRIP = NO + +// :Mark: Packaging +PRODUCT_NAME = $(PROJECT_NAME) + +// :Mark: Language configurations +CLANG_ENABLE_MODULES = NO +CLANG_ENABLE_OBJC_ARC = YES +CLANG_CXX_LIBRARY = libc++ +CLANG_CXX_LANGUAGE_STANDARD = gnu++14 +GCC_C_LANGUAGE_STANDARD = gnu99 + +// :Mark: Search paths +ALWAYS_SEARCH_USER_PATHS = NO +USER_HEADER_SEARCH_PATHS = "$(SRCROOT)/../AppCenter/AppCenter"/** + +// :Mark: Code signing +CODE_SIGN_IDENTITY = + +// :Mark: Linking +DEAD_CODE_STRIPPING = NO + +// :Mark: Code generation +GCC_NO_COMMON_BLOCKS = YES + +// :Mark: Preprocessing +ENABLE_STRICT_OBJC_MSGSEND = YES + +// :Mark: OTHER_CFLAGS +GLOBAL_CFLAGS = -Wshorten-64-to-32 -Wall -fstack-protector-strong -fpie +OTHER_CFLAGS = $(GLOBAL_CFLAGS) + +// :Mark: Extension API Only +// Make sure we don't use API that are not available in extensions. +// See https://pewpewthespells.com/blog/buildsettings.html for info about the flag. +APPLICATION_EXTENSION_API_ONLY = YES + +// :Mark: GCC_PREPROCESSOR_DEFINITIONS +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) APP_CENTER_C_VERSION="\""$(VERSION_STRING)"\"" APP_CENTER_C_BUILD="\""$(BUILD_NUMBER)"\"" + +// :Mark: Configuration of warnings. We're listing every warning that we disabled and explain the reason why. +// +// -everything +// We want the best possible diagnostics, so we simply enable everything that exists, and then opt–out of what doesn’t make sense for us. + +// -objc-missing-property-synthesis +// This isn’t a real issue since we don’t have any interest in building on extremely old Clangs. +// (Also, each property that cannot be auto–synthesized triggers at least a warning…) + +// -float-equal +// While we could change all 90 instances of floating point comparison to a method that includes an epsilon, this is overkill and not needed. +// See http://stackoverflow.com/questions/11421756/weverything-yielding-comparing-floating-point-with-or-is-unsafe + +// -pedantic +// Generates too much noise. + +// -padded +// This isn’t really an issue to us, since we’re not programming embedded systems. + +// -c++98 +// We don't want to compile our code for C++98, no need to be warned about incompatibility. + +// -c++98-compat-pedantic +// We don't want to compile our code for C++98, no need to be warned about incompatibility. + +// -auto-import +// Standard ``import`` is used by tons of files and C++ code limits the possibility to use @import, so we don't. => Disabled. + +// -assign-enum +// A lot of api use enums as params but Apple's docs suggest passing in 0 which causes annoying warnings. + +// -exit-time-destructors +// Global destructors are obvious, no need to warn about them. + +// -global-constructors +// Global constructors are obvious, no need to warn about them. + +// -cast-align +// We're not interested in this one as the Mach-O format is itself well-aligned and the original memory allocation +// happens through malloc() and mmap() which always return at least 16byte alignment. +// Read more about alignment in the c++ reference: http://en.cppreference.com/w/cpp/language/object#Alignment. + +WARNING_CFLAGS = -Weverything -Wno-objc-missing-property-synthesis -Wno-float-equal -Wno-pedantic -Wno-padded -Wno-sign-conversion -Wno-c++98-compat -Wno-c++98-compat-pedantic -Wno-auto-import -Wno-assign-enum -Wno-exit-time-destructors -Wno-global-constructors -Wno-cast-align + +// These are all partially (but not completely?) independent of WARNING_CFLAGS +// and need to be specified explicitly. +GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES +GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES +GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR +GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES +GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES +GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES +GCC_WARN_MISSING_PARENTHESES = YES +GCC_WARN_CHECK_SWITCH_STATEMENTS = YES +GCC_WARN_UNUSED_FUNCTION = YES +GCC_WARN_UNUSED_LABEL = YES +GCC_WARN_UNUSED_PARAMETER = YES +GCC_WARN_UNUSED_VARIABLE = YES +GCC_WARN_UNUSED_VALUE = YES +GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE +GCC_WARN_UNKNOWN_PRAGMAS = YES +GCC_WARN_SHADOW = YES +GCC_WARN_FOUR_CHARACTER_CONSTANTS = YES +GCC_WARN_64_TO_32_BIT_CONVERSION = YES +GCC_WARN_ABOUT_MISSING_NEWLINE = YES +GCC_WARN_UNDECLARED_SELECTOR = YES +GCC_WARN_ABOUT_INVALID_OFFSETOF_MACRO = YES +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES +CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES +CLANG_WARN_BOOL_CONVERSION = YES +CLANG_WARN_COMMA = YES +CLANG_WARN_CONSTANT_CONVERSION = YES +CLANG_WARN_CXX0X_EXTENSIONS = YES +CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES +CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR +CLANG_WARN_EMPTY_BODY = YES +CLANG_WARN_ENUM_CONVERSION = YES +CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES +CLANG_WARN_INFINITE_RECURSION = YES +CLANG_WARN_INT_CONVERSION = YES +CLANG_WARN_OBJC_EXPLICIT_OWNERSHIP_TYPE = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR +CLANG_WARN_RANGE_LOOP_ANALYSIS = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES +CLANG_WARN_SUSPICIOUS_MOVE = YES +CLANG_WARN_UNREACHABLE_CODE = YES +CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = YES +CLANG_WARN_OBJC_INTERFACE_IVARS = YES + +// Enable extra analyze modes +CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES +CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES +CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES +CLANG_ANALYZER_NONNULL = YES +CLANG_ANALYZER_GCD_PERFORMANCE = YES +CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES + +// Enable warnings-are-errors for all modes. Don't do this just yet. +SWIFT_TREAT_WARNINGS_AS_ERRORS = YES +GCC_TREAT_WARNINGS_AS_ERRORS = YES + +// Swift Compiler - Code Generation +SWIFT_ENFORCE_EXCLUSIVE_ACCESS = on + +// Option for xcframework +BUILD_LIBRARIES_FOR_DISTRIBUTION = YES diff --git a/submodules/AppCenter-sdk/Config/Tests iOS.xcconfig b/submodules/AppCenter-sdk/Config/Tests iOS.xcconfig new file mode 100644 index 0000000000..7fb6ae0c44 --- /dev/null +++ b/submodules/AppCenter-sdk/Config/Tests iOS.xcconfig @@ -0,0 +1,13 @@ +#include "./Tests.xcconfig" +#include "./iOS.xcconfig" + +OTHER_LDFLAGS = $(OTHER_LDFLAGS) -framework AuthenticationServices +OTHER_LDFLAGS = $(OTHER_LDFLAGS) -framework CoreTelephony +OTHER_LDFLAGS = $(OTHER_LDFLAGS) -framework SafariServices +OTHER_LDFLAGS = $(OTHER_LDFLAGS) -framework UIKit +OTHER_LDFLAGS = $(OTHER_LDFLAGS) -framework WebKit + +LD_RUNPATH_SEARCH_PATHS = @executable_path/Frameworks @loader_path/Frameworks + +// OCMock and OCHamcrest have a bunch of warnings so we just disable this only for testing. +GCC_TREAT_WARNINGS_AS_ERRORS = NO diff --git a/submodules/AppCenter-sdk/Config/Tests macOS.xcconfig b/submodules/AppCenter-sdk/Config/Tests macOS.xcconfig new file mode 100644 index 0000000000..189e98efb2 --- /dev/null +++ b/submodules/AppCenter-sdk/Config/Tests macOS.xcconfig @@ -0,0 +1,12 @@ +#include "./Tests.xcconfig" +#include "./macOS.xcconfig" + +OTHER_LDFLAGS = $(OTHER_LDFLAGS) -framework AppKit +OTHER_LDFLAGS = $(OTHER_LDFLAGS) -framework GSS +OTHER_LDFLAGS = $(OTHER_LDFLAGS) -framework IOKit +OTHER_LDFLAGS = $(OTHER_LDFLAGS) -framework SecurityInterface + +LD_RUNPATH_SEARCH_PATHS = @executable_path/Frameworks @loader_path/Frameworks $(TOOLCHAIN_DIR)/usr/lib/swift/macosx + +// OCMock and OCHamcrest have a bunch of warnings so we just disable this only for testing. +GCC_TREAT_WARNINGS_AS_ERRORS = NO diff --git a/submodules/AppCenter-sdk/Config/Tests tvOS.xcconfig b/submodules/AppCenter-sdk/Config/Tests tvOS.xcconfig new file mode 100644 index 0000000000..f1452420cb --- /dev/null +++ b/submodules/AppCenter-sdk/Config/Tests tvOS.xcconfig @@ -0,0 +1,13 @@ +#include "./Tests.xcconfig" +#include "./tvOS.xcconfig" + +OTHER_LDFLAGS = $(OTHER_LDFLAGS) -framework UIKit + +LD_RUNPATH_SEARCH_PATHS = @executable_path/Frameworks @loader_path/Frameworks + +// OCMock and OCHamcrest have a bunch of warnings so we just disable this only for testing. +GCC_TREAT_WARNINGS_AS_ERRORS = NO + +// OCHTTPStubs for tvOS cannot generate code coverage data without these flags. +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES +DEFINES_MODULE = YES diff --git a/submodules/AppCenter-sdk/Config/Tests.xcconfig b/submodules/AppCenter-sdk/Config/Tests.xcconfig new file mode 100644 index 0000000000..6598b26339 --- /dev/null +++ b/submodules/AppCenter-sdk/Config/Tests.xcconfig @@ -0,0 +1,18 @@ +FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../Vendor/$(APPCENTER_BUILD_PLATFORM)"/** +HEADER_SEARCH_PATHS = "$(SRCROOT)/../Vendor/$(APPCENTER_BUILD_PLATFORM)"/** "$(SRCROOT)/../Vendor/SQLite3" +LIBRARY_SEARCH_PATHS = "$(SRCROOT)/../Vendor/$(APPCENTER_BUILD_PLATFORM)"/** + +APPLICATION_EXTENSION_API_ONLY = NO +INFOPLIST_FILE = $(PROJECT_NAME)Tests/Info.plist + +// OCMock assumes the use of 'unqualified id' +WARNING_CFLAGS = -Wno-objc-messaging-id + +OTHER_CFLAGS = $(inherited) -iframework "$(PLATFORM_DIR)/Developer/Library/Frameworks" + +OTHER_LDFLAGS = $(inherited) -ObjC -lz +OTHER_LDFLAGS = $(OTHER_LDFLAGS) -framework Foundation +OTHER_LDFLAGS = $(OTHER_LDFLAGS) -framework Security +OTHER_LDFLAGS = $(OTHER_LDFLAGS) -framework SystemConfiguration + +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES diff --git a/submodules/AppCenter-sdk/Config/Version.xcconfig b/submodules/AppCenter-sdk/Config/Version.xcconfig new file mode 100644 index 0000000000..18ef34bf20 --- /dev/null +++ b/submodules/AppCenter-sdk/Config/Version.xcconfig @@ -0,0 +1,2 @@ +BUILD_NUMBER = 1 +VERSION_STRING = 4.0.1 diff --git a/submodules/AppCenter-sdk/Config/XCFramework.xcconfig b/submodules/AppCenter-sdk/Config/XCFramework.xcconfig new file mode 100644 index 0000000000..430cc43c00 --- /dev/null +++ b/submodules/AppCenter-sdk/Config/XCFramework.xcconfig @@ -0,0 +1 @@ +EFFECTIVE_PLATFORM_NAME = -xcframework \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Config/iOS Universal.xcconfig b/submodules/AppCenter-sdk/Config/iOS Universal.xcconfig new file mode 100644 index 0000000000..63bf6be2a3 --- /dev/null +++ b/submodules/AppCenter-sdk/Config/iOS Universal.xcconfig @@ -0,0 +1,3 @@ +#include "./iOS.xcconfig" + +EFFECTIVE_PLATFORM_NAME = -iphoneuniversal diff --git a/submodules/AppCenter-sdk/Config/iOS.xcconfig b/submodules/AppCenter-sdk/Config/iOS.xcconfig new file mode 100644 index 0000000000..ee96de5f7f --- /dev/null +++ b/submodules/AppCenter-sdk/Config/iOS.xcconfig @@ -0,0 +1,11 @@ +SDKROOT = iphoneos +TARGETED_DEVICE_FAMILY = 1,2 +SUPPORTS_MACCATALYST = YES +ARCHS[sdk=iphoneos*] = $(ARCHS_STANDARD) armv7s arm64e +SKIP_INSTALL = YES + +OTHER_CFLAGS = $(inherited) -fembed-bitcode-marker +OTHER_CFLAGS[config=Release][sdk=iphoneos*] = $(GLOBAL_CFLAGS) -fembed-bitcode +OTHER_CFLAGS[sdk=macosx*] = $(GLOBAL_CFLAGS) + +APPCENTER_BUILD_PLATFORM = iOS diff --git a/submodules/AppCenter-sdk/Config/macOS.xcconfig b/submodules/AppCenter-sdk/Config/macOS.xcconfig new file mode 100644 index 0000000000..1bf506f05c --- /dev/null +++ b/submodules/AppCenter-sdk/Config/macOS.xcconfig @@ -0,0 +1,4 @@ +SDKROOT = macosx +SKIP_INSTALL = YES + +APPCENTER_BUILD_PLATFORM = macOS diff --git a/submodules/AppCenter-sdk/Config/tvOS Universal.xcconfig b/submodules/AppCenter-sdk/Config/tvOS Universal.xcconfig new file mode 100644 index 0000000000..a9901d9cf2 --- /dev/null +++ b/submodules/AppCenter-sdk/Config/tvOS Universal.xcconfig @@ -0,0 +1,3 @@ +#include "./tvOS.xcconfig" + +EFFECTIVE_PLATFORM_NAME = -appletvuniversal diff --git a/submodules/AppCenter-sdk/Config/tvOS.xcconfig b/submodules/AppCenter-sdk/Config/tvOS.xcconfig new file mode 100644 index 0000000000..1ffccc4370 --- /dev/null +++ b/submodules/AppCenter-sdk/Config/tvOS.xcconfig @@ -0,0 +1,8 @@ +SDKROOT = appletvos +SKIP_INSTALL = YES +TARGETED_DEVICE_FAMILY = 3 + +OTHER_CFLAGS = $(inherited) -fembed-bitcode-marker +OTHER_CFLAGS[config=Release][sdk=appletvos*] = $(GLOBAL_CFLAGS) -fembed-bitcode + +APPCENTER_BUILD_PLATFORM = tvOS diff --git a/submodules/AppCenter-sdk/Config/watchOS.xcconfig b/submodules/AppCenter-sdk/Config/watchOS.xcconfig new file mode 100644 index 0000000000..2ece28fb3b --- /dev/null +++ b/submodules/AppCenter-sdk/Config/watchOS.xcconfig @@ -0,0 +1,5 @@ +SDKROOT = watchos +SKIP_INSTALL = YES +TARGETED_DEVICE_FAMILY = 4 +ARCHS = $(ARCHS_STANDARD) +OTHER_CFLAGS = $(inherited) -fgnu-inline-asm diff --git a/submodules/AppCenter-sdk/CrashLib/Config/CrashLib Debug.xcconfig b/submodules/AppCenter-sdk/CrashLib/Config/CrashLib Debug.xcconfig new file mode 100644 index 0000000000..b45579abe0 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/Config/CrashLib Debug.xcconfig @@ -0,0 +1,4 @@ +#include "./CrashLib.xcconfig" + +ENABLE_TESTABILITY = YES +COPY_PHASE_STRIP = NO diff --git a/submodules/AppCenter-sdk/CrashLib/Config/CrashLib Release.xcconfig b/submodules/AppCenter-sdk/CrashLib/Config/CrashLib Release.xcconfig new file mode 100644 index 0000000000..e04989cbd9 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/Config/CrashLib Release.xcconfig @@ -0,0 +1 @@ +#include "./CrashLib.xcconfig" diff --git a/submodules/AppCenter-sdk/CrashLib/Config/CrashLib.xcconfig b/submodules/AppCenter-sdk/CrashLib/Config/CrashLib.xcconfig new file mode 100644 index 0000000000..fe59bcc3d0 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/Config/CrashLib.xcconfig @@ -0,0 +1,20 @@ +#include "../../Config/App.xcconfig" + +PRODUCT_NAME = $(TARGET_NAME) + +DYLIB_COMPATIBILITY_VERSION = 1 +DYLIB_CURRENT_VERSION = 1 +DYLIB_INSTALL_NAME_BASE = @rpath +INSTALL_PATH = $(LOCAL_LIBRARY_DIR)/Frameworks + +ALWAYS_SEARCH_USER_PATHS = NO +CLANG_ENABLE_OBJC_ARC = YES +ENABLE_STRICT_OBJC_MSGSEND = YES +DEFINES_MODULE = YES +LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks +SWIFT_OBJC_BRIDGING_HEADER = CrashLib/CrashLib-Bridging-Header.h +SWIFT_VERSION = 4.2 +SWIFT_COMPILATION_MODE = wholemodule +GLOBAL_CFLAGS = -fno-optimize-sibling-calls +OTHER_CFLAGS = $(GLOBAL_CFLAGS) +GCC_NO_COMMON_BLOCKS = YES diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib.xcodeproj/project.pbxproj b/submodules/AppCenter-sdk/CrashLib/CrashLib.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..2284f47084 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib.xcodeproj/project.pbxproj @@ -0,0 +1,1121 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 789FEDBD23E05B630026D59E /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 789FEDBC23E05B630026D59E /* Cocoa.framework */; }; + 78FBB4A8239FAD0100ADFA1F /* MSCrashDelayedObjCException.h in Headers */ = {isa = PBXBuildFile; fileRef = 78FBB4A6239FAD0000ADFA1F /* MSCrashDelayedObjCException.h */; }; + 78FBB4A9239FAD0100ADFA1F /* MSCrashDelayedObjCException.m in Sources */ = {isa = PBXBuildFile; fileRef = 78FBB4A7239FAD0100ADFA1F /* MSCrashDelayedObjCException.m */; }; + 801C25311F04001300F4859B /* CrashLib-Bridging-Header.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187A21DD2A7F900D820D8 /* CrashLib-Bridging-Header.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8065B5801F4C4024004FF622 /* CrashLib.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187B61DD2A7F900D820D8 /* CrashLib.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8065B5811F4C4024004FF622 /* MSCrash.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187891DD2A7F900D820D8 /* MSCrash.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8065B5821F4C4024004FF622 /* MSCrash.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187971DD2A7F900D820D8 /* MSCrash.m */; }; + 8065B5831F4C4024004FF622 /* MSCrashAbort.h in Headers */ = {isa = PBXBuildFile; fileRef = B241878D1DD2A7F900D820D8 /* MSCrashAbort.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8065B5841F4C4024004FF622 /* MSCrashAbort.m in Sources */ = {isa = PBXBuildFile; fileRef = B241879B1DD2A7F900D820D8 /* MSCrashAbort.m */; }; + 8065B5851F4C4024004FF622 /* MSCrashAsyncSafeThread.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187B81DD2A7F900D820D8 /* MSCrashAsyncSafeThread.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8065B5861F4C4024004FF622 /* MSCrashAsyncSafeThread.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187B31DD2A7F900D820D8 /* MSCrashAsyncSafeThread.m */; }; + 8065B5871F4C4024004FF622 /* MSCrashCXXException.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187B01DD2A7F900D820D8 /* MSCrashCXXException.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8065B5881F4C4024004FF622 /* MSCrashCXXException.mm in Sources */ = {isa = PBXBuildFile; fileRef = B24187AF1DD2A7F900D820D8 /* MSCrashCXXException.mm */; }; + 8065B5891F4C4024004FF622 /* MSCrashCorruptMalloc.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187A81DD2A7F900D820D8 /* MSCrashCorruptMalloc.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8065B58A1F4C4024004FF622 /* MSCrashCorruptMalloc.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187A51DD2A7F900D820D8 /* MSCrashCorruptMalloc.m */; }; + 8065B58B1F4C4024004FF622 /* MSCrashCorruptObjC.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187901DD2A7F900D820D8 /* MSCrashCorruptObjC.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8065B58C1F4C4024004FF622 /* MSCrashCorruptObjC.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187991DD2A7F900D820D8 /* MSCrashCorruptObjC.m */; }; + 8065B58D1F4C4024004FF622 /* MSCrashNULL.h in Headers */ = {isa = PBXBuildFile; fileRef = B241878C1DD2A7F900D820D8 /* MSCrashNULL.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8065B58E1F4C4024004FF622 /* MSCrashNULL.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187A01DD2A7F900D820D8 /* MSCrashNULL.m */; }; + 8065B58F1F4C4024004FF622 /* MSCrashNSLog.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187AA1DD2A7F900D820D8 /* MSCrashNSLog.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8065B5901F4C4024004FF622 /* MSCrashNSLog.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187931DD2A7F900D820D8 /* MSCrashNSLog.m */; }; + 8065B5911F4C4024004FF622 /* MSCrashFramelessDWARF.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187BA1DD2A7F900D820D8 /* MSCrashFramelessDWARF.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8065B5921F4C4024004FF622 /* MSCrashFramelessDWARF.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187BB1DD2A7F900D820D8 /* MSCrashFramelessDWARF.m */; }; + 8065B5931F4C4024004FF622 /* MSCrashGarbage.h in Headers */ = {isa = PBXBuildFile; fileRef = B241878E1DD2A7F900D820D8 /* MSCrashGarbage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8065B5941F4C4024004FF622 /* MSCrashGarbage.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187AD1DD2A7F900D820D8 /* MSCrashGarbage.m */; }; + 8065B5951F4C4024004FF622 /* MSCrashNXPage.h in Headers */ = {isa = PBXBuildFile; fileRef = B241879A1DD2A7F900D820D8 /* MSCrashNXPage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8065B5961F4C4024004FF622 /* MSCrashNXPage.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187AE1DD2A7F900D820D8 /* MSCrashNXPage.m */; }; + 8065B5971F4C4024004FF622 /* MSCrashObjCException.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187A71DD2A7F900D820D8 /* MSCrashObjCException.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8065B5981F4C4024004FF622 /* MSCrashObjCException.m in Sources */ = {isa = PBXBuildFile; fileRef = B241879D1DD2A7F900D820D8 /* MSCrashObjCException.m */; }; + 8065B5991F4C4024004FF622 /* MSCrashObjCMsgSend.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187911DD2A7F900D820D8 /* MSCrashObjCMsgSend.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8065B59A1F4C4024004FF622 /* MSCrashObjCMsgSend.m in Sources */ = {isa = PBXBuildFile; fileRef = B241879E1DD2A7F900D820D8 /* MSCrashObjCMsgSend.m */; }; + 8065B59B1F4C4024004FF622 /* MSCrashOverwriteLinkRegister.h in Headers */ = {isa = PBXBuildFile; fileRef = B241878F1DD2A7F900D820D8 /* MSCrashOverwriteLinkRegister.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8065B59C1F4C4024004FF622 /* MSCrashOverwriteLinkRegister.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187A11DD2A7F900D820D8 /* MSCrashOverwriteLinkRegister.m */; }; + 8065B59D1F4C4024004FF622 /* MSCrashPrivInst.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187951DD2A7F900D820D8 /* MSCrashPrivInst.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8065B59E1F4C4024004FF622 /* MSCrashPrivInst.m in Sources */ = {isa = PBXBuildFile; fileRef = B241879C1DD2A7F900D820D8 /* MSCrashPrivInst.m */; }; + 8065B59F1F4C4024004FF622 /* MSCrashReleasedObject.h in Headers */ = {isa = PBXBuildFile; fileRef = B241878A1DD2A7F900D820D8 /* MSCrashReleasedObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8065B5A01F4C4024004FF622 /* MSCrashReleasedObject.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187A31DD2A7F900D820D8 /* MSCrashReleasedObject.m */; }; + 8065B5A11F4C4024004FF622 /* MSCrashROPage.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187AB1DD2A7F900D820D8 /* MSCrashROPage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8065B5A21F4C4024004FF622 /* MSCrashROPage.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187A61DD2A7F900D820D8 /* MSCrashROPage.m */; }; + 8065B5A31F4C4024004FF622 /* MSCrashSmashStackBottom.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187A91DD2A7F900D820D8 /* MSCrashSmashStackBottom.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8065B5A41F4C4024004FF622 /* MSCrashSmashStackBottom.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187941DD2A7F900D820D8 /* MSCrashSmashStackBottom.m */; }; + 8065B5A51F4C4024004FF622 /* MSCrashSmashStackTop.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187B11DD2A7F900D820D8 /* MSCrashSmashStackTop.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8065B5A61F4C4024004FF622 /* MSCrashSmashStackTop.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187961DD2A7F900D820D8 /* MSCrashSmashStackTop.m */; }; + 8065B5A71F4C4024004FF622 /* MSCrashStackGuard.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187B21DD2A7F900D820D8 /* MSCrashStackGuard.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8065B5A81F4C4024004FF622 /* MSCrashStackGuard.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187B41DD2A7F900D820D8 /* MSCrashStackGuard.m */; }; + 8065B5A91F4C4024004FF622 /* MSCrashSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24187AC1DD2A7F900D820D8 /* MSCrashSwift.swift */; }; + 8065B5AA1F4C4024004FF622 /* MSCrashTrap.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187921DD2A7F900D820D8 /* MSCrashTrap.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8065B5AB1F4C4024004FF622 /* MSCrashTrap.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187981DD2A7F900D820D8 /* MSCrashTrap.m */; }; + 8065B5AC1F4C4024004FF622 /* MSCrashUndefInst.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187B51DD2A7F900D820D8 /* MSCrashUndefInst.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8065B5AD1F4C4024004FF622 /* MSCrashUndefInst.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187B71DD2A7F900D820D8 /* MSCrashUndefInst.m */; }; + 8065B5AE1F4C4024004FF622 /* MSFramelessDWARF_i386.s in Sources */ = {isa = PBXBuildFile; fileRef = B24187A41DD2A7F900D820D8 /* MSFramelessDWARF_i386.s */; }; + 8065B5AF1F4C4024004FF622 /* MSFramelessDWARF_arm32.S in Sources */ = {isa = PBXBuildFile; fileRef = B241878B1DD2A7F900D820D8 /* MSFramelessDWARF_arm32.S */; }; + 8065B5B01F4C4024004FF622 /* MSFramelessDWARF_x86_64.s in Sources */ = {isa = PBXBuildFile; fileRef = B24187B91DD2A7F900D820D8 /* MSFramelessDWARF_x86_64.s */; }; + 8065B5B11F4C4024004FF622 /* MSFramelessDWARF_arm64.s in Sources */ = {isa = PBXBuildFile; fileRef = B241879F1DD2A7F900D820D8 /* MSFramelessDWARF_arm64.s */; }; + 8065B5B21F4C4024004FF622 /* CrashLib-Bridging-Header.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187A21DD2A7F900D820D8 /* CrashLib-Bridging-Header.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 80F991291F0265C60040FAD7 /* MSCrash.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187891DD2A7F900D820D8 /* MSCrash.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 80F9912A1F0265C60040FAD7 /* MSCrash.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187971DD2A7F900D820D8 /* MSCrash.m */; }; + 80F9912B1F0265C60040FAD7 /* MSCrashAbort.h in Headers */ = {isa = PBXBuildFile; fileRef = B241878D1DD2A7F900D820D8 /* MSCrashAbort.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 80F9912C1F0265C60040FAD7 /* MSCrashAbort.m in Sources */ = {isa = PBXBuildFile; fileRef = B241879B1DD2A7F900D820D8 /* MSCrashAbort.m */; }; + 80F9912D1F0265C60040FAD7 /* MSCrashAsyncSafeThread.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187B81DD2A7F900D820D8 /* MSCrashAsyncSafeThread.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 80F9912E1F0265C60040FAD7 /* MSCrashAsyncSafeThread.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187B31DD2A7F900D820D8 /* MSCrashAsyncSafeThread.m */; }; + 80F9912F1F0265C60040FAD7 /* MSCrashCXXException.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187B01DD2A7F900D820D8 /* MSCrashCXXException.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 80F991301F0265C60040FAD7 /* MSCrashCXXException.mm in Sources */ = {isa = PBXBuildFile; fileRef = B24187AF1DD2A7F900D820D8 /* MSCrashCXXException.mm */; }; + 80F991311F0265C60040FAD7 /* MSCrashCorruptMalloc.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187A81DD2A7F900D820D8 /* MSCrashCorruptMalloc.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 80F991321F0265C60040FAD7 /* MSCrashCorruptMalloc.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187A51DD2A7F900D820D8 /* MSCrashCorruptMalloc.m */; }; + 80F991331F0265C60040FAD7 /* MSCrashCorruptObjC.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187901DD2A7F900D820D8 /* MSCrashCorruptObjC.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 80F991341F0265C60040FAD7 /* MSCrashCorruptObjC.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187991DD2A7F900D820D8 /* MSCrashCorruptObjC.m */; }; + 80F991351F0265C60040FAD7 /* MSCrashNULL.h in Headers */ = {isa = PBXBuildFile; fileRef = B241878C1DD2A7F900D820D8 /* MSCrashNULL.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 80F991361F0265C60040FAD7 /* MSCrashNULL.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187A01DD2A7F900D820D8 /* MSCrashNULL.m */; }; + 80F991371F0265C60040FAD7 /* MSCrashNSLog.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187AA1DD2A7F900D820D8 /* MSCrashNSLog.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 80F991381F0265C60040FAD7 /* MSCrashNSLog.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187931DD2A7F900D820D8 /* MSCrashNSLog.m */; }; + 80F991391F0265C60040FAD7 /* MSCrashFramelessDWARF.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187BA1DD2A7F900D820D8 /* MSCrashFramelessDWARF.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 80F9913A1F0265C60040FAD7 /* MSCrashFramelessDWARF.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187BB1DD2A7F900D820D8 /* MSCrashFramelessDWARF.m */; }; + 80F9913B1F0265C60040FAD7 /* MSCrashGarbage.h in Headers */ = {isa = PBXBuildFile; fileRef = B241878E1DD2A7F900D820D8 /* MSCrashGarbage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 80F9913C1F0265C60040FAD7 /* MSCrashGarbage.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187AD1DD2A7F900D820D8 /* MSCrashGarbage.m */; }; + 80F9913D1F0265C60040FAD7 /* MSCrashNXPage.h in Headers */ = {isa = PBXBuildFile; fileRef = B241879A1DD2A7F900D820D8 /* MSCrashNXPage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 80F9913E1F0265C60040FAD7 /* MSCrashNXPage.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187AE1DD2A7F900D820D8 /* MSCrashNXPage.m */; }; + 80F9913F1F0265C60040FAD7 /* MSCrashObjCException.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187A71DD2A7F900D820D8 /* MSCrashObjCException.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 80F991401F0265C60040FAD7 /* MSCrashObjCException.m in Sources */ = {isa = PBXBuildFile; fileRef = B241879D1DD2A7F900D820D8 /* MSCrashObjCException.m */; }; + 80F991411F0265C60040FAD7 /* MSCrashObjCMsgSend.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187911DD2A7F900D820D8 /* MSCrashObjCMsgSend.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 80F991421F0265C60040FAD7 /* MSCrashObjCMsgSend.m in Sources */ = {isa = PBXBuildFile; fileRef = B241879E1DD2A7F900D820D8 /* MSCrashObjCMsgSend.m */; }; + 80F991431F0265C60040FAD7 /* MSCrashOverwriteLinkRegister.h in Headers */ = {isa = PBXBuildFile; fileRef = B241878F1DD2A7F900D820D8 /* MSCrashOverwriteLinkRegister.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 80F991441F0265C60040FAD7 /* MSCrashOverwriteLinkRegister.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187A11DD2A7F900D820D8 /* MSCrashOverwriteLinkRegister.m */; }; + 80F991451F0265C60040FAD7 /* MSCrashPrivInst.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187951DD2A7F900D820D8 /* MSCrashPrivInst.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 80F991461F0265C60040FAD7 /* MSCrashPrivInst.m in Sources */ = {isa = PBXBuildFile; fileRef = B241879C1DD2A7F900D820D8 /* MSCrashPrivInst.m */; }; + 80F991471F0265C60040FAD7 /* MSCrashReleasedObject.h in Headers */ = {isa = PBXBuildFile; fileRef = B241878A1DD2A7F900D820D8 /* MSCrashReleasedObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 80F991481F0265C60040FAD7 /* MSCrashReleasedObject.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187A31DD2A7F900D820D8 /* MSCrashReleasedObject.m */; }; + 80F991491F0265C60040FAD7 /* MSCrashROPage.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187AB1DD2A7F900D820D8 /* MSCrashROPage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 80F9914A1F0265C60040FAD7 /* MSCrashROPage.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187A61DD2A7F900D820D8 /* MSCrashROPage.m */; }; + 80F9914B1F0265C60040FAD7 /* MSCrashSmashStackBottom.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187A91DD2A7F900D820D8 /* MSCrashSmashStackBottom.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 80F9914C1F0265C60040FAD7 /* MSCrashSmashStackBottom.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187941DD2A7F900D820D8 /* MSCrashSmashStackBottom.m */; }; + 80F9914D1F0265C60040FAD7 /* MSCrashSmashStackTop.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187B11DD2A7F900D820D8 /* MSCrashSmashStackTop.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 80F9914E1F0265C60040FAD7 /* MSCrashSmashStackTop.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187961DD2A7F900D820D8 /* MSCrashSmashStackTop.m */; }; + 80F9914F1F0265C60040FAD7 /* MSCrashStackGuard.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187B21DD2A7F900D820D8 /* MSCrashStackGuard.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 80F991501F0265C60040FAD7 /* MSCrashStackGuard.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187B41DD2A7F900D820D8 /* MSCrashStackGuard.m */; }; + 80F991511F0265C60040FAD7 /* MSCrashSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24187AC1DD2A7F900D820D8 /* MSCrashSwift.swift */; }; + 80F991521F0265C60040FAD7 /* MSCrashTrap.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187921DD2A7F900D820D8 /* MSCrashTrap.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 80F991531F0265C60040FAD7 /* MSCrashTrap.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187981DD2A7F900D820D8 /* MSCrashTrap.m */; }; + 80F991541F0265C60040FAD7 /* MSCrashUndefInst.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187B51DD2A7F900D820D8 /* MSCrashUndefInst.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 80F991551F0265C60040FAD7 /* MSCrashUndefInst.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187B71DD2A7F900D820D8 /* MSCrashUndefInst.m */; }; + 80F991561F0265C60040FAD7 /* MSFramelessDWARF_i386.s in Sources */ = {isa = PBXBuildFile; fileRef = B24187A41DD2A7F900D820D8 /* MSFramelessDWARF_i386.s */; }; + 80F991571F0265C60040FAD7 /* MSFramelessDWARF_arm32.S in Sources */ = {isa = PBXBuildFile; fileRef = B241878B1DD2A7F900D820D8 /* MSFramelessDWARF_arm32.S */; }; + 80F991581F0265C60040FAD7 /* MSFramelessDWARF_x86_64.s in Sources */ = {isa = PBXBuildFile; fileRef = B24187B91DD2A7F900D820D8 /* MSFramelessDWARF_x86_64.s */; }; + 80F991591F0265C60040FAD7 /* MSFramelessDWARF_arm64.s in Sources */ = {isa = PBXBuildFile; fileRef = B241879F1DD2A7F900D820D8 /* MSFramelessDWARF_arm64.s */; }; + 80F9915B1F02671A0040FAD7 /* CrashLib.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187B61DD2A7F900D820D8 /* CrashLib.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84D067B3244864DC000B708B /* MSCrashStackGuard.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187B41DD2A7F900D820D8 /* MSCrashStackGuard.m */; }; + 84D067B4244864DC000B708B /* MSFramelessDWARF_arm32.S in Sources */ = {isa = PBXBuildFile; fileRef = B241878B1DD2A7F900D820D8 /* MSFramelessDWARF_arm32.S */; }; + 84D067B5244864DC000B708B /* MSCrashFramelessDWARF.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187BB1DD2A7F900D820D8 /* MSCrashFramelessDWARF.m */; }; + 84D067B6244864DC000B708B /* MSCrashCorruptMalloc.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187A51DD2A7F900D820D8 /* MSCrashCorruptMalloc.m */; }; + 84D067B7244864DC000B708B /* MSCrashCXXCustomException.mm in Sources */ = {isa = PBXBuildFile; fileRef = C2B7DF5423DF355100243AC7 /* MSCrashCXXCustomException.mm */; }; + 84D067B8244864DC000B708B /* MSFramelessDWARF_x86_64.s in Sources */ = {isa = PBXBuildFile; fileRef = B24187B91DD2A7F900D820D8 /* MSFramelessDWARF_x86_64.s */; }; + 84D067B9244864DC000B708B /* MSCrashUndefInst.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187B71DD2A7F900D820D8 /* MSCrashUndefInst.m */; }; + 84D067BA244864DC000B708B /* MSCrashAbort.m in Sources */ = {isa = PBXBuildFile; fileRef = B241879B1DD2A7F900D820D8 /* MSCrashAbort.m */; }; + 84D067BB244864DC000B708B /* MSCrashROPage.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187A61DD2A7F900D820D8 /* MSCrashROPage.m */; }; + 84D067BC244864DC000B708B /* MSCrashGarbage.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187AD1DD2A7F900D820D8 /* MSCrashGarbage.m */; }; + 84D067BD244864DC000B708B /* MSFramelessDWARF_arm64.s in Sources */ = {isa = PBXBuildFile; fileRef = B241879F1DD2A7F900D820D8 /* MSFramelessDWARF_arm64.s */; }; + 84D067BE244864DC000B708B /* MSCrashCXXException.mm in Sources */ = {isa = PBXBuildFile; fileRef = B24187AF1DD2A7F900D820D8 /* MSCrashCXXException.mm */; }; + 84D067BF244864DC000B708B /* MSCrashSmashStackBottom.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187941DD2A7F900D820D8 /* MSCrashSmashStackBottom.m */; }; + 84D067C0244864DC000B708B /* MSCrashSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24187AC1DD2A7F900D820D8 /* MSCrashSwift.swift */; }; + 84D067C2244864DC000B708B /* MSCrash.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187971DD2A7F900D820D8 /* MSCrash.m */; }; + 84D067C3244864DC000B708B /* MSCrashTrap.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187981DD2A7F900D820D8 /* MSCrashTrap.m */; }; + 84D067C4244864DC000B708B /* MSCrashNULL.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187A01DD2A7F900D820D8 /* MSCrashNULL.m */; }; + 84D067C5244864DC000B708B /* MSCrashSmashStackTop.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187961DD2A7F900D820D8 /* MSCrashSmashStackTop.m */; }; + 84D067C6244864DC000B708B /* MSCrashObjCException.m in Sources */ = {isa = PBXBuildFile; fileRef = B241879D1DD2A7F900D820D8 /* MSCrashObjCException.m */; }; + 84D067C7244864DC000B708B /* MSFramelessDWARF_i386.s in Sources */ = {isa = PBXBuildFile; fileRef = B24187A41DD2A7F900D820D8 /* MSFramelessDWARF_i386.s */; }; + 84D067C8244864DC000B708B /* MSCrashNSLog.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187931DD2A7F900D820D8 /* MSCrashNSLog.m */; }; + 84D067C9244864DC000B708B /* MSCrashObjCMsgSend.m in Sources */ = {isa = PBXBuildFile; fileRef = B241879E1DD2A7F900D820D8 /* MSCrashObjCMsgSend.m */; }; + 84D067CA244864DC000B708B /* MSCrashOverwriteLinkRegister.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187A11DD2A7F900D820D8 /* MSCrashOverwriteLinkRegister.m */; }; + 84D067CB244864DC000B708B /* MSCrashPrivInst.m in Sources */ = {isa = PBXBuildFile; fileRef = B241879C1DD2A7F900D820D8 /* MSCrashPrivInst.m */; }; + 84D067CC244864DC000B708B /* MSCrashAsyncSafeThread.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187B31DD2A7F900D820D8 /* MSCrashAsyncSafeThread.m */; }; + 84D067CD244864DC000B708B /* MSCrashNXPage.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187AE1DD2A7F900D820D8 /* MSCrashNXPage.m */; }; + 84D067CE244864DC000B708B /* MSCrashCorruptObjC.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187991DD2A7F900D820D8 /* MSCrashCorruptObjC.m */; }; + 84D067CF244864DC000B708B /* MSCrashReleasedObject.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187A31DD2A7F900D820D8 /* MSCrashReleasedObject.m */; }; + 84D067D3244864DC000B708B /* MSCrashROPage.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187AB1DD2A7F900D820D8 /* MSCrashROPage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84D067D4244864DC000B708B /* MSCrashOverwriteLinkRegister.h in Headers */ = {isa = PBXBuildFile; fileRef = B241878F1DD2A7F900D820D8 /* MSCrashOverwriteLinkRegister.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84D067D5244864DC000B708B /* MSCrashUndefInst.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187B51DD2A7F900D820D8 /* MSCrashUndefInst.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84D067D6244864DC000B708B /* MSCrashCXXCustomException.h in Headers */ = {isa = PBXBuildFile; fileRef = C2B7DF5323DF355100243AC7 /* MSCrashCXXCustomException.h */; }; + 84D067D7244864DC000B708B /* MSCrashFramelessDWARF.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187BA1DD2A7F900D820D8 /* MSCrashFramelessDWARF.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84D067D8244864DC000B708B /* MSCrashAsyncSafeThread.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187B81DD2A7F900D820D8 /* MSCrashAsyncSafeThread.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84D067D9244864DC000B708B /* MSCrashCXXException.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187B01DD2A7F900D820D8 /* MSCrashCXXException.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84D067DA244864DC000B708B /* MSCrashCorruptObjC.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187901DD2A7F900D820D8 /* MSCrashCorruptObjC.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84D067DB244864DC000B708B /* CrashLib-Bridging-Header.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187A21DD2A7F900D820D8 /* CrashLib-Bridging-Header.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84D067DC244864DC000B708B /* MSCrashObjCException.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187A71DD2A7F900D820D8 /* MSCrashObjCException.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84D067DD244864DC000B708B /* MSCrashCorruptMalloc.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187A81DD2A7F900D820D8 /* MSCrashCorruptMalloc.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84D067DE244864DC000B708B /* MSCrashSmashStackBottom.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187A91DD2A7F900D820D8 /* MSCrashSmashStackBottom.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84D067DF244864DC000B708B /* MSCrashTrap.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187921DD2A7F900D820D8 /* MSCrashTrap.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84D067E0244864DC000B708B /* MSCrashPrivInst.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187951DD2A7F900D820D8 /* MSCrashPrivInst.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84D067E2244864DC000B708B /* MSCrashObjCMsgSend.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187911DD2A7F900D820D8 /* MSCrashObjCMsgSend.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84D067E3244864DC000B708B /* MSCrashNSLog.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187AA1DD2A7F900D820D8 /* MSCrashNSLog.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84D067E4244864DC000B708B /* MSCrash.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187891DD2A7F900D820D8 /* MSCrash.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84D067E5244864DC000B708B /* MSCrashGarbage.h in Headers */ = {isa = PBXBuildFile; fileRef = B241878E1DD2A7F900D820D8 /* MSCrashGarbage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84D067E6244864DC000B708B /* MSCrashAbort.h in Headers */ = {isa = PBXBuildFile; fileRef = B241878D1DD2A7F900D820D8 /* MSCrashAbort.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84D067E7244864DC000B708B /* MSCrashReleasedObject.h in Headers */ = {isa = PBXBuildFile; fileRef = B241878A1DD2A7F900D820D8 /* MSCrashReleasedObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84D067E8244864DC000B708B /* MSCrashNXPage.h in Headers */ = {isa = PBXBuildFile; fileRef = B241879A1DD2A7F900D820D8 /* MSCrashNXPage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84D067E9244864DC000B708B /* CrashLib.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187B61DD2A7F900D820D8 /* CrashLib.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84D067EA244864DC000B708B /* MSCrashStackGuard.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187B21DD2A7F900D820D8 /* MSCrashStackGuard.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84D067EB244864DC000B708B /* MSCrashSmashStackTop.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187B11DD2A7F900D820D8 /* MSCrashSmashStackTop.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 84D067EC244864DC000B708B /* MSCrashNULL.h in Headers */ = {isa = PBXBuildFile; fileRef = B241878C1DD2A7F900D820D8 /* MSCrashNULL.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B24187BC1DD2A7F900D820D8 /* MSCrash.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187891DD2A7F900D820D8 /* MSCrash.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B24187BD1DD2A7F900D820D8 /* MSCrashReleasedObject.h in Headers */ = {isa = PBXBuildFile; fileRef = B241878A1DD2A7F900D820D8 /* MSCrashReleasedObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B24187BE1DD2A7F900D820D8 /* MSFramelessDWARF_arm32.S in Sources */ = {isa = PBXBuildFile; fileRef = B241878B1DD2A7F900D820D8 /* MSFramelessDWARF_arm32.S */; }; + B24187BF1DD2A7F900D820D8 /* MSCrashNULL.h in Headers */ = {isa = PBXBuildFile; fileRef = B241878C1DD2A7F900D820D8 /* MSCrashNULL.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B24187C01DD2A7F900D820D8 /* MSCrashAbort.h in Headers */ = {isa = PBXBuildFile; fileRef = B241878D1DD2A7F900D820D8 /* MSCrashAbort.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B24187C11DD2A7F900D820D8 /* MSCrashGarbage.h in Headers */ = {isa = PBXBuildFile; fileRef = B241878E1DD2A7F900D820D8 /* MSCrashGarbage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B24187C21DD2A7F900D820D8 /* MSCrashOverwriteLinkRegister.h in Headers */ = {isa = PBXBuildFile; fileRef = B241878F1DD2A7F900D820D8 /* MSCrashOverwriteLinkRegister.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B24187C31DD2A7F900D820D8 /* MSCrashCorruptObjC.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187901DD2A7F900D820D8 /* MSCrashCorruptObjC.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B24187C41DD2A7F900D820D8 /* MSCrashObjCMsgSend.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187911DD2A7F900D820D8 /* MSCrashObjCMsgSend.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B24187C51DD2A7F900D820D8 /* MSCrashTrap.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187921DD2A7F900D820D8 /* MSCrashTrap.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B24187C61DD2A7F900D820D8 /* MSCrashNSLog.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187931DD2A7F900D820D8 /* MSCrashNSLog.m */; }; + B24187C71DD2A7F900D820D8 /* MSCrashSmashStackBottom.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187941DD2A7F900D820D8 /* MSCrashSmashStackBottom.m */; }; + B24187C81DD2A7F900D820D8 /* MSCrashPrivInst.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187951DD2A7F900D820D8 /* MSCrashPrivInst.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B24187C91DD2A7F900D820D8 /* MSCrashSmashStackTop.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187961DD2A7F900D820D8 /* MSCrashSmashStackTop.m */; }; + B24187CA1DD2A7F900D820D8 /* MSCrash.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187971DD2A7F900D820D8 /* MSCrash.m */; }; + B24187CB1DD2A7F900D820D8 /* MSCrashTrap.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187981DD2A7F900D820D8 /* MSCrashTrap.m */; }; + B24187CC1DD2A7F900D820D8 /* MSCrashCorruptObjC.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187991DD2A7F900D820D8 /* MSCrashCorruptObjC.m */; }; + B24187CD1DD2A7F900D820D8 /* MSCrashNXPage.h in Headers */ = {isa = PBXBuildFile; fileRef = B241879A1DD2A7F900D820D8 /* MSCrashNXPage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B24187CE1DD2A7F900D820D8 /* MSCrashAbort.m in Sources */ = {isa = PBXBuildFile; fileRef = B241879B1DD2A7F900D820D8 /* MSCrashAbort.m */; }; + B24187CF1DD2A7F900D820D8 /* MSCrashPrivInst.m in Sources */ = {isa = PBXBuildFile; fileRef = B241879C1DD2A7F900D820D8 /* MSCrashPrivInst.m */; }; + B24187D01DD2A7F900D820D8 /* MSCrashObjCException.m in Sources */ = {isa = PBXBuildFile; fileRef = B241879D1DD2A7F900D820D8 /* MSCrashObjCException.m */; }; + B24187D11DD2A7F900D820D8 /* MSCrashObjCMsgSend.m in Sources */ = {isa = PBXBuildFile; fileRef = B241879E1DD2A7F900D820D8 /* MSCrashObjCMsgSend.m */; }; + B24187D21DD2A7F900D820D8 /* MSFramelessDWARF_arm64.s in Sources */ = {isa = PBXBuildFile; fileRef = B241879F1DD2A7F900D820D8 /* MSFramelessDWARF_arm64.s */; }; + B24187D31DD2A7F900D820D8 /* MSCrashNULL.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187A01DD2A7F900D820D8 /* MSCrashNULL.m */; }; + B24187D41DD2A7F900D820D8 /* MSCrashOverwriteLinkRegister.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187A11DD2A7F900D820D8 /* MSCrashOverwriteLinkRegister.m */; }; + B24187D51DD2A7F900D820D8 /* CrashLib-Bridging-Header.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187A21DD2A7F900D820D8 /* CrashLib-Bridging-Header.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B24187D61DD2A7F900D820D8 /* MSCrashReleasedObject.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187A31DD2A7F900D820D8 /* MSCrashReleasedObject.m */; }; + B24187D71DD2A7F900D820D8 /* MSFramelessDWARF_i386.s in Sources */ = {isa = PBXBuildFile; fileRef = B24187A41DD2A7F900D820D8 /* MSFramelessDWARF_i386.s */; }; + B24187D81DD2A7F900D820D8 /* MSCrashCorruptMalloc.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187A51DD2A7F900D820D8 /* MSCrashCorruptMalloc.m */; }; + B24187D91DD2A7F900D820D8 /* MSCrashROPage.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187A61DD2A7F900D820D8 /* MSCrashROPage.m */; }; + B24187DA1DD2A7F900D820D8 /* MSCrashObjCException.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187A71DD2A7F900D820D8 /* MSCrashObjCException.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B24187DB1DD2A7F900D820D8 /* MSCrashCorruptMalloc.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187A81DD2A7F900D820D8 /* MSCrashCorruptMalloc.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B24187DC1DD2A7F900D820D8 /* MSCrashSmashStackBottom.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187A91DD2A7F900D820D8 /* MSCrashSmashStackBottom.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B24187DD1DD2A7F900D820D8 /* MSCrashNSLog.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187AA1DD2A7F900D820D8 /* MSCrashNSLog.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B24187DE1DD2A7F900D820D8 /* MSCrashROPage.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187AB1DD2A7F900D820D8 /* MSCrashROPage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B24187DF1DD2A7F900D820D8 /* MSCrashSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24187AC1DD2A7F900D820D8 /* MSCrashSwift.swift */; }; + B24187E01DD2A7F900D820D8 /* MSCrashGarbage.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187AD1DD2A7F900D820D8 /* MSCrashGarbage.m */; }; + B24187E11DD2A7F900D820D8 /* MSCrashNXPage.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187AE1DD2A7F900D820D8 /* MSCrashNXPage.m */; }; + B24187E21DD2A7F900D820D8 /* MSCrashCXXException.mm in Sources */ = {isa = PBXBuildFile; fileRef = B24187AF1DD2A7F900D820D8 /* MSCrashCXXException.mm */; }; + B24187E31DD2A7F900D820D8 /* MSCrashCXXException.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187B01DD2A7F900D820D8 /* MSCrashCXXException.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B24187E41DD2A7F900D820D8 /* MSCrashSmashStackTop.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187B11DD2A7F900D820D8 /* MSCrashSmashStackTop.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B24187E51DD2A7F900D820D8 /* MSCrashStackGuard.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187B21DD2A7F900D820D8 /* MSCrashStackGuard.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B24187E61DD2A7F900D820D8 /* MSCrashAsyncSafeThread.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187B31DD2A7F900D820D8 /* MSCrashAsyncSafeThread.m */; }; + B24187E71DD2A7F900D820D8 /* MSCrashStackGuard.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187B41DD2A7F900D820D8 /* MSCrashStackGuard.m */; }; + B24187E81DD2A7F900D820D8 /* MSCrashUndefInst.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187B51DD2A7F900D820D8 /* MSCrashUndefInst.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B24187E91DD2A7F900D820D8 /* CrashLib.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187B61DD2A7F900D820D8 /* CrashLib.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B24187EA1DD2A7F900D820D8 /* MSCrashUndefInst.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187B71DD2A7F900D820D8 /* MSCrashUndefInst.m */; }; + B24187EB1DD2A7F900D820D8 /* MSCrashAsyncSafeThread.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187B81DD2A7F900D820D8 /* MSCrashAsyncSafeThread.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B24187EC1DD2A7F900D820D8 /* MSFramelessDWARF_x86_64.s in Sources */ = {isa = PBXBuildFile; fileRef = B24187B91DD2A7F900D820D8 /* MSFramelessDWARF_x86_64.s */; }; + B24187ED1DD2A7F900D820D8 /* MSCrashFramelessDWARF.h in Headers */ = {isa = PBXBuildFile; fileRef = B24187BA1DD2A7F900D820D8 /* MSCrashFramelessDWARF.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B24187EE1DD2A7F900D820D8 /* MSCrashFramelessDWARF.m in Sources */ = {isa = PBXBuildFile; fileRef = B24187BB1DD2A7F900D820D8 /* MSCrashFramelessDWARF.m */; }; + C2392ED52464252C00425640 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C2392ED42464252C00425640 /* UIKit.framework */; }; + C2392ED72464253C00425640 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C2392ED62464253C00425640 /* UIKit.framework */; }; + C2392ED92464254D00425640 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C2392ED82464254D00425640 /* UIKit.framework */; }; + C2B7DF5523DF355100243AC7 /* MSCrashCXXCustomException.h in Headers */ = {isa = PBXBuildFile; fileRef = C2B7DF5323DF355100243AC7 /* MSCrashCXXCustomException.h */; }; + C2B7DF5623DF355100243AC7 /* MSCrashCXXCustomException.h in Headers */ = {isa = PBXBuildFile; fileRef = C2B7DF5323DF355100243AC7 /* MSCrashCXXCustomException.h */; }; + C2B7DF5723DF355100243AC7 /* MSCrashCXXCustomException.h in Headers */ = {isa = PBXBuildFile; fileRef = C2B7DF5323DF355100243AC7 /* MSCrashCXXCustomException.h */; }; + C2B7DF5823DF355100243AC7 /* MSCrashCXXCustomException.mm in Sources */ = {isa = PBXBuildFile; fileRef = C2B7DF5423DF355100243AC7 /* MSCrashCXXCustomException.mm */; }; + C2B7DF5923DF355100243AC7 /* MSCrashCXXCustomException.mm in Sources */ = {isa = PBXBuildFile; fileRef = C2B7DF5423DF355100243AC7 /* MSCrashCXXCustomException.mm */; }; + C2B7DF5A23DF355100243AC7 /* MSCrashCXXCustomException.mm in Sources */ = {isa = PBXBuildFile; fileRef = C2B7DF5423DF355100243AC7 /* MSCrashCXXCustomException.mm */; }; + C2CF7C0323E03565001985CE /* MSCrashCustomView.h in Headers */ = {isa = PBXBuildFile; fileRef = C2CF7C0123E03565001985CE /* MSCrashCustomView.h */; }; + C2CF7C0423E03565001985CE /* MSCrashCustomView.h in Headers */ = {isa = PBXBuildFile; fileRef = C2CF7C0123E03565001985CE /* MSCrashCustomView.h */; }; + C2CF7C0523E03565001985CE /* MSCrashCustomView.h in Headers */ = {isa = PBXBuildFile; fileRef = C2CF7C0123E03565001985CE /* MSCrashCustomView.h */; }; + C2CF7C0623E03565001985CE /* MSCrashCustomView.m in Sources */ = {isa = PBXBuildFile; fileRef = C2CF7C0223E03565001985CE /* MSCrashCustomView.m */; }; + C2CF7C0723E03565001985CE /* MSCrashCustomView.m in Sources */ = {isa = PBXBuildFile; fileRef = C2CF7C0223E03565001985CE /* MSCrashCustomView.m */; }; + C2CF7C0823E03565001985CE /* MSCrashCustomView.m in Sources */ = {isa = PBXBuildFile; fileRef = C2CF7C0223E03565001985CE /* MSCrashCustomView.m */; }; + C942E7BE22F0655500F91A4B /* MSCrashOutOfMemory.mm in Sources */ = {isa = PBXBuildFile; fileRef = C942E7BD22F0655500F91A4B /* MSCrashOutOfMemory.mm */; }; + C942E7C022F0657500F91A4B /* MSCrashOutOfMemory.h in Headers */ = {isa = PBXBuildFile; fileRef = C942E7BF22F0657500F91A4B /* MSCrashOutOfMemory.h */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 789FEDBC23E05B630026D59E /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; + 78FBB4A6239FAD0000ADFA1F /* MSCrashDelayedObjCException.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSCrashDelayedObjCException.h; sourceTree = ""; }; + 78FBB4A7239FAD0100ADFA1F /* MSCrashDelayedObjCException.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSCrashDelayedObjCException.m; sourceTree = ""; }; + 8065B5781F4C3FF4004FF622 /* CrashLibMac.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CrashLibMac.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 80F991211F0265070040FAD7 /* CrashLibTV.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CrashLibTV.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 84D067F1244864DC000B708B /* CrashLibWatch.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CrashLibWatch.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 84D067F3244867E8000B708B /* watchOS.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = watchOS.xcconfig; path = ../../Config/watchOS.xcconfig; sourceTree = ""; }; + B21A71DB1DD2A38C0044BB1C /* CrashLibIOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CrashLibIOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B21A71DF1DD2A38C0044BB1C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B24187891DD2A7F900D820D8 /* MSCrash.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSCrash.h; sourceTree = ""; }; + B241878A1DD2A7F900D820D8 /* MSCrashReleasedObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSCrashReleasedObject.h; sourceTree = ""; }; + B241878B1DD2A7F900D820D8 /* MSFramelessDWARF_arm32.S */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.asm; path = MSFramelessDWARF_arm32.S; sourceTree = ""; }; + B241878C1DD2A7F900D820D8 /* MSCrashNULL.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSCrashNULL.h; sourceTree = ""; }; + B241878D1DD2A7F900D820D8 /* MSCrashAbort.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSCrashAbort.h; sourceTree = ""; }; + B241878E1DD2A7F900D820D8 /* MSCrashGarbage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSCrashGarbage.h; sourceTree = ""; }; + B241878F1DD2A7F900D820D8 /* MSCrashOverwriteLinkRegister.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSCrashOverwriteLinkRegister.h; sourceTree = ""; }; + B24187901DD2A7F900D820D8 /* MSCrashCorruptObjC.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSCrashCorruptObjC.h; sourceTree = ""; }; + B24187911DD2A7F900D820D8 /* MSCrashObjCMsgSend.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSCrashObjCMsgSend.h; sourceTree = ""; }; + B24187921DD2A7F900D820D8 /* MSCrashTrap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSCrashTrap.h; sourceTree = ""; }; + B24187931DD2A7F900D820D8 /* MSCrashNSLog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSCrashNSLog.m; sourceTree = ""; }; + B24187941DD2A7F900D820D8 /* MSCrashSmashStackBottom.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSCrashSmashStackBottom.m; sourceTree = ""; }; + B24187951DD2A7F900D820D8 /* MSCrashPrivInst.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSCrashPrivInst.h; sourceTree = ""; }; + B24187961DD2A7F900D820D8 /* MSCrashSmashStackTop.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSCrashSmashStackTop.m; sourceTree = ""; }; + B24187971DD2A7F900D820D8 /* MSCrash.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSCrash.m; sourceTree = ""; }; + B24187981DD2A7F900D820D8 /* MSCrashTrap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSCrashTrap.m; sourceTree = ""; }; + B24187991DD2A7F900D820D8 /* MSCrashCorruptObjC.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSCrashCorruptObjC.m; sourceTree = ""; }; + B241879A1DD2A7F900D820D8 /* MSCrashNXPage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSCrashNXPage.h; sourceTree = ""; }; + B241879B1DD2A7F900D820D8 /* MSCrashAbort.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSCrashAbort.m; sourceTree = ""; }; + B241879C1DD2A7F900D820D8 /* MSCrashPrivInst.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSCrashPrivInst.m; sourceTree = ""; }; + B241879D1DD2A7F900D820D8 /* MSCrashObjCException.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSCrashObjCException.m; sourceTree = ""; }; + B241879E1DD2A7F900D820D8 /* MSCrashObjCMsgSend.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSCrashObjCMsgSend.m; sourceTree = ""; }; + B241879F1DD2A7F900D820D8 /* MSFramelessDWARF_arm64.s */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.asm; path = MSFramelessDWARF_arm64.s; sourceTree = ""; }; + B24187A01DD2A7F900D820D8 /* MSCrashNULL.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSCrashNULL.m; sourceTree = ""; }; + B24187A11DD2A7F900D820D8 /* MSCrashOverwriteLinkRegister.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSCrashOverwriteLinkRegister.m; sourceTree = ""; }; + B24187A21DD2A7F900D820D8 /* CrashLib-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CrashLib-Bridging-Header.h"; sourceTree = ""; }; + B24187A31DD2A7F900D820D8 /* MSCrashReleasedObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSCrashReleasedObject.m; sourceTree = ""; }; + B24187A41DD2A7F900D820D8 /* MSFramelessDWARF_i386.s */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.asm; path = MSFramelessDWARF_i386.s; sourceTree = ""; }; + B24187A51DD2A7F900D820D8 /* MSCrashCorruptMalloc.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSCrashCorruptMalloc.m; sourceTree = ""; }; + B24187A61DD2A7F900D820D8 /* MSCrashROPage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSCrashROPage.m; sourceTree = ""; }; + B24187A71DD2A7F900D820D8 /* MSCrashObjCException.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSCrashObjCException.h; sourceTree = ""; }; + B24187A81DD2A7F900D820D8 /* MSCrashCorruptMalloc.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSCrashCorruptMalloc.h; sourceTree = ""; }; + B24187A91DD2A7F900D820D8 /* MSCrashSmashStackBottom.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSCrashSmashStackBottom.h; sourceTree = ""; }; + B24187AA1DD2A7F900D820D8 /* MSCrashNSLog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSCrashNSLog.h; sourceTree = ""; }; + B24187AB1DD2A7F900D820D8 /* MSCrashROPage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSCrashROPage.h; sourceTree = ""; }; + B24187AC1DD2A7F900D820D8 /* MSCrashSwift.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MSCrashSwift.swift; sourceTree = ""; }; + B24187AD1DD2A7F900D820D8 /* MSCrashGarbage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSCrashGarbage.m; sourceTree = ""; }; + B24187AE1DD2A7F900D820D8 /* MSCrashNXPage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSCrashNXPage.m; sourceTree = ""; }; + B24187AF1DD2A7F900D820D8 /* MSCrashCXXException.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MSCrashCXXException.mm; sourceTree = ""; }; + B24187B01DD2A7F900D820D8 /* MSCrashCXXException.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSCrashCXXException.h; sourceTree = ""; }; + B24187B11DD2A7F900D820D8 /* MSCrashSmashStackTop.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSCrashSmashStackTop.h; sourceTree = ""; }; + B24187B21DD2A7F900D820D8 /* MSCrashStackGuard.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSCrashStackGuard.h; sourceTree = ""; }; + B24187B31DD2A7F900D820D8 /* MSCrashAsyncSafeThread.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSCrashAsyncSafeThread.m; sourceTree = ""; }; + B24187B41DD2A7F900D820D8 /* MSCrashStackGuard.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSCrashStackGuard.m; sourceTree = ""; }; + B24187B51DD2A7F900D820D8 /* MSCrashUndefInst.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSCrashUndefInst.h; sourceTree = ""; }; + B24187B61DD2A7F900D820D8 /* CrashLib.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CrashLib.h; sourceTree = ""; }; + B24187B71DD2A7F900D820D8 /* MSCrashUndefInst.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSCrashUndefInst.m; sourceTree = ""; }; + B24187B81DD2A7F900D820D8 /* MSCrashAsyncSafeThread.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSCrashAsyncSafeThread.h; sourceTree = ""; }; + B24187B91DD2A7F900D820D8 /* MSFramelessDWARF_x86_64.s */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.asm; path = MSFramelessDWARF_x86_64.s; sourceTree = ""; }; + B24187BA1DD2A7F900D820D8 /* MSCrashFramelessDWARF.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSCrashFramelessDWARF.h; sourceTree = ""; }; + B24187BB1DD2A7F900D820D8 /* MSCrashFramelessDWARF.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSCrashFramelessDWARF.m; sourceTree = ""; }; + C2392ED42464252C00425640 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; }; + C2392ED62464253C00425640 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk/System/Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; }; + C2392ED82464254D00425640 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS.sdk/System/Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; }; + C2B2102121B691F100F116BD /* CrashLib.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = CrashLib.xcconfig; sourceTree = ""; }; + C2B2102321B6A9FF00F116BD /* iOS.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = iOS.xcconfig; path = ../../Config/iOS.xcconfig; sourceTree = ""; }; + C2B2102421B6A9FF00F116BD /* macOS.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = macOS.xcconfig; path = ../../Config/macOS.xcconfig; sourceTree = ""; }; + C2B2102721B6A9FF00F116BD /* tvOS.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = tvOS.xcconfig; path = ../../Config/tvOS.xcconfig; sourceTree = ""; }; + C2B7DF5323DF355100243AC7 /* MSCrashCXXCustomException.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSCrashCXXCustomException.h; sourceTree = ""; }; + C2B7DF5423DF355100243AC7 /* MSCrashCXXCustomException.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MSCrashCXXCustomException.mm; sourceTree = ""; }; + C2CF7C0123E03565001985CE /* MSCrashCustomView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSCrashCustomView.h; sourceTree = ""; }; + C2CF7C0223E03565001985CE /* MSCrashCustomView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MSCrashCustomView.m; sourceTree = ""; }; + C942E7BD22F0655500F91A4B /* MSCrashOutOfMemory.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MSCrashOutOfMemory.mm; sourceTree = ""; }; + C942E7BF22F0657500F91A4B /* MSCrashOutOfMemory.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSCrashOutOfMemory.h; sourceTree = ""; }; + DF1F8F7D2269D7AB002B0B9B /* CrashLib Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "CrashLib Debug.xcconfig"; sourceTree = ""; }; + DF31CF972272FD0100A39F1B /* CrashLib Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "CrashLib Release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8065B5741F4C3FF4004FF622 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 789FEDBD23E05B630026D59E /* Cocoa.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 80F9911D1F0265070040FAD7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C2392ED72464253C00425640 /* UIKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 84D067D0244864DC000B708B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C2392ED92464254D00425640 /* UIKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B21A71D71DD2A38C0044BB1C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C2392ED52464252C00425640 /* UIKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 789FEDBB23E05B630026D59E /* Frameworks */ = { + isa = PBXGroup; + children = ( + C2392ED42464252C00425640 /* UIKit.framework */, + C2392ED62464253C00425640 /* UIKit.framework */, + C2392ED82464254D00425640 /* UIKit.framework */, + 789FEDBC23E05B630026D59E /* Cocoa.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + B21A71D11DD2A38C0044BB1C = { + isa = PBXGroup; + children = ( + C2B2102021B691F100F116BD /* Config */, + B21A71DD1DD2A38C0044BB1C /* CrashLib */, + B21A71DC1DD2A38C0044BB1C /* Products */, + 789FEDBB23E05B630026D59E /* Frameworks */, + ); + sourceTree = ""; + }; + B21A71DC1DD2A38C0044BB1C /* Products */ = { + isa = PBXGroup; + children = ( + B21A71DB1DD2A38C0044BB1C /* CrashLibIOS.framework */, + 80F991211F0265070040FAD7 /* CrashLibTV.framework */, + 8065B5781F4C3FF4004FF622 /* CrashLibMac.framework */, + 84D067F1244864DC000B708B /* CrashLibWatch.framework */, + ); + name = Products; + sourceTree = ""; + }; + B21A71DD1DD2A38C0044BB1C /* CrashLib */ = { + isa = PBXGroup; + children = ( + B24187A21DD2A7F900D820D8 /* CrashLib-Bridging-Header.h */, + B24187B61DD2A7F900D820D8 /* CrashLib.h */, + B21A71DF1DD2A38C0044BB1C /* Info.plist */, + B24187891DD2A7F900D820D8 /* MSCrash.h */, + B24187971DD2A7F900D820D8 /* MSCrash.m */, + B241878D1DD2A7F900D820D8 /* MSCrashAbort.h */, + B241879B1DD2A7F900D820D8 /* MSCrashAbort.m */, + B24187B81DD2A7F900D820D8 /* MSCrashAsyncSafeThread.h */, + B24187B31DD2A7F900D820D8 /* MSCrashAsyncSafeThread.m */, + B24187A81DD2A7F900D820D8 /* MSCrashCorruptMalloc.h */, + B24187A51DD2A7F900D820D8 /* MSCrashCorruptMalloc.m */, + B24187901DD2A7F900D820D8 /* MSCrashCorruptObjC.h */, + B24187991DD2A7F900D820D8 /* MSCrashCorruptObjC.m */, + C2CF7C0123E03565001985CE /* MSCrashCustomView.h */, + C2CF7C0223E03565001985CE /* MSCrashCustomView.m */, + C2B7DF5323DF355100243AC7 /* MSCrashCXXCustomException.h */, + C2B7DF5423DF355100243AC7 /* MSCrashCXXCustomException.mm */, + B24187B01DD2A7F900D820D8 /* MSCrashCXXException.h */, + B24187AF1DD2A7F900D820D8 /* MSCrashCXXException.mm */, + 78FBB4A6239FAD0000ADFA1F /* MSCrashDelayedObjCException.h */, + 78FBB4A7239FAD0100ADFA1F /* MSCrashDelayedObjCException.m */, + B24187BA1DD2A7F900D820D8 /* MSCrashFramelessDWARF.h */, + B24187BB1DD2A7F900D820D8 /* MSCrashFramelessDWARF.m */, + B241878E1DD2A7F900D820D8 /* MSCrashGarbage.h */, + B24187AD1DD2A7F900D820D8 /* MSCrashGarbage.m */, + B24187AA1DD2A7F900D820D8 /* MSCrashNSLog.h */, + B24187931DD2A7F900D820D8 /* MSCrashNSLog.m */, + B241878C1DD2A7F900D820D8 /* MSCrashNULL.h */, + B24187A01DD2A7F900D820D8 /* MSCrashNULL.m */, + B241879A1DD2A7F900D820D8 /* MSCrashNXPage.h */, + B24187AE1DD2A7F900D820D8 /* MSCrashNXPage.m */, + B24187A71DD2A7F900D820D8 /* MSCrashObjCException.h */, + B241879D1DD2A7F900D820D8 /* MSCrashObjCException.m */, + B24187911DD2A7F900D820D8 /* MSCrashObjCMsgSend.h */, + B241879E1DD2A7F900D820D8 /* MSCrashObjCMsgSend.m */, + C942E7BF22F0657500F91A4B /* MSCrashOutOfMemory.h */, + C942E7BD22F0655500F91A4B /* MSCrashOutOfMemory.mm */, + B241878F1DD2A7F900D820D8 /* MSCrashOverwriteLinkRegister.h */, + B24187A11DD2A7F900D820D8 /* MSCrashOverwriteLinkRegister.m */, + B24187951DD2A7F900D820D8 /* MSCrashPrivInst.h */, + B241879C1DD2A7F900D820D8 /* MSCrashPrivInst.m */, + B241878A1DD2A7F900D820D8 /* MSCrashReleasedObject.h */, + B24187A31DD2A7F900D820D8 /* MSCrashReleasedObject.m */, + B24187AB1DD2A7F900D820D8 /* MSCrashROPage.h */, + B24187A61DD2A7F900D820D8 /* MSCrashROPage.m */, + B24187A91DD2A7F900D820D8 /* MSCrashSmashStackBottom.h */, + B24187941DD2A7F900D820D8 /* MSCrashSmashStackBottom.m */, + B24187B11DD2A7F900D820D8 /* MSCrashSmashStackTop.h */, + B24187961DD2A7F900D820D8 /* MSCrashSmashStackTop.m */, + B24187B21DD2A7F900D820D8 /* MSCrashStackGuard.h */, + B24187B41DD2A7F900D820D8 /* MSCrashStackGuard.m */, + B24187AC1DD2A7F900D820D8 /* MSCrashSwift.swift */, + B24187921DD2A7F900D820D8 /* MSCrashTrap.h */, + B24187981DD2A7F900D820D8 /* MSCrashTrap.m */, + B24187B51DD2A7F900D820D8 /* MSCrashUndefInst.h */, + B24187B71DD2A7F900D820D8 /* MSCrashUndefInst.m */, + B241878B1DD2A7F900D820D8 /* MSFramelessDWARF_arm32.S */, + B241879F1DD2A7F900D820D8 /* MSFramelessDWARF_arm64.s */, + B24187A41DD2A7F900D820D8 /* MSFramelessDWARF_i386.s */, + B24187B91DD2A7F900D820D8 /* MSFramelessDWARF_x86_64.s */, + ); + path = CrashLib; + sourceTree = ""; + }; + C2B2102021B691F100F116BD /* Config */ = { + isa = PBXGroup; + children = ( + 84D067F3244867E8000B708B /* watchOS.xcconfig */, + C2B2102321B6A9FF00F116BD /* iOS.xcconfig */, + C2B2102421B6A9FF00F116BD /* macOS.xcconfig */, + C2B2102721B6A9FF00F116BD /* tvOS.xcconfig */, + C2B2102121B691F100F116BD /* CrashLib.xcconfig */, + DF1F8F7D2269D7AB002B0B9B /* CrashLib Debug.xcconfig */, + DF31CF972272FD0100A39F1B /* CrashLib Release.xcconfig */, + ); + path = Config; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 8065B5751F4C3FF4004FF622 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 8065B5A11F4C4024004FF622 /* MSCrashROPage.h in Headers */, + 8065B59B1F4C4024004FF622 /* MSCrashOverwriteLinkRegister.h in Headers */, + C2CF7C0523E03565001985CE /* MSCrashCustomView.h in Headers */, + 8065B5AC1F4C4024004FF622 /* MSCrashUndefInst.h in Headers */, + 8065B5911F4C4024004FF622 /* MSCrashFramelessDWARF.h in Headers */, + 78FBB4A8239FAD0100ADFA1F /* MSCrashDelayedObjCException.h in Headers */, + 8065B5851F4C4024004FF622 /* MSCrashAsyncSafeThread.h in Headers */, + 8065B5871F4C4024004FF622 /* MSCrashCXXException.h in Headers */, + 8065B58B1F4C4024004FF622 /* MSCrashCorruptObjC.h in Headers */, + 8065B5971F4C4024004FF622 /* MSCrashObjCException.h in Headers */, + 8065B5891F4C4024004FF622 /* MSCrashCorruptMalloc.h in Headers */, + 8065B5B21F4C4024004FF622 /* CrashLib-Bridging-Header.h in Headers */, + 8065B5A31F4C4024004FF622 /* MSCrashSmashStackBottom.h in Headers */, + 8065B5AA1F4C4024004FF622 /* MSCrashTrap.h in Headers */, + C2B7DF5723DF355100243AC7 /* MSCrashCXXCustomException.h in Headers */, + 8065B59D1F4C4024004FF622 /* MSCrashPrivInst.h in Headers */, + 8065B5991F4C4024004FF622 /* MSCrashObjCMsgSend.h in Headers */, + 8065B58F1F4C4024004FF622 /* MSCrashNSLog.h in Headers */, + 8065B5811F4C4024004FF622 /* MSCrash.h in Headers */, + 8065B5931F4C4024004FF622 /* MSCrashGarbage.h in Headers */, + 8065B5831F4C4024004FF622 /* MSCrashAbort.h in Headers */, + 8065B59F1F4C4024004FF622 /* MSCrashReleasedObject.h in Headers */, + 8065B5951F4C4024004FF622 /* MSCrashNXPage.h in Headers */, + 8065B5A71F4C4024004FF622 /* MSCrashStackGuard.h in Headers */, + 8065B5801F4C4024004FF622 /* CrashLib.h in Headers */, + 8065B5A51F4C4024004FF622 /* MSCrashSmashStackTop.h in Headers */, + 8065B58D1F4C4024004FF622 /* MSCrashNULL.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 80F9911E1F0265070040FAD7 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 80F991491F0265C60040FAD7 /* MSCrashROPage.h in Headers */, + 80F991431F0265C60040FAD7 /* MSCrashOverwriteLinkRegister.h in Headers */, + 80F991541F0265C60040FAD7 /* MSCrashUndefInst.h in Headers */, + C2B7DF5623DF355100243AC7 /* MSCrashCXXCustomException.h in Headers */, + 80F991391F0265C60040FAD7 /* MSCrashFramelessDWARF.h in Headers */, + 80F9912D1F0265C60040FAD7 /* MSCrashAsyncSafeThread.h in Headers */, + 80F9912F1F0265C60040FAD7 /* MSCrashCXXException.h in Headers */, + 80F991331F0265C60040FAD7 /* MSCrashCorruptObjC.h in Headers */, + 801C25311F04001300F4859B /* CrashLib-Bridging-Header.h in Headers */, + 80F9913F1F0265C60040FAD7 /* MSCrashObjCException.h in Headers */, + 80F991311F0265C60040FAD7 /* MSCrashCorruptMalloc.h in Headers */, + 80F9914B1F0265C60040FAD7 /* MSCrashSmashStackBottom.h in Headers */, + 80F991521F0265C60040FAD7 /* MSCrashTrap.h in Headers */, + 80F991451F0265C60040FAD7 /* MSCrashPrivInst.h in Headers */, + C2CF7C0423E03565001985CE /* MSCrashCustomView.h in Headers */, + 80F991411F0265C60040FAD7 /* MSCrashObjCMsgSend.h in Headers */, + 80F991371F0265C60040FAD7 /* MSCrashNSLog.h in Headers */, + 80F991291F0265C60040FAD7 /* MSCrash.h in Headers */, + 80F9913B1F0265C60040FAD7 /* MSCrashGarbage.h in Headers */, + 80F9912B1F0265C60040FAD7 /* MSCrashAbort.h in Headers */, + 80F991471F0265C60040FAD7 /* MSCrashReleasedObject.h in Headers */, + 80F9913D1F0265C60040FAD7 /* MSCrashNXPage.h in Headers */, + 80F9915B1F02671A0040FAD7 /* CrashLib.h in Headers */, + 80F9914F1F0265C60040FAD7 /* MSCrashStackGuard.h in Headers */, + 80F9914D1F0265C60040FAD7 /* MSCrashSmashStackTop.h in Headers */, + 80F991351F0265C60040FAD7 /* MSCrashNULL.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 84D067D2244864DC000B708B /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 84D067D3244864DC000B708B /* MSCrashROPage.h in Headers */, + 84D067D4244864DC000B708B /* MSCrashOverwriteLinkRegister.h in Headers */, + 84D067D5244864DC000B708B /* MSCrashUndefInst.h in Headers */, + 84D067D6244864DC000B708B /* MSCrashCXXCustomException.h in Headers */, + 84D067D7244864DC000B708B /* MSCrashFramelessDWARF.h in Headers */, + 84D067D8244864DC000B708B /* MSCrashAsyncSafeThread.h in Headers */, + 84D067D9244864DC000B708B /* MSCrashCXXException.h in Headers */, + 84D067DA244864DC000B708B /* MSCrashCorruptObjC.h in Headers */, + 84D067DB244864DC000B708B /* CrashLib-Bridging-Header.h in Headers */, + 84D067DC244864DC000B708B /* MSCrashObjCException.h in Headers */, + 84D067DD244864DC000B708B /* MSCrashCorruptMalloc.h in Headers */, + 84D067DE244864DC000B708B /* MSCrashSmashStackBottom.h in Headers */, + 84D067DF244864DC000B708B /* MSCrashTrap.h in Headers */, + 84D067E0244864DC000B708B /* MSCrashPrivInst.h in Headers */, + 84D067E2244864DC000B708B /* MSCrashObjCMsgSend.h in Headers */, + 84D067E3244864DC000B708B /* MSCrashNSLog.h in Headers */, + 84D067E4244864DC000B708B /* MSCrash.h in Headers */, + 84D067E5244864DC000B708B /* MSCrashGarbage.h in Headers */, + 84D067E6244864DC000B708B /* MSCrashAbort.h in Headers */, + 84D067E7244864DC000B708B /* MSCrashReleasedObject.h in Headers */, + 84D067E8244864DC000B708B /* MSCrashNXPage.h in Headers */, + 84D067E9244864DC000B708B /* CrashLib.h in Headers */, + 84D067EA244864DC000B708B /* MSCrashStackGuard.h in Headers */, + 84D067EB244864DC000B708B /* MSCrashSmashStackTop.h in Headers */, + 84D067EC244864DC000B708B /* MSCrashNULL.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B21A71D81DD2A38C0044BB1C /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + B24187E91DD2A7F900D820D8 /* CrashLib.h in Headers */, + B24187ED1DD2A7F900D820D8 /* MSCrashFramelessDWARF.h in Headers */, + C2CF7C0323E03565001985CE /* MSCrashCustomView.h in Headers */, + B24187DB1DD2A7F900D820D8 /* MSCrashCorruptMalloc.h in Headers */, + B24187BD1DD2A7F900D820D8 /* MSCrashReleasedObject.h in Headers */, + B24187C81DD2A7F900D820D8 /* MSCrashPrivInst.h in Headers */, + B24187BC1DD2A7F900D820D8 /* MSCrash.h in Headers */, + B24187E41DD2A7F900D820D8 /* MSCrashSmashStackTop.h in Headers */, + B24187BF1DD2A7F900D820D8 /* MSCrashNULL.h in Headers */, + B24187DA1DD2A7F900D820D8 /* MSCrashObjCException.h in Headers */, + B24187DE1DD2A7F900D820D8 /* MSCrashROPage.h in Headers */, + B24187E81DD2A7F900D820D8 /* MSCrashUndefInst.h in Headers */, + C942E7C022F0657500F91A4B /* MSCrashOutOfMemory.h in Headers */, + B24187C01DD2A7F900D820D8 /* MSCrashAbort.h in Headers */, + C2B7DF5523DF355100243AC7 /* MSCrashCXXCustomException.h in Headers */, + B24187EB1DD2A7F900D820D8 /* MSCrashAsyncSafeThread.h in Headers */, + B24187DC1DD2A7F900D820D8 /* MSCrashSmashStackBottom.h in Headers */, + B24187C41DD2A7F900D820D8 /* MSCrashObjCMsgSend.h in Headers */, + B24187C11DD2A7F900D820D8 /* MSCrashGarbage.h in Headers */, + B24187DD1DD2A7F900D820D8 /* MSCrashNSLog.h in Headers */, + B24187E31DD2A7F900D820D8 /* MSCrashCXXException.h in Headers */, + B24187D51DD2A7F900D820D8 /* CrashLib-Bridging-Header.h in Headers */, + B24187CD1DD2A7F900D820D8 /* MSCrashNXPage.h in Headers */, + B24187C51DD2A7F900D820D8 /* MSCrashTrap.h in Headers */, + B24187E51DD2A7F900D820D8 /* MSCrashStackGuard.h in Headers */, + B24187C21DD2A7F900D820D8 /* MSCrashOverwriteLinkRegister.h in Headers */, + B24187C31DD2A7F900D820D8 /* MSCrashCorruptObjC.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 8065B5771F4C3FF4004FF622 /* CrashLibMac */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8065B57F1F4C3FF4004FF622 /* Build configuration list for PBXNativeTarget "CrashLibMac" */; + buildPhases = ( + C2B2102B21B6C31C00F116BD /* Verify no buildSettings */, + 8065B5731F4C3FF4004FF622 /* Sources */, + 8065B5741F4C3FF4004FF622 /* Frameworks */, + 8065B5751F4C3FF4004FF622 /* Headers */, + 8065B5761F4C3FF4004FF622 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = CrashLibMac; + productName = CrashLibMac; + productReference = 8065B5781F4C3FF4004FF622 /* CrashLibMac.framework */; + productType = "com.apple.product-type.framework"; + }; + 80F991201F0265070040FAD7 /* CrashLibTV */ = { + isa = PBXNativeTarget; + buildConfigurationList = 80F991261F0265070040FAD7 /* Build configuration list for PBXNativeTarget "CrashLibTV" */; + buildPhases = ( + C2B2102A21B6C30B00F116BD /* Verify no buildSettings */, + 80F9911C1F0265070040FAD7 /* Sources */, + 80F9911D1F0265070040FAD7 /* Frameworks */, + 80F9911E1F0265070040FAD7 /* Headers */, + 80F9911F1F0265070040FAD7 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = CrashLibTV; + productName = CrashLibTV; + productReference = 80F991211F0265070040FAD7 /* CrashLibTV.framework */; + productType = "com.apple.product-type.framework"; + }; + 84D067B0244864DC000B708B /* CrashLibWatch */ = { + isa = PBXNativeTarget; + buildConfigurationList = 84D067EE244864DC000B708B /* Build configuration list for PBXNativeTarget "CrashLibWatch" */; + buildPhases = ( + 84D067B1244864DC000B708B /* Verify no buildSettings */, + 84D067B2244864DC000B708B /* Sources */, + 84D067D0244864DC000B708B /* Frameworks */, + 84D067D2244864DC000B708B /* Headers */, + 84D067ED244864DC000B708B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = CrashLibWatch; + productName = CrashLibTV; + productReference = 84D067F1244864DC000B708B /* CrashLibWatch.framework */; + productType = "com.apple.product-type.framework"; + }; + B21A71DA1DD2A38C0044BB1C /* CrashLibIOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = B21A71E31DD2A38C0044BB1C /* Build configuration list for PBXNativeTarget "CrashLibIOS" */; + buildPhases = ( + C2B2102921B6C2DC00F116BD /* Verify no buildSettings */, + B21A71D61DD2A38C0044BB1C /* Sources */, + B21A71D71DD2A38C0044BB1C /* Frameworks */, + B21A71D81DD2A38C0044BB1C /* Headers */, + B21A71D91DD2A38C0044BB1C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = CrashLibIOS; + productName = CrashLibIOS; + productReference = B21A71DB1DD2A38C0044BB1C /* CrashLibIOS.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + B21A71D21DD2A38C0044BB1C /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1010; + ORGANIZATIONNAME = Microsoft; + TargetAttributes = { + 8065B5771F4C3FF4004FF622 = { + CreatedOnToolsVersion = 8.3.3; + ProvisioningStyle = Automatic; + }; + 80F991201F0265070040FAD7 = { + CreatedOnToolsVersion = 8.3.3; + ProvisioningStyle = Automatic; + }; + B21A71DA1DD2A38C0044BB1C = { + CreatedOnToolsVersion = 8.1; + LastSwiftMigration = 0810; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = B21A71D51DD2A38C0044BB1C /* Build configuration list for PBXProject "CrashLib" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + English, + en, + ); + mainGroup = B21A71D11DD2A38C0044BB1C; + productRefGroup = B21A71DC1DD2A38C0044BB1C /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + B21A71DA1DD2A38C0044BB1C /* CrashLibIOS */, + 80F991201F0265070040FAD7 /* CrashLibTV */, + 8065B5771F4C3FF4004FF622 /* CrashLibMac */, + 84D067B0244864DC000B708B /* CrashLibWatch */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8065B5761F4C3FF4004FF622 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 80F9911F1F0265070040FAD7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 84D067ED244864DC000B708B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B21A71D91DD2A38C0044BB1C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 84D067B1244864DC000B708B /* Verify no buildSettings */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Verify no buildSettings"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/../Scripts/VerifyNoBuildSettings.swift\" ${PROJECT_NAME}.xcodeproj/project.pbxproj\n"; + showEnvVarsInLog = 0; + }; + C2B2102921B6C2DC00F116BD /* Verify no buildSettings */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Verify no buildSettings"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/../Scripts/VerifyNoBuildSettings.swift\" ${PROJECT_NAME}.xcodeproj/project.pbxproj\n"; + showEnvVarsInLog = 0; + }; + C2B2102A21B6C30B00F116BD /* Verify no buildSettings */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Verify no buildSettings"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/../Scripts/VerifyNoBuildSettings.swift\" ${PROJECT_NAME}.xcodeproj/project.pbxproj\n"; + showEnvVarsInLog = 0; + }; + C2B2102B21B6C31C00F116BD /* Verify no buildSettings */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Verify no buildSettings"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/../Scripts/VerifyNoBuildSettings.swift\" ${PROJECT_NAME}.xcodeproj/project.pbxproj\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8065B5731F4C3FF4004FF622 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8065B5A81F4C4024004FF622 /* MSCrashStackGuard.m in Sources */, + 8065B5AF1F4C4024004FF622 /* MSFramelessDWARF_arm32.S in Sources */, + 8065B5921F4C4024004FF622 /* MSCrashFramelessDWARF.m in Sources */, + 8065B58A1F4C4024004FF622 /* MSCrashCorruptMalloc.m in Sources */, + 8065B5B01F4C4024004FF622 /* MSFramelessDWARF_x86_64.s in Sources */, + 8065B5AD1F4C4024004FF622 /* MSCrashUndefInst.m in Sources */, + 8065B5841F4C4024004FF622 /* MSCrashAbort.m in Sources */, + 8065B5A21F4C4024004FF622 /* MSCrashROPage.m in Sources */, + 8065B5941F4C4024004FF622 /* MSCrashGarbage.m in Sources */, + 8065B5B11F4C4024004FF622 /* MSFramelessDWARF_arm64.s in Sources */, + 8065B5881F4C4024004FF622 /* MSCrashCXXException.mm in Sources */, + 8065B5A41F4C4024004FF622 /* MSCrashSmashStackBottom.m in Sources */, + 8065B5A91F4C4024004FF622 /* MSCrashSwift.swift in Sources */, + 8065B5821F4C4024004FF622 /* MSCrash.m in Sources */, + 8065B5AB1F4C4024004FF622 /* MSCrashTrap.m in Sources */, + 8065B58E1F4C4024004FF622 /* MSCrashNULL.m in Sources */, + 8065B5A61F4C4024004FF622 /* MSCrashSmashStackTop.m in Sources */, + C2CF7C0823E03565001985CE /* MSCrashCustomView.m in Sources */, + 8065B5981F4C4024004FF622 /* MSCrashObjCException.m in Sources */, + 8065B5AE1F4C4024004FF622 /* MSFramelessDWARF_i386.s in Sources */, + C2B7DF5A23DF355100243AC7 /* MSCrashCXXCustomException.mm in Sources */, + 8065B5901F4C4024004FF622 /* MSCrashNSLog.m in Sources */, + 78FBB4A9239FAD0100ADFA1F /* MSCrashDelayedObjCException.m in Sources */, + 8065B59A1F4C4024004FF622 /* MSCrashObjCMsgSend.m in Sources */, + 8065B59C1F4C4024004FF622 /* MSCrashOverwriteLinkRegister.m in Sources */, + 8065B59E1F4C4024004FF622 /* MSCrashPrivInst.m in Sources */, + 8065B5861F4C4024004FF622 /* MSCrashAsyncSafeThread.m in Sources */, + 8065B5961F4C4024004FF622 /* MSCrashNXPage.m in Sources */, + 8065B58C1F4C4024004FF622 /* MSCrashCorruptObjC.m in Sources */, + 8065B5A01F4C4024004FF622 /* MSCrashReleasedObject.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 80F9911C1F0265070040FAD7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 80F991501F0265C60040FAD7 /* MSCrashStackGuard.m in Sources */, + 80F991571F0265C60040FAD7 /* MSFramelessDWARF_arm32.S in Sources */, + 80F9913A1F0265C60040FAD7 /* MSCrashFramelessDWARF.m in Sources */, + 80F991321F0265C60040FAD7 /* MSCrashCorruptMalloc.m in Sources */, + C2B7DF5923DF355100243AC7 /* MSCrashCXXCustomException.mm in Sources */, + 80F991581F0265C60040FAD7 /* MSFramelessDWARF_x86_64.s in Sources */, + 80F991551F0265C60040FAD7 /* MSCrashUndefInst.m in Sources */, + 80F9912C1F0265C60040FAD7 /* MSCrashAbort.m in Sources */, + 80F9914A1F0265C60040FAD7 /* MSCrashROPage.m in Sources */, + 80F9913C1F0265C60040FAD7 /* MSCrashGarbage.m in Sources */, + 80F991591F0265C60040FAD7 /* MSFramelessDWARF_arm64.s in Sources */, + 80F991301F0265C60040FAD7 /* MSCrashCXXException.mm in Sources */, + 80F9914C1F0265C60040FAD7 /* MSCrashSmashStackBottom.m in Sources */, + 80F991511F0265C60040FAD7 /* MSCrashSwift.swift in Sources */, + C2CF7C0723E03565001985CE /* MSCrashCustomView.m in Sources */, + 80F9912A1F0265C60040FAD7 /* MSCrash.m in Sources */, + 80F991531F0265C60040FAD7 /* MSCrashTrap.m in Sources */, + 80F991361F0265C60040FAD7 /* MSCrashNULL.m in Sources */, + 80F9914E1F0265C60040FAD7 /* MSCrashSmashStackTop.m in Sources */, + 80F991401F0265C60040FAD7 /* MSCrashObjCException.m in Sources */, + 80F991561F0265C60040FAD7 /* MSFramelessDWARF_i386.s in Sources */, + 80F991381F0265C60040FAD7 /* MSCrashNSLog.m in Sources */, + 80F991421F0265C60040FAD7 /* MSCrashObjCMsgSend.m in Sources */, + 80F991441F0265C60040FAD7 /* MSCrashOverwriteLinkRegister.m in Sources */, + 80F991461F0265C60040FAD7 /* MSCrashPrivInst.m in Sources */, + 80F9912E1F0265C60040FAD7 /* MSCrashAsyncSafeThread.m in Sources */, + 80F9913E1F0265C60040FAD7 /* MSCrashNXPage.m in Sources */, + 80F991341F0265C60040FAD7 /* MSCrashCorruptObjC.m in Sources */, + 80F991481F0265C60040FAD7 /* MSCrashReleasedObject.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 84D067B2244864DC000B708B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 84D067B3244864DC000B708B /* MSCrashStackGuard.m in Sources */, + 84D067B4244864DC000B708B /* MSFramelessDWARF_arm32.S in Sources */, + 84D067B5244864DC000B708B /* MSCrashFramelessDWARF.m in Sources */, + 84D067B6244864DC000B708B /* MSCrashCorruptMalloc.m in Sources */, + 84D067B7244864DC000B708B /* MSCrashCXXCustomException.mm in Sources */, + 84D067B8244864DC000B708B /* MSFramelessDWARF_x86_64.s in Sources */, + 84D067B9244864DC000B708B /* MSCrashUndefInst.m in Sources */, + 84D067BA244864DC000B708B /* MSCrashAbort.m in Sources */, + 84D067BB244864DC000B708B /* MSCrashROPage.m in Sources */, + 84D067BC244864DC000B708B /* MSCrashGarbage.m in Sources */, + 84D067BD244864DC000B708B /* MSFramelessDWARF_arm64.s in Sources */, + 84D067BE244864DC000B708B /* MSCrashCXXException.mm in Sources */, + 84D067BF244864DC000B708B /* MSCrashSmashStackBottom.m in Sources */, + 84D067C0244864DC000B708B /* MSCrashSwift.swift in Sources */, + 84D067C2244864DC000B708B /* MSCrash.m in Sources */, + 84D067C3244864DC000B708B /* MSCrashTrap.m in Sources */, + 84D067C4244864DC000B708B /* MSCrashNULL.m in Sources */, + 84D067C5244864DC000B708B /* MSCrashSmashStackTop.m in Sources */, + 84D067C6244864DC000B708B /* MSCrashObjCException.m in Sources */, + 84D067C7244864DC000B708B /* MSFramelessDWARF_i386.s in Sources */, + 84D067C8244864DC000B708B /* MSCrashNSLog.m in Sources */, + 84D067C9244864DC000B708B /* MSCrashObjCMsgSend.m in Sources */, + 84D067CA244864DC000B708B /* MSCrashOverwriteLinkRegister.m in Sources */, + 84D067CB244864DC000B708B /* MSCrashPrivInst.m in Sources */, + 84D067CC244864DC000B708B /* MSCrashAsyncSafeThread.m in Sources */, + 84D067CD244864DC000B708B /* MSCrashNXPage.m in Sources */, + 84D067CE244864DC000B708B /* MSCrashCorruptObjC.m in Sources */, + 84D067CF244864DC000B708B /* MSCrashReleasedObject.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B21A71D61DD2A38C0044BB1C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B24187EC1DD2A7F900D820D8 /* MSFramelessDWARF_x86_64.s in Sources */, + B24187D61DD2A7F900D820D8 /* MSCrashReleasedObject.m in Sources */, + B24187D81DD2A7F900D820D8 /* MSCrashCorruptMalloc.m in Sources */, + C942E7BE22F0655500F91A4B /* MSCrashOutOfMemory.mm in Sources */, + B24187CA1DD2A7F900D820D8 /* MSCrash.m in Sources */, + B24187BE1DD2A7F900D820D8 /* MSFramelessDWARF_arm32.S in Sources */, + B24187DF1DD2A7F900D820D8 /* MSCrashSwift.swift in Sources */, + B24187D21DD2A7F900D820D8 /* MSFramelessDWARF_arm64.s in Sources */, + B24187E11DD2A7F900D820D8 /* MSCrashNXPage.m in Sources */, + B24187CC1DD2A7F900D820D8 /* MSCrashCorruptObjC.m in Sources */, + B24187D31DD2A7F900D820D8 /* MSCrashNULL.m in Sources */, + B24187C91DD2A7F900D820D8 /* MSCrashSmashStackTop.m in Sources */, + B24187E61DD2A7F900D820D8 /* MSCrashAsyncSafeThread.m in Sources */, + B24187CE1DD2A7F900D820D8 /* MSCrashAbort.m in Sources */, + B24187E21DD2A7F900D820D8 /* MSCrashCXXException.mm in Sources */, + B24187CF1DD2A7F900D820D8 /* MSCrashPrivInst.m in Sources */, + B24187D71DD2A7F900D820D8 /* MSFramelessDWARF_i386.s in Sources */, + C2CF7C0623E03565001985CE /* MSCrashCustomView.m in Sources */, + B24187D91DD2A7F900D820D8 /* MSCrashROPage.m in Sources */, + B24187C61DD2A7F900D820D8 /* MSCrashNSLog.m in Sources */, + C2B7DF5823DF355100243AC7 /* MSCrashCXXCustomException.mm in Sources */, + B24187D01DD2A7F900D820D8 /* MSCrashObjCException.m in Sources */, + B24187E71DD2A7F900D820D8 /* MSCrashStackGuard.m in Sources */, + B24187D41DD2A7F900D820D8 /* MSCrashOverwriteLinkRegister.m in Sources */, + B24187CB1DD2A7F900D820D8 /* MSCrashTrap.m in Sources */, + B24187C71DD2A7F900D820D8 /* MSCrashSmashStackBottom.m in Sources */, + B24187D11DD2A7F900D820D8 /* MSCrashObjCMsgSend.m in Sources */, + B24187EE1DD2A7F900D820D8 /* MSCrashFramelessDWARF.m in Sources */, + B24187E01DD2A7F900D820D8 /* MSCrashGarbage.m in Sources */, + B24187EA1DD2A7F900D820D8 /* MSCrashUndefInst.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 8065B57D1F4C3FF4004FF622 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C2B2102421B6A9FF00F116BD /* macOS.xcconfig */; + buildSettings = { + INFOPLIST_FILE = CrashLib/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.appcenter.CrashLibMac; + }; + name = Debug; + }; + 8065B57E1F4C3FF4004FF622 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C2B2102421B6A9FF00F116BD /* macOS.xcconfig */; + buildSettings = { + INFOPLIST_FILE = CrashLib/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.appcenter.CrashLibMac; + }; + name = Release; + }; + 80F991271F0265070040FAD7 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C2B2102721B6A9FF00F116BD /* tvOS.xcconfig */; + buildSettings = { + INFOPLIST_FILE = CrashLib/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.appcenter.CrashLibTV; + }; + name = Debug; + }; + 80F991281F0265070040FAD7 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C2B2102721B6A9FF00F116BD /* tvOS.xcconfig */; + buildSettings = { + INFOPLIST_FILE = CrashLib/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.appcenter.CrashLibTV; + }; + name = Release; + }; + 84D067EF244864DC000B708B /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 84D067F3244867E8000B708B /* watchOS.xcconfig */; + buildSettings = { + INFOPLIST_FILE = CrashLib/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.appcenter.CrashLibWatch; + }; + name = Debug; + }; + 84D067F0244864DC000B708B /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 84D067F3244867E8000B708B /* watchOS.xcconfig */; + buildSettings = { + INFOPLIST_FILE = CrashLib/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.appcenter.CrashLibWatch; + }; + name = Release; + }; + B21A71E11DD2A38C0044BB1C /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DF1F8F7D2269D7AB002B0B9B /* CrashLib Debug.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + B21A71E21DD2A38C0044BB1C /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DF31CF972272FD0100A39F1B /* CrashLib Release.xcconfig */; + buildSettings = { + }; + name = Release; + }; + B21A71E41DD2A38C0044BB1C /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C2B2102321B6A9FF00F116BD /* iOS.xcconfig */; + buildSettings = { + INFOPLIST_FILE = CrashLib/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.appcenter.CrashLibIOS; + }; + name = Debug; + }; + B21A71E51DD2A38C0044BB1C /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C2B2102321B6A9FF00F116BD /* iOS.xcconfig */; + buildSettings = { + INFOPLIST_FILE = CrashLib/Info.plist; + PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.appcenter.CrashLibIOS; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 8065B57F1F4C3FF4004FF622 /* Build configuration list for PBXNativeTarget "CrashLibMac" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8065B57D1F4C3FF4004FF622 /* Debug */, + 8065B57E1F4C3FF4004FF622 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 80F991261F0265070040FAD7 /* Build configuration list for PBXNativeTarget "CrashLibTV" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 80F991271F0265070040FAD7 /* Debug */, + 80F991281F0265070040FAD7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 84D067EE244864DC000B708B /* Build configuration list for PBXNativeTarget "CrashLibWatch" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 84D067EF244864DC000B708B /* Debug */, + 84D067F0244864DC000B708B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B21A71D51DD2A38C0044BB1C /* Build configuration list for PBXProject "CrashLib" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B21A71E11DD2A38C0044BB1C /* Debug */, + B21A71E21DD2A38C0044BB1C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B21A71E31DD2A38C0044BB1C /* Build configuration list for PBXNativeTarget "CrashLibIOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B21A71E41DD2A38C0044BB1C /* Debug */, + B21A71E51DD2A38C0044BB1C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = B21A71D21DD2A38C0044BB1C /* Project object */; +} diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/CrashLib-Bridging-Header.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/CrashLib-Bridging-Header.h new file mode 100644 index 0000000000..8fb72dd0c0 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/CrashLib-Bridging-Header.h @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrash.h" diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/CrashLib.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/CrashLib.h new file mode 100644 index 0000000000..06c0ca990e --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/CrashLib.h @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + + +#import "MSCrash.h" +#import "MSCrashNULL.h" +#import "MSCrashGarbage.h" +#import "MSCrashROPage.h" +#import "MSCrashNXPage.h" +#import "MSCrashUndefInst.h" +#import "MSCrashPrivInst.h" +#import "MSCrashAbort.h" +#import "MSCrashTrap.h" +#import "MSCrashObjCException.h" +#import "MSCrashCXXException.h" +#import "MSCrashReleasedObject.h" +#import "MSCrashObjCMsgSend.h" +#import "MSCrashCorruptMalloc.h" +#import "MSCrashSmashStackTop.h" +#import "MSCrashSmashStackBottom.h" +#import "MSCrashCorruptObjC.h" +#import "MSCrashNSLog.h" +#import "MSCrashAsyncSafeThread.h" +#import "MSCrashOverwriteLinkRegister.h" +#import "MSCrashStackGuard.h" diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/Info.plist b/submodules/AppCenter-sdk/CrashLib/CrashLib/Info.plist new file mode 100644 index 0000000000..fbe1e6b314 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrash.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrash.h new file mode 100644 index 0000000000..9cb23ce870 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrash.h @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +@interface MSCrash : NSObject + ++ (NSArray *)allCrashes; + ++ (void)registerCrash:(MSCrash *)crash; + ++ (void)unregisterCrash:(MSCrash *)crash; + ++ (void)removeAllCrashes; + +@property(nonatomic, copy, readonly) NSString *category; +@property(nonatomic, copy, readonly) NSString *title; +@property(nonatomic, copy, readonly) NSString *desc; + +- (void)crash; + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrash.m b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrash.m new file mode 100644 index 0000000000..2466209827 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrash.m @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrash.h" + +static NSMutableSet *crashTypes = nil; + +@implementation MSCrash + ++ (void)initialize { + static dispatch_once_t predicate = 0; + + dispatch_once(&predicate, ^{ + crashTypes = [[NSMutableSet alloc] init]; + }); +} + ++ (NSArray *)allCrashes { + return crashTypes.allObjects; +} + ++ (void)registerCrash:(MSCrash *)crash { + [crashTypes addObject:crash]; +} + ++ (void)removeAllCrashes { + [crashTypes removeAllObjects]; +} + ++ (void)unregisterCrash:(MSCrash *)crash { + [crashTypes removeObject:crash]; +} + +- (NSString *)category { + return @"NONE"; +} + +- (NSString *)title { + return @"NONE"; +} + +- (NSString *)desc { + return @"NONE"; +} + +- (void)crash { + NSLog(@"I'm supposed to crash here."); +} + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashAbort.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashAbort.h new file mode 100644 index 0000000000..166223d9a6 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashAbort.h @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrash.h" + +@interface MSCrashAbort : MSCrash + +- (void)crash __attribute__((noreturn)); + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashAbort.m b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashAbort.m new file mode 100644 index 0000000000..12dd91edf9 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashAbort.m @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrashAbort.h" + +@implementation MSCrashAbort + +- (NSString *)category { + return @"SIGTRAP"; +} + +- (NSString *)title { + return @"Call abort()"; +} + +- (NSString *)desc { + return @"Call abort() to terminate the program."; +} + +- (void)crash __attribute__((noreturn)) { + abort(); +} + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashAsyncSafeThread.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashAsyncSafeThread.h new file mode 100644 index 0000000000..c99443f683 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashAsyncSafeThread.h @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrash.h" + +@interface MSCrashAsyncSafeThread : MSCrash + +@end \ No newline at end of file diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashAsyncSafeThread.m b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashAsyncSafeThread.m new file mode 100644 index 0000000000..90f8e66fd7 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashAsyncSafeThread.m @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrashAsyncSafeThread.h" +#import + +@implementation MSCrashAsyncSafeThread + +- (NSString *)category { + return @"Async-Safety"; +} + +- (NSString *)title { + return @"Crash with _pthread_list_lock held"; +} + +- (NSString *)desc { + return @"" + "Triggers a crash with libsystem_pthread's _pthread_list_lock held, " + "causing non-async-safe crash reporters that use pthread APIs to deadlock."; +} + +- (void)crash { + pthread_getname_np(pthread_self(), ((char *) 0x1), 1); + + /* This is unreachable, but prevents clang from applying TCO to the above when + * optimization is enabled. */ + NSLog(@"I'm here from the tail call prevention department."); +} + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCXXCustomException.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCXXCustomException.h new file mode 100644 index 0000000000..d30cd873bd --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCXXCustomException.h @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrash.h" + +@interface MSCrashCXXCustomException : MSCrash + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCXXCustomException.mm b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCXXCustomException.mm new file mode 100644 index 0000000000..8492cf3732 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCXXCustomException.mm @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrashCXXCustomException.h" + +class my_custom_exception { +}; + +@implementation MSCrashCXXCustomException + +- (NSString *)category { + return @"Exceptions"; +} + +- (NSString *)title { + return @"Throw Custom C++ exception"; +} + +- (NSString *)desc { + return @"Throw an uncaught C++ exception that cannot be cast to std::exception."; +} + +- (void)crash __attribute__((noreturn)) { + throw new my_custom_exception; +} + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCXXException.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCXXException.h new file mode 100644 index 0000000000..bc0a5268bb --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCXXException.h @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrash.h" + +@interface MSCrashCXXException : MSCrash + +- (void)crash __attribute__((noreturn)); + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCXXException.mm b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCXXException.mm new file mode 100644 index 0000000000..3e4427c1e1 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCXXException.mm @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrashCXXException.h" +#import + +class kaboom_exception : public std::exception { + virtual const char *what() const throw(); +}; + +const char *kaboom_exception::what() const throw() { + return "If this had been a real exception, you would be cursing now."; +} + +@implementation MSCrashCXXException + +- (NSString *)category { + return @"Exceptions"; +} + +- (NSString *)title { + return @"Throw C++ exception"; +} + +- (NSString *)desc { + return @"" + "Throw an uncaught C++ exception. " + "This is a difficult case for crash reporters to handle, " + "as it involves the destruction of the data necessary to generate a correct backtrace."; +} + +- (void)crash __attribute__((noreturn)) { + throw new kaboom_exception; +} + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCorruptMalloc.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCorruptMalloc.h new file mode 100644 index 0000000000..75121cfbaa --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCorruptMalloc.h @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrash.h" + +@interface MSCrashCorruptMalloc : MSCrash + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCorruptMalloc.m b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCorruptMalloc.m new file mode 100644 index 0000000000..b26876b624 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCorruptMalloc.m @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrashCorruptMalloc.h" +#import +#import + +@implementation MSCrashCorruptMalloc + +- (NSString *)category { + return @"Various"; +} + +- (NSString *)title { + return @"Corrupt malloc()'s internal tracking information"; +} + +- (NSString *)desc { + return @"" + "Write garbage into data areas used by malloc to track memory allocations. " + "This simulates the kind of heap overflow and/or heap corruption likely to occur in an application; " + "if the crash reporter itself uses malloc, the corrupted heap will likely trigger a crash in the crash reporter itself."; +} + +- (void)crash { + /* Smash the heap, and keep smashing it until we eventually hit something non-writable, or trigger + * a malloc error (e.g., in NSLog). */ + uint8_t *memory = malloc(10); + while (true) { + NSLog(@"Smashing [%p - %p]", memory, memory + PAGE_SIZE); + memset((void *) trunc_page((vm_address_t) memory), 0xAB, PAGE_SIZE); + memory += PAGE_SIZE; + } +} + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCorruptObjC.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCorruptObjC.h new file mode 100644 index 0000000000..5e9ce2bc92 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCorruptObjC.h @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrash.h" + +@interface MSCrashCorruptObjC : MSCrash + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCorruptObjC.m b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCorruptObjC.m new file mode 100644 index 0000000000..f556e4b99e --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCorruptObjC.m @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrashCorruptObjC.h" +#import +#import +#import +#import + +@implementation MSCrashCorruptObjC + +- (NSString *)category { + return @"Various"; +} + +- (NSString *)title { + return @"Corrupt the Objective-C runtime's structures"; +} + +- (NSString *)desc { + return @"" + "Write garbage into data areas used by the Objective-C runtime to track classes and objects. " + "Bugs of this nature are why crash reporters cannot use Objective-C in their crash handling code, " + "as attempting to do so is likely to lead to a crash in the crash reporting code."; +} + +- (void)crash { + Class objClass = [NSObject class]; + + // VERY VERY PRIVATE INTERNAL RUNTIME DETAILS VERY VERY EVIL THIS IS BAD!!! + struct objc_cache_t { + uintptr_t mask; /* total = mask + 1 */ + uintptr_t occupied; + void *buckets[1]; + }; + struct objc_class_t { + struct objc_class_t *isa; + struct objc_class_t *superclass; + struct objc_cache_t cache; + IMP *vtable; + uintptr_t data_NEVER_USE; // class_rw_t * plus custom rr/alloc flags + }; + +#if __i386__ && !TARGET_IPHONE_SIMULATOR +#define __bridge +#endif + + struct objc_class_t *objClassInternal = (__bridge struct objc_class_t *) objClass; + + // Trashes NSObject's method cache + memset(&objClassInternal->cache, 0xa5, sizeof(struct objc_cache_t)); + + [self description]; +} + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCustomView.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCustomView.h new file mode 100644 index 0000000000..e6951dcf5a --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCustomView.h @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrash.h" + +@interface MSCrashCustomView : MSCrash + +@end + diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCustomView.m b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCustomView.m new file mode 100644 index 0000000000..75905c3a48 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashCustomView.m @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrashCustomView.h" + +#if TARGET_OS_OSX +#import +#else +#import +#endif + +#if TARGET_OS_OSX +@interface MSCustomView : NSView +#else +@interface MSCustomView : UIView +#endif + +@end + +@implementation MSCustomView + +#if TARGET_OS_OSX +-(void)drawRect:(NSRect)rect { +#else +-(void)drawRect:(CGRect)rect { +#endif + [super drawRect:rect]; + @throw [NSException exceptionWithName:NSGenericException reason:@"Objective-C exception from drawing custom view." + userInfo:@{NSLocalizedDescriptionKey: @"Something goes wrong in drawRect:"}]; +} + +@end + +@implementation MSCrashCustomView + +- (NSString *)category { + return @"Exceptions"; +} + +- (NSString *)title { + return @"Throw Objective-C exception during drawing custom view"; +} + +- (NSString *)desc { + return @"Throw an uncaught Objective-C exception during drawing custom view."; +} + +- (void)crash { + MSCustomView* view = [[MSCustomView new] initWithFrame:CGRectMake(0, 0, 100, 100)]; +#if TARGET_OS_OSX + [NSApplication.sharedApplication.mainWindow.contentView addSubview:view]; +#else + UIApplication.sharedApplication.keyWindow.rootViewController.view = view; +#endif +} + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashDelayedObjCException.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashDelayedObjCException.h new file mode 100644 index 0000000000..e519e4261a --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashDelayedObjCException.h @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrash.h" + +@interface MSCrashDelayedObjCException : MSCrash + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashDelayedObjCException.m b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashDelayedObjCException.m new file mode 100644 index 0000000000..e5403b0c4b --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashDelayedObjCException.m @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrashDelayedObjCException.h" + +@implementation MSCrashDelayedObjCException + +- (NSString *)category { + return @"Exceptions"; +} + +- (NSString *)title { + return @"Throw Objective-C exception outside of IBAction"; +} + +- (NSString *)desc { + return @"Throw an uncaught Objective-C exception outside of IBAction."; +} + +- (void)crash { + [self performSelector:@selector(delayedException) withObject:nil afterDelay:0.1]; +} + +- (void)delayedException { + @throw [NSException exceptionWithName:NSGenericException reason:@"An uncaught exception!" + userInfo:@{NSLocalizedDescriptionKey: @"Catching your exceptions!"}]; +} + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashFramelessDWARF.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashFramelessDWARF.h new file mode 100644 index 0000000000..4dd1f4d912 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashFramelessDWARF.h @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrash.h" + +@interface MSCrashFramelessDWARF : MSCrash + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashFramelessDWARF.m b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashFramelessDWARF.m new file mode 100644 index 0000000000..2434a4f332 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashFramelessDWARF.m @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrashFramelessDWARF.h" + +/* Our assembly implemented test function */ +extern void MSFramelessDWARF_test(void); + +/* Called by the assembly code paths to trigger the actual NULL dereference */ +extern void MSFramelessDWARF_test_crash(void); + +void MSFramelessDWARF_test_crash(void) { + *((volatile uint8_t *) NULL) = 0xFF; +} + +@implementation MSCrashFramelessDWARF + +- (NSString *)category { + return @"Various"; +} + +- (NSString *)title { + return @"DWARF Unwinding"; +} + +- (NSString *)desc { + return @"" + "Trigger a crash in a frame that requires DWARF or Compact Unwind support to correctly unwind. " + "Unwinders that do not support DWARF will terminate on the second frame. " + "The tests will fail for all unwinders on ARMv6 and ARMv7 (DWARF/eh_frame is unsupported). "; +} + +- (void)crash { + MSFramelessDWARF_test(); +} + + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashGarbage.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashGarbage.h new file mode 100644 index 0000000000..127872a18d --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashGarbage.h @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrash.h" + +@interface MSCrashGarbage : MSCrash +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashGarbage.m b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashGarbage.m new file mode 100644 index 0000000000..a31ab07fd9 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashGarbage.m @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrashGarbage.h" +#import + +@implementation MSCrashGarbage + +- (NSString *)category { + return @"SIGSEGV"; +} + +- (NSString *)title { + return @"Dereference a bad pointer"; +} + +- (NSString *)desc { + return @"Attempt to read from a garbage pointer that's not mapped but also isn't NULL."; +} + +- (void)crash { + void *ptr = mmap(NULL, (size_t) getpagesize(), PROT_NONE, MAP_ANON | MAP_PRIVATE, -1, 0); + + if (ptr != MAP_FAILED) + munmap(ptr, (size_t) getpagesize()); + +#if __i386__ + asm volatile ( "mov %0, %%eax\n\tmov (%%eax), %%eax" : : "X" (ptr) : "memory", "eax" ); +#elif __x86_64__ + asm volatile ( "mov %0, %%rax\n\tmov (%%rax), %%rax" : : "X" (ptr) : "memory", "rax" ); +#elif __arm__ && __ARM_ARCH == 7 + asm volatile ( "mov r4, %0\n\tldr r4, [r4]" : : "X" (ptr) : "memory", "r4" ); +#elif __arm__ && __ARM_ARCH == 6 + asm volatile ( "mov r4, %0\n\tldr r4, [r4]" : : "X" (ptr) : "memory", "r4" ); +#elif __arm64__ + asm volatile ( "mov x4, %0\n\tldr x4, [x4]" : : "X" (ptr) : "memory", "x4" ); +#endif +} + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashNSLog.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashNSLog.h new file mode 100644 index 0000000000..e425c12b52 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashNSLog.h @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrash.h" + +@interface MSCrashNSLog : MSCrash + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashNSLog.m b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashNSLog.m new file mode 100644 index 0000000000..581056cb58 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashNSLog.m @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrashNSLog.h" + +@implementation MSCrashNSLog + +- (NSString *)category { + return @"Objective-C"; +} + +- (NSString *)title { + return @"Access a non-object as an object"; +} + +- (NSString *)desc { + return @"Call NSLog(@\"%@\", 16);, causing a crash when the runtime attempts to treat 16 as a pointer to an object."; +} + +- (void)crash { +#if __i386__ && !TARGET_IPHONE_SIMULATOR +#define __bridge +#endif + + NSLog(@"%@", (__bridge id) (void *) 16); +} + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashNULL.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashNULL.h new file mode 100644 index 0000000000..f7aa513e31 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashNULL.h @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrash.h" + +@interface MSCrashNULL : MSCrash +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashNULL.m b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashNULL.m new file mode 100644 index 0000000000..4bbc8a120c --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashNULL.m @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrashNULL.h" + +@implementation MSCrashNULL + +- (NSString *)category { + return @"SIGSEGV"; +} + +- (NSString *)title { + return @"Dereference a NULL pointer"; +} + +- (NSString *)desc { + return @"Attempt to read from 0x0, which causes a segmentation violation."; +} + +- (void)crash { + volatile char *ptr = NULL; + (void) *ptr; +} + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashNXPage.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashNXPage.h new file mode 100644 index 0000000000..3c07f4a538 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashNXPage.h @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrash.h" + +@interface MSCrashNXPage : MSCrash + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashNXPage.m b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashNXPage.m new file mode 100644 index 0000000000..6a21976fed --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashNXPage.m @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import + +#import "MSCrashNXPage.h" + +@implementation MSCrashNXPage + +- (NSString *)category { + return @"SIGSEGV"; +} + +- (NSString *)title { + return @"Jump into an NX page"; +} + +- (NSString *)desc { + return @"Call a function pointer to memory in a non-executable page."; +} + +static void __attribute__((noinline)) real_NXcrash(void) { + /** + * Solution and explanation by Gwynne: + * When generating an NX crash, previously the code would explicitly jump to NULL, which modern versions of Clang + * correctly optimize out as provable undefined behavior (the compiler is free to do whatever it wants if it can + * prove that the code will always dereference NULL). Instead, map a valid memory space without the execute + * permission and jump to that pointer. + */ + void *ptr = mmap(NULL, (size_t)getpagesize(), PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0); + if (ptr != MAP_FAILED) { + ((void (*)(void))ptr)(); + } + munmap(ptr, (size_t)getpagesize()); +} + +- (void)crash { + real_NXcrash(); +} + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashObjCException.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashObjCException.h new file mode 100644 index 0000000000..3fbe22e4c4 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashObjCException.h @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrash.h" + +@interface MSCrashObjCException : MSCrash + +- (void)crash __attribute__((noreturn)); + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashObjCException.m b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashObjCException.m new file mode 100644 index 0000000000..63b980b8c2 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashObjCException.m @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrashObjCException.h" + +@implementation MSCrashObjCException + +- (NSString *)category { + return @"Exceptions"; +} + +- (NSString *)title { + return @"Throw Objective-C exception"; +} + +- (NSString *)desc { + return @"" + "Throw an uncaught Objective-C exception. " + "It's possible to generate a better crash report here compared to the C++ Exception case " + "because NSUncaughtExceptionHandler can be used, which isn't available for C++ extensions."; +} + +- (void)crash __attribute__((noreturn)) { + @throw [NSException exceptionWithName:NSGenericException reason:@"An uncaught exception! SCREAM." + userInfo:@{NSLocalizedDescriptionKey: @"I'm in your program, catching your exceptions!"}]; +} + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashObjCMsgSend.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashObjCMsgSend.h new file mode 100644 index 0000000000..4492c88528 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashObjCMsgSend.h @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrash.h" + +@interface MSCrashObjCMsgSend : MSCrash + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashObjCMsgSend.m b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashObjCMsgSend.m new file mode 100644 index 0000000000..933df94f90 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashObjCMsgSend.m @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrashObjCMsgSend.h" +#import + +@implementation MSCrashObjCMsgSend + +- (NSString *)category { + return @"Objective-C"; +} + +- (NSString *)title { + return @"Crash inside objc_msgSend()"; +} + +- (NSString *)desc { + return @"Send a message to an invalid object, resulting in a crash inside objc_msgSend()."; +} + +- (void)crash { + struct { + void *isa; + } corruptObj = { + .isa = (void *) 42 + }; + +#if __i386__ && !TARGET_IPHONE_SIMULATOR +#define __bridge +#endif + [(__bridge id) &corruptObj stringWithFormat: + @"%u, %u, %u, %u, %u, %u, %f, %f, %c, %c, %s, %s, %@, %@" + " %u, %u, %u, %u, %u, %u, %f, %f, %c, %c, %s, %s, %@, %@", + 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 9.0, 10.0, 'a', 'b', "C", "D", @"E", @"F", + 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 9.0, 10.0, 'a', 'b', "C", "D", @"E", @"F"]; +} + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashOutOfMemory.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashOutOfMemory.h new file mode 100644 index 0000000000..6af37a7202 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashOutOfMemory.h @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrash.h" + +@interface MSCrashOutOfMemory : MSCrash + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashOutOfMemory.mm b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashOutOfMemory.mm new file mode 100644 index 0000000000..3bede7fc01 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashOutOfMemory.mm @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrashOutOfMemory.h" + +@interface MSCrashOutOfMemory () + +@property NSMutableArray *buffers; + +@property size_t allocated; + +@end + +@implementation MSCrashOutOfMemory + +- (instancetype)init { + self = [super init]; + if (self) { + _buffers = [NSMutableArray new]; + _allocated = 0; + } + return self; +} + +- (NSString *)category { + return @"Memory"; +} + +- (NSString *)title { + return @"Produce memory shortage (OOM)"; +} + +- (NSString *)desc { + return @"" + "Execute an infinite loop with excessive memory allocation which " + "causes an OS to terminate app."; +} + +- (void)crash { + const size_t blockSize = 128 * 1024 * 1024; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 100 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ + void *buffer = malloc(blockSize); + memset(buffer, 42, blockSize); + [self.buffers addObject:[NSValue valueWithPointer:buffer]]; + self.allocated += blockSize; + NSLog(@"Allocated %zu MB", self.allocated / (1024 * 1024)); + [self crash]; + }); +} + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashOverwriteLinkRegister.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashOverwriteLinkRegister.h new file mode 100644 index 0000000000..eaffaaa758 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashOverwriteLinkRegister.h @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrash.h" + +@interface MSCrashOverwriteLinkRegister : MSCrash + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashOverwriteLinkRegister.m b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashOverwriteLinkRegister.m new file mode 100644 index 0000000000..b0ff2c7d4d --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashOverwriteLinkRegister.m @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrashOverwriteLinkRegister.h" + +@implementation MSCrashOverwriteLinkRegister + +- (NSString *)category { + return @"Various"; +} + +- (NSString *)title { + return @"Overwrite link register, then crash"; +} + +- (NSString *)desc { + return @"" + "Trigger a crash after first overwriting the link register. " + "Crash reporters that insert a stack frame based on the link register can generate duplicate or incorrect stack frames in the report. " + "This does not apply to architectures that do not use a link register, such as x86-64."; +} + +- (void)crash { + /* Call a method to trigger modification of LR. We use the result below to + * convince the compiler to order this function the way we want it. */ + uintptr_t ptr = (uintptr_t) [NSObject class]; + + /* Make-work code that simply advances the PC to better demonstrate the discrepency. We use the + * 'ptr' value here to make sure the compiler doesn't optimize-away this code, or re-order it below + * the method call. */ + ptr += ptr; + ptr -= 42; + ptr += ptr % (ptr - 42); + + /* Crash within the method (using a write to the NULL page); the link register will be pointing at + * the make-work code. We use the 'ptr' value to control compiler ordering. */ + *((uintptr_t volatile *) NULL) = ptr; +} + + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashPrivInst.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashPrivInst.h new file mode 100644 index 0000000000..ccb3e0b5c2 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashPrivInst.h @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrash.h" + +@interface MSCrashPrivInst : MSCrash + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashPrivInst.m b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashPrivInst.m new file mode 100644 index 0000000000..81d6e8b4b5 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashPrivInst.m @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrashPrivInst.h" + +@implementation MSCrashPrivInst + +- (NSString *)category { + return @"SIGILL"; +} + +- (NSString *)title { + return @"Execute a privileged instruction"; +} + +- (NSString *)desc { + return @"Attempt to execute an instruction that can only be executed in supervisor mode."; +} + +- (void)crash { +#if __i386__ + asm volatile ( "hlt" : : : ); +#elif __x86_64__ + asm volatile ( "hlt" : : : ); +#elif __arm__ && __ARM_ARCH == 7 && __thumb__ + asm volatile ( ".long 0xf7f08000" : : : ); +#elif __arm__ && __ARM_ARCH == 7 + asm volatile ( ".long 0xe1400070" : : : ); +#elif __arm__ && __ARM_ARCH == 6 && __thumb__ + asm volatile ( ".long 0xf5ff8f00" : : : ); +#elif __arm__ && __ARM_ARCH == 6 + asm volatile ( ".long 0xe14ff000" : : : ); +#elif __arm64__ + /* Invalidate all EL1&0 regime stage 1 and 2 TLB entries. This should + * not be possible from userspace, for hopefully obvious reasons :-) */ + asm volatile ( "tlbi alle1" : : : ); +#endif +} + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashROPage.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashROPage.h new file mode 100644 index 0000000000..35a8cbfc33 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashROPage.h @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrash.h" + +@interface MSCrashROPage : MSCrash + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashROPage.m b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashROPage.m new file mode 100644 index 0000000000..d54448ecce --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashROPage.m @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrashROPage.h" + +@implementation MSCrashROPage + +static void __attribute__((used)) dummyfunc(void) { +} + +- (NSString *)category { + return @"SIGBUS"; +} + +- (NSString *)title { + return @"Write to a read-only page"; +} + +- (NSString *)desc { + return @"Attempt to write to a page into which the app's code is mapped."; +} + +- (void)crash { + volatile char *ptr = (char *) dummyfunc; + *ptr = 0; +} + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashReleasedObject.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashReleasedObject.h new file mode 100644 index 0000000000..d62e46836f --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashReleasedObject.h @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrash.h" + +@interface MSCrashReleasedObject : MSCrash + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashReleasedObject.m b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashReleasedObject.m new file mode 100644 index 0000000000..8befcf0cf6 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashReleasedObject.m @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrashReleasedObject.h" +#import + +@implementation MSCrashReleasedObject + +- (NSString *)category { + return @"Objective-C"; +} + +- (NSString *)title { + return @"Message a released object"; +} + +- (NSString *)desc { + return @"Send a message to an object whose memory has already been freed."; +} + +- (void)crash { +#if __i386__ && !TARGET_IPHONE_SIMULATOR + NSObject *object = [[NSObject alloc] init]; +#else + NSObject *__unsafe_unretained object = (__bridge NSObject *) CFBridgingRetain([[NSObject alloc] init]); +#endif + +#if __i386__ && !TARGET_IPHONE_SIMULATOR + [object release]; +#else + CFRelease((__bridge CFTypeRef) object); +#endif + ^__attribute__((noreturn)) { + for (;;) { + [object self]; + [object description]; + [object debugDescription]; + } + }(); +} + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashSmashStackBottom.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashSmashStackBottom.h new file mode 100644 index 0000000000..41be98f4f2 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashSmashStackBottom.h @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrash.h" + +@interface MSCrashSmashStackBottom : MSCrash + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashSmashStackBottom.m b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashSmashStackBottom.m new file mode 100644 index 0000000000..31ce372117 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashSmashStackBottom.m @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrashSmashStackBottom.h" + +@implementation MSCrashSmashStackBottom + +- (NSString *)category { + return @"Various"; +} + +- (NSString *)title { + return @"Smash the bottom of the stack"; +} + +- (NSString *)desc { + return @"" + "Overwrite data below the current stack pointer. This will destroy the current function. " + "Reporting of this crash is expected to fail. Succeeding is basically luck."; +} + +- (void)crash { + void *sp = NULL; + +#if __i386__ + asm volatile ( "mov %%esp, %0" : "=X" (sp) : : ); +#elif __x86_64__ + asm volatile ( "mov %%rsp, %0" : "=X" (sp) : : ); +#elif __arm__ && __ARM_ARCH == 7 + asm volatile ( "mov %0, sp" : "=X" (sp) : : ); +#elif __arm__ && __ARM_ARCH == 6 + asm volatile ( "mov %0, sp" : "=X" (sp) : : ); +#elif __arm64__ + asm volatile ( "mov %0, sp" : "=X" (sp) : : ); +#endif + + memset(sp, 0xa5, 0x100); +} + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashSmashStackTop.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashSmashStackTop.h new file mode 100644 index 0000000000..29ffbb2532 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashSmashStackTop.h @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrash.h" + +@interface MSCrashSmashStackTop : MSCrash + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashSmashStackTop.m b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashSmashStackTop.m new file mode 100644 index 0000000000..0c3052d6cf --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashSmashStackTop.m @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrashSmashStackTop.h" + +@implementation MSCrashSmashStackTop + +- (NSString *)category { + return @"Various"; +} + +- (NSString *)title { + return @"Smash the top of the stack"; +} + +- (NSString *)desc { + return @"" + "Overwrite data above the current stack pointer. This will destroy the current stack trace. " + "Reporting of this crash is expected to fail. Succeeding is basically luck. " + "Apple added additional checks that prevent this crash from happening in iOS 12 and up."; +} + +- (void)crash { + void *sp = NULL; + +#if __i386__ + asm volatile ( "mov %%esp, %0" : "=X" (sp) : : ); +#elif __x86_64__ + asm volatile ( "mov %%rsp, %0" : "=X" (sp) : : ); +#elif __arm__ && __ARM_ARCH == 7 + asm volatile ( "mov %0, sp" : "=X" (sp) : : ); +#elif __arm__ && __ARM_ARCH == 6 + asm volatile ( "mov %0, sp" : "=X" (sp) : : ); +#elif __arm64__ + asm volatile ( "mov %0, sp" : "=X" (sp) : : ); +#endif + + memset(sp - 0x100, 0xa5, 0x100); +} + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashStackGuard.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashStackGuard.h new file mode 100644 index 0000000000..64a74c9eef --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashStackGuard.h @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrash.h" + +@interface MSCrashStackGuard : MSCrash + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashStackGuard.m b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashStackGuard.m new file mode 100644 index 0000000000..3accab1e19 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashStackGuard.m @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrashStackGuard.h" + +@implementation MSCrashStackGuard + +- (NSString *)category { + return @"SIGSEGV"; +} + +- (NSString *)title { + return @"Stack overflow"; +} + +- (NSString *)desc { + return @"" + "Execute an infinitely recursive method, which overflows the stack and " + "causes a crash by attempting to write to the guard page at the end."; +} + +- (void)crash { + [self crash]; + + /* This is unreachable, but prevents clang from applying TCO to the above when + * optimization is enabled. */ + NSLog(@"I'm here from the tail call prevention department."); +} + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashSwift.swift b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashSwift.swift new file mode 100644 index 0000000000..0a0655dcd8 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashSwift.swift @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import Foundation + +class MSCrashSwift: MSCrash { + override var category: String { + return "Various"; + } + override var title: String { + return "Swift"; + } + override var desc: String { + return "Trigger a crash from inside a Swift method."; + } + override func crash() { + let buf: UnsafeMutablePointer? = nil; + + buf![1] = 1; + } +} diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashTrap.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashTrap.h new file mode 100644 index 0000000000..fb1572c5ee --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashTrap.h @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrash.h" + +@interface MSCrashTrap : MSCrash + +- (void)crash __attribute__((noreturn)); + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashTrap.m b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashTrap.m new file mode 100644 index 0000000000..2bf777d17d --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashTrap.m @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrashTrap.h" + +@implementation MSCrashTrap + +- (NSString *)category { + return @"SIGTRAP"; +} + +- (NSString *)title { + return @"Call __builtin_trap()"; +} + +- (NSString *)desc { + return @"Call __builtin_trap() to generate a trap exception."; +} + +- (void)crash __attribute__((noreturn)) { + __builtin_trap(); +} + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashUndefInst.h b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashUndefInst.h new file mode 100644 index 0000000000..5568425372 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashUndefInst.h @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrash.h" + +@interface MSCrashUndefInst : MSCrash + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashUndefInst.m b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashUndefInst.m new file mode 100644 index 0000000000..e29e57aa45 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSCrashUndefInst.m @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#import "MSCrashUndefInst.h" + +@implementation MSCrashUndefInst + +- (NSString *)category { + return @"SIGILL"; +} + +- (NSString *)title { + return @"Execute an undefined instruction"; +} + +- (NSString *)desc { + return @"Attempt to execute an instructiondinn not to be defined on the current architecture."; +} + +- (void)crash { +#if __i386__ + asm volatile ( "ud2" : : : ); +#elif __x86_64__ + asm volatile ( "ud2" : : : ); +#elif __arm__ && __ARM_ARCH == 7 && __thumb__ + asm volatile ( ".word 0xde00" : : : ); +#elif __arm__ && __ARM_ARCH == 7 + asm volatile ( ".long 0xf7f8a000" : : : ); +#elif __arm__ && __ARM_ARCH == 6 && __thumb__ + asm volatile ( ".word 0xde00" : : : ); +#elif __arm__ && __ARM_ARCH == 6 + asm volatile ( ".long 0xf7f8a000" : : : ); +#elif __arm64__ + asm volatile ( ".long 0xf7f8a000" : : : ); +#endif +} + +@end diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSFramelessDWARF_arm32.S b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSFramelessDWARF_arm32.S new file mode 100644 index 0000000000..d69bb9311e --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSFramelessDWARF_arm32.S @@ -0,0 +1,48 @@ +/* + * Extracted from PLCrashReporter's 1.2-RC2 frame unwinding test cases. + * + * Copyright (c) 2013-2014 Plausible Labs, Inc. All rights reserved. + * Copyright (c) 2008-2011 Apple Inc. All rights reserved. + * + * This file contains Original Code and/or Modifications of Original Code + * as defined in and that are subject to the Apple Public Source License + * Version 2.0 (the 'License'). You may not use this file except in + * compliance with the License. Please obtain a copy of the License at + * http://www.opensource.apple.com/apsl/ and read it before using this + * file. + * + * The Original Code and all software distributed under the License are + * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER + * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, + * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. + * Please see the License for the specific language governing rights and + * limitations under the License. + */ + +#ifdef __arm__ +.text + +.align 2 +.globl _MSFramelessDWARF_test +_MSFramelessDWARF_test: +// iOS/ARM doesn't support shipping eh_frame/compact unwind data, +// so we trigger the bug here, but provide no hand-generated DWARF +// data to allow unwinding. +LT0_start: + push {r4, r5} +LT0_sub_sp: + mov r4, fp // Save FP + mov r5, lr // Save LR + eor lr, lr // Zero LR + eor r7, r7 // Zero FP + + bl _MSFramelessDWARF_test_crash + + mov fp, r4 // Restore FP + mov lr, r5 // Restore LR + pop {r4, r5} + mov pc, lr +LT0_end: + +#endif /* __arm__ */ \ No newline at end of file diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSFramelessDWARF_arm64.s b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSFramelessDWARF_arm64.s new file mode 100644 index 0000000000..ffc06fdc59 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSFramelessDWARF_arm64.s @@ -0,0 +1,112 @@ +/* + * Extracted from PLCrashReporter's 1.2-RC2 frame unwinding test cases. + * + * Copyright (c) 2013-2014 Plausible Labs, Inc. All rights reserved. + * Copyright (c) 2008-2011 Apple Inc. All rights reserved. + * + * This file contains Original Code and/or Modifications of Original Code + * as defined in and that are subject to the Apple Public Source License + * Version 2.0 (the 'License'). You may not use this file except in + * compliance with the License. Please obtain a copy of the License at + * http://www.opensource.apple.com/apsl/ and read it before using this + * file. + * + * The Original Code and all software distributed under the License are + * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER + * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, + * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. + * Please see the License for the specific language governing rights and + * limitations under the License. + */ + +#ifdef __arm64__ + +.text +.align 2 +.globl _MSFramelessDWARF_test +_MSFramelessDWARF_test: + stp x20, x19, [sp, #-16]! +LT1_sub_sp: + mov x19, fp ; Save FP + mov x20, lr ; Save LR + mov fp, xzr ; Overwrite FP + mov lr, xzr ; Overwrite LR + bl _MSFramelessDWARF_test_crash ; Trigger crash + ldp x20, x19, [sp], #16 + mov fp, x19 ; Restore FP + mov lr, x20 ; Restore LR + ret +LT1_end: + +.section __TEXT,__eh_frame,coalesced,no_toc+strip_static_syms+live_support +; Standard CIE for our test functions +EH_frame1: +.set L$set$0,LECIE1-LSCIE1 +.long L$set$0 ; Length of Common Information Entry +LSCIE1: +.long 0x0 ; CIE Identifier Tag +.byte 0x1 ; CIE Version +.ascii "zR\0" ; CIE Augmentation +.byte 0x1 ; uleb128 0x1; CIE Code Alignment Factor +.byte 0x78 ; sleb128 -8; CIE Data Alignment Factor +.byte 0x1E ; CIE RA Column +.byte 0x1 ; uleb128 0x1; Augmentation size +.byte 0x10 ; FDE Encoding (pcrel) +.byte 0xc ; DW_CFA_def_cfa +.byte 0x1F ; uleb128 31 (x31) +.byte 0x0 ; uleb128 0x0 +.align 3 +LECIE1: + +; Generates our common FDE header for register-saved test cases. +; Arguments: +; 0 - Test number (eg, 0, 1, 2). Used to resolve local label names for +; the given test, and to name FDE-specific labels. +; 1 - Test name (eg, x19_x20, no_reg) +; 2 - Stack size, as a uleb128 value +.macro fde_header +.globl _MSFramelessDWARF_$1.eh +_MSFramelessDWARF_$1.eh: +LSFDE$0: +.set Lset0$0,LEFDE$0-LASFDE$0 +.long Lset0$0 ; FDE Length +LASFDE$0: +.long LASFDE$0-EH_frame1 ; FDE CIE offset +.quad _MSFramelessDWARF_$1-. ; FDE initial location +.set Lset1$0,LT$0_end-_MSFramelessDWARF_$1 +.quad Lset1$0 ; FDE address range +.byte 0x0 ; uleb128 0x0; Augmentation size +.byte 0x4 ; DW_CFA_advance_loc4 +.set Lset2$0,LT$0_sub_sp-_MSFramelessDWARF_$1 +.long Lset2$0 +.byte 0xe ; DW_CFA_def_cfa_offset +.byte $2 ; uleb128 stack offset +.endmacro + +; Generates our common FDE printer +; Arguments: +; 0 - Test number (eg, 0, 1, 2). +.macro fde_footer +.align 3 +LEFDE$0: +.endmacro + +; DW_CFA_register rules appear to trigger an ld bug: +; "could not create compact unwind for _MSFramelessDWARF_test: saved registers do not fit in stack size" +; We're only saving two register on the stack, so perhaps ld64 register +; counting code incorrectly assumes DW_CFA_register consumes stack space. +fde_header 1, test, 0x10 +.byte 0x93 ; DW_CFA_offset, column 0x13 +.byte 0x2 ; uleb128 0x2 +.byte 0x94 ; DW_CFA_offset, column 0x14 +.byte 0x3 ; uleb128 0x3 +.byte 0x09 ; DW_CFA_register +.byte 0x1D ; uleb128 29 (fp) +.byte 0x13 ; uleb128 13 (x19) +.byte 0x09 ; DW_CFA_register +.byte 0x1E ; uleb128 29 (lr) +.byte 0x14 ; uleb128 13 (x20) +fde_footer 1 + +#endif /* __arm64__ */ \ No newline at end of file diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSFramelessDWARF_i386.s b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSFramelessDWARF_i386.s new file mode 100644 index 0000000000..a4a78593c6 --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSFramelessDWARF_i386.s @@ -0,0 +1,101 @@ +/* + * Extracted from PLCrashReporter's 1.2-RC2 frame unwinding test cases. + * + * Copyright (c) 2013-2014 Plausible Labs, Inc. All rights reserved. + * Copyright (c) 2008-2011 Apple Inc. All rights reserved. + * + * This file contains Original Code and/or Modifications of Original Code + * as defined in and that are subject to the Apple Public Source License + * Version 2.0 (the 'License'). You may not use this file except in + * compliance with the License. Please obtain a copy of the License at + * http://www.opensource.apple.com/apsl/ and read it before using this + * file. + * + * The Original Code and all software distributed under the License are + * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER + * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, + * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. + * Please see the License for the specific language governing rights and + * limitations under the License. + */ + +#ifdef __i386__ + +.text + +# Frameless, save esi, edi, ebp +.globl _MSFramelessDWARF_test +_MSFramelessDWARF_test: +LFB6: +subl $28, %esp +LCFI17: +movl %esi, 16(%esp) +LCFI18: +movl %edi, 20(%esp) +LCFI19: +movl %ebp, 24(%esp) +LCFI20: +movl $0, %esi +movl $0, %edi +movl $0, %ebp + +call _MSFramelessDWARF_test_crash + +movl 16(%esp), %esi +movl 20(%esp), %edi +movl 24(%esp), %ebp +addl $28, %esp +ret +LFE6: + +.section __TEXT,__eh_frame,coalesced,no_toc+strip_static_syms+live_support +EH_frame1: +.set L$set$0,LECIE1-LSCIE1 +.long L$set$0 # Length of Common Information Entry +LSCIE1: +.long 0x0 # CIE Identifier Tag +.byte 0x1 # CIE Version +.ascii "zR\0" # CIE Augmentation +.byte 0x1 # uleb128 0x1; CIE Code Alignment Factor +.byte 0x7c # sleb128 -4; CIE Data Alignment Factor +.byte 0x8 # CIE RA Column +.byte 0x1 # uleb128 0x1; Augmentation size +.byte 0x10 # FDE Encoding (pcrel) +.byte 0xc # DW_CFA_def_cfa +.byte 0x5 # uleb128 0x5 +.byte 0x4 # uleb128 0x4 +.byte 0x88 # DW_CFA_offset, column 0x8 +.byte 0x1 # uleb128 0x1 +.align 2 +LECIE1: + +.globl _MSFramelessDWARF_test.eh +_MSFramelessDWARF_test.eh: +LSFDE17: +.set L$set$28,LEFDE17-LASFDE17 +.long L$set$28 # FDE Length +LASFDE17: +.long LASFDE17-EH_frame1 # FDE CIE offset +.long LFB6-. # FDE initial location +.set L$set$29,LFE6-LFB6 +.long L$set$29 # FDE address range +.byte 0x0 # uleb128 0x0; Augmentation size +.byte 0x4 # DW_CFA_advance_loc4 +.set L$set$30,LCFI17-LFB6 +.long L$set$30 +.byte 0xe # DW_CFA_def_cfa_offset +.byte 0x20 # uleb128 0x20 +.byte 0x4 # DW_CFA_advance_loc4 +.set L$set$31,LCFI20-LCFI17 +.long L$set$31 +.byte 0x84 # DW_CFA_offset, column 0x4 +.byte 0x2 # uleb128 0x2 +.byte 0x87 # DW_CFA_offset, column 0x7 +.byte 0x3 # uleb128 0x3 +.byte 0x86 # DW_CFA_offset, column 0x6 +.byte 0x4 # uleb128 0x4 +.align 2 +LEFDE17: + +#endif /* __i386__ */ \ No newline at end of file diff --git a/submodules/AppCenter-sdk/CrashLib/CrashLib/MSFramelessDWARF_x86_64.s b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSFramelessDWARF_x86_64.s new file mode 100644 index 0000000000..dbbcfe80dd --- /dev/null +++ b/submodules/AppCenter-sdk/CrashLib/CrashLib/MSFramelessDWARF_x86_64.s @@ -0,0 +1,89 @@ +/* + * Extracted from PLCrashReporter's 1.2-RC2 frame unwinding test cases. + * + * Copyright (c) 2013-2014 Plausible Labs, Inc. All rights reserved. + * Copyright (c) 2008-2011 Apple Inc. All rights reserved. + * + * This file contains Original Code and/or Modifications of Original Code + * as defined in and that are subject to the Apple Public Source License + * Version 2.0 (the 'License'). You may not use this file except in + * compliance with the License. Please obtain a copy of the License at + * http://www.opensource.apple.com/apsl/ and read it before using this + * file. + * + * The Original Code and all software distributed under the License are + * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER + * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, + * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. + * Please see the License for the specific language governing rights and + * limitations under the License. + */ + +#ifdef __x86_64__ + +.text +.globl _MSFramelessDWARF_test +_MSFramelessDWARF_test: +# Frameless, saved rbx and rbp +LFB7: +movq %rbx, -16(%rsp) +LCFI8: +movq %rbp, -8(%rsp) +LCFI9: +subq $24, %rsp +LCFI10: +movq $0, %rbp +movq $0, %rbx +call _MSFramelessDWARF_test_crash +movq 8(%rsp), %rbx +movq 16(%rsp), %rbp +addq $24, %rsp +ret +LFE7: + +.section __TEXT,__eh_frame,coalesced,no_toc+strip_static_syms+live_support +EH_frame1: +.set L$set$0,LECIE1-LSCIE1 +.long L$set$0 # Length of Common Information Entry +LSCIE1: +.long 0x0 # CIE Identifier Tag +.byte 0x1 # CIE Version +.ascii "zR\0" # CIE Augmentation +.byte 0x1 # uleb128 0x1; CIE Code Alignment Factor +.byte 0x78 # sleb128 -8; CIE Data Alignment Factor +.byte 0x10 # CIE RA Column +.byte 0x1 # uleb128 0x1; Augmentation size +.byte 0x10 # FDE Encoding (pcrel) +.byte 0xc # DW_CFA_def_cfa +.byte 0x7 # uleb128 0x7 +.byte 0x8 # uleb128 0x8 +.byte 0x90 # DW_CFA_offset, column 0x10 +.byte 0x1 # uleb128 0x1 +.align 3 +LECIE1: + +.globl _MSFramelessDWARF_test.eh +_MSFramelessDWARF_test.eh: +LSFDE14: +.set L$set$21,LEFDE14-LASFDE14 +.long L$set$21 # FDE Length +LASFDE14: +.long LASFDE14-EH_frame1 # FDE CIE offset +.quad LFB7-. # FDE initial location +.set L$set$22,LFE7-LFB7 +.quad L$set$22 # FDE address range +.byte 0x0 # uleb128 0x0; Augmentation size +.byte 0x4 # DW_CFA_advance_loc4 +.set L$set$23,LCFI10-LFB7 +.long L$set$23 +.byte 0xe # DW_CFA_def_cfa_offset +.byte 0x20 # uleb128 0x20 +.byte 0x86 # DW_CFA_offset, column 0x6 +.byte 0x2 # uleb128 0x2 +.byte 0x83 # DW_CFA_offset, column 0x3 +.byte 0x3 # uleb128 0x3 +.align 3 +LEFDE14: + +#endif /* __x86_64__ */ \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/.gitignore b/submodules/AppCenter-sdk/Vendor/OCHamcrest/.gitignore new file mode 100644 index 0000000000..9afdc2fe56 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/.gitignore @@ -0,0 +1,10 @@ +*.gcda +*.gcno +.DS_Store +.idea +build/ +*.pbxuser +*.perspectivev3 +project.xcworkspace/ +xcuserdata/ +compile_commands.json diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/.slather.yml b/submodules/AppCenter-sdk/Vendor/OCHamcrest/.slather.yml new file mode 100644 index 0000000000..f3290fd8db --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/.slather.yml @@ -0,0 +1,6 @@ +coverage_service: coveralls +xcodeproj: ./Source/OCHamcrest.xcodeproj +scheme: libochamcrest +ignore: + - Source/Tests/* + \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/.travis.yml b/submodules/AppCenter-sdk/Vendor/OCHamcrest/.travis.yml new file mode 100644 index 0000000000..254e7d8cd2 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/.travis.yml @@ -0,0 +1,14 @@ +language: objective-c +osx_image: xcode11.6 + +before_install: + - gem install slather + +script: + - set -o pipefail; + - xcodebuild test -project Source/OCHamcrest.xcodeproj -scheme OCHamcrest -sdk macosx | xcpretty + - xcodebuild test -project Source/OCHamcrest.xcodeproj -scheme libochamcrest -sdk iphonesimulator -destination "platform=iOS Simulator,OS=latest,name=iPhone 8" | xcpretty + - pod spec lint --quick + +after_success: + - slather diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/CHANGELOG.md b/submodules/AppCenter-sdk/Vendor/OCHamcrest/CHANGELOG.md new file mode 100644 index 0000000000..963cd199fa --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/CHANGELOG.md @@ -0,0 +1,725 @@ +Version 7.1.2 +------------- +_23 Sep 2019_ + +**Fixes:** + +- Fix warning about double-quoted includes in public headers. + + +Version 7.1.1 +------------- +_15 Jun 2018_ + +**Fixes:** + +- Fixed crash with HCArgumentCaptor capturing objects that don't conform to NSCopyable. + + +Version 7.1.0 +------------- +_21 Mar 2018_ + +**Features:** + +- Made OCHamcrest/OCHamcrestIOS into modules, so you can `@import` them. CocoaPods users should specify `use_frameworks!` + +**Fixes:** + +- Fixed crash with HCArgumentCaptor capturing blocks. + + +Version 7.0.2 +------------- +_19 Sep 2017_ + +**Fixes:** + +- Fixed new warnings from Xcode 9. + + +Version 7.0.1 +------------- +_12 Aug 2017_ + +**Fixes:** + +- Remove exposed instance variables that triggered warnings for `-Wobjc-interface-ivars`. + +I doubt that it affects anyone, but converting public ivars to private properties does have the +potential to break backwards compatibility. Please notify me if you have any code that complains. + +**Project changes:** + +- Increase macOS deployment target to 10.10. (iOS was already at 8.0.) + +(Version 7.0.0 mistakenly required projects to Enable Modules and had out-of-date documentation.) + + +Version 6.1.1 +------------- +_06 Mar 2017_ + +**Fixes:** + +- Fixed nullability mistake: HCWrapInMatcher(nil) should return nil. + + +Version 6.1.0 +------------- +_17 Feb 2017_ + +**Features:** + +- Adopt latest Objective-C annotations for designated initializers, unavailable initializers, typed arrays, nullability. + + +Version 6.0.0 +------------- +_04 Aug 2016_ + +**Features:** + +- Improved mismatch descriptions for `contains`, `containsIn` when actual collection exceeds expected size. + +**Deleted:** + +- `equalToIgnoringWhiteSpace` matcher (deprecated in v5.4.0) +- HCCollectMatchers (deprecated in v5.3.0) + +**Project changes:** + +- Increased deployment targets to OS X 10.9, iOS 7.0. + + +Version 5.4.0 +------------- +_03 Jun 2016_ + +**Features:** + +- Added `captureEnabled` property to HCArgumentCaptor to control whether subsequent matched values are captured. + +**Improvements:** + +- Updated CocoaPods instructions and examples to CocoaPods 1.0. + +**Deprecated:** + +- `equalToIgnoringWhiteSpace` has been renamed to `equalToCompressingWhiteSpace`. +- Known issue: warning on this deprecation + + +Version 5.3.0 +------------- +_22 May 2016_ + +**Fixes:** + +- Removed semicolons that triggered warnings for `-Wsemicolon-before-method-body`. _Thanks to: Sylvain Defresne_ +- Describe `isIn` matcher in README. + +**Features:** + +- Rewrote `assertThatAfter` to use runloop observer instead of while loop comparing dates. The condition is now tested on every pump of the runloop instead of polling after a predefined delay. _Thanks to: Dan Fleming_ + +**Deprecated:** + +- Deprecated HCCollectMatchers. Instead, follow the example of HCAllOf.m and break it into two steps: HCCollectItems, then HCWrapIntoMatchers. This will let you expose a new interface to your matcher that takes an NSArray. + + +Version 5.2.0 +------------- +_16 Jan 2016_ + +**Fixes:** + +- Fixed umbrella header for Carthage. _Thanks to: Sylvain Rebaud, Engin Kurutepe_ + +**Features:** + +- Improved mismatch descriptions for `allOf`, `allOfIn`. + + +Version 5.1.0 +------------- +_14 Dec 2015_ + +**Features:** + +- Added HCDescribeMismatch, a helper function to describe mismatches the way `assertThat` does. +- Added Carthage support for Mac, iOS, watchOS and tvOS. _Thanks to: Nikolaj Schumacher_ + + +Version 5.0.0 +------------- +_02 Nov 2015_ + +For detailed discussion on v5.0.0, see https://qualitycoding.org/ochamcrest-v5-0-0/ + +**Features:** + +- Instead of enabling short syntax by defining HC_SHORTHAND, short syntax is now enabled by default. + To disable it, #define HC_DISABLE_SHORT_SYNTAX. +- Matchers which take nil-terminated lists have "In" variants which take a single NSArray, like + `allOfIn`. The matcher `hasEntriesIn` is an exception and takes an NSDictionary. +- Improved documentation on all matchers. Documentation is now shown for matchers with fixed numbers + of arguments. All matchers provide argument hinting. + +**Renamed:** + +- Renamed long syntax for `containsInRelativeOrder` from prefix hc_ to HC_ to conform to other + matchers. + +**Deleted:** + +- `equalToBool` matcher (deprecated in v4.1.0) +- `containsString` matcher (deprecated in v4.2.0) +- `assertThatAfter`/`futureValueOf` (deprecated in v4.2.0) +- `HC_testFailureHandlerChain()` (deprecated in v4.2.0) + + +Version 4.3.2 +------------- +_31 Oct 2015_ + +**Project changes:** + +- Enabling "Symbols hidden by default" in 4.3.1 was overkill, preventing people from using the + prebuilt Mac framework. + + +Version 4.3.1 +------------- +_24 Oct 2015_ + +**Project changes:** + +- Remove debug symbols from Release configuration, which bloated the libraries and kept folks from + using the prebuilt iOS framework. + + +Version 4.3.0 +------------- +_11 Oct 2015_ + +**Features:** + +- New matcher `containsInRelativeOrder` matches collections containing items in relative order. +- New matcher HCArgumentCaptor matches anything, capturing matched values. + +**Project changes:** + +- Updated project settings to Xcode 7, with tests now run by XCTest. + + +Version 4.2.0 +------------- +_11 Sep 2015_ + +**Fixes:** + +- Fixed "Incompatible pointer types sending 'Class' to parameter of type 'NSString *'" warning on + `instanceOf`. + +**Features:** + +- Improved readability of asynchronous tests: `assertWithTimeout(1, thatEventually(var), is(@10));` +- Added ability to add custom test failure reporter. See HCTestFailureReporterChain. + +**Deprecated:** + +- Deprecated `containsString`; use `containsSubstring` instead. `containsString` clashes with an + NSString method introduced in iOS 8. +- Deprecated `assertThatAfter`/`futureValueOf`. Use `assertWithTimeout`/`thatEventually` instead. +- Deprecated `HC_testFailureHandlerChain()`; use `[HCTestFailureReporterChain reporterChain]` + instead. + + +Version 4.1.1 +------------- +_31 Dec 2014_ + +- Oops! Add the new features to OCHamcrest.h + + +Version 4.1.0 +------------- +_30 Dec 2014_ + +**Fixes:** + +- Fixed crash when OCHamcrest tries to describe an OCMockito mock object. _Thanks to: Michael Seghers_ +- Fixed crash when `equalToBool` attempts to match a non-number. + +**Features:** + +- `assertThatAfter` tests asynchronous code, retrying the assertion until a given timeout. + Wrap the code you want to evaluate in `futureValueOf`. _Thanks to: Sergio Padrino_ +- New matcher `everyItem` matches collections if every item satisfies a given matcher. +- New matcher `throwsException` matches a block if it throws an exception satisfying a given + matcher. +- New matchers `isTrue` and `isFalse` match non-zero and zero NSNumbers. Intended to replace + `equalToBool`. + +**Improvements:** + +- Added new base class HCDiagnosingMatcher to simplify complex matchers. +- `equalToBool` matcher can no longer be created with a value other than YES or NO. This especially + avoids the accidental @YES. +- Improved ordered comparison matchers (`greaterThan`, etc.) so that when the given object can't be + compared, the matchers return NO instead of throwing an exception. +- Improved mismatch descriptions for `hasItem`. +- Improved mismatch descriptions for `hasProperty` to show actual property value or "no property". +- Improved mismatch descriptions for `onlyContains`, especially in reporting all elements that don't + match. +- Updated project to make it run-path dependent. _Thanks to: csano_ + +**Deprecated:** + +- `equalToBool` deprecated in favor of `isTrue` and `isFalse`. `equalToBool(YES)` had too much + potential for semantic error since any non-zero number evaluates to true. + + + +Version 4.0.1 +------------- +_04 Jun 2014_ + +**Project changes:** + +- Increased deployment targets to OS X 10.8, iOS 6.0. + + +Version 4.0.0 +------------- +_10 May 2014_ + +This is a refactoring release with potential backwards compatibility issues for writers of custom +matchers: + +- Almost all ivars have been converted to hidden properties. Let me know if this trips you up. +- If you subclass HCInvocationMatcher for a custom matcher, the ivars have been renamed. +- If you import HCCollectMatchers.h for a custom matcher, change this to import HCCollect.h. +- HCTestFailureHandler has changed from a protocol to a class. + +Also, if you're not using CocoaPods, specify `-ObjC` in your "Other Linker Flags". + + +Version 3.0.1 +------------- +_29 Oct 2013_ + +**Fixes:** + +- Fixed problem where isNot did not ask the sub-matcher's mismatch description. _Thanks to: James + Richard and Jonathan Barnes_ +- Fixed crash in `describedAs` matcher . _Thanks to: Nikolaj Schumacher_ +- Fixed crash in `hasProperty` matcher when the property is a primitive type. _Thanks to: Nikolaj + Schumacher_ + +**Improvements:** + +- Changed matcher factory methods to return plain `id` so that matchers can be used without casting + to `(id)` for OCMockito arguments. +- Added support for 64-bit iOS devices. + +**Examples & Documentation:** + +- Updated examples so they are based on Apple's templates for main target vs. test target. Added + CocoaPods examples. +- Eliminated DocSet. Documentation will be in the main README and in the OCHamcrest wiki, + https://github.com/hamcrest/OCHamcrest/wiki/_pages + + +Version 3.0.0 +------------- +_06 Sep 2013_ + +**Features:** + +- Added support for XCTest. _Special thanks to Jiajun "gaosboy" for pointing the way, and to Richard + Clem for testing._ +- Made unit test integration more flexible with HC_testFailureHandlerChain. It can be called from + outside OCHamcrest to signal a test failure within the current testing framework. (At present it + tries XCTest, then SenTestCase, then falls back on raising a generic exception.) + +**Deleted:** + +- HCRequireNonNilString.h (deprecated in v1.2) +- `empty` matcher (deprecated in v2.1.0) + + +Version 2.1.0 +------------- +_23 Jun 2013_ + +**Fixes:** + +- Made build script flexible so that doxygen isn't required, or can be in a different location. + _Thanks to: Bennett Smith_ +- Fixed problem formatting percent symbols in assertion failures. _Thanks to: Nikolaj Schumacher_ +- Fixed wrong descriptions in the unordered collection matcher. _Thanks to: Nikolaj Schumacher_ +- Fixed underlying cause of crash in Mac version on assertion failure. With this fix, we could switch + back to optimized code. _Thanks to: Nikolaj Schumacher_ +- Fixed MacExample so it finds OCHamcrest. + +**Features:** + +- Added support for XCTest. (This was undone by subsequent updates to XCTest.) + +**Deprecated:** + +- `empty` clashed with the C++ string method of the same name. It has been renamed to `isEmpty`. + +**Project changes:** + +- Increased deployment targets to Mac OS X 10.7, iOS 5.0. + + +Version 2.0.1 +------------- +_12 May 2013_ + +**Fixes:** + +- Fixed crash in Mac version on assertion failure. (Problem with optimized code) +- Fixed crash in `instanceOf` and `isA` when argument was `nil`. + +**Improvements:** + +- Updated example projects to Xcode 4.6. + + +Version 2.0.0 +------------- +_13 Apr 2013_ + +This release adopts Semantic Versioning (http://semver.org). Since removal of deprecated items is a +backwards incompatible change, the major version number is incremented. _Thanks to: Jens Nerup_ + +**Fixes:** + +- Fixed GTM compatibility -- avoid shadowing `conformsToProtocol:` + +**New matchers:** + +- `isA` matches objects that are instances of a given class, but not of any subclass. + +**Improved matchers:** + +- `equalToBool` has a better description. _Thanks to: Jonathan Crooke_ +- `instanceOf` mismatch description now includes actual class. + +**Deleted following items deprecated in v1.8:** + +- C++ template function `boxNumber` +- `HC_conformsToProtocol` (which was renamed to `HC_conformsTo`) + +**Project changes:** + +- Updated project settings to Xcode 4.6. _Thanks to: Florian Buerger_ +- Fixed unit tests so they remain quiet when handling expected test failures. +- Replaced older code coverage scripts with XcodeCoverage submodule. + + +Version 1.9 +----------- +_23 Nov 2012_ + +**Improved matchers:** + +- Changed `hasCount` / `hasCountOf` mismatch description so that count comes first (if object + has a count). + +**Project changes:** + +- Fixed warnings revealed by latest Xcode. _Thanks to: David Hart_ +- Changed iOS Architecture support to "Standard" (which includes armv7s) +- Changed Mac Architecture support to 64-bit only +- Converted source, tests, and examples to ARC + + +Version 1.8 +----------- +_09 Jul 2012_ + +The primary purpose of this release is to make it easier to add OCHamcrest to iOS projects. No more +need to specify "Other Linker Flags"! Depending on your project, you may be able to eliminate: + + * `-lstdc++` + * `-ObjC` + +Also, the repository has a new official home: https://github.com/hamcrest/OCHamcrest/ + +**No need to specify "Other Linker Flags" in iOS projects:** + +- Changed all Objective-C++ to Objective-C +- Eliminated categories + +**Deprecated:** + +- C++ template function `boxNumber`. +- `conformsToProtocol` clashed with the method of the same name. It has been renamed to + `conformsTo`. + +**Deleted following items deprecated in v1.2:** + +- `-[HCDescription appendValue]` +- `+[HCInvocationMatcher createInvocationForSelector:onClass:]` +- NSObject+SelfDescribingValue + + +Version 1.7 +----------- +_20 Feb 2012_ + +**New matchers:** + +- `conformsToProtocol` matches objects that conform to a given protocol. _Thanks to: Todd Farrell_ + +**Improved matchers:** + +- `hasProperty` now works for methods with primitive return types. _Thanks to: Christopher + Pickslay_ + +**Other improvements:** + +- Rewrote introductory sections of documentation. + + +Version 1.6 +----------- +_27 Sep 2011_ + +**Fixes:** + +- `stringContainsInOrder` was missing from the master header; now it's there. + +**New matchers:** + +- `hasProperty` matches the return value of a method with a given name. (It could be a property, + but really can be any method with no arguments that returns an object.) _Thanks to: Justin + Shacklette_ + +**Improvements:** + +- Rewrote documentation. +- Matchers that require a nil-terminated list now generate a compiler error if you don't have + `nil` at the end. + + +Version 1.5 +----------- +_29 Apr 2011_ + +**Fixes:** + +- Fixed crash when trying to describe an object with `nil` description. + +**Packaging:** + +- Updated project to Xcode 4. iOS framework / documentation / distribution scripts are now external, + run from command-line instead of Xcode. +- Improved documentation by adding Factory headings pointing from implementing classes back to their + factories. + +**New matcher:** + +- `stringContainsInOrder` matches string containing given list of substrings, in order. + +**Improved matchers:** + +- Changed `sameInstance` mismatch description to omit address when describing `nil`. +- For consistency, changed `anyOf` and `allOf` to implicitly wrap non-matcher values in + `equalTo`. + + +Version 1.4 +----------- +_13 Feb 2011_ + +**New matchers:** + +- `hasEntries` matches dictionary containing key-value pairs satisfying a given list of + alternating keys and value matchers. + +**Improvements:** + +- Added complete descriptions to macros so they appear in Xcode 4 Quick Help. (Couldn't add + arguments to macros without breaking backwards compatibility.) + +**Improved descriptions:** + +- Improved description of `hasEntry`, removing colon so it doesn't get truncated in Xcode's error + display. +- `is` no longer says "is ..." in its description, but just lets the inner description pass + through. +- Consistently use articles to begin descriptions, such as "a dictionary containing" instead of + "dictionary containing". + + +Version 1.3 +----------- +_05 Jan 2011_ + +**Improved descriptions:** + +- Fixed `contains` and `containsInAnyOrder` to describe mismatch if item is not a collection. +- Fixed `describedAs` and `is` to use their nested matchers to generate mismatch descriptions. +- `sameInstance` is more readable, and includes object memory addresses. +- If object has a count, `hasCount` mismatch describes actual count. +- Don't wrap angle brackets around a description that already has them. +- Improved readability of several matchers. + +**Other improvements:** + +- `instanceOf` now guards against `nil` being passed as the expected type. + + +Version 1.2 +----------- +_03 Jan 2011_ + +**Fixes:** + +- Fixed assertThat to describe the diagnosis of the mismatch, not just the mismatched value. + +**New matchers:** + +- `hasCount` matches collections for which `-count` satisfies a given matcher. +- `hasCountOf` matches collections for which `-count` equals a given count. +- `empty` matches empty collection. + +**Improvements:** + +- Expanded helper class HCInvocationMatcher: + * New property `shortMismatchDescription` determines whether mismatch description is short or + long. Default is long description. + * New method `-invokeOn:` invokes stored invocation on given item. +- Since `nil` cannot be directly stored in collections, collection matchers now guard against + `nil`. +- Expanded BaseDescription's `-appendDescriptionOf:` to handle all types of values, not just + self-describing values. + +**Deprecated:** + +- Description's `-appendValue:` no longer needed; call `-appendDescriptionOf:` instead. This + also means NSObject+SelfDescribingValue is no longer needed. +- Renamed HCInvocation's helper class method `+createInvocationForSelector:onClass:` to + `+invocationForSelector:onClass:` +- New helper function `HCRequireNonNilObject` should be used in place of + `HCRequireNonNilString`. + + +Version 1.1.2 +------------- +_28 Dec 2010_ + +**Fixes:** + +- Fixed crash that occurred when trying to describe the matchers `allOf`, `anyOf` and `isIn` + on iOS. Related to `-ObjC` linker flag. +- Fix problems introduced in broken release v1.1: + * Added the new matchers to the master header and to the iOS target. + * Fixed distribution zip file to preserve symlinks in frameworks. + +**New matchers:** + +- `contains` matches collections with matching items in order. +- `containsInAnyOrder` matches collections with matching items in any order. + +**Improvements:** + +- Changed documentation from HTML to Xcode documentation set. Run `make install` from the + Documentation folder to install. +- Rearranged documentation modules to make things easier to find. +- Changed convenience methods to invoke superclass and return `id`, to support possibility of + subclassing matchers. + + +Version 1.0 +----------- +_26 Oct 2010_ + +First official release, including: + +* Documentation +* Examples for + - Cocoa + - iOS + - Creating a custom matcher + + +Before v1.0 +----------- + +_07 Oct 2010_ + +* For iOS: Added OCHamcrestIOS.framework target which provides release builds for both simulator and + device into a single framework. Yes, a framework that can be used for iOS development without + requiring users to mess with header search paths and link settings. Simply drop the framework into + your project and `#import ` _Thanks to: Aaron Jacobs_ + +_06 Oct 2010_ + +* Work around bug in iPhone simulator that causes a test failure to terminate the app. _Thanks to: + Aaron Jacobs_ + +_06 Sep 2010_ + +* Added static library target and changes for iOS. + +_01 Dec 2009_ + +* Added `assertThat___` and `equalTo___` for all types understood by NSNumber. For example: + `assertThatInt(42, equalToInt(42))` + +_24 Nov 2009_ + +* Changed `assertThat` behavior to work more seamlessly with OCUnit: Instead of throwing an + exception, it calls the same method as OCUnit's assertion macros to fail the test. As a result, a + failing `assertThat` will not terminate the test, so that the test can record other failures. + (Following normal OCUnit behavior, you can instruct the test case to terminate at the first + failure by invoking `raiseAfterFailure`.) + This change requires that `assertThat` be called only within subclasses of SenTestCase, which + wasn't the case before. You will need to recompile your tests. + If you need an original-style assertion that can be called outside of a SenTestCase, email your + request to hamcrest-dev@googlegroups.com + +_24 Nov 2009_ + +* Support Xcode 3.2's redesigned Build Results window by removing colons from `assertThat` + description. + +_17 Oct 2009_ + +* Added helper class HCInvocationMatcher for building other matchers from NSInvocations. See + HCHasDescription for an example. + +_11 Aug 2009_ + +* Renamed framework to OCHamcrest. + +_07 Jul 2009_ + +* Added support for Mac OS X 10.4 projects. + +_28 Jan 2009_ + +* Fixed compiler errors when used with Objective-C++ (.mm files). + +_24 Jan 2009_ + +* Added means for matchers to describe mismatches. You can use either + `matches:describingMismatchTo:` to do it one shot, or call `describeMismatchOf:To:` once you + know a particular item does not match. + +_18 Jul 2008_ + +* Changed matchers whose description looks similar to a function call so that the description + matches the name of the factory function. + +_13 Apr 2008_ + +* Initial release diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/CustomDateMatcher-CocoaPods/Example.xcodeproj/project.pbxproj b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/CustomDateMatcher-CocoaPods/Example.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..5444dd00ec --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/CustomDateMatcher-CocoaPods/Example.xcodeproj/project.pbxproj @@ -0,0 +1,431 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 082CEC3F18144C2F0013FC27 /* Example.m in Sources */ = {isa = PBXBuildFile; fileRef = 082CEC3E18144C2F0013FC27 /* Example.m */; }; + 082CEC4A18144C2F0013FC27 /* libExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 082CEC3118144C2F0013FC27 /* libExample.a */; }; + 082CEC5218144C2F0013FC27 /* ExampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 082CEC5118144C2F0013FC27 /* ExampleTests.m */; }; + 082CEC5D18144CA10013FC27 /* IsGivenDayOfWeek.m in Sources */ = {isa = PBXBuildFile; fileRef = 082CEC5C18144CA10013FC27 /* IsGivenDayOfWeek.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 082CEC4818144C2F0013FC27 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 082CEC2918144C2F0013FC27 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 082CEC3018144C2F0013FC27; + remoteInfo = Example; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 082CEC3118144C2F0013FC27 /* libExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libExample.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 082CEC3D18144C2F0013FC27 /* Example.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Example.h; sourceTree = ""; }; + 082CEC3E18144C2F0013FC27 /* Example.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Example.m; sourceTree = ""; }; + 082CEC4418144C2F0013FC27 /* ExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 082CEC4D18144C2F0013FC27 /* ExampleTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "ExampleTests-Info.plist"; sourceTree = ""; }; + 082CEC5118144C2F0013FC27 /* ExampleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ExampleTests.m; sourceTree = ""; }; + 082CEC5B18144CA10013FC27 /* IsGivenDayOfWeek.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IsGivenDayOfWeek.h; sourceTree = ""; }; + 082CEC5C18144CA10013FC27 /* IsGivenDayOfWeek.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IsGivenDayOfWeek.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 082CEC2E18144C2F0013FC27 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 082CEC4118144C2F0013FC27 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 082CEC4A18144C2F0013FC27 /* libExample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 082CEC2818144C2F0013FC27 = { + isa = PBXGroup; + children = ( + 082CEC3A18144C2F0013FC27 /* Example */, + 082CEC4B18144C2F0013FC27 /* ExampleTests */, + 082CEC3218144C2F0013FC27 /* Products */, + ); + sourceTree = ""; + }; + 082CEC3218144C2F0013FC27 /* Products */ = { + isa = PBXGroup; + children = ( + 082CEC3118144C2F0013FC27 /* libExample.a */, + 082CEC4418144C2F0013FC27 /* ExampleTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 082CEC3A18144C2F0013FC27 /* Example */ = { + isa = PBXGroup; + children = ( + 082CEC3D18144C2F0013FC27 /* Example.h */, + 082CEC3E18144C2F0013FC27 /* Example.m */, + ); + path = Example; + sourceTree = ""; + }; + 082CEC4B18144C2F0013FC27 /* ExampleTests */ = { + isa = PBXGroup; + children = ( + 082CEC5B18144CA10013FC27 /* IsGivenDayOfWeek.h */, + 082CEC5C18144CA10013FC27 /* IsGivenDayOfWeek.m */, + 082CEC5118144C2F0013FC27 /* ExampleTests.m */, + 082CEC4C18144C2F0013FC27 /* Supporting Files */, + ); + path = ExampleTests; + sourceTree = ""; + }; + 082CEC4C18144C2F0013FC27 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 082CEC4D18144C2F0013FC27 /* ExampleTests-Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 082CEC3018144C2F0013FC27 /* Example */ = { + isa = PBXNativeTarget; + buildConfigurationList = 082CEC5518144C2F0013FC27 /* Build configuration list for PBXNativeTarget "Example" */; + buildPhases = ( + 082CEC2D18144C2F0013FC27 /* Sources */, + 082CEC2E18144C2F0013FC27 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Example; + productName = Example; + productReference = 082CEC3118144C2F0013FC27 /* libExample.a */; + productType = "com.apple.product-type.library.static"; + }; + 082CEC4318144C2F0013FC27 /* ExampleTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 082CEC5818144C2F0013FC27 /* Build configuration list for PBXNativeTarget "ExampleTests" */; + buildPhases = ( + FA900F3031C93D61924662F4 /* [CP] Check Pods Manifest.lock */, + 082CEC4018144C2F0013FC27 /* Sources */, + 082CEC4118144C2F0013FC27 /* Frameworks */, + 0AF219B78FC62E026F664444 /* [CP] Embed Pods Frameworks */, + D2CE231A6814BFA54AD915E3 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + 082CEC4918144C2F0013FC27 /* PBXTargetDependency */, + ); + name = ExampleTests; + productName = ExampleTests; + productReference = 082CEC4418144C2F0013FC27 /* ExampleTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 082CEC2918144C2F0013FC27 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = "Quality Coding"; + }; + buildConfigurationList = 082CEC2C18144C2F0013FC27 /* Build configuration list for PBXProject "Example" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 082CEC2818144C2F0013FC27; + productRefGroup = 082CEC3218144C2F0013FC27 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 082CEC3018144C2F0013FC27 /* Example */, + 082CEC4318144C2F0013FC27 /* ExampleTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0AF219B78FC62E026F664444 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-ExampleTests/Pods-ExampleTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + D2CE231A6814BFA54AD915E3 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-ExampleTests/Pods-ExampleTests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + FA900F3031C93D61924662F4 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-ExampleTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 082CEC2D18144C2F0013FC27 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 082CEC3F18144C2F0013FC27 /* Example.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 082CEC4018144C2F0013FC27 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 082CEC5218144C2F0013FC27 /* ExampleTests.m in Sources */, + 082CEC5D18144CA10013FC27 /* IsGivenDayOfWeek.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 082CEC4918144C2F0013FC27 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 082CEC3018144C2F0013FC27 /* Example */; + targetProxy = 082CEC4818144C2F0013FC27 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 082CEC5318144C2F0013FC27 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_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_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + 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; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.10; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + }; + name = Debug; + }; + 082CEC5418144C2F0013FC27 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_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_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + 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; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.10; + SDKROOT = macosx; + }; + name = Release; + }; + 082CEC5618144C2F0013FC27 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 082CEC5718144C2F0013FC27 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + 082CEC5918144C2F0013FC27 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(DEVELOPER_FRAMEWORKS_DIR)", + "$(inherited)", + ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = "ExampleTests/ExampleTests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "org.qualitycoding.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = xctest; + }; + name = Debug; + }; + 082CEC5A18144C2F0013FC27 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(DEVELOPER_FRAMEWORKS_DIR)", + "$(inherited)", + ); + INFOPLIST_FILE = "ExampleTests/ExampleTests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "org.qualitycoding.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = xctest; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 082CEC2C18144C2F0013FC27 /* Build configuration list for PBXProject "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 082CEC5318144C2F0013FC27 /* Debug */, + 082CEC5418144C2F0013FC27 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 082CEC5518144C2F0013FC27 /* Build configuration list for PBXNativeTarget "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 082CEC5618144C2F0013FC27 /* Debug */, + 082CEC5718144C2F0013FC27 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 082CEC5818144C2F0013FC27 /* Build configuration list for PBXNativeTarget "ExampleTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 082CEC5918144C2F0013FC27 /* Debug */, + 082CEC5A18144C2F0013FC27 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 082CEC2918144C2F0013FC27 /* Project object */; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/CustomDateMatcher-CocoaPods/Example/Example.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/CustomDateMatcher-CocoaPods/Example/Example.h new file mode 100644 index 0000000000..7382782e97 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/CustomDateMatcher-CocoaPods/Example/Example.h @@ -0,0 +1,4 @@ +@import Foundation; + +@interface Example : NSObject +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/CustomDateMatcher-CocoaPods/Example/Example.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/CustomDateMatcher-CocoaPods/Example/Example.m new file mode 100644 index 0000000000..f85d8711cc --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/CustomDateMatcher-CocoaPods/Example/Example.m @@ -0,0 +1,4 @@ +#import "Example.h" + +@implementation Example +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/CustomDateMatcher-CocoaPods/ExampleTests/ExampleTests-Info.plist b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/CustomDateMatcher-CocoaPods/ExampleTests/ExampleTests-Info.plist new file mode 100644 index 0000000000..169b6f710e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/CustomDateMatcher-CocoaPods/ExampleTests/ExampleTests-Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/CustomDateMatcher-CocoaPods/ExampleTests/ExampleTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/CustomDateMatcher-CocoaPods/ExampleTests/ExampleTests.m new file mode 100644 index 0000000000..c408cf2b26 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/CustomDateMatcher-CocoaPods/ExampleTests/ExampleTests.m @@ -0,0 +1,44 @@ +#import "IsGivenDayOfWeek.h" +@import OCHamcrest; +@import XCTest; + + +@interface ExampleTests : XCTestCase +@end + +@implementation ExampleTests + +- (void)testDateIsOnASaturday +{ + // Example of a successful match. + NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; + dateComponents.day = 26; + dateComponents.month = 4; + dateComponents.year = 2008; + NSCalendar *gregorianCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; + NSDate *date = [gregorianCalendar dateFromComponents:dateComponents]; + + assertThat(date, is(onASaturday())); +} + +- (void)testFailsWithMismatchedDate +{ + // Example of what happens with date that doesn't match. + NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; + dateComponents.day = 6; + dateComponents.month = 4; + dateComponents.year = 2008; + NSCalendar *gregorianCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; + NSDate *date = [gregorianCalendar dateFromComponents:dateComponents]; + + assertThat(date, is(onASaturday())); +} + +- (void)testFailsWithNonDate +{ + // Example of what happens with object that isn't a date. + NSString* nonDate = @"oops"; + assertThat(nonDate, is(onASaturday())); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/CustomDateMatcher-CocoaPods/ExampleTests/IsGivenDayOfWeek.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/CustomDateMatcher-CocoaPods/ExampleTests/IsGivenDayOfWeek.h new file mode 100644 index 0000000000..53bfb5611a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/CustomDateMatcher-CocoaPods/ExampleTests/IsGivenDayOfWeek.h @@ -0,0 +1,15 @@ +#import + + +// Matches dates that fall on a given day of the week. +@interface IsGivenDayOfWeek : HCBaseMatcher + +@property (nonatomic, readonly, assign) NSInteger dayOfWeek; // Sunday is 1, Saturday is 7 + +- (instancetype)initWithDayOfWeek:(NSInteger)dayOfWeek; + +@end + + +// Factory function to generate Saturday matcher. +FOUNDATION_EXPORT id onASaturday(); diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/CustomDateMatcher-CocoaPods/ExampleTests/IsGivenDayOfWeek.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/CustomDateMatcher-CocoaPods/ExampleTests/IsGivenDayOfWeek.m new file mode 100644 index 0000000000..8cefe95fc9 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/CustomDateMatcher-CocoaPods/ExampleTests/IsGivenDayOfWeek.m @@ -0,0 +1,39 @@ +#import "IsGivenDayOfWeek.h" + +static NSString* const dayAsString[] = + { @"ZERO", @"Sunday", @"Monday", @"Tuesday", @"Wednesday", @"Thursday", @"Friday", @"Saturday" }; + + +@implementation IsGivenDayOfWeek + +- (instancetype)initWithDayOfWeek:(NSInteger)dayOfWeek +{ + self = [super init]; + if (self) + _dayOfWeek = dayOfWeek; + return self; +} + +// Test whether item matches. +- (BOOL)matches:(id)item +{ + if (![item isKindOfClass:[NSDate class]]) + return NO; + + NSCalendar *gregorianCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; + return [gregorianCalendar component:NSCalendarUnitWeekday fromDate:item] == self.dayOfWeek; +} + +// Describe the matcher. +- (void)describeTo:(id )description +{ + [[description appendText:@"date falling on "] appendText:dayAsString[self.dayOfWeek]]; +} + +@end + + +id onASaturday() +{ + return [[IsGivenDayOfWeek alloc] initWithDayOfWeek:7]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/CustomDateMatcher-CocoaPods/Podfile b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/CustomDateMatcher-CocoaPods/Podfile new file mode 100644 index 0000000000..3fe0e7b30f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/CustomDateMatcher-CocoaPods/Podfile @@ -0,0 +1,4 @@ +target :ExampleTests do + inherit! :search_paths + pod 'OCHamcrest', '~> 7.1' +end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/CustomDateMatcher-CocoaPods/README.md b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/CustomDateMatcher-CocoaPods/README.md new file mode 100644 index 0000000000..3a1b2f34d6 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/CustomDateMatcher-CocoaPods/README.md @@ -0,0 +1,7 @@ +To set up this example: + +1. Terminal: sudo gem install cocoapods +2. Terminal: pod install +3. Open the generated Example.xcworkspace + +Then command-U to run unit tests. diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-CocoaPods/Example.xcodeproj/project.pbxproj b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-CocoaPods/Example.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..35b4b1d202 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-CocoaPods/Example.xcodeproj/project.pbxproj @@ -0,0 +1,384 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 082CEB6C181382F30013FC27 /* libExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 082CEB53181382F30013FC27 /* libExample.a */; }; + 082CEB74181382F30013FC27 /* ExampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 082CEB73181382F30013FC27 /* ExampleTests.m */; }; + 082CEB7F181385590013FC27 /* Example.h in Headers */ = {isa = PBXBuildFile; fileRef = 082CEB7D181385590013FC27 /* Example.h */; }; + 082CEB80181385590013FC27 /* Example.m in Sources */ = {isa = PBXBuildFile; fileRef = 082CEB7E181385590013FC27 /* Example.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 082CEB6A181382F30013FC27 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 082CEB4B181382F30013FC27 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 082CEB52181382F30013FC27; + remoteInfo = Example; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 082CEB53181382F30013FC27 /* libExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libExample.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 082CEB66181382F30013FC27 /* ExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 082CEB6F181382F30013FC27 /* ExampleTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "ExampleTests-Info.plist"; sourceTree = ""; }; + 082CEB73181382F30013FC27 /* ExampleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ExampleTests.m; sourceTree = ""; }; + 082CEB7D181385590013FC27 /* Example.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Example.h; sourceTree = ""; }; + 082CEB7E181385590013FC27 /* Example.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Example.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 082CEB50181382F30013FC27 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 082CEB63181382F30013FC27 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 082CEB6C181382F30013FC27 /* libExample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 082CEB4A181382F30013FC27 = { + isa = PBXGroup; + children = ( + 082CEB5C181382F30013FC27 /* Example */, + 082CEB6D181382F30013FC27 /* ExampleTests */, + 082CEB54181382F30013FC27 /* Products */, + ); + sourceTree = ""; + }; + 082CEB54181382F30013FC27 /* Products */ = { + isa = PBXGroup; + children = ( + 082CEB53181382F30013FC27 /* libExample.a */, + 082CEB66181382F30013FC27 /* ExampleTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 082CEB5C181382F30013FC27 /* Example */ = { + isa = PBXGroup; + children = ( + 082CEB7D181385590013FC27 /* Example.h */, + 082CEB7E181385590013FC27 /* Example.m */, + ); + path = Example; + sourceTree = ""; + }; + 082CEB6D181382F30013FC27 /* ExampleTests */ = { + isa = PBXGroup; + children = ( + 082CEB73181382F30013FC27 /* ExampleTests.m */, + 082CEB6E181382F30013FC27 /* Supporting Files */, + ); + path = ExampleTests; + sourceTree = ""; + }; + 082CEB6E181382F30013FC27 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 082CEB6F181382F30013FC27 /* ExampleTests-Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 082CEB51181382F30013FC27 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 082CEB7F181385590013FC27 /* Example.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 082CEB52181382F30013FC27 /* Example */ = { + isa = PBXNativeTarget; + buildConfigurationList = 082CEB77181382F30013FC27 /* Build configuration list for PBXNativeTarget "Example" */; + buildPhases = ( + 082CEB4F181382F30013FC27 /* Sources */, + 082CEB50181382F30013FC27 /* Frameworks */, + 082CEB51181382F30013FC27 /* Headers */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Example; + productName = Example; + productReference = 082CEB53181382F30013FC27 /* libExample.a */; + productType = "com.apple.product-type.library.static"; + }; + 082CEB65181382F30013FC27 /* ExampleTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 082CEB7A181382F30013FC27 /* Build configuration list for PBXNativeTarget "ExampleTests" */; + buildPhases = ( + 082CEB62181382F30013FC27 /* Sources */, + 082CEB63181382F30013FC27 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 082CEB6B181382F30013FC27 /* PBXTargetDependency */, + ); + name = ExampleTests; + productName = ExampleTests; + productReference = 082CEB66181382F30013FC27 /* ExampleTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 082CEB4B181382F30013FC27 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = "Quality Coding"; + }; + buildConfigurationList = 082CEB4E181382F30013FC27 /* Build configuration list for PBXProject "Example" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 082CEB4A181382F30013FC27; + productRefGroup = 082CEB54181382F30013FC27 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 082CEB52181382F30013FC27 /* Example */, + 082CEB65181382F30013FC27 /* ExampleTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 082CEB4F181382F30013FC27 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 082CEB80181385590013FC27 /* Example.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 082CEB62181382F30013FC27 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 082CEB74181382F30013FC27 /* ExampleTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 082CEB6B181382F30013FC27 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 082CEB52181382F30013FC27 /* Example */; + targetProxy = 082CEB6A181382F30013FC27 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 082CEB75181382F30013FC27 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_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_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + 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; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.10; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + }; + name = Debug; + }; + 082CEB76181382F30013FC27 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_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_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + 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; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.10; + SDKROOT = macosx; + }; + name = Release; + }; + 082CEB78181382F30013FC27 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 082CEB79181382F30013FC27 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + 082CEB7B181382F30013FC27 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(DEVELOPER_FRAMEWORKS_DIR)", + "$(inherited)", + ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = "ExampleTests/ExampleTests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "org.qualitycoding.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = xctest; + }; + name = Debug; + }; + 082CEB7C181382F30013FC27 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(DEVELOPER_FRAMEWORKS_DIR)", + "$(inherited)", + ); + INFOPLIST_FILE = "ExampleTests/ExampleTests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "org.qualitycoding.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = xctest; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 082CEB4E181382F30013FC27 /* Build configuration list for PBXProject "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 082CEB75181382F30013FC27 /* Debug */, + 082CEB76181382F30013FC27 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 082CEB77181382F30013FC27 /* Build configuration list for PBXNativeTarget "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 082CEB78181382F30013FC27 /* Debug */, + 082CEB79181382F30013FC27 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 082CEB7A181382F30013FC27 /* Build configuration list for PBXNativeTarget "ExampleTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 082CEB7B181382F30013FC27 /* Debug */, + 082CEB7C181382F30013FC27 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 082CEB4B181382F30013FC27 /* Project object */; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-CocoaPods/Example/Example.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-CocoaPods/Example/Example.h new file mode 100644 index 0000000000..7382782e97 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-CocoaPods/Example/Example.h @@ -0,0 +1,4 @@ +@import Foundation; + +@interface Example : NSObject +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-CocoaPods/Example/Example.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-CocoaPods/Example/Example.m new file mode 100644 index 0000000000..f85d8711cc --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-CocoaPods/Example/Example.m @@ -0,0 +1,4 @@ +#import "Example.h" + +@implementation Example +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-CocoaPods/ExampleTests/ExampleTests-Info.plist b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-CocoaPods/ExampleTests/ExampleTests-Info.plist new file mode 100644 index 0000000000..169b6f710e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-CocoaPods/ExampleTests/ExampleTests-Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-CocoaPods/ExampleTests/ExampleTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-CocoaPods/ExampleTests/ExampleTests.m new file mode 100644 index 0000000000..47c5a908f2 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-CocoaPods/ExampleTests/ExampleTests.m @@ -0,0 +1,17 @@ +@import OCHamcrest; +@import XCTest; + + +@interface ExampleTests : XCTestCase +@end + +@implementation ExampleTests + +- (void)testUsingAssertThat +{ + assertThat(@"xx", is(@"xx")); + assertThat(@"yy", isNot(@"xx")); + assertThat(@"i like cheese", containsSubstring(@"cheese")); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-CocoaPods/Podfile b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-CocoaPods/Podfile new file mode 100644 index 0000000000..56230b6de0 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-CocoaPods/Podfile @@ -0,0 +1,4 @@ +target 'ExampleTests' do + inherit! :search_paths + pod 'OCHamcrest', '~> 7.1' +end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-CocoaPods/README.md b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-CocoaPods/README.md new file mode 100644 index 0000000000..2c2ee468b1 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-CocoaPods/README.md @@ -0,0 +1,7 @@ +To set up this example: + +1. Terminal: sudo gem install cocoapods +2. Terminal: pod install +3. Open the generated Example.xcworkspace + +Then command-U to run unit tests. Try changing one of the tests to fail. diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-Framework/Example.xcodeproj/project.pbxproj b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-Framework/Example.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..4cd058ca5d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-Framework/Example.xcodeproj/project.pbxproj @@ -0,0 +1,405 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 082CEB6C181382F30013FC27 /* libExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 082CEB53181382F30013FC27 /* libExample.a */; }; + 082CEB74181382F30013FC27 /* ExampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 082CEB73181382F30013FC27 /* ExampleTests.m */; }; + 082CEB7F181385590013FC27 /* Example.h in Headers */ = {isa = PBXBuildFile; fileRef = 082CEB7D181385590013FC27 /* Example.h */; }; + 082CEB80181385590013FC27 /* Example.m in Sources */ = {isa = PBXBuildFile; fileRef = 082CEB7E181385590013FC27 /* Example.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 082CEB6A181382F30013FC27 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 082CEB4B181382F30013FC27 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 082CEB52181382F30013FC27; + remoteInfo = Example; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 082CEB851813989F0013FC27 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 16; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 082CEB53181382F30013FC27 /* libExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libExample.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 082CEB66181382F30013FC27 /* ExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 082CEB6F181382F30013FC27 /* ExampleTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "ExampleTests-Info.plist"; sourceTree = ""; }; + 082CEB73181382F30013FC27 /* ExampleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ExampleTests.m; sourceTree = ""; }; + 082CEB7D181385590013FC27 /* Example.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Example.h; sourceTree = ""; }; + 082CEB7E181385590013FC27 /* Example.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Example.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 082CEB50181382F30013FC27 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 082CEB63181382F30013FC27 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 082CEB6C181382F30013FC27 /* libExample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 082CEB4A181382F30013FC27 = { + isa = PBXGroup; + children = ( + 082CEB5C181382F30013FC27 /* Example */, + 082CEB6D181382F30013FC27 /* ExampleTests */, + 082CEB54181382F30013FC27 /* Products */, + ); + sourceTree = ""; + }; + 082CEB54181382F30013FC27 /* Products */ = { + isa = PBXGroup; + children = ( + 082CEB53181382F30013FC27 /* libExample.a */, + 082CEB66181382F30013FC27 /* ExampleTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 082CEB5C181382F30013FC27 /* Example */ = { + isa = PBXGroup; + children = ( + 082CEB7D181385590013FC27 /* Example.h */, + 082CEB7E181385590013FC27 /* Example.m */, + 082CEB5D181382F30013FC27 /* Supporting Files */, + ); + path = Example; + sourceTree = ""; + }; + 082CEB5D181382F30013FC27 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 082CEB6D181382F30013FC27 /* ExampleTests */ = { + isa = PBXGroup; + children = ( + 082CEB73181382F30013FC27 /* ExampleTests.m */, + 082CEB6E181382F30013FC27 /* Supporting Files */, + ); + path = ExampleTests; + sourceTree = ""; + }; + 082CEB6E181382F30013FC27 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 082CEB6F181382F30013FC27 /* ExampleTests-Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 082CEB51181382F30013FC27 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 082CEB7F181385590013FC27 /* Example.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 082CEB52181382F30013FC27 /* Example */ = { + isa = PBXNativeTarget; + buildConfigurationList = 082CEB77181382F30013FC27 /* Build configuration list for PBXNativeTarget "Example" */; + buildPhases = ( + 082CEB4F181382F30013FC27 /* Sources */, + 082CEB50181382F30013FC27 /* Frameworks */, + 082CEB51181382F30013FC27 /* Headers */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Example; + productName = Example; + productReference = 082CEB53181382F30013FC27 /* libExample.a */; + productType = "com.apple.product-type.library.static"; + }; + 082CEB65181382F30013FC27 /* ExampleTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 082CEB7A181382F30013FC27 /* Build configuration list for PBXNativeTarget "ExampleTests" */; + buildPhases = ( + 082CEB62181382F30013FC27 /* Sources */, + 082CEB63181382F30013FC27 /* Frameworks */, + 082CEB851813989F0013FC27 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + 082CEB6B181382F30013FC27 /* PBXTargetDependency */, + ); + name = ExampleTests; + productName = ExampleTests; + productReference = 082CEB66181382F30013FC27 /* ExampleTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 082CEB4B181382F30013FC27 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = "Quality Coding"; + }; + buildConfigurationList = 082CEB4E181382F30013FC27 /* Build configuration list for PBXProject "Example" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 082CEB4A181382F30013FC27; + productRefGroup = 082CEB54181382F30013FC27 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 082CEB52181382F30013FC27 /* Example */, + 082CEB65181382F30013FC27 /* ExampleTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 082CEB4F181382F30013FC27 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 082CEB80181385590013FC27 /* Example.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 082CEB62181382F30013FC27 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 082CEB74181382F30013FC27 /* ExampleTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 082CEB6B181382F30013FC27 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 082CEB52181382F30013FC27 /* Example */; + targetProxy = 082CEB6A181382F30013FC27 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 082CEB75181382F30013FC27 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_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_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + 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; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.10; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + }; + name = Debug; + }; + 082CEB76181382F30013FC27 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_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_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + 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; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.10; + SDKROOT = macosx; + }; + name = Release; + }; + 082CEB78181382F30013FC27 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "/Users/joreid/Downloads/OCHamcrest-master/Examples/MacExample-Framework", + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 082CEB79181382F30013FC27 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "/Users/joreid/Downloads/OCHamcrest-master/Examples/MacExample-Framework", + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + 082CEB7B181382F30013FC27 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = "ExampleTests/ExampleTests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "org.qualitycoding.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = xctest; + }; + name = Debug; + }; + 082CEB7C181382F30013FC27 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = "ExampleTests/ExampleTests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "org.qualitycoding.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = xctest; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 082CEB4E181382F30013FC27 /* Build configuration list for PBXProject "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 082CEB75181382F30013FC27 /* Debug */, + 082CEB76181382F30013FC27 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 082CEB77181382F30013FC27 /* Build configuration list for PBXNativeTarget "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 082CEB78181382F30013FC27 /* Debug */, + 082CEB79181382F30013FC27 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 082CEB7A181382F30013FC27 /* Build configuration list for PBXNativeTarget "ExampleTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 082CEB7B181382F30013FC27 /* Debug */, + 082CEB7C181382F30013FC27 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 082CEB4B181382F30013FC27 /* Project object */; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-Framework/Example/Example.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-Framework/Example/Example.h new file mode 100644 index 0000000000..7382782e97 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-Framework/Example/Example.h @@ -0,0 +1,4 @@ +@import Foundation; + +@interface Example : NSObject +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-Framework/Example/Example.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-Framework/Example/Example.m new file mode 100644 index 0000000000..f85d8711cc --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-Framework/Example/Example.m @@ -0,0 +1,4 @@ +#import "Example.h" + +@implementation Example +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-Framework/ExampleTests/ExampleTests-Info.plist b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-Framework/ExampleTests/ExampleTests-Info.plist new file mode 100644 index 0000000000..169b6f710e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-Framework/ExampleTests/ExampleTests-Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-Framework/ExampleTests/ExampleTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-Framework/ExampleTests/ExampleTests.m new file mode 100644 index 0000000000..47c5a908f2 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-Framework/ExampleTests/ExampleTests.m @@ -0,0 +1,17 @@ +@import OCHamcrest; +@import XCTest; + + +@interface ExampleTests : XCTestCase +@end + +@implementation ExampleTests + +- (void)testUsingAssertThat +{ + assertThat(@"xx", is(@"xx")); + assertThat(@"yy", isNot(@"xx")); + assertThat(@"i like cheese", containsSubstring(@"cheese")); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-Framework/README.md b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-Framework/README.md new file mode 100644 index 0000000000..3d3d9418d7 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/MacExample-Framework/README.md @@ -0,0 +1,15 @@ +To set up this example, open Example.xcodeproj: + +1. Drag OCHamcrest.framework into the project, specifying: + * "Copy items into destination group's folder" + * Add to targets: ExampleTests + +2. In Build Settings, add -ObjC to "Other Linker Flags". Whether you do this at + the target level or project level doesn't matter, as long as the change is + applied to the ExampleTests target. + +3. Open the Build Phases for the ExampleTests target: + * Drag OCHamcrest.framework into the Copy Files phase + * Destination: Products Directory + +Then command-U to run unit tests. Try changing one of the tests to fail. diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Cocoapods/Example.xcodeproj/project.pbxproj b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Cocoapods/Example.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..d4507ec7eb --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Cocoapods/Example.xcodeproj/project.pbxproj @@ -0,0 +1,431 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 082CEBD11813A77A0013FC27 /* Example.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 082CEBD01813A77A0013FC27 /* Example.h */; }; + 082CEBD31813A77A0013FC27 /* Example.m in Sources */ = {isa = PBXBuildFile; fileRef = 082CEBD21813A77A0013FC27 /* Example.m */; }; + 082CEBE01813A77A0013FC27 /* libExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 082CEBC81813A77A0013FC27 /* libExample.a */; }; + 082CEBE81813A77A0013FC27 /* ExampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 082CEBE71813A77A0013FC27 /* ExampleTests.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 082CEBDE1813A77A0013FC27 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 082CEBC01813A77A0013FC27 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 082CEBC71813A77A0013FC27; + remoteInfo = Example; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 082CEBC61813A77A0013FC27 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + 082CEBD11813A77A0013FC27 /* Example.h in CopyFiles */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 082CEBC81813A77A0013FC27 /* libExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libExample.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 082CEBD01813A77A0013FC27 /* Example.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Example.h; sourceTree = ""; }; + 082CEBD21813A77A0013FC27 /* Example.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Example.m; sourceTree = ""; }; + 082CEBD81813A77A0013FC27 /* ExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 082CEBE31813A77A0013FC27 /* ExampleTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "ExampleTests-Info.plist"; sourceTree = ""; }; + 082CEBE71813A77A0013FC27 /* ExampleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ExampleTests.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 082CEBC51813A77A0013FC27 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 082CEBD51813A77A0013FC27 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 082CEBE01813A77A0013FC27 /* libExample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 082CEBBF1813A77A0013FC27 = { + isa = PBXGroup; + children = ( + 082CEBCD1813A77A0013FC27 /* Example */, + 082CEBE11813A77A0013FC27 /* ExampleTests */, + 082CEBC91813A77A0013FC27 /* Products */, + ); + sourceTree = ""; + }; + 082CEBC91813A77A0013FC27 /* Products */ = { + isa = PBXGroup; + children = ( + 082CEBC81813A77A0013FC27 /* libExample.a */, + 082CEBD81813A77A0013FC27 /* ExampleTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 082CEBCD1813A77A0013FC27 /* Example */ = { + isa = PBXGroup; + children = ( + 082CEBD01813A77A0013FC27 /* Example.h */, + 082CEBD21813A77A0013FC27 /* Example.m */, + ); + path = Example; + sourceTree = ""; + }; + 082CEBE11813A77A0013FC27 /* ExampleTests */ = { + isa = PBXGroup; + children = ( + 082CEBE71813A77A0013FC27 /* ExampleTests.m */, + 082CEBE21813A77A0013FC27 /* Supporting Files */, + ); + path = ExampleTests; + sourceTree = ""; + }; + 082CEBE21813A77A0013FC27 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 082CEBE31813A77A0013FC27 /* ExampleTests-Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 082CEBC71813A77A0013FC27 /* Example */ = { + isa = PBXNativeTarget; + buildConfigurationList = 082CEBEB1813A77A0013FC27 /* Build configuration list for PBXNativeTarget "Example" */; + buildPhases = ( + 082CEBC41813A77A0013FC27 /* Sources */, + 082CEBC51813A77A0013FC27 /* Frameworks */, + 082CEBC61813A77A0013FC27 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Example; + productName = Example; + productReference = 082CEBC81813A77A0013FC27 /* libExample.a */; + productType = "com.apple.product-type.library.static"; + }; + 082CEBD71813A77A0013FC27 /* ExampleTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 082CEBEE1813A77A0013FC27 /* Build configuration list for PBXNativeTarget "ExampleTests" */; + buildPhases = ( + 921019B4BF438D65C06B73F6 /* 📦 Check Pods Manifest.lock */, + 082CEBD41813A77A0013FC27 /* Sources */, + 082CEBD51813A77A0013FC27 /* Frameworks */, + D985B0B78F768EC1B36E12A7 /* 📦 Embed Pods Frameworks */, + BCDACCB0410B417C25C9AF31 /* 📦 Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + 082CEBDF1813A77A0013FC27 /* PBXTargetDependency */, + ); + name = ExampleTests; + productName = ExampleTests; + productReference = 082CEBD81813A77A0013FC27 /* ExampleTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 082CEBC01813A77A0013FC27 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = "Quality Coding"; + }; + buildConfigurationList = 082CEBC31813A77A0013FC27 /* Build configuration list for PBXProject "Example" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 082CEBBF1813A77A0013FC27; + productRefGroup = 082CEBC91813A77A0013FC27 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 082CEBC71813A77A0013FC27 /* Example */, + 082CEBD71813A77A0013FC27 /* ExampleTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXShellScriptBuildPhase section */ + 921019B4BF438D65C06B73F6 /* 📦 Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "📦 Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; + BCDACCB0410B417C25C9AF31 /* 📦 Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "📦 Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-ExampleTests/Pods-ExampleTests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + D985B0B78F768EC1B36E12A7 /* 📦 Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "📦 Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-ExampleTests/Pods-ExampleTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 082CEBC41813A77A0013FC27 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 082CEBD31813A77A0013FC27 /* Example.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 082CEBD41813A77A0013FC27 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 082CEBE81813A77A0013FC27 /* ExampleTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 082CEBDF1813A77A0013FC27 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 082CEBC71813A77A0013FC27 /* Example */; + targetProxy = 082CEBDE1813A77A0013FC27 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 082CEBE91813A77A0013FC27 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_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_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + 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; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 082CEBEA1813A77A0013FC27 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_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_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + 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; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 082CEBEC1813A77A0013FC27 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + DSTROOT = /tmp/Example.dst; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 082CEBED1813A77A0013FC27 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + DSTROOT = /tmp/Example.dst; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Release; + }; + 082CEBEF1813A77A0013FC27 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = NO; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = "ExampleTests/ExampleTests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "org.qualitycoding.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = xctest; + }; + name = Debug; + }; + 082CEBF01813A77A0013FC27 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = NO; + INFOPLIST_FILE = "ExampleTests/ExampleTests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "org.qualitycoding.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = xctest; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 082CEBC31813A77A0013FC27 /* Build configuration list for PBXProject "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 082CEBE91813A77A0013FC27 /* Debug */, + 082CEBEA1813A77A0013FC27 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 082CEBEB1813A77A0013FC27 /* Build configuration list for PBXNativeTarget "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 082CEBEC1813A77A0013FC27 /* Debug */, + 082CEBED1813A77A0013FC27 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 082CEBEE1813A77A0013FC27 /* Build configuration list for PBXNativeTarget "ExampleTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 082CEBEF1813A77A0013FC27 /* Debug */, + 082CEBF01813A77A0013FC27 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 082CEBC01813A77A0013FC27 /* Project object */; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Cocoapods/Example/Example.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Cocoapods/Example/Example.h new file mode 100644 index 0000000000..7382782e97 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Cocoapods/Example/Example.h @@ -0,0 +1,4 @@ +@import Foundation; + +@interface Example : NSObject +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Cocoapods/Example/Example.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Cocoapods/Example/Example.m new file mode 100644 index 0000000000..f85d8711cc --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Cocoapods/Example/Example.m @@ -0,0 +1,4 @@ +#import "Example.h" + +@implementation Example +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Cocoapods/ExampleTests/ExampleTests-Info.plist b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Cocoapods/ExampleTests/ExampleTests-Info.plist new file mode 100644 index 0000000000..169b6f710e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Cocoapods/ExampleTests/ExampleTests-Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Cocoapods/ExampleTests/ExampleTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Cocoapods/ExampleTests/ExampleTests.m new file mode 100644 index 0000000000..3c832d64f6 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Cocoapods/ExampleTests/ExampleTests.m @@ -0,0 +1,17 @@ +@import OCHamcrest; // Specify OCHamcrest when using Cocoapods +@import XCTest; + + +@interface ExampleTests : XCTestCase +@end + +@implementation ExampleTests + +- (void)testUsingAssertThat +{ + assertThat(@"xx", is(@"xx")); + assertThat(@"yy", isNot(@"xx")); + assertThat(@"i like cheese", containsSubstring(@"cheese")); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Cocoapods/Podfile b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Cocoapods/Podfile new file mode 100644 index 0000000000..56230b6de0 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Cocoapods/Podfile @@ -0,0 +1,4 @@ +target 'ExampleTests' do + inherit! :search_paths + pod 'OCHamcrest', '~> 7.1' +end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Cocoapods/README.md b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Cocoapods/README.md new file mode 100644 index 0000000000..2c2ee468b1 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Cocoapods/README.md @@ -0,0 +1,7 @@ +To set up this example: + +1. Terminal: sudo gem install cocoapods +2. Terminal: pod install +3. Open the generated Example.xcworkspace + +Then command-U to run unit tests. Try changing one of the tests to fail. diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Framework/Example.xcodeproj/project.pbxproj b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Framework/Example.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..d0be7398a1 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Framework/Example.xcodeproj/project.pbxproj @@ -0,0 +1,397 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 082CEB9B1813A25D0013FC27 /* Example.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 082CEB9A1813A25D0013FC27 /* Example.h */; }; + 082CEB9D1813A25D0013FC27 /* Example.m in Sources */ = {isa = PBXBuildFile; fileRef = 082CEB9C1813A25D0013FC27 /* Example.m */; }; + 082CEBAA1813A25D0013FC27 /* libExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 082CEB921813A25D0013FC27 /* libExample.a */; }; + 082CEBB21813A25D0013FC27 /* ExampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 082CEBB11813A25D0013FC27 /* ExampleTests.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 082CEBA81813A25D0013FC27 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 082CEB8A1813A25D0013FC27 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 082CEB911813A25D0013FC27; + remoteInfo = Example; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 082CEB901813A25D0013FC27 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + 082CEB9B1813A25D0013FC27 /* Example.h in CopyFiles */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 082CEB921813A25D0013FC27 /* libExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libExample.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 082CEB9A1813A25D0013FC27 /* Example.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Example.h; sourceTree = ""; }; + 082CEB9C1813A25D0013FC27 /* Example.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Example.m; sourceTree = ""; }; + 082CEBA21813A25D0013FC27 /* ExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 082CEBAD1813A25D0013FC27 /* ExampleTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "ExampleTests-Info.plist"; sourceTree = ""; }; + 082CEBB11813A25D0013FC27 /* ExampleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ExampleTests.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 082CEB8F1813A25D0013FC27 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 082CEB9F1813A25D0013FC27 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 082CEBAA1813A25D0013FC27 /* libExample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 082CEB891813A25D0013FC27 = { + isa = PBXGroup; + children = ( + 082CEB971813A25D0013FC27 /* Example */, + 082CEBAB1813A25D0013FC27 /* ExampleTests */, + 082CEB931813A25D0013FC27 /* Products */, + ); + sourceTree = ""; + }; + 082CEB931813A25D0013FC27 /* Products */ = { + isa = PBXGroup; + children = ( + 082CEB921813A25D0013FC27 /* libExample.a */, + 082CEBA21813A25D0013FC27 /* ExampleTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 082CEB971813A25D0013FC27 /* Example */ = { + isa = PBXGroup; + children = ( + 082CEB9A1813A25D0013FC27 /* Example.h */, + 082CEB9C1813A25D0013FC27 /* Example.m */, + ); + path = Example; + sourceTree = ""; + }; + 082CEBAB1813A25D0013FC27 /* ExampleTests */ = { + isa = PBXGroup; + children = ( + 082CEBB11813A25D0013FC27 /* ExampleTests.m */, + 082CEBAC1813A25D0013FC27 /* Supporting Files */, + ); + path = ExampleTests; + sourceTree = ""; + }; + 082CEBAC1813A25D0013FC27 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 082CEBAD1813A25D0013FC27 /* ExampleTests-Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 082CEB911813A25D0013FC27 /* Example */ = { + isa = PBXNativeTarget; + buildConfigurationList = 082CEBB51813A25D0013FC27 /* Build configuration list for PBXNativeTarget "Example" */; + buildPhases = ( + 082CEB8E1813A25D0013FC27 /* Sources */, + 082CEB8F1813A25D0013FC27 /* Frameworks */, + 082CEB901813A25D0013FC27 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Example; + productName = Example; + productReference = 082CEB921813A25D0013FC27 /* libExample.a */; + productType = "com.apple.product-type.library.static"; + }; + 082CEBA11813A25D0013FC27 /* ExampleTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 082CEBB81813A25D0013FC27 /* Build configuration list for PBXNativeTarget "ExampleTests" */; + buildPhases = ( + 082CEB9E1813A25D0013FC27 /* Sources */, + 082CEB9F1813A25D0013FC27 /* Frameworks */, + 082CEBA01813A25D0013FC27 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 082CEBA91813A25D0013FC27 /* PBXTargetDependency */, + ); + name = ExampleTests; + productName = ExampleTests; + productReference = 082CEBA21813A25D0013FC27 /* ExampleTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 082CEB8A1813A25D0013FC27 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = "Quality Coding"; + }; + buildConfigurationList = 082CEB8D1813A25D0013FC27 /* Build configuration list for PBXProject "Example" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 082CEB891813A25D0013FC27; + productRefGroup = 082CEB931813A25D0013FC27 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 082CEB911813A25D0013FC27 /* Example */, + 082CEBA11813A25D0013FC27 /* ExampleTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 082CEBA01813A25D0013FC27 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 082CEB8E1813A25D0013FC27 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 082CEB9D1813A25D0013FC27 /* Example.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 082CEB9E1813A25D0013FC27 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 082CEBB21813A25D0013FC27 /* ExampleTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 082CEBA91813A25D0013FC27 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 082CEB911813A25D0013FC27 /* Example */; + targetProxy = 082CEBA81813A25D0013FC27 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 082CEBB31813A25D0013FC27 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_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_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + 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; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 082CEBB41813A25D0013FC27 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_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_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = YES; + 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; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 082CEBB61813A25D0013FC27 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + DSTROOT = /tmp/Example.dst; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 082CEBB71813A25D0013FC27 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + DSTROOT = /tmp/Example.dst; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Release; + }; + 082CEBB91813A25D0013FC27 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = "ExampleTests/ExampleTests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "org.qualitycoding.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = xctest; + }; + name = Debug; + }; + 082CEBBA1813A25D0013FC27 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + INFOPLIST_FILE = "ExampleTests/ExampleTests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "org.qualitycoding.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = xctest; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 082CEB8D1813A25D0013FC27 /* Build configuration list for PBXProject "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 082CEBB31813A25D0013FC27 /* Debug */, + 082CEBB41813A25D0013FC27 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 082CEBB51813A25D0013FC27 /* Build configuration list for PBXNativeTarget "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 082CEBB61813A25D0013FC27 /* Debug */, + 082CEBB71813A25D0013FC27 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 082CEBB81813A25D0013FC27 /* Build configuration list for PBXNativeTarget "ExampleTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 082CEBB91813A25D0013FC27 /* Debug */, + 082CEBBA1813A25D0013FC27 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 082CEB8A1813A25D0013FC27 /* Project object */; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Framework/Example/Example.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Framework/Example/Example.h new file mode 100644 index 0000000000..7382782e97 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Framework/Example/Example.h @@ -0,0 +1,4 @@ +@import Foundation; + +@interface Example : NSObject +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Framework/Example/Example.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Framework/Example/Example.m new file mode 100644 index 0000000000..f85d8711cc --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Framework/Example/Example.m @@ -0,0 +1,4 @@ +#import "Example.h" + +@implementation Example +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Framework/ExampleTests/ExampleTests-Info.plist b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Framework/ExampleTests/ExampleTests-Info.plist new file mode 100644 index 0000000000..169b6f710e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Framework/ExampleTests/ExampleTests-Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Framework/ExampleTests/ExampleTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Framework/ExampleTests/ExampleTests.m new file mode 100644 index 0000000000..9d2aabc4d3 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Framework/ExampleTests/ExampleTests.m @@ -0,0 +1,17 @@ +@import OCHamcrestIOS; // Specify OCHamcrestIOS for prebuilt framework +@import XCTest; + + +@interface ExampleTests : XCTestCase +@end + +@implementation ExampleTests + +- (void)testUsingAssertThat +{ + assertThat(@"xx", is(@"xx")); + assertThat(@"yy", isNot(@"xx")); + assertThat(@"i like cheese", containsSubstring(@"cheese")); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Framework/README.md b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Framework/README.md new file mode 100644 index 0000000000..b52173482e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Examples/iOSExample-Framework/README.md @@ -0,0 +1,12 @@ +To set up this example, open Example.xcodeproj: + +1. Drag OCHamcrestIOS.framework into the project, specifying: + * "Copy items into destination group's folder" + * Add to targets: ExampleTests +2. In Build Settings, add -ObjC to "Other Linker Flags". Whether you do this at + the target level or project level doesn't matter, as long as the change is + applied to the ExampleTests target. + + +Then command-U to run unit tests. Try changing one of the tests to fail. + diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/LICENSE.txt b/submodules/AppCenter-sdk/Vendor/OCHamcrest/LICENSE.txt new file mode 100644 index 0000000000..77a3c821f9 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/LICENSE.txt @@ -0,0 +1,13 @@ +OCHamcrest by Jon Reid, https://qualitycoding.org/ +Copyright 2020 hamcrest.org +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +Neither the name of Hamcrest nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +(BSD License) diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/OCHamcrest.podspec b/submodules/AppCenter-sdk/Vendor/OCHamcrest/OCHamcrest.podspec new file mode 100644 index 0000000000..45d978bdc9 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/OCHamcrest.podspec @@ -0,0 +1,39 @@ +Pod::Spec.new do |s| + s.name = 'OCHamcrest' + s.version = '7.1.2' + s.summary = 'Hamcrest for Objective-C: Powerful, combinable, extensible matchers for verification.' + s.description = <<-DESC + OCHamcrest is: + + * a library of "matcher" objects for declaring rules to check whether a + given object matches those rules. + * a framework for writing your own matchers. + + Matchers are useful for a variety of purposes, such as UI validation. But + they're most commonly used for writing unit tests that are expressive and + flexible. + + OCHamcrest is compatible with: + + * XCTest + * OCUnit (SenTestingKit) + * Kiwi + * Cedar + * GHUnit + * Google Toolbox for Mac (GTM) + * OCMock + * OCMockito + DESC + s.homepage = 'https://github.com/hamcrest/OCHamcrest' + s.license = { :type => 'BSD' } + s.author = { 'Jon Reid' => 'jon@qualitycoding.org' } + s.social_media_url = 'https://twitter.com/qcoding' + + s.ios.deployment_target = '8.0' + s.osx.deployment_target = '10.10' + s.tvos.deployment_target = '9.0' + s.source = { :git => 'https://github.com/hamcrest/OCHamcrest.git', :tag => 'v7.1.2' } + s.source_files = 'Source/OCHamcrest.h', 'Source/Core/**/*.{h,m}', 'Source/Library/**/*.{h,m}' + s.private_header_files = 'Source/Core/Helpers/HCRunloopRunner.h', 'Source/Core/Helpers/NSInvocation+OCHamcrest.h', 'Source/Core/Helpers/ReturnValueGetters/*.h', 'Source/Core/Helpers/TestFailureReporters/HCGenericTestFailureReporter.h', 'Source/Core/Helpers/TestFailureReporters/HCSenTestFailureReporter.h', 'Source/Core/Helpers/TestFailureReporters/HCXCTestFailureReporter.h' + s.requires_arc = true +end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/README.md b/submodules/AppCenter-sdk/Vendor/OCHamcrest/README.md new file mode 100644 index 0000000000..45a0e1c31b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/README.md @@ -0,0 +1,244 @@ +![ochamcrest](http://hamcrest.org/images/logo.jpg) + +What is OCHamcrest? +------------------- + +[![Build Status](https://travis-ci.org/hamcrest/OCHamcrest.svg?branch=master)](https://travis-ci.org/hamcrest/OCHamcrest) +[![Coverage Status](https://coveralls.io/repos/hamcrest/OCHamcrest/badge.svg?branch=master)](https://coveralls.io/r/hamcrest/OCHamcrest?branch=master) +[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) +[![Cocoapods Version](https://cocoapod-badges.herokuapp.com/v/OCHamcrest/badge.png)](https://cocoapods.org/pods/OCHamcrest) +[![Twitter Follow](https://img.shields.io/twitter/follow/qcoding.svg?style=social)](https://twitter.com/qcoding) + +OCHamcrest is an Objective-C module providing: + +* a library of "matcher" objects for declaring rules to check whether a given object matches those + rules. +* a framework for writing your own matchers. + +Matchers are useful for a variety of purposes, such as UI validation. But they're most commonly used +for writing unit tests that are expressive and flexible. + + +My first OCHamcrest test +------------------------ + +We'll start by writing a very simple Xcode unit test, but instead of using XCTest's +`XCTAssertEqualObjects` function, we'll use OCHamcrest's `assertThat` construct and a predefined +matcher: + +```obj-c +@import OCHamcrest; +@import XCTest; + +@interface BiscuitTest : XCTestCase +@end + +@implementation BiscuitTest + +- (void)testEquals +{ + Biscuit* theBiscuit = [[Biscuit alloc] initWithName:@"Ginger"]; + Biscuit* myBiscuit = [[Biscuit alloc] initWithName:@"Ginger"]; + assertThat(theBiscuit, equalTo(myBiscuit)); +} + +@end +``` + +The `assertThat` function is a stylized sentence for making a test assertion. In this example, the +subject of the assertion is the object `theBiscuit`, which is the first method parameter. The second +method parameter is a matcher for `Biscuit` objects, here a matcher that checks one object is equal +to another using the `-isEqual:` method. The test passes since the `Biscuit` class defines an +`-isEqual:` method. + +OCHamcrest's functions are actually declared with an "HC_" package prefix (such as `HC_assertThat` +and `HC_equalTo`) to avoid name clashes. To make test writing faster and test code more legible, +optional short syntax is provided by default. For example, instead of writing `HC_assertThat`, +simply write `assertThat`. + + +Predefined matchers +------------------- + +OCHamcrest comes with a library of useful matchers: + +* Object + + * `conformsTo` - match object that conforms to protocol + * `equalTo` - match equal object + * `hasDescription` - match object's `-description` + * `hasProperty` - match return value of method with given name + * `instanceOf` - match object type + * `isA` - match object type precisely, no subclasses + * `nilValue`, `notNilValue` - match `nil`, or not `nil` + * `sameInstance` - match same object + * `throwsException` - match block that throws an exception + * HCArgumentCaptor - match anything, capturing all values + +* Number + + * `closeTo` - match number close to a given value + * `greaterThan`, `greaterThanOrEqualTo`, `lessThan`, + `lessThanOrEqualTo` - match numeric ordering + * `isFalse` - match zero + * `isTrue` - match non-zero + +* Text + + * `containsSubstring` - match part of a string + * `endsWith` - match the end of a string + * `equalToIgnoringCase` - match the complete string but ignore case + * `equalToIgnoringWhitespace` - match the complete string but ignore extra + whitespace + * `startsWith` - match the beginning of a string + * `stringContainsInOrder`, `stringContainsInOrderIn` - match parts of a string, in relative order + +* Logical + + * `allOf`, `allOfIn` - "and" together all matchers + * `anyOf`, `anyOfIn` - "or" together all matchers + * `anything` - match anything (useful in composite matchers when you don't + care about a particular value) + * `isNot` - negate the matcher + +* Collection + + * `contains`, `containsIn` - exactly match the entire collection + * `containsInAnyOrder`, `containsInAnyOrderIn` - match the entire collection, but in any order + * `containsInRelativeOrder`, `containsInRelativeOrderIn` - match collection containing items in relative order + * `everyItem` - match if every item in a collection satisfies a given matcher + * `hasCount` - match number of elements against another matcher + * `hasCountOf` - match collection with given number of elements + * `hasEntries` - match dictionary with key-value pairs in a dictionary + * `hasEntriesIn` - match dictionary with key-value pairs in a list + * `hasEntry` - match dictionary containing a key-value pair + * `hasItem` - match if given item appears in the collection + * `hasItems`, `hasItemsIn` - match if all given items appear in the collection, in any order + * `hasKey` - match dictionary with a key + * `hasValue` - match dictionary with a value + * `isEmpty` - match empty collection + * `isIn` - match when object is in given collection + * `onlyContains`, `onlyContainsIn` - match if collection's items appear in given list + +* Decorator + + * `describedAs` - give the matcher a custom failure description + * `is` - decorator to improve readability - see "Syntactic sugar" below + +The arguments for many of these matchers accept not just a matching value, but +another matcher, so matchers can be composed for greater flexibility. For +example, `only_contains(endsWith(@"."))` will match any collection where every +item is a string ending with period. + + +Syntactic sugar +--------------- + +OCHamcrest strives to make your tests as readable as possible. For example, the `is` matcher is a +wrapper that doesn't add any extra behavior to the underlying matcher. The following assertions are +all equivalent: + +```obj-c +assertThat(theBiscuit, equalTo(myBiscuit)); +assertThat(theBiscuit, is(equalTo(myBiscuit))); +assertThat(theBiscuit, is(myBiscuit)); +``` + +The last form is allowed since `is` wraps non-matcher arguments with `equalTo`. Other matchers that +take matchers as arguments provide similar shortcuts, wrapping non-matcher arguments in `equalTo`. + + +How can I assert on an asynchronous call? +----------------------------------------- + +`assertWithTimeout` will keep evaluating an expression until the matcher is satisfied or a timeout +is reached. For example, + +```obj-c +assertWithTimeout(5, thatEventually(self.someString), is(@"expected")); +``` + +This repeatedly checks for this string to evaluate to "expected" before timing out after 5 seconds. +`thatEventually` is a convenience macro to create a block. + + +Writing custom matchers +----------------------- + +OCHamcrest comes bundled with lots of useful matchers, but you'll probably find that you need to +create your own from time to time to fit your testing needs. See the +["Writing Custom Matchers" guide for more information](https://github.com/hamcrest/OCHamcrest/wiki/Writing-Custom-Matchers). + + +What about Swift? +----------------- + +Try the [native Swift implementation of Hamcrest](https://github.com/nschum/SwiftHamcrest). + + +How do I add OCHamcrest to my project? +-------------------------------------- + +The Examples folder shows projects using OCHamcrest either through CocoaPods or through the prebuilt +frameworks, for iOS and macOS development. + +### CocoaPods + +If you want to add OCHamcrest using Cocoapods then add the following dependency to your Podfile. +Most people will want OCHamcrest in their test targets, and not include any pods from their main +targets: + +```ruby +target 'MyTests' do + inherit! :search_paths + use_frameworks! + pod 'OCHamcrest', '~> 7.0' +end +``` + +Use the following import: + + @import OCHamcrest; + +### Carthage + +Add the following to your Cartfile: + + github "hamcrest/OCHamcrest" ~> 7.0 + +Then drag the the built framework from the appropriate Carthage/Build directory into your project, +but with "Copy items into destination group's folder" disabled. + +### Prebuilt Frameworks + +Prebuilt binaries are available on [GitHub](https://github.com/hamcrest/OCHamcrest/releases/). The +binaries are packaged as frameworks: + +* __OCHamcrestIOS.framework__ for iOS development +* __OCHamcrest.framework__ for macOS development + +Drag the appropriate framework into your project, specifying "Copy items into destination group's +folder". Then specify `-ObjC` in your "Other Linker Flags". + +#### iOS Development: + +Use the following import: + + @import OCHamcrestIOS; + +#### macOS Development: + +Add a "Copy Files" build phase to copy OCHamcrest.framework to your Products Directory. + +Use the following import: + + @import OCHamcrest; + +### Build Your Own + +If you want to build OCHamcrest yourself, clone the repo, then + +```sh +$ cd Source +$ ./MakeDistribution.sh +``` diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCAssertThat.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCAssertThat.h new file mode 100644 index 0000000000..93b889badf --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCAssertThat.h @@ -0,0 +1,96 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +@protocol HCMatcher; + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @header + * Assertion macros for using matchers in testing frameworks. + * Unmet assertions are reported to the HCTestFailureReporterChain. + */ + + +FOUNDATION_EXPORT void HC_assertThatWithLocation(id testCase, _Nullable id actual, id matcher, + const char *fileName, int lineNumber); + +#define HC_assertThat(actual, matcher) \ + HC_assertThatWithLocation(self, actual, matcher, __FILE__, __LINE__) + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract assertThat(actual, matcher) - + * Asserts that actual value satisfies matcher. + * @param actual The object to evaluate as the actual value. + * @param matcher The matcher to satisfy as the expected condition. + * @discussion assertThat passes the actual value to the matcher for evaluation. If the matcher is + * not satisfied, it is reported to the HCTestFailureReporterChain. + * + * Use assertThat in test case methods. It's designed to integrate with XCTest and other testing + * frameworks where individual tests are executed as methods. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_assertThat instead. + */ +#define assertThat(actual, matcher) HC_assertThat(actual, matcher) +#endif + + +typedef _Nonnull id (^HCFutureValue)(void); + +FOUNDATION_EXPORT void HC_assertWithTimeoutAndLocation(id testCase, NSTimeInterval timeout, + HCFutureValue actualBlock, id matcher, + const char *fileName, int lineNumber); + +#define HC_assertWithTimeout(timeout, actualBlock, matcher) \ + HC_assertWithTimeoutAndLocation(self, timeout, actualBlock, matcher, __FILE__, __LINE__) + +#define HC_thatEventually(actual) ^{ return actual; } + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract assertWithTimeout(timeout, actualBlock, matcher) - + * Asserts that a value provided by a block will satisfy matcher within the specified time. + * @param timeout Maximum time to wait for passing behavior, specified in seconds. + * @param actualBlock A block providing the object to repeatedly evaluate as the actual value. + * @param matcher The matcher to satisfy as the expected condition. + * @discussion assertWithTimeout polls a value provided by a block to asynchronously + * satisfy the matcher. The block is evaluated repeatedly for an actual value, which is passed to + * the matcher for evaluation. If the matcher is not satisfied within the timeout, it is reported to + * the HCTestFailureReporterChain. + * + * An easy way of providing the actualBlock is to use the macro thatEventually. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_assertWithTimeout instead. +*/ +#define assertWithTimeout(timeout, actualBlock, matcher) HC_assertWithTimeout(timeout, actualBlock, matcher) + + +/*! + * @abstract thatEventually(actual) - + * Evaluates actual value at future time. + * @param actual The object to evaluate as the actual value. + * @discussion Wraps actual in a block so that it can be repeatedly evaluated by + * assertWithTimeout. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_thatEventually instead. + */ +#define thatEventually(actual) HC_thatEventually(actual) +#endif + + +/*! + * @abstract "Expected , but " + * @discussion Helper function to let you describe mismatches the way assertThat does. + */ +FOUNDATION_EXPORT NSString *HCDescribeMismatch(id matcher, id actual); + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCAssertThat.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCAssertThat.m new file mode 100644 index 0000000000..5a479ab0d0 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCAssertThat.m @@ -0,0 +1,58 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCAssertThat.h" + +#import "HCRunloopRunner.h" +#import "HCStringDescription.h" +#import "HCMatcher.h" +#import "HCTestFailure.h" +#import "HCTestFailureReporter.h" +#import "HCTestFailureReporterChain.h" + +static void reportMismatch(id testCase, id actual, id matcher, + char const *fileName, int lineNumber) +{ + HCTestFailure *failure = [[HCTestFailure alloc] initWithTestCase:testCase + fileName:[NSString stringWithUTF8String:fileName] + lineNumber:(NSUInteger)lineNumber + reason:HCDescribeMismatch(matcher, actual)]; + HCTestFailureReporter *chain = [HCTestFailureReporterChain reporterChain]; + [chain handleFailure:failure]; +} + +void HC_assertThatWithLocation(id testCase, _Nullable id actual, id matcher, + const char *fileName, int lineNumber) +{ + if (![matcher matches:actual]) + reportMismatch(testCase, actual, matcher, fileName, lineNumber); +} + +void HC_assertWithTimeoutAndLocation(id testCase, NSTimeInterval timeout, + HCFutureValue actualBlock, id matcher, + const char *fileName, int lineNumber) +{ + __block BOOL match = [matcher matches:actualBlock()]; + + if (!match) + { + HCRunloopRunner *runner = [[HCRunloopRunner alloc] initWithFulfillmentBlock:^{ + match = [matcher matches:actualBlock()]; + return match; + }]; + [runner runUntilFulfilledOrTimeout:timeout]; + } + + if (!match) + reportMismatch(testCase, actualBlock(), matcher, fileName, lineNumber); +} + +NSString *HCDescribeMismatch(id matcher, id actual) +{ + HCStringDescription *description = [HCStringDescription stringDescription]; + [[[description appendText:@"Expected "] + appendDescriptionOf:matcher] + appendText:@", but "]; + [matcher describeMismatchOf:actual to:description]; + return description.description; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCBaseDescription.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCBaseDescription.h new file mode 100644 index 0000000000..40ce496597 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCBaseDescription.h @@ -0,0 +1,29 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Base class for all HCDescription implementations. + */ +@interface HCBaseDescription : NSObject +@end + + +/*! + * @abstract Methods that must be provided by subclasses of HCBaseDescription. + */ +@interface HCBaseDescription (SubclassResponsibility) + +/*! + * @abstract Appends the specified string to the description. + */ +- (void)append:(NSString *)str; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCBaseDescription.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCBaseDescription.m new file mode 100644 index 0000000000..02f083952a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCBaseDescription.m @@ -0,0 +1,101 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCBaseDescription.h" + +#import "HCMatcher.h" + + +@implementation HCBaseDescription + +- (id )appendText:(NSString *)text +{ + [self append:text]; + return self; +} + +- (id )appendDescriptionOf:(nullable id)value +{ + if (value == nil) + [self append:@"nil"]; + else if ([value conformsToProtocol:@protocol(HCSelfDescribing)]) + [value describeTo:self]; + else if ([value respondsToSelector:@selector(isKindOfClass:)] && [value isKindOfClass:[NSString class]]) + [self toCSyntaxString:value]; + else + [self appendObjectDescriptionOf:value]; + + return self; +} + +- (id )appendObjectDescriptionOf:(id)value +{ + NSString *description = [value description]; + NSUInteger descriptionLength = description.length; + if (descriptionLength == 0) + [self append:[NSString stringWithFormat:@"<%@: %p>", NSStringFromClass([value class]), (__bridge void *)value]]; + else if ([description characterAtIndex:0] == '<' + && [description characterAtIndex:descriptionLength - 1] == '>') + { + [self append:description]; + } + else + { + [self append:@"<"]; + [self append:description]; + [self append:@">"]; + } + return self; +} + +- (id )appendList:(NSArray *)values + start:(NSString *)start + separator:(NSString *)separator + end:(NSString *)end +{ + BOOL separate = NO; + + [self append:start]; + for (id item in values) + { + if (separate) + [self append:separator]; + [self appendDescriptionOf:item]; + separate = YES; + } + [self append:end]; + return self; +} + +- (void)toCSyntaxString:(NSString *)unformatted +{ + [self append:@"\""]; + NSUInteger length = unformatted.length; + for (NSUInteger index = 0; index < length; ++index) + [self toCSyntax:[unformatted characterAtIndex:index]]; + [self append:@"\""]; +} + +- (void)toCSyntax:(unichar)ch +{ + switch (ch) + { + case '"': + [self append:@"\\\""]; + break; + case '\n': + [self append:@"\\n"]; + break; + case '\r': + [self append:@"\\r"]; + break; + case '\t': + [self append:@"\\t"]; + break; + default: + [self append:[NSString stringWithCharacters:&ch length:1]]; + break; + } +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCBaseMatcher.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCBaseMatcher.h new file mode 100644 index 0000000000..1c65c52a55 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCBaseMatcher.h @@ -0,0 +1,25 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import +#import + +#define HC_ABSTRACT_METHOD [self subclassResponsibility:_cmd] + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Base class for all HCMatcher implementations. + * @discussion Simple matchers can just subclass HCBaseMatcher and implement -matches: + * and -describeTo:. But if the matching algorithm has several "no match" paths, + * consider subclassing HCDiagnosingMatcher instead. + */ +@interface HCBaseMatcher : NSObject + +/*! @abstract Raises exception that command (a pseudo-abstract method) is not implemented. */ +- (void)subclassResponsibility:(SEL)command; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCBaseMatcher.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCBaseMatcher.m new file mode 100644 index 0000000000..37d4e8f7bc --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCBaseMatcher.m @@ -0,0 +1,52 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCBaseMatcher.h" + +#import "HCStringDescription.h" + + +@implementation HCBaseMatcher + +- (NSString *)description +{ + return [HCStringDescription stringFrom:self]; +} + +- (BOOL)matches:(nullable id)item +{ + HC_ABSTRACT_METHOD; + return NO; +} + +- (BOOL)matches:(nullable id)item describingMismatchTo:(id )mismatchDescription +{ + BOOL matchResult = [self matches:item]; + if (!matchResult) + [self describeMismatchOf:item to:mismatchDescription]; + return matchResult; +} + +- (void)describeMismatchOf:(nullable id)item to:(nullable id )mismatchDescription +{ + [[mismatchDescription appendText:@"was "] appendDescriptionOf:item]; +} + +- (void)describeTo:(id )description +{ + HC_ABSTRACT_METHOD; +} + +- (void)subclassResponsibility:(SEL)command +{ + NSString *className = NSStringFromClass([self class]); + [NSException raise:NSGenericException + format:@"-[%@ %@] not implemented", className, NSStringFromSelector(command)]; +} + +- (id)copyWithZone:(NSZone *)zone +{ + return self; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCDescription.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCDescription.h new file mode 100644 index 0000000000..42aea9982f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCDescription.h @@ -0,0 +1,39 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract A description of an HCMatcher. + * @discussion An HCMatcher will describe itself to a description which can later be used for reporting. + */ +@protocol HCDescription + +/*! + * @abstract Appends some plain text to the description. + * @return self, for chaining. + */ +- (id )appendText:(NSString *)text; + +/*! + * @abstract Appends description of specified value to description. + * @discussion If the value implements the HCSelfDescribing protocol, then it will be used. + * @return self, for chaining. + */ +- (id )appendDescriptionOf:(nullable id)value; + +/*! + * @abstract Appends a list of objects to the description. + * @return self, for chaining. + */ +- (id )appendList:(NSArray *)values + start:(NSString *)start + separator:(NSString *)separator + end:(NSString *)end; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCDiagnosingMatcher.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCDiagnosingMatcher.h new file mode 100644 index 0000000000..e9fc9f1241 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCDiagnosingMatcher.h @@ -0,0 +1,19 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Base class for matchers that generate mismatch descriptions during the matching. + * @discussion Some matching algorithms have several "no match" paths. It helps to make the mismatch + * description as precise as possible, but we don't want to have to repeat the matching logic to do + * so. For such matchers, subclass HCDiagnosingMatcher and implement HCMatcher's + * -matches:describingMismatchTo:. +*/ +@interface HCDiagnosingMatcher : HCBaseMatcher +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCDiagnosingMatcher.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCDiagnosingMatcher.m new file mode 100644 index 0000000000..3cd81261f7 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCDiagnosingMatcher.m @@ -0,0 +1,25 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCDiagnosingMatcher.h" + + +@implementation HCDiagnosingMatcher + +- (BOOL)matches:(nullable id)item +{ + return [self matches:item describingMismatchTo:nil]; +} + +- (BOOL)matches:(nullable id)item describingMismatchTo:(id )mismatchDescription +{ + HC_ABSTRACT_METHOD; + return NO; +} + +- (void)describeMismatchOf:(nullable id)item to:(nullable id )mismatchDescription +{ + [self matches:item describingMismatchTo:mismatchDescription]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCMatcher.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCMatcher.h new file mode 100644 index 0000000000..ab4478a39e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCMatcher.h @@ -0,0 +1,47 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract A matcher over acceptable values. + * @discussion A matcher is able to describe itself to give feedback when it fails. + * + * HCMatcher implementations should not directly implement this protocol. Instead, extend the + * HCBaseMatcher class, which will ensure that the HCMatcher API can grow to support new features + * and remain compatible with all HCMatcher implementations. + */ +@protocol HCMatcher + +/*! + * @abstract Evaluates the matcher for argument item. + * @param item The object against which the matcher is evaluated. + * @return YES if item matches, otherwise NO. + */ +- (BOOL)matches:(nullable id)item; + +/*! + * @abstract Evaluates the matcher for argument item. + * @param item The object against which the matcher is evaluated. + * @param mismatchDescription The description to be built or appended to if item does not match. + * @return YES if item matches, otherwise NO. + */ +- (BOOL)matches:(nullable id)item describingMismatchTo:(nullable id )mismatchDescription; + +/*! + * @abstract Generates a description of why the matcher has not accepted the item. + * @param item The item that the HCMatcher has rejected. + * @param mismatchDescription The description to be built or appended to. + * @discussion The description will be part of a larger description of why a matching failed, so it + * should be concise. + * + * This method assumes that matches:item is false, but will not check this. + */ +- (void)describeMismatchOf:(nullable id)item to:(nullable id )mismatchDescription; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCSelfDescribing.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCSelfDescribing.h new file mode 100644 index 0000000000..1ab0c8a402 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCSelfDescribing.h @@ -0,0 +1,26 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import // Convenience header + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract The ability of an object to describe itself. + */ +@protocol HCSelfDescribing + +/*! + * @abstract Generates a description of the object. + * @param description The description to be built or appended to. + * @discussion The description may be part of a description of a larger object of which this is just + * a component, so it should be worded appropriately. + */ +- (void)describeTo:(id )description; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCStringDescription.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCStringDescription.h new file mode 100644 index 0000000000..ddf08e55d0 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCStringDescription.h @@ -0,0 +1,36 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +@protocol HCSelfDescribing; + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract An HCDescription that is stored as a string. + */ +@interface HCStringDescription : HCBaseDescription + + +/*! + * @abstract Returns the description of an HCSelfDescribing object as a string. + * @param selfDescribing The object to be described. + * @return The description of the object. + */ ++ (NSString *)stringFrom:(id )selfDescribing; + +/*! + * @abstract Creates and returns an empty description. + */ ++ (instancetype)stringDescription; + +/*! + * @abstract Initializes a newly allocated HCStringDescription that is initially empty. + */ +- (instancetype)init NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCStringDescription.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCStringDescription.m new file mode 100644 index 0000000000..449601d61c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/HCStringDescription.m @@ -0,0 +1,45 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCStringDescription.h" + +#import "HCSelfDescribing.h" + + +@interface HCStringDescription () +@property (nonatomic, strong) NSMutableString *accumulator; +@end + +@implementation HCStringDescription + ++ (NSString *)stringFrom:(id )selfDescribing +{ + HCStringDescription *description = [HCStringDescription stringDescription]; + [description appendDescriptionOf:selfDescribing]; + return description.description; +} + ++ (instancetype)stringDescription +{ + return [[HCStringDescription alloc] init]; +} + +- (instancetype)init +{ + self = [super init]; + if (self) + _accumulator = [[NSMutableString alloc] init]; + return self; +} + +- (NSString *)description +{ + return self.accumulator; +} + +- (void)append:(NSString *)str +{ + [self.accumulator appendString:str]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCCollect.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCCollect.h new file mode 100644 index 0000000000..959a5ea879 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCCollect.h @@ -0,0 +1,26 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import + +@protocol HCMatcher; + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Returns an array of values from a variable-length comma-separated list terminated + * by nil. + */ +FOUNDATION_EXPORT NSArray * HCCollectItems(id item, va_list args); + +/*! + * @abstract Returns an array of matchers from a mixed array of items and matchers. + * @discussion Each item is wrapped in HCWrapInMatcher to transform non-matcher items into equality + * matchers. + */ +FOUNDATION_EXPORT NSArray> * HCWrapIntoMatchers(NSArray *items); + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCCollect.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCCollect.m new file mode 100644 index 0000000000..6a7dd54ac5 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCCollect.m @@ -0,0 +1,43 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCCollect.h" + +#import "HCWrapInMatcher.h" + +/*! + * @abstract Returns an array of wrapped items from a variable-length comma-separated list + * terminated by nil. + * @discussion Each item is transformed by passing it to the specified wrap function. + */ +static NSArray * HCCollectWrappedItems(id item, va_list args, id (*wrap)(id)) +{ + NSMutableArray *list = [NSMutableArray arrayWithObject:wrap(item)]; + + id nextItem = va_arg(args, id); + while (nextItem) + { + [list addObject:wrap(nextItem)]; + nextItem = va_arg(args, id); + } + + return list; +} + +static id passThrough(id value) +{ + return value; +} + +NSArray * HCCollectItems(id item, va_list args) +{ + return HCCollectWrappedItems(item, args, passThrough); +} + +NSArray> * HCWrapIntoMatchers(NSArray *items) +{ + NSMutableArray> *matchers = [[NSMutableArray alloc] init]; + for (id item in items) + [matchers addObject:HCWrapInMatcher(item)]; + return matchers; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCInvocationMatcher.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCInvocationMatcher.h new file mode 100644 index 0000000000..20ab3ae4e9 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCInvocationMatcher.h @@ -0,0 +1,42 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Supporting class for matching a feature of an object. + * @discussion Tests whether the result of passing the specified invocation to the value satisfies + * the specified matcher. + */ +@interface HCInvocationMatcher : HCBaseMatcher + + +/*! + * @abstract Determines whether a mismatch will be described in short form. + * @discussion Default is long form, which describes the object, the name of the invocation, and the + * sub-matcher's mismatch diagnosis. Short form only has the sub-matcher's mismatch diagnosis. + */ +@property (nonatomic, assign) BOOL shortMismatchDescription; + +/*! + * @abstract Initializes a newly allocated HCInvocationMatcher with an invocation and a matcher. + */ +- (instancetype)initWithInvocation:(NSInvocation *)anInvocation matching:(id )aMatcher NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +/*! + * @abstract Invokes stored invocation on the specified item and returns the result. + */ +- (id)invokeOn:(id)item; + +/*! + * @abstract Returns string representation of the invocation's selector. + */ +- (NSString *)stringFromSelector; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCInvocationMatcher.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCInvocationMatcher.m new file mode 100644 index 0000000000..c87fbc8885 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCInvocationMatcher.m @@ -0,0 +1,81 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCInvocationMatcher.h" + + +@interface HCInvocationMatcher () +@property (nonatomic, strong) NSInvocation *invocation; +@property (nonatomic, strong) id subMatcher; +@end + +@implementation HCInvocationMatcher + +- (instancetype)initWithInvocation:(NSInvocation *)anInvocation matching:(id )aMatcher +{ + self = [super init]; + if (self) + { + _invocation = anInvocation; + _subMatcher = aMatcher; + } + return self; +} + +- (BOOL)matches:(nullable id)item +{ + if ([self invocationNotSupportedForItem:item]) + return NO; + + return [self.subMatcher matches:[self invokeOn:item]]; +} + +- (BOOL)invocationNotSupportedForItem:(id)item +{ + return ![item respondsToSelector:self.invocation.selector]; +} + +- (id)invokeOn:(id)item +{ + __unsafe_unretained id result = nil; + [self.invocation invokeWithTarget:item]; + [self.invocation getReturnValue:&result]; + return result; +} + +- (void)describeMismatchOf:(nullable id)item to:(nullable id )mismatchDescription +{ + if ([self invocationNotSupportedForItem:item]) + [super describeMismatchOf:item to:mismatchDescription]; + else + { + [self describeLongMismatchDescriptionOf:item to:mismatchDescription]; + [self.subMatcher describeMismatchOf:[self invokeOn:item] to:mismatchDescription]; + } +} + +- (void)describeLongMismatchDescriptionOf:(id)item to:(id )mismatchDescription +{ + if (!self.shortMismatchDescription) + { + [[[[mismatchDescription appendDescriptionOf:item] + appendText:@" "] + appendText:[self stringFromSelector]] + appendText:@" "]; + } +} + +- (void)describeTo:(id )description +{ + [[[[description appendText:@"an object with "] + appendText:[self stringFromSelector]] + appendText:@" "] + appendDescriptionOf:self.subMatcher]; +} + +- (NSString *)stringFromSelector +{ + return NSStringFromSelector(self.invocation.selector); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCRequireNonNilObject.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCRequireNonNilObject.h new file mode 100644 index 0000000000..e2c62f13a2 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCRequireNonNilObject.h @@ -0,0 +1,14 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Throws an NSException if obj is nil. +*/ +FOUNDATION_EXPORT void HCRequireNonNilObject(id obj); + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCRequireNonNilObject.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCRequireNonNilObject.m new file mode 100644 index 0000000000..13841a4e1d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCRequireNonNilObject.m @@ -0,0 +1,15 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCRequireNonNilObject.h" + + +void HCRequireNonNilObject(id obj) +{ + if (obj == nil) + { + @throw [NSException exceptionWithName:@"NilObject" + reason:@"Must be non-nil object" + userInfo:nil]; + } +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCRunloopRunner.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCRunloopRunner.h new file mode 100644 index 0000000000..13db45299a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCRunloopRunner.h @@ -0,0 +1,21 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Runs runloop until fulfilled, or timeout is reached. + * @discussion Based on http://bou.io/CTTRunLoopRunUntil.html + */ +@interface HCRunloopRunner : NSObject + +- (instancetype)initWithFulfillmentBlock:(BOOL (^)(void))fulfillmentBlock NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; +- (void)runUntilFulfilledOrTimeout:(CFTimeInterval)timeout; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCRunloopRunner.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCRunloopRunner.m new file mode 100644 index 0000000000..cfda6d931c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCRunloopRunner.m @@ -0,0 +1,39 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCRunloopRunner.h" + + +@implementation HCRunloopRunner +{ + CFRunLoopObserverRef _observer; +} + +- (instancetype)initWithFulfillmentBlock:(BOOL (^)(void))fulfillmentBlock +{ + self = [super init]; + if (self) + { + _observer = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { + if (fulfillmentBlock()) + CFRunLoopStop(CFRunLoopGetCurrent()); + else + CFRunLoopWakeUp(CFRunLoopGetCurrent()); + }); + CFRunLoopAddObserver(CFRunLoopGetCurrent(), _observer, kCFRunLoopDefaultMode); + } + return self; +} + +- (void)dealloc +{ + CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), _observer, kCFRunLoopDefaultMode); + CFRelease(_observer); +} + +- (void)runUntilFulfilledOrTimeout:(CFTimeInterval)timeout +{ + CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout, false); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCWrapInMatcher.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCWrapInMatcher.h new file mode 100644 index 0000000000..5b6839132c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCWrapInMatcher.h @@ -0,0 +1,17 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +@protocol HCMatcher; + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Wraps argument in a matcher, if necessary. + * @return The argument as-is if it is already a matcher, otherwise wrapped in an equalTo matcher. + */ +FOUNDATION_EXPORT _Nullable id HCWrapInMatcher(_Nullable id matcherOrValue); + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCWrapInMatcher.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCWrapInMatcher.m new file mode 100644 index 0000000000..8fd5e33229 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/HCWrapInMatcher.m @@ -0,0 +1,17 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCWrapInMatcher.h" + +#import "HCIsEqual.h" + + +_Nullable id HCWrapInMatcher(_Nullable id matcherOrValue) +{ + if (!matcherOrValue) + return nil; + + if ([matcherOrValue conformsToProtocol:@protocol(HCMatcher)]) + return matcherOrValue; + return HC_equalTo(matcherOrValue); +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/NSInvocation+OCHamcrest.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/NSInvocation+OCHamcrest.h new file mode 100644 index 0000000000..fecb1ed8ee --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/NSInvocation+OCHamcrest.h @@ -0,0 +1,17 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +@interface NSInvocation (OCHamcrest) + ++ (NSInvocation *)och_invocationWithTarget:(id)target selector:(SEL)selector; ++ (NSInvocation *)och_invocationOnObjectOfType:(Class)aClass selector:(SEL)selector; +- (id)och_invoke; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/NSInvocation+OCHamcrest.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/NSInvocation+OCHamcrest.m new file mode 100644 index 0000000000..21195169ea --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/NSInvocation+OCHamcrest.m @@ -0,0 +1,45 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "NSInvocation+OCHamcrest.h" + +#import "HCReturnValueGetter.h" +#import "HCReturnTypeHandlerChain.h" + + +@implementation NSInvocation (OCHamcrest) + ++ (NSInvocation *)och_invocationWithTarget:(id)target selector:(SEL)selector +{ + NSMethodSignature *signature = [target methodSignatureForSelector:selector]; + NSInvocation *invocation= [self och_invocationWithSignature:signature selector:selector]; + invocation.target = target; + return invocation; +} + ++ (NSInvocation *)och_invocationOnObjectOfType:(Class)aClass selector:(SEL)selector +{ + NSMethodSignature *signature = [aClass instanceMethodSignatureForSelector:selector]; + return [self och_invocationWithSignature:signature selector:selector]; +} + ++ (NSInvocation *)och_invocationWithSignature:(NSMethodSignature *)signature selector:(SEL)selector +{ + NSInvocation *invocation = [[self class] invocationWithMethodSignature:signature]; + invocation.selector = selector; + return invocation; +} + +- (id)och_invoke +{ + [self invoke]; + return [self och_returnValue]; +} + +- (id)och_returnValue +{ + char const *returnType = self.methodSignature.methodReturnType; + return [HCReturnValueGetterChain() returnValueOfType:returnType fromInvocation:self]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCBoolReturnGetter.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCBoolReturnGetter.h new file mode 100644 index 0000000000..4e0d4eccb9 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCBoolReturnGetter.h @@ -0,0 +1,16 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCReturnValueGetter.h" + + +NS_ASSUME_NONNULL_BEGIN + +@interface HCBoolReturnGetter : HCReturnValueGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithType:(char const *)handlerType successor:(nullable HCReturnValueGetter *)successor NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCBoolReturnGetter.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCBoolReturnGetter.m new file mode 100644 index 0000000000..e04e88bbed --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCBoolReturnGetter.m @@ -0,0 +1,22 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCBoolReturnGetter.h" + + +@implementation HCBoolReturnGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor +{ + self = [super initWithType:@encode(BOOL) successor:successor]; + return self; +} + +- (id)returnValueFromInvocation:(NSInvocation *)invocation +{ + BOOL value; + [invocation getReturnValue:&value]; + return @(value); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCCharReturnGetter.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCCharReturnGetter.h new file mode 100644 index 0000000000..b0214a61b7 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCCharReturnGetter.h @@ -0,0 +1,16 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCReturnValueGetter.h" + + +NS_ASSUME_NONNULL_BEGIN + +@interface HCCharReturnGetter : HCReturnValueGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithType:(char const *)handlerType successor:(nullable HCReturnValueGetter *)successor NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCCharReturnGetter.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCCharReturnGetter.m new file mode 100644 index 0000000000..a09e53062c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCCharReturnGetter.m @@ -0,0 +1,22 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCCharReturnGetter.h" + + +@implementation HCCharReturnGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor +{ + self = [super initWithType:@encode(char) successor:successor]; + return self; +} + +- (id)returnValueFromInvocation:(NSInvocation *)invocation +{ + char value; + [invocation getReturnValue:&value]; + return @(value); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCDoubleReturnGetter.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCDoubleReturnGetter.h new file mode 100644 index 0000000000..9f78dc830c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCDoubleReturnGetter.h @@ -0,0 +1,16 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCReturnValueGetter.h" + + +NS_ASSUME_NONNULL_BEGIN + +@interface HCDoubleReturnGetter : HCReturnValueGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithType:(char const *)handlerType successor:(nullable HCReturnValueGetter *)successor NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCDoubleReturnGetter.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCDoubleReturnGetter.m new file mode 100644 index 0000000000..ab5e8a8be8 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCDoubleReturnGetter.m @@ -0,0 +1,22 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCDoubleReturnGetter.h" + + +@implementation HCDoubleReturnGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor +{ + self = [super initWithType:@encode(double) successor:successor]; + return self; +} + +- (id)returnValueFromInvocation:(NSInvocation *)invocation +{ + double value; + [invocation getReturnValue:&value]; + return @(value); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCFloatReturnGetter.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCFloatReturnGetter.h new file mode 100644 index 0000000000..544abd652c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCFloatReturnGetter.h @@ -0,0 +1,16 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCReturnValueGetter.h" + + +NS_ASSUME_NONNULL_BEGIN + +@interface HCFloatReturnGetter : HCReturnValueGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithType:(char const *)handlerType successor:(nullable HCReturnValueGetter *)successor NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCFloatReturnGetter.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCFloatReturnGetter.m new file mode 100644 index 0000000000..7edc17eb80 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCFloatReturnGetter.m @@ -0,0 +1,22 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCFloatReturnGetter.h" + + +@implementation HCFloatReturnGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor +{ + self = [super initWithType:@encode(float) successor:successor]; + return self; +} + +- (id)returnValueFromInvocation:(NSInvocation *)invocation +{ + float value; + [invocation getReturnValue:&value]; + return @(value); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCIntReturnGetter.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCIntReturnGetter.h new file mode 100644 index 0000000000..5ed812a0df --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCIntReturnGetter.h @@ -0,0 +1,16 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCReturnValueGetter.h" + + +NS_ASSUME_NONNULL_BEGIN + +@interface HCIntReturnGetter : HCReturnValueGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithType:(char const *)handlerType successor:(nullable HCReturnValueGetter *)successor NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCIntReturnGetter.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCIntReturnGetter.m new file mode 100644 index 0000000000..49c4469293 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCIntReturnGetter.m @@ -0,0 +1,22 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCIntReturnGetter.h" + + +@implementation HCIntReturnGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor +{ + self = [super initWithType:@encode(int) successor:successor]; + return self; +} + +- (id)returnValueFromInvocation:(NSInvocation *)invocation +{ + int value; + [invocation getReturnValue:&value]; + return @(value); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCLongLongReturnGetter.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCLongLongReturnGetter.h new file mode 100644 index 0000000000..25654285b1 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCLongLongReturnGetter.h @@ -0,0 +1,16 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCReturnValueGetter.h" + + +NS_ASSUME_NONNULL_BEGIN + +@interface HCLongLongReturnGetter : HCReturnValueGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithType:(char const *)handlerType successor:(nullable HCReturnValueGetter *)successor NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCLongLongReturnGetter.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCLongLongReturnGetter.m new file mode 100644 index 0000000000..ff3fc0ab9b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCLongLongReturnGetter.m @@ -0,0 +1,22 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCLongLongReturnGetter.h" + + +@implementation HCLongLongReturnGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor +{ + self = [super initWithType:@encode(long long) successor:successor]; + return self; +} + +- (id)returnValueFromInvocation:(NSInvocation *)invocation +{ + long long value; + [invocation getReturnValue:&value]; + return @(value); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCLongReturnGetter.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCLongReturnGetter.h new file mode 100644 index 0000000000..77d6f21c2a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCLongReturnGetter.h @@ -0,0 +1,16 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCReturnValueGetter.h" + + +NS_ASSUME_NONNULL_BEGIN + +@interface HCLongReturnGetter : HCReturnValueGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithType:(char const *)handlerType successor:(nullable HCReturnValueGetter *)successor NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCLongReturnGetter.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCLongReturnGetter.m new file mode 100644 index 0000000000..1e2a82a5fa --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCLongReturnGetter.m @@ -0,0 +1,22 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCLongReturnGetter.h" + + +@implementation HCLongReturnGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor +{ + self = [super initWithType:@encode(long) successor:successor]; + return self; +} + +- (id)returnValueFromInvocation:(NSInvocation *)invocation +{ + long value; + [invocation getReturnValue:&value]; + return @(value); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCObjectReturnGetter.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCObjectReturnGetter.h new file mode 100644 index 0000000000..aa18d125b3 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCObjectReturnGetter.h @@ -0,0 +1,16 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCReturnValueGetter.h" + + +NS_ASSUME_NONNULL_BEGIN + +@interface HCObjectReturnGetter : HCReturnValueGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithType:(char const *)handlerType successor:(nullable HCReturnValueGetter *)successor NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCObjectReturnGetter.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCObjectReturnGetter.m new file mode 100644 index 0000000000..45c27fcdc6 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCObjectReturnGetter.m @@ -0,0 +1,22 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCObjectReturnGetter.h" + + +@implementation HCObjectReturnGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor +{ + self = [super initWithType:@encode(id) successor:successor]; + return self; +} + +- (id)returnValueFromInvocation:(NSInvocation *)invocation +{ + __unsafe_unretained id value; + [invocation getReturnValue:&value]; + return value; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCReturnTypeHandlerChain.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCReturnTypeHandlerChain.h new file mode 100644 index 0000000000..d695912e2d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCReturnTypeHandlerChain.h @@ -0,0 +1,12 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +@class HCReturnValueGetter; + + +/*! + * @abstract Returns chain of return type handlers. + */ +FOUNDATION_EXPORT HCReturnValueGetter *HCReturnValueGetterChain(void); diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCReturnTypeHandlerChain.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCReturnTypeHandlerChain.m new file mode 100644 index 0000000000..0feea3baa5 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCReturnTypeHandlerChain.m @@ -0,0 +1,44 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCReturnTypeHandlerChain.h" + +#import "HCObjectReturnGetter.h" +#import "HCCharReturnGetter.h" +#import "HCBoolReturnGetter.h" +#import "HCIntReturnGetter.h" +#import "HCShortReturnGetter.h" +#import "HCLongReturnGetter.h" +#import "HCLongLongReturnGetter.h" +#import "HCUnsignedCharReturnGetter.h" +#import "HCUnsignedIntReturnGetter.h" +#import "HCUnsignedShortReturnGetter.h" +#import "HCUnsignedLongReturnGetter.h" +#import "HCUnsignedLongLongReturnGetter.h" +#import "HCFloatReturnGetter.h" +#import "HCDoubleReturnGetter.h" + + +HCReturnValueGetter *HCReturnValueGetterChain(void) +{ + static HCReturnValueGetter *chain = nil; + if (!chain) + { + HCReturnValueGetter *doubleHandler = [[HCDoubleReturnGetter alloc] initWithSuccessor:nil]; + HCReturnValueGetter *floatHandler = [[HCFloatReturnGetter alloc] initWithSuccessor:doubleHandler]; + HCReturnValueGetter *uLongLongHandler = [[HCUnsignedLongLongReturnGetter alloc] initWithSuccessor:floatHandler]; + HCReturnValueGetter *uLongHandler = [[HCUnsignedLongReturnGetter alloc] initWithSuccessor:uLongLongHandler]; + HCReturnValueGetter *uShortHandler = [[HCUnsignedShortReturnGetter alloc] initWithSuccessor:uLongHandler]; + HCReturnValueGetter *uIntHandler = [[HCUnsignedIntReturnGetter alloc] initWithSuccessor:uShortHandler]; + HCReturnValueGetter *uCharHandler = [[HCUnsignedCharReturnGetter alloc] initWithSuccessor:uIntHandler]; + HCReturnValueGetter *longLongHandler = [[HCLongLongReturnGetter alloc] initWithSuccessor:uCharHandler]; + HCReturnValueGetter *longHandler = [[HCLongReturnGetter alloc] initWithSuccessor:longLongHandler]; + HCReturnValueGetter *shortHandler = [[HCShortReturnGetter alloc] initWithSuccessor:longHandler]; + HCReturnValueGetter *intHandler = [[HCIntReturnGetter alloc] initWithSuccessor:shortHandler]; + HCReturnValueGetter *boolHandler = [[HCBoolReturnGetter alloc] initWithSuccessor:intHandler]; + HCReturnValueGetter *charHandler = [[HCCharReturnGetter alloc] initWithSuccessor:boolHandler]; + HCReturnValueGetter *objectHandler = [[HCObjectReturnGetter alloc] initWithSuccessor:charHandler]; + chain = objectHandler; + } + return chain; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCReturnValueGetter.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCReturnValueGetter.h new file mode 100644 index 0000000000..5ae4bdca3c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCReturnValueGetter.h @@ -0,0 +1,20 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Chain-of-responsibility for handling NSInvocation return types. + */ +@interface HCReturnValueGetter : NSObject + +- (instancetype)initWithType:(char const *)handlerType successor:(nullable HCReturnValueGetter *)successor NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; +- (id)returnValueOfType:(char const *)type fromInvocation:(NSInvocation *)invocation; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCReturnValueGetter.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCReturnValueGetter.m new file mode 100644 index 0000000000..bf4d1aa391 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCReturnValueGetter.m @@ -0,0 +1,42 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCReturnValueGetter.h" + + +@interface HCReturnValueGetter (SubclassResponsibility) +- (id)returnValueFromInvocation:(NSInvocation *)invocation; +@end + +@interface HCReturnValueGetter () +@property (nonatomic, assign, readonly) char const *handlerType; +@property (nullable, nonatomic, strong, readonly) HCReturnValueGetter *successor; +@end + +@implementation HCReturnValueGetter + +- (instancetype)initWithType:(char const *)handlerType successor:(nullable HCReturnValueGetter *)successor +{ + self = [super init]; + if (self) + { + _handlerType = handlerType; + _successor = successor; + } + return self; +} + +- (BOOL)handlesReturnType:(char const *)returnType +{ + return strcmp(returnType, self.handlerType) == 0; +} + +- (id)returnValueOfType:(char const *)type fromInvocation:(NSInvocation *)invocation +{ + if ([self handlesReturnType:type]) + return [self returnValueFromInvocation:invocation]; + + return [self.successor returnValueOfType:type fromInvocation:invocation]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCShortReturnGetter.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCShortReturnGetter.h new file mode 100644 index 0000000000..ca244fb464 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCShortReturnGetter.h @@ -0,0 +1,16 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCReturnValueGetter.h" + + +NS_ASSUME_NONNULL_BEGIN + +@interface HCShortReturnGetter : HCReturnValueGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithType:(char const *)handlerType successor:(nullable HCReturnValueGetter *)successor NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCShortReturnGetter.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCShortReturnGetter.m new file mode 100644 index 0000000000..8fd6cfa017 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCShortReturnGetter.m @@ -0,0 +1,22 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCShortReturnGetter.h" + + +@implementation HCShortReturnGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor +{ + self = [super initWithType:@encode(short) successor:successor]; + return self; +} + +- (id)returnValueFromInvocation:(NSInvocation *)invocation +{ + short value; + [invocation getReturnValue:&value]; + return @(value); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedCharReturnGetter.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedCharReturnGetter.h new file mode 100644 index 0000000000..35f2138861 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedCharReturnGetter.h @@ -0,0 +1,16 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCReturnValueGetter.h" + + +NS_ASSUME_NONNULL_BEGIN + +@interface HCUnsignedCharReturnGetter : HCReturnValueGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithType:(char const *)handlerType successor:(nullable HCReturnValueGetter *)successor NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedCharReturnGetter.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedCharReturnGetter.m new file mode 100644 index 0000000000..40edd1dcdc --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedCharReturnGetter.m @@ -0,0 +1,22 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCUnsignedCharReturnGetter.h" + + +@implementation HCUnsignedCharReturnGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor +{ + self = [super initWithType:@encode(unsigned char) successor:successor]; + return self; +} + +- (id)returnValueFromInvocation:(NSInvocation *)invocation +{ + unsigned char value; + [invocation getReturnValue:&value]; + return @(value); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedIntReturnGetter.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedIntReturnGetter.h new file mode 100644 index 0000000000..ba8d9df15b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedIntReturnGetter.h @@ -0,0 +1,16 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCReturnValueGetter.h" + + +NS_ASSUME_NONNULL_BEGIN + +@interface HCUnsignedIntReturnGetter : HCReturnValueGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithType:(char const *)handlerType successor:(nullable HCReturnValueGetter *)successor NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedIntReturnGetter.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedIntReturnGetter.m new file mode 100644 index 0000000000..2f379e1e7c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedIntReturnGetter.m @@ -0,0 +1,22 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCUnsignedIntReturnGetter.h" + + +@implementation HCUnsignedIntReturnGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor +{ + self = [super initWithType:@encode(unsigned int) successor:successor]; + return self; +} + +- (id)returnValueFromInvocation:(NSInvocation *)invocation +{ + unsigned int value; + [invocation getReturnValue:&value]; + return @(value); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedLongLongReturnGetter.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedLongLongReturnGetter.h new file mode 100644 index 0000000000..cd2f795229 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedLongLongReturnGetter.h @@ -0,0 +1,16 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCReturnValueGetter.h" + + +NS_ASSUME_NONNULL_BEGIN + +@interface HCUnsignedLongLongReturnGetter : HCReturnValueGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithType:(char const *)handlerType successor:(nullable HCReturnValueGetter *)successor NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedLongLongReturnGetter.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedLongLongReturnGetter.m new file mode 100644 index 0000000000..100a0e647a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedLongLongReturnGetter.m @@ -0,0 +1,22 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCUnsignedLongLongReturnGetter.h" + + +@implementation HCUnsignedLongLongReturnGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor +{ + self = [super initWithType:@encode(unsigned long long) successor:successor]; + return self; +} + +- (id)returnValueFromInvocation:(NSInvocation *)invocation +{ + unsigned long long value; + [invocation getReturnValue:&value]; + return @(value); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedLongReturnGetter.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedLongReturnGetter.h new file mode 100644 index 0000000000..849ed78e26 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedLongReturnGetter.h @@ -0,0 +1,16 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCReturnValueGetter.h" + + +NS_ASSUME_NONNULL_BEGIN + +@interface HCUnsignedLongReturnGetter : HCReturnValueGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithType:(char const *)handlerType successor:(nullable HCReturnValueGetter *)successor NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedLongReturnGetter.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedLongReturnGetter.m new file mode 100644 index 0000000000..81be41bcd0 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedLongReturnGetter.m @@ -0,0 +1,22 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCUnsignedLongReturnGetter.h" + + +@implementation HCUnsignedLongReturnGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor +{ + self = [super initWithType:@encode(unsigned long) successor:successor]; + return self; +} + +- (id)returnValueFromInvocation:(NSInvocation *)invocation +{ + unsigned long value; + [invocation getReturnValue:&value]; + return @(value); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedShortReturnGetter.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedShortReturnGetter.h new file mode 100644 index 0000000000..ad16587656 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedShortReturnGetter.h @@ -0,0 +1,16 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCReturnValueGetter.h" + + +NS_ASSUME_NONNULL_BEGIN + +@interface HCUnsignedShortReturnGetter : HCReturnValueGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithType:(char const *)handlerType successor:(nullable HCReturnValueGetter *)successor NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedShortReturnGetter.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedShortReturnGetter.m new file mode 100644 index 0000000000..0e8e661c35 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/ReturnValueGetters/HCUnsignedShortReturnGetter.m @@ -0,0 +1,22 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCUnsignedShortReturnGetter.h" + + +@implementation HCUnsignedShortReturnGetter + +- (instancetype)initWithSuccessor:(nullable HCReturnValueGetter *)successor +{ + self = [super initWithType:@encode(unsigned short) successor:successor]; + return self; +} + +- (id)returnValueFromInvocation:(NSInvocation *)invocation +{ + unsigned short value; + [invocation getReturnValue:&value]; + return @(value); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCGenericTestFailureReporter.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCGenericTestFailureReporter.h new file mode 100644 index 0000000000..caac217f66 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCGenericTestFailureReporter.h @@ -0,0 +1,8 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCTestFailureReporter.h" + + +@interface HCGenericTestFailureReporter : HCTestFailureReporter +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCGenericTestFailureReporter.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCGenericTestFailureReporter.m new file mode 100644 index 0000000000..6452b1ff5f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCGenericTestFailureReporter.m @@ -0,0 +1,31 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCGenericTestFailureReporter.h" + +#import "HCTestFailure.h" + + +@implementation HCGenericTestFailureReporter + +- (BOOL)willHandleFailure:(HCTestFailure *)failure +{ + return YES; +} + +- (void)executeHandlingOfFailure:(HCTestFailure *)failure +{ + NSException *exception = [self createExceptionForFailure:failure]; + [exception raise]; +} + +- (NSException *)createExceptionForFailure:(HCTestFailure *)failure +{ + NSString *failureReason = [NSString stringWithFormat:@"%@:%lu: matcher error: %@", + failure.fileName, + (unsigned long)failure.lineNumber, + failure.reason]; + return [NSException exceptionWithName:@"HCGenericTestFailure" reason:failureReason userInfo:nil]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCSenTestFailureReporter.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCSenTestFailureReporter.h new file mode 100644 index 0000000000..b2eeea1f9c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCSenTestFailureReporter.h @@ -0,0 +1,8 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCTestFailureReporter.h" + + +@interface HCSenTestFailureReporter : HCTestFailureReporter +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCSenTestFailureReporter.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCSenTestFailureReporter.m new file mode 100644 index 0000000000..189b4c6338 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCSenTestFailureReporter.m @@ -0,0 +1,69 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCSenTestFailureReporter.h" + +#import "HCTestFailure.h" +#import "NSInvocation+OCHamcrest.h" + +@interface NSObject (PretendMethodsExistOnNSObjectToAvoidLinkingSenTestingKit) + ++ (NSException *)failureInFile:(NSString *)filename + atLine:(int)lineNumber + withDescription:(NSString *)formatString, ...; + +- (void)failWithException:(NSException *)exception; + +@end + + +@interface NSInvocation (OCHamcrest_SenTestingKit) +@end + +@implementation NSInvocation (OCHamcrest_SenTestingKit) + ++ (NSInvocation *)och_SenTestFailureInFile:(NSString *)fileName + atLine:(NSUInteger)lineNumber + description:(NSString *)description +{ + // SenTestingKit expects a format string, but NSInvocation does not support varargs. + // Mask % symbols in the string so they aren't treated as placeholders. + NSString *massagedDescription = [description stringByReplacingOccurrencesOfString:@"%" + withString:@"%%"]; + + NSInvocation *invocation = [NSInvocation och_invocationWithTarget:[NSException class] + selector:@selector(failureInFile:atLine:withDescription:)]; + [invocation setArgument:&fileName atIndex:2]; + [invocation setArgument:&lineNumber atIndex:3]; + [invocation setArgument:&massagedDescription atIndex:4]; + return invocation; +} + +@end + + +@implementation HCSenTestFailureReporter + +- (BOOL)willHandleFailure:(HCTestFailure *)failure +{ + return [failure.testCase respondsToSelector:@selector(failWithException:)]; +} + +- (void)executeHandlingOfFailure:(HCTestFailure *)failure +{ + NSException *exception = [self createExceptionForFailure:failure]; + [failure.testCase failWithException:exception]; +} + +- (NSException *)createExceptionForFailure:(HCTestFailure *)failure +{ + NSInvocation *invocation = [NSInvocation och_SenTestFailureInFile:failure.fileName + atLine:failure.lineNumber + description:failure.reason]; + [invocation invoke]; + __unsafe_unretained NSException *result = nil; + [invocation getReturnValue:&result]; + return result; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCTestFailure.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCTestFailure.h new file mode 100644 index 0000000000..0e2b65e4e7 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCTestFailure.h @@ -0,0 +1,42 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + @abstract Test failure location and reason. + */ +@interface HCTestFailure : NSObject + +/*! + * @abstract Test case used to run test method. + * @discussion Can be nil. + * + * For unmet OCHamcrest assertions, if the assertion was assertThat or + * assertWithTimeout, testCase will be the test case instance. + */ +@property (nonatomic, strong, readonly) id testCase; + +/*! @abstract File name to report. */ +@property (nonatomic, copy, readonly) NSString *fileName; + +/*! @abstract Line number to report. */ +@property (nonatomic, assign, readonly) NSUInteger lineNumber; + +/*! @abstract Failure reason to report. */ +@property (nonatomic, strong, readonly) NSString *reason; + +/*! + * @abstract Initializes a newly allocated instance of a test failure. + */ +- (instancetype)initWithTestCase:(id)testCase + fileName:(NSString *)fileName + lineNumber:(NSUInteger)lineNumber + reason:(NSString *)reason; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCTestFailure.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCTestFailure.m new file mode 100644 index 0000000000..008bdef80f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCTestFailure.m @@ -0,0 +1,25 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCTestFailure.h" + + +@implementation HCTestFailure + +- (instancetype)initWithTestCase:(id)testCase + fileName:(NSString *)fileName + lineNumber:(NSUInteger)lineNumber + reason:(NSString *)reason +{ + self = [super init]; + if (self) + { + _testCase = testCase; + _fileName = [fileName copy]; + _lineNumber = lineNumber; + _reason = [reason copy]; + } + return self; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCTestFailureReporter.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCTestFailureReporter.h new file mode 100644 index 0000000000..0ef0c4f873 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCTestFailureReporter.h @@ -0,0 +1,25 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +@class HCTestFailure; + + +NS_ASSUME_NONNULL_BEGIN + +/*! + Chain-of-responsibility for handling test failures. + */ +@interface HCTestFailureReporter : NSObject + +@property (nullable, nonatomic, strong) HCTestFailureReporter *successor; + +/*! + Handle test failure at specific location, or pass to successor. + */ +- (void)handleFailure:(HCTestFailure *)failure; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCTestFailureReporter.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCTestFailureReporter.m new file mode 100644 index 0000000000..ec35ffbd98 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCTestFailureReporter.m @@ -0,0 +1,22 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCTestFailureReporter.h" + +@interface HCTestFailureReporter (SubclassResponsibility) +- (BOOL)willHandleFailure:(HCTestFailure *)failure; +- (void)executeHandlingOfFailure:(HCTestFailure *)failure; +@end + + +@implementation HCTestFailureReporter + +- (void)handleFailure:(HCTestFailure *)failure +{ + if ([self willHandleFailure:failure]) + [self executeHandlingOfFailure:failure]; + else + [self.successor handleFailure:failure]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCTestFailureReporterChain.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCTestFailureReporterChain.h new file mode 100644 index 0000000000..69e5720ab7 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCTestFailureReporterChain.h @@ -0,0 +1,36 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +@class HCTestFailureReporter; + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Manage chain-of-responsibility for reporting test failures. + * @discussion This provides a generic way of reporting test failures without knowing about the + * underlying test framework. By default, we try XCTest first, then SenTestingKit. If we run out of + * options, the final catch-all is to throw an NSException describing the test failure. + */ +@interface HCTestFailureReporterChain : NSObject + +/*! + * @abstract Returns current chain of test failure reporters. + */ ++ (HCTestFailureReporter *)reporterChain; + +/*! + * @abstract Adds specified test failure reporter to head of chain-of-responsibility. + */ ++ (void)addReporter:(HCTestFailureReporter *)reporter; + +/*! + * @abstract Resets chain-of-responsibility to default. + */ ++ (void)reset; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCTestFailureReporterChain.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCTestFailureReporterChain.m new file mode 100644 index 0000000000..285cf47dc2 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCTestFailureReporterChain.m @@ -0,0 +1,41 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCTestFailureReporterChain.h" + +#import "HCGenericTestFailureReporter.h" +#import "HCSenTestFailureReporter.h" +#import "HCXCTestFailureReporter.h" + +static HCTestFailureReporter *chainHead = nil; + + +@implementation HCTestFailureReporterChain + ++ (HCTestFailureReporter *)reporterChain +{ + if (!chainHead) + { + HCTestFailureReporter *xctestReporter = [[HCXCTestFailureReporter alloc] init]; + HCTestFailureReporter *ocunitReporter = [[HCSenTestFailureReporter alloc] init]; + HCTestFailureReporter *genericReporter = [[HCGenericTestFailureReporter alloc] init]; + + chainHead = xctestReporter; + xctestReporter.successor = ocunitReporter; + ocunitReporter.successor = genericReporter; + } + return chainHead; +} + ++ (void)addReporter:(HCTestFailureReporter *)reporter +{ + reporter.successor = [self reporterChain]; + chainHead = reporter; +} + ++ (void)reset +{ + chainHead = nil; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCXCTestFailureReporter.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCXCTestFailureReporter.h new file mode 100644 index 0000000000..03605a1c55 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCXCTestFailureReporter.h @@ -0,0 +1,8 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCTestFailureReporter.h" + + +@interface HCXCTestFailureReporter : HCTestFailureReporter +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCXCTestFailureReporter.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCXCTestFailureReporter.m new file mode 100644 index 0000000000..9b835228e9 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Core/Helpers/TestFailureReporters/HCXCTestFailureReporter.m @@ -0,0 +1,33 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCXCTestFailureReporter.h" + +#import "HCTestFailure.h" + +@interface NSObject (PretendMethodExistsOnNSObjectToAvoidLinkingXCTest) + +- (void)recordFailureWithDescription:(NSString *)description + inFile:(NSString *)filename + atLine:(NSUInteger)lineNumber + expected:(BOOL)expected; + +@end + + +@implementation HCXCTestFailureReporter + +- (BOOL)willHandleFailure:(HCTestFailure *)failure +{ + return [failure.testCase respondsToSelector:@selector(recordFailureWithDescription:inFile:atLine:expected:)]; +} + +- (void)executeHandlingOfFailure:(HCTestFailure *)failure +{ + [failure.testCase recordFailureWithDescription:failure.reason + inFile:failure.fileName + atLine:failure.lineNumber + expected:YES]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCEvery.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCEvery.h new file mode 100644 index 0000000000..f8bf1f1cbc --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCEvery.h @@ -0,0 +1,45 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Matches if every item in a collection satisfies a nested matcher. + */ +@interface HCEvery : HCDiagnosingMatcher + +@property (nonatomic, strong, readonly) id matcher; + +- (instancetype)initWithMatcher:(id )matcher NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + + +FOUNDATION_EXPORT id HC_everyItem(id itemMatcher); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher for collections that matches when the examined collection's items are + * all matched by the specified matcher. + * @param itemMatcher The matcher to apply to every item provided by the examined collection. + * @discussion This matcher works on any collection that conforms to the NSFastEnumeration protocol, + * performing a single pass. + * + * Example
+ *
assertThat(\@[\@"bar", \@"baz"], everyItem(startsWith(\@"ba")))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_everyItem instead. + */ +static inline id everyItem(id itemMatcher) +{ + return HC_everyItem(itemMatcher); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCEvery.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCEvery.m new file mode 100644 index 0000000000..f344c126e2 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCEvery.m @@ -0,0 +1,74 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCEvery.h" + +#import "HCRequireNonNilObject.h" + + +@implementation HCEvery + +- (instancetype)initWithMatcher:(id )matcher +{ + HCRequireNonNilObject(matcher); + + self = [super init]; + if (self) + _matcher = matcher; + return self; +} + +- (BOOL)matches:(id)collection describingMismatchTo:(id )mismatchDescription +{ + if (![collection conformsToProtocol:@protocol(NSFastEnumeration)]) + { + [[mismatchDescription appendText:@"was non-collection "] appendDescriptionOf:collection]; + return NO; + } + + if ([collection count] == 0) + { + [mismatchDescription appendText:@"was empty"]; + return NO; + } + + for (id item in collection) + { + if (![self.matcher matches:item]) + { + [self describeAllMismatchesInCollection:collection to:mismatchDescription]; + return NO; + } + } + return YES; +} + +- (void)describeAllMismatchesInCollection:(id)collection to:(id )mismatchDescription +{ + [mismatchDescription appendText:@"mismatches were: ["]; + BOOL isPastFirst = NO; + for (id item in collection) + { + if (![self.matcher matches:item]) + { + if (isPastFirst) + [mismatchDescription appendText:@", "]; + [self.matcher describeMismatchOf:item to:mismatchDescription]; + isPastFirst = YES; + } + } + [mismatchDescription appendText:@"]"]; +} + +- (void)describeTo:(id )description +{ + [[description appendText:@"every item is "] appendDescriptionOf:self.matcher]; +} + +@end + + +id HC_everyItem(id itemMatcher) +{ + return [[HCEvery alloc] initWithMatcher:itemMatcher]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCHasCount.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCHasCount.h new file mode 100644 index 0000000000..f3b45c0f17 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCHasCount.h @@ -0,0 +1,63 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Matches if collection size satisfies a nested matcher. + */ +@interface HCHasCount : HCBaseMatcher + +- (instancetype)initWithMatcher:(id )countMatcher NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + + +FOUNDATION_EXPORT id HC_hasCount(id countMatcher); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object's -count method + * returns a value that satisfies the specified matcher. + * @param countMatcher A matcher for the count of an examined collection. + * @discussion + * Example
+ *
assertThat(\@[\@"foo", \@"bar"], hasCount(equalTo(@2)))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_hasCount instead. + */ +static inline id hasCount(id countMatcher) +{ + return HC_hasCount(countMatcher); +} +#endif + + +FOUNDATION_EXPORT id HC_hasCountOf(NSUInteger count); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object's -count method + * returns a value that equals the specified value. + * @param value Value to compare against as the expected count. + * @discussion + * Example
+ *
assertThat(\@[\@"foo", \@"bar"], hasCountOf(2))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_hasCountOf instead. + */ +static inline id hasCountOf(NSUInteger value) +{ + return HC_hasCountOf(value); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCHasCount.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCHasCount.m new file mode 100644 index 0000000000..961c63b48c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCHasCount.m @@ -0,0 +1,65 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCHasCount.h" + +#import "HCIsEqual.h" + + +@interface HCHasCount () +@property (nonatomic, strong, readonly) id countMatcher; +@end + +@implementation HCHasCount + +- (instancetype)initWithMatcher:(id )countMatcher +{ + self = [super init]; + if (self) + _countMatcher = countMatcher; + return self; +} + +- (BOOL)matches:(nullable id)item +{ + if (![self itemHasCount:item]) + return NO; + + NSNumber *count = @([item count]); + return [self.countMatcher matches:count]; +} + +- (BOOL)itemHasCount:(id)item +{ + return [item respondsToSelector:@selector(count)]; +} + +- (void)describeMismatchOf:(nullable id)item to:(nullable id )mismatchDescription +{ + [mismatchDescription appendText:@"was "]; + if ([self itemHasCount:item]) + { + [[[mismatchDescription appendText:@"count of "] + appendDescriptionOf:@([item count])] + appendText:@" with "]; + } + [mismatchDescription appendDescriptionOf:item]; +} + +- (void)describeTo:(id )description +{ + [[description appendText:@"a collection with count of "] appendDescriptionOf:self.countMatcher]; +} + +@end + + +id HC_hasCount(id countMatcher) +{ + return [[HCHasCount alloc] initWithMatcher:countMatcher]; +} + +id HC_hasCountOf(NSUInteger value) +{ + return HC_hasCount(HC_equalTo(@(value))); +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContaining.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContaining.h new file mode 100644 index 0000000000..b2abf5a2fd --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContaining.h @@ -0,0 +1,95 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Matches if any item in a collection satisfies a nested matcher. + */ +@interface HCIsCollectionContaining : HCDiagnosingMatcher + +- (instancetype)initWithMatcher:(id )elementMatcher NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + + +FOUNDATION_EXPORT id HC_hasItem(id itemMatcher); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract hasItem(itemMatcher) - + * Creates a matcher for collections that matches when at least one item in the examined collection + * satisfies the specified matcher. + * @param itemMatcher The matcher to apply to collection elements, or an expected value + * for equalTo matching. + * @discussion This matcher works on any collection that conforms to the NSFastEnumeration protocol, + * performing a single pass. + * + * If itemMatcher is not a matcher, it is implicitly wrapped in an equalTo matcher + * to check for equality. + * + * Example
+ *
assertThat(\@[\@1, \@2, \@3], hasItem(equalTo(\@2)))
+ * + *
assertThat(\@[\@1, \@2, \@3], hasItem(\@2))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_hasItem instead. + */ +#define hasItem HC_hasItem +#endif + + +FOUNDATION_EXPORT id HC_hasItemsIn(NSArray *itemMatchers); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher for collections that matches when all specified matchers are + * satisfied by any item in the examined collection. + * @param itemMatchers An array of matchers. Any element that is not a matcher is implicitly wrapped + * in an equalTo matcher to check for equality. + * @discussion This matcher works on any collection that conforms to the NSFastEnumeration protocol, + * performing one pass for each matcher. + * + * Example
+ *
assertThat(\@[\@"foo", \@"bar", \@"baz"], hasItems(\@[endsWith(\@"z"), endsWith(\@"o")]))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_hasItemsIn instead. + */ +static inline id hasItemsIn(NSArray *itemMatchers) +{ + return HC_hasItemsIn(itemMatchers); +} +#endif + + +FOUNDATION_EXPORT id HC_hasItems(id itemMatchers, ...) NS_REQUIRES_NIL_TERMINATION; + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher for collections that matches when all specified matchers are + * satisfied by any item in the examined collection. + * @param itemMatchers... A comma-separated list of matchers ending with nil. + * Any argument that is not a matcher is implicitly wrapped in an equalTo matcher to check + * for equality. + * @discussion This matcher works on any collection that conforms to the NSFastEnumeration protocol, + * performing one pass for each matcher. + * + * Example
+ *
assertThat(\@[\@"foo", \@"bar", \@"baz"], hasItems(endsWith(\@"z"), endsWith(\@"o"), nil))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_hasItems instead. + */ +#define hasItems(itemMatchers...) HC_hasItems(itemMatchers) +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContaining.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContaining.m new file mode 100644 index 0000000000..f9a48b650d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContaining.m @@ -0,0 +1,88 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCIsCollectionContaining.h" + +#import "HCAllOf.h" +#import "HCCollect.h" +#import "HCRequireNonNilObject.h" +#import "HCWrapInMatcher.h" + + +@interface HCIsCollectionContaining () +@property (nonatomic, strong, readonly) id elementMatcher; +@end + +@implementation HCIsCollectionContaining + +- (instancetype)initWithMatcher:(id )elementMatcher +{ + self = [super init]; + if (self) + _elementMatcher = elementMatcher; + return self; +} + +- (BOOL)matches:(id)collection describingMismatchTo:(id )mismatchDescription +{ + if (![collection conformsToProtocol:@protocol(NSFastEnumeration)]) + { + [[mismatchDescription appendText:@"was non-collection "] appendDescriptionOf:collection]; + return NO; + } + + if ([collection count] == 0) + { + [mismatchDescription appendText:@"was empty"]; + return NO; + } + + for (id item in collection) + if ([self.elementMatcher matches:item]) + return YES; + + [mismatchDescription appendText:@"mismatches were: ["]; + BOOL isPastFirst = NO; + for (id item in collection) + { + if (isPastFirst) + [mismatchDescription appendText:@", "]; + [self.elementMatcher describeMismatchOf:item to:mismatchDescription]; + isPastFirst = YES; + } + [mismatchDescription appendText:@"]"]; + return NO; +} + +- (void)describeTo:(id )description +{ + [[description appendText:@"a collection containing "] + appendDescriptionOf:self.elementMatcher]; +} + +@end + + +id HC_hasItem(id itemMatcher) +{ + HCRequireNonNilObject(itemMatcher); + return [[HCIsCollectionContaining alloc] initWithMatcher:HCWrapInMatcher(itemMatcher)]; +} + +id HC_hasItemsIn(NSArray *itemMatchers) +{ + NSMutableArray *matchers = [[NSMutableArray alloc] init]; + for (id itemMatcher in itemMatchers) + [matchers addObject:HC_hasItem(itemMatcher)]; + return HC_allOfIn(matchers); +} + +id HC_hasItems(id itemMatchers, ...) +{ + va_list args; + va_start(args, itemMatchers); + NSArray *array = HCCollectItems(itemMatchers, args); + va_end(args); + + return HC_hasItemsIn(array); +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContainingInAnyOrder.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContainingInAnyOrder.h new file mode 100644 index 0000000000..274b6f8d86 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContainingInAnyOrder.h @@ -0,0 +1,79 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Matches if every item in a collection, in any order, satisfy a list of nested matchers. + */ +@interface HCIsCollectionContainingInAnyOrder : HCDiagnosingMatcher + +- (instancetype)initWithMatchers:(NSArray> *)itemMatchers NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + + +FOUNDATION_EXPORT id HC_containsInAnyOrderIn(NSArray *itemMatchers); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates an order-agnostic matcher for collections that matches when each item in the + * examined collection satisfies one matcher anywhere in the specified list of matchers. + * @param itemMatchers An array of matchers. Any element that is not a matcher is implicitly wrapped + * in an equalTo matcher to check for equality. + * @discussion This matcher works on any collection that conforms to the NSFastEnumeration protocol, + * performing a single pass. For a positive match, the examined collection must be of the same + * length as the specified list of matchers. + * + * Note: Each matcher in the specified list will only be used once during a given examination, so + * be careful when specifying matchers that may be satisfied by more than one entry in an examined + * collection. + * + * Examples
+ *
assertThat(\@[\@"foo", \@"bar"], containsInAnyOrderIn(\@[equalTo(\@"bar"), equalTo(\@"foo")]))
+ *
assertThat(\@[\@"foo", \@"bar"], containsInAnyOrderIn(@[\@"bar", \@"foo"]))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_containsInAnyOrderIn instead. + */ +static inline id containsInAnyOrderIn(NSArray *itemMatchers) +{ + return HC_containsInAnyOrderIn(itemMatchers); +} +#endif + + +FOUNDATION_EXPORT id HC_containsInAnyOrder(id itemMatchers, ...) NS_REQUIRES_NIL_TERMINATION; + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates an order-agnostic matcher for collections that matches when each item in the + * examined collection satisfies one matcher anywhere in the specified list of matchers. + * @param itemMatchers... A comma-separated list of matchers ending with nil. + * Any argument that is not a matcher is implicitly wrapped in an equalTo matcher to check + * for equality. + * @discussion This matcher works on any collection that conforms to the NSFastEnumeration protocol, + * performing a single pass. For a positive match, the examined collection must be of the same + * length as the specified list of matchers. + * + * Note: Each matcher in the specified list will only be used once during a given examination, so + * be careful when specifying matchers that may be satisfied by more than one entry in an examined + * collection. + * + * Examples
+ *
assertThat(\@[\@"foo", \@"bar"], containsInAnyOrder(equalTo(\@"bar"), equalTo(\@"foo"), nil))
+ *
assertThat(\@[\@"foo", \@"bar"], containsInAnyOrder(\@"bar", \@"foo", nil))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_containsInAnyOrder instead. + */ +#define containsInAnyOrder(itemMatchers...) HC_containsInAnyOrder(itemMatchers) +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContainingInAnyOrder.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContainingInAnyOrder.m new file mode 100644 index 0000000000..712babc2be --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContainingInAnyOrder.m @@ -0,0 +1,115 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCIsCollectionContainingInAnyOrder.h" + +#import "HCCollect.h" + + +@interface HCMatchingInAnyOrder : NSObject +@property (nonatomic, copy, readonly) NSMutableArray> *matchers; +@property (nonatomic, strong, readonly) id mismatchDescription; +@end + +@implementation HCMatchingInAnyOrder + +- (instancetype)initWithMatchers:(NSArray> *)itemMatchers + mismatchDescription:(id )description +{ + self = [super init]; + if (self) + { + _matchers = [itemMatchers mutableCopy]; + _mismatchDescription = description; + } + return self; +} + +- (BOOL)matches:(nullable id)item +{ + NSUInteger index = 0; + for (id matcher in self.matchers) + { + if ([matcher matches:item]) + { + [self.matchers removeObjectAtIndex:index]; + return YES; + } + ++index; + } + [[self.mismatchDescription appendText:@"not matched: "] + appendDescriptionOf:item]; + return NO; +} + +- (BOOL)isFinishedWith:(NSArray *)collection +{ + if (self.matchers.count == 0) + return YES; + + [[[[self.mismatchDescription appendText:@"no item matches: "] + appendList:self.matchers start:@"" separator:@", " end:@""] + appendText:@" in "] + appendList:collection start:@"[" separator:@", " end:@"]"]; + return NO; +} + +@end + + +@interface HCIsCollectionContainingInAnyOrder () +@property (nonatomic, copy, readonly) NSArray> *matchers; +@end + +@implementation HCIsCollectionContainingInAnyOrder + +- (instancetype)initWithMatchers:(NSArray> *)itemMatchers +{ + self = [super init]; + if (self) + _matchers = [itemMatchers copy]; + return self; +} + +- (BOOL)matches:(id)collection describingMismatchTo:(id )mismatchDescription +{ + if (![collection conformsToProtocol:@protocol(NSFastEnumeration)]) + { + [[mismatchDescription appendText:@"was non-collection "] appendDescriptionOf:collection]; + return NO; + } + + HCMatchingInAnyOrder *matchSequence = + [[HCMatchingInAnyOrder alloc] initWithMatchers:self.matchers + mismatchDescription:mismatchDescription]; + for (id item in collection) + if (![matchSequence matches:item]) + return NO; + + return [matchSequence isFinishedWith:collection]; +} + +- (void)describeTo:(id )description +{ + [[[description appendText:@"a collection over "] + appendList:self.matchers start:@"[" separator:@", " end:@"]"] + appendText:@" in any order"]; +} + +@end + + +id HC_containsInAnyOrderIn(NSArray *itemMatchers) +{ + return [[HCIsCollectionContainingInAnyOrder alloc] initWithMatchers:HCWrapIntoMatchers(itemMatchers)]; +} + +id HC_containsInAnyOrder(id itemMatchers, ...) +{ + va_list args; + va_start(args, itemMatchers); + NSArray *array = HCCollectItems(itemMatchers, args); + va_end(args); + + return HC_containsInAnyOrderIn(array); +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContainingInOrder.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContainingInOrder.h new file mode 100644 index 0000000000..73def8b78a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContainingInOrder.h @@ -0,0 +1,71 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Matches if every item in a collection satisfies a list of nested matchers, in order. + */ +@interface HCIsCollectionContainingInOrder : HCDiagnosingMatcher + +- (instancetype)initWithMatchers:(NSArray> *)itemMatchers NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + + +FOUNDATION_EXPORT id HC_containsIn(NSArray *itemMatchers); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher for collections that matches when each item in the examined + * collection satisfies the corresponding matcher in the specified list of matchers. + * @param itemMatchers An array of matchers. Any element that is not a matcher is implicitly wrapped + * in an equalTo matcher to check for equality. + * @discussion This matcher works on any collection that conforms to the NSFastEnumeration protocol, + * performing a single pass. For a positive match, the examined collection must be of the same + * length as the specified list of matchers. + * + * Examples
+ *
assertThat(\@[\@"foo", \@"bar"], containsIn(\@[equalTo(\@"foo"), equalTo(\@"bar")]))
+ *
assertThat(\@[\@"foo", \@"bar"], containsIn(\@[\@"foo", \@"bar"]))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_containsIn instead.) + */ +static inline id containsIn(NSArray *itemMatchers) +{ + return HC_containsIn(itemMatchers); +} +#endif + + +FOUNDATION_EXPORT id HC_contains(id itemMatchers, ...) NS_REQUIRES_NIL_TERMINATION; + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher for collections that matches when each item in the examined + * collection satisfies the corresponding matcher in the specified list of matchers. + * @param itemMatchers... A comma-separated list of matchers ending with nil. + * Any argument that is not a matcher is implicitly wrapped in an equalTo matcher to check + * for equality. + * @discussion This matcher works on any collection that conforms to the NSFastEnumeration protocol, + * performing a single pass. For a positive match, the examined collection must be of the same + * length as the specified list of matchers. + * + * Examples
+ *
assertThat(\@[\@"foo", \@"bar"], contains(equalTo(\@"foo"), equalTo(\@"bar"), nil))
+ *
assertThat(\@[\@"foo", \@"bar"], contains(\@"foo", \@"bar", nil))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_contains instead.) + */ +#define contains(itemMatchers...) HC_contains(itemMatchers) +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContainingInOrder.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContainingInOrder.m new file mode 100644 index 0000000000..05f83888c3 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContainingInOrder.m @@ -0,0 +1,134 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCIsCollectionContainingInOrder.h" + +#import "HCCollect.h" + + +@interface HCMatchSequence : NSObject +@property (nonatomic, copy, readonly) NSArray> *matchers; +@property (nonatomic, strong, readonly) id mismatchDescription; +@property (nonatomic, assign) NSUInteger nextMatchIndex; +@end + +@implementation HCMatchSequence + +- (instancetype)initWithMatchers:(NSArray> *)itemMatchers + mismatchDescription:(id )description +{ + self = [super init]; + if (self) + { + _matchers = [itemMatchers copy]; + _mismatchDescription = description; + } + return self; +} + +- (BOOL)matches:(nullable id)item +{ + return [self isNotSurplus:item] && [self isMatched:item]; +} + +- (BOOL)isFinished +{ + if (self.nextMatchIndex < self.matchers.count) + { + [[self.mismatchDescription appendText:@"no item was "] + appendDescriptionOf:self.matchers[self.nextMatchIndex]]; + return NO; + } + return YES; +} + +- (BOOL)isMatched:(id)item +{ + id matcher = self.matchers[self.nextMatchIndex]; + if (![matcher matches:item]) + { + [self describeMismatchOfMatcher:matcher item:item]; + return NO; + } + ++self.nextMatchIndex; + return YES; +} + +- (BOOL)isNotSurplus:(id)item +{ + if (self.matchers.count <= self.nextMatchIndex) + { + [[self.mismatchDescription + appendText:[NSString stringWithFormat:@"exceeded count of %lu with item ", + (unsigned long)self.matchers.count]] + appendDescriptionOf:item]; + return NO; + } + return YES; +} + +- (void)describeMismatchOfMatcher:(id )matcher item:(id)item +{ + [self.mismatchDescription appendText:[NSString stringWithFormat:@"item %lu: ", + (unsigned long)self.nextMatchIndex]]; + [matcher describeMismatchOf:item to:self.mismatchDescription]; +} + +@end + + +@interface HCIsCollectionContainingInOrder () +@property (nonatomic, copy, readonly) NSArray> *matchers; +@end + +@implementation HCIsCollectionContainingInOrder + +- (instancetype)initWithMatchers:(NSArray> *)itemMatchers +{ + self = [super init]; + if (self) + _matchers = [itemMatchers copy]; + return self; +} + +- (BOOL)matches:(id)collection describingMismatchTo:(id )mismatchDescription +{ + if (![collection conformsToProtocol:@protocol(NSFastEnumeration)]) + { + [[mismatchDescription appendText:@"was non-collection "] appendDescriptionOf:collection]; + return NO; + } + + HCMatchSequence *matchSequence = + [[HCMatchSequence alloc] initWithMatchers:self.matchers + mismatchDescription:mismatchDescription]; + for (id item in collection) + if (![matchSequence matches:item]) + return NO; + + return [matchSequence isFinished]; +} + +- (void)describeTo:(id )description +{ + [[description appendText:@"a collection containing "] + appendList:self.matchers start:@"[" separator:@", " end:@"]"]; +} + +@end + + +id HC_containsIn(NSArray *itemMatchers) +{ + return [[HCIsCollectionContainingInOrder alloc] initWithMatchers:HCWrapIntoMatchers(itemMatchers)]; +} + +id HC_contains(id itemMatchers, ...) +{ + va_list args; + va_start(args, itemMatchers); + NSArray *array = HCCollectItems(itemMatchers, args); + va_end(args); + + return HC_containsIn(array); +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContainingInRelativeOrder.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContainingInRelativeOrder.h new file mode 100644 index 0000000000..0f29915dcb --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContainingInRelativeOrder.h @@ -0,0 +1,48 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Matches if every item in a collection satisfies a list of nested matchers, in order. + */ +@interface HCIsCollectionContainingInRelativeOrder : HCDiagnosingMatcher + +- (instancetype)initWithMatchers:(NSArray> *)itemMatchers NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + + +FOUNDATION_EXPORT id HC_containsInRelativeOrder(NSArray *itemMatchers); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher for collections that matches when the examined collection contains + * items satisfying the specified list of matchers, in the same relative order. + * @param itemMatchers Array of matchers that must be satisfied by the items provided by the + * examined collection in the same relative order. + * @discussion This matcher works on any collection that conforms to the NSFastEnumeration protocol, + * performing a single pass. + * + * Any element of itemMatchers that is not a matcher is implicitly wrapped in an + * equalTo matcher to check for equality. + * + * Examples
+ *
assertThat(\@[\@1, \@2, \@3, \@4, \@5], containsInRelativeOrder(equalTo(\@2), equalTo(\@4)))
+ *
assertThat(\@[\@1, \@2, \@3, \@4, \@5], containsInRelativeOrder(\@2, \@4))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_containsInRelativeOrder instead. + */ +static inline id containsInRelativeOrder(NSArray *itemMatchers) +{ + return HC_containsInRelativeOrder(itemMatchers); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContainingInRelativeOrder.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContainingInRelativeOrder.m new file mode 100644 index 0000000000..24b31186a7 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionContainingInRelativeOrder.m @@ -0,0 +1,124 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCIsCollectionContainingInRelativeOrder.h" + +#import "HCCollect.h" + + +static void HCRequireNonEmptyArray(NSArray *array) +{ + if (!array.count) + { + @throw [NSException exceptionWithName:@"EmptyArray" + reason:@"Must be non-empty array" + userInfo:nil]; + } +} + + +@interface HCMatchSequenceInRelativeOrder : NSObject +@property (nonatomic, copy, readonly) NSArray> *matchers; +@property (nonatomic, strong, readonly) id mismatchDescription; +@property (nonatomic, assign) NSUInteger nextMatchIndex; +@property (nonatomic, strong) id lastMatchedItem; +@end + +@implementation HCMatchSequenceInRelativeOrder + +- (instancetype)initWithMatchers:(NSArray> *)itemMatchers + mismatchDescription:(id )description +{ + self = [super init]; + if (self) + { + _matchers = [itemMatchers copy]; + _mismatchDescription = description; + } + return self; +} + +- (void)processItems:(NSArray *)sequence +{ + for (id item in sequence) + { + if (self.nextMatchIndex < self.matchers.count) + { + id matcher = self.matchers[self.nextMatchIndex]; + if ([matcher matches:item]) + { + self.lastMatchedItem = item; + self.nextMatchIndex += 1; + } + } + } +} + +- (BOOL)isFinished +{ + if (self.nextMatchIndex < self.matchers.count) + { + [[self.mismatchDescription + appendDescriptionOf:self.matchers[self.nextMatchIndex]] + appendText:@" was not found"]; + if (self.lastMatchedItem != nil) + { + [[self.mismatchDescription + appendText:@" after "] + appendDescriptionOf:self.lastMatchedItem]; + } + return NO; + } + return YES; +} + +@end + + +@interface HCIsCollectionContainingInRelativeOrder () +@property (nonatomic, copy, readonly) NSArray> *matchers; +@end + +@implementation HCIsCollectionContainingInRelativeOrder + +- (instancetype)initWithMatchers:(NSArray> *)itemMatchers +{ + HCRequireNonEmptyArray(itemMatchers); + + self = [super init]; + if (self) + _matchers = [itemMatchers copy]; + return self; +} + +- (BOOL)matches:(id)collection describingMismatchTo:(id )mismatchDescription +{ + if (![collection conformsToProtocol:@protocol(NSFastEnumeration)]) + { + [[mismatchDescription appendText:@"was non-collection "] appendDescriptionOf:collection]; + return NO; + } + + HCMatchSequenceInRelativeOrder *matchSequenceInRelativeOrder = + [[HCMatchSequenceInRelativeOrder alloc] initWithMatchers:self.matchers + mismatchDescription:mismatchDescription]; + [matchSequenceInRelativeOrder processItems:collection]; + return [matchSequenceInRelativeOrder isFinished]; +} + +- (void)describeTo:(id )description +{ + [[[description + appendText:@"a collection containing "] + appendList:self.matchers start:@"[" separator:@", " end:@"]"] + appendText:@" in relative order"]; +} + +@end + + +id HC_containsInRelativeOrder(NSArray *itemMatchers) +{ + NSArray *matchers = HCWrapIntoMatchers(itemMatchers); + return [[HCIsCollectionContainingInRelativeOrder alloc] initWithMatchers:matchers]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionOnlyContaining.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionOnlyContaining.h new file mode 100644 index 0000000000..a13fe85530 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionOnlyContaining.h @@ -0,0 +1,62 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Matches if every item in a collection satisfies any of the nested matchers. + */ +@interface HCIsCollectionOnlyContaining : HCEvery +@end + +FOUNDATION_EXPORT id HC_onlyContainsIn(NSArray *itemMatchers); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher for collections that matches when each item of the examined + * collection satisfies any of the specified matchers. + * @param itemMatchers An array of matchers. Any element that is not a matcher is implicitly wrapped + * in an equalTo matcher to check for equality. + * @discussion This matcher works on any collection that conforms to the NSFastEnumeration protocol, + * performing a single pass. Any matcher may match multiple elements. + * + * Example
+ *
assertThat(\@[\@"Jon", \@"John", \@"Bob"], onlyContainsIn(\@[startsWith(\@"Jo"), startsWith(\@("Bo")]))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_onlyContainsIn instead. + */ +static inline id onlyContainsIn(NSArray *itemMatchers) +{ + return HC_onlyContainsIn(itemMatchers); +} +#endif + + +FOUNDATION_EXPORT id HC_onlyContains(id itemMatchers, ...) NS_REQUIRES_NIL_TERMINATION; + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher for collections that matches when each item of the examined + * collection satisfies any of the specified matchers. + * @param itemMatchers... A comma-separated list of matchers ending with nil. + * Any argument that is not a matcher is implicitly wrapped in an equalTo matcher to check for + * equality. + * @discussion This matcher works on any collection that conforms to the NSFastEnumeration protocol, + * performing a single pass. Any matcher may match multiple elements. + * + * Example
+ *
assertThat(\@[\@"Jon", \@"John", \@"Bob"], onlyContains(startsWith(\@"Jo"), startsWith(\@("Bo"), nil))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_onlyContains instead. + */ +#define onlyContains(itemMatchers...) HC_onlyContains(itemMatchers) +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionOnlyContaining.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionOnlyContaining.m new file mode 100644 index 0000000000..ecfbfdd9d1 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsCollectionOnlyContaining.m @@ -0,0 +1,34 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCIsCollectionOnlyContaining.h" + +#import "HCAnyOf.h" +#import "HCCollect.h" + + +@implementation HCIsCollectionOnlyContaining + +- (void)describeTo:(id )description +{ + [[description appendText:@"a collection containing items matching "] + appendDescriptionOf:self.matcher]; +} + +@end + + +id HC_onlyContainsIn(NSArray *itemMatchers) +{ + return [[HCIsCollectionOnlyContaining alloc] initWithMatcher:HC_anyOfIn(itemMatchers)]; +} + +id HC_onlyContains(id itemMatchers, ...) +{ + va_list args; + va_start(args, itemMatchers); + NSArray *array = HCCollectItems(itemMatchers, args); + va_end(args); + + return HC_onlyContainsIn(array); +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContaining.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContaining.h new file mode 100644 index 0000000000..eb834a0fcb --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContaining.h @@ -0,0 +1,47 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Matches if any entry in a dictionary satisfies the nested pair of matchers. + */ +@interface HCIsDictionaryContaining : HCBaseMatcher + +- (instancetype)initWithKeyMatcher:(id )keyMatcher + valueMatcher:(id )valueMatcher NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + + +FOUNDATION_EXPORT id HC_hasEntry(id keyMatcher, id valueMatcher); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher for NSDictionaries that matches when the examined dictionary contains + * at least one entry whose key satisfies the specified keyMatcher and whose + * value satisfies the specified valueMatcher. + * @param keyMatcher The matcher to satisfy for the key, or an expected value for equalTo matching. + * @param valueMatcher The matcher to satisfy for the value, or an expected value for equalTo matching. + * @discussion Any argument that is not a matcher is implicitly wrapped in an equalTo + * matcher to check for equality. + * + * Examples
+ *
assertThat(myDictionary, hasEntry(equalTo(\@"foo"), equalTo(\@"bar")))
+ *
assertThat(myDictionary, hasEntry(\@"foo", \@"bar"))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_hasEntry instead. + */ +static inline id hasEntry(id keyMatcher, id valueMatcher) +{ + return HC_hasEntry(keyMatcher, valueMatcher); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContaining.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContaining.m new file mode 100644 index 0000000000..d97f2b29d3 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContaining.m @@ -0,0 +1,56 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCIsDictionaryContaining.h" + +#import "HCRequireNonNilObject.h" +#import "HCWrapInMatcher.h" + + +@interface HCIsDictionaryContaining () +@property (nonatomic, strong, readonly) id keyMatcher; +@property (nonatomic, strong, readonly) id valueMatcher; +@end + +@implementation HCIsDictionaryContaining + +- (instancetype)initWithKeyMatcher:(id )keyMatcher + valueMatcher:(id )valueMatcher +{ + self = [super init]; + if (self) + { + _keyMatcher = keyMatcher; + _valueMatcher = valueMatcher; + } + return self; +} + +- (BOOL)matches:(id)dict +{ + if ([dict isKindOfClass:[NSDictionary class]]) + for (id oneKey in dict) + if ([self.keyMatcher matches:oneKey] && [self.valueMatcher matches:dict[oneKey]]) + return YES; + return NO; +} + +- (void)describeTo:(id )description +{ + [[[[[description appendText:@"a dictionary containing { "] + appendDescriptionOf:self.keyMatcher] + appendText:@" = "] + appendDescriptionOf:self.valueMatcher] + appendText:@"; }"]; +} + +@end + + +id HC_hasEntry(id keyMatcher, id valueMatcher) +{ + HCRequireNonNilObject(keyMatcher); + HCRequireNonNilObject(valueMatcher); + return [[HCIsDictionaryContaining alloc] initWithKeyMatcher:HCWrapInMatcher(keyMatcher) + valueMatcher:HCWrapInMatcher(valueMatcher)]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContainingEntries.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContainingEntries.h new file mode 100644 index 0000000000..09387bbd2d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContainingEntries.h @@ -0,0 +1,68 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Matches if dictionary contains entries that satisfy the list of keys and value + * matchers. + */ +@interface HCIsDictionaryContainingEntries : HCDiagnosingMatcher + +- (instancetype)initWithKeys:(NSArray *)keys + valueMatchers:(NSArray> *)valueMatchers NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + +FOUNDATION_EXPORT id HC_hasEntriesIn(NSDictionary *valueMatchersForKeys); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher for NSDictionaries that matches when the examined dictionary contains + * entries satisfying a dictionary of keys and their value matchers. + * @param valueMatchersForKeys A dictionary of keys (not matchers) and their value matchers. Any + * value argument that is not a matcher is implicitly wrapped in an equalTo matcher to + * check for equality. + * @discussion + * Examples
+ *
assertThat(personDict, hasEntriesIn(\@{\@"firstName": equalTo(\@"Jon"), \@"lastName": equalTo(\@"Reid")}))
+ *
assertThat(personDict, hasEntriesIn(\@{\@"firstName": \@"Jon", \@"lastName": \@"Reid"}))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_hasEntryIn instead. + */ +static inline id hasEntriesIn(NSDictionary *valueMatchersForKeys) +{ + return HC_hasEntriesIn(valueMatchersForKeys); +} +#endif + +FOUNDATION_EXPORT id HC_hasEntries(id keysAndValueMatchers, ...) NS_REQUIRES_NIL_TERMINATION; + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher for NSDictionaries that matches when the examined dictionary contains + * entries satisfying a list of alternating keys and their value matchers. + * @param keysAndValueMatchers... A key (not a matcher) to look up, followed by a value matcher or + * an expected value for equalTo matching, in a comma-separated list ending + * with nil + * @discussion Note that the keys must be actual keys, not matchers. Any value argument that is not + * a matcher is implicitly wrapped in an equalTo matcher to check for equality. + * + * Examples
+ *
assertThat(personDict, hasEntries(\@"firstName", equalTo(\@"Jon"), \@"lastName", equalTo(\@"Reid"), nil))
+ *
assertThat(personDict, hasEntries(\@"firstName", \@"Jon", \@"lastName", \@"Reid", nil))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_hasEntry instead. + */ +#define hasEntries(keysAndValueMatchers...) HC_hasEntries(keysAndValueMatchers) +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContainingEntries.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContainingEntries.m new file mode 100644 index 0000000000..73a68ea002 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContainingEntries.m @@ -0,0 +1,132 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCIsDictionaryContainingEntries.h" + +#import "HCWrapInMatcher.h" + + +@interface HCIsDictionaryContainingEntries () +@property (nonatomic, copy, readonly) NSArray *keys; +@property (nonatomic, copy, readonly) NSArray> *valueMatchers; +@end + +@implementation HCIsDictionaryContainingEntries + +- (instancetype)initWithKeys:(NSArray *)keys + valueMatchers:(NSArray> *)valueMatchers +{ + self = [super init]; + if (self) + { + _keys = [keys copy]; + _valueMatchers = [valueMatchers copy]; + } + return self; +} + +- (BOOL)matches:(id)dict describingMismatchTo:(id )mismatchDescription +{ + if (![dict isKindOfClass:[NSDictionary class]]) + { + [[mismatchDescription appendText:@"was non-dictionary "] appendDescriptionOf:dict]; + return NO; + } + + NSUInteger count = self.keys.count; + for (NSUInteger index = 0; index < count; ++index) + { + id key = self.keys[index]; + if (dict[key] == nil) + { + [[[[mismatchDescription appendText:@"no "] + appendDescriptionOf:key] + appendText:@" key in "] + appendDescriptionOf:dict]; + return NO; + } + + id valueMatcher = self.valueMatchers[index]; + id actualValue = dict[key]; + + if (![valueMatcher matches:actualValue]) + { + [[[[mismatchDescription appendText:@"value for "] + appendDescriptionOf:key] + appendText:@" was "] + appendDescriptionOf:actualValue]; + return NO; + } + } + + return YES; +} + +- (void)describeKeyValueAtIndex:(NSUInteger)index to:(id )description +{ + [[[[description appendDescriptionOf:self.keys[index]] + appendText:@" = "] + appendDescriptionOf:self.valueMatchers[index]] + appendText:@"; "]; +} + +- (void)describeTo:(id )description +{ + [description appendText:@"a dictionary containing { "]; + NSUInteger count = [self.keys count]; + NSUInteger index = 0; + for (; index < count - 1; ++index) + [self describeKeyValueAtIndex:index to:description]; + [self describeKeyValueAtIndex:index to:description]; + [description appendText:@"}"]; +} + +@end + + +static void requirePairedObject(id obj) +{ + if (obj == nil) + { + @throw [NSException exceptionWithName:@"NilObject" + reason:@"HC_hasEntries keys and value matchers must be paired" + userInfo:nil]; + } +} + + +id HC_hasEntriesIn(NSDictionary *valueMatchersForKeys) +{ + NSArray *keys = valueMatchersForKeys.allKeys; + NSMutableArray> *valueMatchers = [[NSMutableArray alloc] init]; + for (id key in keys) + [valueMatchers addObject:HCWrapInMatcher(valueMatchersForKeys[key])]; + + return [[HCIsDictionaryContainingEntries alloc] initWithKeys:keys + valueMatchers:valueMatchers]; +} + +id HC_hasEntries(id keysAndValueMatchers, ...) +{ + va_list args; + va_start(args, keysAndValueMatchers); + + id key = keysAndValueMatchers; + id valueMatcher = va_arg(args, id); + requirePairedObject(valueMatcher); + NSMutableArray *keys = [NSMutableArray arrayWithObject:key]; + NSMutableArray> *valueMatchers = [NSMutableArray arrayWithObject:HCWrapInMatcher(valueMatcher)]; + + key = va_arg(args, id); + while (key != nil) + { + [keys addObject:key]; + valueMatcher = va_arg(args, id); + requirePairedObject(valueMatcher); + [valueMatchers addObject:HCWrapInMatcher(valueMatcher)]; + key = va_arg(args, id); + } + + return [[HCIsDictionaryContainingEntries alloc] initWithKeys:keys + valueMatchers:valueMatchers]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContainingKey.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContainingKey.h new file mode 100644 index 0000000000..c1f82e1736 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContainingKey.h @@ -0,0 +1,44 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Matches if any entry in a dictionary has a key satisfying the nested matcher. + */ +@interface HCIsDictionaryContainingKey : HCBaseMatcher + +- (instancetype)initWithKeyMatcher:(id )keyMatcher NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + + +FOUNDATION_EXPORT id HC_hasKey(id keyMatcher); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher for NSDictionaries that matches when the examined dictionary contains + * at least key that satisfies the specified matcher. + * @param keyMatcher The matcher to satisfy for the key, or an expected value for equalTo matching. + * @discussion Any argument that is not a matcher is implicitly wrapped in an equalTo + * matcher to check for equality. + * + * Examples
+ *
assertThat(myDictionary, hasEntry(equalTo(\@"foo")))
+ *
assertThat(myDictionary, hasEntry(\@"foo"))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_hasKey instead. + */ +static inline id hasKey(id keyMatcher) +{ + return HC_hasKey(keyMatcher); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContainingKey.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContainingKey.m new file mode 100644 index 0000000000..7cd726ac86 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContainingKey.m @@ -0,0 +1,46 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCIsDictionaryContainingKey.h" + +#import "HCRequireNonNilObject.h" +#import "HCWrapInMatcher.h" + + +@interface HCIsDictionaryContainingKey () +@property (nonatomic, strong, readonly) id keyMatcher; +@end + +@implementation HCIsDictionaryContainingKey + +- (instancetype)initWithKeyMatcher:(id )keyMatcher +{ + self = [super init]; + if (self) + _keyMatcher = keyMatcher; + return self; +} + +- (BOOL)matches:(id)dict +{ + if ([dict isKindOfClass:[NSDictionary class]]) + for (id oneKey in dict) + if ([self.keyMatcher matches:oneKey]) + return YES; + return NO; +} + +- (void)describeTo:(id )description +{ + [[description appendText:@"a dictionary containing key "] + appendDescriptionOf:self.keyMatcher]; +} + +@end + + +id HC_hasKey(id keyMatcher) +{ + HCRequireNonNilObject(keyMatcher); + return [[HCIsDictionaryContainingKey alloc] initWithKeyMatcher:HCWrapInMatcher(keyMatcher)]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContainingValue.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContainingValue.h new file mode 100644 index 0000000000..39b5167285 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContainingValue.h @@ -0,0 +1,46 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Matches if any entry in a dictionary has a value satisfying the nested matcher. + */ +@interface HCIsDictionaryContainingValue : HCBaseMatcher + +- (instancetype)initWithValueMatcher:(id )valueMatcher NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + + +FOUNDATION_EXPORT id HC_hasValue(id valueMatcher); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher for NSDictionaries that matches when the examined dictionary contains + * at least value that satisfies the specified matcher. + * @param valueMatcher The matcher to satisfy for the value, or an expected value for equalTo matching. + * @discussion This matcher works on any collection that has an -allValues method. + * + * Any argument that is not a matcher is implicitly wrapped in an equalTo matcher to check + * for equality. + * + * Examples
+ *
assertThat(myDictionary, hasValue(equalTo(\@"bar")))
+ *
assertThat(myDictionary, hasValue(\@"bar"))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_hasValue instead. + */ +static inline id hasValue(id valueMatcher) +{ + return HC_hasValue(valueMatcher); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContainingValue.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContainingValue.m new file mode 100644 index 0000000000..4cb88c3194 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsDictionaryContainingValue.m @@ -0,0 +1,46 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCIsDictionaryContainingValue.h" + +#import "HCRequireNonNilObject.h" +#import "HCWrapInMatcher.h" + + +@interface HCIsDictionaryContainingValue () +@property (nonatomic, strong, readonly) id valueMatcher; +@end + +@implementation HCIsDictionaryContainingValue + +- (instancetype)initWithValueMatcher:(id )valueMatcher +{ + self = [super init]; + if (self) + _valueMatcher = valueMatcher; + return self; +} + +- (BOOL)matches:(id)dict +{ + if ([dict respondsToSelector:@selector(allValues)]) + for (id oneValue in [dict allValues]) + if ([self.valueMatcher matches:oneValue]) + return YES; + return NO; +} + +- (void)describeTo:(id )description +{ + [[description appendText:@"a dictionary containing value "] + appendDescriptionOf:self.valueMatcher]; +} + +@end + + +id HC_hasValue(id valueMatcher) +{ + HCRequireNonNilObject(valueMatcher); + return [[HCIsDictionaryContainingValue alloc] initWithValueMatcher:HCWrapInMatcher(valueMatcher)]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsEmptyCollection.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsEmptyCollection.h new file mode 100644 index 0000000000..d2ac59703d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsEmptyCollection.h @@ -0,0 +1,39 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Matches empty collections. + */ +@interface HCIsEmptyCollection : HCHasCount + +- (instancetype)init; + +@end + + +FOUNDATION_EXPORT id HC_isEmpty(void); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches any examined object whose -count method + * returns zero. + * + * Example
+ *
assertThat(\@[], isEmpty())
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_isEmpty instead. + */ +static inline id isEmpty(void) +{ + return HC_isEmpty(); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsEmptyCollection.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsEmptyCollection.m new file mode 100644 index 0000000000..2e7ae6442b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsEmptyCollection.m @@ -0,0 +1,33 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCIsEmptyCollection.h" + +#import "HCIsEqual.h" + + +@implementation HCIsEmptyCollection + +- (instancetype)init +{ + self = [super initWithMatcher:HC_equalTo(@0)]; + return self; +} + +- (void)describeMismatchOf:(nullable id)item to:(nullable id )mismatchDescription +{ + [[mismatchDescription appendText:@"was "] appendDescriptionOf:item]; +} + +- (void)describeTo:(id )description +{ + [description appendText:@"empty collection"]; +} + +@end + + +FOUNDATION_EXPORT id HC_isEmpty(void) +{ + return [[HCIsEmptyCollection alloc] init]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsIn.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsIn.h new file mode 100644 index 0000000000..4816224880 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsIn.h @@ -0,0 +1,43 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Matches if examined object is contained within the nested collection. + */ +@interface HCIsIn : HCBaseMatcher + +- (instancetype)initWithCollection:(id)collection NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + + +FOUNDATION_EXPORT id HC_isIn(id aCollection); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is found within the specified + * collection. + * @param aCollection The collection to search. + * @discussion Invokes -containsObject: on aCollection to determine if the + * examined object is an element of the collection. + * + * Example
+ *
assertThat(\@"foo", isIn(\@@[\@"bar", \@"foo"]))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_isIn instead. + */ +static inline id isIn(id aCollection) +{ + return HC_isIn(aCollection); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsIn.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsIn.m new file mode 100644 index 0000000000..6cb12dafc5 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Collection/HCIsIn.m @@ -0,0 +1,45 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCIsIn.h" + + +@interface HCIsIn () +@property (nonatomic, strong, readonly) id collection; +@end + +@implementation HCIsIn + +- (instancetype)initWithCollection:(id)collection +{ + if (![collection respondsToSelector:@selector(containsObject:)]) + { + @throw [NSException exceptionWithName:@"NotAContainer" + reason:@"Object must respond to -containsObject:" + userInfo:nil]; + } + + self = [super init]; + if (self) + _collection = collection; + return self; +} + +- (BOOL)matches:(nullable id)item +{ + return [self.collection containsObject:item]; +} + +- (void)describeTo:(id )description +{ + [[description appendText:@"one of "] + appendList:self.collection start:@"{" separator:@", " end:@"}"]; +} + +@end + + +id HC_isIn(id aCollection) +{ + return [[HCIsIn alloc] initWithCollection:aCollection]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Decorator/HCDescribedAs.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Decorator/HCDescribedAs.h new file mode 100644 index 0000000000..a260e37c2b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Decorator/HCDescribedAs.h @@ -0,0 +1,41 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Provides a custom description to another matcher. + */ +@interface HCDescribedAs : HCBaseMatcher + +- (instancetype)initWithDescription:(NSString *)description + forMatcher:(id )matcher + overValues:(NSArray *)templateValues NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + + +FOUNDATION_EXPORT id HC_describedAs(NSString *description, id matcher, ...) NS_REQUIRES_NIL_TERMINATION; + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Wraps an existing matcher, overriding its description with that specified. All other + * functions are delegated to the decorated matcher, including its mismatch description. + * @param description The new description for the wrapped matcher. + * @param matcher The matcher to wrap, followed by a comma-separated list of substitution + * values ending with nil. + * @discussion The description may contain substitution placeholders %0, %1, etc. These will be + * replaced by any values that follow the matcher. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_describedAs instead. + */ +#define describedAs(description, matcher, ...) HC_describedAs(description, matcher, ##__VA_ARGS__) +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Decorator/HCDescribedAs.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Decorator/HCDescribedAs.m new file mode 100644 index 0000000000..389d09586e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Decorator/HCDescribedAs.m @@ -0,0 +1,121 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCDescribedAs.h" + + +@interface NSString (OCHamcrest) +@end + +@implementation NSString (OCHamcrest) + +// Parse decimal number (-1 if not found) and return remaining string. +- (NSString *)och_getDecimalNumber:(int *)number +{ + int decimal = 0; + BOOL readDigit = NO; + + NSUInteger length = self.length; + NSUInteger index; + for (index = 0; index < length; ++index) + { + unichar character = [self characterAtIndex:index]; + if (!isdigit(character)) + break; + decimal = decimal * 10 + character - '0'; + readDigit = YES; + } + + if (!readDigit) + { + *number = -1; + return self; + } + *number = decimal; + return [self substringFromIndex:index]; +} + +@end + + +@interface HCDescribedAs () +@property (nonatomic, copy, readonly) NSString *descriptionTemplate; +@property (nonatomic, strong, readonly) id matcher; +@property (nonatomic, copy, readonly) NSArray *values; +@end + +@implementation HCDescribedAs + +- (instancetype)initWithDescription:(NSString *)description + forMatcher:(id )matcher + overValues:(NSArray *)templateValues +{ + self = [super init]; + if (self) + { + _descriptionTemplate = [description copy]; + _matcher = matcher; + _values = [templateValues copy]; + } + return self; +} + +- (BOOL)matches:(nullable id)item +{ + return [self.matcher matches:item]; +} + +- (void)describeMismatchOf:(nullable id)item to:(nullable id )mismatchDescription +{ + [self.matcher describeMismatchOf:item to:mismatchDescription]; +} + +- (void)describeTo:(id )description +{ + NSArray *components = [self.descriptionTemplate componentsSeparatedByString:@"%"]; + BOOL firstComponent = YES; + for (NSString *component in components) + { + if (firstComponent) + { + firstComponent = NO; + [description appendText:component]; + } + else + { + [self appendTemplateForComponent:component toDescription:description]; + } + } +} + +- (void)appendTemplateForComponent:(NSString *)component toDescription:(id )description +{ + int index; + NSString *remainder = [component och_getDecimalNumber:&index]; + if (index < 0) + [[description appendText:@"%"] appendText:component]; + else + [[description appendDescriptionOf:self.values[(NSUInteger)index]] appendText:remainder]; +} + +@end + + +id HC_describedAs(NSString *description, id matcher, ...) +{ + NSMutableArray *valueList = [NSMutableArray array]; + + va_list args; + va_start(args, matcher); + id value = va_arg(args, id); + while (value != nil) + { + [valueList addObject:value]; + value = va_arg(args, id); + } + va_end(args); + + return [[HCDescribedAs alloc] initWithDescription:description + forMatcher:matcher + overValues:valueList]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Decorator/HCIs.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Decorator/HCIs.h new file mode 100644 index 0000000000..9ae975e115 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Decorator/HCIs.h @@ -0,0 +1,55 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Decorates another matcher. + */ +@interface HCIs : HCBaseMatcher + +- (instancetype)initWithMatcher:(id )matcher NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + + +FOUNDATION_EXPORT id HC_is(_Nullable id value); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Wraps an existing matcher, or provides a shortcut to the frequently + * used is(equalTo(x)). + * @param value The matcher to satisfy, or an expected value for equalTo matching. + * @discussion + * If valueis a matcher, its behavior is retained, but the test may be slightly more + * expressive. For example: + *
    + *
  • assertThat(\@(value), equalTo(\@5))
  • + *
  • assertThat(\@(value), is(equalTo(\@5)))
  • + *
+ * + * If valueis not a matcher, it is wrapped in an equalTo matcher. This makes the + * following statements equivalent: + *
    + *
  • assertThat(cheese, equalTo(smelly))
  • + *
  • assertThat(cheese, is(equalTo(smelly)))
  • + *
  • assertThat(cheese, is(smelly))
  • + *
+ * + * Choose the style that makes your expression most readable. This will vary depending on context. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_is instead. + */ +static inline id is(_Nullable id value) +{ + return HC_is(value); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Decorator/HCIs.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Decorator/HCIs.m new file mode 100644 index 0000000000..862c8bc64c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Decorator/HCIs.m @@ -0,0 +1,44 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCIs.h" + +#import "HCWrapInMatcher.h" + + +@interface HCIs () +@property (nonatomic, strong, readonly) id matcher; +@end + +@implementation HCIs + +- (instancetype)initWithMatcher:(id )matcher +{ + self = [super init]; + if (self) + _matcher = matcher; + return self; +} + +- (BOOL)matches:(nullable id)item +{ + return [self.matcher matches:item]; +} + +- (void)describeMismatchOf:(nullable id)item to:(nullable id )mismatchDescription +{ + [self.matcher describeMismatchOf:item to:mismatchDescription]; +} + +- (void)describeTo:(id )description +{ + [description appendDescriptionOf:self.matcher]; +} + +@end + + +id HC_is(_Nullable id value) +{ + return [[HCIs alloc] initWithMatcher:HCWrapInMatcher(value)]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Logical/HCAllOf.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Logical/HCAllOf.h new file mode 100644 index 0000000000..ee97791d04 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Logical/HCAllOf.h @@ -0,0 +1,64 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Calculates the logical conjunction of multiple matchers. + * @discussion Evaluation is shortcut, so subsequent matchers are not called if an earlier matcher + * returns NO. + */ +@interface HCAllOf : HCDiagnosingMatcher + +- (instancetype)initWithMatchers:(NSArray> *)matchers NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + + +FOUNDATION_EXPORT id HC_allOfIn(NSArray> *matchers); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object matches all of the + * specified matchers. + * @param matchers An array of matchers. Any element that is not a matcher is implicitly wrapped in + * an equalTo matcher to check for equality. + * @discussion + * Example
+ *
assertThat(\@"myValue", allOfIn(\@[startsWith(\@"my"), containsSubstring(\@"Val")]))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_allOfIn instead. + */ +static inline id allOfIn(NSArray *matchers) +{ + return HC_allOfIn(matchers); +} +#endif + + +FOUNDATION_EXPORT id HC_allOf(id matchers, ...) NS_REQUIRES_NIL_TERMINATION; + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object matches all of the + * specified matchers. + * @param matchers... A comma-separated list of matchers ending with nil. Any argument + * that is not a matcher is implicitly wrapped in an equalTo matcher to check for equality. + * @discussion + * Example
+ *
assertThat(\@"myValue", allOf(startsWith(\@"my"), containsSubstring(\@"Val"), nil))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_allOf instead. + */ +#define allOf(matchers...) HC_allOf(matchers) +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Logical/HCAllOf.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Logical/HCAllOf.m new file mode 100644 index 0000000000..eb42529d92 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Logical/HCAllOf.m @@ -0,0 +1,60 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCAllOf.h" + +#import "HCCollect.h" + + +@interface HCAllOf () +@property (nonatomic, copy, readonly) NSArray> *matchers; +@end + +@implementation HCAllOf + +- (instancetype)initWithMatchers:(NSArray> *)matchers +{ + self = [super init]; + if (self) + _matchers = [matchers copy]; + return self; +} + +- (BOOL)matches:(nullable id)item describingMismatchTo:(id )mismatchDescription +{ + for (id oneMatcher in self.matchers) + { + if (![oneMatcher matches:item]) + { + [[[mismatchDescription appendText:@"instead of "] + appendDescriptionOf:oneMatcher] + appendText:@", "]; + [oneMatcher describeMismatchOf:item to:mismatchDescription]; + return NO; + } + } + return YES; +} + +- (void)describeTo:(id )description +{ + [description appendList:self.matchers start:@"(" separator:@" and " end:@")"]; +} + +@end + + +id HC_allOfIn(NSArray *matchers) +{ + return [[HCAllOf alloc] initWithMatchers:HCWrapIntoMatchers(matchers)]; +} + +id HC_allOf(id matchers, ...) +{ + va_list args; + va_start(args, matchers); + NSArray *array = HCCollectItems(matchers, args); + va_end(args); + + return HC_allOfIn(array); +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Logical/HCAnyOf.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Logical/HCAnyOf.h new file mode 100644 index 0000000000..fdbf7529ca --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Logical/HCAnyOf.h @@ -0,0 +1,62 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Calculates the logical disjunction of multiple matchers. + * @discussion Evaluation is shortcut, so subsequent matchers are not called if an earlier matcher + * returns NO. + */ +@interface HCAnyOf : HCBaseMatcher + +- (instancetype)initWithMatchers:(NSArray> *)matchers NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + +FOUNDATION_EXPORT id HC_anyOfIn(NSArray *matchers); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object matches any of the + * specified matchers. + * @param matchers An array of matchers. Any element that is not a matcher is implicitly wrapped in + * an equalTo matcher to check for equality. + * @discussion + * Example
+ *
assertThat(\@"myValue", allOf(\@[startsWith(\@"foo"), containsSubstring(\@"Val")]))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_anyOf instead. + */ +static inline id anyOfIn(NSArray *matchers) +{ + return HC_anyOfIn(matchers); +} +#endif + +FOUNDATION_EXPORT id HC_anyOf(id matchers, ...) NS_REQUIRES_NIL_TERMINATION; + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object matches any of the + * specified matchers. + * @param matchers... A comma-separated list of matchers ending with nil. Any argument + * that is not a matcher is implicitly wrapped in an equalTo matcher to check for equality. + * @discussion + * Example
+ *
assertThat(\@"myValue", allOf(startsWith(\@"foo"), containsSubstring(\@"Val"), nil))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_anyOf instead. + */ +#define anyOf(matchers...) HC_anyOf(matchers) +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Logical/HCAnyOf.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Logical/HCAnyOf.m new file mode 100644 index 0000000000..330ffcf86c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Logical/HCAnyOf.m @@ -0,0 +1,52 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCAnyOf.h" + +#import "HCCollect.h" + + +@interface HCAnyOf () +@property (nonatomic, copy, readonly) NSArray> *matchers; +@end + +@implementation HCAnyOf + +- (instancetype)initWithMatchers:(NSArray> *)matchers +{ + self = [super init]; + if (self) + _matchers = [matchers copy]; + return self; +} + +- (BOOL)matches:(nullable id)item +{ + for (id oneMatcher in self.matchers) + if ([oneMatcher matches:item]) + return YES; + return NO; +} + +- (void)describeTo:(id )description +{ + [description appendList:self.matchers start:@"(" separator:@" or " end:@")"]; +} + +@end + + +id HC_anyOfIn(NSArray *matchers) +{ + return [[HCAnyOf alloc] initWithMatchers:HCWrapIntoMatchers(matchers)]; +} + +id HC_anyOf(id matchers, ...) +{ + va_list args; + va_start(args, matchers); + NSArray *array = HCCollectItems(matchers, args); + va_end(args); + + return HC_anyOfIn(array); +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Logical/HCIsAnything.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Logical/HCIsAnything.h new file mode 100644 index 0000000000..ae078e044b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Logical/HCIsAnything.h @@ -0,0 +1,55 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Matches anything. + */ +@interface HCIsAnything : HCBaseMatcher + +- (instancetype)init; +- (instancetype)initWithDescription:(NSString *)description NS_DESIGNATED_INITIALIZER; + +@end + + +FOUNDATION_EXPORT id HC_anything(void); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that always matches, regardless of the examined object. + * @discussion + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_anything instead. + */ +static inline id anything(void) +{ + return HC_anything(); +} +#endif + + +FOUNDATION_EXPORT id HC_anythingWithDescription(NSString *description); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches anything, regardless of the examined object, but + * describes itself with the specified NSString. + * @param description A meaningful string used to describe this matcher. + * @discussion + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_anything instead. + */ +static inline id anythingWithDescription(NSString *description) +{ + return HC_anythingWithDescription(description); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Logical/HCIsAnything.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Logical/HCIsAnything.m new file mode 100644 index 0000000000..b1c58ef2d8 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Logical/HCIsAnything.m @@ -0,0 +1,47 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCIsAnything.h" + + +@implementation HCIsAnything +{ + NSString *_description; +} + +- (instancetype)init +{ + self = [self initWithDescription:@"ANYTHING"]; + return self; +} + +- (instancetype)initWithDescription:(NSString *)description +{ + self = [super init]; + if (self) + _description = [description copy]; + return self; +} + +- (BOOL)matches:(nullable id)item +{ + return YES; +} + +- (void)describeTo:(id )aDescription +{ + [aDescription appendText:_description]; +} + +@end + + +id HC_anything() +{ + return [[HCIsAnything alloc] init]; +} + +id HC_anythingWithDescription(NSString *description) +{ + return [[HCIsAnything alloc] initWithDescription:description]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Logical/HCIsNot.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Logical/HCIsNot.h new file mode 100644 index 0000000000..deaf5f04d1 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Logical/HCIsNot.h @@ -0,0 +1,44 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Calculates the logical negation of a matcher. + */ +@interface HCIsNot : HCBaseMatcher + +- (instancetype)initWithMatcher:(id )matcher NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + + +FOUNDATION_EXPORT id HC_isNot(_Nullable id value); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that wraps an existing matcher, but inverts the logic by which it + * will match. + * @param value The matcher to negate, or an expected value to match for inequality. + * @discussion If value is not a matcher, it is implicitly wrapped in an equalTo + * matcher to check for equality, and thus matches for inequality. + * + * Examples
+ *
assertThat(cheese, isNot(equalTo(smelly)))
+ *
assertThat(cheese, isNot(smelly))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_isNot instead. + */ +static inline id isNot(_Nullable id value) +{ + return HC_isNot(value); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Logical/HCIsNot.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Logical/HCIsNot.m new file mode 100644 index 0000000000..01afa5cc61 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Logical/HCIsNot.m @@ -0,0 +1,43 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCIsNot.h" + +#import "HCWrapInMatcher.h" + + +@interface HCIsNot () +@property (nonatomic, strong, readonly) id matcher; +@end + +@implementation HCIsNot + +- (instancetype)initWithMatcher:(id )matcher +{ + self = [super init]; + if (self) + _matcher = matcher; + return self; +} + +- (BOOL)matches:(nullable id)item +{ + return ![self.matcher matches:item]; +} + +- (void)describeTo:(id )description +{ + [[description appendText:@"not "] appendDescriptionOf:self.matcher]; +} + +- (void)describeMismatchOf:(nullable id)item to:(nullable id )mismatchDescription +{ + [self.matcher describeMismatchOf:item to:mismatchDescription]; +} +@end + + +id HC_isNot(_Nullable id value) +{ + return [[HCIsNot alloc] initWithMatcher:HCWrapInMatcher(value)]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCIsCloseTo.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCIsCloseTo.h new file mode 100644 index 0000000000..0630c468e7 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCIsCloseTo.h @@ -0,0 +1,43 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Matchers numbers close to a value, within a delta range. + */ +@interface HCIsCloseTo : HCBaseMatcher + +- (instancetype)initWithValue:(double)value delta:(double)delta NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + + +FOUNDATION_EXPORT id HC_closeTo(double value, double delta); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher for NSNumbers that matches when the examined number is close to the + * specified value, within the specified delta. + * @param value The expected value of matching numbers. + * @param delta The delta within which matches will be allowed. + * @discussion Invokes -doubleValue on the examined number to get its value. + * + * Example
+ *
assertThat(\@1.03, closeTo(1.0, 0.03)
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_closeTo instead. + */ +static inline id closeTo(double value, double delta) +{ + return HC_closeTo(value, delta); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCIsCloseTo.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCIsCloseTo.m new file mode 100644 index 0000000000..319e2811dc --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCIsCloseTo.m @@ -0,0 +1,69 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCIsCloseTo.h" + + +@interface HCIsCloseTo () +@property (nonatomic, assign, readonly) double value; +@property (nonatomic, assign, readonly) double delta; +@end + +@implementation HCIsCloseTo + +- (id)initWithValue:(double)value delta:(double)delta +{ + self = [super init]; + if (self) + { + _value = value; + _delta = delta; + } + return self; +} + +- (BOOL)matches:(nullable id)item +{ + if ([self itemIsNotNumber:item]) + return NO; + + return [self actualDelta:item] <= self.delta; +} + +- (double)actualDelta:(id)item +{ + return fabs([item doubleValue] - self.value); +} + +- (BOOL)itemIsNotNumber:(id)item +{ + return ![item isKindOfClass:[NSNumber class]]; +} + +- (void)describeMismatchOf:(nullable id)item to:(nullable id )mismatchDescription +{ + if ([self itemIsNotNumber:item]) + [super describeMismatchOf:item to:mismatchDescription]; + else + { + [[[mismatchDescription appendDescriptionOf:item] + appendText:@" differed by "] + appendDescriptionOf:@([self actualDelta:item])]; + } +} + +- (void)describeTo:(id )description +{ + [[[[description appendText:@"a numeric value within "] + appendDescriptionOf:@(self.delta)] + appendText:@" of "] + appendDescriptionOf:@(self.value)]; +} + +@end + + +id HC_closeTo(double value, double delta) +{ + return [[HCIsCloseTo alloc] initWithValue:value delta:delta]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCIsEqualToNumber.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCIsEqualToNumber.h new file mode 100644 index 0000000000..c67363b777 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCIsEqualToNumber.h @@ -0,0 +1,289 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +FOUNDATION_EXPORT id HC_equalToChar(char value); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is equal to an NSNumber created + * from the specified char value. + * @param value The char value from which to create an NSNumber. + * @discussion Consider using equalTo(\@(value)) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_equalToChar instead. + */ +static inline id equalToChar(char value) +{ + return HC_equalToChar(value); +} +#endif + + +FOUNDATION_EXPORT id HC_equalToDouble(double value); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is equal to an NSNumber created + * from the specified double value. + * @param value The double value from which to create an NSNumber. + * @discussion Consider using equalTo(\@(value)) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_equalToDouble instead. + */ +static inline id equalToDouble(double value) +{ + return HC_equalToDouble(value); +} +#endif + + +FOUNDATION_EXPORT id HC_equalToFloat(float value); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is equal to an NSNumber created + * from the specified float value. + * @param value The float value from which to create an NSNumber. + * @discussion Consider using equalTo(\@(value)) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_equalToFloat instead. + */ +static inline id equalToFloat(float value) +{ + return HC_equalToFloat(value); +} +#endif + + +FOUNDATION_EXPORT id HC_equalToInt(int value); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is equal to an NSNumber created + * from the specified int value. + * @param value The int value from which to create an NSNumber. + * @discussion Consider using equalTo(\@(value)) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_equalToInt instead. + */ +static inline id equalToInt(int value) +{ + return HC_equalToInt(value); +} +#endif + + +FOUNDATION_EXPORT id HC_equalToLong(long value); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is equal to an NSNumber created + * from the specified long value. + * @param value The long value from which to create an NSNumber. + * @discussion Consider using equalTo(\@(value)) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_equalToLong instead. + */ +static inline id equalToLong(long value) +{ + return HC_equalToLong(value); +} +#endif + + +FOUNDATION_EXPORT id HC_equalToLongLong(long long value); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is equal to an NSNumber created + * from the specified long long value. + * @param value The long long value from which to create an NSNumber. + * @discussion Consider using equalTo(\@(value)) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_equalToLongLong instead. + */ +static inline id equalToLongLong(long long value) +{ + return HC_equalToLongLong(value); +} +#endif + + +FOUNDATION_EXPORT id HC_equalToShort(short value); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is equal to an NSNumber created + * from the specified short value. + * @param value The short value from which to create an NSNumber. + * @discussion Consider using equalTo(\@(value)) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_equalToShort instead. + */ +static inline id equalToShort(short value) +{ + return HC_equalToShort(value); +} +#endif + + +FOUNDATION_EXPORT id HC_equalToUnsignedChar(unsigned char value); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract equalToUnsignedChar(value) - + * Creates a matcher that matches when the examined object is equal to an NSNumber created from the + * specified unsigned char value. + * @param value The unsigned char value from which to create an NSNumber. + * @discussion Consider using equalTo(\@(value)) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_equalToUnsignedChar instead. + */ +static inline id equalToUnsignedChar(unsigned char value) +{ + return HC_equalToUnsignedChar(value); +} +#endif + + +FOUNDATION_EXPORT id HC_equalToUnsignedInt(unsigned int value); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is equal to an NSNumber created + * from the specified unsigned int value. + * @param value The unsigned int value from which to create an NSNumber. + * @discussion Consider using equalTo(\@(value)) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_equalToUnsignedInt instead. + */ +static inline id equalToUnsignedInt(unsigned int value) +{ + return HC_equalToUnsignedInt(value); +} +#endif + + +FOUNDATION_EXPORT id HC_equalToUnsignedLong(unsigned long value); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is equal to an NSNumber created + * from the specified unsigned long value. + * @param value The unsigned long value from which to create an NSNumber. + * @discussion Consider using equalTo(\@(value)) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_equalToUnsignedLong instead. + */ +static inline id equalToUnsignedLong(unsigned long value) +{ + return HC_equalToUnsignedLong(value); +} +#endif + + +FOUNDATION_EXPORT id HC_equalToUnsignedLongLong(unsigned long long value); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is equal to an NSNumber created + * from the specified unsigned long long value. + * @param value The unsigned long long value from which to create an NSNumber. + * @discussion Consider using equalTo(\@(value)) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_equalToUnsignedLongLong instead. + */ +static inline id equalToUnsignedLongLong(unsigned long long value) +{ + return HC_equalToUnsignedLongLong(value); +} +#endif + + +FOUNDATION_EXPORT id HC_equalToUnsignedShort(unsigned short value); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is equal to an NSNumber created + * from the specified unsigned short value. + * @param value The unsigned short value from which to create an NSNumber. + * @discussion Consider using equalTo(\@(value)) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_equalToUnsignedShort instead. + */ +static inline id equalToUnsignedShort(unsigned short value) +{ + return HC_equalToUnsignedShort(value); +} +#endif + + +FOUNDATION_EXPORT id HC_equalToInteger(NSInteger value); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is equal to an NSNumber created + * from the specified NSInteger value. + * @param value The NSInteger value from which to create an NSNumber. + * @discussion Consider using equalTo(\@(value)) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_equalToInteger instead. + */ +static inline id equalToInteger(NSInteger value) +{ + return HC_equalToInteger(value); +} +#endif + + +FOUNDATION_EXPORT id HC_equalToUnsignedInteger(NSUInteger value); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is equal to an NSNumber created + * from the specified NSUInteger value. + * @param value The NSUInteger value from which to create an NSNumber. + * @discussion Consider using equalTo(\@(value)) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_equalToUnsignedInteger instead. + */ +static inline id equalToUnsignedInteger(NSUInteger value) +{ + return HC_equalToUnsignedInteger(value); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCIsEqualToNumber.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCIsEqualToNumber.m new file mode 100644 index 0000000000..4ed2c5c445 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCIsEqualToNumber.m @@ -0,0 +1,77 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCIsEqualToNumber.h" + +#import "HCIsEqual.h" + + +FOUNDATION_EXPORT id HC_equalToChar(char value) +{ + return HC_equalTo(@(value)); +} + +FOUNDATION_EXPORT id HC_equalToDouble(double value) +{ + return HC_equalTo(@(value)); +} + +FOUNDATION_EXPORT id HC_equalToFloat(float value) +{ + return HC_equalTo(@(value)); +} + +FOUNDATION_EXPORT id HC_equalToInt(int value) +{ + return HC_equalTo(@(value)); +} + +FOUNDATION_EXPORT id HC_equalToLong(long value) +{ + return HC_equalTo(@(value)); +} + +FOUNDATION_EXPORT id HC_equalToLongLong(long long value) +{ + return HC_equalTo(@(value)); +} + +FOUNDATION_EXPORT id HC_equalToShort(short value) +{ + return HC_equalTo(@(value)); +} + +FOUNDATION_EXPORT id HC_equalToUnsignedChar(unsigned char value) +{ + return HC_equalTo(@(value)); +} + +FOUNDATION_EXPORT id HC_equalToUnsignedInt(unsigned int value) +{ + return HC_equalTo(@(value)); +} + +FOUNDATION_EXPORT id HC_equalToUnsignedLong(unsigned long value) +{ + return HC_equalTo(@(value)); +} + +FOUNDATION_EXPORT id HC_equalToUnsignedLongLong(unsigned long long value) +{ + return HC_equalTo(@(value)); +} + +FOUNDATION_EXPORT id HC_equalToUnsignedShort(unsigned short value) +{ + return HC_equalTo(@(value)); +} + +FOUNDATION_EXPORT id HC_equalToInteger(NSInteger value) +{ + return HC_equalTo(@(value)); +} + +FOUNDATION_EXPORT id HC_equalToUnsignedInteger(NSUInteger value) +{ + return HC_equalTo(@(value)); +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCIsTrueFalse.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCIsTrueFalse.h new file mode 100644 index 0000000000..78e7d472ce --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCIsTrueFalse.h @@ -0,0 +1,55 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Matches true values. + */ +@interface HCIsTrue : HCBaseMatcher +@end + +/*! + * @abstract Matches false values. + */ +@interface HCIsFalse : HCBaseMatcher +@end + + +FOUNDATION_EXPORT id HC_isTrue(void); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is an non-zero NSNumber. + * @discussion + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_isTrue instead. + */ +static inline id isTrue(void) +{ + return HC_isTrue(); +} +#endif + + +FOUNDATION_EXPORT id HC_isFalse(void); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is NSNumber zero. + * @discussion + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_isFalse instead. +*/ +static inline id isFalse(void) +{ + return HC_isFalse(); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCIsTrueFalse.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCIsTrueFalse.m new file mode 100644 index 0000000000..d095b94da2 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCIsTrueFalse.m @@ -0,0 +1,55 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCIsTrueFalse.h" + + +@implementation HCIsTrue + +- (BOOL)matches:(nullable id)item +{ + if (![item isKindOfClass:[NSNumber class]]) + return NO; + + return [item boolValue]; +} + +- (void)describeTo:(id )description +{ + [description appendText:@"true (non-zero)"]; +} + +@end + + +FOUNDATION_EXPORT id HC_isTrue(void) +{ + return [[HCIsTrue alloc] init]; +} + + +#pragma mark - + +@implementation HCIsFalse + +- (BOOL)matches:(nullable id)item +{ + if (![item isKindOfClass:[NSNumber class]]) + return NO; + + return ![item boolValue]; +} + +- (void)describeTo:(id )description +{ + [description appendText:@"false (zero)"]; +} + +@end + + +FOUNDATION_EXPORT id HC_isFalse(void) +{ + return [[HCIsFalse alloc] init]; +} + diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCNumberAssert.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCNumberAssert.h new file mode 100644 index 0000000000..7ea0808943 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCNumberAssert.h @@ -0,0 +1,340 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +@protocol HCMatcher; + + +NS_ASSUME_NONNULL_BEGIN + +FOUNDATION_EXPORT void HC_assertThatBoolWithLocation(id testCase, BOOL actual, + id matcher, char const *fileName, int lineNumber); + +#define HC_assertThatBool(actual, matcher) \ + HC_assertThatBoolWithLocation(self, actual, matcher, __FILE__, __LINE__) + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract assertThatBool(actual, matcher) - + * Asserts that BOOL actual value, converted to an NSNumber, satisfies matcher. + * @param actual The BOOL value to convert to an NSNumber for evaluation. + * @param matcher The matcher to satisfy as the expected condition. + * @discussion Consider using assertThat(\@(actual), matcher) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_assertThatBool instead. + */ +#define assertThatBool(actual, matcher) HC_assertThatBool(actual, matcher) +#endif + + +FOUNDATION_EXPORT void HC_assertThatCharWithLocation(id testCase, char actual, + id matcher, char const *fileName, int lineNumber); + +#define HC_assertThatChar(actual, matcher) \ + HC_assertThatCharWithLocation(self, actual, matcher, __FILE__, __LINE__) + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract assertThatChar(actual, matcher) - + * Asserts that char actual value, converted to an NSNumber, satisfies matcher. + * @param actual The char value to convert to an NSNumber for evaluation. + * @param matcher The matcher to satisfy as the expected condition. + * @discussion Consider using assertThat(\@(actual), matcher) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_assertThatChar instead. + */ +#define assertThatChar(actual, matcher) HC_assertThatChar(actual, matcher) +#endif + + +FOUNDATION_EXPORT void HC_assertThatDoubleWithLocation(id testCase, double actual, + id matcher, char const *fileName, int lineNumber); + +#define HC_assertThatDouble(actual, matcher) \ + HC_assertThatDoubleWithLocation(self, actual, matcher, __FILE__, __LINE__) + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract HC_assertThatDouble(actual, matcher) - + * Asserts that double actual value, converted to an NSNumber, satisfies matcher. + * @param actual The double value to convert to an NSNumber for evaluation. + * @param matcher The matcher to satisfy as the expected condition. + * @discussion Consider using assertThat(\@(actual), matcher) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_assertThatDouble instead. + */ +#define assertThatDouble(actual, matcher) HC_assertThatDouble(actual, matcher) +#endif + + +FOUNDATION_EXPORT void HC_assertThatFloatWithLocation(id testCase, float actual, + id matcher, char const *fileName, int lineNumber); + +#define HC_assertThatFloat(actual, matcher) \ + HC_assertThatFloatWithLocation(self, actual, matcher, __FILE__, __LINE__) + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract assertThatFloat(actual, matcher) - + * Asserts that float actual value, converted to an NSNumber, satisfies matcher. + * @param actual The float value to convert to an NSNumber for evaluation. + * @param matcher The matcher to satisfy as the expected condition. + * @discussion Consider using assertThat(\@(actual), matcher) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_assertThatFloat instead. + */ +#define assertThatFloat(actual, matcher) HC_assertThatFloat(actual, matcher) +#endif + + +FOUNDATION_EXPORT void HC_assertThatIntWithLocation(id testCase, int actual, + id matcher, char const *fileName, int lineNumber); + +#define HC_assertThatInt(actual, matcher) \ + HC_assertThatIntWithLocation(self, actual, matcher, __FILE__, __LINE__) + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract assertThatInt(actual, matcher) - + * Asserts that int actual value, converted to an NSNumber, satisfies matcher. + * @param actual The int value to convert to an NSNumber for evaluation. + * @param matcher The matcher to satisfy as the expected condition. + * @discussion Consider using assertThat(\@(actual), matcher) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_assertThatInt instead. + */ +#define assertThatInt(actual, matcher) HC_assertThatInt(actual, matcher) +#endif + + +FOUNDATION_EXPORT void HC_assertThatLongWithLocation(id testCase, long actual, + id matcher, char const *fileName, int lineNumber); + +#define HC_assertThatLong(actual, matcher) \ + HC_assertThatLongWithLocation(self, actual, matcher, __FILE__, __LINE__) + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract assertThatLong(actual, matcher) - + * Asserts that long actual value, converted to an NSNumber, satisfies matcher. + * @param actual The long value to convert to an NSNumber for evaluation. + * @param matcher The matcher to satisfy as the expected condition. + * @discussion Consider using assertThat(\@(actual), matcher) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_assertThatLong instead. + */ +#define assertThatLong(actual, matcher) HC_assertThatLong(actual, matcher) +#endif + + +FOUNDATION_EXPORT void HC_assertThatLongLongWithLocation(id testCase, long long actual, + id matcher, char const *fileName, int lineNumber); + +#define HC_assertThatLongLong(actual, matcher) \ + HC_assertThatLongLongWithLocation(self, actual, matcher, __FILE__, __LINE__) + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract assertThatLongLong(actual, matcher) - + * Asserts that long long actual value, converted to an NSNumber, satisfies matcher. + * @param actual The long long value to convert to an NSNumber for evaluation. + * @param matcher The matcher to satisfy as the expected condition. + * @discussion Consider using assertThat(\@(actual), matcher) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_assertThatLongLong instead. + */ +#define assertThatLongLong(actual, matcher) HC_assertThatLongLong(actual, matcher) +#endif + + +FOUNDATION_EXPORT void HC_assertThatShortWithLocation(id testCase, short actual, + id matcher, char const *fileName, int lineNumber); + +#define HC_assertThatShort(actual, matcher) \ + HC_assertThatShortWithLocation(self, actual, matcher, __FILE__, __LINE__) + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract assertThatShort(actual, matcher) - + * Asserts that short actual value, converted to an NSNumber, satisfies matcher. + * @param actual The short value to convert to an NSNumber for evaluation. + * @param matcher The matcher to satisfy as the expected condition. + * @discussion Consider using assertThat(\@(actual), matcher) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_assertThatShort instead. + */ +#define assertThatShort(actual, matcher) HC_assertThatShort(actual, matcher) +#endif + + +FOUNDATION_EXPORT void HC_assertThatUnsignedCharWithLocation(id testCase, unsigned char actual, + id matcher, char const *fileName, int lineNumber); + +#define HC_assertThatUnsignedChar(actual, matcher) \ + HC_assertThatUnsignedCharWithLocation(self, actual, matcher, __FILE__, __LINE__) + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract assertThatUnsignedChar(actual, matcher) - + * Asserts that unsigned char actual value, converted to an NSNumber, satisfies matcher. + * @param actual The unsigned char value to convert to an NSNumber for evaluation. + * @param matcher The matcher to satisfy as the expected condition. + * @discussion Consider using assertThat(\@(actual), matcher) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_assertThatUnsignedChar instead. + */ +#define assertThatUnsignedChar(actual, matcher) HC_assertThatUnsignedChar(actual, matcher) +#endif + + +FOUNDATION_EXPORT void HC_assertThatUnsignedIntWithLocation(id testCase, unsigned int actual, + id matcher, char const *fileName, int lineNumber); + +#define HC_assertThatUnsignedInt(actual, matcher) \ + HC_assertThatUnsignedIntWithLocation(self, actual, matcher, __FILE__, __LINE__) + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract assertThatUnsignedInt(actual, matcher) - + * Asserts that unsigned int actual value, converted to an NSNumber, satisfies matcher. + * @param actual The unsigned int value to convert to an NSNumber for evaluation. + * @param matcher The matcher to satisfy as the expected condition. + * @discussion Consider using assertThat(\@(actual), matcher) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_assertThatUnsignedInt instead. + */ +#define assertThatUnsignedInt(actual, matcher) HC_assertThatUnsignedInt(actual, matcher) +#endif + + +FOUNDATION_EXPORT void HC_assertThatUnsignedLongWithLocation(id testCase, unsigned long actual, + id matcher, char const *fileName, int lineNumber); + +#define HC_assertThatUnsignedLong(actual, matcher) \ + HC_assertThatUnsignedLongWithLocation(self, actual, matcher, __FILE__, __LINE__) + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract assertThatUnsignedLong(actual, matcher) - + * Asserts that unsigned long actual value, converted to an NSNumber, satisfies matcher. + * @param actual The unsigned long value to convert to an NSNumber for evaluation. + * @param matcher The matcher to satisfy as the expected condition. + * @discussion Consider using assertThat(\@(actual), matcher) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_assertThatUnsignedLong instead. + */ +#define assertThatUnsignedLong(actual, matcher) HC_assertThatUnsignedLong(actual, matcher) +#endif + + +FOUNDATION_EXPORT void HC_assertThatUnsignedLongLongWithLocation(id testCase, unsigned long long actual, + id matcher, char const *fileName, int lineNumber); + +#define HC_assertThatUnsignedLongLong(actual, matcher) \ + HC_assertThatUnsignedLongLongWithLocation(self, actual, matcher, __FILE__, __LINE__) + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract assertThatUnsignedLongLong(actual, matcher) - + * Asserts that unsigned long long actual value, converted to an NSNumber, satisfies matcher. + * @param actual The unsigned long long value to convert to an NSNumber for evaluation. + * @param matcher The matcher to satisfy as the expected condition. + * @discussion Consider using assertThat(\@(actual), matcher) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_assertThatUnsignedLongLong instead. + */ +#define assertThatUnsignedLongLong(actual, matcher) HC_assertThatUnsignedLongLong(actual, matcher) +#endif + + +FOUNDATION_EXPORT void HC_assertThatUnsignedShortWithLocation(id testCase, unsigned short actual, + id matcher, char const *fileName, int lineNumber); + +#define HC_assertThatUnsignedShort(actual, matcher) \ + HC_assertThatUnsignedShortWithLocation(self, actual, matcher, __FILE__, __LINE__) + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract assertThatUnsignedShort(actual, matcher) - + * Asserts that unsigned short actual value, converted to an NSNumber, satisfies matcher. + * @param actual The unsigned short value to convert to an NSNumber for evaluation. + * @param matcher The matcher to satisfy as the expected condition. + * @discussion Consider using assertThat(\@(actual), matcher) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_assertThatUnsignedShort instead. + */ +#define assertThatUnsignedShort(actual, matcher) HC_assertThatUnsignedShort(actual, matcher) +#endif + + +FOUNDATION_EXPORT void HC_assertThatIntegerWithLocation(id testCase, NSInteger actual, + id matcher, char const *fileName, int lineNumber); + +#define HC_assertThatInteger(actual, matcher) \ + HC_assertThatIntegerWithLocation(self, actual, matcher, __FILE__, __LINE__) + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract assertThatInteger(actual, matcher) - + * Asserts that NSInteger actual value, converted to an NSNumber, satisfies matcher. + * @param actual The NSInteger value to convert to an NSNumber for evaluation. + * @param matcher The matcher to satisfy as the expected condition. + * @discussion Consider using assertThat(\@(actual), matcher) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_assertThatInteger instead. + */ +#define assertThatInteger(actual, matcher) HC_assertThatInteger(actual, matcher) +#endif + + +FOUNDATION_EXPORT void HC_assertThatUnsignedIntegerWithLocation(id testCase, NSUInteger actual, + id matcher, char const *fileName, int lineNumber); + +#define HC_assertThatUnsignedInteger(actual, matcher) \ + HC_assertThatUnsignedIntegerWithLocation(self, actual, matcher, __FILE__, __LINE__) + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract assertThatUnsignedInteger(actual, matcher) - + * Asserts that NSUInteger actual value, converted to an NSNumber, satisfies matcher. + * @param actual The NSUInteger value to convert to an NSNumber for evaluation. + * @param matcher The matcher to satisfy as the expected condition. + * @discussion Consider using assertThat(\@(actual), matcher) instead. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_assertThatUnsignedInteger instead. + */ +#define assertThatUnsignedInteger(actual, matcher) HC_assertThatUnsignedInteger(actual, matcher) +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCNumberAssert.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCNumberAssert.m new file mode 100644 index 0000000000..9c861ebb1d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCNumberAssert.m @@ -0,0 +1,97 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCNumberAssert.h" + +#import "HCAssertThat.h" + + +FOUNDATION_EXPORT void HC_assertThatBoolWithLocation(id testCase, BOOL actual, + id matcher, char const * fileName, int lineNumber) +{ + HC_assertThatWithLocation(testCase, @(actual), matcher, fileName, lineNumber); +} + +FOUNDATION_EXPORT void HC_assertThatCharWithLocation(id testCase, char actual, + id matcher, char const * fileName, int lineNumber) +{ + HC_assertThatWithLocation(testCase, @(actual), matcher, fileName, lineNumber); +} + +FOUNDATION_EXPORT void HC_assertThatDoubleWithLocation(id testCase, double actual, + id matcher, char const * fileName, int lineNumber) +{ + HC_assertThatWithLocation(testCase, @(actual), matcher, fileName, lineNumber); +} + +FOUNDATION_EXPORT void HC_assertThatFloatWithLocation(id testCase, float actual, + id matcher, char const * fileName, int lineNumber) +{ + HC_assertThatWithLocation(testCase, @(actual), matcher, fileName, lineNumber); +} + +FOUNDATION_EXPORT void HC_assertThatIntWithLocation(id testCase, int actual, + id matcher, char const * fileName, int lineNumber) +{ + HC_assertThatWithLocation(testCase, @(actual), matcher, fileName, lineNumber); +} + +FOUNDATION_EXPORT void HC_assertThatLongWithLocation(id testCase, long actual, + id matcher, char const * fileName, int lineNumber) +{ + HC_assertThatWithLocation(testCase, @(actual), matcher, fileName, lineNumber); +} + +FOUNDATION_EXPORT void HC_assertThatLongLongWithLocation(id testCase, long long actual, + id matcher, char const * fileName, int lineNumber) +{ + HC_assertThatWithLocation(testCase, @(actual), matcher, fileName, lineNumber); +} + +FOUNDATION_EXPORT void HC_assertThatShortWithLocation(id testCase, short actual, + id matcher, char const * fileName, int lineNumber) +{ + HC_assertThatWithLocation(testCase, @(actual), matcher, fileName, lineNumber); +} + +FOUNDATION_EXPORT void HC_assertThatUnsignedCharWithLocation(id testCase, unsigned char actual, + id matcher, char const * fileName, int lineNumber) +{ + HC_assertThatWithLocation(testCase, @(actual), matcher, fileName, lineNumber); +} + +FOUNDATION_EXPORT void HC_assertThatUnsignedIntWithLocation(id testCase, unsigned int actual, + id matcher, char const * fileName, int lineNumber) +{ + HC_assertThatWithLocation(testCase, @(actual), matcher, fileName, lineNumber); +} + +FOUNDATION_EXPORT void HC_assertThatUnsignedLongWithLocation(id testCase, unsigned long actual, + id matcher, char const * fileName, int lineNumber) +{ + HC_assertThatWithLocation(testCase, @(actual), matcher, fileName, lineNumber); +} + +FOUNDATION_EXPORT void HC_assertThatUnsignedLongLongWithLocation(id testCase, unsigned long long actual, + id matcher, char const * fileName, int lineNumber) +{ + HC_assertThatWithLocation(testCase, @(actual), matcher, fileName, lineNumber); +} + +FOUNDATION_EXPORT void HC_assertThatUnsignedShortWithLocation(id testCase, unsigned short actual, + id matcher, char const * fileName, int lineNumber) +{ + HC_assertThatWithLocation(testCase, @(actual), matcher, fileName, lineNumber); +} + +FOUNDATION_EXPORT void HC_assertThatIntegerWithLocation(id testCase, NSInteger actual, + id matcher, char const * fileName, int lineNumber) +{ + HC_assertThatWithLocation(testCase, @(actual), matcher, fileName, lineNumber); +} + +FOUNDATION_EXPORT void HC_assertThatUnsignedIntegerWithLocation(id testCase, NSUInteger actual, + id matcher, char const * fileName, int lineNumber) +{ + HC_assertThatWithLocation(testCase, @(actual), matcher, fileName, lineNumber); +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCOrderingComparison.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCOrderingComparison.h new file mode 100644 index 0000000000..372679505d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCOrderingComparison.h @@ -0,0 +1,114 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Matches values with -compare:. + */ +@interface HCOrderingComparison : HCBaseMatcher + +- (instancetype)initComparing:(id)expectedValue + minCompare:(NSComparisonResult)min + maxCompare:(NSComparisonResult)max + comparisonDescription:(NSString *)comparisonDescription NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + + +FOUNDATION_EXPORT id HC_greaterThan(id value); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is greater than the specified + * value, as reported by the -compare: method of the examined object. + * @param value The value which, when passed to the -compare: method of the examined + * object, should return NSOrderedAscending. + * @discussion + * Example
+ *
assertThat(\@2, greaterThan(\@1))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_greaterThan instead. + */ +static inline id greaterThan(id value) +{ + return HC_greaterThan(value); +} +#endif + + +FOUNDATION_EXPORT id HC_greaterThanOrEqualTo(id value); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is greater than or equal to the + * specified value, as reported by the -compare: method of the examined object. + * @param value The value which, when passed to the -compare: method of the examined + * object, should return NSOrderedAscending or NSOrderedSame. + * @discussion + * Example
+ *
assertThat(\@1, greaterThanOrEqualTo(\@1))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_greaterThanOrEqualTo instead. + */ +static inline id greaterThanOrEqualTo(id value) +{ + return HC_greaterThanOrEqualTo(value); +} +#endif + + +FOUNDATION_EXPORT id HC_lessThan(id value); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is less than the specified + * value, as reported by the -compare: method of the examined object. + * @param value The value which, when passed to the -compare: method of the examined + * object, should return NSOrderedDescending. + * @discussion + * Example
+ *
assertThat(\@1, lessThan(\@2))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_lessThan instead. + */ +static inline id lessThan(id value) +{ + return HC_lessThan(value); +} +#endif + + +FOUNDATION_EXPORT id HC_lessThanOrEqualTo(id value); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is less than or equal to the + * specified value, as reported by the -compare: method of the examined object. + * @param value The value which, when passed to the -compare: method of the examined + * object, should return NSOrderedDescending or NSOrderedSame. + * @discussion + * Example
+ *
assertThat(\@1, lessThanOrEqualTo(\@1))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_lessThanOrEqualTo instead. + */ +static inline id lessThanOrEqualTo(id value) +{ + return HC_lessThanOrEqualTo(value); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCOrderingComparison.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCOrderingComparison.m new file mode 100644 index 0000000000..237953f0a6 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Number/HCOrderingComparison.m @@ -0,0 +1,97 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCOrderingComparison.h" + + +@interface HCOrderingComparison () +@property (nonatomic, strong, readonly) id expected; +@property (nonatomic, assign, readonly) NSComparisonResult minCompare; +@property (nonatomic, assign, readonly) NSComparisonResult maxCompare; +@property (nonatomic, copy, readonly) NSString *comparisonDescription; +@end + +@implementation HCOrderingComparison + +- (instancetype)initComparing:(id)expectedValue + minCompare:(NSComparisonResult)min + maxCompare:(NSComparisonResult)max + comparisonDescription:(NSString *)description +{ + if (![expectedValue respondsToSelector:@selector(compare:)]) + { + @throw [NSException exceptionWithName: @"UncomparableObject" + reason: @"Object must respond to compare:" + userInfo: nil]; + } + + self = [super init]; + if (self) + { + _expected = expectedValue; + _minCompare = min; + _maxCompare = max; + _comparisonDescription = [description copy]; + } + return self; +} + +- (BOOL)matches:(nullable id)item +{ + if (item == nil) + return NO; + + NSComparisonResult compare; + @try + { + compare = [self.expected compare:item]; + } + @catch (NSException *e) + { + return NO; + } + return self.minCompare <= compare && compare <= self.maxCompare; +} + +- (void)describeTo:(id )description +{ + [[[[description appendText:@"a value "] + appendText:self.comparisonDescription] + appendText:@" "] + appendDescriptionOf:self.expected]; +} + +@end + + +id HC_greaterThan(id value) +{ + return [[HCOrderingComparison alloc] initComparing:value + minCompare:NSOrderedAscending + maxCompare:NSOrderedAscending + comparisonDescription:@"greater than"]; +} + +id HC_greaterThanOrEqualTo(id value) +{ + return [[HCOrderingComparison alloc] initComparing:value + minCompare:NSOrderedAscending + maxCompare:NSOrderedSame + comparisonDescription:@"greater than or equal to"]; +} + +id HC_lessThan(id value) +{ + return [[HCOrderingComparison alloc] initComparing:value + minCompare:NSOrderedDescending + maxCompare:NSOrderedDescending + comparisonDescription:@"less than"]; +} + +id HC_lessThanOrEqualTo(id value) +{ + return [[HCOrderingComparison alloc] initComparing:value + minCompare:NSOrderedSame + maxCompare:NSOrderedDescending + comparisonDescription:@"less than or equal to"]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCArgumentCaptor.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCArgumentCaptor.h new file mode 100644 index 0000000000..f9e3011a3b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCArgumentCaptor.h @@ -0,0 +1,44 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Matches anything, capturing all values. + * @discussion This matcher captures all values it was given to match, and always evaluates to + * YES. Use it to capture argument values for further assertions. + * + * Unlike other matchers, this matcher is not idempotent. It should be created outside of any + * expression so that it can be queried for the items it captured. + */ +@interface HCArgumentCaptor : HCIsAnything + +/*! + * @abstract Returns the captured value. + * @discussion If -matches: was called more than once then this property returns the + * last captured value. + * + * If -matches: was never invoked and so no value was captured, this property returns + * nil. But if nil was captured, this property returns NSNull. + */ +@property (nullable, nonatomic, readonly) id value; + +/*! + * @abstract Returns all captured values. + * @discussion Returns an array containing all captured values, in the order in which they were + * captured. nil values are converted to NSNull. + */ +@property (nonatomic, readonly) NSArray *allValues; + +/*! + * @abstract Determines whether subsequent matched values are captured. + * @discussion YES by default. + */ +@property (nonatomic, assign) BOOL captureEnabled; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCArgumentCaptor.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCArgumentCaptor.m new file mode 100644 index 0000000000..72ead750e0 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCArgumentCaptor.m @@ -0,0 +1,56 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCArgumentCaptor.h" + + +@interface HCArgumentCaptor () +@property (nonatomic, strong, readonly) NSMutableArray *values; +@end + +@implementation HCArgumentCaptor + +@dynamic allValues; +@dynamic value; + +- (instancetype)init +{ + self = [super initWithDescription:@""]; + if (self) + { + _values = [[NSMutableArray alloc] init]; + _captureEnabled = YES; + } + return self; +} + +- (BOOL)matches:(nullable id)item +{ + [self capture:item]; + return [super matches:item]; +} + +- (void)capture:(id)item +{ + if (self.captureEnabled) + { + id value = item ?: [NSNull null]; + if ([value conformsToProtocol:@protocol(NSCopying)]) + value = [value copy]; + [self.values addObject:value]; + } +} + +- (id)value +{ + if (!self.values.count) + return nil; + return self.values.lastObject; +} + +- (NSArray *)allValues +{ + return [self.values copy]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCClassMatcher.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCClassMatcher.h new file mode 100644 index 0000000000..6702c65e51 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCClassMatcher.h @@ -0,0 +1,18 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +@interface HCClassMatcher : HCBaseMatcher + +@property (nonatomic, strong, readonly) Class theClass; + +- (instancetype)initWithClass:(Class)aClass NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCClassMatcher.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCClassMatcher.m new file mode 100644 index 0000000000..66e7cb6798 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCClassMatcher.m @@ -0,0 +1,43 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCClassMatcher.h" + +#import "HCRequireNonNilObject.h" + + +@interface HCClassMatcher (SubclassResponsibility) +- (NSString *)expectation; +@end + + +@implementation HCClassMatcher + +- (instancetype)initWithClass:(Class)aClass +{ + HCRequireNonNilObject(aClass); + + self = [super init]; + if (self) + _theClass = aClass; + return self; +} + +- (void)describeTo:(id )description +{ + [[description appendText:[self expectation]] + appendText:NSStringFromClass(self.theClass)]; +} + +- (void)describeMismatchOf:(nullable id)item to:(nullable id )mismatchDescription +{ + [mismatchDescription appendText:@"was "]; + if (item) + { + [[mismatchDescription appendText:NSStringFromClass([item class])] + appendText:@" instance "]; + } + [mismatchDescription appendDescriptionOf:item]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCConformsToProtocol.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCConformsToProtocol.h new file mode 100644 index 0000000000..547d7008f0 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCConformsToProtocol.h @@ -0,0 +1,42 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt +// Contribution by Todd Farrell + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Matches objects that conform to specified protocol. + */ +@interface HCConformsToProtocol : HCBaseMatcher + +- (instancetype)initWithProtocol:(Protocol *)protocol NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + + +FOUNDATION_EXPORT id HC_conformsTo(Protocol *aProtocol); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object conforms to the specified + * protocol. + * @param aProtocol The protocol to compare against as the expected protocol. + * @discussion + * Example
+ *
assertThat(myObject, conformsTo(\@protocol(NSCoding))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_conformsTo instead. + */ +static inline id conformsTo(Protocol *aProtocol) +{ + return HC_conformsTo(aProtocol); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCConformsToProtocol.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCConformsToProtocol.m new file mode 100644 index 0000000000..b9b8267f0c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCConformsToProtocol.m @@ -0,0 +1,44 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt +// Contribution by Todd Farrell +// + +#import "HCConformsToProtocol.h" + +#import "HCRequireNonNilObject.h" + + +@interface HCConformsToProtocol () +@property (nonatomic, strong, readonly) Protocol *protocol; +@end + +@implementation HCConformsToProtocol + +- (instancetype)initWithProtocol:(Protocol *)protocol +{ + HCRequireNonNilObject(protocol); + + self = [super init]; + if (self) + _protocol = protocol; + return self; +} + +- (BOOL)matches:(nullable id)item +{ + return [item conformsToProtocol:self.protocol]; +} + +- (void)describeTo:(id )description +{ + [[description appendText:@"an object that conforms to "] + appendText:NSStringFromProtocol(self.protocol)]; +} + +@end + + +id HC_conformsTo(Protocol *aProtocol) +{ + return [[HCConformsToProtocol alloc] initWithProtocol:aProtocol]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCHasDescription.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCHasDescription.h new file mode 100644 index 0000000000..21eba9683b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCHasDescription.h @@ -0,0 +1,45 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Matches objects whose description satisfies a nested matcher. + */ +@interface HCHasDescription : HCInvocationMatcher + +- (instancetype)initWithDescription:(id )descriptionMatcher NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithInvocation:(NSInvocation *)anInvocation matching:(id )aMatcher NS_UNAVAILABLE; + +@end + + +FOUNDATION_EXPORT id HC_hasDescription(id descriptionMatcher); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object's -description + * satisfies the specified matcher. + * @param descriptionMatcher The matcher used to verify the description result, or an expected value + * for equalTo matching. + * @discussion If descriptionMatcher is not a matcher, it is implicitly wrapped in + * an equalTo matcher to check for equality. + * + * Examples
+ *
assertThat(myObject, hasDescription(equalTo(\@"foo"))
+ *
assertThat(myObject, hasDescription(\@"foo"))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_hasDescription instead. + */ +static inline id hasDescription(id descriptionMatcher) +{ + return HC_hasDescription(descriptionMatcher); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCHasDescription.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCHasDescription.m new file mode 100644 index 0000000000..1064019537 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCHasDescription.m @@ -0,0 +1,28 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCHasDescription.h" + +#import "HCWrapInMatcher.h" +#import "NSInvocation+OCHamcrest.h" + + +@implementation HCHasDescription + +- (instancetype)initWithDescription:(id )descriptionMatcher +{ + NSInvocation *anInvocation = [NSInvocation och_invocationOnObjectOfType:[NSObject class] + selector:@selector(description)]; + self = [super initWithInvocation:anInvocation matching:descriptionMatcher]; + if (self) + self.shortMismatchDescription = YES; + return self; +} + +@end + + +id HC_hasDescription(id descriptionMatcher) +{ + return [[HCHasDescription alloc] initWithDescription:HCWrapInMatcher(descriptionMatcher)]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCHasProperty.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCHasProperty.h new file mode 100644 index 0000000000..afb2620a77 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCHasProperty.h @@ -0,0 +1,47 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt +// Contribution by Justin Shacklette + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Matches objects whose "property" (or simple method) satisfies a nested matcher. + */ +@interface HCHasProperty : HCDiagnosingMatcher + +- (instancetype)initWithProperty:(NSString *)propertyName value:(id )valueMatcher NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + + +FOUNDATION_EXPORT id HC_hasProperty(NSString *propertyName, _Nullable id valueMatcher); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object has an instance method with the + * specified name whose return value satisfies the specified matcher. + * @param propertyName The name of an instance method without arguments that returns an object. + * @param valueMatcher The matcher to satisfy for the return value, or an expected value for + * equalTo matching. + * @discussion Note: While this matcher factory is called "hasProperty", it applies to the return + * values of any instance methods without arguments, not just properties. + * + * Examples
+ *
assertThat(person, hasProperty(\@"firstName", equalTo(\@"Joe")))
+ *
assertThat(person, hasProperty(\@"firstName", \@"Joe"))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_hasProperty instead. + */ +static inline id hasProperty(NSString *propertyName, _Nullable id valueMatcher) +{ + return HC_hasProperty(propertyName, valueMatcher); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCHasProperty.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCHasProperty.m new file mode 100644 index 0000000000..69379d9479 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCHasProperty.m @@ -0,0 +1,71 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt +// Contribution by Justin Shacklette + +#import "HCHasProperty.h" + +#import "HCRequireNonNilObject.h" +#import "HCWrapInMatcher.h" +#import "NSInvocation+OCHamcrest.h" + + +@interface HCHasProperty () +@property (nonatomic, copy, readonly) NSString *propertyName; +@property (nonatomic, strong, readonly) id valueMatcher; +@end + +@implementation HCHasProperty + +- (instancetype)initWithProperty:(NSString *)propertyName value:(id )valueMatcher +{ + HCRequireNonNilObject(propertyName); + + self = [super init]; + if (self != nil) + { + _propertyName = [propertyName copy]; + _valueMatcher = valueMatcher; + } + return self; +} + +- (BOOL)matches:(nullable id)item describingMismatchTo:(id )mismatchDescription +{ + SEL propertyGetter = NSSelectorFromString(self.propertyName); + if (![item respondsToSelector:propertyGetter]) + { + [[[[mismatchDescription appendText:@"no "] + appendText:self.propertyName] + appendText:@" on "] + appendDescriptionOf:item]; + return NO; + } + + NSInvocation *getterInvocation = [NSInvocation och_invocationWithTarget:item selector:propertyGetter]; + id propertyValue = [getterInvocation och_invoke]; + BOOL match = [self.valueMatcher matches:propertyValue]; + if (!match) + { + [[[[[mismatchDescription appendText:self.propertyName] + appendText:@" was "] + appendDescriptionOf:propertyValue] + appendText:@" on "] + appendDescriptionOf:item]; + } + return match; +} + +- (void)describeTo:(id )description +{ + [[[[description appendText:@"an object with "] + appendText:self.propertyName] + appendText:@" "] + appendDescriptionOf:self.valueMatcher]; +} +@end + + +id HC_hasProperty(NSString *propertyName, _Nullable id valueMatcher) +{ + return [[HCHasProperty alloc] initWithProperty:propertyName value:HCWrapInMatcher(valueMatcher)]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsEqual.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsEqual.h new file mode 100644 index 0000000000..329aa17b1c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsEqual.h @@ -0,0 +1,41 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Is the value equal to another value, as tested by the -isEqual: method? + */ +@interface HCIsEqual : HCBaseMatcher + +- (instancetype)initEqualTo:(nullable id)expectedValue NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + + +FOUNDATION_EXPORT id HC_equalTo(_Nullable id operand); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is equal to the specified + * object, as determined by calling the -isEqual: method on the examined object. + * @param operand The object to compare against as the expected value. + * @discussion If the specified operand is nil, then the created matcher will match if + * the examined object itself is nil, or if the examined object's -isEqual: + * method returns YES when passed a nil. + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_equalTo instead. + */ +static inline id equalTo(_Nullable id operand) +{ + return HC_equalTo(operand); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsEqual.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsEqual.m new file mode 100644 index 0000000000..d82612d3e7 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsEqual.m @@ -0,0 +1,46 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCIsEqual.h" + + +@interface HCIsEqual () +@property (nullable, nonatomic, strong, readonly) id expectedValue; +@end + +@implementation HCIsEqual + +- (instancetype)initEqualTo:(nullable id)expectedValue +{ + self = [super init]; + if (self) + _expectedValue = expectedValue; + return self; +} + +- (BOOL)matches:(nullable id)item +{ + if (item == nil) + return self.expectedValue == nil; + return [item isEqual:self.expectedValue]; +} + +- (void)describeTo:(id )description +{ + if ([self.expectedValue conformsToProtocol:@protocol(HCMatcher)]) + { + [[[description appendText:@"<"] + appendDescriptionOf:self.expectedValue] + appendText:@">"]; + } + else + [description appendDescriptionOf:self.expectedValue]; +} + +@end + + +id HC_equalTo(_Nullable id operand) +{ + return [[HCIsEqual alloc] initEqualTo:operand]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsInstanceOf.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsInstanceOf.h new file mode 100644 index 0000000000..cf28d2e11d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsInstanceOf.h @@ -0,0 +1,37 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Matches objects that are of a given class or any subclass. + */ +@interface HCIsInstanceOf : HCClassMatcher +@end + + +FOUNDATION_EXPORT id HC_instanceOf(Class expectedClass); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is an instance of, or inherits + * from, the specified class. + * @param expectedClass The class to compare against as the expected class. + * @discussion + * Example
+ *
assertThat(canoe, instanceOf([Canoe class]))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_instanceOf instead. + */ +static inline id instanceOf(Class expectedClass) +{ + return HC_instanceOf(expectedClass); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsInstanceOf.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsInstanceOf.m new file mode 100644 index 0000000000..dd2a97899e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsInstanceOf.m @@ -0,0 +1,25 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCIsInstanceOf.h" + + +@implementation HCIsInstanceOf + +- (BOOL)matches:(nullable id)item +{ + return [item isKindOfClass:self.theClass]; +} + +- (NSString *)expectation +{ + return @"an instance of "; +} + +@end + + +id HC_instanceOf(Class expectedClass) +{ + return [[HCIsInstanceOf alloc] initWithClass:expectedClass]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsNil.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsNil.h new file mode 100644 index 0000000000..bc98b7b07a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsNil.h @@ -0,0 +1,55 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Is the value nil? + */ +@interface HCIsNil : HCBaseMatcher +@end + + +FOUNDATION_EXPORT id HC_nilValue(void); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is nil. + * @discussion + * Example
+ *
assertThat(myObject, nilValue())
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_nilValue instead. + */ +static inline id nilValue(void) +{ + return HC_nilValue(); +} +#endif + + +FOUNDATION_EXPORT id HC_notNilValue(void); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is not nil. + * @discussion + * Example
+ *
assertThat(myObject, notNilValue())
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_notNilValue instead. + */ +static inline id notNilValue(void) +{ + return HC_notNilValue(); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsNil.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsNil.m new file mode 100644 index 0000000000..0eebf0229f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsNil.m @@ -0,0 +1,32 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCIsNil.h" + +#import "HCIsNot.h" + + +@implementation HCIsNil + +- (BOOL)matches:(nullable id)item +{ + return item == nil; +} + +- (void)describeTo:(id )description +{ + [description appendText:@"nil"]; +} + +@end + + +id HC_nilValue() +{ + return [[HCIsNil alloc] init]; +} + +id HC_notNilValue() +{ + return HC_isNot([[HCIsNil alloc] init]); +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsSame.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsSame.h new file mode 100644 index 0000000000..8ed0b49bf6 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsSame.h @@ -0,0 +1,41 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Is the value the same object as another value? + */ +@interface HCIsSame : HCBaseMatcher + +- (instancetype)initSameAs:(nullable id)object NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + + +FOUNDATION_EXPORT id HC_sameInstance(_Nullable id expectedInstance); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches only when the examined object is the same instance as + * the specified target object. + * @param expectedInstance The expected instance. + * @discussion + * Example
+ *
assertThat(delegate, sameInstance(expectedDelegate))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_sameInstance instead. + */ +static inline id sameInstance(_Nullable id expectedInstance) +{ + return HC_sameInstance(expectedInstance); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsSame.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsSame.m new file mode 100644 index 0000000000..e73b27a590 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsSame.m @@ -0,0 +1,46 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCIsSame.h" + + +@interface HCIsSame () +@property (nonatomic, strong, readonly) id object; +@end + +@implementation HCIsSame + +- (instancetype)initSameAs:(nullable id)object +{ + self = [super init]; + if (self) + _object = object; + return self; +} + +- (BOOL)matches:(nullable id)item +{ + return item == self.object; +} + +- (void)describeMismatchOf:(nullable id)item to:(nullable id )mismatchDescription +{ + [mismatchDescription appendText:@"was "]; + if (item) + [mismatchDescription appendText:[NSString stringWithFormat:@"%p ", (__bridge void *)item]]; + [mismatchDescription appendDescriptionOf:item]; +} + +- (void)describeTo:(id )description +{ + [[description appendText:[NSString stringWithFormat:@"same instance as %p ", (__bridge void *)self.object]] + appendDescriptionOf:self.object]; +} + +@end + + +id HC_sameInstance(_Nullable id expectedInstance) +{ + return [[HCIsSame alloc] initSameAs:expectedInstance]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsTypeOf.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsTypeOf.h new file mode 100644 index 0000000000..634b50b6f6 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsTypeOf.h @@ -0,0 +1,37 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Matches objects that are of a given class. + */ +@interface HCIsTypeOf : HCClassMatcher +@end + + +FOUNDATION_EXPORT id HC_isA(Class expectedClass); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is an instance of the specified + * class, but not of any subclass. + * @param expectedClass The class to compare against as the expected class. + * @discussion + * Example
+ *
assertThat(canoe, isA([Canoe class]))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_isA instead. + */ +static inline id isA(Class expectedClass) +{ + return HC_isA(expectedClass); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsTypeOf.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsTypeOf.m new file mode 100644 index 0000000000..37bb8a2c9c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCIsTypeOf.m @@ -0,0 +1,25 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCIsTypeOf.h" + + +@implementation HCIsTypeOf + +- (BOOL)matches:(nullable id)item +{ + return [item isMemberOfClass:self.theClass]; +} + +- (NSString *)expectation +{ + return @"an exact instance of "; +} + +@end + + +id HC_isA(Class expectedClass) +{ + return [[HCIsTypeOf alloc] initWithClass:expectedClass]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCThrowsException.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCThrowsException.h new file mode 100644 index 0000000000..eef5f04844 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCThrowsException.h @@ -0,0 +1,41 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Does executing a block throw an exception which satisfies a nested matcher? + */ +@interface HCThrowsException : HCDiagnosingMatcher + +- (id)initWithExceptionMatcher:(id)exceptionMatcher NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + + +FOUNDATION_EXPORT id HC_throwsException(id exceptionMatcher); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is a block which, when + * executed, throws an exception satisfying the specified matcher. + * @param exceptionMatcher The matcher to satisfy when passed the exception. + * @discussion + * Example
+ *
assertThat(^{ [obj somethingBad]; }, throwsException(hasProperty(@"reason", @"EXPECTED REASON")))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_throwsException instead. + */ +static inline id throwsException(id exceptionMatcher) +{ + return HC_throwsException(exceptionMatcher); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCThrowsException.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCThrowsException.m new file mode 100644 index 0000000000..18d6770b0f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Object/HCThrowsException.m @@ -0,0 +1,84 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCThrowsException.h" + + +static void HCRequireMatcher(id obj) +{ + if (![obj conformsToProtocol:@protocol(HCMatcher)]) + { + @throw [NSException exceptionWithName:@"NonMatcher" + reason:@"Must be matcher" + userInfo:nil]; + } +} + + +@interface HCThrowsException() +@property (nonatomic, strong, readonly) id exceptionMatcher; +@end + +@implementation HCThrowsException + +- (id)initWithExceptionMatcher:(id)exceptionMatcher +{ + HCRequireMatcher(exceptionMatcher); + + self = [super init]; + if (self) + _exceptionMatcher = exceptionMatcher; + return self; +} + +- (BOOL)matches:(nullable id)item describingMismatchTo:(id )mismatchDescription +{ + if (![self isBlock:item]) + { + [[mismatchDescription appendText:@"was non-block "] appendDescriptionOf:item]; + return NO; + } + + typedef void (^HCThrowsExceptionBlock)(void); + HCThrowsExceptionBlock block = item; + @try + { + block(); + } + @catch (id exception) + { + BOOL match = [self.exceptionMatcher matches:exception]; + if (!match) + { + [mismatchDescription appendText:@"exception thrown but "]; + [self.exceptionMatcher describeMismatchOf:exception to:mismatchDescription]; + } + return match; + } + + [mismatchDescription appendText:@"no exception thrown"]; + return NO; +} + +- (BOOL)isBlock:(id)item +{ + id block = ^{}; + Class blockClass = [block class]; + while ([blockClass superclass] != [NSObject class]) + blockClass = [blockClass superclass]; + return [item isKindOfClass:blockClass]; +} + +- (void)describeTo:(id )description +{ + [[description appendText:@"a block with no arguments, throwing an exception which is "] + appendDescriptionOf:self.exceptionMatcher]; +} + +@end + + +id HC_throwsException(id exceptionMatcher) +{ + return [[HCThrowsException alloc] initWithExceptionMatcher:exceptionMatcher]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCIsEqualCompressingWhiteSpace.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCIsEqualCompressingWhiteSpace.h new file mode 100644 index 0000000000..2e48da3408 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCIsEqualCompressingWhiteSpace.h @@ -0,0 +1,46 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Tests if a string is equal to another string, when whitespace differences are (mostly) ignored. + */ +@interface HCIsEqualCompressingWhiteSpace : HCBaseMatcher + +- (instancetype)initWithString:(NSString *)string NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + + +FOUNDATION_EXPORT id HC_equalToCompressingWhiteSpace(NSString *expectedString); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher for NSStrings that matches when the examined string is equal to the + * specified expected string, when whitespace differences are (mostly) ignored. + * @param expectedString The expected value of matched strings. (Must not be nil.) + * @discussion To be exact, the following whitespace rules are applied: + *
    + *
  • all leading and trailing whitespace of both the expectedString and the examined string are ignored
  • + *
  • any remaining whitespace, appearing within either string, is collapsed to a single space before comparison
  • + *
+ * + * Example
+ *
assertThat(\@"   my\tfoo  bar ", equalToCompressingWhiteSpace(\@" my  foo bar"))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_equalToCompressingWhiteSpace instead. + */ +static inline id equalToCompressingWhiteSpace(NSString *expectedString) +{ + return HC_equalToCompressingWhiteSpace(expectedString); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCIsEqualCompressingWhiteSpace.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCIsEqualCompressingWhiteSpace.m new file mode 100644 index 0000000000..cffaebe79e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCIsEqualCompressingWhiteSpace.m @@ -0,0 +1,62 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCIsEqualCompressingWhiteSpace.h" + +#import "HCRequireNonNilObject.h" + + +static NSString *stripSpaces(NSString *string) +{ + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\s+" + options:0 + error:NULL]; + NSString *modifiedString = [regex stringByReplacingMatchesInString:string + options:0 + range:NSMakeRange(0, string.length) + withTemplate:@" "]; + return [modifiedString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; +} + + +@interface HCIsEqualCompressingWhiteSpace () +@property (nonatomic, copy, readonly) NSString *originalString; +@property (nonatomic, copy, readonly) NSString *strippedString; +@end + +@implementation HCIsEqualCompressingWhiteSpace + +- (instancetype)initWithString:(NSString *)string +{ + HCRequireNonNilObject(string); + + self = [super init]; + if (self) + { + _originalString = [string copy]; + _strippedString = [stripSpaces(string) copy]; + } + return self; +} + +- (BOOL)matches:(nullable id)item +{ + if (![item isKindOfClass:[NSString class]]) + return NO; + + return [self.strippedString isEqualToString:stripSpaces(item)]; +} + +- (void)describeTo:(id )description +{ + [[description appendDescriptionOf:self.originalString] + appendText:@" ignoring whitespace"]; +} + +@end + + +id HC_equalToCompressingWhiteSpace(NSString *expectedString) +{ + return [[HCIsEqualCompressingWhiteSpace alloc] initWithString:expectedString]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCIsEqualIgnoringCase.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCIsEqualIgnoringCase.h new file mode 100644 index 0000000000..ad3487e66f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCIsEqualIgnoringCase.h @@ -0,0 +1,41 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Tests if a string is equal to another string, regardless of the case. + */ +@interface HCIsEqualIgnoringCase : HCBaseMatcher + +- (instancetype)initWithString:(NSString *)string NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + + +FOUNDATION_EXPORT id HC_equalToIgnoringCase(NSString *expectedString); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher for NSStrings that matches when the examined string is equal to the + * specified expected string, ignoring case differences. + * @param expectedString The expected value of matched strings. (Must not be nil.) + * @discussion + * Example
+ *
assertThat(\@"Foo", equalToIgnoringCase(\@"FOO"))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_equalToIgnoringCase instead. + */ +static inline id equalToIgnoringCase(NSString *expectedString) +{ + return HC_equalToIgnoringCase(expectedString); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCIsEqualIgnoringCase.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCIsEqualIgnoringCase.m new file mode 100644 index 0000000000..d57dc22144 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCIsEqualIgnoringCase.m @@ -0,0 +1,45 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCIsEqualIgnoringCase.h" + +#import "HCRequireNonNilObject.h" + + +@interface HCIsEqualIgnoringCase () +@property (nonatomic, copy, readonly) NSString *string; +@end + +@implementation HCIsEqualIgnoringCase + +- (instancetype)initWithString:(NSString *)string +{ + HCRequireNonNilObject(string); + + self = [super init]; + if (self) + _string = [string copy]; + return self; +} + +- (BOOL)matches:(nullable id)item +{ + if (![item isKindOfClass:[NSString class]]) + return NO; + + return [self.string caseInsensitiveCompare:item] == NSOrderedSame; +} + +- (void)describeTo:(id )description +{ + [[description appendDescriptionOf:self.string] + appendText:@" ignoring case"]; +} + +@end + + +id HC_equalToIgnoringCase(NSString *expectedString) +{ + return [[HCIsEqualIgnoringCase alloc] initWithString:expectedString]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCStringContains.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCStringContains.h new file mode 100644 index 0000000000..c6faa838cb --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCStringContains.h @@ -0,0 +1,39 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Tests if string that contains a substring. + */ +@interface HCStringContains : HCSubstringMatcher +@end + + +FOUNDATION_EXPORT id HC_containsSubstring(NSString *substring); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is a string containing the + * specified substring anywhere. + * @param substring The string to search for. (Must not be nil.) + * @discussion The matcher invokes -rangeOfString: on the examined object, passing the + * specified substring and matching if it is found. + * + * Example
+ *
assertThat(\@"myStringOfNote", containsSubstring(\@"ring"))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_containsSubstring instead. + */ +static inline id containsSubstring(NSString *substring) +{ + return HC_containsSubstring(substring); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCStringContains.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCStringContains.m new file mode 100644 index 0000000000..a40cb3bd0c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCStringContains.m @@ -0,0 +1,28 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCStringContains.h" + + +@implementation HCStringContains + +- (BOOL)matches:(nullable id)item +{ + if (![item respondsToSelector:@selector(rangeOfString:)]) + return NO; + + return [item rangeOfString:self.substring].location != NSNotFound; +} + +- (NSString *)relationship +{ + return @"containing"; +} + +@end + + +id HC_containsSubstring(NSString *substring) +{ + return [[HCStringContains alloc] initWithSubstring:substring]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCStringContainsInOrder.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCStringContainsInOrder.h new file mode 100644 index 0000000000..e74e9b1685 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCStringContainsInOrder.h @@ -0,0 +1,62 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Tests if string that contains a list of substrings in relative order. + */ +@interface HCStringContainsInOrder : HCBaseMatcher + +- (instancetype)initWithSubstrings:(NSArray *)substrings NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + + +FOUNDATION_EXPORT id HC_stringContainsInOrderIn(NSArray *substrings); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates matcher for NSStrings that matches when the examined string contains all of the + * specified substrings, considering the order of their appearance. + * @param substrings An array of strings. + * @discussion + * Example
+ *
assertThat(\@"myfoobarbaz", stringContainsInOrderIn(\@[\@"bar", \@"foo"]))
+ * fails as "foo" occurs before "bar" in the string "myfoobarbaz" + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_stringContainsInOrderIn instead. + */ +static inline id stringContainsInOrderIn(NSArray *substrings) +{ + return HC_stringContainsInOrderIn(substrings); +} +#endif + + +FOUNDATION_EXPORT id HC_stringContainsInOrder(NSString *substrings, ...) NS_REQUIRES_NIL_TERMINATION; + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates matcher for NSStrings that matches when the examined string contains all of the + * specified substrings, considering the order of their appearance. + * @param substrings... A comma-separated list of strings, ending with nil. + * @discussion + * Example
+ *
assertThat(\@"myfoobarbaz", stringContainsInOrder(\@"bar", \@"foo", nil))
+ * fails as "foo" occurs before "bar" in the string "myfoobarbaz" + * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_stringContainsInOrder instead. + */ +#define stringContainsInOrder(substrings...) HC_stringContainsInOrder(substrings) +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCStringContainsInOrder.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCStringContainsInOrder.m new file mode 100644 index 0000000000..fbe69822d5 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCStringContainsInOrder.m @@ -0,0 +1,78 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCStringContainsInOrder.h" + +#import "HCCollect.h" + + +static void requireElementsToBeStrings(NSArray *array) +{ + for (id element in array) + { + if (![element isKindOfClass:[NSString class]]) + { + @throw [NSException exceptionWithName:@"NotAString" + reason:@"Arguments must be strings" + userInfo:nil]; + } + } +} + + +@interface HCStringContainsInOrder () +@property (nonatomic, copy, readonly) NSArray *substrings; +@end + +@implementation HCStringContainsInOrder + +- (instancetype)initWithSubstrings:(NSArray *)substrings +{ + self = [super init]; + if (self) + { + requireElementsToBeStrings(substrings); + _substrings = [substrings copy]; + } + return self; +} + +- (BOOL)matches:(nullable id)item +{ + if (![item isKindOfClass:[NSString class]]) + return NO; + + NSRange searchRange = NSMakeRange(0, [item length]); + for (NSString *substring in self.substrings) + { + NSRange substringRange = [item rangeOfString:substring options:0 range:searchRange]; + if (substringRange.location == NSNotFound) + return NO; + searchRange.location = substringRange.location + substringRange.length; + searchRange.length = [item length] - searchRange.location; + } + return YES; +} + +- (void)describeTo:(id )description +{ + [description appendList:self.substrings start:@"a string containing " separator:@", " end:@" in order"]; +} + +@end + + +id HC_stringContainsInOrderIn(NSArray *substrings) +{ + return [[HCStringContainsInOrder alloc] initWithSubstrings:substrings]; +} + +id HC_stringContainsInOrder(NSString *substrings, ...) +{ + va_list args; + va_start(args, substrings); + NSArray *array = HCCollectItems(substrings, args); + va_end(args); + + return HC_stringContainsInOrderIn(array); +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCStringEndsWith.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCStringEndsWith.h new file mode 100644 index 0000000000..996d4d2d1d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCStringEndsWith.h @@ -0,0 +1,40 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Tests if string ends with a substring. + */ +@interface HCStringEndsWith : HCSubstringMatcher +@end + + +FOUNDATION_EXPORT id HC_endsWith(NSString *suffix); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is a string that ends with the + * specified string. + * @param suffix The substring that the returned matcher will expect at the end of any examined + * string. (Must not be nil.) + * @discussion The matcher invokes -hasSuffix: on the examined object, passing the + * specified suffix. + * + * Example
+ *
assertThat(\@"myStringOfNote", endsWith(\@"Note"))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_endsWith instead. + */ +static inline id endsWith(NSString *suffix) +{ + return HC_endsWith(suffix); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCStringEndsWith.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCStringEndsWith.m new file mode 100644 index 0000000000..25958fc0c9 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCStringEndsWith.m @@ -0,0 +1,28 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCStringEndsWith.h" + + +@implementation HCStringEndsWith + +- (BOOL)matches:(nullable id)item +{ + if (![item respondsToSelector:@selector(hasSuffix:)]) + return NO; + + return [item hasSuffix:self.substring]; +} + +- (NSString *)relationship +{ + return @"ending with"; +} + +@end + + +id HC_endsWith(NSString *suffix) +{ + return [[HCStringEndsWith alloc] initWithSubstring:suffix]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCStringStartsWith.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCStringStartsWith.h new file mode 100644 index 0000000000..da909dbe41 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCStringStartsWith.h @@ -0,0 +1,40 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +/*! + * @abstract Tests string starts with a substring. + */ +@interface HCStringStartsWith : HCSubstringMatcher +@end + + +FOUNDATION_EXPORT id HC_startsWith(NSString *prefix); + +#ifndef HC_DISABLE_SHORT_SYNTAX +/*! + * @abstract Creates a matcher that matches when the examined object is a string that starts with + * the specified string. + * @param prefix The substring that the returned matcher will expect at the start of any examined + * string. (Must not be nil.) + * @discussion The matcher invokes -hasPrefix: on the examined object, passing the + * specified prefix. + * + * Example
+ *
assertThat(\@"myStringOfNote", startsWith(\@"my"))
+ * + * Name Clash
+ * In the event of a name clash, #define HC_DISABLE_SHORT_SYNTAX and use the synonym + * HC_startsWith instead. + */ +static inline id startsWith(NSString *prefix) +{ + return HC_startsWith(prefix); +} +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCStringStartsWith.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCStringStartsWith.m new file mode 100644 index 0000000000..0d8c7ebce7 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCStringStartsWith.m @@ -0,0 +1,28 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCStringStartsWith.h" + + +@implementation HCStringStartsWith + +- (BOOL)matches:(nullable id)item +{ + if (![item respondsToSelector:@selector(hasPrefix:)]) + return NO; + + return [item hasPrefix:self.substring]; +} + +- (NSString *)relationship +{ + return @"starting with"; +} + +@end + + +id HC_startsWith(NSString *prefix) +{ + return [[HCStringStartsWith alloc] initWithSubstring:prefix]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCSubstringMatcher.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCSubstringMatcher.h new file mode 100644 index 0000000000..e02fc8e3e1 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCSubstringMatcher.h @@ -0,0 +1,18 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +@interface HCSubstringMatcher : HCBaseMatcher + +@property (nonatomic, copy, readonly) NSString *substring; + +- (instancetype)initWithSubstring:(NSString *)substring NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCSubstringMatcher.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCSubstringMatcher.m new file mode 100644 index 0000000000..c9f2cac916 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Library/Text/HCSubstringMatcher.m @@ -0,0 +1,34 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCSubstringMatcher.h" + +#import "HCRequireNonNilObject.h" + + +@interface HCSubstringMatcher (SubclassResponsibility) +- (NSString *)relationship; +@end + + +@implementation HCSubstringMatcher + +- (instancetype)initWithSubstring:(NSString *)substring +{ + HCRequireNonNilObject(substring); + + self = [super init]; + if (self) + _substring = [substring copy]; + return self; +} + +- (void)describeTo:(id )description +{ + [[[[description appendText:@"a string "] + appendText:[self relationship]] + appendText:@" "] + appendDescriptionOf:self.substring]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/MakeDistribution.sh b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/MakeDistribution.sh new file mode 100755 index 0000000000..616540c6bb --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/MakeDistribution.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +VERSION=7.1.2 +DISTFILE=OCHamcrest-${VERSION} +DISTPATH=build/${DISTFILE} +PROJECTROOT=.. + +echo Preparing clean build +rm -rf build +mkdir build + +echo Building OCHamcrest - Release +xcodebuild -configuration Release -target OCHamcrest +OUT=$? +if [ "${OUT}" -ne "0" ]; then + echo OCHamcrest release build failed + exit ${OUT} +fi + +echo Building OCHamcrestIOS - Release +source MakeIOSFramework.sh +OUT=$? +if [ "${OUT}" -ne "0" ]; then + echo OCHamcrestIOS release build failed + exit ${OUT} +fi + +echo Assembling Distribution +rm -rf "${DISTPATH}" +mkdir "${DISTPATH}" +cp -R "build/Release/OCHamcrest.framework" "${DISTPATH}" +cp -R "build/Release/OCHamcrestIOS.framework" "${DISTPATH}" +cp "${PROJECTROOT}/README.md" "${DISTPATH}" +cp "${PROJECTROOT}/CHANGELOG.md" "${DISTPATH}" +cp "${PROJECTROOT}/LICENSE.txt" "${DISTPATH}" +cp -R "${PROJECTROOT}/Examples" "${DISTPATH}" + +find "${DISTPATH}/Examples" -type d \( -name 'build' -or -name 'xcuserdata' -or -name '.svn' -or -name '.git' \) | while read DIR +do + rm -R "${DIR}"; +done + +find "${DISTPATH}/Examples" -type f \( -name '*.pbxuser' -or -name '*.perspectivev3' -or -name '*.mode1v3' -or -name '.DS_Store' -or -name '.gitignore' \) | while read FILE +do + rm "${FILE}"; +done + +pushd build +zip --recurse-paths --symlinks ${DISTFILE}.zip ${DISTFILE} +open . +popd diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/MakeIOSFramework.sh b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/MakeIOSFramework.sh new file mode 100755 index 0000000000..a953212669 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/MakeIOSFramework.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +# First build the OS X framework to get its folder structure. +xcodebuild -configuration Release -target OCHamcrest -sdk macosx + +# We'll copy the OS X framework to a new location, then modify it in place. +OSX_FRAMEWORK="build/Release/OCHamcrest.framework/" +IOS_FRAMEWORK="build/Release/OCHamcrestIOS.framework/" + +# Trigger builds of the static library for both the simulator and the device. +xcodebuild -configuration Release -target libochamcrest -sdk iphoneos +OUT=$? +if [ "${OUT}" -ne "0" ]; then + echo Device build failed + exit ${OUT} +fi +xcodebuild -configuration Release -target libochamcrest -sdk iphonesimulator +OUT=$? +if [ "${OUT}" -ne "0" ]; then + echo Simulator build failed + exit ${OUT} +fi + +# Copy the OS X framework to the new location. +mkdir -p "${IOS_FRAMEWORK}" +rsync -q -a --delete "${OSX_FRAMEWORK}" "${IOS_FRAMEWORK}" + +# Rename the main header. +mv "${IOS_FRAMEWORK}/Headers/OCHamcrest.h" "${IOS_FRAMEWORK}/Headers/OCHamcrestIOS.h" + +# Update all imports to use the new framework name. +IMPORT_EXPRESSION="s/#import + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2020 hamcrest.org + NSPrincipalClass + + + diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/OCHamcrest.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/OCHamcrest.h new file mode 100644 index 0000000000..0b728e6c02 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/OCHamcrest.h @@ -0,0 +1,59 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +FOUNDATION_EXPORT double OCHamcrestVersionNumber; +FOUNDATION_EXPORT const unsigned char OCHamcrestVersionString[]; + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +// Carthage workaround: Include transitive public headers +#import +#import +#import +#import +#import diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/OCHamcrest.xcodeproj/project.pbxproj b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/OCHamcrest.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..cabd2ae75c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/OCHamcrest.xcodeproj/project.pbxproj @@ -0,0 +1,3243 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 081BEE721345979F003F846A /* libochamcrest.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 081BEE621345979F003F846A /* libochamcrest.a */; }; + 082E7C3113FF676E004A22FE /* MockSenTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 082E7C2B13FF676E004A22FE /* MockSenTestCase.m */; }; + 082E7C3213FF676E004A22FE /* MockSenTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 082E7C2B13FF676E004A22FE /* MockSenTestCase.m */; }; + 082E7C3313FF676E004A22FE /* HCBaseMatcherTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 082E7C2C13FF676E004A22FE /* HCBaseMatcherTests.m */; }; + 082E7C3413FF676E004A22FE /* HCBaseMatcherTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 082E7C2C13FF676E004A22FE /* HCBaseMatcherTests.m */; }; + 082E7C3513FF676E004A22FE /* HCInvocationMatcherTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 082E7C2D13FF676E004A22FE /* HCInvocationMatcherTests.m */; }; + 082E7C3613FF676E004A22FE /* HCInvocationMatcherTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 082E7C2D13FF676E004A22FE /* HCInvocationMatcherTests.m */; }; + 082E7C3713FF676E004A22FE /* HCStringDescriptionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 082E7C2E13FF676E004A22FE /* HCStringDescriptionTests.m */; }; + 082E7C3813FF676E004A22FE /* HCStringDescriptionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 082E7C2E13FF676E004A22FE /* HCStringDescriptionTests.m */; }; + 0846B6E013EE5F9D00EA68E8 /* HCDescribedAsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0846B6DC13EE5F9D00EA68E8 /* HCDescribedAsTests.m */; }; + 0846B6E113EE5F9D00EA68E8 /* HCDescribedAsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0846B6DC13EE5F9D00EA68E8 /* HCDescribedAsTests.m */; }; + 0846B6E213EE5F9D00EA68E8 /* HCIsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0846B6DD13EE5F9D00EA68E8 /* HCIsTests.m */; }; + 0846B6E313EE5F9D00EA68E8 /* HCIsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0846B6DD13EE5F9D00EA68E8 /* HCIsTests.m */; }; + 0846B6E413EE5F9D00EA68E8 /* NeverMatch.m in Sources */ = {isa = PBXBuildFile; fileRef = 0846B6DF13EE5F9D00EA68E8 /* NeverMatch.m */; }; + 0846B6E513EE5F9D00EA68E8 /* NeverMatch.m in Sources */ = {isa = PBXBuildFile; fileRef = 0846B6DF13EE5F9D00EA68E8 /* NeverMatch.m */; }; + 0846B6EB13EE5FB400EA68E8 /* HCAllOfTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0846B6E713EE5FB400EA68E8 /* HCAllOfTests.m */; }; + 0846B6EC13EE5FB400EA68E8 /* HCAllOfTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0846B6E713EE5FB400EA68E8 /* HCAllOfTests.m */; }; + 0846B6ED13EE5FB400EA68E8 /* HCAnyOfTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0846B6E813EE5FB400EA68E8 /* HCAnyOfTests.m */; }; + 0846B6EE13EE5FB400EA68E8 /* HCAnyOfTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0846B6E813EE5FB400EA68E8 /* HCAnyOfTests.m */; }; + 0846B6EF13EE5FB400EA68E8 /* HCIsAnythingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0846B6E913EE5FB400EA68E8 /* HCIsAnythingTests.m */; }; + 0846B6F013EE5FB400EA68E8 /* HCIsAnythingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0846B6E913EE5FB400EA68E8 /* HCIsAnythingTests.m */; }; + 0846B6F113EE5FB400EA68E8 /* HCIsNotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0846B6EA13EE5FB400EA68E8 /* HCIsNotTests.m */; }; + 0846B6F213EE5FB400EA68E8 /* HCIsNotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0846B6EA13EE5FB400EA68E8 /* HCIsNotTests.m */; }; + 0846B6F713EE5FD200EA68E8 /* HCIsEqualTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0846B6F313EE5FD200EA68E8 /* HCIsEqualTests.m */; }; + 0846B6F813EE5FD200EA68E8 /* HCIsEqualTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0846B6F313EE5FD200EA68E8 /* HCIsEqualTests.m */; }; + 0846B6F913EE5FD200EA68E8 /* HCIsInstanceOfTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0846B6F413EE5FD200EA68E8 /* HCIsInstanceOfTests.m */; }; + 0846B6FA13EE5FD200EA68E8 /* HCIsInstanceOfTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0846B6F413EE5FD200EA68E8 /* HCIsInstanceOfTests.m */; }; + 0846B6FB13EE5FD200EA68E8 /* HCIsNilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0846B6F513EE5FD200EA68E8 /* HCIsNilTests.m */; }; + 0846B6FC13EE5FD200EA68E8 /* HCIsNilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0846B6F513EE5FD200EA68E8 /* HCIsNilTests.m */; }; + 0846B6FD13EE5FD200EA68E8 /* HCIsSameTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0846B6F613EE5FD200EA68E8 /* HCIsSameTests.m */; }; + 0846B6FE13EE5FD200EA68E8 /* HCIsSameTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0846B6F613EE5FD200EA68E8 /* HCIsSameTests.m */; }; + 0850A44E17165D2A0019BBE0 /* SomeClassAndSubclass.m in Sources */ = {isa = PBXBuildFile; fileRef = 0850A44D17165D2A0019BBE0 /* SomeClassAndSubclass.m */; }; + 0850A44F17165D2A0019BBE0 /* SomeClassAndSubclass.m in Sources */ = {isa = PBXBuildFile; fileRef = 0850A44D17165D2A0019BBE0 /* SomeClassAndSubclass.m */; }; + 0859831013459CA400BE7892 /* FakeWithCount.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876033613440AD8001B439B /* FakeWithCount.m */; }; + 0859831113459CA400BE7892 /* FakeWithoutCount.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876033813440AD8001B439B /* FakeWithoutCount.m */; }; + 0859831213459CA400BE7892 /* HCHasCountTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876033913440AD8001B439B /* HCHasCountTests.m */; }; + 0859831313459CA400BE7892 /* HCHasDescriptionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876035813440AD8001B439B /* HCHasDescriptionTests.m */; }; + 0859831613459CA400BE7892 /* HCIsCloseToTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876035313440AD8001B439B /* HCIsCloseToTests.m */; }; + 0859831713459CA400BE7892 /* HCIsCollectionContainingInAnyOrderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876033A13440AD8001B439B /* HCIsCollectionContainingInAnyOrderTests.m */; }; + 0859831813459CA400BE7892 /* HCIsCollectionContainingInOrderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876033B13440AD8001B439B /* HCIsCollectionContainingInOrderTests.m */; }; + 0859831913459CA400BE7892 /* HCIsCollectionContainingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876033C13440AD8001B439B /* HCIsCollectionContainingTests.m */; }; + 0859831A13459CA400BE7892 /* HCIsCollectionOnlyContainingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876033D13440AD8001B439B /* HCIsCollectionOnlyContainingTests.m */; }; + 0859831B13459CA400BE7892 /* HCIsDictionaryContainingEntriesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876033E13440AD8001B439B /* HCIsDictionaryContainingEntriesTests.m */; }; + 0859831C13459CA400BE7892 /* HCIsDictionaryContainingKeyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876033F13440AD8001B439B /* HCIsDictionaryContainingKeyTests.m */; }; + 0859831D13459CA400BE7892 /* HCIsDictionaryContainingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876034013440AD8001B439B /* HCIsDictionaryContainingTests.m */; }; + 0859831E13459CA400BE7892 /* HCIsDictionaryContainingValueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876034113440AD8001B439B /* HCIsDictionaryContainingValueTests.m */; }; + 0859831F13459CA400BE7892 /* HCIsEmptyCollectionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876034213440AD8001B439B /* HCIsEmptyCollectionTests.m */; }; + 0859832013459CA400BE7892 /* HCIsEqualIgnoringCaseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876035B13440AD8001B439B /* HCIsEqualIgnoringCaseTests.m */; }; + 0859832113459CA400BE7892 /* HCIsEqualCompressingWhiteSpaceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876035C13440AD8001B439B /* HCIsEqualCompressingWhiteSpaceTests.m */; }; + 0859832313459CA400BE7892 /* HCIsEqualToNumberTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876035413440AD8001B439B /* HCIsEqualToNumberTests.m */; }; + 0859832513459CA400BE7892 /* HCIsInTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876034313440AD8001B439B /* HCIsInTests.m */; }; + 0859832B13459CA400BE7892 /* HCNumberAssertTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876035513440AD8001B439B /* HCNumberAssertTests.m */; }; + 0859832C13459CA400BE7892 /* HCOrderingComparisonTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876035613440AD8001B439B /* HCOrderingComparisonTests.m */; }; + 0859832D13459CA400BE7892 /* HCStringContainsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876035D13440AD8001B439B /* HCStringContainsTests.m */; }; + 0859832F13459CA400BE7892 /* HCStringEndsWithTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876035E13440AD8001B439B /* HCStringEndsWithTests.m */; }; + 0859833013459CA400BE7892 /* HCStringStartsWithTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876035F13440AD8001B439B /* HCStringStartsWithTests.m */; }; + 087601FB13440807001B439B /* OCHamcrest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 087601E213440806001B439B /* OCHamcrest.framework */; }; + 0876036313440AD8001B439B /* FakeWithCount.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876033613440AD8001B439B /* FakeWithCount.m */; }; + 0876036413440AD8001B439B /* FakeWithoutCount.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876033813440AD8001B439B /* FakeWithoutCount.m */; }; + 0876036513440AD8001B439B /* HCHasCountTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876033913440AD8001B439B /* HCHasCountTests.m */; }; + 0876036613440AD8001B439B /* HCIsCollectionContainingInAnyOrderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876033A13440AD8001B439B /* HCIsCollectionContainingInAnyOrderTests.m */; }; + 0876036713440AD8001B439B /* HCIsCollectionContainingInOrderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876033B13440AD8001B439B /* HCIsCollectionContainingInOrderTests.m */; }; + 0876036813440AD8001B439B /* HCIsCollectionContainingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876033C13440AD8001B439B /* HCIsCollectionContainingTests.m */; }; + 0876036913440AD8001B439B /* HCIsCollectionOnlyContainingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876033D13440AD8001B439B /* HCIsCollectionOnlyContainingTests.m */; }; + 0876036A13440AD8001B439B /* HCIsDictionaryContainingEntriesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876033E13440AD8001B439B /* HCIsDictionaryContainingEntriesTests.m */; }; + 0876036B13440AD8001B439B /* HCIsDictionaryContainingKeyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876033F13440AD8001B439B /* HCIsDictionaryContainingKeyTests.m */; }; + 0876036C13440AD8001B439B /* HCIsDictionaryContainingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876034013440AD8001B439B /* HCIsDictionaryContainingTests.m */; }; + 0876036D13440AD8001B439B /* HCIsDictionaryContainingValueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876034113440AD8001B439B /* HCIsDictionaryContainingValueTests.m */; }; + 0876036E13440AD8001B439B /* HCIsEmptyCollectionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876034213440AD8001B439B /* HCIsEmptyCollectionTests.m */; }; + 0876036F13440AD8001B439B /* HCIsInTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876034313440AD8001B439B /* HCIsInTests.m */; }; + 0876037C13440AD8001B439B /* HCIsCloseToTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876035313440AD8001B439B /* HCIsCloseToTests.m */; }; + 0876037D13440AD8001B439B /* HCIsEqualToNumberTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876035413440AD8001B439B /* HCIsEqualToNumberTests.m */; }; + 0876037E13440AD8001B439B /* HCNumberAssertTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876035513440AD8001B439B /* HCNumberAssertTests.m */; }; + 0876037F13440AD8001B439B /* HCOrderingComparisonTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876035613440AD8001B439B /* HCOrderingComparisonTests.m */; }; + 0876038013440AD8001B439B /* HCHasDescriptionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876035813440AD8001B439B /* HCHasDescriptionTests.m */; }; + 0876038213440AD8001B439B /* HCIsEqualIgnoringCaseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876035B13440AD8001B439B /* HCIsEqualIgnoringCaseTests.m */; }; + 0876038313440AD8001B439B /* HCIsEqualCompressingWhiteSpaceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876035C13440AD8001B439B /* HCIsEqualCompressingWhiteSpaceTests.m */; }; + 0876038413440AD8001B439B /* HCStringContainsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876035D13440AD8001B439B /* HCStringContainsTests.m */; }; + 0876038513440AD8001B439B /* HCStringEndsWithTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876035E13440AD8001B439B /* HCStringEndsWithTests.m */; }; + 0876038613440AD8001B439B /* HCStringStartsWithTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0876035F13440AD8001B439B /* HCStringStartsWithTests.m */; }; + 088FB0AB136A6DEA00C191E1 /* HCStringContainsInOrderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 088FB0AA136A6DEA00C191E1 /* HCStringContainsInOrderTests.m */; }; + 088FB0AC136A6DEA00C191E1 /* HCStringContainsInOrderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 088FB0AA136A6DEA00C191E1 /* HCStringContainsInOrderTests.m */; }; + 0E498AA61BC4D45200BA28F0 /* HCIsCollectionContainingInRelativeOrder.m in Sources */ = {isa = PBXBuildFile; fileRef = 0E5ECE6C1BC20C3300C15D2A /* HCIsCollectionContainingInRelativeOrder.m */; }; + 0E5ECE6D1BC20C3300C15D2A /* HCIsCollectionContainingInRelativeOrder.h in Headers */ = {isa = PBXBuildFile; fileRef = 0E5ECE6B1BC20C3300C15D2A /* HCIsCollectionContainingInRelativeOrder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0E5ECE6E1BC20C3300C15D2A /* HCIsCollectionContainingInRelativeOrder.m in Sources */ = {isa = PBXBuildFile; fileRef = 0E5ECE6C1BC20C3300C15D2A /* HCIsCollectionContainingInRelativeOrder.m */; }; + 24C5C00B1C17794900C2BAFD /* HCAssertThat.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333CF8C391D5A08F331B45 /* HCAssertThat.m */; }; + 24C5C00C1C17794900C2BAFD /* HCBaseDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333785CDF67F8F0284B9CA /* HCBaseDescription.m */; }; + 24C5C00D1C17794900C2BAFD /* HCBaseMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3338BCBEA080BE71CA0C9D /* HCBaseMatcher.m */; }; + 24C5C00E1C17794900C2BAFD /* HCDiagnosingMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 31DDCB570769099D33C22810 /* HCDiagnosingMatcher.m */; }; + 24C5C00F1C17794900C2BAFD /* HCStringDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333D43717AD7243D63A4AB /* HCStringDescription.m */; }; + 24C5C0101C17794900C2BAFD /* HCBoolReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3331B99B691F7BFEF48591 /* HCBoolReturnGetter.m */; }; + 24C5C0111C17794900C2BAFD /* HCCharReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333FED5158848B95B043F1 /* HCCharReturnGetter.m */; }; + 24C5C0121C17794900C2BAFD /* HCDoubleReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33303898210A59268030C8 /* HCDoubleReturnGetter.m */; }; + 24C5C0131C17794900C2BAFD /* HCFloatReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333C975A48CD4BC2EA4ED3 /* HCFloatReturnGetter.m */; }; + 24C5C0141C17794900C2BAFD /* HCIntReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3339614DAE31258BA73352 /* HCIntReturnGetter.m */; }; + 24C5C0151C17794900C2BAFD /* HCLongLongReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33330C459CF68E13DDD1D6 /* HCLongLongReturnGetter.m */; }; + 24C5C0161C17794900C2BAFD /* HCLongReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333334135CAFF20A0E19FF /* HCLongReturnGetter.m */; }; + 24C5C0171C17794900C2BAFD /* HCObjectReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33355AD08369793188986F /* HCObjectReturnGetter.m */; }; + 24C5C0181C17794900C2BAFD /* HCReturnValueGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3331AAA2AB993E60458B6B /* HCReturnValueGetter.m */; }; + 24C5C0191C17794900C2BAFD /* HCReturnTypeHandlerChain.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333BD87164C606AE0BE027 /* HCReturnTypeHandlerChain.m */; }; + 24C5C01A1C17794900C2BAFD /* HCShortReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33363375E9DE829A8AD41D /* HCShortReturnGetter.m */; }; + 24C5C01B1C17794900C2BAFD /* HCUnsignedCharReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333F5AFEB5565130751633 /* HCUnsignedCharReturnGetter.m */; }; + 24C5C01C1C17794900C2BAFD /* HCUnsignedIntReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333CA45EE4CF9D260DD50D /* HCUnsignedIntReturnGetter.m */; }; + 24C5C01D1C17794900C2BAFD /* HCUnsignedLongLongReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33347A9E4365F763632850 /* HCUnsignedLongLongReturnGetter.m */; }; + 24C5C01E1C17794900C2BAFD /* HCUnsignedLongReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33333B48A97B0771A3C256 /* HCUnsignedLongReturnGetter.m */; }; + 24C5C01F1C17794900C2BAFD /* HCUnsignedShortReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333A5C3A9A42E4521010F1 /* HCUnsignedShortReturnGetter.m */; }; + 24C5C0201C17794900C2BAFD /* HCGenericTestFailureReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3331E3E2C324F3D4618E9D /* HCGenericTestFailureReporter.m */; }; + 24C5C0211C17794900C2BAFD /* HCSenTestFailureReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333F6051CD0B739527337A /* HCSenTestFailureReporter.m */; }; + 24C5C0221C17794900C2BAFD /* HCTestFailure.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333F6535F43C37CBF517FA /* HCTestFailure.m */; }; + 24C5C0231C17794900C2BAFD /* HCTestFailureReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333528C8E2ED8489ACB161 /* HCTestFailureReporter.m */; }; + 24C5C0241C17794900C2BAFD /* HCTestFailureReporterChain.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3337085D9BEFA6D52E7EB0 /* HCTestFailureReporterChain.m */; }; + 24C5C0251C17794900C2BAFD /* HCXCTestFailureReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333ABD683BCE8C8E9028C5 /* HCXCTestFailureReporter.m */; }; + 24C5C0261C17794900C2BAFD /* HCCollect.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3333FD5A8D6F4A718D5D98 /* HCCollect.m */; }; + 24C5C0271C17794900C2BAFD /* HCInvocationMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3337B908D229FD08452B2D /* HCInvocationMatcher.m */; }; + 24C5C0281C17794900C2BAFD /* HCRequireNonNilObject.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333C88565D58DE2AAAAD15 /* HCRequireNonNilObject.m */; }; + 24C5C0291C17794900C2BAFD /* HCWrapInMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3335EC3CC47489B4C5395E /* HCWrapInMatcher.m */; }; + 24C5C02A1C17794900C2BAFD /* NSInvocation+OCHamcrest.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33309F9D72ED2CF53532A8 /* NSInvocation+OCHamcrest.m */; }; + 24C5C02B1C17794900C2BAFD /* HCEvery.m in Sources */ = {isa = PBXBuildFile; fileRef = 746045661A29625E00196267 /* HCEvery.m */; }; + 24C5C02C1C17794900C2BAFD /* HCHasCount.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333E594CE94324024C2E19 /* HCHasCount.m */; }; + 24C5C02D1C17794900C2BAFD /* HCIsCollectionContaining.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3338AB92DDE4ACF56D7C1B /* HCIsCollectionContaining.m */; }; + 24C5C02E1C17794900C2BAFD /* HCIsCollectionContainingInAnyOrder.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333FE413C7E8FA9C811BF1 /* HCIsCollectionContainingInAnyOrder.m */; }; + 24C5C02F1C17794900C2BAFD /* HCIsCollectionContainingInOrder.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333986B013CA09B71D376B /* HCIsCollectionContainingInOrder.m */; }; + 24C5C0301C17794900C2BAFD /* HCIsCollectionContainingInRelativeOrder.m in Sources */ = {isa = PBXBuildFile; fileRef = 0E5ECE6C1BC20C3300C15D2A /* HCIsCollectionContainingInRelativeOrder.m */; }; + 24C5C0311C17794900C2BAFD /* HCIsCollectionOnlyContaining.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33380693E900C743EDF861 /* HCIsCollectionOnlyContaining.m */; }; + 24C5C0321C17794900C2BAFD /* HCIsDictionaryContaining.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333049BE335209B4207B2A /* HCIsDictionaryContaining.m */; }; + 24C5C0331C17794900C2BAFD /* HCIsDictionaryContainingEntries.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3331619B45E4E82B0FE72A /* HCIsDictionaryContainingEntries.m */; }; + 24C5C0341C17794900C2BAFD /* HCIsDictionaryContainingKey.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333393BDEDD21E2CB1A2FF /* HCIsDictionaryContainingKey.m */; }; + 24C5C0351C17794900C2BAFD /* HCIsDictionaryContainingValue.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3339100D7159DBF2387893 /* HCIsDictionaryContainingValue.m */; }; + 24C5C0361C17794900C2BAFD /* HCIsEmptyCollection.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33328F1CBC78AC3D9B6E78 /* HCIsEmptyCollection.m */; }; + 24C5C0371C17794900C2BAFD /* HCIsIn.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333522A78D6A7D1A8414B3 /* HCIsIn.m */; }; + 24C5C0381C17794900C2BAFD /* HCDescribedAs.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3334C2039E9686ABB73AE2 /* HCDescribedAs.m */; }; + 24C5C0391C17794900C2BAFD /* HCIs.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33334930C42DF9114F9D34 /* HCIs.m */; }; + 24C5C03A1C17794900C2BAFD /* HCAllOf.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3336446380772E9629E08E /* HCAllOf.m */; }; + 24C5C03B1C17794900C2BAFD /* HCAnyOf.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333BF1C94AD91D6F5F5A95 /* HCAnyOf.m */; }; + 24C5C03C1C17794900C2BAFD /* HCIsAnything.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333AF2F081FE96420F97C0 /* HCIsAnything.m */; }; + 24C5C03D1C17794900C2BAFD /* HCIsNot.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33347EC840056B268877BF /* HCIsNot.m */; }; + 24C5C03E1C17794900C2BAFD /* HCIsCloseTo.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3331EECFE91DCA05C34EFD /* HCIsCloseTo.m */; }; + 24C5C03F1C17794900C2BAFD /* HCIsEqualToNumber.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333DC21DC44C0F76ED3DB6 /* HCIsEqualToNumber.m */; }; + 24C5C0401C17794900C2BAFD /* HCIsTrueFalse.m in Sources */ = {isa = PBXBuildFile; fileRef = 74153B621A53606100FEF450 /* HCIsTrueFalse.m */; }; + 24C5C0411C17794900C2BAFD /* HCNumberAssert.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333A801AD666BCC23ED955 /* HCNumberAssert.m */; }; + 24C5C0421C17794900C2BAFD /* HCOrderingComparison.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3336E4FAD686F2CFFF91B0 /* HCOrderingComparison.m */; }; + 24C5C0431C17794900C2BAFD /* HCArgumentCaptor.m in Sources */ = {isa = PBXBuildFile; fileRef = 609E92AABBB75D3F5E47A0E4 /* HCArgumentCaptor.m */; }; + 24C5C0441C17794900C2BAFD /* HCClassMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333C70DFAD4A867B769381 /* HCClassMatcher.m */; }; + 24C5C0451C17794900C2BAFD /* HCConformsToProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3334EF9BFFB721161BB23F /* HCConformsToProtocol.m */; }; + 24C5C0461C17794900C2BAFD /* HCHasDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3334125896535BFB7DBBA7 /* HCHasDescription.m */; }; + 24C5C0471C17794900C2BAFD /* HCHasProperty.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33343C9B176C5DE07DE003 /* HCHasProperty.m */; }; + 24C5C0481C17794900C2BAFD /* HCIsEqual.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333DFF0305457C0CD69303 /* HCIsEqual.m */; }; + 24C5C0491C17794900C2BAFD /* HCIsInstanceOf.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333830B0D43E7B79BB537C /* HCIsInstanceOf.m */; }; + 24C5C04A1C17794900C2BAFD /* HCIsNil.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3335CC804D0A774E5F1041 /* HCIsNil.m */; }; + 24C5C04B1C17794900C2BAFD /* HCIsSame.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333594FB0AC2DAC3B73077 /* HCIsSame.m */; }; + 24C5C04C1C17794900C2BAFD /* HCIsTypeOf.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33322097042E108614BDAD /* HCIsTypeOf.m */; }; + 24C5C04D1C17794900C2BAFD /* HCThrowsException.m in Sources */ = {isa = PBXBuildFile; fileRef = 74FDDD141A3FF39900177999 /* HCThrowsException.m */; }; + 24C5C04E1C17794900C2BAFD /* HCIsEqualIgnoringCase.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333A9273EFADF46D908BC4 /* HCIsEqualIgnoringCase.m */; }; + 24C5C04F1C17794900C2BAFD /* HCIsEqualCompressingWhiteSpace.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33377384F2BF4EA9694DA7 /* HCIsEqualCompressingWhiteSpace.m */; }; + 24C5C0501C17794900C2BAFD /* HCStringContains.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333EA4C4546D46CF8A4044 /* HCStringContains.m */; }; + 24C5C0511C17794900C2BAFD /* HCStringContainsInOrder.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3339B2CAF2C82161ED14F8 /* HCStringContainsInOrder.m */; }; + 24C5C0521C17794900C2BAFD /* HCStringEndsWith.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3335974ADA5C5408FE0F69 /* HCStringEndsWith.m */; }; + 24C5C0531C17794900C2BAFD /* HCStringStartsWith.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333A7430335F388E6DCF71 /* HCStringStartsWith.m */; }; + 24C5C0541C17794900C2BAFD /* HCSubstringMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33331178EE64329AB6A6DA /* HCSubstringMatcher.m */; }; + 24C5C0551C17796500C2BAFD /* HCAssertThat.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333CF8C391D5A08F331B45 /* HCAssertThat.m */; }; + 24C5C0561C17796500C2BAFD /* HCBaseDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333785CDF67F8F0284B9CA /* HCBaseDescription.m */; }; + 24C5C0571C17796500C2BAFD /* HCBaseMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3338BCBEA080BE71CA0C9D /* HCBaseMatcher.m */; }; + 24C5C0581C17796500C2BAFD /* HCDiagnosingMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 31DDCB570769099D33C22810 /* HCDiagnosingMatcher.m */; }; + 24C5C0591C17796500C2BAFD /* HCStringDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333D43717AD7243D63A4AB /* HCStringDescription.m */; }; + 24C5C05A1C17796500C2BAFD /* HCBoolReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3331B99B691F7BFEF48591 /* HCBoolReturnGetter.m */; }; + 24C5C05B1C17796500C2BAFD /* HCCharReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333FED5158848B95B043F1 /* HCCharReturnGetter.m */; }; + 24C5C05C1C17796500C2BAFD /* HCDoubleReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33303898210A59268030C8 /* HCDoubleReturnGetter.m */; }; + 24C5C05D1C17796500C2BAFD /* HCFloatReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333C975A48CD4BC2EA4ED3 /* HCFloatReturnGetter.m */; }; + 24C5C05E1C17796500C2BAFD /* HCIntReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3339614DAE31258BA73352 /* HCIntReturnGetter.m */; }; + 24C5C05F1C17796500C2BAFD /* HCLongLongReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33330C459CF68E13DDD1D6 /* HCLongLongReturnGetter.m */; }; + 24C5C0601C17796500C2BAFD /* HCLongReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333334135CAFF20A0E19FF /* HCLongReturnGetter.m */; }; + 24C5C0611C17796500C2BAFD /* HCObjectReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33355AD08369793188986F /* HCObjectReturnGetter.m */; }; + 24C5C0621C17796500C2BAFD /* HCReturnValueGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3331AAA2AB993E60458B6B /* HCReturnValueGetter.m */; }; + 24C5C0631C17796500C2BAFD /* HCReturnTypeHandlerChain.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333BD87164C606AE0BE027 /* HCReturnTypeHandlerChain.m */; }; + 24C5C0641C17796500C2BAFD /* HCShortReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33363375E9DE829A8AD41D /* HCShortReturnGetter.m */; }; + 24C5C0651C17796500C2BAFD /* HCUnsignedCharReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333F5AFEB5565130751633 /* HCUnsignedCharReturnGetter.m */; }; + 24C5C0661C17796500C2BAFD /* HCUnsignedIntReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333CA45EE4CF9D260DD50D /* HCUnsignedIntReturnGetter.m */; }; + 24C5C0671C17796500C2BAFD /* HCUnsignedLongLongReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33347A9E4365F763632850 /* HCUnsignedLongLongReturnGetter.m */; }; + 24C5C0681C17796500C2BAFD /* HCUnsignedLongReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33333B48A97B0771A3C256 /* HCUnsignedLongReturnGetter.m */; }; + 24C5C0691C17796500C2BAFD /* HCUnsignedShortReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333A5C3A9A42E4521010F1 /* HCUnsignedShortReturnGetter.m */; }; + 24C5C06A1C17796500C2BAFD /* HCGenericTestFailureReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3331E3E2C324F3D4618E9D /* HCGenericTestFailureReporter.m */; }; + 24C5C06B1C17796500C2BAFD /* HCSenTestFailureReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333F6051CD0B739527337A /* HCSenTestFailureReporter.m */; }; + 24C5C06C1C17796500C2BAFD /* HCTestFailure.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333F6535F43C37CBF517FA /* HCTestFailure.m */; }; + 24C5C06D1C17796500C2BAFD /* HCTestFailureReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333528C8E2ED8489ACB161 /* HCTestFailureReporter.m */; }; + 24C5C06E1C17796600C2BAFD /* HCTestFailureReporterChain.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3337085D9BEFA6D52E7EB0 /* HCTestFailureReporterChain.m */; }; + 24C5C06F1C17796600C2BAFD /* HCXCTestFailureReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333ABD683BCE8C8E9028C5 /* HCXCTestFailureReporter.m */; }; + 24C5C0701C17796600C2BAFD /* HCCollect.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3333FD5A8D6F4A718D5D98 /* HCCollect.m */; }; + 24C5C0711C17796600C2BAFD /* HCInvocationMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3337B908D229FD08452B2D /* HCInvocationMatcher.m */; }; + 24C5C0721C17796600C2BAFD /* HCRequireNonNilObject.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333C88565D58DE2AAAAD15 /* HCRequireNonNilObject.m */; }; + 24C5C0731C17796600C2BAFD /* HCWrapInMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3335EC3CC47489B4C5395E /* HCWrapInMatcher.m */; }; + 24C5C0741C17796600C2BAFD /* NSInvocation+OCHamcrest.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33309F9D72ED2CF53532A8 /* NSInvocation+OCHamcrest.m */; }; + 24C5C0751C17796600C2BAFD /* HCEvery.m in Sources */ = {isa = PBXBuildFile; fileRef = 746045661A29625E00196267 /* HCEvery.m */; }; + 24C5C0761C17796600C2BAFD /* HCHasCount.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333E594CE94324024C2E19 /* HCHasCount.m */; }; + 24C5C0771C17796600C2BAFD /* HCIsCollectionContaining.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3338AB92DDE4ACF56D7C1B /* HCIsCollectionContaining.m */; }; + 24C5C0781C17796600C2BAFD /* HCIsCollectionContainingInAnyOrder.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333FE413C7E8FA9C811BF1 /* HCIsCollectionContainingInAnyOrder.m */; }; + 24C5C0791C17796600C2BAFD /* HCIsCollectionContainingInOrder.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333986B013CA09B71D376B /* HCIsCollectionContainingInOrder.m */; }; + 24C5C07A1C17796600C2BAFD /* HCIsCollectionContainingInRelativeOrder.m in Sources */ = {isa = PBXBuildFile; fileRef = 0E5ECE6C1BC20C3300C15D2A /* HCIsCollectionContainingInRelativeOrder.m */; }; + 24C5C07B1C17796600C2BAFD /* HCIsCollectionOnlyContaining.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33380693E900C743EDF861 /* HCIsCollectionOnlyContaining.m */; }; + 24C5C07C1C17796600C2BAFD /* HCIsDictionaryContaining.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333049BE335209B4207B2A /* HCIsDictionaryContaining.m */; }; + 24C5C07D1C17796600C2BAFD /* HCIsDictionaryContainingEntries.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3331619B45E4E82B0FE72A /* HCIsDictionaryContainingEntries.m */; }; + 24C5C07E1C17796600C2BAFD /* HCIsDictionaryContainingKey.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333393BDEDD21E2CB1A2FF /* HCIsDictionaryContainingKey.m */; }; + 24C5C07F1C17796600C2BAFD /* HCIsDictionaryContainingValue.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3339100D7159DBF2387893 /* HCIsDictionaryContainingValue.m */; }; + 24C5C0801C17796600C2BAFD /* HCIsEmptyCollection.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33328F1CBC78AC3D9B6E78 /* HCIsEmptyCollection.m */; }; + 24C5C0811C17796600C2BAFD /* HCIsIn.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333522A78D6A7D1A8414B3 /* HCIsIn.m */; }; + 24C5C0821C17796600C2BAFD /* HCDescribedAs.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3334C2039E9686ABB73AE2 /* HCDescribedAs.m */; }; + 24C5C0831C17796600C2BAFD /* HCIs.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33334930C42DF9114F9D34 /* HCIs.m */; }; + 24C5C0841C17796600C2BAFD /* HCAllOf.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3336446380772E9629E08E /* HCAllOf.m */; }; + 24C5C0851C17796600C2BAFD /* HCAnyOf.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333BF1C94AD91D6F5F5A95 /* HCAnyOf.m */; }; + 24C5C0861C17796600C2BAFD /* HCIsAnything.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333AF2F081FE96420F97C0 /* HCIsAnything.m */; }; + 24C5C0871C17796600C2BAFD /* HCIsNot.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33347EC840056B268877BF /* HCIsNot.m */; }; + 24C5C0881C17796600C2BAFD /* HCIsCloseTo.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3331EECFE91DCA05C34EFD /* HCIsCloseTo.m */; }; + 24C5C0891C17796600C2BAFD /* HCIsEqualToNumber.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333DC21DC44C0F76ED3DB6 /* HCIsEqualToNumber.m */; }; + 24C5C08A1C17796600C2BAFD /* HCIsTrueFalse.m in Sources */ = {isa = PBXBuildFile; fileRef = 74153B621A53606100FEF450 /* HCIsTrueFalse.m */; }; + 24C5C08B1C17796600C2BAFD /* HCNumberAssert.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333A801AD666BCC23ED955 /* HCNumberAssert.m */; }; + 24C5C08C1C17796600C2BAFD /* HCOrderingComparison.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3336E4FAD686F2CFFF91B0 /* HCOrderingComparison.m */; }; + 24C5C08D1C17796600C2BAFD /* HCArgumentCaptor.m in Sources */ = {isa = PBXBuildFile; fileRef = 609E92AABBB75D3F5E47A0E4 /* HCArgumentCaptor.m */; }; + 24C5C08E1C17796600C2BAFD /* HCClassMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333C70DFAD4A867B769381 /* HCClassMatcher.m */; }; + 24C5C08F1C17796600C2BAFD /* HCConformsToProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3334EF9BFFB721161BB23F /* HCConformsToProtocol.m */; }; + 24C5C0901C17796600C2BAFD /* HCHasDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3334125896535BFB7DBBA7 /* HCHasDescription.m */; }; + 24C5C0911C17796600C2BAFD /* HCHasProperty.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33343C9B176C5DE07DE003 /* HCHasProperty.m */; }; + 24C5C0921C17796600C2BAFD /* HCIsEqual.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333DFF0305457C0CD69303 /* HCIsEqual.m */; }; + 24C5C0931C17796600C2BAFD /* HCIsInstanceOf.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333830B0D43E7B79BB537C /* HCIsInstanceOf.m */; }; + 24C5C0941C17796600C2BAFD /* HCIsNil.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3335CC804D0A774E5F1041 /* HCIsNil.m */; }; + 24C5C0951C17796600C2BAFD /* HCIsSame.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333594FB0AC2DAC3B73077 /* HCIsSame.m */; }; + 24C5C0961C17796600C2BAFD /* HCIsTypeOf.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33322097042E108614BDAD /* HCIsTypeOf.m */; }; + 24C5C0971C17796600C2BAFD /* HCThrowsException.m in Sources */ = {isa = PBXBuildFile; fileRef = 74FDDD141A3FF39900177999 /* HCThrowsException.m */; }; + 24C5C0981C17796600C2BAFD /* HCIsEqualIgnoringCase.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333A9273EFADF46D908BC4 /* HCIsEqualIgnoringCase.m */; }; + 24C5C0991C17796600C2BAFD /* HCIsEqualCompressingWhiteSpace.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33377384F2BF4EA9694DA7 /* HCIsEqualCompressingWhiteSpace.m */; }; + 24C5C09A1C17796600C2BAFD /* HCStringContains.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333EA4C4546D46CF8A4044 /* HCStringContains.m */; }; + 24C5C09B1C17796600C2BAFD /* HCStringContainsInOrder.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3339B2CAF2C82161ED14F8 /* HCStringContainsInOrder.m */; }; + 24C5C09C1C17796600C2BAFD /* HCStringEndsWith.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3335974ADA5C5408FE0F69 /* HCStringEndsWith.m */; }; + 24C5C09D1C17796600C2BAFD /* HCStringStartsWith.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333A7430335F388E6DCF71 /* HCStringStartsWith.m */; }; + 24C5C09E1C17796600C2BAFD /* HCSubstringMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33331178EE64329AB6A6DA /* HCSubstringMatcher.m */; }; + 24C5C09F1C17797200C2BAFD /* HCAssertThat.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333CF8C391D5A08F331B45 /* HCAssertThat.m */; }; + 24C5C0A01C17797200C2BAFD /* HCBaseDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333785CDF67F8F0284B9CA /* HCBaseDescription.m */; }; + 24C5C0A11C17797200C2BAFD /* HCBaseMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3338BCBEA080BE71CA0C9D /* HCBaseMatcher.m */; }; + 24C5C0A21C17797200C2BAFD /* HCDiagnosingMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 31DDCB570769099D33C22810 /* HCDiagnosingMatcher.m */; }; + 24C5C0A31C17797200C2BAFD /* HCStringDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333D43717AD7243D63A4AB /* HCStringDescription.m */; }; + 24C5C0A41C17797200C2BAFD /* HCBoolReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3331B99B691F7BFEF48591 /* HCBoolReturnGetter.m */; }; + 24C5C0A51C17797200C2BAFD /* HCCharReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333FED5158848B95B043F1 /* HCCharReturnGetter.m */; }; + 24C5C0A61C17797200C2BAFD /* HCDoubleReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33303898210A59268030C8 /* HCDoubleReturnGetter.m */; }; + 24C5C0A71C17797200C2BAFD /* HCFloatReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333C975A48CD4BC2EA4ED3 /* HCFloatReturnGetter.m */; }; + 24C5C0A81C17797200C2BAFD /* HCIntReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3339614DAE31258BA73352 /* HCIntReturnGetter.m */; }; + 24C5C0A91C17797200C2BAFD /* HCLongLongReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33330C459CF68E13DDD1D6 /* HCLongLongReturnGetter.m */; }; + 24C5C0AA1C17797200C2BAFD /* HCLongReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333334135CAFF20A0E19FF /* HCLongReturnGetter.m */; }; + 24C5C0AB1C17797200C2BAFD /* HCObjectReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33355AD08369793188986F /* HCObjectReturnGetter.m */; }; + 24C5C0AC1C17797200C2BAFD /* HCReturnValueGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3331AAA2AB993E60458B6B /* HCReturnValueGetter.m */; }; + 24C5C0AD1C17797200C2BAFD /* HCReturnTypeHandlerChain.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333BD87164C606AE0BE027 /* HCReturnTypeHandlerChain.m */; }; + 24C5C0AE1C17797200C2BAFD /* HCShortReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33363375E9DE829A8AD41D /* HCShortReturnGetter.m */; }; + 24C5C0AF1C17797200C2BAFD /* HCUnsignedCharReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333F5AFEB5565130751633 /* HCUnsignedCharReturnGetter.m */; }; + 24C5C0B01C17797200C2BAFD /* HCUnsignedIntReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333CA45EE4CF9D260DD50D /* HCUnsignedIntReturnGetter.m */; }; + 24C5C0B11C17797200C2BAFD /* HCUnsignedLongLongReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33347A9E4365F763632850 /* HCUnsignedLongLongReturnGetter.m */; }; + 24C5C0B21C17797200C2BAFD /* HCUnsignedLongReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33333B48A97B0771A3C256 /* HCUnsignedLongReturnGetter.m */; }; + 24C5C0B31C17797200C2BAFD /* HCUnsignedShortReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333A5C3A9A42E4521010F1 /* HCUnsignedShortReturnGetter.m */; }; + 24C5C0B41C17797200C2BAFD /* HCGenericTestFailureReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3331E3E2C324F3D4618E9D /* HCGenericTestFailureReporter.m */; }; + 24C5C0B51C17797200C2BAFD /* HCSenTestFailureReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333F6051CD0B739527337A /* HCSenTestFailureReporter.m */; }; + 24C5C0B61C17797200C2BAFD /* HCTestFailure.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333F6535F43C37CBF517FA /* HCTestFailure.m */; }; + 24C5C0B71C17797200C2BAFD /* HCTestFailureReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333528C8E2ED8489ACB161 /* HCTestFailureReporter.m */; }; + 24C5C0B81C17797200C2BAFD /* HCTestFailureReporterChain.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3337085D9BEFA6D52E7EB0 /* HCTestFailureReporterChain.m */; }; + 24C5C0B91C17797200C2BAFD /* HCXCTestFailureReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333ABD683BCE8C8E9028C5 /* HCXCTestFailureReporter.m */; }; + 24C5C0BA1C17797200C2BAFD /* HCCollect.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3333FD5A8D6F4A718D5D98 /* HCCollect.m */; }; + 24C5C0BB1C17797200C2BAFD /* HCInvocationMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3337B908D229FD08452B2D /* HCInvocationMatcher.m */; }; + 24C5C0BC1C17797200C2BAFD /* HCRequireNonNilObject.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333C88565D58DE2AAAAD15 /* HCRequireNonNilObject.m */; }; + 24C5C0BD1C17797200C2BAFD /* HCWrapInMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3335EC3CC47489B4C5395E /* HCWrapInMatcher.m */; }; + 24C5C0BE1C17797200C2BAFD /* NSInvocation+OCHamcrest.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33309F9D72ED2CF53532A8 /* NSInvocation+OCHamcrest.m */; }; + 24C5C0BF1C17797200C2BAFD /* HCEvery.m in Sources */ = {isa = PBXBuildFile; fileRef = 746045661A29625E00196267 /* HCEvery.m */; }; + 24C5C0C01C17797200C2BAFD /* HCHasCount.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333E594CE94324024C2E19 /* HCHasCount.m */; }; + 24C5C0C11C17797200C2BAFD /* HCIsCollectionContaining.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3338AB92DDE4ACF56D7C1B /* HCIsCollectionContaining.m */; }; + 24C5C0C21C17797200C2BAFD /* HCIsCollectionContainingInAnyOrder.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333FE413C7E8FA9C811BF1 /* HCIsCollectionContainingInAnyOrder.m */; }; + 24C5C0C31C17797200C2BAFD /* HCIsCollectionContainingInOrder.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333986B013CA09B71D376B /* HCIsCollectionContainingInOrder.m */; }; + 24C5C0C41C17797200C2BAFD /* HCIsCollectionContainingInRelativeOrder.m in Sources */ = {isa = PBXBuildFile; fileRef = 0E5ECE6C1BC20C3300C15D2A /* HCIsCollectionContainingInRelativeOrder.m */; }; + 24C5C0C51C17797200C2BAFD /* HCIsCollectionOnlyContaining.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33380693E900C743EDF861 /* HCIsCollectionOnlyContaining.m */; }; + 24C5C0C61C17797200C2BAFD /* HCIsDictionaryContaining.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333049BE335209B4207B2A /* HCIsDictionaryContaining.m */; }; + 24C5C0C71C17797200C2BAFD /* HCIsDictionaryContainingEntries.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3331619B45E4E82B0FE72A /* HCIsDictionaryContainingEntries.m */; }; + 24C5C0C81C17797200C2BAFD /* HCIsDictionaryContainingKey.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333393BDEDD21E2CB1A2FF /* HCIsDictionaryContainingKey.m */; }; + 24C5C0C91C17797200C2BAFD /* HCIsDictionaryContainingValue.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3339100D7159DBF2387893 /* HCIsDictionaryContainingValue.m */; }; + 24C5C0CA1C17797200C2BAFD /* HCIsEmptyCollection.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33328F1CBC78AC3D9B6E78 /* HCIsEmptyCollection.m */; }; + 24C5C0CB1C17797200C2BAFD /* HCIsIn.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333522A78D6A7D1A8414B3 /* HCIsIn.m */; }; + 24C5C0CC1C17797200C2BAFD /* HCDescribedAs.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3334C2039E9686ABB73AE2 /* HCDescribedAs.m */; }; + 24C5C0CD1C17797200C2BAFD /* HCIs.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33334930C42DF9114F9D34 /* HCIs.m */; }; + 24C5C0CE1C17797200C2BAFD /* HCAllOf.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3336446380772E9629E08E /* HCAllOf.m */; }; + 24C5C0CF1C17797200C2BAFD /* HCAnyOf.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333BF1C94AD91D6F5F5A95 /* HCAnyOf.m */; }; + 24C5C0D01C17797200C2BAFD /* HCIsAnything.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333AF2F081FE96420F97C0 /* HCIsAnything.m */; }; + 24C5C0D11C17797200C2BAFD /* HCIsNot.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33347EC840056B268877BF /* HCIsNot.m */; }; + 24C5C0D21C17797200C2BAFD /* HCIsCloseTo.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3331EECFE91DCA05C34EFD /* HCIsCloseTo.m */; }; + 24C5C0D31C17797200C2BAFD /* HCIsEqualToNumber.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333DC21DC44C0F76ED3DB6 /* HCIsEqualToNumber.m */; }; + 24C5C0D41C17797200C2BAFD /* HCIsTrueFalse.m in Sources */ = {isa = PBXBuildFile; fileRef = 74153B621A53606100FEF450 /* HCIsTrueFalse.m */; }; + 24C5C0D51C17797200C2BAFD /* HCNumberAssert.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333A801AD666BCC23ED955 /* HCNumberAssert.m */; }; + 24C5C0D61C17797200C2BAFD /* HCOrderingComparison.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3336E4FAD686F2CFFF91B0 /* HCOrderingComparison.m */; }; + 24C5C0D71C17797200C2BAFD /* HCArgumentCaptor.m in Sources */ = {isa = PBXBuildFile; fileRef = 609E92AABBB75D3F5E47A0E4 /* HCArgumentCaptor.m */; }; + 24C5C0D81C17797200C2BAFD /* HCClassMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333C70DFAD4A867B769381 /* HCClassMatcher.m */; }; + 24C5C0D91C17797200C2BAFD /* HCConformsToProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3334EF9BFFB721161BB23F /* HCConformsToProtocol.m */; }; + 24C5C0DA1C17797200C2BAFD /* HCHasDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3334125896535BFB7DBBA7 /* HCHasDescription.m */; }; + 24C5C0DB1C17797200C2BAFD /* HCHasProperty.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33343C9B176C5DE07DE003 /* HCHasProperty.m */; }; + 24C5C0DC1C17797200C2BAFD /* HCIsEqual.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333DFF0305457C0CD69303 /* HCIsEqual.m */; }; + 24C5C0DD1C17797200C2BAFD /* HCIsInstanceOf.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333830B0D43E7B79BB537C /* HCIsInstanceOf.m */; }; + 24C5C0DE1C17797200C2BAFD /* HCIsNil.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3335CC804D0A774E5F1041 /* HCIsNil.m */; }; + 24C5C0DF1C17797200C2BAFD /* HCIsSame.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333594FB0AC2DAC3B73077 /* HCIsSame.m */; }; + 24C5C0E01C17797200C2BAFD /* HCIsTypeOf.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33322097042E108614BDAD /* HCIsTypeOf.m */; }; + 24C5C0E11C17797200C2BAFD /* HCThrowsException.m in Sources */ = {isa = PBXBuildFile; fileRef = 74FDDD141A3FF39900177999 /* HCThrowsException.m */; }; + 24C5C0E21C17797200C2BAFD /* HCIsEqualIgnoringCase.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333A9273EFADF46D908BC4 /* HCIsEqualIgnoringCase.m */; }; + 24C5C0E31C17797200C2BAFD /* HCIsEqualCompressingWhiteSpace.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33377384F2BF4EA9694DA7 /* HCIsEqualCompressingWhiteSpace.m */; }; + 24C5C0E41C17797200C2BAFD /* HCStringContains.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333EA4C4546D46CF8A4044 /* HCStringContains.m */; }; + 24C5C0E51C17797200C2BAFD /* HCStringContainsInOrder.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3339B2CAF2C82161ED14F8 /* HCStringContainsInOrder.m */; }; + 24C5C0E61C17797200C2BAFD /* HCStringEndsWith.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3335974ADA5C5408FE0F69 /* HCStringEndsWith.m */; }; + 24C5C0E71C17797200C2BAFD /* HCStringStartsWith.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333A7430335F388E6DCF71 /* HCStringStartsWith.m */; }; + 24C5C0E81C17797200C2BAFD /* HCSubstringMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33331178EE64329AB6A6DA /* HCSubstringMatcher.m */; }; + 24C5C1391C177F1B00C2BAFD /* OCHamcrest.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33356A6185FA4F1B6E240F /* OCHamcrest.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C13A1C177F1B00C2BAFD /* HCAssertThat.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333792C123FE94F1A3D447 /* HCAssertThat.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C13B1C177F1B00C2BAFD /* HCBaseDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333D869F9ECB46825CA726 /* HCBaseDescription.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C13C1C177F1B00C2BAFD /* HCBaseMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333E53F31A4853CD1B9548 /* HCBaseMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C13D1C177F1B00C2BAFD /* HCDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333D2D401EA09BE78BF7E9 /* HCDescription.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C13E1C177F1B00C2BAFD /* HCDiagnosingMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 31DDCE62F97F2042F0A8BDAE /* HCDiagnosingMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C13F1C177F1B00C2BAFD /* HCMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3334F31A5AB57A69174ECB /* HCMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1401C177F1B00C2BAFD /* HCSelfDescribing.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333B7F19A5645CF7FC3E7E /* HCSelfDescribing.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1411C177F1B00C2BAFD /* HCStringDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333215A6C1406E57ED34C4 /* HCStringDescription.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1421C177F1B00C2BAFD /* HCBoolReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333EDC6C01648EEBC00D5E /* HCBoolReturnGetter.h */; }; + 24C5C1431C177F1B00C2BAFD /* HCCharReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333C5BD261A2F9861F78A4 /* HCCharReturnGetter.h */; }; + 24C5C1441C177F1B00C2BAFD /* HCDoubleReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333F67A18EA568D7683A58 /* HCDoubleReturnGetter.h */; }; + 24C5C1451C177F1B00C2BAFD /* HCFloatReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333FF31E6FD6CB3A0DC45E /* HCFloatReturnGetter.h */; }; + 24C5C1461C177F1B00C2BAFD /* HCIntReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333725B2814AE9CF044B5B /* HCIntReturnGetter.h */; }; + 24C5C1471C177F1B00C2BAFD /* HCLongLongReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3332508DE228EEA28B7324 /* HCLongLongReturnGetter.h */; }; + 24C5C1481C177F1B00C2BAFD /* HCLongReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333C3F40A01E4269FA22BE /* HCLongReturnGetter.h */; }; + 24C5C1491C177F1B00C2BAFD /* HCObjectReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333FACCFA7D57857673665 /* HCObjectReturnGetter.h */; }; + 24C5C14A1C177F1B00C2BAFD /* HCReturnValueGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333CC6471D7E5CB41558EF /* HCReturnValueGetter.h */; }; + 24C5C14B1C177F1B00C2BAFD /* HCReturnTypeHandlerChain.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333191F9604EA23746459E /* HCReturnTypeHandlerChain.h */; }; + 24C5C14C1C177F1B00C2BAFD /* HCShortReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3335A8FE1E47B0F4407027 /* HCShortReturnGetter.h */; }; + 24C5C14D1C177F1B00C2BAFD /* HCUnsignedCharReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333BE1D8EFF4596D019A40 /* HCUnsignedCharReturnGetter.h */; }; + 24C5C14E1C177F1B00C2BAFD /* HCUnsignedIntReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331B51251AD3B252636B5 /* HCUnsignedIntReturnGetter.h */; }; + 24C5C14F1C177F1B00C2BAFD /* HCUnsignedLongLongReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33312F8485C256B14867AF /* HCUnsignedLongLongReturnGetter.h */; }; + 24C5C1501C177F1B00C2BAFD /* HCUnsignedLongReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3338DE3E5C649EB399B649 /* HCUnsignedLongReturnGetter.h */; }; + 24C5C1511C177F1B00C2BAFD /* HCUnsignedShortReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333A258B9C1CDFB1B184C3 /* HCUnsignedShortReturnGetter.h */; }; + 24C5C1521C177F1B00C2BAFD /* HCGenericTestFailureReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33391EC19272E15C3D2E1A /* HCGenericTestFailureReporter.h */; }; + 24C5C1531C177F1B00C2BAFD /* HCSenTestFailureReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33339BAC2DACD2306832A8 /* HCSenTestFailureReporter.h */; }; + 24C5C1541C177F1B00C2BAFD /* HCTestFailure.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333930FE07807045EEE866 /* HCTestFailure.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1551C177F1C00C2BAFD /* HCTestFailureReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3337F8ADEA5FCE59018962 /* HCTestFailureReporter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1561C177F1C00C2BAFD /* HCTestFailureReporterChain.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3336D2152CF1F88C8FF743 /* HCTestFailureReporterChain.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1571C177F1C00C2BAFD /* HCXCTestFailureReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33394F75CEE379A525320B /* HCXCTestFailureReporter.h */; }; + 24C5C1581C177F1C00C2BAFD /* HCCollect.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333C7DFDB2404398E9E6C5 /* HCCollect.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1591C177F1C00C2BAFD /* HCInvocationMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333D00EB0E21D420A7B22B /* HCInvocationMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C15A1C177F1C00C2BAFD /* HCRequireNonNilObject.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331127524965C9BCAD83A /* HCRequireNonNilObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C15B1C177F1C00C2BAFD /* HCWrapInMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333D5C9BD053542A0530EB /* HCWrapInMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C15C1C177F1C00C2BAFD /* NSInvocation+OCHamcrest.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3339BE4C0ECEAB7169772A /* NSInvocation+OCHamcrest.h */; }; + 24C5C15D1C177F1C00C2BAFD /* HCEvery.h in Headers */ = {isa = PBXBuildFile; fileRef = 746045651A29625E00196267 /* HCEvery.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C15E1C177F1C00C2BAFD /* HCHasCount.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331C8CBB96396DDA74467 /* HCHasCount.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C15F1C177F1C00C2BAFD /* HCIsCollectionContaining.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331A34E31C49FE230D9BE /* HCIsCollectionContaining.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1601C177F1C00C2BAFD /* HCIsCollectionContainingInAnyOrder.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3332CD4593BA0B619AE30C /* HCIsCollectionContainingInAnyOrder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1611C177F1C00C2BAFD /* HCIsCollectionContainingInOrder.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33312A13BF3686BE841AD7 /* HCIsCollectionContainingInOrder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1621C177F1C00C2BAFD /* HCIsCollectionContainingInRelativeOrder.h in Headers */ = {isa = PBXBuildFile; fileRef = 0E5ECE6B1BC20C3300C15D2A /* HCIsCollectionContainingInRelativeOrder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1631C177F1C00C2BAFD /* HCIsCollectionOnlyContaining.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333BC37F9AB8BFAB79AE16 /* HCIsCollectionOnlyContaining.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1641C177F1C00C2BAFD /* HCIsDictionaryContaining.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333FDE3455183AB9302310 /* HCIsDictionaryContaining.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1651C177F1C00C2BAFD /* HCIsDictionaryContainingEntries.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333F8E4CF5AD791C99B6FA /* HCIsDictionaryContainingEntries.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1661C177F1D00C2BAFD /* HCIsDictionaryContainingKey.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333E412026940E16FEC9C0 /* HCIsDictionaryContainingKey.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1671C177F1D00C2BAFD /* HCIsDictionaryContainingValue.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333A5C2975038FF06BDCBA /* HCIsDictionaryContainingValue.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1681C177F1D00C2BAFD /* HCIsEmptyCollection.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3338EDDD9B8C82083DD7FB /* HCIsEmptyCollection.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1691C177F1D00C2BAFD /* HCIsIn.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3336DDA8D0B1E91E937FD4 /* HCIsIn.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C16A1C177F1D00C2BAFD /* HCDescribedAs.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331D467F6B759BE9C3246 /* HCDescribedAs.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C16B1C177F1D00C2BAFD /* HCIs.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33384895217507C28D0896 /* HCIs.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C16C1C177F1D00C2BAFD /* HCAllOf.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333255C8855929B9264684 /* HCAllOf.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C16D1C177F1D00C2BAFD /* HCAnyOf.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333034CD6A38647E176A9A /* HCAnyOf.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C16E1C177F1D00C2BAFD /* HCIsAnything.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333EC183D2675A9DF05F67 /* HCIsAnything.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C16F1C177F1D00C2BAFD /* HCIsNot.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33317270E4A854112BCCEA /* HCIsNot.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1701C177F1D00C2BAFD /* HCIsCloseTo.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33346362D58A13AA1BA54B /* HCIsCloseTo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1711C177F1E00C2BAFD /* HCIsEqualToNumber.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3330701E28789385EAB1C9 /* HCIsEqualToNumber.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1721C177F1E00C2BAFD /* HCIsTrueFalse.h in Headers */ = {isa = PBXBuildFile; fileRef = 74153B611A53606100FEF450 /* HCIsTrueFalse.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1731C177F1E00C2BAFD /* HCNumberAssert.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333EEBE8C48A68933572E9 /* HCNumberAssert.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1741C177F1E00C2BAFD /* HCOrderingComparison.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3337CE41E8621958EAE8BF /* HCOrderingComparison.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1751C177F1E00C2BAFD /* HCArgumentCaptor.h in Headers */ = {isa = PBXBuildFile; fileRef = 609E9B6B3B80E4914607E267 /* HCArgumentCaptor.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1761C177F1E00C2BAFD /* HCClassMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33365155F84099730C9B56 /* HCClassMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1771C177F1E00C2BAFD /* HCConformsToProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3334410C93469CF325AA1D /* HCConformsToProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1781C177F1E00C2BAFD /* HCHasDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33387896CF4C830AAE4633 /* HCHasDescription.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1791C177F1E00C2BAFD /* HCHasProperty.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333B2A6FFEC019E0678999 /* HCHasProperty.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C17A1C177F1F00C2BAFD /* HCIsEqual.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333503A24A236A19B8A468 /* HCIsEqual.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C17B1C177F1F00C2BAFD /* HCIsInstanceOf.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33386A5E683DD0DC1AACE2 /* HCIsInstanceOf.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C17C1C177F1F00C2BAFD /* HCIsNil.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33359133039118AE88E2A8 /* HCIsNil.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C17D1C177F1F00C2BAFD /* HCIsSame.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333C93865B64F27B2639FE /* HCIsSame.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C17E1C177F1F00C2BAFD /* HCIsTypeOf.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33334CF68184328CD67F30 /* HCIsTypeOf.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C17F1C177F1F00C2BAFD /* HCThrowsException.h in Headers */ = {isa = PBXBuildFile; fileRef = 74FDDD131A3FF39900177999 /* HCThrowsException.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1801C177F1F00C2BAFD /* HCIsEqualIgnoringCase.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33371136854FB7CAF2A5F7 /* HCIsEqualIgnoringCase.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1811C177F1F00C2BAFD /* HCIsEqualCompressingWhiteSpace.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33338462C2743FF78AA51B /* HCIsEqualCompressingWhiteSpace.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1821C177F2000C2BAFD /* HCStringContains.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333144F9FA03AC04F8027D /* HCStringContains.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1831C177F2000C2BAFD /* HCStringContainsInOrder.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331C74ACFA7AB0675A7FE /* HCStringContainsInOrder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1841C177F2000C2BAFD /* HCStringEndsWith.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3333AF4B760A982CF9F9E3 /* HCStringEndsWith.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1851C177F2000C2BAFD /* HCStringStartsWith.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3336FA0B545673BD3630E2 /* HCStringStartsWith.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1861C177F2000C2BAFD /* HCSubstringMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3338B60F3A83AE527E91A7 /* HCSubstringMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1871C177F5800C2BAFD /* OCHamcrest.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33356A6185FA4F1B6E240F /* OCHamcrest.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1881C177F5800C2BAFD /* HCAssertThat.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333792C123FE94F1A3D447 /* HCAssertThat.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1891C177F5800C2BAFD /* HCBaseDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333D869F9ECB46825CA726 /* HCBaseDescription.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C18A1C177F5800C2BAFD /* HCBaseMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333E53F31A4853CD1B9548 /* HCBaseMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C18B1C177F5800C2BAFD /* HCDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333D2D401EA09BE78BF7E9 /* HCDescription.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C18C1C177F5800C2BAFD /* HCDiagnosingMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 31DDCE62F97F2042F0A8BDAE /* HCDiagnosingMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C18D1C177F5800C2BAFD /* HCMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3334F31A5AB57A69174ECB /* HCMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C18E1C177F5800C2BAFD /* HCSelfDescribing.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333B7F19A5645CF7FC3E7E /* HCSelfDescribing.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C18F1C177F5800C2BAFD /* HCStringDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333215A6C1406E57ED34C4 /* HCStringDescription.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1901C177F5800C2BAFD /* HCBoolReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333EDC6C01648EEBC00D5E /* HCBoolReturnGetter.h */; }; + 24C5C1911C177F5800C2BAFD /* HCCharReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333C5BD261A2F9861F78A4 /* HCCharReturnGetter.h */; }; + 24C5C1921C177F5800C2BAFD /* HCDoubleReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333F67A18EA568D7683A58 /* HCDoubleReturnGetter.h */; }; + 24C5C1931C177F5800C2BAFD /* HCFloatReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333FF31E6FD6CB3A0DC45E /* HCFloatReturnGetter.h */; }; + 24C5C1941C177F5800C2BAFD /* HCIntReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333725B2814AE9CF044B5B /* HCIntReturnGetter.h */; }; + 24C5C1951C177F5800C2BAFD /* HCLongLongReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3332508DE228EEA28B7324 /* HCLongLongReturnGetter.h */; }; + 24C5C1961C177F5800C2BAFD /* HCLongReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333C3F40A01E4269FA22BE /* HCLongReturnGetter.h */; }; + 24C5C1971C177F5800C2BAFD /* HCObjectReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333FACCFA7D57857673665 /* HCObjectReturnGetter.h */; }; + 24C5C1981C177F5800C2BAFD /* HCReturnValueGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333CC6471D7E5CB41558EF /* HCReturnValueGetter.h */; }; + 24C5C1991C177F5800C2BAFD /* HCReturnTypeHandlerChain.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333191F9604EA23746459E /* HCReturnTypeHandlerChain.h */; }; + 24C5C19A1C177F5800C2BAFD /* HCShortReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3335A8FE1E47B0F4407027 /* HCShortReturnGetter.h */; }; + 24C5C19B1C177F5800C2BAFD /* HCUnsignedCharReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333BE1D8EFF4596D019A40 /* HCUnsignedCharReturnGetter.h */; }; + 24C5C19C1C177F5800C2BAFD /* HCUnsignedIntReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331B51251AD3B252636B5 /* HCUnsignedIntReturnGetter.h */; }; + 24C5C19D1C177F5800C2BAFD /* HCUnsignedLongLongReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33312F8485C256B14867AF /* HCUnsignedLongLongReturnGetter.h */; }; + 24C5C19E1C177F5800C2BAFD /* HCUnsignedLongReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3338DE3E5C649EB399B649 /* HCUnsignedLongReturnGetter.h */; }; + 24C5C19F1C177F5800C2BAFD /* HCUnsignedShortReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333A258B9C1CDFB1B184C3 /* HCUnsignedShortReturnGetter.h */; }; + 24C5C1A01C177F5800C2BAFD /* HCGenericTestFailureReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33391EC19272E15C3D2E1A /* HCGenericTestFailureReporter.h */; }; + 24C5C1A11C177F5800C2BAFD /* HCSenTestFailureReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33339BAC2DACD2306832A8 /* HCSenTestFailureReporter.h */; }; + 24C5C1A21C177F5800C2BAFD /* HCTestFailure.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333930FE07807045EEE866 /* HCTestFailure.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1A31C177F5800C2BAFD /* HCTestFailureReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3337F8ADEA5FCE59018962 /* HCTestFailureReporter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1A41C177F5800C2BAFD /* HCTestFailureReporterChain.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3336D2152CF1F88C8FF743 /* HCTestFailureReporterChain.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1A51C177F5800C2BAFD /* HCXCTestFailureReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33394F75CEE379A525320B /* HCXCTestFailureReporter.h */; }; + 24C5C1A61C177F5800C2BAFD /* HCCollect.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333C7DFDB2404398E9E6C5 /* HCCollect.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1A71C177F5800C2BAFD /* HCInvocationMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333D00EB0E21D420A7B22B /* HCInvocationMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1A81C177F5900C2BAFD /* HCRequireNonNilObject.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331127524965C9BCAD83A /* HCRequireNonNilObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1A91C177F5900C2BAFD /* HCWrapInMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333D5C9BD053542A0530EB /* HCWrapInMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1AA1C177F5900C2BAFD /* NSInvocation+OCHamcrest.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3339BE4C0ECEAB7169772A /* NSInvocation+OCHamcrest.h */; }; + 24C5C1AB1C177F5900C2BAFD /* HCEvery.h in Headers */ = {isa = PBXBuildFile; fileRef = 746045651A29625E00196267 /* HCEvery.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1AC1C177F5900C2BAFD /* HCHasCount.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331C8CBB96396DDA74467 /* HCHasCount.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1AD1C177F5900C2BAFD /* HCIsCollectionContaining.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331A34E31C49FE230D9BE /* HCIsCollectionContaining.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1AE1C177F5900C2BAFD /* HCIsCollectionContainingInAnyOrder.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3332CD4593BA0B619AE30C /* HCIsCollectionContainingInAnyOrder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1AF1C177F5900C2BAFD /* HCIsCollectionContainingInOrder.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33312A13BF3686BE841AD7 /* HCIsCollectionContainingInOrder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1B01C177F5900C2BAFD /* HCIsCollectionContainingInRelativeOrder.h in Headers */ = {isa = PBXBuildFile; fileRef = 0E5ECE6B1BC20C3300C15D2A /* HCIsCollectionContainingInRelativeOrder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1B11C177F5900C2BAFD /* HCIsCollectionOnlyContaining.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333BC37F9AB8BFAB79AE16 /* HCIsCollectionOnlyContaining.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1B21C177F5900C2BAFD /* HCIsDictionaryContaining.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333FDE3455183AB9302310 /* HCIsDictionaryContaining.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1B31C177F5900C2BAFD /* HCIsDictionaryContainingEntries.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333F8E4CF5AD791C99B6FA /* HCIsDictionaryContainingEntries.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1B41C177F5900C2BAFD /* HCIsDictionaryContainingKey.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333E412026940E16FEC9C0 /* HCIsDictionaryContainingKey.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1B51C177F5900C2BAFD /* HCIsDictionaryContainingValue.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333A5C2975038FF06BDCBA /* HCIsDictionaryContainingValue.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1B61C177F5A00C2BAFD /* HCIsEmptyCollection.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3338EDDD9B8C82083DD7FB /* HCIsEmptyCollection.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1B71C177F5A00C2BAFD /* HCIsIn.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3336DDA8D0B1E91E937FD4 /* HCIsIn.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1B81C177F5A00C2BAFD /* HCDescribedAs.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331D467F6B759BE9C3246 /* HCDescribedAs.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1B91C177F5A00C2BAFD /* HCIs.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33384895217507C28D0896 /* HCIs.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1BA1C177F5A00C2BAFD /* HCAllOf.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333255C8855929B9264684 /* HCAllOf.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1BB1C177F5A00C2BAFD /* HCAnyOf.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333034CD6A38647E176A9A /* HCAnyOf.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1BC1C177F5A00C2BAFD /* HCIsAnything.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333EC183D2675A9DF05F67 /* HCIsAnything.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1BD1C177F5A00C2BAFD /* HCIsNot.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33317270E4A854112BCCEA /* HCIsNot.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1BE1C177F5A00C2BAFD /* HCIsCloseTo.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33346362D58A13AA1BA54B /* HCIsCloseTo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1BF1C177F5A00C2BAFD /* HCIsEqualToNumber.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3330701E28789385EAB1C9 /* HCIsEqualToNumber.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1C01C177F5A00C2BAFD /* HCIsTrueFalse.h in Headers */ = {isa = PBXBuildFile; fileRef = 74153B611A53606100FEF450 /* HCIsTrueFalse.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1C11C177F5A00C2BAFD /* HCNumberAssert.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333EEBE8C48A68933572E9 /* HCNumberAssert.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1C21C177F5B00C2BAFD /* HCOrderingComparison.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3337CE41E8621958EAE8BF /* HCOrderingComparison.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1C31C177F5B00C2BAFD /* HCArgumentCaptor.h in Headers */ = {isa = PBXBuildFile; fileRef = 609E9B6B3B80E4914607E267 /* HCArgumentCaptor.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1C41C177F5B00C2BAFD /* HCClassMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33365155F84099730C9B56 /* HCClassMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1C51C177F5B00C2BAFD /* HCConformsToProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3334410C93469CF325AA1D /* HCConformsToProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1C61C177F5B00C2BAFD /* HCHasDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33387896CF4C830AAE4633 /* HCHasDescription.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1C71C177F5B00C2BAFD /* HCHasProperty.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333B2A6FFEC019E0678999 /* HCHasProperty.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1C81C177F5B00C2BAFD /* HCIsEqual.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333503A24A236A19B8A468 /* HCIsEqual.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1C91C177F5B00C2BAFD /* HCIsInstanceOf.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33386A5E683DD0DC1AACE2 /* HCIsInstanceOf.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1CA1C177F5B00C2BAFD /* HCIsNil.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33359133039118AE88E2A8 /* HCIsNil.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1CB1C177F5C00C2BAFD /* HCIsSame.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333C93865B64F27B2639FE /* HCIsSame.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1CC1C177F5C00C2BAFD /* HCIsTypeOf.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33334CF68184328CD67F30 /* HCIsTypeOf.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1CD1C177F5C00C2BAFD /* HCThrowsException.h in Headers */ = {isa = PBXBuildFile; fileRef = 74FDDD131A3FF39900177999 /* HCThrowsException.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1CE1C177F5C00C2BAFD /* HCIsEqualIgnoringCase.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33371136854FB7CAF2A5F7 /* HCIsEqualIgnoringCase.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1CF1C177F5C00C2BAFD /* HCIsEqualCompressingWhiteSpace.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33338462C2743FF78AA51B /* HCIsEqualCompressingWhiteSpace.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1D01C177F5C00C2BAFD /* HCStringContains.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333144F9FA03AC04F8027D /* HCStringContains.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1D11C177F5C00C2BAFD /* HCStringContainsInOrder.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331C74ACFA7AB0675A7FE /* HCStringContainsInOrder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1D21C177F5D00C2BAFD /* HCStringEndsWith.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3333AF4B760A982CF9F9E3 /* HCStringEndsWith.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1D31C177F5D00C2BAFD /* HCStringStartsWith.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3336FA0B545673BD3630E2 /* HCStringStartsWith.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1D41C177F5D00C2BAFD /* HCSubstringMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3338B60F3A83AE527E91A7 /* HCSubstringMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1D51C177F7200C2BAFD /* OCHamcrest.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33356A6185FA4F1B6E240F /* OCHamcrest.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1D61C177F7200C2BAFD /* HCAssertThat.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333792C123FE94F1A3D447 /* HCAssertThat.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1D71C177F7200C2BAFD /* HCBaseDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333D869F9ECB46825CA726 /* HCBaseDescription.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1D81C177F7200C2BAFD /* HCBaseMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333E53F31A4853CD1B9548 /* HCBaseMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1D91C177F7200C2BAFD /* HCDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333D2D401EA09BE78BF7E9 /* HCDescription.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1DA1C177F7200C2BAFD /* HCDiagnosingMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 31DDCE62F97F2042F0A8BDAE /* HCDiagnosingMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1DB1C177F7200C2BAFD /* HCMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3334F31A5AB57A69174ECB /* HCMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1DC1C177F7200C2BAFD /* HCSelfDescribing.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333B7F19A5645CF7FC3E7E /* HCSelfDescribing.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1DD1C177F7200C2BAFD /* HCStringDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333215A6C1406E57ED34C4 /* HCStringDescription.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1DE1C177F7200C2BAFD /* HCBoolReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333EDC6C01648EEBC00D5E /* HCBoolReturnGetter.h */; }; + 24C5C1DF1C177F7200C2BAFD /* HCCharReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333C5BD261A2F9861F78A4 /* HCCharReturnGetter.h */; }; + 24C5C1E01C177F7200C2BAFD /* HCDoubleReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333F67A18EA568D7683A58 /* HCDoubleReturnGetter.h */; }; + 24C5C1E11C177F7200C2BAFD /* HCFloatReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333FF31E6FD6CB3A0DC45E /* HCFloatReturnGetter.h */; }; + 24C5C1E21C177F7200C2BAFD /* HCIntReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333725B2814AE9CF044B5B /* HCIntReturnGetter.h */; }; + 24C5C1E31C177F7200C2BAFD /* HCLongLongReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3332508DE228EEA28B7324 /* HCLongLongReturnGetter.h */; }; + 24C5C1E41C177F7200C2BAFD /* HCLongReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333C3F40A01E4269FA22BE /* HCLongReturnGetter.h */; }; + 24C5C1E51C177F7200C2BAFD /* HCObjectReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333FACCFA7D57857673665 /* HCObjectReturnGetter.h */; }; + 24C5C1E61C177F7200C2BAFD /* HCReturnValueGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333CC6471D7E5CB41558EF /* HCReturnValueGetter.h */; }; + 24C5C1E71C177F7200C2BAFD /* HCReturnTypeHandlerChain.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333191F9604EA23746459E /* HCReturnTypeHandlerChain.h */; }; + 24C5C1E81C177F7200C2BAFD /* HCShortReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3335A8FE1E47B0F4407027 /* HCShortReturnGetter.h */; }; + 24C5C1E91C177F7200C2BAFD /* HCUnsignedCharReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333BE1D8EFF4596D019A40 /* HCUnsignedCharReturnGetter.h */; }; + 24C5C1EA1C177F7200C2BAFD /* HCUnsignedIntReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331B51251AD3B252636B5 /* HCUnsignedIntReturnGetter.h */; }; + 24C5C1EB1C177F7200C2BAFD /* HCUnsignedLongLongReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33312F8485C256B14867AF /* HCUnsignedLongLongReturnGetter.h */; }; + 24C5C1EC1C177F7200C2BAFD /* HCUnsignedLongReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3338DE3E5C649EB399B649 /* HCUnsignedLongReturnGetter.h */; }; + 24C5C1ED1C177F7200C2BAFD /* HCUnsignedShortReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333A258B9C1CDFB1B184C3 /* HCUnsignedShortReturnGetter.h */; }; + 24C5C1EE1C177F7200C2BAFD /* HCGenericTestFailureReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33391EC19272E15C3D2E1A /* HCGenericTestFailureReporter.h */; }; + 24C5C1EF1C177F7200C2BAFD /* HCSenTestFailureReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33339BAC2DACD2306832A8 /* HCSenTestFailureReporter.h */; }; + 24C5C1F01C177F7200C2BAFD /* HCTestFailure.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333930FE07807045EEE866 /* HCTestFailure.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1F11C177F7200C2BAFD /* HCTestFailureReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3337F8ADEA5FCE59018962 /* HCTestFailureReporter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1F21C177F7200C2BAFD /* HCTestFailureReporterChain.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3336D2152CF1F88C8FF743 /* HCTestFailureReporterChain.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1F31C177F7300C2BAFD /* HCXCTestFailureReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33394F75CEE379A525320B /* HCXCTestFailureReporter.h */; }; + 24C5C1F41C177F7300C2BAFD /* HCCollect.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333C7DFDB2404398E9E6C5 /* HCCollect.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1F51C177F7300C2BAFD /* HCInvocationMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333D00EB0E21D420A7B22B /* HCInvocationMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1F61C177F7300C2BAFD /* HCRequireNonNilObject.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331127524965C9BCAD83A /* HCRequireNonNilObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1F71C177F7300C2BAFD /* HCWrapInMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333D5C9BD053542A0530EB /* HCWrapInMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1F81C177F7300C2BAFD /* NSInvocation+OCHamcrest.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3339BE4C0ECEAB7169772A /* NSInvocation+OCHamcrest.h */; }; + 24C5C1F91C177F7300C2BAFD /* HCEvery.h in Headers */ = {isa = PBXBuildFile; fileRef = 746045651A29625E00196267 /* HCEvery.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1FA1C177F7300C2BAFD /* HCHasCount.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331C8CBB96396DDA74467 /* HCHasCount.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1FB1C177F7300C2BAFD /* HCIsCollectionContaining.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331A34E31C49FE230D9BE /* HCIsCollectionContaining.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1FC1C177F7300C2BAFD /* HCIsCollectionContainingInAnyOrder.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3332CD4593BA0B619AE30C /* HCIsCollectionContainingInAnyOrder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1FD1C177F7300C2BAFD /* HCIsCollectionContainingInOrder.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33312A13BF3686BE841AD7 /* HCIsCollectionContainingInOrder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1FE1C177F7300C2BAFD /* HCIsCollectionContainingInRelativeOrder.h in Headers */ = {isa = PBXBuildFile; fileRef = 0E5ECE6B1BC20C3300C15D2A /* HCIsCollectionContainingInRelativeOrder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C1FF1C177F7300C2BAFD /* HCIsCollectionOnlyContaining.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333BC37F9AB8BFAB79AE16 /* HCIsCollectionOnlyContaining.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C2001C177F7300C2BAFD /* HCIsDictionaryContaining.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333FDE3455183AB9302310 /* HCIsDictionaryContaining.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C2011C177F7300C2BAFD /* HCIsDictionaryContainingEntries.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333F8E4CF5AD791C99B6FA /* HCIsDictionaryContainingEntries.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C2021C177F7300C2BAFD /* HCIsDictionaryContainingKey.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333E412026940E16FEC9C0 /* HCIsDictionaryContainingKey.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C2031C177F7400C2BAFD /* HCIsDictionaryContainingValue.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333A5C2975038FF06BDCBA /* HCIsDictionaryContainingValue.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C2041C177F7400C2BAFD /* HCIsEmptyCollection.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3338EDDD9B8C82083DD7FB /* HCIsEmptyCollection.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C2051C177F7400C2BAFD /* HCIsIn.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3336DDA8D0B1E91E937FD4 /* HCIsIn.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C2061C177F7400C2BAFD /* HCDescribedAs.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331D467F6B759BE9C3246 /* HCDescribedAs.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C2071C177F7400C2BAFD /* HCIs.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33384895217507C28D0896 /* HCIs.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C2081C177F7400C2BAFD /* HCAllOf.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333255C8855929B9264684 /* HCAllOf.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C2091C177F7400C2BAFD /* HCAnyOf.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333034CD6A38647E176A9A /* HCAnyOf.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C20A1C177F7400C2BAFD /* HCIsAnything.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333EC183D2675A9DF05F67 /* HCIsAnything.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C20B1C177F7400C2BAFD /* HCIsNot.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33317270E4A854112BCCEA /* HCIsNot.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C20C1C177F7400C2BAFD /* HCIsCloseTo.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33346362D58A13AA1BA54B /* HCIsCloseTo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C20D1C177F7400C2BAFD /* HCIsEqualToNumber.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3330701E28789385EAB1C9 /* HCIsEqualToNumber.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C20E1C177F7400C2BAFD /* HCIsTrueFalse.h in Headers */ = {isa = PBXBuildFile; fileRef = 74153B611A53606100FEF450 /* HCIsTrueFalse.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C20F1C177F7500C2BAFD /* HCNumberAssert.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333EEBE8C48A68933572E9 /* HCNumberAssert.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C2101C177F7500C2BAFD /* HCOrderingComparison.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3337CE41E8621958EAE8BF /* HCOrderingComparison.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C2111C177F7500C2BAFD /* HCArgumentCaptor.h in Headers */ = {isa = PBXBuildFile; fileRef = 609E9B6B3B80E4914607E267 /* HCArgumentCaptor.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C2121C177F7500C2BAFD /* HCClassMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33365155F84099730C9B56 /* HCClassMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C2131C177F7500C2BAFD /* HCConformsToProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3334410C93469CF325AA1D /* HCConformsToProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C2141C177F7500C2BAFD /* HCHasDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33387896CF4C830AAE4633 /* HCHasDescription.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C2151C177F7500C2BAFD /* HCHasProperty.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333B2A6FFEC019E0678999 /* HCHasProperty.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C2161C177F7500C2BAFD /* HCIsEqual.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333503A24A236A19B8A468 /* HCIsEqual.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C2171C177F7500C2BAFD /* HCIsInstanceOf.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33386A5E683DD0DC1AACE2 /* HCIsInstanceOf.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C2181C177F7600C2BAFD /* HCIsNil.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33359133039118AE88E2A8 /* HCIsNil.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C2191C177F7600C2BAFD /* HCIsSame.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333C93865B64F27B2639FE /* HCIsSame.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C21A1C177F7600C2BAFD /* HCIsTypeOf.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33334CF68184328CD67F30 /* HCIsTypeOf.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C21B1C177F7600C2BAFD /* HCThrowsException.h in Headers */ = {isa = PBXBuildFile; fileRef = 74FDDD131A3FF39900177999 /* HCThrowsException.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C21C1C177F7600C2BAFD /* HCIsEqualIgnoringCase.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33371136854FB7CAF2A5F7 /* HCIsEqualIgnoringCase.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C21D1C177F7600C2BAFD /* HCIsEqualCompressingWhiteSpace.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33338462C2743FF78AA51B /* HCIsEqualCompressingWhiteSpace.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C21E1C177F7600C2BAFD /* HCStringContains.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333144F9FA03AC04F8027D /* HCStringContains.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C21F1C177F7600C2BAFD /* HCStringContainsInOrder.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331C74ACFA7AB0675A7FE /* HCStringContainsInOrder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C2201C177F7700C2BAFD /* HCStringEndsWith.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3333AF4B760A982CF9F9E3 /* HCStringEndsWith.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C2211C177F7700C2BAFD /* HCStringStartsWith.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3336FA0B545673BD3630E2 /* HCStringStartsWith.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 24C5C2221C177F7700C2BAFD /* HCSubstringMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3338B60F3A83AE527E91A7 /* HCSubstringMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 31DDC19066E25E0DB52E3441 /* Mismatchable.m in Sources */ = {isa = PBXBuildFile; fileRef = 31DDCC467D6EFFC559012D61 /* Mismatchable.m */; }; + 31DDC972FA9B967EC328D4F3 /* HCDiagnosingMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 31DDCE62F97F2042F0A8BDAE /* HCDiagnosingMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 31DDC99BB130EC06BA6BFBEC /* HCDiagnosingMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 31DDCB570769099D33C22810 /* HCDiagnosingMatcher.m */; }; + 31DDCC204A3D141D7AC6FC02 /* Mismatchable.m in Sources */ = {isa = PBXBuildFile; fileRef = 31DDCC467D6EFFC559012D61 /* Mismatchable.m */; }; + 31DDCDCEAABD4CB0D7580AFE /* HCDiagnosingMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 31DDCE62F97F2042F0A8BDAE /* HCDiagnosingMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 31DDCEC51963ADABADBC576B /* HCDiagnosingMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 31DDCB570769099D33C22810 /* HCDiagnosingMatcher.m */; }; + 482FE2D31E6DDC2B00AC0009 /* HCWrapInMatcherTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 482FE2D21E6DDC2B00AC0009 /* HCWrapInMatcherTests.m */; }; + 482FE2D41E6DDC2B00AC0009 /* HCWrapInMatcherTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 482FE2D21E6DDC2B00AC0009 /* HCWrapInMatcherTests.m */; }; + 486C64312062241D00A1BFDE /* HCAssertThat.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333792C123FE94F1A3D447 /* HCAssertThat.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C64322062245600A1BFDE /* HCBaseDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333D869F9ECB46825CA726 /* HCBaseDescription.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C64332062245800A1BFDE /* HCBaseMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333E53F31A4853CD1B9548 /* HCBaseMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C64342062245B00A1BFDE /* HCDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333D2D401EA09BE78BF7E9 /* HCDescription.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C64352062245F00A1BFDE /* HCMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3334F31A5AB57A69174ECB /* HCMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C64362062246100A1BFDE /* HCSelfDescribing.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333B7F19A5645CF7FC3E7E /* HCSelfDescribing.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C64372062247200A1BFDE /* HCStringDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333215A6C1406E57ED34C4 /* HCStringDescription.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C6438206224E300A1BFDE /* HCGenericTestFailureReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33391EC19272E15C3D2E1A /* HCGenericTestFailureReporter.h */; }; + 486C6439206224E700A1BFDE /* HCSenTestFailureReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33339BAC2DACD2306832A8 /* HCSenTestFailureReporter.h */; }; + 486C643A206224EB00A1BFDE /* HCTestFailure.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333930FE07807045EEE866 /* HCTestFailure.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C643B206224EE00A1BFDE /* HCTestFailureReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3337F8ADEA5FCE59018962 /* HCTestFailureReporter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C643C206224F300A1BFDE /* HCTestFailureReporterChain.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3336D2152CF1F88C8FF743 /* HCTestFailureReporterChain.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C643D206224F700A1BFDE /* HCXCTestFailureReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33394F75CEE379A525320B /* HCXCTestFailureReporter.h */; }; + 486C643E206224FB00A1BFDE /* HCCollect.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333C7DFDB2404398E9E6C5 /* HCCollect.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C643F2062250000A1BFDE /* HCInvocationMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333D00EB0E21D420A7B22B /* HCInvocationMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C64402062250400A1BFDE /* HCRequireNonNilObject.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331127524965C9BCAD83A /* HCRequireNonNilObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C64412062250D00A1BFDE /* HCWrapInMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333D5C9BD053542A0530EB /* HCWrapInMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C64422062251200A1BFDE /* NSInvocation+OCHamcrest.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3339BE4C0ECEAB7169772A /* NSInvocation+OCHamcrest.h */; }; + 486C64432062253900A1BFDE /* HCHasCount.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331C8CBB96396DDA74467 /* HCHasCount.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C64442062253D00A1BFDE /* HCIsCollectionContaining.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331A34E31C49FE230D9BE /* HCIsCollectionContaining.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C64452062254300A1BFDE /* HCIsCollectionContainingInAnyOrder.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3332CD4593BA0B619AE30C /* HCIsCollectionContainingInAnyOrder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C64462062254600A1BFDE /* HCIsCollectionContainingInOrder.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33312A13BF3686BE841AD7 /* HCIsCollectionContainingInOrder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C64472062254E00A1BFDE /* HCIsCollectionContainingInRelativeOrder.h in Headers */ = {isa = PBXBuildFile; fileRef = 0E5ECE6B1BC20C3300C15D2A /* HCIsCollectionContainingInRelativeOrder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C64482062255100A1BFDE /* HCIsCollectionOnlyContaining.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333BC37F9AB8BFAB79AE16 /* HCIsCollectionOnlyContaining.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C64492062255300A1BFDE /* HCIsDictionaryContaining.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333FDE3455183AB9302310 /* HCIsDictionaryContaining.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C644A2062255700A1BFDE /* HCIsDictionaryContainingEntries.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333F8E4CF5AD791C99B6FA /* HCIsDictionaryContainingEntries.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C644B2062255900A1BFDE /* HCIsDictionaryContainingKey.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333E412026940E16FEC9C0 /* HCIsDictionaryContainingKey.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C644C2062255B00A1BFDE /* HCIsDictionaryContainingValue.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333A5C2975038FF06BDCBA /* HCIsDictionaryContainingValue.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C644D2062256100A1BFDE /* HCIsEmptyCollection.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3338EDDD9B8C82083DD7FB /* HCIsEmptyCollection.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C644E2062256400A1BFDE /* HCIsIn.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3336DDA8D0B1E91E937FD4 /* HCIsIn.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C644F2062256B00A1BFDE /* HCDescribedAs.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331D467F6B759BE9C3246 /* HCDescribedAs.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C64502062256E00A1BFDE /* HCIs.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33384895217507C28D0896 /* HCIs.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C64512062257000A1BFDE /* HCAllOf.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333255C8855929B9264684 /* HCAllOf.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C64522062257200A1BFDE /* HCAnyOf.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333034CD6A38647E176A9A /* HCAnyOf.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C64532062257500A1BFDE /* HCIsAnything.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333EC183D2675A9DF05F67 /* HCIsAnything.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C64542062257800A1BFDE /* HCIsNot.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33317270E4A854112BCCEA /* HCIsNot.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C64552062257B00A1BFDE /* HCIsCloseTo.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33346362D58A13AA1BA54B /* HCIsCloseTo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C64562062258400A1BFDE /* HCIsEqualToNumber.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3330701E28789385EAB1C9 /* HCIsEqualToNumber.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C64572062258600A1BFDE /* HCIsTrueFalse.h in Headers */ = {isa = PBXBuildFile; fileRef = 74153B611A53606100FEF450 /* HCIsTrueFalse.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C64582062258800A1BFDE /* HCNumberAssert.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333EEBE8C48A68933572E9 /* HCNumberAssert.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C64592062258A00A1BFDE /* HCOrderingComparison.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3337CE41E8621958EAE8BF /* HCOrderingComparison.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C645A2062258F00A1BFDE /* HCArgumentCaptor.h in Headers */ = {isa = PBXBuildFile; fileRef = 609E9B6B3B80E4914607E267 /* HCArgumentCaptor.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C645B2062259700A1BFDE /* HCClassMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33365155F84099730C9B56 /* HCClassMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C645C2062259A00A1BFDE /* HCConformsToProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3334410C93469CF325AA1D /* HCConformsToProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C645D2062259C00A1BFDE /* HCHasDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33387896CF4C830AAE4633 /* HCHasDescription.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C645E2062259E00A1BFDE /* HCHasProperty.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333B2A6FFEC019E0678999 /* HCHasProperty.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C645F206225A100A1BFDE /* HCIsEqual.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333503A24A236A19B8A468 /* HCIsEqual.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C6460206225A700A1BFDE /* HCIsInstanceOf.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33386A5E683DD0DC1AACE2 /* HCIsInstanceOf.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C6461206225AA00A1BFDE /* HCIsNil.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33359133039118AE88E2A8 /* HCIsNil.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C6462206225AD00A1BFDE /* HCIsSame.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333C93865B64F27B2639FE /* HCIsSame.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C6463206225AF00A1BFDE /* HCIsTypeOf.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33334CF68184328CD67F30 /* HCIsTypeOf.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C6464206225BB00A1BFDE /* HCThrowsException.h in Headers */ = {isa = PBXBuildFile; fileRef = 74FDDD131A3FF39900177999 /* HCThrowsException.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C6465206225C700A1BFDE /* HCIsEqualCompressingWhiteSpace.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33338462C2743FF78AA51B /* HCIsEqualCompressingWhiteSpace.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C6466206225CA00A1BFDE /* HCIsEqualIgnoringCase.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33371136854FB7CAF2A5F7 /* HCIsEqualIgnoringCase.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C6467206225CD00A1BFDE /* HCStringContains.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333144F9FA03AC04F8027D /* HCStringContains.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C6468206225D100A1BFDE /* HCStringContainsInOrder.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331C74ACFA7AB0675A7FE /* HCStringContainsInOrder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C6469206225D400A1BFDE /* HCStringEndsWith.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3333AF4B760A982CF9F9E3 /* HCStringEndsWith.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C646A206225D700A1BFDE /* HCStringStartsWith.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3336FA0B545673BD3630E2 /* HCStringStartsWith.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C646B206225D900A1BFDE /* HCSubstringMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3338B60F3A83AE527E91A7 /* HCSubstringMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 486C646C2062274100A1BFDE /* OCHamcrest.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33356A6185FA4F1B6E240F /* OCHamcrest.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 5F2773481436FAA600B9683A /* HCConformsToProtocolTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F2773441436F81000B9683A /* HCConformsToProtocolTests.m */; }; + 5F2773491436FAA700B9683A /* HCConformsToProtocolTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F2773441436F81000B9683A /* HCConformsToProtocolTests.m */; }; + 609E9068872F708CA4DA4785 /* MatcherTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 609E91D3F5F10354528F7FF8 /* MatcherTestCase.m */; }; + 609E90D232E0B31844E7BA6D /* HCRunloopRunner.m in Sources */ = {isa = PBXBuildFile; fileRef = 609E9BDAE6E9C3466DF16818 /* HCRunloopRunner.m */; }; + 609E9214DB7BBD12E81BC5F0 /* HCRunloopRunner.h in Headers */ = {isa = PBXBuildFile; fileRef = 609E92B06196813A410D4FF4 /* HCRunloopRunner.h */; }; + 609E921B14508E9DCA1FB303 /* HCRunloopRunner.h in Headers */ = {isa = PBXBuildFile; fileRef = 609E92B06196813A410D4FF4 /* HCRunloopRunner.h */; }; + 609E93B6BAA64F7836FB754E /* HCIsCollectionContainingInRelativeOrderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 609E99FE80B578287C9DBDFE /* HCIsCollectionContainingInRelativeOrderTests.m */; }; + 609E941591B81120F216AAAF /* InterceptingTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 609E97D2700DBCFB2161C50A /* InterceptingTestCase.m */; }; + 609E941C05213BA7214F6C6B /* HCRunloopRunner.h in Headers */ = {isa = PBXBuildFile; fileRef = 609E92B06196813A410D4FF4 /* HCRunloopRunner.h */; }; + 609E945B39EA3C98F2B6C224 /* HCRunloopRunner.m in Sources */ = {isa = PBXBuildFile; fileRef = 609E9BDAE6E9C3466DF16818 /* HCRunloopRunner.m */; }; + 609E9502808F7E64A3060B23 /* HCArgumentCaptor.h in Headers */ = {isa = PBXBuildFile; fileRef = 609E9B6B3B80E4914607E267 /* HCArgumentCaptor.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 609E95B8EB994E0768A22EB5 /* HCTestFailureReporterChainTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 609E99ADF272619359F01300 /* HCTestFailureReporterChainTests.m */; }; + 609E95FB6FCE5CDCA1E7A9BF /* HCArgumentCaptor.m in Sources */ = {isa = PBXBuildFile; fileRef = 609E92AABBB75D3F5E47A0E4 /* HCArgumentCaptor.m */; }; + 609E97770916062AC9B0D5BB /* MatcherTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 609E91D3F5F10354528F7FF8 /* MatcherTestCase.m */; }; + 609E99C4BEBB5A70318577B2 /* HCTestFailureReporterChainTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 609E99ADF272619359F01300 /* HCTestFailureReporterChainTests.m */; }; + 609E9A2DDB1D66CF6B8384FF /* HCRunloopRunner.m in Sources */ = {isa = PBXBuildFile; fileRef = 609E9BDAE6E9C3466DF16818 /* HCRunloopRunner.m */; }; + 609E9A8C8D6D971BFE42AA1E /* HCRunloopRunner.m in Sources */ = {isa = PBXBuildFile; fileRef = 609E9BDAE6E9C3466DF16818 /* HCRunloopRunner.m */; }; + 609E9B41637AEC0C99B77B10 /* InterceptingTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 609E97D2700DBCFB2161C50A /* InterceptingTestCase.m */; }; + 609E9B9227BDE67289F74F23 /* HCArgumentCaptor.m in Sources */ = {isa = PBXBuildFile; fileRef = 609E92AABBB75D3F5E47A0E4 /* HCArgumentCaptor.m */; }; + 609E9BACF54CA8CF5C8F4825 /* HCRunloopRunner.h in Headers */ = {isa = PBXBuildFile; fileRef = 609E92B06196813A410D4FF4 /* HCRunloopRunner.h */; }; + 609E9BD70804EB4AD2CF687B /* HCArgumentCaptorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 609E960725E106DC73EA9D60 /* HCArgumentCaptorTests.m */; }; + 609E9DB70E41F094B68A20C3 /* HCIsCollectionContainingInRelativeOrderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 609E99FE80B578287C9DBDFE /* HCIsCollectionContainingInRelativeOrderTests.m */; }; + 609E9E7A947E1D0029F3233D /* HCArgumentCaptorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 609E960725E106DC73EA9D60 /* HCArgumentCaptorTests.m */; }; + 609E9F1238326DBB8D61D4AE /* HCRunloopRunner.m in Sources */ = {isa = PBXBuildFile; fileRef = 609E9BDAE6E9C3466DF16818 /* HCRunloopRunner.m */; }; + 609E9F22CD4AE2C7290741BA /* HCRunloopRunner.h in Headers */ = {isa = PBXBuildFile; fileRef = 609E92B06196813A410D4FF4 /* HCRunloopRunner.h */; }; + 74153B5F1A535FEA00FEF450 /* HCIsTrueFalseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 74153B5E1A535FEA00FEF450 /* HCIsTrueFalseTests.m */; }; + 74153B601A535FEA00FEF450 /* HCIsTrueFalseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 74153B5E1A535FEA00FEF450 /* HCIsTrueFalseTests.m */; }; + 74153B631A53606100FEF450 /* HCIsTrueFalse.h in Headers */ = {isa = PBXBuildFile; fileRef = 74153B611A53606100FEF450 /* HCIsTrueFalse.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 74153B651A53606100FEF450 /* HCIsTrueFalse.m in Sources */ = {isa = PBXBuildFile; fileRef = 74153B621A53606100FEF450 /* HCIsTrueFalse.m */; }; + 74153B661A53606100FEF450 /* HCIsTrueFalse.m in Sources */ = {isa = PBXBuildFile; fileRef = 74153B621A53606100FEF450 /* HCIsTrueFalse.m */; }; + 7458920F1A2AAD2C00BAAC76 /* AssertWithTimeoutTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7458920E1A2AAD2C00BAAC76 /* AssertWithTimeoutTests.m */; }; + 745892101A2AAD2C00BAAC76 /* AssertWithTimeoutTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7458920E1A2AAD2C00BAAC76 /* AssertWithTimeoutTests.m */; }; + 746045671A29625E00196267 /* HCEvery.h in Headers */ = {isa = PBXBuildFile; fileRef = 746045651A29625E00196267 /* HCEvery.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 746045681A29625E00196267 /* HCEvery.h in Headers */ = {isa = PBXBuildFile; fileRef = 746045651A29625E00196267 /* HCEvery.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 746045691A29625E00196267 /* HCEvery.m in Sources */ = {isa = PBXBuildFile; fileRef = 746045661A29625E00196267 /* HCEvery.m */; }; + 7460456A1A29625E00196267 /* HCEvery.m in Sources */ = {isa = PBXBuildFile; fileRef = 746045661A29625E00196267 /* HCEvery.m */; }; + 7460456C1A2964AF00196267 /* HCEveryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7460456B1A2964AF00196267 /* HCEveryTests.m */; }; + 7460456D1A2964AF00196267 /* HCEveryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7460456B1A2964AF00196267 /* HCEveryTests.m */; }; + 747776C71A3F5060000A6E1D /* HCThrowsExceptionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 747776C61A3F5060000A6E1D /* HCThrowsExceptionTests.m */; }; + 747776C81A3F5060000A6E1D /* HCThrowsExceptionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 747776C61A3F5060000A6E1D /* HCThrowsExceptionTests.m */; }; + 74D7A7B51A2B902F00CD6CC0 /* DiagnosingMatcherTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 74D7A7B41A2B902F00CD6CC0 /* DiagnosingMatcherTest.m */; }; + 74D7A7B61A2B902F00CD6CC0 /* DiagnosingMatcherTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 74D7A7B41A2B902F00CD6CC0 /* DiagnosingMatcherTest.m */; }; + 74FDDD151A3FF39900177999 /* HCThrowsException.h in Headers */ = {isa = PBXBuildFile; fileRef = 74FDDD131A3FF39900177999 /* HCThrowsException.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 74FDDD171A3FF39900177999 /* HCThrowsException.m in Sources */ = {isa = PBXBuildFile; fileRef = 74FDDD141A3FF39900177999 /* HCThrowsException.m */; }; + 74FDDD181A3FF39900177999 /* HCThrowsException.m in Sources */ = {isa = PBXBuildFile; fileRef = 74FDDD141A3FF39900177999 /* HCThrowsException.m */; }; + 77C7B8771429361200DE60CC /* HCHasPropertyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 77C7B8761429361200DE60CC /* HCHasPropertyTests.m */; }; + 77C7B87A14293D0700DE60CC /* HCHasPropertyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 77C7B8761429361200DE60CC /* HCHasPropertyTests.m */; }; + BB33301A68F74D210C803FEA /* HCIsNil.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3335CC804D0A774E5F1041 /* HCIsNil.m */; }; + BB333020F5ED4E3979BA1714 /* NSInvocation+OCHamcrest.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3339BE4C0ECEAB7169772A /* NSInvocation+OCHamcrest.h */; }; + BB33302772CACC61A1B4DB74 /* HCTestFailureReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333528C8E2ED8489ACB161 /* HCTestFailureReporter.m */; }; + BB333031608DFFD2E76CD745 /* HCDoubleReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333F67A18EA568D7683A58 /* HCDoubleReturnGetter.h */; }; + BB33303F6D1004392A6EF76A /* HCIsTypeOfTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33373DA43C990FB5DA0D35 /* HCIsTypeOfTests.m */; }; + BB3330504048A30E7C190A19 /* HCReturnTypeHandlerChain.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333BD87164C606AE0BE027 /* HCReturnTypeHandlerChain.m */; }; + BB3330581DF04CB5115BCD11 /* HCIsTypeOfTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33373DA43C990FB5DA0D35 /* HCIsTypeOfTests.m */; }; + BB33306BE19806EB64EEC358 /* HCInvocationMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3337B908D229FD08452B2D /* HCInvocationMatcher.m */; }; + BB333083DABBB403F4E3A935 /* HCIsCollectionContainingInAnyOrder.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3332CD4593BA0B619AE30C /* HCIsCollectionContainingInAnyOrder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB33309748583DA78A7C98ED /* HCCharReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333FED5158848B95B043F1 /* HCCharReturnGetter.m */; }; + BB3330B6F8137019671EBC0C /* HCIsEqualCompressingWhiteSpace.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33338462C2743FF78AA51B /* HCIsEqualCompressingWhiteSpace.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB3330B79DF7A5A15B7182E8 /* HCReturnTypeHandlerChain.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333BD87164C606AE0BE027 /* HCReturnTypeHandlerChain.m */; }; + BB3330C8507C35031D255243 /* HCNumberAssert.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333A801AD666BCC23ED955 /* HCNumberAssert.m */; }; + BB3330D4CB9195C81B32E5AB /* HCSubstringMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33331178EE64329AB6A6DA /* HCSubstringMatcher.m */; }; + BB3331012E23C3795F3A1280 /* HCIsCollectionOnlyContaining.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333BC37F9AB8BFAB79AE16 /* HCIsCollectionOnlyContaining.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB333108B4630B94C5328B1A /* HCBaseMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3338BCBEA080BE71CA0C9D /* HCBaseMatcher.m */; }; + BB33311497FE29C3318919A7 /* HCIsDictionaryContainingValue.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3339100D7159DBF2387893 /* HCIsDictionaryContainingValue.m */; }; + BB33311A38FFA6C158C5F9B3 /* HCIsDictionaryContainingEntries.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333F8E4CF5AD791C99B6FA /* HCIsDictionaryContainingEntries.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB33311F075C5E807D23AF38 /* HCDescribedAs.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331D467F6B759BE9C3246 /* HCDescribedAs.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB33311FC27FAE5DF6F82911 /* HCIsEmptyCollection.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3338EDDD9B8C82083DD7FB /* HCIsEmptyCollection.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB33313D3DCEF8B438964913 /* HCRequireNonNilObject.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333C88565D58DE2AAAAD15 /* HCRequireNonNilObject.m */; }; + BB33315FDFFBAC0E1DB10869 /* HCStringDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333D43717AD7243D63A4AB /* HCStringDescription.m */; }; + BB33317BFCB5DC5993AA5C91 /* HCRequireNonNilObject.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333C88565D58DE2AAAAD15 /* HCRequireNonNilObject.m */; }; + BB333181C1BD36B166BDCABB /* HCIsDictionaryContainingKey.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333393BDEDD21E2CB1A2FF /* HCIsDictionaryContainingKey.m */; }; + BB33318EE99C688C6076DEBE /* HCUnsignedLongLongReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33312F8485C256B14867AF /* HCUnsignedLongLongReturnGetter.h */; }; + BB3331A2CC8B05A72A0E3E8B /* HCStringDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333215A6C1406E57ED34C4 /* HCStringDescription.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB3331A77D7C99A487A89434 /* HCBoolReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3331B99B691F7BFEF48591 /* HCBoolReturnGetter.m */; }; + BB3331D1B77DB4D834A7FDF5 /* HCOrderingComparison.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3336E4FAD686F2CFFF91B0 /* HCOrderingComparison.m */; }; + BB3331D8A9949686B47D2203 /* HCUnsignedShortReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333A5C3A9A42E4521010F1 /* HCUnsignedShortReturnGetter.m */; }; + BB3331E9AEEC3D10110E90ED /* HCIsEqualToNumber.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333DC21DC44C0F76ED3DB6 /* HCIsEqualToNumber.m */; }; + BB3332098FC93350263E39FF /* HCIsEmptyCollection.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33328F1CBC78AC3D9B6E78 /* HCIsEmptyCollection.m */; }; + BB333210C6DF7BB6238B7747 /* HCIsEqualIgnoringCase.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333A9273EFADF46D908BC4 /* HCIsEqualIgnoringCase.m */; }; + BB33321509F1659FB29AD59E /* HCIntReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3339614DAE31258BA73352 /* HCIntReturnGetter.m */; }; + BB3332191B1A602F69B8CC77 /* HCLongReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333C3F40A01E4269FA22BE /* HCLongReturnGetter.h */; }; + BB33322D2EF1C40E940F38B2 /* HCHasCount.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333E594CE94324024C2E19 /* HCHasCount.m */; }; + BB33323968557EF5235B3AFE /* HCIsInstanceOf.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333830B0D43E7B79BB537C /* HCIsInstanceOf.m */; }; + BB3332440D6C92F1E9650F9A /* HCUnsignedLongLongReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33312F8485C256B14867AF /* HCUnsignedLongLongReturnGetter.h */; }; + BB333260BA38F45B4864A846 /* HCLongLongReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3332508DE228EEA28B7324 /* HCLongLongReturnGetter.h */; }; + BB3332673485E95D8599222E /* HCAllOf.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3336446380772E9629E08E /* HCAllOf.m */; }; + BB33327669EF4A7B5889DBAD /* HCClassMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333C70DFAD4A867B769381 /* HCClassMatcher.m */; }; + BB333280BE085A371ABECECA /* HCIsDictionaryContainingEntries.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3331619B45E4E82B0FE72A /* HCIsDictionaryContainingEntries.m */; }; + BB3332839F5E2F874F6BAE41 /* HCBaseMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3338BCBEA080BE71CA0C9D /* HCBaseMatcher.m */; }; + BB33328AC0BB3E658B1BB14F /* HCHasCount.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331C8CBB96396DDA74467 /* HCHasCount.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB33328F6BBE1FF834BA90EC /* HCIsInstanceOf.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333830B0D43E7B79BB537C /* HCIsInstanceOf.m */; }; + BB33329CF0CDD9545D3FFCEC /* HCAssertThat.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333CF8C391D5A08F331B45 /* HCAssertThat.m */; }; + BB3332C8A3C41D9A4A4211AC /* HCUnsignedLongLongReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33347A9E4365F763632850 /* HCUnsignedLongLongReturnGetter.m */; }; + BB3332D67329789E3971E2C3 /* HCIsCollectionContainingInAnyOrder.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333FE413C7E8FA9C811BF1 /* HCIsCollectionContainingInAnyOrder.m */; }; + BB3332D945BB26BEF3E37662 /* HCCollect.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3333FD5A8D6F4A718D5D98 /* HCCollect.m */; }; + BB3332DB2D2CA8A11AF95DA7 /* HCDescribedAs.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3334C2039E9686ABB73AE2 /* HCDescribedAs.m */; }; + BB3332EA9B7B56B9364B4023 /* HCUnsignedCharReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333BE1D8EFF4596D019A40 /* HCUnsignedCharReturnGetter.h */; }; + BB333328EA1EFCED9C811321 /* HCFloatReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333C975A48CD4BC2EA4ED3 /* HCFloatReturnGetter.m */; }; + BB3333346B22FC64994ED11E /* HCTestFailureReporterChain.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3337085D9BEFA6D52E7EB0 /* HCTestFailureReporterChain.m */; }; + BB33333C0F1EED12D79C5845 /* HCConformsToProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3334410C93469CF325AA1D /* HCConformsToProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB3333502C76D73346A73631 /* HCLongReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333334135CAFF20A0E19FF /* HCLongReturnGetter.m */; }; + BB33335138021752A7522E67 /* HCIsCollectionOnlyContaining.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33380693E900C743EDF861 /* HCIsCollectionOnlyContaining.m */; }; + BB333352385755A59DEC95BD /* HCIsDictionaryContaining.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333FDE3455183AB9302310 /* HCIsDictionaryContaining.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB333375251766FC537D86BC /* HCAllOf.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3336446380772E9629E08E /* HCAllOf.m */; }; + BB3333754B75C34B2EF2B4E1 /* HCBaseMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333E53F31A4853CD1B9548 /* HCBaseMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB3333818CFAC6A68AEBC72E /* HCGenericTestFailureReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3331E3E2C324F3D4618E9D /* HCGenericTestFailureReporter.m */; }; + BB33338BB29B93F85C891624 /* HCIsAnything.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333AF2F081FE96420F97C0 /* HCIsAnything.m */; }; + BB33339ACB5D4E6DE2165E78 /* HCIsDictionaryContaining.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333049BE335209B4207B2A /* HCIsDictionaryContaining.m */; }; + BB33339CC090FAA28495D835 /* HCTestFailureReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333528C8E2ED8489ACB161 /* HCTestFailureReporter.m */; }; + BB3333AADA8EEF37F45AF012 /* HCConformsToProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3334EF9BFFB721161BB23F /* HCConformsToProtocol.m */; }; + BB3333B08155109232BEBD15 /* HCStringContainsInOrder.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3339B2CAF2C82161ED14F8 /* HCStringContainsInOrder.m */; }; + BB3333BCD2E3A2D26AA15D40 /* HCBoolReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3331B99B691F7BFEF48591 /* HCBoolReturnGetter.m */; }; + BB3333CBF2F94416D6F3E265 /* HCUnsignedLongReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33333B48A97B0771A3C256 /* HCUnsignedLongReturnGetter.m */; }; + BB3333CF7706F55C9F5C7C83 /* HCIsCollectionContainingInAnyOrder.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333FE413C7E8FA9C811BF1 /* HCIsCollectionContainingInAnyOrder.m */; }; + BB3333FFD04D52C9DC48644E /* HCStringEndsWith.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3333AF4B760A982CF9F9E3 /* HCStringEndsWith.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB3334311D89D9395F45A48D /* HCHasProperty.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33343C9B176C5DE07DE003 /* HCHasProperty.m */; }; + BB33343CA2ADA0E7E367E8F9 /* HCAnyOf.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333BF1C94AD91D6F5F5A95 /* HCAnyOf.m */; }; + BB3334425297552B3C06F861 /* HCObjectReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333FACCFA7D57857673665 /* HCObjectReturnGetter.h */; }; + BB333451E989F24386136004 /* HCXCTestFailureReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333ABD683BCE8C8E9028C5 /* HCXCTestFailureReporter.m */; }; + BB33345773AB590845EB68D6 /* HCStringStartsWith.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333A7430335F388E6DCF71 /* HCStringStartsWith.m */; }; + BB3334612BBBB6871530AB79 /* HCIsSame.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333594FB0AC2DAC3B73077 /* HCIsSame.m */; }; + BB33346AFA507C00FA481E8D /* HCIsCollectionContainingInOrder.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333986B013CA09B71D376B /* HCIsCollectionContainingInOrder.m */; }; + BB333494B740EF5FC786BB56 /* HCIntReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333725B2814AE9CF044B5B /* HCIntReturnGetter.h */; }; + BB3334B153329F550E1EFEBD /* HCIsNil.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33359133039118AE88E2A8 /* HCIsNil.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB3334B9AE78134252D2CBC7 /* HCSenTestFailureReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333F6051CD0B739527337A /* HCSenTestFailureReporter.m */; }; + BB3334CE1D4AE1517BD55CC5 /* HCUnsignedShortReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333A5C3A9A42E4521010F1 /* HCUnsignedShortReturnGetter.m */; }; + BB3334E55A7868E28215B693 /* OCHamcrest.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33356A6185FA4F1B6E240F /* OCHamcrest.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB3335190594259F5D74CAE1 /* HCIsTypeOf.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33322097042E108614BDAD /* HCIsTypeOf.m */; }; + BB333523DC58D4FD4797B9C7 /* HCLongLongReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3332508DE228EEA28B7324 /* HCLongLongReturnGetter.h */; }; + BB33356A7CF5DC030AAE9CCB /* HCIsDictionaryContaining.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333049BE335209B4207B2A /* HCIsDictionaryContaining.m */; }; + BB33357C5F1F3BC687DDF54C /* HCStringEndsWith.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3335974ADA5C5408FE0F69 /* HCStringEndsWith.m */; }; + BB333597D5BACF80DB73F49C /* HCIsCloseTo.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3331EECFE91DCA05C34EFD /* HCIsCloseTo.m */; }; + BB3335B51CB6C13C046E7C94 /* HCIs.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33334930C42DF9114F9D34 /* HCIs.m */; }; + BB3335B708E6AB9321294A1C /* HCIsEqual.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333503A24A236A19B8A468 /* HCIsEqual.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB3335C23B645B2E01C1DD95 /* HCLongReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333C3F40A01E4269FA22BE /* HCLongReturnGetter.h */; }; + BB3335CF33C2945D68DF426D /* HCShortReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3335A8FE1E47B0F4407027 /* HCShortReturnGetter.h */; }; + BB3335EAC1E9E47F87D50EDF /* HCReturnValueGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3331AAA2AB993E60458B6B /* HCReturnValueGetter.m */; }; + BB3335FB367CC6AD40BF6BA7 /* HCDoubleReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33303898210A59268030C8 /* HCDoubleReturnGetter.m */; }; + BB3336121208AAC5B1DDE8E9 /* HCUnsignedIntReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331B51251AD3B252636B5 /* HCUnsignedIntReturnGetter.h */; }; + BB333625D9CC6652DA94FF09 /* HCUnsignedLongReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3338DE3E5C649EB399B649 /* HCUnsignedLongReturnGetter.h */; }; + BB33362FA587A9B15E5C0D9B /* HCShortReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33363375E9DE829A8AD41D /* HCShortReturnGetter.m */; }; + BB333637DDB74BF66C729C62 /* HCIsNot.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33347EC840056B268877BF /* HCIsNot.m */; }; + BB333643284129107CAD7AB8 /* HCIsDictionaryContainingKey.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333E412026940E16FEC9C0 /* HCIsDictionaryContainingKey.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB33365B91A88641580F886E /* HCCharReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333C5BD261A2F9861F78A4 /* HCCharReturnGetter.h */; }; + BB33366E651777BD9B2A98FD /* HCIsNot.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33317270E4A854112BCCEA /* HCIsNot.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB33367DD8C7A4D5F5F96E81 /* HCIsCollectionContainingInOrder.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33312A13BF3686BE841AD7 /* HCIsCollectionContainingInOrder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB33369C21D2B44C72195326 /* HCTestFailure.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333F6535F43C37CBF517FA /* HCTestFailure.m */; }; + BB3336AD40784CC4673DB87F /* HCStringEndsWith.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3335974ADA5C5408FE0F69 /* HCStringEndsWith.m */; }; + BB3336B101EC9615B113A6D5 /* HCUnsignedCharReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333F5AFEB5565130751633 /* HCUnsignedCharReturnGetter.m */; }; + BB3336E5733952CCFE8A6BBC /* HCUnsignedCharReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333F5AFEB5565130751633 /* HCUnsignedCharReturnGetter.m */; }; + BB3336E5FCF909801BB0D103 /* HCStringContainsInOrder.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331C74ACFA7AB0675A7FE /* HCStringContainsInOrder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB3336F96B099B5EFE2E670E /* HCIs.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33334930C42DF9114F9D34 /* HCIs.m */; }; + BB33370D6F5B9B4978104E4B /* HCIntReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333725B2814AE9CF044B5B /* HCIntReturnGetter.h */; }; + BB33371F6028F465B81A4E5F /* HCReturnTypeHandlerChain.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333191F9604EA23746459E /* HCReturnTypeHandlerChain.h */; }; + BB3337267CA4ACA9E3EB6896 /* HCCharReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333C5BD261A2F9861F78A4 /* HCCharReturnGetter.h */; }; + BB3337466031638452E372CC /* HCHasDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3334125896535BFB7DBBA7 /* HCHasDescription.m */; }; + BB333788D86B329768056FCB /* HCHasProperty.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333B2A6FFEC019E0678999 /* HCHasProperty.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB3337895E6D085546B857EC /* HCObjectReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33355AD08369793188986F /* HCObjectReturnGetter.m */; }; + BB33379125D73946A16F7B0D /* HCHasProperty.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33343C9B176C5DE07DE003 /* HCHasProperty.m */; }; + BB3337947E2E43605545E743 /* HCStringContains.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333EA4C4546D46CF8A4044 /* HCStringContains.m */; }; + BB33379F2611DE99B3B43A90 /* HCStringContains.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333EA4C4546D46CF8A4044 /* HCStringContains.m */; }; + BB3337AB405029A86CDF3FAD /* HCDoubleReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333F67A18EA568D7683A58 /* HCDoubleReturnGetter.h */; }; + BB3337B4866BAF4A120919CF /* HCXCTestFailureReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33394F75CEE379A525320B /* HCXCTestFailureReporter.h */; }; + BB3337BBAAC8E23B9A74CBF1 /* HCSelfDescribing.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333B7F19A5645CF7FC3E7E /* HCSelfDescribing.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB3337CC95C9FD768019495D /* HCUnsignedIntReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333CA45EE4CF9D260DD50D /* HCUnsignedIntReturnGetter.m */; }; + BB3337DBEBED3443E8DB7A52 /* HCFloatReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333C975A48CD4BC2EA4ED3 /* HCFloatReturnGetter.m */; }; + BB3337E931781280D48FB372 /* HCXCTestFailureReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333ABD683BCE8C8E9028C5 /* HCXCTestFailureReporter.m */; }; + BB3337F52BA7EAA5CFCAE142 /* HCIsEmptyCollection.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33328F1CBC78AC3D9B6E78 /* HCIsEmptyCollection.m */; }; + BB3338179D48057215091538 /* HCReturnValueGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333CC6471D7E5CB41558EF /* HCReturnValueGetter.h */; }; + BB3338188B4D09D845A55E00 /* HCAssertThat.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333792C123FE94F1A3D447 /* HCAssertThat.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB333839A36AEC819CDABEBF /* HCNumberAssert.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333EEBE8C48A68933572E9 /* HCNumberAssert.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB333842914F0FF2E8F70FAA /* HCIsEqualIgnoringCase.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33371136854FB7CAF2A5F7 /* HCIsEqualIgnoringCase.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB3338485B4DA85A777544B3 /* HCCollect.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3333FD5A8D6F4A718D5D98 /* HCCollect.m */; }; + BB33387D94ADA6748B02A289 /* HCInvocationMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3337B908D229FD08452B2D /* HCInvocationMatcher.m */; }; + BB33389388EB3A27D7037C56 /* HCSenTestFailureReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33339BAC2DACD2306832A8 /* HCSenTestFailureReporter.h */; }; + BB3338A5F9D50886A7A82129 /* HCIs.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33384895217507C28D0896 /* HCIs.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB3338B40D8EA4F74C514153 /* HCWrapInMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333D5C9BD053542A0530EB /* HCWrapInMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB3338D75677FE93976B1D6E /* HCIsCollectionContainingInOrder.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333986B013CA09B71D376B /* HCIsCollectionContainingInOrder.m */; }; + BB3338E155E644CFB619CFEF /* HCObjectReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33355AD08369793188986F /* HCObjectReturnGetter.m */; }; + BB3338FF145C0BCE0B1A6C33 /* HCIsIn.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3336DDA8D0B1E91E937FD4 /* HCIsIn.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB33390F0791F299C712BAF5 /* HCIsIn.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333522A78D6A7D1A8414B3 /* HCIsIn.m */; }; + BB33392916E29A808728FF91 /* HCStringDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333D43717AD7243D63A4AB /* HCStringDescription.m */; }; + BB33395D7C71ECDB178C10F9 /* HCMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3334F31A5AB57A69174ECB /* HCMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB333987DAE91CFE7D31B8B4 /* HCIsSame.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333C93865B64F27B2639FE /* HCIsSame.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB333995C1E6476B0C239FB9 /* HCIsNil.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3335CC804D0A774E5F1041 /* HCIsNil.m */; }; + BB3339A4AEEDCDF3C4E96491 /* HCBoolReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333EDC6C01648EEBC00D5E /* HCBoolReturnGetter.h */; }; + BB3339A5133E437AFDDE7B15 /* HCIsDictionaryContainingValue.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3339100D7159DBF2387893 /* HCIsDictionaryContainingValue.m */; }; + BB3339BE48FB5782E8F9A9F5 /* HCClassMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33365155F84099730C9B56 /* HCClassMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB3339C714B5E796538B65F1 /* NSInvocation+OCHamcrest.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33309F9D72ED2CF53532A8 /* NSInvocation+OCHamcrest.m */; }; + BB3339CC63BCC9186973C491 /* HCTestFailure.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333F6535F43C37CBF517FA /* HCTestFailure.m */; }; + BB3339D3E0792F98EE3F7D07 /* HCShortReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33363375E9DE829A8AD41D /* HCShortReturnGetter.m */; }; + BB3339D5C07EC353A46EFEFE /* HCTestFailure.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333930FE07807045EEE866 /* HCTestFailure.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB3339D6DAC64560919B4B71 /* HCUnsignedShortReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333A258B9C1CDFB1B184C3 /* HCUnsignedShortReturnGetter.h */; }; + BB3339D8979DFFEBB1AEB26D /* HCLongReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333334135CAFF20A0E19FF /* HCLongReturnGetter.m */; }; + BB3339F396185713397718B5 /* HCIsCloseTo.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33346362D58A13AA1BA54B /* HCIsCloseTo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB3339F4D9293AA86914FB66 /* HCIsDictionaryContainingEntries.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3331619B45E4E82B0FE72A /* HCIsDictionaryContainingEntries.m */; }; + BB333A0391F4B076785DEBA9 /* HCIsAnything.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333AF2F081FE96420F97C0 /* HCIsAnything.m */; }; + BB333A10646B1A68ACCDC472 /* HCUnsignedShortReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333A258B9C1CDFB1B184C3 /* HCUnsignedShortReturnGetter.h */; }; + BB333A11937F03ABF822AFA7 /* HCDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333D2D401EA09BE78BF7E9 /* HCDescription.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB333A358C42850FD4C55B3D /* HCBaseDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333785CDF67F8F0284B9CA /* HCBaseDescription.m */; }; + BB333A3F01C18400B5A131C6 /* HCIsCollectionContaining.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3338AB92DDE4ACF56D7C1B /* HCIsCollectionContaining.m */; }; + BB333A62F20EA2AE048FC857 /* HCObjectReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333FACCFA7D57857673665 /* HCObjectReturnGetter.h */; }; + BB333A663782157FB25A5E45 /* HCAssertThat.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333CF8C391D5A08F331B45 /* HCAssertThat.m */; }; + BB333A7CCC7A2BD01898722C /* HCStringStartsWith.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333A7430335F388E6DCF71 /* HCStringStartsWith.m */; }; + BB333A7CCE8A0DF95052F67C /* HCInvocationMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333D00EB0E21D420A7B22B /* HCInvocationMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB333AB8B39B203DF15C9445 /* HCIsSame.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333594FB0AC2DAC3B73077 /* HCIsSame.m */; }; + BB333ABCAF8D67405B9C023D /* HCIsInstanceOf.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33386A5E683DD0DC1AACE2 /* HCIsInstanceOf.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB333ACAD45079E5160B4B2D /* HCLongLongReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33330C459CF68E13DDD1D6 /* HCLongLongReturnGetter.m */; }; + BB333ADE67DDD25C6E6EF058 /* HCFloatReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333FF31E6FD6CB3A0DC45E /* HCFloatReturnGetter.h */; }; + BB333AFA559C22D92650E91C /* HCIsEqualCompressingWhiteSpace.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33377384F2BF4EA9694DA7 /* HCIsEqualCompressingWhiteSpace.m */; }; + BB333B08E01B2E88828DA51C /* HCAnyOf.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333034CD6A38647E176A9A /* HCAnyOf.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB333B4AB9E7656A98D0D172 /* HCUnsignedLongLongReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33347A9E4365F763632850 /* HCUnsignedLongLongReturnGetter.m */; }; + BB333B4B561F53C31972B9D9 /* HCFloatReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333FF31E6FD6CB3A0DC45E /* HCFloatReturnGetter.h */; }; + BB333B9914086A169E24347E /* HCIsEqualToNumber.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3330701E28789385EAB1C9 /* HCIsEqualToNumber.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB333BA6882190C2BDCABECC /* HCAllOf.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333255C8855929B9264684 /* HCAllOf.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB333BBD9153A5B7C1DD0F86 /* HCHasCount.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333E594CE94324024C2E19 /* HCHasCount.m */; }; + BB333BCE07B55ED523B267F7 /* HCRequireNonNilObject.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331127524965C9BCAD83A /* HCRequireNonNilObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB333BE50C075D306EDA2E98 /* HCReturnTypeHandlerChain.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333191F9604EA23746459E /* HCReturnTypeHandlerChain.h */; }; + BB333BE5AC86690747041C24 /* HCWrapInMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3335EC3CC47489B4C5395E /* HCWrapInMatcher.m */; }; + BB333BF2FCAE2C05C337CE46 /* HCIsCloseTo.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3331EECFE91DCA05C34EFD /* HCIsCloseTo.m */; }; + BB333C23AD2D7D8BF49032C1 /* HCOrderingComparison.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3336E4FAD686F2CFFF91B0 /* HCOrderingComparison.m */; }; + BB333C35826116418990B390 /* HCIsNot.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33347EC840056B268877BF /* HCIsNot.m */; }; + BB333C372274619E93D2D9E8 /* HCGenericTestFailureReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3331E3E2C324F3D4618E9D /* HCGenericTestFailureReporter.m */; }; + BB333C48F0B219D8132548B4 /* HCIsEqualToNumber.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333DC21DC44C0F76ED3DB6 /* HCIsEqualToNumber.m */; }; + BB333C6E35F2398771CEA2C3 /* HCIsEqualIgnoringCase.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333A9273EFADF46D908BC4 /* HCIsEqualIgnoringCase.m */; }; + BB333C78E5F9DB528838CA5A /* HCIntReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3339614DAE31258BA73352 /* HCIntReturnGetter.m */; }; + BB333C793CAFFF7E4FB5BA2D /* HCCollect.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333C7DFDB2404398E9E6C5 /* HCCollect.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB333C8BC4684992F0A533CE /* HCHasDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33387896CF4C830AAE4633 /* HCHasDescription.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB333C8FF5557126F41B7291 /* HCIsCollectionContaining.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331A34E31C49FE230D9BE /* HCIsCollectionContaining.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB333D17BEC11419274E2D8F /* HCAnyOf.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333BF1C94AD91D6F5F5A95 /* HCAnyOf.m */; }; + BB333D2770D5C4DD8E4D7633 /* HCWrapInMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3335EC3CC47489B4C5395E /* HCWrapInMatcher.m */; }; + BB333D2C925DC2010C1B25FA /* HCSubstringMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33331178EE64329AB6A6DA /* HCSubstringMatcher.m */; }; + BB333D3938678ED9EF3E7E8D /* HCIsCollectionOnlyContaining.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33380693E900C743EDF861 /* HCIsCollectionOnlyContaining.m */; }; + BB333D3CAC82C5DFD2D18F2D /* HCBaseDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333D869F9ECB46825CA726 /* HCBaseDescription.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB333D632775553288FEA2FE /* HCHasDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3334125896535BFB7DBBA7 /* HCHasDescription.m */; }; + BB333D6CD37B858BD8F989F1 /* HCClassMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333C70DFAD4A867B769381 /* HCClassMatcher.m */; }; + BB333D6DC563CDDEDF3B6DC9 /* HCIsDictionaryContainingKey.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333393BDEDD21E2CB1A2FF /* HCIsDictionaryContainingKey.m */; }; + BB333D81687E3E6481265818 /* HCIsTypeOf.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33322097042E108614BDAD /* HCIsTypeOf.m */; }; + BB333D83E68045BBEA3B3796 /* HCShortReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3335A8FE1E47B0F4407027 /* HCShortReturnGetter.h */; }; + BB333D94082F074BB61E233C /* HCBaseDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333785CDF67F8F0284B9CA /* HCBaseDescription.m */; }; + BB333DB953FACB91AFC1A5C2 /* HCIsCollectionContaining.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3338AB92DDE4ACF56D7C1B /* HCIsCollectionContaining.m */; }; + BB333DD97E87592CF49128AD /* HCSubstringMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3338B60F3A83AE527E91A7 /* HCSubstringMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB333DE89A2F8557B268A355 /* HCReturnValueGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333CC6471D7E5CB41558EF /* HCReturnValueGetter.h */; }; + BB333DEC51AFB20AADBB68D6 /* HCDescribedAs.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3334C2039E9686ABB73AE2 /* HCDescribedAs.m */; }; + BB333DF4A40F8816030C14DE /* HCIsAnything.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333EC183D2675A9DF05F67 /* HCIsAnything.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB333DFB6E6D7C86888EC7AA /* HCUnsignedIntReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333CA45EE4CF9D260DD50D /* HCUnsignedIntReturnGetter.m */; }; + BB333E360A90374209909E7E /* HCUnsignedLongReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33333B48A97B0771A3C256 /* HCUnsignedLongReturnGetter.m */; }; + BB333E4BEBC363D5F315B27F /* HCIsDictionaryContainingValue.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333A5C2975038FF06BDCBA /* HCIsDictionaryContainingValue.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB333E7F416B46CE6777D695 /* HCStringStartsWith.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3336FA0B545673BD3630E2 /* HCStringStartsWith.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB333E8BB98FF2089077826D /* HCIsTypeOf.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33334CF68184328CD67F30 /* HCIsTypeOf.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB333E932D2D03A14B886055 /* HCNumberAssert.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333A801AD666BCC23ED955 /* HCNumberAssert.m */; }; + BB333EA439826DB6B82DDE54 /* HCGenericTestFailureReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB33391EC19272E15C3D2E1A /* HCGenericTestFailureReporter.h */; }; + BB333EA74F069D831202108E /* HCReturnValueGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3331AAA2AB993E60458B6B /* HCReturnValueGetter.m */; }; + BB333EB3405CFA5A54AB30CE /* HCIsEqual.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333DFF0305457C0CD69303 /* HCIsEqual.m */; }; + BB333EB3C38A3D9EEAEC842A /* HCOrderingComparison.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3337CE41E8621958EAE8BF /* HCOrderingComparison.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB333EBAF352F0961A83E1A6 /* HCIsEqual.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333DFF0305457C0CD69303 /* HCIsEqual.m */; }; + BB333EDC3E3320C65FB854B5 /* HCBoolReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333EDC6C01648EEBC00D5E /* HCBoolReturnGetter.h */; }; + BB333EECA8B5D2C68CC8EF72 /* NSInvocation+OCHamcrest.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33309F9D72ED2CF53532A8 /* NSInvocation+OCHamcrest.m */; }; + BB333EEEAF4355DA0CCB64D4 /* HCUnsignedCharReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333BE1D8EFF4596D019A40 /* HCUnsignedCharReturnGetter.h */; }; + BB333EF9E0C14539AE686528 /* HCCharReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333FED5158848B95B043F1 /* HCCharReturnGetter.m */; }; + BB333F22130C3E2815EB3CE4 /* HCIsEqualCompressingWhiteSpace.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33377384F2BF4EA9694DA7 /* HCIsEqualCompressingWhiteSpace.m */; }; + BB333F5B324A75A04610D267 /* HCTestFailureReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3337F8ADEA5FCE59018962 /* HCTestFailureReporter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB333F7FB7BA3AF26A162752 /* HCStringContainsInOrder.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3339B2CAF2C82161ED14F8 /* HCStringContainsInOrder.m */; }; + BB333F8CC565D8068C47B3FE /* HCDoubleReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33303898210A59268030C8 /* HCDoubleReturnGetter.m */; }; + BB333F9085EBAAC25CDC41EA /* HCTestFailureReporterChain.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3336D2152CF1F88C8FF743 /* HCTestFailureReporterChain.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB333F90C371ADB4443F7CB0 /* HCUnsignedIntReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3331B51251AD3B252636B5 /* HCUnsignedIntReturnGetter.h */; }; + BB333FC3961C09A902683A7F /* HCSenTestFailureReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333F6051CD0B739527337A /* HCSenTestFailureReporter.m */; }; + BB333FD5A85510AE5AA9B519 /* HCLongLongReturnGetter.m in Sources */ = {isa = PBXBuildFile; fileRef = BB33330C459CF68E13DDD1D6 /* HCLongLongReturnGetter.m */; }; + BB333FE2E6441F4ADFE460AC /* HCIsIn.m in Sources */ = {isa = PBXBuildFile; fileRef = BB333522A78D6A7D1A8414B3 /* HCIsIn.m */; }; + BB333FE3A9E3E2083DCDE4AF /* HCUnsignedLongReturnGetter.h in Headers */ = {isa = PBXBuildFile; fileRef = BB3338DE3E5C649EB399B649 /* HCUnsignedLongReturnGetter.h */; }; + BB333FEC8B49B249E92B3671 /* HCStringContains.h in Headers */ = {isa = PBXBuildFile; fileRef = BB333144F9FA03AC04F8027D /* HCStringContains.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BB333FF1353091B71E6A3B33 /* HCTestFailureReporterChain.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3337085D9BEFA6D52E7EB0 /* HCTestFailureReporterChain.m */; }; + BB333FFD6EAB7828C98E11EE /* HCConformsToProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = BB3334EF9BFFB721161BB23F /* HCConformsToProtocol.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 081BEE701345979F003F846A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 087601D813440806001B439B /* Project object */; + proxyType = 1; + remoteGlobalIDString = 081BEE611345979F003F846A; + remoteInfo = libochamcrest; + }; + 087601F913440807001B439B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 087601D813440806001B439B /* Project object */; + proxyType = 1; + remoteGlobalIDString = 087601E113440806001B439B; + remoteInfo = OCHamcrest; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 081BEE621345979F003F846A /* libochamcrest.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libochamcrest.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 081BEE6C1345979F003F846A /* libochamcrestTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = libochamcrestTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 082E7C2B13FF676E004A22FE /* MockSenTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MockSenTestCase.m; sourceTree = ""; }; + 082E7C2C13FF676E004A22FE /* HCBaseMatcherTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCBaseMatcherTests.m; sourceTree = ""; }; + 082E7C2D13FF676E004A22FE /* HCInvocationMatcherTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCInvocationMatcherTests.m; sourceTree = ""; }; + 082E7C2E13FF676E004A22FE /* HCStringDescriptionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCStringDescriptionTests.m; sourceTree = ""; }; + 0846B6DC13EE5F9D00EA68E8 /* HCDescribedAsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCDescribedAsTests.m; sourceTree = ""; }; + 0846B6DD13EE5F9D00EA68E8 /* HCIsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsTests.m; sourceTree = ""; }; + 0846B6DE13EE5F9D00EA68E8 /* NeverMatch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NeverMatch.h; sourceTree = ""; }; + 0846B6DF13EE5F9D00EA68E8 /* NeverMatch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NeverMatch.m; sourceTree = ""; }; + 0846B6E713EE5FB400EA68E8 /* HCAllOfTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCAllOfTests.m; sourceTree = ""; }; + 0846B6E813EE5FB400EA68E8 /* HCAnyOfTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCAnyOfTests.m; sourceTree = ""; }; + 0846B6E913EE5FB400EA68E8 /* HCIsAnythingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsAnythingTests.m; sourceTree = ""; }; + 0846B6EA13EE5FB400EA68E8 /* HCIsNotTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsNotTests.m; sourceTree = ""; }; + 0846B6F313EE5FD200EA68E8 /* HCIsEqualTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsEqualTests.m; sourceTree = ""; }; + 0846B6F413EE5FD200EA68E8 /* HCIsInstanceOfTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsInstanceOfTests.m; sourceTree = ""; }; + 0846B6F513EE5FD200EA68E8 /* HCIsNilTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsNilTests.m; sourceTree = ""; }; + 0846B6F613EE5FD200EA68E8 /* HCIsSameTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsSameTests.m; sourceTree = ""; }; + 0850A44C17165D2A0019BBE0 /* SomeClassAndSubclass.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SomeClassAndSubclass.h; sourceTree = ""; }; + 0850A44D17165D2A0019BBE0 /* SomeClassAndSubclass.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SomeClassAndSubclass.m; sourceTree = ""; }; + 087601E213440806001B439B /* OCHamcrest.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OCHamcrest.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 087601F713440807001B439B /* OCHamcrestTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OCHamcrestTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 0876033513440AD8001B439B /* FakeWithCount.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FakeWithCount.h; sourceTree = ""; }; + 0876033613440AD8001B439B /* FakeWithCount.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FakeWithCount.m; sourceTree = ""; }; + 0876033713440AD8001B439B /* FakeWithoutCount.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FakeWithoutCount.h; sourceTree = ""; }; + 0876033813440AD8001B439B /* FakeWithoutCount.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FakeWithoutCount.m; sourceTree = ""; }; + 0876033913440AD8001B439B /* HCHasCountTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCHasCountTests.m; sourceTree = ""; }; + 0876033A13440AD8001B439B /* HCIsCollectionContainingInAnyOrderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsCollectionContainingInAnyOrderTests.m; sourceTree = ""; }; + 0876033B13440AD8001B439B /* HCIsCollectionContainingInOrderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsCollectionContainingInOrderTests.m; sourceTree = ""; }; + 0876033C13440AD8001B439B /* HCIsCollectionContainingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsCollectionContainingTests.m; sourceTree = ""; }; + 0876033D13440AD8001B439B /* HCIsCollectionOnlyContainingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsCollectionOnlyContainingTests.m; sourceTree = ""; }; + 0876033E13440AD8001B439B /* HCIsDictionaryContainingEntriesTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsDictionaryContainingEntriesTests.m; sourceTree = ""; }; + 0876033F13440AD8001B439B /* HCIsDictionaryContainingKeyTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsDictionaryContainingKeyTests.m; sourceTree = ""; }; + 0876034013440AD8001B439B /* HCIsDictionaryContainingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsDictionaryContainingTests.m; sourceTree = ""; }; + 0876034113440AD8001B439B /* HCIsDictionaryContainingValueTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsDictionaryContainingValueTests.m; sourceTree = ""; }; + 0876034213440AD8001B439B /* HCIsEmptyCollectionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsEmptyCollectionTests.m; sourceTree = ""; }; + 0876034313440AD8001B439B /* HCIsInTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsInTests.m; sourceTree = ""; }; + 0876035313440AD8001B439B /* HCIsCloseToTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsCloseToTests.m; sourceTree = ""; }; + 0876035413440AD8001B439B /* HCIsEqualToNumberTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsEqualToNumberTests.m; sourceTree = ""; }; + 0876035513440AD8001B439B /* HCNumberAssertTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCNumberAssertTests.m; sourceTree = ""; }; + 0876035613440AD8001B439B /* HCOrderingComparisonTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCOrderingComparisonTests.m; sourceTree = ""; }; + 0876035813440AD8001B439B /* HCHasDescriptionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCHasDescriptionTests.m; sourceTree = ""; }; + 0876035B13440AD8001B439B /* HCIsEqualIgnoringCaseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsEqualIgnoringCaseTests.m; sourceTree = ""; }; + 0876035C13440AD8001B439B /* HCIsEqualCompressingWhiteSpaceTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsEqualCompressingWhiteSpaceTests.m; sourceTree = ""; }; + 0876035D13440AD8001B439B /* HCStringContainsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCStringContainsTests.m; sourceTree = ""; }; + 0876035E13440AD8001B439B /* HCStringEndsWithTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCStringEndsWithTests.m; sourceTree = ""; }; + 0876035F13440AD8001B439B /* HCStringStartsWithTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCStringStartsWithTests.m; sourceTree = ""; }; + 0876038D13440B80001B439B /* OCHamcrest-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "OCHamcrest-Info.plist"; sourceTree = ""; }; + 0876039113440BB0001B439B /* Tests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Tests-Info.plist"; sourceTree = ""; }; + 088FB0AA136A6DEA00C191E1 /* HCStringContainsInOrderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCStringContainsInOrderTests.m; sourceTree = ""; }; + 08F39BAA1711426200745958 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = CHANGELOG.md; path = ../CHANGELOG.md; sourceTree = ""; }; + 0E49BBDB1C47498C00418A3C /* XcodeWarnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = XcodeWarnings.xcconfig; sourceTree = ""; }; + 0E5ECE6B1BC20C3300C15D2A /* HCIsCollectionContainingInRelativeOrder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCIsCollectionContainingInRelativeOrder.h; sourceTree = ""; }; + 0E5ECE6C1BC20C3300C15D2A /* HCIsCollectionContainingInRelativeOrder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsCollectionContainingInRelativeOrder.m; sourceTree = ""; }; + 24C5BFE91C1777D900C2BAFD /* OCHamcrest.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OCHamcrest.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 24C5BFF61C17781400C2BAFD /* OCHamcrest.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OCHamcrest.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 24C5C0031C17782500C2BAFD /* OCHamcrest.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OCHamcrest.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 31DDCB570769099D33C22810 /* HCDiagnosingMatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCDiagnosingMatcher.m; sourceTree = ""; }; + 31DDCC467D6EFFC559012D61 /* Mismatchable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Mismatchable.m; sourceTree = ""; }; + 31DDCCF46E1766AF4AC97BA5 /* Mismatchable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Mismatchable.h; sourceTree = ""; }; + 31DDCE62F97F2042F0A8BDAE /* HCDiagnosingMatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCDiagnosingMatcher.h; sourceTree = ""; }; + 482FE2D21E6DDC2B00AC0009 /* HCWrapInMatcherTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCWrapInMatcherTests.m; sourceTree = ""; }; + 5F2773441436F81000B9683A /* HCConformsToProtocolTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCConformsToProtocolTests.m; sourceTree = ""; }; + 609E91D3F5F10354528F7FF8 /* MatcherTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MatcherTestCase.m; sourceTree = ""; }; + 609E92AABBB75D3F5E47A0E4 /* HCArgumentCaptor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCArgumentCaptor.m; sourceTree = ""; }; + 609E92B06196813A410D4FF4 /* HCRunloopRunner.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCRunloopRunner.h; sourceTree = ""; }; + 609E93C91C2B1DFA3BC3F9A8 /* MatcherTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MatcherTestCase.h; sourceTree = ""; }; + 609E95FA7156B2A6FD85D509 /* InterceptingTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InterceptingTestCase.h; sourceTree = ""; }; + 609E960725E106DC73EA9D60 /* HCArgumentCaptorTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCArgumentCaptorTests.m; sourceTree = ""; }; + 609E97D2700DBCFB2161C50A /* InterceptingTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InterceptingTestCase.m; sourceTree = ""; }; + 609E99ADF272619359F01300 /* HCTestFailureReporterChainTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCTestFailureReporterChainTests.m; sourceTree = ""; }; + 609E99FE80B578287C9DBDFE /* HCIsCollectionContainingInRelativeOrderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsCollectionContainingInRelativeOrderTests.m; sourceTree = ""; }; + 609E9B6B3B80E4914607E267 /* HCArgumentCaptor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCArgumentCaptor.h; sourceTree = ""; }; + 609E9BDAE6E9C3466DF16818 /* HCRunloopRunner.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCRunloopRunner.m; sourceTree = ""; }; + 74153B5E1A535FEA00FEF450 /* HCIsTrueFalseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsTrueFalseTests.m; sourceTree = ""; }; + 74153B611A53606100FEF450 /* HCIsTrueFalse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCIsTrueFalse.h; sourceTree = ""; }; + 74153B621A53606100FEF450 /* HCIsTrueFalse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsTrueFalse.m; sourceTree = ""; }; + 7458920E1A2AAD2C00BAAC76 /* AssertWithTimeoutTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AssertWithTimeoutTests.m; sourceTree = ""; }; + 746045651A29625E00196267 /* HCEvery.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCEvery.h; sourceTree = ""; }; + 746045661A29625E00196267 /* HCEvery.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCEvery.m; sourceTree = ""; }; + 7460456B1A2964AF00196267 /* HCEveryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCEveryTests.m; sourceTree = ""; }; + 747776C61A3F5060000A6E1D /* HCThrowsExceptionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCThrowsExceptionTests.m; sourceTree = ""; }; + 74D7A7B41A2B902F00CD6CC0 /* DiagnosingMatcherTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DiagnosingMatcherTest.m; sourceTree = ""; }; + 74FDDD131A3FF39900177999 /* HCThrowsException.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCThrowsException.h; sourceTree = ""; }; + 74FDDD141A3FF39900177999 /* HCThrowsException.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCThrowsException.m; sourceTree = ""; }; + 77C7B8761429361200DE60CC /* HCHasPropertyTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCHasPropertyTests.m; sourceTree = ""; }; + BB333034CD6A38647E176A9A /* HCAnyOf.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCAnyOf.h; sourceTree = ""; }; + BB33303898210A59268030C8 /* HCDoubleReturnGetter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCDoubleReturnGetter.m; sourceTree = ""; }; + BB333049BE335209B4207B2A /* HCIsDictionaryContaining.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsDictionaryContaining.m; sourceTree = ""; }; + BB3330701E28789385EAB1C9 /* HCIsEqualToNumber.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCIsEqualToNumber.h; sourceTree = ""; }; + BB33309F9D72ED2CF53532A8 /* NSInvocation+OCHamcrest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSInvocation+OCHamcrest.m"; sourceTree = ""; }; + BB3331127524965C9BCAD83A /* HCRequireNonNilObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCRequireNonNilObject.h; sourceTree = ""; }; + BB33312A13BF3686BE841AD7 /* HCIsCollectionContainingInOrder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCIsCollectionContainingInOrder.h; sourceTree = ""; }; + BB33312F8485C256B14867AF /* HCUnsignedLongLongReturnGetter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCUnsignedLongLongReturnGetter.h; sourceTree = ""; }; + BB333144F9FA03AC04F8027D /* HCStringContains.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCStringContains.h; sourceTree = ""; }; + BB3331619B45E4E82B0FE72A /* HCIsDictionaryContainingEntries.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsDictionaryContainingEntries.m; sourceTree = ""; }; + BB33317270E4A854112BCCEA /* HCIsNot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCIsNot.h; sourceTree = ""; }; + BB333191F9604EA23746459E /* HCReturnTypeHandlerChain.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCReturnTypeHandlerChain.h; sourceTree = ""; }; + BB3331A34E31C49FE230D9BE /* HCIsCollectionContaining.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCIsCollectionContaining.h; sourceTree = ""; }; + BB3331AAA2AB993E60458B6B /* HCReturnValueGetter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCReturnValueGetter.m; sourceTree = ""; }; + BB3331B51251AD3B252636B5 /* HCUnsignedIntReturnGetter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCUnsignedIntReturnGetter.h; sourceTree = ""; }; + BB3331B99B691F7BFEF48591 /* HCBoolReturnGetter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCBoolReturnGetter.m; sourceTree = ""; }; + BB3331C74ACFA7AB0675A7FE /* HCStringContainsInOrder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCStringContainsInOrder.h; sourceTree = ""; }; + BB3331C8CBB96396DDA74467 /* HCHasCount.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCHasCount.h; sourceTree = ""; }; + BB3331D467F6B759BE9C3246 /* HCDescribedAs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCDescribedAs.h; sourceTree = ""; }; + BB3331E3E2C324F3D4618E9D /* HCGenericTestFailureReporter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCGenericTestFailureReporter.m; sourceTree = ""; }; + BB3331EECFE91DCA05C34EFD /* HCIsCloseTo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsCloseTo.m; sourceTree = ""; }; + BB333215A6C1406E57ED34C4 /* HCStringDescription.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCStringDescription.h; sourceTree = ""; }; + BB33322097042E108614BDAD /* HCIsTypeOf.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsTypeOf.m; sourceTree = ""; }; + BB3332508DE228EEA28B7324 /* HCLongLongReturnGetter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCLongLongReturnGetter.h; sourceTree = ""; }; + BB333255C8855929B9264684 /* HCAllOf.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCAllOf.h; sourceTree = ""; }; + BB33328F1CBC78AC3D9B6E78 /* HCIsEmptyCollection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsEmptyCollection.m; sourceTree = ""; }; + BB3332CD4593BA0B619AE30C /* HCIsCollectionContainingInAnyOrder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCIsCollectionContainingInAnyOrder.h; sourceTree = ""; }; + BB33330C459CF68E13DDD1D6 /* HCLongLongReturnGetter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCLongLongReturnGetter.m; sourceTree = ""; }; + BB33331178EE64329AB6A6DA /* HCSubstringMatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCSubstringMatcher.m; sourceTree = ""; }; + BB333334135CAFF20A0E19FF /* HCLongReturnGetter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCLongReturnGetter.m; sourceTree = ""; }; + BB33333B48A97B0771A3C256 /* HCUnsignedLongReturnGetter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCUnsignedLongReturnGetter.m; sourceTree = ""; }; + BB33334930C42DF9114F9D34 /* HCIs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIs.m; sourceTree = ""; }; + BB33334CF68184328CD67F30 /* HCIsTypeOf.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCIsTypeOf.h; sourceTree = ""; }; + BB33338462C2743FF78AA51B /* HCIsEqualCompressingWhiteSpace.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCIsEqualCompressingWhiteSpace.h; sourceTree = ""; }; + BB333393BDEDD21E2CB1A2FF /* HCIsDictionaryContainingKey.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsDictionaryContainingKey.m; sourceTree = ""; }; + BB33339BAC2DACD2306832A8 /* HCSenTestFailureReporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCSenTestFailureReporter.h; sourceTree = ""; }; + BB3333AF4B760A982CF9F9E3 /* HCStringEndsWith.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCStringEndsWith.h; sourceTree = ""; }; + BB3333FD5A8D6F4A718D5D98 /* HCCollect.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCCollect.m; sourceTree = ""; }; + BB3334125896535BFB7DBBA7 /* HCHasDescription.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCHasDescription.m; sourceTree = ""; }; + BB33343C9B176C5DE07DE003 /* HCHasProperty.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCHasProperty.m; sourceTree = ""; }; + BB3334410C93469CF325AA1D /* HCConformsToProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCConformsToProtocol.h; sourceTree = ""; }; + BB33346362D58A13AA1BA54B /* HCIsCloseTo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCIsCloseTo.h; sourceTree = ""; }; + BB33347A9E4365F763632850 /* HCUnsignedLongLongReturnGetter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCUnsignedLongLongReturnGetter.m; sourceTree = ""; }; + BB33347EC840056B268877BF /* HCIsNot.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsNot.m; sourceTree = ""; }; + BB3334C2039E9686ABB73AE2 /* HCDescribedAs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCDescribedAs.m; sourceTree = ""; }; + BB3334EF9BFFB721161BB23F /* HCConformsToProtocol.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCConformsToProtocol.m; sourceTree = ""; }; + BB3334F31A5AB57A69174ECB /* HCMatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCMatcher.h; sourceTree = ""; }; + BB333503A24A236A19B8A468 /* HCIsEqual.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCIsEqual.h; sourceTree = ""; }; + BB333522A78D6A7D1A8414B3 /* HCIsIn.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsIn.m; sourceTree = ""; }; + BB333528C8E2ED8489ACB161 /* HCTestFailureReporter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCTestFailureReporter.m; sourceTree = ""; }; + BB33355AD08369793188986F /* HCObjectReturnGetter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCObjectReturnGetter.m; sourceTree = ""; }; + BB33356A6185FA4F1B6E240F /* OCHamcrest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCHamcrest.h; sourceTree = ""; }; + BB33359133039118AE88E2A8 /* HCIsNil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCIsNil.h; sourceTree = ""; }; + BB333594FB0AC2DAC3B73077 /* HCIsSame.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsSame.m; sourceTree = ""; }; + BB3335974ADA5C5408FE0F69 /* HCStringEndsWith.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCStringEndsWith.m; sourceTree = ""; }; + BB3335A8FE1E47B0F4407027 /* HCShortReturnGetter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCShortReturnGetter.h; sourceTree = ""; }; + BB3335CC804D0A774E5F1041 /* HCIsNil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsNil.m; sourceTree = ""; }; + BB3335EC3CC47489B4C5395E /* HCWrapInMatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCWrapInMatcher.m; sourceTree = ""; }; + BB33363375E9DE829A8AD41D /* HCShortReturnGetter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCShortReturnGetter.m; sourceTree = ""; }; + BB3336446380772E9629E08E /* HCAllOf.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCAllOf.m; sourceTree = ""; }; + BB33365155F84099730C9B56 /* HCClassMatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCClassMatcher.h; sourceTree = ""; }; + BB3336D2152CF1F88C8FF743 /* HCTestFailureReporterChain.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCTestFailureReporterChain.h; sourceTree = ""; }; + BB3336DDA8D0B1E91E937FD4 /* HCIsIn.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCIsIn.h; sourceTree = ""; }; + BB3336E4FAD686F2CFFF91B0 /* HCOrderingComparison.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCOrderingComparison.m; sourceTree = ""; }; + BB3336FA0B545673BD3630E2 /* HCStringStartsWith.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCStringStartsWith.h; sourceTree = ""; }; + BB3337085D9BEFA6D52E7EB0 /* HCTestFailureReporterChain.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCTestFailureReporterChain.m; sourceTree = ""; }; + BB33371136854FB7CAF2A5F7 /* HCIsEqualIgnoringCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCIsEqualIgnoringCase.h; sourceTree = ""; }; + BB333725B2814AE9CF044B5B /* HCIntReturnGetter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCIntReturnGetter.h; sourceTree = ""; }; + BB33373DA43C990FB5DA0D35 /* HCIsTypeOfTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsTypeOfTests.m; sourceTree = ""; }; + BB33377384F2BF4EA9694DA7 /* HCIsEqualCompressingWhiteSpace.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsEqualCompressingWhiteSpace.m; sourceTree = ""; }; + BB333785CDF67F8F0284B9CA /* HCBaseDescription.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCBaseDescription.m; sourceTree = ""; }; + BB333792C123FE94F1A3D447 /* HCAssertThat.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCAssertThat.h; sourceTree = ""; }; + BB3337B908D229FD08452B2D /* HCInvocationMatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCInvocationMatcher.m; sourceTree = ""; }; + BB3337CE41E8621958EAE8BF /* HCOrderingComparison.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCOrderingComparison.h; sourceTree = ""; }; + BB3337F8ADEA5FCE59018962 /* HCTestFailureReporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCTestFailureReporter.h; sourceTree = ""; }; + BB33380693E900C743EDF861 /* HCIsCollectionOnlyContaining.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsCollectionOnlyContaining.m; sourceTree = ""; }; + BB333830B0D43E7B79BB537C /* HCIsInstanceOf.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsInstanceOf.m; sourceTree = ""; }; + BB33384895217507C28D0896 /* HCIs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCIs.h; sourceTree = ""; }; + BB33386A5E683DD0DC1AACE2 /* HCIsInstanceOf.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCIsInstanceOf.h; sourceTree = ""; }; + BB33387896CF4C830AAE4633 /* HCHasDescription.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCHasDescription.h; sourceTree = ""; }; + BB3338AB92DDE4ACF56D7C1B /* HCIsCollectionContaining.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsCollectionContaining.m; sourceTree = ""; }; + BB3338B60F3A83AE527E91A7 /* HCSubstringMatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCSubstringMatcher.h; sourceTree = ""; }; + BB3338BCBEA080BE71CA0C9D /* HCBaseMatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCBaseMatcher.m; sourceTree = ""; }; + BB3338DE3E5C649EB399B649 /* HCUnsignedLongReturnGetter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCUnsignedLongReturnGetter.h; sourceTree = ""; }; + BB3338EDDD9B8C82083DD7FB /* HCIsEmptyCollection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCIsEmptyCollection.h; sourceTree = ""; }; + BB3339100D7159DBF2387893 /* HCIsDictionaryContainingValue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsDictionaryContainingValue.m; sourceTree = ""; }; + BB33391EC19272E15C3D2E1A /* HCGenericTestFailureReporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCGenericTestFailureReporter.h; sourceTree = ""; }; + BB333930FE07807045EEE866 /* HCTestFailure.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCTestFailure.h; sourceTree = ""; }; + BB33394F75CEE379A525320B /* HCXCTestFailureReporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCXCTestFailureReporter.h; sourceTree = ""; }; + BB3339614DAE31258BA73352 /* HCIntReturnGetter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIntReturnGetter.m; sourceTree = ""; }; + BB333986B013CA09B71D376B /* HCIsCollectionContainingInOrder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsCollectionContainingInOrder.m; sourceTree = ""; }; + BB3339B2CAF2C82161ED14F8 /* HCStringContainsInOrder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCStringContainsInOrder.m; sourceTree = ""; }; + BB3339BE4C0ECEAB7169772A /* NSInvocation+OCHamcrest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSInvocation+OCHamcrest.h"; sourceTree = ""; }; + BB333A258B9C1CDFB1B184C3 /* HCUnsignedShortReturnGetter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCUnsignedShortReturnGetter.h; sourceTree = ""; }; + BB333A5C2975038FF06BDCBA /* HCIsDictionaryContainingValue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCIsDictionaryContainingValue.h; sourceTree = ""; }; + BB333A5C3A9A42E4521010F1 /* HCUnsignedShortReturnGetter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCUnsignedShortReturnGetter.m; sourceTree = ""; }; + BB333A7430335F388E6DCF71 /* HCStringStartsWith.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCStringStartsWith.m; sourceTree = ""; }; + BB333A801AD666BCC23ED955 /* HCNumberAssert.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCNumberAssert.m; sourceTree = ""; }; + BB333A9273EFADF46D908BC4 /* HCIsEqualIgnoringCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsEqualIgnoringCase.m; sourceTree = ""; }; + BB333ABD683BCE8C8E9028C5 /* HCXCTestFailureReporter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCXCTestFailureReporter.m; sourceTree = ""; }; + BB333AF2F081FE96420F97C0 /* HCIsAnything.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsAnything.m; sourceTree = ""; }; + BB333B2A6FFEC019E0678999 /* HCHasProperty.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCHasProperty.h; sourceTree = ""; }; + BB333B7F19A5645CF7FC3E7E /* HCSelfDescribing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCSelfDescribing.h; sourceTree = ""; }; + BB333BC37F9AB8BFAB79AE16 /* HCIsCollectionOnlyContaining.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCIsCollectionOnlyContaining.h; sourceTree = ""; }; + BB333BD87164C606AE0BE027 /* HCReturnTypeHandlerChain.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCReturnTypeHandlerChain.m; sourceTree = ""; }; + BB333BE1D8EFF4596D019A40 /* HCUnsignedCharReturnGetter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCUnsignedCharReturnGetter.h; sourceTree = ""; }; + BB333BF1C94AD91D6F5F5A95 /* HCAnyOf.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCAnyOf.m; sourceTree = ""; }; + BB333C3F40A01E4269FA22BE /* HCLongReturnGetter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCLongReturnGetter.h; sourceTree = ""; }; + BB333C5BD261A2F9861F78A4 /* HCCharReturnGetter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCCharReturnGetter.h; sourceTree = ""; }; + BB333C70DFAD4A867B769381 /* HCClassMatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCClassMatcher.m; sourceTree = ""; }; + BB333C7DFDB2404398E9E6C5 /* HCCollect.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCCollect.h; sourceTree = ""; }; + BB333C88565D58DE2AAAAD15 /* HCRequireNonNilObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCRequireNonNilObject.m; sourceTree = ""; }; + BB333C93865B64F27B2639FE /* HCIsSame.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCIsSame.h; sourceTree = ""; }; + BB333C975A48CD4BC2EA4ED3 /* HCFloatReturnGetter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCFloatReturnGetter.m; sourceTree = ""; }; + BB333CA45EE4CF9D260DD50D /* HCUnsignedIntReturnGetter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCUnsignedIntReturnGetter.m; sourceTree = ""; }; + BB333CC6471D7E5CB41558EF /* HCReturnValueGetter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCReturnValueGetter.h; sourceTree = ""; }; + BB333CF8C391D5A08F331B45 /* HCAssertThat.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCAssertThat.m; sourceTree = ""; }; + BB333D00EB0E21D420A7B22B /* HCInvocationMatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCInvocationMatcher.h; sourceTree = ""; }; + BB333D2D401EA09BE78BF7E9 /* HCDescription.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCDescription.h; sourceTree = ""; }; + BB333D43717AD7243D63A4AB /* HCStringDescription.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCStringDescription.m; sourceTree = ""; }; + BB333D5C9BD053542A0530EB /* HCWrapInMatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCWrapInMatcher.h; sourceTree = ""; }; + BB333D869F9ECB46825CA726 /* HCBaseDescription.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCBaseDescription.h; sourceTree = ""; }; + BB333DC21DC44C0F76ED3DB6 /* HCIsEqualToNumber.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsEqualToNumber.m; sourceTree = ""; }; + BB333DFF0305457C0CD69303 /* HCIsEqual.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsEqual.m; sourceTree = ""; }; + BB333E412026940E16FEC9C0 /* HCIsDictionaryContainingKey.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCIsDictionaryContainingKey.h; sourceTree = ""; }; + BB333E53F31A4853CD1B9548 /* HCBaseMatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCBaseMatcher.h; sourceTree = ""; }; + BB333E594CE94324024C2E19 /* HCHasCount.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCHasCount.m; sourceTree = ""; }; + BB333EA4C4546D46CF8A4044 /* HCStringContains.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCStringContains.m; sourceTree = ""; }; + BB333EC183D2675A9DF05F67 /* HCIsAnything.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCIsAnything.h; sourceTree = ""; }; + BB333EDC6C01648EEBC00D5E /* HCBoolReturnGetter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCBoolReturnGetter.h; sourceTree = ""; }; + BB333EEBE8C48A68933572E9 /* HCNumberAssert.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCNumberAssert.h; sourceTree = ""; }; + BB333F5AFEB5565130751633 /* HCUnsignedCharReturnGetter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCUnsignedCharReturnGetter.m; sourceTree = ""; }; + BB333F6051CD0B739527337A /* HCSenTestFailureReporter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCSenTestFailureReporter.m; sourceTree = ""; }; + BB333F6535F43C37CBF517FA /* HCTestFailure.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCTestFailure.m; sourceTree = ""; }; + BB333F67A18EA568D7683A58 /* HCDoubleReturnGetter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCDoubleReturnGetter.h; sourceTree = ""; }; + BB333F8E4CF5AD791C99B6FA /* HCIsDictionaryContainingEntries.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCIsDictionaryContainingEntries.h; sourceTree = ""; }; + BB333FACCFA7D57857673665 /* HCObjectReturnGetter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCObjectReturnGetter.h; sourceTree = ""; }; + BB333FDE3455183AB9302310 /* HCIsDictionaryContaining.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCIsDictionaryContaining.h; sourceTree = ""; }; + BB333FE413C7E8FA9C811BF1 /* HCIsCollectionContainingInAnyOrder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCIsCollectionContainingInAnyOrder.m; sourceTree = ""; }; + BB333FED5158848B95B043F1 /* HCCharReturnGetter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HCCharReturnGetter.m; sourceTree = ""; }; + BB333FF31E6FD6CB3A0DC45E /* HCFloatReturnGetter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HCFloatReturnGetter.h; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 081BEE5F1345979F003F846A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 081BEE681345979F003F846A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 081BEE721345979F003F846A /* libochamcrest.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 087601DE13440806001B439B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 087601F313440807001B439B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 087601FB13440807001B439B /* OCHamcrest.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 24C5BFE51C1777D900C2BAFD /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 24C5BFF21C17781400C2BAFD /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 24C5BFFF1C17782500C2BAFD /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 082E7C2813FF676E004A22FE /* Core */ = { + isa = PBXGroup; + children = ( + 7458920E1A2AAD2C00BAAC76 /* AssertWithTimeoutTests.m */, + 74D7A7B41A2B902F00CD6CC0 /* DiagnosingMatcherTest.m */, + 082E7C2C13FF676E004A22FE /* HCBaseMatcherTests.m */, + 082E7C2D13FF676E004A22FE /* HCInvocationMatcherTests.m */, + 082E7C2E13FF676E004A22FE /* HCStringDescriptionTests.m */, + 609E99ADF272619359F01300 /* HCTestFailureReporterChainTests.m */, + 482FE2D21E6DDC2B00AC0009 /* HCWrapInMatcherTests.m */, + 082E7C2B13FF676E004A22FE /* MockSenTestCase.m */, + ); + path = Core; + sourceTree = ""; + }; + 0846B6DB13EE5F9D00EA68E8 /* Decorator */ = { + isa = PBXGroup; + children = ( + 0846B6DC13EE5F9D00EA68E8 /* HCDescribedAsTests.m */, + 0846B6DD13EE5F9D00EA68E8 /* HCIsTests.m */, + 0846B6DE13EE5F9D00EA68E8 /* NeverMatch.h */, + 0846B6DF13EE5F9D00EA68E8 /* NeverMatch.m */, + ); + path = Decorator; + sourceTree = ""; + }; + 0846B6E613EE5FB400EA68E8 /* Logical */ = { + isa = PBXGroup; + children = ( + 0846B6E713EE5FB400EA68E8 /* HCAllOfTests.m */, + 0846B6E813EE5FB400EA68E8 /* HCAnyOfTests.m */, + 0846B6E913EE5FB400EA68E8 /* HCIsAnythingTests.m */, + 0846B6EA13EE5FB400EA68E8 /* HCIsNotTests.m */, + ); + path = Logical; + sourceTree = ""; + }; + 087601D613440806001B439B = { + isa = PBXGroup; + children = ( + 08F39BAA1711426200745958 /* CHANGELOG.md */, + BB33356A6185FA4F1B6E240F /* OCHamcrest.h */, + BB3336D16B2F9F646F4DE5A9 /* Core */, + BB333E40286A981A2343E031 /* Library */, + 0876032F13440AD8001B439B /* UnitTests */, + 0876038C13440B64001B439B /* Supporting Files */, + 087601E313440806001B439B /* Products */, + ); + indentWidth = 4; + sourceTree = ""; + tabWidth = 4; + usesTabs = 0; + }; + 087601E313440806001B439B /* Products */ = { + isa = PBXGroup; + children = ( + 087601E213440806001B439B /* OCHamcrest.framework */, + 087601F713440807001B439B /* OCHamcrestTests.xctest */, + 081BEE621345979F003F846A /* libochamcrest.a */, + 081BEE6C1345979F003F846A /* libochamcrestTests.xctest */, + 24C5BFE91C1777D900C2BAFD /* OCHamcrest.framework */, + 24C5BFF61C17781400C2BAFD /* OCHamcrest.framework */, + 24C5C0031C17782500C2BAFD /* OCHamcrest.framework */, + ); + name = Products; + sourceTree = ""; + }; + 0876032F13440AD8001B439B /* UnitTests */ = { + isa = PBXGroup; + children = ( + 082E7C2813FF676E004A22FE /* Core */, + 0876033413440AD8001B439B /* Collection */, + 0846B6DB13EE5F9D00EA68E8 /* Decorator */, + 0846B6E613EE5FB400EA68E8 /* Logical */, + 0876035213440AD8001B439B /* Number */, + 0876035713440AD8001B439B /* Object */, + 0876035A13440AD8001B439B /* Text */, + 609E97D2700DBCFB2161C50A /* InterceptingTestCase.m */, + 609E95FA7156B2A6FD85D509 /* InterceptingTestCase.h */, + 609E93C91C2B1DFA3BC3F9A8 /* MatcherTestCase.h */, + 609E91D3F5F10354528F7FF8 /* MatcherTestCase.m */, + ); + name = UnitTests; + path = Tests; + sourceTree = ""; + }; + 0876033413440AD8001B439B /* Collection */ = { + isa = PBXGroup; + children = ( + 0876033513440AD8001B439B /* FakeWithCount.h */, + 0876033613440AD8001B439B /* FakeWithCount.m */, + 0876033713440AD8001B439B /* FakeWithoutCount.h */, + 0876033813440AD8001B439B /* FakeWithoutCount.m */, + 7460456B1A2964AF00196267 /* HCEveryTests.m */, + 0876033913440AD8001B439B /* HCHasCountTests.m */, + 0876033A13440AD8001B439B /* HCIsCollectionContainingInAnyOrderTests.m */, + 0876033B13440AD8001B439B /* HCIsCollectionContainingInOrderTests.m */, + 609E99FE80B578287C9DBDFE /* HCIsCollectionContainingInRelativeOrderTests.m */, + 0876033C13440AD8001B439B /* HCIsCollectionContainingTests.m */, + 0876033D13440AD8001B439B /* HCIsCollectionOnlyContainingTests.m */, + 0876033E13440AD8001B439B /* HCIsDictionaryContainingEntriesTests.m */, + 0876033F13440AD8001B439B /* HCIsDictionaryContainingKeyTests.m */, + 0876034013440AD8001B439B /* HCIsDictionaryContainingTests.m */, + 0876034113440AD8001B439B /* HCIsDictionaryContainingValueTests.m */, + 0876034213440AD8001B439B /* HCIsEmptyCollectionTests.m */, + 0876034313440AD8001B439B /* HCIsInTests.m */, + 31DDCCF46E1766AF4AC97BA5 /* Mismatchable.h */, + 31DDCC467D6EFFC559012D61 /* Mismatchable.m */, + ); + path = Collection; + sourceTree = ""; + }; + 0876035213440AD8001B439B /* Number */ = { + isa = PBXGroup; + children = ( + 0876035313440AD8001B439B /* HCIsCloseToTests.m */, + 0876035413440AD8001B439B /* HCIsEqualToNumberTests.m */, + 74153B5E1A535FEA00FEF450 /* HCIsTrueFalseTests.m */, + 0876035513440AD8001B439B /* HCNumberAssertTests.m */, + 0876035613440AD8001B439B /* HCOrderingComparisonTests.m */, + ); + path = Number; + sourceTree = ""; + }; + 0876035713440AD8001B439B /* Object */ = { + isa = PBXGroup; + children = ( + 609E960725E106DC73EA9D60 /* HCArgumentCaptorTests.m */, + 5F2773441436F81000B9683A /* HCConformsToProtocolTests.m */, + 0876035813440AD8001B439B /* HCHasDescriptionTests.m */, + 77C7B8761429361200DE60CC /* HCHasPropertyTests.m */, + 0846B6F313EE5FD200EA68E8 /* HCIsEqualTests.m */, + 0846B6F413EE5FD200EA68E8 /* HCIsInstanceOfTests.m */, + 0846B6F513EE5FD200EA68E8 /* HCIsNilTests.m */, + 0846B6F613EE5FD200EA68E8 /* HCIsSameTests.m */, + BB33373DA43C990FB5DA0D35 /* HCIsTypeOfTests.m */, + 747776C61A3F5060000A6E1D /* HCThrowsExceptionTests.m */, + 0850A44C17165D2A0019BBE0 /* SomeClassAndSubclass.h */, + 0850A44D17165D2A0019BBE0 /* SomeClassAndSubclass.m */, + ); + path = Object; + sourceTree = ""; + }; + 0876035A13440AD8001B439B /* Text */ = { + isa = PBXGroup; + children = ( + 0876035C13440AD8001B439B /* HCIsEqualCompressingWhiteSpaceTests.m */, + 0876035B13440AD8001B439B /* HCIsEqualIgnoringCaseTests.m */, + 0876035D13440AD8001B439B /* HCStringContainsTests.m */, + 088FB0AA136A6DEA00C191E1 /* HCStringContainsInOrderTests.m */, + 0876035E13440AD8001B439B /* HCStringEndsWithTests.m */, + 0876035F13440AD8001B439B /* HCStringStartsWithTests.m */, + ); + path = Text; + sourceTree = ""; + }; + 0876038C13440B64001B439B /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 0876038D13440B80001B439B /* OCHamcrest-Info.plist */, + 0876039113440BB0001B439B /* Tests-Info.plist */, + 0E49BBDB1C47498C00418A3C /* XcodeWarnings.xcconfig */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + BB333072EBF64C385754421B /* Object */ = { + isa = PBXGroup; + children = ( + 609E9B6B3B80E4914607E267 /* HCArgumentCaptor.h */, + 609E92AABBB75D3F5E47A0E4 /* HCArgumentCaptor.m */, + BB33365155F84099730C9B56 /* HCClassMatcher.h */, + BB333C70DFAD4A867B769381 /* HCClassMatcher.m */, + BB3334410C93469CF325AA1D /* HCConformsToProtocol.h */, + BB3334EF9BFFB721161BB23F /* HCConformsToProtocol.m */, + BB33387896CF4C830AAE4633 /* HCHasDescription.h */, + BB3334125896535BFB7DBBA7 /* HCHasDescription.m */, + BB333B2A6FFEC019E0678999 /* HCHasProperty.h */, + BB33343C9B176C5DE07DE003 /* HCHasProperty.m */, + BB333503A24A236A19B8A468 /* HCIsEqual.h */, + BB333DFF0305457C0CD69303 /* HCIsEqual.m */, + BB33386A5E683DD0DC1AACE2 /* HCIsInstanceOf.h */, + BB333830B0D43E7B79BB537C /* HCIsInstanceOf.m */, + BB33359133039118AE88E2A8 /* HCIsNil.h */, + BB3335CC804D0A774E5F1041 /* HCIsNil.m */, + BB333C93865B64F27B2639FE /* HCIsSame.h */, + BB333594FB0AC2DAC3B73077 /* HCIsSame.m */, + BB33334CF68184328CD67F30 /* HCIsTypeOf.h */, + BB33322097042E108614BDAD /* HCIsTypeOf.m */, + 74FDDD131A3FF39900177999 /* HCThrowsException.h */, + 74FDDD141A3FF39900177999 /* HCThrowsException.m */, + ); + path = Object; + sourceTree = ""; + }; + BB3332B119BE5F32E3F02CE3 /* TestFailureReporters */ = { + isa = PBXGroup; + children = ( + BB33391EC19272E15C3D2E1A /* HCGenericTestFailureReporter.h */, + BB3331E3E2C324F3D4618E9D /* HCGenericTestFailureReporter.m */, + BB33339BAC2DACD2306832A8 /* HCSenTestFailureReporter.h */, + BB333F6051CD0B739527337A /* HCSenTestFailureReporter.m */, + BB333930FE07807045EEE866 /* HCTestFailure.h */, + BB333F6535F43C37CBF517FA /* HCTestFailure.m */, + BB3337F8ADEA5FCE59018962 /* HCTestFailureReporter.h */, + BB333528C8E2ED8489ACB161 /* HCTestFailureReporter.m */, + BB3336D2152CF1F88C8FF743 /* HCTestFailureReporterChain.h */, + BB3337085D9BEFA6D52E7EB0 /* HCTestFailureReporterChain.m */, + BB33394F75CEE379A525320B /* HCXCTestFailureReporter.h */, + BB333ABD683BCE8C8E9028C5 /* HCXCTestFailureReporter.m */, + ); + path = TestFailureReporters; + sourceTree = ""; + }; + BB3336D16B2F9F646F4DE5A9 /* Core */ = { + isa = PBXGroup; + children = ( + BB333792C123FE94F1A3D447 /* HCAssertThat.h */, + BB333CF8C391D5A08F331B45 /* HCAssertThat.m */, + BB333D869F9ECB46825CA726 /* HCBaseDescription.h */, + BB333785CDF67F8F0284B9CA /* HCBaseDescription.m */, + BB333E53F31A4853CD1B9548 /* HCBaseMatcher.h */, + BB3338BCBEA080BE71CA0C9D /* HCBaseMatcher.m */, + BB333D2D401EA09BE78BF7E9 /* HCDescription.h */, + 31DDCE62F97F2042F0A8BDAE /* HCDiagnosingMatcher.h */, + 31DDCB570769099D33C22810 /* HCDiagnosingMatcher.m */, + BB3334F31A5AB57A69174ECB /* HCMatcher.h */, + BB333B7F19A5645CF7FC3E7E /* HCSelfDescribing.h */, + BB333215A6C1406E57ED34C4 /* HCStringDescription.h */, + BB333D43717AD7243D63A4AB /* HCStringDescription.m */, + BB33378F4CD8AE4C7EFC1CF0 /* Helpers */, + ); + path = Core; + sourceTree = ""; + }; + BB33378F4CD8AE4C7EFC1CF0 /* Helpers */ = { + isa = PBXGroup; + children = ( + BB333B223BB1036A1090E089 /* ReturnValueGetters */, + BB3332B119BE5F32E3F02CE3 /* TestFailureReporters */, + BB333C7DFDB2404398E9E6C5 /* HCCollect.h */, + BB3333FD5A8D6F4A718D5D98 /* HCCollect.m */, + BB333D00EB0E21D420A7B22B /* HCInvocationMatcher.h */, + BB3337B908D229FD08452B2D /* HCInvocationMatcher.m */, + BB3331127524965C9BCAD83A /* HCRequireNonNilObject.h */, + BB333C88565D58DE2AAAAD15 /* HCRequireNonNilObject.m */, + 609E92B06196813A410D4FF4 /* HCRunloopRunner.h */, + 609E9BDAE6E9C3466DF16818 /* HCRunloopRunner.m */, + BB333D5C9BD053542A0530EB /* HCWrapInMatcher.h */, + BB3335EC3CC47489B4C5395E /* HCWrapInMatcher.m */, + BB3339BE4C0ECEAB7169772A /* NSInvocation+OCHamcrest.h */, + BB33309F9D72ED2CF53532A8 /* NSInvocation+OCHamcrest.m */, + ); + path = Helpers; + sourceTree = ""; + }; + BB3338CCF66D717444E7C679 /* Logical */ = { + isa = PBXGroup; + children = ( + BB333255C8855929B9264684 /* HCAllOf.h */, + BB3336446380772E9629E08E /* HCAllOf.m */, + BB333034CD6A38647E176A9A /* HCAnyOf.h */, + BB333BF1C94AD91D6F5F5A95 /* HCAnyOf.m */, + BB333EC183D2675A9DF05F67 /* HCIsAnything.h */, + BB333AF2F081FE96420F97C0 /* HCIsAnything.m */, + BB33317270E4A854112BCCEA /* HCIsNot.h */, + BB33347EC840056B268877BF /* HCIsNot.m */, + ); + path = Logical; + sourceTree = ""; + }; + BB333B223BB1036A1090E089 /* ReturnValueGetters */ = { + isa = PBXGroup; + children = ( + BB333EDC6C01648EEBC00D5E /* HCBoolReturnGetter.h */, + BB3331B99B691F7BFEF48591 /* HCBoolReturnGetter.m */, + BB333C5BD261A2F9861F78A4 /* HCCharReturnGetter.h */, + BB333FED5158848B95B043F1 /* HCCharReturnGetter.m */, + BB333F67A18EA568D7683A58 /* HCDoubleReturnGetter.h */, + BB33303898210A59268030C8 /* HCDoubleReturnGetter.m */, + BB333FF31E6FD6CB3A0DC45E /* HCFloatReturnGetter.h */, + BB333C975A48CD4BC2EA4ED3 /* HCFloatReturnGetter.m */, + BB333725B2814AE9CF044B5B /* HCIntReturnGetter.h */, + BB3339614DAE31258BA73352 /* HCIntReturnGetter.m */, + BB3332508DE228EEA28B7324 /* HCLongLongReturnGetter.h */, + BB33330C459CF68E13DDD1D6 /* HCLongLongReturnGetter.m */, + BB333C3F40A01E4269FA22BE /* HCLongReturnGetter.h */, + BB333334135CAFF20A0E19FF /* HCLongReturnGetter.m */, + BB333FACCFA7D57857673665 /* HCObjectReturnGetter.h */, + BB33355AD08369793188986F /* HCObjectReturnGetter.m */, + BB333CC6471D7E5CB41558EF /* HCReturnValueGetter.h */, + BB3331AAA2AB993E60458B6B /* HCReturnValueGetter.m */, + BB333191F9604EA23746459E /* HCReturnTypeHandlerChain.h */, + BB333BD87164C606AE0BE027 /* HCReturnTypeHandlerChain.m */, + BB3335A8FE1E47B0F4407027 /* HCShortReturnGetter.h */, + BB33363375E9DE829A8AD41D /* HCShortReturnGetter.m */, + BB333BE1D8EFF4596D019A40 /* HCUnsignedCharReturnGetter.h */, + BB333F5AFEB5565130751633 /* HCUnsignedCharReturnGetter.m */, + BB3331B51251AD3B252636B5 /* HCUnsignedIntReturnGetter.h */, + BB333CA45EE4CF9D260DD50D /* HCUnsignedIntReturnGetter.m */, + BB33312F8485C256B14867AF /* HCUnsignedLongLongReturnGetter.h */, + BB33347A9E4365F763632850 /* HCUnsignedLongLongReturnGetter.m */, + BB3338DE3E5C649EB399B649 /* HCUnsignedLongReturnGetter.h */, + BB33333B48A97B0771A3C256 /* HCUnsignedLongReturnGetter.m */, + BB333A258B9C1CDFB1B184C3 /* HCUnsignedShortReturnGetter.h */, + BB333A5C3A9A42E4521010F1 /* HCUnsignedShortReturnGetter.m */, + ); + path = ReturnValueGetters; + sourceTree = ""; + }; + BB333BCFCFCA269190F9F148 /* Number */ = { + isa = PBXGroup; + children = ( + BB33346362D58A13AA1BA54B /* HCIsCloseTo.h */, + BB3331EECFE91DCA05C34EFD /* HCIsCloseTo.m */, + BB3330701E28789385EAB1C9 /* HCIsEqualToNumber.h */, + BB333DC21DC44C0F76ED3DB6 /* HCIsEqualToNumber.m */, + 74153B611A53606100FEF450 /* HCIsTrueFalse.h */, + 74153B621A53606100FEF450 /* HCIsTrueFalse.m */, + BB333EEBE8C48A68933572E9 /* HCNumberAssert.h */, + BB333A801AD666BCC23ED955 /* HCNumberAssert.m */, + BB3337CE41E8621958EAE8BF /* HCOrderingComparison.h */, + BB3336E4FAD686F2CFFF91B0 /* HCOrderingComparison.m */, + ); + path = Number; + sourceTree = ""; + }; + BB333D13C0B32471699C8E7F /* Decorator */ = { + isa = PBXGroup; + children = ( + BB3331D467F6B759BE9C3246 /* HCDescribedAs.h */, + BB3334C2039E9686ABB73AE2 /* HCDescribedAs.m */, + BB33384895217507C28D0896 /* HCIs.h */, + BB33334930C42DF9114F9D34 /* HCIs.m */, + ); + path = Decorator; + sourceTree = ""; + }; + BB333E40286A981A2343E031 /* Library */ = { + isa = PBXGroup; + children = ( + BB333F0D2EFE975002ABB45C /* Collection */, + BB333D13C0B32471699C8E7F /* Decorator */, + BB3338CCF66D717444E7C679 /* Logical */, + BB333BCFCFCA269190F9F148 /* Number */, + BB333072EBF64C385754421B /* Object */, + BB333EA5543B2EAB4E34B3CF /* Text */, + ); + path = Library; + sourceTree = ""; + }; + BB333EA5543B2EAB4E34B3CF /* Text */ = { + isa = PBXGroup; + children = ( + BB33338462C2743FF78AA51B /* HCIsEqualCompressingWhiteSpace.h */, + BB33377384F2BF4EA9694DA7 /* HCIsEqualCompressingWhiteSpace.m */, + BB33371136854FB7CAF2A5F7 /* HCIsEqualIgnoringCase.h */, + BB333A9273EFADF46D908BC4 /* HCIsEqualIgnoringCase.m */, + BB333144F9FA03AC04F8027D /* HCStringContains.h */, + BB333EA4C4546D46CF8A4044 /* HCStringContains.m */, + BB3331C74ACFA7AB0675A7FE /* HCStringContainsInOrder.h */, + BB3339B2CAF2C82161ED14F8 /* HCStringContainsInOrder.m */, + BB3333AF4B760A982CF9F9E3 /* HCStringEndsWith.h */, + BB3335974ADA5C5408FE0F69 /* HCStringEndsWith.m */, + BB3336FA0B545673BD3630E2 /* HCStringStartsWith.h */, + BB333A7430335F388E6DCF71 /* HCStringStartsWith.m */, + BB3338B60F3A83AE527E91A7 /* HCSubstringMatcher.h */, + BB33331178EE64329AB6A6DA /* HCSubstringMatcher.m */, + ); + path = Text; + sourceTree = ""; + }; + BB333F0D2EFE975002ABB45C /* Collection */ = { + isa = PBXGroup; + children = ( + 746045651A29625E00196267 /* HCEvery.h */, + 746045661A29625E00196267 /* HCEvery.m */, + BB3331C8CBB96396DDA74467 /* HCHasCount.h */, + BB333E594CE94324024C2E19 /* HCHasCount.m */, + BB3331A34E31C49FE230D9BE /* HCIsCollectionContaining.h */, + BB3338AB92DDE4ACF56D7C1B /* HCIsCollectionContaining.m */, + BB3332CD4593BA0B619AE30C /* HCIsCollectionContainingInAnyOrder.h */, + BB333FE413C7E8FA9C811BF1 /* HCIsCollectionContainingInAnyOrder.m */, + BB33312A13BF3686BE841AD7 /* HCIsCollectionContainingInOrder.h */, + BB333986B013CA09B71D376B /* HCIsCollectionContainingInOrder.m */, + 0E5ECE6B1BC20C3300C15D2A /* HCIsCollectionContainingInRelativeOrder.h */, + 0E5ECE6C1BC20C3300C15D2A /* HCIsCollectionContainingInRelativeOrder.m */, + BB333BC37F9AB8BFAB79AE16 /* HCIsCollectionOnlyContaining.h */, + BB33380693E900C743EDF861 /* HCIsCollectionOnlyContaining.m */, + BB333FDE3455183AB9302310 /* HCIsDictionaryContaining.h */, + BB333049BE335209B4207B2A /* HCIsDictionaryContaining.m */, + BB333F8E4CF5AD791C99B6FA /* HCIsDictionaryContainingEntries.h */, + BB3331619B45E4E82B0FE72A /* HCIsDictionaryContainingEntries.m */, + BB333E412026940E16FEC9C0 /* HCIsDictionaryContainingKey.h */, + BB333393BDEDD21E2CB1A2FF /* HCIsDictionaryContainingKey.m */, + BB333A5C2975038FF06BDCBA /* HCIsDictionaryContainingValue.h */, + BB3339100D7159DBF2387893 /* HCIsDictionaryContainingValue.m */, + BB3338EDDD9B8C82083DD7FB /* HCIsEmptyCollection.h */, + BB33328F1CBC78AC3D9B6E78 /* HCIsEmptyCollection.m */, + BB3336DDA8D0B1E91E937FD4 /* HCIsIn.h */, + BB333522A78D6A7D1A8414B3 /* HCIsIn.m */, + ); + path = Collection; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 081BEE601345979F003F846A /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 486C64412062250D00A1BFDE /* HCWrapInMatcher.h in Headers */, + 486C643F2062250000A1BFDE /* HCInvocationMatcher.h in Headers */, + 486C64312062241D00A1BFDE /* HCAssertThat.h in Headers */, + 486C64322062245600A1BFDE /* HCBaseDescription.h in Headers */, + 486C64332062245800A1BFDE /* HCBaseMatcher.h in Headers */, + 486C64342062245B00A1BFDE /* HCDescription.h in Headers */, + 31DDCDCEAABD4CB0D7580AFE /* HCDiagnosingMatcher.h in Headers */, + 486C64352062245F00A1BFDE /* HCMatcher.h in Headers */, + 486C64372062247200A1BFDE /* HCStringDescription.h in Headers */, + 486C64362062246100A1BFDE /* HCSelfDescribing.h in Headers */, + 746045681A29625E00196267 /* HCEvery.h in Headers */, + 486C64432062253900A1BFDE /* HCHasCount.h in Headers */, + 486C64442062253D00A1BFDE /* HCIsCollectionContaining.h in Headers */, + 486C64452062254300A1BFDE /* HCIsCollectionContainingInAnyOrder.h in Headers */, + 486C64462062254600A1BFDE /* HCIsCollectionContainingInOrder.h in Headers */, + 486C64472062254E00A1BFDE /* HCIsCollectionContainingInRelativeOrder.h in Headers */, + 486C64482062255100A1BFDE /* HCIsCollectionOnlyContaining.h in Headers */, + 486C64492062255300A1BFDE /* HCIsDictionaryContaining.h in Headers */, + 486C644A2062255700A1BFDE /* HCIsDictionaryContainingEntries.h in Headers */, + 486C644B2062255900A1BFDE /* HCIsDictionaryContainingKey.h in Headers */, + 486C644C2062255B00A1BFDE /* HCIsDictionaryContainingValue.h in Headers */, + 486C644D2062256100A1BFDE /* HCIsEmptyCollection.h in Headers */, + 486C644E2062256400A1BFDE /* HCIsIn.h in Headers */, + 486C644F2062256B00A1BFDE /* HCDescribedAs.h in Headers */, + 486C64502062256E00A1BFDE /* HCIs.h in Headers */, + 486C64512062257000A1BFDE /* HCAllOf.h in Headers */, + 486C64522062257200A1BFDE /* HCAnyOf.h in Headers */, + 486C64532062257500A1BFDE /* HCIsAnything.h in Headers */, + 486C64542062257800A1BFDE /* HCIsNot.h in Headers */, + 486C64552062257B00A1BFDE /* HCIsCloseTo.h in Headers */, + 486C64562062258400A1BFDE /* HCIsEqualToNumber.h in Headers */, + 486C64572062258600A1BFDE /* HCIsTrueFalse.h in Headers */, + 486C64582062258800A1BFDE /* HCNumberAssert.h in Headers */, + 486C64592062258A00A1BFDE /* HCOrderingComparison.h in Headers */, + 486C645A2062258F00A1BFDE /* HCArgumentCaptor.h in Headers */, + 486C645B2062259700A1BFDE /* HCClassMatcher.h in Headers */, + 486C645C2062259A00A1BFDE /* HCConformsToProtocol.h in Headers */, + 486C645D2062259C00A1BFDE /* HCHasDescription.h in Headers */, + 486C645E2062259E00A1BFDE /* HCHasProperty.h in Headers */, + 486C645F206225A100A1BFDE /* HCIsEqual.h in Headers */, + 486C6460206225A700A1BFDE /* HCIsInstanceOf.h in Headers */, + 486C6461206225AA00A1BFDE /* HCIsNil.h in Headers */, + 486C6462206225AD00A1BFDE /* HCIsSame.h in Headers */, + 486C6463206225AF00A1BFDE /* HCIsTypeOf.h in Headers */, + 486C6464206225BB00A1BFDE /* HCThrowsException.h in Headers */, + 486C6465206225C700A1BFDE /* HCIsEqualCompressingWhiteSpace.h in Headers */, + 486C6466206225CA00A1BFDE /* HCIsEqualIgnoringCase.h in Headers */, + 486C6467206225CD00A1BFDE /* HCStringContains.h in Headers */, + 486C6468206225D100A1BFDE /* HCStringContainsInOrder.h in Headers */, + 486C6469206225D400A1BFDE /* HCStringEndsWith.h in Headers */, + 486C646A206225D700A1BFDE /* HCStringStartsWith.h in Headers */, + 486C646B206225D900A1BFDE /* HCSubstringMatcher.h in Headers */, + 486C646C2062274100A1BFDE /* OCHamcrest.h in Headers */, + 486C64402062250400A1BFDE /* HCRequireNonNilObject.h in Headers */, + 486C643E206224FB00A1BFDE /* HCCollect.h in Headers */, + 486C643A206224EB00A1BFDE /* HCTestFailure.h in Headers */, + 486C643B206224EE00A1BFDE /* HCTestFailureReporter.h in Headers */, + 486C643C206224F300A1BFDE /* HCTestFailureReporterChain.h in Headers */, + BB3339A4AEEDCDF3C4E96491 /* HCBoolReturnGetter.h in Headers */, + BB33365B91A88641580F886E /* HCCharReturnGetter.h in Headers */, + BB333031608DFFD2E76CD745 /* HCDoubleReturnGetter.h in Headers */, + BB333B4B561F53C31972B9D9 /* HCFloatReturnGetter.h in Headers */, + BB333494B740EF5FC786BB56 /* HCIntReturnGetter.h in Headers */, + BB333523DC58D4FD4797B9C7 /* HCLongLongReturnGetter.h in Headers */, + BB3332191B1A602F69B8CC77 /* HCLongReturnGetter.h in Headers */, + BB3334425297552B3C06F861 /* HCObjectReturnGetter.h in Headers */, + BB3338179D48057215091538 /* HCReturnValueGetter.h in Headers */, + BB333BE50C075D306EDA2E98 /* HCReturnTypeHandlerChain.h in Headers */, + BB333D83E68045BBEA3B3796 /* HCShortReturnGetter.h in Headers */, + BB3332EA9B7B56B9364B4023 /* HCUnsignedCharReturnGetter.h in Headers */, + BB333F90C371ADB4443F7CB0 /* HCUnsignedIntReturnGetter.h in Headers */, + BB33318EE99C688C6076DEBE /* HCUnsignedLongLongReturnGetter.h in Headers */, + BB333625D9CC6652DA94FF09 /* HCUnsignedLongReturnGetter.h in Headers */, + BB333A10646B1A68ACCDC472 /* HCUnsignedShortReturnGetter.h in Headers */, + 486C6438206224E300A1BFDE /* HCGenericTestFailureReporter.h in Headers */, + 486C6439206224E700A1BFDE /* HCSenTestFailureReporter.h in Headers */, + 486C643D206224F700A1BFDE /* HCXCTestFailureReporter.h in Headers */, + 609E9F22CD4AE2C7290741BA /* HCRunloopRunner.h in Headers */, + 486C64422062251200A1BFDE /* NSInvocation+OCHamcrest.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 087601DF13440806001B439B /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + BB3338188B4D09D845A55E00 /* HCAssertThat.h in Headers */, + BB333D3CAC82C5DFD2D18F2D /* HCBaseDescription.h in Headers */, + BB3333754B75C34B2EF2B4E1 /* HCBaseMatcher.h in Headers */, + BB333A11937F03ABF822AFA7 /* HCDescription.h in Headers */, + BB33395D7C71ECDB178C10F9 /* HCMatcher.h in Headers */, + BB3337BBAAC8E23B9A74CBF1 /* HCSelfDescribing.h in Headers */, + BB3331A2CC8B05A72A0E3E8B /* HCStringDescription.h in Headers */, + BB33328AC0BB3E658B1BB14F /* HCHasCount.h in Headers */, + BB333C8FF5557126F41B7291 /* HCIsCollectionContaining.h in Headers */, + BB333083DABBB403F4E3A935 /* HCIsCollectionContainingInAnyOrder.h in Headers */, + BB33367DD8C7A4D5F5F96E81 /* HCIsCollectionContainingInOrder.h in Headers */, + BB3331012E23C3795F3A1280 /* HCIsCollectionOnlyContaining.h in Headers */, + 74FDDD151A3FF39900177999 /* HCThrowsException.h in Headers */, + BB333352385755A59DEC95BD /* HCIsDictionaryContaining.h in Headers */, + BB33311A38FFA6C158C5F9B3 /* HCIsDictionaryContainingEntries.h in Headers */, + BB333643284129107CAD7AB8 /* HCIsDictionaryContainingKey.h in Headers */, + BB333E4BEBC363D5F315B27F /* HCIsDictionaryContainingValue.h in Headers */, + BB33311FC27FAE5DF6F82911 /* HCIsEmptyCollection.h in Headers */, + BB3338FF145C0BCE0B1A6C33 /* HCIsIn.h in Headers */, + BB33311F075C5E807D23AF38 /* HCDescribedAs.h in Headers */, + BB3338A5F9D50886A7A82129 /* HCIs.h in Headers */, + BB333BA6882190C2BDCABECC /* HCAllOf.h in Headers */, + BB333B08E01B2E88828DA51C /* HCAnyOf.h in Headers */, + BB333DF4A40F8816030C14DE /* HCIsAnything.h in Headers */, + BB33366E651777BD9B2A98FD /* HCIsNot.h in Headers */, + BB3339F396185713397718B5 /* HCIsCloseTo.h in Headers */, + BB333B9914086A169E24347E /* HCIsEqualToNumber.h in Headers */, + BB333839A36AEC819CDABEBF /* HCNumberAssert.h in Headers */, + BB333EB3C38A3D9EEAEC842A /* HCOrderingComparison.h in Headers */, + BB3339BE48FB5782E8F9A9F5 /* HCClassMatcher.h in Headers */, + BB33333C0F1EED12D79C5845 /* HCConformsToProtocol.h in Headers */, + BB333C8BC4684992F0A533CE /* HCHasDescription.h in Headers */, + BB333788D86B329768056FCB /* HCHasProperty.h in Headers */, + BB3335B708E6AB9321294A1C /* HCIsEqual.h in Headers */, + BB333ABCAF8D67405B9C023D /* HCIsInstanceOf.h in Headers */, + BB3334B153329F550E1EFEBD /* HCIsNil.h in Headers */, + BB333987DAE91CFE7D31B8B4 /* HCIsSame.h in Headers */, + BB333E8BB98FF2089077826D /* HCIsTypeOf.h in Headers */, + BB333842914F0FF2E8F70FAA /* HCIsEqualIgnoringCase.h in Headers */, + BB3330B6F8137019671EBC0C /* HCIsEqualCompressingWhiteSpace.h in Headers */, + BB333FEC8B49B249E92B3671 /* HCStringContains.h in Headers */, + BB3336E5FCF909801BB0D103 /* HCStringContainsInOrder.h in Headers */, + 0E5ECE6D1BC20C3300C15D2A /* HCIsCollectionContainingInRelativeOrder.h in Headers */, + BB3333FFD04D52C9DC48644E /* HCStringEndsWith.h in Headers */, + 74153B631A53606100FEF450 /* HCIsTrueFalse.h in Headers */, + BB333E7F416B46CE6777D695 /* HCStringStartsWith.h in Headers */, + BB333DD97E87592CF49128AD /* HCSubstringMatcher.h in Headers */, + BB3334E55A7868E28215B693 /* OCHamcrest.h in Headers */, + BB333EA439826DB6B82DDE54 /* HCGenericTestFailureReporter.h in Headers */, + BB33389388EB3A27D7037C56 /* HCSenTestFailureReporter.h in Headers */, + BB3339D5C07EC353A46EFEFE /* HCTestFailure.h in Headers */, + BB333F5B324A75A04610D267 /* HCTestFailureReporter.h in Headers */, + 746045671A29625E00196267 /* HCEvery.h in Headers */, + BB333F9085EBAAC25CDC41EA /* HCTestFailureReporterChain.h in Headers */, + BB3337B4866BAF4A120919CF /* HCXCTestFailureReporter.h in Headers */, + BB333EDC3E3320C65FB854B5 /* HCBoolReturnGetter.h in Headers */, + BB3337267CA4ACA9E3EB6896 /* HCCharReturnGetter.h in Headers */, + BB3337AB405029A86CDF3FAD /* HCDoubleReturnGetter.h in Headers */, + BB333ADE67DDD25C6E6EF058 /* HCFloatReturnGetter.h in Headers */, + BB33370D6F5B9B4978104E4B /* HCIntReturnGetter.h in Headers */, + BB333260BA38F45B4864A846 /* HCLongLongReturnGetter.h in Headers */, + BB3335C23B645B2E01C1DD95 /* HCLongReturnGetter.h in Headers */, + BB333A62F20EA2AE048FC857 /* HCObjectReturnGetter.h in Headers */, + BB333DE89A2F8557B268A355 /* HCReturnValueGetter.h in Headers */, + BB33371F6028F465B81A4E5F /* HCReturnTypeHandlerChain.h in Headers */, + BB3335CF33C2945D68DF426D /* HCShortReturnGetter.h in Headers */, + BB333EEEAF4355DA0CCB64D4 /* HCUnsignedCharReturnGetter.h in Headers */, + BB3336121208AAC5B1DDE8E9 /* HCUnsignedIntReturnGetter.h in Headers */, + BB3332440D6C92F1E9650F9A /* HCUnsignedLongLongReturnGetter.h in Headers */, + BB333FE3A9E3E2083DCDE4AF /* HCUnsignedLongReturnGetter.h in Headers */, + BB3339D6DAC64560919B4B71 /* HCUnsignedShortReturnGetter.h in Headers */, + BB333BCE07B55ED523B267F7 /* HCRequireNonNilObject.h in Headers */, + BB333A7CCE8A0DF95052F67C /* HCInvocationMatcher.h in Headers */, + BB333C793CAFFF7E4FB5BA2D /* HCCollect.h in Headers */, + BB333020F5ED4E3979BA1714 /* NSInvocation+OCHamcrest.h in Headers */, + BB3338B40D8EA4F74C514153 /* HCWrapInMatcher.h in Headers */, + 31DDC972FA9B967EC328D4F3 /* HCDiagnosingMatcher.h in Headers */, + 609E9502808F7E64A3060B23 /* HCArgumentCaptor.h in Headers */, + 609E921B14508E9DCA1FB303 /* HCRunloopRunner.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 24C5BFE61C1777D900C2BAFD /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 24C5C1391C177F1B00C2BAFD /* OCHamcrest.h in Headers */, + 24C5C13A1C177F1B00C2BAFD /* HCAssertThat.h in Headers */, + 24C5C13B1C177F1B00C2BAFD /* HCBaseDescription.h in Headers */, + 24C5C13C1C177F1B00C2BAFD /* HCBaseMatcher.h in Headers */, + 24C5C13D1C177F1B00C2BAFD /* HCDescription.h in Headers */, + 24C5C13E1C177F1B00C2BAFD /* HCDiagnosingMatcher.h in Headers */, + 24C5C13F1C177F1B00C2BAFD /* HCMatcher.h in Headers */, + 24C5C1401C177F1B00C2BAFD /* HCSelfDescribing.h in Headers */, + 24C5C1411C177F1B00C2BAFD /* HCStringDescription.h in Headers */, + 24C5C1421C177F1B00C2BAFD /* HCBoolReturnGetter.h in Headers */, + 24C5C1431C177F1B00C2BAFD /* HCCharReturnGetter.h in Headers */, + 24C5C1441C177F1B00C2BAFD /* HCDoubleReturnGetter.h in Headers */, + 24C5C1451C177F1B00C2BAFD /* HCFloatReturnGetter.h in Headers */, + 24C5C1461C177F1B00C2BAFD /* HCIntReturnGetter.h in Headers */, + 24C5C1471C177F1B00C2BAFD /* HCLongLongReturnGetter.h in Headers */, + 24C5C1481C177F1B00C2BAFD /* HCLongReturnGetter.h in Headers */, + 24C5C1491C177F1B00C2BAFD /* HCObjectReturnGetter.h in Headers */, + 24C5C14A1C177F1B00C2BAFD /* HCReturnValueGetter.h in Headers */, + 24C5C14B1C177F1B00C2BAFD /* HCReturnTypeHandlerChain.h in Headers */, + 24C5C14C1C177F1B00C2BAFD /* HCShortReturnGetter.h in Headers */, + 24C5C14D1C177F1B00C2BAFD /* HCUnsignedCharReturnGetter.h in Headers */, + 24C5C14E1C177F1B00C2BAFD /* HCUnsignedIntReturnGetter.h in Headers */, + 24C5C14F1C177F1B00C2BAFD /* HCUnsignedLongLongReturnGetter.h in Headers */, + 24C5C1501C177F1B00C2BAFD /* HCUnsignedLongReturnGetter.h in Headers */, + 24C5C1511C177F1B00C2BAFD /* HCUnsignedShortReturnGetter.h in Headers */, + 24C5C1521C177F1B00C2BAFD /* HCGenericTestFailureReporter.h in Headers */, + 24C5C1531C177F1B00C2BAFD /* HCSenTestFailureReporter.h in Headers */, + 24C5C1541C177F1B00C2BAFD /* HCTestFailure.h in Headers */, + 24C5C1551C177F1C00C2BAFD /* HCTestFailureReporter.h in Headers */, + 24C5C1561C177F1C00C2BAFD /* HCTestFailureReporterChain.h in Headers */, + 24C5C1571C177F1C00C2BAFD /* HCXCTestFailureReporter.h in Headers */, + 24C5C1581C177F1C00C2BAFD /* HCCollect.h in Headers */, + 24C5C1591C177F1C00C2BAFD /* HCInvocationMatcher.h in Headers */, + 24C5C15A1C177F1C00C2BAFD /* HCRequireNonNilObject.h in Headers */, + 24C5C15B1C177F1C00C2BAFD /* HCWrapInMatcher.h in Headers */, + 24C5C15C1C177F1C00C2BAFD /* NSInvocation+OCHamcrest.h in Headers */, + 24C5C15D1C177F1C00C2BAFD /* HCEvery.h in Headers */, + 24C5C15E1C177F1C00C2BAFD /* HCHasCount.h in Headers */, + 24C5C15F1C177F1C00C2BAFD /* HCIsCollectionContaining.h in Headers */, + 24C5C1601C177F1C00C2BAFD /* HCIsCollectionContainingInAnyOrder.h in Headers */, + 24C5C1611C177F1C00C2BAFD /* HCIsCollectionContainingInOrder.h in Headers */, + 24C5C1621C177F1C00C2BAFD /* HCIsCollectionContainingInRelativeOrder.h in Headers */, + 24C5C1631C177F1C00C2BAFD /* HCIsCollectionOnlyContaining.h in Headers */, + 24C5C1641C177F1C00C2BAFD /* HCIsDictionaryContaining.h in Headers */, + 24C5C1651C177F1C00C2BAFD /* HCIsDictionaryContainingEntries.h in Headers */, + 24C5C1661C177F1D00C2BAFD /* HCIsDictionaryContainingKey.h in Headers */, + 24C5C1671C177F1D00C2BAFD /* HCIsDictionaryContainingValue.h in Headers */, + 24C5C1681C177F1D00C2BAFD /* HCIsEmptyCollection.h in Headers */, + 24C5C1691C177F1D00C2BAFD /* HCIsIn.h in Headers */, + 24C5C16A1C177F1D00C2BAFD /* HCDescribedAs.h in Headers */, + 24C5C16B1C177F1D00C2BAFD /* HCIs.h in Headers */, + 24C5C16C1C177F1D00C2BAFD /* HCAllOf.h in Headers */, + 24C5C16D1C177F1D00C2BAFD /* HCAnyOf.h in Headers */, + 24C5C16E1C177F1D00C2BAFD /* HCIsAnything.h in Headers */, + 24C5C16F1C177F1D00C2BAFD /* HCIsNot.h in Headers */, + 24C5C1701C177F1D00C2BAFD /* HCIsCloseTo.h in Headers */, + 24C5C1711C177F1E00C2BAFD /* HCIsEqualToNumber.h in Headers */, + 24C5C1721C177F1E00C2BAFD /* HCIsTrueFalse.h in Headers */, + 24C5C1731C177F1E00C2BAFD /* HCNumberAssert.h in Headers */, + 24C5C1741C177F1E00C2BAFD /* HCOrderingComparison.h in Headers */, + 24C5C1751C177F1E00C2BAFD /* HCArgumentCaptor.h in Headers */, + 24C5C1761C177F1E00C2BAFD /* HCClassMatcher.h in Headers */, + 24C5C1771C177F1E00C2BAFD /* HCConformsToProtocol.h in Headers */, + 24C5C1781C177F1E00C2BAFD /* HCHasDescription.h in Headers */, + 24C5C1791C177F1E00C2BAFD /* HCHasProperty.h in Headers */, + 24C5C17A1C177F1F00C2BAFD /* HCIsEqual.h in Headers */, + 24C5C17B1C177F1F00C2BAFD /* HCIsInstanceOf.h in Headers */, + 24C5C17C1C177F1F00C2BAFD /* HCIsNil.h in Headers */, + 24C5C17D1C177F1F00C2BAFD /* HCIsSame.h in Headers */, + 24C5C17E1C177F1F00C2BAFD /* HCIsTypeOf.h in Headers */, + 24C5C17F1C177F1F00C2BAFD /* HCThrowsException.h in Headers */, + 24C5C1801C177F1F00C2BAFD /* HCIsEqualIgnoringCase.h in Headers */, + 24C5C1811C177F1F00C2BAFD /* HCIsEqualCompressingWhiteSpace.h in Headers */, + 24C5C1821C177F2000C2BAFD /* HCStringContains.h in Headers */, + 24C5C1831C177F2000C2BAFD /* HCStringContainsInOrder.h in Headers */, + 24C5C1841C177F2000C2BAFD /* HCStringEndsWith.h in Headers */, + 24C5C1851C177F2000C2BAFD /* HCStringStartsWith.h in Headers */, + 24C5C1861C177F2000C2BAFD /* HCSubstringMatcher.h in Headers */, + 609E9214DB7BBD12E81BC5F0 /* HCRunloopRunner.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 24C5BFF31C17781400C2BAFD /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 24C5C1871C177F5800C2BAFD /* OCHamcrest.h in Headers */, + 24C5C1881C177F5800C2BAFD /* HCAssertThat.h in Headers */, + 24C5C1891C177F5800C2BAFD /* HCBaseDescription.h in Headers */, + 24C5C18A1C177F5800C2BAFD /* HCBaseMatcher.h in Headers */, + 24C5C18B1C177F5800C2BAFD /* HCDescription.h in Headers */, + 24C5C18C1C177F5800C2BAFD /* HCDiagnosingMatcher.h in Headers */, + 24C5C18D1C177F5800C2BAFD /* HCMatcher.h in Headers */, + 24C5C18E1C177F5800C2BAFD /* HCSelfDescribing.h in Headers */, + 24C5C18F1C177F5800C2BAFD /* HCStringDescription.h in Headers */, + 24C5C1901C177F5800C2BAFD /* HCBoolReturnGetter.h in Headers */, + 24C5C1911C177F5800C2BAFD /* HCCharReturnGetter.h in Headers */, + 24C5C1921C177F5800C2BAFD /* HCDoubleReturnGetter.h in Headers */, + 24C5C1931C177F5800C2BAFD /* HCFloatReturnGetter.h in Headers */, + 24C5C1941C177F5800C2BAFD /* HCIntReturnGetter.h in Headers */, + 24C5C1951C177F5800C2BAFD /* HCLongLongReturnGetter.h in Headers */, + 24C5C1961C177F5800C2BAFD /* HCLongReturnGetter.h in Headers */, + 24C5C1971C177F5800C2BAFD /* HCObjectReturnGetter.h in Headers */, + 24C5C1981C177F5800C2BAFD /* HCReturnValueGetter.h in Headers */, + 24C5C1991C177F5800C2BAFD /* HCReturnTypeHandlerChain.h in Headers */, + 24C5C19A1C177F5800C2BAFD /* HCShortReturnGetter.h in Headers */, + 24C5C19B1C177F5800C2BAFD /* HCUnsignedCharReturnGetter.h in Headers */, + 24C5C19C1C177F5800C2BAFD /* HCUnsignedIntReturnGetter.h in Headers */, + 24C5C19D1C177F5800C2BAFD /* HCUnsignedLongLongReturnGetter.h in Headers */, + 24C5C19E1C177F5800C2BAFD /* HCUnsignedLongReturnGetter.h in Headers */, + 24C5C19F1C177F5800C2BAFD /* HCUnsignedShortReturnGetter.h in Headers */, + 24C5C1A01C177F5800C2BAFD /* HCGenericTestFailureReporter.h in Headers */, + 24C5C1A11C177F5800C2BAFD /* HCSenTestFailureReporter.h in Headers */, + 24C5C1A21C177F5800C2BAFD /* HCTestFailure.h in Headers */, + 24C5C1A31C177F5800C2BAFD /* HCTestFailureReporter.h in Headers */, + 24C5C1A41C177F5800C2BAFD /* HCTestFailureReporterChain.h in Headers */, + 24C5C1A51C177F5800C2BAFD /* HCXCTestFailureReporter.h in Headers */, + 24C5C1A61C177F5800C2BAFD /* HCCollect.h in Headers */, + 24C5C1A71C177F5800C2BAFD /* HCInvocationMatcher.h in Headers */, + 24C5C1A81C177F5900C2BAFD /* HCRequireNonNilObject.h in Headers */, + 24C5C1A91C177F5900C2BAFD /* HCWrapInMatcher.h in Headers */, + 24C5C1AA1C177F5900C2BAFD /* NSInvocation+OCHamcrest.h in Headers */, + 24C5C1AB1C177F5900C2BAFD /* HCEvery.h in Headers */, + 24C5C1AC1C177F5900C2BAFD /* HCHasCount.h in Headers */, + 24C5C1AD1C177F5900C2BAFD /* HCIsCollectionContaining.h in Headers */, + 24C5C1AE1C177F5900C2BAFD /* HCIsCollectionContainingInAnyOrder.h in Headers */, + 24C5C1AF1C177F5900C2BAFD /* HCIsCollectionContainingInOrder.h in Headers */, + 24C5C1B01C177F5900C2BAFD /* HCIsCollectionContainingInRelativeOrder.h in Headers */, + 24C5C1B11C177F5900C2BAFD /* HCIsCollectionOnlyContaining.h in Headers */, + 24C5C1B21C177F5900C2BAFD /* HCIsDictionaryContaining.h in Headers */, + 24C5C1B31C177F5900C2BAFD /* HCIsDictionaryContainingEntries.h in Headers */, + 24C5C1B41C177F5900C2BAFD /* HCIsDictionaryContainingKey.h in Headers */, + 24C5C1B51C177F5900C2BAFD /* HCIsDictionaryContainingValue.h in Headers */, + 24C5C1B61C177F5A00C2BAFD /* HCIsEmptyCollection.h in Headers */, + 24C5C1B71C177F5A00C2BAFD /* HCIsIn.h in Headers */, + 24C5C1B81C177F5A00C2BAFD /* HCDescribedAs.h in Headers */, + 24C5C1B91C177F5A00C2BAFD /* HCIs.h in Headers */, + 24C5C1BA1C177F5A00C2BAFD /* HCAllOf.h in Headers */, + 24C5C1BB1C177F5A00C2BAFD /* HCAnyOf.h in Headers */, + 24C5C1BC1C177F5A00C2BAFD /* HCIsAnything.h in Headers */, + 24C5C1BD1C177F5A00C2BAFD /* HCIsNot.h in Headers */, + 24C5C1BE1C177F5A00C2BAFD /* HCIsCloseTo.h in Headers */, + 24C5C1BF1C177F5A00C2BAFD /* HCIsEqualToNumber.h in Headers */, + 24C5C1C01C177F5A00C2BAFD /* HCIsTrueFalse.h in Headers */, + 24C5C1C11C177F5A00C2BAFD /* HCNumberAssert.h in Headers */, + 24C5C1C21C177F5B00C2BAFD /* HCOrderingComparison.h in Headers */, + 24C5C1C31C177F5B00C2BAFD /* HCArgumentCaptor.h in Headers */, + 24C5C1C41C177F5B00C2BAFD /* HCClassMatcher.h in Headers */, + 24C5C1C51C177F5B00C2BAFD /* HCConformsToProtocol.h in Headers */, + 24C5C1C61C177F5B00C2BAFD /* HCHasDescription.h in Headers */, + 24C5C1C71C177F5B00C2BAFD /* HCHasProperty.h in Headers */, + 24C5C1C81C177F5B00C2BAFD /* HCIsEqual.h in Headers */, + 24C5C1C91C177F5B00C2BAFD /* HCIsInstanceOf.h in Headers */, + 24C5C1CA1C177F5B00C2BAFD /* HCIsNil.h in Headers */, + 24C5C1CB1C177F5C00C2BAFD /* HCIsSame.h in Headers */, + 24C5C1CC1C177F5C00C2BAFD /* HCIsTypeOf.h in Headers */, + 24C5C1CD1C177F5C00C2BAFD /* HCThrowsException.h in Headers */, + 24C5C1CE1C177F5C00C2BAFD /* HCIsEqualIgnoringCase.h in Headers */, + 24C5C1CF1C177F5C00C2BAFD /* HCIsEqualCompressingWhiteSpace.h in Headers */, + 24C5C1D01C177F5C00C2BAFD /* HCStringContains.h in Headers */, + 24C5C1D11C177F5C00C2BAFD /* HCStringContainsInOrder.h in Headers */, + 24C5C1D21C177F5D00C2BAFD /* HCStringEndsWith.h in Headers */, + 24C5C1D31C177F5D00C2BAFD /* HCStringStartsWith.h in Headers */, + 24C5C1D41C177F5D00C2BAFD /* HCSubstringMatcher.h in Headers */, + 609E9BACF54CA8CF5C8F4825 /* HCRunloopRunner.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 24C5C0001C17782500C2BAFD /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 24C5C1D51C177F7200C2BAFD /* OCHamcrest.h in Headers */, + 24C5C1D61C177F7200C2BAFD /* HCAssertThat.h in Headers */, + 24C5C1D71C177F7200C2BAFD /* HCBaseDescription.h in Headers */, + 24C5C1D81C177F7200C2BAFD /* HCBaseMatcher.h in Headers */, + 24C5C1D91C177F7200C2BAFD /* HCDescription.h in Headers */, + 24C5C1DA1C177F7200C2BAFD /* HCDiagnosingMatcher.h in Headers */, + 24C5C1DB1C177F7200C2BAFD /* HCMatcher.h in Headers */, + 24C5C1DC1C177F7200C2BAFD /* HCSelfDescribing.h in Headers */, + 24C5C1DD1C177F7200C2BAFD /* HCStringDescription.h in Headers */, + 24C5C1DE1C177F7200C2BAFD /* HCBoolReturnGetter.h in Headers */, + 24C5C1DF1C177F7200C2BAFD /* HCCharReturnGetter.h in Headers */, + 24C5C1E01C177F7200C2BAFD /* HCDoubleReturnGetter.h in Headers */, + 24C5C1E11C177F7200C2BAFD /* HCFloatReturnGetter.h in Headers */, + 24C5C1E21C177F7200C2BAFD /* HCIntReturnGetter.h in Headers */, + 24C5C1E31C177F7200C2BAFD /* HCLongLongReturnGetter.h in Headers */, + 24C5C1E41C177F7200C2BAFD /* HCLongReturnGetter.h in Headers */, + 24C5C1E51C177F7200C2BAFD /* HCObjectReturnGetter.h in Headers */, + 24C5C1E61C177F7200C2BAFD /* HCReturnValueGetter.h in Headers */, + 24C5C1E71C177F7200C2BAFD /* HCReturnTypeHandlerChain.h in Headers */, + 24C5C1E81C177F7200C2BAFD /* HCShortReturnGetter.h in Headers */, + 24C5C1E91C177F7200C2BAFD /* HCUnsignedCharReturnGetter.h in Headers */, + 24C5C1EA1C177F7200C2BAFD /* HCUnsignedIntReturnGetter.h in Headers */, + 24C5C1EB1C177F7200C2BAFD /* HCUnsignedLongLongReturnGetter.h in Headers */, + 24C5C1EC1C177F7200C2BAFD /* HCUnsignedLongReturnGetter.h in Headers */, + 24C5C1ED1C177F7200C2BAFD /* HCUnsignedShortReturnGetter.h in Headers */, + 24C5C1EE1C177F7200C2BAFD /* HCGenericTestFailureReporter.h in Headers */, + 24C5C1EF1C177F7200C2BAFD /* HCSenTestFailureReporter.h in Headers */, + 24C5C1F01C177F7200C2BAFD /* HCTestFailure.h in Headers */, + 24C5C1F11C177F7200C2BAFD /* HCTestFailureReporter.h in Headers */, + 24C5C1F21C177F7200C2BAFD /* HCTestFailureReporterChain.h in Headers */, + 24C5C1F31C177F7300C2BAFD /* HCXCTestFailureReporter.h in Headers */, + 24C5C1F41C177F7300C2BAFD /* HCCollect.h in Headers */, + 24C5C1F51C177F7300C2BAFD /* HCInvocationMatcher.h in Headers */, + 24C5C1F61C177F7300C2BAFD /* HCRequireNonNilObject.h in Headers */, + 24C5C1F71C177F7300C2BAFD /* HCWrapInMatcher.h in Headers */, + 24C5C1F81C177F7300C2BAFD /* NSInvocation+OCHamcrest.h in Headers */, + 24C5C1F91C177F7300C2BAFD /* HCEvery.h in Headers */, + 24C5C1FA1C177F7300C2BAFD /* HCHasCount.h in Headers */, + 24C5C1FB1C177F7300C2BAFD /* HCIsCollectionContaining.h in Headers */, + 24C5C1FC1C177F7300C2BAFD /* HCIsCollectionContainingInAnyOrder.h in Headers */, + 24C5C1FD1C177F7300C2BAFD /* HCIsCollectionContainingInOrder.h in Headers */, + 24C5C1FE1C177F7300C2BAFD /* HCIsCollectionContainingInRelativeOrder.h in Headers */, + 24C5C1FF1C177F7300C2BAFD /* HCIsCollectionOnlyContaining.h in Headers */, + 24C5C2001C177F7300C2BAFD /* HCIsDictionaryContaining.h in Headers */, + 24C5C2011C177F7300C2BAFD /* HCIsDictionaryContainingEntries.h in Headers */, + 24C5C2021C177F7300C2BAFD /* HCIsDictionaryContainingKey.h in Headers */, + 24C5C2031C177F7400C2BAFD /* HCIsDictionaryContainingValue.h in Headers */, + 24C5C2041C177F7400C2BAFD /* HCIsEmptyCollection.h in Headers */, + 24C5C2051C177F7400C2BAFD /* HCIsIn.h in Headers */, + 24C5C2061C177F7400C2BAFD /* HCDescribedAs.h in Headers */, + 24C5C2071C177F7400C2BAFD /* HCIs.h in Headers */, + 24C5C2081C177F7400C2BAFD /* HCAllOf.h in Headers */, + 24C5C2091C177F7400C2BAFD /* HCAnyOf.h in Headers */, + 24C5C20A1C177F7400C2BAFD /* HCIsAnything.h in Headers */, + 24C5C20B1C177F7400C2BAFD /* HCIsNot.h in Headers */, + 24C5C20C1C177F7400C2BAFD /* HCIsCloseTo.h in Headers */, + 24C5C20D1C177F7400C2BAFD /* HCIsEqualToNumber.h in Headers */, + 24C5C20E1C177F7400C2BAFD /* HCIsTrueFalse.h in Headers */, + 24C5C20F1C177F7500C2BAFD /* HCNumberAssert.h in Headers */, + 24C5C2101C177F7500C2BAFD /* HCOrderingComparison.h in Headers */, + 24C5C2111C177F7500C2BAFD /* HCArgumentCaptor.h in Headers */, + 24C5C2121C177F7500C2BAFD /* HCClassMatcher.h in Headers */, + 24C5C2131C177F7500C2BAFD /* HCConformsToProtocol.h in Headers */, + 24C5C2141C177F7500C2BAFD /* HCHasDescription.h in Headers */, + 24C5C2151C177F7500C2BAFD /* HCHasProperty.h in Headers */, + 24C5C2161C177F7500C2BAFD /* HCIsEqual.h in Headers */, + 24C5C2171C177F7500C2BAFD /* HCIsInstanceOf.h in Headers */, + 24C5C2181C177F7600C2BAFD /* HCIsNil.h in Headers */, + 24C5C2191C177F7600C2BAFD /* HCIsSame.h in Headers */, + 24C5C21A1C177F7600C2BAFD /* HCIsTypeOf.h in Headers */, + 24C5C21B1C177F7600C2BAFD /* HCThrowsException.h in Headers */, + 24C5C21C1C177F7600C2BAFD /* HCIsEqualIgnoringCase.h in Headers */, + 24C5C21D1C177F7600C2BAFD /* HCIsEqualCompressingWhiteSpace.h in Headers */, + 24C5C21E1C177F7600C2BAFD /* HCStringContains.h in Headers */, + 24C5C21F1C177F7600C2BAFD /* HCStringContainsInOrder.h in Headers */, + 24C5C2201C177F7700C2BAFD /* HCStringEndsWith.h in Headers */, + 24C5C2211C177F7700C2BAFD /* HCStringStartsWith.h in Headers */, + 24C5C2221C177F7700C2BAFD /* HCSubstringMatcher.h in Headers */, + 609E941C05213BA7214F6C6B /* HCRunloopRunner.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 081BEE611345979F003F846A /* libochamcrest */ = { + isa = PBXNativeTarget; + buildConfigurationList = 081BEE7E1345979F003F846A /* Build configuration list for PBXNativeTarget "libochamcrest" */; + buildPhases = ( + 081BEE5E1345979F003F846A /* Sources */, + 081BEE5F1345979F003F846A /* Frameworks */, + 081BEE601345979F003F846A /* Headers */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = libochamcrest; + productName = libochamcrest; + productReference = 081BEE621345979F003F846A /* libochamcrest.a */; + productType = "com.apple.product-type.library.static"; + }; + 081BEE6B1345979F003F846A /* libochamcrestTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 081BEE811345979F003F846A /* Build configuration list for PBXNativeTarget "libochamcrestTests" */; + buildPhases = ( + 081BEE671345979F003F846A /* Sources */, + 081BEE681345979F003F846A /* Frameworks */, + 081BEE691345979F003F846A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 081BEE711345979F003F846A /* PBXTargetDependency */, + ); + name = libochamcrestTests; + productName = libochamcrestTests; + productReference = 081BEE6C1345979F003F846A /* libochamcrestTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 087601E113440806001B439B /* OCHamcrest */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0876020913440807001B439B /* Build configuration list for PBXNativeTarget "OCHamcrest" */; + buildPhases = ( + 087601DD13440806001B439B /* Sources */, + 087601DE13440806001B439B /* Frameworks */, + 087601DF13440806001B439B /* Headers */, + 087601E013440806001B439B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = OCHamcrest; + productName = OCHamcrest; + productReference = 087601E213440806001B439B /* OCHamcrest.framework */; + productType = "com.apple.product-type.framework"; + }; + 087601F613440807001B439B /* OCHamcrestTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0876020C13440807001B439B /* Build configuration list for PBXNativeTarget "OCHamcrestTests" */; + buildPhases = ( + 087601F213440807001B439B /* Sources */, + 087601F313440807001B439B /* Frameworks */, + 087601F413440807001B439B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 087601FA13440807001B439B /* PBXTargetDependency */, + ); + name = OCHamcrestTests; + productName = OCHamcrestTests; + productReference = 087601F713440807001B439B /* OCHamcrestTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 24C5BFE81C1777D900C2BAFD /* OCHamcrest-iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 24C5BFF01C1777D900C2BAFD /* Build configuration list for PBXNativeTarget "OCHamcrest-iOS" */; + buildPhases = ( + 24C5BFE41C1777D900C2BAFD /* Sources */, + 24C5BFE51C1777D900C2BAFD /* Frameworks */, + 24C5BFE61C1777D900C2BAFD /* Headers */, + 24C5BFE71C1777D900C2BAFD /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "OCHamcrest-iOS"; + productName = "OCHamcrest-iOS"; + productReference = 24C5BFE91C1777D900C2BAFD /* OCHamcrest.framework */; + productType = "com.apple.product-type.framework"; + }; + 24C5BFF51C17781400C2BAFD /* OCHamcrest-tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 24C5BFFB1C17781400C2BAFD /* Build configuration list for PBXNativeTarget "OCHamcrest-tvOS" */; + buildPhases = ( + 24C5BFF11C17781400C2BAFD /* Sources */, + 24C5BFF21C17781400C2BAFD /* Frameworks */, + 24C5BFF31C17781400C2BAFD /* Headers */, + 24C5BFF41C17781400C2BAFD /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "OCHamcrest-tvOS"; + productName = "OCHamcrest-tvOS"; + productReference = 24C5BFF61C17781400C2BAFD /* OCHamcrest.framework */; + productType = "com.apple.product-type.framework"; + }; + 24C5C0021C17782500C2BAFD /* OCHamcrest-watchOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 24C5C0081C17782500C2BAFD /* Build configuration list for PBXNativeTarget "OCHamcrest-watchOS" */; + buildPhases = ( + 24C5BFFE1C17782500C2BAFD /* Sources */, + 24C5BFFF1C17782500C2BAFD /* Frameworks */, + 24C5C0001C17782500C2BAFD /* Headers */, + 24C5C0011C17782500C2BAFD /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "OCHamcrest-watchOS"; + productName = "OCHamcrest-watchOS"; + productReference = 24C5C0031C17782500C2BAFD /* OCHamcrest.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 087601D813440806001B439B /* Project object */ = { + isa = PBXProject; + attributes = { + LastTestingUpgradeCheck = 0640; + LastUpgradeCheck = 0810; + ORGANIZATIONNAME = hamcrest.org; + TargetAttributes = { + 24C5BFE81C1777D900C2BAFD = { + CreatedOnToolsVersion = 7.1.1; + }; + 24C5BFF51C17781400C2BAFD = { + CreatedOnToolsVersion = 7.1.1; + }; + 24C5C0021C17782500C2BAFD = { + CreatedOnToolsVersion = 7.1.1; + }; + }; + }; + buildConfigurationList = 087601DB13440806001B439B /* Build configuration list for PBXProject "OCHamcrest" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 087601D613440806001B439B; + productRefGroup = 087601E313440806001B439B /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 087601E113440806001B439B /* OCHamcrest */, + 087601F613440807001B439B /* OCHamcrestTests */, + 081BEE611345979F003F846A /* libochamcrest */, + 081BEE6B1345979F003F846A /* libochamcrestTests */, + 24C5BFE81C1777D900C2BAFD /* OCHamcrest-iOS */, + 24C5BFF51C17781400C2BAFD /* OCHamcrest-tvOS */, + 24C5C0021C17782500C2BAFD /* OCHamcrest-watchOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 081BEE691345979F003F846A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 087601E013440806001B439B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 087601F413440807001B439B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 24C5BFE71C1777D900C2BAFD /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 24C5BFF41C17781400C2BAFD /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 24C5C0011C17782500C2BAFD /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 081BEE5E1345979F003F846A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BB333A663782157FB25A5E45 /* HCAssertThat.m in Sources */, + BB333A358C42850FD4C55B3D /* HCBaseDescription.m in Sources */, + BB3332839F5E2F874F6BAE41 /* HCBaseMatcher.m in Sources */, + BB33392916E29A808728FF91 /* HCStringDescription.m in Sources */, + BB333BBD9153A5B7C1DD0F86 /* HCHasCount.m in Sources */, + BB333DB953FACB91AFC1A5C2 /* HCIsCollectionContaining.m in Sources */, + BB3333CF7706F55C9F5C7C83 /* HCIsCollectionContainingInAnyOrder.m in Sources */, + BB3338D75677FE93976B1D6E /* HCIsCollectionContainingInOrder.m in Sources */, + BB333D3938678ED9EF3E7E8D /* HCIsCollectionOnlyContaining.m in Sources */, + BB33339ACB5D4E6DE2165E78 /* HCIsDictionaryContaining.m in Sources */, + BB3339F4D9293AA86914FB66 /* HCIsDictionaryContainingEntries.m in Sources */, + BB333D6DC563CDDEDF3B6DC9 /* HCIsDictionaryContainingKey.m in Sources */, + BB3339A5133E437AFDDE7B15 /* HCIsDictionaryContainingValue.m in Sources */, + BB3332098FC93350263E39FF /* HCIsEmptyCollection.m in Sources */, + BB333FE2E6441F4ADFE460AC /* HCIsIn.m in Sources */, + BB333DEC51AFB20AADBB68D6 /* HCDescribedAs.m in Sources */, + BB3336F96B099B5EFE2E670E /* HCIs.m in Sources */, + BB333375251766FC537D86BC /* HCAllOf.m in Sources */, + BB33343CA2ADA0E7E367E8F9 /* HCAnyOf.m in Sources */, + BB33338BB29B93F85C891624 /* HCIsAnything.m in Sources */, + 74153B661A53606100FEF450 /* HCIsTrueFalse.m in Sources */, + BB333C35826116418990B390 /* HCIsNot.m in Sources */, + BB333597D5BACF80DB73F49C /* HCIsCloseTo.m in Sources */, + BB333C48F0B219D8132548B4 /* HCIsEqualToNumber.m in Sources */, + BB3330C8507C35031D255243 /* HCNumberAssert.m in Sources */, + BB3331D1B77DB4D834A7FDF5 /* HCOrderingComparison.m in Sources */, + BB33327669EF4A7B5889DBAD /* HCClassMatcher.m in Sources */, + BB333FFD6EAB7828C98E11EE /* HCConformsToProtocol.m in Sources */, + BB333D632775553288FEA2FE /* HCHasDescription.m in Sources */, + BB33379125D73946A16F7B0D /* HCHasProperty.m in Sources */, + BB333EB3405CFA5A54AB30CE /* HCIsEqual.m in Sources */, + 7460456A1A29625E00196267 /* HCEvery.m in Sources */, + BB33323968557EF5235B3AFE /* HCIsInstanceOf.m in Sources */, + BB33301A68F74D210C803FEA /* HCIsNil.m in Sources */, + 74FDDD181A3FF39900177999 /* HCThrowsException.m in Sources */, + BB3334612BBBB6871530AB79 /* HCIsSame.m in Sources */, + BB3335190594259F5D74CAE1 /* HCIsTypeOf.m in Sources */, + BB333210C6DF7BB6238B7747 /* HCIsEqualIgnoringCase.m in Sources */, + BB333F22130C3E2815EB3CE4 /* HCIsEqualCompressingWhiteSpace.m in Sources */, + BB3337947E2E43605545E743 /* HCStringContains.m in Sources */, + BB333F7FB7BA3AF26A162752 /* HCStringContainsInOrder.m in Sources */, + BB3336AD40784CC4673DB87F /* HCStringEndsWith.m in Sources */, + BB333A7CCC7A2BD01898722C /* HCStringStartsWith.m in Sources */, + BB333D2C925DC2010C1B25FA /* HCSubstringMatcher.m in Sources */, + BB333C372274619E93D2D9E8 /* HCGenericTestFailureReporter.m in Sources */, + BB3334B9AE78134252D2CBC7 /* HCSenTestFailureReporter.m in Sources */, + BB33369C21D2B44C72195326 /* HCTestFailure.m in Sources */, + BB33302772CACC61A1B4DB74 /* HCTestFailureReporter.m in Sources */, + BB3333346B22FC64994ED11E /* HCTestFailureReporterChain.m in Sources */, + BB3337E931781280D48FB372 /* HCXCTestFailureReporter.m in Sources */, + BB3331A77D7C99A487A89434 /* HCBoolReturnGetter.m in Sources */, + BB333EF9E0C14539AE686528 /* HCCharReturnGetter.m in Sources */, + BB333F8CC565D8068C47B3FE /* HCDoubleReturnGetter.m in Sources */, + BB333328EA1EFCED9C811321 /* HCFloatReturnGetter.m in Sources */, + BB33321509F1659FB29AD59E /* HCIntReturnGetter.m in Sources */, + BB333ACAD45079E5160B4B2D /* HCLongLongReturnGetter.m in Sources */, + BB3339D8979DFFEBB1AEB26D /* HCLongReturnGetter.m in Sources */, + 0E498AA61BC4D45200BA28F0 /* HCIsCollectionContainingInRelativeOrder.m in Sources */, + BB3337895E6D085546B857EC /* HCObjectReturnGetter.m in Sources */, + BB333EA74F069D831202108E /* HCReturnValueGetter.m in Sources */, + BB3330B79DF7A5A15B7182E8 /* HCReturnTypeHandlerChain.m in Sources */, + BB3339D3E0792F98EE3F7D07 /* HCShortReturnGetter.m in Sources */, + BB3336E5733952CCFE8A6BBC /* HCUnsignedCharReturnGetter.m in Sources */, + BB333DFB6E6D7C86888EC7AA /* HCUnsignedIntReturnGetter.m in Sources */, + BB333B4AB9E7656A98D0D172 /* HCUnsignedLongLongReturnGetter.m in Sources */, + BB333E360A90374209909E7E /* HCUnsignedLongReturnGetter.m in Sources */, + BB3334CE1D4AE1517BD55CC5 /* HCUnsignedShortReturnGetter.m in Sources */, + BB333EECA8B5D2C68CC8EF72 /* NSInvocation+OCHamcrest.m in Sources */, + BB33313D3DCEF8B438964913 /* HCRequireNonNilObject.m in Sources */, + BB333D2770D5C4DD8E4D7633 /* HCWrapInMatcher.m in Sources */, + BB3338485B4DA85A777544B3 /* HCCollect.m in Sources */, + BB33387D94ADA6748B02A289 /* HCInvocationMatcher.m in Sources */, + 31DDCEC51963ADABADBC576B /* HCDiagnosingMatcher.m in Sources */, + 609E9B9227BDE67289F74F23 /* HCArgumentCaptor.m in Sources */, + 609E9F1238326DBB8D61D4AE /* HCRunloopRunner.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 081BEE671345979F003F846A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0859831013459CA400BE7892 /* FakeWithCount.m in Sources */, + 0859831113459CA400BE7892 /* FakeWithoutCount.m in Sources */, + 0859831213459CA400BE7892 /* HCHasCountTests.m in Sources */, + 0859831313459CA400BE7892 /* HCHasDescriptionTests.m in Sources */, + 0859831613459CA400BE7892 /* HCIsCloseToTests.m in Sources */, + 0859831713459CA400BE7892 /* HCIsCollectionContainingInAnyOrderTests.m in Sources */, + 0859831813459CA400BE7892 /* HCIsCollectionContainingInOrderTests.m in Sources */, + 0859831913459CA400BE7892 /* HCIsCollectionContainingTests.m in Sources */, + 0859831A13459CA400BE7892 /* HCIsCollectionOnlyContainingTests.m in Sources */, + 0859831B13459CA400BE7892 /* HCIsDictionaryContainingEntriesTests.m in Sources */, + 0859831C13459CA400BE7892 /* HCIsDictionaryContainingKeyTests.m in Sources */, + 0859831D13459CA400BE7892 /* HCIsDictionaryContainingTests.m in Sources */, + 0859831E13459CA400BE7892 /* HCIsDictionaryContainingValueTests.m in Sources */, + 0859831F13459CA400BE7892 /* HCIsEmptyCollectionTests.m in Sources */, + 0859832013459CA400BE7892 /* HCIsEqualIgnoringCaseTests.m in Sources */, + 0859832113459CA400BE7892 /* HCIsEqualCompressingWhiteSpaceTests.m in Sources */, + 0859832313459CA400BE7892 /* HCIsEqualToNumberTests.m in Sources */, + 0859832513459CA400BE7892 /* HCIsInTests.m in Sources */, + 0859832B13459CA400BE7892 /* HCNumberAssertTests.m in Sources */, + 7460456D1A2964AF00196267 /* HCEveryTests.m in Sources */, + 0859832C13459CA400BE7892 /* HCOrderingComparisonTests.m in Sources */, + 0859832D13459CA400BE7892 /* HCStringContainsTests.m in Sources */, + 0859832F13459CA400BE7892 /* HCStringEndsWithTests.m in Sources */, + 0859833013459CA400BE7892 /* HCStringStartsWithTests.m in Sources */, + 088FB0AC136A6DEA00C191E1 /* HCStringContainsInOrderTests.m in Sources */, + 74153B601A535FEA00FEF450 /* HCIsTrueFalseTests.m in Sources */, + 74D7A7B61A2B902F00CD6CC0 /* DiagnosingMatcherTest.m in Sources */, + 0846B6E113EE5F9D00EA68E8 /* HCDescribedAsTests.m in Sources */, + 0846B6E313EE5F9D00EA68E8 /* HCIsTests.m in Sources */, + 482FE2D41E6DDC2B00AC0009 /* HCWrapInMatcherTests.m in Sources */, + 0846B6E513EE5F9D00EA68E8 /* NeverMatch.m in Sources */, + 0846B6EC13EE5FB400EA68E8 /* HCAllOfTests.m in Sources */, + 0846B6EE13EE5FB400EA68E8 /* HCAnyOfTests.m in Sources */, + 745892101A2AAD2C00BAAC76 /* AssertWithTimeoutTests.m in Sources */, + 0846B6F013EE5FB400EA68E8 /* HCIsAnythingTests.m in Sources */, + 747776C81A3F5060000A6E1D /* HCThrowsExceptionTests.m in Sources */, + 0846B6F213EE5FB400EA68E8 /* HCIsNotTests.m in Sources */, + 0846B6F813EE5FD200EA68E8 /* HCIsEqualTests.m in Sources */, + 0846B6FA13EE5FD200EA68E8 /* HCIsInstanceOfTests.m in Sources */, + 0846B6FC13EE5FD200EA68E8 /* HCIsNilTests.m in Sources */, + 0846B6FE13EE5FD200EA68E8 /* HCIsSameTests.m in Sources */, + 082E7C3213FF676E004A22FE /* MockSenTestCase.m in Sources */, + 082E7C3413FF676E004A22FE /* HCBaseMatcherTests.m in Sources */, + 082E7C3613FF676E004A22FE /* HCInvocationMatcherTests.m in Sources */, + 082E7C3813FF676E004A22FE /* HCStringDescriptionTests.m in Sources */, + 77C7B87A14293D0700DE60CC /* HCHasPropertyTests.m in Sources */, + 5F2773491436FAA700B9683A /* HCConformsToProtocolTests.m in Sources */, + BB3330581DF04CB5115BCD11 /* HCIsTypeOfTests.m in Sources */, + 0850A44F17165D2A0019BBE0 /* SomeClassAndSubclass.m in Sources */, + 31DDC19066E25E0DB52E3441 /* Mismatchable.m in Sources */, + 609E95B8EB994E0768A22EB5 /* HCTestFailureReporterChainTests.m in Sources */, + 609E9B41637AEC0C99B77B10 /* InterceptingTestCase.m in Sources */, + 609E9BD70804EB4AD2CF687B /* HCArgumentCaptorTests.m in Sources */, + 609E9068872F708CA4DA4785 /* MatcherTestCase.m in Sources */, + 609E93B6BAA64F7836FB754E /* HCIsCollectionContainingInRelativeOrderTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 087601DD13440806001B439B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BB33329CF0CDD9545D3FFCEC /* HCAssertThat.m in Sources */, + BB333D94082F074BB61E233C /* HCBaseDescription.m in Sources */, + BB333108B4630B94C5328B1A /* HCBaseMatcher.m in Sources */, + BB33315FDFFBAC0E1DB10869 /* HCStringDescription.m in Sources */, + BB33322D2EF1C40E940F38B2 /* HCHasCount.m in Sources */, + BB333A3F01C18400B5A131C6 /* HCIsCollectionContaining.m in Sources */, + BB3332D67329789E3971E2C3 /* HCIsCollectionContainingInAnyOrder.m in Sources */, + BB33346AFA507C00FA481E8D /* HCIsCollectionContainingInOrder.m in Sources */, + BB33335138021752A7522E67 /* HCIsCollectionOnlyContaining.m in Sources */, + BB33356A7CF5DC030AAE9CCB /* HCIsDictionaryContaining.m in Sources */, + BB333280BE085A371ABECECA /* HCIsDictionaryContainingEntries.m in Sources */, + BB333181C1BD36B166BDCABB /* HCIsDictionaryContainingKey.m in Sources */, + BB33311497FE29C3318919A7 /* HCIsDictionaryContainingValue.m in Sources */, + BB3337F52BA7EAA5CFCAE142 /* HCIsEmptyCollection.m in Sources */, + BB33390F0791F299C712BAF5 /* HCIsIn.m in Sources */, + BB3332DB2D2CA8A11AF95DA7 /* HCDescribedAs.m in Sources */, + BB3335B51CB6C13C046E7C94 /* HCIs.m in Sources */, + BB3332673485E95D8599222E /* HCAllOf.m in Sources */, + BB333D17BEC11419274E2D8F /* HCAnyOf.m in Sources */, + BB333A0391F4B076785DEBA9 /* HCIsAnything.m in Sources */, + 74153B651A53606100FEF450 /* HCIsTrueFalse.m in Sources */, + BB333637DDB74BF66C729C62 /* HCIsNot.m in Sources */, + BB333BF2FCAE2C05C337CE46 /* HCIsCloseTo.m in Sources */, + BB3331E9AEEC3D10110E90ED /* HCIsEqualToNumber.m in Sources */, + BB333E932D2D03A14B886055 /* HCNumberAssert.m in Sources */, + BB333C23AD2D7D8BF49032C1 /* HCOrderingComparison.m in Sources */, + BB333D6CD37B858BD8F989F1 /* HCClassMatcher.m in Sources */, + BB3333AADA8EEF37F45AF012 /* HCConformsToProtocol.m in Sources */, + BB3337466031638452E372CC /* HCHasDescription.m in Sources */, + BB3334311D89D9395F45A48D /* HCHasProperty.m in Sources */, + BB333EBAF352F0961A83E1A6 /* HCIsEqual.m in Sources */, + 746045691A29625E00196267 /* HCEvery.m in Sources */, + BB33328F6BBE1FF834BA90EC /* HCIsInstanceOf.m in Sources */, + BB333995C1E6476B0C239FB9 /* HCIsNil.m in Sources */, + 74FDDD171A3FF39900177999 /* HCThrowsException.m in Sources */, + BB333AB8B39B203DF15C9445 /* HCIsSame.m in Sources */, + BB333D81687E3E6481265818 /* HCIsTypeOf.m in Sources */, + BB333C6E35F2398771CEA2C3 /* HCIsEqualIgnoringCase.m in Sources */, + BB333AFA559C22D92650E91C /* HCIsEqualCompressingWhiteSpace.m in Sources */, + BB33379F2611DE99B3B43A90 /* HCStringContains.m in Sources */, + BB3333B08155109232BEBD15 /* HCStringContainsInOrder.m in Sources */, + BB33357C5F1F3BC687DDF54C /* HCStringEndsWith.m in Sources */, + BB33345773AB590845EB68D6 /* HCStringStartsWith.m in Sources */, + BB3330D4CB9195C81B32E5AB /* HCSubstringMatcher.m in Sources */, + BB3333818CFAC6A68AEBC72E /* HCGenericTestFailureReporter.m in Sources */, + BB333FC3961C09A902683A7F /* HCSenTestFailureReporter.m in Sources */, + BB3339CC63BCC9186973C491 /* HCTestFailure.m in Sources */, + BB33339CC090FAA28495D835 /* HCTestFailureReporter.m in Sources */, + BB333FF1353091B71E6A3B33 /* HCTestFailureReporterChain.m in Sources */, + BB333451E989F24386136004 /* HCXCTestFailureReporter.m in Sources */, + BB3333BCD2E3A2D26AA15D40 /* HCBoolReturnGetter.m in Sources */, + BB33309748583DA78A7C98ED /* HCCharReturnGetter.m in Sources */, + BB3335FB367CC6AD40BF6BA7 /* HCDoubleReturnGetter.m in Sources */, + BB3337DBEBED3443E8DB7A52 /* HCFloatReturnGetter.m in Sources */, + BB333C78E5F9DB528838CA5A /* HCIntReturnGetter.m in Sources */, + BB333FD5A85510AE5AA9B519 /* HCLongLongReturnGetter.m in Sources */, + BB3333502C76D73346A73631 /* HCLongReturnGetter.m in Sources */, + 0E5ECE6E1BC20C3300C15D2A /* HCIsCollectionContainingInRelativeOrder.m in Sources */, + BB3338E155E644CFB619CFEF /* HCObjectReturnGetter.m in Sources */, + BB3335EAC1E9E47F87D50EDF /* HCReturnValueGetter.m in Sources */, + BB3330504048A30E7C190A19 /* HCReturnTypeHandlerChain.m in Sources */, + BB33362FA587A9B15E5C0D9B /* HCShortReturnGetter.m in Sources */, + BB3336B101EC9615B113A6D5 /* HCUnsignedCharReturnGetter.m in Sources */, + BB3337CC95C9FD768019495D /* HCUnsignedIntReturnGetter.m in Sources */, + BB3332C8A3C41D9A4A4211AC /* HCUnsignedLongLongReturnGetter.m in Sources */, + BB3333CBF2F94416D6F3E265 /* HCUnsignedLongReturnGetter.m in Sources */, + BB3331D8A9949686B47D2203 /* HCUnsignedShortReturnGetter.m in Sources */, + BB3339C714B5E796538B65F1 /* NSInvocation+OCHamcrest.m in Sources */, + BB33317BFCB5DC5993AA5C91 /* HCRequireNonNilObject.m in Sources */, + BB333BE5AC86690747041C24 /* HCWrapInMatcher.m in Sources */, + BB3332D945BB26BEF3E37662 /* HCCollect.m in Sources */, + BB33306BE19806EB64EEC358 /* HCInvocationMatcher.m in Sources */, + 31DDC99BB130EC06BA6BFBEC /* HCDiagnosingMatcher.m in Sources */, + 609E95FB6FCE5CDCA1E7A9BF /* HCArgumentCaptor.m in Sources */, + 609E9A8C8D6D971BFE42AA1E /* HCRunloopRunner.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 087601F213440807001B439B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0876036313440AD8001B439B /* FakeWithCount.m in Sources */, + 0876036413440AD8001B439B /* FakeWithoutCount.m in Sources */, + 0876036513440AD8001B439B /* HCHasCountTests.m in Sources */, + 0876036613440AD8001B439B /* HCIsCollectionContainingInAnyOrderTests.m in Sources */, + 0876036713440AD8001B439B /* HCIsCollectionContainingInOrderTests.m in Sources */, + 0876036813440AD8001B439B /* HCIsCollectionContainingTests.m in Sources */, + 0876036913440AD8001B439B /* HCIsCollectionOnlyContainingTests.m in Sources */, + 0876036A13440AD8001B439B /* HCIsDictionaryContainingEntriesTests.m in Sources */, + 0876036B13440AD8001B439B /* HCIsDictionaryContainingKeyTests.m in Sources */, + 0876036C13440AD8001B439B /* HCIsDictionaryContainingTests.m in Sources */, + 0876036D13440AD8001B439B /* HCIsDictionaryContainingValueTests.m in Sources */, + 0876036E13440AD8001B439B /* HCIsEmptyCollectionTests.m in Sources */, + 0876036F13440AD8001B439B /* HCIsInTests.m in Sources */, + 0876037C13440AD8001B439B /* HCIsCloseToTests.m in Sources */, + 0876037D13440AD8001B439B /* HCIsEqualToNumberTests.m in Sources */, + 0876037E13440AD8001B439B /* HCNumberAssertTests.m in Sources */, + 0876037F13440AD8001B439B /* HCOrderingComparisonTests.m in Sources */, + 0876038013440AD8001B439B /* HCHasDescriptionTests.m in Sources */, + 0876038213440AD8001B439B /* HCIsEqualIgnoringCaseTests.m in Sources */, + 7460456C1A2964AF00196267 /* HCEveryTests.m in Sources */, + 0876038313440AD8001B439B /* HCIsEqualCompressingWhiteSpaceTests.m in Sources */, + 0876038413440AD8001B439B /* HCStringContainsTests.m in Sources */, + 0876038513440AD8001B439B /* HCStringEndsWithTests.m in Sources */, + 0876038613440AD8001B439B /* HCStringStartsWithTests.m in Sources */, + 088FB0AB136A6DEA00C191E1 /* HCStringContainsInOrderTests.m in Sources */, + 74153B5F1A535FEA00FEF450 /* HCIsTrueFalseTests.m in Sources */, + 74D7A7B51A2B902F00CD6CC0 /* DiagnosingMatcherTest.m in Sources */, + 0846B6E013EE5F9D00EA68E8 /* HCDescribedAsTests.m in Sources */, + 0846B6E213EE5F9D00EA68E8 /* HCIsTests.m in Sources */, + 482FE2D31E6DDC2B00AC0009 /* HCWrapInMatcherTests.m in Sources */, + 0846B6E413EE5F9D00EA68E8 /* NeverMatch.m in Sources */, + 0846B6EB13EE5FB400EA68E8 /* HCAllOfTests.m in Sources */, + 0846B6ED13EE5FB400EA68E8 /* HCAnyOfTests.m in Sources */, + 7458920F1A2AAD2C00BAAC76 /* AssertWithTimeoutTests.m in Sources */, + 0846B6EF13EE5FB400EA68E8 /* HCIsAnythingTests.m in Sources */, + 747776C71A3F5060000A6E1D /* HCThrowsExceptionTests.m in Sources */, + 0846B6F113EE5FB400EA68E8 /* HCIsNotTests.m in Sources */, + 0846B6F713EE5FD200EA68E8 /* HCIsEqualTests.m in Sources */, + 0846B6F913EE5FD200EA68E8 /* HCIsInstanceOfTests.m in Sources */, + 0846B6FB13EE5FD200EA68E8 /* HCIsNilTests.m in Sources */, + 0846B6FD13EE5FD200EA68E8 /* HCIsSameTests.m in Sources */, + 082E7C3113FF676E004A22FE /* MockSenTestCase.m in Sources */, + 082E7C3313FF676E004A22FE /* HCBaseMatcherTests.m in Sources */, + 082E7C3513FF676E004A22FE /* HCInvocationMatcherTests.m in Sources */, + 082E7C3713FF676E004A22FE /* HCStringDescriptionTests.m in Sources */, + 77C7B8771429361200DE60CC /* HCHasPropertyTests.m in Sources */, + 5F2773481436FAA600B9683A /* HCConformsToProtocolTests.m in Sources */, + BB33303F6D1004392A6EF76A /* HCIsTypeOfTests.m in Sources */, + 0850A44E17165D2A0019BBE0 /* SomeClassAndSubclass.m in Sources */, + 31DDCC204A3D141D7AC6FC02 /* Mismatchable.m in Sources */, + 609E99C4BEBB5A70318577B2 /* HCTestFailureReporterChainTests.m in Sources */, + 609E941591B81120F216AAAF /* InterceptingTestCase.m in Sources */, + 609E9E7A947E1D0029F3233D /* HCArgumentCaptorTests.m in Sources */, + 609E97770916062AC9B0D5BB /* MatcherTestCase.m in Sources */, + 609E9DB70E41F094B68A20C3 /* HCIsCollectionContainingInRelativeOrderTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 24C5BFE41C1777D900C2BAFD /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 24C5C00B1C17794900C2BAFD /* HCAssertThat.m in Sources */, + 24C5C00C1C17794900C2BAFD /* HCBaseDescription.m in Sources */, + 24C5C00D1C17794900C2BAFD /* HCBaseMatcher.m in Sources */, + 24C5C00E1C17794900C2BAFD /* HCDiagnosingMatcher.m in Sources */, + 24C5C00F1C17794900C2BAFD /* HCStringDescription.m in Sources */, + 24C5C0101C17794900C2BAFD /* HCBoolReturnGetter.m in Sources */, + 24C5C0111C17794900C2BAFD /* HCCharReturnGetter.m in Sources */, + 24C5C0121C17794900C2BAFD /* HCDoubleReturnGetter.m in Sources */, + 24C5C0131C17794900C2BAFD /* HCFloatReturnGetter.m in Sources */, + 24C5C0141C17794900C2BAFD /* HCIntReturnGetter.m in Sources */, + 24C5C0151C17794900C2BAFD /* HCLongLongReturnGetter.m in Sources */, + 24C5C0161C17794900C2BAFD /* HCLongReturnGetter.m in Sources */, + 24C5C0171C17794900C2BAFD /* HCObjectReturnGetter.m in Sources */, + 24C5C0181C17794900C2BAFD /* HCReturnValueGetter.m in Sources */, + 24C5C0191C17794900C2BAFD /* HCReturnTypeHandlerChain.m in Sources */, + 24C5C01A1C17794900C2BAFD /* HCShortReturnGetter.m in Sources */, + 24C5C01B1C17794900C2BAFD /* HCUnsignedCharReturnGetter.m in Sources */, + 24C5C01C1C17794900C2BAFD /* HCUnsignedIntReturnGetter.m in Sources */, + 24C5C01D1C17794900C2BAFD /* HCUnsignedLongLongReturnGetter.m in Sources */, + 24C5C01E1C17794900C2BAFD /* HCUnsignedLongReturnGetter.m in Sources */, + 24C5C01F1C17794900C2BAFD /* HCUnsignedShortReturnGetter.m in Sources */, + 24C5C0201C17794900C2BAFD /* HCGenericTestFailureReporter.m in Sources */, + 24C5C0211C17794900C2BAFD /* HCSenTestFailureReporter.m in Sources */, + 24C5C0221C17794900C2BAFD /* HCTestFailure.m in Sources */, + 24C5C0231C17794900C2BAFD /* HCTestFailureReporter.m in Sources */, + 24C5C0241C17794900C2BAFD /* HCTestFailureReporterChain.m in Sources */, + 24C5C0251C17794900C2BAFD /* HCXCTestFailureReporter.m in Sources */, + 24C5C0261C17794900C2BAFD /* HCCollect.m in Sources */, + 24C5C0271C17794900C2BAFD /* HCInvocationMatcher.m in Sources */, + 24C5C0281C17794900C2BAFD /* HCRequireNonNilObject.m in Sources */, + 24C5C0291C17794900C2BAFD /* HCWrapInMatcher.m in Sources */, + 24C5C02A1C17794900C2BAFD /* NSInvocation+OCHamcrest.m in Sources */, + 24C5C02B1C17794900C2BAFD /* HCEvery.m in Sources */, + 24C5C02C1C17794900C2BAFD /* HCHasCount.m in Sources */, + 24C5C02D1C17794900C2BAFD /* HCIsCollectionContaining.m in Sources */, + 24C5C02E1C17794900C2BAFD /* HCIsCollectionContainingInAnyOrder.m in Sources */, + 24C5C02F1C17794900C2BAFD /* HCIsCollectionContainingInOrder.m in Sources */, + 24C5C0301C17794900C2BAFD /* HCIsCollectionContainingInRelativeOrder.m in Sources */, + 24C5C0311C17794900C2BAFD /* HCIsCollectionOnlyContaining.m in Sources */, + 24C5C0321C17794900C2BAFD /* HCIsDictionaryContaining.m in Sources */, + 24C5C0331C17794900C2BAFD /* HCIsDictionaryContainingEntries.m in Sources */, + 24C5C0341C17794900C2BAFD /* HCIsDictionaryContainingKey.m in Sources */, + 24C5C0351C17794900C2BAFD /* HCIsDictionaryContainingValue.m in Sources */, + 24C5C0361C17794900C2BAFD /* HCIsEmptyCollection.m in Sources */, + 24C5C0371C17794900C2BAFD /* HCIsIn.m in Sources */, + 24C5C0381C17794900C2BAFD /* HCDescribedAs.m in Sources */, + 24C5C0391C17794900C2BAFD /* HCIs.m in Sources */, + 24C5C03A1C17794900C2BAFD /* HCAllOf.m in Sources */, + 24C5C03B1C17794900C2BAFD /* HCAnyOf.m in Sources */, + 24C5C03C1C17794900C2BAFD /* HCIsAnything.m in Sources */, + 24C5C03D1C17794900C2BAFD /* HCIsNot.m in Sources */, + 24C5C03E1C17794900C2BAFD /* HCIsCloseTo.m in Sources */, + 24C5C03F1C17794900C2BAFD /* HCIsEqualToNumber.m in Sources */, + 24C5C0401C17794900C2BAFD /* HCIsTrueFalse.m in Sources */, + 24C5C0411C17794900C2BAFD /* HCNumberAssert.m in Sources */, + 24C5C0421C17794900C2BAFD /* HCOrderingComparison.m in Sources */, + 24C5C0431C17794900C2BAFD /* HCArgumentCaptor.m in Sources */, + 24C5C0441C17794900C2BAFD /* HCClassMatcher.m in Sources */, + 24C5C0451C17794900C2BAFD /* HCConformsToProtocol.m in Sources */, + 24C5C0461C17794900C2BAFD /* HCHasDescription.m in Sources */, + 24C5C0471C17794900C2BAFD /* HCHasProperty.m in Sources */, + 24C5C0481C17794900C2BAFD /* HCIsEqual.m in Sources */, + 24C5C0491C17794900C2BAFD /* HCIsInstanceOf.m in Sources */, + 24C5C04A1C17794900C2BAFD /* HCIsNil.m in Sources */, + 24C5C04B1C17794900C2BAFD /* HCIsSame.m in Sources */, + 24C5C04C1C17794900C2BAFD /* HCIsTypeOf.m in Sources */, + 24C5C04D1C17794900C2BAFD /* HCThrowsException.m in Sources */, + 24C5C04E1C17794900C2BAFD /* HCIsEqualIgnoringCase.m in Sources */, + 24C5C04F1C17794900C2BAFD /* HCIsEqualCompressingWhiteSpace.m in Sources */, + 24C5C0501C17794900C2BAFD /* HCStringContains.m in Sources */, + 24C5C0511C17794900C2BAFD /* HCStringContainsInOrder.m in Sources */, + 24C5C0521C17794900C2BAFD /* HCStringEndsWith.m in Sources */, + 24C5C0531C17794900C2BAFD /* HCStringStartsWith.m in Sources */, + 24C5C0541C17794900C2BAFD /* HCSubstringMatcher.m in Sources */, + 609E90D232E0B31844E7BA6D /* HCRunloopRunner.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 24C5BFF11C17781400C2BAFD /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 24C5C0551C17796500C2BAFD /* HCAssertThat.m in Sources */, + 24C5C0561C17796500C2BAFD /* HCBaseDescription.m in Sources */, + 24C5C0571C17796500C2BAFD /* HCBaseMatcher.m in Sources */, + 24C5C0581C17796500C2BAFD /* HCDiagnosingMatcher.m in Sources */, + 24C5C0591C17796500C2BAFD /* HCStringDescription.m in Sources */, + 24C5C05A1C17796500C2BAFD /* HCBoolReturnGetter.m in Sources */, + 24C5C05B1C17796500C2BAFD /* HCCharReturnGetter.m in Sources */, + 24C5C05C1C17796500C2BAFD /* HCDoubleReturnGetter.m in Sources */, + 24C5C05D1C17796500C2BAFD /* HCFloatReturnGetter.m in Sources */, + 24C5C05E1C17796500C2BAFD /* HCIntReturnGetter.m in Sources */, + 24C5C05F1C17796500C2BAFD /* HCLongLongReturnGetter.m in Sources */, + 24C5C0601C17796500C2BAFD /* HCLongReturnGetter.m in Sources */, + 24C5C0611C17796500C2BAFD /* HCObjectReturnGetter.m in Sources */, + 24C5C0621C17796500C2BAFD /* HCReturnValueGetter.m in Sources */, + 24C5C0631C17796500C2BAFD /* HCReturnTypeHandlerChain.m in Sources */, + 24C5C0641C17796500C2BAFD /* HCShortReturnGetter.m in Sources */, + 24C5C0651C17796500C2BAFD /* HCUnsignedCharReturnGetter.m in Sources */, + 24C5C0661C17796500C2BAFD /* HCUnsignedIntReturnGetter.m in Sources */, + 24C5C0671C17796500C2BAFD /* HCUnsignedLongLongReturnGetter.m in Sources */, + 24C5C0681C17796500C2BAFD /* HCUnsignedLongReturnGetter.m in Sources */, + 24C5C0691C17796500C2BAFD /* HCUnsignedShortReturnGetter.m in Sources */, + 24C5C06A1C17796500C2BAFD /* HCGenericTestFailureReporter.m in Sources */, + 24C5C06B1C17796500C2BAFD /* HCSenTestFailureReporter.m in Sources */, + 24C5C06C1C17796500C2BAFD /* HCTestFailure.m in Sources */, + 24C5C06D1C17796500C2BAFD /* HCTestFailureReporter.m in Sources */, + 24C5C06E1C17796600C2BAFD /* HCTestFailureReporterChain.m in Sources */, + 24C5C06F1C17796600C2BAFD /* HCXCTestFailureReporter.m in Sources */, + 24C5C0701C17796600C2BAFD /* HCCollect.m in Sources */, + 24C5C0711C17796600C2BAFD /* HCInvocationMatcher.m in Sources */, + 24C5C0721C17796600C2BAFD /* HCRequireNonNilObject.m in Sources */, + 24C5C0731C17796600C2BAFD /* HCWrapInMatcher.m in Sources */, + 24C5C0741C17796600C2BAFD /* NSInvocation+OCHamcrest.m in Sources */, + 24C5C0751C17796600C2BAFD /* HCEvery.m in Sources */, + 24C5C0761C17796600C2BAFD /* HCHasCount.m in Sources */, + 24C5C0771C17796600C2BAFD /* HCIsCollectionContaining.m in Sources */, + 24C5C0781C17796600C2BAFD /* HCIsCollectionContainingInAnyOrder.m in Sources */, + 24C5C0791C17796600C2BAFD /* HCIsCollectionContainingInOrder.m in Sources */, + 24C5C07A1C17796600C2BAFD /* HCIsCollectionContainingInRelativeOrder.m in Sources */, + 24C5C07B1C17796600C2BAFD /* HCIsCollectionOnlyContaining.m in Sources */, + 24C5C07C1C17796600C2BAFD /* HCIsDictionaryContaining.m in Sources */, + 24C5C07D1C17796600C2BAFD /* HCIsDictionaryContainingEntries.m in Sources */, + 24C5C07E1C17796600C2BAFD /* HCIsDictionaryContainingKey.m in Sources */, + 24C5C07F1C17796600C2BAFD /* HCIsDictionaryContainingValue.m in Sources */, + 24C5C0801C17796600C2BAFD /* HCIsEmptyCollection.m in Sources */, + 24C5C0811C17796600C2BAFD /* HCIsIn.m in Sources */, + 24C5C0821C17796600C2BAFD /* HCDescribedAs.m in Sources */, + 24C5C0831C17796600C2BAFD /* HCIs.m in Sources */, + 24C5C0841C17796600C2BAFD /* HCAllOf.m in Sources */, + 24C5C0851C17796600C2BAFD /* HCAnyOf.m in Sources */, + 24C5C0861C17796600C2BAFD /* HCIsAnything.m in Sources */, + 24C5C0871C17796600C2BAFD /* HCIsNot.m in Sources */, + 24C5C0881C17796600C2BAFD /* HCIsCloseTo.m in Sources */, + 24C5C0891C17796600C2BAFD /* HCIsEqualToNumber.m in Sources */, + 24C5C08A1C17796600C2BAFD /* HCIsTrueFalse.m in Sources */, + 24C5C08B1C17796600C2BAFD /* HCNumberAssert.m in Sources */, + 24C5C08C1C17796600C2BAFD /* HCOrderingComparison.m in Sources */, + 24C5C08D1C17796600C2BAFD /* HCArgumentCaptor.m in Sources */, + 24C5C08E1C17796600C2BAFD /* HCClassMatcher.m in Sources */, + 24C5C08F1C17796600C2BAFD /* HCConformsToProtocol.m in Sources */, + 24C5C0901C17796600C2BAFD /* HCHasDescription.m in Sources */, + 24C5C0911C17796600C2BAFD /* HCHasProperty.m in Sources */, + 24C5C0921C17796600C2BAFD /* HCIsEqual.m in Sources */, + 24C5C0931C17796600C2BAFD /* HCIsInstanceOf.m in Sources */, + 24C5C0941C17796600C2BAFD /* HCIsNil.m in Sources */, + 24C5C0951C17796600C2BAFD /* HCIsSame.m in Sources */, + 24C5C0961C17796600C2BAFD /* HCIsTypeOf.m in Sources */, + 24C5C0971C17796600C2BAFD /* HCThrowsException.m in Sources */, + 24C5C0981C17796600C2BAFD /* HCIsEqualIgnoringCase.m in Sources */, + 24C5C0991C17796600C2BAFD /* HCIsEqualCompressingWhiteSpace.m in Sources */, + 24C5C09A1C17796600C2BAFD /* HCStringContains.m in Sources */, + 24C5C09B1C17796600C2BAFD /* HCStringContainsInOrder.m in Sources */, + 24C5C09C1C17796600C2BAFD /* HCStringEndsWith.m in Sources */, + 24C5C09D1C17796600C2BAFD /* HCStringStartsWith.m in Sources */, + 24C5C09E1C17796600C2BAFD /* HCSubstringMatcher.m in Sources */, + 609E9A2DDB1D66CF6B8384FF /* HCRunloopRunner.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 24C5BFFE1C17782500C2BAFD /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 24C5C09F1C17797200C2BAFD /* HCAssertThat.m in Sources */, + 24C5C0A01C17797200C2BAFD /* HCBaseDescription.m in Sources */, + 24C5C0A11C17797200C2BAFD /* HCBaseMatcher.m in Sources */, + 24C5C0A21C17797200C2BAFD /* HCDiagnosingMatcher.m in Sources */, + 24C5C0A31C17797200C2BAFD /* HCStringDescription.m in Sources */, + 24C5C0A41C17797200C2BAFD /* HCBoolReturnGetter.m in Sources */, + 24C5C0A51C17797200C2BAFD /* HCCharReturnGetter.m in Sources */, + 24C5C0A61C17797200C2BAFD /* HCDoubleReturnGetter.m in Sources */, + 24C5C0A71C17797200C2BAFD /* HCFloatReturnGetter.m in Sources */, + 24C5C0A81C17797200C2BAFD /* HCIntReturnGetter.m in Sources */, + 24C5C0A91C17797200C2BAFD /* HCLongLongReturnGetter.m in Sources */, + 24C5C0AA1C17797200C2BAFD /* HCLongReturnGetter.m in Sources */, + 24C5C0AB1C17797200C2BAFD /* HCObjectReturnGetter.m in Sources */, + 24C5C0AC1C17797200C2BAFD /* HCReturnValueGetter.m in Sources */, + 24C5C0AD1C17797200C2BAFD /* HCReturnTypeHandlerChain.m in Sources */, + 24C5C0AE1C17797200C2BAFD /* HCShortReturnGetter.m in Sources */, + 24C5C0AF1C17797200C2BAFD /* HCUnsignedCharReturnGetter.m in Sources */, + 24C5C0B01C17797200C2BAFD /* HCUnsignedIntReturnGetter.m in Sources */, + 24C5C0B11C17797200C2BAFD /* HCUnsignedLongLongReturnGetter.m in Sources */, + 24C5C0B21C17797200C2BAFD /* HCUnsignedLongReturnGetter.m in Sources */, + 24C5C0B31C17797200C2BAFD /* HCUnsignedShortReturnGetter.m in Sources */, + 24C5C0B41C17797200C2BAFD /* HCGenericTestFailureReporter.m in Sources */, + 24C5C0B51C17797200C2BAFD /* HCSenTestFailureReporter.m in Sources */, + 24C5C0B61C17797200C2BAFD /* HCTestFailure.m in Sources */, + 24C5C0B71C17797200C2BAFD /* HCTestFailureReporter.m in Sources */, + 24C5C0B81C17797200C2BAFD /* HCTestFailureReporterChain.m in Sources */, + 24C5C0B91C17797200C2BAFD /* HCXCTestFailureReporter.m in Sources */, + 24C5C0BA1C17797200C2BAFD /* HCCollect.m in Sources */, + 24C5C0BB1C17797200C2BAFD /* HCInvocationMatcher.m in Sources */, + 24C5C0BC1C17797200C2BAFD /* HCRequireNonNilObject.m in Sources */, + 24C5C0BD1C17797200C2BAFD /* HCWrapInMatcher.m in Sources */, + 24C5C0BE1C17797200C2BAFD /* NSInvocation+OCHamcrest.m in Sources */, + 24C5C0BF1C17797200C2BAFD /* HCEvery.m in Sources */, + 24C5C0C01C17797200C2BAFD /* HCHasCount.m in Sources */, + 24C5C0C11C17797200C2BAFD /* HCIsCollectionContaining.m in Sources */, + 24C5C0C21C17797200C2BAFD /* HCIsCollectionContainingInAnyOrder.m in Sources */, + 24C5C0C31C17797200C2BAFD /* HCIsCollectionContainingInOrder.m in Sources */, + 24C5C0C41C17797200C2BAFD /* HCIsCollectionContainingInRelativeOrder.m in Sources */, + 24C5C0C51C17797200C2BAFD /* HCIsCollectionOnlyContaining.m in Sources */, + 24C5C0C61C17797200C2BAFD /* HCIsDictionaryContaining.m in Sources */, + 24C5C0C71C17797200C2BAFD /* HCIsDictionaryContainingEntries.m in Sources */, + 24C5C0C81C17797200C2BAFD /* HCIsDictionaryContainingKey.m in Sources */, + 24C5C0C91C17797200C2BAFD /* HCIsDictionaryContainingValue.m in Sources */, + 24C5C0CA1C17797200C2BAFD /* HCIsEmptyCollection.m in Sources */, + 24C5C0CB1C17797200C2BAFD /* HCIsIn.m in Sources */, + 24C5C0CC1C17797200C2BAFD /* HCDescribedAs.m in Sources */, + 24C5C0CD1C17797200C2BAFD /* HCIs.m in Sources */, + 24C5C0CE1C17797200C2BAFD /* HCAllOf.m in Sources */, + 24C5C0CF1C17797200C2BAFD /* HCAnyOf.m in Sources */, + 24C5C0D01C17797200C2BAFD /* HCIsAnything.m in Sources */, + 24C5C0D11C17797200C2BAFD /* HCIsNot.m in Sources */, + 24C5C0D21C17797200C2BAFD /* HCIsCloseTo.m in Sources */, + 24C5C0D31C17797200C2BAFD /* HCIsEqualToNumber.m in Sources */, + 24C5C0D41C17797200C2BAFD /* HCIsTrueFalse.m in Sources */, + 24C5C0D51C17797200C2BAFD /* HCNumberAssert.m in Sources */, + 24C5C0D61C17797200C2BAFD /* HCOrderingComparison.m in Sources */, + 24C5C0D71C17797200C2BAFD /* HCArgumentCaptor.m in Sources */, + 24C5C0D81C17797200C2BAFD /* HCClassMatcher.m in Sources */, + 24C5C0D91C17797200C2BAFD /* HCConformsToProtocol.m in Sources */, + 24C5C0DA1C17797200C2BAFD /* HCHasDescription.m in Sources */, + 24C5C0DB1C17797200C2BAFD /* HCHasProperty.m in Sources */, + 24C5C0DC1C17797200C2BAFD /* HCIsEqual.m in Sources */, + 24C5C0DD1C17797200C2BAFD /* HCIsInstanceOf.m in Sources */, + 24C5C0DE1C17797200C2BAFD /* HCIsNil.m in Sources */, + 24C5C0DF1C17797200C2BAFD /* HCIsSame.m in Sources */, + 24C5C0E01C17797200C2BAFD /* HCIsTypeOf.m in Sources */, + 24C5C0E11C17797200C2BAFD /* HCThrowsException.m in Sources */, + 24C5C0E21C17797200C2BAFD /* HCIsEqualIgnoringCase.m in Sources */, + 24C5C0E31C17797200C2BAFD /* HCIsEqualCompressingWhiteSpace.m in Sources */, + 24C5C0E41C17797200C2BAFD /* HCStringContains.m in Sources */, + 24C5C0E51C17797200C2BAFD /* HCStringContainsInOrder.m in Sources */, + 24C5C0E61C17797200C2BAFD /* HCStringEndsWith.m in Sources */, + 24C5C0E71C17797200C2BAFD /* HCStringStartsWith.m in Sources */, + 24C5C0E81C17797200C2BAFD /* HCSubstringMatcher.m in Sources */, + 609E945B39EA3C98F2B6C224 /* HCRunloopRunner.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 081BEE711345979F003F846A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 081BEE611345979F003F846A /* libochamcrest */; + targetProxy = 081BEE701345979F003F846A /* PBXContainerItemProxy */; + }; + 087601FA13440807001B439B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 087601E113440806001B439B /* OCHamcrest */; + targetProxy = 087601F913440807001B439B /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 081BEE7F1345979F003F846A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0E49BBDB1C47498C00418A3C /* XcodeWarnings.xcconfig */; + buildSettings = { + DSTROOT = /tmp/libochamcrest.dst; + OTHER_CFLAGS = "-fobjc-arc-exceptions"; + PRODUCT_NAME = ochamcrest; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 081BEE801345979F003F846A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0E49BBDB1C47498C00418A3C /* XcodeWarnings.xcconfig */; + buildSettings = { + DSTROOT = /tmp/libochamcrest.dst; + OTHER_CFLAGS = "-fobjc-arc-exceptions"; + PRODUCT_NAME = ochamcrest; + SDKROOT = iphoneos; + }; + name = Release; + }; + 081BEE821345979F003F846A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0E49BBDB1C47498C00418A3C /* XcodeWarnings.xcconfig */; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = "$(DEVELOPER_LIBRARY_DIR)/Frameworks"; + INFOPLIST_FILE = "Tests-Info.plist"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_BUNDLE_IDENTIFIER = org.hamcrest.Tests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 081BEE831345979F003F846A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0E49BBDB1C47498C00418A3C /* XcodeWarnings.xcconfig */; + buildSettings = { + FRAMEWORK_SEARCH_PATHS = "$(DEVELOPER_LIBRARY_DIR)/Frameworks"; + INFOPLIST_FILE = "Tests-Info.plist"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_BUNDLE_IDENTIFIER = org.hamcrest.Tests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + }; + name = Release; + }; + 0876020713440807001B439B /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0E49BBDB1C47498C00418A3C /* XcodeWarnings.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = DEBUG; + GCC_VERSION = ""; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_DYLIB_INSTALL_NAME = "@rpath/$(EXECUTABLE_PATH)"; + MACOSX_DEPLOYMENT_TARGET = 10.10; + ONLY_ACTIVE_ARCH = YES; + }; + name = Debug; + }; + 0876020813440807001B439B /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0E49BBDB1C47498C00418A3C /* XcodeWarnings.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + ENABLE_NS_ASSERTIONS = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 3; + GCC_VERSION = ""; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_DYLIB_INSTALL_NAME = "@rpath/$(EXECUTABLE_PATH)"; + MACOSX_DEPLOYMENT_TARGET = 10.10; + }; + name = Release; + }; + 0876020A13440807001B439B /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0E49BBDB1C47498C00418A3C /* XcodeWarnings.xcconfig */; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 7.1.2; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = "OCHamcrest-Info.plist"; + MARKETING_VERSION = 7.1.2; + OTHER_CFLAGS = "-fobjc-arc-exceptions"; + PRODUCT_BUNDLE_IDENTIFIER = "org.hamcrest.${PRODUCT_NAME}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + WRAPPER_EXTENSION = framework; + }; + name = Debug; + }; + 0876020B13440807001B439B /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0E49BBDB1C47498C00418A3C /* XcodeWarnings.xcconfig */; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = YES; + CURRENT_PROJECT_VERSION = 7.1.2; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + FRAMEWORK_VERSION = A; + INFOPLIST_FILE = "OCHamcrest-Info.plist"; + MARKETING_VERSION = 7.1.2; + OTHER_CFLAGS = "-fobjc-arc-exceptions"; + PRODUCT_BUNDLE_IDENTIFIER = "org.hamcrest.${PRODUCT_NAME}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + WRAPPER_EXTENSION = framework; + }; + name = Release; + }; + 0876020D13440807001B439B /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0E49BBDB1C47498C00418A3C /* XcodeWarnings.xcconfig */; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + FRAMEWORK_SEARCH_PATHS = "$(DEVELOPER_LIBRARY_DIR)/Frameworks"; + INFOPLIST_FILE = "Tests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = org.hamcrest.Tests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + }; + name = Debug; + }; + 0876020E13440807001B439B /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0E49BBDB1C47498C00418A3C /* XcodeWarnings.xcconfig */; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = YES; + FRAMEWORK_SEARCH_PATHS = "$(DEVELOPER_LIBRARY_DIR)/Frameworks"; + INFOPLIST_FILE = "Tests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = org.hamcrest.Tests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + }; + name = Release; + }; + 24C5BFEE1C1777D900C2BAFD /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0E49BBDB1C47498C00418A3C /* XcodeWarnings.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_WARN_UNREACHABLE_CODE = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = "OCHamcrest-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.hamcrest.OCHamcrest.OCHamcrest-iOS"; + PRODUCT_NAME = OCHamcrest; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 24C5BFEF1C1777D900C2BAFD /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0E49BBDB1C47498C00418A3C /* XcodeWarnings.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_WARN_UNREACHABLE_CODE = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = "OCHamcrest-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.hamcrest.OCHamcrest.OCHamcrest-iOS"; + PRODUCT_NAME = OCHamcrest; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 24C5BFFC1C17781400C2BAFD /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0E49BBDB1C47498C00418A3C /* XcodeWarnings.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_WARN_UNREACHABLE_CODE = YES; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = "OCHamcrest-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.hamcrest.OCHamcrest.OCHamcrest-tvOS"; + PRODUCT_NAME = OCHamcrest; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 9.0; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 24C5BFFD1C17781400C2BAFD /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0E49BBDB1C47498C00418A3C /* XcodeWarnings.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_WARN_UNREACHABLE_CODE = YES; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = "OCHamcrest-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.hamcrest.OCHamcrest.OCHamcrest-tvOS"; + PRODUCT_NAME = OCHamcrest; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 9.0; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 24C5C0091C17782500C2BAFD /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0E49BBDB1C47498C00418A3C /* XcodeWarnings.xcconfig */; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_WARN_UNREACHABLE_CODE = YES; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = "OCHamcrest-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.hamcrest.OCHamcrest.OCHamcrest-watchOS"; + PRODUCT_NAME = OCHamcrest; + SDKROOT = watchos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = 4; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + WATCHOS_DEPLOYMENT_TARGET = 2.0; + }; + name = Debug; + }; + 24C5C00A1C17782500C2BAFD /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0E49BBDB1C47498C00418A3C /* XcodeWarnings.xcconfig */; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_WARN_UNREACHABLE_CODE = YES; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = "OCHamcrest-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "org.hamcrest.OCHamcrest.OCHamcrest-watchOS"; + PRODUCT_NAME = OCHamcrest; + SDKROOT = watchos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = 4; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + WATCHOS_DEPLOYMENT_TARGET = 2.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 081BEE7E1345979F003F846A /* Build configuration list for PBXNativeTarget "libochamcrest" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 081BEE7F1345979F003F846A /* Debug */, + 081BEE801345979F003F846A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 081BEE811345979F003F846A /* Build configuration list for PBXNativeTarget "libochamcrestTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 081BEE821345979F003F846A /* Debug */, + 081BEE831345979F003F846A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 087601DB13440806001B439B /* Build configuration list for PBXProject "OCHamcrest" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0876020713440807001B439B /* Debug */, + 0876020813440807001B439B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 0876020913440807001B439B /* Build configuration list for PBXNativeTarget "OCHamcrest" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0876020A13440807001B439B /* Debug */, + 0876020B13440807001B439B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 0876020C13440807001B439B /* Build configuration list for PBXNativeTarget "OCHamcrestTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0876020D13440807001B439B /* Debug */, + 0876020E13440807001B439B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 24C5BFF01C1777D900C2BAFD /* Build configuration list for PBXNativeTarget "OCHamcrest-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 24C5BFEE1C1777D900C2BAFD /* Debug */, + 24C5BFEF1C1777D900C2BAFD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 24C5BFFB1C17781400C2BAFD /* Build configuration list for PBXNativeTarget "OCHamcrest-tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 24C5BFFC1C17781400C2BAFD /* Debug */, + 24C5BFFD1C17781400C2BAFD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 24C5C0081C17782500C2BAFD /* Build configuration list for PBXNativeTarget "OCHamcrest-watchOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 24C5C0091C17782500C2BAFD /* Debug */, + 24C5C00A1C17782500C2BAFD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 087601D813440806001B439B /* Project object */; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests-Info.plist b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests-Info.plist new file mode 100644 index 0000000000..460a7d931c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests-Info.plist @@ -0,0 +1,20 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + BNDL + CFBundleSignature + ???? + CFBundleVersion + 1.0 + + diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/FakeWithCount.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/FakeWithCount.h new file mode 100644 index 0000000000..6b1da50a1e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/FakeWithCount.h @@ -0,0 +1,18 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +@import Foundation; + + +NS_ASSUME_NONNULL_BEGIN + +@interface FakeWithCount : NSObject + +@property (nonatomic, assign, readonly) NSUInteger count; + ++ (instancetype)fakeWithCount:(NSUInteger)fakeCount; +- (instancetype)initWithCount:(NSUInteger)fakeCount; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/FakeWithCount.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/FakeWithCount.m new file mode 100644 index 0000000000..1373681eb0 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/FakeWithCount.m @@ -0,0 +1,27 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "FakeWithCount.h" + + +@implementation FakeWithCount + ++ (instancetype)fakeWithCount:(NSUInteger)fakeCount +{ + return [[self alloc] initWithCount:fakeCount]; +} + +- (instancetype)initWithCount:(NSUInteger)fakeCount +{ + self = [super init]; + if (self) + _count = fakeCount; + return self; +} + +- (NSString *)description +{ + return @"FakeWithCount"; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/FakeWithoutCount.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/FakeWithoutCount.h new file mode 100644 index 0000000000..88042c2c9a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/FakeWithoutCount.h @@ -0,0 +1,15 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +@import Foundation; + + +NS_ASSUME_NONNULL_BEGIN + +@interface FakeWithoutCount : NSObject + ++ (instancetype)fake; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/FakeWithoutCount.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/FakeWithoutCount.m new file mode 100644 index 0000000000..93827eb02c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/FakeWithoutCount.m @@ -0,0 +1,19 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "FakeWithoutCount.h" + + +@implementation FakeWithoutCount + ++ (instancetype)fake +{ + return [[self alloc] init]; +} + +- (NSString *)description +{ + return @"FakeWithoutCount"; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCEveryTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCEveryTests.m new file mode 100644 index 0000000000..e113b53406 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCEveryTests.m @@ -0,0 +1,75 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import +#import + +#import "MatcherTestCase.h" +#import "Mismatchable.h" + + +@interface EveryItemTests : MatcherTestCase +@end + +@implementation EveryItemTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = everyItem(equalTo(@"irrelevant")); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_doesNotMatch_emptyCollection +{ + id matcher = everyItem(equalTo(@"irrelevant")); + + assertMismatchDescription(@"was empty", matcher, @[]); +} + +- (void)test_reportAllElementsThatDoNotMatch +{ + id matcher = everyItem([Mismatchable mismatchable:@"a"]); + + assertMismatchDescription(@"mismatches were: [mismatched: b, mismatched: c]", matcher, (@[@"b", @"a", @"c"])); +} + +- (void)test_doesNotMatch_nonCollection +{ + id matcher = everyItem(equalTo(@"irrelevant")); + + assertMismatchDescription(@"was non-collection nil", matcher, nil); +} + +- (void)test_matches_singletonCollection +{ + assertMatches(@"singleton collection", everyItem(equalTo(@1)), [NSSet setWithObject:@1]); +} + +- (void)test_matches_allItemsWithOneMatcher +{ + assertMatches(@"one matcher", everyItem(lessThan(@4)), (@[@1, @2, @3])); +} + +- (void)test_doesNotMatch_collectionWithMismatchingItem +{ + assertDoesNotMatch(@"4 is not less than 4", everyItem(lessThan(@4)), (@[@2, @3, @4])); +} + +- (void)test_matcherCreation_requiresNonNilArgument +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertThrows(everyItem(nil), @"Should require non-nil argument"); +#pragma clang diagnostic pop +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"every item is a value less than <4>", everyItem(lessThan(@4))); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCHasCountTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCHasCountTests.m new file mode 100644 index 0000000000..afb3d40b1b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCHasCountTests.m @@ -0,0 +1,121 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import +#import + +#import "MatcherTestCase.h" +#import "FakeWithCount.h" +#import "FakeWithoutCount.h" + + +@interface HasCountTests : MatcherTestCase +@end + +@implementation HasCountTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = hasCount(equalTo(@42)); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_convertsCountToNSNumberAndPassesToNestedMatcher +{ + FakeWithCount *fakeWithCount = [FakeWithCount fakeWithCount:5]; + + assertMatches(@"same number", hasCount(equalTo(@5)), fakeWithCount); + assertDoesNotMatch(@"different number", hasCount(equalTo(@6)), fakeWithCount); +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"a collection with count of a value greater than <5>", + hasCount(greaterThan(@(5)))); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + assertNoMismatchDescription(hasCountOf(2), ([NSSet setWithObjects:@1, @2, nil])); +} + +- (void)test_mismatchDescription_forItemWithWrongCount +{ + assertMismatchDescription(@"was count of <42> with ", + hasCount(equalTo(@1)), [FakeWithCount fakeWithCount:42]); +} + +- (void)test_mismatchDescription_forItemWithoutCount +{ + assertMismatchDescription(@"was ", + hasCount(equalTo(@1)), [FakeWithoutCount fake]); +} + +- (void)test_describesMismatch_forItemWithWrongCount +{ + assertDescribeMismatch(@"was count of <42> with ", + hasCount(equalTo(@1)), [FakeWithCount fakeWithCount:42]); +} + +- (void)test_describesMismatch_forItemWithoutCount +{ + assertDescribeMismatch(@"was ", + hasCount(equalTo(@1)), [FakeWithoutCount fake]); +} + +@end + + +@interface HasCountOfTests : MatcherTestCase +@end + +@implementation HasCountOfTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = hasCountOf(42); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_hasCountOf_isShortcutForEqualToUnsignedInteger +{ + FakeWithCount *fakeWithCount = [FakeWithCount fakeWithCount:5]; + + assertMatches(@"same number", hasCountOf(5), fakeWithCount); + assertDoesNotMatch(@"different number", hasCountOf(6), fakeWithCount); +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"a collection with count of <5>", hasCountOf(5)); +} + +- (void)test_mismatchDescription_forItemWithWrongCount +{ + assertMismatchDescription(@"was count of <42> with ", + hasCountOf(1), [FakeWithCount fakeWithCount:42]); +} + +- (void)test_mismatchDescription_forItemWithoutCount +{ + assertMismatchDescription(@"was ", hasCountOf(1), [FakeWithoutCount fake]); +} + +- (void)test_describesMismatch_forItemWithWrongCount +{ + assertDescribeMismatch(@"was count of <42> with ", + hasCountOf(1), [FakeWithCount fakeWithCount:42]); +} + +- (void)test_describesMismatch_forItemWithoutCount +{ + assertDescribeMismatch(@"was ", hasCountOf(1), [FakeWithoutCount fake]); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsCollectionContainingInAnyOrderTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsCollectionContainingInAnyOrderTests.m new file mode 100644 index 0000000000..b501576fdb --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsCollectionContainingInAnyOrderTests.m @@ -0,0 +1,112 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import + +#import "MatcherTestCase.h" + + +@interface ContainsInAnyOrderTests : MatcherTestCase +@end + +@implementation ContainsInAnyOrderTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = containsInAnyOrder(equalTo(@"irrelevant"), nil); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_doesNotMatch_nonCollection +{ + id matcher = containsInAnyOrder(equalTo(@"irrelevant"), nil); + + assertDoesNotMatch(@"Non collection", matcher, [[NSObject alloc] init]); +} + +- (void)test_matches_singleItemCollection +{ + assertMatches(@"single item", (containsInAnyOrder(equalTo(@1), nil)), @[@1]); +} + +- (void)test_doesNotMatch_empty +{ + id matcher = containsInAnyOrder(equalTo(@1), equalTo(@2), nil); + + assertMismatchDescription(@"no item matches: <1>, <2> in []", matcher, @[]); +} + +- (void)test_matches_collectionOutOfOrder +{ + id matcher = containsInAnyOrder(equalTo(@1), equalTo(@2), nil); + + assertMatches(@"Out of order", matcher, (@[@2, @1])); +} + +- (void)test_matches_collectionInOfOrder +{ + id matcher = containsInAnyOrder(equalTo(@1), equalTo(@2), nil); + + assertMatches(@"In order", matcher, (@[@1, @2])); +} + +- (void)test_providesConvenientShortcutForMatchingWithEqualTo +{ + id matcher = containsInAnyOrder(@1, @2, nil); + + assertMatches(@"Values automatically wrapped with equalTo", matcher, (@[@2, @1])); +} + +- (void)test_arrayVariant_providesConvenientShortcutForMatchingWithEqualTo +{ + id matcher = containsInAnyOrderIn(@[@1, @2]); + + assertMatches(@"Values automatically wrapped with equalTo", matcher, (@[@2, @1])); +} + +- (void)test_doesNotMatch_nil +{ + id matcher = containsInAnyOrder(@1, nil); + + assertMismatchDescription(@"was non-collection nil", matcher, nil); +} + +- (void)test_doesNotMatch_ifOneOfMultipleItemsMismatch +{ + id matcher = containsInAnyOrder(@1, @2, @3, nil); + + assertMismatchDescription(@"not matched: <4>", matcher, (@[@1, @2, @4])); +} + +- (void)test_doesNotMatch_ifThereAreMoreElementsThanMatchers +{ + id matcher = containsInAnyOrder(@1, @3, nil); + + assertMismatchDescription(@"not matched: <2>", matcher, (@[@1, @2, @3])); +} + +- (void)test_doesNotMatch_ifThereAreMoreMatchersThanElements +{ + id matcher = containsInAnyOrder(@1, @2, @3, @4, nil); + + assertMismatchDescription(@"no item matches: <4> in [<1>, <2>, <3>]", matcher, (@[@1, @2, @3])); +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"a collection over [<1>, <2>] in any order", + containsInAnyOrder(@1, @2, nil)); +} + +- (void)test_describeMismatch +{ + assertDescribeMismatch(@"not matched: <3>", + (containsInAnyOrder(@1, @2, nil)), + (@[@1, @3])); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsCollectionContainingInOrderTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsCollectionContainingInOrderTests.m new file mode 100644 index 0000000000..e5972f6d09 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsCollectionContainingInOrderTests.m @@ -0,0 +1,120 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import + +#import "MatcherTestCase.h" + + +@interface ContainsTests : MatcherTestCase +@end + +@implementation ContainsTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = contains(equalTo(@"irrelevant"), nil); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_doesNotMatch_nonCollection +{ + id matcher = contains(equalTo(@"irrelevant"), nil); + + assertDoesNotMatch(@"Non collection", matcher, [[NSObject alloc] init]); +} + +- (void)test_matches_singleItemCollection +{ + id matcher = contains(equalTo(@1), nil); + + assertMatches(@"Single item collection", matcher, @[@1]); +} + +- (void)test_matches_multipleItemCollection +{ + id matcher = contains(equalTo(@1), equalTo(@2), equalTo(@3), nil); + + assertMatches(@"Multiple item sequence", matcher, (@[@1, @2, @3])); +} + +- (void)test_providesConvenientShortcutForMatchingWithEqualTo +{ + id matcher = contains(@1, @2, @3, nil); + + assertMatches(@"Values automatically wrapped with equalTo", matcher, (@[@1, @2, @3])); +} + +- (void)test_arrayVariant_providesConvenientShortcutForMatchingWithEqualTo +{ + id matcher = containsIn(@[@1, @2, @3]); + + assertMatches(@"Values automatically wrapped with equalTo", matcher, (@[@1, @2, @3])); +} + +- (void)test_doesNotMatch_withMoreElementsThanExpected +{ + id matcher = contains(@1, @2, @3, nil); + + assertMismatchDescription(@"exceeded count of 3 with item <999>", matcher, (@[@1, @2, @3, @999])); +} + +- (void)test_doesNotMatch_withFewerElementsThanExpected +{ + id matcher = contains(@1, @2, @3, nil); + + assertMismatchDescription(@"no item was <3>", matcher, (@[@1, @2])); +} + +- (void)test_doesNotMatch_ifSingleItemMismatches +{ + id matcher = contains(@4, nil); + + assertMismatchDescription(@"item 0: was <3>", matcher, @[@3]); +} + +- (void)test_doesNotMatch_ifOneOfMultipleItemsMismatch +{ + id matcher = contains(@1, @2, @3, nil); + + assertMismatchDescription(@"item 2: was <4>", matcher, (@[@1, @2, @4])); +} + +- (void)test_doesNotMatch_nil +{ + assertDoesNotMatch(@"Should not match nil", contains(@1, nil), nil); +} + +- (void)test_doesNotMatch_emptyCollection +{ + assertMismatchDescription(@"no item was <4>", (contains(@4, nil)), @[]); +} + +- (void)test_doesNotMatch_objectWithoutEnumerator +{ + assertDoesNotMatch(@"should not match object without enumerator", + contains(@1, nil), [[NSObject alloc] init]); +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"a collection containing [<1>, <2>]", contains(@1, @2, nil)); +} + +- (void)test_describeMismatch +{ + assertDescribeMismatch(@"item 1: was <3>", + (contains(@1, @2, nil)), + (@[@1, @3])); +} + +- (void)test_describeMismatch_ofNonCollection +{ + assertDescribeMismatch(@"was non-collection nil", (contains(@1, @2, nil)), nil); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsCollectionContainingInRelativeOrderTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsCollectionContainingInRelativeOrderTests.m new file mode 100644 index 0000000000..52cebb5bda --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsCollectionContainingInRelativeOrderTests.m @@ -0,0 +1,148 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import + +#import "MatcherTestCase.h" + + +@interface ContainsInRelativeOrderTests : MatcherTestCase +@end + +@implementation ContainsInRelativeOrderTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = containsInRelativeOrder(@[equalTo(@"irrelevant")]); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_doesNotMatch_nonCollection +{ + id matcher = containsInRelativeOrder(@[equalTo(@"irrelevant")]); + + assertDoesNotMatch(@"Non collection", matcher, [[NSObject alloc] init]); +} + +- (void)test_matches_singleItemCollection +{ + id matcher = containsInRelativeOrder(@[equalTo(@1)]); + + assertMatches(@"Single item collection", matcher, @[@1]); +} + +- (void)test_matches_multipleItemCollection +{ + id matcher = containsInRelativeOrder(@[equalTo(@1), equalTo(@2), equalTo(@3)]); + + assertMatches(@"Multiple item sequence", matcher, (@[@1, @2, @3])); +} + +- (void)test_providesConvenientShortcutForMatchingWithEqualTo +{ + id matcher = containsInRelativeOrder(@[@1, @2, @3]); + + assertMatches(@"Values automatically wrapped with equalTo", matcher, (@[@1, @2, @3])); +} + +- (void)test_matches_withMoreElementsThanExpectedAtBeginning +{ + id matcher = containsInRelativeOrder(@[@2, @3, @4]); + + assertMatches(@"More elements at beginning", matcher, (@[@1, @2, @3, @4])); +} + +- (void)test_matches_withMoreElementsThanExpectedAtEnd +{ + id matcher = containsInRelativeOrder(@[@1, @2, @3]); + + assertMatches(@"More elements at end", matcher, (@[@1, @2, @3, @4])); +} + +- (void)test_matches_withMoreElementsThanExpectedInBetween +{ + id matcher = containsInRelativeOrder(@[@1, @3]); + + assertMatches(@"More elements in between", matcher, (@[@1, @2, @3])); +} + +- (void)test_matches_subsection +{ + id matcher = containsInRelativeOrder(@[@2, @3]); + + assertMatches(@"Subsection of collection", matcher, (@[@1, @2, @3, @4])); +} + +- (void)test_matches_withSingleGapAndNotFirstOrLast +{ + id matcher = containsInRelativeOrder(@[@2, @4]); + + assertMatches(@"Subsection with single gaps without a first or last match", matcher, (@[@1, @2, @3, @4, @5])); +} + +- (void)test_matches_subsectionWithManyGaps +{ + id matcher = containsInRelativeOrder(@[@2, @4, @6]); + + assertMatches(@"Subsection with many gaps collection", matcher, (@[@1, @2, @3, @4, @5, @6, @7])); +} + +- (void)test_doesNotMatch_withFewerElementsThanExpected +{ + id matcher = containsInRelativeOrder(@[@1, @2, @3]); + + assertMismatchDescription(@"<3> was not found after <2>", matcher, (@[@1, @2])); +} + +- (void)test_doesNotMatch_ifSingleItemNotFound +{ + id matcher = containsInRelativeOrder(@[@4]); + + assertMismatchDescription(@"<4> was not found", matcher, (@[@3])); +} + +- (void)test_doesNotMatch_ifOneOfMultipleItemsNotFound +{ + id matcher = containsInRelativeOrder(@[@1, @2, @3]); + + assertMismatchDescription(@"<3> was not found after <2>", matcher, (@[@1, @2, @4])); +} + +- (void)test_doesNotMatch_nil +{ + assertDoesNotMatch(@"Should not match nil", containsInRelativeOrder(@[@1]), nil); +} + +- (void)test_doesNotMatch_emptyCollection +{ + assertMismatchDescription(@"<4> was not found", containsInRelativeOrder(@[@4]), @[]); +} + +- (void)test_doesNotMatch_objectWithoutEnumerator +{ + assertDoesNotMatch(@"should not match object without enumerator", + containsInRelativeOrder(@[@1]), [[NSObject alloc] init]); +} + +- (void)test_matcherCreation_requiresNonEmptyArgument +{ + XCTAssertThrows(containsInRelativeOrder(@[]), @"Should require non-empty array"); +} + +- (void)test_hasReadableDescription +{ + id matcher = containsInRelativeOrder(@[@1, @2]); + + assertDescription(@"a collection containing [<1>, <2>] in relative order", matcher); +} + +- (void)test_describeMismatch_ofNonCollection +{ + assertDescribeMismatch(@"was non-collection nil", (containsInRelativeOrder(@[@1])), nil); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsCollectionContainingTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsCollectionContainingTests.m new file mode 100644 index 0000000000..1df1770939 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsCollectionContainingTests.m @@ -0,0 +1,115 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import + +#import "MatcherTestCase.h" +#import "Mismatchable.h" + + +@interface HasItemTests : MatcherTestCase +@end + +@implementation HasItemTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = hasItem(equalTo(@"irrelevant")); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_aCollectionThatContainsAnElementForTheGivenMatcher +{ + id matcher = hasItem(equalTo(@1)); + + assertMatches(@"list containing 1", matcher, (@[@1, @2, @3])); +} + +- (void)test_doesNotMatch_collectionWithoutAnElementForGivenMatcher +{ + id matcher = hasItem([Mismatchable mismatchable:@"a"]); + + assertMismatchDescription(@"mismatches were: [mismatched: b, mismatched: c]", matcher, (@[@"b", @"c"])); + assertMismatchDescription(@"was empty", matcher, @[]); +} + +- (void)test_doesNotMatch_nil +{ + assertDoesNotMatch(@"doesn't match nil", hasItem(equalTo(@1)), nil); +} + +- (void)test_providesConvenientShortcutForMatchingWithEqualTo +{ + assertMatches(@"list contains '1'", hasItem(@1), ([NSSet setWithObjects:@1, @2, @3, nil])); + assertDoesNotMatch(@"list without '1'", hasItem(@1), ([NSSet setWithObjects:@2, @3, nil])); +} + +- (void)test_doesNotMatch_nonCollection +{ + assertMismatchDescription(@"was non-collection nil", hasItem(equalTo(@1)), nil); +} + +- (void)test_matcherCreation_requiresNonNilArgument +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertThrows(hasItem(nil), @"Should require non-nil argument"); +#pragma clang diagnostic pop +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"a collection containing <1>", hasItem(@1)); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + assertNoMismatchDescription(hasItem(@1), ([NSSet setWithObjects:@1, @2, nil])); +} + +- (void)test_describeMismatch +{ + assertDescribeMismatch(@"was non-collection \"bad\"", hasItem(@1), @"bad"); +} + +- (void)test_matches_multipleItemsInCollection +{ + id matcher1 = hasItems(equalTo(@1), equalTo(@2), equalTo(@3), nil); + assertMatches(@"list containing all items", matcher1, (@[@1, @2, @3])); + + id matcher2 = hasItems(@1, @2, @3, nil); + assertMatches(@"list containing all items (without matchers)", matcher2, (@[@1, @2, @3])); + + id matcher3 = hasItems(equalTo(@1), equalTo(@2), equalTo(@3), nil); + assertMatches(@"list containing all items in any order", matcher3, (@[@3, @2, @1])); + + id matcher4 = hasItems(equalTo(@1), equalTo(@2), equalTo(@3), nil); + assertMatches(@"list containing all items plus others", matcher4, (@[@5, @3, @2, @1, @4])); + + id matcher5 = hasItems(equalTo(@1), equalTo(@2), equalTo(@3), nil); + assertDoesNotMatch(@"not match list unless it contains all items", matcher5, (@[@5, @3, @2, @4])); // '1' missing +} + +- (void)test_hasItems_providesConvenientShortcutForMatchingWIthEqualTo +{ + assertMatches(@"list containing all items", hasItems(@1, @2, @3, nil), (@[ @1, @2, @3 ])); +} + +- (void)test_arrayVariant_providesConvenientShortcutForMatchingWIthEqualTo +{ + assertMatches(@"list containing all items", hasItemsIn(@[@1, @2, @3]), (@[ @1, @2, @3 ])); +} + +- (void)test_reportsMismatchWithAReadableDescriptionForMultipleItems +{ + id matcher = hasItems(@3, @4, nil); + + assertMismatchDescription(@"instead of a collection containing <4>, mismatches were: [was <1>, was <2>, was <3>]", + matcher, (@[@1, @2, @3])); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsCollectionOnlyContainingTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsCollectionOnlyContainingTests.m new file mode 100644 index 0000000000..b0fe8805fd --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsCollectionOnlyContainingTests.m @@ -0,0 +1,103 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import +#import + +#import "MatcherTestCase.h" +#import "Mismatchable.h" + + +@interface OnlyContainsTests : MatcherTestCase +@end + +@implementation OnlyContainsTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = onlyContains(equalTo(@"irrelevant"), nil); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_doesNotMatch_emptyCollection +{ + id matcher = onlyContains(equalTo(@"irrelevant"), nil); + + assertMismatchDescription(@"was empty", matcher, @[]); +} + +- (void)test_reportAllElementsThatDoNotMatch +{ + id matcher = onlyContains([Mismatchable mismatchable:@"a"], nil); + + assertMismatchDescription(@"mismatches were: [was \"b\", was \"c\"]", matcher, (@[@"b", @"a", @"c"])); +} + +- (void)test_doesNotMatch_nonCollection +{ + id matcher = onlyContains(equalTo(@"irrelevant"), nil); + + assertMismatchDescription(@"was non-collection nil", matcher, nil); +} + +- (void)test_matches_singletonCollection +{ + assertMatches(@"singleton collection", + onlyContains(equalTo(@1), nil), + [NSSet setWithObject:@1]); +} + +- (void)test_matches_allItemsWithOneMatcher +{ + assertMatches(@"one matcher", + onlyContains(lessThan(@4), nil), + (@[@1, @2, @3])); +} + +- (void)test_matches_allItemsWithMultipleMatchers +{ + assertMatches(@"multiple matcher", + onlyContains(lessThan(@4), equalTo(@"hi"), nil), + (@[@1, @"hi", @2, @3])); +} + +- (void)test_providesConvenientShortcutForMatchingWithEqualTo +{ + assertMatches(@"Values automatically wrapped with equal_to", + onlyContains(lessThan(@4), @"hi", nil), + (@[@1, @"hi", @2, @3])); +} + +- (void)test_arrayVariant_providesConvenientShortcutForMatchingWithEqualTo +{ + assertMatches(@"Values automatically wrapped with equal_to", + onlyContainsIn(@[lessThan(@4), @"hi"]), + (@[@1, @"hi", @2, @3])); +} + +- (void)test_doesNotMatch_collectionWithMismatchingItem +{ + assertDoesNotMatch(@"4 is not less than 4", + onlyContains(lessThan(@4), nil), + (@[@2, @3, @4])); +} + +- (void)test_matcherCreation_requiresNonNilArgument +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertThrows(onlyContains(nil), @"Should require non-nil list"); +#pragma clang diagnostic pop +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"a collection containing items matching (<1> or <2>)", + onlyContains(@1, @2, nil)); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsDictionaryContainingEntriesTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsDictionaryContainingEntriesTests.m new file mode 100644 index 0000000000..b6410ec855 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsDictionaryContainingEntriesTests.m @@ -0,0 +1,141 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import + +#import "MatcherTestCase.h" + + +@interface HasEntriesTests : MatcherTestCase +@end + +@implementation HasEntriesTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = hasEntries(@"irrelevant", @"irrelevant", nil); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matcherCreation_requiresEvenNumberOfArgs +{ + XCTAssertThrows(hasEntries(@"a", nil), @"Should require pairs of arguments"); +} + +- (void)test_doesNotMatch_nonDictionary +{ + id object = [[NSObject alloc] init]; + assertDoesNotMatch(@"not dictionary", hasEntries(@"a", equalTo(@1), nil), object); +} + +- (void)test_matches_dictionaryContainingSingleKeyWithMatchingValue +{ + NSDictionary *dict = @{@"a": @1, + @"b": @2}; + + assertMatches(@"has a:1", hasEntries(@"a", equalTo(@1), nil), dict); + assertMatches(@"has b:2", hasEntries(@"b", equalTo(@2), nil), dict); + assertDoesNotMatch(@"no b:3", hasEntries(@"b", equalTo(@3), nil), dict); + assertDoesNotMatch(@"no c:2", hasEntries(@"c", equalTo(@2), nil), dict); +} + +- (void)test_matches_dictionaryContainingMultipleKeysWithMatchingValues +{ + NSDictionary *dict = @{@"a": @1, + @"b": @2, + @"c": @3}; + + assertMatches(@"has a & b", hasEntries(@"a", equalTo(@1), @"b", equalTo(@2), nil), dict); + assertMatches(@"has c & a", hasEntries(@"c", equalTo(@3), @"a", equalTo(@1), nil), dict); + assertDoesNotMatch(@"no d:3", hasEntries(@"d", equalTo(@3), nil), dict); +} + +- (void)test_providesConvenientShortcutForMatchingWithEqualTo +{ + NSDictionary *dict = @{@"a": @1, + @"b": @2, + @"c": @3}; + + assertMatches(@"has a & b", hasEntries(@"a", @1, @"b", @2, nil), dict); + assertMatches(@"has c & a", hasEntries(@"c", @3, @"a", @1, nil), dict); + assertDoesNotMatch(@"no d:3", hasEntries(@"d", @3, nil), dict); +} + +- (void)test_dictionaryVariant_providesConvenientShortcutForMatchingWithEqualTo +{ + NSDictionary *dict = @{@"a": @1, + @"b": @2, + @"c": @3}; + + assertMatches(@"has a & b", hasEntriesIn(@{@"a": @1, @"b": @2}), dict); + assertMatches(@"has c & a", hasEntriesIn(@{@"c": @3, @"a": @1}), dict); + assertDoesNotMatch(@"no d:3", hasEntriesIn(@{@"d": @3}), dict); +} + +- (void)test_doesNotMatch_nil +{ + assertDoesNotMatch(@"nil", hasEntries(@"a", @1, nil), nil); +} + +- (void)test_matcherCreation_requiresNonNilArguments +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertThrows(hasEntries(nil, @"value", nil), @"Should require non-nil argument"); + XCTAssertThrows(hasEntries(@"key", nil, nil), @"Should require non-nil argument"); +#pragma clang diagnostic pop +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"a dictionary containing { \"a\" = <1>; \"b\" = <2>; }", + hasEntries(@"a", @1, @"b", @2, nil)); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + NSDictionary *dict = @{@"a": @1}; + assertNoMismatchDescription(hasEntries(@"a", @1, nil), dict); +} + +- (void)test_mismatchDescription_ofNonDictionary_showsActualArgument +{ + assertMismatchDescription(@"was non-dictionary \"bad\"", hasEntries(@"a", @1, nil), @"bad"); +} + +- (void)test_mismatchDescription_ofDictionaryWithoutKey +{ + NSDictionary *dict = @{@"a": @1, @"c": @3}; + assertMismatchDescription(@"no \"b\" key in <{\n a = 1;\n c = 3;\n}>", + hasEntries(@"a", @1, @"b", @2, nil), dict); +} + +- (void)test_mismatchDescription_ofDictionaryWithNonMatchingValue +{ + NSDictionary *dict = @{@"a": @2}; + assertMismatchDescription(@"value for \"a\" was <2>", hasEntries(@"a", @1, nil), dict); +} + +- (void)test_describeMismatch_ofNonDictionaryShowsActualArgument +{ + assertDescribeMismatch(@"was non-dictionary \"bad\"", hasEntries(@"a", @1, nil), @"bad"); +} + +- (void)test_describeMismatch_ofDictionaryWithoutKey +{ + NSDictionary *dict = @{@"a": @1, @"c": @3}; + assertDescribeMismatch(@"no \"b\" key in <{\n a = 1;\n c = 3;\n}>", + hasEntries(@"a", @1, @"b", @2, nil), dict); +} + +- (void)test_describeMismatch_ofDictionaryWithNonMatchingValue +{ + NSDictionary *dict = @{@"a": @2}; + assertDescribeMismatch(@"value for \"a\" was <2>", hasEntries(@"a", @1, nil), dict); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsDictionaryContainingKeyTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsDictionaryContainingKeyTests.m new file mode 100644 index 0000000000..54018e763f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsDictionaryContainingKeyTests.m @@ -0,0 +1,93 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import + +#import "MatcherTestCase.h" + + +@interface HasKeyTests : MatcherTestCase +@end + +@implementation HasKeyTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = hasKey(@"irrelevant"); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_singletonDictionaryContainingKey +{ + NSDictionary *dict = @{@"a": @1}; + + assertMatches(@"Matches single key", hasKey(equalTo(@"a")), dict); +} + +- (void)test_matches_dictionaryContainingKey +{ + NSDictionary *dict = @{@"a": @1, + @"b": @2, + @"c": @3}; + + assertMatches(@"Matches a", hasKey(equalTo(@"a")), dict); + assertMatches(@"Matches c", hasKey(equalTo(@"c")), dict); +} + +- (void)test_providesConvenientShortcutForMatchingWithEqualTo +{ + NSDictionary *dict = @{@"a": @1, + @"b": @2, + @"c": @3}; + + assertMatches(@"Matches c", hasKey(@"c"), dict); +} + +- (void)test_doesNotMatch_emptyDictionary +{ + assertDoesNotMatch(@"empty", hasKey(@"Foo"), @{}); +} + +- (void)test_doesNotMatch_dictionaryMissingKey +{ + NSDictionary *dict = @{@"a": @1, + @"b": @2, + @"c": @3}; + + assertDoesNotMatch(@"no matching key", hasKey(@"d"), dict); +} + +- (void)test_matcherCreation_requiresNonNilArgument +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertThrows(hasKey(nil), @"Should require non-nil argument"); +#pragma clang diagnostic pop +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"a dictionary containing key \"a\"", hasKey(@"a")); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + NSDictionary *dict = @{@"a": @1}; + assertNoMismatchDescription(hasKey(@"a"), dict); +} + +- (void)test_mismatchDescription_showsActualArgument +{ + assertMismatchDescription(@"was \"bad\"", hasKey(@"a"), @"bad"); +} + +- (void)test_describeMismatch +{ + assertDescribeMismatch(@"was \"bad\"", hasKey(@"a"), @"bad"); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsDictionaryContainingTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsDictionaryContainingTests.m new file mode 100644 index 0000000000..9eaa9426ae --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsDictionaryContainingTests.m @@ -0,0 +1,80 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import +#import + +#import "MatcherTestCase.h" + + +@interface HasEntryTests : MatcherTestCase +@end + +@implementation HasEntryTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = hasEntry(@"irrelevant", @"irrelevant"); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_dictionaryContainingMatchingKeyAndValue +{ + NSDictionary *dict = @{@"a": @1, + @"b": @2}; + + assertMatches(@"has a:1", hasEntry(equalTo(@"a"), equalTo(@1)), dict); + assertMatches(@"has b:2", hasEntry(equalTo(@"b"), equalTo(@2)), dict); + assertDoesNotMatch(@"no c:3", hasEntry(equalTo(@"c"), equalTo(@3)), dict); +} + +- (void)test_providesConvenientShortcutForMatchingWithEqualTo +{ + NSDictionary *dict = @{@"a": @1, + @"b": @2}; + + assertMatches(@"has a:1", hasEntry(@"a", equalTo(@1)), dict); + assertMatches(@"has b:2", hasEntry(equalTo(@"b"), @2), dict); + assertDoesNotMatch(@"no c:3", hasEntry(@"c", @3), dict); +} + +- (void)test_doesNotMatch_nil +{ + assertDoesNotMatch(@"nil", hasEntry(anything(), anything()), nil); +} + +- (void)test_matcherCreation_requiresNonNilArguments +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertThrows(hasEntry(nil, @"value"), @"Should require non-nil argument"); + XCTAssertThrows(hasEntry(@"key", nil), @"Should require non-nil argument"); +#pragma clang diagnostic pop +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"a dictionary containing { \"a\" = <1>; }", hasEntry(@"a", @1)); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + NSDictionary *dict = @{@"a": @1}; + assertNoMismatchDescription(hasEntry(@"a", @1), dict); +} + +- (void)test_mismatchDescription_showsActualArgument +{ + assertMismatchDescription(@"was \"bad\"", hasEntry(@"a", @1), @"bad"); +} + +- (void)test_describeMismatch +{ + assertDescribeMismatch(@"was \"bad\"", hasEntry(@"a", @1), @"bad"); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsDictionaryContainingValueTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsDictionaryContainingValueTests.m new file mode 100644 index 0000000000..b004bd4c64 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsDictionaryContainingValueTests.m @@ -0,0 +1,93 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import + +#import "MatcherTestCase.h" + + +@interface HasValueTests : MatcherTestCase +@end + +@implementation HasValueTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = hasValue(@"irrelevant"); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_singletonDictionaryContainingValue +{ + NSDictionary *dict = @{@"a": @1}; + + assertMatches(@"same single value", hasValue(equalTo(@1)), dict); +} + +- (void)test_matches_dictionaryContainingValue +{ + NSDictionary *dict = @{@"a": @1, + @"b": @2, + @"c": @3}; + + assertMatches(@"Matches 1", hasValue(equalTo(@1)), dict); + assertMatches(@"Matches 3", hasValue(equalTo(@3)), dict); +} + +- (void)test_providesConvenientShortcutForMatchingWithEqualTo +{ + NSDictionary *dict = @{@"a": @1, + @"b": @2, + @"c": @3}; + + assertMatches(@"Matches 3", hasValue(@3), dict); +} + +- (void)test_doesNotMatch_emptyDictionary +{ + assertDoesNotMatch(@"Empty dictionary", hasValue(@"Foo"), @{}); +} + +- (void)test_doesNotMatch_dictionaryMissingValue +{ + NSDictionary *dict = @{@"a": @1, + @"b": @2, + @"c": @3}; + + assertDoesNotMatch(@"no matching value", hasValue(@4), dict); +} + +- (void)test_matcherCreation_requiresNonNilArgument +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertThrows(hasValue(nil), @"Should require non-nil argument"); +#pragma clang diagnostic pop +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"a dictionary containing value <1>", hasValue(@1)); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + NSDictionary *dict = @{@"a": @1}; + assertNoMismatchDescription(hasValue(@1), dict); +} + +- (void)test_mismatchDescription_showsActualArgument +{ + assertMismatchDescription(@"was \"bad\"", hasValue(@1), @"bad"); +} + +- (void)test_describeMismatch +{ + assertDescribeMismatch(@"was \"bad\"", hasValue(@1), @"bad"); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsEmptyCollectionTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsEmptyCollectionTests.m new file mode 100644 index 0000000000..cf6b5c32bb --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsEmptyCollectionTests.m @@ -0,0 +1,60 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import "MatcherTestCase.h" +#import "FakeWithCount.h" +#import "FakeWithoutCount.h" + + +@interface IsEmptyTests : MatcherTestCase +@end + + +@implementation IsEmptyTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = isEmpty(); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_doesNotMatch_nonCollection +{ + assertDoesNotMatch(@"Non collection", isEmpty(), [[NSObject alloc] init]); +} + +- (void)test_matches_emptyCollection +{ + assertMatches(@"empty", isEmpty(), [FakeWithCount fakeWithCount:0]); +} + +- (void)test_doesNotMatchesNonEmptyCollection +{ + assertDoesNotMatch(@"non-empty", isEmpty(), [FakeWithCount fakeWithCount:1]); +} + +- (void)test_doesNotMatch_itemWithoutCount +{ + assertDoesNotMatch(@"no count", isEmpty(), [FakeWithoutCount fake]); +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"empty collection", isEmpty()); +} + +- (void)test_mismatchDescription_showsActualArgument +{ + assertMismatchDescription(@"was \"bad\"", isEmpty(), @"bad"); +} + +- (void)test_describesMismatch +{ + assertDescribeMismatch(@"was ", isEmpty(), [FakeWithCount fakeWithCount:1]); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsInTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsInTests.m new file mode 100644 index 0000000000..8f69dbc3ac --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/HCIsInTests.m @@ -0,0 +1,65 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import "MatcherTestCase.h" + + +@interface IsInTests : MatcherTestCase +@end + +@implementation IsInTests + + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = isIn(@[@1, @2, @3]); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_ifArgumentIsInCollection +{ + id matcher = isIn(@[@1, @2, @3]); + + assertMatches(@"has 1", matcher, @1); + assertMatches(@"has 2", matcher, @2); + assertMatches(@"has 3", matcher, @3); + assertDoesNotMatch(@"no 4", matcher, @4); +} + +- (void)test_matcherCreation_requiresObjectWithContainsObjectMethod +{ + id object = [[NSObject alloc] init]; + + XCTAssertThrows(isIn(object), @"object does not have -containsObject: method"); +} + +- (void)test_matcherCreation_requiresNonNilArgument +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertThrows(isIn(nil), @"Should require non-nil argument"); +#pragma clang diagnostic pop +} + +- (void)test_hasReadableDescription +{ + id matcher = isIn(@[@1, @2, @3]); + + assertDescription(@"one of {<1>, <2>, <3>}", matcher); +} + +- (void)test_mismatchDescription_showsActualArgument +{ + assertMismatchDescription(@"was \"bad\"", isIn(@[@1]), @"bad"); +} + +- (void)test_describesMismatch +{ + assertDescribeMismatch(@"was \"bad\"", isIn(@[@1]), @"bad"); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/Mismatchable.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/Mismatchable.h new file mode 100644 index 0000000000..6e9d31951a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/Mismatchable.h @@ -0,0 +1,15 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +@NS_ASSUME_NONNULL_BEGIN + +interface Mismatchable : HCBaseMatcher + ++ (instancetype)mismatchable:(NSString *)string; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/Mismatchable.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/Mismatchable.m new file mode 100644 index 0000000000..a41dc20731 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Collection/Mismatchable.m @@ -0,0 +1,41 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "Mismatchable.h" + + +@interface Mismatchable () +@property (nonatomic, copy, readonly) NSString *string; +@end + +@implementation Mismatchable + ++ (instancetype)mismatchable:(NSString *)string +{ + return [[self alloc] initMismatchableString:string]; +} + +- (instancetype)initMismatchableString:(NSString *)string +{ + self = [super init]; + if (self) + _string = [string copy]; + return self; +} + +- (BOOL)matches:(nullable id)item +{ + return [self.string isEqualToString:item]; +} + +- (void)describeMismatchOf:(nullable id)item to:(nullable id )mismatchDescription +{ + [[mismatchDescription appendText:@"mismatched: "] appendText:item]; +} + +- (void)describeTo:(id )description +{ + [[description appendText:@"mismatchable: "] appendText:self.string]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Core/AssertWithTimeoutTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Core/AssertWithTimeoutTests.m new file mode 100644 index 0000000000..86157ccf94 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Core/AssertWithTimeoutTests.m @@ -0,0 +1,116 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt +// Contribution by Sergio Padrino + + +#import + +#import + +#import "InterceptingTestCase.h" + +#import + +static NSTimeInterval const TIME_ERROR_MARGIN = 0.1f; + + +static NSTimeInterval machTimeInSeconds(void) +{ + static mach_timebase_info_data_t sTimebaseInfo; + uint64_t machTime = mach_absolute_time(); + + if (sTimebaseInfo.denom == 0) { + (void) mach_timebase_info(&sTimebaseInfo); + } + + NSTimeInterval ratio = (NSTimeInterval)sTimebaseInfo.numer / sTimebaseInfo.denom; + return ratio * machTime / NSEC_PER_SEC; +} + + +@interface AssertWithTimeoutTests : InterceptingTestCase +@end + + +@implementation AssertWithTimeoutTests + +- (void)test_shouldBeSilentOnSuccessfulMatch_withTimeoutZero +{ + assertWithTimeout(0, thatEventually(@"foo"), equalTo(@"foo")); + + XCTAssertNil(self.testFailure); +} + +- (void)test_shouldBeSilentOnSuccessfulMatch_withTimeoutGreaterThanZero +{ + assertWithTimeout(5, thatEventually(@"foo"), equalTo(@"foo")); + + XCTAssertNil(self.testFailure); +} + +- (void)test_failsImmediately_withTimeoutZero +{ + NSTimeInterval maxTime = 0; + NSTimeInterval waitTime = [self timeExecutingBlock:^{ + assertWithTimeout(maxTime, thatEventually(@"foo"), equalTo(@"bar")); + }]; + + XCTAssertEqualWithAccuracy(waitTime, maxTime, TIME_ERROR_MARGIN, + @"Assert should have failed immediately"); +} + +- (void)test_fails_afterTimeoutGreaterThanZero +{ + NSTimeInterval maxTime = 0.2; + NSTimeInterval waitTime = [self timeExecutingBlock:^{ + assertWithTimeout(maxTime, thatEventually(@"foo"), equalTo(@"bar")); + }]; + + XCTAssertEqualWithAccuracy(waitTime, maxTime, TIME_ERROR_MARGIN, + @"Assert should have failed after %f seconds", maxTime); +} + +- (void)test_assertWithTimeoutGreaterThanZero_shouldSucceedNotImmediatelyButBeforeTimeout +{ + NSTimeInterval maxTime = 1.0; + NSTimeInterval succeedTime = 0.2; + __block NSString *futureBar = @"foo"; + dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(succeedTime * NSEC_PER_SEC)); + dispatch_after(popTime, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) { + futureBar = @"bar"; + }); + + NSTimeInterval waitTime = [self timeExecutingBlock:^{ + assertWithTimeout(maxTime, thatEventually(futureBar), equalTo(@"bar")); + }]; + + XCTAssertTrue(waitTime > succeedTime - 0.01, @"Expect assert to terminate after value is changed, but was %lf", waitTime); + XCTAssertTrue(waitTime < maxTime, @"Expect assert to terminate before timeout, but was %lf", waitTime); +} + +- (NSTimeInterval)timeExecutingBlock:(void (^)(void))block +{ + NSTimeInterval start = machTimeInSeconds(); + block(); + return machTimeInSeconds() - start; +} + +- (void)assertThatResultString:(NSString *)resultString containsExpectedString:(NSString *)expectedString +{ + XCTAssertNotNil(resultString); + XCTAssertTrue([resultString rangeOfString:expectedString].location != NSNotFound); +} + +- (void)test_assertionError_shouldDescribeExpectedAndActual +{ + NSString *expected = @"EXPECTED"; + NSString *actual = @"ACTUAL"; + NSString *expectedMessage = @"Expected \"EXPECTED\", but was \"ACTUAL\""; + NSTimeInterval irrelevantMaxTime = 0; + + assertWithTimeout(irrelevantMaxTime, thatEventually(actual), equalTo(expected)); + + [self assertThatResultString:self.testFailure.reason containsExpectedString:expectedMessage]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Core/DiagnosingMatcherTest.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Core/DiagnosingMatcherTest.m new file mode 100644 index 0000000000..878fc4d74f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Core/DiagnosingMatcherTest.m @@ -0,0 +1,35 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import "MatcherTestCase.h" + + +@interface IncompleteDiagnosingMatcher : HCDiagnosingMatcher +@end + +@implementation IncompleteDiagnosingMatcher + +// Let's say we mistakenly implemented -matches: instead of -matches:describingMismatchTo: +- (BOOL)matches:(nullable id)item +{ + return YES; +} + +@end + + +@interface IncompleteDiagnosingMatcherTest : MatcherTestCase +@end + +@implementation IncompleteDiagnosingMatcherTest + +- (void)test_subclassShouldBeRequiredToDefineMatchesDescribingMismatchToMethod +{ + id matcher = [[IncompleteDiagnosingMatcher alloc] init]; + + XCTAssertThrows([matcher matches:nil describingMismatchTo:nil]); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Core/HCBaseMatcherTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Core/HCBaseMatcherTests.m new file mode 100644 index 0000000000..cbed95ba0f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Core/HCBaseMatcherTests.m @@ -0,0 +1,94 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import "MatcherTestCase.h" +#import + + +@interface BaseMatcherWithDescription : HCBaseMatcher +@end + +@implementation BaseMatcherWithDescription + +- (void)describeTo:(id )description +{ + [description appendText:@"SOME DESCRIPTION"]; +} + +@end + + +@interface HCBaseMatcherTests : MatcherTestCase +@end + +@implementation HCBaseMatcherTests +{ + BaseMatcherWithDescription *matcher; +} + +- (void)setUp +{ + [super setUp]; + matcher = [[BaseMatcherWithDescription alloc] init]; +} + +- (void)tearDown +{ + matcher = nil; + [super tearDown]; +} + +- (void)test_description_shouldDescribeMatcher +{ + XCTAssertEqualObjects(matcher.description, @"SOME DESCRIPTION"); +} + +- (void)test_shouldSupportImmutableCopying +{ + BaseMatcherWithDescription *matcherCopy = [matcher copy]; + XCTAssertEqual(matcherCopy, matcher); +} + +@end + + +@interface IncompleteBaseMatcher : HCBaseMatcher +@end + +@implementation IncompleteBaseMatcher +@end + + +@interface IncompleteMatcherTests : MatcherTestCase +@end + +@implementation IncompleteMatcherTests +{ + IncompleteBaseMatcher *matcher; +} + +- (void)setUp +{ + [super setUp]; + matcher = [[IncompleteBaseMatcher alloc] init]; +} + +- (void)tearDown +{ + matcher = nil; + [super tearDown]; +} + +- (void)test_subclassShouldBeRequiredToDefineMatchesMethod +{ + XCTAssertThrows([matcher matches:nil]); +} + +- (void)test_subclassShouldBeRequiredToDefineDescribeToMethod +{ + XCTAssertThrows([matcher describeTo:[[HCStringDescription alloc] init]]); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Core/HCInvocationMatcherTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Core/HCInvocationMatcherTests.m new file mode 100644 index 0000000000..20d5e56e4b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Core/HCInvocationMatcherTests.m @@ -0,0 +1,137 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import + +#import "MatcherTestCase.h" + + +@interface Match : HCIsEqual +@end + +@implementation Match + ++ (instancetype)matches:(id)arg +{ + return [[Match alloc] initEqualTo:arg]; +} + +- (void)describeMismatchOf:(id)item to:(id )description +{ + [description appendText:@"MISMATCH"]; +} + +@end + + +@interface Thingy : NSObject +@end + +@implementation Thingy +{ + NSString *result; +} + ++ (instancetype) thingyWithResult:(NSString *)result +{ + return [[Thingy alloc] initWithResult:result]; +} + +- (instancetype)initWithResult:(NSString *)aResult +{ + self = [super init]; + if (self) + result = aResult; + return self; +} + +- (NSString *)description +{ + return @"Thingy"; +} + +- (NSString *)result +{ + return result; +} + +@end + + +@interface ShouldNotMatch : NSObject +@end + +@implementation ShouldNotMatch + +- (NSString *)description +{ + return @"ShouldNotMatch"; +} + +@end + + +@interface HCInvocationMatcherTests : MatcherTestCase +@end + +@implementation HCInvocationMatcherTests +{ + HCInvocationMatcher *resultMatcher; +} + +- (void)setUp +{ + [super setUp]; + Class aClass = [Thingy class]; + NSMethodSignature *signature = [aClass instanceMethodSignatureForSelector:@selector(result)]; + NSInvocation *invocation = [[[NSInvocation class] class] invocationWithMethodSignature:signature]; + [invocation setSelector:@selector(result)]; + + resultMatcher = [[HCInvocationMatcher alloc] initWithInvocation:invocation + matching:[Match matches:@"bar"]]; +} + +- (void)tearDown +{ + resultMatcher = nil; + [super tearDown]; +} + +- (void)test_matches_feature +{ + assertMatches(@"invoke on Thingy", resultMatcher, [Thingy thingyWithResult:@"bar"]); + assertDescription(@"an object with result \"bar\"", resultMatcher); +} + +- (void)test_mismatch_withDefaultLongDescription +{ + assertMismatchDescription(@" result MISMATCH", resultMatcher, + [Thingy thingyWithResult:@"foo"]); +} + +- (void)test_mismatch_withShortDescription +{ + [resultMatcher setShortMismatchDescription:YES]; + assertMismatchDescription(@"MISMATCH", resultMatcher, + [Thingy thingyWithResult:@"foo"]); +} + +- (void)test_doesNotMatch_nil +{ + assertMismatchDescription(@"was nil", resultMatcher, nil); +} + +- (void)test_doesNotMatch_objectWithoutMethod +{ + assertDoesNotMatch(@"was ", resultMatcher, [[ShouldNotMatch alloc] init]); +} + +- (void)test_objectWithoutMethodShortDescription_isSameAsLongForm +{ + [resultMatcher setShortMismatchDescription:YES]; + assertDoesNotMatch(@"was ", resultMatcher, [[ShouldNotMatch alloc] init]); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Core/HCStringDescriptionTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Core/HCStringDescriptionTests.m new file mode 100644 index 0000000000..0267cd9d53 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Core/HCStringDescriptionTests.m @@ -0,0 +1,218 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import + +@import XCTest; + + +@interface FakeSelfDescribing : NSObject +@end + +@implementation FakeSelfDescribing + +- (void)describeTo:(id )description +{ + [description appendText:@"DESCRIPTION"]; +} + +@end + + +@interface ObjectDescriptionWithLessThan : NSObject +@end + +@implementation ObjectDescriptionWithLessThan + +- (NSString *)description +{ + return @"< is less than"; +} + +@end + + +@interface ObjectWithNilDescription : NSObject +@end + +@implementation ObjectWithNilDescription + +- (NSString *)description +{ + return nil; +} + +@end + + +@interface ProxyObjectSuchAsMock : NSProxy +@property (nonatomic, copy, readonly) NSString *descriptionText; +@end + +@implementation ProxyObjectSuchAsMock + +- (instancetype)initWithDescription:(NSString *)description +{ + _descriptionText = [description copy]; + return self; +} + +- (NSString *)description +{ + return self.descriptionText; +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector +{ + return [[NSObject class] methodSignatureForSelector:aSelector]; +} + +- (void)forwardInvocation:(NSInvocation *)invocation +{ +} + +@end + + +@interface HCStringDescriptionTests : XCTestCase +@end + +@implementation HCStringDescriptionTests +{ + HCStringDescription *description; +} + +- (void)setUp +{ + [super setUp]; + description = [[HCStringDescription alloc] init]; +} + +- (void)tearDown +{ + description = nil; + [super tearDown]; +} + +- (void)test_describesNil +{ + [description appendDescriptionOf:nil]; + + XCTAssertEqualObjects(description.description, @"nil"); +} + +- (void)test_letsSelfDescribingObjectDescribeItself +{ + [description appendDescriptionOf:[[FakeSelfDescribing alloc] init]]; + + XCTAssertEqualObjects(description.description, @"DESCRIPTION"); +} + +- (void)test_describesStringInQuotes +{ + [description appendDescriptionOf:@"FOO"]; + + XCTAssertEqualObjects(description.description, @"\"FOO\""); +} + +- (void)test_descriptionOfStringWithQuotesShouldExpandToCSyntax +{ + [description appendDescriptionOf:@"a\"b"]; + + XCTAssertEqualObjects(description.description, @"\"a\\\"b\""); +} + +- (void)test_descriptionOfStringWithNewlineShouldExpandToCSyntax +{ + [description appendDescriptionOf:@"a\nb"]; + + XCTAssertEqualObjects(description.description, @"\"a\\nb\""); +} + +- (void)test_descriptionOfStringWithCarriageReturnShouldExpandToCSyntax +{ + [description appendDescriptionOf:@"a\rb"]; + + XCTAssertEqualObjects(description.description, @"\"a\\rb\""); +} + +- (void)test_descriptionOfStringWithTabShouldExpandToCSyntax +{ + [description appendDescriptionOf:@"a\tb"]; + + XCTAssertEqualObjects(description.description, @"\"a\\tb\""); +} + +- (void)test_wrapsNonSelfDescribingObjectInAngleBrackets +{ + [description appendDescriptionOf:@42]; + + XCTAssertEqualObjects(description.description, @"<42>"); +} + +- (void)test_shouldNotAddAngleBracketsIfObjectDescriptionAlreadyHasThem +{ + [description appendDescriptionOf:[[NSObject alloc] init]]; + NSPredicate *expected = [NSPredicate predicateWithFormat: + @"SELF MATCHES ''"]; + XCTAssertTrue([expected evaluateWithObject:description.description]); +} + +- (void)test_wrapsNonSelfDescribingObjectInAngleBracketsIfItDoesNotEndInClosingBracket +{ + ObjectDescriptionWithLessThan *lessThanDescription = [[ObjectDescriptionWithLessThan alloc] init]; + [description appendDescriptionOf:lessThanDescription]; + + XCTAssertEqualObjects(description.description, @"<< is less than>"); +} + +- (void)test_canDescribeObjectWithNilDescription +{ + [description appendDescriptionOf:[[ObjectWithNilDescription alloc] init]]; + NSPredicate *expected = [NSPredicate predicateWithFormat: + @"SELF MATCHES ''"]; + XCTAssertTrue([expected evaluateWithObject:description.description]); +} + +- (void)test_appendListWithEmptyListShouldHaveStartAndEndOnly +{ + [description appendList:@[] + start:@"[" + separator:@"," + end:@"]"]; + + XCTAssertEqualObjects(description.description, @"[]"); +} + +- (void)test_appendListWithOneItemShouldHaveStartItemAndEnd +{ + [description appendList:@[@"a"] + start:@"[" + separator:@"," + end:@"]"]; + + XCTAssertEqualObjects(description.description, @"[\"a\"]"); +} + +- (void)test_appendListWithTwoItemsShouldHaveItemsWithSeparator +{ + [description appendList:@[@"a", @"b"] + start:@"[" + separator:@"," + end:@"]"]; + + XCTAssertEqualObjects(description.description, @"[\"a\",\"b\"]"); +} + +- (void)test_ableToDescribeProxyObject +{ + id proxy = [[ProxyObjectSuchAsMock alloc] initWithDescription:@"DESCRIPTION"]; + + [description appendDescriptionOf:proxy]; + + XCTAssertEqualObjects(description.description, @""); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Core/HCTestFailureReporterChainTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Core/HCTestFailureReporterChainTests.m new file mode 100644 index 0000000000..13b43ca702 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Core/HCTestFailureReporterChainTests.m @@ -0,0 +1,58 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import + +@import XCTest; + + +@interface HCTestFailureReporterChainTests : XCTestCase +@end + +@implementation HCTestFailureReporterChainTests + +- (void)tearDown +{ + [HCTestFailureReporterChain reset]; + [super tearDown]; +} + +- (void)test_defaultChain_shouldPointToXCTestHandlerAsHeadOfChain +{ + HCTestFailureReporter *chain = [HCTestFailureReporterChain reporterChain]; + + XCTAssertEqualObjects(NSStringFromClass([chain class]), @"HCXCTestFailureReporter"); + XCTAssertNotNil(chain.successor); +} + +- (void)test_addReporter_shouldSetHeadOfChainToGivenHandler +{ + HCTestFailureReporter *reporter = [[HCTestFailureReporter alloc] init]; + + [HCTestFailureReporterChain addReporter:reporter]; + + XCTAssertEqual([HCTestFailureReporterChain reporterChain], reporter); +} + +- (void)test_addReporter_shouldSetHandlerSuccessorToPreviousHeadOfChain +{ + HCTestFailureReporter *reporter = [[HCTestFailureReporter alloc] init]; + HCTestFailureReporter *oldHead = [HCTestFailureReporterChain reporterChain]; + + [HCTestFailureReporterChain addReporter:reporter]; + + XCTAssertEqual(reporter.successor, oldHead); +} + +- (void)test_addReporter_shouldSetHandlerSuccessorEvenIfHeadOfChainHasNotBeenReferenced +{ + HCTestFailureReporter *reporter = [[HCTestFailureReporter alloc] init]; + + [HCTestFailureReporterChain addReporter:reporter]; + + XCTAssertNotNil(reporter.successor); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Core/HCWrapInMatcherTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Core/HCWrapInMatcherTests.m new file mode 100644 index 0000000000..8e3602fd31 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Core/HCWrapInMatcherTests.m @@ -0,0 +1,19 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +@import XCTest; + + +@interface HCWrapInMatcherTests : XCTestCase +@end + +@implementation HCWrapInMatcherTests + +- (void)test_wrapInMatcher_withNil_shouldReturnNil +{ + XCTAssertNil(HCWrapInMatcher(nil)); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Core/MockSenTestCase.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Core/MockSenTestCase.m new file mode 100644 index 0000000000..1290e9cba5 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Core/MockSenTestCase.m @@ -0,0 +1,295 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import +#import +#import + +@import XCTest; + + +@interface NSException (OCHamcrest_SenTestFailure) + +- (NSString *)filename; +- (NSNumber *)lineNumber; +- (NSString *)description; + ++ (NSException *)failureInFile:(NSString *)filename atLine:(int)lineNumber withDescription:(NSString *)formatString, ...; + +@end + +@implementation NSException (OCHamcrest_SenTestFailure) + +- (NSString *)filename +{ + return self.userInfo[@"filename"]; +} + +- (NSNumber *)lineNumber +{ + return self.userInfo[@"lineNumber"]; +} + +- (NSString *)description +{ + return self.userInfo[@"description"]; +} + ++ (NSException *)failureInFile:(NSString *)filename atLine:(int)lineNumber withDescription:(NSString *)formatString, ... +{ + return [self exceptionWithName:@"OCHamcrest_SenTestFailure" reason:nil userInfo:@{ + @"filename" : filename, + @"lineNumber" : @(lineNumber), + @"description" : [formatString stringByReplacingOccurrencesOfString:@"%%" withString:@"%"] + }]; +} + +@end + + +// Used to swizzle HCXCTestFailureReporter to not handle failure, so it will fall through to HCSenTestFailureReporter. +static BOOL doNotHandleFailure(id self, SEL _cmd, HCTestFailure *failure) +{ + return NO; +} + +@interface MockSenTestCase : XCTestCase +@property (nonatomic, copy) NSString *failureDescription; +@property (nonatomic, copy) NSString *failureFile; +@property (nonatomic, assign) NSUInteger failureLine; +@end + +@implementation MockSenTestCase +{ + Class XCTestFailureReporterClass; + SEL doNotHandleFailureSelector; +} + +- (void)setUp +{ + [super setUp]; + XCTestFailureReporterClass = NSClassFromString(@"HCXCTestFailureReporter"); + doNotHandleFailureSelector = NSSelectorFromString(@"doNotHandleFailure:"); + [self addDoNotHandleFailureMethodToXCTestFailureReporter]; + [self swizzleXCTestFailureReporter]; +} + +- (void)tearDown +{ + [self swizzleXCTestFailureReporter]; + [super tearDown]; +} + +- (void)addDoNotHandleFailureMethodToXCTestFailureReporter +{ + if (![XCTestFailureReporterClass instancesRespondToSelector:doNotHandleFailureSelector]) + { + BOOL success = class_addMethod( + XCTestFailureReporterClass, + doNotHandleFailureSelector, + (IMP)doNotHandleFailure, + "c@:@"); + XCTAssertTrue(success); + } +} + +- (void)swizzleXCTestFailureReporter +{ + Method originalMethod = class_getInstanceMethod(XCTestFailureReporterClass, NSSelectorFromString(@"willHandleFailure:")); + Method swizzledMethod = class_getInstanceMethod(XCTestFailureReporterClass, doNotHandleFailureSelector); + method_exchangeImplementations(originalMethod, swizzledMethod); +} + +- (void)failWithException:(NSException *)exception +{ + XCTAssertEqualObjects(exception.name, @"OCHamcrest_SenTestFailure"); + self.failureDescription = exception.userInfo[@"description"]; + self.failureFile = exception.userInfo[@"filename"]; + self.failureLine = [exception.userInfo[@"lineNumber"] unsignedIntegerValue]; +} + +- (void)testSuccessfulMatch_ShouldBeSilent +{ + assertThat(@"foo", equalTo(@"foo")); + + XCTAssertNil(self.failureDescription); +} + +- (void)testOCUnitAssertionError_ShouldDescribeExpectedAndActual +{ + NSString *expected = @"EXPECTED"; + NSString *actual = @"ACTUAL"; + NSString *expectedMessage = @"Expected \"EXPECTED\", but was \"ACTUAL\""; + + assertThat(actual, equalTo(expected)); + + XCTAssertEqualObjects(self.failureDescription, expectedMessage); +} + +- (void)testOCUnitAssertionError_ShouldCorrectlyDescribeStringsWithPercentSymbols +{ + NSString *expected = @"%s"; + NSString *actual = @"%d"; + NSString *expectedMessage = @"Expected \"%s\", but was \"%d\""; + + assertThat(actual, equalTo(expected)); + + XCTAssertEqualObjects(self.failureDescription, expectedMessage); +} + +@end + + +@interface MockXCTestCase : XCTestCase +@property (nonatomic, assign) BOOL interceptFailure; +@property (nonatomic, assign) NSUInteger failureCount; +@property (nonatomic, copy) NSString *failureDescription; +@property (nonatomic, copy) NSString *failureFile; +@property (nonatomic, assign) NSUInteger failureLine; +@property (nonatomic, assign) BOOL failureExpected; +@end + +@implementation MockXCTestCase + +- (void)recordFailureWithDescription:(NSString *)description + inFile:(NSString *)filePath + atLine:(NSUInteger)lineNumber + expected:(BOOL)expected +{ + if (!self.interceptFailure) + [super recordFailureWithDescription:description inFile:filePath atLine:lineNumber expected:expected]; + else + { + self.failureCount += 1; + self.failureDescription = description; + self.failureFile = filePath; + self.failureLine = lineNumber; + self.failureExpected = expected; + } +} + +- (void)testXCTestCase_WithMatch_ShouldNotRecordFailure +{ + self.interceptFailure = YES; + assertThat(@0, equalTo(@0)); + self.interceptFailure = NO; + + XCTAssertEqual(self.failureCount, 0U); +} + +- (void)testXCTestCase_WithMismatch_ShouldRecordFailure +{ + self.interceptFailure = YES; + assertThat(@1, equalTo(@0)); + self.interceptFailure = NO; + + XCTAssertEqual(self.failureCount, 1U); +} + +- (void)testXCTestCase_WithMismatch_ShouldRecordFailureWithMismatchDescription +{ + NSString *expected = @"EXPECTED"; + NSString *actual = @"ACTUAL"; + NSString *expectedMessage = @"Expected \"EXPECTED\", but was \"ACTUAL\""; + + self.interceptFailure = YES; + assertThat(actual, equalTo(expected)); + self.interceptFailure = NO; + + XCTAssertEqualObjects(expectedMessage, self.failureDescription); +} + +- (void)testXCTestCase_WithMismatch_ShouldRecordFailureWithCurrentFileName +{ + self.interceptFailure = YES; + assertThat(@1, equalTo(@0)); + self.interceptFailure = NO; + + XCTAssertEqualObjects( + [NSString stringWithCString:__FILE__ encoding:NSUTF8StringEncoding], + self.failureFile); +} + +- (void)testXCTestCase_WithMismatch_ShouldRecordFailureWithCurrentLineNumber +{ + self.interceptFailure = YES; + NSUInteger assertLine = __LINE__ + 1; + assertThat(@1, equalTo(@0)); + self.interceptFailure = NO; + + XCTAssertEqual(self.failureLine, assertLine); +} + +- (void)testXCTestCase_WithMismatch_ShouldRecordFailureAsExpectedMeaningAnAssertionFailure +{ + self.interceptFailure = YES; + assertThat(@1, equalTo(@0)); + self.interceptFailure = NO; + + XCTAssertTrue(self.failureExpected); +} + +@end + + +@interface GenericTestCase : NSObject +@end + +@implementation GenericTestCase +@end + +@interface GenericTestCaseTest : XCTestCase +@end + +@implementation GenericTestCaseTest +{ + GenericTestCase *testCase; +} + +- (void)setUp +{ + [super setUp]; + testCase = [[GenericTestCase alloc] init]; +} + +- (void)assertThatResultString:(NSString *)resultString containsExpectedString:(NSString *)expectedString +{ + XCTAssertNotNil(resultString); + XCTAssertTrue([resultString rangeOfString:expectedString].location != NSNotFound); +} + +- (void)testGenericTestCase_ShouldRaiseExceptionWithReasonContainingMismatchDescription +{ + NSString *expected = @"EXPECTED"; + NSString *actual = @"ACTUAL"; + NSString *expectedMessage = @"Expected \"EXPECTED\", but was \"ACTUAL\""; + + @try + { + HC_assertThatWithLocation(testCase, actual, equalTo(expected), "", 0); + } + @catch (NSException* exception) + { + [self assertThatResultString:[exception reason] containsExpectedString:expectedMessage]; + return; + } + XCTFail(@"Expected exception"); +} + +- (void)testGenericTestCase_ShouldRaiseExceptionWithReasonContainingLocation +{ + @try + { + HC_assertThatWithLocation(testCase, @1, equalTo(@0), "FILENAME", 123); + } + @catch (NSException* exception) + { + [self assertThatResultString:[exception reason] containsExpectedString:@"FILENAME:123"]; + return; + } + XCTFail(@"Expected exception"); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Decorator/HCDescribedAsTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Decorator/HCDescribedAsTests.m new file mode 100644 index 0000000000..2c0064d4b4 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Decorator/HCDescribedAsTests.m @@ -0,0 +1,97 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import +#import + +#import "MatcherTestCase.h" + + +@interface DescribedAsTests : MatcherTestCase +@end + +@implementation DescribedAsTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = describedAs(@"irrelevant", anything(), nil); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_overridesDescriptionOfOtherMatcherWithThatPassedToInitializer +{ + id matcher = describedAs(@"my description", anything(), nil); + + assertDescription(@"my description", matcher); +} + +- (void)test_appendsValuesToDescription +{ + id matcher = describedAs(@"value 1 = %0, value 2 = %1", + anything(), + @33, + @97, + nil); + + assertDescription(@"value 1 = <33>, value 2 = <97>", matcher); +} + +- (void)test_handlesSubstitutionAtBeginning +{ + id matcher = describedAs(@"%0ok", + anything(), + @33, + nil); + + assertDescription(@"<33>ok", matcher); +} + +- (void)test_handlesSubstitutionAtEnd +{ + id matcher = describedAs(@"ok%0", + anything(), + @33, + nil); + + assertDescription(@"ok<33>", matcher); +} + +- (void)test_doesNotProcessPercentFollowedByNonDigit +{ + id matcher = describedAs(@"With 33% remaining", anything(), nil); + + assertDescription(@"With 33% remaining", matcher); +} + +- (void)test_delegatesMatchingToNestedMatcher +{ + id matcher = describedAs(@"m1 description", equalTo(@"hi"), nil); + + assertMatches(@"should match", matcher, @"hi"); + assertDoesNotMatch(@"should not match", matcher, @"oi"); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + assertNoMismatchDescription(describedAs(@"irrelevant", anything(), nil), @"hi"); +} + +- (void)test_delegatesMismatchDescriptionToNestedMatcher +{ + id matcher = describedAs(@"irrelevant", equalTo(@2), nil); + + assertMismatchDescription(@"was <1>", matcher, @1); +} + +- (void)test_delegatesDescribeMismatchToNestedMatcher +{ + id matcher = describedAs(@"irrelevant", equalTo(@2), nil); + + assertDescribeMismatch(@"was <1>", matcher, @1); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Decorator/HCIsTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Decorator/HCIsTests.m new file mode 100644 index 0000000000..074c7c587c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Decorator/HCIsTests.m @@ -0,0 +1,66 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import + +#import "MatcherTestCase.h" +#import "NeverMatch.h" + + +@interface IsTests : MatcherTestCase +@end + +@implementation IsTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = is(@"irrelevant"); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_delegatesMatchingToNestedMatcher +{ + assertMatches(@"should match", is(equalTo(@"A")), @"A"); + assertMatches(@"should match", is(equalTo(@"B")), @"B"); + assertDoesNotMatch(@"should not match", is(equalTo(@"A")), @"B"); + assertDoesNotMatch(@"should not match", is(equalTo(@"B")), @"A"); +} + +- (void)test_descriptionShouldPassThrough +{ + assertDescription(@"\"A\"", is(equalTo(@"A"))); +} + +- (void)test_providesConvenientShortcutForIsEqualTo +{ + assertMatches(@"should match", is(@"A"), @"A"); + assertMatches(@"should match", is(@"B"), @"B"); + assertDoesNotMatch(@"should not match", is(@"A"), @"B"); + assertDoesNotMatch(@"should not match", is(@"B"), @"A"); + assertDescription(@"\"A\"", is(@"A")); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + assertNoMismatchDescription(is(@"A"), @"A"); +} + +- (void)test_delegatesMismatchDescriptionToNestedMatcher +{ + assertMismatchDescription([NeverMatch mismatchDescription], + is([NeverMatch neverMatch]), + @"hi"); +} + +- (void)test_delegatesDescribeMismatchToNestedMatcher +{ + assertDescribeMismatch([NeverMatch mismatchDescription], + is([NeverMatch neverMatch]), + @"hi"); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Decorator/NeverMatch.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Decorator/NeverMatch.h new file mode 100644 index 0000000000..490d1b0563 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Decorator/NeverMatch.h @@ -0,0 +1,18 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + + +NS_ASSUME_NONNULL_BEGIN + +@interface NeverMatch : HCBaseMatcher + ++ (id)neverMatch; ++ (NSString *)mismatchDescription; +- (BOOL)matches:(nullable id)item; +- (void)describeMismatchOf:(nullable id)item to:(nullable id )mismatchDescription; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Decorator/NeverMatch.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Decorator/NeverMatch.m new file mode 100644 index 0000000000..9fc6bb2793 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Decorator/NeverMatch.m @@ -0,0 +1,29 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "NeverMatch.h" + + +@implementation NeverMatch + ++ (id)neverMatch +{ + return [[self alloc] init]; +} + ++ (NSString *)mismatchDescription +{ + return @"NEVERMATCH"; +} + +- (BOOL)matches:(nullable id)item +{ + return NO; +} + +- (void)describeMismatchOf:(nullable id)item to:(nullable id )mismatchDescription +{ + [mismatchDescription appendText:[NeverMatch mismatchDescription]]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/InterceptingTestCase.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/InterceptingTestCase.h new file mode 100644 index 0000000000..80e6357dc1 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/InterceptingTestCase.h @@ -0,0 +1,15 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +@import XCTest; + +#import "HCTestFailure.h" // Convenience import + + +NS_ASSUME_NONNULL_BEGIN + +@interface InterceptingTestCase : XCTestCase +@property (nonatomic, strong) HCTestFailure *testFailure; +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/InterceptingTestCase.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/InterceptingTestCase.m new file mode 100644 index 0000000000..5681bfa431 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/InterceptingTestCase.m @@ -0,0 +1,42 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "InterceptingTestCase.h" + +#import "HCTestFailureReporter.h" +#import "HCTestFailureReporterChain.h" + + +@interface InterceptingTestFailureReporter : HCTestFailureReporter +@end + +@implementation InterceptingTestFailureReporter + +- (BOOL)willHandleFailure:(HCTestFailure *)failure +{ + return YES; +} + +- (void)executeHandlingOfFailure:(HCTestFailure *)failure +{ + [failure.testCase setTestFailure:failure]; +} + +@end + + +@implementation InterceptingTestCase + +- (void)setUp +{ + [super setUp]; + [HCTestFailureReporterChain addReporter:[[InterceptingTestFailureReporter alloc] init]]; +} + +- (void)tearDown +{ + [HCTestFailureReporterChain reset]; + [super tearDown]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Logical/HCAllOfTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Logical/HCAllOfTests.m new file mode 100644 index 0000000000..a68ce5f397 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Logical/HCAllOfTests.m @@ -0,0 +1,80 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import +#import +#import + +#import "MatcherTestCase.h" + + +@interface AllOfTests : MatcherTestCase +@end + +@implementation AllOfTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = allOf(equalTo(@"irrelevant"), equalTo(@"irrelevant"), nil); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_evaluatesToTheTheLogicalConjunctionOfTwoOtherMatchers +{ + id matcher = allOf(startsWith(@"goo"), endsWith(@"ood"), nil); + + assertMatches(@"didn't pass both sub-matchers", matcher, @"good"); + assertDoesNotMatch(@"didn't fail first sub-matcher", matcher, @"mood"); + assertDoesNotMatch(@"didn't fail second sub-matcher", matcher, @"goon"); + assertDoesNotMatch(@"didn't fail both sub-matchers", matcher, @"fred"); +} + +- (void)test_evaluatesToTheTheLogicalConjunctionOfManyOtherMatchers +{ + id matcher = allOf(startsWith(@"g"), startsWith(@"go"), endsWith(@"d"), startsWith(@"go"), startsWith(@"goo"), nil); + + assertMatches(@"didn't pass all sub-matchers", matcher, @"good"); + assertDoesNotMatch(@"didn't fail middle sub-matcher", matcher, @"goon"); +} + +- (void)test_providesConvenientShortcutForMatchingWithEqualTo +{ + assertMatches(@"both matchers", allOf(@"good", @"good", nil), @"good"); +} + +- (void)test_arrayVariant_providesConvenientShortcutForMatchingWithEqualTo +{ + assertMatches(@"both matchers", allOfIn(@[@"good", @"good"]), @"good"); +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"(\"good\" and \"bad\" and \"ugly\")", + allOf(equalTo(@"good"), equalTo(@"bad"), equalTo(@"ugly"), nil)); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + assertNoMismatchDescription(allOf(equalTo(@"good"), equalTo(@"good"), nil), + @"good"); +} + +- (void)test_mismatchDescription_describesFirstFailingMatch +{ + assertMismatchDescription(@"instead of \"good\", was \"bad\"", + allOf(equalTo(@"bad"), equalTo(@"good"), nil), + @"bad"); +} + +- (void)test_describeMismatch +{ + assertDescribeMismatch(@"instead of \"good\", was \"bad\"", + allOf(equalTo(@"bad"), equalTo(@"good"), nil), + @"bad"); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Logical/HCAnyOfTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Logical/HCAnyOfTests.m new file mode 100644 index 0000000000..68f24ab590 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Logical/HCAnyOfTests.m @@ -0,0 +1,92 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import +#import +#import + +#import "MatcherTestCase.h" + + +@interface AnyOfTests : MatcherTestCase +@end + +@implementation AnyOfTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = anyOf(equalTo(@"irrelevant"), nil); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_evaluatesToTheTheLogicalDisjunctionOfTwoOtherMatchers +{ + id matcher = anyOf(startsWith(@"goo"), endsWith(@"ood"), nil); + + assertMatches(@"didn't pass both sub-matchers", matcher, @"good"); + assertMatches(@"didn't pass second sub-matcher", matcher, @"mood"); + assertMatches(@"didn't pass first sub-matcher", matcher, @"goon"); + assertDoesNotMatch(@"didn't fail both sub-matchers", matcher, @"flan"); +} + +- (void)test_evaluatesToTheTheLogicalDisjunctionOfManyOtherMatchers +{ + id matcher = anyOf(startsWith(@"g"), startsWith(@"go"), endsWith(@"d"), startsWith(@"go"), startsWith(@"goo"), nil); + + assertMatches(@"didn't pass middle sub-matcher", matcher, @"vlad"); + assertDoesNotMatch(@"didn't fail all sub-matchers", matcher, @"flan"); +} + +- (void)test_providesConvenientShortcutForMatchingWithEqualTo +{ + assertMatches(@"first matcher", anyOf(@"good", @"bad", nil), @"good"); + assertMatches(@"second matcher", anyOf(@"bad", @"good", nil), @"good"); + assertMatches(@"both matchers", anyOf(@"good", @"good", nil), @"good"); +} + +- (void)test_arrayVariant_providesConvenientShortcutForMatchingWithEqualTo +{ + assertMatches(@"first matcher", anyOfIn(@[@"good", @"bad"]), @"good"); + assertMatches(@"second matcher", anyOfIn(@[@"bad", @"good"]), @"good"); + assertMatches(@"both matchers", anyOfIn(@[@"good", @"good"]), @"good"); +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"(\"good\" or \"bad\" or \"ugly\")", + anyOf(equalTo(@"good"), equalTo(@"bad"), equalTo(@"ugly"), nil)); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + assertNoMismatchDescription(anyOf(equalTo(@"good"), equalTo(@"good"), nil), + @"good"); +} + +- (void)test_mismatchDescription_describesFirstFailingMatch +{ + assertMismatchDescription(@"was \"ugly\"", + anyOf(equalTo(@"bad"), equalTo(@"good"), nil), + @"ugly"); +} + +- (void)test_describeMismatch +{ + assertDescribeMismatch(@"was \"ugly\"", + anyOf(equalTo(@"bad"), equalTo(@"good"), nil), + @"ugly"); +} + +- (void)test_matcherCreation_requiresNonNilArgument +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertThrows(anyOf(nil), @"Should require non-nil list"); +#pragma clang diagnostic pop +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Logical/HCIsAnythingTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Logical/HCIsAnythingTests.m new file mode 100644 index 0000000000..5fef8bc586 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Logical/HCIsAnythingTests.m @@ -0,0 +1,45 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import "MatcherTestCase.h" + + +@interface IsAnythingTests : MatcherTestCase +@end + +@implementation IsAnythingTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = anything(); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_alwaysEvaluatesToTrue +{ + assertMatches(@"nil", anything(), nil); + assertMatches(@"object", anything(), [[NSObject alloc] init]); + assertMatches(@"string", anything(), @"hi"); +} + +- (void)test_hasUsefulDefaultDescription +{ + assertDescription(@"ANYTHING", anything()); +} + +- (void)test_canOverrideDescription +{ + NSString *description = @"DESCRIPTION"; + assertDescription(description, anythingWithDescription(description)); +} + +- (void)test_matchAlwaysSucceedsSoShouldNotGenerateMismatchDescription +{ + assertNoMismatchDescription(anything(), @"hi"); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Logical/HCIsNotTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Logical/HCIsNotTests.m new file mode 100644 index 0000000000..32325682cc --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Logical/HCIsNotTests.m @@ -0,0 +1,70 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import +#import + +#import "MatcherTestCase.h" +#import "HCIsInstanceOf.h" + + +@interface IsNotTests : MatcherTestCase +@end + +@implementation IsNotTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = isNot(@"irrelevant"); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_evaluatesToTheTheLogicalNegationOfAnotherMatcher +{ + id matcher = isNot(equalTo(@"A")); + + assertMatches(@"invert mismatch", matcher, @"B"); + assertDoesNotMatch(@"invert match", matcher, @"A"); +} + +- (void)test_providesConvenientShortcutForNotEqualTo +{ + id matcher = isNot(@"A"); + + assertMatches(@"invert mismatch", matcher, @"B"); + assertDoesNotMatch(@"invert match", matcher, @"A"); +} + +- (void)test_usesDescriptionOfNegatedMatcherWithPrefix +{ + assertDescription(@"not an instance of NSString", isNot(instanceOf([NSString class]))); + assertDescription(@"not \"A\"", isNot(@"A")); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + assertNoMismatchDescription(isNot(@"A"), @"B"); +} + +- (void)test_mismatchDescription_showsActualArgument +{ + assertMismatchDescription(@"was \"A\"", isNot(@"A"), @"A"); +} + +- (void)test_mismatchDescription_showsActualSubMatcherDescription +{ + NSArray *item = @[@"A", @"B"]; + NSString *expected = [NSString stringWithFormat:@"was count of <2> with <%@>", item]; + assertMismatchDescription(expected, isNot(hasCountOf(item.count)), item); +} + +- (void)test_describeMismatch +{ + assertDescribeMismatch(@"was \"A\"", isNot(@"A"), @"A"); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/MatcherTestCase.h b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/MatcherTestCase.h new file mode 100644 index 0000000000..a392447e88 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/MatcherTestCase.h @@ -0,0 +1,70 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +@import XCTest; + +@protocol HCMatcher; + + +NS_ASSUME_NONNULL_BEGIN + +@interface MatcherTestCase : XCTestCase + +- (void)assertMatcherSafeWithNil:(id )matcher + inFile:(const char *)fileName atLine:(NSUInteger)lineNumber; + +- (void)assertMatcherSafeWithUnknownType:(id )matcher + inFile:(const char *)fileName atLine:(NSUInteger)lineNumber; + +- (void)assertMatcher:(id )matcher matches:(nullable id)arg message:(NSString *)expectation + inFile:(const char *)fileName atLine:(NSUInteger)lineNumber; + +- (void)assertFalse:(BOOL)condition message:(NSString *)message + inFile:(const char *)fileName atLine:(NSUInteger)lineNumber; + +- (void)assertMatcher:(id )matcher hasDescription:(NSString *)expected + inFile:(const char *)fileName atLine:(NSUInteger)lineNumber; + +- (void)assertMatcher:(id )matcher hasNoMismatchDescriptionFor:(nullable id)arg + inFile:(const char *)fileName atLine:(NSUInteger)lineNumber; + +- (void)assertMatcher:(id )matcher matching:(nullable id)arg yieldsMismatchDescription:(NSString *)expected + inFile:(const char *)fileName atLine:(NSUInteger)lineNumber; + +- (void)assertMatcher:(id )matcher matching:(nullable id)arg + yieldsMismatchDescriptionPrefix:(NSString *)expectedPrefix + inFile:(const char *)fileName atLine:(NSUInteger)lineNumber; + +- (void)assertMatcher:(id )matcher matching:(nullable id)arg describesMismatch:(NSString *)expected + inFile:(const char *)fileName atLine:(NSUInteger)lineNumber; + +@end + +#define assertNilSafe(matcher) \ + [self assertMatcherSafeWithNil:matcher inFile:__FILE__ atLine:__LINE__] + +#define assertUnknownTypeSafe(matcher) \ + [self assertMatcherSafeWithUnknownType:matcher inFile:__FILE__ atLine:__LINE__] + +#define assertMatches(aMessage, matcher, arg) \ + [self assertMatcher:matcher matches:arg message:aMessage inFile:__FILE__ atLine:__LINE__] + +#define assertDoesNotMatch(aMessage, matcher, arg) \ + [self assertFalse:[matcher matches:arg] message:aMessage inFile:__FILE__ atLine:__LINE__] + +#define assertDescription(expected, matcher) \ + [self assertMatcher:matcher hasDescription:expected inFile:__FILE__ atLine:__LINE__] + +#define assertNoMismatchDescription(matcher, arg) \ + [self assertMatcher:matcher hasNoMismatchDescriptionFor:arg inFile:__FILE__ atLine:__LINE__] + +#define assertMismatchDescription(expected, matcher, arg) \ + [self assertMatcher:matcher matching:arg yieldsMismatchDescription:expected inFile:__FILE__ atLine:__LINE__] + +#define assertMismatchDescriptionPrefix(expectedPrefix, matcher, arg) \ + [self assertMatcher:matcher matching:arg yieldsMismatchDescriptionPrefix:expectedPrefix inFile:__FILE__ atLine:__LINE__] + +#define assertDescribeMismatch(expected, matcher, arg) \ + [self assertMatcher:matcher matching:arg describesMismatch:expected inFile:__FILE__ atLine:__LINE__] + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/MatcherTestCase.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/MatcherTestCase.m new file mode 100644 index 0000000000..7ece2005d2 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/MatcherTestCase.m @@ -0,0 +1,165 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "MatcherTestCase.h" + +#import +#import + + +static NSString *mismatchDescription(id matcher, id arg) +{ + HCStringDescription *description = [HCStringDescription stringDescription]; + [matcher describeMismatchOf:arg to:description]; + return description.description; +} + + +@implementation MatcherTestCase + +- (void)failWithMessage:(NSString *)message + inFile:(char const *)fileName atLine:(NSUInteger)lineNumber +{ + [self recordFailureWithDescription:message inFile:@(fileName) atLine:lineNumber expected:YES]; +} + +- (void)failEqualityBetweenObject:(id)left andObject:(id)right withMessage:(NSString *)message + inFile:(char const *)fileName atLine:(NSUInteger)lineNumber +{ + [self recordFailureWithDescription:message inFile:@(fileName) atLine:lineNumber expected:YES]; +} + +- (void)assertMatcherSafeWithNil:(id )matcher + inFile:(const char *)fileName atLine:(NSUInteger)lineNumber +{ + @try + { + [matcher matches:nil]; + } + @catch (NSException *e) + { + [self failWithMessage:@"Matcher was not nil safe" + inFile:fileName atLine:lineNumber]; + } +} + +- (void)assertMatcherSafeWithUnknownType:(id )matcher + inFile:(const char *)fileName atLine:(NSUInteger)lineNumber +{ + @try + { + [matcher matches:[[NSObject alloc] init]]; + } + @catch (NSException *e) + { + [self failWithMessage:@"Matcher was not unknown type safe" + inFile:fileName atLine:lineNumber]; + } +} + +- (void)assertMatcher:(id )matcher matches:(nullable id)arg message:(NSString *)expectation + inFile:(const char *)fileName atLine:(NSUInteger)lineNumber +{ + if (![matcher matches:arg]) + { + NSString *message = [NSString stringWithFormat:@"%@ because '%@'", + expectation, mismatchDescription(matcher, arg)]; + [self failWithMessage:message inFile:fileName atLine:lineNumber]; + } +} + +- (void)assertTrue:(BOOL)condition message:(NSString *)message + inFile:(const char *)fileName atLine:(NSUInteger)lineNumber +{ + if (!condition) + { + [self failWithMessage:message inFile:fileName atLine:lineNumber]; + } +} + +- (void)assertFalse:(BOOL)condition message:(NSString *)message + inFile:(const char *)fileName atLine:(NSUInteger)lineNumber +{ + [self assertTrue:!condition message:message inFile:fileName atLine:lineNumber]; +} + +- (void)assertString:(NSString *)str1 equalsString:(NSString *)str2 message:(NSString *)message + inFile:(const char *)fileName atLine:(NSUInteger)lineNumber +{ + if (![str1 isEqualToString:str2]) + { + [self failEqualityBetweenObject:str1 andObject:str2 withMessage:message + inFile:fileName atLine:lineNumber]; + } +} + +- (void)assertDescription:(HCStringDescription *)description matches:(NSString *)expected + inFile:(const char *)fileName atLine:(NSUInteger)lineNumber +{ + NSString *actual = description.description; + NSString *message = [NSString stringWithFormat:@"Expected description '%@' but got '%@", expected, actual]; + [self assertString:expected equalsString:actual message:message + inFile:fileName atLine:lineNumber]; +} + +- (void)assertMatcher:(id )matcher hasDescription:(NSString *)expected + inFile:(const char *)fileName atLine:(NSUInteger)lineNumber +{ + HCStringDescription *description = [HCStringDescription stringDescription]; + [description appendDescriptionOf:matcher]; + [self assertDescription:description matches:expected inFile:fileName atLine:lineNumber]; +} + +- (void)assertMatcher:(id )matcher hasNoMismatchDescriptionFor:(nullable id)arg + inFile:(const char *)fileName atLine:(NSUInteger)lineNumber +{ + HCStringDescription *description = [HCStringDescription stringDescription]; + [self assertTrue:[matcher matches:arg] message:@"Precondition: Matcher should match item" + inFile:fileName atLine:lineNumber]; + if (description.description.length != 0) + { + [self failWithMessage:@"Expected no mismatch description" + inFile:fileName atLine:lineNumber]; + } +} + +- (void)assertMatcher:(id )matcher matching:(nullable id)arg yieldsMismatchDescription:(NSString *)expected + inFile:(const char *)fileName atLine:(NSUInteger)lineNumber +{ + HCStringDescription *description = [HCStringDescription stringDescription]; + // Make sure matcher has been called before, like assertThat would have done. + [matcher matches:arg]; + [self assertFalse:[matcher matches:arg describingMismatchTo:description] + message:@"Precondition: Matcher should not match item" + inFile:fileName atLine:lineNumber]; + [self assertDescription:description matches:expected inFile:fileName atLine:lineNumber]; +} + +- (void)assertMatcher:(id )matcher matching:(nullable id)arg + yieldsMismatchDescriptionPrefix:(NSString *)expectedPrefix + inFile:(const char *)fileName atLine:(NSUInteger)lineNumber +{ + HCStringDescription *description = [HCStringDescription stringDescription]; + // Make sure matcher has been called before, like assertThat would have done. + [matcher matches:arg]; + [self assertFalse:[matcher matches:arg describingMismatchTo:description] + message:@"Precondition: Matcher should not match item" + inFile:fileName atLine:lineNumber]; + NSString *actual = description.description; + if (![actual hasPrefix:expectedPrefix]) + { + [self failEqualityBetweenObject:actual andObject:expectedPrefix + withMessage:@"Expected mismatch description prefix match" + inFile:fileName atLine:lineNumber]; + } +} + +- (void)assertMatcher:(id )matcher matching:(nullable id)arg describesMismatch:(NSString *)expected + inFile:(const char *)fileName atLine:(NSUInteger)lineNumber +{ + HCStringDescription *description = [HCStringDescription stringDescription]; + [matcher describeMismatchOf:arg to:description]; + [self assertDescription:description matches:expected inFile:fileName atLine:lineNumber]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Number/HCIsCloseToTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Number/HCIsCloseToTests.m new file mode 100644 index 0000000000..5a9bc0386d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Number/HCIsCloseToTests.m @@ -0,0 +1,75 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import "MatcherTestCase.h" + + +@interface CloseToTests : MatcherTestCase +@end + +@implementation CloseToTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + double irrelevant = 0.1; + id matcher = closeTo(irrelevant, irrelevant); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_ifArgumentIsEqualToADoubleValueWithinSomeError +{ + id matcher = closeTo(1.0, 0.5); + + assertMatches(@"equal", matcher, @1.0); + assertMatches(@"less but within delta", matcher, @0.5); + assertMatches(@"greater but within delta", matcher, @1.5); + + assertDoesNotMatch(@"too small", matcher, @0.4); + assertDoesNotMatch(@"too big", matcher, @1.6); +} + +- (void)test_doesNotMatch_nonNumber +{ + id matcher = closeTo(1.0, 0.5); + + assertDoesNotMatch(@"not a number", matcher, @"a"); + assertDoesNotMatch(@"not a number", matcher, nil); +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"a numeric value within <0.5> of <1>", closeTo(1.0, 0.5)); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + assertNoMismatchDescription(closeTo(1.0, 0.5), (@1.0)); +} + +- (void)test_mismatchDescription_showsActualDeltaIfArgumentIsNumeric +{ + assertMismatchDescription(@"<1.7> differed by <0.7>", + (closeTo(1.0, 0.5)), @1.7); +} + +- (void)test_mismatchDescription_showsActualArgumentIfNotNumeric +{ + assertMismatchDescription(@"was \"bad\"", (closeTo(1.0, 0.5)), @"bad"); +} + +- (void)test_describeMismatch_showsActualDeltaIfArgumentIsNumeric +{ + assertDescribeMismatch(@"<1.7> differed by <0.7>", + (closeTo(1.0, 0.5)), @1.7); +} + +- (void)test_describeMismatch_showsActualArgumentIfNotNumeric +{ + assertDescribeMismatch(@"was \"bad\"", (closeTo(1.0, 0.5)), @"bad"); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Number/HCIsEqualToNumberTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Number/HCIsEqualToNumberTests.m new file mode 100644 index 0000000000..a26d015c2e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Number/HCIsEqualToNumberTests.m @@ -0,0 +1,398 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import "MatcherTestCase.h" + + +@interface EqualToCharTests : MatcherTestCase +@end + +@implementation EqualToCharTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + char irrelevant = 0; + id matcher = equalToChar(irrelevant); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_equalNSNumber +{ + assertMatches(@"Large char", equalToChar(CHAR_MAX), [NSNumber numberWithChar:CHAR_MAX]); + assertMatches(@"Small char", equalToChar(CHAR_MIN), [NSNumber numberWithChar:CHAR_MIN]); +} + +- (void)test_doesNotMatch_differentNumber +{ + assertDoesNotMatch(@"Different", equalToChar(CHAR_MAX), [NSNumber numberWithChar:CHAR_MIN]); +} + +@end + + +@interface EqualToDoubleTests : MatcherTestCase +@end + +@implementation EqualToDoubleTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + double irrelevant = 0; + id matcher = equalToDouble(irrelevant); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_equalNSNumber +{ + assertMatches(@"Large double", equalToDouble(DBL_MAX), [NSNumber numberWithDouble:DBL_MAX]); + assertMatches(@"Small double", equalToDouble(DBL_MIN), [NSNumber numberWithDouble:DBL_MIN]); +} + +- (void)test_doesNotMatch_differentNumber +{ + assertDoesNotMatch(@"Different", equalToDouble(DBL_MAX), [NSNumber numberWithDouble:DBL_MIN]); +} + +@end + + +@interface EqualToFloatTests : MatcherTestCase +@end + +@implementation EqualToFloatTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + float irrelevant = 0; + id matcher = equalToFloat(irrelevant); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_equalNSNumber +{ + assertMatches(@"Large float", equalToFloat(FLT_MAX), [NSNumber numberWithFloat:FLT_MAX]); + assertMatches(@"Small float", equalToFloat(FLT_MIN), [NSNumber numberWithFloat:FLT_MIN]); +} + +- (void)test_doesNotMatch_differentNumber +{ + assertDoesNotMatch(@"Different", equalToFloat(FLT_MAX), [NSNumber numberWithFloat:FLT_MIN]); +} + +@end + + +@interface EqualToIntTests : MatcherTestCase +@end + +@implementation EqualToIntTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + int irrelevant = 0; + id matcher = equalToInt(irrelevant); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_equalNSNumber +{ + assertMatches(@"Large int", equalToInt(INT_MAX), [NSNumber numberWithInt:INT_MAX]); + assertMatches(@"Small int", equalToInt(INT_MIN), [NSNumber numberWithInt:INT_MIN]); +} + +- (void)test_doesNotMatch_differentNumber +{ + assertDoesNotMatch(@"Different", equalToInt(INT_MAX), [NSNumber numberWithInt:INT_MIN]); +} + +@end + + +@interface EqualToLongTests : MatcherTestCase +@end + +@implementation EqualToLongTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + long irrelevant = 0; + id matcher = equalToLong(irrelevant); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_equalNSNumber +{ + assertMatches(@"Large long", equalToLong(LONG_MAX), [NSNumber numberWithLong:LONG_MAX]); + assertMatches(@"Small long", equalToLong(LONG_MIN), [NSNumber numberWithLong:LONG_MIN]); +} + +- (void)test_doesNotMatch_differentNumber +{ + assertDoesNotMatch(@"Different", equalToLong(LONG_MAX), [NSNumber numberWithLong:LONG_MIN]); +} + +@end + + +@interface EqualToLongLongTests : MatcherTestCase +@end + +@implementation EqualToLongLongTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + long long irrelevant = 0; + id matcher = equalToLongLong(irrelevant); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_equalNSNumber +{ + assertMatches(@"Large long long", equalToLongLong(LLONG_MAX), [NSNumber numberWithLongLong:LLONG_MAX]); + assertMatches(@"Small long long", equalToLongLong(LLONG_MIN), [NSNumber numberWithLongLong:LLONG_MIN]); +} + +- (void)test_doesNotMatch_differentNumber +{ + assertDoesNotMatch(@"Different", equalToLongLong(LLONG_MAX), [NSNumber numberWithLongLong:LLONG_MIN]); +} + +@end + + +@interface EqualToShortTests : MatcherTestCase +@end + +@implementation EqualToShortTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + short irrelevant = 0; + id matcher = equalToShort(irrelevant); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_equalNSNumber +{ + assertMatches(@"Large short", equalToShort(SHRT_MAX), [NSNumber numberWithShort:SHRT_MAX]); + assertMatches(@"Small short", equalToShort(SHRT_MIN), [NSNumber numberWithShort:SHRT_MIN]); +} + +- (void)test_doesNotMatch_differentNumber +{ + assertDoesNotMatch(@"Different", equalToShort(SHRT_MAX), [NSNumber numberWithShort:SHRT_MIN]); +} + +@end + + +@interface EqualToUnsignedCharTests : MatcherTestCase +@end + +@implementation EqualToUnsignedCharTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + unsigned char irrelevant = 0; + id matcher = equalToUnsignedChar(irrelevant); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_equalNSNumber +{ + assertMatches(@"Large unsigned char", equalToUnsignedChar(UCHAR_MAX), [NSNumber numberWithUnsignedChar:UCHAR_MAX]); + assertMatches(@"Small unsigned char", equalToUnsignedChar(0), [NSNumber numberWithUnsignedChar:0]); +} + +- (void)test_doesNotMatch_differentNumber +{ + assertDoesNotMatch(@"Different", equalToUnsignedChar(CHAR_MAX), [NSNumber numberWithUnsignedChar:0]); +} + +@end + + +@interface EqualToUnsignedIntTests : MatcherTestCase +@end + +@implementation EqualToUnsignedIntTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + unsigned int irrelevant = 0; + id matcher = equalToUnsignedInt(irrelevant); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_equalNSNumber +{ + assertMatches(@"Large unsigned int", equalToUnsignedInt(UINT_MAX), [NSNumber numberWithUnsignedInt:UINT_MAX]); + assertMatches(@"Small unsigned int", equalToUnsignedInt(0), [NSNumber numberWithUnsignedInt:0]); +} + +- (void)test_doesNotMatch_differentNumber +{ + assertDoesNotMatch(@"Different", equalToUnsignedInt(INT_MAX), [NSNumber numberWithUnsignedInt:0]); +} + +@end + + +@interface EqualToUnsignedLongTests : MatcherTestCase +@end + +@implementation EqualToUnsignedLongTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + unsigned long irrelevant = 0; + id matcher = equalToUnsignedLong(irrelevant); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_equalNSNumber +{ + assertMatches(@"Large unsigned long", equalToUnsignedLong(ULONG_MAX), [NSNumber numberWithUnsignedLong:ULONG_MAX]); + assertMatches(@"Small unsigned long", equalToUnsignedLong(0), [NSNumber numberWithUnsignedLong:0]); +} + +- (void)test_doesNotMatch_differentNumber +{ + assertDoesNotMatch(@"Different", equalToUnsignedLong(LONG_MAX), [NSNumber numberWithUnsignedLong:0]); +} + +@end + + +@interface EqualToUnsignedLongLongTests : MatcherTestCase +@end + +@implementation EqualToUnsignedLongLongTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + unsigned long long irrelevant = 0; + id matcher = equalToUnsignedLongLong(irrelevant); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_equalNSNumber +{ + assertMatches(@"Large unsigned long long", equalToUnsignedLongLong(ULLONG_MAX), [NSNumber numberWithUnsignedLongLong:ULLONG_MAX]); + assertMatches(@"Small unsigned long long", equalToUnsignedLongLong(0), [NSNumber numberWithUnsignedLongLong:0]); +} + +- (void)test_doesNotMatch_differentNumber +{ + assertDoesNotMatch(@"Different", equalToUnsignedLongLong(ULLONG_MAX), [NSNumber numberWithUnsignedLongLong:0]); +} + +@end + + +@interface EqualToUnsignedShortTests : MatcherTestCase +@end + +@implementation EqualToUnsignedShortTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + unsigned short irrelevant = 0; + id matcher = equalToUnsignedShort(irrelevant); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_equalNSNumber +{ + assertMatches(@"Large unsigned short", equalToUnsignedShort(USHRT_MAX), [NSNumber numberWithUnsignedShort:USHRT_MAX]); + assertMatches(@"Small unsigned short", equalToUnsignedShort(0), [NSNumber numberWithUnsignedShort:0]); +} + +- (void)test_doesNotMatch_differentNumber +{ + assertDoesNotMatch(@"Different", equalToUnsignedShort(USHRT_MAX), [NSNumber numberWithUnsignedShort:0]); +} + +@end + + +@interface EqualToIntegerTests : MatcherTestCase +@end + +@implementation EqualToIntegerTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + NSInteger irrelevant = 0; + id matcher = equalToInteger(irrelevant); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_equalNSNumber +{ + assertMatches(@"Large NSInteger", equalToInteger(INT_MAX), [NSNumber numberWithInteger:INT_MAX]); + assertMatches(@"Small NSInteger", equalToInteger(INT_MIN), [NSNumber numberWithInteger:INT_MIN]); +} + +- (void)test_doesNotMatch_differentNumber +{ + assertDoesNotMatch(@"Different", equalToInteger(INT_MAX), [NSNumber numberWithInteger:INT_MIN]); +} + +@end + + +@interface EqualToUnsignedIntegerTests : MatcherTestCase +@end + +@implementation EqualToUnsignedIntegerTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + NSUInteger irrelevant = 0; + id matcher = equalToUnsignedInteger(irrelevant); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_equalNSNumber +{ + assertMatches(@"Large NSUInteger", equalToUnsignedInteger(UINT_MAX), [NSNumber numberWithUnsignedInteger:UINT_MAX]); + assertMatches(@"Small NSUInteger", equalToUnsignedInteger(0), [NSNumber numberWithUnsignedInteger:0]); +} + +- (void)test_doesNotMatch_differentNumber +{ + assertDoesNotMatch(@"Different", equalToUnsignedInteger(INT_MAX), [NSNumber numberWithUnsignedInteger:0]); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Number/HCIsTrueFalseTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Number/HCIsTrueFalseTests.m new file mode 100644 index 0000000000..8e844ca2d0 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Number/HCIsTrueFalseTests.m @@ -0,0 +1,99 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + + // System under test +#import + +#import "MatcherTestCase.h" + + +@interface IsTrueTests : MatcherTestCase +@end + +@implementation IsTrueTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = isTrue(); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_nonZero +{ + assertMatches(@"boolean YES", isTrue(), @YES); + assertMatches(@"non-zero", isTrue(), @123); +} + +- (void)test_doesNotMatch_zero +{ + assertDoesNotMatch(@"boolean NO", isTrue(), @NO); + assertDoesNotMatch(@"zero is false", isTrue(), @0); +} + +- (void)test_doesNotMatch_nonNumber +{ + assertDoesNotMatch(@"non-number", isTrue(), [[NSObject alloc] init]); +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"true (non-zero)", isTrue()); +} + +- (void)test_describesMismatch_ofDifferentNumber +{ + assertMismatchDescription(@"was <0>", isTrue(), @0); +} + +- (void)test_describesMismatch_ofNonNumber +{ + assertMismatchDescriptionPrefix(@"was ", isFalse(), @123); +} + +- (void)test_describesMismatch_ofNonNumber +{ + assertMismatchDescriptionPrefix(@"was + +#import +#import "HCTestFailure.h" +#import "InterceptingTestCase.h" + + +@interface HCNumberAssertTests : InterceptingTestCase +@end + +@implementation HCNumberAssertTests + +- (void)test_success_withBool +{ + assertThatBool(YES, equalTo([NSNumber numberWithBool:YES])); + + XCTAssertNil(self.testFailure); +} + +- (void)test_failure_withBool +{ + assertThatBool(YES, equalTo([NSNumber numberWithBool:NO])); + + XCTAssertEqualObjects(self.testFailure.reason, @"Expected <0>, but was <1>"); +} + +- (void)test_success_withChar +{ + assertThatChar('A', equalTo(@'A')); + + XCTAssertNil(self.testFailure); +} + +- (void)test_failure_withChar +{ + assertThatChar('B', equalTo(@'A')); + XCTAssertEqualObjects(self.testFailure.reason, @"Expected <65>, but was <66>"); +} + +- (void)test_success_withDouble +{ + assertThatDouble(1.5, equalTo(@1.5)); + + XCTAssertNil(self.testFailure); +} + +- (void)test_failure_withDouble +{ + assertThatDouble(2.5, equalTo(@1.5)); + + XCTAssertEqualObjects(self.testFailure.reason, @"Expected <1.5>, but was <2.5>"); +} + +- (void)test_success_withFloat +{ + assertThatFloat(1.5f, equalTo(@1.5f)); + + XCTAssertNil(self.testFailure); +} + +- (void)test_failure_withFloat +{ + assertThatFloat(2.5f, equalTo(@1.5f)); + + XCTAssertEqualObjects(self.testFailure.reason, @"Expected <1.5>, but was <2.5>"); +} + +- (void)test_success_withInt +{ + assertThatInt(1, equalTo(@1)); + + XCTAssertNil(self.testFailure); +} + +- (void)test_failure_withInt +{ + assertThatInt(2, equalTo(@1)); + + XCTAssertEqualObjects(self.testFailure.reason, @"Expected <1>, but was <2>"); +} + +- (void)test_success_withLong +{ + assertThatLong(1L, equalTo(@1L)); + + XCTAssertNil(self.testFailure); +} + +- (void)test_failure_withLong +{ + assertThatLong(2L, equalTo(@1L)); + + XCTAssertEqualObjects(self.testFailure.reason, @"Expected <1>, but was <2>"); +} + +- (void)test_success_withLongLong +{ + assertThatLongLong(1LL, equalTo(@1LL)); + + XCTAssertNil(self.testFailure); +} + +- (void)test_failure_withLongLong +{ + assertThatLongLong(2LL, equalTo(@1LL)); + + XCTAssertEqualObjects(self.testFailure.reason, @"Expected <1>, but was <2>"); +} + +- (void)test_success_withShort +{ + assertThatShort(1, equalTo([NSNumber numberWithShort:1])); + + XCTAssertNil(self.testFailure); +} + +- (void)test_failure_withShort +{ + assertThatShort(2, equalTo([NSNumber numberWithShort:1])); + + XCTAssertEqualObjects(self.testFailure.reason, @"Expected <1>, but was <2>"); +} + +- (void)test_success_withUnsignedChar +{ + assertThatUnsignedChar('A', equalTo([NSNumber numberWithUnsignedChar:'A'])); + + XCTAssertNil(self.testFailure); +} + +- (void)test_failure_withUnsignedChar +{ + assertThatUnsignedChar('B', equalTo([NSNumber numberWithUnsignedChar:'A'])); + + XCTAssertEqualObjects(self.testFailure.reason, @"Expected <65>, but was <66>"); +} + +- (void)test_success_withUnsignedInt +{ + assertThatUnsignedInt(1U, equalTo(@1U)); + + XCTAssertNil(self.testFailure); +} + +- (void)test_failure_withUnsignedInt +{ + assertThatUnsignedInt(2U, equalTo(@1U)); + + XCTAssertEqualObjects(self.testFailure.reason, @"Expected <1>, but was <2>"); +} + +- (void)test_success_withUnsignedLong +{ + assertThatUnsignedLong(1UL, equalTo(@1UL)); + + XCTAssertNil(self.testFailure); +} + +- (void)test_failure_withUnsignedLong +{ + assertThatUnsignedLong(2UL, equalTo(@1UL)); + + XCTAssertEqualObjects(self.testFailure.reason, @"Expected <1>, but was <2>"); +} + +- (void)test_success_withUnsignedLongLong +{ + assertThatUnsignedLongLong(1ULL, equalTo(@1ULL)); + + XCTAssertNil(self.testFailure); +} + +- (void)test_failure_withUnsignedLongLong +{ + assertThatUnsignedLongLong(2ULL, equalTo(@1ULL)); + + XCTAssertEqualObjects(self.testFailure.reason, @"Expected <1>, but was <2>"); +} + +- (void)test_success_withUnsignedShort +{ + assertThatUnsignedShort(1U, equalTo([NSNumber numberWithUnsignedShort:1U])); + + XCTAssertNil(self.testFailure); +} + +- (void)test_failure_withUnsignedShort +{ + assertThatUnsignedShort(2U, equalTo([NSNumber numberWithUnsignedShort:1U])); + + XCTAssertEqualObjects(self.testFailure.reason, @"Expected <1>, but was <2>"); +} + +- (void)test_success_withInteger +{ + assertThatInteger(1, equalTo(@1)); + + XCTAssertNil(self.testFailure); +} + +- (void)test_failure_withInteger +{ + assertThatInteger(2, equalTo(@1)); + + XCTAssertEqualObjects(self.testFailure.reason, @"Expected <1>, but was <2>"); +} + +- (void)test_success_withUnsignedInteger +{ + assertThatUnsignedInteger(1, equalTo([NSNumber numberWithUnsignedInteger:1])); + + XCTAssertNil(self.testFailure); +} + +- (void)test_failure_withUnsignedInteger +{ + assertThatUnsignedInteger(2, equalTo([NSNumber numberWithUnsignedInteger:1])); + + XCTAssertEqualObjects(self.testFailure.reason, @"Expected <1>, but was <2>"); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Number/HCOrderingComparisonTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Number/HCOrderingComparisonTests.m new file mode 100644 index 0000000000..f4819caea7 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Number/HCOrderingComparisonTests.m @@ -0,0 +1,111 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import "MatcherTestCase.h" + + +@interface HCOrderingComparisonTests : MatcherTestCase +@end + +@implementation HCOrderingComparisonTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = greaterThan(@1); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_comparesObjects_forGreaterThan +{ + assertMatches(@"match", greaterThan(@1), @2); + assertDoesNotMatch(@"no match", greaterThan(@1), @1); +} + +- (void)test_comparesObjects_forLessThan +{ + assertMatches(@"match", lessThan(@1), @0); + assertDoesNotMatch(@"no match", lessThan(@1), @1); +} + +- (void)test_comparesObjects_forGreaterThanOrEqualTo +{ + assertMatches(@"match", greaterThanOrEqualTo(@1), @2); + assertMatches(@"match", greaterThanOrEqualTo(@1), @1); + assertDoesNotMatch(@"no match", greaterThanOrEqualTo(@1), @0); +} + +- (void)test_comparesObjects_forLessThanOrEqualTo +{ + assertMatches(@"match", lessThanOrEqualTo(@1), @0); + assertMatches(@"match", lessThanOrEqualTo(@1), @1); + assertDoesNotMatch(@"no match", lessThanOrEqualTo(@1), @2); +} + +- (void)test_doesNotMatch_nil +{ + assertDoesNotMatch(@"nil argument", greaterThan(@1), nil); +} + +- (void)test_supportsDifferentTypesOfComparableObjects +{ + assertMatches(@"strings", greaterThan(@"bb"), @"cc"); + assertMatches(@"dates", lessThan([NSDate date]), [NSDate distantPast]); +} + +- (void)test_doesNotMatch_objectThatDoesNotCompare +{ + assertDoesNotMatch(@"can't compare", lessThan(@1), [NSDate date]); + assertDoesNotMatch(@"can't compare", greaterThan(@1), [NSDate date]); +} + +- (void)test_matcherCreation_requiresObjectWithCompareMethod +{ + id object = [[NSObject alloc] init]; + XCTAssertThrows(greaterThan(object), @"object does not have -compare: method"); +} + +- (void)test_hasReadableDescription +{ + id one = @1; + + assertDescription(@"a value greater than <1>", greaterThan(one)); + assertDescription(@"a value greater than or equal to <1>", greaterThanOrEqualTo(one)); + assertDescription(@"a value less than <1>", lessThan(one)); + assertDescription(@"a value less than or equal to <1>", lessThanOrEqualTo(one)); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + id one = @1; + + assertNoMismatchDescription(greaterThan(one), @2); + assertNoMismatchDescription(lessThan(one), @0); + assertNoMismatchDescription(greaterThanOrEqualTo(one), @1); + assertNoMismatchDescription(lessThanOrEqualTo(one), @1); +} + +- (void)test_mismatchDescription +{ + id one = @1; + + assertMismatchDescription(@"was <0>", greaterThan(one), @0); + assertMismatchDescription(@"was <2>", lessThan(one), @2); + assertMismatchDescription(@"was <0>", greaterThanOrEqualTo(one), @0); + assertMismatchDescription(@"was <2>", lessThanOrEqualTo(one), @2); +} + +- (void)test_describeMismatch +{ + id one = @1; + + assertDescribeMismatch(@"was <0>", greaterThan(one), @0); + assertDescribeMismatch(@"was <2>", lessThan(one), @2); + assertDescribeMismatch(@"was <0>", greaterThanOrEqualTo(one), @0); + assertDescribeMismatch(@"was <2>", lessThanOrEqualTo(one), @2); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCArgumentCaptorTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCArgumentCaptorTests.m new file mode 100644 index 0000000000..60d51fafa3 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCArgumentCaptorTests.m @@ -0,0 +1,127 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import "HCArgumentCaptor.h" + +#import "MatcherTestCase.h" + + +@interface HCArgumentCaptorTests : MatcherTestCase +@end + +@implementation HCArgumentCaptorTests +{ + HCArgumentCaptor *sut; +} + +- (void)setUp +{ + [super setUp]; + sut = [[HCArgumentCaptor alloc] init]; +} + +- (void)tearDown +{ + sut = nil; + [super tearDown]; +} + +- (void)test_matcher_shouldAlwaysEvaluateToTrue +{ + assertMatches(@"nil", sut, nil); + assertMatches(@"some object", sut, @123); +} + +- (void)test_matcher_shouldHaveReadableDescription +{ + assertDescription(@"", sut); +} + +- (void)test_value_shouldBeLastCapturedValue +{ + [sut matches:@"FOO"]; + [sut matches:@"BAR"]; + + XCTAssertEqualObjects(sut.value, @"BAR"); +} + +- (void)test_value_shouldBeCopyIfItCanBeCopied +{ + NSMutableString *original = [@"FOO" mutableCopy]; + + [sut matches:original]; + + XCTAssertFalse(sut.value == original); +} + +- (void)test_value_shouldBeOriginalIfItCannotBeCopied +{ + id original = [[NSObject alloc] init]; + + [sut matches:original]; + + XCTAssertTrue(sut.value == original); +} + +- (void)test_value_withNothingCaptured_shouldReturnNil +{ + XCTAssertNil(sut.value); +} + +- (void)test_value_givenNil_shouldReturnNSNull +{ + [sut matches:@"FOO"]; + [sut matches:nil]; + + XCTAssertEqualObjects(sut.value, [NSNull null]); +} + +- (void)test_allValues_shouldCaptureValuesInOrder +{ + [sut matches:@"FOO"]; + [sut matches:@"BAR"]; + + XCTAssertEqual(sut.allValues.count, 2U); + XCTAssertEqualObjects(sut.allValues[0], @"FOO"); + XCTAssertEqualObjects(sut.allValues[1], @"BAR"); +} + +- (void)test_allValues_turningOffCaptureEnabled_shouldNotCaptureSubsequentValues +{ + [sut matches:@"FOO"]; + sut.captureEnabled = NO; + [sut matches:@"BAR"]; + [sut matches:@"BAZ"]; + + XCTAssertEqual(sut.allValues.count, 1U); + XCTAssertEqualObjects(sut.allValues[0], @"FOO"); +} + +- (void)test_allValues_turningCaptureEnabledBackOn_shouldCaptureSubsequentValues +{ + sut.captureEnabled = NO; + [sut matches:@"FOO"]; + sut.captureEnabled = YES; + [sut matches:@"BAR"]; + [sut matches:@"BAZ"]; + + XCTAssertEqual(sut.allValues.count, 2U); + XCTAssertEqualObjects(sut.allValues[0], @"BAR"); + XCTAssertEqualObjects(sut.allValues[1], @"BAZ"); +} + +- (void)test_allValues_givenNil_shouldCaptureNSNull +{ + [sut matches:nil]; + + XCTAssertEqualObjects(sut.allValues[0], [NSNull null]); +} + +- (void)test_allValues_shouldReturnImmutableArray +{ + [sut matches:@"FOO"]; + + XCTAssertFalse([sut.allValues respondsToSelector:@selector(addObject:)]); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCConformsToProtocolTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCConformsToProtocolTests.m new file mode 100644 index 0000000000..b5dd298d75 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCConformsToProtocolTests.m @@ -0,0 +1,76 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt +// Contribution by Todd Farrell + +#import + +#import "MatcherTestCase.h" + + +@protocol TestProtocol +@end + +@interface TestClass : NSObject +@end + +@implementation TestClass + ++ (instancetype)test_class +{ + return [[TestClass alloc] init]; +} + +@end + +@interface ConformsToTests : MatcherTestCase +@end + +@implementation ConformsToTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = conformsTo(@protocol(TestProtocol)); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_ifArgumentConformsToASpecificProtocol +{ + TestClass *instance = [TestClass test_class]; + + assertMatches(@"conforms to protocol", conformsTo(@protocol(TestProtocol)), instance); + + assertDoesNotMatch(@"does not conform to protocol", conformsTo(@protocol(TestProtocol)), @"hi"); + assertDoesNotMatch(@"nil", conformsTo(@protocol(TestProtocol)), nil); +} + +- (void)test_matcherCreation_requiresNonNilArgument +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertThrows(conformsTo(nil), @"Should require non-nil argument"); +#pragma clang diagnostic pop +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"an object that conforms to TestProtocol", conformsTo(@protocol(TestProtocol))); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + assertNoMismatchDescription(conformsTo(@protocol(NSObject)), @"hi"); +} + +- (void)test_mismatchDescription_showsActualArgument +{ + assertMismatchDescription(@"was \"bad\"", conformsTo(@protocol(TestProtocol)), @"bad"); +} + +- (void)test_describeMismatch +{ + assertDescribeMismatch(@"was \"bad\"", conformsTo(@protocol(TestProtocol)), @"bad"); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCHasDescriptionTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCHasDescriptionTests.m new file mode 100644 index 0000000000..83e3b1374f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCHasDescriptionTests.m @@ -0,0 +1,75 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import + +#import "MatcherTestCase.h" + + +static NSString *fakeDescription = @"DESCRIPTION"; + +@interface FakeWithDescription : NSObject +@end + +@implementation FakeWithDescription ++ (instancetype)fake { return [[self alloc] init]; } +- (NSString *)description { return fakeDescription; } +@end + + +@interface HasDescriptionTests : MatcherTestCase +@end + +@implementation HasDescriptionTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = hasDescription(equalTo(@"irrelevant")); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_passesResultOfDescriptionToNestedMatcher +{ + FakeWithDescription* fake = [FakeWithDescription fake]; + assertMatches(@"equal", hasDescription(equalTo(fakeDescription)), fake); + assertDoesNotMatch(@"unequal", hasDescription(equalTo(@"foo")), fake); +} + +- (void)test_providesConvenientShortcutForDescriptionEqualTo +{ + FakeWithDescription* fake = [FakeWithDescription fake]; + assertMatches(@"equal", hasDescription(fakeDescription), fake); + assertDoesNotMatch(@"unequal", hasDescription(@"foo"), fake); +} + +- (void)test_mismatchDoesNotRepeatTheDescription +{ + FakeWithDescription* fake = [FakeWithDescription fake]; + assertMismatchDescription(@"was \"DESCRIPTION\"", hasDescription(@"foo"), fake); +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"an object with description \"foo\"", hasDescription(@"foo")); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + assertNoMismatchDescription(hasDescription(@"DESCRIPTION"), [FakeWithDescription fake]); +} + +- (void)test_mismatchDescription_showsActualArgument +{ + assertMismatchDescription(@"was \"bad\"", hasDescription(@"foo"), @"bad"); +} + +- (void)test_describeMismatch +{ + assertDescribeMismatch(@"was \"bad\"", hasDescription(@"foo"), @"bad"); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCHasPropertyTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCHasPropertyTests.m new file mode 100644 index 0000000000..8cdf9e0554 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCHasPropertyTests.m @@ -0,0 +1,322 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt +// Contribution by Justin Shacklette + +#import + +#import +#import + +#import "MatcherTestCase.h" + + +@interface Person : NSObject +@property (nonatomic, copy) NSString *name; +- (NSNumber *)shoeSize; +@end + +@implementation Person +{ + NSNumber *_shoeSize; +} + +- (instancetype)initWithName:(NSString *)name shoeSize:(int)shoeSize +{ + self = [super init]; + if (self) + { + _name = name; + _shoeSize = [[NSNumber alloc] initWithInt:shoeSize]; + } + return self; +} + +- (NSNumber *)shoeSize +{ + return _shoeSize; +} + +- (NSString *)description +{ + return @"Person"; +} + +@end + + +@interface NotAPerson : NSObject +@end + +@implementation NotAPerson + +- (NSString *)description +{ + return @"NotAPerson"; +} + +@end + + +@interface HasPropertyTests : MatcherTestCase +@end + +@implementation HasPropertyTests +{ + Person *joe; + Person *nobody; +} + +- (void)setUp +{ + [super setUp]; + joe = [[Person alloc] initWithName:@"Joe" shoeSize:13]; + nobody = [[Person alloc] initWithName:nil shoeSize:0]; +} + +- (void)tearDown +{ + joe = nil; + nobody = nil; + [super tearDown]; +} + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = hasProperty(@"irrelevant", @"irrelevant"); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_canMatchStringPropertyValues +{ + assertMatches(@"equal string property values", hasProperty(@"name", @"Joe"), joe); + assertDoesNotMatch(@"unequal string property values", hasProperty(@"name", @"Bob"), joe); + assertDoesNotMatch(@"unequal string property values", hasProperty(@"name", nil), joe); +} + +- (void)test_canMatchStringPropertyValuesWithMatchers +{ + assertMatches(@"equal string property values", hasProperty(@"name", equalTo(@"Joe")), joe); + assertDoesNotMatch(@"unequal string property values", hasProperty(@"name", equalTo(@"Bob")), joe); + assertDoesNotMatch(@"unequal string property values", hasProperty(@"name", nilValue()), joe); +} + +- (void)test_canMatchNumberPropertyValues +{ + assertMatches(@"equal int property values", hasProperty(@"shoeSize", equalTo(@13)), joe); + assertDoesNotMatch(@"unequal int property values", hasProperty(@"shoeSize", equalTo(@3)), joe); + assertDoesNotMatch(@"unequal int property values", hasProperty(@"shoeSize", equalTo(@-3)), joe); +} + +- (void)test_nilPropertyValues +{ + assertMatches(@"equal nil property values", hasProperty(@"name", nilValue()), nobody); + assertDoesNotMatch(@"unequal nil property values", hasProperty(@"name", @"Bob"), nobody); +} + +- (void)test_matcherCreation_requiresNonNilPropertyName +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertThrows(hasProperty(nil, nil), @"Should require non-nil property name"); +#pragma clang diagnostic pop +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"an object with name \"Joe\"", hasProperty(@"name", @"Joe")); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + assertNoMismatchDescription(hasProperty(@"name", @"Joe"), joe); +} + +- (void)test_mismatchDescription_onObjectWithoutProperty_shouldSayNoProperty +{ + id matcher = hasProperty(@"name", @"Joe"); + NotAPerson *noProperty = [[NotAPerson alloc] init]; + + assertMismatchDescription(@"no name on ", matcher, noProperty); +} + +- (void)test_mismatchDescription_onObjectWithProperty_shouldShowActualValue +{ + id matcher = hasProperty(@"name", @"Bob"); + + assertMismatchDescription(@"name was \"Joe\" on ", matcher, joe); +} + +- (void)test_describeMismatch +{ + id matcher = hasProperty(@"name", @"Bob"); + + assertDescribeMismatch(@"name was \"Joe\" on ", matcher, joe); +} + +@end + + +@interface ValueHolder : NSObject + +@property (nonatomic, assign) BOOL boolValue; +@property (nonatomic, assign) char charValue; +@property (nonatomic, assign) int intValue; +@property (nonatomic, assign) short shortValue; +@property (nonatomic, assign) long longValue; +@property (nonatomic, assign) long long longLongValue; +@property (nonatomic, assign) unsigned char unsignedCharValue; +@property (nonatomic, assign) unsigned int unsignedIntValue; +@property (nonatomic, assign) unsigned short unsignedShortValue; +@property (nonatomic, assign) unsigned long unsignedLongValue; +@property (nonatomic, assign) unsigned long long unsignedLongLongValue; +@property (nonatomic, assign) float floatValue; +@property (nonatomic, assign) double doubleValue; + +@end + +@implementation ValueHolder +@end + + +@interface HasPropertyPrimitiveTests : MatcherTestCase +@end + +@implementation HasPropertyPrimitiveTests +{ + ValueHolder *foo; +} + +- (void)setUp +{ + [super setUp]; + foo = [[ValueHolder alloc] init]; +} + +- (void)tearDown +{ + foo = nil; + [super tearDown]; +} + +- (void)test_canMatchPrimitiveBoolValues +{ + foo.boolValue = YES; + assertMatches(@"BOOL should match", hasProperty(@"boolValue", equalTo(@YES)), foo); + assertDoesNotMatch(@"BOOL should not match", hasProperty(@"boolValue", equalTo(@NO)), foo); +} + +- (void)test_canMatchPrimitiveCharValues +{ + foo.charValue = 'a'; + assertMatches(@"char should match", hasProperty(@"charValue", equalTo(@'a')), foo); + assertDoesNotMatch(@"char should not match", hasProperty(@"charValue", equalTo(@'b')), foo); +} + +- (void)test_canMatchPrimitiveIntValues +{ + foo.intValue = INT_MIN; + assertMatches(@"int should match", hasProperty(@"intValue", equalTo(@INT_MIN)), foo); + assertDoesNotMatch(@"int should not match", hasProperty(@"intValue", equalTo(@-2)), foo); +} + +- (void)test_canMatchPrimitiveShortValues +{ + foo.shortValue = -2; + assertMatches(@"short should match", hasProperty(@"shortValue", equalTo(@-2)), foo); + assertDoesNotMatch(@"short should not match", hasProperty(@"shortValue", equalTo(@-1)), foo); +} + +- (void)test_canMatchPrimitiveLongValues +{ + foo.longValue = LONG_MIN; + assertMatches(@"long should match", hasProperty(@"longValue", equalTo(@LONG_MIN)), foo); + assertDoesNotMatch(@"long should not match", + hasProperty(@"longValue", equalTo(@(LONG_MIN + 1))), + foo); +} + +- (void)test_canMatchPrimitiveLongLongValues +{ + foo.longLongValue = LLONG_MIN; + assertMatches(@"long long should match", + hasProperty(@"longLongValue", equalTo(@(LLONG_MIN))), + foo); + assertDoesNotMatch(@"long long should not match", + hasProperty(@"longLongValue", equalTo(@(LLONG_MIN + 1))), + foo); +} + +- (void)test_canMatchPrimitiveUnsignedCharValues +{ + foo.unsignedCharValue = 'b'; + assertMatches(@"unsigned char should match", + hasProperty(@"unsignedCharValue", equalTo(@'b')), + foo); + assertDoesNotMatch(@"unsigned char should not match", + hasProperty(@"unsignedCharValue", equalTo(@'c')), + foo); +} + +- (void)test_canMatchPrimitiveUnsignedIntValues +{ + foo.unsignedIntValue = UINT_MAX; + assertMatches(@"unsigned int should match", + hasProperty(@"unsignedIntValue", equalTo(@UINT_MAX)), + foo); + assertDoesNotMatch(@"unsigned int should not match", + hasProperty(@"unsignedIntValue", equalTo(@(UINT_MAX - 1))), + foo); +} + +- (void)test_canMatchPrimitiveUnsignedShortValues +{ + foo.unsignedShortValue = 2; + assertMatches(@"unsigned short should match", + hasProperty(@"unsignedShortValue", equalTo(@2)), + foo); + assertDoesNotMatch(@"unsigned short should not match", + hasProperty(@"unsignedShortValue", equalTo(@3)), + foo); +} + +- (void)test_canMatchPrimitiveUnsignedLongValues +{ + foo.unsignedLongValue = ULONG_MAX; + assertMatches(@"unsigned long should match", + hasProperty(@"unsignedLongValue", equalTo(@ULONG_MAX)), + foo); + assertDoesNotMatch(@"unsigned long should not match", + hasProperty(@"unsignedLongValue", equalTo(@(ULONG_MAX - 1))), + foo); +} + +- (void)test_canMatchPrimitiveUnsignedLongLongValues +{ + foo.unsignedLongLongValue = ULLONG_MAX; + assertMatches(@"unsigned long long should match", + hasProperty(@"unsignedLongLongValue", equalTo(@ULLONG_MAX)), + foo); + assertDoesNotMatch(@"unsigned long long should not match", + hasProperty(@"unsignedLongLongValue", equalTo(@(ULLONG_MAX - 1))), + foo); +} + +- (void)test_canMatchPrimitiveFloatValues +{ + foo.floatValue = 1.2f; + assertMatches(@"float should match", hasProperty(@"floatValue", equalTo(@1.2f)), foo); + assertDoesNotMatch(@"float should not match", hasProperty(@"floatValue", equalTo(@1.3f)), foo); +} + +- (void)test_canMatchPrimitiveDoubleValues +{ + foo.doubleValue = DBL_MAX; + assertMatches(@"double should match", hasProperty(@"doubleValue", equalTo(@DBL_MAX)), foo); + assertDoesNotMatch(@"double should not match", + hasProperty(@"doubleValue", equalTo(@3.14)), + foo); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCIsEqualTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCIsEqualTests.m new file mode 100644 index 0000000000..79b76bcbf4 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCIsEqualTests.m @@ -0,0 +1,99 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import "MatcherTestCase.h" + + +@interface FakeArgument : NSObject +@end + +@implementation FakeArgument +- (NSString *)description { return @"ARGUMENT DESCRIPTION"; } +@end + + +@interface AlwaysEqual : NSObject +@end + +@implementation AlwaysEqual +- (BOOL)isEqual:(id)anObject { return YES; } +@end + + +@interface NeverEqual : NSObject +@end + +@implementation NeverEqual +- (BOOL)isEqual:(id)anObject { return NO; } +@end + + +@interface EqualToTests : MatcherTestCase +@end + +@implementation EqualToTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = equalTo(@"irrelevant"); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_comparesObjectsUsingIsEqualMethod +{ + assertMatches(@"equal strings", equalTo(@"hi"), @"hi"); + assertDoesNotMatch(@"unequal strings", equalTo(@"hi"), @"bye"); +} + +- (void)test_canCompareNilValues +{ + assertMatches(@"nil equals nil", equalTo(nil), nil); + + assertDoesNotMatch(@"nil as argument", equalTo(@"hi"), nil); + assertDoesNotMatch(@"nil in equalTo", equalTo(nil), @"hi"); +} + +- (void)test_honorsIsEqualImplementationEvenWithNilValues +{ + assertMatches(@"always equal", equalTo(nil), [[AlwaysEqual alloc] init]); + assertDoesNotMatch(@"never equal", equalTo(nil), [[NeverEqual alloc] init]); +} + +- (void)test_includesTheResultOfCallingDescriptionOnItsArgumentInTheDescription +{ + assertDescription(@"", equalTo([[FakeArgument alloc] init])); +} + +- (void)test_returnsAnObviousDescriptionIfCreatedWithANestedMatcherByMistake +{ + id innerMatcher = equalTo(@"NestedMatcher"); + assertDescription(([@[@"<", [innerMatcher description], @">"] + componentsJoinedByString:@""]), + equalTo(innerMatcher)); +} + +- (void)test_returnsGoodDescriptionIfCreatedWithNilReference +{ + assertDescription(@"nil", equalTo(nil)); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + assertNoMismatchDescription(equalTo(@"hi"), @"hi"); +} + +- (void)test_mismatchDescription_showsActualArgument +{ + assertMismatchDescription(@"was \"bad\"", equalTo(@"good"), @"bad"); +} + +- (void)test_describeMismatch +{ + assertDescribeMismatch(@"was \"bad\"", equalTo(@"good"), @"bad"); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCIsInstanceOfTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCIsInstanceOfTests.m new file mode 100644 index 0000000000..1ce289f5ef --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCIsInstanceOfTests.m @@ -0,0 +1,80 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import "MatcherTestCase.h" +#import "SomeClassAndSubclass.h" + + +@interface InstanceOfTests : MatcherTestCase +@end + +@implementation InstanceOfTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = instanceOf([SomeClass class]); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_ifArgumentIsInstanceOfGivenClass +{ + SomeClass *obj = [[SomeClass alloc] init]; + assertMatches(@"same class", instanceOf([SomeClass class]), obj); +} + +- (void)test_matches_ifArgumentIsSubclassOfGivenClass +{ + SomeSubclass *sub = [[SomeSubclass alloc] init]; + assertMatches(@"subclass", instanceOf([SomeClass class]), sub); +} + +- (void)test_doesNotMatch_ifArgumentIsInstanceOfDifferentClass +{ + assertDoesNotMatch(@"different class", instanceOf([SomeClass class]), @"hi"); +} + +- (void)test_doesNotMatch_ifArgumentIsNil +{ + assertDoesNotMatch(@"nil", instanceOf([NSNumber class]), nil); +} + +- (void)test_matcherCreation_requiresNonNilArgument +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertThrows(instanceOf(nil), @"Should require non-nil argument"); +#pragma clang diagnostic pop +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"an instance of SomeClass", instanceOf([SomeClass class])); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + assertNoMismatchDescription(instanceOf([SomeClass class]), [[SomeClass alloc] init]); +} + +- (void)test_mismatchDescription_showsClassOfActualArgument +{ + assertMismatchDescription(@"was SomeClass instance ", + instanceOf([NSValue class]), [[SomeClass alloc] init]); +} + +- (void)test_mismatchDescription_handlesNilArgument +{ + assertMismatchDescription(@"was nil", instanceOf([NSValue class]), nil); +} + +- (void)test_describeMismatch +{ + assertDescribeMismatch(@"was SomeClass instance ", + instanceOf([NSValue class]), [[SomeClass alloc] init]); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCIsNilTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCIsNilTests.m new file mode 100644 index 0000000000..06dc1d90a9 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCIsNilTests.m @@ -0,0 +1,103 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import "MatcherTestCase.h" + + +@interface NilValueTests : MatcherTestCase +@end + +@implementation NilValueTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = nilValue(); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_ifArgumentIsNil +{ + assertMatches(@"nil", nilValue(), nil); +} + +- (void)test_doesNotMatch_ifArgumentIsNotNil +{ + id ANY_NON_NULL_ARGUMENT = [[NSObject alloc] init]; + + assertDoesNotMatch(@"not nil", nilValue(), ANY_NON_NULL_ARGUMENT); +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"nil", nilValue()); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + assertNoMismatchDescription(nilValue(), nil); +} + +- (void)test_mismatchDescription_showsActualArgument +{ + assertMismatchDescription(@"was \"bad\"", nilValue(), @"bad"); +} + +- (void)test_describeMismatch +{ + assertDescribeMismatch(@"was \"bad\"", nilValue(), @"bad"); +} + +@end + + +@interface NotNilValueTests : MatcherTestCase +@end + +@implementation NotNilValueTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = notNilValue(); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_ifArgumentIsNotNil +{ + id ANY_NON_NULL_ARGUMENT = [[NSObject alloc] init]; + + assertMatches(@"not nil", notNilValue(), ANY_NON_NULL_ARGUMENT); +} + +- (void)test_doesNotMatch_ifArgumentIsNil +{ + assertDoesNotMatch(@"nil", notNilValue(), nil); +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"not nil", notNilValue()); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + assertNoMismatchDescription(notNilValue(), @"hi"); +} + +- (void)test_mismatchDescription_showsActualArgument +{ + assertMismatchDescription(@"was nil", notNilValue(), nil); +} + +- (void)test_describeMismatch +{ + assertDescribeMismatch(@"was nil", notNilValue(), nil); +} + +@end + diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCIsSameTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCIsSameTests.m new file mode 100644 index 0000000000..6c3d069f3b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCIsSameTests.m @@ -0,0 +1,92 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import +#import +#import + +#import "MatcherTestCase.h" + + +@interface SameInstanceTests : MatcherTestCase +@end + +@implementation SameInstanceTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = sameInstance(@"irrelevant"); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_ifArgumentIsReferenceToSpecifiedObject +{ + id o1 = [[NSObject alloc] init]; + id o2 = [[NSObject alloc] init]; + + assertThat(o1, sameInstance(o1)); + assertThat(o2, isNot(sameInstance(o1))); +} + +- (void)test_doesNotMatch_equalObjects +{ + NSString *string1 = @"foobar"; + NSString *string2 = [@"foo" stringByAppendingString:@"bar"]; + + assertDoesNotMatch(@"not the same object", sameInstance(string1), string2); +} + +- (void)test_descriptionIncludesMemoryAddress +{ + HCStringDescription *description = [HCStringDescription stringDescription]; + NSPredicate *expected = [NSPredicate predicateWithFormat: + @"SELF MATCHES 'same instance as 0x[0-9a-fA-F]+ \"abc\"'"]; + + [description appendDescriptionOf:sameInstance(@"abc")]; + XCTAssertTrue([expected evaluateWithObject:description.description]); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + id o1 = [[NSObject alloc] init]; + assertNoMismatchDescription(sameInstance(o1), o1); +} + +- (void)test_mismatchDescription_showsActualArgumentAddress +{ + id matcher = sameInstance(@"foo"); + HCStringDescription *description = [HCStringDescription stringDescription]; + NSPredicate *expected = [NSPredicate predicateWithFormat: + @"SELF MATCHES 'was 0x[0-9a-fA-F]+ \"hi\"'"]; + + BOOL result = [matcher matches:@"hi" describingMismatchTo:description]; + XCTAssertFalse(result, @"Precondition: Matcher should not match item"); + XCTAssertTrue([expected evaluateWithObject:description.description]); +} + +- (void)test_mismatchDescription_withNilShouldNotIncludeAddress +{ + assertMismatchDescription(@"was nil", sameInstance(@"foo"), nil); +} + +- (void)test_describeMismatch +{ + id matcher = sameInstance(@"foo"); + HCStringDescription *description = [HCStringDescription stringDescription]; + NSPredicate *expected = [NSPredicate predicateWithFormat: + @"SELF MATCHES 'was 0x[0-9a-fA-F]+ \"hi\"'"]; + + [matcher describeMismatchOf:@"hi" to:description]; + XCTAssertTrue([expected evaluateWithObject:description.description]); +} + +- (void)test_describeMismatch_withNilShouldNotIncludeAddress +{ + assertDescribeMismatch(@"was nil", sameInstance(@"foo"), nil); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCIsTypeOfTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCIsTypeOfTests.m new file mode 100644 index 0000000000..084f9113cd --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCIsTypeOfTests.m @@ -0,0 +1,80 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import "MatcherTestCase.h" +#import "SomeClassAndSubclass.h" + + +@interface HCIsTypeOfTests : MatcherTestCase +@end + +@implementation HCIsTypeOfTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = isA([SomeClass class]); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_ifArgumentIsInstanceOfGivenClass +{ + SomeClass *obj = [[SomeClass alloc] init]; + assertMatches(@"same class", isA([SomeClass class]), obj); +} + +- (void)test_doesNotMatch_ifArgumentIsSubclassOfGivenClass +{ + SomeSubclass *sub = [[SomeSubclass alloc] init]; + assertDoesNotMatch(@"subclass", isA([SomeClass class]), sub); +} + +- (void)test_doesNotMatch_ifArgumentIsInstanceOfDifferentClass +{ + assertDoesNotMatch(@"different class", isA([SomeClass class]), @"hi"); +} + +- (void)test_doesNotMatch_ifArgumentIsNil +{ + assertDoesNotMatch(@"nil", isA([NSNumber class]), nil); +} + +- (void)test_matcherCreation_requiresNonNilArgument +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertThrows(isA(nil), @"Should require non-nil argument"); +#pragma clang diagnostic pop +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"an exact instance of SomeClass", isA([SomeClass class])); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + assertNoMismatchDescription(isA([SomeClass class]), [[SomeClass alloc] init]); +} + +- (void)test_mismatchDescription_showsClassOfActualArgument +{ + assertMismatchDescription(@"was SomeSubclass instance ", + isA([SomeClass class]), [[SomeSubclass alloc] init]); +} + +- (void)test_mismatchDescription_handlesNilArgument +{ + assertMismatchDescription(@"was nil", isA([SomeClass class]), nil); +} + +- (void)test_describeMismatch +{ + assertDescribeMismatch(@"was SomeSubclass instance ", + isA([SomeClass class]), [[SomeSubclass alloc] init]); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCThrowsExceptionTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCThrowsExceptionTests.m new file mode 100644 index 0000000000..25066cfb20 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Object/HCThrowsExceptionTests.m @@ -0,0 +1,97 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import +#import +#import + +#import "MatcherTestCase.h" + + +@interface ThrowsExceptionTests : MatcherTestCase +@end + +@implementation ThrowsExceptionTests + +- (void)test_copesWithNilsAndUnknownTypes +{ + id matcher = throwsException(anything()); + + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_doesNotMatch_blockNotThrowingException +{ + id matcher = throwsException(anything()); + + assertDoesNotMatch(@"does not throw", matcher, ^{}); +} + +- (void)test_matches_blockThrowingExceptionSatisfyingMatcher +{ + NSException *exception = [NSException exceptionWithName:@"" reason:@"" userInfo:nil]; + id matcher = throwsException(sameInstance(exception)); + + assertMatches(@"throws matching exception", matcher, ^{ @throw exception; }); +} + +- (void)test_doesNotMatch_blockThrowingExceptionNotSatisfyingMatcher +{ + id matcher = throwsException(hasProperty(@"name", @"FOO")); + + assertDoesNotMatch(@"throws non-matching exception", matcher, + ^{ @throw [NSException exceptionWithName:@"BAR" reason:@"" userInfo:nil]; }); +} + +- (void)test_doesNotMatch_nonBlock +{ + id matcher = throwsException(anything()); + + assertDoesNotMatch(@"not a block", matcher, [[NSObject alloc] init]); +} + +- (void)test_matcherCreation_requiresMatcherArgument +{ + XCTAssertThrows(throwsException([[NSObject alloc] init]), @"Should require matcher argument"); +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"a block with no arguments, throwing an exception which is an object with name \"FOO\"", + throwsException(hasProperty(@"name", @"FOO"))); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + id matcher = throwsException(anything()); + + assertNoMismatchDescription(matcher, + ^{ @throw [NSException exceptionWithName:@"" reason:@"" userInfo:nil]; }); +} + +- (void)test_mismatchDescription_OnNonBlock_shouldSayNeedABlock +{ + id matcher = throwsException(anything()); + + assertMismatchDescription(@"was non-block nil", matcher, nil); +} + +- (void)test_mismatchDescription_OnBlockNotThrowingException_shouldSayNoThrow +{ + id matcher = throwsException(anything()); + + assertMismatchDescription(@"no exception thrown", matcher, ^{}); +} + +- (void)test_mismatchDescription_OnBlockThrowingExceptionNotSatisfyingMatcher +{ + id matcher = throwsException(hasProperty(@"name", @"FOO")); + + assertMismatchDescriptionPrefix(@"exception thrown but name was \"BAR\" on matcher; +} + +- (void)setUp +{ + [super setUp]; + matcher = equalToCompressingWhiteSpace(@" Hello World how\n are we? "); +} + +- (void)tearDown +{ + matcher = nil; + [super tearDown]; +} + +- (void)test_copesWithNilsAndUnknownTypes +{ + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_ifWordsAreSameButWhitespaceDiffers +{ + assertMatches(@"less whitespace", matcher, @"Hello World how are we?"); + assertMatches(@"more whitespace", matcher, @" Hello World how are \n\n\twe?"); +} + +- (void)test_doesNotMatch_ifTextOtherThanWhitespaceDiffers +{ + assertDoesNotMatch(@"wrong word", matcher, @"Hello PLANET how are we?"); + assertDoesNotMatch(@"incomplete", matcher, @"Hello World how are we"); +} + +- (void)test_doesNotMatch_ifWhitespaceIsAddedOrRemovedInMiddleOfWord +{ + assertDoesNotMatch(@"need whitespace between Hello and World", + matcher, @"HelloWorld how are we?"); + assertDoesNotMatch(@"wrong whitespace within World", + matcher, @"Hello Wo rld how are we?"); +} + +- (void)test_matcherCreation_requiresNonNilArgument +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertThrows(equalToCompressingWhiteSpace(nil), @"Should require non-nil argument"); +#pragma clang diagnostic pop +} + +- (void)test_doesNotMatch_ifMatchingAgainstNonString +{ + assertDoesNotMatch(@"non-string", matcher, @3); +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"\" Hello World how\\n are we? \" ignoring whitespace", matcher); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + assertNoMismatchDescription(equalToCompressingWhiteSpace(@"foo\nbar"), @"foo bar"); +} + +- (void)test_mismatchDescription_showsActualArgument +{ + assertMismatchDescription(@"was \"bad\"", matcher, @"bad"); +} + +- (void)test_describeMismatch +{ + assertDescribeMismatch(@"was \"bad\"", matcher, @"bad"); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Text/HCIsEqualIgnoringCaseTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Text/HCIsEqualIgnoringCaseTests.m new file mode 100644 index 0000000000..19544bf7bb --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Text/HCIsEqualIgnoringCaseTests.m @@ -0,0 +1,83 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import "MatcherTestCase.h" + + +@interface IsEqualIgnoringCaseTests : MatcherTestCase +@end + +@implementation IsEqualIgnoringCaseTests +{ + id matcher; +} + +- (void)setUp +{ + [super setUp]; + matcher = equalToIgnoringCase(@"heLLo"); +} + +- (void)tearDown +{ + matcher = nil; + [super tearDown]; +} + +- (void)test_copesWithNilsAndUnknownTypes +{ + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_ignoringCaseOfCharsInString +{ + assertMatches(@"all upper", matcher, @"HELLO"); + assertMatches(@"all lower", matcher, @"hello"); + assertMatches(@"mixed up", matcher, @"HelLo"); + + assertDoesNotMatch(@"no match", matcher, @"bye"); +} + +- (void)test_doesNotMatch_ifAdditionalWhitespaceIsPresent +{ + assertDoesNotMatch(@"whitespace suffix", matcher, @"heLLo "); + assertDoesNotMatch(@"whitespace prefix", matcher, @" heLLo"); +} + +- (void)test_matcherCreation_requiresNonNilArgument +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertThrows(equalToIgnoringCase(nil), @"Should require non-nil argument"); +#pragma clang diagnostic pop +} + +- (void)test_doesNotMatch_ifMatchingAgainstNonString +{ + assertDoesNotMatch(@"non-string", matcher, @3); +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"\"heLLo\" ignoring case", matcher); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + assertNoMismatchDescription(matcher, @"hello"); +} + +- (void)test_mismatchDescription_showsActualArgument +{ + assertMismatchDescription(@"was \"bad\"", matcher, @"bad"); +} + +- (void)test_describeMismatch +{ + assertDescribeMismatch(@"was \"bad\"", matcher, @"bad"); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Text/HCStringContainsInOrderTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Text/HCStringContainsInOrderTests.m new file mode 100644 index 0000000000..e868c286a5 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Text/HCStringContainsInOrderTests.m @@ -0,0 +1,98 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import "MatcherTestCase.h" + + +@interface StringContainsInOrderTests : MatcherTestCase +@end + +@implementation StringContainsInOrderTests +{ + id matcher; +} + +- (void)setUp +{ + [super setUp]; + matcher = stringContainsInOrder(@"string one", @"string two", @"string three", nil); +} + +- (void)tearDown +{ + matcher = nil; + [super tearDown]; +} + +- (void)test_copesWithNilsAndUnknownTypes +{ + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_ifOrderIsCorrect +{ + assertMatches(@"correct order", matcher, @"string one then string two followed by string three"); +} + +- (void)test_arrayVariant_matchesIfOrderIsCorrect +{ + id variantMatcher = stringContainsInOrderIn(@[@"string one", @"string two", @"string three"]); + + assertMatches(@"correct order", variantMatcher, @"string one then string two followed by string three"); +} + +- (void)test_doesNotMatch_ifOrderIsIncorrect +{ + assertDoesNotMatch(@"incorrect order", matcher, @"string two then string one followed by string three"); +} + +- (void)test_doesNotMatch_ifExpectedSubstringsAreMissing +{ + assertDoesNotMatch(@"missing string one", matcher, @"string two then string three"); + assertDoesNotMatch(@"missing string two", matcher, @"string one then string three"); + assertDoesNotMatch(@"missing string three", matcher, @"string one then string two"); +} + +- (void)test_matcherCreation_requiresNonNilArgument +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertThrows(stringContainsInOrder(nil), @"Should require non-nil argument"); +#pragma clang diagnostic pop +} + +- (void)test_matcherCreation_requiresStringArguments +{ + XCTAssertThrows(stringContainsInOrder(@"one", @2, nil), @"Should require strings"); +} + +- (void)test_doesNotMatch_ifMatchingAgainstNonString +{ + assertDoesNotMatch(@"non-string", matcher, @3); +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"a string containing \"string one\", \"string two\", \"string three\" in order", + matcher); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + assertNoMismatchDescription(matcher, @"string one then string two followed by string three"); +} + +- (void)test_mismatchDescription_showsActualArgument +{ + assertMismatchDescription(@"was \"bad\"", matcher, @"bad"); +} + +- (void)test_describeMismatch +{ + assertDescribeMismatch(@"was \"bad\"", matcher, @"bad"); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Text/HCStringContainsTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Text/HCStringContainsTests.m new file mode 100644 index 0000000000..dc2edeec9c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Text/HCStringContainsTests.m @@ -0,0 +1,87 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import "MatcherTestCase.h" + +static NSString *EXCERPT = @"EXCERPT"; + + +@interface ContainsSubstringTests : MatcherTestCase +@end + +@implementation ContainsSubstringTests +{ + id matcher; +} + +- (void)setUp +{ + [super setUp]; + matcher = containsSubstring(EXCERPT); +} + +- (void)tearDown +{ + matcher = nil; + [super tearDown]; +} + +- (void)test_copesWithNilsAndUnknownTypes +{ + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_ifArgumentContainsSpecifiedSubstring +{ + assertMatches(@"excerpt at beginning", matcher, [EXCERPT stringByAppendingString:@"END"]); + assertMatches(@"excerpt at end", matcher, [@"START" stringByAppendingString:EXCERPT]); + assertMatches(@"excerpt in middle", matcher, + [[@"START" stringByAppendingString:EXCERPT] stringByAppendingString:@"END"]); + assertMatches(@"excerpt repeated", matcher, [EXCERPT stringByAppendingString:EXCERPT]); + + assertDoesNotMatch(@"excerpt not in string", matcher, @"whatever"); + assertDoesNotMatch(@"only part of excerpt", matcher, [EXCERPT substringFromIndex:1]); +} + +- (void)test_matches_ifArgumentIsEqualToSubstring +{ + assertMatches(@"excerpt is entire string", matcher, EXCERPT); +} + +- (void)test_matcherCreation_requiresNonNilArgument +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertThrows(containsSubstring(nil), @"Should require non-nil argument"); +#pragma clang diagnostic pop +} + +- (void)test_doesNotMatch_ifMatchingAgainstNonString +{ + assertDoesNotMatch(@"non-string", matcher, @3); +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"a string containing \"EXCERPT\"", matcher); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + assertNoMismatchDescription(matcher, EXCERPT); +} + +- (void)test_mismatchDescription_showsActualArgument +{ + assertMismatchDescription(@"was \"bad\"", matcher, @"bad"); +} + +- (void)test_describeMismatch +{ + assertDescribeMismatch(@"was \"bad\"", matcher, @"bad"); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Text/HCStringEndsWithTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Text/HCStringEndsWithTests.m new file mode 100644 index 0000000000..a8aea1bf24 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Text/HCStringEndsWithTests.m @@ -0,0 +1,87 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import "MatcherTestCase.h" + +static NSString *EXCERPT = @"EXCERPT"; + + +@interface EndsWithTests : MatcherTestCase +@end + +@implementation EndsWithTests +{ + id matcher; +} + +- (void)setUp +{ + [super setUp]; + matcher = endsWith(EXCERPT); +} + +- (void)tearDown +{ + matcher = nil; + [super tearDown]; +} + +- (void)test_copesWithNilsAndUnknownTypes +{ + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_ifArgumentContainsSpecifiedSubstring +{ + assertDoesNotMatch(@"excerpt at beginning", matcher, [EXCERPT stringByAppendingString:@"END"]); + assertMatches(@"excerpt at end", matcher, [@"START" stringByAppendingString:EXCERPT]); + assertDoesNotMatch(@"excerpt in middle", matcher, + [[@"START" stringByAppendingString:EXCERPT] stringByAppendingString:@"END"]); + assertMatches(@"excerpt repeated", matcher, [EXCERPT stringByAppendingString:EXCERPT]); + + assertDoesNotMatch(@"excerpt not in string", matcher, @"whatever"); + assertDoesNotMatch(@"only part of excerpt", matcher, [EXCERPT substringFromIndex:1]); +} + +- (void)test_matches_ifArgumentIsEqualToSubstring +{ + assertMatches(@"excerpt is entire string", matcher, EXCERPT); +} + +- (void)test_matcherCreation_requiresNonNilArgument +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertThrows(endsWith(nil), @"Should require non-nil argument"); +#pragma clang diagnostic pop +} + +- (void)test_doesNotMatch_ifMatchingAgainstNonString +{ + assertDoesNotMatch(@"non-string", matcher, @3); +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"a string ending with \"EXCERPT\"", matcher); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + assertNoMismatchDescription(matcher, EXCERPT); +} + +- (void)test_mismatchDescription_showsActualArgument +{ + assertMismatchDescription(@"was \"bad\"", matcher, @"bad"); +} + +- (void)test_describeMismatch +{ + assertDescribeMismatch(@"was \"bad\"", matcher, @"bad"); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Text/HCStringStartsWithTests.m b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Text/HCStringStartsWithTests.m new file mode 100644 index 0000000000..8112faa278 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/Tests/Text/HCStringStartsWithTests.m @@ -0,0 +1,87 @@ +// OCHamcrest by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 hamcrest. See LICENSE.txt + +#import + +#import "MatcherTestCase.h" + +static NSString *EXCERPT = @"EXCERPT"; + + +@interface StartsWithTests : MatcherTestCase +@end + +@implementation StartsWithTests +{ + id matcher; +} + +- (void)setUp +{ + [super setUp]; + matcher = startsWith(EXCERPT); +} + +- (void)tearDown +{ + matcher = nil; + [super tearDown]; +} + +- (void)test_copesWithNilsAndUnknownTypes +{ + assertNilSafe(matcher); + assertUnknownTypeSafe(matcher); +} + +- (void)test_matches_ifArgumentContainsSpecifiedSubstring +{ + assertMatches(@"excerpt at beginning", matcher, [EXCERPT stringByAppendingString:@"END"]); + assertDoesNotMatch(@"excerpt at end", matcher, [@"START" stringByAppendingString:EXCERPT]); + assertDoesNotMatch(@"excerpt in middle", matcher, + [[@"START" stringByAppendingString:EXCERPT] stringByAppendingString:@"END"]); + assertMatches(@"excerpt repeated", matcher, [EXCERPT stringByAppendingString:EXCERPT]); + + assertDoesNotMatch(@"excerpt not in string", matcher, @"whatever"); + assertDoesNotMatch(@"only part of excerpt", matcher, [EXCERPT substringFromIndex:1]); +} + +- (void)test_matches_ifArgumentIsEqualToSubstring +{ + assertMatches(@"excerpt is entire string", matcher, EXCERPT); +} + +- (void)test_matcherCreation_requiresNonNilArgument +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + XCTAssertThrows(startsWith(nil), @"Should require non-nil argument"); +#pragma clang diagnostic pop +} + +- (void)test_doesNotMatch_ifMatchingAgainstNonString +{ + assertDoesNotMatch(@"non-string", matcher, @3); +} + +- (void)test_hasReadableDescription +{ + assertDescription(@"a string starting with \"EXCERPT\"", matcher); +} + +- (void)test_successfulMatchDoesNotGenerateMismatchDescription +{ + assertNoMismatchDescription(matcher, EXCERPT); +} + +- (void)test_mismatchDescription_showsActualArgument +{ + assertMismatchDescription(@"was \"bad\"", matcher, @"bad"); +} + +- (void)test_describeMismatch +{ + assertDescribeMismatch(@"was \"bad\"", matcher, @"bad"); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/XcodeWarnings.xcconfig b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/XcodeWarnings.xcconfig new file mode 100644 index 0000000000..55b97cf025 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCHamcrest/Source/XcodeWarnings.xcconfig @@ -0,0 +1,146 @@ +// XcodeWarnings by Jon Reid, https://qualitycoding.org/ +// Copyright 2020 Quality Coding, Inc. See LICENSE.txt +// Source: https://github.com/jonreid/XcodeWarnings + +// Apple Clang - Address Sanitizer +CLANG_ADDRESS_SANITIZER_CONTAINER_OVERFLOW = YES + +// Apple Clang - Code Generation +GCC_STRICT_ALIASING = YES +GCC_REUSE_STRINGS = YES +GCC_NO_COMMON_BLOCKS = YES + +// Apple Clang - Language +GCC_ENABLE_TRIGRAPHS = NO + +// Apple Clang - Preprocessing +ENABLE_STRICT_OBJC_MSGSEND = YES + +// Apple Clang - Undefined Behavior Sanitizer +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_INTEGER = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES + +// Apple Clang - Warning Policies +//GCC_WARN_PEDANTIC = YES +//GCC_TREAT_WARNINGS_AS_ERRORS = YES +//SWIFT_TREAT_WARNINGS_AS_ERRORS = YES + +// Apple Clang - Warnings - All languages +CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES +GCC_WARN_CHECK_SWITCH_STATEMENTS = YES +GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS = YES +CLANG_WARN_DOCUMENTATION_COMMENTS = YES +CLANG_WARN_EMPTY_BODY = YES +GCC_WARN_FOUR_CHARACTER_CONSTANTS = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_BOOL_CONVERSION = YES +CLANG_WARN_CONSTANT_CONVERSION = YES +GCC_WARN_64_TO_32_BIT_CONVERSION = YES +CLANG_WARN_ENUM_CONVERSION = YES +CLANG_WARN_FLOAT_CONVERSION = YES +CLANG_WARN_INT_CONVERSION = YES +CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES +CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES +CLANG_WARN_INFINITE_RECURSION = YES +GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES +GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR +GCC_WARN_MISSING_PARENTHESES = YES +GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES +GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES +GCC_WARN_ABOUT_MISSING_NEWLINE = YES +CLANG_WARN_ASSIGN_ENUM = YES +CLANG_WARN_PRIVATE_MODULE = YES +GCC_WARN_ABOUT_POINTER_SIGNEDNESS = YES +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES +CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = YES +GCC_WARN_SIGN_COMPARE = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES +CLANG_WARN_PRAGMA_PACK = YES +GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES +GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES +GCC_WARN_TYPECHECK_CALLS_TO_PRINTF = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE +GCC_WARN_UNKNOWN_PRAGMAS = YES +CLANG_WARN_UNREACHABLE_CODE = YES_AGGRESSIVE +GCC_WARN_UNUSED_FUNCTION = YES +GCC_WARN_UNUSED_LABEL = YES +//GCC_WARN_UNUSED_PARAMETER = YES +GCC_WARN_UNUSED_VALUE = YES +GCC_WARN_UNUSED_VARIABLE = YES + +// Apple Clang - Warnings - C++ +CLANG_WARN_VEXING_PARSE = YES +CLANG_WARN_DELETE_NON_VIRTUAL_DTOR = YES +CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES +GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES +GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES +CLANG_WARN_RANGE_LOOP_ANALYSIS = YES +CLANG_WARN_SUSPICIOUS_MOVE = YES +GCC_WARN_ABOUT_INVALID_OFFSETOF_MACRO = YES +CLANG_WARN_ATOMIC_IMPLICIT_SEQ_CST = YES +CLANG_WARN_CXX0X_EXTENSIONS = YES + +// Apple Clang - Warnings - Objective-C +CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES +CLANG_WARN_OBJC_LITERAL_CONVERSION = YES +GCC_WARN_ALLOW_INCOMPLETE_PROTOCOL = YES +CLANG_WARN_OBJC_INTERFACE_IVARS = YES +CLANG_WARN_MISSING_NOESCAPE = YES +CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES +//OCHamcrest: GCC_WARN_STRICT_SELECTOR_MATCH = YES +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR + +// Apple Clang - Warnings - Objective-C and ARC +CLANG_WARN_OBJC_EXPLICIT_OWNERSHIP_TYPE = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES_AGGRESSIVE +CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES + +// Static Analyzer - Analysis Policy +RUN_CLANG_STATIC_ANALYZER = YES +CLANG_STATIC_ANALYZER_MODE_ON_ANALYZE_ACTION = Deep +CLANG_STATIC_ANALYZER_MODE = Deep + +// Static Analyzer - Generic Issues +CLANG_ANALYZER_DEADCODE_DEADSTORES = YES +CLANG_ANALYZER_MEMORY_MANAGEMENT = YES +CLANG_ANALYZER_NONNULL = YES +CLANG_ANALYZER_USE_AFTER_MOVE = YES_AGGRESSIVE + +// Static Analyzer - Issues - Apple APIs +CLANG_ANALYZER_OBJC_NSCFERROR = YES +CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES +CLANG_ANALYZER_LOCALIZABILITY_EMPTY_CONTEXT = YES +CLANG_ANALYZER_OBJC_COLLECTIONS = YES +CLANG_ANALYZER_GCD = YES +CLANG_ANALYZER_GCD_PERFORMANCE = YES +CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE +CLANG_ANALYZER_LIBKERN_RETAIN_COUNT = YES + +// Static Analyzer - Issues - Objective-C +CLANG_ANALYZER_OBJC_ATSYNC = YES +CLANG_ANALYZER_OBJC_DEALLOC = YES +CLANG_ANALYZER_OBJC_INCOMP_METHOD_TYPES = YES +CLANG_ANALYZER_OBJC_GENERICS = YES +CLANG_ANALYZER_OBJC_UNUSED_IVARS = YES +CLANG_ANALYZER_OBJC_SELF_INIT = YES +CLANG_ANALYZER_OBJC_RETAIN_COUNT = YES + +// Static Analyzer - Issues - Security +CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES +CLANG_ANALYZER_SECURITY_KEYCHAIN_API = YES +CLANG_ANALYZER_SECURITY_INSECUREAPI_UNCHECKEDRETURN = YES +CLANG_ANALYZER_SECURITY_INSECUREAPI_GETPW_GETS = YES +CLANG_ANALYZER_SECURITY_INSECUREAPI_MKSTEMP = YES +CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES +CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES +CLANG_ANALYZER_SECURITY_INSECUREAPI_VFORK = YES + +// Swift Compiler - Code Generation +SWIFT_ENFORCE_EXCLUSIVE_ACCESS = on diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/.gitignore b/submodules/AppCenter-sdk/Vendor/OCMock/.gitignore new file mode 100644 index 0000000000..09e8ce9741 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +xcuserdata +*.xcworkspace +.idea +build +Carthage diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/.travis.yml b/submodules/AppCenter-sdk/Vendor/OCMock/.travis.yml new file mode 100644 index 0000000000..526bb0b540 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/.travis.yml @@ -0,0 +1,5 @@ +language: objective-c +osx_image: xcode12.2 +before_install: + - gem install xcpretty -N --quiet +script: make ci diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/CONTRIBUTING.md b/submodules/AppCenter-sdk/Vendor/OCMock/CONTRIBUTING.md new file mode 100644 index 0000000000..b5bda84ece --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/CONTRIBUTING.md @@ -0,0 +1,49 @@ +# Contributing to OCMock + +First off, thanks for taking the time to contribute to OCMock! + +The following is a set of guidelines for contributing to OCMock. These are just guidelines, not rules. Use your best judgment and feel free to propose changes to this document in a pull request. + +This project adheres to the [Contributor Covenant 1.2](http://contributor-covenant.org/version/1/2/0). By participating, you are expected to uphold this code. + + +## Submitting issues + +* If you have a question about using OCMock, please do not open an issue. Ask the question on StackOverflow using the [ocmock](http://stackoverflow.com/questions/tagged/ocmock) tag. + +* If you have encountered an issue or you want to suggest an enhancement, perform a [cursory search](https://github.com/erikdoe/ocmock/issues?q=is%3Aissue) to see if a similar issue has already been submitted. + +* When you submit an issue, please try to provide a [minimal, complete, and verifiable example](http://stackoverflow.com/help/mcve), ideally with a failing unit test. The easier it is to understand and reproduce the issue, the more likely it is that we can provide a fix. + +* Include the version of OCMock you are using. If applicable include names and versions of other testing frameworks involved. + +* Include stacktraces; they are immensely helpful. + + +## Pull requests + +* Create all pull requests from `master`. Do not include other pull requests that have not been merged yet. + +* Limit each pull request to one feature. If you have made several changes, please submit multiple pull requests. Do not include seemingly trival changes, e.g. upgrading the Xcode version, in a pull request for a feature or bugfix. + +* If you add a new feature provide tests that specify how the feature works. + +* If you have to add files, please make sure that the code builds for the OS X framework and the iOS library using Xcode. Also try to make sure that the Cocoapod and Carthage builds work. + +* Respect the coding conventions (see below). + +* Do not include the number of a related issue in the title of a pull request. Give the pull request a descriptive title and reference any issues from the description. + +* Once you have created the pull request, an automated build is kicked off on [Travis CI](https://travis-ci.org/erikdoe/ocmock/pull_requests). Please verify after a few minutes that the build on the server succeeded. Pull requests with failing builds are ignored and will be closed within a few weeks if they are not fixed. + +* Please don't post comments like "Ping?" or similar on your own pull requests. I understand that it can be frustrating not to get a response quickly but, unfortunately, I often don't have time to work on OCMock and can't always look at pull requests as they come in. It's difficult for me to predict when I'll get a chance to do so, but rest assured, pull requests are definitely not ignored. + + +## Coding conventions + +Please remember that OCMock has been around for 10+ years. Some of the coding conventions used in OCMock may contradict modern guidelines. However, in the interest of keeping the codebase consistent, please respect the conventions used. In particular: + +* Opening and closing braces always go on a separate line. +* No spaces between keywords like `if` and `for` and the following bracket. +* No underscores for instance variables. +* OCMock itself (framework and library) does not use ARC; retains and releases must be sent manually. The unit tests, however, do use ARC. diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/ArcExample/ArcExample.xcodeproj/project.pbxproj b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/ArcExample/ArcExample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..94a959a5e8 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/ArcExample/ArcExample.xcodeproj/project.pbxproj @@ -0,0 +1,244 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 0351A22A152F65DA005B52FE /* OCMock.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0351A229152F65DA005B52FE /* OCMock.framework */; }; + 035F7F1E14CDBBBC00D21121 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 035F7F1D14CDBBBC00D21121 /* Foundation.framework */; }; + 035F7F2114CDBBBC00D21121 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 035F7F2014CDBBBC00D21121 /* main.m */; }; + 035F7F2514CDBBBC00D21121 /* ArcExample.1 in CopyFiles */ = {isa = PBXBuildFile; fileRef = 035F7F2414CDBBBC00D21121 /* ArcExample.1 */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 035F7F1714CDBBBC00D21121 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + 035F7F2514CDBBBC00D21121 /* ArcExample.1 in CopyFiles */, + ); + runOnlyForDeploymentPostprocessing = 1; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0351A229152F65DA005B52FE /* OCMock.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OCMock.framework; path = /Library/Frameworks/OCMock.framework; sourceTree = ""; }; + 035F7F1914CDBBBC00D21121 /* ArcExample */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = ArcExample; sourceTree = BUILT_PRODUCTS_DIR; }; + 035F7F1D14CDBBBC00D21121 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + 035F7F2014CDBBBC00D21121 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 035F7F2314CDBBBC00D21121 /* ArcExample-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ArcExample-Prefix.pch"; sourceTree = ""; }; + 035F7F2414CDBBBC00D21121 /* ArcExample.1 */ = {isa = PBXFileReference; lastKnownFileType = text.man; path = ArcExample.1; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 035F7F1614CDBBBC00D21121 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 035F7F1E14CDBBBC00D21121 /* Foundation.framework in Frameworks */, + 0351A22A152F65DA005B52FE /* OCMock.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 035F7F0E14CDBBBC00D21121 = { + isa = PBXGroup; + children = ( + 035F7F1F14CDBBBC00D21121 /* ArcExample */, + 035F7F1C14CDBBBC00D21121 /* Frameworks */, + 035F7F1A14CDBBBC00D21121 /* Products */, + ); + sourceTree = ""; + }; + 035F7F1A14CDBBBC00D21121 /* Products */ = { + isa = PBXGroup; + children = ( + 035F7F1914CDBBBC00D21121 /* ArcExample */, + ); + name = Products; + sourceTree = ""; + }; + 035F7F1C14CDBBBC00D21121 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 0351A229152F65DA005B52FE /* OCMock.framework */, + 035F7F1D14CDBBBC00D21121 /* Foundation.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 035F7F1F14CDBBBC00D21121 /* ArcExample */ = { + isa = PBXGroup; + children = ( + 035F7F2014CDBBBC00D21121 /* main.m */, + 035F7F2414CDBBBC00D21121 /* ArcExample.1 */, + 035F7F2214CDBBBC00D21121 /* Supporting Files */, + ); + path = ArcExample; + sourceTree = ""; + }; + 035F7F2214CDBBBC00D21121 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 035F7F2314CDBBBC00D21121 /* ArcExample-Prefix.pch */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 035F7F1814CDBBBC00D21121 /* ArcExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 035F7F2814CDBBBC00D21121 /* Build configuration list for PBXNativeTarget "ArcExample" */; + buildPhases = ( + 035F7F1514CDBBBC00D21121 /* Sources */, + 035F7F1614CDBBBC00D21121 /* Frameworks */, + 035F7F1714CDBBBC00D21121 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ArcExample; + productName = ArcExample; + productReference = 035F7F1914CDBBBC00D21121 /* ArcExample */; + productType = "com.apple.product-type.tool"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 035F7F1014CDBBBC00D21121 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0430; + ORGANIZATIONNAME = ThoughtWorks; + }; + buildConfigurationList = 035F7F1314CDBBBC00D21121 /* Build configuration list for PBXProject "ArcExample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 035F7F0E14CDBBBC00D21121; + productRefGroup = 035F7F1A14CDBBBC00D21121 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 035F7F1814CDBBBC00D21121 /* ArcExample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 035F7F1514CDBBBC00D21121 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 035F7F2114CDBBBC00D21121 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 035F7F2614CDBBBC00D21121 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD_64_BIT)"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_OBJCPP_ARC_ABI = YES; + COPY_PHASE_STRIP = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_VERSION = com.apple.compilers.llvm.clang.1_0; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.7; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx10.7; + }; + name = Debug; + }; + 035F7F2714CDBBBC00D21121 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD_64_BIT)"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_OBJCPP_ARC_ABI = YES; + COPY_PHASE_STRIP = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + GCC_VERSION = com.apple.compilers.llvm.clang.1_0; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.7; + SDKROOT = macosx10.7; + }; + name = Release; + }; + 035F7F2914CDBBBC00D21121 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "ArcExample/ArcExample-Prefix.pch"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 035F7F2A14CDBBBC00D21121 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "ArcExample/ArcExample-Prefix.pch"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 035F7F1314CDBBBC00D21121 /* Build configuration list for PBXProject "ArcExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 035F7F2614CDBBBC00D21121 /* Debug */, + 035F7F2714CDBBBC00D21121 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 035F7F2814CDBBBC00D21121 /* Build configuration list for PBXNativeTarget "ArcExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 035F7F2914CDBBBC00D21121 /* Debug */, + 035F7F2A14CDBBBC00D21121 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 035F7F1014CDBBBC00D21121 /* Project object */; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/ArcExample/ArcExample/ArcExample-Prefix.pch b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/ArcExample/ArcExample/ArcExample-Prefix.pch new file mode 100644 index 0000000000..ad04264a46 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/ArcExample/ArcExample/ArcExample-Prefix.pch @@ -0,0 +1,7 @@ +// +// Prefix header for all source files of the 'ArcExample' target in the 'ArcExample' project +// + +#ifdef __OBJC__ + #import +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/ArcExample/ArcExample/ArcExample.1 b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/ArcExample/ArcExample/ArcExample.1 new file mode 100644 index 0000000000..1568cef463 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/ArcExample/ArcExample/ArcExample.1 @@ -0,0 +1,79 @@ +.\"Modified from man(1) of FreeBSD, the NetBSD mdoc.template, and mdoc.samples. +.\"See Also: +.\"man mdoc.samples for a complete listing of options +.\"man mdoc for the short list of editing options +.\"/usr/share/misc/mdoc.template +.Dd 23/01/12 \" DATE +.Dt ArcExample 1 \" Program name and manual section number +.Os Darwin +.Sh NAME \" Section Header - required - don't modify +.Nm ArcExample, +.\" The following lines are read in generating the apropos(man -k) database. Use only key +.\" words here as the database is built based on the words here and in the .ND line. +.Nm Other_name_for_same_program(), +.Nm Yet another name for the same program. +.\" Use .Nm macro to designate other names for the documented program. +.Nd This line parsed for whatis database. +.Sh SYNOPSIS \" Section Header - required - don't modify +.Nm +.Op Fl abcd \" [-abcd] +.Op Fl a Ar path \" [-a path] +.Op Ar file \" [file] +.Op Ar \" [file ...] +.Ar arg0 \" Underlined argument - use .Ar anywhere to underline +arg2 ... \" Arguments +.Sh DESCRIPTION \" Section Header - required - don't modify +Use the .Nm macro to refer to your program throughout the man page like such: +.Nm +Underlining is accomplished with the .Ar macro like this: +.Ar underlined text . +.Pp \" Inserts a space +A list of items with descriptions: +.Bl -tag -width -indent \" Begins a tagged list +.It item a \" Each item preceded by .It macro +Description of item a +.It item b +Description of item b +.El \" Ends the list +.Pp +A list of flags and their descriptions: +.Bl -tag -width -indent \" Differs from above in tag removed +.It Fl a \"-a flag as a list item +Description of -a flag +.It Fl b +Description of -b flag +.El \" Ends the list +.Pp +.\" .Sh ENVIRONMENT \" May not be needed +.\" .Bl -tag -width "ENV_VAR_1" -indent \" ENV_VAR_1 is width of the string ENV_VAR_1 +.\" .It Ev ENV_VAR_1 +.\" Description of ENV_VAR_1 +.\" .It Ev ENV_VAR_2 +.\" Description of ENV_VAR_2 +.\" .El +.Sh FILES \" File used or created by the topic of the man page +.Bl -tag -width "/Users/joeuser/Library/really_long_file_name" -compact +.It Pa /usr/share/file_name +FILE_1 description +.It Pa /Users/joeuser/Library/really_long_file_name +FILE_2 description +.El \" Ends the list +.\" .Sh DIAGNOSTICS \" May not be needed +.\" .Bl -diag +.\" .It Diagnostic Tag +.\" Diagnostic informtion here. +.\" .It Diagnostic Tag +.\" Diagnostic informtion here. +.\" .El +.Sh SEE ALSO +.\" List links in ascending order by section, alphabetically within a section. +.\" Please do not reference files that do not exist without filing a bug report +.Xr a 1 , +.Xr b 1 , +.Xr c 1 , +.Xr a 2 , +.Xr b 2 , +.Xr a 3 , +.Xr b 3 +.\" .Sh BUGS \" Document known, unremedied bugs +.\" .Sh HISTORY \" Document history if command behaves in a unique manner \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/ArcExample/ArcExample/main.m b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/ArcExample/ArcExample/main.m new file mode 100644 index 0000000000..aecc32de48 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/ArcExample/ArcExample/main.m @@ -0,0 +1,34 @@ + +#import +#import + +@protocol SomeDelegateProtocol +- (void)doStuff; +@end + +@interface SomeClass : NSObject +@property (nonatomic, weak) id delegate; +@end + +@implementation SomeClass + +@synthesize delegate; + +@end + + +int main (int argc, const char * argv[]) +{ + + @autoreleasepool { + + SomeClass *someObject = [[SomeClass alloc] init]; + id delegate = [OCMockObject mockForProtocol:@protocol(SomeDelegateProtocol)]; + someObject.delegate = delegate; + NSLog(@"delegate = %@", delegate); + NSLog(@"someObject = %@", someObject.delegate); + + } + return 0; +} + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples.xcodeproj/project.pbxproj b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..c1044a7ffa --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples.xcodeproj/project.pbxproj @@ -0,0 +1,501 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 03151B301948E756009A27B8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03151B2F1948E756009A27B8 /* AppDelegate.swift */; }; + 03151B321948E756009A27B8 /* MasterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03151B311948E756009A27B8 /* MasterViewController.swift */; }; + 03151B341948E756009A27B8 /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03151B331948E756009A27B8 /* DetailViewController.swift */; }; + 03151B371948E756009A27B8 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 03151B351948E756009A27B8 /* Main.storyboard */; }; + 03151B391948E756009A27B8 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 03151B381948E756009A27B8 /* Images.xcassets */; }; + 03151B451948E757009A27B8 /* SwiftExamplesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03151B441948E757009A27B8 /* SwiftExamplesTests.swift */; }; + 03151B531948E7FF009A27B8 /* libOCMock.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 03151B521948E7FF009A27B8 /* libOCMock.a */; }; + 03151B561948E891009A27B8 /* MockTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03151B551948E891009A27B8 /* MockTests.m */; }; + 032E56391948EAE4000CEB43 /* Connection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032E56371948EAE4000CEB43 /* Connection.swift */; }; + 032E563A1948EAE4000CEB43 /* Controller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 032E56381948EAE4000CEB43 /* Controller.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 03151B3F1948E757009A27B8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 03151B221948E756009A27B8 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 03151B291948E756009A27B8; + remoteInfo = SwiftExamples; + }; + 03151B4E1948E7E9009A27B8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 03151B221948E756009A27B8 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 03151B291948E756009A27B8; + remoteInfo = SwiftExamples; + }; + 03151B501948E7EA009A27B8 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 03151B221948E756009A27B8 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 03151B291948E756009A27B8; + remoteInfo = SwiftExamples; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 03151B2A1948E756009A27B8 /* SwiftExamples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftExamples.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 03151B2E1948E756009A27B8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 03151B2F1948E756009A27B8 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 03151B311948E756009A27B8 /* MasterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterViewController.swift; sourceTree = ""; }; + 03151B331948E756009A27B8 /* DetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = ""; }; + 03151B361948E756009A27B8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 03151B381948E756009A27B8 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + 03151B3E1948E757009A27B8 /* SwiftExamplesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftExamplesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 03151B431948E757009A27B8 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 03151B441948E757009A27B8 /* SwiftExamplesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftExamplesTests.swift; sourceTree = ""; }; + 03151B521948E7FF009A27B8 /* libOCMock.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libOCMock.a; path = usr/lib/libOCMock.a; sourceTree = ""; }; + 03151B541948E890009A27B8 /* SwiftExamplesTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SwiftExamplesTests-Bridging-Header.h"; sourceTree = ""; }; + 03151B551948E891009A27B8 /* MockTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MockTests.m; sourceTree = ""; }; + 032E56371948EAE4000CEB43 /* Connection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Connection.swift; path = SwiftExamples/Connection.swift; sourceTree = SOURCE_ROOT; }; + 032E56381948EAE4000CEB43 /* Controller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Controller.swift; path = SwiftExamples/Controller.swift; sourceTree = SOURCE_ROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 03151B271948E756009A27B8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 03151B3B1948E757009A27B8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 03151B531948E7FF009A27B8 /* libOCMock.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 03151B211948E756009A27B8 = { + isa = PBXGroup; + children = ( + 03151B521948E7FF009A27B8 /* libOCMock.a */, + 03151B2C1948E756009A27B8 /* SwiftExamples */, + 03151B411948E757009A27B8 /* SwiftExamplesTests */, + 03151B2B1948E756009A27B8 /* Products */, + ); + sourceTree = ""; + }; + 03151B2B1948E756009A27B8 /* Products */ = { + isa = PBXGroup; + children = ( + 03151B2A1948E756009A27B8 /* SwiftExamples.app */, + 03151B3E1948E757009A27B8 /* SwiftExamplesTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 03151B2C1948E756009A27B8 /* SwiftExamples */ = { + isa = PBXGroup; + children = ( + 03151B2F1948E756009A27B8 /* AppDelegate.swift */, + 03151B311948E756009A27B8 /* MasterViewController.swift */, + 03151B331948E756009A27B8 /* DetailViewController.swift */, + 03151B351948E756009A27B8 /* Main.storyboard */, + 03151B381948E756009A27B8 /* Images.xcassets */, + 03151B2D1948E756009A27B8 /* Supporting Files */, + ); + path = SwiftExamples; + sourceTree = ""; + }; + 03151B2D1948E756009A27B8 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 03151B2E1948E756009A27B8 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 03151B411948E757009A27B8 /* SwiftExamplesTests */ = { + isa = PBXGroup; + children = ( + 032E56371948EAE4000CEB43 /* Connection.swift */, + 032E56381948EAE4000CEB43 /* Controller.swift */, + 03151B441948E757009A27B8 /* SwiftExamplesTests.swift */, + 03151B551948E891009A27B8 /* MockTests.m */, + 03151B421948E757009A27B8 /* Supporting Files */, + 03151B541948E890009A27B8 /* SwiftExamplesTests-Bridging-Header.h */, + ); + path = SwiftExamplesTests; + sourceTree = ""; + }; + 03151B421948E757009A27B8 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 03151B431948E757009A27B8 /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 03151B291948E756009A27B8 /* SwiftExamples */ = { + isa = PBXNativeTarget; + buildConfigurationList = 03151B481948E757009A27B8 /* Build configuration list for PBXNativeTarget "SwiftExamples" */; + buildPhases = ( + 03151B261948E756009A27B8 /* Sources */, + 03151B271948E756009A27B8 /* Frameworks */, + 03151B281948E756009A27B8 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SwiftExamples; + productName = SwiftExamples; + productReference = 03151B2A1948E756009A27B8 /* SwiftExamples.app */; + productType = "com.apple.product-type.application"; + }; + 03151B3D1948E757009A27B8 /* SwiftExamplesTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 03151B4B1948E757009A27B8 /* Build configuration list for PBXNativeTarget "SwiftExamplesTests" */; + buildPhases = ( + 03151B3A1948E757009A27B8 /* Sources */, + 03151B3B1948E757009A27B8 /* Frameworks */, + 03151B3C1948E757009A27B8 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 03151B401948E757009A27B8 /* PBXTargetDependency */, + 03151B4F1948E7E9009A27B8 /* PBXTargetDependency */, + 03151B511948E7EA009A27B8 /* PBXTargetDependency */, + ); + name = SwiftExamplesTests; + productName = SwiftExamplesTests; + productReference = 03151B3E1948E757009A27B8 /* SwiftExamplesTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 03151B221948E756009A27B8 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftMigration = 0700; + LastSwiftUpdateCheck = 0700; + LastUpgradeCheck = 0600; + ORGANIZATIONNAME = "Mulle Kybernetik"; + TargetAttributes = { + 03151B291948E756009A27B8 = { + CreatedOnToolsVersion = 6.0; + }; + 03151B3D1948E757009A27B8 = { + CreatedOnToolsVersion = 6.0; + TestTargetID = 03151B291948E756009A27B8; + }; + }; + }; + buildConfigurationList = 03151B251948E756009A27B8 /* Build configuration list for PBXProject "SwiftExamples" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 03151B211948E756009A27B8; + productRefGroup = 03151B2B1948E756009A27B8 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 03151B291948E756009A27B8 /* SwiftExamples */, + 03151B3D1948E757009A27B8 /* SwiftExamplesTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 03151B281948E756009A27B8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 03151B371948E756009A27B8 /* Main.storyboard in Resources */, + 03151B391948E756009A27B8 /* Images.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 03151B3C1948E757009A27B8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 03151B261948E756009A27B8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 03151B341948E756009A27B8 /* DetailViewController.swift in Sources */, + 03151B321948E756009A27B8 /* MasterViewController.swift in Sources */, + 03151B301948E756009A27B8 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 03151B3A1948E757009A27B8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 03151B451948E757009A27B8 /* SwiftExamplesTests.swift in Sources */, + 032E56391948EAE4000CEB43 /* Connection.swift in Sources */, + 03151B561948E891009A27B8 /* MockTests.m in Sources */, + 032E563A1948EAE4000CEB43 /* Controller.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 03151B401948E757009A27B8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 03151B291948E756009A27B8 /* SwiftExamples */; + targetProxy = 03151B3F1948E757009A27B8 /* PBXContainerItemProxy */; + }; + 03151B4F1948E7E9009A27B8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 03151B291948E756009A27B8 /* SwiftExamples */; + targetProxy = 03151B4E1948E7E9009A27B8 /* PBXContainerItemProxy */; + }; + 03151B511948E7EA009A27B8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 03151B291948E756009A27B8 /* SwiftExamples */; + targetProxy = 03151B501948E7EA009A27B8 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 03151B351948E756009A27B8 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 03151B361948E756009A27B8 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 03151B461948E757009A27B8 /* Debug */ = { + isa = XCBuildConfiguration; + 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_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + 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; + METAL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 03151B471948E757009A27B8 /* Release */ = { + isa = XCBuildConfiguration; + 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_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + 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; + METAL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 03151B491948E757009A27B8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + INFOPLIST_FILE = SwiftExamples/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 03151B4A1948E757009A27B8 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + INFOPLIST_FILE = SwiftExamples/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + 03151B4C1948E757009A27B8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/SwiftExamples.app/SwiftExamples"; + CLANG_ENABLE_MODULES = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "/Applications/Xcode6-Beta.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include", + "$(PROJECT_DIR)/usr/include", + ); + INFOPLIST_FILE = SwiftExamplesTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/usr/lib", + ); + METAL_ENABLE_DEBUG_INFO = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + XCTest, + "-ObjC", + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "SwiftExamplesTests/SwiftExamplesTests-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TEST_HOST = "$(BUNDLE_LOADER)"; + }; + name = Debug; + }; + 03151B4D1948E757009A27B8 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/SwiftExamples.app/SwiftExamples"; + CLANG_ENABLE_MODULES = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + ); + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "/Applications/Xcode6-Beta.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include", + "$(PROJECT_DIR)/usr/include", + ); + INFOPLIST_FILE = SwiftExamplesTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/usr/lib", + ); + METAL_ENABLE_DEBUG_INFO = NO; + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + XCTest, + "-ObjC", + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "SwiftExamplesTests/SwiftExamplesTests-Bridging-Header.h"; + TEST_HOST = "$(BUNDLE_LOADER)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 03151B251948E756009A27B8 /* Build configuration list for PBXProject "SwiftExamples" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 03151B461948E757009A27B8 /* Debug */, + 03151B471948E757009A27B8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 03151B481948E757009A27B8 /* Build configuration list for PBXNativeTarget "SwiftExamples" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 03151B491948E757009A27B8 /* Debug */, + 03151B4A1948E757009A27B8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 03151B4B1948E757009A27B8 /* Build configuration list for PBXNativeTarget "SwiftExamplesTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 03151B4C1948E757009A27B8 /* Debug */, + 03151B4D1948E757009A27B8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 03151B221948E756009A27B8 /* Project object */; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples/AppDelegate.swift b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples/AppDelegate.swift new file mode 100644 index 0000000000..503095801b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples/AppDelegate.swift @@ -0,0 +1,46 @@ +// +// AppDelegate.swift +// SwiftExamples +// +// Created by Erik Doernenburg on 11/06/2014. +// Copyright (c) 2014 Mulle Kybernetik. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + + func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { + // Override point for customization after application launch. + return true + } + + func applicationWillResignActive(application: UIApplication) { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. + } + + func applicationDidEnterBackground(application: UIApplication) { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + } + + func applicationWillEnterForeground(application: UIApplication) { + // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. + } + + func applicationDidBecomeActive(application: UIApplication) { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + } + + func applicationWillTerminate(application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + +} + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples/Base.lproj/Main.storyboard b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples/Base.lproj/Main.storyboard new file mode 100644 index 0000000000..24781fe5d4 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples/Base.lproj/Main.storyboard @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples/Connection.swift b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples/Connection.swift new file mode 100644 index 0000000000..c1a16572b8 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples/Connection.swift @@ -0,0 +1,20 @@ +// +// Connection.swift +// SwiftExperiments +// +// Created by Erik D on 05/06/14. +// Copyright (c) 2014 Mulle Kybernetik. All rights reserved. +// + +import Foundation + +@objc +protocol Connection { + func fetchData() -> String +} + +class ServerConnection : NSObject, Connection { + func fetchData() -> String { + return "real data returned from other system" + } +} diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples/Controller.swift b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples/Controller.swift new file mode 100644 index 0000000000..f60948cfa4 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples/Controller.swift @@ -0,0 +1,27 @@ +// +// Controller.swift +// SwiftExperiments +// +// Created by Erik D on 05/06/14. +// Copyright (c) 2014 Mulle Kybernetik. All rights reserved. +// + +import Foundation + +class Controller: NSObject { + var connection: Connection; + var data: String; + + class func newController() -> Controller { + return Controller() + } + + override init() { + self.connection = ServerConnection(); + self.data = ""; + } + + func redisplay() { + data = connection.fetchData(); + } +} diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples/DetailViewController.swift b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples/DetailViewController.swift new file mode 100644 index 0000000000..5674c78814 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples/DetailViewController.swift @@ -0,0 +1,46 @@ +// +// DetailViewController.swift +// SwiftExamples +// +// Created by Erik Doernenburg on 11/06/2014. +// Copyright (c) 2014 Mulle Kybernetik. All rights reserved. +// + +import UIKit + +class DetailViewController: UIViewController { + + @IBOutlet weak var detailDescriptionLabel: UILabel! + + + var detailItem: AnyObject? { + didSet { + // Update the view. + self.configureView() + } + } + + func configureView() { + // Update the user interface for the detail item. + if let detail: AnyObject = self.detailItem { + if let label = self.detailDescriptionLabel { + label.text = detail.description + } + } + } + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view, typically from a nib. + self.configureView() + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + +} + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples/Images.xcassets/AppIcon.appiconset/Contents.json b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..91bf9c14a7 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,53 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "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" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples/Images.xcassets/LaunchImage.launchimage/Contents.json b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples/Images.xcassets/LaunchImage.launchimage/Contents.json new file mode 100644 index 0000000000..6f870a4629 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples/Images.xcassets/LaunchImage.launchimage/Contents.json @@ -0,0 +1,51 @@ +{ + "images" : [ + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "subtype" : "retina4", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "1x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples/Info.plist b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples/Info.plist new file mode 100644 index 0000000000..5304abd96b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples/Info.plist @@ -0,0 +1,42 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + com.mulle-kybernetik.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UIStatusBarTintParameters + + UINavigationBar + + Style + UIBarStyleDefault + Translucent + + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples/MasterViewController.swift b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples/MasterViewController.swift new file mode 100644 index 0000000000..9a9010f7e2 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamples/MasterViewController.swift @@ -0,0 +1,87 @@ +// +// MasterViewController.swift +// SwiftExamples +// +// Created by Erik Doernenburg on 11/06/2014. +// Copyright (c) 2014 Mulle Kybernetik. All rights reserved. +// + +import UIKit + +class MasterViewController: UITableViewController { + + var objects = NSMutableArray() + + + override func awakeFromNib() { + super.awakeFromNib() + } + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view, typically from a nib. + self.navigationItem.leftBarButtonItem = self.editButtonItem() + + let addButton = UIBarButtonItem(barButtonSystemItem: .Add, target: self, action: "insertNewObject:") + self.navigationItem.rightBarButtonItem = addButton + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } + + func insertNewObject(sender: AnyObject) { + objects.insertObject(NSDate(), atIndex: 0) + let indexPath = NSIndexPath(forRow: 0, inSection: 0) + self.tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic) + } + + // MARK: - Segues + + override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { + if segue.identifier == "showDetail" { + if let indexPath = self.tableView.indexPathForSelectedRow { + let object = objects[indexPath.row] as! NSDate + (segue.destinationViewController as! DetailViewController).detailItem = object + } + } + } + + // MARK: - Table View + + override func numberOfSectionsInTableView(tableView: UITableView) -> Int { + return 1 + } + + override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return objects.count + } + + override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell + + let object = objects[indexPath.row] as! NSDate + cell.textLabel!.text = object.description + return cell + } + + override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { + // Return false if you do not want the specified item to be editable. + return true + } + + override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { + if editingStyle == .Delete { + objects.removeObjectAtIndex(indexPath.row) + tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade) + } else if editingStyle == .Insert { + // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view. + } + } + + +} + + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamplesTests/Info.plist b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamplesTests/Info.plist new file mode 100644 index 0000000000..6faa33491a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamplesTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + com.mulle-kybernetik.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamplesTests/MockTests.m b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamplesTests/MockTests.m new file mode 100644 index 0000000000..748a2423ca --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamplesTests/MockTests.m @@ -0,0 +1,71 @@ +// +// MockTests.m +// SwiftExamples +// +// Created by Erik Doernenburg on 11/06/2014. +// Copyright (c) 2014 Mulle Kybernetik. All rights reserved. +// + +#import +#import +#import "SwiftExamplesTests-Swift.h" + + +@interface MockTests : XCTestCase + +@end + +@implementation MockTests + +- (void)testThatOCMockWorksInSwiftProject +{ + id mock = OCMClassMock([NSString class]); + + [mock lowercaseString]; + + OCMVerify([mock lowercaseString]); +} + +- (void)testMockingAnObject +{ + id mock = OCMClassMock([ServerConnection class]); + OCMStub([mock fetchData]).andReturn(@"stubbed!"); + + Controller *controller = [Controller newController]; + controller.connection = mock; + + [controller redisplay]; + + OCMVerify([mock fetchData]); + XCTAssertEqualObjects(@"stubbed!", controller.data, @"Excpected stubbed data in controller."); +} + +- (void)testPartiallyMockingAnObject +{ + ServerConnection *testConnection = [ServerConnection new]; + id mock = OCMPartialMock(testConnection); + OCMStub([mock fetchData]).andReturn(@"stubbed!"); + + Controller *controller = [Controller newController]; + controller.connection = testConnection; + + [controller redisplay]; + + OCMVerify([mock fetchData]); + XCTAssertEqualObjects(@"stubbed!", controller.data, @"Excpected stubbed data in controller."); +} + +- (void)testPartiallyMockingAnObject2 +{ + Controller *controller = [Controller newController]; + + id mock = OCMPartialMock((NSObject *)controller.connection); // we know connection is derived from NSObject... + OCMStub([mock fetchData]).andReturn(@"stubbed!"); + + [controller redisplay]; + + OCMVerify([mock fetchData]); + XCTAssertEqualObjects(@"stubbed!", controller.data, @"Excpected stubbed data in controller."); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamplesTests/SwiftExamplesTests-Bridging-Header.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamplesTests/SwiftExamplesTests-Bridging-Header.h new file mode 100644 index 0000000000..1b2cb5d6d0 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamplesTests/SwiftExamplesTests-Bridging-Header.h @@ -0,0 +1,4 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamplesTests/SwiftExamplesTests.swift b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamplesTests/SwiftExamplesTests.swift new file mode 100644 index 0000000000..462c68b319 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/SwiftExamplesTests/SwiftExamplesTests.swift @@ -0,0 +1,35 @@ +// +// SwiftExamplesTests.swift +// SwiftExamplesTests +// +// Created by Erik Doernenburg on 11/06/2014. +// Copyright (c) 2014 Mulle Kybernetik. All rights reserved. +// + +import XCTest + +class SwiftExamplesTests: 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.measureBlock() { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/usr/include/OCMock/NSNotificationCenter+OCMAdditions.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/usr/include/OCMock/NSNotificationCenter+OCMAdditions.h new file mode 100644 index 0000000000..c20a9c2b20 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/usr/include/OCMock/NSNotificationCenter+OCMAdditions.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2009-2014 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@class OCObserverMockObject; + + +@interface NSNotificationCenter(OCMAdditions) + +- (void)addMockObserver:(OCObserverMockObject *)notificationObserver name:(NSString *)notificationName object:(id)notificationSender; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/usr/include/OCMock/OCMArg.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/usr/include/OCMock/OCMArg.h new file mode 100644 index 0000000000..14fb1617f0 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/usr/include/OCMock/OCMArg.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2009-2014 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@interface OCMArg : NSObject + +// constraining arguments + ++ (id)any; ++ (SEL)anySelector; ++ (void *)anyPointer; ++ (id __autoreleasing *)anyObjectRef; ++ (id)isNil; ++ (id)isNotNil; ++ (id)isNotEqual:(id)value; ++ (id)checkWithSelector:(SEL)selector onObject:(id)anObject; ++ (id)checkWithBlock:(BOOL (^)(id obj))block; + +// manipulating arguments + ++ (id *)setTo:(id)value; ++ (void *)setToValue:(NSValue *)value; + +// internal use only + ++ (id)resolveSpecialValues:(NSValue *)value; + +@end + +#define OCMOCK_ANY [OCMArg any] + +#if defined(__GNUC__) && !defined(__STRICT_ANSI__) + #define OCMOCK_VALUE(variable) \ + ({ __typeof__(variable) __v = (variable); [NSValue value:&__v withObjCType:@encode(__typeof__(__v))]; }) +#else + #define OCMOCK_VALUE(variable) [NSValue value:&variable withObjCType:@encode(__typeof__(variable))] +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/usr/include/OCMock/OCMConstraint.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/usr/include/OCMock/OCMConstraint.h new file mode 100644 index 0000000000..b606a54eab --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/usr/include/OCMock/OCMConstraint.h @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2007-2014 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + + +@interface OCMConstraint : NSObject + ++ (id)constraint; +- (BOOL)evaluate:(id)value; + +// if you are looking for any, isNil, etc, they have moved to OCMArg + +// try to use [OCMArg checkWith...] instead of the constraintWith... methods below + ++ (id)constraintWithSelector:(SEL)aSelector onObject:(id)anObject; ++ (id)constraintWithSelector:(SEL)aSelector onObject:(id)anObject withValue:(id)aValue; + + +@end + +@interface OCMAnyConstraint : OCMConstraint +@end + +@interface OCMIsNilConstraint : OCMConstraint +@end + +@interface OCMIsNotNilConstraint : OCMConstraint +@end + +@interface OCMIsNotEqualConstraint : OCMConstraint +{ + @public + id testValue; +} + +@end + +@interface OCMInvocationConstraint : OCMConstraint +{ + @public + NSInvocation *invocation; +} + +@end + +@interface OCMBlockConstraint : OCMConstraint +{ + BOOL (^block)(id); +} + +- (id)initWithConstraintBlock:(BOOL (^)(id))block; + +@end + + +#define CONSTRAINT(aSelector) [OCMConstraint constraintWithSelector:aSelector onObject:self] +#define CONSTRAINTV(aSelector, aValue) [OCMConstraint constraintWithSelector:aSelector onObject:self withValue:(aValue)] diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/usr/include/OCMock/OCMLocation.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/usr/include/OCMock/OCMLocation.h new file mode 100644 index 0000000000..136d9c9f0e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/usr/include/OCMock/OCMLocation.h @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2014 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@interface OCMLocation : NSObject +{ + id testCase; + NSString *file; + NSUInteger line; +} + ++ (id)locationWithTestCase:(id)aTestCase file:(NSString *)aFile line:(NSUInteger)aLine; + +- (id)initWithTestCase:(id)aTestCase file:(NSString *)aFile line:(NSUInteger)aLine; + +- (id)testCase; +- (NSString *)file; +- (NSUInteger)line; + +@end + +extern OCMLocation *OCMMakeLocation(id testCase, const char *file, int line); diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/usr/include/OCMock/OCMMacroState.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/usr/include/OCMock/OCMMacroState.h new file mode 100644 index 0000000000..81820b29fe --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/usr/include/OCMock/OCMMacroState.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2014 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@class OCMLocation; +@class OCMockRecorder; + + +@interface OCMMacroState : NSObject +{ +} + ++ (void)beginStubMacro; ++ (OCMockRecorder *)endStubMacro; + ++ (void)beginExpectMacro; ++ (OCMockRecorder *)endExpectMacro; + ++ (void)beginVerifyMacroAtLocation:(OCMLocation *)aLocation; ++ (void)endVerifyMacro; + ++ (OCMMacroState *)globalState; + +- (void)switchToClassMethod; +- (BOOL)hasSwitchedToClassMethod; + +- (void)handleInvocation:(NSInvocation *)anInvocation; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/usr/include/OCMock/OCMock.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/usr/include/OCMock/OCMock.h new file mode 100644 index 0000000000..eaaf51a8b0 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/usr/include/OCMock/OCMock.h @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2004-2014 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import +#import +#import +#import +#import +#import + + +#define OCMClassMock(cls) [OCMockObject niceMockForClass:cls] + +#define OCMStrictClassMock(cls) [OCMockObject mockForClass:cls] + +#define OCMProtocolMock(protocol) [OCMockObject niceMockForProtocol:protocol] + +#define OCMStrictProtocolMock(protocol) [OCMockObject mockForProtocol:protocol] + +#define OCMPartialMock(obj) [OCMockObject partialMockForObject:obj] + +#define OCMObserverMock() [OCMockObject observerMock] + + +#define OCMStub(invocation) \ +({ \ + [OCMMacroState beginStubMacro]; \ + invocation; \ + [OCMMacroState endStubMacro]; \ +}) + +#define OCMExpect(invocation) \ +({ \ + [OCMMacroState beginExpectMacro]; \ + invocation; \ + [OCMMacroState endExpectMacro]; \ +}) + +#define ClassMethod(invocation) \ + [[OCMMacroState globalState] switchToClassMethod]; \ + invocation; + + +#define OCMVerifyAll(mock) [mock verifyAtLocation:OCMMakeLocation(self, __FILE__, __LINE__)] + +#define OCMVerifyAllWithDelay(mock, delay) [mock verifyWithDelay:delay atLocation:OCMMakeLocation(self, __FILE__, __LINE__)] + +#define OCMVerify(invocation) \ +({ \ + [OCMMacroState beginVerifyMacroAtLocation:OCMMakeLocation(self, __FILE__, __LINE__)]; \ + invocation; \ + [OCMMacroState endVerifyMacro]; \ +}) diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/usr/include/OCMock/OCMockObject.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/usr/include/OCMock/OCMockObject.h new file mode 100644 index 0000000000..9ef1c78b77 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/usr/include/OCMock/OCMockObject.h @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2004-2014 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@class OCMLocation; +@class OCMockRecorder; +@class OCMInvocationMatcher; + + +@interface OCMockObject : NSProxy +{ + BOOL isNice; + BOOL expectationOrderMatters; + NSMutableArray *recorders; + NSMutableArray *expectations; + NSMutableArray *rejections; + NSMutableArray *exceptions; + NSMutableArray *invocations; +} + ++ (id)mockForClass:(Class)aClass; ++ (id)mockForProtocol:(Protocol *)aProtocol; ++ (id)partialMockForObject:(NSObject *)anObject; + ++ (id)niceMockForClass:(Class)aClass; ++ (id)niceMockForProtocol:(Protocol *)aProtocol; + ++ (id)observerMock; + +- (id)init; + +- (void)setExpectationOrderMatters:(BOOL)flag; + +- (id)stub; +- (id)expect; +- (id)reject; + +- (id)verify; +- (id)verifyAtLocation:(OCMLocation *)location; + +- (void)verifyWithDelay:(NSTimeInterval)delay; +- (void)verifyWithDelay:(NSTimeInterval)delay atLocation:(OCMLocation *)location; + +- (void)stopMocking; + +// internal use only + +- (void)prepareForMockingMethod:(__unused SEL)aSelector; +- (void)prepareForMockingClassMethod:(__unused SEL)aSelector; + +- (BOOL)handleInvocation:(NSInvocation *)anInvocation; +- (void)handleUnRecordedInvocation:(NSInvocation *)anInvocation; +- (BOOL)handleSelector:(SEL)sel; + +- (void)verifyInvocation:(OCMInvocationMatcher *)matcher; +- (void)verifyInvocation:(OCMInvocationMatcher *)matcher atLocation:(OCMLocation *)location; + +@end + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/usr/include/OCMock/OCMockRecorder.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/usr/include/OCMock/OCMockRecorder.h new file mode 100644 index 0000000000..ba066f919c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/usr/include/OCMock/OCMockRecorder.h @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2004-2014 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@class OCMockObject; +@class OCMInvocationMatcher; + + +@interface OCMockRecorder : NSProxy +{ + OCMockObject *mockObject; + OCMInvocationMatcher *invocationMatcher; + NSMutableArray *invocationHandlers; +} + +- (id)initWithMockObject:(OCMockObject *)aMockObject; + +//- (void)releaseInvocation; + +- (id)andReturn:(id)anObject; +- (id)andReturnValue:(NSValue *)aValue; +- (id)andThrow:(NSException *)anException; +- (id)andPost:(NSNotification *)aNotification; +- (id)andCall:(SEL)selector onObject:(id)anObject; +- (id)andDo:(void (^)(NSInvocation *invocation))block; +- (id)andForwardToRealObject; + +- (id)classMethod; +- (id)ignoringNonObjectArgs; + +- (OCMInvocationMatcher *)invocationMatcher; + +- (void)addInvocationHandler:(id)aHandler; +- (NSArray *)invocationHandlers; + +@end + + +@interface OCMockRecorder(Properties) + +#define andReturn(aValue) _andReturn(({ typeof(aValue) _v = (aValue); [NSValue value:&_v withObjCType:@encode(typeof(_v))]; })) +@property (nonatomic, readonly) OCMockRecorder *(^ _andReturn)(NSValue *); + +#define andThrow(anException) _andThrow(anException) +@property (nonatomic, readonly) OCMockRecorder *(^ _andThrow)(NSException *); + +#define andPost(aNotification) _andPost(aNotification) +@property (nonatomic, readonly) OCMockRecorder *(^ _andPost)(NSNotification *); + +#define andCall(anObject, aSelector) _andCall(anObject, aSelector) +@property (nonatomic, readonly) OCMockRecorder *(^ _andCall)(id, SEL); + +#define andDo(aBlock) _andDo(aBlock) +@property (nonatomic, readonly) OCMockRecorder *(^ _andDo)(void (^)(NSInvocation *)); + +#define andForwardToRealObject() _andForwardToRealObject() +@property (nonatomic, readonly) OCMockRecorder *(^ _andForwardToRealObject)(void); + +@end + + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/usr/lib/libOCMock.a b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/usr/lib/libOCMock.a new file mode 100644 index 0000000000..df3c5c4a68 Binary files /dev/null and b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/SwiftExamples/usr/lib/libOCMock.a differ diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example.xcodeproj/project.pbxproj b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..c87b976add --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example.xcodeproj/project.pbxproj @@ -0,0 +1,505 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 0395293E15043C7A00D25639 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0395293D15043C7A00D25639 /* UIKit.framework */; }; + 0395294015043C7A00D25639 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0395293F15043C7A00D25639 /* Foundation.framework */; }; + 0395294215043C7A00D25639 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0395294115043C7A00D25639 /* CoreGraphics.framework */; }; + 0395294815043C7A00D25639 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0395294615043C7A00D25639 /* InfoPlist.strings */; }; + 0395294A15043C7A00D25639 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 0395294915043C7A00D25639 /* main.m */; }; + 0395294E15043C7A00D25639 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 0395294D15043C7A00D25639 /* AppDelegate.m */; }; + 0395295115043C7A00D25639 /* MainStoryboard_iPhone.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0395294F15043C7A00D25639 /* MainStoryboard_iPhone.storyboard */; }; + 0395295415043C7A00D25639 /* MainStoryboard_iPad.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0395295215043C7A00D25639 /* MainStoryboard_iPad.storyboard */; }; + 0395295715043C7A00D25639 /* MasterViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 0395295615043C7A00D25639 /* MasterViewController.m */; }; + 0395295A15043C7A00D25639 /* DetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 0395295915043C7A00D25639 /* DetailViewController.m */; }; + 0395296215043C7A00D25639 /* SenTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0395296115043C7A00D25639 /* SenTestingKit.framework */; }; + 0395296315043C7A00D25639 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0395293D15043C7A00D25639 /* UIKit.framework */; }; + 0395296415043C7A00D25639 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0395293F15043C7A00D25639 /* Foundation.framework */; }; + 0395296C15043C7A00D25639 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0395296A15043C7A00D25639 /* InfoPlist.strings */; }; + 0395296F15043C7A00D25639 /* iOS5ExampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0395296E15043C7A00D25639 /* iOS5ExampleTests.m */; }; + 0395297915043CF000D25639 /* libOCMock.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0395297815043CF000D25639 /* libOCMock.a */; }; + 03FA415E1614FCFD00F9324F /* ProtocolTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03FA415D1614FCFD00F9324F /* ProtocolTests.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 0395296515043C7A00D25639 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0395293015043C7A00D25639 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 0395293815043C7A00D25639; + remoteInfo = iOS5Example; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 0395293915043C7A00D25639 /* iOS5Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iOS5Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 0395293D15043C7A00D25639 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + 0395293F15043C7A00D25639 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + 0395294115043C7A00D25639 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + 0395294515043C7A00D25639 /* iOS5Example-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "iOS5Example-Info.plist"; sourceTree = ""; }; + 0395294715043C7A00D25639 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 0395294915043C7A00D25639 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 0395294B15043C7A00D25639 /* iOS5Example-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "iOS5Example-Prefix.pch"; sourceTree = ""; }; + 0395294C15043C7A00D25639 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 0395294D15043C7A00D25639 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 0395295015043C7A00D25639 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = en; path = en.lproj/MainStoryboard_iPhone.storyboard; sourceTree = ""; }; + 0395295315043C7A00D25639 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = en; path = en.lproj/MainStoryboard_iPad.storyboard; sourceTree = ""; }; + 0395295515043C7A00D25639 /* MasterViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MasterViewController.h; sourceTree = ""; }; + 0395295615043C7A00D25639 /* MasterViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MasterViewController.m; sourceTree = ""; }; + 0395295815043C7A00D25639 /* DetailViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DetailViewController.h; sourceTree = ""; }; + 0395295915043C7A00D25639 /* DetailViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DetailViewController.m; sourceTree = ""; }; + 0395296015043C7A00D25639 /* iOS5ExampleTests.octest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = iOS5ExampleTests.octest; sourceTree = BUILT_PRODUCTS_DIR; }; + 0395296115043C7A00D25639 /* SenTestingKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SenTestingKit.framework; path = Library/Frameworks/SenTestingKit.framework; sourceTree = DEVELOPER_DIR; }; + 0395296915043C7A00D25639 /* iOS5ExampleTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "iOS5ExampleTests-Info.plist"; sourceTree = ""; }; + 0395296B15043C7A00D25639 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 0395296D15043C7A00D25639 /* iOS5ExampleTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = iOS5ExampleTests.h; sourceTree = ""; }; + 0395296E15043C7A00D25639 /* iOS5ExampleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = iOS5ExampleTests.m; sourceTree = ""; }; + 0395297815043CF000D25639 /* libOCMock.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libOCMock.a; path = usr/lib/libOCMock.a; sourceTree = ""; }; + 03FA415C1614FCFD00F9324F /* ProtocolTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ProtocolTests.h; sourceTree = ""; }; + 03FA415D1614FCFD00F9324F /* ProtocolTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ProtocolTests.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 0395293615043C7A00D25639 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0395293E15043C7A00D25639 /* UIKit.framework in Frameworks */, + 0395294015043C7A00D25639 /* Foundation.framework in Frameworks */, + 0395294215043C7A00D25639 /* CoreGraphics.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0395295C15043C7A00D25639 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0395297915043CF000D25639 /* libOCMock.a in Frameworks */, + 0395296215043C7A00D25639 /* SenTestingKit.framework in Frameworks */, + 0395296315043C7A00D25639 /* UIKit.framework in Frameworks */, + 0395296415043C7A00D25639 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0395292E15043C7A00D25639 = { + isa = PBXGroup; + children = ( + 0395294315043C7A00D25639 /* iOS5Example */, + 0395296715043C7A00D25639 /* iOS5ExampleTests */, + 0395297A15043D8600D25639 /* Libraries */, + 0395293C15043C7A00D25639 /* Frameworks */, + 0395293A15043C7A00D25639 /* Products */, + ); + sourceTree = ""; + }; + 0395293A15043C7A00D25639 /* Products */ = { + isa = PBXGroup; + children = ( + 0395293915043C7A00D25639 /* iOS5Example.app */, + 0395296015043C7A00D25639 /* iOS5ExampleTests.octest */, + ); + name = Products; + sourceTree = ""; + }; + 0395293C15043C7A00D25639 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 0395293D15043C7A00D25639 /* UIKit.framework */, + 0395293F15043C7A00D25639 /* Foundation.framework */, + 0395294115043C7A00D25639 /* CoreGraphics.framework */, + 0395296115043C7A00D25639 /* SenTestingKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 0395294315043C7A00D25639 /* iOS5Example */ = { + isa = PBXGroup; + children = ( + 0395294C15043C7A00D25639 /* AppDelegate.h */, + 0395294D15043C7A00D25639 /* AppDelegate.m */, + 0395294F15043C7A00D25639 /* MainStoryboard_iPhone.storyboard */, + 0395295215043C7A00D25639 /* MainStoryboard_iPad.storyboard */, + 0395295515043C7A00D25639 /* MasterViewController.h */, + 0395295615043C7A00D25639 /* MasterViewController.m */, + 0395295815043C7A00D25639 /* DetailViewController.h */, + 0395295915043C7A00D25639 /* DetailViewController.m */, + 0395294415043C7A00D25639 /* Supporting Files */, + ); + path = iOS5Example; + sourceTree = ""; + }; + 0395294415043C7A00D25639 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 0395294515043C7A00D25639 /* iOS5Example-Info.plist */, + 0395294615043C7A00D25639 /* InfoPlist.strings */, + 0395294915043C7A00D25639 /* main.m */, + 0395294B15043C7A00D25639 /* iOS5Example-Prefix.pch */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 0395296715043C7A00D25639 /* iOS5ExampleTests */ = { + isa = PBXGroup; + children = ( + 0395296D15043C7A00D25639 /* iOS5ExampleTests.h */, + 0395296E15043C7A00D25639 /* iOS5ExampleTests.m */, + 03FA415C1614FCFD00F9324F /* ProtocolTests.h */, + 03FA415D1614FCFD00F9324F /* ProtocolTests.m */, + 0395296815043C7A00D25639 /* Supporting Files */, + ); + path = iOS5ExampleTests; + sourceTree = ""; + }; + 0395296815043C7A00D25639 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 0395296915043C7A00D25639 /* iOS5ExampleTests-Info.plist */, + 0395296A15043C7A00D25639 /* InfoPlist.strings */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 0395297A15043D8600D25639 /* Libraries */ = { + isa = PBXGroup; + children = ( + 0395297815043CF000D25639 /* libOCMock.a */, + ); + name = Libraries; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 0395293815043C7A00D25639 /* iOS5Example */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0395297215043C7A00D25639 /* Build configuration list for PBXNativeTarget "iOS5Example" */; + buildPhases = ( + 0395293515043C7A00D25639 /* Sources */, + 0395293615043C7A00D25639 /* Frameworks */, + 0395293715043C7A00D25639 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = iOS5Example; + productName = iOS5Example; + productReference = 0395293915043C7A00D25639 /* iOS5Example.app */; + productType = "com.apple.product-type.application"; + }; + 0395295F15043C7A00D25639 /* iOS5ExampleTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0395297515043C7A00D25639 /* Build configuration list for PBXNativeTarget "iOS5ExampleTests" */; + buildPhases = ( + 0395295B15043C7A00D25639 /* Sources */, + 0395295C15043C7A00D25639 /* Frameworks */, + 0395295D15043C7A00D25639 /* Resources */, + 0395295E15043C7A00D25639 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 0395296615043C7A00D25639 /* PBXTargetDependency */, + ); + name = iOS5ExampleTests; + productName = iOS5ExampleTests; + productReference = 0395296015043C7A00D25639 /* iOS5ExampleTests.octest */; + productType = "com.apple.product-type.bundle"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 0395293015043C7A00D25639 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0420; + ORGANIZATIONNAME = ThoughtWorks; + }; + buildConfigurationList = 0395293315043C7A00D25639 /* Build configuration list for PBXProject "iOS5Example" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 0395292E15043C7A00D25639; + productRefGroup = 0395293A15043C7A00D25639 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 0395293815043C7A00D25639 /* iOS5Example */, + 0395295F15043C7A00D25639 /* iOS5ExampleTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 0395293715043C7A00D25639 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0395294815043C7A00D25639 /* InfoPlist.strings in Resources */, + 0395295115043C7A00D25639 /* MainStoryboard_iPhone.storyboard in Resources */, + 0395295415043C7A00D25639 /* MainStoryboard_iPad.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0395295D15043C7A00D25639 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0395296C15043C7A00D25639 /* InfoPlist.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0395295E15043C7A00D25639 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Run the unit tests in this test bundle.\n\"${SYSTEM_DEVELOPER_DIR}/Tools/RunUnitTests\"\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 0395293515043C7A00D25639 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0395294A15043C7A00D25639 /* main.m in Sources */, + 0395294E15043C7A00D25639 /* AppDelegate.m in Sources */, + 0395295715043C7A00D25639 /* MasterViewController.m in Sources */, + 0395295A15043C7A00D25639 /* DetailViewController.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0395295B15043C7A00D25639 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0395296F15043C7A00D25639 /* iOS5ExampleTests.m in Sources */, + 03FA415E1614FCFD00F9324F /* ProtocolTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 0395296615043C7A00D25639 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 0395293815043C7A00D25639 /* iOS5Example */; + targetProxy = 0395296515043C7A00D25639 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 0395294615043C7A00D25639 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 0395294715043C7A00D25639 /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + 0395294F15043C7A00D25639 /* MainStoryboard_iPhone.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 0395295015043C7A00D25639 /* en */, + ); + name = MainStoryboard_iPhone.storyboard; + sourceTree = ""; + }; + 0395295215043C7A00D25639 /* MainStoryboard_iPad.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 0395295315043C7A00D25639 /* en */, + ); + name = MainStoryboard_iPad.storyboard; + sourceTree = ""; + }; + 0395296A15043C7A00D25639 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 0395296B15043C7A00D25639 /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 0395297015043C7A00D25639 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD_32_BIT)"; + CLANG_ENABLE_OBJC_ARC = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_VERSION = com.apple.compilers.llvm.clang.1_0; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 5.0; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 0395297115043C7A00D25639 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD_32_BIT)"; + CLANG_ENABLE_OBJC_ARC = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_VERSION = com.apple.compilers.llvm.clang.1_0; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 5.0; + OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 0395297315043C7A00D25639 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "iOS5Example/iOS5Example-Prefix.pch"; + INFOPLIST_FILE = "iOS5Example/iOS5Example-Info.plist"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = app; + }; + name = Debug; + }; + 0395297415043C7A00D25639 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "iOS5Example/iOS5Example-Prefix.pch"; + INFOPLIST_FILE = "iOS5Example/iOS5Example-Info.plist"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = app; + }; + name = Release; + }; + 0395297615043C7A00D25639 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/iOS5Example.app/iOS5Example"; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(DEVELOPER_LIBRARY_DIR)/Frameworks", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "iOS5Example/iOS5Example-Prefix.pch"; + HEADER_SEARCH_PATHS = "\"$(SRCROOT)/usr/include\""; + INFOPLIST_FILE = "iOS5ExampleTests/iOS5ExampleTests-Info.plist"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/usr/lib\"", + ); + OTHER_LDFLAGS = ( + "-force_load", + "\"$(SRCROOT)/usr/lib/libOCMock.a\"", + "-ObjC", + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUNDLE_LOADER)"; + WRAPPER_EXTENSION = octest; + }; + name = Debug; + }; + 0395297715043C7A00D25639 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/iOS5Example.app/iOS5Example"; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(DEVELOPER_LIBRARY_DIR)/Frameworks", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "iOS5Example/iOS5Example-Prefix.pch"; + HEADER_SEARCH_PATHS = "\"$(SRCROOT)/usr/include\""; + INFOPLIST_FILE = "iOS5ExampleTests/iOS5ExampleTests-Info.plist"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/usr/lib\"", + ); + OTHER_LDFLAGS = ( + "-force_load", + "\"$(SRCROOT)/usr/lib/libOCMock.a\"", + "-ObjC", + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUNDLE_LOADER)"; + WRAPPER_EXTENSION = octest; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 0395293315043C7A00D25639 /* Build configuration list for PBXProject "iOS5Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0395297015043C7A00D25639 /* Debug */, + 0395297115043C7A00D25639 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 0395297215043C7A00D25639 /* Build configuration list for PBXNativeTarget "iOS5Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0395297315043C7A00D25639 /* Debug */, + 0395297415043C7A00D25639 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 0395297515043C7A00D25639 /* Build configuration list for PBXNativeTarget "iOS5ExampleTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0395297615043C7A00D25639 /* Debug */, + 0395297715043C7A00D25639 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 0395293015043C7A00D25639 /* Project object */; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/AppDelegate.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/AppDelegate.h new file mode 100644 index 0000000000..3d820dfb12 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/AppDelegate.h @@ -0,0 +1,12 @@ +// +// AppDelegate.h +// iOS5Example +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/AppDelegate.m b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/AppDelegate.m new file mode 100644 index 0000000000..74663ab43a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/AppDelegate.m @@ -0,0 +1,62 @@ +// +// AppDelegate.m +// iOS5Example +// + +#import "AppDelegate.h" + +@implementation AppDelegate + +@synthesize window = _window; + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + // Override point for customization after application launch. + if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController; + UINavigationController *navigationController = [splitViewController.viewControllers lastObject]; + splitViewController.delegate = (id)navigationController.topViewController; + } + return YES; +} + +- (void)applicationWillResignActive:(UIApplication *)application +{ + /* + Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. + */ +} + +- (void)applicationDidEnterBackground:(UIApplication *)application +{ + /* + Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + */ +} + +- (void)applicationWillEnterForeground:(UIApplication *)application +{ + /* + Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. + */ +} + +- (void)applicationDidBecomeActive:(UIApplication *)application +{ + /* + Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + */ +} + +- (void)applicationWillTerminate:(UIApplication *)application +{ + /* + Called when the application is about to terminate. + Save data if appropriate. + See also applicationDidEnterBackground:. + */ +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/DetailViewController.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/DetailViewController.h new file mode 100644 index 0000000000..c313806f20 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/DetailViewController.h @@ -0,0 +1,14 @@ +// +// DetailViewController.h +// iOS5Example +// + +#import + +@interface DetailViewController : UIViewController + +@property (strong, nonatomic) id detailItem; + +@property (strong, nonatomic) IBOutlet UILabel *detailDescriptionLabel; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/DetailViewController.m b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/DetailViewController.m new file mode 100644 index 0000000000..606f9dc5a5 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/DetailViewController.m @@ -0,0 +1,112 @@ +// +// DetailViewController.m +// iOS5Example +// + +#import "DetailViewController.h" + +@interface DetailViewController () +@property (strong, nonatomic) UIPopoverController *masterPopoverController; +- (void)configureView; +@end + +@implementation DetailViewController + +@synthesize detailItem = _detailItem; +@synthesize detailDescriptionLabel = _detailDescriptionLabel; +@synthesize masterPopoverController = _masterPopoverController; + +#pragma mark - Managing the detail item + +- (void)setDetailItem:(id)newDetailItem +{ + if (_detailItem != newDetailItem) { + _detailItem = newDetailItem; + + // Update the view. + [self configureView]; + } + + if (self.masterPopoverController != nil) { + [self.masterPopoverController dismissPopoverAnimated:YES]; + } +} + +- (void)configureView +{ + // Update the user interface for the detail item. + + if (self.detailItem) { + self.detailDescriptionLabel.text = [self.detailItem description]; + } +} + +- (void)didReceiveMemoryWarning +{ + [super didReceiveMemoryWarning]; + // Release any cached data, images, etc that aren't in use. +} + +#pragma mark - View lifecycle + +- (void)viewDidLoad +{ + [super viewDidLoad]; + // Do any additional setup after loading the view, typically from a nib. + [self configureView]; +} + +- (void)viewDidUnload +{ + [super viewDidUnload]; + // Release any retained subviews of the main view. + // e.g. self.myOutlet = nil; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; +} + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; +} + +- (void)viewWillDisappear:(BOOL)animated +{ + [super viewWillDisappear:animated]; +} + +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; +} + +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation +{ + // Return YES for supported orientations + if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) { + return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown); + } else { + return YES; + } +} + +#pragma mark - Split view + +- (void)splitViewController:(UISplitViewController *)splitController willHideViewController:(UIViewController *)viewController withBarButtonItem:(UIBarButtonItem *)barButtonItem forPopoverController:(UIPopoverController *)popoverController +{ + barButtonItem.title = NSLocalizedString(@"Master", @"Master"); + [self.navigationItem setLeftBarButtonItem:barButtonItem animated:YES]; + self.masterPopoverController = popoverController; +} + +- (void)splitViewController:(UISplitViewController *)splitController willShowViewController:(UIViewController *)viewController invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem +{ + // Called when the view is shown again in the split view, invalidating the button and popover controller. + [self.navigationItem setLeftBarButtonItem:nil animated:YES]; + self.masterPopoverController = nil; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/MasterViewController.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/MasterViewController.h new file mode 100644 index 0000000000..44e95c672a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/MasterViewController.h @@ -0,0 +1,14 @@ +// +// MasterViewController.h +// iOS5Example +// + +#import + +@class DetailViewController; + +@interface MasterViewController : UITableViewController + +@property (strong, nonatomic) DetailViewController *detailViewController; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/MasterViewController.m b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/MasterViewController.m new file mode 100644 index 0000000000..0a9934b6b9 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/MasterViewController.m @@ -0,0 +1,116 @@ +// +// MasterViewController.m +// iOS5Example +// + +#import "MasterViewController.h" + +#import "DetailViewController.h" + +@implementation MasterViewController + +@synthesize detailViewController = _detailViewController; + +- (void)awakeFromNib +{ + if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + self.clearsSelectionOnViewWillAppear = NO; + self.contentSizeForViewInPopover = CGSizeMake(320.0, 600.0); + } + [super awakeFromNib]; +} + +- (void)didReceiveMemoryWarning +{ + [super didReceiveMemoryWarning]; + // Release any cached data, images, etc that aren't in use. +} + +#pragma mark - View lifecycle + +- (void)viewDidLoad +{ + [super viewDidLoad]; + // Do any additional setup after loading the view, typically from a nib. + self.detailViewController = (DetailViewController *)[[self.splitViewController.viewControllers lastObject] topViewController]; + if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + [self.tableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] animated:NO scrollPosition:UITableViewScrollPositionMiddle]; + } +} + +- (void)viewDidUnload +{ + [super viewDidUnload]; + // Release any retained subviews of the main view. + // e.g. self.myOutlet = nil; +} + +- (void)viewWillAppear:(BOOL)animated +{ + [super viewWillAppear:animated]; +} + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; +} + +- (void)viewWillDisappear:(BOOL)animated +{ + [super viewWillDisappear:animated]; +} + +- (void)viewDidDisappear:(BOOL)animated +{ + [super viewDidDisappear:animated]; +} + +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation +{ + // Return YES for supported orientations + if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) { + return (interfaceOrientation != UIInterfaceOrientationPortraitUpsideDown); + } else { + return YES; + } +} + +/* +// Override to support conditional editing of the table view. +- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath +{ + // Return NO if you do not want the specified item to be editable. + return YES; +} +*/ + + +// Override to support editing the table view. +- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (editingStyle == UITableViewCellEditingStyleDelete) { + // Delete the row from the data source. + [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; + } else if (editingStyle == UITableViewCellEditingStyleInsert) { + // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view. + } +} + + +/* +// Override to support rearranging the table view. +- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath +{ +} +*/ + +/* +// Override to support conditional rearranging of the table view. +- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath +{ + // Return NO if you do not want the item to be re-orderable. + return YES; +} +*/ + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/en.lproj/InfoPlist.strings b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..477b28ff8f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/en.lproj/MainStoryboard_iPad.storyboard b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/en.lproj/MainStoryboard_iPad.storyboard new file mode 100644 index 0000000000..5d5deaf1fb --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/en.lproj/MainStoryboard_iPad.storyboard @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/en.lproj/MainStoryboard_iPhone.storyboard b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/en.lproj/MainStoryboard_iPhone.storyboard new file mode 100644 index 0000000000..fa1b905b98 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/en.lproj/MainStoryboard_iPhone.storyboard @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/iOS5Example-Info.plist b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/iOS5Example-Info.plist new file mode 100644 index 0000000000..7f6794daa8 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/iOS5Example-Info.plist @@ -0,0 +1,51 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${PRODUCT_NAME} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIconFiles + + CFBundleIdentifier + com.mulle-kybernetik.ocmock.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + LSRequiresIPhoneOS + + UIMainStoryboardFile + MainStoryboard_iPhone + UIMainStoryboardFile~ipad + MainStoryboard_iPad + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/iOS5Example-Prefix.pch b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/iOS5Example-Prefix.pch new file mode 100644 index 0000000000..a52d8b5bcf --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/iOS5Example-Prefix.pch @@ -0,0 +1,14 @@ +// +// Prefix header for all source files of the 'iOS5Example' target in the 'iOS5Example' project +// + +#import + +#ifndef __IPHONE_5_0 +#warning "This project uses features only available in iOS SDK 5.0 and later." +#endif + +#ifdef __OBJC__ + #import + #import +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/main.m b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/main.m new file mode 100644 index 0000000000..bf2dfbdce1 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5Example/main.m @@ -0,0 +1,15 @@ +// +// main.m +// iOS5Example +// + +#import + +#import "AppDelegate.h" + +int main(int argc, char *argv[]) +{ + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5ExampleTests/ProtocolTests.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5ExampleTests/ProtocolTests.h new file mode 100644 index 0000000000..3d4b89f62e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5ExampleTests/ProtocolTests.h @@ -0,0 +1,10 @@ +// +// ProtocolTests.h +// iOS5Example +// + +#import + +@interface ProtocolTests : SenTestCase + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5ExampleTests/ProtocolTests.m b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5ExampleTests/ProtocolTests.m new file mode 100644 index 0000000000..4a3dd1dd5d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5ExampleTests/ProtocolTests.m @@ -0,0 +1,56 @@ +// +// ProtocolTests.m +// iOS5Example +// + +#import +#import "ProtocolTests.h" + + +@protocol AuthenticationServiceProtocol + +- (void)loginWithEmail:(NSString *)email andPassword:(NSString *)password; + +@end + +@interface Foo : NSObject +{ + id authService; +} + +- (id)initWithAuthenticationService:(id)anAuthService; +- (void)doStuff; + +@end + +@implementation Foo + +- (id)initWithAuthenticationService:(id)anAuthService +{ + self = [super init]; + authService = anAuthService; + return self; +} + +- (void)doStuff +{ + [authService loginWithEmail:@"x" andPassword:@"y"]; +} + +@end + +@implementation ProtocolTests + +- (void)testTheProtocol +{ + id authService = [OCMockObject mockForProtocol:@protocol(AuthenticationServiceProtocol)]; + id foo = [[Foo alloc] initWithAuthenticationService:authService]; + + [[authService expect] loginWithEmail:[OCMArg any] andPassword:[OCMArg any]]; + + [foo doStuff]; + + [authService verify]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5ExampleTests/en.lproj/InfoPlist.strings b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5ExampleTests/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..477b28ff8f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5ExampleTests/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5ExampleTests/iOS5ExampleTests-Info.plist b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5ExampleTests/iOS5ExampleTests-Info.plist new file mode 100644 index 0000000000..b066986d46 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5ExampleTests/iOS5ExampleTests-Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + com.mulle-kybernetik.ocmock.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5ExampleTests/iOS5ExampleTests.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5ExampleTests/iOS5ExampleTests.h new file mode 100644 index 0000000000..4012ffd062 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5ExampleTests/iOS5ExampleTests.h @@ -0,0 +1,10 @@ +// +// iOS5ExampleTests.h +// iOS5ExampleTests +// + +#import + +@interface iOS5ExampleTests : SenTestCase + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5ExampleTests/iOS5ExampleTests.m b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5ExampleTests/iOS5ExampleTests.m new file mode 100644 index 0000000000..eeab0ecbec --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/iOS5ExampleTests/iOS5ExampleTests.m @@ -0,0 +1,47 @@ +// +// iOS5ExampleTests.m +// iOS5ExampleTests +// + +#import +#import "MasterViewController.h" +#import "iOS5ExampleTests.h" + +@implementation iOS5ExampleTests + +- (void)setUp +{ + [super setUp]; + + // Set-up code here. +} + +- (void)tearDown +{ + // Tear-down code here. + + [super tearDown]; +} + +- (void)testMasterViewControllerDeletesItemsFromTableView +{ + // Test set-up + + MasterViewController *controller = [[MasterViewController alloc] init]; + NSIndexPath *dummyIndexPath = [NSIndexPath indexPathWithIndex:3]; + id tableViewMock = [OCMockObject mockForClass:[UITableView class]]; + [[tableViewMock expect] deleteRowsAtIndexPaths:[NSArray arrayWithObject:dummyIndexPath] withRowAnimation:UITableViewRowAnimationFade]; + + // Invoke functionality to be tested + // If you want to see the test fail you can, for example, change the editing style to UITableViewCellEditingStyleNone. In + // that case the method in the controller does not make a call to the table view and the mock will raise an exception when + // verify is called further down. + + [controller tableView:tableViewMock commitEditingStyle:UITableViewCellEditingStyleDelete forRowAtIndexPath:dummyIndexPath]; + + // Verify that expectations were met + + [tableViewMock verify]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/usr/include/OCMock/NSNotificationCenter+OCMAdditions.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/usr/include/OCMock/NSNotificationCenter+OCMAdditions.h new file mode 100644 index 0000000000..ab4832bbfb --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/usr/include/OCMock/NSNotificationCenter+OCMAdditions.h @@ -0,0 +1,15 @@ +//--------------------------------------------------------------------------------------- +// $Id$ +// Copyright (c) 2009 by Mulle Kybernetik. See License file for details. +//--------------------------------------------------------------------------------------- + +#import + +@class OCMockObserver; + + +@interface NSNotificationCenter(OCMAdditions) + +- (void)addMockObserver:(OCMockObserver *)notificationObserver name:(NSString *)notificationName object:(id)notificationSender; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/usr/include/OCMock/OCMArg.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/usr/include/OCMock/OCMArg.h new file mode 100644 index 0000000000..669c094878 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/usr/include/OCMock/OCMArg.h @@ -0,0 +1,33 @@ +//--------------------------------------------------------------------------------------- +// $Id$ +// Copyright (c) 2009-2010 by Mulle Kybernetik. See License file for details. +//--------------------------------------------------------------------------------------- + +#import + +@interface OCMArg : NSObject + +// constraining arguments + ++ (id)any; ++ (void *)anyPointer; ++ (id)isNil; ++ (id)isNotNil; ++ (id)isNotEqual:(id)value; ++ (id)checkWithSelector:(SEL)selector onObject:(id)anObject; +#if NS_BLOCKS_AVAILABLE ++ (id)checkWithBlock:(BOOL (^)(id))block; +#endif + +// manipulating arguments + ++ (id *)setTo:(id)value; + +// internal use only + ++ (id)resolveSpecialValues:(NSValue *)value; + +@end + +#define OCMOCK_ANY [OCMArg any] +#define OCMOCK_VALUE(variable) [NSValue value:&variable withObjCType:@encode(__typeof__(variable))] diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/usr/include/OCMock/OCMConstraint.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/usr/include/OCMock/OCMConstraint.h new file mode 100644 index 0000000000..3ae1264603 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/usr/include/OCMock/OCMConstraint.h @@ -0,0 +1,64 @@ +//--------------------------------------------------------------------------------------- +// $Id$ +// Copyright (c) 2007-2010 by Mulle Kybernetik. See License file for details. +//--------------------------------------------------------------------------------------- + +#import + + +@interface OCMConstraint : NSObject + ++ (id)constraint; +- (BOOL)evaluate:(id)value; + +// if you are looking for any, isNil, etc, they have moved to OCMArg + +// try to use [OCMArg checkWith...] instead of the constraintWith... methods below + ++ (id)constraintWithSelector:(SEL)aSelector onObject:(id)anObject; ++ (id)constraintWithSelector:(SEL)aSelector onObject:(id)anObject withValue:(id)aValue; + + +@end + +@interface OCMAnyConstraint : OCMConstraint +@end + +@interface OCMIsNilConstraint : OCMConstraint +@end + +@interface OCMIsNotNilConstraint : OCMConstraint +@end + +@interface OCMIsNotEqualConstraint : OCMConstraint +{ + @public + id testValue; +} + +@end + +@interface OCMInvocationConstraint : OCMConstraint +{ + @public + NSInvocation *invocation; +} + +@end + +#if NS_BLOCKS_AVAILABLE + +@interface OCMBlockConstraint : OCMConstraint +{ + BOOL (^block)(id); +} + +- (id)initWithConstraintBlock:(BOOL (^)(id))block; + +@end + +#endif + + +#define CONSTRAINT(aSelector) [OCMConstraint constraintWithSelector:aSelector onObject:self] +#define CONSTRAINTV(aSelector, aValue) [OCMConstraint constraintWithSelector:aSelector onObject:self withValue:(aValue)] diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/usr/include/OCMock/OCMock.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/usr/include/OCMock/OCMock.h new file mode 100644 index 0000000000..e18de58a1d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/usr/include/OCMock/OCMock.h @@ -0,0 +1,10 @@ +//--------------------------------------------------------------------------------------- +// $Id$ +// Copyright (c) 2004-2008 by Mulle Kybernetik. See License file for details. +//--------------------------------------------------------------------------------------- + +#import +#import +#import +#import +#import diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/usr/include/OCMock/OCMockObject.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/usr/include/OCMock/OCMockObject.h new file mode 100644 index 0000000000..ebd2ba35e3 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/usr/include/OCMock/OCMockObject.h @@ -0,0 +1,43 @@ +//--------------------------------------------------------------------------------------- +// $Id$ +// Copyright (c) 2004-2008 by Mulle Kybernetik. See License file for details. +//--------------------------------------------------------------------------------------- + +#import + +@interface OCMockObject : NSProxy +{ + BOOL isNice; + BOOL expectationOrderMatters; + NSMutableArray *recorders; + NSMutableArray *expectations; + NSMutableArray *rejections; + NSMutableArray *exceptions; +} + ++ (id)mockForClass:(Class)aClass; ++ (id)mockForProtocol:(Protocol *)aProtocol; ++ (id)partialMockForObject:(NSObject *)anObject; + ++ (id)niceMockForClass:(Class)aClass; ++ (id)niceMockForProtocol:(Protocol *)aProtocol; + ++ (id)observerMock; + +- (id)init; + +- (void)setExpectationOrderMatters:(BOOL)flag; + +- (id)stub; +- (id)expect; +- (id)reject; + +- (void)verify; + +// internal use only + +- (id)getNewRecorder; +- (BOOL)handleInvocation:(NSInvocation *)anInvocation; +- (void)handleUnRecordedInvocation:(NSInvocation *)anInvocation; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/usr/include/OCMock/OCMockRecorder.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/usr/include/OCMock/OCMockRecorder.h new file mode 100644 index 0000000000..b11a25387f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/usr/include/OCMock/OCMockRecorder.h @@ -0,0 +1,32 @@ +//--------------------------------------------------------------------------------------- +// $Id$ +// Copyright (c) 2004-2010 by Mulle Kybernetik. See License file for details. +//--------------------------------------------------------------------------------------- + +#import + +@interface OCMockRecorder : NSProxy +{ + id signatureResolver; + NSInvocation *recordedInvocation; + NSMutableArray *invocationHandlers; +} + +- (id)initWithSignatureResolver:(id)anObject; + +- (BOOL)matchesInvocation:(NSInvocation *)anInvocation; +- (void)releaseInvocation; + +- (id)andReturn:(id)anObject; +- (id)andReturnValue:(NSValue *)aValue; +- (id)andThrow:(NSException *)anException; +- (id)andPost:(NSNotification *)aNotification; +- (id)andCall:(SEL)selector onObject:(id)anObject; +#if NS_BLOCKS_AVAILABLE +- (id)andDo:(void (^)(NSInvocation *))block; +#endif +- (id)andForwardToRealObject; + +- (NSArray *)invocationHandlers; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/usr/lib/libOCMock.a b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/usr/lib/libOCMock.a new file mode 100644 index 0000000000..dae5ac185a Binary files /dev/null and b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS5Example/usr/lib/libOCMock.a differ diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example.xcodeproj/project.pbxproj b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..a1ae51df5d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example.xcodeproj/project.pbxproj @@ -0,0 +1,533 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 03C231581946D3CC00F90643 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03C231571946D3CC00F90643 /* Foundation.framework */; }; + 03C2315C1946D3CC00F90643 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03C2315B1946D3CC00F90643 /* UIKit.framework */; }; + 03C231621946D3CC00F90643 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 03C231601946D3CC00F90643 /* InfoPlist.strings */; }; + 03C231641946D3CC00F90643 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 03C231631946D3CC00F90643 /* main.m */; }; + 03C231681946D3CC00F90643 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 03C231671946D3CC00F90643 /* AppDelegate.m */; }; + 03C2316B1946D3CC00F90643 /* Main_iPhone.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 03C231691946D3CC00F90643 /* Main_iPhone.storyboard */; }; + 03C2316E1946D3CC00F90643 /* Main_iPad.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 03C2316C1946D3CC00F90643 /* Main_iPad.storyboard */; }; + 03C231711946D3CC00F90643 /* MasterViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 03C231701946D3CC00F90643 /* MasterViewController.m */; }; + 03C231741946D3CC00F90643 /* DetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 03C231731946D3CC00F90643 /* DetailViewController.m */; }; + 03C231761946D3CC00F90643 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 03C231751946D3CC00F90643 /* Images.xcassets */; }; + 03C2317D1946D3CC00F90643 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03C2317C1946D3CC00F90643 /* XCTest.framework */; }; + 03C2317E1946D3CC00F90643 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03C231571946D3CC00F90643 /* Foundation.framework */; }; + 03C2317F1946D3CC00F90643 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03C2315B1946D3CC00F90643 /* UIKit.framework */; }; + 03C231871946D3CC00F90643 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 03C231851946D3CC00F90643 /* InfoPlist.strings */; }; + 03C231891946D3CC00F90643 /* iOS7ExampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03C231881946D3CC00F90643 /* iOS7ExampleTests.m */; }; + 03C231951946D56100F90643 /* libOCMock.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 03C231921946D45200F90643 /* libOCMock.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 03C231801946D3CC00F90643 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 03C2314C1946D3CC00F90643 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 03C231531946D3CC00F90643; + remoteInfo = iOS7Example; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 03C231541946D3CC00F90643 /* iOS7Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iOS7Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 03C231571946D3CC00F90643 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + 03C2315B1946D3CC00F90643 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + 03C2315F1946D3CC00F90643 /* iOS7Example-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "iOS7Example-Info.plist"; sourceTree = ""; }; + 03C231611946D3CC00F90643 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 03C231631946D3CC00F90643 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 03C231651946D3CC00F90643 /* iOS7Example-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "iOS7Example-Prefix.pch"; sourceTree = ""; }; + 03C231661946D3CC00F90643 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 03C231671946D3CC00F90643 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 03C2316A1946D3CC00F90643 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main_iPhone.storyboard; sourceTree = ""; }; + 03C2316D1946D3CC00F90643 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main_iPad.storyboard; sourceTree = ""; }; + 03C2316F1946D3CC00F90643 /* MasterViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MasterViewController.h; sourceTree = ""; }; + 03C231701946D3CC00F90643 /* MasterViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MasterViewController.m; sourceTree = ""; }; + 03C231721946D3CC00F90643 /* DetailViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DetailViewController.h; sourceTree = ""; }; + 03C231731946D3CC00F90643 /* DetailViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DetailViewController.m; sourceTree = ""; }; + 03C231751946D3CC00F90643 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + 03C2317B1946D3CC00F90643 /* iOS7ExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = iOS7ExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 03C2317C1946D3CC00F90643 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; + 03C231841946D3CC00F90643 /* iOS7ExampleTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "iOS7ExampleTests-Info.plist"; sourceTree = ""; }; + 03C231861946D3CC00F90643 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 03C231881946D3CC00F90643 /* iOS7ExampleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = iOS7ExampleTests.m; sourceTree = ""; }; + 03C231921946D45200F90643 /* libOCMock.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libOCMock.a; path = usr/lib/libOCMock.a; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 03C231511946D3CC00F90643 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 03C2315C1946D3CC00F90643 /* UIKit.framework in Frameworks */, + 03C231581946D3CC00F90643 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 03C231781946D3CC00F90643 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 03C231951946D56100F90643 /* libOCMock.a in Frameworks */, + 03C2317D1946D3CC00F90643 /* XCTest.framework in Frameworks */, + 03C2317F1946D3CC00F90643 /* UIKit.framework in Frameworks */, + 03C2317E1946D3CC00F90643 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 03C2314B1946D3CC00F90643 = { + isa = PBXGroup; + children = ( + 03C2315D1946D3CC00F90643 /* iOS7Example */, + 03C231821946D3CC00F90643 /* iOS7ExampleTests */, + 03C231561946D3CC00F90643 /* Frameworks */, + 03C231551946D3CC00F90643 /* Products */, + ); + sourceTree = ""; + }; + 03C231551946D3CC00F90643 /* Products */ = { + isa = PBXGroup; + children = ( + 03C231541946D3CC00F90643 /* iOS7Example.app */, + 03C2317B1946D3CC00F90643 /* iOS7ExampleTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 03C231561946D3CC00F90643 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 03C231921946D45200F90643 /* libOCMock.a */, + 03C231571946D3CC00F90643 /* Foundation.framework */, + 03C2315B1946D3CC00F90643 /* UIKit.framework */, + 03C2317C1946D3CC00F90643 /* XCTest.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 03C2315D1946D3CC00F90643 /* iOS7Example */ = { + isa = PBXGroup; + children = ( + 03C231661946D3CC00F90643 /* AppDelegate.h */, + 03C231671946D3CC00F90643 /* AppDelegate.m */, + 03C231691946D3CC00F90643 /* Main_iPhone.storyboard */, + 03C2316C1946D3CC00F90643 /* Main_iPad.storyboard */, + 03C2316F1946D3CC00F90643 /* MasterViewController.h */, + 03C231701946D3CC00F90643 /* MasterViewController.m */, + 03C231721946D3CC00F90643 /* DetailViewController.h */, + 03C231731946D3CC00F90643 /* DetailViewController.m */, + 03C231751946D3CC00F90643 /* Images.xcassets */, + 03C2315E1946D3CC00F90643 /* Supporting Files */, + ); + path = iOS7Example; + sourceTree = ""; + }; + 03C2315E1946D3CC00F90643 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 03C2315F1946D3CC00F90643 /* iOS7Example-Info.plist */, + 03C231601946D3CC00F90643 /* InfoPlist.strings */, + 03C231631946D3CC00F90643 /* main.m */, + 03C231651946D3CC00F90643 /* iOS7Example-Prefix.pch */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 03C231821946D3CC00F90643 /* iOS7ExampleTests */ = { + isa = PBXGroup; + children = ( + 03C231881946D3CC00F90643 /* iOS7ExampleTests.m */, + 03C231831946D3CC00F90643 /* Supporting Files */, + ); + path = iOS7ExampleTests; + sourceTree = ""; + }; + 03C231831946D3CC00F90643 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 03C231841946D3CC00F90643 /* iOS7ExampleTests-Info.plist */, + 03C231851946D3CC00F90643 /* InfoPlist.strings */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 03C231531946D3CC00F90643 /* iOS7Example */ = { + isa = PBXNativeTarget; + buildConfigurationList = 03C2318C1946D3CC00F90643 /* Build configuration list for PBXNativeTarget "iOS7Example" */; + buildPhases = ( + 03C231501946D3CC00F90643 /* Sources */, + 03C231511946D3CC00F90643 /* Frameworks */, + 03C231521946D3CC00F90643 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = iOS7Example; + productName = iOS7Example; + productReference = 03C231541946D3CC00F90643 /* iOS7Example.app */; + productType = "com.apple.product-type.application"; + }; + 03C2317A1946D3CC00F90643 /* iOS7ExampleTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 03C2318F1946D3CC00F90643 /* Build configuration list for PBXNativeTarget "iOS7ExampleTests" */; + buildPhases = ( + 03C231771946D3CC00F90643 /* Sources */, + 03C231781946D3CC00F90643 /* Frameworks */, + 03C231791946D3CC00F90643 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 03C231811946D3CC00F90643 /* PBXTargetDependency */, + ); + name = iOS7ExampleTests; + productName = iOS7ExampleTests; + productReference = 03C2317B1946D3CC00F90643 /* iOS7ExampleTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 03C2314C1946D3CC00F90643 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0510; + ORGANIZATIONNAME = "Erik Doernenburg"; + TargetAttributes = { + 03C2317A1946D3CC00F90643 = { + TestTargetID = 03C231531946D3CC00F90643; + }; + }; + }; + buildConfigurationList = 03C2314F1946D3CC00F90643 /* Build configuration list for PBXProject "iOS7Example" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 03C2314B1946D3CC00F90643; + productRefGroup = 03C231551946D3CC00F90643 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 03C231531946D3CC00F90643 /* iOS7Example */, + 03C2317A1946D3CC00F90643 /* iOS7ExampleTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 03C231521946D3CC00F90643 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 03C2316E1946D3CC00F90643 /* Main_iPad.storyboard in Resources */, + 03C231761946D3CC00F90643 /* Images.xcassets in Resources */, + 03C2316B1946D3CC00F90643 /* Main_iPhone.storyboard in Resources */, + 03C231621946D3CC00F90643 /* InfoPlist.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 03C231791946D3CC00F90643 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 03C231871946D3CC00F90643 /* InfoPlist.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 03C231501946D3CC00F90643 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 03C231681946D3CC00F90643 /* AppDelegate.m in Sources */, + 03C231711946D3CC00F90643 /* MasterViewController.m in Sources */, + 03C231641946D3CC00F90643 /* main.m in Sources */, + 03C231741946D3CC00F90643 /* DetailViewController.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 03C231771946D3CC00F90643 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 03C231891946D3CC00F90643 /* iOS7ExampleTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 03C231811946D3CC00F90643 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 03C231531946D3CC00F90643 /* iOS7Example */; + targetProxy = 03C231801946D3CC00F90643 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 03C231601946D3CC00F90643 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 03C231611946D3CC00F90643 /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + 03C231691946D3CC00F90643 /* Main_iPhone.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 03C2316A1946D3CC00F90643 /* Base */, + ); + name = Main_iPhone.storyboard; + sourceTree = ""; + }; + 03C2316C1946D3CC00F90643 /* Main_iPad.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 03C2316D1946D3CC00F90643 /* Base */, + ); + name = Main_iPad.storyboard; + sourceTree = ""; + }; + 03C231851946D3CC00F90643 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 03C231861946D3CC00F90643 /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 03C2318A1946D3CC00F90643 /* Debug */ = { + isa = XCBuildConfiguration; + 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_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + 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 = 7.1; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 03C2318B1946D3CC00F90643 /* Release */ = { + isa = XCBuildConfiguration; + 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_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + 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 = 7.1; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 03C2318D1946D3CC00F90643 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "iOS7Example/iOS7Example-Prefix.pch"; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + ); + INFOPLIST_FILE = "iOS7Example/iOS7Example-Info.plist"; + LIBRARY_SEARCH_PATHS = "$(inherited)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = app; + }; + name = Debug; + }; + 03C2318E1946D3CC00F90643 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "iOS7Example/iOS7Example-Prefix.pch"; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + ); + INFOPLIST_FILE = "iOS7Example/iOS7Example-Info.plist"; + LIBRARY_SEARCH_PATHS = "$(inherited)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = app; + }; + name = Release; + }; + 03C231901946D3CC00F90643 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/iOS7Example.app/iOS7Example"; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "iOS7Example/iOS7Example-Prefix.pch"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(PROJECT_DIR)/usr/include", + ); + INFOPLIST_FILE = "iOS7ExampleTests/iOS7ExampleTests-Info.plist"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/usr/lib", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + XCTest, + "-ObjC", + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUNDLE_LOADER)"; + WRAPPER_EXTENSION = xctest; + }; + name = Debug; + }; + 03C231911946D3CC00F90643 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/iOS7Example.app/iOS7Example"; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "iOS7Example/iOS7Example-Prefix.pch"; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(PROJECT_DIR)/usr/include", + ); + INFOPLIST_FILE = "iOS7ExampleTests/iOS7ExampleTests-Info.plist"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/usr/lib", + ); + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + XCTest, + "-ObjC", + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUNDLE_LOADER)"; + WRAPPER_EXTENSION = xctest; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 03C2314F1946D3CC00F90643 /* Build configuration list for PBXProject "iOS7Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 03C2318A1946D3CC00F90643 /* Debug */, + 03C2318B1946D3CC00F90643 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 03C2318C1946D3CC00F90643 /* Build configuration list for PBXNativeTarget "iOS7Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 03C2318D1946D3CC00F90643 /* Debug */, + 03C2318E1946D3CC00F90643 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 03C2318F1946D3CC00F90643 /* Build configuration list for PBXNativeTarget "iOS7ExampleTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 03C231901946D3CC00F90643 /* Debug */, + 03C231911946D3CC00F90643 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 03C2314C1946D3CC00F90643 /* Project object */; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/AppDelegate.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/AppDelegate.h new file mode 100644 index 0000000000..dda1da6a53 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/AppDelegate.h @@ -0,0 +1,15 @@ +// +// AppDelegate.h +// iOS7Example +// +// Created by Erik Doernenburg on 10/06/2014. +// Copyright (c) 2014 Erik Doernenburg. All rights reserved. +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/AppDelegate.m b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/AppDelegate.m new file mode 100644 index 0000000000..c37cf65f05 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/AppDelegate.m @@ -0,0 +1,51 @@ +// +// AppDelegate.m +// iOS7Example +// +// Created by Erik Doernenburg on 10/06/2014. +// Copyright (c) 2014 Erik Doernenburg. All rights reserved. +// + +#import "AppDelegate.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + // Override point for customization after application launch. + if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController; + UINavigationController *navigationController = [splitViewController.viewControllers lastObject]; + splitViewController.delegate = (id)navigationController.topViewController; + } + return YES; +} + +- (void)applicationWillResignActive:(UIApplication *)application +{ + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. +} + +- (void)applicationDidEnterBackground:(UIApplication *)application +{ + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. +} + +- (void)applicationWillEnterForeground:(UIApplication *)application +{ + // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. +} + +- (void)applicationDidBecomeActive:(UIApplication *)application +{ + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. +} + +- (void)applicationWillTerminate:(UIApplication *)application +{ + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/Base.lproj/Main_iPad.storyboard b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/Base.lproj/Main_iPad.storyboard new file mode 100644 index 0000000000..abb693c39a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/Base.lproj/Main_iPad.storyboard @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/Base.lproj/Main_iPhone.storyboard b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/Base.lproj/Main_iPhone.storyboard new file mode 100644 index 0000000000..fa901eb340 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/Base.lproj/Main_iPhone.storyboard @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/DetailViewController.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/DetailViewController.h new file mode 100644 index 0000000000..9567997b51 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/DetailViewController.h @@ -0,0 +1,16 @@ +// +// DetailViewController.h +// iOS7Example +// +// Created by Erik Doernenburg on 10/06/2014. +// Copyright (c) 2014 Erik Doernenburg. All rights reserved. +// + +#import + +@interface DetailViewController : UIViewController + +@property (strong, nonatomic) id detailItem; + +@property (weak, nonatomic) IBOutlet UILabel *detailDescriptionLabel; +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/DetailViewController.m b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/DetailViewController.m new file mode 100644 index 0000000000..81c78d8c75 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/DetailViewController.m @@ -0,0 +1,72 @@ +// +// DetailViewController.m +// iOS7Example +// +// Created by Erik Doernenburg on 10/06/2014. +// Copyright (c) 2014 Erik Doernenburg. All rights reserved. +// + +#import "DetailViewController.h" + +@interface DetailViewController () +@property (strong, nonatomic) UIPopoverController *masterPopoverController; +- (void)configureView; +@end + +@implementation DetailViewController + +#pragma mark - Managing the detail item + +- (void)setDetailItem:(id)newDetailItem +{ + if (_detailItem != newDetailItem) { + _detailItem = newDetailItem; + + // Update the view. + [self configureView]; + } + + if (self.masterPopoverController != nil) { + [self.masterPopoverController dismissPopoverAnimated:YES]; + } +} + +- (void)configureView +{ + // Update the user interface for the detail item. + + if (self.detailItem) { + self.detailDescriptionLabel.text = [self.detailItem description]; + } +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + // Do any additional setup after loading the view, typically from a nib. + [self configureView]; +} + +- (void)didReceiveMemoryWarning +{ + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +#pragma mark - Split view + +- (void)splitViewController:(UISplitViewController *)splitController willHideViewController:(UIViewController *)viewController withBarButtonItem:(UIBarButtonItem *)barButtonItem forPopoverController:(UIPopoverController *)popoverController +{ + barButtonItem.title = NSLocalizedString(@"Master", @"Master"); + [self.navigationItem setLeftBarButtonItem:barButtonItem animated:YES]; + self.masterPopoverController = popoverController; +} + +- (void)splitViewController:(UISplitViewController *)splitController willShowViewController:(UIViewController *)viewController invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem +{ + // Called when the view is shown again in the split view, invalidating the button and popover controller. + [self.navigationItem setLeftBarButtonItem:nil animated:YES]; + self.masterPopoverController = nil; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/Images.xcassets/AppIcon.appiconset/Contents.json b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..91bf9c14a7 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,53 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "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" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/Images.xcassets/LaunchImage.launchimage/Contents.json b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/Images.xcassets/LaunchImage.launchimage/Contents.json new file mode 100644 index 0000000000..6f870a4629 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/Images.xcassets/LaunchImage.launchimage/Contents.json @@ -0,0 +1,51 @@ +{ + "images" : [ + { + "orientation" : "portrait", + "idiom" : "iphone", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "iphone", + "subtype" : "retina4", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "1x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "1x" + }, + { + "orientation" : "portrait", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + }, + { + "orientation" : "landscape", + "idiom" : "ipad", + "extent" : "full-screen", + "minimum-system-version" : "7.0", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/MasterViewController.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/MasterViewController.h new file mode 100644 index 0000000000..a1a918386b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/MasterViewController.h @@ -0,0 +1,17 @@ +// +// MasterViewController.h +// iOS7Example +// +// Created by Erik Doernenburg on 10/06/2014. +// Copyright (c) 2014 Erik Doernenburg. All rights reserved. +// + +#import + +@class DetailViewController; + +@interface MasterViewController : UITableViewController + +@property (strong, nonatomic) DetailViewController *detailViewController; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/MasterViewController.m b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/MasterViewController.m new file mode 100644 index 0000000000..bfcf96a60b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/MasterViewController.m @@ -0,0 +1,126 @@ +// +// MasterViewController.m +// iOS7Example +// +// Created by Erik Doernenburg on 10/06/2014. +// Copyright (c) 2014 Erik Doernenburg. All rights reserved. +// + +#import "MasterViewController.h" + +#import "DetailViewController.h" + +@interface MasterViewController () { + NSMutableArray *_objects; +} +@end + +@implementation MasterViewController + +- (void)awakeFromNib +{ + if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + self.clearsSelectionOnViewWillAppear = NO; + self.preferredContentSize = CGSizeMake(320.0, 600.0); + } + [super awakeFromNib]; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + // Do any additional setup after loading the view, typically from a nib. + self.navigationItem.leftBarButtonItem = self.editButtonItem; + + UIBarButtonItem *addButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(insertNewObject:)]; + self.navigationItem.rightBarButtonItem = addButton; + self.detailViewController = (DetailViewController *)[[self.splitViewController.viewControllers lastObject] topViewController]; +} + +- (void)didReceiveMemoryWarning +{ + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +- (void)insertNewObject:(id)sender +{ + if (!_objects) { + _objects = [[NSMutableArray alloc] init]; + } + [_objects insertObject:[NSDate date] atIndex:0]; + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0]; + [self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; +} + +#pragma mark - Table View + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView +{ + return 1; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section +{ + return _objects.count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath +{ + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; + + NSDate *object = _objects[indexPath.row]; + cell.textLabel.text = [object description]; + return cell; +} + +- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath +{ + // Return NO if you do not want the specified item to be editable. + return YES; +} + +- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath +{ + if (editingStyle == UITableViewCellEditingStyleDelete) { + [_objects removeObjectAtIndex:indexPath.row]; + [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; + } else if (editingStyle == UITableViewCellEditingStyleInsert) { + // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view. + } +} + +/* +// Override to support rearranging the table view. +- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath +{ +} +*/ + +/* +// Override to support conditional rearranging of the table view. +- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath +{ + // Return NO if you do not want the item to be re-orderable. + return YES; +} +*/ + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath +{ + if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { + NSDate *object = _objects[indexPath.row]; + self.detailViewController.detailItem = object; + } +} + +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender +{ + if ([[segue identifier] isEqualToString:@"showDetail"]) { + NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow]; + NSDate *object = _objects[indexPath.row]; + [[segue destinationViewController] setDetailItem:object]; + } +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/en.lproj/InfoPlist.strings b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..477b28ff8f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/iOS7Example-Info.plist b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/iOS7Example-Info.plist new file mode 100644 index 0000000000..504587f709 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/iOS7Example-Info.plist @@ -0,0 +1,59 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${PRODUCT_NAME} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + com.mulle-kybernetik.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + LSRequiresIPhoneOS + + UIMainStoryboardFile + Main_iPhone + UIMainStoryboardFile~ipad + Main_iPad + UIRequiredDeviceCapabilities + + armv7 + + UIStatusBarTintParameters + + UINavigationBar + + Style + UIBarStyleDefault + Translucent + + + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/iOS7Example-Prefix.pch b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/iOS7Example-Prefix.pch new file mode 100644 index 0000000000..82a2bb4507 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/iOS7Example-Prefix.pch @@ -0,0 +1,16 @@ +// +// Prefix header +// +// The contents of this file are implicitly included at the beginning of every source file. +// + +#import + +#ifndef __IPHONE_5_0 +#warning "This project uses features only available in iOS SDK 5.0 and later." +#endif + +#ifdef __OBJC__ + #import + #import +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/main.m b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/main.m new file mode 100644 index 0000000000..947145eac5 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7Example/main.m @@ -0,0 +1,18 @@ +// +// main.m +// iOS7Example +// +// Created by Erik Doernenburg on 10/06/2014. +// Copyright (c) 2014 Erik Doernenburg. All rights reserved. +// + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) +{ + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7ExampleTests/en.lproj/InfoPlist.strings b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7ExampleTests/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..477b28ff8f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7ExampleTests/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7ExampleTests/iOS7ExampleTests-Info.plist b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7ExampleTests/iOS7ExampleTests-Info.plist new file mode 100644 index 0000000000..a409125227 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7ExampleTests/iOS7ExampleTests-Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + com.mulle-kybernetik.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7ExampleTests/iOS7ExampleTests.m b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7ExampleTests/iOS7ExampleTests.m new file mode 100644 index 0000000000..8428e57339 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/iOS7ExampleTests/iOS7ExampleTests.m @@ -0,0 +1,54 @@ +// +// iOS7ExampleTests.m +// iOS7ExampleTests +// +// Created by Erik Doernenburg on 10/06/2014. +// Copyright (c) 2014 Erik Doernenburg. All rights reserved. +// + +#import +#import +#import +#import "MasterViewController.h" + +@interface iOS7ExampleTests : XCTestCase + +@end + +@implementation iOS7ExampleTests + +- (void)setUp +{ + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)tearDown +{ + // Put teardown code here. This method is called after the invocation of each test method in the class. + [super tearDown]; +} + +- (void)testMasterViewControllerDeletesItemsFromTableView +{ + // Test set-up + + MasterViewController *controller = [[MasterViewController alloc] init]; + NSIndexPath *dummyIndexPath = [NSIndexPath indexPathForRow:1 inSection:0]; + + id tableViewMock = OCMClassMock([UITableView class]); + + // Invoke functionality to be tested + // If you want to see the test fail you can, for example, change the editing style to + // UITableViewCellEditingStyleNone. In that case the method in the controller does not + // make a call to the table view and the mock will raise an exception when verify is + // called further down. + + [controller tableView:tableViewMock commitEditingStyle:UITableViewCellEditingStyleDelete forRowAtIndexPath:dummyIndexPath]; + + // Verify that expected methods were called + + OCMVerify([tableViewMock deleteRowsAtIndexPaths:[NSArray arrayWithObject:dummyIndexPath] withRowAnimation:UITableViewRowAnimationFade]); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/include/OCMock/NSNotificationCenter+OCMAdditions.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/include/OCMock/NSNotificationCenter+OCMAdditions.h new file mode 100644 index 0000000000..c20a9c2b20 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/include/OCMock/NSNotificationCenter+OCMAdditions.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2009-2014 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@class OCObserverMockObject; + + +@interface NSNotificationCenter(OCMAdditions) + +- (void)addMockObserver:(OCObserverMockObject *)notificationObserver name:(NSString *)notificationName object:(id)notificationSender; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/include/OCMock/OCMArg.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/include/OCMock/OCMArg.h new file mode 100644 index 0000000000..14fb1617f0 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/include/OCMock/OCMArg.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2009-2014 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@interface OCMArg : NSObject + +// constraining arguments + ++ (id)any; ++ (SEL)anySelector; ++ (void *)anyPointer; ++ (id __autoreleasing *)anyObjectRef; ++ (id)isNil; ++ (id)isNotNil; ++ (id)isNotEqual:(id)value; ++ (id)checkWithSelector:(SEL)selector onObject:(id)anObject; ++ (id)checkWithBlock:(BOOL (^)(id obj))block; + +// manipulating arguments + ++ (id *)setTo:(id)value; ++ (void *)setToValue:(NSValue *)value; + +// internal use only + ++ (id)resolveSpecialValues:(NSValue *)value; + +@end + +#define OCMOCK_ANY [OCMArg any] + +#if defined(__GNUC__) && !defined(__STRICT_ANSI__) + #define OCMOCK_VALUE(variable) \ + ({ __typeof__(variable) __v = (variable); [NSValue value:&__v withObjCType:@encode(__typeof__(__v))]; }) +#else + #define OCMOCK_VALUE(variable) [NSValue value:&variable withObjCType:@encode(__typeof__(variable))] +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/include/OCMock/OCMConstraint.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/include/OCMock/OCMConstraint.h new file mode 100644 index 0000000000..b606a54eab --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/include/OCMock/OCMConstraint.h @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2007-2014 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + + +@interface OCMConstraint : NSObject + ++ (id)constraint; +- (BOOL)evaluate:(id)value; + +// if you are looking for any, isNil, etc, they have moved to OCMArg + +// try to use [OCMArg checkWith...] instead of the constraintWith... methods below + ++ (id)constraintWithSelector:(SEL)aSelector onObject:(id)anObject; ++ (id)constraintWithSelector:(SEL)aSelector onObject:(id)anObject withValue:(id)aValue; + + +@end + +@interface OCMAnyConstraint : OCMConstraint +@end + +@interface OCMIsNilConstraint : OCMConstraint +@end + +@interface OCMIsNotNilConstraint : OCMConstraint +@end + +@interface OCMIsNotEqualConstraint : OCMConstraint +{ + @public + id testValue; +} + +@end + +@interface OCMInvocationConstraint : OCMConstraint +{ + @public + NSInvocation *invocation; +} + +@end + +@interface OCMBlockConstraint : OCMConstraint +{ + BOOL (^block)(id); +} + +- (id)initWithConstraintBlock:(BOOL (^)(id))block; + +@end + + +#define CONSTRAINT(aSelector) [OCMConstraint constraintWithSelector:aSelector onObject:self] +#define CONSTRAINTV(aSelector, aValue) [OCMConstraint constraintWithSelector:aSelector onObject:self withValue:(aValue)] diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/include/OCMock/OCMLocation.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/include/OCMock/OCMLocation.h new file mode 100644 index 0000000000..136d9c9f0e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/include/OCMock/OCMLocation.h @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2014 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@interface OCMLocation : NSObject +{ + id testCase; + NSString *file; + NSUInteger line; +} + ++ (id)locationWithTestCase:(id)aTestCase file:(NSString *)aFile line:(NSUInteger)aLine; + +- (id)initWithTestCase:(id)aTestCase file:(NSString *)aFile line:(NSUInteger)aLine; + +- (id)testCase; +- (NSString *)file; +- (NSUInteger)line; + +@end + +extern OCMLocation *OCMMakeLocation(id testCase, const char *file, int line); diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/include/OCMock/OCMMacroState.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/include/OCMock/OCMMacroState.h new file mode 100644 index 0000000000..4b2d635086 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/include/OCMock/OCMMacroState.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2014 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@class OCMLocation; +@class OCMRecorder; +@class OCMStubRecorder; +@class OCMockObject; + + +@interface OCMMacroState : NSObject +{ + OCMRecorder *recorder; +} + ++ (void)beginStubMacro; ++ (OCMStubRecorder *)endStubMacro; + ++ (void)beginExpectMacro; ++ (OCMStubRecorder *)endExpectMacro; + ++ (void)beginVerifyMacroAtLocation:(OCMLocation *)aLocation; ++ (void)endVerifyMacro; + ++ (OCMMacroState *)globalState; + +- (OCMRecorder *)recorder; + +- (void)switchToClassMethod; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/include/OCMock/OCMRecorder.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/include/OCMock/OCMRecorder.h new file mode 100644 index 0000000000..aa1f40179a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/include/OCMock/OCMRecorder.h @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2014 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@class OCMockObject; +@class OCMInvocationMatcher; + + +@interface OCMRecorder : NSProxy +{ + OCMockObject *mockObject; + OCMInvocationMatcher *invocationMatcher; +} + +- (id)init; +- (id)initWithMockObject:(OCMockObject *)aMockObject; + +- (void)setMockObject:(OCMockObject *)aMockObject; + +- (OCMInvocationMatcher *)invocationMatcher; + +- (id)classMethod; +- (id)ignoringNonObjectArgs; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/include/OCMock/OCMStubRecorder.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/include/OCMock/OCMStubRecorder.h new file mode 100644 index 0000000000..73b401f894 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/include/OCMock/OCMStubRecorder.h @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2004-2014 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMRecorder.h" + + +@interface OCMStubRecorder : OCMRecorder + +- (id)andReturn:(id)anObject; +- (id)andReturnValue:(NSValue *)aValue; +- (id)andThrow:(NSException *)anException; +- (id)andPost:(NSNotification *)aNotification; +- (id)andCall:(SEL)selector onObject:(id)anObject; +- (id)andDo:(void (^)(NSInvocation *invocation))block; +- (id)andForwardToRealObject; + +@end + + +@interface OCMStubRecorder (Properties) + +#define andReturn(aValue) _andReturn(({ typeof(aValue) _v = (aValue); [NSValue value:&_v withObjCType:@encode(typeof(_v))]; })) +@property (nonatomic, readonly) OCMStubRecorder *(^ _andReturn)(NSValue *); + +#define andThrow(anException) _andThrow(anException) +@property (nonatomic, readonly) OCMStubRecorder *(^ _andThrow)(NSException *); + +#define andPost(aNotification) _andPost(aNotification) +@property (nonatomic, readonly) OCMStubRecorder *(^ _andPost)(NSNotification *); + +#define andCall(anObject, aSelector) _andCall(anObject, aSelector) +@property (nonatomic, readonly) OCMStubRecorder *(^ _andCall)(id, SEL); + +#define andDo(aBlock) _andDo(aBlock) +@property (nonatomic, readonly) OCMStubRecorder *(^ _andDo)(void (^)(NSInvocation *)); + +#define andForwardToRealObject() _andForwardToRealObject() +@property (nonatomic, readonly) OCMStubRecorder *(^ _andForwardToRealObject)(void); + +@end + + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/include/OCMock/OCMock.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/include/OCMock/OCMock.h new file mode 100644 index 0000000000..f0083b3507 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/include/OCMock/OCMock.h @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2004-2014 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import +#import +#import +#import +#import +#import +#import + + +#define OCMClassMock(cls) [OCMockObject niceMockForClass:cls] + +#define OCMStrictClassMock(cls) [OCMockObject mockForClass:cls] + +#define OCMProtocolMock(protocol) [OCMockObject niceMockForProtocol:protocol] + +#define OCMStrictProtocolMock(protocol) [OCMockObject mockForProtocol:protocol] + +#define OCMPartialMock(obj) [OCMockObject partialMockForObject:obj] + +#define OCMObserverMock() [OCMockObject observerMock] + + +#define OCMStub(invocation) \ +({ \ + _OCMSilenceWarnings( \ + [OCMMacroState beginStubMacro]; \ + invocation; \ + [OCMMacroState endStubMacro]; \ + ); \ +}) + +#define OCMExpect(invocation) \ +({ \ + _OCMSilenceWarnings( \ + [OCMMacroState beginExpectMacro]; \ + invocation; \ + [OCMMacroState endExpectMacro]; \ + ); \ +}) + +#define ClassMethod(invocation) \ + _OCMSilenceWarnings( \ + [[OCMMacroState globalState] switchToClassMethod]; \ + invocation; \ + ); + + +#define OCMVerifyAll(mock) [mock verifyAtLocation:OCMMakeLocation(self, __FILE__, __LINE__)] + +#define OCMVerifyAllWithDelay(mock, delay) [mock verifyWithDelay:delay atLocation:OCMMakeLocation(self, __FILE__, __LINE__)] + +#define OCMVerify(invocation) \ +({ \ + _OCMSilenceWarnings( \ + [OCMMacroState beginVerifyMacroAtLocation:OCMMakeLocation(self, __FILE__, __LINE__)]; \ + invocation; \ + [OCMMacroState endVerifyMacro]; \ + ); \ +}) + +#define _OCMSilenceWarnings(macro) \ +({ \ + _Pragma("clang diagnostic push") \ + _Pragma("clang diagnostic ignored \"-Wunused-value\"") \ + macro \ + _Pragma("clang diagnostic pop") \ +}) diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/include/OCMock/OCMockObject.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/include/OCMock/OCMockObject.h new file mode 100644 index 0000000000..ff62bb5b18 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/include/OCMock/OCMockObject.h @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2004-2014 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@class OCMLocation; +@class OCMInvocationStub; +@class OCMStubRecorder; +@class OCMInvocationMatcher; +@class OCMInvocationExpectation; + + +@interface OCMockObject : NSProxy +{ + BOOL isNice; + BOOL expectationOrderMatters; + NSMutableArray *stubs; + NSMutableArray *expectations; + NSMutableArray *exceptions; + NSMutableArray *invocations; +} + ++ (id)mockForClass:(Class)aClass; ++ (id)mockForProtocol:(Protocol *)aProtocol; ++ (id)partialMockForObject:(NSObject *)anObject; + ++ (id)niceMockForClass:(Class)aClass; ++ (id)niceMockForProtocol:(Protocol *)aProtocol; + ++ (id)observerMock; + +- (id)init; + +- (void)setExpectationOrderMatters:(BOOL)flag; + +- (id)stub; +- (id)expect; +- (id)reject; + +- (id)verify; +- (id)verifyAtLocation:(OCMLocation *)location; + +- (void)verifyWithDelay:(NSTimeInterval)delay; +- (void)verifyWithDelay:(NSTimeInterval)delay atLocation:(OCMLocation *)location; + +- (void)stopMocking; + +// internal use only + +- (void)addStub:(OCMInvocationStub *)aStub; +- (void)addExpectation:(OCMInvocationExpectation *)anExpectation; + +- (BOOL)handleInvocation:(NSInvocation *)anInvocation; +- (void)handleUnRecordedInvocation:(NSInvocation *)anInvocation; +- (BOOL)handleSelector:(SEL)sel; + +- (void)verifyInvocation:(OCMInvocationMatcher *)matcher; +- (void)verifyInvocation:(OCMInvocationMatcher *)matcher atLocation:(OCMLocation *)location; + +@end + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/lib/libOCMock.a b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/lib/libOCMock.a new file mode 100644 index 0000000000..c96cbf04c7 Binary files /dev/null and b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS7Example/usr/lib/libOCMock.a differ diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example.xcodeproj/project.pbxproj b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..9b0e1c02e0 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example.xcodeproj/project.pbxproj @@ -0,0 +1,439 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 03456F481BBA6D51001C86A3 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 03456F471BBA6D51001C86A3 /* main.m */; }; + 03456F4B1BBA6D51001C86A3 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 03456F4A1BBA6D51001C86A3 /* AppDelegate.m */; }; + 03456F4E1BBA6D51001C86A3 /* MasterViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 03456F4D1BBA6D51001C86A3 /* MasterViewController.m */; }; + 03456F511BBA6D51001C86A3 /* DetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 03456F501BBA6D51001C86A3 /* DetailViewController.m */; }; + 03456F541BBA6D51001C86A3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 03456F521BBA6D51001C86A3 /* Main.storyboard */; }; + 03456F561BBA6D51001C86A3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 03456F551BBA6D51001C86A3 /* Assets.xcassets */; }; + 03456F591BBA6D51001C86A3 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 03456F571BBA6D51001C86A3 /* LaunchScreen.storyboard */; }; + 03456F641BBA6D51001C86A3 /* iOS9ExampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03456F631BBA6D51001C86A3 /* iOS9ExampleTests.m */; }; + 03456F6F1BBA7082001C86A3 /* libOCMock.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 03456F6E1BBA7082001C86A3 /* libOCMock.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 03456F601BBA6D51001C86A3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 03456F3B1BBA6D51001C86A3 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 03456F421BBA6D51001C86A3; + remoteInfo = iOS9Example; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 03456F431BBA6D51001C86A3 /* iOS9Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iOS9Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 03456F471BBA6D51001C86A3 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 03456F491BBA6D51001C86A3 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 03456F4A1BBA6D51001C86A3 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 03456F4C1BBA6D51001C86A3 /* MasterViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MasterViewController.h; sourceTree = ""; }; + 03456F4D1BBA6D51001C86A3 /* MasterViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MasterViewController.m; sourceTree = ""; }; + 03456F4F1BBA6D51001C86A3 /* DetailViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DetailViewController.h; sourceTree = ""; }; + 03456F501BBA6D51001C86A3 /* DetailViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DetailViewController.m; sourceTree = ""; }; + 03456F531BBA6D51001C86A3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 03456F551BBA6D51001C86A3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 03456F581BBA6D51001C86A3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 03456F5A1BBA6D51001C86A3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 03456F5F1BBA6D51001C86A3 /* iOS9ExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = iOS9ExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 03456F631BBA6D51001C86A3 /* iOS9ExampleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = iOS9ExampleTests.m; sourceTree = ""; }; + 03456F651BBA6D51001C86A3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 03456F6E1BBA7082001C86A3 /* libOCMock.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libOCMock.a; path = usr/lib/libOCMock.a; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 03456F401BBA6D51001C86A3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 03456F5C1BBA6D51001C86A3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 03456F6F1BBA7082001C86A3 /* libOCMock.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 03456F3A1BBA6D51001C86A3 = { + isa = PBXGroup; + children = ( + 03456F6E1BBA7082001C86A3 /* libOCMock.a */, + 03456F451BBA6D51001C86A3 /* iOS9Example */, + 03456F621BBA6D51001C86A3 /* iOS9ExampleTests */, + 03456F441BBA6D51001C86A3 /* Products */, + ); + sourceTree = ""; + }; + 03456F441BBA6D51001C86A3 /* Products */ = { + isa = PBXGroup; + children = ( + 03456F431BBA6D51001C86A3 /* iOS9Example.app */, + 03456F5F1BBA6D51001C86A3 /* iOS9ExampleTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 03456F451BBA6D51001C86A3 /* iOS9Example */ = { + isa = PBXGroup; + children = ( + 03456F491BBA6D51001C86A3 /* AppDelegate.h */, + 03456F4A1BBA6D51001C86A3 /* AppDelegate.m */, + 03456F4C1BBA6D51001C86A3 /* MasterViewController.h */, + 03456F4D1BBA6D51001C86A3 /* MasterViewController.m */, + 03456F4F1BBA6D51001C86A3 /* DetailViewController.h */, + 03456F501BBA6D51001C86A3 /* DetailViewController.m */, + 03456F521BBA6D51001C86A3 /* Main.storyboard */, + 03456F551BBA6D51001C86A3 /* Assets.xcassets */, + 03456F571BBA6D51001C86A3 /* LaunchScreen.storyboard */, + 03456F5A1BBA6D51001C86A3 /* Info.plist */, + 03456F461BBA6D51001C86A3 /* Supporting Files */, + ); + path = iOS9Example; + sourceTree = ""; + }; + 03456F461BBA6D51001C86A3 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 03456F471BBA6D51001C86A3 /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 03456F621BBA6D51001C86A3 /* iOS9ExampleTests */ = { + isa = PBXGroup; + children = ( + 03456F631BBA6D51001C86A3 /* iOS9ExampleTests.m */, + 03456F651BBA6D51001C86A3 /* Info.plist */, + ); + path = iOS9ExampleTests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 03456F421BBA6D51001C86A3 /* iOS9Example */ = { + isa = PBXNativeTarget; + buildConfigurationList = 03456F681BBA6D51001C86A3 /* Build configuration list for PBXNativeTarget "iOS9Example" */; + buildPhases = ( + 03456F3F1BBA6D51001C86A3 /* Sources */, + 03456F401BBA6D51001C86A3 /* Frameworks */, + 03456F411BBA6D51001C86A3 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = iOS9Example; + productName = iOS9Example; + productReference = 03456F431BBA6D51001C86A3 /* iOS9Example.app */; + productType = "com.apple.product-type.application"; + }; + 03456F5E1BBA6D51001C86A3 /* iOS9ExampleTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 03456F6B1BBA6D51001C86A3 /* Build configuration list for PBXNativeTarget "iOS9ExampleTests" */; + buildPhases = ( + 03456F5B1BBA6D51001C86A3 /* Sources */, + 03456F5C1BBA6D51001C86A3 /* Frameworks */, + 03456F5D1BBA6D51001C86A3 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 03456F611BBA6D51001C86A3 /* PBXTargetDependency */, + ); + name = iOS9ExampleTests; + productName = iOS9ExampleTests; + productReference = 03456F5F1BBA6D51001C86A3 /* iOS9ExampleTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 03456F3B1BBA6D51001C86A3 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0700; + ORGANIZATIONNAME = "Mulle Kybernetik"; + TargetAttributes = { + 03456F421BBA6D51001C86A3 = { + CreatedOnToolsVersion = 7.0.1; + }; + 03456F5E1BBA6D51001C86A3 = { + CreatedOnToolsVersion = 7.0.1; + TestTargetID = 03456F421BBA6D51001C86A3; + }; + }; + }; + buildConfigurationList = 03456F3E1BBA6D51001C86A3 /* Build configuration list for PBXProject "iOS9Example" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 03456F3A1BBA6D51001C86A3; + productRefGroup = 03456F441BBA6D51001C86A3 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 03456F421BBA6D51001C86A3 /* iOS9Example */, + 03456F5E1BBA6D51001C86A3 /* iOS9ExampleTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 03456F411BBA6D51001C86A3 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 03456F591BBA6D51001C86A3 /* LaunchScreen.storyboard in Resources */, + 03456F561BBA6D51001C86A3 /* Assets.xcassets in Resources */, + 03456F541BBA6D51001C86A3 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 03456F5D1BBA6D51001C86A3 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 03456F3F1BBA6D51001C86A3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 03456F4B1BBA6D51001C86A3 /* AppDelegate.m in Sources */, + 03456F4E1BBA6D51001C86A3 /* MasterViewController.m in Sources */, + 03456F481BBA6D51001C86A3 /* main.m in Sources */, + 03456F511BBA6D51001C86A3 /* DetailViewController.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 03456F5B1BBA6D51001C86A3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 03456F641BBA6D51001C86A3 /* iOS9ExampleTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 03456F611BBA6D51001C86A3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 03456F421BBA6D51001C86A3 /* iOS9Example */; + targetProxy = 03456F601BBA6D51001C86A3 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 03456F521BBA6D51001C86A3 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 03456F531BBA6D51001C86A3 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 03456F571BBA6D51001C86A3 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 03456F581BBA6D51001C86A3 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 03456F661BBA6D51001C86A3 /* Debug */ = { + isa = XCBuildConfiguration; + 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_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + 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_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 = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 03456F671BBA6D51001C86A3 /* Release */ = { + isa = XCBuildConfiguration; + 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_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "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 = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 03456F691BBA6D51001C86A3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = iOS9Example/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.mulle-kybernetik.iOS9Example"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 03456F6A1BBA6D51001C86A3 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = iOS9Example/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.mulle-kybernetik.iOS9Example"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + 03456F6C1BBA6D51001C86A3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/usr/include"; + INFOPLIST_FILE = iOS9ExampleTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/usr/lib", + ); + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_BUNDLE_IDENTIFIER = "com.mulle-kybernetik.iOS9ExampleTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/iOS9Example.app/iOS9Example"; + USER_HEADER_SEARCH_PATHS = ""; + }; + name = Debug; + }; + 03456F6D1BBA6D51001C86A3 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/usr/include"; + INFOPLIST_FILE = iOS9ExampleTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/usr/lib", + ); + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_BUNDLE_IDENTIFIER = "com.mulle-kybernetik.iOS9ExampleTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/iOS9Example.app/iOS9Example"; + USER_HEADER_SEARCH_PATHS = ""; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 03456F3E1BBA6D51001C86A3 /* Build configuration list for PBXProject "iOS9Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 03456F661BBA6D51001C86A3 /* Debug */, + 03456F671BBA6D51001C86A3 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 03456F681BBA6D51001C86A3 /* Build configuration list for PBXNativeTarget "iOS9Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 03456F691BBA6D51001C86A3 /* Debug */, + 03456F6A1BBA6D51001C86A3 /* Release */, + ); + defaultConfigurationIsVisible = 0; + }; + 03456F6B1BBA6D51001C86A3 /* Build configuration list for PBXNativeTarget "iOS9ExampleTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 03456F6C1BBA6D51001C86A3 /* Debug */, + 03456F6D1BBA6D51001C86A3 /* Release */, + ); + defaultConfigurationIsVisible = 0; + }; +/* End XCConfigurationList section */ + }; + rootObject = 03456F3B1BBA6D51001C86A3 /* Project object */; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/AppDelegate.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/AppDelegate.h new file mode 100644 index 0000000000..1b80991011 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/AppDelegate.h @@ -0,0 +1,17 @@ +// +// AppDelegate.h +// iOS9Example +// +// Created by Erik Doernenburg on 29/09/2015. +// Copyright © 2015 Erik Doernenburg. All rights reserved. +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + + +@end + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/AppDelegate.m b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/AppDelegate.m new file mode 100644 index 0000000000..48c3d9bc1f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/AppDelegate.m @@ -0,0 +1,61 @@ +// +// AppDelegate.m +// iOS9Example +// +// Created by Erik Doernenburg on 29/09/2015. +// Copyright © 2015 Erik Doernenburg. All rights reserved. +// + +#import "AppDelegate.h" +#import "DetailViewController.h" + +@interface AppDelegate () + +@end + +@implementation AppDelegate + + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Override point for customization after application launch. + UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController; + UINavigationController *navigationController = [splitViewController.viewControllers lastObject]; + navigationController.topViewController.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem; + splitViewController.delegate = self; + return YES; +} + +- (void)applicationWillResignActive:(UIApplication *)application { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. +} + +- (void)applicationDidEnterBackground:(UIApplication *)application { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. +} + +- (void)applicationWillEnterForeground:(UIApplication *)application { + // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. +} + +- (void)applicationDidBecomeActive:(UIApplication *)application { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. +} + +- (void)applicationWillTerminate:(UIApplication *)application { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. +} + +#pragma mark - Split view + +- (BOOL)splitViewController:(UISplitViewController *)splitViewController collapseSecondaryViewController:(UIViewController *)secondaryViewController ontoPrimaryViewController:(UIViewController *)primaryViewController { + if ([secondaryViewController isKindOfClass:[UINavigationController class]] && [[(UINavigationController *)secondaryViewController topViewController] isKindOfClass:[DetailViewController class]] && ([(DetailViewController *)[(UINavigationController *)secondaryViewController topViewController] detailItem] == nil)) { + // Return YES to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded. + return YES; + } else { + return NO; + } +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/Assets.xcassets/AppIcon.appiconset/Contents.json b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..118c98f746 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,38 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/Base.lproj/LaunchScreen.storyboard b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..2e721e1833 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/Base.lproj/Main.storyboard b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/Base.lproj/Main.storyboard new file mode 100644 index 0000000000..4a0ef53c1c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/Base.lproj/Main.storyboard @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/DetailViewController.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/DetailViewController.h new file mode 100644 index 0000000000..d94d610675 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/DetailViewController.h @@ -0,0 +1,17 @@ +// +// DetailViewController.h +// iOS9Example +// +// Created by Erik Doernenburg on 29/09/2015. +// Copyright © 2015 Erik Doernenburg. All rights reserved. +// + +#import + +@interface DetailViewController : UIViewController + +@property (strong, nonatomic) id detailItem; +@property (weak, nonatomic) IBOutlet UILabel *detailDescriptionLabel; + +@end + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/DetailViewController.m b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/DetailViewController.m new file mode 100644 index 0000000000..9d06448aae --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/DetailViewController.m @@ -0,0 +1,46 @@ +// +// DetailViewController.m +// iOS9Example +// +// Created by Erik Doernenburg on 29/09/2015. +// Copyright © 2015 Erik Doernenburg. All rights reserved. +// + +#import "DetailViewController.h" + +@interface DetailViewController () + +@end + +@implementation DetailViewController + +#pragma mark - Managing the detail item + +- (void)setDetailItem:(id)newDetailItem { + if (_detailItem != newDetailItem) { + _detailItem = newDetailItem; + + // Update the view. + [self configureView]; + } +} + +- (void)configureView { + // Update the user interface for the detail item. + if (self.detailItem) { + self.detailDescriptionLabel.text = [self.detailItem description]; + } +} + +- (void)viewDidLoad { + [super viewDidLoad]; + // Do any additional setup after loading the view, typically from a nib. + [self configureView]; +} + +- (void)didReceiveMemoryWarning { + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/Info.plist b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/Info.plist new file mode 100644 index 0000000000..e214702ec8 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/Info.plist @@ -0,0 +1,50 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UIStatusBarTintParameters + + UINavigationBar + + Style + UIBarStyleDefault + Translucent + + + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/MasterViewController.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/MasterViewController.h new file mode 100644 index 0000000000..15ad246255 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/MasterViewController.h @@ -0,0 +1,19 @@ +// +// MasterViewController.h +// iOS9Example +// +// Created by Erik Doernenburg on 29/09/2015. +// Copyright © 2015 Erik Doernenburg. All rights reserved. +// + +#import + +@class DetailViewController; + +@interface MasterViewController : UITableViewController + +@property (strong, nonatomic) DetailViewController *detailViewController; + + +@end + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/MasterViewController.m b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/MasterViewController.m new file mode 100644 index 0000000000..1f07bb3f95 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/MasterViewController.m @@ -0,0 +1,93 @@ +// +// MasterViewController.m +// iOS9Example +// +// Created by Erik Doernenburg on 29/09/2015. +// Copyright © 2015 Erik Doernenburg. All rights reserved. +// + +#import "MasterViewController.h" +#import "DetailViewController.h" + +@interface MasterViewController () + +@property NSMutableArray *objects; +@end + +@implementation MasterViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + // Do any additional setup after loading the view, typically from a nib. + self.navigationItem.leftBarButtonItem = self.editButtonItem; + + UIBarButtonItem *addButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(insertNewObject:)]; + self.navigationItem.rightBarButtonItem = addButton; + self.detailViewController = (DetailViewController *)[[self.splitViewController.viewControllers lastObject] topViewController]; +} + +- (void)viewWillAppear:(BOOL)animated { + self.clearsSelectionOnViewWillAppear = self.splitViewController.isCollapsed; + [super viewWillAppear:animated]; +} + +- (void)didReceiveMemoryWarning { + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +- (void)insertNewObject:(id)sender { + if (!self.objects) { + self.objects = [[NSMutableArray alloc] init]; + } + [self.objects insertObject:[NSDate date] atIndex:0]; + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0]; + [self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; +} + +#pragma mark - Segues + +- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { + if ([[segue identifier] isEqualToString:@"showDetail"]) { + NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow]; + NSDate *object = self.objects[indexPath.row]; + DetailViewController *controller = (DetailViewController *)[[segue destinationViewController] topViewController]; + [controller setDetailItem:object]; + controller.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem; + controller.navigationItem.leftItemsSupplementBackButton = YES; + } +} + +#pragma mark - Table View + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return 1; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return self.objects.count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; + + NSDate *object = self.objects[indexPath.row]; + cell.textLabel.text = [object description]; + return cell; +} + +- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { + // Return NO if you do not want the specified item to be editable. + return YES; +} + +- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { + if (editingStyle == UITableViewCellEditingStyleDelete) { + [self.objects removeObjectAtIndex:indexPath.row]; + [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; + } else if (editingStyle == UITableViewCellEditingStyleInsert) { + // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view. + } +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/main.m b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/main.m new file mode 100644 index 0000000000..15f0d27df8 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9Example/main.m @@ -0,0 +1,16 @@ +// +// main.m +// iOS9Example +// +// Created by Erik Doernenburg on 29/09/2015. +// Copyright © 2015 Erik Doernenburg. All rights reserved. +// + +#import +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9ExampleTests/Info.plist b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9ExampleTests/Info.plist new file mode 100644 index 0000000000..ba72822e87 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9ExampleTests/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 + 1 + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9ExampleTests/iOS9ExampleTests.m b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9ExampleTests/iOS9ExampleTests.m new file mode 100644 index 0000000000..dde436f43e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/iOS9ExampleTests/iOS9ExampleTests.m @@ -0,0 +1,41 @@ +// +// iOS9ExampleTests.m +// iOS9ExampleTests +// +// Created by Erik Doernenburg on 29/09/2015. +// Copyright © 2015 Erik Doernenburg. All rights reserved. +// + +#import +#import +#import "MasterViewController.h" + +@interface iOS9ExampleTests : XCTestCase + +@end + +@implementation iOS9ExampleTests + +- (void)testMasterViewControllerDeletesItemsFromTableView +{ + // Test set-up + + MasterViewController *controller = [[MasterViewController alloc] init]; + NSIndexPath *dummyIndexPath = [NSIndexPath indexPathForRow:1 inSection:0]; + + id tableViewMock = OCMClassMock([UITableView class]); + + // Invoke functionality to be tested + // If you want to see the test fail you can, for example, change the editing style to + // UITableViewCellEditingStyleNone. In that case the method in the controller does not + // make a call to the table view and the mock will raise an exception when verify is + // called further down. + + [controller tableView:tableViewMock commitEditingStyle:UITableViewCellEditingStyleDelete forRowAtIndexPath:dummyIndexPath]; + + // Verify that expected methods were called + + OCMVerify([tableViewMock deleteRowsAtIndexPaths:@[dummyIndexPath] withRowAnimation:UITableViewRowAnimationFade]); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/NSNotificationCenter+OCMAdditions.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/NSNotificationCenter+OCMAdditions.h new file mode 100644 index 0000000000..7d58aabea8 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/NSNotificationCenter+OCMAdditions.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2009-2016 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@class OCObserverMockObject; + + +@interface NSNotificationCenter(OCMAdditions) + +- (void)addMockObserver:(OCObserverMockObject *)notificationObserver name:(NSString *)notificationName object:(id)notificationSender; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/OCMArg.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/OCMArg.h new file mode 100644 index 0000000000..6df735e99e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/OCMArg.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2009-2016 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@interface OCMArg : NSObject + +// constraining arguments + ++ (id)any; ++ (SEL)anySelector; ++ (void *)anyPointer; ++ (id __autoreleasing *)anyObjectRef; ++ (id)isNil; ++ (id)isNotNil; ++ (id)isEqual:(id)value; ++ (id)isNotEqual:(id)value; ++ (id)isKindOfClass:(Class)cls; ++ (id)checkWithSelector:(SEL)selector onObject:(id)anObject; ++ (id)checkWithBlock:(BOOL (^)(id obj))block; + +// manipulating arguments + ++ (id *)setTo:(id)value; ++ (void *)setToValue:(NSValue *)value; ++ (id)invokeBlock; ++ (id)invokeBlockWithArgs:(id)first,... NS_REQUIRES_NIL_TERMINATION; + ++ (id)defaultValue; + +// internal use only + ++ (id)resolveSpecialValues:(NSValue *)value; + +@end + +#define OCMOCK_ANY [OCMArg any] + +#if defined(__GNUC__) && !defined(__STRICT_ANSI__) + #define OCMOCK_VALUE(variable) \ + ({ __typeof__(variable) __v = (variable); [NSValue value:&__v withObjCType:@encode(__typeof__(__v))]; }) +#else + #define OCMOCK_VALUE(variable) [NSValue value:&variable withObjCType:@encode(__typeof__(variable))] +#endif + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/OCMConstraint.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/OCMConstraint.h new file mode 100644 index 0000000000..19fc1a713c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/OCMConstraint.h @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2007-2016 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + + +@interface OCMConstraint : NSObject + ++ (instancetype)constraint; +- (BOOL)evaluate:(id)value; + +// if you are looking for any, isNil, etc, they have moved to OCMArg + +// try to use [OCMArg checkWith...] instead of the constraintWith... methods below + ++ (instancetype)constraintWithSelector:(SEL)aSelector onObject:(id)anObject; ++ (instancetype)constraintWithSelector:(SEL)aSelector onObject:(id)anObject withValue:(id)aValue; + + +@end + +@interface OCMAnyConstraint : OCMConstraint +@end + +@interface OCMIsNilConstraint : OCMConstraint +@end + +@interface OCMIsNotNilConstraint : OCMConstraint +@end + +@interface OCMIsNotEqualConstraint : OCMConstraint +{ + @public + id testValue; +} + +@end + +@interface OCMInvocationConstraint : OCMConstraint +{ + @public + NSInvocation *invocation; +} + +@end + +@interface OCMBlockConstraint : OCMConstraint +{ + BOOL (^block)(id); +} + +- (instancetype)initWithConstraintBlock:(BOOL (^)(id))block; + +@end + + +#define CONSTRAINT(aSelector) [OCMConstraint constraintWithSelector:aSelector onObject:self] +#define CONSTRAINTV(aSelector, aValue) [OCMConstraint constraintWithSelector:aSelector onObject:self withValue:(aValue)] diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/OCMFunctions.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/OCMFunctions.h new file mode 100644 index 0000000000..b0c2df353c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/OCMFunctions.h @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2014-2016 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + + +#if defined(__cplusplus) +#define OCMOCK_EXTERN extern "C" +#else +#define OCMOCK_EXTERN extern +#endif + + +OCMOCK_EXTERN BOOL OCMIsObjectType(const char *objCType); diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/OCMLocation.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/OCMLocation.h new file mode 100644 index 0000000000..7870c52978 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/OCMLocation.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2014-2016 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCMFunctions.h" + + +@interface OCMLocation : NSObject +{ + id testCase; + NSString *file; + NSUInteger line; +} + ++ (instancetype)locationWithTestCase:(id)aTestCase file:(NSString *)aFile line:(NSUInteger)aLine; + +- (instancetype)initWithTestCase:(id)aTestCase file:(NSString *)aFile line:(NSUInteger)aLine; + +- (id)testCase; +- (NSString *)file; +- (NSUInteger)line; + +@end + +OCMOCK_EXTERN OCMLocation *OCMMakeLocation(id testCase, const char *file, int line); diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/OCMMacroState.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/OCMMacroState.h new file mode 100644 index 0000000000..dba41bebdc --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/OCMMacroState.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2014-2016 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@class OCMLocation; +@class OCMRecorder; +@class OCMStubRecorder; +@class OCMockObject; + + +@interface OCMMacroState : NSObject +{ + OCMRecorder *recorder; +} + ++ (void)beginStubMacro; ++ (OCMStubRecorder *)endStubMacro; + ++ (void)beginExpectMacro; ++ (OCMStubRecorder *)endExpectMacro; + ++ (void)beginRejectMacro; ++ (OCMStubRecorder *)endRejectMacro; + ++ (void)beginVerifyMacroAtLocation:(OCMLocation *)aLocation; ++ (void)endVerifyMacro; + ++ (OCMMacroState *)globalState; + +- (OCMRecorder *)recorder; + +- (void)switchToClassMethod; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/OCMRecorder.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/OCMRecorder.h new file mode 100644 index 0000000000..9670d085f4 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/OCMRecorder.h @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2014-2016 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@class OCMockObject; +@class OCMInvocationMatcher; + + +@interface OCMRecorder : NSProxy +{ + OCMockObject *mockObject; + OCMInvocationMatcher *invocationMatcher; +} + +- (instancetype)init; +- (instancetype)initWithMockObject:(OCMockObject *)aMockObject; + +- (void)setMockObject:(OCMockObject *)aMockObject; + +- (OCMInvocationMatcher *)invocationMatcher; + +- (id)classMethod; +- (id)ignoringNonObjectArgs; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/OCMStubRecorder.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/OCMStubRecorder.h new file mode 100644 index 0000000000..e32029fc22 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/OCMStubRecorder.h @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2004-2016 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import +#import + +@interface OCMStubRecorder : OCMRecorder + +- (id)andReturn:(id)anObject; +- (id)andReturnValue:(NSValue *)aValue; +- (id)andThrow:(NSException *)anException; +- (id)andPost:(NSNotification *)aNotification; +- (id)andCall:(SEL)selector onObject:(id)anObject; +- (id)andDo:(void (^)(NSInvocation *invocation))block; +- (id)andForwardToRealObject; + +@end + + +@interface OCMStubRecorder (Properties) + +#define andReturn(aValue) _andReturn(({ \ + __typeof__(aValue) _val = (aValue); \ + NSValue *_nsval = [NSValue value:&_val withObjCType:@encode(__typeof__(_val))]; \ + if (OCMIsObjectType(@encode(__typeof(_val)))) { \ + objc_setAssociatedObject(_nsval, "OCMAssociatedBoxedValue", *(__unsafe_unretained id *) (void *) &_val, OBJC_ASSOCIATION_RETAIN); \ + } \ + _nsval; \ +})) +@property (nonatomic, readonly) OCMStubRecorder *(^ _andReturn)(NSValue *); + +#define andThrow(anException) _andThrow(anException) +@property (nonatomic, readonly) OCMStubRecorder *(^ _andThrow)(NSException *); + +#define andPost(aNotification) _andPost(aNotification) +@property (nonatomic, readonly) OCMStubRecorder *(^ _andPost)(NSNotification *); + +#define andCall(anObject, aSelector) _andCall(anObject, aSelector) +@property (nonatomic, readonly) OCMStubRecorder *(^ _andCall)(id, SEL); + +#define andDo(aBlock) _andDo(aBlock) +@property (nonatomic, readonly) OCMStubRecorder *(^ _andDo)(void (^)(NSInvocation *)); + +#define andForwardToRealObject() _andForwardToRealObject() +@property (nonatomic, readonly) OCMStubRecorder *(^ _andForwardToRealObject)(void); + +@end + + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/OCMock.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/OCMock.h new file mode 100644 index 0000000000..9d558135bf --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/OCMock.h @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2004-2016 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import +#import +#import +#import +#import +#import +#import +#import + + +#define OCMClassMock(cls) [OCMockObject niceMockForClass:cls] + +#define OCMStrictClassMock(cls) [OCMockObject mockForClass:cls] + +#define OCMProtocolMock(protocol) [OCMockObject niceMockForProtocol:protocol] + +#define OCMStrictProtocolMock(protocol) [OCMockObject mockForProtocol:protocol] + +#define OCMPartialMock(obj) [OCMockObject partialMockForObject:obj] + +#define OCMObserverMock() [OCMockObject observerMock] + + +#define OCMStub(invocation) \ +({ \ + _OCMSilenceWarnings( \ + [OCMMacroState beginStubMacro]; \ + OCMStubRecorder *recorder = nil; \ + @try{ \ + invocation; \ + }@finally{ \ + recorder = [OCMMacroState endStubMacro]; \ + } \ + recorder; \ + ); \ +}) + +#define OCMExpect(invocation) \ +({ \ + _OCMSilenceWarnings( \ + [OCMMacroState beginExpectMacro]; \ + OCMStubRecorder *recorder = nil; \ + @try{ \ + invocation; \ + }@finally{ \ + recorder = [OCMMacroState endExpectMacro]; \ + } \ + recorder; \ + ); \ +}) + +#define OCMReject(invocation) \ +({ \ + _OCMSilenceWarnings( \ + [OCMMacroState beginRejectMacro]; \ + OCMStubRecorder *recorder = nil; \ + @try{ \ + invocation; \ + }@finally{ \ + recorder = [OCMMacroState endRejectMacro]; \ + } \ + recorder; \ + ); \ +}) + +#define ClassMethod(invocation) \ + _OCMSilenceWarnings( \ + [[OCMMacroState globalState] switchToClassMethod]; \ + invocation; \ + ); + + +#define OCMVerifyAll(mock) [mock verifyAtLocation:OCMMakeLocation(self, __FILE__, __LINE__)] + +#define OCMVerifyAllWithDelay(mock, delay) [mock verifyWithDelay:delay atLocation:OCMMakeLocation(self, __FILE__, __LINE__)] + +#define OCMVerify(invocation) \ +({ \ + _OCMSilenceWarnings( \ + [OCMMacroState beginVerifyMacroAtLocation:OCMMakeLocation(self, __FILE__, __LINE__)]; \ + @try{ \ + invocation; \ + }@finally{ \ + [OCMMacroState endVerifyMacro]; \ + } \ + ); \ +}) + +#define _OCMSilenceWarnings(macro) \ +({ \ + _Pragma("clang diagnostic push") \ + _Pragma("clang diagnostic ignored \"-Wunused-value\"") \ + _Pragma("clang diagnostic ignored \"-Wunused-getter-return-value\"") \ + macro \ + _Pragma("clang diagnostic pop") \ +}) diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/OCMockObject.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/OCMockObject.h new file mode 100644 index 0000000000..31f7ac41d9 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/include/OCMock/OCMockObject.h @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2004-2016 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@class OCMLocation; +@class OCMInvocationStub; +@class OCMStubRecorder; +@class OCMInvocationMatcher; +@class OCMInvocationExpectation; + + +@interface OCMockObject : NSProxy +{ + BOOL isNice; + BOOL expectationOrderMatters; + NSMutableArray *stubs; + NSMutableArray *expectations; + NSMutableArray *exceptions; + NSMutableArray *invocations; +} + ++ (id)mockForClass:(Class)aClass; ++ (id)mockForProtocol:(Protocol *)aProtocol; ++ (id)partialMockForObject:(NSObject *)anObject; + ++ (id)niceMockForClass:(Class)aClass; ++ (id)niceMockForProtocol:(Protocol *)aProtocol; + ++ (id)observerMock; + +- (instancetype)init; + +- (void)setExpectationOrderMatters:(BOOL)flag; + +- (id)stub; +- (id)expect; +- (id)reject; + +- (id)verify; +- (id)verifyAtLocation:(OCMLocation *)location; + +- (void)verifyWithDelay:(NSTimeInterval)delay; +- (void)verifyWithDelay:(NSTimeInterval)delay atLocation:(OCMLocation *)location; + +- (void)stopMocking; + +// internal use only + +- (void)addStub:(OCMInvocationStub *)aStub; +- (void)addExpectation:(OCMInvocationExpectation *)anExpectation; + +- (BOOL)handleInvocation:(NSInvocation *)anInvocation; +- (void)handleUnRecordedInvocation:(NSInvocation *)anInvocation; +- (BOOL)handleSelector:(SEL)sel; + +- (void)verifyInvocation:(OCMInvocationMatcher *)matcher; +- (void)verifyInvocation:(OCMInvocationMatcher *)matcher atLocation:(OCMLocation *)location; + +@end + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/lib/libOCMock.a b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/lib/libOCMock.a new file mode 100644 index 0000000000..8edb6e3413 Binary files /dev/null and b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iOS9Example/usr/lib/libOCMock.a differ diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Classes/RootViewController.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Classes/RootViewController.h new file mode 100644 index 0000000000..feed5fe647 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Classes/RootViewController.h @@ -0,0 +1,14 @@ +// +// RootViewController.h +// iPhoneExample +// +// Created by Erik Doernenburg on 20/07/10. +// Copyright Mulle Kybernetik 2010. All rights reserved. +// + +#import + +@interface RootViewController : UITableViewController { +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Classes/RootViewController.m b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Classes/RootViewController.m new file mode 100644 index 0000000000..436b91a7bb --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Classes/RootViewController.m @@ -0,0 +1,62 @@ +// +// RootViewController.m +// iPhoneExample +// +// Created by Erik Doernenburg on 20/07/10. +// Copyright Mulle Kybernetik 2010. All rights reserved. +// + +#import "RootViewController.h" + + +@implementation RootViewController + + +#pragma mark - +#pragma mark Table view data source + +// Customize the number of sections in the table view. +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return 1; +} + + +// Customize the number of rows in the table view. +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return 1; +} + + +// Customize the appearance of table view cells. +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + + static NSString *CellIdentifier = @"HelloWorldCell"; + + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; + if (cell == nil) { + cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; + } + + cell.textLabel.text = @"Hello World!"; + + return cell; +} + + +#pragma mark - +#pragma mark Table view delegate + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + + /* + <#DetailViewController#> *detailViewController = [[<#DetailViewController#> alloc] initWithNibName:@"<#Nib name#>" bundle:nil]; + // ... + // Pass the selected object to the new view controller. + [self.navigationController pushViewController:detailViewController animated:YES]; + [detailViewController release]; + */ +} + + +@end + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Classes/iPhoneExampleAppDelegate.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Classes/iPhoneExampleAppDelegate.h new file mode 100644 index 0000000000..e80f3fdf73 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Classes/iPhoneExampleAppDelegate.h @@ -0,0 +1,21 @@ +// +// iPhoneExampleAppDelegate.h +// iPhoneExample +// +// Created by Erik Doernenburg on 20/07/10. +// Copyright Mulle Kybernetik 2010. All rights reserved. +// + +#import + +@interface iPhoneExampleAppDelegate : NSObject { + + UIWindow *window; + UINavigationController *navigationController; +} + +@property (nonatomic, strong) IBOutlet UIWindow *window; +@property (nonatomic, strong) IBOutlet UINavigationController *navigationController; + +@end + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Classes/iPhoneExampleAppDelegate.m b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Classes/iPhoneExampleAppDelegate.m new file mode 100644 index 0000000000..5be22d9292 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Classes/iPhoneExampleAppDelegate.m @@ -0,0 +1,90 @@ +// +// iPhoneExampleAppDelegate.m +// iPhoneExample +// +// Created by Erik Doernenburg on 20/07/10. +// Copyright Mulle Kybernetik 2010. All rights reserved. +// + +#import "iPhoneExampleAppDelegate.h" +#import "RootViewController.h" + + +@implementation iPhoneExampleAppDelegate + +@synthesize window; +@synthesize navigationController; + + +#pragma mark - +#pragma mark Application lifecycle + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + + // Override point for customization after application launch. + + // Add the navigation controller's view to the window and display. + [window addSubview:navigationController.view]; + [window makeKeyAndVisible]; + + return YES; +} + + +- (void)applicationWillResignActive:(UIApplication *)application { + /* + Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. + */ +} + + +- (void)applicationDidEnterBackground:(UIApplication *)application { + /* + Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + If your application supports background execution, called instead of applicationWillTerminate: when the user quits. + */ +} + + +- (void)applicationWillEnterForeground:(UIApplication *)application { + /* + Called as part of transition from the background to the inactive state: here you can undo many of the changes made on entering the background. + */ +} + + +- (void)applicationDidBecomeActive:(UIApplication *)application { + /* + Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + */ +} + + +- (void)applicationWillTerminate:(UIApplication *)application { + /* + Called when the application is about to terminate. + See also applicationDidEnterBackground:. + */ +} + + +#pragma mark - +#pragma mark Memory management + +- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application { + /* + Free up as much memory as possible by purging cached data objects that can be recreated (or reloaded from disk) later. + */ +} + + +- (void)dealloc { + [navigationController release]; + [window release]; + [super dealloc]; +} + + +@end + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Libraries/Headers/OCMock/NSNotificationCenter+OCMAdditions.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Libraries/Headers/OCMock/NSNotificationCenter+OCMAdditions.h new file mode 100644 index 0000000000..7d5d6d1894 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Libraries/Headers/OCMock/NSNotificationCenter+OCMAdditions.h @@ -0,0 +1,15 @@ +//--------------------------------------------------------------------------------------- +// $Id: NSNotificationCenter+OCMAdditions.h 57 2010-07-19 06:14:27Z erik $ +// Copyright (c) 2009 by Mulle Kybernetik. See License file for details. +//--------------------------------------------------------------------------------------- + +#import + +@class OCMockObserver; + + +@interface NSNotificationCenter(OCMAdditions) + +- (void)addMockObserver:(OCMockObserver *)notificationObserver name:(NSString *)notificationName object:(id)notificationSender; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Libraries/Headers/OCMock/OCMArg.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Libraries/Headers/OCMock/OCMArg.h new file mode 100644 index 0000000000..e174a33780 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Libraries/Headers/OCMock/OCMArg.h @@ -0,0 +1,33 @@ +//--------------------------------------------------------------------------------------- +// $Id: OCMArg.h 57 2010-07-19 06:14:27Z erik $ +// Copyright (c) 2009-2010 by Mulle Kybernetik. See License file for details. +//--------------------------------------------------------------------------------------- + +#import + +@interface OCMArg : NSObject + +// constraining arguments + ++ (id)any; ++ (void *)anyPointer; ++ (id)isNil; ++ (id)isNotNil; ++ (id)isNotEqual:(id)value; ++ (id)checkWithSelector:(SEL)selector onObject:(id)anObject; +#if NS_BLOCKS_AVAILABLE ++ (id)checkWithBlock:(BOOL (^)(id))block; +#endif + +// manipulating arguments + ++ (id *)setTo:(id)value; + +// internal use only + ++ (id)resolveSpecialValues:(NSValue *)value; + +@end + +#define OCMOCK_ANY [OCMArg any] +#define OCMOCK_VALUE(variable) [NSValue value:&variable withObjCType:@encode(typeof(variable))] diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Libraries/Headers/OCMock/OCMConstraint.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Libraries/Headers/OCMock/OCMConstraint.h new file mode 100644 index 0000000000..72b23e89ce --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Libraries/Headers/OCMock/OCMConstraint.h @@ -0,0 +1,64 @@ +//--------------------------------------------------------------------------------------- +// $Id: OCMConstraint.h 57 2010-07-19 06:14:27Z erik $ +// Copyright (c) 2007-2010 by Mulle Kybernetik. See License file for details. +//--------------------------------------------------------------------------------------- + +#import + + +@interface OCMConstraint : NSObject + ++ (id)constraint; +- (BOOL)evaluate:(id)value; + +// if you are looking for any, isNil, etc, they have moved to OCMArg + +// try to use [OCMArg checkWith...] instead of the constraintWith... methods below + ++ (id)constraintWithSelector:(SEL)aSelector onObject:(id)anObject; ++ (id)constraintWithSelector:(SEL)aSelector onObject:(id)anObject withValue:(id)aValue; + + +@end + +@interface OCMAnyConstraint : OCMConstraint +@end + +@interface OCMIsNilConstraint : OCMConstraint +@end + +@interface OCMIsNotNilConstraint : OCMConstraint +@end + +@interface OCMIsNotEqualConstraint : OCMConstraint +{ + @public + id testValue; +} + +@end + +@interface OCMInvocationConstraint : OCMConstraint +{ + @public + NSInvocation *invocation; +} + +@end + +#if NS_BLOCKS_AVAILABLE + +@interface OCMBlockConstraint : OCMConstraint +{ + BOOL (^block)(id); +} + +- (id)initWithConstraintBlock:(BOOL (^)(id))block; + +@end + +#endif + + +#define CONSTRAINT(aSelector) [OCMConstraint constraintWithSelector:aSelector onObject:self] +#define CONSTRAINTV(aSelector, aValue) [OCMConstraint constraintWithSelector:aSelector onObject:self withValue:(aValue)] diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Libraries/Headers/OCMock/OCMock.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Libraries/Headers/OCMock/OCMock.h new file mode 100644 index 0000000000..892b3cc225 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Libraries/Headers/OCMock/OCMock.h @@ -0,0 +1,10 @@ +//--------------------------------------------------------------------------------------- +// $Id: OCMock.h 39 2009-04-09 05:31:28Z erik $ +// Copyright (c) 2004-2008 by Mulle Kybernetik. See License file for details. +//--------------------------------------------------------------------------------------- + +#import +#import +#import +#import +#import diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Libraries/Headers/OCMock/OCMockObject.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Libraries/Headers/OCMock/OCMockObject.h new file mode 100644 index 0000000000..a334b5759c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Libraries/Headers/OCMock/OCMockObject.h @@ -0,0 +1,41 @@ +//--------------------------------------------------------------------------------------- +// $Id: OCMockObject.h 52 2009-08-14 07:21:10Z erik $ +// Copyright (c) 2004-2008 by Mulle Kybernetik. See License file for details. +//--------------------------------------------------------------------------------------- + +#import + +@interface OCMockObject : NSProxy +{ + BOOL isNice; + BOOL expectationOrderMatters; + NSMutableArray *recorders; + NSMutableArray *expectations; + NSMutableArray *exceptions; +} + ++ (id)mockForClass:(Class)aClass; ++ (id)mockForProtocol:(Protocol *)aProtocol; ++ (id)partialMockForObject:(NSObject *)anObject; + ++ (id)niceMockForClass:(Class)aClass; ++ (id)niceMockForProtocol:(Protocol *)aProtocol; + ++ (id)observerMock; + +- (id)init; + +- (void)setExpectationOrderMatters:(BOOL)flag; + +- (id)stub; +- (id)expect; + +- (void)verify; + +// internal use only + +- (id)getNewRecorder; +- (BOOL)handleInvocation:(NSInvocation *)anInvocation; +- (void)handleUnRecordedInvocation:(NSInvocation *)anInvocation; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Libraries/Headers/OCMock/OCMockRecorder.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Libraries/Headers/OCMock/OCMockRecorder.h new file mode 100644 index 0000000000..7302f7f308 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Libraries/Headers/OCMock/OCMockRecorder.h @@ -0,0 +1,31 @@ +//--------------------------------------------------------------------------------------- +// $Id: OCMockRecorder.h 57 2010-07-19 06:14:27Z erik $ +// Copyright (c) 2004-2010 by Mulle Kybernetik. See License file for details. +//--------------------------------------------------------------------------------------- + +#import + +@interface OCMockRecorder : NSProxy +{ + id signatureResolver; + NSInvocation *recordedInvocation; + NSMutableArray *invocationHandlers; +} + +- (id)initWithSignatureResolver:(id)anObject; + +- (BOOL)matchesInvocation:(NSInvocation *)anInvocation; +- (void)releaseInvocation; + +- (id)andReturn:(id)anObject; +- (id)andReturnValue:(NSValue *)aValue; +- (id)andThrow:(NSException *)anException; +- (id)andPost:(NSNotification *)aNotification; +- (id)andCall:(SEL)selector onObject:(id)anObject; +#if NS_BLOCKS_AVAILABLE +- (id)andDo:(void (^)(NSInvocation *))block; +#endif + +- (NSArray *)invocationHandlers; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Libraries/libOCMock.a b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Libraries/libOCMock.a new file mode 100644 index 0000000000..813d8a571d Binary files /dev/null and b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Libraries/libOCMock.a differ diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/MainWindow.xib b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/MainWindow.xib new file mode 100644 index 0000000000..64262ef6d0 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/MainWindow.xib @@ -0,0 +1,542 @@ + + + + 1024 + 10D571 + 786 + 1038.29 + 460.00 + + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + 112 + + + YES + + + + YES + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + YES + + YES + + + YES + + + + YES + + IBFilesOwner + IBCocoaTouchFramework + + + IBFirstResponder + IBCocoaTouchFramework + + + IBCocoaTouchFramework + + + + 1316 + + {320, 480} + + 1 + MSAxIDEAA + + NO + NO + + IBCocoaTouchFramework + YES + + + + + 1 + + IBCocoaTouchFramework + NO + + + 256 + {0, 0} + NO + YES + YES + IBCocoaTouchFramework + + + YES + + + + IBCocoaTouchFramework + + + RootViewController + + + 1 + + IBCocoaTouchFramework + NO + + + + + + + YES + + + delegate + + + + 4 + + + + window + + + + 5 + + + + navigationController + + + + 15 + + + + + YES + + 0 + + + + + + 2 + + + YES + + + + + -1 + + + File's Owner + + + 3 + + + + + -2 + + + + + 9 + + + YES + + + + + + + 11 + + + + + 13 + + + YES + + + + + + 14 + + + + + + + YES + + YES + -1.CustomClassName + -2.CustomClassName + 11.IBPluginDependency + 13.CustomClassName + 13.IBPluginDependency + 2.IBAttributePlaceholdersKey + 2.IBEditorWindowLastContentRect + 2.IBPluginDependency + 3.CustomClassName + 3.IBPluginDependency + 9.IBEditorWindowLastContentRect + 9.IBPluginDependency + + + YES + UIApplication + UIResponder + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + RootViewController + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + YES + + + YES + + + {{673, 376}, {320, 480}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + iPhoneExampleAppDelegate + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + {{186, 376}, {320, 480}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + YES + + + YES + + + + + YES + + + YES + + + + 16 + + + + YES + + RootViewController + UITableViewController + + IBProjectSource + Classes/RootViewController.h + + + + UIWindow + UIView + + IBUserSource + + + + + iPhoneExampleAppDelegate + NSObject + + YES + + YES + navigationController + window + + + YES + UINavigationController + UIWindow + + + + YES + + YES + navigationController + window + + + YES + + navigationController + UINavigationController + + + window + UIWindow + + + + + IBProjectSource + Classes/iPhoneExampleAppDelegate.h + + + + + YES + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSError.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSFileManager.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyValueCoding.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyValueObserving.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyedArchiver.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSObject.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSRunLoop.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSThread.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURL.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURLConnection.h + + + + NSObject + + IBFrameworkSource + UIKit.framework/Headers/UIAccessibility.h + + + + NSObject + + IBFrameworkSource + UIKit.framework/Headers/UINibLoading.h + + + + NSObject + + IBFrameworkSource + UIKit.framework/Headers/UIResponder.h + + + + UIApplication + UIResponder + + IBFrameworkSource + UIKit.framework/Headers/UIApplication.h + + + + UIBarButtonItem + UIBarItem + + IBFrameworkSource + UIKit.framework/Headers/UIBarButtonItem.h + + + + UIBarItem + NSObject + + IBFrameworkSource + UIKit.framework/Headers/UIBarItem.h + + + + UINavigationBar + UIView + + IBFrameworkSource + UIKit.framework/Headers/UINavigationBar.h + + + + UINavigationController + UIViewController + + IBFrameworkSource + UIKit.framework/Headers/UINavigationController.h + + + + UINavigationItem + NSObject + + + + UIResponder + NSObject + + + + UISearchBar + UIView + + IBFrameworkSource + UIKit.framework/Headers/UISearchBar.h + + + + UISearchDisplayController + NSObject + + IBFrameworkSource + UIKit.framework/Headers/UISearchDisplayController.h + + + + UITableViewController + UIViewController + + IBFrameworkSource + UIKit.framework/Headers/UITableViewController.h + + + + UIView + + IBFrameworkSource + UIKit.framework/Headers/UITextField.h + + + + UIView + UIResponder + + IBFrameworkSource + UIKit.framework/Headers/UIView.h + + + + UIViewController + + + + UIViewController + + IBFrameworkSource + UIKit.framework/Headers/UIPopoverController.h + + + + UIViewController + + IBFrameworkSource + UIKit.framework/Headers/UISplitViewController.h + + + + UIViewController + + IBFrameworkSource + UIKit.framework/Headers/UITabBarController.h + + + + UIViewController + UIResponder + + IBFrameworkSource + UIKit.framework/Headers/UIViewController.h + + + + UIWindow + UIView + + IBFrameworkSource + UIKit.framework/Headers/UIWindow.h + + + + + 0 + IBCocoaTouchFramework + + com.apple.InterfaceBuilder.CocoaTouchPlugin.iPhoneOS + + + + com.apple.InterfaceBuilder.CocoaTouchPlugin.InterfaceBuilder3 + + + YES + iPhoneExample.xcodeproj + 3 + 112 + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/OCMockLogo.png b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/OCMockLogo.png new file mode 100644 index 0000000000..c5a210993c Binary files /dev/null and b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/OCMockLogo.png differ diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/RootViewController.xib b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/RootViewController.xib new file mode 100644 index 0000000000..9b230756c5 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/RootViewController.xib @@ -0,0 +1,384 @@ + + + + 784 + 10D541 + 760 + 1038.29 + 460.00 + + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + 81 + + + YES + + + + YES + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + YES + + YES + + + YES + + + + YES + + IBFilesOwner + IBCocoaTouchFramework + + + IBFirstResponder + IBCocoaTouchFramework + + + + 274 + {320, 247} + + + 3 + MQA + + NO + YES + NO + IBCocoaTouchFramework + NO + 1 + 0 + YES + 44 + 22 + 22 + + + + + YES + + + view + + + + 3 + + + + dataSource + + + + 4 + + + + delegate + + + + 5 + + + + + YES + + 0 + + + + + + -1 + + + File's Owner + + + -2 + + + + + 2 + + + + + + + YES + + YES + -1.CustomClassName + -2.CustomClassName + 2.IBEditorWindowLastContentRect + 2.IBPluginDependency + + + YES + RootViewController + UIResponder + {{144, 609}, {320, 247}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + + + + YES + + + YES + + + + + YES + + + YES + + + + 5 + + + + YES + + RootViewController + UITableViewController + + IBProjectSource + Classes/RootViewController.h + + + + + YES + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSError.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSFileManager.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyValueCoding.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyValueObserving.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSKeyedArchiver.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSNetServices.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSObject.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSPort.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSRunLoop.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSStream.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSThread.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURL.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSURLConnection.h + + + + NSObject + + IBFrameworkSource + Foundation.framework/Headers/NSXMLParser.h + + + + NSObject + + IBFrameworkSource + UIKit.framework/Headers/UIAccessibility.h + + + + NSObject + + IBFrameworkSource + UIKit.framework/Headers/UINibLoading.h + + + + NSObject + + IBFrameworkSource + UIKit.framework/Headers/UIResponder.h + + + + UIResponder + NSObject + + + + UIScrollView + UIView + + IBFrameworkSource + UIKit.framework/Headers/UIScrollView.h + + + + UISearchBar + UIView + + IBFrameworkSource + UIKit.framework/Headers/UISearchBar.h + + + + UISearchDisplayController + NSObject + + IBFrameworkSource + UIKit.framework/Headers/UISearchDisplayController.h + + + + UITableView + UIScrollView + + IBFrameworkSource + UIKit.framework/Headers/UITableView.h + + + + UITableViewController + UIViewController + + IBFrameworkSource + UIKit.framework/Headers/UITableViewController.h + + + + UIView + + IBFrameworkSource + UIKit.framework/Headers/UITextField.h + + + + UIView + UIResponder + + IBFrameworkSource + UIKit.framework/Headers/UIView.h + + + + UIViewController + + IBFrameworkSource + UIKit.framework/Headers/UINavigationController.h + + + + UIViewController + + IBFrameworkSource + UIKit.framework/Headers/UITabBarController.h + + + + UIViewController + UIResponder + + IBFrameworkSource + UIKit.framework/Headers/UIViewController.h + + + + + 0 + IBCocoaTouchFramework + + com.apple.InterfaceBuilder.CocoaTouchPlugin.iPhoneOS + + + + com.apple.InterfaceBuilder.CocoaTouchPlugin.InterfaceBuilder3 + + + YES + iPhoneExample.xcodeproj + 3 + 81 + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Tests/RootViewControllerTests.h b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Tests/RootViewControllerTests.h new file mode 100644 index 0000000000..56013944a0 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Tests/RootViewControllerTests.h @@ -0,0 +1,17 @@ +// +// RootViewControllerTests.h +// iPhoneExample +// +// Created by Erik Doernenburg on 20/07/10. +// Copyright 2010 Mulle Kybernetik. All rights reserved. +// +// See Also: http://developer.apple.com/iphone/library/documentation/Xcode/Conceptual/iphone_development/135-Unit_Testing_Applications/unit_testing_applications.html + + +#import +#import + + +@interface RootViewControllerTests : SenTestCase + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Tests/RootViewControllerTests.m b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Tests/RootViewControllerTests.m new file mode 100644 index 0000000000..45ba525186 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/Tests/RootViewControllerTests.m @@ -0,0 +1,75 @@ +// +// RootViewControllerTests.m +// iPhoneExample +// +// Created by Erik Doernenburg on 20/07/10. +// Copyright 2010 Mulle Kybernetik. All rights reserved. +// + +#import +#import "RootViewControllerTests.h" +#import "RootViewController.h" + +/* + + A simple test suite to test the RootViewController. It needs to be run as an "application test" as described + in the following Apple document. This project is set up following the steps outlined in the document. + + http://developer.apple.com/iphone/library/documentation/xcode/conceptual/iphone_development/135-Unit_Testing_Applications/unit_testing_applications.html + + The first test could be run as a "logic test" but the second test invokes functionality that sets a cell + label, which requires instantiation of a font, and that is not possible outside the device/simulator. The + following discussion has a bit more detail: + + http://stackoverflow.com/questions/1689586/why-does-instantiating-a-uifont-in-an-iphone-unit-test-cause-a-crash + + As far as I am aware you cannot run the tests in the simulator; at least when I try to run the tests in the + simulator, the app just launches but there is no output in the debugger window that would indicate the tests + were run. + + The first test should pass but the second test should fail, and you should be able to see something like the + following in your device log: + + Test Suite '/var/mobile/Applications/4CEA4E8D-069E-4363-A4B5-E01AF01176CE/iPhoneExample.app/iPhoneExampleTests.octest(Tests)' started at 2010-07-28 10:33:45 +1000 + Test Suite 'RootViewControllerTests' started at 2010-07-28 10:33:45 +1000 + Test Case '-[RootViewControllerTests testControllerReturnsCorrectNumberOfRows]' passed (0.000 seconds). + Unknown.m:0: error: -[RootViewControllerTests testControllerSetsUpCellCorrectly] : OCMockObject[UITableView]: unexpected method invoked: dequeueReusableCellWithIdentifier:@"Cell" + expected: dequeueReusableCellWithIdentifier:@"HelloWorldCell" + Test Case '-[RootViewControllerTests testControllerSetsUpCellCorrectly]' failed (0.002 seconds). + Test Suite 'RootViewControllerTests' finished at 2010-07-28 10:33:45 +1000. + Executed 2 tests, with 1 failure (1 unexpected) in 0.002 (0.006) seconds + + The failure occurs when then RootViewController sends the dequeueReusableCellWithIdentifier: method to the mock + table view. The mock view is set up to expect that method call with the string "HelloWorldCell" as an argument, + but the RootViewController calls the method with just "Cell" as an argument. When you change the identifier in + line 75 of the RootViewController to "HelloWorldCell" and re-run the tests, they should both pass. + + */ + + +@implementation RootViewControllerTests + +- (void)testControllerReturnsCorrectNumberOfRows +{ + RootViewController *controller = [[[RootViewController alloc] initWithStyle:UITableViewStylePlain] autorelease]; + + STAssertEquals(1, [controller tableView:nil numberOfRowsInSection:0], @"Should have returned correct number of rows."); +} + + +- (void)testControllerSetsUpCellCorrectly +{ + RootViewController *controller = [[[RootViewController alloc] initWithStyle:UITableViewStylePlain] autorelease]; + id mockTableView = [OCMockObject mockForClass:[UITableView class]]; + [[[mockTableView expect] andReturn:nil] dequeueReusableCellWithIdentifier:@"HelloWorldCell"]; + + UITableViewCell *cell = [controller tableView:mockTableView cellForRowAtIndexPath:nil]; + + STAssertNotNil(cell, @"Should have returned a cell"); + STAssertEqualObjects(@"Hello World!", cell.textLabel.text, @"Should have set label"); + [mockTableView verify]; +} + + + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/iPhoneExample-Info.plist b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/iPhoneExample-Info.plist new file mode 100644 index 0000000000..104366eb0c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/iPhoneExample-Info.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleDisplayName + ${PRODUCT_NAME} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIconFile + OCMockLogo.png + CFBundleIdentifier + com.mulle-kybernetik.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleSignature + ???? + CFBundleVersion + 1.0 + LSRequiresIPhoneOS + + NSMainNibFile + MainWindow + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/iPhoneExample.xcodeproj/project.pbxproj b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/iPhoneExample.xcodeproj/project.pbxproj new file mode 100755 index 0000000000..f1d8991ea2 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/iPhoneExample.xcodeproj/project.pbxproj @@ -0,0 +1,538 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 036EF43D11F5BE7700A41604 /* RootViewControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03E41EB011F5B0D100147791 /* RootViewControllerTests.m */; }; + 036EF43E11F5BE7900A41604 /* RootViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 28C286E00D94DF7D0034E888 /* RootViewController.m */; }; + 036EF43F11F5BE7900A41604 /* iPhoneExampleAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D3623250D0F684500981E51 /* iPhoneExampleAppDelegate.m */; }; + 036EF4CC11F5D67600A41604 /* libOCMock.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 03E41EBE11F5B13F00147791 /* libOCMock.a */; }; + 03B85C4E11FFAF1100347AAD /* MainWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 28AD735F0D9D9599002E5188 /* MainWindow.xib */; }; + 03B85C4F11FFAF1100347AAD /* RootViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 28F335F01007B36200424DE2 /* RootViewController.xib */; }; + 03B85C5111FFAF1100347AAD /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; }; + 03B85C6311FFB03900347AAD /* iPhoneExampleTests.octest in Resources */ = {isa = PBXBuildFile; fileRef = 03E41F0811F5BE2F00147791 /* iPhoneExampleTests.octest */; }; + 03B85C6B11FFB04A00347AAD /* OCMockLogo.png in Resources */ = {isa = PBXBuildFile; fileRef = 03B85C6A11FFB04A00347AAD /* OCMockLogo.png */; }; + 03B85C6C11FFB04A00347AAD /* OCMockLogo.png in Resources */ = {isa = PBXBuildFile; fileRef = 03B85C6A11FFB04A00347AAD /* OCMockLogo.png */; }; + 03B85CCF11FFB88E00347AAD /* iPhoneExampleAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D3623250D0F684500981E51 /* iPhoneExampleAppDelegate.m */; }; + 03B85CD011FFB89000347AAD /* RootViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 28C286E00D94DF7D0034E888 /* RootViewController.m */; }; + 03DE954B12F2FFB0003852B7 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03DE954A12F2FFB0003852B7 /* CoreGraphics.framework */; }; + 03DE954D12F2FFB0003852B7 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03DE954C12F2FFB0003852B7 /* Foundation.framework */; }; + 03DE954F12F2FFB0003852B7 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03DE954E12F2FFB0003852B7 /* UIKit.framework */; }; + 1D3623260D0F684500981E51 /* iPhoneExampleAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D3623250D0F684500981E51 /* iPhoneExampleAppDelegate.m */; }; + 1D60589B0D05DD56006BFB54 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; }; + 28AD73600D9D9599002E5188 /* MainWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 28AD735F0D9D9599002E5188 /* MainWindow.xib */; }; + 28C286E10D94DF7D0034E888 /* RootViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 28C286E00D94DF7D0034E888 /* RootViewController.m */; }; + 28F335F11007B36200424DE2 /* RootViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 28F335F01007B36200424DE2 /* RootViewController.xib */; }; + 5E2E834C197735D5006E5062 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03DE954E12F2FFB0003852B7 /* UIKit.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 03B85C5E11FFB03100347AAD /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */; + proxyType = 1; + remoteGlobalIDString = 03E41F0711F5BE2F00147791; + remoteInfo = iPhoneExampleTests; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 03B85C5B11FFAF1100347AAD /* iPhoneExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iPhoneExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 03B85C6A11FFB04A00347AAD /* OCMockLogo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = OCMockLogo.png; sourceTree = ""; }; + 03DE954A12F2FFB0003852B7 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + 03DE954C12F2FFB0003852B7 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + 03DE954E12F2FFB0003852B7 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + 03E41EAF11F5B0D100147791 /* RootViewControllerTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RootViewControllerTests.h; sourceTree = ""; }; + 03E41EB011F5B0D100147791 /* RootViewControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RootViewControllerTests.m; sourceTree = ""; }; + 03E41EBE11F5B13F00147791 /* libOCMock.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libOCMock.a; path = Libraries/libOCMock.a; sourceTree = ""; }; + 03E41F0811F5BE2F00147791 /* iPhoneExampleTests.octest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = iPhoneExampleTests.octest; sourceTree = BUILT_PRODUCTS_DIR; }; + 03E41F0911F5BE2F00147791 /* iPhoneExampleTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "iPhoneExampleTests-Info.plist"; sourceTree = ""; }; + 1D3623240D0F684500981E51 /* iPhoneExampleAppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = iPhoneExampleAppDelegate.h; sourceTree = ""; }; + 1D3623250D0F684500981E51 /* iPhoneExampleAppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = iPhoneExampleAppDelegate.m; sourceTree = ""; }; + 1D6058910D05DD3D006BFB54 /* iPhoneExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iPhoneExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 28A0AAE50D9B0CCF005BE974 /* iPhoneExample_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = iPhoneExample_Prefix.pch; sourceTree = ""; }; + 28AD735F0D9D9599002E5188 /* MainWindow.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainWindow.xib; sourceTree = ""; }; + 28C286DF0D94DF7D0034E888 /* RootViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RootViewController.h; sourceTree = ""; }; + 28C286E00D94DF7D0034E888 /* RootViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RootViewController.m; sourceTree = ""; }; + 28F335F01007B36200424DE2 /* RootViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RootViewController.xib; sourceTree = ""; }; + 29B97316FDCFA39411CA2CEA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 8D1107310486CEB800E47090 /* iPhoneExample-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "iPhoneExample-Info.plist"; plistStructureDefinitionIdentifier = "com.apple.xcode.plist.structure-definition.iphone.info-plist"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 03B85C5411FFAF1100347AAD /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 03DE954B12F2FFB0003852B7 /* CoreGraphics.framework in Frameworks */, + 03DE954D12F2FFB0003852B7 /* Foundation.framework in Frameworks */, + 03DE954F12F2FFB0003852B7 /* UIKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 03E41F0511F5BE2F00147791 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 036EF4CC11F5D67600A41604 /* libOCMock.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1D60588F0D05DD3D006BFB54 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5E2E834C197735D5006E5062 /* UIKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 03E41E8311F5AF0400147791 /* Tests */ = { + isa = PBXGroup; + children = ( + 03E41EAF11F5B0D100147791 /* RootViewControllerTests.h */, + 03E41EB011F5B0D100147791 /* RootViewControllerTests.m */, + ); + path = Tests; + sourceTree = ""; + }; + 03E41EBC11F5B12100147791 /* Libraries */ = { + isa = PBXGroup; + children = ( + 03E41EBE11F5B13F00147791 /* libOCMock.a */, + ); + name = Libraries; + sourceTree = ""; + }; + 080E96DDFE201D6D7F000001 /* Classes */ = { + isa = PBXGroup; + children = ( + 28C286DF0D94DF7D0034E888 /* RootViewController.h */, + 28C286E00D94DF7D0034E888 /* RootViewController.m */, + 1D3623240D0F684500981E51 /* iPhoneExampleAppDelegate.h */, + 1D3623250D0F684500981E51 /* iPhoneExampleAppDelegate.m */, + ); + path = Classes; + sourceTree = ""; + }; + 19C28FACFE9D520D11CA2CBB /* Products */ = { + isa = PBXGroup; + children = ( + 1D6058910D05DD3D006BFB54 /* iPhoneExample.app */, + 03E41F0811F5BE2F00147791 /* iPhoneExampleTests.octest */, + 03B85C5B11FFAF1100347AAD /* iPhoneExample.app */, + ); + name = Products; + sourceTree = ""; + }; + 29B97314FDCFA39411CA2CEA /* CustomTemplate */ = { + isa = PBXGroup; + children = ( + 080E96DDFE201D6D7F000001 /* Classes */, + 03E41E8311F5AF0400147791 /* Tests */, + 29B97315FDCFA39411CA2CEA /* Other Sources */, + 29B97317FDCFA39411CA2CEA /* Resources */, + 03E41EBC11F5B12100147791 /* Libraries */, + 29B97323FDCFA39411CA2CEA /* Frameworks */, + 19C28FACFE9D520D11CA2CBB /* Products */, + ); + name = CustomTemplate; + sourceTree = ""; + }; + 29B97315FDCFA39411CA2CEA /* Other Sources */ = { + isa = PBXGroup; + children = ( + 28A0AAE50D9B0CCF005BE974 /* iPhoneExample_Prefix.pch */, + 29B97316FDCFA39411CA2CEA /* main.m */, + ); + name = "Other Sources"; + sourceTree = ""; + }; + 29B97317FDCFA39411CA2CEA /* Resources */ = { + isa = PBXGroup; + children = ( + 28F335F01007B36200424DE2 /* RootViewController.xib */, + 28AD735F0D9D9599002E5188 /* MainWindow.xib */, + 8D1107310486CEB800E47090 /* iPhoneExample-Info.plist */, + 03E41F0911F5BE2F00147791 /* iPhoneExampleTests-Info.plist */, + 03B85C6A11FFB04A00347AAD /* OCMockLogo.png */, + ); + name = Resources; + sourceTree = ""; + }; + 29B97323FDCFA39411CA2CEA /* Frameworks */ = { + isa = PBXGroup; + children = ( + 03DE954A12F2FFB0003852B7 /* CoreGraphics.framework */, + 03DE954C12F2FFB0003852B7 /* Foundation.framework */, + 03DE954E12F2FFB0003852B7 /* UIKit.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 03B85C4C11FFAF1100347AAD /* iPhoneExampleTesting */ = { + isa = PBXNativeTarget; + buildConfigurationList = 03B85C5811FFAF1100347AAD /* Build configuration list for PBXNativeTarget "iPhoneExampleTesting" */; + buildPhases = ( + 03B85C4D11FFAF1100347AAD /* Resources */, + 03B85C5011FFAF1100347AAD /* Sources */, + 03B85C5411FFAF1100347AAD /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 03B85C5F11FFB03100347AAD /* PBXTargetDependency */, + ); + name = iPhoneExampleTesting; + productName = iPhoneExample; + productReference = 03B85C5B11FFAF1100347AAD /* iPhoneExample.app */; + productType = "com.apple.product-type.application"; + }; + 03E41F0711F5BE2F00147791 /* iPhoneExampleTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 03E41F0F11F5BE3000147791 /* Build configuration list for PBXNativeTarget "iPhoneExampleTests" */; + buildPhases = ( + 03E41F0311F5BE2F00147791 /* Resources */, + 03E41F0411F5BE2F00147791 /* Sources */, + 03E41F0511F5BE2F00147791 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = iPhoneExampleTests; + productName = iPhoneExampleTests; + productReference = 03E41F0811F5BE2F00147791 /* iPhoneExampleTests.octest */; + productType = "com.apple.product-type.bundle"; + }; + 1D6058900D05DD3D006BFB54 /* iPhoneExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1D6058960D05DD3E006BFB54 /* Build configuration list for PBXNativeTarget "iPhoneExample" */; + buildPhases = ( + 1D60588D0D05DD3D006BFB54 /* Resources */, + 1D60588E0D05DD3D006BFB54 /* Sources */, + 1D60588F0D05DD3D006BFB54 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = iPhoneExample; + productName = iPhoneExample; + productReference = 1D6058910D05DD3D006BFB54 /* iPhoneExample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 29B97313FDCFA39411CA2CEA /* Project object */ = { + isa = PBXProject; + attributes = { + ORGANIZATIONNAME = "Mulle Kybernetik"; + }; + buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "iPhoneExample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 1; + knownRegions = ( + English, + Japanese, + French, + German, + en, + ); + mainGroup = 29B97314FDCFA39411CA2CEA /* CustomTemplate */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 1D6058900D05DD3D006BFB54 /* iPhoneExample */, + 03E41F0711F5BE2F00147791 /* iPhoneExampleTests */, + 03B85C4C11FFAF1100347AAD /* iPhoneExampleTesting */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 03B85C4D11FFAF1100347AAD /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 03B85C6C11FFB04A00347AAD /* OCMockLogo.png in Resources */, + 03B85C4E11FFAF1100347AAD /* MainWindow.xib in Resources */, + 03B85C4F11FFAF1100347AAD /* RootViewController.xib in Resources */, + 03B85C6311FFB03900347AAD /* iPhoneExampleTests.octest in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 03E41F0311F5BE2F00147791 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1D60588D0D05DD3D006BFB54 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 28AD73600D9D9599002E5188 /* MainWindow.xib in Resources */, + 28F335F11007B36200424DE2 /* RootViewController.xib in Resources */, + 03B85C6B11FFB04A00347AAD /* OCMockLogo.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 03B85C5011FFAF1100347AAD /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 03B85C5111FFAF1100347AAD /* main.m in Sources */, + 03B85CCF11FFB88E00347AAD /* iPhoneExampleAppDelegate.m in Sources */, + 03B85CD011FFB89000347AAD /* RootViewController.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 03E41F0411F5BE2F00147791 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 036EF43D11F5BE7700A41604 /* RootViewControllerTests.m in Sources */, + 036EF43E11F5BE7900A41604 /* RootViewController.m in Sources */, + 036EF43F11F5BE7900A41604 /* iPhoneExampleAppDelegate.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1D60588E0D05DD3D006BFB54 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1D60589B0D05DD56006BFB54 /* main.m in Sources */, + 1D3623260D0F684500981E51 /* iPhoneExampleAppDelegate.m in Sources */, + 28C286E10D94DF7D0034E888 /* RootViewController.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 03B85C5F11FFB03100347AAD /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 03E41F0711F5BE2F00147791 /* iPhoneExampleTests */; + targetProxy = 03B85C5E11FFB03100347AAD /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 03B85C5911FFAF1100347AAD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + COPY_PHASE_STRIP = NO; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = iPhoneExample_Prefix.pch; + INFOPLIST_FILE = "iPhoneExample-Info.plist"; + PRODUCT_NAME = iPhoneExample; + }; + name = Debug; + }; + 03B85C5A11FFAF1100347AAD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + COPY_PHASE_STRIP = YES; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = iPhoneExample_Prefix.pch; + INFOPLIST_FILE = "iPhoneExample-Info.plist"; + PRODUCT_NAME = iPhoneExample; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 03E41F0D11F5BE3000147791 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + COPY_PHASE_STRIP = NO; + FRAMEWORK_SEARCH_PATHS = ( + "\"$(SDKROOT)/Developer/Library/Frameworks\"", + "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\"", + ); + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_FIX_AND_CONTINUE = NO; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/Libraries/Headers"; + INFOPLIST_FILE = "iPhoneExampleTests-Info.plist"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(PROJECT_DIR)/Libraries\"", + ); + OTHER_LDFLAGS = ( + "-ObjC", + "-force_load", + "$(PROJECT_DIR)/Libraries/libOCMock.a", + "-framework", + Foundation, + "-framework", + SenTestingKit, + "-framework", + UIKit, + ); + PREBINDING = NO; + PRODUCT_NAME = iPhoneExampleTests; + WRAPPER_EXTENSION = octest; + }; + name = Debug; + }; + 03E41F0E11F5BE3000147791 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + COPY_PHASE_STRIP = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + FRAMEWORK_SEARCH_PATHS = ( + "\"$(SDKROOT)/Developer/Library/Frameworks\"", + "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\"", + ); + GCC_ENABLE_FIX_AND_CONTINUE = NO; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/Libraries/Headers"; + INFOPLIST_FILE = "iPhoneExampleTests-Info.plist"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(PROJECT_DIR)/Libraries\"", + ); + OTHER_LDFLAGS = ( + "-ObjC", + "-force_load", + "$(PROJECT_DIR)/Libraries/libOCMock.a", + "-framework", + Foundation, + "-framework", + SenTestingKit, + "-framework", + UIKit, + ); + PREBINDING = NO; + PRODUCT_NAME = iPhoneExampleTests; + WRAPPER_EXTENSION = octest; + ZERO_LINK = NO; + }; + name = Release; + }; + 1D6058940D05DD3E006BFB54 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + COPY_PHASE_STRIP = NO; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = iPhoneExample_Prefix.pch; + INFOPLIST_FILE = "iPhoneExample-Info.plist"; + PRODUCT_NAME = iPhoneExample; + }; + name = Debug; + }; + 1D6058950D05DD3E006BFB54 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + COPY_PHASE_STRIP = YES; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = iPhoneExample_Prefix.pch; + INFOPLIST_FILE = "iPhoneExample-Info.plist"; + PRODUCT_NAME = iPhoneExample; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + C01FCF4F08A954540054247B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD_32_BIT)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + GCC_C_LANGUAGE_STANDARD = c99; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 3.2; + PREBINDING = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + C01FCF5008A954540054247B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD_32_BIT)"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + GCC_C_LANGUAGE_STANDARD = c99; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 3.2; + OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; + PREBINDING = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 03B85C5811FFAF1100347AAD /* Build configuration list for PBXNativeTarget "iPhoneExampleTesting" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 03B85C5911FFAF1100347AAD /* Debug */, + 03B85C5A11FFAF1100347AAD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 03E41F0F11F5BE3000147791 /* Build configuration list for PBXNativeTarget "iPhoneExampleTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 03E41F0D11F5BE3000147791 /* Debug */, + 03E41F0E11F5BE3000147791 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1D6058960D05DD3E006BFB54 /* Build configuration list for PBXNativeTarget "iPhoneExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1D6058940D05DD3E006BFB54 /* Debug */, + 1D6058950D05DD3E006BFB54 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C01FCF4E08A954540054247B /* Build configuration list for PBXProject "iPhoneExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C01FCF4F08A954540054247B /* Debug */, + C01FCF5008A954540054247B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 29B97313FDCFA39411CA2CEA /* Project object */; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/iPhoneExampleTests-Info.plist b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/iPhoneExampleTests-Info.plist new file mode 100644 index 0000000000..94eb7f4a58 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/iPhoneExampleTests-Info.plist @@ -0,0 +1,20 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + com.yourcompany.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + BNDL + CFBundleSignature + ???? + CFBundleVersion + 1.0 + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/iPhoneExample_Prefix.pch b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/iPhoneExample_Prefix.pch new file mode 100644 index 0000000000..7832bacdeb --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/iPhoneExample_Prefix.pch @@ -0,0 +1,14 @@ +// +// Prefix header for all source files of the 'iPhoneExample' target in the 'iPhoneExample' project +// +#import + +#ifndef __IPHONE_3_0 +#warning "This project uses features only available in iPhone SDK 3.0 and later." +#endif + + +#ifdef __OBJC__ + #import + #import +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/main.m b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/main.m new file mode 100644 index 0000000000..f5826a3e9a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Examples/iPhoneExample/main.m @@ -0,0 +1,17 @@ +// +// main.m +// iPhoneExample +// +// Created by Erik Doernenburg on 20/07/10. +// Copyright Mulle Kybernetik 2010. All rights reserved. +// + +#import + +int main(int argc, char *argv[]) { + + NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; + int retVal = UIApplicationMain(argc, argv, nil, nil); + [pool release]; + return retVal; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/License.txt b/submodules/AppCenter-sdk/Vendor/OCMock/License.txt new file mode 100644 index 0000000000..f433b1a53f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/License.txt @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Makefile b/submodules/AppCenter-sdk/Vendor/OCMock/Makefile new file mode 100644 index 0000000000..1e5969b026 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Makefile @@ -0,0 +1,35 @@ +# This makefile is mainly intended for use on the CI server (Travis). It +# requires xcpretty to be installed. + +# If you are trying to build a release locally consider using the build.rb +# script in the Tools directory instead. + + +BUILD_DIR = OBJROOT="$(CURDIR)/build" SYMROOT="$(CURDIR)/build" +SHELL = /bin/bash -e -o pipefail +IOS32 = -scheme OCMockLib -destination 'platform=iOS Simulator,OS=10.3.1,name=iPad (5th generation)' $(BUILD_DIR) +IOS64 = -scheme OCMockLib -destination 'platform=iOS Simulator,OS=latest,name=iPhone 11' $(BUILD_DIR) +MACOSX = -scheme OCMock -sdk macosx $(BUILD_DIR) +XCODEBUILD = xcodebuild -project "$(CURDIR)/Source/OCMock.xcodeproj" + +ci: clean test + +clean: + $(XCODEBUILD) clean + rm -rf "$(CURDIR)/build" + +test: test-ios test-macosx + +test-ios: test-ios32 test-ios64 + +test-ios32: + @echo "Running 32-bit iOS tests..." + $(XCODEBUILD) $(IOS32) test | xcpretty -c + +test-ios64: + @echo "Running 64-bit iOS tests..." + $(XCODEBUILD) $(IOS64) test | xcpretty -c + +test-macosx: + @echo "Running OS X tests..." + $(XCODEBUILD) $(MACOSX) test | xcpretty -c diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/OCMock.podspec b/submodules/AppCenter-sdk/Vendor/OCMock/OCMock.podspec new file mode 100644 index 0000000000..0705ec8208 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/OCMock.podspec @@ -0,0 +1,36 @@ +Pod::Spec.new do |s| + s.name = "OCMock" + s.version = "3.7.1" + + s.summary = "Mock objects for Objective-C" + s.description = <<-DESC + OCMock is an Objective-C implementation of mock objects. It provides + stubs that return pre-determined values for specific method invocations, + dynamic mocks that can be used to verify interaction patterns, and + partial mocks to overwrite selected methods of existing objects. + DESC + + s.homepage = "http://ocmock.org" + s.documentation_url = "http://ocmock.org/reference/" + s.license = { :type => "Apache 2.0", :file => "License.txt" } + + s.author = { "Erik Doernenburg" => "erik@doernenburg.com" } + s.social_media_url = "http://twitter.com/erikdoe" + + s.source = { :git => "https://github.com/erikdoe/ocmock.git", :tag => "v3.7.1" } + s.source_files = "Source/OCMock/*.{h,m}" + + s.requires_arc = false + s.ios.deployment_target = '8.0' + s.osx.deployment_target = '10.9' + s.tvos.deployment_target = '9.0' + s.watchos.deployment_target = '4.0' + + s.public_header_files = ["OCMock.h", "OCMockObject.h", "OCMArg.h", "OCMConstraint.h", + "OCMLocation.h", "OCMMacroState.h", "OCMRecorder.h", + "OCMStubRecorder.h", "NSNotificationCenter+OCMAdditions.h", + "OCMFunctions.h", "OCMVerifier.h", "OCMQuantifier.h", + "OCMockMacros.h" ] + .map { |file| "Source/OCMock/" + file } + +end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/README.md b/submodules/AppCenter-sdk/Vendor/OCMock/README.md new file mode 100644 index 0000000000..948822e60a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/README.md @@ -0,0 +1,10 @@ +OCMock +====== + +OCMock is an Objective-C implementation of mock objects. + +For downloads, documentation, and support please visit [ocmock.org][]. + +[![Build Status](https://travis-ci.org/erikdoe/ocmock.svg?branch=master)](https://travis-ci.org/erikdoe/ocmock) + + [ocmock.org]: http://ocmock.org/ diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/Cartfile b/submodules/AppCenter-sdk/Vendor/OCMock/Source/Cartfile new file mode 100644 index 0000000000..dc86185add --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/Cartfile @@ -0,0 +1 @@ +github "hamcrest/OCHamcrest" ~> 7.0 diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/Cartfile.resolved b/submodules/AppCenter-sdk/Vendor/OCMock/Source/Cartfile.resolved new file mode 100644 index 0000000000..f4143f9701 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/Cartfile.resolved @@ -0,0 +1 @@ +github "hamcrest/OCHamcrest" "v7.1.2" diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/Changes.txt b/submodules/AppCenter-sdk/Vendor/OCMock/Source/Changes.txt new file mode 100644 index 0000000000..170529051b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/Changes.txt @@ -0,0 +1,388 @@ +Listing of notable changes by release. More detail is usually found in the Git +commit messages and/or the pull requests. + +OCMock 3.7.1 (2020-07-26) + +* Fixed a bug that caused double-counting of method invocations on partial + mocks under certain circumstances. + + +OCMock 3.7 (2020-07-15) + +* Fixed mocking init methods when using ARC. This worked before but could + result in memory related crashes. (Dave MacLachlan) +* Added support for non-escaping blocks. (Dave MacLachlan) +* Numerous bug-fixes and improvements to developer experience, e.g. clearer + error messages. (Dave MacLachlan) + + +OCMock 3.6 (2020-02-16) + +* Added support for quantifiers. Please see documentation section 3.3 as well + as #302 for details +* Added preprocessor macros to disable short syntax (in case of clashes). See + section 9.6. for details. +* Set up dependency management with Carthage. This means that in order to + build OCMock from source you must have Carthage installed on your system. + See https://github.com/Carthage/Carthage for details. You do not need to have + Carthage installed to use OCMock, and if you want to use OCHamcrest with + OCMock you must make OCHamcrest available in your project, using whatever + dependency management system you use for your project. +* Added description of mock object to the exception that is thrown when the + mock is used after stopMocking was called (Chaoshuai Lu) + + +OCMock 3.5 (2019-11-17) + +* Added macro to tell the mock to ignore non-object arguments (Yura Samsoniuk) +* Added checks, which throw an expection if no interaction happens with a + mock object inside the stub/expect/reject/verify macros. This helps in cases + where by accident the real object is used in these macros. (Anton Vlasov) +* Changed stopMocking so that arguments will be forcibly released to break + retain cycles. This MAY cause some existing tests to fail. However, it + was always bad practice to use a mock after calling stopMocking. (Ian) +* Added checks, which throw an exception when stopMocking has been called + and afterwards an attempt is made to use functionality that will definitely + not work after stopMocking has been called. +* Changed stub so that return value is not retained if the value is the mock + itself, because that creates a retain cycle. (Anton Vlasov) +* Added support for watchOS (Morgan Chen) + + +OCMock 3.4.3 (2018-11-04) + +* Changed behaviour when a second mock takes over class method mocking from + an older mock for the same class. Now, stopMocking is no longer called on + the first mock, which means that the class generated by it stays around + until stopMocking is called, directly or from dealloc; see issue #357 for + details. +* Changed sequence in which steps are taken to swap out class methods, in + order to improve thread-safety; see issue #328 for details. (Lily Ballard) + + +OCMock 3.4.2 (2018-06-25) + +* Skipping attempt to replace class methods on subclasses of NSManagedObject, + which fixes a conflict in newer iOS runtimes. +* Skipping initialisation of mock when init is called again. This allows to + stub alloc/init; with some limitations. (Alan Terranova) +* Removing dynamically created subclasses when a second mock replaces a mock + for the same class. (David Sansome) +* Retaining class arguments to invocations. (Jack Wu) + + +OCMock 3.4.1 (2017-11-04) + +* Added workaround for crashes when mocking NSManagedObjects. As a side effect + it is no longer possible to verify the invocation of class methods on + NSManagedObject; see issue #338 for details. +* Fixed a memory leak with andThrow:. (Nick Gillett) +* Fixed bug that prevented protocol mocks from being used with + invokeWithBlock:. (Christopher McGrath) +* Changed type comparison for structs to match new runtime. (Sylvain Defresne) +* Added polyfill for compilation with 10.9 and lower SDKs. (Sylvain Defresne) + + +OCMock 3.4 (2016-12-22) + +* OCMock now requires iOS 8 as minimum deployment target +* Managed objects can now be mocked (Alan Fineberg, Kyle Van Essen) +* Notifications with a user info dictionary can be observed and verified + with the macro syntax. +* Now considering structs with unknown names to be the same type as a named + struct, as long as either the actual definition matches or one of them is + opaque. +* Fixed bug causing verifyWithDelay to be held up by rejects (Nikolay Kasyanov) +* Fixed bug where a mock would not claim conformance to protocols declared in + a superclass (Werner Altewischer) + + +OCMock 3.3.1 (2016-07-01) + +* Now throwing an exception when an attempt is made to stub the init method. +* Fixed crash when trying to mock NSArray. + + +OCMock 3.3 (2016-04-12) + +* Made the use of mock objects thread safe. You still have to setup the mocks + and verify them from a main thread (Ian Anderson) +* Added modern syntax for reject (Piotr Tobolski) + + +OCMock 3.2.2 (2016-01-20) + +* Fixed recently introduced bug that caused crashes when using NULL pointers + with the pass-by-ref argument setter (Ian Anderson) + + +OCMock 3.2.1 (2016-01-13) + +* Added support for tvOS (Nikita Lutsenko) +* Disposing dynamically generated subclasses after use (David Stites) +* Build script now signs frameworks in releases. The signing identity can + be changed in the script. + + +OCMock 3.2 (2015-10-03) + +* Can mock dynamic properties (imhuntingwabbits) +* Can invoke blocks passed as arguments to stubs (Stephen Fortune) +* Stubbed exceptions are no longer re-raised in verify +* Fixed C++ compilation issues (Jonathan Crooke, Daniel Demiss) +* Add module support for the OS X framework (Ian Anderson) + + +OCMock 3.1.4 / 3.1.5 (2015-08-26) + +* Fixed deployment target in podspec + + +OCMock 3.1.3 (2015-08-12) + +* Now throwing exception when trying to create mocks for nil (Nick Gravelyn) +* Fixed ARC related bug when boxing macro args (Richard Ross) +* Added target for dynamic iOS framework, which makes OCMock compatible with + Carthage (Piet Brauer) +* Memory management and other small bug fixes + + +OCMock 3.1.2 (2015-01-08) + +* Fixed bugs around reject and expectation orders (Mason Glidden, Ben Asher) +* Small adjustments to build file and dependencies + + +OCMock 3.1.1 (2014-08-23) + +* Fixed a recently introduced bug that resulted in class arguments and return + values not to be considered objects (Patrick Hartling, Max Shcheglov) + + +OCMock 3.1 (2014-08-22) + +* Converting number types to make andReturn more intuitive (Carl Lindberg) +* Macros now silence warnings about unused return values (Gordon Fontenot) +* Added isKindOfClass constraint (Ash Furrow) +* Performance and stability improvements. As a result it is no longer possible + use verify-after-running to verify certain methods: + - All methods implemented by NSObject and categories on it + - Private methods in core Apple classes, ie. the class name has an NS or UI + prefix and the method has an underscore prefix and/or suffix. + + +OCMock 3.0.2 (2014-07-07) + +* Fixed podspec + + +OCMock 3.0.1 (2014-07-06) + +* Fixed bug that prevented stubs from returning nil +* Fixed bug related to handling of weak references +* Improved error message when trying to mock undefined method +* Added support for matching of char* arguments + + +OCMock 3.0 (2014-06-12) + +* Added macro for verify with delay +* Fixed several critical bugs +* Allowing nil as block in stub action. With partial mocks this makes it + possible to overwrite a method to do nothing (Sam Stigler) +* More descriptive messages when trying to verify unknown method + + +OCMock 3.0.M3 (2014-05-31) + +* Changed license to Apache 2 license +* Added support for verify-after-run for class methods and for methods sent + directly to the real object covered by a partial mock. +* Using a temporary meta class subclass for mocking class methods, enabling + full clean-up. As a consequence class methods mocked on a given class are no + longer mocked in all subclasses. +* Throwing descriptive exception when attempting to create partial mock on + toll-free bridged classes and tagged pointers (Mark Larsen) + + +OCMock 3.0.M2 (2014-05-07) + +* Added support from verify-after-run. Only works for methods that are sent + to a mock object. Does not work for classes and methods sent directly to + the real object covered by a partial mock. +* Failures without location are now thrown as OCMockTestFailure exception, + not as NSInternalInconsistencyException + + +OCMock 3.0.M1 (2014-04-26) + +* Added macros for modern syntax +* Automatic deregistration of observer mocks + + + + +OCMock 2.2.4 (2014-04-04) + +2014-04-05 + +* Switched unit test for OCMock itself to XCTest. +* Added andForwardToRealObject support for class methods (Carl Lindberg) +* Extended OCMockObject with verifyWithDelay (Charles Harley, Daniel + Doubrovkine) + + +OCMock 2.2.2 (2013-12-19) + +* Added implementation for Apple-interal NSIsKind informal protocol (Brian + Gerstle) +* Various fixes for method with structure returns (Carl Lindberg) +* Added a specially typed method for object references to OCMArg. +* Fixed bug that caused matching to be aborted on first ignored non-object arg. +* Fixed a bug where partial mocks wouldn't clean up mocked class methods. + (we7teck) +* Improved value macro so it can take constant arguments and expressions. (Carl + Lindberg) +* Fixed a bug that caused crashes when methods that require "special" struct + returns were mocked in partial mocks. (Carl Lindberg) + + +OCMock 2.2.1 (2013-07-24) + +* Fixed several bugs regarding class method mocking in class hierarchies. +* Fixed bug preventing the same class method to be expected more than once. + + +OCMock 2.2 (2013-07-02) + +* Can ignore non-object arguments on a per-invocation basis. +* Added constraint for any selector. + + +OCMock 2.1.2 (2013-06-19) + +* Constraints implement NSCopying for OS X 10.9 SDK compatibility. + + +OCMock 2.1 (2013-03-15) + +* Stubbing an object creation method now handles retain count correctly. +* Added support for forwardingTagetForSelector: (thanks to Jeff Watkins) +* Added class method mocking capability to class mock objects +* Added implementation of isKindOfClass: to class mock objects +* Allowing to set non-object pass-by-ref args (thanks to Glenn L. Austin) +* Calling a previously expected method on a partial mock is no longer an error. + + +OCMock 2.0 (2012-03-02) + +* Avoiding deprecated method to convert to a C string (thanks to Kushal + Pisavadia) +* Recreated project from scratch with new conventions in Xcode 4.2 (thanks to + Matt Di Pasquale) +* Arguments only need to be equal, don't have to have same class + + + + +OCMock 1.77 (2011-02-15) + +* Added feature to explicitly disable a partial mock +* Updated example to work with iOS 4.2. + + +OCMock 1.70 (2010-08-21) + +* Added feature to explicitly reject methods on nice mocks (thanks to Heath + Borders) +* Added feature to forward method to real object from partial mock (thanks to + Marco Sandrini) +* Fix to allow block arguments (thanks to Justin DeWind) +* Now building OCMock library for simulator (i386) and device (armv7) +* Updated example to run tests on device +* Changed OCMOCK_VALUE macro to be iOS compatible (thanks to Derek Clarkson) +* Added a new target to build a static library for iOS use +* Created an example showing how to use OCMock in an iOS project +* Various small clean-ups; no change in functionality (thanks to Jonah Williams) +* Added block constraints and invocation handler (thanks to Justin DeWind) + + +OCMock 1.55 (2009-10-16) + +* Fixed broken test for array argument descciptions (Craig Beck) +* Disambiguated mock table method name to avoid compiler warning +* Renamed some variables to avoid warnings when using -Wshadow +* Partial mocks are now deallocated as they should +* Fixed problems that occurred when using mocks as arguments +* OnCall methods now have same signature as replaced ones. +* Fixed possible retain bug (Daniel Eggert) +* Added feature that allows to verify expectations are called in sequence. +* Improved detection of unqualified method return type. +* Fixed bug that caused crash when using method swizzling with void return type. +* Added support for calling arbitrary methods when stubbed methods are invoked. +* Added support for posting notifications (based on Jean-Francois Dontigny's + code) +* Fixed bug around complex type encodings (Jean-Francois Dontigny) +* Partial mocks now work on object reference and self (thanks to Mike Mangino) +* Added partial mocks (calls to the original object reference cannot be mocked) + + +OCMock 1.42 (2009-05-19) + +* Mock observers now handle user infos on notifications. +* Added inital support for mock observers (loosely based on Dave Dribbin's idea) +* Moved factory methods from OCMConstraint to OCMArg +* Added pass by ref argument setters +* Linked install name now uses @rpath (Dave Dribbin) +* Added support for respondsToSelector (Dave Dribin) +* Added constraint for any pointer +* Now comparing selectors as strings (Dado Colussi) + + +OCMock 1.29 (2008-07-07) + +* Resetting invocation target in recorder to avoid retain cycles. +* Added optional integration with hamcrest for constraints +* Now building quad-fat; the 64-bit versions are somewhat experimental though +* Using new functions to deal with protocols (Evan Doll) +* Added support for void* parameters (Tuukka Norri) +* Fixed a bug that could caused crashes when non-char const pointers were + described +* Fixed bug to allow mocking of methods with type qualifieres (Nikita Zhuk) +* Added a simple constraint implementation. + + +OCMock 1.17 (2007-06-04) + +* Now re-throwing fail-fast exceptions, for unexpected invocations for example, + when verify is called; in case the first throw is ignored by a framework. +* Added nice mocks, i.e. mocks that don't raise on unknown methods (Mark Thomas) +* Fixed bug that prevented expectations after invocations (M. Scott Ford) +* Added possibility to throw an exception, based on code by Justin DeWind +* Added Evan Doll's bugfix, which forwards conformsToProtocol: methods when + necessary +* Added the ability to match struct arguments, based on code contributed by + Daniel Eggert +* Better description of arguments, based on code contributed by Jeremy Higgs +* Added the ability to create multiple identical expectations on the mock + object (Jeremy Higgs) +* Added the ability to mock out nil arguments (Jeremy Higgs) +* Added slightly modified version of Jon Reid's contribution, which adds the + possibility to stub primitive return values. +* Added Jon Reid's bugfix that prevents a crash when trying to stub an unknown + method on a protocol. + + +OCMock 1.10 (2005-10-03) + +* Upgraded to build and run tests using the OCUnit that is now part of XCode. +* Added XCode 2.1 project +* Added Richard Clark's contribution, which provides support for scalar + arguments. +* Added support for mocking formal protocols + + +OCMock 1.6 (2004-08-30) + +* MockObject and Recorder now inherit from NSProxy. + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock.xcodeproj/project.pbxproj b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..35e2fac96e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock.xcodeproj/project.pbxproj @@ -0,0 +1,2262 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 031E50581BB4A56300E257C3 /* OCMBoxedReturnValueProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 031E50571BB4A56300E257C3 /* OCMBoxedReturnValueProviderTests.m */; }; + 031E50591BB4A56300E257C3 /* OCMBoxedReturnValueProviderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 031E50571BB4A56300E257C3 /* OCMBoxedReturnValueProviderTests.m */; }; + 0322DA65191188D100CACAF1 /* OCMockObjectVerifyAfterRunTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0322DA64191188D100CACAF1 /* OCMockObjectVerifyAfterRunTests.m */; }; + 0322DA66191188D100CACAF1 /* OCMockObjectVerifyAfterRunTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0322DA64191188D100CACAF1 /* OCMockObjectVerifyAfterRunTests.m */; }; + 0322DA6919118B4600CACAF1 /* OCMVerifier.h in Headers */ = {isa = PBXBuildFile; fileRef = 0322DA6719118B4600CACAF1 /* OCMVerifier.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0322DA6A19118B4600CACAF1 /* OCMVerifier.h in Headers */ = {isa = PBXBuildFile; fileRef = 0322DA6719118B4600CACAF1 /* OCMVerifier.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0322DA6B19118B4600CACAF1 /* OCMVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 0322DA6819118B4600CACAF1 /* OCMVerifier.m */; }; + 0322DA6C19118B4600CACAF1 /* OCMVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 0322DA6819118B4600CACAF1 /* OCMVerifier.m */; }; + 033AB1FA24F046C7002014AE /* OCMockMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 033AB1F924F046C7002014AE /* OCMockMacros.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 033AB1FB24F046C7002014AE /* OCMockMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 033AB1F924F046C7002014AE /* OCMockMacros.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 033AB1FC24F046C7002014AE /* OCMockMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 033AB1F924F046C7002014AE /* OCMockMacros.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 033AB1FD24F046C7002014AE /* OCMockMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 033AB1F924F046C7002014AE /* OCMockMacros.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 033AB1FE24F046C7002014AE /* OCMockMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 033AB1F924F046C7002014AE /* OCMockMacros.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 033E1FF414FEF5E0004456B0 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 033E1FF314FEF5E0004456B0 /* Foundation.framework */; }; + 03565A4218F05721003AE91E /* OCMockObjectPartialMocksTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03AC5C1416DF9FA500D82ECD /* OCMockObjectPartialMocksTests.m */; }; + 03565A4318F05721003AE91E /* OCMockObjectClassMethodMockingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 039F91C516EFB493006C3D70 /* OCMockObjectClassMethodMockingTests.m */; }; + 03565A4418F05721003AE91E /* OCMockObjectProtocolMocksTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA286BFBD8B9D068B41E7EF /* OCMockObjectProtocolMocksTests.m */; }; + 03565A4518F05721003AE91E /* OCMockObjectForwardingTargetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0354D71F16F23AF5001766BB /* OCMockObjectForwardingTargetTests.m */; }; + 03565A4618F05721003AE91E /* OCMockObjectHamcrestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B316231463350E0052CD09 /* OCMockObjectHamcrestTests.m */; }; + 03565A4718F05721003AE91E /* OCMConstraintTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B316211463350E0052CD09 /* OCMConstraintTests.m */; }; + 03565A4818F05721003AE91E /* OCMStubRecorderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B316271463350E0052CD09 /* OCMStubRecorderTests.m */; }; + 03565A4918F05721003AE91E /* OCMArgTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28EDBF243639C57F88A1B /* OCMArgTests.m */; }; + 03565A4A18F05721003AE91E /* OCObserverMockObjectTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B316291463350E0052CD09 /* OCObserverMockObjectTests.m */; }; + 03565A4B18F05721003AE91E /* NSInvocationOCMAdditionsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3161F1463350E0052CD09 /* NSInvocationOCMAdditionsTests.m */; }; + 03565A4C18F05721003AE91E /* NSMethodSignatureOCMAdditionsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28DEDB9163597B7C49F3D /* NSMethodSignatureOCMAdditionsTests.m */; }; + 03618D83195B553400389166 /* OCMRecorder.h in Headers */ = {isa = PBXBuildFile; fileRef = 03618D81195B553400389166 /* OCMRecorder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 03618D84195B553400389166 /* OCMRecorder.h in Headers */ = {isa = PBXBuildFile; fileRef = 03618D81195B553400389166 /* OCMRecorder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 03618D85195B553400389166 /* OCMRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 03618D82195B553400389166 /* OCMRecorder.m */; }; + 03618D86195B553400389166 /* OCMRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 03618D82195B553400389166 /* OCMRecorder.m */; }; + 036865641D3571A8005E6BEE /* OCMQuantifierTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 036865631D3571A8005E6BEE /* OCMQuantifierTests.m */; }; + 036865651D3571A8005E6BEE /* OCMQuantifierTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 036865631D3571A8005E6BEE /* OCMQuantifierTests.m */; }; + 036865681D3572ED005E6BEE /* OCMQuantifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 036865671D3572ED005E6BEE /* OCMQuantifier.m */; }; + 036865691D3572ED005E6BEE /* OCMQuantifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 036865671D3572ED005E6BEE /* OCMQuantifier.m */; }; + 0368656A1D3572ED005E6BEE /* OCMQuantifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 036865671D3572ED005E6BEE /* OCMQuantifier.m */; }; + 0368656B1D3572ED005E6BEE /* OCMQuantifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 036865671D3572ED005E6BEE /* OCMQuantifier.m */; }; + 0368656D1D357317005E6BEE /* OCMQuantifier.h in Headers */ = {isa = PBXBuildFile; fileRef = 0368656C1D35730B005E6BEE /* OCMQuantifier.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0368656E1D357318005E6BEE /* OCMQuantifier.h in Headers */ = {isa = PBXBuildFile; fileRef = 0368656C1D35730B005E6BEE /* OCMQuantifier.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0368656F1D357319005E6BEE /* OCMQuantifier.h in Headers */ = {isa = PBXBuildFile; fileRef = 0368656C1D35730B005E6BEE /* OCMQuantifier.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 036865701D35731A005E6BEE /* OCMQuantifier.h in Headers */ = {isa = PBXBuildFile; fileRef = 0368656C1D35730B005E6BEE /* OCMQuantifier.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 037ECD5418FAD84100AF0E4C /* OCMInvocationMatcherTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 037ECD5318FAD84100AF0E4C /* OCMInvocationMatcherTests.m */; }; + 037ECD5518FAD84100AF0E4C /* OCMInvocationMatcherTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 037ECD5318FAD84100AF0E4C /* OCMInvocationMatcherTests.m */; }; + 038599F723807B06002B3ABE /* OCMockObjectInternalTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 038599F623807B06002B3ABE /* OCMockObjectInternalTests.m */; }; + 038599F823807B06002B3ABE /* OCMockObjectInternalTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 038599F623807B06002B3ABE /* OCMockObjectInternalTests.m */; }; + 03A1CC9E23F89F36005ADA04 /* OCMQuantifier.h in Headers */ = {isa = PBXBuildFile; fileRef = 0368656C1D35730B005E6BEE /* OCMQuantifier.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 03A1CC9F23F8A045005ADA04 /* OCMQuantifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 036865671D3572ED005E6BEE /* OCMQuantifier.m */; }; + 03B315AF146333BF0052CD09 /* NSInvocation+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31585146333BF0052CD09 /* NSInvocation+OCMAdditions.h */; }; + 03B315B0146333BF0052CD09 /* NSInvocation+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31585146333BF0052CD09 /* NSInvocation+OCMAdditions.h */; }; + 03B315B1146333BF0052CD09 /* NSInvocation+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31586146333BF0052CD09 /* NSInvocation+OCMAdditions.m */; }; + 03B315B3146333BF0052CD09 /* NSInvocation+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31586146333BF0052CD09 /* NSInvocation+OCMAdditions.m */; }; + 03B315B4146333BF0052CD09 /* NSMethodSignature+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31587146333BF0052CD09 /* NSMethodSignature+OCMAdditions.h */; }; + 03B315B5146333BF0052CD09 /* NSMethodSignature+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31587146333BF0052CD09 /* NSMethodSignature+OCMAdditions.h */; }; + 03B315B6146333BF0052CD09 /* NSMethodSignature+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31588146333BF0052CD09 /* NSMethodSignature+OCMAdditions.m */; }; + 03B315B8146333BF0052CD09 /* NSMethodSignature+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31588146333BF0052CD09 /* NSMethodSignature+OCMAdditions.m */; }; + 03B315B9146333BF0052CD09 /* NSNotificationCenter+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31589146333BF0052CD09 /* NSNotificationCenter+OCMAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 03B315BA146333BF0052CD09 /* NSNotificationCenter+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31589146333BF0052CD09 /* NSNotificationCenter+OCMAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 03B315BB146333BF0052CD09 /* NSNotificationCenter+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3158A146333BF0052CD09 /* NSNotificationCenter+OCMAdditions.m */; }; + 03B315BD146333BF0052CD09 /* NSNotificationCenter+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3158A146333BF0052CD09 /* NSNotificationCenter+OCMAdditions.m */; }; + 03B315BE146333BF0052CD09 /* OCClassMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3158B146333BF0052CD09 /* OCClassMockObject.h */; }; + 03B315BF146333BF0052CD09 /* OCClassMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3158B146333BF0052CD09 /* OCClassMockObject.h */; }; + 03B315C0146333BF0052CD09 /* OCClassMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3158C146333BF0052CD09 /* OCClassMockObject.m */; }; + 03B315C2146333BF0052CD09 /* OCClassMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3158C146333BF0052CD09 /* OCClassMockObject.m */; }; + 03B315C3146333BF0052CD09 /* OCMArg.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3158D146333BF0052CD09 /* OCMArg.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 03B315C4146333BF0052CD09 /* OCMArg.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3158D146333BF0052CD09 /* OCMArg.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 03B315C5146333BF0052CD09 /* OCMArg.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3158E146333BF0052CD09 /* OCMArg.m */; }; + 03B315C7146333BF0052CD09 /* OCMArg.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3158E146333BF0052CD09 /* OCMArg.m */; }; + 03B315C8146333BF0052CD09 /* OCMBlockCaller.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3158F146333BF0052CD09 /* OCMBlockCaller.h */; }; + 03B315C9146333BF0052CD09 /* OCMBlockCaller.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3158F146333BF0052CD09 /* OCMBlockCaller.h */; }; + 03B315CA146333BF0052CD09 /* OCMBlockCaller.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31590146333BF0052CD09 /* OCMBlockCaller.m */; }; + 03B315CC146333BF0052CD09 /* OCMBlockCaller.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31590146333BF0052CD09 /* OCMBlockCaller.m */; }; + 03B315CD146333BF0052CD09 /* OCMBoxedReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31591146333BF0052CD09 /* OCMBoxedReturnValueProvider.h */; }; + 03B315CE146333BF0052CD09 /* OCMBoxedReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31591146333BF0052CD09 /* OCMBoxedReturnValueProvider.h */; }; + 03B315CF146333BF0052CD09 /* OCMBoxedReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31592146333BF0052CD09 /* OCMBoxedReturnValueProvider.m */; }; + 03B315D1146333BF0052CD09 /* OCMBoxedReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31592146333BF0052CD09 /* OCMBoxedReturnValueProvider.m */; }; + 03B315D2146333BF0052CD09 /* OCMConstraint.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31593146333BF0052CD09 /* OCMConstraint.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 03B315D3146333BF0052CD09 /* OCMConstraint.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31593146333BF0052CD09 /* OCMConstraint.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 03B315D4146333BF0052CD09 /* OCMConstraint.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31594146333BF0052CD09 /* OCMConstraint.m */; }; + 03B315D6146333BF0052CD09 /* OCMConstraint.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31594146333BF0052CD09 /* OCMConstraint.m */; }; + 03B315D7146333BF0052CD09 /* OCMExceptionReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31595146333BF0052CD09 /* OCMExceptionReturnValueProvider.h */; }; + 03B315D8146333BF0052CD09 /* OCMExceptionReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31595146333BF0052CD09 /* OCMExceptionReturnValueProvider.h */; }; + 03B315D9146333BF0052CD09 /* OCMExceptionReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31596146333BF0052CD09 /* OCMExceptionReturnValueProvider.m */; }; + 03B315DB146333BF0052CD09 /* OCMExceptionReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31596146333BF0052CD09 /* OCMExceptionReturnValueProvider.m */; }; + 03B315DC146333BF0052CD09 /* OCMIndirectReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31597146333BF0052CD09 /* OCMIndirectReturnValueProvider.h */; }; + 03B315DD146333BF0052CD09 /* OCMIndirectReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31597146333BF0052CD09 /* OCMIndirectReturnValueProvider.h */; }; + 03B315DE146333BF0052CD09 /* OCMIndirectReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31598146333BF0052CD09 /* OCMIndirectReturnValueProvider.m */; }; + 03B315E0146333BF0052CD09 /* OCMIndirectReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31598146333BF0052CD09 /* OCMIndirectReturnValueProvider.m */; }; + 03B315E1146333BF0052CD09 /* OCMNotificationPoster.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31599146333BF0052CD09 /* OCMNotificationPoster.h */; }; + 03B315E2146333BF0052CD09 /* OCMNotificationPoster.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31599146333BF0052CD09 /* OCMNotificationPoster.h */; }; + 03B315E3146333BF0052CD09 /* OCMNotificationPoster.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3159A146333BF0052CD09 /* OCMNotificationPoster.m */; }; + 03B315E5146333BF0052CD09 /* OCMNotificationPoster.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3159A146333BF0052CD09 /* OCMNotificationPoster.m */; }; + 03B315E6146333BF0052CD09 /* OCMObserverRecorder.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3159B146333BF0052CD09 /* OCMObserverRecorder.h */; }; + 03B315E7146333BF0052CD09 /* OCMObserverRecorder.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3159B146333BF0052CD09 /* OCMObserverRecorder.h */; }; + 03B315E8146333BF0052CD09 /* OCMObserverRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3159C146333BF0052CD09 /* OCMObserverRecorder.m */; }; + 03B315EA146333BF0052CD09 /* OCMObserverRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3159C146333BF0052CD09 /* OCMObserverRecorder.m */; }; + 03B315EB146333C00052CD09 /* OCMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3159D146333BF0052CD09 /* OCMockObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 03B315EC146333C00052CD09 /* OCMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3159D146333BF0052CD09 /* OCMockObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 03B315ED146333C00052CD09 /* OCMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3159E146333BF0052CD09 /* OCMockObject.m */; }; + 03B315EF146333C00052CD09 /* OCMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3159E146333BF0052CD09 /* OCMockObject.m */; }; + 03B315F0146333C00052CD09 /* OCMStubRecorder.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3159F146333BF0052CD09 /* OCMStubRecorder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 03B315F1146333C00052CD09 /* OCMStubRecorder.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3159F146333BF0052CD09 /* OCMStubRecorder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 03B315F2146333C00052CD09 /* OCMStubRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315A0146333BF0052CD09 /* OCMStubRecorder.m */; }; + 03B315F4146333C00052CD09 /* OCMStubRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315A0146333BF0052CD09 /* OCMStubRecorder.m */; }; + 03B315F5146333C00052CD09 /* OCMPassByRefSetter.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315A1146333BF0052CD09 /* OCMPassByRefSetter.h */; }; + 03B315F6146333C00052CD09 /* OCMPassByRefSetter.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315A1146333BF0052CD09 /* OCMPassByRefSetter.h */; }; + 03B315F7146333C00052CD09 /* OCMPassByRefSetter.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315A2146333BF0052CD09 /* OCMPassByRefSetter.m */; }; + 03B315F9146333C00052CD09 /* OCMPassByRefSetter.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315A2146333BF0052CD09 /* OCMPassByRefSetter.m */; }; + 03B315FA146333C00052CD09 /* OCMRealObjectForwarder.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315A3146333BF0052CD09 /* OCMRealObjectForwarder.h */; }; + 03B315FB146333C00052CD09 /* OCMRealObjectForwarder.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315A3146333BF0052CD09 /* OCMRealObjectForwarder.h */; }; + 03B315FC146333C00052CD09 /* OCMRealObjectForwarder.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315A4146333BF0052CD09 /* OCMRealObjectForwarder.m */; }; + 03B315FE146333C00052CD09 /* OCMRealObjectForwarder.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315A4146333BF0052CD09 /* OCMRealObjectForwarder.m */; }; + 03B315FF146333C00052CD09 /* OCMObjectReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315A5146333BF0052CD09 /* OCMObjectReturnValueProvider.h */; }; + 03B31601146333C00052CD09 /* OCMObjectReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315A6146333BF0052CD09 /* OCMObjectReturnValueProvider.m */; }; + 03B31603146333C00052CD09 /* OCMObjectReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315A6146333BF0052CD09 /* OCMObjectReturnValueProvider.m */; }; + 03B31604146333C00052CD09 /* OCObserverMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315A7146333BF0052CD09 /* OCObserverMockObject.h */; }; + 03B31605146333C00052CD09 /* OCObserverMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315A7146333BF0052CD09 /* OCObserverMockObject.h */; }; + 03B31606146333C00052CD09 /* OCObserverMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315A8146333BF0052CD09 /* OCObserverMockObject.m */; }; + 03B31608146333C00052CD09 /* OCObserverMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315A8146333BF0052CD09 /* OCObserverMockObject.m */; }; + 03B31609146333C00052CD09 /* OCPartialMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315A9146333BF0052CD09 /* OCPartialMockObject.h */; }; + 03B3160A146333C00052CD09 /* OCPartialMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315A9146333BF0052CD09 /* OCPartialMockObject.h */; }; + 03B3160B146333C00052CD09 /* OCPartialMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315AA146333BF0052CD09 /* OCPartialMockObject.m */; }; + 03B3160D146333C00052CD09 /* OCPartialMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315AA146333BF0052CD09 /* OCPartialMockObject.m */; }; + 03B31613146333C00052CD09 /* OCProtocolMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315AD146333BF0052CD09 /* OCProtocolMockObject.h */; }; + 03B31614146333C00052CD09 /* OCProtocolMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315AD146333BF0052CD09 /* OCProtocolMockObject.h */; }; + 03B31615146333C00052CD09 /* OCProtocolMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315AE146333BF0052CD09 /* OCProtocolMockObject.m */; }; + 03B31617146333C00052CD09 /* OCProtocolMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315AE146333BF0052CD09 /* OCProtocolMockObject.m */; }; + 03B3168B14633A4F0052CD09 /* OCMock.h in Headers */ = {isa = PBXBuildFile; fileRef = 030EF0B814632FD000B04273 /* OCMock.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 03B3168E14633D9C0052CD09 /* OCMock.h in Headers */ = {isa = PBXBuildFile; fileRef = 030EF0B814632FD000B04273 /* OCMock.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 03C7BF0A195DA2F200A545DD /* OCMInvocationStub.h in Headers */ = {isa = PBXBuildFile; fileRef = 03C7BF08195DA2F200A545DD /* OCMInvocationStub.h */; }; + 03C7BF0B195DA2F200A545DD /* OCMInvocationStub.h in Headers */ = {isa = PBXBuildFile; fileRef = 03C7BF08195DA2F200A545DD /* OCMInvocationStub.h */; }; + 03C7BF0C195DA2F200A545DD /* OCMInvocationStub.m in Sources */ = {isa = PBXBuildFile; fileRef = 03C7BF09195DA2F200A545DD /* OCMInvocationStub.m */; }; + 03C7BF0D195DA2F200A545DD /* OCMInvocationStub.m in Sources */ = {isa = PBXBuildFile; fileRef = 03C7BF09195DA2F200A545DD /* OCMInvocationStub.m */; }; + 03C7BF10195DAB5300A545DD /* OCMExpectationRecorder.h in Headers */ = {isa = PBXBuildFile; fileRef = 03C7BF0E195DAB5300A545DD /* OCMExpectationRecorder.h */; }; + 03C7BF11195DAB5300A545DD /* OCMExpectationRecorder.h in Headers */ = {isa = PBXBuildFile; fileRef = 03C7BF0E195DAB5300A545DD /* OCMExpectationRecorder.h */; }; + 03C7BF12195DAB5300A545DD /* OCMExpectationRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 03C7BF0F195DAB5300A545DD /* OCMExpectationRecorder.m */; }; + 03C7BF13195DAB5300A545DD /* OCMExpectationRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 03C7BF0F195DAB5300A545DD /* OCMExpectationRecorder.m */; }; + 03C7BF1619606E7A00A545DD /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03565A1D18F05626003AE91E /* XCTest.framework */; }; + 03C7BF1719606EFD00A545DD /* OCMock.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 030EF0A814632FD000B04273 /* OCMock.framework */; }; + 03C9CA1D18F05A75006DF94D /* OCMockObjectProtocolMocksTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA286BFBD8B9D068B41E7EF /* OCMockObjectProtocolMocksTests.m */; }; + 03C9CA1E18F05A84006DF94D /* OCMArgTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28EDBF243639C57F88A1B /* OCMArgTests.m */; }; + 03C9CA1F18F05A8E006DF94D /* NSMethodSignatureOCMAdditionsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28DEDB9163597B7C49F3D /* NSMethodSignatureOCMAdditionsTests.m */; }; + 03CED2E22390770C001845CC /* OCHamcrest.framework.dSYM in Resources */ = {isa = PBXBuildFile; fileRef = 03CED2E02390770B001845CC /* OCHamcrest.framework.dSYM */; }; + 03CED2E32390770C001845CC /* OCHamcrest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03CED2E12390770C001845CC /* OCHamcrest.framework */; }; + 03CED2E423907759001845CC /* OCHamcrest.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 03CED2E12390770C001845CC /* OCHamcrest.framework */; }; + 03CED2E523907759001845CC /* OCHamcrest.framework.dSYM in CopyFiles */ = {isa = PBXBuildFile; fileRef = 03CED2E02390770B001845CC /* OCHamcrest.framework.dSYM */; }; + 03DCED6D183406BC0059089E /* NSObject+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28991BEFD67DEF2CCF7D2 /* NSObject+OCMAdditions.m */; }; + 03DCED6F183406DA0059089E /* NSObject+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA28C5F4A475BEC0C28BDC3 /* NSObject+OCMAdditions.h */; }; + 03E0FAD81B93C00B000C5096 /* OCMBlockArgCaller.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA2891034E7B73AA3511D17 /* OCMBlockArgCaller.h */; }; + 03E0FAD91B93C01A000C5096 /* OCMBlockArgCaller.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA283D58AA7569D8A5B0C57 /* OCMBlockArgCaller.m */; }; + 03E98D4B18F308B400522D42 /* OCMLocation.h in Headers */ = {isa = PBXBuildFile; fileRef = 03E98D4918F308B400522D42 /* OCMLocation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 03E98D4C18F308B400522D42 /* OCMLocation.h in Headers */ = {isa = PBXBuildFile; fileRef = 03E98D4918F308B400522D42 /* OCMLocation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 03E98D4D18F308B400522D42 /* OCMLocation.m in Sources */ = {isa = PBXBuildFile; fileRef = 03E98D4A18F308B400522D42 /* OCMLocation.m */; }; + 03E98D4E18F308B400522D42 /* OCMLocation.m in Sources */ = {isa = PBXBuildFile; fileRef = 03E98D4A18F308B400522D42 /* OCMLocation.m */; }; + 03E98D5018F310EE00522D42 /* OCMockObjectMacroTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03E98D4F18F310EE00522D42 /* OCMockObjectMacroTests.m */; }; + 03E98D5118F310EE00522D42 /* OCMockObjectMacroTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03E98D4F18F310EE00522D42 /* OCMockObjectMacroTests.m */; }; + 2FA2800D12E95D5ABC965753 /* OCMMacroState.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA280987F4EA8A4D79000D0 /* OCMMacroState.m */; }; + 2FA2805AB1944A206EEE383B /* NSValue+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28896E5EEFD7C2F12C2F8 /* NSValue+OCMAdditions.m */; }; + 2FA280E60213BA09F007C173 /* OCMArgAction.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA2833B48908EAD36444671 /* OCMArgAction.h */; }; + 2FA281328683C83959C109C1 /* OCMArgAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28A7898E1046DE957A035 /* OCMArgAction.m */; }; + 2FA2816186A83E8CDBC3A2E2 /* OCMArgAction.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA2833B48908EAD36444671 /* OCMArgAction.h */; }; + 2FA28184FE3B28D87DF3DE44 /* OCMNonRetainingObjectReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA280EB5E8CDEEAE76861F7 /* OCMNonRetainingObjectReturnValueProvider.m */; }; + 2FA28209EB1F41964DF3031A /* OCMMacroState.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA280987F4EA8A4D79000D0 /* OCMMacroState.m */; }; + 2FA2821C9066C5E439288C76 /* OCMMacroState.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA28006D043CBDBBAEF6E3F /* OCMMacroState.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2FA28246CD449A01717B1CEC /* OCMockObjectTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA2822E19948FC997965267 /* OCMockObjectTests.m */; }; + 2FA28295E1F58F40A77D7448 /* OCMockObjectRuntimeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA2813F93050582D83E1499 /* OCMockObjectRuntimeTests.m */; }; + 2FA282C6A3FFB4AD9061CB08 /* OCMArgAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28A7898E1046DE957A035 /* OCMArgAction.m */; }; + 2FA282F0C0A4CABE4E2C7646 /* OCMInvocationMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28CB1EB14C6B2BE9A20C6 /* OCMInvocationMatcher.m */; }; + 2FA2839F33289795284C32FB /* OCMockObjectTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA2822E19948FC997965267 /* OCMockObjectTests.m */; }; + 2FA283BF22B1AA96AFCCD66C /* OCMNonRetainingObjectReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA280EB5E8CDEEAE76861F7 /* OCMNonRetainingObjectReturnValueProvider.m */; }; + 2FA283C39309951ED4DA2969 /* NSValue+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA286EAC1682979C696D4D6 /* NSValue+OCMAdditions.h */; }; + 2FA283EC733E512211268010 /* OCMNonRetainingObjectReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA280EB5E8CDEEAE76861F7 /* OCMNonRetainingObjectReturnValueProvider.m */; }; + 2FA28405211B752287972F42 /* OCMBlockArgCaller.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA283D58AA7569D8A5B0C57 /* OCMBlockArgCaller.m */; }; + 2FA284EBCD82935B4F8A5A2C /* OCMInvocationExpectation.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA288509D545BDBE094BCC8 /* OCMInvocationExpectation.h */; }; + 2FA28598D23EC190EE19F3AE /* OCMFunctions.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA28EC49F6C59B940AE6D00 /* OCMFunctions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2FA28641AAD0AC2F876C9E48 /* OCMInvocationMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA28CCC33B99D6B658E7AF8 /* OCMInvocationMatcher.h */; }; + 2FA286C7B8862280E6C1A585 /* OCMInvocationExpectation.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA2875564A6AD0510A847A6 /* OCMInvocationExpectation.m */; }; + 2FA286F90395E317F983A20A /* OCMFunctions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28C8717BE4B7A89119BA2 /* OCMFunctions.m */; }; + 2FA287ACE547BB41937BDEC3 /* NSObject+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA28C5F4A475BEC0C28BDC3 /* NSObject+OCMAdditions.h */; }; + 2FA28806443827E286F12F6F /* OCMNonRetainingObjectReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA28B1133B294089D2245AC /* OCMNonRetainingObjectReturnValueProvider.h */; }; + 2FA2883A0BD4BF6A03AAB49C /* OCMNonRetainingObjectReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA28B1133B294089D2245AC /* OCMNonRetainingObjectReturnValueProvider.h */; }; + 2FA288447C48241BE2CAA63D /* OCMFunctions.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA28EC49F6C59B940AE6D00 /* OCMFunctions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2FA288B20FB3A42BA0B7BAB8 /* OCMInvocationMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28CB1EB14C6B2BE9A20C6 /* OCMInvocationMatcher.m */; }; + 2FA28AB33F01A7D980F2C705 /* OCMockObjectDynamicPropertyMockingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28EE3142412BD601026EF /* OCMockObjectDynamicPropertyMockingTests.m */; }; + 2FA28AFBD67EAB9DD1F23BF5 /* OCMNonRetainingObjectReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA28B1133B294089D2245AC /* OCMNonRetainingObjectReturnValueProvider.h */; }; + 2FA28B6CEF4CC6FD5E7D09B5 /* OCMMacroState.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA28006D043CBDBBAEF6E3F /* OCMMacroState.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2FA28B7BDB3319A499E90525 /* OCMBlockArgCaller.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA2891034E7B73AA3511D17 /* OCMBlockArgCaller.h */; }; + 2FA28BA90F0D55E4CF6787F0 /* OCMFunctions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28C8717BE4B7A89119BA2 /* OCMFunctions.m */; }; + 2FA28BCF08C1CBC133E8E84E /* OCMArgAction.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA2833B48908EAD36444671 /* OCMArgAction.h */; }; + 2FA28C1FE2E8E2C114E3FA54 /* OCMNonRetainingObjectReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA28B1133B294089D2245AC /* OCMNonRetainingObjectReturnValueProvider.h */; }; + 2FA28C42D466C7002E6F9B24 /* OCMNonRetainingObjectReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA280EB5E8CDEEAE76861F7 /* OCMNonRetainingObjectReturnValueProvider.m */; }; + 2FA28D38ED9D83A3E0A620B3 /* NSObject+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28991BEFD67DEF2CCF7D2 /* NSObject+OCMAdditions.m */; }; + 2FA28D9C8D2141B003D39E7F /* OCMArgAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28A7898E1046DE957A035 /* OCMArgAction.m */; }; + 2FA28DC9A2D732666124D640 /* OCMBlockArgCaller.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA2891034E7B73AA3511D17 /* OCMBlockArgCaller.h */; }; + 2FA28E1EB6B8536785258DF5 /* OCMInvocationMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA28CCC33B99D6B658E7AF8 /* OCMInvocationMatcher.h */; }; + 2FA28E506BE0D91A95A20243 /* OCMInvocationExpectation.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA2875564A6AD0510A847A6 /* OCMInvocationExpectation.m */; }; + 2FA28E6C62F66CC0A54D0A70 /* OCMNonRetainingObjectReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA280EB5E8CDEEAE76861F7 /* OCMNonRetainingObjectReturnValueProvider.m */; }; + 2FA28F090622C9DF868CCA8C /* NSValue+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA286EAC1682979C696D4D6 /* NSValue+OCMAdditions.h */; }; + 2FA28F12AAD384A8CB16094B /* OCMockObjectDynamicPropertyMockingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28EE3142412BD601026EF /* OCMockObjectDynamicPropertyMockingTests.m */; }; + 2FA28F1FA69D313B4040896E /* OCMBlockArgCaller.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA283D58AA7569D8A5B0C57 /* OCMBlockArgCaller.m */; }; + 2FA28F7D0718648C36686617 /* OCMNonRetainingObjectReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA28B1133B294089D2245AC /* OCMNonRetainingObjectReturnValueProvider.h */; }; + 2FA28FA53C57236B6DD64E82 /* OCMockObjectRuntimeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA2813F93050582D83E1499 /* OCMockObjectRuntimeTests.m */; }; + 2FA28FE116284C3E4A7FF179 /* OCMInvocationExpectation.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA288509D545BDBE094BCC8 /* OCMInvocationExpectation.h */; }; + 2FA28FEAEF9333D2C214DF53 /* NSValue+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28896E5EEFD7C2F12C2F8 /* NSValue+OCMAdditions.m */; }; + 3C0FF06A1BAA3FD10021AD20 /* OCMFunctionsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 03F370CA1BAA1DE800CAD3E8 /* OCMFunctionsPrivate.h */; }; + 3C0FF06B1BAA3FD20021AD20 /* OCMFunctionsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 03F370CA1BAA1DE800CAD3E8 /* OCMFunctionsPrivate.h */; }; + 3C76716C1BB3EBC500FDC9F4 /* TestClassWithCustomReferenceCounting.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CFBDD761BB3DB200050D9C5 /* TestClassWithCustomReferenceCounting.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + 3CFBDD771BB3DB200050D9C5 /* TestClassWithCustomReferenceCounting.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CFBDD761BB3DB200050D9C5 /* TestClassWithCustomReferenceCounting.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + 817EB1171BD765130047E85A /* OCMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3159E146333BF0052CD09 /* OCMockObject.m */; }; + 817EB1181BD765130047E85A /* OCClassMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3158C146333BF0052CD09 /* OCClassMockObject.m */; }; + 817EB1191BD765130047E85A /* OCPartialMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315AA146333BF0052CD09 /* OCPartialMockObject.m */; }; + 817EB11A1BD765130047E85A /* OCProtocolMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315AE146333BF0052CD09 /* OCProtocolMockObject.m */; }; + 817EB11B1BD765130047E85A /* OCMRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 03618D82195B553400389166 /* OCMRecorder.m */; }; + 817EB11C1BD765130047E85A /* OCMStubRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315A0146333BF0052CD09 /* OCMStubRecorder.m */; }; + 817EB11D1BD765130047E85A /* OCMExpectationRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 03C7BF0F195DAB5300A545DD /* OCMExpectationRecorder.m */; }; + 817EB11E1BD765130047E85A /* OCMVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 0322DA6819118B4600CACAF1 /* OCMVerifier.m */; }; + 817EB11F1BD765130047E85A /* OCMInvocationMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28CB1EB14C6B2BE9A20C6 /* OCMInvocationMatcher.m */; }; + 817EB1201BD765130047E85A /* OCMInvocationStub.m in Sources */ = {isa = PBXBuildFile; fileRef = 03C7BF09195DA2F200A545DD /* OCMInvocationStub.m */; }; + 817EB1211BD765130047E85A /* OCMInvocationExpectation.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA2875564A6AD0510A847A6 /* OCMInvocationExpectation.m */; }; + 817EB1221BD765130047E85A /* OCMRealObjectForwarder.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315A4146333BF0052CD09 /* OCMRealObjectForwarder.m */; }; + 817EB1231BD765130047E85A /* OCMBlockCaller.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31590146333BF0052CD09 /* OCMBlockCaller.m */; }; + 817EB1241BD765130047E85A /* OCMBoxedReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31592146333BF0052CD09 /* OCMBoxedReturnValueProvider.m */; }; + 817EB1251BD765130047E85A /* OCMExceptionReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31596146333BF0052CD09 /* OCMExceptionReturnValueProvider.m */; }; + 817EB1261BD765130047E85A /* OCMIndirectReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31598146333BF0052CD09 /* OCMIndirectReturnValueProvider.m */; }; + 817EB1271BD765130047E85A /* OCMNotificationPoster.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3159A146333BF0052CD09 /* OCMNotificationPoster.m */; }; + 817EB1281BD765130047E85A /* OCMObjectReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315A6146333BF0052CD09 /* OCMObjectReturnValueProvider.m */; }; + 817EB1291BD765130047E85A /* OCMLocation.m in Sources */ = {isa = PBXBuildFile; fileRef = 03E98D4A18F308B400522D42 /* OCMLocation.m */; }; + 817EB12A1BD765130047E85A /* OCMMacroState.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA280987F4EA8A4D79000D0 /* OCMMacroState.m */; }; + 817EB12B1BD765130047E85A /* OCMBlockArgCaller.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA283D58AA7569D8A5B0C57 /* OCMBlockArgCaller.m */; }; + 817EB12C1BD765130047E85A /* OCObserverMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315A8146333BF0052CD09 /* OCObserverMockObject.m */; }; + 817EB12D1BD765130047E85A /* OCMObserverRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3159C146333BF0052CD09 /* OCMObserverRecorder.m */; }; + 817EB12E1BD765130047E85A /* OCMArg.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3158E146333BF0052CD09 /* OCMArg.m */; }; + 817EB12F1BD765130047E85A /* OCMConstraint.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31594146333BF0052CD09 /* OCMConstraint.m */; }; + 817EB1301BD765130047E85A /* OCMPassByRefSetter.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315A2146333BF0052CD09 /* OCMPassByRefSetter.m */; }; + 817EB1311BD765130047E85A /* NSInvocation+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31586146333BF0052CD09 /* NSInvocation+OCMAdditions.m */; }; + 817EB1321BD765130047E85A /* NSMethodSignature+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31588146333BF0052CD09 /* NSMethodSignature+OCMAdditions.m */; }; + 817EB1331BD765130047E85A /* NSNotificationCenter+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3158A146333BF0052CD09 /* NSNotificationCenter+OCMAdditions.m */; }; + 817EB1341BD765130047E85A /* NSObject+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28991BEFD67DEF2CCF7D2 /* NSObject+OCMAdditions.m */; }; + 817EB1351BD765130047E85A /* NSValue+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28896E5EEFD7C2F12C2F8 /* NSValue+OCMAdditions.m */; }; + 817EB1361BD765130047E85A /* OCMFunctions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28C8717BE4B7A89119BA2 /* OCMFunctions.m */; }; + 817EB1371BD765130047E85A /* OCMArgAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28A7898E1046DE957A035 /* OCMArgAction.m */; }; + 817EB1391BD765130047E85A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0B9510A1B0080D500942C38 /* Foundation.framework */; }; + 817EB13B1BD765130047E85A /* OCMock.h in Headers */ = {isa = PBXBuildFile; fileRef = 030EF0B814632FD000B04273 /* OCMock.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 817EB13C1BD765130047E85A /* OCMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3159D146333BF0052CD09 /* OCMockObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 817EB13D1BD765130047E85A /* OCMStubRecorder.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3159F146333BF0052CD09 /* OCMStubRecorder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 817EB13E1BD765130047E85A /* OCMArg.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3158D146333BF0052CD09 /* OCMArg.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 817EB13F1BD765130047E85A /* OCMConstraint.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31593146333BF0052CD09 /* OCMConstraint.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 817EB1401BD765130047E85A /* OCMRecorder.h in Headers */ = {isa = PBXBuildFile; fileRef = 03618D81195B553400389166 /* OCMRecorder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 817EB1411BD765130047E85A /* NSNotificationCenter+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31589146333BF0052CD09 /* NSNotificationCenter+OCMAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 817EB1421BD765130047E85A /* OCMLocation.h in Headers */ = {isa = PBXBuildFile; fileRef = 03E98D4918F308B400522D42 /* OCMLocation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 817EB1431BD765130047E85A /* OCMMacroState.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA28006D043CBDBBAEF6E3F /* OCMMacroState.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 817EB1441BD765130047E85A /* OCClassMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3158B146333BF0052CD09 /* OCClassMockObject.h */; }; + 817EB1451BD765130047E85A /* OCPartialMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315A9146333BF0052CD09 /* OCPartialMockObject.h */; }; + 817EB1461BD765130047E85A /* OCProtocolMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315AD146333BF0052CD09 /* OCProtocolMockObject.h */; }; + 817EB1471BD765130047E85A /* OCMExpectationRecorder.h in Headers */ = {isa = PBXBuildFile; fileRef = 03C7BF0E195DAB5300A545DD /* OCMExpectationRecorder.h */; }; + 817EB1481BD765130047E85A /* OCMVerifier.h in Headers */ = {isa = PBXBuildFile; fileRef = 0322DA6719118B4600CACAF1 /* OCMVerifier.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 817EB1491BD765130047E85A /* OCMInvocationMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA28CCC33B99D6B658E7AF8 /* OCMInvocationMatcher.h */; }; + 817EB14A1BD765130047E85A /* OCMInvocationStub.h in Headers */ = {isa = PBXBuildFile; fileRef = 03C7BF08195DA2F200A545DD /* OCMInvocationStub.h */; }; + 817EB14B1BD765130047E85A /* OCMInvocationExpectation.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA288509D545BDBE094BCC8 /* OCMInvocationExpectation.h */; }; + 817EB14C1BD765130047E85A /* OCMRealObjectForwarder.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315A3146333BF0052CD09 /* OCMRealObjectForwarder.h */; }; + 817EB14D1BD765130047E85A /* OCMBlockCaller.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3158F146333BF0052CD09 /* OCMBlockCaller.h */; }; + 817EB14E1BD765130047E85A /* OCMBoxedReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31591146333BF0052CD09 /* OCMBoxedReturnValueProvider.h */; }; + 817EB14F1BD765130047E85A /* OCMExceptionReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31595146333BF0052CD09 /* OCMExceptionReturnValueProvider.h */; }; + 817EB1501BD765130047E85A /* OCMIndirectReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31597146333BF0052CD09 /* OCMIndirectReturnValueProvider.h */; }; + 817EB1511BD765130047E85A /* OCMNotificationPoster.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31599146333BF0052CD09 /* OCMNotificationPoster.h */; }; + 817EB1521BD765130047E85A /* OCMObjectReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315A5146333BF0052CD09 /* OCMObjectReturnValueProvider.h */; }; + 817EB1531BD765130047E85A /* OCMFunctionsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 03F370CA1BAA1DE800CAD3E8 /* OCMFunctionsPrivate.h */; }; + 817EB1541BD765130047E85A /* OCObserverMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315A7146333BF0052CD09 /* OCObserverMockObject.h */; }; + 817EB1551BD765130047E85A /* OCMObserverRecorder.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3159B146333BF0052CD09 /* OCMObserverRecorder.h */; }; + 817EB1561BD765130047E85A /* OCMPassByRefSetter.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315A1146333BF0052CD09 /* OCMPassByRefSetter.h */; }; + 817EB1571BD765130047E85A /* NSInvocation+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31585146333BF0052CD09 /* NSInvocation+OCMAdditions.h */; }; + 817EB1581BD765130047E85A /* NSMethodSignature+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31587146333BF0052CD09 /* NSMethodSignature+OCMAdditions.h */; }; + 817EB1591BD765130047E85A /* NSObject+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA28C5F4A475BEC0C28BDC3 /* NSObject+OCMAdditions.h */; }; + 817EB15A1BD765130047E85A /* NSValue+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA286EAC1682979C696D4D6 /* NSValue+OCMAdditions.h */; }; + 817EB15B1BD765130047E85A /* OCMFunctions.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA28EC49F6C59B940AE6D00 /* OCMFunctions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 817EB15C1BD765130047E85A /* OCMBlockArgCaller.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA2891034E7B73AA3511D17 /* OCMBlockArgCaller.h */; }; + 817EB15D1BD765130047E85A /* OCMArgAction.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA2833B48908EAD36444671 /* OCMArgAction.h */; }; + 817EB1661BD7674D0047E85A /* OCMFunctionsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 03F370CA1BAA1DE800CAD3E8 /* OCMFunctionsPrivate.h */; }; + 8B11D4B72448E2E900247BE2 /* OCMCPlusPlus98Tests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8B11D4B62448E2E900247BE2 /* OCMCPlusPlus98Tests.mm */; settings = {COMPILER_FLAGS = "-std=gnu++98"; }; }; + 8B11D4B82448E2F400247BE2 /* OCMCPlusPlus98Tests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8B11D4B62448E2E900247BE2 /* OCMCPlusPlus98Tests.mm */; settings = {COMPILER_FLAGS = "-std=gnu++98"; }; }; + 8B11D4BA2448E53600247BE2 /* OCMCPlusPlus11Tests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8B11D4B92448E53600247BE2 /* OCMCPlusPlus11Tests.mm */; settings = {COMPILER_FLAGS = "-std=gnu++11"; }; }; + 8B11D4BB2448E53600247BE2 /* OCMCPlusPlus11Tests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8B11D4B92448E53600247BE2 /* OCMCPlusPlus11Tests.mm */; settings = {COMPILER_FLAGS = "-std=gnu++11"; }; }; + 8BF73E53246CA75E00B9A52C /* OCMNoEscapeBlockTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BF73E52246CA75E00B9A52C /* OCMNoEscapeBlockTests.m */; settings = {COMPILER_FLAGS = "-Xclang -fexperimental-optimized-noescape"; }; }; + 8BF73E54246CA75E00B9A52C /* OCMNoEscapeBlockTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BF73E52246CA75E00B9A52C /* OCMNoEscapeBlockTests.m */; settings = {COMPILER_FLAGS = "-Xclang -fexperimental-optimized-noescape"; }; }; + 8DE97C5522B43EE60098C63F /* OCMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3159E146333BF0052CD09 /* OCMockObject.m */; }; + 8DE97C5622B43EE60098C63F /* OCClassMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3158C146333BF0052CD09 /* OCClassMockObject.m */; }; + 8DE97C5722B43EE60098C63F /* OCPartialMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315AA146333BF0052CD09 /* OCPartialMockObject.m */; }; + 8DE97C5822B43EE60098C63F /* OCProtocolMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315AE146333BF0052CD09 /* OCProtocolMockObject.m */; }; + 8DE97C5922B43EE60098C63F /* OCMRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 03618D82195B553400389166 /* OCMRecorder.m */; }; + 8DE97C5A22B43EE60098C63F /* OCMStubRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315A0146333BF0052CD09 /* OCMStubRecorder.m */; }; + 8DE97C5B22B43EE60098C63F /* OCMExpectationRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 03C7BF0F195DAB5300A545DD /* OCMExpectationRecorder.m */; }; + 8DE97C5C22B43EE60098C63F /* OCMVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 0322DA6819118B4600CACAF1 /* OCMVerifier.m */; }; + 8DE97C5D22B43EE60098C63F /* OCMInvocationMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28CB1EB14C6B2BE9A20C6 /* OCMInvocationMatcher.m */; }; + 8DE97C5E22B43EE60098C63F /* OCMInvocationStub.m in Sources */ = {isa = PBXBuildFile; fileRef = 03C7BF09195DA2F200A545DD /* OCMInvocationStub.m */; }; + 8DE97C5F22B43EE60098C63F /* OCMInvocationExpectation.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA2875564A6AD0510A847A6 /* OCMInvocationExpectation.m */; }; + 8DE97C6022B43EE60098C63F /* OCMRealObjectForwarder.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315A4146333BF0052CD09 /* OCMRealObjectForwarder.m */; }; + 8DE97C6122B43EE60098C63F /* OCMBlockCaller.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31590146333BF0052CD09 /* OCMBlockCaller.m */; }; + 8DE97C6222B43EE60098C63F /* OCMBoxedReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31592146333BF0052CD09 /* OCMBoxedReturnValueProvider.m */; }; + 8DE97C6322B43EE60098C63F /* OCMExceptionReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31596146333BF0052CD09 /* OCMExceptionReturnValueProvider.m */; }; + 8DE97C6422B43EE60098C63F /* OCMIndirectReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31598146333BF0052CD09 /* OCMIndirectReturnValueProvider.m */; }; + 8DE97C6522B43EE60098C63F /* OCMNotificationPoster.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3159A146333BF0052CD09 /* OCMNotificationPoster.m */; }; + 8DE97C6622B43EE60098C63F /* OCMObjectReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315A6146333BF0052CD09 /* OCMObjectReturnValueProvider.m */; }; + 8DE97C6722B43EE60098C63F /* OCMLocation.m in Sources */ = {isa = PBXBuildFile; fileRef = 03E98D4A18F308B400522D42 /* OCMLocation.m */; }; + 8DE97C6822B43EE60098C63F /* OCMMacroState.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA280987F4EA8A4D79000D0 /* OCMMacroState.m */; }; + 8DE97C6922B43EE60098C63F /* OCMBlockArgCaller.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA283D58AA7569D8A5B0C57 /* OCMBlockArgCaller.m */; }; + 8DE97C6A22B43EE60098C63F /* OCObserverMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315A8146333BF0052CD09 /* OCObserverMockObject.m */; }; + 8DE97C6B22B43EE60098C63F /* OCMObserverRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3159C146333BF0052CD09 /* OCMObserverRecorder.m */; }; + 8DE97C6C22B43EE60098C63F /* OCMArg.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3158E146333BF0052CD09 /* OCMArg.m */; }; + 8DE97C6D22B43EE60098C63F /* OCMConstraint.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31594146333BF0052CD09 /* OCMConstraint.m */; }; + 8DE97C6E22B43EE60098C63F /* OCMPassByRefSetter.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315A2146333BF0052CD09 /* OCMPassByRefSetter.m */; }; + 8DE97C6F22B43EE60098C63F /* NSInvocation+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31586146333BF0052CD09 /* NSInvocation+OCMAdditions.m */; }; + 8DE97C7022B43EE60098C63F /* NSMethodSignature+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31588146333BF0052CD09 /* NSMethodSignature+OCMAdditions.m */; }; + 8DE97C7122B43EE60098C63F /* NSNotificationCenter+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3158A146333BF0052CD09 /* NSNotificationCenter+OCMAdditions.m */; }; + 8DE97C7222B43EE60098C63F /* NSObject+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28991BEFD67DEF2CCF7D2 /* NSObject+OCMAdditions.m */; }; + 8DE97C7322B43EE60098C63F /* NSValue+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28896E5EEFD7C2F12C2F8 /* NSValue+OCMAdditions.m */; }; + 8DE97C7422B43EE60098C63F /* OCMFunctions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28C8717BE4B7A89119BA2 /* OCMFunctions.m */; }; + 8DE97C7522B43EE60098C63F /* OCMArgAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28A7898E1046DE957A035 /* OCMArgAction.m */; }; + 8DE97C7722B43EE60098C63F /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0B9510A1B0080D500942C38 /* Foundation.framework */; }; + 8DE97C7922B43EE60098C63F /* OCMock.h in Headers */ = {isa = PBXBuildFile; fileRef = 030EF0B814632FD000B04273 /* OCMock.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8DE97C7A22B43EE60098C63F /* OCMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3159D146333BF0052CD09 /* OCMockObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8DE97C7B22B43EE60098C63F /* OCMStubRecorder.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3159F146333BF0052CD09 /* OCMStubRecorder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8DE97C7C22B43EE60098C63F /* OCMArg.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3158D146333BF0052CD09 /* OCMArg.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8DE97C7D22B43EE60098C63F /* OCMConstraint.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31593146333BF0052CD09 /* OCMConstraint.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8DE97C7E22B43EE60098C63F /* OCMRecorder.h in Headers */ = {isa = PBXBuildFile; fileRef = 03618D81195B553400389166 /* OCMRecorder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8DE97C7F22B43EE60098C63F /* NSNotificationCenter+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31589146333BF0052CD09 /* NSNotificationCenter+OCMAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8DE97C8022B43EE60098C63F /* OCMLocation.h in Headers */ = {isa = PBXBuildFile; fileRef = 03E98D4918F308B400522D42 /* OCMLocation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8DE97C8122B43EE60098C63F /* OCMMacroState.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA28006D043CBDBBAEF6E3F /* OCMMacroState.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8DE97C8222B43EE60098C63F /* OCClassMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3158B146333BF0052CD09 /* OCClassMockObject.h */; }; + 8DE97C8322B43EE60098C63F /* OCPartialMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315A9146333BF0052CD09 /* OCPartialMockObject.h */; }; + 8DE97C8422B43EE60098C63F /* OCProtocolMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315AD146333BF0052CD09 /* OCProtocolMockObject.h */; }; + 8DE97C8522B43EE60098C63F /* OCMExpectationRecorder.h in Headers */ = {isa = PBXBuildFile; fileRef = 03C7BF0E195DAB5300A545DD /* OCMExpectationRecorder.h */; }; + 8DE97C8622B43EE60098C63F /* OCMVerifier.h in Headers */ = {isa = PBXBuildFile; fileRef = 0322DA6719118B4600CACAF1 /* OCMVerifier.h */; }; + 8DE97C8722B43EE60098C63F /* OCMInvocationMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA28CCC33B99D6B658E7AF8 /* OCMInvocationMatcher.h */; }; + 8DE97C8822B43EE60098C63F /* OCMInvocationStub.h in Headers */ = {isa = PBXBuildFile; fileRef = 03C7BF08195DA2F200A545DD /* OCMInvocationStub.h */; }; + 8DE97C8922B43EE60098C63F /* OCMInvocationExpectation.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA288509D545BDBE094BCC8 /* OCMInvocationExpectation.h */; }; + 8DE97C8A22B43EE60098C63F /* OCMRealObjectForwarder.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315A3146333BF0052CD09 /* OCMRealObjectForwarder.h */; }; + 8DE97C8B22B43EE60098C63F /* OCMBlockCaller.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3158F146333BF0052CD09 /* OCMBlockCaller.h */; }; + 8DE97C8C22B43EE60098C63F /* OCMBoxedReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31591146333BF0052CD09 /* OCMBoxedReturnValueProvider.h */; }; + 8DE97C8D22B43EE60098C63F /* OCMExceptionReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31595146333BF0052CD09 /* OCMExceptionReturnValueProvider.h */; }; + 8DE97C8E22B43EE60098C63F /* OCMIndirectReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31597146333BF0052CD09 /* OCMIndirectReturnValueProvider.h */; }; + 8DE97C8F22B43EE60098C63F /* OCMNotificationPoster.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31599146333BF0052CD09 /* OCMNotificationPoster.h */; }; + 8DE97C9022B43EE60098C63F /* OCMObjectReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315A5146333BF0052CD09 /* OCMObjectReturnValueProvider.h */; }; + 8DE97C9122B43EE60098C63F /* OCMFunctionsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 03F370CA1BAA1DE800CAD3E8 /* OCMFunctionsPrivate.h */; }; + 8DE97C9222B43EE60098C63F /* OCObserverMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315A7146333BF0052CD09 /* OCObserverMockObject.h */; }; + 8DE97C9322B43EE60098C63F /* OCMObserverRecorder.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3159B146333BF0052CD09 /* OCMObserverRecorder.h */; }; + 8DE97C9422B43EE60098C63F /* OCMPassByRefSetter.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315A1146333BF0052CD09 /* OCMPassByRefSetter.h */; }; + 8DE97C9522B43EE60098C63F /* NSInvocation+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31585146333BF0052CD09 /* NSInvocation+OCMAdditions.h */; }; + 8DE97C9622B43EE60098C63F /* NSMethodSignature+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31587146333BF0052CD09 /* NSMethodSignature+OCMAdditions.h */; }; + 8DE97C9722B43EE60098C63F /* NSObject+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA28C5F4A475BEC0C28BDC3 /* NSObject+OCMAdditions.h */; }; + 8DE97C9822B43EE60098C63F /* NSValue+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA286EAC1682979C696D4D6 /* NSValue+OCMAdditions.h */; }; + 8DE97C9922B43EE60098C63F /* OCMFunctions.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA28EC49F6C59B940AE6D00 /* OCMFunctions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8DE97C9A22B43EE60098C63F /* OCMBlockArgCaller.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA2891034E7B73AA3511D17 /* OCMBlockArgCaller.h */; }; + 8DE97C9B22B43EE60098C63F /* OCMArgAction.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA2833B48908EAD36444671 /* OCMArgAction.h */; }; + A02926821CA0725A00594AAF /* TestObjects.xcdatamodeld in Resources */ = {isa = PBXBuildFile; fileRef = A02926801CA0725A00594AAF /* TestObjects.xcdatamodeld */; }; + A06930951CA1BFC900513023 /* TestObjects.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = A02926801CA0725A00594AAF /* TestObjects.xcdatamodeld */; }; + D31108C41828DBD600737925 /* OCMockObjectPartialMocksTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03AC5C1416DF9FA500D82ECD /* OCMockObjectPartialMocksTests.m */; }; + D31108C51828DBD600737925 /* OCMockObjectClassMethodMockingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 039F91C516EFB493006C3D70 /* OCMockObjectClassMethodMockingTests.m */; }; + D31108C61828DBD600737925 /* OCMockObjectForwardingTargetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0354D71F16F23AF5001766BB /* OCMockObjectForwardingTargetTests.m */; }; + D31108C71828DBD600737925 /* OCMConstraintTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B316211463350E0052CD09 /* OCMConstraintTests.m */; }; + D31108C81828DBD600737925 /* OCMStubRecorderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B316271463350E0052CD09 /* OCMStubRecorderTests.m */; }; + D31108C91828DBD600737925 /* OCObserverMockObjectTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B316291463350E0052CD09 /* OCObserverMockObjectTests.m */; }; + D31108CA1828DBD600737925 /* NSInvocationOCMAdditionsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3161F1463350E0052CD09 /* NSInvocationOCMAdditionsTests.m */; }; + D31108CB1828DC1300737925 /* libOCMock.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 030EF0DC14632FF700B04273 /* libOCMock.a */; }; + F0B9510B1B0080D500942C38 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F0B9510A1B0080D500942C38 /* Foundation.framework */; }; + F0B9510C1B0080EC00942C38 /* OCMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3159E146333BF0052CD09 /* OCMockObject.m */; }; + F0B9510D1B0080EC00942C38 /* OCClassMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3158C146333BF0052CD09 /* OCClassMockObject.m */; }; + F0B9510E1B0080EC00942C38 /* OCPartialMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315AA146333BF0052CD09 /* OCPartialMockObject.m */; }; + F0B9510F1B0080EC00942C38 /* OCProtocolMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315AE146333BF0052CD09 /* OCProtocolMockObject.m */; }; + F0B951101B0080EC00942C38 /* OCMRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 03618D82195B553400389166 /* OCMRecorder.m */; }; + F0B951111B0080EC00942C38 /* OCMStubRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315A0146333BF0052CD09 /* OCMStubRecorder.m */; }; + F0B951121B0080EC00942C38 /* OCMExpectationRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 03C7BF0F195DAB5300A545DD /* OCMExpectationRecorder.m */; }; + F0B951131B0080EC00942C38 /* OCMVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 0322DA6819118B4600CACAF1 /* OCMVerifier.m */; }; + F0B951141B0080EC00942C38 /* OCMInvocationMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28CB1EB14C6B2BE9A20C6 /* OCMInvocationMatcher.m */; }; + F0B951151B0080EC00942C38 /* OCMInvocationStub.m in Sources */ = {isa = PBXBuildFile; fileRef = 03C7BF09195DA2F200A545DD /* OCMInvocationStub.m */; }; + F0B951161B0080EC00942C38 /* OCMInvocationExpectation.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA2875564A6AD0510A847A6 /* OCMInvocationExpectation.m */; }; + F0B951171B0080EC00942C38 /* OCMRealObjectForwarder.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315A4146333BF0052CD09 /* OCMRealObjectForwarder.m */; }; + F0B951181B0080EC00942C38 /* OCMBlockCaller.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31590146333BF0052CD09 /* OCMBlockCaller.m */; }; + F0B951191B0080EC00942C38 /* OCMBoxedReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31592146333BF0052CD09 /* OCMBoxedReturnValueProvider.m */; }; + F0B9511A1B0080EC00942C38 /* OCMExceptionReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31596146333BF0052CD09 /* OCMExceptionReturnValueProvider.m */; }; + F0B9511B1B0080EC00942C38 /* OCMIndirectReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31598146333BF0052CD09 /* OCMIndirectReturnValueProvider.m */; }; + F0B9511C1B0080EC00942C38 /* OCMNotificationPoster.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3159A146333BF0052CD09 /* OCMNotificationPoster.m */; }; + F0B9511D1B0080EC00942C38 /* OCMObjectReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315A6146333BF0052CD09 /* OCMObjectReturnValueProvider.m */; }; + F0B9511E1B0080EC00942C38 /* OCMLocation.m in Sources */ = {isa = PBXBuildFile; fileRef = 03E98D4A18F308B400522D42 /* OCMLocation.m */; }; + F0B9511F1B0080EC00942C38 /* OCMMacroState.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA280987F4EA8A4D79000D0 /* OCMMacroState.m */; }; + F0B951201B0080EC00942C38 /* OCObserverMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315A8146333BF0052CD09 /* OCObserverMockObject.m */; }; + F0B951211B0080EC00942C38 /* OCMObserverRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3159C146333BF0052CD09 /* OCMObserverRecorder.m */; }; + F0B951221B0080EC00942C38 /* OCMArg.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3158E146333BF0052CD09 /* OCMArg.m */; }; + F0B951231B0080EC00942C38 /* OCMConstraint.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31594146333BF0052CD09 /* OCMConstraint.m */; }; + F0B951241B0080EC00942C38 /* OCMPassByRefSetter.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315A2146333BF0052CD09 /* OCMPassByRefSetter.m */; }; + F0B951251B0080EC00942C38 /* NSInvocation+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31586146333BF0052CD09 /* NSInvocation+OCMAdditions.m */; }; + F0B951261B0080EC00942C38 /* NSMethodSignature+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B31588146333BF0052CD09 /* NSMethodSignature+OCMAdditions.m */; }; + F0B951271B0080EC00942C38 /* NSNotificationCenter+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3158A146333BF0052CD09 /* NSNotificationCenter+OCMAdditions.m */; }; + F0B951281B0080EC00942C38 /* NSObject+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28991BEFD67DEF2CCF7D2 /* NSObject+OCMAdditions.m */; }; + F0B951291B0080EC00942C38 /* NSValue+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28896E5EEFD7C2F12C2F8 /* NSValue+OCMAdditions.m */; }; + F0B9512A1B0080EC00942C38 /* OCMFunctions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA28C8717BE4B7A89119BA2 /* OCMFunctions.m */; }; + F0B9512B1B00810C00942C38 /* OCMock.h in Headers */ = {isa = PBXBuildFile; fileRef = 030EF0B814632FD000B04273 /* OCMock.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F0B9512C1B00810C00942C38 /* OCMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3159D146333BF0052CD09 /* OCMockObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F0B9512D1B00810C00942C38 /* OCClassMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3158B146333BF0052CD09 /* OCClassMockObject.h */; }; + F0B9512E1B00810C00942C38 /* OCPartialMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315A9146333BF0052CD09 /* OCPartialMockObject.h */; }; + F0B9512F1B00810C00942C38 /* OCProtocolMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315AD146333BF0052CD09 /* OCProtocolMockObject.h */; }; + F0B951301B00810C00942C38 /* OCMRecorder.h in Headers */ = {isa = PBXBuildFile; fileRef = 03618D81195B553400389166 /* OCMRecorder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F0B951311B00810C00942C38 /* OCMStubRecorder.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3159F146333BF0052CD09 /* OCMStubRecorder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F0B951321B00810C00942C38 /* OCMExpectationRecorder.h in Headers */ = {isa = PBXBuildFile; fileRef = 03C7BF0E195DAB5300A545DD /* OCMExpectationRecorder.h */; }; + F0B951331B00810C00942C38 /* OCMVerifier.h in Headers */ = {isa = PBXBuildFile; fileRef = 0322DA6719118B4600CACAF1 /* OCMVerifier.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F0B951341B00810C00942C38 /* OCMInvocationMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA28CCC33B99D6B658E7AF8 /* OCMInvocationMatcher.h */; }; + F0B951351B00810C00942C38 /* OCMInvocationStub.h in Headers */ = {isa = PBXBuildFile; fileRef = 03C7BF08195DA2F200A545DD /* OCMInvocationStub.h */; }; + F0B951361B00810C00942C38 /* OCMInvocationExpectation.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA288509D545BDBE094BCC8 /* OCMInvocationExpectation.h */; }; + F0B951371B00810C00942C38 /* OCMRealObjectForwarder.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315A3146333BF0052CD09 /* OCMRealObjectForwarder.h */; }; + F0B951381B00810C00942C38 /* OCMBlockCaller.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3158F146333BF0052CD09 /* OCMBlockCaller.h */; }; + F0B951391B00810C00942C38 /* OCMBoxedReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31591146333BF0052CD09 /* OCMBoxedReturnValueProvider.h */; }; + F0B9513A1B00810C00942C38 /* OCMExceptionReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31595146333BF0052CD09 /* OCMExceptionReturnValueProvider.h */; }; + F0B9513B1B00810C00942C38 /* OCMIndirectReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31597146333BF0052CD09 /* OCMIndirectReturnValueProvider.h */; }; + F0B9513C1B00810C00942C38 /* OCMNotificationPoster.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31599146333BF0052CD09 /* OCMNotificationPoster.h */; }; + F0B9513D1B00810C00942C38 /* OCMObjectReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315A5146333BF0052CD09 /* OCMObjectReturnValueProvider.h */; }; + F0B9513E1B00810C00942C38 /* OCMLocation.h in Headers */ = {isa = PBXBuildFile; fileRef = 03E98D4918F308B400522D42 /* OCMLocation.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F0B9513F1B00810C00942C38 /* OCMMacroState.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA28006D043CBDBBAEF6E3F /* OCMMacroState.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F0B951401B00810C00942C38 /* OCObserverMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315A7146333BF0052CD09 /* OCObserverMockObject.h */; }; + F0B951411B00810C00942C38 /* OCMObserverRecorder.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3159B146333BF0052CD09 /* OCMObserverRecorder.h */; }; + F0B951421B00810C00942C38 /* OCMArg.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B3158D146333BF0052CD09 /* OCMArg.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F0B951431B00810C00942C38 /* OCMConstraint.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31593146333BF0052CD09 /* OCMConstraint.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F0B951441B00810C00942C38 /* OCMPassByRefSetter.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B315A1146333BF0052CD09 /* OCMPassByRefSetter.h */; }; + F0B951451B00810C00942C38 /* NSInvocation+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31585146333BF0052CD09 /* NSInvocation+OCMAdditions.h */; }; + F0B951461B00810C00942C38 /* NSMethodSignature+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31587146333BF0052CD09 /* NSMethodSignature+OCMAdditions.h */; }; + F0B951471B00810C00942C38 /* NSNotificationCenter+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 03B31589146333BF0052CD09 /* NSNotificationCenter+OCMAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F0B951481B00810C00942C38 /* NSObject+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA28C5F4A475BEC0C28BDC3 /* NSObject+OCMAdditions.h */; }; + F0B951491B00810C00942C38 /* NSValue+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA286EAC1682979C696D4D6 /* NSValue+OCMAdditions.h */; }; + F0B9514A1B00810C00942C38 /* OCMFunctions.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA28EC49F6C59B940AE6D00 /* OCMFunctions.h */; settings = {ATTRIBUTES = (Public, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 03565A3C18F0566F003AE91E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 030EF09E14632FD000B04273 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 030EF0A714632FD000B04273; + remoteInfo = OCMock; + }; + D31108BE1828DB8700737925 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 030EF09E14632FD000B04273 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 030EF0DB14632FF700B04273; + remoteInfo = OCMockLib; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 0397296423907547000E1C45 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 16; + files = ( + 03CED2E423907759001845CC /* OCHamcrest.framework in CopyFiles */, + 03CED2E523907759001845CC /* OCHamcrest.framework.dSYM in CopyFiles */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 030EF0A814632FD000B04273 /* OCMock.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OCMock.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 030EF0B314632FD000B04273 /* OCMock-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "OCMock-Info.plist"; sourceTree = ""; }; + 030EF0B714632FD000B04273 /* OCMock-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OCMock-Prefix.pch"; sourceTree = ""; }; + 030EF0B814632FD000B04273 /* OCMock.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCMock.h; sourceTree = ""; }; + 030EF0DC14632FF700B04273 /* libOCMock.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libOCMock.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 030EF0E114632FF700B04273 /* OCMockLib-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OCMockLib-Prefix.pch"; sourceTree = ""; }; + 031E50571BB4A56300E257C3 /* OCMBoxedReturnValueProviderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMBoxedReturnValueProviderTests.m; sourceTree = ""; }; + 0322DA64191188D100CACAF1 /* OCMockObjectVerifyAfterRunTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMockObjectVerifyAfterRunTests.m; sourceTree = ""; }; + 0322DA6719118B4600CACAF1 /* OCMVerifier.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMVerifier.h; sourceTree = ""; }; + 0322DA6819118B4600CACAF1 /* OCMVerifier.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMVerifier.m; sourceTree = ""; }; + 033AB1F924F046C7002014AE /* OCMockMacros.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCMockMacros.h; sourceTree = ""; }; + 033E1FF314FEF5E0004456B0 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + 0354D71F16F23AF5001766BB /* OCMockObjectForwardingTargetTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMockObjectForwardingTargetTests.m; sourceTree = ""; }; + 03565A1D18F05626003AE91E /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; + 03565A3118F0566E003AE91E /* OCMockTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OCMockTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 03565A3518F0566F003AE91E /* OCMockTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "OCMockTests-Info.plist"; sourceTree = ""; }; + 03565A3B18F0566F003AE91E /* OCMockTests-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OCMockTests-Prefix.pch"; sourceTree = ""; }; + 03618D81195B553400389166 /* OCMRecorder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMRecorder.h; sourceTree = ""; }; + 03618D82195B553400389166 /* OCMRecorder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMRecorder.m; sourceTree = ""; }; + 036865631D3571A8005E6BEE /* OCMQuantifierTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMQuantifierTests.m; sourceTree = ""; }; + 036865671D3572ED005E6BEE /* OCMQuantifier.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMQuantifier.m; sourceTree = ""; }; + 0368656C1D35730B005E6BEE /* OCMQuantifier.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OCMQuantifier.h; sourceTree = ""; }; + 037ECD5318FAD84100AF0E4C /* OCMInvocationMatcherTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMInvocationMatcherTests.m; sourceTree = ""; }; + 038599F623807B06002B3ABE /* OCMockObjectInternalTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCMockObjectInternalTests.m; sourceTree = ""; }; + 039F91C516EFB493006C3D70 /* OCMockObjectClassMethodMockingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMockObjectClassMethodMockingTests.m; sourceTree = ""; }; + 03AC5C1416DF9FA500D82ECD /* OCMockObjectPartialMocksTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMockObjectPartialMocksTests.m; sourceTree = ""; }; + 03B31585146333BF0052CD09 /* NSInvocation+OCMAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSInvocation+OCMAdditions.h"; sourceTree = ""; }; + 03B31586146333BF0052CD09 /* NSInvocation+OCMAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSInvocation+OCMAdditions.m"; sourceTree = ""; }; + 03B31587146333BF0052CD09 /* NSMethodSignature+OCMAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSMethodSignature+OCMAdditions.h"; sourceTree = ""; }; + 03B31588146333BF0052CD09 /* NSMethodSignature+OCMAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSMethodSignature+OCMAdditions.m"; sourceTree = ""; }; + 03B31589146333BF0052CD09 /* NSNotificationCenter+OCMAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSNotificationCenter+OCMAdditions.h"; sourceTree = ""; }; + 03B3158A146333BF0052CD09 /* NSNotificationCenter+OCMAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSNotificationCenter+OCMAdditions.m"; sourceTree = ""; }; + 03B3158B146333BF0052CD09 /* OCClassMockObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCClassMockObject.h; sourceTree = ""; }; + 03B3158C146333BF0052CD09 /* OCClassMockObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCClassMockObject.m; sourceTree = ""; }; + 03B3158D146333BF0052CD09 /* OCMArg.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMArg.h; sourceTree = ""; }; + 03B3158E146333BF0052CD09 /* OCMArg.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMArg.m; sourceTree = ""; }; + 03B3158F146333BF0052CD09 /* OCMBlockCaller.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMBlockCaller.h; sourceTree = ""; }; + 03B31590146333BF0052CD09 /* OCMBlockCaller.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMBlockCaller.m; sourceTree = ""; }; + 03B31591146333BF0052CD09 /* OCMBoxedReturnValueProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMBoxedReturnValueProvider.h; sourceTree = ""; }; + 03B31592146333BF0052CD09 /* OCMBoxedReturnValueProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMBoxedReturnValueProvider.m; sourceTree = ""; }; + 03B31593146333BF0052CD09 /* OCMConstraint.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMConstraint.h; sourceTree = ""; }; + 03B31594146333BF0052CD09 /* OCMConstraint.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMConstraint.m; sourceTree = ""; }; + 03B31595146333BF0052CD09 /* OCMExceptionReturnValueProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMExceptionReturnValueProvider.h; sourceTree = ""; }; + 03B31596146333BF0052CD09 /* OCMExceptionReturnValueProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMExceptionReturnValueProvider.m; sourceTree = ""; }; + 03B31597146333BF0052CD09 /* OCMIndirectReturnValueProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMIndirectReturnValueProvider.h; sourceTree = ""; }; + 03B31598146333BF0052CD09 /* OCMIndirectReturnValueProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMIndirectReturnValueProvider.m; sourceTree = ""; }; + 03B31599146333BF0052CD09 /* OCMNotificationPoster.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMNotificationPoster.h; sourceTree = ""; }; + 03B3159A146333BF0052CD09 /* OCMNotificationPoster.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMNotificationPoster.m; sourceTree = ""; }; + 03B3159B146333BF0052CD09 /* OCMObserverRecorder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMObserverRecorder.h; sourceTree = ""; }; + 03B3159C146333BF0052CD09 /* OCMObserverRecorder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMObserverRecorder.m; sourceTree = ""; }; + 03B3159D146333BF0052CD09 /* OCMockObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMockObject.h; sourceTree = ""; }; + 03B3159E146333BF0052CD09 /* OCMockObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMockObject.m; sourceTree = ""; }; + 03B3159F146333BF0052CD09 /* OCMStubRecorder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMStubRecorder.h; sourceTree = ""; }; + 03B315A0146333BF0052CD09 /* OCMStubRecorder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMStubRecorder.m; sourceTree = ""; }; + 03B315A1146333BF0052CD09 /* OCMPassByRefSetter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMPassByRefSetter.h; sourceTree = ""; }; + 03B315A2146333BF0052CD09 /* OCMPassByRefSetter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMPassByRefSetter.m; sourceTree = ""; }; + 03B315A3146333BF0052CD09 /* OCMRealObjectForwarder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMRealObjectForwarder.h; sourceTree = ""; }; + 03B315A4146333BF0052CD09 /* OCMRealObjectForwarder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMRealObjectForwarder.m; sourceTree = ""; }; + 03B315A5146333BF0052CD09 /* OCMObjectReturnValueProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMObjectReturnValueProvider.h; sourceTree = ""; }; + 03B315A6146333BF0052CD09 /* OCMObjectReturnValueProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMObjectReturnValueProvider.m; sourceTree = ""; }; + 03B315A7146333BF0052CD09 /* OCObserverMockObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCObserverMockObject.h; sourceTree = ""; }; + 03B315A8146333BF0052CD09 /* OCObserverMockObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCObserverMockObject.m; sourceTree = ""; }; + 03B315A9146333BF0052CD09 /* OCPartialMockObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCPartialMockObject.h; sourceTree = ""; }; + 03B315AA146333BF0052CD09 /* OCPartialMockObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCPartialMockObject.m; sourceTree = ""; }; + 03B315AD146333BF0052CD09 /* OCProtocolMockObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCProtocolMockObject.h; sourceTree = ""; }; + 03B315AE146333BF0052CD09 /* OCProtocolMockObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCProtocolMockObject.m; sourceTree = ""; }; + 03B3161F1463350E0052CD09 /* NSInvocationOCMAdditionsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSInvocationOCMAdditionsTests.m; sourceTree = ""; }; + 03B316211463350E0052CD09 /* OCMConstraintTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMConstraintTests.m; sourceTree = ""; }; + 03B316231463350E0052CD09 /* OCMockObjectHamcrestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMockObjectHamcrestTests.m; sourceTree = ""; }; + 03B316271463350E0052CD09 /* OCMStubRecorderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMStubRecorderTests.m; sourceTree = ""; }; + 03B316291463350E0052CD09 /* OCObserverMockObjectTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCObserverMockObjectTests.m; sourceTree = ""; }; + 03B3168F14633FAB0052CD09 /* Changes.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = Changes.txt; sourceTree = ""; }; + 03BA728F23907AD600877EF8 /* Cartfile.resolved */ = {isa = PBXFileReference; lastKnownFileType = text; path = Cartfile.resolved; sourceTree = SOURCE_ROOT; }; + 03BA729023907AD600877EF8 /* Cartfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Cartfile; sourceTree = SOURCE_ROOT; }; + 03C7BF08195DA2F200A545DD /* OCMInvocationStub.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMInvocationStub.h; sourceTree = ""; }; + 03C7BF09195DA2F200A545DD /* OCMInvocationStub.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMInvocationStub.m; sourceTree = ""; }; + 03C7BF0E195DAB5300A545DD /* OCMExpectationRecorder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMExpectationRecorder.h; sourceTree = ""; }; + 03C7BF0F195DAB5300A545DD /* OCMExpectationRecorder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMExpectationRecorder.m; sourceTree = ""; }; + 03CED2E02390770B001845CC /* OCHamcrest.framework.dSYM */ = {isa = PBXFileReference; lastKnownFileType = wrapper.dsym; name = OCHamcrest.framework.dSYM; path = Carthage/Build/Mac/OCHamcrest.framework.dSYM; sourceTree = ""; }; + 03CED2E12390770C001845CC /* OCHamcrest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OCHamcrest.framework; path = Carthage/Build/Mac/OCHamcrest.framework; sourceTree = ""; }; + 03E98D4918F308B400522D42 /* OCMLocation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMLocation.h; sourceTree = ""; }; + 03E98D4A18F308B400522D42 /* OCMLocation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMLocation.m; sourceTree = ""; }; + 03E98D4F18F310EE00522D42 /* OCMockObjectMacroTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMockObjectMacroTests.m; sourceTree = ""; }; + 03F370CA1BAA1DE800CAD3E8 /* OCMFunctionsPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMFunctionsPrivate.h; sourceTree = ""; }; + 2FA28006D043CBDBBAEF6E3F /* OCMMacroState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMMacroState.h; sourceTree = ""; }; + 2FA280987F4EA8A4D79000D0 /* OCMMacroState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMMacroState.m; sourceTree = ""; }; + 2FA280EB5E8CDEEAE76861F7 /* OCMNonRetainingObjectReturnValueProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMNonRetainingObjectReturnValueProvider.m; sourceTree = ""; }; + 2FA2813F93050582D83E1499 /* OCMockObjectRuntimeTests.m */ = {isa = PBXFileReference; explicitFileType = sourcecode.c.objc; fileEncoding = 4; path = OCMockObjectRuntimeTests.m; sourceTree = ""; }; + 2FA2822E19948FC997965267 /* OCMockObjectTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMockObjectTests.m; sourceTree = ""; }; + 2FA2833B48908EAD36444671 /* OCMArgAction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMArgAction.h; sourceTree = ""; }; + 2FA283D58AA7569D8A5B0C57 /* OCMBlockArgCaller.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMBlockArgCaller.m; sourceTree = ""; }; + 2FA286BFBD8B9D068B41E7EF /* OCMockObjectProtocolMocksTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMockObjectProtocolMocksTests.m; sourceTree = ""; }; + 2FA286EAC1682979C696D4D6 /* NSValue+OCMAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSValue+OCMAdditions.h"; sourceTree = ""; }; + 2FA2875564A6AD0510A847A6 /* OCMInvocationExpectation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMInvocationExpectation.m; sourceTree = ""; }; + 2FA288509D545BDBE094BCC8 /* OCMInvocationExpectation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMInvocationExpectation.h; sourceTree = ""; }; + 2FA28896E5EEFD7C2F12C2F8 /* NSValue+OCMAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSValue+OCMAdditions.m"; sourceTree = ""; }; + 2FA2891034E7B73AA3511D17 /* OCMBlockArgCaller.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMBlockArgCaller.h; sourceTree = ""; }; + 2FA28991BEFD67DEF2CCF7D2 /* NSObject+OCMAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSObject+OCMAdditions.m"; sourceTree = ""; }; + 2FA28A7898E1046DE957A035 /* OCMArgAction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMArgAction.m; sourceTree = ""; }; + 2FA28B1133B294089D2245AC /* OCMNonRetainingObjectReturnValueProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMNonRetainingObjectReturnValueProvider.h; sourceTree = ""; }; + 2FA28C5F4A475BEC0C28BDC3 /* NSObject+OCMAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSObject+OCMAdditions.h"; sourceTree = ""; }; + 2FA28C8717BE4B7A89119BA2 /* OCMFunctions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMFunctions.m; sourceTree = ""; }; + 2FA28CB1EB14C6B2BE9A20C6 /* OCMInvocationMatcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMInvocationMatcher.m; sourceTree = ""; }; + 2FA28CCC33B99D6B658E7AF8 /* OCMInvocationMatcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMInvocationMatcher.h; sourceTree = ""; }; + 2FA28DEDB9163597B7C49F3D /* NSMethodSignatureOCMAdditionsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSMethodSignatureOCMAdditionsTests.m; sourceTree = ""; }; + 2FA28EC49F6C59B940AE6D00 /* OCMFunctions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCMFunctions.h; sourceTree = ""; }; + 2FA28EDBF243639C57F88A1B /* OCMArgTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMArgTests.m; sourceTree = ""; }; + 2FA28EE3142412BD601026EF /* OCMockObjectDynamicPropertyMockingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMockObjectDynamicPropertyMockingTests.m; sourceTree = ""; }; + 3CFBDD751BB3DB200050D9C5 /* TestClassWithCustomReferenceCounting.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TestClassWithCustomReferenceCounting.h; sourceTree = ""; }; + 3CFBDD761BB3DB200050D9C5 /* TestClassWithCustomReferenceCounting.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TestClassWithCustomReferenceCounting.m; sourceTree = ""; }; + 817EB1621BD765130047E85A /* OCMock.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OCMock.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8B11D4B62448E2E900247BE2 /* OCMCPlusPlus98Tests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = OCMCPlusPlus98Tests.mm; sourceTree = ""; }; + 8B11D4B92448E53600247BE2 /* OCMCPlusPlus11Tests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = OCMCPlusPlus11Tests.mm; sourceTree = ""; }; + 8BF73E52246CA75E00B9A52C /* OCMNoEscapeBlockTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMNoEscapeBlockTests.m; sourceTree = ""; }; + 8DE97CA022B43EE60098C63F /* OCMock.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OCMock.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A02926811CA0725A00594AAF /* TestObjects.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = TestObjects.xcdatamodel; sourceTree = ""; }; + D31108AD1828DB8700737925 /* OCMockLibTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OCMockLibTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + D31108B71828DB8700737925 /* OCMockLibTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "OCMockLibTests-Info.plist"; sourceTree = ""; }; + D31108BD1828DB8700737925 /* OCMockLibTests-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OCMockLibTests-Prefix.pch"; sourceTree = ""; }; + F0B950F11B0080BE00942C38 /* OCMock.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OCMock.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F0B9510A1B0080D500942C38 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS8.3.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 030EF0A414632FD000B04273 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 033E1FF414FEF5E0004456B0 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 030EF0D914632FF700B04273 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 03565A2E18F0566E003AE91E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 03C7BF1619606E7A00A545DD /* XCTest.framework in Frameworks */, + 03C7BF1719606EFD00A545DD /* OCMock.framework in Frameworks */, + 03CED2E32390770C001845CC /* OCHamcrest.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 817EB1381BD765130047E85A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 817EB1391BD765130047E85A /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8DE97C7622B43EE60098C63F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8DE97C7722B43EE60098C63F /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D31108AA1828DB8700737925 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D31108CB1828DC1300737925 /* libOCMock.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F0B950ED1B0080BE00942C38 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F0B9510B1B0080D500942C38 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 030EF09C14632FD000B04273 = { + isa = PBXGroup; + children = ( + 03B3168F14633FAB0052CD09 /* Changes.txt */, + 03BA729023907AD600877EF8 /* Cartfile */, + 03BA728F23907AD600877EF8 /* Cartfile.resolved */, + 030EF0B114632FD000B04273 /* OCMock */, + 03565A3318F0566F003AE91E /* OCMockTests */, + 030EF0DF14632FF700B04273 /* OCMockLib */, + D31108B51828DB8700737925 /* OCMockLibTests */, + 03565A1C18F05626003AE91E /* Frameworks */, + 030EF0A914632FD000B04273 /* Products */, + ); + sourceTree = ""; + }; + 030EF0A914632FD000B04273 /* Products */ = { + isa = PBXGroup; + children = ( + 030EF0A814632FD000B04273 /* OCMock.framework */, + 030EF0DC14632FF700B04273 /* libOCMock.a */, + D31108AD1828DB8700737925 /* OCMockLibTests.xctest */, + 03565A3118F0566E003AE91E /* OCMockTests.xctest */, + F0B950F11B0080BE00942C38 /* OCMock.framework */, + 817EB1621BD765130047E85A /* OCMock.framework */, + 8DE97CA022B43EE60098C63F /* OCMock.framework */, + ); + name = Products; + sourceTree = ""; + }; + 030EF0B114632FD000B04273 /* OCMock */ = { + isa = PBXGroup; + children = ( + 030EF0B814632FD000B04273 /* OCMock.h */, + 03B315841463334E0052CD09 /* Core Mocks */, + 03B3161B146334530052CD09 /* Observer Mocks */, + 03B31618146333CA0052CD09 /* Foundation Additions */, + 030EF0B214632FD000B04273 /* Supporting Files */, + 033E1FF914FEF724004456B0 /* Frameworks */, + ); + path = OCMock; + sourceTree = ""; + }; + 030EF0B214632FD000B04273 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 030EF0B314632FD000B04273 /* OCMock-Info.plist */, + 030EF0B714632FD000B04273 /* OCMock-Prefix.pch */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 030EF0DF14632FF700B04273 /* OCMockLib */ = { + isa = PBXGroup; + children = ( + 030EF0E014632FF700B04273 /* Supporting Files */, + ); + path = OCMockLib; + sourceTree = ""; + }; + 030EF0E014632FF700B04273 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 030EF0E114632FF700B04273 /* OCMockLib-Prefix.pch */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 033E1FF914FEF724004456B0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 033E1FF314FEF5E0004456B0 /* Foundation.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 03565A1C18F05626003AE91E /* Frameworks */ = { + isa = PBXGroup; + children = ( + F0B9510A1B0080D500942C38 /* Foundation.framework */, + 03565A1D18F05626003AE91E /* XCTest.framework */, + 03CED2E12390770C001845CC /* OCHamcrest.framework */, + 03CED2E02390770B001845CC /* OCHamcrest.framework.dSYM */, + ); + name = Frameworks; + sourceTree = ""; + }; + 03565A3318F0566F003AE91E /* OCMockTests */ = { + isa = PBXGroup; + children = ( + 2FA2822E19948FC997965267 /* OCMockObjectTests.m */, + 03AC5C1416DF9FA500D82ECD /* OCMockObjectPartialMocksTests.m */, + 039F91C516EFB493006C3D70 /* OCMockObjectClassMethodMockingTests.m */, + 2FA286BFBD8B9D068B41E7EF /* OCMockObjectProtocolMocksTests.m */, + 2FA28EE3142412BD601026EF /* OCMockObjectDynamicPropertyMockingTests.m */, + 03E98D4F18F310EE00522D42 /* OCMockObjectMacroTests.m */, + 0354D71F16F23AF5001766BB /* OCMockObjectForwardingTargetTests.m */, + 0322DA64191188D100CACAF1 /* OCMockObjectVerifyAfterRunTests.m */, + 03B316231463350E0052CD09 /* OCMockObjectHamcrestTests.m */, + 038599F623807B06002B3ABE /* OCMockObjectInternalTests.m */, + 2FA2813F93050582D83E1499 /* OCMockObjectRuntimeTests.m */, + 8BF73E52246CA75E00B9A52C /* OCMNoEscapeBlockTests.m */, + 03B316271463350E0052CD09 /* OCMStubRecorderTests.m */, + 037ECD5318FAD84100AF0E4C /* OCMInvocationMatcherTests.m */, + 031E50571BB4A56300E257C3 /* OCMBoxedReturnValueProviderTests.m */, + 03B316211463350E0052CD09 /* OCMConstraintTests.m */, + 8B11D4B62448E2E900247BE2 /* OCMCPlusPlus98Tests.mm */, + 8B11D4B92448E53600247BE2 /* OCMCPlusPlus11Tests.mm */, + 2FA28EDBF243639C57F88A1B /* OCMArgTests.m */, + 036865631D3571A8005E6BEE /* OCMQuantifierTests.m */, + 03B316291463350E0052CD09 /* OCObserverMockObjectTests.m */, + 03B3161F1463350E0052CD09 /* NSInvocationOCMAdditionsTests.m */, + 2FA28DEDB9163597B7C49F3D /* NSMethodSignatureOCMAdditionsTests.m */, + 3CFBDD751BB3DB200050D9C5 /* TestClassWithCustomReferenceCounting.h */, + 3CFBDD761BB3DB200050D9C5 /* TestClassWithCustomReferenceCounting.m */, + 03565A3418F0566F003AE91E /* Supporting Files */, + ); + path = OCMockTests; + sourceTree = ""; + }; + 03565A3418F0566F003AE91E /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 03565A3518F0566F003AE91E /* OCMockTests-Info.plist */, + 03565A3B18F0566F003AE91E /* OCMockTests-Prefix.pch */, + A02926801CA0725A00594AAF /* TestObjects.xcdatamodeld */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 036865661D3572C2005E6BEE /* Quantifier */ = { + isa = PBXGroup; + children = ( + 0368656C1D35730B005E6BEE /* OCMQuantifier.h */, + 036865671D3572ED005E6BEE /* OCMQuantifier.m */, + ); + name = Quantifier; + sourceTree = ""; + }; + 037ECD5618FB0D2E00AF0E4C /* Helper */ = { + isa = PBXGroup; + children = ( + 03E98D4918F308B400522D42 /* OCMLocation.h */, + 03E98D4A18F308B400522D42 /* OCMLocation.m */, + 2FA28006D043CBDBBAEF6E3F /* OCMMacroState.h */, + 2FA280987F4EA8A4D79000D0 /* OCMMacroState.m */, + ); + name = Helper; + sourceTree = ""; + }; + 03B315841463334E0052CD09 /* Core Mocks */ = { + isa = PBXGroup; + children = ( + 033AB1F924F046C7002014AE /* OCMockMacros.h */, + 03B3159D146333BF0052CD09 /* OCMockObject.h */, + 03B3159E146333BF0052CD09 /* OCMockObject.m */, + 03B3158B146333BF0052CD09 /* OCClassMockObject.h */, + 03B3158C146333BF0052CD09 /* OCClassMockObject.m */, + 03B315A9146333BF0052CD09 /* OCPartialMockObject.h */, + 03B315AA146333BF0052CD09 /* OCPartialMockObject.m */, + 03B315AD146333BF0052CD09 /* OCProtocolMockObject.h */, + 03B315AE146333BF0052CD09 /* OCProtocolMockObject.m */, + 03C7BF14195DAB7900A545DD /* Recorder */, + 03C7BF15195DAB8500A545DD /* Matcher */, + 03B31619146334040052CD09 /* Invocation Actions */, + 03B3161A146334320052CD09 /* Argument Constraints and Actions */, + 036865661D3572C2005E6BEE /* Quantifier */, + 037ECD5618FB0D2E00AF0E4C /* Helper */, + ); + name = "Core Mocks"; + sourceTree = ""; + }; + 03B31618146333CA0052CD09 /* Foundation Additions */ = { + isa = PBXGroup; + children = ( + 03B31585146333BF0052CD09 /* NSInvocation+OCMAdditions.h */, + 03B31586146333BF0052CD09 /* NSInvocation+OCMAdditions.m */, + 03B31587146333BF0052CD09 /* NSMethodSignature+OCMAdditions.h */, + 03B31588146333BF0052CD09 /* NSMethodSignature+OCMAdditions.m */, + 03B31589146333BF0052CD09 /* NSNotificationCenter+OCMAdditions.h */, + 03B3158A146333BF0052CD09 /* NSNotificationCenter+OCMAdditions.m */, + 2FA28C5F4A475BEC0C28BDC3 /* NSObject+OCMAdditions.h */, + 2FA28991BEFD67DEF2CCF7D2 /* NSObject+OCMAdditions.m */, + 2FA286EAC1682979C696D4D6 /* NSValue+OCMAdditions.h */, + 2FA28896E5EEFD7C2F12C2F8 /* NSValue+OCMAdditions.m */, + 2FA28EC49F6C59B940AE6D00 /* OCMFunctions.h */, + 03F370CA1BAA1DE800CAD3E8 /* OCMFunctionsPrivate.h */, + 2FA28C8717BE4B7A89119BA2 /* OCMFunctions.m */, + ); + name = "Foundation Additions"; + sourceTree = ""; + }; + 03B31619146334040052CD09 /* Invocation Actions */ = { + isa = PBXGroup; + children = ( + 03B315A3146333BF0052CD09 /* OCMRealObjectForwarder.h */, + 03B315A4146333BF0052CD09 /* OCMRealObjectForwarder.m */, + 03B3158F146333BF0052CD09 /* OCMBlockCaller.h */, + 03B31590146333BF0052CD09 /* OCMBlockCaller.m */, + 03B31591146333BF0052CD09 /* OCMBoxedReturnValueProvider.h */, + 03B31592146333BF0052CD09 /* OCMBoxedReturnValueProvider.m */, + 03B31595146333BF0052CD09 /* OCMExceptionReturnValueProvider.h */, + 03B31596146333BF0052CD09 /* OCMExceptionReturnValueProvider.m */, + 03B31597146333BF0052CD09 /* OCMIndirectReturnValueProvider.h */, + 03B31598146333BF0052CD09 /* OCMIndirectReturnValueProvider.m */, + 03B31599146333BF0052CD09 /* OCMNotificationPoster.h */, + 03B3159A146333BF0052CD09 /* OCMNotificationPoster.m */, + 03B315A5146333BF0052CD09 /* OCMObjectReturnValueProvider.h */, + 03B315A6146333BF0052CD09 /* OCMObjectReturnValueProvider.m */, + 2FA28B1133B294089D2245AC /* OCMNonRetainingObjectReturnValueProvider.h */, + 2FA280EB5E8CDEEAE76861F7 /* OCMNonRetainingObjectReturnValueProvider.m */, + ); + name = "Invocation Actions"; + sourceTree = ""; + }; + 03B3161A146334320052CD09 /* Argument Constraints and Actions */ = { + isa = PBXGroup; + children = ( + 03B3158D146333BF0052CD09 /* OCMArg.h */, + 03B3158E146333BF0052CD09 /* OCMArg.m */, + 03B31593146333BF0052CD09 /* OCMConstraint.h */, + 03B31594146333BF0052CD09 /* OCMConstraint.m */, + 2FA2833B48908EAD36444671 /* OCMArgAction.h */, + 2FA28A7898E1046DE957A035 /* OCMArgAction.m */, + 03B315A1146333BF0052CD09 /* OCMPassByRefSetter.h */, + 03B315A2146333BF0052CD09 /* OCMPassByRefSetter.m */, + 2FA2891034E7B73AA3511D17 /* OCMBlockArgCaller.h */, + 2FA283D58AA7569D8A5B0C57 /* OCMBlockArgCaller.m */, + ); + name = "Argument Constraints and Actions"; + sourceTree = ""; + }; + 03B3161B146334530052CD09 /* Observer Mocks */ = { + isa = PBXGroup; + children = ( + 03B315A7146333BF0052CD09 /* OCObserverMockObject.h */, + 03B315A8146333BF0052CD09 /* OCObserverMockObject.m */, + 03B3159B146333BF0052CD09 /* OCMObserverRecorder.h */, + 03B3159C146333BF0052CD09 /* OCMObserverRecorder.m */, + ); + name = "Observer Mocks"; + sourceTree = ""; + }; + 03C7BF14195DAB7900A545DD /* Recorder */ = { + isa = PBXGroup; + children = ( + 03618D81195B553400389166 /* OCMRecorder.h */, + 03618D82195B553400389166 /* OCMRecorder.m */, + 03B3159F146333BF0052CD09 /* OCMStubRecorder.h */, + 03B315A0146333BF0052CD09 /* OCMStubRecorder.m */, + 03C7BF0E195DAB5300A545DD /* OCMExpectationRecorder.h */, + 03C7BF0F195DAB5300A545DD /* OCMExpectationRecorder.m */, + 0322DA6719118B4600CACAF1 /* OCMVerifier.h */, + 0322DA6819118B4600CACAF1 /* OCMVerifier.m */, + ); + name = Recorder; + sourceTree = ""; + }; + 03C7BF15195DAB8500A545DD /* Matcher */ = { + isa = PBXGroup; + children = ( + 2FA28CCC33B99D6B658E7AF8 /* OCMInvocationMatcher.h */, + 2FA28CB1EB14C6B2BE9A20C6 /* OCMInvocationMatcher.m */, + 03C7BF08195DA2F200A545DD /* OCMInvocationStub.h */, + 03C7BF09195DA2F200A545DD /* OCMInvocationStub.m */, + 2FA288509D545BDBE094BCC8 /* OCMInvocationExpectation.h */, + 2FA2875564A6AD0510A847A6 /* OCMInvocationExpectation.m */, + ); + name = Matcher; + sourceTree = ""; + }; + D31108B51828DB8700737925 /* OCMockLibTests */ = { + isa = PBXGroup; + children = ( + D31108B61828DB8700737925 /* Supporting Files */, + ); + path = OCMockLibTests; + sourceTree = ""; + }; + D31108B61828DB8700737925 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + D31108B71828DB8700737925 /* OCMockLibTests-Info.plist */, + D31108BD1828DB8700737925 /* OCMockLibTests-Prefix.pch */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 030EF0A514632FD000B04273 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 03B3168B14633A4F0052CD09 /* OCMock.h in Headers */, + 03B315EB146333C00052CD09 /* OCMockObject.h in Headers */, + 03B315F0146333C00052CD09 /* OCMStubRecorder.h in Headers */, + 03B315C3146333BF0052CD09 /* OCMArg.h in Headers */, + 03B315D2146333BF0052CD09 /* OCMConstraint.h in Headers */, + 03618D83195B553400389166 /* OCMRecorder.h in Headers */, + 03B315B9146333BF0052CD09 /* NSNotificationCenter+OCMAdditions.h in Headers */, + 03B315AF146333BF0052CD09 /* NSInvocation+OCMAdditions.h in Headers */, + 03B315B4146333BF0052CD09 /* NSMethodSignature+OCMAdditions.h in Headers */, + 03C7BF0A195DA2F200A545DD /* OCMInvocationStub.h in Headers */, + 03B315BE146333BF0052CD09 /* OCClassMockObject.h in Headers */, + 03B315C8146333BF0052CD09 /* OCMBlockCaller.h in Headers */, + 03C7BF10195DAB5300A545DD /* OCMExpectationRecorder.h in Headers */, + 03B315CD146333BF0052CD09 /* OCMBoxedReturnValueProvider.h in Headers */, + 03B315D7146333BF0052CD09 /* OCMExceptionReturnValueProvider.h in Headers */, + 03B315DC146333BF0052CD09 /* OCMIndirectReturnValueProvider.h in Headers */, + 03E98D4B18F308B400522D42 /* OCMLocation.h in Headers */, + 03B315E1146333BF0052CD09 /* OCMNotificationPoster.h in Headers */, + 03B315E6146333BF0052CD09 /* OCMObserverRecorder.h in Headers */, + 03B315F5146333C00052CD09 /* OCMPassByRefSetter.h in Headers */, + 03B315FA146333C00052CD09 /* OCMRealObjectForwarder.h in Headers */, + 03B315FF146333C00052CD09 /* OCMObjectReturnValueProvider.h in Headers */, + 03B31604146333C00052CD09 /* OCObserverMockObject.h in Headers */, + 03B31609146333C00052CD09 /* OCPartialMockObject.h in Headers */, + 0368656D1D357317005E6BEE /* OCMQuantifier.h in Headers */, + 3C0FF06A1BAA3FD10021AD20 /* OCMFunctionsPrivate.h in Headers */, + 03B31613146333C00052CD09 /* OCProtocolMockObject.h in Headers */, + 2FA287ACE547BB41937BDEC3 /* NSObject+OCMAdditions.h in Headers */, + 2FA28641AAD0AC2F876C9E48 /* OCMInvocationMatcher.h in Headers */, + 0322DA6919118B4600CACAF1 /* OCMVerifier.h in Headers */, + 2FA288447C48241BE2CAA63D /* OCMFunctions.h in Headers */, + 2FA28FE116284C3E4A7FF179 /* OCMInvocationExpectation.h in Headers */, + 2FA2821C9066C5E439288C76 /* OCMMacroState.h in Headers */, + 2FA28F090622C9DF868CCA8C /* NSValue+OCMAdditions.h in Headers */, + 2FA28DC9A2D732666124D640 /* OCMBlockArgCaller.h in Headers */, + 033AB1FA24F046C7002014AE /* OCMockMacros.h in Headers */, + 2FA28BCF08C1CBC133E8E84E /* OCMArgAction.h in Headers */, + 2FA28C1FE2E8E2C114E3FA54 /* OCMNonRetainingObjectReturnValueProvider.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 030EF0DA14632FF700B04273 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 03B3168E14633D9C0052CD09 /* OCMock.h in Headers */, + 03B315EC146333C00052CD09 /* OCMockObject.h in Headers */, + 03B315F1146333C00052CD09 /* OCMStubRecorder.h in Headers */, + 03B315C4146333BF0052CD09 /* OCMArg.h in Headers */, + 03B315D3146333BF0052CD09 /* OCMConstraint.h in Headers */, + 03618D84195B553400389166 /* OCMRecorder.h in Headers */, + 03B315BA146333BF0052CD09 /* NSNotificationCenter+OCMAdditions.h in Headers */, + 03B315B0146333BF0052CD09 /* NSInvocation+OCMAdditions.h in Headers */, + 03B315B5146333BF0052CD09 /* NSMethodSignature+OCMAdditions.h in Headers */, + 03C7BF0B195DA2F200A545DD /* OCMInvocationStub.h in Headers */, + 03B315BF146333BF0052CD09 /* OCClassMockObject.h in Headers */, + 03B315C9146333BF0052CD09 /* OCMBlockCaller.h in Headers */, + 033AB1FB24F046C7002014AE /* OCMockMacros.h in Headers */, + 03E0FAD81B93C00B000C5096 /* OCMBlockArgCaller.h in Headers */, + 03C7BF11195DAB5300A545DD /* OCMExpectationRecorder.h in Headers */, + 03B315CE146333BF0052CD09 /* OCMBoxedReturnValueProvider.h in Headers */, + 03B315D8146333BF0052CD09 /* OCMExceptionReturnValueProvider.h in Headers */, + 03B315DD146333BF0052CD09 /* OCMIndirectReturnValueProvider.h in Headers */, + 03E98D4C18F308B400522D42 /* OCMLocation.h in Headers */, + 03B315E2146333BF0052CD09 /* OCMNotificationPoster.h in Headers */, + 03B315E7146333BF0052CD09 /* OCMObserverRecorder.h in Headers */, + 03B315F6146333C00052CD09 /* OCMPassByRefSetter.h in Headers */, + 03B315FB146333C00052CD09 /* OCMRealObjectForwarder.h in Headers */, + 03DCED6F183406DA0059089E /* NSObject+OCMAdditions.h in Headers */, + 0368656E1D357318005E6BEE /* OCMQuantifier.h in Headers */, + 817EB1661BD7674D0047E85A /* OCMFunctionsPrivate.h in Headers */, + 03B31605146333C00052CD09 /* OCObserverMockObject.h in Headers */, + 03B3160A146333C00052CD09 /* OCPartialMockObject.h in Headers */, + 03B31614146333C00052CD09 /* OCProtocolMockObject.h in Headers */, + 2FA28E1EB6B8536785258DF5 /* OCMInvocationMatcher.h in Headers */, + 0322DA6A19118B4600CACAF1 /* OCMVerifier.h in Headers */, + 2FA28598D23EC190EE19F3AE /* OCMFunctions.h in Headers */, + 2FA284EBCD82935B4F8A5A2C /* OCMInvocationExpectation.h in Headers */, + 2FA28B6CEF4CC6FD5E7D09B5 /* OCMMacroState.h in Headers */, + 2FA283C39309951ED4DA2969 /* NSValue+OCMAdditions.h in Headers */, + 2FA2816186A83E8CDBC3A2E2 /* OCMArgAction.h in Headers */, + 2FA28F7D0718648C36686617 /* OCMNonRetainingObjectReturnValueProvider.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 817EB13A1BD765130047E85A /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 817EB13B1BD765130047E85A /* OCMock.h in Headers */, + 817EB13C1BD765130047E85A /* OCMockObject.h in Headers */, + 817EB13D1BD765130047E85A /* OCMStubRecorder.h in Headers */, + 817EB13E1BD765130047E85A /* OCMArg.h in Headers */, + 817EB13F1BD765130047E85A /* OCMConstraint.h in Headers */, + 817EB1401BD765130047E85A /* OCMRecorder.h in Headers */, + 817EB1411BD765130047E85A /* NSNotificationCenter+OCMAdditions.h in Headers */, + 817EB1421BD765130047E85A /* OCMLocation.h in Headers */, + 817EB1431BD765130047E85A /* OCMMacroState.h in Headers */, + 817EB1441BD765130047E85A /* OCClassMockObject.h in Headers */, + 817EB1451BD765130047E85A /* OCPartialMockObject.h in Headers */, + 817EB1461BD765130047E85A /* OCProtocolMockObject.h in Headers */, + 817EB1471BD765130047E85A /* OCMExpectationRecorder.h in Headers */, + 817EB1481BD765130047E85A /* OCMVerifier.h in Headers */, + 817EB1491BD765130047E85A /* OCMInvocationMatcher.h in Headers */, + 817EB14A1BD765130047E85A /* OCMInvocationStub.h in Headers */, + 817EB14B1BD765130047E85A /* OCMInvocationExpectation.h in Headers */, + 817EB14C1BD765130047E85A /* OCMRealObjectForwarder.h in Headers */, + 817EB14D1BD765130047E85A /* OCMBlockCaller.h in Headers */, + 817EB14E1BD765130047E85A /* OCMBoxedReturnValueProvider.h in Headers */, + 817EB14F1BD765130047E85A /* OCMExceptionReturnValueProvider.h in Headers */, + 817EB1501BD765130047E85A /* OCMIndirectReturnValueProvider.h in Headers */, + 817EB1511BD765130047E85A /* OCMNotificationPoster.h in Headers */, + 817EB1521BD765130047E85A /* OCMObjectReturnValueProvider.h in Headers */, + 036865701D35731A005E6BEE /* OCMQuantifier.h in Headers */, + 817EB1531BD765130047E85A /* OCMFunctionsPrivate.h in Headers */, + 817EB1541BD765130047E85A /* OCObserverMockObject.h in Headers */, + 817EB1551BD765130047E85A /* OCMObserverRecorder.h in Headers */, + 817EB1561BD765130047E85A /* OCMPassByRefSetter.h in Headers */, + 817EB1571BD765130047E85A /* NSInvocation+OCMAdditions.h in Headers */, + 817EB1581BD765130047E85A /* NSMethodSignature+OCMAdditions.h in Headers */, + 817EB1591BD765130047E85A /* NSObject+OCMAdditions.h in Headers */, + 817EB15A1BD765130047E85A /* NSValue+OCMAdditions.h in Headers */, + 817EB15B1BD765130047E85A /* OCMFunctions.h in Headers */, + 817EB15C1BD765130047E85A /* OCMBlockArgCaller.h in Headers */, + 033AB1FD24F046C7002014AE /* OCMockMacros.h in Headers */, + 817EB15D1BD765130047E85A /* OCMArgAction.h in Headers */, + 2FA28806443827E286F12F6F /* OCMNonRetainingObjectReturnValueProvider.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8DE97C7822B43EE60098C63F /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 8DE97C7922B43EE60098C63F /* OCMock.h in Headers */, + 8DE97C7A22B43EE60098C63F /* OCMockObject.h in Headers */, + 8DE97C7B22B43EE60098C63F /* OCMStubRecorder.h in Headers */, + 8DE97C7C22B43EE60098C63F /* OCMArg.h in Headers */, + 8DE97C7D22B43EE60098C63F /* OCMConstraint.h in Headers */, + 8DE97C7E22B43EE60098C63F /* OCMRecorder.h in Headers */, + 8DE97C7F22B43EE60098C63F /* NSNotificationCenter+OCMAdditions.h in Headers */, + 8DE97C8022B43EE60098C63F /* OCMLocation.h in Headers */, + 8DE97C8122B43EE60098C63F /* OCMMacroState.h in Headers */, + 8DE97C8222B43EE60098C63F /* OCClassMockObject.h in Headers */, + 8DE97C8322B43EE60098C63F /* OCPartialMockObject.h in Headers */, + 8DE97C8422B43EE60098C63F /* OCProtocolMockObject.h in Headers */, + 8DE97C8522B43EE60098C63F /* OCMExpectationRecorder.h in Headers */, + 8DE97C8622B43EE60098C63F /* OCMVerifier.h in Headers */, + 8DE97C8722B43EE60098C63F /* OCMInvocationMatcher.h in Headers */, + 8DE97C8822B43EE60098C63F /* OCMInvocationStub.h in Headers */, + 8DE97C8922B43EE60098C63F /* OCMInvocationExpectation.h in Headers */, + 8DE97C8A22B43EE60098C63F /* OCMRealObjectForwarder.h in Headers */, + 8DE97C8B22B43EE60098C63F /* OCMBlockCaller.h in Headers */, + 8DE97C8C22B43EE60098C63F /* OCMBoxedReturnValueProvider.h in Headers */, + 8DE97C8D22B43EE60098C63F /* OCMExceptionReturnValueProvider.h in Headers */, + 8DE97C8E22B43EE60098C63F /* OCMIndirectReturnValueProvider.h in Headers */, + 8DE97C8F22B43EE60098C63F /* OCMNotificationPoster.h in Headers */, + 8DE97C9022B43EE60098C63F /* OCMObjectReturnValueProvider.h in Headers */, + 8DE97C9122B43EE60098C63F /* OCMFunctionsPrivate.h in Headers */, + 8DE97C9222B43EE60098C63F /* OCObserverMockObject.h in Headers */, + 8DE97C9322B43EE60098C63F /* OCMObserverRecorder.h in Headers */, + 8DE97C9422B43EE60098C63F /* OCMPassByRefSetter.h in Headers */, + 8DE97C9522B43EE60098C63F /* NSInvocation+OCMAdditions.h in Headers */, + 8DE97C9622B43EE60098C63F /* NSMethodSignature+OCMAdditions.h in Headers */, + 8DE97C9722B43EE60098C63F /* NSObject+OCMAdditions.h in Headers */, + 03A1CC9E23F89F36005ADA04 /* OCMQuantifier.h in Headers */, + 8DE97C9822B43EE60098C63F /* NSValue+OCMAdditions.h in Headers */, + 8DE97C9922B43EE60098C63F /* OCMFunctions.h in Headers */, + 8DE97C9A22B43EE60098C63F /* OCMBlockArgCaller.h in Headers */, + 033AB1FE24F046C7002014AE /* OCMockMacros.h in Headers */, + 8DE97C9B22B43EE60098C63F /* OCMArgAction.h in Headers */, + 2FA2883A0BD4BF6A03AAB49C /* OCMNonRetainingObjectReturnValueProvider.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F0B950EE1B0080BE00942C38 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + F0B9512B1B00810C00942C38 /* OCMock.h in Headers */, + F0B9512C1B00810C00942C38 /* OCMockObject.h in Headers */, + F0B951311B00810C00942C38 /* OCMStubRecorder.h in Headers */, + F0B951421B00810C00942C38 /* OCMArg.h in Headers */, + F0B951431B00810C00942C38 /* OCMConstraint.h in Headers */, + F0B951301B00810C00942C38 /* OCMRecorder.h in Headers */, + F0B951471B00810C00942C38 /* NSNotificationCenter+OCMAdditions.h in Headers */, + F0B9513E1B00810C00942C38 /* OCMLocation.h in Headers */, + F0B9513F1B00810C00942C38 /* OCMMacroState.h in Headers */, + F0B9512D1B00810C00942C38 /* OCClassMockObject.h in Headers */, + F0B9512E1B00810C00942C38 /* OCPartialMockObject.h in Headers */, + F0B9512F1B00810C00942C38 /* OCProtocolMockObject.h in Headers */, + F0B951321B00810C00942C38 /* OCMExpectationRecorder.h in Headers */, + F0B951331B00810C00942C38 /* OCMVerifier.h in Headers */, + F0B951341B00810C00942C38 /* OCMInvocationMatcher.h in Headers */, + F0B951351B00810C00942C38 /* OCMInvocationStub.h in Headers */, + F0B951361B00810C00942C38 /* OCMInvocationExpectation.h in Headers */, + F0B951371B00810C00942C38 /* OCMRealObjectForwarder.h in Headers */, + F0B951381B00810C00942C38 /* OCMBlockCaller.h in Headers */, + F0B951391B00810C00942C38 /* OCMBoxedReturnValueProvider.h in Headers */, + F0B9513A1B00810C00942C38 /* OCMExceptionReturnValueProvider.h in Headers */, + F0B9513B1B00810C00942C38 /* OCMIndirectReturnValueProvider.h in Headers */, + F0B9513C1B00810C00942C38 /* OCMNotificationPoster.h in Headers */, + F0B9513D1B00810C00942C38 /* OCMObjectReturnValueProvider.h in Headers */, + 0368656F1D357319005E6BEE /* OCMQuantifier.h in Headers */, + 3C0FF06B1BAA3FD20021AD20 /* OCMFunctionsPrivate.h in Headers */, + F0B951401B00810C00942C38 /* OCObserverMockObject.h in Headers */, + F0B951411B00810C00942C38 /* OCMObserverRecorder.h in Headers */, + F0B951441B00810C00942C38 /* OCMPassByRefSetter.h in Headers */, + F0B951451B00810C00942C38 /* NSInvocation+OCMAdditions.h in Headers */, + F0B951461B00810C00942C38 /* NSMethodSignature+OCMAdditions.h in Headers */, + F0B951481B00810C00942C38 /* NSObject+OCMAdditions.h in Headers */, + F0B951491B00810C00942C38 /* NSValue+OCMAdditions.h in Headers */, + F0B9514A1B00810C00942C38 /* OCMFunctions.h in Headers */, + 2FA28B7BDB3319A499E90525 /* OCMBlockArgCaller.h in Headers */, + 033AB1FC24F046C7002014AE /* OCMockMacros.h in Headers */, + 2FA280E60213BA09F007C173 /* OCMArgAction.h in Headers */, + 2FA28AFBD67EAB9DD1F23BF5 /* OCMNonRetainingObjectReturnValueProvider.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 030EF0A714632FD000B04273 /* OCMock */ = { + isa = PBXNativeTarget; + buildConfigurationList = 030EF0D214632FD000B04273 /* Build configuration list for PBXNativeTarget "OCMock" */; + buildPhases = ( + 030EF0A314632FD000B04273 /* Sources */, + 030EF0A414632FD000B04273 /* Frameworks */, + 030EF0A514632FD000B04273 /* Headers */, + 030EF0A614632FD000B04273 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = OCMock; + productName = OCMock; + productReference = 030EF0A814632FD000B04273 /* OCMock.framework */; + productType = "com.apple.product-type.framework"; + }; + 030EF0DB14632FF700B04273 /* OCMockLib */ = { + isa = PBXNativeTarget; + buildConfigurationList = 030EF0E514632FF700B04273 /* Build configuration list for PBXNativeTarget "OCMockLib" */; + buildPhases = ( + 030EF0D814632FF700B04273 /* Sources */, + 030EF0D914632FF700B04273 /* Frameworks */, + 030EF0DA14632FF700B04273 /* Headers */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = OCMockLib; + productName = OCMockLib; + productReference = 030EF0DC14632FF700B04273 /* libOCMock.a */; + productType = "com.apple.product-type.library.static"; + }; + 03565A3018F0566E003AE91E /* OCMockTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 03565A3E18F0566F003AE91E /* Build configuration list for PBXNativeTarget "OCMockTests" */; + buildPhases = ( + 03565A4F18F058A0003AE91E /* ShellScript */, + 0397296423907547000E1C45 /* CopyFiles */, + 03565A2D18F0566E003AE91E /* Sources */, + 03565A2E18F0566E003AE91E /* Frameworks */, + 03565A2F18F0566E003AE91E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 03565A3D18F0566F003AE91E /* PBXTargetDependency */, + ); + name = OCMockTests; + productName = OCMockTests; + productReference = 03565A3118F0566E003AE91E /* OCMockTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 817EB1151BD765130047E85A /* OCMock tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 817EB15F1BD765130047E85A /* Build configuration list for PBXNativeTarget "OCMock tvOS" */; + buildPhases = ( + 817EB1161BD765130047E85A /* Sources */, + 817EB1381BD765130047E85A /* Frameworks */, + 817EB13A1BD765130047E85A /* Headers */, + 817EB15E1BD765130047E85A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "OCMock tvOS"; + productName = "OCMock iOS"; + productReference = 817EB1621BD765130047E85A /* OCMock.framework */; + productType = "com.apple.product-type.framework"; + }; + 8DE97C5322B43EE60098C63F /* OCMock watchOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8DE97C9D22B43EE60098C63F /* Build configuration list for PBXNativeTarget "OCMock watchOS" */; + buildPhases = ( + 8DE97C5422B43EE60098C63F /* Sources */, + 8DE97C7622B43EE60098C63F /* Frameworks */, + 8DE97C7822B43EE60098C63F /* Headers */, + 8DE97C9C22B43EE60098C63F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "OCMock watchOS"; + productName = "OCMock iOS"; + productReference = 8DE97CA022B43EE60098C63F /* OCMock.framework */; + productType = "com.apple.product-type.framework"; + }; + D31108AC1828DB8700737925 /* OCMockLibTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = D31108C01828DB8700737925 /* Build configuration list for PBXNativeTarget "OCMockLibTests" */; + buildPhases = ( + D31108A91828DB8700737925 /* Sources */, + D31108AA1828DB8700737925 /* Frameworks */, + D31108AB1828DB8700737925 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D31108BF1828DB8700737925 /* PBXTargetDependency */, + ); + name = OCMockLibTests; + productName = OCMockLibTests; + productReference = D31108AD1828DB8700737925 /* OCMockLibTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + F0B950F01B0080BE00942C38 /* OCMock iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = F0B951041B0080BE00942C38 /* Build configuration list for PBXNativeTarget "OCMock iOS" */; + buildPhases = ( + F0B950EC1B0080BE00942C38 /* Sources */, + F0B950ED1B0080BE00942C38 /* Frameworks */, + F0B950EE1B0080BE00942C38 /* Headers */, + F0B950EF1B0080BE00942C38 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "OCMock iOS"; + productName = "OCMock iOS"; + productReference = F0B950F11B0080BE00942C38 /* OCMock.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 030EF09E14632FD000B04273 /* Project object */ = { + isa = PBXProject; + attributes = { + LastTestingUpgradeCheck = 0510; + LastUpgradeCheck = 1220; + ORGANIZATIONNAME = "Mulle Kybernetik"; + TargetAttributes = { + 03565A3018F0566E003AE91E = { + TestTargetID = 030EF0A714632FD000B04273; + }; + D31108AC1828DB8700737925 = { + TestTargetID = 030EF0DB14632FF700B04273; + }; + F0B950F01B0080BE00942C38 = { + CreatedOnToolsVersion = 6.3.1; + }; + }; + }; + buildConfigurationList = 030EF0A114632FD000B04273 /* Build configuration list for PBXProject "OCMock" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 030EF09C14632FD000B04273; + productRefGroup = 030EF0A914632FD000B04273 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 030EF0A714632FD000B04273 /* OCMock */, + 03565A3018F0566E003AE91E /* OCMockTests */, + 030EF0DB14632FF700B04273 /* OCMockLib */, + D31108AC1828DB8700737925 /* OCMockLibTests */, + F0B950F01B0080BE00942C38 /* OCMock iOS */, + 817EB1151BD765130047E85A /* OCMock tvOS */, + 8DE97C5322B43EE60098C63F /* OCMock watchOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 030EF0A614632FD000B04273 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 03565A2F18F0566E003AE91E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A02926821CA0725A00594AAF /* TestObjects.xcdatamodeld in Resources */, + 03CED2E22390770C001845CC /* OCHamcrest.framework.dSYM in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 817EB15E1BD765130047E85A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8DE97C9C22B43EE60098C63F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D31108AB1828DB8700737925 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F0B950EF1B0080BE00942C38 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 03565A4F18F058A0003AE91E /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Resolve dependencies using Carthage\nif [ ! -d ${PROJECT_DIR}/Carthage/Build ]; then\n carthage update --platform macOS\nfi\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 030EF0A314632FD000B04273 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 03B315B1146333BF0052CD09 /* NSInvocation+OCMAdditions.m in Sources */, + 03B315B6146333BF0052CD09 /* NSMethodSignature+OCMAdditions.m in Sources */, + 03618D85195B553400389166 /* OCMRecorder.m in Sources */, + 03C7BF0C195DA2F200A545DD /* OCMInvocationStub.m in Sources */, + 03B315BB146333BF0052CD09 /* NSNotificationCenter+OCMAdditions.m in Sources */, + 0322DA6B19118B4600CACAF1 /* OCMVerifier.m in Sources */, + 03B315C0146333BF0052CD09 /* OCClassMockObject.m in Sources */, + 03B315C5146333BF0052CD09 /* OCMArg.m in Sources */, + 03B315CA146333BF0052CD09 /* OCMBlockCaller.m in Sources */, + 036865681D3572ED005E6BEE /* OCMQuantifier.m in Sources */, + 03B315CF146333BF0052CD09 /* OCMBoxedReturnValueProvider.m in Sources */, + 03B315D4146333BF0052CD09 /* OCMConstraint.m in Sources */, + 03B315D9146333BF0052CD09 /* OCMExceptionReturnValueProvider.m in Sources */, + 03B315DE146333BF0052CD09 /* OCMIndirectReturnValueProvider.m in Sources */, + 03B315E3146333BF0052CD09 /* OCMNotificationPoster.m in Sources */, + 03B315E8146333BF0052CD09 /* OCMObserverRecorder.m in Sources */, + 03B315ED146333C00052CD09 /* OCMockObject.m in Sources */, + 03B315F2146333C00052CD09 /* OCMStubRecorder.m in Sources */, + 03B315F7146333C00052CD09 /* OCMPassByRefSetter.m in Sources */, + 03B315FC146333C00052CD09 /* OCMRealObjectForwarder.m in Sources */, + 03E98D4D18F308B400522D42 /* OCMLocation.m in Sources */, + 03B31601146333C00052CD09 /* OCMObjectReturnValueProvider.m in Sources */, + 03B31606146333C00052CD09 /* OCObserverMockObject.m in Sources */, + 03C7BF12195DAB5300A545DD /* OCMExpectationRecorder.m in Sources */, + 03B3160B146333C00052CD09 /* OCPartialMockObject.m in Sources */, + 03B31615146333C00052CD09 /* OCProtocolMockObject.m in Sources */, + 2FA28D38ED9D83A3E0A620B3 /* NSObject+OCMAdditions.m in Sources */, + 2FA282F0C0A4CABE4E2C7646 /* OCMInvocationMatcher.m in Sources */, + 2FA28BA90F0D55E4CF6787F0 /* OCMFunctions.m in Sources */, + 2FA286C7B8862280E6C1A585 /* OCMInvocationExpectation.m in Sources */, + 2FA2800D12E95D5ABC965753 /* OCMMacroState.m in Sources */, + 2FA2805AB1944A206EEE383B /* NSValue+OCMAdditions.m in Sources */, + 2FA28F1FA69D313B4040896E /* OCMBlockArgCaller.m in Sources */, + 2FA28D9C8D2141B003D39E7F /* OCMArgAction.m in Sources */, + 2FA28C42D466C7002E6F9B24 /* OCMNonRetainingObjectReturnValueProvider.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 030EF0D814632FF700B04273 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 03B315B3146333BF0052CD09 /* NSInvocation+OCMAdditions.m in Sources */, + 03B315B8146333BF0052CD09 /* NSMethodSignature+OCMAdditions.m in Sources */, + 03618D86195B553400389166 /* OCMRecorder.m in Sources */, + 03C7BF0D195DA2F200A545DD /* OCMInvocationStub.m in Sources */, + 03B315BD146333BF0052CD09 /* NSNotificationCenter+OCMAdditions.m in Sources */, + 0322DA6C19118B4600CACAF1 /* OCMVerifier.m in Sources */, + 03B315C2146333BF0052CD09 /* OCClassMockObject.m in Sources */, + 03B315C7146333BF0052CD09 /* OCMArg.m in Sources */, + 03B315CC146333BF0052CD09 /* OCMBlockCaller.m in Sources */, + 036865691D3572ED005E6BEE /* OCMQuantifier.m in Sources */, + 03B315D1146333BF0052CD09 /* OCMBoxedReturnValueProvider.m in Sources */, + 03DCED6D183406BC0059089E /* NSObject+OCMAdditions.m in Sources */, + 03B315D6146333BF0052CD09 /* OCMConstraint.m in Sources */, + 03B315DB146333BF0052CD09 /* OCMExceptionReturnValueProvider.m in Sources */, + 03B315E0146333BF0052CD09 /* OCMIndirectReturnValueProvider.m in Sources */, + 03B315E5146333BF0052CD09 /* OCMNotificationPoster.m in Sources */, + 03B315EA146333BF0052CD09 /* OCMObserverRecorder.m in Sources */, + 03B315EF146333C00052CD09 /* OCMockObject.m in Sources */, + 03B315F4146333C00052CD09 /* OCMStubRecorder.m in Sources */, + 03B315F9146333C00052CD09 /* OCMPassByRefSetter.m in Sources */, + 03E98D4E18F308B400522D42 /* OCMLocation.m in Sources */, + 03B315FE146333C00052CD09 /* OCMRealObjectForwarder.m in Sources */, + 03B31603146333C00052CD09 /* OCMObjectReturnValueProvider.m in Sources */, + 03C7BF13195DAB5300A545DD /* OCMExpectationRecorder.m in Sources */, + 03B31608146333C00052CD09 /* OCObserverMockObject.m in Sources */, + 03B3160D146333C00052CD09 /* OCPartialMockObject.m in Sources */, + 03B31617146333C00052CD09 /* OCProtocolMockObject.m in Sources */, + 2FA288B20FB3A42BA0B7BAB8 /* OCMInvocationMatcher.m in Sources */, + 2FA286F90395E317F983A20A /* OCMFunctions.m in Sources */, + 2FA28E506BE0D91A95A20243 /* OCMInvocationExpectation.m in Sources */, + 2FA28209EB1F41964DF3031A /* OCMMacroState.m in Sources */, + 2FA28FEAEF9333D2C214DF53 /* NSValue+OCMAdditions.m in Sources */, + 2FA28405211B752287972F42 /* OCMBlockArgCaller.m in Sources */, + 2FA282C6A3FFB4AD9061CB08 /* OCMArgAction.m in Sources */, + 2FA28184FE3B28D87DF3DE44 /* OCMNonRetainingObjectReturnValueProvider.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 03565A2D18F0566E003AE91E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 037ECD5418FAD84100AF0E4C /* OCMInvocationMatcherTests.m in Sources */, + 03565A4418F05721003AE91E /* OCMockObjectProtocolMocksTests.m in Sources */, + 03565A4B18F05721003AE91E /* NSInvocationOCMAdditionsTests.m in Sources */, + 03565A4618F05721003AE91E /* OCMockObjectHamcrestTests.m in Sources */, + 3CFBDD771BB3DB200050D9C5 /* TestClassWithCustomReferenceCounting.m in Sources */, + 03E98D5018F310EE00522D42 /* OCMockObjectMacroTests.m in Sources */, + 03565A4A18F05721003AE91E /* OCObserverMockObjectTests.m in Sources */, + 03565A4318F05721003AE91E /* OCMockObjectClassMethodMockingTests.m in Sources */, + 03565A4918F05721003AE91E /* OCMArgTests.m in Sources */, + 036865641D3571A8005E6BEE /* OCMQuantifierTests.m in Sources */, + 0322DA65191188D100CACAF1 /* OCMockObjectVerifyAfterRunTests.m in Sources */, + 03565A4718F05721003AE91E /* OCMConstraintTests.m in Sources */, + 03565A4218F05721003AE91E /* OCMockObjectPartialMocksTests.m in Sources */, + 03565A4C18F05721003AE91E /* NSMethodSignatureOCMAdditionsTests.m in Sources */, + 03565A4818F05721003AE91E /* OCMStubRecorderTests.m in Sources */, + 03565A4518F05721003AE91E /* OCMockObjectForwardingTargetTests.m in Sources */, + 2FA28FA53C57236B6DD64E82 /* OCMockObjectRuntimeTests.m in Sources */, + 8B11D4BA2448E53600247BE2 /* OCMCPlusPlus11Tests.mm in Sources */, + 2FA2839F33289795284C32FB /* OCMockObjectTests.m in Sources */, + 038599F723807B06002B3ABE /* OCMockObjectInternalTests.m in Sources */, + 8BF73E53246CA75E00B9A52C /* OCMNoEscapeBlockTests.m in Sources */, + 8B11D4B72448E2E900247BE2 /* OCMCPlusPlus98Tests.mm in Sources */, + 2FA28AB33F01A7D980F2C705 /* OCMockObjectDynamicPropertyMockingTests.m in Sources */, + 031E50581BB4A56300E257C3 /* OCMBoxedReturnValueProviderTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 817EB1161BD765130047E85A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 817EB1171BD765130047E85A /* OCMockObject.m in Sources */, + 817EB1181BD765130047E85A /* OCClassMockObject.m in Sources */, + 817EB1191BD765130047E85A /* OCPartialMockObject.m in Sources */, + 817EB11A1BD765130047E85A /* OCProtocolMockObject.m in Sources */, + 817EB11B1BD765130047E85A /* OCMRecorder.m in Sources */, + 817EB11C1BD765130047E85A /* OCMStubRecorder.m in Sources */, + 817EB11D1BD765130047E85A /* OCMExpectationRecorder.m in Sources */, + 817EB11E1BD765130047E85A /* OCMVerifier.m in Sources */, + 817EB11F1BD765130047E85A /* OCMInvocationMatcher.m in Sources */, + 0368656B1D3572ED005E6BEE /* OCMQuantifier.m in Sources */, + 817EB1201BD765130047E85A /* OCMInvocationStub.m in Sources */, + 817EB1211BD765130047E85A /* OCMInvocationExpectation.m in Sources */, + 817EB1221BD765130047E85A /* OCMRealObjectForwarder.m in Sources */, + 817EB1231BD765130047E85A /* OCMBlockCaller.m in Sources */, + 817EB1241BD765130047E85A /* OCMBoxedReturnValueProvider.m in Sources */, + 817EB1251BD765130047E85A /* OCMExceptionReturnValueProvider.m in Sources */, + 817EB1261BD765130047E85A /* OCMIndirectReturnValueProvider.m in Sources */, + 817EB1271BD765130047E85A /* OCMNotificationPoster.m in Sources */, + 817EB1281BD765130047E85A /* OCMObjectReturnValueProvider.m in Sources */, + 817EB1291BD765130047E85A /* OCMLocation.m in Sources */, + 817EB12A1BD765130047E85A /* OCMMacroState.m in Sources */, + 817EB12B1BD765130047E85A /* OCMBlockArgCaller.m in Sources */, + 817EB12C1BD765130047E85A /* OCObserverMockObject.m in Sources */, + 817EB12D1BD765130047E85A /* OCMObserverRecorder.m in Sources */, + 817EB12E1BD765130047E85A /* OCMArg.m in Sources */, + 817EB12F1BD765130047E85A /* OCMConstraint.m in Sources */, + 817EB1301BD765130047E85A /* OCMPassByRefSetter.m in Sources */, + 817EB1311BD765130047E85A /* NSInvocation+OCMAdditions.m in Sources */, + 817EB1321BD765130047E85A /* NSMethodSignature+OCMAdditions.m in Sources */, + 817EB1331BD765130047E85A /* NSNotificationCenter+OCMAdditions.m in Sources */, + 817EB1341BD765130047E85A /* NSObject+OCMAdditions.m in Sources */, + 817EB1351BD765130047E85A /* NSValue+OCMAdditions.m in Sources */, + 817EB1361BD765130047E85A /* OCMFunctions.m in Sources */, + 817EB1371BD765130047E85A /* OCMArgAction.m in Sources */, + 2FA283BF22B1AA96AFCCD66C /* OCMNonRetainingObjectReturnValueProvider.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8DE97C5422B43EE60098C63F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8DE97C5522B43EE60098C63F /* OCMockObject.m in Sources */, + 8DE97C5622B43EE60098C63F /* OCClassMockObject.m in Sources */, + 8DE97C5722B43EE60098C63F /* OCPartialMockObject.m in Sources */, + 8DE97C5822B43EE60098C63F /* OCProtocolMockObject.m in Sources */, + 8DE97C5922B43EE60098C63F /* OCMRecorder.m in Sources */, + 03A1CC9F23F8A045005ADA04 /* OCMQuantifier.m in Sources */, + 8DE97C5A22B43EE60098C63F /* OCMStubRecorder.m in Sources */, + 8DE97C5B22B43EE60098C63F /* OCMExpectationRecorder.m in Sources */, + 8DE97C5C22B43EE60098C63F /* OCMVerifier.m in Sources */, + 8DE97C5D22B43EE60098C63F /* OCMInvocationMatcher.m in Sources */, + 8DE97C5E22B43EE60098C63F /* OCMInvocationStub.m in Sources */, + 8DE97C5F22B43EE60098C63F /* OCMInvocationExpectation.m in Sources */, + 8DE97C6022B43EE60098C63F /* OCMRealObjectForwarder.m in Sources */, + 8DE97C6122B43EE60098C63F /* OCMBlockCaller.m in Sources */, + 8DE97C6222B43EE60098C63F /* OCMBoxedReturnValueProvider.m in Sources */, + 8DE97C6322B43EE60098C63F /* OCMExceptionReturnValueProvider.m in Sources */, + 8DE97C6422B43EE60098C63F /* OCMIndirectReturnValueProvider.m in Sources */, + 8DE97C6522B43EE60098C63F /* OCMNotificationPoster.m in Sources */, + 8DE97C6622B43EE60098C63F /* OCMObjectReturnValueProvider.m in Sources */, + 8DE97C6722B43EE60098C63F /* OCMLocation.m in Sources */, + 8DE97C6822B43EE60098C63F /* OCMMacroState.m in Sources */, + 8DE97C6922B43EE60098C63F /* OCMBlockArgCaller.m in Sources */, + 8DE97C6A22B43EE60098C63F /* OCObserverMockObject.m in Sources */, + 8DE97C6B22B43EE60098C63F /* OCMObserverRecorder.m in Sources */, + 8DE97C6C22B43EE60098C63F /* OCMArg.m in Sources */, + 8DE97C6D22B43EE60098C63F /* OCMConstraint.m in Sources */, + 8DE97C6E22B43EE60098C63F /* OCMPassByRefSetter.m in Sources */, + 8DE97C6F22B43EE60098C63F /* NSInvocation+OCMAdditions.m in Sources */, + 8DE97C7022B43EE60098C63F /* NSMethodSignature+OCMAdditions.m in Sources */, + 8DE97C7122B43EE60098C63F /* NSNotificationCenter+OCMAdditions.m in Sources */, + 8DE97C7222B43EE60098C63F /* NSObject+OCMAdditions.m in Sources */, + 8DE97C7322B43EE60098C63F /* NSValue+OCMAdditions.m in Sources */, + 8DE97C7422B43EE60098C63F /* OCMFunctions.m in Sources */, + 8DE97C7522B43EE60098C63F /* OCMArgAction.m in Sources */, + 2FA283EC733E512211268010 /* OCMNonRetainingObjectReturnValueProvider.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D31108A91828DB8700737925 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3C76716C1BB3EBC500FDC9F4 /* TestClassWithCustomReferenceCounting.m in Sources */, + 031E50591BB4A56300E257C3 /* OCMBoxedReturnValueProviderTests.m in Sources */, + 03C9CA1E18F05A84006DF94D /* OCMArgTests.m in Sources */, + 036865651D3571A8005E6BEE /* OCMQuantifierTests.m in Sources */, + 0322DA66191188D100CACAF1 /* OCMockObjectVerifyAfterRunTests.m in Sources */, + D31108C51828DBD600737925 /* OCMockObjectClassMethodMockingTests.m in Sources */, + D31108C71828DBD600737925 /* OCMConstraintTests.m in Sources */, + D31108C91828DBD600737925 /* OCObserverMockObjectTests.m in Sources */, + 037ECD5518FAD84100AF0E4C /* OCMInvocationMatcherTests.m in Sources */, + D31108C41828DBD600737925 /* OCMockObjectPartialMocksTests.m in Sources */, + D31108C81828DBD600737925 /* OCMStubRecorderTests.m in Sources */, + D31108C61828DBD600737925 /* OCMockObjectForwardingTargetTests.m in Sources */, + D31108CA1828DBD600737925 /* NSInvocationOCMAdditionsTests.m in Sources */, + 03C9CA1F18F05A8E006DF94D /* NSMethodSignatureOCMAdditionsTests.m in Sources */, + 03C9CA1D18F05A75006DF94D /* OCMockObjectProtocolMocksTests.m in Sources */, + 03E98D5118F310EE00522D42 /* OCMockObjectMacroTests.m in Sources */, + A06930951CA1BFC900513023 /* TestObjects.xcdatamodeld in Sources */, + 8B11D4BB2448E53600247BE2 /* OCMCPlusPlus11Tests.mm in Sources */, + 2FA28295E1F58F40A77D7448 /* OCMockObjectRuntimeTests.m in Sources */, + 038599F823807B06002B3ABE /* OCMockObjectInternalTests.m in Sources */, + 8BF73E54246CA75E00B9A52C /* OCMNoEscapeBlockTests.m in Sources */, + 8B11D4B82448E2F400247BE2 /* OCMCPlusPlus98Tests.mm in Sources */, + 2FA28246CD449A01717B1CEC /* OCMockObjectTests.m in Sources */, + 2FA28F12AAD384A8CB16094B /* OCMockObjectDynamicPropertyMockingTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F0B950EC1B0080BE00942C38 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F0B9510C1B0080EC00942C38 /* OCMockObject.m in Sources */, + F0B9510D1B0080EC00942C38 /* OCClassMockObject.m in Sources */, + F0B9510E1B0080EC00942C38 /* OCPartialMockObject.m in Sources */, + F0B9510F1B0080EC00942C38 /* OCProtocolMockObject.m in Sources */, + F0B951101B0080EC00942C38 /* OCMRecorder.m in Sources */, + F0B951111B0080EC00942C38 /* OCMStubRecorder.m in Sources */, + F0B951121B0080EC00942C38 /* OCMExpectationRecorder.m in Sources */, + F0B951131B0080EC00942C38 /* OCMVerifier.m in Sources */, + F0B951141B0080EC00942C38 /* OCMInvocationMatcher.m in Sources */, + 0368656A1D3572ED005E6BEE /* OCMQuantifier.m in Sources */, + F0B951151B0080EC00942C38 /* OCMInvocationStub.m in Sources */, + F0B951161B0080EC00942C38 /* OCMInvocationExpectation.m in Sources */, + F0B951171B0080EC00942C38 /* OCMRealObjectForwarder.m in Sources */, + F0B951181B0080EC00942C38 /* OCMBlockCaller.m in Sources */, + F0B951191B0080EC00942C38 /* OCMBoxedReturnValueProvider.m in Sources */, + F0B9511A1B0080EC00942C38 /* OCMExceptionReturnValueProvider.m in Sources */, + F0B9511B1B0080EC00942C38 /* OCMIndirectReturnValueProvider.m in Sources */, + F0B9511C1B0080EC00942C38 /* OCMNotificationPoster.m in Sources */, + F0B9511D1B0080EC00942C38 /* OCMObjectReturnValueProvider.m in Sources */, + F0B9511E1B0080EC00942C38 /* OCMLocation.m in Sources */, + F0B9511F1B0080EC00942C38 /* OCMMacroState.m in Sources */, + 03E0FAD91B93C01A000C5096 /* OCMBlockArgCaller.m in Sources */, + F0B951201B0080EC00942C38 /* OCObserverMockObject.m in Sources */, + F0B951211B0080EC00942C38 /* OCMObserverRecorder.m in Sources */, + F0B951221B0080EC00942C38 /* OCMArg.m in Sources */, + F0B951231B0080EC00942C38 /* OCMConstraint.m in Sources */, + F0B951241B0080EC00942C38 /* OCMPassByRefSetter.m in Sources */, + F0B951251B0080EC00942C38 /* NSInvocation+OCMAdditions.m in Sources */, + F0B951261B0080EC00942C38 /* NSMethodSignature+OCMAdditions.m in Sources */, + F0B951271B0080EC00942C38 /* NSNotificationCenter+OCMAdditions.m in Sources */, + F0B951281B0080EC00942C38 /* NSObject+OCMAdditions.m in Sources */, + F0B951291B0080EC00942C38 /* NSValue+OCMAdditions.m in Sources */, + F0B9512A1B0080EC00942C38 /* OCMFunctions.m in Sources */, + 2FA281328683C83959C109C1 /* OCMArgAction.m in Sources */, + 2FA28E6C62F66CC0A54D0A70 /* OCMNonRetainingObjectReturnValueProvider.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 03565A3D18F0566F003AE91E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 030EF0A714632FD000B04273 /* OCMock */; + targetProxy = 03565A3C18F0566F003AE91E /* PBXContainerItemProxy */; + }; + D31108BF1828DB8700737925 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 030EF0DB14632FF700B04273 /* OCMockLib */; + targetProxy = D31108BE1828DB8700737925 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 030EF0D014632FD000B04273 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_GCD_PERFORMANCE = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; + CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; + CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; + CLANG_UNDEFINED_BEHAVIOR_SANITIZER_INTEGER = YES; + CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = 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_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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_VERSION = com.apple.compilers.llvm.clang.1_0; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MACOSX_DEPLOYMENT_TARGET = 10.10; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + TVOS_DEPLOYMENT_TARGET = 9.0; + WATCHOS_DEPLOYMENT_TARGET = 4.0; + }; + name = Debug; + }; + 030EF0D114632FD000B04273 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_GCD_PERFORMANCE = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; + CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; + CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; + CLANG_UNDEFINED_BEHAVIOR_SANITIZER_INTEGER = YES; + CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = 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_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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_VERSION = com.apple.compilers.llvm.clang.1_0; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MACOSX_DEPLOYMENT_TARGET = 10.10; + SDKROOT = macosx; + TVOS_DEPLOYMENT_TARGET = 9.0; + WATCHOS_DEPLOYMENT_TARGET = 4.0; + }; + name = Release; + }; + 030EF0D314632FD000B04273 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + FRAMEWORK_VERSION = A; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "OCMock/OCMock-Prefix.pch"; + INFOPLIST_FILE = "OCMock/OCMock-Info.plist"; + LD_DYLIB_INSTALL_NAME = "@rpath/$(EXECUTABLE_PATH)"; + PRODUCT_BUNDLE_IDENTIFIER = "com.mulle-kybernetik.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = framework; + }; + name = Debug; + }; + 030EF0D414632FD000B04273 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + FRAMEWORK_VERSION = A; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "OCMock/OCMock-Prefix.pch"; + INFOPLIST_FILE = "OCMock/OCMock-Info.plist"; + LD_DYLIB_INSTALL_NAME = "@rpath/$(EXECUTABLE_PATH)"; + PRODUCT_BUNDLE_IDENTIFIER = "com.mulle-kybernetik.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = framework; + }; + name = Release; + }; + 030EF0E614632FF700B04273 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = YES; + DSTROOT = /tmp/OCMockLib.dst; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "OCMockLib/OCMockLib-Prefix.pch"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = OCMock; + PUBLIC_HEADERS_FOLDER_PATH = "$(PRODUCT_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALID_ARCHS = "arm64 armv7 armv7s arm64e"; + "VALID_ARCHS[sdk=iphonesimulator*]" = "x86_64 i386"; + }; + name = Debug; + }; + 030EF0E714632FF700B04273 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = YES; + DSTROOT = /tmp/OCMockLib.dst; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "OCMockLib/OCMockLib-Prefix.pch"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = OCMock; + PUBLIC_HEADERS_FOLDER_PATH = "$(PRODUCT_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VALID_ARCHS = "arm64 armv7 armv7s arm64e"; + "VALID_ARCHS[sdk=iphonesimulator*]" = "x86_64 i386"; + }; + name = Release; + }; + 03565A3F18F0566F003AE91E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/Mac", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "OCMockTests/OCMockTests-Prefix.pch"; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + INFOPLIST_FILE = "OCMockTests/OCMockTests-Info.plist"; + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_BUNDLE_IDENTIFIER = "com.mulle-kybernetik.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = xctest; + }; + name = Debug; + }; + 03565A4018F0566F003AE91E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Carthage/Build/Mac", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "OCMockTests/OCMockTests-Prefix.pch"; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + INFOPLIST_FILE = "OCMockTests/OCMockTests-Info.plist"; + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_BUNDLE_IDENTIFIER = "com.mulle-kybernetik.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = xctest; + }; + name = Release; + }; + 817EB1601BD765130047E85A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + INFOPLIST_FILE = "OCMock/OCMock-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.mulle-kybernetik.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = OCMock; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 817EB1611BD765130047E85A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + INFOPLIST_FILE = "OCMock/OCMock-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "com.mulle-kybernetik.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = OCMock; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 8DE97C9E22B43EE60098C63F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + INFOPLIST_FILE = "OCMock/OCMock-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.mulle-kybernetik.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = OCMock; + SDKROOT = watchos; + SKIP_INSTALL = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 8DE97C9F22B43EE60098C63F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + INFOPLIST_FILE = "OCMock/OCMock-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "com.mulle-kybernetik.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = OCMock; + SDKROOT = watchos; + SKIP_INSTALL = YES; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + D31108C11828DB8700737925 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + 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_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "OCMockLibTests/OCMockLibTests-Prefix.pch"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + INFOPLIST_FILE = "OCMockLibTests/OCMockLibTests-Info.plist"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_BUNDLE_IDENTIFIER = "org.ocmock.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + }; + name = Debug; + }; + D31108C21828DB8700737925 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + 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_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + ENABLE_NS_ASSERTIONS = NO; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "OCMockLibTests/OCMockLibTests-Prefix.pch"; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + INFOPLIST_FILE = "OCMockLibTests/OCMockLibTests-Info.plist"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_BUNDLE_IDENTIFIER = "org.ocmock.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + F0B951051B0080BE00942C38 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "OCMock/OCMock-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.mulle-kybernetik.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = OCMock; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + F0B951061B0080BE00942C38 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_NS_ASSERTIONS = NO; + INFOPLIST_FILE = "OCMock/OCMock-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.mulle-kybernetik.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = OCMock; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 030EF0A114632FD000B04273 /* Build configuration list for PBXProject "OCMock" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 030EF0D014632FD000B04273 /* Debug */, + 030EF0D114632FD000B04273 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 030EF0D214632FD000B04273 /* Build configuration list for PBXNativeTarget "OCMock" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 030EF0D314632FD000B04273 /* Debug */, + 030EF0D414632FD000B04273 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 030EF0E514632FF700B04273 /* Build configuration list for PBXNativeTarget "OCMockLib" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 030EF0E614632FF700B04273 /* Debug */, + 030EF0E714632FF700B04273 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 03565A3E18F0566F003AE91E /* Build configuration list for PBXNativeTarget "OCMockTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 03565A3F18F0566F003AE91E /* Debug */, + 03565A4018F0566F003AE91E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 817EB15F1BD765130047E85A /* Build configuration list for PBXNativeTarget "OCMock tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 817EB1601BD765130047E85A /* Debug */, + 817EB1611BD765130047E85A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8DE97C9D22B43EE60098C63F /* Build configuration list for PBXNativeTarget "OCMock watchOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8DE97C9E22B43EE60098C63F /* Debug */, + 8DE97C9F22B43EE60098C63F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D31108C01828DB8700737925 /* Build configuration list for PBXNativeTarget "OCMockLibTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D31108C11828DB8700737925 /* Debug */, + D31108C21828DB8700737925 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F0B951041B0080BE00942C38 /* Build configuration list for PBXNativeTarget "OCMock iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F0B951051B0080BE00942C38 /* Debug */, + F0B951061B0080BE00942C38 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCVersionGroup section */ + A02926801CA0725A00594AAF /* TestObjects.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + A02926811CA0725A00594AAF /* TestObjects.xcdatamodel */, + ); + currentVersion = A02926811CA0725A00594AAF /* TestObjects.xcdatamodel */; + name = TestObjects.xcdatamodeld; + path = Resources/TestObjects.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ + }; + rootObject = 030EF09E14632FD000B04273 /* Project object */; +} diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSInvocation+OCMAdditions.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSInvocation+OCMAdditions.h new file mode 100644 index 0000000000..bfcd6ef2ee --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSInvocation+OCMAdditions.h @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2006-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@interface NSInvocation(OCMAdditions) + ++ (NSInvocation *)invocationForBlock:(id)block withArguments:(NSArray *)arguments; + +- (void)retainObjectArgumentsExcludingObject:(id)objectToExclude; + +- (id)getArgumentAtIndexAsObject:(NSInteger)argIndex; + +- (NSString *)invocationDescription; + +- (NSString *)argumentDescriptionAtIndex:(NSInteger)argIndex; + +- (NSString *)objectDescriptionAtIndex:(NSInteger)anInt; +- (NSString *)charDescriptionAtIndex:(NSInteger)anInt; +- (NSString *)unsignedCharDescriptionAtIndex:(NSInteger)anInt; +- (NSString *)intDescriptionAtIndex:(NSInteger)anInt; +- (NSString *)unsignedIntDescriptionAtIndex:(NSInteger)anInt; +- (NSString *)shortDescriptionAtIndex:(NSInteger)anInt; +- (NSString *)unsignedShortDescriptionAtIndex:(NSInteger)anInt; +- (NSString *)longDescriptionAtIndex:(NSInteger)anInt; +- (NSString *)unsignedLongDescriptionAtIndex:(NSInteger)anInt; +- (NSString *)longLongDescriptionAtIndex:(NSInteger)anInt; +- (NSString *)unsignedLongLongDescriptionAtIndex:(NSInteger)anInt; +- (NSString *)doubleDescriptionAtIndex:(NSInteger)anInt; +- (NSString *)floatDescriptionAtIndex:(NSInteger)anInt; +- (NSString *)structDescriptionAtIndex:(NSInteger)anInt; +- (NSString *)pointerDescriptionAtIndex:(NSInteger)anInt; +- (NSString *)cStringDescriptionAtIndex:(NSInteger)anInt; +- (NSString *)selectorDescriptionAtIndex:(NSInteger)anInt; + +- (BOOL)methodIsInInitFamily; +- (BOOL)methodIsInAllocFamily; +- (BOOL)methodIsInCopyFamily; +- (BOOL)methodIsInMutableCopyFamily; +- (BOOL)methodIsInNewFamily; + + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSInvocation+OCMAdditions.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSInvocation+OCMAdditions.m new file mode 100644 index 0000000000..8f44a9f2ca --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSInvocation+OCMAdditions.m @@ -0,0 +1,616 @@ +/* + * Copyright (c) 2006-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "NSInvocation+OCMAdditions.h" +#import "OCMArg.h" +#import "OCMFunctionsPrivate.h" +#import "NSMethodSignature+OCMAdditions.h" + +#if (TARGET_OS_OSX && (!defined(__MAC_10_10) || __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_10)) || \ + (TARGET_OS_IPHONE && (!defined(__IPHONE_8_0) || __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0)) +static BOOL OCMObjectIsClass(id object) { + return class_isMetaClass(object_getClass(object)); +} +#define object_isClass OCMObjectIsClass +#endif + +static NSString *const OCMArgAnyPointerDescription = @"<[OCMArg anyPointer]>"; + + +@implementation NSInvocation(OCMAdditions) + ++ (NSInvocation *)invocationForBlock:(id)block withArguments:(NSArray *)arguments +{ + NSMethodSignature *sig = [NSMethodSignature signatureForBlock:block]; + NSInvocation *inv = [self invocationWithMethodSignature:sig]; + + NSUInteger numArgsRequired = sig.numberOfArguments - 1; + if((arguments != nil) && ([arguments count] != numArgsRequired)) + [NSException raise:NSInvalidArgumentException format:@"Specified too few arguments for block; expected %lu arguments.", (unsigned long) numArgsRequired]; + + for(NSUInteger i = 0, j = 1; i < numArgsRequired; ++i, ++j) + { + id arg = [arguments objectAtIndex:i]; + [inv setArgumentWithObject:arg atIndex:j]; + } + + return inv; + +} + + +static NSString *const OCMRetainedObjectArgumentsKey = @"OCMRetainedObjectArgumentsKey"; + +- (void)retainObjectArgumentsExcludingObject:(id)objectToExclude +{ + if(objc_getAssociatedObject(self, OCMRetainedObjectArgumentsKey) != nil) + { + // looks like we've retained the arguments already; do nothing else + return; + } + + NSMutableArray *retainedArguments = [[NSMutableArray alloc] init]; + + id target = [self target]; + if((target != nil) && (target != objectToExclude) && !object_isClass(target)) + { + // Bad things will happen if the target is a block since it's not being + // copied. There isn't a very good way to tell if an invocation's target + // is a block though (the argument type at index 0 is always "@" even if + // the target is a Class or block), and in practice it's OK since you + // can't mock a block. + [retainedArguments addObject:target]; + } + + NSUInteger numberOfArguments = [[self methodSignature] numberOfArguments]; + for(NSUInteger index = 2; index < numberOfArguments; index++) + { + const char *argumentType = [[self methodSignature] getArgumentTypeAtIndex:index]; + if(OCMIsObjectType(argumentType)) + { + id argument; + [self getArgument:&argument atIndex:index]; + if((argument != nil) && (argument != objectToExclude)) + { + if(OCMIsBlockType(argumentType)) + { + // Block types need to be copied because they could be stack blocks. + // However, non-escaping blocks have a lifetime that is stack-based and they + // treat copy/release as a no-op. For details see: + // https://reviews.llvm.org/rGdbfa453e4138bb977644929c69d1c71e5e8b4bee + // If we keep a reference to a non-escaping block in retainedArguments, it + // will end up as dangling pointer, resulting in a crash later. + if(OCMIsNonEscapingBlock(argument) == NO) + { + id blockArgument = [argument copy]; + [retainedArguments addObject:blockArgument]; + [blockArgument release]; + } + } + else if(OCMIsClassType(argumentType) && object_isClass(argument)) + { + // The argument's type is class and the passed argument is a class. In this + // case do not retain the argument. Note: Even though the type is class the + // argument could be a non-class, e.g. an instance of OCMArg. + } + else + { + [retainedArguments addObject:argument]; + } + } + } + } + + const char *returnType = [[self methodSignature] methodReturnType]; + if(OCMIsObjectType(returnType)) + { + id returnValue; + [self getReturnValue:&returnValue]; + if((returnValue != nil) && (returnValue != objectToExclude)) + { + if(OCMIsBlockType(returnType)) + { + // See above for an explanation + if(OCMIsNonEscapingBlock(returnValue) == NO) + { + id blockReturnValue = [returnValue copy]; + [retainedArguments addObject:blockReturnValue]; + [blockReturnValue release]; + } + } + else + { + [retainedArguments addObject:returnValue]; + } + } + } + + objc_setAssociatedObject(self, OCMRetainedObjectArgumentsKey, retainedArguments, OBJC_ASSOCIATION_RETAIN); + [retainedArguments release]; +} + + +- (void)setArgumentWithObject:(id)arg atIndex:(NSInteger)idx +{ + const char *typeEncoding = [[self methodSignature] getArgumentTypeAtIndex:idx]; + if((arg == nil) || ([arg respondsToSelector:@selector(isKindOfClass:)] && [arg isKindOfClass:[NSNull class]])) + { + if(typeEncoding[0] == '^') + { + void *nullPtr = NULL; + [self setArgument:&nullPtr atIndex:idx]; + } + else if(typeEncoding[0] == '@') + { + id nilObj = nil; + [self setArgument:&nilObj atIndex:idx]; + } + else if(OCMNumberTypeForObjCType(typeEncoding)) + { + NSUInteger argSize; + NSGetSizeAndAlignment(typeEncoding, NULL, &argSize); + void *argBuffer = calloc(1, argSize); + [self setArgument:argBuffer atIndex:idx]; + free(argBuffer); + } + else + { + [NSException raise:NSInvalidArgumentException format:@"Unable to create default value for type '%s'.", typeEncoding]; + } + } + else if(OCMIsObjectType(typeEncoding)) + { + [self setArgument:&arg atIndex:idx]; + } + else + { + if(![arg isKindOfClass:[NSValue class]]) + [NSException raise:NSInvalidArgumentException format:@"Argument '%@' should be boxed in NSValue.", arg]; + + char const *valEncoding = [arg objCType]; + + /// @note Here we allow any data pointer to be passed as a void pointer and + /// any numerical types to be passed as arguments to the block. + BOOL takesVoidPtr = !strcmp(typeEncoding, "^v") && valEncoding[0] == '^'; + BOOL takesNumber = OCMNumberTypeForObjCType(typeEncoding) && OCMNumberTypeForObjCType(valEncoding); + + if(!takesVoidPtr && !takesNumber && !OCMEqualTypesAllowingOpaqueStructs(typeEncoding, valEncoding)) + [NSException raise:NSInvalidArgumentException format:@"Argument type mismatch; type of argument required is '%s' but type of value provided is '%s'", typeEncoding, valEncoding]; + + NSUInteger argSize; + NSGetSizeAndAlignment(typeEncoding, &argSize, NULL); + void *argBuffer = malloc(argSize); + [arg getValue:argBuffer]; + [self setArgument:argBuffer atIndex:idx]; + free(argBuffer); + } + +} + + +- (id)getArgumentAtIndexAsObject:(NSInteger)argIndex +{ + const char *argType = OCMTypeWithoutQualifiers([[self methodSignature] getArgumentTypeAtIndex:(NSUInteger)argIndex]); + + if((strlen(argType) > 1) && (strchr("{^", argType[0]) == NULL) && (strcmp("@?", argType) != 0)) + [NSException raise:NSInvalidArgumentException format:@"Cannot handle argument type '%s'.", argType]; + + if(OCMIsObjectType(argType)) + { + id value; + [self getArgument:&value atIndex:argIndex]; + return value; + } + + switch(argType[0]) + { + case ':': + { + SEL s = (SEL)0; + [self getArgument:&s atIndex:argIndex]; + return [NSValue valueWithBytes:&s objCType:":"]; + } + case 'i': + { + int value; + [self getArgument:&value atIndex:argIndex]; + return @(value); + } + case 's': + { + short value; + [self getArgument:&value atIndex:argIndex]; + return @(value); + } + case 'l': + { + long value; + [self getArgument:&value atIndex:argIndex]; + return @(value); + } + case 'q': + { + long long value; + [self getArgument:&value atIndex:argIndex]; + return @(value); + } + case 'c': + { + char value; + [self getArgument:&value atIndex:argIndex]; + return @(value); + } + case 'C': + { + unsigned char value; + [self getArgument:&value atIndex:argIndex]; + return @(value); + } + case 'I': + { + unsigned int value; + [self getArgument:&value atIndex:argIndex]; + return @(value); + } + case 'S': + { + unsigned short value; + [self getArgument:&value atIndex:argIndex]; + return @(value); + } + case 'L': + { + unsigned long value; + [self getArgument:&value atIndex:argIndex]; + return @(value); + } + case 'Q': + { + unsigned long long value; + [self getArgument:&value atIndex:argIndex]; + return @(value); + } + case 'f': + { + float value; + [self getArgument:&value atIndex:argIndex]; + return @(value); + } + case 'd': + { + double value; + [self getArgument:&value atIndex:argIndex]; + return @(value); + } + case 'D': + { + long double value; + [self getArgument:&value atIndex:argIndex]; + return [NSValue valueWithBytes:&value objCType:@encode(__typeof__(value))]; + } + case 'B': + { + bool value; + [self getArgument:&value atIndex:argIndex]; + return @(value); + } + case '^': + case '*': + { + void *value = NULL; + [self getArgument:&value atIndex:argIndex]; + return [NSValue valueWithPointer:value]; + } + case '{': // structure + { + NSUInteger argSize; + NSGetSizeAndAlignment([[self methodSignature] getArgumentTypeAtIndex:(NSUInteger)argIndex], &argSize, NULL); + if(argSize == 0) // TODO: Can this happen? Is frameLength a good choice in that case? + argSize = [[self methodSignature] frameLength]; + NSMutableData *argumentData = [[[NSMutableData alloc] initWithLength:argSize] autorelease]; + [self getArgument:[argumentData mutableBytes] atIndex:argIndex]; + return [NSValue valueWithBytes:[argumentData bytes] objCType:argType]; + } + + } + [NSException raise:NSInvalidArgumentException format:@"Argument type '%s' not supported", argType]; + return nil; +} + + +- (NSString *)invocationDescription +{ + NSMethodSignature *methodSignature = [self methodSignature]; + NSUInteger numberOfArgs = [methodSignature numberOfArguments]; + + if (numberOfArgs == 2) + return NSStringFromSelector([self selector]); + + NSArray *selectorParts = [NSStringFromSelector([self selector]) componentsSeparatedByString:@":"]; + NSMutableString *description = [[NSMutableString alloc] init]; + NSUInteger i; + for(i = 2; i < numberOfArgs; i++) + { + [description appendFormat:@"%@%@:", (i > 2 ? @" " : @""), [selectorParts objectAtIndex:(i - 2)]]; + [description appendString:[self argumentDescriptionAtIndex:(NSInteger)i]]; + } + + return [description autorelease]; +} + +- (NSString *)argumentDescriptionAtIndex:(NSInteger)argIndex +{ + const char *argType = OCMTypeWithoutQualifiers([[self methodSignature] getArgumentTypeAtIndex:(NSUInteger)argIndex]); + + switch(*argType) + { + case '@': return [self objectDescriptionAtIndex:argIndex]; + case 'B': return [self boolDescriptionAtIndex:argIndex]; + case 'c': return [self charDescriptionAtIndex:argIndex]; + case 'C': return [self unsignedCharDescriptionAtIndex:argIndex]; + case 'i': return [self intDescriptionAtIndex:argIndex]; + case 'I': return [self unsignedIntDescriptionAtIndex:argIndex]; + case 's': return [self shortDescriptionAtIndex:argIndex]; + case 'S': return [self unsignedShortDescriptionAtIndex:argIndex]; + case 'l': return [self longDescriptionAtIndex:argIndex]; + case 'L': return [self unsignedLongDescriptionAtIndex:argIndex]; + case 'q': return [self longLongDescriptionAtIndex:argIndex]; + case 'Q': return [self unsignedLongLongDescriptionAtIndex:argIndex]; + case 'd': return [self doubleDescriptionAtIndex:argIndex]; + case 'f': return [self floatDescriptionAtIndex:argIndex]; + case 'D': return [self longDoubleDescriptionAtIndex:argIndex]; + case '{': return [self structDescriptionAtIndex:argIndex]; + case '^': return [self pointerDescriptionAtIndex:argIndex]; + case '*': return [self cStringDescriptionAtIndex:argIndex]; + case ':': return [self selectorDescriptionAtIndex:argIndex]; + default: return [@""]; // avoid confusion with trigraphs... + } + +} + +- (NSString *)objectDescriptionAtIndex:(NSInteger)anInt +{ + id object; + + [self getArgument:&object atIndex:anInt]; + if (object == nil) + return @"nil"; + else if(![object isProxy] && [object isKindOfClass:[NSString class]]) + return [NSString stringWithFormat:@"@\"%@\"", [object description]]; + else + // The description cannot be nil, if it is then replace it + return [object description] ?: @""; +} + +- (NSString *)boolDescriptionAtIndex:(NSInteger)anInt +{ + bool value; + [self getArgument:&value atIndex:anInt]; + return value? @"YES" : @"NO"; +} + +- (NSString *)charDescriptionAtIndex:(NSInteger)anInt +{ + unsigned char buffer[128]; + memset(buffer, 0x0, 128); + + [self getArgument:&buffer atIndex:anInt]; + + // If there's only one character in the buffer, and it's 0 or 1, then we have a BOOL + if (buffer[1] == '\0' && (buffer[0] == 0 || buffer[0] == 1)) + return (buffer[0] == 1 ? @"YES" : @"NO"); + else + return [NSString stringWithFormat:@"'%c'", *buffer]; +} + +- (NSString *)unsignedCharDescriptionAtIndex:(NSInteger)anInt +{ + unsigned char buffer[128]; + memset(buffer, 0x0, 128); + + [self getArgument:&buffer atIndex:anInt]; + return [NSString stringWithFormat:@"'%c'", *buffer]; +} + +- (NSString *)intDescriptionAtIndex:(NSInteger)anInt +{ + int intValue; + + [self getArgument:&intValue atIndex:anInt]; + return [NSString stringWithFormat:@"%d", intValue]; +} + +- (NSString *)unsignedIntDescriptionAtIndex:(NSInteger)anInt +{ + unsigned int intValue; + + [self getArgument:&intValue atIndex:anInt]; + return [NSString stringWithFormat:@"%d", intValue]; +} + +- (NSString *)shortDescriptionAtIndex:(NSInteger)anInt +{ + short shortValue; + + [self getArgument:&shortValue atIndex:anInt]; + return [NSString stringWithFormat:@"%hi", shortValue]; +} + +- (NSString *)unsignedShortDescriptionAtIndex:(NSInteger)anInt +{ + unsigned short shortValue; + + [self getArgument:&shortValue atIndex:anInt]; + return [NSString stringWithFormat:@"%hu", shortValue]; +} + +- (NSString *)longDescriptionAtIndex:(NSInteger)anInt +{ + long longValue; + + [self getArgument:&longValue atIndex:anInt]; + return [NSString stringWithFormat:@"%ld", longValue]; +} + +- (NSString *)unsignedLongDescriptionAtIndex:(NSInteger)anInt +{ + unsigned long longValue; + + [self getArgument:&longValue atIndex:anInt]; + return [NSString stringWithFormat:@"%lu", longValue]; +} + +- (NSString *)longLongDescriptionAtIndex:(NSInteger)anInt +{ + long long longLongValue; + + [self getArgument:&longLongValue atIndex:anInt]; + return [NSString stringWithFormat:@"%qi", longLongValue]; +} + +- (NSString *)unsignedLongLongDescriptionAtIndex:(NSInteger)anInt +{ + unsigned long long longLongValue; + + [self getArgument:&longLongValue atIndex:anInt]; + return [NSString stringWithFormat:@"%qu", longLongValue]; +} + +- (NSString *)doubleDescriptionAtIndex:(NSInteger)anInt +{ + double doubleValue; + + [self getArgument:&doubleValue atIndex:anInt]; + return [NSString stringWithFormat:@"%f", doubleValue]; +} + +- (NSString *)floatDescriptionAtIndex:(NSInteger)anInt +{ + float floatValue; + + [self getArgument:&floatValue atIndex:anInt]; + return [NSString stringWithFormat:@"%f", floatValue]; +} + +- (NSString *)longDoubleDescriptionAtIndex:(NSInteger)anInt +{ + long double longDoubleValue; + + [self getArgument:&longDoubleValue atIndex:anInt]; + return [NSString stringWithFormat:@"%Lf", longDoubleValue]; +} + +- (NSString *)structDescriptionAtIndex:(NSInteger)anInt +{ + return [NSString stringWithFormat:@"(%@)", [[self getArgumentAtIndexAsObject:anInt] description]]; +} + +- (NSString *)pointerDescriptionAtIndex:(NSInteger)anInt +{ + void *buffer; + + [self getArgument:&buffer atIndex:anInt]; + + if(buffer == [OCMArg anyPointer]) + return OCMArgAnyPointerDescription; + else + return [NSString stringWithFormat:@"%p", buffer]; +} + +- (NSString *)cStringDescriptionAtIndex:(NSInteger)anInt +{ + char *cStringPtr; + + [self getArgument:&cStringPtr atIndex:anInt]; + + if(cStringPtr == [OCMArg anyPointer]) + { + return OCMArgAnyPointerDescription; + } + else + { + char buffer[104]; + strlcpy(buffer, cStringPtr, sizeof(buffer)); + strlcpy(buffer + 100, "...", (sizeof(buffer) - 100)); + return [NSString stringWithFormat:@"\"%s\"", buffer]; + } +} + +- (NSString *)selectorDescriptionAtIndex:(NSInteger)anInt +{ + SEL selectorValue; + + [self getArgument:&selectorValue atIndex:anInt]; + return [NSString stringWithFormat:@"@selector(%@)", NSStringFromSelector(selectorValue)]; +} + + +- (BOOL)isMethodFamily:(NSString *)family +{ + // Definitions here: https://clang.llvm.org/docs/AutomaticReferenceCounting.html#method-families + + NSMethodSignature *signature = [self methodSignature]; + if(OCMIsObjectType(signature.methodReturnType) == NO) + { + return NO; + } + + NSString *selString = NSStringFromSelector([self selector]); + NSRange underscoreRange = [selString rangeOfString:@"^_*" options:NSRegularExpressionSearch]; + selString = [selString substringFromIndex:NSMaxRange(underscoreRange)]; + + if([selString hasPrefix:family] == NO) + { + return NO; + } + NSUInteger familyLength = [family length]; + if(([selString length] > familyLength) && + ([[NSCharacterSet lowercaseLetterCharacterSet] characterIsMember:[selString characterAtIndex:familyLength]])) + { + return NO; + } + return YES; +} + + +- (BOOL)methodIsInInitFamily +{ + return [self isMethodFamily:@"init"]; +} + +- (BOOL)methodIsInAllocFamily +{ + return [self isMethodFamily:@"alloc"]; +} + +- (BOOL)methodIsInCopyFamily +{ + return [self isMethodFamily:@"copy"]; +} + +- (BOOL)methodIsInMutableCopyFamily +{ + return [self isMethodFamily:@"mutableCopy"]; +} + +- (BOOL)methodIsInNewFamily +{ + return [self isMethodFamily:@"new"]; +} + + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSMethodSignature+OCMAdditions.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSMethodSignature+OCMAdditions.h new file mode 100644 index 0000000000..6d86e8cb0a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSMethodSignature+OCMAdditions.h @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2009-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@interface NSMethodSignature(OCMAdditions) + ++ (NSMethodSignature *)signatureForDynamicPropertyAccessedWithSelector:(SEL)selector inClass:(Class)aClass; ++ (NSMethodSignature *)signatureForBlock:(id)block; + +- (BOOL)usesSpecialStructureReturn; + +- (NSString *)fullTypeString; +- (const char *)fullObjCTypes; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSMethodSignature+OCMAdditions.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSMethodSignature+OCMAdditions.m new file mode 100644 index 0000000000..259860340e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSMethodSignature+OCMAdditions.m @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2009-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "NSMethodSignature+OCMAdditions.h" +#import "OCMFunctionsPrivate.h" + + +@implementation NSMethodSignature(OCMAdditions) + +#pragma mark Signatures for dynamic properties + ++ (NSMethodSignature *)signatureForDynamicPropertyAccessedWithSelector:(SEL)selector inClass:(Class)aClass +{ + BOOL isGetter = YES; + objc_property_t property = [self propertyMatchingSelector:selector inClass:aClass isGetter:&isGetter]; + if(property == NULL) + return nil; + + const char *propertyAttributesString = property_getAttributes(property); + NSArray *propertyAttributes = [[NSString stringWithCString:propertyAttributesString + encoding:NSASCIIStringEncoding] componentsSeparatedByString:@","]; + NSString *typeStr = nil; + BOOL isDynamic = NO; + for(NSString *attribute in propertyAttributes) + { + if([attribute isEqualToString:@"D"]) + isDynamic = YES; + else if([attribute hasPrefix:@"T"]) + typeStr = [attribute substringFromIndex:1]; + } + + if(!isDynamic) + return nil; + + NSRange r = [typeStr rangeOfString:@"\""]; // incomplete workaround to deal with structs + if(r.location != NSNotFound) + typeStr = [typeStr substringToIndex:r.location]; + + NSString *sigStringFormat = isGetter ? @"%@@:" : @"v@:%@"; + const char *sigCString = [[NSString stringWithFormat:sigStringFormat, typeStr] cStringUsingEncoding:NSASCIIStringEncoding]; + return [NSMethodSignature signatureWithObjCTypes:sigCString]; +} + + ++ (objc_property_t)propertyMatchingSelector:(SEL)selector inClass:(Class)aClass isGetter:(BOOL *)isGetterPtr +{ + NSString *propertyName = NSStringFromSelector(selector); + + // first try selector as is aassuming it's a getter + objc_property_t property = class_getProperty(aClass, [propertyName cStringUsingEncoding:NSASCIIStringEncoding]); + if(property != NULL) + { + *isGetterPtr = YES; + return property; + } + + // try setter next if selector starts with "set" + if([propertyName hasPrefix:@"set"]) + { + propertyName = [propertyName substringFromIndex:@"set".length]; + propertyName = [propertyName stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[[propertyName substringToIndex:1] lowercaseString]]; + if([propertyName hasSuffix:@":"]) + propertyName = [propertyName substringToIndex:[propertyName length] - 1]; + + property = class_getProperty(aClass, [propertyName cStringUsingEncoding:NSASCIIStringEncoding]); + if(property != NULL) + { + *isGetterPtr = NO; + return property; + } + } + + // search through properties with custom getter/setter that corresponds to selector + unsigned int propertiesCount = 0; + objc_property_t *allProperties = class_copyPropertyList(aClass, &propertiesCount); + for(unsigned int i = 0 ; i < propertiesCount; i++) + { + NSArray *propertyAttributes = [[NSString stringWithCString:property_getAttributes(allProperties[i]) + encoding:NSASCIIStringEncoding] componentsSeparatedByString:@","]; + for(NSString *attribute in propertyAttributes) + { + if(([attribute hasPrefix:@"G"] || [attribute hasPrefix:@"S"]) && + [[attribute substringFromIndex:1] isEqualToString:propertyName]) + { + *isGetterPtr = ![attribute hasPrefix:@"S"]; + property = allProperties[i]; + i = propertiesCount; + break; + } + } + } + free(allProperties); + + return property; +} + + +#pragma mark Signatures for blocks + ++ (NSMethodSignature *)signatureForBlock:(id)block +{ + /* For a more complete implementation of parsing the block data structure see: + * + * https://github.com/ebf/CTObjectiveCRuntimeAdditions/tree/master/CTObjectiveCRuntimeAdditions/CTObjectiveCRuntimeAdditions + */ + + struct OCMBlockDef *blockRef = (__bridge struct OCMBlockDef *) block; + + if(!(blockRef->flags & OCMBlockDescriptionFlagsHasSignature)) + return nil; + + void *signatureLocation = blockRef->descriptor; + signatureLocation += sizeof(unsigned long int); + signatureLocation += sizeof(unsigned long int); + if(blockRef->flags & OCMBlockDescriptionFlagsHasCopyDispose) + { + signatureLocation += sizeof(void (*)(void *dst, void *src)); + signatureLocation += sizeof(void (*)(void *src)); + } + + const char *signature = (*(const char **) signatureLocation); + return [NSMethodSignature signatureWithObjCTypes:signature]; +} + + +#pragma mark Extended attributes + +- (BOOL)usesSpecialStructureReturn +{ + const char *types = OCMTypeWithoutQualifiers([self methodReturnType]); + + if((types == NULL) || (types[0] != '{')) + return NO; + + /* In some cases structures are returned by ref. The rules are complex and depend on the + architecture, see: + + http://sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html + http://developer.apple.com/library/mac/#documentation/DeveloperTools/Conceptual/LowLevelABI/000-Introduction/introduction.html + https://github.com/atgreen/libffi/blob/master/src/x86/ffi64.c + http://www.uclibc.org/docs/psABI-x86_64.pdf + http://infocenter.arm.com/help/topic/com.arm.doc.ihi0042e/IHI0042E_aapcs.pdf + + NSMethodSignature knows the details but has no API to return it, though it is in + the debugDescription. Horribly kludgy. + */ + NSRange range = [[self debugDescription] rangeOfString:@"is special struct return? YES"]; + return range.length > 0; +} + + +- (NSString *)fullTypeString +{ + NSMutableString *typeString = [NSMutableString string]; + [typeString appendFormat:@"%s", [self methodReturnType]]; + for (NSUInteger i=0; i<[self numberOfArguments]; i++) + [typeString appendFormat:@"%s", [self getArgumentTypeAtIndex:i]]; + return typeString; +} + + +- (const char *)fullObjCTypes +{ + return [[self fullTypeString] UTF8String]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSNotificationCenter+OCMAdditions.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSNotificationCenter+OCMAdditions.h new file mode 100644 index 0000000000..164ec2aa3b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSNotificationCenter+OCMAdditions.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2009-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@class OCObserverMockObject; + + +@interface NSNotificationCenter(OCMAdditions) + +- (void)addMockObserver:(OCObserverMockObject *)notificationObserver name:(NSString *)notificationName object:(id)notificationSender; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSNotificationCenter+OCMAdditions.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSNotificationCenter+OCMAdditions.m new file mode 100644 index 0000000000..c9f43dbe28 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSNotificationCenter+OCMAdditions.m @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2009-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "NSNotificationCenter+OCMAdditions.h" +#import "OCObserverMockObject.h" + + +@implementation NSNotificationCenter(OCMAdditions) + +- (void)addMockObserver:(OCObserverMockObject *)notificationObserver name:(NSString *)notificationName object:(id)notificationSender +{ + [notificationObserver autoRemoveFromCenter:self]; + [self addObserver:notificationObserver selector:@selector(handleNotification:) name:notificationName object:notificationSender]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSObject+OCMAdditions.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSObject+OCMAdditions.h new file mode 100644 index 0000000000..86d7063ec3 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSObject+OCMAdditions.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2013-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@interface NSObject(OCMAdditions) + ++ (IMP)instanceMethodForwarderForSelector:(SEL)aSelector; ++ (void)enumerateMethodsInClass:(Class)aClass usingBlock:(void (^)(Class cls, SEL sel))aBlock; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSObject+OCMAdditions.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSObject+OCMAdditions.m new file mode 100644 index 0000000000..b2a65ea2af --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSObject+OCMAdditions.m @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2009-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "NSObject+OCMAdditions.h" +#import "NSMethodSignature+OCMAdditions.h" + + +@implementation NSObject(OCMAdditions) + ++ (IMP)instanceMethodForwarderForSelector:(SEL)aSelector +{ + // use sel_registerName() and not @selector to avoid warning + SEL selectorWithNoImplementation = sel_registerName("methodWhichMustNotExist::::"); + +#ifndef __arm64__ + static NSMutableDictionary *_OCMReturnTypeCache; + + if(_OCMReturnTypeCache == nil) + _OCMReturnTypeCache = [[NSMutableDictionary alloc] init]; + + BOOL needsStructureReturn; + void *rawCacheKey[2] = { (void *)self, aSelector }; + NSData *cacheKey = [NSData dataWithBytes:rawCacheKey length:sizeof(rawCacheKey)]; + NSNumber *cachedValue = [_OCMReturnTypeCache objectForKey:cacheKey]; + + if(cachedValue == nil) + { + NSMethodSignature *sig = [self instanceMethodSignatureForSelector:aSelector]; + needsStructureReturn = [sig usesSpecialStructureReturn]; + [_OCMReturnTypeCache setObject:@(needsStructureReturn) forKey:cacheKey]; + } + else + { + needsStructureReturn = [cachedValue boolValue]; + } + + if(needsStructureReturn) + return class_getMethodImplementation_stret([NSObject class], selectorWithNoImplementation); +#endif + + return class_getMethodImplementation([NSObject class], selectorWithNoImplementation); +} + + ++ (void)enumerateMethodsInClass:(Class)aClass usingBlock:(void (^)(Class cls, SEL sel))aBlock +{ + for(Class cls = aClass; cls != nil; cls = class_getSuperclass(cls)) + { + Method *methodList = class_copyMethodList(cls, NULL); + if(methodList == NULL) + continue; + + for(Method *mPtr = methodList; *mPtr != NULL; mPtr++) + { + SEL sel = method_getName(*mPtr); + aBlock(cls, sel); + } + free(methodList); + } +} + + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSValue+OCMAdditions.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSValue+OCMAdditions.h new file mode 100644 index 0000000000..3e09b8ef60 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSValue+OCMAdditions.h @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2014-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@interface NSValue(OCMAdditions) + +- (BOOL)getBytes:(void *)outputBuf objCType:(const char *)targetType; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSValue+OCMAdditions.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSValue+OCMAdditions.m new file mode 100644 index 0000000000..aab0a50b92 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/NSValue+OCMAdditions.m @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2014-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "NSValue+OCMAdditions.h" +#import "OCMFunctionsPrivate.h" + + +@implementation NSValue(OCMAdditions) + +static NSNumber *OCMNumberForValue(NSValue *value) +{ +#define CREATE_NUM(_type) ({ _type _v; [value getValue:&_v]; @(_v); }) + switch([value objCType][0]) + { + case 'c': return CREATE_NUM(char); + case 'C': return CREATE_NUM(unsigned char); + case 'B': return CREATE_NUM(bool); + case 's': return CREATE_NUM(short); + case 'S': return CREATE_NUM(unsigned short); + case 'i': return CREATE_NUM(int); + case 'I': return CREATE_NUM(unsigned int); + case 'l': return CREATE_NUM(long); + case 'L': return CREATE_NUM(unsigned long); + case 'q': return CREATE_NUM(long long); + case 'Q': return CREATE_NUM(unsigned long long); + case 'f': return CREATE_NUM(float); + case 'd': return CREATE_NUM(double); + default: return nil; + } +} + + +- (BOOL)getBytes:(void *)outputBuf objCType:(const char *)targetType +{ + /* + * See if they are similar number types, and if we can convert losslessly between them. + * For the most part, we set things up to use CFNumberGetValue, which returns false if + * conversion will be lossy. + */ + CFNumberType inputType = OCMNumberTypeForObjCType([self objCType]); + CFNumberType outputType = OCMNumberTypeForObjCType(targetType); + + if(inputType == 0 || outputType == 0) // one or both are non-number types + return NO; + + NSNumber *inputNumber = [self isKindOfClass:[NSNumber class]] ? (NSNumber *)self : OCMNumberForValue(self); + + /* + * Due to some legacy, back-compatible requirements in CFNumber.c, CFNumberGetValue can return true for + * some conversions which should not be allowed (by reading source, conversions from integer types to + * 8-bit or 16-bit integer types). So, check ourselves. + */ + long long min; + long long max; + long long val = [inputNumber longLongValue]; + switch(targetType[0]) + { + case 'B': + case 'c': min = CHAR_MIN; max = CHAR_MAX; break; + case 'C': min = 0; max = UCHAR_MAX; break; + case 's': min = SHRT_MIN; max = SHRT_MAX; break; + case 'S': min = 0; max = USHRT_MAX; break; + default: min = LLONG_MIN; max = LLONG_MAX; break; + } + if(val < min || val > max) + return NO; + + /* Get the number, and return NO if the value was out of range or conversion was lossy */ + return CFNumberGetValue((CFNumberRef)inputNumber, outputType, outputBuf); +} + + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCClassMockObject.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCClassMockObject.h new file mode 100644 index 0000000000..a480ff9b5f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCClassMockObject.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2005-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMockObject.h" + +@interface OCClassMockObject : OCMockObject +{ + Class mockedClass; + Class originalMetaClass; + Class classCreatedForNewMetaClass; +} + +- (id)initWithClass:(Class)aClass; + +- (Class)mockedClass; +- (Class)mockObjectClass; // since -class returns the mockedClass + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCClassMockObject.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCClassMockObject.m new file mode 100644 index 0000000000..7a7f45786f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCClassMockObject.m @@ -0,0 +1,306 @@ +/* + * Copyright (c) 2005-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCClassMockObject.h" +#import "OCMInvocationStub.h" +#import "OCMFunctionsPrivate.h" +#import "NSMethodSignature+OCMAdditions.h" +#import "NSObject+OCMAdditions.h" + + +@implementation OCClassMockObject + +#pragma mark Initialisers, description, accessors, etc. + +- (id)initWithClass:(Class)aClass +{ + if(aClass == Nil) + [NSException raise:NSInvalidArgumentException format:@"Class cannot be Nil."]; + + [super init]; + mockedClass = aClass; + [self prepareClassForClassMethodMocking]; + return self; +} + +- (void)dealloc +{ + [self stopMocking]; + [super dealloc]; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"OCClassMockObject(%@)", NSStringFromClass(mockedClass)]; +} + +- (Class)mockedClass +{ + return mockedClass; +} + + +#pragma mark Extending/overriding superclass behaviour + +- (void)stopMocking +{ + if(originalMetaClass != nil) + { + [self stopMockingClassMethods]; + } + if(classCreatedForNewMetaClass != nil) + { + OCMDisposeSubclass(classCreatedForNewMetaClass); + classCreatedForNewMetaClass = nil; + } + [super stopMocking]; +} + + +- (void)stopMockingClassMethods +{ + OCMSetAssociatedMockForClass(nil, mockedClass); + object_setClass(mockedClass, originalMetaClass); + originalMetaClass = nil; + /* created meta class will be disposed later because partial mocks create another subclass depending on it */ +} + + +- (void)addStub:(OCMInvocationStub *)aStub +{ + [super addStub:aStub]; + if([aStub recordedAsClassMethod]) + [self setupForwarderForClassMethodSelector:[[aStub recordedInvocation] selector]]; +} + + +#pragma mark Class method mocking + +- (void)prepareClassForClassMethodMocking +{ + /* the runtime and OCMock depend on string and array; we don't intercept methods on them to avoid endless loops */ + if([[mockedClass class] isSubclassOfClass:[NSString class]] || [[mockedClass class] isSubclassOfClass:[NSArray class]]) + return; + + /* trying to replace class methods on NSManagedObject and subclasses of it doesn't work; see #339 */ + if([mockedClass isSubclassOfClass:objc_getClass("NSManagedObject")]) + return; + + /* if there is another mock for this exact class, stop it */ + id otherMock = OCMGetAssociatedMockForClass(mockedClass, NO); + if(otherMock != nil) + [otherMock stopMockingClassMethods]; + + OCMSetAssociatedMockForClass(self, mockedClass); + + /* dynamically create a subclass and use its meta class as the meta class for the mocked class */ + classCreatedForNewMetaClass = OCMCreateSubclass(mockedClass, mockedClass); + originalMetaClass = object_getClass(mockedClass); + id newMetaClass = object_getClass(classCreatedForNewMetaClass); + + /* create a dummy initialize method */ + Method myDummyInitializeMethod = class_getInstanceMethod([self mockObjectClass], @selector(initializeForClassObject)); + const char *initializeTypes = method_getTypeEncoding(myDummyInitializeMethod); + IMP myDummyInitializeIMP = method_getImplementation(myDummyInitializeMethod); + class_addMethod(newMetaClass, @selector(initialize), myDummyInitializeIMP, initializeTypes); + + object_setClass(mockedClass, newMetaClass); // only after dummy initialize is installed (iOS9) + + /* point forwardInvocation: of the object to the implementation in the mock */ + Method myForwardMethod = class_getInstanceMethod([self mockObjectClass], @selector(forwardInvocationForClassObject:)); + IMP myForwardIMP = method_getImplementation(myForwardMethod); + class_addMethod(newMetaClass, @selector(forwardInvocation:), myForwardIMP, method_getTypeEncoding(myForwardMethod)); + + /* adding forwarder for most class methods (instance methods on meta class) to allow for verify after run */ + NSArray *methodBlackList = @[@"class", @"forwardingTargetForSelector:", @"methodSignatureForSelector:", @"forwardInvocation:", @"isBlock", + @"instanceMethodForwarderForSelector:", @"instanceMethodSignatureForSelector:", @"resolveClassMethod:"]; + [NSObject enumerateMethodsInClass:originalMetaClass usingBlock:^(Class cls, SEL sel) { + if((cls == object_getClass([NSObject class])) || (cls == [NSObject class]) || (cls == object_getClass(cls))) + return; + if(OCMIsApplePrivateMethod(cls, sel)) + return; + if([methodBlackList containsObject:NSStringFromSelector(sel)]) + return; + @try + { + [self setupForwarderForClassMethodSelector:sel]; + } + @catch(NSException *e) + { + // ignore for now + } + }]; +} + + +- (void)setupForwarderForClassMethodSelector:(SEL)selector +{ + SEL aliasSelector = OCMAliasForOriginalSelector(selector); + if(class_getClassMethod(mockedClass, aliasSelector) != NULL) + return; + + Method originalMethod = class_getClassMethod(mockedClass, selector); + IMP originalIMP = method_getImplementation(originalMethod); + const char *types = method_getTypeEncoding(originalMethod); + + Class metaClass = object_getClass(mockedClass); + IMP forwarderIMP = [originalMetaClass instanceMethodForwarderForSelector:selector]; + class_addMethod(metaClass, aliasSelector, originalIMP, types); + class_replaceMethod(metaClass, selector, forwarderIMP, types); +} + + +- (void)forwardInvocationForClassObject:(NSInvocation *)anInvocation +{ + // in here "self" is a reference to the real class, not the mock + OCClassMockObject *mock = OCMGetAssociatedMockForClass((Class) self, YES); + if(mock == nil) + { + [NSException raise:NSInternalInconsistencyException format:@"No mock for class %@", NSStringFromClass((Class)self)]; + } + if([mock handleInvocation:anInvocation] == NO) + { + [anInvocation setSelector:OCMAliasForOriginalSelector([anInvocation selector])]; + [anInvocation invoke]; + } +} + +- (void)initializeForClassObject +{ + // we really just want to have an implementation so that the superclass's is not called +} + + +#pragma mark Proxy API + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector +{ + NSMethodSignature *signature = [mockedClass instanceMethodSignatureForSelector:aSelector]; + if(signature == nil) + { + signature = [NSMethodSignature signatureForDynamicPropertyAccessedWithSelector:aSelector inClass:mockedClass]; + } + return signature; +} + +- (Class)mockObjectClass +{ + return [super class]; +} + +- (Class)class +{ + return mockedClass; +} + +- (BOOL)respondsToSelector:(SEL)selector +{ + return [mockedClass instancesRespondToSelector:selector]; +} + +- (BOOL)isKindOfClass:(Class)aClass +{ + return [mockedClass isSubclassOfClass:aClass]; +} + +- (BOOL)conformsToProtocol:(Protocol *)aProtocol +{ + Class clazz = mockedClass; + while (clazz != nil) { + if (class_conformsToProtocol(clazz, aProtocol)) { + return YES; + } + clazz = class_getSuperclass(clazz); + } + return NO; +} + +@end + + +#pragma mark - + +/* + taken from: + `class-dump -f isNS /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator7.0.sdk/System/Library/Frameworks/CoreFoundation.framework` + + @ interface NSObject (__NSIsKinds) + - (_Bool)isNSValue__; + - (_Bool)isNSTimeZone__; + - (_Bool)isNSString__; + - (_Bool)isNSSet__; + - (_Bool)isNSOrderedSet__; + - (_Bool)isNSNumber__; + - (_Bool)isNSDictionary__; + - (_Bool)isNSDate__; + - (_Bool)isNSData__; + - (_Bool)isNSArray__; + */ + +@implementation OCClassMockObject(NSIsKindsImplementation) + +- (BOOL)isNSValue__ +{ + return [mockedClass isSubclassOfClass:[NSValue class]]; +} + +- (BOOL)isNSTimeZone__ +{ + return [mockedClass isSubclassOfClass:[NSTimeZone class]]; +} + +- (BOOL)isNSSet__ +{ + return [mockedClass isSubclassOfClass:[NSSet class]]; +} + +- (BOOL)isNSOrderedSet__ +{ + return [mockedClass isSubclassOfClass:[NSOrderedSet class]]; +} + +- (BOOL)isNSNumber__ +{ + return [mockedClass isSubclassOfClass:[NSNumber class]]; +} + +- (BOOL)isNSDate__ +{ + return [mockedClass isSubclassOfClass:[NSDate class]]; +} + +- (BOOL)isNSString__ +{ + return [mockedClass isSubclassOfClass:[NSString class]]; +} + +- (BOOL)isNSDictionary__ +{ + return [mockedClass isSubclassOfClass:[NSDictionary class]]; +} + +- (BOOL)isNSData__ +{ + return [mockedClass isSubclassOfClass:[NSData class]]; +} + +- (BOOL)isNSArray__ +{ + return [mockedClass isSubclassOfClass:[NSArray class]]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMArg.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMArg.h new file mode 100644 index 0000000000..562804868c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMArg.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2009-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@interface OCMArg : NSObject + +// constraining arguments + ++ (id)any; ++ (SEL)anySelector; ++ (void *)anyPointer; ++ (id __autoreleasing *)anyObjectRef; ++ (id)isNil; ++ (id)isNotNil; ++ (id)isEqual:(id)value; ++ (id)isNotEqual:(id)value; ++ (id)isKindOfClass:(Class)cls; ++ (id)checkWithSelector:(SEL)selector onObject:(id)anObject; ++ (id)checkWithBlock:(BOOL (^)(id obj))block; + +// manipulating arguments + ++ (id *)setTo:(id)value; ++ (void *)setToValue:(NSValue *)value; ++ (id)invokeBlock; ++ (id)invokeBlockWithArgs:(id)first,... NS_REQUIRES_NIL_TERMINATION; + ++ (id)defaultValue; + +// internal use only + ++ (id)resolveSpecialValues:(NSValue *)value; + +@end + +#define OCMOCK_ANY [OCMArg any] + +#if defined(__GNUC__) && !defined(__STRICT_ANSI__) + #define OCMOCK_VALUE(variable) \ + ({ __typeof__(variable) __v = (variable); [NSValue value:&__v withObjCType:@encode(__typeof__(__v))]; }) +#else + #define OCMOCK_VALUE(variable) [NSValue value:&variable withObjCType:@encode(__typeof__(variable))] +#endif + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMArg.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMArg.m new file mode 100644 index 0000000000..2a656584e3 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMArg.m @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2009-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCMArg.h" +#import "OCMBlockArgCaller.h" +#import "OCMConstraint.h" +#import "OCMPassByRefSetter.h" + + +@implementation OCMArg + ++ (id)any +{ + return [OCMAnyConstraint constraint]; +} + ++ (void *)anyPointer +{ + return (void *)0x01234567; +} + ++ (id __autoreleasing *)anyObjectRef +{ + return (id *)0x01234567; +} + ++ (SEL)anySelector +{ + return NSSelectorFromString(@"aSelectorThatMatchesAnySelector"); +} + ++ (id)isNil +{ + return [OCMIsNilConstraint constraint]; +} + ++ (id)isNotNil +{ + return [OCMIsNotNilConstraint constraint]; +} + ++ (id)isEqual:(id)value +{ + return value; +} + ++ (id)isNotEqual:(id)value +{ + OCMIsNotEqualConstraint *constraint = [OCMIsNotEqualConstraint constraint]; + constraint->testValue = value; + return constraint; +} + ++ (id)isKindOfClass:(Class)cls +{ + return [[[OCMBlockConstraint alloc] initWithConstraintBlock:^BOOL(id obj) { + return [obj isKindOfClass:cls]; + }] autorelease]; +} + ++ (id)checkWithSelector:(SEL)selector onObject:(id)anObject +{ + return [OCMConstraint constraintWithSelector:selector onObject:anObject]; +} + ++ (id)checkWithBlock:(BOOL (^)(id))block +{ + return [[[OCMBlockConstraint alloc] initWithConstraintBlock:block] autorelease]; +} + ++ (id *)setTo:(id)value +{ + return (id *)[[[OCMPassByRefSetter alloc] initWithValue:value] autorelease]; +} + ++ (void *)setToValue:(NSValue *)value +{ + return (id *)[[[OCMPassByRefSetter alloc] initWithValue:value] autorelease]; +} + ++ (id)invokeBlock +{ + return [[[OCMBlockArgCaller alloc] init] autorelease]; +} + ++ (id)invokeBlockWithArgs:(id)first,... NS_REQUIRES_NIL_TERMINATION +{ + + NSMutableArray *params = [NSMutableArray array]; + va_list args; + if(first) + { + [params addObject:first]; + va_start(args, first); + id obj; + while((obj = va_arg(args, id))) + { + [params addObject:obj]; + } + va_end(args); + } + return [[[OCMBlockArgCaller alloc] initWithBlockArguments:params] autorelease]; + +} + ++ (id)defaultValue +{ + return [NSNull null]; +} + + ++ (id)resolveSpecialValues:(NSValue *)value +{ + const char *type = [value objCType]; + if(type[0] == '^') + { + void *pointer = [value pointerValue]; + if(pointer == (void *)0x01234567) + return [OCMArg any]; + if((pointer != NULL) && (object_getClass((id)pointer) == [OCMPassByRefSetter class])) + return (id)pointer; + } + else if(type[0] == ':') + { + SEL selector; + [value getValue:&selector]; + if(selector == NSSelectorFromString(@"aSelectorThatMatchesAnySelector")) + return [OCMArg any]; + } + return value; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMArgAction.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMArgAction.h new file mode 100644 index 0000000000..92ce4b5541 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMArgAction.h @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2015-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@interface OCMArgAction : NSObject + +- (void)handleArgument:(id)argument; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMArgAction.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMArgAction.m new file mode 100644 index 0000000000..1c83b12338 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMArgAction.m @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2015-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMArgAction.h" + + +@implementation OCMArgAction + +- (void)handleArgument:(id)argument +{ + +} + + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMBlockArgCaller.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMBlockArgCaller.h new file mode 100644 index 0000000000..934a1f524f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMBlockArgCaller.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2015-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMArgAction.h" + +@interface OCMBlockArgCaller : OCMArgAction +{ + NSArray *arguments; +} + +- (instancetype)initWithBlockArguments:(NSArray *)someArgs; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMBlockArgCaller.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMBlockArgCaller.m new file mode 100644 index 0000000000..9b9a29c623 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMBlockArgCaller.m @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2015-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMBlockArgCaller.h" +#import "NSInvocation+OCMAdditions.h" + + +@implementation OCMBlockArgCaller + +- (instancetype)initWithBlockArguments:(NSArray *)someArgs +{ + self = [super init]; + if(self) + { + arguments = [someArgs copy]; + } + return self; +} + +- (void)dealloc +{ + [arguments release]; + [super dealloc]; +} + +- (id)copyWithZone:(NSZone *)zone +{ + return [self retain]; +} + +- (void)handleArgument:(id)aBlock +{ + if(aBlock) + { + NSInvocation *inv = [NSInvocation invocationForBlock:aBlock withArguments:arguments]; + [inv invokeWithTarget:aBlock]; + } +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMBlockCaller.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMBlockCaller.h new file mode 100644 index 0000000000..44b582f788 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMBlockCaller.h @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2010-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@interface OCMBlockCaller : NSObject +{ + void (^block)(NSInvocation *); +} + +- (id)initWithCallBlock:(void (^)(NSInvocation *))theBlock; + +- (void)handleInvocation:(NSInvocation *)anInvocation; + +@end + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMBlockCaller.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMBlockCaller.m new file mode 100644 index 0000000000..f8e4def075 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMBlockCaller.m @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2010-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMBlockCaller.h" + + +@implementation OCMBlockCaller + +-(id)initWithCallBlock:(void (^)(NSInvocation *))theBlock +{ + if ((self = [super init])) + { + block = [theBlock copy]; + } + + return self; +} + +-(void)dealloc +{ + [block release]; + [super dealloc]; +} + +- (void)handleInvocation:(NSInvocation *)anInvocation +{ + if (block != nil) + { + block(anInvocation); + } +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMBoxedReturnValueProvider.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMBoxedReturnValueProvider.h new file mode 100644 index 0000000000..01b6d2712a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMBoxedReturnValueProvider.h @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2009-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMObjectReturnValueProvider.h" + +@interface OCMBoxedReturnValueProvider : OCMObjectReturnValueProvider +{ +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMBoxedReturnValueProvider.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMBoxedReturnValueProvider.m new file mode 100644 index 0000000000..6e77ec9443 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMBoxedReturnValueProvider.m @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2009-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMBoxedReturnValueProvider.h" +#import "OCMFunctionsPrivate.h" +#import "NSValue+OCMAdditions.h" + + +@implementation OCMBoxedReturnValueProvider + +- (void)handleInvocation:(NSInvocation *)anInvocation +{ + NSUInteger valueSize = 0; + NSValue *returnValueAsNSValue = (NSValue *)returnValue; + NSGetSizeAndAlignment([returnValueAsNSValue objCType], &valueSize, NULL); + char valueBuffer[valueSize]; + [returnValueAsNSValue getValue:valueBuffer]; + + const char *returnType = [[anInvocation methodSignature] methodReturnType]; + + if([self isMethodReturnType:returnType compatibleWithValueType:[returnValueAsNSValue objCType] + value:valueBuffer valueSize:valueSize]) + { + [anInvocation setReturnValue:valueBuffer]; + } + else if([returnValueAsNSValue getBytes:valueBuffer objCType:returnType]) + { + [anInvocation setReturnValue:valueBuffer]; + } + else + { + [NSException raise:NSInvalidArgumentException + format:@"Return value cannot be used for method; method signature declares '%s' but value is '%s'.", returnType, [returnValueAsNSValue objCType]]; + } +} + +- (BOOL)isMethodReturnType:(const char *)returnType compatibleWithValueType:(const char *)valueType value:(const void *)value valueSize:(size_t)valueSize +{ + /* Same types are obviously compatible */ + if(strcmp(returnType, valueType) == 0) + return YES; + + /* Special treatment for nil and Nil */ + if(strcmp(returnType, @encode(id)) == 0 || strcmp(returnType, @encode(Class)) == 0) + return OCMIsNilValue(valueType, value, valueSize); + + return OCMEqualTypesAllowingOpaqueStructs(returnType, valueType); +} + + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMConstraint.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMConstraint.h new file mode 100644 index 0000000000..76235be980 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMConstraint.h @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2007-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@interface OCMConstraint : NSObject + ++ (instancetype)constraint; +- (BOOL)evaluate:(id)value; + +// if you are looking for any, isNil, etc, they have moved to OCMArg + +// try to use [OCMArg checkWith...] instead of the constraintWith... methods below + ++ (instancetype)constraintWithSelector:(SEL)aSelector onObject:(id)anObject; ++ (instancetype)constraintWithSelector:(SEL)aSelector onObject:(id)anObject withValue:(id)aValue; + + +@end + +@interface OCMAnyConstraint : OCMConstraint +@end + +@interface OCMIsNilConstraint : OCMConstraint +@end + +@interface OCMIsNotNilConstraint : OCMConstraint +@end + +@interface OCMIsNotEqualConstraint : OCMConstraint +{ + @public + id testValue; +} + +@end + +@interface OCMInvocationConstraint : OCMConstraint +{ + @public + NSInvocation *invocation; +} + +@end + +@interface OCMBlockConstraint : OCMConstraint +{ + BOOL (^block)(id); +} + +- (instancetype)initWithConstraintBlock:(BOOL (^)(id))block; + +@end + +#ifndef OCM_DISABLE_SHORT_SYNTAX +#define CONSTRAINT(aSelector) [OCMConstraint constraintWithSelector:aSelector onObject:self] +#define CONSTRAINTV(aSelector, aValue) [OCMConstraint constraintWithSelector:aSelector onObject:self withValue:(aValue)] +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMConstraint.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMConstraint.m new file mode 100644 index 0000000000..dccc6c92a3 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMConstraint.m @@ -0,0 +1,157 @@ +#import +/* + * Copyright (c) 2007-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMConstraint.h" + + +@implementation OCMConstraint + ++ (instancetype)constraint +{ + return [[[self alloc] init] autorelease]; +} + +- (BOOL)evaluate:(id)value +{ + return NO; +} + +- (id)copyWithZone:(struct _NSZone *)zone __unused +{ + return [self retain]; +} + ++ (instancetype)constraintWithSelector:(SEL)aSelector onObject:(id)anObject +{ + OCMInvocationConstraint *constraint = [OCMInvocationConstraint constraint]; + NSMethodSignature *signature = [anObject methodSignatureForSelector:aSelector]; + if(signature == nil) + [NSException raise:NSInvalidArgumentException format:@"Unkown selector %@ used in constraint.", NSStringFromSelector(aSelector)]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setTarget:anObject]; + [invocation setSelector:aSelector]; + constraint->invocation = invocation; + return constraint; +} + ++ (instancetype)constraintWithSelector:(SEL)aSelector onObject:(id)anObject withValue:(id)aValue +{ + OCMInvocationConstraint *constraint = (OCMInvocationConstraint *)[self constraintWithSelector:aSelector onObject:anObject]; + if([[constraint->invocation methodSignature] numberOfArguments] < 4) + [NSException raise:NSInvalidArgumentException format:@"Constraint with value requires selector with two arguments."]; + [constraint->invocation setArgument:&aValue atIndex:3]; + return constraint; +} + + +@end + + + +#pragma mark - + +@implementation OCMAnyConstraint + +- (BOOL)evaluate:(id)value +{ + return YES; +} + +@end + + + +#pragma mark - + +@implementation OCMIsNilConstraint + +- (BOOL)evaluate:(id)value +{ + return value == nil; +} + +@end + + + +#pragma mark - + +@implementation OCMIsNotNilConstraint + +- (BOOL)evaluate:(id)value +{ + return value != nil; +} + +@end + + + +#pragma mark - + +@implementation OCMIsNotEqualConstraint + +- (BOOL)evaluate:(id)value +{ + return ![value isEqual:testValue]; +} + +@end + + + +#pragma mark - + +@implementation OCMInvocationConstraint + +- (BOOL)evaluate:(id)value +{ + [invocation setArgument:&value atIndex:2]; // should test if constraint takes arg + [invocation invoke]; + BOOL returnValue; + [invocation getReturnValue:&returnValue]; + return returnValue; +} + +@end + +#pragma mark - + +@implementation OCMBlockConstraint + +- (instancetype)initWithConstraintBlock:(BOOL (^)(id))aBlock +{ + if ((self = [super init])) + { + block = [aBlock copy]; + } + + return self; +} + +- (void)dealloc { + [block release]; + [super dealloc]; +} + +- (BOOL)evaluate:(id)value +{ + return block ? block(value) : NO; +} + + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMExceptionReturnValueProvider.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMExceptionReturnValueProvider.h new file mode 100644 index 0000000000..81160a5bdb --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMExceptionReturnValueProvider.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2009-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMObjectReturnValueProvider.h" + +extern NSString *OCMStubbedException; + +@interface OCMExceptionReturnValueProvider : OCMObjectReturnValueProvider +{ +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMExceptionReturnValueProvider.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMExceptionReturnValueProvider.m new file mode 100644 index 0000000000..d63cd285f3 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMExceptionReturnValueProvider.m @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2009-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMExceptionReturnValueProvider.h" + + +@implementation OCMExceptionReturnValueProvider + +NSString *OCMStubbedException = @"OCMStubbedException"; + + +- (void)handleInvocation:(NSInvocation *)anInvocation +{ + [[NSException exceptionWithName:OCMStubbedException reason:@"Exception stubbed in test." userInfo:@{ @"exception": returnValue }] raise]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMExpectationRecorder.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMExpectationRecorder.h new file mode 100644 index 0000000000..8243146d5c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMExpectationRecorder.h @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2004-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMStubRecorder.h" + +@interface OCMExpectationRecorder : OCMStubRecorder + +- (id)never; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMExpectationRecorder.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMExpectationRecorder.m new file mode 100644 index 0000000000..c5001e262c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMExpectationRecorder.m @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2004-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMExpectationRecorder.h" +#import "OCMockObject.h" +#import "OCMInvocationExpectation.h" + + +@implementation OCMExpectationRecorder + +#pragma mark Initialisers, description, accessors, etc. + +- (id)init +{ + self = [super init]; + [invocationMatcher release]; + invocationMatcher = [[OCMInvocationExpectation alloc] init]; + return self; +} + +- (OCMInvocationExpectation *)expectation +{ + return (OCMInvocationExpectation *)invocationMatcher; +} + + +#pragma mark Modifying the expectation + +- (id)never +{ + [[self expectation] setMatchAndReject:YES]; + return self; +} + + +#pragma mark Finishing recording + +- (void)forwardInvocation:(NSInvocation *)anInvocation +{ + [super forwardInvocation:anInvocation]; + [mockObject addExpectation:[self expectation]]; +} + + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMFunctions.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMFunctions.h new file mode 100644 index 0000000000..16ce0423d3 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMFunctions.h @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2014-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + + +#if defined(__cplusplus) +#define OCMOCK_EXTERN extern "C" +#else +#define OCMOCK_EXTERN extern +#endif + + +OCMOCK_EXTERN BOOL OCMIsObjectType(const char *objCType); diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMFunctions.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMFunctions.m new file mode 100644 index 0000000000..51a553a706 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMFunctions.m @@ -0,0 +1,468 @@ +/* + * Copyright (c) 2014-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCMFunctionsPrivate.h" +#import "OCClassMockObject.h" +#import "OCPartialMockObject.h" +#import "OCMLocation.h" + + +#pragma mark Known private API + +@interface NSException(OCMKnownExceptionMethods) ++ (NSException *)failureInFile:(NSString *)file atLine:(int)line withDescription:(NSString *)formatString, ...; +@end + +@interface NSObject(OCMKnownTestCaseMethods) +- (void)recordFailureWithDescription:(NSString *)description inFile:(NSString *)file atLine:(NSUInteger)line expected:(BOOL)expected; +- (void)failWithException:(NSException *)exception; +@end + + +#pragma mark Functions related to ObjC type system + +const char *OCMTypeWithoutQualifiers(const char *objCType) +{ + while(strchr("rnNoORV", objCType[0]) != NULL) + objCType += 1; + return objCType; +} + + +static BOOL OCMIsUnqualifiedClassType(const char *unqualifiedObjCType) +{ + return (strcmp(unqualifiedObjCType, @encode(Class)) == 0); +} + + +static BOOL OCMIsUnqualifiedBlockType(const char *unqualifiedObjCType) +{ + char blockType[] = @encode(void(^)(void)); + if(strcmp(unqualifiedObjCType, blockType) == 0) + return YES; + + // sometimes block argument/return types are tacked onto the type, in angle brackets + if(strncmp(unqualifiedObjCType, blockType, sizeof(blockType) - 1) == 0 && unqualifiedObjCType[sizeof(blockType) - 1] == '<') + return YES; + + return NO; +} + +BOOL OCMIsClassType(const char *objCType) +{ + return OCMIsUnqualifiedClassType(OCMTypeWithoutQualifiers(objCType)); +} + +BOOL OCMIsBlockType(const char *objCType) +{ + return OCMIsUnqualifiedBlockType(OCMTypeWithoutQualifiers(objCType)); +} + + +BOOL OCMIsObjectType(const char *objCType) +{ + const char *unqualifiedObjCType = OCMTypeWithoutQualifiers(objCType); + + char objectType[] = @encode(id); + if(strcmp(unqualifiedObjCType, objectType) == 0 || OCMIsUnqualifiedClassType(unqualifiedObjCType)) + return YES; + + // sometimes the name of an object's class is tacked onto the type, in double quotes + if(strncmp(unqualifiedObjCType, objectType, sizeof(objectType) - 1) == 0 && unqualifiedObjCType[sizeof(objectType) - 1] == '"') + return YES; + + // if the returnType is a typedef to an object, it has the form ^{OriginClass=#} + NSString *regexString = @"^\\^\\{(.*)=#.*\\}"; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:regexString options:0 error:NULL]; + NSString *type = [NSString stringWithCString:unqualifiedObjCType encoding:NSASCIIStringEncoding]; + if([regex numberOfMatchesInString:type options:0 range:NSMakeRange(0, type.length)] > 0) + return YES; + + // if the return type is a block we treat it like an object + return OCMIsUnqualifiedBlockType(unqualifiedObjCType); +} + + +CFNumberType OCMNumberTypeForObjCType(const char *objcType) +{ + switch (objcType[0]) + { + case 'c': return kCFNumberCharType; + case 'C': return kCFNumberCharType; + case 'B': return kCFNumberCharType; + case 's': return kCFNumberShortType; + case 'S': return kCFNumberShortType; + case 'i': return kCFNumberIntType; + case 'I': return kCFNumberIntType; + case 'l': return kCFNumberLongType; + case 'L': return kCFNumberLongType; + case 'q': return kCFNumberLongLongType; + case 'Q': return kCFNumberLongLongType; + case 'f': return kCFNumberFloatType; + case 'd': return kCFNumberDoubleType; + default: return 0; + } +} + + +static BOOL ParseStructType(const char *type, const char **typeEnd, const char **typeNameEnd, const char **typeEqualSign) +{ + if (type[0] != '{' && type[0] != '(') + return NO; + + *typeNameEnd = NULL; + *typeEqualSign = NULL; + + const char endChar = type[0] == '{' ? '}' : ')'; + for (const char* ptr = type + 1; *ptr; ++ptr) { + switch (*ptr) { + case '(': + case '{': + { + const char *subTypeEnd; + const char *subTypeNameEnd; + const char *subTypeEqualSign; + if (!ParseStructType(ptr, &subTypeEnd, &subTypeNameEnd, &subTypeEqualSign)) + return NO; + ptr = subTypeEnd; + break; + } + case '=': + { + if (!*typeEqualSign) { + *typeNameEnd = ptr; + *typeEqualSign = ptr; + } + break; + } + case ')': + case '}': + { + if (*ptr == endChar) { + *typeEnd = ptr; + if (!*typeNameEnd) + *typeNameEnd = ptr; + return YES; + } + break; + } + default: + break; + } + } + + return NO; +} + + +/* + * Sometimes an external type is an opaque struct (which will have an @encode of "{structName}" + * or "{structName=}") but the actual method return type, or property type, will know the contents + * of the struct (so will have an objcType of say "{structName=iiSS}". This function will determine + * those are equal provided they have the same structure name, otherwise everything else will be + * compared textually. This can happen particularly for pointers to such structures, which still + * encode what is being pointed to. + * + * In addition, this funtion will consider structures with unknown names, encoded as "{?=}, equal to + * structures with any name. This means that "{?=dd}" and "{foo=dd}", and even "{?=}" and "{foo=dd}", + * are considered equal. + * + * For some types some runtime functions throw exceptions, which is why we wrap this in an + * exception handler just below. + */ +static BOOL OCMEqualTypesAllowingOpaqueStructsInternal(const char *type1, const char *type2) +{ + type1 = OCMTypeWithoutQualifiers(type1); + type2 = OCMTypeWithoutQualifiers(type2); + + switch (type1[0]) + { + case '{': + case '(': + { + if (type2[0] != type1[0]) + return NO; + + const char *type1End; + const char *type1NameEnd; + const char *type1EqualSign; + if (!ParseStructType(type1, &type1End, &type1NameEnd, &type1EqualSign)) + return NO; + + const char *type2End; + const char *type2NameEnd; + const char *type2EqualSign; + if (!ParseStructType(type2, &type2End, &type2NameEnd, &type2EqualSign)) + return NO; + + /* Opaque types either don't have an equals sign (just the name and the end brace), or + * empty content after the equals sign. + * We want that to compare the same as a type of the same name but with the content. + */ + BOOL type1Opaque = (type1EqualSign == NULL || type1EqualSign + 1 == type1End); + BOOL type2Opaque = (type2EqualSign == NULL || type2EqualSign + 2 == type2End); + intptr_t type1NameLen = type1NameEnd - type1; + intptr_t type2NameLen = type2NameEnd - type2; + + /* If the names are not equal and neither of the names is a question mark, return NO */ + if ((type1NameLen != type2NameLen || strncmp(type1, type2, type1NameLen)) && + !((type1NameLen == 2) && (type1[1] == '?')) && !((type2NameLen == 2) && (type2[1] == '?')) && + !(type1NameLen == 1 || type2NameLen == 1)) + return NO; + + /* If the same name, and at least one is opaque, that is close enough. */ + if (type1Opaque || type2Opaque) + return YES; + + /* Otherwise, compare all the elements. Use NSGetSizeAndAlignment to walk through the struct elements. */ + type1 = type1EqualSign + 1; + type2 = type2EqualSign + 1; + while (type1 != type1End && *type1) + { + if (!OCMEqualTypesAllowingOpaqueStructs(type1, type2)) + return NO; + + if (*type1 != '{' && *type1 != '(') { + type1 = NSGetSizeAndAlignment(type1, NULL, NULL); + type2 = NSGetSizeAndAlignment(type2, NULL, NULL); + } else { + const char *subType1End; + const char *subType1NameEnd; + const char *subType1EqualSign; + if (!ParseStructType(type1, &subType1End, &subType1NameEnd, &subType1EqualSign)) + return NO; + + const char *subType2End; + const char *subType2NameEnd; + const char *subType2EqualSign; + if (!ParseStructType(type2, &subType2End, &subType2NameEnd, &subType2EqualSign)) + return NO; + + type1 = subType1End + 1; + type2 = subType2End + 1; + } + } + return YES; + } + case '^': + /* for a pointer, make sure the other is a pointer, then recursively compare the rest */ + if (type2[0] != type1[0]) + return NO; + return OCMEqualTypesAllowingOpaqueStructs(type1 + 1, type2 + 1); + + case '?': + return type2[0] == '?'; + + case '\0': + return type2[0] == '\0'; + + default: + { + // Move the type pointers past the current types, then compare that region + const char *afterType1 = NSGetSizeAndAlignment(type1, NULL, NULL); + const char *afterType2 = NSGetSizeAndAlignment(type2, NULL, NULL); + intptr_t type1Len = afterType1 - type1; + intptr_t type2Len = afterType2 - type2; + + return (type1Len == type2Len && (strncmp(type1, type2, type1Len) == 0)); + } + } +} + +BOOL OCMEqualTypesAllowingOpaqueStructs(const char *type1, const char *type2) +{ + @try + { + return OCMEqualTypesAllowingOpaqueStructsInternal(type1, type2); + } + @catch (NSException *e) + { + /* Probably a bitfield or something that NSGetSizeAndAlignment chokes on, oh well */ + return NO; + } +} + +BOOL OCMIsNilValue(const char *objectCType, const void *value, size_t valueSize) +{ + // First, check value itself + for(size_t i = 0; i < valueSize; i++) + if(((const char *)value)[i] != 0) + return NO; + + // Depending on the compilation settings of the file where the return value gets recorded, + // nil and Nil get potentially different encodings. Check all known encodings. + if((strcmp(objectCType, @encode(void *)) == 0) || // Standard Objective-C + (strcmp(objectCType, @encode(int)) == 0) || // 32 bit C++ (before nullptr) + (strcmp(objectCType, @encode(long long)) == 0) || // 64 bit C++ (before nullptr) + (strcmp(objectCType, @encode(char *)) == 0)) // C++ with nullptr + return YES; + + return NO; +} + + +BOOL OCMIsAppleBaseClass(Class cls) +{ + return (cls == [NSObject class]) || (cls == [NSProxy class]); +} + +BOOL OCMIsApplePrivateMethod(Class cls, SEL sel) +{ + NSString *className = NSStringFromClass(cls); + NSString *selName = NSStringFromSelector(sel); + return ([className hasPrefix:@"NS"] || [className hasPrefix:@"UI"]) && + ([selName hasPrefix:@"_"] || [selName hasSuffix:@"_"]); +} + + +BOOL OCMIsNonEscapingBlock(id block) +{ + struct OCMBlockDef *blockRef = (__bridge struct OCMBlockDef *)block; + return (blockRef->flags & OCMBlockIsNoEscape) != 0; +} + + +#pragma mark Creating and disposing classes + +static NSString *const OCMSubclassPrefix = @"OCMock_"; + +Class OCMCreateSubclass(Class class, void *ref) +{ + const char *className = [[NSString stringWithFormat:@"%@%@-%p-%u", OCMSubclassPrefix, NSStringFromClass(class), ref, arc4random()] UTF8String]; + Class subclass = objc_allocateClassPair(class, className, 0); + objc_registerClassPair(subclass); + return subclass; +} + +BOOL OCMIsMockSubclass(Class cls) +{ + return [NSStringFromClass(cls) hasPrefix:OCMSubclassPrefix]; +} + +void OCMDisposeSubclass(Class cls) +{ + if(!OCMIsMockSubclass(cls)) + { + [NSException raise:NSInvalidArgumentException format:@"Not a mock subclass; found %@\nThe subclass dynamically created by OCMock has been replaced by another class. This can happen when KVO or CoreData create their own dynamic subclass after OCMock created its subclass.\nYou will need to reorder initialization and/or teardown so that classes are created and disposed of in the right order.", NSStringFromClass(cls)]; + } + objc_disposeClassPair(cls); +} + + +#pragma mark Alias for renaming real methods + +static NSString *const OCMRealMethodAliasPrefix = @"ocmock_replaced_"; +static const char *const OCMRealMethodAliasPrefixCString = "ocmock_replaced_"; + +BOOL OCMIsAliasSelector(SEL selector) +{ + return [NSStringFromSelector(selector) hasPrefix:OCMRealMethodAliasPrefix]; +} + +SEL OCMAliasForOriginalSelector(SEL selector) +{ + char aliasName[2048]; + const char *originalName = sel_getName(selector); + strlcpy(aliasName, OCMRealMethodAliasPrefixCString, sizeof(aliasName)); + strlcat(aliasName, originalName, sizeof(aliasName)); + return sel_registerName(aliasName); +} + +SEL OCMOriginalSelectorForAlias(SEL selector) +{ + if(!OCMIsAliasSelector(selector)) + [NSException raise:NSInvalidArgumentException format:@"Not an alias selector; found %@", NSStringFromSelector(selector)]; + NSString *string = NSStringFromSelector(selector); + return NSSelectorFromString([string substringFromIndex:[OCMRealMethodAliasPrefix length]]); +} + + +#pragma mark Wrappers around associative references + +static NSString *const OCMClassMethodMockObjectKey = @"OCMClassMethodMockObjectKey"; + +void OCMSetAssociatedMockForClass(OCClassMockObject *mock, Class aClass) +{ + if((mock != nil) && (objc_getAssociatedObject(aClass, OCMClassMethodMockObjectKey) != nil)) + [NSException raise:NSInternalInconsistencyException format:@"Another mock is already associated with class %@", NSStringFromClass(aClass)]; + objc_setAssociatedObject(aClass, OCMClassMethodMockObjectKey, mock, OBJC_ASSOCIATION_ASSIGN); +} + +OCClassMockObject *OCMGetAssociatedMockForClass(Class aClass, BOOL includeSuperclasses) +{ + OCClassMockObject *mock = nil; + do + { + mock = objc_getAssociatedObject(aClass, OCMClassMethodMockObjectKey); + aClass = class_getSuperclass(aClass); + } + while((mock == nil) && (aClass != nil) && includeSuperclasses); + return mock; +} + +static NSString *const OCMPartialMockObjectKey = @"OCMPartialMockObjectKey"; + +void OCMSetAssociatedMockForObject(OCClassMockObject *mock, id anObject) +{ + if((mock != nil) && (objc_getAssociatedObject(anObject, OCMPartialMockObjectKey) != nil)) + [NSException raise:NSInternalInconsistencyException format:@"Another mock is already associated with object %@", anObject]; + objc_setAssociatedObject(anObject, OCMPartialMockObjectKey, mock, OBJC_ASSOCIATION_ASSIGN); +} + +OCPartialMockObject *OCMGetAssociatedMockForObject(id anObject) +{ + return objc_getAssociatedObject(anObject, OCMPartialMockObjectKey); +} + + +#pragma mark Functions related to IDE error reporting + +void OCMReportFailure(OCMLocation *loc, NSString *description) +{ + id testCase = [loc testCase]; + if((testCase != nil) && [testCase respondsToSelector:@selector(recordFailureWithDescription:inFile:atLine:expected:)]) + { + [testCase recordFailureWithDescription:description inFile:[loc file] atLine:[loc line] expected:NO]; + } + else if((testCase != nil) && [testCase respondsToSelector:@selector(failWithException:)]) + { + NSException *exception = nil; + if([NSException instancesRespondToSelector:@selector(failureInFile:atLine:withDescription:)]) + { + exception = [NSException failureInFile:[loc file] atLine:(int)[loc line] withDescription:description]; + } + else + { + NSString *reason = [NSString stringWithFormat:@"%@:%lu %@", [loc file], (unsigned long)[loc line], description]; + exception = [NSException exceptionWithName:@"OCMockTestFailure" reason:reason userInfo:nil]; + } + [testCase failWithException:exception]; + } + else if(loc != nil) + { + NSLog(@"%@:%lu %@", [loc file], (unsigned long)[loc line], description); + NSString *reason = [NSString stringWithFormat:@"%@:%lu %@", [loc file], (unsigned long)[loc line], description]; + [[NSException exceptionWithName:@"OCMockTestFailure" reason:reason userInfo:nil] raise]; + + } + else + { + NSLog(@"%@", description); + [[NSException exceptionWithName:@"OCMockTestFailure" reason:description userInfo:nil] raise]; + } + +} diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMFunctionsPrivate.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMFunctionsPrivate.h new file mode 100644 index 0000000000..fdf8e41baf --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMFunctionsPrivate.h @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2014-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@class OCMLocation; +@class OCClassMockObject; +@class OCPartialMockObject; + + +BOOL OCMIsClassType(const char *objCType); +BOOL OCMIsBlockType(const char *objCType); +BOOL OCMIsObjectType(const char *objCType); +const char *OCMTypeWithoutQualifiers(const char *objCType); +BOOL OCMEqualTypesAllowingOpaqueStructs(const char *type1, const char *type2); +CFNumberType OCMNumberTypeForObjCType(const char *objcType); +BOOL OCMIsNilValue(const char *objectCType, const void *value, size_t valueSize); + +BOOL OCMIsAppleBaseClass(Class cls); +BOOL OCMIsApplePrivateMethod(Class cls, SEL sel); + +Class OCMCreateSubclass(Class cls, void *ref); +BOOL OCMIsMockSubclass(Class cls); +void OCMDisposeSubclass(Class cls); + +BOOL OCMIsAliasSelector(SEL selector); +SEL OCMAliasForOriginalSelector(SEL selector); +SEL OCMOriginalSelectorForAlias(SEL selector); + +void OCMSetAssociatedMockForClass(OCClassMockObject *mock, Class aClass); +OCClassMockObject *OCMGetAssociatedMockForClass(Class aClass, BOOL includeSuperclasses); + +void OCMSetAssociatedMockForObject(OCClassMockObject *mock, id anObject); +OCPartialMockObject *OCMGetAssociatedMockForObject(id anObject); + +void OCMReportFailure(OCMLocation *loc, NSString *description); + +BOOL OCMIsNonEscapingBlock(id block); + + + +struct OCMBlockDef +{ + void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock + int flags; + int reserved; + void (*invoke)(void *, ...); + struct block_descriptor { + unsigned long int reserved; // NULL + unsigned long int size; // sizeof(struct Block_literal_1) + // optional helper functions + void (*copy_helper)(void *dst, void *src); // IFF (1<<25) + void (*dispose_helper)(void *src); // IFF (1<<25) + // required ABI.2010.3.16 + const char *signature; // IFF (1<<30) + } *descriptor; +}; + +enum +{ + OCMBlockIsNoEscape = (1 << 23), + OCMBlockDescriptionFlagsHasCopyDispose = (1 << 25), + OCMBlockDescriptionFlagsHasSignature = (1 << 30) +}; + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMIndirectReturnValueProvider.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMIndirectReturnValueProvider.h new file mode 100644 index 0000000000..f02ae96567 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMIndirectReturnValueProvider.h @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2009-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@interface OCMIndirectReturnValueProvider : NSObject +{ + id provider; + SEL selector; +} + +- (id)initWithProvider:(id)aProvider andSelector:(SEL)aSelector; + +- (void)handleInvocation:(NSInvocation *)anInvocation; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMIndirectReturnValueProvider.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMIndirectReturnValueProvider.m new file mode 100644 index 0000000000..a74e7294db --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMIndirectReturnValueProvider.m @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2009-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMIndirectReturnValueProvider.h" + + +@implementation OCMIndirectReturnValueProvider + +- (id)initWithProvider:(id)aProvider andSelector:(SEL)aSelector +{ + if ((self = [super init])) + { + provider = [aProvider retain]; + selector = aSelector; + } + + return self; +} + +- (void)dealloc +{ + [provider release]; + [super dealloc]; +} + +- (void)handleInvocation:(NSInvocation *)anInvocation +{ + id originalTarget = [anInvocation target]; + SEL originalSelector = [anInvocation selector]; + + [anInvocation setTarget:provider]; + [anInvocation setSelector:selector]; + [anInvocation invoke]; + + [anInvocation setTarget:originalTarget]; + [anInvocation setSelector:originalSelector]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMInvocationExpectation.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMInvocationExpectation.h new file mode 100644 index 0000000000..aa53f1e707 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMInvocationExpectation.h @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2014-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMInvocationStub.h" + +@interface OCMInvocationExpectation : OCMInvocationStub +{ + BOOL matchAndReject; + BOOL isSatisfied; +} + +- (void)setMatchAndReject:(BOOL)flag; +- (BOOL)isMatchAndReject; + +- (BOOL)isSatisfied; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMInvocationExpectation.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMInvocationExpectation.m new file mode 100644 index 0000000000..068581be09 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMInvocationExpectation.m @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2014-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMInvocationExpectation.h" +#import "NSInvocation+OCMAdditions.h" + + +@implementation OCMInvocationExpectation + +- (void)setMatchAndReject:(BOOL)flag +{ + matchAndReject = flag; + if(matchAndReject) + isSatisfied = YES; +} + +- (BOOL)isMatchAndReject +{ + return matchAndReject; +} + +- (BOOL)isSatisfied +{ + return isSatisfied; +} + +- (void)addInvocationAction:(id)anAction +{ + if(matchAndReject) + { + [NSException raise:NSInternalInconsistencyException format:@"%@: cannot add action to a reject stub; got %@", + [self description], anAction]; + } + [super addInvocationAction:anAction]; +} + +- (void)handleInvocation:(NSInvocation *)anInvocation +{ + if(matchAndReject) + { + isSatisfied = NO; + [NSException raise:NSInternalInconsistencyException format:@"%@: explicitly disallowed method invoked: %@", + [self description], [anInvocation invocationDescription]]; + } + else + { + [super handleInvocation:anInvocation]; + isSatisfied = YES; + } +} + + + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMInvocationMatcher.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMInvocationMatcher.h new file mode 100644 index 0000000000..c98dcd89e1 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMInvocationMatcher.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2014-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@interface OCMInvocationMatcher : NSObject +{ + NSInvocation *recordedInvocation; + BOOL recordedAsClassMethod; + BOOL ignoreNonObjectArgs; +} + +- (void)setInvocation:(NSInvocation *)anInvocation; +- (NSInvocation *)recordedInvocation; + +- (void)setRecordedAsClassMethod:(BOOL)flag; +- (BOOL)recordedAsClassMethod; + +- (void)setIgnoreNonObjectArgs:(BOOL)flag; + +- (BOOL)matchesSelector:(SEL)aSelector; +- (BOOL)matchesInvocation:(NSInvocation *)anInvocation; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMInvocationMatcher.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMInvocationMatcher.m new file mode 100644 index 0000000000..fb621e6ec7 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMInvocationMatcher.m @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2014-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCMInvocationMatcher.h" +#import "OCMArg.h" +#import "OCMConstraint.h" +#import "OCMPassByRefSetter.h" +#import "OCMFunctionsPrivate.h" +#import "NSInvocation+OCMAdditions.h" + + +@interface NSObject(HCMatcherDummy) +- (BOOL)matches:(id)item; +@end + + +@implementation OCMInvocationMatcher + +- (void)dealloc +{ + [recordedInvocation release]; + [super dealloc]; +} + +- (void)setInvocation:(NSInvocation *)anInvocation +{ + [recordedInvocation release]; + // Don't do a regular -retainArguments on the invocation that we use for matching. NSInvocation + // effectively does an strcpy on char* arguments which messes up matching them literally and blows + // up with anyPointer (in strlen since it's not actually a C string). Also on the off-chance that + // anInvocation contains self as an argument, -retainArguments would create a retain cycle. + [anInvocation retainObjectArgumentsExcludingObject:self]; + recordedInvocation = [anInvocation retain]; +} + +- (void)setRecordedAsClassMethod:(BOOL)flag +{ + recordedAsClassMethod = flag; +} + +- (BOOL)recordedAsClassMethod +{ + return recordedAsClassMethod; +} + +- (void)setIgnoreNonObjectArgs:(BOOL)flag +{ + ignoreNonObjectArgs = flag; +} + +- (NSString *)description +{ + return [recordedInvocation invocationDescription]; +} + +- (NSInvocation *)recordedInvocation +{ + return recordedInvocation; +} + +- (BOOL)matchesSelector:(SEL)sel +{ + if(sel == [recordedInvocation selector]) + return YES; + if(OCMIsAliasSelector(sel) && + OCMOriginalSelectorForAlias(sel) == [recordedInvocation selector]) + return YES; + + return NO; +} + +- (BOOL)matchesInvocation:(NSInvocation *)anInvocation +{ + id target = [anInvocation target]; + BOOL isClassMethodInvocation = (target != nil) && (target == [target class]); + if(isClassMethodInvocation != recordedAsClassMethod) + return NO; + + if(![self matchesSelector:[anInvocation selector]]) + return NO; + + NSMethodSignature *signature = [recordedInvocation methodSignature]; + NSUInteger n = [signature numberOfArguments]; + for(NSUInteger i = 2; i < n; i++) + { + if(ignoreNonObjectArgs && !OCMIsObjectType([signature getArgumentTypeAtIndex:i])) + { + continue; + } + + id recordedArg = [recordedInvocation getArgumentAtIndexAsObject:i]; + id passedArg = [anInvocation getArgumentAtIndexAsObject:i]; + + if([recordedArg isProxy]) + { + if(![recordedArg isEqual:passedArg]) + return NO; + continue; + } + + if([recordedArg isKindOfClass:[NSValue class]]) + recordedArg = [OCMArg resolveSpecialValues:recordedArg]; + + if([recordedArg isKindOfClass:[OCMConstraint class]]) + { + if([recordedArg evaluate:passedArg] == NO) + return NO; + } + else if([recordedArg isKindOfClass:[OCMArgAction class]]) + { + // ignore, will be dealt with in handleInvocation: where applicable + } + else if([recordedArg conformsToProtocol:objc_getProtocol("HCMatcher")]) + { + if([recordedArg matches:passedArg] == NO) + return NO; + } + else + { + if(([recordedArg class] == [NSNumber class]) && + ([(NSNumber*)recordedArg compare:(NSNumber*)passedArg] != NSOrderedSame)) + return NO; + if(([recordedArg isEqual:passedArg] == NO) && + !((recordedArg == nil) && (passedArg == nil))) + return NO; + } + } + return YES; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMInvocationStub.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMInvocationStub.h new file mode 100644 index 0000000000..183e442e73 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMInvocationStub.h @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2014-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMInvocationMatcher.h" + +@interface OCMInvocationStub : OCMInvocationMatcher +{ + NSMutableArray *invocationActions; +} + +- (void)addInvocationAction:(id)anAction; +- (NSArray *)invocationActions; + +- (void)handleInvocation:(NSInvocation *)anInvocation; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMInvocationStub.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMInvocationStub.m new file mode 100644 index 0000000000..fd8ecafc0f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMInvocationStub.m @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2014-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMInvocationStub.h" +#import "OCMArg.h" +#import "OCMArgAction.h" +#import "NSInvocation+OCMAdditions.h" + +#define UNSET_RETURN_VALUE_MARKER ((id)0x01234567) + + +@implementation OCMInvocationStub + +- (id)init +{ + self = [super init]; + invocationActions = [[NSMutableArray alloc] init]; + return self; +} + +- (void)dealloc +{ + [invocationActions release]; + [super dealloc]; +} + + +- (void)addInvocationAction:(id)anAction +{ + [invocationActions addObject:anAction]; +} + +- (NSArray *)invocationActions +{ + return invocationActions; +} + + +- (void)handleInvocation:(NSInvocation *)anInvocation +{ + [self invokeArgActionsForInvocation:anInvocation]; + + if([anInvocation methodIsInInitFamily]) + { + id returnVal = UNSET_RETURN_VALUE_MARKER; + [anInvocation setReturnValue:&returnVal]; + + [self invokeActionsForInvocation:anInvocation]; + + [anInvocation getReturnValue:&returnVal]; + if(returnVal == UNSET_RETURN_VALUE_MARKER) + { + [NSException raise:NSInvalidArgumentException format:@"%@ was stubbed but no return value set. A return value is required for an init method. If you intended to return nil, make this explicit with .andReturn(nil)", NSStringFromSelector([anInvocation selector])]; + } + } + else + { + [self invokeActionsForInvocation:anInvocation]; + } +} + +- (void)invokeArgActionsForInvocation:(NSInvocation *)anInvocation +{ + NSMethodSignature *signature = [recordedInvocation methodSignature]; + NSUInteger n = [signature numberOfArguments]; + for(NSUInteger i = 2; i < n; i++) + { + id recordedArg = [recordedInvocation getArgumentAtIndexAsObject:i]; + id passedArg = [anInvocation getArgumentAtIndexAsObject:i]; + + if([recordedArg isProxy]) + continue; + + if([recordedArg isKindOfClass:[NSValue class]]) + recordedArg = [OCMArg resolveSpecialValues:recordedArg]; + + if([recordedArg isKindOfClass:[OCMArgAction class]]) + [recordedArg handleArgument:passedArg]; + } +} + +- (void)invokeActionsForInvocation:(NSInvocation *)anInvocation +{ + [invocationActions makeObjectsPerformSelector:@selector(handleInvocation:) withObject:anInvocation]; +} + + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMLocation.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMLocation.h new file mode 100644 index 0000000000..1aa6df4062 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMLocation.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2014-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import + +@interface OCMLocation : NSObject +{ + id testCase; + NSString *file; + NSUInteger line; +} + ++ (instancetype)locationWithTestCase:(id)aTestCase file:(NSString *)aFile line:(NSUInteger)aLine; + +- (instancetype)initWithTestCase:(id)aTestCase file:(NSString *)aFile line:(NSUInteger)aLine; + +- (id)testCase; +- (NSString *)file; +- (NSUInteger)line; + +@end + +OCMOCK_EXTERN OCMLocation *OCMMakeLocation(id testCase, const char *file, int line); diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMLocation.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMLocation.m new file mode 100644 index 0000000000..959bc198e7 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMLocation.m @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2014-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMLocation.h" + + +@implementation OCMLocation + ++ (instancetype)locationWithTestCase:(id)aTestCase file:(NSString *)aFile line:(NSUInteger)aLine +{ + return [[[OCMLocation alloc] initWithTestCase:aTestCase file:aFile line:aLine] autorelease]; +} + +- (instancetype)initWithTestCase:(id)aTestCase file:(NSString *)aFile line:(NSUInteger)aLine +{ + if ((self = [super init])) + { + testCase = aTestCase; + file = [aFile retain]; + line = aLine; + } + + return self; +} + +- (void)dealloc +{ + [file release]; + [super dealloc]; +} + +- (id)testCase +{ + return testCase; +} + +- (NSString *)file +{ + return file; +} + +- (NSUInteger)line +{ + return line; +} + +@end + + +OCMLocation *OCMMakeLocation(id testCase, const char *fileCString, int line) +{ + return [OCMLocation locationWithTestCase:testCase file:[NSString stringWithUTF8String:fileCString] line:line]; +} + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMMacroState.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMMacroState.h new file mode 100644 index 0000000000..a5dda11a44 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMMacroState.h @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2014-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@class OCMLocation; +@class OCMQuantifier; +@class OCMRecorder; +@class OCMStubRecorder; +@class OCMockObject; + + +@interface OCMMacroState : NSObject +{ + id recorder; + BOOL invocationDidThrow; +} + ++ (void)beginStubMacro; ++ (OCMStubRecorder *)endStubMacro; + ++ (void)beginExpectMacro; ++ (OCMStubRecorder *)endExpectMacro; + ++ (void)beginRejectMacro; ++ (OCMStubRecorder *)endRejectMacro; + ++ (void)beginVerifyMacroAtLocation:(OCMLocation *)aLocation; ++ (void)beginVerifyMacroAtLocation:(OCMLocation *)aLocation withQuantifier:(OCMQuantifier *)quantifier; ++ (void)endVerifyMacro; + ++ (OCMMacroState *)globalState; + +- (void)setRecorder:(id)aRecorder; +- (id)recorder; + +- (void)switchToClassMethod; + +- (void)setInvocationDidThrow:(BOOL)flag; +- (BOOL)invocationDidThrow; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMMacroState.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMMacroState.m new file mode 100644 index 0000000000..a40667cca6 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMMacroState.m @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2014-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMMacroState.h" +#import "OCMExpectationRecorder.h" +#import "OCMVerifier.h" + + +@implementation OCMMacroState + +static NSString *const OCMGlobalStateKey = @"OCMGlobalStateKey"; + +#pragma mark Methods to begin/end macros + ++ (void)beginStubMacro +{ + OCMStubRecorder *recorder = [[[OCMStubRecorder alloc] init] autorelease]; + OCMMacroState *macroState = [[OCMMacroState alloc] initWithRecorder:recorder]; + [NSThread currentThread].threadDictionary[OCMGlobalStateKey] = macroState; + [macroState release]; +} + ++ (OCMStubRecorder *)endStubMacro +{ + NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary; + OCMMacroState *globalState = threadDictionary[OCMGlobalStateKey]; + OCMStubRecorder *recorder = [[(OCMStubRecorder *)[globalState recorder] retain] autorelease]; + BOOL didThrow = [globalState invocationDidThrow]; + [threadDictionary removeObjectForKey:OCMGlobalStateKey]; + if(didThrow == NO && [recorder didRecordInvocation] == NO) + { + [NSException raise:NSInternalInconsistencyException + format:@"Did not record an invocation in OCMStub/OCMExpect/OCMReject.\n" + @"Possible causes are:\n" + @"- The receiver is not a mock object.\n" + @"- The selector conflicts with a selector implemented by OCMStubRecorder/OCMExpectationRecorder."]; + } + return recorder; +} + + ++ (void)beginExpectMacro +{ + OCMExpectationRecorder *recorder = [[[OCMExpectationRecorder alloc] init] autorelease]; + OCMMacroState *macroState = [[OCMMacroState alloc] initWithRecorder:recorder]; + [NSThread currentThread].threadDictionary[OCMGlobalStateKey] = macroState; + [macroState release]; +} + ++ (OCMStubRecorder *)endExpectMacro +{ + return [self endStubMacro]; +} + + ++ (void)beginRejectMacro +{ + OCMExpectationRecorder *recorder = [[[OCMExpectationRecorder alloc] init] autorelease]; + OCMMacroState *macroState = [[OCMMacroState alloc] initWithRecorder:recorder]; + [NSThread currentThread].threadDictionary[OCMGlobalStateKey] = macroState; + [macroState release]; +} + ++ (OCMStubRecorder *)endRejectMacro +{ + OCMMacroState *globalState = [NSThread currentThread].threadDictionary[OCMGlobalStateKey]; + // Calling never after the invocation to avoid running afoul of ARC's expectations on + // return values from init methods. + [(OCMExpectationRecorder *)[globalState recorder] never]; + return [self endStubMacro]; +} + + ++ (void)beginVerifyMacroAtLocation:(OCMLocation *)aLocation +{ + return [self beginVerifyMacroAtLocation:aLocation withQuantifier:nil]; +} + ++ (void)beginVerifyMacroAtLocation:(OCMLocation *)aLocation withQuantifier:(OCMQuantifier *)quantifier +{ + OCMVerifier *recorder = [[[OCMVerifier alloc] init] autorelease]; + [recorder setLocation:aLocation]; + [recorder setQuantifier:quantifier]; + OCMMacroState *macroState = [[OCMMacroState alloc] initWithRecorder:recorder]; + [NSThread currentThread].threadDictionary[OCMGlobalStateKey] = macroState; + [macroState release]; +} + ++ (void)endVerifyMacro +{ + NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary; + OCMMacroState *globalState = threadDictionary[OCMGlobalStateKey]; + OCMVerifier *verifier = [[(OCMVerifier *)[globalState recorder] retain] autorelease]; + BOOL didThrow = [globalState invocationDidThrow]; + [threadDictionary removeObjectForKey:OCMGlobalStateKey]; + if(didThrow == NO && [verifier didRecordInvocation] == NO) + { + [NSException raise:NSInternalInconsistencyException + format:@"Did not record an invocation in OCMVerify.\n" + @"Possible causes are:\n" + @"- The receiver is not a mock object.\n" + @"- The selector conflicts with a selector implemented by OCMVerifier."]; + } +} + + +#pragma mark Accessing global state + ++ (OCMMacroState *)globalState +{ + return [NSThread currentThread].threadDictionary[OCMGlobalStateKey]; +} + + +#pragma mark Init, dealloc, accessors + +- (id)initWithRecorder:(OCMRecorder *)aRecorder +{ + if((self = [super init])) + { + recorder = [aRecorder retain]; + } + + return self; +} + +- (void)dealloc +{ + [recorder release]; + if([NSThread currentThread].threadDictionary[OCMGlobalStateKey] == self) + [NSException raise:NSInternalInconsistencyException format:@"Unexpected dealloc while set as the global state"]; + [super dealloc]; +} + +- (void)setRecorder:(OCMRecorder *)aRecorder +{ + [recorder autorelease]; + recorder = [aRecorder retain]; +} + +- (OCMRecorder *)recorder +{ + return recorder; +} + +- (void)setInvocationDidThrow:(BOOL)flag +{ + invocationDidThrow = flag; +} + +- (BOOL)invocationDidThrow +{ + return invocationDidThrow; +} + + +#pragma mark Changing the recorder + +- (void)switchToClassMethod +{ + [recorder classMethod]; +} + + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMNonRetainingObjectReturnValueProvider.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMNonRetainingObjectReturnValueProvider.h new file mode 100644 index 0000000000..1d9b8f7055 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMNonRetainingObjectReturnValueProvider.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2019-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@interface OCMNonRetainingObjectReturnValueProvider : NSObject +{ + id returnValue; +} + +- (instancetype)initWithValue:(id)aValue; + +- (void)handleInvocation:(NSInvocation *)anInvocation; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMNonRetainingObjectReturnValueProvider.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMNonRetainingObjectReturnValueProvider.m new file mode 100644 index 0000000000..b3f182884f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMNonRetainingObjectReturnValueProvider.m @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2019-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMNonRetainingObjectReturnValueProvider.h" +#import "OCMFunctions.h" +#import "NSInvocation+OCMAdditions.h" + + +@implementation OCMNonRetainingObjectReturnValueProvider + +- (instancetype)initWithValue:(id)aValue +{ + if ((self = [super init])) + returnValue = aValue; + return self; +} + +- (void)handleInvocation:(NSInvocation *)anInvocation +{ + if(!OCMIsObjectType([[anInvocation methodSignature] methodReturnType])) + { + @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Expected invocation with object return type. Did you mean to use andReturnValue: instead?" userInfo:nil]; + } + + if([anInvocation methodIsInAllocFamily] || [anInvocation methodIsInNewFamily] || + [anInvocation methodIsInCopyFamily] || [anInvocation methodIsInMutableCopyFamily]) + { + // methods that "create" an object return it with an extra retain count + [returnValue retain]; + } + else if([anInvocation methodIsInInitFamily]) + { + // init family methods "consume" self and retain their return value. Do the retain first in case the return value and self are the same. + [returnValue retain]; + [[anInvocation target] release]; + } + else + { + // avoid potential problems with the return value being release too early + returnValue = [[returnValue retain] autorelease]; + } + [anInvocation setReturnValue:&returnValue]; +} +@end + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMNotificationPoster.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMNotificationPoster.h new file mode 100644 index 0000000000..20c750b768 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMNotificationPoster.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2009-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@interface OCMNotificationPoster : NSObject +{ + NSNotification *notification; +} + +- (id)initWithNotification:(id)aNotification; + +- (void)handleInvocation:(NSInvocation *)anInvocation; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMNotificationPoster.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMNotificationPoster.m new file mode 100644 index 0000000000..766476f3e3 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMNotificationPoster.m @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2009-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMNotificationPoster.h" + + +@implementation OCMNotificationPoster + +- (id)initWithNotification:(id)aNotification +{ + if ((self = [super init])) + { + notification = [aNotification retain]; + } + + return self; +} + +- (void)dealloc +{ + [notification release]; + [super dealloc]; +} + +- (void)handleInvocation:(NSInvocation *)anInvocation +{ + [[NSNotificationCenter defaultCenter] postNotification:notification]; +} + + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMObjectReturnValueProvider.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMObjectReturnValueProvider.h new file mode 100644 index 0000000000..c162239750 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMObjectReturnValueProvider.h @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2009-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMNonRetainingObjectReturnValueProvider.h" + +@interface OCMObjectReturnValueProvider : OCMNonRetainingObjectReturnValueProvider + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMObjectReturnValueProvider.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMObjectReturnValueProvider.m new file mode 100644 index 0000000000..c0ac09b234 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMObjectReturnValueProvider.m @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2009-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMObjectReturnValueProvider.h" + + +@implementation OCMObjectReturnValueProvider + +- (instancetype)initWithValue:(id)aValue +{ + if((self = [super initWithValue:aValue])) + [returnValue retain]; + return self; +} + +- (void)dealloc +{ + [returnValue release]; + [super dealloc]; +} + + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMObserverRecorder.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMObserverRecorder.h new file mode 100644 index 0000000000..04d6a53b8d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMObserverRecorder.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2009-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@interface OCMObserverRecorder : NSObject +{ + NSNotification *recordedNotification; +} + +- (NSNotification *)notificationWithName:(NSString *)name object:(id)sender; + +- (NSNotification *)notificationWithName:(NSString *)name object:(id)sender userInfo:(NSDictionary *)userInfo; + +- (BOOL)matchesNotification:(NSNotification *)aNotification; + +- (BOOL)argument:(id)expectedArg matchesArgument:(id)observedArg; + +- (BOOL)didRecordInvocation __used; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMObserverRecorder.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMObserverRecorder.m new file mode 100644 index 0000000000..140d17f688 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMObserverRecorder.m @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2009-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCMObserverRecorder.h" +#import "OCMConstraint.h" + + +@interface NSObject(HCMatcherDummy) +- (BOOL)matches:(id)item; +@end + +#pragma mark - + + +@implementation OCMObserverRecorder + +#pragma mark Initialisers, description, accessors, etc. + +- (void)dealloc +{ + [recordedNotification release]; + [super dealloc]; +} + +- (BOOL)didRecordInvocation +{ + return YES; // Needed for macro use, and recorder can only end up in macro state if it was used. +} + + +#pragma mark Recording + +- (NSNotification *)notificationWithName:(NSString *)name object:(id)sender +{ + recordedNotification = [[NSNotification notificationWithName:name object:sender] retain]; + return nil; +} + +- (NSNotification *)notificationWithName:(NSString *)name object:(id)sender userInfo:(NSDictionary *)userInfo +{ + recordedNotification = [[NSNotification notificationWithName:name object:sender userInfo:userInfo] retain]; + return nil; +} + + +#pragma mark Verification + +- (BOOL)matchesNotification:(NSNotification *)aNotification +{ + return [self argument:[recordedNotification name] matchesArgument:[aNotification name]] && + [self argument:[recordedNotification object] matchesArgument:[aNotification object]] && + [self argument:[recordedNotification userInfo] matchesArgument:[aNotification userInfo]]; +} + +- (BOOL)argument:(id)expectedArg matchesArgument:(id)observedArg +{ + if([expectedArg isKindOfClass:[OCMConstraint class]]) + { + return [expectedArg evaluate:observedArg]; + } + else if([expectedArg conformsToProtocol:objc_getProtocol("HCMatcher")]) + { + return [expectedArg matches:observedArg]; + } + else if (expectedArg == observedArg) + { + return YES; + } + else if (expectedArg == nil || observedArg == nil) + { + return NO; + } + else + { + return [expectedArg isEqual:observedArg]; + } +} + + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMPassByRefSetter.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMPassByRefSetter.h new file mode 100644 index 0000000000..fb889ab64b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMPassByRefSetter.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2009-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMArgAction.h" + +@interface OCMPassByRefSetter : OCMArgAction +{ + id value; +} + +- (id)initWithValue:(id)value; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMPassByRefSetter.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMPassByRefSetter.m new file mode 100644 index 0000000000..fdd3cfe0d3 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMPassByRefSetter.m @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2009-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMPassByRefSetter.h" + + +@implementation OCMPassByRefSetter + +- (id)initWithValue:(id)aValue +{ + if ((self = [super init])) + { + value = [aValue retain]; + } + + return self; +} + +- (void)dealloc +{ + [value release]; + [super dealloc]; +} + +- (void)handleArgument:(id)arg +{ + void *pointerValue = [arg pointerValue]; + if(pointerValue != NULL) + { + if([value isKindOfClass:[NSValue class]]) + [(NSValue *)value getValue:pointerValue]; + else + *(id *)pointerValue = value; + } +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMQuantifier.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMQuantifier.h new file mode 100644 index 0000000000..d85e8b1f41 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMQuantifier.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2016-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@interface OCMQuantifier : NSObject +{ + NSUInteger expectedCount; +} + ++ (instancetype)never; ++ (instancetype)exactly:(NSUInteger)count; ++ (instancetype)atLeast:(NSUInteger)count; ++ (instancetype)atMost:(NSUInteger)count; + +- (BOOL)isValidCount:(NSUInteger)count; + +- (NSString *)description; + +@end + + +#define OCMNever() ([OCMQuantifier never]) +#define OCMTimes(n) ([OCMQuantifier exactly:(n)]) +#define OCMAtLeast(n) ([OCMQuantifier atLeast:(n)]) +#define OCMAtMost(n) ([OCMQuantifier atMost:(n)]) + +#ifndef OCM_DISABLE_SHORT_QSYNTAX +#define never() OCMNever() +#define times(n) OCMTimes(n) +#define atLeast(n) OCMAtLeast(n) +#define atMost(n) OCMAtMost(n) +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMQuantifier.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMQuantifier.m new file mode 100644 index 0000000000..f9980743a5 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMQuantifier.m @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2016-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMQuantifier.h" +#import "OCMMacroState.h" +#import "OCMVerifier.h" + + +@interface OCMExactCountQuantifier : OCMQuantifier + +@end + +@interface OCMAtLeastQuantifier : OCMQuantifier + +@end + +@interface OCMAtMostQuantifier : OCMQuantifier + +@end + + + +@implementation OCMQuantifier + ++ (instancetype)exactly:(NSUInteger)count +{ + return [[[OCMExactCountQuantifier alloc] initWithCount:count] autorelease]; +} + ++ (instancetype)never +{ + return [self exactly:0]; +} + ++ (instancetype)atLeast:(NSUInteger)count +{ + return [[[OCMAtLeastQuantifier alloc] initWithCount:count] autorelease]; +} + ++ (instancetype)atMost:(NSUInteger)count +{ + return [[[OCMAtMostQuantifier alloc] initWithCount:count] autorelease]; +} + + +- (instancetype)initWithCount:(NSUInteger)count +{ + if((self = [super init]) != nil) + { + expectedCount = count; + [(OCMVerifier *)[[OCMMacroState globalState] recorder] setQuantifier:self]; + } + return self; +} + + +- (BOOL)isValidCount:(NSUInteger)count +{ + return NO; +} + +- (NSString *)description +{ + switch(expectedCount) + { + case 0: return @"never"; + case 1: return @"once"; + default: return [NSString stringWithFormat:@"%lu times", (unsigned long)expectedCount]; + } +} + +@end + + +@implementation OCMExactCountQuantifier + +- (BOOL)isValidCount:(NSUInteger)count +{ + return count == expectedCount; +} + +@end + + +@implementation OCMAtLeastQuantifier + +- (instancetype)initWithCount:(NSUInteger)count +{ + if(count == 0) + @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Count for an at-least quantifier cannot be zero." userInfo:nil]; + return [super initWithCount:count]; +} + +- (BOOL)isValidCount:(NSUInteger)count +{ + return count >= expectedCount; +} + +- (NSString *)description +{ + return [@"at least " stringByAppendingString:[super description]]; +} + +@end + + +@implementation OCMAtMostQuantifier + +- (instancetype)initWithCount:(NSUInteger)count +{ + if(count == 0) + @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Count for an at-most quantifier cannot be zero. Use never or exactly-zero quantifier instead." userInfo:nil]; + return [super initWithCount:count]; +} + +- (BOOL)isValidCount:(NSUInteger)count +{ + return count <= expectedCount; +} + +- (NSString *)description +{ + return [@"at most " stringByAppendingString:[super description]]; +} + +@end + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMRealObjectForwarder.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMRealObjectForwarder.h new file mode 100644 index 0000000000..37438a2c39 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMRealObjectForwarder.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2010-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@interface OCMRealObjectForwarder : NSObject +{ +} + +- (void)handleInvocation:(NSInvocation *)anInvocation; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMRealObjectForwarder.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMRealObjectForwarder.m new file mode 100644 index 0000000000..e84f5e350b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMRealObjectForwarder.m @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2010-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCMRealObjectForwarder.h" +#import "OCPartialMockObject.h" +#import "OCMFunctionsPrivate.h" + + +@implementation OCMRealObjectForwarder + +- (void)handleInvocation:(NSInvocation *)anInvocation +{ + id invocationTarget = [anInvocation target]; + + [anInvocation setSelector:OCMAliasForOriginalSelector([anInvocation selector])]; + if ([invocationTarget isProxy]) + { + if (class_getInstanceMethod([invocationTarget mockObjectClass], @selector(realObject))) + { + // the method has been invoked on the mock, we need to change the target to the real object + [anInvocation setTarget:[(OCPartialMockObject *)invocationTarget realObject]]; + } + else + { + [NSException raise:NSInternalInconsistencyException + format:@"Method andForwardToRealObject can only be used with partial mocks and class methods."]; + } + } + + [anInvocation invoke]; +} + + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMRecorder.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMRecorder.h new file mode 100644 index 0000000000..b10d54df05 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMRecorder.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2014-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@class OCMockObject; +@class OCMInvocationMatcher; + + +@interface OCMRecorder : NSProxy +{ + OCMockObject *mockObject; + OCMInvocationMatcher *invocationMatcher; + BOOL didRecordInvocation; + BOOL shouldReturnMockFromInit; +} + + +- (instancetype)init; +- (instancetype)initWithMockObject:(OCMockObject *)aMockObject; + +- (void)setMockObject:(OCMockObject *)aMockObject; +- (void)setShouldReturnMockFromInit:(BOOL)flag; + +- (OCMInvocationMatcher *)invocationMatcher; +- (BOOL)didRecordInvocation; + +- (id)classMethod; +- (id)ignoringNonObjectArgs; + + +@end + +@interface OCMRecorder (Properties) + +#define ignoringNonObjectArgs() _ignoringNonObjectArgs() +@property (nonatomic, readonly) OCMRecorder *(^ _ignoringNonObjectArgs)(void); + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMRecorder.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMRecorder.m new file mode 100644 index 0000000000..7f4ad8697e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMRecorder.m @@ -0,0 +1,152 @@ +#import +/* + * Copyright (c) 2014-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCMRecorder.h" +#import "OCClassMockObject.h" +#import "OCMInvocationMatcher.h" +#import "NSInvocation+OCMAdditions.h" + + +@implementation OCMRecorder + +- (instancetype)init +{ + // no super, we're inheriting from NSProxy + didRecordInvocation = NO; + shouldReturnMockFromInit = NO; + return self; +} + +- (instancetype)initWithMockObject:(OCMockObject *)aMockObject +{ + [self init]; + [self setMockObject:aMockObject]; + return self; +} + +- (void)setMockObject:(OCMockObject *)aMockObject +{ + mockObject = aMockObject; +} + +- (void)setShouldReturnMockFromInit:(BOOL)flag +{ + shouldReturnMockFromInit = flag; +} + +- (void)dealloc +{ + [invocationMatcher release]; + [super dealloc]; +} + +- (NSString *)description +{ + return [invocationMatcher description]; +} + +- (OCMInvocationMatcher *)invocationMatcher +{ + return invocationMatcher; +} + +- (BOOL)didRecordInvocation +{ + return didRecordInvocation; +} + + +#pragma mark Modifying the matcher + +- (id)classMethod +{ + // should we handle the case where this is called with a mock that isn't a class mock? + [invocationMatcher setRecordedAsClassMethod:YES]; + return self; +} + +- (id)ignoringNonObjectArgs +{ + [invocationMatcher setIgnoreNonObjectArgs:YES]; + return self; +} + + +#pragma mark Recording the actual invocation + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector +{ + if([invocationMatcher recordedAsClassMethod]) + return [[(OCClassMockObject *)mockObject mockedClass] methodSignatureForSelector:aSelector]; + + NSMethodSignature *signature = [mockObject methodSignatureForSelector:aSelector]; + if(signature == nil) + { + // if we're a working with a class mock and there is a class method, auto-switch + if(([object_getClass(mockObject) isSubclassOfClass:[OCClassMockObject class]]) && + ([[(OCClassMockObject *)mockObject mockedClass] respondsToSelector:aSelector])) + { + [self classMethod]; + signature = [self methodSignatureForSelector:aSelector]; + } + } + return signature; +} + +- (void)forwardInvocation:(NSInvocation *)anInvocation +{ + [anInvocation setTarget:nil]; + didRecordInvocation = YES; + [invocationMatcher setInvocation:anInvocation]; + + // Code with ARC may retain the receiver of an init method before invoking it. In that case it + // relies on the init method returning an object it can release. So, we must set the correct + // return value here. Normally, the correct return value is the recorder but sometimes it's the + // mock. The decision is easier to make in the mock, which is why the mock sets a flag in the + // recorder and we simply use the flag here. + if([anInvocation methodIsInInitFamily]) + { + id returnValue = shouldReturnMockFromInit ? (id)mockObject : (id)self; + [anInvocation setReturnValue:&returnValue]; + } +} + +- (void)doesNotRecognizeSelector:(SEL)aSelector __used +{ + [NSException raise:NSInvalidArgumentException format:@"%@: cannot stub/expect/verify method '%@' because no such method exists in the mocked class.", mockObject, NSStringFromSelector(aSelector)]; +} + + +@end + + +@implementation OCMRecorder (Properties) + +@dynamic _ignoringNonObjectArgs; + +- (OCMRecorder *(^)(void))_ignoringNonObjectArgs +{ + id (^theBlock)(void) = ^ (void) + { + return [self ignoringNonObjectArgs]; + }; + return [[theBlock copy] autorelease]; +} + + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMStubRecorder.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMStubRecorder.h new file mode 100644 index 0000000000..205a3a8f20 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMStubRecorder.h @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2004-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import + +#import + +@interface OCMStubRecorder : OCMRecorder + +- (id)andReturn:(id)anObject; +- (id)andReturnValue:(NSValue *)aValue; +- (id)andThrow:(NSException *)anException; +- (id)andPost:(NSNotification *)aNotification; +- (id)andCall:(SEL)selector onObject:(id)anObject; +- (id)andDo:(void (^)(NSInvocation *invocation))block; +- (id)andForwardToRealObject; + +@end + + +@interface OCMStubRecorder (Properties) + +#define andReturn(aValue) _andReturn(({ \ + __typeof__(aValue) _val = (aValue); \ + NSValue *_nsval = [NSValue value:&_val withObjCType:@encode(__typeof__(_val))]; \ + if (OCMIsObjectType(@encode(__typeof(_val)))) { \ + objc_setAssociatedObject(_nsval, "OCMAssociatedBoxedValue", *(__unsafe_unretained id *) (void *) &_val, OBJC_ASSOCIATION_RETAIN); \ + } \ + _nsval; \ +})) +@property (nonatomic, readonly) OCMStubRecorder *(^ _andReturn)(NSValue *); + +#define andThrow(anException) _andThrow(anException) +@property (nonatomic, readonly) OCMStubRecorder *(^ _andThrow)(NSException *); + +#define andPost(aNotification) _andPost(aNotification) +@property (nonatomic, readonly) OCMStubRecorder *(^ _andPost)(NSNotification *); + +#define andCall(anObject, aSelector) _andCall(anObject, aSelector) +@property (nonatomic, readonly) OCMStubRecorder *(^ _andCall)(id, SEL); + +#define andDo(aBlock) _andDo(aBlock) +@property (nonatomic, readonly) OCMStubRecorder *(^ _andDo)(void (^)(NSInvocation *)); + +#define andForwardToRealObject() _andForwardToRealObject() +@property (nonatomic, readonly) OCMStubRecorder *(^ _andForwardToRealObject)(void); + +@property (nonatomic, readonly) OCMStubRecorder *(^ _ignoringNonObjectArgs)(void); + +@end + + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMStubRecorder.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMStubRecorder.m new file mode 100644 index 0000000000..c075283957 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMStubRecorder.m @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2004-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMStubRecorder.h" +#import "OCClassMockObject.h" +#import "OCMInvocationStub.h" +#import "OCMBlockCaller.h" +#import "OCMBoxedReturnValueProvider.h" +#import "OCMExceptionReturnValueProvider.h" +#import "OCMIndirectReturnValueProvider.h" +#import "OCMNotificationPoster.h" +#import "OCMRealObjectForwarder.h" + + +@implementation OCMStubRecorder + +#pragma mark Initialisers, description, accessors, etc. + +- (id)init +{ + if(invocationMatcher != nil) + [NSException raise:NSInternalInconsistencyException format:@"** Method init invoked twice on stub recorder. Are you trying to mock the init method? This is currently not supported."]; + + self = [super init]; + invocationMatcher = [[OCMInvocationStub alloc] init]; + return self; +} + +- (OCMInvocationStub *)stub +{ + return (OCMInvocationStub *)invocationMatcher; +} + + +#pragma mark Recording invocation actions + +- (id)andReturn:(id)anObject +{ + id action; + if(anObject == mockObject) + { + action = [[[OCMNonRetainingObjectReturnValueProvider alloc] initWithValue:anObject] autorelease]; + } + else + { + action = [[[OCMObjectReturnValueProvider alloc] initWithValue:anObject] autorelease]; + } + [[self stub] addInvocationAction:action]; + return self; +} + +- (id)andReturnValue:(NSValue *)aValue +{ + [[self stub] addInvocationAction:[[[OCMBoxedReturnValueProvider alloc] initWithValue:aValue] autorelease]]; + return self; +} + +- (id)andThrow:(NSException *)anException +{ + [[self stub] addInvocationAction:[[[OCMExceptionReturnValueProvider alloc] initWithValue:anException] autorelease]]; + return self; +} + +- (id)andPost:(NSNotification *)aNotification +{ + [[self stub] addInvocationAction:[[[OCMNotificationPoster alloc] initWithNotification:aNotification] autorelease]]; + return self; +} + +- (id)andCall:(SEL)selector onObject:(id)anObject +{ + [[self stub] addInvocationAction:[[[OCMIndirectReturnValueProvider alloc] initWithProvider:anObject andSelector:selector] autorelease]]; + return self; +} + +- (id)andDo:(void (^)(NSInvocation *))aBlock +{ + [[self stub] addInvocationAction:[[[OCMBlockCaller alloc] initWithCallBlock:aBlock] autorelease]]; + return self; +} + +- (id)andForwardToRealObject +{ + [[self stub] addInvocationAction:[[[OCMRealObjectForwarder alloc] init] autorelease]]; + return self; +} + + +#pragma mark Finishing recording + +- (void)forwardInvocation:(NSInvocation *)anInvocation +{ + [super forwardInvocation:anInvocation]; + [mockObject addStub:[self stub]]; +} + + +@end + + +@implementation OCMStubRecorder (Properties) + +@dynamic _andReturn; + +- (OCMStubRecorder *(^)(NSValue *))_andReturn +{ + id (^theBlock)(id) = ^ (NSValue *aValue) + { + if(OCMIsObjectType([aValue objCType])) + { + id objValue = nil; + [aValue getValue:&objValue]; + return [self andReturn:objValue]; + } + else + { + return [self andReturnValue:aValue]; + } + }; + return [[theBlock copy] autorelease]; +} + + +@dynamic _andThrow; + +- (OCMStubRecorder *(^)(NSException *))_andThrow +{ + id (^theBlock)(id) = ^ (NSException * anException) + { + return [self andThrow:anException]; + }; + return [[theBlock copy] autorelease]; +} + + +@dynamic _andPost; + +- (OCMStubRecorder *(^)(NSNotification *))_andPost +{ + id (^theBlock)(id) = ^ (NSNotification * aNotification) + { + return [self andPost:aNotification]; + }; + return [[theBlock copy] autorelease]; +} + + +@dynamic _andCall; + +- (OCMStubRecorder *(^)(id, SEL))_andCall +{ + id (^theBlock)(id, SEL) = ^ (id anObject, SEL aSelector) + { + return [self andCall:aSelector onObject:anObject]; + }; + return [[theBlock copy] autorelease]; +} + + +@dynamic _andDo; + +- (OCMStubRecorder *(^)(void (^)(NSInvocation *)))_andDo +{ + id (^theBlock)(void (^)(NSInvocation *)) = ^ (void (^ blockToCall)(NSInvocation *)) + { + return [self andDo:blockToCall]; + }; + return [[theBlock copy] autorelease]; +} + + +@dynamic _andForwardToRealObject; + +- (OCMStubRecorder *(^)(void))_andForwardToRealObject +{ + id (^theBlock)(void) = ^ (void) + { + return [self andForwardToRealObject]; + }; + return [[theBlock copy] autorelease]; +} + + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMVerifier.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMVerifier.h new file mode 100644 index 0000000000..e53eb934f7 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMVerifier.h @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2014-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@class OCMLocation; +@class OCMQuantifier; + +@interface OCMVerifier : OCMRecorder + +@property(strong) OCMLocation *location; +@property(strong) OCMQuantifier *quantifier; + +- (instancetype)withQuantifier:(OCMQuantifier *)quantifier; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMVerifier.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMVerifier.m new file mode 100644 index 0000000000..a96bb74ba2 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMVerifier.m @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2014-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMVerifier.h" +#import "OCMockObject.h" +#import "OCMInvocationMatcher.h" +#import "OCMLocation.h" +#import "OCMQuantifier.h" + + +@implementation OCMVerifier + +- (id)init +{ + if(invocationMatcher != nil) + [NSException raise:NSInternalInconsistencyException format:@"** Method init invoked twice on verifier. Are you trying to verify the init method? This is currently not supported."]; + if ((self = [super init])) + { + invocationMatcher = [[OCMInvocationMatcher alloc] init]; + } + + return self; +} + +- (instancetype)withQuantifier:(OCMQuantifier *)quantifier +{ + [self setQuantifier:quantifier]; + return self; +} + +- (void)forwardInvocation:(NSInvocation *)anInvocation +{ + [super forwardInvocation:anInvocation]; + [mockObject verifyInvocation:invocationMatcher withQuantifier:self.quantifier atLocation:self.location]; +} + +- (void)dealloc +{ + [_location release]; + [_quantifier release]; + [super dealloc]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMock-Info.plist b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMock-Info.plist new file mode 100644 index 0000000000..b88d8b0550 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMock-Info.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2004-2020 Mulle Kybernetik. + NSPrincipalClass + + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMock-Prefix.pch b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMock-Prefix.pch new file mode 100644 index 0000000000..9f3ef37cad --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMock-Prefix.pch @@ -0,0 +1,7 @@ +// +// Prefix header for all source files of the 'OCMock' target in the 'OCMock' project +// + +#ifdef __OBJC__ + #import +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMock.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMock.h new file mode 100644 index 0000000000..5548859c29 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMock.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2004-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMockMacros.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMockMacros.h new file mode 100644 index 0000000000..bdcac41e80 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMockMacros.h @@ -0,0 +1,151 @@ +/* +* Copyright (c) 2014-2020 Erik Doernenburg and contributors +* +* Licensed under the Apache License, Version 2.0 (the "License"); you may +* not use these files except in compliance with the License. You may obtain +* a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +* License for the specific language governing permissions and limitations +* under the License. +*/ + + +#ifdef OCM_DISABLE_SHORT_SYNTAX +#define OCM_DISABLE_SHORT_QSYNTAX +#endif + + +#define OCMClassMock(cls) [OCMockObject niceMockForClass:cls] + +#define OCMStrictClassMock(cls) [OCMockObject mockForClass:cls] + +#define OCMProtocolMock(protocol) [OCMockObject niceMockForProtocol:protocol] + +#define OCMStrictProtocolMock(protocol) [OCMockObject mockForProtocol:protocol] + +#define OCMPartialMock(obj) [OCMockObject partialMockForObject:obj] + +#define OCMObserverMock() [OCMockObject observerMock] + + +#define OCMStub(invocation) \ +({ \ + _OCMSilenceWarnings( \ + [OCMMacroState beginStubMacro]; \ + OCMStubRecorder *recorder = nil; \ + @try{ \ + invocation; \ + }@catch(...){ \ + [[OCMMacroState globalState] setInvocationDidThrow:YES]; \ + @throw; \ + }@finally{ \ + recorder = [OCMMacroState endStubMacro]; \ + } \ + recorder; \ + ); \ +}) + +#define OCMExpect(invocation) \ +({ \ + _OCMSilenceWarnings( \ + [OCMMacroState beginExpectMacro]; \ + OCMStubRecorder *recorder = nil; \ + @try{ \ + invocation; \ + }@catch(...){ \ + [[OCMMacroState globalState] setInvocationDidThrow:YES]; \ + @throw; \ + }@finally{ \ + recorder = [OCMMacroState endExpectMacro]; \ + } \ + recorder; \ + ); \ +}) + +#define OCMReject(invocation) \ +({ \ + _OCMSilenceWarnings( \ + [OCMMacroState beginRejectMacro]; \ + OCMStubRecorder *recorder = nil; \ + @try{ \ + invocation; \ + }@catch(...){ \ + [[OCMMacroState globalState] setInvocationDidThrow:YES]; \ + @throw; \ + }@finally{ \ + recorder = [OCMMacroState endRejectMacro]; \ + } \ + recorder; \ + ); \ +}) + + + +#define OCMClassMethod(invocation) \ + _OCMSilenceWarnings( \ + [[OCMMacroState globalState] switchToClassMethod]; \ + invocation; \ + ); + + +#ifndef OCM_DISABLE_SHORT_SYNTAX +#define ClassMethod(invocation) OCMClassMethod(invocation) +#endif + + +#define OCMVerifyAll(mock) [(OCMockObject *)mock verifyAtLocation:OCMMakeLocation(self, __FILE__, __LINE__)] + +#define OCMVerifyAllWithDelay(mock, delay) [(OCMockObject *)mock verifyWithDelay:delay atLocation:OCMMakeLocation(self, __FILE__, __LINE__)] + +#define _OCMVerify(invocation) \ +({ \ + _OCMSilenceWarnings( \ + [OCMMacroState beginVerifyMacroAtLocation:OCMMakeLocation(self, __FILE__, __LINE__)]; \ + @try{ \ + invocation; \ + }@catch(...){ \ + [[OCMMacroState globalState] setInvocationDidThrow:YES]; \ + @throw; \ + }@finally{ \ + [OCMMacroState endVerifyMacro]; \ + } \ + ); \ +}) + +#define _OCMVerifyWithQuantifier(quantifier, invocation) \ +({ \ + _OCMSilenceWarnings( \ + [OCMMacroState beginVerifyMacroAtLocation:OCMMakeLocation(self, __FILE__, __LINE__) withQuantifier:quantifier]; \ + @try{ \ + invocation; \ + }@catch(...){ \ + [[OCMMacroState globalState] setInvocationDidThrow:YES]; \ + @throw; \ + }@finally{ \ + [OCMMacroState endVerifyMacro]; \ + } \ + ); \ +}) + +// explanation for macros below here: https://stackoverflow.com/questions/3046889/optional-parameters-with-c-macros + +#define _OCMVerify_1(A) _OCMVerify(A) +#define _OCMVerify_2(A,B) _OCMVerifyWithQuantifier(A, B) +#define _OCMVerify_X(x,A,B,FUNC, ...) FUNC +#define OCMVerify(...) _OCMVerify_X(,##__VA_ARGS__, _OCMVerify_2(__VA_ARGS__), _OCMVerify_1(__VA_ARGS__)) + + +#define _OCMSilenceWarnings(macro) \ +({ \ + _Pragma("clang diagnostic push") \ + _Pragma("clang diagnostic ignored \"-Wunused-value\"") \ + _Pragma("clang diagnostic ignored \"-Wunused-getter-return-value\"") \ + _Pragma("clang diagnostic ignored \"-Wstrict-selector-match\"") \ + macro \ + _Pragma("clang diagnostic pop") \ +}) diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMockObject.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMockObject.h new file mode 100644 index 0000000000..64a47c448d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMockObject.h @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2004-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@class OCMLocation; +@class OCMQuantifier; +@class OCMInvocationStub; +@class OCMStubRecorder; +@class OCMInvocationMatcher; +@class OCMInvocationExpectation; + + +@interface OCMockObject : NSProxy +{ + BOOL isNice; + BOOL expectationOrderMatters; + NSMutableArray *stubs; + NSMutableArray *expectations; + NSMutableArray *exceptions; + NSMutableArray *invocations; +} + ++ (id)mockForClass:(Class)aClass; ++ (id)mockForProtocol:(Protocol *)aProtocol; ++ (id)partialMockForObject:(NSObject *)anObject; + ++ (id)niceMockForClass:(Class)aClass; ++ (id)niceMockForProtocol:(Protocol *)aProtocol; + ++ (id)observerMock; + +- (instancetype)init; + +- (void)setExpectationOrderMatters:(BOOL)flag; + +- (id)stub; +- (id)expect; +- (id)reject; + +- (id)verify; +- (id)verifyAtLocation:(OCMLocation *)location; + +- (void)verifyWithDelay:(NSTimeInterval)delay; +- (void)verifyWithDelay:(NSTimeInterval)delay atLocation:(OCMLocation *)location; + +- (void)stopMocking; + +// internal use only + +- (void)addStub:(OCMInvocationStub *)aStub; +- (void)addExpectation:(OCMInvocationExpectation *)anExpectation; +- (void)addInvocation:(NSInvocation *)anInvocation; + +- (BOOL)handleInvocation:(NSInvocation *)anInvocation; +- (void)handleUnRecordedInvocation:(NSInvocation *)anInvocation; +- (BOOL)handleSelector:(SEL)sel; + +- (void)verifyInvocation:(OCMInvocationMatcher *)matcher; +- (void)verifyInvocation:(OCMInvocationMatcher *)matcher atLocation:(OCMLocation *)location; +- (void)verifyInvocation:(OCMInvocationMatcher *)matcher withQuantifier:(OCMQuantifier *)quantifier atLocation:(OCMLocation *)location; +- (NSString *)descriptionForVerificationFailureWithMatcher:(OCMInvocationMatcher *)matcher quantifier:(OCMQuantifier *)quantifier invocationCount:(NSUInteger)count; + +@end + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMockObject.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMockObject.m new file mode 100644 index 0000000000..6ebfbe4f34 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCMockObject.m @@ -0,0 +1,527 @@ +/* + * Copyright (c) 2004-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMockObject.h" +#import "OCClassMockObject.h" +#import "OCProtocolMockObject.h" +#import "OCPartialMockObject.h" +#import "OCObserverMockObject.h" +#import "OCMExceptionReturnValueProvider.h" +#import "OCMExpectationRecorder.h" +#import "OCMInvocationExpectation.h" +#import "OCMLocation.h" +#import "OCMMacroState.h" +#import "OCMQuantifier.h" +#import "OCMVerifier.h" +#import "OCMFunctionsPrivate.h" +#import "NSInvocation+OCMAdditions.h" + + +@implementation OCMockObject + +#pragma mark Class initialisation + ++ (void)initialize +{ + if([[NSInvocation class] instanceMethodSignatureForSelector:@selector(getArgumentAtIndexAsObject:)] == NULL) + [NSException raise:NSInternalInconsistencyException format:@"** Expected method not present; the method getArgumentAtIndexAsObject: is not implemented by NSInvocation. If you see this exception it is likely that you are using the static library version of OCMock and your project is not configured correctly to load categories from static libraries. Did you forget to add the -ObjC linker flag?"]; +} + + +#pragma mark Factory methods + ++ (id)mockForClass:(Class)aClass +{ + return [[[OCClassMockObject alloc] initWithClass:aClass] autorelease]; +} + ++ (id)mockForProtocol:(Protocol *)aProtocol +{ + return [[[OCProtocolMockObject alloc] initWithProtocol:aProtocol] autorelease]; +} + ++ (id)partialMockForObject:(NSObject *)anObject +{ + return [[[OCPartialMockObject alloc] initWithObject:anObject] autorelease]; +} + + ++ (id)niceMockForClass:(Class)aClass +{ + return [self _makeNice:[self mockForClass:aClass]]; +} + ++ (id)niceMockForProtocol:(Protocol *)aProtocol +{ + return [self _makeNice:[self mockForProtocol:aProtocol]]; +} + + ++ (id)_makeNice:(OCMockObject *)mock +{ + mock->isNice = YES; + return mock; +} + + ++ (id)observerMock +{ + return [[[OCObserverMockObject alloc] init] autorelease]; +} + + +#pragma mark Initialisers, description, accessors, etc. + +- (instancetype)init +{ + // check if we are called from inside a macro + OCMRecorder *recorder = [[OCMMacroState globalState] recorder]; + if(recorder != nil) + { + [recorder setMockObject:self]; + return (id)[recorder init]; + } + + // skip initialisation when init is called again, which can happen when stubbing alloc/init + if(stubs != nil) + { + return self; + } + + // no [super init], we're inheriting from NSProxy + expectationOrderMatters = NO; + stubs = [[NSMutableArray alloc] init]; + expectations = [[NSMutableArray alloc] init]; + exceptions = [[NSMutableArray alloc] init]; + invocations = [[NSMutableArray alloc] init]; + return self; +} + +- (void)dealloc +{ + [stubs release]; + [expectations release]; + [exceptions release]; + [invocations release]; + [super dealloc]; +} + +- (NSString *)description +{ + return @"OCMockObject"; +} + +- (void)addStub:(OCMInvocationStub *)aStub +{ + [self assertInvocationsArrayIsPresent]; + @synchronized(stubs) + { + [stubs addObject:aStub]; + } +} + +- (OCMInvocationStub *)stubForInvocation:(NSInvocation *)anInvocation +{ + @synchronized(stubs) + { + for(OCMInvocationStub *stub in stubs) + if([stub matchesInvocation:anInvocation]) + return stub; + return nil; + } +} + +- (void)addExpectation:(OCMInvocationExpectation *)anExpectation +{ + @synchronized(expectations) + { + [expectations addObject:anExpectation]; + } +} + +- (void)assertInvocationsArrayIsPresent +{ + if(invocations == nil) + { + [NSException raise:NSInternalInconsistencyException format:@"** Cannot use mock object %@ at %p. This error usually occurs when a mock object is used after stopMocking has been called on it. In most cases it is not necessary to call stopMocking. If you know you have to, please make sure that the mock object is not used afterwards.", [self description], (void *)self]; + } +} + +- (void)addInvocation:(NSInvocation *)anInvocation +{ + @synchronized(invocations) + { + // We can't do a normal retain arguments on anInvocation because its target/arguments/return + // value could be self. That would produce a retain cycle self->invocations->anInvocation->self. + // However we need to retain everything on anInvocation that isn't self because we expect them to + // stick around after this method returns. Use our special method to retain just what's needed. + // This still doesn't completely prevent retain cycles since any of the arguments could have a + // strong reference to self. Those will have to be broken with manual calls to -stopMocking. + [anInvocation retainObjectArgumentsExcludingObject:self]; + [invocations addObject:anInvocation]; + } +} + + +#pragma mark Public API + +- (void)setExpectationOrderMatters:(BOOL)flag +{ + expectationOrderMatters = flag; +} + +- (void)stopMocking +{ + // invocations can contain objects that clients expect to be deallocated by now, + // and they can also have a strong reference to self, creating a retain cycle. Get + // rid of all of the invocations to hopefully let their objects deallocate, and to + // break any retain cycles involving self. + @synchronized(invocations) + { + [invocations removeAllObjects]; + [invocations autorelease]; + invocations = nil; + } +} + + +- (id)stub +{ + return [[[OCMStubRecorder alloc] initWithMockObject:self] autorelease]; +} + +- (id)expect +{ + return [[[OCMExpectationRecorder alloc] initWithMockObject:self] autorelease]; +} + +- (id)reject +{ + return [[self expect] never]; +} + + +- (id)verify +{ + return [self verifyAtLocation:nil]; +} + +- (id)verifyAtLocation:(OCMLocation *)location +{ + NSMutableArray *unsatisfiedExpectations = [NSMutableArray array]; + @synchronized(expectations) + { + for(OCMInvocationExpectation *e in expectations) + { + if(![e isSatisfied]) + [unsatisfiedExpectations addObject:e]; + } + } + + if([unsatisfiedExpectations count] == 1) + { + NSString *description = [NSString stringWithFormat:@"%@: expected method was not invoked: %@", + [self description], [[unsatisfiedExpectations objectAtIndex:0] description]]; + OCMReportFailure(location, description); + } + else if([unsatisfiedExpectations count] > 0) + { + NSString *description = [NSString stringWithFormat:@"%@: %@ expected methods were not invoked: %@", + [self description], @([unsatisfiedExpectations count]), [self _stubDescriptions:YES]]; + OCMReportFailure(location, description); + } + + OCMInvocationExpectation *firstException = nil; + @synchronized(exceptions) + { + firstException = [exceptions.firstObject retain]; + } + if(firstException) + { + NSString *description = [NSString stringWithFormat:@"%@: %@ (This is a strict mock failure that was ignored when it actually occurred.)", + [self description], [firstException description]]; + OCMReportFailure(location, description); + } + [firstException release]; + + return [[[OCMVerifier alloc] initWithMockObject:self] autorelease]; +} + + +- (void)verifyWithDelay:(NSTimeInterval)delay +{ + [self verifyWithDelay:delay atLocation:nil]; +} + +- (void)verifyWithDelay:(NSTimeInterval)delay atLocation:(OCMLocation *)location +{ + NSTimeInterval step = 0.01; + while(delay > 0) + { + @synchronized(expectations) + { + BOOL allExpectationsAreMatchAndReject = YES; + for(OCMInvocationExpectation *expectation in expectations) + { + if(![expectation isMatchAndReject]) + { + allExpectationsAreMatchAndReject = NO; + break; + } + } + if(allExpectationsAreMatchAndReject) + break; + } + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:MIN(step, delay)]]; + delay -= step; + step *= 2; + } + [self verifyAtLocation:location]; +} + + +#pragma mark Verify after running + +- (void)verifyInvocation:(OCMInvocationMatcher *)matcher +{ + [self verifyInvocation:matcher atLocation:nil]; +} + +- (void)verifyInvocation:(OCMInvocationMatcher *)matcher atLocation:(OCMLocation *)location +{ + [self verifyInvocation:matcher withQuantifier:nil atLocation:location]; +} + +- (void)verifyInvocation:(OCMInvocationMatcher *)matcher withQuantifier:(OCMQuantifier *)quantifier atLocation:(OCMLocation *)location +{ + NSUInteger count = 0; + [self assertInvocationsArrayIsPresent]; + @synchronized(invocations) + { + for(NSInvocation *invocation in invocations) + { + if([matcher matchesInvocation:invocation]) + count += 1; + } + } + if(quantifier == nil) + quantifier = [OCMQuantifier atLeast:1]; + if(![quantifier isValidCount:count]) + { + NSString *description = [self descriptionForVerificationFailureWithMatcher:matcher quantifier:quantifier invocationCount:count]; + OCMReportFailure(location, description); + } +} + +- (NSString *)descriptionForVerificationFailureWithMatcher:(OCMInvocationMatcher *)matcher quantifier:(OCMQuantifier *)quantifier invocationCount:(NSUInteger)count +{ + NSString *actualDescription = nil; + switch(count) + { + case 0: actualDescription = @"not invoked"; break; + case 1: actualDescription = @"invoked once"; break; + default: actualDescription = [NSString stringWithFormat:@"invoked %lu times", (unsigned long)count]; break; + } + + return [NSString stringWithFormat:@"%@: Method `%@` was %@; but was expected %@.", + [self description], [matcher description], actualDescription, [quantifier description]]; +} + + +#pragma mark Handling invocations + +- (id)forwardingTargetForSelector:(SEL)aSelector +{ + if([OCMMacroState globalState] != nil) + { + OCMRecorder *recorder = [[OCMMacroState globalState] recorder]; + [recorder setMockObject:self]; + // In order for ARC to work correctly, the recorder has to set up return values for + // methods in the init family of methods. If the mock forwards a method to the recorder + // that it will record, i.e. a method that the recorder does not implement, then the + // recorder must set the mock as the return value. Otherwise it must use itself. + [recorder setShouldReturnMockFromInit:(class_getInstanceMethod(object_getClass(recorder), aSelector) == NO)]; + return recorder; + } + return nil; +} + + +- (BOOL)handleSelector:(SEL)sel +{ + @synchronized(stubs) + { + for(OCMInvocationStub *recorder in stubs) + if([recorder matchesSelector:sel]) + return YES; + } + return NO; +} + +- (void)forwardInvocation:(NSInvocation *)anInvocation +{ + @try + { + if([self handleInvocation:anInvocation] == NO) + [self handleUnRecordedInvocation:anInvocation]; + } + @catch(NSException *e) + { + if([[e name] isEqualToString:OCMStubbedException]) + { + e = [[e userInfo] objectForKey:@"exception"]; + } + else + { + // add non-stubbed method to list of exceptions to be re-raised in verify + @synchronized(exceptions) + { + [exceptions addObject:e]; + } + } + [e raise]; + } +} + +- (BOOL)handleInvocation:(NSInvocation *)anInvocation +{ + [self assertInvocationsArrayIsPresent]; + [self addInvocation:anInvocation]; + + OCMInvocationStub *stub = [self stubForInvocation:anInvocation]; + if(stub == nil) + return NO; + + // Retain the stub in case it ends up being removed because we still need it at the end for handleInvocation: + [stub retain]; + + BOOL removeStub = NO; + @synchronized(expectations) + { + if([expectations containsObject:stub]) + { + OCMInvocationExpectation *expectation = [self _nextExpectedInvocation]; + if(expectationOrderMatters && (expectation != stub)) + { + [NSException raise:NSInternalInconsistencyException format:@"%@: unexpected method invoked: %@\n\texpected:\t%@", + [self description], [stub description], [[expectations objectAtIndex:0] description]]; + } + + // We can't check isSatisfied yet, since the stub won't be satisfied until we call + // handleInvocation: since we'll still have the current expectation in the expectations array, which + // will cause an exception if expectationOrderMatters is YES and we're not ready for any future + // expected methods to be called yet + if(![(OCMInvocationExpectation *)stub isMatchAndReject]) + { + [expectations removeObject:stub]; + removeStub = YES; + } + } + } + if(removeStub) + { + @synchronized(stubs) + { + [stubs removeObject:stub]; + } + } + + @try + { + [stub handleInvocation:anInvocation]; + } + @finally + { + [stub release]; + } + + return YES; +} + +// Must be synchronized on expectations when calling this method. +- (OCMInvocationExpectation *)_nextExpectedInvocation +{ + for(OCMInvocationExpectation *expectation in expectations) + if(![expectation isMatchAndReject]) + return expectation; + return nil; +} + +- (void)handleUnRecordedInvocation:(NSInvocation *)anInvocation +{ + if(isNice == NO) + { + [NSException raise:NSInternalInconsistencyException format:@"%@: unexpected method invoked: %@ %@", + [self description], [anInvocation invocationDescription], [self _stubDescriptions:NO]]; + } +} + +- (void)doesNotRecognizeSelector:(SEL)aSelector __unused +{ + if([OCMMacroState globalState] != nil) + { + // we can't do anything clever with the macro state because we must raise an exception here + [NSException raise:NSInvalidArgumentException format:@"%@: Cannot stub/expect/verify method '%@' because no such method exists in the mocked class.", + [self description], NSStringFromSelector(aSelector)]; + } + else + { + [NSException raise:NSInvalidArgumentException format:@"-[%@ %@]: unrecognized selector sent to instance %p", + [self description], NSStringFromSelector(aSelector), (void *)self]; + } +} + + +#pragma mark Helper methods + +- (NSString *)_stubDescriptions:(BOOL)onlyExpectations +{ + NSMutableString *outputString = [NSMutableString string]; + NSArray *stubsCopy = nil; + @synchronized(stubs) + { + stubsCopy = [stubs copy]; + } + for(OCMStubRecorder *stub in stubsCopy) + { + BOOL expectationsContainStub = NO; + @synchronized(expectations) + { + expectationsContainStub = [expectations containsObject:stub]; + } + + NSString *prefix = @""; + + if(onlyExpectations) + { + if(expectationsContainStub == NO) + continue; + } + else + { + if(expectationsContainStub) + prefix = @"expected:\t"; + else + prefix = @"stubbed:\t"; + } + [outputString appendFormat:@"\n\t%@%@", prefix, [stub description]]; + } + [stubsCopy release]; + return outputString; +} + + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCObserverMockObject.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCObserverMockObject.h new file mode 100644 index 0000000000..40d48b0688 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCObserverMockObject.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2009-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@class OCMLocation; + + +@interface OCObserverMockObject : NSObject +{ + BOOL expectationOrderMatters; + NSMutableArray *recorders; + NSMutableArray *centers; +} + +- (void)setExpectationOrderMatters:(BOOL)flag; + +- (id)expect; + +- (void)verify; +- (void)verifyAtLocation:(OCMLocation *)location; + +- (void)handleNotification:(NSNotification *)aNotification; + +// internal use + +- (void)autoRemoveFromCenter:(NSNotificationCenter *)aCenter; +- (NSNotification *)notificationWithName:(NSString *)name object:(id)sender; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCObserverMockObject.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCObserverMockObject.m new file mode 100644 index 0000000000..c5c3f6cfe5 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCObserverMockObject.m @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2009-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCObserverMockObject.h" +#import "OCMLocation.h" +#import "OCMMacroState.h" +#import "OCMObserverRecorder.h" +#import "OCMFunctionsPrivate.h" + + +@implementation OCObserverMockObject + +#pragma mark Initialisers, description, accessors, etc. + +- (id)init +{ + if ((self = [super init])) + { + recorders = [[NSMutableArray alloc] init]; + centers = [[NSMutableArray alloc] init]; + } + + return self; +} + +- (id)retain +{ + return [super retain]; +} + +- (void)dealloc +{ + for(NSNotificationCenter *c in centers) + [c removeObserver:self]; + [centers release]; + [recorders release]; + [super dealloc]; +} + +- (NSString *)description +{ + return @"OCObserverMockObject"; +} + +- (void)setExpectationOrderMatters:(BOOL)flag +{ + expectationOrderMatters = flag; +} + +- (void)autoRemoveFromCenter:(NSNotificationCenter *)aCenter +{ + @synchronized(centers) + { + [centers addObject:aCenter]; + } +} + + +#pragma mark Public API + +- (id)expect +{ + OCMObserverRecorder *recorder = [[[OCMObserverRecorder alloc] init] autorelease]; + @synchronized(recorders) + { + [recorders addObject:recorder]; + } + return recorder; +} + +- (void)verify +{ + [self verifyAtLocation:nil]; +} + +- (void)verifyAtLocation:(OCMLocation *)location +{ + @synchronized(recorders) + { + if([recorders count] == 1) + { + NSString *description = [NSString stringWithFormat:@"%@: expected notification was not observed: %@", + [self description], [[recorders lastObject] description]]; + OCMReportFailure(location, description); + } + else if([recorders count] > 0) + { + NSString *description = [NSString stringWithFormat:@"%@ : %@ expected notifications were not observed.", + [self description], @([recorders count])]; + OCMReportFailure(location, description); + } + } +} + + +#pragma mark Receiving recording requests via macro + +// This is a bit of a hack. The methods simply assume that when they are called from within a macro that it's +// the OCMExpect macro. That creates a recorder for mock objects, which we cannot use here. So, we overwrite +// it with a newly allocated recorder. + +- (NSNotification *)notificationWithName:(NSString *)name object:(id)sender +{ + if([OCMMacroState globalState] != nil) + { + id recorder = [self expect]; + [[OCMMacroState globalState] setRecorder:recorder]; + return [recorder notificationWithName:name object:sender]; + } + return nil; +} + +- (NSNotification *)notificationWithName:(NSString *)name object:(id)sender userInfo:(NSDictionary *)userInfo +{ + if([OCMMacroState globalState] != nil) + { + id recorder = [self expect]; + [[OCMMacroState globalState] setRecorder:recorder]; + return [recorder notificationWithName:name object:sender userInfo:userInfo]; + } + return nil; +} + + +#pragma mark Receiving notifications + +- (void)handleNotification:(NSNotification *)aNotification +{ + @synchronized(recorders) + { + NSUInteger i, limit; + + limit = expectationOrderMatters ? 1 : [recorders count]; + for(i = 0; i < limit; i++) + { + if([[recorders objectAtIndex:i] matchesNotification:aNotification]) + { + [recorders removeObjectAtIndex:i]; + return; + } + } + } + [NSException raise:NSInternalInconsistencyException format:@"%@: unexpected notification observed: %@", [self description], + [aNotification description]]; +} + + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCPartialMockObject.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCPartialMockObject.h new file mode 100644 index 0000000000..289ce8790d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCPartialMockObject.h @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2009-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCClassMockObject.h" + +@interface OCPartialMockObject : OCClassMockObject +{ + NSObject *realObject; + NSInvocation *invocationFromMock; +} + +- (id)initWithObject:(NSObject *)anObject; + +- (NSObject *)realObject; + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCPartialMockObject.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCPartialMockObject.m new file mode 100644 index 0000000000..58b7f3071a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCPartialMockObject.m @@ -0,0 +1,281 @@ +/* + * Copyright (c) 2009-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCPartialMockObject.h" +#import "NSObject+OCMAdditions.h" +#import "OCMFunctionsPrivate.h" +#import "OCMInvocationStub.h" +#import "NSInvocation+OCMAdditions.h" +#import "NSMethodSignature+OCMAdditions.h" + + +@implementation OCPartialMockObject + +#pragma mark Initialisers, description, accessors, etc. + +- (id)initWithObject:(NSObject *)anObject +{ + if(anObject == nil) + [NSException raise:NSInvalidArgumentException format:@"Object cannot be nil."]; + + Class const class = [self classToSubclassForObject:anObject]; + [self assertClassIsSupported:class]; + [super initWithClass:class]; + realObject = [anObject retain]; + [self prepareObjectForInstanceMethodMocking]; + return self; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"OCPartialMockObject(%@)", NSStringFromClass(mockedClass)]; +} + +- (NSObject *)realObject +{ + return realObject; +} + +#pragma mark Helper methods + +- (void)assertClassIsSupported:(Class)class +{ + NSString *classname = NSStringFromClass(class); + NSString *reason = nil; + if([classname hasPrefix:@"__NSTagged"] || [classname hasPrefix:@"NSTagged"]) + reason = [NSString stringWithFormat:@"OCMock does not support partially mocking tagged classes; got %@", classname]; + else if([classname hasPrefix:@"__NSCF"]) + reason = [NSString stringWithFormat:@"OCMock does not support partially mocking toll-free bridged classes; got %@", classname]; + + if(reason != nil) + [[NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil] raise]; +} + +- (Class)classToSubclassForObject:(id)object +{ + /* object_getClass() gives us the actual class backing the object, whereas [object class] + * is sometimes overridden, by KVO or CoreData, for example, to return a subclass. + * + * With KVO, if we replace and subclass the actual class, as returned by object_getClass(), + * we lose notifications. So, in that case only, we return the class reported by the class + * method. + */ + + if([object observationInfo] != NULL) + return [object class]; + + return object_getClass(object); +} + +#pragma mark Extending/overriding superclass behaviour + +- (void)stopMocking +{ + if(realObject != nil) + { + Class partialMockClass = object_getClass(realObject); + OCMSetAssociatedMockForObject(nil, realObject); + object_setClass(realObject, [self mockedClass]); + [realObject release]; + realObject = nil; + OCMDisposeSubclass(partialMockClass); + } + [super stopMocking]; +} + +- (void)addStub:(OCMInvocationStub *)aStub +{ + [super addStub:aStub]; + if(![aStub recordedAsClassMethod]) + [self setupForwarderForSelector:[[aStub recordedInvocation] selector]]; +} + +- (void)addInvocation:(NSInvocation *)anInvocation +{ + // If the mock invokes a method on the real object we end up here a second time, but because + // the mock has added the invocation already we do not want to add it again. + if((invocationFromMock == nil) || ([anInvocation selector] != [invocationFromMock selector])) + [super addInvocation:anInvocation]; +} + +- (void)handleUnRecordedInvocation:(NSInvocation *)anInvocation +{ + // In the case of an init that is called on a mock we must return the mock instance and + // not the realObject if the underlying init returns the realObject because at the call site + // ARC will have retained the target and the release/retain count must balance. If we return + // the realObject, then realObject will be over released and the mock will leak. Equally if + // we are called on the realObject we need to make sure not to return the mock. + id targetReceivingInit = nil; + if([anInvocation methodIsInInitFamily]) + { + targetReceivingInit = [anInvocation target]; + [realObject retain]; + } + + invocationFromMock = anInvocation; + [anInvocation invokeWithTarget:realObject]; + invocationFromMock = nil; + + if(targetReceivingInit) + { + id returnVal; + [anInvocation getReturnValue:&returnVal]; + if(returnVal == realObject) + { + [anInvocation setReturnValue:&self]; + [realObject release]; + [self retain]; + } + [targetReceivingInit release]; + } +} + + +#pragma mark Subclass management + +- (void)prepareObjectForInstanceMethodMocking +{ + OCMSetAssociatedMockForObject(self, realObject); + + /* dynamically create a subclass and set it as the class of the object */ + Class subclass = OCMCreateSubclass(mockedClass, realObject); + object_setClass(realObject, subclass); + + /* point forwardInvocation: of the object to the implementation in the mock */ + Method myForwardMethod = class_getInstanceMethod([self mockObjectClass], @selector(forwardInvocationForRealObject:)); + IMP myForwardIMP = method_getImplementation(myForwardMethod); + class_addMethod(subclass, @selector(forwardInvocation:), myForwardIMP, method_getTypeEncoding(myForwardMethod)); + + /* do the same for forwardingTargetForSelector, remember existing imp with alias selector */ + Method myForwardingTargetMethod = class_getInstanceMethod([self mockObjectClass], @selector(forwardingTargetForSelectorForRealObject:)); + IMP myForwardingTargetIMP = method_getImplementation(myForwardingTargetMethod); + IMP originalForwardingTargetIMP = [mockedClass instanceMethodForSelector:@selector(forwardingTargetForSelector:)]; + class_addMethod(subclass, @selector(forwardingTargetForSelector:), myForwardingTargetIMP, method_getTypeEncoding(myForwardingTargetMethod)); + class_addMethod(subclass, @selector(ocmock_replaced_forwardingTargetForSelector:), originalForwardingTargetIMP, method_getTypeEncoding(myForwardingTargetMethod)); + + /* We also override the -class method to return the original class */ + Method myObjectClassMethod = class_getInstanceMethod([self mockObjectClass], @selector(classForRealObject)); + const char *objectClassTypes = method_getTypeEncoding(myObjectClassMethod); + IMP myObjectClassImp = method_getImplementation(myObjectClassMethod); + class_addMethod(subclass, @selector(class), myObjectClassImp, objectClassTypes); + + /* Adding forwarder for most instance methods to allow for verify after run */ + NSArray *methodBlackList = @[@"class", @"forwardingTargetForSelector:", @"methodSignatureForSelector:", @"forwardInvocation:", + @"allowsWeakReference", @"retainWeakReference", @"isBlock", @"retainCount", @"retain", @"release", @"autorelease"]; + [NSObject enumerateMethodsInClass:mockedClass usingBlock:^(Class cls, SEL sel) { + if(OCMIsAppleBaseClass(cls) || OCMIsApplePrivateMethod(cls, sel)) + return; + if([methodBlackList containsObject:NSStringFromSelector(sel)]) + return; + @try + { + [self setupForwarderForSelector:sel]; + } + @catch(NSException *e) + { + // ignore for now + } + }]; +} + +- (void)setupForwarderForSelector:(SEL)sel +{ + SEL aliasSelector = OCMAliasForOriginalSelector(sel); + if(class_getInstanceMethod(object_getClass(realObject), aliasSelector) != NULL) + return; + + Method originalMethod = class_getInstanceMethod(mockedClass, sel); + /* Might be NULL if the selector is forwarded to another class */ + IMP originalIMP = (originalMethod != NULL) ? method_getImplementation(originalMethod) : NULL; + const char *types = (originalMethod != NULL) ? method_getTypeEncoding(originalMethod) : NULL; + // TODO: check the fallback implementation is actually sufficient + if(types == NULL) + types = ([[mockedClass instanceMethodSignatureForSelector:sel] fullObjCTypes]); + + Class subclass = object_getClass([self realObject]); + IMP forwarderIMP = [mockedClass instanceMethodForwarderForSelector:sel]; + class_replaceMethod(subclass, sel, forwarderIMP, types); + class_addMethod(subclass, aliasSelector, originalIMP, types); +} + + +// Implementation of the -class method; return the Class that was reported with [realObject class] prior to mocking +- (Class)classForRealObject +{ + // in here "self" is a reference to the real object, not the mock + OCPartialMockObject *mock = OCMGetAssociatedMockForObject(self); + if(mock == nil) + [NSException raise:NSInternalInconsistencyException format:@"No partial mock for object %p", self]; + return [mock mockedClass]; +} + + +- (id)forwardingTargetForSelectorForRealObject:(SEL)sel +{ + // in here "self" is a reference to the real object, not the mock + OCPartialMockObject *mock = OCMGetAssociatedMockForObject(self); + if(mock == nil) + [NSException raise:NSInternalInconsistencyException format:@"No partial mock for object %p", self]; + if([mock handleSelector:sel]) + return self; + + return [self ocmock_replaced_forwardingTargetForSelector:sel]; +} + +// Make the compiler happy in -forwardingTargetForSelectorForRealObject: because it can't find the message… +- (id)ocmock_replaced_forwardingTargetForSelector:(SEL)sel +{ + return nil; +} + + +- (void)forwardInvocationForRealObject:(NSInvocation *)anInvocation +{ + // in here "self" is a reference to the real object, not the mock + OCPartialMockObject *mock = OCMGetAssociatedMockForObject(self); + if(mock == nil) + [NSException raise:NSInternalInconsistencyException format:@"No partial mock for object %p", self]; + + if([mock handleInvocation:anInvocation] == NO) + { + [anInvocation setSelector:OCMAliasForOriginalSelector([anInvocation selector])]; + [anInvocation invoke]; + } +} + + +#pragma mark Verification handling + +- (NSString *)descriptionForVerificationFailureWithMatcher:(OCMInvocationMatcher *)matcher quantifier:(OCMQuantifier *)quantifier invocationCount:(NSUInteger)count +{ + SEL matcherSel = [[matcher recordedInvocation] selector]; + __block BOOL stubbingMightHelp = NO; + [NSObject enumerateMethodsInClass:mockedClass usingBlock:^(Class cls, SEL sel) { + if(sel == matcherSel) + stubbingMightHelp = OCMIsAppleBaseClass(cls) || OCMIsApplePrivateMethod(cls, sel); + }]; + + NSString *description = [super descriptionForVerificationFailureWithMatcher:matcher quantifier:quantifier invocationCount:count]; + if(stubbingMightHelp) + { + description = [description stringByAppendingFormat:@" Adding a stub for the method may resolve the issue, e.g. `OCMStub([mockObject %@]).andForwardToRealObject()`", [matcher description]]; + } + return description; +} + + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCProtocolMockObject.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCProtocolMockObject.h new file mode 100644 index 0000000000..59e5394c8c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCProtocolMockObject.h @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2005-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import "OCMockObject.h" + +@interface OCProtocolMockObject : OCMockObject +{ + Protocol *mockedProtocol; +} + +- (id)initWithProtocol:(Protocol *)aProtocol; + +@end + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCProtocolMockObject.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCProtocolMockObject.m new file mode 100644 index 0000000000..0b175857ff --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMock/OCProtocolMockObject.m @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2005-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCProtocolMockObject.h" + + +@implementation OCProtocolMockObject + +#pragma mark Initialisers, description, accessors, etc. + +- (id)initWithProtocol:(Protocol *)aProtocol +{ + if(aProtocol == nil) + [NSException raise:NSInvalidArgumentException format:@"Protocol cannot be nil."]; + + [super init]; + mockedProtocol = aProtocol; + return self; +} + +- (NSString *)description +{ + const char* name = protocol_getName(mockedProtocol); + return [NSString stringWithFormat:@"OCProtocolMockObject(%s)", name]; +} + +#pragma mark Proxy API + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector +{ + struct { BOOL isRequired; BOOL isInstance; } opts[4] = { {YES, YES}, {NO, YES}, {YES, NO}, {NO, NO} }; + for(int i = 0; i < 4; i++) + { + struct objc_method_description methodDescription = protocol_getMethodDescription(mockedProtocol, aSelector, opts[i].isRequired, opts[i].isInstance); + if(methodDescription.name != NULL) + return [NSMethodSignature signatureWithObjCTypes:methodDescription.types]; + } + return nil; +} + +- (BOOL)conformsToProtocol:(Protocol *)aProtocol +{ + return protocol_conformsToProtocol(mockedProtocol, aProtocol); +} + +- (BOOL)respondsToSelector:(SEL)selector +{ + return ([self methodSignatureForSelector:selector] != nil); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockLib/OCMockLib-Prefix.pch b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockLib/OCMockLib-Prefix.pch new file mode 100644 index 0000000000..d88e7f0730 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockLib/OCMockLib-Prefix.pch @@ -0,0 +1,7 @@ +// +// Prefix header for all source files of the 'OCMockLib' target in the 'OCMockLib' project +// + +#ifdef __OBJC__ + #import +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockLibTests/OCMockLibTests-Info.plist b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockLibTests/OCMockLibTests-Info.plist new file mode 100644 index 0000000000..169b6f710e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockLibTests/OCMockLibTests-Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockLibTests/OCMockLibTests-Prefix.pch b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockLibTests/OCMockLibTests-Prefix.pch new file mode 100644 index 0000000000..47815d65bb --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockLibTests/OCMockLibTests-Prefix.pch @@ -0,0 +1,10 @@ +// +// Prefix header +// +// The contents of this file are implicitly included at the beginning of every source file. +// + +#ifdef __OBJC__ + #import + #import +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/NSInvocationOCMAdditionsTests.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/NSInvocationOCMAdditionsTests.m new file mode 100644 index 0000000000..526269f5d7 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/NSInvocationOCMAdditionsTests.m @@ -0,0 +1,359 @@ +/* + * Copyright (c) 2006-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "NSInvocation+OCMAdditions.h" +#import "OCMArg.h" + + +@implementation NSValue(OCMTestAdditions) + +- (id)initialValue +{ + return nil; +} + +- (id)__init +{ + return [self init]; // keep compiler happy +} + +- (int)initNonObject +{ + return 0; +} + +- (id)ocmtest_initWithLongDouble:(long double)ldbl +{ + return [self initWithBytes:&ldbl objCType:@encode(__typeof__(ldbl))]; +} + +@end + + +@interface NSInvocationOCMAdditionsTests : XCTestCase + +@end + + +@implementation NSInvocationOCMAdditionsTests + +- (NSInvocation *)invocationForClass:(Class)cls selector:(SEL)selector +{ + NSMethodSignature *signature = [cls instanceMethodSignatureForSelector:selector]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setSelector:selector]; + return invocation; +} + + +- (void)testInvocationDescriptionWithNoArguments +{ + NSInvocation *invocation = [self invocationForClass:[NSString class] selector:@selector(lowercaseString)]; + XCTAssertEqualObjects(@"lowercaseString", [invocation invocationDescription], @""); +} + +- (void)testInvocationDescriptionWithObjectArgument +{ + NSInvocation *invocation = [self invocationForClass:[NSNumber class] selector:@selector(isEqualToNumber:)]; + // Give it one argument (starts at index 2) + NSNumber *argument = @1; + [invocation setArgument:&argument atIndex:2]; + + NSString *expected = [NSString stringWithFormat:@"isEqualToNumber:%d", 1]; + XCTAssertEqualObjects(expected, [invocation invocationDescription], @""); +} + +- (void)testInvocationDescriptionWithNSStringArgument +{ + NSInvocation *invocation = [self invocationForClass:[NSString class] selector:@selector(isEqualToString:)]; + NSString *argument = @"TEST_STRING"; + [invocation setArgument:&argument atIndex:2]; + + NSString *expected = [NSString stringWithFormat:@"isEqualToString:@\"%@\"", @"TEST_STRING"]; + XCTAssertEqualObjects(expected, [invocation invocationDescription], @""); +} + +- (void)testInvocationDescriptionWithObjectArguments +{ + NSInvocation *invocation = [self invocationForClass:[NSArray class] selector:@selector(setValue:forKey:)]; + NSNumber *argumentOne = @1; + NSString *argumentTwo = @"TEST_STRING"; + [invocation setArgument:&argumentOne atIndex:2]; + [invocation setArgument:&argumentTwo atIndex:3]; + + NSString *expected = [NSString stringWithFormat:@"setValue:%d forKey:@\"%@\"", 1, @"TEST_STRING"]; + XCTAssertEqualObjects(expected, [invocation invocationDescription], @""); +} + +- (void)testInvocationDescriptionWithArrayArgument +{ + NSInvocation *invocation = [self invocationForClass:[NSMutableArray class] selector:@selector(addObjectsFromArray:)]; + NSArray *argument = @[@"TEST_STRING"]; + [invocation setArgument:&argument atIndex:2]; + + NSString *expected = [NSString stringWithFormat:@"addObjectsFromArray:%@", [argument description]]; + XCTAssertEqualObjects(expected, [invocation invocationDescription], @""); +} + +- (void)testInvocationDescriptionWithIntArgument +{ + NSInvocation *invocation = [self invocationForClass:[NSNumber class] selector:@selector(initWithInt:)]; + int argumentOne = 1; + [invocation setArgument:&argumentOne atIndex:2]; + + NSString *expected = [NSString stringWithFormat:@"initWithInt:%d", 1]; + XCTAssertEqualObjects(expected, [invocation invocationDescription], @""); +} + +- (void)testInvocationDescriptionWithUnsignedIntArgument +{ + NSInvocation *invocation = [self invocationForClass:[NSNumber class] selector:@selector(initWithUnsignedInt:)]; + unsigned int argumentOne = 1; + [invocation setArgument:&argumentOne atIndex:2]; + + NSString *expected = [NSString stringWithFormat:@"initWithUnsignedInt:%d", 1]; + XCTAssertEqualObjects(expected, [invocation invocationDescription], @""); +} + +- (void)testInvocationDescriptionWithBoolArgument +{ + NSInvocation *invocation = [self invocationForClass:[NSNumber class] selector:@selector(initWithBool:)]; + BOOL argumentOne = TRUE; + [invocation setArgument:&argumentOne atIndex:2]; + + NSString *expected = [NSString stringWithFormat:@"initWithBool:YES"]; + XCTAssertEqualObjects(expected, [invocation invocationDescription], @""); +} + +- (void)testInvocationDescriptionWithCharArgument +{ + NSInvocation *invocation = [self invocationForClass:[NSNumber class] selector:@selector(initWithChar:)]; + char argumentOne = 'd'; + [invocation setArgument:&argumentOne atIndex:2]; + + NSString *expected = [NSString stringWithFormat:@"initWithChar:'%c'", argumentOne]; + XCTAssertEqualObjects(expected, [invocation invocationDescription], @""); +} + +- (void)testInvocationDescriptionWithUnsignedCharArgument +{ + NSInvocation *invocation = [self invocationForClass:[NSNumber class] selector:@selector(initWithUnsignedChar:)]; + unsigned char argumentOne = 'd'; + [invocation setArgument:&argumentOne atIndex:2]; + + NSString *expected = [NSString stringWithFormat:@"initWithUnsignedChar:'%c'", argumentOne]; + XCTAssertEqualObjects(expected, [invocation invocationDescription], @""); +} + +- (void)testInvocationDescriptionWithDoubleArgument +{ + NSInvocation *invocation = [self invocationForClass:[NSNumber class] selector:@selector(initWithDouble:)]; + double argumentOne = 1; + [invocation setArgument:&argumentOne atIndex:2]; + + NSString *expected = [NSString stringWithFormat:@"initWithDouble:%f", argumentOne]; + XCTAssertEqualObjects(expected, [invocation invocationDescription], @""); +} + +- (void)testInvocationDescriptionWithFloatArgument +{ + NSInvocation *invocation = [self invocationForClass:[NSNumber class] selector:@selector(initWithFloat:)]; + float argumentOne = 1; + [invocation setArgument:&argumentOne atIndex:2]; + + NSString *expected = [NSString stringWithFormat:@"initWithFloat:%f", argumentOne]; + XCTAssertEqualObjects(expected, [invocation invocationDescription], @""); +} + +- (void)testInvocationDescriptionWithLongDoubleArgument +{ + NSInvocation *invocation = [self invocationForClass:[NSValue class] selector:@selector(ocmtest_initWithLongDouble:)]; + long double argumentOne = 1; + [invocation setArgument:&argumentOne atIndex:2]; + + NSString *expected = [NSString stringWithFormat:@"ocmtest_initWithLongDouble:%Lf", argumentOne]; + XCTAssertEqualObjects(expected, [invocation invocationDescription], @""); +} + +- (void)testInvocationDescriptionWithLongArgument +{ + NSInvocation *invocation = [self invocationForClass:[NSNumber class] selector:@selector(initWithLong:)]; + long argumentOne = 1; + [invocation setArgument:&argumentOne atIndex:2]; + + NSString *expected = [NSString stringWithFormat:@"initWithLong:%ld", argumentOne]; + XCTAssertEqualObjects(expected, [invocation invocationDescription], @""); +} + +- (void)testInvocationDescriptionWithUnsignedLongArgument +{ + NSInvocation *invocation = [self invocationForClass:[NSNumber class] selector:@selector(initWithUnsignedLong:)]; + unsigned long argumentOne = 1; + [invocation setArgument:&argumentOne atIndex:2]; + + NSString *expected = [NSString stringWithFormat:@"initWithUnsignedLong:%lu", argumentOne]; + XCTAssertEqualObjects(expected, [invocation invocationDescription], @""); +} + +- (void)testInvocationDescriptionWithLongLongArgument +{ + NSInvocation *invocation = [self invocationForClass:[NSNumber class] selector:@selector(initWithLongLong:)]; + long long argumentOne = 1; + [invocation setArgument:&argumentOne atIndex:2]; + + NSString *expected = [NSString stringWithFormat:@"initWithLongLong:%qi", argumentOne]; + XCTAssertEqualObjects(expected, [invocation invocationDescription], @""); +} + +- (void)testInvocationDescriptionWithUnsignedLongLongArgument +{ + NSInvocation *invocation = [self invocationForClass:[NSNumber class] selector:@selector(initWithUnsignedLongLong:)]; + unsigned long long argumentOne = 1; + [invocation setArgument:&argumentOne atIndex:2]; + + NSString *expected = [NSString stringWithFormat:@"initWithUnsignedLongLong:%qu", argumentOne]; + XCTAssertEqualObjects(expected, [invocation invocationDescription], @""); +} + +- (void)testInvocationDescriptionWithShortArgument +{ + NSInvocation *invocation = [self invocationForClass:[NSNumber class] selector:@selector(initWithShort:)]; + short argumentOne = 1; + [invocation setArgument:&argumentOne atIndex:2]; + + NSString *expected = [NSString stringWithFormat:@"initWithShort:%hi", argumentOne]; + XCTAssertEqualObjects(expected, [invocation invocationDescription], @""); +} + +- (void)testInvocationDescriptionWithUnsignedShortArgument +{ + NSInvocation *invocation = [self invocationForClass:[NSNumber class] selector:@selector(initWithUnsignedShort:)]; + unsigned short argumentOne = 1; + [invocation setArgument:&argumentOne atIndex:2]; + + NSString *expected = [NSString stringWithFormat:@"initWithUnsignedShort:%hu", argumentOne]; + XCTAssertEqualObjects(expected, [invocation invocationDescription], @""); +} + +- (void)testInvocationDescriptionWithStructArgument +{ + NSInvocation *invocation = [self invocationForClass:[NSString class] selector:@selector(substringWithRange:)]; + NSRange range; + range.location = 2; + range.length = 4; + [invocation setArgument:&range atIndex:2]; + + NSString *expected = @"substringWithRange:(NSRange: {2, 4})"; + XCTAssertEqualObjects(expected, [invocation invocationDescription], @""); +} + +- (void)testInvocationDescriptionWithCStringArgument +{ + NSInvocation *invocation = [self invocationForClass:[NSString class] selector:@selector(initWithUTF8String:)]; + NSString *string = @"A string that is longer than 100 characters. 123456789 123456789 123456789 123456789 123456789 123456789"; + const char *cString = [string UTF8String]; + [invocation setArgument:&cString atIndex:2]; + + NSString *expected = [NSString stringWithFormat:@"initWithUTF8String:\"%@...\"", [string substringToIndex:100]]; + XCTAssertEqualObjects(expected, [invocation invocationDescription], @""); +} + +- (void)testInvocationDescriptionWithCStringArgumentAnyPointer +{ + NSInvocation *invocation = [self invocationForClass:[NSString class] selector:@selector(initWithUTF8String:)]; + const char *cString = [OCMArg anyPointer]; + [invocation setArgument:&cString atIndex:2]; + + XCTAssertEqualObjects(@"initWithUTF8String:<[OCMArg anyPointer]>", [invocation invocationDescription]); +} + +- (void)testInvocationDescriptionWithSelectorArgument +{ + NSInvocation *invocation = [self invocationForClass:[NSString class] selector:@selector(respondsToSelector:)]; + SEL selectorValue = @selector(testInvocationDescriptionWithSelectorArgument); + [invocation setArgument:&selectorValue atIndex:2]; + + NSString *expected = [NSString stringWithFormat:@"respondsToSelector:@selector(%@)", NSStringFromSelector(selectorValue)]; + XCTAssertEqualObjects(expected, [invocation invocationDescription], @""); +} + +- (void)testInvocationDescriptionWithPointerArgument +{ + NSInvocation *invocation = [self invocationForClass:[NSData class] selector:@selector(initWithBytes:length:)]; + NSData *data = [@"foo" dataUsingEncoding:NSUTF8StringEncoding]; + const void *bytes = [[@"foo" dataUsingEncoding:NSUTF8StringEncoding] bytes]; + NSUInteger length = [data length]; + [invocation setArgument:&bytes atIndex:2]; + [invocation setArgument:&length atIndex:3]; + + NSString *expected1 = [NSString stringWithFormat:@"initWithBytes:"]; + NSString *expected2 = [NSString stringWithFormat:@"length:%lu", (unsigned long)length]; + NSString *invocationDescription = [invocation invocationDescription]; + XCTAssertTrue([invocationDescription rangeOfString:expected1].length > 0, @""); + XCTAssertTrue([invocationDescription rangeOfString:expected2].length > 0, @""); +} + +- (void)testInvocationDescriptionWithPointerArgumentAnyPointer +{ + NSInvocation *invocation = [self invocationForClass:[NSData class] selector:@selector(initWithBytes:length:)]; + const void *bytes = [OCMArg anyPointer]; + NSUInteger length = 1500; + [invocation setArgument:&bytes atIndex:2]; + [invocation setArgument:&length atIndex:3]; + + NSString *expected = [NSString stringWithFormat:@"initWithBytes:<[OCMArg anyPointer]> length:%lu", (unsigned long)length]; + NSString *invocationDescription = [invocation invocationDescription]; + XCTAssertEqualObjects(expected, invocationDescription); +} + + +- (void)testInvocationDescriptionWithNilArgument +{ + NSInvocation *invocation = [self invocationForClass:[NSString class] selector:@selector(initWithString:)]; + NSString *argString = nil; + [invocation setArgument:&argString atIndex:2]; + + NSString *expected = [NSString stringWithFormat:@"initWithString:nil"]; + XCTAssertEqualObjects(expected, [invocation invocationDescription], @""); +} + +- (void)testCategorizesInitMethodFamilyCorrectly +{ + NSInvocation *invocation; + + invocation = [self invocationForClass:[NSString class] selector:@selector(init)]; + XCTAssertTrue([invocation methodIsInInitFamily]); + + invocation = [self invocationForClass:[NSString class] selector:@selector(initWithString:)]; + XCTAssertTrue([invocation methodIsInInitFamily]); + + invocation = [self invocationForClass:[NSValue class] selector:@selector(__init)]; + XCTAssertTrue([invocation methodIsInInitFamily]); + + invocation = [self invocationForClass:[NSObject class] selector:@selector(copy)]; + XCTAssertFalse([invocation methodIsInInitFamily]); + + invocation = [self invocationForClass:[NSValue class] selector:@selector(initialValue)]; + XCTAssertFalse([invocation methodIsInInitFamily]); + + invocation = [self invocationForClass:[NSValue class] selector:@selector(initNonObject)]; + XCTAssertFalse([invocation methodIsInInitFamily]); +} + + + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/NSMethodSignatureOCMAdditionsTests.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/NSMethodSignatureOCMAdditionsTests.m new file mode 100644 index 0000000000..43ed432fcc --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/NSMethodSignatureOCMAdditionsTests.m @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2013-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "NSMethodSignature+OCMAdditions.h" + + +#if TARGET_OS_IPHONE +#define NSPoint CGPoint +#define NSSize CGSize +#define NSRect CGRect +#endif + +@interface NSMethodSignatureOCMAdditionsTests : XCTestCase + +@end + +@implementation NSMethodSignatureOCMAdditionsTests + +- (void)testDeterminesThatSpecialReturnIsNotNeededForNonStruct +{ + const char *types = "i"; + NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:types]; + XCTAssertFalse([sig usesSpecialStructureReturn], @"Should have determined no need for special (stret) return."); +} + +- (void)testDeterminesThatSpecialReturnIsNeededForLargeStruct +{ + // This type should(!) require special returns for all architectures + const char *types = "{CATransform3D=ffffffffffffffff}"; + NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:types]; + XCTAssertTrue([sig usesSpecialStructureReturn], @"Should have determined need for special (stret) return."); +} + +- (void)testArchDependentSpecialReturns +{ +#define ASSERT_ENC(expected, enctype) do {\ + BOOL useSpecial = expected; \ + XCTAssertNoThrow(useSpecial = [[NSMethodSignature signatureWithObjCTypes:enctype] usesSpecialStructureReturn], \ + @"NSMethodSignature failed for type '%s'", enctype); \ + XCTAssertEqual((int)useSpecial, (int)expected,\ + @"Special (stret) return incorrect for type '%s'", enctype); \ + } while (0) +#define ASSERT_TYPE(expected, type) ASSERT_ENC(expected, @encode(type)) + +#if __x86_64__ + ASSERT_TYPE(YES,NSRect); + ASSERT_TYPE(NO, NSPoint); + ASSERT_TYPE(NO, NSRange); + ASSERT_ENC(NO, "{foo=ffff}"); + ASSERT_ENC(YES,"{foo=fffff}"); + ASSERT_ENC(YES,"{foo=D}"); + ASSERT_ENC(NO, "{foo=t}"); + ASSERT_ENC(YES,"{foo=TT}"); + ASSERT_TYPE(NO, __int128_t); + ASSERT_TYPE(NO, long double); + ASSERT_ENC(YES,"{nlist_64=(?=I)CCSQ}16@0:8"); +#endif +#if __i386__ + ASSERT_TYPE(YES,NSRect); + ASSERT_TYPE(NO, NSPoint); + ASSERT_TYPE(NO, NSRange); + ASSERT_TYPE(NO, long double); + ASSERT_ENC(NO, "{foo=ff}"); + ASSERT_ENC(YES,"{foo=fff}"); + ASSERT_ENC(NO, "{foo=c}"); + ASSERT_ENC(NO, "{foo=cc}"); + ASSERT_ENC(YES,"{foo=ccc}"); + ASSERT_ENC(NO, "{foo=cccc}"); + ASSERT_ENC(YES,"{foo=cccccc}"); + ASSERT_ENC(NO, "{foo=cccccccc}"); + ASSERT_ENC(YES,"{foo=D}"); +#endif +#if __arm__ + ASSERT_TYPE(YES, NSRect); + ASSERT_TYPE(YES, NSPoint); + ASSERT_TYPE(YES, NSRange); + ASSERT_ENC(NO, "{foo=f}"); + ASSERT_ENC(YES,"{foo=ff}"); + ASSERT_ENC(NO, "{foo=c}"); + ASSERT_ENC(NO, "{foo=cc}"); + ASSERT_ENC(NO, "{foo=ccc}"); + ASSERT_ENC(NO, "{foo=cccc}"); + ASSERT_ENC(YES,"{foo=ccccc}"); +#endif +} + +- (void)testNSMethodSignatureDebugDescriptionWorksTheWayWeExpectIt +{ + const char *types = "{CATransform3D=ffffffffffffffff}"; + NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:types]; + NSString *debugDescription = [sig debugDescription]; + NSRange stretYESRange = [debugDescription rangeOfString:@"is special struct return? YES"]; + NSRange stretNORange = [debugDescription rangeOfString:@"is special struct return? NO"]; + XCTAssertTrue(stretYESRange.length > 0 || stretNORange.length > 0, @"NSMethodSignature debugDescription has changed; need to change OCPartialMockObject impl"); +} + +- (void)testCreatesCorrectSignatureForBlockWithNoArgsAndVoidReturn +{ + void (^block)(void) = ^() { + }; + + NSMethodSignature *sig = [NSMethodSignature signatureForBlock:block]; + XCTAssertNotNil(sig, @"Should have created signature"); + + NSString *actual = [NSString stringWithCString:[sig methodReturnType] encoding:NSASCIIStringEncoding]; + XCTAssertEqualObjects(@"v", actual, @"Should have created signature with correct return type"); + // i am not completely sure why this has one argument; the actual type string is "v8@?0" + XCTAssertEqual(1, [sig numberOfArguments], @"Should have created signature with correct number of arguments"); +} + +- (void)testCreatesCorrectSignatureForBlockWithSomeArgsAndVoidReturn +{ + void (^block)(NSString *, BOOL *) = ^(NSString *line, BOOL *stop) { + }; + + NSMethodSignature *sig = [NSMethodSignature signatureForBlock:block]; + XCTAssertNotNil(sig, @"Should have created signature"); + + NSString *actual = [NSString stringWithCString:[sig methodReturnType] encoding:NSASCIIStringEncoding]; + XCTAssertEqualObjects(@"v", actual, @"Should have created signature with correct return type"); + // see comment in testCreatesCorrectSignatureForBlockWithNoArgsAndVoidReturn for discussion of extra argument + XCTAssertEqual(3, [sig numberOfArguments], @"Should have created signature with correct number of arguments"); + actual = [NSString stringWithCString:[sig getArgumentTypeAtIndex:1] encoding:NSASCIIStringEncoding]; + // seems to add the name of the class in quotes after the type + XCTAssertEqualObjects(@"@", [actual substringToIndex:1], @"Should have created signature with correct type of second argument"); + XCTAssertTrue(strcmp(@encode(BOOL *), [sig getArgumentTypeAtIndex:2]) == 0, @"Should have created signature with correct type of second argument"); +} + +- (void)testCreatesCorrectSignatureForBlockWithBoolReturn +{ + BOOL (^block)(void) = ^() { + return NO; + }; + + NSMethodSignature *sig = [NSMethodSignature signatureForBlock:block]; + XCTAssertNotNil(sig, @"Should have created signature"); + + XCTAssertTrue(strcmp(@encode(BOOL), [sig methodReturnType]) == 0, @"Should have created signature with correct return type"); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMArgTests.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMArgTests.m new file mode 100644 index 0000000000..3f07eee20b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMArgTests.m @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2013-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCMArg.h" +#import "OCMConstraint.h" +#import "OCMPassByRefSetter.h" + +#if TARGET_OS_IPHONE +#define NSRect CGRect +#define NSZeroRect CGRectZero +#define NSMakeRect CGRectMake +#define valueWithRect valueWithCGRect +#endif + +@interface OCMArgTests : XCTestCase + +@end + + +@implementation OCMArgTests + +- (void)testValueMacroCreatesCorrectValueObjects +{ + NSRange range = NSMakeRange(5, 5); + XCTAssertEqualObjects(OCMOCK_VALUE(range), [NSValue valueWithRange:range]); +#if !(TARGET_OS_IPHONE && TARGET_RT_64_BIT) + /* This should work everywhere but I can't get it to work on iOS 64-bit */ + XCTAssertEqualObjects(OCMOCK_VALUE((BOOL){YES}), @YES); +#endif + XCTAssertEqualObjects(OCMOCK_VALUE(42), @42); +#if !TARGET_OS_IPHONE + XCTAssertEqualObjects(OCMOCK_VALUE(NSZeroRect), [NSValue valueWithRect:NSZeroRect]); +#endif + XCTAssertEqualObjects(OCMOCK_VALUE([@"0123456789" rangeOfString:@"56789"]), [NSValue valueWithRange:range]); +} + +- (void)testIsKindOfClassCheck +{ + OCMBlockConstraint *constraint = [OCMArg isKindOfClass:[NSString class]]; + + XCTAssertTrue([constraint evaluate:@"foo"], @"Should have accepted \"foo\"."); + XCTAssertFalse([constraint evaluate:[NSArray array]], @"Should not have accepted other value."); + XCTAssertFalse([constraint evaluate:nil], @"Should not have accepted nil."); +} + +-(void)testResolvesSpecialAnySelectorToAnyConstraint +{ + SEL anySelector = [OCMArg anySelector]; + NSValue *anySelectorValue = [NSValue valueWithBytes:&anySelector objCType:@encode(SEL)]; + + XCTAssertTrue([[OCMArg resolveSpecialValues:anySelectorValue] isKindOfClass:[OCMAnyConstraint class]]); +} + +-(void)testDoesNotTreatOtherSelectorsAsSpecialValue +{ + NSValue *arbitrary = [NSValue value:NSSelectorFromString(@"someSelector") withObjCType:@encode(SEL)]; + XCTAssertEqual([OCMArg resolveSpecialValues:arbitrary], arbitrary, @"Should have returned selector as is."); +} + +-(void)testResolvesSpecialAnyPointerToAnyConstraint +{ + void *anyPointer = [OCMArg anyPointer]; + NSValue *anyPointerValue = [NSValue valueWithPointer:anyPointer]; + + XCTAssertTrue([[OCMArg resolveSpecialValues:anyPointerValue] isKindOfClass:[OCMAnyConstraint class]]); +} + +-(void)testResolvesPassByRefSetterValueToSetterInstance +{ + NSNumber *value = @1; + OCMPassByRefSetter *setter = [[OCMPassByRefSetter alloc] initWithValue:value]; + NSValue *passByRefSetterValue = [NSValue value:&setter withObjCType:@encode(void *)]; + XCTAssertEqual([OCMArg resolveSpecialValues:passByRefSetterValue], setter, @"Should have unwrapped setter instance."); +} + +- (void)testDoesNotModifyOtherPointersToObjects +{ + NSValue *objectPointer = [NSValue value:&self withObjCType:@encode(void *)]; + XCTAssertEqual([OCMArg resolveSpecialValues:objectPointer], objectPointer, @"Should have returned value as is."); +} + +- (void)testHandlesNonObjectPointersGracefully +{ + long numberThatRepresentsInValidClassPointer = 0x08; + long *pointer = &numberThatRepresentsInValidClassPointer; + NSValue *nonObjectPointerValue = [NSValue value:&pointer withObjCType:@encode(void *)]; + XCTAssertEqual([OCMArg resolveSpecialValues:nonObjectPointerValue], nonObjectPointerValue, @"Should have returned value as is."); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMBoxedReturnValueProviderTests.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMBoxedReturnValueProviderTests.m new file mode 100644 index 0000000000..e647065b42 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMBoxedReturnValueProviderTests.m @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2015-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCMBoxedReturnValueProvider.h" + + +@interface OCMBoxedReturnValueProvider(Private) +- (BOOL)isMethodReturnType:(const char *)returnType compatibleWithValueType:(const char *)valueType value:(const char*)value valueSize:(size_t)valueSize; +@end + + +@interface OCMBoxedReturnValueProviderTests : XCTestCase + +@end + +@implementation OCMBoxedReturnValueProviderTests + +- (void)testCorrectEqualityForCppProperty +{ + // see https://github.com/erikdoe/ocmock/issues/96 + const char *type1 = + "r^{GURL={basic_string, std::__1::alloca" + "tor >={__compressed_pair, std::__1::allocator >::__rep, std::__1::allocator >={__rep}}}B{Parsed={Component=ii}{Component=ii}{Component=ii}{Compo" + "nent=ii}{Component=ii}{Component=ii}{Component=ii}{Component=ii}^{Parsed}" + "}{scoped_ptr >={scoped_ptr_impl >={Data=^{GURL}}}}}"; + + const char *type2 = + "r^{GURL={basic_string, std::__1::alloca" + "tor >={__compressed_pair, std::__1::allocator >::__rep, std::__1::allocator >={__rep=(?={__long=II*}{__short=(?=Cc)[11c]}{__raw=[3L]})}}}B{Parse" + "d={Component=ii}{Component=ii}{Component=ii}{Component=ii}{Component=ii}{" + "Component=ii}{Component=ii}{Component=ii}^{Parsed}}{scoped_ptr >={scoped_ptr_impl >={Data=^{GURL}}}}}"; + + const char *type3 = + "r^{GURL}"; + + OCMBoxedReturnValueProvider *boxed = [OCMBoxedReturnValueProvider new]; + XCTAssertTrue([boxed isMethodReturnType:type1 compatibleWithValueType:type2 value:NULL valueSize:0]); + XCTAssertTrue([boxed isMethodReturnType:type1 compatibleWithValueType:type3 value:NULL valueSize:0]); + XCTAssertTrue([boxed isMethodReturnType:type2 compatibleWithValueType:type1 value:NULL valueSize:0]); + XCTAssertTrue([boxed isMethodReturnType:type2 compatibleWithValueType:type3 value:NULL valueSize:0]); + XCTAssertTrue([boxed isMethodReturnType:type3 compatibleWithValueType:type1 value:NULL valueSize:0]); + XCTAssertTrue([boxed isMethodReturnType:type3 compatibleWithValueType:type2 value:NULL valueSize:0]); +} + + +- (void)testCorrectEqualityForCppReturnTypesWithVtables +{ + // see https://github.com/erikdoe/ocmock/issues/247 + const char *type1 = + "^{S=^^?{basic_string, std::__1::allocat" + "or >={__compressed_pair, std::__1::allocator >::__rep, std::__1::allocator >={__rep}}}}"; + + const char *type2 = + "^{S=^^?{basic_string, std::__1::allocat" + "or >={__compressed_pair, std::__1::allocator >::__rep, std::__1::allocator >={__rep=(?={__long=QQ*}{__short=(?=Cc)[23c]}{__raw=[3Q]})}}}}"; + + OCMBoxedReturnValueProvider *boxed = [OCMBoxedReturnValueProvider new]; + XCTAssertTrue([boxed isMethodReturnType:type1 compatibleWithValueType:type2 value:NULL valueSize:0]); +} + + +- (void)testCorrectEqualityForStructureWithUnknownName +{ + // see https://github.com/erikdoe/ocmock/issues/333 + const char *type1 = "{?=dd}"; + const char *type2 = "{CLLocationCoordinate2D=dd}"; + + OCMBoxedReturnValueProvider *boxed = [OCMBoxedReturnValueProvider new]; + XCTAssertTrue([boxed isMethodReturnType:type1 compatibleWithValueType:type2 value:NULL valueSize:0]); +} + + +- (void)testCorrectEqualityForStructureWithoutName +{ + // see https://github.com/erikdoe/ocmock/issues/342 + const char *type1 = "r^{GURL={basic_string, std::__1::allocator >={__compressed_pair, std::__1::allocator >::__r" + "ep, std::__1::allocator >={__rep}}}B{Parsed={Component=ii}{Compo" + "nent=ii}{Component=ii}{Component=ii}{Component=ii}{Component=ii}{Compo" + "nent=ii}{Component=ii}B^{}}{unique_ptr >={__compressed_pair >=^{" + "}}}}"; + const char *type2 = "r^{GURL={basic_string, std::__1::allocator >={__compressed_pair, std::__1::allocator >::__r" + "ep, std::__1::allocator >={__rep=(?={__long=QQ*}{__short=(?=Cc)[" + "23c]}{__raw=[3Q]})}}}B{Parsed={Component=ii}{Component=ii}{Component=i" + "i}{Component=ii}{Component=ii}{Component=ii}{Component=ii}{Component=i" + "i}B^{Parsed}}{unique_ptr >={__com" + "pressed_pair >=^{GURL}}}}"; + + OCMBoxedReturnValueProvider *boxed = [OCMBoxedReturnValueProvider new]; + XCTAssertTrue([boxed isMethodReturnType:type1 compatibleWithValueType:type2 value:NULL valueSize:0]); +} + +@end + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMCPlusPlus11Tests.mm b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMCPlusPlus11Tests.mm new file mode 100644 index 0000000000..f441b64df2 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMCPlusPlus11Tests.mm @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCMock.h" + +#if !defined(__cplusplus) +#error This file must be compiled with C++ +#endif + +#if !__has_feature(cxx_nullptr) +#error This file must be compiled with a version of C++ that supports nullptr +#endif + +@interface OCMCPlusPlus11Tests : XCTestCase +@end + + +@implementation OCMCPlusPlus11Tests + +- (void)testSetsUpStubReturningNilForIdReturnType +{ + id mock = OCMPartialMock([NSArray arrayWithObject:@"Foo"]); + + OCMExpect([mock lastObject]).andReturn(nil); + XCTAssertNil([mock lastObject], @"Should have returned stubbed value"); + + OCMExpect([mock lastObject]).andReturn(Nil); + XCTAssertNil([mock lastObject], @"Should have returned stubbed value"); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMCPlusPlus98Tests.mm b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMCPlusPlus98Tests.mm new file mode 100644 index 0000000000..0a6e715f09 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMCPlusPlus98Tests.mm @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCMock.h" + +#if !defined(__cplusplus) +#error This file must be compiled with C++ +#endif + +#if __has_feature(cxx_nullptr) +#error This file must be compiled with a version of C++ (98) that doesn't support nullptr +#endif + +@interface OCMCPlusPlus98Tests : XCTestCase +@end + + +@implementation OCMCPlusPlus98Tests + +- (void)testSetsUpStubReturningNilForIdReturnType +{ + id mock = OCMPartialMock([NSArray arrayWithObject:@"Foo"]); + + OCMExpect([mock lastObject]).andReturn(nil); + XCTAssertNil([mock lastObject], @"Should have returned stubbed value"); + + OCMExpect([mock lastObject]).andReturn(Nil); + XCTAssertNil([mock lastObject], @"Should have returned stubbed value"); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMConstraintTests.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMConstraintTests.m new file mode 100644 index 0000000000..06718dbc66 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMConstraintTests.m @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2004-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCMConstraint.h" + + +@interface OCMConstraintTests : XCTestCase +{ + BOOL didCallCustomConstraint; +} + +@end + + +@implementation OCMConstraintTests + +- (void)setUp +{ + didCallCustomConstraint = NO; +} + +- (void)testAnyAcceptsAnything +{ + OCMConstraint *constraint = [OCMAnyConstraint constraint]; + XCTAssertTrue([constraint evaluate:@"foo"], @"Should have accepted a value."); + XCTAssertTrue([constraint evaluate:@"bar"], @"Should have accepted another value."); + XCTAssertTrue([constraint evaluate:nil], @"Should have accepted nil."); +} + +- (void)testIsNilAcceptsOnlyNil +{ + OCMConstraint *constraint = [OCMIsNilConstraint constraint]; + + XCTAssertFalse([constraint evaluate:@"foo"], @"Should not have accepted a value."); + XCTAssertTrue([constraint evaluate:nil], @"Should have accepted nil."); +} + +- (void)testIsNotNilAcceptsAnythingButNil +{ + OCMConstraint *constraint = [OCMIsNotNilConstraint constraint]; + + XCTAssertTrue([constraint evaluate:@"foo"], @"Should have accepted a value."); + XCTAssertFalse([constraint evaluate:nil], @"Should not have accepted nil."); +} + +- (void)testNotEqualAcceptsAnythingButValue +{ + OCMIsNotEqualConstraint *constraint = [OCMIsNotEqualConstraint constraint]; + constraint->testValue = @"foo"; + + XCTAssertFalse([constraint evaluate:@"foo"], @"Should not have accepted value."); + XCTAssertTrue([constraint evaluate:@"bar"], @"Should have accepted other value."); + XCTAssertTrue([constraint evaluate:nil], @"Should have accepted nil."); +} + + +- (BOOL)checkArg:(id)theArg +{ + didCallCustomConstraint = YES; + return [theArg isEqualToString:@"foo"]; +} + +- (void)testUsesPlainMethod +{ + OCMConstraint *constraint = CONSTRAINT(@selector(checkArg:)); + + XCTAssertTrue([constraint evaluate:@"foo"], @"Should have accepted foo."); + XCTAssertTrue(didCallCustomConstraint, @"Should have used custom method."); + XCTAssertFalse([constraint evaluate:@"bar"], @"Should not have accepted bar."); +} + + +- (BOOL)checkArg:(id)theArg withValue:(id)value +{ + didCallCustomConstraint = YES; + return [theArg isEqual:value]; +} + +- (void)testUsesMethodWithValue +{ + OCMConstraint *constraint = CONSTRAINTV(@selector(checkArg:withValue:), @"foo"); + + XCTAssertTrue([constraint evaluate:@"foo"], @"Should have accepted foo."); + XCTAssertTrue(didCallCustomConstraint, @"Should have used custom method."); + XCTAssertFalse([constraint evaluate:@"bar"], @"Should not have accepted bar."); +} + + +- (void)testRaisesExceptionWhenConstraintMethodDoesNotTakeArgument +{ + XCTAssertThrows(CONSTRAINTV(@selector(checkArg:), @"bar"), @"Should have thrown for invalid constraint method."); +} + + +- (void)testRaisesExceptionOnUnknownSelector +{ + // We use a selector that this test does not implement + XCTAssertThrows(CONSTRAINTV(@selector(arrayWithArray:), @"bar"), @"Should have thrown for unknown constraint method."); +} + + +-(void)testUsesBlock +{ + BOOL (^checkForFooBlock)(id) = ^(id value) + { + return [value isEqualToString:@"foo"]; + }; + + OCMBlockConstraint *constraint = [[OCMBlockConstraint alloc] initWithConstraintBlock:checkForFooBlock]; + + XCTAssertTrue([constraint evaluate:@"foo"], @"Should have accepted foo."); + XCTAssertFalse([constraint evaluate:@"bar"], @"Should not have accepted bar."); +} + +-(void)testBlockConstraintCanCaptureArgument +{ + __block NSString *captured; + BOOL (^captureArgBlock)(id) = ^(id value) + { + captured = value; + return YES; + }; + + OCMBlockConstraint *constraint = [[OCMBlockConstraint alloc] initWithConstraintBlock:captureArgBlock]; + + [constraint evaluate:@"foo"]; + XCTAssertEqualObjects(@"foo", captured, @"Should have captured value from last invocation."); + [constraint evaluate:@"bar"]; + XCTAssertEqualObjects(@"bar", captured, @"Should have captured value from last invocation."); +} + +- (void)testEvaluateNilBlockReturnsNo +{ + OCMBlockConstraint *constraint = [[OCMBlockConstraint alloc] initWithConstraintBlock:nil]; + + XCTAssertFalse([constraint evaluate:@"foo"]); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMInvocationMatcherTests.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMInvocationMatcherTests.m new file mode 100644 index 0000000000..bb01e324f3 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMInvocationMatcherTests.m @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2014-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCMock.h" +#import "OCMInvocationMatcher.h" +#import "OCMFunctionsPrivate.h" + + +@interface TestClassForRecorder : NSObject + +- (void)methodWithInt:(int)i andObject:(id)o; + +- (void)methodWithClass:(Class)class; + +@end + +@implementation TestClassForRecorder + +- (void)methodWithInt:(int)i andObject:(id)o +{ +} + +- (void)methodWithClass:(Class)class +{ +} + +@end + +@interface OCMInvocationMatcherTests : XCTestCase + +@end + +@implementation OCMInvocationMatcherTests + +- (NSInvocation *)invocationForTargetClass:(Class)aClass selector:(SEL)aSelector +{ + NSMethodSignature *signature = [aClass instanceMethodSignatureForSelector:aSelector]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setSelector:aSelector]; + return invocation; +} + +- (void)testMatchesAliasedSelector +{ + OCMInvocationMatcher *matcher = [[OCMInvocationMatcher alloc] init]; + NSInvocation *recordedInvocation = [self invocationForTargetClass:[NSString class] selector:@selector(uppercaseString)]; + [matcher setInvocation:recordedInvocation]; + + SEL actual = OCMAliasForOriginalSelector(@selector(uppercaseString)); + + XCTAssertTrue([matcher matchesSelector:actual], @"Should have matched."); +} + +- (void)testOnlyMatchesInvocationWithRightArguments +{ + NSString *recorded = @"recorded"; + NSString *actual = @"actual"; + + OCMInvocationMatcher *matcher = [[OCMInvocationMatcher alloc] init]; + NSInvocation *recordedInvocation = [self invocationForTargetClass:[NSString class] selector:@selector(initWithString:)]; + [recordedInvocation setArgument:&recorded atIndex:2]; + [matcher setInvocation:recordedInvocation]; + + NSInvocation *testInvocation = [self invocationForTargetClass:[NSString class] selector:@selector(initWithString:)]; + [testInvocation setArgument:&actual atIndex:2]; + XCTAssertFalse([matcher matchesInvocation:testInvocation], @"Should not match."); +} + +-(void)testSelectivelyIgnoresNonObjectArguments +{ + id any = [OCMArg any]; + NSUInteger zero = 0; + NSString *arg1 = @"I (.*) mocks."; + NSUInteger arg2 = NSRegularExpressionSearch; + + OCMInvocationMatcher *matcher = [[OCMInvocationMatcher alloc] init]; + NSInvocation *recordedInvocation = [self invocationForTargetClass:[NSString class] selector:@selector(rangeOfString:options:)]; + [recordedInvocation setArgument:&any atIndex:2]; + [recordedInvocation setArgument:&zero atIndex:3]; + [matcher setInvocation:recordedInvocation]; + [matcher setIgnoreNonObjectArgs:YES]; + + NSInvocation *testInvocation = [self invocationForTargetClass:[NSString class] selector:@selector(rangeOfString:options:)]; + [testInvocation setArgument:&arg1 atIndex:2]; + [testInvocation setArgument:&arg2 atIndex:3]; + XCTAssertTrue([matcher matchesInvocation:testInvocation], @"Should match."); +} + +-(void)testSelectivelyIgnoresNonObjectArgumentsAndStillFailsWhenFollowingObjectArgsDontMatch +{ + int arg1 = 17; + NSString *recorded = @"recorded"; + NSString *actual = @"actual"; + + OCMInvocationMatcher *matcher = [[OCMInvocationMatcher alloc] init]; + NSInvocation *recordedInvocation = [self invocationForTargetClass:[TestClassForRecorder class] selector:@selector(methodWithInt:andObject:)]; + [recordedInvocation setArgument:&arg1 atIndex:2]; + [recordedInvocation setArgument:&recorded atIndex:3]; + [matcher setInvocation:recordedInvocation]; + [matcher setIgnoreNonObjectArgs:YES]; + + NSInvocation *testInvocation = [self invocationForTargetClass:[TestClassForRecorder class] selector:@selector(methodWithInt:andObject:)]; + [testInvocation setArgument:&arg1 atIndex:2]; + [testInvocation setArgument:&actual atIndex:3]; + XCTAssertFalse([matcher matchesInvocation:testInvocation], @"Should not match."); +} + +- (void)testMatchesInvocationWithClassObjectArgument +{ + Class arg1 = NSObject.class; + + OCMInvocationMatcher *matcher = [[OCMInvocationMatcher alloc] init]; + NSInvocation *recordedInvocation = [self invocationForTargetClass:[TestClassForRecorder class] selector:@selector(methodWithClass:)]; + [recordedInvocation setArgument:&arg1 atIndex:2]; + [matcher setInvocation:recordedInvocation]; + + NSInvocation *testInvocation = [self invocationForTargetClass:[TestClassForRecorder class] selector:@selector(methodWithClass:)]; + [testInvocation setArgument:&arg1 atIndex:2]; + XCTAssertTrue([matcher matchesInvocation:testInvocation], @"Should match."); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMNoEscapeBlockTests.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMNoEscapeBlockTests.m new file mode 100644 index 0000000000..5b0ff9989e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMNoEscapeBlockTests.m @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCMock.h" +#import "OCMFunctionsPrivate.h" + + +@interface NSString(NoEscapeBlock) +@end + +@implementation NSString(NoEscapeBlock) + +- (void)methodWithNoEscapeBlock:(void(NS_NOESCAPE ^)(void))block +{ +} + +@end + +// Verifies that the block being passed in is a noescape block. +@interface BlockCapturer : NSProxy +@end + +@implementation BlockCapturer +{ + XCTestExpectation *expectation; +} + +- (instancetype)initWithExpectation:(XCTestExpectation *)anExpectation +{ + expectation = anExpectation; + return self; +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector +{ + return [NSString instanceMethodSignatureForSelector:selector]; +} + +- (void)forwardInvocation:(NSInvocation *)invocation +{ + __unsafe_unretained id block; + [invocation getArgument:&block atIndex:2]; + if(OCMIsNonEscapingBlock(block)) + { + [expectation fulfill]; + } +} + +@end + + +@interface OCMNoEscapeBlockTests : XCTestCase +@end + +@implementation OCMNoEscapeBlockTests + +- (void)testThatBlocksAreNoEscape +{ + // This tests that this file is compiled with + // `-Xclang -fexperimental-optimized-noescape` or equivalent. + XCTestExpectation *expectation = [self expectationWithDescription:@"Block should be noescape"]; + id blockCapturer = [[BlockCapturer alloc] initWithExpectation:expectation]; + int i = 0; + [blockCapturer methodWithNoEscapeBlock:^{ + // Force i to be pulled into the closure. + (void)i; + }]; + [self waitForExpectationsWithTimeout:0 handler:nil]; +} + +- (void)testNoEscapeBlocksAreNotRetained +{ + // This tests that OCMock can handle noescape blocks. + // It crashes if it fails + id mock = [OCMockObject mockForClass:[NSString class]]; + [[mock stub] methodWithNoEscapeBlock:[OCMArg invokeBlock]]; + int i = 0; + [mock methodWithNoEscapeBlock:^{ + // Force i to be pulled into the closure. + (void)i; + }]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMQuantifierTests.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMQuantifierTests.m new file mode 100644 index 0000000000..060866399f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMQuantifierTests.m @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2016-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCMock.h" + + +@interface TestClassForQuantifiers : NSObject + +- (void)doStuff; + +@end + +@implementation TestClassForQuantifiers + +- (void)doStuff +{ +} + +@end + + +@interface OCMQuantifierTests : XCTestCase +{ + BOOL expectFailure; + BOOL didRecordFailure; +} + +@end + + +@implementation OCMQuantifierTests + +- (void)setUp +{ + expectFailure = NO; +} + +- (void)recordFailureWithDescription:(NSString *)description inFile:(NSString *)file atLine:(NSUInteger)line expected:(BOOL)expected +{ + if(expectFailure) + { + didRecordFailure = YES; + } + else + { + [super recordFailureWithDescription:description inFile:file atLine:line expected:expected]; + } +} + + +- (void)testExactlyThrowsWhenCountTooSmall +{ + id mock = OCMClassMock([TestClassForQuantifiers class]); + + [mock doStuff]; + + XCTAssertThrows([[[mock verify] withQuantifier:[OCMQuantifier exactly:2]] doStuff]); +} + +- (void)testExactlyMatchesCount +{ + id mock = OCMClassMock([TestClassForQuantifiers class]); + + [mock doStuff]; + [mock doStuff]; + + [[[mock verify] withQuantifier:[OCMQuantifier exactly:2]] doStuff]; +} + +- (void)testExactlyThrowsWhenCountTooLarge +{ + id mock = OCMClassMock([TestClassForQuantifiers class]); + + [mock doStuff]; + [mock doStuff]; + [mock doStuff]; + + XCTAssertThrows([[[mock verify] withQuantifier:[OCMQuantifier exactly:2]] doStuff]); +} + + +- (void)testAtLeastThrowsWhenMinimumCountIsNotReached +{ + id mock = OCMClassMock([TestClassForQuantifiers class]); + + [mock doStuff]; + + XCTAssertThrows([[[mock verify] withQuantifier:[OCMQuantifier atLeast:2]] doStuff]); +} + +- (void)testAtLeastMatchesMinimumCount +{ + id mock = OCMClassMock([TestClassForQuantifiers class]); + + [mock doStuff]; + [mock doStuff]; + + [[[mock verify] withQuantifier:[OCMQuantifier atLeast:2]] doStuff]; +} + +- (void)testAtLeastMatchesMoreThanMinimumCount +{ + id mock = OCMClassMock([TestClassForQuantifiers class]); + + [mock doStuff]; + [mock doStuff]; + [mock doStuff]; + + [[[mock verify] withQuantifier:[OCMQuantifier atLeast:2]] doStuff]; +} + +- (void)testAtLeastThrowsWhenInitializedWithZeroCount +{ + XCTAssertThrows([OCMQuantifier atLeast:0]); +} + + +- (void)testAtMostMatchesUpToMaximumCount +{ + id mock = OCMClassMock([TestClassForQuantifiers class]); + + [mock doStuff]; + + [[[mock verify] withQuantifier:[OCMQuantifier atMost:1]] doStuff]; +} + +- (void)testAtMostThrowsWhenMaximumCountIsExceeded +{ + id mock = OCMClassMock([TestClassForQuantifiers class]); + + [mock doStuff]; + [mock doStuff]; + + XCTAssertThrows([[[mock verify] withQuantifier:[OCMQuantifier atMost:1]] doStuff]); +} + +- (void)testAtMostThrowsWhenInitializedWithZeroCount +{ + XCTAssertThrows([OCMQuantifier atMost:0]); +} + + +- (void)testNeverThrowsWhenInvocationsOccurred +{ + id mock = OCMClassMock([TestClassForQuantifiers class]); + + [mock doStuff]; + + XCTAssertThrows([[[mock verify] withQuantifier:[OCMQuantifier never]] doStuff]); +} + + +- (void)testQuantifierMacro +{ + id mock = OCMClassMock([TestClassForQuantifiers class]); + [mock doStuff]; + [mock doStuff]; + OCMVerify(atLeast(2), [mock doStuff]); +} + +- (void)testQuantifierMacroFailure +{ + id mock = OCMClassMock([TestClassForQuantifiers class]); + expectFailure = YES; + OCMVerify(atLeast(1), [mock doStuff]); + expectFailure = NO; + XCTAssertTrue(didRecordFailure); +} + +@end + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMStubRecorderTests.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMStubRecorderTests.m new file mode 100644 index 0000000000..b15db8030d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMStubRecorderTests.m @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2004-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCMock.h" +#import "OCMObjectReturnValueProvider.h" +#import "OCMExceptionReturnValueProvider.h" +#import "OCMInvocationStub.h" + + +@interface OCMStubRecorderTests : XCTestCase + +@end + + +@implementation OCMStubRecorderTests + +- (void)testCreatesInvocationMatcher +{ + NSString *arg = @"I love mocks."; + + id mock = [OCMockObject mockForClass:[NSString class]]; + OCMStubRecorder *recorder = [[OCMStubRecorder alloc] initWithMockObject:mock]; + [(id)recorder stringByAppendingString:arg]; + + NSMethodSignature *signature = [NSString instanceMethodSignatureForSelector:@selector(stringByAppendingString:)]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setSelector:@selector(stringByAppendingString:)]; + [invocation setArgument:&arg atIndex:2]; + XCTAssertTrue([[recorder invocationMatcher] matchesInvocation:invocation], @"Should match."); +} + +- (void)testAddsReturnValueProvider +{ + id mock = [OCMockObject mockForClass:[NSString class]]; + OCMStubRecorder *recorder = [[OCMStubRecorder alloc] initWithMockObject:mock]; + [recorder andReturn:@"foo"]; + NSArray *actionList = [(OCMInvocationStub *)[recorder invocationMatcher] invocationActions]; + + XCTAssertEqual((NSUInteger)1, [actionList count], @"Should have added one action."); + XCTAssertEqualObjects([OCMObjectReturnValueProvider class], [[actionList objectAtIndex:0] class], @"Should have added correct action."); +} + +- (void)testAddsExceptionReturnValueProvider +{ + id mock = [OCMockObject mockForClass:[NSString class]]; + OCMStubRecorder *recorder = [[OCMStubRecorder alloc] initWithMockObject:mock]; + [recorder andThrow:[NSException exceptionWithName:@"TestException" reason:@"A reason" userInfo:nil]]; + NSArray *actionList = [(OCMInvocationStub *)[recorder invocationMatcher] invocationActions]; + + XCTAssertEqual((NSUInteger)1, [actionList count], @"Should have added one action."); + XCTAssertEqualObjects([OCMExceptionReturnValueProvider class], [[actionList objectAtIndex:0] class], @"Should have added correct action."); + +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectClassMethodMockingTests.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectClassMethodMockingTests.m new file mode 100644 index 0000000000..ef26fef67a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectClassMethodMockingTests.m @@ -0,0 +1,378 @@ +/* + * Copyright (c) 2013-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCMock.h" +#import "OCClassMockObject.h" +#import "OCPartialMockObject.h" + + +#pragma mark Helper classes + +@interface TestClassWithClassMethods : NSObject ++ (NSString *)foo; ++ (NSString *)bar; ++ (void)bazWithArgument:(id)argument; +- (NSString *)bar; +@end + +@implementation TestClassWithClassMethods + +static NSUInteger initializeCallCount = 0; + ++ (void)initialize +{ + initializeCallCount += 1; +} + ++ (NSUInteger)initializeCallCount +{ + return initializeCallCount; +} + ++ (NSString *)foo +{ + return @"Foo-ClassMethod"; +} + ++ (NSString *)bar +{ + return @"Bar-ClassMethod"; +} + ++ (void)bazWithArgument:(id)argument +{ +} + +- (NSString *)bar +{ + return @"Bar"; +} + +@end + + +@interface TestSubclassWithClassMethods : TestClassWithClassMethods + +@end + +@implementation TestSubclassWithClassMethods + +@end + + +@interface OCMockObjectClassMethodMockingTests : XCTestCase + +@end + + +@implementation OCMockObjectClassMethodMockingTests + +#pragma mark Tests stubbing class methods + +- (void)testCanStubClassMethod +{ + id mock = [OCMockObject mockForClass:[TestClassWithClassMethods class]]; + + [[[[mock stub] classMethod] andReturn:@"mocked"] foo]; + + XCTAssertEqualObjects(@"mocked", [TestClassWithClassMethods foo], @"Should have stubbed class method."); +} + +- (void)testCanExpectTheSameClassMethodMoreThanOnce +{ + id mock = [OCMockObject mockForClass:[TestClassWithClassMethods class]]; + [[[[mock expect] classMethod] andReturn:@"mocked-foo"] foo]; + [[[[mock expect] classMethod] andReturn:@"mocked-foo2"] foo]; + + XCTAssertEqualObjects(@"mocked-foo", [TestClassWithClassMethods foo], @"Should have stubbed class method 'foo'."); + XCTAssertEqualObjects(@"mocked-foo2", [TestClassWithClassMethods foo], @"Should have stubbed class method 'foo2'."); +} + +- (void)testClassReceivesMethodsAfterStopWasCalled +{ + id mock = [OCMockObject mockForClass:[TestClassWithClassMethods class]]; + + [[[[mock stub] classMethod] andReturn:@"mocked"] foo]; + [mock stopMocking]; + + XCTAssertEqualObjects(@"Foo-ClassMethod", [TestClassWithClassMethods foo], @"Should not have stubbed class method."); +} + +- (void)testClassReceivesMethodAgainWhenExpectedCallOccurred +{ + id mock = [OCMockObject mockForClass:[TestClassWithClassMethods class]]; + + [[[[mock expect] classMethod] andReturn:@"mocked"] foo]; + + XCTAssertEqualObjects(@"mocked", [TestClassWithClassMethods foo], @"Should have stubbed method."); + XCTAssertEqualObjects(@"Foo-ClassMethod", [TestClassWithClassMethods foo], @"Should have 'unstubbed' method."); +} + +- (void)testCanStubClassMethodFromMockForSubclass +{ + id subclassMock = [OCMockObject mockForClass:[TestSubclassWithClassMethods class]]; + + [[[[subclassMock stub] classMethod] andReturn:@"mocked-subclass"] foo]; + XCTAssertEqualObjects(@"mocked-subclass", [TestSubclassWithClassMethods foo], @"Should have stubbed method."); + XCTAssertEqualObjects(@"Foo-ClassMethod", [TestClassWithClassMethods foo], @"Should not have stubbed method in superclass."); +} + +- (void)testSuperclassReceivesMethodsAfterStopWasCalled +{ + id mock = [OCMockObject mockForClass:[TestSubclassWithClassMethods class]]; + + [[[[mock stub] classMethod] andReturn:@"mocked"] foo]; + [mock stopMocking]; + + XCTAssertEqualObjects(@"Foo-ClassMethod", [TestSubclassWithClassMethods foo], @"Should not have stubbed class method."); +} + +- (void)testCanReplaceSameMethodInSubclassAfterSuperclassMockWasStopped +{ + id superclassMock = [OCMockObject mockForClass:[TestClassWithClassMethods class]]; + id subclassMock = [OCMockObject mockForClass:[TestSubclassWithClassMethods class]]; + + [[[[superclassMock stub] classMethod] andReturn:@"mocked-superclass"] foo]; + [superclassMock stopMocking]; + + [[[[subclassMock stub] classMethod] andReturn:@"mocked-subclass"] foo]; + XCTAssertEqualObjects(@"mocked-subclass", [TestSubclassWithClassMethods foo], @"Should have stubbed method"); +} + +- (void)testCanReplaceSameMethodInSuperclassAfterSubclassMockWasStopped +{ + id superclassMock = [OCMockObject mockForClass:[TestClassWithClassMethods class]]; + id subclassMock = [OCMockObject mockForClass:[TestSubclassWithClassMethods class]]; + + [[[[subclassMock stub] classMethod] andReturn:@"mocked-subclass"] foo]; + [subclassMock stopMocking]; + + [[[[superclassMock stub] classMethod] andReturn:@"mocked-superclass"] foo]; + XCTAssertEqualObjects(@"mocked-superclass", [TestClassWithClassMethods foo], @"Should have stubbed method"); +} + +- (void)testStubbingIsOnlyActiveAtTheClassItWasAdded +{ + // stage 1: stub in superclass affects only superclass + id superclassMock = [OCMockObject mockForClass:[TestClassWithClassMethods class]]; + [[[[superclassMock stub] classMethod] andReturn:@"mocked-superclass"] foo]; + XCTAssertEqualObjects(@"mocked-superclass", [TestClassWithClassMethods foo], @"Should have stubbed method"); + XCTAssertEqualObjects(@"Foo-ClassMethod", [TestSubclassWithClassMethods foo], @"Should NOT have stubbed method"); + [superclassMock stopMocking]; + + // stage 2: stub in subclass affects only subclass + id subclassMock = [OCMockObject mockForClass:[TestSubclassWithClassMethods class]]; + [[[[subclassMock stub] classMethod] andReturn:@"mocked-subclass"] foo]; + XCTAssertEqualObjects(@"Foo-ClassMethod", [TestClassWithClassMethods foo], @"Should NOT have stubbed method"); + XCTAssertEqualObjects(@"mocked-subclass", [TestSubclassWithClassMethods foo], @"Should have stubbed method"); + [subclassMock stopMocking]; + + // stage 3: like stage 1; also demonstrates that subclass cleared all stubs + id superclassMock2 = [OCMockObject mockForClass:[TestClassWithClassMethods class]]; + [[[[superclassMock2 stub] classMethod] andReturn:@"mocked-superclass"] foo]; + XCTAssertEqualObjects(@"mocked-superclass", [TestClassWithClassMethods foo], @"Should have stubbed method"); + XCTAssertEqualObjects(@"Foo-ClassMethod", [TestSubclassWithClassMethods foo], @"Should NOT have stubbed method"); +} + +- (void)testStubsOnlyClassMethodWhenInstanceMethodWithSameNameExists +{ + id mock = [OCMockObject mockForClass:[TestClassWithClassMethods class]]; + + [[[[mock stub] classMethod] andReturn:@"mocked"] bar]; + + XCTAssertEqualObjects(@"mocked", [TestClassWithClassMethods bar], @"Should have stubbed class method."); + XCTAssertThrows([mock bar], @"Should not have stubbed instance method."); +} + +- (void)testStubsClassMethodWhenNoInstanceMethodExistsWithName +{ + id mock = [OCMockObject mockForClass:[TestClassWithClassMethods class]]; + + [[[mock stub] andReturn:@"mocked"] foo]; + + XCTAssertEqualObjects(@"mocked", [TestClassWithClassMethods foo], @"Should have stubbed class method."); +} + +- (void)testStubsCanDistinguishInstanceAndClassMethods +{ + id mock = [OCMockObject mockForClass:[TestClassWithClassMethods class]]; + + [[[[mock stub] classMethod] andReturn:@"mocked-class"] bar]; + [[[mock stub] andReturn:@"mocked-instance"] bar]; + + XCTAssertEqualObjects(@"mocked-class", [TestClassWithClassMethods bar], @"Should have stubbed class method."); + XCTAssertEqualObjects(@"mocked-instance", [mock bar], @"Should have stubbed instance method."); +} + +- (void)testRevertsAllStubbedMethodsOnDealloc +{ + id mock = [[OCClassMockObject alloc] initWithClass:[TestClassWithClassMethods class]]; + + [[[[mock stub] classMethod] andReturn:@"mocked-foo"] foo]; + [[[[mock stub] classMethod] andReturn:@"mocked-bar"] bar]; + + XCTAssertEqualObjects(@"mocked-foo", [TestClassWithClassMethods foo], @"Should have stubbed class method 'foo'."); + XCTAssertEqualObjects(@"mocked-bar", [TestClassWithClassMethods bar], @"Should have stubbed class method 'bar'."); + + mock = nil; + + XCTAssertEqualObjects(@"Foo-ClassMethod", [TestClassWithClassMethods foo], @"Should have 'unstubbed' class method 'foo'."); + XCTAssertEqualObjects(@"Bar-ClassMethod", [TestClassWithClassMethods bar], @"Should have 'unstubbed' class method 'bar'."); +} + +- (void)testRevertsAllStubbedMethodsOnPartialMockDealloc +{ + id mock = [[OCPartialMockObject alloc] initWithClass:[TestClassWithClassMethods class]]; + + [[[[mock stub] classMethod] andReturn:@"mocked-foo"] foo]; + [[[[mock stub] classMethod] andReturn:@"mocked-bar"] bar]; + + XCTAssertEqualObjects(@"mocked-foo", [TestClassWithClassMethods foo], @"Should have stubbed class method 'foo'."); + XCTAssertEqualObjects(@"mocked-bar", [TestClassWithClassMethods bar], @"Should have stubbed class method 'bar'."); + + mock = nil; + + XCTAssertEqualObjects(@"Foo-ClassMethod", [TestClassWithClassMethods foo], @"Should have 'unstubbed' class method 'foo'."); + XCTAssertEqualObjects(@"Bar-ClassMethod", [TestClassWithClassMethods bar], @"Should have 'unstubbed' class method 'bar'."); +} + +- (void)testSecondClassMockDeactivatesFirst +{ + id mock1 = [[OCClassMockObject alloc] initWithClass:[TestClassWithClassMethods class]]; + [[[mock1 stub] andReturn:@"mocked-foo-1"] foo]; + + id mock2 = [[OCClassMockObject alloc] initWithClass:[TestClassWithClassMethods class]]; + XCTAssertEqualObjects(@"Foo-ClassMethod", [TestClassWithClassMethods foo]); + + [mock2 stopMocking]; + XCTAssertNoThrow([TestClassWithClassMethods foo]); +} + +- (void)testStopMockingDisposesMetaClass +{ + id mock = [[OCClassMockObject alloc] initWithClass:[TestClassWithClassMethods class]]; + + char *createdSubclassName = strdup(object_getClassName([TestClassWithClassMethods class])); + XCTAssertNotNil(objc_lookUpClass(createdSubclassName)); + + [mock stopMocking]; + XCTAssertNil(objc_lookUpClass(createdSubclassName)); + free(createdSubclassName); +} + +- (void)testSecondClassMockDisposesFirstMetaClass +{ + id mock1 = [[OCClassMockObject alloc] initWithClass:[TestClassWithClassMethods class]]; + char *createdSubclassName1 = strdup(object_getClassName([TestClassWithClassMethods class])); + XCTAssertNotNil(objc_lookUpClass(createdSubclassName1)); + + id mock2 = [[OCClassMockObject alloc] initWithClass:[TestClassWithClassMethods class]]; + char *createdSubclassName2 = strdup(object_getClassName([TestClassWithClassMethods class])); + XCTAssertNotNil(objc_lookUpClass(createdSubclassName2)); + + [mock1 stopMocking]; + [mock2 stopMocking]; + + XCTAssertNil(objc_lookUpClass(createdSubclassName1)); + XCTAssertNil(objc_lookUpClass(createdSubclassName2)); + + free(createdSubclassName1); + free(createdSubclassName2); +} + +- (void)testForwardToRealObject +{ + NSString *classFooValue = [TestClassWithClassMethods foo]; + NSString *classBarValue = [TestClassWithClassMethods bar]; + id mock = [OCMockObject mockForClass:[TestClassWithClassMethods class]]; + + [[[[mock expect] classMethod] andForwardToRealObject] foo]; + NSString *result = [TestClassWithClassMethods foo]; + XCTAssertEqualObjects(result, classFooValue); + XCTAssertNoThrow([mock verify]); + + [[[mock expect] andForwardToRealObject] foo]; + result = [TestClassWithClassMethods foo]; + XCTAssertEqualObjects(result, classFooValue); + XCTAssertNoThrow([mock verify]); + + [[[[mock expect] classMethod] andForwardToRealObject] bar]; + result = [TestClassWithClassMethods bar]; + XCTAssertEqualObjects(result, classBarValue); + XCTAssertNoThrow([mock verify]); + + [[[[mock expect] classMethod] andForwardToRealObject] bar]; + XCTAssertThrowsSpecificNamed([mock bar], NSException, NSInternalInconsistencyException, @""); + + [[[mock expect] andForwardToRealObject] bar]; + XCTAssertThrowsSpecificNamed([mock bar], NSException, NSInternalInconsistencyException, @"Did not get the exception saying andForwardToRealObject not supported"); + + [[[mock expect] andForwardToRealObject] foo]; + XCTAssertThrows([mock foo]); +} + +- (void)testRefusesToCreateClassMockForNilClass +{ + XCTAssertThrows(OCMClassMock(nil)); +} + +- (void)testInitializeIsNotCalledOnMockedClass +{ + NSUInteger countBefore = [TestClassWithClassMethods initializeCallCount]; + + id mock = [OCMockObject mockForClass:[TestClassWithClassMethods class]]; + [TestClassWithClassMethods foo]; + [[mock verify] foo]; + + NSUInteger countAfter = [TestClassWithClassMethods initializeCallCount]; + + XCTAssertEqual(countBefore, countAfter, @"Creating a mock should not have resulted in call to +initialize"); +} + +- (void)testCanStubNSObjectClassMethodsIncludingAlloc +{ + TestClassWithClassMethods *dummyObject = [[TestClassWithClassMethods alloc] init]; + + id mock = [OCMockObject mockForClass:[TestClassWithClassMethods class]]; + [[[mock stub] andReturn:dummyObject] new]; + + id newObject = [TestClassWithClassMethods new]; + + XCTAssertEqualObjects(dummyObject, newObject, @"Should have stubbed +new method"); +} + +- (void)testArgumentsGetReleasedAfterStopMocking +{ + __weak id weakArgument; + id mock = OCMClassMock([TestClassWithClassMethods class]); + @autoreleasepool { + NSObject *argument = [NSObject new]; + weakArgument = argument; + [TestClassWithClassMethods bazWithArgument:argument]; + [mock stopMocking]; + } + XCTAssertNil(weakArgument); +} + +- (void)testThrowsWhenAttemptingToStubClassMethodOnStoppedMock +{ + id mock = [OCClassMockObject mockForClass:[TestClassWithClassMethods class]]; + [mock stopMocking]; + XCTAssertThrowsSpecificNamed([[[mock stub] andReturn:@"hello"] foo], NSException, NSInternalInconsistencyException); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectDynamicPropertyMockingTests.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectDynamicPropertyMockingTests.m new file mode 100644 index 0000000000..904ce33930 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectDynamicPropertyMockingTests.m @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2015-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCMock.h" + + +#pragma mark Helper classes + +@interface TestClassWithDynamicProperties : NSObject +@property(nonatomic, copy) NSDictionary *anObject; +@property(nonatomic, assign) NSUInteger aUInt; +@property(nonatomic, assign) NSInteger __aPrivateInt; +@property(getter=customGetter, setter=customSetter:) NSDictionary *aCustomProperty; + +@end + +@implementation TestClassWithDynamicProperties +@dynamic anObject; +@dynamic aUInt; +@dynamic __aPrivateInt; +@dynamic aCustomProperty; + +@end + + +@interface OCMockObjectDynamicPropertyMockingTests : XCTestCase + +@end + +@implementation OCMockObjectDynamicPropertyMockingTests + +#pragma mark Tests stubbing dynamic properties + +- (void)testCanStubDynamicPropertiesWithIdType +{ + id mock = [OCMockObject mockForClass:[TestClassWithDynamicProperties class]]; + NSDictionary *testDict = @{@"test-key" : @"test-value"}; + [[[mock stub] andReturn:testDict] anObject]; + XCTAssertEqualObjects(testDict, [mock anObject]); +} + +- (void)testCanStubDynamicPropertiesWithUIntType +{ + id mock = [OCMockObject mockForClass:[TestClassWithDynamicProperties class]]; + NSUInteger someUInt = 5; + [[[mock stub] andReturnValue:OCMOCK_VALUE(someUInt)] aUInt]; + XCTAssertEqual(5, [mock aUInt]); +} + +- (void)testCanStubDynamicPropertiesWithIntType +{ + id mock = [OCMockObject mockForClass:[TestClassWithDynamicProperties class]]; + NSInteger someInt = -10; + [[[mock stub] andReturnValue:OCMOCK_VALUE(someInt)] __aPrivateInt]; + XCTAssertEqual(-10, [mock __aPrivateInt]); +} + +- (void)testCanStubDynamicPropertiesWithCustomGetter +{ + id mock = [OCMockObject mockForClass:[TestClassWithDynamicProperties class]]; + NSDictionary *testDict = @{@"test-key" : @"test-value"}; + [[[mock stub] andReturn:testDict] customGetter]; + XCTAssertEqualObjects(testDict, [mock customGetter]); +} + +- (void)testCanMockSetterForDynamicProperty +{ + id mock = [OCMockObject mockForClass:[TestClassWithDynamicProperties class]]; + NSDictionary *dummyObject = @{@"test-key" : @"test-value"}; + + [[mock expect] setAnObject:dummyObject]; + [mock setAnObject:dummyObject]; + [mock verify]; +} + +- (void)testCanMockSetterForDynamicPropertyWithCustomSetter +{ + id mock = [OCMockObject mockForClass:[TestClassWithDynamicProperties class]]; + NSDictionary *dummyObject = @{@"test-key" : @"test-value"}; + + [[mock expect] customSetter:dummyObject]; + [mock customSetter:dummyObject]; + [mock verify]; + +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectForwardingTargetTests.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectForwardingTargetTests.m new file mode 100644 index 0000000000..2bc1354686 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectForwardingTargetTests.m @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2013-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import +#import "OCMock.h" + + +#pragma mark Helper classes + +@interface InternalObject : NSObject +{ + NSString *_name; +} +@property (nonatomic, strong) NSString *name; +@end + +@interface PublicObject : NSObject +{ + InternalObject *_internal; +}; +@property (nonatomic, strong) NSString *name; +@end + +@implementation InternalObject + +@synthesize name = _name; + +@end + + +@implementation PublicObject + +@dynamic name; + +- (instancetype)initWithInternal:(InternalObject *)internal +{ + self = [super init]; + if (!self) + return self; + + _internal = internal; + return self; +} + +- (instancetype)init +{ + return [self initWithInternal:[[InternalObject alloc] init]]; +} + +- (id)forwardingTargetForSelector:(SEL)selector +{ + if (selector == @selector(name) || + selector == @selector(setName:)) + return _internal; + return [super forwardingTargetForSelector:selector]; +} + ++ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)selector +{ + NSMethodSignature *signature = [super instanceMethodSignatureForSelector:selector]; + if (signature) + return signature; + else + return [InternalObject instanceMethodSignatureForSelector:selector]; +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector +{ + NSMethodSignature *signature = [super methodSignatureForSelector:selector]; + if (signature) + return signature; + + return [[self forwardingTargetForSelector:selector] methodSignatureForSelector:selector]; +} + +- (BOOL)respondsToSelector:(SEL)selector +{ + if ([super respondsToSelector:selector]) + return YES; + + return [[self forwardingTargetForSelector:selector] respondsToSelector:selector]; +} + ++ (BOOL)instancesRespondToSelector:(SEL)selector +{ + if (class_respondsToSelector(self, selector)) + return YES; + + return [InternalObject instancesRespondToSelector:selector]; +} + +- (id)valueForUndefinedKey:(NSString *)key +{ + return [_internal valueForKey:key]; +} + +- (void)setValue:(id)value forUndefinedKey:(NSString *)key +{ + [_internal setValue:value forKey:key]; +} + +@end + + +#pragma mark Tests + + +@interface OCMockForwardingTargetTests : XCTestCase + +@end + + +@implementation OCMockForwardingTargetTests + +- (void)testNameShouldForwardToInternal +{ + InternalObject *internal = [[InternalObject alloc] init]; + internal.name = @"Internal Object"; + PublicObject *public = [[PublicObject alloc] initWithInternal:internal]; + XCTAssertEqualObjects(@"Internal Object", public.name); +} + +- (void)testStubsMethodImplementation +{ + PublicObject *public = [[PublicObject alloc] init]; + id mock = [OCMockObject partialMockForObject:public]; + + [[[mock stub] andReturn:@"FOO"] name]; + XCTAssertEqualObjects(@"FOO", [mock name]); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectHamcrestTests.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectHamcrestTests.m new file mode 100644 index 0000000000..dee620e997 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectHamcrestTests.m @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2004-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import +#import "OCMock.h" + + +@interface OCMockObjectHamcrestTests : XCTestCase + +@end + + +@implementation OCMockObjectHamcrestTests + +- (void)testAcceptsStubbedMethodWithHamcrestConstraint +{ + id mock = [OCMockObject mockForClass:[NSString class]]; + [[mock stub] hasSuffix:(id)startsWith(@"foo")]; + [mock hasSuffix:@"foobar"]; +} + + +- (void)testRejectsUnstubbedMethodWithHamcrestConstraint +{ + id mock = [OCMockObject mockForClass:[NSString class]]; + [[mock stub] hasSuffix:(id)anyOf(equalTo(@"foo"), equalTo(@"bar"), NULL)]; + XCTAssertThrows([mock hasSuffix:@"foobar"], @"Should have raised an exception."); +} + + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectInternalTests.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectInternalTests.m new file mode 100644 index 0000000000..22418e39fb --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectInternalTests.m @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2019-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCMock.h" + + +#pragma mark Helper classes + +@interface TestClassForInternalTests : NSObject + +@property (nonatomic, copy) NSString *title; + +- (void)doStuffWithClass:(Class)aClass; + +@end + +@implementation TestClassForInternalTests + +@synthesize title; + +- (void)doStuffWithClass:(Class)aClass +{ + // stubbed out anyway +} + +@end + + + +@interface OCMockObjectInternalTests : XCTestCase + +@end + +@implementation OCMockObjectInternalTests + +#pragma mark Tests + +- (void)testReRaisesFailFastExceptionsOnVerify +{ + id mock = [OCMockObject mockForClass:[NSString class]]; + @try + { + [mock lowercaseString]; + } + @catch(NSException *exception) + { + // expected + } + XCTAssertThrows([mock verify], @"Should have reraised the exception."); +} + + +- (void)testDoesNotReRaiseStubbedExceptions +{ + id mock = [OCMockObject mockForClass:[NSString class]]; + [[[mock expect] andThrow:[NSException exceptionWithName:@"ExceptionForTest" reason:@"test" userInfo:nil]] lowercaseString]; + @try + { + [mock lowercaseString]; + } + @catch(NSException *exception) + { + // expected + } + XCTAssertNoThrow([mock verify], @"Should not have reraised stubbed exception."); + +} + +- (void)testAndThrowDoesntLeak +{ + __weak NSException *exception = nil; + @autoreleasepool { + id mock = [OCMockObject partialMockForObject:[NSProcessInfo processInfo]]; + exception = [NSException exceptionWithName:NSGenericException + reason:nil + userInfo:nil]; + [[[mock expect] andThrow:exception] arguments]; + + BOOL threw = NO; + @try + { + [[NSProcessInfo processInfo] arguments]; + } + @catch (NSException *ex) + { + threw = YES; + } + XCTAssertTrue(threw); + [mock verify]; [mock stopMocking]; mock = nil; + } + + XCTAssertNil(exception, @"The exception should have been released by now"); +} + +- (void)testReRaisesRejectExceptionsOnVerify +{ + id mock = [OCMockObject niceMockForClass:[NSString class]]; + [[mock reject] uppercaseString]; + @try + { + [mock uppercaseString]; + } + @catch(NSException *exception) + { + // expected + } + XCTAssertThrows([mock verify], @"Should have reraised the exception."); +} + + +- (void)testCanCreateExpectationsAfterInvocations +{ + id mock = [OCMockObject mockForClass:[NSString class]]; + [[mock expect] lowercaseString]; + [mock lowercaseString]; + [mock expect]; +} + + +- (void)testArgumentConstraintsAreOnlyCalledAsOftenAsTheMethodIsCalled +{ + __block int count = 0; + + id mock = [OCMockObject mockForClass:[NSString class]]; + [[mock stub] hasSuffix:[OCMArg checkWithBlock:^(id value) { count++; return YES; }]]; + + [mock hasSuffix:@"foo"]; + [mock hasSuffix:@"bar"]; + + XCTAssertEqual(2, count, @"Should have evaluated constraint only twice"); +} + + +- (void)testVerifyWithDelayDoesNotWaitForRejects +{ + id mock = [OCMockObject niceMockForClass:[NSString class]]; + + [[mock reject] hasSuffix:OCMOCK_ANY]; + [[mock expect] hasPrefix:OCMOCK_ANY]; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [mock hasPrefix:@"foo"]; + }); + + NSDate *start = [NSDate date]; + [mock verifyWithDelay:4]; + NSDate *end = [NSDate date]; + + XCTAssertTrue([end timeIntervalSinceDate:start] < 3, @"Should have returned before delay was up"); +} + + +- (void)testDoesNotReinitialiseMockWhenInitIsCalledMoreThanOnce +{ + id mock = OCMClassMock([TestClassForInternalTests class]); + OCMStub([mock alloc]).andReturn(mock); + OCMStub([mock title]).andReturn(@"foo"); + + TestClassForInternalTests *object = [[TestClassForInternalTests alloc] init]; + XCTAssertEqualObjects(@"foo", object.title); +} + + +- (void)testClassArgsAreRetained +{ + + id mockWithClassMethod = OCMClassMock([TestClassForInternalTests class]); + @autoreleasepool { + [[mockWithClassMethod stub] doStuffWithClass:[OCMArg any]]; + } + XCTAssertNoThrow([mockWithClassMethod doStuffWithClass:[NSString class]]); +} + + +- (void)testArgumentsGetReleasedAfterStopMocking +{ + __weak id weakArgument; + id mock = OCMClassMock([TestClassForInternalTests class]); + @autoreleasepool { + NSMutableString *title = [NSMutableString new]; + weakArgument = title; + [mock setTitle:title]; + [mock stopMocking]; + } + XCTAssertNil(weakArgument); +} + +- (void)testRaisesWhenAttemptingToVerifyInvocationsAfterStopMocking +{ + id mock = OCMClassMock([TestClassForInternalTests class]); + + [mock title]; + [mock stopMocking]; + + @try { + [[mock verify] title]; + XCTFail(@"Should have thrown an NSInternalInconsistencyException when attempting to verify after stopMocking."); + } @catch (NSException *ex) { + XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException); + XCTAssertTrue([ex.reason containsString:[mock description]]); + } +} + +- (void)testRaisesWhenAttemptingToUseAfterStopMocking +{ + id mock = OCMClassMock([TestClassForInternalTests class]); + + [mock stopMocking]; + + @try { + [mock title]; + XCTFail(@"Should have thrown an NSInternalInconsistencyException when attempting to use after stopMocking."); + } @catch (NSException *ex) { + XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException); + XCTAssertTrue([ex.reason containsString:[mock description]]); + } +} + + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectMacroTests.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectMacroTests.m new file mode 100644 index 0000000000..43e3e5a4f1 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectMacroTests.m @@ -0,0 +1,587 @@ +/* + * Copyright (c) 2014-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCMock.h" + + +@protocol TestProtocolForMacroTesting +- (NSString *)stringValue; +@end + +@interface TestClassForMacroTesting : NSObject + +@end + +@implementation TestClassForMacroTesting + +- (NSString *)stringValue +{ + return @"FOO"; +} + +@end + + +@interface TestClassWithDecimalReturnMethod : NSObject + +- (NSDecimalNumber*)method; + +@end + +@implementation TestClassWithDecimalReturnMethod + +- (NSDecimalNumber*)method { + return nil; +} + +@end + +@interface TestClassWithClassReturnMethod : NSObject + +- (Class)method; + +@end + +@implementation TestClassWithClassReturnMethod + +- (Class)method +{ + return [self class]; +} + +@end + + + +// implemented in OCMockObjectClassMethodMockingTests + +@interface TestClassWithClassMethods : NSObject ++ (NSString *)foo; ++ (NSString *)bar; +- (NSString *)bar; +@end + + + +@interface OCMockObjectMacroTests : XCTestCase +{ + BOOL shouldCaptureFailure; + NSString *reportedDescription; + NSString *reportedFile; + NSUInteger reportedLine; +} + +@end + + +@implementation OCMockObjectMacroTests + +- (void)recordFailureWithDescription:(NSString *)description inFile:(NSString *)file atLine:(NSUInteger)line expected:(BOOL)expected +{ + if(shouldCaptureFailure) + { + reportedDescription = description; + reportedFile = file; + reportedLine = line; + } + else + { + [super recordFailureWithDescription:description inFile:file atLine:line expected:expected]; + } +} + + +- (void)testReportsVerifyFailureWithCorrectLocation +{ + id mock = OCMClassMock([NSString class]); + + [[mock expect] lowercaseString]; + + shouldCaptureFailure = YES; + OCMVerifyAll(mock); const char *expectedFile = __FILE__; int expectedLine = __LINE__; + shouldCaptureFailure = NO; + + XCTAssertNotNil(reportedDescription, @"Should have recorded a failure with description."); + XCTAssertEqualObjects([NSString stringWithUTF8String:expectedFile], reportedFile, @"Should have reported correct file."); + XCTAssertEqual(expectedLine, (int)reportedLine, @"Should have reported correct line"); +} + +- (void)testReportsIgnoredExceptionsAtVerifyLocation +{ + id mock = OCMClassMock([NSString class]); + + [[mock reject] lowercaseString]; + + @try + { + [mock lowercaseString]; + } + @catch (NSException *exception) + { + // ignore; the mock will rethrow this in verify + } + + shouldCaptureFailure = YES; + OCMVerifyAll(mock); const char *expectedFile = __FILE__; int expectedLine = __LINE__; + shouldCaptureFailure = NO; + + XCTAssertTrue([reportedDescription rangeOfString:@"ignored"].location != NSNotFound, @"Should have reported ignored exceptions."); + XCTAssertEqualObjects([NSString stringWithUTF8String:expectedFile], reportedFile, @"Should have reported correct file."); + XCTAssertEqual(expectedLine, (int)reportedLine, @"Should have reported correct line"); +} + +- (void)testReportsVerifyWithDelayFailureWithCorrectLocation +{ + id mock = OCMClassMock([NSString class]); + + [[mock expect] lowercaseString]; + + shouldCaptureFailure = YES; + OCMVerifyAllWithDelay(mock, 0.05); const char *expectedFile = __FILE__; int expectedLine = __LINE__; + shouldCaptureFailure = NO; + + XCTAssertNotNil(reportedDescription, @"Should have recorded a failure with description."); + XCTAssertEqualObjects([NSString stringWithUTF8String:expectedFile], reportedFile, @"Should have reported correct file."); + XCTAssertEqual(expectedLine, (int)reportedLine, @"Should have reported correct line"); +} + + +- (void)testSetsUpStubsForCorrectMethods +{ + id mock = OCMStrictClassMock([NSString class]); + + OCMStub([mock uppercaseString]).andReturn(@"TEST_STRING"); + + XCTAssertEqualObjects(@"TEST_STRING", [mock uppercaseString], @"Should have returned stubbed value"); + XCTAssertThrows([mock lowercaseString]); +} + +- (void)testSetsUpStubsWithNonObjectReturnValues +{ + id mock = OCMStrictClassMock([NSString class]); + + OCMStub([mock boolValue]).andReturn(YES); + + XCTAssertEqual(YES, [mock boolValue], @"Should have returned stubbed value"); +} + +- (void)testSetsUpStubsWithStructureReturnValues +{ + id mock = OCMStrictClassMock([NSString class]); + + NSRange expected = NSMakeRange(123, 456); + OCMStub([mock rangeOfString:[OCMArg any]]).andReturn(expected); + + NSRange actual = [mock rangeOfString:@"substring"]; + XCTAssertEqual((NSUInteger)123, actual.location, @"Should have returned stubbed value"); + XCTAssertEqual((NSUInteger)456, actual.length, @"Should have returned stubbed value"); +} + +- (void)testSetsUpStubReturningNilForIdReturnType +{ + id mock = OCMPartialMock([NSArray arrayWithObject:@"Foo"]); + + OCMStub([mock lastObject]).andReturn(nil); + XCTAssertNil([mock lastObject], @"Should have returned stubbed value"); +} + +- (void)testSetsUpStubReturningNilForClassReturnType +{ + id mock = OCMPartialMock([[TestClassWithClassReturnMethod alloc] init]); + + OCMStub([mock method]).andReturn(Nil); + XCTAssertNil([mock method], @"Should have returned stubbed value"); + + // sometimes nil is used where Nil should be used + OCMStub([mock method]).andReturn(nil); + XCTAssertNil([mock method], @"Should have returned stubbed value"); +} + +- (void)testSetsUpExceptionThrowing +{ + id mock = OCMClassMock([NSString class]); + + OCMStub([mock uppercaseString]).andThrow([NSException exceptionWithName:@"TestException" reason:@"Testing" userInfo:nil]); + + XCTAssertThrowsSpecificNamed([mock uppercaseString], NSException, @"TestException", @"Should have thrown correct exception"); +} + +- (void)testSetsUpNotificationPostingAndNotificationObserving +{ + id mock = OCMProtocolMock(@protocol(TestProtocolForMacroTesting)); + + NSNotification *n = [NSNotification notificationWithName:@"TestNotification" object:nil]; + + id observer = OCMObserverMock(); + [[NSNotificationCenter defaultCenter] addMockObserver:observer name:[n name] object:nil]; + OCMExpect([observer notificationWithName:[n name] object:[OCMArg any]]); + + OCMStub([mock stringValue]).andPost(n); + + [mock stringValue]; + + OCMVerifyAll(observer); +} + +- (void)testNotificationObservingWithUserInfo +{ + id observer = OCMObserverMock(); + [[NSNotificationCenter defaultCenter] addMockObserver:observer name:@"TestNotificationWithInfo" object:nil]; + OCMExpect([observer notificationWithName:@"TestNotificationWithInfo" object:[OCMArg any] userInfo:[OCMArg any]]); + + [[NSNotificationCenter defaultCenter] postNotificationName:@"TestNotificationWithInfo" object:self userInfo:@{ @"foo": @"bar" }]; + + OCMVerifyAll(observer); +} + + +- (void)testSetsUpSubstituteCall +{ + id mock = OCMStrictProtocolMock(@protocol(TestProtocolForMacroTesting)); + + OCMStub([mock stringValue]).andCall(self, @selector(stringValueForTesting)); + + XCTAssertEqualObjects([mock stringValue], @"TEST_STRING_FROM_TESTCASE", @"Should have called method from test case"); +} + +- (NSString *)stringValueForTesting +{ + return @"TEST_STRING_FROM_TESTCASE"; +} + + +- (void)testCanChainPropertyBasedActions +{ + id mock = OCMPartialMock([[TestClassForMacroTesting alloc] init]); + + __block BOOL didCallBlock = NO; + void (^theBlock)(NSInvocation *) = ^(NSInvocation *invocation) + { + didCallBlock = YES; + }; + + OCMStub([mock stringValue]).andDo(theBlock).andForwardToRealObject(); + + NSString *actual = [mock stringValue]; + + XCTAssertTrue(didCallBlock, @"Should have called block"); + XCTAssertEqualObjects(@"FOO", actual, @"Should have forwarded invocation"); +} + + +- (void)testCanUseVariablesInInvocationSpec +{ + id mock = OCMStrictClassMock([NSString class]); + + NSString *expected = @"foo"; + OCMStub([mock rangeOfString:expected]).andReturn(NSMakeRange(0, 3)); + + XCTAssertThrows([mock rangeOfString:@"bar"], @"Should not have accepted invocation with non-matching arg."); +} + + +- (void)testSetsUpExpectations +{ + id mock = OCMClassMock([TestClassForMacroTesting class]); + + OCMExpect([mock stringValue]).andReturn(@"TEST_STRING"); + + XCTAssertThrows([mock verify], @"Should have complained about expected method not being invoked"); + + XCTAssertEqual([mock stringValue], @"TEST_STRING", @"Should have stubbed method, too"); + XCTAssertNoThrow([mock verify], @"Should have accepted invocation as matching expectation"); +} + + +- (void)testSetsUpReject +{ + id mock = OCMClassMock([TestClassForMacroTesting class]); + + OCMReject([mock stringValue]); + + XCTAssertNoThrow([mock verify], @"Should have accepted invocation rejected method not being invoked"); + XCTAssertThrows([mock stringValue], @"Should have complained during rejected method being invoked"); + XCTAssertThrows([mock verify], @"Should have complained about rejected method being invoked"); +} + +- (void)testThrowsWhenTryingToAddActionToReject +{ + id mock = OCMClassMock([TestClassForMacroTesting class]); + XCTAssertThrows(OCMReject([mock stringValue]).andReturn(@"Foo")); +} + +- (void)testShouldNotReportErrorWhenMethodWasInvoked +{ + id mock = OCMClassMock([NSString class]); + + [mock lowercaseString]; + + shouldCaptureFailure = YES; + OCMVerify([mock lowercaseString]); + shouldCaptureFailure = NO; + + XCTAssertNil(reportedDescription, @"Should not have recorded a failure."); +} + +- (void)testShouldReportErrorWhenMethodWasNotInvoked +{ + id mock = OCMClassMock([NSString class]); + + [mock lowercaseString]; + + shouldCaptureFailure = YES; + OCMVerify([mock uppercaseString]); const char *expectedFile = __FILE__; int expectedLine = __LINE__; + shouldCaptureFailure = NO; + + XCTAssertNotNil(reportedDescription, @"Should have recorded a failure with description."); + XCTAssertEqualObjects([NSString stringWithUTF8String:expectedFile], reportedFile, @"Should have reported correct file."); + XCTAssertEqual(expectedLine, (int)reportedLine, @"Should have reported correct line"); +} + +- (void)testShouldThrowDescriptiveExceptionWhenTryingToVerifyUnimplementedMethod +{ + id mock = OCMClassMock([NSString class]); + + // have not found a way to report the error; it seems we must throw an + // exception to get out of the forwarding machinery + XCTAssertThrowsSpecificNamed(OCMVerify([mock arrayByAddingObject:@"foo"]), + NSException, NSInvalidArgumentException, @"should throw NSInvalidArgumentException exception"); +} + + +- (void)testShouldThrowExceptionWhenNotUsingMockInMacroThatRequiresMock +{ + id realObject = [NSMutableArray array]; + + XCTAssertThrowsSpecificNamed(OCMStub([realObject addObject:@"foo"]), NSException, NSInternalInconsistencyException); + XCTAssertThrowsSpecificNamed(OCMExpect([realObject addObject:@"foo"]), NSException, NSInternalInconsistencyException); + XCTAssertThrowsSpecificNamed(OCMReject([realObject addObject:@"foo"]), NSException, NSInternalInconsistencyException); + XCTAssertThrowsSpecificNamed(OCMVerify([realObject addObject:@"foo"]), NSException, NSInternalInconsistencyException); +} + +- (void)testShouldHintAtPossibleReasonWhenNotUsingMockInMacroThatRequiresMock +{ + @try + { + id realObject = [NSMutableArray array]; + OCMStub([realObject addObject:@"foo"]); + } + @catch (NSException *e) + { + XCTAssertTrue([[e reason] containsString:@"The receiver is not a mock object."]); + } +} + +- (void)testShouldThrowExceptionWhenMockingMethodThatCannotBeMocked +{ + id mock = OCMClassMock([NSString class]); + + XCTAssertThrowsSpecificNamed(OCMStub([mock description]), NSException, NSInternalInconsistencyException); + XCTAssertThrowsSpecificNamed(OCMExpect([mock description]), NSException, NSInternalInconsistencyException); + XCTAssertThrowsSpecificNamed(OCMReject([mock description]), NSException, NSInternalInconsistencyException); + XCTAssertThrowsSpecificNamed(OCMVerify([mock description]), NSException, NSInternalInconsistencyException); +} + +- (void)testShouldHintAtPossibleReasonWhenMockingMethodThatCannotBeMocked +{ + @try + { + id mock = OCMClassMock([NSString class]); + OCMStub([mock description]); + } + @catch (NSException *e) + { + XCTAssertTrue([[e reason] containsString:@"The selector conflicts with a selector implemented by OCMStubRecorder/OCMExpectationRecorder."]); + } +} + +- (void)testShouldHintAtPossibleReasonWhenVerifyingMethodThatCannotBeMocked +{ + @try + { + id mock = OCMClassMock([NSString class]); + OCMVerify([mock description]); + } + @catch (NSException *e) + { + XCTAssertTrue([[e reason] containsString:@"The selector conflicts with a selector implemented by OCMVerifier."]); + } +} + + +- (void)testCanExplicitlySelectClassMethodForStubs +{ + id mock = OCMClassMock([TestClassWithClassMethods class]); + + OCMStub(ClassMethod([mock bar])).andReturn(@"mocked-class"); + OCMStub([mock bar]).andReturn(@"mocked-instance"); + + XCTAssertEqualObjects(@"mocked-class", [TestClassWithClassMethods bar], @"Should have stubbed class method."); + XCTAssertEqualObjects(@"mocked-instance", [mock bar], @"Should have stubbed instance method."); +} + +- (void)testSelectsInstanceMethodForStubsWhenAmbiguous +{ + id mock = OCMClassMock([TestClassWithClassMethods class]); + + OCMStub([mock bar]).andReturn(@"mocked-instance"); + + XCTAssertEqualObjects(@"mocked-instance", [mock bar], @"Should have stubbed instance method."); +} + +- (void)testSelectsClassMethodForStubsWhenUnambiguous +{ + id mock = OCMClassMock([TestClassWithClassMethods class]); + + OCMStub([mock foo]).andReturn(@"mocked-class"); + + XCTAssertEqualObjects(@"mocked-class", [TestClassWithClassMethods foo], @"Should have stubbed class method."); +} + + +- (void)testCanExplicitlySelectClassMethodForVerify +{ + id mock = OCMClassMock([TestClassWithClassMethods class]); + + [TestClassWithClassMethods bar]; + + OCMVerify(ClassMethod([mock bar])); +} + +- (void)testSelectsInstanceMethodForVerifyWhenAmbiguous +{ + id mock = OCMClassMock([TestClassWithClassMethods class]); + + [mock bar]; + + OCMVerify([mock bar]); +} + +- (void)testSelectsClassMethodForVerifyWhenUnambiguous +{ + id mock = OCMClassMock([TestClassWithClassMethods class]); + + [TestClassWithClassMethods foo]; + + OCMVerify([mock foo]); +} + + +- (void)testCanUseMacroToStubMethodWithDecimalReturnValue +{ + id mock = OCMClassMock([TestClassWithDecimalReturnMethod class]); + + OCMStub([mock method]).andReturn([NSDecimalNumber decimalNumberWithDecimal:[@0 decimalValue]]); + + XCTAssertEqualObjects([mock method], [NSDecimalNumber decimalNumberWithDecimal:[@0 decimalValue]]); +} + + +- (void)testCanUseMacroToStubMethodWithAnyNonObjectArgument +{ + id mock = OCMStrictClassMock([NSString class]); + + OCMStub([mock commonPrefixWithString:@"foo" options:0]).ignoringNonObjectArgs(); + + XCTAssertNoThrow([mock commonPrefixWithString:@"foo" options:NSCaseInsensitiveSearch]); +} + +- (void)testCanUseMacroToStubMethodWithAnyNonObjectArgumentChainedWithOCMStubRecorder +{ + id mock = OCMClassMock([NSString class]); + + OCMStub([mock commonPrefixWithString:@"foo" options:0]).ignoringNonObjectArgs().andReturn(@"f"); + + XCTAssertEqualObjects(@"f", [mock commonPrefixWithString:@"foo" options:NSCaseInsensitiveSearch]); +} + +- (void)testReturnsCorrectObjectFromInitMethodCalledOnRecorderInsideMacro +{ + // Because of the way the macros work, you can call recorder methods on the mock and they will + // work correctly. Technically these are a mix of old syntax and new. + // + // There are no assertions here, the tests will crash with an incorrect implementation. + // + // Note that the andReturn:nil has to be first because this is the stub that will actually be + // used and we're now making sure that a return value is specified for init methods. + id mock = OCMClassMock([NSString class]); + OCMStub([[mock andReturn:nil] initWithString:OCMOCK_ANY]); + OCMStub([[mock ignoringNonObjectArgs] initWithString:OCMOCK_ANY]); + OCMStub([[mock andReturnValue:nil] initWithString:OCMOCK_ANY]); + OCMStub([[mock andThrow:nil] initWithString:OCMOCK_ANY]); + OCMStub([[mock andPost:nil] initWithString:OCMOCK_ANY]); + OCMStub([[mock andCall:nil onObject:nil] initWithString:OCMOCK_ANY]); + OCMStub([[mock andDo:nil] initWithString:OCMOCK_ANY]); + OCMStub([[mock andForwardToRealObject] initWithString:OCMOCK_ANY]); + OCMExpect([[mock never] initWithString:OCMOCK_ANY]); + __unused id value = [mock initWithString:@"hello"]; + _OCMVerify([(id)[mock withQuantifier:nil] initWithString:OCMOCK_ANY]); + + // Test multiple levels of recorder methods. + OCMStub([[[[mock ignoringNonObjectArgs] andReturn:nil] andThrow:nil] initWithString:OCMOCK_ANY]); +} + +- (void)testStubMacroPassesExceptionThrough +{ + id mock = OCMClassMock([TestClassForMacroTesting class]); + @try + { + OCMStub([mock init]).andReturn(mock); + XCTFail(@"An exception should have been thrown."); + } + @catch(NSException *exception) + { + XCTAssertEqualObjects(exception.name, NSInternalInconsistencyException); + XCTAssertTrue([exception.reason containsString:@"Method init invoked twice on stub recorder"]); + } +} + +- (void)testExpectMacroPassesExceptionThrough +{ + id mock = OCMClassMock([TestClassForMacroTesting class]); + @try + { + OCMExpect([mock init]).andReturn(mock); + XCTFail(@"An exception should have been thrown."); + } + @catch(NSException *exception) + { + XCTAssertEqualObjects(exception.name, NSInternalInconsistencyException); + XCTAssertTrue([exception.reason containsString:@"Method init invoked twice on stub recorder"]); + } +} + +- (void)testVerifyMacroPassExceptionsThrough +{ + id mock = OCMClassMock([TestClassForMacroTesting class]); + @try + { + // The -Wunused-value is a workaround for https://bugs.llvm.org/show_bug.cgi?id=45245 + _Pragma("clang diagnostic push") + _Pragma("clang diagnostic ignored \"-Wunused-value\"") + OCMVerify([mock init]); + _Pragma("clang diagnostic pop") + XCTFail(@"An exception should have been thrown."); + } + @catch(NSException *exception) + { + XCTAssertEqualObjects(exception.name, NSInternalInconsistencyException); + XCTAssertTrue([exception.reason containsString:@"Method init invoked twice on verifier"]); + } +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectPartialMocksTests.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectPartialMocksTests.m new file mode 100644 index 0000000000..1fd2154dcd --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectPartialMocksTests.m @@ -0,0 +1,790 @@ +/* + * Copyright (c) 2013-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import +#import "OCMock.h" +#import "OCPartialMockObject.h" +#import "TestClassWithCustomReferenceCounting.h" + +#if TARGET_OS_IPHONE +#define NSRect CGRect +#define NSZeroRect CGRectZero +#define NSMakeRect CGRectMake +#define valueWithRect valueWithCGRect +#endif + + +#pragma mark Helper classes + +@interface TestClassWithSimpleMethod : NSObject ++ (NSUInteger)initializeCallCount; +- (NSString *)foo; +- (void)bar:(id)someArgument; +@end + +@implementation TestClassWithSimpleMethod + +static NSUInteger initializeCallCount = 0; + ++ (void)initialize +{ + initializeCallCount += 1; +} + ++ (NSUInteger)initializeCallCount +{ + return initializeCallCount; +} + +- (NSString *)foo +{ + return @"Foo"; +} + +- (void)bar:(id)someArgument // maybe we should make it explicit that the arg is retainable +{ + +} + +@end + +@interface TestClassThatObservesFoo : NSObject +{ + @public + id observedObject; +} +@end + +@implementation TestClassThatObservesFoo + +- (instancetype)initWithObject:(id)object +{ + if((self = [super init])) + observedObject = object; + return self; +} + +- (void)dealloc +{ + [self stopObserving]; +} + +- (void)startObserving +{ + [observedObject addObserver:self forKeyPath:@"foo" options:0 context:NULL]; +} + +- (void)stopObserving +{ + if(observedObject != nil) + { + [observedObject removeObserver:self forKeyPath:@"foo" context:NULL]; + observedObject = nil; + } +} + +@end + + +@interface TestClassThatCallsSelf : NSObject +{ + int methodInt; +} + +- (NSString *)method1; +- (NSString *)method2; +- (NSRect)methodRect1; +- (NSRect)methodRect2; +- (int)methodInt; +- (void)methodVoid; +- (void)setMethodInt:(int)anInt; +@end + +@implementation TestClassThatCallsSelf + +- (NSString *)method1 +{ + id retVal = [self method2]; + return retVal; +} + +- (NSString *)method2 +{ + return @"Foo"; +} + + +- (NSRect)methodRect1 +{ + NSRect retVal = [self methodRect2]; + return retVal; +} + +- (NSRect)methodRect2 +{ + return NSMakeRect(10, 10, 10, 10); +} + +- (int)methodInt +{ + return methodInt; +} + +- (void)methodVoid +{ +} + +- (void)setMethodInt:(int)anInt +{ + methodInt = anInt; +} + +@end + + +@interface NSObject(OCMCategoryForTesting) + +- (NSString *)categoryMethod; + +@end + +@implementation NSObject(OCMCategoryForTesting) + +- (NSString *)categoryMethod +{ + return @"Foo-Category"; +} + +@end + + +@interface OCMockObjectPartialMocksTests : XCTestCase +{ + int numKVOCallbacks; +} + +@end + +@interface OCTestManagedObject : NSManagedObject + +@property (nonatomic, copy) NSString *name; +@property (nonatomic, assign) int32_t sortOrder; + +@property (nonatomic, strong) OCTestManagedObject *toOneRelationship; +@property (nonatomic, strong) NSSet *toManyRelationship; + +@end + +@interface OCTestManagedObject (CoreDataGeneratedAccessors) + +- (void)addToManyRelationshipObject:(OCTestManagedObject *)value; +- (void)removeToManyRelationshipObject:(OCTestManagedObject *)value; +- (void)addToManyRelationship:(NSSet *)values; +- (void)removeToManyRelationship:(NSSet *)values; + +@end + +@implementation OCTestManagedObject + +@dynamic name; +@dynamic sortOrder; + +@dynamic toOneRelationship; +@dynamic toManyRelationship; + +@end + + +#pragma mark Category for testing + +@interface OCPartialMockObject(AccessToInvocationsForTesting) + +- (NSArray *)invocationsExcludingInitialize; + +@end + +@implementation OCPartialMockObject(AccessToInvocationsForTesting) + +- (NSArray *)invocationsExcludingInitialize +{ + NSMutableArray *filteredInvocations = [[NSMutableArray alloc] init]; + for(NSInvocation *i in invocations) + if([NSStringFromSelector([i selector]) hasSuffix:@"initialize"] == NO) + [filteredInvocations addObject:i]; + + return filteredInvocations; +} + +@end + + +@implementation OCMockObjectPartialMocksTests + +#pragma mark Test for description + +- (void)testDescription +{ + TestClassWithSimpleMethod *object = [[TestClassWithSimpleMethod alloc] init]; + id mock = [OCMockObject partialMockForObject:object]; + XCTAssertEqualObjects([mock description], @"OCPartialMockObject(TestClassWithSimpleMethod)"); +} + +#pragma mark Tests for stubbing with partial mocks + +- (void)testStubsMethodsOnPartialMock +{ + TestClassWithSimpleMethod *object = [[TestClassWithSimpleMethod alloc] init]; + id mock = [OCMockObject partialMockForObject:object]; + [[[mock stub] andReturn:@"hi"] foo]; + XCTAssertEqualObjects(@"hi", [mock foo], @"Should have returned stubbed value"); +} + +- (void)testForwardsUnstubbedMethodsCallsToRealObjectOnPartialMock +{ + TestClassWithSimpleMethod *object = [[TestClassWithSimpleMethod alloc] init]; + id mock = [OCMockObject partialMockForObject:object]; + XCTAssertEqualObjects(@"Foo", [mock foo], @"Should have returned value from real object."); +} + +//- (void)testForwardsUnstubbedMethodsCallsToRealObjectOnPartialMockForTollFreeBridgedClasses +//{ +// mock = [OCMockObject partialMockForObject:[NSString stringWithString:@"hello2"]]; +// STAssertEqualObjects(@"HELLO2", [mock uppercaseString], @"Should have returned value from real object."); +//} + +- (void)testStubsMethodOnRealObjectReference +{ + TestClassWithSimpleMethod *realObject = [[TestClassWithSimpleMethod alloc] init]; + id mock = [OCMockObject partialMockForObject:realObject]; + [[[mock stub] andReturn:@"TestFoo"] foo]; + XCTAssertEqualObjects(@"TestFoo", [realObject foo], @"Should have stubbed method."); +} + +- (void)testCallsToSelfInRealObjectAreShadowedByPartialMock +{ + TestClassThatCallsSelf *realObject = [[TestClassThatCallsSelf alloc] init]; + id mock = [OCMockObject partialMockForObject:realObject]; + [[[mock stub] andReturn:@"FooFoo"] method2]; + XCTAssertEqualObjects(@"FooFoo", [mock method1], @"Should have called through to stubbed method."); +} + +- (void)testCallsToSelfInRealObjectStructReturnAreShadowedByPartialMock +{ + TestClassThatCallsSelf *realObject = [[TestClassThatCallsSelf alloc] init]; + id mock = [OCMockObject partialMockForObject:realObject]; + [[[mock stub] andReturnValue:OCMOCK_VALUE(NSZeroRect)] methodRect2]; +#if TARGET_OS_IPHONE +#define NSEqualRects CGRectEqualToRect +#endif + XCTAssertTrue(NSEqualRects(NSZeroRect, [mock methodRect1]), @"Should have called through to stubbed method."); +} + +- (void)testInvocationsOfNSObjectCategoryMethodsCanBeStubbed +{ + TestClassThatCallsSelf *realObject = [[TestClassThatCallsSelf alloc] init]; + id mock = [OCMockObject partialMockForObject:realObject]; + [[[mock stub] andReturn:@"stubbed"] categoryMethod]; + XCTAssertEqualObjects(@"stubbed", [realObject categoryMethod], @"Should have stubbed NSObject's method"); +} + + +#pragma mark Tests for remembering invocations for later verification + +- (void)testRecordsInvocationWhenRealObjectIsUsed +{ + TestClassWithSimpleMethod *realObject = [[TestClassWithSimpleMethod alloc] init]; + id mock = [OCMockObject partialMockForObject:realObject]; + + [realObject foo]; + + XCTAssertEqual(1, [[mock invocationsExcludingInitialize] count]); +} + +- (void)testRecordsInvocationWhenMockIsUsed +{ + TestClassWithSimpleMethod *realObject = [[TestClassWithSimpleMethod alloc] init]; + id mock = [OCMockObject partialMockForObject:realObject]; + + [mock foo]; + + XCTAssertEqual(1, [[mock invocationsExcludingInitialize] count]); +} + +- (void)testRecordsInvocationWhenRealObjectIsUsedAndMethodIsStubbed +{ + TestClassWithSimpleMethod *realObject = [[TestClassWithSimpleMethod alloc] init]; + id mock = [OCMockObject partialMockForObject:realObject]; + [[[mock stub] andReturn:@"bar"] foo]; + + [realObject foo]; + + XCTAssertEqual(1, [[mock invocationsExcludingInitialize] count]); +} + +- (void)testRecordsInvocationWhenMockIsUsedAndMethodIsStubbed +{ + TestClassWithSimpleMethod *realObject = [[TestClassWithSimpleMethod alloc] init]; + id mock = [OCMockObject partialMockForObject:realObject]; + [[[mock stub] andReturn:@"bar"] foo]; + + [mock foo]; + + XCTAssertEqual(1, [[mock invocationsExcludingInitialize] count]); +} + +- (void)testRecordsInvocationWhenMockIsUsedAndMethodIsStubbedAndForwardsToRealObject +{ + TestClassWithSimpleMethod *realObject = [[TestClassWithSimpleMethod alloc] init]; + id mock = [OCMockObject partialMockForObject:realObject]; + [[[mock stub] andForwardToRealObject] foo]; + + [mock foo]; + + XCTAssertEqual(1, [[mock invocationsExcludingInitialize] count]); +} + + +#pragma mark Tests for behaviour when setting up partial mocks + +- (void)testPartialMockClassOverrideReportsOriginalClass +{ + TestClassThatCallsSelf *realObject = [[TestClassThatCallsSelf alloc] init]; + Class origClass = [realObject class]; + id mock = [OCMockObject partialMockForObject:realObject]; + XCTAssertEqualObjects([realObject class], origClass, @"Override of -class method did not work"); + XCTAssertEqualObjects([mock class], origClass, @"Mock proxy -class method did not work"); + XCTAssertFalse(origClass == object_getClass(realObject), @"Subclassing did not work"); + [mock stopMocking]; + XCTAssertEqualObjects([realObject class], origClass, @"Classes different after stopMocking"); + XCTAssertEqualObjects(object_getClass(realObject), origClass, @"Classes different after stopMocking"); +} + +- (void)testInitializeIsNotCalledOnMockedClass +{ + NSUInteger countBefore = [TestClassWithSimpleMethod initializeCallCount]; + + TestClassWithSimpleMethod *object = [[TestClassWithSimpleMethod alloc] init]; + id mock = [OCMockObject partialMockForObject:object]; + [[[mock expect] andForwardToRealObject] foo]; + [object foo]; + + NSUInteger countAfter = [TestClassWithSimpleMethod initializeCallCount]; + + XCTAssertEqual(countBefore, countAfter, @"Creating a mock should not have resulted in call to +initialize"); +} + + +- (void)testRefusesToCreateTwoPartialMocksForTheSameObject +{ + id object = [[TestClassThatCallsSelf alloc] init]; + + id partialMock1 = [OCMockObject partialMockForObject:object]; + + XCTAssertNotNil(partialMock1, @"Should have created first partial mock."); + XCTAssertThrows([OCMockObject partialMockForObject:object], @"Should not have allowed creation of second partial mock"); +} + +- (void)testRefusesToCreatePartialMockForTollFreeBridgedClasses +{ + id object = CFBridgingRelease(CFStringCreateWithCString(kCFAllocatorDefault, "foo", kCFStringEncodingASCII)); + XCTAssertThrowsSpecificNamed([OCMockObject partialMockForObject:object], + NSException, + NSInvalidArgumentException, + @"should throw NSInvalidArgumentException exception"); +} + +#if TARGET_RT_64_BIT + +- (void)testRefusesToCreatePartialMockForTaggedPointers +{ + NSDate *object = [NSDate dateWithTimeIntervalSince1970:0]; + XCTAssertThrowsSpecificNamed([OCMockObject partialMockForObject:object], + NSException, + NSInvalidArgumentException, + @"should throw NSInvalidArgumentException exception"); +} + +#endif + +- (void)testRefusesToCreatePartialMockForNilObject +{ + XCTAssertThrows(OCMPartialMock(nil)); +} + +- (void)testPartialMockOfCustomReferenceCountingObject +{ + /* The point of using an object that implements its own reference counting methods is to force + -retain to be called even though the test is compiled with ARC. (Normally ARC does some magic + that bypasses dispatching to -retain.) Issue #245 turned up a recursive crash when partial + mocks used a forwarder for -retain. */ + TestClassWithCustomReferenceCounting *realObject = [TestClassWithCustomReferenceCounting new]; + id partialMock = OCMPartialMock(realObject); + XCTAssertNotNil(partialMock); +} + +- (void)testSettingUpSecondPartialMockForSameClassDoesNotAffectInstanceMethods +{ + TestClassWithSimpleMethod *object1 = [[TestClassWithSimpleMethod alloc] init]; + TestClassWithSimpleMethod *object2 = [[TestClassWithSimpleMethod alloc] init]; + + TestClassWithSimpleMethod *mock1 = OCMPartialMock(object1); + XCTAssertEqualObjects(@"Foo", [object1 foo]); + + TestClassWithSimpleMethod *mock2 = OCMPartialMock(object2); + XCTAssertEqualObjects(@"Foo", [object1 foo]); + XCTAssertEqualObjects(@"Foo", [object2 foo]); + + XCTAssertEqualObjects(@"Foo", [mock1 foo]); + XCTAssertEqualObjects(@"Foo", [mock2 foo]); +} + +- (void)testSettingUpSecondPartialMockForSameClassDoesNotAffectStubs +{ + TestClassWithSimpleMethod *object1 = [[TestClassWithSimpleMethod alloc] init]; + TestClassWithSimpleMethod *object2 = [[TestClassWithSimpleMethod alloc] init]; + + TestClassWithSimpleMethod *mock1 = OCMPartialMock(object1); + XCTAssertEqualObjects(@"Foo", [object1 foo]); + OCMStub([mock1 foo]).andReturn(@"Bar"); + XCTAssertEqualObjects(@"Bar", [object1 foo]); + + TestClassWithSimpleMethod *mock2 = OCMPartialMock(object2); + XCTAssertEqualObjects(@"Bar", [object1 foo]); + XCTAssertEqualObjects(@"Foo", [object2 foo]); + + XCTAssertEqualObjects(@"Bar", [mock1 foo]); + XCTAssertEqualObjects(@"Foo", [mock2 foo]); +} + + +#pragma mark Tests for Core Data interaction with mocks + +- (void)testMockingManagedObject +{ + // Set up the Core Data stack for the test. + + NSManagedObjectModel *const model = [NSManagedObjectModel mergedModelFromBundles:@[[NSBundle bundleForClass:self.class]]]; + NSEntityDescription *const entity = model.entitiesByName[NSStringFromClass([OCTestManagedObject class])]; + NSPersistentStoreCoordinator *const coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model]; + [coordinator addPersistentStoreWithType:NSInMemoryStoreType configuration:nil URL:nil options:nil error:NULL]; + NSManagedObjectContext *const context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; + + // Create and mock a real core data object. + + OCTestManagedObject *const realObject = [[OCTestManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:context]; + OCTestManagedObject *const partialMock = [OCMockObject partialMockForObject:realObject]; + + // Verify the subclassing behaviour is as we expect. + + Class const runtimeObjectClass = object_getClass(realObject); + Class const reportedClass = [realObject class]; + + // Core Data generates a dynamic subclass at runtime to implement modeled proprerties. + // It will look something like "OCTestManagedObject_OCTestManagedObject_". + XCTAssertTrue([runtimeObjectClass isSubclassOfClass:reportedClass]); + XCTAssertNotEqual(runtimeObjectClass, reportedClass); + + // Verify accessors and setters for attributes work as expected. + + partialMock.name = @"OCMock"; + partialMock.sortOrder = 120; + + XCTAssertEqualObjects(partialMock.name, @"OCMock"); + XCTAssertEqual(partialMock.sortOrder, 120); + + partialMock.name = nil; + partialMock.sortOrder = 0; + + XCTAssertNil(partialMock.name); + XCTAssertEqual(partialMock.sortOrder, 0); + + // Verify to-many relationships work as expected. + + OCTestManagedObject *const realObject2 = [[OCTestManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:context]; + OCTestManagedObject *const realObject3 = [[OCTestManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:context]; + + [partialMock addToManyRelationshipObject:realObject2]; + + XCTAssertEqualObjects(partialMock.toManyRelationship, [NSSet setWithObject:realObject2]); + XCTAssertEqualObjects(realObject2.toManyRelationship, [NSSet setWithObject:realObject]); + + partialMock.toOneRelationship = realObject3; + + XCTAssertEqualObjects(partialMock.toOneRelationship, realObject3); + XCTAssertEqualObjects(realObject3.toOneRelationship, realObject); + + // Verify saving the context works as expected. + + NSError *saveError = nil; + [context save:&saveError]; + + XCTAssertNil(saveError); +} + +#pragma mark Tests for KVO interaction with mocks + +/* Starting KVO observations on an already-mocked object generally should work. */ +- (void)testAddingKVOObserverOnPartialMock +{ + static char *MyContext; + TestClassThatCallsSelf *realObject = [[TestClassThatCallsSelf alloc] init]; + Class origClass = [realObject class]; + + id mock = [OCMockObject partialMockForObject:realObject]; + Class ourSubclass = object_getClass(realObject); + + [realObject addObserver:self forKeyPath:@"methodInt" options:NSKeyValueObservingOptionNew context:MyContext]; + Class kvoClass = object_getClass(realObject); + + /* KVO additionally overrides the -class method, but they return the superclass of their special + subclass, which in this case is the special mock subclass */ + XCTAssertEqualObjects([realObject class], ourSubclass, @"KVO override of class did not return our subclass"); + XCTAssertFalse(ourSubclass == kvoClass, @"KVO with subclass did not work"); + + [realObject setMethodInt:45]; + XCTAssertEqual(numKVOCallbacks, 1, @"did not get subclass KVO notification"); + [mock setMethodInt:47]; + XCTAssertEqual(numKVOCallbacks, 2, @"did not get mock KVO notification"); + + [realObject removeObserver:self forKeyPath:@"methodInt" context:MyContext]; + XCTAssertEqualObjects([realObject class], origClass, @"Classes different after stopKVO"); + XCTAssertEqualObjects(object_getClass(realObject), ourSubclass, @"Classes different after stopKVO"); + + [mock stopMocking]; + XCTAssertEqualObjects([realObject class], origClass, @"Classes different after stopMocking"); + XCTAssertEqualObjects(object_getClass(realObject), origClass, @"Classes different after stopMocking"); +} + +/* Mocking a class which already has KVO observations does not work, but does not crash. */ +- (void)testPartialMockOnKVOObserved +{ + static char *MyContext; + TestClassThatCallsSelf *realObject = [[TestClassThatCallsSelf alloc] init]; + Class origClass = [realObject class]; + + [realObject addObserver:self forKeyPath:@"methodInt" options:NSKeyValueObservingOptionNew context:MyContext]; + Class kvoClass = object_getClass(realObject); + + id mock = [OCMockObject partialMockForObject:realObject]; + Class ourSubclass = object_getClass(realObject); + + XCTAssertEqualObjects([realObject class], origClass, @"We did not preserve the original [self class]"); + XCTAssertFalse(ourSubclass == kvoClass, @"KVO with subclass did not work"); + + /* Due to the way we replace the object's class, the KVO class gets overwritten and + KVO notifications stop functioning. If we did not do this, the presence of the mock + subclass would cause KVO to crash, at least without further tinkering. */ + [realObject setMethodInt:45]; + XCTAssertEqual(numKVOCallbacks, 0, @"got subclass KVO notification"); + [mock setMethodInt:47]; + XCTAssertEqual(numKVOCallbacks, 0, @"got mock KVO notification"); + + [mock stopMocking]; + XCTAssertEqualObjects([realObject class], origClass, @"Classes different after stopMocking"); + XCTAssertEqualObjects(object_getClass(realObject), origClass, @"class different after stopMocking"); + + [realObject removeObserver:self forKeyPath:@"methodInt" context:MyContext]; + XCTAssertEqualObjects([realObject class], origClass, @"Classes different after stopKVO"); + XCTAssertEqualObjects(object_getClass(realObject), origClass, @"Classes different after stopKVO"); +} + + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + numKVOCallbacks++; +} + + +#pragma mark Tests for end of stubbing with partial mocks + +- (void)testReturnsToRealImplementationWhenExpectedCallOccurred +{ + TestClassWithSimpleMethod *realObject = [[TestClassWithSimpleMethod alloc] init]; + id mock = [OCMockObject partialMockForObject:realObject]; + [[[mock expect] andReturn:@"TestFoo"] foo]; + XCTAssertEqualObjects(@"TestFoo", [realObject foo], @"Should have stubbed method."); + XCTAssertEqualObjects(@"Foo", [realObject foo], @"Should have 'unstubbed' method."); +} + +- (void)testRestoresObjectWhenStopped +{ + TestClassWithSimpleMethod *realObject = [[TestClassWithSimpleMethod alloc] init]; + id mock = [OCMockObject partialMockForObject:realObject]; + [[[mock stub] andReturn:@"TestFoo"] foo]; + XCTAssertEqualObjects(@"TestFoo", [realObject foo], @"Should have stubbed method."); + XCTAssertEqualObjects(@"TestFoo", [realObject foo], @"Should have stubbed method."); + [mock stopMocking]; + XCTAssertEqualObjects(@"Foo", [realObject foo], @"Should have 'unstubbed' method."); +} + +- (void)testArgumentsGetReleasedAfterStopMocking +{ + __weak id weakArgument; + TestClassWithSimpleMethod *realObject = [[TestClassWithSimpleMethod alloc] init]; + id mock = OCMPartialMock(realObject); + @autoreleasepool { + NSObject *argument = [NSObject new]; + weakArgument = argument; + [mock bar:argument]; + [mock stopMocking]; + } + XCTAssertNil(weakArgument); +} + + +#pragma mark Tests for explicit forward to real object with partial mocks + +- (void)testForwardsToRealObjectWhenSetUpAndCalledOnMock +{ + TestClassWithSimpleMethod *realObject = [[TestClassWithSimpleMethod alloc] init]; + id mock = [OCMockObject partialMockForObject:realObject]; + + [[[mock expect] andForwardToRealObject] foo]; + XCTAssertEqual(@"Foo", [mock foo], @"Should have called method on real object."); + + [mock verify]; +} + +- (void)testForwardsToRealObjectWhenSetUpAndCalledOnRealObject +{ + TestClassWithSimpleMethod *realObject = [[TestClassWithSimpleMethod alloc] init]; + id mock = [OCMockObject partialMockForObject:realObject]; + + [[[mock expect] andForwardToRealObject] foo]; + XCTAssertEqual(@"Foo", [realObject foo], @"Should have called method on real object."); + + [mock verify]; +} + +- (void)testReturnValueFromRealObjectShouldBeReturnedEvenWithPrecedingAndCall +{ + TestClassThatCallsSelf *object = [[TestClassThatCallsSelf alloc] init]; + OCMockObject *mock = OCMPartialMock(object); + [[[[mock stub] andCall:@selector(firstReturnValueMethod) onObject:self] andForwardToRealObject] method2]; + XCTAssertEqualObjects([object method2], @"Foo", @"Should have returned value from real object."); +} + +- (NSString *)firstReturnValueMethod +{ + return @"Bar"; +} + +- (void)testExpectedMethodCallsExpectedMethodWithExpectationOrdering +{ + TestClassThatCallsSelf *object = [[TestClassThatCallsSelf alloc] init]; + id mock = OCMPartialMock(object); + [mock setExpectationOrderMatters:YES]; + [[[mock expect] andForwardToRealObject] method1]; + [[[mock expect] andForwardToRealObject] method2]; + XCTAssertNoThrow([object method1], @"Calling an expected method that internally calls another expected method should not make expectations appear to be out of order."); +} + + +#pragma mark Tests for method swizzling with partial mocks + +- (NSString *)differentMethodInDifferentClass +{ + return @"swizzled!"; +} + +- (void)testImplementsMethodSwizzling +{ + // using partial mocks and the indirect return value provider + TestClassThatCallsSelf *foo = [[TestClassThatCallsSelf alloc] init]; + id mock = [OCMockObject partialMockForObject:foo]; + [[[mock stub] andCall:@selector(differentMethodInDifferentClass) onObject:self] method1]; + XCTAssertEqualObjects(@"swizzled!", [foo method1], @"Should have returned value from different method"); +} + + +- (void)aMethodWithVoidReturn +{ +} + +- (void)testMethodSwizzlingWorksForVoidReturns +{ + TestClassThatCallsSelf *foo = [[TestClassThatCallsSelf alloc] init]; + id mock = [OCMockObject partialMockForObject:foo]; + [[[mock stub] andCall:@selector(aMethodWithVoidReturn) onObject:self] methodVoid]; + XCTAssertNoThrow([foo method1], @"Should have worked."); +} + + +#pragma mark Tests for exception messages + +- (void)testVerifyFailureIncludesHintForPartialMockMethodsThatDontGetForwarderInstalled +{ + TestClassThatCallsSelf *realObject = [[TestClassThatCallsSelf alloc] init]; + id mock = [OCMockObject partialMockForObject:realObject]; + [realObject categoryMethod]; + @try + { + [[mock verify] categoryMethod]; + XCTFail(@"An exception should have been thrown."); + } + @catch(NSException *e) + { + XCTAssertTrue([[e reason] containsString:@"Adding a stub"]); + } +} + +- (void)testDoesNotIncludeHintWhenMockIsNotPartialMock +{ + id mock = [OCMockObject niceMockForClass:[TestClassThatCallsSelf class]]; + @try + { + [[mock verify] categoryMethod]; + XCTFail(@"An exception should have been thrown."); + } + @catch(NSException *e) + { + XCTAssertFalse([[e reason] containsString:@"Adding a stub"]); + } + +} + +- (void)testDoesNotIncludeHintWhenStubbingIsNotGoingToHelp +{ + TestClassThatCallsSelf *realObject = [[TestClassThatCallsSelf alloc] init]; + id mock = [OCMockObject partialMockForObject:realObject]; + @try + { + [[mock verify] method2]; + XCTFail(@"An exception should have been thrown."); + + } + @catch(NSException *e) + { + XCTAssertFalse([[e reason] containsString:@"Adding a stub"]); + } +} + +- (void)testThrowsExceptionWhenAttemptingToTearDownWrongClass +{ + TestClassWithSimpleMethod *realObject = [[TestClassWithSimpleMethod alloc] init]; + TestClassThatObservesFoo *observer = [[TestClassThatObservesFoo alloc] initWithObject:realObject]; + id mock = [OCMockObject partialMockForObject:realObject]; + [observer startObserving]; + + // If we invoked stopObserving here, then stopMocking would work; but we want to test the error case. + XCTAssertThrowsSpecificNamed([mock stopMocking], NSException, NSInvalidArgumentException); + + // Must reset the object here to avoid any attempt to remove the observer, which would fail. + observer->observedObject = nil; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectProtocolMocksTests.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectProtocolMocksTests.m new file mode 100644 index 0000000000..d11dfec9d6 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectProtocolMocksTests.m @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2013-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCMock.h" + + +// -------------------------------------------------------------------------------------- +// Helper classes and protocols for testing +// -------------------------------------------------------------------------------------- + +@protocol TestProtocol ++ (NSString *)stringValueClassMethod; +- (int)primitiveValue; +@optional +- (id)objectValue; +- (void)voidWithArgument:(id)argument; +@end + +@interface InterfaceForTypedef : NSObject { + int prop1; + NSObject *prop2; +} +@end + +@implementation InterfaceForTypedef +@end + +typedef InterfaceForTypedef TypedefInterface; +typedef InterfaceForTypedef* PointerTypedefInterface; + +@protocol ProtocolWithTypedefs +- (TypedefInterface*)typedefReturnValue1; +- (PointerTypedefInterface)typedefReturnValue2; +- (void)typedefParameter:(TypedefInterface*)parameter; +@end + + + +@interface OCMockObjectProtocolMocksTests : XCTestCase + +@end + + +@implementation OCMockObjectProtocolMocksTests + +// -------------------------------------------------------------------------------------- +// Tests +// -------------------------------------------------------------------------------------- + +- (void)testCanMockFormalProtocol +{ + id mock = [OCMockObject mockForProtocol:@protocol(NSLocking)]; + [[mock expect] lock]; + + [mock lock]; + + [mock verify]; +} + +- (void)testDescription +{ + id mock = [OCMockObject mockForProtocol:@protocol(NSLocking)]; + XCTAssertEqualObjects([mock description], @"OCProtocolMockObject(NSLocking)"); +} + +- (void)testRaisesWhenUnknownMethodIsCalledOnProtocol +{ + id mock = [OCMockObject mockForProtocol:@protocol(NSLocking)]; + XCTAssertThrows([mock lowercaseString], @"Should have raised an exception."); +} + +- (void)testConformsToMockedProtocol +{ + id mock = [OCMockObject mockForProtocol:@protocol(NSLocking)]; + XCTAssertTrue([mock conformsToProtocol:@protocol(NSLocking)]); +} + +- (void)testRespondsToValidProtocolRequiredSelector +{ + id mock = [OCMockObject mockForProtocol:@protocol(TestProtocol)]; + XCTAssertTrue([mock respondsToSelector:@selector(primitiveValue)]); +} + +- (void)testRespondsToValidProtocolOptionalSelector +{ + id mock = [OCMockObject mockForProtocol:@protocol(TestProtocol)]; + XCTAssertTrue([mock respondsToSelector:@selector(objectValue)]); +} + +- (void)testDoesNotRespondToInvalidProtocolSelector +{ + id mock = [OCMockObject mockForProtocol:@protocol(TestProtocol)]; + XCTAssertFalse([mock respondsToSelector:@selector(testDoesNotRespondToInvalidProtocolSelector)]); +} + +- (void)testWithTypedefReturnType { + id mock = [OCMockObject mockForProtocol:@protocol(ProtocolWithTypedefs)]; + XCTAssertNoThrow([[[mock stub] andReturn:[TypedefInterface new]] typedefReturnValue1], @"Should accept a typedefed return-type"); + XCTAssertNoThrow([mock typedefReturnValue1]); +} + +- (void)testWithTypedefPointerReturnType { + id mock = [OCMockObject mockForProtocol:@protocol(ProtocolWithTypedefs)]; + XCTAssertNoThrow([[[mock stub] andReturn:[TypedefInterface new]] typedefReturnValue2], @"Should accept a typedefed return-type"); + XCTAssertNoThrow([mock typedefReturnValue2]); +} + +- (void)testWithTypedefParameter { + id mock = [OCMockObject mockForProtocol:@protocol(ProtocolWithTypedefs)]; + XCTAssertNoThrow([[mock stub] typedefParameter:nil], @"Should accept a typedefed parameter-type"); + XCTAssertNoThrow([mock typedefParameter:nil]); +} + + +- (void)testReturnDefaultValueWhenUnknownMethodIsCalledOnNiceProtocolMock +{ + id mock = [OCMockObject niceMockForProtocol:@protocol(TestProtocol)]; + XCTAssertTrue(0 == [mock primitiveValue], @"Should return 0 on unexpected method call (for nice mock)."); + [mock verify]; +} + +- (void)testRaisesAnExceptionWenAnExpectedMethodIsNotCalledOnNiceProtocolMock +{ + id mock = [OCMockObject niceMockForProtocol:@protocol(TestProtocol)]; + [[mock expect] primitiveValue]; + XCTAssertThrows([mock verify], @"Should have raised an exception because method was not called."); +} + +- (void)testProtocolClassMethod +{ + id mock = OCMProtocolMock(@protocol(TestProtocol)); + OCMStub([mock stringValueClassMethod]).andReturn(@"stubbed"); + id result = [mock stringValueClassMethod]; + XCTAssertEqual(@"stubbed", result, @"Should have stubbed the class method."); +} + +- (void)testRefusesToCreateProtocolMockForNilProtocol +{ + XCTAssertThrows(OCMProtocolMock(nil)); +} + +- (void)testArgumentsGetReleasedAfterStopMocking +{ + __weak id weakArgument; + id mock = OCMProtocolMock(@protocol(TestProtocol)); + @autoreleasepool { + NSObject *argument = [NSObject new]; + weakArgument = argument; + [mock voidWithArgument:argument]; + [mock stopMocking]; + } + XCTAssertNil(weakArgument); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectRuntimeTests.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectRuntimeTests.m new file mode 100644 index 0000000000..1c0b8b914b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectRuntimeTests.m @@ -0,0 +1,476 @@ +/* + * Copyright (c) 2014-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCMock.h" + + +#pragma mark Helper classes + +@interface TestClassWithTypeQualifierMethod : NSObject + +- (void)aSpecialMethod:(byref in void *)someArg; + +@end + +@implementation TestClassWithTypeQualifierMethod + +- (void)aSpecialMethod:(byref in __unused void *)someArg +{ +} + +@end + + +typedef NSString TypedefString; + +@interface TestClassWithTypedefObjectArgument : NSObject + +- (NSString *)stringForTypedef:(TypedefString *)string; + +@end + +@implementation TestClassWithTypedefObjectArgument + +- (NSString *)stringForTypedef:(TypedefString *)string +{ + return @"Whatever. Doesn't matter."; +} +@end + + +@interface TestDelegate : NSObject + +- (void)go; + +@end + +@implementation TestDelegate + +- (void)go +{ +} + +@end + +@interface TestClassWithDelegate : NSObject + +@property (nonatomic, weak) TestDelegate *delegate; + +@end + +@implementation TestClassWithDelegate + +- (void)run +{ + TestDelegate *delegate = self.delegate; + [delegate go]; +} + +@end + + +@interface NSValueSubclassForTesting : NSValue + +@end + +@implementation NSValueSubclassForTesting + +@end + + +@interface TestClassWithInitMethod : NSObject +@end + +@implementation TestClassWithInitMethod + +- (id)initMethodNotCalledJustInit +{ + return [super init]; +} + +- (id)initMethodWithNestedInit +{ + return [self initMethodNotCalledJustInit]; +} + +@end + + +@interface TestClassWithResolveMethods : NSObject +@end + +@implementation TestClassWithResolveMethods + ++ (BOOL)resolveInstanceMethod:(SEL)sel +{ + return [super resolveInstanceMethod:sel]; +} + ++ (void)classMethod { +} + ++ (BOOL)resolveClassMethod:(SEL)sel +{ + return [super resolveClassMethod:sel]; +} + +- (void)instanceMethod __used +{ +} + +@end + +// This class imitates a bit how CALayer functions internally; +// see https://github.com/erikdoe/ocmock/issues/411 +@interface TestClassWithResolveMethodsLikeCALayer : TestClassWithResolveMethods +@end + +@implementation TestClassWithResolveMethodsLikeCALayer + ++ (void)aMethodWithClass:(Class)cls __used +{ +} + ++ (BOOL)resolveInstanceMethod:(SEL)sel { + // resolve must call a class method with self as an argument. + [self aMethodWithClass:self]; + return NO; +} + +@end + + + + +#pragma mark Tests for interaction with runtime and foundation conventions + +@interface OCMockObjectRuntimeTests : XCTestCase + +@end + +@implementation OCMockObjectRuntimeTests + +- (void)testRespondsToValidSelector +{ + id mock = [OCMockObject mockForClass:[NSString class]]; + XCTAssertTrue([mock respondsToSelector:@selector(lowercaseString)]); +} + + +- (void)testDoesNotRespondToInvalidSelector +{ + id mock = [OCMockObject mockForClass:[NSString class]]; + // We use a selector that's not implemented by the mock + XCTAssertFalse([mock respondsToSelector:@selector(arrayWithArray:)]); +} + + +- (void)testCanStubValueForKeyMethod +{ + id mock = [OCMockObject mockForClass:[NSObject class]]; + [[[mock stub] andReturn:@"SomeValue"] valueForKey:@"SomeKey"]; + + id returnValue = [mock valueForKey:@"SomeKey"]; + + XCTAssertEqualObjects(@"SomeValue", returnValue, @"Should have returned value that was set up."); +} + + +- (void)testMockConformsToProtocolImplementedInSuperclass +{ + id mock = [OCMockObject mockForClass:[NSValueSubclassForTesting class]]; + XCTAssertTrue([mock conformsToProtocol:@protocol(NSCopying)]); + +} + +- (void)testCanMockNSMutableArray +{ + id mock = [OCMockObject niceMockForClass:[NSMutableArray class]]; + id anArray = [[NSMutableArray alloc] init]; +#pragma unused(mock, anArray) +} + + +- (void)testForwardsIsKindOfClass +{ + id mock = [OCMockObject mockForClass:[NSString class]]; + XCTAssertTrue([mock isKindOfClass:[NSString class]], @"Should have pretended to be the mocked class."); +} + + +- (void)testWorksWithTypeQualifiers +{ + id myMock = [OCMockObject mockForClass:[TestClassWithTypeQualifierMethod class]]; + + XCTAssertNoThrow([[myMock expect] aSpecialMethod:"foo"], @"Should not complain about method with type qualifiers."); + XCTAssertNoThrow([myMock aSpecialMethod:"foo"], @"Should not complain about method with type qualifiers."); +} + +- (void)testWorksWithTypedefsToObjects +{ + id myMock = [OCMockObject mockForClass:[TestClassWithTypedefObjectArgument class]]; + [[[myMock stub] andReturn:@"stubbed"] stringForTypedef:[OCMArg any]]; + id actualReturn = [myMock stringForTypedef:@"Some arg that shouldn't matter"]; + XCTAssertEqualObjects(actualReturn, @"stubbed", @"Should have matched invocation."); +} + + +#if 0 // can't test this with ARC +- (void)testAdjustsRetainCountWhenStubbingMethodsThatCreateObjects +{ + id mock = [OCMockObject mockForClass:[NSString class]]; + NSString *objectToReturn = [NSString stringWithFormat:@"This is not a %@.", @"string constant"]; +#pragma clang diagnostic push +#pragma ide diagnostic ignored "NotReleasedValue" + [[[mock stub] andReturn:objectToReturn] mutableCopy]; +#pragma clang diagnostic pop + + NSUInteger retainCountBefore = [objectToReturn retainCount]; + id returnedObject = [mock mutableCopy]; + [returnedObject release]; // the expectation is that we have to call release after a copy + NSUInteger retainCountAfter = [objectToReturn retainCount]; + + XCTAssertEqualObjects(objectToReturn, returnedObject, @"Should have stubbed copy method"); + XCTAssertEqual(retainCountBefore, retainCountAfter, @"Should have incremented retain count in copy stub."); +} +#endif + +- (void)testComplainsWhenUnimplementedMethodIsCalled +{ + id mock = [OCMockObject mockForClass:[NSString class]]; + XCTAssertThrowsSpecificNamed([mock performSelector:@selector(sortedArrayHint)], NSException, NSInvalidArgumentException); +} + +- (void)testComplainsWhenAttemptIsMadeToStubInitMethod +{ + id mock = [OCMockObject mockForClass:[NSString class]]; + XCTAssertThrows([[[mock stub] init] andReturn:nil]); +} + +- (void)testComplainsWhenAttemptIsMadeToStubInitMethodViaMacro +{ + id mock = [OCMockObject mockForClass:[NSString class]]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-value" + XCTAssertThrows(OCMStub([mock init])); +#pragma clang diagnostic pop +} + + +- (void)testMockShouldNotRaiseWhenDescribing +{ + id mock = [OCMockObject mockForClass:[NSObject class]]; + + XCTAssertNoThrow(NSLog(@"Testing description handling dummy methods... %@ %@ %@ %@ %@", + @{@"foo": mock}, + @[mock], + [NSSet setWithObject:mock], + [mock description], + mock), + @"asking for the description of a mock shouldn't cause a test to fail."); +} + + +- (void)testPartialMockShouldNotRaiseWhenDescribing +{ + id mock = [OCMockObject partialMockForObject:[[NSObject alloc] init]]; + + XCTAssertNoThrow(NSLog(@"Testing description handling dummy methods... %@ %@ %@ %@ %@", + @{@"bar": mock}, + @[mock], + [NSSet setWithObject:mock], + [mock description], + mock), + @"asking for the description of a mock shouldn't cause a test to fail."); + [mock stopMocking]; +} + + +- (void)testWeakReferencesShouldStayAround +{ + TestClassWithDelegate *object = [TestClassWithDelegate new]; + + TestDelegate *delegate = [TestDelegate new]; + object.delegate = delegate; + XCTAssertNotNil(object.delegate, @"Should have delegate"); + + id mockDelegate = OCMPartialMock(delegate); + XCTAssertNotNil(object.delegate, @"Should still have delegate"); + + [object run]; + + OCMVerify([mockDelegate go]); + XCTAssertNotNil(object.delegate, @"Should still have delegate"); +} + + +- (void)testDynamicSubclassesShouldBeDisposed +{ + int numClassesBefore = objc_getClassList(NULL, 0); + + id mock = [OCMockObject mockForClass:[TestDelegate class]]; + [mock stopMocking]; + + int numClassesAfter = objc_getClassList(NULL, 0); + XCTAssertEqual(numClassesBefore, numClassesAfter, @"Should have disposed dynamically generated classes."); +} + + +- (void)testClassesWithResolveMethodsCanBeMocked +{ + // If this test fails it will crash due to recursion. + __unused id mock = OCMClassMock([TestClassWithResolveMethods class]); +} + +- (void)testWithClassesWithResolveMethodSimilarToCALayer +{ + // If this test fails it will crash. + TestClassWithResolveMethodsLikeCALayer *object = [[TestClassWithResolveMethodsLikeCALayer alloc] init]; + __unused id mock = OCMPartialMock(object); +} + + +#pragma mark verify mocks work properly when mocking init + +- (void)testPartialMockNestedInitReturnsCorrectSelfAndDoesntLeak +{ + __weak id controlRefForMock; + __weak id controlRefForRealObject; + @autoreleasepool + { + TestClassWithInitMethod *realObject = [TestClassWithInitMethod alloc]; + controlRefForRealObject = realObject; + id mock = [OCMockObject partialMockForObject:realObject]; + controlRefForMock = mock; + + // Intentionally comparing pointers in all assertions below. + + XCTAssertEqual(mock, [mock initMethodNotCalledJustInit]); + XCTAssertEqual(realObject, [realObject initMethodNotCalledJustInit], @"No Stub, so realObject should be returned"); + + __unused id value = [[[mock stub] andReturn:mock] initMethodNotCalledJustInit]; + + XCTAssertEqual(mock, [mock initMethodWithNestedInit]); + XCTAssertEqual(mock, [realObject initMethodWithNestedInit], @"Stubbed, so mock should be returned"); + } + XCTAssertNil(controlRefForMock, @"Mock should not be leaked."); + XCTAssertNil(controlRefForRealObject, @"Real object should not be leaked."); +} + +- (void)testPartialMockNestedInitReturnsCorrectSelfAndDoesntLeakWithMacro +{ + __weak id controlRefForMock; + __weak id controlRefForRealObject; + @autoreleasepool + { + TestClassWithInitMethod *realObject = [TestClassWithInitMethod alloc]; + controlRefForRealObject = realObject; + id mock = [OCMockObject partialMockForObject:realObject]; + controlRefForMock = mock; + + // Intentionally comparing pointers in all assertions below. + + XCTAssertEqual(mock, [mock initMethodNotCalledJustInit]); + XCTAssertEqual(realObject, [realObject initMethodNotCalledJustInit], @"No Stub, so realObject should be returned"); + + OCMStub([mock initMethodNotCalledJustInit]).andReturn(mock); + + XCTAssertEqual(mock, [mock initMethodWithNestedInit]); + XCTAssertEqual(mock, [realObject initMethodWithNestedInit], @"Stubbed, so mock should be returned"); + } + XCTAssertNil(controlRefForMock, @"Mock should not be leaked."); + XCTAssertNil(controlRefForRealObject, @"Real object should not be leaked."); +} + +- (void)testInitStubReturningDifferentObjectDoesntLeak { + __weak id controlRefForMock; + __weak id controlRefForRealObject; + @autoreleasepool + { + TestClassWithInitMethod *realObject = [[TestClassWithInitMethod alloc] init]; + controlRefForRealObject = realObject; + id mock = OCMClassMock([TestClassWithInitMethod class]); + controlRefForMock = mock; + __unused id value = [[[mock stub] andReturn:realObject] initMethodNotCalledJustInit]; + XCTAssertEqualObjects(realObject, [mock initMethodNotCalledJustInit], @"Mock should return stubbed object."); + } + XCTAssertNil(controlRefForMock, @"Mock should not be leaked."); + XCTAssertNil(controlRefForRealObject, @"Real object should not be leaked."); +} + +- (void)testInitStubReturningDifferentObjectDoesntLeakWithMacro +{ + __weak id controlRefForMock; + __weak id controlRefForRealObject; + @autoreleasepool + { + TestClassWithInitMethod *realObject = [[TestClassWithInitMethod alloc] init]; + controlRefForRealObject = realObject; + id mock = OCMClassMock([TestClassWithInitMethod class]); + controlRefForMock = mock; + OCMStub([mock initMethodNotCalledJustInit]).andReturn(realObject); + XCTAssertEqualObjects(realObject, [mock initMethodNotCalledJustInit], @"Mock should return stubbed object."); + } + XCTAssertNil(controlRefForMock, @"Mock should not be leaked."); + XCTAssertNil(controlRefForRealObject, @"Real object should not be leaked."); +} + +- (void)testInitStubWithNoReturnValueSetDoesntLeak +{ + __weak id controlRef; + @autoreleasepool + { + id mock = OCMClassMock([TestClassWithInitMethod class]); + controlRef = mock; + __unused id value = [[mock stub] initMethodNotCalledJustInit]; + } + XCTAssertNil(controlRef, @"Mock should not be leaked."); +} + +- (void)testInitStubWithNoReturnValueSetDoesntLeakWithMacro +{ + __weak id controlRef; + @autoreleasepool + { + id mock = OCMClassMock([TestClassWithInitMethod class]); + controlRef = mock; + OCMStub([mock initMethodNotCalledJustInit]); + } + XCTAssertNil(controlRef, @"Mock should not be leaked."); +} + +- (void)testInitStubWithNoReturnValueSetThrowsWhenCalled +{ + id mock = OCMClassMock([TestClassWithInitMethod class]); + __unused id value = [[mock stub] initMethodNotCalledJustInit]; + XCTAssertThrowsSpecificNamed([mock initMethodNotCalledJustInit], NSException, NSInvalidArgumentException); +} + +- (void)testInitStubWithNoReturnValueSetThrowsWhenCalledWithMacro +{ + id mock = OCMClassMock([TestClassWithInitMethod class]); + OCMStub([mock initMethodNotCalledJustInit]); + XCTAssertThrowsSpecificNamed([mock initMethodNotCalledJustInit], NSException, NSInvalidArgumentException); +} + +// TODO: Verify intent of this test added in #391 +//- (void)testInitStubWithRejectMacro { +// id mock = OCMClassMock([TestClassWithInitMethod class]); +// OCMReject([mock initMethodNotCalledJustInit]); +//} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectTests.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectTests.m new file mode 100644 index 0000000000..5f0f3d116f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectTests.m @@ -0,0 +1,1105 @@ +/* + * Copyright (c) 2004-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCMock/OCMock.h" + + +#pragma mark Helper classes and protocols for testing + +@interface TestClassWithSelectorMethod : NSObject + +- (void)doWithSelector:(SEL)aSelector; + +@end + +@implementation TestClassWithSelectorMethod + +- (void)doWithSelector:(__unused SEL)aSelector +{ +} + +@end + + +@interface TestClassWithIntPointerMethod : NSObject + +- (void)returnValueInPointer:(int *)ptr; + +@end + +@implementation TestClassWithIntPointerMethod + +- (void)returnValueInPointer:(int *)ptr +{ + *ptr = 555; +} + +@end + +@interface TestClassWithOpaquePointerMethod : NSObject +typedef struct TestOpaque *OpaquePtr; + +- (OpaquePtr)opaquePtrValue; + +@end + +@implementation TestClassWithOpaquePointerMethod + +typedef struct TestOpaque { + int i; + int j; +} TestOpaque; + +TestOpaque myOpaque; + +- (OpaquePtr)opaquePtrValue +{ + myOpaque.i = 3; + myOpaque.i = 4; + return &myOpaque; +} + +@end + +@interface TestClassWithProperty : NSObject + +@property (nonatomic, copy) NSString *title; + +@end + +@implementation TestClassWithProperty + +@synthesize title; + +@end + + +@interface TestClassWithBlockArgMethod : NSObject + +- (void)doStuffWithBlock:(__unused void (^)(void))block andString:(id)aString; + +@end + +@implementation TestClassWithBlockArgMethod + +- (void)doStuffWithBlock:(__unused void (^)(void))block andString:(id)aString; +{ + // stubbed out anyway +} + +@end + + +@interface TestClassWithByReferenceMethod : NSObject + +- (void)returnValuesInObjectPointer:(id *)objectPointer booleanPointer:(BOOL *)booleanPointer; + +@end + +@implementation TestClassWithByReferenceMethod + +- (void)returnValuesInObjectPointer:(id *)objectPointer booleanPointer:(BOOL *)booleanPointer +{ + if(objectPointer != NULL) + *objectPointer = [[NSObject alloc] init]; + if(booleanPointer != NULL) + *booleanPointer = NO; +} + +@end + + +@interface NotificationRecorderForTesting : NSObject +{ + @public + NSNotification *notification; +} + +@end + +@implementation NotificationRecorderForTesting + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)receiveNotification:(NSNotification *)aNotification +{ + notification = aNotification; +} + +@end + + +@protocol TestProtocol ++ (NSString *)stringValueClassMethod; +- (int)primitiveValue; +@optional +- (id)objectValue; +@end + + +@interface TestClassWithProtocolBlockArgMethod : NSObject + +- (void)doStuffWithBlock:(__unused void (^)(id arg))block; + +@end + +@implementation TestClassWithProtocolBlockArgMethod + +- (void)doStuffWithBlock:(__unused void (^)(id arg))block; +{ + // stubbed out anyway +} + +@end + + +@interface TestClassWithClassMethod : NSObject + ++ (id)shared; +- (NSString *)stringValue; + +@end + +@implementation TestClassWithClassMethod + ++ (id)shared +{ + return [TestClassWithClassMethod new]; +} + +- (NSString *)stringValue; +{ + return @"foo"; +} + +@end + + +static NSString *TestNotification = @"TestNotification"; + + +@interface OCMockObjectTests : XCTestCase +{ + id mock; +} + +@end + + + +#pragma mark setup + +@implementation OCMockObjectTests + +- (void)setUp +{ + mock = [OCMockObject mockForClass:[NSString class]]; +} + +- (void)testDescription +{ + XCTAssertEqualObjects([mock description], @"OCClassMockObject(NSString)"); +} + +#pragma mark accepting stubbed methods / rejecting methods not stubbed + +- (void)testAcceptsStubbedMethod +{ + [[mock stub] lowercaseString]; + [mock lowercaseString]; +} + +- (void)testRaisesExceptionWhenUnknownMethodIsCalled +{ + [[mock stub] lowercaseString]; + XCTAssertThrows([mock uppercaseString], @"Should have raised an exception."); +} + + +- (void)testAcceptsStubbedMethodWithSpecificArgument +{ + [[mock stub] hasSuffix:@"foo"]; + [mock hasSuffix:@"foo"]; +} + + +- (void)testAcceptsStubbedMethodWithConstraint +{ + [[mock stub] hasSuffix:[OCMArg any]]; + [mock hasSuffix:@"foo"]; + [mock hasSuffix:@"bar"]; +} + +- (void)testAcceptsStubbedMethodWithBlockArgument +{ + mock = [OCMockObject mockForClass:[NSArray class]]; + [[mock stub] indexesOfObjectsPassingTest:[OCMArg any]]; + [mock indexesOfObjectsPassingTest:^(id obj, NSUInteger idx, BOOL *stop) { return YES; }]; +} + + +- (void)testAcceptsStubbedMethodWithBlockConstraint +{ + [[mock stub] hasSuffix:[OCMArg checkWithBlock:^(id value) { return [value isEqualToString:@"foo"]; }]]; + + XCTAssertNoThrow([mock hasSuffix:@"foo"], @"Should not have thrown a exception"); + XCTAssertThrows([mock hasSuffix:@"bar"], @"Should have thrown a exception"); +} + + +- (void)testAcceptsStubbedMethodWithNilArgument +{ + [[mock stub] uppercaseStringWithLocale:nil]; + [mock uppercaseStringWithLocale:nil]; +} + +- (void)testRaisesExceptionWhenMethodWithWrongArgumentIsCalled +{ + [[mock stub] hasSuffix:@"foo"]; + XCTAssertThrows([mock hasSuffix:@"xyz"], @"Should have raised an exception."); +} + + +- (void)testAcceptsStubbedMethodWithScalarArgument +{ + [[mock stub] stringByPaddingToLength:20 withString:@"foo" startingAtIndex:5]; + [mock stringByPaddingToLength:20 withString:@"foo" startingAtIndex:5]; +} + +- (void)testRaisesExceptionWhenMethodWithOneWrongScalarArgumentIsCalled +{ + [[mock stub] stringByPaddingToLength:20 withString:@"foo" startingAtIndex:5]; + XCTAssertThrows([mock stringByPaddingToLength:20 withString:@"foo" startingAtIndex:3], @"Should have raised an exception."); +} + + +- (void)testAcceptsStubbedMethodWithSelectorArgument +{ + mock = [OCMockObject mockForClass:[TestClassWithSelectorMethod class]]; + [[mock stub] doWithSelector:@selector(allKeys)]; + [mock doWithSelector:@selector(allKeys)]; +} + +- (void)testRaisesExceptionWhenMethodWithWrongSelectorArgumentIsCalled +{ + mock = [OCMockObject mockForClass:[TestClassWithSelectorMethod class]]; + [[mock stub] doWithSelector:@selector(allKeys)]; + XCTAssertThrows([mock doWithSelector:@selector(allValues)]); +} + +- (void)testAcceptsStubbedMethodWithAnySelectorArgument +{ + mock = [OCMockObject mockForClass:[TestClassWithSelectorMethod class]]; + [[mock stub] doWithSelector:[OCMArg anySelector]]; + [mock doWithSelector:@selector(allKeys)]; +} + + +- (void)testAcceptsStubbedMethodWithPointerArgument +{ + NSError __autoreleasing *error; + [[[mock stub] andReturnValue:@YES] writeToFile:[OCMArg any] atomically:YES encoding:NSMacOSRomanStringEncoding error:&error]; + + XCTAssertTrue([mock writeToFile:@"foo" atomically:YES encoding:NSMacOSRomanStringEncoding error:&error]); +} + +- (void)testRaisesExceptionWhenMethodWithWrongPointerArgumentIsCalled +{ + NSString *string; + NSString *anotherString; + NSArray *array; + + [[mock stub] completePathIntoString:&string caseSensitive:YES matchesIntoArray:&array filterTypes:[OCMArg any]]; + + XCTAssertThrows([mock completePathIntoString:&anotherString caseSensitive:YES matchesIntoArray:&array filterTypes:[OCMArg any]]); +} + +- (void)testAcceptsStubbedMethodWithAnyPointerArgument +{ + [[mock stub] getCharacters:[OCMArg anyPointer]]; + + unichar buffer[10]; + XCTAssertNoThrow([mock getCharacters:buffer], @"Should have stubbed method."); +} + + +- (void)testAcceptsStubbedMethodWithMatchingCharPointer +{ + char buffer[10] = "foo"; + [[[mock stub] andReturnValue:@YES] getCString:buffer maxLength:10 encoding:NSASCIIStringEncoding]; + + BOOL result = [mock getCString:buffer maxLength:10 encoding:NSASCIIStringEncoding]; + + XCTAssertEqual(YES, result, @"Should have stubbed method."); +} + +- (void)testAcceptsStubbedMethodWithAnyPointerArgumentForCharPointer +{ + + [[[mock stub] andReturnValue:@YES] getCString:[OCMArg anyPointer] maxLength:10 encoding:NSASCIIStringEncoding]; + + char buffer[10] = "foo"; + BOOL result = [mock getCString:buffer maxLength:10 encoding:NSASCIIStringEncoding]; + + XCTAssertEqual(YES, result, @"Should have stubbed method."); +} + + +- (void)testAcceptsStubbedMethodWithAnyObjectRefArgument +{ + NSError *error; + [[[mock stub] andReturnValue:@YES] writeToFile:[OCMArg any] atomically:YES encoding:NSMacOSRomanStringEncoding error:[OCMArg anyObjectRef]]; + + XCTAssertTrue([mock writeToFile:@"foo" atomically:YES encoding:NSMacOSRomanStringEncoding error:&error]); +} + +- (void)testAcceptsStubbedMethodWithVoidPointerArgument +{ + char bytes[8]; + mock = [OCMockObject mockForClass:[NSMutableData class]]; + [[mock stub] appendBytes:bytes length:8]; + [mock appendBytes:bytes length:8]; +} + + +- (void)testRaisesExceptionWhenMethodWithWrongVoidPointerArgumentIsCalled +{ + mock = [OCMockObject mockForClass:[NSMutableData class]]; + [[mock stub] appendBytes:"foo" length:3]; + XCTAssertThrows([mock appendBytes:"bar" length:3], @"Should have raised an exception."); +} + + +- (void)testAcceptsStubbedMethodWithPointerPointerArgument +{ + NSError __autoreleasing *error = nil; + [[mock stub] writeToFile:@"foo.txt" atomically:NO encoding:NSASCIIStringEncoding error:&error]; + [mock writeToFile:@"foo.txt" atomically:NO encoding:NSASCIIStringEncoding error:&error]; +} + + +- (void)testRaisesExceptionWhenMethodWithWrongPointerPointerArgumentIsCalled +{ + NSError *error = nil, *error2; + [[mock stub] writeToFile:@"foo.txt" atomically:NO encoding:NSASCIIStringEncoding error:&error]; + XCTAssertThrows([mock writeToFile:@"foo.txt" atomically:NO encoding:NSASCIIStringEncoding error:&error2], @"Should have raised."); +} + + +- (void)testAcceptsStubbedMethodWithStructArgument +{ + NSRange range = NSMakeRange(0,20); + [[mock stub] substringWithRange:range]; + [mock substringWithRange:range]; +} + + +- (void)testRaisesExceptionWhenMethodWithWrongStructArgumentIsCalled +{ + NSRange range = NSMakeRange(0,20); + NSRange otherRange = NSMakeRange(0,10); + [[mock stub] substringWithRange:range]; + XCTAssertThrows([mock substringWithRange:otherRange], @"Should have raised an exception."); +} + + +- (void)testCanPassMocksAsArguments +{ + id mockArg = [OCMockObject mockForClass:[NSString class]]; + [[mock stub] stringByAppendingString:[OCMArg any]]; + [mock stringByAppendingString:mockArg]; +} + +- (void)testCanStubWithMockArguments +{ + id mockArg = [OCMockObject mockForClass:[NSString class]]; + [[mock stub] stringByAppendingString:mockArg]; + [mock stringByAppendingString:mockArg]; +} + +- (void)testRaisesExceptionWhenStubbedMockArgIsNotUsed +{ + id mockArg = [OCMockObject mockForClass:[NSString class]]; + [[mock stub] stringByAppendingString:mockArg]; + XCTAssertThrows([mock stringByAppendingString:@"foo"], @"Should have raised an exception."); +} + +- (void)testRaisesExceptionWhenDifferentMockArgumentIsPassed +{ + id expectedArg = [OCMockObject mockForClass:[NSString class]]; + id otherArg = [OCMockObject mockForClass:[NSString class]]; + [[mock stub] stringByAppendingString:otherArg]; + XCTAssertThrows([mock stringByAppendingString:expectedArg], @"Should have raised an exception."); +} + + +- (void)testAcceptsStubbedMethodWithAnyNonObjectArgument +{ + [[[mock stub] ignoringNonObjectArgs] rangeOfString:@"foo" options:0]; + [mock rangeOfString:@"foo" options:NSRegularExpressionSearch]; +} + +- (void)testRaisesExceptionWhenMethodWithMixedArgumentsIsCalledWithWrongObjectArgument +{ + [[[mock stub] ignoringNonObjectArgs] rangeOfString:@"foo" options:0]; + XCTAssertThrows([mock rangeOfString:@"bar" options:NSRegularExpressionSearch], @"Should have raised an exception."); +} + +- (void)testBlocksAreNotConsideredNonObjectArguments +{ + [[[mock stub] ignoringNonObjectArgs] enumerateLinesUsingBlock:[OCMArg invokeBlock]]; + __block BOOL blockWasInvoked = NO; + [mock enumerateLinesUsingBlock:^(NSString * _Nonnull line, BOOL * _Nonnull stop) { + blockWasInvoked = YES; + }]; + XCTAssertTrue(blockWasInvoked, @"Should not have ignored the block argument."); +} + +- (void)testThrowsWhenAttemptingToStubMethodOnStoppedMock +{ + [mock stopMocking]; + XCTAssertThrowsSpecificNamed([[mock stub] rangeOfString:@"foo" options:0], NSException, NSInternalInconsistencyException); +} + + +#pragma mark returning values from stubbed methods + +- (void)testReturnsStubbedReturnValue +{ + [[[mock stub] andReturn:@"megamock"] lowercaseString]; + id returnValue = [mock lowercaseString]; + + XCTAssertEqualObjects(@"megamock", returnValue, @"Should have returned stubbed value."); +} + +- (void)testReturnsStubbedIntReturnValue +{ + [[[mock stub] andReturnValue:@42] intValue]; + int returnValue = [mock intValue]; + + XCTAssertEqual(42, returnValue, @"Should have returned stubbed value."); +} + +- (void)testReturnsStubbedUnsignedLongReturnValue +{ + mock = [OCMockObject mockForClass:[NSNumber class]]; + [[[mock expect] andReturnValue:@42LU] unsignedLongValue]; + unsigned long returnValue = [mock unsignedLongValue]; + XCTAssertEqual(returnValue, 42LU, @"Should have returned stubbed value."); + + [[[mock expect] andReturnValue:@42] unsignedLongValue]; + returnValue = [mock unsignedLongValue]; + XCTAssertEqual(returnValue, 42LU, @"Should have returned stubbed value."); + + [[[mock expect] andReturnValue:@42.0] unsignedLongValue]; + returnValue = [mock unsignedLongValue]; + XCTAssertEqual(returnValue, 42LU, @"Should have returned stubbed value."); + + [[[mock expect] andReturnValue:OCMOCK_VALUE((char)42)] unsignedLongValue]; + returnValue = [mock unsignedLongValue]; + XCTAssertEqual(returnValue, 42LU, @"Should have returned stubbed value."); + + [[[mock expect] andReturnValue:OCMOCK_VALUE((float)42)] unsignedLongValue]; + returnValue = [mock unsignedLongValue]; + XCTAssertEqual(returnValue, 42LU, @"Should have returned stubbed value."); + + [[[mock expect] andReturnValue:OCMOCK_VALUE((float)42.5)] unsignedLongValue]; + XCTAssertThrows([mock unsignedLongValue], @"Should not be able to convert non-integer float to long"); + +#if !__LP64__ + [[[mock expect] andReturnValue:OCMOCK_VALUE((long long)LLONG_MAX)] unsignedLongValue]; + XCTAssertThrows([mock unsignedLongValue], @"Should not be able to convert large long long to long"); +#endif +} + +- (void)testReturnsStubbedBoolReturnValue +{ + [[[mock expect] andReturnValue:@YES] boolValue]; + BOOL returnValue = [mock boolValue]; + XCTAssertEqual(returnValue, YES, @"Should have returned stubbed value."); + + [[[mock expect] andReturnValue:OCMOCK_VALUE(YES)] boolValue]; + returnValue = [mock boolValue]; + XCTAssertEqual(returnValue, YES, @"Should have returned stubbed value."); + + [[[mock expect] andReturnValue:OCMOCK_VALUE(1)] boolValue]; + returnValue = [mock boolValue]; + XCTAssertEqual(returnValue, YES, @"Should have returned stubbed value."); + + [[[mock expect] andReturnValue:OCMOCK_VALUE(300)] boolValue]; + XCTAssertThrows([mock boolValue], @"Should not be able to convert large integer into BOOL"); +} + +- (void)testRaisesWhenBoxedValueTypesDoNotMatch +{ + [[[mock stub] andReturnValue:[NSValue valueWithRange:NSMakeRange(0, 0)]] intValue]; + + XCTAssertThrows([mock intValue], @"Should have raised an exception."); +} + +- (void)testOpaqueStructComparison +{ + TestClassWithOpaquePointerMethod *obj = [TestClassWithOpaquePointerMethod new]; + OpaquePtr val = [obj opaquePtrValue]; + id mockVal = [OCMockObject partialMockForObject:obj]; + [[[mockVal stub] andReturnValue:OCMOCK_VALUE(val)] opaquePtrValue]; + OpaquePtr val2 = [obj opaquePtrValue]; + XCTAssertEqual(val, val2); +} + +- (void)testReturnsStubbedNilReturnValue +{ + [[[mock stub] andReturn:nil] uppercaseString]; + + id returnValue = [mock uppercaseString]; + + XCTAssertNil(returnValue, @"Should have returned stubbed value, which is nil."); +} + +- (void)testReturnsStubbedValueForProperty +{ + TestClassWithProperty *myMock = [OCMockObject mockForClass:[TestClassWithProperty class]]; + + [[[(id)myMock stub] andReturn:@"stubbed title"] title]; + + XCTAssertEqualObjects(@"stubbed title", myMock.title); +} + +- (void)testReturningMockFromMethodItStubsDoesntCreateRetainCycle +{ + @autoreleasepool { + id mockWithShortLifetime = OCMClassMock([TestClassWithClassMethod class]); + [[[mockWithShortLifetime stub] andReturn:@"bar"] stringValue]; + [[[mockWithShortLifetime stub] andReturn:mockWithShortLifetime] shared]; + } + id singleton = [TestClassWithClassMethod shared]; + + XCTAssertEqualObjects(@"foo", [singleton stringValue], @"Should return value from real implementation (because shared is not stubbed anymore)."); +} + +- (void)testReturningMockFromMethodItStubsDoesntCreateRetainCycleWhenUsingMacro +{ + @autoreleasepool { + id mockWithShortLifetime = OCMClassMock([TestClassWithClassMethod class]); + OCMStub([mockWithShortLifetime stringValue]).andReturn(@"bar"); + OCMStub([mockWithShortLifetime shared]).andReturn(mockWithShortLifetime); + } + id singleton = [TestClassWithClassMethod shared]; + + XCTAssertEqualObjects(@"foo", [singleton stringValue], @"Should return value from real implementation (because shared is not stubbed anymore)."); +} + + + +#pragma mark beyond stubbing: raising exceptions, posting notifications, etc. + +- (void)testRaisesExceptionWhenAskedTo +{ + NSException *exception = [NSException exceptionWithName:@"TestException" reason:@"test" userInfo:nil]; + [[[mock expect] andThrow:exception] lowercaseString]; + + XCTAssertThrows([mock lowercaseString], @"Should have raised an exception."); +} + +- (void)testPostsNotificationWhenAskedTo +{ + NotificationRecorderForTesting *observer = [[NotificationRecorderForTesting alloc] init]; + [[NSNotificationCenter defaultCenter] addObserver:observer selector:@selector(receiveNotification:) name:TestNotification object:nil]; + + NSNotification *notification = [NSNotification notificationWithName:TestNotification object:self]; + [[[mock stub] andPost:notification] lowercaseString]; + + [mock lowercaseString]; + + XCTAssertNotNil(observer->notification, @"Should have sent a notification."); + XCTAssertEqualObjects(TestNotification, [observer->notification name], @"Name should match posted one."); + XCTAssertEqualObjects(self, [observer->notification object], @"Object should match posted one."); +} + +- (void)testPostsNotificationInAdditionToReturningValue +{ + NotificationRecorderForTesting *observer = [[NotificationRecorderForTesting alloc] init]; + [[NSNotificationCenter defaultCenter] addObserver:observer selector:@selector(receiveNotification:) name:TestNotification object:nil]; + + NSNotification *notification = [NSNotification notificationWithName:TestNotification object:self]; + [[[[mock stub] andReturn:@"foo"] andPost:notification] lowercaseString]; + + XCTAssertEqualObjects(@"foo", [mock lowercaseString], @"Should have returned stubbed value."); + XCTAssertNotNil(observer->notification, @"Should have sent a notification."); +} + + +- (NSString *)valueForString:(NSString *)aString andMask:(NSStringCompareOptions)mask +{ + return [NSString stringWithFormat:@"[%@, %ld]", aString, (long)mask]; +} + +- (void)testCallsAlternativeMethodAndPassesOriginalArgumentsAndReturnsValue +{ + [[[mock stub] andCall:@selector(valueForString:andMask:) onObject:self] commonPrefixWithString:@"FOO" options:NSCaseInsensitiveSearch]; + + NSString *returnValue = [mock commonPrefixWithString:@"FOO" options:NSCaseInsensitiveSearch]; + + XCTAssertEqualObjects(@"[FOO, 1]", returnValue, @"Should have passed and returned invocation."); +} + + +- (void)testCallsBlockWhichCanSetUpReturnValue +{ + void (^theBlock)(NSInvocation *) = ^(NSInvocation *invocation) + { + NSString *value; + [invocation getArgument:&value atIndex:2]; + value = [NSString stringWithFormat:@"MOCK %@", value]; + [invocation setReturnValue:&value]; + }; + + [[[mock stub] andDo:theBlock] stringByAppendingString:[OCMArg any]]; + + XCTAssertEqualObjects(@"MOCK foo", [mock stringByAppendingString:@"foo"], @"Should have called block."); + XCTAssertEqualObjects(@"MOCK bar", [mock stringByAppendingString:@"bar"], @"Should have called block."); +} + +- (void)testHandlesNilPassedAsBlock +{ + [[[mock stub] andDo:nil] stringByAppendingString:[OCMArg any]]; + + XCTAssertNoThrow([mock stringByAppendingString:@"foo"], @"Should have done nothing."); + XCTAssertNil([mock stringByAppendingString:@"foo"], @"Should have returned default value."); +} + + +- (void)testThrowsWhenTryingToUseForwardToRealObjectOnNonPartialMock +{ + XCTAssertThrows([[[mock expect] andForwardToRealObject] name], @"Should have raised and exception."); +} + + +#pragma mark returning values in pass-by-reference arguments + +- (void)testReturnsValuesInPassByReferenceArguments +{ + NSString *expectedName = @"Test"; + NSArray *expectedArray = [NSArray array]; + + [[mock expect] completePathIntoString:[OCMArg setTo:expectedName] caseSensitive:YES + matchesIntoArray:[OCMArg setTo:expectedArray] filterTypes:[OCMArg any]]; + + NSString *actualName = nil; + NSArray *actualArray = nil; + [mock completePathIntoString:&actualName caseSensitive:YES matchesIntoArray:&actualArray filterTypes:nil]; + + XCTAssertNoThrow([mock verify], @"An unexpected exception was thrown"); + XCTAssertEqualObjects(expectedName, actualName, @"The two string objects should be equal"); + XCTAssertEqualObjects(expectedArray, actualArray, @"The two array objects should be equal"); +} + + +- (void)testReturnsValuesInNonObjectPassByReferenceArguments +{ + mock = [OCMockObject mockForClass:[TestClassWithIntPointerMethod class]]; + [[mock stub] returnValueInPointer:[OCMArg setToValue:@1234]]; + + int actualValue = 0; + [mock returnValueInPointer:&actualValue]; + + XCTAssertEqual(1234, actualValue, @"Should have returned value via pass by ref argument."); + +} + + +- (void)testReturnsValuesInNullPassByReferenceArguments +{ + mock = OCMClassMock([TestClassWithByReferenceMethod class]); + OCMStub([mock returnValuesInObjectPointer:[OCMArg setTo:nil] booleanPointer:[OCMArg setToValue:@NO]]); + [mock returnValuesInObjectPointer:NULL booleanPointer:NULL]; + OCMVerify([mock returnValuesInObjectPointer:NULL booleanPointer:NULL]); +} + + +#pragma mark invoking block arguments + +- (void)testInvokesBlockWithArgs +{ + + BOOL bVal = YES, *bPtr = &bVal; + [[mock stub] enumerateLinesUsingBlock:[OCMArg invokeBlockWithArgs:@"First param", OCMOCK_VALUE(bPtr), nil]]; + + __block BOOL wasCalled = NO; + __block NSString *firstParam; + __block BOOL *secondParam; + void (^block)(NSString *, BOOL *) = ^(NSString *line, BOOL *stop) + { + wasCalled = YES; + firstParam = line; + secondParam = stop; + }; + [mock enumerateLinesUsingBlock:block]; + + XCTAssertTrue(wasCalled, @"Should have invoked block."); + XCTAssertEqualObjects(firstParam, @"First param", @"First param not passed to the block"); + XCTAssertEqual(secondParam, bPtr, @"Second params don't match"); +} + +- (void)testThrowsIfBoxedValueNotFound +{ + [[mock stub] enumerateLinesUsingBlock:[OCMArg invokeBlockWithArgs:@"123", @"Not an NSValue", nil]]; + + XCTAssertThrowsSpecificNamed([mock enumerateLinesUsingBlock:^(NSString *line, BOOL *stop) {}], NSException, NSInvalidArgumentException, @"Should have raised an exception."); +} + +- (void)testThrowsIfArgTypesMismatch +{ + [[mock stub] enumerateLinesUsingBlock:[OCMArg invokeBlockWithArgs:@"123", @YES, nil]]; + + XCTAssertThrowsSpecificNamed([mock enumerateLinesUsingBlock:^(NSString *line, BOOL *stop) {}], NSException, NSInvalidArgumentException, @"Should have raised an exception."); +} + +- (void)testThrowsIfArgsLengthMismatch +{ + [[mock stub] enumerateLinesUsingBlock:[OCMArg invokeBlockWithArgs:@"First but no second", nil]]; + + XCTAssertThrowsSpecificNamed([mock enumerateLinesUsingBlock:^(NSString *line, BOOL *stop) {}], NSException, NSInvalidArgumentException, @"Should have raised an exception."); +} + +- (void)testThrowsForUnknownDefaults +{ + /// @note Should throw because we don't construct default values for the NSRange struct + /// arguments. + [[mock stub] enumerateSubstringsInRange:NSMakeRange(0, 10) options:NSStringEnumerationByLines usingBlock:[OCMArg invokeBlock]]; + + XCTAssertThrowsSpecificNamed([mock enumerateSubstringsInRange:NSMakeRange(0, 10) options:NSStringEnumerationByLines usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop) {}], NSException, NSInvalidArgumentException, @"No exception occurred"); +} + +- (void)testThrowsForIndividualUnknownDefault +{ + /// @note Should throw because of the third argument (we don't construct a default for struct + /// values). + [[mock stub] enumerateSubstringsInRange:NSMakeRange(0, 10) options:NSStringEnumerationByLines usingBlock:[OCMArg invokeBlockWithArgs:@"String 1", OCMOCK_VALUE(NSMakeRange(0, 10)), [OCMArg defaultValue], [OCMArg defaultValue], nil]]; + + XCTAssertThrowsSpecificNamed([mock enumerateSubstringsInRange:NSMakeRange(0, 10) options:NSStringEnumerationByLines usingBlock:^(NSString * _Nullable substring, NSRange substringRange, NSRange enclosingRange, BOOL * _Nonnull stop) {}], NSException, NSInvalidArgumentException, @"No exception occurred"); +} + +- (void)testInvokesBlockWithDefaultArgs +{ + [[mock stub] enumerateLinesUsingBlock:[OCMArg invokeBlockWithArgs:[OCMArg defaultValue], [OCMArg defaultValue], nil]]; + + __block NSString *firstParam; + __block BOOL *secondParam; + void (^block)(NSString *, BOOL *) = ^(NSString *line, BOOL *stop) + { + firstParam = line; + secondParam = stop; + }; + [mock enumerateLinesUsingBlock:block]; + + XCTAssertNil(firstParam, @"First param does not default to nil"); + XCTAssertEqual(secondParam, NULL, @"Second param does not default to NULL"); +} + +- (void)testInvokesBlockWithAllDefaultArgs +{ + [[mock stub] enumerateLinesUsingBlock:[OCMArg invokeBlock]]; + + __block NSString *firstParam; + __block BOOL *secondParam; + void (^block)(NSString *, BOOL *) = ^(NSString *line, BOOL *stop) + { + firstParam = line; + secondParam = stop; + }; + [mock enumerateLinesUsingBlock:block]; + + XCTAssertNil(firstParam, @"First param does not default to nil"); + XCTAssertEqual(secondParam, NULL, @"Second param does not default to NULL"); +} + +- (void)testOnlyInvokesBlockWhenInvocationMatches +{ + mock = [OCMockObject mockForClass:[TestClassWithBlockArgMethod class]]; + [[mock stub] doStuffWithBlock:[OCMArg invokeBlock] andString:@"foo"]; + [[mock stub] doStuffWithBlock:[OCMArg any] andString:@"bar"]; + __block BOOL blockWasInvoked = NO; + [mock doStuffWithBlock:^() { blockWasInvoked = YES; } andString:@"bar"]; + XCTAssertFalse(blockWasInvoked, @"Should not have invoked block."); +} + +- (void)testInvokesBlockWithClassMockArgs +{ + id mockString = OCMClassMock([NSString class]); + [[mock stub] enumerateLinesUsingBlock:[OCMArg invokeBlockWithArgs:mockString, [OCMArg defaultValue], nil]]; + + __block BOOL wasCalled = NO; + __block NSString *firstParam; + void (^block)(NSString *, BOOL *) = ^(NSString *line, BOOL *stop) + { + wasCalled = YES; + firstParam = line; + }; + [mock enumerateLinesUsingBlock:block]; + + XCTAssertEqual(wasCalled, YES, @"Should have invoked block."); + XCTAssertEqual(firstParam, mockString, @"First param does not match."); +} + +- (void)testInvokesBlockWithProtocolMockArgs +{ + id mockProtocol = OCMProtocolMock(@protocol(TestProtocol)); + id mockObject = OCMClassMock([TestClassWithProtocolBlockArgMethod class]); + [[mockObject stub] doStuffWithBlock:[OCMArg invokeBlockWithArgs:mockProtocol, nil]]; + + __block BOOL wasCalled = NO; + __block id firstParam; + void (^block)(id) = ^(id arg) { + wasCalled = YES; + firstParam = arg; + }; + [mockObject doStuffWithBlock:block]; + + XCTAssertTrue(wasCalled, @"Should have invoked block."); + XCTAssertEqual(firstParam, mockProtocol, @"Param does not match"); +} + +#pragma mark accepting expected methods + +- (void)testAcceptsExpectedMethod +{ + [[mock expect] lowercaseString]; + [mock lowercaseString]; +} + + +- (void)testAcceptsExpectedMethodAndReturnsValue +{ + [[[mock expect] andReturn:@"Objective-C"] lowercaseString]; + id returnValue = [mock lowercaseString]; + + XCTAssertEqualObjects(@"Objective-C", returnValue, @"Should have returned stubbed value."); +} + + +- (void)testAcceptsExpectedMethodsInRecordedSequence +{ + [[mock expect] lowercaseString]; + [[mock expect] uppercaseString]; + + [mock lowercaseString]; + [mock uppercaseString]; +} + + +- (void)testAcceptsExpectedMethodsInDifferentSequence +{ + [[mock expect] lowercaseString]; + [[mock expect] uppercaseString]; + + [mock uppercaseString]; + [mock lowercaseString]; +} + + +#pragma mark verifying expected methods + +- (void)testAcceptsAndVerifiesExpectedMethods +{ + [[mock expect] lowercaseString]; + [[mock expect] uppercaseString]; + + [mock lowercaseString]; + [mock uppercaseString]; + + [mock verify]; +} + + +- (void)testRaisesExceptionOnVerifyWhenNotAllExpectedMethodsWereCalled +{ + [[mock expect] lowercaseString]; + [[mock expect] uppercaseString]; + + [mock lowercaseString]; + + XCTAssertThrows([mock verify], @"Should have raised an exception."); +} + +- (void)testAcceptsAndVerifiesTwoExpectedInvocationsOfSameMethod +{ + [[mock expect] lowercaseString]; + [[mock expect] lowercaseString]; + + [mock lowercaseString]; + [mock lowercaseString]; + + [mock verify]; +} + + +- (void)testAcceptsAndVerifiesTwoExpectedInvocationsOfSameMethodAndReturnsCorrespondingValues +{ + [[[mock expect] andReturn:@"foo"] lowercaseString]; + [[[mock expect] andReturn:@"bar"] lowercaseString]; + + XCTAssertEqualObjects(@"foo", [mock lowercaseString], @"Should have returned first stubbed value"); + XCTAssertEqualObjects(@"bar", [mock lowercaseString], @"Should have returned seconds stubbed value"); + + [mock verify]; +} + +- (void)testReturnsStubbedValuesIndependentOfExpectations +{ + [[mock stub] hasSuffix:@"foo"]; + [[mock expect] hasSuffix:@"bar"]; + + [mock hasSuffix:@"foo"]; + [mock hasSuffix:@"bar"]; + [mock hasSuffix:@"foo"]; // Since it's a stub, shouldn't matter how many times we call this + + [mock verify]; +} + +-(void)testAcceptsAndVerifiesMethodsWithSelectorArgument +{ + [[mock expect] performSelector:@selector(lowercaseString)]; + [mock performSelector:@selector(lowercaseString)]; + [mock verify]; +} + + +#pragma mark verify with delay + +- (void)testAcceptsAndVerifiesExpectedMethodsWithDelay +{ + [[mock expect] lowercaseString]; + [[mock expect] uppercaseString]; + + [mock lowercaseString]; + [mock uppercaseString]; + + [mock verifyWithDelay:1]; +} + +- (void)testAcceptsAndVerifiesExpectedMethodsWithDelayBlock +{ + dispatch_async(dispatch_queue_create("mockqueue", nil), ^{ + [NSThread sleepForTimeInterval:0.1]; + [self->mock lowercaseString]; + }); + + [[mock expect] lowercaseString]; + [mock verifyWithDelay:1]; +} + +- (void)testFailsVerifyExpectedMethodsWithoutDelay +{ + dispatch_async(dispatch_queue_create("mockqueue", nil), ^{ + [NSThread sleepForTimeInterval:0.1]; + [self->mock lowercaseString]; + }); + + [[mock expect] lowercaseString]; + XCTAssertThrows([mock verify], @"Should have raised an exception because method was not called in time."); + [mock verifyWithDelay:1]; +} + +- (void)testFailsVerifyExpectedMethodsWithDelay +{ + [[mock expect] lowercaseString]; + XCTAssertThrows([mock verifyWithDelay:0.1], @"Should have raised an exception because method was not called."); +} + +#pragma mark ordered expectations + +- (void)testAcceptsExpectedMethodsInRecordedSequenceWhenOrderMatters +{ + [mock setExpectationOrderMatters:YES]; + + [[mock expect] lowercaseString]; + [[mock expect] uppercaseString]; + + XCTAssertNoThrow([mock lowercaseString], @"Should have accepted expected method in sequence."); + XCTAssertNoThrow([mock uppercaseString], @"Should have accepted expected method in sequence."); +} + +- (void)testRaisesExceptionWhenSequenceIsWrongAndOrderMatters +{ + [mock setExpectationOrderMatters:YES]; + + [[mock expect] lowercaseString]; + [[mock expect] uppercaseString]; + + XCTAssertThrows([mock uppercaseString], @"Should have complained about wrong sequence."); +} + +- (void)testRejectThenExpectWithExpectationOrdering +{ + [mock setExpectationOrderMatters:YES]; + [[mock reject] lowercaseString]; + [[mock expect] uppercaseString]; + XCTAssertNoThrow([mock uppercaseString], @"Since lowercaseString should be rejected, we shouldn't expect it to be called before uppercaseString."); +} + + + +#pragma mark nice mocks don't complain about unknown methods, unless told to + +- (void)testReturnsDefaultValueWhenUnknownMethodIsCalledOnNiceClassMock +{ + mock = [OCMockObject niceMockForClass:[NSString class]]; + XCTAssertNil([mock lowercaseString], @"Should return nil on unexpected method call (for nice mock)."); + [mock verify]; +} + +- (void)testRaisesAnExceptionWhenAnExpectedMethodIsNotCalledOnNiceClassMock +{ + mock = [OCMockObject niceMockForClass:[NSString class]]; + [[[mock expect] andReturn:@"HELLO!"] uppercaseString]; + XCTAssertThrows([mock verify], @"Should have raised an exception because method was not called."); +} + +- (void)testThrowsWhenRejectedMethodIsCalledOnNiceMock +{ + mock = [OCMockObject niceMockForClass:[NSString class]]; + + [[mock reject] uppercaseString]; + XCTAssertThrows([mock uppercaseString], @"Should have complained about rejected method being called."); +} + +- (void)testThrowsWhenTryingToAddActionToReject +{ + mock = [OCMockObject niceMockForClass:[NSString class]]; + XCTAssertThrows([[[mock reject] andReturn:@"Foo"] stringValue]); +} + +- (void)testUncalledRejectStubDoesNotCountAsExpectation +{ + mock = [OCMockObject niceMockForClass:[NSString class]]; + + [[mock expect] lowercaseString]; + [[mock reject] uppercaseString]; + [mock lowercaseString]; + + XCTAssertNoThrow([mock verify], @"Should not have any unmet expectations."); + +} + + +@end + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectVerifyAfterRunTests.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectVerifyAfterRunTests.m new file mode 100644 index 0000000000..56e1d62fd5 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockObjectVerifyAfterRunTests.m @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2014-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCMock.h" + + +@interface TestBaseClassForVerifyAfterRun : NSObject + ++ (NSString *)classMethod1; +- (NSString *)method2; + +@end + +@implementation TestBaseClassForVerifyAfterRun + ++ (NSString *)classMethod1 +{ + return @"Foo-ClassMethod"; +} + +- (NSString *)method2 +{ + return @"Foo"; +} + +@end + +@interface TestClassForVerifyAfterRun : TestBaseClassForVerifyAfterRun + +- (NSString *)method1; + +@end + +@implementation TestClassForVerifyAfterRun + +- (NSString *)method1 +{ + id retVal = [self method2]; + return retVal; +} + +@end + +@interface OCMockObjectVerifyAfterRunTests : XCTestCase + +@end + + +@implementation OCMockObjectVerifyAfterRunTests + +- (void)testDoesNotThrowWhenMethodWasInvoked +{ + id mock = [OCMockObject niceMockForClass:[NSString class]]; + + [mock lowercaseString]; + + XCTAssertNoThrow([[mock verify] lowercaseString], @"Should not have thrown an exception for method that was called."); +} + +- (void)testThrowsWhenMethodWasNotInvoked +{ + id mock = [OCMockObject niceMockForClass:[NSString class]]; + + [mock lowercaseString]; + + XCTAssertThrows([[mock verify] uppercaseString], @"Should have thrown an exception for a method that was not called."); +} + +- (void)testDoesNotThrowWhenMethodWasInvokedOnPartialMock +{ + TestClassForVerifyAfterRun *testObject = [[TestClassForVerifyAfterRun alloc] init]; + id mock = [OCMockObject partialMockForObject:testObject]; + + [mock method2]; + + XCTAssertNoThrow([[mock verify] method2], @"Should not have thrown an exception for method that was called."); +} + +- (void)testDoesNotThrowWhenMethodWasInvokedOnRealObjectEvenInSuperclass +{ + TestClassForVerifyAfterRun *testObject = [[TestClassForVerifyAfterRun alloc] init]; + id mock = [OCMockObject partialMockForObject:testObject]; + + NSString *string = [testObject method1]; + + XCTAssertEqualObjects(@"Foo", string, @"Should have returned value from actual implementation."); + XCTAssertNoThrow([[mock verify] method2], @"Should not have thrown an exception for method that was called."); +} + +- (void)testDoesNotThrowWhenClassMethodWasInvoked +{ + id mock = [OCMockObject niceMockForClass:[TestBaseClassForVerifyAfterRun class]]; + + [TestBaseClassForVerifyAfterRun classMethod1]; + + XCTAssertNoThrow([[mock verify] classMethod1], @"Should not have thrown an exception for class method that was called."); +} + +- (void)testThrowsWhenClassMethodWasNotInvoked +{ + id mock = [OCMockObject niceMockForClass:[TestBaseClassForVerifyAfterRun class]]; + + XCTAssertThrows([[mock verify] classMethod1], @"Should have thrown an exception for class method that was not called."); +} + +- (void)testThrowsWhenVerificationIsAttemptedAfterStopMocking +{ + id mock = [OCMockObject niceMockForClass:[TestBaseClassForVerifyAfterRun class]]; + + [TestBaseClassForVerifyAfterRun classMethod1]; + [mock stopMocking]; + + @try + { + [[mock verify] classMethod1]; + XCTFail(@"Should have thrown an exception."); + } + @catch(NSException *e) + { + XCTAssertEqualObjects([e name], NSInternalInconsistencyException); + XCTAssertTrue([[e reason] containsString:@"after stopMocking has been called"]); + } +} + + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockTests-Info.plist b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockTests-Info.plist new file mode 100644 index 0000000000..169b6f710e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockTests-Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockTests-Prefix.pch b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockTests-Prefix.pch new file mode 100644 index 0000000000..35d76409fa --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCMockTests-Prefix.pch @@ -0,0 +1,9 @@ +// +// Prefix header +// +// The contents of this file are implicitly included at the beginning of every source file. +// + +#ifdef __OBJC__ + #import +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCObserverMockObjectTests.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCObserverMockObjectTests.m new file mode 100644 index 0000000000..dab74bc8e0 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/OCObserverMockObjectTests.m @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2009-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "OCMock.h" + + +static NSString *TestNotificationOne = @"TestNotificationOne"; + + +@interface OCObserverMockObjectTest : XCTestCase +{ + NSNotificationCenter *center; + id mock; +} + +@end + + +@implementation OCObserverMockObjectTest + +- (void)setUp +{ + center = [[NSNotificationCenter alloc] init]; + mock = [OCMockObject observerMock]; +} + +- (void)testAcceptsExpectedNotification +{ + [center addMockObserver:mock name:TestNotificationOne object:nil]; + [[mock expect] notificationWithName:TestNotificationOne object:[OCMArg any]]; + + [center postNotificationName:TestNotificationOne object:self]; + + [mock verify]; +} + +- (void)testAcceptsExpectedNotificationWithSpecifiedObjectAndUserInfo +{ + [center addMockObserver:mock name:TestNotificationOne object:nil]; + NSDictionary *info = @{@"key": @"foo"}; + [[mock expect] notificationWithName:TestNotificationOne object:self userInfo:info]; + + [center postNotificationName:TestNotificationOne object:self userInfo:info]; + + [mock verify]; +} + +- (void)testAcceptsNotificationsInAnyOrder +{ + [center addMockObserver:mock name:TestNotificationOne object:nil]; + [[mock expect] notificationWithName:TestNotificationOne object:self]; + [[mock expect] notificationWithName:TestNotificationOne object:[OCMArg any]]; + + [center postNotificationName:TestNotificationOne object:[NSString string]]; + [center postNotificationName:TestNotificationOne object:self]; +} + +- (void)testAcceptsNotificationsInCorrectOrderWhenOrderMatters +{ + [mock setExpectationOrderMatters:YES]; + + [center addMockObserver:mock name:TestNotificationOne object:nil]; + [[mock expect] notificationWithName:TestNotificationOne object:self]; + [[mock expect] notificationWithName:TestNotificationOne object:[OCMArg any]]; + + [center postNotificationName:TestNotificationOne object:self]; + [center postNotificationName:TestNotificationOne object:[NSString string]]; +} + +- (void)testRaisesExceptionWhenSequenceIsWrongAndOrderMatters +{ + [mock setExpectationOrderMatters:YES]; + + [center addMockObserver:mock name:TestNotificationOne object:nil]; + [[mock expect] notificationWithName:TestNotificationOne object:self]; + [[mock expect] notificationWithName:TestNotificationOne object:[OCMArg any]]; + + XCTAssertThrows([center postNotificationName:TestNotificationOne object:[NSString string]], @"Should have complained about sequence."); +} + +- (void)testRaisesEvenThoughOverlappingExpectationsCouldHaveBeenSatisfied +{ + // this test demonstrates a shortcoming, not a feature + [center addMockObserver:mock name:TestNotificationOne object:nil]; + [[mock expect] notificationWithName:TestNotificationOne object:[OCMArg any]]; + [[mock expect] notificationWithName:TestNotificationOne object:self]; + + [center postNotificationName:TestNotificationOne object:self]; + XCTAssertThrows([center postNotificationName:TestNotificationOne object:[NSString string]]); +} + +- (void)testRaisesExceptionWhenUnexpectedNotificationIsReceived +{ + [center addMockObserver:mock name:TestNotificationOne object:nil]; + + XCTAssertThrows([center postNotificationName:TestNotificationOne object:self]); +} + +- (void)testRaisesWhenNotificationWithWrongObjectIsReceived +{ + [center addMockObserver:mock name:TestNotificationOne object:nil]; + [[mock expect] notificationWithName:TestNotificationOne object:self]; + + XCTAssertThrows([center postNotificationName:TestNotificationOne object:[NSString string]]); +} + +- (void)testRaisesWhenNotificationWithWrongUserInfoIsReceived +{ + [center addMockObserver:mock name:TestNotificationOne object:nil]; + [[mock expect] notificationWithName:TestNotificationOne object:self + userInfo:@{@"key": @"foo"}]; + XCTAssertThrows([center postNotificationName:TestNotificationOne object:[NSString string] + userInfo:@{@"key": @"bar"}]); +} + +- (void)testRaisesOnVerifyWhenExpectedNotificationIsNotSent +{ + [center addMockObserver:mock name:TestNotificationOne object:nil]; + [[mock expect] notificationWithName:TestNotificationOne object:[OCMArg any]]; + + XCTAssertThrows([mock verify]); +} + +- (void)testRaisesOnVerifyWhenNotAllNotificationsWereSent +{ + [center addMockObserver:mock name:TestNotificationOne object:nil]; + [[mock expect] notificationWithName:TestNotificationOne object:[OCMArg any]]; + [[mock expect] notificationWithName:TestNotificationOne object:self]; + + [center postNotificationName:TestNotificationOne object:self]; + XCTAssertThrows([mock verify]); +} + +- (void)testChecksNotificationNamesCorrectly +{ + NSString *notificationName = @"MyNotification"; + + [center addMockObserver:mock name:notificationName object:nil]; + [[mock expect] notificationWithName:[notificationName mutableCopy] object:[OCMArg any]]; + + [center postNotificationName:notificationName object:self]; + + [mock verify]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/Resources/TestObjects.xcdatamodeld/TestObjects.xcdatamodel/contents b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/Resources/TestObjects.xcdatamodeld/TestObjects.xcdatamodel/contents new file mode 100644 index 0000000000..26bdc39fb1 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/Resources/TestObjects.xcdatamodeld/TestObjects.xcdatamodel/contents @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/TestClassWithCustomReferenceCounting.h b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/TestClassWithCustomReferenceCounting.h new file mode 100644 index 0000000000..054b40d975 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/TestClassWithCustomReferenceCounting.h @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2015-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import + +@interface TestClassWithCustomReferenceCounting : NSObject +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/TestClassWithCustomReferenceCounting.m b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/TestClassWithCustomReferenceCounting.m new file mode 100644 index 0000000000..7a4139cf6c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Source/OCMockTests/TestClassWithCustomReferenceCounting.m @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2015-2020 Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#import +#import "TestClassWithCustomReferenceCounting.h" + + +@implementation TestClassWithCustomReferenceCounting +{ +#if __LP64__ + int64_t retainCount; +#else + int32_t retainCount; +#endif +} + +- (NSUInteger)retainCount +{ + return retainCount + 1; +} + +- (instancetype)retain +{ +#if __LP64__ + OSAtomicIncrement64(&retainCount); +#else + OSAtomicIncrement32(&retainCount); +#endif + return self; +} + +- (oneway void)release +{ +#if __LP64__ + int64_t newRetainCount = OSAtomicDecrement64(&retainCount); +#else + int32_t newRetainCount = OSAtomicDecrement32(&retainCount); +#endif + if (newRetainCount == -1) + [self dealloc]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Tools/build.rb b/submodules/AppCenter-sdk/Vendor/OCMock/Tools/build.rb new file mode 100755 index 0000000000..4c2a66bbd3 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Tools/build.rb @@ -0,0 +1,224 @@ +#!/usr/bin/env ruby + +class Builder + + def initialize + @env = Environment.new() + @worker = CompositeWorker.new([Logger.new(), Executer.new()]) + end + + def makeRelease + createWorkingDirectories + downloadSource + copySource + buildModules + signFrameworks "erik@doernenburg.com" + createPackage "ocmock-3.7.1.dmg", "OCMock 3.7.1" + sanityCheck + openPackageDir + end + + def justBuild + createWorkingDirectories + downloadSource + buildModules + openPackageDir + end + + def createWorkingDirectories + @worker.run("mkdir -p #{@env.sourcedir}") + @worker.run("mkdir -p #{@env.productdir}") + @worker.run("mkdir -p #{@env.packagedir}") + end + + def downloadSource + @worker.run("git archive master | tar -x -v -C #{@env.sourcedir}") + end + + def copySource + @worker.run("cp -R #{@env.sourcedir}/Source #{@env.productdir}") + end + + def buildModules + @worker.chdir("#{@env.sourcedir}/Source") + + @worker.run("xcodebuild -project OCMock.xcodeproj -target OCMock OBJROOT=#{@env.objroot} SYMROOT=#{@env.symroot}") + osxproductdir = "#{@env.productdir}/macOS" + @worker.run("mkdir -p #{osxproductdir}") + @worker.run("cp -R #{@env.symroot}/Release/OCMock.framework #{osxproductdir}") + + @worker.run("xcodebuild -project OCMock.xcodeproj -target OCMockLib -sdk iphoneos14.2 OBJROOT=#{@env.objroot} SYMROOT=#{@env.symroot}") + @worker.run("xcodebuild -project OCMock.xcodeproj -target OCMockLib -sdk iphonesimulator14.2 OBJROOT=#{@env.objroot} SYMROOT=#{@env.symroot}") + ioslibproductdir = "#{@env.productdir}/iOS\\ library" + @worker.run("mkdir -p #{ioslibproductdir}") + @worker.run("cp -R #{@env.symroot}/Release-iphoneos/OCMock #{ioslibproductdir}") + @worker.run("lipo -create -output #{ioslibproductdir}/libOCMock.a #{@env.symroot}/Release-iphoneos/libOCMock.a #{@env.symroot}/Release-iphonesimulator/libOCMock.a") + + @worker.run("xcodebuild -project OCMock.xcodeproj -target 'OCMock iOS' -sdk iphoneos14.2 OBJROOT=#{@env.objroot} SYMROOT=#{@env.symroot}") + iosproductdir = "#{@env.productdir}/iOS\\ framework" + @worker.run("mkdir -p #{iosproductdir}") + @worker.run("cp -R #{@env.symroot}/Release-iphoneos/OCMock.framework #{iosproductdir}") + + @worker.run("xcodebuild -project OCMock.xcodeproj -target 'OCMock tvOS' -sdk appletvos14.2 OBJROOT=#{@env.objroot} SYMROOT=#{@env.symroot}") + tvosproductdir = "#{@env.productdir}/tvOS" + @worker.run("mkdir -p #{tvosproductdir}") + @worker.run("cp -R #{@env.symroot}/Release-appletvos/OCMock.framework #{tvosproductdir}") + + @worker.run("xcodebuild -project OCMock.xcodeproj -target 'OCMock watchOS' -sdk watchos7.1 OBJROOT=#{@env.objroot} SYMROOT=#{@env.symroot}") + watchosproductdir = "#{@env.productdir}/watchOS" + @worker.run("mkdir -p #{watchosproductdir}") + @worker.run("cp -R #{@env.symroot}/Release-watchos/OCMock.framework #{watchosproductdir}") + end + + def signFrameworks(identity) + osxproductdir = "#{@env.productdir}/macOS" + iosproductdir = "#{@env.productdir}/iOS\\ framework" + tvosproductdir = "#{@env.productdir}/tvOS" + watchosproductdir = "#{@env.productdir}/watchOS" + + @worker.run("codesign -f -s 'Apple Development: #{identity}' #{osxproductdir}/OCMock.framework") + @worker.run("codesign -f -s 'Apple Development: #{identity}' #{iosproductdir}/OCMock.framework") + @worker.run("codesign -f -s 'Apple Development: #{identity}' #{tvosproductdir}/OCMock.framework") + @worker.run("codesign -f -s 'Apple Development: #{identity}' #{watchosproductdir}/OCMock.framework") + end + + def createPackage(packagename, volumename) + @worker.chdir(@env.packagedir) + @worker.run("hdiutil create -size 7m temp.dmg -layout NONE") + disk_id = nil + @worker.run("hdid -nomount temp.dmg") { |hdid| disk_id = hdid.readline.split[0] } + @worker.run("newfs_hfs -v '#{volumename}' #{disk_id}") + @worker.run("hdiutil eject #{disk_id}") + @worker.run("hdid temp.dmg") { |hdid| disk_id = hdid.readline.split[0] } + @worker.run("cp -R #{@env.productdir}/* '/Volumes/#{volumename}'") + @worker.run("hdiutil eject #{disk_id}") + @worker.run("hdiutil convert -format UDZO temp.dmg -o #{@env.packagedir}/#{packagename} -imagekey zlib-level=9") + @worker.run("rm temp.dmg") + end + + def openPackageDir + @worker.run("open #{@env.packagedir}") + end + + def sanityCheck + osxproductdir = "#{@env.productdir}/macOS" + ioslibproductdir = "#{@env.productdir}/iOS\\ library" + iosproductdir = "#{@env.productdir}/iOS\\ framework" + tvosproductdir = "#{@env.productdir}/tvOS" + watchosproductdir = "#{@env.productdir}/watchOS" + + archs = nil + @worker.run("lipo -info #{osxproductdir}/OCMock.framework/OCMock") { |lipo| archs = /re: (.*)/.match(lipo.readline)[1].strip() } + puts "^^ wrong architecture for macOS framework; found: #{archs}\n\n" unless archs == "x86_64 arm64" + @worker.run("lipo -info #{ioslibproductdir}/libOCMock.a") { |lipo| archs = /re: (.*)/.match(lipo.readline)[1].strip() } + puts "^^ wrong architectures for iOS library; found: #{archs}\n\n" unless archs == "armv7 i386 x86_64 arm64" + @worker.run("lipo -info #{iosproductdir}/OCMock.framework/OCMock") { |lipo| archs = /re: (.*)/.match(lipo.readline)[1].strip() } + puts "^^ wrong architectures for iOS framework; found: #{archs}\n\n" unless archs == "armv7 arm64" + @worker.run("lipo -info #{tvosproductdir}/OCMock.framework/OCMock") { |lipo| archs = /re: (.*)/.match(lipo.readline)[1].strip() } + puts "^^ wrong architectures for tvOS framework; found: #{archs}\n\n" unless archs == "arm64" + @worker.run("lipo -info #{watchosproductdir}/OCMock.framework/OCMock") { |lipo| archs = /re: (.*)/.match(lipo.readline)[1].strip() } + puts "^^ wrong architectures for watchOS framework; found: #{archs}\n\n" unless archs == "armv7k arm64_32" + + @worker.run("codesign -dvv #{osxproductdir}/OCMock.framework") + @worker.run("codesign -dvv #{iosproductdir}/OCMock.framework") + @worker.run("codesign -dvv #{tvosproductdir}/OCMock.framework") + @worker.run("codesign -dvv #{watchosproductdir}/OCMock.framework") + end + + def upload(packagename, dest) + @worker.run("scp #{@env.packagedir}/#{packagename} #{dest}") + end + + def cleanup + @worker.run("chmod -R u+w #{@env.tmpdir}") + @worker.run("rm -rf #{@env.tmpdir}"); + end + +end + + +## Environment +## use attributes to configure manager for your environment + +class Environment + def initialize() + @tmpdir = "/tmp/ocmock.#{Process.pid}" + @sourcedir = tmpdir + "/Source" + @productdir = tmpdir + "/Products" + @packagedir = tmpdir + @objroot = tmpdir + '/Build/Intermediates' + @symroot = tmpdir + '/Build' + end + + attr_accessor :tmpdir, :sourcedir, :productdir, :packagedir, :objroot, :symroot +end + + +## Logger (Worker) +## prints commands + +class Logger + def chdir(dir) + puts "## chdir #{dir}" + end + + def run(cmd) + puts "## #{cmd}" + end +end + + +## Executer (Worker) +## actually runs commands + +class Executer + def chdir(dir) + Dir.chdir(dir) + end + + def run(cmd, &block) + if block == nil + if !system(cmd) + puts "** command failed with error" + exit + end + else + IO.popen(cmd, &block) + end + end +end + + +## Composite Worker (Worker) +## sends commands to multiple workers + +class CompositeWorker + def initialize(workers) + @workers = workers + end + + def chdir(dir) + @workers.each { |w| w.chdir(dir) } + end + + def run(cmd) + @workers.each { |w| w.run(cmd) } + end + + def run(cmd, &block) + @workers.each { |w| w.run(cmd, &block) } + end +end + + +if /Tools$/.match(Dir.pwd) + Dir.chdir("..") +end + +if ARGV[0] == '-r' + Builder.new.makeRelease +else + Builder.new.justBuild +end + + diff --git a/submodules/AppCenter-sdk/Vendor/OCMock/Tools/updatebanner.rb b/submodules/AppCenter-sdk/Vendor/OCMock/Tools/updatebanner.rb new file mode 100755 index 0000000000..194c321e3d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OCMock/Tools/updatebanner.rb @@ -0,0 +1,66 @@ +#!/usr/bin/env ruby + +def update_directory(directory) + Dir.entries(directory).each do | file | + path = "#{directory}/#{file}" + if File.directory?(path) + update_directory(path) unless (file =~ /^[.]+$/) + else + update_file(path) if file =~ /\.(h|m|mm)$/ + end + end +end + +def update_file(filename) + tmpname = "#{filename}.orig" + `mv #{filename} #{tmpname}` + infile = File.open("#{tmpname}", "r") + outfile = File.open("#{filename}", "w") + replace_banner(infile, outfile) + `rm #{tmpname}` +end + +def replace_banner(infile, outfile) + in_banner = true + year = nil + infile.each_line do | line | + if in_banner + copyright_match = /Copyright \(c\) ([0-9]{4})/.match(line) + if copyright_match + year = copyright_match[1] + end + if !(line =~ /^\/\//) && !(line =~ /^[\/ ]\*/) + write_banner(outfile, year) + in_banner = false + end + end + if !in_banner + outfile.puts line + end + end +end + +def write_banner(outfile, year) + banner = <<-EOS +/* + * Copyright (c) %YEARS% Erik Doernenburg and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use these files except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + EOS + years = (year != "2020") ? "#{year}-2020" : year + banner.gsub!(/%YEARS%/, years) + outfile.write(banner) +end + +update_directory(ARGV[0]) diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/.github/ISSUE_TEMPLATE.md b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000000..fed0db5dd2 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,29 @@ + + +### New Issue Checklist + +- [ ] I have tried with the latest version of OHHTTPStubs +- [ ] I have read the [README](https://github.com/AliSoftware/OHHTTPStubs/blob/master/README.md) +- [ ] I have read the [Using the right Swift Version of `OHHTTPStubs` for your project](https://github.com/AliSoftware/OHHTTPStubs#using-the-right-swift-version-of-ohhttpstubs-for-your-project) section +- [ ] I have searched in the [existing issues](https://github.com/AliSoftware/OHHTTPStubs/issues?utf8=✓&q=is%3Aissue) +- [ ] I have read [the OHHTTPStubs wiki](https://github.com/AliSoftware/OHHTTPStubs/wiki) to see if there wasn't a detailed page talking about my issue + +### Environment + +- version of OHHTTPStubs: [LIB VERSION HERE] +- integration method you are using: + * [ ] Cocoapods + * [ ] Carthage + * [ ] submodule + * [ ] other +- version of the tool you use: [INSERT VERSION HERE] + +### Issue Description + +[DESCRIBE YOUR ISSUE HERE] + +##### Complete output when you encounter the issue (if any) + +``` +[INSERT OUTPUT HERE] +``` diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/.github/PULL_REQUEST_TEMPLATE.md b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..20f298039d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,17 @@ + + +### Checklist + +- [ ] I've checked that all new and existing tests pass +- [ ] I've updated the documentation if necessary +- [ ] I've added an entry in the CHANGELOG to credit myself + +### Description + + + +### Motivation and Context + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/.gitignore b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/.gitignore new file mode 100644 index 0000000000..cd54e37e86 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/.gitignore @@ -0,0 +1,27 @@ +# Xcode +build/ +.build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +profile +*.moved-aside +.DS_Store +*.xccheckout +IDEWorkspaceChecks.plist + +# Carthage +Carthage/ +OHHTTPStubs.framework.zip + +# Rubymine +.idea/ + +# SPM +.swiftpm/ diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/.travis.yml b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/.travis.yml new file mode 100644 index 0000000000..edec9e73dc --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/.travis.yml @@ -0,0 +1,92 @@ +language: objective-c + +branches: + only: + - master + +before_install: + - gem install xcpretty --no-document --quiet + +matrix: + include: + - osx_image: xcode9.1 + env: RAKETASK="ios[iOS StaticLib,iPhone 7,latest,build-for-testing test-without-building,OHHTTPSTUBS_SKIP_TIMING_TESTS=1 OHHTTPSTUBS_SKIP_REDIRECT_TESTS=1 SWIFT_VERSION=4.0]" + - osx_image: xcode9.1 + env: RAKETASK="ios[iOS Framework,iPhone 7,latest,build-for-testing test-without-building,OHHTTPSTUBS_SKIP_REDIRECT_TESTS=1 SWIFT_VERSION=4.0]" + - osx_image: xcode9.1 + env: RAKETASK="osx[Mac Framework,x86_64,build-for-testing test-without-building,OHHTTPSTUBS_SKIP_TIMING_TESTS=1 OHHTTPSTUBS_SKIP_REDIRECT_TESTS=1 SWIFT_VERSION=4.0]" + - osx_image: xcode9.1 + env: RAKETASK="tvos[tvOS Framework,latest,build-for-testing test-without-building,OHHTTPSTUBS_SKIP_TIMING_TESTS=1 OHHTTPSTUBS_SKIP_REDIRECT_TESTS=1 SWIFT_VERSION=4.0]" + - osx_image: xcode9.1 + env: RAKETASK="build_carthage_frameworks[iOS,3.2]" + - osx_image: xcode9.1 + env: RAKETASK="build_carthage_frameworks[tvOS,3.2]" + - osx_image: xcode9.1 + env: RAKETASK="build_carthage_frameworks[MacOS,4.0]" + - osx_image: xcode10.1 + env: RAKETASK="ios[iOS StaticLib,iPhone 7,latest,build-for-testing test-without-building,OHHTTPSTUBS_SKIP_TIMING_TESTS=1 OHHTTPSTUBS_SKIP_REDIRECT_TESTS=1 SWIFT_VERSION=4.2]" + - osx_image: xcode10.1 + env: RAKETASK="ios[iOS Framework,iPhone 7,latest,build-for-testing test-without-building,OHHTTPSTUBS_SKIP_REDIRECT_TESTS=1 SWIFT_VERSION=4.2]" + - osx_image: xcode10.1 + env: RAKETASK="tvos[tvOS Framework,latest,build-for-testing test-without-building,OHHTTPSTUBS_SKIP_TIMING_TESTS=1 OHHTTPSTUBS_SKIP_REDIRECT_TESTS=1 SWIFT_VERSION=4.2]" + - osx_image: xcode10.1 + env: RAKETASK="osx[Mac Framework,x86_64,build-for-testing test-without-building,OHHTTPSTUBS_SKIP_TIMING_TESTS=1 OHHTTPSTUBS_SKIP_REDIRECT_TESTS=1 SWIFT_VERSION=4.2]" + - osx_image: xcode10.1 + env: RAKETASK="spm_test" + - osx_image: xcode10.1 + env: RAKETASK="build_carthage_frameworks[iOS,4.1]" + - osx_image: xcode10.1 + env: RAKETASK="build_carthage_frameworks[tvOS,4.2]" + - osx_image: xcode10.1 + env: RAKETASK="build_carthage_frameworks[MacOS,4.2]" + - osx_image: xcode10.2 + env: RAKETASK="ios[iOS StaticLib,iPhone 7,latest,build-for-testing test-without-building,OHHTTPSTUBS_SKIP_TIMING_TESTS=1 OHHTTPSTUBS_SKIP_REDIRECT_TESTS=1]" + - osx_image: xcode10.2 + env: RAKETASK="ios[iOS Framework,iPhone 7,latest,build-for-testing test-without-building,OHHTTPSTUBS_SKIP_REDIRECT_TESTS=1]" + - osx_image: xcode10.2 + env: RAKETASK="tvos[tvOS Framework,latest,build-for-testing test-without-building,OHHTTPSTUBS_SKIP_TIMING_TESTS=1 OHHTTPSTUBS_SKIP_REDIRECT_TESTS=1]" + - osx_image: xcode10.2 + env: RAKETASK="osx[Mac Framework,x86_64,build-for-testing test-without-building,OHHTTPSTUBS_SKIP_TIMING_TESTS=1 OHHTTPSTUBS_SKIP_REDIRECT_TESTS=1]" + - osx_image: xcode10.2 + env: RAKETASK="spm_test" + - osx_image: xcode10.2 + env: RAKETASK="build_carthage_frameworks[iOS,5.0]" + - osx_image: xcode10.2 + env: RAKETASK="build_carthage_frameworks[tvOS,5.0]" + - osx_image: xcode10.2 + env: RAKETASK="build_carthage_frameworks[MacOS,5.0]" + - osx_image: xcode11 + env: RAKETASK="ios[iOS StaticLib,iPhone 8,latest,build-for-testing test-without-building,OHHTTPSTUBS_SKIP_TIMING_TESTS=1 OHHTTPSTUBS_SKIP_REDIRECT_TESTS=1]" + - osx_image: xcode11 + env: RAKETASK="ios[iOS Framework,iPhone 8,latest,build-for-testing test-without-building,OHHTTPSTUBS_SKIP_REDIRECT_TESTS=1]" + - osx_image: xcode11 + env: RAKETASK="tvos[tvOS Framework,latest,build-for-testing test-without-building,OHHTTPSTUBS_SKIP_TIMING_TESTS=1 OHHTTPSTUBS_SKIP_REDIRECT_TESTS=1]" + - osx_image: xcode11 + env: RAKETASK="osx[Mac Framework,x86_64,build-for-testing test-without-building,OHHTTPSTUBS_SKIP_TIMING_TESTS=1 OHHTTPSTUBS_SKIP_REDIRECT_TESTS=1]" + - osx_image: xcode11 + env: RAKETASK="spm_test" + - osx_image: xcode11 + env: RAKETASK="build_carthage_frameworks[iOS,5.1]" + - osx_image: xcode11 + env: RAKETASK="build_carthage_frameworks[tvOS,5.1]" + - osx_image: xcode11 + env: RAKETASK="build_carthage_frameworks[MacOS,5.1]" + - osx_image: xcode11 + env: RAKETASK="build_example_apps" + + +script: + - rake "$RAKETASK" + +before_deploy: + - carthage build --no-skip-current + - carthage archive OHHTTPStubs + +deploy: + provider: releases + api_key: + secure: LJfogUcxlaXczvPyu+s2SAG7SXyhjQbc/kCiNjEO61ehLg0dK0bmfXHm0yeBQQPoQCF5qiWC+5HYQnCaMNmEhP4WHy6RZtmmrg1iiNbeLsRzk8COm2vv+zRgoFXU5K7j2LkfvTSrLPTYR1d+PM/S/XJzMDxrJjryM+mf12DxlnA= + file: OHHTTPStubs.framework.zip + skip_cleanup: true + on: + tags: true diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/CHANGELOG.md b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/CHANGELOG.md new file mode 100644 index 0000000000..a8daae7060 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/CHANGELOG.md @@ -0,0 +1,505 @@ +# OHHTTPStubs — CHANGELOG + +## Master + +* Added `hasFormBody(_:)` matcher. +[@417-72KI](https://github.com/417-72KI) +* Added fix for Xcode 12 - Warnings related to iOS 8 support (Swift Package Manager) #328 +[@kikeenrique](https://github.com/kikeenrique) + +## [9.0.0](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/9.0.0) + +* Added support for Swift Package Manager and dropped OH from all class names. + [@jeffctown](https://github.com/jeffctown) + + +## [8.0.0](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/8.0.0) + +* Update default Swift Version to 5.0 +[@croig](https://github.com/CRoig) + +>Notes: +> * No code changes were required (except from a little missing comma which caused a compilation error). Only xcshemes and xcodeproj were changed. + +## [7.0.0](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/7.0.0) + +* Updating default Swift Version to 4.2. + [@jeffctown](https://github.com/jeffctown) +* Updating example projects to Swift 4.2 and Xcode 10.1. + [@jeffctown](https://github.com/jeffctown) +* Updating iOS Lib Tests to have a minimum iOS version of 8.0. + [@jeffctown](https://github.com/jeffctown) + +> Notes: +> * Bumping this version to 7.0.0 because it's now using the Swift 4 APIs. +> * This version is still compatible with Swift 3.x when integrating with CocoaPods, as CocoaPods uses the same `SWIFT_VERSION` as your app project does so it adapts automatically and it's transparent for users. +> * If you're using Carthage and need Swift 3.x compatibility, you can follow the tips in the installation instructions of the `README.md`. +> * CI is now only testing Swift 4.x on Xcode 9.1 and 10.1. +> * Thank you to [@hellensoloviy](https://github.com/hellensoloviy), [@robertoferraz](https://github.com/robertoferraz), [@rckoenes](https://github.com/rckoenes), [@NikSativa](https://github.com/NikSativa) for their pull requests updating Swift! + +## [6.2.0](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/6.2.0) + +* Enabled application extension API only. + [@lightsprint09](https://github.com/lightsprint09) +* Disabled a flaky redirect test and adding the known issue with redirects to the README. + [@jeffctown](https://github.com/jeffctown) + [#301](https://github.com/AliSoftware/OHHTTPStubs/pull/301) +* Added `isMethodHEAD()` to the `Swift` helpers. + [@Simon-Kaz](https://github.com/Simon-Kaz) + [#294](https://github.com/AliSoftware/OHHTTPStubs/pull/294) +* Fixed issue with not preserving correct headers when following 3xx + redirects. + [@sberrevoets](https://github.com/sberrevoets) + +## [6.1.0](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/6.1.0) + +* Updated deployment target for the pod to 7.0 to remove warning for old APIs. + [@AliSoftware](https://github.com/AliSoftware) +* Fixed HTTP Method retention for 301,302,307,308 status redirects. + [@mikelupo](https://github.com/mikelupo) +* Added `hasJsonBody(_:)` matcher. + [@pimnijman](https://github.com/pimnijman) +* Added `onStubMissing` to report missing stubs. + [@c1ira](https://github.com/c1ira) + [#264](https://github.com/AliSoftware/OHHTTPStubs/pull/264) +* Fixed `URLRequest.ohhttpStubs_httpBody` function in Swift 3 and 4. + [@mplorentz](https://github.com/mplorentz) +* Added absolute url matcher. + [@victorg1991](https://github.com/victorg1991) + [#254](https://github.com/AliSoftware/OHHTTPStubs/pull/254) +* Fixed up empty lines with whitespace inside test case classes. + [@mikelupo](https://github.com/mikelupo) + [#251](https://github.com/AliSoftware/OHHTTPStubs/pull/251) +* Fixed potential memory leaks with use of NSURLSession as detected by our devs. + [@mikelupo](https://github.com/mikelupo) + [#250](https://github.com/AliSoftware/OHHTTPStubs/pull/250) +* Add precondition assertions in `isScheme` and `isHost` matchers and some documentation in `isHost`, `isScheme` and `isPath`. + [@Liquidsoul](https://github.com/Liquidsoul) + [#248](https://github.com/AliSoftware/OHHTTPStubs/pull/248) + +## [6.0.0](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/6.0.0) + +* Made Swift 3 the default. `master` is now compatible with 3.0 and 3.1. + [@Liquidsoul](https://github.com/Liquidsoul) + [@cohilla](https://github.com/cohilla) + [#240](https://github.com/AliSoftware/OHHTTPStubs/pull/240) +* The `pod 'OHHTTPStubs/Swift'` subspec now includes the `URLSession` and `JSON` subspecs. + [@AliSoftware](https://github.com/AliSoftware) +* Added some matchers to the Swift APIs: `hasBody(…)`, `pathEndsWith(…)` and `pathMatches(…)`. + [@AliSoftware](https://github.com/AliSoftware) + +> Notes: +> +> * Bumping this version to 6.0.0 because it's now using the Swift 3 APIs, +> but in practice it's entirely retro-compatible with previous `5.2.3-swift3` branch +> * This version is still compatible with Swift 2.3 when integrating with CocoaPods, as CocoaPods uses the same `SWIFT_VERSION` as your app project does so it adapts automatically and it's transparent for users. +> * If you're using Carthage though, we stopped providing Swift-2.3-specific branches ourselves (too much maintainance work), but if you still need Swift 2.3 compatibility, you can follow the tips in the installation instructions of the `README.md`. + +## [5.2.3](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/5.2.3) + +* Reverted [#216](https://github.com/AliSoftware/OHHTTPStubs/pull/216) until better solution, as it was never active and can't make it compile for all subspec configurations. +* Improved documentation about `dynamicType:` vs `type(of:)`. + [Antondomashnev](https://github.com/Antondomashnev) + [#221](https://github.com/AliSoftware/OHHTTPStubs/pull/221) +* Fixed a race condition that occasionally prevented redirect callbacks. + [@morrowa](https://github.com/morrowa) + [#224](https://github.com/AliSoftware/OHHTTPStubs/pull/224) +* Fixed response timing for zero-length stub data. + [@morrowa](https://github.com/morrowa) + [#224](https://github.com/AliSoftware/OHHTTPStubs/pull/224) + +## [5.2.2](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/5.2.2) + +* Added `@discardableResult` to func stub for swift 3. + [@mrkite](https://github.com/mrkite), [#203](https://github.com/AliSoftware/OHHTTPStubs/pull/203) +* Removed `ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES` to avoid embedding Swift standard libraries when building with Carthage. + [@MattesGroeger](https://github.com/MattesGroeger) + [#217](https://github.com/AliSoftware/OHHTTPStubs/pull/217) + [@kylejm](https://github.com/kylejm) + [#220](https://github.com/AliSoftware/OHHTTPStubs/pull/220) +* Add `OHHTTPStubs_HTTPBody` to `URLRequest` in Swift 3.0. + [@marcelofabri](https://github.com/marcelofabri) + [#216](https://github.com/AliSoftware/OHHTTPStubs/pull/216) +* Migrate samples in `swift3` branch to Swift 3. + [@dhardiman](https://gitub.com/dhardiman) + [#205](https://github.com/AliSoftware/OHHTTPStubs/pull/205) + +## [5.2.1](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/5.2.1) + +* Fix typos in README and documentation. + [@AliSoftware](https://github.com/AliSoftware) + [@tmpsantos](https://github.com/tmpsantos) + [#198](https://github.com/AliSoftware/OHHTTPStubs/pull/198) +* Fixes Swift 3.0 GM compatibility (`@escaping`) in the `swift-3.0` branch. + [@ikesyo](https://github.com/ikesyo) + [#201](https://github.com/AliSoftware/OHHTTPStubs/pull/201) + +## [5.2.0](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/5.2.0) + +* Added Swift 2.3/Xcode 8 support. This is compatible both Swift 2.2/Xcode 7.3 and Swift 2.3/Xcode 8. + [@ikesyo](https://github.com/ikesyo) + [#184](https://github.com/AliSoftware/OHHTTPStubs/pull/184) +* Added Swift 3.0 support. + [@mxcl](https://github.com/mxcl) + [@Liquidsoul](https://github.com/Liquidsoul) + [#192](https://github.com/AliSoftware/OHHTTPStubs/pull/192) +* Set deployment targets at the project level to be used in a universal target. + [@ikesyo](https://github.com/ikesyo) + [#185](https://github.com/AliSoftware/OHHTTPStubs/pull/185) +* Fix: Carthage support and Examples configurations. + [@Liquidsoul](https://github.com/Liquidsoul) + [#190](https://github.com/AliSoftware/OHHTTPStubs/issues/190) + +## [5.1.0](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/5.1.0) + +* Bugfix: task completion block never called when not following redirects. + [@adurdin](https://github.com/adurdin) + [#175](https://github.com/AliSoftware/OHHTTPStubs/pull/175) +* Declare in the project settings that the library contains swift code. + [@rodericj](https://github.com/rodericj) + [#173](https://github.com/AliSoftware/OHHTTPStubs/pull/173) +* Adjusted parsing of Mocktail files to allow headers to start on line 4. + [@Ashton-W](https://github.com/Ashton-W) + [#172](https://github.com/AliSoftware/OHHTTPStubs/pull/172) +* Allows access to the `HTTPBody` of POST request when using `NSURLSession` + [(Wiki entry)](https://github.com/AliSoftware/OHHTTPStubs/wiki/Testing-for-the-request-body-in-your-stubs) + [@iosphere](https://github.com/iosphere/) + [#166](https://github.com/AliSoftware/OHHTTPStubs/pull/166) + [#180](https://github.com/AliSoftware/OHHTTPStubs/pull/180) + +## [5.0.0](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/5.0.0) + +* Added `pathStartsWith(_:)` to the `Swift` helpers. + [@sdduursma](https://github.com/sdduursma) + [#163](https://github.com/AliSoftware/OHHTTPStubs/pull/163) +* Added more logging blocks for debugging and better insight into when OHHTTPStubs returns stubs and redirects. + [@jzucker2](https://github.com/jzucker2) + [#161](https://github.com/AliSoftware/OHHTTPStubs/pull/161) +* Added matchers that check whether a request has a particular header present, and a matcher to check if a request has a header with a key and value. + [@hq-mobile](https://github.com/hq-mobile) + [#160](https://github.com/AliSoftware/OHHTTPStubs/pull/160) + +_Note that this last change also changed the signature of the `onStubActivation:` (hence the bump to `5.0.0`) so you'll have to update your code if you used this for debugging your stubs._ + +## [4.8.0](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/4.8.0) + +* Added `isEnabled` and `isEnabledForSessionConfiguration` getter methods. + [@jzucker2](https://github.com/jzucker2) + [#159](https://github.com/AliSoftware/OHHTTPStubs/pull/159) + +## [4.7.1](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/4.7.1) + +* Bumps OSX Deployment Target to 10.9 to add Swift support. + [@JeanAzzopardi](https://github.com/JeanAzzopardi) + [#154](https://github.com/AliSoftware/OHHTTPStubs/pull/154) +* Added the `${CURRENT_PROJECT_VERSION}` to the `Info.plist` files of the`OHHTTPStubs.framework` so it matches what is expected by iTunes Connect. + [@siemensikkema](https://github.com/siemensikkema) + [#140](https://github.com/AliSoftware/OHHTTPStubs/pull/140) + +## [4.7.0](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/4.7.0) + +* Added `isMethodPATCH()` to the `Swift` helpers. + [@attheodo](https://github.com/attheodo) + [#145](https://github.com/AliSoftware/OHHTTPStubs/issues/145) +* Fixed nullability annotation on `onStubActivation:` method parameter. + [@DerLobi](https://github.com/DerLobi) + [#144](https://github.com/AliSoftware/OHHTTPStubs/pull/144) + +## [4.6.0](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/4.5.1) + +* Added `isMethodGET()`, `isMethodPUT()`, `isMethodPOST()` and `isMethodDELETE()` to the `Swift` helpers. + [#137](https://github.com/AliSoftware/OHHTTPStubs/issues/137) + +## [4.5.1](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/4.5.1) + +* Added missing `tvOS` and `watchOS` platforms to the `Swift` subspec. + [@pantuspavel](https://github.com/pantuspavel) + [#136](https://github.com/AliSoftware/OHHTTPStubs/pull/136) + +## [4.5.0](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/4.5.0) — tvOS + +* Added support for tvOS. + [@tiagomartinho](https://github.com/tiagomartinho) + [#134](https://github.com/AliSoftware/OHHTTPStubs/pull/134) + +## [4.4.0](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/4.4.0) + +* Fixed issue with Umbrella Headers. + [#127](https://github.com/AliSoftware/OHHTTPStubs/issues/127) + [#131](https://github.com/AliSoftware/OHHTTPStubs/pull/131) +* Added methods for creating `HTTPStubsResponse`s from `NSURL`s that represent file system resources. + [@MaxGabriel](https://github.com/MaxGabriel) + [#129](https://github.com/AliSoftware/OHHTTPStubs/pull/129) +* Bumped Swift subspec compatibility to OSX 10.9 instead of 10.7. + + +## [4.3.0](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/4.3.0) + +* Xcode projects updated to Xcode 7.0 Final +* Added a `Swift` subspec that adds helper global functions to ease & make more compact the use of `OHHTTPStubs` from Swift 2.0 + [#111](https://github.com/AliSoftware/OHHTTPStubs/issues/111) + +> If you're using `OHHTTPStubs` in a **Swift 2.0** project, it's recommended to add `pod 'OHHTTPStubs/Swift` to your `Podfile` so you can use those handy helpers. + +## [4.2.1](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/4.2.1) + +* Fix the Examples Xcode project + lib Podfile that were referencing old target names + [@mikelupo](https://github.com/mikelupo) + [#117](https://github.com/AliSoftware/OHHTTPStubs/pull/117) +* Added two new constants for download speed: `OHHTTPStubsDownloadSpeed1KBPS` = 1kbps and `OHHTTPStubsDownloadSpeedSLOW` = 1.5 kpbs. + [@mikelupo](https://github.com/mikelupo) + [#114](https://github.com/AliSoftware/OHHTTPStubs/pull/114) + +## [4.2.0](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/4.2.0) — Splitting in subspecs + +* The `OHHTTPStubs` spec has been splitted into **multiple subspecs**: + * The default subspec (used when you simply use `pod 'OHHTTPStubs'` in your `Podfile`) contains the subspecs `Core`, `NSURLSession`, `JSON` & `OHPathHelpers` (so that it matches the features that most people use). + * Other optional subspecs are `HTTPMessage` and `Mocktail` (which are opt-in because used by much less people). If you want to use them, you'll need to request them explicitly in your `Podfile` using `pod 'OHHTTPStubs/Mocktail` for example. +* The iOS Unit Tests are now also run for the framework as well as for the static library, to ensure the tests pass in both contexts _(because frameworks sometimes introduce subtleties like when using `NSBundle`, so it's worth testing in that context too)_ +* Added support for stubs written in the [Mocktail](https://github.com/square/objc-mocktail) format. + [@JinlianWang](https://github.com/JinlianWang) + [#108](https://github.com/AliSoftware/OHHTTPStubs/pull/108) + +## [4.1.0](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/4.1.0) — watchOS 2 + +* Added support for using `OHHTTPStubs` in watchOS 2.0 targets. +* Improved compatibility macros (nullability annotations) — and tested against Xcode 7 beta 4. + +## [4.0.2](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/4.0.2) + +* Fix `OHResourceBundle` name mismatch between header and implementation. + [@tibr](https://github.com/tibr) + [#103](https://github.com/AliSoftware/OHHTTPStubs/pull/103) + +## [4.0.1](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/4.0.1) + +* Fix threading in `NSURLProtocol` subclass calling `NSURLProtocolClient` callbacks from wrong thread. + [@nsprogrammer](https://github.com/nsprogrammer) + [#96](https://github.com/AliSoftware/OHHTTPStubs/pull/96) + +## [4.0.0](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/4.0.0) — Improvements for Swift + +* Annotated the library with _nullability_ attributes to generate a better API when used in Swift +* Migrated the path utility macros to functions in `OHPathHelpers.h`, for Swift compatibility. + [#100](https://github.com/AliSoftware/OHHTTPStubs/issues/100) +* Added a complete Swift Demo Project. + [#88](https://github.com/AliSoftware/OHHTTPStubs/issues/88) +* Removed the `XCTestExpectation` subspec that was added for Xcode 5 support — Now that Xcode 6 is widely adopted, you shouldn't need this anymore (but in case you still need it, I will probably create a dedicated pod for that) + +## [3.1.12](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/3.1.12) + +* Fixed issue with HTTP 300 return code (multiple-choice) that is not supposed to redirect. + [@tarbrain](https://github.com/tarbrain) + [#92](https://github.com/AliSoftware/OHHTTPStubs/pull/92) + +## [3.1.11](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/3.1.11) + +* Added [Carthage](https://github.com/Carthage/Carthage) support +* Splitted the Xcode projects for more clarity (one dedicated to build the lib and run Unit Tests, and one for the Demo) +* Got rid of the `git submodule` used for Unit Tests against [AFNetworking](https://github.com/AFNetworking/AFNetworking) — it is now imported using [CocoaPods](http://cocoapods.org) and only for the lib's Unit Tests targets. + [@corinnekrych](https://github.com/corinnekrych) + [#90](https://github.com/AliSoftware/OHHTTPStubs/pull/90) +* Improved [Travis-CI](https://travis-ci.org/AliSoftware/OHHTTPStubs) integration. We now use a build matrix to have paralellized and independant builds for each scheme (iOS Static Lib, iOS Dynamic Framework, OSX Framework) +* Fixed [#80](https://github.com/AliSoftware/OHHTTPStubs/issues/80) again (there was still an issue for people using Xcode 5 & SDK 7.1… if those people still exists) + +## [3.1.10](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/3.1.10) + +* Fix headers for people still building with Xcode 5 & SDK 7 ([#80](https://github.com/AliSoftware/OHHTTPStubs/issues/80)) + +## [3.1.9](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/3.1.9) + +* Use `NS_DESIGNATED_INITIALIZER` macro on designated initializer methods ([#79](https://github.com/AliSoftware/OHHTTPStubs/pull/79)) + +## [3.1.8](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/3.1.8) + +* Use `application/json` instead of `text/json` in `README`'s example ([#75](https://github.com/AliSoftware/OHHTTPStubs/pull/75)) +* Fixed an issue with empty files (when using `responseWithFileAtPath:statusCode:headers:` but the file at the specified path is empty) + +## [3.1.7](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/3.1.7) + +* Added `DEFINES_MODULE` Flag to be easily imported in Swift ([#74](https://github.com/AliSoftware/OHHTTPStubs/pull/74)) + +_(I also moved [Travis-CI build system](https://travis-ci.org/AliSoftware/OHHTTPStubs) so it now uses `xcpretty` instead of `xctool` to run Unit Tests)_ + +## [3.1.6](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/3.1.6) + +* Fixed issue with the main thread stalling when an `NSException` was raised in the response block +* Fixed an issue with `OHHTTPStubs/XCTestExpectation` conditional compilation in Xcode 6.0 & OSX SDK. + _(the condition was previously testing available SDKs instead of Xcode version, which led to errors with Xcode 6.0 not having the latest 10.10 SDK yet, but still having the `XCTestExpectation` already anyway)_ + +## [3.1.5](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/3.1.5) + +* Migrated Unit Tests to XCTest. +* Added `XCTestExpectation` subspec containing my own implementation for Xcode 5 support + +## [3.1.4](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/3.1.4) + +* Fix issue that made stubs never being called on iOS8 (#65). + +> _As of Xcode6 Beta4, **`OHHTTPStubs` compatibility with iOS8** has been validated now._ + + +## [3.1.3](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/3.1.3) + +* Fix #66: Use the ivar directly in initialization (to avoid KVO side effects) + +## [3.1.2](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/3.1.2) + +* Fix broken link in README (#61) +* Don't override Content-Length header when already set (#62) + +## [3.1.1](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/3.1.1) + +* Fixing a crash when using very very long data #57/#59 +* Fixing issue #51 regarding a probable race condition when stubs were removed before the request has finished +* Shorten the README.md file and moved all the usage examples in a dedicated wiki page to avoid a endless and frightening README + +## [3.1.0](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/3.1.0) + +* The `HTTPStubsDescriptor` protocol now inherits from the `NSObject` protocol + +## [3.0.4](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/3.0.4) + +Fixing issue #47 when stubs were not called, especially when the `OHHTTPStubs` pod were loaded both by the application AND the test target/bundle. See also [[A tricky case with Application Tests]]. + +* `NSURLSessionConfiguration` 's swizzling (to add automatic support of `OHHTTPStubs` to `NSURLSession`) is now done in the `+load` method of an `NSURLSessionConfiguration` category, to be sure it is loaded (and swizzled) only once, even if `OHHTTPStubs` is loaded by two different bundles. +* The stubs activation of `NSURLSessionConfiguration` no longer uses `objc_getClass` but uses a call to the `OHHTTPStubs` class instead, which ensure that it uses the correct `OHHTTPStubs` class in the current bundle instead of always using the one loaded from the main bundle. + +## [3.0.3](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/3.0.3) + +* Adding Mac framework & Mac Test Target (#44) +* Adding known limitations in README + +## [3.0.2](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/3.0.2) + +* Fixed issue with cookies when `request.URL` is `nil` ([#39](https://github.com/AliSoftware/OHHTTPStubs/pull/39)) +* Fixed missing `-ObjC` flag in Unit Tests target _(that made it unable to call category methods)_ +* Fixed Unit Tests on iOS6 _(`NSURLSession`-related Unit Tests now only executed when run on iOS7+ or OSX10.9+, and skipped if targeted for an earlier OS version, as `NSURLSession` was not available then)_ + +## [3.0.1](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/3.0.1) + +* Fixed issue with `NSURLSessionConfiguration` auto-swizzling (#37 & #38) + +> _Now `OHHTTPStubs` automagically works with `NSURLSessionConfiguration` **without the need** to enable it for every `NSURLSessionConfiguration` before creating the `NSURLSession`: the `defaultSessionConfiguration` and `ephemeralSessionConfiguration` are now preconfigured automatically to work with `OHHTTPStubs`)_ + +## [3.0.0](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/3.0.0) + +* Removed deprecated methods. + +> The Old API has now totally disappeared, leaving only a clean and simple API without the spam due to old deprecated methods. + +Note: **If you have already removed the calls to all `OHHTTPStubs` deprecated API in your code, you can switch to this `3.0.0` version without any further changes in your code**. + +## [2.4.0](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/2.4.0) + +* Added support for `NSURLSession` (thx to @ndonald2) [#31](https://github.com/AliSoftware/OHHTTPStubs/issues/31) [#34](https://github.com/AliSoftware/OHHTTPStubs/issues/34) + +## [2.3.1](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/2.3.1) + +* Fixed bug with HTTPStubsResponse+JSON when `nil` headers dictionary + +## [2.3.0](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/2.3.0) + +* Added the ability to give a name to a stub, for debugging purposes (property `name` of `id`) +* Added `allStubs` method to list all installed stubs (with their name if they have one, see previous point) +* Added `+[OHHTTPStubs onStubActivation:]` method to execute arbitrary code each time a stub is activated. Useful to log which stub is used for each request for example. + +## [2.2.1](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/2.2.1) + +* Complete refactoring to use `NSInputStream` instead of direct use of `NSData` (Thanks to @kcharwood - #28) +* Some other code refactoring to split the code in categories and make it clearer +* Some API changes to make `OHHTTPStubs` to fit the new possibility of setting both `requestTime` and `responseTime`. + * Old API is still there but deprecated, and will be removed in next major version + * To convert to the new API, you will mainly simply: + * extract the `responseTime:` parameter to a method call of its own (`return [HTTPStubsResponse responseWithData:data statusCode:code responseTime:time headers:header];` will become `return [[HTTPStubsResponse responseWithData:data statusCode:code headers:headers] responseTime:time];` etc.) + * convert `responseWithFile:filename` to `responseWithFileAtPath:OHPathForFileInBundle(filename,nil)` + +> Note: version `2.1.0-RC`, `2.1.0-rc.1`, `2.2.0-RC` and `2.2.1-RC` were intermediate Release Candidate versions during the big refactoring and migration to `2.2.1`, with the same new features as listed above basicaly, but without the last-minute bugfixes before official release. + +## [2.0.0](https://github.com/AliSoftware/OHHTTPStubs/releases/tag/2.0.0) + +* Simplified API + * removed instance methods, no more public `sharedInstance`: directly call class methods on the `OHHTTPStubs` class + * The old and problematic `addRequestHandler:` method has been deprecated and should not be used anymore. Use `stubRequestsPassingTest:withStubResponse:` instead, which is more efficient +* Added API documentation in the headers +* Remove all internal uses of Apple's private APIs + +> _Be careful: if you forgot to remove your use of `OHHTTPStubs` and your stubs from the binary you sent to the AppStore, your app would have been rejected by Apple before 2.0.0, as it was using private API (which was a way to make sure not to forget to remove them), but now it would be accepted silently. So don't forget to remove your stubs and `OHHTTPStubs` from your final binary!_ + +## [1.2.2](https://github.com/AliSoftware/OHHTTPStubs/tree/1.2.2) + +* Fixed Deadlock introduced by 1.2.1 + +## [1.2.1](https://github.com/AliSoftware/OHHTTPStubs/tree/1.2.1) + +* Improved thread-safety (#21) +* Stop sending messages to `NSURLProtoclClient` after `stopLoading` + +> _This version is buggy as it introduced a deadlock when performing a request on the main thread. 1.2.2 fixes that issue._ + +## [1.2.0](https://github.com/AliSoftware/OHHTTPStubs/tree/1.2.0) + +* Added support for "HTTP Message Data" stubs generated with `curl -is ` to replay them easily (#27). See the `README.md` for more info +* Added redirect support for 3xx response codes (#23) +* Dropped non-ARC support. Now `OHHTTPStubs` is to be compiled using ARC. _(This should not change anything as it is intended to be integrated using CocoaPods or compiled in a separate xcodeproj anyway)_ + +## [1.1.2](https://github.com/AliSoftware/OHHTTPStubs/tree/1.1.2) + +Easier integration process: + +* Use `#import ` again +* But adding the path to the library headers in your application project's `HEADER_SEARCH_PATH` is no longer needed! + +## [1.1.1](https://github.com/AliSoftware/OHHTTPStubs/tree/1.1.1) + +* Fixed crash when calling "setEnabled:" / "registerClass:" multiple times +* New integration process: we don't use the `PortableLibrary.xcconfig` anymore (as it generated problems for people using configuration with names other than "Debug" and "Release"). _(1)_ + +_You will now have to indicate the folder containing headers for `OHHTTPStubs` in your `HEADER_SEARCH_PATH` Build Settings, and we are back to `#import "OHHTTPStubs.h"` until a better solution is found_ + +> _(1) This modification for the integration process did only last for version 1.1.1. Version 1.1.2 restored `#import ` (but using a much better solution than the previous xcconfig used) and filling `HEADER_SEARCH_PATH` is no longer needed in further versions. See changelog for 1.1.2 above._ + +## [1.1.0](https://github.com/AliSoftware/OHHTTPStubs/tree/1.1.0) + +* Added new API `shouldStubRequest:withRequestHandler:` to avoid useless building of stubbed response like `addRequestHandler:` does + +## [1.0.6](https://github.com/AliSoftware/OHHTTPStubs/tree/1.0.6) + +* Adding support for cookies (Set-Cookie headers) + +## [1.0.5](https://github.com/AliSoftware/OHHTTPStubs/tree/1.0.5) + +* Added Unit Tests +* Removed calls to the deprecated `dispatch_get_current_queue()` GCD function (was used with `dispatch_after` to add fake delay to stubbed responses) + +## [1.0.4](https://github.com/AliSoftware/OHHTTPStubs/tree/1.0.4) + +* Fixed #6 : "responseWithError:" released response object too soon + +## [1.0.3](https://github.com/AliSoftware/OHHTTPStubs/tree/1.0.3) + +* Fixed small compilation issues #4 (issue in sample code) & #5 (ARC invalid cast) + +## [1.0.2](https://github.com/AliSoftware/OHHTTPStubs/tree/1.0.2) + +* Embedded `OHHTTPStubs` in a neat static library for nicer integration in your Xcode4 workspaces. + +## [1.0.1](https://github.com/AliSoftware/OHHTTPStubs/tree/1.0.1) + +* Fix issue when used in a SenTestCase + +## [1.0.0](https://github.com/AliSoftware/OHHTTPStubs/tree/1.0.0) + +* Cleaning API, added `removeLastHandler` and `removeRequestHandler:` method. +* Now first stable API in this version. +* Example project now compatible with ARC and non-ARC environments + +## [0.2.0](https://github.com/AliSoftware/OHHTTPStubs/tree/0.2.0) + +* Added Example project +* Added ARC support +* Some fixes + +## [0.1.0](https://github.com/AliSoftware/OHHTTPStubs/tree/0.1.0) + +* Initial version diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/MainViewController.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/MainViewController.h new file mode 100644 index 0000000000..bf1a8f5fce --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/MainViewController.h @@ -0,0 +1,13 @@ +// +// MainViewController.h +// OHHTTPStubsDemo +// +// Created by Olivier Halligon on 11/08/12. +// Copyright (c) 2012 AliSoftware. All rights reserved. +// + +#import + +@interface MainViewController : UIViewController + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/MainViewController.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/MainViewController.m new file mode 100644 index 0000000000..91c1b88739 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/MainViewController.m @@ -0,0 +1,174 @@ +// +// MainViewController.m +// OHHTTPStubsDemo +// +// Created by Olivier Halligon on 11/08/12. +// Copyright (c) 2012 AliSoftware. All rights reserved. +// + +#import "MainViewController.h" +#import +#import + + +@interface MainViewController () +// IBOutlets +@property (retain, nonatomic) IBOutlet UISwitch *delaySwitch; +@property (retain, nonatomic) IBOutlet UITextView *textView; +@property (retain, nonatomic) IBOutlet UISwitch *installTextStubSwitch; +@property (retain, nonatomic) IBOutlet UIImageView *imageView; +@property (retain, nonatomic) IBOutlet UISwitch *installImageStubSwitch; +@end + +@implementation MainViewController + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Init & Dealloc + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + [self installTextStub:self.installTextStubSwitch]; + [self installImageStub:self.installImageStubSwitch]; + [HTTPStubs onStubActivation:^(NSURLRequest * _Nonnull request, id _Nonnull stub, HTTPStubsResponse * _Nonnull responseStub) { + NSLog(@"[OHHTTPStubs] Request to %@ has been stubbed with %@", request.URL, stub.name); + }]; +} + +- (BOOL)shouldUseDelay { + __block BOOL res = NO; + dispatch_sync(dispatch_get_main_queue(), ^{ + res = self.delaySwitch.on; + }); + return res; +} + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Global stubs activation + +- (IBAction)toggleStubs:(UISwitch *)sender +{ + [HTTPStubs setEnabled:sender.on]; + self.delaySwitch.enabled = sender.on; + self.installTextStubSwitch.enabled = sender.on; + self.installImageStubSwitch.enabled = sender.on; + + NSLog(@"Installed (%@) stubs: %@", (sender.on?@"and enabled":@"but disabled"), HTTPStubs.allStubs); +} + + + + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Text Download and Stub + + +- (IBAction)downloadText:(UIButton*)sender +{ + sender.enabled = NO; + self.textView.text = nil; + + NSString* urlString = @"http://www.opensource.apple.com/source/Git/Git-26/src/git-htmldocs/git-commit.txt?txt"; + NSURLRequest* req = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString]]; + + // This is a very handy way to send an asynchronous method, but only available in iOS5+ + [NSURLConnection sendAsynchronousRequest:req + queue:[NSOperationQueue mainQueue] + completionHandler:^(NSURLResponse* resp, NSData* data, NSError* error) + { + sender.enabled = YES; + NSString* receivedText = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding]; + self.textView.text = receivedText; + }]; +} + + + + +- (IBAction)installTextStub:(UISwitch *)sender +{ + static id textStub = nil; // Note: no need to retain this value, it is retained by the OHHTTPStubs itself already + if (sender.on) + { + // Install + textStub = [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + // This stub will only configure stub requests for "*.txt" files + return [request.URL.pathExtension isEqualToString:@"txt"]; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + // Stub txt files with this + return [[HTTPStubsResponse responseWithFileAtPath:OHPathForFile(@"stub.txt", self.class) + statusCode:200 + headers:@{@"Content-Type":@"text/plain"}] + requestTime:[self shouldUseDelay] ? 2.f: 0.f + responseTime:OHHTTPStubsDownloadSpeedWifi]; + }]; + textStub.name = @"Text stub"; + } + else + { + // Uninstall + [HTTPStubs removeStub:textStub]; + } +} + + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Image Download and Stub + +- (IBAction)downloadImage:(UIButton*)sender +{ + sender.enabled = NO; + + NSString* urlString = @"http://images.apple.com/support/assets/images/products/iphone/hero_iphone4-5_wide.png"; + NSURLRequest* req = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString]]; + + // This is a very handy way to send an asynchronous method, but only available in iOS5+ + [NSURLConnection sendAsynchronousRequest:req + queue:[NSOperationQueue mainQueue] + completionHandler:^(NSURLResponse* resp, NSData* data, NSError* error) + { + dispatch_async(dispatch_get_main_queue(), ^{ + sender.enabled = YES; + self.imageView.image = [UIImage imageWithData:data]; + }); + }]; +} + +- (IBAction)installImageStub:(UISwitch *)sender +{ + static id imageStub = nil; // Note: no need to retain this value, it is retained by the OHHTTPStubs itself already :) + if (sender.on) + { + // Install + imageStub = [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + // This stub will only configure stub requests for "*.png" files + return [request.URL.pathExtension isEqualToString:@"png"]; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + // Stub jpg files with this + return [[HTTPStubsResponse responseWithFileAtPath:OHPathForFile(@"stub.jpg", self.class) + statusCode:200 + headers:@{@"Content-Type":@"image/jpeg"}] + requestTime:[self shouldUseDelay] ? 2.f: 0.f + responseTime:OHHTTPStubsDownloadSpeedWifi]; + }]; + imageStub.name = @"Image stub"; + } + else + { + // Uninstall + [HTTPStubs removeStub:imageStub]; + } +} + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Cleaning + +- (IBAction)clearResults +{ + self.textView.text = @""; + self.imageView.image = nil; +} + +@end + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/MainViewController.xib b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/MainViewController.xib new file mode 100644 index 0000000000..5639029389 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/MainViewController.xib @@ -0,0 +1,201 @@ + + + + + + + + + + + + + Menlo-Regular + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/OHHTTPStubsDemo.xcodeproj/project.pbxproj b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/OHHTTPStubsDemo.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..a0d8bfd5fa --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/OHHTTPStubsDemo.xcodeproj/project.pbxproj @@ -0,0 +1,395 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 0984831517805426002A99FF /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0984831417805426002A99FF /* CFNetwork.framework */; }; + 098FBDD415D704E800623941 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 098FBDD315D704E800623941 /* UIKit.framework */; }; + 098FBDD615D704E800623941 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 098FBDD515D704E800623941 /* Foundation.framework */; }; + 098FBDD815D704E800623941 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 098FBDD715D704E800623941 /* CoreGraphics.framework */; }; + 098FBDE015D704E800623941 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 098FBDDF15D704E800623941 /* main.m */; }; + 098FBDED15D7056200623941 /* MainViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 098FBDEB15D7056200623941 /* MainViewController.m */; }; + 098FBDEE15D7056200623941 /* MainViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 098FBDEC15D7056200623941 /* MainViewController.xib */; }; + 099C7343169016D800239880 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 099C7342169016D800239880 /* Default-568h@2x.png */; }; + 1F9ADC8C230037EE00F87660 /* stub.txt in Resources */ = {isa = PBXBuildFile; fileRef = 1F9ADC8A230037EE00F87660 /* stub.txt */; }; + 1F9ADC8D230037EE00F87660 /* stub.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 1F9ADC8B230037EE00F87660 /* stub.jpg */; }; + E8C65A455EC5B63D1737AF29 /* libPods-OHHTTPStubsDemo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6F2958101F89CBC90FC44F99 /* libPods-OHHTTPStubsDemo.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0984831417805426002A99FF /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; }; + 098FBDCF15D704E800623941 /* OHHTTPStubsDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OHHTTPStubsDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 098FBDD315D704E800623941 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + 098FBDD515D704E800623941 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + 098FBDD715D704E800623941 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + 098FBDDB15D704E800623941 /* OHHTTPStubsDemo-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "OHHTTPStubsDemo-Info.plist"; sourceTree = ""; }; + 098FBDDF15D704E800623941 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 098FBDE115D704E800623941 /* OHHTTPStubsDemo-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "OHHTTPStubsDemo-Prefix.pch"; sourceTree = ""; }; + 098FBDEA15D7056200623941 /* MainViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MainViewController.h; sourceTree = ""; }; + 098FBDEB15D7056200623941 /* MainViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MainViewController.m; sourceTree = ""; }; + 098FBDEC15D7056200623941 /* MainViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MainViewController.xib; sourceTree = ""; }; + 099C7342169016D800239880 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = ""; }; + 1F9ADC8A230037EE00F87660 /* stub.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = stub.txt; sourceTree = ""; }; + 1F9ADC8B230037EE00F87660 /* stub.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = stub.jpg; sourceTree = ""; }; + 48556A011AA6E9FD0074B154 /* libPods-OHHTTPStubs.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libPods-OHHTTPStubs.a"; path = "Pods/../build/Debug-iphoneos/libPods-OHHTTPStubs.a"; sourceTree = ""; }; + 6F2958101F89CBC90FC44F99 /* libPods-OHHTTPStubsDemo.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-OHHTTPStubsDemo.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 801E977139BBDAC0CC11ECAC /* Pods-OHHTTPStubsDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OHHTTPStubsDemo.debug.xcconfig"; path = "Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo.debug.xcconfig"; sourceTree = ""; }; + B6814E1693353E0AADB9895C /* Pods-OHHTTPStubsDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OHHTTPStubsDemo.release.xcconfig"; path = "Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 098FBDCC15D704E800623941 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0984831517805426002A99FF /* CFNetwork.framework in Frameworks */, + 098FBDD415D704E800623941 /* UIKit.framework in Frameworks */, + 098FBDD615D704E800623941 /* Foundation.framework in Frameworks */, + 098FBDD815D704E800623941 /* CoreGraphics.framework in Frameworks */, + E8C65A455EC5B63D1737AF29 /* libPods-OHHTTPStubsDemo.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 098FBDC415D704E800623941 = { + isa = PBXGroup; + children = ( + 098FBDD915D704E800623941 /* OHHTTPStubsDemo */, + 098FBDD215D704E800623941 /* Frameworks */, + 098FBDD015D704E800623941 /* Products */, + 1B8830D2F747CB6ADB7875D1 /* Pods */, + ); + sourceTree = ""; + }; + 098FBDD015D704E800623941 /* Products */ = { + isa = PBXGroup; + children = ( + 098FBDCF15D704E800623941 /* OHHTTPStubsDemo.app */, + ); + name = Products; + sourceTree = ""; + }; + 098FBDD215D704E800623941 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 48556A011AA6E9FD0074B154 /* libPods-OHHTTPStubs.a */, + 0984831417805426002A99FF /* CFNetwork.framework */, + 098FBDD315D704E800623941 /* UIKit.framework */, + 098FBDD515D704E800623941 /* Foundation.framework */, + 098FBDD715D704E800623941 /* CoreGraphics.framework */, + 6F2958101F89CBC90FC44F99 /* libPods-OHHTTPStubsDemo.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 098FBDD915D704E800623941 /* OHHTTPStubsDemo */ = { + isa = PBXGroup; + children = ( + 098FBDEA15D7056200623941 /* MainViewController.h */, + 098FBDEB15D7056200623941 /* MainViewController.m */, + 098FBDEC15D7056200623941 /* MainViewController.xib */, + 1F9ADC89230037EE00F87660 /* Stubs */, + 098FBDDA15D704E800623941 /* Supporting Files */, + ); + name = OHHTTPStubsDemo; + sourceTree = ""; + }; + 098FBDDA15D704E800623941 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 099C7342169016D800239880 /* Default-568h@2x.png */, + 098FBDDB15D704E800623941 /* OHHTTPStubsDemo-Info.plist */, + 098FBDDF15D704E800623941 /* main.m */, + 098FBDE115D704E800623941 /* OHHTTPStubsDemo-Prefix.pch */, + ); + path = "Supporting Files"; + sourceTree = ""; + }; + 1B8830D2F747CB6ADB7875D1 /* Pods */ = { + isa = PBXGroup; + children = ( + 801E977139BBDAC0CC11ECAC /* Pods-OHHTTPStubsDemo.debug.xcconfig */, + B6814E1693353E0AADB9895C /* Pods-OHHTTPStubsDemo.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 1F9ADC89230037EE00F87660 /* Stubs */ = { + isa = PBXGroup; + children = ( + 1F9ADC8A230037EE00F87660 /* stub.txt */, + 1F9ADC8B230037EE00F87660 /* stub.jpg */, + ); + name = Stubs; + path = ../Stubs; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 098FBDCE15D704E800623941 /* OHHTTPStubsDemo */ = { + isa = PBXNativeTarget; + buildConfigurationList = 098FBDE715D704E800623941 /* Build configuration list for PBXNativeTarget "OHHTTPStubsDemo" */; + buildPhases = ( + 630F94225164E5ED58A2AD5D /* [CP] Check Pods Manifest.lock */, + 098FBDCB15D704E800623941 /* Sources */, + 098FBDCC15D704E800623941 /* Frameworks */, + 098FBDCD15D704E800623941 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = OHHTTPStubsDemo; + productName = OHHTTPStubsDemo; + productReference = 098FBDCF15D704E800623941 /* OHHTTPStubsDemo.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 098FBDC615D704E800623941 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = AliSoftware; + TargetAttributes = { + 098FBDCE15D704E800623941 = { + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 098FBDC915D704E800623941 /* Build configuration list for PBXProject "OHHTTPStubsDemo" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 098FBDC415D704E800623941; + productRefGroup = 098FBDD015D704E800623941 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 098FBDCE15D704E800623941 /* OHHTTPStubsDemo */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 098FBDCD15D704E800623941 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 098FBDEE15D7056200623941 /* MainViewController.xib in Resources */, + 1F9ADC8D230037EE00F87660 /* stub.jpg in Resources */, + 1F9ADC8C230037EE00F87660 /* stub.txt in Resources */, + 099C7343169016D800239880 /* Default-568h@2x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 630F94225164E5ED58A2AD5D /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-OHHTTPStubsDemo-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 098FBDCB15D704E800623941 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 098FBDE015D704E800623941 /* main.m in Sources */, + 098FBDED15D7056200623941 /* MainViewController.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 098FBDE515D704E800623941 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + 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_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; + GCC_VERSION = com.apple.compilers.llvm.clang.1_0; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES; + GCC_WARN_ABOUT_MISSING_NEWLINE = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; + GCC_WARN_SHADOW = YES; + GCC_WARN_SIGN_COMPARE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNKNOWN_PRAGMAS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = "-ObjC"; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 098FBDE615D704E800623941 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES; + GCC_VERSION = com.apple.compilers.llvm.clang.1_0; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES; + GCC_WARN_ABOUT_MISSING_NEWLINE = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; + GCC_WARN_SHADOW = YES; + GCC_WARN_SIGN_COMPARE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNKNOWN_PRAGMAS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1"; + OTHER_LDFLAGS = "-ObjC"; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 098FBDE815D704E800623941 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 801E977139BBDAC0CC11ECAC /* Pods-OHHTTPStubsDemo.debug.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_ARC = YES; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Supporting Files/OHHTTPStubsDemo-Prefix.pch"; + INFOPLIST_FILE = "$(SRCROOT)/Supporting Files/OHHTTPStubsDemo-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "com.alisoftware.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = app; + }; + name = Debug; + }; + 098FBDE915D704E800623941 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B6814E1693353E0AADB9895C /* Pods-OHHTTPStubsDemo.release.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_ARC = YES; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Supporting Files/OHHTTPStubsDemo-Prefix.pch"; + INFOPLIST_FILE = "$(SRCROOT)/Supporting Files/OHHTTPStubsDemo-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "com.alisoftware.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = app; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 098FBDC915D704E800623941 /* Build configuration list for PBXProject "OHHTTPStubsDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 098FBDE515D704E800623941 /* Debug */, + 098FBDE615D704E800623941 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 098FBDE715D704E800623941 /* Build configuration list for PBXNativeTarget "OHHTTPStubsDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 098FBDE815D704E800623941 /* Debug */, + 098FBDE915D704E800623941 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 098FBDC615D704E800623941 /* Project object */; +} diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/OHHTTPStubsDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/OHHTTPStubsDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..68ace104de --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/OHHTTPStubsDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/OHHTTPStubsDemo.xcodeproj/xcshareddata/xcschemes/OHHTTPStubsDemo.xcscheme b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/OHHTTPStubsDemo.xcodeproj/xcshareddata/xcschemes/OHHTTPStubsDemo.xcscheme new file mode 100644 index 0000000000..8144e53863 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/OHHTTPStubsDemo.xcodeproj/xcshareddata/xcschemes/OHHTTPStubsDemo.xcscheme @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/OHHTTPStubsDemo.xcworkspace/contents.xcworkspacedata b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/OHHTTPStubsDemo.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..cfc0e73f1c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/OHHTTPStubsDemo.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/OHHTTPStubsDemo.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/OHHTTPStubsDemo.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000000..0c67376eba --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/OHHTTPStubsDemo.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Podfile b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Podfile new file mode 100644 index 0000000000..c382cea7be --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Podfile @@ -0,0 +1,8 @@ +source 'https://github.com/CocoaPods/Specs.git' + +project 'OHHTTPStubsDemo.xcodeproj' +platform :ios, '8.0' + +target 'OHHTTPStubsDemo' do + pod 'OHHTTPStubs', :path => '../..' +end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Podfile.lock b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Podfile.lock new file mode 100644 index 0000000000..7ce86df4b5 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Podfile.lock @@ -0,0 +1,28 @@ +PODS: + - OHHTTPStubs (9.0.0): + - OHHTTPStubs/Default (= 9.0.0) + - OHHTTPStubs/Core (9.0.0) + - OHHTTPStubs/Default (9.0.0): + - OHHTTPStubs/Core + - OHHTTPStubs/JSON + - OHHTTPStubs/NSURLSession + - OHHTTPStubs/OHPathHelpers + - OHHTTPStubs/JSON (9.0.0): + - OHHTTPStubs/Core + - OHHTTPStubs/NSURLSession (9.0.0): + - OHHTTPStubs/Core + - OHHTTPStubs/OHPathHelpers (9.0.0) + +DEPENDENCIES: + - OHHTTPStubs (from `../..`) + +EXTERNAL SOURCES: + OHHTTPStubs: + :path: "../.." + +SPEC CHECKSUMS: + OHHTTPStubs: cb29d2a9d09a828ecb93349a2b0c64f99e0db89f + +PODFILE CHECKSUM: 9a67077a86911aa4a252748903da3d5ea5d5d922 + +COCOAPODS: 1.7.5 diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Private/OHHTTPStubs/Compatibility.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Private/OHHTTPStubs/Compatibility.h new file mode 120000 index 0000000000..9657ef98f6 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Private/OHHTTPStubs/Compatibility.h @@ -0,0 +1 @@ +../../../../../../Sources/OHHTTPStubs/include/Compatibility.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Private/OHHTTPStubs/HTTPStubs.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Private/OHHTTPStubs/HTTPStubs.h new file mode 120000 index 0000000000..c1a203d77c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Private/OHHTTPStubs/HTTPStubs.h @@ -0,0 +1 @@ +../../../../../../Sources/OHHTTPStubs/include/HTTPStubs.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Private/OHHTTPStubs/HTTPStubsMethodSwizzling.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Private/OHHTTPStubs/HTTPStubsMethodSwizzling.h new file mode 120000 index 0000000000..f7ea5f5176 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Private/OHHTTPStubs/HTTPStubsMethodSwizzling.h @@ -0,0 +1 @@ +../../../../../../Sources/OHHTTPStubs/HTTPStubsMethodSwizzling.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Private/OHHTTPStubs/HTTPStubsPathHelpers.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Private/OHHTTPStubs/HTTPStubsPathHelpers.h new file mode 120000 index 0000000000..ceb407e96b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Private/OHHTTPStubs/HTTPStubsPathHelpers.h @@ -0,0 +1 @@ +../../../../../../Sources/OHHTTPStubs/include/HTTPStubsPathHelpers.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Private/OHHTTPStubs/HTTPStubsResponse+JSON.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Private/OHHTTPStubs/HTTPStubsResponse+JSON.h new file mode 120000 index 0000000000..0323927b55 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Private/OHHTTPStubs/HTTPStubsResponse+JSON.h @@ -0,0 +1 @@ +../../../../../../Sources/OHHTTPStubs/include/HTTPStubsResponse+JSON.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Private/OHHTTPStubs/HTTPStubsResponse.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Private/OHHTTPStubs/HTTPStubsResponse.h new file mode 120000 index 0000000000..284036e208 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Private/OHHTTPStubs/HTTPStubsResponse.h @@ -0,0 +1 @@ +../../../../../../Sources/OHHTTPStubs/include/HTTPStubsResponse.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Private/OHHTTPStubs/NSURLRequest+HTTPBodyTesting.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Private/OHHTTPStubs/NSURLRequest+HTTPBodyTesting.h new file mode 120000 index 0000000000..f5fd7755b9 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Private/OHHTTPStubs/NSURLRequest+HTTPBodyTesting.h @@ -0,0 +1 @@ +../../../../../../Sources/OHHTTPStubs/include/NSURLRequest+HTTPBodyTesting.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Public/OHHTTPStubs/Compatibility.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Public/OHHTTPStubs/Compatibility.h new file mode 120000 index 0000000000..9657ef98f6 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Public/OHHTTPStubs/Compatibility.h @@ -0,0 +1 @@ +../../../../../../Sources/OHHTTPStubs/include/Compatibility.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Public/OHHTTPStubs/HTTPStubs.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Public/OHHTTPStubs/HTTPStubs.h new file mode 120000 index 0000000000..c1a203d77c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Public/OHHTTPStubs/HTTPStubs.h @@ -0,0 +1 @@ +../../../../../../Sources/OHHTTPStubs/include/HTTPStubs.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Public/OHHTTPStubs/HTTPStubsPathHelpers.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Public/OHHTTPStubs/HTTPStubsPathHelpers.h new file mode 120000 index 0000000000..ceb407e96b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Public/OHHTTPStubs/HTTPStubsPathHelpers.h @@ -0,0 +1 @@ +../../../../../../Sources/OHHTTPStubs/include/HTTPStubsPathHelpers.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Public/OHHTTPStubs/HTTPStubsResponse+JSON.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Public/OHHTTPStubs/HTTPStubsResponse+JSON.h new file mode 120000 index 0000000000..0323927b55 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Public/OHHTTPStubs/HTTPStubsResponse+JSON.h @@ -0,0 +1 @@ +../../../../../../Sources/OHHTTPStubs/include/HTTPStubsResponse+JSON.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Public/OHHTTPStubs/HTTPStubsResponse.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Public/OHHTTPStubs/HTTPStubsResponse.h new file mode 120000 index 0000000000..284036e208 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Public/OHHTTPStubs/HTTPStubsResponse.h @@ -0,0 +1 @@ +../../../../../../Sources/OHHTTPStubs/include/HTTPStubsResponse.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Public/OHHTTPStubs/NSURLRequest+HTTPBodyTesting.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Public/OHHTTPStubs/NSURLRequest+HTTPBodyTesting.h new file mode 120000 index 0000000000..f5fd7755b9 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Headers/Public/OHHTTPStubs/NSURLRequest+HTTPBodyTesting.h @@ -0,0 +1 @@ +../../../../../../Sources/OHHTTPStubs/include/NSURLRequest+HTTPBodyTesting.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Local Podspecs/OHHTTPStubs.podspec.json b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Local Podspecs/OHHTTPStubs.podspec.json new file mode 100644 index 0000000000..389a736d39 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Local Podspecs/OHHTTPStubs.podspec.json @@ -0,0 +1,128 @@ +{ + "name": "OHHTTPStubs", + "version": "9.0.0", + "summary": "Framework to stub your network requests like HTTP and help you write network unit tests with XCTest.", + "description": "A class to stub network requests easily:\n\n * Test your apps with fake network data (stubbed from file)\n * You can also customize your response headers and status code\n * Use customized stubs depending on the requests\n * Use custom response time to simulate slow network.\n * This works with any request (HTTP, HTTPS, or any protocol) sent using\n the iOS URL Loading System (NSURLConnection, NSURLSession, AFNetworking, …)\n * This is really useful in unit testing, when you need to test network features\n but don't want to hit the real network and fake some response data instead.\n * Has useful convenience methods to stub JSON content or fixture from a file\n * Compatible with Swift", + "homepage": "https://github.com/AliSoftware/OHHTTPStubs", + "license": "MIT", + "authors": { + "Olivier Halligon": "olivier.halligon+ae@gmail.com" + }, + "source": { + "git": "https://github.com/AliSoftware/OHHTTPStubs.git", + "tag": "9.0.0" + }, + "frameworks": [ + "Foundation", + "CFNetwork" + ], + "requires_arc": true, + "platforms": { + "ios": "8.0", + "osx": "10.9", + "watchos": "2.0", + "tvos": "9.0" + }, + "swift_versions": [ + "3.0", + "3.1", + "3.2", + "4.0", + "4.1", + "4.2", + "5.0", + "5.1" + ], + "default_subspecs": "Default", + "subspecs": [ + { + "name": "Default", + "dependencies": { + "OHHTTPStubs/Core": [ + + ], + "OHHTTPStubs/NSURLSession": [ + + ], + "OHHTTPStubs/JSON": [ + + ], + "OHHTTPStubs/OHPathHelpers": [ + + ] + } + }, + { + "name": "Core", + "source_files": [ + "Sources/OHHTTPStubs/**/HTTPStubs.{h,m}", + "Sources/OHHTTPStubs/**/HTTPStubsResponse.{h,m}", + "Sources/OHHTTPStubs/include/Compatibility.h" + ] + }, + { + "name": "NSURLSession", + "dependencies": { + "OHHTTPStubs/Core": [ + + ] + }, + "source_files": [ + "Sources/OHHTTPStubs/**/NSURLRequest+HTTPBodyTesting.{h,m}", + "Sources/OHHTTPStubs/**/HTTPStubs+NSURLSessionConfiguration.{h,m}", + "Sources/OHHTTPStubs/**/HTTPStubsMethodSwizzling.{h,m}" + ], + "private_header_files": "Sources/OHHTTPStubs/**/HTTPStubsMethodSwizzling.h" + }, + { + "name": "JSON", + "dependencies": { + "OHHTTPStubs/Core": [ + + ] + }, + "source_files": "Sources/OHHTTPStubs/**/HTTPStubsResponse+JSON.{h,m}" + }, + { + "name": "HTTPMessage", + "dependencies": { + "OHHTTPStubs/Core": [ + + ] + }, + "source_files": "Sources/HTTPMessage/**/*.{h,m}" + }, + { + "name": "Mocktail", + "dependencies": { + "OHHTTPStubs/Core": [ + + ] + }, + "source_files": "Sources/Mocktail/**/*.{h,m}" + }, + { + "name": "OHPathHelpers", + "source_files": [ + "Sources/OHHTTPStubs/**/HTTPStubsPathHelpers.{h,m}", + "Sources/OHHTTPStubs/include/Compatibility.h" + ] + }, + { + "name": "Swift", + "platforms": { + "ios": "8.0", + "osx": "10.9", + "watchos": "2.0", + "tvos": "9.0" + }, + "dependencies": { + "OHHTTPStubs/Default": [ + + ] + }, + "source_files": "Sources/OHHTTPStubsSwift/*.swift" + } + ], + "swift_version": "5.1" +} diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Manifest.lock b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Manifest.lock new file mode 100644 index 0000000000..7ce86df4b5 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Manifest.lock @@ -0,0 +1,28 @@ +PODS: + - OHHTTPStubs (9.0.0): + - OHHTTPStubs/Default (= 9.0.0) + - OHHTTPStubs/Core (9.0.0) + - OHHTTPStubs/Default (9.0.0): + - OHHTTPStubs/Core + - OHHTTPStubs/JSON + - OHHTTPStubs/NSURLSession + - OHHTTPStubs/OHPathHelpers + - OHHTTPStubs/JSON (9.0.0): + - OHHTTPStubs/Core + - OHHTTPStubs/NSURLSession (9.0.0): + - OHHTTPStubs/Core + - OHHTTPStubs/OHPathHelpers (9.0.0) + +DEPENDENCIES: + - OHHTTPStubs (from `../..`) + +EXTERNAL SOURCES: + OHHTTPStubs: + :path: "../.." + +SPEC CHECKSUMS: + OHHTTPStubs: cb29d2a9d09a828ecb93349a2b0c64f99e0db89f + +PODFILE CHECKSUM: 9a67077a86911aa4a252748903da3d5ea5d5d922 + +COCOAPODS: 1.7.5 diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Pods.xcodeproj/project.pbxproj b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Pods.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..1bfebd2760 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Pods.xcodeproj/project.pbxproj @@ -0,0 +1,630 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 05D04D5BA99C63E9CE718806BFBE5378 /* HTTPStubsResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = B27A88D4A3BFDE23226FB5B81297D50E /* HTTPStubsResponse.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 2B81B3D6140D286219E09207A4A76CB2 /* HTTPStubsMethodSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = BA41C6C4D6F584F0639B6D5F89672BB2 /* HTTPStubsMethodSwizzling.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 39A047A944570A4A2B5C8E627DB12511 /* HTTPStubs+NSURLSessionConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 19F3BA23F8748445E5CD3F1B344D49EA /* HTTPStubs+NSURLSessionConfiguration.m */; }; + 497A8DDB766AE6FE948AA248CCD1F434 /* HTTPStubs.m in Sources */ = {isa = PBXBuildFile; fileRef = 18A17568479A4FE36CDAF4ABC2E020DA /* HTTPStubs.m */; }; + 6829EA42E7094A040A0FA1C64C653C4C /* OHHTTPStubs-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = C5C6B95C41B64AD40F910DF2955A9C05 /* OHHTTPStubs-dummy.m */; }; + 80FAF560A96DFB636A33D463A3417ECF /* Pods-OHHTTPStubsDemo-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 2169D273ACA9F35F68305A705EE988D5 /* Pods-OHHTTPStubsDemo-dummy.m */; }; + 8B47F893258916A6CABE7F44BA59B8BD /* NSURLRequest+HTTPBodyTesting.h in Headers */ = {isa = PBXBuildFile; fileRef = FFE2DC5F4932F9A6D902924360B61162 /* NSURLRequest+HTTPBodyTesting.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 95511583081A96910E8C92E0778FFEE0 /* HTTPStubsResponse+JSON.h in Headers */ = {isa = PBXBuildFile; fileRef = 5933DE4839DF8A0564CBADD7227B688B /* HTTPStubsResponse+JSON.h */; settings = {ATTRIBUTES = (Project, ); }; }; + A007DA77C67B5D7ABB0964C373B3047C /* HTTPStubsPathHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = F945AFCC961B816FA1468F0919FCE8BD /* HTTPStubsPathHelpers.h */; settings = {ATTRIBUTES = (Project, ); }; }; + AF29B3A8319A3078570DB7348BDC96BC /* HTTPStubsPathHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = EA40ABA4427589A3A5BE3BC586B68C95 /* HTTPStubsPathHelpers.m */; }; + BBDE3955B4BB65D5CC3FE4241F6F7194 /* Compatibility.h in Headers */ = {isa = PBXBuildFile; fileRef = B0F8FC4D06EBA717FAB0BA66F19FC133 /* Compatibility.h */; settings = {ATTRIBUTES = (Project, ); }; }; + BD490CD193473C6F388E0A5711974AF8 /* HTTPStubs.h in Headers */ = {isa = PBXBuildFile; fileRef = 6614931A9F2D3D77F89BC889D5781BF7 /* HTTPStubs.h */; settings = {ATTRIBUTES = (Project, ); }; }; + C070992EC73066BE6F90AFA1C3A8B834 /* HTTPStubsResponse+JSON.m in Sources */ = {isa = PBXBuildFile; fileRef = 9388442BB5E940A92AD8B8FBE2FC3DB0 /* HTTPStubsResponse+JSON.m */; }; + C483FF83B9A4C871111EBB9F8F024D02 /* HTTPStubsMethodSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = 76CA8AA23B8BF14273DB175FB51B71D8 /* HTTPStubsMethodSwizzling.m */; }; + DF0EDBAFF5E4AC6599018383C2344FE6 /* NSURLRequest+HTTPBodyTesting.m in Sources */ = {isa = PBXBuildFile; fileRef = E937FA983A2EE811E08094A643529998 /* NSURLRequest+HTTPBodyTesting.m */; }; + F4670514839A03F81092B93536410C55 /* HTTPStubsResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 845DA5D9D9CBC8C767DA682F67CDDB30 /* HTTPStubsResponse.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 0FB21B4EBE0C3FFC465E9F34ADFC7845 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = A983A2D06C5B6AA3D6ABA5CCC0A16725; + remoteInfo = OHHTTPStubs; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 00EF0B55C6C0A1F37D57E9BA7C5855FE /* OHHTTPStubs.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = OHHTTPStubs.xcconfig; sourceTree = ""; }; + 0AA1D1F88F6299E7E5A1AE00A1BD636C /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; path = LICENSE; sourceTree = ""; }; + 17E33041B314FA837A3CAEB9DF3CDE9F /* libOHHTTPStubs.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; name = libOHHTTPStubs.a; path = libOHHTTPStubs.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 18A17568479A4FE36CDAF4ABC2E020DA /* HTTPStubs.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = HTTPStubs.m; path = Sources/OHHTTPStubs/HTTPStubs.m; sourceTree = ""; }; + 19F3BA23F8748445E5CD3F1B344D49EA /* HTTPStubs+NSURLSessionConfiguration.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "HTTPStubs+NSURLSessionConfiguration.m"; path = "Sources/OHHTTPStubs/HTTPStubs+NSURLSessionConfiguration.m"; sourceTree = ""; }; + 2169D273ACA9F35F68305A705EE988D5 /* Pods-OHHTTPStubsDemo-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-OHHTTPStubsDemo-dummy.m"; sourceTree = ""; }; + 42FA0EF1B0E02E80A15887FDE55C1DF2 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; path = README.md; sourceTree = ""; }; + 5933DE4839DF8A0564CBADD7227B688B /* HTTPStubsResponse+JSON.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "HTTPStubsResponse+JSON.h"; sourceTree = ""; }; + 5C6E57AA6EE3246CC7A70F569EC42B2C /* libPods-OHHTTPStubsDemo.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; name = "libPods-OHHTTPStubsDemo.a"; path = "libPods-OHHTTPStubsDemo.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 6614931A9F2D3D77F89BC889D5781BF7 /* HTTPStubs.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = HTTPStubs.h; sourceTree = ""; }; + 76CA8AA23B8BF14273DB175FB51B71D8 /* HTTPStubsMethodSwizzling.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = HTTPStubsMethodSwizzling.m; path = Sources/OHHTTPStubs/HTTPStubsMethodSwizzling.m; sourceTree = ""; }; + 845DA5D9D9CBC8C767DA682F67CDDB30 /* HTTPStubsResponse.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = HTTPStubsResponse.m; path = Sources/OHHTTPStubs/HTTPStubsResponse.m; sourceTree = ""; }; + 9388442BB5E940A92AD8B8FBE2FC3DB0 /* HTTPStubsResponse+JSON.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "HTTPStubsResponse+JSON.m"; path = "Sources/OHHTTPStubs/HTTPStubsResponse+JSON.m"; sourceTree = ""; }; + 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; + AAAAB884699C361A9CFDB6725B839D6C /* Pods-OHHTTPStubsDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-OHHTTPStubsDemo.debug.xcconfig"; sourceTree = ""; }; + AD936D2A49FC74FE62F4902EF24F3E88 /* Pods-OHHTTPStubsDemo-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-OHHTTPStubsDemo-acknowledgements.plist"; sourceTree = ""; }; + B0F8FC4D06EBA717FAB0BA66F19FC133 /* Compatibility.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = Compatibility.h; sourceTree = ""; }; + B27A88D4A3BFDE23226FB5B81297D50E /* HTTPStubsResponse.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = HTTPStubsResponse.h; sourceTree = ""; }; + BA41C6C4D6F584F0639B6D5F89672BB2 /* HTTPStubsMethodSwizzling.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = HTTPStubsMethodSwizzling.h; path = Sources/OHHTTPStubs/HTTPStubsMethodSwizzling.h; sourceTree = ""; }; + BED3B389C513208FB48004F765A53526 /* Pods-OHHTTPStubsDemo-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-OHHTTPStubsDemo-acknowledgements.markdown"; sourceTree = ""; }; + C5C6B95C41B64AD40F910DF2955A9C05 /* OHHTTPStubs-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "OHHTTPStubs-dummy.m"; sourceTree = ""; }; + E937FA983A2EE811E08094A643529998 /* NSURLRequest+HTTPBodyTesting.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSURLRequest+HTTPBodyTesting.m"; path = "Sources/OHHTTPStubs/NSURLRequest+HTTPBodyTesting.m"; sourceTree = ""; }; + EA40ABA4427589A3A5BE3BC586B68C95 /* HTTPStubsPathHelpers.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = HTTPStubsPathHelpers.m; path = Sources/OHHTTPStubs/HTTPStubsPathHelpers.m; sourceTree = ""; }; + F3CFF2F57EBC43E25F4482E13115E75C /* OHHTTPStubs.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; path = OHHTTPStubs.podspec; sourceTree = ""; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; + F6905472866F02BC4D2DE8BC0BF53907 /* OHHTTPStubs-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "OHHTTPStubs-prefix.pch"; sourceTree = ""; }; + F945AFCC961B816FA1468F0919FCE8BD /* HTTPStubsPathHelpers.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = HTTPStubsPathHelpers.h; sourceTree = ""; }; + FC88D895104CA8ECC7CF0F5669A9A00B /* Pods-OHHTTPStubsDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-OHHTTPStubsDemo.release.xcconfig"; sourceTree = ""; }; + FFE2DC5F4932F9A6D902924360B61162 /* NSURLRequest+HTTPBodyTesting.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "NSURLRequest+HTTPBodyTesting.h"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 68A287CF1529D3AF8B895CE99D3E0067 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F18A388836E9F5A2E85D0E54DB637C80 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 38444F6AD89EA4E543FFBD90F78D3D04 /* NSURLSession */ = { + isa = PBXGroup; + children = ( + 19F3BA23F8748445E5CD3F1B344D49EA /* HTTPStubs+NSURLSessionConfiguration.m */, + BA41C6C4D6F584F0639B6D5F89672BB2 /* HTTPStubsMethodSwizzling.h */, + 76CA8AA23B8BF14273DB175FB51B71D8 /* HTTPStubsMethodSwizzling.m */, + E937FA983A2EE811E08094A643529998 /* NSURLRequest+HTTPBodyTesting.m */, + 8A8784BDEE6991EAF505C96C792B82D9 /* include */, + ); + name = NSURLSession; + sourceTree = ""; + }; + 6EAAF31504A9C002CEBBB04B6A66FBC1 /* include */ = { + isa = PBXGroup; + children = ( + 5933DE4839DF8A0564CBADD7227B688B /* HTTPStubsResponse+JSON.h */, + ); + name = include; + path = Sources/OHHTTPStubs/include; + sourceTree = ""; + }; + 7D4C225DF5043E5E6E5B17FFE56AEDD6 /* Pod */ = { + isa = PBXGroup; + children = ( + 0AA1D1F88F6299E7E5A1AE00A1BD636C /* LICENSE */, + F3CFF2F57EBC43E25F4482E13115E75C /* OHHTTPStubs.podspec */, + 42FA0EF1B0E02E80A15887FDE55C1DF2 /* README.md */, + ); + name = Pod; + sourceTree = ""; + }; + 801719C05F2882477EC1CBD01710155D /* JSON */ = { + isa = PBXGroup; + children = ( + 9388442BB5E940A92AD8B8FBE2FC3DB0 /* HTTPStubsResponse+JSON.m */, + 6EAAF31504A9C002CEBBB04B6A66FBC1 /* include */, + ); + name = JSON; + sourceTree = ""; + }; + 82644D6A9BCEBFEE0BD227A1C7CE7047 /* Support Files */ = { + isa = PBXGroup; + children = ( + 00EF0B55C6C0A1F37D57E9BA7C5855FE /* OHHTTPStubs.xcconfig */, + C5C6B95C41B64AD40F910DF2955A9C05 /* OHHTTPStubs-dummy.m */, + F6905472866F02BC4D2DE8BC0BF53907 /* OHHTTPStubs-prefix.pch */, + ); + name = "Support Files"; + path = "Examples/ObjC/Pods/Target Support Files/OHHTTPStubs"; + sourceTree = ""; + }; + 891CE504A27276006B5145ECF89645DA /* OHPathHelpers */ = { + isa = PBXGroup; + children = ( + EA40ABA4427589A3A5BE3BC586B68C95 /* HTTPStubsPathHelpers.m */, + 8EBD1B74109EFA143FC64C72F3C1CAEB /* include */, + ); + name = OHPathHelpers; + sourceTree = ""; + }; + 8A8784BDEE6991EAF505C96C792B82D9 /* include */ = { + isa = PBXGroup; + children = ( + FFE2DC5F4932F9A6D902924360B61162 /* NSURLRequest+HTTPBodyTesting.h */, + ); + name = include; + path = Sources/OHHTTPStubs/include; + sourceTree = ""; + }; + 8EBD1B74109EFA143FC64C72F3C1CAEB /* include */ = { + isa = PBXGroup; + children = ( + F945AFCC961B816FA1468F0919FCE8BD /* HTTPStubsPathHelpers.h */, + ); + name = include; + path = Sources/OHHTTPStubs/include; + sourceTree = ""; + }; + 99E2EFEB1468E6BC88D8711D61FDB9AD /* Core */ = { + isa = PBXGroup; + children = ( + 18A17568479A4FE36CDAF4ABC2E020DA /* HTTPStubs.m */, + 845DA5D9D9CBC8C767DA682F67CDDB30 /* HTTPStubsResponse.m */, + FD3BBE5AA4EE6AF881E1DE6B6D04F765 /* include */, + ); + name = Core; + sourceTree = ""; + }; + 9DF898CB87CDDF6540022FCA0232A1E9 /* Products */ = { + isa = PBXGroup; + children = ( + 17E33041B314FA837A3CAEB9DF3CDE9F /* libOHHTTPStubs.a */, + 5C6E57AA6EE3246CC7A70F569EC42B2C /* libPods-OHHTTPStubsDemo.a */, + ); + name = Products; + sourceTree = ""; + }; + BB2E615DD131B88A20456F3D3F9BCD5A /* Pods-OHHTTPStubsDemo */ = { + isa = PBXGroup; + children = ( + BED3B389C513208FB48004F765A53526 /* Pods-OHHTTPStubsDemo-acknowledgements.markdown */, + AD936D2A49FC74FE62F4902EF24F3E88 /* Pods-OHHTTPStubsDemo-acknowledgements.plist */, + 2169D273ACA9F35F68305A705EE988D5 /* Pods-OHHTTPStubsDemo-dummy.m */, + AAAAB884699C361A9CFDB6725B839D6C /* Pods-OHHTTPStubsDemo.debug.xcconfig */, + FC88D895104CA8ECC7CF0F5669A9A00B /* Pods-OHHTTPStubsDemo.release.xcconfig */, + ); + name = "Pods-OHHTTPStubsDemo"; + path = "Target Support Files/Pods-OHHTTPStubsDemo"; + sourceTree = ""; + }; + CF1408CF629C7361332E53B88F7BD30C = { + isa = PBXGroup; + children = ( + 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */, + F345DC827FF7C227A1272505063FBC70 /* Development Pods */, + D89477F20FB1DE18A04690586D7808C4 /* Frameworks */, + 9DF898CB87CDDF6540022FCA0232A1E9 /* Products */, + F148C1DA9D17D83001CBDD403442F240 /* Targets Support Files */, + ); + sourceTree = ""; + }; + D084A78E64DC966EA5F26388CEC83E58 /* OHHTTPStubs */ = { + isa = PBXGroup; + children = ( + 99E2EFEB1468E6BC88D8711D61FDB9AD /* Core */, + 801719C05F2882477EC1CBD01710155D /* JSON */, + 38444F6AD89EA4E543FFBD90F78D3D04 /* NSURLSession */, + 891CE504A27276006B5145ECF89645DA /* OHPathHelpers */, + 7D4C225DF5043E5E6E5B17FFE56AEDD6 /* Pod */, + 82644D6A9BCEBFEE0BD227A1C7CE7047 /* Support Files */, + ); + name = OHHTTPStubs; + path = ../../..; + sourceTree = ""; + }; + D89477F20FB1DE18A04690586D7808C4 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + F148C1DA9D17D83001CBDD403442F240 /* Targets Support Files */ = { + isa = PBXGroup; + children = ( + BB2E615DD131B88A20456F3D3F9BCD5A /* Pods-OHHTTPStubsDemo */, + ); + name = "Targets Support Files"; + sourceTree = ""; + }; + F345DC827FF7C227A1272505063FBC70 /* Development Pods */ = { + isa = PBXGroup; + children = ( + D084A78E64DC966EA5F26388CEC83E58 /* OHHTTPStubs */, + ); + name = "Development Pods"; + sourceTree = ""; + }; + FD3BBE5AA4EE6AF881E1DE6B6D04F765 /* include */ = { + isa = PBXGroup; + children = ( + B0F8FC4D06EBA717FAB0BA66F19FC133 /* Compatibility.h */, + 6614931A9F2D3D77F89BC889D5781BF7 /* HTTPStubs.h */, + B27A88D4A3BFDE23226FB5B81297D50E /* HTTPStubsResponse.h */, + ); + name = include; + path = Sources/OHHTTPStubs/include; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 8D8888AEB3E2ED0D5193812C19F5F414 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F38E73C08B6E033F51DAAA4CC0DBAD2D /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + BBDE3955B4BB65D5CC3FE4241F6F7194 /* Compatibility.h in Headers */, + BD490CD193473C6F388E0A5711974AF8 /* HTTPStubs.h in Headers */, + 2B81B3D6140D286219E09207A4A76CB2 /* HTTPStubsMethodSwizzling.h in Headers */, + A007DA77C67B5D7ABB0964C373B3047C /* HTTPStubsPathHelpers.h in Headers */, + 95511583081A96910E8C92E0778FFEE0 /* HTTPStubsResponse+JSON.h in Headers */, + 05D04D5BA99C63E9CE718806BFBE5378 /* HTTPStubsResponse.h in Headers */, + 8B47F893258916A6CABE7F44BA59B8BD /* NSURLRequest+HTTPBodyTesting.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + A983A2D06C5B6AA3D6ABA5CCC0A16725 /* OHHTTPStubs */ = { + isa = PBXNativeTarget; + buildConfigurationList = BBBE26D72775B28517E34991E51FABC8 /* Build configuration list for PBXNativeTarget "OHHTTPStubs" */; + buildPhases = ( + F38E73C08B6E033F51DAAA4CC0DBAD2D /* Headers */, + 638E52731691F7BD0AB00171E23524CB /* Sources */, + 68A287CF1529D3AF8B895CE99D3E0067 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = OHHTTPStubs; + productName = OHHTTPStubs; + productReference = 17E33041B314FA837A3CAEB9DF3CDE9F /* libOHHTTPStubs.a */; + productType = "com.apple.product-type.library.static"; + }; + E9FB9E199F29311FAE9B4F0FCD5CCF2D /* Pods-OHHTTPStubsDemo */ = { + isa = PBXNativeTarget; + buildConfigurationList = 928B9C5058C12CFA023DBC85F7EC22F2 /* Build configuration list for PBXNativeTarget "Pods-OHHTTPStubsDemo" */; + buildPhases = ( + 8D8888AEB3E2ED0D5193812C19F5F414 /* Headers */, + 8BB1DA7D9C8A9354F324733D10080894 /* Sources */, + F18A388836E9F5A2E85D0E54DB637C80 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + DEC168277EDE21B4E3B6F736D2694224 /* PBXTargetDependency */, + ); + name = "Pods-OHHTTPStubsDemo"; + productName = "Pods-OHHTTPStubsDemo"; + productReference = 5C6E57AA6EE3246CC7A70F569EC42B2C /* libPods-OHHTTPStubsDemo.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + BFDFE7DC352907FC980B868725387E98 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1100; + LastUpgradeCheck = 1100; + }; + buildConfigurationList = 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = CF1408CF629C7361332E53B88F7BD30C; + productRefGroup = 9DF898CB87CDDF6540022FCA0232A1E9 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A983A2D06C5B6AA3D6ABA5CCC0A16725 /* OHHTTPStubs */, + E9FB9E199F29311FAE9B4F0FCD5CCF2D /* Pods-OHHTTPStubsDemo */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 638E52731691F7BD0AB00171E23524CB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 39A047A944570A4A2B5C8E627DB12511 /* HTTPStubs+NSURLSessionConfiguration.m in Sources */, + 497A8DDB766AE6FE948AA248CCD1F434 /* HTTPStubs.m in Sources */, + C483FF83B9A4C871111EBB9F8F024D02 /* HTTPStubsMethodSwizzling.m in Sources */, + AF29B3A8319A3078570DB7348BDC96BC /* HTTPStubsPathHelpers.m in Sources */, + C070992EC73066BE6F90AFA1C3A8B834 /* HTTPStubsResponse+JSON.m in Sources */, + F4670514839A03F81092B93536410C55 /* HTTPStubsResponse.m in Sources */, + DF0EDBAFF5E4AC6599018383C2344FE6 /* NSURLRequest+HTTPBodyTesting.m in Sources */, + 6829EA42E7094A040A0FA1C64C653C4C /* OHHTTPStubs-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8BB1DA7D9C8A9354F324733D10080894 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 80FAF560A96DFB636A33D463A3417ECF /* Pods-OHHTTPStubsDemo-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + DEC168277EDE21B4E3B6F736D2694224 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = OHHTTPStubs; + target = A983A2D06C5B6AA3D6ABA5CCC0A16725 /* OHHTTPStubs */; + targetProxy = 0FB21B4EBE0C3FFC465E9F34ADFC7845 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 0AFCF23D8416DB360180C1A0A0D325A4 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 00EF0B55C6C0A1F37D57E9BA7C5855FE /* OHHTTPStubs.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + GCC_PREFIX_HEADER = "Target Support Files/OHHTTPStubs/OHHTTPStubs-prefix.pch"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PRIVATE_HEADERS_FOLDER_PATH = ""; + PRODUCT_MODULE_NAME = OHHTTPStubs; + PRODUCT_NAME = OHHTTPStubs; + PUBLIC_HEADERS_FOLDER_PATH = ""; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 5.1; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 0F2B12892D8667B6B8BB2E82430D303D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AAAAB884699C361A9CFDB6725B839D6C /* Pods-OHHTTPStubsDemo.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MACH_O_TYPE = staticlib; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 4BE66A09A74FD25164AAB3C2645B9B93 /* Release */ = { + isa = XCBuildConfiguration; + 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; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "POD_CONFIGURATION_RELEASE=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; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + SYMROOT = "${SRCROOT}/../build"; + }; + name = Release; + }; + 6E3D083C01B96E88EF16D9B7120B1996 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 00EF0B55C6C0A1F37D57E9BA7C5855FE /* OHHTTPStubs.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + GCC_PREFIX_HEADER = "Target Support Files/OHHTTPStubs/OHHTTPStubs-prefix.pch"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PRIVATE_HEADERS_FOLDER_PATH = ""; + PRODUCT_MODULE_NAME = OHHTTPStubs; + PRODUCT_NAME = OHHTTPStubs; + PUBLIC_HEADERS_FOLDER_PATH = ""; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 5.1; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 7EF7227D9B20A1D549000096ACCB23D7 /* Debug */ = { + isa = XCBuildConfiguration; + 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; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + 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 = ( + "POD_CONFIGURATION_DEBUG=1", + "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; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + SYMROOT = "${SRCROOT}/../build"; + }; + name = Debug; + }; + A61FD5AE0C13C8BDB6D5F0829E5FE27A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FC88D895104CA8ECC7CF0F5669A9A00B /* Pods-OHHTTPStubsDemo.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MACH_O_TYPE = staticlib; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7EF7227D9B20A1D549000096ACCB23D7 /* Debug */, + 4BE66A09A74FD25164AAB3C2645B9B93 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 928B9C5058C12CFA023DBC85F7EC22F2 /* Build configuration list for PBXNativeTarget "Pods-OHHTTPStubsDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0F2B12892D8667B6B8BB2E82430D303D /* Debug */, + A61FD5AE0C13C8BDB6D5F0829E5FE27A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + BBBE26D72775B28517E34991E51FABC8 /* Build configuration list for PBXNativeTarget "OHHTTPStubs" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0AFCF23D8416DB360180C1A0A0D325A4 /* Debug */, + 6E3D083C01B96E88EF16D9B7120B1996 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = BFDFE7DC352907FC980B868725387E98 /* Project object */; +} diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs-dummy.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs-dummy.m new file mode 100644 index 0000000000..4deafde22c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_OHHTTPStubs : NSObject +@end +@implementation PodsDummy_OHHTTPStubs +@end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs-prefix.pch b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs-prefix.pch new file mode 100644 index 0000000000..beb2a24418 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs-prefix.pch @@ -0,0 +1,12 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs.xcconfig b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs.xcconfig new file mode 100644 index 0000000000..384e655c68 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs.xcconfig @@ -0,0 +1,9 @@ +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/OHHTTPStubs +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Private/OHHTTPStubs" "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/OHHTTPStubs" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/../../.. +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-acknowledgements.markdown b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-acknowledgements.markdown new file mode 100644 index 0000000000..751297baaf --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-acknowledgements.markdown @@ -0,0 +1,15 @@ +# Acknowledgements +This application makes use of the following third party libraries: + +## OHHTTPStubs + +- MIT LICENSE - + +Copyright (c) 2012 Olivier Halligon + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Generated by CocoaPods - https://cocoapods.org diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-acknowledgements.plist b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-acknowledgements.plist new file mode 100644 index 0000000000..49d5a452ac --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-acknowledgements.plist @@ -0,0 +1,47 @@ + + + + + PreferenceSpecifiers + + + FooterText + This application makes use of the following third party libraries: + Title + Acknowledgements + Type + PSGroupSpecifier + + + FooterText + - MIT LICENSE - + +Copyright (c) 2012 Olivier Halligon + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + License + MIT + Title + OHHTTPStubs + Type + PSGroupSpecifier + + + FooterText + Generated by CocoaPods - https://cocoapods.org + Title + + Type + PSGroupSpecifier + + + StringsTable + Acknowledgements + Title + Acknowledgements + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-dummy.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-dummy.m new file mode 100644 index 0000000000..7dd9301f50 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Pods_OHHTTPStubsDemo : NSObject +@end +@implementation PodsDummy_Pods_OHHTTPStubsDemo +@end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-frameworks.sh b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-frameworks.sh new file mode 100755 index 0000000000..88dd537990 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-frameworks.sh @@ -0,0 +1,105 @@ +#!/bin/sh +set -e + +echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" +mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + +SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" + +# This protects against multiple targets copying the same framework dependency at the same time. The solution +# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html +RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") + +install_framework() +{ + if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then + local source="${BUILT_PRODUCTS_DIR}/$1" + elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then + local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" + elif [ -r "$1" ]; then + local source="$1" + fi + + local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + + if [ -L "${source}" ]; then + echo "Symlinked..." + source="$(readlink "${source}")" + fi + + # Use filter instead of exclude so missing patterns don't throw errors. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" + + local basename + basename="$(basename -s .framework "$1")" + binary="${destination}/${basename}.framework/${basename}" + if ! [ -r "$binary" ]; then + binary="${destination}/${basename}" + fi + + # Strip invalid architectures so "fat" simulator / device frameworks work on device + if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then + strip_invalid_archs "$binary" + fi + + # Resign the code if required by the build settings to avoid unstable apps + code_sign_if_enabled "${destination}/$(basename "$1")" + + # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. + if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then + local swift_runtime_libs + swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u && exit ${PIPESTATUS[0]}) + for lib in $swift_runtime_libs; do + echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" + rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" + code_sign_if_enabled "${destination}/${lib}" + done + fi +} + +# Copies the dSYM of a vendored framework +install_dsym() { + local source="$1" + if [ -r "$source" ]; then + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DWARF_DSYM_FOLDER_PATH}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DWARF_DSYM_FOLDER_PATH}" + fi +} + +# Signs a framework with the provided identity +code_sign_if_enabled() { + if [ -n "${EXPANDED_CODE_SIGN_IDENTITY}" -a "${CODE_SIGNING_REQUIRED}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then + # Use the current code_sign_identitiy + echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" + local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS} --preserve-metadata=identifier,entitlements '$1'" + + if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + code_sign_cmd="$code_sign_cmd &" + fi + echo "$code_sign_cmd" + eval "$code_sign_cmd" + fi +} + +# Strip invalid architectures +strip_invalid_archs() { + binary="$1" + # Get architectures for current file + archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | rev)" + stripped="" + for arch in $archs; do + if ! [[ "${ARCHS}" == *"$arch"* ]]; then + # Strip non-valid architectures in-place + lipo -remove "$arch" -output "$binary" "$binary" || exit 1 + stripped="$stripped $arch" + fi + done + if [[ "$stripped" ]]; then + echo "Stripped $binary of architectures:$stripped" + fi +} + +if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + wait +fi diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-resources.sh b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-resources.sh new file mode 100755 index 0000000000..a7df4405b6 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-resources.sh @@ -0,0 +1,106 @@ +#!/bin/sh +set -e + +mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" + +RESOURCES_TO_COPY=${PODS_ROOT}/resources-to-copy-${TARGETNAME}.txt +> "$RESOURCES_TO_COPY" + +XCASSET_FILES=() + +# This protects against multiple targets copying the same framework dependency at the same time. The solution +# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html +RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") + +case "${TARGETED_DEVICE_FAMILY}" in + 1,2) + TARGET_DEVICE_ARGS="--target-device ipad --target-device iphone" + ;; + 1) + TARGET_DEVICE_ARGS="--target-device iphone" + ;; + 2) + TARGET_DEVICE_ARGS="--target-device ipad" + ;; + 3) + TARGET_DEVICE_ARGS="--target-device tv" + ;; + 4) + TARGET_DEVICE_ARGS="--target-device watch" + ;; + *) + TARGET_DEVICE_ARGS="--target-device mac" + ;; +esac + +install_resource() +{ + if [[ "$1" = /* ]] ; then + RESOURCE_PATH="$1" + else + RESOURCE_PATH="${PODS_ROOT}/$1" + fi + if [[ ! -e "$RESOURCE_PATH" ]] ; then + cat << EOM +error: Resource "$RESOURCE_PATH" not found. Run 'pod install' to update the copy resources script. +EOM + exit 1 + fi + case $RESOURCE_PATH in + *.storyboard) + echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true + ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} + ;; + *.xib) + echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true + ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} + ;; + *.framework) + echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true + mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + ;; + *.xcdatamodel) + echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH"`.mom\"" || true + xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodel`.mom" + ;; + *.xcdatamodeld) + echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd\"" || true + xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd" + ;; + *.xcmappingmodel) + echo "xcrun mapc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm\"" || true + xcrun mapc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm" + ;; + *.xcassets) + ABSOLUTE_XCASSET_FILE="$RESOURCE_PATH" + XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE") + ;; + *) + echo "$RESOURCE_PATH" || true + echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY" + ;; + esac +} + +mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +if [[ "${ACTION}" == "install" ]] && [[ "${SKIP_INSTALL}" == "NO" ]]; then + mkdir -p "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" + rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +fi +rm -f "$RESOURCES_TO_COPY" + +if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "$XCASSET_FILES" ] +then + # Find all other xcassets (this unfortunately includes those of path pods and other targets). + OTHER_XCASSETS=$(find "$PWD" -iname "*.xcassets" -type d) + while read line; do + if [[ $line != "${PODS_ROOT}*" ]]; then + XCASSET_FILES+=("$line") + fi + done <<<"$OTHER_XCASSETS" + + printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +fi diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo.debug.xcconfig b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo.debug.xcconfig new file mode 100644 index 0000000000..6fc9bcbdb7 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo.debug.xcconfig @@ -0,0 +1,8 @@ +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/OHHTTPStubs" +LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/OHHTTPStubs" +OTHER_LDFLAGS = $(inherited) -ObjC -l"OHHTTPStubs" -framework "CFNetwork" -framework "Foundation" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo.release.xcconfig b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo.release.xcconfig new file mode 100644 index 0000000000..6fc9bcbdb7 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo.release.xcconfig @@ -0,0 +1,8 @@ +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/OHHTTPStubs" +LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/OHHTTPStubs" +OTHER_LDFLAGS = $(inherited) -ObjC -l"OHHTTPStubs" -framework "CFNetwork" -framework "Foundation" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Supporting Files/Default-568h@2x.png b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Supporting Files/Default-568h@2x.png new file mode 100644 index 0000000000..0891b7aabf Binary files /dev/null and b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Supporting Files/Default-568h@2x.png differ diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Supporting Files/OHHTTPStubsDemo-Info.plist b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Supporting Files/OHHTTPStubsDemo-Info.plist new file mode 100644 index 0000000000..25bfd55966 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Supporting Files/OHHTTPStubsDemo-Info.plist @@ -0,0 +1,40 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + ${PRODUCT_NAME} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + LSRequiresIPhoneOS + + NSMainNibFile + MainViewController + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Supporting Files/OHHTTPStubsDemo-Prefix.pch b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Supporting Files/OHHTTPStubsDemo-Prefix.pch new file mode 100644 index 0000000000..20ef719789 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Supporting Files/OHHTTPStubsDemo-Prefix.pch @@ -0,0 +1,14 @@ +// +// Prefix header for all source files of the 'OHHTTPStubsDemo' target in the 'OHHTTPStubsDemo' project +// + +#import + +#ifndef __IPHONE_3_0 +#warning "This project uses features only available in iOS SDK 3.0 and later." +#endif + +#ifdef __OBJC__ + #import + #import +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Supporting Files/main.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Supporting Files/main.m new file mode 100644 index 0000000000..478bfb3959 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/ObjC/Supporting Files/main.m @@ -0,0 +1,17 @@ +// +// main.m +// OHHTTPStubsDemo +// +// Created by Olivier Halligon on 11/08/12. +// Copyright (c) 2012 AliSoftware. All rights reserved. +// + +#import + + +int main(int argc, char *argv[]) +{ + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, nil); + } +} diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Stubs/stub.jpg b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Stubs/stub.jpg new file mode 100644 index 0000000000..c42a21cd74 Binary files /dev/null and b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Stubs/stub.jpg differ diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Stubs/stub.txt b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Stubs/stub.txt new file mode 100644 index 0000000000..e662f91b23 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Stubs/stub.txt @@ -0,0 +1,3 @@ +This is the text from your stub. + +Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. Ut wisis enim ad minim veniam, quis nostrud exerci tution ullam corper suscipit lobortis nisi ut aliquip ex ea commodo consequat. Duis te feugi facilisi. Duis autem dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit au gue duis dolore te feugat nulla facilisi. diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/AppDelegate.swift b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/AppDelegate.swift new file mode 100644 index 0000000000..275ca657af --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/AppDelegate.swift @@ -0,0 +1,28 @@ +// +// AppDelegate.swift +// OHHTTPStubsDemo +// +// Created by Olivier Halligon on 18/04/2015. +// Copyright (c) 2015 AliSoftware. All rights reserved. +// + +import UIKit + +#if swift(>=4) +#else +extension UIApplication { + typealias LaunchOptionsKey = UIApplicationLaunchOptionsKey +} +#endif + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } +} + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Main.storyboard b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Main.storyboard new file mode 100644 index 0000000000..77c5012c07 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Main.storyboard @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/MainViewController.swift b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/MainViewController.swift new file mode 100644 index 0000000000..fc083ad692 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/MainViewController.swift @@ -0,0 +1,133 @@ +// +// ViewController.swift +// OHHTTPStubsDemo +// +// Created by Olivier Halligon on 18/04/2015. +// Copyright (c) 2015 AliSoftware. All rights reserved. +// + +import UIKit +import OHHTTPStubs + +class MainViewController: UIViewController { + + //////////////////////////////////////////////////////////////////////////////// + // MARK: - Outlets + + @IBOutlet var delaySwitch: UISwitch! + @IBOutlet var textView: UITextView! + @IBOutlet var installTextStubSwitch: UISwitch! + @IBOutlet var imageView: UIImageView! + @IBOutlet var installImageStubSwitch: UISwitch! + + //////////////////////////////////////////////////////////////////////////////// + // MARK: - Init & Dealloc + + override func viewDidLoad() { + super.viewDidLoad() + + installTextStub(self.installTextStubSwitch) + installImageStub(self.installImageStubSwitch) + HTTPStubs.onStubActivation { (request: URLRequest, stub: HTTPStubsDescriptor, response: HTTPStubsResponse) in + print("[OHHTTPStubs] Request to \(request.url!) has been stubbed with \(String(describing: stub.name))") + } + } + + //////////////////////////////////////////////////////////////////////////////// + // MARK: - Global stubs activation + + @IBAction func toggleStubs(_ sender: UISwitch) { + HTTPStubs.setEnabled(sender.isOn) + self.delaySwitch.isEnabled = sender.isOn + self.installTextStubSwitch.isEnabled = sender.isOn + self.installImageStubSwitch.isEnabled = sender.isOn + + let state = sender.isOn ? "and enabled" : "but disabled" + print("Installed (\(state)) stubs: \(HTTPStubs.allStubs())") + } + + + + //////////////////////////////////////////////////////////////////////////////// + // MARK: - Text Download and Stub + + + @IBAction func downloadText(_ sender: UIButton) { + sender.isEnabled = false + self.textView.text = nil + + let urlString = "http://www.opensource.apple.com/source/Git/Git-26/src/git-htmldocs/git-commit.txt?txt" + let req = URLRequest(url: URL(string: urlString)!) + + NSURLConnection.sendAsynchronousRequest(req, queue: OperationQueue.main) { (_, data, _) in + sender.isEnabled = true + if let receivedData = data, let receivedText = NSString(data: receivedData, encoding: String.Encoding.ascii.rawValue) { + self.textView.text = receivedText as String + } + } + } + + weak var textStub: HTTPStubsDescriptor? + @IBAction func installTextStub(_ sender: UISwitch) { + if sender.isOn { + // Install + let stubPath = OHPathForFile("stub.txt", type(of: self)) + textStub = stub(condition: isExtension("txt")) { [weak self] _ in + let useDelay = DispatchQueue.main.sync { self?.delaySwitch.isOn ?? false } + return fixture(filePath: stubPath!, headers: ["Content-Type":"text/plain"]) + .requestTime(useDelay ? 2.0 : 0.0, responseTime:OHHTTPStubsDownloadSpeedWifi) + } + textStub?.name = "Text stub" + } else { + // Uninstall + HTTPStubs.removeStub(textStub!) + } + } + + + //////////////////////////////////////////////////////////////////////////////// + // MARK: - Image Download and Stub + + @IBAction func downloadImage(_ sender: UIButton) { + sender.isEnabled = false + self.imageView.image = nil + + let urlString = "http://images.apple.com/support/assets/images/products/iphone/hero_iphone4-5_wide.png" + let req = URLRequest(url: URL(string: urlString)!) + + NSURLConnection.sendAsynchronousRequest(req, queue: OperationQueue.main) { (_, data, _) in + sender.isEnabled = true + if let receivedData = data { + DispatchQueue.main.async { + self.imageView.image = UIImage(data: receivedData) + } + } + } + } + + weak var imageStub: HTTPStubsDescriptor? + @IBAction func installImageStub(_ sender: UISwitch) { + if sender.isOn { + // Install + let stubPath = OHPathForFile("stub.jpg", type(of: self)) + imageStub = stub(condition: isExtension("png") || isExtension("jpg") || isExtension("gif")) { [weak self] _ in + let useDelay = DispatchQueue.main.sync { self?.delaySwitch.isOn ?? false } + return fixture(filePath: stubPath!, headers: ["Content-Type":"image/jpeg"]) + .requestTime(useDelay ? 2.0 : 0.0, responseTime: OHHTTPStubsDownloadSpeedWifi) + } + imageStub?.name = "Image stub" + } else { + // Uninstall + HTTPStubs.removeStub(imageStub!) + } + } + + //////////////////////////////////////////////////////////////////////////////// + // MARK: - Cleaning + + @IBAction func clearResults() { + self.textView.text = "" + self.imageView.image = nil + } + +} diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/OHHTTPStubsDemo.xcodeproj/project.pbxproj b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/OHHTTPStubsDemo.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..d0c1d2c439 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/OHHTTPStubsDemo.xcodeproj/project.pbxproj @@ -0,0 +1,403 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 099F74141AE2D049001108A5 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 099F74131AE2D049001108A5 /* Images.xcassets */; }; + 099F742F1AE2D572001108A5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 099F742D1AE2D572001108A5 /* Main.storyboard */; }; + 099F74351AE2D5EB001108A5 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 099F74341AE2D5EB001108A5 /* LaunchScreen.xib */; }; + 099F743C1AE2D632001108A5 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 099F743B1AE2D632001108A5 /* Default-568h@2x.png */; }; + 099F74401AE2D703001108A5 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099F740E1AE2D049001108A5 /* MainViewController.swift */; }; + 099F74421AE2D7E0001108A5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099F74411AE2D7E0001108A5 /* AppDelegate.swift */; }; + 1F9ADC912300383700F87660 /* stub.txt in Resources */ = {isa = PBXBuildFile; fileRef = 1F9ADC8F2300383700F87660 /* stub.txt */; }; + 1F9ADC922300383700F87660 /* stub.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 1F9ADC902300383700F87660 /* stub.jpg */; }; + D03D1F718E78AB9B9E2B5FEB /* Pods_OHHTTPStubsDemo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B08422DAA265AAF936725B6E /* Pods_OHHTTPStubsDemo.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 099F74071AE2D049001108A5 /* OHHTTPStubsDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OHHTTPStubsDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 099F740B1AE2D049001108A5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 099F740E1AE2D049001108A5 /* MainViewController.swift */ = {isa = PBXFileReference; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; tabWidth = 4; }; + 099F74131AE2D049001108A5 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + 099F742D1AE2D572001108A5 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; + 099F74341AE2D5EB001108A5 /* LaunchScreen.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = LaunchScreen.xib; sourceTree = ""; }; + 099F743B1AE2D632001108A5 /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Default-568h@2x.png"; path = "Supporting Files/Default-568h@2x.png"; sourceTree = SOURCE_ROOT; }; + 099F74411AE2D7E0001108A5 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 0BFB1DB3496791F522727353 /* Pods-OHHTTPStubsDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OHHTTPStubsDemo.release.xcconfig"; path = "Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo.release.xcconfig"; sourceTree = ""; }; + 1F9ADC8F2300383700F87660 /* stub.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = stub.txt; sourceTree = ""; }; + 1F9ADC902300383700F87660 /* stub.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = stub.jpg; sourceTree = ""; }; + B08422DAA265AAF936725B6E /* Pods_OHHTTPStubsDemo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_OHHTTPStubsDemo.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F6C6334E088CD830EF8D38DB /* Pods.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + FE789C901264C7AF3BDF48CB /* Pods-OHHTTPStubsDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OHHTTPStubsDemo.debug.xcconfig"; path = "Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 099F74041AE2D049001108A5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D03D1F718E78AB9B9E2B5FEB /* Pods_OHHTTPStubsDemo.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 05EA12948915C7B39A9C3BDD /* Pods */ = { + isa = PBXGroup; + children = ( + FE789C901264C7AF3BDF48CB /* Pods-OHHTTPStubsDemo.debug.xcconfig */, + 0BFB1DB3496791F522727353 /* Pods-OHHTTPStubsDemo.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 099F73FE1AE2D049001108A5 = { + isa = PBXGroup; + children = ( + 099F74091AE2D049001108A5 /* OHHTTPStubsDemo */, + 099F74081AE2D049001108A5 /* Products */, + 3116686BFC89EEC0587776C7 /* Frameworks */, + 05EA12948915C7B39A9C3BDD /* Pods */, + ); + sourceTree = ""; + }; + 099F74081AE2D049001108A5 /* Products */ = { + isa = PBXGroup; + children = ( + 099F74071AE2D049001108A5 /* OHHTTPStubsDemo.app */, + ); + name = Products; + sourceTree = ""; + }; + 099F74091AE2D049001108A5 /* OHHTTPStubsDemo */ = { + isa = PBXGroup; + children = ( + 099F74411AE2D7E0001108A5 /* AppDelegate.swift */, + 099F740E1AE2D049001108A5 /* MainViewController.swift */, + 099F742D1AE2D572001108A5 /* Main.storyboard */, + 1F9ADC8E2300383700F87660 /* Stubs */, + 099F740A1AE2D049001108A5 /* Supporting Files */, + ); + name = OHHTTPStubsDemo; + sourceTree = ""; + }; + 099F740A1AE2D049001108A5 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 099F743B1AE2D632001108A5 /* Default-568h@2x.png */, + 099F74341AE2D5EB001108A5 /* LaunchScreen.xib */, + 099F74131AE2D049001108A5 /* Images.xcassets */, + 099F740B1AE2D049001108A5 /* Info.plist */, + ); + path = "Supporting Files"; + sourceTree = ""; + }; + 1F9ADC8E2300383700F87660 /* Stubs */ = { + isa = PBXGroup; + children = ( + 1F9ADC8F2300383700F87660 /* stub.txt */, + 1F9ADC902300383700F87660 /* stub.jpg */, + ); + name = Stubs; + path = ../Stubs; + sourceTree = ""; + }; + 3116686BFC89EEC0587776C7 /* Frameworks */ = { + isa = PBXGroup; + children = ( + F6C6334E088CD830EF8D38DB /* Pods.framework */, + B08422DAA265AAF936725B6E /* Pods_OHHTTPStubsDemo.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 099F74061AE2D049001108A5 /* OHHTTPStubsDemo */ = { + isa = PBXNativeTarget; + buildConfigurationList = 099F74261AE2D049001108A5 /* Build configuration list for PBXNativeTarget "OHHTTPStubsDemo" */; + buildPhases = ( + 39E0450680C8382CD8350B52 /* [CP] Check Pods Manifest.lock */, + 099F74031AE2D049001108A5 /* Sources */, + 099F74041AE2D049001108A5 /* Frameworks */, + 099F74051AE2D049001108A5 /* Resources */, + 9A5C20C1702511945337DFF2 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = OHHTTPStubsDemo; + productName = OHHTTPStubsDemo; + productReference = 099F74071AE2D049001108A5 /* OHHTTPStubsDemo.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 099F73FF1AE2D049001108A5 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0700; + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = AliSoftware; + TargetAttributes = { + 099F74061AE2D049001108A5 = { + CreatedOnToolsVersion = 6.3; + LastSwiftMigration = 1020; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 099F74021AE2D049001108A5 /* Build configuration list for PBXProject "OHHTTPStubsDemo" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 099F73FE1AE2D049001108A5; + productRefGroup = 099F74081AE2D049001108A5 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 099F74061AE2D049001108A5 /* OHHTTPStubsDemo */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 099F74051AE2D049001108A5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 099F74351AE2D5EB001108A5 /* LaunchScreen.xib in Resources */, + 1F9ADC922300383700F87660 /* stub.jpg in Resources */, + 099F742F1AE2D572001108A5 /* Main.storyboard in Resources */, + 099F74141AE2D049001108A5 /* Images.xcassets in Resources */, + 099F743C1AE2D632001108A5 /* Default-568h@2x.png in Resources */, + 1F9ADC912300383700F87660 /* stub.txt in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 39E0450680C8382CD8350B52 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-OHHTTPStubsDemo-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9A5C20C1702511945337DFF2 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/OHHTTPStubs/OHHTTPStubs.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OHHTTPStubs.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 099F74031AE2D049001108A5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 099F74421AE2D7E0001108A5 /* AppDelegate.swift in Sources */, + 099F74401AE2D703001108A5 /* MainViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 099F74241AE2D049001108A5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_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_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "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.3; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 099F74251AE2D049001108A5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_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_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "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.3; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 099F74271AE2D049001108A5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FE789C901264C7AF3BDF48CB /* Pods-OHHTTPStubsDemo.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = "$(SRCROOT)/Supporting Files/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.alisoftware.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 099F74281AE2D049001108A5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0BFB1DB3496791F522727353 /* Pods-OHHTTPStubsDemo.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + INFOPLIST_FILE = "$(SRCROOT)/Supporting Files/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.alisoftware.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 099F74021AE2D049001108A5 /* Build configuration list for PBXProject "OHHTTPStubsDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 099F74241AE2D049001108A5 /* Debug */, + 099F74251AE2D049001108A5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 099F74261AE2D049001108A5 /* Build configuration list for PBXNativeTarget "OHHTTPStubsDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 099F74271AE2D049001108A5 /* Debug */, + 099F74281AE2D049001108A5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 099F73FF1AE2D049001108A5 /* Project object */; +} diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/OHHTTPStubsDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/OHHTTPStubsDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/OHHTTPStubsDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/OHHTTPStubsDemo.xcodeproj/xcshareddata/xcschemes/OHHTTPStubsDemo.xcscheme b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/OHHTTPStubsDemo.xcodeproj/xcshareddata/xcschemes/OHHTTPStubsDemo.xcscheme new file mode 100644 index 0000000000..6f0dd7eeb3 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/OHHTTPStubsDemo.xcodeproj/xcshareddata/xcschemes/OHHTTPStubsDemo.xcscheme @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/OHHTTPStubsDemo.xcworkspace/contents.xcworkspacedata b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/OHHTTPStubsDemo.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..cfc0e73f1c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/OHHTTPStubsDemo.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/OHHTTPStubsDemo.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/OHHTTPStubsDemo.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000000..0c67376eba --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/OHHTTPStubsDemo.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Podfile b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Podfile new file mode 100644 index 0000000000..4ecf510684 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Podfile @@ -0,0 +1,10 @@ +source 'https://github.com/CocoaPods/Specs.git' + +project 'OHHTTPStubsDemo.xcodeproj' +platform :ios, '8.0' +use_frameworks! + +target 'OHHTTPStubsDemo' do + pod 'OHHTTPStubs', :path => '../..' + pod 'OHHTTPStubs/Swift', :path => '../..' +end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Podfile.lock b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Podfile.lock new file mode 100644 index 0000000000..5aeb24233a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Podfile.lock @@ -0,0 +1,31 @@ +PODS: + - OHHTTPStubs (9.0.0): + - OHHTTPStubs/Default (= 9.0.0) + - OHHTTPStubs/Core (9.0.0) + - OHHTTPStubs/Default (9.0.0): + - OHHTTPStubs/Core + - OHHTTPStubs/JSON + - OHHTTPStubs/NSURLSession + - OHHTTPStubs/OHPathHelpers + - OHHTTPStubs/JSON (9.0.0): + - OHHTTPStubs/Core + - OHHTTPStubs/NSURLSession (9.0.0): + - OHHTTPStubs/Core + - OHHTTPStubs/OHPathHelpers (9.0.0) + - OHHTTPStubs/Swift (9.0.0): + - OHHTTPStubs/Default + +DEPENDENCIES: + - OHHTTPStubs (from `../..`) + - OHHTTPStubs/Swift (from `../..`) + +EXTERNAL SOURCES: + OHHTTPStubs: + :path: "../.." + +SPEC CHECKSUMS: + OHHTTPStubs: cb29d2a9d09a828ecb93349a2b0c64f99e0db89f + +PODFILE CHECKSUM: 7da7c441ea9ff6f06b633c908b7a7294805f5602 + +COCOAPODS: 1.7.5 diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Local Podspecs/OHHTTPStubs.podspec.json b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Local Podspecs/OHHTTPStubs.podspec.json new file mode 100644 index 0000000000..389a736d39 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Local Podspecs/OHHTTPStubs.podspec.json @@ -0,0 +1,128 @@ +{ + "name": "OHHTTPStubs", + "version": "9.0.0", + "summary": "Framework to stub your network requests like HTTP and help you write network unit tests with XCTest.", + "description": "A class to stub network requests easily:\n\n * Test your apps with fake network data (stubbed from file)\n * You can also customize your response headers and status code\n * Use customized stubs depending on the requests\n * Use custom response time to simulate slow network.\n * This works with any request (HTTP, HTTPS, or any protocol) sent using\n the iOS URL Loading System (NSURLConnection, NSURLSession, AFNetworking, …)\n * This is really useful in unit testing, when you need to test network features\n but don't want to hit the real network and fake some response data instead.\n * Has useful convenience methods to stub JSON content or fixture from a file\n * Compatible with Swift", + "homepage": "https://github.com/AliSoftware/OHHTTPStubs", + "license": "MIT", + "authors": { + "Olivier Halligon": "olivier.halligon+ae@gmail.com" + }, + "source": { + "git": "https://github.com/AliSoftware/OHHTTPStubs.git", + "tag": "9.0.0" + }, + "frameworks": [ + "Foundation", + "CFNetwork" + ], + "requires_arc": true, + "platforms": { + "ios": "8.0", + "osx": "10.9", + "watchos": "2.0", + "tvos": "9.0" + }, + "swift_versions": [ + "3.0", + "3.1", + "3.2", + "4.0", + "4.1", + "4.2", + "5.0", + "5.1" + ], + "default_subspecs": "Default", + "subspecs": [ + { + "name": "Default", + "dependencies": { + "OHHTTPStubs/Core": [ + + ], + "OHHTTPStubs/NSURLSession": [ + + ], + "OHHTTPStubs/JSON": [ + + ], + "OHHTTPStubs/OHPathHelpers": [ + + ] + } + }, + { + "name": "Core", + "source_files": [ + "Sources/OHHTTPStubs/**/HTTPStubs.{h,m}", + "Sources/OHHTTPStubs/**/HTTPStubsResponse.{h,m}", + "Sources/OHHTTPStubs/include/Compatibility.h" + ] + }, + { + "name": "NSURLSession", + "dependencies": { + "OHHTTPStubs/Core": [ + + ] + }, + "source_files": [ + "Sources/OHHTTPStubs/**/NSURLRequest+HTTPBodyTesting.{h,m}", + "Sources/OHHTTPStubs/**/HTTPStubs+NSURLSessionConfiguration.{h,m}", + "Sources/OHHTTPStubs/**/HTTPStubsMethodSwizzling.{h,m}" + ], + "private_header_files": "Sources/OHHTTPStubs/**/HTTPStubsMethodSwizzling.h" + }, + { + "name": "JSON", + "dependencies": { + "OHHTTPStubs/Core": [ + + ] + }, + "source_files": "Sources/OHHTTPStubs/**/HTTPStubsResponse+JSON.{h,m}" + }, + { + "name": "HTTPMessage", + "dependencies": { + "OHHTTPStubs/Core": [ + + ] + }, + "source_files": "Sources/HTTPMessage/**/*.{h,m}" + }, + { + "name": "Mocktail", + "dependencies": { + "OHHTTPStubs/Core": [ + + ] + }, + "source_files": "Sources/Mocktail/**/*.{h,m}" + }, + { + "name": "OHPathHelpers", + "source_files": [ + "Sources/OHHTTPStubs/**/HTTPStubsPathHelpers.{h,m}", + "Sources/OHHTTPStubs/include/Compatibility.h" + ] + }, + { + "name": "Swift", + "platforms": { + "ios": "8.0", + "osx": "10.9", + "watchos": "2.0", + "tvos": "9.0" + }, + "dependencies": { + "OHHTTPStubs/Default": [ + + ] + }, + "source_files": "Sources/OHHTTPStubsSwift/*.swift" + } + ], + "swift_version": "5.1" +} diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Manifest.lock b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Manifest.lock new file mode 100644 index 0000000000..5aeb24233a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Manifest.lock @@ -0,0 +1,31 @@ +PODS: + - OHHTTPStubs (9.0.0): + - OHHTTPStubs/Default (= 9.0.0) + - OHHTTPStubs/Core (9.0.0) + - OHHTTPStubs/Default (9.0.0): + - OHHTTPStubs/Core + - OHHTTPStubs/JSON + - OHHTTPStubs/NSURLSession + - OHHTTPStubs/OHPathHelpers + - OHHTTPStubs/JSON (9.0.0): + - OHHTTPStubs/Core + - OHHTTPStubs/NSURLSession (9.0.0): + - OHHTTPStubs/Core + - OHHTTPStubs/OHPathHelpers (9.0.0) + - OHHTTPStubs/Swift (9.0.0): + - OHHTTPStubs/Default + +DEPENDENCIES: + - OHHTTPStubs (from `../..`) + - OHHTTPStubs/Swift (from `../..`) + +EXTERNAL SOURCES: + OHHTTPStubs: + :path: "../.." + +SPEC CHECKSUMS: + OHHTTPStubs: cb29d2a9d09a828ecb93349a2b0c64f99e0db89f + +PODFILE CHECKSUM: 7da7c441ea9ff6f06b633c908b7a7294805f5602 + +COCOAPODS: 1.7.5 diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Pods.xcodeproj/project.pbxproj b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Pods.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..4b2cb7ef5e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Pods.xcodeproj/project.pbxproj @@ -0,0 +1,735 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 0CB065DB113F30C55F60A1C586659954 /* NSURLRequest+HTTPBodyTesting.h in Headers */ = {isa = PBXBuildFile; fileRef = 93DCF21B4A5ED607D572B87FE52B4090 /* NSURLRequest+HTTPBodyTesting.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 157CB49BDF6F5DF4B1CC9AA170BD9236 /* OHHTTPStubs-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 880715B4A0CB1AA40DF941409257BAF1 /* OHHTTPStubs-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 17C6F79FBB5A76BFC28143BCD606E4A1 /* HTTPStubsMethodSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = 8A5F4E3CA974E831B4ACBD3C4506B1BC /* HTTPStubsMethodSwizzling.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 19C57194223F9047F42D6B322C3FF454 /* HTTPStubsResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 7606431D5AFDD34541E6ED3939E91B14 /* HTTPStubsResponse.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1F7678DA34C83E57229D405CF3C011CB /* HTTPStubsResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = ADDA0A78242FB5B13AEA87E9D2198F61 /* HTTPStubsResponse.m */; }; + 419EB0ADFF20BE415A2737274DE8236E /* HTTPStubs.m in Sources */ = {isa = PBXBuildFile; fileRef = 4DFC45294DB8F681453EC370426EEB92 /* HTTPStubs.m */; }; + 4ABA69CCF7AFD713AD27EB643D7EFFE8 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 222DAEF0D819F359AFCFF3D4F927E8A7 /* Foundation.framework */; }; + 6DD158884D33F5983347A5FBF75EC622 /* Pods-OHHTTPStubsDemo-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 924DD4D962E1801D1FFBF9D026B8C697 /* Pods-OHHTTPStubsDemo-dummy.m */; }; + 7F14B8F810A18A138A3A249851817548 /* HTTPStubs+NSURLSessionConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = CEC13F28FF8DB2C840134CA4C94D7EB9 /* HTTPStubs+NSURLSessionConfiguration.m */; }; + 81D983B9013D8525DC5858313298D992 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 222DAEF0D819F359AFCFF3D4F927E8A7 /* Foundation.framework */; }; + 98A70B05D1076F61D881ABF8CFF5C16B /* OHHTTPStubs-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 3904BA681582270A5E69E60D627EC112 /* OHHTTPStubs-dummy.m */; }; + 9CCDC9982E9B8E661CF1057CA004C9E6 /* HTTPStubs.h in Headers */ = {isa = PBXBuildFile; fileRef = 49B4ACC997299576049CCBE881CA1F5D /* HTTPStubs.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B7BE2F5532955EFFDD8946AB41B665A9 /* HTTPStubsPathHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = F914D8C81C8B45F4ACAE8E3F3580DEC3 /* HTTPStubsPathHelpers.m */; }; + B80CF687151B2435A1FBE8E2E8A116C7 /* NSURLRequest+HTTPBodyTesting.m in Sources */ = {isa = PBXBuildFile; fileRef = 24B382A2198AC342A03F34B6AED41227 /* NSURLRequest+HTTPBodyTesting.m */; }; + BD69383CF1086726703E4D2FBC759D48 /* Pods-OHHTTPStubsDemo-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = C0C01BE9AB785821028336E312F394AB /* Pods-OHHTTPStubsDemo-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C43E94D141AE76AC23B35500B38A20C0 /* OHHTTPStubsSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = E49A6F402044BE41BEC11B4AC7475C79 /* OHHTTPStubsSwift.swift */; }; + C994EE7DE4EBC4A527A7739988F9D492 /* HTTPStubsPathHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 1CE3DB0A3E605F22F84DDB5333EC471C /* HTTPStubsPathHelpers.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D1F71F0E272F344869FE4CF98D3C9DCB /* Compatibility.h in Headers */ = {isa = PBXBuildFile; fileRef = 10FFD35B300BBD8AFDE278677BDA794E /* Compatibility.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DAC5FAA6F3F4A173C1581C291380FF7A /* HTTPStubsResponse+JSON.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FAF3D59F04DC28E09ADC11CF61197FE /* HTTPStubsResponse+JSON.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E6FB3E5C9B3B8C3FC24616F2D0CC0184 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B079F294DA5D9A879E6D53A91F51A1E /* CFNetwork.framework */; }; + EBEBA91F7D037EDEB2E6A9B314D2D360 /* HTTPStubsResponse+JSON.m in Sources */ = {isa = PBXBuildFile; fileRef = 9642BEA3C02270B37B476E58D1B078EF /* HTTPStubsResponse+JSON.m */; }; + F85473D9E5C158F60A76CB86D325BC9D /* HTTPStubsMethodSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CEE86A0A06FC4D56F8DA9B7498CD211 /* HTTPStubsMethodSwizzling.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 0D11DC70C317006E242A827AA8A3F877 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = A983A2D06C5B6AA3D6ABA5CCC0A16725; + remoteInfo = OHHTTPStubs; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 062EF4B011CA7A84E66720EB968E9D3A /* OHHTTPStubs.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = OHHTTPStubs.xcconfig; sourceTree = ""; }; + 08BE7E158236FE3C15AE19B8E44CF4B7 /* Pods-OHHTTPStubsDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-OHHTTPStubsDemo.debug.xcconfig"; sourceTree = ""; }; + 10FFD35B300BBD8AFDE278677BDA794E /* Compatibility.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = Compatibility.h; sourceTree = ""; }; + 119AA68774C02221091759AB06A4D056 /* OHHTTPStubs.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; path = OHHTTPStubs.podspec; sourceTree = ""; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; + 17E33041B314FA837A3CAEB9DF3CDE9F /* OHHTTPStubs.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = OHHTTPStubs.framework; path = OHHTTPStubs.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1BD3E4B450E657E9884F0266FFC6279E /* Pods-OHHTTPStubsDemo-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-OHHTTPStubsDemo-Info.plist"; sourceTree = ""; }; + 1CE3DB0A3E605F22F84DDB5333EC471C /* HTTPStubsPathHelpers.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = HTTPStubsPathHelpers.h; sourceTree = ""; }; + 1FAF3D59F04DC28E09ADC11CF61197FE /* HTTPStubsResponse+JSON.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "HTTPStubsResponse+JSON.h"; sourceTree = ""; }; + 222DAEF0D819F359AFCFF3D4F927E8A7 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; + 24B382A2198AC342A03F34B6AED41227 /* NSURLRequest+HTTPBodyTesting.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSURLRequest+HTTPBodyTesting.m"; path = "Sources/OHHTTPStubs/NSURLRequest+HTTPBodyTesting.m"; sourceTree = ""; }; + 3904BA681582270A5E69E60D627EC112 /* OHHTTPStubs-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "OHHTTPStubs-dummy.m"; sourceTree = ""; }; + 49B4ACC997299576049CCBE881CA1F5D /* HTTPStubs.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = HTTPStubs.h; sourceTree = ""; }; + 4DFC45294DB8F681453EC370426EEB92 /* HTTPStubs.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = HTTPStubs.m; path = Sources/OHHTTPStubs/HTTPStubs.m; sourceTree = ""; }; + 5BA1BE194252930C988E0706D792AC98 /* Pods-OHHTTPStubsDemo.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-OHHTTPStubsDemo.modulemap"; sourceTree = ""; }; + 5C6E57AA6EE3246CC7A70F569EC42B2C /* Pods_OHHTTPStubsDemo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Pods_OHHTTPStubsDemo.framework; path = "Pods-OHHTTPStubsDemo.framework"; sourceTree = BUILT_PRODUCTS_DIR; }; + 5CEE86A0A06FC4D56F8DA9B7498CD211 /* HTTPStubsMethodSwizzling.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = HTTPStubsMethodSwizzling.m; path = Sources/OHHTTPStubs/HTTPStubsMethodSwizzling.m; sourceTree = ""; }; + 6712045ADFC585722A5E8F930D456D41 /* OHHTTPStubs-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "OHHTTPStubs-prefix.pch"; sourceTree = ""; }; + 75CB66EEEEEC51EBFE68B3C446443E12 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; path = README.md; sourceTree = ""; }; + 7606431D5AFDD34541E6ED3939E91B14 /* HTTPStubsResponse.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = HTTPStubsResponse.h; sourceTree = ""; }; + 8356BA17AE0F94E9BC19D3A03DB3CA0C /* Pods-OHHTTPStubsDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-OHHTTPStubsDemo.release.xcconfig"; sourceTree = ""; }; + 880715B4A0CB1AA40DF941409257BAF1 /* OHHTTPStubs-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "OHHTTPStubs-umbrella.h"; sourceTree = ""; }; + 8A5F4E3CA974E831B4ACBD3C4506B1BC /* HTTPStubsMethodSwizzling.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = HTTPStubsMethodSwizzling.h; path = Sources/OHHTTPStubs/HTTPStubsMethodSwizzling.h; sourceTree = ""; }; + 8B079F294DA5D9A879E6D53A91F51A1E /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/CFNetwork.framework; sourceTree = DEVELOPER_DIR; }; + 924DD4D962E1801D1FFBF9D026B8C697 /* Pods-OHHTTPStubsDemo-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-OHHTTPStubsDemo-dummy.m"; sourceTree = ""; }; + 93DCF21B4A5ED607D572B87FE52B4090 /* NSURLRequest+HTTPBodyTesting.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "NSURLRequest+HTTPBodyTesting.h"; sourceTree = ""; }; + 9642BEA3C02270B37B476E58D1B078EF /* HTTPStubsResponse+JSON.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "HTTPStubsResponse+JSON.m"; path = "Sources/OHHTTPStubs/HTTPStubsResponse+JSON.m"; sourceTree = ""; }; + 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; + ADDA0A78242FB5B13AEA87E9D2198F61 /* HTTPStubsResponse.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = HTTPStubsResponse.m; path = Sources/OHHTTPStubs/HTTPStubsResponse.m; sourceTree = ""; }; + C0C01BE9AB785821028336E312F394AB /* Pods-OHHTTPStubsDemo-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-OHHTTPStubsDemo-umbrella.h"; sourceTree = ""; }; + C12A98E9252AFE45F8F4414CAF8E8E0B /* Pods-OHHTTPStubsDemo-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-OHHTTPStubsDemo-frameworks.sh"; sourceTree = ""; }; + C60F0F70FB383F89626D847826107AE4 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; path = LICENSE; sourceTree = ""; }; + CCFCF8A8C411D7A1D41B7C5D487FD196 /* Pods-OHHTTPStubsDemo-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-OHHTTPStubsDemo-acknowledgements.plist"; sourceTree = ""; }; + CEC13F28FF8DB2C840134CA4C94D7EB9 /* HTTPStubs+NSURLSessionConfiguration.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "HTTPStubs+NSURLSessionConfiguration.m"; path = "Sources/OHHTTPStubs/HTTPStubs+NSURLSessionConfiguration.m"; sourceTree = ""; }; + DFCA37746C8D877F0C4459410AB256DA /* Pods-OHHTTPStubsDemo-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-OHHTTPStubsDemo-acknowledgements.markdown"; sourceTree = ""; }; + E49A6F402044BE41BEC11B4AC7475C79 /* OHHTTPStubsSwift.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OHHTTPStubsSwift.swift; path = Sources/OHHTTPStubsSwift/OHHTTPStubsSwift.swift; sourceTree = ""; }; + E717A5F8FE2119023611FDA87321C671 /* OHHTTPStubs-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "OHHTTPStubs-Info.plist"; sourceTree = ""; }; + F914D8C81C8B45F4ACAE8E3F3580DEC3 /* HTTPStubsPathHelpers.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = HTTPStubsPathHelpers.m; path = Sources/OHHTTPStubs/HTTPStubsPathHelpers.m; sourceTree = ""; }; + FDBC66D26DF2DD7BF8AB5FD89A8AED93 /* OHHTTPStubs.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = OHHTTPStubs.modulemap; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 34B911BFD422CFC2E838428D11EE7530 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4ABA69CCF7AFD713AD27EB643D7EFFE8 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5CE63E52033B9EB192CC73B2CB7700E8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E6FB3E5C9B3B8C3FC24616F2D0CC0184 /* CFNetwork.framework in Frameworks */, + 81D983B9013D8525DC5858313298D992 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 16241EF7FD32BD1D9842A738CD81D55D /* OHPathHelpers */ = { + isa = PBXGroup; + children = ( + F914D8C81C8B45F4ACAE8E3F3580DEC3 /* HTTPStubsPathHelpers.m */, + DAD799EB9BAF9F08FBA42DA29511200F /* include */, + ); + name = OHPathHelpers; + sourceTree = ""; + }; + 1628BF05B4CAFDCC3549A101F5A10A17 /* Frameworks */ = { + isa = PBXGroup; + children = ( + E34DCC8E2CF86B8D72232914781A840D /* iOS */, + ); + name = Frameworks; + sourceTree = ""; + }; + 3B302F49B731AEC4F859D13AB2A344CA /* OHHTTPStubs */ = { + isa = PBXGroup; + children = ( + 7E8D2DBB815BAD4ADDF28F5F14ECEDE5 /* Core */, + 6267FE0A3447F9085A09C92B950BB71D /* JSON */, + C23B8F990C904193C5FE177DDA443335 /* NSURLSession */, + 16241EF7FD32BD1D9842A738CD81D55D /* OHPathHelpers */, + 556325C860710153DCF6D0244E8311F7 /* Pod */, + 9598F1891B407C01FE62B4CFB7E5AAB5 /* Support Files */, + CDC60EF38E4810CE47DA4D48076FCEB1 /* Swift */, + ); + name = OHHTTPStubs; + path = ../../..; + sourceTree = ""; + }; + 556325C860710153DCF6D0244E8311F7 /* Pod */ = { + isa = PBXGroup; + children = ( + C60F0F70FB383F89626D847826107AE4 /* LICENSE */, + 119AA68774C02221091759AB06A4D056 /* OHHTTPStubs.podspec */, + 75CB66EEEEEC51EBFE68B3C446443E12 /* README.md */, + ); + name = Pod; + sourceTree = ""; + }; + 6267FE0A3447F9085A09C92B950BB71D /* JSON */ = { + isa = PBXGroup; + children = ( + 9642BEA3C02270B37B476E58D1B078EF /* HTTPStubsResponse+JSON.m */, + 8CFBDAE23FC82D7EDA9ACDD133AE07AA /* include */, + ); + name = JSON; + sourceTree = ""; + }; + 636DDD4F63585C52B5EED562AC1396FC /* Targets Support Files */ = { + isa = PBXGroup; + children = ( + A57ABF541A1026714332DF80A25ECCBA /* Pods-OHHTTPStubsDemo */, + ); + name = "Targets Support Files"; + sourceTree = ""; + }; + 7E8D2DBB815BAD4ADDF28F5F14ECEDE5 /* Core */ = { + isa = PBXGroup; + children = ( + 4DFC45294DB8F681453EC370426EEB92 /* HTTPStubs.m */, + ADDA0A78242FB5B13AEA87E9D2198F61 /* HTTPStubsResponse.m */, + B4EA30E2042CF0343C9E5A22FF7F85A6 /* include */, + ); + name = Core; + sourceTree = ""; + }; + 8CFBDAE23FC82D7EDA9ACDD133AE07AA /* include */ = { + isa = PBXGroup; + children = ( + 1FAF3D59F04DC28E09ADC11CF61197FE /* HTTPStubsResponse+JSON.h */, + ); + name = include; + path = Sources/OHHTTPStubs/include; + sourceTree = ""; + }; + 9598F1891B407C01FE62B4CFB7E5AAB5 /* Support Files */ = { + isa = PBXGroup; + children = ( + FDBC66D26DF2DD7BF8AB5FD89A8AED93 /* OHHTTPStubs.modulemap */, + 062EF4B011CA7A84E66720EB968E9D3A /* OHHTTPStubs.xcconfig */, + 3904BA681582270A5E69E60D627EC112 /* OHHTTPStubs-dummy.m */, + E717A5F8FE2119023611FDA87321C671 /* OHHTTPStubs-Info.plist */, + 6712045ADFC585722A5E8F930D456D41 /* OHHTTPStubs-prefix.pch */, + 880715B4A0CB1AA40DF941409257BAF1 /* OHHTTPStubs-umbrella.h */, + ); + name = "Support Files"; + path = "Examples/Swift/Pods/Target Support Files/OHHTTPStubs"; + sourceTree = ""; + }; + A57ABF541A1026714332DF80A25ECCBA /* Pods-OHHTTPStubsDemo */ = { + isa = PBXGroup; + children = ( + 5BA1BE194252930C988E0706D792AC98 /* Pods-OHHTTPStubsDemo.modulemap */, + DFCA37746C8D877F0C4459410AB256DA /* Pods-OHHTTPStubsDemo-acknowledgements.markdown */, + CCFCF8A8C411D7A1D41B7C5D487FD196 /* Pods-OHHTTPStubsDemo-acknowledgements.plist */, + 924DD4D962E1801D1FFBF9D026B8C697 /* Pods-OHHTTPStubsDemo-dummy.m */, + C12A98E9252AFE45F8F4414CAF8E8E0B /* Pods-OHHTTPStubsDemo-frameworks.sh */, + 1BD3E4B450E657E9884F0266FFC6279E /* Pods-OHHTTPStubsDemo-Info.plist */, + C0C01BE9AB785821028336E312F394AB /* Pods-OHHTTPStubsDemo-umbrella.h */, + 08BE7E158236FE3C15AE19B8E44CF4B7 /* Pods-OHHTTPStubsDemo.debug.xcconfig */, + 8356BA17AE0F94E9BC19D3A03DB3CA0C /* Pods-OHHTTPStubsDemo.release.xcconfig */, + ); + name = "Pods-OHHTTPStubsDemo"; + path = "Target Support Files/Pods-OHHTTPStubsDemo"; + sourceTree = ""; + }; + A7398268C48C20FE751AF64C8CE7F615 /* include */ = { + isa = PBXGroup; + children = ( + 93DCF21B4A5ED607D572B87FE52B4090 /* NSURLRequest+HTTPBodyTesting.h */, + ); + name = include; + path = Sources/OHHTTPStubs/include; + sourceTree = ""; + }; + B4EA30E2042CF0343C9E5A22FF7F85A6 /* include */ = { + isa = PBXGroup; + children = ( + 10FFD35B300BBD8AFDE278677BDA794E /* Compatibility.h */, + 49B4ACC997299576049CCBE881CA1F5D /* HTTPStubs.h */, + 7606431D5AFDD34541E6ED3939E91B14 /* HTTPStubsResponse.h */, + ); + name = include; + path = Sources/OHHTTPStubs/include; + sourceTree = ""; + }; + C23B8F990C904193C5FE177DDA443335 /* NSURLSession */ = { + isa = PBXGroup; + children = ( + CEC13F28FF8DB2C840134CA4C94D7EB9 /* HTTPStubs+NSURLSessionConfiguration.m */, + 8A5F4E3CA974E831B4ACBD3C4506B1BC /* HTTPStubsMethodSwizzling.h */, + 5CEE86A0A06FC4D56F8DA9B7498CD211 /* HTTPStubsMethodSwizzling.m */, + 24B382A2198AC342A03F34B6AED41227 /* NSURLRequest+HTTPBodyTesting.m */, + A7398268C48C20FE751AF64C8CE7F615 /* include */, + ); + name = NSURLSession; + sourceTree = ""; + }; + CDC60EF38E4810CE47DA4D48076FCEB1 /* Swift */ = { + isa = PBXGroup; + children = ( + E49A6F402044BE41BEC11B4AC7475C79 /* OHHTTPStubsSwift.swift */, + ); + name = Swift; + sourceTree = ""; + }; + CF1408CF629C7361332E53B88F7BD30C = { + isa = PBXGroup; + children = ( + 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */, + FA42DA98722882DDA17A3352880F942A /* Development Pods */, + 1628BF05B4CAFDCC3549A101F5A10A17 /* Frameworks */, + CF904FFC24486461099DABAE05216816 /* Products */, + 636DDD4F63585C52B5EED562AC1396FC /* Targets Support Files */, + ); + sourceTree = ""; + }; + CF904FFC24486461099DABAE05216816 /* Products */ = { + isa = PBXGroup; + children = ( + 17E33041B314FA837A3CAEB9DF3CDE9F /* OHHTTPStubs.framework */, + 5C6E57AA6EE3246CC7A70F569EC42B2C /* Pods_OHHTTPStubsDemo.framework */, + ); + name = Products; + sourceTree = ""; + }; + DAD799EB9BAF9F08FBA42DA29511200F /* include */ = { + isa = PBXGroup; + children = ( + 1CE3DB0A3E605F22F84DDB5333EC471C /* HTTPStubsPathHelpers.h */, + ); + name = include; + path = Sources/OHHTTPStubs/include; + sourceTree = ""; + }; + E34DCC8E2CF86B8D72232914781A840D /* iOS */ = { + isa = PBXGroup; + children = ( + 8B079F294DA5D9A879E6D53A91F51A1E /* CFNetwork.framework */, + 222DAEF0D819F359AFCFF3D4F927E8A7 /* Foundation.framework */, + ); + name = iOS; + sourceTree = ""; + }; + FA42DA98722882DDA17A3352880F942A /* Development Pods */ = { + isa = PBXGroup; + children = ( + 3B302F49B731AEC4F859D13AB2A344CA /* OHHTTPStubs */, + ); + name = "Development Pods"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + A41B2117E2D5C884D5F69CD3B3C317E8 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + BD69383CF1086726703E4D2FBC759D48 /* Pods-OHHTTPStubsDemo-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B37B7B547A7889EF36A0B35136B261AD /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D1F71F0E272F344869FE4CF98D3C9DCB /* Compatibility.h in Headers */, + 9CCDC9982E9B8E661CF1057CA004C9E6 /* HTTPStubs.h in Headers */, + 17C6F79FBB5A76BFC28143BCD606E4A1 /* HTTPStubsMethodSwizzling.h in Headers */, + C994EE7DE4EBC4A527A7739988F9D492 /* HTTPStubsPathHelpers.h in Headers */, + DAC5FAA6F3F4A173C1581C291380FF7A /* HTTPStubsResponse+JSON.h in Headers */, + 19C57194223F9047F42D6B322C3FF454 /* HTTPStubsResponse.h in Headers */, + 0CB065DB113F30C55F60A1C586659954 /* NSURLRequest+HTTPBodyTesting.h in Headers */, + 157CB49BDF6F5DF4B1CC9AA170BD9236 /* OHHTTPStubs-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + A983A2D06C5B6AA3D6ABA5CCC0A16725 /* OHHTTPStubs */ = { + isa = PBXNativeTarget; + buildConfigurationList = 922F19B1A739BBFD2F4284423D72D365 /* Build configuration list for PBXNativeTarget "OHHTTPStubs" */; + buildPhases = ( + B37B7B547A7889EF36A0B35136B261AD /* Headers */, + 587E048EB99239D0DAB50119EE4E9BA7 /* Sources */, + 5CE63E52033B9EB192CC73B2CB7700E8 /* Frameworks */, + 0FC690F64DA2FCEC04C4AD1BEC983511 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = OHHTTPStubs; + productName = OHHTTPStubs; + productReference = 17E33041B314FA837A3CAEB9DF3CDE9F /* OHHTTPStubs.framework */; + productType = "com.apple.product-type.framework"; + }; + E9FB9E199F29311FAE9B4F0FCD5CCF2D /* Pods-OHHTTPStubsDemo */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1C6B5E1F46C0AE18738BA7F6AB8ED81E /* Build configuration list for PBXNativeTarget "Pods-OHHTTPStubsDemo" */; + buildPhases = ( + A41B2117E2D5C884D5F69CD3B3C317E8 /* Headers */, + 258B84F8D21023219B56B22406BC86F9 /* Sources */, + 34B911BFD422CFC2E838428D11EE7530 /* Frameworks */, + 433CA4845055A32C5E39175181F00750 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + E5EFDBD55E53E9A9255B67A4E5D81134 /* PBXTargetDependency */, + ); + name = "Pods-OHHTTPStubsDemo"; + productName = "Pods-OHHTTPStubsDemo"; + productReference = 5C6E57AA6EE3246CC7A70F569EC42B2C /* Pods_OHHTTPStubsDemo.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + BFDFE7DC352907FC980B868725387E98 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1100; + LastUpgradeCheck = 1100; + }; + buildConfigurationList = 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = CF1408CF629C7361332E53B88F7BD30C; + productRefGroup = CF904FFC24486461099DABAE05216816 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A983A2D06C5B6AA3D6ABA5CCC0A16725 /* OHHTTPStubs */, + E9FB9E199F29311FAE9B4F0FCD5CCF2D /* Pods-OHHTTPStubsDemo */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 0FC690F64DA2FCEC04C4AD1BEC983511 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 433CA4845055A32C5E39175181F00750 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 258B84F8D21023219B56B22406BC86F9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6DD158884D33F5983347A5FBF75EC622 /* Pods-OHHTTPStubsDemo-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 587E048EB99239D0DAB50119EE4E9BA7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7F14B8F810A18A138A3A249851817548 /* HTTPStubs+NSURLSessionConfiguration.m in Sources */, + 419EB0ADFF20BE415A2737274DE8236E /* HTTPStubs.m in Sources */, + F85473D9E5C158F60A76CB86D325BC9D /* HTTPStubsMethodSwizzling.m in Sources */, + B7BE2F5532955EFFDD8946AB41B665A9 /* HTTPStubsPathHelpers.m in Sources */, + EBEBA91F7D037EDEB2E6A9B314D2D360 /* HTTPStubsResponse+JSON.m in Sources */, + 1F7678DA34C83E57229D405CF3C011CB /* HTTPStubsResponse.m in Sources */, + B80CF687151B2435A1FBE8E2E8A116C7 /* NSURLRequest+HTTPBodyTesting.m in Sources */, + 98A70B05D1076F61D881ABF8CFF5C16B /* OHHTTPStubs-dummy.m in Sources */, + C43E94D141AE76AC23B35500B38A20C0 /* OHHTTPStubsSwift.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + E5EFDBD55E53E9A9255B67A4E5D81134 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = OHHTTPStubs; + target = A983A2D06C5B6AA3D6ABA5CCC0A16725 /* OHHTTPStubs */; + targetProxy = 0D11DC70C317006E242A827AA8A3F877 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 4BE66A09A74FD25164AAB3C2645B9B93 /* Release */ = { + isa = XCBuildConfiguration; + 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; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "POD_CONFIGURATION_RELEASE=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; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + SYMROOT = "${SRCROOT}/../build"; + }; + name = Release; + }; + 6B126C3891804F64C29A6D45AEEA9454 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 08BE7E158236FE3C15AE19B8E44CF4B7 /* Pods-OHHTTPStubsDemo.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 7EF7227D9B20A1D549000096ACCB23D7 /* Debug */ = { + isa = XCBuildConfiguration; + 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; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + 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 = ( + "POD_CONFIGURATION_DEBUG=1", + "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; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + SYMROOT = "${SRCROOT}/../build"; + }; + name = Debug; + }; + 8AD107CAB076327F3C14469334222604 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8356BA17AE0F94E9BC19D3A03DB3CA0C /* Pods-OHHTTPStubsDemo.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + EBF9EC8C7FCCF763DFC8A783B01F28B5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 062EF4B011CA7A84E66720EB968E9D3A /* OHHTTPStubs.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREFIX_HEADER = "Target Support Files/OHHTTPStubs/OHHTTPStubs-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/OHHTTPStubs/OHHTTPStubs-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MODULEMAP_FILE = "Target Support Files/OHHTTPStubs/OHHTTPStubs.modulemap"; + PRODUCT_MODULE_NAME = OHHTTPStubs; + PRODUCT_NAME = OHHTTPStubs; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 5.1; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + F8068B0476263D7E198F7BC21FD7E2E8 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 062EF4B011CA7A84E66720EB968E9D3A /* OHHTTPStubs.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREFIX_HEADER = "Target Support Files/OHHTTPStubs/OHHTTPStubs-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/OHHTTPStubs/OHHTTPStubs-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MODULEMAP_FILE = "Target Support Files/OHHTTPStubs/OHHTTPStubs.modulemap"; + PRODUCT_MODULE_NAME = OHHTTPStubs; + PRODUCT_NAME = OHHTTPStubs; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 5.1; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1C6B5E1F46C0AE18738BA7F6AB8ED81E /* Build configuration list for PBXNativeTarget "Pods-OHHTTPStubsDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6B126C3891804F64C29A6D45AEEA9454 /* Debug */, + 8AD107CAB076327F3C14469334222604 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7EF7227D9B20A1D549000096ACCB23D7 /* Debug */, + 4BE66A09A74FD25164AAB3C2645B9B93 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 922F19B1A739BBFD2F4284423D72D365 /* Build configuration list for PBXNativeTarget "OHHTTPStubs" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EBF9EC8C7FCCF763DFC8A783B01F28B5 /* Debug */, + F8068B0476263D7E198F7BC21FD7E2E8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = BFDFE7DC352907FC980B868725387E98 /* Project object */; +} diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs-Info.plist b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs-Info.plist new file mode 100644 index 0000000000..ee83e4d8d3 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 9.0.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs-dummy.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs-dummy.m new file mode 100644 index 0000000000..4deafde22c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_OHHTTPStubs : NSObject +@end +@implementation PodsDummy_OHHTTPStubs +@end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs-prefix.pch b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs-prefix.pch new file mode 100644 index 0000000000..beb2a24418 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs-prefix.pch @@ -0,0 +1,12 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs-umbrella.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs-umbrella.h new file mode 100644 index 0000000000..af1232ad6f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs-umbrella.h @@ -0,0 +1,23 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + +#import "HTTPStubs.h" +#import "HTTPStubsResponse.h" +#import "Compatibility.h" +#import "HTTPStubsResponse+JSON.h" +#import "NSURLRequest+HTTPBodyTesting.h" +#import "HTTPStubsPathHelpers.h" +#import "Compatibility.h" + +FOUNDATION_EXPORT double OHHTTPStubsVersionNumber; +FOUNDATION_EXPORT const unsigned char OHHTTPStubsVersionString[]; + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs.modulemap b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs.modulemap new file mode 100644 index 0000000000..268a7c33d4 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs.modulemap @@ -0,0 +1,6 @@ +framework module OHHTTPStubs { + umbrella header "OHHTTPStubs-umbrella.h" + + export * + module * { export * } +} diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs.xcconfig b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs.xcconfig new file mode 100644 index 0000000000..b735e9f7e6 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs.xcconfig @@ -0,0 +1,10 @@ +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/OHHTTPStubs +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +OTHER_LDFLAGS = $(inherited) -framework "CFNetwork" -framework "Foundation" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/../../.. +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Info.plist b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Info.plist new file mode 100644 index 0000000000..2243fe6e27 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-Info.plist b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-Info.plist new file mode 100644 index 0000000000..2243fe6e27 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-acknowledgements.markdown b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-acknowledgements.markdown new file mode 100644 index 0000000000..751297baaf --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-acknowledgements.markdown @@ -0,0 +1,15 @@ +# Acknowledgements +This application makes use of the following third party libraries: + +## OHHTTPStubs + +- MIT LICENSE - + +Copyright (c) 2012 Olivier Halligon + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Generated by CocoaPods - https://cocoapods.org diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-acknowledgements.plist b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-acknowledgements.plist new file mode 100644 index 0000000000..49d5a452ac --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-acknowledgements.plist @@ -0,0 +1,47 @@ + + + + + PreferenceSpecifiers + + + FooterText + This application makes use of the following third party libraries: + Title + Acknowledgements + Type + PSGroupSpecifier + + + FooterText + - MIT LICENSE - + +Copyright (c) 2012 Olivier Halligon + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + License + MIT + Title + OHHTTPStubs + Type + PSGroupSpecifier + + + FooterText + Generated by CocoaPods - https://cocoapods.org + Title + + Type + PSGroupSpecifier + + + StringsTable + Acknowledgements + Title + Acknowledgements + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-dummy.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-dummy.m new file mode 100644 index 0000000000..7dd9301f50 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Pods_OHHTTPStubsDemo : NSObject +@end +@implementation PodsDummy_Pods_OHHTTPStubsDemo +@end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-frameworks.sh b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-frameworks.sh new file mode 100755 index 0000000000..2677f22bc1 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-frameworks.sh @@ -0,0 +1,171 @@ +#!/bin/sh +set -e +set -u +set -o pipefail + +function on_error { + echo "$(realpath -mq "${0}"):$1: error: Unexpected failure" +} +trap 'on_error $LINENO' ERR + +if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then + # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy + # frameworks to, so exit 0 (signalling the script phase was successful). + exit 0 +fi + +echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" +mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + +COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" +SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" + +# Used as a return value for each invocation of `strip_invalid_archs` function. +STRIP_BINARY_RETVAL=0 + +# This protects against multiple targets copying the same framework dependency at the same time. The solution +# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html +RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") + +# Copies and strips a vendored framework +install_framework() +{ + if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then + local source="${BUILT_PRODUCTS_DIR}/$1" + elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then + local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" + elif [ -r "$1" ]; then + local source="$1" + fi + + local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + + if [ -L "${source}" ]; then + echo "Symlinked..." + source="$(readlink "${source}")" + fi + + # Use filter instead of exclude so missing patterns don't throw errors. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" + + local basename + basename="$(basename -s .framework "$1")" + binary="${destination}/${basename}.framework/${basename}" + + if ! [ -r "$binary" ]; then + binary="${destination}/${basename}" + elif [ -L "${binary}" ]; then + echo "Destination binary is symlinked..." + dirname="$(dirname "${binary}")" + binary="${dirname}/$(readlink "${binary}")" + fi + + # Strip invalid architectures so "fat" simulator / device frameworks work on device + if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then + strip_invalid_archs "$binary" + fi + + # Resign the code if required by the build settings to avoid unstable apps + code_sign_if_enabled "${destination}/$(basename "$1")" + + # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. + if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then + local swift_runtime_libs + swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) + for lib in $swift_runtime_libs; do + echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" + rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" + code_sign_if_enabled "${destination}/${lib}" + done + fi +} + +# Copies and strips a vendored dSYM +install_dsym() { + local source="$1" + if [ -r "$source" ]; then + # Copy the dSYM into a the targets temp dir. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" + + local basename + basename="$(basename -s .framework.dSYM "$source")" + binary="${DERIVED_FILES_DIR}/${basename}.framework.dSYM/Contents/Resources/DWARF/${basename}" + + # Strip invalid architectures so "fat" simulator / device frameworks work on device + if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then + strip_invalid_archs "$binary" + fi + + if [[ $STRIP_BINARY_RETVAL == 1 ]]; then + # Move the stripped file into its final destination. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.framework.dSYM" "${DWARF_DSYM_FOLDER_PATH}" + else + # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. + touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.framework.dSYM" + fi + fi +} + +# Copies the bcsymbolmap files of a vendored framework +install_bcsymbolmap() { + local bcsymbolmap_path="$1" + local destination="${BUILT_PRODUCTS_DIR}" + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}" +} + +# Signs a framework with the provided identity +code_sign_if_enabled() { + if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then + # Use the current code_sign_identity + echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" + local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" + + if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + code_sign_cmd="$code_sign_cmd &" + fi + echo "$code_sign_cmd" + eval "$code_sign_cmd" + fi +} + +# Strip invalid architectures +strip_invalid_archs() { + binary="$1" + # Get architectures for current target binary + binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" + # Intersect them with the architectures we are building for + intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" + # If there are no archs supported by this binary then warn the user + if [[ -z "$intersected_archs" ]]; then + echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." + STRIP_BINARY_RETVAL=0 + return + fi + stripped="" + for arch in $binary_archs; do + if ! [[ "${ARCHS}" == *"$arch"* ]]; then + # Strip non-valid architectures in-place + lipo -remove "$arch" -output "$binary" "$binary" + stripped="$stripped $arch" + fi + done + if [[ "$stripped" ]]; then + echo "Stripped $binary of architectures:$stripped" + fi + STRIP_BINARY_RETVAL=1 +} + + +if [[ "$CONFIGURATION" == "Debug" ]]; then + install_framework "${BUILT_PRODUCTS_DIR}/OHHTTPStubs/OHHTTPStubs.framework" +fi +if [[ "$CONFIGURATION" == "Release" ]]; then + install_framework "${BUILT_PRODUCTS_DIR}/OHHTTPStubs/OHHTTPStubs.framework" +fi +if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + wait +fi diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-resources.sh b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-resources.sh new file mode 100755 index 0000000000..a7df4405b6 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-resources.sh @@ -0,0 +1,106 @@ +#!/bin/sh +set -e + +mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" + +RESOURCES_TO_COPY=${PODS_ROOT}/resources-to-copy-${TARGETNAME}.txt +> "$RESOURCES_TO_COPY" + +XCASSET_FILES=() + +# This protects against multiple targets copying the same framework dependency at the same time. The solution +# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html +RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") + +case "${TARGETED_DEVICE_FAMILY}" in + 1,2) + TARGET_DEVICE_ARGS="--target-device ipad --target-device iphone" + ;; + 1) + TARGET_DEVICE_ARGS="--target-device iphone" + ;; + 2) + TARGET_DEVICE_ARGS="--target-device ipad" + ;; + 3) + TARGET_DEVICE_ARGS="--target-device tv" + ;; + 4) + TARGET_DEVICE_ARGS="--target-device watch" + ;; + *) + TARGET_DEVICE_ARGS="--target-device mac" + ;; +esac + +install_resource() +{ + if [[ "$1" = /* ]] ; then + RESOURCE_PATH="$1" + else + RESOURCE_PATH="${PODS_ROOT}/$1" + fi + if [[ ! -e "$RESOURCE_PATH" ]] ; then + cat << EOM +error: Resource "$RESOURCE_PATH" not found. Run 'pod install' to update the copy resources script. +EOM + exit 1 + fi + case $RESOURCE_PATH in + *.storyboard) + echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true + ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} + ;; + *.xib) + echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true + ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} + ;; + *.framework) + echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true + mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + ;; + *.xcdatamodel) + echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH"`.mom\"" || true + xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodel`.mom" + ;; + *.xcdatamodeld) + echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd\"" || true + xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd" + ;; + *.xcmappingmodel) + echo "xcrun mapc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm\"" || true + xcrun mapc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm" + ;; + *.xcassets) + ABSOLUTE_XCASSET_FILE="$RESOURCE_PATH" + XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE") + ;; + *) + echo "$RESOURCE_PATH" || true + echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY" + ;; + esac +} + +mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +if [[ "${ACTION}" == "install" ]] && [[ "${SKIP_INSTALL}" == "NO" ]]; then + mkdir -p "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" + rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +fi +rm -f "$RESOURCES_TO_COPY" + +if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "$XCASSET_FILES" ] +then + # Find all other xcassets (this unfortunately includes those of path pods and other targets). + OTHER_XCASSETS=$(find "$PWD" -iname "*.xcassets" -type d) + while read line; do + if [[ $line != "${PODS_ROOT}*" ]]; then + XCASSET_FILES+=("$line") + fi + done <<<"$OTHER_XCASSETS" + + printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +fi diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-umbrella.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-umbrella.h new file mode 100644 index 0000000000..47c92a55fc --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double Pods_OHHTTPStubsDemoVersionNumber; +FOUNDATION_EXPORT const unsigned char Pods_OHHTTPStubsDemoVersionString[]; + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo.debug.xcconfig b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo.debug.xcconfig new file mode 100644 index 0000000000..d2e954d1d6 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo.debug.xcconfig @@ -0,0 +1,11 @@ +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/OHHTTPStubs" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/OHHTTPStubs/OHHTTPStubs.framework/Headers" +LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' +OTHER_LDFLAGS = $(inherited) -framework "CFNetwork" -framework "Foundation" -framework "OHHTTPStubs" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo.modulemap b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo.modulemap new file mode 100644 index 0000000000..782618bb07 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo.modulemap @@ -0,0 +1,6 @@ +framework module Pods_OHHTTPStubsDemo { + umbrella header "Pods-OHHTTPStubsDemo-umbrella.h" + + export * + module * { export * } +} diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo.release.xcconfig b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo.release.xcconfig new file mode 100644 index 0000000000..d2e954d1d6 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Pods/Target Support Files/Pods-OHHTTPStubsDemo/Pods-OHHTTPStubsDemo.release.xcconfig @@ -0,0 +1,11 @@ +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/OHHTTPStubs" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/OHHTTPStubs/OHHTTPStubs.framework/Headers" +LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' +OTHER_LDFLAGS = $(inherited) -framework "CFNetwork" -framework "Foundation" -framework "OHHTTPStubs" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Supporting Files/Default-568h@2x.png b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Supporting Files/Default-568h@2x.png new file mode 100644 index 0000000000..0891b7aabf Binary files /dev/null and b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Supporting Files/Default-568h@2x.png differ diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Supporting Files/Images.xcassets/AppIcon.appiconset/Contents.json b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Supporting Files/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..36d2c80d88 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Supporting Files/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Supporting Files/Info.plist b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Supporting Files/Info.plist new file mode 100644 index 0000000000..0ca4d80efa --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Supporting Files/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Supporting Files/LaunchScreen.xib b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Supporting Files/LaunchScreen.xib new file mode 100644 index 0000000000..a9be7a994c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/Swift/Supporting Files/LaunchScreen.xib @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo.xcodeproj/project.pbxproj b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..603763a0af --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo.xcodeproj/project.pbxproj @@ -0,0 +1,389 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 52; + objects = { + +/* Begin PBXBuildFile section */ + 1F206B6122FF819000B98E3C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F206B6022FF819000B98E3C /* AppDelegate.swift */; }; + 1F206B6322FF819000B98E3C /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F206B6222FF819000B98E3C /* SceneDelegate.swift */; }; + 1F206B6822FF819000B98E3C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1F206B6622FF819000B98E3C /* Main.storyboard */; }; + 1F206B6A22FF819200B98E3C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1F206B6922FF819200B98E3C /* Assets.xcassets */; }; + 1F206B6D22FF819200B98E3C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1F206B6B22FF819200B98E3C /* LaunchScreen.storyboard */; }; + 1F206B7522FF8ADF00B98E3C /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F206B7422FF8ADF00B98E3C /* MainViewController.swift */; }; + 1F83D58B22FFB8D80004EE44 /* OHHTTPStubs in Frameworks */ = {isa = PBXBuildFile; productRef = 1F83D58A22FFB8D80004EE44 /* OHHTTPStubs */; }; + 1FDDCA6722FF975200D47058 /* OHHTTPStubsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 1FDDCA6622FF975200D47058 /* OHHTTPStubsSwift */; }; + 1FDF83BD22FF905B00BC47DF /* stub.txt in Resources */ = {isa = PBXBuildFile; fileRef = 1FDF83BB22FF905B00BC47DF /* stub.txt */; }; + 1FDF83BE22FF905B00BC47DF /* stub.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 1FDF83BC22FF905B00BC47DF /* stub.jpg */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 1F206B5D22FF819000B98E3C /* OHHTTPStubsDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OHHTTPStubsDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 1F206B6022FF819000B98E3C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 1F206B6222FF819000B98E3C /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 1F206B6722FF819000B98E3C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 1F206B6922FF819200B98E3C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 1F206B6C22FF819200B98E3C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 1F206B6E22FF819200B98E3C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 1F206B7422FF8ADF00B98E3C /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; + 1F206B7822FF8BED00B98E3C /* OHHTTPStubs */ = {isa = PBXFileReference; lastKnownFileType = folder; name = OHHTTPStubs; path = ../..; sourceTree = ""; }; + 1FDF83BB22FF905B00BC47DF /* stub.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = stub.txt; sourceTree = ""; }; + 1FDF83BC22FF905B00BC47DF /* stub.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = stub.jpg; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 1F206B5A22FF819000B98E3C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1FDDCA6722FF975200D47058 /* OHHTTPStubsSwift in Frameworks */, + 1F83D58B22FFB8D80004EE44 /* OHHTTPStubs in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1F206B5422FF819000B98E3C = { + isa = PBXGroup; + children = ( + 1F206B5F22FF819000B98E3C /* OHHTTPStubsDemo */, + 1F206B7822FF8BED00B98E3C /* OHHTTPStubs */, + 1F206B5E22FF819000B98E3C /* Products */, + 1F206B7922FF8C0D00B98E3C /* Frameworks */, + ); + sourceTree = ""; + }; + 1F206B5E22FF819000B98E3C /* Products */ = { + isa = PBXGroup; + children = ( + 1F206B5D22FF819000B98E3C /* OHHTTPStubsDemo.app */, + ); + name = Products; + sourceTree = ""; + }; + 1F206B5F22FF819000B98E3C /* OHHTTPStubsDemo */ = { + isa = PBXGroup; + children = ( + 1F206B6022FF819000B98E3C /* AppDelegate.swift */, + 1F206B6922FF819200B98E3C /* Assets.xcassets */, + 1F206B6E22FF819200B98E3C /* Info.plist */, + 1F206B6B22FF819200B98E3C /* LaunchScreen.storyboard */, + 1F206B6622FF819000B98E3C /* Main.storyboard */, + 1F206B7422FF8ADF00B98E3C /* MainViewController.swift */, + 1F206B6222FF819000B98E3C /* SceneDelegate.swift */, + 1FDF83BA22FF905B00BC47DF /* Stubs */, + ); + path = OHHTTPStubsDemo; + sourceTree = ""; + }; + 1F206B7922FF8C0D00B98E3C /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + 1FDF83BA22FF905B00BC47DF /* Stubs */ = { + isa = PBXGroup; + children = ( + 1FDF83BB22FF905B00BC47DF /* stub.txt */, + 1FDF83BC22FF905B00BC47DF /* stub.jpg */, + ); + name = Stubs; + path = ../../Stubs; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 1F206B5C22FF819000B98E3C /* OHHTTPStubsDemo */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1F206B7122FF819200B98E3C /* Build configuration list for PBXNativeTarget "OHHTTPStubsDemo" */; + buildPhases = ( + 1F206B5922FF819000B98E3C /* Sources */, + 1F206B5A22FF819000B98E3C /* Frameworks */, + 1F206B5B22FF819000B98E3C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = OHHTTPStubsDemo; + packageProductDependencies = ( + 1FDDCA6622FF975200D47058 /* OHHTTPStubsSwift */, + 1F83D58A22FFB8D80004EE44 /* OHHTTPStubs */, + ); + productName = OHHTTPStubsDemo; + productReference = 1F206B5D22FF819000B98E3C /* OHHTTPStubsDemo.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 1F206B5522FF819000B98E3C /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1100; + LastUpgradeCheck = 1100; + ORGANIZATIONNAME = AliSoftware; + TargetAttributes = { + 1F206B5C22FF819000B98E3C = { + CreatedOnToolsVersion = 11.0; + }; + }; + }; + buildConfigurationList = 1F206B5822FF819000B98E3C /* Build configuration list for PBXProject "OHHTTPStubsDemo" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 1F206B5422FF819000B98E3C; + productRefGroup = 1F206B5E22FF819000B98E3C /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 1F206B5C22FF819000B98E3C /* OHHTTPStubsDemo */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 1F206B5B22FF819000B98E3C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1FDF83BD22FF905B00BC47DF /* stub.txt in Resources */, + 1FDF83BE22FF905B00BC47DF /* stub.jpg in Resources */, + 1F206B6D22FF819200B98E3C /* LaunchScreen.storyboard in Resources */, + 1F206B6A22FF819200B98E3C /* Assets.xcassets in Resources */, + 1F206B6822FF819000B98E3C /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 1F206B5922FF819000B98E3C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1F206B6122FF819000B98E3C /* AppDelegate.swift in Sources */, + 1F206B6322FF819000B98E3C /* SceneDelegate.swift in Sources */, + 1F206B7522FF8ADF00B98E3C /* MainViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 1F206B6622FF819000B98E3C /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 1F206B6722FF819000B98E3C /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 1F206B6B22FF819200B98E3C /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 1F206B6C22FF819200B98E3C /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 1F206B6F22FF819200B98E3C /* Debug */ = { + isa = XCBuildConfiguration; + 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; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + 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; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 1F206B7022FF819200B98E3C /* Release */ = { + isa = XCBuildConfiguration; + 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; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 1F206B7222FF819200B98E3C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = NTX63W5EU6; + INFOPLIST_FILE = OHHTTPStubsDemo/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.alisoftware.OHHTTPStubsDemo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 1F206B7322FF819200B98E3C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = NTX63W5EU6; + INFOPLIST_FILE = OHHTTPStubsDemo/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.alisoftware.OHHTTPStubsDemo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1F206B5822FF819000B98E3C /* Build configuration list for PBXProject "OHHTTPStubsDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1F206B6F22FF819200B98E3C /* Debug */, + 1F206B7022FF819200B98E3C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1F206B7122FF819200B98E3C /* Build configuration list for PBXNativeTarget "OHHTTPStubsDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1F206B7222FF819200B98E3C /* Debug */, + 1F206B7322FF819200B98E3C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + 1F83D58A22FFB8D80004EE44 /* OHHTTPStubs */ = { + isa = XCSwiftPackageProductDependency; + productName = OHHTTPStubs; + }; + 1FDDCA6622FF975200D47058 /* OHHTTPStubsSwift */ = { + isa = XCSwiftPackageProductDependency; + productName = OHHTTPStubsSwift; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 1F206B5522FF819000B98E3C /* Project object */; +} diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..68ace104de --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo/AppDelegate.swift b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo/AppDelegate.swift new file mode 100644 index 0000000000..a975d11c5d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo/AppDelegate.swift @@ -0,0 +1,19 @@ +// +// AppDelegate.swift +// OHHTTPStubsDemo +// +// Created by Olivier Halligon on 18/04/2015. +// Copyright (c) 2015 AliSoftware. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + return true + } +} + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo/Assets.xcassets/AppIcon.appiconset/Contents.json b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..d8db8d65fd --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "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" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "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" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo/Assets.xcassets/Contents.json b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..da4a164c91 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo/Base.lproj/LaunchScreen.storyboard b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..865e9329f3 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo/Base.lproj/Main.storyboard b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo/Base.lproj/Main.storyboard new file mode 100644 index 0000000000..77c5012c07 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo/Base.lproj/Main.storyboard @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo/Info.plist b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo/Info.plist new file mode 100644 index 0000000000..2a3483c0d2 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo/Info.plist @@ -0,0 +1,64 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo/MainViewController.swift b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo/MainViewController.swift new file mode 100644 index 0000000000..64f3e35e0a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo/MainViewController.swift @@ -0,0 +1,143 @@ +// +// ViewController.swift +// OHHTTPStubsDemo +// +// Created by Olivier Halligon on 18/04/2015. +// Copyright (c) 2015 AliSoftware. All rights reserved. +// + +import UIKit +import OHHTTPStubs +import OHHTTPStubsSwift + +class MainViewController: UIViewController { + + //////////////////////////////////////////////////////////////////////////////// + // MARK: - Outlets + + @IBOutlet var delaySwitch: UISwitch! + @IBOutlet var textView: UITextView! + @IBOutlet var installTextStubSwitch: UISwitch! + @IBOutlet var imageView: UIImageView! + @IBOutlet var installImageStubSwitch: UISwitch! + + //////////////////////////////////////////////////////////////////////////////// + // MARK: - Init & Dealloc + + override func viewDidLoad() { + super.viewDidLoad() + + installTextStub(self.installTextStubSwitch) + installImageStub(self.installImageStubSwitch) + + HTTPStubs.onStubActivation { (request: URLRequest, stub: HTTPStubsDescriptor, response: HTTPStubsResponse) in + print("[OHHTTPStubs] Request to \(request.url!) has been stubbed with \(String(describing: stub.name))") + } + } + + //////////////////////////////////////////////////////////////////////////////// + // MARK: - Global stubs activation + + @IBAction func toggleStubs(_ sender: UISwitch) { + HTTPStubs.setEnabled(sender.isOn) + self.delaySwitch.isEnabled = sender.isOn + self.installTextStubSwitch.isEnabled = sender.isOn + self.installImageStubSwitch.isEnabled = sender.isOn + + let state = sender.isOn ? "and enabled" : "but disabled" + print("Installed (\(state)) stubs: \(HTTPStubs.allStubs())") + } + + + + //////////////////////////////////////////////////////////////////////////////// + // MARK: - Text Download and Stub + + + @IBAction func downloadText(_ sender: UIButton) { + sender.isEnabled = false + self.textView.text = nil + + let urlString = "http://www.opensource.apple.com/source/Git/Git-26/src/git-htmldocs/git-commit.txt?txt" + let req = URLRequest(url: URL(string: urlString)!) + + URLSession.shared.dataTask(with: req) { [weak self] (data, _, _) in + DispatchQueue.main.async { + guard let self = self else { + return + } + sender.isEnabled = true + if let receivedData = data, let receivedText = NSString(data: receivedData, encoding: String.Encoding.ascii.rawValue) { + self.textView.text = receivedText as String + } + } + }.resume() + } + + weak var textStub: HTTPStubsDescriptor? + @IBAction func installTextStub(_ sender: UISwitch) { + if sender.isOn { + // Install + let stubPath = OHPathForFile("stub.txt", type(of: self)) + textStub = stub(condition: isExtension("txt")) { [weak self] _ in + let useDelay = DispatchQueue.main.sync { self?.delaySwitch.isOn ?? false } + return fixture(filePath: stubPath!, headers: ["Content-Type":"text/plain"]) + .requestTime(useDelay ? 2.0 : 0.0, responseTime:OHHTTPStubsDownloadSpeedWifi) + } + textStub?.name = "Text stub" + } else { + // Uninstall + HTTPStubs.removeStub(textStub!) + } + } + + + //////////////////////////////////////////////////////////////////////////////// + // MARK: - Image Download and Stub + + @IBAction func downloadImage(_ sender: UIButton) { + sender.isEnabled = false + self.imageView.image = nil + + let urlString = "http://images.apple.com/support/assets/images/products/iphone/hero_iphone4-5_wide.png" + let req = URLRequest(url: URL(string: urlString)!) + + URLSession.shared.dataTask(with: req) { [weak self] (data, _, _) in + guard let self = self else { + return + } + DispatchQueue.main.async { + sender.isEnabled = true + if let receivedData = data { + self.imageView.image = UIImage(data: receivedData) + } + } + }.resume() + } + + weak var imageStub: HTTPStubsDescriptor? + @IBAction func installImageStub(_ sender: UISwitch) { + if sender.isOn { + // Install + let stubPath = OHPathForFile("stub.jpg", type(of: self)) + imageStub = stub(condition: isExtension("png") || isExtension("jpg") || isExtension("gif")) { [weak self] _ in + let useDelay = DispatchQueue.main.sync { self?.delaySwitch.isOn ?? false } + return fixture(filePath: stubPath!, headers: ["Content-Type":"image/jpeg"]) + .requestTime(useDelay ? 2.0 : 0.0, responseTime: OHHTTPStubsDownloadSpeedWifi) + } + imageStub?.name = "Image stub" + } else { + // Uninstall + HTTPStubs.removeStub(imageStub!) + } + } + + //////////////////////////////////////////////////////////////////////////////// + // MARK: - Cleaning + + @IBAction func clearResults() { + self.textView.text = "" + self.imageView.image = nil + } + +} diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo/SceneDelegate.swift b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo/SceneDelegate.swift new file mode 100644 index 0000000000..2be6139c70 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Examples/SwiftPackageManager/OHHTTPStubsDemo/SceneDelegate.swift @@ -0,0 +1,53 @@ +// +// SceneDelegate.swift +// OHHTTPStubsDemo +// +// Created by Jeff Lett on 8/10/19. +// Copyright © 2019 AliSoftware. All rights reserved. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/LICENSE b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/LICENSE new file mode 100644 index 0000000000..a83928dd2e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/LICENSE @@ -0,0 +1,9 @@ +- MIT LICENSE - + +Copyright (c) 2012 Olivier Halligon + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubs.podspec b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubs.podspec new file mode 100644 index 0000000000..53729b4fb1 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubs.podspec @@ -0,0 +1,88 @@ +Pod::Spec.new do |s| + + s.name = "OHHTTPStubs" + s.version = "9.0.0" + + s.summary = "Framework to stub your network requests like HTTP and help you write network unit tests with XCTest." + s.description = <<-DESC.gsub(/^ +\|/,'') + |A class to stub network requests easily: + | + | * Test your apps with fake network data (stubbed from file) + | * You can also customize your response headers and status code + | * Use customized stubs depending on the requests + | * Use custom response time to simulate slow network. + | * This works with any request (HTTP, HTTPS, or any protocol) sent using + | the iOS URL Loading System (NSURLConnection, NSURLSession, AFNetworking, …) + | * This is really useful in unit testing, when you need to test network features + | but don't want to hit the real network and fake some response data instead. + | * Has useful convenience methods to stub JSON content or fixture from a file + | * Compatible with Swift + DESC + + s.homepage = "https://github.com/AliSoftware/OHHTTPStubs" + s.license = "MIT" + s.authors = { 'Olivier Halligon' => 'olivier.halligon+ae@gmail.com' } + + s.source = { :git => "https://github.com/AliSoftware/OHHTTPStubs.git", :tag => s.version.to_s } + + s.frameworks = 'Foundation', 'CFNetwork' + + s.requires_arc = true + s.ios.deployment_target = '8.0' + s.osx.deployment_target = '10.9' + s.watchos.deployment_target = '2.0' + s.tvos.deployment_target = '9.0' + s.swift_versions = ['3.0', '3.1', '3.2', '4.0', '4.1', '4.2', '5.0', '5.1'] + + s.default_subspec = 'Default' + # Default subspec that includes the most commonly-used components + s.subspec 'Default' do |default| + default.dependency 'OHHTTPStubs/Core' + default.dependency 'OHHTTPStubs/NSURLSession' + default.dependency 'OHHTTPStubs/JSON' + default.dependency 'OHHTTPStubs/OHPathHelpers' + end + + # The Core subspec, containing the library core needed in all cases + s.subspec 'Core' do |core| + core.source_files = "Sources/OHHTTPStubs/**/HTTPStubs.{h,m}", "Sources/OHHTTPStubs/**/HTTPStubsResponse.{h,m}", + "Sources/OHHTTPStubs/include/Compatibility.h" + end + + # Optional subspecs + s.subspec 'NSURLSession' do |urlsession| + urlsession.dependency 'OHHTTPStubs/Core' + urlsession.source_files = "Sources/OHHTTPStubs/**/NSURLRequest+HTTPBodyTesting.{h,m}", "Sources/OHHTTPStubs/**/HTTPStubs+NSURLSessionConfiguration.{h,m}", "Sources/OHHTTPStubs/**/HTTPStubsMethodSwizzling.{h,m}" + urlsession.private_header_files = "Sources/OHHTTPStubs/**/HTTPStubsMethodSwizzling.h" + end + + s.subspec 'JSON' do |json| + json.dependency 'OHHTTPStubs/Core' + json.source_files = "Sources/OHHTTPStubs/**/HTTPStubsResponse+JSON.{h,m}" + end + + s.subspec 'HTTPMessage' do |httpmessage| + httpmessage.dependency 'OHHTTPStubs/Core' + httpmessage.source_files = "Sources/HTTPMessage/**/*.{h,m}" + end + + s.subspec 'Mocktail' do |mocktail| + mocktail.dependency 'OHHTTPStubs/Core' + mocktail.source_files = "Sources/Mocktail/**/*.{h,m}" + end + + s.subspec 'OHPathHelpers' do |pathhelper| + pathhelper.source_files = "Sources/OHHTTPStubs/**/HTTPStubsPathHelpers.{h,m}", "Sources/OHHTTPStubs/include/Compatibility.h" + end + + s.subspec 'Swift' do |swift| + swift.ios.deployment_target = '8.0' + swift.osx.deployment_target = '10.9' + swift.watchos.deployment_target = '2.0' + swift.tvos.deployment_target = '9.0' + + swift.dependency 'OHHTTPStubs/Default' + swift.source_files = "Sources/OHHTTPStubsSwift/*.swift" + end + +end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubs.xcodeproj/project.pbxproj b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubs.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..ac4e60a5f3 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubs.xcodeproj/project.pbxproj @@ -0,0 +1,1705 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 09110A4519805F4800D175E4 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 09110A4419805F4800D175E4 /* Foundation.framework */; }; + 09110A5319805F4800D175E4 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 09110A5219805F4800D175E4 /* XCTest.framework */; }; + 09110A5419805F4800D175E4 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 09110A4419805F4800D175E4 /* Foundation.framework */; }; + 09110A5919805F4800D175E4 /* libOHHTTPStubs.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 09110A4119805F4800D175E4 /* libOHHTTPStubs.a */; }; + 093442EC1B80EC4A00A91535 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 09110A5219805F4800D175E4 /* XCTest.framework */; }; + 093442EE1B80EC4A00A91535 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 09110A4419805F4800D175E4 /* Foundation.framework */; }; + 093442FD1B80ED2600A91535 /* OHHTTPStubs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 725CD99B1A9EB65100F84C8B /* OHHTTPStubs.framework */; }; + 095981D319806A7900807DBE /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 09110A5219805F4800D175E4 /* XCTest.framework */; }; + 095981D719806A7900807DBE /* OHHTTPStubs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 095981C219806A7900807DBE /* OHHTTPStubs.framework */; }; + 0A17E6601EBA2C3238103F9C /* libPods-TestingPods-OHHTTPStubs iOS Lib Tests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8506514BF475EFA0F5BB9452 /* libPods-TestingPods-OHHTTPStubs iOS Lib Tests.a */; }; + 1F462BB322FD9671000B7253 /* MocktailTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FCC5CEE22FD95D700472F5B /* MocktailTests.m */; }; + 1F462BB822FD96C6000B7253 /* MocktailFolder in Resources */ = {isa = PBXBuildFile; fileRef = 1F462BB722FD96C6000B7253 /* MocktailFolder */; }; + 1F462BB922FD96C6000B7253 /* MocktailFolder in Resources */ = {isa = PBXBuildFile; fileRef = 1F462BB722FD96C6000B7253 /* MocktailFolder */; }; + 1F462BBA22FD96C6000B7253 /* MocktailFolder in Resources */ = {isa = PBXBuildFile; fileRef = 1F462BB722FD96C6000B7253 /* MocktailFolder */; }; + 1F462BBB22FD96C6000B7253 /* MocktailFolder in Resources */ = {isa = PBXBuildFile; fileRef = 1F462BB722FD96C6000B7253 /* MocktailFolder */; }; + 1F462BBC22FD96FF000B7253 /* login.tail in Resources */ = {isa = PBXBuildFile; fileRef = 1FCC5CE922FD95D700472F5B /* login.tail */; }; + 1F462BBF22FD9B8F000B7253 /* OHHTTPStubs.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FCC5CD022FD95CC00472F5B /* OHHTTPStubs.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1F462BC022FD9CC8000B7253 /* OHHTTPStubs.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FCC5CD022FD95CC00472F5B /* OHHTTPStubs.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1F51F12422FE4D48003463C1 /* SwiftHelpersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F51F12322FE4C53003463C1 /* SwiftHelpersTests.swift */; }; + 1F51F12522FE52CA003463C1 /* SwiftHelpersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F51F12322FE4C53003463C1 /* SwiftHelpersTests.swift */; }; + 1F51F12622FE52D0003463C1 /* SwiftHelpersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F51F12322FE4C53003463C1 /* SwiftHelpersTests.swift */; }; + 1FB9EFF722FFBE670027737A /* NSURLRequest+HTTPBodyTesting.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFE822FFBE670027737A /* NSURLRequest+HTTPBodyTesting.m */; }; + 1FB9EFF822FFBE670027737A /* NSURLRequest+HTTPBodyTesting.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFE822FFBE670027737A /* NSURLRequest+HTTPBodyTesting.m */; }; + 1FB9EFF922FFBE670027737A /* NSURLRequest+HTTPBodyTesting.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFE822FFBE670027737A /* NSURLRequest+HTTPBodyTesting.m */; }; + 1FB9EFFA22FFBE670027737A /* NSURLRequest+HTTPBodyTesting.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFE822FFBE670027737A /* NSURLRequest+HTTPBodyTesting.m */; }; + 1FB9EFFB22FFBE670027737A /* HTTPStubsMethodSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFE922FFBE670027737A /* HTTPStubsMethodSwizzling.m */; }; + 1FB9EFFC22FFBE670027737A /* HTTPStubsMethodSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFE922FFBE670027737A /* HTTPStubsMethodSwizzling.m */; }; + 1FB9EFFD22FFBE670027737A /* HTTPStubsMethodSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFE922FFBE670027737A /* HTTPStubsMethodSwizzling.m */; }; + 1FB9EFFE22FFBE670027737A /* HTTPStubsMethodSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFE922FFBE670027737A /* HTTPStubsMethodSwizzling.m */; }; + 1FB9EFFF22FFBE670027737A /* HTTPStubsPathHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFEA22FFBE670027737A /* HTTPStubsPathHelpers.m */; }; + 1FB9F00022FFBE670027737A /* HTTPStubsPathHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFEA22FFBE670027737A /* HTTPStubsPathHelpers.m */; }; + 1FB9F00122FFBE670027737A /* HTTPStubsPathHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFEA22FFBE670027737A /* HTTPStubsPathHelpers.m */; }; + 1FB9F00222FFBE670027737A /* HTTPStubsPathHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFEA22FFBE670027737A /* HTTPStubsPathHelpers.m */; }; + 1FB9F00322FFBE670027737A /* HTTPStubs.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFEB22FFBE670027737A /* HTTPStubs.m */; }; + 1FB9F00422FFBE670027737A /* HTTPStubs.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFEB22FFBE670027737A /* HTTPStubs.m */; }; + 1FB9F00522FFBE670027737A /* HTTPStubs.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFEB22FFBE670027737A /* HTTPStubs.m */; }; + 1FB9F00622FFBE670027737A /* HTTPStubs.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFEB22FFBE670027737A /* HTTPStubs.m */; }; + 1FB9F00722FFBE670027737A /* Compatibility.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FB9EFED22FFBE670027737A /* Compatibility.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1FB9F00822FFBE670027737A /* Compatibility.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FB9EFED22FFBE670027737A /* Compatibility.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1FB9F00922FFBE670027737A /* Compatibility.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FB9EFED22FFBE670027737A /* Compatibility.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1FB9F00A22FFBE670027737A /* HTTPStubsResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FB9EFEE22FFBE670027737A /* HTTPStubsResponse.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1FB9F00B22FFBE670027737A /* HTTPStubsResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FB9EFEE22FFBE670027737A /* HTTPStubsResponse.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1FB9F00C22FFBE670027737A /* HTTPStubsResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FB9EFEE22FFBE670027737A /* HTTPStubsResponse.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1FB9F00D22FFBE670027737A /* NSURLRequest+HTTPBodyTesting.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FB9EFEF22FFBE670027737A /* NSURLRequest+HTTPBodyTesting.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1FB9F00E22FFBE670027737A /* NSURLRequest+HTTPBodyTesting.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FB9EFEF22FFBE670027737A /* NSURLRequest+HTTPBodyTesting.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1FB9F00F22FFBE670027737A /* NSURLRequest+HTTPBodyTesting.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FB9EFEF22FFBE670027737A /* NSURLRequest+HTTPBodyTesting.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1FB9F01022FFBE670027737A /* HTTPStubsPathHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FB9EFF022FFBE670027737A /* HTTPStubsPathHelpers.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1FB9F01122FFBE670027737A /* HTTPStubsPathHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FB9EFF022FFBE670027737A /* HTTPStubsPathHelpers.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1FB9F01222FFBE670027737A /* HTTPStubsPathHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FB9EFF022FFBE670027737A /* HTTPStubsPathHelpers.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1FB9F01322FFBE670027737A /* HTTPStubs.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FB9EFF122FFBE670027737A /* HTTPStubs.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1FB9F01422FFBE670027737A /* HTTPStubs.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FB9EFF122FFBE670027737A /* HTTPStubs.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1FB9F01522FFBE670027737A /* HTTPStubs.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FB9EFF122FFBE670027737A /* HTTPStubs.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1FB9F01622FFBE670027737A /* HTTPStubsResponse+JSON.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FB9EFF222FFBE670027737A /* HTTPStubsResponse+JSON.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1FB9F01722FFBE670027737A /* HTTPStubsResponse+JSON.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FB9EFF222FFBE670027737A /* HTTPStubsResponse+JSON.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1FB9F01822FFBE670027737A /* HTTPStubsResponse+JSON.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FB9EFF222FFBE670027737A /* HTTPStubsResponse+JSON.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1FB9F01922FFBE670027737A /* HTTPStubsResponse+JSON.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFF322FFBE670027737A /* HTTPStubsResponse+JSON.m */; }; + 1FB9F01A22FFBE670027737A /* HTTPStubsResponse+JSON.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFF322FFBE670027737A /* HTTPStubsResponse+JSON.m */; }; + 1FB9F01B22FFBE670027737A /* HTTPStubsResponse+JSON.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFF322FFBE670027737A /* HTTPStubsResponse+JSON.m */; }; + 1FB9F01C22FFBE670027737A /* HTTPStubsResponse+JSON.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFF322FFBE670027737A /* HTTPStubsResponse+JSON.m */; }; + 1FB9F01D22FFBE670027737A /* HTTPStubs+NSURLSessionConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFF422FFBE670027737A /* HTTPStubs+NSURLSessionConfiguration.m */; }; + 1FB9F01E22FFBE670027737A /* HTTPStubs+NSURLSessionConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFF422FFBE670027737A /* HTTPStubs+NSURLSessionConfiguration.m */; }; + 1FB9F01F22FFBE670027737A /* HTTPStubs+NSURLSessionConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFF422FFBE670027737A /* HTTPStubs+NSURLSessionConfiguration.m */; }; + 1FB9F02022FFBE670027737A /* HTTPStubs+NSURLSessionConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFF422FFBE670027737A /* HTTPStubs+NSURLSessionConfiguration.m */; }; + 1FB9F02122FFBE670027737A /* HTTPStubsMethodSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FB9EFF522FFBE670027737A /* HTTPStubsMethodSwizzling.h */; }; + 1FB9F02222FFBE670027737A /* HTTPStubsMethodSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FB9EFF522FFBE670027737A /* HTTPStubsMethodSwizzling.h */; }; + 1FB9F02322FFBE670027737A /* HTTPStubsMethodSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FB9EFF522FFBE670027737A /* HTTPStubsMethodSwizzling.h */; }; + 1FB9F02422FFBE670027737A /* HTTPStubsResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFF622FFBE670027737A /* HTTPStubsResponse.m */; }; + 1FB9F02522FFBE670027737A /* HTTPStubsResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFF622FFBE670027737A /* HTTPStubsResponse.m */; }; + 1FB9F02622FFBE670027737A /* HTTPStubsResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFF622FFBE670027737A /* HTTPStubsResponse.m */; }; + 1FB9F02722FFBE670027737A /* HTTPStubsResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9EFF622FFBE670027737A /* HTTPStubsResponse.m */; }; + 1FB9F02822FFBFB00027737A /* OHHTTPStubs.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FCC5CD022FD95CC00472F5B /* OHHTTPStubs.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1FB9F03222FFC0CF0027737A /* NSURLConnectionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F02A22FFC0CF0027737A /* NSURLConnectionTests.m */; }; + 1FB9F03322FFC0CF0027737A /* NSURLConnectionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F02A22FFC0CF0027737A /* NSURLConnectionTests.m */; }; + 1FB9F03422FFC0CF0027737A /* NSURLConnectionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F02A22FFC0CF0027737A /* NSURLConnectionTests.m */; }; + 1FB9F03522FFC0CF0027737A /* NSURLConnectionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F02A22FFC0CF0027737A /* NSURLConnectionTests.m */; }; + 1FB9F03622FFC0CF0027737A /* NSURLSessionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F02B22FFC0CF0027737A /* NSURLSessionTests.m */; }; + 1FB9F03722FFC0CF0027737A /* NSURLSessionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F02B22FFC0CF0027737A /* NSURLSessionTests.m */; }; + 1FB9F03822FFC0CF0027737A /* NSURLSessionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F02B22FFC0CF0027737A /* NSURLSessionTests.m */; }; + 1FB9F03922FFC0CF0027737A /* NSURLSessionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F02B22FFC0CF0027737A /* NSURLSessionTests.m */; }; + 1FB9F03A22FFC0CF0027737A /* TimingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F02C22FFC0CF0027737A /* TimingTests.m */; }; + 1FB9F03B22FFC0CF0027737A /* TimingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F02C22FFC0CF0027737A /* TimingTests.m */; }; + 1FB9F03C22FFC0CF0027737A /* TimingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F02C22FFC0CF0027737A /* TimingTests.m */; }; + 1FB9F03D22FFC0CF0027737A /* TimingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F02C22FFC0CF0027737A /* TimingTests.m */; }; + 1FB9F03E22FFC0CF0027737A /* OHPathHelpersTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F02D22FFC0CF0027737A /* OHPathHelpersTests.m */; }; + 1FB9F03F22FFC0CF0027737A /* OHPathHelpersTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F02D22FFC0CF0027737A /* OHPathHelpersTests.m */; }; + 1FB9F04022FFC0CF0027737A /* OHPathHelpersTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F02D22FFC0CF0027737A /* OHPathHelpersTests.m */; }; + 1FB9F04122FFC0CF0027737A /* OHPathHelpersTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F02D22FFC0CF0027737A /* OHPathHelpersTests.m */; }; + 1FB9F04222FFC0CF0027737A /* AFNetworkingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F02E22FFC0CF0027737A /* AFNetworkingTests.m */; }; + 1FB9F04322FFC0CF0027737A /* AFNetworkingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F02E22FFC0CF0027737A /* AFNetworkingTests.m */; }; + 1FB9F04422FFC0CF0027737A /* AFNetworkingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F02E22FFC0CF0027737A /* AFNetworkingTests.m */; }; + 1FB9F04522FFC0CF0027737A /* AFNetworkingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F02E22FFC0CF0027737A /* AFNetworkingTests.m */; }; + 1FB9F04622FFC0CF0027737A /* WithContentsOfURLTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F02F22FFC0CF0027737A /* WithContentsOfURLTests.m */; }; + 1FB9F04722FFC0CF0027737A /* WithContentsOfURLTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F02F22FFC0CF0027737A /* WithContentsOfURLTests.m */; }; + 1FB9F04822FFC0CF0027737A /* WithContentsOfURLTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F02F22FFC0CF0027737A /* WithContentsOfURLTests.m */; }; + 1FB9F04922FFC0CF0027737A /* WithContentsOfURLTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F02F22FFC0CF0027737A /* WithContentsOfURLTests.m */; }; + 1FB9F04A22FFC0CF0027737A /* NSURLConnectionDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F03022FFC0CF0027737A /* NSURLConnectionDelegateTests.m */; }; + 1FB9F04B22FFC0CF0027737A /* NSURLConnectionDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F03022FFC0CF0027737A /* NSURLConnectionDelegateTests.m */; }; + 1FB9F04C22FFC0CF0027737A /* NSURLConnectionDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F03022FFC0CF0027737A /* NSURLConnectionDelegateTests.m */; }; + 1FB9F04D22FFC0CF0027737A /* NSURLConnectionDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F03022FFC0CF0027737A /* NSURLConnectionDelegateTests.m */; }; + 1FB9F04E22FFC0CF0027737A /* NilValuesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F03122FFC0CF0027737A /* NilValuesTests.m */; }; + 1FB9F04F22FFC0CF0027737A /* NilValuesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F03122FFC0CF0027737A /* NilValuesTests.m */; }; + 1FB9F05022FFC0CF0027737A /* NilValuesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F03122FFC0CF0027737A /* NilValuesTests.m */; }; + 1FB9F05122FFC0CF0027737A /* NilValuesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FB9F03122FFC0CF0027737A /* NilValuesTests.m */; }; + 1FCC5C9C22FD95C200472F5B /* HTTPStubsResponse+HTTPMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FCC5C7422FD95C200472F5B /* HTTPStubsResponse+HTTPMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1FCC5C9D22FD95C200472F5B /* HTTPStubsResponse+HTTPMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FCC5C7422FD95C200472F5B /* HTTPStubsResponse+HTTPMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1FCC5C9E22FD95C200472F5B /* HTTPStubsResponse+HTTPMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FCC5C7422FD95C200472F5B /* HTTPStubsResponse+HTTPMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1FCC5C9F22FD95C200472F5B /* HTTPStubsResponse+HTTPMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FCC5C7522FD95C200472F5B /* HTTPStubsResponse+HTTPMessage.m */; }; + 1FCC5CA022FD95C200472F5B /* HTTPStubsResponse+HTTPMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FCC5C7522FD95C200472F5B /* HTTPStubsResponse+HTTPMessage.m */; }; + 1FCC5CA122FD95C200472F5B /* HTTPStubsResponse+HTTPMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FCC5C7522FD95C200472F5B /* HTTPStubsResponse+HTTPMessage.m */; }; + 1FCC5CA222FD95C200472F5B /* HTTPStubsResponse+HTTPMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FCC5C7522FD95C200472F5B /* HTTPStubsResponse+HTTPMessage.m */; }; + 1FCC5CA322FD95C200472F5B /* HTTPStubs+Mocktail.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FCC5C7822FD95C200472F5B /* HTTPStubs+Mocktail.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1FCC5CA422FD95C200472F5B /* HTTPStubs+Mocktail.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FCC5C7822FD95C200472F5B /* HTTPStubs+Mocktail.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1FCC5CA522FD95C200472F5B /* HTTPStubs+Mocktail.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FCC5C7822FD95C200472F5B /* HTTPStubs+Mocktail.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1FCC5CA622FD95C200472F5B /* HTTPStubs+Mocktail.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FCC5C7922FD95C200472F5B /* HTTPStubs+Mocktail.m */; }; + 1FCC5CA722FD95C200472F5B /* HTTPStubs+Mocktail.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FCC5C7922FD95C200472F5B /* HTTPStubs+Mocktail.m */; }; + 1FCC5CA822FD95C200472F5B /* HTTPStubs+Mocktail.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FCC5C7922FD95C200472F5B /* HTTPStubs+Mocktail.m */; }; + 1FCC5CA922FD95C200472F5B /* HTTPStubs+Mocktail.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FCC5C7922FD95C200472F5B /* HTTPStubs+Mocktail.m */; }; + 1FCC5CB922FD95C200472F5B /* OHHTTPStubsSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FCC5C8322FD95C200472F5B /* OHHTTPStubsSwift.swift */; }; + 1FCC5CBA22FD95C200472F5B /* OHHTTPStubsSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FCC5C8322FD95C200472F5B /* OHHTTPStubsSwift.swift */; }; + 1FCC5CBB22FD95C200472F5B /* OHHTTPStubsSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FCC5C8322FD95C200472F5B /* OHHTTPStubsSwift.swift */; }; + 1FCC5D2122FD95D700472F5B /* emptyfile.json in Resources */ = {isa = PBXBuildFile; fileRef = 1FCC5CE722FD95D700472F5B /* emptyfile.json */; }; + 1FCC5D2222FD95D700472F5B /* emptyfile.json in Resources */ = {isa = PBXBuildFile; fileRef = 1FCC5CE722FD95D700472F5B /* emptyfile.json */; }; + 1FCC5D2322FD95D700472F5B /* emptyfile.json in Resources */ = {isa = PBXBuildFile; fileRef = 1FCC5CE722FD95D700472F5B /* emptyfile.json */; }; + 1FCC5D2422FD95D700472F5B /* emptyfile.json in Resources */ = {isa = PBXBuildFile; fileRef = 1FCC5CE722FD95D700472F5B /* emptyfile.json */; }; + 1FCC5D2522FD95D700472F5B /* empty.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 1FCC5CE822FD95D700472F5B /* empty.bundle */; }; + 1FCC5D2622FD95D700472F5B /* empty.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 1FCC5CE822FD95D700472F5B /* empty.bundle */; }; + 1FCC5D2722FD95D700472F5B /* empty.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 1FCC5CE822FD95D700472F5B /* empty.bundle */; }; + 1FCC5D2822FD95D700472F5B /* empty.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 1FCC5CE822FD95D700472F5B /* empty.bundle */; }; + 1FCC5D2A22FD95D700472F5B /* login.tail in Resources */ = {isa = PBXBuildFile; fileRef = 1FCC5CE922FD95D700472F5B /* login.tail */; }; + 1FCC5D2B22FD95D700472F5B /* login.tail in Resources */ = {isa = PBXBuildFile; fileRef = 1FCC5CE922FD95D700472F5B /* login.tail */; }; + 1FCC5D2C22FD95D700472F5B /* login.tail in Resources */ = {isa = PBXBuildFile; fileRef = 1FCC5CE922FD95D700472F5B /* login.tail */; }; + 1FCC5D2D22FD95D700472F5B /* login_content_type_and_headers.tail in Resources */ = {isa = PBXBuildFile; fileRef = 1FCC5CEA22FD95D700472F5B /* login_content_type_and_headers.tail */; }; + 1FCC5D2E22FD95D700472F5B /* login_content_type_and_headers.tail in Resources */ = {isa = PBXBuildFile; fileRef = 1FCC5CEA22FD95D700472F5B /* login_content_type_and_headers.tail */; }; + 1FCC5D2F22FD95D700472F5B /* login_content_type_and_headers.tail in Resources */ = {isa = PBXBuildFile; fileRef = 1FCC5CEA22FD95D700472F5B /* login_content_type_and_headers.tail */; }; + 1FCC5D3022FD95D700472F5B /* login_content_type_and_headers.tail in Resources */ = {isa = PBXBuildFile; fileRef = 1FCC5CEA22FD95D700472F5B /* login_content_type_and_headers.tail */; }; + 1FCC5D3122FD95D700472F5B /* login_headers.tail in Resources */ = {isa = PBXBuildFile; fileRef = 1FCC5CEB22FD95D700472F5B /* login_headers.tail */; }; + 1FCC5D3222FD95D700472F5B /* login_headers.tail in Resources */ = {isa = PBXBuildFile; fileRef = 1FCC5CEB22FD95D700472F5B /* login_headers.tail */; }; + 1FCC5D3322FD95D700472F5B /* login_headers.tail in Resources */ = {isa = PBXBuildFile; fileRef = 1FCC5CEB22FD95D700472F5B /* login_headers.tail */; }; + 1FCC5D3422FD95D700472F5B /* login_headers.tail in Resources */ = {isa = PBXBuildFile; fileRef = 1FCC5CEB22FD95D700472F5B /* login_headers.tail */; }; + 1FCC5D3522FD95D700472F5B /* login_content_type.tail in Resources */ = {isa = PBXBuildFile; fileRef = 1FCC5CEC22FD95D700472F5B /* login_content_type.tail */; }; + 1FCC5D3622FD95D700472F5B /* login_content_type.tail in Resources */ = {isa = PBXBuildFile; fileRef = 1FCC5CEC22FD95D700472F5B /* login_content_type.tail */; }; + 1FCC5D3722FD95D700472F5B /* login_content_type.tail in Resources */ = {isa = PBXBuildFile; fileRef = 1FCC5CEC22FD95D700472F5B /* login_content_type.tail */; }; + 1FCC5D3822FD95D700472F5B /* login_content_type.tail in Resources */ = {isa = PBXBuildFile; fileRef = 1FCC5CEC22FD95D700472F5B /* login_content_type.tail */; }; + 1FCC5D3A22FD95D700472F5B /* MocktailTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FCC5CEE22FD95D700472F5B /* MocktailTests.m */; }; + 1FCC5D3B22FD95D700472F5B /* MocktailTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FCC5CEE22FD95D700472F5B /* MocktailTests.m */; }; + 1FCC5D3C22FD95D700472F5B /* MocktailTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1FCC5CEE22FD95D700472F5B /* MocktailTests.m */; }; + 27C0764180FF776B29627EB9 /* libPods-TestingPods-OHHTTPStubs iOS Fmk Tests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 669D2FDD93B6C9E283419C17 /* libPods-TestingPods-OHHTTPStubs iOS Fmk Tests.a */; }; + B1B6E783F3DB19A4DD60CF23 /* libPods-TestingPods-OHHTTPStubs tvOS Fmk Tests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CA3EE5A02EC251DC1AC3A131 /* libPods-TestingPods-OHHTTPStubs tvOS Fmk Tests.a */; }; + DCC5BE75D7490EBB6625E45C /* libPods-TestingPods-OHHTTPStubs Mac Tests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F8E695A8205C9F383F637AB /* libPods-TestingPods-OHHTTPStubs Mac Tests.a */; }; + EA100ABC1BE15BE400129352 /* OHHTTPStubs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAA436A51BE1598D000E9E99 /* OHHTTPStubs.framework */; }; + EA9D27231BE15C740078CAA0 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA9D27221BE15C740078CAA0 /* Foundation.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 09110A5719805F4800D175E4 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 09110A3919805F4800D175E4 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 09110A4019805F4800D175E4; + remoteInfo = "OHHTTPStubs iOS"; + }; + 093442FB1B80ED1C00A91535 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 09110A3919805F4800D175E4 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 725CD99A1A9EB65100F84C8B; + remoteInfo = "OHHTTPStubs iOS Framework"; + }; + 095981D519806A7900807DBE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 09110A3919805F4800D175E4 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 095981C119806A7900807DBE; + remoteInfo = "OHHTTPStubs Mac"; + }; + EA100ABD1BE15BE400129352 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 09110A3919805F4800D175E4 /* Project object */; + proxyType = 1; + remoteGlobalIDString = EAA4368D1BE1598D000E9E99; + remoteInfo = "OHHTTPStubs tvOS Framework"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 09110A3F19805F4800D175E4 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 070E9B74647A6F3AE67C97A7 /* Pods-TestingPods-OHHTTPStubs Mac Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TestingPods-OHHTTPStubs Mac Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs Mac Tests/Pods-TestingPods-OHHTTPStubs Mac Tests.release.xcconfig"; sourceTree = ""; }; + 09110A4119805F4800D175E4 /* libOHHTTPStubs.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libOHHTTPStubs.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 09110A4419805F4800D175E4 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + 09110A5119805F4800D175E4 /* OHHTTPStubs iOS Lib Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "OHHTTPStubs iOS Lib Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 09110A5219805F4800D175E4 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; + 093442F91B80EC4A00A91535 /* OHHTTPStubs iOS Fmk Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "OHHTTPStubs iOS Fmk Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 0959819819806A4200807DBE /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + 0959819919806A4200807DBE /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; + 0959819A19806A4200807DBE /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; + 095981C219806A7900807DBE /* OHHTTPStubs.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OHHTTPStubs.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 095981D219806A7900807DBE /* OHHTTPStubs Mac Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "OHHTTPStubs Mac Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 1F462BB722FD96C6000B7253 /* MocktailFolder */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MocktailFolder; sourceTree = ""; }; + 1F51F12322FE4C53003463C1 /* SwiftHelpersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftHelpersTests.swift; sourceTree = ""; }; + 1FB9EFE822FFBE670027737A /* NSURLRequest+HTTPBodyTesting.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSURLRequest+HTTPBodyTesting.m"; sourceTree = ""; }; + 1FB9EFE922FFBE670027737A /* HTTPStubsMethodSwizzling.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTTPStubsMethodSwizzling.m; sourceTree = ""; }; + 1FB9EFEA22FFBE670027737A /* HTTPStubsPathHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTTPStubsPathHelpers.m; sourceTree = ""; }; + 1FB9EFEB22FFBE670027737A /* HTTPStubs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTTPStubs.m; sourceTree = ""; }; + 1FB9EFED22FFBE670027737A /* Compatibility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Compatibility.h; sourceTree = ""; }; + 1FB9EFEE22FFBE670027737A /* HTTPStubsResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTTPStubsResponse.h; sourceTree = ""; }; + 1FB9EFEF22FFBE670027737A /* NSURLRequest+HTTPBodyTesting.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSURLRequest+HTTPBodyTesting.h"; sourceTree = ""; }; + 1FB9EFF022FFBE670027737A /* HTTPStubsPathHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTTPStubsPathHelpers.h; sourceTree = ""; }; + 1FB9EFF122FFBE670027737A /* HTTPStubs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTTPStubs.h; sourceTree = ""; }; + 1FB9EFF222FFBE670027737A /* HTTPStubsResponse+JSON.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "HTTPStubsResponse+JSON.h"; sourceTree = ""; }; + 1FB9EFF322FFBE670027737A /* HTTPStubsResponse+JSON.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "HTTPStubsResponse+JSON.m"; sourceTree = ""; }; + 1FB9EFF422FFBE670027737A /* HTTPStubs+NSURLSessionConfiguration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "HTTPStubs+NSURLSessionConfiguration.m"; sourceTree = ""; }; + 1FB9EFF522FFBE670027737A /* HTTPStubsMethodSwizzling.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HTTPStubsMethodSwizzling.h; sourceTree = ""; }; + 1FB9EFF622FFBE670027737A /* HTTPStubsResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HTTPStubsResponse.m; sourceTree = ""; }; + 1FB9F02A22FFC0CF0027737A /* NSURLConnectionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSURLConnectionTests.m; sourceTree = ""; }; + 1FB9F02B22FFC0CF0027737A /* NSURLSessionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSURLSessionTests.m; sourceTree = ""; }; + 1FB9F02C22FFC0CF0027737A /* TimingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TimingTests.m; sourceTree = ""; }; + 1FB9F02D22FFC0CF0027737A /* OHPathHelpersTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OHPathHelpersTests.m; sourceTree = ""; }; + 1FB9F02E22FFC0CF0027737A /* AFNetworkingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AFNetworkingTests.m; sourceTree = ""; }; + 1FB9F02F22FFC0CF0027737A /* WithContentsOfURLTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WithContentsOfURLTests.m; sourceTree = ""; }; + 1FB9F03022FFC0CF0027737A /* NSURLConnectionDelegateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSURLConnectionDelegateTests.m; sourceTree = ""; }; + 1FB9F03122FFC0CF0027737A /* NilValuesTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NilValuesTests.m; sourceTree = ""; }; + 1FCC5C7422FD95C200472F5B /* HTTPStubsResponse+HTTPMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "HTTPStubsResponse+HTTPMessage.h"; sourceTree = ""; }; + 1FCC5C7522FD95C200472F5B /* HTTPStubsResponse+HTTPMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "HTTPStubsResponse+HTTPMessage.m"; sourceTree = ""; }; + 1FCC5C7822FD95C200472F5B /* HTTPStubs+Mocktail.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "HTTPStubs+Mocktail.h"; sourceTree = ""; }; + 1FCC5C7922FD95C200472F5B /* HTTPStubs+Mocktail.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "HTTPStubs+Mocktail.m"; sourceTree = ""; }; + 1FCC5C8322FD95C200472F5B /* OHHTTPStubsSwift.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OHHTTPStubsSwift.swift; sourceTree = ""; }; + 1FCC5CCF22FD95CC00472F5B /* OHHTTPStubs Mac-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "OHHTTPStubs Mac-Info.plist"; sourceTree = ""; }; + 1FCC5CD022FD95CC00472F5B /* OHHTTPStubs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OHHTTPStubs.h; sourceTree = ""; }; + 1FCC5CD122FD95CC00472F5B /* OHHTTPStubs iOS-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "OHHTTPStubs iOS-Info.plist"; sourceTree = ""; }; + 1FCC5CD422FD95D700472F5B /* UnitTests-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "UnitTests-Info.plist"; sourceTree = ""; }; + 1FCC5CDC22FD95D700472F5B /* UnitTests-Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UnitTests-Prefix.pch"; sourceTree = ""; }; + 1FCC5CE722FD95D700472F5B /* emptyfile.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = emptyfile.json; sourceTree = ""; }; + 1FCC5CE822FD95D700472F5B /* empty.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = empty.bundle; sourceTree = ""; }; + 1FCC5CE922FD95D700472F5B /* login.tail */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = login.tail; sourceTree = ""; }; + 1FCC5CEA22FD95D700472F5B /* login_content_type_and_headers.tail */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = login_content_type_and_headers.tail; sourceTree = ""; }; + 1FCC5CEB22FD95D700472F5B /* login_headers.tail */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = login_headers.tail; sourceTree = ""; }; + 1FCC5CEC22FD95D700472F5B /* login_content_type.tail */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = login_content_type.tail; sourceTree = ""; }; + 1FCC5CEE22FD95D700472F5B /* MocktailTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MocktailTests.m; sourceTree = ""; }; + 1FE7BADB223157DB00FFF120 /* OHHTTPStubsProject.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = OHHTTPStubsProject.xcconfig; sourceTree = ""; }; + 451243C1FC2A423646391951 /* Pods-TestingPods-OHHTTPStubs iOS Fmk Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TestingPods-OHHTTPStubs iOS Fmk Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests.debug.xcconfig"; sourceTree = ""; }; + 4F8E695A8205C9F383F637AB /* libPods-TestingPods-OHHTTPStubs Mac Tests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-TestingPods-OHHTTPStubs Mac Tests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 655D2E61F142E992FA46C016 /* Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests.release.xcconfig"; sourceTree = ""; }; + 669D2FDD93B6C9E283419C17 /* libPods-TestingPods-OHHTTPStubs iOS Fmk Tests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-TestingPods-OHHTTPStubs iOS Fmk Tests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 6AE736FB5D3930C8263AAF4D /* Pods-TestingPods-OHHTTPStubs iOS Lib Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TestingPods-OHHTTPStubs iOS Lib Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Lib Tests/Pods-TestingPods-OHHTTPStubs iOS Lib Tests.release.xcconfig"; sourceTree = ""; }; + 725CD99B1A9EB65100F84C8B /* OHHTTPStubs.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OHHTTPStubs.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8506514BF475EFA0F5BB9452 /* libPods-TestingPods-OHHTTPStubs iOS Lib Tests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-TestingPods-OHHTTPStubs iOS Lib Tests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + A19D7C7DF9A479562786D4AD /* Pods-TestingPods-OHHTTPStubs iOS Lib Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TestingPods-OHHTTPStubs iOS Lib Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Lib Tests/Pods-TestingPods-OHHTTPStubs iOS Lib Tests.debug.xcconfig"; sourceTree = ""; }; + CA3EE5A02EC251DC1AC3A131 /* libPods-TestingPods-OHHTTPStubs tvOS Fmk Tests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-TestingPods-OHHTTPStubs tvOS Fmk Tests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + E8D14171F5CFC33738E0F6A0 /* Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests.debug.xcconfig"; sourceTree = ""; }; + EA100AB71BE15BE400129352 /* OHHTTPStubs tvOS Fmk Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "OHHTTPStubs tvOS Fmk Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + EA9D27221BE15C740078CAA0 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS9.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; + EAA436A51BE1598D000E9E99 /* OHHTTPStubs.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OHHTTPStubs.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F976A15FC6C27BA51150B691 /* Pods-TestingPods-OHHTTPStubs iOS Fmk Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TestingPods-OHHTTPStubs iOS Fmk Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests.release.xcconfig"; sourceTree = ""; }; + FADAD1A74682F410A97EE06F /* Pods-TestingPods-OHHTTPStubs Mac Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TestingPods-OHHTTPStubs Mac Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs Mac Tests/Pods-TestingPods-OHHTTPStubs Mac Tests.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 09110A3E19805F4800D175E4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 09110A4519805F4800D175E4 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 09110A4E19805F4800D175E4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 09110A5319805F4800D175E4 /* XCTest.framework in Frameworks */, + 09110A5419805F4800D175E4 /* Foundation.framework in Frameworks */, + 09110A5919805F4800D175E4 /* libOHHTTPStubs.a in Frameworks */, + 0A17E6601EBA2C3238103F9C /* libPods-TestingPods-OHHTTPStubs iOS Lib Tests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 093442EB1B80EC4A00A91535 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 093442EC1B80EC4A00A91535 /* XCTest.framework in Frameworks */, + 093442EE1B80EC4A00A91535 /* Foundation.framework in Frameworks */, + 093442FD1B80ED2600A91535 /* OHHTTPStubs.framework in Frameworks */, + 27C0764180FF776B29627EB9 /* libPods-TestingPods-OHHTTPStubs iOS Fmk Tests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 095981BE19806A7900807DBE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 095981CF19806A7900807DBE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 095981D319806A7900807DBE /* XCTest.framework in Frameworks */, + 095981D719806A7900807DBE /* OHHTTPStubs.framework in Frameworks */, + DCC5BE75D7490EBB6625E45C /* libPods-TestingPods-OHHTTPStubs Mac Tests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 725CD9971A9EB65100F84C8B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EA100AB41BE15BE400129352 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + EA9D27231BE15C740078CAA0 /* Foundation.framework in Frameworks */, + EA100ABC1BE15BE400129352 /* OHHTTPStubs.framework in Frameworks */, + B1B6E783F3DB19A4DD60CF23 /* libPods-TestingPods-OHHTTPStubs tvOS Fmk Tests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EAA436971BE1598D000E9E99 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 09110A3819805F4800D175E4 = { + isa = PBXGroup; + children = ( + 1FE7BADB223157DB00FFF120 /* OHHTTPStubsProject.xcconfig */, + 1FCC5C6A22FD95C200472F5B /* Sources */, + 1FCC5CD322FD95D700472F5B /* Tests */, + 09110A4319805F4800D175E4 /* Frameworks */, + 09110A4219805F4800D175E4 /* Products */, + 71E7CDE1C6A8345F6C70E7D1 /* Pods */, + ); + indentWidth = 4; + sourceTree = ""; + tabWidth = 4; + }; + 09110A4219805F4800D175E4 /* Products */ = { + isa = PBXGroup; + children = ( + 09110A4119805F4800D175E4 /* libOHHTTPStubs.a */, + 09110A5119805F4800D175E4 /* OHHTTPStubs iOS Lib Tests.xctest */, + 095981C219806A7900807DBE /* OHHTTPStubs.framework */, + 095981D219806A7900807DBE /* OHHTTPStubs Mac Tests.xctest */, + 725CD99B1A9EB65100F84C8B /* OHHTTPStubs.framework */, + 093442F91B80EC4A00A91535 /* OHHTTPStubs iOS Fmk Tests.xctest */, + EAA436A51BE1598D000E9E99 /* OHHTTPStubs.framework */, + EA100AB71BE15BE400129352 /* OHHTTPStubs tvOS Fmk Tests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 09110A4319805F4800D175E4 /* Frameworks */ = { + isa = PBXGroup; + children = ( + EA9D27221BE15C740078CAA0 /* Foundation.framework */, + 09110A4419805F4800D175E4 /* Foundation.framework */, + 09110A5219805F4800D175E4 /* XCTest.framework */, + 0959819719806A4200807DBE /* Other Frameworks */, + 4F8E695A8205C9F383F637AB /* libPods-TestingPods-OHHTTPStubs Mac Tests.a */, + 669D2FDD93B6C9E283419C17 /* libPods-TestingPods-OHHTTPStubs iOS Fmk Tests.a */, + 8506514BF475EFA0F5BB9452 /* libPods-TestingPods-OHHTTPStubs iOS Lib Tests.a */, + CA3EE5A02EC251DC1AC3A131 /* libPods-TestingPods-OHHTTPStubs tvOS Fmk Tests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 0959819719806A4200807DBE /* Other Frameworks */ = { + isa = PBXGroup; + children = ( + 0959819819806A4200807DBE /* Foundation.framework */, + 0959819919806A4200807DBE /* CoreData.framework */, + 0959819A19806A4200807DBE /* AppKit.framework */, + ); + name = "Other Frameworks"; + sourceTree = ""; + }; + 1F462BBD22FD9765000B7253 /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 1FCC5CD422FD95D700472F5B /* UnitTests-Info.plist */, + 1FCC5CDC22FD95D700472F5B /* UnitTests-Prefix.pch */, + ); + path = "Supporting Files"; + sourceTree = ""; + }; + 1F51F12222FE4C53003463C1 /* OHHTTPStubsSwiftTests */ = { + isa = PBXGroup; + children = ( + 1F51F12322FE4C53003463C1 /* SwiftHelpersTests.swift */, + ); + name = OHHTTPStubsSwiftTests; + path = Tests/OHHTTPStubsSwiftTests; + sourceTree = SOURCE_ROOT; + }; + 1FB9EFE722FFBE670027737A /* OHHTTPStubs */ = { + isa = PBXGroup; + children = ( + 1FB9EFE822FFBE670027737A /* NSURLRequest+HTTPBodyTesting.m */, + 1FB9EFE922FFBE670027737A /* HTTPStubsMethodSwizzling.m */, + 1FB9EFEA22FFBE670027737A /* HTTPStubsPathHelpers.m */, + 1FB9EFEB22FFBE670027737A /* HTTPStubs.m */, + 1FB9EFEC22FFBE670027737A /* include */, + 1FB9EFF322FFBE670027737A /* HTTPStubsResponse+JSON.m */, + 1FB9EFF422FFBE670027737A /* HTTPStubs+NSURLSessionConfiguration.m */, + 1FB9EFF522FFBE670027737A /* HTTPStubsMethodSwizzling.h */, + 1FB9EFF622FFBE670027737A /* HTTPStubsResponse.m */, + ); + name = OHHTTPStubs; + path = Sources/OHHTTPStubs; + sourceTree = SOURCE_ROOT; + }; + 1FB9EFEC22FFBE670027737A /* include */ = { + isa = PBXGroup; + children = ( + 1FB9EFED22FFBE670027737A /* Compatibility.h */, + 1FB9EFEE22FFBE670027737A /* HTTPStubsResponse.h */, + 1FB9EFEF22FFBE670027737A /* NSURLRequest+HTTPBodyTesting.h */, + 1FB9EFF022FFBE670027737A /* HTTPStubsPathHelpers.h */, + 1FB9EFF122FFBE670027737A /* HTTPStubs.h */, + 1FB9EFF222FFBE670027737A /* HTTPStubsResponse+JSON.h */, + ); + path = include; + sourceTree = ""; + }; + 1FB9F02922FFC0CF0027737A /* OHHTTPStubsTests */ = { + isa = PBXGroup; + children = ( + 1FB9F02A22FFC0CF0027737A /* NSURLConnectionTests.m */, + 1FB9F02B22FFC0CF0027737A /* NSURLSessionTests.m */, + 1FB9F02C22FFC0CF0027737A /* TimingTests.m */, + 1FB9F02D22FFC0CF0027737A /* OHPathHelpersTests.m */, + 1FB9F02E22FFC0CF0027737A /* AFNetworkingTests.m */, + 1FB9F02F22FFC0CF0027737A /* WithContentsOfURLTests.m */, + 1FB9F03022FFC0CF0027737A /* NSURLConnectionDelegateTests.m */, + 1FB9F03122FFC0CF0027737A /* NilValuesTests.m */, + ); + path = OHHTTPStubsTests; + sourceTree = ""; + }; + 1FCC5C6A22FD95C200472F5B /* Sources */ = { + isa = PBXGroup; + children = ( + 1FCC5C7222FD95C200472F5B /* HTTPMessage */, + 1FCC5C7622FD95C200472F5B /* Mocktail */, + 1FB9EFE722FFBE670027737A /* OHHTTPStubs */, + 1FCC5C8222FD95C200472F5B /* OHHTTPStubsSwift */, + 1FCC5CCE22FD95CC00472F5B /* Supporting Files */, + ); + path = Sources; + sourceTree = ""; + }; + 1FCC5C7222FD95C200472F5B /* HTTPMessage */ = { + isa = PBXGroup; + children = ( + 1FCC5C7322FD95C200472F5B /* include */, + 1FCC5C7522FD95C200472F5B /* HTTPStubsResponse+HTTPMessage.m */, + ); + path = HTTPMessage; + sourceTree = ""; + }; + 1FCC5C7322FD95C200472F5B /* include */ = { + isa = PBXGroup; + children = ( + 1FCC5C7422FD95C200472F5B /* HTTPStubsResponse+HTTPMessage.h */, + ); + path = include; + sourceTree = ""; + }; + 1FCC5C7622FD95C200472F5B /* Mocktail */ = { + isa = PBXGroup; + children = ( + 1FCC5C7722FD95C200472F5B /* include */, + 1FCC5C7922FD95C200472F5B /* HTTPStubs+Mocktail.m */, + ); + path = Mocktail; + sourceTree = ""; + }; + 1FCC5C7722FD95C200472F5B /* include */ = { + isa = PBXGroup; + children = ( + 1FCC5C7822FD95C200472F5B /* HTTPStubs+Mocktail.h */, + ); + path = include; + sourceTree = ""; + }; + 1FCC5C8222FD95C200472F5B /* OHHTTPStubsSwift */ = { + isa = PBXGroup; + children = ( + 1FCC5C8322FD95C200472F5B /* OHHTTPStubsSwift.swift */, + ); + path = OHHTTPStubsSwift; + sourceTree = ""; + }; + 1FCC5CCE22FD95CC00472F5B /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 1FCC5CCF22FD95CC00472F5B /* OHHTTPStubs Mac-Info.plist */, + 1FCC5CD022FD95CC00472F5B /* OHHTTPStubs.h */, + 1FCC5CD122FD95CC00472F5B /* OHHTTPStubs iOS-Info.plist */, + ); + path = "Supporting Files"; + sourceTree = ""; + }; + 1FCC5CD322FD95D700472F5B /* Tests */ = { + isa = PBXGroup; + children = ( + 1FCC5CE222FD95D700472F5B /* Fixtures */, + 1FCC5CED22FD95D700472F5B /* MocktailTests */, + 1FB9F02922FFC0CF0027737A /* OHHTTPStubsTests */, + 1F51F12222FE4C53003463C1 /* OHHTTPStubsSwiftTests */, + 1F462BBD22FD9765000B7253 /* Supporting Files */, + ); + path = Tests; + sourceTree = ""; + }; + 1FCC5CE222FD95D700472F5B /* Fixtures */ = { + isa = PBXGroup; + children = ( + 1FCC5CE722FD95D700472F5B /* emptyfile.json */, + 1FCC5CE822FD95D700472F5B /* empty.bundle */, + 1FCC5CE922FD95D700472F5B /* login.tail */, + 1FCC5CEA22FD95D700472F5B /* login_content_type_and_headers.tail */, + 1FCC5CEB22FD95D700472F5B /* login_headers.tail */, + 1FCC5CEC22FD95D700472F5B /* login_content_type.tail */, + 1F462BB722FD96C6000B7253 /* MocktailFolder */, + ); + path = Fixtures; + sourceTree = ""; + }; + 1FCC5CED22FD95D700472F5B /* MocktailTests */ = { + isa = PBXGroup; + children = ( + 1FCC5CEE22FD95D700472F5B /* MocktailTests.m */, + ); + path = MocktailTests; + sourceTree = ""; + }; + 71E7CDE1C6A8345F6C70E7D1 /* Pods */ = { + isa = PBXGroup; + children = ( + FADAD1A74682F410A97EE06F /* Pods-TestingPods-OHHTTPStubs Mac Tests.debug.xcconfig */, + 070E9B74647A6F3AE67C97A7 /* Pods-TestingPods-OHHTTPStubs Mac Tests.release.xcconfig */, + 451243C1FC2A423646391951 /* Pods-TestingPods-OHHTTPStubs iOS Fmk Tests.debug.xcconfig */, + F976A15FC6C27BA51150B691 /* Pods-TestingPods-OHHTTPStubs iOS Fmk Tests.release.xcconfig */, + A19D7C7DF9A479562786D4AD /* Pods-TestingPods-OHHTTPStubs iOS Lib Tests.debug.xcconfig */, + 6AE736FB5D3930C8263AAF4D /* Pods-TestingPods-OHHTTPStubs iOS Lib Tests.release.xcconfig */, + E8D14171F5CFC33738E0F6A0 /* Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests.debug.xcconfig */, + 655D2E61F142E992FA46C016 /* Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 095981BF19806A7900807DBE /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 1FB9F00822FFBE670027737A /* Compatibility.h in Headers */, + 1FB9F01422FFBE670027737A /* HTTPStubs.h in Headers */, + 1FB9F00B22FFBE670027737A /* HTTPStubsResponse.h in Headers */, + 1FB9F01722FFBE670027737A /* HTTPStubsResponse+JSON.h in Headers */, + 1FCC5C9D22FD95C200472F5B /* HTTPStubsResponse+HTTPMessage.h in Headers */, + 1FCC5CA422FD95C200472F5B /* HTTPStubs+Mocktail.h in Headers */, + 1FB9F01122FFBE670027737A /* HTTPStubsPathHelpers.h in Headers */, + 1FB9F00E22FFBE670027737A /* NSURLRequest+HTTPBodyTesting.h in Headers */, + 1F462BBF22FD9B8F000B7253 /* OHHTTPStubs.h in Headers */, + 1FB9F02222FFBE670027737A /* HTTPStubsMethodSwizzling.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 725CD9981A9EB65100F84C8B /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 1FB9F00722FFBE670027737A /* Compatibility.h in Headers */, + 1FB9F01322FFBE670027737A /* HTTPStubs.h in Headers */, + 1FB9F00A22FFBE670027737A /* HTTPStubsResponse.h in Headers */, + 1FB9F01622FFBE670027737A /* HTTPStubsResponse+JSON.h in Headers */, + 1FCC5C9C22FD95C200472F5B /* HTTPStubsResponse+HTTPMessage.h in Headers */, + 1FCC5CA322FD95C200472F5B /* HTTPStubs+Mocktail.h in Headers */, + 1FB9F01022FFBE670027737A /* HTTPStubsPathHelpers.h in Headers */, + 1FB9F00D22FFBE670027737A /* NSURLRequest+HTTPBodyTesting.h in Headers */, + 1FB9F02822FFBFB00027737A /* OHHTTPStubs.h in Headers */, + 1FB9F02122FFBE670027737A /* HTTPStubsMethodSwizzling.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EAA436981BE1598D000E9E99 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 1FB9F00922FFBE670027737A /* Compatibility.h in Headers */, + 1FB9F01522FFBE670027737A /* HTTPStubs.h in Headers */, + 1FB9F00C22FFBE670027737A /* HTTPStubsResponse.h in Headers */, + 1FB9F01822FFBE670027737A /* HTTPStubsResponse+JSON.h in Headers */, + 1FCC5C9E22FD95C200472F5B /* HTTPStubsResponse+HTTPMessage.h in Headers */, + 1FCC5CA522FD95C200472F5B /* HTTPStubs+Mocktail.h in Headers */, + 1FB9F01222FFBE670027737A /* HTTPStubsPathHelpers.h in Headers */, + 1FB9F00F22FFBE670027737A /* NSURLRequest+HTTPBodyTesting.h in Headers */, + 1F462BC022FD9CC8000B7253 /* OHHTTPStubs.h in Headers */, + 1FB9F02322FFBE670027737A /* HTTPStubsMethodSwizzling.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 09110A4019805F4800D175E4 /* OHHTTPStubs iOS StaticLib */ = { + isa = PBXNativeTarget; + buildConfigurationList = 09110A6419805F4800D175E4 /* Build configuration list for PBXNativeTarget "OHHTTPStubs iOS StaticLib" */; + buildPhases = ( + 09110A3D19805F4800D175E4 /* Sources */, + 09110A3E19805F4800D175E4 /* Frameworks */, + 09110A3F19805F4800D175E4 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "OHHTTPStubs iOS StaticLib"; + productName = OHHTTPStubs; + productReference = 09110A4119805F4800D175E4 /* libOHHTTPStubs.a */; + productType = "com.apple.product-type.library.static"; + }; + 09110A5019805F4800D175E4 /* OHHTTPStubs iOS Lib Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 09110A6719805F4800D175E4 /* Build configuration list for PBXNativeTarget "OHHTTPStubs iOS Lib Tests" */; + buildPhases = ( + F244F100556A4B704DFFA149 /* [CP] Check Pods Manifest.lock */, + 09110A4D19805F4800D175E4 /* Sources */, + 09110A4E19805F4800D175E4 /* Frameworks */, + 09110A4F19805F4800D175E4 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 09110A5819805F4800D175E4 /* PBXTargetDependency */, + ); + name = "OHHTTPStubs iOS Lib Tests"; + productName = OHHTTPStubsTests; + productReference = 09110A5119805F4800D175E4 /* OHHTTPStubs iOS Lib Tests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 093442DD1B80EC4A00A91535 /* OHHTTPStubs iOS Fmk Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 093442F61B80EC4A00A91535 /* Build configuration list for PBXNativeTarget "OHHTTPStubs iOS Fmk Tests" */; + buildPhases = ( + 509B16F98DA9B995D11E5530 /* [CP] Check Pods Manifest.lock */, + 093442E11B80EC4A00A91535 /* Sources */, + 093442EB1B80EC4A00A91535 /* Frameworks */, + 093442F01B80EC4A00A91535 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 093442FC1B80ED1C00A91535 /* PBXTargetDependency */, + ); + name = "OHHTTPStubs iOS Fmk Tests"; + productName = OHHTTPStubsTests; + productReference = 093442F91B80EC4A00A91535 /* OHHTTPStubs iOS Fmk Tests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 095981C119806A7900807DBE /* OHHTTPStubs Mac Framework */ = { + isa = PBXNativeTarget; + buildConfigurationList = 095981E019806A7900807DBE /* Build configuration list for PBXNativeTarget "OHHTTPStubs Mac Framework" */; + buildPhases = ( + 095981BD19806A7900807DBE /* Sources */, + 095981BE19806A7900807DBE /* Frameworks */, + 095981BF19806A7900807DBE /* Headers */, + 095981C019806A7900807DBE /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "OHHTTPStubs Mac Framework"; + productName = "OHHTTPStubs Mac"; + productReference = 095981C219806A7900807DBE /* OHHTTPStubs.framework */; + productType = "com.apple.product-type.framework"; + }; + 095981D119806A7900807DBE /* OHHTTPStubs Mac Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 095981E319806A7900807DBE /* Build configuration list for PBXNativeTarget "OHHTTPStubs Mac Tests" */; + buildPhases = ( + D04E5F0E6620A79E830DAB76 /* [CP] Check Pods Manifest.lock */, + 095981CE19806A7900807DBE /* Sources */, + 095981CF19806A7900807DBE /* Frameworks */, + 095981D019806A7900807DBE /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 095981D619806A7900807DBE /* PBXTargetDependency */, + ); + name = "OHHTTPStubs Mac Tests"; + productName = "OHHTTPStubs Mac Tests"; + productReference = 095981D219806A7900807DBE /* OHHTTPStubs Mac Tests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 725CD99A1A9EB65100F84C8B /* OHHTTPStubs iOS Framework */ = { + isa = PBXNativeTarget; + buildConfigurationList = 725CD9B21A9EB65200F84C8B /* Build configuration list for PBXNativeTarget "OHHTTPStubs iOS Framework" */; + buildPhases = ( + 725CD9961A9EB65100F84C8B /* Sources */, + 725CD9971A9EB65100F84C8B /* Frameworks */, + 725CD9981A9EB65100F84C8B /* Headers */, + 725CD9991A9EB65100F84C8B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "OHHTTPStubs iOS Framework"; + productName = "OHHTTPStubs iOS Framework"; + productReference = 725CD99B1A9EB65100F84C8B /* OHHTTPStubs.framework */; + productType = "com.apple.product-type.framework"; + }; + EA100AB61BE15BE400129352 /* OHHTTPStubs tvOS Fmk Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = EA100ABF1BE15BE400129352 /* Build configuration list for PBXNativeTarget "OHHTTPStubs tvOS Fmk Tests" */; + buildPhases = ( + 2D85DF1BBDB54BCF00A5AEAE /* [CP] Check Pods Manifest.lock */, + EA100AB31BE15BE400129352 /* Sources */, + EA100AB41BE15BE400129352 /* Frameworks */, + EA100AB51BE15BE400129352 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + EA100ABE1BE15BE400129352 /* PBXTargetDependency */, + ); + name = "OHHTTPStubs tvOS Fmk Tests"; + productName = "OHHTTPStubs tvOS Fmk Tests"; + productReference = EA100AB71BE15BE400129352 /* OHHTTPStubs tvOS Fmk Tests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + EAA4368D1BE1598D000E9E99 /* OHHTTPStubs tvOS Framework */ = { + isa = PBXNativeTarget; + buildConfigurationList = EAA436A21BE1598D000E9E99 /* Build configuration list for PBXNativeTarget "OHHTTPStubs tvOS Framework" */; + buildPhases = ( + EAA4368E1BE1598D000E9E99 /* Sources */, + EAA436971BE1598D000E9E99 /* Frameworks */, + EAA436981BE1598D000E9E99 /* Headers */, + EAA436A11BE1598D000E9E99 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "OHHTTPStubs tvOS Framework"; + productName = "OHHTTPStubs iOS Framework"; + productReference = EAA436A51BE1598D000E9E99 /* OHHTTPStubs.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 09110A3919805F4800D175E4 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0710; + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = AliSoftware; + TargetAttributes = { + 09110A5019805F4800D175E4 = { + LastSwiftMigration = 0800; + TestTargetID = 09110A4019805F4800D175E4; + }; + 093442DD1B80EC4A00A91535 = { + LastSwiftMigration = 1020; + }; + 095981D119806A7900807DBE = { + LastSwiftMigration = 0800; + TestTargetID = 095981C119806A7900807DBE; + }; + 725CD99A1A9EB65100F84C8B = { + CreatedOnToolsVersion = 6.1.1; + LastSwiftMigration = 1020; + }; + EA100AB61BE15BE400129352 = { + CreatedOnToolsVersion = 7.1; + LastSwiftMigration = 0800; + }; + }; + }; + buildConfigurationList = 09110A3C19805F4800D175E4 /* Build configuration list for PBXProject "OHHTTPStubs" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 09110A3819805F4800D175E4; + productRefGroup = 09110A4219805F4800D175E4 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 09110A4019805F4800D175E4 /* OHHTTPStubs iOS StaticLib */, + 09110A5019805F4800D175E4 /* OHHTTPStubs iOS Lib Tests */, + 725CD99A1A9EB65100F84C8B /* OHHTTPStubs iOS Framework */, + 093442DD1B80EC4A00A91535 /* OHHTTPStubs iOS Fmk Tests */, + 095981C119806A7900807DBE /* OHHTTPStubs Mac Framework */, + 095981D119806A7900807DBE /* OHHTTPStubs Mac Tests */, + EAA4368D1BE1598D000E9E99 /* OHHTTPStubs tvOS Framework */, + EA100AB61BE15BE400129352 /* OHHTTPStubs tvOS Fmk Tests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 09110A4F19805F4800D175E4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1FCC5D2D22FD95D700472F5B /* login_content_type_and_headers.tail in Resources */, + 1FCC5D2122FD95D700472F5B /* emptyfile.json in Resources */, + 1FCC5D3122FD95D700472F5B /* login_headers.tail in Resources */, + 1FCC5D2522FD95D700472F5B /* empty.bundle in Resources */, + 1F462BB822FD96C6000B7253 /* MocktailFolder in Resources */, + 1F462BBC22FD96FF000B7253 /* login.tail in Resources */, + 1FCC5D3522FD95D700472F5B /* login_content_type.tail in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 093442F01B80EC4A00A91535 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1FCC5D2E22FD95D700472F5B /* login_content_type_and_headers.tail in Resources */, + 1FCC5D2222FD95D700472F5B /* emptyfile.json in Resources */, + 1FCC5D3222FD95D700472F5B /* login_headers.tail in Resources */, + 1FCC5D2622FD95D700472F5B /* empty.bundle in Resources */, + 1F462BB922FD96C6000B7253 /* MocktailFolder in Resources */, + 1FCC5D2A22FD95D700472F5B /* login.tail in Resources */, + 1FCC5D3622FD95D700472F5B /* login_content_type.tail in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 095981C019806A7900807DBE /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 095981D019806A7900807DBE /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1FCC5D2F22FD95D700472F5B /* login_content_type_and_headers.tail in Resources */, + 1F462BBA22FD96C6000B7253 /* MocktailFolder in Resources */, + 1FCC5D3322FD95D700472F5B /* login_headers.tail in Resources */, + 1FCC5D2322FD95D700472F5B /* emptyfile.json in Resources */, + 1FCC5D2722FD95D700472F5B /* empty.bundle in Resources */, + 1FCC5D2B22FD95D700472F5B /* login.tail in Resources */, + 1FCC5D3722FD95D700472F5B /* login_content_type.tail in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 725CD9991A9EB65100F84C8B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EA100AB51BE15BE400129352 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1FCC5D3022FD95D700472F5B /* login_content_type_and_headers.tail in Resources */, + 1FCC5D2822FD95D700472F5B /* empty.bundle in Resources */, + 1FCC5D3422FD95D700472F5B /* login_headers.tail in Resources */, + 1FCC5D2422FD95D700472F5B /* emptyfile.json in Resources */, + 1FCC5D2C22FD95D700472F5B /* login.tail in Resources */, + 1F462BBB22FD96C6000B7253 /* MocktailFolder in Resources */, + 1FCC5D3822FD95D700472F5B /* login_content_type.tail in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EAA436A11BE1598D000E9E99 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 2D85DF1BBDB54BCF00A5AEAE /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 509B16F98DA9B995D11E5530 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + D04E5F0E6620A79E830DAB76 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-TestingPods-OHHTTPStubs Mac Tests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F244F100556A4B704DFFA149 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-TestingPods-OHHTTPStubs iOS Lib Tests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 09110A3D19805F4800D175E4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1FB9EFF722FFBE670027737A /* NSURLRequest+HTTPBodyTesting.m in Sources */, + 1FB9EFFB22FFBE670027737A /* HTTPStubsMethodSwizzling.m in Sources */, + 1FB9F00322FFBE670027737A /* HTTPStubs.m in Sources */, + 1FB9F01D22FFBE670027737A /* HTTPStubs+NSURLSessionConfiguration.m in Sources */, + 1FCC5CA622FD95C200472F5B /* HTTPStubs+Mocktail.m in Sources */, + 1FB9F02422FFBE670027737A /* HTTPStubsResponse.m in Sources */, + 1FB9F01922FFBE670027737A /* HTTPStubsResponse+JSON.m in Sources */, + 1FCC5C9F22FD95C200472F5B /* HTTPStubsResponse+HTTPMessage.m in Sources */, + 1FB9EFFF22FFBE670027737A /* HTTPStubsPathHelpers.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 09110A4D19805F4800D175E4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1FB9F03A22FFC0CF0027737A /* TimingTests.m in Sources */, + 1FB9F03E22FFC0CF0027737A /* OHPathHelpersTests.m in Sources */, + 1FB9F04A22FFC0CF0027737A /* NSURLConnectionDelegateTests.m in Sources */, + 1FB9F04622FFC0CF0027737A /* WithContentsOfURLTests.m in Sources */, + 1FB9F04222FFC0CF0027737A /* AFNetworkingTests.m in Sources */, + 1F462BB322FD9671000B7253 /* MocktailTests.m in Sources */, + 1FB9F03622FFC0CF0027737A /* NSURLSessionTests.m in Sources */, + 1FB9F03222FFC0CF0027737A /* NSURLConnectionTests.m in Sources */, + 1FB9F04E22FFC0CF0027737A /* NilValuesTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 093442E11B80EC4A00A91535 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1FB9F03B22FFC0CF0027737A /* TimingTests.m in Sources */, + 1FB9F03F22FFC0CF0027737A /* OHPathHelpersTests.m in Sources */, + 1FB9F04B22FFC0CF0027737A /* NSURLConnectionDelegateTests.m in Sources */, + 1F51F12422FE4D48003463C1 /* SwiftHelpersTests.swift in Sources */, + 1FB9F04722FFC0CF0027737A /* WithContentsOfURLTests.m in Sources */, + 1FB9F04322FFC0CF0027737A /* AFNetworkingTests.m in Sources */, + 1FCC5D3A22FD95D700472F5B /* MocktailTests.m in Sources */, + 1FB9F03722FFC0CF0027737A /* NSURLSessionTests.m in Sources */, + 1FB9F03322FFC0CF0027737A /* NSURLConnectionTests.m in Sources */, + 1FB9F04F22FFC0CF0027737A /* NilValuesTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 095981BD19806A7900807DBE /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1FB9F00522FFBE670027737A /* HTTPStubs.m in Sources */, + 1FB9F01F22FFBE670027737A /* HTTPStubs+NSURLSessionConfiguration.m in Sources */, + 1FCC5CA822FD95C200472F5B /* HTTPStubs+Mocktail.m in Sources */, + 1FB9EFFD22FFBE670027737A /* HTTPStubsMethodSwizzling.m in Sources */, + 1FB9F02622FFBE670027737A /* HTTPStubsResponse.m in Sources */, + 1FB9F01B22FFBE670027737A /* HTTPStubsResponse+JSON.m in Sources */, + 1FCC5CA122FD95C200472F5B /* HTTPStubsResponse+HTTPMessage.m in Sources */, + 1FCC5CBA22FD95C200472F5B /* OHHTTPStubsSwift.swift in Sources */, + 1FB9F00122FFBE670027737A /* HTTPStubsPathHelpers.m in Sources */, + 1FB9EFF922FFBE670027737A /* NSURLRequest+HTTPBodyTesting.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 095981CE19806A7900807DBE /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1FB9F03422FFC0CF0027737A /* NSURLConnectionTests.m in Sources */, + 1FCC5D3B22FD95D700472F5B /* MocktailTests.m in Sources */, + 1FB9F03C22FFC0CF0027737A /* TimingTests.m in Sources */, + 1F51F12522FE52CA003463C1 /* SwiftHelpersTests.swift in Sources */, + 1FB9F04022FFC0CF0027737A /* OHPathHelpersTests.m in Sources */, + 1FB9F04822FFC0CF0027737A /* WithContentsOfURLTests.m in Sources */, + 1FB9F05022FFC0CF0027737A /* NilValuesTests.m in Sources */, + 1FB9F04422FFC0CF0027737A /* AFNetworkingTests.m in Sources */, + 1FB9F03822FFC0CF0027737A /* NSURLSessionTests.m in Sources */, + 1FB9F04C22FFC0CF0027737A /* NSURLConnectionDelegateTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 725CD9961A9EB65100F84C8B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1FB9F00422FFBE670027737A /* HTTPStubs.m in Sources */, + 1FB9F01E22FFBE670027737A /* HTTPStubs+NSURLSessionConfiguration.m in Sources */, + 1FCC5CA722FD95C200472F5B /* HTTPStubs+Mocktail.m in Sources */, + 1FB9EFFC22FFBE670027737A /* HTTPStubsMethodSwizzling.m in Sources */, + 1FB9F02522FFBE670027737A /* HTTPStubsResponse.m in Sources */, + 1FB9F01A22FFBE670027737A /* HTTPStubsResponse+JSON.m in Sources */, + 1FCC5CA022FD95C200472F5B /* HTTPStubsResponse+HTTPMessage.m in Sources */, + 1FCC5CB922FD95C200472F5B /* OHHTTPStubsSwift.swift in Sources */, + 1FB9F00022FFBE670027737A /* HTTPStubsPathHelpers.m in Sources */, + 1FB9EFF822FFBE670027737A /* NSURLRequest+HTTPBodyTesting.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EA100AB31BE15BE400129352 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1FB9F05122FFC0CF0027737A /* NilValuesTests.m in Sources */, + 1FB9F04922FFC0CF0027737A /* WithContentsOfURLTests.m in Sources */, + 1FB9F03922FFC0CF0027737A /* NSURLSessionTests.m in Sources */, + 1F51F12622FE52D0003463C1 /* SwiftHelpersTests.swift in Sources */, + 1FB9F04122FFC0CF0027737A /* OHPathHelpersTests.m in Sources */, + 1FB9F03D22FFC0CF0027737A /* TimingTests.m in Sources */, + 1FB9F04D22FFC0CF0027737A /* NSURLConnectionDelegateTests.m in Sources */, + 1FB9F04522FFC0CF0027737A /* AFNetworkingTests.m in Sources */, + 1FCC5D3C22FD95D700472F5B /* MocktailTests.m in Sources */, + 1FB9F03522FFC0CF0027737A /* NSURLConnectionTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EAA4368E1BE1598D000E9E99 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1FB9F00622FFBE670027737A /* HTTPStubs.m in Sources */, + 1FB9F02022FFBE670027737A /* HTTPStubs+NSURLSessionConfiguration.m in Sources */, + 1FCC5CA922FD95C200472F5B /* HTTPStubs+Mocktail.m in Sources */, + 1FB9EFFE22FFBE670027737A /* HTTPStubsMethodSwizzling.m in Sources */, + 1FB9F02722FFBE670027737A /* HTTPStubsResponse.m in Sources */, + 1FB9F01C22FFBE670027737A /* HTTPStubsResponse+JSON.m in Sources */, + 1FCC5CA222FD95C200472F5B /* HTTPStubsResponse+HTTPMessage.m in Sources */, + 1FCC5CBB22FD95C200472F5B /* OHHTTPStubsSwift.swift in Sources */, + 1FB9F00222FFBE670027737A /* HTTPStubsPathHelpers.m in Sources */, + 1FB9EFFA22FFBE670027737A /* NSURLRequest+HTTPBodyTesting.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 09110A5819805F4800D175E4 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 09110A4019805F4800D175E4 /* OHHTTPStubs iOS StaticLib */; + targetProxy = 09110A5719805F4800D175E4 /* PBXContainerItemProxy */; + }; + 093442FC1B80ED1C00A91535 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 725CD99A1A9EB65100F84C8B /* OHHTTPStubs iOS Framework */; + targetProxy = 093442FB1B80ED1C00A91535 /* PBXContainerItemProxy */; + }; + 095981D619806A7900807DBE /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 095981C119806A7900807DBE /* OHHTTPStubs Mac Framework */; + targetProxy = 095981D519806A7900807DBE /* PBXContainerItemProxy */; + }; + EA100ABE1BE15BE400129352 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = EAA4368D1BE1598D000E9E99 /* OHHTTPStubs tvOS Framework */; + targetProxy = EA100ABD1BE15BE400129352 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 09110A6219805F4800D175E4 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1FE7BADB223157DB00FFF120 /* OHHTTPStubsProject.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_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_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGNING_REQUIRED = NO; + CODE_SIGN_IDENTITY = ""; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 8.0.0; + DYLIB_CURRENT_VERSION = 8.0.0; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.9; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_SWIFT3_OBJC_INFERENCE = Default; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TVOS_DEPLOYMENT_TARGET = 9.0; + }; + name = Debug; + }; + 09110A6319805F4800D175E4 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1FE7BADB223157DB00FFF120 /* OHHTTPStubsProject.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + 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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_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_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGNING_REQUIRED = NO; + CODE_SIGN_IDENTITY = ""; + COPY_PHASE_STRIP = YES; + CURRENT_PROJECT_VERSION = 8.0.0; + DYLIB_CURRENT_VERSION = 8.0.0; + 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; + MACOSX_DEPLOYMENT_TARGET = 10.9; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_SWIFT3_OBJC_INFERENCE = Default; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TVOS_DEPLOYMENT_TARGET = 9.0; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 09110A6519805F4800D175E4 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + DSTROOT = /tmp/OHHTTPStubs.dst; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = OHHTTPStubs; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 09110A6619805F4800D175E4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + DSTROOT = /tmp/OHHTTPStubs.dst; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = OHHTTPStubs; + SKIP_INSTALL = YES; + }; + name = Release; + }; + 09110A6819805F4800D175E4 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A19D7C7DF9A479562786D4AD /* Pods-TestingPods-OHHTTPStubs iOS Lib Tests.debug.xcconfig */; + buildSettings = { + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Tests/Supporting Files/UnitTests-Prefix.pch"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "OHHTTPSTUBS_USE_STATIC_LIBRARY=1", + ); + INFOPLIST_FILE = "Tests/Supporting Files/UnitTests-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.alisoftware.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = xctest; + }; + name = Debug; + }; + 09110A6919805F4800D175E4 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6AE736FB5D3930C8263AAF4D /* Pods-TestingPods-OHHTTPStubs iOS Lib Tests.release.xcconfig */; + buildSettings = { + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Tests/Supporting Files/UnitTests-Prefix.pch"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "COCOAPODS=1", + "OHHTTPSTUBS_USE_STATIC_LIBRARY=1", + ); + INFOPLIST_FILE = "Tests/Supporting Files/UnitTests-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.alisoftware.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = xctest; + }; + name = Release; + }; + 093442F71B80EC4A00A91535 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 451243C1FC2A423646391951 /* Pods-TestingPods-OHHTTPStubs iOS Fmk Tests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; + CLANG_ENABLE_MODULES = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Tests/Supporting Files/UnitTests-Prefix.pch"; + INFOPLIST_FILE = "Tests/Supporting Files/UnitTests-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.alisoftware.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + WRAPPER_EXTENSION = xctest; + }; + name = Debug; + }; + 093442F81B80EC4A00A91535 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F976A15FC6C27BA51150B691 /* Pods-TestingPods-OHHTTPStubs iOS Fmk Tests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; + CLANG_ENABLE_MODULES = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Tests/Supporting Files/UnitTests-Prefix.pch"; + INFOPLIST_FILE = "Tests/Supporting Files/UnitTests-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.alisoftware.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + WRAPPER_EXTENSION = xctest; + }; + name = Release; + }; + 095981E119806A7900807DBE /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + COMBINE_HIDPI_IMAGES = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + FRAMEWORK_VERSION = A; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + INFOPLIST_FILE = "Sources/Supporting Files/OHHTTPStubs Mac-Info.plist"; + INSTALL_PATH = "@rpath"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.alisoftware.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = OHHTTPStubs; + SDKROOT = macosx; + WRAPPER_EXTENSION = framework; + }; + name = Debug; + }; + 095981E219806A7900807DBE /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(DEVELOPER_FRAMEWORKS_DIR)", + ); + FRAMEWORK_VERSION = A; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + INFOPLIST_FILE = "Sources/Supporting Files/OHHTTPStubs Mac-Info.plist"; + INSTALL_PATH = "@rpath"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = "com.alisoftware.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = OHHTTPStubs; + SDKROOT = macosx; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + WRAPPER_EXTENSION = framework; + }; + name = Release; + }; + 095981E419806A7900807DBE /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FADAD1A74682F410A97EE06F /* Pods-TestingPods-OHHTTPStubs Mac Tests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; + COMBINE_HIDPI_IMAGES = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(DEVELOPER_FRAMEWORKS_DIR)", + "$(inherited)", + ); + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Tests/Supporting Files/UnitTests-Prefix.pch"; + LD_RUNPATH_SEARCH_PATHS = "$inherited @executable_path/../Frameworks @loader_path/../Frameworks"; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + WRAPPER_EXTENSION = xctest; + }; + name = Debug; + }; + 095981E519806A7900807DBE /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 070E9B74647A6F3AE67C97A7 /* Pods-TestingPods-OHHTTPStubs Mac Tests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + FRAMEWORK_SEARCH_PATHS = ( + "$(DEVELOPER_FRAMEWORKS_DIR)", + "$(inherited)", + ); + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "Tests/Supporting Files/UnitTests-Prefix.pch"; + LD_RUNPATH_SEARCH_PATHS = "$inherited @executable_path/../Frameworks @loader_path/../Frameworks"; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + ); + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + WRAPPER_EXTENSION = xctest; + }; + name = Release; + }; + 725CD9AE1A9EB65200F84C8B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + INFOPLIST_FILE = "Sources/Supporting Files/OHHTTPStubs iOS-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.alisoftware.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = OHHTTPStubs; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 725CD9AF1A9EB65200F84C8B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + INFOPLIST_FILE = "Sources/Supporting Files/OHHTTPStubs iOS-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "com.alisoftware.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = OHHTTPStubs; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + }; + name = Release; + }; + EA100AC01BE15BE400129352 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E8D14171F5CFC33738E0F6A0 /* Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_NO_COMMON_BLOCKS = YES; + INFOPLIST_FILE = "Tests/Supporting Files/UnitTests-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + PRODUCT_BUNDLE_IDENTIFIER = "alisoftware.OHHTTPStubs-tvOS-Fmk-Tests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + TARGETED_DEVICE_FAMILY = 3; + }; + name = Debug; + }; + EA100AC11BE15BE400129352 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 655D2E61F142E992FA46C016 /* Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_NO_COMMON_BLOCKS = YES; + INFOPLIST_FILE = "Tests/Supporting Files/UnitTests-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "alisoftware.OHHTTPStubs-tvOS-Fmk-Tests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = 3; + }; + name = Release; + }; + EAA436A31BE1598D000E9E99 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + INFOPLIST_FILE = "Sources/Supporting Files/OHHTTPStubs iOS-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.alisoftware.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = OHHTTPStubs; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = 3; + }; + name = Debug; + }; + EAA436A41BE1598D000E9E99 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + INFOPLIST_FILE = "Sources/Supporting Files/OHHTTPStubs iOS-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_BUNDLE_IDENTIFIER = "com.alisoftware.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = OHHTTPStubs; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = 3; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 09110A3C19805F4800D175E4 /* Build configuration list for PBXProject "OHHTTPStubs" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 09110A6219805F4800D175E4 /* Debug */, + 09110A6319805F4800D175E4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 09110A6419805F4800D175E4 /* Build configuration list for PBXNativeTarget "OHHTTPStubs iOS StaticLib" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 09110A6519805F4800D175E4 /* Debug */, + 09110A6619805F4800D175E4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 09110A6719805F4800D175E4 /* Build configuration list for PBXNativeTarget "OHHTTPStubs iOS Lib Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 09110A6819805F4800D175E4 /* Debug */, + 09110A6919805F4800D175E4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 093442F61B80EC4A00A91535 /* Build configuration list for PBXNativeTarget "OHHTTPStubs iOS Fmk Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 093442F71B80EC4A00A91535 /* Debug */, + 093442F81B80EC4A00A91535 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 095981E019806A7900807DBE /* Build configuration list for PBXNativeTarget "OHHTTPStubs Mac Framework" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 095981E119806A7900807DBE /* Debug */, + 095981E219806A7900807DBE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 095981E319806A7900807DBE /* Build configuration list for PBXNativeTarget "OHHTTPStubs Mac Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 095981E419806A7900807DBE /* Debug */, + 095981E519806A7900807DBE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 725CD9B21A9EB65200F84C8B /* Build configuration list for PBXNativeTarget "OHHTTPStubs iOS Framework" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 725CD9AE1A9EB65200F84C8B /* Debug */, + 725CD9AF1A9EB65200F84C8B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + EA100ABF1BE15BE400129352 /* Build configuration list for PBXNativeTarget "OHHTTPStubs tvOS Fmk Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EA100AC01BE15BE400129352 /* Debug */, + EA100AC11BE15BE400129352 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + EAA436A21BE1598D000E9E99 /* Build configuration list for PBXNativeTarget "OHHTTPStubs tvOS Framework" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EAA436A31BE1598D000E9E99 /* Debug */, + EAA436A41BE1598D000E9E99 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 09110A3919805F4800D175E4 /* Project object */; +} diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubs.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubs.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..25fccc6ba4 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubs.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubs.xcodeproj/xcshareddata/xcschemes/OHHTTPStubs Mac Framework.xcscheme b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubs.xcodeproj/xcshareddata/xcschemes/OHHTTPStubs Mac Framework.xcscheme new file mode 100644 index 0000000000..29efabc6db --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubs.xcodeproj/xcshareddata/xcschemes/OHHTTPStubs Mac Framework.xcscheme @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubs.xcodeproj/xcshareddata/xcschemes/OHHTTPStubs iOS Framework.xcscheme b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubs.xcodeproj/xcshareddata/xcschemes/OHHTTPStubs iOS Framework.xcscheme new file mode 100644 index 0000000000..5cf9b5374d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubs.xcodeproj/xcshareddata/xcschemes/OHHTTPStubs iOS Framework.xcscheme @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubs.xcodeproj/xcshareddata/xcschemes/OHHTTPStubs iOS StaticLib.xcscheme b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubs.xcodeproj/xcshareddata/xcschemes/OHHTTPStubs iOS StaticLib.xcscheme new file mode 100644 index 0000000000..bc0cf0b443 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubs.xcodeproj/xcshareddata/xcschemes/OHHTTPStubs iOS StaticLib.xcscheme @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubs.xcodeproj/xcshareddata/xcschemes/OHHTTPStubs tvOS Framework.xcscheme b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubs.xcodeproj/xcshareddata/xcschemes/OHHTTPStubs tvOS Framework.xcscheme new file mode 100644 index 0000000000..6d51630986 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubs.xcodeproj/xcshareddata/xcschemes/OHHTTPStubs tvOS Framework.xcscheme @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubs.xcworkspace/contents.xcworkspacedata b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubs.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..b09d46988b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubs.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubs.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubs.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000000..08de0be8d3 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubs.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubsProject.xcconfig b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubsProject.xcconfig new file mode 100644 index 0000000000..5209339a3c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/OHHTTPStubsProject.xcconfig @@ -0,0 +1,30 @@ +// +// OHHTTPStubsProject.xcconfig +// OHHTTPStubs +// +// Created by Jeff Lett on 3/7/19. +// Copyright © 2019 AliSoftware. All rights reserved. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 + +// These build settings are used to disable specific unit tests. +// They can and should be overridden when running tests in CI using an xcodebuild argument. + + +// xcodebuild Example: +// xcodebuild -workspace OHHTTPStubs/OHHTTPStubs.xcworkspace -scheme "OHHTTPStubs iOS StaticLib" -sdk iphonesimulator -configuration Debug ONLY_ACTIVE_ARCH=NO OHHTTPSTUBS_SKIP_TIMING_TESTS=1 -destination 'name=iPhone 7,OS=latest' clean build test +// +// rake Example: +// rake ios['iOS StaticLib','latest','build-for-testing test-without-building',"OHHTTPSTUBS_SKIP_TIMING_TESTS=1"] +OHHTTPSTUBS_SKIP_TIMING_TESTS=0 + +// xcodebuild Example: +// xcodebuild -workspace OHHTTPStubs/OHHTTPStubs.xcworkspace -scheme "OHHTTPStubs iOS StaticLib" -sdk iphonesimulator -configuration Debug ONLY_ACTIVE_ARCH=NO OHHTTPSTUBS_SKIP_REDIRECT_TESTS=1 -destination 'name=iPhone 7,OS=latest' clean build test +// +// rake Example: +// rake ios['iOS StaticLib','latest','build-for-testing test-without-building',"OHHTTPSTUBS_SKIP_REDIRECT_TESTS=1"] +OHHTTPSTUBS_SKIP_REDIRECT_TESTS=0 + +GCC_PREPROCESSOR_DEFINITIONS=$(inherited) OHHTTPSTUBS_SKIP_TIMING_TESTS=$(OHHTTPSTUBS_SKIP_TIMING_TESTS) OHHTTPSTUBS_SKIP_REDIRECT_TESTS=$(OHHTTPSTUBS_SKIP_REDIRECT_TESTS) diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Package.swift b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Package.swift new file mode 100644 index 0000000000..a9f1553667 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Package.swift @@ -0,0 +1,38 @@ +// swift-tools-version:4.0 +import PackageDescription + +let package = Package( + name: "OHHTTPStubs", + products: [ + .library( + name: "OHHTTPStubs", + targets: [ + "OHHTTPStubs", + ] + ), + .library( + name: "OHHTTPStubsSwift", + targets: [ + "OHHTTPStubs", + "OHHTTPStubsSwift" + ] + ) + ], + dependencies: [ + ], + targets: [ + .target( + name: "OHHTTPStubs", + dependencies: []), + .testTarget( + name: "OHHTTPStubsTests", + dependencies: ["OHHTTPStubs"]), + .target( + name: "OHHTTPStubsSwift", + dependencies: ["OHHTTPStubs"]), + .testTarget( + name: "OHHTTPStubsSwiftTests", + dependencies: ["OHHTTPStubsSwift", "OHHTTPStubs"] + ) + ] +) diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Package@swift-5.swift b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Package@swift-5.swift new file mode 100644 index 0000000000..ce31904c32 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Package@swift-5.swift @@ -0,0 +1,41 @@ +// swift-tools-version:5.0 +import PackageDescription + +let package = Package( + name: "OHHTTPStubs", + platforms: [ + .macOS(.v10_10), .iOS(.v9), .watchOS(.v2), .tvOS(.v9) + ], + products: [ + .library( + name: "OHHTTPStubs", + targets: [ + "OHHTTPStubs", + ] + ), + .library( + name: "OHHTTPStubsSwift", + targets: [ + "OHHTTPStubs", + "OHHTTPStubsSwift" + ] + ) + ], + dependencies: [ + ], + targets: [ + .target( + name: "OHHTTPStubs", + dependencies: []), + .testTarget( + name: "OHHTTPStubsTests", + dependencies: ["OHHTTPStubs"]), + .target( + name: "OHHTTPStubsSwift", + dependencies: ["OHHTTPStubs"]), + .testTarget( + name: "OHHTTPStubsSwiftTests", + dependencies: ["OHHTTPStubsSwift", "OHHTTPStubs"] + ) + ] +) diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Podfile b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Podfile new file mode 100644 index 0000000000..9c276ade09 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Podfile @@ -0,0 +1,24 @@ +source 'https://github.com/CocoaPods/Specs.git' + +project 'OHHTTPStubs.xcodeproj' +inhibit_all_warnings! + +abstract_target 'TestingPods' do + pod 'AFNetworking', '~> 3.0' + + target 'OHHTTPStubs iOS Lib Tests' do + platform :ios, '8.0' + end + + target 'OHHTTPStubs iOS Fmk Tests' do + platform :ios, '8.0' + end + + target 'OHHTTPStubs Mac Tests' do + platform :osx, '10.9' + end + + target 'OHHTTPStubs tvOS Fmk Tests' do + platform :tvos, '9.0' + end +end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Podfile.lock b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Podfile.lock new file mode 100644 index 0000000000..33679d72ff --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Podfile.lock @@ -0,0 +1,30 @@ +PODS: + - AFNetworking (3.0.4): + - AFNetworking/NSURLSession (= 3.0.4) + - AFNetworking/Reachability (= 3.0.4) + - AFNetworking/Security (= 3.0.4) + - AFNetworking/Serialization (= 3.0.4) + - AFNetworking/UIKit (= 3.0.4) + - AFNetworking/NSURLSession (3.0.4): + - AFNetworking/Reachability + - AFNetworking/Security + - AFNetworking/Serialization + - AFNetworking/Reachability (3.0.4) + - AFNetworking/Security (3.0.4) + - AFNetworking/Serialization (3.0.4) + - AFNetworking/UIKit (3.0.4): + - AFNetworking/NSURLSession + +DEPENDENCIES: + - AFNetworking (~> 3.0) + +SPEC REPOS: + https://github.com/cocoapods/specs.git: + - AFNetworking + +SPEC CHECKSUMS: + AFNetworking: a0075feb321559dc78d9d85b55d11caa19eabb93 + +PODFILE CHECKSUM: 8d0eff399cf1d98e2ff7220113aed7a0d13ff37d + +COCOAPODS: 1.6.0.beta.2 diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFHTTPSessionManager.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFHTTPSessionManager.h new file mode 100644 index 0000000000..55ed92ec52 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFHTTPSessionManager.h @@ -0,0 +1,295 @@ +// AFHTTPSessionManager.h +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import +#if !TARGET_OS_WATCH +#import +#endif +#import + +#if TARGET_OS_IOS || TARGET_OS_WATCH || TARGET_OS_TV +#import +#else +#import +#endif + +#import "AFURLSessionManager.h" + +/** + `AFHTTPSessionManager` is a subclass of `AFURLSessionManager` with convenience methods for making HTTP requests. When a `baseURL` is provided, requests made with the `GET` / `POST` / et al. convenience methods can be made with relative paths. + + ## Subclassing Notes + + Developers targeting iOS 7 or Mac OS X 10.9 or later that deal extensively with a web service are encouraged to subclass `AFHTTPSessionManager`, providing a class method that returns a shared singleton object on which authentication and other configuration can be shared across the application. + + For developers targeting iOS 6 or Mac OS X 10.8 or earlier, `AFHTTPRequestOperationManager` may be used to similar effect. + + ## Methods to Override + + To change the behavior of all data task operation construction, which is also used in the `GET` / `POST` / et al. convenience methods, override `dataTaskWithRequest:completionHandler:`. + + ## Serialization + + Requests created by an HTTP client will contain default headers and encode parameters according to the `requestSerializer` property, which is an object conforming to ``. + + Responses received from the server are automatically validated and serialized by the `responseSerializers` property, which is an object conforming to `` + + ## URL Construction Using Relative Paths + + For HTTP convenience methods, the request serializer constructs URLs from the path relative to the `-baseURL`, using `NSURL +URLWithString:relativeToURL:`, when provided. If `baseURL` is `nil`, `path` needs to resolve to a valid `NSURL` object using `NSURL +URLWithString:`. + + Below are a few examples of how `baseURL` and relative paths interact: + + NSURL *baseURL = [NSURL URLWithString:@"http://example.com/v1/"]; + [NSURL URLWithString:@"foo" relativeToURL:baseURL]; // http://example.com/v1/foo + [NSURL URLWithString:@"foo?bar=baz" relativeToURL:baseURL]; // http://example.com/v1/foo?bar=baz + [NSURL URLWithString:@"/foo" relativeToURL:baseURL]; // http://example.com/foo + [NSURL URLWithString:@"foo/" relativeToURL:baseURL]; // http://example.com/v1/foo + [NSURL URLWithString:@"/foo/" relativeToURL:baseURL]; // http://example.com/foo/ + [NSURL URLWithString:@"http://example2.com/" relativeToURL:baseURL]; // http://example2.com/ + + Also important to note is that a trailing slash will be added to any `baseURL` without one. This would otherwise cause unexpected behavior when constructing URLs using paths without a leading slash. + + @warning Managers for background sessions must be owned for the duration of their use. This can be accomplished by creating an application-wide or shared singleton instance. + */ + +NS_ASSUME_NONNULL_BEGIN + +@interface AFHTTPSessionManager : AFURLSessionManager + +/** + The URL used to construct requests from relative paths in methods like `requestWithMethod:URLString:parameters:`, and the `GET` / `POST` / et al. convenience methods. + */ +@property (readonly, nonatomic, strong, nullable) NSURL *baseURL; + +/** + Requests created with `requestWithMethod:URLString:parameters:` & `multipartFormRequestWithMethod:URLString:parameters:constructingBodyWithBlock:` are constructed with a set of default headers using a parameter serialization specified by this property. By default, this is set to an instance of `AFHTTPRequestSerializer`, which serializes query string parameters for `GET`, `HEAD`, and `DELETE` requests, or otherwise URL-form-encodes HTTP message bodies. + + @warning `requestSerializer` must not be `nil`. + */ +@property (nonatomic, strong) AFHTTPRequestSerializer * requestSerializer; + +/** + Responses sent from the server in data tasks created with `dataTaskWithRequest:success:failure:` and run using the `GET` / `POST` / et al. convenience methods are automatically validated and serialized by the response serializer. By default, this property is set to an instance of `AFJSONResponseSerializer`. + + @warning `responseSerializer` must not be `nil`. + */ +@property (nonatomic, strong) AFHTTPResponseSerializer * responseSerializer; + +///--------------------- +/// @name Initialization +///--------------------- + +/** + Creates and returns an `AFHTTPSessionManager` object. + */ ++ (instancetype)manager; + +/** + Initializes an `AFHTTPSessionManager` object with the specified base URL. + + @param url The base URL for the HTTP client. + + @return The newly-initialized HTTP client + */ +- (instancetype)initWithBaseURL:(nullable NSURL *)url; + +/** + Initializes an `AFHTTPSessionManager` object with the specified base URL. + + This is the designated initializer. + + @param url The base URL for the HTTP client. + @param configuration The configuration used to create the managed session. + + @return The newly-initialized HTTP client + */ +- (instancetype)initWithBaseURL:(nullable NSURL *)url + sessionConfiguration:(nullable NSURLSessionConfiguration *)configuration NS_DESIGNATED_INITIALIZER; + +///--------------------------- +/// @name Making HTTP Requests +///--------------------------- + +/** + Creates and runs an `NSURLSessionDataTask` with a `GET` request. + + @param URLString The URL string used to create the request URL. + @param parameters The parameters to be encoded according to the client request serializer. + @param success A block object to be executed when the task finishes successfully. This block has no return value and takes two arguments: the data task, and the response object created by the client response serializer. + @param failure A block object to be executed when the task finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a two arguments: the data task and the error describing the network or parsing error that occurred. + + @see -dataTaskWithRequest:completionHandler: + */ +- (nullable NSURLSessionDataTask *)GET:(NSString *)URLString + parameters:(nullable id)parameters + success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success + failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure DEPRECATED_ATTRIBUTE; + + +/** + Creates and runs an `NSURLSessionDataTask` with a `GET` request. + + @param URLString The URL string used to create the request URL. + @param parameters The parameters to be encoded according to the client request serializer. + @param progress A block object to be executed when the download progress is updated. Note this block is called on the session queue, not the main queue. + @param success A block object to be executed when the task finishes successfully. This block has no return value and takes two arguments: the data task, and the response object created by the client response serializer. + @param failure A block object to be executed when the task finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a two arguments: the data task and the error describing the network or parsing error that occurred. + + @see -dataTaskWithRequest:uploadProgress:downloadProgress:completionHandler: + */ +- (nullable NSURLSessionDataTask *)GET:(NSString *)URLString + parameters:(nullable id)parameters + progress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgress + success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success + failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure; + +/** + Creates and runs an `NSURLSessionDataTask` with a `HEAD` request. + + @param URLString The URL string used to create the request URL. + @param parameters The parameters to be encoded according to the client request serializer. + @param success A block object to be executed when the task finishes successfully. This block has no return value and takes a single arguments: the data task. + @param failure A block object to be executed when the task finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a two arguments: the data task and the error describing the network or parsing error that occurred. + + @see -dataTaskWithRequest:completionHandler: + */ +- (nullable NSURLSessionDataTask *)HEAD:(NSString *)URLString + parameters:(nullable id)parameters + success:(nullable void (^)(NSURLSessionDataTask *task))success + failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure; + +/** + Creates and runs an `NSURLSessionDataTask` with a `POST` request. + + @param URLString The URL string used to create the request URL. + @param parameters The parameters to be encoded according to the client request serializer. + @param success A block object to be executed when the task finishes successfully. This block has no return value and takes two arguments: the data task, and the response object created by the client response serializer. + @param failure A block object to be executed when the task finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a two arguments: the data task and the error describing the network or parsing error that occurred. + + @see -dataTaskWithRequest:completionHandler: + */ +- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString + parameters:(nullable id)parameters + success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success + failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure DEPRECATED_ATTRIBUTE; + +/** + Creates and runs an `NSURLSessionDataTask` with a `POST` request. + + @param URLString The URL string used to create the request URL. + @param parameters The parameters to be encoded according to the client request serializer. + @param progress A block object to be executed when the upload progress is updated. Note this block is called on the session queue, not the main queue. + @param success A block object to be executed when the task finishes successfully. This block has no return value and takes two arguments: the data task, and the response object created by the client response serializer. + @param failure A block object to be executed when the task finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a two arguments: the data task and the error describing the network or parsing error that occurred. + + @see -dataTaskWithRequest:uploadProgress:downloadProgress:completionHandler: + */ +- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString + parameters:(nullable id)parameters + progress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgress + success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success + failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure; + +/** + Creates and runs an `NSURLSessionDataTask` with a multipart `POST` request. + + @param URLString The URL string used to create the request URL. + @param parameters The parameters to be encoded according to the client request serializer. + @param block A block that takes a single argument and appends data to the HTTP body. The block argument is an object adopting the `AFMultipartFormData` protocol. + @param success A block object to be executed when the task finishes successfully. This block has no return value and takes two arguments: the data task, and the response object created by the client response serializer. + @param failure A block object to be executed when the task finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a two arguments: the data task and the error describing the network or parsing error that occurred. + + @see -dataTaskWithRequest:completionHandler: + */ +- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString + parameters:(nullable id)parameters + constructingBodyWithBlock:(nullable void (^)(id formData))block + success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success + failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure DEPRECATED_ATTRIBUTE; + +/** + Creates and runs an `NSURLSessionDataTask` with a multipart `POST` request. + + @param URLString The URL string used to create the request URL. + @param parameters The parameters to be encoded according to the client request serializer. + @param block A block that takes a single argument and appends data to the HTTP body. The block argument is an object adopting the `AFMultipartFormData` protocol. + @param progress A block object to be executed when the upload progress is updated. Note this block is called on the session queue, not the main queue. + @param success A block object to be executed when the task finishes successfully. This block has no return value and takes two arguments: the data task, and the response object created by the client response serializer. + @param failure A block object to be executed when the task finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a two arguments: the data task and the error describing the network or parsing error that occurred. + + @see -dataTaskWithRequest:uploadProgress:downloadProgress:completionHandler: + */ +- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString + parameters:(nullable id)parameters + constructingBodyWithBlock:(nullable void (^)(id formData))block + progress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgress + success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success + failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure; + +/** + Creates and runs an `NSURLSessionDataTask` with a `PUT` request. + + @param URLString The URL string used to create the request URL. + @param parameters The parameters to be encoded according to the client request serializer. + @param success A block object to be executed when the task finishes successfully. This block has no return value and takes two arguments: the data task, and the response object created by the client response serializer. + @param failure A block object to be executed when the task finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a two arguments: the data task and the error describing the network or parsing error that occurred. + + @see -dataTaskWithRequest:completionHandler: + */ +- (nullable NSURLSessionDataTask *)PUT:(NSString *)URLString + parameters:(nullable id)parameters + success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success + failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure; + +/** + Creates and runs an `NSURLSessionDataTask` with a `PATCH` request. + + @param URLString The URL string used to create the request URL. + @param parameters The parameters to be encoded according to the client request serializer. + @param success A block object to be executed when the task finishes successfully. This block has no return value and takes two arguments: the data task, and the response object created by the client response serializer. + @param failure A block object to be executed when the task finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a two arguments: the data task and the error describing the network or parsing error that occurred. + + @see -dataTaskWithRequest:completionHandler: + */ +- (nullable NSURLSessionDataTask *)PATCH:(NSString *)URLString + parameters:(nullable id)parameters + success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success + failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure; + +/** + Creates and runs an `NSURLSessionDataTask` with a `DELETE` request. + + @param URLString The URL string used to create the request URL. + @param parameters The parameters to be encoded according to the client request serializer. + @param success A block object to be executed when the task finishes successfully. This block has no return value and takes two arguments: the data task, and the response object created by the client response serializer. + @param failure A block object to be executed when the task finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a two arguments: the data task and the error describing the network or parsing error that occurred. + + @see -dataTaskWithRequest:completionHandler: + */ +- (nullable NSURLSessionDataTask *)DELETE:(NSString *)URLString + parameters:(nullable id)parameters + success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success + failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFHTTPSessionManager.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFHTTPSessionManager.m new file mode 100644 index 0000000000..a28cc6edc5 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFHTTPSessionManager.m @@ -0,0 +1,361 @@ +// AFHTTPSessionManager.m +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "AFHTTPSessionManager.h" + +#import "AFURLRequestSerialization.h" +#import "AFURLResponseSerialization.h" + +#import +#import +#import + +#import +#import +#import +#import +#import + +#if TARGET_OS_IOS || TARGET_OS_TV +#import +#elif TARGET_OS_WATCH +#import +#endif + +@interface AFHTTPSessionManager () +@property (readwrite, nonatomic, strong) NSURL *baseURL; +@end + +@implementation AFHTTPSessionManager +@dynamic responseSerializer; + ++ (instancetype)manager { + return [[[self class] alloc] initWithBaseURL:nil]; +} + +- (instancetype)init { + return [self initWithBaseURL:nil]; +} + +- (instancetype)initWithBaseURL:(NSURL *)url { + return [self initWithBaseURL:url sessionConfiguration:nil]; +} + +- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration { + return [self initWithBaseURL:nil sessionConfiguration:configuration]; +} + +- (instancetype)initWithBaseURL:(NSURL *)url + sessionConfiguration:(NSURLSessionConfiguration *)configuration +{ + self = [super initWithSessionConfiguration:configuration]; + if (!self) { + return nil; + } + + // Ensure terminal slash for baseURL path, so that NSURL +URLWithString:relativeToURL: works as expected + if ([[url path] length] > 0 && ![[url absoluteString] hasSuffix:@"/"]) { + url = [url URLByAppendingPathComponent:@""]; + } + + self.baseURL = url; + + self.requestSerializer = [AFHTTPRequestSerializer serializer]; + self.responseSerializer = [AFJSONResponseSerializer serializer]; + + return self; +} + +#pragma mark - + +- (void)setRequestSerializer:(AFHTTPRequestSerializer *)requestSerializer { + NSParameterAssert(requestSerializer); + + _requestSerializer = requestSerializer; +} + +- (void)setResponseSerializer:(AFHTTPResponseSerializer *)responseSerializer { + NSParameterAssert(responseSerializer); + + [super setResponseSerializer:responseSerializer]; +} + +#pragma mark - + +- (NSURLSessionDataTask *)GET:(NSString *)URLString + parameters:(id)parameters + success:(void (^)(NSURLSessionDataTask *task, id responseObject))success + failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure +{ + + return [self GET:URLString parameters:parameters progress:nil success:success failure:failure]; +} + +- (NSURLSessionDataTask *)GET:(NSString *)URLString + parameters:(id)parameters + progress:(void (^)(NSProgress * _Nonnull))downloadProgress + success:(void (^)(NSURLSessionDataTask * _Nonnull, id _Nullable))success + failure:(void (^)(NSURLSessionDataTask * _Nullable, NSError * _Nonnull))failure +{ + + NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"GET" + URLString:URLString + parameters:parameters + uploadProgress:nil + downloadProgress:downloadProgress + success:success + failure:failure]; + + [dataTask resume]; + + return dataTask; +} + +- (NSURLSessionDataTask *)HEAD:(NSString *)URLString + parameters:(id)parameters + success:(void (^)(NSURLSessionDataTask *task))success + failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure +{ + NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"HEAD" URLString:URLString parameters:parameters uploadProgress:nil downloadProgress:nil success:^(NSURLSessionDataTask *task, __unused id responseObject) { + if (success) { + success(task); + } + } failure:failure]; + + [dataTask resume]; + + return dataTask; +} + +- (NSURLSessionDataTask *)POST:(NSString *)URLString + parameters:(id)parameters + success:(void (^)(NSURLSessionDataTask *task, id responseObject))success + failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure +{ + return [self POST:URLString parameters:parameters progress:nil success:success failure:failure]; +} + +- (NSURLSessionDataTask *)POST:(NSString *)URLString + parameters:(id)parameters + progress:(void (^)(NSProgress * _Nonnull))uploadProgress + success:(void (^)(NSURLSessionDataTask * _Nonnull, id _Nullable))success + failure:(void (^)(NSURLSessionDataTask * _Nullable, NSError * _Nonnull))failure +{ + NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"POST" URLString:URLString parameters:parameters uploadProgress:uploadProgress downloadProgress:nil success:success failure:failure]; + + [dataTask resume]; + + return dataTask; +} + +- (NSURLSessionDataTask *)POST:(NSString *)URLString + parameters:(nullable id)parameters + constructingBodyWithBlock:(nullable void (^)(id _Nonnull))block + success:(nullable void (^)(NSURLSessionDataTask * _Nonnull, id _Nullable))success + failure:(nullable void (^)(NSURLSessionDataTask * _Nullable, NSError * _Nonnull))failure +{ + return [self POST:URLString parameters:parameters constructingBodyWithBlock:block progress:nil success:success failure:failure]; +} + +- (NSURLSessionDataTask *)POST:(NSString *)URLString + parameters:(id)parameters + constructingBodyWithBlock:(void (^)(id formData))block + progress:(nullable void (^)(NSProgress * _Nonnull))uploadProgress + success:(void (^)(NSURLSessionDataTask *task, id responseObject))success + failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure +{ + NSError *serializationError = nil; + NSMutableURLRequest *request = [self.requestSerializer multipartFormRequestWithMethod:@"POST" URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters constructingBodyWithBlock:block error:&serializationError]; + if (serializationError) { + if (failure) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgnu" + dispatch_async(self.completionQueue ?: dispatch_get_main_queue(), ^{ + failure(nil, serializationError); + }); +#pragma clang diagnostic pop + } + + return nil; + } + + __block NSURLSessionDataTask *task = [self uploadTaskWithStreamedRequest:request progress:uploadProgress completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *error) { + if (error) { + if (failure) { + failure(task, error); + } + } else { + if (success) { + success(task, responseObject); + } + } + }]; + + [task resume]; + + return task; +} + +- (NSURLSessionDataTask *)PUT:(NSString *)URLString + parameters:(id)parameters + success:(void (^)(NSURLSessionDataTask *task, id responseObject))success + failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure +{ + NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"PUT" URLString:URLString parameters:parameters uploadProgress:nil downloadProgress:nil success:success failure:failure]; + + [dataTask resume]; + + return dataTask; +} + +- (NSURLSessionDataTask *)PATCH:(NSString *)URLString + parameters:(id)parameters + success:(void (^)(NSURLSessionDataTask *task, id responseObject))success + failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure +{ + NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"PATCH" URLString:URLString parameters:parameters uploadProgress:nil downloadProgress:nil success:success failure:failure]; + + [dataTask resume]; + + return dataTask; +} + +- (NSURLSessionDataTask *)DELETE:(NSString *)URLString + parameters:(id)parameters + success:(void (^)(NSURLSessionDataTask *task, id responseObject))success + failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure +{ + NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"DELETE" URLString:URLString parameters:parameters uploadProgress:nil downloadProgress:nil success:success failure:failure]; + + [dataTask resume]; + + return dataTask; +} + +- (NSURLSessionDataTask *)dataTaskWithHTTPMethod:(NSString *)method + URLString:(NSString *)URLString + parameters:(id)parameters + uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgress + downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgress + success:(void (^)(NSURLSessionDataTask *, id))success + failure:(void (^)(NSURLSessionDataTask *, NSError *))failure +{ + NSError *serializationError = nil; + NSMutableURLRequest *request = [self.requestSerializer requestWithMethod:method URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters error:&serializationError]; + if (serializationError) { + if (failure) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgnu" + dispatch_async(self.completionQueue ?: dispatch_get_main_queue(), ^{ + failure(nil, serializationError); + }); +#pragma clang diagnostic pop + } + + return nil; + } + + __block NSURLSessionDataTask *dataTask = nil; + dataTask = [self dataTaskWithRequest:request + uploadProgress:uploadProgress + downloadProgress:downloadProgress + completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *error) { + if (error) { + if (failure) { + failure(dataTask, error); + } + } else { + if (success) { + success(dataTask, responseObject); + } + } + }]; + + return dataTask; +} + +#pragma mark - NSObject + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@: %p, baseURL: %@, session: %@, operationQueue: %@>", NSStringFromClass([self class]), self, [self.baseURL absoluteString], self.session, self.operationQueue]; +} + +#pragma mark - NSSecureCoding + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (instancetype)initWithCoder:(NSCoder *)decoder { + NSURL *baseURL = [decoder decodeObjectOfClass:[NSURL class] forKey:NSStringFromSelector(@selector(baseURL))]; + NSURLSessionConfiguration *configuration = [decoder decodeObjectOfClass:[NSURLSessionConfiguration class] forKey:@"sessionConfiguration"]; + if (!configuration) { + NSString *configurationIdentifier = [decoder decodeObjectOfClass:[NSString class] forKey:@"identifier"]; + if (configurationIdentifier) { +#if (defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 1100) + configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:configurationIdentifier]; +#else + configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:configurationIdentifier]; +#endif + } + } + + self = [self initWithBaseURL:baseURL sessionConfiguration:configuration]; + if (!self) { + return nil; + } + + self.requestSerializer = [decoder decodeObjectOfClass:[AFHTTPRequestSerializer class] forKey:NSStringFromSelector(@selector(requestSerializer))]; + self.responseSerializer = [decoder decodeObjectOfClass:[AFHTTPResponseSerializer class] forKey:NSStringFromSelector(@selector(responseSerializer))]; + AFSecurityPolicy *decodedPolicy = [decoder decodeObjectOfClass:[AFSecurityPolicy class] forKey:NSStringFromSelector(@selector(securityPolicy))]; + if (decodedPolicy) { + self.securityPolicy = decodedPolicy; + } + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + + [coder encodeObject:self.baseURL forKey:NSStringFromSelector(@selector(baseURL))]; + if ([self.session.configuration conformsToProtocol:@protocol(NSCoding)]) { + [coder encodeObject:self.session.configuration forKey:@"sessionConfiguration"]; + } else { + [coder encodeObject:self.session.configuration.identifier forKey:@"identifier"]; + } + [coder encodeObject:self.requestSerializer forKey:NSStringFromSelector(@selector(requestSerializer))]; + [coder encodeObject:self.responseSerializer forKey:NSStringFromSelector(@selector(responseSerializer))]; + [coder encodeObject:self.securityPolicy forKey:NSStringFromSelector(@selector(securityPolicy))]; +} + +#pragma mark - NSCopying + +- (instancetype)copyWithZone:(NSZone *)zone { + AFHTTPSessionManager *HTTPClient = [[[self class] allocWithZone:zone] initWithBaseURL:self.baseURL sessionConfiguration:self.session.configuration]; + + HTTPClient.requestSerializer = [self.requestSerializer copyWithZone:zone]; + HTTPClient.responseSerializer = [self.responseSerializer copyWithZone:zone]; + HTTPClient.securityPolicy = [self.securityPolicy copyWithZone:zone]; + return HTTPClient; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFNetworkReachabilityManager.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFNetworkReachabilityManager.h new file mode 100644 index 0000000000..4cf0496d27 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFNetworkReachabilityManager.h @@ -0,0 +1,206 @@ +// AFNetworkReachabilityManager.h +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +#if !TARGET_OS_WATCH +#import + +typedef NS_ENUM(NSInteger, AFNetworkReachabilityStatus) { + AFNetworkReachabilityStatusUnknown = -1, + AFNetworkReachabilityStatusNotReachable = 0, + AFNetworkReachabilityStatusReachableViaWWAN = 1, + AFNetworkReachabilityStatusReachableViaWiFi = 2, +}; + +NS_ASSUME_NONNULL_BEGIN + +/** + `AFNetworkReachabilityManager` monitors the reachability of domains, and addresses for both WWAN and WiFi network interfaces. + + Reachability can be used to determine background information about why a network operation failed, or to trigger a network operation retrying when a connection is established. It should not be used to prevent a user from initiating a network request, as it's possible that an initial request may be required to establish reachability. + + See Apple's Reachability Sample Code (https://developer.apple.com/library/ios/samplecode/reachability/) + + @warning Instances of `AFNetworkReachabilityManager` must be started with `-startMonitoring` before reachability status can be determined. + */ +@interface AFNetworkReachabilityManager : NSObject + +/** + The current network reachability status. + */ +@property (readonly, nonatomic, assign) AFNetworkReachabilityStatus networkReachabilityStatus; + +/** + Whether or not the network is currently reachable. + */ +@property (readonly, nonatomic, assign, getter = isReachable) BOOL reachable; + +/** + Whether or not the network is currently reachable via WWAN. + */ +@property (readonly, nonatomic, assign, getter = isReachableViaWWAN) BOOL reachableViaWWAN; + +/** + Whether or not the network is currently reachable via WiFi. + */ +@property (readonly, nonatomic, assign, getter = isReachableViaWiFi) BOOL reachableViaWiFi; + +///--------------------- +/// @name Initialization +///--------------------- + +/** + Returns the shared network reachability manager. + */ ++ (instancetype)sharedManager; + +/** + Creates and returns a network reachability manager with the default socket address. + + @return An initialized network reachability manager, actively monitoring the default socket address. + */ ++ (instancetype)manager; + +/** + Creates and returns a network reachability manager for the specified domain. + + @param domain The domain used to evaluate network reachability. + + @return An initialized network reachability manager, actively monitoring the specified domain. + */ ++ (instancetype)managerForDomain:(NSString *)domain; + +/** + Creates and returns a network reachability manager for the socket address. + + @param address The socket address (`sockaddr_in6`) used to evaluate network reachability. + + @return An initialized network reachability manager, actively monitoring the specified socket address. + */ ++ (instancetype)managerForAddress:(const void *)address; + +/** + Initializes an instance of a network reachability manager from the specified reachability object. + + @param reachability The reachability object to monitor. + + @return An initialized network reachability manager, actively monitoring the specified reachability. + */ +- (instancetype)initWithReachability:(SCNetworkReachabilityRef)reachability NS_DESIGNATED_INITIALIZER; + +///-------------------------------------------------- +/// @name Starting & Stopping Reachability Monitoring +///-------------------------------------------------- + +/** + Starts monitoring for changes in network reachability status. + */ +- (void)startMonitoring; + +/** + Stops monitoring for changes in network reachability status. + */ +- (void)stopMonitoring; + +///------------------------------------------------- +/// @name Getting Localized Reachability Description +///------------------------------------------------- + +/** + Returns a localized string representation of the current network reachability status. + */ +- (NSString *)localizedNetworkReachabilityStatusString; + +///--------------------------------------------------- +/// @name Setting Network Reachability Change Callback +///--------------------------------------------------- + +/** + Sets a callback to be executed when the network availability of the `baseURL` host changes. + + @param block A block object to be executed when the network availability of the `baseURL` host changes.. This block has no return value and takes a single argument which represents the various reachability states from the device to the `baseURL`. + */ +- (void)setReachabilityStatusChangeBlock:(nullable void (^)(AFNetworkReachabilityStatus status))block; + +@end + +///---------------- +/// @name Constants +///---------------- + +/** + ## Network Reachability + + The following constants are provided by `AFNetworkReachabilityManager` as possible network reachability statuses. + + enum { + AFNetworkReachabilityStatusUnknown, + AFNetworkReachabilityStatusNotReachable, + AFNetworkReachabilityStatusReachableViaWWAN, + AFNetworkReachabilityStatusReachableViaWiFi, + } + + `AFNetworkReachabilityStatusUnknown` + The `baseURL` host reachability is not known. + + `AFNetworkReachabilityStatusNotReachable` + The `baseURL` host cannot be reached. + + `AFNetworkReachabilityStatusReachableViaWWAN` + The `baseURL` host can be reached via a cellular connection, such as EDGE or GPRS. + + `AFNetworkReachabilityStatusReachableViaWiFi` + The `baseURL` host can be reached via a Wi-Fi connection. + + ### Keys for Notification UserInfo Dictionary + + Strings that are used as keys in a `userInfo` dictionary in a network reachability status change notification. + + `AFNetworkingReachabilityNotificationStatusItem` + A key in the userInfo dictionary in a `AFNetworkingReachabilityDidChangeNotification` notification. + The corresponding value is an `NSNumber` object representing the `AFNetworkReachabilityStatus` value for the current reachability status. + */ + +///-------------------- +/// @name Notifications +///-------------------- + +/** + Posted when network reachability changes. + This notification assigns no notification object. The `userInfo` dictionary contains an `NSNumber` object under the `AFNetworkingReachabilityNotificationStatusItem` key, representing the `AFNetworkReachabilityStatus` value for the current network reachability. + + @warning In order for network reachability to be monitored, include the `SystemConfiguration` framework in the active target's "Link Binary With Library" build phase, and add `#import ` to the header prefix of the project (`Prefix.pch`). + */ +FOUNDATION_EXPORT NSString * const AFNetworkingReachabilityDidChangeNotification; +FOUNDATION_EXPORT NSString * const AFNetworkingReachabilityNotificationStatusItem; + +///-------------------- +/// @name Functions +///-------------------- + +/** + Returns a localized string representation of an `AFNetworkReachabilityStatus` value. + */ +FOUNDATION_EXPORT NSString * AFStringFromNetworkReachabilityStatus(AFNetworkReachabilityStatus status); + +NS_ASSUME_NONNULL_END +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFNetworkReachabilityManager.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFNetworkReachabilityManager.m new file mode 100644 index 0000000000..5fba0f7019 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFNetworkReachabilityManager.m @@ -0,0 +1,260 @@ +// AFNetworkReachabilityManager.m +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "AFNetworkReachabilityManager.h" +#if !TARGET_OS_WATCH + +#import +#import +#import +#import +#import + +NSString * const AFNetworkingReachabilityDidChangeNotification = @"com.alamofire.networking.reachability.change"; +NSString * const AFNetworkingReachabilityNotificationStatusItem = @"AFNetworkingReachabilityNotificationStatusItem"; + +typedef void (^AFNetworkReachabilityStatusBlock)(AFNetworkReachabilityStatus status); + +NSString * AFStringFromNetworkReachabilityStatus(AFNetworkReachabilityStatus status) { + switch (status) { + case AFNetworkReachabilityStatusNotReachable: + return NSLocalizedStringFromTable(@"Not Reachable", @"AFNetworking", nil); + case AFNetworkReachabilityStatusReachableViaWWAN: + return NSLocalizedStringFromTable(@"Reachable via WWAN", @"AFNetworking", nil); + case AFNetworkReachabilityStatusReachableViaWiFi: + return NSLocalizedStringFromTable(@"Reachable via WiFi", @"AFNetworking", nil); + case AFNetworkReachabilityStatusUnknown: + default: + return NSLocalizedStringFromTable(@"Unknown", @"AFNetworking", nil); + } +} + +static AFNetworkReachabilityStatus AFNetworkReachabilityStatusForFlags(SCNetworkReachabilityFlags flags) { + BOOL isReachable = ((flags & kSCNetworkReachabilityFlagsReachable) != 0); + BOOL needsConnection = ((flags & kSCNetworkReachabilityFlagsConnectionRequired) != 0); + BOOL canConnectionAutomatically = (((flags & kSCNetworkReachabilityFlagsConnectionOnDemand ) != 0) || ((flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) != 0)); + BOOL canConnectWithoutUserInteraction = (canConnectionAutomatically && (flags & kSCNetworkReachabilityFlagsInterventionRequired) == 0); + BOOL isNetworkReachable = (isReachable && (!needsConnection || canConnectWithoutUserInteraction)); + + AFNetworkReachabilityStatus status = AFNetworkReachabilityStatusUnknown; + if (isNetworkReachable == NO) { + status = AFNetworkReachabilityStatusNotReachable; + } +#if TARGET_OS_IPHONE + else if ((flags & kSCNetworkReachabilityFlagsIsWWAN) != 0) { + status = AFNetworkReachabilityStatusReachableViaWWAN; + } +#endif + else { + status = AFNetworkReachabilityStatusReachableViaWiFi; + } + + return status; +} + +/** + * Queue a status change notification for the main thread. + * + * This is done to ensure that the notifications are received in the same order + * as they are sent. If notifications are sent directly, it is possible that + * a queued notification (for an earlier status condition) is processed after + * the later update, resulting in the listener being left in the wrong state. + */ +static void AFPostReachabilityStatusChange(SCNetworkReachabilityFlags flags, AFNetworkReachabilityStatusBlock block) { + AFNetworkReachabilityStatus status = AFNetworkReachabilityStatusForFlags(flags); + dispatch_async(dispatch_get_main_queue(), ^{ + if (block) { + block(status); + } + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + NSDictionary *userInfo = @{ AFNetworkingReachabilityNotificationStatusItem: @(status) }; + [notificationCenter postNotificationName:AFNetworkingReachabilityDidChangeNotification object:nil userInfo:userInfo]; + }); +} + +static void AFNetworkReachabilityCallback(SCNetworkReachabilityRef __unused target, SCNetworkReachabilityFlags flags, void *info) { + AFPostReachabilityStatusChange(flags, (__bridge AFNetworkReachabilityStatusBlock)info); +} + + +static const void * AFNetworkReachabilityRetainCallback(const void *info) { + return Block_copy(info); +} + +static void AFNetworkReachabilityReleaseCallback(const void *info) { + if (info) { + Block_release(info); + } +} + +@interface AFNetworkReachabilityManager () +@property (readwrite, nonatomic, strong) id networkReachability; +@property (readwrite, nonatomic, assign) AFNetworkReachabilityStatus networkReachabilityStatus; +@property (readwrite, nonatomic, copy) AFNetworkReachabilityStatusBlock networkReachabilityStatusBlock; +@end + +@implementation AFNetworkReachabilityManager + ++ (instancetype)sharedManager { + static AFNetworkReachabilityManager *_sharedManager = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _sharedManager = [self manager]; + }); + + return _sharedManager; +} + +#ifndef __clang_analyzer__ ++ (instancetype)managerForDomain:(NSString *)domain { + SCNetworkReachabilityRef reachability = SCNetworkReachabilityCreateWithName(kCFAllocatorDefault, [domain UTF8String]); + + AFNetworkReachabilityManager *manager = [[self alloc] initWithReachability:reachability]; + + return manager; +} +#endif + +#ifndef __clang_analyzer__ ++ (instancetype)managerForAddress:(const void *)address { + SCNetworkReachabilityRef reachability = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr *)address); + AFNetworkReachabilityManager *manager = [[self alloc] initWithReachability:reachability]; + + return manager; +} +#endif + ++ (instancetype)manager +{ +#if (defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED >= 90000) || (defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 101100) + struct sockaddr_in6 address; + bzero(&address, sizeof(address)); + address.sin6_len = sizeof(address); + address.sin6_family = AF_INET6; +#else + struct sockaddr_in address; + bzero(&address, sizeof(address)); + address.sin_len = sizeof(address); + address.sin_family = AF_INET; +#endif + return [self managerForAddress:&address]; +} + +- (instancetype)initWithReachability:(SCNetworkReachabilityRef)reachability { + self = [super init]; + if (!self) { + return nil; + } + + self.networkReachability = CFBridgingRelease(reachability); + self.networkReachabilityStatus = AFNetworkReachabilityStatusUnknown; + + return self; +} + +- (instancetype)init NS_UNAVAILABLE +{ + return nil; +} + +- (void)dealloc { + [self stopMonitoring]; +} + +#pragma mark - + +- (BOOL)isReachable { + return [self isReachableViaWWAN] || [self isReachableViaWiFi]; +} + +- (BOOL)isReachableViaWWAN { + return self.networkReachabilityStatus == AFNetworkReachabilityStatusReachableViaWWAN; +} + +- (BOOL)isReachableViaWiFi { + return self.networkReachabilityStatus == AFNetworkReachabilityStatusReachableViaWiFi; +} + +#pragma mark - + +- (void)startMonitoring { + [self stopMonitoring]; + + if (!self.networkReachability) { + return; + } + + __weak __typeof(self)weakSelf = self; + AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) { + __strong __typeof(weakSelf)strongSelf = weakSelf; + + strongSelf.networkReachabilityStatus = status; + if (strongSelf.networkReachabilityStatusBlock) { + strongSelf.networkReachabilityStatusBlock(status); + } + + }; + + id networkReachability = self.networkReachability; + SCNetworkReachabilityContext context = {0, (__bridge void *)callback, AFNetworkReachabilityRetainCallback, AFNetworkReachabilityReleaseCallback, NULL}; + SCNetworkReachabilitySetCallback((__bridge SCNetworkReachabilityRef)networkReachability, AFNetworkReachabilityCallback, &context); + SCNetworkReachabilityScheduleWithRunLoop((__bridge SCNetworkReachabilityRef)networkReachability, CFRunLoopGetMain(), kCFRunLoopCommonModes); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0),^{ + SCNetworkReachabilityFlags flags; + if (SCNetworkReachabilityGetFlags((__bridge SCNetworkReachabilityRef)networkReachability, &flags)) { + AFPostReachabilityStatusChange(flags, callback); + } + }); +} + +- (void)stopMonitoring { + if (!self.networkReachability) { + return; + } + + SCNetworkReachabilityUnscheduleFromRunLoop((__bridge SCNetworkReachabilityRef)self.networkReachability, CFRunLoopGetMain(), kCFRunLoopCommonModes); +} + +#pragma mark - + +- (NSString *)localizedNetworkReachabilityStatusString { + return AFStringFromNetworkReachabilityStatus(self.networkReachabilityStatus); +} + +#pragma mark - + +- (void)setReachabilityStatusChangeBlock:(void (^)(AFNetworkReachabilityStatus status))block { + self.networkReachabilityStatusBlock = block; +} + +#pragma mark - NSKeyValueObserving + ++ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key { + if ([key isEqualToString:@"reachable"] || [key isEqualToString:@"reachableViaWWAN"] || [key isEqualToString:@"reachableViaWiFi"]) { + return [NSSet setWithObject:@"networkReachabilityStatus"]; + } + + return [super keyPathsForValuesAffectingValueForKey:key]; +} + +@end +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFNetworking.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFNetworking.h new file mode 100644 index 0000000000..e2fb2f44e6 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFNetworking.h @@ -0,0 +1,41 @@ +// AFNetworking.h +// +// Copyright (c) 2013 AFNetworking (http://afnetworking.com/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import +#import +#import + +#ifndef _AFNETWORKING_ + #define _AFNETWORKING_ + + #import "AFURLRequestSerialization.h" + #import "AFURLResponseSerialization.h" + #import "AFSecurityPolicy.h" + +#if !TARGET_OS_WATCH + #import "AFNetworkReachabilityManager.h" +#endif + + #import "AFURLSessionManager.h" + #import "AFHTTPSessionManager.h" + +#endif /* _AFNETWORKING_ */ diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFSecurityPolicy.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFSecurityPolicy.h new file mode 100644 index 0000000000..90fa2129c3 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFSecurityPolicy.h @@ -0,0 +1,154 @@ +// AFSecurityPolicy.h +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import +#import + +typedef NS_ENUM(NSUInteger, AFSSLPinningMode) { + AFSSLPinningModeNone, + AFSSLPinningModePublicKey, + AFSSLPinningModeCertificate, +}; + +/** + `AFSecurityPolicy` evaluates server trust against pinned X.509 certificates and public keys over secure connections. + + Adding pinned SSL certificates to your app helps prevent man-in-the-middle attacks and other vulnerabilities. Applications dealing with sensitive customer data or financial information are strongly encouraged to route all communication over an HTTPS connection with SSL pinning configured and enabled. + */ + +NS_ASSUME_NONNULL_BEGIN + +@interface AFSecurityPolicy : NSObject + +/** + The criteria by which server trust should be evaluated against the pinned SSL certificates. Defaults to `AFSSLPinningModeNone`. + */ +@property (readonly, nonatomic, assign) AFSSLPinningMode SSLPinningMode; + +/** + The certificates used to evaluate server trust according to the SSL pinning mode. + + By default, this property is set to any (`.cer`) certificates included in the target compiling AFNetworking. Note that if you are using AFNetworking as embedded framework, no certificates will be pinned by default. Use `certificatesInBundle` to load certificates from your target, and then create a new policy by calling `policyWithPinningMode:withPinnedCertificates`. + + Note that if pinning is enabled, `evaluateServerTrust:forDomain:` will return true if any pinned certificate matches. + */ +@property (nonatomic, strong, nullable) NSSet *pinnedCertificates; + +/** + Whether or not to trust servers with an invalid or expired SSL certificates. Defaults to `NO`. + */ +@property (nonatomic, assign) BOOL allowInvalidCertificates; + +/** + Whether or not to validate the domain name in the certificate's CN field. Defaults to `YES`. + */ +@property (nonatomic, assign) BOOL validatesDomainName; + +///----------------------------------------- +/// @name Getting Certificates from the Bundle +///----------------------------------------- + +/** + Returns any certificates included in the bundle. If you are using AFNetworking as an embedded framework, you must use this method to find the certificates you have included in your app bundle, and use them when creating your security policy by calling `policyWithPinningMode:withPinnedCertificates`. + + @return The certificates included in the given bundle. + */ ++ (NSSet *)certificatesInBundle:(NSBundle *)bundle; + +///----------------------------------------- +/// @name Getting Specific Security Policies +///----------------------------------------- + +/** + Returns the shared default security policy, which does not allow invalid certificates, validates domain name, and does not validate against pinned certificates or public keys. + + @return The default security policy. + */ ++ (instancetype)defaultPolicy; + +///--------------------- +/// @name Initialization +///--------------------- + +/** + Creates and returns a security policy with the specified pinning mode. + + @param pinningMode The SSL pinning mode. + + @return A new security policy. + */ ++ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode; + +/** + Creates and returns a security policy with the specified pinning mode. + + @param pinningMode The SSL pinning mode. + @param pinnedCertificates The certificates to pin against. + + @return A new security policy. + */ ++ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode withPinnedCertificates:(NSSet *)pinnedCertificates; + +///------------------------------ +/// @name Evaluating Server Trust +///------------------------------ + +/** + Whether or not the specified server trust should be accepted, based on the security policy. + + This method should be used when responding to an authentication challenge from a server. + + @param serverTrust The X.509 certificate trust of the server. + @param domain The domain of serverTrust. If `nil`, the domain will not be validated. + + @return Whether or not to trust the server. + */ +- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust + forDomain:(nullable NSString *)domain; + +@end + +NS_ASSUME_NONNULL_END + +///---------------- +/// @name Constants +///---------------- + +/** + ## SSL Pinning Modes + + The following constants are provided by `AFSSLPinningMode` as possible SSL pinning modes. + + enum { + AFSSLPinningModeNone, + AFSSLPinningModePublicKey, + AFSSLPinningModeCertificate, + } + + `AFSSLPinningModeNone` + Do not used pinned certificates to validate servers. + + `AFSSLPinningModePublicKey` + Validate host certificates against public keys of pinned certificates. + + `AFSSLPinningModeCertificate` + Validate host certificates against pinned certificates. +*/ diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFSecurityPolicy.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFSecurityPolicy.m new file mode 100644 index 0000000000..3704cca0e1 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFSecurityPolicy.m @@ -0,0 +1,353 @@ +// AFSecurityPolicy.m +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "AFSecurityPolicy.h" + +#import + +#if !TARGET_OS_IOS && !TARGET_OS_WATCH && !TARGET_OS_TV +static NSData * AFSecKeyGetData(SecKeyRef key) { + CFDataRef data = NULL; + + __Require_noErr_Quiet(SecItemExport(key, kSecFormatUnknown, kSecItemPemArmour, NULL, &data), _out); + + return (__bridge_transfer NSData *)data; + +_out: + if (data) { + CFRelease(data); + } + + return nil; +} +#endif + +static BOOL AFSecKeyIsEqualToKey(SecKeyRef key1, SecKeyRef key2) { +#if TARGET_OS_IOS || TARGET_OS_WATCH || TARGET_OS_TV + return [(__bridge id)key1 isEqual:(__bridge id)key2]; +#else + return [AFSecKeyGetData(key1) isEqual:AFSecKeyGetData(key2)]; +#endif +} + +static id AFPublicKeyForCertificate(NSData *certificate) { + id allowedPublicKey = nil; + SecCertificateRef allowedCertificate; + SecCertificateRef allowedCertificates[1]; + CFArrayRef tempCertificates = nil; + SecPolicyRef policy = nil; + SecTrustRef allowedTrust = nil; + SecTrustResultType result; + + allowedCertificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificate); + __Require_Quiet(allowedCertificate != NULL, _out); + + allowedCertificates[0] = allowedCertificate; + tempCertificates = CFArrayCreate(NULL, (const void **)allowedCertificates, 1, NULL); + + policy = SecPolicyCreateBasicX509(); + __Require_noErr_Quiet(SecTrustCreateWithCertificates(tempCertificates, policy, &allowedTrust), _out); + __Require_noErr_Quiet(SecTrustEvaluate(allowedTrust, &result), _out); + + allowedPublicKey = (__bridge_transfer id)SecTrustCopyPublicKey(allowedTrust); + +_out: + if (allowedTrust) { + CFRelease(allowedTrust); + } + + if (policy) { + CFRelease(policy); + } + + if (tempCertificates) { + CFRelease(tempCertificates); + } + + if (allowedCertificate) { + CFRelease(allowedCertificate); + } + + return allowedPublicKey; +} + +static BOOL AFServerTrustIsValid(SecTrustRef serverTrust) { + BOOL isValid = NO; + SecTrustResultType result; + __Require_noErr_Quiet(SecTrustEvaluate(serverTrust, &result), _out); + + isValid = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed); + +_out: + return isValid; +} + +static NSArray * AFCertificateTrustChainForServerTrust(SecTrustRef serverTrust) { + CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust); + NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount]; + + for (CFIndex i = 0; i < certificateCount; i++) { + SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i); + [trustChain addObject:(__bridge_transfer NSData *)SecCertificateCopyData(certificate)]; + } + + return [NSArray arrayWithArray:trustChain]; +} + +static NSArray * AFPublicKeyTrustChainForServerTrust(SecTrustRef serverTrust) { + SecPolicyRef policy = SecPolicyCreateBasicX509(); + CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust); + NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount]; + for (CFIndex i = 0; i < certificateCount; i++) { + SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i); + + SecCertificateRef someCertificates[] = {certificate}; + CFArrayRef certificates = CFArrayCreate(NULL, (const void **)someCertificates, 1, NULL); + + SecTrustRef trust; + __Require_noErr_Quiet(SecTrustCreateWithCertificates(certificates, policy, &trust), _out); + + SecTrustResultType result; + __Require_noErr_Quiet(SecTrustEvaluate(trust, &result), _out); + + [trustChain addObject:(__bridge_transfer id)SecTrustCopyPublicKey(trust)]; + + _out: + if (trust) { + CFRelease(trust); + } + + if (certificates) { + CFRelease(certificates); + } + + continue; + } + CFRelease(policy); + + return [NSArray arrayWithArray:trustChain]; +} + +#pragma mark - + +@interface AFSecurityPolicy() +@property (readwrite, nonatomic, assign) AFSSLPinningMode SSLPinningMode; +@property (readwrite, nonatomic, strong) NSSet *pinnedPublicKeys; +@end + +@implementation AFSecurityPolicy + ++ (NSSet *)certificatesInBundle:(NSBundle *)bundle { + NSArray *paths = [bundle pathsForResourcesOfType:@"cer" inDirectory:@"."]; + + NSMutableSet *certificates = [NSMutableSet setWithCapacity:[paths count]]; + for (NSString *path in paths) { + NSData *certificateData = [NSData dataWithContentsOfFile:path]; + [certificates addObject:certificateData]; + } + + return [NSSet setWithSet:certificates]; +} + ++ (NSSet *)defaultPinnedCertificates { + static NSSet *_defaultPinnedCertificates = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSBundle *bundle = [NSBundle bundleForClass:[self class]]; + _defaultPinnedCertificates = [self certificatesInBundle:bundle]; + }); + + return _defaultPinnedCertificates; +} + ++ (instancetype)defaultPolicy { + AFSecurityPolicy *securityPolicy = [[self alloc] init]; + securityPolicy.SSLPinningMode = AFSSLPinningModeNone; + + return securityPolicy; +} + ++ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode { + return [self policyWithPinningMode:pinningMode withPinnedCertificates:[self defaultPinnedCertificates]]; +} + ++ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode withPinnedCertificates:(NSSet *)pinnedCertificates { + AFSecurityPolicy *securityPolicy = [[self alloc] init]; + securityPolicy.SSLPinningMode = pinningMode; + + [securityPolicy setPinnedCertificates:pinnedCertificates]; + + return securityPolicy; +} + +- (instancetype)init { + self = [super init]; + if (!self) { + return nil; + } + + self.validatesDomainName = YES; + + return self; +} + +- (void)setPinnedCertificates:(NSSet *)pinnedCertificates { + _pinnedCertificates = pinnedCertificates; + + if (self.pinnedCertificates) { + NSMutableSet *mutablePinnedPublicKeys = [NSMutableSet setWithCapacity:[self.pinnedCertificates count]]; + for (NSData *certificate in self.pinnedCertificates) { + id publicKey = AFPublicKeyForCertificate(certificate); + if (!publicKey) { + continue; + } + [mutablePinnedPublicKeys addObject:publicKey]; + } + self.pinnedPublicKeys = [NSSet setWithSet:mutablePinnedPublicKeys]; + } else { + self.pinnedPublicKeys = nil; + } +} + +#pragma mark - + +- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust + forDomain:(NSString *)domain +{ + if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.SSLPinningMode == AFSSLPinningModeNone || [self.pinnedCertificates count] == 0)) { + // https://developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/NetworkingTopics/Articles/OverridingSSLChainValidationCorrectly.html + // According to the docs, you should only trust your provided certs for evaluation. + // Pinned certificates are added to the trust. Without pinned certificates, + // there is nothing to evaluate against. + // + // From Apple Docs: + // "Do not implicitly trust self-signed certificates as anchors (kSecTrustOptionImplicitAnchors). + // Instead, add your own (self-signed) CA certificate to the list of trusted anchors." + NSLog(@"In order to validate a domain name for self signed certificates, you MUST use pinning."); + return NO; + } + + NSMutableArray *policies = [NSMutableArray array]; + if (self.validatesDomainName) { + [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)]; + } else { + [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()]; + } + + SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies); + + if (self.SSLPinningMode == AFSSLPinningModeNone) { + return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust); + } else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) { + return NO; + } + + switch (self.SSLPinningMode) { + case AFSSLPinningModeNone: + default: + return NO; + case AFSSLPinningModeCertificate: { + NSMutableArray *pinnedCertificates = [NSMutableArray array]; + for (NSData *certificateData in self.pinnedCertificates) { + [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)]; + } + SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates); + + if (!AFServerTrustIsValid(serverTrust)) { + return NO; + } + + // obtain the chain after being validated, which *should* contain the pinned certificate in the last position (if it's the Root CA) + NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust); + + for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) { + if ([self.pinnedCertificates containsObject:trustChainCertificate]) { + return YES; + } + } + + return NO; + } + case AFSSLPinningModePublicKey: { + NSUInteger trustedPublicKeyCount = 0; + NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust); + + for (id trustChainPublicKey in publicKeys) { + for (id pinnedPublicKey in self.pinnedPublicKeys) { + if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) { + trustedPublicKeyCount += 1; + } + } + } + return trustedPublicKeyCount > 0; + } + } + + return NO; +} + +#pragma mark - NSKeyValueObserving + ++ (NSSet *)keyPathsForValuesAffectingPinnedPublicKeys { + return [NSSet setWithObject:@"pinnedCertificates"]; +} + +#pragma mark - NSSecureCoding + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (instancetype)initWithCoder:(NSCoder *)decoder { + + self = [self init]; + if (!self) { + return nil; + } + + self.SSLPinningMode = [[decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(SSLPinningMode))] unsignedIntegerValue]; + self.allowInvalidCertificates = [decoder decodeBoolForKey:NSStringFromSelector(@selector(allowInvalidCertificates))]; + self.validatesDomainName = [decoder decodeBoolForKey:NSStringFromSelector(@selector(validatesDomainName))]; + self.pinnedCertificates = [decoder decodeObjectOfClass:[NSArray class] forKey:NSStringFromSelector(@selector(pinnedCertificates))]; + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:[NSNumber numberWithUnsignedInteger:self.SSLPinningMode] forKey:NSStringFromSelector(@selector(SSLPinningMode))]; + [coder encodeBool:self.allowInvalidCertificates forKey:NSStringFromSelector(@selector(allowInvalidCertificates))]; + [coder encodeBool:self.validatesDomainName forKey:NSStringFromSelector(@selector(validatesDomainName))]; + [coder encodeObject:self.pinnedCertificates forKey:NSStringFromSelector(@selector(pinnedCertificates))]; +} + +#pragma mark - NSCopying + +- (instancetype)copyWithZone:(NSZone *)zone { + AFSecurityPolicy *securityPolicy = [[[self class] allocWithZone:zone] init]; + securityPolicy.SSLPinningMode = self.SSLPinningMode; + securityPolicy.allowInvalidCertificates = self.allowInvalidCertificates; + securityPolicy.validatesDomainName = self.validatesDomainName; + securityPolicy.pinnedCertificates = [self.pinnedCertificates copyWithZone:zone]; + + return securityPolicy; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFURLRequestSerialization.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFURLRequestSerialization.h new file mode 100644 index 0000000000..134b7dd705 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFURLRequestSerialization.h @@ -0,0 +1,454 @@ +// AFURLRequestSerialization.h +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import +#import + +#if TARGET_OS_IOS || TARGET_OS_TV +#import +#elif TARGET_OS_WATCH +#import +#endif + +NS_ASSUME_NONNULL_BEGIN + +/** + The `AFURLRequestSerialization` protocol is adopted by an object that encodes parameters for a specified HTTP requests. Request serializers may encode parameters as query strings, HTTP bodies, setting the appropriate HTTP header fields as necessary. + + For example, a JSON request serializer may set the HTTP body of the request to a JSON representation, and set the `Content-Type` HTTP header field value to `application/json`. + */ +@protocol AFURLRequestSerialization + +/** + Returns a request with the specified parameters encoded into a copy of the original request. + + @param request The original request. + @param parameters The parameters to be encoded. + @param error The error that occurred while attempting to encode the request parameters. + + @return A serialized request. + */ +- (nullable NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request + withParameters:(nullable id)parameters + error:(NSError * _Nullable __autoreleasing *)error NS_SWIFT_NOTHROW; + +@end + +#pragma mark - + +/** + + */ +typedef NS_ENUM(NSUInteger, AFHTTPRequestQueryStringSerializationStyle) { + AFHTTPRequestQueryStringDefaultStyle = 0, +}; + +@protocol AFMultipartFormData; + +/** + `AFHTTPRequestSerializer` conforms to the `AFURLRequestSerialization` & `AFURLResponseSerialization` protocols, offering a concrete base implementation of query string / URL form-encoded parameter serialization and default request headers, as well as response status code and content type validation. + + Any request or response serializer dealing with HTTP is encouraged to subclass `AFHTTPRequestSerializer` in order to ensure consistent default behavior. + */ +@interface AFHTTPRequestSerializer : NSObject + +/** + The string encoding used to serialize parameters. `NSUTF8StringEncoding` by default. + */ +@property (nonatomic, assign) NSStringEncoding stringEncoding; + +/** + Whether created requests can use the device’s cellular radio (if present). `YES` by default. + + @see NSMutableURLRequest -setAllowsCellularAccess: + */ +@property (nonatomic, assign) BOOL allowsCellularAccess; + +/** + The cache policy of created requests. `NSURLRequestUseProtocolCachePolicy` by default. + + @see NSMutableURLRequest -setCachePolicy: + */ +@property (nonatomic, assign) NSURLRequestCachePolicy cachePolicy; + +/** + Whether created requests should use the default cookie handling. `YES` by default. + + @see NSMutableURLRequest -setHTTPShouldHandleCookies: + */ +@property (nonatomic, assign) BOOL HTTPShouldHandleCookies; + +/** + Whether created requests can continue transmitting data before receiving a response from an earlier transmission. `NO` by default + + @see NSMutableURLRequest -setHTTPShouldUsePipelining: + */ +@property (nonatomic, assign) BOOL HTTPShouldUsePipelining; + +/** + The network service type for created requests. `NSURLNetworkServiceTypeDefault` by default. + + @see NSMutableURLRequest -setNetworkServiceType: + */ +@property (nonatomic, assign) NSURLRequestNetworkServiceType networkServiceType; + +/** + The timeout interval, in seconds, for created requests. The default timeout interval is 60 seconds. + + @see NSMutableURLRequest -setTimeoutInterval: + */ +@property (nonatomic, assign) NSTimeInterval timeoutInterval; + +///--------------------------------------- +/// @name Configuring HTTP Request Headers +///--------------------------------------- + +/** + Default HTTP header field values to be applied to serialized requests. By default, these include the following: + + - `Accept-Language` with the contents of `NSLocale +preferredLanguages` + - `User-Agent` with the contents of various bundle identifiers and OS designations + + @discussion To add or remove default request headers, use `setValue:forHTTPHeaderField:`. + */ +@property (readonly, nonatomic, strong) NSDictionary *HTTPRequestHeaders; + +/** + Creates and returns a serializer with default configuration. + */ ++ (instancetype)serializer; + +/** + Sets the value for the HTTP headers set in request objects made by the HTTP client. If `nil`, removes the existing value for that header. + + @param field The HTTP header to set a default value for + @param value The value set as default for the specified header, or `nil` + */ +- (void)setValue:(nullable NSString *)value +forHTTPHeaderField:(NSString *)field; + +/** + Returns the value for the HTTP headers set in the request serializer. + + @param field The HTTP header to retrieve the default value for + + @return The value set as default for the specified header, or `nil` + */ +- (nullable NSString *)valueForHTTPHeaderField:(NSString *)field; + +/** + Sets the "Authorization" HTTP header set in request objects made by the HTTP client to a basic authentication value with Base64-encoded username and password. This overwrites any existing value for this header. + + @param username The HTTP basic auth username + @param password The HTTP basic auth password + */ +- (void)setAuthorizationHeaderFieldWithUsername:(NSString *)username + password:(NSString *)password; + +/** + Clears any existing value for the "Authorization" HTTP header. + */ +- (void)clearAuthorizationHeader; + +///------------------------------------------------------- +/// @name Configuring Query String Parameter Serialization +///------------------------------------------------------- + +/** + HTTP methods for which serialized requests will encode parameters as a query string. `GET`, `HEAD`, and `DELETE` by default. + */ +@property (nonatomic, strong) NSSet *HTTPMethodsEncodingParametersInURI; + +/** + Set the method of query string serialization according to one of the pre-defined styles. + + @param style The serialization style. + + @see AFHTTPRequestQueryStringSerializationStyle + */ +- (void)setQueryStringSerializationWithStyle:(AFHTTPRequestQueryStringSerializationStyle)style; + +/** + Set the a custom method of query string serialization according to the specified block. + + @param block A block that defines a process of encoding parameters into a query string. This block returns the query string and takes three arguments: the request, the parameters to encode, and the error that occurred when attempting to encode parameters for the given request. + */ +- (void)setQueryStringSerializationWithBlock:(nullable NSString * (^)(NSURLRequest *request, id parameters, NSError * __autoreleasing *error))block; + +///------------------------------- +/// @name Creating Request Objects +///------------------------------- + +/** + Creates an `NSMutableURLRequest` object with the specified HTTP method and URL string. + + If the HTTP method is `GET`, `HEAD`, or `DELETE`, the parameters will be used to construct a url-encoded query string that is appended to the request's URL. Otherwise, the parameters will be encoded according to the value of the `parameterEncoding` property, and set as the request body. + + @param method The HTTP method for the request, such as `GET`, `POST`, `PUT`, or `DELETE`. This parameter must not be `nil`. + @param URLString The URL string used to create the request URL. + @param parameters The parameters to be either set as a query string for `GET` requests, or the request HTTP body. + @param error The error that occurred while constructing the request. + + @return An `NSMutableURLRequest` object. + */ +- (NSMutableURLRequest *)requestWithMethod:(NSString *)method + URLString:(NSString *)URLString + parameters:(nullable id)parameters + error:(NSError * _Nullable __autoreleasing *)error; + +/** + Creates an `NSMutableURLRequest` object with the specified HTTP method and URLString, and constructs a `multipart/form-data` HTTP body, using the specified parameters and multipart form data block. See http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.2 + + Multipart form requests are automatically streamed, reading files directly from disk along with in-memory data in a single HTTP body. The resulting `NSMutableURLRequest` object has an `HTTPBodyStream` property, so refrain from setting `HTTPBodyStream` or `HTTPBody` on this request object, as it will clear out the multipart form body stream. + + @param method The HTTP method for the request. This parameter must not be `GET` or `HEAD`, or `nil`. + @param URLString The URL string used to create the request URL. + @param parameters The parameters to be encoded and set in the request HTTP body. + @param block A block that takes a single argument and appends data to the HTTP body. The block argument is an object adopting the `AFMultipartFormData` protocol. + @param error The error that occurred while constructing the request. + + @return An `NSMutableURLRequest` object + */ +- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method + URLString:(NSString *)URLString + parameters:(nullable NSDictionary *)parameters + constructingBodyWithBlock:(nullable void (^)(id formData))block + error:(NSError * _Nullable __autoreleasing *)error; + +/** + Creates an `NSMutableURLRequest` by removing the `HTTPBodyStream` from a request, and asynchronously writing its contents into the specified file, invoking the completion handler when finished. + + @param request The multipart form request. The `HTTPBodyStream` property of `request` must not be `nil`. + @param fileURL The file URL to write multipart form contents to. + @param handler A handler block to execute. + + @discussion There is a bug in `NSURLSessionTask` that causes requests to not send a `Content-Length` header when streaming contents from an HTTP body, which is notably problematic when interacting with the Amazon S3 webservice. As a workaround, this method takes a request constructed with `multipartFormRequestWithMethod:URLString:parameters:constructingBodyWithBlock:error:`, or any other request with an `HTTPBodyStream`, writes the contents to the specified file and returns a copy of the original request with the `HTTPBodyStream` property set to `nil`. From here, the file can either be passed to `AFURLSessionManager -uploadTaskWithRequest:fromFile:progress:completionHandler:`, or have its contents read into an `NSData` that's assigned to the `HTTPBody` property of the request. + + @see https://github.com/AFNetworking/AFNetworking/issues/1398 + */ +- (NSMutableURLRequest *)requestWithMultipartFormRequest:(NSURLRequest *)request + writingStreamContentsToFile:(NSURL *)fileURL + completionHandler:(nullable void (^)(NSError * _Nullable error))handler; + +@end + +#pragma mark - + +/** + The `AFMultipartFormData` protocol defines the methods supported by the parameter in the block argument of `AFHTTPRequestSerializer -multipartFormRequestWithMethod:URLString:parameters:constructingBodyWithBlock:`. + */ +@protocol AFMultipartFormData + +/** + Appends the HTTP header `Content-Disposition: file; filename=#{generated filename}; name=#{name}"` and `Content-Type: #{generated mimeType}`, followed by the encoded file data and the multipart form boundary. + + The filename and MIME type for this data in the form will be automatically generated, using the last path component of the `fileURL` and system associated MIME type for the `fileURL` extension, respectively. + + @param fileURL The URL corresponding to the file whose content will be appended to the form. This parameter must not be `nil`. + @param name The name to be associated with the specified data. This parameter must not be `nil`. + @param error If an error occurs, upon return contains an `NSError` object that describes the problem. + + @return `YES` if the file data was successfully appended, otherwise `NO`. + */ +- (BOOL)appendPartWithFileURL:(NSURL *)fileURL + name:(NSString *)name + error:(NSError * _Nullable __autoreleasing *)error; + +/** + Appends the HTTP header `Content-Disposition: file; filename=#{filename}; name=#{name}"` and `Content-Type: #{mimeType}`, followed by the encoded file data and the multipart form boundary. + + @param fileURL The URL corresponding to the file whose content will be appended to the form. This parameter must not be `nil`. + @param name The name to be associated with the specified data. This parameter must not be `nil`. + @param fileName The file name to be used in the `Content-Disposition` header. This parameter must not be `nil`. + @param mimeType The declared MIME type of the file data. This parameter must not be `nil`. + @param error If an error occurs, upon return contains an `NSError` object that describes the problem. + + @return `YES` if the file data was successfully appended otherwise `NO`. + */ +- (BOOL)appendPartWithFileURL:(NSURL *)fileURL + name:(NSString *)name + fileName:(NSString *)fileName + mimeType:(NSString *)mimeType + error:(NSError * _Nullable __autoreleasing *)error; + +/** + Appends the HTTP header `Content-Disposition: file; filename=#{filename}; name=#{name}"` and `Content-Type: #{mimeType}`, followed by the data from the input stream and the multipart form boundary. + + @param inputStream The input stream to be appended to the form data + @param name The name to be associated with the specified input stream. This parameter must not be `nil`. + @param fileName The filename to be associated with the specified input stream. This parameter must not be `nil`. + @param length The length of the specified input stream in bytes. + @param mimeType The MIME type of the specified data. (For example, the MIME type for a JPEG image is image/jpeg.) For a list of valid MIME types, see http://www.iana.org/assignments/media-types/. This parameter must not be `nil`. + */ +- (void)appendPartWithInputStream:(nullable NSInputStream *)inputStream + name:(NSString *)name + fileName:(NSString *)fileName + length:(int64_t)length + mimeType:(NSString *)mimeType; + +/** + Appends the HTTP header `Content-Disposition: file; filename=#{filename}; name=#{name}"` and `Content-Type: #{mimeType}`, followed by the encoded file data and the multipart form boundary. + + @param data The data to be encoded and appended to the form data. + @param name The name to be associated with the specified data. This parameter must not be `nil`. + @param fileName The filename to be associated with the specified data. This parameter must not be `nil`. + @param mimeType The MIME type of the specified data. (For example, the MIME type for a JPEG image is image/jpeg.) For a list of valid MIME types, see http://www.iana.org/assignments/media-types/. This parameter must not be `nil`. + */ +- (void)appendPartWithFileData:(NSData *)data + name:(NSString *)name + fileName:(NSString *)fileName + mimeType:(NSString *)mimeType; + +/** + Appends the HTTP headers `Content-Disposition: form-data; name=#{name}"`, followed by the encoded data and the multipart form boundary. + + @param data The data to be encoded and appended to the form data. + @param name The name to be associated with the specified data. This parameter must not be `nil`. + */ + +- (void)appendPartWithFormData:(NSData *)data + name:(NSString *)name; + + +/** + Appends HTTP headers, followed by the encoded data and the multipart form boundary. + + @param headers The HTTP headers to be appended to the form data. + @param body The data to be encoded and appended to the form data. This parameter must not be `nil`. + */ +- (void)appendPartWithHeaders:(nullable NSDictionary *)headers + body:(NSData *)body; + +/** + Throttles request bandwidth by limiting the packet size and adding a delay for each chunk read from the upload stream. + + When uploading over a 3G or EDGE connection, requests may fail with "request body stream exhausted". Setting a maximum packet size and delay according to the recommended values (`kAFUploadStream3GSuggestedPacketSize` and `kAFUploadStream3GSuggestedDelay`) lowers the risk of the input stream exceeding its allocated bandwidth. Unfortunately, there is no definite way to distinguish between a 3G, EDGE, or LTE connection over `NSURLConnection`. As such, it is not recommended that you throttle bandwidth based solely on network reachability. Instead, you should consider checking for the "request body stream exhausted" in a failure block, and then retrying the request with throttled bandwidth. + + @param numberOfBytes Maximum packet size, in number of bytes. The default packet size for an input stream is 16kb. + @param delay Duration of delay each time a packet is read. By default, no delay is set. + */ +- (void)throttleBandwidthWithPacketSize:(NSUInteger)numberOfBytes + delay:(NSTimeInterval)delay; + +@end + +#pragma mark - + +/** + `AFJSONRequestSerializer` is a subclass of `AFHTTPRequestSerializer` that encodes parameters as JSON using `NSJSONSerialization`, setting the `Content-Type` of the encoded request to `application/json`. + */ +@interface AFJSONRequestSerializer : AFHTTPRequestSerializer + +/** + Options for writing the request JSON data from Foundation objects. For possible values, see the `NSJSONSerialization` documentation section "NSJSONWritingOptions". `0` by default. + */ +@property (nonatomic, assign) NSJSONWritingOptions writingOptions; + +/** + Creates and returns a JSON serializer with specified reading and writing options. + + @param writingOptions The specified JSON writing options. + */ ++ (instancetype)serializerWithWritingOptions:(NSJSONWritingOptions)writingOptions; + +@end + +#pragma mark - + +/** + `AFPropertyListRequestSerializer` is a subclass of `AFHTTPRequestSerializer` that encodes parameters as JSON using `NSPropertyListSerializer`, setting the `Content-Type` of the encoded request to `application/x-plist`. + */ +@interface AFPropertyListRequestSerializer : AFHTTPRequestSerializer + +/** + The property list format. Possible values are described in "NSPropertyListFormat". + */ +@property (nonatomic, assign) NSPropertyListFormat format; + +/** + @warning The `writeOptions` property is currently unused. + */ +@property (nonatomic, assign) NSPropertyListWriteOptions writeOptions; + +/** + Creates and returns a property list serializer with a specified format, read options, and write options. + + @param format The property list format. + @param writeOptions The property list write options. + + @warning The `writeOptions` property is currently unused. + */ ++ (instancetype)serializerWithFormat:(NSPropertyListFormat)format + writeOptions:(NSPropertyListWriteOptions)writeOptions; + +@end + +#pragma mark - + +///---------------- +/// @name Constants +///---------------- + +/** + ## Error Domains + + The following error domain is predefined. + + - `NSString * const AFURLRequestSerializationErrorDomain` + + ### Constants + + `AFURLRequestSerializationErrorDomain` + AFURLRequestSerializer errors. Error codes for `AFURLRequestSerializationErrorDomain` correspond to codes in `NSURLErrorDomain`. + */ +FOUNDATION_EXPORT NSString * const AFURLRequestSerializationErrorDomain; + +/** + ## User info dictionary keys + + These keys may exist in the user info dictionary, in addition to those defined for NSError. + + - `NSString * const AFNetworkingOperationFailingURLRequestErrorKey` + + ### Constants + + `AFNetworkingOperationFailingURLRequestErrorKey` + The corresponding value is an `NSURLRequest` containing the request of the operation associated with an error. This key is only present in the `AFURLRequestSerializationErrorDomain`. + */ +FOUNDATION_EXPORT NSString * const AFNetworkingOperationFailingURLRequestErrorKey; + +/** + ## Throttling Bandwidth for HTTP Request Input Streams + + @see -throttleBandwidthWithPacketSize:delay: + + ### Constants + + `kAFUploadStream3GSuggestedPacketSize` + Maximum packet size, in number of bytes. Equal to 16kb. + + `kAFUploadStream3GSuggestedDelay` + Duration of delay each time a packet is read. Equal to 0.2 seconds. + */ +FOUNDATION_EXPORT NSUInteger const kAFUploadStream3GSuggestedPacketSize; +FOUNDATION_EXPORT NSTimeInterval const kAFUploadStream3GSuggestedDelay; + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFURLRequestSerialization.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFURLRequestSerialization.m new file mode 100644 index 0000000000..bbab7c48b9 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFURLRequestSerialization.m @@ -0,0 +1,1376 @@ +// AFURLRequestSerialization.m +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "AFURLRequestSerialization.h" + +#if TARGET_OS_IOS || TARGET_OS_WATCH || TARGET_OS_TV +#import +#else +#import +#endif + +NSString * const AFURLRequestSerializationErrorDomain = @"com.alamofire.error.serialization.request"; +NSString * const AFNetworkingOperationFailingURLRequestErrorKey = @"com.alamofire.serialization.request.error.response"; + +typedef NSString * (^AFQueryStringSerializationBlock)(NSURLRequest *request, id parameters, NSError *__autoreleasing *error); + +/** + Returns a percent-escaped string following RFC 3986 for a query string key or value. + RFC 3986 states that the following characters are "reserved" characters. + - General Delimiters: ":", "#", "[", "]", "@", "?", "/" + - Sub-Delimiters: "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "=" + + In RFC 3986 - Section 3.4, it states that the "?" and "/" characters should not be escaped to allow + query strings to include a URL. Therefore, all "reserved" characters with the exception of "?" and "/" + should be percent-escaped in the query string. + - parameter string: The string to be percent-escaped. + - returns: The percent-escaped string. + */ +static NSString * AFPercentEscapedStringFromString(NSString *string) { + static NSString * const kAFCharactersGeneralDelimitersToEncode = @":#[]@"; // does not include "?" or "/" due to RFC 3986 - Section 3.4 + static NSString * const kAFCharactersSubDelimitersToEncode = @"!$&'()*+,;="; + + NSMutableCharacterSet * allowedCharacterSet = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy]; + [allowedCharacterSet removeCharactersInString:[kAFCharactersGeneralDelimitersToEncode stringByAppendingString:kAFCharactersSubDelimitersToEncode]]; + + // FIXME: https://github.com/AFNetworking/AFNetworking/pull/3028 + // return [string stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet]; + + static NSUInteger const batchSize = 50; + + NSUInteger index = 0; + NSMutableString *escaped = @"".mutableCopy; + + while (index < string.length) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wgnu" + NSUInteger length = MIN(string.length - index, batchSize); +#pragma GCC diagnostic pop + NSRange range = NSMakeRange(index, length); + + // To avoid breaking up character sequences such as 👴🏻👮🏽 + range = [string rangeOfComposedCharacterSequencesForRange:range]; + + NSString *substring = [string substringWithRange:range]; + NSString *encoded = [substring stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet]; + [escaped appendString:encoded]; + + index += range.length; + } + + return escaped; +} + +#pragma mark - + +@interface AFQueryStringPair : NSObject +@property (readwrite, nonatomic, strong) id field; +@property (readwrite, nonatomic, strong) id value; + +- (instancetype)initWithField:(id)field value:(id)value; + +- (NSString *)URLEncodedStringValue; +@end + +@implementation AFQueryStringPair + +- (instancetype)initWithField:(id)field value:(id)value { + self = [super init]; + if (!self) { + return nil; + } + + self.field = field; + self.value = value; + + return self; +} + +- (NSString *)URLEncodedStringValue { + if (!self.value || [self.value isEqual:[NSNull null]]) { + return AFPercentEscapedStringFromString([self.field description]); + } else { + return [NSString stringWithFormat:@"%@=%@", AFPercentEscapedStringFromString([self.field description]), AFPercentEscapedStringFromString([self.value description])]; + } +} + +@end + +#pragma mark - + +FOUNDATION_EXPORT NSArray * AFQueryStringPairsFromDictionary(NSDictionary *dictionary); +FOUNDATION_EXPORT NSArray * AFQueryStringPairsFromKeyAndValue(NSString *key, id value); + +static NSString * AFQueryStringFromParameters(NSDictionary *parameters) { + NSMutableArray *mutablePairs = [NSMutableArray array]; + for (AFQueryStringPair *pair in AFQueryStringPairsFromDictionary(parameters)) { + [mutablePairs addObject:[pair URLEncodedStringValue]]; + } + + return [mutablePairs componentsJoinedByString:@"&"]; +} + +NSArray * AFQueryStringPairsFromDictionary(NSDictionary *dictionary) { + return AFQueryStringPairsFromKeyAndValue(nil, dictionary); +} + +NSArray * AFQueryStringPairsFromKeyAndValue(NSString *key, id value) { + NSMutableArray *mutableQueryStringComponents = [NSMutableArray array]; + + NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"description" ascending:YES selector:@selector(compare:)]; + + if ([value isKindOfClass:[NSDictionary class]]) { + NSDictionary *dictionary = value; + // Sort dictionary keys to ensure consistent ordering in query string, which is important when deserializing potentially ambiguous sequences, such as an array of dictionaries + for (id nestedKey in [dictionary.allKeys sortedArrayUsingDescriptors:@[ sortDescriptor ]]) { + id nestedValue = dictionary[nestedKey]; + if (nestedValue) { + [mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue((key ? [NSString stringWithFormat:@"%@[%@]", key, nestedKey] : nestedKey), nestedValue)]; + } + } + } else if ([value isKindOfClass:[NSArray class]]) { + NSArray *array = value; + for (id nestedValue in array) { + [mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue([NSString stringWithFormat:@"%@[]", key], nestedValue)]; + } + } else if ([value isKindOfClass:[NSSet class]]) { + NSSet *set = value; + for (id obj in [set sortedArrayUsingDescriptors:@[ sortDescriptor ]]) { + [mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue(key, obj)]; + } + } else { + [mutableQueryStringComponents addObject:[[AFQueryStringPair alloc] initWithField:key value:value]]; + } + + return mutableQueryStringComponents; +} + +#pragma mark - + +@interface AFStreamingMultipartFormData : NSObject +- (instancetype)initWithURLRequest:(NSMutableURLRequest *)urlRequest + stringEncoding:(NSStringEncoding)encoding; + +- (NSMutableURLRequest *)requestByFinalizingMultipartFormData; +@end + +#pragma mark - + +static NSArray * AFHTTPRequestSerializerObservedKeyPaths() { + static NSArray *_AFHTTPRequestSerializerObservedKeyPaths = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _AFHTTPRequestSerializerObservedKeyPaths = @[NSStringFromSelector(@selector(allowsCellularAccess)), NSStringFromSelector(@selector(cachePolicy)), NSStringFromSelector(@selector(HTTPShouldHandleCookies)), NSStringFromSelector(@selector(HTTPShouldUsePipelining)), NSStringFromSelector(@selector(networkServiceType)), NSStringFromSelector(@selector(timeoutInterval))]; + }); + + return _AFHTTPRequestSerializerObservedKeyPaths; +} + +static void *AFHTTPRequestSerializerObserverContext = &AFHTTPRequestSerializerObserverContext; + +@interface AFHTTPRequestSerializer () +@property (readwrite, nonatomic, strong) NSMutableSet *mutableObservedChangedKeyPaths; +@property (readwrite, nonatomic, strong) NSMutableDictionary *mutableHTTPRequestHeaders; +@property (readwrite, nonatomic, assign) AFHTTPRequestQueryStringSerializationStyle queryStringSerializationStyle; +@property (readwrite, nonatomic, copy) AFQueryStringSerializationBlock queryStringSerialization; +@end + +@implementation AFHTTPRequestSerializer + ++ (instancetype)serializer { + return [[self alloc] init]; +} + +- (instancetype)init { + self = [super init]; + if (!self) { + return nil; + } + + self.stringEncoding = NSUTF8StringEncoding; + + self.mutableHTTPRequestHeaders = [NSMutableDictionary dictionary]; + + // Accept-Language HTTP Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4 + NSMutableArray *acceptLanguagesComponents = [NSMutableArray array]; + [[NSLocale preferredLanguages] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + float q = 1.0f - (idx * 0.1f); + [acceptLanguagesComponents addObject:[NSString stringWithFormat:@"%@;q=%0.1g", obj, q]]; + *stop = q <= 0.5f; + }]; + [self setValue:[acceptLanguagesComponents componentsJoinedByString:@", "] forHTTPHeaderField:@"Accept-Language"]; + + NSString *userAgent = nil; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgnu" +#if TARGET_OS_IOS + // User-Agent Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43 + userAgent = [NSString stringWithFormat:@"%@/%@ (%@; iOS %@; Scale/%0.2f)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[UIDevice currentDevice] model], [[UIDevice currentDevice] systemVersion], [[UIScreen mainScreen] scale]]; +#elif TARGET_OS_WATCH + // User-Agent Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43 + userAgent = [NSString stringWithFormat:@"%@/%@ (%@; watchOS %@; Scale/%0.2f)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[WKInterfaceDevice currentDevice] model], [[WKInterfaceDevice currentDevice] systemVersion], [[WKInterfaceDevice currentDevice] screenScale]]; +#elif defined(__MAC_OS_X_VERSION_MIN_REQUIRED) + userAgent = [NSString stringWithFormat:@"%@/%@ (Mac OS X %@)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[NSProcessInfo processInfo] operatingSystemVersionString]]; +#endif +#pragma clang diagnostic pop + if (userAgent) { + if (![userAgent canBeConvertedToEncoding:NSASCIIStringEncoding]) { + NSMutableString *mutableUserAgent = [userAgent mutableCopy]; + if (CFStringTransform((__bridge CFMutableStringRef)(mutableUserAgent), NULL, (__bridge CFStringRef)@"Any-Latin; Latin-ASCII; [:^ASCII:] Remove", false)) { + userAgent = mutableUserAgent; + } + } + [self setValue:userAgent forHTTPHeaderField:@"User-Agent"]; + } + + // HTTP Method Definitions; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html + self.HTTPMethodsEncodingParametersInURI = [NSSet setWithObjects:@"GET", @"HEAD", @"DELETE", nil]; + + self.mutableObservedChangedKeyPaths = [NSMutableSet set]; + for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) { + if ([self respondsToSelector:NSSelectorFromString(keyPath)]) { + [self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:AFHTTPRequestSerializerObserverContext]; + } + } + + return self; +} + +- (void)dealloc { + for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) { + if ([self respondsToSelector:NSSelectorFromString(keyPath)]) { + [self removeObserver:self forKeyPath:keyPath context:AFHTTPRequestSerializerObserverContext]; + } + } +} + +#pragma mark - + +// Workarounds for crashing behavior using Key-Value Observing with XCTest +// See https://github.com/AFNetworking/AFNetworking/issues/2523 + +- (void)setAllowsCellularAccess:(BOOL)allowsCellularAccess { + [self willChangeValueForKey:NSStringFromSelector(@selector(allowsCellularAccess))]; + _allowsCellularAccess = allowsCellularAccess; + [self didChangeValueForKey:NSStringFromSelector(@selector(allowsCellularAccess))]; +} + +- (void)setCachePolicy:(NSURLRequestCachePolicy)cachePolicy { + [self willChangeValueForKey:NSStringFromSelector(@selector(cachePolicy))]; + _cachePolicy = cachePolicy; + [self didChangeValueForKey:NSStringFromSelector(@selector(cachePolicy))]; +} + +- (void)setHTTPShouldHandleCookies:(BOOL)HTTPShouldHandleCookies { + [self willChangeValueForKey:NSStringFromSelector(@selector(HTTPShouldHandleCookies))]; + _HTTPShouldHandleCookies = HTTPShouldHandleCookies; + [self didChangeValueForKey:NSStringFromSelector(@selector(HTTPShouldHandleCookies))]; +} + +- (void)setHTTPShouldUsePipelining:(BOOL)HTTPShouldUsePipelining { + [self willChangeValueForKey:NSStringFromSelector(@selector(HTTPShouldUsePipelining))]; + _HTTPShouldUsePipelining = HTTPShouldUsePipelining; + [self didChangeValueForKey:NSStringFromSelector(@selector(HTTPShouldUsePipelining))]; +} + +- (void)setNetworkServiceType:(NSURLRequestNetworkServiceType)networkServiceType { + [self willChangeValueForKey:NSStringFromSelector(@selector(networkServiceType))]; + _networkServiceType = networkServiceType; + [self didChangeValueForKey:NSStringFromSelector(@selector(networkServiceType))]; +} + +- (void)setTimeoutInterval:(NSTimeInterval)timeoutInterval { + [self willChangeValueForKey:NSStringFromSelector(@selector(timeoutInterval))]; + _timeoutInterval = timeoutInterval; + [self didChangeValueForKey:NSStringFromSelector(@selector(timeoutInterval))]; +} + +#pragma mark - + +- (NSDictionary *)HTTPRequestHeaders { + return [NSDictionary dictionaryWithDictionary:self.mutableHTTPRequestHeaders]; +} + +- (void)setValue:(NSString *)value +forHTTPHeaderField:(NSString *)field +{ + [self.mutableHTTPRequestHeaders setValue:value forKey:field]; +} + +- (NSString *)valueForHTTPHeaderField:(NSString *)field { + return [self.mutableHTTPRequestHeaders valueForKey:field]; +} + +- (void)setAuthorizationHeaderFieldWithUsername:(NSString *)username + password:(NSString *)password +{ + NSData *basicAuthCredentials = [[NSString stringWithFormat:@"%@:%@", username, password] dataUsingEncoding:NSUTF8StringEncoding]; + NSString *base64AuthCredentials = [basicAuthCredentials base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]; + [self setValue:[NSString stringWithFormat:@"Basic %@", base64AuthCredentials] forHTTPHeaderField:@"Authorization"]; +} + +- (void)clearAuthorizationHeader { + [self.mutableHTTPRequestHeaders removeObjectForKey:@"Authorization"]; +} + +#pragma mark - + +- (void)setQueryStringSerializationWithStyle:(AFHTTPRequestQueryStringSerializationStyle)style { + self.queryStringSerializationStyle = style; + self.queryStringSerialization = nil; +} + +- (void)setQueryStringSerializationWithBlock:(NSString *(^)(NSURLRequest *, id, NSError *__autoreleasing *))block { + self.queryStringSerialization = block; +} + +#pragma mark - + +- (NSMutableURLRequest *)requestWithMethod:(NSString *)method + URLString:(NSString *)URLString + parameters:(id)parameters + error:(NSError *__autoreleasing *)error +{ + NSParameterAssert(method); + NSParameterAssert(URLString); + + NSURL *url = [NSURL URLWithString:URLString]; + + NSParameterAssert(url); + + NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url]; + mutableRequest.HTTPMethod = method; + + for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) { + if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) { + [mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath]; + } + } + + mutableRequest = [[self requestBySerializingRequest:mutableRequest withParameters:parameters error:error] mutableCopy]; + + return mutableRequest; +} + +- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method + URLString:(NSString *)URLString + parameters:(NSDictionary *)parameters + constructingBodyWithBlock:(void (^)(id formData))block + error:(NSError *__autoreleasing *)error +{ + NSParameterAssert(method); + NSParameterAssert(![method isEqualToString:@"GET"] && ![method isEqualToString:@"HEAD"]); + + NSMutableURLRequest *mutableRequest = [self requestWithMethod:method URLString:URLString parameters:nil error:error]; + + __block AFStreamingMultipartFormData *formData = [[AFStreamingMultipartFormData alloc] initWithURLRequest:mutableRequest stringEncoding:NSUTF8StringEncoding]; + + if (parameters) { + for (AFQueryStringPair *pair in AFQueryStringPairsFromDictionary(parameters)) { + NSData *data = nil; + if ([pair.value isKindOfClass:[NSData class]]) { + data = pair.value; + } else if ([pair.value isEqual:[NSNull null]]) { + data = [NSData data]; + } else { + data = [[pair.value description] dataUsingEncoding:self.stringEncoding]; + } + + if (data) { + [formData appendPartWithFormData:data name:[pair.field description]]; + } + } + } + + if (block) { + block(formData); + } + + return [formData requestByFinalizingMultipartFormData]; +} + +- (NSMutableURLRequest *)requestWithMultipartFormRequest:(NSURLRequest *)request + writingStreamContentsToFile:(NSURL *)fileURL + completionHandler:(void (^)(NSError *error))handler +{ + NSParameterAssert(request.HTTPBodyStream); + NSParameterAssert([fileURL isFileURL]); + + NSInputStream *inputStream = request.HTTPBodyStream; + NSOutputStream *outputStream = [[NSOutputStream alloc] initWithURL:fileURL append:NO]; + __block NSError *error = nil; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; + [outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; + + [inputStream open]; + [outputStream open]; + + while ([inputStream hasBytesAvailable] && [outputStream hasSpaceAvailable]) { + uint8_t buffer[1024]; + + NSInteger bytesRead = [inputStream read:buffer maxLength:1024]; + if (inputStream.streamError || bytesRead < 0) { + error = inputStream.streamError; + break; + } + + NSInteger bytesWritten = [outputStream write:buffer maxLength:(NSUInteger)bytesRead]; + if (outputStream.streamError || bytesWritten < 0) { + error = outputStream.streamError; + break; + } + + if (bytesRead == 0 && bytesWritten == 0) { + break; + } + } + + [outputStream close]; + [inputStream close]; + + if (handler) { + dispatch_async(dispatch_get_main_queue(), ^{ + handler(error); + }); + } + }); + + NSMutableURLRequest *mutableRequest = [request mutableCopy]; + mutableRequest.HTTPBodyStream = nil; + + return mutableRequest; +} + +#pragma mark - AFURLRequestSerialization + +- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request + withParameters:(id)parameters + error:(NSError *__autoreleasing *)error +{ + NSParameterAssert(request); + + NSMutableURLRequest *mutableRequest = [request mutableCopy]; + + [self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) { + if (![request valueForHTTPHeaderField:field]) { + [mutableRequest setValue:value forHTTPHeaderField:field]; + } + }]; + + NSString *query = nil; + if (parameters) { + if (self.queryStringSerialization) { + NSError *serializationError; + query = self.queryStringSerialization(request, parameters, &serializationError); + + if (serializationError) { + if (error) { + *error = serializationError; + } + + return nil; + } + } else { + switch (self.queryStringSerializationStyle) { + case AFHTTPRequestQueryStringDefaultStyle: + query = AFQueryStringFromParameters(parameters); + break; + } + } + } + + if ([self.HTTPMethodsEncodingParametersInURI containsObject:[[request HTTPMethod] uppercaseString]]) { + if (query) { + mutableRequest.URL = [NSURL URLWithString:[[mutableRequest.URL absoluteString] stringByAppendingFormat:mutableRequest.URL.query ? @"&%@" : @"?%@", query]]; + } + } else { + // #2864: an empty string is a valid x-www-form-urlencoded payload + if (!query) { + query = @""; + } + if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) { + [mutableRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"]; + } + [mutableRequest setHTTPBody:[query dataUsingEncoding:self.stringEncoding]]; + } + + return mutableRequest; +} + +#pragma mark - NSKeyValueObserving + ++ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key { + if ([AFHTTPRequestSerializerObservedKeyPaths() containsObject:key]) { + return NO; + } + + return [super automaticallyNotifiesObserversForKey:key]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(__unused id)object + change:(NSDictionary *)change + context:(void *)context +{ + if (context == AFHTTPRequestSerializerObserverContext) { + if ([change[NSKeyValueChangeNewKey] isEqual:[NSNull null]]) { + [self.mutableObservedChangedKeyPaths removeObject:keyPath]; + } else { + [self.mutableObservedChangedKeyPaths addObject:keyPath]; + } + } +} + +#pragma mark - NSSecureCoding + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (instancetype)initWithCoder:(NSCoder *)decoder { + self = [self init]; + if (!self) { + return nil; + } + + self.mutableHTTPRequestHeaders = [[decoder decodeObjectOfClass:[NSDictionary class] forKey:NSStringFromSelector(@selector(mutableHTTPRequestHeaders))] mutableCopy]; + self.queryStringSerializationStyle = (AFHTTPRequestQueryStringSerializationStyle)[[decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(queryStringSerializationStyle))] unsignedIntegerValue]; + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:self.mutableHTTPRequestHeaders forKey:NSStringFromSelector(@selector(mutableHTTPRequestHeaders))]; + [coder encodeInteger:self.queryStringSerializationStyle forKey:NSStringFromSelector(@selector(queryStringSerializationStyle))]; +} + +#pragma mark - NSCopying + +- (instancetype)copyWithZone:(NSZone *)zone { + AFHTTPRequestSerializer *serializer = [[[self class] allocWithZone:zone] init]; + serializer.mutableHTTPRequestHeaders = [self.mutableHTTPRequestHeaders mutableCopyWithZone:zone]; + serializer.queryStringSerializationStyle = self.queryStringSerializationStyle; + serializer.queryStringSerialization = self.queryStringSerialization; + + return serializer; +} + +@end + +#pragma mark - + +static NSString * AFCreateMultipartFormBoundary() { + return [NSString stringWithFormat:@"Boundary+%08X%08X", arc4random(), arc4random()]; +} + +static NSString * const kAFMultipartFormCRLF = @"\r\n"; + +static inline NSString * AFMultipartFormInitialBoundary(NSString *boundary) { + return [NSString stringWithFormat:@"--%@%@", boundary, kAFMultipartFormCRLF]; +} + +static inline NSString * AFMultipartFormEncapsulationBoundary(NSString *boundary) { + return [NSString stringWithFormat:@"%@--%@%@", kAFMultipartFormCRLF, boundary, kAFMultipartFormCRLF]; +} + +static inline NSString * AFMultipartFormFinalBoundary(NSString *boundary) { + return [NSString stringWithFormat:@"%@--%@--%@", kAFMultipartFormCRLF, boundary, kAFMultipartFormCRLF]; +} + +static inline NSString * AFContentTypeForPathExtension(NSString *extension) { + NSString *UTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)extension, NULL); + NSString *contentType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)UTI, kUTTagClassMIMEType); + if (!contentType) { + return @"application/octet-stream"; + } else { + return contentType; + } +} + +NSUInteger const kAFUploadStream3GSuggestedPacketSize = 1024 * 16; +NSTimeInterval const kAFUploadStream3GSuggestedDelay = 0.2; + +@interface AFHTTPBodyPart : NSObject +@property (nonatomic, assign) NSStringEncoding stringEncoding; +@property (nonatomic, strong) NSDictionary *headers; +@property (nonatomic, copy) NSString *boundary; +@property (nonatomic, strong) id body; +@property (nonatomic, assign) unsigned long long bodyContentLength; +@property (nonatomic, strong) NSInputStream *inputStream; + +@property (nonatomic, assign) BOOL hasInitialBoundary; +@property (nonatomic, assign) BOOL hasFinalBoundary; + +@property (readonly, nonatomic, assign, getter = hasBytesAvailable) BOOL bytesAvailable; +@property (readonly, nonatomic, assign) unsigned long long contentLength; + +- (NSInteger)read:(uint8_t *)buffer + maxLength:(NSUInteger)length; +@end + +@interface AFMultipartBodyStream : NSInputStream +@property (nonatomic, assign) NSUInteger numberOfBytesInPacket; +@property (nonatomic, assign) NSTimeInterval delay; +@property (nonatomic, strong) NSInputStream *inputStream; +@property (readonly, nonatomic, assign) unsigned long long contentLength; +@property (readonly, nonatomic, assign, getter = isEmpty) BOOL empty; + +- (instancetype)initWithStringEncoding:(NSStringEncoding)encoding; +- (void)setInitialAndFinalBoundaries; +- (void)appendHTTPBodyPart:(AFHTTPBodyPart *)bodyPart; +@end + +#pragma mark - + +@interface AFStreamingMultipartFormData () +@property (readwrite, nonatomic, copy) NSMutableURLRequest *request; +@property (readwrite, nonatomic, assign) NSStringEncoding stringEncoding; +@property (readwrite, nonatomic, copy) NSString *boundary; +@property (readwrite, nonatomic, strong) AFMultipartBodyStream *bodyStream; +@end + +@implementation AFStreamingMultipartFormData + +- (instancetype)initWithURLRequest:(NSMutableURLRequest *)urlRequest + stringEncoding:(NSStringEncoding)encoding +{ + self = [super init]; + if (!self) { + return nil; + } + + self.request = urlRequest; + self.stringEncoding = encoding; + self.boundary = AFCreateMultipartFormBoundary(); + self.bodyStream = [[AFMultipartBodyStream alloc] initWithStringEncoding:encoding]; + + return self; +} + +- (BOOL)appendPartWithFileURL:(NSURL *)fileURL + name:(NSString *)name + error:(NSError * __autoreleasing *)error +{ + NSParameterAssert(fileURL); + NSParameterAssert(name); + + NSString *fileName = [fileURL lastPathComponent]; + NSString *mimeType = AFContentTypeForPathExtension([fileURL pathExtension]); + + return [self appendPartWithFileURL:fileURL name:name fileName:fileName mimeType:mimeType error:error]; +} + +- (BOOL)appendPartWithFileURL:(NSURL *)fileURL + name:(NSString *)name + fileName:(NSString *)fileName + mimeType:(NSString *)mimeType + error:(NSError * __autoreleasing *)error +{ + NSParameterAssert(fileURL); + NSParameterAssert(name); + NSParameterAssert(fileName); + NSParameterAssert(mimeType); + + if (![fileURL isFileURL]) { + NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey: NSLocalizedStringFromTable(@"Expected URL to be a file URL", @"AFNetworking", nil)}; + if (error) { + *error = [[NSError alloc] initWithDomain:AFURLRequestSerializationErrorDomain code:NSURLErrorBadURL userInfo:userInfo]; + } + + return NO; + } else if ([fileURL checkResourceIsReachableAndReturnError:error] == NO) { + NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey: NSLocalizedStringFromTable(@"File URL not reachable.", @"AFNetworking", nil)}; + if (error) { + *error = [[NSError alloc] initWithDomain:AFURLRequestSerializationErrorDomain code:NSURLErrorBadURL userInfo:userInfo]; + } + + return NO; + } + + NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[fileURL path] error:error]; + if (!fileAttributes) { + return NO; + } + + NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary]; + [mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"; filename=\"%@\"", name, fileName] forKey:@"Content-Disposition"]; + [mutableHeaders setValue:mimeType forKey:@"Content-Type"]; + + AFHTTPBodyPart *bodyPart = [[AFHTTPBodyPart alloc] init]; + bodyPart.stringEncoding = self.stringEncoding; + bodyPart.headers = mutableHeaders; + bodyPart.boundary = self.boundary; + bodyPart.body = fileURL; + bodyPart.bodyContentLength = [fileAttributes[NSFileSize] unsignedLongLongValue]; + [self.bodyStream appendHTTPBodyPart:bodyPart]; + + return YES; +} + +- (void)appendPartWithInputStream:(NSInputStream *)inputStream + name:(NSString *)name + fileName:(NSString *)fileName + length:(int64_t)length + mimeType:(NSString *)mimeType +{ + NSParameterAssert(name); + NSParameterAssert(fileName); + NSParameterAssert(mimeType); + + NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary]; + [mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"; filename=\"%@\"", name, fileName] forKey:@"Content-Disposition"]; + [mutableHeaders setValue:mimeType forKey:@"Content-Type"]; + + AFHTTPBodyPart *bodyPart = [[AFHTTPBodyPart alloc] init]; + bodyPart.stringEncoding = self.stringEncoding; + bodyPart.headers = mutableHeaders; + bodyPart.boundary = self.boundary; + bodyPart.body = inputStream; + + bodyPart.bodyContentLength = (unsigned long long)length; + + [self.bodyStream appendHTTPBodyPart:bodyPart]; +} + +- (void)appendPartWithFileData:(NSData *)data + name:(NSString *)name + fileName:(NSString *)fileName + mimeType:(NSString *)mimeType +{ + NSParameterAssert(name); + NSParameterAssert(fileName); + NSParameterAssert(mimeType); + + NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary]; + [mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"; filename=\"%@\"", name, fileName] forKey:@"Content-Disposition"]; + [mutableHeaders setValue:mimeType forKey:@"Content-Type"]; + + [self appendPartWithHeaders:mutableHeaders body:data]; +} + +- (void)appendPartWithFormData:(NSData *)data + name:(NSString *)name +{ + NSParameterAssert(name); + + NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary]; + [mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"", name] forKey:@"Content-Disposition"]; + + [self appendPartWithHeaders:mutableHeaders body:data]; +} + +- (void)appendPartWithHeaders:(NSDictionary *)headers + body:(NSData *)body +{ + NSParameterAssert(body); + + AFHTTPBodyPart *bodyPart = [[AFHTTPBodyPart alloc] init]; + bodyPart.stringEncoding = self.stringEncoding; + bodyPart.headers = headers; + bodyPart.boundary = self.boundary; + bodyPart.bodyContentLength = [body length]; + bodyPart.body = body; + + [self.bodyStream appendHTTPBodyPart:bodyPart]; +} + +- (void)throttleBandwidthWithPacketSize:(NSUInteger)numberOfBytes + delay:(NSTimeInterval)delay +{ + self.bodyStream.numberOfBytesInPacket = numberOfBytes; + self.bodyStream.delay = delay; +} + +- (NSMutableURLRequest *)requestByFinalizingMultipartFormData { + if ([self.bodyStream isEmpty]) { + return self.request; + } + + // Reset the initial and final boundaries to ensure correct Content-Length + [self.bodyStream setInitialAndFinalBoundaries]; + [self.request setHTTPBodyStream:self.bodyStream]; + + [self.request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", self.boundary] forHTTPHeaderField:@"Content-Type"]; + [self.request setValue:[NSString stringWithFormat:@"%llu", [self.bodyStream contentLength]] forHTTPHeaderField:@"Content-Length"]; + + return self.request; +} + +@end + +#pragma mark - + +@interface NSStream () +@property (readwrite) NSStreamStatus streamStatus; +@property (readwrite, copy) NSError *streamError; +@end + +@interface AFMultipartBodyStream () +@property (readwrite, nonatomic, assign) NSStringEncoding stringEncoding; +@property (readwrite, nonatomic, strong) NSMutableArray *HTTPBodyParts; +@property (readwrite, nonatomic, strong) NSEnumerator *HTTPBodyPartEnumerator; +@property (readwrite, nonatomic, strong) AFHTTPBodyPart *currentHTTPBodyPart; +@property (readwrite, nonatomic, strong) NSOutputStream *outputStream; +@property (readwrite, nonatomic, strong) NSMutableData *buffer; +@end + +@implementation AFMultipartBodyStream +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wimplicit-atomic-properties" +#if (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000) || (defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 1100) +@synthesize delegate; +#endif +@synthesize streamStatus; +@synthesize streamError; +#pragma clang diagnostic pop + +- (instancetype)initWithStringEncoding:(NSStringEncoding)encoding { + self = [super init]; + if (!self) { + return nil; + } + + self.stringEncoding = encoding; + self.HTTPBodyParts = [NSMutableArray array]; + self.numberOfBytesInPacket = NSIntegerMax; + + return self; +} + +- (void)setInitialAndFinalBoundaries { + if ([self.HTTPBodyParts count] > 0) { + for (AFHTTPBodyPart *bodyPart in self.HTTPBodyParts) { + bodyPart.hasInitialBoundary = NO; + bodyPart.hasFinalBoundary = NO; + } + + [[self.HTTPBodyParts firstObject] setHasInitialBoundary:YES]; + [[self.HTTPBodyParts lastObject] setHasFinalBoundary:YES]; + } +} + +- (void)appendHTTPBodyPart:(AFHTTPBodyPart *)bodyPart { + [self.HTTPBodyParts addObject:bodyPart]; +} + +- (BOOL)isEmpty { + return [self.HTTPBodyParts count] == 0; +} + +#pragma mark - NSInputStream + +- (NSInteger)read:(uint8_t *)buffer + maxLength:(NSUInteger)length +{ + if ([self streamStatus] == NSStreamStatusClosed) { + return 0; + } + + NSInteger totalNumberOfBytesRead = 0; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgnu" + while ((NSUInteger)totalNumberOfBytesRead < MIN(length, self.numberOfBytesInPacket)) { + if (!self.currentHTTPBodyPart || ![self.currentHTTPBodyPart hasBytesAvailable]) { + if (!(self.currentHTTPBodyPart = [self.HTTPBodyPartEnumerator nextObject])) { + break; + } + } else { + NSUInteger maxLength = MIN(length, self.numberOfBytesInPacket) - (NSUInteger)totalNumberOfBytesRead; + NSInteger numberOfBytesRead = [self.currentHTTPBodyPart read:&buffer[totalNumberOfBytesRead] maxLength:maxLength]; + if (numberOfBytesRead == -1) { + self.streamError = self.currentHTTPBodyPart.inputStream.streamError; + break; + } else { + totalNumberOfBytesRead += numberOfBytesRead; + + if (self.delay > 0.0f) { + [NSThread sleepForTimeInterval:self.delay]; + } + } + } + } +#pragma clang diagnostic pop + + return totalNumberOfBytesRead; +} + +- (BOOL)getBuffer:(__unused uint8_t **)buffer + length:(__unused NSUInteger *)len +{ + return NO; +} + +- (BOOL)hasBytesAvailable { + return [self streamStatus] == NSStreamStatusOpen; +} + +#pragma mark - NSStream + +- (void)open { + if (self.streamStatus == NSStreamStatusOpen) { + return; + } + + self.streamStatus = NSStreamStatusOpen; + + [self setInitialAndFinalBoundaries]; + self.HTTPBodyPartEnumerator = [self.HTTPBodyParts objectEnumerator]; +} + +- (void)close { + self.streamStatus = NSStreamStatusClosed; +} + +- (id)propertyForKey:(__unused NSString *)key { + return nil; +} + +- (BOOL)setProperty:(__unused id)property + forKey:(__unused NSString *)key +{ + return NO; +} + +- (void)scheduleInRunLoop:(__unused NSRunLoop *)aRunLoop + forMode:(__unused NSString *)mode +{} + +- (void)removeFromRunLoop:(__unused NSRunLoop *)aRunLoop + forMode:(__unused NSString *)mode +{} + +- (unsigned long long)contentLength { + unsigned long long length = 0; + for (AFHTTPBodyPart *bodyPart in self.HTTPBodyParts) { + length += [bodyPart contentLength]; + } + + return length; +} + +#pragma mark - Undocumented CFReadStream Bridged Methods + +- (void)_scheduleInCFRunLoop:(__unused CFRunLoopRef)aRunLoop + forMode:(__unused CFStringRef)aMode +{} + +- (void)_unscheduleFromCFRunLoop:(__unused CFRunLoopRef)aRunLoop + forMode:(__unused CFStringRef)aMode +{} + +- (BOOL)_setCFClientFlags:(__unused CFOptionFlags)inFlags + callback:(__unused CFReadStreamClientCallBack)inCallback + context:(__unused CFStreamClientContext *)inContext { + return NO; +} + +#pragma mark - NSCopying + +- (instancetype)copyWithZone:(NSZone *)zone { + AFMultipartBodyStream *bodyStreamCopy = [[[self class] allocWithZone:zone] initWithStringEncoding:self.stringEncoding]; + + for (AFHTTPBodyPart *bodyPart in self.HTTPBodyParts) { + [bodyStreamCopy appendHTTPBodyPart:[bodyPart copy]]; + } + + [bodyStreamCopy setInitialAndFinalBoundaries]; + + return bodyStreamCopy; +} + +@end + +#pragma mark - + +typedef enum { + AFEncapsulationBoundaryPhase = 1, + AFHeaderPhase = 2, + AFBodyPhase = 3, + AFFinalBoundaryPhase = 4, +} AFHTTPBodyPartReadPhase; + +@interface AFHTTPBodyPart () { + AFHTTPBodyPartReadPhase _phase; + NSInputStream *_inputStream; + unsigned long long _phaseReadOffset; +} + +- (BOOL)transitionToNextPhase; +- (NSInteger)readData:(NSData *)data + intoBuffer:(uint8_t *)buffer + maxLength:(NSUInteger)length; +@end + +@implementation AFHTTPBodyPart + +- (instancetype)init { + self = [super init]; + if (!self) { + return nil; + } + + [self transitionToNextPhase]; + + return self; +} + +- (void)dealloc { + if (_inputStream) { + [_inputStream close]; + _inputStream = nil; + } +} + +- (NSInputStream *)inputStream { + if (!_inputStream) { + if ([self.body isKindOfClass:[NSData class]]) { + _inputStream = [NSInputStream inputStreamWithData:self.body]; + } else if ([self.body isKindOfClass:[NSURL class]]) { + _inputStream = [NSInputStream inputStreamWithURL:self.body]; + } else if ([self.body isKindOfClass:[NSInputStream class]]) { + _inputStream = self.body; + } else { + _inputStream = [NSInputStream inputStreamWithData:[NSData data]]; + } + } + + return _inputStream; +} + +- (NSString *)stringForHeaders { + NSMutableString *headerString = [NSMutableString string]; + for (NSString *field in [self.headers allKeys]) { + [headerString appendString:[NSString stringWithFormat:@"%@: %@%@", field, [self.headers valueForKey:field], kAFMultipartFormCRLF]]; + } + [headerString appendString:kAFMultipartFormCRLF]; + + return [NSString stringWithString:headerString]; +} + +- (unsigned long long)contentLength { + unsigned long long length = 0; + + NSData *encapsulationBoundaryData = [([self hasInitialBoundary] ? AFMultipartFormInitialBoundary(self.boundary) : AFMultipartFormEncapsulationBoundary(self.boundary)) dataUsingEncoding:self.stringEncoding]; + length += [encapsulationBoundaryData length]; + + NSData *headersData = [[self stringForHeaders] dataUsingEncoding:self.stringEncoding]; + length += [headersData length]; + + length += _bodyContentLength; + + NSData *closingBoundaryData = ([self hasFinalBoundary] ? [AFMultipartFormFinalBoundary(self.boundary) dataUsingEncoding:self.stringEncoding] : [NSData data]); + length += [closingBoundaryData length]; + + return length; +} + +- (BOOL)hasBytesAvailable { + // Allows `read:maxLength:` to be called again if `AFMultipartFormFinalBoundary` doesn't fit into the available buffer + if (_phase == AFFinalBoundaryPhase) { + return YES; + } + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wcovered-switch-default" + switch (self.inputStream.streamStatus) { + case NSStreamStatusNotOpen: + case NSStreamStatusOpening: + case NSStreamStatusOpen: + case NSStreamStatusReading: + case NSStreamStatusWriting: + return YES; + case NSStreamStatusAtEnd: + case NSStreamStatusClosed: + case NSStreamStatusError: + default: + return NO; + } +#pragma clang diagnostic pop +} + +- (NSInteger)read:(uint8_t *)buffer + maxLength:(NSUInteger)length +{ + NSInteger totalNumberOfBytesRead = 0; + + if (_phase == AFEncapsulationBoundaryPhase) { + NSData *encapsulationBoundaryData = [([self hasInitialBoundary] ? AFMultipartFormInitialBoundary(self.boundary) : AFMultipartFormEncapsulationBoundary(self.boundary)) dataUsingEncoding:self.stringEncoding]; + totalNumberOfBytesRead += [self readData:encapsulationBoundaryData intoBuffer:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)]; + } + + if (_phase == AFHeaderPhase) { + NSData *headersData = [[self stringForHeaders] dataUsingEncoding:self.stringEncoding]; + totalNumberOfBytesRead += [self readData:headersData intoBuffer:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)]; + } + + if (_phase == AFBodyPhase) { + NSInteger numberOfBytesRead = 0; + + numberOfBytesRead = [self.inputStream read:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)]; + if (numberOfBytesRead == -1) { + return -1; + } else { + totalNumberOfBytesRead += numberOfBytesRead; + + if ([self.inputStream streamStatus] >= NSStreamStatusAtEnd) { + [self transitionToNextPhase]; + } + } + } + + if (_phase == AFFinalBoundaryPhase) { + NSData *closingBoundaryData = ([self hasFinalBoundary] ? [AFMultipartFormFinalBoundary(self.boundary) dataUsingEncoding:self.stringEncoding] : [NSData data]); + totalNumberOfBytesRead += [self readData:closingBoundaryData intoBuffer:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)]; + } + + return totalNumberOfBytesRead; +} + +- (NSInteger)readData:(NSData *)data + intoBuffer:(uint8_t *)buffer + maxLength:(NSUInteger)length +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgnu" + NSRange range = NSMakeRange((NSUInteger)_phaseReadOffset, MIN([data length] - ((NSUInteger)_phaseReadOffset), length)); + [data getBytes:buffer range:range]; +#pragma clang diagnostic pop + + _phaseReadOffset += range.length; + + if (((NSUInteger)_phaseReadOffset) >= [data length]) { + [self transitionToNextPhase]; + } + + return (NSInteger)range.length; +} + +- (BOOL)transitionToNextPhase { + if (![[NSThread currentThread] isMainThread]) { + dispatch_sync(dispatch_get_main_queue(), ^{ + [self transitionToNextPhase]; + }); + return YES; + } + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wcovered-switch-default" + switch (_phase) { + case AFEncapsulationBoundaryPhase: + _phase = AFHeaderPhase; + break; + case AFHeaderPhase: + [self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; + [self.inputStream open]; + _phase = AFBodyPhase; + break; + case AFBodyPhase: + [self.inputStream close]; + _phase = AFFinalBoundaryPhase; + break; + case AFFinalBoundaryPhase: + default: + _phase = AFEncapsulationBoundaryPhase; + break; + } + _phaseReadOffset = 0; +#pragma clang diagnostic pop + + return YES; +} + +#pragma mark - NSCopying + +- (instancetype)copyWithZone:(NSZone *)zone { + AFHTTPBodyPart *bodyPart = [[[self class] allocWithZone:zone] init]; + + bodyPart.stringEncoding = self.stringEncoding; + bodyPart.headers = self.headers; + bodyPart.bodyContentLength = self.bodyContentLength; + bodyPart.body = self.body; + bodyPart.boundary = self.boundary; + + return bodyPart; +} + +@end + +#pragma mark - + +@implementation AFJSONRequestSerializer + ++ (instancetype)serializer { + return [self serializerWithWritingOptions:(NSJSONWritingOptions)0]; +} + ++ (instancetype)serializerWithWritingOptions:(NSJSONWritingOptions)writingOptions +{ + AFJSONRequestSerializer *serializer = [[self alloc] init]; + serializer.writingOptions = writingOptions; + + return serializer; +} + +#pragma mark - AFURLRequestSerialization + +- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request + withParameters:(id)parameters + error:(NSError *__autoreleasing *)error +{ + NSParameterAssert(request); + + if ([self.HTTPMethodsEncodingParametersInURI containsObject:[[request HTTPMethod] uppercaseString]]) { + return [super requestBySerializingRequest:request withParameters:parameters error:error]; + } + + NSMutableURLRequest *mutableRequest = [request mutableCopy]; + + [self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) { + if (![request valueForHTTPHeaderField:field]) { + [mutableRequest setValue:value forHTTPHeaderField:field]; + } + }]; + + if (parameters) { + if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) { + [mutableRequest setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + } + + [mutableRequest setHTTPBody:[NSJSONSerialization dataWithJSONObject:parameters options:self.writingOptions error:error]]; + } + + return mutableRequest; +} + +#pragma mark - NSSecureCoding + +- (instancetype)initWithCoder:(NSCoder *)decoder { + self = [super initWithCoder:decoder]; + if (!self) { + return nil; + } + + self.writingOptions = [[decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(writingOptions))] unsignedIntegerValue]; + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + + [coder encodeInteger:self.writingOptions forKey:NSStringFromSelector(@selector(writingOptions))]; +} + +#pragma mark - NSCopying + +- (instancetype)copyWithZone:(NSZone *)zone { + AFJSONRequestSerializer *serializer = [super copyWithZone:zone]; + serializer.writingOptions = self.writingOptions; + + return serializer; +} + +@end + +#pragma mark - + +@implementation AFPropertyListRequestSerializer + ++ (instancetype)serializer { + return [self serializerWithFormat:NSPropertyListXMLFormat_v1_0 writeOptions:0]; +} + ++ (instancetype)serializerWithFormat:(NSPropertyListFormat)format + writeOptions:(NSPropertyListWriteOptions)writeOptions +{ + AFPropertyListRequestSerializer *serializer = [[self alloc] init]; + serializer.format = format; + serializer.writeOptions = writeOptions; + + return serializer; +} + +#pragma mark - AFURLRequestSerializer + +- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request + withParameters:(id)parameters + error:(NSError *__autoreleasing *)error +{ + NSParameterAssert(request); + + if ([self.HTTPMethodsEncodingParametersInURI containsObject:[[request HTTPMethod] uppercaseString]]) { + return [super requestBySerializingRequest:request withParameters:parameters error:error]; + } + + NSMutableURLRequest *mutableRequest = [request mutableCopy]; + + [self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) { + if (![request valueForHTTPHeaderField:field]) { + [mutableRequest setValue:value forHTTPHeaderField:field]; + } + }]; + + if (parameters) { + if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) { + [mutableRequest setValue:@"application/x-plist" forHTTPHeaderField:@"Content-Type"]; + } + + [mutableRequest setHTTPBody:[NSPropertyListSerialization dataWithPropertyList:parameters format:self.format options:self.writeOptions error:error]]; + } + + return mutableRequest; +} + +#pragma mark - NSSecureCoding + +- (instancetype)initWithCoder:(NSCoder *)decoder { + self = [super initWithCoder:decoder]; + if (!self) { + return nil; + } + + self.format = (NSPropertyListFormat)[[decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(format))] unsignedIntegerValue]; + self.writeOptions = [[decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(writeOptions))] unsignedIntegerValue]; + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + + [coder encodeInteger:self.format forKey:NSStringFromSelector(@selector(format))]; + [coder encodeObject:@(self.writeOptions) forKey:NSStringFromSelector(@selector(writeOptions))]; +} + +#pragma mark - NSCopying + +- (instancetype)copyWithZone:(NSZone *)zone { + AFPropertyListRequestSerializer *serializer = [super copyWithZone:zone]; + serializer.format = self.format; + serializer.writeOptions = self.writeOptions; + + return serializer; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFURLResponseSerialization.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFURLResponseSerialization.h new file mode 100644 index 0000000000..f9e14c69a2 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFURLResponseSerialization.h @@ -0,0 +1,311 @@ +// AFURLResponseSerialization.h +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + The `AFURLResponseSerialization` protocol is adopted by an object that decodes data into a more useful object representation, according to details in the server response. Response serializers may additionally perform validation on the incoming response and data. + + For example, a JSON response serializer may check for an acceptable status code (`2XX` range) and content type (`application/json`), decoding a valid JSON response into an object. + */ +@protocol AFURLResponseSerialization + +/** + The response object decoded from the data associated with a specified response. + + @param response The response to be processed. + @param data The response data to be decoded. + @param error The error that occurred while attempting to decode the response data. + + @return The object decoded from the specified response data. + */ +- (nullable id)responseObjectForResponse:(nullable NSURLResponse *)response + data:(nullable NSData *)data + error:(NSError * _Nullable __autoreleasing *)error NS_SWIFT_NOTHROW; + +@end + +#pragma mark - + +/** + `AFHTTPResponseSerializer` conforms to the `AFURLRequestSerialization` & `AFURLResponseSerialization` protocols, offering a concrete base implementation of query string / URL form-encoded parameter serialization and default request headers, as well as response status code and content type validation. + + Any request or response serializer dealing with HTTP is encouraged to subclass `AFHTTPResponseSerializer` in order to ensure consistent default behavior. + */ +@interface AFHTTPResponseSerializer : NSObject + +- (instancetype)init; + +/** + The string encoding used to serialize data received from the server, when no string encoding is specified by the response. `NSUTF8StringEncoding` by default. + */ +@property (nonatomic, assign) NSStringEncoding stringEncoding; + +/** + Creates and returns a serializer with default configuration. + */ ++ (instancetype)serializer; + +///----------------------------------------- +/// @name Configuring Response Serialization +///----------------------------------------- + +/** + The acceptable HTTP status codes for responses. When non-`nil`, responses with status codes not contained by the set will result in an error during validation. + + See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html + */ +@property (nonatomic, copy, nullable) NSIndexSet *acceptableStatusCodes; + +/** + The acceptable MIME types for responses. When non-`nil`, responses with a `Content-Type` with MIME types that do not intersect with the set will result in an error during validation. + */ +@property (nonatomic, copy, nullable) NSSet *acceptableContentTypes; + +/** + Validates the specified response and data. + + In its base implementation, this method checks for an acceptable status code and content type. Subclasses may wish to add other domain-specific checks. + + @param response The response to be validated. + @param data The data associated with the response. + @param error The error that occurred while attempting to validate the response. + + @return `YES` if the response is valid, otherwise `NO`. + */ +- (BOOL)validateResponse:(nullable NSHTTPURLResponse *)response + data:(nullable NSData *)data + error:(NSError * _Nullable __autoreleasing *)error; + +@end + +#pragma mark - + + +/** + `AFJSONResponseSerializer` is a subclass of `AFHTTPResponseSerializer` that validates and decodes JSON responses. + + By default, `AFJSONResponseSerializer` accepts the following MIME types, which includes the official standard, `application/json`, as well as other commonly-used types: + + - `application/json` + - `text/json` + - `text/javascript` + */ +@interface AFJSONResponseSerializer : AFHTTPResponseSerializer + +- (instancetype)init; + +/** + Options for reading the response JSON data and creating the Foundation objects. For possible values, see the `NSJSONSerialization` documentation section "NSJSONReadingOptions". `0` by default. + */ +@property (nonatomic, assign) NSJSONReadingOptions readingOptions; + +/** + Whether to remove keys with `NSNull` values from response JSON. Defaults to `NO`. + */ +@property (nonatomic, assign) BOOL removesKeysWithNullValues; + +/** + Creates and returns a JSON serializer with specified reading and writing options. + + @param readingOptions The specified JSON reading options. + */ ++ (instancetype)serializerWithReadingOptions:(NSJSONReadingOptions)readingOptions; + +@end + +#pragma mark - + +/** + `AFXMLParserResponseSerializer` is a subclass of `AFHTTPResponseSerializer` that validates and decodes XML responses as an `NSXMLParser` objects. + + By default, `AFXMLParserResponseSerializer` accepts the following MIME types, which includes the official standard, `application/xml`, as well as other commonly-used types: + + - `application/xml` + - `text/xml` + */ +@interface AFXMLParserResponseSerializer : AFHTTPResponseSerializer + +@end + +#pragma mark - + +#ifdef __MAC_OS_X_VERSION_MIN_REQUIRED + +/** + `AFXMLDocumentResponseSerializer` is a subclass of `AFHTTPResponseSerializer` that validates and decodes XML responses as an `NSXMLDocument` objects. + + By default, `AFXMLDocumentResponseSerializer` accepts the following MIME types, which includes the official standard, `application/xml`, as well as other commonly-used types: + + - `application/xml` + - `text/xml` + */ +@interface AFXMLDocumentResponseSerializer : AFHTTPResponseSerializer + +- (instancetype)init; + +/** + Input and output options specifically intended for `NSXMLDocument` objects. For possible values, see the `NSJSONSerialization` documentation section "NSJSONReadingOptions". `0` by default. + */ +@property (nonatomic, assign) NSUInteger options; + +/** + Creates and returns an XML document serializer with the specified options. + + @param mask The XML document options. + */ ++ (instancetype)serializerWithXMLDocumentOptions:(NSUInteger)mask; + +@end + +#endif + +#pragma mark - + +/** + `AFPropertyListResponseSerializer` is a subclass of `AFHTTPResponseSerializer` that validates and decodes XML responses as an `NSXMLDocument` objects. + + By default, `AFPropertyListResponseSerializer` accepts the following MIME types: + + - `application/x-plist` + */ +@interface AFPropertyListResponseSerializer : AFHTTPResponseSerializer + +- (instancetype)init; + +/** + The property list format. Possible values are described in "NSPropertyListFormat". + */ +@property (nonatomic, assign) NSPropertyListFormat format; + +/** + The property list reading options. Possible values are described in "NSPropertyListMutabilityOptions." + */ +@property (nonatomic, assign) NSPropertyListReadOptions readOptions; + +/** + Creates and returns a property list serializer with a specified format, read options, and write options. + + @param format The property list format. + @param readOptions The property list reading options. + */ ++ (instancetype)serializerWithFormat:(NSPropertyListFormat)format + readOptions:(NSPropertyListReadOptions)readOptions; + +@end + +#pragma mark - + +/** + `AFImageResponseSerializer` is a subclass of `AFHTTPResponseSerializer` that validates and decodes image responses. + + By default, `AFImageResponseSerializer` accepts the following MIME types, which correspond to the image formats supported by UIImage or NSImage: + + - `image/tiff` + - `image/jpeg` + - `image/gif` + - `image/png` + - `image/ico` + - `image/x-icon` + - `image/bmp` + - `image/x-bmp` + - `image/x-xbitmap` + - `image/x-win-bitmap` + */ +@interface AFImageResponseSerializer : AFHTTPResponseSerializer + +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_WATCH +/** + The scale factor used when interpreting the image data to construct `responseImage`. Specifying a scale factor of 1.0 results in an image whose size matches the pixel-based dimensions of the image. Applying a different scale factor changes the size of the image as reported by the size property. This is set to the value of scale of the main screen by default, which automatically scales images for retina displays, for instance. + */ +@property (nonatomic, assign) CGFloat imageScale; + +/** + Whether to automatically inflate response image data for compressed formats (such as PNG or JPEG). Enabling this can significantly improve drawing performance on iOS when used with `setCompletionBlockWithSuccess:failure:`, as it allows a bitmap representation to be constructed in the background rather than on the main thread. `YES` by default. + */ +@property (nonatomic, assign) BOOL automaticallyInflatesResponseImage; +#endif + +@end + +#pragma mark - + +/** + `AFCompoundSerializer` is a subclass of `AFHTTPResponseSerializer` that delegates the response serialization to the first `AFHTTPResponseSerializer` object that returns an object for `responseObjectForResponse:data:error:`, falling back on the default behavior of `AFHTTPResponseSerializer`. This is useful for supporting multiple potential types and structures of server responses with a single serializer. + */ +@interface AFCompoundResponseSerializer : AFHTTPResponseSerializer + +/** + The component response serializers. + */ +@property (readonly, nonatomic, copy) NSArray > *responseSerializers; + +/** + Creates and returns a compound serializer comprised of the specified response serializers. + + @warning Each response serializer specified must be a subclass of `AFHTTPResponseSerializer`, and response to `-validateResponse:data:error:`. + */ ++ (instancetype)compoundSerializerWithResponseSerializers:(NSArray > *)responseSerializers; + +@end + +///---------------- +/// @name Constants +///---------------- + +/** + ## Error Domains + + The following error domain is predefined. + + - `NSString * const AFURLResponseSerializationErrorDomain` + + ### Constants + + `AFURLResponseSerializationErrorDomain` + AFURLResponseSerializer errors. Error codes for `AFURLResponseSerializationErrorDomain` correspond to codes in `NSURLErrorDomain`. + */ +FOUNDATION_EXPORT NSString * const AFURLResponseSerializationErrorDomain; + +/** + ## User info dictionary keys + + These keys may exist in the user info dictionary, in addition to those defined for NSError. + + - `NSString * const AFNetworkingOperationFailingURLResponseErrorKey` + - `NSString * const AFNetworkingOperationFailingURLResponseDataErrorKey` + + ### Constants + + `AFNetworkingOperationFailingURLResponseErrorKey` + The corresponding value is an `NSURLResponse` containing the response of the operation associated with an error. This key is only present in the `AFURLResponseSerializationErrorDomain`. + + `AFNetworkingOperationFailingURLResponseDataErrorKey` + The corresponding value is an `NSData` containing the original data of the operation associated with an error. This key is only present in the `AFURLResponseSerializationErrorDomain`. + */ +FOUNDATION_EXPORT NSString * const AFNetworkingOperationFailingURLResponseErrorKey; + +FOUNDATION_EXPORT NSString * const AFNetworkingOperationFailingURLResponseDataErrorKey; + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFURLResponseSerialization.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFURLResponseSerialization.m new file mode 100755 index 0000000000..ef5e334235 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFURLResponseSerialization.m @@ -0,0 +1,828 @@ +// AFURLResponseSerialization.m +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "AFURLResponseSerialization.h" + +#import + +#if TARGET_OS_IOS +#import +#elif TARGET_OS_WATCH +#import +#elif defined(__MAC_OS_X_VERSION_MIN_REQUIRED) +#import +#endif + +NSString * const AFURLResponseSerializationErrorDomain = @"com.alamofire.error.serialization.response"; +NSString * const AFNetworkingOperationFailingURLResponseErrorKey = @"com.alamofire.serialization.response.error.response"; +NSString * const AFNetworkingOperationFailingURLResponseDataErrorKey = @"com.alamofire.serialization.response.error.data"; + +static NSError * AFErrorWithUnderlyingError(NSError *error, NSError *underlyingError) { + if (!error) { + return underlyingError; + } + + if (!underlyingError || error.userInfo[NSUnderlyingErrorKey]) { + return error; + } + + NSMutableDictionary *mutableUserInfo = [error.userInfo mutableCopy]; + mutableUserInfo[NSUnderlyingErrorKey] = underlyingError; + + return [[NSError alloc] initWithDomain:error.domain code:error.code userInfo:mutableUserInfo]; +} + +static BOOL AFErrorOrUnderlyingErrorHasCodeInDomain(NSError *error, NSInteger code, NSString *domain) { + if ([error.domain isEqualToString:domain] && error.code == code) { + return YES; + } else if (error.userInfo[NSUnderlyingErrorKey]) { + return AFErrorOrUnderlyingErrorHasCodeInDomain(error.userInfo[NSUnderlyingErrorKey], code, domain); + } + + return NO; +} + +static id AFJSONObjectByRemovingKeysWithNullValues(id JSONObject, NSJSONReadingOptions readingOptions) { + if ([JSONObject isKindOfClass:[NSArray class]]) { + NSMutableArray *mutableArray = [NSMutableArray arrayWithCapacity:[(NSArray *)JSONObject count]]; + for (id value in (NSArray *)JSONObject) { + [mutableArray addObject:AFJSONObjectByRemovingKeysWithNullValues(value, readingOptions)]; + } + + return (readingOptions & NSJSONReadingMutableContainers) ? mutableArray : [NSArray arrayWithArray:mutableArray]; + } else if ([JSONObject isKindOfClass:[NSDictionary class]]) { + NSMutableDictionary *mutableDictionary = [NSMutableDictionary dictionaryWithDictionary:JSONObject]; + for (id key in [(NSDictionary *)JSONObject allKeys]) { + id value = (NSDictionary *)JSONObject[key]; + if (!value || [value isEqual:[NSNull null]]) { + [mutableDictionary removeObjectForKey:key]; + } else if ([value isKindOfClass:[NSArray class]] || [value isKindOfClass:[NSDictionary class]]) { + mutableDictionary[key] = AFJSONObjectByRemovingKeysWithNullValues(value, readingOptions); + } + } + + return (readingOptions & NSJSONReadingMutableContainers) ? mutableDictionary : [NSDictionary dictionaryWithDictionary:mutableDictionary]; + } + + return JSONObject; +} + +@implementation AFHTTPResponseSerializer + ++ (instancetype)serializer { + return [[self alloc] init]; +} + +- (instancetype)init { + self = [super init]; + if (!self) { + return nil; + } + + self.stringEncoding = NSUTF8StringEncoding; + + self.acceptableStatusCodes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(200, 100)]; + self.acceptableContentTypes = nil; + + return self; +} + +#pragma mark - + +- (BOOL)validateResponse:(NSHTTPURLResponse *)response + data:(NSData *)data + error:(NSError * __autoreleasing *)error +{ + BOOL responseIsValid = YES; + NSError *validationError = nil; + + if (response && [response isKindOfClass:[NSHTTPURLResponse class]]) { + if (self.acceptableContentTypes && ![self.acceptableContentTypes containsObject:[response MIMEType]]) { + if ([data length] > 0 && [response URL]) { + NSMutableDictionary *mutableUserInfo = [@{ + NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedStringFromTable(@"Request failed: unacceptable content-type: %@", @"AFNetworking", nil), [response MIMEType]], + NSURLErrorFailingURLErrorKey:[response URL], + AFNetworkingOperationFailingURLResponseErrorKey: response, + } mutableCopy]; + if (data) { + mutableUserInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] = data; + } + + validationError = AFErrorWithUnderlyingError([NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorCannotDecodeContentData userInfo:mutableUserInfo], validationError); + } + + responseIsValid = NO; + } + + if (self.acceptableStatusCodes && ![self.acceptableStatusCodes containsIndex:(NSUInteger)response.statusCode] && [response URL]) { + NSMutableDictionary *mutableUserInfo = [@{ + NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedStringFromTable(@"Request failed: %@ (%ld)", @"AFNetworking", nil), [NSHTTPURLResponse localizedStringForStatusCode:response.statusCode], (long)response.statusCode], + NSURLErrorFailingURLErrorKey:[response URL], + AFNetworkingOperationFailingURLResponseErrorKey: response, + } mutableCopy]; + + if (data) { + mutableUserInfo[AFNetworkingOperationFailingURLResponseDataErrorKey] = data; + } + + validationError = AFErrorWithUnderlyingError([NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorBadServerResponse userInfo:mutableUserInfo], validationError); + + responseIsValid = NO; + } + } + + if (error && !responseIsValid) { + *error = validationError; + } + + return responseIsValid; +} + +#pragma mark - AFURLResponseSerialization + +- (id)responseObjectForResponse:(NSURLResponse *)response + data:(NSData *)data + error:(NSError *__autoreleasing *)error +{ + [self validateResponse:(NSHTTPURLResponse *)response data:data error:error]; + + return data; +} + +#pragma mark - NSSecureCoding + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (instancetype)initWithCoder:(NSCoder *)decoder { + self = [self init]; + if (!self) { + return nil; + } + + self.acceptableStatusCodes = [decoder decodeObjectOfClass:[NSIndexSet class] forKey:NSStringFromSelector(@selector(acceptableStatusCodes))]; + self.acceptableContentTypes = [decoder decodeObjectOfClass:[NSIndexSet class] forKey:NSStringFromSelector(@selector(acceptableContentTypes))]; + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:self.acceptableStatusCodes forKey:NSStringFromSelector(@selector(acceptableStatusCodes))]; + [coder encodeObject:self.acceptableContentTypes forKey:NSStringFromSelector(@selector(acceptableContentTypes))]; +} + +#pragma mark - NSCopying + +- (instancetype)copyWithZone:(NSZone *)zone { + AFHTTPResponseSerializer *serializer = [[[self class] allocWithZone:zone] init]; + serializer.acceptableStatusCodes = [self.acceptableStatusCodes copyWithZone:zone]; + serializer.acceptableContentTypes = [self.acceptableContentTypes copyWithZone:zone]; + + return serializer; +} + +@end + +#pragma mark - + +@implementation AFJSONResponseSerializer + ++ (instancetype)serializer { + return [self serializerWithReadingOptions:(NSJSONReadingOptions)0]; +} + ++ (instancetype)serializerWithReadingOptions:(NSJSONReadingOptions)readingOptions { + AFJSONResponseSerializer *serializer = [[self alloc] init]; + serializer.readingOptions = readingOptions; + + return serializer; +} + +- (instancetype)init { + self = [super init]; + if (!self) { + return nil; + } + + self.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", nil]; + + return self; +} + +#pragma mark - AFURLResponseSerialization + +- (id)responseObjectForResponse:(NSURLResponse *)response + data:(NSData *)data + error:(NSError *__autoreleasing *)error +{ + if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) { + if (!error || AFErrorOrUnderlyingErrorHasCodeInDomain(*error, NSURLErrorCannotDecodeContentData, AFURLResponseSerializationErrorDomain)) { + return nil; + } + } + + // Workaround for behavior of Rails to return a single space for `head :ok` (a workaround for a bug in Safari), which is not interpreted as valid input by NSJSONSerialization. + // See https://github.com/rails/rails/issues/1742 + NSStringEncoding stringEncoding = self.stringEncoding; + if (response.textEncodingName) { + CFStringEncoding encoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName); + if (encoding != kCFStringEncodingInvalidId) { + stringEncoding = CFStringConvertEncodingToNSStringEncoding(encoding); + } + } + + id responseObject = nil; + NSError *serializationError = nil; + @autoreleasepool { + NSString *responseString = [[NSString alloc] initWithData:data encoding:stringEncoding]; + if (responseString && ![responseString isEqualToString:@" "]) { + // Workaround for a bug in NSJSONSerialization when Unicode character escape codes are used instead of the actual character + // See http://stackoverflow.com/a/12843465/157142 + data = [responseString dataUsingEncoding:NSUTF8StringEncoding]; + + if (data) { + if ([data length] > 0) { + responseObject = [NSJSONSerialization JSONObjectWithData:data options:self.readingOptions error:&serializationError]; + } else { + return nil; + } + } else { + NSDictionary *userInfo = @{ + NSLocalizedDescriptionKey: NSLocalizedStringFromTable(@"Data failed decoding as a UTF-8 string", @"AFNetworking", nil), + NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedStringFromTable(@"Could not decode string: %@", @"AFNetworking", nil), responseString] + }; + + serializationError = [NSError errorWithDomain:AFURLResponseSerializationErrorDomain code:NSURLErrorCannotDecodeContentData userInfo:userInfo]; + } + } + } + + if (self.removesKeysWithNullValues && responseObject) { + responseObject = AFJSONObjectByRemovingKeysWithNullValues(responseObject, self.readingOptions); + } + + if (error) { + *error = AFErrorWithUnderlyingError(serializationError, *error); + } + + return responseObject; +} + +#pragma mark - NSSecureCoding + +- (instancetype)initWithCoder:(NSCoder *)decoder { + self = [super initWithCoder:decoder]; + if (!self) { + return nil; + } + + self.readingOptions = [[decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(readingOptions))] unsignedIntegerValue]; + self.removesKeysWithNullValues = [[decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(removesKeysWithNullValues))] boolValue]; + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + + [coder encodeObject:@(self.readingOptions) forKey:NSStringFromSelector(@selector(readingOptions))]; + [coder encodeObject:@(self.removesKeysWithNullValues) forKey:NSStringFromSelector(@selector(removesKeysWithNullValues))]; +} + +#pragma mark - NSCopying + +- (instancetype)copyWithZone:(NSZone *)zone { + AFJSONResponseSerializer *serializer = [[[self class] allocWithZone:zone] init]; + serializer.readingOptions = self.readingOptions; + serializer.removesKeysWithNullValues = self.removesKeysWithNullValues; + + return serializer; +} + +@end + +#pragma mark - + +@implementation AFXMLParserResponseSerializer + ++ (instancetype)serializer { + AFXMLParserResponseSerializer *serializer = [[self alloc] init]; + + return serializer; +} + +- (instancetype)init { + self = [super init]; + if (!self) { + return nil; + } + + self.acceptableContentTypes = [[NSSet alloc] initWithObjects:@"application/xml", @"text/xml", nil]; + + return self; +} + +#pragma mark - AFURLResponseSerialization + +- (id)responseObjectForResponse:(NSHTTPURLResponse *)response + data:(NSData *)data + error:(NSError *__autoreleasing *)error +{ + if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) { + if (!error || AFErrorOrUnderlyingErrorHasCodeInDomain(*error, NSURLErrorCannotDecodeContentData, AFURLResponseSerializationErrorDomain)) { + return nil; + } + } + + return [[NSXMLParser alloc] initWithData:data]; +} + +@end + +#pragma mark - + +#ifdef __MAC_OS_X_VERSION_MIN_REQUIRED + +@implementation AFXMLDocumentResponseSerializer + ++ (instancetype)serializer { + return [self serializerWithXMLDocumentOptions:0]; +} + ++ (instancetype)serializerWithXMLDocumentOptions:(NSUInteger)mask { + AFXMLDocumentResponseSerializer *serializer = [[self alloc] init]; + serializer.options = mask; + + return serializer; +} + +- (instancetype)init { + self = [super init]; + if (!self) { + return nil; + } + + self.acceptableContentTypes = [[NSSet alloc] initWithObjects:@"application/xml", @"text/xml", nil]; + + return self; +} + +#pragma mark - AFURLResponseSerialization + +- (id)responseObjectForResponse:(NSURLResponse *)response + data:(NSData *)data + error:(NSError *__autoreleasing *)error +{ + if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) { + if (!error || AFErrorOrUnderlyingErrorHasCodeInDomain(*error, NSURLErrorCannotDecodeContentData, AFURLResponseSerializationErrorDomain)) { + return nil; + } + } + + NSError *serializationError = nil; + NSXMLDocument *document = [[NSXMLDocument alloc] initWithData:data options:self.options error:&serializationError]; + + if (error) { + *error = AFErrorWithUnderlyingError(serializationError, *error); + } + + return document; +} + +#pragma mark - NSSecureCoding + +- (instancetype)initWithCoder:(NSCoder *)decoder { + self = [super initWithCoder:decoder]; + if (!self) { + return nil; + } + + self.options = [[decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(options))] unsignedIntegerValue]; + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + + [coder encodeObject:@(self.options) forKey:NSStringFromSelector(@selector(options))]; +} + +#pragma mark - NSCopying + +- (instancetype)copyWithZone:(NSZone *)zone { + AFXMLDocumentResponseSerializer *serializer = [[[self class] allocWithZone:zone] init]; + serializer.options = self.options; + + return serializer; +} + +@end + +#endif + +#pragma mark - + +@implementation AFPropertyListResponseSerializer + ++ (instancetype)serializer { + return [self serializerWithFormat:NSPropertyListXMLFormat_v1_0 readOptions:0]; +} + ++ (instancetype)serializerWithFormat:(NSPropertyListFormat)format + readOptions:(NSPropertyListReadOptions)readOptions +{ + AFPropertyListResponseSerializer *serializer = [[self alloc] init]; + serializer.format = format; + serializer.readOptions = readOptions; + + return serializer; +} + +- (instancetype)init { + self = [super init]; + if (!self) { + return nil; + } + + self.acceptableContentTypes = [[NSSet alloc] initWithObjects:@"application/x-plist", nil]; + + return self; +} + +#pragma mark - AFURLResponseSerialization + +- (id)responseObjectForResponse:(NSURLResponse *)response + data:(NSData *)data + error:(NSError *__autoreleasing *)error +{ + if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) { + if (!error || AFErrorOrUnderlyingErrorHasCodeInDomain(*error, NSURLErrorCannotDecodeContentData, AFURLResponseSerializationErrorDomain)) { + return nil; + } + } + + id responseObject; + NSError *serializationError = nil; + + if (data) { + responseObject = [NSPropertyListSerialization propertyListWithData:data options:self.readOptions format:NULL error:&serializationError]; + } + + if (error) { + *error = AFErrorWithUnderlyingError(serializationError, *error); + } + + return responseObject; +} + +#pragma mark - NSSecureCoding + +- (instancetype)initWithCoder:(NSCoder *)decoder { + self = [super initWithCoder:decoder]; + if (!self) { + return nil; + } + + self.format = (NSPropertyListFormat)[[decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(format))] unsignedIntegerValue]; + self.readOptions = [[decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(readOptions))] unsignedIntegerValue]; + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + + [coder encodeObject:@(self.format) forKey:NSStringFromSelector(@selector(format))]; + [coder encodeObject:@(self.readOptions) forKey:NSStringFromSelector(@selector(readOptions))]; +} + +#pragma mark - NSCopying + +- (instancetype)copyWithZone:(NSZone *)zone { + AFPropertyListResponseSerializer *serializer = [[[self class] allocWithZone:zone] init]; + serializer.format = self.format; + serializer.readOptions = self.readOptions; + + return serializer; +} + +@end + +#pragma mark - + +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_WATCH +#import +#import + +@interface UIImage (AFNetworkingSafeImageLoading) ++ (UIImage *)af_safeImageWithData:(NSData *)data; +@end + +static NSLock* imageLock = nil; + +@implementation UIImage (AFNetworkingSafeImageLoading) + ++ (UIImage *)af_safeImageWithData:(NSData *)data { + UIImage* image = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + imageLock = [[NSLock alloc] init]; + }); + + [imageLock lock]; + image = [UIImage imageWithData:data]; + [imageLock unlock]; + return image; +} + +@end + +static UIImage * AFImageWithDataAtScale(NSData *data, CGFloat scale) { + UIImage *image = [UIImage af_safeImageWithData:data]; + if (image.images) { + return image; + } + + return [[UIImage alloc] initWithCGImage:[image CGImage] scale:scale orientation:image.imageOrientation]; +} + +static UIImage * AFInflatedImageFromResponseWithDataAtScale(NSHTTPURLResponse *response, NSData *data, CGFloat scale) { + if (!data || [data length] == 0) { + return nil; + } + + CGImageRef imageRef = NULL; + CGDataProviderRef dataProvider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data); + + if ([response.MIMEType isEqualToString:@"image/png"]) { + imageRef = CGImageCreateWithPNGDataProvider(dataProvider, NULL, true, kCGRenderingIntentDefault); + } else if ([response.MIMEType isEqualToString:@"image/jpeg"]) { + imageRef = CGImageCreateWithJPEGDataProvider(dataProvider, NULL, true, kCGRenderingIntentDefault); + + if (imageRef) { + CGColorSpaceRef imageColorSpace = CGImageGetColorSpace(imageRef); + CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(imageColorSpace); + + // CGImageCreateWithJPEGDataProvider does not properly handle CMKY, so fall back to AFImageWithDataAtScale + if (imageColorSpaceModel == kCGColorSpaceModelCMYK) { + CGImageRelease(imageRef); + imageRef = NULL; + } + } + } + + CGDataProviderRelease(dataProvider); + + UIImage *image = AFImageWithDataAtScale(data, scale); + if (!imageRef) { + if (image.images || !image) { + return image; + } + + imageRef = CGImageCreateCopy([image CGImage]); + if (!imageRef) { + return nil; + } + } + + size_t width = CGImageGetWidth(imageRef); + size_t height = CGImageGetHeight(imageRef); + size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef); + + if (width * height > 1024 * 1024 || bitsPerComponent > 8) { + CGImageRelease(imageRef); + + return image; + } + + // CGImageGetBytesPerRow() calculates incorrectly in iOS 5.0, so defer to CGBitmapContextCreate + size_t bytesPerRow = 0; + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGColorSpaceModel colorSpaceModel = CGColorSpaceGetModel(colorSpace); + CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef); + + if (colorSpaceModel == kCGColorSpaceModelRGB) { + uint32_t alpha = (bitmapInfo & kCGBitmapAlphaInfoMask); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wassign-enum" + if (alpha == kCGImageAlphaNone) { + bitmapInfo &= ~kCGBitmapAlphaInfoMask; + bitmapInfo |= kCGImageAlphaNoneSkipFirst; + } else if (!(alpha == kCGImageAlphaNoneSkipFirst || alpha == kCGImageAlphaNoneSkipLast)) { + bitmapInfo &= ~kCGBitmapAlphaInfoMask; + bitmapInfo |= kCGImageAlphaPremultipliedFirst; + } +#pragma clang diagnostic pop + } + + CGContextRef context = CGBitmapContextCreate(NULL, width, height, bitsPerComponent, bytesPerRow, colorSpace, bitmapInfo); + + CGColorSpaceRelease(colorSpace); + + if (!context) { + CGImageRelease(imageRef); + + return image; + } + + CGContextDrawImage(context, CGRectMake(0.0f, 0.0f, width, height), imageRef); + CGImageRef inflatedImageRef = CGBitmapContextCreateImage(context); + + CGContextRelease(context); + + UIImage *inflatedImage = [[UIImage alloc] initWithCGImage:inflatedImageRef scale:scale orientation:image.imageOrientation]; + + CGImageRelease(inflatedImageRef); + CGImageRelease(imageRef); + + return inflatedImage; +} +#endif + + +@implementation AFImageResponseSerializer + +- (instancetype)init { + self = [super init]; + if (!self) { + return nil; + } + + self.acceptableContentTypes = [[NSSet alloc] initWithObjects:@"image/tiff", @"image/jpeg", @"image/gif", @"image/png", @"image/ico", @"image/x-icon", @"image/bmp", @"image/x-bmp", @"image/x-xbitmap", @"image/x-win-bitmap", nil]; + +#if TARGET_OS_IOS || TARGET_OS_TV + self.imageScale = [[UIScreen mainScreen] scale]; + self.automaticallyInflatesResponseImage = YES; +#elif TARGET_OS_WATCH + self.imageScale = [[WKInterfaceDevice currentDevice] screenScale]; + self.automaticallyInflatesResponseImage = YES; +#endif + + return self; +} + +#pragma mark - AFURLResponseSerializer + +- (id)responseObjectForResponse:(NSURLResponse *)response + data:(NSData *)data + error:(NSError *__autoreleasing *)error +{ + if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) { + if (!error || AFErrorOrUnderlyingErrorHasCodeInDomain(*error, NSURLErrorCannotDecodeContentData, AFURLResponseSerializationErrorDomain)) { + return nil; + } + } + +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_WATCH + if (self.automaticallyInflatesResponseImage) { + return AFInflatedImageFromResponseWithDataAtScale((NSHTTPURLResponse *)response, data, self.imageScale); + } else { + return AFImageWithDataAtScale(data, self.imageScale); + } +#else + // Ensure that the image is set to it's correct pixel width and height + NSBitmapImageRep *bitimage = [[NSBitmapImageRep alloc] initWithData:data]; + NSImage *image = [[NSImage alloc] initWithSize:NSMakeSize([bitimage pixelsWide], [bitimage pixelsHigh])]; + [image addRepresentation:bitimage]; + + return image; +#endif + + return nil; +} + +#pragma mark - NSSecureCoding + +- (instancetype)initWithCoder:(NSCoder *)decoder { + self = [super initWithCoder:decoder]; + if (!self) { + return nil; + } + +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_WATCH + NSNumber *imageScale = [decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(imageScale))]; +#if CGFLOAT_IS_DOUBLE + self.imageScale = [imageScale doubleValue]; +#else + self.imageScale = [imageScale floatValue]; +#endif + + self.automaticallyInflatesResponseImage = [decoder decodeBoolForKey:NSStringFromSelector(@selector(automaticallyInflatesResponseImage))]; +#endif + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_WATCH + [coder encodeObject:@(self.imageScale) forKey:NSStringFromSelector(@selector(imageScale))]; + [coder encodeBool:self.automaticallyInflatesResponseImage forKey:NSStringFromSelector(@selector(automaticallyInflatesResponseImage))]; +#endif +} + +#pragma mark - NSCopying + +- (instancetype)copyWithZone:(NSZone *)zone { + AFImageResponseSerializer *serializer = [[[self class] allocWithZone:zone] init]; + +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_WATCH + serializer.imageScale = self.imageScale; + serializer.automaticallyInflatesResponseImage = self.automaticallyInflatesResponseImage; +#endif + + return serializer; +} + +@end + +#pragma mark - + +@interface AFCompoundResponseSerializer () +@property (readwrite, nonatomic, copy) NSArray *responseSerializers; +@end + +@implementation AFCompoundResponseSerializer + ++ (instancetype)compoundSerializerWithResponseSerializers:(NSArray *)responseSerializers { + AFCompoundResponseSerializer *serializer = [[self alloc] init]; + serializer.responseSerializers = responseSerializers; + + return serializer; +} + +#pragma mark - AFURLResponseSerialization + +- (id)responseObjectForResponse:(NSURLResponse *)response + data:(NSData *)data + error:(NSError *__autoreleasing *)error +{ + for (id serializer in self.responseSerializers) { + if (![serializer isKindOfClass:[AFHTTPResponseSerializer class]]) { + continue; + } + + NSError *serializerError = nil; + id responseObject = [serializer responseObjectForResponse:response data:data error:&serializerError]; + if (responseObject) { + if (error) { + *error = AFErrorWithUnderlyingError(serializerError, *error); + } + + return responseObject; + } + } + + return [super responseObjectForResponse:response data:data error:error]; +} + +#pragma mark - NSSecureCoding + +- (instancetype)initWithCoder:(NSCoder *)decoder { + self = [super initWithCoder:decoder]; + if (!self) { + return nil; + } + + self.responseSerializers = [decoder decodeObjectOfClass:[NSArray class] forKey:NSStringFromSelector(@selector(responseSerializers))]; + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + + [coder encodeObject:self.responseSerializers forKey:NSStringFromSelector(@selector(responseSerializers))]; +} + +#pragma mark - NSCopying + +- (instancetype)copyWithZone:(NSZone *)zone { + AFCompoundResponseSerializer *serializer = [[[self class] allocWithZone:zone] init]; + serializer.responseSerializers = self.responseSerializers; + + return serializer; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFURLSessionManager.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFURLSessionManager.h new file mode 100644 index 0000000000..be91828489 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFURLSessionManager.h @@ -0,0 +1,499 @@ +// AFURLSessionManager.h +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + + +#import + +#import "AFURLResponseSerialization.h" +#import "AFURLRequestSerialization.h" +#import "AFSecurityPolicy.h" +#if !TARGET_OS_WATCH +#import "AFNetworkReachabilityManager.h" +#endif + +/** + `AFURLSessionManager` creates and manages an `NSURLSession` object based on a specified `NSURLSessionConfiguration` object, which conforms to ``, ``, ``, and ``. + + ## Subclassing Notes + + This is the base class for `AFHTTPSessionManager`, which adds functionality specific to making HTTP requests. If you are looking to extend `AFURLSessionManager` specifically for HTTP, consider subclassing `AFHTTPSessionManager` instead. + + ## NSURLSession & NSURLSessionTask Delegate Methods + + `AFURLSessionManager` implements the following delegate methods: + + ### `NSURLSessionDelegate` + + - `URLSession:didBecomeInvalidWithError:` + - `URLSession:didReceiveChallenge:completionHandler:` + - `URLSessionDidFinishEventsForBackgroundURLSession:` + + ### `NSURLSessionTaskDelegate` + + - `URLSession:willPerformHTTPRedirection:newRequest:completionHandler:` + - `URLSession:task:didReceiveChallenge:completionHandler:` + - `URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:` + - `URLSession:task:didCompleteWithError:` + + ### `NSURLSessionDataDelegate` + + - `URLSession:dataTask:didReceiveResponse:completionHandler:` + - `URLSession:dataTask:didBecomeDownloadTask:` + - `URLSession:dataTask:didReceiveData:` + - `URLSession:dataTask:willCacheResponse:completionHandler:` + + ### `NSURLSessionDownloadDelegate` + + - `URLSession:downloadTask:didFinishDownloadingToURL:` + - `URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesWritten:totalBytesExpectedToWrite:` + - `URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:` + + If any of these methods are overridden in a subclass, they _must_ call the `super` implementation first. + + ## Network Reachability Monitoring + + Network reachability status and change monitoring is available through the `reachabilityManager` property. Applications may choose to monitor network reachability conditions in order to prevent or suspend any outbound requests. See `AFNetworkReachabilityManager` for more details. + + ## NSCoding Caveats + + - Encoded managers do not include any block properties. Be sure to set delegate callback blocks when using `-initWithCoder:` or `NSKeyedUnarchiver`. + + ## NSCopying Caveats + + - `-copy` and `-copyWithZone:` return a new manager with a new `NSURLSession` created from the configuration of the original. + - Operation copies do not include any delegate callback blocks, as they often strongly captures a reference to `self`, which would otherwise have the unintuitive side-effect of pointing to the _original_ session manager when copied. + + @warning Managers for background sessions must be owned for the duration of their use. This can be accomplished by creating an application-wide or shared singleton instance. + */ + +NS_ASSUME_NONNULL_BEGIN + +@interface AFURLSessionManager : NSObject + +/** + The managed session. + */ +@property (readonly, nonatomic, strong) NSURLSession *session; + +/** + The operation queue on which delegate callbacks are run. + */ +@property (readonly, nonatomic, strong) NSOperationQueue *operationQueue; + +/** + Responses sent from the server in data tasks created with `dataTaskWithRequest:success:failure:` and run using the `GET` / `POST` / et al. convenience methods are automatically validated and serialized by the response serializer. By default, this property is set to an instance of `AFJSONResponseSerializer`. + + @warning `responseSerializer` must not be `nil`. + */ +@property (nonatomic, strong) id responseSerializer; + +///------------------------------- +/// @name Managing Security Policy +///------------------------------- + +/** + The security policy used by created session to evaluate server trust for secure connections. `AFURLSessionManager` uses the `defaultPolicy` unless otherwise specified. + */ +@property (nonatomic, strong) AFSecurityPolicy *securityPolicy; + +#if !TARGET_OS_WATCH +///-------------------------------------- +/// @name Monitoring Network Reachability +///-------------------------------------- + +/** + The network reachability manager. `AFURLSessionManager` uses the `sharedManager` by default. + */ +@property (readwrite, nonatomic, strong) AFNetworkReachabilityManager *reachabilityManager; +#endif + +///---------------------------- +/// @name Getting Session Tasks +///---------------------------- + +/** + The data, upload, and download tasks currently run by the managed session. + */ +@property (readonly, nonatomic, strong) NSArray *tasks; + +/** + The data tasks currently run by the managed session. + */ +@property (readonly, nonatomic, strong) NSArray *dataTasks; + +/** + The upload tasks currently run by the managed session. + */ +@property (readonly, nonatomic, strong) NSArray *uploadTasks; + +/** + The download tasks currently run by the managed session. + */ +@property (readonly, nonatomic, strong) NSArray *downloadTasks; + +///------------------------------- +/// @name Managing Callback Queues +///------------------------------- + +/** + The dispatch queue for `completionBlock`. If `NULL` (default), the main queue is used. + */ +@property (nonatomic, strong, nullable) dispatch_queue_t completionQueue; + +/** + The dispatch group for `completionBlock`. If `NULL` (default), a private dispatch group is used. + */ +@property (nonatomic, strong, nullable) dispatch_group_t completionGroup; + +///--------------------------------- +/// @name Working Around System Bugs +///--------------------------------- + +/** + Whether to attempt to retry creation of upload tasks for background sessions when initial call returns `nil`. `NO` by default. + + @bug As of iOS 7.0, there is a bug where upload tasks created for background tasks are sometimes `nil`. As a workaround, if this property is `YES`, AFNetworking will follow Apple's recommendation to try creating the task again. + + @see https://github.com/AFNetworking/AFNetworking/issues/1675 + */ +@property (nonatomic, assign) BOOL attemptsToRecreateUploadTasksForBackgroundSessions; + +///--------------------- +/// @name Initialization +///--------------------- + +/** + Creates and returns a manager for a session created with the specified configuration. This is the designated initializer. + + @param configuration The configuration used to create the managed session. + + @return A manager for a newly-created session. + */ +- (instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)configuration NS_DESIGNATED_INITIALIZER; + +/** + Invalidates the managed session, optionally canceling pending tasks. + + @param cancelPendingTasks Whether or not to cancel pending tasks. + */ +- (void)invalidateSessionCancelingTasks:(BOOL)cancelPendingTasks; + +///------------------------- +/// @name Running Data Tasks +///------------------------- + +/** + Creates an `NSURLSessionDataTask` with the specified request. + + @param request The HTTP request for the request. + @param completionHandler A block object to be executed when the task finishes. This block has no return value and takes three arguments: the server response, the response object created by that serializer, and the error that occurred, if any. + */ +- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request + completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject, NSError * _Nullable error))completionHandler; + +/** + Creates an `NSURLSessionDataTask` with the specified request. + + @param request The HTTP request for the request. + @param uploadProgress A block object to be executed when the upload progress is updated. Note this block is called on the session queue, not the main queue. + @param downloadProgress A block object to be executed when the download progress is updated. Note this block is called on the session queue, not the main queue. + @param completionHandler A block object to be executed when the task finishes. This block has no return value and takes three arguments: the server response, the response object created by that serializer, and the error that occurred, if any. + */ +- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request + uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgressBlock + downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgressBlock + completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject, NSError * _Nullable error))completionHandler; + +///--------------------------- +/// @name Running Upload Tasks +///--------------------------- + +/** + Creates an `NSURLSessionUploadTask` with the specified request for a local file. + + @param request The HTTP request for the request. + @param fileURL A URL to the local file to be uploaded. + @param progress A block object to be executed when the upload progress is updated. Note this block is called on the session queue, not the main queue. + @param completionHandler A block object to be executed when the task finishes. This block has no return value and takes three arguments: the server response, the response object created by that serializer, and the error that occurred, if any. + + @see `attemptsToRecreateUploadTasksForBackgroundSessions` + */ +- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request + fromFile:(NSURL *)fileURL + progress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgressBlock + completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject, NSError * _Nullable error))completionHandler; + +/** + Creates an `NSURLSessionUploadTask` with the specified request for an HTTP body. + + @param request The HTTP request for the request. + @param bodyData A data object containing the HTTP body to be uploaded. + @param progress A block object to be executed when the upload progress is updated. Note this block is called on the session queue, not the main queue. + @param completionHandler A block object to be executed when the task finishes. This block has no return value and takes three arguments: the server response, the response object created by that serializer, and the error that occurred, if any. + */ +- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request + fromData:(nullable NSData *)bodyData + progress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgressBlock + completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject, NSError * _Nullable error))completionHandler; + +/** + Creates an `NSURLSessionUploadTask` with the specified streaming request. + + @param request The HTTP request for the request. + @param progress A block object to be executed when the upload progress is updated. Note this block is called on the session queue, not the main queue. + @param completionHandler A block object to be executed when the task finishes. This block has no return value and takes three arguments: the server response, the response object created by that serializer, and the error that occurred, if any. + */ +- (NSURLSessionUploadTask *)uploadTaskWithStreamedRequest:(NSURLRequest *)request + progress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgressBlock + completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject, NSError * _Nullable error))completionHandler; + +///----------------------------- +/// @name Running Download Tasks +///----------------------------- + +/** + Creates an `NSURLSessionDownloadTask` with the specified request. + + @param request The HTTP request for the request. + @param progress A block object to be executed when the download progress is updated. Note this block is called on the session queue, not the main queue. + @param destination A block object to be executed in order to determine the destination of the downloaded file. This block takes two arguments, the target path & the server response, and returns the desired file URL of the resulting download. The temporary file used during the download will be automatically deleted after being moved to the returned URL. + @param completionHandler A block to be executed when a task finishes. This block has no return value and takes three arguments: the server response, the path of the downloaded file, and the error describing the network or parsing error that occurred, if any. + + @warning If using a background `NSURLSessionConfiguration` on iOS, these blocks will be lost when the app is terminated. Background sessions may prefer to use `-setDownloadTaskDidFinishDownloadingBlock:` to specify the URL for saving the downloaded file, rather than the destination block of this method. + */ +- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request + progress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgressBlock + destination:(nullable NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination + completionHandler:(nullable void (^)(NSURLResponse *response, NSURL * _Nullable filePath, NSError * _Nullable error))completionHandler; + +/** + Creates an `NSURLSessionDownloadTask` with the specified resume data. + + @param resumeData The data used to resume downloading. + @param progress A block object to be executed when the download progress is updated. Note this block is called on the session queue, not the main queue. + @param destination A block object to be executed in order to determine the destination of the downloaded file. This block takes two arguments, the target path & the server response, and returns the desired file URL of the resulting download. The temporary file used during the download will be automatically deleted after being moved to the returned URL. + @param completionHandler A block to be executed when a task finishes. This block has no return value and takes three arguments: the server response, the path of the downloaded file, and the error describing the network or parsing error that occurred, if any. + */ +- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData + progress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgressBlock + destination:(nullable NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination + completionHandler:(nullable void (^)(NSURLResponse *response, NSURL * _Nullable filePath, NSError * _Nullable error))completionHandler; + +///--------------------------------- +/// @name Getting Progress for Tasks +///--------------------------------- + +/** + Returns the upload progress of the specified task. + + @param task The session task. Must not be `nil`. + + @return An `NSProgress` object reporting the upload progress of a task, or `nil` if the progress is unavailable. + */ +- (nullable NSProgress *)uploadProgressForTask:(NSURLSessionTask *)task; + +/** + Returns the download progress of the specified task. + + @param task The session task. Must not be `nil`. + + @return An `NSProgress` object reporting the download progress of a task, or `nil` if the progress is unavailable. + */ +- (nullable NSProgress *)downloadProgressForTask:(NSURLSessionTask *)task; + +///----------------------------------------- +/// @name Setting Session Delegate Callbacks +///----------------------------------------- + +/** + Sets a block to be executed when the managed session becomes invalid, as handled by the `NSURLSessionDelegate` method `URLSession:didBecomeInvalidWithError:`. + + @param block A block object to be executed when the managed session becomes invalid. The block has no return value, and takes two arguments: the session, and the error related to the cause of invalidation. + */ +- (void)setSessionDidBecomeInvalidBlock:(nullable void (^)(NSURLSession *session, NSError *error))block; + +/** + Sets a block to be executed when a connection level authentication challenge has occurred, as handled by the `NSURLSessionDelegate` method `URLSession:didReceiveChallenge:completionHandler:`. + + @param block A block object to be executed when a connection level authentication challenge has occurred. The block returns the disposition of the authentication challenge, and takes three arguments: the session, the authentication challenge, and a pointer to the credential that should be used to resolve the challenge. + */ +- (void)setSessionDidReceiveAuthenticationChallengeBlock:(nullable NSURLSessionAuthChallengeDisposition (^)(NSURLSession *session, NSURLAuthenticationChallenge *challenge, NSURLCredential * _Nullable __autoreleasing * _Nullable credential))block; + +///-------------------------------------- +/// @name Setting Task Delegate Callbacks +///-------------------------------------- + +/** + Sets a block to be executed when a task requires a new request body stream to send to the remote server, as handled by the `NSURLSessionTaskDelegate` method `URLSession:task:needNewBodyStream:`. + + @param block A block object to be executed when a task requires a new request body stream. + */ +- (void)setTaskNeedNewBodyStreamBlock:(nullable NSInputStream * (^)(NSURLSession *session, NSURLSessionTask *task))block; + +/** + Sets a block to be executed when an HTTP request is attempting to perform a redirection to a different URL, as handled by the `NSURLSessionTaskDelegate` method `URLSession:willPerformHTTPRedirection:newRequest:completionHandler:`. + + @param block A block object to be executed when an HTTP request is attempting to perform a redirection to a different URL. The block returns the request to be made for the redirection, and takes four arguments: the session, the task, the redirection response, and the request corresponding to the redirection response. + */ +- (void)setTaskWillPerformHTTPRedirectionBlock:(nullable NSURLRequest * (^)(NSURLSession *session, NSURLSessionTask *task, NSURLResponse *response, NSURLRequest *request))block; + +/** + Sets a block to be executed when a session task has received a request specific authentication challenge, as handled by the `NSURLSessionTaskDelegate` method `URLSession:task:didReceiveChallenge:completionHandler:`. + + @param block A block object to be executed when a session task has received a request specific authentication challenge. The block returns the disposition of the authentication challenge, and takes four arguments: the session, the task, the authentication challenge, and a pointer to the credential that should be used to resolve the challenge. + */ +- (void)setTaskDidReceiveAuthenticationChallengeBlock:(nullable NSURLSessionAuthChallengeDisposition (^)(NSURLSession *session, NSURLSessionTask *task, NSURLAuthenticationChallenge *challenge, NSURLCredential * _Nullable __autoreleasing * _Nullable credential))block; + +/** + Sets a block to be executed periodically to track upload progress, as handled by the `NSURLSessionTaskDelegate` method `URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:`. + + @param block A block object to be called when an undetermined number of bytes have been uploaded to the server. This block has no return value and takes five arguments: the session, the task, the number of bytes written since the last time the upload progress block was called, the total bytes written, and the total bytes expected to be written during the request, as initially determined by the length of the HTTP body. This block may be called multiple times, and will execute on the main thread. + */ +- (void)setTaskDidSendBodyDataBlock:(nullable void (^)(NSURLSession *session, NSURLSessionTask *task, int64_t bytesSent, int64_t totalBytesSent, int64_t totalBytesExpectedToSend))block; + +/** + Sets a block to be executed as the last message related to a specific task, as handled by the `NSURLSessionTaskDelegate` method `URLSession:task:didCompleteWithError:`. + + @param block A block object to be executed when a session task is completed. The block has no return value, and takes three arguments: the session, the task, and any error that occurred in the process of executing the task. + */ +- (void)setTaskDidCompleteBlock:(nullable void (^)(NSURLSession *session, NSURLSessionTask *task, NSError * _Nullable error))block; + +///------------------------------------------- +/// @name Setting Data Task Delegate Callbacks +///------------------------------------------- + +/** + Sets a block to be executed when a data task has received a response, as handled by the `NSURLSessionDataDelegate` method `URLSession:dataTask:didReceiveResponse:completionHandler:`. + + @param block A block object to be executed when a data task has received a response. The block returns the disposition of the session response, and takes three arguments: the session, the data task, and the received response. + */ +- (void)setDataTaskDidReceiveResponseBlock:(nullable NSURLSessionResponseDisposition (^)(NSURLSession *session, NSURLSessionDataTask *dataTask, NSURLResponse *response))block; + +/** + Sets a block to be executed when a data task has become a download task, as handled by the `NSURLSessionDataDelegate` method `URLSession:dataTask:didBecomeDownloadTask:`. + + @param block A block object to be executed when a data task has become a download task. The block has no return value, and takes three arguments: the session, the data task, and the download task it has become. + */ +- (void)setDataTaskDidBecomeDownloadTaskBlock:(nullable void (^)(NSURLSession *session, NSURLSessionDataTask *dataTask, NSURLSessionDownloadTask *downloadTask))block; + +/** + Sets a block to be executed when a data task receives data, as handled by the `NSURLSessionDataDelegate` method `URLSession:dataTask:didReceiveData:`. + + @param block A block object to be called when an undetermined number of bytes have been downloaded from the server. This block has no return value and takes three arguments: the session, the data task, and the data received. This block may be called multiple times, and will execute on the session manager operation queue. + */ +- (void)setDataTaskDidReceiveDataBlock:(nullable void (^)(NSURLSession *session, NSURLSessionDataTask *dataTask, NSData *data))block; + +/** + Sets a block to be executed to determine the caching behavior of a data task, as handled by the `NSURLSessionDataDelegate` method `URLSession:dataTask:willCacheResponse:completionHandler:`. + + @param block A block object to be executed to determine the caching behavior of a data task. The block returns the response to cache, and takes three arguments: the session, the data task, and the proposed cached URL response. + */ +- (void)setDataTaskWillCacheResponseBlock:(nullable NSCachedURLResponse * (^)(NSURLSession *session, NSURLSessionDataTask *dataTask, NSCachedURLResponse *proposedResponse))block; + +/** + Sets a block to be executed once all messages enqueued for a session have been delivered, as handled by the `NSURLSessionDataDelegate` method `URLSessionDidFinishEventsForBackgroundURLSession:`. + + @param block A block object to be executed once all messages enqueued for a session have been delivered. The block has no return value and takes a single argument: the session. + */ +- (void)setDidFinishEventsForBackgroundURLSessionBlock:(nullable void (^)(NSURLSession *session))block; + +///----------------------------------------------- +/// @name Setting Download Task Delegate Callbacks +///----------------------------------------------- + +/** + Sets a block to be executed when a download task has completed a download, as handled by the `NSURLSessionDownloadDelegate` method `URLSession:downloadTask:didFinishDownloadingToURL:`. + + @param block A block object to be executed when a download task has completed. The block returns the URL the download should be moved to, and takes three arguments: the session, the download task, and the temporary location of the downloaded file. If the file manager encounters an error while attempting to move the temporary file to the destination, an `AFURLSessionDownloadTaskDidFailToMoveFileNotification` will be posted, with the download task as its object, and the user info of the error. + */ +- (void)setDownloadTaskDidFinishDownloadingBlock:(nullable NSURL * _Nullable (^)(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, NSURL *location))block; + +/** + Sets a block to be executed periodically to track download progress, as handled by the `NSURLSessionDownloadDelegate` method `URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesWritten:totalBytesExpectedToWrite:`. + + @param block A block object to be called when an undetermined number of bytes have been downloaded from the server. This block has no return value and takes five arguments: the session, the download task, the number of bytes read since the last time the download progress block was called, the total bytes read, and the total bytes expected to be read during the request, as initially determined by the expected content size of the `NSHTTPURLResponse` object. This block may be called multiple times, and will execute on the session manager operation queue. + */ +- (void)setDownloadTaskDidWriteDataBlock:(nullable void (^)(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, int64_t bytesWritten, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite))block; + +/** + Sets a block to be executed when a download task has been resumed, as handled by the `NSURLSessionDownloadDelegate` method `URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:`. + + @param block A block object to be executed when a download task has been resumed. The block has no return value and takes four arguments: the session, the download task, the file offset of the resumed download, and the total number of bytes expected to be downloaded. + */ +- (void)setDownloadTaskDidResumeBlock:(nullable void (^)(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, int64_t fileOffset, int64_t expectedTotalBytes))block; + +@end + +///-------------------- +/// @name Notifications +///-------------------- + +/** + Posted when a task resumes. + */ +FOUNDATION_EXPORT NSString * const AFNetworkingTaskDidResumeNotification; + +/** + Posted when a task finishes executing. Includes a userInfo dictionary with additional information about the task. + */ +FOUNDATION_EXPORT NSString * const AFNetworkingTaskDidCompleteNotification; + +/** + Posted when a task suspends its execution. + */ +FOUNDATION_EXPORT NSString * const AFNetworkingTaskDidSuspendNotification; + +/** + Posted when a session is invalidated. + */ +FOUNDATION_EXPORT NSString * const AFURLSessionDidInvalidateNotification; + +/** + Posted when a session download task encountered an error when moving the temporary download file to a specified destination. + */ +FOUNDATION_EXPORT NSString * const AFURLSessionDownloadTaskDidFailToMoveFileNotification; + +/** + The raw response data of the task. Included in the userInfo dictionary of the `AFNetworkingTaskDidCompleteNotification` if response data exists for the task. + */ +FOUNDATION_EXPORT NSString * const AFNetworkingTaskDidCompleteResponseDataKey; + +/** + The serialized response object of the task. Included in the userInfo dictionary of the `AFNetworkingTaskDidCompleteNotification` if the response was serialized. + */ +FOUNDATION_EXPORT NSString * const AFNetworkingTaskDidCompleteSerializedResponseKey; + +/** + The response serializer used to serialize the response. Included in the userInfo dictionary of the `AFNetworkingTaskDidCompleteNotification` if the task has an associated response serializer. + */ +FOUNDATION_EXPORT NSString * const AFNetworkingTaskDidCompleteResponseSerializerKey; + +/** + The file path associated with the download task. Included in the userInfo dictionary of the `AFNetworkingTaskDidCompleteNotification` if an the response data has been stored directly to disk. + */ +FOUNDATION_EXPORT NSString * const AFNetworkingTaskDidCompleteAssetPathKey; + +/** + Any error associated with the task, or the serialization of the response. Included in the userInfo dictionary of the `AFNetworkingTaskDidCompleteNotification` if an error exists. + */ +FOUNDATION_EXPORT NSString * const AFNetworkingTaskDidCompleteErrorKey; + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFURLSessionManager.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFURLSessionManager.m new file mode 100644 index 0000000000..de447aea99 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/AFNetworking/AFURLSessionManager.m @@ -0,0 +1,1244 @@ +// AFURLSessionManager.m +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "AFURLSessionManager.h" +#import + +#ifndef NSFoundationVersionNumber_iOS_8_0 +#define NSFoundationVersionNumber_With_Fixed_5871104061079552_bug 1140.11 +#else +#define NSFoundationVersionNumber_With_Fixed_5871104061079552_bug NSFoundationVersionNumber_iOS_8_0 +#endif + +static dispatch_queue_t url_session_manager_creation_queue() { + static dispatch_queue_t af_url_session_manager_creation_queue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + af_url_session_manager_creation_queue = dispatch_queue_create("com.alamofire.networking.session.manager.creation", DISPATCH_QUEUE_SERIAL); + }); + + return af_url_session_manager_creation_queue; +} + +static void url_session_manager_create_task_safely(dispatch_block_t block) { + if (NSFoundationVersionNumber < NSFoundationVersionNumber_With_Fixed_5871104061079552_bug) { + // Fix of bug + // Open Radar:http://openradar.appspot.com/radar?id=5871104061079552 (status: Fixed in iOS8) + // Issue about:https://github.com/AFNetworking/AFNetworking/issues/2093 + dispatch_sync(url_session_manager_creation_queue(), block); + } else { + block(); + } +} + +static dispatch_queue_t url_session_manager_processing_queue() { + static dispatch_queue_t af_url_session_manager_processing_queue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + af_url_session_manager_processing_queue = dispatch_queue_create("com.alamofire.networking.session.manager.processing", DISPATCH_QUEUE_CONCURRENT); + }); + + return af_url_session_manager_processing_queue; +} + +static dispatch_group_t url_session_manager_completion_group() { + static dispatch_group_t af_url_session_manager_completion_group; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + af_url_session_manager_completion_group = dispatch_group_create(); + }); + + return af_url_session_manager_completion_group; +} + +NSString * const AFNetworkingTaskDidResumeNotification = @"com.alamofire.networking.task.resume"; +NSString * const AFNetworkingTaskDidCompleteNotification = @"com.alamofire.networking.task.complete"; +NSString * const AFNetworkingTaskDidSuspendNotification = @"com.alamofire.networking.task.suspend"; +NSString * const AFURLSessionDidInvalidateNotification = @"com.alamofire.networking.session.invalidate"; +NSString * const AFURLSessionDownloadTaskDidFailToMoveFileNotification = @"com.alamofire.networking.session.download.file-manager-error"; + +NSString * const AFNetworkingTaskDidCompleteSerializedResponseKey = @"com.alamofire.networking.task.complete.serializedresponse"; +NSString * const AFNetworkingTaskDidCompleteResponseSerializerKey = @"com.alamofire.networking.task.complete.responseserializer"; +NSString * const AFNetworkingTaskDidCompleteResponseDataKey = @"com.alamofire.networking.complete.finish.responsedata"; +NSString * const AFNetworkingTaskDidCompleteErrorKey = @"com.alamofire.networking.task.complete.error"; +NSString * const AFNetworkingTaskDidCompleteAssetPathKey = @"com.alamofire.networking.task.complete.assetpath"; + +static NSString * const AFURLSessionManagerLockName = @"com.alamofire.networking.session.manager.lock"; + +static NSUInteger const AFMaximumNumberOfAttemptsToRecreateBackgroundSessionUploadTask = 3; + +static void * AFTaskStateChangedContext = &AFTaskStateChangedContext; + +typedef void (^AFURLSessionDidBecomeInvalidBlock)(NSURLSession *session, NSError *error); +typedef NSURLSessionAuthChallengeDisposition (^AFURLSessionDidReceiveAuthenticationChallengeBlock)(NSURLSession *session, NSURLAuthenticationChallenge *challenge, NSURLCredential * __autoreleasing *credential); + +typedef NSURLRequest * (^AFURLSessionTaskWillPerformHTTPRedirectionBlock)(NSURLSession *session, NSURLSessionTask *task, NSURLResponse *response, NSURLRequest *request); +typedef NSURLSessionAuthChallengeDisposition (^AFURLSessionTaskDidReceiveAuthenticationChallengeBlock)(NSURLSession *session, NSURLSessionTask *task, NSURLAuthenticationChallenge *challenge, NSURLCredential * __autoreleasing *credential); +typedef void (^AFURLSessionDidFinishEventsForBackgroundURLSessionBlock)(NSURLSession *session); + +typedef NSInputStream * (^AFURLSessionTaskNeedNewBodyStreamBlock)(NSURLSession *session, NSURLSessionTask *task); +typedef void (^AFURLSessionTaskDidSendBodyDataBlock)(NSURLSession *session, NSURLSessionTask *task, int64_t bytesSent, int64_t totalBytesSent, int64_t totalBytesExpectedToSend); +typedef void (^AFURLSessionTaskDidCompleteBlock)(NSURLSession *session, NSURLSessionTask *task, NSError *error); + +typedef NSURLSessionResponseDisposition (^AFURLSessionDataTaskDidReceiveResponseBlock)(NSURLSession *session, NSURLSessionDataTask *dataTask, NSURLResponse *response); +typedef void (^AFURLSessionDataTaskDidBecomeDownloadTaskBlock)(NSURLSession *session, NSURLSessionDataTask *dataTask, NSURLSessionDownloadTask *downloadTask); +typedef void (^AFURLSessionDataTaskDidReceiveDataBlock)(NSURLSession *session, NSURLSessionDataTask *dataTask, NSData *data); +typedef NSCachedURLResponse * (^AFURLSessionDataTaskWillCacheResponseBlock)(NSURLSession *session, NSURLSessionDataTask *dataTask, NSCachedURLResponse *proposedResponse); + +typedef NSURL * (^AFURLSessionDownloadTaskDidFinishDownloadingBlock)(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, NSURL *location); +typedef void (^AFURLSessionDownloadTaskDidWriteDataBlock)(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, int64_t bytesWritten, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite); +typedef void (^AFURLSessionDownloadTaskDidResumeBlock)(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, int64_t fileOffset, int64_t expectedTotalBytes); +typedef void (^AFURLSessionTaskProgressBlock)(NSProgress *); + +typedef void (^AFURLSessionTaskCompletionHandler)(NSURLResponse *response, id responseObject, NSError *error); + + +#pragma mark - + +@interface AFURLSessionManagerTaskDelegate : NSObject +@property (nonatomic, weak) AFURLSessionManager *manager; +@property (nonatomic, strong) NSMutableData *mutableData; +@property (nonatomic, strong) NSProgress *uploadProgress; +@property (nonatomic, strong) NSProgress *downloadProgress; +@property (nonatomic, copy) NSURL *downloadFileURL; +@property (nonatomic, copy) AFURLSessionDownloadTaskDidFinishDownloadingBlock downloadTaskDidFinishDownloading; +@property (nonatomic, copy) AFURLSessionTaskProgressBlock uploadProgressBlock; +@property (nonatomic, copy) AFURLSessionTaskProgressBlock downloadProgressBlock; +@property (nonatomic, copy) AFURLSessionTaskCompletionHandler completionHandler; +@end + +@implementation AFURLSessionManagerTaskDelegate + +- (instancetype)init { + self = [super init]; + if (!self) { + return nil; + } + + self.mutableData = [NSMutableData data]; + self.uploadProgress = [[NSProgress alloc] initWithParent:nil userInfo:nil]; + self.uploadProgress.totalUnitCount = NSURLSessionTransferSizeUnknown; + + self.downloadProgress = [[NSProgress alloc] initWithParent:nil userInfo:nil]; + self.downloadProgress.totalUnitCount = NSURLSessionTransferSizeUnknown; + return self; +} + +#pragma mark - NSProgress Tracking + +- (void)setupProgressForTask:(NSURLSessionTask *)task { + __weak __typeof__(task) weakTask = task; + + self.uploadProgress.totalUnitCount = task.countOfBytesExpectedToSend; + self.downloadProgress.totalUnitCount = task.countOfBytesExpectedToReceive; + [self.uploadProgress setCancellable:YES]; + [self.uploadProgress setCancellationHandler:^{ + __typeof__(weakTask) strongTask = weakTask; + [strongTask cancel]; + }]; + [self.uploadProgress setPausable:YES]; + [self.uploadProgress setPausingHandler:^{ + __typeof__(weakTask) strongTask = weakTask; + [strongTask suspend]; + }]; + if ([self.uploadProgress respondsToSelector:@selector(setResumingHandler:)]) { + [self.uploadProgress setResumingHandler:^{ + __typeof__(weakTask) strongTask = weakTask; + [strongTask resume]; + }]; + } + + [self.downloadProgress setCancellable:YES]; + [self.downloadProgress setCancellationHandler:^{ + __typeof__(weakTask) strongTask = weakTask; + [strongTask cancel]; + }]; + [self.downloadProgress setPausable:YES]; + [self.downloadProgress setPausingHandler:^{ + __typeof__(weakTask) strongTask = weakTask; + [strongTask suspend]; + }]; + + if ([self.downloadProgress respondsToSelector:@selector(setResumingHandler:)]) { + [self.downloadProgress setResumingHandler:^{ + __typeof__(weakTask) strongTask = weakTask; + [strongTask resume]; + }]; + } + + [task addObserver:self + forKeyPath:NSStringFromSelector(@selector(countOfBytesReceived)) + options:NSKeyValueObservingOptionNew + context:NULL]; + [task addObserver:self + forKeyPath:NSStringFromSelector(@selector(countOfBytesExpectedToReceive)) + options:NSKeyValueObservingOptionNew + context:NULL]; + + [task addObserver:self + forKeyPath:NSStringFromSelector(@selector(countOfBytesSent)) + options:NSKeyValueObservingOptionNew + context:NULL]; + [task addObserver:self + forKeyPath:NSStringFromSelector(@selector(countOfBytesExpectedToSend)) + options:NSKeyValueObservingOptionNew + context:NULL]; + + [self.downloadProgress addObserver:self + forKeyPath:NSStringFromSelector(@selector(fractionCompleted)) + options:NSKeyValueObservingOptionNew + context:NULL]; + [self.uploadProgress addObserver:self + forKeyPath:NSStringFromSelector(@selector(fractionCompleted)) + options:NSKeyValueObservingOptionNew + context:NULL]; +} + +- (void)cleanUpProgressForTask:(NSURLSessionTask *)task { + [task removeObserver:self forKeyPath:NSStringFromSelector(@selector(countOfBytesReceived))]; + [task removeObserver:self forKeyPath:NSStringFromSelector(@selector(countOfBytesExpectedToReceive))]; + [task removeObserver:self forKeyPath:NSStringFromSelector(@selector(countOfBytesSent))]; + [task removeObserver:self forKeyPath:NSStringFromSelector(@selector(countOfBytesExpectedToSend))]; + [self.downloadProgress removeObserver:self forKeyPath:NSStringFromSelector(@selector(fractionCompleted))]; + [self.uploadProgress removeObserver:self forKeyPath:NSStringFromSelector(@selector(fractionCompleted))]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if ([object isKindOfClass:[NSURLSessionTask class]]) { + if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesReceived))]) { + self.downloadProgress.completedUnitCount = [change[@"new"] longLongValue]; + } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesExpectedToReceive))]) { + self.downloadProgress.totalUnitCount = [change[@"new"] longLongValue]; + } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesSent))]) { + self.uploadProgress.completedUnitCount = [change[@"new"] longLongValue]; + } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesExpectedToSend))]) { + self.uploadProgress.totalUnitCount = [change[@"new"] longLongValue]; + } + } + else if ([object isEqual:self.downloadProgress]) { + if (self.downloadProgressBlock) { + self.downloadProgressBlock(object); + } + } + else if ([object isEqual:self.uploadProgress]) { + if (self.uploadProgressBlock) { + self.uploadProgressBlock(object); + } + } +} + +#pragma mark - NSURLSessionTaskDelegate + +- (void)URLSession:(__unused NSURLSession *)session + task:(NSURLSessionTask *)task +didCompleteWithError:(NSError *)error +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgnu" + __strong AFURLSessionManager *manager = self.manager; + + __block id responseObject = nil; + + __block NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; + userInfo[AFNetworkingTaskDidCompleteResponseSerializerKey] = manager.responseSerializer; + + //Performance Improvement from #2672 + NSData *data = nil; + if (self.mutableData) { + data = [self.mutableData copy]; + //We no longer need the reference, so nil it out to gain back some memory. + self.mutableData = nil; + } + + if (self.downloadFileURL) { + userInfo[AFNetworkingTaskDidCompleteAssetPathKey] = self.downloadFileURL; + } else if (data) { + userInfo[AFNetworkingTaskDidCompleteResponseDataKey] = data; + } + + if (error) { + userInfo[AFNetworkingTaskDidCompleteErrorKey] = error; + + dispatch_group_async(manager.completionGroup ?: url_session_manager_completion_group(), manager.completionQueue ?: dispatch_get_main_queue(), ^{ + if (self.completionHandler) { + self.completionHandler(task.response, responseObject, error); + } + + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingTaskDidCompleteNotification object:task userInfo:userInfo]; + }); + }); + } else { + dispatch_async(url_session_manager_processing_queue(), ^{ + NSError *serializationError = nil; + responseObject = [manager.responseSerializer responseObjectForResponse:task.response data:data error:&serializationError]; + + if (self.downloadFileURL) { + responseObject = self.downloadFileURL; + } + + if (responseObject) { + userInfo[AFNetworkingTaskDidCompleteSerializedResponseKey] = responseObject; + } + + if (serializationError) { + userInfo[AFNetworkingTaskDidCompleteErrorKey] = serializationError; + } + + dispatch_group_async(manager.completionGroup ?: url_session_manager_completion_group(), manager.completionQueue ?: dispatch_get_main_queue(), ^{ + if (self.completionHandler) { + self.completionHandler(task.response, responseObject, serializationError); + } + + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingTaskDidCompleteNotification object:task userInfo:userInfo]; + }); + }); + }); + } +#pragma clang diagnostic pop +} + +#pragma mark - NSURLSessionDataTaskDelegate + +- (void)URLSession:(__unused NSURLSession *)session + dataTask:(__unused NSURLSessionDataTask *)dataTask + didReceiveData:(NSData *)data +{ + [self.mutableData appendData:data]; +} + +#pragma mark - NSURLSessionDownloadTaskDelegate + +- (void)URLSession:(NSURLSession *)session + downloadTask:(NSURLSessionDownloadTask *)downloadTask +didFinishDownloadingToURL:(NSURL *)location +{ + NSError *fileManagerError = nil; + self.downloadFileURL = nil; + + if (self.downloadTaskDidFinishDownloading) { + self.downloadFileURL = self.downloadTaskDidFinishDownloading(session, downloadTask, location); + if (self.downloadFileURL) { + [[NSFileManager defaultManager] moveItemAtURL:location toURL:self.downloadFileURL error:&fileManagerError]; + + if (fileManagerError) { + [[NSNotificationCenter defaultCenter] postNotificationName:AFURLSessionDownloadTaskDidFailToMoveFileNotification object:downloadTask userInfo:fileManagerError.userInfo]; + } + } + } +} + +@end + +#pragma mark - + +/** + * A workaround for issues related to key-value observing the `state` of an `NSURLSessionTask`. + * + * See: + * - https://github.com/AFNetworking/AFNetworking/issues/1477 + * - https://github.com/AFNetworking/AFNetworking/issues/2638 + * - https://github.com/AFNetworking/AFNetworking/pull/2702 + */ + +static inline void af_swizzleSelector(Class theClass, SEL originalSelector, SEL swizzledSelector) { + Method originalMethod = class_getInstanceMethod(theClass, originalSelector); + Method swizzledMethod = class_getInstanceMethod(theClass, swizzledSelector); + method_exchangeImplementations(originalMethod, swizzledMethod); +} + +static inline BOOL af_addMethod(Class theClass, SEL selector, Method method) { + return class_addMethod(theClass, selector, method_getImplementation(method), method_getTypeEncoding(method)); +} + +static NSString * const AFNSURLSessionTaskDidResumeNotification = @"com.alamofire.networking.nsurlsessiontask.resume"; +static NSString * const AFNSURLSessionTaskDidSuspendNotification = @"com.alamofire.networking.nsurlsessiontask.suspend"; + +@interface _AFURLSessionTaskSwizzling : NSObject + +@end + +@implementation _AFURLSessionTaskSwizzling + ++ (void)load { + /** + WARNING: Trouble Ahead + https://github.com/AFNetworking/AFNetworking/pull/2702 + */ + + if (NSClassFromString(@"NSURLSessionTask")) { + /** + iOS 7 and iOS 8 differ in NSURLSessionTask implementation, which makes the next bit of code a bit tricky. + Many Unit Tests have been built to validate as much of this behavior has possible. + Here is what we know: + - NSURLSessionTasks are implemented with class clusters, meaning the class you request from the API isn't actually the type of class you will get back. + - Simply referencing `[NSURLSessionTask class]` will not work. You need to ask an `NSURLSession` to actually create an object, and grab the class from there. + - On iOS 7, `localDataTask` is a `__NSCFLocalDataTask`, which inherits from `__NSCFLocalSessionTask`, which inherits from `__NSCFURLSessionTask`. + - On iOS 8, `localDataTask` is a `__NSCFLocalDataTask`, which inherits from `__NSCFLocalSessionTask`, which inherits from `NSURLSessionTask`. + - On iOS 7, `__NSCFLocalSessionTask` and `__NSCFURLSessionTask` are the only two classes that have their own implementations of `resume` and `suspend`, and `__NSCFLocalSessionTask` DOES NOT CALL SUPER. This means both classes need to be swizzled. + - On iOS 8, `NSURLSessionTask` is the only class that implements `resume` and `suspend`. This means this is the only class that needs to be swizzled. + - Because `NSURLSessionTask` is not involved in the class hierarchy for every version of iOS, its easier to add the swizzled methods to a dummy class and manage them there. + + Some Assumptions: + - No implementations of `resume` or `suspend` call super. If this were to change in a future version of iOS, we'd need to handle it. + - No background task classes override `resume` or `suspend` + + The current solution: + 1) Grab an instance of `__NSCFLocalDataTask` by asking an instance of `NSURLSession` for a data task. + 2) Grab a pointer to the original implementation of `af_resume` + 3) Check to see if the current class has an implementation of resume. If so, continue to step 4. + 4) Grab the super class of the current class. + 5) Grab a pointer for the current class to the current implementation of `resume`. + 6) Grab a pointer for the super class to the current implementation of `resume`. + 7) If the current class implementation of `resume` is not equal to the super class implementation of `resume` AND the current implementation of `resume` is not equal to the original implementation of `af_resume`, THEN swizzle the methods + 8) Set the current class to the super class, and repeat steps 3-8 + */ + NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration]; + NSURLSession * session = [NSURLSession sessionWithConfiguration:configuration]; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnonnull" + NSURLSessionDataTask *localDataTask = [session dataTaskWithURL:nil]; +#pragma clang diagnostic pop + IMP originalAFResumeIMP = method_getImplementation(class_getInstanceMethod([self class], @selector(af_resume))); + Class currentClass = [localDataTask class]; + + while (class_getInstanceMethod(currentClass, @selector(resume))) { + Class superClass = [currentClass superclass]; + IMP classResumeIMP = method_getImplementation(class_getInstanceMethod(currentClass, @selector(resume))); + IMP superclassResumeIMP = method_getImplementation(class_getInstanceMethod(superClass, @selector(resume))); + if (classResumeIMP != superclassResumeIMP && + originalAFResumeIMP != classResumeIMP) { + [self swizzleResumeAndSuspendMethodForClass:currentClass]; + } + currentClass = [currentClass superclass]; + } + + [localDataTask cancel]; + [session finishTasksAndInvalidate]; + } +} + ++ (void)swizzleResumeAndSuspendMethodForClass:(Class)theClass { + Method afResumeMethod = class_getInstanceMethod(self, @selector(af_resume)); + Method afSuspendMethod = class_getInstanceMethod(self, @selector(af_suspend)); + + if (af_addMethod(theClass, @selector(af_resume), afResumeMethod)) { + af_swizzleSelector(theClass, @selector(resume), @selector(af_resume)); + } + + if (af_addMethod(theClass, @selector(af_suspend), afSuspendMethod)) { + af_swizzleSelector(theClass, @selector(suspend), @selector(af_suspend)); + } +} + +- (NSURLSessionTaskState)state { + NSAssert(NO, @"State method should never be called in the actual dummy class"); + return NSURLSessionTaskStateCanceling; +} + +- (void)af_resume { + NSAssert([self respondsToSelector:@selector(state)], @"Does not respond to state"); + NSURLSessionTaskState state = [self state]; + [self af_resume]; + + if (state != NSURLSessionTaskStateRunning) { + [[NSNotificationCenter defaultCenter] postNotificationName:AFNSURLSessionTaskDidResumeNotification object:self]; + } +} + +- (void)af_suspend { + NSAssert([self respondsToSelector:@selector(state)], @"Does not respond to state"); + NSURLSessionTaskState state = [self state]; + [self af_suspend]; + + if (state != NSURLSessionTaskStateSuspended) { + [[NSNotificationCenter defaultCenter] postNotificationName:AFNSURLSessionTaskDidSuspendNotification object:self]; + } +} +@end + +#pragma mark - + +@interface AFURLSessionManager () +@property (readwrite, nonatomic, strong) NSURLSessionConfiguration *sessionConfiguration; +@property (readwrite, nonatomic, strong) NSOperationQueue *operationQueue; +@property (readwrite, nonatomic, strong) NSURLSession *session; +@property (readwrite, nonatomic, strong) NSMutableDictionary *mutableTaskDelegatesKeyedByTaskIdentifier; +@property (readonly, nonatomic, copy) NSString *taskDescriptionForSessionTasks; +@property (readwrite, nonatomic, strong) NSLock *lock; +@property (readwrite, nonatomic, copy) AFURLSessionDidBecomeInvalidBlock sessionDidBecomeInvalid; +@property (readwrite, nonatomic, copy) AFURLSessionDidReceiveAuthenticationChallengeBlock sessionDidReceiveAuthenticationChallenge; +@property (readwrite, nonatomic, copy) AFURLSessionDidFinishEventsForBackgroundURLSessionBlock didFinishEventsForBackgroundURLSession; +@property (readwrite, nonatomic, copy) AFURLSessionTaskWillPerformHTTPRedirectionBlock taskWillPerformHTTPRedirection; +@property (readwrite, nonatomic, copy) AFURLSessionTaskDidReceiveAuthenticationChallengeBlock taskDidReceiveAuthenticationChallenge; +@property (readwrite, nonatomic, copy) AFURLSessionTaskNeedNewBodyStreamBlock taskNeedNewBodyStream; +@property (readwrite, nonatomic, copy) AFURLSessionTaskDidSendBodyDataBlock taskDidSendBodyData; +@property (readwrite, nonatomic, copy) AFURLSessionTaskDidCompleteBlock taskDidComplete; +@property (readwrite, nonatomic, copy) AFURLSessionDataTaskDidReceiveResponseBlock dataTaskDidReceiveResponse; +@property (readwrite, nonatomic, copy) AFURLSessionDataTaskDidBecomeDownloadTaskBlock dataTaskDidBecomeDownloadTask; +@property (readwrite, nonatomic, copy) AFURLSessionDataTaskDidReceiveDataBlock dataTaskDidReceiveData; +@property (readwrite, nonatomic, copy) AFURLSessionDataTaskWillCacheResponseBlock dataTaskWillCacheResponse; +@property (readwrite, nonatomic, copy) AFURLSessionDownloadTaskDidFinishDownloadingBlock downloadTaskDidFinishDownloading; +@property (readwrite, nonatomic, copy) AFURLSessionDownloadTaskDidWriteDataBlock downloadTaskDidWriteData; +@property (readwrite, nonatomic, copy) AFURLSessionDownloadTaskDidResumeBlock downloadTaskDidResume; +@end + +@implementation AFURLSessionManager + +- (instancetype)init { + return [self initWithSessionConfiguration:nil]; +} + +- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration { + self = [super init]; + if (!self) { + return nil; + } + + if (!configuration) { + configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; + } + + self.sessionConfiguration = configuration; + + self.operationQueue = [[NSOperationQueue alloc] init]; + self.operationQueue.maxConcurrentOperationCount = 1; + + self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue]; + + self.responseSerializer = [AFJSONResponseSerializer serializer]; + + self.securityPolicy = [AFSecurityPolicy defaultPolicy]; + +#if !TARGET_OS_WATCH + self.reachabilityManager = [AFNetworkReachabilityManager sharedManager]; +#endif + + self.mutableTaskDelegatesKeyedByTaskIdentifier = [[NSMutableDictionary alloc] init]; + + self.lock = [[NSLock alloc] init]; + self.lock.name = AFURLSessionManagerLockName; + + [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) { + for (NSURLSessionDataTask *task in dataTasks) { + [self addDelegateForDataTask:task uploadProgress:nil downloadProgress:nil completionHandler:nil]; + } + + for (NSURLSessionUploadTask *uploadTask in uploadTasks) { + [self addDelegateForUploadTask:uploadTask progress:nil completionHandler:nil]; + } + + for (NSURLSessionDownloadTask *downloadTask in downloadTasks) { + [self addDelegateForDownloadTask:downloadTask progress:nil destination:nil completionHandler:nil]; + } + }]; + + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +#pragma mark - + +- (NSString *)taskDescriptionForSessionTasks { + return [NSString stringWithFormat:@"%p", self]; +} + +- (void)taskDidResume:(NSNotification *)notification { + NSURLSessionTask *task = notification.object; + if ([task respondsToSelector:@selector(taskDescription)]) { + if ([task.taskDescription isEqualToString:self.taskDescriptionForSessionTasks]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingTaskDidResumeNotification object:task]; + }); + } + } +} + +- (void)taskDidSuspend:(NSNotification *)notification { + NSURLSessionTask *task = notification.object; + if ([task respondsToSelector:@selector(taskDescription)]) { + if ([task.taskDescription isEqualToString:self.taskDescriptionForSessionTasks]) { + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingTaskDidSuspendNotification object:task]; + }); + } + } +} + +#pragma mark - + +- (AFURLSessionManagerTaskDelegate *)delegateForTask:(NSURLSessionTask *)task { + NSParameterAssert(task); + + AFURLSessionManagerTaskDelegate *delegate = nil; + [self.lock lock]; + delegate = self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)]; + [self.lock unlock]; + + return delegate; +} + +- (void)setDelegate:(AFURLSessionManagerTaskDelegate *)delegate + forTask:(NSURLSessionTask *)task +{ + NSParameterAssert(task); + NSParameterAssert(delegate); + + [self.lock lock]; + self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)] = delegate; + [delegate setupProgressForTask:task]; + [self addNotificationObserverForTask:task]; + [self.lock unlock]; +} + +- (void)addDelegateForDataTask:(NSURLSessionDataTask *)dataTask + uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgressBlock + downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgressBlock + completionHandler:(void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler +{ + AFURLSessionManagerTaskDelegate *delegate = [[AFURLSessionManagerTaskDelegate alloc] init]; + delegate.manager = self; + delegate.completionHandler = completionHandler; + + dataTask.taskDescription = self.taskDescriptionForSessionTasks; + [self setDelegate:delegate forTask:dataTask]; + + delegate.uploadProgressBlock = uploadProgressBlock; + delegate.downloadProgressBlock = downloadProgressBlock; +} + +- (void)addDelegateForUploadTask:(NSURLSessionUploadTask *)uploadTask + progress:(void (^)(NSProgress *uploadProgress)) uploadProgressBlock + completionHandler:(void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler +{ + AFURLSessionManagerTaskDelegate *delegate = [[AFURLSessionManagerTaskDelegate alloc] init]; + delegate.manager = self; + delegate.completionHandler = completionHandler; + + uploadTask.taskDescription = self.taskDescriptionForSessionTasks; + + [self setDelegate:delegate forTask:uploadTask]; + + delegate.uploadProgressBlock = uploadProgressBlock; +} + +- (void)addDelegateForDownloadTask:(NSURLSessionDownloadTask *)downloadTask + progress:(void (^)(NSProgress *downloadProgress)) downloadProgressBlock + destination:(NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination + completionHandler:(void (^)(NSURLResponse *response, NSURL *filePath, NSError *error))completionHandler +{ + AFURLSessionManagerTaskDelegate *delegate = [[AFURLSessionManagerTaskDelegate alloc] init]; + delegate.manager = self; + delegate.completionHandler = completionHandler; + + if (destination) { + delegate.downloadTaskDidFinishDownloading = ^NSURL * (NSURLSession * __unused session, NSURLSessionDownloadTask *task, NSURL *location) { + return destination(location, task.response); + }; + } + + downloadTask.taskDescription = self.taskDescriptionForSessionTasks; + + [self setDelegate:delegate forTask:downloadTask]; + + delegate.downloadProgressBlock = downloadProgressBlock; +} + +- (void)removeDelegateForTask:(NSURLSessionTask *)task { + NSParameterAssert(task); + + AFURLSessionManagerTaskDelegate *delegate = [self delegateForTask:task]; + [self.lock lock]; + [delegate cleanUpProgressForTask:task]; + [self removeNotificationObserverForTask:task]; + [self.mutableTaskDelegatesKeyedByTaskIdentifier removeObjectForKey:@(task.taskIdentifier)]; + [self.lock unlock]; +} + +#pragma mark - + +- (NSArray *)tasksForKeyPath:(NSString *)keyPath { + __block NSArray *tasks = nil; + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) { + if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) { + tasks = dataTasks; + } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) { + tasks = uploadTasks; + } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) { + tasks = downloadTasks; + } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) { + tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"]; + } + + dispatch_semaphore_signal(semaphore); + }]; + + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + + return tasks; +} + +- (NSArray *)tasks { + return [self tasksForKeyPath:NSStringFromSelector(_cmd)]; +} + +- (NSArray *)dataTasks { + return [self tasksForKeyPath:NSStringFromSelector(_cmd)]; +} + +- (NSArray *)uploadTasks { + return [self tasksForKeyPath:NSStringFromSelector(_cmd)]; +} + +- (NSArray *)downloadTasks { + return [self tasksForKeyPath:NSStringFromSelector(_cmd)]; +} + +#pragma mark - + +- (void)invalidateSessionCancelingTasks:(BOOL)cancelPendingTasks { + dispatch_async(dispatch_get_main_queue(), ^{ + if (cancelPendingTasks) { + [self.session invalidateAndCancel]; + } else { + [self.session finishTasksAndInvalidate]; + } + }); +} + +#pragma mark - + +- (void)setResponseSerializer:(id )responseSerializer { + NSParameterAssert(responseSerializer); + + _responseSerializer = responseSerializer; +} + +#pragma mark - +- (void)addNotificationObserverForTask:(NSURLSessionTask *)task { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(taskDidResume:) name:AFNSURLSessionTaskDidResumeNotification object:task]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(taskDidSuspend:) name:AFNSURLSessionTaskDidSuspendNotification object:task]; +} + +- (void)removeNotificationObserverForTask:(NSURLSessionTask *)task { + [[NSNotificationCenter defaultCenter] removeObserver:self name:AFNSURLSessionTaskDidSuspendNotification object:task]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:AFNSURLSessionTaskDidResumeNotification object:task]; +} + +#pragma mark - + +- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request + completionHandler:(void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler +{ + return [self dataTaskWithRequest:request uploadProgress:nil downloadProgress:nil completionHandler:completionHandler]; +} + +- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request + uploadProgress:(nullable void (^)(NSProgress *uploadProgress)) uploadProgressBlock + downloadProgress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgressBlock + completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject, NSError * _Nullable error))completionHandler { + + __block NSURLSessionDataTask *dataTask = nil; + url_session_manager_create_task_safely(^{ + dataTask = [self.session dataTaskWithRequest:request]; + }); + + [self addDelegateForDataTask:dataTask uploadProgress:uploadProgressBlock downloadProgress:downloadProgressBlock completionHandler:completionHandler]; + + return dataTask; +} + +#pragma mark - + +- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request + fromFile:(NSURL *)fileURL + progress:(void (^)(NSProgress *uploadProgress)) uploadProgressBlock + completionHandler:(void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler +{ + __block NSURLSessionUploadTask *uploadTask = nil; + url_session_manager_create_task_safely(^{ + uploadTask = [self.session uploadTaskWithRequest:request fromFile:fileURL]; + }); + + if (!uploadTask && self.attemptsToRecreateUploadTasksForBackgroundSessions && self.session.configuration.identifier) { + for (NSUInteger attempts = 0; !uploadTask && attempts < AFMaximumNumberOfAttemptsToRecreateBackgroundSessionUploadTask; attempts++) { + uploadTask = [self.session uploadTaskWithRequest:request fromFile:fileURL]; + } + } + + [self addDelegateForUploadTask:uploadTask progress:uploadProgressBlock completionHandler:completionHandler]; + + return uploadTask; +} + +- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request + fromData:(NSData *)bodyData + progress:(void (^)(NSProgress *uploadProgress)) uploadProgressBlock + completionHandler:(void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler +{ + __block NSURLSessionUploadTask *uploadTask = nil; + url_session_manager_create_task_safely(^{ + uploadTask = [self.session uploadTaskWithRequest:request fromData:bodyData]; + }); + + [self addDelegateForUploadTask:uploadTask progress:uploadProgressBlock completionHandler:completionHandler]; + + return uploadTask; +} + +- (NSURLSessionUploadTask *)uploadTaskWithStreamedRequest:(NSURLRequest *)request + progress:(void (^)(NSProgress *uploadProgress)) uploadProgressBlock + completionHandler:(void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler +{ + __block NSURLSessionUploadTask *uploadTask = nil; + url_session_manager_create_task_safely(^{ + uploadTask = [self.session uploadTaskWithStreamedRequest:request]; + }); + + [self addDelegateForUploadTask:uploadTask progress:uploadProgressBlock completionHandler:completionHandler]; + + return uploadTask; +} + +#pragma mark - + +- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request + progress:(void (^)(NSProgress *downloadProgress)) downloadProgressBlock + destination:(NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination + completionHandler:(void (^)(NSURLResponse *response, NSURL *filePath, NSError *error))completionHandler +{ + __block NSURLSessionDownloadTask *downloadTask = nil; + url_session_manager_create_task_safely(^{ + downloadTask = [self.session downloadTaskWithRequest:request]; + }); + + [self addDelegateForDownloadTask:downloadTask progress:downloadProgressBlock destination:destination completionHandler:completionHandler]; + + return downloadTask; +} + +- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData + progress:(void (^)(NSProgress *downloadProgress)) downloadProgressBlock + destination:(NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination + completionHandler:(void (^)(NSURLResponse *response, NSURL *filePath, NSError *error))completionHandler +{ + __block NSURLSessionDownloadTask *downloadTask = nil; + url_session_manager_create_task_safely(^{ + downloadTask = [self.session downloadTaskWithResumeData:resumeData]; + }); + + [self addDelegateForDownloadTask:downloadTask progress:downloadProgressBlock destination:destination completionHandler:completionHandler]; + + return downloadTask; +} + +#pragma mark - +- (NSProgress *)uploadProgressForTask:(NSURLSessionTask *)task { + return [[self delegateForTask:task] uploadProgress]; +} + +- (NSProgress *)downloadProgressForTask:(NSURLSessionTask *)task { + return [[self delegateForTask:task] downloadProgress]; +} + +#pragma mark - + +- (void)setSessionDidBecomeInvalidBlock:(void (^)(NSURLSession *session, NSError *error))block { + self.sessionDidBecomeInvalid = block; +} + +- (void)setSessionDidReceiveAuthenticationChallengeBlock:(NSURLSessionAuthChallengeDisposition (^)(NSURLSession *session, NSURLAuthenticationChallenge *challenge, NSURLCredential * __autoreleasing *credential))block { + self.sessionDidReceiveAuthenticationChallenge = block; +} + +- (void)setDidFinishEventsForBackgroundURLSessionBlock:(void (^)(NSURLSession *session))block { + self.didFinishEventsForBackgroundURLSession = block; +} + +#pragma mark - + +- (void)setTaskNeedNewBodyStreamBlock:(NSInputStream * (^)(NSURLSession *session, NSURLSessionTask *task))block { + self.taskNeedNewBodyStream = block; +} + +- (void)setTaskWillPerformHTTPRedirectionBlock:(NSURLRequest * (^)(NSURLSession *session, NSURLSessionTask *task, NSURLResponse *response, NSURLRequest *request))block { + self.taskWillPerformHTTPRedirection = block; +} + +- (void)setTaskDidReceiveAuthenticationChallengeBlock:(NSURLSessionAuthChallengeDisposition (^)(NSURLSession *session, NSURLSessionTask *task, NSURLAuthenticationChallenge *challenge, NSURLCredential * __autoreleasing *credential))block { + self.taskDidReceiveAuthenticationChallenge = block; +} + +- (void)setTaskDidSendBodyDataBlock:(void (^)(NSURLSession *session, NSURLSessionTask *task, int64_t bytesSent, int64_t totalBytesSent, int64_t totalBytesExpectedToSend))block { + self.taskDidSendBodyData = block; +} + +- (void)setTaskDidCompleteBlock:(void (^)(NSURLSession *session, NSURLSessionTask *task, NSError *error))block { + self.taskDidComplete = block; +} + +#pragma mark - + +- (void)setDataTaskDidReceiveResponseBlock:(NSURLSessionResponseDisposition (^)(NSURLSession *session, NSURLSessionDataTask *dataTask, NSURLResponse *response))block { + self.dataTaskDidReceiveResponse = block; +} + +- (void)setDataTaskDidBecomeDownloadTaskBlock:(void (^)(NSURLSession *session, NSURLSessionDataTask *dataTask, NSURLSessionDownloadTask *downloadTask))block { + self.dataTaskDidBecomeDownloadTask = block; +} + +- (void)setDataTaskDidReceiveDataBlock:(void (^)(NSURLSession *session, NSURLSessionDataTask *dataTask, NSData *data))block { + self.dataTaskDidReceiveData = block; +} + +- (void)setDataTaskWillCacheResponseBlock:(NSCachedURLResponse * (^)(NSURLSession *session, NSURLSessionDataTask *dataTask, NSCachedURLResponse *proposedResponse))block { + self.dataTaskWillCacheResponse = block; +} + +#pragma mark - + +- (void)setDownloadTaskDidFinishDownloadingBlock:(NSURL * (^)(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, NSURL *location))block { + self.downloadTaskDidFinishDownloading = block; +} + +- (void)setDownloadTaskDidWriteDataBlock:(void (^)(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, int64_t bytesWritten, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite))block { + self.downloadTaskDidWriteData = block; +} + +- (void)setDownloadTaskDidResumeBlock:(void (^)(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, int64_t fileOffset, int64_t expectedTotalBytes))block { + self.downloadTaskDidResume = block; +} + +#pragma mark - NSObject + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@: %p, session: %@, operationQueue: %@>", NSStringFromClass([self class]), self, self.session, self.operationQueue]; +} + +- (BOOL)respondsToSelector:(SEL)selector { + if (selector == @selector(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)) { + return self.taskWillPerformHTTPRedirection != nil; + } else if (selector == @selector(URLSession:dataTask:didReceiveResponse:completionHandler:)) { + return self.dataTaskDidReceiveResponse != nil; + } else if (selector == @selector(URLSession:dataTask:willCacheResponse:completionHandler:)) { + return self.dataTaskWillCacheResponse != nil; + } else if (selector == @selector(URLSessionDidFinishEventsForBackgroundURLSession:)) { + return self.didFinishEventsForBackgroundURLSession != nil; + } + + return [[self class] instancesRespondToSelector:selector]; +} + +#pragma mark - NSURLSessionDelegate + +- (void)URLSession:(NSURLSession *)session +didBecomeInvalidWithError:(NSError *)error +{ + if (self.sessionDidBecomeInvalid) { + self.sessionDidBecomeInvalid(session, error); + } + + [[NSNotificationCenter defaultCenter] postNotificationName:AFURLSessionDidInvalidateNotification object:session]; +} + +- (void)URLSession:(NSURLSession *)session +didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge + completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler +{ + NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling; + __block NSURLCredential *credential = nil; + + if (self.sessionDidReceiveAuthenticationChallenge) { + disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential); + } else { + if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { + if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) { + credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; + if (credential) { + disposition = NSURLSessionAuthChallengeUseCredential; + } else { + disposition = NSURLSessionAuthChallengePerformDefaultHandling; + } + } else { + disposition = NSURLSessionAuthChallengeRejectProtectionSpace; + } + } else { + disposition = NSURLSessionAuthChallengePerformDefaultHandling; + } + } + + if (completionHandler) { + completionHandler(disposition, credential); + } +} + +#pragma mark - NSURLSessionTaskDelegate + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task +willPerformHTTPRedirection:(NSHTTPURLResponse *)response + newRequest:(NSURLRequest *)request + completionHandler:(void (^)(NSURLRequest *))completionHandler +{ + NSURLRequest *redirectRequest = request; + + if (self.taskWillPerformHTTPRedirection) { + redirectRequest = self.taskWillPerformHTTPRedirection(session, task, response, request); + } + + if (completionHandler) { + completionHandler(redirectRequest); + } +} + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task +didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge + completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler +{ + NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling; + __block NSURLCredential *credential = nil; + + if (self.taskDidReceiveAuthenticationChallenge) { + disposition = self.taskDidReceiveAuthenticationChallenge(session, task, challenge, &credential); + } else { + if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { + if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) { + disposition = NSURLSessionAuthChallengeUseCredential; + credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; + } else { + disposition = NSURLSessionAuthChallengeRejectProtectionSpace; + } + } else { + disposition = NSURLSessionAuthChallengePerformDefaultHandling; + } + } + + if (completionHandler) { + completionHandler(disposition, credential); + } +} + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task + needNewBodyStream:(void (^)(NSInputStream *bodyStream))completionHandler +{ + NSInputStream *inputStream = nil; + + if (self.taskNeedNewBodyStream) { + inputStream = self.taskNeedNewBodyStream(session, task); + } else if (task.originalRequest.HTTPBodyStream && [task.originalRequest.HTTPBodyStream conformsToProtocol:@protocol(NSCopying)]) { + inputStream = [task.originalRequest.HTTPBodyStream copy]; + } + + if (completionHandler) { + completionHandler(inputStream); + } +} + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task + didSendBodyData:(int64_t)bytesSent + totalBytesSent:(int64_t)totalBytesSent +totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend +{ + + int64_t totalUnitCount = totalBytesExpectedToSend; + if(totalUnitCount == NSURLSessionTransferSizeUnknown) { + NSString *contentLength = [task.originalRequest valueForHTTPHeaderField:@"Content-Length"]; + if(contentLength) { + totalUnitCount = (int64_t) [contentLength longLongValue]; + } + } + + if (self.taskDidSendBodyData) { + self.taskDidSendBodyData(session, task, bytesSent, totalBytesSent, totalUnitCount); + } +} + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task +didCompleteWithError:(NSError *)error +{ + AFURLSessionManagerTaskDelegate *delegate = [self delegateForTask:task]; + + // delegate may be nil when completing a task in the background + if (delegate) { + [delegate URLSession:session task:task didCompleteWithError:error]; + + [self removeDelegateForTask:task]; + } + + if (self.taskDidComplete) { + self.taskDidComplete(session, task, error); + } +} + +#pragma mark - NSURLSessionDataDelegate + +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask +didReceiveResponse:(NSURLResponse *)response + completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler +{ + NSURLSessionResponseDisposition disposition = NSURLSessionResponseAllow; + + if (self.dataTaskDidReceiveResponse) { + disposition = self.dataTaskDidReceiveResponse(session, dataTask, response); + } + + if (completionHandler) { + completionHandler(disposition); + } +} + +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask +didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask +{ + AFURLSessionManagerTaskDelegate *delegate = [self delegateForTask:dataTask]; + if (delegate) { + [self removeDelegateForTask:dataTask]; + [self setDelegate:delegate forTask:downloadTask]; + } + + if (self.dataTaskDidBecomeDownloadTask) { + self.dataTaskDidBecomeDownloadTask(session, dataTask, downloadTask); + } +} + +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask + didReceiveData:(NSData *)data +{ + + AFURLSessionManagerTaskDelegate *delegate = [self delegateForTask:dataTask]; + [delegate URLSession:session dataTask:dataTask didReceiveData:data]; + + if (self.dataTaskDidReceiveData) { + self.dataTaskDidReceiveData(session, dataTask, data); + } +} + +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask + willCacheResponse:(NSCachedURLResponse *)proposedResponse + completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler +{ + NSCachedURLResponse *cachedResponse = proposedResponse; + + if (self.dataTaskWillCacheResponse) { + cachedResponse = self.dataTaskWillCacheResponse(session, dataTask, proposedResponse); + } + + if (completionHandler) { + completionHandler(cachedResponse); + } +} + +- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session { + if (self.didFinishEventsForBackgroundURLSession) { + dispatch_async(dispatch_get_main_queue(), ^{ + self.didFinishEventsForBackgroundURLSession(session); + }); + } +} + +#pragma mark - NSURLSessionDownloadDelegate + +- (void)URLSession:(NSURLSession *)session + downloadTask:(NSURLSessionDownloadTask *)downloadTask +didFinishDownloadingToURL:(NSURL *)location +{ + AFURLSessionManagerTaskDelegate *delegate = [self delegateForTask:downloadTask]; + if (self.downloadTaskDidFinishDownloading) { + NSURL *fileURL = self.downloadTaskDidFinishDownloading(session, downloadTask, location); + if (fileURL) { + delegate.downloadFileURL = fileURL; + NSError *error = nil; + [[NSFileManager defaultManager] moveItemAtURL:location toURL:fileURL error:&error]; + if (error) { + [[NSNotificationCenter defaultCenter] postNotificationName:AFURLSessionDownloadTaskDidFailToMoveFileNotification object:downloadTask userInfo:error.userInfo]; + } + + return; + } + } + + if (delegate) { + [delegate URLSession:session downloadTask:downloadTask didFinishDownloadingToURL:location]; + } +} + +- (void)URLSession:(NSURLSession *)session + downloadTask:(NSURLSessionDownloadTask *)downloadTask + didWriteData:(int64_t)bytesWritten + totalBytesWritten:(int64_t)totalBytesWritten +totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite +{ + if (self.downloadTaskDidWriteData) { + self.downloadTaskDidWriteData(session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite); + } +} + +- (void)URLSession:(NSURLSession *)session + downloadTask:(NSURLSessionDownloadTask *)downloadTask + didResumeAtOffset:(int64_t)fileOffset +expectedTotalBytes:(int64_t)expectedTotalBytes +{ + if (self.downloadTaskDidResume) { + self.downloadTaskDidResume(session, downloadTask, fileOffset, expectedTotalBytes); + } +} + +#pragma mark - NSSecureCoding + ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (instancetype)initWithCoder:(NSCoder *)decoder { + NSURLSessionConfiguration *configuration = [decoder decodeObjectOfClass:[NSURLSessionConfiguration class] forKey:@"sessionConfiguration"]; + + self = [self initWithSessionConfiguration:configuration]; + if (!self) { + return nil; + } + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:self.session.configuration forKey:@"sessionConfiguration"]; +} + +#pragma mark - NSCopying + +- (instancetype)copyWithZone:(NSZone *)zone { + return [[[self class] allocWithZone:zone] initWithSessionConfiguration:self.session.configuration]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/LICENSE b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/LICENSE new file mode 100644 index 0000000000..91f125b005 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/README.md b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/README.md new file mode 100644 index 0000000000..f784681462 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/README.md @@ -0,0 +1,320 @@ +

+ AFNetworking +

+ +[![Build Status](https://travis-ci.org/AFNetworking/AFNetworking.svg)](https://travis-ci.org/AFNetworking/AFNetworking) +[![codecov.io](https://codecov.io/github/AFNetworking/AFNetworking/coverage.svg?branch=master)](https://codecov.io/github/AFNetworking/AFNetworking?branch=master) +[![Cocoapods Compatible](https://img.shields.io/cocoapods/v/AFNetworking.svg)](https://img.shields.io/cocoapods/v/AFNetworking.svg) +[![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) +[![Platform](https://img.shields.io/cocoapods/p/AFNetworking.svg?style=flat)](http://cocoadocs.org/docsets/AFNetworking) +[![Twitter](https://img.shields.io/badge/twitter-@AFNetworking-blue.svg?style=flat)](http://twitter.com/AFNetworking) + +AFNetworking is a delightful networking library for iOS and Mac OS X. It's built on top of the [Foundation URL Loading System](http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/URLLoadingSystem/URLLoadingSystem.html), extending the powerful high-level networking abstractions built into Cocoa. It has a modular architecture with well-designed, feature-rich APIs that are a joy to use. + +Perhaps the most important feature of all, however, is the amazing community of developers who use and contribute to AFNetworking every day. AFNetworking powers some of the most popular and critically-acclaimed apps on the iPhone, iPad, and Mac. + +Choose AFNetworking for your next project, or migrate over your existing projects—you'll be happy you did! + +## How To Get Started + +- [Download AFNetworking](https://github.com/AFNetworking/AFNetworking/archive/master.zip) and try out the included Mac and iPhone example apps +- Read the ["Getting Started" guide](https://github.com/AFNetworking/AFNetworking/wiki/Getting-Started-with-AFNetworking), [FAQ](https://github.com/AFNetworking/AFNetworking/wiki/AFNetworking-FAQ), or [other articles on the Wiki](https://github.com/AFNetworking/AFNetworking/wiki) +- Check out the [documentation](http://cocoadocs.org/docsets/AFNetworking/) for a comprehensive look at all of the APIs available in AFNetworking +- Read the [AFNetworking 3.0 Migration Guide](https://github.com/AFNetworking/AFNetworking/wiki/AFNetworking-3.0-Migration-Guide) for an overview of the architectural changes from 2.0. + +## Communication + +- If you **need help**, use [Stack Overflow](http://stackoverflow.com/questions/tagged/afnetworking). (Tag 'afnetworking') +- If you'd like to **ask a general question**, use [Stack Overflow](http://stackoverflow.com/questions/tagged/afnetworking). +- If you **found a bug**, _and can provide steps to reliably reproduce it_, open an issue. +- If you **have a feature request**, open an issue. +- If you **want to contribute**, submit a pull request. + +## Installation +AFNetworking supports multiple methods for installing the library in a project. + +## Installation with CocoaPods + +[CocoaPods](http://cocoapods.org) is a dependency manager for Objective-C, which automates and simplifies the process of using 3rd-party libraries like AFNetworking in your projects. See the ["Getting Started" guide for more information](https://github.com/AFNetworking/AFNetworking/wiki/Getting-Started-with-AFNetworking). You can install it with the following command: + +```bash +$ gem install cocoapods +``` + +> CocoaPods 0.39.0+ is required to build AFNetworking 3.0.0+. + +#### Podfile + +To integrate AFNetworking into your Xcode project using CocoaPods, specify it in your `Podfile`: + +```ruby +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '8.0' + +pod 'AFNetworking', '~> 3.0' +``` + +Then, run the following command: + +```bash +$ pod install +``` + +### Installation with Carthage + +[Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. + +You can install Carthage with [Homebrew](http://brew.sh/) using the following command: + +```bash +$ brew update +$ brew install carthage +``` + +To integrate AFNetworking into your Xcode project using Carthage, specify it in your `Cartfile`: + +```ogdl +github "AFNetworking/AFNetworking" ~> 3.0 +``` + +Run `carthage` to build the framework and drag the built `AFNetworking.framework` into your Xcode project. + +## Requirements + +| AFNetworking Version | Minimum iOS Target | Minimum OS X Target | Minimum watchOS Target | Minimum tvOS Target | Notes | +|:--------------------:|:---------------------------:|:----------------------------:|:----------------------------:|:----------------------------:|:-------------------------------------------------------------------------:| +| 3.x | iOS 7 | OS X 10.9 | watchOS 2.0 | tvOS 9.0 | Xcode 7+ is required. `NSURLConnectionOperation` support has been removed. | +| 2.6 -> 2.6.3 | iOS 7 | OS X 10.9 | watchOS 2.0 | n/a | Xcode 7+ is required. | +| 2.0 -> 2.5.4 | iOS 6 | OS X 10.8 | n/a | n/a | Xcode 5+ is required. `NSURLSession` subspec requires iOS 7 or OS X 10.9. | +| 1.x | iOS 5 | Mac OS X 10.7 | n/a | n/a | +| 0.10.x | iOS 4 | Mac OS X 10.6 | n/a | n/a | + +(OS X projects must support [64-bit with modern Cocoa runtime](https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtVersionsPlatforms.html)). + +> Programming in Swift? Try [Alamofire](https://github.com/Alamofire/Alamofire) for a more conventional set of APIs. + +## Architecture + +### NSURLSession + +- `AFURLSessionManager` +- `AFHTTPSessionManager` + +### Serialization + +* `` + - `AFHTTPRequestSerializer` + - `AFJSONRequestSerializer` + - `AFPropertyListRequestSerializer` +* `` + - `AFHTTPResponseSerializer` + - `AFJSONResponseSerializer` + - `AFXMLParserResponseSerializer` + - `AFXMLDocumentResponseSerializer` _(Mac OS X)_ + - `AFPropertyListResponseSerializer` + - `AFImageResponseSerializer` + - `AFCompoundResponseSerializer` + +### Additional Functionality + +- `AFSecurityPolicy` +- `AFNetworkReachabilityManager` + +## Usage + +### AFURLSessionManager + +`AFURLSessionManager` creates and manages an `NSURLSession` object based on a specified `NSURLSessionConfiguration` object, which conforms to ``, ``, ``, and ``. + +#### Creating a Download Task + +```objective-c +NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; +AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration]; + +NSURL *URL = [NSURL URLWithString:@"http://example.com/download.zip"]; +NSURLRequest *request = [NSURLRequest requestWithURL:URL]; + +NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:nil destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) { + NSURL *documentsDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil]; + return [documentsDirectoryURL URLByAppendingPathComponent:[response suggestedFilename]]; +} completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) { + NSLog(@"File downloaded to: %@", filePath); +}]; +[downloadTask resume]; +``` + +#### Creating an Upload Task + +```objective-c +NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; +AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration]; + +NSURL *URL = [NSURL URLWithString:@"http://example.com/upload"]; +NSURLRequest *request = [NSURLRequest requestWithURL:URL]; + +NSURL *filePath = [NSURL fileURLWithPath:@"file://path/to/image.png"]; +NSURLSessionUploadTask *uploadTask = [manager uploadTaskWithRequest:request fromFile:filePath progress:nil completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) { + if (error) { + NSLog(@"Error: %@", error); + } else { + NSLog(@"Success: %@ %@", response, responseObject); + } +}]; +[uploadTask resume]; +``` + +#### Creating an Upload Task for a Multi-Part Request, with Progress + +```objective-c +NSMutableURLRequest *request = [[AFHTTPRequestSerializer serializer] multipartFormRequestWithMethod:@"POST" URLString:@"http://example.com/upload" parameters:nil constructingBodyWithBlock:^(id formData) { + [formData appendPartWithFileURL:[NSURL fileURLWithPath:@"file://path/to/image.jpg"] name:@"file" fileName:@"filename.jpg" mimeType:@"image/jpeg" error:nil]; + } error:nil]; + +AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; + +NSURLSessionUploadTask *uploadTask; +uploadTask = [manager + uploadTaskWithStreamedRequest:request + progress:^(NSProgress * _Nonnull uploadProgress) { + // This is not called back on the main queue. + // You are responsible for dispatching to the main queue for UI updates + dispatch_async(dispatch_get_main_queue(), ^{ + //Update the progress view + [progressView setProgress:uploadProgress.fractionCompleted]; + }); + } + completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) { + if (error) { + NSLog(@"Error: %@", error); + } else { + NSLog(@"%@ %@", response, responseObject); + } + }]; + +[uploadTask resume]; +``` + +#### Creating a Data Task + +```objective-c +NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; +AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration]; + +NSURL *URL = [NSURL URLWithString:@"http://example.com/upload"]; +NSURLRequest *request = [NSURLRequest requestWithURL:URL]; + +NSURLSessionDataTask *dataTask = [manager dataTaskWithRequest:request completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) { + if (error) { + NSLog(@"Error: %@", error); + } else { + NSLog(@"%@ %@", response, responseObject); + } +}]; +[dataTask resume]; +``` + +--- + +### Request Serialization + +Request serializers create requests from URL strings, encoding parameters as either a query string or HTTP body. + +```objective-c +NSString *URLString = @"http://example.com"; +NSDictionary *parameters = @{@"foo": @"bar", @"baz": @[@1, @2, @3]}; +``` + +#### Query String Parameter Encoding + +```objective-c +[[AFHTTPRequestSerializer serializer] requestWithMethod:@"GET" URLString:URLString parameters:parameters error:nil]; +``` + + GET http://example.com?foo=bar&baz[]=1&baz[]=2&baz[]=3 + +#### URL Form Parameter Encoding + +```objective-c +[[AFHTTPRequestSerializer serializer] requestWithMethod:@"POST" URLString:URLString parameters:parameters]; +``` + + POST http://example.com/ + Content-Type: application/x-www-form-urlencoded + + foo=bar&baz[]=1&baz[]=2&baz[]=3 + +#### JSON Parameter Encoding + +```objective-c +[[AFJSONRequestSerializer serializer] requestWithMethod:@"POST" URLString:URLString parameters:parameters]; +``` + + POST http://example.com/ + Content-Type: application/json + + {"foo": "bar", "baz": [1,2,3]} + +--- + +### Network Reachability Manager + +`AFNetworkReachabilityManager` monitors the reachability of domains, and addresses for both WWAN and WiFi network interfaces. + +* Do not use Reachability to determine if the original request should be sent. + * You should try to send it. +* You can use Reachability to determine when a request should be automatically retried. + * Although it may still fail, a Reachability notification that the connectivity is available is a good time to retry something. +* Network reachability is a useful tool for determining why a request might have failed. + * After a network request has failed, telling the user they're offline is better than giving them a more technical but accurate error, such as "request timed out." + +See also [WWDC 2012 session 706, "Networking Best Practices."](https://developer.apple.com/videos/play/wwdc2012-706/). + +#### Shared Network Reachability + +```objective-c +[[AFNetworkReachabilityManager sharedManager] setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) { + NSLog(@"Reachability: %@", AFStringFromNetworkReachabilityStatus(status)); +}]; + +[[AFNetworkReachabilityManager sharedManager] startMonitoring]; +``` + +--- + +### Security Policy + +`AFSecurityPolicy` evaluates server trust against pinned X.509 certificates and public keys over secure connections. + +Adding pinned SSL certificates to your app helps prevent man-in-the-middle attacks and other vulnerabilities. Applications dealing with sensitive customer data or financial information are strongly encouraged to route all communication over an HTTPS connection with SSL pinning configured and enabled. + +#### Allowing Invalid SSL Certificates + +```objective-c +AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; +manager.securityPolicy.allowInvalidCertificates = YES; // not recommended for production +``` + +--- + +## Unit Tests + +AFNetworking includes a suite of unit tests within the Tests subdirectory. These tests can be run simply be executed the test action on the platform framework you would like to test. + +## Credits + +AFNetworking is owned and maintained by the [Alamofire Software Foundation](http://alamofire.org). + +AFNetworking was originally created by [Scott Raymond](https://github.com/sco/) and [Mattt Thompson](https://github.com/mattt/) in the development of [Gowalla for iPhone](http://en.wikipedia.org/wiki/Gowalla). + +AFNetworking's logo was designed by [Alan Defibaugh](http://www.alandefibaugh.com/). + +And most of all, thanks to AFNetworking's [growing list of contributors](https://github.com/AFNetworking/AFNetworking/contributors). + +### Security Disclosure + +If you believe you have identified a security vulnerability with AFNetworking, you should report it as soon as possible via email to security@alamofire.org. Please do not post it to a public issue tracker. + +## License + +AFNetworking is released under the MIT license. See LICENSE for details. diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/AFAutoPurgingImageCache.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/AFAutoPurgingImageCache.h new file mode 100644 index 0000000000..e89b951ee3 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/AFAutoPurgingImageCache.h @@ -0,0 +1,149 @@ +// AFAutoPurgingImageCache.h +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import +#import + +#if TARGET_OS_IOS || TARGET_OS_TV +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + The `AFImageCache` protocol defines a set of APIs for adding, removing and fetching images from a cache synchronously. + */ +@protocol AFImageCache + +/** + Adds the image to the cache with the given identifier. + + @param image The image to cache. + @param identifier The unique identifier for the image in the cache. + */ +- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier; + +/** + Removes the image from the cache matching the given identifier. + + @param identifier The unique identifier for the image in the cache. + + @return A BOOL indicating whether or not the image was removed from the cache. + */ +- (BOOL)removeImageWithIdentifier:(NSString *)identifier; + +/** + Removes all images from the cache. + + @return A BOOL indicating whether or not all images were removed from the cache. + */ +- (BOOL)removeAllImages; + +/** + Returns the image in the cache associated with the given identifier. + + @param identifier The unique identifier for the image in the cache. + + @return An image for the matching identifier, or nil. + */ +- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier; +@end + + +/** + The `ImageRequestCache` protocol extends the `ImageCache` protocol by adding methods for adding, removing and fetching images from a cache given an `NSURLRequest` and additional identifier. + */ +@protocol AFImageRequestCache + +/** + Adds the image to the cache using an identifier created from the request and additional identifier. + + @param image The image to cache. + @param request The unique URL request identifing the image asset. + @param identifier The additional identifier to apply to the URL request to identify the image. + */ +- (void)addImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier; + +/** + Removes the image from the cache using an identifier created from the request and additional identifier. + + @param request The unique URL request identifing the image asset. + @param identifier The additional identifier to apply to the URL request to identify the image. + + @return A BOOL indicating whether or not all images were removed from the cache. + */ +- (BOOL)removeImageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier; + +/** + Returns the image from the cache associated with an identifier created from the request and additional identifier. + + @param request The unique URL request identifing the image asset. + @param identifier The additional identifier to apply to the URL request to identify the image. + + @return An image for the matching request and identifier, or nil. + */ +- (nullable UIImage *)imageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(nullable NSString *)identifier; + +@end + +/** + The `AutoPurgingImageCache` in an in-memory image cache used to store images up to a given memory capacity. When the memory capacity is reached, the image cache is sorted by last access date, then the oldest image is continuously purged until the preferred memory usage after purge is met. Each time an image is accessed through the cache, the internal access date of the image is updated. + */ +@interface AFAutoPurgingImageCache : NSObject + +/** + The total memory capacity of the cache in bytes. + */ +@property (nonatomic, assign) UInt64 memoryCapacity; + +/** + The preferred memory usage after purge in bytes. During a purge, images will be purged until the memory capacity drops below this limit. + */ +@property (nonatomic, assign) UInt64 preferredMemoryUsageAfterPurge; + +/** + The current total memory usage in bytes of all images stored within the cache. + */ +@property (nonatomic, assign, readonly) UInt64 memoryUsage; + +/** + Initialies the `AutoPurgingImageCache` instance with default values for memory capacity and preferred memory usage after purge limit. `memoryCapcity` defaults to `100 MB`. `preferredMemoryUsageAfterPurge` defaults to `60 MB`. + + @return The new `AutoPurgingImageCache` instance. + */ +- (instancetype)init; + +/** + Initialies the `AutoPurgingImageCache` instance with the given memory capacity and preferred memory usage + after purge limit. + + @param memoryCapacity The total memory capacity of the cache in bytes. + @param preferredMemoryUsageAfterPurge The preferred memory usage after purge in bytes. + + @return The new `AutoPurgingImageCache` instance. + */ +- (instancetype)initWithMemoryCapacity:(UInt64)memoryCapacity preferredMemoryCapacity:(UInt64)preferredMemoryCapacity; + +@end + +NS_ASSUME_NONNULL_END + +#endif + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/AFAutoPurgingImageCache.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/AFAutoPurgingImageCache.m new file mode 100644 index 0000000000..326fe4f687 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/AFAutoPurgingImageCache.m @@ -0,0 +1,201 @@ +// AFAutoPurgingImageCache.m +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +#if TARGET_OS_IOS || TARGET_OS_TV + +#import "AFAutoPurgingImageCache.h" + +@interface AFCachedImage : NSObject + +@property (nonatomic, strong) UIImage *image; +@property (nonatomic, strong) NSString *identifier; +@property (nonatomic, assign) UInt64 totalBytes; +@property (nonatomic, strong) NSDate *lastAccessDate; +@property (nonatomic, assign) UInt64 currentMemoryUsage; + +@end + +@implementation AFCachedImage + +-(instancetype)initWithImage:(UIImage *)image identifier:(NSString *)identifier { + if (self = [self init]) { + self.image = image; + self.identifier = identifier; + + CGSize imageSize = CGSizeMake(image.size.width * image.scale, image.size.height * image.scale); + CGFloat bytesPerPixel = 4.0; + CGFloat bytesPerRow = imageSize.width * bytesPerPixel; + self.totalBytes = (UInt64)bytesPerPixel * (UInt64)bytesPerRow; + self.lastAccessDate = [NSDate date]; + } + return self; +} + +- (UIImage*)accessImage { + self.lastAccessDate = [NSDate date]; + return self.image; +} + +- (NSString *)description { + NSString *descriptionString = [NSString stringWithFormat:@"Idenfitier: %@ lastAccessDate: %@ ", self.identifier, self.lastAccessDate]; + return descriptionString; + +} + +@end + +@interface AFAutoPurgingImageCache () +@property (nonatomic, strong) NSMutableDictionary *cachedImages; +@property (nonatomic, assign) UInt64 currentMemoryUsage; +@property (nonatomic, strong) dispatch_queue_t synchronizationQueue; +@end + +@implementation AFAutoPurgingImageCache + +- (instancetype)init { + return [self initWithMemoryCapacity:100 * 1024 * 1024 preferredMemoryCapacity:60 * 1024 * 1024]; +} + +- (instancetype)initWithMemoryCapacity:(UInt64)memoryCapacity preferredMemoryCapacity:(UInt64)preferredMemoryCapacity { + if (self = [super init]) { + self.memoryCapacity = memoryCapacity; + self.preferredMemoryUsageAfterPurge = preferredMemoryCapacity; + self.cachedImages = [[NSMutableDictionary alloc] init]; + + NSString *queueName = [NSString stringWithFormat:@"com.alamofire.autopurgingimagecache-%@", [[NSUUID UUID] UUIDString]]; + self.synchronizationQueue = dispatch_queue_create([queueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT); + + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(removeAllImages) + name:UIApplicationDidReceiveMemoryWarningNotification + object:nil]; + + } + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (UInt64)memoryUsage { + __block UInt64 result = 0; + dispatch_sync(self.synchronizationQueue, ^{ + result = self.currentMemoryUsage; + }); + return result; +} + +- (void)addImage:(UIImage *)image withIdentifier:(NSString *)identifier { + dispatch_barrier_async(self.synchronizationQueue, ^{ + AFCachedImage *cacheImage = [[AFCachedImage alloc] initWithImage:image identifier:identifier]; + + AFCachedImage *previousCachedImage = self.cachedImages[identifier]; + if (previousCachedImage != nil) { + self.currentMemoryUsage -= previousCachedImage.totalBytes; + } + + self.cachedImages[identifier] = cacheImage; + self.currentMemoryUsage += cacheImage.totalBytes; + }); + + dispatch_barrier_async(self.synchronizationQueue, ^{ + if (self.currentMemoryUsage > self.memoryCapacity) { + UInt64 bytesToPurge = self.currentMemoryUsage - self.preferredMemoryUsageAfterPurge; + NSMutableArray *sortedImages = [NSMutableArray arrayWithArray:self.cachedImages.allValues]; + NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastAccessDate" + ascending:YES]; + [sortedImages sortUsingDescriptors:@[sortDescriptor]]; + + UInt64 bytesPurged = 0; + + for (AFCachedImage *cachedImage in sortedImages) { + [self.cachedImages removeObjectForKey:cachedImage.identifier]; + bytesPurged += cachedImage.totalBytes; + if (bytesPurged >= bytesToPurge) { + break ; + } + } + self.currentMemoryUsage -= bytesPurged; + } + }); +} + +- (BOOL)removeImageWithIdentifier:(NSString *)identifier { + __block BOOL removed = NO; + dispatch_barrier_sync(self.synchronizationQueue, ^{ + AFCachedImage *cachedImage = self.cachedImages[identifier]; + if (cachedImage != nil) { + [self.cachedImages removeObjectForKey:identifier]; + self.currentMemoryUsage -= cachedImage.totalBytes; + removed = YES; + } + }); + return removed; +} + +- (BOOL)removeAllImages { + __block BOOL removed = NO; + dispatch_barrier_sync(self.synchronizationQueue, ^{ + if (self.cachedImages.count > 0) { + [self.cachedImages removeAllObjects]; + self.currentMemoryUsage = 0; + removed = YES; + } + }); + return removed; +} + +- (nullable UIImage *)imageWithIdentifier:(NSString *)identifier { + __block UIImage *image = nil; + dispatch_sync(self.synchronizationQueue, ^{ + AFCachedImage *cachedImage = self.cachedImages[identifier]; + image = [cachedImage accessImage]; + }); + return image; +} + +- (void)addImage:(UIImage *)image forRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)identifier { + [self addImage:image withIdentifier:[self imageCacheKeyFromURLRequest:request withAdditionalIdentifier:identifier]]; +} + +- (BOOL)removeImageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)identifier { + return [self removeImageWithIdentifier:[self imageCacheKeyFromURLRequest:request withAdditionalIdentifier:identifier]]; +} + +- (nullable UIImage *)imageforRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)identifier { + return [self imageWithIdentifier:[self imageCacheKeyFromURLRequest:request withAdditionalIdentifier:identifier]]; +} + +- (NSString *)imageCacheKeyFromURLRequest:(NSURLRequest *)request withAdditionalIdentifier:(NSString *)additionalIdentifier { + NSString *key = request.URL.absoluteString; + if (additionalIdentifier != nil) { + key = [key stringByAppendingString:additionalIdentifier]; + } + return key; +} + +@end + +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/AFImageDownloader.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/AFImageDownloader.h new file mode 100644 index 0000000000..b35e1855e1 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/AFImageDownloader.h @@ -0,0 +1,157 @@ +// AFImageDownloader.h +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +#if TARGET_OS_IOS || TARGET_OS_TV + +#import +#import "AFAutoPurgingImageCache.h" +#import "AFHTTPSessionManager.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, AFImageDownloadPrioritization) { + AFImageDownloadPrioritizationFIFO, + AFImageDownloadPrioritizationLIFO +}; + +/** + The `AFImageDownloadReceipt` is an object vended by the `AFImageDownloader` when starting a data task. It can be used to cancel active tasks running on the `AFImageDownloader` session. As a general rule, image data tasks should be cancelled using the `AFImageDownloadReceipt` instead of calling `cancel` directly on the `task` itself. The `AFImageDownloader` is optimized to handle duplicate task scenarios as well as pending versus active downloads. + */ +@interface AFImageDownloadReceipt : NSObject + +/** + The data task created by the `AFImageDownloader`. +*/ +@property (nonatomic, strong) NSURLSessionDataTask *task; + +/** + The unique identifier for the success and failure blocks when duplicate requests are made. + */ +@property (nonatomic, strong) NSUUID *receiptID; +@end + +/** The `AFImageDownloader` class is responsible for downloading images in parallel on a prioritized queue. Incoming downloads are added to the front or back of the queue depending on the download prioritization. Each downloaded image is cached in the underlying `NSURLCache` as well as the in-memory image cache. By default, any download request with a cached image equivalent in the image cache will automatically be served the cached image representation. + */ +@interface AFImageDownloader : NSObject + +/** + The image cache used to store all downloaded images in. `AFAutoPurgingImageCache` by default. + */ +@property (nonatomic, strong, nullable) id imageCache; + +/** + The `AFHTTPSessionManager` used to download images. By default, this is configured with an `AFImageResponseSerializer`, and a shared `NSURLCache` for all image downloads. + */ +@property (nonatomic, strong) AFHTTPSessionManager *sessionManager; + +/** + Defines the order prioritization of incoming download requests being inserted into the queue. `AFImageDownloadPrioritizationFIFO` by default. + */ +@property (nonatomic, assign) AFImageDownloadPrioritization downloadPrioritizaton; + +/** + The shared default instance of `AFImageDownloader` initialized with default values. + */ ++ (instancetype)defaultInstance; + +/** + Creates a default `NSURLCache` with common usage parameter values. + + @returns The default `NSURLCache` instance. + */ ++ (NSURLCache *)defaultURLCache; + +/** + Default initializer + + @return An instance of `AFImageDownloader` initialized with default values. + */ +- (instancetype)init; + +/** + Initializes the `AFImageDownloader` instance with the given session manager, download prioritization, maximum active download count and image cache. + + @param sessionManager The session manager to use to download images. + @param downloadPrioritization The download prioritization of the download queue. + @param maximumActiveDownloads The maximum number of active downloads allowed at any given time. Recommend `4`. + @param imageCache The image cache used to store all downloaded images in. + + @return The new `AFImageDownloader` instance. + */ +- (instancetype)initWithSessionManager:(AFHTTPSessionManager *)sessionManager + downloadPrioritization:(AFImageDownloadPrioritization)downloadPrioritization + maximumActiveDownloads:(NSInteger)maximumActiveDownloads + imageCache:(nullable id )imageCache; + +/** + Creates a data task using the `sessionManager` instance for the specified URL request. + + If the same data task is already in the queue or currently being downloaded, the success and failure blocks are + appended to the already existing task. Once the task completes, all success or failure blocks attached to the + task are executed in the order they were added. + + @param request The URL request. + @param success A block to be executed when the image data task finishes successfully. This block has no return value and takes three arguments: the request sent from the client, the response received from the server, and the image created from the response data of request. If the image was returned from cache, the response parameter will be `nil`. + @param failure A block object to be executed when the image data task finishes unsuccessfully, or that finishes successfully. This block has no return value and takes three arguments: the request sent from the client, the response received from the server, and the error object describing the network or parsing error that occurred. + + @return The image download receipt for the data task if available. `nil` if the image is stored in the cache. + cache and the URL request cache policy allows the cache to be used. + */ +- (nullable AFImageDownloadReceipt *)downloadImageForURLRequest:(NSURLRequest *)request + success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *responseObject))success + failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure; + +/** + Creates a data task using the `sessionManager` instance for the specified URL request. + + If the same data task is already in the queue or currently being downloaded, the success and failure blocks are + appended to the already existing task. Once the task completes, all success or failure blocks attached to the + task are executed in the order they were added. + + @param request The URL request. + @param request The identifier to use for the download receipt that will be created for this request. This must be a unique identifier that does not represent any other request. + @param success A block to be executed when the image data task finishes successfully. This block has no return value and takes three arguments: the request sent from the client, the response received from the server, and the image created from the response data of request. If the image was returned from cache, the response parameter will be `nil`. + @param failure A block object to be executed when the image data task finishes unsuccessfully, or that finishes successfully. This block has no return value and takes three arguments: the request sent from the client, the response received from the server, and the error object describing the network or parsing error that occurred. + + @return The image download receipt for the data task if available. `nil` if the image is stored in the cache. + cache and the URL request cache policy allows the cache to be used. + */ +- (nullable AFImageDownloadReceipt *)downloadImageForURLRequest:(NSURLRequest *)request + withReceiptID:(NSUUID *)receiptID + success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *responseObject))success + failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure; + +/** + Cancels the data task in the receipt by removing the corresponding success and failure blocks and cancelling the data task if necessary. + + If the data task is pending in the queue, it will be cancelled if no other success and failure blocks are registered with the data task. If the data task is currently executing or is already completed, the success and failure blocks are removed and will not be called when the task finishes. + + @param imageDownloadReceipt The image download receipt to cancel. + */ +- (void)cancelTaskForImageDownloadReceipt:(AFImageDownloadReceipt *)imageDownloadReceipt; + +@end + +#endif + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/AFImageDownloader.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/AFImageDownloader.m new file mode 100644 index 0000000000..bab4c02548 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/AFImageDownloader.m @@ -0,0 +1,371 @@ +// AFImageDownloader.m +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +#if TARGET_OS_IOS || TARGET_OS_TV + +#import "AFImageDownloader.h" +#import "AFHTTPSessionManager.h" + +@interface AFImageDownloaderResponseHandler : NSObject +@property (nonatomic, strong) NSUUID *uuid; +@property (nonatomic, copy) void (^successBlock)(NSURLRequest*, NSHTTPURLResponse*, UIImage*); +@property (nonatomic, copy) void (^failureBlock)(NSURLRequest*, NSHTTPURLResponse*, NSError*); +@end + +@implementation AFImageDownloaderResponseHandler + +- (instancetype)initWithUUID:(NSUUID *)uuid + success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *responseObject))success + failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure { + if (self = [self init]) { + self.uuid = uuid; + self.successBlock = success; + self.failureBlock = failure; + } + return self; +} + +- (NSString *)description { + return [NSString stringWithFormat: @"UUID: %@", [self.uuid UUIDString]]; +} + +@end + +@interface AFImageDownloaderMergedTask : NSObject +@property (nonatomic, strong) NSString *identifier; +@property (nonatomic, strong) NSURLSessionDataTask *task; +@property (nonatomic, strong) NSMutableArray *responseHandlers; + +@end + +@implementation AFImageDownloaderMergedTask + +- (instancetype)initWithIdentifier:(NSString *)identifier task:(NSURLSessionDataTask *)task { + if (self = [self init]) { + self.identifier = identifier; + self.task = task; + self.responseHandlers = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void)addResponseHandler:(AFImageDownloaderResponseHandler*)handler { + [self.responseHandlers addObject:handler]; +} + +- (void)removeResponseHandler:(AFImageDownloaderResponseHandler*)handler { + [self.responseHandlers removeObject:handler]; +} + +@end + +@implementation AFImageDownloadReceipt + +- (instancetype)initWithReceiptID:(NSUUID *)receiptID task:(NSURLSessionDataTask *)task { + if (self = [self init]) { + self.receiptID = receiptID; + self.task = task; + } + return self; +} + +@end + +@interface AFImageDownloader () + +@property (nonatomic, strong) dispatch_queue_t synchronizationQueue; +@property (nonatomic, strong) dispatch_queue_t responseQueue; + +@property (nonatomic, assign) NSInteger maximumActiveDownloads; +@property (nonatomic, assign) NSInteger activeRequestCount; + +@property (nonatomic, strong) NSMutableArray *queuedMergedTasks; +@property (nonatomic, strong) NSMutableDictionary *mergedTasks; + +@end + + +@implementation AFImageDownloader +#if TARGET_OS_MACCATALYST +#else ++ (NSURLCache *)defaultURLCache { + return [[NSURLCache alloc] initWithMemoryCapacity:20 * 1024 * 1024 + diskCapacity:150 * 1024 * 1024 + diskPath:@"com.alamofire.imagedownloader"]; +} +#endif + ++ (NSURLSessionConfiguration *)defaultURLSessionConfiguration { + NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; + + //TODO set the default HTTP headers + + configuration.HTTPShouldSetCookies = YES; + configuration.HTTPShouldUsePipelining = NO; + + configuration.requestCachePolicy = NSURLRequestUseProtocolCachePolicy; + configuration.allowsCellularAccess = YES; + configuration.timeoutIntervalForRequest = 60.0; + configuration.URLCache = [AFImageDownloader defaultURLCache]; + + return configuration; +} + +- (instancetype)init { + NSURLSessionConfiguration *defaultConfiguration = [self.class defaultURLSessionConfiguration]; + AFHTTPSessionManager *sessionManager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:defaultConfiguration]; + sessionManager.responseSerializer = [AFImageResponseSerializer serializer]; + + return [self initWithSessionManager:sessionManager + downloadPrioritization:AFImageDownloadPrioritizationFIFO + maximumActiveDownloads:4 + imageCache:[[AFAutoPurgingImageCache alloc] init]]; +} + +- (instancetype)initWithSessionManager:(AFHTTPSessionManager *)sessionManager + downloadPrioritization:(AFImageDownloadPrioritization)downloadPrioritization + maximumActiveDownloads:(NSInteger)maximumActiveDownloads + imageCache:(id )imageCache { + if (self = [super init]) { + self.sessionManager = sessionManager; + + self.downloadPrioritizaton = downloadPrioritization; + self.maximumActiveDownloads = maximumActiveDownloads; + self.imageCache = imageCache; + + self.queuedMergedTasks = [[NSMutableArray alloc] init]; + self.mergedTasks = [[NSMutableDictionary alloc] init]; + self.activeRequestCount = 0; + + NSString *name = [NSString stringWithFormat:@"com.alamofire.imagedownloader.synchronizationqueue-%@", [[NSUUID UUID] UUIDString]]; + self.synchronizationQueue = dispatch_queue_create([name cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_SERIAL); + + name = [NSString stringWithFormat:@"com.alamofire.imagedownloader.responsequeue-%@", [[NSUUID UUID] UUIDString]]; + self.responseQueue = dispatch_queue_create([name cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_CONCURRENT); + } + + return self; +} + ++ (instancetype)defaultInstance { + static AFImageDownloader *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[self alloc] init]; + }); + return sharedInstance; +} + +- (nullable AFImageDownloadReceipt *)downloadImageForURLRequest:(NSURLRequest *)request + success:(void (^)(NSURLRequest * _Nonnull, NSHTTPURLResponse * _Nullable, UIImage * _Nonnull))success + failure:(void (^)(NSURLRequest * _Nonnull, NSHTTPURLResponse * _Nullable, NSError * _Nonnull))failure { + return [self downloadImageForURLRequest:request withReceiptID:[NSUUID UUID] success:success failure:failure]; +} + +- (nullable AFImageDownloadReceipt *)downloadImageForURLRequest:(NSURLRequest *)request + withReceiptID:(nonnull NSUUID *)receiptID + success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *responseObject))success + failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure { + __block NSURLSessionDataTask *task = nil; + dispatch_sync(self.synchronizationQueue, ^{ + NSString *identifier = request.URL.absoluteString; + + // 1) Append the success and failure blocks to a pre-existing request if it already exists + AFImageDownloaderMergedTask *existingMergedTask = self.mergedTasks[identifier]; + if (existingMergedTask != nil) { + AFImageDownloaderResponseHandler *handler = [[AFImageDownloaderResponseHandler alloc] initWithUUID:receiptID success:success failure:failure]; + [existingMergedTask addResponseHandler:handler]; + task = existingMergedTask.task; + return; + } + + // 2) Attempt to load the image from the image cache if the cache policy allows it + switch (request.cachePolicy) { + case NSURLRequestUseProtocolCachePolicy: + case NSURLRequestReturnCacheDataElseLoad: + case NSURLRequestReturnCacheDataDontLoad: { + UIImage *cachedImage = [self.imageCache imageforRequest:request withAdditionalIdentifier:nil]; + if (cachedImage != nil) { + if (success) { + dispatch_async(dispatch_get_main_queue(), ^{ + success(request, nil, cachedImage); + }); + } + return; + } + break; + } + default: + break; + } + + // 3) Create the request and set up authentication, validation and response serialization + NSURLSessionDataTask *createdTask; + __weak __typeof__(self) weakSelf = self; + + createdTask = [self.sessionManager + dataTaskWithRequest:request + completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) { + dispatch_async(self.responseQueue, ^{ + __strong __typeof__(weakSelf) strongSelf = weakSelf; + AFImageDownloaderMergedTask *mergedTask = [strongSelf safelyRemoveMergedTaskWithIdentifier:identifier]; + if (error) { + for (AFImageDownloaderResponseHandler *handler in mergedTask.responseHandlers) { + if (handler.failureBlock) { + dispatch_async(dispatch_get_main_queue(), ^{ + handler.failureBlock(request, (NSHTTPURLResponse*)response, error); + }); + } + } + } else { + [strongSelf.imageCache addImage:responseObject forRequest:request withAdditionalIdentifier:nil]; + + for (AFImageDownloaderResponseHandler *handler in mergedTask.responseHandlers) { + if (handler.successBlock) { + dispatch_async(dispatch_get_main_queue(), ^{ + handler.successBlock(request, (NSHTTPURLResponse*)response, responseObject); + }); + } + } + + } + [strongSelf safelyDecrementActiveTaskCount]; + [strongSelf safelyStartNextTaskIfNecessary]; + }); + }]; + + // 4) Store the response handler for use when the request completes + AFImageDownloaderResponseHandler *handler = [[AFImageDownloaderResponseHandler alloc] initWithUUID:receiptID + success:success + failure:failure]; + AFImageDownloaderMergedTask *mergedTask = [[AFImageDownloaderMergedTask alloc] + initWithIdentifier:identifier + task:createdTask]; + [mergedTask addResponseHandler:handler]; + self.mergedTasks[identifier] = mergedTask; + + // 5) Either start the request or enqueue it depending on the current active request count + if ([self isActiveRequestCountBelowMaximumLimit]) { + [self startMergedTask:mergedTask]; + } else { + [self enqueueMergedTask:mergedTask]; + } + + task = mergedTask.task; + }); + if (task) { + return [[AFImageDownloadReceipt alloc] initWithReceiptID:receiptID task:task]; + } else { + return nil; + } +} + +- (void)cancelTaskForImageDownloadReceipt:(AFImageDownloadReceipt *)imageDownloadReceipt { + dispatch_sync(self.synchronizationQueue, ^{ + NSString *identifier = imageDownloadReceipt.task.originalRequest.URL.absoluteString; + AFImageDownloaderMergedTask *mergedTask = self.mergedTasks[identifier]; + NSUInteger index = [mergedTask.responseHandlers indexOfObjectPassingTest:^BOOL(AFImageDownloaderResponseHandler * _Nonnull handler, __unused NSUInteger idx, __unused BOOL * _Nonnull stop) { + return handler.uuid == imageDownloadReceipt.receiptID; + }]; + + if (index != NSNotFound) { + AFImageDownloaderResponseHandler *handler = mergedTask.responseHandlers[index]; + [mergedTask removeResponseHandler:handler]; + NSString *failureReason = [NSString stringWithFormat:@"ImageDownloader cancelled URL request: %@",imageDownloadReceipt.task.originalRequest.URL.absoluteString]; + NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey:failureReason}; + NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:userInfo]; + if (handler.failureBlock) { + dispatch_async(dispatch_get_main_queue(), ^{ + handler.failureBlock(imageDownloadReceipt.task.originalRequest, nil, error); + }); + } + } + + if (mergedTask.responseHandlers.count == 0 && mergedTask.task.state == NSURLSessionTaskStateSuspended) { + [mergedTask.task cancel]; + } + }); +} + +- (AFImageDownloaderMergedTask*)safelyRemoveMergedTaskWithIdentifier:(NSString *)identifier { + __block AFImageDownloaderMergedTask *mergedTask = nil; + dispatch_sync(self.synchronizationQueue, ^{ + mergedTask = self.mergedTasks[identifier]; + [self.mergedTasks removeObjectForKey:identifier]; + + }); + return mergedTask; +} + +- (void)safelyDecrementActiveTaskCount { + dispatch_sync(self.synchronizationQueue, ^{ + if (self.activeRequestCount > 0) { + self.activeRequestCount -= 1; + } + }); +} + +- (void)safelyStartNextTaskIfNecessary { + dispatch_sync(self.synchronizationQueue, ^{ + if ([self isActiveRequestCountBelowMaximumLimit]) { + while (self.queuedMergedTasks.count > 0) { + AFImageDownloaderMergedTask *mergedTask = [self dequeueMergedTask]; + if (mergedTask.task.state == NSURLSessionTaskStateSuspended) { + [self startMergedTask:mergedTask]; + break; + } + } + } + }); +} + +- (void)startMergedTask:(AFImageDownloaderMergedTask *)mergedTask { + [mergedTask.task resume]; + ++self.activeRequestCount; +} + +- (void)enqueueMergedTask:(AFImageDownloaderMergedTask *)mergedTask { + switch (self.downloadPrioritizaton) { + case AFImageDownloadPrioritizationFIFO: + [self.queuedMergedTasks addObject:mergedTask]; + break; + case AFImageDownloadPrioritizationLIFO: + [self.queuedMergedTasks insertObject:mergedTask atIndex:0]; + break; + } +} + +- (AFImageDownloaderMergedTask *)dequeueMergedTask { + AFImageDownloaderMergedTask *mergedTask = nil; + mergedTask = [self.queuedMergedTasks firstObject]; + [self.queuedMergedTasks removeObject:mergedTask]; + return mergedTask; +} + +- (BOOL)isActiveRequestCountBelowMaximumLimit { + return self.activeRequestCount < self.maximumActiveDownloads; +} + +@end + +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/AFNetworkActivityIndicatorManager.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/AFNetworkActivityIndicatorManager.h new file mode 100644 index 0000000000..a627a6d64d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/AFNetworkActivityIndicatorManager.h @@ -0,0 +1,103 @@ +// AFNetworkActivityIndicatorManager.h +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +#import + +#if TARGET_OS_IOS + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + `AFNetworkActivityIndicatorManager` manages the state of the network activity indicator in the status bar. When enabled, it will listen for notifications indicating that a session task has started or finished, and start or stop animating the indicator accordingly. The number of active requests is incremented and decremented much like a stack or a semaphore, and the activity indicator will animate so long as that number is greater than zero. + + You should enable the shared instance of `AFNetworkActivityIndicatorManager` when your application finishes launching. In `AppDelegate application:didFinishLaunchingWithOptions:` you can do so with the following code: + + [[AFNetworkActivityIndicatorManager sharedManager] setEnabled:YES]; + + By setting `enabled` to `YES` for `sharedManager`, the network activity indicator will show and hide automatically as requests start and finish. You should not ever need to call `incrementActivityCount` or `decrementActivityCount` yourself. + + See the Apple Human Interface Guidelines section about the Network Activity Indicator for more information: + http://developer.apple.com/library/iOS/#documentation/UserExperience/Conceptual/MobileHIG/UIElementGuidelines/UIElementGuidelines.html#//apple_ref/doc/uid/TP40006556-CH13-SW44 + */ +NS_EXTENSION_UNAVAILABLE_IOS("Use view controller based solutions where appropriate instead.") +@interface AFNetworkActivityIndicatorManager : NSObject + +/** + A Boolean value indicating whether the manager is enabled. + + If YES, the manager will change status bar network activity indicator according to network operation notifications it receives. The default value is NO. + */ +@property (nonatomic, assign, getter = isEnabled) BOOL enabled; + +/** + A Boolean value indicating whether the network activity indicator manager is currently active. +*/ +@property (readonly, nonatomic, assign, getter=isNetworkActivityIndicatorVisible) BOOL networkActivityIndicatorVisible; + +/** + A time interval indicating the minimum duration of networking activity that should occur before the activity indicator is displayed. The default value 1 second. If the network activity indicator should be displayed immediately when network activity occurs, this value should be set to 0 seconds. + + Apple's HIG describes the following: + + > Display the network activity indicator to provide feedback when your app accesses the network for more than a couple of seconds. If the operation finishes sooner than that, you don’t have to show the network activity indicator, because the indicator is likely to disappear before users notice its presence. + + */ +@property (nonatomic, assign) NSTimeInterval activationDelay; + +/** + A time interval indicating the duration of time of no networking activity required before the activity indicator is disabled. This allows for continuous display of the network activity indicator across multiple requests. The default value is 0.17 seconds. + */ + +@property (nonatomic, assign) NSTimeInterval completionDelay; + +/** + Returns the shared network activity indicator manager object for the system. + + @return The systemwide network activity indicator manager. + */ ++ (instancetype)sharedManager; + +/** + Increments the number of active network requests. If this number was zero before incrementing, this will start animating the status bar network activity indicator. + */ +- (void)incrementActivityCount; + +/** + Decrements the number of active network requests. If this number becomes zero after decrementing, this will stop animating the status bar network activity indicator. + */ +- (void)decrementActivityCount; + +/** + Set the a custom method to be executed when the network activity indicator manager should be hidden/shown. By default, this is null, and the UIApplication Network Activity Indicator will be managed automatically. If this block is set, it is the responsiblity of the caller to manager the network activity indicator going forward. + + @param block A block to be executed when the network activity indicator status changes. + */ +- (void)setNetworkingActivityActionWithBlock:(nullable void (^)(BOOL networkActivityIndicatorVisible))block; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/AFNetworkActivityIndicatorManager.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/AFNetworkActivityIndicatorManager.m new file mode 100644 index 0000000000..0615fa9fad --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/AFNetworkActivityIndicatorManager.m @@ -0,0 +1,261 @@ +// AFNetworkActivityIndicatorManager.m +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "AFNetworkActivityIndicatorManager.h" + +#if TARGET_OS_IOS +#import "AFURLSessionManager.h" + +typedef NS_ENUM(NSInteger, AFNetworkActivityManagerState) { + AFNetworkActivityManagerStateNotActive, + AFNetworkActivityManagerStateDelayingStart, + AFNetworkActivityManagerStateActive, + AFNetworkActivityManagerStateDelayingEnd +}; + +static NSTimeInterval const kDefaultAFNetworkActivityManagerActivationDelay = 1.0; +static NSTimeInterval const kDefaultAFNetworkActivityManagerCompletionDelay = 0.17; + +static NSURLRequest * AFNetworkRequestFromNotification(NSNotification *notification) { + if ([[notification object] respondsToSelector:@selector(originalRequest)]) { + return [(NSURLSessionTask *)[notification object] originalRequest]; + } else { + return nil; + } +} + +typedef void (^AFNetworkActivityActionBlock)(BOOL networkActivityIndicatorVisible); + +@interface AFNetworkActivityIndicatorManager () +@property (readwrite, nonatomic, assign) NSInteger activityCount; +@property (readwrite, nonatomic, strong) NSTimer *activationDelayTimer; +@property (readwrite, nonatomic, strong) NSTimer *completionDelayTimer; +@property (readonly, nonatomic, getter = isNetworkActivityOccurring) BOOL networkActivityOccurring; +@property (nonatomic, copy) AFNetworkActivityActionBlock networkActivityActionBlock; +@property (nonatomic, assign) AFNetworkActivityManagerState currentState; +@property (nonatomic, assign, getter=isNetworkActivityIndicatorVisible) BOOL networkActivityIndicatorVisible; + +- (void)updateCurrentStateForNetworkActivityChange; +@end + +@implementation AFNetworkActivityIndicatorManager + ++ (instancetype)sharedManager { + static AFNetworkActivityIndicatorManager *_sharedManager = nil; + static dispatch_once_t oncePredicate; + dispatch_once(&oncePredicate, ^{ + _sharedManager = [[self alloc] init]; + }); + + return _sharedManager; +} + +- (instancetype)init { + self = [super init]; + if (!self) { + return nil; + } + self.currentState = AFNetworkActivityManagerStateNotActive; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkRequestDidStart:) name:AFNetworkingTaskDidResumeNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkRequestDidFinish:) name:AFNetworkingTaskDidSuspendNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkRequestDidFinish:) name:AFNetworkingTaskDidCompleteNotification object:nil]; + self.activationDelay = kDefaultAFNetworkActivityManagerActivationDelay; + self.completionDelay = kDefaultAFNetworkActivityManagerCompletionDelay; + + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + [_activationDelayTimer invalidate]; + [_completionDelayTimer invalidate]; +} + +- (void)setEnabled:(BOOL)enabled { + _enabled = enabled; + if (enabled == NO) { + [self setCurrentState:AFNetworkActivityManagerStateNotActive]; + } +} + +- (void)setNetworkingActivityActionWithBlock:(void (^)(BOOL networkActivityIndicatorVisible))block { + self.networkActivityActionBlock = block; +} + +- (BOOL)isNetworkActivityOccurring { + @synchronized(self) { + return self.activityCount > 0; + } +} + +- (void)setNetworkActivityIndicatorVisible:(BOOL)networkActivityIndicatorVisible { + if (_networkActivityIndicatorVisible != networkActivityIndicatorVisible) { + [self willChangeValueForKey:@"networkActivityIndicatorVisible"]; + @synchronized(self) { + _networkActivityIndicatorVisible = networkActivityIndicatorVisible; + } + [self didChangeValueForKey:@"networkActivityIndicatorVisible"]; + if (self.networkActivityActionBlock) { + self.networkActivityActionBlock(networkActivityIndicatorVisible); + } else { + [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:networkActivityIndicatorVisible]; + } + } +} + +- (void)setActivityCount:(NSInteger)activityCount { + @synchronized(self) { + _activityCount = activityCount; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + [self updateCurrentStateForNetworkActivityChange]; + }); +} + +- (void)incrementActivityCount { + [self willChangeValueForKey:@"activityCount"]; + @synchronized(self) { + _activityCount++; + } + [self didChangeValueForKey:@"activityCount"]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [self updateCurrentStateForNetworkActivityChange]; + }); +} + +- (void)decrementActivityCount { + [self willChangeValueForKey:@"activityCount"]; + @synchronized(self) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgnu" + _activityCount = MAX(_activityCount - 1, 0); +#pragma clang diagnostic pop + } + [self didChangeValueForKey:@"activityCount"]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [self updateCurrentStateForNetworkActivityChange]; + }); +} + +- (void)networkRequestDidStart:(NSNotification *)notification { + if ([AFNetworkRequestFromNotification(notification) URL]) { + [self incrementActivityCount]; + } +} + +- (void)networkRequestDidFinish:(NSNotification *)notification { + if ([AFNetworkRequestFromNotification(notification) URL]) { + [self decrementActivityCount]; + } +} + +#pragma mark - Internal State Management +- (void)setCurrentState:(AFNetworkActivityManagerState)currentState { + @synchronized(self) { + if (_currentState != currentState) { + [self willChangeValueForKey:@"currentState"]; + _currentState = currentState; + switch (currentState) { + case AFNetworkActivityManagerStateNotActive: + [self cancelActivationDelayTimer]; + [self cancelCompletionDelayTimer]; + [self setNetworkActivityIndicatorVisible:NO]; + break; + case AFNetworkActivityManagerStateDelayingStart: + [self startActivationDelayTimer]; + break; + case AFNetworkActivityManagerStateActive: + [self cancelCompletionDelayTimer]; + [self setNetworkActivityIndicatorVisible:YES]; + break; + case AFNetworkActivityManagerStateDelayingEnd: + [self startCompletionDelayTimer]; + break; + } + } + [self didChangeValueForKey:@"currentState"]; + } +} + +- (void)updateCurrentStateForNetworkActivityChange { + if (self.enabled) { + switch (self.currentState) { + case AFNetworkActivityManagerStateNotActive: + if (self.isNetworkActivityOccurring) { + [self setCurrentState:AFNetworkActivityManagerStateDelayingStart]; + } + break; + case AFNetworkActivityManagerStateDelayingStart: + //No op. Let the delay timer finish out. + break; + case AFNetworkActivityManagerStateActive: + if (!self.isNetworkActivityOccurring) { + [self setCurrentState:AFNetworkActivityManagerStateDelayingEnd]; + } + break; + case AFNetworkActivityManagerStateDelayingEnd: + if (self.isNetworkActivityOccurring) { + [self setCurrentState:AFNetworkActivityManagerStateActive]; + } + break; + } + } +} + +- (void)startActivationDelayTimer { + self.activationDelayTimer = [NSTimer + timerWithTimeInterval:self.activationDelay target:self selector:@selector(activationDelayTimerFired) userInfo:nil repeats:NO]; + [[NSRunLoop mainRunLoop] addTimer:self.activationDelayTimer forMode:NSRunLoopCommonModes]; +} + +- (void)activationDelayTimerFired { + if (self.networkActivityOccurring) { + [self setCurrentState:AFNetworkActivityManagerStateActive]; + } else { + [self setCurrentState:AFNetworkActivityManagerStateNotActive]; + } +} + +- (void)startCompletionDelayTimer { + [self.completionDelayTimer invalidate]; + self.completionDelayTimer = [NSTimer timerWithTimeInterval:self.completionDelay target:self selector:@selector(completionDelayTimerFired) userInfo:nil repeats:NO]; + [[NSRunLoop mainRunLoop] addTimer:self.completionDelayTimer forMode:NSRunLoopCommonModes]; +} + +- (void)completionDelayTimerFired { + [self setCurrentState:AFNetworkActivityManagerStateNotActive]; +} + +- (void)cancelActivationDelayTimer { + [self.activationDelayTimer invalidate]; +} + +- (void)cancelCompletionDelayTimer { + [self.completionDelayTimer invalidate]; +} + +@end + +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIActivityIndicatorView+AFNetworking.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIActivityIndicatorView+AFNetworking.h new file mode 100644 index 0000000000..b6ef044d7b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIActivityIndicatorView+AFNetworking.h @@ -0,0 +1,48 @@ +// UIActivityIndicatorView+AFNetworking.h +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +#import + +#if TARGET_OS_IOS || TARGET_OS_TV + +#import + +/** + This category adds methods to the UIKit framework's `UIActivityIndicatorView` class. The methods in this category provide support for automatically starting and stopping animation depending on the loading state of a session task. + */ +@interface UIActivityIndicatorView (AFNetworking) + +///---------------------------------- +/// @name Animating for Session Tasks +///---------------------------------- + +/** + Binds the animating state to the state of the specified task. + + @param task The task. If `nil`, automatic updating from any previously specified operation will be disabled. + */ +- (void)setAnimatingWithStateOfTask:(nullable NSURLSessionTask *)task; + +@end + +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIActivityIndicatorView+AFNetworking.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIActivityIndicatorView+AFNetworking.m new file mode 100644 index 0000000000..fcf1c0c9c2 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIActivityIndicatorView+AFNetworking.m @@ -0,0 +1,124 @@ +// UIActivityIndicatorView+AFNetworking.m +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "UIActivityIndicatorView+AFNetworking.h" +#import + +#if TARGET_OS_IOS || TARGET_OS_TV + +#import "AFURLSessionManager.h" + +@interface AFActivityIndicatorViewNotificationObserver : NSObject +@property (readonly, nonatomic, weak) UIActivityIndicatorView *activityIndicatorView; +- (instancetype)initWithActivityIndicatorView:(UIActivityIndicatorView *)activityIndicatorView; + +- (void)setAnimatingWithStateOfTask:(NSURLSessionTask *)task; + +@end + +@implementation UIActivityIndicatorView (AFNetworking) + +- (AFActivityIndicatorViewNotificationObserver *)af_notificationObserver { + AFActivityIndicatorViewNotificationObserver *notificationObserver = objc_getAssociatedObject(self, @selector(af_notificationObserver)); + if (notificationObserver == nil) { + notificationObserver = [[AFActivityIndicatorViewNotificationObserver alloc] initWithActivityIndicatorView:self]; + objc_setAssociatedObject(self, @selector(af_notificationObserver), notificationObserver, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + return notificationObserver; +} + +- (void)setAnimatingWithStateOfTask:(NSURLSessionTask *)task { + [[self af_notificationObserver] setAnimatingWithStateOfTask:task]; +} + +@end + +@implementation AFActivityIndicatorViewNotificationObserver + +- (instancetype)initWithActivityIndicatorView:(UIActivityIndicatorView *)activityIndicatorView +{ + self = [super init]; + if (self) { + _activityIndicatorView = activityIndicatorView; + } + return self; +} + +- (void)setAnimatingWithStateOfTask:(NSURLSessionTask *)task { + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + + [notificationCenter removeObserver:self name:AFNetworkingTaskDidResumeNotification object:nil]; + [notificationCenter removeObserver:self name:AFNetworkingTaskDidSuspendNotification object:nil]; + [notificationCenter removeObserver:self name:AFNetworkingTaskDidCompleteNotification object:nil]; + + if (task) { + if (task.state != NSURLSessionTaskStateCompleted) { + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreceiver-is-weak" +#pragma clang diagnostic ignored "-Warc-repeated-use-of-weak" + if (task.state == NSURLSessionTaskStateRunning) { + [self.activityIndicatorView startAnimating]; + } else { + [self.activityIndicatorView stopAnimating]; + } +#pragma clang diagnostic pop + + [notificationCenter addObserver:self selector:@selector(af_startAnimating) name:AFNetworkingTaskDidResumeNotification object:task]; + [notificationCenter addObserver:self selector:@selector(af_stopAnimating) name:AFNetworkingTaskDidCompleteNotification object:task]; + [notificationCenter addObserver:self selector:@selector(af_stopAnimating) name:AFNetworkingTaskDidSuspendNotification object:task]; + } + } +} + +#pragma mark - + +- (void)af_startAnimating { + dispatch_async(dispatch_get_main_queue(), ^{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreceiver-is-weak" + [self.activityIndicatorView startAnimating]; +#pragma clang diagnostic pop + }); +} + +- (void)af_stopAnimating { + dispatch_async(dispatch_get_main_queue(), ^{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreceiver-is-weak" + [self.activityIndicatorView stopAnimating]; +#pragma clang diagnostic pop + }); +} + +#pragma mark - + +- (void)dealloc { + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + + [notificationCenter removeObserver:self name:AFNetworkingTaskDidCompleteNotification object:nil]; + [notificationCenter removeObserver:self name:AFNetworkingTaskDidResumeNotification object:nil]; + [notificationCenter removeObserver:self name:AFNetworkingTaskDidSuspendNotification object:nil]; +} + +@end + +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIButton+AFNetworking.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIButton+AFNetworking.h new file mode 100644 index 0000000000..98b911e138 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIButton+AFNetworking.h @@ -0,0 +1,175 @@ +// UIButton+AFNetworking.h +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +#import + +#if TARGET_OS_IOS || TARGET_OS_TV + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class AFImageDownloader; + +/** + This category adds methods to the UIKit framework's `UIButton` class. The methods in this category provide support for loading remote images and background images asynchronously from a URL. + + @warning Compound values for control `state` (such as `UIControlStateHighlighted | UIControlStateDisabled`) are unsupported. + */ +@interface UIButton (AFNetworking) + +///------------------------------------ +/// @name Accessing the Image Downloader +///------------------------------------ + +/** + Set the shared image downloader used to download images. + + @param imageDownloader The shared image downloader used to download images. +*/ ++ (void)setSharedImageDownloader:(AFImageDownloader *)imageDownloader; + +/** + The shared image downloader used to download images. + */ ++ (AFImageDownloader *)sharedImageDownloader; + +///-------------------- +/// @name Setting Image +///-------------------- + +/** + Asynchronously downloads an image from the specified URL, and sets it as the image for the specified state once the request is finished. Any previous image request for the receiver will be cancelled. + + If the image is cached locally, the image is set immediately, otherwise the specified placeholder image will be set immediately, and then the remote image will be set once the request is finished. + + @param state The control state. + @param url The URL used for the image request. + */ +- (void)setImageForState:(UIControlState)state + withURL:(NSURL *)url; + +/** + Asynchronously downloads an image from the specified URL, and sets it as the image for the specified state once the request is finished. Any previous image request for the receiver will be cancelled. + + If the image is cached locally, the image is set immediately, otherwise the specified placeholder image will be set immediately, and then the remote image will be set once the request is finished. + + @param state The control state. + @param url The URL used for the image request. + @param placeholderImage The image to be set initially, until the image request finishes. If `nil`, the button will not change its image until the image request finishes. + */ +- (void)setImageForState:(UIControlState)state + withURL:(NSURL *)url + placeholderImage:(nullable UIImage *)placeholderImage; + +/** + Asynchronously downloads an image from the specified URL request, and sets it as the image for the specified state once the request is finished. Any previous image request for the receiver will be cancelled. + + If the image is cached locally, the image is set immediately, otherwise the specified placeholder image will be set immediately, and then the remote image will be set once the request is finished. + + If a success block is specified, it is the responsibility of the block to set the image of the button before returning. If no success block is specified, the default behavior of setting the image with `setImage:forState:` is applied. + + @param state The control state. + @param urlRequest The URL request used for the image request. + @param placeholderImage The image to be set initially, until the image request finishes. If `nil`, the button will not change its image until the image request finishes. + @param success A block to be executed when the image data task finishes successfully. This block has no return value and takes three arguments: the request sent from the client, the response received from the server, and the image created from the response data of request. If the image was returned from cache, the response parameter will be `nil`. + @param failure A block object to be executed when the image data task finishes unsuccessfully, or that finishes successfully. This block has no return value and takes three arguments: the request sent from the client, the response received from the server, and the error object describing the network or parsing error that occurred. + */ +- (void)setImageForState:(UIControlState)state + withURLRequest:(NSURLRequest *)urlRequest + placeholderImage:(nullable UIImage *)placeholderImage + success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *image))success + failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure; + + +///------------------------------- +/// @name Setting Background Image +///------------------------------- + +/** + Asynchronously downloads an image from the specified URL, and sets it as the background image for the specified state once the request is finished. Any previous background image request for the receiver will be cancelled. + + If the background image is cached locally, the background image is set immediately, otherwise the specified placeholder background image will be set immediately, and then the remote background image will be set once the request is finished. + + @param state The control state. + @param url The URL used for the background image request. + */ +- (void)setBackgroundImageForState:(UIControlState)state + withURL:(NSURL *)url; + +/** + Asynchronously downloads an image from the specified URL, and sets it as the background image for the specified state once the request is finished. Any previous image request for the receiver will be cancelled. + + If the image is cached locally, the image is set immediately, otherwise the specified placeholder image will be set immediately, and then the remote image will be set once the request is finished. + + @param state The control state. + @param url The URL used for the background image request. + @param placeholderImage The background image to be set initially, until the background image request finishes. If `nil`, the button will not change its background image until the background image request finishes. + */ +- (void)setBackgroundImageForState:(UIControlState)state + withURL:(NSURL *)url + placeholderImage:(nullable UIImage *)placeholderImage; + +/** + Asynchronously downloads an image from the specified URL request, and sets it as the image for the specified state once the request is finished. Any previous image request for the receiver will be cancelled. + + If the image is cached locally, the image is set immediately, otherwise the specified placeholder image will be set immediately, and then the remote image will be set once the request is finished. + + If a success block is specified, it is the responsibility of the block to set the image of the button before returning. If no success block is specified, the default behavior of setting the image with `setBackgroundImage:forState:` is applied. + + @param state The control state. + @param urlRequest The URL request used for the image request. + @param placeholderImage The background image to be set initially, until the background image request finishes. If `nil`, the button will not change its background image until the background image request finishes. + @param success A block to be executed when the image data task finishes successfully. This block has no return value and takes three arguments: the request sent from the client, the response received from the server, and the image created from the response data of request. If the image was returned from cache, the response parameter will be `nil`. + @param failure A block object to be executed when the image data task finishes unsuccessfully, or that finishes successfully. This block has no return value and takes three arguments: the request sent from the client, the response received from the server, and the error object describing the network or parsing error that occurred. + */ +- (void)setBackgroundImageForState:(UIControlState)state + withURLRequest:(NSURLRequest *)urlRequest + placeholderImage:(nullable UIImage *)placeholderImage + success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *image))success + failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure; + + +///------------------------------ +/// @name Canceling Image Loading +///------------------------------ + +/** + Cancels any executing image task for the specified control state of the receiver, if one exists. + + @param state The control state. + */ +- (void)cancelImageDownloadTaskForState:(UIControlState)state; + +/** + Cancels any executing background image task for the specified control state of the receiver, if one exists. + + @param state The control state. + */ +- (void)cancelBackgroundImageDownloadTaskForState:(UIControlState)state; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIButton+AFNetworking.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIButton+AFNetworking.m new file mode 100644 index 0000000000..ceb6ebc600 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIButton+AFNetworking.m @@ -0,0 +1,305 @@ +// UIButton+AFNetworking.m +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "UIButton+AFNetworking.h" + +#import + +#if TARGET_OS_IOS || TARGET_OS_TV + +#import "UIImageView+AFNetworking.h" +#import "AFImageDownloader.h" + +@interface UIButton (_AFNetworking) +@end + +@implementation UIButton (_AFNetworking) + +#pragma mark - + +static char AFImageDownloadReceiptNormal; +static char AFImageDownloadReceiptHighlighted; +static char AFImageDownloadReceiptSelected; +static char AFImageDownloadReceiptDisabled; + +static const char * af_imageDownloadReceiptKeyForState(UIControlState state) { + switch (state) { + case UIControlStateHighlighted: + return &AFImageDownloadReceiptHighlighted; + case UIControlStateSelected: + return &AFImageDownloadReceiptSelected; + case UIControlStateDisabled: + return &AFImageDownloadReceiptDisabled; + case UIControlStateNormal: + default: + return &AFImageDownloadReceiptNormal; + } +} + +- (AFImageDownloadReceipt *)af_imageDownloadReceiptForState:(UIControlState)state { + return (AFImageDownloadReceipt *)objc_getAssociatedObject(self, af_imageDownloadReceiptKeyForState(state)); +} + +- (void)af_setImageDownloadReceipt:(AFImageDownloadReceipt *)imageDownloadReceipt + forState:(UIControlState)state +{ + objc_setAssociatedObject(self, af_imageDownloadReceiptKeyForState(state), imageDownloadReceipt, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +#pragma mark - + +static char AFBackgroundImageDownloadReceiptNormal; +static char AFBackgroundImageDownloadReceiptHighlighted; +static char AFBackgroundImageDownloadReceiptSelected; +static char AFBackgroundImageDownloadReceiptDisabled; + +static const char * af_backgroundImageDownloadReceiptKeyForState(UIControlState state) { + switch (state) { + case UIControlStateHighlighted: + return &AFBackgroundImageDownloadReceiptHighlighted; + case UIControlStateSelected: + return &AFBackgroundImageDownloadReceiptSelected; + case UIControlStateDisabled: + return &AFBackgroundImageDownloadReceiptDisabled; + case UIControlStateNormal: + default: + return &AFBackgroundImageDownloadReceiptNormal; + } +} + +- (AFImageDownloadReceipt *)af_backgroundImageDownloadReceiptForState:(UIControlState)state { + return (AFImageDownloadReceipt *)objc_getAssociatedObject(self, af_backgroundImageDownloadReceiptKeyForState(state)); +} + +- (void)af_setBackgroundImageDownloadReceipt:(AFImageDownloadReceipt *)imageDownloadReceipt + forState:(UIControlState)state +{ + objc_setAssociatedObject(self, af_backgroundImageDownloadReceiptKeyForState(state), imageDownloadReceipt, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +@end + +#pragma mark - + +@implementation UIButton (AFNetworking) + ++ (AFImageDownloader *)sharedImageDownloader { + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgnu" + return objc_getAssociatedObject(self, @selector(sharedImageDownloader)) ?: [AFImageDownloader defaultInstance]; +#pragma clang diagnostic pop +} + ++ (void)setSharedImageDownloader:(AFImageDownloader *)imageDownloader { + objc_setAssociatedObject(self, @selector(sharedImageDownloader), imageDownloader, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +#pragma mark - + +- (void)setImageForState:(UIControlState)state + withURL:(NSURL *)url +{ + [self setImageForState:state withURL:url placeholderImage:nil]; +} + +- (void)setImageForState:(UIControlState)state + withURL:(NSURL *)url + placeholderImage:(UIImage *)placeholderImage +{ + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + [request addValue:@"image/*" forHTTPHeaderField:@"Accept"]; + + [self setImageForState:state withURLRequest:request placeholderImage:placeholderImage success:nil failure:nil]; +} + +- (void)setImageForState:(UIControlState)state + withURLRequest:(NSURLRequest *)urlRequest + placeholderImage:(nullable UIImage *)placeholderImage + success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *image))success + failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure +{ + if ([self isActiveTaskURLEqualToURLRequest:urlRequest forState:state]) { + return; + } + + [self cancelImageDownloadTaskForState:state]; + + AFImageDownloader *downloader = [[self class] sharedImageDownloader]; + id imageCache = downloader.imageCache; + + //Use the image from the image cache if it exists + UIImage *cachedImage = [imageCache imageforRequest:urlRequest withAdditionalIdentifier:nil]; + if (cachedImage) { + if (success) { + success(urlRequest, nil, cachedImage); + } else { + [self setImage:cachedImage forState:state]; + } + [self af_setImageDownloadReceipt:nil forState:state]; + } else { + if (placeholderImage) { + [self setImage:placeholderImage forState:state]; + } + + __weak __typeof(self)weakSelf = self; + NSUUID *downloadID = [NSUUID UUID]; + AFImageDownloadReceipt *receipt; + receipt = [downloader + downloadImageForURLRequest:urlRequest + withReceiptID:downloadID + success:^(NSURLRequest * _Nonnull request, NSHTTPURLResponse * _Nullable response, UIImage * _Nonnull responseObject) { + __strong __typeof(weakSelf)strongSelf = weakSelf; + if ([[strongSelf af_imageDownloadReceiptForState:state].receiptID isEqual:downloadID]) { + if (success) { + success(request, response, responseObject); + } else if(responseObject) { + [strongSelf setImage:responseObject forState:state]; + } + [strongSelf af_setImageDownloadReceipt:nil forState:state]; + } + + } + failure:^(NSURLRequest * _Nonnull request, NSHTTPURLResponse * _Nullable response, NSError * _Nonnull error) { + __strong __typeof(weakSelf)strongSelf = weakSelf; + if ([[strongSelf af_imageDownloadReceiptForState:state].receiptID isEqual:downloadID]) { + if (failure) { + failure(request, response, error); + } + [strongSelf af_setImageDownloadReceipt:nil forState:state]; + } + }]; + + [self af_setImageDownloadReceipt:receipt forState:state]; + } +} + +#pragma mark - + +- (void)setBackgroundImageForState:(UIControlState)state + withURL:(NSURL *)url +{ + [self setBackgroundImageForState:state withURL:url placeholderImage:nil]; +} + +- (void)setBackgroundImageForState:(UIControlState)state + withURL:(NSURL *)url + placeholderImage:(nullable UIImage *)placeholderImage +{ + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + [request addValue:@"image/*" forHTTPHeaderField:@"Accept"]; + + [self setBackgroundImageForState:state withURLRequest:request placeholderImage:placeholderImage success:nil failure:nil]; +} + +- (void)setBackgroundImageForState:(UIControlState)state + withURLRequest:(NSURLRequest *)urlRequest + placeholderImage:(nullable UIImage *)placeholderImage + success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *image))success + failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure +{ + if ([self isActiveBackgroundTaskURLEqualToURLRequest:urlRequest forState:state]) { + return; + } + + [self cancelImageDownloadTaskForState:state]; + + AFImageDownloader *downloader = [[self class] sharedImageDownloader]; + id imageCache = downloader.imageCache; + + //Use the image from the image cache if it exists + UIImage *cachedImage = [imageCache imageforRequest:urlRequest withAdditionalIdentifier:nil]; + if (cachedImage) { + if (success) { + success(urlRequest, nil, cachedImage); + } else { + [self setBackgroundImage:cachedImage forState:state]; + } + [self af_setBackgroundImageDownloadReceipt:nil forState:state]; + } else { + if (placeholderImage) { + [self setBackgroundImage:placeholderImage forState:state]; + } + + __weak __typeof(self)weakSelf = self; + NSUUID *downloadID = [NSUUID UUID]; + AFImageDownloadReceipt *receipt; + receipt = [downloader + downloadImageForURLRequest:urlRequest + withReceiptID:downloadID + success:^(NSURLRequest * _Nonnull request, NSHTTPURLResponse * _Nullable response, UIImage * _Nonnull responseObject) { + __strong __typeof(weakSelf)strongSelf = weakSelf; + if ([[strongSelf af_backgroundImageDownloadReceiptForState:state].receiptID isEqual:downloadID]) { + if (success) { + success(request, response, responseObject); + } else if(responseObject) { + [strongSelf setBackgroundImage:responseObject forState:state]; + } + [strongSelf af_setImageDownloadReceipt:nil forState:state]; + } + + } + failure:^(NSURLRequest * _Nonnull request, NSHTTPURLResponse * _Nullable response, NSError * _Nonnull error) { + __strong __typeof(weakSelf)strongSelf = weakSelf; + if ([[strongSelf af_backgroundImageDownloadReceiptForState:state].receiptID isEqual:downloadID]) { + if (failure) { + failure(request, response, error); + } + [strongSelf af_setBackgroundImageDownloadReceipt:nil forState:state]; + } + }]; + + [self af_setBackgroundImageDownloadReceipt:receipt forState:state]; + } +} + +#pragma mark - + +- (void)cancelImageDownloadTaskForState:(UIControlState)state { + AFImageDownloadReceipt *receipt = [self af_imageDownloadReceiptForState:state]; + if (receipt != nil) { + [[self.class sharedImageDownloader] cancelTaskForImageDownloadReceipt:receipt]; + [self af_setImageDownloadReceipt:nil forState:state]; + } +} + +- (void)cancelBackgroundImageDownloadTaskForState:(UIControlState)state { + AFImageDownloadReceipt *receipt = [self af_backgroundImageDownloadReceiptForState:state]; + if (receipt != nil) { + [[self.class sharedImageDownloader] cancelTaskForImageDownloadReceipt:receipt]; + [self af_setBackgroundImageDownloadReceipt:nil forState:state]; + } +} + +- (BOOL)isActiveTaskURLEqualToURLRequest:(NSURLRequest *)urlRequest forState:(UIControlState)state { + AFImageDownloadReceipt *receipt = [self af_imageDownloadReceiptForState:state]; + return [receipt.task.originalRequest.URL.absoluteString isEqualToString:urlRequest.URL.absoluteString]; +} + +- (BOOL)isActiveBackgroundTaskURLEqualToURLRequest:(NSURLRequest *)urlRequest forState:(UIControlState)state { + AFImageDownloadReceipt *receipt = [self af_backgroundImageDownloadReceiptForState:state]; + return [receipt.task.originalRequest.URL.absoluteString isEqualToString:urlRequest.URL.absoluteString]; +} + + +@end + +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIImage+AFNetworking.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIImage+AFNetworking.h new file mode 100644 index 0000000000..14744cddd5 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIImage+AFNetworking.h @@ -0,0 +1,35 @@ +// +// UIImage+AFNetworking.h +// +// +// Created by Paulo Ferreira on 08/07/15. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if TARGET_OS_IOS || TARGET_OS_TV + +#import + +@interface UIImage (AFNetworking) + ++ (UIImage*) safeImageWithData:(NSData*)data; + +@end + +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIImageView+AFNetworking.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIImageView+AFNetworking.h new file mode 100644 index 0000000000..ce9ae2e61e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIImageView+AFNetworking.h @@ -0,0 +1,109 @@ +// UIImageView+AFNetworking.h +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +#import + +#if TARGET_OS_IOS || TARGET_OS_TV + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class AFImageDownloader; + +/** + This category adds methods to the UIKit framework's `UIImageView` class. The methods in this category provide support for loading remote images asynchronously from a URL. + */ +@interface UIImageView (AFNetworking) + +///------------------------------------ +/// @name Accessing the Image Downloader +///------------------------------------ + +/** + Set the shared image downloader used to download images. + + @param imageDownloader The shared image downloader used to download images. + */ ++ (void)setSharedImageDownloader:(AFImageDownloader *)imageDownloader; + +/** + The shared image downloader used to download images. + */ ++ (AFImageDownloader *)sharedImageDownloader; + +///-------------------- +/// @name Setting Image +///-------------------- + +/** + Asynchronously downloads an image from the specified URL, and sets it once the request is finished. Any previous image request for the receiver will be cancelled. + + If the image is cached locally, the image is set immediately, otherwise the specified placeholder image will be set immediately, and then the remote image will be set once the request is finished. + + By default, URL requests have a `Accept` header field value of "image / *", a cache policy of `NSURLCacheStorageAllowed` and a timeout interval of 30 seconds, and are set not handle cookies. To configure URL requests differently, use `setImageWithURLRequest:placeholderImage:success:failure:` + + @param url The URL used for the image request. + */ +- (void)setImageWithURL:(NSURL *)url; + +/** + Asynchronously downloads an image from the specified URL, and sets it once the request is finished. Any previous image request for the receiver will be cancelled. + + If the image is cached locally, the image is set immediately, otherwise the specified placeholder image will be set immediately, and then the remote image will be set once the request is finished. + + By default, URL requests have a `Accept` header field value of "image / *", a cache policy of `NSURLCacheStorageAllowed` and a timeout interval of 30 seconds, and are set not handle cookies. To configure URL requests differently, use `setImageWithURLRequest:placeholderImage:success:failure:` + + @param url The URL used for the image request. + @param placeholderImage The image to be set initially, until the image request finishes. If `nil`, the image view will not change its image until the image request finishes. + */ +- (void)setImageWithURL:(NSURL *)url + placeholderImage:(nullable UIImage *)placeholderImage; + +/** + Asynchronously downloads an image from the specified URL request, and sets it once the request is finished. Any previous image request for the receiver will be cancelled. + + If the image is cached locally, the image is set immediately, otherwise the specified placeholder image will be set immediately, and then the remote image will be set once the request is finished. + + If a success block is specified, it is the responsibility of the block to set the image of the image view before returning. If no success block is specified, the default behavior of setting the image with `self.image = image` is applied. + + @param urlRequest The URL request used for the image request. + @param placeholderImage The image to be set initially, until the image request finishes. If `nil`, the image view will not change its image until the image request finishes. + @param success A block to be executed when the image data task finishes successfully. This block has no return value and takes three arguments: the request sent from the client, the response received from the server, and the image created from the response data of request. If the image was returned from cache, the response parameter will be `nil`. + @param failure A block object to be executed when the image data task finishes unsuccessfully, or that finishes successfully. This block has no return value and takes three arguments: the request sent from the client, the response received from the server, and the error object describing the network or parsing error that occurred. + */ +- (void)setImageWithURLRequest:(NSURLRequest *)urlRequest + placeholderImage:(nullable UIImage *)placeholderImage + success:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *image))success + failure:(nullable void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure; + +/** + Cancels any executing image operation for the receiver, if one exists. + */ +- (void)cancelImageDownloadTask; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIImageView+AFNetworking.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIImageView+AFNetworking.m new file mode 100644 index 0000000000..a97d5cc4f8 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIImageView+AFNetworking.m @@ -0,0 +1,161 @@ +// UIImageView+AFNetworking.m +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "UIImageView+AFNetworking.h" + +#import + +#if TARGET_OS_IOS || TARGET_OS_TV + +#import "AFImageDownloader.h" + +@interface UIImageView (_AFNetworking) +@property (readwrite, nonatomic, strong, setter = af_setActiveImageDownloadReceipt:) AFImageDownloadReceipt *af_activeImageDownloadReceipt; +@end + +@implementation UIImageView (_AFNetworking) + +- (AFImageDownloadReceipt *)af_activeImageDownloadReceipt { + return (AFImageDownloadReceipt *)objc_getAssociatedObject(self, @selector(af_activeImageDownloadReceipt)); +} + +- (void)af_setActiveImageDownloadReceipt:(AFImageDownloadReceipt *)imageDownloadReceipt { + objc_setAssociatedObject(self, @selector(af_activeImageDownloadReceipt), imageDownloadReceipt, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +@end + +#pragma mark - + +@implementation UIImageView (AFNetworking) + ++ (AFImageDownloader *)sharedImageDownloader { + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgnu" + return objc_getAssociatedObject(self, @selector(sharedImageDownloader)) ?: [AFImageDownloader defaultInstance]; +#pragma clang diagnostic pop +} + ++ (void)setSharedImageDownloader:(AFImageDownloader *)imageDownloader { + objc_setAssociatedObject(self, @selector(sharedImageDownloader), imageDownloader, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +#pragma mark - + +- (void)setImageWithURL:(NSURL *)url { + [self setImageWithURL:url placeholderImage:nil]; +} + +- (void)setImageWithURL:(NSURL *)url + placeholderImage:(UIImage *)placeholderImage +{ + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + [request addValue:@"image/*" forHTTPHeaderField:@"Accept"]; + + [self setImageWithURLRequest:request placeholderImage:placeholderImage success:nil failure:nil]; +} + +- (void)setImageWithURLRequest:(NSURLRequest *)urlRequest + placeholderImage:(UIImage *)placeholderImage + success:(void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, UIImage *image))success + failure:(void (^)(NSURLRequest *request, NSHTTPURLResponse * _Nullable response, NSError *error))failure +{ + + if ([urlRequest URL] == nil) { + [self cancelImageDownloadTask]; + self.image = placeholderImage; + return; + } + + if ([self isActiveTaskURLEqualToURLRequest:urlRequest]){ + return; + } + + [self cancelImageDownloadTask]; + + AFImageDownloader *downloader = [[self class] sharedImageDownloader]; + id imageCache = downloader.imageCache; + + //Use the image from the image cache if it exists + UIImage *cachedImage = [imageCache imageforRequest:urlRequest withAdditionalIdentifier:nil]; + if (cachedImage) { + if (success) { + success(urlRequest, nil, cachedImage); + } else { + self.image = cachedImage; + } + [self clearActiveDownloadInformation]; + } else { + if (placeholderImage) { + self.image = placeholderImage; + } + + __weak __typeof(self)weakSelf = self; + NSUUID *downloadID = [NSUUID UUID]; + AFImageDownloadReceipt *receipt; + receipt = [downloader + downloadImageForURLRequest:urlRequest + withReceiptID:downloadID + success:^(NSURLRequest * _Nonnull request, NSHTTPURLResponse * _Nullable response, UIImage * _Nonnull responseObject) { + __strong __typeof(weakSelf)strongSelf = weakSelf; + if ([strongSelf.af_activeImageDownloadReceipt.receiptID isEqual:downloadID]) { + if (success) { + success(request, response, responseObject); + } else if(responseObject) { + strongSelf.image = responseObject; + } + [strongSelf clearActiveDownloadInformation]; + } + + } + failure:^(NSURLRequest * _Nonnull request, NSHTTPURLResponse * _Nullable response, NSError * _Nonnull error) { + __strong __typeof(weakSelf)strongSelf = weakSelf; + if ([strongSelf.af_activeImageDownloadReceipt.receiptID isEqual:downloadID]) { + if (failure) { + failure(request, response, error); + } + [strongSelf clearActiveDownloadInformation]; + } + }]; + + self.af_activeImageDownloadReceipt = receipt; + } +} + +- (void)cancelImageDownloadTask { + if (self.af_activeImageDownloadReceipt != nil) { + [[self.class sharedImageDownloader] cancelTaskForImageDownloadReceipt:self.af_activeImageDownloadReceipt]; + [self clearActiveDownloadInformation]; + } +} + +- (void)clearActiveDownloadInformation { + self.af_activeImageDownloadReceipt = nil; +} + +- (BOOL)isActiveTaskURLEqualToURLRequest:(NSURLRequest *)urlRequest { + return [self.af_activeImageDownloadReceipt.task.originalRequest.URL.absoluteString isEqualToString:urlRequest.URL.absoluteString]; +} + +@end + +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIKit+AFNetworking.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIKit+AFNetworking.h new file mode 100644 index 0000000000..b36ee0c5bb --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIKit+AFNetworking.h @@ -0,0 +1,42 @@ +// UIKit+AFNetworking.h +// +// Copyright (c) 2013 AFNetworking (http://afnetworking.com/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if TARGET_OS_IOS || TARGET_OS_TV +#import + +#ifndef _UIKIT_AFNETWORKING_ + #define _UIKIT_AFNETWORKING_ + +#if TARGET_OS_IOS + #import "AFAutoPurgingImageCache.h" + #import "AFImageDownloader.h" + #import "AFNetworkActivityIndicatorManager.h" + #import "UIRefreshControl+AFNetworking.h" + #import "UIWebView+AFNetworking.h" +#endif + + #import "UIActivityIndicatorView+AFNetworking.h" + #import "UIButton+AFNetworking.h" + #import "UIImageView+AFNetworking.h" + #import "UIProgressView+AFNetworking.h" +#endif /* _UIKIT_AFNETWORKING_ */ +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIProgressView+AFNetworking.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIProgressView+AFNetworking.h new file mode 100644 index 0000000000..a0c463b5f1 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIProgressView+AFNetworking.h @@ -0,0 +1,64 @@ +// UIProgressView+AFNetworking.h +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +#import + +#if TARGET_OS_IOS || TARGET_OS_TV + +#import + +NS_ASSUME_NONNULL_BEGIN + + +/** + This category adds methods to the UIKit framework's `UIProgressView` class. The methods in this category provide support for binding the progress to the upload and download progress of a session task. + */ +@interface UIProgressView (AFNetworking) + +///------------------------------------ +/// @name Setting Session Task Progress +///------------------------------------ + +/** + Binds the progress to the upload progress of the specified session task. + + @param task The session task. + @param animated `YES` if the change should be animated, `NO` if the change should happen immediately. + */ +- (void)setProgressWithUploadProgressOfTask:(NSURLSessionUploadTask *)task + animated:(BOOL)animated; + +/** + Binds the progress to the download progress of the specified session task. + + @param task The session task. + @param animated `YES` if the change should be animated, `NO` if the change should happen immediately. + */ +- (void)setProgressWithDownloadProgressOfTask:(NSURLSessionDownloadTask *)task + animated:(BOOL)animated; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIProgressView+AFNetworking.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIProgressView+AFNetworking.m new file mode 100644 index 0000000000..6680bacce6 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIProgressView+AFNetworking.m @@ -0,0 +1,118 @@ +// UIProgressView+AFNetworking.m +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "UIProgressView+AFNetworking.h" + +#import + +#if TARGET_OS_IOS || TARGET_OS_TV + +#import "AFURLSessionManager.h" + +static void * AFTaskCountOfBytesSentContext = &AFTaskCountOfBytesSentContext; +static void * AFTaskCountOfBytesReceivedContext = &AFTaskCountOfBytesReceivedContext; + +#pragma mark - + +@implementation UIProgressView (AFNetworking) + +- (BOOL)af_uploadProgressAnimated { + return [(NSNumber *)objc_getAssociatedObject(self, @selector(af_uploadProgressAnimated)) boolValue]; +} + +- (void)af_setUploadProgressAnimated:(BOOL)animated { + objc_setAssociatedObject(self, @selector(af_uploadProgressAnimated), @(animated), OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (BOOL)af_downloadProgressAnimated { + return [(NSNumber *)objc_getAssociatedObject(self, @selector(af_downloadProgressAnimated)) boolValue]; +} + +- (void)af_setDownloadProgressAnimated:(BOOL)animated { + objc_setAssociatedObject(self, @selector(af_downloadProgressAnimated), @(animated), OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +#pragma mark - + +- (void)setProgressWithUploadProgressOfTask:(NSURLSessionUploadTask *)task + animated:(BOOL)animated +{ + [task addObserver:self forKeyPath:@"state" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesSentContext]; + [task addObserver:self forKeyPath:@"countOfBytesSent" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesSentContext]; + + [self af_setUploadProgressAnimated:animated]; +} + +- (void)setProgressWithDownloadProgressOfTask:(NSURLSessionDownloadTask *)task + animated:(BOOL)animated +{ + [task addObserver:self forKeyPath:@"state" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesReceivedContext]; + [task addObserver:self forKeyPath:@"countOfBytesReceived" options:(NSKeyValueObservingOptions)0 context:AFTaskCountOfBytesReceivedContext]; + + [self af_setDownloadProgressAnimated:animated]; +} + +#pragma mark - NSKeyValueObserving + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(__unused NSDictionary *)change + context:(void *)context +{ + if (context == AFTaskCountOfBytesSentContext || context == AFTaskCountOfBytesReceivedContext) { + if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesSent))]) { + if ([object countOfBytesExpectedToSend] > 0) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self setProgress:[object countOfBytesSent] / ([object countOfBytesExpectedToSend] * 1.0f) animated:self.af_uploadProgressAnimated]; + }); + } + } + + if ([keyPath isEqualToString:NSStringFromSelector(@selector(countOfBytesReceived))]) { + if ([object countOfBytesExpectedToReceive] > 0) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self setProgress:[object countOfBytesReceived] / ([object countOfBytesExpectedToReceive] * 1.0f) animated:self.af_downloadProgressAnimated]; + }); + } + } + + if ([keyPath isEqualToString:NSStringFromSelector(@selector(state))]) { + if ([(NSURLSessionTask *)object state] == NSURLSessionTaskStateCompleted) { + @try { + [object removeObserver:self forKeyPath:NSStringFromSelector(@selector(state))]; + + if (context == AFTaskCountOfBytesSentContext) { + [object removeObserver:self forKeyPath:NSStringFromSelector(@selector(countOfBytesSent))]; + } + + if (context == AFTaskCountOfBytesReceivedContext) { + [object removeObserver:self forKeyPath:NSStringFromSelector(@selector(countOfBytesReceived))]; + } + } + @catch (NSException * __unused exception) {} + } + } + } +} + +@end + +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIRefreshControl+AFNetworking.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIRefreshControl+AFNetworking.h new file mode 100644 index 0000000000..f6930a98e3 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIRefreshControl+AFNetworking.h @@ -0,0 +1,53 @@ +// UIRefreshControl+AFNetworking.m +// +// Copyright (c) 2014 AFNetworking (http://afnetworking.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +#import + +#if TARGET_OS_IOS + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + This category adds methods to the UIKit framework's `UIRefreshControl` class. The methods in this category provide support for automatically beginning and ending refreshing depending on the loading state of a session task. + */ +@interface UIRefreshControl (AFNetworking) + +///----------------------------------- +/// @name Refreshing for Session Tasks +///----------------------------------- + +/** + Binds the refreshing state to the state of the specified task. + + @param task The task. If `nil`, automatic updating from any previously specified operation will be disabled. + */ +- (void)setRefreshingWithStateOfTask:(NSURLSessionTask *)task; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIRefreshControl+AFNetworking.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIRefreshControl+AFNetworking.m new file mode 100644 index 0000000000..ddc033b963 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIRefreshControl+AFNetworking.m @@ -0,0 +1,122 @@ +// UIRefreshControl+AFNetworking.m +// +// Copyright (c) 2014 AFNetworking (http://afnetworking.com) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "UIRefreshControl+AFNetworking.h" +#import + +#if TARGET_OS_IOS + +#import "AFURLSessionManager.h" + +@interface AFRefreshControlNotificationObserver : NSObject +@property (readonly, nonatomic, weak) UIRefreshControl *refreshControl; +- (instancetype)initWithActivityRefreshControl:(UIRefreshControl *)refreshControl; + +- (void)setRefreshingWithStateOfTask:(NSURLSessionTask *)task; + +@end + +@implementation UIRefreshControl (AFNetworking) + +- (AFRefreshControlNotificationObserver *)af_notificationObserver { + AFRefreshControlNotificationObserver *notificationObserver = objc_getAssociatedObject(self, @selector(af_notificationObserver)); + if (notificationObserver == nil) { + notificationObserver = [[AFRefreshControlNotificationObserver alloc] initWithActivityRefreshControl:self]; + objc_setAssociatedObject(self, @selector(af_notificationObserver), notificationObserver, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + return notificationObserver; +} + +- (void)setRefreshingWithStateOfTask:(NSURLSessionTask *)task { + [[self af_notificationObserver] setRefreshingWithStateOfTask:task]; +} + +@end + +@implementation AFRefreshControlNotificationObserver + +- (instancetype)initWithActivityRefreshControl:(UIRefreshControl *)refreshControl +{ + self = [super init]; + if (self) { + _refreshControl = refreshControl; + } + return self; +} + +- (void)setRefreshingWithStateOfTask:(NSURLSessionTask *)task { + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + + [notificationCenter removeObserver:self name:AFNetworkingTaskDidResumeNotification object:nil]; + [notificationCenter removeObserver:self name:AFNetworkingTaskDidSuspendNotification object:nil]; + [notificationCenter removeObserver:self name:AFNetworkingTaskDidCompleteNotification object:nil]; + + if (task) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreceiver-is-weak" +#pragma clang diagnostic ignored "-Warc-repeated-use-of-weak" + if (task.state == NSURLSessionTaskStateRunning) { + [self.refreshControl beginRefreshing]; + + [notificationCenter addObserver:self selector:@selector(af_beginRefreshing) name:AFNetworkingTaskDidResumeNotification object:task]; + [notificationCenter addObserver:self selector:@selector(af_endRefreshing) name:AFNetworkingTaskDidCompleteNotification object:task]; + [notificationCenter addObserver:self selector:@selector(af_endRefreshing) name:AFNetworkingTaskDidSuspendNotification object:task]; + } else { + [self.refreshControl endRefreshing]; + } +#pragma clang diagnostic pop + } +} + +#pragma mark - + +- (void)af_beginRefreshing { + dispatch_async(dispatch_get_main_queue(), ^{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreceiver-is-weak" + [self.refreshControl beginRefreshing]; +#pragma clang diagnostic pop + }); +} + +- (void)af_endRefreshing { + dispatch_async(dispatch_get_main_queue(), ^{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreceiver-is-weak" + [self.refreshControl endRefreshing]; +#pragma clang diagnostic pop + }); +} + +#pragma mark - + +- (void)dealloc { + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + + [notificationCenter removeObserver:self name:AFNetworkingTaskDidCompleteNotification object:nil]; + [notificationCenter removeObserver:self name:AFNetworkingTaskDidResumeNotification object:nil]; + [notificationCenter removeObserver:self name:AFNetworkingTaskDidSuspendNotification object:nil]; +} + +@end + +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIWebView+AFNetworking.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIWebView+AFNetworking.h new file mode 100644 index 0000000000..777dce2a57 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIWebView+AFNetworking.h @@ -0,0 +1,80 @@ +// UIWebView+AFNetworking.h +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import + +#import + +#if TARGET_OS_IOS + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class AFHTTPSessionManager; + +/** + This category adds methods to the UIKit framework's `UIWebView` class. The methods in this category provide increased control over the request cycle, including progress monitoring and success / failure handling. + + @discussion When using these category methods, make sure to assign `delegate` for the web view, which implements `–webView:shouldStartLoadWithRequest:navigationType:` appropriately. This allows for tapped links to be loaded through AFNetworking, and can ensure that `canGoBack` & `canGoForward` update their values correctly. + */ +@interface UIWebView (AFNetworking) + +/** + The session manager used to download all requests. + */ +@property (nonatomic, strong) AFHTTPSessionManager *sessionManager; + +/** + Asynchronously loads the specified request. + + @param request A URL request identifying the location of the content to load. This must not be `nil`. + @param progress A progress object monitoring the current download progress. + @param success A block object to be executed when the request finishes loading successfully. This block returns the HTML string to be loaded by the web view, and takes two arguments: the response, and the response string. + @param failure A block object to be executed when the data task finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a single argument: the error that occurred. + */ +- (void)loadRequest:(NSURLRequest *)request + progress:(NSProgress * _Nullable __autoreleasing * _Nullable)progress + success:(nullable NSString * (^)(NSHTTPURLResponse *response, NSString *HTML))success + failure:(nullable void (^)(NSError *error))failure; + +/** + Asynchronously loads the data associated with a particular request with a specified MIME type and text encoding. + + @param request A URL request identifying the location of the content to load. This must not be `nil`. + @param MIMEType The MIME type of the content. Defaults to the content type of the response if not specified. + @param textEncodingName The IANA encoding name, as in `utf-8` or `utf-16`. Defaults to the response text encoding if not specified. +@param progress A progress object monitoring the current download progress. + @param success A block object to be executed when the request finishes loading successfully. This block returns the data to be loaded by the web view and takes two arguments: the response, and the downloaded data. + @param failure A block object to be executed when the data task finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the response data. This block has no return value and takes a single argument: the error that occurred. + */ +- (void)loadRequest:(NSURLRequest *)request + MIMEType:(nullable NSString *)MIMEType + textEncodingName:(nullable NSString *)textEncodingName + progress:(NSProgress * _Nullable __autoreleasing * _Nullable)progress + success:(nullable NSData * (^)(NSHTTPURLResponse *response, NSData *data))success + failure:(nullable void (^)(NSError *error))failure; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIWebView+AFNetworking.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIWebView+AFNetworking.m new file mode 100644 index 0000000000..ac089da15f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/AFNetworking/UIKit+AFNetworking/UIWebView+AFNetworking.m @@ -0,0 +1,160 @@ +// UIWebView+AFNetworking.m +// Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import "UIWebView+AFNetworking.h" + +#import + +#if TARGET_OS_IOS + +#import "AFHTTPSessionManager.h" +#import "AFURLResponseSerialization.h" +#import "AFURLRequestSerialization.h" + +@interface UIWebView (_AFNetworking) +@property (readwrite, nonatomic, strong, setter = af_setURLSessionTask:) NSURLSessionDataTask *af_URLSessionTask; +@end + +@implementation UIWebView (_AFNetworking) + +- (NSURLSessionDataTask *)af_URLSessionTask { + return (NSURLSessionDataTask *)objc_getAssociatedObject(self, @selector(af_URLSessionTask)); +} + +- (void)af_setURLSessionTask:(NSURLSessionDataTask *)af_URLSessionTask { + objc_setAssociatedObject(self, @selector(af_URLSessionTask), af_URLSessionTask, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +@end + +#pragma mark - + +@implementation UIWebView (AFNetworking) + +- (AFHTTPSessionManager *)sessionManager { + static AFHTTPSessionManager *_af_defaultHTTPSessionManager = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _af_defaultHTTPSessionManager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; + _af_defaultHTTPSessionManager.requestSerializer = [AFHTTPRequestSerializer serializer]; + _af_defaultHTTPSessionManager.responseSerializer = [AFHTTPResponseSerializer serializer]; + }); + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgnu" + return objc_getAssociatedObject(self, @selector(sessionManager)) ?: _af_defaultHTTPSessionManager; +#pragma clang diagnostic pop +} + +- (void)setSessionManager:(AFHTTPSessionManager *)sessionManager { + objc_setAssociatedObject(self, @selector(sessionManager), sessionManager, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (AFHTTPResponseSerializer *)responseSerializer { + static AFHTTPResponseSerializer *_af_defaultResponseSerializer = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _af_defaultResponseSerializer = [AFHTTPResponseSerializer serializer]; + }); + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgnu" + return objc_getAssociatedObject(self, @selector(responseSerializer)) ?: _af_defaultResponseSerializer; +#pragma clang diagnostic pop +} + +- (void)setResponseSerializer:(AFHTTPResponseSerializer *)responseSerializer { + objc_setAssociatedObject(self, @selector(responseSerializer), responseSerializer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +#pragma mark - + +- (void)loadRequest:(NSURLRequest *)request + progress:(NSProgress * _Nullable __autoreleasing * _Nullable)progress + success:(NSString * (^)(NSHTTPURLResponse *response, NSString *HTML))success + failure:(void (^)(NSError *error))failure +{ + [self loadRequest:request MIMEType:nil textEncodingName:nil progress:progress success:^NSData *(NSHTTPURLResponse *response, NSData *data) { + NSStringEncoding stringEncoding = NSUTF8StringEncoding; + if (response.textEncodingName) { + CFStringEncoding encoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)response.textEncodingName); + if (encoding != kCFStringEncodingInvalidId) { + stringEncoding = CFStringConvertEncodingToNSStringEncoding(encoding); + } + } + + NSString *string = [[NSString alloc] initWithData:data encoding:stringEncoding]; + if (success) { + string = success(response, string); + } + + return [string dataUsingEncoding:stringEncoding]; + } failure:failure]; +} + +- (void)loadRequest:(NSURLRequest *)request + MIMEType:(NSString *)MIMEType + textEncodingName:(NSString *)textEncodingName + progress:(NSProgress * _Nullable __autoreleasing * _Nullable)progress + success:(NSData * (^)(NSHTTPURLResponse *response, NSData *data))success + failure:(void (^)(NSError *error))failure +{ + NSParameterAssert(request); + + if (self.af_URLSessionTask.state == NSURLSessionTaskStateRunning || self.af_URLSessionTask.state == NSURLSessionTaskStateSuspended) { + [self.af_URLSessionTask cancel]; + } + self.af_URLSessionTask = nil; + + __weak __typeof(self)weakSelf = self; + NSURLSessionDataTask *dataTask; + dataTask = [self.sessionManager + GET:request.URL.absoluteString + parameters:nil + progress:nil + success:^(NSURLSessionDataTask * _Nonnull task, id _Nonnull responseObject) { + __strong __typeof(weakSelf) strongSelf = weakSelf; + if (success) { + success((NSHTTPURLResponse *)task.response, responseObject); + } + [strongSelf loadData:responseObject MIMEType:MIMEType textEncodingName:textEncodingName baseURL:[task.currentRequest URL]]; + + if ([strongSelf.delegate respondsToSelector:@selector(webViewDidStartLoad:)]) { + [strongSelf.delegate webViewDidFinishLoad:strongSelf]; + } + } + failure:^(NSURLSessionDataTask * _Nonnull task, NSError * _Nonnull error) { + if (failure) { + failure(error); + } + }]; + self.af_URLSessionTask = dataTask; + *progress = [self.sessionManager downloadProgressForTask:dataTask]; + [self.af_URLSessionTask resume]; + + if ([self.delegate respondsToSelector:@selector(webViewDidStartLoad:)]) { + [self.delegate webViewDidStartLoad:self]; + } +} + +@end + +#endif \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFAutoPurgingImageCache.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFAutoPurgingImageCache.h new file mode 120000 index 0000000000..f9dc7db14b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFAutoPurgingImageCache.h @@ -0,0 +1 @@ +../../../AFNetworking/UIKit+AFNetworking/AFAutoPurgingImageCache.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFHTTPSessionManager.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFHTTPSessionManager.h new file mode 120000 index 0000000000..56feb9fb85 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFHTTPSessionManager.h @@ -0,0 +1 @@ +../../../AFNetworking/AFNetworking/AFHTTPSessionManager.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFImageDownloader.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFImageDownloader.h new file mode 120000 index 0000000000..ce47c927fd --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFImageDownloader.h @@ -0,0 +1 @@ +../../../AFNetworking/UIKit+AFNetworking/AFImageDownloader.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFNetworkActivityIndicatorManager.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFNetworkActivityIndicatorManager.h new file mode 120000 index 0000000000..67519d9848 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFNetworkActivityIndicatorManager.h @@ -0,0 +1 @@ +../../../AFNetworking/UIKit+AFNetworking/AFNetworkActivityIndicatorManager.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFNetworkReachabilityManager.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFNetworkReachabilityManager.h new file mode 120000 index 0000000000..68fc7744f2 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFNetworkReachabilityManager.h @@ -0,0 +1 @@ +../../../AFNetworking/AFNetworking/AFNetworkReachabilityManager.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFNetworking.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFNetworking.h new file mode 120000 index 0000000000..a5a38da7dc --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFNetworking.h @@ -0,0 +1 @@ +../../../AFNetworking/AFNetworking/AFNetworking.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFSecurityPolicy.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFSecurityPolicy.h new file mode 120000 index 0000000000..fd1322db9c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFSecurityPolicy.h @@ -0,0 +1 @@ +../../../AFNetworking/AFNetworking/AFSecurityPolicy.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFURLRequestSerialization.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFURLRequestSerialization.h new file mode 120000 index 0000000000..ca8209b81f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFURLRequestSerialization.h @@ -0,0 +1 @@ +../../../AFNetworking/AFNetworking/AFURLRequestSerialization.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFURLResponseSerialization.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFURLResponseSerialization.h new file mode 120000 index 0000000000..e36a765d82 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFURLResponseSerialization.h @@ -0,0 +1 @@ +../../../AFNetworking/AFNetworking/AFURLResponseSerialization.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFURLSessionManager.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFURLSessionManager.h new file mode 120000 index 0000000000..835101de7b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/AFURLSessionManager.h @@ -0,0 +1 @@ +../../../AFNetworking/AFNetworking/AFURLSessionManager.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/UIActivityIndicatorView+AFNetworking.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/UIActivityIndicatorView+AFNetworking.h new file mode 120000 index 0000000000..c534ebfb02 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/UIActivityIndicatorView+AFNetworking.h @@ -0,0 +1 @@ +../../../AFNetworking/UIKit+AFNetworking/UIActivityIndicatorView+AFNetworking.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/UIButton+AFNetworking.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/UIButton+AFNetworking.h new file mode 120000 index 0000000000..8f2e221939 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/UIButton+AFNetworking.h @@ -0,0 +1 @@ +../../../AFNetworking/UIKit+AFNetworking/UIButton+AFNetworking.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/UIImage+AFNetworking.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/UIImage+AFNetworking.h new file mode 120000 index 0000000000..74f6649909 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/UIImage+AFNetworking.h @@ -0,0 +1 @@ +../../../AFNetworking/UIKit+AFNetworking/UIImage+AFNetworking.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/UIImageView+AFNetworking.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/UIImageView+AFNetworking.h new file mode 120000 index 0000000000..a95d67380f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/UIImageView+AFNetworking.h @@ -0,0 +1 @@ +../../../AFNetworking/UIKit+AFNetworking/UIImageView+AFNetworking.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/UIKit+AFNetworking.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/UIKit+AFNetworking.h new file mode 120000 index 0000000000..95017cce57 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/UIKit+AFNetworking.h @@ -0,0 +1 @@ +../../../AFNetworking/UIKit+AFNetworking/UIKit+AFNetworking.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/UIProgressView+AFNetworking.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/UIProgressView+AFNetworking.h new file mode 120000 index 0000000000..730b167dcd --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/UIProgressView+AFNetworking.h @@ -0,0 +1 @@ +../../../AFNetworking/UIKit+AFNetworking/UIProgressView+AFNetworking.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/UIRefreshControl+AFNetworking.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/UIRefreshControl+AFNetworking.h new file mode 120000 index 0000000000..8efd826209 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/UIRefreshControl+AFNetworking.h @@ -0,0 +1 @@ +../../../AFNetworking/UIKit+AFNetworking/UIRefreshControl+AFNetworking.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/UIWebView+AFNetworking.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/UIWebView+AFNetworking.h new file mode 120000 index 0000000000..c8df6ef17b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Private/AFNetworking/UIWebView+AFNetworking.h @@ -0,0 +1 @@ +../../../AFNetworking/UIKit+AFNetworking/UIWebView+AFNetworking.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFAutoPurgingImageCache.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFAutoPurgingImageCache.h new file mode 120000 index 0000000000..f9dc7db14b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFAutoPurgingImageCache.h @@ -0,0 +1 @@ +../../../AFNetworking/UIKit+AFNetworking/AFAutoPurgingImageCache.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFHTTPSessionManager.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFHTTPSessionManager.h new file mode 120000 index 0000000000..56feb9fb85 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFHTTPSessionManager.h @@ -0,0 +1 @@ +../../../AFNetworking/AFNetworking/AFHTTPSessionManager.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFImageDownloader.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFImageDownloader.h new file mode 120000 index 0000000000..ce47c927fd --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFImageDownloader.h @@ -0,0 +1 @@ +../../../AFNetworking/UIKit+AFNetworking/AFImageDownloader.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFNetworkActivityIndicatorManager.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFNetworkActivityIndicatorManager.h new file mode 120000 index 0000000000..67519d9848 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFNetworkActivityIndicatorManager.h @@ -0,0 +1 @@ +../../../AFNetworking/UIKit+AFNetworking/AFNetworkActivityIndicatorManager.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFNetworkReachabilityManager.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFNetworkReachabilityManager.h new file mode 120000 index 0000000000..68fc7744f2 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFNetworkReachabilityManager.h @@ -0,0 +1 @@ +../../../AFNetworking/AFNetworking/AFNetworkReachabilityManager.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFNetworking.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFNetworking.h new file mode 120000 index 0000000000..a5a38da7dc --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFNetworking.h @@ -0,0 +1 @@ +../../../AFNetworking/AFNetworking/AFNetworking.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFSecurityPolicy.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFSecurityPolicy.h new file mode 120000 index 0000000000..fd1322db9c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFSecurityPolicy.h @@ -0,0 +1 @@ +../../../AFNetworking/AFNetworking/AFSecurityPolicy.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFURLRequestSerialization.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFURLRequestSerialization.h new file mode 120000 index 0000000000..ca8209b81f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFURLRequestSerialization.h @@ -0,0 +1 @@ +../../../AFNetworking/AFNetworking/AFURLRequestSerialization.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFURLResponseSerialization.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFURLResponseSerialization.h new file mode 120000 index 0000000000..e36a765d82 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFURLResponseSerialization.h @@ -0,0 +1 @@ +../../../AFNetworking/AFNetworking/AFURLResponseSerialization.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFURLSessionManager.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFURLSessionManager.h new file mode 120000 index 0000000000..835101de7b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/AFURLSessionManager.h @@ -0,0 +1 @@ +../../../AFNetworking/AFNetworking/AFURLSessionManager.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/UIActivityIndicatorView+AFNetworking.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/UIActivityIndicatorView+AFNetworking.h new file mode 120000 index 0000000000..c534ebfb02 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/UIActivityIndicatorView+AFNetworking.h @@ -0,0 +1 @@ +../../../AFNetworking/UIKit+AFNetworking/UIActivityIndicatorView+AFNetworking.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/UIButton+AFNetworking.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/UIButton+AFNetworking.h new file mode 120000 index 0000000000..8f2e221939 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/UIButton+AFNetworking.h @@ -0,0 +1 @@ +../../../AFNetworking/UIKit+AFNetworking/UIButton+AFNetworking.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/UIImage+AFNetworking.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/UIImage+AFNetworking.h new file mode 120000 index 0000000000..74f6649909 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/UIImage+AFNetworking.h @@ -0,0 +1 @@ +../../../AFNetworking/UIKit+AFNetworking/UIImage+AFNetworking.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/UIImageView+AFNetworking.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/UIImageView+AFNetworking.h new file mode 120000 index 0000000000..a95d67380f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/UIImageView+AFNetworking.h @@ -0,0 +1 @@ +../../../AFNetworking/UIKit+AFNetworking/UIImageView+AFNetworking.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/UIKit+AFNetworking.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/UIKit+AFNetworking.h new file mode 120000 index 0000000000..95017cce57 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/UIKit+AFNetworking.h @@ -0,0 +1 @@ +../../../AFNetworking/UIKit+AFNetworking/UIKit+AFNetworking.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/UIProgressView+AFNetworking.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/UIProgressView+AFNetworking.h new file mode 120000 index 0000000000..730b167dcd --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/UIProgressView+AFNetworking.h @@ -0,0 +1 @@ +../../../AFNetworking/UIKit+AFNetworking/UIProgressView+AFNetworking.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/UIRefreshControl+AFNetworking.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/UIRefreshControl+AFNetworking.h new file mode 120000 index 0000000000..8efd826209 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/UIRefreshControl+AFNetworking.h @@ -0,0 +1 @@ +../../../AFNetworking/UIKit+AFNetworking/UIRefreshControl+AFNetworking.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/UIWebView+AFNetworking.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/UIWebView+AFNetworking.h new file mode 120000 index 0000000000..c8df6ef17b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Headers/Public/AFNetworking/UIWebView+AFNetworking.h @@ -0,0 +1 @@ +../../../AFNetworking/UIKit+AFNetworking/UIWebView+AFNetworking.h \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Manifest.lock b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Manifest.lock new file mode 100644 index 0000000000..33679d72ff --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Manifest.lock @@ -0,0 +1,30 @@ +PODS: + - AFNetworking (3.0.4): + - AFNetworking/NSURLSession (= 3.0.4) + - AFNetworking/Reachability (= 3.0.4) + - AFNetworking/Security (= 3.0.4) + - AFNetworking/Serialization (= 3.0.4) + - AFNetworking/UIKit (= 3.0.4) + - AFNetworking/NSURLSession (3.0.4): + - AFNetworking/Reachability + - AFNetworking/Security + - AFNetworking/Serialization + - AFNetworking/Reachability (3.0.4) + - AFNetworking/Security (3.0.4) + - AFNetworking/Serialization (3.0.4) + - AFNetworking/UIKit (3.0.4): + - AFNetworking/NSURLSession + +DEPENDENCIES: + - AFNetworking (~> 3.0) + +SPEC REPOS: + https://github.com/cocoapods/specs.git: + - AFNetworking + +SPEC CHECKSUMS: + AFNetworking: a0075feb321559dc78d9d85b55d11caa19eabb93 + +PODFILE CHECKSUM: 8d0eff399cf1d98e2ff7220113aed7a0d13ff37d + +COCOAPODS: 1.6.0.beta.2 diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Pods.xcodeproj/project.pbxproj b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Pods.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..d8e997e8f5 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Pods.xcodeproj/project.pbxproj @@ -0,0 +1,1353 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 03826B8703E219C11E4C2DAB9A634325 /* UIImage+AFNetworking.h in Headers */ = {isa = PBXBuildFile; fileRef = 7118A16B4CE6C2F5425CDEEE77040A9A /* UIImage+AFNetworking.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 0A5C58997AAC65BF6B09039F0DF6F9FC /* UIWebView+AFNetworking.h in Headers */ = {isa = PBXBuildFile; fileRef = D7E209D170F390B80C67336D1D58526E /* UIWebView+AFNetworking.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 0A90CEAFD09D78174FC763C95E302179 /* AFURLRequestSerialization.m in Sources */ = {isa = PBXBuildFile; fileRef = 82DF63A147D90320B4812AFB422015AA /* AFURLRequestSerialization.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 1097BF248FADB4F17D799B52916E6FDB /* AFURLRequestSerialization.h in Headers */ = {isa = PBXBuildFile; fileRef = A3B7FFADD03754D2228E8993F0F0CF2A /* AFURLRequestSerialization.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 116B684420898BB169C74737B17C20A2 /* UIWebView+AFNetworking.h in Headers */ = {isa = PBXBuildFile; fileRef = D7E209D170F390B80C67336D1D58526E /* UIWebView+AFNetworking.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 141CEED22EC7683E8EBCA3FA9A98427A /* UIRefreshControl+AFNetworking.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D3DAE5E44B0768704E76DA7829CEA05 /* UIRefreshControl+AFNetworking.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 1B310C2202C5838DE17FD736B4C99FF1 /* UIRefreshControl+AFNetworking.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D3DAE5E44B0768704E76DA7829CEA05 /* UIRefreshControl+AFNetworking.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 23DC83D398DC88B656CB82A621197CD4 /* UIProgressView+AFNetworking.m in Sources */ = {isa = PBXBuildFile; fileRef = 067E6D26AAF8113D673714F5797BE715 /* UIProgressView+AFNetworking.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 2566BAE1CFDA5654F2FDA6BDFF2D80DB /* AFNetworking.h in Headers */ = {isa = PBXBuildFile; fileRef = A39FEEABC1551F49F0B0FE21D2AD9D6C /* AFNetworking.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 2F86B19B954FC0CBA21DB7BB3075B977 /* AFNetworking-c94d3492-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 284C26DCEC89225FE257E8AF6557BAED /* AFNetworking-c94d3492-dummy.m */; }; + 31E0BFAE0DE4C1C58E9EFD93DC054597 /* AFURLSessionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = AB97A15AA490770939273DBC0069730A /* AFURLSessionManager.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 32A0D7DB742D61FEF014C54D48960857 /* AFImageDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = D1ACB67B32DAEB8AD59AA058818CBD97 /* AFImageDownloader.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 32EC063C076486D1E539337E3590A00D /* AFNetworking.h in Headers */ = {isa = PBXBuildFile; fileRef = A39FEEABC1551F49F0B0FE21D2AD9D6C /* AFNetworking.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 32F75AFB06C5BB3D64C7E504D5DABC07 /* AFNetworkReachabilityManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 5AC90BD8F86F957D3DC47D89B2BB040B /* AFNetworkReachabilityManager.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 36994F1813AD8E417C23A6544D30A357 /* UIImageView+AFNetworking.h in Headers */ = {isa = PBXBuildFile; fileRef = 067029DEE90E57DFFB5748B732462B45 /* UIImageView+AFNetworking.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 384FC592DDCAAB5C04CB8A1B3FE2D22E /* UIKit+AFNetworking.h in Headers */ = {isa = PBXBuildFile; fileRef = D8FA05DE98BEC9B0C224676B3C39B74A /* UIKit+AFNetworking.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 3961DC887BC7E7D2862D38BE20072553 /* AFNetworking.h in Headers */ = {isa = PBXBuildFile; fileRef = A39FEEABC1551F49F0B0FE21D2AD9D6C /* AFNetworking.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 3BF6D0D7ABF591F0C2A3D0ED8255FA15 /* AFURLResponseSerialization.m in Sources */ = {isa = PBXBuildFile; fileRef = C0B108B686488E21A171DAA4D18AD881 /* AFURLResponseSerialization.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 419794E37141ECCF8D65524851BBF4B0 /* AFURLResponseSerialization.m in Sources */ = {isa = PBXBuildFile; fileRef = C0B108B686488E21A171DAA4D18AD881 /* AFURLResponseSerialization.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 4221D8805C6A539DB6910646BEED3695 /* AFURLRequestSerialization.m in Sources */ = {isa = PBXBuildFile; fileRef = 82DF63A147D90320B4812AFB422015AA /* AFURLRequestSerialization.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 44194FA22CBDE3FB910E7B67D70E2000 /* AFAutoPurgingImageCache.h in Headers */ = {isa = PBXBuildFile; fileRef = CF9CF6605A13B92B6E686264E06C3CF6 /* AFAutoPurgingImageCache.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 45DAC3ED54E9D38488DA4B62162F98AF /* AFImageDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = D1ACB67B32DAEB8AD59AA058818CBD97 /* AFImageDownloader.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 49ECB3B7AB3028A11367187119BC8BB7 /* AFNetworkActivityIndicatorManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 523760D236AEACFBD199B6042A6D92D4 /* AFNetworkActivityIndicatorManager.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 4FD29B68A3B5F7B8FF60AA198DD25790 /* AFNetworking-iOS-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 84C2852D436E477D989044D9F70FD4CF /* AFNetworking-iOS-dummy.m */; }; + 507F784759A259B26712FA7588DAB168 /* UIButton+AFNetworking.h in Headers */ = {isa = PBXBuildFile; fileRef = F2BCCE7C59CC730D5599FD2277D210CE /* UIButton+AFNetworking.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 522E8E8D06D2046D0CD0DB65C09DDD26 /* UIImageView+AFNetworking.h in Headers */ = {isa = PBXBuildFile; fileRef = 067029DEE90E57DFFB5748B732462B45 /* UIImageView+AFNetworking.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 5420902A4AB0BD57FF81A99B147C7C99 /* AFSecurityPolicy.h in Headers */ = {isa = PBXBuildFile; fileRef = A4EA2BE1C35EB50F0B89D8423EB6F7B7 /* AFSecurityPolicy.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 55026A7B694A5F2FA53A2A75C97DF673 /* AFURLResponseSerialization.h in Headers */ = {isa = PBXBuildFile; fileRef = F3EC32E0205FA2F4314674D854DCC8B6 /* AFURLResponseSerialization.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 55663564E902A5510FC3881DA1FBF67E /* AFURLRequestSerialization.m in Sources */ = {isa = PBXBuildFile; fileRef = 82DF63A147D90320B4812AFB422015AA /* AFURLRequestSerialization.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 557288FE943D212DA96D05AA9D9BBBC1 /* UIWebView+AFNetworking.m in Sources */ = {isa = PBXBuildFile; fileRef = 82F53B7CEF2CEEC442FCE43999EC54ED /* UIWebView+AFNetworking.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 56D369D51173A5314079D3D8C9EE569B /* AFNetworkReachabilityManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 5AC90BD8F86F957D3DC47D89B2BB040B /* AFNetworkReachabilityManager.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 5B9099D9A7BB21987AA44C6F88422575 /* UIButton+AFNetworking.m in Sources */ = {isa = PBXBuildFile; fileRef = 5AA57189482E366F3DDEB76A906DAC8B /* UIButton+AFNetworking.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 5CEC3FD0B4DBBA6995C6477672C096FB /* UIProgressView+AFNetworking.h in Headers */ = {isa = PBXBuildFile; fileRef = B9DEFEF6F25E74F9117C91B173D22A96 /* UIProgressView+AFNetworking.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 5FC64ACBF1ED123C69507A9176E0F134 /* AFURLSessionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = AB97A15AA490770939273DBC0069730A /* AFURLSessionManager.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 5FDCA4EEF5F7E9DEC06741EDC7C91923 /* AFNetworkReachabilityManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 8CC1FF5E4D9879068E2CA6DD5C9FDAD1 /* AFNetworkReachabilityManager.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 61443F016D0C1732F73913538E4E7908 /* AFSecurityPolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F0339F512FBBF8E15BCFDFAC7119ACD /* AFSecurityPolicy.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 643C4AB032AB560491B42F8FB0297EC4 /* AFNetworkReachabilityManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 8CC1FF5E4D9879068E2CA6DD5C9FDAD1 /* AFNetworkReachabilityManager.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 6A4F2D9505BF8DF249C7AFB1D479944F /* UIActivityIndicatorView+AFNetworking.h in Headers */ = {isa = PBXBuildFile; fileRef = EDCEAD97A5F72670715036A6AB785B92 /* UIActivityIndicatorView+AFNetworking.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 6C54A1A08A4CCCD443E90EB8D3DF768C /* AFNetworking-tvOS-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = ED29F21403BDB1E5DD379E272051A69E /* AFNetworking-tvOS-dummy.m */; }; + 73AB5781DC7940FB178273DEFBCB0033 /* AFURLResponseSerialization.h in Headers */ = {isa = PBXBuildFile; fileRef = F3EC32E0205FA2F4314674D854DCC8B6 /* AFURLResponseSerialization.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 7B84F986E685989A630FA5280DD03C70 /* UIWebView+AFNetworking.m in Sources */ = {isa = PBXBuildFile; fileRef = 82F53B7CEF2CEEC442FCE43999EC54ED /* UIWebView+AFNetworking.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 7E22C3FBCD4BC396FF1C43FBF301C6D8 /* AFNetworkActivityIndicatorManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 8E520EE78F252B3D97595A45BD13A540 /* AFNetworkActivityIndicatorManager.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 7F514C121F982BAC1BDA0F422BC5B971 /* UIButton+AFNetworking.h in Headers */ = {isa = PBXBuildFile; fileRef = F2BCCE7C59CC730D5599FD2277D210CE /* UIButton+AFNetworking.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 8AE476DAE4A8371DB6457ED40A842DFA /* AFImageDownloader.h in Headers */ = {isa = PBXBuildFile; fileRef = 490EC63B38541DE3F08700C6B42BA8A1 /* AFImageDownloader.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 8B0682AA90BB73680ED459DE5D093CF6 /* AFURLRequestSerialization.h in Headers */ = {isa = PBXBuildFile; fileRef = A3B7FFADD03754D2228E8993F0F0CF2A /* AFURLRequestSerialization.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 8C176A815B409BC36CE64A5807FA1075 /* AFHTTPSessionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B7251B546B1E78F8558F39EABEA675B7 /* AFHTTPSessionManager.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 907268E34649AB2AAB074BCA44C8000F /* AFAutoPurgingImageCache.m in Sources */ = {isa = PBXBuildFile; fileRef = A25308E5B3B2A8F72FAA398BA1C009A6 /* AFAutoPurgingImageCache.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 9208B4637E11A54EB5544487CA571CB3 /* UIActivityIndicatorView+AFNetworking.h in Headers */ = {isa = PBXBuildFile; fileRef = EDCEAD97A5F72670715036A6AB785B92 /* UIActivityIndicatorView+AFNetworking.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 9344A6A89825F6253753BD008FF0548A /* AFImageDownloader.h in Headers */ = {isa = PBXBuildFile; fileRef = 490EC63B38541DE3F08700C6B42BA8A1 /* AFImageDownloader.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 946EFF93146FA52AFD42520697349936 /* Pods-TestingPods-OHHTTPStubs Mac Tests-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 2802EA0AF2A76C463F3C503A0FE4B8A1 /* Pods-TestingPods-OHHTTPStubs Mac Tests-dummy.m */; }; + 9591A2AD0135BE09CA49199BE0DF89A3 /* AFHTTPSessionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B7251B546B1E78F8558F39EABEA675B7 /* AFHTTPSessionManager.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 97926D543BBAAFE8E14CC544E31F4BD0 /* Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 7CB1CB5970F98C80FF4808D6C6E1BF57 /* Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-dummy.m */; }; + 979BD766D2CB1552B4C87C5D24C5E483 /* AFURLRequestSerialization.h in Headers */ = {isa = PBXBuildFile; fileRef = A3B7FFADD03754D2228E8993F0F0CF2A /* AFURLRequestSerialization.h */; settings = {ATTRIBUTES = (Project, ); }; }; + 9CE73AE2C7D0BB9764D2BC98B5B405F0 /* UIImageView+AFNetworking.m in Sources */ = {isa = PBXBuildFile; fileRef = 7691028C093F81E5766AD53ED2F6E0B8 /* UIImageView+AFNetworking.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + A05A453F76B4CB397C463F65ADC5A597 /* UIProgressView+AFNetworking.h in Headers */ = {isa = PBXBuildFile; fileRef = B9DEFEF6F25E74F9117C91B173D22A96 /* UIProgressView+AFNetworking.h */; settings = {ATTRIBUTES = (Project, ); }; }; + A5BC0E662634EB2667EBB67F9AA855F7 /* AFHTTPSessionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B7251B546B1E78F8558F39EABEA675B7 /* AFHTTPSessionManager.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + A91CA6F5355347DFCEA29231DED2D805 /* AFSecurityPolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F0339F512FBBF8E15BCFDFAC7119ACD /* AFSecurityPolicy.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + AFBE38BD371CDEC001DDD749A136F5E9 /* AFHTTPSessionManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 1D2CA139ADDD2E9918936FDF6B7650C6 /* AFHTTPSessionManager.h */; settings = {ATTRIBUTES = (Project, ); }; }; + B1D14C1DBFF8048DFA8593DB11FCFF44 /* Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = DFA732EC710C331C88DDC0E923D582A5 /* Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-dummy.m */; }; + B382A7EE4B5A34A04BF7EF1E77096F46 /* AFHTTPSessionManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 1D2CA139ADDD2E9918936FDF6B7650C6 /* AFHTTPSessionManager.h */; settings = {ATTRIBUTES = (Project, ); }; }; + C3DEC73556C1E9825D3EEEC4EF3222D0 /* AFSecurityPolicy.h in Headers */ = {isa = PBXBuildFile; fileRef = A4EA2BE1C35EB50F0B89D8423EB6F7B7 /* AFSecurityPolicy.h */; settings = {ATTRIBUTES = (Project, ); }; }; + C95B654D0777E3F59E627DB8154FBF64 /* AFNetworkActivityIndicatorManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 523760D236AEACFBD199B6042A6D92D4 /* AFNetworkActivityIndicatorManager.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + CA05A3A9B350442758AA8113DC21C005 /* AFURLSessionManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 4622E5A19C9D5A8DDBED0433C745C5C9 /* AFURLSessionManager.h */; settings = {ATTRIBUTES = (Project, ); }; }; + D2291731C1F2969AC93B01DD98613A66 /* AFNetworkReachabilityManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 5AC90BD8F86F957D3DC47D89B2BB040B /* AFNetworkReachabilityManager.h */; settings = {ATTRIBUTES = (Project, ); }; }; + D61AF81D29FD5A1B3D1BAE2AC75D880C /* AFURLSessionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = AB97A15AA490770939273DBC0069730A /* AFURLSessionManager.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + D7C129AE47D408B85B5AC1AFEEB29A9A /* UIRefreshControl+AFNetworking.h in Headers */ = {isa = PBXBuildFile; fileRef = 0121AF19B4DEE0ED795452A716E6D45F /* UIRefreshControl+AFNetworking.h */; settings = {ATTRIBUTES = (Project, ); }; }; + D8ACF9ED0002D52D205E1119E7DBD5D9 /* UIProgressView+AFNetworking.m in Sources */ = {isa = PBXBuildFile; fileRef = 067E6D26AAF8113D673714F5797BE715 /* UIProgressView+AFNetworking.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + D9425DE952F0E042DA9C10785C6A9B7C /* UIActivityIndicatorView+AFNetworking.m in Sources */ = {isa = PBXBuildFile; fileRef = 419A7B2C9EBA571FBEBE2914F2246A01 /* UIActivityIndicatorView+AFNetworking.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + DB57CCE4CF63C252CE14C817DE15AACD /* AFNetworkActivityIndicatorManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 8E520EE78F252B3D97595A45BD13A540 /* AFNetworkActivityIndicatorManager.h */; settings = {ATTRIBUTES = (Project, ); }; }; + DBED2A43013D393E98DB8B2A97045DC4 /* AFURLResponseSerialization.h in Headers */ = {isa = PBXBuildFile; fileRef = F3EC32E0205FA2F4314674D854DCC8B6 /* AFURLResponseSerialization.h */; settings = {ATTRIBUTES = (Project, ); }; }; + DC1BD3654EAAD82B3E9F44B808026E81 /* UIImage+AFNetworking.h in Headers */ = {isa = PBXBuildFile; fileRef = 7118A16B4CE6C2F5425CDEEE77040A9A /* UIImage+AFNetworking.h */; settings = {ATTRIBUTES = (Project, ); }; }; + E0E06D042DA0183FA128B3698E5E006F /* UIButton+AFNetworking.m in Sources */ = {isa = PBXBuildFile; fileRef = 5AA57189482E366F3DDEB76A906DAC8B /* UIButton+AFNetworking.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + E24C16DF4A790AA901FC78D88AD356FC /* AFSecurityPolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F0339F512FBBF8E15BCFDFAC7119ACD /* AFSecurityPolicy.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + E3771BDC96E71EC693D8E0077F1D91AC /* AFURLResponseSerialization.m in Sources */ = {isa = PBXBuildFile; fileRef = C0B108B686488E21A171DAA4D18AD881 /* AFURLResponseSerialization.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + E6AB215237138C2A62B8D6DEA6524BBF /* UIKit+AFNetworking.h in Headers */ = {isa = PBXBuildFile; fileRef = D8FA05DE98BEC9B0C224676B3C39B74A /* UIKit+AFNetworking.h */; settings = {ATTRIBUTES = (Project, ); }; }; + E918FB9D9BEEF76125067E8CC98D963D /* UIActivityIndicatorView+AFNetworking.m in Sources */ = {isa = PBXBuildFile; fileRef = 419A7B2C9EBA571FBEBE2914F2246A01 /* UIActivityIndicatorView+AFNetworking.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + EAABE565D6D62D27D232B94F4F86CF2F /* AFAutoPurgingImageCache.m in Sources */ = {isa = PBXBuildFile; fileRef = A25308E5B3B2A8F72FAA398BA1C009A6 /* AFAutoPurgingImageCache.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + EADE64D3244517944416C363DA822B18 /* AFURLSessionManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 4622E5A19C9D5A8DDBED0433C745C5C9 /* AFURLSessionManager.h */; settings = {ATTRIBUTES = (Project, ); }; }; + EB6CED28E29434212736C804F1F63677 /* AFHTTPSessionManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 1D2CA139ADDD2E9918936FDF6B7650C6 /* AFHTTPSessionManager.h */; settings = {ATTRIBUTES = (Project, ); }; }; + EBA21C2504105E270C349BAF94C81CF6 /* UIImageView+AFNetworking.m in Sources */ = {isa = PBXBuildFile; fileRef = 7691028C093F81E5766AD53ED2F6E0B8 /* UIImageView+AFNetworking.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + F1EB67B1A9C1A9458E65D2BBA3CF356E /* AFNetworkReachabilityManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 8CC1FF5E4D9879068E2CA6DD5C9FDAD1 /* AFNetworkReachabilityManager.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + F850FC543153A64DFE92955ECDB4F337 /* Pods-TestingPods-OHHTTPStubs iOS Lib Tests-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = B13F476E703C08E81870243C9FFE7752 /* Pods-TestingPods-OHHTTPStubs iOS Lib Tests-dummy.m */; }; + FC53F62EFC6A879D99DE2F22CE3388CD /* UIRefreshControl+AFNetworking.h in Headers */ = {isa = PBXBuildFile; fileRef = 0121AF19B4DEE0ED795452A716E6D45F /* UIRefreshControl+AFNetworking.h */; settings = {ATTRIBUTES = (Project, ); }; }; + FDB93829F88A10DD4F52FA5D174DD216 /* AFAutoPurgingImageCache.h in Headers */ = {isa = PBXBuildFile; fileRef = CF9CF6605A13B92B6E686264E06C3CF6 /* AFAutoPurgingImageCache.h */; settings = {ATTRIBUTES = (Project, ); }; }; + FEC5848363B04369FA95F167C751C820 /* AFURLSessionManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 4622E5A19C9D5A8DDBED0433C745C5C9 /* AFURLSessionManager.h */; settings = {ATTRIBUTES = (Project, ); }; }; + FFB6618C19D61DBE626F365FD27153E9 /* AFSecurityPolicy.h in Headers */ = {isa = PBXBuildFile; fileRef = A4EA2BE1C35EB50F0B89D8423EB6F7B7 /* AFSecurityPolicy.h */; settings = {ATTRIBUTES = (Project, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 3E8F186EC3374BF269EF3836BA0B646E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D41D8CD98F00B204E9800998ECF8427E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 294529C77198ECB248E415B29FDC9441; + remoteInfo = "AFNetworking-iOS"; + }; + 512D17CE7818DBB43842FC8176228B38 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D41D8CD98F00B204E9800998ECF8427E /* Project object */; + proxyType = 1; + remoteGlobalIDString = D70B3FB3184A448A3E88112CBF71A2AA; + remoteInfo = "AFNetworking-tvOS"; + }; + A498827688C4618D5E134F7CA98F0A02 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D41D8CD98F00B204E9800998ECF8427E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 294529C77198ECB248E415B29FDC9441; + remoteInfo = "AFNetworking-iOS"; + }; + F85EB9E30D7904CCA7FF65EE3B498A01 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D41D8CD98F00B204E9800998ECF8427E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 163E987AE2E029D9EB0B15C75EDE7869; + remoteInfo = "AFNetworking-c94d3492"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 0121AF19B4DEE0ED795452A716E6D45F /* UIRefreshControl+AFNetworking.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIRefreshControl+AFNetworking.h"; path = "UIKit+AFNetworking/UIRefreshControl+AFNetworking.h"; sourceTree = ""; }; + 050C64A16F4965CDA1CDB676C9163041 /* AFNetworking-c94d3492-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "AFNetworking-c94d3492-prefix.pch"; sourceTree = ""; }; + 067029DEE90E57DFFB5748B732462B45 /* UIImageView+AFNetworking.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIImageView+AFNetworking.h"; path = "UIKit+AFNetworking/UIImageView+AFNetworking.h"; sourceTree = ""; }; + 067E6D26AAF8113D673714F5797BE715 /* UIProgressView+AFNetworking.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIProgressView+AFNetworking.m"; path = "UIKit+AFNetworking/UIProgressView+AFNetworking.m"; sourceTree = ""; }; + 07573BF6F4B141A9D9FE32643F3BCDA6 /* libPods-TestingPods-OHHTTPStubs Mac Tests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-TestingPods-OHHTTPStubs Mac Tests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 0B4EA1A2506057DEBCB41E0A519B8F4B /* libAFNetworking-c94d3492.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libAFNetworking-c94d3492.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 1D2CA139ADDD2E9918936FDF6B7650C6 /* AFHTTPSessionManager.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = AFHTTPSessionManager.h; path = AFNetworking/AFHTTPSessionManager.h; sourceTree = ""; }; + 224B56DD647BF513E9D8F34F6A809DF6 /* Pods-TestingPods-OHHTTPStubs iOS Fmk Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-TestingPods-OHHTTPStubs iOS Fmk Tests.debug.xcconfig"; sourceTree = ""; }; + 225EA8457A11BA3434034790BCDC374E /* Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests.release.xcconfig"; sourceTree = ""; }; + 2802EA0AF2A76C463F3C503A0FE4B8A1 /* Pods-TestingPods-OHHTTPStubs Mac Tests-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-TestingPods-OHHTTPStubs Mac Tests-dummy.m"; sourceTree = ""; }; + 284C26DCEC89225FE257E8AF6557BAED /* AFNetworking-c94d3492-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "AFNetworking-c94d3492-dummy.m"; sourceTree = ""; }; + 375ECC43A9266AB5C20E91070910BE33 /* Pods-TestingPods-OHHTTPStubs iOS Fmk Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-TestingPods-OHHTTPStubs iOS Fmk Tests.release.xcconfig"; sourceTree = ""; }; + 3D3DAE5E44B0768704E76DA7829CEA05 /* UIRefreshControl+AFNetworking.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIRefreshControl+AFNetworking.m"; path = "UIKit+AFNetworking/UIRefreshControl+AFNetworking.m"; sourceTree = ""; }; + 419A7B2C9EBA571FBEBE2914F2246A01 /* UIActivityIndicatorView+AFNetworking.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIActivityIndicatorView+AFNetworking.m"; path = "UIKit+AFNetworking/UIActivityIndicatorView+AFNetworking.m"; sourceTree = ""; }; + 45A396A85D76FDCBD6D52CF077E055B3 /* Pods-TestingPods-OHHTTPStubs iOS Lib Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-TestingPods-OHHTTPStubs iOS Lib Tests.debug.xcconfig"; sourceTree = ""; }; + 4622E5A19C9D5A8DDBED0433C745C5C9 /* AFURLSessionManager.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = AFURLSessionManager.h; path = AFNetworking/AFURLSessionManager.h; sourceTree = ""; }; + 490EC63B38541DE3F08700C6B42BA8A1 /* AFImageDownloader.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = AFImageDownloader.h; path = "UIKit+AFNetworking/AFImageDownloader.h"; sourceTree = ""; }; + 493EC68E4539B78F01F0D9C3B778DD99 /* Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-acknowledgements.markdown"; sourceTree = ""; }; + 5154A04BF6B939CC5C91B26BC3B235C6 /* Pods-TestingPods-OHHTTPStubs iOS Lib Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-TestingPods-OHHTTPStubs iOS Lib Tests.release.xcconfig"; sourceTree = ""; }; + 523760D236AEACFBD199B6042A6D92D4 /* AFNetworkActivityIndicatorManager.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = AFNetworkActivityIndicatorManager.m; path = "UIKit+AFNetworking/AFNetworkActivityIndicatorManager.m"; sourceTree = ""; }; + 5700278C2E8921B744D90A32E4B2E33E /* AFNetworking-tvOS.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "AFNetworking-tvOS.xcconfig"; path = "../AFNetworking-tvOS/AFNetworking-tvOS.xcconfig"; sourceTree = ""; }; + 57B933FBD9EB44273FE9CF231ED93FE5 /* Pods-TestingPods-OHHTTPStubs Mac Tests-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-TestingPods-OHHTTPStubs Mac Tests-acknowledgements.plist"; sourceTree = ""; }; + 5AA57189482E366F3DDEB76A906DAC8B /* UIButton+AFNetworking.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIButton+AFNetworking.m"; path = "UIKit+AFNetworking/UIButton+AFNetworking.m"; sourceTree = ""; }; + 5AC90BD8F86F957D3DC47D89B2BB040B /* AFNetworkReachabilityManager.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = AFNetworkReachabilityManager.h; path = AFNetworking/AFNetworkReachabilityManager.h; sourceTree = ""; }; + 64D97EE63038FA87063A3BE46DA25121 /* Pods-TestingPods-OHHTTPStubs Mac Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-TestingPods-OHHTTPStubs Mac Tests.debug.xcconfig"; sourceTree = ""; }; + 67E13C37EDB0E2B2CCA2C622D66E8385 /* Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-acknowledgements.plist"; sourceTree = ""; }; + 6A41CBBC25FDD16D2765EEC7598B9DEB /* Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-acknowledgements.plist"; sourceTree = ""; }; + 6FBA92CB9D96563B602A87AAFCFCAD7C /* AFNetworking-c94d3492.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "AFNetworking-c94d3492.xcconfig"; sourceTree = ""; }; + 7118A16B4CE6C2F5425CDEEE77040A9A /* UIImage+AFNetworking.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIImage+AFNetworking.h"; path = "UIKit+AFNetworking/UIImage+AFNetworking.h"; sourceTree = ""; }; + 7691028C093F81E5766AD53ED2F6E0B8 /* UIImageView+AFNetworking.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIImageView+AFNetworking.m"; path = "UIKit+AFNetworking/UIImageView+AFNetworking.m"; sourceTree = ""; }; + 795C1F36B6AA25F784C83826F0CD6062 /* libAFNetworking-iOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libAFNetworking-iOS.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 7CB1CB5970F98C80FF4808D6C6E1BF57 /* Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-dummy.m"; sourceTree = ""; }; + 82DF63A147D90320B4812AFB422015AA /* AFURLRequestSerialization.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = AFURLRequestSerialization.m; path = AFNetworking/AFURLRequestSerialization.m; sourceTree = ""; }; + 82F53B7CEF2CEEC442FCE43999EC54ED /* UIWebView+AFNetworking.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIWebView+AFNetworking.m"; path = "UIKit+AFNetworking/UIWebView+AFNetworking.m"; sourceTree = ""; }; + 84C2852D436E477D989044D9F70FD4CF /* AFNetworking-iOS-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "AFNetworking-iOS-dummy.m"; path = "../AFNetworking-iOS/AFNetworking-iOS-dummy.m"; sourceTree = ""; }; + 86C0DCC2C56D6DBE078F6EB0D0F5ADA8 /* Pods-TestingPods-OHHTTPStubs iOS Lib Tests-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-TestingPods-OHHTTPStubs iOS Lib Tests-acknowledgements.markdown"; sourceTree = ""; }; + 8CC1FF5E4D9879068E2CA6DD5C9FDAD1 /* AFNetworkReachabilityManager.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = AFNetworkReachabilityManager.m; path = AFNetworking/AFNetworkReachabilityManager.m; sourceTree = ""; }; + 8E520EE78F252B3D97595A45BD13A540 /* AFNetworkActivityIndicatorManager.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = AFNetworkActivityIndicatorManager.h; path = "UIKit+AFNetworking/AFNetworkActivityIndicatorManager.h"; sourceTree = ""; }; + 8F0339F512FBBF8E15BCFDFAC7119ACD /* AFSecurityPolicy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = AFSecurityPolicy.m; path = AFNetworking/AFSecurityPolicy.m; sourceTree = ""; }; + 927D82381B7AF1B27D8797DBA9F40E1A /* libPods-TestingPods-OHHTTPStubs tvOS Fmk Tests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-TestingPods-OHHTTPStubs tvOS Fmk Tests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 98F9A5A8A5CBAEFBC64CC66328EC4E99 /* Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-acknowledgements.markdown"; sourceTree = ""; }; + 98FF5E774D2798BE804928E49ABF28ED /* Pods-TestingPods-OHHTTPStubs iOS Lib Tests-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-TestingPods-OHHTTPStubs iOS Lib Tests-acknowledgements.plist"; sourceTree = ""; }; + 9C0FDF8B2D2F3C5D0A765353B2CBCD33 /* AFNetworking-iOS-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "AFNetworking-iOS-prefix.pch"; path = "../AFNetworking-iOS/AFNetworking-iOS-prefix.pch"; sourceTree = ""; }; + A1133EF1E8EBE4F5AEB6EEFEE986A103 /* AFNetworking-tvOS-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "AFNetworking-tvOS-prefix.pch"; path = "../AFNetworking-tvOS/AFNetworking-tvOS-prefix.pch"; sourceTree = ""; }; + A25308E5B3B2A8F72FAA398BA1C009A6 /* AFAutoPurgingImageCache.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = AFAutoPurgingImageCache.m; path = "UIKit+AFNetworking/AFAutoPurgingImageCache.m"; sourceTree = ""; }; + A39FEEABC1551F49F0B0FE21D2AD9D6C /* AFNetworking.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = AFNetworking.h; path = AFNetworking/AFNetworking.h; sourceTree = ""; }; + A3B7FFADD03754D2228E8993F0F0CF2A /* AFURLRequestSerialization.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = AFURLRequestSerialization.h; path = AFNetworking/AFURLRequestSerialization.h; sourceTree = ""; }; + A4EA2BE1C35EB50F0B89D8423EB6F7B7 /* AFSecurityPolicy.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = AFSecurityPolicy.h; path = AFNetworking/AFSecurityPolicy.h; sourceTree = ""; }; + A7FFBE99241E2C30FB09E85564DA1D88 /* Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests.debug.xcconfig"; sourceTree = ""; }; + A9C1FF3C99BB9C97E4C549796C5A4098 /* libPods-TestingPods-OHHTTPStubs iOS Lib Tests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-TestingPods-OHHTTPStubs iOS Lib Tests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + AB97A15AA490770939273DBC0069730A /* AFURLSessionManager.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = AFURLSessionManager.m; path = AFNetworking/AFURLSessionManager.m; sourceTree = ""; }; + B13F476E703C08E81870243C9FFE7752 /* Pods-TestingPods-OHHTTPStubs iOS Lib Tests-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-TestingPods-OHHTTPStubs iOS Lib Tests-dummy.m"; sourceTree = ""; }; + B36AB396275697EEE1A5B13BA8A38FE3 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; name = Podfile; path = ../../../../System/Volumes/Data/Users/jeff/OHHTTPStubs/Podfile; sourceTree = ""; tabWidth = 2; }; + B7251B546B1E78F8558F39EABEA675B7 /* AFHTTPSessionManager.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = AFHTTPSessionManager.m; path = AFNetworking/AFHTTPSessionManager.m; sourceTree = ""; }; + B9DEFEF6F25E74F9117C91B173D22A96 /* UIProgressView+AFNetworking.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIProgressView+AFNetworking.h"; path = "UIKit+AFNetworking/UIProgressView+AFNetworking.h"; sourceTree = ""; }; + C0B108B686488E21A171DAA4D18AD881 /* AFURLResponseSerialization.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = AFURLResponseSerialization.m; path = AFNetworking/AFURLResponseSerialization.m; sourceTree = ""; }; + CF9CF6605A13B92B6E686264E06C3CF6 /* AFAutoPurgingImageCache.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = AFAutoPurgingImageCache.h; path = "UIKit+AFNetworking/AFAutoPurgingImageCache.h"; sourceTree = ""; }; + D1ACB67B32DAEB8AD59AA058818CBD97 /* AFImageDownloader.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = AFImageDownloader.m; path = "UIKit+AFNetworking/AFImageDownloader.m"; sourceTree = ""; }; + D2B487F97B81E3910E1CA2A32AB6FF96 /* AFNetworking-iOS.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "AFNetworking-iOS.xcconfig"; path = "../AFNetworking-iOS/AFNetworking-iOS.xcconfig"; sourceTree = ""; }; + D7E209D170F390B80C67336D1D58526E /* UIWebView+AFNetworking.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIWebView+AFNetworking.h"; path = "UIKit+AFNetworking/UIWebView+AFNetworking.h"; sourceTree = ""; }; + D8FA05DE98BEC9B0C224676B3C39B74A /* UIKit+AFNetworking.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIKit+AFNetworking.h"; path = "UIKit+AFNetworking/UIKit+AFNetworking.h"; sourceTree = ""; }; + DFA732EC710C331C88DDC0E923D582A5 /* Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-dummy.m"; sourceTree = ""; }; + E87B284887D29B72E6EA5F7E15004BD3 /* Pods-TestingPods-OHHTTPStubs Mac Tests-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-TestingPods-OHHTTPStubs Mac Tests-acknowledgements.markdown"; sourceTree = ""; }; + ED29F21403BDB1E5DD379E272051A69E /* AFNetworking-tvOS-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "AFNetworking-tvOS-dummy.m"; path = "../AFNetworking-tvOS/AFNetworking-tvOS-dummy.m"; sourceTree = ""; }; + EDCEAD97A5F72670715036A6AB785B92 /* UIActivityIndicatorView+AFNetworking.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIActivityIndicatorView+AFNetworking.h"; path = "UIKit+AFNetworking/UIActivityIndicatorView+AFNetworking.h"; sourceTree = ""; }; + F048257A3F3D2308F1C9ADADAD0C0111 /* libPods-TestingPods-OHHTTPStubs iOS Fmk Tests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-TestingPods-OHHTTPStubs iOS Fmk Tests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + F2BCCE7C59CC730D5599FD2277D210CE /* UIButton+AFNetworking.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIButton+AFNetworking.h"; path = "UIKit+AFNetworking/UIButton+AFNetworking.h"; sourceTree = ""; }; + F3EC32E0205FA2F4314674D854DCC8B6 /* AFURLResponseSerialization.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = AFURLResponseSerialization.h; path = AFNetworking/AFURLResponseSerialization.h; sourceTree = ""; }; + F75CD6E2E41BE668BB8D768FD76FB0EC /* Pods-TestingPods-OHHTTPStubs Mac Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-TestingPods-OHHTTPStubs Mac Tests.release.xcconfig"; sourceTree = ""; }; + F9376FC632354CA218B3C47EAB560527 /* libAFNetworking-tvOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libAFNetworking-tvOS.a"; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 17CBF30B5F9D88B2841609EE21624DDF /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7A6204F09A25143D89CC871BF538877D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8170D86D056338A72444D741DB242CBC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B3A7B8716F946A969B3C87B511DFBB06 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C750BCC5FA7C0105F40CD44E94A36BA7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D2ABD6FD334887DD00B0DF3AB54AB3E4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E1E635BA6B525F7669EE39D1D75F88B8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0F8D2E47FE03D3B91B51069F7C273AF4 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + 190D59C57DED819DD203147D959921A8 /* Targets Support Files */ = { + isa = PBXGroup; + children = ( + B637022DA2CF1998C39889882CF50F0D /* Pods-TestingPods-OHHTTPStubs iOS Fmk Tests */, + 990AF325A5C324B018C486BD357D4B27 /* Pods-TestingPods-OHHTTPStubs iOS Lib Tests */, + B1B370E089BE8D4D0D2A56AEB884B6A7 /* Pods-TestingPods-OHHTTPStubs Mac Tests */, + 444EA8F1F6807A713399FC7D2A923CC2 /* Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests */, + ); + name = "Targets Support Files"; + sourceTree = ""; + }; + 2E9AD032BEF6F7FF3B3CE4BC2394E430 /* AFNetworking */ = { + isa = PBXGroup; + children = ( + A39FEEABC1551F49F0B0FE21D2AD9D6C /* AFNetworking.h */, + 7C7B2E05BDA98C95A960867F196C3CEF /* NSURLSession */, + B7F3D444F4024BB920EEF9C30D0D5287 /* Reachability */, + 817AFBB9248C30D5E13CA3E7A6471316 /* Security */, + BC1A26AFC715C6CA26CB8777253D3FD7 /* Serialization */, + AF493CBE5C8523B8C3AEDC2A6E478E3D /* Support Files */, + 97810C007420D0BAA15125FF4BC689F5 /* UIKit */, + ); + path = AFNetworking; + sourceTree = ""; + }; + 444EA8F1F6807A713399FC7D2A923CC2 /* Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests */ = { + isa = PBXGroup; + children = ( + 493EC68E4539B78F01F0D9C3B778DD99 /* Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-acknowledgements.markdown */, + 67E13C37EDB0E2B2CCA2C622D66E8385 /* Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-acknowledgements.plist */, + 7CB1CB5970F98C80FF4808D6C6E1BF57 /* Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-dummy.m */, + A7FFBE99241E2C30FB09E85564DA1D88 /* Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests.debug.xcconfig */, + 225EA8457A11BA3434034790BCDC374E /* Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests.release.xcconfig */, + ); + name = "Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests"; + path = "Target Support Files/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests"; + sourceTree = ""; + }; + 641BDD347DE7B8BB81D6CA5727C6EC5A /* Pods */ = { + isa = PBXGroup; + children = ( + 2E9AD032BEF6F7FF3B3CE4BC2394E430 /* AFNetworking */, + ); + name = Pods; + sourceTree = ""; + }; + 7C7B2E05BDA98C95A960867F196C3CEF /* NSURLSession */ = { + isa = PBXGroup; + children = ( + 1D2CA139ADDD2E9918936FDF6B7650C6 /* AFHTTPSessionManager.h */, + B7251B546B1E78F8558F39EABEA675B7 /* AFHTTPSessionManager.m */, + 4622E5A19C9D5A8DDBED0433C745C5C9 /* AFURLSessionManager.h */, + AB97A15AA490770939273DBC0069730A /* AFURLSessionManager.m */, + ); + name = NSURLSession; + sourceTree = ""; + }; + 7DB346D0F39D3F0E887471402A8071AB = { + isa = PBXGroup; + children = ( + B36AB396275697EEE1A5B13BA8A38FE3 /* Podfile */, + 0F8D2E47FE03D3B91B51069F7C273AF4 /* Frameworks */, + 641BDD347DE7B8BB81D6CA5727C6EC5A /* Pods */, + E4F6EBFC997C5CDB484BAFF85AEAF253 /* Products */, + 190D59C57DED819DD203147D959921A8 /* Targets Support Files */, + ); + sourceTree = ""; + }; + 817AFBB9248C30D5E13CA3E7A6471316 /* Security */ = { + isa = PBXGroup; + children = ( + A4EA2BE1C35EB50F0B89D8423EB6F7B7 /* AFSecurityPolicy.h */, + 8F0339F512FBBF8E15BCFDFAC7119ACD /* AFSecurityPolicy.m */, + ); + name = Security; + sourceTree = ""; + }; + 97810C007420D0BAA15125FF4BC689F5 /* UIKit */ = { + isa = PBXGroup; + children = ( + CF9CF6605A13B92B6E686264E06C3CF6 /* AFAutoPurgingImageCache.h */, + A25308E5B3B2A8F72FAA398BA1C009A6 /* AFAutoPurgingImageCache.m */, + 490EC63B38541DE3F08700C6B42BA8A1 /* AFImageDownloader.h */, + D1ACB67B32DAEB8AD59AA058818CBD97 /* AFImageDownloader.m */, + 8E520EE78F252B3D97595A45BD13A540 /* AFNetworkActivityIndicatorManager.h */, + 523760D236AEACFBD199B6042A6D92D4 /* AFNetworkActivityIndicatorManager.m */, + EDCEAD97A5F72670715036A6AB785B92 /* UIActivityIndicatorView+AFNetworking.h */, + 419A7B2C9EBA571FBEBE2914F2246A01 /* UIActivityIndicatorView+AFNetworking.m */, + F2BCCE7C59CC730D5599FD2277D210CE /* UIButton+AFNetworking.h */, + 5AA57189482E366F3DDEB76A906DAC8B /* UIButton+AFNetworking.m */, + 7118A16B4CE6C2F5425CDEEE77040A9A /* UIImage+AFNetworking.h */, + 067029DEE90E57DFFB5748B732462B45 /* UIImageView+AFNetworking.h */, + 7691028C093F81E5766AD53ED2F6E0B8 /* UIImageView+AFNetworking.m */, + D8FA05DE98BEC9B0C224676B3C39B74A /* UIKit+AFNetworking.h */, + B9DEFEF6F25E74F9117C91B173D22A96 /* UIProgressView+AFNetworking.h */, + 067E6D26AAF8113D673714F5797BE715 /* UIProgressView+AFNetworking.m */, + 0121AF19B4DEE0ED795452A716E6D45F /* UIRefreshControl+AFNetworking.h */, + 3D3DAE5E44B0768704E76DA7829CEA05 /* UIRefreshControl+AFNetworking.m */, + D7E209D170F390B80C67336D1D58526E /* UIWebView+AFNetworking.h */, + 82F53B7CEF2CEEC442FCE43999EC54ED /* UIWebView+AFNetworking.m */, + ); + name = UIKit; + sourceTree = ""; + }; + 990AF325A5C324B018C486BD357D4B27 /* Pods-TestingPods-OHHTTPStubs iOS Lib Tests */ = { + isa = PBXGroup; + children = ( + 86C0DCC2C56D6DBE078F6EB0D0F5ADA8 /* Pods-TestingPods-OHHTTPStubs iOS Lib Tests-acknowledgements.markdown */, + 98FF5E774D2798BE804928E49ABF28ED /* Pods-TestingPods-OHHTTPStubs iOS Lib Tests-acknowledgements.plist */, + B13F476E703C08E81870243C9FFE7752 /* Pods-TestingPods-OHHTTPStubs iOS Lib Tests-dummy.m */, + 45A396A85D76FDCBD6D52CF077E055B3 /* Pods-TestingPods-OHHTTPStubs iOS Lib Tests.debug.xcconfig */, + 5154A04BF6B939CC5C91B26BC3B235C6 /* Pods-TestingPods-OHHTTPStubs iOS Lib Tests.release.xcconfig */, + ); + name = "Pods-TestingPods-OHHTTPStubs iOS Lib Tests"; + path = "Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Lib Tests"; + sourceTree = ""; + }; + AF493CBE5C8523B8C3AEDC2A6E478E3D /* Support Files */ = { + isa = PBXGroup; + children = ( + 6FBA92CB9D96563B602A87AAFCFCAD7C /* AFNetworking-c94d3492.xcconfig */, + 284C26DCEC89225FE257E8AF6557BAED /* AFNetworking-c94d3492-dummy.m */, + 050C64A16F4965CDA1CDB676C9163041 /* AFNetworking-c94d3492-prefix.pch */, + D2B487F97B81E3910E1CA2A32AB6FF96 /* AFNetworking-iOS.xcconfig */, + 84C2852D436E477D989044D9F70FD4CF /* AFNetworking-iOS-dummy.m */, + 9C0FDF8B2D2F3C5D0A765353B2CBCD33 /* AFNetworking-iOS-prefix.pch */, + 5700278C2E8921B744D90A32E4B2E33E /* AFNetworking-tvOS.xcconfig */, + ED29F21403BDB1E5DD379E272051A69E /* AFNetworking-tvOS-dummy.m */, + A1133EF1E8EBE4F5AEB6EEFEE986A103 /* AFNetworking-tvOS-prefix.pch */, + ); + name = "Support Files"; + path = "../Target Support Files/AFNetworking-c94d3492"; + sourceTree = ""; + }; + B1B370E089BE8D4D0D2A56AEB884B6A7 /* Pods-TestingPods-OHHTTPStubs Mac Tests */ = { + isa = PBXGroup; + children = ( + E87B284887D29B72E6EA5F7E15004BD3 /* Pods-TestingPods-OHHTTPStubs Mac Tests-acknowledgements.markdown */, + 57B933FBD9EB44273FE9CF231ED93FE5 /* Pods-TestingPods-OHHTTPStubs Mac Tests-acknowledgements.plist */, + 2802EA0AF2A76C463F3C503A0FE4B8A1 /* Pods-TestingPods-OHHTTPStubs Mac Tests-dummy.m */, + 64D97EE63038FA87063A3BE46DA25121 /* Pods-TestingPods-OHHTTPStubs Mac Tests.debug.xcconfig */, + F75CD6E2E41BE668BB8D768FD76FB0EC /* Pods-TestingPods-OHHTTPStubs Mac Tests.release.xcconfig */, + ); + name = "Pods-TestingPods-OHHTTPStubs Mac Tests"; + path = "Target Support Files/Pods-TestingPods-OHHTTPStubs Mac Tests"; + sourceTree = ""; + }; + B637022DA2CF1998C39889882CF50F0D /* Pods-TestingPods-OHHTTPStubs iOS Fmk Tests */ = { + isa = PBXGroup; + children = ( + 98F9A5A8A5CBAEFBC64CC66328EC4E99 /* Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-acknowledgements.markdown */, + 6A41CBBC25FDD16D2765EEC7598B9DEB /* Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-acknowledgements.plist */, + DFA732EC710C331C88DDC0E923D582A5 /* Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-dummy.m */, + 224B56DD647BF513E9D8F34F6A809DF6 /* Pods-TestingPods-OHHTTPStubs iOS Fmk Tests.debug.xcconfig */, + 375ECC43A9266AB5C20E91070910BE33 /* Pods-TestingPods-OHHTTPStubs iOS Fmk Tests.release.xcconfig */, + ); + name = "Pods-TestingPods-OHHTTPStubs iOS Fmk Tests"; + path = "Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests"; + sourceTree = ""; + }; + B7F3D444F4024BB920EEF9C30D0D5287 /* Reachability */ = { + isa = PBXGroup; + children = ( + 5AC90BD8F86F957D3DC47D89B2BB040B /* AFNetworkReachabilityManager.h */, + 8CC1FF5E4D9879068E2CA6DD5C9FDAD1 /* AFNetworkReachabilityManager.m */, + ); + name = Reachability; + sourceTree = ""; + }; + BC1A26AFC715C6CA26CB8777253D3FD7 /* Serialization */ = { + isa = PBXGroup; + children = ( + A3B7FFADD03754D2228E8993F0F0CF2A /* AFURLRequestSerialization.h */, + 82DF63A147D90320B4812AFB422015AA /* AFURLRequestSerialization.m */, + F3EC32E0205FA2F4314674D854DCC8B6 /* AFURLResponseSerialization.h */, + C0B108B686488E21A171DAA4D18AD881 /* AFURLResponseSerialization.m */, + ); + name = Serialization; + sourceTree = ""; + }; + E4F6EBFC997C5CDB484BAFF85AEAF253 /* Products */ = { + isa = PBXGroup; + children = ( + 0B4EA1A2506057DEBCB41E0A519B8F4B /* libAFNetworking-c94d3492.a */, + 795C1F36B6AA25F784C83826F0CD6062 /* libAFNetworking-iOS.a */, + F9376FC632354CA218B3C47EAB560527 /* libAFNetworking-tvOS.a */, + F048257A3F3D2308F1C9ADADAD0C0111 /* libPods-TestingPods-OHHTTPStubs iOS Fmk Tests.a */, + A9C1FF3C99BB9C97E4C549796C5A4098 /* libPods-TestingPods-OHHTTPStubs iOS Lib Tests.a */, + 07573BF6F4B141A9D9FE32643F3BCDA6 /* libPods-TestingPods-OHHTTPStubs Mac Tests.a */, + 927D82381B7AF1B27D8797DBA9F40E1A /* libPods-TestingPods-OHHTTPStubs tvOS Fmk Tests.a */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 3A5F15F5F11158DB2F59024DD2D933DC /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + FDB93829F88A10DD4F52FA5D174DD216 /* AFAutoPurgingImageCache.h in Headers */, + B382A7EE4B5A34A04BF7EF1E77096F46 /* AFHTTPSessionManager.h in Headers */, + 8AE476DAE4A8371DB6457ED40A842DFA /* AFImageDownloader.h in Headers */, + DB57CCE4CF63C252CE14C817DE15AACD /* AFNetworkActivityIndicatorManager.h in Headers */, + 3961DC887BC7E7D2862D38BE20072553 /* AFNetworking.h in Headers */, + 32F75AFB06C5BB3D64C7E504D5DABC07 /* AFNetworkReachabilityManager.h in Headers */, + C3DEC73556C1E9825D3EEEC4EF3222D0 /* AFSecurityPolicy.h in Headers */, + 8B0682AA90BB73680ED459DE5D093CF6 /* AFURLRequestSerialization.h in Headers */, + 55026A7B694A5F2FA53A2A75C97DF673 /* AFURLResponseSerialization.h in Headers */, + EADE64D3244517944416C363DA822B18 /* AFURLSessionManager.h in Headers */, + 9208B4637E11A54EB5544487CA571CB3 /* UIActivityIndicatorView+AFNetworking.h in Headers */, + 507F784759A259B26712FA7588DAB168 /* UIButton+AFNetworking.h in Headers */, + 03826B8703E219C11E4C2DAB9A634325 /* UIImage+AFNetworking.h in Headers */, + 36994F1813AD8E417C23A6544D30A357 /* UIImageView+AFNetworking.h in Headers */, + 384FC592DDCAAB5C04CB8A1B3FE2D22E /* UIKit+AFNetworking.h in Headers */, + A05A453F76B4CB397C463F65ADC5A597 /* UIProgressView+AFNetworking.h in Headers */, + D7C129AE47D408B85B5AC1AFEEB29A9A /* UIRefreshControl+AFNetworking.h in Headers */, + 0A5C58997AAC65BF6B09039F0DF6F9FC /* UIWebView+AFNetworking.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4F71E7BD2882DD0B6B8BFFCEA9EA718F /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + AFBE38BD371CDEC001DDD749A136F5E9 /* AFHTTPSessionManager.h in Headers */, + 32EC063C076486D1E539337E3590A00D /* AFNetworking.h in Headers */, + 56D369D51173A5314079D3D8C9EE569B /* AFNetworkReachabilityManager.h in Headers */, + 5420902A4AB0BD57FF81A99B147C7C99 /* AFSecurityPolicy.h in Headers */, + 979BD766D2CB1552B4C87C5D24C5E483 /* AFURLRequestSerialization.h in Headers */, + 73AB5781DC7940FB178273DEFBCB0033 /* AFURLResponseSerialization.h in Headers */, + CA05A3A9B350442758AA8113DC21C005 /* AFURLSessionManager.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8337B73BFC2E8D923592B00B48D181CA /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AC5F42B1DBE9B50DCF746FBCD8809709 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C248C2E3CD3CB4ACBDFB44274A5DB4AF /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 44194FA22CBDE3FB910E7B67D70E2000 /* AFAutoPurgingImageCache.h in Headers */, + EB6CED28E29434212736C804F1F63677 /* AFHTTPSessionManager.h in Headers */, + 9344A6A89825F6253753BD008FF0548A /* AFImageDownloader.h in Headers */, + 7E22C3FBCD4BC396FF1C43FBF301C6D8 /* AFNetworkActivityIndicatorManager.h in Headers */, + 2566BAE1CFDA5654F2FDA6BDFF2D80DB /* AFNetworking.h in Headers */, + D2291731C1F2969AC93B01DD98613A66 /* AFNetworkReachabilityManager.h in Headers */, + FFB6618C19D61DBE626F365FD27153E9 /* AFSecurityPolicy.h in Headers */, + 1097BF248FADB4F17D799B52916E6FDB /* AFURLRequestSerialization.h in Headers */, + DBED2A43013D393E98DB8B2A97045DC4 /* AFURLResponseSerialization.h in Headers */, + FEC5848363B04369FA95F167C751C820 /* AFURLSessionManager.h in Headers */, + 6A4F2D9505BF8DF249C7AFB1D479944F /* UIActivityIndicatorView+AFNetworking.h in Headers */, + 7F514C121F982BAC1BDA0F422BC5B971 /* UIButton+AFNetworking.h in Headers */, + DC1BD3654EAAD82B3E9F44B808026E81 /* UIImage+AFNetworking.h in Headers */, + 522E8E8D06D2046D0CD0DB65C09DDD26 /* UIImageView+AFNetworking.h in Headers */, + E6AB215237138C2A62B8D6DEA6524BBF /* UIKit+AFNetworking.h in Headers */, + 5CEC3FD0B4DBBA6995C6477672C096FB /* UIProgressView+AFNetworking.h in Headers */, + FC53F62EFC6A879D99DE2F22CE3388CD /* UIRefreshControl+AFNetworking.h in Headers */, + 116B684420898BB169C74737B17C20A2 /* UIWebView+AFNetworking.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C369321C5C7F833BE593224ED6F2FB99 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D75A5F8B5A5D32140A4BDECD2CE080ED /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 0A36AD1317D0B4A3A94BFB9ED1E640D2 /* Pods-TestingPods-OHHTTPStubs iOS Lib Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 61315F1E7D712E5C10A013F7CA3738F2 /* Build configuration list for PBXNativeTarget "Pods-TestingPods-OHHTTPStubs iOS Lib Tests" */; + buildPhases = ( + D75A5F8B5A5D32140A4BDECD2CE080ED /* Headers */, + E873E989E9F6557504C3691DBA37CCBF /* Sources */, + 17CBF30B5F9D88B2841609EE21624DDF /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 793B2CDC9B24F3CA7A1DC4BB6C234AFC /* PBXTargetDependency */, + ); + name = "Pods-TestingPods-OHHTTPStubs iOS Lib Tests"; + productName = "Pods-TestingPods-OHHTTPStubs iOS Lib Tests"; + productReference = A9C1FF3C99BB9C97E4C549796C5A4098 /* libPods-TestingPods-OHHTTPStubs iOS Lib Tests.a */; + productType = "com.apple.product-type.library.static"; + }; + 163E987AE2E029D9EB0B15C75EDE7869 /* AFNetworking-c94d3492 */ = { + isa = PBXNativeTarget; + buildConfigurationList = D5C19D094090B561DE0D6763C010330B /* Build configuration list for PBXNativeTarget "AFNetworking-c94d3492" */; + buildPhases = ( + 4F71E7BD2882DD0B6B8BFFCEA9EA718F /* Headers */, + 49D08AC53ED5353308334DE7E4DE0C32 /* Sources */, + E1E635BA6B525F7669EE39D1D75F88B8 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "AFNetworking-c94d3492"; + productName = "AFNetworking-c94d3492"; + productReference = 0B4EA1A2506057DEBCB41E0A519B8F4B /* libAFNetworking-c94d3492.a */; + productType = "com.apple.product-type.library.static"; + }; + 294529C77198ECB248E415B29FDC9441 /* AFNetworking-iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = E255DDD270476C203438B6706952D083 /* Build configuration list for PBXNativeTarget "AFNetworking-iOS" */; + buildPhases = ( + 3A5F15F5F11158DB2F59024DD2D933DC /* Headers */, + D5696087864FB9982848D799CAB72950 /* Sources */, + 8170D86D056338A72444D741DB242CBC /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "AFNetworking-iOS"; + productName = "AFNetworking-iOS"; + productReference = 795C1F36B6AA25F784C83826F0CD6062 /* libAFNetworking-iOS.a */; + productType = "com.apple.product-type.library.static"; + }; + 5F08154FE575078358A2BD13C331046B /* Pods-TestingPods-OHHTTPStubs Mac Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6E102AEADA192885A73F950AFC0FCC79 /* Build configuration list for PBXNativeTarget "Pods-TestingPods-OHHTTPStubs Mac Tests" */; + buildPhases = ( + C369321C5C7F833BE593224ED6F2FB99 /* Headers */, + 80AFF6D26BEB01E3C5A35437A574FF4F /* Sources */, + B3A7B8716F946A969B3C87B511DFBB06 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 67EA370C59DC523A241BC72B78563533 /* PBXTargetDependency */, + ); + name = "Pods-TestingPods-OHHTTPStubs Mac Tests"; + productName = "Pods-TestingPods-OHHTTPStubs Mac Tests"; + productReference = 07573BF6F4B141A9D9FE32643F3BCDA6 /* libPods-TestingPods-OHHTTPStubs Mac Tests.a */; + productType = "com.apple.product-type.library.static"; + }; + B93B7613DEC5DD7B2A5FAB1A9071B504 /* Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3BDF0FBD49C6F8B40C551849FAA61459 /* Build configuration list for PBXNativeTarget "Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests" */; + buildPhases = ( + AC5F42B1DBE9B50DCF746FBCD8809709 /* Headers */, + 5F1AEC4BEF86EB08837C8CEEB206DA60 /* Sources */, + D2ABD6FD334887DD00B0DF3AB54AB3E4 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + AFB25B32E7ED42BEC067C5F909AEFE7F /* PBXTargetDependency */, + ); + name = "Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests"; + productName = "Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests"; + productReference = 927D82381B7AF1B27D8797DBA9F40E1A /* libPods-TestingPods-OHHTTPStubs tvOS Fmk Tests.a */; + productType = "com.apple.product-type.library.static"; + }; + D70B3FB3184A448A3E88112CBF71A2AA /* AFNetworking-tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = A6B6FC863E8EA6582C55995B87A8E595 /* Build configuration list for PBXNativeTarget "AFNetworking-tvOS" */; + buildPhases = ( + C248C2E3CD3CB4ACBDFB44274A5DB4AF /* Headers */, + 15D87767F031BF1F471E711A1DE27D92 /* Sources */, + C750BCC5FA7C0105F40CD44E94A36BA7 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "AFNetworking-tvOS"; + productName = "AFNetworking-tvOS"; + productReference = F9376FC632354CA218B3C47EAB560527 /* libAFNetworking-tvOS.a */; + productType = "com.apple.product-type.library.static"; + }; + E066DB79AA19C4175D8A0F28DD775447 /* Pods-TestingPods-OHHTTPStubs iOS Fmk Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4D26715B140BC177322AF14551B3739B /* Build configuration list for PBXNativeTarget "Pods-TestingPods-OHHTTPStubs iOS Fmk Tests" */; + buildPhases = ( + 8337B73BFC2E8D923592B00B48D181CA /* Headers */, + 9F013DFB01EC2C2A23E01368FB959DCE /* Sources */, + 7A6204F09A25143D89CC871BF538877D /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + C9DA06B86A50A50FA261BE5EAC3A4735 /* PBXTargetDependency */, + ); + name = "Pods-TestingPods-OHHTTPStubs iOS Fmk Tests"; + productName = "Pods-TestingPods-OHHTTPStubs iOS Fmk Tests"; + productReference = F048257A3F3D2308F1C9ADADAD0C0111 /* libPods-TestingPods-OHHTTPStubs iOS Fmk Tests.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D41D8CD98F00B204E9800998ECF8427E /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0930; + LastUpgradeCheck = 1020; + }; + buildConfigurationList = 2D8E8EC45A3A1A1D94AE762CB5028504 /* Build configuration list for PBXProject "Pods" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 7DB346D0F39D3F0E887471402A8071AB; + productRefGroup = E4F6EBFC997C5CDB484BAFF85AEAF253 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 163E987AE2E029D9EB0B15C75EDE7869 /* AFNetworking-c94d3492 */, + 294529C77198ECB248E415B29FDC9441 /* AFNetworking-iOS */, + D70B3FB3184A448A3E88112CBF71A2AA /* AFNetworking-tvOS */, + E066DB79AA19C4175D8A0F28DD775447 /* Pods-TestingPods-OHHTTPStubs iOS Fmk Tests */, + 0A36AD1317D0B4A3A94BFB9ED1E640D2 /* Pods-TestingPods-OHHTTPStubs iOS Lib Tests */, + 5F08154FE575078358A2BD13C331046B /* Pods-TestingPods-OHHTTPStubs Mac Tests */, + B93B7613DEC5DD7B2A5FAB1A9071B504 /* Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 15D87767F031BF1F471E711A1DE27D92 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + EAABE565D6D62D27D232B94F4F86CF2F /* AFAutoPurgingImageCache.m in Sources */, + 8C176A815B409BC36CE64A5807FA1075 /* AFHTTPSessionManager.m in Sources */, + 45DAC3ED54E9D38488DA4B62162F98AF /* AFImageDownloader.m in Sources */, + 49ECB3B7AB3028A11367187119BC8BB7 /* AFNetworkActivityIndicatorManager.m in Sources */, + 6C54A1A08A4CCCD443E90EB8D3DF768C /* AFNetworking-tvOS-dummy.m in Sources */, + 643C4AB032AB560491B42F8FB0297EC4 /* AFNetworkReachabilityManager.m in Sources */, + 61443F016D0C1732F73913538E4E7908 /* AFSecurityPolicy.m in Sources */, + 4221D8805C6A539DB6910646BEED3695 /* AFURLRequestSerialization.m in Sources */, + 419794E37141ECCF8D65524851BBF4B0 /* AFURLResponseSerialization.m in Sources */, + 5FC64ACBF1ED123C69507A9176E0F134 /* AFURLSessionManager.m in Sources */, + D9425DE952F0E042DA9C10785C6A9B7C /* UIActivityIndicatorView+AFNetworking.m in Sources */, + 5B9099D9A7BB21987AA44C6F88422575 /* UIButton+AFNetworking.m in Sources */, + EBA21C2504105E270C349BAF94C81CF6 /* UIImageView+AFNetworking.m in Sources */, + D8ACF9ED0002D52D205E1119E7DBD5D9 /* UIProgressView+AFNetworking.m in Sources */, + 1B310C2202C5838DE17FD736B4C99FF1 /* UIRefreshControl+AFNetworking.m in Sources */, + 557288FE943D212DA96D05AA9D9BBBC1 /* UIWebView+AFNetworking.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 49D08AC53ED5353308334DE7E4DE0C32 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A5BC0E662634EB2667EBB67F9AA855F7 /* AFHTTPSessionManager.m in Sources */, + 2F86B19B954FC0CBA21DB7BB3075B977 /* AFNetworking-c94d3492-dummy.m in Sources */, + F1EB67B1A9C1A9458E65D2BBA3CF356E /* AFNetworkReachabilityManager.m in Sources */, + A91CA6F5355347DFCEA29231DED2D805 /* AFSecurityPolicy.m in Sources */, + 55663564E902A5510FC3881DA1FBF67E /* AFURLRequestSerialization.m in Sources */, + E3771BDC96E71EC693D8E0077F1D91AC /* AFURLResponseSerialization.m in Sources */, + 31E0BFAE0DE4C1C58E9EFD93DC054597 /* AFURLSessionManager.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5F1AEC4BEF86EB08837C8CEEB206DA60 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97926D543BBAAFE8E14CC544E31F4BD0 /* Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 80AFF6D26BEB01E3C5A35437A574FF4F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 946EFF93146FA52AFD42520697349936 /* Pods-TestingPods-OHHTTPStubs Mac Tests-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9F013DFB01EC2C2A23E01368FB959DCE /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B1D14C1DBFF8048DFA8593DB11FCFF44 /* Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D5696087864FB9982848D799CAB72950 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 907268E34649AB2AAB074BCA44C8000F /* AFAutoPurgingImageCache.m in Sources */, + 9591A2AD0135BE09CA49199BE0DF89A3 /* AFHTTPSessionManager.m in Sources */, + 32A0D7DB742D61FEF014C54D48960857 /* AFImageDownloader.m in Sources */, + C95B654D0777E3F59E627DB8154FBF64 /* AFNetworkActivityIndicatorManager.m in Sources */, + 4FD29B68A3B5F7B8FF60AA198DD25790 /* AFNetworking-iOS-dummy.m in Sources */, + 5FDCA4EEF5F7E9DEC06741EDC7C91923 /* AFNetworkReachabilityManager.m in Sources */, + E24C16DF4A790AA901FC78D88AD356FC /* AFSecurityPolicy.m in Sources */, + 0A90CEAFD09D78174FC763C95E302179 /* AFURLRequestSerialization.m in Sources */, + 3BF6D0D7ABF591F0C2A3D0ED8255FA15 /* AFURLResponseSerialization.m in Sources */, + D61AF81D29FD5A1B3D1BAE2AC75D880C /* AFURLSessionManager.m in Sources */, + E918FB9D9BEEF76125067E8CC98D963D /* UIActivityIndicatorView+AFNetworking.m in Sources */, + E0E06D042DA0183FA128B3698E5E006F /* UIButton+AFNetworking.m in Sources */, + 9CE73AE2C7D0BB9764D2BC98B5B405F0 /* UIImageView+AFNetworking.m in Sources */, + 23DC83D398DC88B656CB82A621197CD4 /* UIProgressView+AFNetworking.m in Sources */, + 141CEED22EC7683E8EBCA3FA9A98427A /* UIRefreshControl+AFNetworking.m in Sources */, + 7B84F986E685989A630FA5280DD03C70 /* UIWebView+AFNetworking.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E873E989E9F6557504C3691DBA37CCBF /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F850FC543153A64DFE92955ECDB4F337 /* Pods-TestingPods-OHHTTPStubs iOS Lib Tests-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 67EA370C59DC523A241BC72B78563533 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = "AFNetworking-c94d3492"; + target = 163E987AE2E029D9EB0B15C75EDE7869 /* AFNetworking-c94d3492 */; + targetProxy = F85EB9E30D7904CCA7FF65EE3B498A01 /* PBXContainerItemProxy */; + }; + 793B2CDC9B24F3CA7A1DC4BB6C234AFC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = "AFNetworking-iOS"; + target = 294529C77198ECB248E415B29FDC9441 /* AFNetworking-iOS */; + targetProxy = 3E8F186EC3374BF269EF3836BA0B646E /* PBXContainerItemProxy */; + }; + AFB25B32E7ED42BEC067C5F909AEFE7F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = "AFNetworking-tvOS"; + target = D70B3FB3184A448A3E88112CBF71A2AA /* AFNetworking-tvOS */; + targetProxy = 512D17CE7818DBB43842FC8176228B38 /* PBXContainerItemProxy */; + }; + C9DA06B86A50A50FA261BE5EAC3A4735 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = "AFNetworking-iOS"; + target = 294529C77198ECB248E415B29FDC9441 /* AFNetworking-iOS */; + targetProxy = A498827688C4618D5E134F7CA98F0A02 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 02B3AB406648BB84F70B6F5DEA108237 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6FBA92CB9D96563B602A87AAFCFCAD7C /* AFNetworking-c94d3492.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + EXECUTABLE_PREFIX = lib; + GCC_PREFIX_HEADER = "Target Support Files/AFNetworking-c94d3492/AFNetworking-c94d3492-prefix.pch"; + MACOSX_DEPLOYMENT_TARGET = 10.9; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PRIVATE_HEADERS_FOLDER_PATH = ""; + PRODUCT_MODULE_NAME = AFNetworking; + PRODUCT_NAME = "AFNetworking-c94d3492"; + PUBLIC_HEADERS_FOLDER_PATH = ""; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 4.2; + }; + name = Debug; + }; + 1DDF6121A925E14FFDCAC59DFBCF4523 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + 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; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + 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 = ( + "POD_CONFIGURATION_DEBUG=1", + "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; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MACOSX_DEPLOYMENT_TARGET = 10.9; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.2; + SYMROOT = "${SRCROOT}/../build"; + TVOS_DEPLOYMENT_TARGET = 9.0; + }; + name = Debug; + }; + 25831FB79A5C56A31CE19090703E2593 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5700278C2E8921B744D90A32E4B2E33E /* AFNetworking-tvOS.xcconfig */; + buildSettings = { + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + GCC_PREFIX_HEADER = "Target Support Files/AFNetworking-tvOS/AFNetworking-tvOS-prefix.pch"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PRIVATE_HEADERS_FOLDER_PATH = ""; + PRODUCT_MODULE_NAME = AFNetworking; + PRODUCT_NAME = "AFNetworking-tvOS"; + PUBLIC_HEADERS_FOLDER_PATH = ""; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 9.0; + }; + name = Debug; + }; + 345B1F678F11C05E971C1CFAA0BC1667 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F75CD6E2E41BE668BB8D768FD76FB0EC /* Pods-TestingPods-OHHTTPStubs Mac Tests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + EXECUTABLE_PREFIX = lib; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.9; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + }; + name = Release; + }; + 43CF42126E8E71F38E7356350FA61E57 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5700278C2E8921B744D90A32E4B2E33E /* AFNetworking-tvOS.xcconfig */; + buildSettings = { + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + GCC_PREFIX_HEADER = "Target Support Files/AFNetworking-tvOS/AFNetworking-tvOS-prefix.pch"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PRIVATE_HEADERS_FOLDER_PATH = ""; + PRODUCT_MODULE_NAME = AFNetworking; + PRODUCT_NAME = "AFNetworking-tvOS"; + PUBLIC_HEADERS_FOLDER_PATH = ""; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 9.0; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 447A11DFF9E43C48963560DD326E721D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 224B56DD647BF513E9D8F34F6A809DF6 /* Pods-TestingPods-OHHTTPStubs iOS Fmk Tests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MACH_O_TYPE = staticlib; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 61CC5216F1BEBC340B383D4F722559F8 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 375ECC43A9266AB5C20E91070910BE33 /* Pods-TestingPods-OHHTTPStubs iOS Fmk Tests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MACH_O_TYPE = staticlib; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 710E4AEF0282FFC10C760902DA36BECF /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 225EA8457A11BA3434034790BCDC374E /* Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + MACH_O_TYPE = staticlib; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 9.0; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 72D797D6A1C236F9084F144930163C4E /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6FBA92CB9D96563B602A87AAFCFCAD7C /* AFNetworking-c94d3492.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + EXECUTABLE_PREFIX = lib; + GCC_PREFIX_HEADER = "Target Support Files/AFNetworking-c94d3492/AFNetworking-c94d3492-prefix.pch"; + MACOSX_DEPLOYMENT_TARGET = 10.9; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PRIVATE_HEADERS_FOLDER_PATH = ""; + PRODUCT_MODULE_NAME = AFNetworking; + PRODUCT_NAME = "AFNetworking-c94d3492"; + PUBLIC_HEADERS_FOLDER_PATH = ""; + SDKROOT = macosx; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 4.2; + }; + name = Release; + }; + 7E16F15DEA6FF81A33CF59C29E9215E0 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D2B487F97B81E3910E1CA2A32AB6FF96 /* AFNetworking-iOS.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + GCC_PREFIX_HEADER = "Target Support Files/AFNetworking-iOS/AFNetworking-iOS-prefix.pch"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PRIVATE_HEADERS_FOLDER_PATH = ""; + PRODUCT_MODULE_NAME = AFNetworking; + PRODUCT_NAME = "AFNetworking-iOS"; + PUBLIC_HEADERS_FOLDER_PATH = ""; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 92EAD65E589E5B42D3B19AD931637F67 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 45A396A85D76FDCBD6D52CF077E055B3 /* Pods-TestingPods-OHHTTPStubs iOS Lib Tests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MACH_O_TYPE = staticlib; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 93BD4C065E51ABBB3E3C4BCE3A847A18 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5154A04BF6B939CC5C91B26BC3B235C6 /* Pods-TestingPods-OHHTTPStubs iOS Lib Tests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MACH_O_TYPE = staticlib; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + CA3C8B4EC312BF16F6268F0CFF1A4F32 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 64D97EE63038FA87063A3BE46DA25121 /* Pods-TestingPods-OHHTTPStubs Mac Tests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + EXECUTABLE_PREFIX = lib; + MACH_O_TYPE = staticlib; + MACOSX_DEPLOYMENT_TARGET = 10.9; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + CD950C4F2A9B356E05BD094AB37FFC21 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D2B487F97B81E3910E1CA2A32AB6FF96 /* AFNetworking-iOS.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + GCC_PREFIX_HEADER = "Target Support Files/AFNetworking-iOS/AFNetworking-iOS-prefix.pch"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PRIVATE_HEADERS_FOLDER_PATH = ""; + PRODUCT_MODULE_NAME = AFNetworking; + PRODUCT_NAME = "AFNetworking-iOS"; + PUBLIC_HEADERS_FOLDER_PATH = ""; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + E0DBB13FA55F7DF77A8AAC8A8CB30F7C /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A7FFBE99241E2C30FB09E85564DA1D88 /* Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + MACH_O_TYPE = staticlib; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 9.0; + }; + name = Debug; + }; + E680C172ADDC70B93B11A1B8C3F6D834 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + 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; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "POD_CONFIGURATION_RELEASE=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; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MACOSX_DEPLOYMENT_TARGET = 10.9; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.2; + SYMROOT = "${SRCROOT}/../build"; + TVOS_DEPLOYMENT_TARGET = 9.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 2D8E8EC45A3A1A1D94AE762CB5028504 /* Build configuration list for PBXProject "Pods" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1DDF6121A925E14FFDCAC59DFBCF4523 /* Debug */, + E680C172ADDC70B93B11A1B8C3F6D834 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3BDF0FBD49C6F8B40C551849FAA61459 /* Build configuration list for PBXNativeTarget "Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E0DBB13FA55F7DF77A8AAC8A8CB30F7C /* Debug */, + 710E4AEF0282FFC10C760902DA36BECF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4D26715B140BC177322AF14551B3739B /* Build configuration list for PBXNativeTarget "Pods-TestingPods-OHHTTPStubs iOS Fmk Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 447A11DFF9E43C48963560DD326E721D /* Debug */, + 61CC5216F1BEBC340B383D4F722559F8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 61315F1E7D712E5C10A013F7CA3738F2 /* Build configuration list for PBXNativeTarget "Pods-TestingPods-OHHTTPStubs iOS Lib Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 92EAD65E589E5B42D3B19AD931637F67 /* Debug */, + 93BD4C065E51ABBB3E3C4BCE3A847A18 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6E102AEADA192885A73F950AFC0FCC79 /* Build configuration list for PBXNativeTarget "Pods-TestingPods-OHHTTPStubs Mac Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CA3C8B4EC312BF16F6268F0CFF1A4F32 /* Debug */, + 345B1F678F11C05E971C1CFAA0BC1667 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A6B6FC863E8EA6582C55995B87A8E595 /* Build configuration list for PBXNativeTarget "AFNetworking-tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 25831FB79A5C56A31CE19090703E2593 /* Debug */, + 43CF42126E8E71F38E7356350FA61E57 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D5C19D094090B561DE0D6763C010330B /* Build configuration list for PBXNativeTarget "AFNetworking-c94d3492" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 02B3AB406648BB84F70B6F5DEA108237 /* Debug */, + 72D797D6A1C236F9084F144930163C4E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E255DDD270476C203438B6706952D083 /* Build configuration list for PBXNativeTarget "AFNetworking-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CD950C4F2A9B356E05BD094AB37FFC21 /* Debug */, + 7E16F15DEA6FF81A33CF59C29E9215E0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = D41D8CD98F00B204E9800998ECF8427E /* Project object */; +} diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/AFNetworking-c94d3492/AFNetworking-c94d3492-dummy.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/AFNetworking-c94d3492/AFNetworking-c94d3492-dummy.m new file mode 100644 index 0000000000..6924234dea --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/AFNetworking-c94d3492/AFNetworking-c94d3492-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_AFNetworking_c94d3492 : NSObject +@end +@implementation PodsDummy_AFNetworking_c94d3492 +@end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/AFNetworking-c94d3492/AFNetworking-c94d3492-prefix.pch b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/AFNetworking-c94d3492/AFNetworking-c94d3492-prefix.pch new file mode 100644 index 0000000000..759a81aab6 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/AFNetworking-c94d3492/AFNetworking-c94d3492-prefix.pch @@ -0,0 +1,23 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + +#ifndef TARGET_OS_IOS + #define TARGET_OS_IOS TARGET_OS_IPHONE +#endif + +#ifndef TARGET_OS_WATCH + #define TARGET_OS_WATCH 0 +#endif + +#ifndef TARGET_OS_TV + #define TARGET_OS_TV 0 +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/AFNetworking-c94d3492/AFNetworking-c94d3492.xcconfig b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/AFNetworking-c94d3492/AFNetworking-c94d3492.xcconfig new file mode 100644 index 0000000000..ef42c1abe4 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/AFNetworking-c94d3492/AFNetworking-c94d3492.xcconfig @@ -0,0 +1,9 @@ +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/AFNetworking-c94d3492 +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Private/AFNetworking" "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/AFNetworking" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/AFNetworking +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/AFNetworking-iOS/AFNetworking-iOS-dummy.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/AFNetworking-iOS/AFNetworking-iOS-dummy.m new file mode 100644 index 0000000000..0c243bce21 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/AFNetworking-iOS/AFNetworking-iOS-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_AFNetworking_iOS : NSObject +@end +@implementation PodsDummy_AFNetworking_iOS +@end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/AFNetworking-iOS/AFNetworking-iOS-prefix.pch b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/AFNetworking-iOS/AFNetworking-iOS-prefix.pch new file mode 100644 index 0000000000..e72247a981 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/AFNetworking-iOS/AFNetworking-iOS-prefix.pch @@ -0,0 +1,23 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + +#ifndef TARGET_OS_IOS + #define TARGET_OS_IOS TARGET_OS_IPHONE +#endif + +#ifndef TARGET_OS_WATCH + #define TARGET_OS_WATCH 0 +#endif + +#ifndef TARGET_OS_TV + #define TARGET_OS_TV 0 +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/AFNetworking-iOS/AFNetworking-iOS.xcconfig b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/AFNetworking-iOS/AFNetworking-iOS.xcconfig new file mode 100644 index 0000000000..f75998719b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/AFNetworking-iOS/AFNetworking-iOS.xcconfig @@ -0,0 +1,9 @@ +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/AFNetworking-iOS +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Private/AFNetworking" "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/AFNetworking" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/AFNetworking +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/AFNetworking-tvOS/AFNetworking-tvOS-dummy.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/AFNetworking-tvOS/AFNetworking-tvOS-dummy.m new file mode 100644 index 0000000000..ed737f38fd --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/AFNetworking-tvOS/AFNetworking-tvOS-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_AFNetworking_tvOS : NSObject +@end +@implementation PodsDummy_AFNetworking_tvOS +@end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/AFNetworking-tvOS/AFNetworking-tvOS-prefix.pch b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/AFNetworking-tvOS/AFNetworking-tvOS-prefix.pch new file mode 100644 index 0000000000..e72247a981 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/AFNetworking-tvOS/AFNetworking-tvOS-prefix.pch @@ -0,0 +1,23 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + +#ifndef TARGET_OS_IOS + #define TARGET_OS_IOS TARGET_OS_IPHONE +#endif + +#ifndef TARGET_OS_WATCH + #define TARGET_OS_WATCH 0 +#endif + +#ifndef TARGET_OS_TV + #define TARGET_OS_TV 0 +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/AFNetworking-tvOS/AFNetworking-tvOS.xcconfig b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/AFNetworking-tvOS/AFNetworking-tvOS.xcconfig new file mode 100644 index 0000000000..b50ac43f07 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/AFNetworking-tvOS/AFNetworking-tvOS.xcconfig @@ -0,0 +1,9 @@ +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/AFNetworking-tvOS +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/Headers/Private" "${PODS_ROOT}/Headers/Private/AFNetworking" "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/AFNetworking" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/AFNetworking +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs Mac Tests/Pods-TestingPods-OHHTTPStubs Mac Tests-acknowledgements.markdown b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs Mac Tests/Pods-TestingPods-OHHTTPStubs Mac Tests-acknowledgements.markdown new file mode 100644 index 0000000000..aa247077f4 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs Mac Tests/Pods-TestingPods-OHHTTPStubs Mac Tests-acknowledgements.markdown @@ -0,0 +1,26 @@ +# Acknowledgements +This application makes use of the following third party libraries: + +## AFNetworking + +Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Generated by CocoaPods - https://cocoapods.org diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs Mac Tests/Pods-TestingPods-OHHTTPStubs Mac Tests-acknowledgements.plist b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs Mac Tests/Pods-TestingPods-OHHTTPStubs Mac Tests-acknowledgements.plist new file mode 100644 index 0000000000..d26166b217 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs Mac Tests/Pods-TestingPods-OHHTTPStubs Mac Tests-acknowledgements.plist @@ -0,0 +1,58 @@ + + + + + PreferenceSpecifiers + + + FooterText + This application makes use of the following third party libraries: + Title + Acknowledgements + Type + PSGroupSpecifier + + + FooterText + Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + License + MIT + Title + AFNetworking + Type + PSGroupSpecifier + + + FooterText + Generated by CocoaPods - https://cocoapods.org + Title + + Type + PSGroupSpecifier + + + StringsTable + Acknowledgements + Title + Acknowledgements + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs Mac Tests/Pods-TestingPods-OHHTTPStubs Mac Tests-dummy.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs Mac Tests/Pods-TestingPods-OHHTTPStubs Mac Tests-dummy.m new file mode 100644 index 0000000000..7efb2b550d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs Mac Tests/Pods-TestingPods-OHHTTPStubs Mac Tests-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Pods_TestingPods_OHHTTPStubs_Mac_Tests : NSObject +@end +@implementation PodsDummy_Pods_TestingPods_OHHTTPStubs_Mac_Tests +@end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs Mac Tests/Pods-TestingPods-OHHTTPStubs Mac Tests-frameworks.sh b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs Mac Tests/Pods-TestingPods-OHHTTPStubs Mac Tests-frameworks.sh new file mode 100755 index 0000000000..893c16a631 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs Mac Tests/Pods-TestingPods-OHHTTPStubs Mac Tests-frameworks.sh @@ -0,0 +1,84 @@ +#!/bin/sh +set -e + +echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" +mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + +SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" + +install_framework() +{ + if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then + local source="${BUILT_PRODUCTS_DIR}/$1" + elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then + local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" + elif [ -r "$1" ]; then + local source="$1" + fi + + local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + + if [ -L "${source}" ]; then + echo "Symlinked..." + source="$(readlink "${source}")" + fi + + # use filter instead of exclude so missing patterns dont' throw errors + echo "rsync -av --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" + rsync -av --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" + + local basename + basename="$(basename -s .framework "$1")" + binary="${destination}/${basename}.framework/${basename}" + if ! [ -r "$binary" ]; then + binary="${destination}/${basename}" + fi + + # Strip invalid architectures so "fat" simulator / device frameworks work on device + if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then + strip_invalid_archs "$binary" + fi + + # Resign the code if required by the build settings to avoid unstable apps + code_sign_if_enabled "${destination}/$(basename "$1")" + + # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. + if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then + local swift_runtime_libs + swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u && exit ${PIPESTATUS[0]}) + for lib in $swift_runtime_libs; do + echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" + rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" + code_sign_if_enabled "${destination}/${lib}" + done + fi +} + +# Signs a framework with the provided identity +code_sign_if_enabled() { + if [ -n "${EXPANDED_CODE_SIGN_IDENTITY}" -a "${CODE_SIGNING_REQUIRED}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then + # Use the current code_sign_identitiy + echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" + echo "/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS} --preserve-metadata=identifier,entitlements \"$1\"" + /usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS} --preserve-metadata=identifier,entitlements "$1" + fi +} + +# Strip invalid architectures +strip_invalid_archs() { + binary="$1" + # Get architectures for current file + archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | rev)" + stripped="" + for arch in $archs; do + if ! [[ "${VALID_ARCHS}" == *"$arch"* ]]; then + # Strip non-valid architectures in-place + lipo -remove "$arch" -output "$binary" "$binary" || exit 1 + stripped="$stripped $arch" + fi + done + if [[ "$stripped" ]]; then + echo "Stripped $binary of architectures:$stripped" + fi +} + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs Mac Tests/Pods-TestingPods-OHHTTPStubs Mac Tests-resources.sh b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs Mac Tests/Pods-TestingPods-OHHTTPStubs Mac Tests-resources.sh new file mode 100755 index 0000000000..0a1561528c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs Mac Tests/Pods-TestingPods-OHHTTPStubs Mac Tests-resources.sh @@ -0,0 +1,102 @@ +#!/bin/sh +set -e + +mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" + +RESOURCES_TO_COPY=${PODS_ROOT}/resources-to-copy-${TARGETNAME}.txt +> "$RESOURCES_TO_COPY" + +XCASSET_FILES=() + +case "${TARGETED_DEVICE_FAMILY}" in + 1,2) + TARGET_DEVICE_ARGS="--target-device ipad --target-device iphone" + ;; + 1) + TARGET_DEVICE_ARGS="--target-device iphone" + ;; + 2) + TARGET_DEVICE_ARGS="--target-device ipad" + ;; + *) + TARGET_DEVICE_ARGS="--target-device mac" + ;; +esac + +realpath() { + DIRECTORY="$(cd "${1%/*}" && pwd)" + FILENAME="${1##*/}" + echo "$DIRECTORY/$FILENAME" +} + +install_resource() +{ + if [[ "$1" = /* ]] ; then + RESOURCE_PATH="$1" + else + RESOURCE_PATH="${PODS_ROOT}/$1" + fi + if [[ ! -e "$RESOURCE_PATH" ]] ; then + cat << EOM +error: Resource "$RESOURCE_PATH" not found. Run 'pod install' to update the copy resources script. +EOM + exit 1 + fi + case $RESOURCE_PATH in + *.storyboard) + echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" + ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} + ;; + *.xib) + echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" + ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} + ;; + *.framework) + echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + echo "rsync -av $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + rsync -av "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + ;; + *.xcdatamodel) + echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH"`.mom\"" + xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodel`.mom" + ;; + *.xcdatamodeld) + echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd\"" + xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd" + ;; + *.xcmappingmodel) + echo "xcrun mapc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm\"" + xcrun mapc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm" + ;; + *.xcassets) + ABSOLUTE_XCASSET_FILE=$(realpath "$RESOURCE_PATH") + XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE") + ;; + *) + echo "$RESOURCE_PATH" + echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY" + ;; + esac +} + +mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +if [[ "${ACTION}" == "install" ]] && [[ "${SKIP_INSTALL}" == "NO" ]]; then + mkdir -p "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" + rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +fi +rm -f "$RESOURCES_TO_COPY" + +if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "$XCASSET_FILES" ] +then + # Find all other xcassets (this unfortunately includes those of path pods and other targets). + OTHER_XCASSETS=$(find "$PWD" -iname "*.xcassets" -type d) + while read line; do + if [[ $line != "`realpath $PODS_ROOT`*" ]]; then + XCASSET_FILES+=("$line") + fi + done <<<"$OTHER_XCASSETS" + + printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +fi diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs Mac Tests/Pods-TestingPods-OHHTTPStubs Mac Tests.debug.xcconfig b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs Mac Tests/Pods-TestingPods-OHHTTPStubs Mac Tests.debug.xcconfig new file mode 100644 index 0000000000..7cd9abcacc --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs Mac Tests/Pods-TestingPods-OHHTTPStubs Mac Tests.debug.xcconfig @@ -0,0 +1,9 @@ +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/AFNetworking" +LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/AFNetworking-c94d3492" +OTHER_CFLAGS = $(inherited) -isystem "${PODS_ROOT}/Headers/Public" -isystem "${PODS_ROOT}/Headers/Public/AFNetworking" +OTHER_LDFLAGS = $(inherited) -ObjC -l"AFNetworking-c94d3492" -framework "CoreServices" -framework "Security" -framework "SystemConfiguration" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs Mac Tests/Pods-TestingPods-OHHTTPStubs Mac Tests.release.xcconfig b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs Mac Tests/Pods-TestingPods-OHHTTPStubs Mac Tests.release.xcconfig new file mode 100644 index 0000000000..7cd9abcacc --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs Mac Tests/Pods-TestingPods-OHHTTPStubs Mac Tests.release.xcconfig @@ -0,0 +1,9 @@ +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/AFNetworking" +LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/AFNetworking-c94d3492" +OTHER_CFLAGS = $(inherited) -isystem "${PODS_ROOT}/Headers/Public" -isystem "${PODS_ROOT}/Headers/Public/AFNetworking" +OTHER_LDFLAGS = $(inherited) -ObjC -l"AFNetworking-c94d3492" -framework "CoreServices" -framework "Security" -framework "SystemConfiguration" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-acknowledgements.markdown b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-acknowledgements.markdown new file mode 100644 index 0000000000..aa247077f4 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-acknowledgements.markdown @@ -0,0 +1,26 @@ +# Acknowledgements +This application makes use of the following third party libraries: + +## AFNetworking + +Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Generated by CocoaPods - https://cocoapods.org diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-acknowledgements.plist b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-acknowledgements.plist new file mode 100644 index 0000000000..d26166b217 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-acknowledgements.plist @@ -0,0 +1,58 @@ + + + + + PreferenceSpecifiers + + + FooterText + This application makes use of the following third party libraries: + Title + Acknowledgements + Type + PSGroupSpecifier + + + FooterText + Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + License + MIT + Title + AFNetworking + Type + PSGroupSpecifier + + + FooterText + Generated by CocoaPods - https://cocoapods.org + Title + + Type + PSGroupSpecifier + + + StringsTable + Acknowledgements + Title + Acknowledgements + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-dummy.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-dummy.m new file mode 100644 index 0000000000..b9c448bc05 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Pods_TestingPods_OHHTTPStubs_iOS_Fmk_Tests : NSObject +@end +@implementation PodsDummy_Pods_TestingPods_OHHTTPStubs_iOS_Fmk_Tests +@end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-frameworks.sh b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-frameworks.sh new file mode 100755 index 0000000000..893c16a631 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-frameworks.sh @@ -0,0 +1,84 @@ +#!/bin/sh +set -e + +echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" +mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + +SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" + +install_framework() +{ + if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then + local source="${BUILT_PRODUCTS_DIR}/$1" + elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then + local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" + elif [ -r "$1" ]; then + local source="$1" + fi + + local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + + if [ -L "${source}" ]; then + echo "Symlinked..." + source="$(readlink "${source}")" + fi + + # use filter instead of exclude so missing patterns dont' throw errors + echo "rsync -av --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" + rsync -av --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" + + local basename + basename="$(basename -s .framework "$1")" + binary="${destination}/${basename}.framework/${basename}" + if ! [ -r "$binary" ]; then + binary="${destination}/${basename}" + fi + + # Strip invalid architectures so "fat" simulator / device frameworks work on device + if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then + strip_invalid_archs "$binary" + fi + + # Resign the code if required by the build settings to avoid unstable apps + code_sign_if_enabled "${destination}/$(basename "$1")" + + # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. + if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then + local swift_runtime_libs + swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u && exit ${PIPESTATUS[0]}) + for lib in $swift_runtime_libs; do + echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" + rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" + code_sign_if_enabled "${destination}/${lib}" + done + fi +} + +# Signs a framework with the provided identity +code_sign_if_enabled() { + if [ -n "${EXPANDED_CODE_SIGN_IDENTITY}" -a "${CODE_SIGNING_REQUIRED}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then + # Use the current code_sign_identitiy + echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" + echo "/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS} --preserve-metadata=identifier,entitlements \"$1\"" + /usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS} --preserve-metadata=identifier,entitlements "$1" + fi +} + +# Strip invalid architectures +strip_invalid_archs() { + binary="$1" + # Get architectures for current file + archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | rev)" + stripped="" + for arch in $archs; do + if ! [[ "${VALID_ARCHS}" == *"$arch"* ]]; then + # Strip non-valid architectures in-place + lipo -remove "$arch" -output "$binary" "$binary" || exit 1 + stripped="$stripped $arch" + fi + done + if [[ "$stripped" ]]; then + echo "Stripped $binary of architectures:$stripped" + fi +} + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-resources.sh b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-resources.sh new file mode 100755 index 0000000000..0a1561528c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests-resources.sh @@ -0,0 +1,102 @@ +#!/bin/sh +set -e + +mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" + +RESOURCES_TO_COPY=${PODS_ROOT}/resources-to-copy-${TARGETNAME}.txt +> "$RESOURCES_TO_COPY" + +XCASSET_FILES=() + +case "${TARGETED_DEVICE_FAMILY}" in + 1,2) + TARGET_DEVICE_ARGS="--target-device ipad --target-device iphone" + ;; + 1) + TARGET_DEVICE_ARGS="--target-device iphone" + ;; + 2) + TARGET_DEVICE_ARGS="--target-device ipad" + ;; + *) + TARGET_DEVICE_ARGS="--target-device mac" + ;; +esac + +realpath() { + DIRECTORY="$(cd "${1%/*}" && pwd)" + FILENAME="${1##*/}" + echo "$DIRECTORY/$FILENAME" +} + +install_resource() +{ + if [[ "$1" = /* ]] ; then + RESOURCE_PATH="$1" + else + RESOURCE_PATH="${PODS_ROOT}/$1" + fi + if [[ ! -e "$RESOURCE_PATH" ]] ; then + cat << EOM +error: Resource "$RESOURCE_PATH" not found. Run 'pod install' to update the copy resources script. +EOM + exit 1 + fi + case $RESOURCE_PATH in + *.storyboard) + echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" + ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} + ;; + *.xib) + echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" + ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} + ;; + *.framework) + echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + echo "rsync -av $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + rsync -av "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + ;; + *.xcdatamodel) + echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH"`.mom\"" + xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodel`.mom" + ;; + *.xcdatamodeld) + echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd\"" + xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd" + ;; + *.xcmappingmodel) + echo "xcrun mapc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm\"" + xcrun mapc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm" + ;; + *.xcassets) + ABSOLUTE_XCASSET_FILE=$(realpath "$RESOURCE_PATH") + XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE") + ;; + *) + echo "$RESOURCE_PATH" + echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY" + ;; + esac +} + +mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +if [[ "${ACTION}" == "install" ]] && [[ "${SKIP_INSTALL}" == "NO" ]]; then + mkdir -p "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" + rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +fi +rm -f "$RESOURCES_TO_COPY" + +if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "$XCASSET_FILES" ] +then + # Find all other xcassets (this unfortunately includes those of path pods and other targets). + OTHER_XCASSETS=$(find "$PWD" -iname "*.xcassets" -type d) + while read line; do + if [[ $line != "`realpath $PODS_ROOT`*" ]]; then + XCASSET_FILES+=("$line") + fi + done <<<"$OTHER_XCASSETS" + + printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +fi diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests.debug.xcconfig b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests.debug.xcconfig new file mode 100644 index 0000000000..f0a68f71e6 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests.debug.xcconfig @@ -0,0 +1,9 @@ +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/AFNetworking" +LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/AFNetworking-iOS" +OTHER_CFLAGS = $(inherited) -isystem "${PODS_ROOT}/Headers/Public" -isystem "${PODS_ROOT}/Headers/Public/AFNetworking" +OTHER_LDFLAGS = $(inherited) -ObjC -l"AFNetworking-iOS" -framework "CoreGraphics" -framework "MobileCoreServices" -framework "Security" -framework "SystemConfiguration" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests.release.xcconfig b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests.release.xcconfig new file mode 100644 index 0000000000..f0a68f71e6 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests/Pods-TestingPods-OHHTTPStubs iOS Fmk Tests.release.xcconfig @@ -0,0 +1,9 @@ +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/AFNetworking" +LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/AFNetworking-iOS" +OTHER_CFLAGS = $(inherited) -isystem "${PODS_ROOT}/Headers/Public" -isystem "${PODS_ROOT}/Headers/Public/AFNetworking" +OTHER_LDFLAGS = $(inherited) -ObjC -l"AFNetworking-iOS" -framework "CoreGraphics" -framework "MobileCoreServices" -framework "Security" -framework "SystemConfiguration" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Lib Tests/Pods-TestingPods-OHHTTPStubs iOS Lib Tests-acknowledgements.markdown b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Lib Tests/Pods-TestingPods-OHHTTPStubs iOS Lib Tests-acknowledgements.markdown new file mode 100644 index 0000000000..aa247077f4 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Lib Tests/Pods-TestingPods-OHHTTPStubs iOS Lib Tests-acknowledgements.markdown @@ -0,0 +1,26 @@ +# Acknowledgements +This application makes use of the following third party libraries: + +## AFNetworking + +Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Generated by CocoaPods - https://cocoapods.org diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Lib Tests/Pods-TestingPods-OHHTTPStubs iOS Lib Tests-acknowledgements.plist b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Lib Tests/Pods-TestingPods-OHHTTPStubs iOS Lib Tests-acknowledgements.plist new file mode 100644 index 0000000000..d26166b217 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Lib Tests/Pods-TestingPods-OHHTTPStubs iOS Lib Tests-acknowledgements.plist @@ -0,0 +1,58 @@ + + + + + PreferenceSpecifiers + + + FooterText + This application makes use of the following third party libraries: + Title + Acknowledgements + Type + PSGroupSpecifier + + + FooterText + Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + License + MIT + Title + AFNetworking + Type + PSGroupSpecifier + + + FooterText + Generated by CocoaPods - https://cocoapods.org + Title + + Type + PSGroupSpecifier + + + StringsTable + Acknowledgements + Title + Acknowledgements + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Lib Tests/Pods-TestingPods-OHHTTPStubs iOS Lib Tests-dummy.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Lib Tests/Pods-TestingPods-OHHTTPStubs iOS Lib Tests-dummy.m new file mode 100644 index 0000000000..7e1b04cd67 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Lib Tests/Pods-TestingPods-OHHTTPStubs iOS Lib Tests-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Pods_TestingPods_OHHTTPStubs_iOS_Lib_Tests : NSObject +@end +@implementation PodsDummy_Pods_TestingPods_OHHTTPStubs_iOS_Lib_Tests +@end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Lib Tests/Pods-TestingPods-OHHTTPStubs iOS Lib Tests-frameworks.sh b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Lib Tests/Pods-TestingPods-OHHTTPStubs iOS Lib Tests-frameworks.sh new file mode 100755 index 0000000000..893c16a631 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Lib Tests/Pods-TestingPods-OHHTTPStubs iOS Lib Tests-frameworks.sh @@ -0,0 +1,84 @@ +#!/bin/sh +set -e + +echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" +mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + +SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" + +install_framework() +{ + if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then + local source="${BUILT_PRODUCTS_DIR}/$1" + elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then + local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" + elif [ -r "$1" ]; then + local source="$1" + fi + + local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + + if [ -L "${source}" ]; then + echo "Symlinked..." + source="$(readlink "${source}")" + fi + + # use filter instead of exclude so missing patterns dont' throw errors + echo "rsync -av --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" + rsync -av --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" + + local basename + basename="$(basename -s .framework "$1")" + binary="${destination}/${basename}.framework/${basename}" + if ! [ -r "$binary" ]; then + binary="${destination}/${basename}" + fi + + # Strip invalid architectures so "fat" simulator / device frameworks work on device + if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then + strip_invalid_archs "$binary" + fi + + # Resign the code if required by the build settings to avoid unstable apps + code_sign_if_enabled "${destination}/$(basename "$1")" + + # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. + if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then + local swift_runtime_libs + swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u && exit ${PIPESTATUS[0]}) + for lib in $swift_runtime_libs; do + echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" + rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" + code_sign_if_enabled "${destination}/${lib}" + done + fi +} + +# Signs a framework with the provided identity +code_sign_if_enabled() { + if [ -n "${EXPANDED_CODE_SIGN_IDENTITY}" -a "${CODE_SIGNING_REQUIRED}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then + # Use the current code_sign_identitiy + echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" + echo "/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS} --preserve-metadata=identifier,entitlements \"$1\"" + /usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS} --preserve-metadata=identifier,entitlements "$1" + fi +} + +# Strip invalid architectures +strip_invalid_archs() { + binary="$1" + # Get architectures for current file + archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | rev)" + stripped="" + for arch in $archs; do + if ! [[ "${VALID_ARCHS}" == *"$arch"* ]]; then + # Strip non-valid architectures in-place + lipo -remove "$arch" -output "$binary" "$binary" || exit 1 + stripped="$stripped $arch" + fi + done + if [[ "$stripped" ]]; then + echo "Stripped $binary of architectures:$stripped" + fi +} + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Lib Tests/Pods-TestingPods-OHHTTPStubs iOS Lib Tests-resources.sh b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Lib Tests/Pods-TestingPods-OHHTTPStubs iOS Lib Tests-resources.sh new file mode 100755 index 0000000000..0a1561528c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Lib Tests/Pods-TestingPods-OHHTTPStubs iOS Lib Tests-resources.sh @@ -0,0 +1,102 @@ +#!/bin/sh +set -e + +mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" + +RESOURCES_TO_COPY=${PODS_ROOT}/resources-to-copy-${TARGETNAME}.txt +> "$RESOURCES_TO_COPY" + +XCASSET_FILES=() + +case "${TARGETED_DEVICE_FAMILY}" in + 1,2) + TARGET_DEVICE_ARGS="--target-device ipad --target-device iphone" + ;; + 1) + TARGET_DEVICE_ARGS="--target-device iphone" + ;; + 2) + TARGET_DEVICE_ARGS="--target-device ipad" + ;; + *) + TARGET_DEVICE_ARGS="--target-device mac" + ;; +esac + +realpath() { + DIRECTORY="$(cd "${1%/*}" && pwd)" + FILENAME="${1##*/}" + echo "$DIRECTORY/$FILENAME" +} + +install_resource() +{ + if [[ "$1" = /* ]] ; then + RESOURCE_PATH="$1" + else + RESOURCE_PATH="${PODS_ROOT}/$1" + fi + if [[ ! -e "$RESOURCE_PATH" ]] ; then + cat << EOM +error: Resource "$RESOURCE_PATH" not found. Run 'pod install' to update the copy resources script. +EOM + exit 1 + fi + case $RESOURCE_PATH in + *.storyboard) + echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" + ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} + ;; + *.xib) + echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" + ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} + ;; + *.framework) + echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + echo "rsync -av $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + rsync -av "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + ;; + *.xcdatamodel) + echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH"`.mom\"" + xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodel`.mom" + ;; + *.xcdatamodeld) + echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd\"" + xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd" + ;; + *.xcmappingmodel) + echo "xcrun mapc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm\"" + xcrun mapc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm" + ;; + *.xcassets) + ABSOLUTE_XCASSET_FILE=$(realpath "$RESOURCE_PATH") + XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE") + ;; + *) + echo "$RESOURCE_PATH" + echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY" + ;; + esac +} + +mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +if [[ "${ACTION}" == "install" ]] && [[ "${SKIP_INSTALL}" == "NO" ]]; then + mkdir -p "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" + rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +fi +rm -f "$RESOURCES_TO_COPY" + +if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "$XCASSET_FILES" ] +then + # Find all other xcassets (this unfortunately includes those of path pods and other targets). + OTHER_XCASSETS=$(find "$PWD" -iname "*.xcassets" -type d) + while read line; do + if [[ $line != "`realpath $PODS_ROOT`*" ]]; then + XCASSET_FILES+=("$line") + fi + done <<<"$OTHER_XCASSETS" + + printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +fi diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Lib Tests/Pods-TestingPods-OHHTTPStubs iOS Lib Tests.debug.xcconfig b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Lib Tests/Pods-TestingPods-OHHTTPStubs iOS Lib Tests.debug.xcconfig new file mode 100644 index 0000000000..f0a68f71e6 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Lib Tests/Pods-TestingPods-OHHTTPStubs iOS Lib Tests.debug.xcconfig @@ -0,0 +1,9 @@ +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/AFNetworking" +LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/AFNetworking-iOS" +OTHER_CFLAGS = $(inherited) -isystem "${PODS_ROOT}/Headers/Public" -isystem "${PODS_ROOT}/Headers/Public/AFNetworking" +OTHER_LDFLAGS = $(inherited) -ObjC -l"AFNetworking-iOS" -framework "CoreGraphics" -framework "MobileCoreServices" -framework "Security" -framework "SystemConfiguration" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Lib Tests/Pods-TestingPods-OHHTTPStubs iOS Lib Tests.release.xcconfig b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Lib Tests/Pods-TestingPods-OHHTTPStubs iOS Lib Tests.release.xcconfig new file mode 100644 index 0000000000..f0a68f71e6 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs iOS Lib Tests/Pods-TestingPods-OHHTTPStubs iOS Lib Tests.release.xcconfig @@ -0,0 +1,9 @@ +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/AFNetworking" +LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/AFNetworking-iOS" +OTHER_CFLAGS = $(inherited) -isystem "${PODS_ROOT}/Headers/Public" -isystem "${PODS_ROOT}/Headers/Public/AFNetworking" +OTHER_LDFLAGS = $(inherited) -ObjC -l"AFNetworking-iOS" -framework "CoreGraphics" -framework "MobileCoreServices" -framework "Security" -framework "SystemConfiguration" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-acknowledgements.markdown b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-acknowledgements.markdown new file mode 100644 index 0000000000..aa247077f4 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-acknowledgements.markdown @@ -0,0 +1,26 @@ +# Acknowledgements +This application makes use of the following third party libraries: + +## AFNetworking + +Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Generated by CocoaPods - https://cocoapods.org diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-acknowledgements.plist b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-acknowledgements.plist new file mode 100644 index 0000000000..d26166b217 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-acknowledgements.plist @@ -0,0 +1,58 @@ + + + + + PreferenceSpecifiers + + + FooterText + This application makes use of the following third party libraries: + Title + Acknowledgements + Type + PSGroupSpecifier + + + FooterText + Copyright (c) 2011–2015 Alamofire Software Foundation (http://alamofire.org/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + License + MIT + Title + AFNetworking + Type + PSGroupSpecifier + + + FooterText + Generated by CocoaPods - https://cocoapods.org + Title + + Type + PSGroupSpecifier + + + StringsTable + Acknowledgements + Title + Acknowledgements + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-dummy.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-dummy.m new file mode 100644 index 0000000000..6e3a120cc9 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Pods_TestingPods_OHHTTPStubs_tvOS_Fmk_Tests : NSObject +@end +@implementation PodsDummy_Pods_TestingPods_OHHTTPStubs_tvOS_Fmk_Tests +@end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-frameworks.sh b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-frameworks.sh new file mode 100755 index 0000000000..893c16a631 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-frameworks.sh @@ -0,0 +1,84 @@ +#!/bin/sh +set -e + +echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" +mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + +SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" + +install_framework() +{ + if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then + local source="${BUILT_PRODUCTS_DIR}/$1" + elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then + local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" + elif [ -r "$1" ]; then + local source="$1" + fi + + local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + + if [ -L "${source}" ]; then + echo "Symlinked..." + source="$(readlink "${source}")" + fi + + # use filter instead of exclude so missing patterns dont' throw errors + echo "rsync -av --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" + rsync -av --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" + + local basename + basename="$(basename -s .framework "$1")" + binary="${destination}/${basename}.framework/${basename}" + if ! [ -r "$binary" ]; then + binary="${destination}/${basename}" + fi + + # Strip invalid architectures so "fat" simulator / device frameworks work on device + if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then + strip_invalid_archs "$binary" + fi + + # Resign the code if required by the build settings to avoid unstable apps + code_sign_if_enabled "${destination}/$(basename "$1")" + + # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. + if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then + local swift_runtime_libs + swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u && exit ${PIPESTATUS[0]}) + for lib in $swift_runtime_libs; do + echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" + rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" + code_sign_if_enabled "${destination}/${lib}" + done + fi +} + +# Signs a framework with the provided identity +code_sign_if_enabled() { + if [ -n "${EXPANDED_CODE_SIGN_IDENTITY}" -a "${CODE_SIGNING_REQUIRED}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then + # Use the current code_sign_identitiy + echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" + echo "/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS} --preserve-metadata=identifier,entitlements \"$1\"" + /usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS} --preserve-metadata=identifier,entitlements "$1" + fi +} + +# Strip invalid architectures +strip_invalid_archs() { + binary="$1" + # Get architectures for current file + archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | rev)" + stripped="" + for arch in $archs; do + if ! [[ "${VALID_ARCHS}" == *"$arch"* ]]; then + # Strip non-valid architectures in-place + lipo -remove "$arch" -output "$binary" "$binary" || exit 1 + stripped="$stripped $arch" + fi + done + if [[ "$stripped" ]]; then + echo "Stripped $binary of architectures:$stripped" + fi +} + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-resources.sh b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-resources.sh new file mode 100755 index 0000000000..0a1561528c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests-resources.sh @@ -0,0 +1,102 @@ +#!/bin/sh +set -e + +mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" + +RESOURCES_TO_COPY=${PODS_ROOT}/resources-to-copy-${TARGETNAME}.txt +> "$RESOURCES_TO_COPY" + +XCASSET_FILES=() + +case "${TARGETED_DEVICE_FAMILY}" in + 1,2) + TARGET_DEVICE_ARGS="--target-device ipad --target-device iphone" + ;; + 1) + TARGET_DEVICE_ARGS="--target-device iphone" + ;; + 2) + TARGET_DEVICE_ARGS="--target-device ipad" + ;; + *) + TARGET_DEVICE_ARGS="--target-device mac" + ;; +esac + +realpath() { + DIRECTORY="$(cd "${1%/*}" && pwd)" + FILENAME="${1##*/}" + echo "$DIRECTORY/$FILENAME" +} + +install_resource() +{ + if [[ "$1" = /* ]] ; then + RESOURCE_PATH="$1" + else + RESOURCE_PATH="${PODS_ROOT}/$1" + fi + if [[ ! -e "$RESOURCE_PATH" ]] ; then + cat << EOM +error: Resource "$RESOURCE_PATH" not found. Run 'pod install' to update the copy resources script. +EOM + exit 1 + fi + case $RESOURCE_PATH in + *.storyboard) + echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" + ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} + ;; + *.xib) + echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" + ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} + ;; + *.framework) + echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + echo "rsync -av $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + rsync -av "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + ;; + *.xcdatamodel) + echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH"`.mom\"" + xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodel`.mom" + ;; + *.xcdatamodeld) + echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd\"" + xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd" + ;; + *.xcmappingmodel) + echo "xcrun mapc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm\"" + xcrun mapc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm" + ;; + *.xcassets) + ABSOLUTE_XCASSET_FILE=$(realpath "$RESOURCE_PATH") + XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE") + ;; + *) + echo "$RESOURCE_PATH" + echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY" + ;; + esac +} + +mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +if [[ "${ACTION}" == "install" ]] && [[ "${SKIP_INSTALL}" == "NO" ]]; then + mkdir -p "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" + rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +fi +rm -f "$RESOURCES_TO_COPY" + +if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "$XCASSET_FILES" ] +then + # Find all other xcassets (this unfortunately includes those of path pods and other targets). + OTHER_XCASSETS=$(find "$PWD" -iname "*.xcassets" -type d) + while read line; do + if [[ $line != "`realpath $PODS_ROOT`*" ]]; then + XCASSET_FILES+=("$line") + fi + done <<<"$OTHER_XCASSETS" + + printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" +fi diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests.debug.xcconfig b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests.debug.xcconfig new file mode 100644 index 0000000000..ed64c4d61c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests.debug.xcconfig @@ -0,0 +1,9 @@ +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/AFNetworking" +LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/AFNetworking-tvOS" +OTHER_CFLAGS = $(inherited) -isystem "${PODS_ROOT}/Headers/Public" -isystem "${PODS_ROOT}/Headers/Public/AFNetworking" +OTHER_LDFLAGS = $(inherited) -ObjC -l"AFNetworking-tvOS" -framework "Security" -framework "SystemConfiguration" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests.release.xcconfig b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests.release.xcconfig new file mode 100644 index 0000000000..ed64c4d61c --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Pods/Target Support Files/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests/Pods-TestingPods-OHHTTPStubs tvOS Fmk Tests.release.xcconfig @@ -0,0 +1,9 @@ +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/AFNetworking" +LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/AFNetworking-tvOS" +OTHER_CFLAGS = $(inherited) -isystem "${PODS_ROOT}/Headers/Public" -isystem "${PODS_ROOT}/Headers/Public/AFNetworking" +OTHER_LDFLAGS = $(inherited) -ObjC -l"AFNetworking-tvOS" -framework "Security" -framework "SystemConfiguration" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/README.md b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/README.md new file mode 100644 index 0000000000..672e7570b6 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/README.md @@ -0,0 +1,231 @@ +OHHTTPStubs +=========== + +[![Platform](http://cocoapod-badges.herokuapp.com/p/OHHTTPStubs/badge.png)](http://cocoadocs.org/docsets/OHHTTPStubs) +[![Language: Swift-2.x/3.x/4.x/5.x](https://img.shields.io/badge/Swift-2.x%2F3.x%2F4.x%2F5.x-orange.svg)](https://swift.org) +[![Build Status](https://travis-ci.org/AliSoftware/OHHTTPStubs.svg?branch=master)](https://travis-ci.org/AliSoftware/OHHTTPStubs) + +[![Version](http://cocoapod-badges.herokuapp.com/v/OHHTTPStubs/badge.png)](http://cocoadocs.org/docsets/OHHTTPStubs) +[![Carthage Supported](https://img.shields.io/badge/carthage-supported-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) +[![Swift Package Manager Supported](https://img.shields.io/badge/spm-supported-4BC51D.svg?style=flat)](https://swift.org/package-manager/) + + +`OHHTTPStubs` is a library designed to stub your network requests very easily. It can help you: + +* test your apps with **fake network data** (stubbed from file) and **simulate slow networks**, to check your application behavior in bad network conditions +* write **unit tests** that use fake network data from your fixtures. + +It works with `NSURLConnection`, `NSURLSession`, `AFNetworking`, `Alamofire` or any networking framework that use Cocoa's URL Loading System. + +[Donate](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=TRTU3UEWEHV92 "Donate") + +---- + +# Documentation & Usage Examples + +`OHHTTPStubs` headers are fully documented using Appledoc-like / Headerdoc-like comments in the header files. You can also [read the **online documentation** here](http://cocoadocs.org/docsets/OHHTTPStubs). + + +## Basic example + +
+In Objective-C + +```objc +[HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request.URL.host isEqualToString:@"mywebservice.com"]; +} withStubResponse:^HTTPStubsResponse*(NSURLRequest *request) { + // Stub it with our "wsresponse.json" stub file (which is in same bundle as self) + NSString* fixture = OHPathForFile(@"wsresponse.json", self.class); + return [HTTPStubsResponse responseWithFileAtPath:fixture + statusCode:200 headers:@{@"Content-Type":@"application/json"}]; +}]; +``` + +
+ +
+In Swift + +This example is using the Swift helpers found in `OHHTTPStubsSwift.swift` provided by the `OHHTTPStubs/Swift` subspec or `OHHTTPStubs` package. + +```swift +stub(condition: isHost("mywebservice.com")) { _ in + // Stub it with our "wsresponse.json" stub file (which is in same bundle as self) + let stubPath = OHPathForFile("wsresponse.json", type(of: self)) + return fixture(filePath: stubPath!, headers: ["Content-Type":"application/json"]) +} +``` + +**Note**: if you're using `OHHTTPStubs`'s Swiftier API (`OHHTTPStubsSwift.swift` and the `Swift` subspec or `OHTTPStubsSwift` package), you can also compose the matcher functions like this: `stub(isScheme("http") && isHost("myhost")) { … }` +
+ +## More examples & Help Topics + +* For a lot more examples, see the dedicated "[Usage Examples](https://github.com/AliSoftware/OHHTTPStubs/wiki/Usage-Examples)" wiki page. +* The wiki also contain [some articles that can help you get started](https://github.com/AliSoftware/OHHTTPStubs/wiki) with (and troubleshoot if needed) `OHHTTPStubs`. + +## Recording requests to replay them later + +Instead of writing the content of the stubs you want to use manually, you can use tools like [SWHttpTrafficRecorder](https://github.com/capitalone/SWHttpTrafficRecorder) to record network requests into files. This way you can later use those files as stub responses. +This tool can record all three formats that are supported by `OHHTTPStubs` (the `HTTPMessage` format, the simple response boby/content file, and the `Mocktail` format). + +_(There are also other ways to perform a similar task, including using `curl -is >foo.response` to generate files compatible with the `HTTPMessage` format, or using other network recording libraries similar to `SWHttpTrafficRecorder`)._ + +# Compatibility + +* `OHHTTPStubs` is compatible with **iOS5+**, **OS X 10.7+**, **tvOS**. +* `OHHTTPStubs` also works with `NSURLSession` as well as any network library wrapping them. +* `OHHTTPStubs` is **fully compatible with Swift 3.x, 4.x and Swift 5.x**. + +_[Nullability annotations](https://developer.apple.com/swift/blog/?id=25) have also been added to the ObjC API to allow a cleaner API when used from Swift even if you don't use the dedicated Swift API wrapper provided by `OHHTTPStubsSwift.swift`._ + +
+Updating to Version 9.0+ + +* All classes dropped the `OH` prefix (`OHHHTTPStubs` -> `HTTPStubs`, `OHHTTPStubsResponse` -> `HTTPStubsResponse`, etc). +* The `OHPathHelpers` class was renamed `HTTPStubsPathHelpers`. +* No method and module names were changed. +
+ + +# Installing in your projects + +## CocoaPods + +Using [CocoaPods](https://guides.cocoapods.org) is the recommended way. + +* If you **intend to use `OHHTTPStubs` from Objective-C only**, add `pod 'OHHTTPStubs'` to your `Podfile`. +* If you **intend to use `OHHTTPStubs` from Swift**, add `pod 'OHHTTPStubs/Swift'` to your `Podfile` instead. + +```ruby +pod 'OHHTTPStubs/Swift' # includes the Default subspec, with support for NSURLSession & JSON, and the Swiftier API wrappers +``` + +### All available subspecs + +`OHHTTPStubs` is split into subspecs so that when using Cocoapods, you can get only what you need, no more, no less. + +* The default subspec includes `NSURLSession`, `JSON`, and `OHPathHelpers` +* The `Swift` subspec adds the Swiftier API to that default subspec +* `HTTPMessage` and `Mocktail` are opt-in subspecs: list them explicitly if you need them +* `OHPathHelpers` doesn't depend on `Core` and can be used independently of `OHHTTPStubs` altogether + +
+List of all the subspecs & their dependencies + +Here's a list of which subspecs are included for each of the different lines you could use in your `Podfile`: + +| Subspec | Core | NSURLSession | JSON | Swift | OHPathHelpers | HTTPMessage | Mocktail | +| --------------------------------- | :--: | :----------: | :--: | :---: | :-----------: | :---------: | :------: | +| `pod 'OHHTTPStubs'` | ✅ | ✅ | ✅ | | ✅ | | | +| `pod 'OHHTTPStubs/Default'` | ✅ | ✅ | ✅ | | ✅ | | | +| `pod 'OHHTTPStubs/Swift'` | ✅ | ✅ | ✅ | ✅ | ✅ | | | +| `pod 'OHHTTPStubs/Core'` | ✅ | | | | | | | +| `pod 'OHHTTPStubs/NSURLSession'` | ✅ | ✅ | | | | | | +| `pod 'OHHTTPStubs/JSON'` | ✅ | | ✅ | | | | | +| `pod 'OHHTTPStubs/OHPathHelpers'` | | | | | ✅ | | | +| `pod 'OHHTTPStubs/HTTPMessage'` | ✅ | | | | | ✅ | | +| `pod 'OHHTTPStubs/Mocktail'` | ✅ | | | | | | ✅ | + +
+ +## Swift Package Manager + +`OHHTTPStubs` is compatible with Swift Package Manager, and provides 2 targets for consumption: `OHHTTPStubs` and `OHHTTPStubsSwift`. + +* `OHHTTPStubs` is equivalent to the `OHHTTPStubs` subspec. +* `OHHTTPStubsSwift` is equivalent to the `OHHTTPStubs/Swift` subspec. + +_Note: We currently do not have support for the HTTPMessage or Mocktail subspecs in Swift Package Manager. If you are interested in these, please open an issue to explain your needs._ + +## Carthage + +`OHHTTPStubs` is also compatible with Carthage. Just add it to your `Cartfile`. + +_Note: The `OHHTTPStubs.framework` built with Carthage will include **all** features of `OHHTTPStubs` turned on (in other words, all subspecs of the pod), including `NSURLSession` and `JSON` support, `OHPathHelpers`, `HTTPMessage` and `Mocktail` support, and the Swiftier API._ + +## Using the right Swift version for your project + +`OHHTTPStubs` supports Swift 3.0 (Xcode 8+), Swift 3.1 (Xcode 8.3+), Swift 3.2 (Xcode 9.0+), Swift 4.0 (Xcode 9.0+), Swift 4.1 (Xcode 9.3+), Swift 4.2 (Xcode 10+), Swift 5.0 (Xcode 10.2), and Swift 5.1 (Xcode 11) however we are only testing Swift 4.x (using Xcode 9.1 and 10.1) and Swift 5.x (using Xcode 10.2 AND 11) in CI. + +Here are some details about the correct setup you need depending on how you integrated `OHHTTPStubs` into your project. + +
+CocoaPods: nothing to do + +If you use CocoaPods version [`1.1.0.beta.1`](https://github.com/CocoaPods/CocoaPods/releases/tag/1.1.0.beta.1) or later, then CocoaPods will compile `OHHTTPStubs` with the right Swift Version matching the one you use for your project automatically. You have nothing to do! 🎉 + +For more info, see [CocoaPods/CocoaPods#5540](https://github.com/CocoaPods/CocoaPods/pull/5540) and [CocoaPods/CocoaPods#5760](https://github.com/CocoaPods/CocoaPods/pull/5760). +
+ +
+Carthage: choose the right version + +The project is set up with `SWIFT_VERSION=5.0` on `master`. + +This means that the framework on `master` will build using: + +* Swift 5.1 on Xcode 11 +* Swift 5.0 on Xcode 10.2 +* Swift 4.2 on Xcode 10.1 +* Swift 4.0 on Xcode 9.1 + +If you want Carthage to build the framework with Swift 3.x you can: + + * either use an older Xcode version + * or use the previous version of `OHHTTPStubs` (6.2.0) — whose `master` branch uses `3.0` + * or fork the repo just to change the `SWIFT_VERSION` build setting to `3.0` + * or build the framework passing a `SWIFT_VERSION` to carthage via `XCODE_XCCONFIG_FILE= carthage build` + +
+ +# Special Considerations + +## Using OHHTTPStubs in your unit tests + +`OHHTTPStubs` is ideal to write unit tests that normally would perform network requests. But if you use it in your unit tests, don't forget to: + +* remove any stubs you installed after each test — to avoid those stubs to still be installed when executing the next Test Case — by calling `[HTTPStubs removeAllStubs]` in your `tearDown` method. [see this wiki page for more info](https://github.com/AliSoftware/OHHTTPStubs/wiki/Remove-stubs-after-each-test) +* be sure to wait until the request has received its response before doing your assertions and letting the test case finish (like for any asynchronous test). [see this wiki page for more info](https://github.com/AliSoftware/OHHTTPStubs/wiki/OHHTTPStubs-and-asynchronous-tests) + +## Automatic loading + +`OHHTTPStubs` is automatically loaded and installed (at the time the library is loaded in memory), both for: + +* requests made using `NSURLConnection` or `[NSURLSession sharedSession]` — [thanks to this code](https://github.com/AliSoftware/OHHTTPStubs/blob/master/OHHTTPStubs/Sources/OHHTTPStubs.m#L107-L113) +* requests made using a `NSURLSession` that was created via `[NSURLSession sessionWithConfiguration:…]` and using either `[NSURLSessionConfiguration defaultSessionConfiguration]` or `[NSURLSessionConfiguration ephemeralSessionConfiguration]` configuration — thanks to [method swizzling](http://nshipster.com/method-swizzling/) done [here in the code](https://github.com/AliSoftware/OHHTTPStubs/blob/master/OHHTTPStubs/Sources/NSURLSession/HTTPStubs+NSURLSessionConfiguration.m). + +If you need to disable (and re-enable) `OHHTTPStubs` — globally or per `NSURLSession` — you can use `[HTTPStubs setEnabled:]` / `[HTTPStubs setEnabled:forSessionConfiguration:]`. + +## Known limitations + +* `OHHTTPStubs` **can't work on background sessions** (sessions created using `[NSURLSessionConfiguration backgroundSessionConfiguration]`) because background sessions don't allow the use of custom `NSURLProtocols` and are handled by the iOS Operating System itself. +* `OHHTTPStubs` don't simulate data upload. The `NSURLProtocolClient` `@protocol` does not provide a way to signal the delegate that data has been **sent** (only that some has been loaded), so any data in the `HTTPBody` or `HTTPBodyStream` of an `NSURLRequest`, or data provided to `-[NSURLSession uploadTaskWithRequest:fromData:];` will be ignored, and more importantly, the `-URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:` delegate method will never be called when you stub the request using `OHHTTPStubs`. +* `OHTTPStubs` **has a known issue with redirects** that we believe is an Apple bug. It has been discussed [here](https://github.com/AliSoftware/OHHTTPStubs/issues/230) and [here](https://github.com/AliSoftware/OHHTTPStubs/issues/280). The actual result of this bug is that redirects with a zero second delay may nondeterministically end up with a null response. + +_As far as I know, there's nothing we can do about those three limitations. Please let me know if you know a solution that would make that possible anyway._ + + +## Submitting to the App Store + +`OHHTTPStubs` **can be used** on apps submitted **on the App Store**. It does not use any private API and nothing prevents you from shipping it. + +But you generally only use stubs during the development phase and want to remove your stubs when submitting to the App Store. So be careful to only include `OHHTTPStubs` when needed (only in your test targets, or only inside `#if DEBUG` sections, or by using [per-Build-Configuration pods](https://guides.cocoapods.org/syntax/podfile.html#pod)) to avoid forgetting to remove it when the time comes that you release for the App Store and you want your requests to hit the real network! + + + +# License and Credits + +This project and library has been created by Olivier Halligon ([@aligatr](https://twitter.com/aligatr) on Twitter) and is under the MIT License. + +It has been inspired by [this article from InfiniteLoop.dk](https://web-beta.archive.org/web/20161219003951/http://www.infinite-loop.dk/blog/2011/09/using-nsurlprotocol-for-injecting-test-data/). + +I would also like to thank: + +* Sébastien Duperron ([@Liquidsoul](https://github.com/Liquidsoul)) for helping me maintaining this library, triaging and responding to issues and PRs +* Kevin Harwood ([@kcharwood](https://github.com/kcharwood)) for migrating the code to `NSInputStream` +* Jinlian Wang ([@JinlianWang](https://github.com/JinlianWang)) for adding Mocktail support +* and everyone else who contributed to this project on GitHub somehow. + +If you want to support the development of this library, feel free to [Donate](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=TRTU3UEWEHV92 "Donate"). Thanks to all contributors so far! diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Rakefile b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Rakefile new file mode 100644 index 0000000000..d62d616463 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Rakefile @@ -0,0 +1,106 @@ +# Build & test OHHTTPStubs lib from the CLI + +desc 'Build an iOS scheme' +task :ios, [:scheme, :device, :ios_version, :action, :additional_args] do |_,args| + destination = "name=#{args.device},OS=#{args.ios_version}" + build("OHHTTPStubs #{args.scheme}", "iphonesimulator", destination, args.action, args.additional_args) +end + +desc 'Build an OSX scheme' +task :osx, [:scheme, :arch, :action, :additional_args] do |_,args| + destination = "arch=#{args.arch}" + build("OHHTTPStubs #{args.scheme}", "macosx", destination, args.action, args.additional_args) +end + +desc 'Build a tvOS scheme' +task :tvos, [:scheme, :tvos_version, :action, :additional_args] do |_,args| + destination = "name=Apple TV,OS=#{args.tvos_version}" + build("OHHTTPStubs #{args.scheme}", "appletvsimulator", destination, args.action, args.additional_args) +end + +desc 'Test Using Swift Package Manager' +task :spm_test do + sh 'swift test -Xcc -DOHHTTPSTUBS_SKIP_REDIRECT_TESTS' +end + +desc 'List installed simulators' +task :simlist do + sh 'xcrun simctl list' +end + +desc 'Build Example Project' +task :build_example_apps do + build_pod_example("Examples/ObjC") + build_pod_example("Examples/Swift") + build_project("Examples/SwiftPackageManager") +end + +desc 'Build Carthage Frameworks' +task :build_carthage_frameworks, [:platform, :swift_version] do |_,args| + puts "Args were: #{args}" + carthage_build(args.platform, args.swift_version) +end + +# Updates Local Pods, Then Builds +def build_pod_example(dir) + sh "pod install --project-directory=#{dir} --verbose" + build_workspace(dir) +end + +# Builds The Example Workspace +def build_workspace(dir) + sh "xcodebuild -workspace #{dir}/OHHTTPStubsDemo.xcworkspace -scheme OHHTTPStubsDemo build CODE_SIGNING_ALLOWED=NO" +end + +# Builds the Example Project +def build_project(dir) + sh "xcodebuild -project #{dir}/OHHTTPStubsDemo.xcodeproj -scheme OHHTTPStubsDemo build CODE_SIGNING_ALLOWED=NO" +end + +# Builds platform using Carthage +def carthage_build(platform, swift_version) + xcconfig = "/tmp/tmp.xcconfig" + config_contents = "SWIFT_VERSION=#{swift_version}" + sh "echo #{config_contents} > #{xcconfig} && XCODE_XCCONFIG_FILE=#{xcconfig} carthage build --platform #{platform} --no-skip-current" +end + +desc 'Run all travis env tasks locally' +task :travis do + require 'YAML' + travis = YAML.load_file('.travis.yml') + travis['matrix']['include'].each do |matrix| + env = matrix['env'] + arg = env.split('=')[1..-1].join('=') + puts "\n" + ('-'*80) + "\n\n" + sh "rake #{arg}" + end +end + + +# Build the xcodebuild command and run it +def build(scheme, sdk, destination, action, additional_args) + puts <<-ANNOUNCE + ============================= + | Action : #{action} + | SDK : #{sdk} + | Scheme : "#{scheme}" + | Destination: #{destination} + | args : "#{additional_args}" + ============================= + + ANNOUNCE + + cmd = %W( + xcodebuild + -workspace OHHTTPStubs.xcworkspace + -scheme "#{scheme}" + -sdk #{sdk} + -configuration Debug + ONLY_ACTIVE_ARCH=NO + #{additional_args} + -destination '#{destination}' + clean #{action} + ) + + sh "set -o pipefail && #{cmd.join(' ')} | xcpretty -c" +end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/HTTPMessage/HTTPStubsResponse+HTTPMessage.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/HTTPMessage/HTTPStubsResponse+HTTPMessage.m new file mode 100644 index 0000000000..061f5c3b89 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/HTTPMessage/HTTPStubsResponse+HTTPMessage.m @@ -0,0 +1,75 @@ +/*********************************************************************************** + * + * Copyright (c) 2012 Olivier Halligon + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + ***********************************************************************************/ + +#if __has_include() || SWIFT_PACKAGE +#import + +#import "HTTPStubsResponse+HTTPMessage.h" + +@implementation HTTPStubsResponse (HTTPMessage) + +#pragma mark Building response from HTTP Message Data (dump from "curl -is") + ++(instancetype)responseWithHTTPMessageData:(NSData*)responseData; +{ + NSData *data = [NSData data]; + NSInteger statusCode = 200; + NSDictionary *headers = @{}; + + CFHTTPMessageRef httpMessage = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, FALSE); + if (httpMessage) + { + CFHTTPMessageAppendBytes(httpMessage, responseData.bytes, responseData.length); + + data = responseData; // By default + + if (CFHTTPMessageIsHeaderComplete(httpMessage)) + { + statusCode = (NSInteger)CFHTTPMessageGetResponseStatusCode(httpMessage); + headers = (__bridge_transfer NSDictionary *)CFHTTPMessageCopyAllHeaderFields(httpMessage); + data = (__bridge_transfer NSData *)CFHTTPMessageCopyBody(httpMessage); + } + CFRelease(httpMessage); + } + + return [self responseWithData:data + statusCode:(int)statusCode + headers:headers]; +} + ++(instancetype)responseNamed:(NSString*)responseName + inBundle:(nullable NSBundle*)responsesBundle +{ + NSURL *responseURL = [responsesBundle?:[NSBundle bundleForClass:self.class] URLForResource:responseName + withExtension:@"response"]; + + NSData *responseData = [NSData dataWithContentsOfURL:responseURL]; + NSAssert(responseData, @"Could not find HTTP response named '%@' in bundle '%@'", responseName, responsesBundle); + + return [self responseWithHTTPMessageData:responseData]; +} + +@end + +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/HTTPMessage/include/HTTPStubsResponse+HTTPMessage.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/HTTPMessage/include/HTTPStubsResponse+HTTPMessage.h new file mode 100644 index 0000000000..ccbb51a445 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/HTTPMessage/include/HTTPStubsResponse+HTTPMessage.h @@ -0,0 +1,79 @@ +/*********************************************************************************** + * + * Copyright (c) 2012 Olivier Halligon + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + ***********************************************************************************/ + + +/* This category is not available on watchOS because CFNetwork is needed for its implementation but isn't available on Nano */ +#if __has_include() + +#import "HTTPStubsResponse.h" +#import "Compatibility.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Adds support for building stubs from "HTTP Messages" conforming to + * the format output by `curl -is` + * + * @note This category is not available on watchOS + */ +@interface HTTPStubsResponse (HTTPMessage) + +/*! @name Building a response from HTTP Message data */ + +// TODO: Try to implement it using NSInputStream someday? + +/** + * Builds a response given a message data as returned by `curl -is [url]`, that is containing both the headers and the body. + * + * This method will split the headers and the body and build a HTTPStubsResponse accordingly + * + * @param responseData The NSData containing the whole HTTP response, including the headers and the body + * + * @return An `HTTPStubsResponse` describing the corresponding response to return by the stub + */ ++(instancetype)responseWithHTTPMessageData:(NSData*)responseData; + +/** + * Builds a response given the name of a `"*.response"` file containing both the headers and the body. + * + * The response file is expected to be in the specified bundle (or the application bundle if nil). + * This method will split the headers and the body and build a HTTPStubsResponse accordingly + * + * @param responseName The name of the `"*.response"` file (without extension) containing the whole + * HTTP response (including the headers and the body) + * @param bundleOrNil The bundle in which the `"*.response"` file is located. If `nil`, the + * `[NSBundle bundleForClass:self.class]` will be used. + * + * @return An `HTTPStubsResponse` describing the corresponding response to return by the stub + */ + ++(instancetype)responseNamed:(NSString*)responseName + inBundle:(nullable NSBundle*)bundleOrNil; + + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/Mocktail/HTTPStubs+Mocktail.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/Mocktail/HTTPStubs+Mocktail.m new file mode 100644 index 0000000000..f59c103efa --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/Mocktail/HTTPStubs+Mocktail.m @@ -0,0 +1,260 @@ +/*********************************************************************************** + * + * Copyright (c) 2015 Jinlian (Sunny) Wang + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + ***********************************************************************************/ + + +//////////////////////////////////////////////////////////////////////////////// + +#import "HTTPStubs+Mocktail.h" + +NSString* const MocktailErrorDomain = @"Mocktail"; + +@implementation HTTPStubs (Mocktail) + + ++(NSArray *)stubRequestsUsingMocktailsAtPath:(NSString *)path inBundle:(nullable NSBundle*)bundleOrNil error:(NSError **)error +{ + NSURL *dirURL = [bundleOrNil?:[NSBundle bundleForClass:self.class] URLForResource:path withExtension:nil]; + if (!dirURL) + { + if (error) + { + *error = [NSError errorWithDomain:MocktailErrorDomain code:OHHTTPStubsMocktailErrorPathDoesNotExist userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Path '%@' does not exist.", path]}]; + } + return nil; + } + + // Make sure path points to a directory + NSNumber *isDirectory; + BOOL success = [dirURL getResourceValue:&isDirectory forKey:NSURLIsDirectoryKey error:nil]; + BOOL isDir = (success && [isDirectory boolValue]); + + if (!isDir) + { + if (error) + { + *error = [NSError errorWithDomain:MocktailErrorDomain code:OHHTTPStubsMocktailErrorPathIsNotFolder userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Path '%@' is not a folder.", path]}]; + } + return nil; + } + + // Read the content of the directory + NSError *bError = nil; + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSArray *fileURLs = [fileManager contentsOfDirectoryAtURL:dirURL includingPropertiesForKeys:nil options:0 error:&bError]; + + if (bError) + { + if (error) + { + *error = [NSError errorWithDomain:MocktailErrorDomain code:OHHTTPStubsMocktailErrorPathFailedToRead userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error reading path '%@'.", dirURL.absoluteString]}]; + } + return nil; + } + + //stub the Mocktail-formatted requests + NSMutableArray *descriptorArray = [[NSMutableArray alloc] initWithCapacity:fileURLs.count]; + for (NSURL *fileURL in fileURLs) + { + if (![fileURL.absoluteString hasSuffix:@".tail"]) + { + continue; + } + id descriptor = [[self class] stubRequestsUsingMocktail:fileURL error: &bError]; + if (descriptor && !bError) + { + [descriptorArray addObject:descriptor]; + } + } + + return descriptorArray; +} + ++(id)stubRequestsUsingMocktailNamed:(NSString *)fileName inBundle:(nullable NSBundle*)bundleOrNil error:(NSError **)error +{ + NSURL *responseURL = [bundleOrNil?:[NSBundle bundleForClass:self.class] URLForResource:fileName withExtension:@"tail"]; + + if (!responseURL) + { + if (error) + { + *error = [NSError errorWithDomain:MocktailErrorDomain code:OHHTTPStubsMocktailErrorPathDoesNotExist userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"File '%@' does not exist.", fileName]}]; + } + return nil; + } + else + { + return [[self class] stubRequestsUsingMocktail:responseURL error:error]; + } +} + ++(id)stubRequestsUsingMocktail:(NSURL *)fileURL error:(NSError **)error +{ + NSError *bError = nil; + NSStringEncoding originalEncoding; + NSString *contentsOfFile = [NSString stringWithContentsOfURL:fileURL usedEncoding:&originalEncoding error:&bError]; + + if (!contentsOfFile || bError) + { + if (error) + { + *error = [NSError errorWithDomain:MocktailErrorDomain code:OHHTTPStubsMocktailErrorPathFailedToRead userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"File '%@' does not read.", fileURL.absoluteString]}]; + } + return nil; + } + + NSScanner *scanner = [NSScanner scannerWithString:contentsOfFile]; + NSString *headerMatter = nil; + [scanner scanUpToString:@"\n\n" intoString:&headerMatter]; + NSArray *lines = [headerMatter componentsSeparatedByString:@"\n"]; + if (lines.count < 4) + { + if (error) + { + *error = [NSError errorWithDomain:MocktailErrorDomain code:OHHTTPStubsMocktailErrorInvalidFileFormat userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"File '%@' has invalid amount of lines:%u.", fileURL.absoluteString, (unsigned)lines.count]}]; + } + return nil; + } + + /* Handle Mocktail format, adapted from Mocktail implementation + For more details on the file format, check out: https://github.com/square/objc-Mocktail */ + NSRegularExpression *methodRegex = [NSRegularExpression regularExpressionWithPattern:lines[0] options:NSRegularExpressionCaseInsensitive error:&bError]; + + if (bError) + { + if (error) + { + *error = [NSError errorWithDomain:MocktailErrorDomain code:OHHTTPStubsMocktailErrorInvalidFileFormat userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"File '%@' has invalid method regular expression pattern: %@.", fileURL.absoluteString, lines[0]]}]; + } + return nil; + } + + NSRegularExpression *absoluteURLRegex = [NSRegularExpression regularExpressionWithPattern:lines[1] options:NSRegularExpressionCaseInsensitive error:&bError]; + + if (bError) + { + if (error) + { + *error = [NSError errorWithDomain:MocktailErrorDomain code:OHHTTPStubsMocktailErrorInvalidFileFormat userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"File '%@' has invalid URL regular expression pattern: %@.", fileURL.absoluteString, lines[1]]}]; + } + return nil; + } + + NSInteger statusCode = [lines[2] integerValue]; + + NSMutableDictionary *headers = @{@"Content-Type":lines[3]}.mutableCopy; + + // From line 4 to '\n\n', expect HTTP response headers. + NSRegularExpression *headerPattern = [NSRegularExpression regularExpressionWithPattern:@"^([^:]+):\\s+(.*)" options:0 error:&bError]; + if (bError) + { + if (error) + { + *error = [NSError errorWithDomain:MocktailErrorDomain code:OHHTTPStubsMocktailErrorInternalError userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Internal error while stubbing file '%@'.", fileURL.absoluteString]}]; + } + return nil; + } + + + // Allow bare Content-Type header on line 4 before named HTTP response headers + NSRegularExpression *bareContentTypePattern = [NSRegularExpression regularExpressionWithPattern:@"^([^:]+)+$" options:0 error:&bError]; + if (bError) + { + if (error) + { + *error = [NSError errorWithDomain:MocktailErrorDomain code:OHHTTPStubsMocktailErrorInternalError userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Internal error while stubbing file '%@'.", fileURL.absoluteString]}]; + } + return nil; + } + + for (NSUInteger line = 3; line < lines.count; line ++) { + NSString *headerLine = lines[line]; + NSTextCheckingResult *match = [headerPattern firstMatchInString:headerLine options:0 range:NSMakeRange(0, headerLine.length)]; + + if (line == 3 && !match) { + match = [bareContentTypePattern firstMatchInString:headerLine options:0 range:NSMakeRange(0, headerLine.length)]; + if (match) { + NSString *key = @"Content-Type"; + NSString *value = [headerLine substringWithRange:[match rangeAtIndex:1]]; + headers[key] = value; + continue; + } + } + + if (match) + { + NSString *key = [headerLine substringWithRange:[match rangeAtIndex:1]]; + NSString *value = [headerLine substringWithRange:[match rangeAtIndex:2]]; + headers[key] = value; + } + else + { + if (error) + { + *error = [NSError errorWithDomain:MocktailErrorDomain code:OHHTTPStubsMocktailErrorInvalidFileHeader userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"File '%@' has invalid header: %@.", fileURL.absoluteString, headerLine]}]; + } + return nil; + } + } + + // Handle binary which is base64 encoded + NSUInteger bodyOffset = [headerMatter dataUsingEncoding:NSUTF8StringEncoding].length + 2; + + return [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + NSString *absoluteURL = (request.URL).absoluteString; + NSString *method = request.HTTPMethod; + + if ([absoluteURLRegex numberOfMatchesInString:absoluteURL options:0 range:NSMakeRange(0, absoluteURL.length)] > 0) + { + if ([methodRegex numberOfMatchesInString:method options:0 range:NSMakeRange(0, method.length)] > 0) + { + return YES; + } + } + + return NO; + } withStubResponse:^HTTPStubsResponse*(NSURLRequest *request) { + if([headers[@"Content-Type"] hasSuffix:@";base64"]) + { + NSString *type = headers[@"Content-Type"]; + NSString *newType = [type substringWithRange:NSMakeRange(0, type.length - 7)]; + headers[@"Content-Type"] = newType; + + NSData *body = [NSData dataWithContentsOfURL:fileURL]; + body = [body subdataWithRange:NSMakeRange(bodyOffset, body.length - bodyOffset)]; + body = [[NSData alloc] initWithBase64EncodedData:body options:NSDataBase64DecodingIgnoreUnknownCharacters]; + + HTTPStubsResponse *response = [HTTPStubsResponse responseWithData:body statusCode:(int)statusCode headers:headers]; + return response; + } + else + { + HTTPStubsResponse *response = [HTTPStubsResponse responseWithFileAtPath:fileURL.path + statusCode:(int)statusCode headers:headers]; + [response.inputStream setProperty:@(bodyOffset) forKey:NSStreamFileCurrentOffsetKey]; + return response; + } + }]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/Mocktail/include/HTTPStubs+Mocktail.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/Mocktail/include/HTTPStubs+Mocktail.h new file mode 100644 index 0000000000..e6ee4c5bc7 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/Mocktail/include/HTTPStubs+Mocktail.h @@ -0,0 +1,100 @@ +/*********************************************************************************** + * + * Copyright (c) 2015 Jinlian (Sunny) Wang + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + ***********************************************************************************/ + + +//////////////////////////////////////////////////////////////////////////////// + +#import "HTTPStubs.h" +#import "Compatibility.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Error codes for the OHHTTPStubs Mocktail category + */ +typedef NS_ENUM(NSInteger, OHHTTPStubsMocktailError) { + /** The specified path does not exist */ + OHHTTPStubsMocktailErrorPathDoesNotExist = 1, + /** The specified path was not readable */ + OHHTTPStubsMocktailErrorPathFailedToRead, + /** The specified path is not a directory */ + OHHTTPStubsMocktailErrorPathIsNotFolder, + /** The specified file is not a valid Mocktail file */ + OHHTTPStubsMocktailErrorInvalidFileFormat, + /** The specified Mocktail file has invalid headers */ + OHHTTPStubsMocktailErrorInvalidFileHeader, + /** An unexpected internal error occured */ + OHHTTPStubsMocktailErrorInternalError +}; + +extern NSString* const MocktailErrorDomain; + +@interface HTTPStubs (Mocktail) + +/** + * Add a stub given a file in the format of Mocktail as defined at https://github.com/square/objc-mocktail. + * + * This method will split the HTTP method Regex, the absolute URL Regex, the headers, the HTTP status code and + * response body, and use them to add a stub. + * + * @param fileName The name of the mocktail file (without extension of '.tail') in the Mocktail format. + * @param bundleOrNil The bundle in which the mocktail file is located. If `nil`, the `[NSBundle bundleForClass:self.class]` will be used. + * @param error An out value that returns any error encountered during stubbing. Returns an NSError object if any error; otherwise returns nil. + * + * @return a stub descriptor that uniquely identifies the stub and can be later used to remove it with + * `removeStub:`. + */ ++(id)stubRequestsUsingMocktailNamed:(NSString *)fileName inBundle:(nullable NSBundle*)bundleOrNil error:(NSError **)error; + +/** + * Add a stub given a file URL in the format of Mocktail as defined at https://github.com/square/objc-mocktail. + * + * This method will split the HTTP method Regex, the absolute URL Regex, the headers, the HTTP status code and + * response body, and use them to add a stub. + * + * @param fileURL The URL pointing to the file in the Mocktail format. + * @param error An out value that returns any error encountered during stubbing. Returns an NSError object if any error; otherwise returns nil. + * + * @return a stub descriptor that uniquely identifies the stub and can be later used to remove it with + * `removeStub:`. + */ ++(id)stubRequestsUsingMocktail:(NSURL *)fileURL error:(NSError **)error; + +/** + * Add stubs using files under a folder in the format of Mocktail as defined at https://github.com/square/objc-mocktail. + * + * This method will retrieve all the files under the folder; for each file with surfix of ".tail", it will split the HTTP method Regex, the absolute URL Regex, the headers, the HTTP status code and response body, and use them to add a stub. + * + * @param path The name of the folder containing files in the Mocktail format. + * @param bundleOrNil The bundle in which the path is located. If `nil`, the `[NSBundle bundleForClass:self.class]` will be used. + * @param error An out value that returns any error encountered during stubbing. Returns an NSError object if any error; otherwise returns nil. + * + * @return an array of stub descriptor that uniquely identifies the stub and can be later used to remove it with + * `removeStub:`. + */ ++(NSArray *)stubRequestsUsingMocktailsAtPath:(NSString *)path inBundle:(nullable NSBundle*)bundleOrNil error:(NSError **)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/HTTPStubs+NSURLSessionConfiguration.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/HTTPStubs+NSURLSessionConfiguration.m new file mode 100644 index 0000000000..3a9532ae53 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/HTTPStubs+NSURLSessionConfiguration.m @@ -0,0 +1,78 @@ +/*********************************************************************************** + * + * Copyright (c) 2012 Olivier Halligon + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + ***********************************************************************************/ + +#import + +#if defined(__IPHONE_7_0) || defined(__MAC_10_9) +#import "HTTPStubs.h" +#import "HTTPStubsMethodSwizzling.h" + +////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This helper is used to swizzle NSURLSessionConfiguration constructor methods + * defaultSessionConfiguration and ephemeralSessionConfiguration to insert the private + * HTTPStubsProtocol into their protocolClasses array so that OHHTTPStubs is automagically + * supported when you create a new NSURLSession based on one of there configurations. + */ + +typedef NSURLSessionConfiguration*(*SessionConfigConstructor)(id,SEL); +static SessionConfigConstructor orig_defaultSessionConfiguration; +static SessionConfigConstructor orig_ephemeralSessionConfiguration; + +static NSURLSessionConfiguration* HTTPStubs_defaultSessionConfiguration(id self, SEL _cmd) +{ + NSURLSessionConfiguration* config = orig_defaultSessionConfiguration(self,_cmd); // call original method + [HTTPStubs setEnabled:YES forSessionConfiguration:config]; //OHHTTPStubsAddProtocolClassToNSURLSessionConfiguration(config); + return config; +} + +static NSURLSessionConfiguration* HTTPStubs_ephemeralSessionConfiguration(id self, SEL _cmd) +{ + NSURLSessionConfiguration* config = orig_ephemeralSessionConfiguration(self,_cmd); // call original method + [HTTPStubs setEnabled:YES forSessionConfiguration:config]; //OHHTTPStubsAddProtocolClassToNSURLSessionConfiguration(config); + return config; +} + +@interface NSURLSessionConfiguration(HTTPStubsSupport) @end + +@implementation NSURLSessionConfiguration(HTTPStubsSupport) + ++(void)load +{ + orig_defaultSessionConfiguration = (SessionConfigConstructor)HTTPStubsReplaceMethod(@selector(defaultSessionConfiguration), + (IMP)HTTPStubs_defaultSessionConfiguration, + [NSURLSessionConfiguration class], + YES); + orig_ephemeralSessionConfiguration = (SessionConfigConstructor)HTTPStubsReplaceMethod(@selector(ephemeralSessionConfiguration), + (IMP)HTTPStubs_ephemeralSessionConfiguration, + [NSURLSessionConfiguration class], + YES); +} + +@end + +#endif /* __IPHONE_7_0 || __MAC_10_9 */ + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/HTTPStubs.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/HTTPStubs.m new file mode 100644 index 0000000000..a423123d3d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/HTTPStubs.m @@ -0,0 +1,658 @@ +/*********************************************************************************** + * + * Copyright (c) 2012 Olivier Halligon + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + ***********************************************************************************/ + +#if ! __has_feature(objc_arc) +#error This file is expected to be compiled with ARC turned ON +#endif + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Imports + +#import "HTTPStubs.h" + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Types & Constants + +@interface HTTPStubsProtocol : NSURLProtocol @end + +static NSTimeInterval const kSlotTime = 0.25; // Must be >0. We will send a chunk of the data from the stream each 'slotTime' seconds + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Private Interfaces + +@interface HTTPStubs() ++ (instancetype)sharedInstance; +@property(atomic, copy) NSMutableArray* stubDescriptors; +@property(atomic, assign) BOOL enabledState; +@property(atomic, copy, nullable) void (^onStubActivationBlock)(NSURLRequest*, id, HTTPStubsResponse*); +@property(atomic, copy, nullable) void (^onStubRedirectBlock)(NSURLRequest*, NSURLRequest*, id, HTTPStubsResponse*); +@property(atomic, copy, nullable) void (^afterStubFinishBlock)(NSURLRequest*, id, HTTPStubsResponse*, NSError*); +@property(atomic, copy, nullable) void (^onStubMissingBlock)(NSURLRequest*); +@end + +@interface HTTPStubsDescriptor : NSObject +@property(atomic, copy) HTTPStubsTestBlock testBlock; +@property(atomic, copy) HTTPStubsResponseBlock responseBlock; +@end + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - HTTPStubsDescriptor Implementation + +@implementation HTTPStubsDescriptor + +@synthesize name = _name; + ++(instancetype)stubDescriptorWithTestBlock:(HTTPStubsTestBlock)testBlock + responseBlock:(HTTPStubsResponseBlock)responseBlock +{ + HTTPStubsDescriptor* stub = [HTTPStubsDescriptor new]; + stub.testBlock = testBlock; + stub.responseBlock = responseBlock; + return stub; +} + +-(NSString*)description +{ + return [NSString stringWithFormat:@"<%@ %p : %@>", self.class, self, self.name]; +} + +@end + + + + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - HTTPStubs Implementation + +@implementation HTTPStubs + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Singleton methods + ++ (instancetype)sharedInstance +{ + static HTTPStubs *sharedInstance = nil; + + static dispatch_once_t predicate; + dispatch_once(&predicate, ^{ + sharedInstance = [[self alloc] init]; + }); + + return sharedInstance; +} + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Setup & Teardown + ++ (void)initialize +{ + if (self == [HTTPStubs class]) + { + [self _setEnable:YES]; + } +} +- (instancetype)init +{ + self = [super init]; + if (self) + { + _stubDescriptors = [NSMutableArray array]; + _enabledState = YES; // assume initialize has already been run + } + return self; +} + +- (void)dealloc +{ + [self.class _setEnable:NO]; +} + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Public class methods + +#pragma mark > Adding & Removing stubs + ++(id)stubRequestsPassingTest:(HTTPStubsTestBlock)testBlock + withStubResponse:(HTTPStubsResponseBlock)responseBlock +{ + HTTPStubsDescriptor* stub = [HTTPStubsDescriptor stubDescriptorWithTestBlock:testBlock + responseBlock:responseBlock]; + [HTTPStubs.sharedInstance addStub:stub]; + return stub; +} + ++(BOOL)removeStub:(id)stubDesc +{ + return [HTTPStubs.sharedInstance removeStub:stubDesc]; +} + ++(void)removeAllStubs +{ + [HTTPStubs.sharedInstance removeAllStubs]; +} + +#pragma mark > Disabling & Re-Enabling stubs + ++(void)_setEnable:(BOOL)enable +{ + if (enable) + { + [NSURLProtocol registerClass:HTTPStubsProtocol.class]; + } + else + { + [NSURLProtocol unregisterClass:HTTPStubsProtocol.class]; + } +} + ++(void)setEnabled:(BOOL)enabled +{ + [HTTPStubs.sharedInstance setEnabled:enabled]; +} + ++(BOOL)isEnabled +{ + return HTTPStubs.sharedInstance.isEnabled; +} + +#if defined(__IPHONE_7_0) || defined(__MAC_10_9) ++ (void)setEnabled:(BOOL)enable forSessionConfiguration:(NSURLSessionConfiguration*)sessionConfig +{ + // Runtime check to make sure the API is available on this version + if ( [sessionConfig respondsToSelector:@selector(protocolClasses)] + && [sessionConfig respondsToSelector:@selector(setProtocolClasses:)]) + { + NSMutableArray * urlProtocolClasses = [NSMutableArray arrayWithArray:sessionConfig.protocolClasses]; + Class protoCls = HTTPStubsProtocol.class; + if (enable && ![urlProtocolClasses containsObject:protoCls]) + { + [urlProtocolClasses insertObject:protoCls atIndex:0]; + } + else if (!enable && [urlProtocolClasses containsObject:protoCls]) + { + [urlProtocolClasses removeObject:protoCls]; + } + sessionConfig.protocolClasses = urlProtocolClasses; + } + else + { + NSLog(@"[OHHTTPStubs] %@ is only available when running on iOS7+/OSX9+. " + @"Use conditions like 'if ([NSURLSessionConfiguration class])' to only call " + @"this method if the user is running iOS7+/OSX9+.", NSStringFromSelector(_cmd)); + } +} + ++ (BOOL)isEnabledForSessionConfiguration:(NSURLSessionConfiguration *)sessionConfig +{ + // Runtime check to make sure the API is available on this version + if ( [sessionConfig respondsToSelector:@selector(protocolClasses)] + && [sessionConfig respondsToSelector:@selector(setProtocolClasses:)]) + { + NSMutableArray * urlProtocolClasses = [NSMutableArray arrayWithArray:sessionConfig.protocolClasses]; + Class protoCls = HTTPStubsProtocol.class; + return [urlProtocolClasses containsObject:protoCls]; + } + else + { + NSLog(@"[OHHTTPStubs] %@ is only available when running on iOS7+/OSX9+. " + @"Use conditions like 'if ([NSURLSessionConfiguration class])' to only call " + @"this method if the user is running iOS7+/OSX9+.", NSStringFromSelector(_cmd)); + return NO; + } +} +#endif + +#pragma mark > Debug Methods + ++(NSArray*)allStubs +{ + return [HTTPStubs.sharedInstance stubDescriptors]; +} + ++(void)onStubActivation:( nullable void(^)(NSURLRequest* request, id stub, HTTPStubsResponse* responseStub) )block +{ + [HTTPStubs.sharedInstance setOnStubActivationBlock:block]; +} + ++(void)onStubRedirectResponse:( nullable void(^)(NSURLRequest* request, NSURLRequest* redirectRequest, id stub, HTTPStubsResponse* responseStub) )block +{ + [HTTPStubs.sharedInstance setOnStubRedirectBlock:block]; +} + ++(void)afterStubFinish:( nullable void(^)(NSURLRequest* request, id stub, HTTPStubsResponse* responseStub, NSError* error) )block +{ + [HTTPStubs.sharedInstance setAfterStubFinishBlock:block]; +} + ++(void)onStubMissing:( nullable void(^)(NSURLRequest* request) )block +{ + [HTTPStubs.sharedInstance setOnStubMissingBlock:block]; +} + + + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Private instance methods + +-(BOOL)isEnabled +{ + BOOL enabled = NO; + @synchronized(self) + { + enabled = _enabledState; + } + return enabled; +} + +-(void)setEnabled:(BOOL)enable +{ + @synchronized(self) + { + _enabledState = enable; + [self.class _setEnable:_enabledState]; + } +} + +-(void)addStub:(HTTPStubsDescriptor*)stubDesc +{ + @synchronized(_stubDescriptors) + { + [_stubDescriptors addObject:stubDesc]; + } +} + +-(BOOL)removeStub:(id)stubDesc +{ + BOOL handlerFound = NO; + @synchronized(_stubDescriptors) + { + handlerFound = [_stubDescriptors containsObject:stubDesc]; + [_stubDescriptors removeObject:stubDesc]; + } + return handlerFound; +} + +-(void)removeAllStubs +{ + @synchronized(_stubDescriptors) + { + [_stubDescriptors removeAllObjects]; + } +} + +- (HTTPStubsDescriptor*)firstStubPassingTestForRequest:(NSURLRequest*)request +{ + HTTPStubsDescriptor* foundStub = nil; + @synchronized(_stubDescriptors) + { + for(HTTPStubsDescriptor* stub in _stubDescriptors.reverseObjectEnumerator) + { + if (stub.testBlock(request)) + { + foundStub = stub; + break; + } + } + } + return foundStub; +} + +@end + + + + + + + + + + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Private Protocol Class + +@interface HTTPStubsProtocol() +@property(assign) BOOL stopped; +@property(strong) HTTPStubsDescriptor* stub; +@property(assign) CFRunLoopRef clientRunLoop; +- (void)executeOnClientRunLoopAfterDelay:(NSTimeInterval)delayInSeconds block:(dispatch_block_t)block; +@end + +@implementation HTTPStubsProtocol + ++ (BOOL)canInitWithRequest:(NSURLRequest *)request +{ + BOOL found = ([HTTPStubs.sharedInstance firstStubPassingTestForRequest:request] != nil); + if (!found && HTTPStubs.sharedInstance.onStubMissingBlock) { + HTTPStubs.sharedInstance.onStubMissingBlock(request); + } + return found; +} + +- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)response client:(id)client +{ + // Make super sure that we never use a cached response. + HTTPStubsProtocol* proto = [super initWithRequest:request cachedResponse:nil client:client]; + proto.stub = [HTTPStubs.sharedInstance firstStubPassingTestForRequest:request]; + return proto; +} + ++ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request +{ + return request; +} + +- (NSCachedURLResponse *)cachedResponse +{ + return nil; +} + +/** Drop certain headers in accordance with + * https://developer.apple.com/documentation/foundation/urlsessionconfiguration/1411532-httpadditionalheaders + */ +- (NSMutableURLRequest *)clearAuthHeadersForRequest:(NSMutableURLRequest *)request { + NSArray* authHeadersToRemove = @[ + @"Authorization", + @"Connection", + @"Host", + @"Proxy-Authenticate", + @"Proxy-Authorization", + @"WWW-Authenticate" + ]; + for (NSString* header in authHeadersToRemove) { + [request setValue:nil forHTTPHeaderField:header]; + } + return request; +} + +- (void)startLoading +{ + self.clientRunLoop = CFRunLoopGetCurrent(); + NSURLRequest* request = self.request; + id client = self.client; + + if (!self.stub) + { + NSDictionary* userInfo = [NSDictionary dictionaryWithObjectsAndKeys: + @"It seems like the stub has been removed BEFORE the response had time to be sent.", + NSLocalizedFailureReasonErrorKey, + @"For more info, see https://github.com/AliSoftware/OHHTTPStubs/wiki/OHHTTPStubs-and-asynchronous-tests", + NSLocalizedRecoverySuggestionErrorKey, + request.URL, // Stop right here if request.URL is nil + NSURLErrorFailingURLErrorKey, + nil]; + NSError* error = [NSError errorWithDomain:@"OHHTTPStubs" code:500 userInfo:userInfo]; + [client URLProtocol:self didFailWithError:error]; + if (HTTPStubs.sharedInstance.afterStubFinishBlock) + { + HTTPStubs.sharedInstance.afterStubFinishBlock(request, self.stub, nil, error); + } + return; + } + + HTTPStubsResponse* responseStub = self.stub.responseBlock(request); + + if (HTTPStubs.sharedInstance.onStubActivationBlock) + { + HTTPStubs.sharedInstance.onStubActivationBlock(request, self.stub, responseStub); + } + + if (responseStub.error == nil) + { + NSHTTPURLResponse* urlResponse = [[NSHTTPURLResponse alloc] initWithURL:request.URL + statusCode:responseStub.statusCode + HTTPVersion:@"HTTP/1.1" + headerFields:responseStub.httpHeaders]; + + // Cookies handling + if (request.HTTPShouldHandleCookies && request.URL) + { + NSArray* cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:responseStub.httpHeaders forURL:request.URL]; + if (cookies) + { + [NSHTTPCookieStorage.sharedHTTPCookieStorage setCookies:cookies forURL:request.URL mainDocumentURL:request.mainDocumentURL]; + } + } + + + NSString* redirectLocation = (responseStub.httpHeaders)[@"Location"]; + NSURL* redirectLocationURL; + if (redirectLocation) + { + redirectLocationURL = [NSURL URLWithString:redirectLocation]; + } + else + { + redirectLocationURL = nil; + } + [self executeOnClientRunLoopAfterDelay:responseStub.requestTime block:^{ + if (!self.stopped) + { + // Notify if a redirection occurred + if (((responseStub.statusCode > 300) && (responseStub.statusCode < 400)) && redirectLocationURL) + { + NSURLRequest *redirectRequest; + NSMutableURLRequest *mReq; + + switch (responseStub.statusCode) + { + case 301: + case 302: + case 307: + case 308: { + //Preserve the original request method and body, and set the new location URL + mReq = [self.request mutableCopy]; + [mReq setURL:redirectLocationURL]; + + mReq = [self clearAuthHeadersForRequest:mReq]; + + redirectRequest = (NSURLRequest*)[mReq copy]; + break; + } + default: + redirectRequest = [NSURLRequest requestWithURL:redirectLocationURL]; + break; + } + + [client URLProtocol:self wasRedirectedToRequest:redirectRequest redirectResponse:urlResponse]; + if (HTTPStubs.sharedInstance.onStubRedirectBlock) + { + HTTPStubs.sharedInstance.onStubRedirectBlock(request, redirectRequest, self.stub, responseStub); + } + } + + // Send the response (even for redirections) + [client URLProtocol:self didReceiveResponse:urlResponse cacheStoragePolicy:NSURLCacheStorageNotAllowed]; + if(responseStub.inputStream.streamStatus == NSStreamStatusNotOpen) + { + [responseStub.inputStream open]; + } + [self streamDataForClient:client + withStubResponse:responseStub + completion:^(NSError * error) + { + [responseStub.inputStream close]; + NSError *blockError = nil; + if (error==nil) + { + [client URLProtocolDidFinishLoading:self]; + } + else + { + [client URLProtocol:self didFailWithError:responseStub.error]; + blockError = responseStub.error; + } + if (HTTPStubs.sharedInstance.afterStubFinishBlock) + { + HTTPStubs.sharedInstance.afterStubFinishBlock(request, self.stub, responseStub, blockError); + } + }]; + } + }]; + } else { + // Send the canned error + [self executeOnClientRunLoopAfterDelay:responseStub.responseTime block:^{ + if (!self.stopped) + { + [client URLProtocol:self didFailWithError:responseStub.error]; + if (HTTPStubs.sharedInstance.afterStubFinishBlock) + { + HTTPStubs.sharedInstance.afterStubFinishBlock(request, self.stub, responseStub, responseStub.error); + } + } + }]; + } +} + +- (void)stopLoading +{ + self.stopped = YES; +} + +typedef struct { + NSTimeInterval slotTime; + double chunkSizePerSlot; + double cumulativeChunkSize; +} HTTPStubsStreamTimingInfo; + +- (void)streamDataForClient:(id)client + withStubResponse:(HTTPStubsResponse*)stubResponse + completion:(void(^)(NSError * error))completion +{ + if (!self.stopped) + { + if ((stubResponse.dataSize>0) && stubResponse.inputStream.hasBytesAvailable) + { + // Compute timing data once and for all for this stub + + HTTPStubsStreamTimingInfo timingInfo = { + .slotTime = kSlotTime, // Must be >0. We will send a chunk of data from the stream each 'slotTime' seconds + .cumulativeChunkSize = 0 + }; + + if(stubResponse.responseTime < 0) + { + // Bytes send each 'slotTime' seconds = Speed in KB/s * 1000 * slotTime in seconds + timingInfo.chunkSizePerSlot = (fabs(stubResponse.responseTime) * 1000) * timingInfo.slotTime; + } + else if (stubResponse.responseTime < kSlotTime) // includes case when responseTime == 0 + { + // We want to send the whole data quicker than the slotTime, so send it all in one chunk. + timingInfo.chunkSizePerSlot = stubResponse.dataSize; + timingInfo.slotTime = stubResponse.responseTime; + } + else + { + // Bytes send each 'slotTime' seconds = (Whole size in bytes / response time) * slotTime = speed in bps * slotTime in seconds + timingInfo.chunkSizePerSlot = ((stubResponse.dataSize/stubResponse.responseTime) * timingInfo.slotTime); + } + + [self streamDataForClient:client + fromStream:stubResponse.inputStream + timingInfo:timingInfo + completion:completion]; + } + else + { + [self executeOnClientRunLoopAfterDelay:stubResponse.responseTime block:^{ + if (completion && !self.stopped) + { + completion(nil); + } + }]; + } + } +} + +- (void) streamDataForClient:(id)client + fromStream:(NSInputStream*)inputStream + timingInfo:(HTTPStubsStreamTimingInfo)timingInfo + completion:(void(^)(NSError * error))completion +{ + NSParameterAssert(timingInfo.chunkSizePerSlot > 0); + + if (inputStream.hasBytesAvailable && (!self.stopped)) + { + // This is needed in case we computed a non-integer chunkSizePerSlot, to avoid cumulative errors + double cumulativeChunkSizeAfterRead = timingInfo.cumulativeChunkSize + timingInfo.chunkSizePerSlot; + NSUInteger chunkSizeToRead = floor(cumulativeChunkSizeAfterRead) - floor(timingInfo.cumulativeChunkSize); + timingInfo.cumulativeChunkSize = cumulativeChunkSizeAfterRead; + + if (chunkSizeToRead == 0) + { + // Nothing to read at this pass, but probably later + [self executeOnClientRunLoopAfterDelay:timingInfo.slotTime block:^{ + [self streamDataForClient:client fromStream:inputStream + timingInfo:timingInfo completion:completion]; + }]; + } else { + uint8_t* buffer = (uint8_t*)malloc(sizeof(uint8_t)*chunkSizeToRead); + NSInteger bytesRead = [inputStream read:buffer maxLength:chunkSizeToRead]; + if (bytesRead > 0) + { + NSData * data = [NSData dataWithBytes:buffer length:bytesRead]; + // Wait for 'slotTime' seconds before sending the chunk. + // If bytesRead < chunkSizePerSlot (because we are near the EOF), adjust slotTime proportionally to the bytes remaining + [self executeOnClientRunLoopAfterDelay:((double)bytesRead / (double)chunkSizeToRead) * timingInfo.slotTime block:^{ + [client URLProtocol:self didLoadData:data]; + [self streamDataForClient:client fromStream:inputStream + timingInfo:timingInfo completion:completion]; + }]; + } + else + { + if (completion) + { + // Note: We may also arrive here with no error if we were just at the end of the stream (EOF) + // In that case, hasBytesAvailable did return YES (because at the limit of OEF) but nothing were read (because EOF) + // But then in that case inputStream.streamError will be nil so that's cool, we won't return an error anyway + completion(inputStream.streamError); + } + } + free(buffer); + } + } + else + { + if (completion) + { + completion(nil); + } + } +} + +///////////////////////////////////////////// +// Delayed execution utility methods +///////////////////////////////////////////// + +- (void)executeOnClientRunLoopAfterDelay:(NSTimeInterval)delayInSeconds block:(dispatch_block_t)block +{ + dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); + dispatch_after(popTime, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + CFRunLoopPerformBlock(self.clientRunLoop, kCFRunLoopDefaultMode, block); + CFRunLoopWakeUp(self.clientRunLoop); + }); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/HTTPStubsMethodSwizzling.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/HTTPStubsMethodSwizzling.h new file mode 100644 index 0000000000..58eec0c752 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/HTTPStubsMethodSwizzling.h @@ -0,0 +1,51 @@ +/*********************************************************************************** + * + * Copyright (c) 2012 Olivier Halligon, 2016 Sebastian Hagedorn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + ***********************************************************************************/ + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Imports + +#import + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Method Swizzling Helpers + +/** + * Replaces the selector's associated method implementation with the + * given implementation (or adds it, if there was no existing one). + * + * @param selector The selector entry in the dispatch table. + * @param newImpl The implementation that will be associated with + * the given selector. + * @param affectedClass The class whose dispatch table will be altered. + * @param isClassMethod Set to YES if the selector denotes a class + * method, or NO if it is an instance method. + * @return The previous implementation associated with + * the swizzled selector. You should store the + * implementation and call it when overwriting + * the selector. + */ +__attribute__((warn_unused_result)) IMP HTTPStubsReplaceMethod(SEL selector, + IMP newImpl, + Class affectedClass, + BOOL isClassMethod); diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/HTTPStubsMethodSwizzling.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/HTTPStubsMethodSwizzling.m new file mode 100644 index 0000000000..b19ce68df0 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/HTTPStubsMethodSwizzling.m @@ -0,0 +1,47 @@ +/*********************************************************************************** + * + * Copyright (c) 2012 Olivier Halligon, 2016 Sebastian Hagedorn + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + ***********************************************************************************/ + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Imports + +#import "HTTPStubsMethodSwizzling.h" + +////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - Method Swizzling Helpers + +IMP HTTPStubsReplaceMethod(SEL selector, + IMP newImpl, + Class affectedClass, + BOOL isClassMethod) +{ + Method origMethod = isClassMethod ? class_getClassMethod(affectedClass, selector) : class_getInstanceMethod(affectedClass, selector); + IMP origImpl = method_getImplementation(origMethod); + + if (!class_addMethod(isClassMethod ? object_getClass(affectedClass) : affectedClass, selector, newImpl, method_getTypeEncoding(origMethod))) + { + method_setImplementation(origMethod, newImpl); + } + + return origImpl; +} diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/HTTPStubsPathHelpers.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/HTTPStubsPathHelpers.m new file mode 100644 index 0000000000..4d8193b301 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/HTTPStubsPathHelpers.m @@ -0,0 +1,52 @@ +/*********************************************************************************** + * + * Copyright (c) 2012 Olivier Halligon + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + ***********************************************************************************/ + + +#import "HTTPStubsPathHelpers.h" + +NSString* __nullable OHPathForFile(NSString* fileName, Class inBundleForClass) +{ + NSBundle* bundle = [NSBundle bundleForClass:inBundleForClass]; + return OHPathForFileInBundle(fileName, bundle); +} + +NSString* __nullable OHPathForFileInBundle(NSString* fileName, NSBundle* bundle) +{ + return [bundle pathForResource:[fileName stringByDeletingPathExtension] + ofType:[fileName pathExtension]]; +} + +NSString* __nullable OHPathForFileInDocumentsDir(NSString* fileName) +{ + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *basePath = (paths.count > 0) ? paths[0] : nil; + return [basePath stringByAppendingPathComponent:fileName]; +} + +NSBundle* __nullable OHResourceBundle(NSString* bundleBasename, Class inBundleForClass) +{ + NSBundle* classBundle = [NSBundle bundleForClass:inBundleForClass]; + return [NSBundle bundleWithPath:[classBundle pathForResource:bundleBasename + ofType:@"bundle"]]; +} diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/HTTPStubsResponse+JSON.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/HTTPStubsResponse+JSON.m new file mode 100644 index 0000000000..679c903237 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/HTTPStubsResponse+JSON.m @@ -0,0 +1,48 @@ +/*********************************************************************************** + * + * Copyright (c) 2012 Olivier Halligon + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + ***********************************************************************************/ + + +#import "HTTPStubsResponse+JSON.h" + +@implementation HTTPStubsResponse (JSON) + +/*! @name Building a response from JSON objects */ + ++ (instancetype)responseWithJSONObject:(id)jsonObject + statusCode:(int)statusCode + headers:(nullable NSDictionary *)httpHeaders +{ + if (!httpHeaders[@"Content-Type"]) + { + NSMutableDictionary* mutableHeaders = [NSMutableDictionary dictionaryWithDictionary:httpHeaders]; + mutableHeaders[@"Content-Type"] = @"application/json"; + httpHeaders = [NSDictionary dictionaryWithDictionary:mutableHeaders]; // make immutable again + } + + return [self responseWithData:[NSJSONSerialization dataWithJSONObject:jsonObject options:0 error:nil] + statusCode:statusCode + headers:httpHeaders]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/HTTPStubsResponse.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/HTTPStubsResponse.m new file mode 100644 index 0000000000..fb9b9808ae --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/HTTPStubsResponse.m @@ -0,0 +1,220 @@ +/*********************************************************************************** + * + * Copyright (c) 2012 Olivier Halligon + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + ***********************************************************************************/ + +#if ! __has_feature(objc_arc) +#error This file is expected to be compiled with ARC turned ON +#endif + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Imports + +#import "HTTPStubsResponse.h" + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Defines & Constants +const double OHHTTPStubsDownloadSpeed1KBPS =- 8 / 8; // kbps -> KB/s +const double OHHTTPStubsDownloadSpeedSLOW =- 12 / 8; // kbps -> KB/s +const double OHHTTPStubsDownloadSpeedGPRS =- 56 / 8; // kbps -> KB/s +const double OHHTTPStubsDownloadSpeedEDGE =- 128 / 8; // kbps -> KB/s +const double OHHTTPStubsDownloadSpeed3G =- 3200 / 8; // kbps -> KB/s +const double OHHTTPStubsDownloadSpeed3GPlus =- 7200 / 8; // kbps -> KB/s +const double OHHTTPStubsDownloadSpeedWifi =- 12000 / 8; // kbps -> KB/s + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Implementation + +@implementation HTTPStubsResponse + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Commodity Constructors + + +#pragma mark > Building response from NSData + ++(instancetype)responseWithData:(NSData*)data + statusCode:(int)statusCode + headers:(nullable NSDictionary*)httpHeaders +{ + HTTPStubsResponse* response = [[self alloc] initWithData:data + statusCode:statusCode + headers:httpHeaders]; + return response; +} + + +#pragma mark > Building response from a file + ++(instancetype)responseWithFileAtPath:(NSString *)filePath + statusCode:(int)statusCode + headers:(nullable NSDictionary *)httpHeaders +{ + HTTPStubsResponse* response = [[self alloc] initWithFileAtPath:filePath + statusCode:statusCode + headers:httpHeaders]; + return response; +} + ++(instancetype)responseWithFileURL:(NSURL *)fileURL + statusCode:(int)statusCode + headers:(nullable NSDictionary *)httpHeaders +{ + HTTPStubsResponse* response = [[self alloc] initWithFileURL:fileURL + statusCode:statusCode + headers:httpHeaders]; + return response; +} + +#pragma mark > Building an error response + ++(instancetype)responseWithError:(NSError*)error +{ + HTTPStubsResponse* response = [[self alloc] initWithError:error]; + return response; +} + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Commotidy Setters + +-(instancetype)responseTime:(NSTimeInterval)responseTime +{ + _responseTime = responseTime; + return self; +} + +-(instancetype)requestTime:(NSTimeInterval)requestTime responseTime:(NSTimeInterval)responseTime +{ + _requestTime = requestTime; + _responseTime = responseTime; + return self; +} + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Initializers + +-(instancetype)init +{ + self = [super init]; + return self; +} + +-(instancetype)initWithInputStream:(NSInputStream*)inputStream + dataSize:(unsigned long long)dataSize + statusCode:(int)statusCode + headers:(nullable NSDictionary*)httpHeaders +{ + self = [super init]; + if (self) + { + _inputStream = inputStream; + _dataSize = dataSize; + _statusCode = statusCode; + NSMutableDictionary * headers = [NSMutableDictionary dictionaryWithDictionary:httpHeaders]; + static NSString *const ContentLengthHeader = @"Content-Length"; + if (!headers[ContentLengthHeader]) + { + headers[ContentLengthHeader] = [NSString stringWithFormat:@"%llu",_dataSize]; + } + _httpHeaders = [NSDictionary dictionaryWithDictionary:headers]; + } + return self; +} + +-(instancetype)initWithFileAtPath:(NSString*)filePath + statusCode:(int)statusCode + headers:(nullable NSDictionary*)httpHeaders +{ + NSURL *fileURL = filePath ? [NSURL fileURLWithPath:filePath] : nil; + self = [self initWithFileURL:fileURL + statusCode:statusCode + headers:httpHeaders]; + return self; +} + +-(instancetype)initWithFileURL:(NSURL *)fileURL + statusCode:(int)statusCode + headers:(nullable NSDictionary *)httpHeaders { + if (!fileURL) { + NSLog(@"%s: nil file path. Returning empty data", __PRETTY_FUNCTION__); + return [self initWithInputStream:[NSInputStream inputStreamWithData:[NSData data]] + dataSize:0 + statusCode:statusCode + headers:httpHeaders]; + } + + // [NSURL -isFileURL] is only available on iOS 8+ + NSAssert([fileURL.scheme isEqualToString:NSURLFileScheme], @"%s: Only file URLs may be passed to this method.",__PRETTY_FUNCTION__); + + NSNumber *fileSize; + NSError *error; + const BOOL success __unused = [fileURL getResourceValue:&fileSize forKey:NSURLFileSizeKey error:&error]; + + NSAssert(success && fileSize, @"%s Couldn't get the file size for URL. \ +The URL was: %@. \ +The operation to retrieve the file size was %@. \ +The error associated with that operation was: %@", + __PRETTY_FUNCTION__, fileURL, success ? @"successful" : @"unsuccessful", error); + + return [self initWithInputStream:[NSInputStream inputStreamWithURL:fileURL] + dataSize:[fileSize unsignedLongLongValue] + statusCode:statusCode + headers:httpHeaders]; +} + +-(instancetype)initWithData:(NSData*)data + statusCode:(int)statusCode + headers:(nullable NSDictionary*)httpHeaders +{ + NSInputStream* inputStream = [NSInputStream inputStreamWithData:data?:[NSData data]]; + self = [self initWithInputStream:inputStream + dataSize:data.length + statusCode:statusCode + headers:httpHeaders]; + return self; +} + +-(instancetype)initWithError:(NSError*)error +{ + self = [super init]; + if (self) { + _error = error; + } + return self; +} + +-(NSString*)debugDescription +{ + return [NSString stringWithFormat:@"<%@ %p requestTime:%f responseTime:%f status:%d dataSize:%llu>", + self.class, self, self.requestTime, self.responseTime, self.statusCode, self.dataSize]; +} + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Accessors + +-(void)setRequestTime:(NSTimeInterval)requestTime +{ + NSAssert(requestTime >= 0, @"Invalid Request Time (%f) for OHHTTPStubResponse. Request time must be greater than or equal to zero",requestTime); + _requestTime = requestTime; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/NSURLRequest+HTTPBodyTesting.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/NSURLRequest+HTTPBodyTesting.m new file mode 100644 index 0000000000..ee3a00496a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/NSURLRequest+HTTPBodyTesting.m @@ -0,0 +1,86 @@ +/*********************************************************************************** +* +* Copyright (c) 2016 Sebastian Hagedorn, Felix Lamouroux +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +* THE SOFTWARE. +* +***********************************************************************************/ + +#import "NSURLRequest+HTTPBodyTesting.h" + +#if defined(__IPHONE_7_0) || defined(__MAC_10_9) + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Imports + +#import "HTTPStubsMethodSwizzling.h" + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - NSURLRequest+CustomHTTPBody + +NSString * const OHHTTPStubs_HTTPBodyKey = @"HTTPBody"; + +@implementation NSURLRequest (HTTPBodyTesting) + +- (NSData*)OHHTTPStubs_HTTPBody +{ + return [NSURLProtocol propertyForKey:OHHTTPStubs_HTTPBodyKey inRequest:self]; +} + +@end + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - NSMutableURLRequest+HTTPBodyTesting + +typedef void(*HTTPStubsSetterIMP)(id, SEL, id); +static HTTPStubsSetterIMP orig_setHTTPBody; + +static void OHHTTPStubs_setHTTPBody(id self, SEL _cmd, NSData* HTTPBody) +{ + // store the http body via NSURLProtocol + if (HTTPBody) { + [NSURLProtocol setProperty:HTTPBody forKey:OHHTTPStubs_HTTPBodyKey inRequest:self]; + } else { + // unfortunately resetting does not work properly as the NSURLSession also uses this to reset the property + } + + orig_setHTTPBody(self, _cmd, HTTPBody); +} + +/** + * Swizzles setHTTPBody: in order to maintain a copy of the http body for later + * reference and calls the original implementation. + * + * @warning Should not be used in production, testing only. + */ +@interface NSMutableURLRequest (HTTPBodyTesting) @end + +@implementation NSMutableURLRequest (HTTPBodyTesting) + ++ (void)load +{ + orig_setHTTPBody = (HTTPStubsSetterIMP)HTTPStubsReplaceMethod(@selector(setHTTPBody:), + (IMP)OHHTTPStubs_setHTTPBody, + [NSMutableURLRequest class], + NO); +} + +@end + +#endif /* __IPHONE_7_0 || __MAC_10_9 */ diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/include/Compatibility.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/include/Compatibility.h new file mode 100644 index 0000000000..b41ddda60f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/include/Compatibility.h @@ -0,0 +1,47 @@ +/*********************************************************************************** + * + * Copyright (c) 2012 Olivier Halligon + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + ***********************************************************************************/ + + +/* + * This file allows to keep compatibility with older SDKs which didn't have + * the latest features and associated macros yet. + */ + + +#ifndef NS_DESIGNATED_INITIALIZER + #if __has_attribute(objc_designated_initializer) + #define NS_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer)) + #else + #define NS_DESIGNATED_INITIALIZER + #endif +#endif + +// Allow to use nullability macros and keywords even if not supported yet +#if ! __has_feature(nullability) + #define NS_ASSUME_NONNULL_BEGIN + #define NS_ASSUME_NONNULL_END + #define nullable + #define __nullable + #define __nonnull +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/include/HTTPStubs.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/include/HTTPStubs.h new file mode 100644 index 0000000000..b4d08d368d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/include/HTTPStubs.h @@ -0,0 +1,219 @@ +/*********************************************************************************** + * + * Copyright (c) 2012 Olivier Halligon + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + ***********************************************************************************/ + + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Imports + +#import + +#import "Compatibility.h" +#import "HTTPStubsResponse.h" + +NS_ASSUME_NONNULL_BEGIN + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Types + +typedef BOOL(^HTTPStubsTestBlock)(NSURLRequest* request); +typedef HTTPStubsResponse* __nonnull (^HTTPStubsResponseBlock)( NSURLRequest* request); + +/** + * This opaque type represents an installed stub and is used to uniquely + * identify a stub once it has been created. + * + * This type is returned by the `stubRequestsPassingTest:withStubResponse:` method + * so that you can later reference it and use this reference to remove the stub later. + * + * This type also let you add arbitrary metadata to a stub to differenciate it + * more easily when debugging. + */ +@protocol HTTPStubsDescriptor +/** + * An arbitrary name that you can set and get to describe your stub. + * Use it as your own convenience. + * + * This is especially useful if you dump all installed stubs using `allStubs` + * or if you want to log which stubs are being triggered using `onStubActivation:`. + */ +@property(nonatomic, strong, nullable) NSString* name; +@end + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Interface + +/** + * Stubs Manager. Use this class to add and remove stubs and stub your network requests. + */ +@interface HTTPStubs : NSObject + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Adding & Removing stubs + + + +/** + * Dedicated method to add a stub + * + * @param testBlock Block that should return `YES` if the request passed as parameter + * should be stubbed with the response block, and `NO` if it should + * hit the real world (or be managed by another stub). + * @param responseBlock Block that will return the `HTTPStubsResponse` (response to + * use for stubbing) corresponding to the given request + * + * @return a stub descriptor that uniquely identifies the stub and can be later used to remove it with `removeStub:`. + * + * @note The returned stub descriptor is retained (`__strong` reference) by `HTTPStubs` + * until it is removed (with one of the `removeStub:` / `removeAllStubs` + * methods); it is thus recommended to keep it in a `__weak` storage (and not `__strong`) + * in your app code, to let the stub descriptor be destroyed and let the variable go + * back to `nil` automatically when the stub is removed. + */ ++(id)stubRequestsPassingTest:(HTTPStubsTestBlock)testBlock + withStubResponse:(HTTPStubsResponseBlock)responseBlock; + +/** + * Remove a stub from the list of stubs + * + * @param stubDesc The stub descriptor that has been returned when adding the stub + * using `stubRequestsPassingTest:withStubResponse:` + * + * @return `YES` if the stub has been successfully removed, `NO` if the parameter was + * not a valid stub identifier + */ ++(BOOL)removeStub:(id)stubDesc; + +/** + * Remove all the stubs from the stubs list. + */ ++(void)removeAllStubs; + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Disabling & Re-Enabling stubs + +/** + * Enable or disable the stubs for the shared session or for `NSURLConnection` + * + * @param enabled If `YES`, enables the stubs. If `NO`, disable all the + * stubs and let all the requests hit the real world. + * + * @note HTTPStubs are enabled by default, so there is no need to call + * this method with `YES` for stubs to work, except if you explicitely + * disabled the stubs before. + * + * @note This only affects requests that are further made using `NSURLConnection` + * or using `[NSURLSession sharedSession]`. This does not affect requests + * sent on an `NSURLSession` created using an `NSURLSessionConfiguration`. + */ ++(void)setEnabled:(BOOL)enabled; + +/** + * Whether or not stubs are enabled for the shared session or for `NSURLConnection` + * + * @return If `YES` the stubs are enabled. If `NO` then the stubs are disabled + */ ++(BOOL)isEnabled; + +#if defined(__IPHONE_7_0) || defined(__MAC_10_9) +/** + * Enable or disable the stubs on a given `NSURLSessionConfiguration`. + * + * @param enabled If `YES`, enables the stubs for this `NSURLSessionConfiguration`. + * If `NO`, disable the stubs and let all the requests hit the real world + * @param sessionConfig The NSURLSessionConfiguration on which to enabled/disable the stubs + * + * @note HTTPStubs are enabled by default on newly created `defaultSessionConfiguration` + * and `ephemeralSessionConfiguration`, so there is no need to call this method with + * `YES` for stubs to work. You generally only use this if you want to disable + * `OHTTPStubs` per `NSURLSession` by calling it before building the `NSURLSession` + * with the `NSURLSessionConfiguration`. + * + * @note Important: As usual according to the way `NSURLSessionConfiguration` works, you + * MUST set this property BEFORE creating the `NSURLSession`. Once the `NSURLSession` + * object is created, they use a deep copy of the `NSURLSessionConfiguration` object + * used to create them, so changing the configuration later does not affect already + * created sessions. + */ ++ (void)setEnabled:(BOOL)enabled forSessionConfiguration:(NSURLSessionConfiguration *)sessionConfig; + +/** + * Whether stubs are enabled or disabled on a given `NSURLSessionConfiguration` + * + * @param sessionConfig The NSURLSessionConfiguration on which to enable/disable the stubs + * + * @return If `YES` the stubs are enabled for sessionConfig. If `NO` then the stubs are disabled + */ ++ (BOOL)isEnabledForSessionConfiguration:(NSURLSessionConfiguration *)sessionConfig; +#endif + +#pragma mark - Debug Methods + +/** + * List all the installed stubs + * + * @return An array of `id` objects currently installed. Useful for debug. + */ ++(NSArray*)allStubs; + +/** + * Setup a block to be called each time a stub is triggered. + * + * Useful if you want to log all your requests being stubbed for example and see which stub + * was used to respond to each request. + * + * @param block The block to call each time a request is being stubbed by OHHTTPStubs. + * Set it to `nil` to do nothing. Defaults is `nil`. + */ ++(void)onStubActivation:( nullable void(^)(NSURLRequest* request, id stub, HTTPStubsResponse* responseStub) )block; + +/** + * Setup a block to be called whenever OHHTTPStubs encounters a redirect request. + * + * @param block The block to call each time a redirect request is being stubbed by OHHTTPStubs. + * Set it to `nil` to do nothing. Defaults is `nil`. + */ ++(void)onStubRedirectResponse:( nullable void(^)(NSURLRequest* request, NSURLRequest* redirectRequest, id stub, HTTPStubsResponse* responseStub) )block; + +/** + * Setup a block to be called each time a stub finishes. Useful if stubs take an insignificant amount + * of time to execute (due to low bandwidth or delayed response time). This block may also be called + * if there are errors generated by OHHTTPStubs in the course of executing a network request. + * + * @param block The block to call each time a request is finished being stubbed by OHHTTPStubs. + * Set it to `nil` to do nothing. Defaults is `nil`. + */ ++(void)afterStubFinish:( nullable void(^)(NSURLRequest* request, id stub, HTTPStubsResponse* responseStub, NSError *error) )block; + +/** + * Setup a block to be called whenever OHHTTPStubs encounters a missing stub. + * + * @param block The block to call each time no stub for a request can be found. + * Set it to `nil` to do nothing. Defaults is `nil`. + */ ++(void)onStubMissing:( nullable void(^)(NSURLRequest* request) )block; + +@end + +NS_ASSUME_NONNULL_END + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/include/HTTPStubsPathHelpers.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/include/HTTPStubsPathHelpers.h new file mode 100644 index 0000000000..d924df476d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/include/HTTPStubsPathHelpers.h @@ -0,0 +1,87 @@ +/*********************************************************************************** + * + * Copyright (c) 2012 Olivier Halligon + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + ***********************************************************************************/ + + +#import + +#import "Compatibility.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Useful function to build a path given a file name and a class. + * + * @param fileName The name of the file to get the path to, including file extension + * @param inBundleForClass The class of the caller, used to determine the current bundle + * in which the file is supposed to be located. + * You should typically pass `self.class` (ObjC) or + * `self.dynamicType` (Swift < 3.0) or `type(of: self)` (Swift >= 3.0) when calling this function. + * + * @return The path of the given file in the same bundle as the inBundleForClass class + */ +NSString* __nullable OHPathForFile(NSString* fileName, Class inBundleForClass); + +/** + * Useful function to build a path given a file name and a bundle. + * + * @param fileName The name of the file to get the path to, including file extension + * @param bundle The bundle in which the file is supposed to be located. + * This parameter can't be null. + * + * @return The path of the given file in given bundle + * + * @note You should avoid using `[NSBundle mainBundle]` for the `bundle` parameter, + * as in the context of Unit Tests, this points to the Simulator's bundle, + * not the bundle of the app under test. That's why `nil` is not an acceptable + * value (so you won't expect it to default to the `mainBundle`). + * You should use `[NSBundle bundleForClass:]` instead. + */ +NSString* __nullable OHPathForFileInBundle(NSString* fileName, NSBundle* bundle); + +/** + * Useful function to build a path to a file in the Documents's directory in the + * app sandbox, used by iTunes File Sharing for example. + * + * @param fileName The name of the file to get the path to, including file extension + * + * @return The path of the file in the Documents directory in your App Sandbox + */ +NSString* __nullable OHPathForFileInDocumentsDir(NSString* fileName); + + + +/** + * Useful function to build an NSBundle located in the application's resources simply from its name + * + * @param bundleBasename The base name, without extension (extension is assumed to be ".bundle"). + * @param inBundleForClass The class of the caller, used to determine the current bundle + * in which the file is supposed to be located. + * You should typically pass `self.class` (ObjC) or + * `self.dynamicType` (Swift) when calling this function. + * + * @return The NSBundle object representing the bundle with the given basename located in your application's resources. + */ +NSBundle* __nullable OHResourceBundle(NSString* bundleBasename, Class inBundleForClass); + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/include/HTTPStubsResponse+JSON.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/include/HTTPStubsResponse+JSON.h new file mode 100644 index 0000000000..c65aa7dcd1 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/include/HTTPStubsResponse+JSON.h @@ -0,0 +1,57 @@ +/*********************************************************************************** + * + * Copyright (c) 2012 Olivier Halligon + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + ***********************************************************************************/ + + +#import "HTTPStubsResponse.h" +#import "Compatibility.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Adds convenience methods to manipulate JSON objects directly. + * Pass in an `NSDictionary` or `NSArray` to generate a corresponding JSON output. + */ +@interface HTTPStubsResponse (JSON) + +/** + * Builds a response given a JSON object for the response body, status code, and headers. + * + * @param jsonObject Object representing the response body. + * Typically a `NSDictionary`; may be any object accepted by `+[NSJSONSerialization dataWithJSONObject:options:error:]` + * @param statusCode The HTTP Status Code to use in the response + * @param httpHeaders The HTTP Headers to return in the response + * If a "Content-Type" header is not included, "Content-Type: application/json" will be added. + * + * @return An `HTTPStubsResponse` describing the corresponding response to return by the stub + * + * @note This method typically calls `responseWithData:statusCode:headers:`, passing the serialized JSON + * object as the data parameter and adding the Content-Type header if necessary. + */ ++ (instancetype)responseWithJSONObject:(id)jsonObject + statusCode:(int)statusCode + headers:(nullable NSDictionary *)httpHeaders; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/include/HTTPStubsResponse.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/include/HTTPStubsResponse.h new file mode 100644 index 0000000000..a340a7508e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/include/HTTPStubsResponse.h @@ -0,0 +1,301 @@ +/*********************************************************************************** + * + * Copyright (c) 2012 Olivier Halligon + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + ***********************************************************************************/ + + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Imports + +#import + +#import "Compatibility.h" + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Defines & Constants + +// Non-standard download speeds +extern const double +OHHTTPStubsDownloadSpeed1KBPS, // 1.0 KB per second +OHHTTPStubsDownloadSpeedSLOW; // 1.5 KB per second + +// Standard download speeds. +extern const double +OHHTTPStubsDownloadSpeedGPRS, +OHHTTPStubsDownloadSpeedEDGE, +OHHTTPStubsDownloadSpeed3G, +OHHTTPStubsDownloadSpeed3GPlus, +OHHTTPStubsDownloadSpeedWifi; + + +NS_ASSUME_NONNULL_BEGIN + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Interface + +/** + * Stubs Response. This describes a stubbed response to be returned by the URL Loading System, + * including its HTTP headers, body, statusCode and response time. + */ +@interface HTTPStubsResponse : NSObject + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Properties + +/** + * The headers to use for the fake response + */ +@property(nonatomic, strong, nullable) NSDictionary* httpHeaders; +/** + * The HTTP status code to use for the fake response + */ +@property(nonatomic, assign) int statusCode; +/** + * The inputStream used when sending the response. + * @note You generally don't manipulate this directly. + */ +@property(nonatomic, strong, nullable) NSInputStream* inputStream; +/** + * The size of the fake response body, in bytes. + */ +@property(nonatomic, assign) unsigned long long dataSize; +/** + * The duration to wait before faking receiving the response headers. + * + * Defaults to 0.0. + */ +@property(nonatomic, assign) NSTimeInterval requestTime; +/** + * The duration to use to send the fake response body. + * + * @note if responseTime<0, it is interpreted as a download speed in KBps ( -200 => 200KB/s ) + */ +@property(nonatomic, assign) NSTimeInterval responseTime; +/** + * The fake error to generate to simulate a network error. + * + * If `error` is non-`nil`, the request will result in a failure and no response will be sent. + */ +@property(nonatomic, strong, nullable) NSError* error; + + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Commodity Constructors +/*! @name Commodity */ + +/* -------------------------------------------------------------------------- */ +#pragma mark > Building response from NSData + +/** + * Builds a response given raw data. + * + * @note Internally calls `-initWithInputStream:dataSize:statusCode:headers:` with and inputStream built from the NSData. + * + * @param data The raw data to return in the response + * @param statusCode The HTTP Status Code to use in the response + * @param httpHeaders The HTTP Headers to return in the response + * @return An `HTTPStubsResponse` describing the corresponding response to return by the stub + */ ++(instancetype)responseWithData:(NSData*)data + statusCode:(int)statusCode + headers:(nullable NSDictionary*)httpHeaders; + + +/* -------------------------------------------------------------------------- */ +#pragma mark > Building response from a file + +/** + * Builds a response given a file path, the status code and headers. + * + * @param filePath The file path that contains the response body to return. + * @param statusCode The HTTP Status Code to use in the response + * @param httpHeaders The HTTP Headers to return in the response + * + * @return An `HTTPStubsResponse` describing the corresponding response to return by the stub + * + * @note It is encouraged to use the OHPathHelpers functions & macros to build + * the filePath parameter easily + */ ++(instancetype)responseWithFileAtPath:(NSString *)filePath + statusCode:(int)statusCode + headers:(nullable NSDictionary*)httpHeaders; + + +/** + * Builds a response given a URL, the status code, and headers. + * + * @param fileURL The URL for the data to return in the response + * @param statusCode The HTTP Status Code to use in the response + * @param httpHeaders The HTTP Headers to return in the response + * + * @return An `HTTPStubsResponse` describing the corresponding response to return by the stub + * + * @note This method applies only to URLs that represent file system resources + */ ++(instancetype)responseWithFileURL:(NSURL *)fileURL + statusCode:(int)statusCode + headers:(nullable NSDictionary *)httpHeaders; + +/* -------------------------------------------------------------------------- */ +#pragma mark > Building an error response + +/** + * Builds a response that corresponds to the given error + * + * @param error The error to use in the stubbed response. + * + * @return An `HTTPStubsResponse` describing the corresponding response to return by the stub + * + * @note For example you could use an error like `[NSError errorWithDomain:NSURLErrorDomain code:kCFURLErrorNotConnectedToInternet userInfo:nil]` + */ ++(instancetype)responseWithError:(NSError*)error; + + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Commotidy Setters + +/** + * Set the `responseTime` of the `HTTPStubsResponse` and return `self`. Useful for chaining method calls. + * + * _Usage example:_ + *
return [[HTTPStubsResponse responseWithData:data statusCode:200 headers:nil] responseTime:5.0];
+ * + * @param responseTime If positive, the amount of time used to send the entire response. + * If negative, the rate in KB/s at which to send the response data. + * Useful to simulate slow networks for example. You may use the + * _OHHTTPStubsDownloadSpeed…_ constants here. + * + * @return `self` (= the same `HTTPStubsResponse` that was the target of this method). + * Returning `self` is useful for chaining method calls. + */ +-(instancetype)responseTime:(NSTimeInterval)responseTime; + +/** + * Set both the `requestTime` and the `responseTime` of the `HTTPStubsResponse` at once. + * Useful for chaining method calls. + * + * _Usage example:_ + *
return [[HTTPStubsResponse responseWithData:data statusCode:200 headers:nil]
+ *            requestTime:1.0 responseTime:5.0];
+ * + * @param requestTime The time to wait before the response begins to send. This value must be greater than or equal to zero. + * @param responseTime If positive, the amount of time used to send the entire response. + * If negative, the rate in KB/s at which to send the response data. + * Useful to simulate slow networks for example. You may use the + * _OHHTTPStubsDownloadSpeed…_ constants here. + * + * @return `self` (= the same `HTTPStubsResponse` that was the target of this method). Useful for chaining method calls. + */ +-(instancetype)requestTime:(NSTimeInterval)requestTime responseTime:(NSTimeInterval)responseTime; + + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Initializers +/*! @name Initializers */ + +/** + * Designated empty initializer + * + * @return An empty `HTTPStubsResponse` on which you need to set either an error or a statusCode, httpHeaders, inputStream and dataSize. + * + * @note This is not recommended to use this method directly. You should use `initWithInputStream:dataSize:statusCode:headers:` instead. + */ +-(instancetype)init NS_DESIGNATED_INITIALIZER; + +/** + * Designed initializer. Initialize a response with the given input stream, dataSize, + * statusCode and headers. + * + * @param inputStream The input stream that will provide the data to return in the response + * @param dataSize The size of the data in the stream. + * @param statusCode The HTTP Status Code to use in the response + * @param httpHeaders The HTTP Headers to return in the response + * + * @return An `HTTPStubsResponse` describing the corresponding response to return by the stub + * + * @note You will probably never need to call this method yourself. Prefer the other initializers (that will call this method eventually) + */ +-(instancetype)initWithInputStream:(NSInputStream*)inputStream + dataSize:(unsigned long long)dataSize + statusCode:(int)statusCode + headers:(nullable NSDictionary*)httpHeaders NS_DESIGNATED_INITIALIZER; + + +/** + * Initialize a response with a given file path, statusCode and headers. + * + * @param filePath The file path of the data to return in the response + * @param statusCode The HTTP Status Code to use in the response + * @param httpHeaders The HTTP Headers to return in the response + * + * @return An `HTTPStubsResponse` describing the corresponding response to return by the stub + * + * @note This method simply builds the NSInputStream, compute the file size, and then call `-initWithInputStream:dataSize:statusCode:headers:` + */ +-(instancetype)initWithFileAtPath:(NSString*)filePath + statusCode:(int)statusCode + headers:(nullable NSDictionary*)httpHeaders; + + +/** + * Initialize a response with a given URL, statusCode and headers. + * + * @param fileURL The URL for the data to return in the response + * @param statusCode The HTTP Status Code to use in the response + * @param httpHeaders The HTTP Headers to return in the response + * + * @return An `HTTPStubsResponse` describing the corresponding response to return by the stub + * + * @note This method applies only to URLs that represent file system resources + */ +-(instancetype)initWithFileURL:(NSURL *)fileURL + statusCode:(int)statusCode + headers:(nullable NSDictionary *)httpHeaders; + +/** + * Initialize a response with the given data, statusCode and headers. + * + * @param data The raw data to return in the response + * @param statusCode The HTTP Status Code to use in the response + * @param httpHeaders The HTTP Headers to return in the response + * + * @return An `HTTPStubsResponse` describing the corresponding response to return by the stub + */ +-(instancetype)initWithData:(NSData*)data + statusCode:(int)statusCode + headers:(nullable NSDictionary*)httpHeaders; + + +/** + * Designed initializer. Initialize a response with the given error. + * + * @param error The error to use in the stubbed response. + * + * @return An `HTTPStubsResponse` describing the corresponding response to return by the stub + * + * @note For example you could use an error like `[NSError errorWithDomain:NSURLErrorDomain code:kCFURLErrorNotConnectedToInternet userInfo:nil]` + */ +-(instancetype)initWithError:(NSError*)error NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/include/NSURLRequest+HTTPBodyTesting.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/include/NSURLRequest+HTTPBodyTesting.h new file mode 100644 index 0000000000..44979c41fb --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubs/include/NSURLRequest+HTTPBodyTesting.h @@ -0,0 +1,48 @@ +/*********************************************************************************** +* +* Copyright (c) 2016 Sebastian Hagedorn, Felix Lamouroux +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in +* all copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +* THE SOFTWARE. +* +***********************************************************************************/ + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - Imports + +#import + +// This category is only useful when NSURLSession is present +#if defined(__IPHONE_7_0) || defined(__MAC_10_9) + +//////////////////////////////////////////////////////////////////////////////// +#pragma mark - NSURLRequest+HTTPBodyTesting + +@interface NSURLRequest (HTTPBodyTesting) +/** + * Unfortunately, when sending POST requests (with a body) using NSURLSession, + * by the time the request arrives at OHHTTPStubs, the HTTPBody of the + * NSURLRequest has been reset to nil. + * + * You can use this method to retrieve the HTTPBody for testing and use it to + * conditionally stub your requests. + */ +- (NSData *)OHHTTPStubs_HTTPBody; +@end + +#endif /* __IPHONE_7_0 || __MAC_10_9 */ diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubsSwift/OHHTTPStubsSwift.swift b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubsSwift/OHHTTPStubsSwift.swift new file mode 100644 index 0000000000..66554ec78a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/OHHTTPStubsSwift/OHHTTPStubsSwift.swift @@ -0,0 +1,532 @@ +/*********************************************************************************** + * + * Copyright (c) 2012 Olivier Halligon + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + ***********************************************************************************/ + +/** + * Swift Helpers + */ + +import Foundation +#if SWIFT_PACKAGE +import OHHTTPStubs +#endif + +#if !swift(>=3.0) + extension HTTPStubs { + private class func stubRequests(passingTest: HTTPStubsTestBlock, withStubResponse: HTTPStubsResponseBlock) -> HTTPStubsDescriptor { + return stubRequestsPassingTest(passingTest, withStubResponse: withStubResponse) + } + } + + extension NSURLRequest { + var httpMethod: String? { return HTTPMethod } + var url: NSURL? { return URL } + } + + extension NSURLComponents { + private convenience init?(url: NSURL, resolvingAgainstBaseURL: Bool) { + self.init(URL: url, resolvingAgainstBaseURL: resolvingAgainstBaseURL) + } + } + + private typealias URLRequest = NSURLRequest + + extension URLRequest { + private func value(forHTTPHeaderField key: String) -> String? { + return valueForHTTPHeaderField(key) + } + } + + extension String { + private func contains(string: String) -> Bool { + return rangeOfString(string) != nil + } + } +#else + extension URLRequest { + public var ohhttpStubs_httpBody: Data? { + return (self as NSURLRequest).ohhttpStubs_HTTPBody() + } + } +#endif + + +// MARK: Syntaxic Sugar for OHHTTPStubs + +/** + * Helper to return a `HTTPStubsResponse` given a fixture path, status code and optional headers. + * + * - Parameter filePath: the path of the file fixture to use for the response + * - Parameter status: the status code to use for the response + * - Parameter headers: the HTTP headers to use for the response + * + * - Returns: The `HTTPStubsResponse` instance that will stub with the given status code + * & headers, and use the file content as the response body. + */ +#if swift(>=3.0) + public func fixture(filePath: String, status: Int32 = 200, headers: [AnyHashable: Any]?) -> HTTPStubsResponse { + return HTTPStubsResponse(fileAtPath: filePath, statusCode: status, headers: headers) + } +#else + public func fixture(filePath: String, status: Int32 = 200, headers: [NSObject: AnyObject]?) -> HTTPStubsResponse { + return HTTPStubsResponse(fileAtPath: filePath, statusCode: status, headers: headers) + } +#endif + +/** + * Helper to call the stubbing function in a more concise way? + * + * - Parameter condition: the matcher block that determine if the request will be stubbed + * - Parameter response: the stub reponse to use if the request is stubbed + * + * - Returns: The opaque `HTTPStubsDescriptor` that uniquely identifies the stub + * and can be later used to remove it with `removeStub:` + */ +#if swift(>=3.0) + @discardableResult + public func stub(condition: @escaping HTTPStubsTestBlock, response: @escaping HTTPStubsResponseBlock) -> HTTPStubsDescriptor { + return HTTPStubs.stubRequests(passingTest: condition, withStubResponse: response) + } +#else + public func stub(condition: HTTPStubsTestBlock, response: HTTPStubsResponseBlock) -> HTTPStubsDescriptor { + return HTTPStubs.stubRequests(passingTest: condition, withStubResponse: response) + } +#endif + + + +// MARK: Create HTTPStubsTestBlock matchers + +/** + * Matcher testing that the `NSURLRequest` is using the **GET** `HTTPMethod` + * + * - Returns: a matcher (HTTPStubsTestBlock) that succeeds only if the request + * is using the GET method + */ +public func isMethodGET() -> HTTPStubsTestBlock { + return { $0.httpMethod == "GET" } +} + +/** + * Matcher testing that the `NSURLRequest` is using the **POST** `HTTPMethod` + * + * - Returns: a matcher (HTTPStubsTestBlock) that succeeds only if the request + * is using the POST method + */ +public func isMethodPOST() -> HTTPStubsTestBlock { + return { $0.httpMethod == "POST" } +} + +/** + * Matcher testing that the `NSURLRequest` is using the **PUT** `HTTPMethod` + * + * - Returns: a matcher (HTTPStubsTestBlock) that succeeds only if the request + * is using the PUT method + */ +public func isMethodPUT() -> HTTPStubsTestBlock { + return { $0.httpMethod == "PUT" } +} + +/** + * Matcher testing that the `NSURLRequest` is using the **PATCH** `HTTPMethod` + * + * - Returns: a matcher (HTTPStubsTestBlock) that succeeds only if the request + * is using the PATCH method + */ +public func isMethodPATCH() -> HTTPStubsTestBlock { + return { $0.httpMethod == "PATCH" } +} + +/** + * Matcher testing that the `NSURLRequest` is using the **DELETE** `HTTPMethod` + * + * - Returns: a matcher (HTTPStubsTestBlock) that succeeds only if the request + * is using the DELETE method + */ +public func isMethodDELETE() -> HTTPStubsTestBlock { + return { $0.httpMethod == "DELETE" } +} + +/** + * Matcher testing that the `NSURLRequest` is using the **HEAD** `HTTPMethod` + * + * - Returns: a matcher (HTTPStubsTestBlock) that succeeds only if the request + * is using the HEAD method + */ +public func isMethodHEAD() -> HTTPStubsTestBlock { + return { $0.httpMethod == "HEAD" } +} + +/** + * Matcher for testing an `NSURLRequest`'s **absolute url string**. + * +* e.g. the absolute url string is `https://api.example.com/signin?user=foo&password=123#anchor` in `https://api.example.com/signin?user=foo&password=123#anchor` + * + * - Parameter url: The absolute url string to match + * + * - Returns: a matcher (HTTPStubsTestBlock) that succeeds only if the request + * has the given absolute url + */ +public func isAbsoluteURLString(_ url: String) -> HTTPStubsTestBlock { + return { req in req.url?.absoluteString == url } +} + +/** + * Matcher for testing an `NSURLRequest`'s **scheme**. + * + * e.g. the scheme part is `https` in `https://api.example.com/signin` + * + * - Parameter scheme: The scheme to match + * + * - Returns: a matcher (HTTPStubsTestBlock) that succeeds only if the request + * has the given scheme + */ +public func isScheme(_ scheme: String) -> HTTPStubsTestBlock { + precondition(!scheme.contains("://"), "The scheme part of an URL never contains '://'. Only use strings like 'https' for this value, and not things like 'https://'") + precondition(!scheme.contains("/"), "The scheme part of an URL never contains any slash. Only use strings like 'https' for this value, and not things like 'https://api.example.com/'") + return { req in req.url?.scheme == scheme } +} + +/** + * Matcher for testing an `NSURLRequest`'s **host**. + * + * e.g. the host part is `api.example.com` in `https://api.example.com/signin`. + * + * - Parameter host: The host to match (e.g. 'api.example.com') + * + * - Returns: a matcher (HTTPStubsTestBlock) that succeeds only if the request + * has the given host + */ +public func isHost(_ host: String) -> HTTPStubsTestBlock { + precondition(!host.contains("/"), "The host part of an URL never contains any slash. Only use strings like 'api.example.com' for this value, and not things like 'https://api.example.com/'") + return { req in req.url?.host == host } +} + +/** + * Matcher for testing an `NSURLRequest`'s **path**. + * + * e.g. the path is `/signin` in `https://api.example.com/signin`. + * + * - Parameter path: The path to match + * + * - Returns: a matcher (HTTPStubsTestBlock) that succeeds only if the request + * has exactly the given path + * + * - Note: URL paths are usually absolute and thus starts with a '/' (which you + * should include in the `path` parameter unless you're testing relative URLs) + */ +public func isPath(_ path: String) -> HTTPStubsTestBlock { + return { req in req.url?.path == path } +} + +private func getPath(_ req: URLRequest) -> String? { + #if swift(>=3.0) + return req.url?.path // In Swift 3, path is non-optional + #else + return req.url?.path + #endif +} +/** + * Matcher for testing the start of an `NSURLRequest`'s **path**. + * + * - Parameter path: The path to match + * + * - Returns: a matcher (HTTPStubsTestBlock) that succeeds only if the request's + * path starts with the given string + * + * - Note: URL paths are usually absolute and thus starts with a '/' (which you + * should include in the `path` parameter unless you're testing relative URLs) + */ +public func pathStartsWith(_ path: String) -> HTTPStubsTestBlock { + return { req in getPath(req)?.hasPrefix(path) ?? false } +} + +/** + * Matcher for testing the end of an `NSURLRequest`'s **path**. + * + * - Parameter path: The path to match + * + * - Returns: a matcher (HTTPStubsTestBlock) that succeeds only if the request's + * path ends with the given string + */ +public func pathEndsWith(_ path: String) -> HTTPStubsTestBlock { + return { req in getPath(req)?.hasSuffix(path) ?? false } +} + +/** + * Matcher for testing if the path of an `NSURLRequest` matches a RegEx. + * + * - Parameter regex: The Regular Expression we want the path to match + * + * - Returns: a matcher (HTTPStubsTestBlock) that succeeds only if the request's + * path matches the given regular expression + * + * - Note: URL paths are usually absolute and thus starts with a '/' + */ +public func pathMatches(_ regex: NSRegularExpression) -> HTTPStubsTestBlock { + return { req in + guard let path = getPath(req) else { return false } + let range = NSRange(location: 0, length: path.utf16.count) + #if swift(>=3.0) + return regex.firstMatch(in: path, options: [], range: range) != nil + #else + return regex.firstMatchInString(path, options: [], range: range) != nil + #endif + } +} + +/** + * Matcher for testing if the path of an `NSURLRequest` matches a RegEx. + * + * - Parameter regexString: The Regular Expression string we want the path to match + * - Parameter options: The Regular Expression options to use. + * Defaults to no option. Common option includes e.g. `.caseInsensitive`. + * + * - Returns: a matcher (HTTPStubsTestBlock) that succeeds only if the request's + * path matches the given regular expression + * + * - Note: This is a convenience function building an NSRegularExpression + * and calling pathMatches(…) with it + */ +#if swift(>=3.0) +public func pathMatches(_ regexString: String, options: NSRegularExpression.Options = []) -> HTTPStubsTestBlock { + guard let regex = try? NSRegularExpression(pattern: regexString, options: options) else { + return { _ in false } + } + return pathMatches(regex) +} +#else + public func pathMatches(_ regexString: String, options: NSRegularExpressionOptions = []) -> HTTPStubsTestBlock { + guard let regex = try? NSRegularExpression(pattern: regexString, options: options) else { + return { _ in false } + } + return pathMatches(regex) + } +#endif + +/** + * Matcher for testing an `NSURLRequest`'s **path extension**. + * + * - Parameter ext: The file extension to match (without the dot) + * + * - Returns: a matcher (HTTPStubsTestBlock) that succeeds only if the request path + * ends with the given extension + */ +public func isExtension(_ ext: String) -> HTTPStubsTestBlock { + return { req in req.url?.pathExtension == ext } +} + +/** + * Matcher for testing an `NSURLRequest`'s **query parameters**. + * + * - Parameter params: The dictionary of query parameters to check the presence for + * + * - Returns: a matcher (HTTPStubsTestBlock) that succeeds if the request contains + * the given query parameters with the given value. + * + * - Note: There is a difference between: + * (1) using `[q:""]`, which matches a query parameter "?q=" with an empty value, and + * (2) using `[q:nil]`, which matches a query parameter "?q" without a value at all + */ +@available(iOS 8.0, OSX 10.10, *) +public func containsQueryParams(_ params: [String:String?]) -> HTTPStubsTestBlock { + return { req in + if let url = req.url { + let comps = NSURLComponents(url: url, resolvingAgainstBaseURL: true) + if let queryItems = comps?.queryItems { + for (k,v) in params { + if queryItems.filter({ qi in qi.name == k && qi.value == v }).count == 0 { return false } + } + return true + } + } + return false + } +} + +/** + * Matcher testing that the `NSURLRequest` headers contain a specific key + * - Parameter name: the name of the key to search for in the `NSURLRequest`'s **allHTTPHeaderFields** property + * + * - Returns: a matcher that returns true if the `NSURLRequest`'s headers contain a value for the key name + */ +public func hasHeaderNamed(_ name: String) -> HTTPStubsTestBlock { + return { (req: URLRequest) -> Bool in + return req.value(forHTTPHeaderField: name) != nil + } +} + +/** + * Matcher testing that the `NSURLRequest` headers contain a specific key and the key's value is equal to the parameter value + * - Parameter name: the name of the key to search for in the `NSURLRequest`'s **allHTTPHeaderFields** property + * - Parameter value: the value to compare against the header's value + * + * - Returns: a matcher that returns true if the `NSURLRequest`'s headers contain a value for the key name and it's value + * is equal to the parameter value + */ +public func hasHeaderNamed(_ name: String, value: String) -> HTTPStubsTestBlock { + return { (req: URLRequest) -> Bool in + return req.value(forHTTPHeaderField: name) == value + } +} + +/** + * Matcher testing that the `NSURLRequest` body contain exactly specific data bytes + * - Parameter body: the Data bytes to expect + * + * - Returns: a matcher that returns true if the `NSURLRequest`'s body is exactly the same as the parameter value + */ +#if swift(>=3.0) + public func hasBody(_ body: Data) -> HTTPStubsTestBlock { + return { req in (req as NSURLRequest).ohhttpStubs_HTTPBody() == body } + } +#else + public func hasBody(_ body: NSData) -> HTTPStubsTestBlock { + return { req in req.OHOHHTTPStubs_HTTPBody() == body } + } +#endif + +/** + * Matcher testing that the `NSURLRequest` body contains a JSON object with the same keys and values + * - Parameter jsonObject: the JSON object to expect + * + * - Returns: a matcher that returns true if the `NSURLRequest`'s body contains a JSON object with the same keys and values as the parameter value + */ +#if swift(>=3.0) +public func hasJsonBody(_ jsonObject: [AnyHashable : Any]) -> HTTPStubsTestBlock { + return { req in + guard + let httpBody = req.ohhttpStubs_httpBody, + let jsonBody = (try? JSONSerialization.jsonObject(with: httpBody, options: [])) as? [AnyHashable : Any] + else { + return false + } + return NSDictionary(dictionary: jsonBody).isEqual(to: jsonObject) + } +} +#endif + +#if swift(>=3.0) +/** + * Matcher testing that the `NSURLRequest` content-type is `application/x-www-form-urlencoded` and body contains a query parameter + * + * - Parameter params: The dictionary of query parameters to check the presence for + * + * - Returns: a matcher that returns true if the `NSURLRequest`'s body contains the same query items as the parameter value + */ +@available(iOS 8.0, OSX 10.10, *) +public func hasFormBody(_ params: [String: String?]) -> HTTPStubsTestBlock { + return hasFormBody(params.map(URLQueryItem.init)) +} + +/** + * Matcher testing that the `NSURLRequest` content-type is `application/x-www-form-urlencoded` and body contains a query parameter + * + * - Parameter queryItems: The array of query parameters to check the presence for + * + * - Returns: a matcher that returns true if the `NSURLRequest`'s body contains the same query items as the parameter value + */ +@available(iOS 8.0, OSX 10.10, *) +public func hasFormBody(_ queryItems: [URLQueryItem]) -> HTTPStubsTestBlock { + return { req in + guard + case "application/x-www-form-urlencoded"? = req.value(forHTTPHeaderField: "Content-Type"), + let httpBody = req.ohhttpStubs_httpBody, + let query = String(data: httpBody, encoding: .utf8) + else { return false } + let items: [URLQueryItem] = { + var comps = URLComponents() + comps.percentEncodedQuery = query + return comps.queryItems ?? [] + }() + return items.sorted(by: { $0.name < $1.name }) == queryItems.sorted(by: { $0.name < $1.name }) + } +} + +/** + * Matcher testing that the `NSURLRequest` content-type is `application/x-www-form-urlencoded` and body contains a query parameter + * + * - Parameter queryItems: The variables of query parameters to check the presence for + * + * - Returns: a matcher that returns true if the `NSURLRequest`'s body contains the same query items as the parameter value + */ +@available(iOS 8.0, OSX 10.10, *) +public func hasFormBody(_ queryItems: URLQueryItem...) -> HTTPStubsTestBlock { + return hasFormBody(queryItems) +} +#endif + +// MARK: Operators on HTTPStubsTestBlock + +/** + * Combine different `HTTPStubsTestBlock` matchers with an 'OR' operation. + * + * - Parameter lhs: the first matcher to test + * - Parameter rhs: the second matcher to test + * + * - Returns: a matcher (`HTTPStubsTestBlock`) that succeeds if either of the given matchers succeeds + */ +#if swift(>=3.0) + public func || (lhs: @escaping HTTPStubsTestBlock, rhs: @escaping HTTPStubsTestBlock) -> HTTPStubsTestBlock { + return { req in lhs(req) || rhs(req) } + } +#else + public func || (lhs: HTTPStubsTestBlock, rhs: HTTPStubsTestBlock) -> HTTPStubsTestBlock { + return { req in lhs(req) || rhs(req) } + } +#endif + +/** + * Combine different `HTTPStubsTestBlock` matchers with an 'AND' operation. + * + * - Parameter lhs: the first matcher to test + * - Parameter rhs: the second matcher to test + * + * - Returns: a matcher (`HTTPStubsTestBlock`) that only succeeds if both of the given matchers succeeds + */ +#if swift(>=3.0) + public func && (lhs: @escaping HTTPStubsTestBlock, rhs: @escaping HTTPStubsTestBlock) -> HTTPStubsTestBlock { + return { req in lhs(req) && rhs(req) } + } +#else + public func && (lhs: HTTPStubsTestBlock, rhs: HTTPStubsTestBlock) -> HTTPStubsTestBlock { + return { req in lhs(req) && rhs(req) } + } +#endif + +/** + * Create the opposite of a given `HTTPStubsTestBlock` matcher. + * + * - Parameter expr: the matcher to negate + * + * - Returns: a matcher (HTTPStubsTestBlock) that only succeeds if the expr matcher fails + */ +#if swift(>=3.0) + public prefix func ! (expr: @escaping HTTPStubsTestBlock) -> HTTPStubsTestBlock { + return { req in !expr(req) } + } +#else + public prefix func ! (expr: HTTPStubsTestBlock) -> HTTPStubsTestBlock { + return { req in !expr(req) } + } +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/Supporting Files/OHHTTPStubs Mac-Info.plist b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/Supporting Files/OHHTTPStubs Mac-Info.plist new file mode 100644 index 0000000000..eed967e3ad --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/Supporting Files/OHHTTPStubs Mac-Info.plist @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2014 AliSoftware. All rights reserved. + NSPrincipalClass + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/Supporting Files/OHHTTPStubs iOS-Info.plist b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/Supporting Files/OHHTTPStubs iOS-Info.plist new file mode 100644 index 0000000000..26d57f697d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/Supporting Files/OHHTTPStubs iOS-Info.plist @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSHumanReadableCopyright + Copyright © 2014 AliSoftware. All rights reserved. + NSPrincipalClass + + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/Supporting Files/OHHTTPStubs.h b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/Supporting Files/OHHTTPStubs.h new file mode 100644 index 0000000000..0e10e788d7 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Sources/Supporting Files/OHHTTPStubs.h @@ -0,0 +1,33 @@ +/*********************************************************************************** + * + * Copyright (c) 2012 Olivier Halligon + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + ***********************************************************************************/ + +#import "Compatibility.h" +#import "NSURLRequest+HTTPBodyTesting.h" +#import "HTTPStubs.h" +#import "HTTPStubsResponse.h" +#import "HTTPStubsResponse+JSON.h" +#import "HTTPStubsResponse+HTTPMessage.h" +#import "HTTPStubs+Mocktail.h" +#import "HTTPStubsPathHelpers.h" + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Fixtures/MocktailFolder/cards.tail b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Fixtures/MocktailFolder/cards.tail new file mode 100644 index 0000000000..596a2ecdb4 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Fixtures/MocktailFolder/cards.tail @@ -0,0 +1,19 @@ +GET +.*/cards +200 +application/json + +[ + { + "gift_card_id": "91857d87-481a-490b-8aff-c72614094398", + "card_number": "515676xxxxxx1234", + "amount": "$25.28", + "expiration_date": "2014-04" + }, + { + "gift_card_id": "0578ebe0-015b-11e4-9191-0800200c9a66", + "card_number": "515676xxxxxx2345", + "amount": "$55.38", + "expiration_date": "2014-05" + } +] \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Fixtures/MocktailFolder/login.tail b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Fixtures/MocktailFolder/login.tail new file mode 100644 index 0000000000..1755e71d57 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Fixtures/MocktailFolder/login.tail @@ -0,0 +1,11 @@ +GET|POST +.*/users +200 +application/json + + +{ +"user_id": "happyuser1", +"user_token": "happytoken", +"status": "SUCCESS" +} \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Fixtures/MocktailFolder/logos_ebay.tail b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Fixtures/MocktailFolder/logos_ebay.tail new file mode 100755 index 0000000000..a21bc088c6 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Fixtures/MocktailFolder/logos_ebay.tail @@ -0,0 +1,6 @@ +GET +ebay.png +200 +image/png;base64 + +iVBORw0KGgoAAAANSUhEUgAAAUAAAACgCAYAAAB9o7WcAABkRklEQVR4nO19d5xkVZX/95x7X1V1nu6JPZlhGIYcJDOAggiIYsCIKCwGDLusq+6u4bfBiNldw7ImFFFRURHURVFJkoc4xCEPTOzp7ulcVe+9e87vj/tedfVMD9M9Xd1dg/X9fGqmu6vqvvvuu++8E78HqKGGGmqooYYaaqihhhpqqKGGGmqooYYaaqihhhpqqKGGGmqooYYaaqihhhpqqKGGGmqooYYaaqihhhpqqKGGGmqooYYaaqihhhpqqKGGGmqoIpCqTvccaqihKqAqARFHD3T87gdP9d96ftY0xqrOAjSl82BiV4x7zbKW4645ZPZrX6OqICICUIU3q0KhICUIgRikD239/VWP9d382jrT5FRhFDrpK0hQgMiF8YA5cs5bVy1sPvRW52IYY2c+9NBDb7n00ktf1tHRcfbixYuvPf/88/9jxYoVq51z1k7yvGqoYU+CAkB90PTcbRsvhYUlEA2/M0VykMCmKEN4ovevZ62Y8bLj6oKW2xRKBKo6AaggJJKZGSRFN3DUtes+/9rN+YeQ4Waj4iZn3dLrockvZDSSITOzbnHxlMUf6oECxtj6n/70pz/7whe+cOz5559/5xlnnHHN6tWrZ77lLW+56eKLL37Naaed9qeaAKyhhgRE5ADBPq0nffmIOW/++wc6r25rsDNU1U2pCqiqaAja4m1Dz9s1Xb/78NHz3nabqhgiI1M5j7HAyyCCwjHByP1bf/XxzsJTmJFZ4lRjo4bAk6S4lj+TCMYNOLWnLv6Xy5szcx8GgDVr1lxw8cUXv+zXv/718kceeeT8+++/v33//fe/7xvf+MZzF1100XUrV65cVTOBa6ihDKpiiThe13vXP3734bf+l6VMBFCAxIybkruFAFYjBdfLi5uPfOQ9B/1iFZPdBigBpFOqju4KCijEELELJX/St9e88cZNgw+5HDcagYOSgpRR0ZVLloBAUCiYDIbcABY17l+48OCr9yaxG43h4KMf/Wh3S0vLkx/72McO22+//frOPvvspmuvvRZf//rXL7722mtPyGaz/Vy5WdVQw54PInIKhyUtR31vv7ZTnspLb8Aw4n1dNDUCUAEHx1nT7J4duHv/J3tueQMAiDpGSWGpEsWFBIoYALC2+0/LNwzch6xpgMD5t3USHhvCIPilUCVAKIaEWLXgff9lKbNR4TX2DRs25A466KArAWD+/PlDf/jDH7BkyZLw+OOP//Y+++yjW7duPaMmAGuoYSQUCgNg8Pj2d38zoByEnBI4EX9Tp3kRgZ3GuHvLzz4KICCwU8KUzmGXUALDOoia2zf+5OMAgZQmfYICAUhh2GhBe+3eLcd1HjTzVd9TKKCkAGTRokUD99xzzzkA0NbW1vjhD3/4cmYeALBp9erVs+bNm3dlTQDumaiiO+BFBiUQWEQFS1uOvGJF6ynPFKJeA2JRAKxTIwRZFaqgOm6Onth287KNAw9dRMSAauA/UR1bQOEsiPF4z40XPdt357I60xwJ3OTKFQKICFADEhVHilUL3/czJn5KNTZEZAG4t7/97f965ZVXHrBmzZrlP/vZz7705je/+V8vu+yyV1133XVX3XHHHSvOO++8L9aCIHseSERJAYNJugsMkwNQdQ73yYemOp6qigXxluPbz/vh2m1//qSq8/FG8p+b9JkkWS8BBbY/2op7tv7yI/MbD/w+oD3pFKtACBKRiQG03rHlxx8RDUHUaFXd5B4UClWAmXQw7uF9W08eXNn28kugAoZVGIg4wX777ff9T37yk68855xz1p511ln3rlix4i1r1qwJrr/++plf+9rXvr1gwYK7axrgHgV/DzJTnWGCYYom4wVA9rzgmGJYMOmOb8FHVwFlQI1CDUZ5EZRBBEPGAYq9Z6z6+r6tJ3cWXL9hChQoFzs+B277wxEqIJrIW3KijrKmCQ92/W5ub7hlFpGBqqSSeGqhpX+SM3cGIKzrX/3uJ7bdPD9nW0Q0nvSJpXtT4ZxhSycuuPCnBDyi0ABEAkDZMDnn3Nlnn33+1Vdf/eb6+voH7r///pXt7e23X3PNNce+7GUv+4CI2FoUeA+AqkIBYoI6cZnv3fzsHx/fMri6LsPrRTVLCp2I24UUIAKcqGQNN1548rJft7fUP6QKJqp2TVABJR8f9WIjFUpMEPaiSOD/Z4B4F9qtEqAWUIjGlimbf7rnlg99/+FzvmKpISKigNR5DS3NTVZCkqw8KWdIZOKBuNO+Ztmnfr5q/nveIhoHTDaalIPtEpoEo0GiTplM25Vr//HWuzt+ubIhaBXRSTZ/EzBZGXBdfGjbWX85Z7//PUOhEe342CFVZZ/eVHouiaqyqjIzxzUTeA9Acl8RAFXw0p/e8PRLb75/00mmPiCnKLOGdvMGVIAY0FiixqwJXnPEwufaW+ofUigTqLoFoI8yMqlYQGPACohABAdwYosZYDhtdl+FLoVKSCMXTAGyIN4G0N0AiMlECsGyGau+umLWWe9Z233VvjluFafKkKT6AQCjdI0m6yRNwFm9t+OXbz5q7tu+nDENdyvUEGhybc1R4TNxFEJMRrcMPvamh7qvW5kzzbGq2ClJFSJSpyHluAGrFl74PwAiqFoQxdt9UolIRMSKiBKRU1XDzGDmGABqAnBPQeKcIqhm6zNAc11sGyy8BJwgiMAAIocok0EQUJULPQBeS5NEoyMFTKhAs0BayPV3x+FzF6D4zJkIn+sVt8Fo1MFwvXnE245RLSwTiI4iAIko1w3b+CdwY8A8U2Fncja3V/7ouoVzHupvgG1wZDIMtgIiB3UM53TEM4iGB6zMmapQjhvdxoFHzOPbbvrIgbNe+RaoKMhU6Ahjnonfhqn5TcAdm378D0NxD5qDmew0xlT4JQ2M9ksXHzX7bXcubjrs16LCTLy98CtNmplj5pJiOuKhUROAewQICkmKjoicE8CJjUVJJPWHTHB0IqgTOGcgRNXqGyZvbwqDTAwYBqASdx4kg3e9Mh669ZU6dM+JWlgbSbzesotpe3cZAwJCvDNNRRQtBLw5+RkAEANYHhi0PLUv1nTWUWurg61X5FoEmRaHoEFgs4mrIgbEJVHKCsoCUiIl4J7NPzv0wFmvnGyVc3SorwxUCBMZ6SqsO/yh7t8vqjNNcJr6ISZXByQQQg2lwbTyCQve8zH/t913iNYE4B4Cr6MBiaJSMugqsdV0u3+nKN13DEgDG76SQNUxyDgiAydDx2n/n1ZE2375H/HgDXVc3DjXqSr7otSAmQRsHBnylQjDZbSMZN+PdteQz3ce1iYIECGYIDSnL++g+zctxUCnQoRBMGAbIGgE6lsFDXNj1LXGsBnAiQCOS1pheR3J7miIDo5z3Oye7L9133W9d75rScvR3xONLcPE3v8rZXtkkkCpP9of674tV767v7ilqT5oi0WdJUp1xEoeUqGp4CUFwbpi3G2Pmv/ea+c2rLzB7wnebVdATQDWMIxpz6rYDqlSoc6AyIEsqfQfEXVf/slo6yWnaP7hLIuCDQTGhIEaqwSCOiTR3qRyQsYjbQhadl+ogpUxGBEOmdeNg+fNwv1bmtCUDeGUAQHCPqDQw+hZl0GuKYvG+SGaFsTI1ivUCcQlApiSgMluLoclQ33xEO7Z+suLlrQc/RMC5YFEKZts4QcgWURmsjIQdS2/e+sv3xmYeqiKLb1b8SMmoS319o/TAjcFc3Hc/Au+AiBxSe6+tl2tpk4NNcCruM74EjRGuO2X3y48dszq4roPnIb8w8awiTXICJEhKDKKiBPhV8E5kLebhWFYcPryrWAoRE3JtiZLsBkCG0F+QLH5UYtnb63DlseycJEB57xaSZKk7+0mVB3XmUZ5pPO6g7oLz76ZyKCUdDxFRco+9QW4b8svP7Ut/2wQcC6utNa3AzR5eJBxg66fjpr3tj/OzC2+XjUGwON6um2PmgCsoWqhiAOQdRp3nFF85pwt7uk3vUOLj8BYq8yBFcBCY05SVwDwhATM6HPw/lcLYCg0OLy9GwfPHUQ+ZBjSJCNEoSKesMoIMhkAkaJzLeOZ27LoXx/ABAIxaYLzbuQKEiBQWMpKX9iBe7b++vTkDVak1V+TDWWGifJx7xH3dPzircZkVXVyrcjhgAtp7Io8K7cYx7S/43/htV5DE8wHrwnAGqoT6gKiIJLBO08beuLkX0rXFXPIGBAHgIpVOBDJMB1SpRlH0nEhifPfKxqGFacv7wBAcElgStUb3FCA1f8OAmxWEeWB9asDbHkw6+doABVAVCHjENYlQaBiApPDmo5rXpGPtq1kMhFUpuQ+FhUCER7p/MM/bxx6FFluFMXkZuJI4pRlsIYyQEe1v/32lsy834g6Jpp4bXRNANZQHShjOVGNAyUThX2/PTX/xOnXcuHRQIOsU4hFqcxquwhQmrlTcT9mSvhJYFbkI8Yh7dtwwNxB5CMDTo6bKmAjRJoSrAFMBuh82mDD6izgAA48jeh4EqdTM1MJlOF61zH4eOuD2/707uQ9miwFcLjqQpnAzml05B1bLn+VZQtS4sm2vQmAIUbRDbjZ9StwzNxz/x8AEHEi+iYmwmoCsIaqQGoaqsZMZCPXf92Z0dPn/J6lB2oCkERTnfQ2KkQJWRacsXyrv/n0hY1ZTd7PZIH+DsXG1XVQR2DGuMvZVBWiAiIwscW9HT87F5DFTMZNdghYVZSI8FjXn9/9/MCa+hyaYoFUNtdnFPgqJY5DLQar2i+4tj5ovV7UVSwJvCYAa6gKkAoUzhAFIsUnzig8e96vSQesmoySxgxMkZ9/F2BSDEWMw+d3Y//ZQxiMGfyC6pfXH1UVNksY2MLYtCYLMYLxnhERgUAQFcrZxvi5vnvmPLHt1gsBQCaJtTrRUonJqEJn3bnpsnNJBWCykx78AAAyWnADdlHDIb2Hz3nTpwGAvduzIqgJwBqqA0QgNSKIqfDce79H4eYM2Hrhp4xq4sGLFQjY4bS9t0KVPDnBKBhp4XonoakX9G6w6Hk6B87tfiInw1gnRdzd8ZMPA5jtNaLJYUhQeB/jkz23nvlk3511GdukqjIl+aJKEoco4PgFF3w2Y+tuF3UBKlieWROANVQDSOGgBI06/veH0nf9fARBDFWv+ZEmUdOJ33DDEoIBMgDZ5BUk/5ffErrDN71PijAUEY5c0I39Z+UxFAdgKv90as6XpzwrID5KbQJF5xMBwh4DtmVCcDuumReCqEPOzMBj3TdlNw8+dgiRgU6OACT1kY5g9ebL3+uQh1UjWkpQr+SBRk6fyGgx7g/2ajzy2UNnve7rCiUirigJRE0A1lAFUCJYSNRxdLTlq+9ghpJ6RhagMnpfKYhKLABHkFjVRdA4TF5F/7/E8GQxJlaYkkzTZJZpi0cVRsY4nLFvB3yIYjgircMhC5SKuAGUelkwEIcOnU9aEJWJdUJ5xcquzggGQZyPu3FPxy8+BGCnmuhuQwH1dbZ4buD+Vz+67YZjcmZG7NSZ0gcqeThK2RjhGympOFLCSQsv/JLhoKgqvL2QnChqlSA1TD9UWMlI3PX9T1L4DGAzohqbSm51Xy1Aoi4mAAFl94o5d4By0A6lepBEUO2EFp6FFB6Dur6AiAVsVSFMmgizJPBJRBhMfIErZs7BE5051AeSEG8xlHaWn8uAKGwA9G8JkN8WIdfqIJFJNN0ygbkLiIrNmsb4wa7fnXHCggtf1pyZe4OqmoT+qRKLVgoDr978k08W3QAaTRvLZJHQkCZEZQSiQPKuyy5rW/XE/m2n/yKhG5Oxr87YUBOANUwz1IJMDBk4Nd7209NAcFA1lDZAqwgICsRwsUXTyXdl5vzj06bxpV8h07wWQB0N8wOSauxcuKHB9VzzSbf1G2dr8Ylm2EBBjtJ8a4UARBBl1JsYpy/vxNqtC30uoPjEaVbaSV2sL4cjA0iB0LMhQHubAGlOodCYeQ6UFRnUc3dhHdZs/e3nVy1419EKUUJlAuYqYolN3DH0xLse6rr2wBw3xoBMmswgNVCKARg4xEpkcNL89/+QyHT6umcbV9rrWDOBa5heqOdciQf+uhT5hwBjvBpQycQ2IlHnbGbOv9xRt/y604KWs97KpukeAvoJ0gF1nYDrBHQryHbb7OINmbn/cEF25S0XcOvr1iOOHGATgebTckEEA0GhyDhmYRf2mxWiEBowj6F3XKLpkBXkt1qEBQY44foZh35DqhAILNXhvs5fzY2l2MBkKkUXTZ7bBrnVW6740EC4FYYCFm8XV2D40eDZFZmMFuJes/+MU7esaD3p20nrSzcZfflqArCGaUK6kT2joev7v3/38sWYid9gaW6eQsmIRjHbmRestgu/8HICelSj7PCHOHkZAojI91lU0ihj7JxfZfe64iBuOiUvLgKRSaY9HLiIQcjaGK9YsQUxUtlgfNeyFzx7ArEiHAQKPRbGpN8dR3KJ+laZWW6INvY/tGTttuvfCwCiMmEVUKHMbNFT3Hjumq1X71dnmkUpqTiZNCouX9bmNI4t57BqwYVfBdDlyRYq7f3zqAnAGqYRqU/NQQZuFs92KArSCda2plUTRuEipszsYjD/P8+CYpAAAwTFJJUiUelIR/5sFBSE0DjDlO3JLPzMZ4hzIHXRcC2vv1kNqY8Iz9+GFa1F5GMeUaL3QnMkAkQFhS4altnjSS0mgo9mwyoUd2+54hMAZhAoHq6P3j1oQm1/T8eVJ2wrrkdAOZl8zgOChdW82xYcOOvMZ5e2HPU1UQf2jZewGxXUu0RNANYwDfAcb1C1AOAKT7xLC88shoFAlaFmQhZwyr1HxBAFuO3c9SazqJ8Qw/MJytj0LLKRagzUH/MdtLz8eXEaEBnZ/ptOGPU2wun7bIWTpCRjDJyyCoCIUehlONmdW1vAnoiBcrbJPdlzR+u6/ns+QsRQiNldS1ihzGSjwah7v3s7rnxdwPXwVA6TDCLEErl6noET51/4eQARUCGH5k5QE4A1TAu8IiUEAFp4aLFIPwjWec7ntNPGbo6dRFJV45gIsM2nfgXAIIDAq3k8VmGjALEBeoKmV2/1w+oO3zUE5CPC0Qu3Yp+ZeQxG7NM4xjQ6EOYJrjj+LD5NuQVJYdRo6Ppxz5afL0/fG1Zyx4kkgXHN1t+8ryv/TFPWNDhRmXQOaiaSvPbaw+ac/dj8xgOuVMTwPs1JPOZkDl5DDaPDMxiXTNXwKedd9wxSLdEP7C5K2Xci4MxcUG4llwSfKmg8zc2TqIStO+Jf1XIpaLP9EWNl1AUOpy/rhpOxM9MQC+KQ4IoWNE5qO6I045AhUJsxjfJI15/e3FVYdwwTx5rYx+OBQpmIXVGG9r5ryxVvYcqkCzepFjCBEEukjXYmjpt/wQ8BdEPJYJIrIGsCsIZpgXe2mQjALI2efW+yy61OUPgB3vvFnnEqUDvrWbILfuT9ixR7N9I4Iq2pAAlm5WHmQiXtsDgShoBCyDhq0VYsbysgX1YjrF6t3fn4jhDnGTRucUWlh4VCEHBOB8LNeGDr1f8PAFTGqlMq0uCLqtfKH+n6wwUbBx6enTUNsZKj8USnx3zIMjCZuOB6zeFz33TJ7Pq9v6AS2+GufpOHmgCsYVrgQ64EAPUSdswrFb2lQmMiJJcKCLFnEgkW9IEz/Z4zj3T8nrYk6GGaYUxbcuOOdtsoYgUaMzFO26cDsfgAhf+2pAUOow4voggL/vMTsfdUHQeUw30dV51WiPtPZDbON4DfFUo1LERknKjMu2vz5R9kDgCoFa1sVlL5IZPUHy1KwbZllgwcP+/vvu3foMm3uVETgDVMKwjQGHA9kzEyCABzK1WER4HrQaZpePDRPsJAPmIcs6gLy1pDDMXJHECJab8zKDQcs1/yhUYha+rdlvxa+0j3tQlLjIxRphLSvr6Pdf/pP5/tu6c+y00xEtIDqbgw8qlE4nP8JJQBHDn/rVfNyC16QDRmH2yafAlYE4A1TAu0lOvnAMk7ECKQiQg2Ak3spWwjIIgcAcwNEVABRxIZKNmdDpSEXRALodHGeMXeXXAxQ1lfmONAvSHrwiR4M8GJErGxMFjd8fPXqrqZxtcz71KSqCqzJxpYuXrzT86HOJiyCGzlRZGv9DGwWnRDZnbdXnL0vLd/059DWnM9+WwzNQFYw5RjxB2pITTuMYgRaBSWXgjDEb+P54UoDBAXA0SAKvkWmLt7LyXf82m4vFPT3Iezvd8vHwPHLuzE0rYQxYgT1uid32oKgot9as54WKJHm6uKQ9Y267q+u+uf6LnlzSBPaLDrL3ty02d675j3eO/N2ayd4QSSxJErn383vLCQSIZw7LwLrmwMZt0l6qxvH0WYCg2wVgtcwzRAfcdLBRTBgGk9+39Mw9MKyqIyQT8FyKqRwRw3rXp6+I+7gRKLDEZNgdn+uAQgFkZzLsSp+2zFd+5aiJwVXz88Wn2zl5xwovDxhwmcf8mvRiIuNvd0XPnuFa0nXQbSQfUtm3Y6ePpQumPz5Z+PJULWEomWJ3RXVhsjEEAGRRnkeY0H6JHtb/kEABCxK/ldK3rE0VETgDVMEzyrL3GuOzP/0x+YJP3C18uqgCrIIrwrpHmBJy7ownUtc7BhgJGzuypyY1TilveCVk3WNrm1PTcc2jH0+Ovn1K+4XDQmIpMkAI08jqoYJuM2DDzwhse6/3x0HTdLWgkyWVACjFIcSWiPn3/Bt7Lc+JSoM0xm0iO/5aiZwDVMOQgEqKTlbgSVAJDA/+/8z2N6ueHv6fDfFC6ASkDqAiW1NIaqjEojFkJjLsSpe3cgErtrogPlCtXYMhQKywENRttwT8fPP+TH9/rrqHMgOADmri1XXFSI+8BkIJPcZpNhtCC9dnHTof2Hz37D/wKY0ofU8DxePKCRL6XEj0oJb/nuvXYYd1IcIn9T8AEQTvhBVQkcARyBOAKM/3lMLzP8PRr5N0ISVFHEU+FM3x4pa/SqJd1Y3BKi4NLKjB0xHKbejSyd8nEA+LgqQcRRlrNY0/n7xf1hx37M1qUM2+VQFUNgdObXvfKBzt+fkDWNCf/h5G5xBSSGw0kLLvyV5cxDojFTBanux4oqEIBj3JyqBqoBRAJ16csFKi5QEQNRTe6slLmyJP5AtPuv4cKCJFvUH0dFjDoXoPwlEqhqAN1V7tXU35ATh+7k5539vvNzJIIXBpQEFyYaodjubwQaDlaM0KqUAB3WINWVveKdvKIA6gCNzfium8I5RkuuiNOWb0XRMYhSLyG2E3Q66o/jRdKgM02Npgw1xt35dW0Pdv7+Xf7st680UVJ4Gum7t/z4/YNRJwLOimA3S+h2CX/SDIOC66W9W44PD5j5qm8hYdCZDkyqDzBhcd3ujxje8ErDCaKqhlRLVZxegKWbhQDPcuswek56DsBsAJKMBYh4M8uJH0Y0qWJKLy5BYu9uYGtKf1MlsOWkTSODmKDMIE7ytPycmIh6ABS2n8hwdBPk91aaIAEiVVJOzBBCwm9GycfTW6N6lctyH1ZK95k0BmdVTYql/EPDr5WUnSFKLFfii96ICMoEKUmrUrJwUq42qumaCNcyk3F47cBI5kH+OicXn0pzBRBhVCE0PPoIkQlNaAqMIwTjkAue2LQQEY5f1Ik/PD4bm/MGOdakhlcSV8B2Q05YAyyHWMNZ3Lv1yvcdNe/cr1kO1ispEaCJrsBMxvUXO05+YOtvTs9xg3OA4UlLQEmuLruYHNlV7e+8ionvFnWWiONJOeQuMKkCcDRRpZRWcCugauEAsIDYxKBRFW+GcyIDAydJf/9LXW/fgBvoZxkYggz0sfYPDMhg/7EyMPg2DOUjKRRYwiK0EAJhBMQxSJ0XdolQLM0l9JmqFJQvA4GshRKBbQAOAmjGApksOJcFcjkx9fUBNTX+hBqbbreNTY3U1CjUWA/b2CRoaWnkpsYbuaHhJiJmlEi+y9LBFAQnga8QYIDIM92Ofv5Vg/IaXVUihbBTNYY4Zobb8RakUX9NuV4UQk40QyBHicQqq0nYiSwofQKqREQCUliFI8DGPooIbGfcEKmoSv8cdT3vUtcfQvuJ3BCcFgEtABL5c9MYqkUQUuGdZTKZvKjuq/GWksTfNRTEgHOMGXUhTl3eiR/ePR+ojwEoWMg/ZEl2XKcKQSDI2Lp4Y//DdY9t+/MnD5x5xjtFxTKZyFNx+QfHPR2/+HxX4Xk02VkUq/O6h5ZpqxWCN8gCGYy32ZVtL7t9/5mvuEBVwVNQ8rYzTK4GSGU3vIqF1whjMPuSd0KU7lPJF1ZJb0+D6+7qjTZuPi7evOkjsmlTr3R2WretJ9a+/rluaKgVxYJKMSbEDhCXqPWJVsUcKAAiToJqVGq4TUQgouSi+xuZ2Od1qQjSp17aBsH/LIkx7bVH/1cxKgoieptC30ZgkGXAWFAmUNTVkWls2mZamreY1lbLs2fHtn1+i5k/78s8d85tQWtrC82YMUjG3FLey5oVgIiFKoPZoVJ9HSqGVNsCO1E2jJjBwgRXjN2B67cNLX6+M9/5zNaBD23qGTqhazDuH8jH7NdSkAksZjdmXHtrrnmv2Y1f3HtO470L2+oHs9bcC4C8VwHGMDmUUkZeaDoSEDROyodjAqsCK1zctbdG6/IoPJ2T4pOfj6Onmrj4nFPXIeK66zQeWswaQaUIpRBQAZdZ7DtY5FRmtBgCG8Njbj6UFCUXQsKJSzrxpydnYfMQoc4AUhLwk/vIIzVG4XDvll+cf+DMM75qiB6GKiuBmUw8FPe+8Z7OK4+0tk4UYIWCUvd3BQMh3mJgCGIYtjhx/oXfBmhI4SzIxNP14J88AahqkyelUyKAOUpOco5EoXUdW1305NOfiZ5+cm7x+edDeW7DK6Vza108OAAUQsAJlNFOZHxegbWOmSMiAtXlvIZWUtWVidmUjDRJDamREQv/tCnLtVIFJVTkXoiilOs1XGTub8Q0fkbDx3QABDrsGoRzwMAAXF9vc/y8tKqLvekNAoz5is3lwK0t4Llz85lFi//PLl2cMXsv2xIsXfb/zIwWy8ydAIrwAiFhJ4YBczz9lrFSLGKYKLbM2dBJ/V1Pdp71m3s3vPnWxzsPemJL35KugVgRRYmGRCW/vvdolMWPLH+ttdHSivaWDS/db9b9r3vJwquO2GvmlYYpFpUhVTKGMcoDQFnVwROWmoiANpGhBh2653PxwF8zMnTb4cg/vlyjdaIScipHhUrTUQIi3x8JAJF/WJZiA+TdvukHUkvF++ZJyzrVjRmkiIQxI1fEy5d14of3zYeaMImveS/PpF5aFcqYRvdkz1/N8wP3z1/UeOjDCiFVAZHFms7fLe4YfAL1ttU5dcxEieuqopPwvIzMko+7+ZBZr31o7xmrLhMVIuI4bQY3HZigAEz9WyVPD6uIDzsYdkm1u4q4vaKnn70gXvtEf/jII/9aXPt4m2zYKPFgP1Mc+7B7YGKyNmYTgJqypOXOQ1WQKCsldT3JTSXe/Ek+Mvxz+Y1XPkzi/PAXN/1sid1IR/yX/kCl/0dGQ+A7zxiov2m8hmATx36gvpjbp576va6qzmnc2Q1s7giK99x3NkiBTAa2ZcbfmcVLTObAfS/NrDzgqczKfZ8MZs/+RTL5CCoGogbM4fDcJjGQkhiimsxdoaSi1rKJ8lF48K/uWv8v3/zz029b/XSXSDFmGOOQ4ZgyBiZrvPOWkgEoWb0yCipR0W0Fp3c+3jXvzkc7zvzK7x8/7cSVs7530enL//vVhyz6oCGCE7HMcKkCBggrGETWOY2sDNz+Tun99cei/uuW8NBjIqrMBFEgJiaQCQQG7ImpShcBIA3SJUypEdJeH8NCbzhYkPrpdtwfYwApSLwxUoyAE5d04bqnZqEzb5A1Cin3Xk4SFAJDhgbjPqze8rMvLGo89E8KAZONYyk0r97843+3lAUA6+8Lb5doaeNWAgRmQNRJYBr4xAUXfiJ5g5OG7iN8xVOJ3RaAmmzuxMSFigvAFBOzKACXHzo2evyJpfm7Vn8qvO++xviZdfPcQJ8/UxuAMxm29Y2xsj9tTjVGVYxWvz3aHhyhoZc7k9PNO0oAhsreH9f5lv08coyyA6umv6YpNCMPxQTKZoBs1rsC4M/VDQzY+L77EK5e/XcIAuK2tv7Mvsu/nj3iqJ/mXnLY1cHixY/CmA4FoE4MJN2cBCClZxo7B90Lg0YKfRVWhRjmulue6DjrI1es+dadj26ZS2wVdQYmYyMA1j+jPCPKjvPY7iEDgKwBB1CCjZxTc/2DHbj+wU3vP+vIha//6jmHn7f33KYbYifWcFI1TMaRKzZHvb+6Ktp6yX46dOcsuChgBsAsTFb8Cqf7yGG0mWz/hxG/juLbGxn4GR9SzYagCMWgta6AU/buxI/ub0fOxFClZI9O3sMsEeAc2Hr3aNd1h3UteP9bZ+YWXwEAD3dd+/ENg2ua60yr07Jev/4SVlYcMawbdF32yHlv/evCpkOuERXi1F9bebKtMWM3BWBJi2JVITA5MsYBaCw8+eQ/56+/4ZjinXcdEz29rkmKBXDGKmVyETe1gIisqhJEoCp2NEPnRYvUHPRbzKbPPbUWHGRARE5VBf399flbb2/K//XWD5qWpn/KrDzgjsyq4zfVr1r1LzxnFmk2B8CB4eBgypxUFUIylteqyBkm/sLvH/7Bv//8wdeHsUamsS4GxDoBuVFyy3a9Dv4mc179CYgJtsFCENA1d25cdNvjnVf+zwVHX/XGIxd+SET7iak+3vab8+Itn73IDd6zkkiViGMNMkn9mNhU4FUrGIqCI7xsSRf+/ORsbC14X6COELGVh99tgoBy1BOux4Nbr/7QSxf9wxVOooV3bb7i70AAq05qFCIhO6V6OwMnzr/wy0Ap3jeZpz4m7KYAJIITo0RKzM4V8g352+/4x6Frr/1weP+aNh0cUgQZobq62NRnOdHzAx+FTf1BiRbJL1Sh+OJGyQ2uAFSggLehbaCUzQoD6qJY8/fcfXT+ztsx+KMfva7hpBN+17p+doxcvXU2ABULIAzngE0cZQk5RBSL6Ad+ePfV3/vj2lebxlzRZDnjxNGENM7tNHNVhfPalzXNWenMR81v+q8b3nXx209u/+hp2YuGnvmHW6T7Z+1EcGysA5gUcQCNh30TVQ4iIHKM1oYCTl7WhR+vmY96W4To5Go/ifsFpEIZasQ9W399wEsXvp+e7LvtHc/03jknZ2c4ETGTOQkiI0PSySfOfv8tc+tX/EE0BpOpiqs2fgGoyqpKZDjWQmH+wJ//8vahq377kfCJta0KZ0x9g8OMGQpRQ6qkiYXvSSqB1OQlTUzUqliG6UHJKzbCT6beG+UcKQA2DDQ0OiWoDgzawd/85lUvf4xQl12C+w47Ao/NXwIRBw5DKJuKLKeqD0/HonTBd26/+qc3PPtq29oYiUhWhNJQeQWOVHbMxPR2TjnIGIqiWe6aa396xrm5S1bPnvFMm7B1xASomCQdNHFQ7jkbiKEoRISTl27Fn59qw7aCQSaY3Pn7CDdBSSkwddJRWFu3puvaHzzSfd2BTotgNJFMYhSCQIikSC22HcfPP//bAEICG99pfvqxawGYrI1CCaJEzAqi5qGb/npR/+U/eVfxsUcWaRCIaWx0RMTqnEEc+/y2Ml9SGgD8W9X2RkNpA4ziNyspNt4vakgBDQJQQ51THjTLn1+LAzY+hbVL98GNhx+LdbMWeG1QHJR31kphdHWJSg10PCRJ8/inH9975U9vePZVQWtdFIkEJb/QpFxDnyZhyCHKN9HZi282n9vvv2XbM4WW7PKMNrU4I87nzA27zPeszUQEhI7R1ljAy5d14fKH2lGHGK78eVJmIVUKaSoYVDhLDfjjus+dV4wHXNY0Q8RxxfUQ8qk03ilrpBhv41Xz372mrW7pj0UdTTXhwQthVAHoXUrJhfBxXFInlq2Nwo0bj+y75H9vyt94cx2Mcdw8IyYIq0hQGoCGY2sjx5zMU9kZtr9RtiMGKm244adgWWHCiK9WODXqBbGDOEy1LhcbAqGADEQN9n1qLZY+9zTuOeQI/OWQo5HP5kDFApR3VkUxyl8JMMpwEBIVNOaC+LvXP/GDS/7v4VeZGU1hLJIZKaQn49lNMBTDRS14/cKb8ZUDvoy8OFYx2rkupsx+jIxVqDoQGOVJpnsSmIBiRHjZsi788ZlZGCgYpD2Gy7ZgxaBl//rjW/QXtzqAmWCgk3BQ8okgIAKKOhi31i3JHNt+7n8AABGMKuKpoLsfC3a8S1LqbkpigaKsCsBa6r/uL9d0vO8fbh28/sY6amxy1FAPiLMiJd7bqgYl0VJfK0ogZs9jzgQlLj0pSf0DoFqtq1QDMgCGKAd1wAl334z3/PYKLO3cDK1vgFEZe/NHJC0ZRampLoOb13ac+NGfr3kV6uuhqpnJXwaCIQcX1ePEtgdw8f5fQ945RI4RWEdRUdG9XsFM0KRmlHi4zHDPAYGhiJzBzPo8TtmrG3nn895LD9/JnoICTIFhMuQrTCu/fkoEJQGTcVE8lDlu3nl/bc62/8ZpzASf9Fwt8oJ0u7t8RP2uExYmhXPBtm9/59dDP73iTNTVR5zNWnUusUJK+VUVD51PCP7qKqWJ2Kpek5U0jWQ4A0uTzxMzlMmBWHk4UGMAECQtWZqOCzd8XLaEq+4JsXFzERliCPy6O2ZkJA/KWPzh+FNx28rDwIWCjzK+4CYv0+pUkbWELLH2hZGQsUbTZMBJ1LaIFOosFmQH8IsjP4bZmQ0YcgYZODjy6e4aM+btC9S3CDRO66lTATiZ16R87SZ+nFQjswz0FXL4+E0r0dvLaF3ksODwIiTSyZfpk2Bmbw8CayxFmpFbMPSBg69+dS6Ycb2oMBOkKjhYEuxgApeEnyqLIehQAd2f/8I1Q9f9+TTTOiNSokBcDFbvuC7FDLVSeWgTBLMvgI8iQbFILo4sQCBDoExWOVvnKLCgjC2ZiSQKiUJIMQJFEUuUJxf7mk0yxiHIKmUyABFD3Jh6Xk8mfNRXfbKqAIDCqEPEOZgwxlk3/B4tA3249vATwVEReMFnk3fOEhRKjGKsKMIpGWsUSaw3ITWYPCigGXx83x9gUW4DuosWlh0USYEbCAJB92ZGXTOVfUsn4FcmeGfVKN/WNBO6PH9RCaAJU5akMd8wZsxuzOPUJV24/N65mAkp5dZOJtKMgRFifTTSkgkehRhx0Q0Fx8//u6/lghnXO3WBAUd+L05X2vOOGMUHqFBRJmZ1hTDo+uSnrglvvuk0O2tWLM4FJH5DjvCTJd+bHiQaALOqSqwD/YHGqmZGiw2WLVsXLN/rr8GixVlun5u1s2Y/ZZqaP625bJYyWUUSkYYoJCwSFYpFGej/t7irc2/dsrUYb9xcDJ95+oTouXVLZMtWL+vrchFlMgQRqxVo4r17SCNTZcdWgFXhyGKQDF66+kZk4iKuOfoUaDH0Jv1O+0xrSTMBAUTMqf3vxUDycCspDiXRiJJ2naxDqZIMSDnvMdpdnSpvTAIXNeHMuXfizLk3orfIsAlt07DQVrABiv2KfL9BY7ODxumxR18dLxiH14eSEi8/NyvQKFZVyw4lY9p/LjlhwnBhCBhEWSgcCClpye7ewMmIJAhjwkuXbsUf17ai6CyYgHgCI4/16NsLu0qbwQzWgusPFjQetOWwuW/6IgAY4jg9s+oQfR47CkAFASwSx9mez372qujGm06zs2ZHzsXBCBarKlD2hm9YI66/nxEYkzvs8HXZk098ou7Qwz4TzGvfStnMIxifnfShrP+fAKiK7B93dc8M1z66vHDzbZ8q3HlXu3R2GNQ3OM4EvjKjaqBeY1LGADXi+PvuhLMBfnfky8D5oTGzvKmgVBObCjgigA05RVKoI854ujDEbIigsE6QsjwjJagYLd3VC2MFgeGEkbFDuHDpLxE7hYBhKbUryr5IAJygbyujoZEgpCAZfSOmhC1pDlxJ7yISdsJOQyIKMhTM3ax1y7pMsNRSsMCpnVdvbPNjxPWfBDXUg+uFOMNKdQNge1S47p3fwODdAhPwRBOvfUSYMKcpj5cu7cUfuueAmBI+5z0cpE7i2J6w4F1fy3J933RQ3Y8VOwhAX7QM03PJd6/O/+lPp5lZbZGIC/xe0qrK3SO2cC5yNNhvsscctaHxnHMurj/00EuJOZ98hNW5IPV3kDd33Giu8zTISYAp6R6qIObHgtmzJJh9wl8bVp3w83Dz5ncMXfP7A4auuvrv3eCg44YGUee4eh4KACAgZfRTHVatvhXdDS24bf+XwOSH4MbytKeEsU8J0BhsjUok4gYj9sqQRS5j4lhAUTEO3KADDDuut6TKBBEqpaqMpqElxAgGgtg14LR5d+DglofRFzIMeX10h2ukPm6Z7xWEIRDk4POgdxYHoSSTQQkgCxURcbGYur05aHvrrdx4yq9s3QE3ws6+n7w6OyrFi0INAQ4gG5lZEK1khawXgifvvRn3FmcijhjYw0ujfNJzn13adOTzB898zY8BAU8D1f1YMVIAigTEHA3ecP1/91/x09O4bWaoTjPlZgRQJSosM6RYiNgGQfNHPvyJhtec9QUmUlJVdULEiY3GnvushJ1kcpRSEUBeVfeKFEiVoZ4CC0T5zNy538m8552Se/lJa3u/9LW/D9c8uK9pmRE7cVXWYEqgAIaQxStvux6bZ83G0zPng8JwF0ERoFRnTA7MRt1AFDc22ODsU/a67bSD5j26z9zG9ub67GdDF8/Y0lN8793P9BSuvnf9G+54dAtpJus4YE6F4M52CwFeGBuH17dfn5i8jJTDdDSVnRiIY8Vgr0VrnRvWNreffnL+/gIyVMNQuTFjF3zih5m2913EtsUC2utTHZQAUS3j+SMQl/n/jAKOKDCU8qtWCERAGDHaG4p46dLNcmvcggabuD33VKgqOcWqhe/6seXMBqfOGJo+vr9dgUos8qoMYom7u0/a8p73/1a6u5o4l1MRKV3zkt9vmi4QJQ5UsAHCUKm+jtr+8z/+ve7wwz6tIgxVQ8ZE6ec1iXaVuZJS2TbKbTksGYffLysL09Tn5AKIgoyNdKgws/Nzn/tj4cYbX8ItM0Sd76RVea/gyCjwb+4p4vnNIbLEZQ3Gh/1y5d9yxMjpEDrmLcB3z3wr8poEPMZwRGaIG4j4uP1m9X/3nUe8a/8FrX9URS/t6PjMRM4ddsUdz376w5evOaFzKMpx1qqIo9FWmkAgcpA4h32bNuBXL/kQCMWkMy0hZQbf4XsEuBioa7WYv9yVSFtGlUne2QVVF8EuCLJ7XXGZaTzhfC/khAmwIIp9CIzKNMnyyDIhYZhWIkb+qVf+WXqvPYVMxnmK/IlDAVgV9OUOw3cG61GINsMiM+yTrXoMXwGG0XzcQ8tbT+i54ICfHASi9cPPsqpQm3bAcEQzMQD6L7vs6/GG9U1UX+dQJvwAJPlxUz3F5NjJv+rvAqfMNOPfPuGFXxxlQKRkOBrxne1ZJnYI3Gz/5vYaLg3/NRmLyERkbAQnAdXnutr+7eOnZA477GHpH2QYM9HOrhPAjkdOwlXIcw7tm5/D8Q/f55loSik9aQBjRzCzusGITzpwTvS7j5z02v0XtP4idpIX1UAUVlVZ/CsTiwoR3/2O4/d+xe8+ctI75tYHkMiBdxI09aJaAAlw4swH0RIUECuXaM52tn4pS36Yd4iLXtiPdi0VBPX0S045F2SX/ehy23jC+ZDQEoGJjIJMCLAM+wfL17HsfyIlQuDXJLu50pKJyEhRgNn1+931krlvuq3g+kDEpShQtURLd45UVQAEzjFlcMKC936LiNerSqBavcIPSBNyRC0MS2Ht4383eO0fD+bmlkidmJQMtBrgExEUzMahv980nXfubxqOPPLTGkUZmCCiErvbFMFwpCKByeZ6Wy+66FJqbACi2CdUT90sdgmjAqNAiDoc/eBdmDWwDWIzoITPlUbJyWKGuiiiWS25wvfedeSprfWZ6yOnGWs4MkwREyIiEvav0DLFhglhLJmjl7dd+V/nH/6fCTFqNPoVUTg1AAuOmnG/d8qOEUwMVySEeQKb0dnpfR2CcSowwdyPXmobTn4HNDLEGQdf87dbl0iVJ0EekTCA2A09e8zct5/dkp2H2BW84OM9QAdMnOeGjOZdn91/1ss7VrSe9AVVBy5Ffqv3LLyn2/ekyOSv+s0/68AgyLIhbypU1dyJCDI0xGbl/tr8hjdeDAAwNqZUFZjiuRJzpM7ZzD57f7XhVWd83Q30M5impQ3j6CBIsjAhWTQP9uPIRx4AMjbR5Nm3FNjhW6woCP7ptOXXLJ/bfFPoXCYwFOIFVpgAl7EcxRLjLccsueQ1Ry7cJEPFgHhHBzhDoGIxN9OH/RrWoSjjeHQlKTKFAez8S8SqLjImu1cYzHzvV3xajMELzX9M4GxKljehYUZCwABCyTfNyLRuPmzWGy4vyAATmdg/oKplL+0ESTRfJHaByeGE+e/5EoD+pIBAS6QVVQr2vj9CvGHjO4ZuuXU/amgQSsrflKpr+ZXYaRhSw2vO/BVnM3epiwlM4i25CobnxoOkc1n9qac8zS0tQBRjsrPsx4pUuJECBoo8Mjjk8YcxY6AXYi18U6btc8IAF8WY3daAt5+49+cAwI69Y5cSjAHQ8Q+v2Pe7sAyM0kDDZxJYzG/ciDm5rYgFoHG1hBUUCgqnNOpSE9ipAqb1rCs4mPsINA4wocY7aRAw2MXndgecMPD7BThm7rn/25SdU4i1YElZp+XJPg5436mRguu3B8181eOLm4/4iYoDsx1uUlsdt8OoYBHvuB+6+a/HyrZuUCYQn+6i1RWOYlKNQhPMnqn1Rx35HQAgNpz6rXfw900RiClWdWT3WfGNzD7Ln9Ji3nofzvSDlBJKe29LxWTQ0r8N+61/CggyJaqkcjCRoBDxKQe1PbWorX6jc4rxPAc5+exx+7RddvherR2SD43hkdLNU9QHWF63ETkTIdax09H5uJYiLigkHl0AakKXRU2nJkGpiewMTWfsfYuVviVKkWcWATCzftltR8x8w28LbgBMNmnFVcUShAxEi8jYJpw4/31vBLBJiRiATEadcaXBZGwkIvvk77zzTWosNK02TzwpVXMKRIpiEWaffZ4w7fP/BFEudU6b7kkKYADJ7r/CeT/gdE9oGFQy2fzVFBBWPv0EIM5HXbdT0BTiAMLJKxf8EcBWhQY0WrnYzo5HJJHToC4Inn7pyjm3wAmwQ46dd/Avq9uceCDHt15EDBcDLtRRXEwEFWcRzBSbO/zLSex8gtpfYsZryBW/smWpkunYR84/9xuNdg4izRsm1rGTWkwutucNAAAiigtxH79k9ut/3N64co2qWC5XAKpj6jsFEwjx88/PjdeubeRczqW9LKoNBAZcjMyy5WncqWrU0/RJl1m8rKA8liST6QFBESGD9i0bMau/F95aHZk2I44oqM/giGUtEeADIuM6iHplHQCOW9EWwphS36lhMUoAx5if60oCIGM9RBqZFYgD4mg0bwP5TKlgSUjBzOepNKvdhCZN7QFAh2Qyn20EQDWum1W37K+HzHnt1UPSDwLLmNtwTjJG9inzyUNOirYxmBses+Cd3wKAhAZwjwEDQPjYoxdLXz8QBJOg41cGfhMQzML5/vfpnc4IaNIXg+bO+1cKDEhkPG79qYMqYmI05QexpHMTYAKMYOZgKJzY+W11fcvnNv4HAPiuXeM4BASUaOYHL2r9aK4hgDqxI3LRFQAJ2jJ9w43jxjh6mrIiAkTRKFkKaaVSsJAUmUo4sQmgCEADJDog9QZOeNSdH84BwPHt5328xc5CLCFX417yZEtG8vEAjpj7pt/Nrdv7DtGYGeNy5k47GACKjzyaLcWzqxaqCBjB7FmfAQDSKuLUSe4H29hQJJudElaP3UXql164ZWMyx3JrhYDIYdnMrDblMuHu5K+W+2LbW+rDRa1ZRSyejKCkjjEYERp4vBogvIGSFBhH4agzgAIwdnYSw3MTC44N5wkdChk8FAQH6KTVfxORijqeVbfskUNnnX1pQXqJ2Y41CDVlIBiNJG9m5Obj2PnnfTP5a/VUg44RvvfDuuctm2BneaXVAYUyWWhDw3oAVSVgSm0kc5bIGlRHCGR0MAgOhLmdW3xZBYYTkIkAiGLJzHoFdmyZPCZouhqCugxjUWu9QIYbbRMUEEKdUTQGEUTHG6RIx1K4iEdPsVCATVMprX3iW4WgkhfnBvxvk2IllTyBSp7rFke2n/Nsg21DpGHyicRjOo1SxpNYAEQsoRvCMfPe8X8zsgtvEHWWiV0VRQ3GBCtdXe9xWzoOQ2BigtqqlOBE3gQ2BmxtAAAqElSLFqiqhhjOB5A8d121bgOCQmDQ0tePlkIevZksyGmp0gUA5rU2lNL7KVG4xp6nl/gSlWEYWDCjgSHl6+E/YNjBspb3Sx/z+Jo073aRG13bTlMD0pOYYBSYQBDpZ8S9SdBicq8ugSPRGPPq9/3iIbNf897bNv5gfqOdrYKIoNOnpxAISaMPFHWQZtfvhaPb3/6x5L0qfuzvHDbe2mm1twcwZrSUreqBAmADk8lFgE9CTrjEpx0k3o4kY+PpbPI8VggM6sNBNOX70JttB2nou/UpOYDt3Bn1nweQTwT6uLp3pRatqIKJBme1BOugWIpUEpECymAWWApTz//Yx9dhQ0sdjaqd+rdLyQwVgbp+wHWlv1Vm0J0fDQQ2AIrHtp//uQc6rvmm08gZMtZTTEyPCEyr2oyaOHR5e1z7BT9qCNrWOI3YkBEkaVfVvv/LYWXbNmghD8nlYLRKI5g+4mQAYOiWWz5hnn7yPBTDjHKVuBwERBkbuo4tCxONpIo4AreD+u5r2ShEY7HgiSVK3H9QkKCtIdgMJKWowLiUqCRIoaQaANTX1pT7DkCfU8/1GaTbS8VriUhzEcd4KT0VvoAMIXYGIpJkQ5VFKOE726V2PE0geUUT14BGmxpEtsGQwU6YsyoIAhFExGFu/b5XHDz7rH+8c9Ol+9QFs0VVmFM3wzRsf2LCUDxgF9QfWHzJ3Ld+EwAYBlBfVTRaaWU1w7qeXqOxA4OrWwNM6sL7Lr30FJE0W6c65B/IU7eLMbB19VDiqo2mg7wAJHFo7BsAFiR/RpLnxYymXFnQtmRijk+IpKc/sz5IEmmorKxLIMpwGow/nprORQE4l/ijypVIP99YemHha4cnaAb7kfMPfpEEgDUEnZKYhJJnk+g+bt7bL72/86qLRWNlpMGk6dlfquREi+aE9ndfnTX1q0UdMYxoSqejgrQ6ak+AjXu2uXSDjpJVWn1oaHReb6iuGptk9XzTpSqG7wHsVbvGOASShHfAU8bDEJpyQWlHJHbreI9S+qkhFwiM9zyWX7FYDMK0rxbGeiUpyT/z50EicAKYUe43cl1l6d+7D4LX99zg3ca3aJ6qXUcg360Gcxv3/8HBs17zD3dt+tH8hmCWKqan3JzByLsBWtR0uB4y96z/ARRMzACGgx9VVAQwFljp638nABDUVJNStVOMEDDVM9nqmckLQwlwSQF+XVRAaebkNUDDjMAOL/FEy5lygUnG9gdRKMCKvLPod3UYn3U6HE0G+WiyKgMkQEqPn0gojTaDJQIowI5tgMYBMkIScpy/O+PZqqfUSlJ40uItx857xyUPbr3607EWHVNgPXP05Aqb7ZslKalTFXNC+7t+bzl3k4/8mqpL0RkPGEP5w5NLWr1mWw2Vg8JH1SGQQjjSflSCIUXGVODGSobIWVNSJSkxX73CZjHoWscpANNhvSBVGOgOdcSJilZ8Fio9SV707p6PCwDA5e98nxYfWwFDEU1xT0cCxwrB/MYDvrj/zFduyrs+y8pTooeWH4HJyJDrM3s1H/PAAbNe9TaviVdHEHIiYFcs+PKePSB6WUMloElLU8CIlgRimiLNRDAVqHhNRwgMDTMklLNXO4OOwgzY0QO5uxjbizSnChFNggGlsYmYncYduXjo4f+XfH533RIRgOa456qPUhSCENjpCDyon394/PzzP52zLXAaTkm+XZrU7t0ADgYGJy1873cN216Fs5gWQ7yyYIJw6lHb48+mhl2inM2+3kXbXXTfHD5VcSrhzmHentV42Fe0vjgLAHYza9L3D1FNcwnLxzCeiWnwL0sAACq7sbWVAAMN17dK90/magAoZDfE9cTBMKLqsKDx4J8e2Hrq4wXttwQz6bZ4+tAisBbcAO/dsqprZdspl/qkeRNXSQ7GhMAp7WxN+9uDMIGN57NO/HM99tGEsjcVgSFkM5WL4+SsgdmuKxUlGtsz+XnekzWOHNrSbBN/H6lLzNwRi2KZ4Ny2q88W13uskHVQMUhJfnea7JX4EKGkGqsCrcUtX/o/hB0BUSC60+9VGjvcjareF9hbH8zckNDVTclUfIIrQcWhKTOnlLVJoNG5yPYwsPfNvAh02b8RjHzq7t5V8yEBRZEng+BzJEadoTDAMdYNzcegq0OGpLKpveoAykDzDyLc9rPP+pFj9k2tUgKu0WZGPvdR4yxRgLj36te7rd/aH8Y4UmFO3AfThCStcVx56RUDAXC+ZWf6tHpRiAz2PvDqLd2qYSTSjpUeu3fVUqZHrUSwYzfg76AYG4fmYlNxtieOrugRFIBjJkA2ffKlWnj8LaBMRBobkATqs3bLP28ACaCOSYVAQSEeuOMtxefe/R0lJ/C9oqFIuZ6mTQYlpTTTgxdjlMCKqi+D2lNALCUbajh58cWOUhIJmKDERkCQcWruvkzJ38QxGJrLlg0/dcvom1tF6Aob8cTgYiyvew75inaWZygpqTGgcJMWnj77isyyn6vN7f9z3zSeAEhZH2d2AIkSlFTaw67v/3O4/qMfYNdJ4IAA58VfWRlexfFilC57AKzJBI6UjCKtMaze66BQ6OAgq6TNpqp3rpXFcODCGiAXChoQIaecBFbHKMC8EQVHjEYMAZxBqapiCkFiwCaCKGN156E4c/YtJe+aP9P0XHZvYn4fJ981Flp4SIuPn/hjN+dDHzAz3vglk12ynihzH3xKiyjil2n4nI0G7zxZO799kfTdVEfGqBoLVpdU6nlavuH5VXjR1JXxQlRXkv8wXhRW7whY1OVMqRU4Tf3NMGYohFS56d1/d2umfcF9Eoa3gnEOgCMBDGKK87OmCArAEPBrAFlV5MF4YPXPHvvR7U/1aVPWeCIXjOOyqUKYEEiMZ2cuAOLiyNKlKbj+Surz2U0Bt247AD1RPQIeglMfgfZVVbsfcPCVYqkAESZjFa6bog2fWOW2fPkErluxAcH8ewm5nGAwkmjTKiqsa9aoAwAU1sQgBCRJPXE6FqkqoEgIcCuBtOOKylBTOuFKjV0JpInQ1SoWJgrL9U3XKOgsAqRa6KV2gE/cFVZw/XGr/i2z9143QMSA+WfTPbWpgjd+ITGwYu2jLVgjm2EarW+5Ma7dmWYlKyiKgNhBpyEHVBWACfHY0Hzc23cwTpx5B/pD9oQ0lZ6MCikTszGq0uPigTvbSfFqRdLtg+GUEZOxBIChUiJt8Mvl61tFVUl9G4lKzZFUDRFileJLoe50kPkDoBaePKKGSYal5qY/EnCWj4QQV2U1SKLliIshhQIDgDpnfenKiyEbaedIcrFYQUSkQiLGDA4CYR5UDECiKZnLmECg5MMOSpxof1O7hOl8jSqcWPx+8yqcNPMOAK6kzVZ8VgqCCoEMsyEhpVhJUsPbsIIkOf7I7ykAA3ExOLu8n1tf/7zb/MUDicw4Vn0XUyOApQjSPECNlRiyhjHCmrYWA8P+QletKFGAGCqK2EV+07GJlVmq0VNSSfjEK3UEYu+uY6ds/HokwqvEEj8WKHzQi4bZhaf6svtgjIGQAqaIP3YchQv3WowF2ecQOgsmV/E5pb67pAseC5SpZMm6pCBmRxeQ+oy3mBxs0P6xf6Ng8VC86YvfIyD2hcYTnGnS6F3dEFSGQNyI6vUBvvjAtrklpiCAyrhtqSkHq4LDogN8NO5vZYsMF6SrT9wsq9sZl/CD97/5Oy7JiUvdW+mxKr4HygP2w39TKFQNmEL0ho24/PnXoM6mKcqT5IlJVc/0lewhBqO8569PhU6/Y0Wj2JoZL+80bRdcRrA5sC01nZ+IrzIZIMm/7ofK0IRGqmH8YG5tJarLJsmjVSxSmEhdDPQOvMT/oYrmmvIkYXIeIUkODEaKp2GRRYkjYCwv384r8QFCPQXWdm6PSp4DbSf4hjOXvGBUMBAM4hfrT8YDPfujMXDwgR0acZYVmUsSXAHpcPlzKfthZDDJy0oDSOiQmY/Mom+ezUAfzAxL3IgS68yE8yaScdwgVAYnNFIN4webmTO7eEYLKJKqbL83DCI4hfRsezcAaDWxt3o71aqIhTir4ixEKvxyFiKAqPGyQ0GqiUmXau+7fpECLJRaXv72TR98iaStZEhEy38a5QGrIDAJhuIAX3ryPDjNgGl0sqfJ352U8CV6lwuJxFAKskv+9zuU2/c2KKC2FTBBqeE8TdQNqADIADoAuJ7kb9Vtib2YwNzc/HMzp/0ukZjTfq7VCU+BFHZsTsoTq0xYM8fEHBObmNjEYK7Yi5hj+DEBY5wGWSDIQG0GGgRQm4UGmTG8AmgQQJL/vcaTaq7D6Q46aTfg6NdMlGGy/fjr1oPwzWfegbbAQRLa9/KZTL5YUIAcCBZQjUSc5UXf+B63vPpCaAQQYMzMvxDX90EQKIzPiplQHM4HWVgBRJursynZixgWAILFS7hw220QHwqrUihgCbJ+Eye/VY2+qqBctHnL6zHQRzApS1BlZ6dQYqViDCxu3roFTT1daAoNnEuPNbZbhxJyo9AE6GloTEKuDEAABiInKEb+OTjujm2joBjFiEtT2/mAohacGcTXn3od9qrfgDfNvxZbQoNMwhUzZYKBrEIkEucyweKvXJKZ/YH3k0RWyTpVZTV1D8G0rVdat79PC5z4kzjpqAAXdXyegd/BK8A1TAEsANiV+6xXoiNIq7e0QlVBNkC0fkO9FAqtnMttwzhzgCcBlDiQmvu+e+llQ3/6g+UZLVAnlQ0mlExTby2dem+IlZuKlA0YouPRkQhKhFw4iLXLD8SPT3kNqBhCRZLGQSnfSeW2gdtOjdvpBUsPa/L46CMXIuAiXtd+PTqLXjuitBuar1dLQiXkS9PGPF31TDia9Cch8s2PE5NX1Qi5UAVBJljyrf8NZl/4flJnwNZ5T6svC2Y7iwRIxpIJJWKVVwBq1NFQtho1TAEsAGT23ecz3Nz8WopFwVydPghVJpuJ4s2bF0bPrntnZuW+XyYRC+ZoWudFSaJEcSiGc9BIFK6yD/Dtrgaxc9aowkaC8THdKYQIGRAGmpoAY7wELykxBk4iRK5yObg+UVswHNkdfcJK6vv9kkNRgX968IPYFrbh/MW/xGAMFJzxrfbIj0UAHCVbdQyqqhc0PtSRhld86qsBwOJIlONQkVtig0X/851M8yvfRxIHYBuXTdrLbzNbh3/FSCk2TpAQShQzbkNKNbVbY9UwfngTeP7CTdmle60rPPrwEq5vkEqW+lQKpArNWNbuHhQefPDwzMp9odCIplMLTCwgdbG6bT0WQcYSGYAr2xkrjVgSfBdLJYYDIyb2Gts4lDYlhlOgY8ac7fJnvCYkQolZXRkU4xhQBfMuug4qJUQNDCYHRwX822PvxIO9y/HP+3wP7dlO9DpG6AIYcmBPypkIHhkxzmjCiCTJ+hGASCBkFJoRReTgYkNqjJn19mcy87/wCgran1KNDJjjEZmBidJIdnbsl9xESok03m2Qnx1FWY23xZT+qYYpAcPFAQd2Y+aIl/wFYRHjYqecQhAAVhiyAQq33PpGiJtHXB0N2FzfwCK3ebNlG0ARJ7WoWrGX/0+H2Y/hbW9KI8HlP+/ixRIjDOqwvm0OEMcYbsqR0swD4e4QKG+HdIRiIio8e/POP+9NWfFCMEnwZtODX2xchbPu+gq+v/41iKUeszMR6tknfztlAFkHGPVJzWmJGm33YigZkNpYDUIBOXaOKC6yqg245YzNueXXfySz5EdHczDvSWhsCEZIzXYzTkxvarMaARKHAUVhoBN5xcUAcRhoDEjh8SC9ujVMDSzYNzapW3X8Lwd//rMLxDlbjcuvAOAU1FAnxYcetsUHH35D7pCDvwkRTs9hyuekaokoKj72yL+4ri5wXV1MQnYCPchGxc7If8fbxl6JkNUiNs1cgE2tbYDEpW5tJbPQCYpR5UzgfBgD4qP2pGnG3SizVk5MZa/qelFowcEgNheb8R8Pvw+XP/8qvG7ujThl9l1Y2vAMmk0I1tBI6IMGJaU7VZnTwyT/i8AwYMVQUXN7G9N05g8zbW/5q2k46v8A3kIQowommFgpFXdlBimxQgGa8epPZmzDbJhcxCo0EXaY5AiqGlk1rT2qEYgmn6i2Bg+buJWR2XefW+0hB/eHd6xuQlMDkmzUaY8yjISC2CiFgxj41VXvzh1y8GUC9Hs3nGop0XfSp+HvA6hGAJoKt9y6r0YR0NDA6lLxV8lVG328Xd54ZaazJkm7FoLHlu2DKMiA8/mkHtgzMlOiAg6FziSnuRvhdk0O5r84GMYGIrucr5fDI/MRvXuPwSYCmQKeHJyFLz35Nnzz2ddj/4Z1ctzsJ/jjh/bd1ZZdvyIaWj8D2gNoHr5xOYPZQLkB4FblYDZRdtlVlDu4nxqOvMXUH/F95voMgIKnqHYGYKXEAqIRM0tShBSqiGEbDv0FGg4bz6KMZdWQHqSGqYMlIoWLDRnb13DmmV8v3nHnJ1gpVlILTKijasWhABA7g8aGeOjmvx48eNMtr2s4adWP4FwAY8KpoLMp+dvFGTI2jp5f/7r8TX89hOsbnAqMr3PVqnhq+Dpff/WEFFZC9De04IFlK4EohHKyWuqDEJzUwvbmkzyY8vKI8WwCQkmY9Q1FzqtmPv1m58QNO18wUQJgYEwIcIgiMe7p3ts9Gx3C/zTvpCsybfILjrYtJimEwBBII0AtQBbKWRA3KNtmCwSPwFOnJWclEVQzIBNhpzRUw5P1XdIsoGrhO6pUCJSmTin28D67exp89aXxPdHrT1z1/YGDD3x39NCjc6ixUeHiKmqWSYmmAjAZK0Ta+61vfT+zYnmXbZ/3e8QuYGsmPSJMiVQR9enCvZd+/xO6rRc0o5ngHIiqJ4quQFKzSlAY5JDHHQcchM6mmeB8HmJQIvqED0YaB6A3H70VwA8JPoVvPEp1cupEvqVkticfvz4RwxN02BKc+mQUJgLsEOrrHJqC0AJ1G9nO2cqEKD3vYc9mmQjT2ALIgMj5SBU70G7k3BHFSfywhj0cnGgIos4ZZvNM41vPvULFAaIupVCvDmgpjUFEwPV15DZv4q5Pffa3blvvWWRNpM7lMMkKqwosRAyZwPVf8fOfDP35LyvQ0iTkNElmqJ4YUurXExByUkDnzDm4bf+jgbCY5LCVm16AJnQo3f3h0QDqkjHGl+BB/iGRVOq09QwWX+KdgDpMPzMBJGQOCocgsEFcFwR3+7MQUjijEAMV/z/EUNnPIOtAJkwo8KvjKVXDtIJ9308AhhUqaFh17CW5l588KH3bLJmMSjXd0Jp6hgB1DtTUjOihB9D10Y/9qrhh42vImAJESEUsMDKVZ8K7XSWAcwZMsRgT9/78F5f3XfLtc6ihSVjASgoCT2vLmu2R5r4RBJYFfzz2ZejL5kDOQTmZb5Lbkda2QoHu/mLJDzb+hlnkk9aTKXT3FcXnlvqH10SWJ83iUyKFKprrc0/msuZGgIjBIalxBHZE7AjkVMkh/RnsG3uMsOtr+FsHE6XbigS+HmBt6/ve92pun5/XoQEQs3f5JzfKdG4bTVIcSuaNi5maZ2j46MO28x8u+tHgX/7yXWVaQMxOFSpOWEWMihioGp8tsjMnfCJYvSBgiBoVMeqEocIAORjjXFf3q7Z99vO39n3jG+dyfSYmYi7l4iGNck4H0ogFgCQ/0DPfEZp0CDccdSIeXLQCFA5535/ScN2HJJyCAEDAlh5POkvs/V7jsegprdEgIHLObO0tMjiNV487orLDGZauvyhmZC0Z8gK3JNfKZjJcL07D2TFl79dQw8iYAZFAJAjmzLmh9cMf/KiKEJwUqeTE1qpqG0IgqHOMphaVvr7Grk9+5oKtH/7ndYO33/E/EkUr2LAQsxKzIyKnqgYiAZwL4CRQ5wJNfvYvF5BK4B1i5IjZgUlBLOHmLWf1Xfbj1VsufO9vB6+99mhubnWgwJJUi4bshYuCAHXJLc9okF7cvf+R+NOhq0CFIpR27rtSADCMLf0FUqgpBVHGqdamny5EIpsHir6TXYU2jabZyCqY2RCkemsNNewWdrwbmGJxjhqOPe678Qfe/8rer37tNGptiwgUQKWqKANTHxdcRBxk4DIZV7j7blO45973ZvbZ96zsMUcO5l7yks8GixY+bFpamsna1YDpx3BmyHZjJVaf6grp718iXd0dhYcf/sfC6rtfFt33wJy4q6Oe6xrFtDRDRYyoAlwt3iRKKlW9FuqY0ST9eGDfl+DXJ5wGhMVdPrw8+zthfW9RBwqxa8oF8C4SGofClC4taedgtHzLQJFguKRhj4e+f9TR0zC8EOa1ZNMARs2mrWG3MJo6oMRMoppveeMbXqN9vb/r+96lL6fWtliJLapG40mNPgUrwwFEKpYaGxWiUfTEE/PCh9fwwOU//aGZ3aq2fT6ZOXP/xLNnbTFNTQFlc0rZwPurijEkLEL6+1m2defjrVtPdFu27OU6u0X7e1mJQHUNsWlpdapi1KVtF9Pk3umHNzwVjgwIgiYZwOpDjsNvjj4FLg5BKgmF/ijfLfkUlGBItvYXWp7fVjx3//bgu75r9Hhp0nzR2dMd/R8aGCwCQSZW1YqETZUSWnsC5s/IpfmKfzv04DVUFKNsSvKuPlVWcdGMd77zNLKZP/R+79uncl1jATaTVXFU0r52nY47qfD6hmcFISWoEwI44Lo6RUO9qBN13X2It3QBcv/LSXy4whv/iXkn3ngUTaKXzEJB4BBYUEurEClIYLUk/NOSNJ2m1sTpipetPQExWdTJIBBY/OHoM3DjgYcDYQiGJuVlY4BhNzQY81Obew7fv70RqmCwjqtVm6ivsHtsfa9DJOAs/HNzwhtlWFIDjEVzGv5c9kY1PIdq2MOw06eyz4g3DCfSct7bX82zZ13V+7X/OgODgxE1NhBEraqAS47/6dyDWpbS4eehqpQ2+aJsBpTNwOd/eff/DlMlIGXyS8ggDFQxXMWw4+enF16gCQBlAqugRYawafY8/HbVqXiqfSkoPwQlwljjuN4CJnKR4MENffGrD/NK4XhOVlUJihgA3/P8tgUQQimZdILmb1og55QUGYO9Ztb/Mnlj2q9GDXsmXlgtIAgMMcQVm155xjmzvvbf7zR7Le6X7i5LKjEziyQNZaoaIv7lnIFzFm4UynknVp2zcM5ChCFSNQnNO4MSAcSolyFwhnDTkcfiu68+F0/NWQgzNFhGczUuMAhY/eS21wI6j5gib9KOdS2IrSEMFuOT73t62zEITAydaBJ0aWzvx4wdzW7IYH5rvWfIqvLtV0P1Ygx2EQmISJ0MZg9ceensb3z99Ia3vPU3GhatDPQJMcfM0GpKAH6xoSzBxXs6fdEu6jSPDEd4bJ8Dcelr3ob/O/Jk5EGwxQIc715RoCgYWSOrn+5euLG3MMcQEs7BMWqRSZDi4Y29umZDH5DZnrR1IiBfaROLtrfVYXZTnUv+WqHxa/hbw9juEmIlQzGcs6ah4e62f/z71836r699OnPYYZulv8/K4JAqcwSzPX1QDZVAkqgOGI4QRsiFeQRQrN17JX706jfg8lPOwnMz5oCGhqBwiMlgd61CVYXJWGzYOoCbHt76Kf+3sZdYp86Fq+9d/2nJF2Gs4cop0gICYjjYZe0Nf2jK2ZtEhWl3ytlqqAFjLmj0hdpkTOyDIxLkDj7o3zNf/uK3h26747zBn1/5keJD9zcDRNTQBLCJfeKx7DRzOr09q4lsYUqRpLOV+8XSpSoFmJgAphhOoANDVsNikFk4v+fRQw9q+WndXtS5aAFiJVCx4HMimZOBJzi1JJ3wh7c+teStxy2GkiqUknmmjSA19ab6+mgQVGEMk+saLLz2p7c9eyyC7PgLScY0QcHhi9v6AEAUhqlmftSwexjjk71sDxMJMYdwYskEmxtPOvFzc/77q4tnfe7zn8oef2I3xTGkZ5uRfF6giMkYUWYdZgTx4w0nkkzUMb5nYIdzZEDI+/G8hUm+yJ9JwMYBGmu+EMu2HoOwaDMr99O2D/7jV2d885tvvO+Vr6PNrXOAMAIVi2nJRjJwOQ3Abs7VqaH6wF23ZtOhv3tg/Y8tsQtFrC/zHZbWWnYoVVAsjgDwt6574sJnNwzC1LGIugpeXYJTAQLGscva/F+oOrIwa9gzsfu5WcwxQVmdGFg7UL/q+E/VHX/ct8Onnpk7dMMNnynefvsp0TPr6qVQBLKBUjYTwVoQ2KoKjbOZxR4PLgs8lyq3fLEGiIyQSiyxYw1D68IIJhsgWLKXZo84/Ja6E45/MjjggP801j7nRPehwoNAHKpmc0mK3nipUXcBAhjEDgYf/9mDbzxyr1lfntucuz8SDQwlTC+JQqhQilUDVdXAmOgvj2455eLfPno66gMVR2aC1W8jp0UEjZ2dP6teD92r7d8B/xypzOg1/C1iNwWgz38jkMDA39HOBSDuyC5ftiW7fNlZcu45q8LH1u6Vv+OuT4b339MYrXt+tvT2+QyujAVlsgJjJeHPY6gnZhhJUfIiQVJvDYYokbB4lUldZF0xBEURE3EGrS1hZp9lnblDD789d9QRX83us7yJ6huuA1AUVYZzSYoOA8QJ35/CN7yonKKlShAo2ZzVB9f1ZN7yjVt+d8UHjn/tvBl1a1Q1THqx+QQoIhhQSIyGPz64+X/OveT21xacggMDEcEoRbi7DSZSV4hp1d5tW2c1ZDoSNusX226pYQqxmwKQEhMoScQlBpgjgrKKEKky19Xdmjvs0Ntyhx16uXNu7+jpZ94VPfpIf/Hhh/85XvvkjGjLZtL+PqviQGyErI0RWJCxIMMJtVQiDP1OHzbXqy09ZTgPw9cKlpE2qKrAObg4AmLHFMfWEQNBADOjxeX2aTfBvvtcn1m5/3XBvivCzKKFXyMfVk+K/p0FyBIBMEbgZJhqhcmzPKWCsFKnkxzdiZJpyOqND3fMOfY/r1v9wTP3/fWrDllw3+KZjd8NDG1RJfQMFfd6YH3v239y67PH/uCmZ05zyhFnAohKUkFXOQINhcYgBKcePP9LALY5lcDSNHcFrGGPxm6bwFT2ZB/+kSRh4BCo2kRQiTHmKbPP8o/l9lmOprPOukyKxcF406Y3xU88+dbi089sduueOyzatGFf19kpGBjkOAwBJMSXbABjFYYjMl5RJKbE6V8SPMHINGwt9aHQxEFVmjd8X9kXvCuTpjykGCbV8qpqWssaI6WAUfF9dUWgIladI3IxVMTHI6xl1OfEzp7FPHfe45mFCx+wS5fkzLJlCBYv/kTQ1tZFhgcB9CKVPSJEgIE//zg5JR5e7HROiSY5ngs32ukiNcsZmpDOpguUCEGzrruoH/zeva//j5aHX79oTvPfN+U4jJ1iU28ht6lraLYrxqCGTExEVkrd3yZOf1WieCBWCeNg9sx6nHbg3McBgImSprx/A07kFLRdWtQkQ5MKc3/g5KhU3j16z177yaO1JYrLNCOCiIUCyrSRs1nNLF36nczSpd+t98u6tw7k93Pbugtx51Z2GzZc7DZtnhVt3hy57i7otp6M9A8skqEhUFgEoggqCkmb6KSVKD6IAAKBmH3SBA8HW4gAZR5dgUyFm0pJ61QVn9em6gVcImxIYYnYy0RrgGwdUF8P29jwPGa0hLZ1JoI5swPT3t4VLFr0MZozywWtrTlubn4MxjyJ7ctmRBiqGTCLXzdWAGOgRq/EbZBcI3IgKBgGboRbTeEEbDIWCALXG0aud13nHKSEM8YoAgpNJmtEYCuX85eY2CBAGWRIUXR0xqr5axe1NVzjxJFh8zeX/kLKzpIF04h+xZMDBUAGDKuGTKDDD+M9Xe6VMFW83lpqYK7KpfoqXzYFYnqGGuue4sYFCBYtAA479LrkeySAarHQqv19H5Cefid9fZDBPkj/EFxfv6J/wLqBgX91+YFGDA05LYSEYhEaFiFRBMQO6mJ/0cRBY1+7sv0VJABkGGqMTz8xDM7kQNksNJsB1eXAdXXKzc2GG5v/y7Q0b6DmpoxtaFRuaVHMaLHc3PI/3FDfDfLE+KMsA0OcTWgUUGpB6i3+cHKWfldIGiLBJj67WMG8g+rmvFZnyLIha/yThyh5UGjGTYJbgpLUGyJAnVNkCRectPQa/y4nrdRfJHfiGFGUfjMQdkJFrcBNal9YSh5BTEYHo26E8YAtf/fF4Kyf8sYGVN53eFhB5DKTGQp10ITgAEyczfVoNvcZO2vOzob9DpCEY1JtLS1/U/UU+gDEOSCOS3lr5VAAMBZkTEKuQCA2IMMAl6eZgABsxegaGkGVVBRIyNsJMF6XYRAgvulN8k5V3Lt+HiqCpjoDEkZf0fkSyNFEuPc3cumX9FxKOU2Vm1lq/hqGxoPOnH74gm0nrZjzPfG9K6tj+aYIKWf3fq2vuLnOthaypn5AVCa1B9iwAGQN4yHb3nBQL1BeWr7nX4Gq6OyinmhQKMmtIAXAqQkEAEokGpTusRLlepqIi440+JAEC0Bm+OFotvt/tyHqmwwpWVINSoItcYIqISYiz9gOSjWYeDh5uJRSjtF8V5qy0UwpCEwKKcY4eEkLXn/Mgo4PX3b/bG7Iqmh5aHkM0q3SCgEBAEMEDobsP5+57xUAHldVy0yxlq3rix5ETtXhgJmn/ecBs06fvnmoA8gkTuM9f+2nWQBu50YdDqAmv5beUTBF26f6jvjo9qi0SVbyLxqAhrW/UeaC0sxphzMsDxiNcojp2FCJIGZo35Cjtx2/7I1X373x8zc/tPU40xSIi8FgeoH1nMQ5K8FYuLi3aM8/ZdnvTt5v7t+LKhnm2B95z78BxwrvMjFQFZ9HO01zIDL+PnwRCD9g2gVgxbDj3fkiuUCTj1SLIo3EUXM2s/6rbzv00hM/ecNx+VCVjUnIDKbe880GiPMxLWlvwKfecOB34GPxBvjbrf0l4ko2JP6bx2T3Ea+h6pHmVSjYAENhOPMlS2d9/4vnHPo1zUcGrCFXMJl5rGAiiJMoA+VvXXDk+xa1Nf7WObVE42WnrqGGnaMmAGtIQuAMUoJhFijwgVP3+f4HX7uyU3qKGcOIx0ooXQkwk7JqhMEw+NJ5L3n4zIPn/zJ2DsbUhF8NlUVNAO4xmLyUgyRfPU2AFhAgKg9/+c2Hrnr/WSs2RT15S0IRM+konk5MXDuk0mjWsGoocZwPg4vfcfj9F718n5NEpNMafnHkXdRQVagJwD0Aw9WA5bkmVHqR0ojfx/uiNOxefkylDIjWfuvth5/+2fMOe4DCUGQoJmM4ZiYln1iEtBoGwE6rckmHX8NnVH4wgSFVNhzH/UVqsBpd9oFjbvzomfu9MhLpImaz89FrqGH38WIJgryoQdAyentJclPUMSlpIhd2P+ZDYDAcnHBZk0/2kW52gkc/fuYBhx61tO2jF11+/8WPPt1NlLVKOePTlpRN2mB9ZzJqe56GtD6YGUogAQhSiEhD8NEHzpFLzjv8nYctbv1Z5FxgDRNBHVShxH9Dcd8apgI1AbhHYLjrigLkIgGKsRFLJJWgFSMCikJxFKMs94+IyDEBIsIvP6D9C7f9x+zLv3vDk/92yfWPv+WZ5wdbiKGaMQ6W1DAzCJx+fVhjLT9MyWh2TlUkFKZizEpE+y9p2XzRafv+4LxVS76QC2y/E+HAcAz4PksljokaaqggSKuNWaWGHeATpNMkauQeWt97cOdQpDahQp5os3EG4ETRmDF88OLWBwNDQ6pKSmnJB8GJGiY4IqLOweKSP96/8WO/unvj8Xc/27l0U3e+IY4TU5iMHzDJm/RRZvWNRQQlyWgCUHtbrvuY5XM2vunI+Ve/4qAF32ypCzaLKCmUmeGrgUr1CKilNtVQcdQE4B6B7X1/0zQLVRJVy8xxIo+xYdvQ29c8vy3zwPq+Vz6+oe/1G7oGXcdAaHoLDmHsICoIjEF9NsCcRusWz6wzKxfMuOSgRa1rjlgyY+P81vprAJAqSFQNJ9U0I46LUQtnaqihhhpqqKGGGmqooYYaaqihhhpqqKGGGmqooYYaaqihhhpqqKGGGmqooYYaaqihhhpqqKGGGmqooYYaaqihhhpqqKGGGmqooYYaaqihhhpqqKGGGmqooYYXBf4/c2werMAFzmwAAAAASUVORK5CYII= \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Fixtures/empty.bundle/nothingtoseehere.json b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Fixtures/empty.bundle/nothingtoseehere.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Fixtures/emptyfile.json b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Fixtures/emptyfile.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Fixtures/login.tail b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Fixtures/login.tail new file mode 100644 index 0000000000..283ca0271b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Fixtures/login.tail @@ -0,0 +1,10 @@ +GET|POST +.*/users +200 +application/json + +{ +"user_id": "happyuser1", +"user_token": "happytoken", +"status": "SUCCESS" +} \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Fixtures/login_content_type.tail b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Fixtures/login_content_type.tail new file mode 100644 index 0000000000..283ca0271b --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Fixtures/login_content_type.tail @@ -0,0 +1,10 @@ +GET|POST +.*/users +200 +application/json + +{ +"user_id": "happyuser1", +"user_token": "happytoken", +"status": "SUCCESS" +} \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Fixtures/login_content_type_and_headers.tail b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Fixtures/login_content_type_and_headers.tail new file mode 100644 index 0000000000..9cfc3e3cc8 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Fixtures/login_content_type_and_headers.tail @@ -0,0 +1,11 @@ +GET|POST +.*/users +200 +application/json +Connection: Close + +{ +"user_id": "happyuser1", +"user_token": "happytoken", +"status": "SUCCESS" +} \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Fixtures/login_headers.tail b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Fixtures/login_headers.tail new file mode 100644 index 0000000000..201d61812f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Fixtures/login_headers.tail @@ -0,0 +1,11 @@ +GET|POST +.*/users +200 +Content-Type: application/json +Connection: Close + +{ +"user_id": "happyuser1", +"user_token": "happytoken", +"status": "SUCCESS" +} \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/MocktailTests/MocktailTests.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/MocktailTests/MocktailTests.m new file mode 100644 index 0000000000..b8738bbd6a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/MocktailTests/MocktailTests.m @@ -0,0 +1,189 @@ +/*********************************************************************************** + * + * Copyright (c) 2015 Jinlian (Sunny) Wang + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + ***********************************************************************************/ + + +//////////////////////////////////////////////////////////////////////////////// + +#import + +#if OHHTTPSTUBS_USE_STATIC_LIBRARY || SWIFT_PACKAGE +#import "HTTPStubs.h" +#import "HTTPStubs+Mocktail.h" +#import "HTTPStubsResponse+JSON.h" +#else +@import OHHTTPStubs; +#endif + +@interface MocktailTests : XCTestCase +@property(nonatomic, strong) NSURLSession *session; +@end + +@implementation MocktailTests + +- (void)setUp +{ + [super setUp]; + [HTTPStubs removeAllStubs]; + + NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; + self.session = [NSURLSession sessionWithConfiguration:configuration delegate:nil delegateQueue:nil]; +} + +- (void)tearDown +{ + [super tearDown]; + [self.session invalidateAndCancel]; + self.session = nil; +} + +- (void)testMocktailLoginSuccess +{ + NSError *error = nil; + NSBundle *bundle = [NSBundle bundleForClass:self.class]; + [HTTPStubs stubRequestsUsingMocktailNamed:@"login" inBundle:bundle error: &error]; + XCTAssertNil(error, @"Error while stubbing 'login.tail':%@", [error localizedDescription]); + [self runLogin]; +} + +- (void)testMocktailsAtFolder +{ + NSError *error = nil; + NSBundle *bundle = [NSBundle bundleForClass:self.class]; + [HTTPStubs stubRequestsUsingMocktailsAtPath:@"MocktailFolder" inBundle:bundle error:&error]; + XCTAssertNil(error, @"Error while stubbing Mocktails at folder 'MocktailFolder': %@", [error localizedDescription]); + [self runLogin]; + [self runGetCards]; +} + +- (void)testMocktailHeaders +{ + NSError *error = nil; + NSBundle *bundle = [NSBundle bundleForClass:self.class]; + [HTTPStubs stubRequestsUsingMocktailNamed:@"login_headers" inBundle:bundle error: &error]; + XCTAssertNil(error, @"Error while stubbing 'login_headers.tail':%@", [error localizedDescription]); + NSHTTPURLResponse *response = [self runLogin]; + XCTAssertEqualObjects(response.allHeaderFields[@"Connection"], @"Close"); +} + +- (void)testMocktailContentType +{ + NSError *error = nil; + NSBundle *bundle = [NSBundle bundleForClass:self.class]; + [HTTPStubs stubRequestsUsingMocktailNamed:@"login_content_type" inBundle:bundle error: &error]; + XCTAssertNil(error, @"Error while stubbing 'login_content_type.tail':%@", [error localizedDescription]); + [self runLogin]; +} + +- (void)testMocktailContentTypeAndHeaders +{ + NSError *error = nil; + NSBundle *bundle = [NSBundle bundleForClass:self.class]; + [HTTPStubs stubRequestsUsingMocktailNamed:@"login_content_type_and_headers" inBundle:bundle error: &error]; + XCTAssertNil(error, @"Error while stubbing 'login_content_type_and_headers.tail':%@", [error localizedDescription]); + NSHTTPURLResponse *response = [self runLogin]; + XCTAssertEqualObjects(response.allHeaderFields[@"Connection"], @"Close"); +} + +- (NSHTTPURLResponse *)runLogin +{ + NSURL *url = [NSURL URLWithString:@"http://happywebservice.com/users"]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url + cachePolicy:NSURLRequestUseProtocolCachePolicy + timeoutInterval:60.0]; + + [request addValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + [request addValue:@"application/json" forHTTPHeaderField:@"Accept"]; + + request.HTTPMethod = @"POST"; + NSDictionary *mapData = @{@"iloveit": @"happyuser1", + @"password": @"username"}; + NSData *postData = [NSJSONSerialization dataWithJSONObject:mapData options:0 error:NULL]; + request.HTTPBody = postData; + + XCTestExpectation* expectation = [self expectationWithDescription:@"NSURLSessionDataTask completed"]; + + __block NSHTTPURLResponse *capturedResponse; + NSURLSessionDataTask *postDataTask = [self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if ([response isKindOfClass:[NSHTTPURLResponse class]]) capturedResponse = (id)response; + XCTAssertNil(error, @"Error while logging in."); + NSDictionary *json = nil; + if(!error && [@"application/json" isEqual:response.MIMEType]) + { + json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + } + + XCTAssertNotNil(json, @"The response is not a json object"); + XCTAssertEqualObjects(json[@"status"], @"SUCCESS", @"The response does to return a successful status"); + XCTAssertNotNil(json[@"user_token"], @"The response does not contain a user token"); + + [expectation fulfill]; + }]; + + [postDataTask resume]; + + [self waitForExpectationsWithTimeout:10 handler:nil]; + + return capturedResponse; +} + +- (NSHTTPURLResponse *)runGetCards +{ + NSURL *url = [NSURL URLWithString:@"http://happywebservice.com/cards"]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url + cachePolicy:NSURLRequestUseProtocolCachePolicy + timeoutInterval:60.0]; + + [request addValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + [request addValue:@"application/json" forHTTPHeaderField:@"Accept"]; + + request.HTTPMethod = @"GET"; + + XCTestExpectation* expectation = [self expectationWithDescription:@"NSURLSessionDataTask completed"]; + + __block NSHTTPURLResponse *capturedResponse; + NSURLSessionDataTask *getDataTask = [self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + if ([response isKindOfClass:[NSHTTPURLResponse class]]) capturedResponse = (id)response; + XCTAssertNil(error, @"Error while getting cards."); + + NSArray *json = nil; + if(!error && [@"application/json" isEqual:response.MIMEType]) + { + json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; + } + + XCTAssertNotNil(json, @"The response is not a json object"); + XCTAssertEqual(json.count, 2, @"The response does not contain 2 cards"); + XCTAssertEqualObjects([json firstObject][@"amount"], @"$25.28", @"The first card amount does not match"); + + [expectation fulfill]; + }]; + + [getDataTask resume]; + + [self waitForExpectationsWithTimeout:10 handler:nil]; + + return capturedResponse; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/OHHTTPStubsSwiftTests/SwiftHelpersTests.swift b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/OHHTTPStubsSwiftTests/SwiftHelpersTests.swift new file mode 100644 index 0000000000..b279149190 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/OHHTTPStubsSwiftTests/SwiftHelpersTests.swift @@ -0,0 +1,639 @@ +// +// SwiftHelpersTests.swift +// OHHTTPStubs +// +// Created by Olivier Halligon on 20/09/2015. +// Copyright © 2015 AliSoftware. All rights reserved. +// + +import Foundation +import XCTest +#if SWIFT_PACKAGE +@testable import OHHTTPStubsSwift +@testable import OHHTTPStubs +#else +@testable import OHHTTPStubs +#endif + +#if swift(>=3.0) +#else +#if swift(>=2.2) + extension SequenceType { + private func enumerated() -> EnumerateSequence { + return enumerate() + } + } + + extension NSMutableURLRequest { + override var httpMethod: String? { + get { + return HTTPMethod + } + set(method) { + self.HTTPMethod = method! + } + } + } +#endif +#endif + +class SwiftHelpersTests : XCTestCase { + + func testHTTPMethod() { + let methods = ["GET", "PUT", "PATCH", "POST", "DELETE", "HEAD", "FOO"] + let matchers = [isMethodGET(), isMethodPUT(), isMethodPATCH(), isMethodPOST(), isMethodDELETE(), isMethodHEAD()] + + for (idxMethod, method) in methods.enumerated() { +#if swift(>=3.0) + var req = URLRequest(url: URL(string: "foo://bar")!) +#else + let req = NSMutableURLRequest(URL: NSURL(string: "foo://bar")!) +#endif + req.httpMethod = method + for (idxMatcher, matcher) in matchers.enumerated() { + let expected = idxMethod == idxMatcher // expect to be true only if indexes match + XCTAssert(matcher(req) == expected, "Function is\(methods[idxMatcher])() failed to test request with HTTP method \(method).") + } + } + } + + func testIsAbsoluteURLString() { + let matcher = isAbsoluteURLString("foo://foo/bar?param1=123¶m2=foo#anchor") + + let urls = [ + "foo:": false, + "foo://foo/bar/baz": false, + "foo://foo/bar?param1=123¶m2=foo#anchor": true, + "foo://foo/bar?param1=123¶m2=foo": false, + "foo://foo/bar?param1=123": false, + "foo://foo/bar": false, + "bar://foo/bar": false, + "bar://foo/ba": false, + "foobar://": false + ] + + for (url, result) in urls { + #if swift(>=3.0) + let req = URLRequest(url: URL(string: url)!) + #else + let req = NSURLRequest(URL: NSURL(string: url)!) + #endif + XCTAssert(matcher(req) == result, "isAbsoluteURLString(\"foo://foo/bar?param1=123¶m2=foo#anchor\") matcher failed when testing url \(url)") + } + } + + func testIsScheme() { + let matcher = isScheme("foo") + + let urls = [ + "foo:": true, + "foo://": true, + "foo://bar/baz": true, + "bar://": false, + "bar://foo/": false, + "foobar://": false + ] + + for (url, result) in urls { +#if swift(>=3.0) + let req = URLRequest(url: URL(string: url)!) +#else + let req = NSURLRequest(URL: NSURL(string: url)!) +#endif + XCTAssert(matcher(req) == result, "isScheme(\"foo\") matcher failed when testing url \(url)") + } + } + + func testIsHost() { + let matcher = isHost("foo") + + let urls = [ + "foo:": false, + "foo://": false, + "foo://bar/baz": false, + "bar://foo": true, + "bar://foo/baz": true, + ] + + for (url, result) in urls { +#if swift(>=3.0) + let req = URLRequest(url: URL(string: url)!) +#else + let req = NSURLRequest(URL: NSURL(string: url)!) +#endif + XCTAssert(matcher(req) == result, "isHost(\"foo\") matcher failed when testing url \(url)") + } + } + +#if swift(>=3.0) + private let isSwift3 = true +#else + private let isSwift3 = false +#endif + + func testIsPath_absoluteURL() { + testIsPath("/foo/bar/baz", isAbsoluteMatcher: true) + } + + func testIsPath_relativeURL() { + testIsPath("foo/bar/baz", isAbsoluteMatcher: false) + } + + func testIsPath(_ path: String, isAbsoluteMatcher: Bool) { + let matcher = isPath(path) + // In Swift 2.3 and before, NSURL was not bridged to the URL value type in Swift + // And it happens that NSURL.path (Swift 2.2) does NOT contain the ";" (and string after) if any + // While URL.path (Swift 2.3/3.0) DOES contain the ";" (and the string after), if any + + let urls = [ + // Absolute URLs + "scheme:": false, + "scheme://": false, + "scheme://foo/bar/baz": false, + "scheme://host/foo/bar": false, + "scheme://host/foo/bar/baz": isAbsoluteMatcher, + "scheme://host/foo/bar/baz?q=1": isAbsoluteMatcher, + "scheme://host/foo/bar/baz#anchor": isAbsoluteMatcher, + "scheme://host/foo/bar/baz;param": isAbsoluteMatcher && !isSwift3, + "scheme://host/foo/bar/baz/wizz": false, + "scheme://host/path#/foo/bar/baz": false, + "scheme://host/path?/foo/bar/baz": false, + "scheme://host/path;/foo/bar/baz": false, + // Relative URLs + "foo/bar/baz": !isAbsoluteMatcher, + "foo/bar/baz?q=1": !isAbsoluteMatcher, + "foo/bar/baz#anchor": !isAbsoluteMatcher, + "foo/bar/baz;param": !isAbsoluteMatcher && !isSwift3, + "foo/bar/baz/wizz": false, + "path#/foo/bar/baz": false, + "path?/foo/bar/baz": false, + "path;/foo/bar/baz": false, + ] + + for (url, result) in urls { +#if swift(>=3.0) + let req = URLRequest(url: URL(string: url)!) +#else + let req = NSURLRequest(URL: NSURL(string: url)!) +#endif + let p = req.url?.path + print("URL: \(url) -> Path: \(String(reflecting: p))") + XCTAssert(matcher(req) == result, "isPath(\"\(path)\") matcher failed when testing url \(url)") + } + } + + func testPathStartsWith_absoluteURL() { + testPathStartsWith("/foo/bar", isAbsoluteMatcher: true) + } + + func testPathStartsWith_relativeURL() { + testPathStartsWith("foo/bar", isAbsoluteMatcher: false) + } + + private func testPathStartsWith(_ path: String, isAbsoluteMatcher: Bool) { + let matcher = pathStartsWith(path) + + let urls = [ + // Absolute URLs + "scheme:": false, + "scheme://": false, + "scheme://foo/bar/baz": false, + "scheme://host/foo/bar": isAbsoluteMatcher, + "scheme://host/foo/bar/baz": isAbsoluteMatcher, + "scheme://host/foo/bar?q=1": isAbsoluteMatcher, + "scheme://host/foo/bar#anchor": isAbsoluteMatcher, + "scheme://host/foo/bar;param": isAbsoluteMatcher, + "scheme://host/path/foo/bar/baz": false, + "scheme://host/path#/foo/bar/baz": false, + "scheme://host/path?/foo/bar/baz": false, + "scheme://host/path;/foo/bar/baz": false, + // Relative URLs + "foo/bar": !isAbsoluteMatcher, + "foo/bar/baz": !isAbsoluteMatcher, + "foo/bar?q=1": !isAbsoluteMatcher, + "foo/bar#anchor": !isAbsoluteMatcher, + "foo/bar;param": !isAbsoluteMatcher, + "path/foo/bar/baz": false, + "path#/foo/bar/baz": false, + "path?/foo/bar/baz": false, + "path;/foo/bar/baz": false, + ] + + for (url, result) in urls { +#if swift(>=3.0) + let req = URLRequest(url: URL(string: url)!) +#else + let req = NSURLRequest(URL: NSURL(string: url)!) +#endif + let p = req.url?.path + print("URL: \(url) -> Path: \(String(reflecting: p))") + XCTAssert(matcher(req) == result, "pathStartsWith(\"\(path)\") matcher failed when testing url \(url)") + } + } + + func testPathEndsWith() { + let path = "/bar" + let matcher = pathEndsWith(path) + + let urls = [ + // Absolute URLs + "scheme:": false, + "scheme://": false, + "scheme://foo/bar/baz": false, + "scheme://host/foo/bar": true, + "scheme://host/foo/bar/baz": false, + "scheme://host/foo/bar?q=1": true, + "scheme://host/foo/bar#anchor": true, + "scheme://host/foo/bar;param": !isSwift3, + "scheme://host/path/foo/bar/baz": false, + "scheme://host/path#/foo/bar": false, + "scheme://host/path?/foo/bar": false, + "scheme://host/path;/foo/bar": isSwift3, + // Relative URLs + "foo/bar": true, + "foo/bar/baz": false, + "foo/bar?q=1": true, + "foo/bar#anchor": true, + "foo/bar;param": !isSwift3, + "path/foo/bar/baz": false, + "path#/foo/bar": false, + "path?/foo/bar": false, + "path;/foo/bar": isSwift3, + ] + + for (url, result) in urls { + #if swift(>=3.0) + let req = URLRequest(url: URL(string: url)!) + #else + let req = NSURLRequest(URL: NSURL(string: url)!) + #endif + let p = req.url?.path + print("URL: \(url) -> Path: \(String(reflecting: p))") + XCTAssert(matcher(req) == result, "pathEndsWith(\"\(path)\") matcher failed when testing url \(url)") + } + } + + func testPathMatches_caseSensitive() { + testPathMatches("/bar/[0-9]+/baz$", caseInsensitive: false) + } + + func testPathMatches_casensensitive() { + testPathMatches("/bar/[0-9]+/baz$", caseInsensitive: true) + } + + private func testPathMatches(_ regexString: String, caseInsensitive: Bool) { + #if swift(>=3.0) + let options: NSRegularExpression.Options = caseInsensitive ? [.caseInsensitive] : [] + #else + let options: NSRegularExpressionOptions = caseInsensitive ? .CaseInsensitive : [] + #endif + let matcher = pathMatches(regexString, options: options) + + let urls = [ + // Case sensitive + "scheme://foo/bar/12/baz": true, + "scheme://host/foo/bar/12": false, + "scheme://host/foo/bar/12/baz?q=1": true, + "scheme://host/foo/bar/12/baz#anchor": true, + "scheme://host/foo/bar/12/baz;param": !isSwift3, + "scheme://host/path/foo/bar/12/baz": true, + "scheme://host/path#/foo/bar/12/baz": false, + "scheme://host/path?/foo/bar/12/baz": false, + "scheme://host/path;/foo/bar/12/baz": isSwift3, + // Case insensitive + "scheme://host/foo/bAr/12/baZ?q=1": caseInsensitive, + "scheme://host/foo/bAr/12/baZ#anchor": caseInsensitive, + "scheme://host/foo/bAr/12/baZ;param": caseInsensitive && !isSwift3, + "scheme://host/path/foo/bAr/12/baZ": caseInsensitive, + "scheme://host/path#/foo/bAr/12/baZ": false, + "scheme://host/path?/foo/bAr/12/baZ": false, + "scheme://host/path;/foo/bAr/12/baZ": caseInsensitive && isSwift3, + ] + + for (url, result) in urls { + #if swift(>=3.0) + let req = URLRequest(url: URL(string: url)!) + #else + let req = NSURLRequest(URL: NSURL(string: url)!) + #endif + let p = req.url?.path + print("URL: \(url) -> Path: \(String(reflecting: p))") + XCTAssert(matcher(req) == result, "pathMatches(\"\(regexString)\"\(caseInsensitive ? "i" : "")) matcher failed when testing url \(url)") + } + } + + func testIsExtension() { + let matcher = isExtension("txt") + + let urls = [ + "txt:": false, + "txt://": false, + "txt://txt/txt/txt": false, + "scheme://host/foo/bar.png": false, + "scheme://host/foo/bar.txt": true, + "scheme://host/foo/bar.txt?q=1": true, + "scheme://host/foo/bar.baz?q=wizz.txt": false, + ] + + for (url, result) in urls { +#if swift(>=3.0) + let req = URLRequest(url: URL(string: url)!) +#else + let req = NSURLRequest(URL: NSURL(string: url)!) +#endif + XCTAssert(matcher(req) == result, "isExtension(\"txt\") matcher failed when testing url \(url)") + } + + } + @available(iOS 8.0, OSX 10.10, *) + func testContainsQueryParams() { + let params: [String: String?] = ["q":"test", "lang":"en", "empty":"", "flag":nil] + let matcher = containsQueryParams(params) + + let urls = [ + "foo://bar": false, + "foo://bar?q=test": false, + "foo://bar?lang=en": false, + "foo://bar#q=test&lang=en&empty=&flag": false, + "foo://bar#lang=en&empty=&flag&q=test": false, + "foo://bar;q=test&lang=en&empty=&flag": false, + "foo://bar;lang=en&empty=&flag&q=test": false, + + "foo://bar?q=test&lang=en&empty=&flag": true, + "foo://bar?lang=en&flag&empty=&q=test": true, + "foo://bar?q=test&lang=en&empty=&flag#anchor": true, + "foo://bar?q=test&lang=en&empty&flag": false, // key "empty" with no value is matched against nil, not "" + "foo://bar?q=test&lang=en&empty=&flag=": false, // key "flag" with empty value is matched against "", not nil + "foo://bar?q=en&lang=test&empty=&flag": false, // param keys and values mismatch + "foo://bar?q=test&lang=en&empty=&flag&&wizz=fuzz": true, + "foo://bar?wizz=fuzz&empty=&lang=en&flag&&q=test": true, + "?q=test&lang=en&empty=&flag": true, + "?lang=en&flag&empty=&q=test": true, + ] + + for (url, result) in urls { +#if swift(>=3.0) + let req = URLRequest(url: URL(string: url)!) +#else + let req = NSURLRequest(URL: NSURL(string: url)!) +#endif + + XCTAssert(matcher(req) == result, "containsQueryParams(\"\(params)\") matcher failed when testing url \(url)") + } + } + + func testHasHeaderNamedIsTrue() { +#if swift(>=3.0) + var req = URLRequest(url: URL(string: "foo://bar")!) +#else + let req = NSMutableURLRequest(URL: NSURL(string: "foo://bar")!) +#endif + req.addValue("1234567890", forHTTPHeaderField: "ArbitraryKey") + + let hasHeader = hasHeaderNamed("ArbitraryKey")(req) + + XCTAssertTrue(hasHeader) + } + + func testHasHeaderNamedIsFalse() { +#if swift(>=3.0) + let req = URLRequest(url: URL(string: "foo://bar")!) +#else + let req = NSURLRequest(URL: NSURL(string: "foo://bar")!) +#endif + + let hasHeader = hasHeaderNamed("ArbitraryKey")(req) + + XCTAssertFalse(hasHeader) + } + + func testHeaderValueForKeyEqualsIsTrue() { +#if swift(>=3.0) + var req = URLRequest(url: URL(string: "foo://bar")!) +#else + let req = NSMutableURLRequest(URL: NSURL(string: "foo://bar")!) +#endif + req.addValue("bar", forHTTPHeaderField: "foo") + + let matchesHeader = hasHeaderNamed("foo", value: "bar")(req) + + XCTAssertTrue(matchesHeader) + } + + func testHeaderValueForKeyEqualsIsFalse() { +#if swift(>=3.0) + var req = URLRequest(url: URL(string: "foo://bar")!) +#else + let req = NSMutableURLRequest(URL: NSURL(string: "foo://bar")!) +#endif + req.addValue("bar", forHTTPHeaderField: "foo") + + let matchesHeader = hasHeaderNamed("foo", value: "baz")(req) + + XCTAssertFalse(matchesHeader) + } + + func testHeaderValueForKeyEqualsDoesNotExist() { +#if swift(>=3.0) + let req = URLRequest(url: URL(string: "foo://bar")!) +#else + let req = NSURLRequest(URL: NSURL(string: "foo://bar")!) +#endif + + let matchesHeader = hasHeaderNamed("foo", value: "baz")(req) + + XCTAssertFalse(matchesHeader) + } + + func test_ohhttpStubs_httpBody() { +#if swift(>=3.0) + let data = "Hello world".data(using: .utf8) + var req = URLRequest(url: URL(string: "foo://bar")!) + req.httpBody = data + XCTAssertEqual(req.ohhttpStubs_httpBody, data) +#else + let data = "Hello world".dataUsingEncoding(NSUTF8StringEncoding) + let req = NSMutableURLRequest(URL: NSURL(string: "foo://bar")!) + req.HTTPBody = data + XCTAssertEqual(req.OHHTTPStubs_HTTPBody(), data) +#endif + } + +#if swift(>=3.0) + func testHasJsonBodyIsTrue() { + let jsonStringsAndObjects = [ + // Exact match + ("{ \"foo\": \"bar\", \"baz\": 42, \"qux\": true }", + ["foo": "bar", "baz": 42, "qux": true]), + // Changed attribute order + ("{ \"qux\": true, \"foo\": \"bar\", \"baz\": 42 }", + ["foo": "bar", "baz": 42, "qux": true]), + // Newlines and indentations + ("{ \"foo\": \"bar\", \n\"baz\": 42, \"qux\": true }", + ["foo": "bar", "baz": 42, "qux": true]), + // Nested objects + ("{ \"foo\": \"bar\", \"baz\": { \"qux\": true, \"quux\": [\"spam\", \"ham\", \"eggs\"] } }", + ["foo": "bar", "baz": ["qux": true, "quux": ["spam", "ham", "eggs"]]]), + // Nested objects with changed attribute order + ("{ \"foo\": \"bar\", \"baz\": { \"quux\": [\"spam\", \"ham\", \"eggs\"], \"qux\": true } }", + ["foo": "bar", "baz": ["qux": true, "quux": ["spam", "ham", "eggs"]]]), + ] + + for (jsonString, expectedJsonObject) in jsonStringsAndObjects { + var req = URLRequest(url: URL(string: "foo://bar")!) + req.httpBody = jsonString.data(using: .utf8) + let matchesJsonBody = hasJsonBody(expectedJsonObject)(req) + + XCTAssertTrue(matchesJsonBody) + } + } +#endif + +#if swift(>=3.0) + func testHasJsonBodyIsFalse() { + let jsonStringsAndObjects = [ + // Changed value + ("{ \"foo\": \"bar\" }", + ["foo": "qux"]), + // Changed key + ("{ \"foo\": \"bar\" }", + ["baz": "bar"]), + // Missing attribute + ("{ \"foo\": \"bar\", \"baz\": 42 }", + ["foo": "bar"]), + // Extraneous attribute + ("{ \"foo\": \"bar\" }", + ["foo": "bar", "baz": 42]), + // Changed order in array + ("{ \"foo\": [\"spam\", \"ham\", \"eggs\"] }", + ["foo": ["spam", "eggs", "ham"]]), + // Nested objects with changed value + ("{ \"foo\": \"bar\", \"baz\": { \"qux\": true } }", + ["foo": "bar", "baz": ["qux": false]]) + ] + + for (jsonString, expectedJsonObject) in jsonStringsAndObjects { + var req = URLRequest(url: URL(string: "foo://bar")!) + req.httpBody = jsonString.data(using: .utf8) + let matchesJsonBody = hasJsonBody(expectedJsonObject)(req) + + XCTAssertFalse(matchesJsonBody) + } + } +#endif + +#if swift(>=3.0) + @available(iOS 8.0, OSX 10.10, *) + func testHasFormBodyIsTrue() { + func assertMatchesFormBody(_ formBody: String, _ expectedKeyValues: [String: String?], file: StaticString = #file, line: UInt = #line) { + var req = URLRequest(url: URL(string: "foo://bar")!) + req.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + req.httpBody = formBody.data(using: .utf8) + XCTAssertTrue(hasFormBody(expectedKeyValues)(req), file: file, line: line) + } + + // Exact match + assertMatchesFormBody("foo=bar&baz=42&qux=true", ["foo": "bar", "baz": "42", "qux": "true"]) + // Changed attribute order + assertMatchesFormBody("qux=true&foo=bar&baz=42", ["foo": "bar", "baz": "42", "qux": "true"]) + // Contains key with no value + assertMatchesFormBody("foo=bar&baz&qux=42", ["foo": "bar", "baz": nil, "qux": "42"]) + // Contains key with empty value + assertMatchesFormBody("foo=bar&baz=&qux=42", ["foo": "bar", "baz": "", "qux": "42"]) + // Contains escaped character + assertMatchesFormBody("foo=bar%40baz", ["foo": "bar@baz"]) + } +#endif + +#if swift(>=3.0) + @available(iOS 8.0, OSX 10.10, *) + func testHasFormBodyIsFalse() { + func assertNotMatchesFormBody(_ formBody: String, _ expectedKeyValues: [String: String?], file: StaticString = #file, line: UInt = #line) { + var req = URLRequest(url: URL(string: "foo://bar")!) + req.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + req.httpBody = formBody.data(using: .utf8) + XCTAssertFalse(hasFormBody(expectedKeyValues)(req), file: file, line: line) + } + // Changed value + assertNotMatchesFormBody("foo=bar&baz=40", ["foo": "bar", "baz": "42"]) + // Changed key + assertNotMatchesFormBody("foo=bar&qux=42", ["foo": "bar", "baz": "42"]) + // Missing attribute + assertNotMatchesFormBody("foo=bar&baz=42&qux=true", ["foo": "bar", "baz": "42"]) + // Extraneous attribute + assertNotMatchesFormBody("foo=bar&baz=42", ["foo": "bar", "baz": "42", "qux": "true"]) + // Missing value + assertNotMatchesFormBody("foo=&bar=baz", ["foo": nil, "bar": "baz"]) + // Extraneous value + assertNotMatchesFormBody("foo&bar=baz", ["foo": "", "baz": "42"]) + } +#endif + + let sampleURLs = [ + // Absolute URLs + "scheme:", + "scheme://", + "scheme://foo/bar/baz", + "scheme://host/foo/bar", + "scheme://host/foo/bar/baz", + "scheme://host/foo/bar/baz?q=1", + "scheme://host/foo/bar/baz#anchor", + "scheme://host/foo/bar/baz;param", + "scheme://host/foo/bar/baz/wizz", + "scheme://host/path#/foo/bar/baz", + "scheme://host/path?/foo/bar/baz", + "scheme://host/path;/foo/bar/baz", + // Relative URLs + "foo/bar/baz", + "foo/bar/baz?q=1", + "foo/bar/baz#anchor", + "foo/bar/baz;param", + "foo/bar/baz/wizz", + "path#/foo/bar/baz", + "path?/foo/bar/baz", + "path;/foo/bar/baz" + ] + + let trueMatcher: HTTPStubsTestBlock = { _ in return true } + let falseMatcher: HTTPStubsTestBlock = { _ in return false } + + func testOrOperator() { + for url in sampleURLs { +#if swift(>=3.0) + let req = URLRequest(url: URL(string: url)!) +#else + let req = NSURLRequest(URL: NSURL(string: url)!) +#endif + XCTAssert((trueMatcher || trueMatcher)(req) == true, "trueMatcher || trueMatcher should result in a trueMatcher") + XCTAssert((trueMatcher || falseMatcher)(req) == true, "trueMatcher || falseMatcher should result in a trueMatcher") + XCTAssert((falseMatcher || trueMatcher)(req) == true, "falseMatcher || trueMatcher should result in a trueMatcher") + XCTAssert((falseMatcher || falseMatcher)(req) == false, "falseMatcher || falseMatcher should result in a falseMatcher") + } + } + + func testAndOperator() { + for url in sampleURLs { +#if swift(>=3.0) + let req = URLRequest(url: URL(string: url)!) +#else + let req = NSURLRequest(URL: NSURL(string: url)!) +#endif + XCTAssert((trueMatcher && trueMatcher)(req) == true, "trueMatcher && trueMatcher should result in a trueMatcher") + XCTAssert((trueMatcher && falseMatcher)(req) == false, "trueMatcher && falseMatcher should result in a falseMatcher") + XCTAssert((falseMatcher && trueMatcher)(req) == false, "falseMatcher && trueMatcher should result in a falseMatcher") + XCTAssert((falseMatcher && falseMatcher)(req) == false, "falseMatcher && falseMatcher should result in a falseMatcher") + } + } + + func testNotOperator() { + for url in sampleURLs { +#if swift(>=3.0) + let req = URLRequest(url: URL(string: url)!) +#else + let req = NSURLRequest(URL: NSURL(string: url)!) +#endif + XCTAssert((!trueMatcher)(req) == false, "!trueMatcher should result in a falseMatcher") + XCTAssert((!falseMatcher)(req) == true, "!falseMatcher should result in a trueMatcher") + } + } +} diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/OHHTTPStubsTests/AFNetworkingTests.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/OHHTTPStubsTests/AFNetworkingTests.m new file mode 100644 index 0000000000..e5b4cf03f3 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/OHHTTPStubsTests/AFNetworkingTests.m @@ -0,0 +1,287 @@ +/*********************************************************************************** + * + * Copyright (c) 2012 Olivier Halligon + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + ***********************************************************************************/ + +#if SWIFT_PACKAGE +#warning "Skipping AFNetworking tests, due to AFNetworking not supporting Swift Package Manager. +#else + +#import +#import + +#if OHHTTPSTUBS_USE_STATIC_LIBRARY +#import "HTTPStubs.h" +#import "HTTPStubsResponse+JSON.h" +#else +@import OHHTTPStubs; +#endif + +#import "AFHTTPSessionManager.h" + +static const NSTimeInterval kResponseTimeMaxDelay = 2.5; + +@interface AFNetworkingTests : XCTestCase @end + +@implementation AFNetworkingTests + +-(void)setUp +{ + [super setUp]; + [HTTPStubs removeAllStubs]; +} + +-(void)test_AFHTTPRequestOperation_success +{ + static const NSTimeInterval kRequestTime = 0.05; + static const NSTimeInterval kResponseTime = 0.1; + NSData* expectedResponse = [NSStringFromSelector(_cmd) dataUsingEncoding:NSUTF8StringEncoding]; + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return YES; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + return [[HTTPStubsResponse responseWithData:expectedResponse statusCode:200 headers:nil] + requestTime:kRequestTime responseTime:kResponseTime]; + }]; + + XCTestExpectation* expectation = [self expectationWithDescription:@"AFHTTPRequestOperation request finished"]; + + NSURL *URL = [NSURL URLWithString:@"http://www.iana.org/domains/example/"]; + + __block __strong id response = nil; + AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; + [manager setResponseSerializer:[AFHTTPResponseSerializer serializer]]; + [manager GET:URL.absoluteString parameters:nil progress:nil success:^(NSURLSessionTask *task, id responseObject) { + response = responseObject; // keep strong reference + [expectation fulfill]; + } failure:^(NSURLSessionTask *operation, NSError *error) { + XCTFail(@"Unexpected network failure"); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kRequestTime+kResponseTime+kResponseTimeMaxDelay handler:nil]; + + XCTAssertEqualObjects(response, expectedResponse, @"Unexpected data received"); +} + +-(void)test_AFHTTPRequestOperation_multiple_choices +{ + static const NSTimeInterval kRequestTime = 0.05; + static const NSTimeInterval kResponseTime = 0.1; + NSData* expectedResponse = [NSStringFromSelector(_cmd) dataUsingEncoding:NSUTF8StringEncoding]; + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return YES; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + return [[HTTPStubsResponse responseWithData:expectedResponse statusCode:300 headers:@{@"Location":@"http://www.iana.org/domains/another/example"}] + requestTime:kRequestTime responseTime:kResponseTime]; + }]; + + XCTestExpectation* expectation = [self expectationWithDescription:@"AFHTTPRequestOperation request finished"]; + + NSURL *URL = [NSURL URLWithString:@"http://www.iana.org/domains/example/"]; + AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; + AFHTTPResponseSerializer* serializer = [AFHTTPResponseSerializer serializer]; + [serializer setAcceptableStatusCodes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(200, 101)]]; + [manager setResponseSerializer:serializer]; + + __block __strong id response = nil; + [manager setTaskWillPerformHTTPRedirectionBlock:^NSURLRequest * (NSURLSession * session, NSURLSessionTask * task, NSURLResponse * response, NSURLRequest * request) { + if (response == nil) { + return request; + } + XCTFail(@"Unexpected redirect"); + return nil; + }]; + + [manager GET:URL.absoluteString parameters:nil progress:nil success:^(NSURLSessionTask *task, id responseObject) { + response = responseObject; // keep strong reference + [expectation fulfill]; + } failure:^(NSURLSessionTask *operation, NSError *error) { + XCTFail(@"Unexpected network failure"); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kRequestTime+kResponseTime+kResponseTimeMaxDelay handler:nil]; + + XCTAssertEqualObjects(response, expectedResponse, @"Unexpected data received"); +} + +-(void)test_AFHTTPRequestOperation_redirect +{ + static const NSTimeInterval kRequestTime = 0.05; + static const NSTimeInterval kResponseTime = 0.1; + + NSURL* redirectURL = [NSURL URLWithString:@"https://httpbin.org/get"]; + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return YES; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + return [[HTTPStubsResponse responseWithData:[NSData data] statusCode:302 headers:@{@"Location":redirectURL.absoluteString}] + requestTime:kRequestTime responseTime:kResponseTime]; + }]; + + XCTestExpectation* redirectExpectation = [self expectationWithDescription:@"AFHTTPRequestOperation request was redirected"]; + XCTestExpectation* expectation = [self expectationWithDescription:@"AFHTTPRequestOperation request finished"]; + + NSURLRequest* req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://httpbin.org/redirect/1"]]; + AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; + AFHTTPResponseSerializer *serializer = [AFHTTPResponseSerializer serializer]; + serializer.acceptableStatusCodes = [NSIndexSet indexSetWithIndex:302]; + [manager setResponseSerializer:serializer]; + + __block __strong NSURL* url = nil; + [manager setTaskWillPerformHTTPRedirectionBlock:^NSURLRequest * (NSURLSession * session, NSURLSessionTask * task, NSURLResponse * response, NSURLRequest * request) { + if (response == nil) { + return request; + } + url = request.URL; + [redirectExpectation fulfill]; + return nil; + }]; + + [manager GET:req.URL.absoluteString parameters:nil progress:nil success:^(NSURLSessionTask *task, id responseObject) { + // Expect the 302 response when the redirection block returns nil (don't follow redirects) + [expectation fulfill]; + } failure:^(NSURLSessionTask *operation, NSError *error) { + XCTFail(@"Unexpected network failure"); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kRequestTime+kResponseTime+kResponseTimeMaxDelay handler:nil]; + + XCTAssertEqualObjects(url, redirectURL, @"Unexpected data received"); +} + +/* + * In order to establish that test_AFHTTPRequestOperation_redirect was incorrect and needed fixing, I needed + * to demonstrate identical behaviour--that is, returning the redirect response itself to the success block-- + * when running without the NSURLProtocol stubbing the request. The test below, if enabled, establishes this, + * as it is identical to test_AFHTTPRequestOperation_redirect except that it does not stub the requests. + */ +#if 0 +-(void)test_AFHTTPRequestOperation_redirect_baseline +{ + NSURL* redirectURL = [NSURL URLWithString:@"https://httpbin.org/get"]; + + XCTestExpectation* redirectExpectation = [self expectationWithDescription:@"AFHTTPRequestOperation request was redirected"]; + XCTestExpectation* expectation = [self expectationWithDescription:@"AFHTTPRequestOperation request finished"]; + + NSURLRequest* req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://httpbin.org/redirect/1"]]; + AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; + AFHTTPResponseSerializer *serializer = [AFHTTPResponseSerializer serializer]; + serializer.acceptableStatusCodes = [NSIndexSet indexSetWithIndex:302]; + [manager setResponseSerializer:serializer]; + + __block __strong NSURL* url = nil; + [manager setTaskWillPerformHTTPRedirectionBlock:^NSURLRequest * (NSURLSession * session, NSURLSessionTask * task, NSURLResponse * response, NSURLRequest * request) { + if (response == nil) { + return request; + } + url = request.URL; + [redirectExpectation fulfill]; + return nil; + }]; + + [manager GET:req.URL.absoluteString parameters:nil progress:nil success:^(NSURLSessionTask *task, id responseObject) { + // Expect the 302 response when the redirection block returns nil (don't follow redirects) + [expectation fulfill]; + } failure:^(NSURLSessionTask *operation, NSError *error) { + XCTFail(@"Unexpected network failure"); + [expectation fulfill]; + }]; + + // Allow a longer timeout as this test actually hits the network + [self waitForExpectationsWithTimeout:10 handler:nil]; + + XCTAssertEqualObjects(url, redirectURL, @"Unexpected data received"); +} +#endif + + +@end + + + +#pragma mark - NSURLSession / AFHTTPURLSession support + +// Compile this only if SDK version (…MAX_ALLOWED) is iOS7+/10.9+ because NSURLSession is a class only known starting these SDKs +// (this code won't compile if we use an eariler SDKs, like when building with Xcode4) +#if (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000) \ +|| (defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 1090) \ +|| (defined(__TV_OS_VERSION_MIN_REQUIRED) || defined(__WATCH_OS_VERSION_MIN_REQUIRED)) + + +#import "AFHTTPSessionManager.h" + +@interface AFNetworkingTests (NSURLSession) @end +@implementation AFNetworkingTests (NSURLSession) + +- (void)test_AFHTTPURLSessionCustom +{ + if ([NSURLSession class] && [NSURLSessionConfiguration class]) + { + static const NSTimeInterval kRequestTime = 0.1; + static const NSTimeInterval kResponseTime = 0.2; + NSDictionary *expectedResponseDict = @{@"Success" : @"Yes"}; + + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request.URL.scheme isEqualToString:@"stubs"]; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + return [[HTTPStubsResponse responseWithJSONObject:expectedResponseDict statusCode:200 headers:nil] + requestTime:kRequestTime responseTime:kResponseTime]; + }]; + + XCTestExpectation* expectation = [self expectationWithDescription:@"AFHTTPSessionManager request finished"]; + + NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; + NSURL* baseURL = [NSURL URLWithString:@"stubs://stubs/"]; + AFHTTPSessionManager *sessionManager = [[AFHTTPSessionManager alloc] initWithBaseURL:baseURL + sessionConfiguration:sessionConfig]; + + __block __strong id response = nil; + [sessionManager GET:@"foo" + parameters:nil + progress:nil + success:^(NSURLSessionDataTask *task, id responseObject) { + response = responseObject; // keep strong reference + [expectation fulfill]; + } + failure:^(NSURLSessionDataTask *task, NSError *error) { + XCTFail(@"Unexpected network failure"); + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kRequestTime+kResponseTime+kResponseTimeMaxDelay handler:nil]; + + XCTAssertEqualObjects(response, expectedResponseDict, @"Unexpected data received"); + } + else + { + NSLog(@"/!\\ Test skipped because the NSURLSession class is not available on this OS version. Run the tests a target with a more recent OS.\n"); + } +} + +@end + +#else +#warning Unit Tests using NSURLSession were not compiled nor executed, because NSURLSession is only available since iOS7/OSX10.9 SDK. \ +-------- Compile using iOS7 or OSX10.9 SDK then launch the tests on the iOS7 simulator or an OSX10.9 target for them to be executed. +#endif +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/OHHTTPStubsTests/NSURLConnectionDelegateTests.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/OHHTTPStubsTests/NSURLConnectionDelegateTests.m new file mode 100644 index 0000000000..1439822667 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/OHHTTPStubsTests/NSURLConnectionDelegateTests.m @@ -0,0 +1,405 @@ +/*********************************************************************************** + * + * Copyright (c) 2012 Olivier Halligon + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + ***********************************************************************************/ + +#import +// tvOS & watchOS deprecate use of NSURLConnection but these tests are based on it +#if (!defined(__TV_OS_VERSION_MIN_REQUIRED) && !defined(__WATCH_OS_VERSION_MIN_REQUIRED)) + +#import + +#if OHHTTPSTUBS_USE_STATIC_LIBRARY || SWIFT_PACKAGE +#import "HTTPStubs.h" +#else +@import OHHTTPStubs; +#endif + +@interface NSURLConnectionDelegateTests : XCTestCase @end + +static const NSTimeInterval kResponseTimeMaxDelay = 2.5; + +@implementation NSURLConnectionDelegateTests +{ + NSMutableData* _data; + NSError* _error; + + NSURL* _redirectRequestURL; + NSInteger _redirectResponseStatusCode; + + XCTestExpectation* _connectionFinishedExpectation; +} + +/////////////////////////////////////////////////////////////////////////////////// +#pragma mark Global Setup + NSURLConnectionDelegate implementation +/////////////////////////////////////////////////////////////////////////////////// + +-(void)setUp +{ + [super setUp]; + _data = [[NSMutableData alloc] init]; + [HTTPStubs removeAllStubs]; +} + +-(void)tearDown +{ + // in case the test timed out and finished before a running NSURLConnection ended, + // we may continue receive delegate messages anyway if we forgot to cancel. + // So avoid sending messages to deallocated object in this case by ensuring we reset it to nil + _data = nil; + _error = nil; + _connectionFinishedExpectation = nil; + [super tearDown]; +} + +- (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)response +{ + _redirectRequestURL = request.URL; + if (response) + { + if ([response isKindOfClass:NSHTTPURLResponse.class]) + { + _redirectResponseStatusCode = ((NSHTTPURLResponse *) response).statusCode; + } + else + { + _redirectResponseStatusCode = 0; + } + } + else + { + // we get a nil response when NSURLConnection canonicalizes the URL, we don't care about that. + } + return request; +} + +-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response +{ + _data.length = 0U; +} + +-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data +{ + [_data appendData:data]; +} + +-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error +{ + _error = error; // keep strong reference + [_connectionFinishedExpectation fulfill]; +} + +-(void)connectionDidFinishLoading:(NSURLConnection *)connection +{ + [_connectionFinishedExpectation fulfill]; +} + + + +/////////////////////////////////////////////////////////////////////////////////// +#pragma mark NSURLConnection + Delegate +/////////////////////////////////////////////////////////////////////////////////// + +-(void)test_NSURLConnectionDelegate_success +{ + static const NSTimeInterval kRequestTime = 0.1; + static const NSTimeInterval kResponseTime = 0.5; + NSData* testData = [NSStringFromSelector(_cmd) dataUsingEncoding:NSUTF8StringEncoding]; + + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return YES; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + return [[HTTPStubsResponse responseWithData:testData + statusCode:200 + headers:nil] + requestTime:kRequestTime responseTime:kResponseTime]; + }]; + + _connectionFinishedExpectation = [self expectationWithDescription:@"NSURLConnection did finish (with error or success)"]; + + NSURLRequest* req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.iana.org/domains/example/"]]; + NSDate* startDate = [NSDate date]; + + NSURLConnection* cxn = [NSURLConnection connectionWithRequest:req delegate:self]; + + [self waitForExpectationsWithTimeout:kRequestTime+kResponseTime+kResponseTimeMaxDelay handler:nil]; + + XCTAssertEqualObjects(_data, testData, @"Invalid data response"); + XCTAssertNil(_error, @"Received unexpected network error %@", _error); + XCTAssertGreaterThan(-[startDate timeIntervalSinceNow], kRequestTime+kResponseTime, @"Invalid response time"); + + // in case we timed out before the end of the request (test failed), cancel the request to avoid further delegate method calls + [cxn cancel]; +} + +-(void)test_NSURLConnectionDelegate_multiple_choices +{ + static const NSTimeInterval kRequestTime = 0.1; + static const NSTimeInterval kResponseTime = 0.5; + NSData* testData = [NSStringFromSelector(_cmd) dataUsingEncoding:NSUTF8StringEncoding]; + + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return YES; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + return [[HTTPStubsResponse responseWithData:testData + statusCode:300 + headers:@{@"Location":@"http://www.iana.org/domains/another/example"}] + requestTime:kRequestTime responseTime:kResponseTime]; + }]; + + _connectionFinishedExpectation = [self expectationWithDescription:@"NSURLConnection did finish (with error or success)"]; + + NSURLRequest* req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.iana.org/domains/example/"]]; + NSDate* startDate = [NSDate date]; + + NSURLConnection* cxn = [NSURLConnection connectionWithRequest:req delegate:self]; + + [self waitForExpectationsWithTimeout:kRequestTime+kResponseTime+kResponseTimeMaxDelay handler:nil]; + + XCTAssertEqualObjects(_data, testData, @"Invalid data response"); + XCTAssertNil(_error, @"Received unexpected network error %@", _error); + XCTAssertGreaterThan(-[startDate timeIntervalSinceNow], kRequestTime+kResponseTime, @"Invalid response time"); + + // in case we timed out before the end of the request (test failed), cancel the request to avoid further delegate method calls + [cxn cancel]; +} + +-(void)test_NSURLConnectionDelegate_error +{ + static const NSTimeInterval kResponseTime = 0.5; + NSError* expectedError = [NSError errorWithDomain:NSURLErrorDomain code:404 userInfo:nil]; + + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return YES; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + HTTPStubsResponse* resp = [HTTPStubsResponse responseWithError:expectedError]; + resp.responseTime = kResponseTime; + return resp; + }]; + + _connectionFinishedExpectation = [self expectationWithDescription:@"NSURLConnection did finish (with error or success)"]; + + NSURLRequest* req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.iana.org/domains/example/"]]; + NSDate* startDate = [NSDate date]; + + NSURLConnection* cxn = [NSURLConnection connectionWithRequest:req delegate:self]; + + [self waitForExpectationsWithTimeout:kResponseTime+kResponseTimeMaxDelay handler:nil]; + + XCTAssertEqual(_data.length, (NSUInteger)0, @"Received unexpected network data %@", _data); + XCTAssertEqualObjects(_error.domain, expectedError.domain, @"Invalid error response domain"); + XCTAssertEqual(_error.code, expectedError.code, @"Invalid error response code"); + XCTAssertGreaterThan(-[startDate timeIntervalSinceNow], kResponseTime, @"Invalid response time"); + + // in case we timed out before the end of the request (test failed), cancel the request to avoid further delegate method calls + [cxn cancel]; +} + + +/////////////////////////////////////////////////////////////////////////////////// +#pragma mark Cancelling requests +/////////////////////////////////////////////////////////////////////////////////// + +-(void)test_NSURLConnection_cancel +{ + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return YES; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + return [[HTTPStubsResponse responseWithData:[@"" dataUsingEncoding:NSUTF8StringEncoding] + statusCode:500 + headers:nil] + requestTime:0.0 responseTime:1.5]; + }]; + + NSURLRequest* req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.iana.org/domains/example/"]]; + NSURLConnection* cxn = [NSURLConnection connectionWithRequest:req delegate:self]; + + XCTestExpectation* waitExpectation = [self expectationWithDescription:@"Waiting 2s, after cancelling in the middle"]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [cxn cancel]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [waitExpectation fulfill]; + }); + }); + + [self waitForExpectationsWithTimeout:5 handler:^(NSError *error) { + // in case we timed out before the end of the request (test failed), cancel the request to avoid further delegate method calls + [cxn cancel]; + }]; + + XCTAssertEqual(_data.length, (NSUInteger)0, @"Received unexpected data but the request should have been cancelled"); + XCTAssertNil(_error, @"Received unexpected network error but the request should have been cancelled"); + +} + + +/////////////////////////////////////////////////////////////////////////////////// +#pragma mark Cancelling requests +/////////////////////////////////////////////////////////////////////////////////// + +-(void)test_NSURLConnection_cookies +{ + NSString* const cookieName = @"SESSIONID"; + NSString* const cookieValue = [NSProcessInfo.processInfo globallyUniqueString]; + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return YES; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + NSString* cookie = [NSString stringWithFormat:@"%@=%@;", cookieName, cookieValue]; + NSDictionary* headers = @{@"Set-Cookie": cookie}; + return [[HTTPStubsResponse responseWithData:[@"Yummy cookies" dataUsingEncoding:NSUTF8StringEncoding] + statusCode:200 + headers:headers] + requestTime:0.0 responseTime:0.1]; + }]; + + _connectionFinishedExpectation = [self expectationWithDescription:@"NSURLConnection did finish (with error or success)"]; + + // Set the cookie accept policy to accept all cookies from the main document domain + // (especially in case the previous policy was "NSHTTPCookieAcceptPolicyNever") + NSHTTPCookieStorage* cookieStorage = NSHTTPCookieStorage.sharedHTTPCookieStorage; + NSHTTPCookieAcceptPolicy previousAcceptPolicy = cookieStorage.cookieAcceptPolicy; // keep it to restore later + cookieStorage.cookieAcceptPolicy = NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain; + + // Send the request and wait for the response containing the Set-Cookie headers + NSURLRequest* req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.iana.org/domains/example/"]]; + NSURLConnection* cxn = [NSURLConnection connectionWithRequest:req delegate:self]; + [self waitForExpectationsWithTimeout:kResponseTimeMaxDelay handler:^(NSError *error) { + [cxn cancel]; // In case we timed out (test failed), cancel the request to avoid further delegate method calls + }]; + + /* Check that the cookie has been properly stored */ + NSArray* cookies = [cookieStorage cookiesForURL:req.URL]; + BOOL cookieFound = NO; + for (NSHTTPCookie* cookie in cookies) + { + if ([cookie.name isEqualToString:cookieName]) + { + cookieFound = YES; + XCTAssertEqualObjects(cookie.value, cookieValue, @"The cookie does not have the expected value"); + } + } + XCTAssertTrue(cookieFound, @"The cookie was not stored as expected"); + + + // As a courtesy, restore previous policy before leaving + cookieStorage.cookieAcceptPolicy = previousAcceptPolicy; + +} + + +/////////////////////////////////////////////////////////////////////////////////// +#pragma mark Redirected requests +/////////////////////////////////////////////////////////////////////////////////// + +- (void)test_NSURLConnection_redirected +{ + static const NSTimeInterval kRequestTime = 0.1; + static const NSTimeInterval kResponseTime = 0.5; + NSData* redirectData = [[NSString stringWithFormat:@"%@ - redirect", NSStringFromSelector(_cmd)] dataUsingEncoding:NSUTF8StringEncoding]; + NSData* testData = [NSStringFromSelector(_cmd) dataUsingEncoding:NSUTF8StringEncoding]; + NSURL* redirectURL = [NSURL URLWithString:@"http://www.yahoo.com/"]; + NSString* redirectCookieName = @"yahooCookie"; + NSString* redirectCookieValue = [NSProcessInfo.processInfo globallyUniqueString]; + + // Set the cookie accept policy to accept all cookies from the main document domain + // (especially in case the previous policy was "NSHTTPCookieAcceptPolicyNever") + NSHTTPCookieStorage* cookieStorage = NSHTTPCookieStorage.sharedHTTPCookieStorage; + NSHTTPCookieAcceptPolicy previousAcceptPolicy = cookieStorage.cookieAcceptPolicy; // keep it to restore later + cookieStorage.cookieAcceptPolicy = NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain; + + NSString* endCookieName = @"googleCookie"; + NSString* endCookieValue = [NSProcessInfo.processInfo globallyUniqueString]; + NSURL *endURL = [NSURL URLWithString:@"http://www.google.com/"]; + + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return YES; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + if ([request.URL isEqual:redirectURL]) { + NSString* redirectCookie = [NSString stringWithFormat:@"%@=%@;", redirectCookieName, redirectCookieValue]; + NSDictionary* headers = @{ @"Location": endURL.absoluteString, + @"Set-Cookie": redirectCookie }; + return [[HTTPStubsResponse responseWithData:redirectData + statusCode:311 // any 300-level request will do + headers:headers] + requestTime:kRequestTime responseTime:kResponseTime]; + } else { + NSString* endCookie = [NSString stringWithFormat:@"%@=%@;", endCookieName, endCookieValue]; + NSDictionary* headers = @{ @"Set-Cookie": endCookie }; + return [[HTTPStubsResponse responseWithData:testData + statusCode:200 + headers:headers] + requestTime:kRequestTime responseTime:kResponseTime]; + } + }]; + + _connectionFinishedExpectation = [self expectationWithDescription:@"NSURLConnection did finish (with error or success)"]; + + NSURLRequest* req = [NSURLRequest requestWithURL:redirectURL]; + NSDate* startDate = [NSDate date]; + + NSURLConnection* cxn = [NSURLConnection connectionWithRequest:req delegate:self]; + + [self waitForExpectationsWithTimeout:2 * (kRequestTime+kResponseTime+kResponseTimeMaxDelay) handler:^(NSError *error) { + // in case we timed out before the end of the request (test failed), cancel the request to avoid further delegate method calls + [cxn cancel]; + }]; + + XCTAssertEqualObjects(_redirectRequestURL, endURL, @"Invalid redirect request URL"); + XCTAssertEqual(_redirectResponseStatusCode, (NSInteger)311, @"Invalid redirect response status code"); + XCTAssertEqualObjects(_data, testData, @"Invalid data response"); + XCTAssertNil(_error, @"Received unexpected network error %@", _error); + XCTAssertGreaterThan(-[startDate timeIntervalSinceNow], (2 * kRequestTime) + kResponseTime, @"Invalid response time"); + + /* Check that the redirect cookie has been properly stored */ + NSArray* redirectCookies = [cookieStorage cookiesForURL:req.URL]; + BOOL redirectCookieFound = NO; + for (NSHTTPCookie* cookie in redirectCookies) + { + if ([cookie.name isEqualToString:redirectCookieName]) + { + redirectCookieFound = YES; + XCTAssertEqualObjects(cookie.value, redirectCookieValue, @"The redirect cookie does not have the expected value"); + } + } + XCTAssertTrue(redirectCookieFound, @"The redirect cookie was not stored as expected"); + + /* Check that the end cookie has been properly stored */ + NSArray* endCookies = [cookieStorage cookiesForURL:endURL]; + BOOL endCookieFound = NO; + for (NSHTTPCookie* cookie in endCookies) + { + if ([cookie.name isEqualToString:endCookieName]) + { + endCookieFound = YES; + XCTAssertEqualObjects(cookie.value, endCookieValue, @"The end cookie does not have the expected value"); + } + } + XCTAssertTrue(endCookieFound, @"The end cookie was not stored as expected"); + + + // As a courtesy, restore previous policy before leaving + cookieStorage.cookieAcceptPolicy = previousAcceptPolicy; +} + +@end + +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/OHHTTPStubsTests/NSURLConnectionTests.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/OHHTTPStubsTests/NSURLConnectionTests.m new file mode 100644 index 0000000000..531d97ba56 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/OHHTTPStubsTests/NSURLConnectionTests.m @@ -0,0 +1,198 @@ +/*********************************************************************************** + * + * Copyright (c) 2012 Olivier Halligon + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + ***********************************************************************************/ + +#import +// tvOS & watchOS deprecate use of NSURLConnection but these tests are based on it +#if (!defined(__TV_OS_VERSION_MIN_REQUIRED) && !defined(__WATCH_OS_VERSION_MIN_REQUIRED)) + +#import + +#if OHHTTPSTUBS_USE_STATIC_LIBRARY || SWIFT_PACKAGE +#import "HTTPStubs.h" +#else +@import OHHTTPStubs; +#endif + +@interface NSURLConnectionTests : XCTestCase @end + +static const NSTimeInterval kResponseTimeMaxDelay = 2.5; + +@implementation NSURLConnectionTests + +-(void)setUp +{ + [super setUp]; + [HTTPStubs removeAllStubs]; +} + +static const NSTimeInterval kRequestTime = 0.1; +static const NSTimeInterval kResponseTime = 0.5; + +/////////////////////////////////////////////////////////////////////////////////// +#pragma mark [NSURLConnection sendSynchronousRequest:returningResponse:error:] +/////////////////////////////////////////////////////////////////////////////////// + +-(void)test_NSURLConnection_sendSyncronousRequest_mainQueue +{ + NSData* testData = [NSStringFromSelector(_cmd) dataUsingEncoding:NSUTF8StringEncoding]; + + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return YES; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + return [[HTTPStubsResponse responseWithData:testData + statusCode:200 + headers:nil] + requestTime:kRequestTime responseTime:kResponseTime]; + }]; + + NSURLRequest* req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.iana.org/domains/example/"]]; + NSDate* startDate = [NSDate date]; + + NSData* data = [NSURLConnection sendSynchronousRequest:req returningResponse:NULL error:NULL]; + + XCTAssertEqualObjects(data, testData, @"Invalid data response"); + XCTAssertGreaterThan(-[startDate timeIntervalSinceNow], kRequestTime+kResponseTime, @"Invalid response time"); +} + +-(void)test_NSURLConnection_sendSyncronousRequest_parallelQueue +{ + XCTestExpectation* expectation = [self expectationWithDescription:@"Synchronous request completed"]; + [[NSOperationQueue new] addOperationWithBlock:^{ + [self test_NSURLConnection_sendSyncronousRequest_mainQueue]; + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kRequestTime+kResponseTime+kResponseTimeMaxDelay handler:nil]; +} + +/////////////////////////////////////////////////////////////////////////////////// +#pragma mark Single [NSURLConnection sendAsynchronousRequest:queue:completionHandler:] +/////////////////////////////////////////////////////////////////////////////////// + +-(void)_test_NSURLConnection_sendAsyncronousRequest_onOperationQueue:(NSOperationQueue*)queue +{ + NSData* testData = [NSStringFromSelector(_cmd) dataUsingEncoding:NSUTF8StringEncoding]; + + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return YES; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + return [[HTTPStubsResponse responseWithData:testData + statusCode:200 + headers:nil] + requestTime:kRequestTime responseTime:kResponseTime]; + }]; + + XCTestExpectation* expectation = [self expectationWithDescription:@"Asynchronous request finished"]; + + NSURLRequest* req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.iana.org/domains/example/"]]; + NSDate* startDate = [NSDate date]; + + [NSURLConnection sendAsynchronousRequest:req queue:queue completionHandler:^(NSURLResponse* resp, NSData* data, NSError* error) + { + XCTAssertEqualObjects(data, testData, @"Invalid data response"); + XCTAssertGreaterThan(-[startDate timeIntervalSinceNow], kRequestTime+kResponseTime, @"Invalid response time"); + + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kRequestTime+kResponseTime+kResponseTimeMaxDelay handler:nil]; +} + + +-(void)test_NSURLConnection_sendAsyncronousRequest_mainQueue +{ + [self _test_NSURLConnection_sendAsyncronousRequest_onOperationQueue:NSOperationQueue.mainQueue]; +} + + +-(void)test_NSURLConnection_sendAsyncronousRequest_parallelQueue +{ + [self _test_NSURLConnection_sendAsyncronousRequest_onOperationQueue:[NSOperationQueue new]]; +} + + +/////////////////////////////////////////////////////////////////////////////////// +#pragma mark Multiple Parallel [NSURLConnection sendAsynchronousRequest:queue:completionHandler:] +/////////////////////////////////////////////////////////////////////////////////// + +-(void)_test_NSURLConnection_sendMultipleAsyncronousRequestsOnOperationQueue:(NSOperationQueue*)queue +{ + __block BOOL testFinished = NO; + NSData* (^dataForRequest)(NSURLRequest*) = ^(NSURLRequest* req) { + return [[NSString stringWithFormat:@"",req.URL.absoluteString] dataUsingEncoding:NSUTF8StringEncoding]; + }; + + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return YES; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + NSData* retData = dataForRequest(request); + NSTimeInterval responseTime = [request.URL.lastPathComponent doubleValue]; + return [[HTTPStubsResponse responseWithData:retData + statusCode:200 + headers:nil] + requestTime:responseTime*.1 responseTime:responseTime]; + }]; + + // Reusable code to send a request that will respond in the given response time + void (^sendAsyncRequest)(NSTimeInterval) = ^(NSTimeInterval responseTime) + { + NSString* desc = [NSString stringWithFormat:@"Asynchronous request with response time %.f finished", responseTime]; + XCTestExpectation* expectation = [self expectationWithDescription:desc]; + + NSString* urlString = [NSString stringWithFormat:@"http://dummyrequest/concurrent/time/%f",responseTime]; + NSURLRequest* req = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString]]; +// [SenTestLog testLogWithFormat:@"== Sending request %@\n", req]; + NSDate* startDate = [NSDate date]; + [NSURLConnection sendAsynchronousRequest:req queue:queue completionHandler:^(NSURLResponse* resp, NSData* data, NSError* error) + { +// [SenTestLog testLogWithFormat:@"== Received response for request %@\n", req]; + XCTAssertEqualObjects(data, dataForRequest(req), @"Invalid data response"); + XCTAssertGreaterThan(-[startDate timeIntervalSinceNow], (responseTime*.1)+responseTime, @"Invalid response time"); + + if (!testFinished) [expectation fulfill]; + }]; + }; + + static NSTimeInterval time3 = 1.5, time2 = 1.0, time1 = 0.5; + sendAsyncRequest(time3); // send this one first, should receive last + sendAsyncRequest(time2); // send this one next, shoud receive 2nd + sendAsyncRequest(time1); // send this one last, should receive first + + [self waitForExpectationsWithTimeout:MAX(time1,MAX(time2,time3))+kResponseTimeMaxDelay handler:nil]; + testFinished = YES; +} + +-(void)test_NSURLConnection_sendMultipleAsyncronousRequests_mainQueue +{ + [self _test_NSURLConnection_sendMultipleAsyncronousRequestsOnOperationQueue:NSOperationQueue.mainQueue]; +} + +-(void)test_NSURLConnection_sendMultipleAsyncronousRequests_parallelQueue +{ + [self _test_NSURLConnection_sendMultipleAsyncronousRequestsOnOperationQueue:[NSOperationQueue new]]; +} + + +@end + +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/OHHTTPStubsTests/NSURLSessionTests.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/OHHTTPStubsTests/NSURLSessionTests.m new file mode 100644 index 0000000000..0791428220 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/OHHTTPStubsTests/NSURLSessionTests.m @@ -0,0 +1,656 @@ +/*********************************************************************************** + * + * Copyright (c) 2012 Olivier Halligon + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + ***********************************************************************************/ + +#import +// Compile this only if SDK version (…MAX_ALLOWED) is iOS7+/10.9+ because NSURLSession is a class only known starting these SDKs +// (this code won't compile if we use an eariler SDKs, like when building with Xcode4) +#if (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000) \ + || (defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 1090) \ + || (defined(__TV_OS_VERSION_MIN_REQUIRED) || defined(__WATCH_OS_VERSION_MIN_REQUIRED)) + +#import + +#if OHHTTPSTUBS_USE_STATIC_LIBRARY || SWIFT_PACKAGE +#import "HTTPStubs.h" +#import "HTTPStubsResponse+JSON.h" +#import "NSURLRequest+HTTPBodyTesting.h" +#else +@import OHHTTPStubs; +#endif + +@interface NSURLSessionTestDelegate: NSObject ++(instancetype)delegateFollowingRedirects:(BOOL)shouldFollowRedirects fulfillOnCompletion:(XCTestExpectation*)expectationToFulfill; +-(void)resetWithNewExpectation:(XCTestExpectation*)expectationToFulfill; +@property(readonly) NSData* receivedData; +@property(readonly) NSError* receivedError; +@end + +@interface NSURLSessionTests : XCTestCase @end + +@implementation NSURLSessionTests + +- (void)setUp +{ + [super setUp]; + [HTTPStubs removeAllStubs]; +} + +- (void)_test_NSURLSession:(NSURLSession*)session + jsonForStub:(id)json + completion:(void(^)(NSError* errorResponse,id jsonResponse))completion +{ + if ([NSURLSession class]) + { + static const NSTimeInterval kRequestTime = 0.0; + static const NSTimeInterval kResponseTime = 0.2; + + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return YES; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + return [[HTTPStubsResponse responseWithJSONObject:json statusCode:200 headers:nil] + requestTime:kRequestTime responseTime:kResponseTime]; + }]; + + XCTestExpectation* expectation = [self expectationWithDescription:@"NSURLSessionDataTask completed"]; + + __block __strong id dataResponse = nil; + __block __strong NSError* errorResponse = nil; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"foo://unknownhost:666"]]; + request.HTTPMethod = @"GET"; + [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + [request setValue:@"application/json" forHTTPHeaderField:@"Accept"]; + + NSURLSessionDataTask *task = [session dataTaskWithRequest:request + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) + { + errorResponse = error; + if (!error) + { + NSError *jsonError = nil; + NSDictionary *jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; + XCTAssertNil(jsonError, @"Unexpected error deserializing JSON response"); + dataResponse = jsonObject; + } + [expectation fulfill]; + }]; + + [task resume]; + + [self waitForExpectationsWithTimeout:kRequestTime+kResponseTime+0.5 handler:^(NSError * _Nullable error) { + completion(errorResponse, dataResponse); + }]; + } +} + +- (void)_test_redirect_NSURLSession:(NSURLSession*)session + httpMethod:(NSString *)requestHTTPMethod + headers:(NSDictionary *)headers + jsonBody:(NSDictionary*)json + delays:(NSTimeInterval)delay + redirectStatusCode:(int)redirectStatusCode + completion:(void(^)(NSString* redirectedRequestMethod, NSDictionary * redirectedRequestHeaders, id redirectedRequestJSONBody, NSHTTPURLResponse *redirectHTTPResponse, id finalJSONResponse, NSError *errorResponse))completion +{ + if ([NSURLSession class]) + { + const NSTimeInterval requestTime = delay; + const NSTimeInterval responseTime = delay; + + __block __strong NSString* capturedRedirectedRequestMethod = nil; + __block __strong NSDictionary* capturedRedirectedRequestHeaders = nil; + __block __strong id capturedRedirectedRequestJSONBody = nil; + __block __strong NSHTTPURLResponse* capturedRedirectHTTPResponse = nil; + __block __strong id capturedResponseJSONBody = nil; + __block __strong NSError* capturedResponseError = nil; + + NSData* requestBody = json ? [NSJSONSerialization dataWithJSONObject:json options:0 error:NULL] : nil; + + // First request: just return a redirect response (3xx, empty body) + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [[[request URL] path] isEqualToString:@"/oldlocation"]; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *originalRequest) { + NSDictionary *headers = @{ @"Location": @"foo://unknownhost:666/newlocation" }; + return [[HTTPStubsResponse responseWithData:[NSData new] + statusCode:redirectStatusCode + headers:headers] + requestTime:requestTime responseTime:responseTime]; + }]; + + // Second request = redirected location: capture method+body of the redirected request + return 200 with the finalJSONResponse + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [[[request URL] path] isEqualToString:@"/newlocation"]; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *redirectedRequest) { + capturedRedirectedRequestMethod = redirectedRequest.HTTPMethod; + capturedRedirectedRequestHeaders = redirectedRequest.allHTTPHeaderFields; + if (redirectedRequest.OHHTTPStubs_HTTPBody) { + capturedRedirectedRequestJSONBody = [NSJSONSerialization JSONObjectWithData:redirectedRequest.OHHTTPStubs_HTTPBody options:0 error:NULL]; + } else { + capturedRedirectedRequestJSONBody = nil; + } + return [[HTTPStubsResponse responseWithJSONObject:@{ @"RequestBody": json ?: [NSNull null] } + statusCode:200 + headers:nil] + requestTime:requestTime responseTime:responseTime]; + }]; + + XCTestExpectation* expectation = [self expectationWithDescription:@"NSURLSessionDataTask completed"]; + + // Building the initial request. + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"foo://unknownhost:666/oldlocation"]]; + request.HTTPMethod = requestHTTPMethod; + request.allHTTPHeaderFields = headers; + if (requestBody) + { + request.HTTPBody = requestBody; + [request setValue:[NSString stringWithFormat:@"%lu", (unsigned long)(request.HTTPBody.length)] forHTTPHeaderField:@"Content-Length"]; + [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + } + [request setValue:@"application/json" forHTTPHeaderField:@"Accept"]; + + NSDate *startTime = [NSDate date]; + NSURLSessionDataTask *task = + [session dataTaskWithRequest:request + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) + { + if (!capturedResponseError) { + // In case there was already a captured error before, we prefer to report the first one rather than the last one + capturedResponseError = error; + } + NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse *)response; + if ([HTTPResponse statusCode] >= 300 && [HTTPResponse statusCode] < 400) { + // Response for the redirect + NSTimeInterval redirectResponseTime = [[NSDate date] timeIntervalSinceDate:startTime]; + XCTAssertGreaterThanOrEqual(redirectResponseTime, (requestTime + responseTime), @"Redirect did not honor request/response time"); + capturedRedirectHTTPResponse = HTTPResponse; + } else { + // Response for the final request + if (!error) { + NSTimeInterval totalResponseTime = [[NSDate date] timeIntervalSinceDate:startTime]; + XCTAssertGreaterThanOrEqual(totalResponseTime, ((2 * requestTime) + responseTime), @"Redirect or final request did not honor request/response time"); + } + + NSDictionary *jsonObject = data ? [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL] : nil; + capturedResponseJSONBody = jsonObject; + } + [expectation fulfill]; + }]; + [task resume]; + + [self waitForExpectationsWithTimeout:(requestTime+responseTime)*2+0.1 handler:nil]; + completion(capturedRedirectedRequestMethod, capturedRedirectedRequestHeaders, capturedRedirectedRequestJSONBody, + capturedRedirectHTTPResponse, + capturedResponseJSONBody, capturedResponseError); + } +} + +// The shared session use the same mechanism as NSURLConnection +// (based on protocols registered via +[NSURLProtocol registerClass:] and all) +// and no NSURLSessionConfiguration +- (void)test_SharedNSURLSession +{ + if ([NSURLSession class]) + { + NSURLSession *session = [NSURLSession sharedSession]; + + NSDictionary* json = @{@"Success": @"Yes"}; + [self _test_NSURLSession:session jsonForStub:json completion:^(NSError *errorResponse, id jsonResponse) { + XCTAssertNil(errorResponse, @"Unexpected error"); + XCTAssertEqualObjects(jsonResponse, json, @"Unexpected data received"); + }]; + + [self _test_redirect_NSURLSession:session httpMethod:@"GET" headers:nil jsonBody:nil delays:0.1 redirectStatusCode:301 + completion:^(NSString *redirectedRequestMethod, NSDictionary *redirectedRequestHeaders, id redirectedRequestJSONBody, NSHTTPURLResponse *redirectHTTPResponse, id finalJSONResponse, NSError *errorResponse) + { + XCTAssertEqualObjects(redirectedRequestMethod, @"GET", @"Expected redirected request to use GET method"); + XCTAssertNil(redirectedRequestJSONBody, @"Expected redirected request to have empty body"); + XCTAssertNil(redirectHTTPResponse, @"Redirect response should not have been captured by the task completion block"); + XCTAssertEqualObjects(finalJSONResponse, @{ @"RequestBody": [NSNull null] }, @"Unexpected data received"); + XCTAssertNil(errorResponse, @"Unexpected error"); + }]; + + [session finishTasksAndInvalidate]; + } + else + { + NSLog(@"/!\\ Test skipped because the NSURLSession class is not available on this OS version. Run the tests a target with a more recent OS.\n"); + } +} + +- (void)test_NSURLSessionDefaultConfig +{ + if ([NSURLSessionConfiguration class] && [NSURLSession class]) + { + NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration]; + NSURLSession *session = [NSURLSession sessionWithConfiguration:config]; + + NSDictionary* json = @{@"Success": @"Yes"}; + [self _test_NSURLSession:session jsonForStub:json completion:^(NSError *errorResponse, id jsonResponse) { + XCTAssertNil(errorResponse, @"Unexpected error"); + XCTAssertEqualObjects(jsonResponse, json, @"Unexpected data received"); + }]; + + [self _test_redirect_NSURLSession:session httpMethod:@"GET" headers:nil jsonBody:nil delays:0.1 redirectStatusCode:301 + completion:^(NSString *redirectedRequestMethod, NSDictionary *redirectedRequestHeaders, id redirectedRequestJSONBody, NSHTTPURLResponse *redirectHTTPResponse, id finalJSONResponse, NSError *errorResponse) + { + XCTAssertEqualObjects(redirectedRequestMethod, @"GET", @"Expected redirected request to use GET method"); + XCTAssertNil(redirectedRequestJSONBody, @"Expected redirected request to have empty body"); + XCTAssertNil(redirectHTTPResponse, @"Redirect response should not have been captured by the task completion block"); + XCTAssertEqualObjects(finalJSONResponse, @{ @"RequestBody": [NSNull null] }, @"Unexpected data received"); + XCTAssertNil(errorResponse, @"Unexpected error"); + }]; + + [session finishTasksAndInvalidate]; + } + else + { + NSLog(@"/!\\ Test skipped because the NSURLSession class is not available on this OS version. Run the tests a target with a more recent OS.\n"); + } +} + +- (void)test_NSURLSessionDefaultConfig_notFollowingRedirects +{ + NSURLSessionTestDelegate* delegate = [NSURLSessionTestDelegate delegateFollowingRedirects:NO fulfillOnCompletion:nil]; + + if ([NSURLSessionConfiguration class] && [NSURLSession class]) + { + NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration]; + NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:delegate delegateQueue:nil]; + + [self _test_redirect_NSURLSession:session httpMethod:@"GET" headers:nil jsonBody:nil delays:0.1 redirectStatusCode:301 + completion:^(NSString *redirectedRequestMethod, NSDictionary *redirectedRequestHeaders, id redirectedRequestJSONBody, NSHTTPURLResponse *redirectHTTPResponse, id finalJSONResponse, NSError *errorResponse) + { + XCTAssertNil(redirectedRequestMethod, @"Expected no redirected request to fire"); + XCTAssertNil(redirectedRequestJSONBody, @"Expected no redirected request to fire"); + XCTAssertNotNil(redirectHTTPResponse, @"Redirect response should have been received"); + XCTAssertNil(finalJSONResponse, @"Unexpected data received when no redirect"); + XCTAssertNil(errorResponse, @"Unexpected error"); + }]; + + [session finishTasksAndInvalidate]; + } + else + { + NSLog(@"/!\\ Test skipped because the NSURLSession class is not available on this OS version. Run the tests a target with a more recent OS.\n"); + } +} + +/** https://github.com/AliSoftware/OHHTTPStubs/issues/230 + Verify that redirects of different methods and status codes are handled properly and + that we retain the HTTP Method for specific HTTP status codes as well as the data payload. + **/ +#if OHHTTPSTUBS_SKIP_REDIRECT_TESTS +#warning Redirect Tests will be skipped for this run. +#else +- (void)test_NSURLSessionDefaultConfig_MethodAndDataRetentionOnRedirect +{ + if ([NSURLSessionConfiguration class] && [NSURLSession class]) + { + NSDictionary* json = @{ @"query": @"Hello World" }; + NSArray* allMethods = @[@"GET", @"HEAD", @"POST", @"PATCH", @"PUT"]; + + /** 301, 302, 307, 308: GET, HEAD, POST, PATCH, PUT should all maintain HTTP method and body unchanged **/ + for (NSNumber* redirectStatusCode in @[@301, @302, @307, @308]) { + int statusCode = redirectStatusCode.intValue; + for (NSString* method in allMethods) { + + NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration]; + NSURLSessionTestDelegate* delegate = [NSURLSessionTestDelegate delegateFollowingRedirects:YES fulfillOnCompletion:nil]; + NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:delegate delegateQueue:nil]; + + [self _test_redirect_NSURLSession:session httpMethod:method headers:nil jsonBody:json delays:0.0 redirectStatusCode:statusCode + completion:^(NSString *redirectedRequestMethod, NSDictionary *redirectedRequestHeaders, id redirectedRequestJSONBody, NSHTTPURLResponse *redirectHTTPResponse, id finalJSONResponse, NSError *errorResponse) + { + XCTAssertEqualObjects(redirectedRequestMethod, method, + @"Expected the HTTP method to be unchanged after %d redirect", statusCode); + XCTAssertEqualObjects(redirectedRequestJSONBody, json, + @"Expected %d-redirected request to have the same body as the original request", statusCode); + XCTAssertNil(redirectHTTPResponse, + @"%d Redirect response should not have been captured by the task completion block", statusCode); + XCTAssertEqualObjects(finalJSONResponse, @{ @"RequestBody": json }, + @"Unexpected JSON response received after %d redirect", statusCode); + XCTAssertNil(errorResponse, @"Unexpected error during %d redirect", statusCode); + }]; + + [session finishTasksAndInvalidate]; + } + } + + /** 303: GET, HEAD, POST, PATCH, PUT should use a GET HTTP method after redirection and not forward the body **/ + for (NSString* method in allMethods) { + + NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration]; + NSURLSessionTestDelegate* delegate = [NSURLSessionTestDelegate delegateFollowingRedirects:YES fulfillOnCompletion:nil]; + NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:delegate delegateQueue:nil]; + + [self _test_redirect_NSURLSession:session httpMethod:method headers:nil jsonBody:json delays:0.0 redirectStatusCode:303 + completion:^(NSString *redirectedRequestMethod, NSDictionary *redirectedRequestHeaders, id redirectedRequestJSONBody, NSHTTPURLResponse *redirectHTTPResponse, id finalJSONResponse, NSError *errorResponse) + { + XCTAssertEqualObjects(redirectedRequestMethod, @"GET", @"Expected 303 redirected request HTTP method to be reset to GET"); + XCTAssertNil(redirectedRequestJSONBody, @"Expected 303-redirected request to have empty body"); + XCTAssertNil(redirectHTTPResponse, @"303 Redirect response should not have been captured by the task completion block"); + XCTAssertEqualObjects(finalJSONResponse, @{ @"RequestBody": json }, @"Unexpected JSON response received after 303 redirect"); + XCTAssertNil(errorResponse, @"Unexpected error during 303 redirect"); + }]; + + [session finishTasksAndInvalidate]; + } + } + else + { + NSLog(@"/!\\ Test skipped because the NSURLSession class is not available on this OS version. Run the tests a target with a more recent OS.\n"); + } +} + +- (void)test_NSURLSessionDefaultConfig_HeaderRetentionPolicyOnRedirect { + if ([NSURLSessionConfiguration class] && [NSURLSession class]) + { + NSArray* allMethods = @[@"GET", @"HEAD", @"POST", @"PATCH", @"PUT"]; + + /** 301, 302, 307, 308: GET, HEAD, POST, PATCH, PUT should all maintain most HTTP headers unchanged **/ + for (NSNumber* redirectStatusCode in @[@301, @302, @307, @308]) { + int statusCode = redirectStatusCode.intValue; + for (NSString* method in allMethods) { + + NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration]; + NSURLSessionTestDelegate* delegate = [NSURLSessionTestDelegate delegateFollowingRedirects:YES fulfillOnCompletion:nil]; + NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:delegate delegateQueue:nil]; + + NSDictionary *headers = @{ + @"Authorization": @"authorization", + @"Connection": @"connection", + @"Preserved1": @"preserved", + @"Host": @"host", + @"Proxy-Authenticate": @"proxy-authenticate", + @"Proxy-Authorization": @"proxy-authorization", + @"Preserved2": @"preserved", + @"WWW-Authenticate": @"www-authenticate", + }; + [self _test_redirect_NSURLSession:session httpMethod:method headers:headers jsonBody:nil delays:0.0 redirectStatusCode:statusCode + completion:^(NSString *redirectedRequestMethod, NSDictionary *redirectedRequestHeaders, id redirectedRequestJSONBody, NSHTTPURLResponse *redirectHTTPResponse, id finalJSONResponse, NSError *errorResponse) + { + XCTAssertNil(redirectedRequestHeaders[@"Authorization"], @"Authorization header is preserved when following redirects"); + XCTAssertNil(redirectedRequestHeaders[@"Connection"], @"Connection header is preserved when following redirects"); + XCTAssertNil(redirectedRequestHeaders[@"Host"], @"Host header is preserved when following redirects"); + XCTAssertNil(redirectedRequestHeaders[@"Proxy-Authenticate"], @"Proxy-Authenticate header is preserved when following redirects"); + XCTAssertNil(redirectedRequestHeaders[@"Proxy-Authorization"], @"Proxy-Authorization header is preserved when following redirects"); + XCTAssertNil(redirectedRequestHeaders[@"WWW-Authenticate"], @"WWW-Authenticate header is preserved when following redirects"); + XCTAssertEqual(redirectedRequestHeaders[@"Preserved1"], @"preserved", @"Regular header is not preserved when following redirects"); + XCTAssertEqual(redirectedRequestHeaders[@"Preserved2"], @"preserved", @"Regular header is not preserved when following redirects"); + }]; + + [session finishTasksAndInvalidate]; + } + } + } + else + { + NSLog(@"/!\\ Test skipped because the NSURLSession class is not available on this OS version. Run the tests a target with a more recent OS.\n"); + } +} +#endif + +- (void)test_NSURLSessionEphemeralConfig +{ + if ([NSURLSessionConfiguration class] && [NSURLSession class]) + { + NSURLSessionConfiguration* config = [NSURLSessionConfiguration ephemeralSessionConfiguration]; + NSURLSession *session = [NSURLSession sessionWithConfiguration:config]; + + NSDictionary* json = @{@"Success": @"Yes"}; + [self _test_NSURLSession:session jsonForStub:json completion:^(NSError *errorResponse, id jsonResponse) { + XCTAssertNil(errorResponse, @"Unexpected error"); + XCTAssertEqualObjects(jsonResponse, json, @"Unexpected data received"); + }]; + + [self _test_redirect_NSURLSession:session httpMethod:@"GET" headers:nil jsonBody:json delays:0.1 redirectStatusCode:301 + completion:^(NSString *redirectedRequestMethod, NSDictionary *redirectedRequestHeaders, id redirectedRequestJSONBody, NSHTTPURLResponse *redirectHTTPResponse, id finalJSONResponse, NSError *errorResponse) + { + XCTAssertEqualObjects(redirectedRequestMethod, @"GET", @"Expected the HTTP method of redirected request to be GET"); + XCTAssertEqualObjects(redirectedRequestJSONBody, json, @"Expected redirected request to have the same body as the original request"); + XCTAssertNil(redirectHTTPResponse, @"Redirect response should not have been captured by the task completion block"); + XCTAssertEqualObjects(finalJSONResponse, @{ @"RequestBody": json }, @"Unexpected JSON response received"); + XCTAssertNil(errorResponse, @"Unexpected error"); + }]; + + [session finishTasksAndInvalidate]; + } + else + { + NSLog(@"/!\\ Test skipped because the NSURLSession class is not available on this OS version. Run the tests a target with a more recent OS.\n"); + } +} + +- (void)test_NSURLSessionDefaultConfig_Disabled +{ + if ([NSURLSessionConfiguration class] && [NSURLSession class]) + { + NSURLSessionConfiguration* config = [NSURLSessionConfiguration ephemeralSessionConfiguration]; + [HTTPStubs setEnabled:NO forSessionConfiguration:config]; // Disable stubs for this session + NSURLSession *session = [NSURLSession sessionWithConfiguration:config]; + + NSDictionary* json = @{@"Success": @"Yes"}; + [self _test_NSURLSession:session jsonForStub:json completion:^(NSError *errorResponse, id jsonResponse) { + // Stubs were disable for this session, so we should get an error instead of the stubs data + XCTAssertNotNil(errorResponse, @"Expected error but none found"); + XCTAssertNil(jsonResponse, @"Data should not have been received as stubs should be disabled"); + }]; + + [self _test_redirect_NSURLSession:session httpMethod:@"GET" headers:nil jsonBody:json delays:0.1 redirectStatusCode:301 + completion:^(NSString *redirectedRequestMethod, NSDictionary *redirectedRequestHeaders, id redirectedRequestJSONBody, NSHTTPURLResponse *finalHTTPResponse, id finalJSONResponse, NSError *errorResponse) + { + // Stubs were disabled for this session, so we should get an error instead of the stubs data + XCTAssertNotNil(errorResponse, @"Expected error but none found"); + XCTAssertNil(finalHTTPResponse, @"Redirect response should not have been received as stubs should be disabled"); + XCTAssertNil(finalJSONResponse, @"Data should not have been received as stubs should be disabled"); + }]; + + [session finishTasksAndInvalidate]; + } + else + { + NSLog(@"/!\\ Test skipped because the NSURLSession class is not available on this OS version. Run the tests a target with a more recent OS.\n"); + } +} + +- (void)test_NSURLSession_DataTask_DelegateMethods +{ + if ([NSURLSessionConfiguration class] && [NSURLSession class]) + { + NSData* expectedResponse = [NSStringFromSelector(_cmd) dataUsingEncoding:NSUTF8StringEncoding]; + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request.URL.scheme isEqualToString:@"stub"]; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + return [[HTTPStubsResponse responseWithData:expectedResponse statusCode:200 headers:nil] + responseTime:0.5]; + }]; + + XCTestExpectation* expectation = [self expectationWithDescription:@"NSURLSessionDataTask completion delegate method called"]; + NSURLSessionTestDelegate* delegate = [NSURLSessionTestDelegate delegateFollowingRedirects:YES fulfillOnCompletion:expectation]; + + NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration]; + NSURLSession* session = [NSURLSession sessionWithConfiguration:config delegate:delegate delegateQueue:nil]; + + [[session dataTaskWithURL:[NSURL URLWithString:@"stub://foo"]] resume]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; + + XCTAssertEqualObjects(delegate.receivedData, expectedResponse, @"Unexpected response"); + XCTAssertNil(delegate.receivedError, @"Unexpected error happened"); + + [session finishTasksAndInvalidate]; + } + else + { + NSLog(@"/!\\ Test skipped because the NSURLSession class is not available on this OS version. Run the tests a target with a more recent OS.\n"); + } +} + +- (void)test_NSURLSessionCustomHTTPBody +{ + if ([NSURLSessionConfiguration class] && [NSURLSession class]) + { + NSData* expectedResponse = [NSStringFromSelector(_cmd) dataUsingEncoding:NSUTF8StringEncoding]; + NSString* expectedBodyString = @"body"; + + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + NSData* body = [request OHHTTPStubs_HTTPBody]; + return [[[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding] isEqualToString:expectedBodyString]; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + return [[HTTPStubsResponse responseWithData:expectedResponse statusCode:200 headers:nil] + responseTime:0.2]; + }]; + + + XCTestExpectation* successExpectation = [self expectationWithDescription:@"Complete successful body test"]; + NSURLSessionTestDelegate* delegate = [NSURLSessionTestDelegate delegateFollowingRedirects:YES fulfillOnCompletion:successExpectation]; + NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration]; + NSURLSession* session = [NSURLSession sessionWithConfiguration:config delegate:delegate delegateQueue:nil]; + + // setup for positive check + NSMutableURLRequest* requestWithBody = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"stub://foo"]]; + requestWithBody.HTTPBody = [expectedBodyString dataUsingEncoding:NSUTF8StringEncoding]; + [[session dataTaskWithRequest:requestWithBody] resume]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; + XCTAssertEqualObjects(delegate.receivedData, expectedResponse, @"Unexpected response: HTTP body check should be successful"); + + // reset for negative check + XCTestExpectation* failureExpectation = [self expectationWithDescription:@"Complete unsuccessful body test"]; + [delegate resetWithNewExpectation:failureExpectation]; + + requestWithBody.HTTPBody = [@"somethingElse" dataUsingEncoding:NSUTF8StringEncoding]; + [[session dataTaskWithRequest:requestWithBody] resume]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; + + XCTAssertNil(delegate.receivedData, @"Unexpected response: HTTP body check should not be successful"); + + [session finishTasksAndInvalidate]; + } + else + { + NSLog(@"/!\\ Test skipped because the NSURLSession class is not available on this OS version. Run the tests a target with a more recent OS.\n"); + } +} + +- (void)test_NSURLSessionNativeHTTPBody +{ + if ([NSURLSessionConfiguration class] && [NSURLSession class]) + { + NSData* expectedResponse = [NSStringFromSelector(_cmd) dataUsingEncoding:NSUTF8StringEncoding]; + NSString* expectedBodyString = @"body"; + + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + NSData* body = [request HTTPBody]; // this is not expected to work correctly + return [[[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding] isEqualToString:expectedBodyString]; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + return [[HTTPStubsResponse responseWithData:expectedResponse statusCode:200 headers:nil] + responseTime:0.2]; + }]; + + XCTestExpectation* expectation = [self expectationWithDescription:@"Complete body test"]; + NSURLSessionTestDelegate* delegate = [NSURLSessionTestDelegate delegateFollowingRedirects:YES fulfillOnCompletion:expectation]; + NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration]; + NSURLSession* session = [NSURLSession sessionWithConfiguration:config delegate:delegate delegateQueue:nil]; + + NSMutableURLRequest* requestWithBody = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"stub://foo"]]; + requestWithBody.HTTPBody = [expectedBodyString dataUsingEncoding:NSUTF8StringEncoding]; + [[session dataTaskWithRequest:requestWithBody] resume]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; + XCTAssertNil(delegate.receivedData, @"[request HTTPBody] is not expected to work. If this has been fixed, the OHHTTPStubs_HTTPBody can be removed."); + + [session finishTasksAndInvalidate]; + } + else + { + NSLog(@"/!\\ Test skipped because the NSURLSession class is not available on this OS version. Run the tests a target with a more recent OS.\n"); + } +} + +@end + + +//--------------------------------------------------------------- +#pragma mark - Delegate + +@implementation NSURLSessionTestDelegate +{ + NSMutableData* _receivedData; + XCTestExpectation* _taskDidCompleteExpectation; + BOOL _shouldFollowRedirects; +} + +- (instancetype)initFollowingRedirects:(BOOL)shouldFollowRedirects fulfillOnCompletion:(XCTestExpectation*)expectationToFulfill +{ + self = [super init]; + if (self) + { + _shouldFollowRedirects = shouldFollowRedirects; + [self resetWithNewExpectation:expectationToFulfill]; + } + return self; +} + ++ (instancetype)delegateFollowingRedirects:(BOOL)shouldFollowRedirects fulfillOnCompletion:(XCTestExpectation*)expectationToFulfill +{ + return [[NSURLSessionTestDelegate alloc] initFollowingRedirects:shouldFollowRedirects fulfillOnCompletion:expectationToFulfill]; +} + +-(void)resetWithNewExpectation:(XCTestExpectation*)expectationToFulfill +{ + _receivedData = nil; + _receivedError = nil; + _taskDidCompleteExpectation = expectationToFulfill; +} + +- (NSData*)receivedData { + return [_receivedData copy]; +} + +- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler +{ + _receivedData = [NSMutableData new]; + completionHandler(NSURLSessionResponseAllow); +} +- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data +{ + [_receivedData appendData:data]; +} +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error +{ + _receivedError = error; + [_taskDidCompleteExpectation fulfill]; +} + +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler +{ + completionHandler(_shouldFollowRedirects ? request : nil); +} + +@end + +#else +#warning Unit Tests using NSURLSession were not compiled nor executed, because NSURLSession is only available since iOS7/OSX10.9 SDK. \ +-------- Compile using iOS7 or OSX10.9 SDK then launch the tests on the iOS7 simulator or an OSX10.9 target for them to be executed. +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/OHHTTPStubsTests/NilValuesTests.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/OHHTTPStubsTests/NilValuesTests.m new file mode 100644 index 0000000000..c4d8ba93a2 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/OHHTTPStubsTests/NilValuesTests.m @@ -0,0 +1,280 @@ +/*********************************************************************************** + * + * Copyright (c) 2012 Olivier Halligon + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + ***********************************************************************************/ + +#import +// tvOS & watchOS deprecate use of NSURLConnection but these tests are based on it +#if (!defined(__TV_OS_VERSION_MIN_REQUIRED) && !defined(__WATCH_OS_VERSION_MIN_REQUIRED)) + +#import + +#if OHHTTPSTUBS_USE_STATIC_LIBRARY || SWIFT_PACKAGE +#import "HTTPStubs.h" +#import "HTTPStubsPathHelpers.h" +#else +@import OHHTTPStubs; +#endif + +static const NSTimeInterval kResponseTimeMaxDelay = 2.5; + +@interface NilValuesTests : XCTestCase @end + +@implementation NilValuesTests + +-(void)setUp +{ + [super setUp]; + [HTTPStubs removeAllStubs]; +} + +- (void)test_NilData +{ + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return YES; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + return [HTTPStubsResponse responseWithData:nil statusCode:400 headers:nil]; +#pragma clang diagnostic pop + }]; + + XCTestExpectation* expectation = [self expectationWithDescription:@"Network request's completionHandler called"]; + + NSURLRequest* req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.iana.org/domains/example/"]]; + + [NSURLConnection sendAsynchronousRequest:req + queue:[NSOperationQueue mainQueue] + completionHandler:^(NSURLResponse* resp, NSData* data, NSError* error) + { + XCTAssertEqual(data.length, (NSUInteger)0, @"Data should be empty"); + + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kResponseTimeMaxDelay handler:nil]; +} + +- (void)test_EmptyData +{ + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return YES; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + return [[HTTPStubsResponse responseWithData:[NSData data] statusCode:400 headers:nil] + requestTime:0.01 responseTime:0.01]; + }]; + + XCTestExpectation* expectation = [self expectationWithDescription:@"Network request's completionHandler called"]; + + NSURLRequest* req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.iana.org/domains/example/"]]; + + [NSURLConnection sendAsynchronousRequest:req + queue:[NSOperationQueue mainQueue] + completionHandler:^(NSURLResponse* resp, NSData* data, NSError* error) + { + XCTAssertEqual(data.length, (NSUInteger)0, @"Data should be empty"); + + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kResponseTimeMaxDelay handler:nil]; +} + +- (void)test_NilPath +{ + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return YES; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + return [[HTTPStubsResponse responseWithFileAtPath:nil statusCode:501 headers:nil] + requestTime:0.01 responseTime:0.01]; +#pragma clang diagnostic pop + }]; + + XCTestExpectation* expectation = [self expectationWithDescription:@"Network request's completionHandler called"]; + + NSURLRequest* req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.iana.org/domains/example/"]]; + + [NSURLConnection sendAsynchronousRequest:req + queue:[NSOperationQueue mainQueue] + completionHandler:^(NSURLResponse* resp, NSData* data, NSError* error) + { + XCTAssertEqual(data.length, (NSUInteger)0, @"Data should be empty"); + + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kResponseTimeMaxDelay handler:nil]; +} + +- (void)test_NilPathWithURL +{ + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return YES; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + return [[HTTPStubsResponse responseWithFileURL:nil statusCode:501 headers:nil] + requestTime:0.01 responseTime:0.01]; +#pragma clang diagnostic pop + }]; + + XCTestExpectation* expectation = [self expectationWithDescription:@"Network request's completionHandler called"]; + + NSURLRequest* req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.iana.org/domains/example/"]]; + + [NSURLConnection sendAsynchronousRequest:req + queue:[NSOperationQueue mainQueue] + completionHandler:^(NSURLResponse* resp, NSData* data, NSError* error) + { + XCTAssertEqual(data.length, (NSUInteger)0, @"Data should be empty"); + + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kResponseTimeMaxDelay handler:nil]; +} + +- (void)test_InvalidPath +{ + XCTAssertThrowsSpecificNamed([HTTPStubsResponse responseWithFileAtPath:@"foo/bar" statusCode:501 headers:nil] + , NSException, NSInternalInconsistencyException, @"An exception should be thrown if a non-file URL is given"); +} + +- (void)test_InvalidPathWithURL +{ + NSURL *httpURL = [NSURL fileURLWithPath:@"foo/bar"]; + NSAssert(httpURL, @"If the URL is nil an empty response is sent instead of an exception being thrown"); + XCTAssertThrowsSpecificNamed([HTTPStubsResponse responseWithFileURL:httpURL statusCode:501 headers:nil] + , NSException, NSInternalInconsistencyException, @"An exception should be thrown if a non-file URL is given"); +} + +- (void)test_NonFileURL +{ + NSURL *httpURL = [NSURL URLWithString:@"http://www.iana.org/domains/example/"]; + NSAssert(httpURL, @"If the URL is nil an empty response is sent instead of an exception being thrown"); + XCTAssertThrowsSpecificNamed([HTTPStubsResponse responseWithFileURL:httpURL statusCode:501 headers:nil] + , NSException, NSInternalInconsistencyException, @"An exception should be thrown if a non-file URL is given"); +} + + +- (void)test_EmptyFile +{ + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return YES; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + NSString* emptyFile = OHPathForFile(@"emptyfile.json", self.class); + return [[HTTPStubsResponse responseWithFileAtPath:emptyFile statusCode:500 headers:nil] + requestTime:0.01 responseTime:0.01]; + }]; + + XCTestExpectation* expectation = [self expectationWithDescription:@"Network request's completionHandler called"]; + + NSURLRequest* req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.iana.org/domains/example/"]]; + + [NSURLConnection sendAsynchronousRequest:req + queue:[NSOperationQueue mainQueue] + completionHandler:^(NSURLResponse* resp, NSData* data, NSError* error) + { + XCTAssertEqual(data.length, (NSUInteger)0, @"Data should be empty"); + + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kResponseTimeMaxDelay handler:nil]; +} + +- (void)test_EmptyFileWithURL +{ + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return YES; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + NSURL *fileURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"emptyfile" withExtension:@"json"]; + return [[HTTPStubsResponse responseWithFileURL:fileURL statusCode:500 headers:nil] + requestTime:0.01 responseTime:0.01]; + }]; + + XCTestExpectation* expectation = [self expectationWithDescription:@"Network request's completionHandler called"]; + + NSURLRequest* req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.iana.org/domains/example/"]]; + + [NSURLConnection sendAsynchronousRequest:req + queue:[NSOperationQueue mainQueue] + completionHandler:^(NSURLResponse* resp, NSData* data, NSError* error) + { + XCTAssertEqual(data.length, (NSUInteger)0, @"Data should be empty"); + + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kResponseTimeMaxDelay handler:nil]; +} + +- (void)_test_NilURLAndCookieHandlingEnabled:(BOOL)handleCookiesEnabled +{ + NSData* expectedResponse = [NSStringFromSelector(_cmd) dataUsingEncoding:NSUTF8StringEncoding]; + + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return YES; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + return [HTTPStubsResponse responseWithData:expectedResponse + statusCode:200 + headers:nil]; + }]; + + XCTestExpectation* expectation = [self expectationWithDescription:@"Network request's completionHandler called"]; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + NSMutableURLRequest* req = [NSMutableURLRequest requestWithURL:nil]; +#pragma clang diagnostic pop + req.HTTPShouldHandleCookies = handleCookiesEnabled; + + __block NSData* response = nil; + + [NSURLConnection sendAsynchronousRequest:req + queue:[NSOperationQueue mainQueue] + completionHandler:^(NSURLResponse* resp, NSData* data, NSError* error) + { + response = data; + [expectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:kResponseTimeMaxDelay handler:nil]; + + XCTAssertEqualObjects(response, expectedResponse, @"Unexpected data received"); +} + +- (void)test_NilURLAndCookieHandlingEnabled +{ + [self _test_NilURLAndCookieHandlingEnabled:YES]; +} + +- (void)test_NilURLAndCookieHandlingDisabled +{ + [self _test_NilURLAndCookieHandlingEnabled:NO]; +} + +@end + +#endif diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/OHHTTPStubsTests/OHPathHelpersTests.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/OHHTTPStubsTests/OHPathHelpersTests.m new file mode 100644 index 0000000000..2db6a94119 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/OHHTTPStubsTests/OHPathHelpersTests.m @@ -0,0 +1,23 @@ +#import + +#if OHHTTPSTUBS_USE_STATIC_LIBRARY || SWIFT_PACKAGE +#import "HTTPStubs.h" +#import "HTTPStubsPathHelpers.h" +#else +@import OHHTTPStubs; +#endif + +@interface HTTPStubsPathHelpersTests : XCTestCase +@end + +@implementation HTTPStubsPathHelpersTests + +- (void)testOHResourceBundle { + NSBundle *classBundle = [NSBundle bundleForClass:self.class]; + NSBundle *expectedBundle = [NSBundle bundleWithPath:[classBundle pathForResource:@"empty" + ofType:@"bundle"]]; + + XCTAssertEqual(OHResourceBundle(@"empty", self.class), expectedBundle); +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/OHHTTPStubsTests/TimingTests.m b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/OHHTTPStubsTests/TimingTests.m new file mode 100644 index 0000000000..eb5dffc252 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/OHHTTPStubsTests/TimingTests.m @@ -0,0 +1,175 @@ +/*********************************************************************************** + * + * Copyright (c) 2012 Olivier Halligon + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + ***********************************************************************************/ + +#if OHHTTPSTUBS_SKIP_TIMING_TESTS +#warning Timing Tests will be skipped for this run. +#else + +#import +// tvOS & watchOS deprecate use of NSURLConnection but these tests are based on it +#if (!defined(__TV_OS_VERSION_MIN_REQUIRED) && !defined(__WATCH_OS_VERSION_MIN_REQUIRED)) + +#import + +#if OHHTTPSTUBS_USE_STATIC_LIBRARY || SWIFT_PACKAGE +#import "HTTPStubs.h" +#else +@import OHHTTPStubs; +#endif + +@interface TimingTests : XCTestCase +{ + NSMutableData* _data; + NSError* _error; + XCTestExpectation* _connectionFinishedExpectation; + +// NSDate* _didReceiveResponseTS; + NSDate* _didFinishLoadingTS; +} +@end + +@implementation TimingTests + +-(void)setUp +{ + [super setUp]; + + _data = [NSMutableData new]; + _error = nil; +// _didReceiveResponseTS = nil; + _didFinishLoadingTS = nil; + [HTTPStubs removeAllStubs]; +} +-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response +{ + _data.length = 0U; + // NOTE: This timing info is not reliable as Cocoa always calls the connection:didReceiveResponse: delegate method just before + // calling the first "connection:didReceiveData:", even if the [id URLProtocol:didReceiveResponse:…] method was called way before. So we are not testing this +// _didReceiveResponseTS = [NSDate date]; +} + +-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data +{ + [_data appendData:data]; +} + +-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error +{ + _error = error; // keep strong reference + _didFinishLoadingTS = [NSDate date]; + [_connectionFinishedExpectation fulfill]; +} + +-(void)connectionDidFinishLoading:(NSURLConnection *)connection +{ + _didFinishLoadingTS = [NSDate date]; + [_connectionFinishedExpectation fulfill]; +} + + +/////////////////////////////////////////////////////////////////////////////////////// + +static NSTimeInterval const kResponseTimeMaxDelay = 2.5; +static NSTimeInterval const kSecurityTimeout = 5.0; + +-(void)_testWithData:(NSData*)stubData requestTime:(NSTimeInterval)requestTime responseTime:(NSTimeInterval)responseTime +{ + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return YES; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + return [[HTTPStubsResponse responseWithData:stubData + statusCode:200 + headers:nil] + requestTime:requestTime responseTime:responseTime]; + }]; + + _connectionFinishedExpectation = [self expectationWithDescription:@"NSURLConnection did finish (with error or success)"]; + + NSURLRequest* req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.iana.org/domains/example/"]]; + NSDate* startTS = [NSDate date]; + + [NSURLConnection connectionWithRequest:req delegate:self]; + + [self waitForExpectationsWithTimeout:requestTime+responseTime+kResponseTimeMaxDelay+kSecurityTimeout handler:nil]; + + XCTAssertEqualObjects(_data, stubData, @"Invalid data response"); + + XCTAssertGreaterThan([_didFinishLoadingTS timeIntervalSinceDate:startTS], requestTime + responseTime, @"Invalid response time"); + + [NSThread sleepForTimeInterval:0.01]; // Time for the test to wrap it all (otherwise we may have "Test did not finish" warning) +} + + + + + + +-(void)test_RequestTime0_ResponseTime0 +{ + NSData* testData = [NSStringFromSelector(_cmd) dataUsingEncoding:NSUTF8StringEncoding]; + [self _testWithData:testData requestTime:0 responseTime:0]; +} + +-(void)test_SmallDataLargeTime_CumulativeAlgorithm +{ + NSData* testData = [NSStringFromSelector(_cmd) dataUsingEncoding:NSUTF8StringEncoding]; + // 21 bytes in 23/4 of a second = 0.913042 byte per slot: we need to check that the cumulative algorithm works + [self _testWithData:testData requestTime:0 responseTime:5.75]; +} + +-(void)test_RequestTime1_ResponseTime0 +{ + NSData* testData = [NSStringFromSelector(_cmd) dataUsingEncoding:NSUTF8StringEncoding]; + [self _testWithData:testData requestTime:1 responseTime:0]; +} + +-(void)test_LongData_RequestTime1_ResponseTime5 +{ + static NSUInteger const kDataLength = 1024; + NSMutableData* testData = [NSMutableData dataWithCapacity:kDataLength]; + NSData* chunk = [[NSProcessInfo.processInfo globallyUniqueString] dataUsingEncoding:NSUTF8StringEncoding]; + while(testData.length + +#if OHHTTPSTUBS_USE_STATIC_LIBRARY || SWIFT_PACKAGE +#import "HTTPStubs.h" +#else +@import OHHTTPStubs; +#endif + +@interface WithContentsOfURLTests : XCTestCase @end + +static const NSTimeInterval kResponseTimeMaxDelay = 2.5; + +@implementation WithContentsOfURLTests + +-(void)setUp +{ + [super setUp]; + [HTTPStubs removeAllStubs]; +} + +static const NSTimeInterval kRequestTime = 0.1; +static const NSTimeInterval kResponseTime = 0.5; + +/////////////////////////////////////////////////////////////////////////////////// +#pragma mark [NSString stringWithContentsOfURL:encoding:error:] +/////////////////////////////////////////////////////////////////////////////////// + +-(void)test_NSString_stringWithContentsOfURL_mainQueue +{ + NSString* testString = NSStringFromSelector(_cmd); + + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return YES; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + return [[HTTPStubsResponse responseWithData:[testString dataUsingEncoding:NSUTF8StringEncoding] + statusCode:200 + headers:nil] + requestTime:kRequestTime responseTime:kResponseTime]; + }]; + + NSDate* startDate = [NSDate date]; + + NSString* string = [NSString stringWithContentsOfURL:[NSURL URLWithString:@"http://www.iana.org/domains/example/"] + encoding:NSUTF8StringEncoding + error:NULL]; + + XCTAssertEqualObjects(string, testString, @"Invalid returned string"); + XCTAssertGreaterThan(-[startDate timeIntervalSinceNow], kResponseTime+kRequestTime, @"Invalid response time"); +} + +-(void)test_NSString_stringWithContentsOfURL_parallelQueue +{ + XCTestExpectation* expectation = [self expectationWithDescription:@"Synchronous download finished"]; + [[NSOperationQueue new] addOperationWithBlock:^{ + [self test_NSString_stringWithContentsOfURL_mainQueue]; + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kRequestTime+kResponseTime+kResponseTimeMaxDelay handler:nil]; +} + +/////////////////////////////////////////////////////////////////////////////////// +#pragma mark [NSData dataWithContentsOfURL:] +/////////////////////////////////////////////////////////////////////////////////// + +-(void)test_NSData_dataWithContentsOfURL_mainQueue +{ + NSData* testData = [NSStringFromSelector(_cmd) dataUsingEncoding:NSUTF8StringEncoding]; + + [HTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return YES; + } withStubResponse:^HTTPStubsResponse *(NSURLRequest *request) { + return [[HTTPStubsResponse responseWithData:testData + statusCode:200 + headers:nil] + requestTime:kRequestTime responseTime:kResponseTime]; + }]; + + NSDate* startDate = [NSDate date]; + + NSData* data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://www.iana.org/domains/example/"]]; + + XCTAssertEqualObjects(data, testData, @"Invalid returned string"); + XCTAssertGreaterThan(-[startDate timeIntervalSinceNow], kRequestTime+kResponseTime, @"Invalid response time"); +} + +-(void)test_NSData_dataWithContentsOfURL_parallelQueue +{ + XCTestExpectation* expectation = [self expectationWithDescription:@"Synchronous download finished"]; + [[NSOperationQueue new] addOperationWithBlock:^{ + [self test_NSData_dataWithContentsOfURL_mainQueue]; + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:kRequestTime+kResponseTime+kResponseTimeMaxDelay handler:nil]; +} + +@end diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Supporting Files/UnitTests-Info.plist b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Supporting Files/UnitTests-Info.plist new file mode 100644 index 0000000000..169b6f710e --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Supporting Files/UnitTests-Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Supporting Files/UnitTests-Prefix.pch b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Supporting Files/UnitTests-Prefix.pch new file mode 100644 index 0000000000..a0a31a66c6 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/OHHTTPStubs/Tests/Supporting Files/UnitTests-Prefix.pch @@ -0,0 +1,7 @@ +// +// Prefix header for all source files of the 'OHHTTPStubs Unit Tests' target in the 'OHHTTPStubs Unit Tests' project +// + +#ifdef __OBJC__ + #import +#endif diff --git a/submodules/AppCenter-sdk/Vendor/PLCrashReporter/.azure-pipelines/analyze-and-test-template.yml b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/.azure-pipelines/analyze-and-test-template.yml new file mode 100644 index 0000000000..d5a7320104 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/.azure-pipelines/analyze-and-test-template.yml @@ -0,0 +1,56 @@ +parameters: +- name: platforms + type: object + +jobs: +- ${{ each platform in parameters.platforms }}: + - job: + displayName: ${{ format('CrashReporter {0} Analyze and Test', platform) }} + pool: + vmImage: internal-macos-10.15 + steps: + - checkout: self + submodules: recursive + + - task: Xcode@5 + displayName: Analyze + inputs: + actions: analyze + configuration: Debug + xcWorkspacePath: CrashReporter.xcodeproj + ${{ if eq(platform, 'MacCatalyst') }}: + scheme: 'CrashReporter iOS' + destinationPlatformOption: macOS + ${{ if ne(platform, 'MacCatalyst') }}: + scheme: 'CrashReporter ${{ platform }}' + destinationPlatformOption: ${{ platform }} + ${{ if eq(platform, 'iOS') }}: + destinationSimulators: 'iPhone 11' + ${{ if eq(platform, 'tvOS') }}: + destinationSimulators: 'Apple TV' + + - task: Xcode@5 + displayName: Test + inputs: + actions: test + configuration: Debug + xcWorkspacePath: CrashReporter.xcodeproj + ${{ if eq(platform, 'MacCatalyst') }}: + scheme: 'CrashReporter iOS' + destinationPlatformOption: macOS + ${{ if ne(platform, 'MacCatalyst') }}: + scheme: 'CrashReporter ${{ platform }}' + destinationPlatformOption: ${{ platform }} + ${{ if eq(platform, 'iOS') }}: + destinationSimulators: 'iPhone 11' + ${{ if eq(platform, 'tvOS') }}: + destinationSimulators: 'Apple TV' + publishJUnitResults: true + timeoutInMinutes: 10 + + - task: PublishBuildArtifacts@1 + displayName: 'Publish Diagnostic Reports' + inputs: + PathtoPublish: /Users/runner/Library/Logs/DiagnosticReports + ArtifactName: 'Test Diagnostic Reports' + condition: failed() diff --git a/submodules/AppCenter-sdk/Vendor/PLCrashReporter/.azure-pipelines/azure-pipelines.yml b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/.azure-pipelines/azure-pipelines.yml new file mode 100644 index 0000000000..24ca285908 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/.azure-pipelines/azure-pipelines.yml @@ -0,0 +1,128 @@ +trigger: +- master + +pr: +- master + +pool: + vmImage: internal-macos-10.15 + +variables: + Configuration: Release + SDK: + + # Xcode 11.3.1 version is the last one that has compatible bitcode with Xcode 11.0 (minimal supported). + XCODE_PATH: '/Applications/Xcode_11.3.1.app/Contents/Developer' + +jobs: +- job: + displayName: Build SDK for All Platforms + steps: + - checkout: self + + - bash: 'brew install doxygen graphviz protobuf-c' + displayName: 'Install dependencies' + + - task: Xcode@5 + displayName: 'Build Crash Reporter' + inputs: + xcWorkspacePath: CrashReporter.xcodeproj + scheme: 'CrashReporter' + xcodeVersion: specifyPath + xcodeDeveloperDir: '$(XCODE_PATH)' + args: 'SYMROOT="$(Build.BinariesDirectory)"' + + - task: Xcode@5 + displayName: 'Build CrashReporter macOS Framework for Apple Silicon' + inputs: + xcWorkspacePath: CrashReporter.xcodeproj + scheme: 'CrashReporter macOS Framework' + xcodeVersion: specifyPath + xcodeDeveloperDir: '/Applications/Xcode_12.2.app/Contents/Developer' + args: 'SYMROOT="$(Build.BinariesDirectory)"' + + - task: Xcode@5 + displayName: 'Build plcrashutil for Apple Silicon' + inputs: + xcWorkspacePath: CrashReporter.xcodeproj + scheme: 'plcrashutil' + xcodeVersion: specifyPath + xcodeDeveloperDir: '/Applications/Xcode_12.2.app/Contents/Developer' + args: 'SYMROOT="$(Build.BinariesDirectory)"' + + - template: build-apple-silicon-template.yml + parameters: + scheme: 'CrashReporter iOS Framework' + destination: + destinationPlatformOption: iOS + destinationTypeOption: simulators + destinationSimulators: 'iPhone 11' + + - template: build-apple-silicon-template.yml + parameters: + scheme: 'CrashReporter iOS Framework' + destination: + destinationPlatformOption: macOS + + - template: build-apple-silicon-template.yml + parameters: + scheme: 'CrashReporter tvOS Framework' + destination: + destinationPlatformOption: tvOS + destinationTypeOption: simulators + destinationSimulators: 'Apple TV' + + - bash: | + cp -f "Release-macosx/libCrashReporter.a" "Release/Static/libCrashReporter-MacOSX-Static.a" + lipo -info "Release/Static/libCrashReporter-MacOSX-Static.a" + rm -rf "Release/Mac OS X Framework/CrashReporter.framework" "Release/Mac OS X Framework/CrashReporter.framework.dSYM" + cp -R "Release-macosx/CrashReporter.framework" "Release-macosx/CrashReporter.framework.dSYM" "Release/Mac OS X Framework" + lipo -info "Release/Mac OS X Framework/CrashReporter.framework/CrashReporter" + cp -f "Release-macosx/plcrashutil" "Release/Tools" + lipo -info "Release/Tools/plcrashutil" + + rm -rf "Release-xcframework/CrashReporter.xcframework/macos-x86_64/CrashReporter.framework" + cp -R "Release-macosx/CrashReporter.framework" "Release-xcframework/CrashReporter.xcframework/macos-x86_64" + lipo "Release-xcframework/CrashReporter.xcframework/ios-i386_x86_64-simulator/CrashReporter.framework/CrashReporter" \ + "Release-iphonesimulator/CrashReporter.framework/CrashReporter" \ + -create -output "Release-xcframework/CrashReporter.xcframework/ios-i386_x86_64-simulator/CrashReporter.framework/CrashReporter" || exit 1 + lipo "Release-xcframework/CrashReporter.xcframework/ios-x86_64-maccatalyst/CrashReporter.framework/Versions/A/CrashReporter" \ + "Release-maccatalyst/CrashReporter.framework/Versions/A/CrashReporter" \ + -create -output "Release-xcframework/CrashReporter.xcframework/ios-x86_64-maccatalyst/CrashReporter.framework/Versions/A/CrashReporter" || exit 1 + lipo "Release-xcframework/CrashReporter.xcframework/tvos-x86_64-simulator/CrashReporter.framework/CrashReporter" \ + "Release-appletvsimulator/CrashReporter.framework/CrashReporter" \ + -create -output "Release-xcframework/CrashReporter.xcframework/tvos-x86_64-simulator/CrashReporter.framework/CrashReporter" || exit 1 + + rm -rf "Release/CrashReporter.xcframework" + for framework in Release-xcframework/CrashReporter.xcframework/*/CrashReporter.framework; do + xcframeworks+=( -framework "$framework") + done + xcodebuild -create-xcframework "${xcframeworks[@]}" -output "Release/CrashReporter.xcframework" + ls "Release/CrashReporter.xcframework" + displayName: 'Combine Binaries' + workingDirectory: '$(Build.BinariesDirectory)' + + - bash: | + VERSION="$(cd $BUILD_SOURCESDIRECTORY && agvtool vers -terse)" + [[ $BUILD_SOURCEBRANCH != 'refs/heads/master' ]] && VERSION="$VERSION+$(cd $BUILD_SOURCESDIRECTORY && git rev-parse --short $BUILD_SOURCEVERSION)" + "$BUILD_SOURCESDIRECTORY/Scripts/create-archive.sh" "PLCrashReporter-$VERSION" "iOS Framework" "tvOS Framework" "Mac OS X Framework" "Tools" + "$BUILD_SOURCESDIRECTORY/Scripts/create-archive.sh" "PLCrashReporter-Static-$VERSION" Static/* + "$BUILD_SOURCESDIRECTORY/Scripts/create-archive.sh" "PLCrashReporter-XCFramework-$VERSION" "CrashReporter.xcframework" + displayName: 'Create Archives' + workingDirectory: '$(Build.BinariesDirectory)/Release' + + - task: CopyFiles@2 + displayName: 'Copy Archives to Staging Directory' + inputs: + SourceFolder: '$(Build.BinariesDirectory)/Release' + Contents: '*.zip' + TargetFolder: '$(Build.ArtifactStagingDirectory)' + + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifacts' + inputs: + ArtifactName: Release + +- template: analyze-and-test-template.yml + parameters: + platforms: [iOS, macOS, tvOS, MacCatalyst ] diff --git a/submodules/AppCenter-sdk/Vendor/PLCrashReporter/.azure-pipelines/build-apple-silicon-template.yml b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/.azure-pipelines/build-apple-silicon-template.yml new file mode 100644 index 0000000000..7efab20b1a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/.azure-pipelines/build-apple-silicon-template.yml @@ -0,0 +1,20 @@ +parameters: +- name: scheme + type: string + +- name: destination + type: object + default: {} + +steps: +- task: Xcode@5 + displayName: ${{ format('Build {0} for Apple Silicon', parameters.scheme) }} + inputs: + xcWorkspacePath: CrashReporter.xcodeproj + scheme: ${{ parameters.scheme }} + configuration: Release + sdk: + xcodeVersion: specifyPath + xcodeDeveloperDir: '/Applications/Xcode_12.2.app/Contents/Developer' + args: 'SYMROOT="$(Build.BinariesDirectory)" ONLY_ACTIVE_ARCH=NO ARCHS=arm64' + ${{ insert }}: ${{ parameters.destination }} diff --git a/submodules/AppCenter-sdk/Vendor/PLCrashReporter/.github/ISSUE_TEMPLATE/feature_request.md b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000..70e8a4bf45 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for the SDK +title: '' +labels: feature request +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context about the feature request here. diff --git a/submodules/AppCenter-sdk/Vendor/PLCrashReporter/.github/ISSUE_TEMPLATE/problem_report.md b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/.github/ISSUE_TEMPLATE/problem_report.md new file mode 100644 index 0000000000..7799c29c4f --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/.github/ISSUE_TEMPLATE/problem_report.md @@ -0,0 +1,30 @@ +--- +name: Problem report +about: Report a problem using the SDK +title: '' +labels: support +assignees: '' + +--- + +### **Description** + +Please describe the issue you are facing using the SDK. + +### **Repro Steps** + +Please list the steps used to reproduce your issue. + +1. +2. + +### **Details** + +1. Which SDK version are you using? +2. Which OS version did you experience the issue on? +3. Which CocoaPods/Carthage/Xcode version are you using? +4. What device version did you see this error on? Were you using an emulator or a physical device? +5. What language are you using? + - [ ] Objective C + - [ ] Swift +6. What third party libraries are you using? diff --git a/submodules/AppCenter-sdk/Vendor/PLCrashReporter/.github/PULL_REQUEST_TEMPLATE.md b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..aab9ff7bce --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,26 @@ + + +Things to consider before you submit the PR: + +* [ ] Has `CHANGELOG.md` been updated? +* [ ] Are tests passing locally? +* [ ] Are the files formatted correctly? +* [ ] Did you add unit tests? +* [ ] Did you test your change with the sample apps? + +## Description + +A few sentences describing the overall goals of the pull request. + +## Related PRs or issues + +List related PRs and other issues. + +## Misc + +Add what's missing, notes on what you tested, additional thoughts or questions. diff --git a/submodules/AppCenter-sdk/Vendor/PLCrashReporter/.gitignore b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/.gitignore new file mode 100644 index 0000000000..7ca66da415 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/.gitignore @@ -0,0 +1,34 @@ +# App Code +**/.idea/ +**/.idea/** + +## Build generated +build/ +DerivedData/ +Documentation/API/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + +## Other +/debug.txt +*.moved-aside +*.xccheckout +*.xcscmblueprint + +## Obj-C/Swift specific +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +# Mac +.DS_Store diff --git a/submodules/AppCenter-sdk/Vendor/PLCrashReporter/CHANGELOG.md b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/CHANGELOG.md new file mode 100644 index 0000000000..37fd738a00 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/CHANGELOG.md @@ -0,0 +1,97 @@ +# PLCrashReporter Change Log + +## Version 1.8.1 + +* Re-build Apple Silicon binaries with [Xcode 12.2 Release Candidate](https://developer.apple.com/news/releases/?id=11052020h) to [be able to](https://developer.apple.com/news/releases/?id=11052020i) submit the applications that use the framework as a binary to the App Store. + +___ + +## Version 1.8.0 + +* Drop support of old versions of iOS and macOS. The minimal version is iOS 9 and macOS 10.9 now. +* Add Apple Silicon support. Note that `arm64` for iOS and tvOS simulators is available only in xcframework or SwiftPM. +* Support saving custom data in crash report, see `PLCrashReporter.customData` property. +* Fix exported symbols list for applying `PLCRASHREPORTER_PREFIX` prefix. +* Fix Xcode 12 compatibility if the framework is used from sources. +* Fix getting the subtype of device architecture on iOS 14. +* Fix crash on collecting register values on `arm64e` devices with iOS 14. + +___ + +## Version 1.7.2 + +* Fix building on Xcode 12 beta. +* Use memory mapping to reduce live reports memory pressure. +* Remove "CrashReporter Key: TODO" from text report output. +* Add `[PLCrashReporter]` prefix for all log messages. + +___ + +## Version 1.7.1 + +* Fix crash on old operating systems: macOS 10.11, iOS 9 and tvOS 9 (and older). +* Fix duplicate symbols in applications with `-all_load` linker flag. +* Fix exporting PLCrashReporter along with an application into `.xcarchive`. +* Fix collecting stacktraces on `arm64e` devices in some cases. + +___ + +## Version 1.7.0 + +* Drop support old versions of Xcode. The minimal version is Xcode 11 now. +* Support [Mac Catalyst](https://developer.apple.com/mac-catalyst/). +* Distribute `.xcframework` archive alongside with the other options. +* Improve reliability of saving crash reports in case of memory corruption. +* Fix symbolication issues with new Objective-C runtime version. +* Add workaround for SwiftPM on Xcode 11.1 bug (`SWIFT_PACKAGE` is not defined) that prevents library usage on macOS. + +___ + +## Version 1.6.0 + +* Support integration via [Carthage](https://github.com/Carthage/Carthage). +* Support integration via [Swift Package Manager](https://swift.org/package-manager). Please note that this way has some limitations: + * macOS 64-bit mach_* APIs is not available here. + * `protobuf-c` symbols are not prefixed, so it can cause conflicts with other libraries. + * Additional architectures like `arm64e` are not built explicitly. +* Migrate to Automatic Reference Counting (ARC). +* Embed required `protoc-c` sources instead of using submodule. No more additional steps on cloning the repo. +* Store sources generated from `*.proto` files to drop `protobuf-c` compiler requirement for building the library. It's required only for contributors now. +* Enable generating debug symbols for static libraries. Previously it was included only to macOS framework. +* Fix framework targets type issue that prevents use the library as a project dependency (instead of binary distribution) in Xcode 11. +* Fix implicit casting warnings. + +___ + +## Version 1.5.1 + +* Fix support for Xcode 10. + +___ + +## Version 1.5.0 + +* Drop support old versions of Xcode and iOS. The minimal versions are Xcode 10 and iOS 8 now. +* Remove `UIKit` dependency on iOS. +* Fix arm64e crash report text formatting. +* Fix possible crash `plcrash_log_writer_set_exception` method when `NSException` instances have a `nil` reason. +* Apply bit mask for non-pointer isa values on macOS x64 (used in runtime symbolication). +* Strip pointer authentication codes on arm64e. + +___ + +## Version 1.4.0 + +* Support macOS 10.15 and XCode 11 and drop support for macOS 10.6. +* Add support for tvOS apps. +* Update `protobuf-c` to version 1.3.2. `protoc-c` code generator binary has been removed from the repo, so it should be installed separately now (`brew install protobuf-c`). `protoc-c` C library is included as a git submodule, please make sure that it's initialized after update (`git submodule update --init`). +* Remove outdated "Google Toolbox for Mac" dependency. +* The sources aren't distributed in the release archive anymore. Please use GitHub snapshot instead. +* Distribute static libraries in a second archive aside the frameworks archive. +* Fix minor bugs in runtime symbolication: use correct bit-mask for the data pointer and correctly reset error code if no categories for currently symbolicating class. +* Add preview support for the arm64e CPU architecture. +* Support for arm64e devices that run an arm64 slice (which is the default for apps that were compiled with Xcode 10 or earlier). +* Remove support for armv6 CPU architecture as it is no longer supported. +* Improve namespacing to avoid symbol collisions when integrating PLCrashReporter. +* Fix a crash that occurred on macOS where PLCrashReporter would be caught in an endless loop handling signals. +* Make it possible to not add an uncaught exception handler via `shouldRegisterUncaughtExceptionHandler` property on `PLCrashReporterConfig`. This scenario is important when using PLCrashReporter inside managed runtimes, i.e. for a Xamarin app. This is not a breaking change and behavior will not change if you use PLCrashReporter. diff --git a/submodules/AppCenter-sdk/Vendor/PLCrashReporter/CrashReporter.xcodeproj/TemplateIcon.icns b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/CrashReporter.xcodeproj/TemplateIcon.icns new file mode 100644 index 0000000000..62cb7015e0 Binary files /dev/null and b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/CrashReporter.xcodeproj/TemplateIcon.icns differ diff --git a/submodules/AppCenter-sdk/Vendor/PLCrashReporter/CrashReporter.xcodeproj/project.pbxproj b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/CrashReporter.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..d302cc9c98 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/CrashReporter.xcodeproj/project.pbxproj @@ -0,0 +1,4469 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 52; + objects = { + +/* Begin PBXAggregateTarget section */ + 058812D410405908009128FB /* CrashReporter Archive */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 058812D71040592A009128FB /* Build configuration list for PBXAggregateTarget "CrashReporter Archive" */; + buildPhases = ( + C25664D023571BC60088513E /* Verify Modifications */, + 058812D310405908009128FB /* Create Archive With Universal Frameworks */, + C25664E62357487D0088513E /* Create Archive With Static Libraries */, + F8CF2BD2246C139400904633 /* Create Archive With XCFramework */, + ); + dependencies = ( + C2C74E1D25385F5B00313817 /* PBXTargetDependency */, + ); + name = "CrashReporter Archive"; + productName = "Disk Image"; + }; + C25664D123571D330088513E /* CrashReporter Documentation */ = { + isa = PBXAggregateTarget; + buildConfigurationList = C25664D223571D340088513E /* Build configuration list for PBXAggregateTarget "CrashReporter Documentation" */; + buildPhases = ( + C25664D523571D3D0088513E /* Generate Documentation */, + ); + dependencies = ( + ); + name = "CrashReporter Documentation"; + productName = Documentation; + }; + C2B90D952456FBD000834AFB /* CrashReporter iOS Universal */ = { + isa = PBXAggregateTarget; + buildConfigurationList = C2B90D962456FBD000834AFB /* Build configuration list for PBXAggregateTarget "CrashReporter iOS Universal" */; + buildPhases = ( + C2B90DAB2457178800834AFB /* Build iOS Device Framework */, + C2C74A842535CCC700313817 /* Build iOS Simulator Framework */, + C2C74E0825385BCE00313817 /* Combine iOS Universal Library */, + C2C74A892535CD8000313817 /* Combine iOS Universal Framework */, + ); + dependencies = ( + ); + name = "CrashReporter iOS Universal"; + productName = "CrashReporter iOS Universal"; + }; + C2B90D992456FBDE00834AFB /* CrashReporter tvOS Universal */ = { + isa = PBXAggregateTarget; + buildConfigurationList = C2B90D9A2456FBDF00834AFB /* Build configuration list for PBXAggregateTarget "CrashReporter tvOS Universal" */; + buildPhases = ( + C2C74C9825372B9800313817 /* Build tvOS Device Framework */, + C2C74C9925372B9A00313817 /* Build tvOS Simulator Framework */, + C2C74E0925385C9700313817 /* Combine tvOS Universal Library */, + C2C74C9A25372B9B00313817 /* Combine tvOS Universal Framework */, + ); + dependencies = ( + ); + name = "CrashReporter tvOS Universal"; + productName = "CrashReporter tvOS Universal"; + }; + C2C74D9B2538508300313817 /* CrashReporter */ = { + isa = PBXAggregateTarget; + buildConfigurationList = C2C74D9E2538508300313817 /* Build configuration list for PBXAggregateTarget "CrashReporter" */; + buildPhases = ( + C2C74DD1253851C300313817 /* Copy Products */, + ); + dependencies = ( + C2C74DC8253850A500313817 /* PBXTargetDependency */, + C2C74DC6253850A500313817 /* PBXTargetDependency */, + C2C74DCA253850A500313817 /* PBXTargetDependency */, + C2C74DC4253850A500313817 /* PBXTargetDependency */, + C2C74DCC253850A500313817 /* PBXTargetDependency */, + C2C74DCE253850A500313817 /* PBXTargetDependency */, + ); + name = CrashReporter; + productName = "Crash Reporter"; + }; + F8B9C72A24695BE600B9FEF6 /* CrashReporter XCFramework */ = { + isa = PBXAggregateTarget; + buildConfigurationList = F8B9C72B24695BE600B9FEF6 /* Build configuration list for PBXAggregateTarget "CrashReporter XCFramework" */; + buildPhases = ( + C2C74CBD253730FA00313817 /* Build Mac Catalyst Framework */, + F8B9C72E24695BF200B9FEF6 /* Combine XCFramework */, + ); + dependencies = ( + F8CF2BCD246C05D100904633 /* PBXTargetDependency */, + F8CF2BD1246C05D100904633 /* PBXTargetDependency */, + C23D0556248A7C440094EC6B /* PBXTargetDependency */, + ); + name = "CrashReporter XCFramework"; + productName = "CrashReporter XCFramework"; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 05102E1717B0151000B5D925 /* PLCrashProcessInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05102E1417B0151000B5D925 /* PLCrashProcessInfo.h */; }; + 05102E1A17B0151000B5D925 /* PLCrashProcessInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05102E1517B0151000B5D925 /* PLCrashProcessInfo.m */; }; + 05102E2417B2B80A00B5D925 /* PLCrashHostInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05102E2217B2B80A00B5D925 /* PLCrashHostInfo.h */; }; + 05102E2617B2B80A00B5D925 /* PLCrashHostInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05102E2217B2B80A00B5D925 /* PLCrashHostInfo.h */; }; + 05102E2817B2B80A00B5D925 /* PLCrashHostInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05102E2317B2B80A00B5D925 /* PLCrashHostInfo.m */; }; + 05102E2A17B2B80A00B5D925 /* PLCrashHostInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05102E2317B2B80A00B5D925 /* PLCrashHostInfo.m */; }; + 0513E23417D15ED400727919 /* PLCrashReportMachExceptionInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 0513E23217D15ED400727919 /* PLCrashReportMachExceptionInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0513E23517D15ED400727919 /* PLCrashReportMachExceptionInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 0513E23217D15ED400727919 /* PLCrashReportMachExceptionInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0513E23617D15ED400727919 /* PLCrashReportMachExceptionInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 0513E23217D15ED400727919 /* PLCrashReportMachExceptionInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0513E23817D15ED400727919 /* PLCrashReportMachExceptionInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 0513E23317D15ED400727919 /* PLCrashReportMachExceptionInfo.m */; }; + 0513E23A17D15ED400727919 /* PLCrashReportMachExceptionInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 0513E23317D15ED400727919 /* PLCrashReportMachExceptionInfo.m */; }; + 0513E23C17D15EE500727919 /* PLCrashReportMachExceptionInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 0513E23217D15ED400727919 /* PLCrashReportMachExceptionInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 051F067C17B6B0D4006D0EFA /* PLCrashMachExceptionPort.h in Headers */ = {isa = PBXBuildFile; fileRef = 051F067917B6B0D4006D0EFA /* PLCrashMachExceptionPort.h */; }; + 051F067F17B6B0D4006D0EFA /* PLCrashMachExceptionPort.m in Sources */ = {isa = PBXBuildFile; fileRef = 051F067A17B6B0D4006D0EFA /* PLCrashMachExceptionPort.m */; }; + 0527062F17CBCCA100E6A5D8 /* PLCrashMachExceptionPort.m in Sources */ = {isa = PBXBuildFile; fileRef = 051F067A17B6B0D4006D0EFA /* PLCrashMachExceptionPort.m */; }; + 0527063017CBCCC200E6A5D8 /* PLCrashProcessInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05102E1517B0151000B5D925 /* PLCrashProcessInfo.m */; }; + 0527063317CCF31100E6A5D8 /* PLCrashFeatureConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A2B3FF1795BA4100934198 /* PLCrashFeatureConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0527063417CCF31400E6A5D8 /* PLCrashFeatureConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A2B3FF1795BA4100934198 /* PLCrashFeatureConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 052A46BE1363650100987004 /* PLCrashAsyncImageList.h in Headers */ = {isa = PBXBuildFile; fileRef = 052A46BC1363650100987004 /* PLCrashAsyncImageList.h */; }; + 052A46BF1363650100987004 /* PLCrashAsyncImageList.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 052A46BD1363650100987004 /* PLCrashAsyncImageList.cpp */; }; + 052A46C21363650100987004 /* PLCrashAsyncImageList.h in Headers */ = {isa = PBXBuildFile; fileRef = 052A46BC1363650100987004 /* PLCrashAsyncImageList.h */; }; + 052A46C31363650100987004 /* PLCrashAsyncImageList.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 052A46BD1363650100987004 /* PLCrashAsyncImageList.cpp */; }; + 053347AD17E16B0200C52E50 /* PLCrashAsyncDwarfEncoding.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 05659DED17455DED00D2EE21 /* PLCrashAsyncDwarfEncoding.cpp */; }; + 054627AB11D998BB007891C7 /* PLCrashReportTextFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 054627A711D998BB007891C7 /* PLCrashReportTextFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 054627AC11D998BB007891C7 /* PLCrashReportTextFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 054627A811D998BB007891C7 /* PLCrashReportTextFormatter.m */; }; + 054627AD11D998BB007891C7 /* PLCrashReportTextFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 054627A711D998BB007891C7 /* PLCrashReportTextFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 054627AF11D998BB007891C7 /* PLCrashReportTextFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 054627A711D998BB007891C7 /* PLCrashReportTextFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 054627B111D998BB007891C7 /* PLCrashReportTextFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 054627A711D998BB007891C7 /* PLCrashReportTextFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 054627B211D998BB007891C7 /* PLCrashReportTextFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 054627A811D998BB007891C7 /* PLCrashReportTextFormatter.m */; }; + 054627B911D99D06007891C7 /* PLCrashReportFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 054627B811D99D06007891C7 /* PLCrashReportFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 054627BA11D99D06007891C7 /* PLCrashReportFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 054627B811D99D06007891C7 /* PLCrashReportFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 054627BC11D99D06007891C7 /* PLCrashReportFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 054627B811D99D06007891C7 /* PLCrashReportFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 054627BD11D99D06007891C7 /* PLCrashReportFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 054627B811D99D06007891C7 /* PLCrashReportFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 054F51080EEC73C80034B184 /* PLCrashReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = 054F51070EEC73C80034B184 /* PLCrashReporter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05614E291A966F5A00D62442 /* libCrashReporter.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 05CD31520EE936A9000FDE88 /* libCrashReporter.a */; }; + 0573B42C1681098E00395F2A /* PLCrashMachExceptionServer.h in Headers */ = {isa = PBXBuildFile; fileRef = 0573B42A1681098E00395F2A /* PLCrashMachExceptionServer.h */; }; + 0573B42E1681098E00395F2A /* PLCrashMachExceptionServer.h in Headers */ = {isa = PBXBuildFile; fileRef = 0573B42A1681098E00395F2A /* PLCrashMachExceptionServer.h */; }; + 0573B4301681098E00395F2A /* PLCrashMachExceptionServer.m in Sources */ = {isa = PBXBuildFile; fileRef = 0573B42B1681098E00395F2A /* PLCrashMachExceptionServer.m */; }; + 0573B4321681098E00395F2A /* PLCrashMachExceptionServer.m in Sources */ = {isa = PBXBuildFile; fileRef = 0573B42B1681098E00395F2A /* PLCrashMachExceptionServer.m */; }; + 0573B4481681107F00395F2A /* PLCrashReportRegisterInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05D9E54E16765A0200B39833 /* PLCrashReportRegisterInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0573B4491681108200395F2A /* PLCrashReportStackFrameInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05D9E5431676598200B39833 /* PLCrashReportStackFrameInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0573B44A1681108500395F2A /* PLCrashReportSymbolInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05D9E55916765D0200B39833 /* PLCrashReportSymbolInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05771CE213683ED4001DE4B1 /* PLCrashReportProcessorInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BB83CB1364A77800D53B84 /* PLCrashReportProcessorInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05771CE313683EDD001DE4B1 /* PLCrashReportMachineInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BB83EF1364AD3E00D53B84 /* PLCrashReportMachineInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 057C9BBE17970F54006B242E /* PLCrashFrameDWARFUnwind.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 05920D1E177B9257001E8975 /* PLCrashFrameDWARFUnwind.cpp */; }; + 057C9BBF17970F6D006B242E /* PLCrashAsyncDwarfEncoding.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 05659DED17455DED00D2EE21 /* PLCrashAsyncDwarfEncoding.cpp */; }; + 057C9BC017970F77006B242E /* PLCrashAsyncDwarfExpression.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 05E7488A176135CE009B8745 /* PLCrashAsyncDwarfExpression.cpp */; }; + 05920D21177B9257001E8975 /* PLCrashFrameDWARFUnwind.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 05920D1E177B9257001E8975 /* PLCrashFrameDWARFUnwind.cpp */; }; + 05920D27177B9257001E8975 /* PLCrashFrameDWARFUnwind.h in Headers */ = {isa = PBXBuildFile; fileRef = 05920D1F177B9257001E8975 /* PLCrashFrameDWARFUnwind.h */; }; + 059666E00EEDDFB8008A0601 /* PLCrashFrameWalker.h in Headers */ = {isa = PBXBuildFile; fileRef = 059666DA0EEDDFB8008A0601 /* PLCrashFrameWalker.h */; }; + 059666E10EEDDFB8008A0601 /* PLCrashFrameWalker.c in Sources */ = {isa = PBXBuildFile; fileRef = 059666DB0EEDDFB8008A0601 /* PLCrashFrameWalker.c */; }; + 059670270EEF6B1A008A0601 /* PLCrashLogWriter.h in Headers */ = {isa = PBXBuildFile; fileRef = 059670250EEF6B1A008A0601 /* PLCrashLogWriter.h */; }; + 059670280EEF6B1A008A0601 /* PLCrashLogWriter.m in Sources */ = {isa = PBXBuildFile; fileRef = 059670260EEF6B1A008A0601 /* PLCrashLogWriter.m */; }; + 05A04D8C15AB38C10011CFA4 /* PLCrashNamespace.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A2077215AB30C9001E3EFC /* PLCrashNamespace.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05A04D8D15AB38CD0011CFA4 /* PLCrashNamespace.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A2077215AB30C9001E3EFC /* PLCrashNamespace.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05A17DC516D7F81600888448 /* PLCrashAsyncThread.c in Sources */ = {isa = PBXBuildFile; fileRef = 05A17DC416D7F81600888448 /* PLCrashAsyncThread.c */; }; + 05A17DC716D7F81600888448 /* PLCrashAsyncThread.c in Sources */ = {isa = PBXBuildFile; fileRef = 05A17DC416D7F81600888448 /* PLCrashAsyncThread.c */; }; + 05A17DCD16D7F82700888448 /* PLCrashAsyncThread.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A17DCC16D7F82700888448 /* PLCrashAsyncThread.h */; }; + 05A17DCF16D7F82700888448 /* PLCrashAsyncThread.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A17DCC16D7F82700888448 /* PLCrashAsyncThread.h */; }; + 05A17DF116DBD0AD00888448 /* PLCrashAsyncThread_x86.c in Sources */ = {isa = PBXBuildFile; fileRef = 05A17DF016DBD0AD00888448 /* PLCrashAsyncThread_x86.c */; }; + 05A17DF316DBD0AD00888448 /* PLCrashAsyncThread_x86.c in Sources */ = {isa = PBXBuildFile; fileRef = 05A17DF016DBD0AD00888448 /* PLCrashAsyncThread_x86.c */; }; + 05A17DF816DBD0C200888448 /* PLCrashAsyncThread_arm.c in Sources */ = {isa = PBXBuildFile; fileRef = 05A17DF516DBD0C200888448 /* PLCrashAsyncThread_arm.c */; }; + 05A5E28117A82751008A75E5 /* PLCrashMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A5E28017A82751008A75E5 /* PLCrashMacros.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05A5E28217A82751008A75E5 /* PLCrashMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A5E28017A82751008A75E5 /* PLCrashMacros.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05A5E28F17C04188008A75E5 /* PLCrashAsyncLinkedList.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 05A5E28717C04188008A75E5 /* PLCrashAsyncLinkedList.hpp */; }; + 05A5E29117C04188008A75E5 /* PLCrashAsyncLinkedList.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 05A5E28717C04188008A75E5 /* PLCrashAsyncLinkedList.hpp */; }; + 05B69E1417CE6271001807C9 /* PLCrashReporterConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BEC43417BF1CB10082CBFB /* PLCrashReporterConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05B929E817C9336600B051E3 /* PLCrashUncaughtExceptionHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 05B929E617C9336600B051E3 /* PLCrashUncaughtExceptionHandler.h */; }; + 05B929EA17C9336600B051E3 /* PLCrashUncaughtExceptionHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 05B929E617C9336600B051E3 /* PLCrashUncaughtExceptionHandler.h */; }; + 05B929EC17C9336600B051E3 /* PLCrashUncaughtExceptionHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 05B929E717C9336600B051E3 /* PLCrashUncaughtExceptionHandler.m */; }; + 05B929EE17C9336600B051E3 /* PLCrashUncaughtExceptionHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 05B929E717C9336600B051E3 /* PLCrashUncaughtExceptionHandler.m */; }; + 05BB83CF1364A77800D53B84 /* PLCrashReportProcessorInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BB83CB1364A77800D53B84 /* PLCrashReportProcessorInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05BB83D01364A77800D53B84 /* PLCrashReportProcessorInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05BB83CC1364A77800D53B84 /* PLCrashReportProcessorInfo.m */; }; + 05BB83D11364A77800D53B84 /* PLCrashReportProcessorInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BB83CB1364A77800D53B84 /* PLCrashReportProcessorInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05BB83D31364A77800D53B84 /* PLCrashReportProcessorInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BB83CB1364A77800D53B84 /* PLCrashReportProcessorInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05BB83D41364A77800D53B84 /* PLCrashReportProcessorInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05BB83CC1364A77800D53B84 /* PLCrashReportProcessorInfo.m */; }; + 05BB83F11364AD3E00D53B84 /* PLCrashReportMachineInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BB83EF1364AD3E00D53B84 /* PLCrashReportMachineInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05BB83F31364AD3E00D53B84 /* PLCrashReportMachineInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BB83EF1364AD3E00D53B84 /* PLCrashReportMachineInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05BB83F41364AD3E00D53B84 /* PLCrashReportMachineInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05BB83F01364AD3E00D53B84 /* PLCrashReportMachineInfo.m */; }; + 05BB83F71364AD3E00D53B84 /* PLCrashReportMachineInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BB83EF1364AD3E00D53B84 /* PLCrashReportMachineInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05BB83F81364AD3E00D53B84 /* PLCrashReportMachineInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05BB83F01364AD3E00D53B84 /* PLCrashReportMachineInfo.m */; }; + 05BB84881364EDF200D53B84 /* PLCrashSysctl.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BB84841364EDF200D53B84 /* PLCrashSysctl.h */; }; + 05BB84891364EDF200D53B84 /* PLCrashSysctl.c in Sources */ = {isa = PBXBuildFile; fileRef = 05BB84851364EDF200D53B84 /* PLCrashSysctl.c */; }; + 05BB848C1364EDF200D53B84 /* PLCrashSysctl.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BB84841364EDF200D53B84 /* PLCrashSysctl.h */; }; + 05BB848D1364EDF200D53B84 /* PLCrashSysctl.c in Sources */ = {isa = PBXBuildFile; fileRef = 05BB84851364EDF200D53B84 /* PLCrashSysctl.c */; }; + 05BEC41717BAF92A0082CBFB /* PLCrashMachExceptionPortSet.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BEC41517BAF92A0082CBFB /* PLCrashMachExceptionPortSet.h */; }; + 05BEC41917BAF92A0082CBFB /* PLCrashMachExceptionPortSet.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BEC41517BAF92A0082CBFB /* PLCrashMachExceptionPortSet.h */; }; + 05BEC41B17BAF92A0082CBFB /* PLCrashMachExceptionPortSet.m in Sources */ = {isa = PBXBuildFile; fileRef = 05BEC41617BAF92A0082CBFB /* PLCrashMachExceptionPortSet.m */; }; + 05BEC41D17BAF92A0082CBFB /* PLCrashMachExceptionPortSet.m in Sources */ = {isa = PBXBuildFile; fileRef = 05BEC41617BAF92A0082CBFB /* PLCrashMachExceptionPortSet.m */; }; + 05BEC42617BD4F290082CBFB /* PLCrashAsyncMachExceptionInfo.c in Sources */ = {isa = PBXBuildFile; fileRef = 05BEC42517BD4F290082CBFB /* PLCrashAsyncMachExceptionInfo.c */; }; + 05BEC42817BD4F290082CBFB /* PLCrashAsyncMachExceptionInfo.c in Sources */ = {isa = PBXBuildFile; fileRef = 05BEC42517BD4F290082CBFB /* PLCrashAsyncMachExceptionInfo.c */; }; + 05BEC43617BF1CB10082CBFB /* PLCrashReporterConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BEC43417BF1CB10082CBFB /* PLCrashReporterConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05BEC43717BF1CB10082CBFB /* PLCrashReporterConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BEC43417BF1CB10082CBFB /* PLCrashReporterConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05BEC43817BF1CB10082CBFB /* PLCrashReporterConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BEC43417BF1CB10082CBFB /* PLCrashReporterConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05BEC43A17BF1CB10082CBFB /* PLCrashReporterConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 05BEC43517BF1CB10082CBFB /* PLCrashReporterConfig.m */; }; + 05BEC43C17BF1CB10082CBFB /* PLCrashReporterConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 05BEC43517BF1CB10082CBFB /* PLCrashReporterConfig.m */; }; + 05C5881D178B898C00BA118D /* PLCrashReportStackFrameInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05D9E5431676598200B39833 /* PLCrashReportStackFrameInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05C5881E178B89A300BA118D /* PLCrashReportSymbolInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05D9E55916765D0200B39833 /* PLCrashReportSymbolInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05C5881F178B89A800BA118D /* PLCrashReportRegisterInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05D9E54E16765A0200B39833 /* PLCrashReportRegisterInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05C76DA6176B8C7000E9B10D /* dwarf_opstream.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 05C76DA4176B8C7000E9B10D /* dwarf_opstream.cpp */; }; + 05C76DA8176B8C7000E9B10D /* dwarf_opstream.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 05C76DA4176B8C7000E9B10D /* dwarf_opstream.cpp */; }; + 05C76DAD176B8C7000E9B10D /* dwarf_opstream.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 05C76DA5176B8C7000E9B10D /* dwarf_opstream.hpp */; }; + 05C76DAF176B8C7000E9B10D /* dwarf_opstream.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 05C76DA5176B8C7000E9B10D /* dwarf_opstream.hpp */; }; + 05C76DC8176FBAF300E9B10D /* PLCrashAsyncDwarfCFAState.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 05C76DC6176FBAF300E9B10D /* PLCrashAsyncDwarfCFAState.cpp */; }; + 05C76DCA176FBAF300E9B10D /* PLCrashAsyncDwarfCFAState.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 05C76DC6176FBAF300E9B10D /* PLCrashAsyncDwarfCFAState.cpp */; }; + 05C76DCF176FBAF300E9B10D /* PLCrashAsyncDwarfCFAState.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 05C76DC7176FBAF300E9B10D /* PLCrashAsyncDwarfCFAState.hpp */; }; + 05C76DD1176FBAF300E9B10D /* PLCrashAsyncDwarfCFAState.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 05C76DC7176FBAF300E9B10D /* PLCrashAsyncDwarfCFAState.hpp */; }; + 05CD318B0EE93A90000FDE88 /* CrashReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = 05CD31890EE93A90000FDE88 /* CrashReporter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05CD318D0EE93A90000FDE88 /* CrashReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = 05CD31890EE93A90000FDE88 /* CrashReporter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05CD318E0EE93A90000FDE88 /* CrashReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = 05CD318A0EE93A90000FDE88 /* CrashReporter.m */; }; + 05CD339C0EE948EB000FDE88 /* PLCrashSignalHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 05CD339A0EE948EB000FDE88 /* PLCrashSignalHandler.h */; }; + 05CD339D0EE948EB000FDE88 /* PLCrashSignalHandler.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05CD339B0EE948EB000FDE88 /* PLCrashSignalHandler.mm */; settings = {COMPILER_FLAGS = "-fno-objc-exceptions"; }; }; + 05CD36470EF24758000FDE88 /* PLCrashAsync.c in Sources */ = {isa = PBXBuildFile; fileRef = 05CD36410EF24758000FDE88 /* PLCrashAsync.c */; }; + 05CD36D30EF25717000FDE88 /* PLCrashLogWriterEncoding.h in Headers */ = {isa = PBXBuildFile; fileRef = 05CD36CC0EF25717000FDE88 /* PLCrashLogWriterEncoding.h */; }; + 05CD36D40EF25717000FDE88 /* PLCrashLogWriterEncoding.c in Sources */ = {isa = PBXBuildFile; fileRef = 05CD36CD0EF25717000FDE88 /* PLCrashLogWriterEncoding.c */; }; + 05D9E5451676598200B39833 /* PLCrashReportStackFrameInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05D9E5431676598200B39833 /* PLCrashReportStackFrameInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05D9E5471676598200B39833 /* PLCrashReportStackFrameInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05D9E5431676598200B39833 /* PLCrashReportStackFrameInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05D9E5491676598200B39833 /* PLCrashReportStackFrameInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05D9E5441676598200B39833 /* PLCrashReportStackFrameInfo.m */; }; + 05D9E54B1676598200B39833 /* PLCrashReportStackFrameInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05D9E5441676598200B39833 /* PLCrashReportStackFrameInfo.m */; }; + 05D9E55016765A0200B39833 /* PLCrashReportRegisterInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05D9E54E16765A0200B39833 /* PLCrashReportRegisterInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05D9E55216765A0200B39833 /* PLCrashReportRegisterInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05D9E54E16765A0200B39833 /* PLCrashReportRegisterInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05D9E55416765A0200B39833 /* PLCrashReportRegisterInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05D9E54F16765A0200B39833 /* PLCrashReportRegisterInfo.m */; }; + 05D9E55616765A0200B39833 /* PLCrashReportRegisterInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05D9E54F16765A0200B39833 /* PLCrashReportRegisterInfo.m */; }; + 05D9E55B16765D0200B39833 /* PLCrashReportSymbolInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05D9E55916765D0200B39833 /* PLCrashReportSymbolInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05D9E55D16765D0200B39833 /* PLCrashReportSymbolInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05D9E55916765D0200B39833 /* PLCrashReportSymbolInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05D9E55F16765D0200B39833 /* PLCrashReportSymbolInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05D9E55A16765D0200B39833 /* PLCrashReportSymbolInfo.m */; }; + 05D9E56116765D0200B39833 /* PLCrashReportSymbolInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05D9E55A16765D0200B39833 /* PLCrashReportSymbolInfo.m */; }; + 05DEE63F1636E62B007E99DC /* PLCrashAsyncMObject.c in Sources */ = {isa = PBXBuildFile; fileRef = 05DEE63E1636E62B007E99DC /* PLCrashAsyncMObject.c */; }; + 05DEE6411636E62B007E99DC /* PLCrashAsyncMObject.c in Sources */ = {isa = PBXBuildFile; fileRef = 05DEE63E1636E62B007E99DC /* PLCrashAsyncMObject.c */; }; + 05E731F80EFA1AE3005EDFB7 /* CrashReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = 05CD318A0EE93A90000FDE88 /* CrashReporter.m */; }; + 05E731F90EFA1AE3005EDFB7 /* PLCrashSignalHandler.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05CD339B0EE948EB000FDE88 /* PLCrashSignalHandler.mm */; settings = {COMPILER_FLAGS = "-fno-objc-exceptions"; }; }; + 05E731FA0EFA1AE3005EDFB7 /* PLCrashFrameWalker.c in Sources */ = {isa = PBXBuildFile; fileRef = 059666DB0EEDDFB8008A0601 /* PLCrashFrameWalker.c */; }; + 05E731FD0EFA1AE3005EDFB7 /* PLCrashLogWriter.m in Sources */ = {isa = PBXBuildFile; fileRef = 059670260EEF6B1A008A0601 /* PLCrashLogWriter.m */; }; + 05E731FE0EFA1AE3005EDFB7 /* PLCrashAsync.c in Sources */ = {isa = PBXBuildFile; fileRef = 05CD36410EF24758000FDE88 /* PLCrashAsync.c */; }; + 05E731FF0EFA1AE3005EDFB7 /* PLCrashLogWriterEncoding.c in Sources */ = {isa = PBXBuildFile; fileRef = 05CD36CD0EF25717000FDE88 /* PLCrashLogWriterEncoding.c */; }; + 05E732000EFA1AE3005EDFB7 /* PLCrashReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F40ACA0EF7379F008050CF /* PLCrashReporter.m */; }; + 05E732010EFA1AE3005EDFB7 /* PLCrashReport.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F411A50EF8DA31008050CF /* PLCrashReport.m */; }; + 05E732040EFA1AE3005EDFB7 /* PLCrashReportSystemInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F413440EF995C0008050CF /* PLCrashReportSystemInfo.m */; }; + 05E732050EFA1AE3005EDFB7 /* PLCrashReportApplicationInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F4141D0EF9A6C4008050CF /* PLCrashReportApplicationInfo.m */; }; + 05E732060EFA1AE3005EDFB7 /* PLCrashReportThreadInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F414810EF9BFAC008050CF /* PLCrashReportThreadInfo.m */; }; + 05E732070EFA1AE3005EDFB7 /* PLCrashReportBinaryImageInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F4150C0EF9DD9B008050CF /* PLCrashReportBinaryImageInfo.m */; }; + 05E732080EFA1AE3005EDFB7 /* PLCrashReportExceptionInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F415520EF9E078008050CF /* PLCrashReportExceptionInfo.m */; }; + 05E734340EFAC46D005EDFB7 /* PLCrashAsyncSignalInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05E734300EFAC46D005EDFB7 /* PLCrashAsyncSignalInfo.h */; }; + 05E734350EFAC46D005EDFB7 /* PLCrashAsyncSignalInfo.c in Sources */ = {isa = PBXBuildFile; fileRef = 05E734310EFAC46D005EDFB7 /* PLCrashAsyncSignalInfo.c */; }; + 05E734380EFAC46D005EDFB7 /* PLCrashAsyncSignalInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05E734300EFAC46D005EDFB7 /* PLCrashAsyncSignalInfo.h */; }; + 05E734390EFAC46D005EDFB7 /* PLCrashAsyncSignalInfo.c in Sources */ = {isa = PBXBuildFile; fileRef = 05E734310EFAC46D005EDFB7 /* PLCrashAsyncSignalInfo.c */; }; + 05E734F90EFAE59C005EDFB7 /* PLCrashReportSignalInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05E734F50EFAE59C005EDFB7 /* PLCrashReportSignalInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05E734FA0EFAE59C005EDFB7 /* PLCrashReportSignalInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E734F60EFAE59C005EDFB7 /* PLCrashReportSignalInfo.m */; }; + 05E734FB0EFAE59C005EDFB7 /* PLCrashReportSignalInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05E734F50EFAE59C005EDFB7 /* PLCrashReportSignalInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05E734FD0EFAE59C005EDFB7 /* PLCrashReportSignalInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05E734F50EFAE59C005EDFB7 /* PLCrashReportSignalInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05E734FE0EFAE59C005EDFB7 /* PLCrashReportSignalInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E734F60EFAE59C005EDFB7 /* PLCrashReportSignalInfo.m */; }; + 05E7484D175E5349009B8745 /* PLCrashAsyncDwarfPrimitives.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 05E7484C175E5349009B8745 /* PLCrashAsyncDwarfPrimitives.cpp */; }; + 05E7484F175E5349009B8745 /* PLCrashAsyncDwarfPrimitives.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 05E7484C175E5349009B8745 /* PLCrashAsyncDwarfPrimitives.cpp */; }; + 05E7485C1760D62A009B8745 /* PLCrashAsyncDwarfFDE.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 05E748591760D62A009B8745 /* PLCrashAsyncDwarfFDE.hpp */; }; + 05E7485F1760D64D009B8745 /* PLCrashAsyncDwarfFDE.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 05E7485E1760D64D009B8745 /* PLCrashAsyncDwarfFDE.cpp */; }; + 05E748611760D64D009B8745 /* PLCrashAsyncDwarfFDE.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 05E7485E1760D64D009B8745 /* PLCrashAsyncDwarfFDE.cpp */; }; + 05E748671760D891009B8745 /* PLCrashAsyncDwarfCIE.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 05E748661760D890009B8745 /* PLCrashAsyncDwarfCIE.cpp */; }; + 05E748691760D891009B8745 /* PLCrashAsyncDwarfCIE.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 05E748661760D890009B8745 /* PLCrashAsyncDwarfCIE.cpp */; }; + 05E7487B176118C2009B8745 /* PLCrashAsyncDwarfCFAStateEvaluation.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 05E7487A176118C1009B8745 /* PLCrashAsyncDwarfCFAStateEvaluation.cpp */; }; + 05E7487D176118C2009B8745 /* PLCrashAsyncDwarfCFAStateEvaluation.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 05E7487A176118C1009B8745 /* PLCrashAsyncDwarfCFAStateEvaluation.cpp */; }; + 05E7488C176135CF009B8745 /* PLCrashAsyncDwarfExpression.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 05E74889176135CE009B8745 /* PLCrashAsyncDwarfExpression.hpp */; }; + 05E7488F176135CF009B8745 /* PLCrashAsyncDwarfExpression.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 05E7488A176135CE009B8745 /* PLCrashAsyncDwarfExpression.cpp */; }; + 05E748AE17616D30009B8745 /* dwarf_stack.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 05E748A617616D30009B8745 /* dwarf_stack.hpp */; }; + 05E748B017616D30009B8745 /* dwarf_stack.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 05E748A617616D30009B8745 /* dwarf_stack.hpp */; }; + 05EB2AF715B454DD0066EB4D /* PLCrashAsyncThread_current.S in Sources */ = {isa = PBXBuildFile; fileRef = 05EB2AF615B454DD0066EB4D /* PLCrashAsyncThread_current.S */; }; + 05EB2AF915B454DD0066EB4D /* PLCrashAsyncThread_current.S in Sources */ = {isa = PBXBuildFile; fileRef = 05EB2AF615B454DD0066EB4D /* PLCrashAsyncThread_current.S */; }; + 05EB2AFD15B456750066EB4D /* PLCrashAsyncThread_current.c in Sources */ = {isa = PBXBuildFile; fileRef = 05EB2AFC15B456750066EB4D /* PLCrashAsyncThread_current.c */; }; + 05EB2AFF15B456750066EB4D /* PLCrashAsyncThread_current.c in Sources */ = {isa = PBXBuildFile; fileRef = 05EB2AFC15B456750066EB4D /* PLCrashAsyncThread_current.c */; }; + 05EB2B0F15B6FDA80066EB4D /* PLCrashReporterNSError.h in Headers */ = {isa = PBXBuildFile; fileRef = 05EB2B0D15B6FDA70066EB4D /* PLCrashReporterNSError.h */; }; + 05EB2B1115B6FDA80066EB4D /* PLCrashReporterNSError.h in Headers */ = {isa = PBXBuildFile; fileRef = 05EB2B0D15B6FDA70066EB4D /* PLCrashReporterNSError.h */; }; + 05EB2B1315B6FDA80066EB4D /* PLCrashReporterNSError.m in Sources */ = {isa = PBXBuildFile; fileRef = 05EB2B0E15B6FDA70066EB4D /* PLCrashReporterNSError.m */; }; + 05EB2B1515B6FDA80066EB4D /* PLCrashReporterNSError.m in Sources */ = {isa = PBXBuildFile; fileRef = 05EB2B0E15B6FDA70066EB4D /* PLCrashReporterNSError.m */; }; + 05EC51D7105316E900DB9D39 /* CrashReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = 05CD31890EE93A90000FDE88 /* CrashReporter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05EC51D9105316E900DB9D39 /* PLCrashReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = 054F51070EEC73C80034B184 /* PLCrashReporter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05EC51DE105316E900DB9D39 /* PLCrashReport.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F411A40EF8DA31008050CF /* PLCrashReport.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05EC51DF105316E900DB9D39 /* PLCrashReportSystemInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F413430EF995C0008050CF /* PLCrashReportSystemInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05EC51E0105316E900DB9D39 /* PLCrashReportApplicationInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F4141C0EF9A6C4008050CF /* PLCrashReportApplicationInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05EC51E1105316E900DB9D39 /* PLCrashReportThreadInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F414800EF9BFAC008050CF /* PLCrashReportThreadInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05EC51E2105316E900DB9D39 /* PLCrashReportBinaryImageInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F4150B0EF9DD9B008050CF /* PLCrashReportBinaryImageInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05EC51E3105316E900DB9D39 /* PLCrashReportExceptionInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F415510EF9E078008050CF /* PLCrashReportExceptionInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05EC51E5105316E900DB9D39 /* PLCrashReportSignalInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05E734F50EFAE59C005EDFB7 /* PLCrashReportSignalInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05F3CD6016DD6A3B007911FB /* PLCrashFrameCompactUnwind.c in Sources */ = {isa = PBXBuildFile; fileRef = 05F3CD5F16DD6A3B007911FB /* PLCrashFrameCompactUnwind.c */; }; + 05F3CD6216DD6A3B007911FB /* PLCrashFrameCompactUnwind.c in Sources */ = {isa = PBXBuildFile; fileRef = 05F3CD5F16DD6A3B007911FB /* PLCrashFrameCompactUnwind.c */; }; + 05F3CD6D16DE7625007911FB /* Tests in Resources */ = {isa = PBXBuildFile; fileRef = 05F3CD6C16DE7625007911FB /* Tests */; }; + 05F3CD6F16DE7625007911FB /* Tests in Resources */ = {isa = PBXBuildFile; fileRef = 05F3CD6C16DE7625007911FB /* Tests */; }; + 05F3CD7416DFC744007911FB /* PLCrashAsyncCompactUnwindEncoding.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F3CD7216DFC744007911FB /* PLCrashAsyncCompactUnwindEncoding.h */; }; + 05F3CD7616DFC744007911FB /* PLCrashAsyncCompactUnwindEncoding.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F3CD7216DFC744007911FB /* PLCrashAsyncCompactUnwindEncoding.h */; }; + 05F3CD7816DFC744007911FB /* PLCrashAsyncCompactUnwindEncoding.c in Sources */ = {isa = PBXBuildFile; fileRef = 05F3CD7316DFC744007911FB /* PLCrashAsyncCompactUnwindEncoding.c */; }; + 05F3CD7A16DFC744007911FB /* PLCrashAsyncCompactUnwindEncoding.c in Sources */ = {isa = PBXBuildFile; fileRef = 05F3CD7316DFC744007911FB /* PLCrashAsyncCompactUnwindEncoding.c */; }; + 05F40ACC0EF7379F008050CF /* PLCrashReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F40ACA0EF7379F008050CF /* PLCrashReporter.m */; }; + 05F411A80EF8DA31008050CF /* PLCrashReport.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F411A40EF8DA31008050CF /* PLCrashReport.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05F411A90EF8DA31008050CF /* PLCrashReport.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F411A50EF8DA31008050CF /* PLCrashReport.m */; }; + 05F411AA0EF8DA31008050CF /* PLCrashReport.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F411A40EF8DA31008050CF /* PLCrashReport.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05F413470EF995C0008050CF /* PLCrashReportSystemInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F413430EF995C0008050CF /* PLCrashReportSystemInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05F413480EF995C0008050CF /* PLCrashReportSystemInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F413440EF995C0008050CF /* PLCrashReportSystemInfo.m */; }; + 05F413490EF995C0008050CF /* PLCrashReportSystemInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F413430EF995C0008050CF /* PLCrashReportSystemInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05F414200EF9A6C4008050CF /* PLCrashReportApplicationInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F4141C0EF9A6C4008050CF /* PLCrashReportApplicationInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05F414220EF9A6C4008050CF /* PLCrashReportApplicationInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F4141C0EF9A6C4008050CF /* PLCrashReportApplicationInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05F414230EF9A6C4008050CF /* PLCrashReportApplicationInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F4141D0EF9A6C4008050CF /* PLCrashReportApplicationInfo.m */; }; + 05F414840EF9BFAC008050CF /* PLCrashReportThreadInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F414800EF9BFAC008050CF /* PLCrashReportThreadInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05F414860EF9BFAC008050CF /* PLCrashReportThreadInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F414800EF9BFAC008050CF /* PLCrashReportThreadInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05F414870EF9BFAC008050CF /* PLCrashReportThreadInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F414810EF9BFAC008050CF /* PLCrashReportThreadInfo.m */; }; + 05F4150F0EF9DD9B008050CF /* PLCrashReportBinaryImageInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F4150B0EF9DD9B008050CF /* PLCrashReportBinaryImageInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05F415110EF9DD9B008050CF /* PLCrashReportBinaryImageInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F4150B0EF9DD9B008050CF /* PLCrashReportBinaryImageInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05F415120EF9DD9B008050CF /* PLCrashReportBinaryImageInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F4150C0EF9DD9B008050CF /* PLCrashReportBinaryImageInfo.m */; }; + 05F415550EF9E078008050CF /* PLCrashReportExceptionInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F415510EF9E078008050CF /* PLCrashReportExceptionInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05F415570EF9E078008050CF /* PLCrashReportExceptionInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F415510EF9E078008050CF /* PLCrashReportExceptionInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 05F415580EF9E078008050CF /* PLCrashReportExceptionInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F415520EF9E078008050CF /* PLCrashReportExceptionInfo.m */; }; + 05F76DD3162F213E00A668C7 /* PLCrashAsyncMachOImage.c in Sources */ = {isa = PBXBuildFile; fileRef = 05F76DD2162F213E00A668C7 /* PLCrashAsyncMachOImage.c */; }; + 05F76DD5162F213E00A668C7 /* PLCrashAsyncMachOImage.c in Sources */ = {isa = PBXBuildFile; fileRef = 05F76DD2162F213E00A668C7 /* PLCrashAsyncMachOImage.c */; }; + 2D0E10461141F7DC00CE1BD6 /* PLCrashReportProcessInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 2D0E10441141F7DC00CE1BD6 /* PLCrashReportProcessInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2D0E10481141F7DC00CE1BD6 /* PLCrashReportProcessInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 2D0E10441141F7DC00CE1BD6 /* PLCrashReportProcessInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2D0E10491141F7DC00CE1BD6 /* PLCrashReportProcessInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D0E10451141F7DC00CE1BD6 /* PLCrashReportProcessInfo.m */; }; + 2D0E104A1141F7DC00CE1BD6 /* PLCrashReportProcessInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 2D0E10441141F7DC00CE1BD6 /* PLCrashReportProcessInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2D0E104B1141F7DC00CE1BD6 /* PLCrashReportProcessInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D0E10451141F7DC00CE1BD6 /* PLCrashReportProcessInfo.m */; }; + 2D0E104E1141F7DC00CE1BD6 /* PLCrashReportProcessInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 2D0E10441141F7DC00CE1BD6 /* PLCrashReportProcessInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D7AF1C4D22D8005A8B4C /* CrashReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = 05CD31890EE93A90000FDE88 /* CrashReporter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D7B01C4D22D8005A8B4C /* PLCrashSignalHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 05CD339A0EE948EB000FDE88 /* PLCrashSignalHandler.h */; }; + 8064D7B11C4D22D8005A8B4C /* PLCrashFrameWalker.h in Headers */ = {isa = PBXBuildFile; fileRef = 059666DA0EEDDFB8008A0601 /* PLCrashFrameWalker.h */; }; + 8064D7B21C4D22D8005A8B4C /* PLCrashLogWriter.h in Headers */ = {isa = PBXBuildFile; fileRef = 059670250EEF6B1A008A0601 /* PLCrashLogWriter.h */; }; + 8064D7B31C4D22D8005A8B4C /* PLCrashLogWriterEncoding.h in Headers */ = {isa = PBXBuildFile; fileRef = 05CD36CC0EF25717000FDE88 /* PLCrashLogWriterEncoding.h */; }; + 8064D7B41C4D22D8005A8B4C /* PLCrashReport.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F411A40EF8DA31008050CF /* PLCrashReport.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D7B51C4D22D8005A8B4C /* PLCrashReportSystemInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F413430EF995C0008050CF /* PLCrashReportSystemInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D7B61C4D22D8005A8B4C /* PLCrashReportApplicationInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F4141C0EF9A6C4008050CF /* PLCrashReportApplicationInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D7B71C4D22D8005A8B4C /* PLCrashReportThreadInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F414800EF9BFAC008050CF /* PLCrashReportThreadInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D7B81C4D22D8005A8B4C /* PLCrashReportBinaryImageInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F4150B0EF9DD9B008050CF /* PLCrashReportBinaryImageInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D7B91C4D22D8005A8B4C /* PLCrashReportExceptionInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F415510EF9E078008050CF /* PLCrashReportExceptionInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D7BA1C4D22D8005A8B4C /* PLCrashAsyncSignalInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05E734300EFAC46D005EDFB7 /* PLCrashAsyncSignalInfo.h */; }; + 8064D7BB1C4D22D8005A8B4C /* PLCrashReportSignalInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05E734F50EFAE59C005EDFB7 /* PLCrashReportSignalInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D7BC1C4D22D8005A8B4C /* PLCrashReportProcessInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 2D0E10441141F7DC00CE1BD6 /* PLCrashReportProcessInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D7BD1C4D22D8005A8B4C /* PLCrashReportTextFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 054627A711D998BB007891C7 /* PLCrashReportTextFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D7BE1C4D22D8005A8B4C /* PLCrashReportFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 054627B811D99D06007891C7 /* PLCrashReportFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D7BF1C4D22D8005A8B4C /* PLCrashAsyncImageList.h in Headers */ = {isa = PBXBuildFile; fileRef = 052A46BC1363650100987004 /* PLCrashAsyncImageList.h */; }; + 8064D7C01C4D22D8005A8B4C /* PLCrashReportProcessorInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BB83CB1364A77800D53B84 /* PLCrashReportProcessorInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D7C11C4D22D8005A8B4C /* PLCrashReportMachineInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BB83EF1364AD3E00D53B84 /* PLCrashReportMachineInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D7C21C4D22D8005A8B4C /* PLCrashSysctl.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BB84841364EDF200D53B84 /* PLCrashSysctl.h */; }; + 8064D7C31C4D22D8005A8B4C /* PLCrashReporterNSError.h in Headers */ = {isa = PBXBuildFile; fileRef = 05EB2B0D15B6FDA70066EB4D /* PLCrashReporterNSError.h */; }; + 8064D7C41C4D22D8005A8B4C /* PLCrashReportStackFrameInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05D9E5431676598200B39833 /* PLCrashReportStackFrameInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D7C51C4D22D8005A8B4C /* PLCrashReportRegisterInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05D9E54E16765A0200B39833 /* PLCrashReportRegisterInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D7C61C4D22D8005A8B4C /* PLCrashReportSymbolInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05D9E55916765D0200B39833 /* PLCrashReportSymbolInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D7C71C4D22D8005A8B4C /* PLCrashMachExceptionServer.h in Headers */ = {isa = PBXBuildFile; fileRef = 0573B42A1681098E00395F2A /* PLCrashMachExceptionServer.h */; }; + 8064D7C81C4D22D8005A8B4C /* PLCrashFrameStackUnwind.h in Headers */ = {isa = PBXBuildFile; fileRef = FCE4522F86AC61C08E9DCC17 /* PLCrashFrameStackUnwind.h */; }; + 8064D7C91C4D22D8005A8B4C /* PLCrashAsyncThread.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A17DCC16D7F82700888448 /* PLCrashAsyncThread.h */; }; + 8064D7CA1C4D22D8005A8B4C /* PLCrashAsyncCompactUnwindEncoding.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F3CD7216DFC744007911FB /* PLCrashAsyncCompactUnwindEncoding.h */; }; + 8064D7CB1C4D22D8005A8B4C /* PLCrashAsyncDwarfFDE.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 05E748591760D62A009B8745 /* PLCrashAsyncDwarfFDE.hpp */; }; + 8064D7CC1C4D22D8005A8B4C /* PLCrashAsyncDwarfExpression.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 05E74889176135CE009B8745 /* PLCrashAsyncDwarfExpression.hpp */; }; + 8064D7CD1C4D22D8005A8B4C /* dwarf_stack.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 05E748A617616D30009B8745 /* dwarf_stack.hpp */; }; + 8064D7CE1C4D22D8005A8B4C /* dwarf_opstream.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 05C76DA5176B8C7000E9B10D /* dwarf_opstream.hpp */; }; + 8064D7CF1C4D22D8005A8B4C /* PLCrashAsyncDwarfCFAState.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 05C76DC7176FBAF300E9B10D /* PLCrashAsyncDwarfCFAState.hpp */; }; + 8064D7D01C4D22D8005A8B4C /* PLCrashFrameDWARFUnwind.h in Headers */ = {isa = PBXBuildFile; fileRef = 05920D1F177B9257001E8975 /* PLCrashFrameDWARFUnwind.h */; }; + 8064D7D11C4D22D8005A8B4C /* PLCrashProcessInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05102E1417B0151000B5D925 /* PLCrashProcessInfo.h */; }; + 8064D7D21C4D22D8005A8B4C /* PLCrashHostInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05102E2217B2B80A00B5D925 /* PLCrashHostInfo.h */; }; + 8064D7D31C4D22D8005A8B4C /* PLCrashMachExceptionPort.h in Headers */ = {isa = PBXBuildFile; fileRef = 051F067917B6B0D4006D0EFA /* PLCrashMachExceptionPort.h */; }; + 8064D7D41C4D22D8005A8B4C /* PLCrashMachExceptionPortSet.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BEC41517BAF92A0082CBFB /* PLCrashMachExceptionPortSet.h */; }; + 8064D7D51C4D22D8005A8B4C /* PLCrashReporterConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BEC43417BF1CB10082CBFB /* PLCrashReporterConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D7D61C4D22D8005A8B4C /* PLCrashAsyncLinkedList.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 05A5E28717C04188008A75E5 /* PLCrashAsyncLinkedList.hpp */; }; + 8064D7D71C4D22D8005A8B4C /* PLCrashUncaughtExceptionHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 05B929E617C9336600B051E3 /* PLCrashUncaughtExceptionHandler.h */; }; + 8064D7D81C4D22D8005A8B4C /* PLCrashReportMachExceptionInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 0513E23217D15ED400727919 /* PLCrashReportMachExceptionInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D7DA1C4D22D8005A8B4C /* CrashReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = 05CD318A0EE93A90000FDE88 /* CrashReporter.m */; }; + 8064D7DB1C4D22D8005A8B4C /* PLCrashAsyncDwarfEncoding.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 05659DED17455DED00D2EE21 /* PLCrashAsyncDwarfEncoding.cpp */; }; + 8064D7DC1C4D22D8005A8B4C /* PLCrashSignalHandler.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05CD339B0EE948EB000FDE88 /* PLCrashSignalHandler.mm */; settings = {COMPILER_FLAGS = "-fno-objc-exceptions"; }; }; + 8064D7DD1C4D22D8005A8B4C /* PLCrashFrameWalker.c in Sources */ = {isa = PBXBuildFile; fileRef = 059666DB0EEDDFB8008A0601 /* PLCrashFrameWalker.c */; }; + 8064D7DE1C4D22D8005A8B4C /* PLCrashLogWriter.m in Sources */ = {isa = PBXBuildFile; fileRef = 059670260EEF6B1A008A0601 /* PLCrashLogWriter.m */; }; + 8064D7DF1C4D22D8005A8B4C /* PLCrashAsync.c in Sources */ = {isa = PBXBuildFile; fileRef = 05CD36410EF24758000FDE88 /* PLCrashAsync.c */; }; + 8064D7E01C4D22D8005A8B4C /* PLCrashLogWriterEncoding.c in Sources */ = {isa = PBXBuildFile; fileRef = 05CD36CD0EF25717000FDE88 /* PLCrashLogWriterEncoding.c */; }; + 8064D7E11C4D22D8005A8B4C /* PLCrashReporter.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F40ACA0EF7379F008050CF /* PLCrashReporter.m */; }; + 8064D7E21C4D22D8005A8B4C /* PLCrashReport.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F411A50EF8DA31008050CF /* PLCrashReport.m */; }; + 8064D7E51C4D22D8005A8B4C /* PLCrashReportSystemInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F413440EF995C0008050CF /* PLCrashReportSystemInfo.m */; }; + 8064D7E61C4D22D8005A8B4C /* PLCrashReportApplicationInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F4141D0EF9A6C4008050CF /* PLCrashReportApplicationInfo.m */; }; + 8064D7E71C4D22D8005A8B4C /* PLCrashReportThreadInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F414810EF9BFAC008050CF /* PLCrashReportThreadInfo.m */; }; + 8064D7E81C4D22D8005A8B4C /* PLCrashReportBinaryImageInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F4150C0EF9DD9B008050CF /* PLCrashReportBinaryImageInfo.m */; }; + 8064D7E91C4D22D8005A8B4C /* PLCrashReportExceptionInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F415520EF9E078008050CF /* PLCrashReportExceptionInfo.m */; }; + 8064D7EA1C4D22D8005A8B4C /* PLCrashAsyncSignalInfo.c in Sources */ = {isa = PBXBuildFile; fileRef = 05E734310EFAC46D005EDFB7 /* PLCrashAsyncSignalInfo.c */; }; + 8064D7EB1C4D22D8005A8B4C /* PLCrashReportSignalInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E734F60EFAE59C005EDFB7 /* PLCrashReportSignalInfo.m */; }; + 8064D7EC1C4D22D8005A8B4C /* PLCrashReportProcessInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D0E10451141F7DC00CE1BD6 /* PLCrashReportProcessInfo.m */; }; + 8064D7ED1C4D22D8005A8B4C /* PLCrashReportTextFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 054627A811D998BB007891C7 /* PLCrashReportTextFormatter.m */; }; + 8064D7EE1C4D22D8005A8B4C /* PLCrashAsyncImageList.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 052A46BD1363650100987004 /* PLCrashAsyncImageList.cpp */; }; + 8064D7EF1C4D22D8005A8B4C /* PLCrashReportProcessorInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05BB83CC1364A77800D53B84 /* PLCrashReportProcessorInfo.m */; }; + 8064D7F01C4D22D8005A8B4C /* PLCrashReportMachineInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05BB83F01364AD3E00D53B84 /* PLCrashReportMachineInfo.m */; }; + 8064D7F11C4D22D8005A8B4C /* PLCrashSysctl.c in Sources */ = {isa = PBXBuildFile; fileRef = 05BB84851364EDF200D53B84 /* PLCrashSysctl.c */; }; + 8064D7F21C4D22D8005A8B4C /* PLCrashAsyncThread_current.S in Sources */ = {isa = PBXBuildFile; fileRef = 05EB2AF615B454DD0066EB4D /* PLCrashAsyncThread_current.S */; }; + 8064D7F31C4D22D8005A8B4C /* PLCrashAsyncThread_current.c in Sources */ = {isa = PBXBuildFile; fileRef = 05EB2AFC15B456750066EB4D /* PLCrashAsyncThread_current.c */; }; + 8064D7F41C4D22D8005A8B4C /* PLCrashReporterNSError.m in Sources */ = {isa = PBXBuildFile; fileRef = 05EB2B0E15B6FDA70066EB4D /* PLCrashReporterNSError.m */; }; + 8064D7F51C4D22D8005A8B4C /* PLCrashAsyncMachOImage.c in Sources */ = {isa = PBXBuildFile; fileRef = 05F76DD2162F213E00A668C7 /* PLCrashAsyncMachOImage.c */; }; + 8064D7F61C4D22D8005A8B4C /* PLCrashAsyncMObject.c in Sources */ = {isa = PBXBuildFile; fileRef = 05DEE63E1636E62B007E99DC /* PLCrashAsyncMObject.c */; }; + 8064D7F71C4D22D8005A8B4C /* PLCrashAsyncObjCSection.mm in Sources */ = {isa = PBXBuildFile; fileRef = C2198DD81640188C006EB46A /* PLCrashAsyncObjCSection.mm */; }; + 8064D7F81C4D22D8005A8B4C /* PLCrashAsyncSymbolication.c in Sources */ = {isa = PBXBuildFile; fileRef = C26022851642FCA6007FC29F /* PLCrashAsyncSymbolication.c */; }; + 8064D7F91C4D22D8005A8B4C /* PLCrashAsyncMachOString.c in Sources */ = {isa = PBXBuildFile; fileRef = C2198E0516441CF5006EB46A /* PLCrashAsyncMachOString.c */; }; + 8064D7FA1C4D22D8005A8B4C /* PLCrashReportStackFrameInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05D9E5441676598200B39833 /* PLCrashReportStackFrameInfo.m */; }; + 8064D7FB1C4D22D8005A8B4C /* PLCrashReportRegisterInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05D9E54F16765A0200B39833 /* PLCrashReportRegisterInfo.m */; }; + 8064D7FC1C4D22D8005A8B4C /* PLCrashReportSymbolInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05D9E55A16765D0200B39833 /* PLCrashReportSymbolInfo.m */; }; + 8064D7FD1C4D22D8005A8B4C /* PLCrashMachExceptionServer.m in Sources */ = {isa = PBXBuildFile; fileRef = 0573B42B1681098E00395F2A /* PLCrashMachExceptionServer.m */; }; + 8064D7FE1C4D22D8005A8B4C /* PLCrashFrameStackUnwind.c in Sources */ = {isa = PBXBuildFile; fileRef = FCE45837C8C773EFFD15C52B /* PLCrashFrameStackUnwind.c */; }; + 8064D7FF1C4D22D8005A8B4C /* PLCrashAsyncThread.c in Sources */ = {isa = PBXBuildFile; fileRef = 05A17DC416D7F81600888448 /* PLCrashAsyncThread.c */; }; + 8064D8001C4D22D8005A8B4C /* PLCrashAsyncThread_x86.c in Sources */ = {isa = PBXBuildFile; fileRef = 05A17DF016DBD0AD00888448 /* PLCrashAsyncThread_x86.c */; }; + 8064D8011C4D22D8005A8B4C /* PLCrashAsyncThread_arm.c in Sources */ = {isa = PBXBuildFile; fileRef = 05A17DF516DBD0C200888448 /* PLCrashAsyncThread_arm.c */; }; + 8064D8021C4D22D8005A8B4C /* PLCrashFrameCompactUnwind.c in Sources */ = {isa = PBXBuildFile; fileRef = 05F3CD5F16DD6A3B007911FB /* PLCrashFrameCompactUnwind.c */; }; + 8064D8031C4D22D8005A8B4C /* PLCrashAsyncCompactUnwindEncoding.c in Sources */ = {isa = PBXBuildFile; fileRef = 05F3CD7316DFC744007911FB /* PLCrashAsyncCompactUnwindEncoding.c */; }; + 8064D8041C4D22D8005A8B4C /* PLCrashAsyncDwarfPrimitives.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 05E7484C175E5349009B8745 /* PLCrashAsyncDwarfPrimitives.cpp */; }; + 8064D8051C4D22D8005A8B4C /* PLCrashAsyncDwarfFDE.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 05E7485E1760D64D009B8745 /* PLCrashAsyncDwarfFDE.cpp */; }; + 8064D8061C4D22D8005A8B4C /* PLCrashAsyncDwarfCIE.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 05E748661760D890009B8745 /* PLCrashAsyncDwarfCIE.cpp */; }; + 8064D8071C4D22D8005A8B4C /* PLCrashAsyncDwarfCFAStateEvaluation.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 05E7487A176118C1009B8745 /* PLCrashAsyncDwarfCFAStateEvaluation.cpp */; }; + 8064D8081C4D22D8005A8B4C /* PLCrashAsyncDwarfExpression.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 05E7488A176135CE009B8745 /* PLCrashAsyncDwarfExpression.cpp */; }; + 8064D80A1C4D22D8005A8B4C /* dwarf_opstream.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 05C76DA4176B8C7000E9B10D /* dwarf_opstream.cpp */; }; + 8064D80B1C4D22D8005A8B4C /* PLCrashAsyncDwarfCFAState.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 05C76DC6176FBAF300E9B10D /* PLCrashAsyncDwarfCFAState.cpp */; }; + 8064D80C1C4D22D8005A8B4C /* PLCrashFrameDWARFUnwind.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 05920D1E177B9257001E8975 /* PLCrashFrameDWARFUnwind.cpp */; }; + 8064D80D1C4D22D8005A8B4C /* PLCrashProcessInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05102E1517B0151000B5D925 /* PLCrashProcessInfo.m */; }; + 8064D80E1C4D22D8005A8B4C /* PLCrashHostInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 05102E2317B2B80A00B5D925 /* PLCrashHostInfo.m */; }; + 8064D80F1C4D22D8005A8B4C /* PLCrashMachExceptionPort.m in Sources */ = {isa = PBXBuildFile; fileRef = 051F067A17B6B0D4006D0EFA /* PLCrashMachExceptionPort.m */; }; + 8064D8101C4D22D8005A8B4C /* PLCrashMachExceptionPortSet.m in Sources */ = {isa = PBXBuildFile; fileRef = 05BEC41617BAF92A0082CBFB /* PLCrashMachExceptionPortSet.m */; }; + 8064D8111C4D22D8005A8B4C /* PLCrashAsyncMachExceptionInfo.c in Sources */ = {isa = PBXBuildFile; fileRef = 05BEC42517BD4F290082CBFB /* PLCrashAsyncMachExceptionInfo.c */; }; + 8064D8121C4D22D8005A8B4C /* PLCrashReporterConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 05BEC43517BF1CB10082CBFB /* PLCrashReporterConfig.m */; }; + 8064D8141C4D22D8005A8B4C /* PLCrashUncaughtExceptionHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 05B929E717C9336600B051E3 /* PLCrashUncaughtExceptionHandler.m */; }; + 8064D8151C4D22D8005A8B4C /* PLCrashReportMachExceptionInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 0513E23317D15ED400727919 /* PLCrashReportMachExceptionInfo.m */; }; + 8064D8911C4D22E5005A8B4C /* CrashReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = 05CD31890EE93A90000FDE88 /* CrashReporter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D8921C4D22E5005A8B4C /* PLCrashNamespace.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A2077215AB30C9001E3EFC /* PLCrashNamespace.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D8941C4D22E5005A8B4C /* PLCrashReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = 054F51070EEC73C80034B184 /* PLCrashReporter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D8981C4D22E5005A8B4C /* PLCrashReport.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F411A40EF8DA31008050CF /* PLCrashReport.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D8991C4D22E5005A8B4C /* PLCrashReportApplicationInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F4141C0EF9A6C4008050CF /* PLCrashReportApplicationInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D89A1C4D22E5005A8B4C /* PLCrashReportRegisterInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05D9E54E16765A0200B39833 /* PLCrashReportRegisterInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D89B1C4D22E5005A8B4C /* PLCrashReportBinaryImageInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F4150B0EF9DD9B008050CF /* PLCrashReportBinaryImageInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D89C1C4D22E5005A8B4C /* PLCrashReportStackFrameInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05D9E5431676598200B39833 /* PLCrashReportStackFrameInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D89D1C4D22E5005A8B4C /* PLCrashReportExceptionInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F415510EF9E078008050CF /* PLCrashReportExceptionInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D89E1C4D22E5005A8B4C /* PLCrashReportThreadInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F414800EF9BFAC008050CF /* PLCrashReportThreadInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D89F1C4D22E5005A8B4C /* PLCrashReportSystemInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F413430EF995C0008050CF /* PLCrashReportSystemInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D8A11C4D22E5005A8B4C /* PLCrashReportSymbolInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05D9E55916765D0200B39833 /* PLCrashReportSymbolInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D8A21C4D22E5005A8B4C /* PLCrashReportProcessInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 2D0E10441141F7DC00CE1BD6 /* PLCrashReportProcessInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D8A31C4D22E5005A8B4C /* PLCrashReportSignalInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05E734F50EFAE59C005EDFB7 /* PLCrashReportSignalInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D8A41C4D22E5005A8B4C /* PLCrashFeatureConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A2B3FF1795BA4100934198 /* PLCrashFeatureConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D8A51C4D22E5005A8B4C /* PLCrashReporterConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BEC43417BF1CB10082CBFB /* PLCrashReporterConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D8A61C4D22E5005A8B4C /* PLCrashReportMachExceptionInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 0513E23217D15ED400727919 /* PLCrashReportMachExceptionInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D8A71C4D22E5005A8B4C /* PLCrashMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A5E28017A82751008A75E5 /* PLCrashMacros.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D8A81C4D22E5005A8B4C /* PLCrashReportTextFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 054627A711D998BB007891C7 /* PLCrashReportTextFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D8A91C4D22E5005A8B4C /* PLCrashReportFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 054627B811D99D06007891C7 /* PLCrashReportFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D8AA1C4D22E5005A8B4C /* PLCrashReportMachineInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BB83EF1364AD3E00D53B84 /* PLCrashReportMachineInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D8AB1C4D22E5005A8B4C /* PLCrashReportProcessorInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BB83CB1364A77800D53B84 /* PLCrashReportProcessorInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8064D92F1C4D27E2005A8B4C /* Tests in Resources */ = {isa = PBXBuildFile; fileRef = 05F3CD6C16DE7625007911FB /* Tests */; }; + 80A63BD81C4D32FB0073B7A3 /* libCrashReporter.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8064D81B1C4D22D8005A8B4C /* libCrashReporter.a */; }; + C202F6482451C4FE00754DF7 /* libCrashReporter.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 05E731F30EFA1AAB005EDFB7 /* libCrashReporter.a */; }; + C2198DD91640188C006EB46A /* PLCrashAsyncObjCSection.mm in Sources */ = {isa = PBXBuildFile; fileRef = C2198DD81640188C006EB46A /* PLCrashAsyncObjCSection.mm */; }; + C2198DDB1640188C006EB46A /* PLCrashAsyncObjCSection.mm in Sources */ = {isa = PBXBuildFile; fileRef = C2198DD81640188C006EB46A /* PLCrashAsyncObjCSection.mm */; }; + C2198E0616441CF5006EB46A /* PLCrashAsyncMachOString.c in Sources */ = {isa = PBXBuildFile; fileRef = C2198E0516441CF5006EB46A /* PLCrashAsyncMachOString.c */; }; + C2198E0816441CF5006EB46A /* PLCrashAsyncMachOString.c in Sources */ = {isa = PBXBuildFile; fileRef = C2198E0516441CF5006EB46A /* PLCrashAsyncMachOString.c */; }; + C238788524574C0100519007 /* libCrashReporter.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 05E731F30EFA1AAB005EDFB7 /* libCrashReporter.a */; }; + C238788624574C0700519007 /* libCrashReporter.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 05E731F30EFA1AAB005EDFB7 /* libCrashReporter.a */; }; + C26022861642FCA6007FC29F /* PLCrashAsyncSymbolication.c in Sources */ = {isa = PBXBuildFile; fileRef = C26022851642FCA6007FC29F /* PLCrashAsyncSymbolication.c */; }; + C26022881642FCA6007FC29F /* PLCrashAsyncSymbolication.c in Sources */ = {isa = PBXBuildFile; fileRef = C26022851642FCA6007FC29F /* PLCrashAsyncSymbolication.c */; }; + C26C52A12451B3E500D20162 /* libCrashReporter.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 05CD31520EE936A9000FDE88 /* libCrashReporter.a */; }; + C26C52A22451B3F900D20162 /* libCrashReporter.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 05E731F30EFA1AAB005EDFB7 /* libCrashReporter.a */; }; + C26C52A32451B41800D20162 /* libCrashReporter.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8064D81B1C4D22D8005A8B4C /* libCrashReporter.a */; }; + C29AD6C82456C69B00360AF7 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = C29AD6C32456C69000360AF7 /* main.m */; }; + C29AD6C92456C69B00360AF7 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = C29AD6C32456C69000360AF7 /* main.m */; }; + C29AD6CA2456C69C00360AF7 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = C29AD6C32456C69000360AF7 /* main.m */; }; + C29AD6CB2456C6A000360AF7 /* fuzz-main.m in Sources */ = {isa = PBXBuildFile; fileRef = C29AD6C72456C69000360AF7 /* fuzz-main.m */; }; + C29AD6CC2456C6A500360AF7 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = C29AD6C52456C69000360AF7 /* main.m */; }; + C29AD6D02456C95800360AF7 /* PLCrashTestThread.m in Sources */ = {isa = PBXBuildFile; fileRef = C29AD6CD2456C94A00360AF7 /* PLCrashTestThread.m */; }; + C29AD6D12456C95800360AF7 /* PLCrashTestThreadTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C29AD6CE2456C94A00360AF7 /* PLCrashTestThreadTests.m */; }; + C29AD6D22456C95900360AF7 /* PLCrashTestThread.m in Sources */ = {isa = PBXBuildFile; fileRef = C29AD6CD2456C94A00360AF7 /* PLCrashTestThread.m */; }; + C29AD6D32456C95900360AF7 /* PLCrashTestThreadTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C29AD6CE2456C94A00360AF7 /* PLCrashTestThreadTests.m */; }; + C29AD6D52456C95A00360AF7 /* PLCrashTestThreadTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C29AD6CE2456C94A00360AF7 /* PLCrashTestThreadTests.m */; }; + C29AD6D72456C9B800360AF7 /* PLCrashLogWriterEncodingTests.pb-c.c in Sources */ = {isa = PBXBuildFile; fileRef = C29AD6D62456C9B800360AF7 /* PLCrashLogWriterEncodingTests.pb-c.c */; }; + C29AD6D82456C9B800360AF7 /* PLCrashLogWriterEncodingTests.pb-c.c in Sources */ = {isa = PBXBuildFile; fileRef = C29AD6D62456C9B800360AF7 /* PLCrashLogWriterEncodingTests.pb-c.c */; }; + C29AD6D92456C9B800360AF7 /* PLCrashLogWriterEncodingTests.pb-c.c in Sources */ = {isa = PBXBuildFile; fileRef = C29AD6D62456C9B800360AF7 /* PLCrashLogWriterEncodingTests.pb-c.c */; }; + C29AD6DA2456CA6C00360AF7 /* PLCrashLogWriterEncodingTests.proto in Sources */ = {isa = PBXBuildFile; fileRef = 052951EE1696A461006EDA8A /* PLCrashLogWriterEncodingTests.proto */; }; + C29AD6DB2456CA7300360AF7 /* PLCrashLogWriterEncodingTests.proto in Sources */ = {isa = PBXBuildFile; fileRef = 052951EE1696A461006EDA8A /* PLCrashLogWriterEncodingTests.proto */; }; + C29AD6DC2456CA7800360AF7 /* PLCrashLogWriterEncodingTests.proto in Sources */ = {isa = PBXBuildFile; fileRef = 052951EE1696A461006EDA8A /* PLCrashLogWriterEncodingTests.proto */; }; + C2B72B0F2453496F00D03ABD /* PLCrashReport.pb-c.c in Sources */ = {isa = PBXBuildFile; fileRef = C2B72B0D2453496E00D03ABD /* PLCrashReport.pb-c.c */; }; + C2B72B102453496F00D03ABD /* PLCrashReport.pb-c.c in Sources */ = {isa = PBXBuildFile; fileRef = C2B72B0D2453496E00D03ABD /* PLCrashReport.pb-c.c */; }; + C2B72B112453496F00D03ABD /* PLCrashReport.pb-c.c in Sources */ = {isa = PBXBuildFile; fileRef = C2B72B0D2453496E00D03ABD /* PLCrashReport.pb-c.c */; }; + C2B72B122453496F00D03ABD /* PLCrashReport.pb-c.h in Headers */ = {isa = PBXBuildFile; fileRef = C2B72B0E2453496F00D03ABD /* PLCrashReport.pb-c.h */; }; + C2B72B132453496F00D03ABD /* PLCrashReport.pb-c.h in Headers */ = {isa = PBXBuildFile; fileRef = C2B72B0E2453496F00D03ABD /* PLCrashReport.pb-c.h */; }; + C2B72B142453496F00D03ABD /* PLCrashReport.pb-c.h in Headers */ = {isa = PBXBuildFile; fileRef = C2B72B0E2453496F00D03ABD /* PLCrashReport.pb-c.h */; }; + C2B72B2124534E5200D03ABD /* PLCrashReport.proto in Sources */ = {isa = PBXBuildFile; fileRef = 059670C70EEFAC3A008A0601 /* PLCrashReport.proto */; }; + C2B72B2224534E5300D03ABD /* PLCrashReport.proto in Sources */ = {isa = PBXBuildFile; fileRef = 059670C70EEFAC3A008A0601 /* PLCrashReport.proto */; }; + C2B72B2324534E5300D03ABD /* PLCrashReport.proto in Sources */ = {isa = PBXBuildFile; fileRef = 059670C70EEFAC3A008A0601 /* PLCrashReport.proto */; }; + C2B72B2624534EE700D03ABD /* protobuf-c.h in Headers */ = {isa = PBXBuildFile; fileRef = C2B72B2424534EE700D03ABD /* protobuf-c.h */; }; + C2B72B2724534EE700D03ABD /* protobuf-c.h in Headers */ = {isa = PBXBuildFile; fileRef = C2B72B2424534EE700D03ABD /* protobuf-c.h */; }; + C2B72B2824534EE700D03ABD /* protobuf-c.h in Headers */ = {isa = PBXBuildFile; fileRef = C2B72B2424534EE700D03ABD /* protobuf-c.h */; }; + C2B72B2924534EE700D03ABD /* protobuf-c.c in Sources */ = {isa = PBXBuildFile; fileRef = C2B72B2524534EE700D03ABD /* protobuf-c.c */; settings = {COMPILER_FLAGS = "-Wno-shorten-64-to-32"; }; }; + C2B72B2A24534EE700D03ABD /* protobuf-c.c in Sources */ = {isa = PBXBuildFile; fileRef = C2B72B2524534EE700D03ABD /* protobuf-c.c */; settings = {COMPILER_FLAGS = "-Wno-shorten-64-to-32"; }; }; + C2B72B2B24534EE700D03ABD /* protobuf-c.c in Sources */ = {isa = PBXBuildFile; fileRef = C2B72B2524534EE700D03ABD /* protobuf-c.c */; settings = {COMPILER_FLAGS = "-Wno-shorten-64-to-32"; }; }; + C2BBCD9A2456E09000F9E820 /* PLCrashTestThread.m in Sources */ = {isa = PBXBuildFile; fileRef = C29AD6CD2456C94A00360AF7 /* PLCrashTestThread.m */; }; + C2BBCD9B2456E0E700F9E820 /* PLCrashAsyncDwarfEncodingTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = C2BBCD7F2456E03D00F9E820 /* PLCrashAsyncDwarfEncodingTests.mm */; }; + C2BBCD9C2456E0E700F9E820 /* PLCrashAsyncLinkedListTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = C2BBCD822456E03D00F9E820 /* PLCrashAsyncLinkedListTests.mm */; }; + C2BBCD9D2456E0E700F9E820 /* PLCrashAsyncMachOStringTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C2BBCD842456E03D00F9E820 /* PLCrashAsyncMachOStringTests.m */; }; + C2BBCD9E2456E0E700F9E820 /* PLCrashFrameStackUnwindTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C2BBCD812456E03D00F9E820 /* PLCrashFrameStackUnwindTests.m */; }; + C2BBCD9F2456E0E700F9E820 /* PLCrashMachExceptionPortTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C2BBCD7E2456E03D00F9E820 /* PLCrashMachExceptionPortTests.m */; }; + C2BBCDA02456E0E700F9E820 /* PLCrashMachExceptionServerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C2BBCD802456E03D00F9E820 /* PLCrashMachExceptionServerTests.m */; }; + C2BBCDA12456E0E700F9E820 /* PLCrashSysctlTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C2BBCD832456E03D00F9E820 /* PLCrashSysctlTests.m */; }; + C2BBCDA22456E0E800F9E820 /* PLCrashAsyncDwarfEncodingTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = C2BBCD7F2456E03D00F9E820 /* PLCrashAsyncDwarfEncodingTests.mm */; }; + C2BBCDA32456E0E800F9E820 /* PLCrashAsyncLinkedListTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = C2BBCD822456E03D00F9E820 /* PLCrashAsyncLinkedListTests.mm */; }; + C2BBCDA42456E0E800F9E820 /* PLCrashAsyncMachOStringTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C2BBCD842456E03D00F9E820 /* PLCrashAsyncMachOStringTests.m */; }; + C2BBCDA52456E0E800F9E820 /* PLCrashFrameStackUnwindTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C2BBCD812456E03D00F9E820 /* PLCrashFrameStackUnwindTests.m */; }; + C2BBCDA62456E0E800F9E820 /* PLCrashMachExceptionPortTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C2BBCD7E2456E03D00F9E820 /* PLCrashMachExceptionPortTests.m */; }; + C2BBCDA72456E0E800F9E820 /* PLCrashMachExceptionServerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C2BBCD802456E03D00F9E820 /* PLCrashMachExceptionServerTests.m */; }; + C2BBCDA82456E0E800F9E820 /* PLCrashSysctlTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C2BBCD832456E03D00F9E820 /* PLCrashSysctlTests.m */; }; + C2BBCDA92456E0E800F9E820 /* PLCrashAsyncDwarfEncodingTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = C2BBCD7F2456E03D00F9E820 /* PLCrashAsyncDwarfEncodingTests.mm */; }; + C2BBCDAA2456E0E800F9E820 /* PLCrashAsyncLinkedListTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = C2BBCD822456E03D00F9E820 /* PLCrashAsyncLinkedListTests.mm */; }; + C2BBCDAB2456E0E800F9E820 /* PLCrashAsyncMachOStringTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C2BBCD842456E03D00F9E820 /* PLCrashAsyncMachOStringTests.m */; }; + C2BBCDAC2456E0E800F9E820 /* PLCrashFrameStackUnwindTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C2BBCD812456E03D00F9E820 /* PLCrashFrameStackUnwindTests.m */; }; + C2BBCDAD2456E0E800F9E820 /* PLCrashMachExceptionPortTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C2BBCD7E2456E03D00F9E820 /* PLCrashMachExceptionPortTests.m */; }; + C2BBCDAE2456E0E800F9E820 /* PLCrashMachExceptionServerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C2BBCD802456E03D00F9E820 /* PLCrashMachExceptionServerTests.m */; }; + C2BBCDAF2456E0E800F9E820 /* PLCrashSysctlTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C2BBCD832456E03D00F9E820 /* PLCrashSysctlTests.m */; }; + C2CBA40724586975001B775F /* CrashReporter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8DC2EF5B0486A6940098B216 /* CrashReporter.framework */; }; + C2CBA40824586975001B775F /* CrashReporter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8DC2EF5B0486A6940098B216 /* CrashReporter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + C2DCD88124658A63007322C5 /* mach_exc.defs in Sources */ = {isa = PBXBuildFile; fileRef = 0581B520168FDB280098C103 /* mach_exc.defs */; }; + C2DCD88224658A63007322C5 /* mach_exc.defs in Sources */ = {isa = PBXBuildFile; fileRef = 0581B520168FDB280098C103 /* mach_exc.defs */; platformFilter = maccatalyst; }; + C2F0ACA024AB7C28004890EC /* CrashReporterFramework.m in Sources */ = {isa = PBXBuildFile; fileRef = C2F0AC9F24AB7C28004890EC /* CrashReporterFramework.m */; }; + C2F0ACA124AB7C28004890EC /* CrashReporterFramework.m in Sources */ = {isa = PBXBuildFile; fileRef = C2F0AC9F24AB7C28004890EC /* CrashReporterFramework.m */; }; + C2F0ACA224AB7C28004890EC /* CrashReporterFramework.m in Sources */ = {isa = PBXBuildFile; fileRef = C2F0AC9F24AB7C28004890EC /* CrashReporterFramework.m */; }; + C2F7F1692451EB2D002BD8BF /* PLCrashAsyncThread_arm.c in Sources */ = {isa = PBXBuildFile; fileRef = 05A17DF516DBD0C200888448 /* PLCrashAsyncThread_arm.c */; }; + C2F7F16A2451EBFF002BD8BF /* PLCrashReporterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F40ADD0EF73A39008050CF /* PLCrashReporterTests.m */; }; + C2F7F16B2451EBFF002BD8BF /* PLCrashReportTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F411AC0EF8DE68008050CF /* PLCrashReportTests.m */; }; + C2F7F16E2451EC00002BD8BF /* PLCrashMachExceptionPortSetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05BEC41F17BAF95C0082CBFB /* PLCrashMachExceptionPortSetTests.m */; }; + C2F7F1702451EC00002BD8BF /* PLCrashUncaughtExceptionHandlerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05B929F017C9337D00B051E3 /* PLCrashUncaughtExceptionHandlerTests.m */; }; + C2F7F1712451EC00002BD8BF /* PLCrashAsyncTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05CD36480EF247A9000FDE88 /* PLCrashAsyncTests.m */; }; + C2F7F1722451EC00002BD8BF /* PLCrashAsyncSignalInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E734830EFAD83B005EDFB7 /* PLCrashAsyncSignalInfoTests.m */; }; + C2F7F1732451EC00002BD8BF /* PLCrashAsyncMachExceptionInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05BEC43017BD4F540082CBFB /* PLCrashAsyncMachExceptionInfoTests.m */; }; + C2F7F1742451EC00002BD8BF /* PLCrashAsyncImageListTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 052A46F713637DE000987004 /* PLCrashAsyncImageListTests.m */; }; + C2F7F1752451EC00002BD8BF /* PLCrashAsyncThreadTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A17DD216D8080A00888448 /* PLCrashAsyncThreadTests.m */; }; + C2F7F1772451EC00002BD8BF /* PLCrashAsyncMObjectTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05DEE64A1636E721007E99DC /* PLCrashAsyncMObjectTests.m */; }; + C2F7F1782451EC00002BD8BF /* PLCrashAsyncSymbolicationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C260228F1642FE9B007FC29F /* PLCrashAsyncSymbolicationTests.m */; }; + C2F7F1792451EC00002BD8BF /* PLCrashAsyncMachOImageTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F76DD9162F238E00A668C7 /* PLCrashAsyncMachOImageTests.m */; }; + C2F7F17B2451EC00002BD8BF /* PLCrashAsyncObjCSectionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C2198DE316402B8A006EB46A /* PLCrashAsyncObjCSectionTests.m */; }; + C2F7F17D2451EC00002BD8BF /* dwarf_stack_tests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05E748B217616D6B009B8745 /* dwarf_stack_tests.mm */; }; + C2F7F17E2451EC00002BD8BF /* dwarf_opstream_tests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05C76DB1176B946E00E9B10D /* dwarf_opstream_tests.mm */; }; + C2F7F17F2451EC00002BD8BF /* PLCrashAsyncDwarfCIETests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05E748711760DBBE009B8745 /* PLCrashAsyncDwarfCIETests.mm */; }; + C2F7F1802451EC00002BD8BF /* PLCrashAsyncDwarfFDETests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05E748751760DBD0009B8745 /* PLCrashAsyncDwarfFDETests.mm */; }; + C2F7F1812451EC00002BD8BF /* PLCrashAsyncDwarfPrimitivesTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05E74855175E5370009B8745 /* PLCrashAsyncDwarfPrimitivesTests.mm */; }; + C2F7F1822451EC00002BD8BF /* PLCrashAsyncDwarfCFAStateTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05C76DD3176FBC1E00E9B10D /* PLCrashAsyncDwarfCFAStateTests.mm */; }; + C2F7F1832451EC00002BD8BF /* PLCrashAsyncDwarfCFAStateEvaluationTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05E74885176118F8009B8745 /* PLCrashAsyncDwarfCFAStateEvaluationTests.mm */; }; + C2F7F1842451EC00002BD8BF /* PLCrashAsyncDwarfExpressionTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05E7489417613AF0009B8745 /* PLCrashAsyncDwarfExpressionTests.mm */; }; + C2F7F1852451EC00002BD8BF /* PLCrashAsyncCompactUnwindEncodingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F3CD8016DFC78D007911FB /* PLCrashAsyncCompactUnwindEncodingTests.m */; }; + C2F7F1872451EC00002BD8BF /* PLCrashHostInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05102E2C17B2B82000B5D925 /* PLCrashHostInfoTests.m */; }; + C2F7F1882451EC00002BD8BF /* PLCrashProcessInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05102E1C17B0152B00B5D925 /* PLCrashProcessInfoTests.m */; }; + C2F7F1892451EC00002BD8BF /* PLCrashLogWriterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0596702D0EEF6B51008A0601 /* PLCrashLogWriterTests.m */; }; + C2F7F18A2451EC00002BD8BF /* PLCrashLogWriterEncodingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 052951E91696965E006EDA8A /* PLCrashLogWriterEncodingTests.m */; }; + C2F7F18D2451EC00002BD8BF /* PLCrashFrameWalkerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 059666E20EEDDFCC008A0601 /* PLCrashFrameWalkerTests.m */; }; + C2F7F18F2451EC00002BD8BF /* PLCrashFrameDWARFUnwindTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05920D29177B92E7001E8975 /* PLCrashFrameDWARFUnwindTests.m */; }; + C2F7F1902451EC00002BD8BF /* PLCrashFrameCompactUnwindTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F3CD6816DD6A7A007911FB /* PLCrashFrameCompactUnwindTests.m */; }; + C2F7F1912451EC00002BD8BF /* unwind_test_harness.c in Sources */ = {isa = PBXBuildFile; fileRef = 05507A0F177CC456009D5168 /* unwind_test_harness.c */; }; + C2F7F1A22451EC00002BD8BF /* PLCrashReporterNSErrorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05EB2B1B15B6FE280066EB4D /* PLCrashReporterNSErrorTests.m */; }; + C2F7F1A32451EC00002BD8BF /* PLCrashTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 05659DF8174D2E1200D2EE21 /* PLCrashTestCase.m */; }; + C2F7F1A42451EC00002BD8BF /* PLCrashReporterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F40ADD0EF73A39008050CF /* PLCrashReporterTests.m */; }; + C2F7F1A52451EC00002BD8BF /* PLCrashReportTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F411AC0EF8DE68008050CF /* PLCrashReportTests.m */; }; + C2F7F1A82451EC00002BD8BF /* PLCrashMachExceptionPortSetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05BEC41F17BAF95C0082CBFB /* PLCrashMachExceptionPortSetTests.m */; }; + C2F7F1A92451EC00002BD8BF /* PLCrashSignalHandlerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05CD33A20EE94931000FDE88 /* PLCrashSignalHandlerTests.m */; }; + C2F7F1AA2451EC00002BD8BF /* PLCrashUncaughtExceptionHandlerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05B929F017C9337D00B051E3 /* PLCrashUncaughtExceptionHandlerTests.m */; }; + C2F7F1AB2451EC00002BD8BF /* PLCrashAsyncTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05CD36480EF247A9000FDE88 /* PLCrashAsyncTests.m */; }; + C2F7F1AC2451EC00002BD8BF /* PLCrashAsyncSignalInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E734830EFAD83B005EDFB7 /* PLCrashAsyncSignalInfoTests.m */; }; + C2F7F1AD2451EC00002BD8BF /* PLCrashAsyncMachExceptionInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05BEC43017BD4F540082CBFB /* PLCrashAsyncMachExceptionInfoTests.m */; }; + C2F7F1AE2451EC00002BD8BF /* PLCrashAsyncImageListTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 052A46F713637DE000987004 /* PLCrashAsyncImageListTests.m */; }; + C2F7F1AF2451EC00002BD8BF /* PLCrashAsyncThreadTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A17DD216D8080A00888448 /* PLCrashAsyncThreadTests.m */; }; + C2F7F1B12451EC00002BD8BF /* PLCrashAsyncMObjectTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05DEE64A1636E721007E99DC /* PLCrashAsyncMObjectTests.m */; }; + C2F7F1B22451EC00002BD8BF /* PLCrashAsyncSymbolicationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C260228F1642FE9B007FC29F /* PLCrashAsyncSymbolicationTests.m */; }; + C2F7F1B32451EC00002BD8BF /* PLCrashAsyncMachOImageTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F76DD9162F238E00A668C7 /* PLCrashAsyncMachOImageTests.m */; }; + C2F7F1B52451EC00002BD8BF /* PLCrashAsyncObjCSectionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C2198DE316402B8A006EB46A /* PLCrashAsyncObjCSectionTests.m */; }; + C2F7F1B72451EC00002BD8BF /* dwarf_stack_tests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05E748B217616D6B009B8745 /* dwarf_stack_tests.mm */; }; + C2F7F1B82451EC00002BD8BF /* dwarf_opstream_tests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05C76DB1176B946E00E9B10D /* dwarf_opstream_tests.mm */; }; + C2F7F1B92451EC00002BD8BF /* PLCrashAsyncDwarfCIETests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05E748711760DBBE009B8745 /* PLCrashAsyncDwarfCIETests.mm */; }; + C2F7F1BA2451EC00002BD8BF /* PLCrashAsyncDwarfFDETests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05E748751760DBD0009B8745 /* PLCrashAsyncDwarfFDETests.mm */; }; + C2F7F1BB2451EC00002BD8BF /* PLCrashAsyncDwarfPrimitivesTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05E74855175E5370009B8745 /* PLCrashAsyncDwarfPrimitivesTests.mm */; }; + C2F7F1BC2451EC00002BD8BF /* PLCrashAsyncDwarfCFAStateTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05C76DD3176FBC1E00E9B10D /* PLCrashAsyncDwarfCFAStateTests.mm */; }; + C2F7F1BD2451EC00002BD8BF /* PLCrashAsyncDwarfCFAStateEvaluationTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05E74885176118F8009B8745 /* PLCrashAsyncDwarfCFAStateEvaluationTests.mm */; }; + C2F7F1BE2451EC00002BD8BF /* PLCrashAsyncDwarfExpressionTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05E7489417613AF0009B8745 /* PLCrashAsyncDwarfExpressionTests.mm */; }; + C2F7F1BF2451EC00002BD8BF /* PLCrashAsyncCompactUnwindEncodingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F3CD8016DFC78D007911FB /* PLCrashAsyncCompactUnwindEncodingTests.m */; }; + C2F7F1C12451EC00002BD8BF /* PLCrashHostInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05102E2C17B2B82000B5D925 /* PLCrashHostInfoTests.m */; }; + C2F7F1C22451EC00002BD8BF /* PLCrashProcessInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05102E1C17B0152B00B5D925 /* PLCrashProcessInfoTests.m */; }; + C2F7F1C32451EC00002BD8BF /* PLCrashLogWriterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0596702D0EEF6B51008A0601 /* PLCrashLogWriterTests.m */; }; + C2F7F1C42451EC00002BD8BF /* PLCrashLogWriterEncodingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 052951E91696965E006EDA8A /* PLCrashLogWriterEncodingTests.m */; }; + C2F7F1C72451EC00002BD8BF /* PLCrashFrameWalkerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 059666E20EEDDFCC008A0601 /* PLCrashFrameWalkerTests.m */; }; + C2F7F1C92451EC00002BD8BF /* PLCrashFrameDWARFUnwindTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05920D29177B92E7001E8975 /* PLCrashFrameDWARFUnwindTests.m */; }; + C2F7F1CA2451EC00002BD8BF /* PLCrashFrameCompactUnwindTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F3CD6816DD6A7A007911FB /* PLCrashFrameCompactUnwindTests.m */; }; + C2F7F1CB2451EC00002BD8BF /* unwind_test_harness.c in Sources */ = {isa = PBXBuildFile; fileRef = 05507A0F177CC456009D5168 /* unwind_test_harness.c */; }; + C2F7F1DC2451EC00002BD8BF /* PLCrashReporterNSErrorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05EB2B1B15B6FE280066EB4D /* PLCrashReporterNSErrorTests.m */; }; + C2F7F1DD2451EC00002BD8BF /* PLCrashTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 05659DF8174D2E1200D2EE21 /* PLCrashTestCase.m */; }; + C2F7F1DE2451EC01002BD8BF /* PLCrashReporterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F40ADD0EF73A39008050CF /* PLCrashReporterTests.m */; }; + C2F7F1DF2451EC01002BD8BF /* PLCrashReportTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F411AC0EF8DE68008050CF /* PLCrashReportTests.m */; }; + C2F7F1E22451EC01002BD8BF /* PLCrashMachExceptionPortSetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05BEC41F17BAF95C0082CBFB /* PLCrashMachExceptionPortSetTests.m */; }; + C2F7F1E32451EC01002BD8BF /* PLCrashSignalHandlerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05CD33A20EE94931000FDE88 /* PLCrashSignalHandlerTests.m */; }; + C2F7F1E42451EC01002BD8BF /* PLCrashUncaughtExceptionHandlerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05B929F017C9337D00B051E3 /* PLCrashUncaughtExceptionHandlerTests.m */; }; + C2F7F1E52451EC01002BD8BF /* PLCrashAsyncTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05CD36480EF247A9000FDE88 /* PLCrashAsyncTests.m */; }; + C2F7F1E62451EC01002BD8BF /* PLCrashAsyncSignalInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05E734830EFAD83B005EDFB7 /* PLCrashAsyncSignalInfoTests.m */; }; + C2F7F1E72451EC01002BD8BF /* PLCrashAsyncMachExceptionInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05BEC43017BD4F540082CBFB /* PLCrashAsyncMachExceptionInfoTests.m */; }; + C2F7F1E82451EC01002BD8BF /* PLCrashAsyncImageListTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 052A46F713637DE000987004 /* PLCrashAsyncImageListTests.m */; }; + C2F7F1E92451EC01002BD8BF /* PLCrashAsyncThreadTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05A17DD216D8080A00888448 /* PLCrashAsyncThreadTests.m */; }; + C2F7F1EB2451EC01002BD8BF /* PLCrashAsyncMObjectTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05DEE64A1636E721007E99DC /* PLCrashAsyncMObjectTests.m */; }; + C2F7F1EC2451EC01002BD8BF /* PLCrashAsyncSymbolicationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C260228F1642FE9B007FC29F /* PLCrashAsyncSymbolicationTests.m */; }; + C2F7F1ED2451EC01002BD8BF /* PLCrashAsyncMachOImageTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F76DD9162F238E00A668C7 /* PLCrashAsyncMachOImageTests.m */; }; + C2F7F1EF2451EC01002BD8BF /* PLCrashAsyncObjCSectionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C2198DE316402B8A006EB46A /* PLCrashAsyncObjCSectionTests.m */; }; + C2F7F1F12451EC01002BD8BF /* dwarf_stack_tests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05E748B217616D6B009B8745 /* dwarf_stack_tests.mm */; }; + C2F7F1F22451EC01002BD8BF /* dwarf_opstream_tests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05C76DB1176B946E00E9B10D /* dwarf_opstream_tests.mm */; }; + C2F7F1F32451EC01002BD8BF /* PLCrashAsyncDwarfCIETests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05E748711760DBBE009B8745 /* PLCrashAsyncDwarfCIETests.mm */; }; + C2F7F1F42451EC01002BD8BF /* PLCrashAsyncDwarfFDETests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05E748751760DBD0009B8745 /* PLCrashAsyncDwarfFDETests.mm */; }; + C2F7F1F52451EC01002BD8BF /* PLCrashAsyncDwarfPrimitivesTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05E74855175E5370009B8745 /* PLCrashAsyncDwarfPrimitivesTests.mm */; }; + C2F7F1F62451EC01002BD8BF /* PLCrashAsyncDwarfCFAStateTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05C76DD3176FBC1E00E9B10D /* PLCrashAsyncDwarfCFAStateTests.mm */; }; + C2F7F1F72451EC01002BD8BF /* PLCrashAsyncDwarfCFAStateEvaluationTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05E74885176118F8009B8745 /* PLCrashAsyncDwarfCFAStateEvaluationTests.mm */; }; + C2F7F1F82451EC01002BD8BF /* PLCrashAsyncDwarfExpressionTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05E7489417613AF0009B8745 /* PLCrashAsyncDwarfExpressionTests.mm */; }; + C2F7F1F92451EC01002BD8BF /* PLCrashAsyncCompactUnwindEncodingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F3CD8016DFC78D007911FB /* PLCrashAsyncCompactUnwindEncodingTests.m */; }; + C2F7F1FB2451EC01002BD8BF /* PLCrashHostInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05102E2C17B2B82000B5D925 /* PLCrashHostInfoTests.m */; }; + C2F7F1FC2451EC01002BD8BF /* PLCrashProcessInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05102E1C17B0152B00B5D925 /* PLCrashProcessInfoTests.m */; }; + C2F7F1FD2451EC01002BD8BF /* PLCrashLogWriterTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0596702D0EEF6B51008A0601 /* PLCrashLogWriterTests.m */; }; + C2F7F1FE2451EC01002BD8BF /* PLCrashLogWriterEncodingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 052951E91696965E006EDA8A /* PLCrashLogWriterEncodingTests.m */; }; + C2F7F2012451EC01002BD8BF /* PLCrashFrameWalkerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 059666E20EEDDFCC008A0601 /* PLCrashFrameWalkerTests.m */; }; + C2F7F2032451EC01002BD8BF /* PLCrashFrameDWARFUnwindTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05920D29177B92E7001E8975 /* PLCrashFrameDWARFUnwindTests.m */; }; + C2F7F2042451EC01002BD8BF /* PLCrashFrameCompactUnwindTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05F3CD6816DD6A7A007911FB /* PLCrashFrameCompactUnwindTests.m */; }; + C2F7F2052451EC01002BD8BF /* unwind_test_harness.c in Sources */ = {isa = PBXBuildFile; fileRef = 05507A0F177CC456009D5168 /* unwind_test_harness.c */; }; + C2F7F2162451EC01002BD8BF /* PLCrashReporterNSErrorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 05EB2B1B15B6FE280066EB4D /* PLCrashReporterNSErrorTests.m */; }; + C2F7F2172451EC01002BD8BF /* PLCrashTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 05659DF8174D2E1200D2EE21 /* PLCrashTestCase.m */; }; + C2F7F2312451F155002BD8BF /* unwind_test_x86_unusual.S in Sources */ = {isa = PBXBuildFile; fileRef = 05C588151788F3E700BA118D /* unwind_test_x86_unusual.S */; }; + C2F7F2322451F156002BD8BF /* unwind_test_x86_unusual.S in Sources */ = {isa = PBXBuildFile; fileRef = 05C588151788F3E700BA118D /* unwind_test_x86_unusual.S */; }; + C2F7F2332451F157002BD8BF /* unwind_test_x86_unusual.S in Sources */ = {isa = PBXBuildFile; fileRef = 05C588151788F3E700BA118D /* unwind_test_x86_unusual.S */; }; + C2F7F2342451F167002BD8BF /* unwind_test_x86.S in Sources */ = {isa = PBXBuildFile; fileRef = 05507A13177CC4D5009D5168 /* unwind_test_x86.S */; }; + C2F7F2352451F167002BD8BF /* unwind_test_x86_64.S in Sources */ = {isa = PBXBuildFile; fileRef = 05507A17177CC50B009D5168 /* unwind_test_x86_64.S */; }; + C2F7F2362451F167002BD8BF /* unwind_test_arm.S in Sources */ = {isa = PBXBuildFile; fileRef = 05920D35178B310A001E8975 /* unwind_test_arm.S */; }; + C2F7F2372451F167002BD8BF /* unwind_test_arm64.S in Sources */ = {isa = PBXBuildFile; fileRef = 053347A517E161CB00C52E50 /* unwind_test_arm64.S */; }; + C2F7F2382451F167002BD8BF /* unwind_test_arm64_frame.S in Sources */ = {isa = PBXBuildFile; fileRef = 05BB3E1617FA043C00F464E9 /* unwind_test_arm64_frame.S */; }; + C2F7F2392451F167002BD8BF /* unwind_test_arm64_frameless.S in Sources */ = {isa = PBXBuildFile; fileRef = 058484AD1804841100A56049 /* unwind_test_arm64_frameless.S */; }; + C2F7F23A2451F167002BD8BF /* unwind_test_x86_64_disable_compact_frame.S in Sources */ = {isa = PBXBuildFile; fileRef = 05507A1B177CC912009D5168 /* unwind_test_x86_64_disable_compact_frame.S */; }; + C2F7F23B2451F167002BD8BF /* unwind_test_x86_64_frame.S in Sources */ = {isa = PBXBuildFile; fileRef = 05507A3E178364E8009D5168 /* unwind_test_x86_64_frame.S */; }; + C2F7F23C2451F167002BD8BF /* unwind_test_x86_64_frameless.S in Sources */ = {isa = PBXBuildFile; fileRef = 05920D2D17848B85001E8975 /* unwind_test_x86_64_frameless.S */; }; + C2F7F23D2451F167002BD8BF /* unwind_test_x86_64_frameless_big.S in Sources */ = {isa = PBXBuildFile; fileRef = 05920D311784C806001E8975 /* unwind_test_x86_64_frameless_big.S */; }; + C2F7F23E2451F167002BD8BF /* unwind_test_x86_64_unusual.S in Sources */ = {isa = PBXBuildFile; fileRef = 05507A4E1784DA8A009D5168 /* unwind_test_x86_64_unusual.S */; }; + C2F7F23F2451F167002BD8BF /* unwind_test_x86_disable_compact_frame.S in Sources */ = {isa = PBXBuildFile; fileRef = 05C588191788F48400BA118D /* unwind_test_x86_disable_compact_frame.S */; }; + C2F7F2402451F167002BD8BF /* unwind_test_x86_frame.S in Sources */ = {isa = PBXBuildFile; fileRef = 05507A521784DEE4009D5168 /* unwind_test_x86_frame.S */; }; + C2F7F2412451F167002BD8BF /* unwind_test_x86_frameless.S in Sources */ = {isa = PBXBuildFile; fileRef = 05C5880D1788CAA400BA118D /* unwind_test_x86_frameless.S */; }; + C2F7F2422451F167002BD8BF /* unwind_test_x86_frameless_big.S in Sources */ = {isa = PBXBuildFile; fileRef = 05C588111788F36800BA118D /* unwind_test_x86_frameless_big.S */; }; + C2F7F2432451F168002BD8BF /* unwind_test_x86.S in Sources */ = {isa = PBXBuildFile; fileRef = 05507A13177CC4D5009D5168 /* unwind_test_x86.S */; }; + C2F7F2442451F168002BD8BF /* unwind_test_x86_64.S in Sources */ = {isa = PBXBuildFile; fileRef = 05507A17177CC50B009D5168 /* unwind_test_x86_64.S */; }; + C2F7F2452451F168002BD8BF /* unwind_test_arm.S in Sources */ = {isa = PBXBuildFile; fileRef = 05920D35178B310A001E8975 /* unwind_test_arm.S */; }; + C2F7F2462451F168002BD8BF /* unwind_test_arm64.S in Sources */ = {isa = PBXBuildFile; fileRef = 053347A517E161CB00C52E50 /* unwind_test_arm64.S */; }; + C2F7F2472451F168002BD8BF /* unwind_test_arm64_frame.S in Sources */ = {isa = PBXBuildFile; fileRef = 05BB3E1617FA043C00F464E9 /* unwind_test_arm64_frame.S */; }; + C2F7F2482451F168002BD8BF /* unwind_test_arm64_frameless.S in Sources */ = {isa = PBXBuildFile; fileRef = 058484AD1804841100A56049 /* unwind_test_arm64_frameless.S */; }; + C2F7F2492451F168002BD8BF /* unwind_test_x86_64_disable_compact_frame.S in Sources */ = {isa = PBXBuildFile; fileRef = 05507A1B177CC912009D5168 /* unwind_test_x86_64_disable_compact_frame.S */; }; + C2F7F24A2451F168002BD8BF /* unwind_test_x86_64_frame.S in Sources */ = {isa = PBXBuildFile; fileRef = 05507A3E178364E8009D5168 /* unwind_test_x86_64_frame.S */; }; + C2F7F24B2451F168002BD8BF /* unwind_test_x86_64_frameless.S in Sources */ = {isa = PBXBuildFile; fileRef = 05920D2D17848B85001E8975 /* unwind_test_x86_64_frameless.S */; }; + C2F7F24C2451F168002BD8BF /* unwind_test_x86_64_frameless_big.S in Sources */ = {isa = PBXBuildFile; fileRef = 05920D311784C806001E8975 /* unwind_test_x86_64_frameless_big.S */; }; + C2F7F24D2451F168002BD8BF /* unwind_test_x86_64_unusual.S in Sources */ = {isa = PBXBuildFile; fileRef = 05507A4E1784DA8A009D5168 /* unwind_test_x86_64_unusual.S */; }; + C2F7F24E2451F168002BD8BF /* unwind_test_x86_disable_compact_frame.S in Sources */ = {isa = PBXBuildFile; fileRef = 05C588191788F48400BA118D /* unwind_test_x86_disable_compact_frame.S */; }; + C2F7F24F2451F168002BD8BF /* unwind_test_x86_frame.S in Sources */ = {isa = PBXBuildFile; fileRef = 05507A521784DEE4009D5168 /* unwind_test_x86_frame.S */; }; + C2F7F2502451F168002BD8BF /* unwind_test_x86_frameless.S in Sources */ = {isa = PBXBuildFile; fileRef = 05C5880D1788CAA400BA118D /* unwind_test_x86_frameless.S */; }; + C2F7F2512451F168002BD8BF /* unwind_test_x86_frameless_big.S in Sources */ = {isa = PBXBuildFile; fileRef = 05C588111788F36800BA118D /* unwind_test_x86_frameless_big.S */; }; + C2F7F2522451F169002BD8BF /* unwind_test_x86.S in Sources */ = {isa = PBXBuildFile; fileRef = 05507A13177CC4D5009D5168 /* unwind_test_x86.S */; }; + C2F7F2532451F169002BD8BF /* unwind_test_x86_64.S in Sources */ = {isa = PBXBuildFile; fileRef = 05507A17177CC50B009D5168 /* unwind_test_x86_64.S */; }; + C2F7F2542451F169002BD8BF /* unwind_test_arm.S in Sources */ = {isa = PBXBuildFile; fileRef = 05920D35178B310A001E8975 /* unwind_test_arm.S */; }; + C2F7F2552451F169002BD8BF /* unwind_test_arm64.S in Sources */ = {isa = PBXBuildFile; fileRef = 053347A517E161CB00C52E50 /* unwind_test_arm64.S */; }; + C2F7F2562451F169002BD8BF /* unwind_test_arm64_frame.S in Sources */ = {isa = PBXBuildFile; fileRef = 05BB3E1617FA043C00F464E9 /* unwind_test_arm64_frame.S */; }; + C2F7F2572451F169002BD8BF /* unwind_test_arm64_frameless.S in Sources */ = {isa = PBXBuildFile; fileRef = 058484AD1804841100A56049 /* unwind_test_arm64_frameless.S */; }; + C2F7F2582451F169002BD8BF /* unwind_test_x86_64_disable_compact_frame.S in Sources */ = {isa = PBXBuildFile; fileRef = 05507A1B177CC912009D5168 /* unwind_test_x86_64_disable_compact_frame.S */; }; + C2F7F2592451F169002BD8BF /* unwind_test_x86_64_frame.S in Sources */ = {isa = PBXBuildFile; fileRef = 05507A3E178364E8009D5168 /* unwind_test_x86_64_frame.S */; }; + C2F7F25A2451F169002BD8BF /* unwind_test_x86_64_frameless.S in Sources */ = {isa = PBXBuildFile; fileRef = 05920D2D17848B85001E8975 /* unwind_test_x86_64_frameless.S */; }; + C2F7F25B2451F169002BD8BF /* unwind_test_x86_64_frameless_big.S in Sources */ = {isa = PBXBuildFile; fileRef = 05920D311784C806001E8975 /* unwind_test_x86_64_frameless_big.S */; }; + C2F7F25C2451F169002BD8BF /* unwind_test_x86_64_unusual.S in Sources */ = {isa = PBXBuildFile; fileRef = 05507A4E1784DA8A009D5168 /* unwind_test_x86_64_unusual.S */; }; + C2F7F25D2451F169002BD8BF /* unwind_test_x86_disable_compact_frame.S in Sources */ = {isa = PBXBuildFile; fileRef = 05C588191788F48400BA118D /* unwind_test_x86_disable_compact_frame.S */; }; + C2F7F25E2451F169002BD8BF /* unwind_test_x86_frame.S in Sources */ = {isa = PBXBuildFile; fileRef = 05507A521784DEE4009D5168 /* unwind_test_x86_frame.S */; }; + C2F7F25F2451F169002BD8BF /* unwind_test_x86_frameless.S in Sources */ = {isa = PBXBuildFile; fileRef = 05C5880D1788CAA400BA118D /* unwind_test_x86_frameless.S */; }; + C2F7F2602451F169002BD8BF /* unwind_test_x86_frameless_big.S in Sources */ = {isa = PBXBuildFile; fileRef = 05C588111788F36800BA118D /* unwind_test_x86_frameless_big.S */; }; + C2F7F2622451F4CE002BD8BF /* CrashReporter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 058812B91040582D009128FB /* CrashReporter.framework */; }; + C2F7F2632451F4D0002BD8BF /* CrashReporter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8064D8BB1C4D22E5005A8B4C /* CrashReporter.framework */; }; + C2F7F26B2451F799002BD8BF /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = C2F7F26A2451F796002BD8BF /* Default-568h@2x.png */; }; + C2F7F26C2451FA63002BD8BF /* PLCrashAsyncMachExceptionInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BEC42D17BD4F400082CBFB /* PLCrashAsyncMachExceptionInfo.h */; }; + C2F7F26D2451FA64002BD8BF /* PLCrashAsyncMachExceptionInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BEC42D17BD4F400082CBFB /* PLCrashAsyncMachExceptionInfo.h */; }; + C2F7F26E2451FA65002BD8BF /* PLCrashAsyncMachExceptionInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BEC42D17BD4F400082CBFB /* PLCrashAsyncMachExceptionInfo.h */; }; + C2F7F26F2451FA9C002BD8BF /* CrashReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = 05CD31890EE93A90000FDE88 /* CrashReporter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C2F7F2702451FAA2002BD8BF /* PLCrashReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = 054F51070EEC73C80034B184 /* PLCrashReporter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C2F7F2712451FAA2002BD8BF /* PLCrashReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = 054F51070EEC73C80034B184 /* PLCrashReporter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C2F7F2722451FAA3002BD8BF /* PLCrashReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = 054F51070EEC73C80034B184 /* PLCrashReporter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C2F7F2732451FAAE002BD8BF /* PLCrashMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A5E28017A82751008A75E5 /* PLCrashMacros.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C2F7F2742451FAAE002BD8BF /* PLCrashMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A5E28017A82751008A75E5 /* PLCrashMacros.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C2F7F2752451FAAF002BD8BF /* PLCrashMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A5E28017A82751008A75E5 /* PLCrashMacros.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C2F7F2762451FAB3002BD8BF /* PLCrashNamespace.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A2077215AB30C9001E3EFC /* PLCrashNamespace.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C2F7F2772451FAB3002BD8BF /* PLCrashNamespace.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A2077215AB30C9001E3EFC /* PLCrashNamespace.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C2F7F2782451FAB4002BD8BF /* PLCrashNamespace.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A2077215AB30C9001E3EFC /* PLCrashNamespace.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C2F7F2792451FAB8002BD8BF /* PLCrashFeatureConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A2B3FF1795BA4100934198 /* PLCrashFeatureConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C2F7F27A2451FAB9002BD8BF /* PLCrashFeatureConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A2B3FF1795BA4100934198 /* PLCrashFeatureConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C2F7F27B2451FAB9002BD8BF /* PLCrashFeatureConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A2B3FF1795BA4100934198 /* PLCrashFeatureConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C2F7F27C2451FABE002BD8BF /* PLCrashReport.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F411A40EF8DA31008050CF /* PLCrashReport.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C2F7F27D2451FAC1002BD8BF /* PLCrashReportApplicationInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F4141C0EF9A6C4008050CF /* PLCrashReportApplicationInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C2F7F27E2451FAC5002BD8BF /* PLCrashReportBinaryImageInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F4150B0EF9DD9B008050CF /* PLCrashReportBinaryImageInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C2F7F27F2451FACC002BD8BF /* PLCrashReportExceptionInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F415510EF9E078008050CF /* PLCrashReportExceptionInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C2F7F2802451FAD7002BD8BF /* PLCrashReportSystemInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F413430EF995C0008050CF /* PLCrashReportSystemInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C2F7F2812451FADB002BD8BF /* PLCrashReportThreadInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F414800EF9BFAC008050CF /* PLCrashReportThreadInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C2F7F2822451FAE2002BD8BF /* PLCrashCompatConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BB3E0A17F61A6E00F464E9 /* PLCrashCompatConstants.h */; }; + C2F7F2832451FAE4002BD8BF /* PLCrashCompatConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BB3E0A17F61A6E00F464E9 /* PLCrashCompatConstants.h */; }; + C2F7F2842451FAE5002BD8BF /* PLCrashCompatConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 05BB3E0A17F61A6E00F464E9 /* PLCrashCompatConstants.h */; }; + C2F7F2852451FAED002BD8BF /* PLCrashMachExceptionPort.h in Headers */ = {isa = PBXBuildFile; fileRef = 051F067917B6B0D4006D0EFA /* PLCrashMachExceptionPort.h */; }; + C2F7F2862451FAF4002BD8BF /* PLCrashSignalHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 05CD339A0EE948EB000FDE88 /* PLCrashSignalHandler.h */; }; + C2F7F2872451FAFE002BD8BF /* PLCrashAsync.h in Headers */ = {isa = PBXBuildFile; fileRef = 059672F00EF08564008A0601 /* PLCrashAsync.h */; }; + C2F7F2882451FAFE002BD8BF /* PLCrashAsync.h in Headers */ = {isa = PBXBuildFile; fileRef = 059672F00EF08564008A0601 /* PLCrashAsync.h */; }; + C2F7F2892451FAFF002BD8BF /* PLCrashAsync.h in Headers */ = {isa = PBXBuildFile; fileRef = 059672F00EF08564008A0601 /* PLCrashAsync.h */; }; + C2F7F28A2451FB0A002BD8BF /* PLCrashAsyncThread_current_defs.h in Headers */ = {isa = PBXBuildFile; fileRef = 05EB2B0115B459770066EB4D /* PLCrashAsyncThread_current_defs.h */; }; + C2F7F28B2451FB0B002BD8BF /* PLCrashAsyncThread_current_defs.h in Headers */ = {isa = PBXBuildFile; fileRef = 05EB2B0115B459770066EB4D /* PLCrashAsyncThread_current_defs.h */; }; + C2F7F28C2451FB0B002BD8BF /* PLCrashAsyncThread_current_defs.h in Headers */ = {isa = PBXBuildFile; fileRef = 05EB2B0115B459770066EB4D /* PLCrashAsyncThread_current_defs.h */; }; + C2F7F28D2451FB10002BD8BF /* PLCrashAsyncThread_x86.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A17DEA16DBCDBF00888448 /* PLCrashAsyncThread_x86.h */; }; + C2F7F28E2451FB10002BD8BF /* PLCrashAsyncThread_x86.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A17DEA16DBCDBF00888448 /* PLCrashAsyncThread_x86.h */; }; + C2F7F28F2451FB11002BD8BF /* PLCrashAsyncThread_x86.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A17DEA16DBCDBF00888448 /* PLCrashAsyncThread_x86.h */; }; + C2F7F2902451FB17002BD8BF /* PLCrashAsyncThread_arm.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A17DEB16DBCDBF00888448 /* PLCrashAsyncThread_arm.h */; }; + C2F7F2912451FB17002BD8BF /* PLCrashAsyncThread_arm.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A17DEB16DBCDBF00888448 /* PLCrashAsyncThread_arm.h */; }; + C2F7F2922451FB18002BD8BF /* PLCrashAsyncThread_arm.h in Headers */ = {isa = PBXBuildFile; fileRef = 05A17DEB16DBCDBF00888448 /* PLCrashAsyncThread_arm.h */; }; + C2F7F2932451FB22002BD8BF /* PLCrashAsyncMObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 05DEE6471636E642007E99DC /* PLCrashAsyncMObject.h */; }; + C2F7F2942451FB23002BD8BF /* PLCrashAsyncMObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 05DEE6471636E642007E99DC /* PLCrashAsyncMObject.h */; }; + C2F7F2952451FB24002BD8BF /* PLCrashAsyncMObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 05DEE6471636E642007E99DC /* PLCrashAsyncMObject.h */; }; + C2F7F2962451FB29002BD8BF /* PLCrashAsyncSymbolication.h in Headers */ = {isa = PBXBuildFile; fileRef = C260228D1642FCAF007FC29F /* PLCrashAsyncSymbolication.h */; }; + C2F7F2972451FB29002BD8BF /* PLCrashAsyncSymbolication.h in Headers */ = {isa = PBXBuildFile; fileRef = C260228D1642FCAF007FC29F /* PLCrashAsyncSymbolication.h */; }; + C2F7F2982451FB2A002BD8BF /* PLCrashAsyncSymbolication.h in Headers */ = {isa = PBXBuildFile; fileRef = C260228D1642FCAF007FC29F /* PLCrashAsyncSymbolication.h */; }; + C2F7F2992451FB2D002BD8BF /* PLCrashAsyncMachOImage.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F76DD7162F215800A668C7 /* PLCrashAsyncMachOImage.h */; }; + C2F7F29A2451FB2E002BD8BF /* PLCrashAsyncMachOImage.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F76DD7162F215800A668C7 /* PLCrashAsyncMachOImage.h */; }; + C2F7F29B2451FB2E002BD8BF /* PLCrashAsyncMachOImage.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F76DD7162F215800A668C7 /* PLCrashAsyncMachOImage.h */; }; + C2F7F29C2451FB32002BD8BF /* PLCrashAsyncMachOString.h in Headers */ = {isa = PBXBuildFile; fileRef = C2198E0E16441D72006EB46A /* PLCrashAsyncMachOString.h */; }; + C2F7F29D2451FB32002BD8BF /* PLCrashAsyncMachOString.h in Headers */ = {isa = PBXBuildFile; fileRef = C2198E0E16441D72006EB46A /* PLCrashAsyncMachOString.h */; }; + C2F7F29E2451FB33002BD8BF /* PLCrashAsyncMachOString.h in Headers */ = {isa = PBXBuildFile; fileRef = C2198E0E16441D72006EB46A /* PLCrashAsyncMachOString.h */; }; + C2F7F29F2451FB35002BD8BF /* PLCrashAsyncObjCSection.h in Headers */ = {isa = PBXBuildFile; fileRef = C2198DE1164018B2006EB46A /* PLCrashAsyncObjCSection.h */; }; + C2F7F2A02451FB36002BD8BF /* PLCrashAsyncObjCSection.h in Headers */ = {isa = PBXBuildFile; fileRef = C2198DE1164018B2006EB46A /* PLCrashAsyncObjCSection.h */; }; + C2F7F2A12451FB36002BD8BF /* PLCrashAsyncObjCSection.h in Headers */ = {isa = PBXBuildFile; fileRef = C2198DE1164018B2006EB46A /* PLCrashAsyncObjCSection.h */; }; + C2F7F2A22451FB3A002BD8BF /* PLCrashAsyncDwarfEncoding.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 05659DEA17455DD400D2EE21 /* PLCrashAsyncDwarfEncoding.hpp */; }; + C2F7F2A32451FB3A002BD8BF /* PLCrashAsyncDwarfEncoding.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 05659DEA17455DD400D2EE21 /* PLCrashAsyncDwarfEncoding.hpp */; }; + C2F7F2A42451FB3B002BD8BF /* PLCrashAsyncDwarfEncoding.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 05659DEA17455DD400D2EE21 /* PLCrashAsyncDwarfEncoding.hpp */; }; + C2F7F2A52451FB43002BD8BF /* dwarf_private.h in Headers */ = {isa = PBXBuildFile; fileRef = 05920D1C1774E218001E8975 /* dwarf_private.h */; }; + C2F7F2A62451FB43002BD8BF /* dwarf_private.h in Headers */ = {isa = PBXBuildFile; fileRef = 05920D1C1774E218001E8975 /* dwarf_private.h */; }; + C2F7F2A72451FB43002BD8BF /* dwarf_private.h in Headers */ = {isa = PBXBuildFile; fileRef = 05920D1C1774E218001E8975 /* dwarf_private.h */; }; + C2F7F2A82451FB5D002BD8BF /* PLCrashAsyncDwarfCIE.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 05E7486E1760D8AE009B8745 /* PLCrashAsyncDwarfCIE.hpp */; }; + C2F7F2A92451FB5D002BD8BF /* PLCrashAsyncDwarfCIE.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 05E7486E1760D8AE009B8745 /* PLCrashAsyncDwarfCIE.hpp */; }; + C2F7F2AA2451FB5E002BD8BF /* PLCrashAsyncDwarfCIE.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 05E7486E1760D8AE009B8745 /* PLCrashAsyncDwarfCIE.hpp */; }; + C2F7F2AB2451FB63002BD8BF /* PLCrashAsyncDwarfFDE.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 05E748591760D62A009B8745 /* PLCrashAsyncDwarfFDE.hpp */; }; + C2F7F2AC2451FB68002BD8BF /* PLCrashAsyncDwarfPrimitives.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 05E74854175E535C009B8745 /* PLCrashAsyncDwarfPrimitives.hpp */; }; + C2F7F2AD2451FB69002BD8BF /* PLCrashAsyncDwarfPrimitives.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 05E74854175E535C009B8745 /* PLCrashAsyncDwarfPrimitives.hpp */; }; + C2F7F2AE2451FB6A002BD8BF /* PLCrashAsyncDwarfPrimitives.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 05E74854175E535C009B8745 /* PLCrashAsyncDwarfPrimitives.hpp */; }; + C2F7F2AF2451FB71002BD8BF /* PLCrashAsyncDwarfExpression.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 05E74889176135CE009B8745 /* PLCrashAsyncDwarfExpression.hpp */; }; + C2F7F2B02451FB80002BD8BF /* PLCrashProcessInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 05102E1417B0151000B5D925 /* PLCrashProcessInfo.h */; }; + C2F7F2B12451FB84002BD8BF /* PLCrashLogWriter.h in Headers */ = {isa = PBXBuildFile; fileRef = 059670250EEF6B1A008A0601 /* PLCrashLogWriter.h */; }; + C2F7F2B22451FC5E002BD8BF /* PLCrashLogWriterEncoding.h in Headers */ = {isa = PBXBuildFile; fileRef = 05CD36CC0EF25717000FDE88 /* PLCrashLogWriterEncoding.h */; }; + C2F7F2B52451FC69002BD8BF /* PLCrashFrameWalker.h in Headers */ = {isa = PBXBuildFile; fileRef = 059666DA0EEDDFB8008A0601 /* PLCrashFrameWalker.h */; }; + C2F7F2B62451FC72002BD8BF /* PLCrashFrameDWARFUnwind.h in Headers */ = {isa = PBXBuildFile; fileRef = 05920D1F177B9257001E8975 /* PLCrashFrameDWARFUnwind.h */; }; + C2F7F2B72451FC76002BD8BF /* PLCrashFrameCompactUnwind.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F3CD6416DD6A58007911FB /* PLCrashFrameCompactUnwind.h */; }; + C2F7F2B92451FC78002BD8BF /* PLCrashFrameCompactUnwind.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F3CD6416DD6A58007911FB /* PLCrashFrameCompactUnwind.h */; }; + C2F7F2BA2451FC78002BD8BF /* PLCrashFrameCompactUnwind.h in Headers */ = {isa = PBXBuildFile; fileRef = 05F3CD6416DD6A58007911FB /* PLCrashFrameCompactUnwind.h */; }; + FCE4550BA74D9DF923CFCD5A /* PLCrashFrameStackUnwind.c in Sources */ = {isa = PBXBuildFile; fileRef = FCE45837C8C773EFFD15C52B /* PLCrashFrameStackUnwind.c */; }; + FCE4586A7041D332D1025F37 /* PLCrashFrameStackUnwind.h in Headers */ = {isa = PBXBuildFile; fileRef = FCE4522F86AC61C08E9DCC17 /* PLCrashFrameStackUnwind.h */; }; + FCE45962BDFEEEFAF00DA7E4 /* PLCrashFrameStackUnwind.c in Sources */ = {isa = PBXBuildFile; fileRef = FCE45837C8C773EFFD15C52B /* PLCrashFrameStackUnwind.c */; }; + FCE45B4FD545A258E0292F25 /* PLCrashFrameStackUnwind.h in Headers */ = {isa = PBXBuildFile; fileRef = FCE4522F86AC61C08E9DCC17 /* PLCrashFrameStackUnwind.h */; }; +/* End PBXBuildFile section */ + +/* Begin PBXBuildRule section */ + C2B72B1A24534DD200D03ABD /* PBXBuildRule */ = { + isa = PBXBuildRule; + compilerSpec = com.apple.compilers.proxy.script; + filePatterns = "*.proto"; + fileType = pattern.proxy; + inputFiles = ( + ); + isEditable = 1; + outputFiles = ( + "${INPUT_FILE_DIR}/${INPUT_FILE_BASE}.pb-c.c", + "${INPUT_FILE_DIR}/${INPUT_FILE_BASE}.pb-c.h", + ); + runOncePerArchitecture = 0; + script = "if type protoc-c > /dev/null; then\n cd \"${INPUT_FILE_DIR}\" && protoc-c --c_out=. \"${INPUT_FILE_NAME}\"\nfi\n"; + }; + C2B72B2C24534F5C00D03ABD /* PBXBuildRule */ = { + isa = PBXBuildRule; + compilerSpec = com.apple.compilers.proxy.script; + filePatterns = "*.proto"; + fileType = pattern.proxy; + inputFiles = ( + ); + isEditable = 1; + outputFiles = ( + "${INPUT_FILE_DIR}/${INPUT_FILE_BASE}.pb-c.c", + "${INPUT_FILE_DIR}/${INPUT_FILE_BASE}.pb-c.h", + ); + runOncePerArchitecture = 0; + script = "if type protoc-c > /dev/null; then\n cd \"${INPUT_FILE_DIR}\" && protoc-c --c_out=. \"${INPUT_FILE_NAME}\"\nfi\n"; + }; + C2B72B2D24534F6200D03ABD /* PBXBuildRule */ = { + isa = PBXBuildRule; + compilerSpec = com.apple.compilers.proxy.script; + filePatterns = "*.proto"; + fileType = pattern.proxy; + inputFiles = ( + ); + isEditable = 1; + outputFiles = ( + "${INPUT_FILE_DIR}/${INPUT_FILE_BASE}.pb-c.c", + "${INPUT_FILE_DIR}/${INPUT_FILE_BASE}.pb-c.h", + ); + runOncePerArchitecture = 0; + script = "if type protoc-c > /dev/null; then\n cd \"${INPUT_FILE_DIR}\" && protoc-c --c_out=. \"${INPUT_FILE_NAME}\"\nfi\n"; + }; + C2B72B2E24534F6800D03ABD /* PBXBuildRule */ = { + isa = PBXBuildRule; + compilerSpec = com.apple.compilers.proxy.script; + filePatterns = "*.proto"; + fileType = pattern.proxy; + inputFiles = ( + ); + isEditable = 1; + outputFiles = ( + "${INPUT_FILE_DIR}/${INPUT_FILE_BASE}.pb-c.c", + "${INPUT_FILE_DIR}/${INPUT_FILE_BASE}.pb-c.h", + ); + runOncePerArchitecture = 0; + script = "if type protoc-c > /dev/null; then\n cd \"${INPUT_FILE_DIR}\" && protoc-c --c_out=. \"${INPUT_FILE_NAME}\"\nfi\n"; + }; + C2B72B2F24534F6B00D03ABD /* PBXBuildRule */ = { + isa = PBXBuildRule; + compilerSpec = com.apple.compilers.proxy.script; + filePatterns = "*.proto"; + fileType = pattern.proxy; + inputFiles = ( + ); + isEditable = 1; + outputFiles = ( + "${INPUT_FILE_DIR}/${INPUT_FILE_BASE}.pb-c.c", + "${INPUT_FILE_DIR}/${INPUT_FILE_BASE}.pb-c.h", + ); + runOncePerArchitecture = 0; + script = "if type protoc-c > /dev/null; then\n cd \"${INPUT_FILE_DIR}\" && protoc-c --c_out=. \"${INPUT_FILE_NAME}\"\nfi\n"; + }; + C2B72B3024534F6F00D03ABD /* PBXBuildRule */ = { + isa = PBXBuildRule; + compilerSpec = com.apple.compilers.proxy.script; + filePatterns = "*.proto"; + fileType = pattern.proxy; + inputFiles = ( + ); + isEditable = 1; + outputFiles = ( + "${INPUT_FILE_DIR}/${INPUT_FILE_BASE}.pb-c.c", + "${INPUT_FILE_DIR}/${INPUT_FILE_BASE}.pb-c.h", + ); + runOncePerArchitecture = 0; + script = "if type protoc-c > /dev/null; then\n cd \"${INPUT_FILE_DIR}\" && protoc-c --c_out=. \"${INPUT_FILE_NAME}\"\nfi\n"; + }; +/* End PBXBuildRule section */ + +/* Begin PBXContainerItemProxy section */ + 050DE25C0F61B92C00152ED3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 05E731F20EFA1AAB005EDFB7; + remoteInfo = "CrashReporter-MacOSX-Static"; + }; + 054F50E80EEC50B30034B184 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 05CD31510EE936A9000FDE88; + remoteInfo = "CrashReporter-iPhoneOS"; + }; + 058812CD104058E7009128FB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 05CD31510EE936A9000FDE88; + remoteInfo = "CrashReporter-iPhoneOS"; + }; + 05E7325C0EFA1F6C005EDFB7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 05E731F20EFA1AAB005EDFB7; + remoteInfo = "CrashReporter-MacOSX-Static"; + }; + 80A63BC01C4D2BE20073B7A3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8064D7AD1C4D22D8005A8B4C; + remoteInfo = "CrashReporter-tvOS-Device"; + }; + 80A63BD91C4D384A0073B7A3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8064D7AD1C4D22D8005A8B4C; + remoteInfo = "CrashReporter-tvOS-Device"; + }; + C23D0555248A7C440094EC6B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8DC2EF4F0486A6940098B216; + remoteInfo = "CrashReporter macOS Framework"; + }; + C26C529F2451B3B300D20162 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 05E731F20EFA1AAB005EDFB7; + remoteInfo = "CrashReporter macOS"; + }; + C26C52A42451B45D00D20162 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 05E731F20EFA1AAB005EDFB7; + remoteInfo = "CrashReporter macOS"; + }; + C2C74DC3253850A500313817 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F8B9C72A24695BE600B9FEF6; + remoteInfo = "CrashReporter XCFramework"; + }; + C2C74DC5253850A500313817 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = C2B90D952456FBD000834AFB; + remoteInfo = "CrashReporter iOS Universal"; + }; + C2C74DC7253850A500313817 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8DC2EF4F0486A6940098B216; + remoteInfo = "CrashReporter macOS Framework"; + }; + C2C74DC9253850A500313817 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = C2B90D992456FBDE00834AFB; + remoteInfo = "CrashReporter tvOS Universal"; + }; + C2C74DCB253850A500313817 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = C25664D123571D330088513E; + remoteInfo = Documentation; + }; + C2C74DCD253850A500313817 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 05E731E20EFA1A3E005EDFB7; + remoteInfo = plcrashutil; + }; + C2C74E1C25385F5B00313817 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = C2C74D9B2538508300313817; + remoteInfo = "Crash Reporter"; + }; + C2CBA40924586975001B775F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8DC2EF4F0486A6940098B216; + remoteInfo = "CrashReporter macOS Framework"; + }; + C2CBA40C245869ED001B775F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 058812B81040582D009128FB; + remoteInfo = "CrashReporter iOS Framework"; + }; + C2CBA40E245869F4001B775F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 8064D88B1C4D22E5005A8B4C; + remoteInfo = "CrashReporter tvOS Framework"; + }; + F8CF2BCC246C05D100904633 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = C2B90D952456FBD000834AFB; + remoteInfo = "CrashReporter iOS Universal"; + }; + F8CF2BD0246C05D100904633 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; + proxyType = 1; + remoteGlobalIDString = C2B90D992456FBDE00834AFB; + remoteInfo = "CrashReporter tvOS Universal"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 05CD34410EEA60F8000FDE88 /* Copy Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Copy Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + C2CBA40B24586975001B775F /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + C2CBA40824586975001B775F /* CrashReporter.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 050DE24D0F61B80B00152ED3 /* Fuzz Testing */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "Fuzz Testing"; sourceTree = BUILT_PRODUCTS_DIR; }; + 050DE28D0F61BB1D00152ED3 /* fuzz_report.plcrash */ = {isa = PBXFileReference; lastKnownFileType = file; path = fuzz_report.plcrash; sourceTree = ""; }; + 05102E1417B0151000B5D925 /* PLCrashProcessInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashProcessInfo.h; sourceTree = ""; }; + 05102E1517B0151000B5D925 /* PLCrashProcessInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashProcessInfo.m; sourceTree = ""; }; + 05102E1C17B0152B00B5D925 /* PLCrashProcessInfoTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashProcessInfoTests.m; sourceTree = ""; }; + 05102E2217B2B80A00B5D925 /* PLCrashHostInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashHostInfo.h; sourceTree = ""; }; + 05102E2317B2B80A00B5D925 /* PLCrashHostInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashHostInfo.m; sourceTree = ""; }; + 05102E2C17B2B82000B5D925 /* PLCrashHostInfoTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashHostInfoTests.m; sourceTree = ""; }; + 0513E23217D15ED400727919 /* PLCrashReportMachExceptionInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashReportMachExceptionInfo.h; sourceTree = ""; }; + 0513E23317D15ED400727919 /* PLCrashReportMachExceptionInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashReportMachExceptionInfo.m; sourceTree = ""; }; + 051F067917B6B0D4006D0EFA /* PLCrashMachExceptionPort.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashMachExceptionPort.h; sourceTree = ""; }; + 051F067A17B6B0D4006D0EFA /* PLCrashMachExceptionPort.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashMachExceptionPort.m; sourceTree = ""; }; + 052951E91696965E006EDA8A /* PLCrashLogWriterEncodingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashLogWriterEncodingTests.m; sourceTree = ""; }; + 052951EE1696A461006EDA8A /* PLCrashLogWriterEncodingTests.proto */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.protobuf; path = PLCrashLogWriterEncodingTests.proto; sourceTree = ""; }; + 052A45CF136353FB00987004 /* DemoCrash iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "DemoCrash iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 052A46BC1363650100987004 /* PLCrashAsyncImageList.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashAsyncImageList.h; sourceTree = ""; }; + 052A46BD1363650100987004 /* PLCrashAsyncImageList.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = PLCrashAsyncImageList.cpp; sourceTree = ""; }; + 052A46F713637DE000987004 /* PLCrashAsyncImageListTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashAsyncImageListTests.m; sourceTree = ""; }; + 052DC863175553DC004335FE /* dwarf_encoding_test.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = dwarf_encoding_test.h; sourceTree = ""; }; + 053347A517E161CB00C52E50 /* unwind_test_arm64.S */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.asm; path = unwind_test_arm64.S; sourceTree = ""; }; + 054627A711D998BB007891C7 /* PLCrashReportTextFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashReportTextFormatter.h; sourceTree = ""; }; + 054627A811D998BB007891C7 /* PLCrashReportTextFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashReportTextFormatter.m; sourceTree = ""; }; + 054627B811D99D06007891C7 /* PLCrashReportFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashReportFormatter.h; sourceTree = ""; }; + 054F51070EEC73C80034B184 /* PLCrashReporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashReporter.h; sourceTree = ""; }; + 05507A0E177CC2C9009D5168 /* README.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = README.txt; sourceTree = ""; }; + 05507A0F177CC456009D5168 /* unwind_test_harness.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = unwind_test_harness.c; sourceTree = ""; }; + 05507A13177CC4D5009D5168 /* unwind_test_x86.S */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.asm; path = unwind_test_x86.S; sourceTree = ""; }; + 05507A17177CC50B009D5168 /* unwind_test_x86_64.S */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.asm; path = unwind_test_x86_64.S; sourceTree = ""; }; + 05507A1B177CC912009D5168 /* unwind_test_x86_64_disable_compact_frame.S */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.asm; path = unwind_test_x86_64_disable_compact_frame.S; sourceTree = ""; }; + 05507A1F177CCB1C009D5168 /* unwind_test_harness.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = unwind_test_harness.h; sourceTree = ""; }; + 05507A3E178364E8009D5168 /* unwind_test_x86_64_frame.S */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.asm; path = unwind_test_x86_64_frame.S; sourceTree = ""; }; + 05507A4E1784DA8A009D5168 /* unwind_test_x86_64_unusual.S */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.asm; path = unwind_test_x86_64_unusual.S; sourceTree = ""; }; + 05507A521784DEE4009D5168 /* unwind_test_x86_frame.S */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.asm; path = unwind_test_x86_frame.S; sourceTree = ""; }; + 05659DEA17455DD400D2EE21 /* PLCrashAsyncDwarfEncoding.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; lineEnding = 0; path = PLCrashAsyncDwarfEncoding.hpp; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + 05659DED17455DED00D2EE21 /* PLCrashAsyncDwarfEncoding.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; lineEnding = 0; path = PLCrashAsyncDwarfEncoding.cpp; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.c; }; + 05659DF7174D2E1200D2EE21 /* PLCrashTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashTestCase.h; sourceTree = ""; }; + 05659DF8174D2E1200D2EE21 /* PLCrashTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashTestCase.m; sourceTree = ""; }; + 0573B42A1681098E00395F2A /* PLCrashMachExceptionServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashMachExceptionServer.h; sourceTree = ""; }; + 0573B42B1681098E00395F2A /* PLCrashMachExceptionServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashMachExceptionServer.m; sourceTree = ""; }; + 0581B520168FDB280098C103 /* mach_exc.defs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.mig; name = mach_exc.defs; path = usr/include/mach/mach_exc.defs; sourceTree = SDKROOT; }; + 058484AD1804841100A56049 /* unwind_test_arm64_frameless.S */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.asm; path = unwind_test_arm64_frameless.S; sourceTree = ""; }; + 058812B91040582D009128FB /* CrashReporter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CrashReporter.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 05920D1C1774E218001E8975 /* dwarf_private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = dwarf_private.h; sourceTree = ""; }; + 05920D1E177B9257001E8975 /* PLCrashFrameDWARFUnwind.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = PLCrashFrameDWARFUnwind.cpp; sourceTree = ""; }; + 05920D1F177B9257001E8975 /* PLCrashFrameDWARFUnwind.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashFrameDWARFUnwind.h; sourceTree = ""; }; + 05920D29177B92E7001E8975 /* PLCrashFrameDWARFUnwindTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashFrameDWARFUnwindTests.m; sourceTree = ""; }; + 05920D2D17848B85001E8975 /* unwind_test_x86_64_frameless.S */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.asm; path = unwind_test_x86_64_frameless.S; sourceTree = ""; }; + 05920D311784C806001E8975 /* unwind_test_x86_64_frameless_big.S */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.asm; path = unwind_test_x86_64_frameless_big.S; sourceTree = ""; }; + 05920D35178B310A001E8975 /* unwind_test_arm.S */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.asm; path = unwind_test_arm.S; sourceTree = ""; }; + 059666DA0EEDDFB8008A0601 /* PLCrashFrameWalker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashFrameWalker.h; sourceTree = ""; }; + 059666DB0EEDDFB8008A0601 /* PLCrashFrameWalker.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = PLCrashFrameWalker.c; sourceTree = ""; }; + 059666E20EEDDFCC008A0601 /* PLCrashFrameWalkerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashFrameWalkerTests.m; sourceTree = ""; }; + 059670250EEF6B1A008A0601 /* PLCrashLogWriter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashLogWriter.h; sourceTree = ""; }; + 059670260EEF6B1A008A0601 /* PLCrashLogWriter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashLogWriter.m; sourceTree = ""; }; + 0596702D0EEF6B51008A0601 /* PLCrashLogWriterTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashLogWriterTests.m; sourceTree = ""; }; + 059670C70EEFAC3A008A0601 /* PLCrashReport.proto */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.protobuf; path = PLCrashReport.proto; sourceTree = ""; }; + 059672F00EF08564008A0601 /* PLCrashAsync.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashAsync.h; sourceTree = ""; }; + 05A17DC416D7F81600888448 /* PLCrashAsyncThread.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = PLCrashAsyncThread.c; sourceTree = ""; }; + 05A17DCC16D7F82700888448 /* PLCrashAsyncThread.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashAsyncThread.h; sourceTree = ""; }; + 05A17DD216D8080A00888448 /* PLCrashAsyncThreadTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashAsyncThreadTests.m; sourceTree = ""; }; + 05A17DEA16DBCDBF00888448 /* PLCrashAsyncThread_x86.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashAsyncThread_x86.h; sourceTree = ""; }; + 05A17DEB16DBCDBF00888448 /* PLCrashAsyncThread_arm.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashAsyncThread_arm.h; sourceTree = ""; }; + 05A17DF016DBD0AD00888448 /* PLCrashAsyncThread_x86.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = PLCrashAsyncThread_x86.c; sourceTree = ""; }; + 05A17DF516DBD0C200888448 /* PLCrashAsyncThread_arm.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = PLCrashAsyncThread_arm.c; sourceTree = ""; }; + 05A2077215AB30C9001E3EFC /* PLCrashNamespace.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PLCrashNamespace.h; sourceTree = ""; }; + 05A2B3FF1795BA4100934198 /* PLCrashFeatureConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PLCrashFeatureConfig.h; sourceTree = ""; }; + 05A5E28017A82751008A75E5 /* PLCrashMacros.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashMacros.h; sourceTree = ""; }; + 05A5E28717C04188008A75E5 /* PLCrashAsyncLinkedList.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = PLCrashAsyncLinkedList.hpp; sourceTree = ""; }; + 05B929E617C9336600B051E3 /* PLCrashUncaughtExceptionHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashUncaughtExceptionHandler.h; sourceTree = ""; }; + 05B929E717C9336600B051E3 /* PLCrashUncaughtExceptionHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashUncaughtExceptionHandler.m; sourceTree = ""; }; + 05B929F017C9337D00B051E3 /* PLCrashUncaughtExceptionHandlerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashUncaughtExceptionHandlerTests.m; sourceTree = ""; }; + 05BB3E0A17F61A6E00F464E9 /* PLCrashCompatConstants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PLCrashCompatConstants.h; sourceTree = ""; }; + 05BB3E1617FA043C00F464E9 /* unwind_test_arm64_frame.S */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.asm; path = unwind_test_arm64_frame.S; sourceTree = ""; }; + 05BB83CB1364A77800D53B84 /* PLCrashReportProcessorInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashReportProcessorInfo.h; sourceTree = ""; }; + 05BB83CC1364A77800D53B84 /* PLCrashReportProcessorInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashReportProcessorInfo.m; sourceTree = ""; }; + 05BB83EF1364AD3E00D53B84 /* PLCrashReportMachineInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashReportMachineInfo.h; sourceTree = ""; }; + 05BB83F01364AD3E00D53B84 /* PLCrashReportMachineInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashReportMachineInfo.m; sourceTree = ""; }; + 05BB84841364EDF200D53B84 /* PLCrashSysctl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashSysctl.h; sourceTree = ""; }; + 05BB84851364EDF200D53B84 /* PLCrashSysctl.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = PLCrashSysctl.c; sourceTree = ""; }; + 05BEC41517BAF92A0082CBFB /* PLCrashMachExceptionPortSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashMachExceptionPortSet.h; sourceTree = ""; }; + 05BEC41617BAF92A0082CBFB /* PLCrashMachExceptionPortSet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashMachExceptionPortSet.m; sourceTree = ""; }; + 05BEC41F17BAF95C0082CBFB /* PLCrashMachExceptionPortSetTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashMachExceptionPortSetTests.m; sourceTree = ""; }; + 05BEC42517BD4F290082CBFB /* PLCrashAsyncMachExceptionInfo.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = PLCrashAsyncMachExceptionInfo.c; sourceTree = ""; }; + 05BEC42D17BD4F400082CBFB /* PLCrashAsyncMachExceptionInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashAsyncMachExceptionInfo.h; sourceTree = ""; }; + 05BEC43017BD4F540082CBFB /* PLCrashAsyncMachExceptionInfoTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashAsyncMachExceptionInfoTests.m; sourceTree = ""; }; + 05BEC43417BF1CB10082CBFB /* PLCrashReporterConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashReporterConfig.h; sourceTree = ""; }; + 05BEC43517BF1CB10082CBFB /* PLCrashReporterConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashReporterConfig.m; sourceTree = ""; }; + 05C5880D1788CAA400BA118D /* unwind_test_x86_frameless.S */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.asm; path = unwind_test_x86_frameless.S; sourceTree = ""; }; + 05C588111788F36800BA118D /* unwind_test_x86_frameless_big.S */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.asm; path = unwind_test_x86_frameless_big.S; sourceTree = ""; }; + 05C588151788F3E700BA118D /* unwind_test_x86_unusual.S */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.asm; path = unwind_test_x86_unusual.S; sourceTree = ""; }; + 05C588191788F48400BA118D /* unwind_test_x86_disable_compact_frame.S */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.asm; path = unwind_test_x86_disable_compact_frame.S; sourceTree = ""; }; + 05C76DA4176B8C7000E9B10D /* dwarf_opstream.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = dwarf_opstream.cpp; sourceTree = ""; }; + 05C76DA5176B8C7000E9B10D /* dwarf_opstream.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = dwarf_opstream.hpp; sourceTree = ""; }; + 05C76DB1176B946E00E9B10D /* dwarf_opstream_tests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = dwarf_opstream_tests.mm; sourceTree = ""; }; + 05C76DC6176FBAF300E9B10D /* PLCrashAsyncDwarfCFAState.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = PLCrashAsyncDwarfCFAState.cpp; sourceTree = ""; }; + 05C76DC7176FBAF300E9B10D /* PLCrashAsyncDwarfCFAState.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = PLCrashAsyncDwarfCFAState.hpp; sourceTree = ""; }; + 05C76DD3176FBC1E00E9B10D /* PLCrashAsyncDwarfCFAStateTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PLCrashAsyncDwarfCFAStateTests.mm; sourceTree = ""; }; + 05CD31520EE936A9000FDE88 /* libCrashReporter.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libCrashReporter.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 05CD31890EE93A90000FDE88 /* CrashReporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CrashReporter.h; sourceTree = ""; }; + 05CD318A0EE93A90000FDE88 /* CrashReporter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CrashReporter.m; sourceTree = ""; }; + 05CD32690EE93DC3000FDE88 /* Tests macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tests macOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 05CD32AF0EE94086000FDE88 /* Tests-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Tests-Info.plist"; sourceTree = ""; }; + 05CD33240EE94439000FDE88 /* Tests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 05CD33520EE9457D000FDE88 /* CrashReporter.exp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.exports; path = CrashReporter.exp; sourceTree = ""; }; + 05CD339A0EE948EB000FDE88 /* PLCrashSignalHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashSignalHandler.h; sourceTree = ""; }; + 05CD339B0EE948EB000FDE88 /* PLCrashSignalHandler.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PLCrashSignalHandler.mm; sourceTree = ""; }; + 05CD33A20EE94931000FDE88 /* PLCrashSignalHandlerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashSignalHandlerTests.m; sourceTree = ""; }; + 05CD36410EF24758000FDE88 /* PLCrashAsync.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = PLCrashAsync.c; sourceTree = ""; }; + 05CD36480EF247A9000FDE88 /* PLCrashAsyncTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashAsyncTests.m; sourceTree = ""; }; + 05CD36CC0EF25717000FDE88 /* PLCrashLogWriterEncoding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashLogWriterEncoding.h; sourceTree = ""; }; + 05CD36CD0EF25717000FDE88 /* PLCrashLogWriterEncoding.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = PLCrashLogWriterEncoding.c; sourceTree = ""; }; + 05D9E5431676598200B39833 /* PLCrashReportStackFrameInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashReportStackFrameInfo.h; sourceTree = ""; }; + 05D9E5441676598200B39833 /* PLCrashReportStackFrameInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashReportStackFrameInfo.m; sourceTree = ""; }; + 05D9E54E16765A0200B39833 /* PLCrashReportRegisterInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashReportRegisterInfo.h; sourceTree = ""; }; + 05D9E54F16765A0200B39833 /* PLCrashReportRegisterInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashReportRegisterInfo.m; sourceTree = ""; }; + 05D9E55916765D0200B39833 /* PLCrashReportSymbolInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashReportSymbolInfo.h; sourceTree = ""; }; + 05D9E55A16765D0200B39833 /* PLCrashReportSymbolInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashReportSymbolInfo.m; sourceTree = ""; }; + 05DEE63E1636E62B007E99DC /* PLCrashAsyncMObject.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = PLCrashAsyncMObject.c; sourceTree = ""; }; + 05DEE6471636E642007E99DC /* PLCrashAsyncMObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashAsyncMObject.h; sourceTree = ""; }; + 05DEE64A1636E721007E99DC /* PLCrashAsyncMObjectTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashAsyncMObjectTests.m; sourceTree = ""; }; + 05E731E30EFA1A3E005EDFB7 /* plcrashutil */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = plcrashutil; sourceTree = BUILT_PRODUCTS_DIR; }; + 05E731F30EFA1AAB005EDFB7 /* libCrashReporter.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libCrashReporter.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 05E734300EFAC46D005EDFB7 /* PLCrashAsyncSignalInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashAsyncSignalInfo.h; sourceTree = ""; }; + 05E734310EFAC46D005EDFB7 /* PLCrashAsyncSignalInfo.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = PLCrashAsyncSignalInfo.c; sourceTree = ""; }; + 05E734830EFAD83B005EDFB7 /* PLCrashAsyncSignalInfoTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashAsyncSignalInfoTests.m; sourceTree = ""; }; + 05E734F50EFAE59C005EDFB7 /* PLCrashReportSignalInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashReportSignalInfo.h; sourceTree = ""; }; + 05E734F60EFAE59C005EDFB7 /* PLCrashReportSignalInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashReportSignalInfo.m; sourceTree = ""; }; + 05E7484C175E5349009B8745 /* PLCrashAsyncDwarfPrimitives.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = PLCrashAsyncDwarfPrimitives.cpp; sourceTree = ""; }; + 05E74854175E535C009B8745 /* PLCrashAsyncDwarfPrimitives.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = PLCrashAsyncDwarfPrimitives.hpp; sourceTree = ""; }; + 05E74855175E5370009B8745 /* PLCrashAsyncDwarfPrimitivesTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PLCrashAsyncDwarfPrimitivesTests.mm; sourceTree = ""; }; + 05E748591760D62A009B8745 /* PLCrashAsyncDwarfFDE.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = PLCrashAsyncDwarfFDE.hpp; sourceTree = ""; }; + 05E7485E1760D64D009B8745 /* PLCrashAsyncDwarfFDE.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = PLCrashAsyncDwarfFDE.cpp; sourceTree = ""; }; + 05E748661760D890009B8745 /* PLCrashAsyncDwarfCIE.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = PLCrashAsyncDwarfCIE.cpp; sourceTree = ""; }; + 05E7486E1760D8AE009B8745 /* PLCrashAsyncDwarfCIE.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = PLCrashAsyncDwarfCIE.hpp; sourceTree = ""; }; + 05E748711760DBBE009B8745 /* PLCrashAsyncDwarfCIETests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PLCrashAsyncDwarfCIETests.mm; sourceTree = ""; }; + 05E748751760DBD0009B8745 /* PLCrashAsyncDwarfFDETests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PLCrashAsyncDwarfFDETests.mm; sourceTree = ""; }; + 05E7487A176118C1009B8745 /* PLCrashAsyncDwarfCFAStateEvaluation.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = PLCrashAsyncDwarfCFAStateEvaluation.cpp; sourceTree = ""; }; + 05E74885176118F8009B8745 /* PLCrashAsyncDwarfCFAStateEvaluationTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PLCrashAsyncDwarfCFAStateEvaluationTests.mm; sourceTree = ""; }; + 05E74889176135CE009B8745 /* PLCrashAsyncDwarfExpression.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = PLCrashAsyncDwarfExpression.hpp; sourceTree = ""; }; + 05E7488A176135CE009B8745 /* PLCrashAsyncDwarfExpression.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = PLCrashAsyncDwarfExpression.cpp; sourceTree = ""; }; + 05E7489417613AF0009B8745 /* PLCrashAsyncDwarfExpressionTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PLCrashAsyncDwarfExpressionTests.mm; sourceTree = ""; }; + 05E748A617616D30009B8745 /* dwarf_stack.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = dwarf_stack.hpp; sourceTree = ""; }; + 05E748B217616D6B009B8745 /* dwarf_stack_tests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = dwarf_stack_tests.mm; sourceTree = ""; }; + 05EB2AF615B454DD0066EB4D /* PLCrashAsyncThread_current.S */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.asm; path = PLCrashAsyncThread_current.S; sourceTree = ""; }; + 05EB2AFC15B456750066EB4D /* PLCrashAsyncThread_current.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = PLCrashAsyncThread_current.c; sourceTree = ""; }; + 05EB2B0115B459770066EB4D /* PLCrashAsyncThread_current_defs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PLCrashAsyncThread_current_defs.h; sourceTree = ""; }; + 05EB2B0D15B6FDA70066EB4D /* PLCrashReporterNSError.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashReporterNSError.h; sourceTree = ""; }; + 05EB2B0E15B6FDA70066EB4D /* PLCrashReporterNSError.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashReporterNSError.m; sourceTree = ""; }; + 05EB2B1B15B6FE280066EB4D /* PLCrashReporterNSErrorTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashReporterNSErrorTests.m; sourceTree = ""; }; + 05F3CD5F16DD6A3B007911FB /* PLCrashFrameCompactUnwind.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = PLCrashFrameCompactUnwind.c; sourceTree = ""; }; + 05F3CD6416DD6A58007911FB /* PLCrashFrameCompactUnwind.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashFrameCompactUnwind.h; sourceTree = ""; }; + 05F3CD6816DD6A7A007911FB /* PLCrashFrameCompactUnwindTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashFrameCompactUnwindTests.m; sourceTree = ""; }; + 05F3CD6C16DE7625007911FB /* Tests */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Tests; sourceTree = ""; }; + 05F3CD7216DFC744007911FB /* PLCrashAsyncCompactUnwindEncoding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashAsyncCompactUnwindEncoding.h; sourceTree = ""; }; + 05F3CD7316DFC744007911FB /* PLCrashAsyncCompactUnwindEncoding.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = PLCrashAsyncCompactUnwindEncoding.c; sourceTree = ""; }; + 05F3CD8016DFC78D007911FB /* PLCrashAsyncCompactUnwindEncodingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashAsyncCompactUnwindEncodingTests.m; sourceTree = ""; }; + 05F40ACA0EF7379F008050CF /* PLCrashReporter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashReporter.m; sourceTree = ""; }; + 05F40ADD0EF73A39008050CF /* PLCrashReporterTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashReporterTests.m; sourceTree = ""; }; + 05F40CE70EF7AB80008050CF /* DemoCrash macOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "DemoCrash macOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 05F40CE90EF7AB80008050CF /* DemoCrash-macOS-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "DemoCrash-macOS-Info.plist"; sourceTree = ""; }; + 05F411A40EF8DA31008050CF /* PLCrashReport.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashReport.h; sourceTree = ""; }; + 05F411A50EF8DA31008050CF /* PLCrashReport.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashReport.m; sourceTree = ""; }; + 05F411AC0EF8DE68008050CF /* PLCrashReportTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashReportTests.m; sourceTree = ""; }; + 05F413430EF995C0008050CF /* PLCrashReportSystemInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashReportSystemInfo.h; sourceTree = ""; }; + 05F413440EF995C0008050CF /* PLCrashReportSystemInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashReportSystemInfo.m; sourceTree = ""; }; + 05F4141C0EF9A6C4008050CF /* PLCrashReportApplicationInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashReportApplicationInfo.h; sourceTree = ""; }; + 05F4141D0EF9A6C4008050CF /* PLCrashReportApplicationInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashReportApplicationInfo.m; sourceTree = ""; }; + 05F414800EF9BFAC008050CF /* PLCrashReportThreadInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashReportThreadInfo.h; sourceTree = ""; }; + 05F414810EF9BFAC008050CF /* PLCrashReportThreadInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashReportThreadInfo.m; sourceTree = ""; }; + 05F4150B0EF9DD9B008050CF /* PLCrashReportBinaryImageInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashReportBinaryImageInfo.h; sourceTree = ""; }; + 05F4150C0EF9DD9B008050CF /* PLCrashReportBinaryImageInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashReportBinaryImageInfo.m; sourceTree = ""; }; + 05F415510EF9E078008050CF /* PLCrashReportExceptionInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashReportExceptionInfo.h; sourceTree = ""; }; + 05F415520EF9E078008050CF /* PLCrashReportExceptionInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashReportExceptionInfo.m; sourceTree = ""; }; + 05F76DD2162F213E00A668C7 /* PLCrashAsyncMachOImage.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = PLCrashAsyncMachOImage.c; sourceTree = ""; }; + 05F76DD7162F215800A668C7 /* PLCrashAsyncMachOImage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PLCrashAsyncMachOImage.h; sourceTree = ""; }; + 05F76DD9162F238E00A668C7 /* PLCrashAsyncMachOImageTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashAsyncMachOImageTests.m; sourceTree = ""; }; + 2D0E10441141F7DC00CE1BD6 /* PLCrashReportProcessInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashReportProcessInfo.h; sourceTree = ""; }; + 2D0E10451141F7DC00CE1BD6 /* PLCrashReportProcessInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashReportProcessInfo.m; sourceTree = ""; }; + 8064D81B1C4D22D8005A8B4C /* libCrashReporter.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libCrashReporter.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 8064D8BB1C4D22E5005A8B4C /* CrashReporter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CrashReporter.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8064D9951C4D27E2005A8B4C /* CrashReporter tvOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "CrashReporter tvOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 8064D9A31C4D27E9005A8B4C /* DemoCrash tvOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "DemoCrash tvOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 8DC2EF5A0486A6940098B216 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 8DC2EF5B0486A6940098B216 /* CrashReporter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CrashReporter.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C2198DD81640188C006EB46A /* PLCrashAsyncObjCSection.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PLCrashAsyncObjCSection.mm; sourceTree = ""; }; + C2198DE1164018B2006EB46A /* PLCrashAsyncObjCSection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PLCrashAsyncObjCSection.h; sourceTree = ""; }; + C2198DE316402B8A006EB46A /* PLCrashAsyncObjCSectionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashAsyncObjCSectionTests.m; sourceTree = ""; }; + C2198E0516441CF5006EB46A /* PLCrashAsyncMachOString.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = PLCrashAsyncMachOString.c; sourceTree = ""; }; + C2198E0E16441D72006EB46A /* PLCrashAsyncMachOString.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PLCrashAsyncMachOString.h; sourceTree = ""; }; + C26022851642FCA6007FC29F /* PLCrashAsyncSymbolication.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = PLCrashAsyncSymbolication.c; sourceTree = ""; }; + C260228D1642FCAF007FC29F /* PLCrashAsyncSymbolication.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PLCrashAsyncSymbolication.h; sourceTree = ""; }; + C260228F1642FE9B007FC29F /* PLCrashAsyncSymbolicationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashAsyncSymbolicationTests.m; sourceTree = ""; }; + C2620D432451CF8B00B11E68 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + C2620D452451CF9400B11E68 /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; }; + C2620D482451D28B00B11E68 /* CrashReporter.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = CrashReporter.modulemap; sourceTree = ""; }; + C29AD6C12456C61E00360AF7 /* SenTestCompat.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SenTestCompat.h; sourceTree = ""; }; + C29AD6C32456C69000360AF7 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + C29AD6C52456C69000360AF7 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + C29AD6C72456C69000360AF7 /* fuzz-main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "fuzz-main.m"; sourceTree = ""; }; + C29AD6CD2456C94A00360AF7 /* PLCrashTestThread.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashTestThread.m; sourceTree = ""; }; + C29AD6CE2456C94A00360AF7 /* PLCrashTestThreadTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashTestThreadTests.m; sourceTree = ""; }; + C29AD6CF2456C94A00360AF7 /* PLCrashTestThread.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashTestThread.h; sourceTree = ""; }; + C29AD6D62456C9B800360AF7 /* PLCrashLogWriterEncodingTests.pb-c.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = "PLCrashLogWriterEncodingTests.pb-c.c"; sourceTree = ""; }; + C2B72B0D2453496E00D03ABD /* PLCrashReport.pb-c.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = "PLCrashReport.pb-c.c"; sourceTree = ""; }; + C2B72B0E2453496F00D03ABD /* PLCrashReport.pb-c.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "PLCrashReport.pb-c.h"; sourceTree = ""; }; + C2B72B15245349C500D03ABD /* PLCrashLogWriterEncodingTests.pb-c.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "PLCrashLogWriterEncodingTests.pb-c.h"; sourceTree = ""; }; + C2B72B2424534EE700D03ABD /* protobuf-c.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "protobuf-c.h"; sourceTree = ""; }; + C2B72B2524534EE700D03ABD /* protobuf-c.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = "protobuf-c.c"; sourceTree = ""; }; + C2B90DA12456FE8200834AFB /* PLCrashReporter.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = PLCrashReporter.podspec; sourceTree = ""; }; + C2B90DA22456FE8200834AFB /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + C2B90DA32456FE8200834AFB /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; + C2B90DA5245714E900834AFB /* generate-documentation.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "generate-documentation.sh"; sourceTree = ""; }; + C2B90DA7245714E900834AFB /* verify-modifications.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "verify-modifications.sh"; sourceTree = ""; }; + C2BBCD7E2456E03D00F9E820 /* PLCrashMachExceptionPortTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashMachExceptionPortTests.m; sourceTree = ""; }; + C2BBCD7F2456E03D00F9E820 /* PLCrashAsyncDwarfEncodingTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PLCrashAsyncDwarfEncodingTests.mm; sourceTree = ""; }; + C2BBCD802456E03D00F9E820 /* PLCrashMachExceptionServerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashMachExceptionServerTests.m; sourceTree = ""; }; + C2BBCD812456E03D00F9E820 /* PLCrashFrameStackUnwindTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashFrameStackUnwindTests.m; sourceTree = ""; }; + C2BBCD822456E03D00F9E820 /* PLCrashAsyncLinkedListTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PLCrashAsyncLinkedListTests.mm; sourceTree = ""; }; + C2BBCD832456E03D00F9E820 /* PLCrashSysctlTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashSysctlTests.m; sourceTree = ""; }; + C2BBCD842456E03D00F9E820 /* PLCrashAsyncMachOStringTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PLCrashAsyncMachOStringTests.m; sourceTree = ""; }; + C2C74A852535CD3A00313817 /* combine-frameworks.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "combine-frameworks.sh"; sourceTree = ""; }; + C2C74A862535CD3A00313817 /* combine-xcframework.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "combine-xcframework.sh"; sourceTree = ""; }; + C2C74A882535CD3A00313817 /* build-framework.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "build-framework.sh"; sourceTree = ""; }; + C2C74E1E25385F8300313817 /* combine-libraries.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "combine-libraries.sh"; sourceTree = ""; }; + C2C74E1F25385F8300313817 /* copy-products.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "copy-products.sh"; sourceTree = ""; }; + C2DCD87724657B24007322C5 /* DemoCrash-iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "DemoCrash-iOS.entitlements"; sourceTree = ""; }; + C2F0AC9F24AB7C28004890EC /* CrashReporterFramework.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CrashReporterFramework.m; sourceTree = ""; }; + C2F7F22F2451F081002BD8BF /* DemoCrash-tvOS-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "DemoCrash-tvOS-Info.plist"; sourceTree = ""; }; + C2F7F2302451F081002BD8BF /* DemoCrash-iOS-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "DemoCrash-iOS-Info.plist"; sourceTree = ""; }; + C2F7F26A2451F796002BD8BF /* Default-568h@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Default-568h@2x.png"; sourceTree = ""; }; + C2F7F2BB2451FE1F002BD8BF /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS13.4.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; + FCE4522F86AC61C08E9DCC17 /* PLCrashFrameStackUnwind.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PLCrashFrameStackUnwind.h; sourceTree = ""; }; + FCE45837C8C773EFFD15C52B /* PLCrashFrameStackUnwind.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = PLCrashFrameStackUnwind.c; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 050DE24B0F61B80B00152ED3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C238788524574C0100519007 /* libCrashReporter.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 052A45CD136353FB00987004 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C2F7F2622451F4CE002BD8BF /* CrashReporter.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 058812B71040582D009128FB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C26C52A12451B3E500D20162 /* libCrashReporter.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05CD31500EE936A9000FDE88 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05CD32660EE93DC3000FDE88 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C202F6482451C4FE00754DF7 /* libCrashReporter.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05CD33220EE94439000FDE88 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 05614E291A966F5A00D62442 /* libCrashReporter.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05E731E10EFA1A3E005EDFB7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C238788624574C0700519007 /* libCrashReporter.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05E731F10EFA1AAB005EDFB7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05F40CE50EF7AB80008050CF /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C2CBA40724586975001B775F /* CrashReporter.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8064D8161C4D22D8005A8B4C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8064D8B61C4D22E5005A8B4C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C26C52A32451B41800D20162 /* libCrashReporter.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8064D98F1C4D27E2005A8B4C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 80A63BD81C4D32FB0073B7A3 /* libCrashReporter.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8064D99E1C4D27E9005A8B4C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C2F7F2632451F4D0002BD8BF /* CrashReporter.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8DC2EF560486A6940098B216 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C26C52A22451B3F900D20162 /* libCrashReporter.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 034768DFFF38A50411DB9C8B /* Products */ = { + isa = PBXGroup; + children = ( + 8DC2EF5B0486A6940098B216 /* CrashReporter.framework */, + 05CD31520EE936A9000FDE88 /* libCrashReporter.a */, + 05CD32690EE93DC3000FDE88 /* Tests macOS.xctest */, + 05CD33240EE94439000FDE88 /* Tests iOS.xctest */, + 05F40CE70EF7AB80008050CF /* DemoCrash macOS.app */, + 05E731E30EFA1A3E005EDFB7 /* plcrashutil */, + 05E731F30EFA1AAB005EDFB7 /* libCrashReporter.a */, + 050DE24D0F61B80B00152ED3 /* Fuzz Testing */, + 058812B91040582D009128FB /* CrashReporter.framework */, + 052A45CF136353FB00987004 /* DemoCrash iOS.app */, + 8064D81B1C4D22D8005A8B4C /* libCrashReporter.a */, + 8064D8BB1C4D22E5005A8B4C /* CrashReporter.framework */, + 8064D9951C4D27E2005A8B4C /* CrashReporter tvOS Tests.xctest */, + 8064D9A31C4D27E9005A8B4C /* DemoCrash tvOS.app */, + ); + name = Products; + sourceTree = ""; + }; + 0513E23117D15E6F00727919 /* Mach Exception Info */ = { + isa = PBXGroup; + children = ( + 0513E23217D15ED400727919 /* PLCrashReportMachExceptionInfo.h */, + 0513E23317D15ED400727919 /* PLCrashReportMachExceptionInfo.m */, + ); + name = "Mach Exception Info"; + sourceTree = ""; + }; + 054627D711D99E9D007891C7 /* Formatters */ = { + isa = PBXGroup; + children = ( + 054627B811D99D06007891C7 /* PLCrashReportFormatter.h */, + 054627A711D998BB007891C7 /* PLCrashReportTextFormatter.h */, + 054627A811D998BB007891C7 /* PLCrashReportTextFormatter.m */, + ); + name = Formatters; + sourceTree = ""; + }; + 055079D9177C9BAB009D5168 /* Libunwind Regression Tests */ = { + isa = PBXGroup; + children = ( + 05507A0E177CC2C9009D5168 /* README.txt */, + 05507A1F177CCB1C009D5168 /* unwind_test_harness.h */, + 05507A0F177CC456009D5168 /* unwind_test_harness.c */, + 05507A13177CC4D5009D5168 /* unwind_test_x86.S */, + 05507A17177CC50B009D5168 /* unwind_test_x86_64.S */, + 05920D35178B310A001E8975 /* unwind_test_arm.S */, + 053347A517E161CB00C52E50 /* unwind_test_arm64.S */, + 05BB3E1617FA043C00F464E9 /* unwind_test_arm64_frame.S */, + 058484AD1804841100A56049 /* unwind_test_arm64_frameless.S */, + 05507A1B177CC912009D5168 /* unwind_test_x86_64_disable_compact_frame.S */, + 05507A3E178364E8009D5168 /* unwind_test_x86_64_frame.S */, + 05920D2D17848B85001E8975 /* unwind_test_x86_64_frameless.S */, + 05920D311784C806001E8975 /* unwind_test_x86_64_frameless_big.S */, + 05507A4E1784DA8A009D5168 /* unwind_test_x86_64_unusual.S */, + 05C588191788F48400BA118D /* unwind_test_x86_disable_compact_frame.S */, + 05507A521784DEE4009D5168 /* unwind_test_x86_frame.S */, + 05C5880D1788CAA400BA118D /* unwind_test_x86_frameless.S */, + 05C588111788F36800BA118D /* unwind_test_x86_frameless_big.S */, + 05C588151788F3E700BA118D /* unwind_test_x86_unusual.S */, + ); + path = "Libunwind Regression Tests"; + sourceTree = ""; + }; + 05659DE91745593000D2EE21 /* DWARF Encoding */ = { + isa = PBXGroup; + children = ( + 05659DEA17455DD400D2EE21 /* PLCrashAsyncDwarfEncoding.hpp */, + 05659DED17455DED00D2EE21 /* PLCrashAsyncDwarfEncoding.cpp */, + 05E748791760DCCA009B8745 /* Private */, + 05E7483D175A384C009B8745 /* Decoding */, + ); + name = "DWARF Encoding"; + sourceTree = ""; + }; + 05737F5B13A4409C007D2A90 /* Processor Info */ = { + isa = PBXGroup; + children = ( + 05BB83CB1364A77800D53B84 /* PLCrashReportProcessorInfo.h */, + 05BB83CC1364A77800D53B84 /* PLCrashReportProcessorInfo.m */, + ); + name = "Processor Info"; + sourceTree = ""; + }; + 0573B4281681097200395F2A /* Mach Exception Server */ = { + isa = PBXGroup; + children = ( + 0573B42A1681098E00395F2A /* PLCrashMachExceptionServer.h */, + 0573B42B1681098E00395F2A /* PLCrashMachExceptionServer.m */, + 051F067917B6B0D4006D0EFA /* PLCrashMachExceptionPort.h */, + 051F067A17B6B0D4006D0EFA /* PLCrashMachExceptionPort.m */, + 05BEC41517BAF92A0082CBFB /* PLCrashMachExceptionPortSet.h */, + 05BEC41617BAF92A0082CBFB /* PLCrashMachExceptionPortSet.m */, + 0581B520168FDB280098C103 /* mach_exc.defs */, + ); + name = "Mach Exception Server"; + sourceTree = ""; + }; + 05920D1D177B9226001E8975 /* DWARF Unwind */ = { + isa = PBXGroup; + children = ( + 05920D1F177B9257001E8975 /* PLCrashFrameDWARFUnwind.h */, + 05920D1E177B9257001E8975 /* PLCrashFrameDWARFUnwind.cpp */, + ); + name = "DWARF Unwind"; + sourceTree = ""; + }; + 0596674D0EEDEBB1008A0601 /* Frame Walking */ = { + isa = PBXGroup; + children = ( + 059666DA0EEDDFB8008A0601 /* PLCrashFrameWalker.h */, + 059666DB0EEDDFB8008A0601 /* PLCrashFrameWalker.c */, + FCE4576E42A51370AD81A734 /* Stack Frame Unwind */, + 05920D1D177B9226001E8975 /* DWARF Unwind */, + 05F3CD5E16DD6A17007911FB /* Apple Compact Unwind */, + ); + name = "Frame Walking"; + sourceTree = ""; + }; + 059670210EEF5459008A0601 /* Crash Log Writer */ = { + isa = PBXGroup; + children = ( + 059670250EEF6B1A008A0601 /* PLCrashLogWriter.h */, + 059670260EEF6B1A008A0601 /* PLCrashLogWriter.m */, + 05CD36CC0EF25717000FDE88 /* PLCrashLogWriterEncoding.h */, + 05CD36CD0EF25717000FDE88 /* PLCrashLogWriterEncoding.c */, + ); + name = "Crash Log Writer"; + sourceTree = ""; + }; + 059670E30EEFADA6008A0601 /* Dependencies */ = { + isa = PBXGroup; + children = ( + C2C80E032350D23B0084D513 /* protobuf-c */, + ); + path = Dependencies; + sourceTree = ""; + }; + 059672EF0EF0853A008A0601 /* Private API */ = { + isa = PBXGroup; + children = ( + 05BB3E0A17F61A6E00F464E9 /* PLCrashCompatConstants.h */, + 0573B4281681097200395F2A /* Mach Exception Server */, + 05BB84AE1364F5BC00D53B84 /* Signal Handler */, + 05B929E517C9333800B051E3 /* ObjC Exception Handler */, + 05BB84AF1364F5C600D53B84 /* Async-Safe APIs */, + 05BB84B01364F5DB00D53B84 /* Host and Process Info */, + 059670210EEF5459008A0601 /* Crash Log Writer */, + 0596674D0EEDEBB1008A0601 /* Frame Walking */, + 05EB2B1F15B6FE400066EB4D /* Error Reporting */, + ); + name = "Private API"; + sourceTree = ""; + }; + 05A17DC316D7F7FB00888448 /* Thread State */ = { + isa = PBXGroup; + children = ( + 05A17DCC16D7F82700888448 /* PLCrashAsyncThread.h */, + 05A17DC416D7F81600888448 /* PLCrashAsyncThread.c */, + 05EB2B0215B4597B0066EB4D /* Current Thread Trampoline */, + 05A17DE916DBCD5B00888448 /* Platforms */, + ); + name = "Thread State"; + sourceTree = ""; + }; + 05A17DDB16D80C7C00888448 /* Test Thread */ = { + isa = PBXGroup; + children = ( + C29AD6CF2456C94A00360AF7 /* PLCrashTestThread.h */, + C29AD6CD2456C94A00360AF7 /* PLCrashTestThread.m */, + C29AD6CE2456C94A00360AF7 /* PLCrashTestThreadTests.m */, + ); + name = "Test Thread"; + sourceTree = ""; + }; + 05A17DE916DBCD5B00888448 /* Platforms */ = { + isa = PBXGroup; + children = ( + 05A17DEA16DBCDBF00888448 /* PLCrashAsyncThread_x86.h */, + 05A17DF016DBD0AD00888448 /* PLCrashAsyncThread_x86.c */, + 05A17DEB16DBCDBF00888448 /* PLCrashAsyncThread_arm.h */, + 05A17DF516DBD0C200888448 /* PLCrashAsyncThread_arm.c */, + ); + name = Platforms; + sourceTree = ""; + }; + 05A5E28517C0411F008A75E5 /* Linked List */ = { + isa = PBXGroup; + children = ( + 05A5E28717C04188008A75E5 /* PLCrashAsyncLinkedList.hpp */, + ); + name = "Linked List"; + sourceTree = ""; + }; + 05B929E517C9333800B051E3 /* ObjC Exception Handler */ = { + isa = PBXGroup; + children = ( + 05B929E617C9336600B051E3 /* PLCrashUncaughtExceptionHandler.h */, + 05B929E717C9336600B051E3 /* PLCrashUncaughtExceptionHandler.m */, + ); + name = "ObjC Exception Handler"; + sourceTree = ""; + }; + 05BB4C9D166D3BE10075171B /* Mach-O ABI */ = { + isa = PBXGroup; + children = ( + 05F76DD7162F215800A668C7 /* PLCrashAsyncMachOImage.h */, + 05F76DD2162F213E00A668C7 /* PLCrashAsyncMachOImage.c */, + C2198E0E16441D72006EB46A /* PLCrashAsyncMachOString.h */, + C2198E0516441CF5006EB46A /* PLCrashAsyncMachOString.c */, + ); + name = "Mach-O ABI"; + sourceTree = ""; + }; + 05BB4C9E166D409B0075171B /* Obj-C ABI */ = { + isa = PBXGroup; + children = ( + C2198DE1164018B2006EB46A /* PLCrashAsyncObjCSection.h */, + C2198DD81640188C006EB46A /* PLCrashAsyncObjCSection.mm */, + ); + name = "Obj-C ABI"; + sourceTree = ""; + }; + 05BB4C9F166D40AD0075171B /* Symbolication */ = { + isa = PBXGroup; + children = ( + C260228D1642FCAF007FC29F /* PLCrashAsyncSymbolication.h */, + C26022851642FCA6007FC29F /* PLCrashAsyncSymbolication.c */, + ); + name = Symbolication; + sourceTree = ""; + }; + 05BB4CA0166D40B90075171B /* Memory Objects */ = { + isa = PBXGroup; + children = ( + 05DEE6471636E642007E99DC /* PLCrashAsyncMObject.h */, + 05DEE63E1636E62B007E99DC /* PLCrashAsyncMObject.c */, + ); + name = "Memory Objects"; + sourceTree = ""; + }; + 05BB83F91364AD4700D53B84 /* System Info */ = { + isa = PBXGroup; + children = ( + 05F413430EF995C0008050CF /* PLCrashReportSystemInfo.h */, + 05F413440EF995C0008050CF /* PLCrashReportSystemInfo.m */, + ); + name = "System Info"; + sourceTree = ""; + }; + 05BB83FA1364AD5900D53B84 /* Application Info */ = { + isa = PBXGroup; + children = ( + 05F4141C0EF9A6C4008050CF /* PLCrashReportApplicationInfo.h */, + 05F4141D0EF9A6C4008050CF /* PLCrashReportApplicationInfo.m */, + ); + name = "Application Info"; + sourceTree = ""; + }; + 05BB84001364AD8300D53B84 /* Process Info */ = { + isa = PBXGroup; + children = ( + 2D0E10441141F7DC00CE1BD6 /* PLCrashReportProcessInfo.h */, + 2D0E10451141F7DC00CE1BD6 /* PLCrashReportProcessInfo.m */, + ); + name = "Process Info"; + sourceTree = ""; + }; + 05BB84011364AD9600D53B84 /* Thread Info */ = { + isa = PBXGroup; + children = ( + 05F414800EF9BFAC008050CF /* PLCrashReportThreadInfo.h */, + 05F414810EF9BFAC008050CF /* PLCrashReportThreadInfo.m */, + ); + name = "Thread Info"; + sourceTree = ""; + }; + 05BB84021364ADA500D53B84 /* Binary Info */ = { + isa = PBXGroup; + children = ( + 05F4150B0EF9DD9B008050CF /* PLCrashReportBinaryImageInfo.h */, + 05F4150C0EF9DD9B008050CF /* PLCrashReportBinaryImageInfo.m */, + ); + name = "Binary Info"; + sourceTree = ""; + }; + 05BB84031364ADC000D53B84 /* Objective-C Exception Info */ = { + isa = PBXGroup; + children = ( + 05F415510EF9E078008050CF /* PLCrashReportExceptionInfo.h */, + 05F415520EF9E078008050CF /* PLCrashReportExceptionInfo.m */, + ); + name = "Objective-C Exception Info"; + sourceTree = ""; + }; + 05BB84041364ADC900D53B84 /* Signal Info */ = { + isa = PBXGroup; + children = ( + 05E734F50EFAE59C005EDFB7 /* PLCrashReportSignalInfo.h */, + 05E734F60EFAE59C005EDFB7 /* PLCrashReportSignalInfo.m */, + ); + name = "Signal Info"; + sourceTree = ""; + }; + 05BB84301364B70100D53B84 /* Machine Info */ = { + isa = PBXGroup; + children = ( + 05BB83EF1364AD3E00D53B84 /* PLCrashReportMachineInfo.h */, + 05BB83F01364AD3E00D53B84 /* PLCrashReportMachineInfo.m */, + ); + name = "Machine Info"; + sourceTree = ""; + }; + 05BB84AE1364F5BC00D53B84 /* Signal Handler */ = { + isa = PBXGroup; + children = ( + 05CD339A0EE948EB000FDE88 /* PLCrashSignalHandler.h */, + 05CD339B0EE948EB000FDE88 /* PLCrashSignalHandler.mm */, + ); + name = "Signal Handler"; + sourceTree = ""; + }; + 05BB84AF1364F5C600D53B84 /* Async-Safe APIs */ = { + isa = PBXGroup; + children = ( + 059672F00EF08564008A0601 /* PLCrashAsync.h */, + 05CD36410EF24758000FDE88 /* PLCrashAsync.c */, + 05E734300EFAC46D005EDFB7 /* PLCrashAsyncSignalInfo.h */, + 05E734310EFAC46D005EDFB7 /* PLCrashAsyncSignalInfo.c */, + 05BEC42D17BD4F400082CBFB /* PLCrashAsyncMachExceptionInfo.h */, + 05BEC42517BD4F290082CBFB /* PLCrashAsyncMachExceptionInfo.c */, + 052A46BC1363650100987004 /* PLCrashAsyncImageList.h */, + 052A46BD1363650100987004 /* PLCrashAsyncImageList.cpp */, + 05A17DC316D7F7FB00888448 /* Thread State */, + 05A5E28517C0411F008A75E5 /* Linked List */, + 05BB4CA0166D40B90075171B /* Memory Objects */, + 05BB4C9F166D40AD0075171B /* Symbolication */, + 05BB4C9D166D3BE10075171B /* Mach-O ABI */, + 05BB4C9E166D409B0075171B /* Obj-C ABI */, + 05659DE91745593000D2EE21 /* DWARF Encoding */, + 05F3CD7116DFC709007911FB /* Compact Unwind Encoding */, + ); + name = "Async-Safe APIs"; + sourceTree = ""; + }; + 05BB84B01364F5DB00D53B84 /* Host and Process Info */ = { + isa = PBXGroup; + children = ( + 05BB84841364EDF200D53B84 /* PLCrashSysctl.h */, + 05BB84851364EDF200D53B84 /* PLCrashSysctl.c */, + 05102E2217B2B80A00B5D925 /* PLCrashHostInfo.h */, + 05102E2317B2B80A00B5D925 /* PLCrashHostInfo.m */, + 05102E1417B0151000B5D925 /* PLCrashProcessInfo.h */, + 05102E1517B0151000B5D925 /* PLCrashProcessInfo.m */, + ); + name = "Host and Process Info"; + sourceTree = ""; + }; + 05D9E5411676596200B39833 /* Stack Frame Info */ = { + isa = PBXGroup; + children = ( + 05D9E5431676598200B39833 /* PLCrashReportStackFrameInfo.h */, + 05D9E5441676598200B39833 /* PLCrashReportStackFrameInfo.m */, + ); + name = "Stack Frame Info"; + sourceTree = ""; + }; + 05D9E54D167659ED00B39833 /* Register Info */ = { + isa = PBXGroup; + children = ( + 05D9E54E16765A0200B39833 /* PLCrashReportRegisterInfo.h */, + 05D9E54F16765A0200B39833 /* PLCrashReportRegisterInfo.m */, + ); + name = "Register Info"; + sourceTree = ""; + }; + 05D9E55816765CEB00B39833 /* Symbol Info */ = { + isa = PBXGroup; + children = ( + 05D9E55916765D0200B39833 /* PLCrashReportSymbolInfo.h */, + 05D9E55A16765D0200B39833 /* PLCrashReportSymbolInfo.m */, + ); + name = "Symbol Info"; + sourceTree = ""; + }; + 05E7483D175A384C009B8745 /* Decoding */ = { + isa = PBXGroup; + children = ( + 05E7486E1760D8AE009B8745 /* PLCrashAsyncDwarfCIE.hpp */, + 05E748661760D890009B8745 /* PLCrashAsyncDwarfCIE.cpp */, + 05E748591760D62A009B8745 /* PLCrashAsyncDwarfFDE.hpp */, + 05E7485E1760D64D009B8745 /* PLCrashAsyncDwarfFDE.cpp */, + 05E74854175E535C009B8745 /* PLCrashAsyncDwarfPrimitives.hpp */, + 05E7484C175E5349009B8745 /* PLCrashAsyncDwarfPrimitives.cpp */, + 05C76DC7176FBAF300E9B10D /* PLCrashAsyncDwarfCFAState.hpp */, + 05C76DC6176FBAF300E9B10D /* PLCrashAsyncDwarfCFAState.cpp */, + 05E7487A176118C1009B8745 /* PLCrashAsyncDwarfCFAStateEvaluation.cpp */, + 05E74889176135CE009B8745 /* PLCrashAsyncDwarfExpression.hpp */, + 05E7488A176135CE009B8745 /* PLCrashAsyncDwarfExpression.cpp */, + ); + name = Decoding; + sourceTree = ""; + }; + 05E748791760DCCA009B8745 /* Private */ = { + isa = PBXGroup; + children = ( + 05920D1C1774E218001E8975 /* dwarf_private.h */, + 05E748A617616D30009B8745 /* dwarf_stack.hpp */, + 05C76DA5176B8C7000E9B10D /* dwarf_opstream.hpp */, + 05C76DA4176B8C7000E9B10D /* dwarf_opstream.cpp */, + ); + name = Private; + sourceTree = ""; + }; + 05EB2B0215B4597B0066EB4D /* Current Thread Trampoline */ = { + isa = PBXGroup; + children = ( + 05EB2AF615B454DD0066EB4D /* PLCrashAsyncThread_current.S */, + 05EB2AFC15B456750066EB4D /* PLCrashAsyncThread_current.c */, + 05EB2B0115B459770066EB4D /* PLCrashAsyncThread_current_defs.h */, + ); + name = "Current Thread Trampoline"; + sourceTree = ""; + }; + 05EB2B1F15B6FE400066EB4D /* Error Reporting */ = { + isa = PBXGroup; + children = ( + 05EB2B0D15B6FDA70066EB4D /* PLCrashReporterNSError.h */, + 05EB2B0E15B6FDA70066EB4D /* PLCrashReporterNSError.m */, + ); + name = "Error Reporting"; + sourceTree = ""; + }; + 05F3CD5E16DD6A17007911FB /* Apple Compact Unwind */ = { + isa = PBXGroup; + children = ( + 05F3CD6416DD6A58007911FB /* PLCrashFrameCompactUnwind.h */, + 05F3CD5F16DD6A3B007911FB /* PLCrashFrameCompactUnwind.c */, + ); + name = "Apple Compact Unwind"; + sourceTree = ""; + }; + 05F3CD7116DFC709007911FB /* Compact Unwind Encoding */ = { + isa = PBXGroup; + children = ( + 05F3CD7216DFC744007911FB /* PLCrashAsyncCompactUnwindEncoding.h */, + 05F3CD7316DFC744007911FB /* PLCrashAsyncCompactUnwindEncoding.c */, + ); + name = "Compact Unwind Encoding"; + sourceTree = ""; + }; + 05F414280EF9BB19008050CF /* Crash Report */ = { + isa = PBXGroup; + children = ( + 05F411A40EF8DA31008050CF /* PLCrashReport.h */, + 05F411A50EF8DA31008050CF /* PLCrashReport.m */, + C2B72B0D2453496E00D03ABD /* PLCrashReport.pb-c.c */, + C2B72B0E2453496F00D03ABD /* PLCrashReport.pb-c.h */, + 059670C70EEFAC3A008A0601 /* PLCrashReport.proto */, + 05BB83FA1364AD5900D53B84 /* Application Info */, + 05BB84021364ADA500D53B84 /* Binary Info */, + 054627D711D99E9D007891C7 /* Formatters */, + 0513E23117D15E6F00727919 /* Mach Exception Info */, + 05BB84301364B70100D53B84 /* Machine Info */, + 05BB84031364ADC000D53B84 /* Objective-C Exception Info */, + 05BB84001364AD8300D53B84 /* Process Info */, + 05737F5B13A4409C007D2A90 /* Processor Info */, + 05D9E54D167659ED00B39833 /* Register Info */, + 05BB84041364ADC900D53B84 /* Signal Info */, + 05D9E5411676596200B39833 /* Stack Frame Info */, + 05D9E55816765CEB00B39833 /* Symbol Info */, + 05BB83F91364AD4700D53B84 /* System Info */, + 05BB84011364AD9600D53B84 /* Thread Info */, + ); + name = "Crash Report"; + sourceTree = ""; + }; + 0867D691FE84028FC02AAC07 /* CrashReporter */ = { + isa = PBXGroup; + children = ( + C2B90DA32456FE8200834AFB /* CHANGELOG.md */, + C2B90DA22456FE8200834AFB /* README.md */, + C2B90DA12456FE8200834AFB /* PLCrashReporter.podspec */, + 08FB77AEFE84172EC02AAC07 /* Source */, + C29AD6C02456C61E00360AF7 /* Tests */, + 059670E30EEFADA6008A0601 /* Dependencies */, + 32C88DFF0371C24200C91783 /* Other Sources */, + 089C1665FE841158C02AAC07 /* Resources */, + C2B90DA4245707BD00834AFB /* Scripts */, + F81CF5EE235A0AE20007FA54 /* Frameworks */, + 034768DFFF38A50411DB9C8B /* Products */, + ); + indentWidth = 4; + name = CrashReporter; + sourceTree = ""; + tabWidth = 4; + usesTabs = 0; + }; + 089C1665FE841158C02AAC07 /* Resources */ = { + isa = PBXGroup; + children = ( + 05CD33520EE9457D000FDE88 /* CrashReporter.exp */, + C2620D482451D28B00B11E68 /* CrashReporter.modulemap */, + C2F7F26A2451F796002BD8BF /* Default-568h@2x.png */, + C2F7F2302451F081002BD8BF /* DemoCrash-iOS-Info.plist */, + 05F40CE90EF7AB80008050CF /* DemoCrash-macOS-Info.plist */, + C2F7F22F2451F081002BD8BF /* DemoCrash-tvOS-Info.plist */, + C2DCD87724657B24007322C5 /* DemoCrash-iOS.entitlements */, + 050DE28D0F61BB1D00152ED3 /* fuzz_report.plcrash */, + 8DC2EF5A0486A6940098B216 /* Info.plist */, + 05F3CD6C16DE7625007911FB /* Tests */, + 05CD32AF0EE94086000FDE88 /* Tests-Info.plist */, + ); + path = Resources; + sourceTree = ""; + }; + 08FB77AEFE84172EC02AAC07 /* Source */ = { + isa = PBXGroup; + children = ( + 05CD31890EE93A90000FDE88 /* CrashReporter.h */, + 05CD318A0EE93A90000FDE88 /* CrashReporter.m */, + C2F0AC9F24AB7C28004890EC /* CrashReporterFramework.m */, + 054F51070EEC73C80034B184 /* PLCrashReporter.h */, + 05F40ACA0EF7379F008050CF /* PLCrashReporter.m */, + 05BEC43417BF1CB10082CBFB /* PLCrashReporterConfig.h */, + 05BEC43517BF1CB10082CBFB /* PLCrashReporterConfig.m */, + 05A5E28017A82751008A75E5 /* PLCrashMacros.h */, + 05A2077215AB30C9001E3EFC /* PLCrashNamespace.h */, + 05A2B3FF1795BA4100934198 /* PLCrashFeatureConfig.h */, + 05F414280EF9BB19008050CF /* Crash Report */, + 059672EF0EF0853A008A0601 /* Private API */, + ); + path = Source; + sourceTree = ""; + }; + 32C88DFF0371C24200C91783 /* Other Sources */ = { + isa = PBXGroup; + children = ( + C29AD6C22456C69000360AF7 /* Crash Demo */, + C29AD6C62456C69000360AF7 /* Fuzz */, + C29AD6C42456C69000360AF7 /* plcrashutil */, + ); + path = "Other Sources"; + sourceTree = ""; + }; + C29AD6C02456C61E00360AF7 /* Tests */ = { + isa = PBXGroup; + children = ( + 055079D9177C9BAB009D5168 /* Libunwind Regression Tests */, + 05A17DDB16D80C7C00888448 /* Test Thread */, + 052DC863175553DC004335FE /* dwarf_encoding_test.h */, + 05C76DB1176B946E00E9B10D /* dwarf_opstream_tests.mm */, + 05E748B217616D6B009B8745 /* dwarf_stack_tests.mm */, + 05F3CD8016DFC78D007911FB /* PLCrashAsyncCompactUnwindEncodingTests.m */, + 05E74885176118F8009B8745 /* PLCrashAsyncDwarfCFAStateEvaluationTests.mm */, + 05C76DD3176FBC1E00E9B10D /* PLCrashAsyncDwarfCFAStateTests.mm */, + 05E748711760DBBE009B8745 /* PLCrashAsyncDwarfCIETests.mm */, + C2BBCD7F2456E03D00F9E820 /* PLCrashAsyncDwarfEncodingTests.mm */, + 05E7489417613AF0009B8745 /* PLCrashAsyncDwarfExpressionTests.mm */, + 05E748751760DBD0009B8745 /* PLCrashAsyncDwarfFDETests.mm */, + 05E74855175E5370009B8745 /* PLCrashAsyncDwarfPrimitivesTests.mm */, + 052A46F713637DE000987004 /* PLCrashAsyncImageListTests.m */, + C2BBCD822456E03D00F9E820 /* PLCrashAsyncLinkedListTests.mm */, + 05BEC43017BD4F540082CBFB /* PLCrashAsyncMachExceptionInfoTests.m */, + 05F76DD9162F238E00A668C7 /* PLCrashAsyncMachOImageTests.m */, + C2BBCD842456E03D00F9E820 /* PLCrashAsyncMachOStringTests.m */, + 05DEE64A1636E721007E99DC /* PLCrashAsyncMObjectTests.m */, + C2198DE316402B8A006EB46A /* PLCrashAsyncObjCSectionTests.m */, + 05E734830EFAD83B005EDFB7 /* PLCrashAsyncSignalInfoTests.m */, + C260228F1642FE9B007FC29F /* PLCrashAsyncSymbolicationTests.m */, + 05CD36480EF247A9000FDE88 /* PLCrashAsyncTests.m */, + 05A17DD216D8080A00888448 /* PLCrashAsyncThreadTests.m */, + 05F3CD6816DD6A7A007911FB /* PLCrashFrameCompactUnwindTests.m */, + 05920D29177B92E7001E8975 /* PLCrashFrameDWARFUnwindTests.m */, + C2BBCD812456E03D00F9E820 /* PLCrashFrameStackUnwindTests.m */, + 059666E20EEDDFCC008A0601 /* PLCrashFrameWalkerTests.m */, + 05102E2C17B2B82000B5D925 /* PLCrashHostInfoTests.m */, + 052951E91696965E006EDA8A /* PLCrashLogWriterEncodingTests.m */, + C29AD6D62456C9B800360AF7 /* PLCrashLogWriterEncodingTests.pb-c.c */, + C2B72B15245349C500D03ABD /* PLCrashLogWriterEncodingTests.pb-c.h */, + 052951EE1696A461006EDA8A /* PLCrashLogWriterEncodingTests.proto */, + 0596702D0EEF6B51008A0601 /* PLCrashLogWriterTests.m */, + 05BEC41F17BAF95C0082CBFB /* PLCrashMachExceptionPortSetTests.m */, + C2BBCD7E2456E03D00F9E820 /* PLCrashMachExceptionPortTests.m */, + C2BBCD802456E03D00F9E820 /* PLCrashMachExceptionServerTests.m */, + 05102E1C17B0152B00B5D925 /* PLCrashProcessInfoTests.m */, + 05EB2B1B15B6FE280066EB4D /* PLCrashReporterNSErrorTests.m */, + 05F40ADD0EF73A39008050CF /* PLCrashReporterTests.m */, + 05F411AC0EF8DE68008050CF /* PLCrashReportTests.m */, + 05CD33A20EE94931000FDE88 /* PLCrashSignalHandlerTests.m */, + C2BBCD832456E03D00F9E820 /* PLCrashSysctlTests.m */, + 05659DF7174D2E1200D2EE21 /* PLCrashTestCase.h */, + 05659DF8174D2E1200D2EE21 /* PLCrashTestCase.m */, + 05B929F017C9337D00B051E3 /* PLCrashUncaughtExceptionHandlerTests.m */, + C29AD6C12456C61E00360AF7 /* SenTestCompat.h */, + ); + path = Tests; + sourceTree = ""; + }; + C29AD6C22456C69000360AF7 /* Crash Demo */ = { + isa = PBXGroup; + children = ( + C29AD6C32456C69000360AF7 /* main.m */, + ); + path = "Crash Demo"; + sourceTree = ""; + }; + C29AD6C42456C69000360AF7 /* plcrashutil */ = { + isa = PBXGroup; + children = ( + C29AD6C52456C69000360AF7 /* main.m */, + ); + path = plcrashutil; + sourceTree = ""; + }; + C29AD6C62456C69000360AF7 /* Fuzz */ = { + isa = PBXGroup; + children = ( + C29AD6C72456C69000360AF7 /* fuzz-main.m */, + ); + path = Fuzz; + sourceTree = ""; + }; + C2B90DA4245707BD00834AFB /* Scripts */ = { + isa = PBXGroup; + children = ( + C2C74A882535CD3A00313817 /* build-framework.sh */, + C2C74A852535CD3A00313817 /* combine-frameworks.sh */, + C2C74E1E25385F8300313817 /* combine-libraries.sh */, + C2C74A862535CD3A00313817 /* combine-xcframework.sh */, + C2C74E1F25385F8300313817 /* copy-products.sh */, + C2B90DA5245714E900834AFB /* generate-documentation.sh */, + C2B90DA7245714E900834AFB /* verify-modifications.sh */, + ); + path = Scripts; + sourceTree = ""; + }; + C2C80E032350D23B0084D513 /* protobuf-c */ = { + isa = PBXGroup; + children = ( + C2B72B2524534EE700D03ABD /* protobuf-c.c */, + C2B72B2424534EE700D03ABD /* protobuf-c.h */, + ); + name = "protobuf-c"; + path = "protobuf-c/protobuf-c"; + sourceTree = ""; + }; + F81CF5EE235A0AE20007FA54 /* Frameworks */ = { + isa = PBXGroup; + children = ( + C2F7F2BB2451FE1F002BD8BF /* Foundation.framework */, + C2620D452451CF9400B11E68 /* CoreServices.framework */, + C2620D432451CF8B00B11E68 /* Foundation.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + FCE4576E42A51370AD81A734 /* Stack Frame Unwind */ = { + isa = PBXGroup; + children = ( + FCE4522F86AC61C08E9DCC17 /* PLCrashFrameStackUnwind.h */, + FCE45837C8C773EFFD15C52B /* PLCrashFrameStackUnwind.c */, + ); + name = "Stack Frame Unwind"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 058812B41040582D009128FB /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 05EC51D7105316E900DB9D39 /* CrashReporter.h in Headers */, + 05A04D8D15AB38CD0011CFA4 /* PLCrashNamespace.h in Headers */, + 05EC51D9105316E900DB9D39 /* PLCrashReporter.h in Headers */, + 05EC51DE105316E900DB9D39 /* PLCrashReport.h in Headers */, + 05EC51E0105316E900DB9D39 /* PLCrashReportApplicationInfo.h in Headers */, + 0573B4481681107F00395F2A /* PLCrashReportRegisterInfo.h in Headers */, + 05EC51E2105316E900DB9D39 /* PLCrashReportBinaryImageInfo.h in Headers */, + 0573B4491681108200395F2A /* PLCrashReportStackFrameInfo.h in Headers */, + 05EC51E3105316E900DB9D39 /* PLCrashReportExceptionInfo.h in Headers */, + 05EC51E1105316E900DB9D39 /* PLCrashReportThreadInfo.h in Headers */, + 05EC51DF105316E900DB9D39 /* PLCrashReportSystemInfo.h in Headers */, + 0573B44A1681108500395F2A /* PLCrashReportSymbolInfo.h in Headers */, + 2D0E104E1141F7DC00CE1BD6 /* PLCrashReportProcessInfo.h in Headers */, + 05EC51E5105316E900DB9D39 /* PLCrashReportSignalInfo.h in Headers */, + 0527063417CCF31400E6A5D8 /* PLCrashFeatureConfig.h in Headers */, + 05B69E1417CE6271001807C9 /* PLCrashReporterConfig.h in Headers */, + 0513E23C17D15EE500727919 /* PLCrashReportMachExceptionInfo.h in Headers */, + 05A5E28217A82751008A75E5 /* PLCrashMacros.h in Headers */, + 054627AD11D998BB007891C7 /* PLCrashReportTextFormatter.h in Headers */, + 054627BD11D99D06007891C7 /* PLCrashReportFormatter.h in Headers */, + 05771CE313683EDD001DE4B1 /* PLCrashReportMachineInfo.h in Headers */, + 05771CE213683ED4001DE4B1 /* PLCrashReportProcessorInfo.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05CD314E0EE936A9000FDE88 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 05CD318D0EE93A90000FDE88 /* CrashReporter.h in Headers */, + C2F7F29D2451FB32002BD8BF /* PLCrashAsyncMachOString.h in Headers */, + C2F7F2972451FB29002BD8BF /* PLCrashAsyncSymbolication.h in Headers */, + 05CD339C0EE948EB000FDE88 /* PLCrashSignalHandler.h in Headers */, + C2F7F27B2451FAB9002BD8BF /* PLCrashFeatureConfig.h in Headers */, + C2F7F26D2451FA64002BD8BF /* PLCrashAsyncMachExceptionInfo.h in Headers */, + C2F7F2A92451FB5D002BD8BF /* PLCrashAsyncDwarfCIE.hpp in Headers */, + 059666E00EEDDFB8008A0601 /* PLCrashFrameWalker.h in Headers */, + 059670270EEF6B1A008A0601 /* PLCrashLogWriter.h in Headers */, + 05CD36D30EF25717000FDE88 /* PLCrashLogWriterEncoding.h in Headers */, + C2F7F2A02451FB36002BD8BF /* PLCrashAsyncObjCSection.h in Headers */, + 05F411A80EF8DA31008050CF /* PLCrashReport.h in Headers */, + C2F7F2742451FAAE002BD8BF /* PLCrashMacros.h in Headers */, + C2F7F2BA2451FC78002BD8BF /* PLCrashFrameCompactUnwind.h in Headers */, + 05F413470EF995C0008050CF /* PLCrashReportSystemInfo.h in Headers */, + 05F414220EF9A6C4008050CF /* PLCrashReportApplicationInfo.h in Headers */, + 05F414860EF9BFAC008050CF /* PLCrashReportThreadInfo.h in Headers */, + C2F7F2832451FAE4002BD8BF /* PLCrashCompatConstants.h in Headers */, + 05F415110EF9DD9B008050CF /* PLCrashReportBinaryImageInfo.h in Headers */, + C2F7F2712451FAA2002BD8BF /* PLCrashReporter.h in Headers */, + 05F415570EF9E078008050CF /* PLCrashReportExceptionInfo.h in Headers */, + 05E734340EFAC46D005EDFB7 /* PLCrashAsyncSignalInfo.h in Headers */, + 05E734F90EFAE59C005EDFB7 /* PLCrashReportSignalInfo.h in Headers */, + 2D0E104A1141F7DC00CE1BD6 /* PLCrashReportProcessInfo.h in Headers */, + 054627AB11D998BB007891C7 /* PLCrashReportTextFormatter.h in Headers */, + C2F7F2942451FB23002BD8BF /* PLCrashAsyncMObject.h in Headers */, + 054627B911D99D06007891C7 /* PLCrashReportFormatter.h in Headers */, + 052A46BE1363650100987004 /* PLCrashAsyncImageList.h in Headers */, + 05BB83CF1364A77800D53B84 /* PLCrashReportProcessorInfo.h in Headers */, + 05BB83F31364AD3E00D53B84 /* PLCrashReportMachineInfo.h in Headers */, + C2F7F2A62451FB43002BD8BF /* dwarf_private.h in Headers */, + 05BB84881364EDF200D53B84 /* PLCrashSysctl.h in Headers */, + 05EB2B1115B6FDA80066EB4D /* PLCrashReporterNSError.h in Headers */, + 05D9E5471676598200B39833 /* PLCrashReportStackFrameInfo.h in Headers */, + 05D9E55216765A0200B39833 /* PLCrashReportRegisterInfo.h in Headers */, + C2B72B2724534EE700D03ABD /* protobuf-c.h in Headers */, + C2F7F2912451FB17002BD8BF /* PLCrashAsyncThread_arm.h in Headers */, + C2F7F2882451FAFE002BD8BF /* PLCrashAsync.h in Headers */, + 05D9E55D16765D0200B39833 /* PLCrashReportSymbolInfo.h in Headers */, + 0573B42E1681098E00395F2A /* PLCrashMachExceptionServer.h in Headers */, + C2F7F28E2451FB10002BD8BF /* PLCrashAsyncThread_x86.h in Headers */, + FCE4586A7041D332D1025F37 /* PLCrashFrameStackUnwind.h in Headers */, + 05A17DCF16D7F82700888448 /* PLCrashAsyncThread.h in Headers */, + C2F7F2772451FAB3002BD8BF /* PLCrashNamespace.h in Headers */, + 05F3CD7616DFC744007911FB /* PLCrashAsyncCompactUnwindEncoding.h in Headers */, + 05E7485C1760D62A009B8745 /* PLCrashAsyncDwarfFDE.hpp in Headers */, + 05E7488C176135CF009B8745 /* PLCrashAsyncDwarfExpression.hpp in Headers */, + C2F7F28B2451FB0B002BD8BF /* PLCrashAsyncThread_current_defs.h in Headers */, + 05E748B017616D30009B8745 /* dwarf_stack.hpp in Headers */, + 05C76DAF176B8C7000E9B10D /* dwarf_opstream.hpp in Headers */, + 05C76DD1176FBAF300E9B10D /* PLCrashAsyncDwarfCFAState.hpp in Headers */, + 05920D27177B9257001E8975 /* PLCrashFrameDWARFUnwind.h in Headers */, + C2F7F2AD2451FB69002BD8BF /* PLCrashAsyncDwarfPrimitives.hpp in Headers */, + 05102E1717B0151000B5D925 /* PLCrashProcessInfo.h in Headers */, + 05102E2617B2B80A00B5D925 /* PLCrashHostInfo.h in Headers */, + C2F7F29A2451FB2E002BD8BF /* PLCrashAsyncMachOImage.h in Headers */, + 051F067C17B6B0D4006D0EFA /* PLCrashMachExceptionPort.h in Headers */, + C2B72B132453496F00D03ABD /* PLCrashReport.pb-c.h in Headers */, + 05BEC41917BAF92A0082CBFB /* PLCrashMachExceptionPortSet.h in Headers */, + C2F7F2A32451FB3A002BD8BF /* PLCrashAsyncDwarfEncoding.hpp in Headers */, + 05BEC43817BF1CB10082CBFB /* PLCrashReporterConfig.h in Headers */, + 05A5E29117C04188008A75E5 /* PLCrashAsyncLinkedList.hpp in Headers */, + 05B929EA17C9336600B051E3 /* PLCrashUncaughtExceptionHandler.h in Headers */, + 0513E23617D15ED400727919 /* PLCrashReportMachExceptionInfo.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05E731EF0EFA1AAB005EDFB7 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + C2F7F2B52451FC69002BD8BF /* PLCrashFrameWalker.h in Headers */, + C2F7F2AB2451FB63002BD8BF /* PLCrashAsyncDwarfFDE.hpp in Headers */, + C2F7F2B02451FB80002BD8BF /* PLCrashProcessInfo.h in Headers */, + 05E734380EFAC46D005EDFB7 /* PLCrashAsyncSignalInfo.h in Headers */, + C2F7F26F2451FA9C002BD8BF /* CrashReporter.h in Headers */, + 05E734FD0EFAE59C005EDFB7 /* PLCrashReportSignalInfo.h in Headers */, + 2D0E10481141F7DC00CE1BD6 /* PLCrashReportProcessInfo.h in Headers */, + C2F7F26C2451FA63002BD8BF /* PLCrashAsyncMachExceptionInfo.h in Headers */, + C2F7F2722451FAA3002BD8BF /* PLCrashReporter.h in Headers */, + 054627B111D998BB007891C7 /* PLCrashReportTextFormatter.h in Headers */, + C2F7F2872451FAFE002BD8BF /* PLCrashAsync.h in Headers */, + C2F7F29E2451FB33002BD8BF /* PLCrashAsyncMachOString.h in Headers */, + C2F7F27C2451FABE002BD8BF /* PLCrashReport.h in Headers */, + C2F7F2B92451FC78002BD8BF /* PLCrashFrameCompactUnwind.h in Headers */, + C2F7F2752451FAAF002BD8BF /* PLCrashMacros.h in Headers */, + C2F7F2AF2451FB71002BD8BF /* PLCrashAsyncDwarfExpression.hpp in Headers */, + 054627BA11D99D06007891C7 /* PLCrashReportFormatter.h in Headers */, + 052A46C21363650100987004 /* PLCrashAsyncImageList.h in Headers */, + 05BB83D31364A77800D53B84 /* PLCrashReportProcessorInfo.h in Headers */, + C2F7F2802451FAD7002BD8BF /* PLCrashReportSystemInfo.h in Headers */, + 05BB83F71364AD3E00D53B84 /* PLCrashReportMachineInfo.h in Headers */, + C2F7F2812451FADB002BD8BF /* PLCrashReportThreadInfo.h in Headers */, + 05BB848C1364EDF200D53B84 /* PLCrashSysctl.h in Headers */, + C2F7F2B22451FC5E002BD8BF /* PLCrashLogWriterEncoding.h in Headers */, + C2F7F2962451FB29002BD8BF /* PLCrashAsyncSymbolication.h in Headers */, + 05EB2B0F15B6FDA80066EB4D /* PLCrashReporterNSError.h in Headers */, + 05D9E5451676598200B39833 /* PLCrashReportStackFrameInfo.h in Headers */, + C2F7F2852451FAED002BD8BF /* PLCrashMachExceptionPort.h in Headers */, + 05D9E55016765A0200B39833 /* PLCrashReportRegisterInfo.h in Headers */, + C2F7F2A52451FB43002BD8BF /* dwarf_private.h in Headers */, + 05D9E55B16765D0200B39833 /* PLCrashReportSymbolInfo.h in Headers */, + C2F7F2AC2451FB68002BD8BF /* PLCrashAsyncDwarfPrimitives.hpp in Headers */, + 0573B42C1681098E00395F2A /* PLCrashMachExceptionServer.h in Headers */, + FCE45B4FD545A258E0292F25 /* PLCrashFrameStackUnwind.h in Headers */, + 05A17DCD16D7F82700888448 /* PLCrashAsyncThread.h in Headers */, + C2B72B2624534EE700D03ABD /* protobuf-c.h in Headers */, + 05F3CD7416DFC744007911FB /* PLCrashAsyncCompactUnwindEncoding.h in Headers */, + C2F7F28F2451FB11002BD8BF /* PLCrashAsyncThread_x86.h in Headers */, + C2F7F27F2451FACC002BD8BF /* PLCrashReportExceptionInfo.h in Headers */, + C2F7F2922451FB18002BD8BF /* PLCrashAsyncThread_arm.h in Headers */, + C2F7F2862451FAF4002BD8BF /* PLCrashSignalHandler.h in Headers */, + 05E748AE17616D30009B8745 /* dwarf_stack.hpp in Headers */, + C2F7F2782451FAB4002BD8BF /* PLCrashNamespace.h in Headers */, + C2F7F2AA2451FB5E002BD8BF /* PLCrashAsyncDwarfCIE.hpp in Headers */, + C2F7F2B62451FC72002BD8BF /* PLCrashFrameDWARFUnwind.h in Headers */, + C2F7F28A2451FB0A002BD8BF /* PLCrashAsyncThread_current_defs.h in Headers */, + C2F7F2842451FAE5002BD8BF /* PLCrashCompatConstants.h in Headers */, + C2F7F2B12451FB84002BD8BF /* PLCrashLogWriter.h in Headers */, + 05C76DAD176B8C7000E9B10D /* dwarf_opstream.hpp in Headers */, + C2F7F2992451FB2D002BD8BF /* PLCrashAsyncMachOImage.h in Headers */, + 05C76DCF176FBAF300E9B10D /* PLCrashAsyncDwarfCFAState.hpp in Headers */, + 05102E2417B2B80A00B5D925 /* PLCrashHostInfo.h in Headers */, + 05BEC41717BAF92A0082CBFB /* PLCrashMachExceptionPortSet.h in Headers */, + C2F7F29F2451FB35002BD8BF /* PLCrashAsyncObjCSection.h in Headers */, + C2F7F2A42451FB3B002BD8BF /* PLCrashAsyncDwarfEncoding.hpp in Headers */, + C2F7F27A2451FAB9002BD8BF /* PLCrashFeatureConfig.h in Headers */, + 05BEC43617BF1CB10082CBFB /* PLCrashReporterConfig.h in Headers */, + C2B72B122453496F00D03ABD /* PLCrashReport.pb-c.h in Headers */, + C2F7F2952451FB24002BD8BF /* PLCrashAsyncMObject.h in Headers */, + C2F7F27E2451FAC5002BD8BF /* PLCrashReportBinaryImageInfo.h in Headers */, + 05A5E28F17C04188008A75E5 /* PLCrashAsyncLinkedList.hpp in Headers */, + 05B929E817C9336600B051E3 /* PLCrashUncaughtExceptionHandler.h in Headers */, + 0513E23417D15ED400727919 /* PLCrashReportMachExceptionInfo.h in Headers */, + C2F7F27D2451FAC1002BD8BF /* PLCrashReportApplicationInfo.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8064D7AE1C4D22D8005A8B4C /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 8064D7AF1C4D22D8005A8B4C /* CrashReporter.h in Headers */, + C2F7F29C2451FB32002BD8BF /* PLCrashAsyncMachOString.h in Headers */, + C2F7F2982451FB2A002BD8BF /* PLCrashAsyncSymbolication.h in Headers */, + 8064D7B01C4D22D8005A8B4C /* PLCrashSignalHandler.h in Headers */, + C2F7F2792451FAB8002BD8BF /* PLCrashFeatureConfig.h in Headers */, + C2F7F26E2451FA65002BD8BF /* PLCrashAsyncMachExceptionInfo.h in Headers */, + C2F7F2A82451FB5D002BD8BF /* PLCrashAsyncDwarfCIE.hpp in Headers */, + 8064D7B11C4D22D8005A8B4C /* PLCrashFrameWalker.h in Headers */, + 8064D7B21C4D22D8005A8B4C /* PLCrashLogWriter.h in Headers */, + 8064D7B31C4D22D8005A8B4C /* PLCrashLogWriterEncoding.h in Headers */, + C2F7F2A12451FB36002BD8BF /* PLCrashAsyncObjCSection.h in Headers */, + 8064D7B41C4D22D8005A8B4C /* PLCrashReport.h in Headers */, + C2F7F2732451FAAE002BD8BF /* PLCrashMacros.h in Headers */, + C2F7F2B72451FC76002BD8BF /* PLCrashFrameCompactUnwind.h in Headers */, + 8064D7B51C4D22D8005A8B4C /* PLCrashReportSystemInfo.h in Headers */, + 8064D7B61C4D22D8005A8B4C /* PLCrashReportApplicationInfo.h in Headers */, + 8064D7B71C4D22D8005A8B4C /* PLCrashReportThreadInfo.h in Headers */, + C2F7F2822451FAE2002BD8BF /* PLCrashCompatConstants.h in Headers */, + 8064D7B81C4D22D8005A8B4C /* PLCrashReportBinaryImageInfo.h in Headers */, + C2F7F2702451FAA2002BD8BF /* PLCrashReporter.h in Headers */, + 8064D7B91C4D22D8005A8B4C /* PLCrashReportExceptionInfo.h in Headers */, + 8064D7BA1C4D22D8005A8B4C /* PLCrashAsyncSignalInfo.h in Headers */, + 8064D7BB1C4D22D8005A8B4C /* PLCrashReportSignalInfo.h in Headers */, + 8064D7BC1C4D22D8005A8B4C /* PLCrashReportProcessInfo.h in Headers */, + 8064D7BD1C4D22D8005A8B4C /* PLCrashReportTextFormatter.h in Headers */, + C2F7F2932451FB22002BD8BF /* PLCrashAsyncMObject.h in Headers */, + 8064D7BE1C4D22D8005A8B4C /* PLCrashReportFormatter.h in Headers */, + 8064D7BF1C4D22D8005A8B4C /* PLCrashAsyncImageList.h in Headers */, + 8064D7C01C4D22D8005A8B4C /* PLCrashReportProcessorInfo.h in Headers */, + 8064D7C11C4D22D8005A8B4C /* PLCrashReportMachineInfo.h in Headers */, + C2F7F2A72451FB43002BD8BF /* dwarf_private.h in Headers */, + 8064D7C21C4D22D8005A8B4C /* PLCrashSysctl.h in Headers */, + 8064D7C31C4D22D8005A8B4C /* PLCrashReporterNSError.h in Headers */, + 8064D7C41C4D22D8005A8B4C /* PLCrashReportStackFrameInfo.h in Headers */, + 8064D7C51C4D22D8005A8B4C /* PLCrashReportRegisterInfo.h in Headers */, + C2B72B2824534EE700D03ABD /* protobuf-c.h in Headers */, + C2F7F2902451FB17002BD8BF /* PLCrashAsyncThread_arm.h in Headers */, + C2F7F2892451FAFF002BD8BF /* PLCrashAsync.h in Headers */, + 8064D7C61C4D22D8005A8B4C /* PLCrashReportSymbolInfo.h in Headers */, + 8064D7C71C4D22D8005A8B4C /* PLCrashMachExceptionServer.h in Headers */, + C2F7F28D2451FB10002BD8BF /* PLCrashAsyncThread_x86.h in Headers */, + 8064D7C81C4D22D8005A8B4C /* PLCrashFrameStackUnwind.h in Headers */, + 8064D7C91C4D22D8005A8B4C /* PLCrashAsyncThread.h in Headers */, + C2F7F2762451FAB3002BD8BF /* PLCrashNamespace.h in Headers */, + 8064D7CA1C4D22D8005A8B4C /* PLCrashAsyncCompactUnwindEncoding.h in Headers */, + 8064D7CB1C4D22D8005A8B4C /* PLCrashAsyncDwarfFDE.hpp in Headers */, + 8064D7CC1C4D22D8005A8B4C /* PLCrashAsyncDwarfExpression.hpp in Headers */, + C2F7F28C2451FB0B002BD8BF /* PLCrashAsyncThread_current_defs.h in Headers */, + 8064D7CD1C4D22D8005A8B4C /* dwarf_stack.hpp in Headers */, + 8064D7CE1C4D22D8005A8B4C /* dwarf_opstream.hpp in Headers */, + 8064D7CF1C4D22D8005A8B4C /* PLCrashAsyncDwarfCFAState.hpp in Headers */, + 8064D7D01C4D22D8005A8B4C /* PLCrashFrameDWARFUnwind.h in Headers */, + C2F7F2AE2451FB6A002BD8BF /* PLCrashAsyncDwarfPrimitives.hpp in Headers */, + 8064D7D11C4D22D8005A8B4C /* PLCrashProcessInfo.h in Headers */, + 8064D7D21C4D22D8005A8B4C /* PLCrashHostInfo.h in Headers */, + C2F7F29B2451FB2E002BD8BF /* PLCrashAsyncMachOImage.h in Headers */, + 8064D7D31C4D22D8005A8B4C /* PLCrashMachExceptionPort.h in Headers */, + C2B72B142453496F00D03ABD /* PLCrashReport.pb-c.h in Headers */, + 8064D7D41C4D22D8005A8B4C /* PLCrashMachExceptionPortSet.h in Headers */, + C2F7F2A22451FB3A002BD8BF /* PLCrashAsyncDwarfEncoding.hpp in Headers */, + 8064D7D51C4D22D8005A8B4C /* PLCrashReporterConfig.h in Headers */, + 8064D7D61C4D22D8005A8B4C /* PLCrashAsyncLinkedList.hpp in Headers */, + 8064D7D71C4D22D8005A8B4C /* PLCrashUncaughtExceptionHandler.h in Headers */, + 8064D7D81C4D22D8005A8B4C /* PLCrashReportMachExceptionInfo.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8064D8901C4D22E5005A8B4C /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 8064D8911C4D22E5005A8B4C /* CrashReporter.h in Headers */, + 8064D8921C4D22E5005A8B4C /* PLCrashNamespace.h in Headers */, + 8064D8941C4D22E5005A8B4C /* PLCrashReporter.h in Headers */, + 8064D8981C4D22E5005A8B4C /* PLCrashReport.h in Headers */, + 8064D8991C4D22E5005A8B4C /* PLCrashReportApplicationInfo.h in Headers */, + 8064D89A1C4D22E5005A8B4C /* PLCrashReportRegisterInfo.h in Headers */, + 8064D89B1C4D22E5005A8B4C /* PLCrashReportBinaryImageInfo.h in Headers */, + 8064D89C1C4D22E5005A8B4C /* PLCrashReportStackFrameInfo.h in Headers */, + 8064D89D1C4D22E5005A8B4C /* PLCrashReportExceptionInfo.h in Headers */, + 8064D89E1C4D22E5005A8B4C /* PLCrashReportThreadInfo.h in Headers */, + 8064D89F1C4D22E5005A8B4C /* PLCrashReportSystemInfo.h in Headers */, + 8064D8A11C4D22E5005A8B4C /* PLCrashReportSymbolInfo.h in Headers */, + 8064D8A21C4D22E5005A8B4C /* PLCrashReportProcessInfo.h in Headers */, + 8064D8A31C4D22E5005A8B4C /* PLCrashReportSignalInfo.h in Headers */, + 8064D8A41C4D22E5005A8B4C /* PLCrashFeatureConfig.h in Headers */, + 8064D8A51C4D22E5005A8B4C /* PLCrashReporterConfig.h in Headers */, + 8064D8A61C4D22E5005A8B4C /* PLCrashReportMachExceptionInfo.h in Headers */, + 8064D8A71C4D22E5005A8B4C /* PLCrashMacros.h in Headers */, + 8064D8A81C4D22E5005A8B4C /* PLCrashReportTextFormatter.h in Headers */, + 8064D8A91C4D22E5005A8B4C /* PLCrashReportFormatter.h in Headers */, + 8064D8AA1C4D22E5005A8B4C /* PLCrashReportMachineInfo.h in Headers */, + 8064D8AB1C4D22E5005A8B4C /* PLCrashReportProcessorInfo.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8DC2EF500486A6940098B216 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 05CD318B0EE93A90000FDE88 /* CrashReporter.h in Headers */, + 05A04D8C15AB38C10011CFA4 /* PLCrashNamespace.h in Headers */, + 05A5E28117A82751008A75E5 /* PLCrashMacros.h in Headers */, + 054F51080EEC73C80034B184 /* PLCrashReporter.h in Headers */, + 05F411AA0EF8DA31008050CF /* PLCrashReport.h in Headers */, + 05F413490EF995C0008050CF /* PLCrashReportSystemInfo.h in Headers */, + 05F414200EF9A6C4008050CF /* PLCrashReportApplicationInfo.h in Headers */, + 05C5881D178B898C00BA118D /* PLCrashReportStackFrameInfo.h in Headers */, + 05C5881E178B89A300BA118D /* PLCrashReportSymbolInfo.h in Headers */, + 05F414840EF9BFAC008050CF /* PLCrashReportThreadInfo.h in Headers */, + 05C5881F178B89A800BA118D /* PLCrashReportRegisterInfo.h in Headers */, + 05F4150F0EF9DD9B008050CF /* PLCrashReportBinaryImageInfo.h in Headers */, + 05F415550EF9E078008050CF /* PLCrashReportExceptionInfo.h in Headers */, + 05E734FB0EFAE59C005EDFB7 /* PLCrashReportSignalInfo.h in Headers */, + 05BEC43717BF1CB10082CBFB /* PLCrashReporterConfig.h in Headers */, + 0527063317CCF31100E6A5D8 /* PLCrashFeatureConfig.h in Headers */, + 0513E23517D15ED400727919 /* PLCrashReportMachExceptionInfo.h in Headers */, + 2D0E10461141F7DC00CE1BD6 /* PLCrashReportProcessInfo.h in Headers */, + 054627AF11D998BB007891C7 /* PLCrashReportTextFormatter.h in Headers */, + 054627BC11D99D06007891C7 /* PLCrashReportFormatter.h in Headers */, + 05BB83D11364A77800D53B84 /* PLCrashReportProcessorInfo.h in Headers */, + 05BB83F11364AD3E00D53B84 /* PLCrashReportMachineInfo.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 050DE24C0F61B80B00152ED3 /* Fuzz Testing */ = { + isa = PBXNativeTarget; + buildConfigurationList = 050DE2590F61B81C00152ED3 /* Build configuration list for PBXNativeTarget "Fuzz Testing" */; + buildPhases = ( + 050DE24A0F61B80B00152ED3 /* Sources */, + 050DE24B0F61B80B00152ED3 /* Frameworks */, + 050DE28C0F61BAC700152ED3 /* Run Fuzzing */, + ); + buildRules = ( + ); + dependencies = ( + 050DE25D0F61B92C00152ED3 /* PBXTargetDependency */, + ); + name = "Fuzz Testing"; + productName = "Fuzz Testing"; + productReference = 050DE24D0F61B80B00152ED3 /* Fuzz Testing */; + productType = "com.apple.product-type.tool"; + }; + 052A45CE136353FB00987004 /* DemoCrash iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 052A45D4136353FC00987004 /* Build configuration list for PBXNativeTarget "DemoCrash iOS" */; + buildPhases = ( + 052A45CB136353FB00987004 /* Resources */, + 052A45CC136353FB00987004 /* Sources */, + 052A45CD136353FB00987004 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + C2CBA40D245869ED001B775F /* PBXTargetDependency */, + ); + name = "DemoCrash iOS"; + productName = "DemoCrash-iOS"; + productReference = 052A45CF136353FB00987004 /* DemoCrash iOS.app */; + productType = "com.apple.product-type.application"; + }; + 058812B81040582D009128FB /* CrashReporter iOS Framework */ = { + isa = PBXNativeTarget; + buildConfigurationList = 058812BE1040582E009128FB /* Build configuration list for PBXNativeTarget "CrashReporter iOS Framework" */; + buildPhases = ( + 058812B41040582D009128FB /* Headers */, + 058812B61040582D009128FB /* Sources */, + 058812B71040582D009128FB /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 058812CE104058E7009128FB /* PBXTargetDependency */, + ); + name = "CrashReporter iOS Framework"; + productName = "PLCrashReporter-iPhone"; + productReference = 058812B91040582D009128FB /* CrashReporter.framework */; + productType = "com.apple.product-type.framework"; + }; + 05CD31510EE936A9000FDE88 /* CrashReporter iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05CD315E0EE936E4000FDE88 /* Build configuration list for PBXNativeTarget "CrashReporter iOS" */; + buildPhases = ( + 05CD314E0EE936A9000FDE88 /* Headers */, + 05CD314F0EE936A9000FDE88 /* Sources */, + 05CD31500EE936A9000FDE88 /* Frameworks */, + ); + buildRules = ( + C2B72B2C24534F5C00D03ABD /* PBXBuildRule */, + ); + dependencies = ( + ); + name = "CrashReporter iOS"; + productName = "CrashReporter-iPhoneOS"; + productReference = 05CD31520EE936A9000FDE88 /* libCrashReporter.a */; + productType = "com.apple.product-type.library.static"; + }; + 05CD32680EE93DC3000FDE88 /* CrashReporter macOS Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05CD326D0EE93DC4000FDE88 /* Build configuration list for PBXNativeTarget "CrashReporter macOS Tests" */; + buildPhases = ( + 05CD32640EE93DC3000FDE88 /* Resources */, + 05CD34410EEA60F8000FDE88 /* Copy Frameworks */, + 05CD32650EE93DC3000FDE88 /* Sources */, + 05CD32660EE93DC3000FDE88 /* Frameworks */, + ); + buildRules = ( + C2B72B2E24534F6800D03ABD /* PBXBuildRule */, + ); + dependencies = ( + C26C52A52451B45D00D20162 /* PBXTargetDependency */, + ); + name = "CrashReporter macOS Tests"; + productName = "$(TARGET_NAME)"; + productReference = 05CD32690EE93DC3000FDE88 /* Tests macOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 05CD33230EE94439000FDE88 /* CrashReporter iOS Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05CD33290EE9443A000FDE88 /* Build configuration list for PBXNativeTarget "CrashReporter iOS Tests" */; + buildPhases = ( + 05CD33200EE94439000FDE88 /* Resources */, + 05CD33210EE94439000FDE88 /* Sources */, + 05CD33220EE94439000FDE88 /* Frameworks */, + ); + buildRules = ( + C2B72B2F24534F6B00D03ABD /* PBXBuildRule */, + ); + dependencies = ( + 054F50E90EEC50B30034B184 /* PBXTargetDependency */, + ); + name = "CrashReporter iOS Tests"; + productName = "$(TARGET_NAME)"; + productReference = 05CD33240EE94439000FDE88 /* Tests iOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 05E731E20EFA1A3E005EDFB7 /* plcrashutil */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05E731EA0EFA1A56005EDFB7 /* Build configuration list for PBXNativeTarget "plcrashutil" */; + buildPhases = ( + 05E731E00EFA1A3E005EDFB7 /* Sources */, + 05E731E10EFA1A3E005EDFB7 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 05E7325D0EFA1F6C005EDFB7 /* PBXTargetDependency */, + ); + name = plcrashutil; + productName = plcrashutil; + productReference = 05E731E30EFA1A3E005EDFB7 /* plcrashutil */; + productType = "com.apple.product-type.tool"; + }; + 05E731F20EFA1AAB005EDFB7 /* CrashReporter macOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05E7320C0EFA1B09005EDFB7 /* Build configuration list for PBXNativeTarget "CrashReporter macOS" */; + buildPhases = ( + 05E731EF0EFA1AAB005EDFB7 /* Headers */, + 05E731F00EFA1AAB005EDFB7 /* Sources */, + 05E731F10EFA1AAB005EDFB7 /* Frameworks */, + ); + buildRules = ( + C2B72B1A24534DD200D03ABD /* PBXBuildRule */, + ); + dependencies = ( + ); + name = "CrashReporter macOS"; + productName = "CrashReporter-MacOSX-Static"; + productReference = 05E731F30EFA1AAB005EDFB7 /* libCrashReporter.a */; + productType = "com.apple.product-type.library.static"; + }; + 05F40CE60EF7AB80008050CF /* DemoCrash macOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 05F40CEC0EF7AB81008050CF /* Build configuration list for PBXNativeTarget "DemoCrash macOS" */; + buildPhases = ( + 05F40CE30EF7AB80008050CF /* Resources */, + 05F40CE40EF7AB80008050CF /* Sources */, + 05F40CE50EF7AB80008050CF /* Frameworks */, + C2CBA40B24586975001B775F /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + C2CBA40A24586975001B775F /* PBXTargetDependency */, + ); + name = "DemoCrash macOS"; + productName = DemoCrash; + productReference = 05F40CE70EF7AB80008050CF /* DemoCrash macOS.app */; + productType = "com.apple.product-type.application"; + }; + 8064D7AD1C4D22D8005A8B4C /* CrashReporter tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8064D8181C4D22D8005A8B4C /* Build configuration list for PBXNativeTarget "CrashReporter tvOS" */; + buildPhases = ( + 8064D7AE1C4D22D8005A8B4C /* Headers */, + 8064D7D91C4D22D8005A8B4C /* Sources */, + 8064D8161C4D22D8005A8B4C /* Frameworks */, + ); + buildRules = ( + C2B72B2D24534F6200D03ABD /* PBXBuildRule */, + ); + dependencies = ( + ); + name = "CrashReporter tvOS"; + productName = "CrashReporter-AppleTVOS"; + productReference = 8064D81B1C4D22D8005A8B4C /* libCrashReporter.a */; + productType = "com.apple.product-type.library.static"; + }; + 8064D88B1C4D22E5005A8B4C /* CrashReporter tvOS Framework */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8064D8B81C4D22E5005A8B4C /* Build configuration list for PBXNativeTarget "CrashReporter tvOS Framework" */; + buildPhases = ( + 8064D8901C4D22E5005A8B4C /* Headers */, + 8064D8B51C4D22E5005A8B4C /* Sources */, + 8064D8B61C4D22E5005A8B4C /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 80A63BDA1C4D384A0073B7A3 /* PBXTargetDependency */, + ); + name = "CrashReporter tvOS Framework"; + productName = "PLCrashReporter-iPhone"; + productReference = 8064D8BB1C4D22E5005A8B4C /* CrashReporter.framework */; + productType = "com.apple.product-type.framework"; + }; + 8064D9281C4D27E2005A8B4C /* CrashReporter tvOS Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8064D9921C4D27E2005A8B4C /* Build configuration list for PBXNativeTarget "CrashReporter tvOS Tests" */; + buildPhases = ( + 8064D92D1C4D27E2005A8B4C /* Resources */, + 8064D9301C4D27E2005A8B4C /* Sources */, + 8064D98F1C4D27E2005A8B4C /* Frameworks */, + ); + buildRules = ( + C2B72B3024534F6F00D03ABD /* PBXBuildRule */, + ); + dependencies = ( + 80A63BC11C4D2BE20073B7A3 /* PBXTargetDependency */, + ); + name = "CrashReporter tvOS Tests"; + productName = "$(TARGET_NAME)"; + productReference = 8064D9951C4D27E2005A8B4C /* CrashReporter tvOS Tests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 8064D9971C4D27E9005A8B4C /* DemoCrash tvOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8064D9A01C4D27E9005A8B4C /* Build configuration list for PBXNativeTarget "DemoCrash tvOS" */; + buildPhases = ( + 8064D99A1C4D27E9005A8B4C /* Resources */, + 8064D99C1C4D27E9005A8B4C /* Sources */, + 8064D99E1C4D27E9005A8B4C /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + C2CBA40F245869F4001B775F /* PBXTargetDependency */, + ); + name = "DemoCrash tvOS"; + productName = "DemoCrash-iOS"; + productReference = 8064D9A31C4D27E9005A8B4C /* DemoCrash tvOS.app */; + productType = "com.apple.product-type.application"; + }; + 8DC2EF4F0486A6940098B216 /* CrashReporter macOS Framework */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1DEB91AD08733DA50010E9CD /* Build configuration list for PBXNativeTarget "CrashReporter macOS Framework" */; + buildPhases = ( + 8DC2EF500486A6940098B216 /* Headers */, + 8DC2EF540486A6940098B216 /* Sources */, + 8DC2EF560486A6940098B216 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + C26C52A02451B3B300D20162 /* PBXTargetDependency */, + ); + name = "CrashReporter macOS Framework"; + productInstallPath = "$(HOME)/Library/Frameworks"; + productName = CrashReporter; + productReference = 8DC2EF5B0486A6940098B216 /* CrashReporter.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 0867D690FE84028FC02AAC07 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1140; + TargetAttributes = { + 052A45CE136353FB00987004 = { + DevelopmentTeam = 5Z97G9NZQ6; + ProvisioningStyle = Automatic; + }; + 05F40CE60EF7AB80008050CF = { + DevelopmentTeam = 5Z97G9NZQ6; + ProvisioningStyle = Automatic; + }; + 8064D88B1C4D22E5005A8B4C = { + ProvisioningStyle = Manual; + }; + 8064D9971C4D27E9005A8B4C = { + DevelopmentTeam = 5Z97G9NZQ6; + }; + C25664D123571D330088513E = { + CreatedOnToolsVersion = 11.1; + ProvisioningStyle = Automatic; + }; + C2B90D952456FBD000834AFB = { + CreatedOnToolsVersion = 10.1; + ProvisioningStyle = Automatic; + }; + C2B90D992456FBDE00834AFB = { + CreatedOnToolsVersion = 10.1; + ProvisioningStyle = Automatic; + }; + C2C74D9B2538508300313817 = { + CreatedOnToolsVersion = 12.2; + }; + }; + }; + buildConfigurationList = 1DEB91B108733DA50010E9CD /* Build configuration list for PBXProject "CrashReporter" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 1; + knownRegions = ( + Base, + en, + ); + mainGroup = 0867D691FE84028FC02AAC07 /* CrashReporter */; + productRefGroup = 034768DFFF38A50411DB9C8B /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + C2C74D9B2538508300313817 /* CrashReporter */, + 058812D410405908009128FB /* CrashReporter Archive */, + C25664D123571D330088513E /* CrashReporter Documentation */, + F8B9C72A24695BE600B9FEF6 /* CrashReporter XCFramework */, + C2B90D952456FBD000834AFB /* CrashReporter iOS Universal */, + C2B90D992456FBDE00834AFB /* CrashReporter tvOS Universal */, + 8DC2EF4F0486A6940098B216 /* CrashReporter macOS Framework */, + 058812B81040582D009128FB /* CrashReporter iOS Framework */, + 8064D88B1C4D22E5005A8B4C /* CrashReporter tvOS Framework */, + 05E731F20EFA1AAB005EDFB7 /* CrashReporter macOS */, + 05CD31510EE936A9000FDE88 /* CrashReporter iOS */, + 8064D7AD1C4D22D8005A8B4C /* CrashReporter tvOS */, + 05CD32680EE93DC3000FDE88 /* CrashReporter macOS Tests */, + 05CD33230EE94439000FDE88 /* CrashReporter iOS Tests */, + 8064D9281C4D27E2005A8B4C /* CrashReporter tvOS Tests */, + 05F40CE60EF7AB80008050CF /* DemoCrash macOS */, + 052A45CE136353FB00987004 /* DemoCrash iOS */, + 8064D9971C4D27E9005A8B4C /* DemoCrash tvOS */, + 05E731E20EFA1A3E005EDFB7 /* plcrashutil */, + 050DE24C0F61B80B00152ED3 /* Fuzz Testing */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 052A45CB136353FB00987004 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C2F7F26B2451F799002BD8BF /* Default-568h@2x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05CD32640EE93DC3000FDE88 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 05F3CD6D16DE7625007911FB /* Tests in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05CD33200EE94439000FDE88 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 05F3CD6F16DE7625007911FB /* Tests in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05F40CE30EF7AB80008050CF /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8064D92D1C4D27E2005A8B4C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8064D92F1C4D27E2005A8B4C /* Tests in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8064D99A1C4D27E9005A8B4C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 050DE28C0F61BAC700152ED3 /* Run Fuzzing */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Fuzzing"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# If zzuf isn't available, nothing to do.\nwhich -s zzuf\nif [ $? != 0 ]; then\n\texit\nfi\n\n# Run 4 jobs at once.\n# Run from seed 0 to 100000.\n# Only fuzz .plcrash files\nzzuf -I '\\.plcrash$' -j 4 -s 0:100000 \"${TARGET_BUILD_DIR}/${EXECUTABLE_PATH}\" \"${SRCROOT}/Resources/fuzz_report.plcrash\"\n"; + showEnvVarsInLog = 0; + }; + 058812D310405908009128FB /* Create Archive With Universal Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Create Archive With Universal Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "cd \"$BUILT_PRODUCTS_DIR\"\n\"$SRCROOT/Scripts/create-archive.sh\" \"$PRODUCT_NAME-$CURRENT_PROJECT_VERSION\" \"iOS Framework\" \"tvOS Framework\" \"Mac OS X Framework\" \"Tools\"\n"; + showEnvVarsInLog = 0; + }; + C25664D023571BC60088513E /* Verify Modifications */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Verify Modifications"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$SRCROOT/Scripts/verify-modifications.sh\"\n"; + showEnvVarsInLog = 0; + }; + C25664D523571D3D0088513E /* Generate Documentation */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Generate Documentation"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$SRCROOT/Scripts/generate-documentation.sh\"\n"; + showEnvVarsInLog = 0; + }; + C25664E62357487D0088513E /* Create Archive With Static Libraries */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Create Archive With Static Libraries"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "cd \"$BUILT_PRODUCTS_DIR\"\n\"$SRCROOT/Scripts/create-archive.sh\" \"$PRODUCT_NAME-Static-$CURRENT_PROJECT_VERSION\" Static/*\n"; + showEnvVarsInLog = 0; + }; + C2B90DAB2457178800834AFB /* Build iOS Device Framework */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Build iOS Device Framework"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$SRCROOT/Scripts/build-framework.sh\" \"$PROJECT_NAME iOS Framework\" \"iphoneos\"\n"; + showEnvVarsInLog = 0; + }; + C2C74A842535CCC700313817 /* Build iOS Simulator Framework */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Build iOS Simulator Framework"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$SRCROOT/Scripts/build-framework.sh\" \"$PROJECT_NAME iOS Framework\" \"iphonesimulator\"\n"; + showEnvVarsInLog = 0; + }; + C2C74A892535CD8000313817 /* Combine iOS Universal Framework */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Combine iOS Universal Framework"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "DEVICE_FRAMEWORK=\"$BUILD_DIR/$CONFIGURATION-iphoneos/$PRODUCT_NAME.framework\"\nSIMULATOR_FRAMEWORK=\"$BUILD_DIR/$CONFIGURATION-iphonesimulator/$PRODUCT_NAME.framework\"\n\"$SRCROOT/Scripts/combine-frameworks.sh\" \"$DEVICE_FRAMEWORK\" \"$SIMULATOR_FRAMEWORK\" \"$BUILT_PRODUCTS_DIR/$PRODUCT_NAME.framework\"\n"; + showEnvVarsInLog = 0; + }; + C2C74C9825372B9800313817 /* Build tvOS Device Framework */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Build tvOS Device Framework"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$SRCROOT/Scripts/build-framework.sh\" \"$PROJECT_NAME tvOS Framework\" \"appletvos\"\n"; + showEnvVarsInLog = 0; + }; + C2C74C9925372B9A00313817 /* Build tvOS Simulator Framework */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Build tvOS Simulator Framework"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$SRCROOT/Scripts/build-framework.sh\" \"$PROJECT_NAME tvOS Framework\" \"appletvsimulator\"\n"; + showEnvVarsInLog = 0; + }; + C2C74C9A25372B9B00313817 /* Combine tvOS Universal Framework */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Combine tvOS Universal Framework"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "DEVICE_FRAMEWORK=\"$BUILD_DIR/$CONFIGURATION-appletvos/$PRODUCT_NAME.framework\"\nSIMULATOR_FRAMEWORK=\"$BUILD_DIR/$CONFIGURATION-appletvsimulator/$PRODUCT_NAME.framework\"\n\"$SRCROOT/Scripts/combine-frameworks.sh\" \"$DEVICE_FRAMEWORK\" \"$SIMULATOR_FRAMEWORK\" \"$BUILT_PRODUCTS_DIR/$PRODUCT_NAME.framework\"\n"; + showEnvVarsInLog = 0; + }; + C2C74CBD253730FA00313817 /* Build Mac Catalyst Framework */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Build Mac Catalyst Framework"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$SRCROOT/Scripts/build-framework.sh\" \"$PROJECT_NAME iOS Framework\" \"maccatalyst\"\n"; + showEnvVarsInLog = 0; + }; + C2C74DD1253851C300313817 /* Copy Products */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Copy Products"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$SRCROOT/Scripts/copy-products.sh\"\n"; + showEnvVarsInLog = 0; + }; + C2C74E0825385BCE00313817 /* Combine iOS Universal Library */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Combine iOS Universal Library"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "DEVICE_LIBRARY=\"$BUILD_DIR/$CONFIGURATION-iphoneos/lib$PRODUCT_NAME.a\"\nSIMULATOR_LIBRARY=\"$BUILD_DIR/$CONFIGURATION-iphonesimulator/lib$PRODUCT_NAME.a\"\n\"$SRCROOT/Scripts/combine-libraries.sh\" \"$DEVICE_LIBRARY\" \"$SIMULATOR_LIBRARY\" \"$BUILT_PRODUCTS_DIR/lib$PRODUCT_NAME.a\"\n"; + showEnvVarsInLog = 0; + }; + C2C74E0925385C9700313817 /* Combine tvOS Universal Library */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Combine tvOS Universal Library"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "DEVICE_LIBRARY=\"$BUILD_DIR/$CONFIGURATION-appletvos/lib$PRODUCT_NAME.a\"\nSIMULATOR_LIBRARY=\"$BUILD_DIR/$CONFIGURATION-appletvsimulator/lib$PRODUCT_NAME.a\"\n\"$SRCROOT/Scripts/combine-libraries.sh\" \"$DEVICE_LIBRARY\" \"$SIMULATOR_LIBRARY\" \"$BUILT_PRODUCTS_DIR/lib$PRODUCT_NAME.a\"\n"; + showEnvVarsInLog = 0; + }; + F8B9C72E24695BF200B9FEF6 /* Combine XCFramework */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Combine XCFramework"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$SRCROOT/Scripts/combine-xcframework.sh\"\n"; + showEnvVarsInLog = 0; + }; + F8CF2BD2246C139400904633 /* Create Archive With XCFramework */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Create Archive With XCFramework"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "cd \"$BUILT_PRODUCTS_DIR\"\n\"$SRCROOT/Scripts/create-archive.sh\" \"$PRODUCT_NAME-XCFramework-$CURRENT_PROJECT_VERSION\" \"$PROJECT_NAME.xcframework\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 050DE24A0F61B80B00152ED3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C29AD6CB2456C6A000360AF7 /* fuzz-main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 052A45CC136353FB00987004 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C29AD6C92456C69B00360AF7 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 058812B61040582D009128FB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C2F0ACA124AB7C28004890EC /* CrashReporterFramework.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05CD314F0EE936A9000FDE88 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 05CD318E0EE93A90000FDE88 /* CrashReporter.m in Sources */, + 053347AD17E16B0200C52E50 /* PLCrashAsyncDwarfEncoding.cpp in Sources */, + 05CD339D0EE948EB000FDE88 /* PLCrashSignalHandler.mm in Sources */, + 059666E10EEDDFB8008A0601 /* PLCrashFrameWalker.c in Sources */, + 059670280EEF6B1A008A0601 /* PLCrashLogWriter.m in Sources */, + 05CD36470EF24758000FDE88 /* PLCrashAsync.c in Sources */, + 05CD36D40EF25717000FDE88 /* PLCrashLogWriterEncoding.c in Sources */, + 05F40ACC0EF7379F008050CF /* PLCrashReporter.m in Sources */, + 05F411A90EF8DA31008050CF /* PLCrashReport.m in Sources */, + 05F413480EF995C0008050CF /* PLCrashReportSystemInfo.m in Sources */, + 05F414230EF9A6C4008050CF /* PLCrashReportApplicationInfo.m in Sources */, + 05F414870EF9BFAC008050CF /* PLCrashReportThreadInfo.m in Sources */, + 05F415120EF9DD9B008050CF /* PLCrashReportBinaryImageInfo.m in Sources */, + 05F415580EF9E078008050CF /* PLCrashReportExceptionInfo.m in Sources */, + 05E734350EFAC46D005EDFB7 /* PLCrashAsyncSignalInfo.c in Sources */, + 05E734FA0EFAE59C005EDFB7 /* PLCrashReportSignalInfo.m in Sources */, + 2D0E104B1141F7DC00CE1BD6 /* PLCrashReportProcessInfo.m in Sources */, + C2DCD88224658A63007322C5 /* mach_exc.defs in Sources */, + C2B72B2224534E5300D03ABD /* PLCrashReport.proto in Sources */, + 054627AC11D998BB007891C7 /* PLCrashReportTextFormatter.m in Sources */, + 052A46BF1363650100987004 /* PLCrashAsyncImageList.cpp in Sources */, + 05BB83D01364A77800D53B84 /* PLCrashReportProcessorInfo.m in Sources */, + 05BB83F41364AD3E00D53B84 /* PLCrashReportMachineInfo.m in Sources */, + C2B72B2A24534EE700D03ABD /* protobuf-c.c in Sources */, + 05BB84891364EDF200D53B84 /* PLCrashSysctl.c in Sources */, + 05EB2AF915B454DD0066EB4D /* PLCrashAsyncThread_current.S in Sources */, + 05EB2AFF15B456750066EB4D /* PLCrashAsyncThread_current.c in Sources */, + 05EB2B1515B6FDA80066EB4D /* PLCrashReporterNSError.m in Sources */, + 05F76DD5162F213E00A668C7 /* PLCrashAsyncMachOImage.c in Sources */, + C2B72B102453496F00D03ABD /* PLCrashReport.pb-c.c in Sources */, + 05DEE6411636E62B007E99DC /* PLCrashAsyncMObject.c in Sources */, + C2198DDB1640188C006EB46A /* PLCrashAsyncObjCSection.mm in Sources */, + C26022881642FCA6007FC29F /* PLCrashAsyncSymbolication.c in Sources */, + C2198E0816441CF5006EB46A /* PLCrashAsyncMachOString.c in Sources */, + 05D9E54B1676598200B39833 /* PLCrashReportStackFrameInfo.m in Sources */, + 05D9E55616765A0200B39833 /* PLCrashReportRegisterInfo.m in Sources */, + 05D9E56116765D0200B39833 /* PLCrashReportSymbolInfo.m in Sources */, + 0573B4321681098E00395F2A /* PLCrashMachExceptionServer.m in Sources */, + FCE45962BDFEEEFAF00DA7E4 /* PLCrashFrameStackUnwind.c in Sources */, + 05A17DC716D7F81600888448 /* PLCrashAsyncThread.c in Sources */, + 05A17DF316DBD0AD00888448 /* PLCrashAsyncThread_x86.c in Sources */, + 05A17DF816DBD0C200888448 /* PLCrashAsyncThread_arm.c in Sources */, + 05F3CD6216DD6A3B007911FB /* PLCrashFrameCompactUnwind.c in Sources */, + 05F3CD7A16DFC744007911FB /* PLCrashAsyncCompactUnwindEncoding.c in Sources */, + 05E7484F175E5349009B8745 /* PLCrashAsyncDwarfPrimitives.cpp in Sources */, + 05E748611760D64D009B8745 /* PLCrashAsyncDwarfFDE.cpp in Sources */, + 05E748691760D891009B8745 /* PLCrashAsyncDwarfCIE.cpp in Sources */, + 05E7487D176118C2009B8745 /* PLCrashAsyncDwarfCFAStateEvaluation.cpp in Sources */, + 05E7488F176135CF009B8745 /* PLCrashAsyncDwarfExpression.cpp in Sources */, + 05C76DA8176B8C7000E9B10D /* dwarf_opstream.cpp in Sources */, + 05C76DCA176FBAF300E9B10D /* PLCrashAsyncDwarfCFAState.cpp in Sources */, + 05920D21177B9257001E8975 /* PLCrashFrameDWARFUnwind.cpp in Sources */, + 05102E1A17B0151000B5D925 /* PLCrashProcessInfo.m in Sources */, + 05102E2A17B2B80A00B5D925 /* PLCrashHostInfo.m in Sources */, + 051F067F17B6B0D4006D0EFA /* PLCrashMachExceptionPort.m in Sources */, + 05BEC41D17BAF92A0082CBFB /* PLCrashMachExceptionPortSet.m in Sources */, + 05BEC42817BD4F290082CBFB /* PLCrashAsyncMachExceptionInfo.c in Sources */, + 05BEC43C17BF1CB10082CBFB /* PLCrashReporterConfig.m in Sources */, + 05B929EE17C9336600B051E3 /* PLCrashUncaughtExceptionHandler.m in Sources */, + 0513E23A17D15ED400727919 /* PLCrashReportMachExceptionInfo.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05CD32650EE93DC3000FDE88 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C29AD6DB2456CA7300360AF7 /* PLCrashLogWriterEncodingTests.proto in Sources */, + C2BBCD9A2456E09000F9E820 /* PLCrashTestThread.m in Sources */, + C2F7F1912451EC00002BD8BF /* unwind_test_harness.c in Sources */, + C2BBCDA12456E0E700F9E820 /* PLCrashSysctlTests.m in Sources */, + C2F7F1722451EC00002BD8BF /* PLCrashAsyncSignalInfoTests.m in Sources */, + C2F7F1792451EC00002BD8BF /* PLCrashAsyncMachOImageTests.m in Sources */, + C2F7F2352451F167002BD8BF /* unwind_test_x86_64.S in Sources */, + C2F7F23D2451F167002BD8BF /* unwind_test_x86_64_frameless_big.S in Sources */, + C29AD6D52456C95A00360AF7 /* PLCrashTestThreadTests.m in Sources */, + C2F7F1822451EC00002BD8BF /* PLCrashAsyncDwarfCFAStateTests.mm in Sources */, + C2F7F2362451F167002BD8BF /* unwind_test_arm.S in Sources */, + C2F7F1732451EC00002BD8BF /* PLCrashAsyncMachExceptionInfoTests.m in Sources */, + C2F7F2372451F167002BD8BF /* unwind_test_arm64.S in Sources */, + C2F7F1752451EC00002BD8BF /* PLCrashAsyncThreadTests.m in Sources */, + C2F7F23C2451F167002BD8BF /* unwind_test_x86_64_frameless.S in Sources */, + C2F7F1A32451EC00002BD8BF /* PLCrashTestCase.m in Sources */, + C2F7F1902451EC00002BD8BF /* PLCrashFrameCompactUnwindTests.m in Sources */, + C2F7F1872451EC00002BD8BF /* PLCrashHostInfoTests.m in Sources */, + C2BBCDA02456E0E700F9E820 /* PLCrashMachExceptionServerTests.m in Sources */, + C2F7F23A2451F167002BD8BF /* unwind_test_x86_64_disable_compact_frame.S in Sources */, + C2F7F1802451EC00002BD8BF /* PLCrashAsyncDwarfFDETests.mm in Sources */, + C2F7F16A2451EBFF002BD8BF /* PLCrashReporterTests.m in Sources */, + C2F7F16B2451EBFF002BD8BF /* PLCrashReportTests.m in Sources */, + C2F7F1772451EC00002BD8BF /* PLCrashAsyncMObjectTests.m in Sources */, + C2F7F2412451F167002BD8BF /* unwind_test_x86_frameless.S in Sources */, + C2F7F1702451EC00002BD8BF /* PLCrashUncaughtExceptionHandlerTests.m in Sources */, + C2F7F1742451EC00002BD8BF /* PLCrashAsyncImageListTests.m in Sources */, + C2F7F2312451F155002BD8BF /* unwind_test_x86_unusual.S in Sources */, + C2F7F18D2451EC00002BD8BF /* PLCrashFrameWalkerTests.m in Sources */, + C2F7F17B2451EC00002BD8BF /* PLCrashAsyncObjCSectionTests.m in Sources */, + C2F7F17F2451EC00002BD8BF /* PLCrashAsyncDwarfCIETests.mm in Sources */, + C2BBCD9D2456E0E700F9E820 /* PLCrashAsyncMachOStringTests.m in Sources */, + C2F7F2422451F167002BD8BF /* unwind_test_x86_frameless_big.S in Sources */, + C2F7F1892451EC00002BD8BF /* PLCrashLogWriterTests.m in Sources */, + C2F7F23F2451F167002BD8BF /* unwind_test_x86_disable_compact_frame.S in Sources */, + C2F7F1782451EC00002BD8BF /* PLCrashAsyncSymbolicationTests.m in Sources */, + C2F7F1712451EC00002BD8BF /* PLCrashAsyncTests.m in Sources */, + C2F7F1812451EC00002BD8BF /* PLCrashAsyncDwarfPrimitivesTests.mm in Sources */, + C2F7F1A22451EC00002BD8BF /* PLCrashReporterNSErrorTests.m in Sources */, + C2F7F1832451EC00002BD8BF /* PLCrashAsyncDwarfCFAStateEvaluationTests.mm in Sources */, + C2F7F1842451EC00002BD8BF /* PLCrashAsyncDwarfExpressionTests.mm in Sources */, + C2F7F16E2451EC00002BD8BF /* PLCrashMachExceptionPortSetTests.m in Sources */, + C2F7F2402451F167002BD8BF /* unwind_test_x86_frame.S in Sources */, + C2F7F18F2451EC00002BD8BF /* PLCrashFrameDWARFUnwindTests.m in Sources */, + C2F7F1882451EC00002BD8BF /* PLCrashProcessInfoTests.m in Sources */, + C2F7F18A2451EC00002BD8BF /* PLCrashLogWriterEncodingTests.m in Sources */, + C2F7F1852451EC00002BD8BF /* PLCrashAsyncCompactUnwindEncodingTests.m in Sources */, + C2BBCD9F2456E0E700F9E820 /* PLCrashMachExceptionPortTests.m in Sources */, + C2F7F23E2451F167002BD8BF /* unwind_test_x86_64_unusual.S in Sources */, + C2F7F2342451F167002BD8BF /* unwind_test_x86.S in Sources */, + C2BBCD9E2456E0E700F9E820 /* PLCrashFrameStackUnwindTests.m in Sources */, + C2BBCD9B2456E0E700F9E820 /* PLCrashAsyncDwarfEncodingTests.mm in Sources */, + C2F7F2392451F167002BD8BF /* unwind_test_arm64_frameless.S in Sources */, + C2F7F17E2451EC00002BD8BF /* dwarf_opstream_tests.mm in Sources */, + C2BBCD9C2456E0E700F9E820 /* PLCrashAsyncLinkedListTests.mm in Sources */, + C2F7F17D2451EC00002BD8BF /* dwarf_stack_tests.mm in Sources */, + C2F7F23B2451F167002BD8BF /* unwind_test_x86_64_frame.S in Sources */, + C2F7F2382451F167002BD8BF /* unwind_test_arm64_frame.S in Sources */, + C29AD6D72456C9B800360AF7 /* PLCrashLogWriterEncodingTests.pb-c.c in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05CD33210EE94439000FDE88 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C29AD6DA2456CA6C00360AF7 /* PLCrashLogWriterEncodingTests.proto in Sources */, + C2F7F1AC2451EC00002BD8BF /* PLCrashAsyncSignalInfoTests.m in Sources */, + C2F7F1B32451EC00002BD8BF /* PLCrashAsyncMachOImageTests.m in Sources */, + C2F7F1BC2451EC00002BD8BF /* PLCrashAsyncDwarfCFAStateTests.mm in Sources */, + C2F7F2442451F168002BD8BF /* unwind_test_x86_64.S in Sources */, + C2F7F24C2451F168002BD8BF /* unwind_test_x86_64_frameless_big.S in Sources */, + C29AD6D32456C95900360AF7 /* PLCrashTestThreadTests.m in Sources */, + C2BBCDA82456E0E800F9E820 /* PLCrashSysctlTests.m in Sources */, + C2F7F2452451F168002BD8BF /* unwind_test_arm.S in Sources */, + C2F7F1AD2451EC00002BD8BF /* PLCrashAsyncMachExceptionInfoTests.m in Sources */, + C2F7F1AF2451EC00002BD8BF /* PLCrashAsyncThreadTests.m in Sources */, + C2F7F2462451F168002BD8BF /* unwind_test_arm64.S in Sources */, + C2F7F24B2451F168002BD8BF /* unwind_test_x86_64_frameless.S in Sources */, + C2F7F1DD2451EC00002BD8BF /* PLCrashTestCase.m in Sources */, + C2F7F1CA2451EC00002BD8BF /* PLCrashFrameCompactUnwindTests.m in Sources */, + C2F7F1C12451EC00002BD8BF /* PLCrashHostInfoTests.m in Sources */, + C2F7F1CB2451EC00002BD8BF /* unwind_test_harness.c in Sources */, + C2BBCDA72456E0E800F9E820 /* PLCrashMachExceptionServerTests.m in Sources */, + C2F7F2492451F168002BD8BF /* unwind_test_x86_64_disable_compact_frame.S in Sources */, + C2F7F1BA2451EC00002BD8BF /* PLCrashAsyncDwarfFDETests.mm in Sources */, + C2BBCDA32456E0E800F9E820 /* PLCrashAsyncLinkedListTests.mm in Sources */, + C2F7F1A42451EC00002BD8BF /* PLCrashReporterTests.m in Sources */, + C2BBCDA22456E0E800F9E820 /* PLCrashAsyncDwarfEncodingTests.mm in Sources */, + C2F7F1A52451EC00002BD8BF /* PLCrashReportTests.m in Sources */, + C2F7F1B12451EC00002BD8BF /* PLCrashAsyncMObjectTests.m in Sources */, + C2F7F2502451F168002BD8BF /* unwind_test_x86_frameless.S in Sources */, + C2F7F1AA2451EC00002BD8BF /* PLCrashUncaughtExceptionHandlerTests.m in Sources */, + C2F7F1AE2451EC00002BD8BF /* PLCrashAsyncImageListTests.m in Sources */, + C2BBCDA62456E0E800F9E820 /* PLCrashMachExceptionPortTests.m in Sources */, + C2F7F2322451F156002BD8BF /* unwind_test_x86_unusual.S in Sources */, + C2F7F1C72451EC00002BD8BF /* PLCrashFrameWalkerTests.m in Sources */, + C2F7F1B52451EC00002BD8BF /* PLCrashAsyncObjCSectionTests.m in Sources */, + C2F7F1B92451EC00002BD8BF /* PLCrashAsyncDwarfCIETests.mm in Sources */, + C2F7F2512451F168002BD8BF /* unwind_test_x86_frameless_big.S in Sources */, + C2F7F1C32451EC00002BD8BF /* PLCrashLogWriterTests.m in Sources */, + C2F7F24E2451F168002BD8BF /* unwind_test_x86_disable_compact_frame.S in Sources */, + C2F7F1B22451EC00002BD8BF /* PLCrashAsyncSymbolicationTests.m in Sources */, + C2F7F1AB2451EC00002BD8BF /* PLCrashAsyncTests.m in Sources */, + C2F7F1BB2451EC00002BD8BF /* PLCrashAsyncDwarfPrimitivesTests.mm in Sources */, + C2F7F1DC2451EC00002BD8BF /* PLCrashReporterNSErrorTests.m in Sources */, + C2F7F1B82451EC00002BD8BF /* dwarf_opstream_tests.mm in Sources */, + C2F7F1BD2451EC00002BD8BF /* PLCrashAsyncDwarfCFAStateEvaluationTests.mm in Sources */, + C2F7F1BE2451EC00002BD8BF /* PLCrashAsyncDwarfExpressionTests.mm in Sources */, + C2F7F24F2451F168002BD8BF /* unwind_test_x86_frame.S in Sources */, + C2F7F1A82451EC00002BD8BF /* PLCrashMachExceptionPortSetTests.m in Sources */, + C2F7F1C92451EC00002BD8BF /* PLCrashFrameDWARFUnwindTests.m in Sources */, + C2BBCDA52456E0E800F9E820 /* PLCrashFrameStackUnwindTests.m in Sources */, + C2F7F1C22451EC00002BD8BF /* PLCrashProcessInfoTests.m in Sources */, + C2F7F1C42451EC00002BD8BF /* PLCrashLogWriterEncodingTests.m in Sources */, + C2F7F24D2451F168002BD8BF /* unwind_test_x86_64_unusual.S in Sources */, + C2F7F1BF2451EC00002BD8BF /* PLCrashAsyncCompactUnwindEncodingTests.m in Sources */, + C2BBCDA42456E0E800F9E820 /* PLCrashAsyncMachOStringTests.m in Sources */, + C2F7F2432451F168002BD8BF /* unwind_test_x86.S in Sources */, + C2F7F2482451F168002BD8BF /* unwind_test_arm64_frameless.S in Sources */, + C2F7F1A92451EC00002BD8BF /* PLCrashSignalHandlerTests.m in Sources */, + C29AD6D22456C95900360AF7 /* PLCrashTestThread.m in Sources */, + C2F7F1B72451EC00002BD8BF /* dwarf_stack_tests.mm in Sources */, + C2F7F24A2451F168002BD8BF /* unwind_test_x86_64_frame.S in Sources */, + C2F7F2472451F168002BD8BF /* unwind_test_arm64_frame.S in Sources */, + C29AD6D82456C9B800360AF7 /* PLCrashLogWriterEncodingTests.pb-c.c in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05E731E00EFA1A3E005EDFB7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C29AD6CC2456C6A500360AF7 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05E731F00EFA1AAB005EDFB7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 05E731F80EFA1AE3005EDFB7 /* CrashReporter.m in Sources */, + 05E731F90EFA1AE3005EDFB7 /* PLCrashSignalHandler.mm in Sources */, + 05E731FA0EFA1AE3005EDFB7 /* PLCrashFrameWalker.c in Sources */, + 05E731FD0EFA1AE3005EDFB7 /* PLCrashLogWriter.m in Sources */, + 05E731FE0EFA1AE3005EDFB7 /* PLCrashAsync.c in Sources */, + 05E731FF0EFA1AE3005EDFB7 /* PLCrashLogWriterEncoding.c in Sources */, + 05E732000EFA1AE3005EDFB7 /* PLCrashReporter.m in Sources */, + 05E732010EFA1AE3005EDFB7 /* PLCrashReport.m in Sources */, + 05E732040EFA1AE3005EDFB7 /* PLCrashReportSystemInfo.m in Sources */, + 05E732050EFA1AE3005EDFB7 /* PLCrashReportApplicationInfo.m in Sources */, + 05E732060EFA1AE3005EDFB7 /* PLCrashReportThreadInfo.m in Sources */, + 05E732070EFA1AE3005EDFB7 /* PLCrashReportBinaryImageInfo.m in Sources */, + 05E732080EFA1AE3005EDFB7 /* PLCrashReportExceptionInfo.m in Sources */, + 05E734390EFAC46D005EDFB7 /* PLCrashAsyncSignalInfo.c in Sources */, + 05E734FE0EFAE59C005EDFB7 /* PLCrashReportSignalInfo.m in Sources */, + 2D0E10491141F7DC00CE1BD6 /* PLCrashReportProcessInfo.m in Sources */, + 054627B211D998BB007891C7 /* PLCrashReportTextFormatter.m in Sources */, + C2DCD88124658A63007322C5 /* mach_exc.defs in Sources */, + 052A46C31363650100987004 /* PLCrashAsyncImageList.cpp in Sources */, + 05BB83D41364A77800D53B84 /* PLCrashReportProcessorInfo.m in Sources */, + 05BB83F81364AD3E00D53B84 /* PLCrashReportMachineInfo.m in Sources */, + 05BB848D1364EDF200D53B84 /* PLCrashSysctl.c in Sources */, + 05EB2AF715B454DD0066EB4D /* PLCrashAsyncThread_current.S in Sources */, + 05EB2AFD15B456750066EB4D /* PLCrashAsyncThread_current.c in Sources */, + 05EB2B1315B6FDA80066EB4D /* PLCrashReporterNSError.m in Sources */, + 05F76DD3162F213E00A668C7 /* PLCrashAsyncMachOImage.c in Sources */, + 05DEE63F1636E62B007E99DC /* PLCrashAsyncMObject.c in Sources */, + C2198DD91640188C006EB46A /* PLCrashAsyncObjCSection.mm in Sources */, + C26022861642FCA6007FC29F /* PLCrashAsyncSymbolication.c in Sources */, + C2198E0616441CF5006EB46A /* PLCrashAsyncMachOString.c in Sources */, + 05D9E5491676598200B39833 /* PLCrashReportStackFrameInfo.m in Sources */, + 05D9E55416765A0200B39833 /* PLCrashReportRegisterInfo.m in Sources */, + 05D9E55F16765D0200B39833 /* PLCrashReportSymbolInfo.m in Sources */, + 0573B4301681098E00395F2A /* PLCrashMachExceptionServer.m in Sources */, + FCE4550BA74D9DF923CFCD5A /* PLCrashFrameStackUnwind.c in Sources */, + 05A17DC516D7F81600888448 /* PLCrashAsyncThread.c in Sources */, + 05A17DF116DBD0AD00888448 /* PLCrashAsyncThread_x86.c in Sources */, + 05F3CD6016DD6A3B007911FB /* PLCrashFrameCompactUnwind.c in Sources */, + C2B72B2924534EE700D03ABD /* protobuf-c.c in Sources */, + 05F3CD7816DFC744007911FB /* PLCrashAsyncCompactUnwindEncoding.c in Sources */, + 05E7484D175E5349009B8745 /* PLCrashAsyncDwarfPrimitives.cpp in Sources */, + 05E7485F1760D64D009B8745 /* PLCrashAsyncDwarfFDE.cpp in Sources */, + 05E748671760D891009B8745 /* PLCrashAsyncDwarfCIE.cpp in Sources */, + 05E7487B176118C2009B8745 /* PLCrashAsyncDwarfCFAStateEvaluation.cpp in Sources */, + 05C76DA6176B8C7000E9B10D /* dwarf_opstream.cpp in Sources */, + C2F7F1692451EB2D002BD8BF /* PLCrashAsyncThread_arm.c in Sources */, + 05C76DC8176FBAF300E9B10D /* PLCrashAsyncDwarfCFAState.cpp in Sources */, + 057C9BBF17970F6D006B242E /* PLCrashAsyncDwarfEncoding.cpp in Sources */, + 057C9BC017970F77006B242E /* PLCrashAsyncDwarfExpression.cpp in Sources */, + 057C9BBE17970F54006B242E /* PLCrashFrameDWARFUnwind.cpp in Sources */, + 05102E2817B2B80A00B5D925 /* PLCrashHostInfo.m in Sources */, + 0527062F17CBCCA100E6A5D8 /* PLCrashMachExceptionPort.m in Sources */, + 05BEC41B17BAF92A0082CBFB /* PLCrashMachExceptionPortSet.m in Sources */, + 05BEC42617BD4F290082CBFB /* PLCrashAsyncMachExceptionInfo.c in Sources */, + C2B72B2124534E5200D03ABD /* PLCrashReport.proto in Sources */, + 05BEC43A17BF1CB10082CBFB /* PLCrashReporterConfig.m in Sources */, + C2B72B0F2453496F00D03ABD /* PLCrashReport.pb-c.c in Sources */, + 05B929EC17C9336600B051E3 /* PLCrashUncaughtExceptionHandler.m in Sources */, + 0527063017CBCCC200E6A5D8 /* PLCrashProcessInfo.m in Sources */, + 0513E23817D15ED400727919 /* PLCrashReportMachExceptionInfo.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 05F40CE40EF7AB80008050CF /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C29AD6C82456C69B00360AF7 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8064D7D91C4D22D8005A8B4C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8064D7DA1C4D22D8005A8B4C /* CrashReporter.m in Sources */, + 8064D7DB1C4D22D8005A8B4C /* PLCrashAsyncDwarfEncoding.cpp in Sources */, + 8064D7DC1C4D22D8005A8B4C /* PLCrashSignalHandler.mm in Sources */, + 8064D7DD1C4D22D8005A8B4C /* PLCrashFrameWalker.c in Sources */, + 8064D7DE1C4D22D8005A8B4C /* PLCrashLogWriter.m in Sources */, + 8064D7DF1C4D22D8005A8B4C /* PLCrashAsync.c in Sources */, + 8064D7E01C4D22D8005A8B4C /* PLCrashLogWriterEncoding.c in Sources */, + 8064D7E11C4D22D8005A8B4C /* PLCrashReporter.m in Sources */, + 8064D7E21C4D22D8005A8B4C /* PLCrashReport.m in Sources */, + 8064D7E51C4D22D8005A8B4C /* PLCrashReportSystemInfo.m in Sources */, + 8064D7E61C4D22D8005A8B4C /* PLCrashReportApplicationInfo.m in Sources */, + 8064D7E71C4D22D8005A8B4C /* PLCrashReportThreadInfo.m in Sources */, + 8064D7E81C4D22D8005A8B4C /* PLCrashReportBinaryImageInfo.m in Sources */, + 8064D7E91C4D22D8005A8B4C /* PLCrashReportExceptionInfo.m in Sources */, + 8064D7EA1C4D22D8005A8B4C /* PLCrashAsyncSignalInfo.c in Sources */, + 8064D7EB1C4D22D8005A8B4C /* PLCrashReportSignalInfo.m in Sources */, + 8064D7EC1C4D22D8005A8B4C /* PLCrashReportProcessInfo.m in Sources */, + C2B72B2324534E5300D03ABD /* PLCrashReport.proto in Sources */, + 8064D7ED1C4D22D8005A8B4C /* PLCrashReportTextFormatter.m in Sources */, + 8064D7EE1C4D22D8005A8B4C /* PLCrashAsyncImageList.cpp in Sources */, + 8064D7EF1C4D22D8005A8B4C /* PLCrashReportProcessorInfo.m in Sources */, + 8064D7F01C4D22D8005A8B4C /* PLCrashReportMachineInfo.m in Sources */, + C2B72B2B24534EE700D03ABD /* protobuf-c.c in Sources */, + 8064D7F11C4D22D8005A8B4C /* PLCrashSysctl.c in Sources */, + 8064D7F21C4D22D8005A8B4C /* PLCrashAsyncThread_current.S in Sources */, + 8064D7F31C4D22D8005A8B4C /* PLCrashAsyncThread_current.c in Sources */, + 8064D7F41C4D22D8005A8B4C /* PLCrashReporterNSError.m in Sources */, + 8064D7F51C4D22D8005A8B4C /* PLCrashAsyncMachOImage.c in Sources */, + C2B72B112453496F00D03ABD /* PLCrashReport.pb-c.c in Sources */, + 8064D7F61C4D22D8005A8B4C /* PLCrashAsyncMObject.c in Sources */, + 8064D7F71C4D22D8005A8B4C /* PLCrashAsyncObjCSection.mm in Sources */, + 8064D7F81C4D22D8005A8B4C /* PLCrashAsyncSymbolication.c in Sources */, + 8064D7F91C4D22D8005A8B4C /* PLCrashAsyncMachOString.c in Sources */, + 8064D7FA1C4D22D8005A8B4C /* PLCrashReportStackFrameInfo.m in Sources */, + 8064D7FB1C4D22D8005A8B4C /* PLCrashReportRegisterInfo.m in Sources */, + 8064D7FC1C4D22D8005A8B4C /* PLCrashReportSymbolInfo.m in Sources */, + 8064D7FD1C4D22D8005A8B4C /* PLCrashMachExceptionServer.m in Sources */, + 8064D7FE1C4D22D8005A8B4C /* PLCrashFrameStackUnwind.c in Sources */, + 8064D7FF1C4D22D8005A8B4C /* PLCrashAsyncThread.c in Sources */, + 8064D8001C4D22D8005A8B4C /* PLCrashAsyncThread_x86.c in Sources */, + 8064D8011C4D22D8005A8B4C /* PLCrashAsyncThread_arm.c in Sources */, + 8064D8021C4D22D8005A8B4C /* PLCrashFrameCompactUnwind.c in Sources */, + 8064D8031C4D22D8005A8B4C /* PLCrashAsyncCompactUnwindEncoding.c in Sources */, + 8064D8041C4D22D8005A8B4C /* PLCrashAsyncDwarfPrimitives.cpp in Sources */, + 8064D8051C4D22D8005A8B4C /* PLCrashAsyncDwarfFDE.cpp in Sources */, + 8064D8061C4D22D8005A8B4C /* PLCrashAsyncDwarfCIE.cpp in Sources */, + 8064D8071C4D22D8005A8B4C /* PLCrashAsyncDwarfCFAStateEvaluation.cpp in Sources */, + 8064D8081C4D22D8005A8B4C /* PLCrashAsyncDwarfExpression.cpp in Sources */, + 8064D80A1C4D22D8005A8B4C /* dwarf_opstream.cpp in Sources */, + 8064D80B1C4D22D8005A8B4C /* PLCrashAsyncDwarfCFAState.cpp in Sources */, + 8064D80C1C4D22D8005A8B4C /* PLCrashFrameDWARFUnwind.cpp in Sources */, + 8064D80D1C4D22D8005A8B4C /* PLCrashProcessInfo.m in Sources */, + 8064D80E1C4D22D8005A8B4C /* PLCrashHostInfo.m in Sources */, + 8064D80F1C4D22D8005A8B4C /* PLCrashMachExceptionPort.m in Sources */, + 8064D8101C4D22D8005A8B4C /* PLCrashMachExceptionPortSet.m in Sources */, + 8064D8111C4D22D8005A8B4C /* PLCrashAsyncMachExceptionInfo.c in Sources */, + 8064D8121C4D22D8005A8B4C /* PLCrashReporterConfig.m in Sources */, + 8064D8141C4D22D8005A8B4C /* PLCrashUncaughtExceptionHandler.m in Sources */, + 8064D8151C4D22D8005A8B4C /* PLCrashReportMachExceptionInfo.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8064D8B51C4D22E5005A8B4C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C2F0ACA224AB7C28004890EC /* CrashReporterFramework.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8064D9301C4D27E2005A8B4C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C29AD6DC2456CA7800360AF7 /* PLCrashLogWriterEncodingTests.proto in Sources */, + C2F7F1E62451EC01002BD8BF /* PLCrashAsyncSignalInfoTests.m in Sources */, + C2F7F1ED2451EC01002BD8BF /* PLCrashAsyncMachOImageTests.m in Sources */, + C2F7F1F62451EC01002BD8BF /* PLCrashAsyncDwarfCFAStateTests.mm in Sources */, + C2F7F2532451F169002BD8BF /* unwind_test_x86_64.S in Sources */, + C2F7F25B2451F169002BD8BF /* unwind_test_x86_64_frameless_big.S in Sources */, + C29AD6D12456C95800360AF7 /* PLCrashTestThreadTests.m in Sources */, + C2BBCDAF2456E0E800F9E820 /* PLCrashSysctlTests.m in Sources */, + C2F7F2542451F169002BD8BF /* unwind_test_arm.S in Sources */, + C2F7F1E72451EC01002BD8BF /* PLCrashAsyncMachExceptionInfoTests.m in Sources */, + C2F7F1E92451EC01002BD8BF /* PLCrashAsyncThreadTests.m in Sources */, + C2F7F2552451F169002BD8BF /* unwind_test_arm64.S in Sources */, + C2F7F25A2451F169002BD8BF /* unwind_test_x86_64_frameless.S in Sources */, + C2F7F2172451EC01002BD8BF /* PLCrashTestCase.m in Sources */, + C2F7F2042451EC01002BD8BF /* PLCrashFrameCompactUnwindTests.m in Sources */, + C2F7F1FB2451EC01002BD8BF /* PLCrashHostInfoTests.m in Sources */, + C2F7F2052451EC01002BD8BF /* unwind_test_harness.c in Sources */, + C2BBCDAE2456E0E800F9E820 /* PLCrashMachExceptionServerTests.m in Sources */, + C2F7F2582451F169002BD8BF /* unwind_test_x86_64_disable_compact_frame.S in Sources */, + C2F7F1F42451EC01002BD8BF /* PLCrashAsyncDwarfFDETests.mm in Sources */, + C2BBCDAA2456E0E800F9E820 /* PLCrashAsyncLinkedListTests.mm in Sources */, + C2F7F1DE2451EC01002BD8BF /* PLCrashReporterTests.m in Sources */, + C2BBCDA92456E0E800F9E820 /* PLCrashAsyncDwarfEncodingTests.mm in Sources */, + C2F7F25F2451F169002BD8BF /* unwind_test_x86_frameless.S in Sources */, + C2F7F1DF2451EC01002BD8BF /* PLCrashReportTests.m in Sources */, + C2F7F1EB2451EC01002BD8BF /* PLCrashAsyncMObjectTests.m in Sources */, + C2F7F2332451F157002BD8BF /* unwind_test_x86_unusual.S in Sources */, + C2F7F1E42451EC01002BD8BF /* PLCrashUncaughtExceptionHandlerTests.m in Sources */, + C2BBCDAD2456E0E800F9E820 /* PLCrashMachExceptionPortTests.m in Sources */, + C2F7F1E82451EC01002BD8BF /* PLCrashAsyncImageListTests.m in Sources */, + C2F7F2012451EC01002BD8BF /* PLCrashFrameWalkerTests.m in Sources */, + C2F7F2602451F169002BD8BF /* unwind_test_x86_frameless_big.S in Sources */, + C2F7F1EF2451EC01002BD8BF /* PLCrashAsyncObjCSectionTests.m in Sources */, + C2F7F25D2451F169002BD8BF /* unwind_test_x86_disable_compact_frame.S in Sources */, + C2F7F1F32451EC01002BD8BF /* PLCrashAsyncDwarfCIETests.mm in Sources */, + C2F7F1FD2451EC01002BD8BF /* PLCrashLogWriterTests.m in Sources */, + C2F7F1EC2451EC01002BD8BF /* PLCrashAsyncSymbolicationTests.m in Sources */, + C2F7F1E52451EC01002BD8BF /* PLCrashAsyncTests.m in Sources */, + C2F7F1F52451EC01002BD8BF /* PLCrashAsyncDwarfPrimitivesTests.mm in Sources */, + C2F7F2162451EC01002BD8BF /* PLCrashReporterNSErrorTests.m in Sources */, + C2F7F1F22451EC01002BD8BF /* dwarf_opstream_tests.mm in Sources */, + C2F7F25E2451F169002BD8BF /* unwind_test_x86_frame.S in Sources */, + C2F7F1F72451EC01002BD8BF /* PLCrashAsyncDwarfCFAStateEvaluationTests.mm in Sources */, + C2F7F1F82451EC01002BD8BF /* PLCrashAsyncDwarfExpressionTests.mm in Sources */, + C2F7F1E22451EC01002BD8BF /* PLCrashMachExceptionPortSetTests.m in Sources */, + C2F7F2032451EC01002BD8BF /* PLCrashFrameDWARFUnwindTests.m in Sources */, + C2BBCDAC2456E0E800F9E820 /* PLCrashFrameStackUnwindTests.m in Sources */, + C2F7F1FC2451EC01002BD8BF /* PLCrashProcessInfoTests.m in Sources */, + C2F7F25C2451F169002BD8BF /* unwind_test_x86_64_unusual.S in Sources */, + C2F7F1FE2451EC01002BD8BF /* PLCrashLogWriterEncodingTests.m in Sources */, + C2F7F2522451F169002BD8BF /* unwind_test_x86.S in Sources */, + C2BBCDAB2456E0E800F9E820 /* PLCrashAsyncMachOStringTests.m in Sources */, + C2F7F1F92451EC01002BD8BF /* PLCrashAsyncCompactUnwindEncodingTests.m in Sources */, + C2F7F2572451F169002BD8BF /* unwind_test_arm64_frameless.S in Sources */, + C2F7F1E32451EC01002BD8BF /* PLCrashSignalHandlerTests.m in Sources */, + C29AD6D02456C95800360AF7 /* PLCrashTestThread.m in Sources */, + C2F7F2592451F169002BD8BF /* unwind_test_x86_64_frame.S in Sources */, + C2F7F1F12451EC01002BD8BF /* dwarf_stack_tests.mm in Sources */, + C2F7F2562451F169002BD8BF /* unwind_test_arm64_frame.S in Sources */, + C29AD6D92456C9B800360AF7 /* PLCrashLogWriterEncodingTests.pb-c.c in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8064D99C1C4D27E9005A8B4C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C29AD6CA2456C69C00360AF7 /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8DC2EF540486A6940098B216 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C2F0ACA024AB7C28004890EC /* CrashReporterFramework.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 050DE25D0F61B92C00152ED3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 05E731F20EFA1AAB005EDFB7 /* CrashReporter macOS */; + targetProxy = 050DE25C0F61B92C00152ED3 /* PBXContainerItemProxy */; + }; + 054F50E90EEC50B30034B184 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 05CD31510EE936A9000FDE88 /* CrashReporter iOS */; + targetProxy = 054F50E80EEC50B30034B184 /* PBXContainerItemProxy */; + }; + 058812CE104058E7009128FB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 05CD31510EE936A9000FDE88 /* CrashReporter iOS */; + targetProxy = 058812CD104058E7009128FB /* PBXContainerItemProxy */; + }; + 05E7325D0EFA1F6C005EDFB7 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 05E731F20EFA1AAB005EDFB7 /* CrashReporter macOS */; + targetProxy = 05E7325C0EFA1F6C005EDFB7 /* PBXContainerItemProxy */; + }; + 80A63BC11C4D2BE20073B7A3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 8064D7AD1C4D22D8005A8B4C /* CrashReporter tvOS */; + targetProxy = 80A63BC01C4D2BE20073B7A3 /* PBXContainerItemProxy */; + }; + 80A63BDA1C4D384A0073B7A3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 8064D7AD1C4D22D8005A8B4C /* CrashReporter tvOS */; + targetProxy = 80A63BD91C4D384A0073B7A3 /* PBXContainerItemProxy */; + }; + C23D0556248A7C440094EC6B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 8DC2EF4F0486A6940098B216 /* CrashReporter macOS Framework */; + targetProxy = C23D0555248A7C440094EC6B /* PBXContainerItemProxy */; + }; + C26C52A02451B3B300D20162 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 05E731F20EFA1AAB005EDFB7 /* CrashReporter macOS */; + targetProxy = C26C529F2451B3B300D20162 /* PBXContainerItemProxy */; + }; + C26C52A52451B45D00D20162 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 05E731F20EFA1AAB005EDFB7 /* CrashReporter macOS */; + targetProxy = C26C52A42451B45D00D20162 /* PBXContainerItemProxy */; + }; + C2C74DC4253850A500313817 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F8B9C72A24695BE600B9FEF6 /* CrashReporter XCFramework */; + targetProxy = C2C74DC3253850A500313817 /* PBXContainerItemProxy */; + }; + C2C74DC6253850A500313817 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = C2B90D952456FBD000834AFB /* CrashReporter iOS Universal */; + targetProxy = C2C74DC5253850A500313817 /* PBXContainerItemProxy */; + }; + C2C74DC8253850A500313817 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 8DC2EF4F0486A6940098B216 /* CrashReporter macOS Framework */; + targetProxy = C2C74DC7253850A500313817 /* PBXContainerItemProxy */; + }; + C2C74DCA253850A500313817 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = C2B90D992456FBDE00834AFB /* CrashReporter tvOS Universal */; + targetProxy = C2C74DC9253850A500313817 /* PBXContainerItemProxy */; + }; + C2C74DCC253850A500313817 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = C25664D123571D330088513E /* CrashReporter Documentation */; + targetProxy = C2C74DCB253850A500313817 /* PBXContainerItemProxy */; + }; + C2C74DCE253850A500313817 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 05E731E20EFA1A3E005EDFB7 /* plcrashutil */; + targetProxy = C2C74DCD253850A500313817 /* PBXContainerItemProxy */; + }; + C2C74E1D25385F5B00313817 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = C2C74D9B2538508300313817 /* CrashReporter */; + targetProxy = C2C74E1C25385F5B00313817 /* PBXContainerItemProxy */; + }; + C2CBA40A24586975001B775F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 8DC2EF4F0486A6940098B216 /* CrashReporter macOS Framework */; + targetProxy = C2CBA40924586975001B775F /* PBXContainerItemProxy */; + }; + C2CBA40D245869ED001B775F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 058812B81040582D009128FB /* CrashReporter iOS Framework */; + targetProxy = C2CBA40C245869ED001B775F /* PBXContainerItemProxy */; + }; + C2CBA40F245869F4001B775F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 8064D88B1C4D22E5005A8B4C /* CrashReporter tvOS Framework */; + targetProxy = C2CBA40E245869F4001B775F /* PBXContainerItemProxy */; + }; + F8CF2BCD246C05D100904633 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = C2B90D952456FBD000834AFB /* CrashReporter iOS Universal */; + targetProxy = F8CF2BCC246C05D100904633 /* PBXContainerItemProxy */; + }; + F8CF2BD1246C05D100904633 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = C2B90D992456FBDE00834AFB /* CrashReporter tvOS Universal */; + targetProxy = F8CF2BD0246C05D100904633 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 050DE24F0F61B80C00152ED3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "-"; + PRODUCT_NAME = "Fuzz Testing"; + SDKROOT = macosx; + }; + name = Debug; + }; + 050DE2500F61B80C00152ED3 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "-"; + PRODUCT_NAME = "Fuzz Testing"; + SDKROOT = macosx; + }; + name = Release; + }; + 052A45D2136353FB00987004 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + "ARCHS[sdk=iphoneos*]" = ( + "$(ARCHS_STANDARD)", + armv7s, + arm64e, + ); + CODE_SIGN_ENTITLEMENTS = "Resources/DemoCrash-iOS.entitlements"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 5Z97G9NZQ6; + INFOPLIST_FILE = "Resources/DemoCrash-iOS-Info.plist"; + OTHER_LDFLAGS = "-all_load"; + PRODUCT_BUNDLE_IDENTIFIER = "com.microsoft.plcrashreporter.DemoCrash-iOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = iphoneos; + SUPPORTS_MACCATALYST = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 052A45D3136353FB00987004 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + "ARCHS[sdk=iphoneos*]" = ( + "$(ARCHS_STANDARD)", + armv7s, + arm64e, + ); + CODE_SIGN_ENTITLEMENTS = "Resources/DemoCrash-iOS.entitlements"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 5Z97G9NZQ6; + INFOPLIST_FILE = "Resources/DemoCrash-iOS-Info.plist"; + OTHER_LDFLAGS = "-all_load"; + PRODUCT_BUNDLE_IDENTIFIER = "com.microsoft.plcrashreporter.DemoCrash-iOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = iphoneos; + SUPPORTS_MACCATALYST = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 058812BC1040582E009128FB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + "ARCHS[sdk=iphoneos*]" = ( + "$(ARCHS_STANDARD)", + armv7s, + arm64e, + ); + "ARCHS[sdk=iphonesimulator*]" = "$(PL_SIMULATOR_ARCHS)"; + DEFINES_MODULE = YES; + GENERATE_MASTER_OBJECT_FILE = YES; + INFOPLIST_FILE = Resources/Info.plist; + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = Resources/CrashReporter.modulemap; + OTHER_CFLAGS = "-fembed-bitcode-marker"; + "OTHER_CFLAGS[sdk=macosx*]" = ""; + PRELINK_LIBS = "$(CONFIGURATION_BUILD_DIR)/lib$(PRODUCT_NAME).a"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 058812BD1040582E009128FB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + "ARCHS[sdk=iphoneos*]" = ( + "$(ARCHS_STANDARD)", + armv7s, + arm64e, + ); + "ARCHS[sdk=iphonesimulator*]" = "$(PL_SIMULATOR_ARCHS)"; + BITCODE_GENERATION_MODE = bitcode; + DEFINES_MODULE = YES; + GENERATE_MASTER_OBJECT_FILE = YES; + INFOPLIST_FILE = Resources/Info.plist; + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = Resources/CrashReporter.modulemap; + OTHER_CFLAGS = "-fembed-bitcode-marker"; + "OTHER_CFLAGS[sdk=iphoneos*]" = "-fembed-bitcode"; + "OTHER_CFLAGS[sdk=macosx*]" = ""; + PRELINK_LIBS = "$(CONFIGURATION_BUILD_DIR)/lib$(PRODUCT_NAME).a"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SUPPORTS_MACCATALYST = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 058812D510405908009128FB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = PLCrashReporter; + }; + name = Debug; + }; + 058812D610405908009128FB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = PLCrashReporter; + }; + name = Release; + }; + 05CD31530EE936AA000FDE88 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + "ARCHS[sdk=iphoneos*]" = ( + "$(ARCHS_STANDARD)", + armv7s, + arm64e, + ); + "ARCHS[sdk=iphonesimulator*]" = "$(PL_SIMULATOR_ARCHS)"; + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + MACH_O_TYPE = staticlib; + OTHER_CFLAGS = "-fembed-bitcode-marker"; + "OTHER_CFLAGS[sdk=macosx*]" = ""; + PUBLIC_HEADERS_FOLDER_PATH = include; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 05CD31540EE936AA000FDE88 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + "ARCHS[sdk=iphoneos*]" = ( + "$(ARCHS_STANDARD)", + armv7s, + arm64e, + ); + "ARCHS[sdk=iphonesimulator*]" = "$(PL_SIMULATOR_ARCHS)"; + BITCODE_GENERATION_MODE = bitcode; + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + MACH_O_TYPE = staticlib; + OTHER_CFLAGS = "-fembed-bitcode-marker"; + "OTHER_CFLAGS[sdk=iphoneos*]" = "-fembed-bitcode"; + "OTHER_CFLAGS[sdk=macosx*]" = ""; + PUBLIC_HEADERS_FOLDER_PATH = include; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 05CD326B0EE93DC4000FDE88 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + DEVELOPMENT_TEAM = 5Z97G9NZQ6; + EFFECTIVE_PLATFORM_NAME = "-macosx"; + INFOPLIST_FILE = "Resources/Tests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "com.microsoft.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "Tests macOS"; + SDKROOT = macosx; + WRAPPER_EXTENSION = xctest; + }; + name = Debug; + }; + 05CD326C0EE93DC4000FDE88 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + DEVELOPMENT_TEAM = 5Z97G9NZQ6; + EFFECTIVE_PLATFORM_NAME = "-macosx"; + INFOPLIST_FILE = "Resources/Tests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "com.microsoft.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "Tests macOS"; + SDKROOT = macosx; + WRAPPER_EXTENSION = xctest; + }; + name = Release; + }; + 05CD33270EE9443A000FDE88 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + "ARCHS[sdk=iphonesimulator*]" = "$(PL_SIMULATOR_ARCHS)"; + DEVELOPMENT_TEAM = 5Z97G9NZQ6; + INFOPLIST_FILE = "Resources/Tests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "com.microsoft.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "Tests iOS"; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + WRAPPER_EXTENSION = xctest; + }; + name = Debug; + }; + 05CD33280EE9443A000FDE88 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + "ARCHS[sdk=iphonesimulator*]" = "$(PL_SIMULATOR_ARCHS)"; + DEVELOPMENT_TEAM = 5Z97G9NZQ6; + INFOPLIST_FILE = "Resources/Tests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "com.microsoft.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "Tests iOS"; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + WRAPPER_EXTENSION = xctest; + }; + name = Release; + }; + 05E731E50EFA1A3E005EDFB7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "-"; + EFFECTIVE_PLATFORM_NAME = "-macosx"; + PRODUCT_NAME = plcrashutil; + SDKROOT = macosx; + }; + name = Debug; + }; + 05E731E60EFA1A3E005EDFB7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "-"; + EFFECTIVE_PLATFORM_NAME = "-macosx"; + PRODUCT_NAME = plcrashutil; + SDKROOT = macosx; + }; + name = Release; + }; + 05E731F40EFA1AAC005EDFB7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + EFFECTIVE_PLATFORM_NAME = "-macosx"; + PUBLIC_HEADERS_FOLDER_PATH = include; + SDKROOT = macosx; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 05E731F50EFA1AAC005EDFB7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + EFFECTIVE_PLATFORM_NAME = "-macosx"; + PUBLIC_HEADERS_FOLDER_PATH = include; + SDKROOT = macosx; + SKIP_INSTALL = YES; + }; + name = Release; + }; + 05F40CEA0EF7AB81008050CF /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 5Z97G9NZQ6; + EFFECTIVE_PLATFORM_NAME = "-macosx"; + INFOPLIST_FILE = "Resources/DemoCrash-macOS-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + OTHER_LDFLAGS = "-all_load"; + PRODUCT_BUNDLE_IDENTIFIER = "com.microsoft.plcrashreporter.DemoCrash-macOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + }; + name = Debug; + }; + 05F40CEB0EF7AB81008050CF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 5Z97G9NZQ6; + EFFECTIVE_PLATFORM_NAME = "-macosx"; + INFOPLIST_FILE = "Resources/DemoCrash-macOS-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + OTHER_LDFLAGS = "-all_load"; + PRODUCT_BUNDLE_IDENTIFIER = "com.microsoft.plcrashreporter.DemoCrash-macOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + }; + name = Release; + }; + 1DEB91AE08733DA50010E9CD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + DEFINES_MODULE = YES; + EFFECTIVE_PLATFORM_NAME = "-macosx"; + EXPORTED_SYMBOLS_FILE = Resources/CrashReporter.exp; + GENERATE_MASTER_OBJECT_FILE = YES; + INFOPLIST_FILE = Resources/Info.plist; + INSTALL_PATH = "@rpath"; + MACH_O_TYPE = mh_dylib; + MODULEMAP_FILE = Resources/CrashReporter.modulemap; + OTHER_LDFLAGS = "-ObjC"; + PRELINK_LIBS = "$(CONFIGURATION_BUILD_DIR)/lib$(PRODUCT_NAME).a"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 1DEB91AF08733DA50010E9CD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + DEFINES_MODULE = YES; + EFFECTIVE_PLATFORM_NAME = "-macosx"; + EXPORTED_SYMBOLS_FILE = Resources/CrashReporter.exp; + GENERATE_MASTER_OBJECT_FILE = YES; + INFOPLIST_FILE = Resources/Info.plist; + INSTALL_PATH = "@rpath"; + MACH_O_TYPE = mh_dylib; + MODULEMAP_FILE = Resources/CrashReporter.modulemap; + OTHER_LDFLAGS = "-ObjC"; + PRELINK_LIBS = "$(CONFIGURATION_BUILD_DIR)/lib$(PRODUCT_NAME).a"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + }; + name = Release; + }; + 1DEB91B208733DA50010E9CD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; + 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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1.8.1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = c11; + GCC_ENABLE_CPP_EXCEPTIONS = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "$(SRCROOT)/Source/PLCrashNamespace.h"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "PLCF_MIN_MACOSX_SDK=$(PL_MIN_MACOSX_SDK)", + PLCR_PRIVATE, + PLCF_DEBUG_BUILD, + ); + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = "\"$(SRCROOT)/Dependencies/protobuf-c\""; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MACOSX_DEPLOYMENT_TARGET = 10.9; + ONLY_ACTIVE_ARCH = YES; + OTHER_LIBTOOLFLAGS = "-no_warning_for_no_symbols"; + PL_SIMULATOR_ARCHS = "i386 x86_64"; + PRODUCT_BUNDLE_IDENTIFIER = "com.microsoft.${PRODUCT_NAME:identifier}"; + PRODUCT_NAME = CrashReporter; + PROTOBUF_C_UNPACK_ERROR = PLCF_DEBUG; + TVOS_DEPLOYMENT_TARGET = 9.0; + WARNING_CFLAGS = "-Wall"; + }; + name = Debug; + }; + 1DEB91B308733DA50010E9CD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + APPLICATION_EXTENSION_API_ONLY = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_CODE_COVERAGE = NO; + 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_DEPRECATED_OBJC_IMPLEMENTATIONS = 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_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1.8.1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = c11; + GCC_ENABLE_CPP_EXCEPTIONS = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "$(SRCROOT)/Source/PLCrashNamespace.h"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "PLCF_MIN_MACOSX_SDK=$(PL_MIN_MACOSX_SDK)", + PLCR_PRIVATE, + PLCF_RELEASE_BUILD, + ); + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = "\"$(SRCROOT)/Dependencies/protobuf-c\""; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MACOSX_DEPLOYMENT_TARGET = 10.9; + OTHER_LIBTOOLFLAGS = "-no_warning_for_no_symbols"; + PL_SIMULATOR_ARCHS = "i386 x86_64"; + PRODUCT_BUNDLE_IDENTIFIER = "com.microsoft.${PRODUCT_NAME:identifier}"; + PRODUCT_NAME = CrashReporter; + PROTOBUF_C_UNPACK_ERROR = PLCF_DEBUG; + TVOS_DEPLOYMENT_TARGET = 9.0; + VALIDATE_PRODUCT = YES; + WARNING_CFLAGS = "-Wall"; + }; + name = Release; + }; + 8064D8191C4D22D8005A8B4C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + "ARCHS[sdk=appletvsimulator*]" = "$(PL_SIMULATOR_ARCHS)"; + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + MACH_O_TYPE = staticlib; + OTHER_CFLAGS = "-fembed-bitcode-marker"; + PUBLIC_HEADERS_FOLDER_PATH = include; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 8064D81A1C4D22D8005A8B4C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + "ARCHS[sdk=appletvsimulator*]" = "$(PL_SIMULATOR_ARCHS)"; + BITCODE_GENERATION_MODE = bitcode; + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + MACH_O_TYPE = staticlib; + OTHER_CFLAGS = "-fembed-bitcode-marker"; + "OTHER_CFLAGS[sdk=appletvos*]" = "-fembed-bitcode"; + PUBLIC_HEADERS_FOLDER_PATH = include; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + }; + name = Release; + }; + 8064D8B91C4D22E5005A8B4C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + "ARCHS[sdk=appletvsimulator*]" = "$(PL_SIMULATOR_ARCHS)"; + CODE_SIGN_STYLE = Manual; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + GENERATE_MASTER_OBJECT_FILE = YES; + INFOPLIST_FILE = Resources/Info.plist; + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = Resources/CrashReporter.modulemap; + OTHER_CFLAGS = "-fembed-bitcode-marker"; + PRELINK_LIBS = "$(CONFIGURATION_BUILD_DIR)/lib$(PRODUCT_NAME).a"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 8064D8BA1C4D22E5005A8B4C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + "ARCHS[sdk=appletvsimulator*]" = "$(PL_SIMULATOR_ARCHS)"; + BITCODE_GENERATION_MODE = bitcode; + CODE_SIGN_STYLE = Manual; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + GENERATE_MASTER_OBJECT_FILE = YES; + INFOPLIST_FILE = Resources/Info.plist; + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = Resources/CrashReporter.modulemap; + OTHER_CFLAGS = "-fembed-bitcode-marker"; + "OTHER_CFLAGS[sdk=appletvos*]" = "-fembed-bitcode"; + PRELINK_LIBS = "$(CONFIGURATION_BUILD_DIR)/lib$(PRODUCT_NAME).a"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + }; + name = Release; + }; + 8064D9931C4D27E2005A8B4C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + "ARCHS[sdk=appletvsimulator*]" = "$(PL_SIMULATOR_ARCHS)"; + DEVELOPMENT_TEAM = 5Z97G9NZQ6; + INFOPLIST_FILE = "Resources/Tests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "com.microsoft.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + WRAPPER_EXTENSION = xctest; + }; + name = Debug; + }; + 8064D9941C4D27E2005A8B4C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + "ARCHS[sdk=appletvsimulator*]" = "$(PL_SIMULATOR_ARCHS)"; + DEVELOPMENT_TEAM = 5Z97G9NZQ6; + INFOPLIST_FILE = "Resources/Tests-Info.plist"; + PRODUCT_BUNDLE_IDENTIFIER = "com.microsoft.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + WRAPPER_EXTENSION = xctest; + }; + name = Release; + }; + 8064D9A11C4D27E9005A8B4C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = 5Z97G9NZQ6; + INFOPLIST_FILE = "Resources/DemoCrash-tvOS-Info.plist"; + OTHER_LDFLAGS = "-all_load"; + PRODUCT_BUNDLE_IDENTIFIER = "com.microsoft.plcrashreporter.DemoCrash-tvOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + }; + name = Debug; + }; + 8064D9A21C4D27E9005A8B4C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = 5Z97G9NZQ6; + INFOPLIST_FILE = "Resources/DemoCrash-tvOS-Info.plist"; + OTHER_LDFLAGS = "-all_load"; + PRODUCT_BUNDLE_IDENTIFIER = "com.microsoft.plcrashreporter.DemoCrash-tvOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + }; + name = Release; + }; + C25664D323571D340088513E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + C25664D423571D340088513E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + C2B90D972456FBD000834AFB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + EFFECTIVE_PLATFORM_NAME = "-iphoneuniversal"; + MODULEMAP_FILE = Resources/CrashReporter.modulemap; + SDKROOT = iphoneos; + }; + name = Debug; + }; + C2B90D982456FBD000834AFB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + EFFECTIVE_PLATFORM_NAME = "-iphoneuniversal"; + MODULEMAP_FILE = Resources/CrashReporter.modulemap; + SDKROOT = iphoneos; + }; + name = Release; + }; + C2B90D9B2456FBDF00834AFB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + EFFECTIVE_PLATFORM_NAME = "-appletvuniversal"; + MODULEMAP_FILE = Resources/CrashReporter.modulemap; + SDKROOT = appletvos; + }; + name = Debug; + }; + C2B90D9C2456FBDF00834AFB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + EFFECTIVE_PLATFORM_NAME = "-appletvuniversal"; + MODULEMAP_FILE = Resources/CrashReporter.modulemap; + SDKROOT = appletvos; + }; + name = Release; + }; + C2C74D9C2538508300313817 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + C2C74D9D2538508300313817 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + F8B9C72C24695BE600B9FEF6 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + EFFECTIVE_PLATFORM_NAME = "-xcframework"; + }; + name = Debug; + }; + F8B9C72D24695BE600B9FEF6 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + EFFECTIVE_PLATFORM_NAME = "-xcframework"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 050DE2590F61B81C00152ED3 /* Build configuration list for PBXNativeTarget "Fuzz Testing" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 050DE24F0F61B80C00152ED3 /* Debug */, + 050DE2500F61B80C00152ED3 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 052A45D4136353FC00987004 /* Build configuration list for PBXNativeTarget "DemoCrash iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 052A45D2136353FB00987004 /* Debug */, + 052A45D3136353FB00987004 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 058812BE1040582E009128FB /* Build configuration list for PBXNativeTarget "CrashReporter iOS Framework" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 058812BC1040582E009128FB /* Debug */, + 058812BD1040582E009128FB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 058812D71040592A009128FB /* Build configuration list for PBXAggregateTarget "CrashReporter Archive" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 058812D510405908009128FB /* Debug */, + 058812D610405908009128FB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05CD315E0EE936E4000FDE88 /* Build configuration list for PBXNativeTarget "CrashReporter iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05CD31530EE936AA000FDE88 /* Debug */, + 05CD31540EE936AA000FDE88 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05CD326D0EE93DC4000FDE88 /* Build configuration list for PBXNativeTarget "CrashReporter macOS Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05CD326B0EE93DC4000FDE88 /* Debug */, + 05CD326C0EE93DC4000FDE88 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05CD33290EE9443A000FDE88 /* Build configuration list for PBXNativeTarget "CrashReporter iOS Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05CD33270EE9443A000FDE88 /* Debug */, + 05CD33280EE9443A000FDE88 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05E731EA0EFA1A56005EDFB7 /* Build configuration list for PBXNativeTarget "plcrashutil" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E731E50EFA1A3E005EDFB7 /* Debug */, + 05E731E60EFA1A3E005EDFB7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05E7320C0EFA1B09005EDFB7 /* Build configuration list for PBXNativeTarget "CrashReporter macOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05E731F40EFA1AAC005EDFB7 /* Debug */, + 05E731F50EFA1AAC005EDFB7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 05F40CEC0EF7AB81008050CF /* Build configuration list for PBXNativeTarget "DemoCrash macOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 05F40CEA0EF7AB81008050CF /* Debug */, + 05F40CEB0EF7AB81008050CF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1DEB91AD08733DA50010E9CD /* Build configuration list for PBXNativeTarget "CrashReporter macOS Framework" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1DEB91AE08733DA50010E9CD /* Debug */, + 1DEB91AF08733DA50010E9CD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1DEB91B108733DA50010E9CD /* Build configuration list for PBXProject "CrashReporter" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1DEB91B208733DA50010E9CD /* Debug */, + 1DEB91B308733DA50010E9CD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8064D8181C4D22D8005A8B4C /* Build configuration list for PBXNativeTarget "CrashReporter tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8064D8191C4D22D8005A8B4C /* Debug */, + 8064D81A1C4D22D8005A8B4C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8064D8B81C4D22E5005A8B4C /* Build configuration list for PBXNativeTarget "CrashReporter tvOS Framework" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8064D8B91C4D22E5005A8B4C /* Debug */, + 8064D8BA1C4D22E5005A8B4C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8064D9921C4D27E2005A8B4C /* Build configuration list for PBXNativeTarget "CrashReporter tvOS Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8064D9931C4D27E2005A8B4C /* Debug */, + 8064D9941C4D27E2005A8B4C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8064D9A01C4D27E9005A8B4C /* Build configuration list for PBXNativeTarget "DemoCrash tvOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8064D9A11C4D27E9005A8B4C /* Debug */, + 8064D9A21C4D27E9005A8B4C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C25664D223571D340088513E /* Build configuration list for PBXAggregateTarget "CrashReporter Documentation" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C25664D323571D340088513E /* Debug */, + C25664D423571D340088513E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C2B90D962456FBD000834AFB /* Build configuration list for PBXAggregateTarget "CrashReporter iOS Universal" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C2B90D972456FBD000834AFB /* Debug */, + C2B90D982456FBD000834AFB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C2B90D9A2456FBDF00834AFB /* Build configuration list for PBXAggregateTarget "CrashReporter tvOS Universal" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C2B90D9B2456FBDF00834AFB /* Debug */, + C2B90D9C2456FBDF00834AFB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C2C74D9E2538508300313817 /* Build configuration list for PBXAggregateTarget "CrashReporter" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C2C74D9C2538508300313817 /* Debug */, + C2C74D9D2538508300313817 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F8B9C72B24695BE600B9FEF6 /* Build configuration list for PBXAggregateTarget "CrashReporter XCFramework" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F8B9C72C24695BE600B9FEF6 /* Debug */, + F8B9C72D24695BE600B9FEF6 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 0867D690FE84028FC02AAC07 /* Project object */; +} diff --git a/submodules/AppCenter-sdk/Vendor/PLCrashReporter/CrashReporter.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/CrashReporter.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/CrashReporter.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/submodules/AppCenter-sdk/Vendor/PLCrashReporter/CrashReporter.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/CrashReporter.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/CrashReporter.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/submodules/AppCenter-sdk/Vendor/PLCrashReporter/Dependencies/cgmanifest.json b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/Dependencies/cgmanifest.json new file mode 100644 index 0000000000..edc14a8f44 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/Dependencies/cgmanifest.json @@ -0,0 +1,15 @@ +{ + "Registrations": [ + { + "component": { + "type": "git", + "git": { + "name": "protobuf-c", + "repositoryUrl": "https://github.com/protobuf-c/protobuf-c.git", + "commitHash": "1390409f4ee4e26d0635310995b516eb702c3f9e" + } + } + } + ], + "Version": 1 +} \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/PLCrashReporter/Dependencies/protobuf-c/protobuf-c/protobuf-c.c b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/Dependencies/protobuf-c/protobuf-c/protobuf-c.c new file mode 100644 index 0000000000..4733b0a65a --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/Dependencies/protobuf-c/protobuf-c/protobuf-c.c @@ -0,0 +1,3666 @@ +/* + * Copyright (c) 2008-2015, Dave Benson and the protobuf-c authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/*! \file + * Support library for `protoc-c` generated code. + * + * This file implements the public API used by the code generated + * by `protoc-c`. + * + * \authors Dave Benson and the protobuf-c authors + * + * \copyright 2008-2014. Licensed under the terms of the [BSD-2-Clause] license. + */ + +/** + * \todo 64-BIT OPTIMIZATION: certain implementations use 32-bit math + * even on 64-bit platforms (uint64_size, uint64_pack, parse_uint64). + * + * \todo Use size_t consistently. + */ + +#include /* for malloc, free */ +#include /* for strcmp, strlen, memcpy, memmove, memset */ + +#include "protobuf-c.h" + +#define TRUE 1 +#define FALSE 0 + +#define PROTOBUF_C__ASSERT_NOT_REACHED() assert(0) + +/* Workaround for Microsoft compilers. */ +#ifdef _MSC_VER +# define inline __inline +#endif + +/** + * \defgroup internal Internal functions and macros + * + * These are not exported by the library but are useful to developers working + * on `libprotobuf-c` itself. + */ + +/** + * \defgroup macros Utility macros for manipulating structures + * + * Macros and constants used to manipulate the base "classes" generated by + * `protobuf-c`. They also define limits and check correctness. + * + * \ingroup internal + * @{ + */ + +/** The maximum length of a 64-bit integer in varint encoding. */ +#define MAX_UINT64_ENCODED_SIZE 10 + +#ifndef PROTOBUF_C_UNPACK_ERROR +# define PROTOBUF_C_UNPACK_ERROR(...) +#endif + +const char protobuf_c_empty_string[] = ""; + +/** + * Internal `ProtobufCMessage` manipulation macro. + * + * Base macro for manipulating a `ProtobufCMessage`. Used by STRUCT_MEMBER() and + * STRUCT_MEMBER_PTR(). + */ +#define STRUCT_MEMBER_P(struct_p, struct_offset) \ + ((void *) ((uint8_t *) (struct_p) + (struct_offset))) + +/** + * Return field in a `ProtobufCMessage` based on offset. + * + * Take a pointer to a `ProtobufCMessage` and find the field at the offset. + * Cast it to the passed type. + */ +#define STRUCT_MEMBER(member_type, struct_p, struct_offset) \ + (*(member_type *) STRUCT_MEMBER_P((struct_p), (struct_offset))) + +/** + * Return field in a `ProtobufCMessage` based on offset. + * + * Take a pointer to a `ProtobufCMessage` and find the field at the offset. Cast + * it to a pointer to the passed type. + */ +#define STRUCT_MEMBER_PTR(member_type, struct_p, struct_offset) \ + ((member_type *) STRUCT_MEMBER_P((struct_p), (struct_offset))) + +/* Assertions for magic numbers. */ + +#define ASSERT_IS_ENUM_DESCRIPTOR(desc) \ + assert((desc)->magic == PROTOBUF_C__ENUM_DESCRIPTOR_MAGIC) + +#define ASSERT_IS_MESSAGE_DESCRIPTOR(desc) \ + assert((desc)->magic == PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC) + +#define ASSERT_IS_MESSAGE(message) \ + ASSERT_IS_MESSAGE_DESCRIPTOR((message)->descriptor) + +#define ASSERT_IS_SERVICE_DESCRIPTOR(desc) \ + assert((desc)->magic == PROTOBUF_C__SERVICE_DESCRIPTOR_MAGIC) + +/**@}*/ + +/* --- version --- */ + +const char * +protobuf_c_version(void) +{ + return PROTOBUF_C_VERSION; +} + +uint32_t +protobuf_c_version_number(void) +{ + return PROTOBUF_C_VERSION_NUMBER; +} + +/* --- allocator --- */ + +static void * +system_alloc(void *allocator_data, size_t size) +{ + return malloc(size); +} + +static void +system_free(void *allocator_data, void *data) +{ + free(data); +} + +static inline void * +do_alloc(ProtobufCAllocator *allocator, size_t size) +{ + return allocator->alloc(allocator->allocator_data, size); +} + +static inline void +do_free(ProtobufCAllocator *allocator, void *data) +{ + if (data != NULL) + allocator->free(allocator->allocator_data, data); +} + +/* + * This allocator uses the system's malloc() and free(). It is the default + * allocator used if NULL is passed as the ProtobufCAllocator to an exported + * function. + */ +static ProtobufCAllocator protobuf_c__allocator = { + .alloc = &system_alloc, + .free = &system_free, + .allocator_data = NULL, +}; + +/* === buffer-simple === */ + +void +protobuf_c_buffer_simple_append(ProtobufCBuffer *buffer, + size_t len, const uint8_t *data) +{ + ProtobufCBufferSimple *simp = (ProtobufCBufferSimple *) buffer; + size_t new_len = simp->len + len; + + if (new_len > simp->alloced) { + ProtobufCAllocator *allocator = simp->allocator; + size_t new_alloced = simp->alloced * 2; + uint8_t *new_data; + + if (allocator == NULL) + allocator = &protobuf_c__allocator; + while (new_alloced < new_len) + new_alloced += new_alloced; + new_data = do_alloc(allocator, new_alloced); + if (!new_data) + return; + memcpy(new_data, simp->data, simp->len); + if (simp->must_free_data) + do_free(allocator, simp->data); + else + simp->must_free_data = TRUE; + simp->data = new_data; + simp->alloced = new_alloced; + } + memcpy(simp->data + simp->len, data, len); + simp->len = new_len; +} + +/** + * \defgroup packedsz protobuf_c_message_get_packed_size() implementation + * + * Routines mainly used by protobuf_c_message_get_packed_size(). + * + * \ingroup internal + * @{ + */ + +/** + * Return the number of bytes required to store the tag for the field. Includes + * 3 bits for the wire-type, and a single bit that denotes the end-of-tag. + * + * \param number + * Field tag to encode. + * \return + * Number of bytes required. + */ +static inline size_t +get_tag_size(uint32_t number) +{ + if (number < (1UL << 4)) { + return 1; + } else if (number < (1UL << 11)) { + return 2; + } else if (number < (1UL << 18)) { + return 3; + } else if (number < (1UL << 25)) { + return 4; + } else { + return 5; + } +} + +/** + * Return the number of bytes required to store a variable-length unsigned + * 32-bit integer in base-128 varint encoding. + * + * \param v + * Value to encode. + * \return + * Number of bytes required. + */ +static inline size_t +uint32_size(uint32_t v) +{ + if (v < (1UL << 7)) { + return 1; + } else if (v < (1UL << 14)) { + return 2; + } else if (v < (1UL << 21)) { + return 3; + } else if (v < (1UL << 28)) { + return 4; + } else { + return 5; + } +} + +/** + * Return the number of bytes required to store a variable-length signed 32-bit + * integer in base-128 varint encoding. + * + * \param v + * Value to encode. + * \return + * Number of bytes required. + */ +static inline size_t +int32_size(int32_t v) +{ + if (v < 0) { + return 10; + } else if (v < (1L << 7)) { + return 1; + } else if (v < (1L << 14)) { + return 2; + } else if (v < (1L << 21)) { + return 3; + } else if (v < (1L << 28)) { + return 4; + } else { + return 5; + } +} + +/** + * Return the ZigZag-encoded 32-bit unsigned integer form of a 32-bit signed + * integer. + * + * \param v + * Value to encode. + * \return + * ZigZag encoded integer. + */ +static inline uint32_t +zigzag32(int32_t v) +{ + if (v < 0) + return (-(uint32_t)v) * 2 - 1; + else + return (uint32_t)(v) * 2; +} + +/** + * Return the number of bytes required to store a signed 32-bit integer, + * converted to an unsigned 32-bit integer with ZigZag encoding, using base-128 + * varint encoding. + * + * \param v + * Value to encode. + * \return + * Number of bytes required. + */ +static inline size_t +sint32_size(int32_t v) +{ + return uint32_size(zigzag32(v)); +} + +/** + * Return the number of bytes required to store a 64-bit unsigned integer in + * base-128 varint encoding. + * + * \param v + * Value to encode. + * \return + * Number of bytes required. + */ +static inline size_t +uint64_size(uint64_t v) +{ + uint32_t upper_v = (uint32_t) (v >> 32); + + if (upper_v == 0) { + return uint32_size((uint32_t) v); + } else if (upper_v < (1UL << 3)) { + return 5; + } else if (upper_v < (1UL << 10)) { + return 6; + } else if (upper_v < (1UL << 17)) { + return 7; + } else if (upper_v < (1UL << 24)) { + return 8; + } else if (upper_v < (1UL << 31)) { + return 9; + } else { + return 10; + } +} + +/** + * Return the ZigZag-encoded 64-bit unsigned integer form of a 64-bit signed + * integer. + * + * \param v + * Value to encode. + * \return + * ZigZag encoded integer. + */ +static inline uint64_t +zigzag64(int64_t v) +{ + if (v < 0) + return (-(uint64_t)v) * 2 - 1; + else + return (uint64_t)(v) * 2; +} + +/** + * Return the number of bytes required to store a signed 64-bit integer, + * converted to an unsigned 64-bit integer with ZigZag encoding, using base-128 + * varint encoding. + * + * \param v + * Value to encode. + * \return + * Number of bytes required. + */ +static inline size_t +sint64_size(int64_t v) +{ + return uint64_size(zigzag64(v)); +} + +/** + * Calculate the serialized size of a single required message field, including + * the space needed by the preceding tag. + * + * \param field + * Field descriptor for member. + * \param member + * Field to encode. + * \return + * Number of bytes required. + */ +static size_t +required_field_get_packed_size(const ProtobufCFieldDescriptor *field, + const void *member) +{ + size_t rv = get_tag_size(field->id); + + switch (field->type) { + case PROTOBUF_C_TYPE_SINT32: + return rv + sint32_size(*(const int32_t *) member); + case PROTOBUF_C_TYPE_ENUM: + case PROTOBUF_C_TYPE_INT32: + return rv + int32_size(*(const int32_t *) member); + case PROTOBUF_C_TYPE_UINT32: + return rv + uint32_size(*(const uint32_t *) member); + case PROTOBUF_C_TYPE_SINT64: + return rv + sint64_size(*(const int64_t *) member); + case PROTOBUF_C_TYPE_INT64: + case PROTOBUF_C_TYPE_UINT64: + return rv + uint64_size(*(const uint64_t *) member); + case PROTOBUF_C_TYPE_SFIXED32: + case PROTOBUF_C_TYPE_FIXED32: + return rv + 4; + case PROTOBUF_C_TYPE_SFIXED64: + case PROTOBUF_C_TYPE_FIXED64: + return rv + 8; + case PROTOBUF_C_TYPE_BOOL: + return rv + 1; + case PROTOBUF_C_TYPE_FLOAT: + return rv + 4; + case PROTOBUF_C_TYPE_DOUBLE: + return rv + 8; + case PROTOBUF_C_TYPE_STRING: { + const char *str = *(char * const *) member; + size_t len = str ? strlen(str) : 0; + return rv + uint32_size(len) + len; + } + case PROTOBUF_C_TYPE_BYTES: { + size_t len = ((const ProtobufCBinaryData *) member)->len; + return rv + uint32_size(len) + len; + } + case PROTOBUF_C_TYPE_MESSAGE: { + const ProtobufCMessage *msg = *(ProtobufCMessage * const *) member; + size_t subrv = msg ? protobuf_c_message_get_packed_size(msg) : 0; + return rv + uint32_size(subrv) + subrv; + } + } + PROTOBUF_C__ASSERT_NOT_REACHED(); + return 0; +} + +/** + * Calculate the serialized size of a single oneof message field, including + * the space needed by the preceding tag. Returns 0 if the oneof field isn't + * selected or is not set. + * + * \param field + * Field descriptor for member. + * \param oneof_case + * Enum value that selects the field in the oneof. + * \param member + * Field to encode. + * \return + * Number of bytes required. + */ +static size_t +oneof_field_get_packed_size(const ProtobufCFieldDescriptor *field, + uint32_t oneof_case, + const void *member) +{ + if (oneof_case != field->id) { + return 0; + } + if (field->type == PROTOBUF_C_TYPE_MESSAGE || + field->type == PROTOBUF_C_TYPE_STRING) + { + const void *ptr = *(const void * const *) member; + if (ptr == NULL || ptr == field->default_value) + return 0; + } + return required_field_get_packed_size(field, member); +} + +/** + * Calculate the serialized size of a single optional message field, including + * the space needed by the preceding tag. Returns 0 if the optional field isn't + * set. + * + * \param field + * Field descriptor for member. + * \param has + * True if the field exists, false if not. + * \param member + * Field to encode. + * \return + * Number of bytes required. + */ +static size_t +optional_field_get_packed_size(const ProtobufCFieldDescriptor *field, + const protobuf_c_boolean has, + const void *member) +{ + if (field->type == PROTOBUF_C_TYPE_MESSAGE || + field->type == PROTOBUF_C_TYPE_STRING) + { + const void *ptr = *(const void * const *) member; + if (ptr == NULL || ptr == field->default_value) + return 0; + } else { + if (!has) + return 0; + } + return required_field_get_packed_size(field, member); +} + +static protobuf_c_boolean +field_is_zeroish(const ProtobufCFieldDescriptor *field, + const void *member) +{ + protobuf_c_boolean ret = FALSE; + + switch (field->type) { + case PROTOBUF_C_TYPE_BOOL: + ret = (0 == *(const protobuf_c_boolean *) member); + break; + case PROTOBUF_C_TYPE_ENUM: + case PROTOBUF_C_TYPE_SINT32: + case PROTOBUF_C_TYPE_INT32: + case PROTOBUF_C_TYPE_UINT32: + case PROTOBUF_C_TYPE_SFIXED32: + case PROTOBUF_C_TYPE_FIXED32: + ret = (0 == *(const uint32_t *) member); + break; + case PROTOBUF_C_TYPE_SINT64: + case PROTOBUF_C_TYPE_INT64: + case PROTOBUF_C_TYPE_UINT64: + case PROTOBUF_C_TYPE_SFIXED64: + case PROTOBUF_C_TYPE_FIXED64: + ret = (0 == *(const uint64_t *) member); + break; + case PROTOBUF_C_TYPE_FLOAT: + ret = (0 == *(const float *) member); + break; + case PROTOBUF_C_TYPE_DOUBLE: + ret = (0 == *(const double *) member); + break; + case PROTOBUF_C_TYPE_STRING: + ret = (NULL == *(const char * const *) member) || + ('\0' == **(const char * const *) member); + break; + case PROTOBUF_C_TYPE_BYTES: + case PROTOBUF_C_TYPE_MESSAGE: + ret = (NULL == *(const void * const *) member); + break; + default: + ret = TRUE; + break; + } + + return ret; +} + +/** + * Calculate the serialized size of a single unlabeled message field, including + * the space needed by the preceding tag. Returns 0 if the field isn't set or + * if it is set to a "zeroish" value (null pointer or 0 for numerical values). + * Unlabeled fields are supported only in proto3. + * + * \param field + * Field descriptor for member. + * \param member + * Field to encode. + * \return + * Number of bytes required. + */ +static size_t +unlabeled_field_get_packed_size(const ProtobufCFieldDescriptor *field, + const void *member) +{ + if (field_is_zeroish(field, member)) + return 0; + return required_field_get_packed_size(field, member); +} + +/** + * Calculate the serialized size of repeated message fields, which may consist + * of any number of values (including 0). Includes the space needed by the + * preceding tags (as needed). + * + * \param field + * Field descriptor for member. + * \param count + * Number of repeated field members. + * \param member + * Field to encode. + * \return + * Number of bytes required. + */ +static size_t +repeated_field_get_packed_size(const ProtobufCFieldDescriptor *field, + size_t count, const void *member) +{ + size_t header_size; + size_t rv = 0; + unsigned i; + void *array = *(void * const *) member; + + if (count == 0) + return 0; + header_size = get_tag_size(field->id); + if (0 == (field->flags & PROTOBUF_C_FIELD_FLAG_PACKED)) + header_size *= count; + + switch (field->type) { + case PROTOBUF_C_TYPE_SINT32: + for (i = 0; i < count; i++) + rv += sint32_size(((int32_t *) array)[i]); + break; + case PROTOBUF_C_TYPE_ENUM: + case PROTOBUF_C_TYPE_INT32: + for (i = 0; i < count; i++) + rv += int32_size(((int32_t *) array)[i]); + break; + case PROTOBUF_C_TYPE_UINT32: + for (i = 0; i < count; i++) + rv += uint32_size(((uint32_t *) array)[i]); + break; + case PROTOBUF_C_TYPE_SINT64: + for (i = 0; i < count; i++) + rv += sint64_size(((int64_t *) array)[i]); + break; + case PROTOBUF_C_TYPE_INT64: + case PROTOBUF_C_TYPE_UINT64: + for (i = 0; i < count; i++) + rv += uint64_size(((uint64_t *) array)[i]); + break; + case PROTOBUF_C_TYPE_SFIXED32: + case PROTOBUF_C_TYPE_FIXED32: + case PROTOBUF_C_TYPE_FLOAT: + rv += 4 * count; + break; + case PROTOBUF_C_TYPE_SFIXED64: + case PROTOBUF_C_TYPE_FIXED64: + case PROTOBUF_C_TYPE_DOUBLE: + rv += 8 * count; + break; + case PROTOBUF_C_TYPE_BOOL: + rv += count; + break; + case PROTOBUF_C_TYPE_STRING: + for (i = 0; i < count; i++) { + size_t len = strlen(((char **) array)[i]); + rv += uint32_size(len) + len; + } + break; + case PROTOBUF_C_TYPE_BYTES: + for (i = 0; i < count; i++) { + size_t len = ((ProtobufCBinaryData *) array)[i].len; + rv += uint32_size(len) + len; + } + break; + case PROTOBUF_C_TYPE_MESSAGE: + for (i = 0; i < count; i++) { + size_t len = protobuf_c_message_get_packed_size( + ((ProtobufCMessage **) array)[i]); + rv += uint32_size(len) + len; + } + break; + } + + if (0 != (field->flags & PROTOBUF_C_FIELD_FLAG_PACKED)) + header_size += uint32_size(rv); + return header_size + rv; +} + +/** + * Calculate the serialized size of an unknown field, i.e. one that is passed + * through mostly uninterpreted. This is required for forward compatibility if + * new fields are added to the message descriptor. + * + * \param field + * Unknown field type. + * \return + * Number of bytes required. + */ +static inline size_t +unknown_field_get_packed_size(const ProtobufCMessageUnknownField *field) +{ + return get_tag_size(field->tag) + field->len; +} + +/**@}*/ + +/* + * Calculate the serialized size of the message. + */ +size_t protobuf_c_message_get_packed_size(const ProtobufCMessage *message) +{ + unsigned i; + size_t rv = 0; + + ASSERT_IS_MESSAGE(message); + for (i = 0; i < message->descriptor->n_fields; i++) { + const ProtobufCFieldDescriptor *field = + message->descriptor->fields + i; + const void *member = + ((const char *) message) + field->offset; + const void *qmember = + ((const char *) message) + field->quantifier_offset; + + if (field->label == PROTOBUF_C_LABEL_REQUIRED) { + rv += required_field_get_packed_size(field, member); + } else if ((field->label == PROTOBUF_C_LABEL_OPTIONAL || + field->label == PROTOBUF_C_LABEL_NONE) && + (0 != (field->flags & PROTOBUF_C_FIELD_FLAG_ONEOF))) { + rv += oneof_field_get_packed_size( + field, + *(const uint32_t *) qmember, + member + ); + } else if (field->label == PROTOBUF_C_LABEL_OPTIONAL) { + rv += optional_field_get_packed_size( + field, + *(protobuf_c_boolean *) qmember, + member + ); + } else if (field->label == PROTOBUF_C_LABEL_NONE) { + rv += unlabeled_field_get_packed_size( + field, + member + ); + } else { + rv += repeated_field_get_packed_size( + field, + *(const size_t *) qmember, + member + ); + } + } + for (i = 0; i < message->n_unknown_fields; i++) + rv += unknown_field_get_packed_size(&message->unknown_fields[i]); + return rv; +} + +/** + * \defgroup pack protobuf_c_message_pack() implementation + * + * Routines mainly used by protobuf_c_message_pack(). + * + * \ingroup internal + * @{ + */ + +/** + * Pack an unsigned 32-bit integer in base-128 varint encoding and return the + * number of bytes written, which must be 5 or less. + * + * \param value + * Value to encode. + * \param[out] out + * Packed value. + * \return + * Number of bytes written to `out`. + */ +static inline size_t +uint32_pack(uint32_t value, uint8_t *out) +{ + unsigned rv = 0; + + if (value >= 0x80) { + out[rv++] = value | 0x80; + value >>= 7; + if (value >= 0x80) { + out[rv++] = value | 0x80; + value >>= 7; + if (value >= 0x80) { + out[rv++] = value | 0x80; + value >>= 7; + if (value >= 0x80) { + out[rv++] = value | 0x80; + value >>= 7; + } + } + } + } + /* assert: value<128 */ + out[rv++] = value; + return rv; +} + +/** + * Pack a signed 32-bit integer and return the number of bytes written. + * Negative numbers are encoded as two's complement 64-bit integers. + * + * \param value + * Value to encode. + * \param[out] out + * Packed value. + * \return + * Number of bytes written to `out`. + */ +static inline size_t +int32_pack(int32_t value, uint8_t *out) +{ + if (value < 0) { + out[0] = value | 0x80; + out[1] = (value >> 7) | 0x80; + out[2] = (value >> 14) | 0x80; + out[3] = (value >> 21) | 0x80; + out[4] = (value >> 28) | 0x80; + out[5] = out[6] = out[7] = out[8] = 0xff; + out[9] = 0x01; + return 10; + } else { + return uint32_pack(value, out); + } +} + +/** + * Pack a signed 32-bit integer using ZigZag encoding and return the number of + * bytes written. + * + * \param value + * Value to encode. + * \param[out] out + * Packed value. + * \return + * Number of bytes written to `out`. + */ +static inline size_t +sint32_pack(int32_t value, uint8_t *out) +{ + return uint32_pack(zigzag32(value), out); +} + +/** + * Pack a 64-bit unsigned integer using base-128 varint encoding and return the + * number of bytes written. + * + * \param value + * Value to encode. + * \param[out] out + * Packed value. + * \return + * Number of bytes written to `out`. + */ +static size_t +uint64_pack(uint64_t value, uint8_t *out) +{ + uint32_t hi = (uint32_t) (value >> 32); + uint32_t lo = (uint32_t) value; + unsigned rv; + + if (hi == 0) + return uint32_pack((uint32_t) lo, out); + out[0] = (lo) | 0x80; + out[1] = (lo >> 7) | 0x80; + out[2] = (lo >> 14) | 0x80; + out[3] = (lo >> 21) | 0x80; + if (hi < 8) { + out[4] = (hi << 4) | (lo >> 28); + return 5; + } else { + out[4] = ((hi & 7) << 4) | (lo >> 28) | 0x80; + hi >>= 3; + } + rv = 5; + while (hi >= 128) { + out[rv++] = hi | 0x80; + hi >>= 7; + } + out[rv++] = hi; + return rv; +} + +/** + * Pack a 64-bit signed integer in ZigZag encoding and return the number of + * bytes written. + * + * \param value + * Value to encode. + * \param[out] out + * Packed value. + * \return + * Number of bytes written to `out`. + */ +static inline size_t +sint64_pack(int64_t value, uint8_t *out) +{ + return uint64_pack(zigzag64(value), out); +} + +/** + * Pack a 32-bit quantity in little-endian byte order. Used for protobuf wire + * types fixed32, sfixed32, float. Similar to "htole32". + * + * \param value + * Value to encode. + * \param[out] out + * Packed value. + * \return + * Number of bytes written to `out`. + */ +static inline size_t +fixed32_pack(uint32_t value, void *out) +{ +#if !defined(WORDS_BIGENDIAN) + memcpy(out, &value, 4); +#else + uint8_t *buf = out; + + buf[0] = value; + buf[1] = value >> 8; + buf[2] = value >> 16; + buf[3] = value >> 24; +#endif + return 4; +} + +/** + * Pack a 64-bit quantity in little-endian byte order. Used for protobuf wire + * types fixed64, sfixed64, double. Similar to "htole64". + * + * \todo The big-endian impl is really only good for 32-bit machines, a 64-bit + * version would be appreciated, plus a way to decide to use 64-bit math where + * convenient. + * + * \param value + * Value to encode. + * \param[out] out + * Packed value. + * \return + * Number of bytes written to `out`. + */ +static inline size_t +fixed64_pack(uint64_t value, void *out) +{ +#if !defined(WORDS_BIGENDIAN) + memcpy(out, &value, 8); +#else + fixed32_pack(value, out); + fixed32_pack(value >> 32, ((char *) out) + 4); +#endif + return 8; +} + +/** + * Pack a boolean value as an integer and return the number of bytes written. + * + * \todo Perhaps on some platforms *out = !!value would be a better impl, b/c + * that is idiomatic C++ in some STL implementations. + * + * \param value + * Value to encode. + * \param[out] out + * Packed value. + * \return + * Number of bytes written to `out`. + */ +static inline size_t +boolean_pack(protobuf_c_boolean value, uint8_t *out) +{ + *out = value ? TRUE : FALSE; + return 1; +} + +/** + * Pack a NUL-terminated C string and return the number of bytes written. The + * output includes a length delimiter. + * + * The NULL pointer is treated as an empty string. This isn't really necessary, + * but it allows people to leave required strings blank. (See Issue #13 in the + * bug tracker for a little more explanation). + * + * \param str + * String to encode. + * \param[out] out + * Packed value. + * \return + * Number of bytes written to `out`. + */ +static inline size_t +string_pack(const char *str, uint8_t *out) +{ + if (str == NULL) { + out[0] = 0; + return 1; + } else { + size_t len = strlen(str); + size_t rv = uint32_pack(len, out); + memcpy(out + rv, str, len); + return rv + len; + } +} + +/** + * Pack a ProtobufCBinaryData and return the number of bytes written. The output + * includes a length delimiter. + * + * \param bd + * ProtobufCBinaryData to encode. + * \param[out] out + * Packed value. + * \return + * Number of bytes written to `out`. + */ +static inline size_t +binary_data_pack(const ProtobufCBinaryData *bd, uint8_t *out) +{ + size_t len = bd->len; + size_t rv = uint32_pack(len, out); + memcpy(out + rv, bd->data, len); + return rv + len; +} + +/** + * Pack a ProtobufCMessage and return the number of bytes written. The output + * includes a length delimiter. + * + * \param message + * ProtobufCMessage object to pack. + * \param[out] out + * Packed message. + * \return + * Number of bytes written to `out`. + */ +static inline size_t +prefixed_message_pack(const ProtobufCMessage *message, uint8_t *out) +{ + if (message == NULL) { + out[0] = 0; + return 1; + } else { + size_t rv = protobuf_c_message_pack(message, out + 1); + uint32_t rv_packed_size = uint32_size(rv); + if (rv_packed_size != 1) + memmove(out + rv_packed_size, out + 1, rv); + return uint32_pack(rv, out) + rv; + } +} + +/** + * Pack a field tag. + * + * Wire-type will be added in required_field_pack(). + * + * \todo Just call uint64_pack on 64-bit platforms. + * + * \param id + * Tag value to encode. + * \param[out] out + * Packed value. + * \return + * Number of bytes written to `out`. + */ +static size_t +tag_pack(uint32_t id, uint8_t *out) +{ + if (id < (1UL << (32 - 3))) + return uint32_pack(id << 3, out); + else + return uint64_pack(((uint64_t) id) << 3, out); +} + +/** + * Pack a required field and return the number of bytes written. + * + * \param field + * Field descriptor. + * \param member + * The field member. + * \param[out] out + * Packed value. + * \return + * Number of bytes written to `out`. + */ +static size_t +required_field_pack(const ProtobufCFieldDescriptor *field, + const void *member, uint8_t *out) +{ + size_t rv = tag_pack(field->id, out); + + switch (field->type) { + case PROTOBUF_C_TYPE_SINT32: + out[0] |= PROTOBUF_C_WIRE_TYPE_VARINT; + return rv + sint32_pack(*(const int32_t *) member, out + rv); + case PROTOBUF_C_TYPE_ENUM: + case PROTOBUF_C_TYPE_INT32: + out[0] |= PROTOBUF_C_WIRE_TYPE_VARINT; + return rv + int32_pack(*(const int32_t *) member, out + rv); + case PROTOBUF_C_TYPE_UINT32: + out[0] |= PROTOBUF_C_WIRE_TYPE_VARINT; + return rv + uint32_pack(*(const uint32_t *) member, out + rv); + case PROTOBUF_C_TYPE_SINT64: + out[0] |= PROTOBUF_C_WIRE_TYPE_VARINT; + return rv + sint64_pack(*(const int64_t *) member, out + rv); + case PROTOBUF_C_TYPE_INT64: + case PROTOBUF_C_TYPE_UINT64: + out[0] |= PROTOBUF_C_WIRE_TYPE_VARINT; + return rv + uint64_pack(*(const uint64_t *) member, out + rv); + case PROTOBUF_C_TYPE_SFIXED32: + case PROTOBUF_C_TYPE_FIXED32: + case PROTOBUF_C_TYPE_FLOAT: + out[0] |= PROTOBUF_C_WIRE_TYPE_32BIT; + return rv + fixed32_pack(*(const uint32_t *) member, out + rv); + case PROTOBUF_C_TYPE_SFIXED64: + case PROTOBUF_C_TYPE_FIXED64: + case PROTOBUF_C_TYPE_DOUBLE: + out[0] |= PROTOBUF_C_WIRE_TYPE_64BIT; + return rv + fixed64_pack(*(const uint64_t *) member, out + rv); + case PROTOBUF_C_TYPE_BOOL: + out[0] |= PROTOBUF_C_WIRE_TYPE_VARINT; + return rv + boolean_pack(*(const protobuf_c_boolean *) member, out + rv); + case PROTOBUF_C_TYPE_STRING: + out[0] |= PROTOBUF_C_WIRE_TYPE_LENGTH_PREFIXED; + return rv + string_pack(*(char *const *) member, out + rv); + case PROTOBUF_C_TYPE_BYTES: + out[0] |= PROTOBUF_C_WIRE_TYPE_LENGTH_PREFIXED; + return rv + binary_data_pack((const ProtobufCBinaryData *) member, out + rv); + case PROTOBUF_C_TYPE_MESSAGE: + out[0] |= PROTOBUF_C_WIRE_TYPE_LENGTH_PREFIXED; + return rv + prefixed_message_pack(*(ProtobufCMessage * const *) member, out + rv); + } + PROTOBUF_C__ASSERT_NOT_REACHED(); + return 0; +} + +/** + * Pack a oneof field and return the number of bytes written. Only packs the + * field that is selected by the case enum. + * + * \param field + * Field descriptor. + * \param oneof_case + * Enum value that selects the field in the oneof. + * \param member + * The field member. + * \param[out] out + * Packed value. + * \return + * Number of bytes written to `out`. + */ +static size_t +oneof_field_pack(const ProtobufCFieldDescriptor *field, + uint32_t oneof_case, + const void *member, uint8_t *out) +{ + if (oneof_case != field->id) { + return 0; + } + if (field->type == PROTOBUF_C_TYPE_MESSAGE || + field->type == PROTOBUF_C_TYPE_STRING) + { + const void *ptr = *(const void * const *) member; + if (ptr == NULL || ptr == field->default_value) + return 0; + } + return required_field_pack(field, member, out); +} + +/** + * Pack an optional field and return the number of bytes written. + * + * \param field + * Field descriptor. + * \param has + * Whether the field is set. + * \param member + * The field member. + * \param[out] out + * Packed value. + * \return + * Number of bytes written to `out`. + */ +static size_t +optional_field_pack(const ProtobufCFieldDescriptor *field, + const protobuf_c_boolean has, + const void *member, uint8_t *out) +{ + if (field->type == PROTOBUF_C_TYPE_MESSAGE || + field->type == PROTOBUF_C_TYPE_STRING) + { + const void *ptr = *(const void * const *) member; + if (ptr == NULL || ptr == field->default_value) + return 0; + } else { + if (!has) + return 0; + } + return required_field_pack(field, member, out); +} + +/** + * Pack an unlabeled field and return the number of bytes written. + * + * \param field + * Field descriptor. + * \param member + * The field member. + * \param[out] out + * Packed value. + * \return + * Number of bytes written to `out`. + */ +static size_t +unlabeled_field_pack(const ProtobufCFieldDescriptor *field, + const void *member, uint8_t *out) +{ + if (field_is_zeroish(field, member)) + return 0; + return required_field_pack(field, member, out); +} + +/** + * Given a field type, return the in-memory size. + * + * \todo Implement as a table lookup. + * + * \param type + * Field type. + * \return + * Size of the field. + */ +static inline size_t +sizeof_elt_in_repeated_array(ProtobufCType type) +{ + switch (type) { + case PROTOBUF_C_TYPE_SINT32: + case PROTOBUF_C_TYPE_INT32: + case PROTOBUF_C_TYPE_UINT32: + case PROTOBUF_C_TYPE_SFIXED32: + case PROTOBUF_C_TYPE_FIXED32: + case PROTOBUF_C_TYPE_FLOAT: + case PROTOBUF_C_TYPE_ENUM: + return 4; + case PROTOBUF_C_TYPE_SINT64: + case PROTOBUF_C_TYPE_INT64: + case PROTOBUF_C_TYPE_UINT64: + case PROTOBUF_C_TYPE_SFIXED64: + case PROTOBUF_C_TYPE_FIXED64: + case PROTOBUF_C_TYPE_DOUBLE: + return 8; + case PROTOBUF_C_TYPE_BOOL: + return sizeof(protobuf_c_boolean); + case PROTOBUF_C_TYPE_STRING: + case PROTOBUF_C_TYPE_MESSAGE: + return sizeof(void *); + case PROTOBUF_C_TYPE_BYTES: + return sizeof(ProtobufCBinaryData); + } + PROTOBUF_C__ASSERT_NOT_REACHED(); + return 0; +} + +/** + * Pack an array of 32-bit quantities. + * + * \param[out] out + * Destination. + * \param[in] in + * Source. + * \param[in] n + * Number of elements in the source array. + */ +static void +copy_to_little_endian_32(void *out, const void *in, const unsigned n) +{ +#if !defined(WORDS_BIGENDIAN) + memcpy(out, in, n * 4); +#else + unsigned i; + const uint32_t *ini = in; + for (i = 0; i < n; i++) + fixed32_pack(ini[i], (uint32_t *) out + i); +#endif +} + +/** + * Pack an array of 64-bit quantities. + * + * \param[out] out + * Destination. + * \param[in] in + * Source. + * \param[in] n + * Number of elements in the source array. + */ +static void +copy_to_little_endian_64(void *out, const void *in, const unsigned n) +{ +#if !defined(WORDS_BIGENDIAN) + memcpy(out, in, n * 8); +#else + unsigned i; + const uint64_t *ini = in; + for (i = 0; i < n; i++) + fixed64_pack(ini[i], (uint64_t *) out + i); +#endif +} + +/** + * Get the minimum number of bytes required to pack a field value of a + * particular type. + * + * \param type + * Field type. + * \return + * Number of bytes. + */ +static unsigned +get_type_min_size(ProtobufCType type) +{ + if (type == PROTOBUF_C_TYPE_SFIXED32 || + type == PROTOBUF_C_TYPE_FIXED32 || + type == PROTOBUF_C_TYPE_FLOAT) + { + return 4; + } + if (type == PROTOBUF_C_TYPE_SFIXED64 || + type == PROTOBUF_C_TYPE_FIXED64 || + type == PROTOBUF_C_TYPE_DOUBLE) + { + return 8; + } + return 1; +} + +/** + * Packs the elements of a repeated field and returns the serialised field and + * its length. + * + * \param field + * Field descriptor. + * \param count + * Number of elements in the repeated field array. + * \param member + * Pointer to the elements for this repeated field. + * \param[out] out + * Serialised representation of the repeated field. + * \return + * Number of bytes serialised to `out`. + */ +static size_t +repeated_field_pack(const ProtobufCFieldDescriptor *field, + size_t count, const void *member, uint8_t *out) +{ + void *array = *(void * const *) member; + unsigned i; + + if (0 != (field->flags & PROTOBUF_C_FIELD_FLAG_PACKED)) { + unsigned header_len; + unsigned len_start; + unsigned min_length; + unsigned payload_len; + unsigned length_size_min; + unsigned actual_length_size; + uint8_t *payload_at; + + if (count == 0) + return 0; + header_len = tag_pack(field->id, out); + out[0] |= PROTOBUF_C_WIRE_TYPE_LENGTH_PREFIXED; + len_start = header_len; + min_length = get_type_min_size(field->type) * count; + length_size_min = uint32_size(min_length); + header_len += length_size_min; + payload_at = out + header_len; + + switch (field->type) { + case PROTOBUF_C_TYPE_SFIXED32: + case PROTOBUF_C_TYPE_FIXED32: + case PROTOBUF_C_TYPE_FLOAT: + copy_to_little_endian_32(payload_at, array, count); + payload_at += count * 4; + break; + case PROTOBUF_C_TYPE_SFIXED64: + case PROTOBUF_C_TYPE_FIXED64: + case PROTOBUF_C_TYPE_DOUBLE: + copy_to_little_endian_64(payload_at, array, count); + payload_at += count * 8; + break; + case PROTOBUF_C_TYPE_ENUM: + case PROTOBUF_C_TYPE_INT32: { + const int32_t *arr = (const int32_t *) array; + for (i = 0; i < count; i++) + payload_at += int32_pack(arr[i], payload_at); + break; + } + case PROTOBUF_C_TYPE_SINT32: { + const int32_t *arr = (const int32_t *) array; + for (i = 0; i < count; i++) + payload_at += sint32_pack(arr[i], payload_at); + break; + } + case PROTOBUF_C_TYPE_SINT64: { + const int64_t *arr = (const int64_t *) array; + for (i = 0; i < count; i++) + payload_at += sint64_pack(arr[i], payload_at); + break; + } + case PROTOBUF_C_TYPE_UINT32: { + const uint32_t *arr = (const uint32_t *) array; + for (i = 0; i < count; i++) + payload_at += uint32_pack(arr[i], payload_at); + break; + } + case PROTOBUF_C_TYPE_INT64: + case PROTOBUF_C_TYPE_UINT64: { + const uint64_t *arr = (const uint64_t *) array; + for (i = 0; i < count; i++) + payload_at += uint64_pack(arr[i], payload_at); + break; + } + case PROTOBUF_C_TYPE_BOOL: { + const protobuf_c_boolean *arr = (const protobuf_c_boolean *) array; + for (i = 0; i < count; i++) + payload_at += boolean_pack(arr[i], payload_at); + break; + } + default: + PROTOBUF_C__ASSERT_NOT_REACHED(); + } + + payload_len = payload_at - (out + header_len); + actual_length_size = uint32_size(payload_len); + if (length_size_min != actual_length_size) { + assert(actual_length_size == length_size_min + 1); + memmove(out + header_len + 1, out + header_len, + payload_len); + header_len++; + } + uint32_pack(payload_len, out + len_start); + return header_len + payload_len; + } else { + /* not "packed" cased */ + /* CONSIDER: optimize this case a bit (by putting the loop inside the switch) */ + size_t rv = 0; + unsigned siz = sizeof_elt_in_repeated_array(field->type); + + for (i = 0; i < count; i++) { + rv += required_field_pack(field, array, out + rv); + array = (char *)array + siz; + } + return rv; + } +} + +static size_t +unknown_field_pack(const ProtobufCMessageUnknownField *field, uint8_t *out) +{ + size_t rv = tag_pack(field->tag, out); + out[0] |= field->wire_type; + memcpy(out + rv, field->data, field->len); + return rv + field->len; +} + +/**@}*/ + +size_t +protobuf_c_message_pack(const ProtobufCMessage *message, uint8_t *out) +{ + unsigned i; + size_t rv = 0; + + ASSERT_IS_MESSAGE(message); + for (i = 0; i < message->descriptor->n_fields; i++) { + const ProtobufCFieldDescriptor *field = + message->descriptor->fields + i; + const void *member = ((const char *) message) + field->offset; + + /* + * It doesn't hurt to compute qmember (a pointer to the + * quantifier field of the structure), but the pointer is only + * valid if the field is: + * - a repeated field, or + * - a field that is part of a oneof + * - an optional field that isn't a pointer type + * (Meaning: not a message or a string). + */ + const void *qmember = + ((const char *) message) + field->quantifier_offset; + + if (field->label == PROTOBUF_C_LABEL_REQUIRED) { + rv += required_field_pack(field, member, out + rv); + } else if ((field->label == PROTOBUF_C_LABEL_OPTIONAL || + field->label == PROTOBUF_C_LABEL_NONE) && + (0 != (field->flags & PROTOBUF_C_FIELD_FLAG_ONEOF))) { + rv += oneof_field_pack( + field, + *(const uint32_t *) qmember, + member, + out + rv + ); + } else if (field->label == PROTOBUF_C_LABEL_OPTIONAL) { + rv += optional_field_pack( + field, + *(const protobuf_c_boolean *) qmember, + member, + out + rv + ); + } else if (field->label == PROTOBUF_C_LABEL_NONE) { + rv += unlabeled_field_pack(field, member, out + rv); + } else { + rv += repeated_field_pack(field, *(const size_t *) qmember, + member, out + rv); + } + } + for (i = 0; i < message->n_unknown_fields; i++) + rv += unknown_field_pack(&message->unknown_fields[i], out + rv); + return rv; +} + +/** + * \defgroup packbuf protobuf_c_message_pack_to_buffer() implementation + * + * Routines mainly used by protobuf_c_message_pack_to_buffer(). + * + * \ingroup internal + * @{ + */ + +/** + * Pack a required field to a virtual buffer. + * + * \param field + * Field descriptor. + * \param member + * The element to be packed. + * \param[out] buffer + * Virtual buffer to append data to. + * \return + * Number of bytes packed. + */ +static size_t +required_field_pack_to_buffer(const ProtobufCFieldDescriptor *field, + const void *member, ProtobufCBuffer *buffer) +{ + size_t rv; + uint8_t scratch[MAX_UINT64_ENCODED_SIZE * 2]; + + rv = tag_pack(field->id, scratch); + switch (field->type) { + case PROTOBUF_C_TYPE_SINT32: + scratch[0] |= PROTOBUF_C_WIRE_TYPE_VARINT; + rv += sint32_pack(*(const int32_t *) member, scratch + rv); + buffer->append(buffer, rv, scratch); + break; + case PROTOBUF_C_TYPE_ENUM: + case PROTOBUF_C_TYPE_INT32: + scratch[0] |= PROTOBUF_C_WIRE_TYPE_VARINT; + rv += int32_pack(*(const int32_t *) member, scratch + rv); + buffer->append(buffer, rv, scratch); + break; + case PROTOBUF_C_TYPE_UINT32: + scratch[0] |= PROTOBUF_C_WIRE_TYPE_VARINT; + rv += uint32_pack(*(const uint32_t *) member, scratch + rv); + buffer->append(buffer, rv, scratch); + break; + case PROTOBUF_C_TYPE_SINT64: + scratch[0] |= PROTOBUF_C_WIRE_TYPE_VARINT; + rv += sint64_pack(*(const int64_t *) member, scratch + rv); + buffer->append(buffer, rv, scratch); + break; + case PROTOBUF_C_TYPE_INT64: + case PROTOBUF_C_TYPE_UINT64: + scratch[0] |= PROTOBUF_C_WIRE_TYPE_VARINT; + rv += uint64_pack(*(const uint64_t *) member, scratch + rv); + buffer->append(buffer, rv, scratch); + break; + case PROTOBUF_C_TYPE_SFIXED32: + case PROTOBUF_C_TYPE_FIXED32: + case PROTOBUF_C_TYPE_FLOAT: + scratch[0] |= PROTOBUF_C_WIRE_TYPE_32BIT; + rv += fixed32_pack(*(const uint32_t *) member, scratch + rv); + buffer->append(buffer, rv, scratch); + break; + case PROTOBUF_C_TYPE_SFIXED64: + case PROTOBUF_C_TYPE_FIXED64: + case PROTOBUF_C_TYPE_DOUBLE: + scratch[0] |= PROTOBUF_C_WIRE_TYPE_64BIT; + rv += fixed64_pack(*(const uint64_t *) member, scratch + rv); + buffer->append(buffer, rv, scratch); + break; + case PROTOBUF_C_TYPE_BOOL: + scratch[0] |= PROTOBUF_C_WIRE_TYPE_VARINT; + rv += boolean_pack(*(const protobuf_c_boolean *) member, scratch + rv); + buffer->append(buffer, rv, scratch); + break; + case PROTOBUF_C_TYPE_STRING: { + const char *str = *(char *const *) member; + size_t sublen = str ? strlen(str) : 0; + + scratch[0] |= PROTOBUF_C_WIRE_TYPE_LENGTH_PREFIXED; + rv += uint32_pack(sublen, scratch + rv); + buffer->append(buffer, rv, scratch); + buffer->append(buffer, sublen, (const uint8_t *) str); + rv += sublen; + break; + } + case PROTOBUF_C_TYPE_BYTES: { + const ProtobufCBinaryData *bd = ((const ProtobufCBinaryData *) member); + size_t sublen = bd->len; + + scratch[0] |= PROTOBUF_C_WIRE_TYPE_LENGTH_PREFIXED; + rv += uint32_pack(sublen, scratch + rv); + buffer->append(buffer, rv, scratch); + buffer->append(buffer, sublen, bd->data); + rv += sublen; + break; + } + case PROTOBUF_C_TYPE_MESSAGE: { + uint8_t simple_buffer_scratch[256]; + size_t sublen; + const ProtobufCMessage *msg = *(ProtobufCMessage * const *) member; + ProtobufCBufferSimple simple_buffer = + PROTOBUF_C_BUFFER_SIMPLE_INIT(simple_buffer_scratch); + + scratch[0] |= PROTOBUF_C_WIRE_TYPE_LENGTH_PREFIXED; + if (msg == NULL) + sublen = 0; + else + sublen = protobuf_c_message_pack_to_buffer(msg, &simple_buffer.base); + rv += uint32_pack(sublen, scratch + rv); + buffer->append(buffer, rv, scratch); + buffer->append(buffer, sublen, simple_buffer.data); + rv += sublen; + PROTOBUF_C_BUFFER_SIMPLE_CLEAR(&simple_buffer); + break; + } + default: + PROTOBUF_C__ASSERT_NOT_REACHED(); + } + return rv; +} + +/** + * Pack a oneof field to a buffer. Only packs the field that is selected by the case enum. + * + * \param field + * Field descriptor. + * \param oneof_case + * Enum value that selects the field in the oneof. + * \param member + * The element to be packed. + * \param[out] buffer + * Virtual buffer to append data to. + * \return + * Number of bytes serialised to `buffer`. + */ +static size_t +oneof_field_pack_to_buffer(const ProtobufCFieldDescriptor *field, + uint32_t oneof_case, + const void *member, ProtobufCBuffer *buffer) +{ + if (oneof_case != field->id) { + return 0; + } + if (field->type == PROTOBUF_C_TYPE_MESSAGE || + field->type == PROTOBUF_C_TYPE_STRING) + { + const void *ptr = *(const void *const *) member; + if (ptr == NULL || ptr == field->default_value) + return 0; + } + return required_field_pack_to_buffer(field, member, buffer); +} + +/** + * Pack an optional field to a buffer. + * + * \param field + * Field descriptor. + * \param has + * Whether the field is set. + * \param member + * The element to be packed. + * \param[out] buffer + * Virtual buffer to append data to. + * \return + * Number of bytes serialised to `buffer`. + */ +static size_t +optional_field_pack_to_buffer(const ProtobufCFieldDescriptor *field, + const protobuf_c_boolean has, + const void *member, ProtobufCBuffer *buffer) +{ + if (field->type == PROTOBUF_C_TYPE_MESSAGE || + field->type == PROTOBUF_C_TYPE_STRING) + { + const void *ptr = *(const void *const *) member; + if (ptr == NULL || ptr == field->default_value) + return 0; + } else { + if (!has) + return 0; + } + return required_field_pack_to_buffer(field, member, buffer); +} + +/** + * Pack an unlabeled field to a buffer. + * + * \param field + * Field descriptor. + * \param member + * The element to be packed. + * \param[out] buffer + * Virtual buffer to append data to. + * \return + * Number of bytes serialised to `buffer`. + */ +static size_t +unlabeled_field_pack_to_buffer(const ProtobufCFieldDescriptor *field, + const void *member, ProtobufCBuffer *buffer) +{ + if (field_is_zeroish(field, member)) + return 0; + return required_field_pack_to_buffer(field, member, buffer); +} + +/** + * Get the packed size of an array of same field type. + * + * \param field + * Field descriptor. + * \param count + * Number of elements of this type. + * \param array + * The elements to get the size of. + * \return + * Number of bytes required. + */ +static size_t +get_packed_payload_length(const ProtobufCFieldDescriptor *field, + unsigned count, const void *array) +{ + unsigned rv = 0; + unsigned i; + + switch (field->type) { + case PROTOBUF_C_TYPE_SFIXED32: + case PROTOBUF_C_TYPE_FIXED32: + case PROTOBUF_C_TYPE_FLOAT: + return count * 4; + case PROTOBUF_C_TYPE_SFIXED64: + case PROTOBUF_C_TYPE_FIXED64: + case PROTOBUF_C_TYPE_DOUBLE: + return count * 8; + case PROTOBUF_C_TYPE_ENUM: + case PROTOBUF_C_TYPE_INT32: { + const int32_t *arr = (const int32_t *) array; + for (i = 0; i < count; i++) + rv += int32_size(arr[i]); + break; + } + case PROTOBUF_C_TYPE_SINT32: { + const int32_t *arr = (const int32_t *) array; + for (i = 0; i < count; i++) + rv += sint32_size(arr[i]); + break; + } + case PROTOBUF_C_TYPE_UINT32: { + const uint32_t *arr = (const uint32_t *) array; + for (i = 0; i < count; i++) + rv += uint32_size(arr[i]); + break; + } + case PROTOBUF_C_TYPE_SINT64: { + const int64_t *arr = (const int64_t *) array; + for (i = 0; i < count; i++) + rv += sint64_size(arr[i]); + break; + } + case PROTOBUF_C_TYPE_INT64: + case PROTOBUF_C_TYPE_UINT64: { + const uint64_t *arr = (const uint64_t *) array; + for (i = 0; i < count; i++) + rv += uint64_size(arr[i]); + break; + } + case PROTOBUF_C_TYPE_BOOL: + return count; + default: + PROTOBUF_C__ASSERT_NOT_REACHED(); + } + return rv; +} + +/** + * Pack an array of same field type to a virtual buffer. + * + * \param field + * Field descriptor. + * \param count + * Number of elements of this type. + * \param array + * The elements to get the size of. + * \param[out] buffer + * Virtual buffer to append data to. + * \return + * Number of bytes packed. + */ +static size_t +pack_buffer_packed_payload(const ProtobufCFieldDescriptor *field, + unsigned count, const void *array, + ProtobufCBuffer *buffer) +{ + uint8_t scratch[16]; + size_t rv = 0; + unsigned i; + + switch (field->type) { + case PROTOBUF_C_TYPE_SFIXED32: + case PROTOBUF_C_TYPE_FIXED32: + case PROTOBUF_C_TYPE_FLOAT: +#if !defined(WORDS_BIGENDIAN) + rv = count * 4; + goto no_packing_needed; +#else + for (i = 0; i < count; i++) { + unsigned len = fixed32_pack(((uint32_t *) array)[i], scratch); + buffer->append(buffer, len, scratch); + rv += len; + } + break; +#endif + case PROTOBUF_C_TYPE_SFIXED64: + case PROTOBUF_C_TYPE_FIXED64: + case PROTOBUF_C_TYPE_DOUBLE: +#if !defined(WORDS_BIGENDIAN) + rv = count * 8; + goto no_packing_needed; +#else + for (i = 0; i < count; i++) { + unsigned len = fixed64_pack(((uint64_t *) array)[i], scratch); + buffer->append(buffer, len, scratch); + rv += len; + } + break; +#endif + case PROTOBUF_C_TYPE_ENUM: + case PROTOBUF_C_TYPE_INT32: + for (i = 0; i < count; i++) { + unsigned len = int32_pack(((int32_t *) array)[i], scratch); + buffer->append(buffer, len, scratch); + rv += len; + } + break; + case PROTOBUF_C_TYPE_SINT32: + for (i = 0; i < count; i++) { + unsigned len = sint32_pack(((int32_t *) array)[i], scratch); + buffer->append(buffer, len, scratch); + rv += len; + } + break; + case PROTOBUF_C_TYPE_UINT32: + for (i = 0; i < count; i++) { + unsigned len = uint32_pack(((uint32_t *) array)[i], scratch); + buffer->append(buffer, len, scratch); + rv += len; + } + break; + case PROTOBUF_C_TYPE_SINT64: + for (i = 0; i < count; i++) { + unsigned len = sint64_pack(((int64_t *) array)[i], scratch); + buffer->append(buffer, len, scratch); + rv += len; + } + break; + case PROTOBUF_C_TYPE_INT64: + case PROTOBUF_C_TYPE_UINT64: + for (i = 0; i < count; i++) { + unsigned len = uint64_pack(((uint64_t *) array)[i], scratch); + buffer->append(buffer, len, scratch); + rv += len; + } + break; + case PROTOBUF_C_TYPE_BOOL: + for (i = 0; i < count; i++) { + unsigned len = boolean_pack(((protobuf_c_boolean *) array)[i], scratch); + buffer->append(buffer, len, scratch); + rv += len; + } + return count; + default: + PROTOBUF_C__ASSERT_NOT_REACHED(); + } + return rv; + +#if !defined(WORDS_BIGENDIAN) +no_packing_needed: + buffer->append(buffer, rv, array); + return rv; +#endif +} + +static size_t +repeated_field_pack_to_buffer(const ProtobufCFieldDescriptor *field, + unsigned count, const void *member, + ProtobufCBuffer *buffer) +{ + char *array = *(char * const *) member; + + if (count == 0) + return 0; + if (0 != (field->flags & PROTOBUF_C_FIELD_FLAG_PACKED)) { + uint8_t scratch[MAX_UINT64_ENCODED_SIZE * 2]; + size_t rv = tag_pack(field->id, scratch); + size_t payload_len = get_packed_payload_length(field, count, array); + size_t tmp; + + scratch[0] |= PROTOBUF_C_WIRE_TYPE_LENGTH_PREFIXED; + rv += uint32_pack(payload_len, scratch + rv); + buffer->append(buffer, rv, scratch); + tmp = pack_buffer_packed_payload(field, count, array, buffer); + assert(tmp == payload_len); + return rv + payload_len; + } else { + size_t siz; + unsigned i; + /* CONSIDER: optimize this case a bit (by putting the loop inside the switch) */ + unsigned rv = 0; + + siz = sizeof_elt_in_repeated_array(field->type); + for (i = 0; i < count; i++) { + rv += required_field_pack_to_buffer(field, array, buffer); + array += siz; + } + return rv; + } +} + +static size_t +unknown_field_pack_to_buffer(const ProtobufCMessageUnknownField *field, + ProtobufCBuffer *buffer) +{ + uint8_t header[MAX_UINT64_ENCODED_SIZE]; + size_t rv = tag_pack(field->tag, header); + + header[0] |= field->wire_type; + buffer->append(buffer, rv, header); + buffer->append(buffer, field->len, field->data); + return rv + field->len; +} + +/**@}*/ + +size_t +protobuf_c_message_pack_to_buffer(const ProtobufCMessage *message, + ProtobufCBuffer *buffer) +{ + unsigned i; + size_t rv = 0; + + ASSERT_IS_MESSAGE(message); + for (i = 0; i < message->descriptor->n_fields; i++) { + const ProtobufCFieldDescriptor *field = + message->descriptor->fields + i; + const void *member = + ((const char *) message) + field->offset; + const void *qmember = + ((const char *) message) + field->quantifier_offset; + + if (field->label == PROTOBUF_C_LABEL_REQUIRED) { + rv += required_field_pack_to_buffer(field, member, buffer); + } else if ((field->label == PROTOBUF_C_LABEL_OPTIONAL || + field->label == PROTOBUF_C_LABEL_NONE) && + (0 != (field->flags & PROTOBUF_C_FIELD_FLAG_ONEOF))) { + rv += oneof_field_pack_to_buffer( + field, + *(const uint32_t *) qmember, + member, + buffer + ); + } else if (field->label == PROTOBUF_C_LABEL_OPTIONAL) { + rv += optional_field_pack_to_buffer( + field, + *(const protobuf_c_boolean *) qmember, + member, + buffer + ); + } else if (field->label == PROTOBUF_C_LABEL_NONE) { + rv += unlabeled_field_pack_to_buffer( + field, + member, + buffer + ); + } else { + rv += repeated_field_pack_to_buffer( + field, + *(const size_t *) qmember, + member, + buffer + ); + } + } + for (i = 0; i < message->n_unknown_fields; i++) + rv += unknown_field_pack_to_buffer(&message->unknown_fields[i], buffer); + + return rv; +} + +/** + * \defgroup unpack unpacking implementation + * + * Routines mainly used by the unpacking functions. + * + * \ingroup internal + * @{ + */ + +static inline int +int_range_lookup(unsigned n_ranges, const ProtobufCIntRange *ranges, int value) +{ + unsigned n; + unsigned start; + + if (n_ranges == 0) + return -1; + start = 0; + n = n_ranges; + while (n > 1) { + unsigned mid = start + n / 2; + + if (value < ranges[mid].start_value) { + n = mid - start; + } else if (value >= ranges[mid].start_value + + (int) (ranges[mid + 1].orig_index - + ranges[mid].orig_index)) + { + unsigned new_start = mid + 1; + n = start + n - new_start; + start = new_start; + } else + return (value - ranges[mid].start_value) + + ranges[mid].orig_index; + } + if (n > 0) { + unsigned start_orig_index = ranges[start].orig_index; + unsigned range_size = + ranges[start + 1].orig_index - start_orig_index; + + if (ranges[start].start_value <= value && + value < (int) (ranges[start].start_value + range_size)) + { + return (value - ranges[start].start_value) + + start_orig_index; + } + } + return -1; +} + +static size_t +parse_tag_and_wiretype(size_t len, + const uint8_t *data, + uint32_t *tag_out, + ProtobufCWireType *wiretype_out) +{ + unsigned max_rv = len > 5 ? 5 : len; + uint32_t tag = (data[0] & 0x7f) >> 3; + unsigned shift = 4; + unsigned rv; + + /* 0 is not a valid tag value */ + if ((data[0] & 0xf8) == 0) { + return 0; + } + + *wiretype_out = data[0] & 7; + if ((data[0] & 0x80) == 0) { + *tag_out = tag; + return 1; + } + for (rv = 1; rv < max_rv; rv++) { + if (data[rv] & 0x80) { + tag |= (data[rv] & 0x7f) << shift; + shift += 7; + } else { + tag |= data[rv] << shift; + *tag_out = tag; + return rv + 1; + } + } + return 0; /* error: bad header */ +} + +/* sizeof(ScannedMember) must be <= (1UL< INT_MAX) { + // Protobuf messages should always be less than 2 GiB in size. + // We also want to return early here so that hdr_len + val does + // not overflow on 32-bit systems. + PROTOBUF_C_UNPACK_ERROR("length prefix of %lu is too large", val); + return 0; + } + if (hdr_len + val > len) { + PROTOBUF_C_UNPACK_ERROR("data too short after length-prefix of %lu", val); + return 0; + } + return hdr_len + val; +} + +static size_t +max_b128_numbers(size_t len, const uint8_t *data) +{ + size_t rv = 0; + while (len--) + if ((*data++ & 0x80) == 0) + ++rv; + return rv; +} + +/**@}*/ + +/** + * Merge earlier message into a latter message. + * + * For numeric types and strings, if the same value appears multiple + * times, the parser accepts the last value it sees. For embedded + * message fields, the parser merges multiple instances of the same + * field. That is, all singular scalar fields in the latter instance + * replace those in the former, singular embedded messages are merged, + * and repeated fields are concatenated. + * + * The earlier message should be freed after calling this function, as + * some of its fields may have been reused and changed to their default + * values during the merge. + */ +static protobuf_c_boolean +merge_messages(ProtobufCMessage *earlier_msg, + ProtobufCMessage *latter_msg, + ProtobufCAllocator *allocator) +{ + unsigned i; + const ProtobufCFieldDescriptor *fields = + latter_msg->descriptor->fields; + for (i = 0; i < latter_msg->descriptor->n_fields; i++) { + if (fields[i].label == PROTOBUF_C_LABEL_REPEATED) { + size_t *n_earlier = + STRUCT_MEMBER_PTR(size_t, earlier_msg, + fields[i].quantifier_offset); + uint8_t **p_earlier = + STRUCT_MEMBER_PTR(uint8_t *, earlier_msg, + fields[i].offset); + size_t *n_latter = + STRUCT_MEMBER_PTR(size_t, latter_msg, + fields[i].quantifier_offset); + uint8_t **p_latter = + STRUCT_MEMBER_PTR(uint8_t *, latter_msg, + fields[i].offset); + + if (*n_earlier > 0) { + if (*n_latter > 0) { + /* Concatenate the repeated field */ + size_t el_size = + sizeof_elt_in_repeated_array(fields[i].type); + uint8_t *new_field; + + new_field = do_alloc(allocator, + (*n_earlier + *n_latter) * el_size); + if (!new_field) + return FALSE; + + memcpy(new_field, *p_earlier, + *n_earlier * el_size); + memcpy(new_field + + *n_earlier * el_size, + *p_latter, + *n_latter * el_size); + + do_free(allocator, *p_latter); + do_free(allocator, *p_earlier); + *p_latter = new_field; + *n_latter = *n_earlier + *n_latter; + } else { + /* Zero copy the repeated field from the earlier message */ + *n_latter = *n_earlier; + *p_latter = *p_earlier; + } + /* Make sure the field does not get double freed */ + *n_earlier = 0; + *p_earlier = 0; + } + } else if (fields[i].label == PROTOBUF_C_LABEL_OPTIONAL || + fields[i].label == PROTOBUF_C_LABEL_NONE) { + const ProtobufCFieldDescriptor *field; + uint32_t *earlier_case_p = STRUCT_MEMBER_PTR(uint32_t, + earlier_msg, + fields[i]. + quantifier_offset); + uint32_t *latter_case_p = STRUCT_MEMBER_PTR(uint32_t, + latter_msg, + fields[i]. + quantifier_offset); + protobuf_c_boolean need_to_merge = FALSE; + void *earlier_elem; + void *latter_elem; + const void *def_val; + + if (fields[i].flags & PROTOBUF_C_FIELD_FLAG_ONEOF) { + if (*latter_case_p == 0) { + /* lookup correct oneof field */ + int field_index = + int_range_lookup( + latter_msg->descriptor + ->n_field_ranges, + latter_msg->descriptor + ->field_ranges, + *earlier_case_p); + if (field_index < 0) + return FALSE; + field = latter_msg->descriptor->fields + + field_index; + } else { + /* Oneof is present in the latter message, move on */ + continue; + } + } else { + field = &fields[i]; + } + + earlier_elem = STRUCT_MEMBER_P(earlier_msg, field->offset); + latter_elem = STRUCT_MEMBER_P(latter_msg, field->offset); + def_val = field->default_value; + + switch (field->type) { + case PROTOBUF_C_TYPE_MESSAGE: { + ProtobufCMessage *em = *(ProtobufCMessage **) earlier_elem; + ProtobufCMessage *lm = *(ProtobufCMessage **) latter_elem; + if (em != NULL) { + if (lm != NULL) { + if (!merge_messages(em, lm, allocator)) + return FALSE; + /* Already merged */ + need_to_merge = FALSE; + } else { + /* Zero copy the message */ + need_to_merge = TRUE; + } + } + break; + } + case PROTOBUF_C_TYPE_BYTES: { + uint8_t *e_data = + ((ProtobufCBinaryData *) earlier_elem)->data; + uint8_t *l_data = + ((ProtobufCBinaryData *) latter_elem)->data; + const ProtobufCBinaryData *d_bd = + (ProtobufCBinaryData *) def_val; + + need_to_merge = + (e_data != NULL && + (d_bd == NULL || + e_data != d_bd->data)) && + (l_data == NULL || + (d_bd != NULL && + l_data == d_bd->data)); + break; + } + case PROTOBUF_C_TYPE_STRING: { + char *e_str = *(char **) earlier_elem; + char *l_str = *(char **) latter_elem; + const char *d_str = def_val; + + need_to_merge = e_str != d_str && l_str == d_str; + break; + } + default: { + /* Could be has field or case enum, the logic is + * equivalent, since 0 (FALSE) means not set for + * oneof */ + need_to_merge = (*earlier_case_p != 0) && + (*latter_case_p == 0); + break; + } + } + + if (need_to_merge) { + size_t el_size = + sizeof_elt_in_repeated_array(field->type); + memcpy(latter_elem, earlier_elem, el_size); + /* + * Reset the element from the old message to 0 + * to make sure earlier message deallocation + * doesn't corrupt zero-copied data in the new + * message, earlier message will be freed after + * this function is called anyway + */ + memset(earlier_elem, 0, el_size); + + if (field->quantifier_offset != 0) { + /* Set the has field or the case enum, + * if applicable */ + *latter_case_p = *earlier_case_p; + *earlier_case_p = 0; + } + } + } + } + return TRUE; +} + +/** + * Count packed elements. + * + * Given a raw slab of packed-repeated values, determine the number of + * elements. This function detects certain kinds of errors but not + * others; the remaining error checking is done by + * parse_packed_repeated_member(). + */ +static protobuf_c_boolean +count_packed_elements(ProtobufCType type, + size_t len, const uint8_t *data, size_t *count_out) +{ + switch (type) { + case PROTOBUF_C_TYPE_SFIXED32: + case PROTOBUF_C_TYPE_FIXED32: + case PROTOBUF_C_TYPE_FLOAT: + if (len % 4 != 0) { + PROTOBUF_C_UNPACK_ERROR("length must be a multiple of 4 for fixed-length 32-bit types"); + return FALSE; + } + *count_out = len / 4; + return TRUE; + case PROTOBUF_C_TYPE_SFIXED64: + case PROTOBUF_C_TYPE_FIXED64: + case PROTOBUF_C_TYPE_DOUBLE: + if (len % 8 != 0) { + PROTOBUF_C_UNPACK_ERROR("length must be a multiple of 8 for fixed-length 64-bit types"); + return FALSE; + } + *count_out = len / 8; + return TRUE; + case PROTOBUF_C_TYPE_ENUM: + case PROTOBUF_C_TYPE_INT32: + case PROTOBUF_C_TYPE_SINT32: + case PROTOBUF_C_TYPE_UINT32: + case PROTOBUF_C_TYPE_INT64: + case PROTOBUF_C_TYPE_SINT64: + case PROTOBUF_C_TYPE_UINT64: + *count_out = max_b128_numbers(len, data); + return TRUE; + case PROTOBUF_C_TYPE_BOOL: + *count_out = len; + return TRUE; + case PROTOBUF_C_TYPE_STRING: + case PROTOBUF_C_TYPE_BYTES: + case PROTOBUF_C_TYPE_MESSAGE: + default: + PROTOBUF_C_UNPACK_ERROR("bad protobuf-c type %u for packed-repeated", type); + return FALSE; + } +} + +static inline uint32_t +parse_uint32(unsigned len, const uint8_t *data) +{ + uint32_t rv = data[0] & 0x7f; + if (len > 1) { + rv |= ((uint32_t) (data[1] & 0x7f) << 7); + if (len > 2) { + rv |= ((uint32_t) (data[2] & 0x7f) << 14); + if (len > 3) { + rv |= ((uint32_t) (data[3] & 0x7f) << 21); + if (len > 4) + rv |= ((uint32_t) (data[4]) << 28); + } + } + } + return rv; +} + +static inline uint32_t +parse_int32(unsigned len, const uint8_t *data) +{ + return parse_uint32(len, data); +} + +static inline int32_t +unzigzag32(uint32_t v) +{ + if (v & 1) + return -(v >> 1) - 1; + else + return v >> 1; +} + +static inline uint32_t +parse_fixed_uint32(const uint8_t *data) +{ +#if !defined(WORDS_BIGENDIAN) + uint32_t t; + memcpy(&t, data, 4); + return t; +#else + return data[0] | + ((uint32_t) (data[1]) << 8) | + ((uint32_t) (data[2]) << 16) | + ((uint32_t) (data[3]) << 24); +#endif +} + +static uint64_t +parse_uint64(unsigned len, const uint8_t *data) +{ + unsigned shift, i; + uint64_t rv; + + if (len < 5) + return parse_uint32(len, data); + rv = ((uint64_t) (data[0] & 0x7f)) | + ((uint64_t) (data[1] & 0x7f) << 7) | + ((uint64_t) (data[2] & 0x7f) << 14) | + ((uint64_t) (data[3] & 0x7f) << 21); + shift = 28; + for (i = 4; i < len; i++) { + rv |= (((uint64_t) (data[i] & 0x7f)) << shift); + shift += 7; + } + return rv; +} + +static inline int64_t +unzigzag64(uint64_t v) +{ + if (v & 1) + return -(v >> 1) - 1; + else + return v >> 1; +} + +static inline uint64_t +parse_fixed_uint64(const uint8_t *data) +{ +#if !defined(WORDS_BIGENDIAN) + uint64_t t; + memcpy(&t, data, 8); + return t; +#else + return (uint64_t) parse_fixed_uint32(data) | + (((uint64_t) parse_fixed_uint32(data + 4)) << 32); +#endif +} + +static protobuf_c_boolean +parse_boolean(unsigned len, const uint8_t *data) +{ + unsigned i; + for (i = 0; i < len; i++) + if (data[i] & 0x7f) + return TRUE; + return FALSE; +} + +static protobuf_c_boolean +parse_required_member(ScannedMember *scanned_member, + void *member, + ProtobufCAllocator *allocator, + protobuf_c_boolean maybe_clear) +{ + unsigned len = scanned_member->len; + const uint8_t *data = scanned_member->data; + ProtobufCWireType wire_type = scanned_member->wire_type; + + switch (scanned_member->field->type) { + case PROTOBUF_C_TYPE_ENUM: + case PROTOBUF_C_TYPE_INT32: + if (wire_type != PROTOBUF_C_WIRE_TYPE_VARINT) + return FALSE; + *(int32_t *) member = parse_int32(len, data); + return TRUE; + case PROTOBUF_C_TYPE_UINT32: + if (wire_type != PROTOBUF_C_WIRE_TYPE_VARINT) + return FALSE; + *(uint32_t *) member = parse_uint32(len, data); + return TRUE; + case PROTOBUF_C_TYPE_SINT32: + if (wire_type != PROTOBUF_C_WIRE_TYPE_VARINT) + return FALSE; + *(int32_t *) member = unzigzag32(parse_uint32(len, data)); + return TRUE; + case PROTOBUF_C_TYPE_SFIXED32: + case PROTOBUF_C_TYPE_FIXED32: + case PROTOBUF_C_TYPE_FLOAT: + if (wire_type != PROTOBUF_C_WIRE_TYPE_32BIT) + return FALSE; + *(uint32_t *) member = parse_fixed_uint32(data); + return TRUE; + case PROTOBUF_C_TYPE_INT64: + case PROTOBUF_C_TYPE_UINT64: + if (wire_type != PROTOBUF_C_WIRE_TYPE_VARINT) + return FALSE; + *(uint64_t *) member = parse_uint64(len, data); + return TRUE; + case PROTOBUF_C_TYPE_SINT64: + if (wire_type != PROTOBUF_C_WIRE_TYPE_VARINT) + return FALSE; + *(int64_t *) member = unzigzag64(parse_uint64(len, data)); + return TRUE; + case PROTOBUF_C_TYPE_SFIXED64: + case PROTOBUF_C_TYPE_FIXED64: + case PROTOBUF_C_TYPE_DOUBLE: + if (wire_type != PROTOBUF_C_WIRE_TYPE_64BIT) + return FALSE; + *(uint64_t *) member = parse_fixed_uint64(data); + return TRUE; + case PROTOBUF_C_TYPE_BOOL: + *(protobuf_c_boolean *) member = parse_boolean(len, data); + return TRUE; + case PROTOBUF_C_TYPE_STRING: { + char **pstr = member; + unsigned pref_len = scanned_member->length_prefix_len; + + if (wire_type != PROTOBUF_C_WIRE_TYPE_LENGTH_PREFIXED) + return FALSE; + + if (maybe_clear && *pstr != NULL) { + const char *def = scanned_member->field->default_value; + if (*pstr != NULL && *pstr != def) + do_free(allocator, *pstr); + } + *pstr = do_alloc(allocator, len - pref_len + 1); + if (*pstr == NULL) + return FALSE; + memcpy(*pstr, data + pref_len, len - pref_len); + (*pstr)[len - pref_len] = 0; + return TRUE; + } + case PROTOBUF_C_TYPE_BYTES: { + ProtobufCBinaryData *bd = member; + const ProtobufCBinaryData *def_bd; + unsigned pref_len = scanned_member->length_prefix_len; + + if (wire_type != PROTOBUF_C_WIRE_TYPE_LENGTH_PREFIXED) + return FALSE; + + def_bd = scanned_member->field->default_value; + if (maybe_clear && + bd->data != NULL && + (def_bd == NULL || bd->data != def_bd->data)) + { + do_free(allocator, bd->data); + } + if (len - pref_len > 0) { + bd->data = do_alloc(allocator, len - pref_len); + if (bd->data == NULL) + return FALSE; + memcpy(bd->data, data + pref_len, len - pref_len); + } else { + bd->data = NULL; + } + bd->len = len - pref_len; + return TRUE; + } + case PROTOBUF_C_TYPE_MESSAGE: { + ProtobufCMessage **pmessage = member; + ProtobufCMessage *subm; + const ProtobufCMessage *def_mess; + protobuf_c_boolean merge_successful = TRUE; + unsigned pref_len = scanned_member->length_prefix_len; + + if (wire_type != PROTOBUF_C_WIRE_TYPE_LENGTH_PREFIXED) + return FALSE; + + def_mess = scanned_member->field->default_value; + subm = protobuf_c_message_unpack(scanned_member->field->descriptor, + allocator, + len - pref_len, + data + pref_len); + + if (maybe_clear && + *pmessage != NULL && + *pmessage != def_mess) + { + if (subm != NULL) + merge_successful = merge_messages(*pmessage, subm, allocator); + /* Delete the previous message */ + protobuf_c_message_free_unpacked(*pmessage, allocator); + } + *pmessage = subm; + if (subm == NULL || !merge_successful) + return FALSE; + return TRUE; + } + } + return FALSE; +} + +static protobuf_c_boolean +parse_oneof_member (ScannedMember *scanned_member, + void *member, + ProtobufCMessage *message, + ProtobufCAllocator *allocator) +{ + uint32_t *oneof_case = STRUCT_MEMBER_PTR(uint32_t, message, + scanned_member->field->quantifier_offset); + + /* If we have already parsed a member of this oneof, free it. */ + if (*oneof_case != 0) { + const ProtobufCFieldDescriptor *old_field; + size_t el_size; + /* lookup field */ + int field_index = + int_range_lookup(message->descriptor->n_field_ranges, + message->descriptor->field_ranges, + *oneof_case); + if (field_index < 0) + return FALSE; + old_field = message->descriptor->fields + field_index; + el_size = sizeof_elt_in_repeated_array(old_field->type); + + switch (old_field->type) { + case PROTOBUF_C_TYPE_STRING: { + char **pstr = member; + const char *def = old_field->default_value; + if (*pstr != NULL && *pstr != def) + do_free(allocator, *pstr); + break; + } + case PROTOBUF_C_TYPE_BYTES: { + ProtobufCBinaryData *bd = member; + const ProtobufCBinaryData *def_bd = old_field->default_value; + if (bd->data != NULL && + (def_bd == NULL || bd->data != def_bd->data)) + { + do_free(allocator, bd->data); + } + break; + } + case PROTOBUF_C_TYPE_MESSAGE: { + ProtobufCMessage **pmessage = member; + const ProtobufCMessage *def_mess = old_field->default_value; + if (*pmessage != NULL && *pmessage != def_mess) + protobuf_c_message_free_unpacked(*pmessage, allocator); + break; + } + default: + break; + } + + memset (member, 0, el_size); + } + if (!parse_required_member (scanned_member, member, allocator, TRUE)) + return FALSE; + + *oneof_case = scanned_member->tag; + return TRUE; +} + + +static protobuf_c_boolean +parse_optional_member(ScannedMember *scanned_member, + void *member, + ProtobufCMessage *message, + ProtobufCAllocator *allocator) +{ + if (!parse_required_member(scanned_member, member, allocator, TRUE)) + return FALSE; + if (scanned_member->field->quantifier_offset != 0) + STRUCT_MEMBER(protobuf_c_boolean, + message, + scanned_member->field->quantifier_offset) = TRUE; + return TRUE; +} + +static protobuf_c_boolean +parse_repeated_member(ScannedMember *scanned_member, + void *member, + ProtobufCMessage *message, + ProtobufCAllocator *allocator) +{ + const ProtobufCFieldDescriptor *field = scanned_member->field; + size_t *p_n = STRUCT_MEMBER_PTR(size_t, message, field->quantifier_offset); + size_t siz = sizeof_elt_in_repeated_array(field->type); + char *array = *(char **) member; + + if (!parse_required_member(scanned_member, array + siz * (*p_n), + allocator, FALSE)) + { + return FALSE; + } + *p_n += 1; + return TRUE; +} + +static unsigned +scan_varint(unsigned len, const uint8_t *data) +{ + unsigned i; + if (len > 10) + len = 10; + for (i = 0; i < len; i++) + if ((data[i] & 0x80) == 0) + break; + if (i == len) + return 0; + return i + 1; +} + +static protobuf_c_boolean +parse_packed_repeated_member(ScannedMember *scanned_member, + void *member, + ProtobufCMessage *message) +{ + const ProtobufCFieldDescriptor *field = scanned_member->field; + size_t *p_n = STRUCT_MEMBER_PTR(size_t, message, field->quantifier_offset); + size_t siz = sizeof_elt_in_repeated_array(field->type); + void *array = *(char **) member + siz * (*p_n); + const uint8_t *at = scanned_member->data + scanned_member->length_prefix_len; + size_t rem = scanned_member->len - scanned_member->length_prefix_len; + size_t count = 0; + unsigned i; + + switch (field->type) { + case PROTOBUF_C_TYPE_SFIXED32: + case PROTOBUF_C_TYPE_FIXED32: + case PROTOBUF_C_TYPE_FLOAT: + count = (scanned_member->len - scanned_member->length_prefix_len) / 4; +#if !defined(WORDS_BIGENDIAN) + goto no_unpacking_needed; +#else + for (i = 0; i < count; i++) { + ((uint32_t *) array)[i] = parse_fixed_uint32(at); + at += 4; + } + break; +#endif + case PROTOBUF_C_TYPE_SFIXED64: + case PROTOBUF_C_TYPE_FIXED64: + case PROTOBUF_C_TYPE_DOUBLE: + count = (scanned_member->len - scanned_member->length_prefix_len) / 8; +#if !defined(WORDS_BIGENDIAN) + goto no_unpacking_needed; +#else + for (i = 0; i < count; i++) { + ((uint64_t *) array)[i] = parse_fixed_uint64(at); + at += 8; + } + break; +#endif + case PROTOBUF_C_TYPE_ENUM: + case PROTOBUF_C_TYPE_INT32: + while (rem > 0) { + unsigned s = scan_varint(rem, at); + if (s == 0) { + PROTOBUF_C_UNPACK_ERROR("bad packed-repeated int32 value"); + return FALSE; + } + ((int32_t *) array)[count++] = parse_int32(s, at); + at += s; + rem -= s; + } + break; + case PROTOBUF_C_TYPE_SINT32: + while (rem > 0) { + unsigned s = scan_varint(rem, at); + if (s == 0) { + PROTOBUF_C_UNPACK_ERROR("bad packed-repeated sint32 value"); + return FALSE; + } + ((int32_t *) array)[count++] = unzigzag32(parse_uint32(s, at)); + at += s; + rem -= s; + } + break; + case PROTOBUF_C_TYPE_UINT32: + while (rem > 0) { + unsigned s = scan_varint(rem, at); + if (s == 0) { + PROTOBUF_C_UNPACK_ERROR("bad packed-repeated enum or uint32 value"); + return FALSE; + } + ((uint32_t *) array)[count++] = parse_uint32(s, at); + at += s; + rem -= s; + } + break; + + case PROTOBUF_C_TYPE_SINT64: + while (rem > 0) { + unsigned s = scan_varint(rem, at); + if (s == 0) { + PROTOBUF_C_UNPACK_ERROR("bad packed-repeated sint64 value"); + return FALSE; + } + ((int64_t *) array)[count++] = unzigzag64(parse_uint64(s, at)); + at += s; + rem -= s; + } + break; + case PROTOBUF_C_TYPE_INT64: + case PROTOBUF_C_TYPE_UINT64: + while (rem > 0) { + unsigned s = scan_varint(rem, at); + if (s == 0) { + PROTOBUF_C_UNPACK_ERROR("bad packed-repeated int64/uint64 value"); + return FALSE; + } + ((int64_t *) array)[count++] = parse_uint64(s, at); + at += s; + rem -= s; + } + break; + case PROTOBUF_C_TYPE_BOOL: + count = rem; + for (i = 0; i < count; i++) { + if (at[i] > 1) { + PROTOBUF_C_UNPACK_ERROR("bad packed-repeated boolean value"); + return FALSE; + } + ((protobuf_c_boolean *) array)[i] = at[i]; + } + break; + default: + PROTOBUF_C__ASSERT_NOT_REACHED(); + } + *p_n += count; + return TRUE; + +#if !defined(WORDS_BIGENDIAN) +no_unpacking_needed: + memcpy(array, at, count * siz); + *p_n += count; + return TRUE; +#endif +} + +static protobuf_c_boolean +is_packable_type(ProtobufCType type) +{ + return + type != PROTOBUF_C_TYPE_STRING && + type != PROTOBUF_C_TYPE_BYTES && + type != PROTOBUF_C_TYPE_MESSAGE; +} + +static protobuf_c_boolean +parse_member(ScannedMember *scanned_member, + ProtobufCMessage *message, + ProtobufCAllocator *allocator) +{ + const ProtobufCFieldDescriptor *field = scanned_member->field; + void *member; + + if (field == NULL) { + ProtobufCMessageUnknownField *ufield = + message->unknown_fields + + (message->n_unknown_fields++); + ufield->tag = scanned_member->tag; + ufield->wire_type = scanned_member->wire_type; + ufield->len = scanned_member->len; + ufield->data = do_alloc(allocator, scanned_member->len); + if (ufield->data == NULL) + return FALSE; + memcpy(ufield->data, scanned_member->data, ufield->len); + return TRUE; + } + member = (char *) message + field->offset; + switch (field->label) { + case PROTOBUF_C_LABEL_REQUIRED: + return parse_required_member(scanned_member, member, + allocator, TRUE); + case PROTOBUF_C_LABEL_OPTIONAL: + case PROTOBUF_C_LABEL_NONE: + if (0 != (field->flags & PROTOBUF_C_FIELD_FLAG_ONEOF)) { + return parse_oneof_member(scanned_member, member, + message, allocator); + } else { + return parse_optional_member(scanned_member, member, + message, allocator); + } + case PROTOBUF_C_LABEL_REPEATED: + if (scanned_member->wire_type == + PROTOBUF_C_WIRE_TYPE_LENGTH_PREFIXED && + (0 != (field->flags & PROTOBUF_C_FIELD_FLAG_PACKED) || + is_packable_type(field->type))) + { + return parse_packed_repeated_member(scanned_member, + member, message); + } else { + return parse_repeated_member(scanned_member, + member, message, + allocator); + } + } + PROTOBUF_C__ASSERT_NOT_REACHED(); + return 0; +} + +/** + * Initialise messages generated by old code. + * + * This function is used if desc->message_init == NULL (which occurs + * for old code, and which would be useful to support allocating + * descriptors dynamically). + */ +static void +message_init_generic(const ProtobufCMessageDescriptor *desc, + ProtobufCMessage *message) +{ + unsigned i; + + memset(message, 0, desc->sizeof_message); + message->descriptor = desc; + for (i = 0; i < desc->n_fields; i++) { + if (desc->fields[i].default_value != NULL && + desc->fields[i].label != PROTOBUF_C_LABEL_REPEATED) + { + void *field = + STRUCT_MEMBER_P(message, desc->fields[i].offset); + const void *dv = desc->fields[i].default_value; + + switch (desc->fields[i].type) { + case PROTOBUF_C_TYPE_INT32: + case PROTOBUF_C_TYPE_SINT32: + case PROTOBUF_C_TYPE_SFIXED32: + case PROTOBUF_C_TYPE_UINT32: + case PROTOBUF_C_TYPE_FIXED32: + case PROTOBUF_C_TYPE_FLOAT: + case PROTOBUF_C_TYPE_ENUM: + memcpy(field, dv, 4); + break; + case PROTOBUF_C_TYPE_INT64: + case PROTOBUF_C_TYPE_SINT64: + case PROTOBUF_C_TYPE_SFIXED64: + case PROTOBUF_C_TYPE_UINT64: + case PROTOBUF_C_TYPE_FIXED64: + case PROTOBUF_C_TYPE_DOUBLE: + memcpy(field, dv, 8); + break; + case PROTOBUF_C_TYPE_BOOL: + memcpy(field, dv, sizeof(protobuf_c_boolean)); + break; + case PROTOBUF_C_TYPE_BYTES: + memcpy(field, dv, sizeof(ProtobufCBinaryData)); + break; + + case PROTOBUF_C_TYPE_STRING: + case PROTOBUF_C_TYPE_MESSAGE: + /* + * The next line essentially implements a cast + * from const, which is totally unavoidable. + */ + *(const void **) field = dv; + break; + } + } + } +} + +/**@}*/ + +/* + * ScannedMember slabs (an unpacking implementation detail). Before doing real + * unpacking, we first scan through the elements to see how many there are (for + * repeated fields), and which field to use (for non-repeated fields given + * twice). + * + * In order to avoid allocations for small messages, we keep a stack-allocated + * slab of ScannedMembers of size FIRST_SCANNED_MEMBER_SLAB_SIZE (16). After we + * fill that up, we allocate each slab twice as large as the previous one. + */ +#define FIRST_SCANNED_MEMBER_SLAB_SIZE_LOG2 4 + +/* + * The number of slabs, including the stack-allocated ones; choose the number so + * that we would overflow if we needed a slab larger than provided. + */ +#define MAX_SCANNED_MEMBER_SLAB \ + (sizeof(unsigned int)*8 - 1 \ + - BOUND_SIZEOF_SCANNED_MEMBER_LOG2 \ + - FIRST_SCANNED_MEMBER_SLAB_SIZE_LOG2) + +#define REQUIRED_FIELD_BITMAP_SET(index) \ + (required_fields_bitmap[(index)/8] |= (1UL<<((index)%8))) + +#define REQUIRED_FIELD_BITMAP_IS_SET(index) \ + (required_fields_bitmap[(index)/8] & (1UL<<((index)%8))) + +ProtobufCMessage * +protobuf_c_message_unpack(const ProtobufCMessageDescriptor *desc, + ProtobufCAllocator *allocator, + size_t len, const uint8_t *data) +{ + ProtobufCMessage *rv; + size_t rem = len; + const uint8_t *at = data; + const ProtobufCFieldDescriptor *last_field = desc->fields + 0; + ScannedMember first_member_slab[1UL << + FIRST_SCANNED_MEMBER_SLAB_SIZE_LOG2]; + + /* + * scanned_member_slabs[i] is an array of arrays of ScannedMember. + * The first slab (scanned_member_slabs[0] is just a pointer to + * first_member_slab), above. All subsequent slabs will be allocated + * using the allocator. + */ + ScannedMember *scanned_member_slabs[MAX_SCANNED_MEMBER_SLAB + 1]; + unsigned which_slab = 0; /* the slab we are currently populating */ + unsigned in_slab_index = 0; /* number of members in the slab */ + size_t n_unknown = 0; + unsigned f; + unsigned j; + unsigned i_slab; + unsigned last_field_index = 0; + unsigned required_fields_bitmap_len; + unsigned char required_fields_bitmap_stack[16]; + unsigned char *required_fields_bitmap = required_fields_bitmap_stack; + protobuf_c_boolean required_fields_bitmap_alloced = FALSE; + + ASSERT_IS_MESSAGE_DESCRIPTOR(desc); + + if (allocator == NULL) + allocator = &protobuf_c__allocator; + + rv = do_alloc(allocator, desc->sizeof_message); + if (!rv) + return (NULL); + scanned_member_slabs[0] = first_member_slab; + + required_fields_bitmap_len = (desc->n_fields + 7) / 8; + if (required_fields_bitmap_len > sizeof(required_fields_bitmap_stack)) { + required_fields_bitmap = do_alloc(allocator, required_fields_bitmap_len); + if (!required_fields_bitmap) { + do_free(allocator, rv); + return (NULL); + } + required_fields_bitmap_alloced = TRUE; + } + memset(required_fields_bitmap, 0, required_fields_bitmap_len); + + /* + * Generated code always defines "message_init". However, we provide a + * fallback for (1) users of old protobuf-c generated-code that do not + * provide the function, and (2) descriptors constructed from some other + * source (most likely, direct construction from the .proto file). + */ + if (desc->message_init != NULL) + protobuf_c_message_init(desc, rv); + else + message_init_generic(desc, rv); + + while (rem > 0) { + uint32_t tag; + ProtobufCWireType wire_type; + size_t used = parse_tag_and_wiretype(rem, at, &tag, &wire_type); + const ProtobufCFieldDescriptor *field; + ScannedMember tmp; + + if (used == 0) { + PROTOBUF_C_UNPACK_ERROR("error parsing tag/wiretype at offset %u", + (unsigned) (at - data)); + goto error_cleanup_during_scan; + } + /* + * \todo Consider optimizing for field[1].id == tag, if field[1] + * exists! + */ + if (last_field == NULL || last_field->id != tag) { + /* lookup field */ + int field_index = + int_range_lookup(desc->n_field_ranges, + desc->field_ranges, + tag); + if (field_index < 0) { + field = NULL; + n_unknown++; + } else { + field = desc->fields + field_index; + last_field = field; + last_field_index = field_index; + } + } else { + field = last_field; + } + + if (field != NULL && field->label == PROTOBUF_C_LABEL_REQUIRED) + REQUIRED_FIELD_BITMAP_SET(last_field_index); + + at += used; + rem -= used; + tmp.tag = tag; + tmp.wire_type = wire_type; + tmp.field = field; + tmp.data = at; + tmp.length_prefix_len = 0; + + switch (wire_type) { + case PROTOBUF_C_WIRE_TYPE_VARINT: { + unsigned max_len = rem < 10 ? rem : 10; + unsigned i; + + for (i = 0; i < max_len; i++) + if ((at[i] & 0x80) == 0) + break; + if (i == max_len) { + PROTOBUF_C_UNPACK_ERROR("unterminated varint at offset %u", + (unsigned) (at - data)); + goto error_cleanup_during_scan; + } + tmp.len = i + 1; + break; + } + case PROTOBUF_C_WIRE_TYPE_64BIT: + if (rem < 8) { + PROTOBUF_C_UNPACK_ERROR("too short after 64bit wiretype at offset %u", + (unsigned) (at - data)); + goto error_cleanup_during_scan; + } + tmp.len = 8; + break; + case PROTOBUF_C_WIRE_TYPE_LENGTH_PREFIXED: { + size_t pref_len; + + tmp.len = scan_length_prefixed_data(rem, at, &pref_len); + if (tmp.len == 0) { + /* NOTE: scan_length_prefixed_data calls UNPACK_ERROR */ + goto error_cleanup_during_scan; + } + tmp.length_prefix_len = pref_len; + break; + } + case PROTOBUF_C_WIRE_TYPE_32BIT: + if (rem < 4) { + PROTOBUF_C_UNPACK_ERROR("too short after 32bit wiretype at offset %u", + (unsigned) (at - data)); + goto error_cleanup_during_scan; + } + tmp.len = 4; + break; + default: + PROTOBUF_C_UNPACK_ERROR("unsupported tag %u at offset %u", + wire_type, (unsigned) (at - data)); + goto error_cleanup_during_scan; + } + + if (in_slab_index == (1UL << + (which_slab + FIRST_SCANNED_MEMBER_SLAB_SIZE_LOG2))) + { + size_t size; + + in_slab_index = 0; + if (which_slab == MAX_SCANNED_MEMBER_SLAB) { + PROTOBUF_C_UNPACK_ERROR("too many fields"); + goto error_cleanup_during_scan; + } + which_slab++; + size = sizeof(ScannedMember) + << (which_slab + FIRST_SCANNED_MEMBER_SLAB_SIZE_LOG2); + scanned_member_slabs[which_slab] = do_alloc(allocator, size); + if (scanned_member_slabs[which_slab] == NULL) + goto error_cleanup_during_scan; + } + scanned_member_slabs[which_slab][in_slab_index++] = tmp; + + if (field != NULL && field->label == PROTOBUF_C_LABEL_REPEATED) { + size_t *n = STRUCT_MEMBER_PTR(size_t, rv, + field->quantifier_offset); + if (wire_type == PROTOBUF_C_WIRE_TYPE_LENGTH_PREFIXED && + (0 != (field->flags & PROTOBUF_C_FIELD_FLAG_PACKED) || + is_packable_type(field->type))) + { + size_t count; + if (!count_packed_elements(field->type, + tmp.len - + tmp.length_prefix_len, + tmp.data + + tmp.length_prefix_len, + &count)) + { + PROTOBUF_C_UNPACK_ERROR("counting packed elements"); + goto error_cleanup_during_scan; + } + *n += count; + } else { + *n += 1; + } + } + + at += tmp.len; + rem -= tmp.len; + } + + /* allocate space for repeated fields, also check that all required fields have been set */ + for (f = 0; f < desc->n_fields; f++) { + const ProtobufCFieldDescriptor *field = desc->fields + f; + if (field->label == PROTOBUF_C_LABEL_REPEATED) { + size_t siz = + sizeof_elt_in_repeated_array(field->type); + size_t *n_ptr = + STRUCT_MEMBER_PTR(size_t, rv, + field->quantifier_offset); + if (*n_ptr != 0) { + unsigned n = *n_ptr; + void *a; + *n_ptr = 0; + assert(rv->descriptor != NULL); +#define CLEAR_REMAINING_N_PTRS() \ + for(f++;f < desc->n_fields; f++) \ + { \ + field = desc->fields + f; \ + if (field->label == PROTOBUF_C_LABEL_REPEATED) \ + STRUCT_MEMBER (size_t, rv, field->quantifier_offset) = 0; \ + } + a = do_alloc(allocator, siz * n); + if (!a) { + CLEAR_REMAINING_N_PTRS(); + goto error_cleanup; + } + STRUCT_MEMBER(void *, rv, field->offset) = a; + } + } else if (field->label == PROTOBUF_C_LABEL_REQUIRED) { + if (field->default_value == NULL && + !REQUIRED_FIELD_BITMAP_IS_SET(f)) + { + CLEAR_REMAINING_N_PTRS(); + PROTOBUF_C_UNPACK_ERROR("message '%s': missing required field '%s'", + desc->name, field->name); + goto error_cleanup; + } + } + } +#undef CLEAR_REMAINING_N_PTRS + + /* allocate space for unknown fields */ + if (n_unknown) { + rv->unknown_fields = do_alloc(allocator, + n_unknown * sizeof(ProtobufCMessageUnknownField)); + if (rv->unknown_fields == NULL) + goto error_cleanup; + } + + /* do real parsing */ + for (i_slab = 0; i_slab <= which_slab; i_slab++) { + unsigned max = (i_slab == which_slab) ? + in_slab_index : (1UL << (i_slab + 4)); + ScannedMember *slab = scanned_member_slabs[i_slab]; + + for (j = 0; j < max; j++) { + if (!parse_member(slab + j, rv, allocator)) { + PROTOBUF_C_UNPACK_ERROR("error parsing member %s of %s", + slab->field ? slab->field->name : "*unknown-field*", + desc->name); + goto error_cleanup; + } + } + } + + /* cleanup */ + for (j = 1; j <= which_slab; j++) + do_free(allocator, scanned_member_slabs[j]); + if (required_fields_bitmap_alloced) + do_free(allocator, required_fields_bitmap); + return rv; + +error_cleanup: + protobuf_c_message_free_unpacked(rv, allocator); + for (j = 1; j <= which_slab; j++) + do_free(allocator, scanned_member_slabs[j]); + if (required_fields_bitmap_alloced) + do_free(allocator, required_fields_bitmap); + return NULL; + +error_cleanup_during_scan: + do_free(allocator, rv); + for (j = 1; j <= which_slab; j++) + do_free(allocator, scanned_member_slabs[j]); + if (required_fields_bitmap_alloced) + do_free(allocator, required_fields_bitmap); + return NULL; +} + +void +protobuf_c_message_free_unpacked(ProtobufCMessage *message, + ProtobufCAllocator *allocator) +{ + const ProtobufCMessageDescriptor *desc; + unsigned f; + + if (message == NULL) + return; + + desc = message->descriptor; + + ASSERT_IS_MESSAGE(message); + + if (allocator == NULL) + allocator = &protobuf_c__allocator; + message->descriptor = NULL; + for (f = 0; f < desc->n_fields; f++) { + if (0 != (desc->fields[f].flags & PROTOBUF_C_FIELD_FLAG_ONEOF) && + desc->fields[f].id != + STRUCT_MEMBER(uint32_t, message, desc->fields[f].quantifier_offset)) + { + /* This is not the selected oneof, skip it */ + continue; + } + + if (desc->fields[f].label == PROTOBUF_C_LABEL_REPEATED) { + size_t n = STRUCT_MEMBER(size_t, + message, + desc->fields[f].quantifier_offset); + void *arr = STRUCT_MEMBER(void *, + message, + desc->fields[f].offset); + + if (arr != NULL) { + if (desc->fields[f].type == PROTOBUF_C_TYPE_STRING) { + unsigned i; + for (i = 0; i < n; i++) + do_free(allocator, ((char **) arr)[i]); + } else if (desc->fields[f].type == PROTOBUF_C_TYPE_BYTES) { + unsigned i; + for (i = 0; i < n; i++) + do_free(allocator, ((ProtobufCBinaryData *) arr)[i].data); + } else if (desc->fields[f].type == PROTOBUF_C_TYPE_MESSAGE) { + unsigned i; + for (i = 0; i < n; i++) + protobuf_c_message_free_unpacked( + ((ProtobufCMessage **) arr)[i], + allocator + ); + } + do_free(allocator, arr); + } + } else if (desc->fields[f].type == PROTOBUF_C_TYPE_STRING) { + char *str = STRUCT_MEMBER(char *, message, + desc->fields[f].offset); + + if (str && str != desc->fields[f].default_value) + do_free(allocator, str); + } else if (desc->fields[f].type == PROTOBUF_C_TYPE_BYTES) { + void *data = STRUCT_MEMBER(ProtobufCBinaryData, message, + desc->fields[f].offset).data; + const ProtobufCBinaryData *default_bd; + + default_bd = desc->fields[f].default_value; + if (data != NULL && + (default_bd == NULL || + default_bd->data != data)) + { + do_free(allocator, data); + } + } else if (desc->fields[f].type == PROTOBUF_C_TYPE_MESSAGE) { + ProtobufCMessage *sm; + + sm = STRUCT_MEMBER(ProtobufCMessage *, message, + desc->fields[f].offset); + if (sm && sm != desc->fields[f].default_value) + protobuf_c_message_free_unpacked(sm, allocator); + } + } + + for (f = 0; f < message->n_unknown_fields; f++) + do_free(allocator, message->unknown_fields[f].data); + if (message->unknown_fields != NULL) + do_free(allocator, message->unknown_fields); + + do_free(allocator, message); +} + +void +protobuf_c_message_init(const ProtobufCMessageDescriptor * descriptor, + void *message) +{ + descriptor->message_init((ProtobufCMessage *) (message)); +} + +protobuf_c_boolean +protobuf_c_message_check(const ProtobufCMessage *message) +{ + unsigned i; + + if (!message || + !message->descriptor || + message->descriptor->magic != PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC) + { + return FALSE; + } + + for (i = 0; i < message->descriptor->n_fields; i++) { + const ProtobufCFieldDescriptor *f = message->descriptor->fields + i; + ProtobufCType type = f->type; + ProtobufCLabel label = f->label; + void *field = STRUCT_MEMBER_P (message, f->offset); + + if (f->flags & PROTOBUF_C_FIELD_FLAG_ONEOF) { + const uint32_t *oneof_case = STRUCT_MEMBER_P (message, f->quantifier_offset); + if (f->id != *oneof_case) { + continue; //Do not check if it is an unpopulated oneof member. + } + } + + if (label == PROTOBUF_C_LABEL_REPEATED) { + size_t *quantity = STRUCT_MEMBER_P (message, f->quantifier_offset); + + if (*quantity > 0 && *(void **) field == NULL) { + return FALSE; + } + + if (type == PROTOBUF_C_TYPE_MESSAGE) { + ProtobufCMessage **submessage = *(ProtobufCMessage ***) field; + unsigned j; + for (j = 0; j < *quantity; j++) { + if (!protobuf_c_message_check(submessage[j])) + return FALSE; + } + } else if (type == PROTOBUF_C_TYPE_STRING) { + char **string = *(char ***) field; + unsigned j; + for (j = 0; j < *quantity; j++) { + if (!string[j]) + return FALSE; + } + } else if (type == PROTOBUF_C_TYPE_BYTES) { + ProtobufCBinaryData *bd = *(ProtobufCBinaryData **) field; + unsigned j; + for (j = 0; j < *quantity; j++) { + if (bd[j].len > 0 && bd[j].data == NULL) + return FALSE; + } + } + + } else { /* PROTOBUF_C_LABEL_REQUIRED or PROTOBUF_C_LABEL_OPTIONAL */ + + if (type == PROTOBUF_C_TYPE_MESSAGE) { + ProtobufCMessage *submessage = *(ProtobufCMessage **) field; + if (label == PROTOBUF_C_LABEL_REQUIRED || submessage != NULL) { + if (!protobuf_c_message_check(submessage)) + return FALSE; + } + } else if (type == PROTOBUF_C_TYPE_STRING) { + char *string = *(char **) field; + if (label == PROTOBUF_C_LABEL_REQUIRED && string == NULL) + return FALSE; + } else if (type == PROTOBUF_C_TYPE_BYTES) { + protobuf_c_boolean *has = STRUCT_MEMBER_P (message, f->quantifier_offset); + ProtobufCBinaryData *bd = field; + if (label == PROTOBUF_C_LABEL_REQUIRED || *has == TRUE) { + if (bd->len > 0 && bd->data == NULL) + return FALSE; + } + } + } + } + + return TRUE; +} + +/* === services === */ + +typedef void (*GenericHandler) (void *service, + const ProtobufCMessage *input, + ProtobufCClosure closure, + void *closure_data); +void +protobuf_c_service_invoke_internal(ProtobufCService *service, + unsigned method_index, + const ProtobufCMessage *input, + ProtobufCClosure closure, + void *closure_data) +{ + GenericHandler *handlers; + GenericHandler handler; + + /* + * Verify that method_index is within range. If this fails, you are + * likely invoking a newly added method on an old service. (Although + * other memory corruption bugs can cause this assertion too.) + */ + assert(method_index < service->descriptor->n_methods); + + /* + * Get the array of virtual methods (which are enumerated by the + * generated code). + */ + handlers = (GenericHandler *) (service + 1); + + /* + * Get our method and invoke it. + * \todo Seems like handler == NULL is a situation that needs handling. + */ + handler = handlers[method_index]; + (*handler)(service, input, closure, closure_data); +} + +void +protobuf_c_service_generated_init(ProtobufCService *service, + const ProtobufCServiceDescriptor *descriptor, + ProtobufCServiceDestroy destroy) +{ + ASSERT_IS_SERVICE_DESCRIPTOR(descriptor); + service->descriptor = descriptor; + service->destroy = destroy; + service->invoke = protobuf_c_service_invoke_internal; + memset(service + 1, 0, descriptor->n_methods * sizeof(GenericHandler)); +} + +void protobuf_c_service_destroy(ProtobufCService *service) +{ + service->destroy(service); +} + +/* --- querying the descriptors --- */ + +const ProtobufCEnumValue * +protobuf_c_enum_descriptor_get_value_by_name(const ProtobufCEnumDescriptor *desc, + const char *name) +{ + unsigned start = 0; + unsigned count; + + if (desc == NULL || desc->values_by_name == NULL) + return NULL; + + count = desc->n_value_names; + + while (count > 1) { + unsigned mid = start + count / 2; + int rv = strcmp(desc->values_by_name[mid].name, name); + if (rv == 0) + return desc->values + desc->values_by_name[mid].index; + else if (rv < 0) { + count = start + count - (mid + 1); + start = mid + 1; + } else + count = mid - start; + } + if (count == 0) + return NULL; + if (strcmp(desc->values_by_name[start].name, name) == 0) + return desc->values + desc->values_by_name[start].index; + return NULL; +} + +const ProtobufCEnumValue * +protobuf_c_enum_descriptor_get_value(const ProtobufCEnumDescriptor *desc, + int value) +{ + int rv = int_range_lookup(desc->n_value_ranges, desc->value_ranges, value); + if (rv < 0) + return NULL; + return desc->values + rv; +} + +const ProtobufCFieldDescriptor * +protobuf_c_message_descriptor_get_field_by_name(const ProtobufCMessageDescriptor *desc, + const char *name) +{ + unsigned start = 0; + unsigned count; + const ProtobufCFieldDescriptor *field; + + if (desc == NULL || desc->fields_sorted_by_name == NULL) + return NULL; + + count = desc->n_fields; + + while (count > 1) { + unsigned mid = start + count / 2; + int rv; + field = desc->fields + desc->fields_sorted_by_name[mid]; + rv = strcmp(field->name, name); + if (rv == 0) + return field; + else if (rv < 0) { + count = start + count - (mid + 1); + start = mid + 1; + } else + count = mid - start; + } + if (count == 0) + return NULL; + field = desc->fields + desc->fields_sorted_by_name[start]; + if (strcmp(field->name, name) == 0) + return field; + return NULL; +} + +const ProtobufCFieldDescriptor * +protobuf_c_message_descriptor_get_field(const ProtobufCMessageDescriptor *desc, + unsigned value) +{ + int rv = int_range_lookup(desc->n_field_ranges,desc->field_ranges, value); + if (rv < 0) + return NULL; + return desc->fields + rv; +} + +const ProtobufCMethodDescriptor * +protobuf_c_service_descriptor_get_method_by_name(const ProtobufCServiceDescriptor *desc, + const char *name) +{ + unsigned start = 0; + unsigned count; + + if (desc == NULL || desc->method_indices_by_name == NULL) + return NULL; + + count = desc->n_methods; + + while (count > 1) { + unsigned mid = start + count / 2; + unsigned mid_index = desc->method_indices_by_name[mid]; + const char *mid_name = desc->methods[mid_index].name; + int rv = strcmp(mid_name, name); + + if (rv == 0) + return desc->methods + desc->method_indices_by_name[mid]; + if (rv < 0) { + count = start + count - (mid + 1); + start = mid + 1; + } else { + count = mid - start; + } + } + if (count == 0) + return NULL; + if (strcmp(desc->methods[desc->method_indices_by_name[start]].name, name) == 0) + return desc->methods + desc->method_indices_by_name[start]; + return NULL; +} diff --git a/submodules/AppCenter-sdk/Vendor/PLCrashReporter/Dependencies/protobuf-c/protobuf-c/protobuf-c.h b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/Dependencies/protobuf-c/protobuf-c/protobuf-c.h new file mode 100755 index 0000000000..e2f027e8e5 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/Dependencies/protobuf-c/protobuf-c/protobuf-c.h @@ -0,0 +1,1106 @@ +/* + * Copyright (c) 2008-2018, Dave Benson and the protobuf-c authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/*! \file + * \mainpage Introduction + * + * This is [protobuf-c], a C implementation of [Protocol Buffers]. + * + * This file defines the public API for the `libprotobuf-c` support library. + * This API includes interfaces that can be used directly by client code as well + * as the interfaces used by the code generated by the `protoc-c` compiler. + * + * The `libprotobuf-c` support library performs the actual serialization and + * deserialization of Protocol Buffers messages. It interacts with structures, + * definitions, and metadata generated by the `protoc-c` compiler from .proto + * files. + * + * \authors Dave Benson and the `protobuf-c` authors. + * + * \copyright 2008-2014. Licensed under the terms of the [BSD-2-Clause] license. + * + * [protobuf-c]: https://github.com/protobuf-c/protobuf-c + * [Protocol Buffers]: https://developers.google.com/protocol-buffers/ + * [BSD-2-Clause]: http://opensource.org/licenses/BSD-2-Clause + * + * \page gencode Generated Code + * + * For each enum, we generate a C enum. For each message, we generate a C + * structure which can be cast to a `ProtobufCMessage`. + * + * For each enum and message, we generate a descriptor object that allows us to + * implement a kind of reflection on the structures. + * + * First, some naming conventions: + * + * - The name of the type for enums and messages and services is camel case + * (meaning WordsAreCrammedTogether) except that double underscores are used + * to delimit scopes. For example, the following `.proto` file: + * +~~~{.proto} + package foo.bar; + message BazBah { + optional int32 val = 1; + } +~~~ + * + * would generate a C type `Foo__Bar__BazBah`. + * + * - Identifiers for functions and globals are all lowercase, with camel case + * words separated by single underscores. For example, one of the function + * prototypes generated by `protoc-c` for the above example: + * +~~~{.c} +Foo__Bar__BazBah * + foo__bar__baz_bah__unpack + (ProtobufCAllocator *allocator, + size_t len, + const uint8_t *data); +~~~ + * + * - Identifiers for enum values contain an uppercase prefix which embeds the + * package name and the enum type name. + * + * - A double underscore is used to separate further components of identifier + * names. + * + * For example, in the name of the unpack function above, the package name + * `foo.bar` has become `foo__bar`, the message name BazBah has become + * `baz_bah`, and the method name is `unpack`. These are all joined with double + * underscores to form the C identifier `foo__bar__baz_bah__unpack`. + * + * We also generate descriptor objects for messages and enums. These are + * declared in the `.pb-c.h` files: + * +~~~{.c} +extern const ProtobufCMessageDescriptor foo__bar__baz_bah__descriptor; +~~~ + * + * The message structures all begin with `ProtobufCMessageDescriptor *` which is + * sufficient to allow them to be cast to `ProtobufCMessage`. + * + * For each message defined in a `.proto` file, we generate a number of + * functions and macros. Each function name contains a prefix based on the + * package name and message name in order to make it a unique C identifier. + * + * - `INIT`. Statically initializes a message object, initializing its + * descriptor and setting its fields to default values. Uninitialized + * messages cannot be processed by the protobuf-c library. + * +~~~{.c} +#define FOO__BAR__BAZ_BAH__INIT \ + { PROTOBUF_C_MESSAGE_INIT (&foo__bar__baz_bah__descriptor), 0 } +~~~ + * - `init()`. Initializes a message object, initializing its descriptor and + * setting its fields to default values. Uninitialized messages cannot be + * processed by the protobuf-c library. + * +~~~{.c} +void foo__bar__baz_bah__init + (Foo__Bar__BazBah *message); +~~~ + * - `unpack()`. Unpacks data for a particular message format. Note that the + * `allocator` parameter is usually `NULL` to indicate that the system's + * `malloc()` and `free()` functions should be used for dynamically allocating + * memory. + * +~~~{.c} +Foo__Bar__BazBah * + foo__bar__baz_bah__unpack + (ProtobufCAllocator *allocator, + size_t len, + const uint8_t *data); +~~~ + * + * - `free_unpacked()`. Frees a message object obtained with the `unpack()` + * method. Freeing `NULL` is allowed (the same as with `free()`). + * +~~~{.c} +void foo__bar__baz_bah__free_unpacked + (Foo__Bar__BazBah *message, + ProtobufCAllocator *allocator); +~~~ + * + * - `get_packed_size()`. Calculates the length in bytes of the serialized + * representation of the message object. + * +~~~{.c} +size_t foo__bar__baz_bah__get_packed_size + (const Foo__Bar__BazBah *message); +~~~ + * + * - `pack()`. Pack a message object into a preallocated buffer. Assumes that + * the buffer is large enough. (Use `get_packed_size()` first.) + * +~~~{.c} +size_t foo__bar__baz_bah__pack + (const Foo__Bar__BazBah *message, + uint8_t *out); +~~~ + * + * - `pack_to_buffer()`. Packs a message into a "virtual buffer". This is an + * object which defines an "append bytes" callback to consume data as it is + * serialized. + * +~~~{.c} +size_t foo__bar__baz_bah__pack_to_buffer + (const Foo__Bar__BazBah *message, + ProtobufCBuffer *buffer); +~~~ + * + * \page pack Packing and unpacking messages + * + * To pack a message, first compute the packed size of the message with + * protobuf_c_message_get_packed_size(), then allocate a buffer of at least + * that size, then call protobuf_c_message_pack(). + * + * Alternatively, a message can be serialized without calculating the final size + * first. Use the protobuf_c_message_pack_to_buffer() function and provide a + * ProtobufCBuffer object which implements an "append" method that consumes + * data. + * + * To unpack a message, call the protobuf_c_message_unpack() function. The + * result can be cast to an object of the type that matches the descriptor for + * the message. + * + * The result of unpacking a message should be freed with + * protobuf_c_message_free_unpacked(). + */ + +#ifndef PROTOBUF_C_H +#define PROTOBUF_C_H + +#include +#include +#include +#include + +#ifdef __cplusplus +# define PROTOBUF_C__BEGIN_DECLS extern "C" { +# define PROTOBUF_C__END_DECLS } +#else +# define PROTOBUF_C__BEGIN_DECLS +# define PROTOBUF_C__END_DECLS +#endif + +PROTOBUF_C__BEGIN_DECLS + +#if defined(_WIN32) && defined(PROTOBUF_C_USE_SHARED_LIB) +# ifdef PROTOBUF_C_EXPORT +# define PROTOBUF_C__API __declspec(dllexport) +# else +# define PROTOBUF_C__API __declspec(dllimport) +# endif +#else +# define PROTOBUF_C__API +#endif + +#if !defined(PROTOBUF_C__NO_DEPRECATED) && \ + ((__GNUC__ > 3) || (__GNUC__ == 3 && __GNUC_MINOR__ >= 1)) +# define PROTOBUF_C__DEPRECATED __attribute__((__deprecated__)) +#else +# define PROTOBUF_C__DEPRECATED +#endif + +#ifndef PROTOBUF_C__FORCE_ENUM_TO_BE_INT_SIZE + #define PROTOBUF_C__FORCE_ENUM_TO_BE_INT_SIZE(enum_name) \ + , _##enum_name##_IS_INT_SIZE = INT_MAX +#endif + +#define PROTOBUF_C__SERVICE_DESCRIPTOR_MAGIC 0x14159bc3 +#define PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC 0x28aaeef9 +#define PROTOBUF_C__ENUM_DESCRIPTOR_MAGIC 0x114315af + +/* Empty string used for initializers */ +extern const char protobuf_c_empty_string[]; + +/** + * \defgroup api Public API + * + * This is the public API for `libprotobuf-c`. These interfaces are stable and + * subject to Semantic Versioning guarantees. + * + * @{ + */ + +/** + * Values for the `flags` word in `ProtobufCFieldDescriptor`. + */ +typedef enum { + /** Set if the field is repeated and marked with the `packed` option. */ + PROTOBUF_C_FIELD_FLAG_PACKED = (1 << 0), + + /** Set if the field is marked with the `deprecated` option. */ + PROTOBUF_C_FIELD_FLAG_DEPRECATED = (1 << 1), + + /** Set if the field is a member of a oneof (union). */ + PROTOBUF_C_FIELD_FLAG_ONEOF = (1 << 2), +} ProtobufCFieldFlag; + +/** + * Message field rules. + * + * \see [Defining A Message Type] in the Protocol Buffers documentation. + * + * [Defining A Message Type]: + * https://developers.google.com/protocol-buffers/docs/proto#simple + */ +typedef enum { + /** A well-formed message must have exactly one of this field. */ + PROTOBUF_C_LABEL_REQUIRED, + + /** + * A well-formed message can have zero or one of this field (but not + * more than one). + */ + PROTOBUF_C_LABEL_OPTIONAL, + + /** + * This field can be repeated any number of times (including zero) in a + * well-formed message. The order of the repeated values will be + * preserved. + */ + PROTOBUF_C_LABEL_REPEATED, + + /** + * This field has no label. This is valid only in proto3 and is + * equivalent to OPTIONAL but no "has" quantifier will be consulted. + */ + PROTOBUF_C_LABEL_NONE, +} ProtobufCLabel; + +/** + * Field value types. + * + * \see [Scalar Value Types] in the Protocol Buffers documentation. + * + * [Scalar Value Types]: + * https://developers.google.com/protocol-buffers/docs/proto#scalar + */ +typedef enum { + PROTOBUF_C_TYPE_INT32, /**< int32 */ + PROTOBUF_C_TYPE_SINT32, /**< signed int32 */ + PROTOBUF_C_TYPE_SFIXED32, /**< signed int32 (4 bytes) */ + PROTOBUF_C_TYPE_INT64, /**< int64 */ + PROTOBUF_C_TYPE_SINT64, /**< signed int64 */ + PROTOBUF_C_TYPE_SFIXED64, /**< signed int64 (8 bytes) */ + PROTOBUF_C_TYPE_UINT32, /**< unsigned int32 */ + PROTOBUF_C_TYPE_FIXED32, /**< unsigned int32 (4 bytes) */ + PROTOBUF_C_TYPE_UINT64, /**< unsigned int64 */ + PROTOBUF_C_TYPE_FIXED64, /**< unsigned int64 (8 bytes) */ + PROTOBUF_C_TYPE_FLOAT, /**< float */ + PROTOBUF_C_TYPE_DOUBLE, /**< double */ + PROTOBUF_C_TYPE_BOOL, /**< boolean */ + PROTOBUF_C_TYPE_ENUM, /**< enumerated type */ + PROTOBUF_C_TYPE_STRING, /**< UTF-8 or ASCII string */ + PROTOBUF_C_TYPE_BYTES, /**< arbitrary byte sequence */ + PROTOBUF_C_TYPE_MESSAGE, /**< nested message */ +} ProtobufCType; + +/** + * Field wire types. + * + * \see [Message Structure] in the Protocol Buffers documentation. + * + * [Message Structure]: + * https://developers.google.com/protocol-buffers/docs/encoding#structure + */ +typedef enum { + PROTOBUF_C_WIRE_TYPE_VARINT = 0, + PROTOBUF_C_WIRE_TYPE_64BIT = 1, + PROTOBUF_C_WIRE_TYPE_LENGTH_PREFIXED = 2, + /* "Start group" and "end group" wire types are unsupported. */ + PROTOBUF_C_WIRE_TYPE_32BIT = 5, +} ProtobufCWireType; + +struct ProtobufCAllocator; +struct ProtobufCBinaryData; +struct ProtobufCBuffer; +struct ProtobufCBufferSimple; +struct ProtobufCEnumDescriptor; +struct ProtobufCEnumValue; +struct ProtobufCEnumValueIndex; +struct ProtobufCFieldDescriptor; +struct ProtobufCIntRange; +struct ProtobufCMessage; +struct ProtobufCMessageDescriptor; +struct ProtobufCMessageUnknownField; +struct ProtobufCMethodDescriptor; +struct ProtobufCService; +struct ProtobufCServiceDescriptor; + +typedef struct ProtobufCAllocator ProtobufCAllocator; +typedef struct ProtobufCBinaryData ProtobufCBinaryData; +typedef struct ProtobufCBuffer ProtobufCBuffer; +typedef struct ProtobufCBufferSimple ProtobufCBufferSimple; +typedef struct ProtobufCEnumDescriptor ProtobufCEnumDescriptor; +typedef struct ProtobufCEnumValue ProtobufCEnumValue; +typedef struct ProtobufCEnumValueIndex ProtobufCEnumValueIndex; +typedef struct ProtobufCFieldDescriptor ProtobufCFieldDescriptor; +typedef struct ProtobufCIntRange ProtobufCIntRange; +typedef struct ProtobufCMessage ProtobufCMessage; +typedef struct ProtobufCMessageDescriptor ProtobufCMessageDescriptor; +typedef struct ProtobufCMessageUnknownField ProtobufCMessageUnknownField; +typedef struct ProtobufCMethodDescriptor ProtobufCMethodDescriptor; +typedef struct ProtobufCService ProtobufCService; +typedef struct ProtobufCServiceDescriptor ProtobufCServiceDescriptor; + +/** Boolean type. */ +typedef int protobuf_c_boolean; + +typedef void (*ProtobufCClosure)(const ProtobufCMessage *, void *closure_data); +typedef void (*ProtobufCMessageInit)(ProtobufCMessage *); +typedef void (*ProtobufCServiceDestroy)(ProtobufCService *); + +/** + * Structure for defining a custom memory allocator. + */ +struct ProtobufCAllocator { + /** Function to allocate memory. */ + void *(*alloc)(void *allocator_data, size_t size); + + /** Function to free memory. */ + void (*free)(void *allocator_data, void *pointer); + + /** Opaque pointer passed to `alloc` and `free` functions. */ + void *allocator_data; +}; + +/** + * Structure for the protobuf `bytes` scalar type. + * + * The data contained in a `ProtobufCBinaryData` is an arbitrary sequence of + * bytes. It may contain embedded `NUL` characters and is not required to be + * `NUL`-terminated. + */ +struct ProtobufCBinaryData { + size_t len; /**< Number of bytes in the `data` field. */ + uint8_t *data; /**< Data bytes. */ +}; + +/** + * Structure for defining a virtual append-only buffer. Used by + * protobuf_c_message_pack_to_buffer() to abstract the consumption of serialized + * bytes. + * + * `ProtobufCBuffer` "subclasses" may be defined on the stack. For example, to + * write to a `FILE` object: + * +~~~{.c} +typedef struct { + ProtobufCBuffer base; + FILE *fp; +} BufferAppendToFile; + +static void +my_buffer_file_append(ProtobufCBuffer *buffer, + size_t len, + const uint8_t *data) +{ + BufferAppendToFile *file_buf = (BufferAppendToFile *) buffer; + fwrite(data, len, 1, file_buf->fp); // XXX: No error handling! +} +~~~ + * + * To use this new type of ProtobufCBuffer, it could be called as follows: + * +~~~{.c} +... +BufferAppendToFile tmp = {0}; +tmp.base.append = my_buffer_file_append; +tmp.fp = fp; +protobuf_c_message_pack_to_buffer(&message, &tmp); +... +~~~ + */ +struct ProtobufCBuffer { + /** Append function. Consumes the `len` bytes stored at `data`. */ + void (*append)(ProtobufCBuffer *buffer, + size_t len, + const uint8_t *data); +}; + +/** + * Simple buffer "subclass" of `ProtobufCBuffer`. + * + * A `ProtobufCBufferSimple` object is declared on the stack and uses a + * scratch buffer provided by the user for the initial allocation. It performs + * exponential resizing, using dynamically allocated memory. A + * `ProtobufCBufferSimple` object can be created and used as follows: + * +~~~{.c} +uint8_t pad[128]; +ProtobufCBufferSimple simple = PROTOBUF_C_BUFFER_SIMPLE_INIT(pad); +ProtobufCBuffer *buffer = (ProtobufCBuffer *) &simple; +~~~ + * + * `buffer` can now be used with `protobuf_c_message_pack_to_buffer()`. Once a + * message has been serialized to a `ProtobufCBufferSimple` object, the + * serialized data bytes can be accessed from the `.data` field. + * + * To free the memory allocated by a `ProtobufCBufferSimple` object, if any, + * call PROTOBUF_C_BUFFER_SIMPLE_CLEAR() on the object, for example: + * +~~~{.c} +PROTOBUF_C_BUFFER_SIMPLE_CLEAR(&simple); +~~~ + * + * \see PROTOBUF_C_BUFFER_SIMPLE_INIT + * \see PROTOBUF_C_BUFFER_SIMPLE_CLEAR + */ +struct ProtobufCBufferSimple { + /** "Base class". */ + ProtobufCBuffer base; + /** Number of bytes allocated in `data`. */ + size_t alloced; + /** Number of bytes currently stored in `data`. */ + size_t len; + /** Data bytes. */ + uint8_t *data; + /** Whether `data` must be freed. */ + protobuf_c_boolean must_free_data; + /** Allocator to use. May be NULL to indicate the system allocator. */ + ProtobufCAllocator *allocator; +}; + +/** + * Describes an enumeration as a whole, with all of its values. + */ +struct ProtobufCEnumDescriptor { + /** Magic value checked to ensure that the API is used correctly. */ + uint32_t magic; + + /** The qualified name (e.g., "namespace.Type"). */ + const char *name; + /** The unqualified name as given in the .proto file (e.g., "Type"). */ + const char *short_name; + /** Identifier used in generated C code. */ + const char *c_name; + /** The dot-separated namespace. */ + const char *package_name; + + /** Number elements in `values`. */ + unsigned n_values; + /** Array of distinct values, sorted by numeric value. */ + const ProtobufCEnumValue *values; + + /** Number of elements in `values_by_name`. */ + unsigned n_value_names; + /** Array of named values, including aliases, sorted by name. */ + const ProtobufCEnumValueIndex *values_by_name; + + /** Number of elements in `value_ranges`. */ + unsigned n_value_ranges; + /** Value ranges, for faster lookups by numeric value. */ + const ProtobufCIntRange *value_ranges; + + /** Reserved for future use. */ + void *reserved1; + /** Reserved for future use. */ + void *reserved2; + /** Reserved for future use. */ + void *reserved3; + /** Reserved for future use. */ + void *reserved4; +}; + +/** + * Represents a single value of an enumeration. + */ +struct ProtobufCEnumValue { + /** The string identifying this value in the .proto file. */ + const char *name; + + /** The string identifying this value in generated C code. */ + const char *c_name; + + /** The numeric value assigned in the .proto file. */ + int value; +}; + +/** + * Used by `ProtobufCEnumDescriptor` to look up enum values. + */ +struct ProtobufCEnumValueIndex { + /** Name of the enum value. */ + const char *name; + /** Index into values[] array. */ + unsigned index; +}; + +/** + * Describes a single field in a message. + */ +struct ProtobufCFieldDescriptor { + /** Name of the field as given in the .proto file. */ + const char *name; + + /** Tag value of the field as given in the .proto file. */ + uint32_t id; + + /** Whether the field is `REQUIRED`, `OPTIONAL`, or `REPEATED`. */ + ProtobufCLabel label; + + /** The type of the field. */ + ProtobufCType type; + + /** + * The offset in bytes of the message's C structure's quantifier field + * (the `has_MEMBER` field for optional members or the `n_MEMBER` field + * for repeated members or the case enum for oneofs). + */ + unsigned quantifier_offset; + + /** + * The offset in bytes into the message's C structure for the member + * itself. + */ + unsigned offset; + + /** + * A type-specific descriptor. + * + * If `type` is `PROTOBUF_C_TYPE_ENUM`, then `descriptor` points to the + * corresponding `ProtobufCEnumDescriptor`. + * + * If `type` is `PROTOBUF_C_TYPE_MESSAGE`, then `descriptor` points to + * the corresponding `ProtobufCMessageDescriptor`. + * + * Otherwise this field is NULL. + */ + const void *descriptor; /* for MESSAGE and ENUM types */ + + /** The default value for this field, if defined. May be NULL. */ + const void *default_value; + + /** + * A flag word. Zero or more of the bits defined in the + * `ProtobufCFieldFlag` enum may be set. + */ + uint32_t flags; + + /** Reserved for future use. */ + unsigned reserved_flags; + /** Reserved for future use. */ + void *reserved2; + /** Reserved for future use. */ + void *reserved3; +}; + +/** + * Helper structure for optimizing int => index lookups in the case + * where the keys are mostly consecutive values, as they presumably are for + * enums and fields. + * + * The data structures requires that the values in the original array are + * sorted. + */ +struct ProtobufCIntRange { + int start_value; + unsigned orig_index; + /* + * NOTE: the number of values in the range can be inferred by looking + * at the next element's orig_index. A dummy element is added to make + * this simple. + */ +}; + +/** + * An instance of a message. + * + * `ProtobufCMessage` is a light-weight "base class" for all messages. + * + * In particular, `ProtobufCMessage` doesn't have any allocation policy + * associated with it. That's because it's common to create `ProtobufCMessage` + * objects on the stack. In fact, that's what we recommend for sending messages. + * If the object is allocated from the stack, you can't really have a memory + * leak. + * + * This means that calls to functions like protobuf_c_message_unpack() which + * return a `ProtobufCMessage` must be paired with a call to a free function, + * like protobuf_c_message_free_unpacked(). + */ +struct ProtobufCMessage { + /** The descriptor for this message type. */ + const ProtobufCMessageDescriptor *descriptor; + /** The number of elements in `unknown_fields`. */ + unsigned n_unknown_fields; + /** The fields that weren't recognized by the parser. */ + ProtobufCMessageUnknownField *unknown_fields; +}; + +/** + * Describes a message. + */ +struct ProtobufCMessageDescriptor { + /** Magic value checked to ensure that the API is used correctly. */ + uint32_t magic; + + /** The qualified name (e.g., "namespace.Type"). */ + const char *name; + /** The unqualified name as given in the .proto file (e.g., "Type"). */ + const char *short_name; + /** Identifier used in generated C code. */ + const char *c_name; + /** The dot-separated namespace. */ + const char *package_name; + + /** + * Size in bytes of the C structure representing an instance of this + * type of message. + */ + size_t sizeof_message; + + /** Number of elements in `fields`. */ + unsigned n_fields; + /** Field descriptors, sorted by tag number. */ + const ProtobufCFieldDescriptor *fields; + /** Used for looking up fields by name. */ + const unsigned *fields_sorted_by_name; + + /** Number of elements in `field_ranges`. */ + unsigned n_field_ranges; + /** Used for looking up fields by id. */ + const ProtobufCIntRange *field_ranges; + + /** Message initialisation function. */ + ProtobufCMessageInit message_init; + + /** Reserved for future use. */ + void *reserved1; + /** Reserved for future use. */ + void *reserved2; + /** Reserved for future use. */ + void *reserved3; +}; + +/** + * An unknown message field. + */ +struct ProtobufCMessageUnknownField { + /** The tag number. */ + uint32_t tag; + /** The wire type of the field. */ + ProtobufCWireType wire_type; + /** Number of bytes in `data`. */ + size_t len; + /** Field data. */ + uint8_t *data; +}; + +/** + * Method descriptor. + */ +struct ProtobufCMethodDescriptor { + /** Method name. */ + const char *name; + /** Input message descriptor. */ + const ProtobufCMessageDescriptor *input; + /** Output message descriptor. */ + const ProtobufCMessageDescriptor *output; +}; + +/** + * Service. + */ +struct ProtobufCService { + /** Service descriptor. */ + const ProtobufCServiceDescriptor *descriptor; + /** Function to invoke the service. */ + void (*invoke)(ProtobufCService *service, + unsigned method_index, + const ProtobufCMessage *input, + ProtobufCClosure closure, + void *closure_data); + /** Function to destroy the service. */ + void (*destroy)(ProtobufCService *service); +}; + +/** + * Service descriptor. + */ +struct ProtobufCServiceDescriptor { + /** Magic value checked to ensure that the API is used correctly. */ + uint32_t magic; + + /** Service name. */ + const char *name; + /** Short version of service name. */ + const char *short_name; + /** C identifier for the service name. */ + const char *c_name; + /** Package name. */ + const char *package; + /** Number of elements in `methods`. */ + unsigned n_methods; + /** Method descriptors, in the order defined in the .proto file. */ + const ProtobufCMethodDescriptor *methods; + /** Sort index of methods. */ + const unsigned *method_indices_by_name; +}; + +/** + * Get the version of the protobuf-c library. Note that this is the version of + * the library linked against, not the version of the headers compiled against. + * + * \return A string containing the version number of protobuf-c. + */ +PROTOBUF_C__API +const char * +protobuf_c_version(void); + +/** + * Get the version of the protobuf-c library. Note that this is the version of + * the library linked against, not the version of the headers compiled against. + * + * \return A 32 bit unsigned integer containing the version number of + * protobuf-c, represented in base-10 as (MAJOR*1E6) + (MINOR*1E3) + PATCH. + */ +PROTOBUF_C__API +uint32_t +protobuf_c_version_number(void); + +/** + * The version of the protobuf-c headers, represented as a string using the same + * format as protobuf_c_version(). + */ +#define PROTOBUF_C_VERSION "1.3.2" + +/** + * The version of the protobuf-c headers, represented as an integer using the + * same format as protobuf_c_version_number(). + */ +#define PROTOBUF_C_VERSION_NUMBER 1003002 + +/** + * The minimum protoc-c version which works with the current version of the + * protobuf-c headers. + */ +#define PROTOBUF_C_MIN_COMPILER_VERSION 1000000 + +/** + * Look up a `ProtobufCEnumValue` from a `ProtobufCEnumDescriptor` by name. + * + * \param desc + * The `ProtobufCEnumDescriptor` object. + * \param name + * The `name` field from the corresponding `ProtobufCEnumValue` object to + * match. + * \return + * A `ProtobufCEnumValue` object. + * \retval NULL + * If not found or if the optimize_for = CODE_SIZE option was set. + */ +PROTOBUF_C__API +const ProtobufCEnumValue * +protobuf_c_enum_descriptor_get_value_by_name( + const ProtobufCEnumDescriptor *desc, + const char *name); + +/** + * Look up a `ProtobufCEnumValue` from a `ProtobufCEnumDescriptor` by numeric + * value. + * + * \param desc + * The `ProtobufCEnumDescriptor` object. + * \param value + * The `value` field from the corresponding `ProtobufCEnumValue` object to + * match. + * + * \return + * A `ProtobufCEnumValue` object. + * \retval NULL + * If not found. + */ +PROTOBUF_C__API +const ProtobufCEnumValue * +protobuf_c_enum_descriptor_get_value( + const ProtobufCEnumDescriptor *desc, + int value); + +/** + * Look up a `ProtobufCFieldDescriptor` from a `ProtobufCMessageDescriptor` by + * the name of the field. + * + * \param desc + * The `ProtobufCMessageDescriptor` object. + * \param name + * The name of the field. + * \return + * A `ProtobufCFieldDescriptor` object. + * \retval NULL + * If not found or if the optimize_for = CODE_SIZE option was set. + */ +PROTOBUF_C__API +const ProtobufCFieldDescriptor * +protobuf_c_message_descriptor_get_field_by_name( + const ProtobufCMessageDescriptor *desc, + const char *name); + +/** + * Look up a `ProtobufCFieldDescriptor` from a `ProtobufCMessageDescriptor` by + * the tag value of the field. + * + * \param desc + * The `ProtobufCMessageDescriptor` object. + * \param value + * The tag value of the field. + * \return + * A `ProtobufCFieldDescriptor` object. + * \retval NULL + * If not found. + */ +PROTOBUF_C__API +const ProtobufCFieldDescriptor * +protobuf_c_message_descriptor_get_field( + const ProtobufCMessageDescriptor *desc, + unsigned value); + +/** + * Determine the number of bytes required to store the serialised message. + * + * \param message + * The message object to serialise. + * \return + * Number of bytes. + */ +PROTOBUF_C__API +size_t +protobuf_c_message_get_packed_size(const ProtobufCMessage *message); + +/** + * Serialise a message from its in-memory representation. + * + * This function stores the serialised bytes of the message in a pre-allocated + * buffer. + * + * \param message + * The message object to serialise. + * \param[out] out + * Buffer to store the bytes of the serialised message. This buffer must + * have enough space to store the packed message. Use + * protobuf_c_message_get_packed_size() to determine the number of bytes + * required. + * \return + * Number of bytes stored in `out`. + */ +PROTOBUF_C__API +size_t +protobuf_c_message_pack(const ProtobufCMessage *message, uint8_t *out); + +/** + * Serialise a message from its in-memory representation to a virtual buffer. + * + * This function calls the `append` method of a `ProtobufCBuffer` object to + * consume the bytes generated by the serialiser. + * + * \param message + * The message object to serialise. + * \param buffer + * The virtual buffer object. + * \return + * Number of bytes passed to the virtual buffer. + */ +PROTOBUF_C__API +size_t +protobuf_c_message_pack_to_buffer( + const ProtobufCMessage *message, + ProtobufCBuffer *buffer); + +/** + * Unpack a serialised message into an in-memory representation. + * + * \param descriptor + * The message descriptor. + * \param allocator + * `ProtobufCAllocator` to use for memory allocation. May be NULL to + * specify the default allocator. + * \param len + * Length in bytes of the serialised message. + * \param data + * Pointer to the serialised message. + * \return + * An unpacked message object. + * \retval NULL + * If an error occurred during unpacking. + */ +PROTOBUF_C__API +ProtobufCMessage * +protobuf_c_message_unpack( + const ProtobufCMessageDescriptor *descriptor, + ProtobufCAllocator *allocator, + size_t len, + const uint8_t *data); + +/** + * Free an unpacked message object. + * + * This function should be used to deallocate the memory used by a call to + * protobuf_c_message_unpack(). + * + * \param message + * The message object to free. May be NULL. + * \param allocator + * `ProtobufCAllocator` to use for memory deallocation. May be NULL to + * specify the default allocator. + */ +PROTOBUF_C__API +void +protobuf_c_message_free_unpacked( + ProtobufCMessage *message, + ProtobufCAllocator *allocator); + +/** + * Check the validity of a message object. + * + * Makes sure all required fields (`PROTOBUF_C_LABEL_REQUIRED`) are present. + * Recursively checks nested messages. + * + * \retval TRUE + * Message is valid. + * \retval FALSE + * Message is invalid. + */ +PROTOBUF_C__API +protobuf_c_boolean +protobuf_c_message_check(const ProtobufCMessage *); + +/** Message initialiser. */ +#define PROTOBUF_C_MESSAGE_INIT(descriptor) { descriptor, 0, NULL } + +/** + * Initialise a message object from a message descriptor. + * + * \param descriptor + * Message descriptor. + * \param message + * Allocated block of memory of size `descriptor->sizeof_message`. + */ +PROTOBUF_C__API +void +protobuf_c_message_init( + const ProtobufCMessageDescriptor *descriptor, + void *message); + +/** + * Free a service. + * + * \param service + * The service object to free. + */ +PROTOBUF_C__API +void +protobuf_c_service_destroy(ProtobufCService *service); + +/** + * Look up a `ProtobufCMethodDescriptor` by name. + * + * \param desc + * Service descriptor. + * \param name + * Name of the method. + * + * \return + * A `ProtobufCMethodDescriptor` object. + * \retval NULL + * If not found or if the optimize_for = CODE_SIZE option was set. + */ +PROTOBUF_C__API +const ProtobufCMethodDescriptor * +protobuf_c_service_descriptor_get_method_by_name( + const ProtobufCServiceDescriptor *desc, + const char *name); + +/** + * Initialise a `ProtobufCBufferSimple` object. + */ +#define PROTOBUF_C_BUFFER_SIMPLE_INIT(array_of_bytes) \ +{ \ + { protobuf_c_buffer_simple_append }, \ + sizeof(array_of_bytes), \ + 0, \ + (array_of_bytes), \ + 0, \ + NULL \ +} + +/** + * Clear a `ProtobufCBufferSimple` object, freeing any allocated memory. + */ +#define PROTOBUF_C_BUFFER_SIMPLE_CLEAR(simp_buf) \ +do { \ + if ((simp_buf)->must_free_data) { \ + if ((simp_buf)->allocator != NULL) \ + (simp_buf)->allocator->free( \ + (simp_buf)->allocator, \ + (simp_buf)->data); \ + else \ + free((simp_buf)->data); \ + } \ +} while (0) + +/** + * The `append` method for `ProtobufCBufferSimple`. + * + * \param buffer + * The buffer object to append to. Must actually be a + * `ProtobufCBufferSimple` object. + * \param len + * Number of bytes in `data`. + * \param data + * Data to append. + */ +PROTOBUF_C__API +void +protobuf_c_buffer_simple_append( + ProtobufCBuffer *buffer, + size_t len, + const unsigned char *data); + +PROTOBUF_C__API +void +protobuf_c_service_generated_init( + ProtobufCService *service, + const ProtobufCServiceDescriptor *descriptor, + ProtobufCServiceDestroy destroy); + +PROTOBUF_C__API +void +protobuf_c_service_invoke_internal( + ProtobufCService *service, + unsigned method_index, + const ProtobufCMessage *input, + ProtobufCClosure closure, + void *closure_data); + +/**@}*/ + +PROTOBUF_C__END_DECLS + +#endif /* PROTOBUF_C_H */ diff --git a/submodules/AppCenter-sdk/Vendor/PLCrashReporter/Documentation/Crash Log Format/IEEEtrantools.sty b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/Documentation/Crash Log Format/IEEEtrantools.sty new file mode 100644 index 0000000000..76ce3930c0 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/Documentation/Crash Log Format/IEEEtrantools.sty @@ -0,0 +1,1858 @@ +%% +%% IEEEtrantools.sty 2007/01/11 version V1.2 +%% +%% +%% This package provides several popular and unique commands from the +%% IEEEtran.cls class (version 1.7) file. +%% +%% The provided commands include \IEEEPARstart, \IEEEitemize, \IEEEenumerate, +%% \IEEEdescription as well as the \IEEEeqnarray, \IEEEeqnarraybox family +%% of commands including support commands such as \IEEEstrut. +%% Also provides the \bstctlcite command for the control entry types of +%% IEEEtran.bst V1.00 and later. +%% +%% IEEEtrantools.sty should not be used with IEEEtran.cls. +%% +%% Support sites: +%% http://www.michaelshell.org/tex/ieeetran/ +%% http://www.ctan.org/tex-archive/macros/latex/contrib/IEEEtran/ +%% +%% +%% Copyright (c) 2002-2007 by Michael Shell +%% See: http://www.michaelshell.org/ +%% for current contact information. +%% +%%************************************************************************* +%% Legal Notice: +%% This code is offered as-is without any warranty either expressed or +%% implied; without even the implied warranty of MERCHANTABILITY or +%% FITNESS FOR A PARTICULAR PURPOSE! +%% User assumes all risk. +%% In no event shall IEEE or any contributor to this code be liable for +%% any damages or losses, including, but not limited to, incidental, +%% consequential, or any other damages, resulting from the use or misuse +%% of any information contained here. +%% +%% All comments are the opinions of their respective authors and are not +%% necessarily endorsed by the IEEE. +%% +%% This work is distributed under the LaTeX Project Public License (LPPL) +%% ( http://www.latex-project.org/ ) version 1.3, and may be freely used, +%% distributed and modified. A copy of the LPPL, version 1.3, is included +%% in the base LaTeX documentation of all distributions of LaTeX released +%% 2003/12/01 or later. +%% Retain all contribution notices and credits. +%% ** Modified files should be clearly indicated as such, including ** +%% ** renaming them and changing author support contact information. ** +%% +%% File list of work: IEEEtrantools.sty, IEEEtrantools_doc.txt +%%************************************************************************* +%% +%% +%% +%% Available package options (e.g., \usepackage[retainorgcmds]{IEEEtrantools} +%% +%% retainorgcmds +%% Prevents IEEEtrantools from overriding existing LaTeX commands. +%% Currently, the only effect is to preserve the original definitions +%% of itemize, enumerate and description. The IEEEtran versions are +%% always available as IEEEitemize, IEEEenumerate and IEEEdescription. +%% +%%******* +% 1/2007 V1.2 (V1.7 of IEEEtran.cls) changes: +% +% 1) Several commands and environments have depreciated in favor of +% replacements with IEEE prefixes to better avoid potential future name +% clashes with other packages. Legacy code retained to allow +% use of the obsolete forms, but with an warning message to the +% console during compilation: +% \IEEEPARstart +% For IED lists: +% \IEEEiedlabeljustifyc, \IEEEiedlabeljustifyl, \IEEEiedlabeljustifyr, +% \IEEEnocalcleftmargin, \IEEElabelindent, \IEEEsetlabelwidth, +% \IEEEusemathlabelsep +% +% 2) These commands/lengths now require the IEEE prefix and do not have +% legacy support: \IEEEnormaljot. +% For IED lists: \ifIEEEnocalcleftmargin, \ifIEEEnolabelindentfactor, +% \IEEEiedlistdecl, \IEEElabelindentfactor +% +% 3) \normalsizebaselineskip no longer provided. +% +% 4) New \IEEEPARstart controls: +% \IEEEPARstartHEIGHTTEXT, \IEEEPARstartFONTSTYLE, \IEEEPARstartCAPSTYLE, +% \IEEEPARstartWORDFONTSTYLE, \IEEEPARstartWORDCAPSTYLE, +% \IEEEPARstartHOFFSET, \IEEEPARstartITLCORRECT +% and the (output) length \IEEEPARstartletwidth. +% +% 5) Provide for an optional argument to \bstctlcite to provide a way to +% specify a different aux file. +% +% +% 11/2002 V1.1 (V1.6b of IEEEtran.cls) changes: +% +% 1) In addition to the IEEE IED lists, the original LaTeX IED style list +% environments are now preserved as LaTeXitemize, LaTeXenumerate, and +% LaTeXdescription. Also, users can now redefine \makelabel within +% IEEE IED list controls. There may be some use for these in specialized +% applications. Thanks to Eli Barzilay for suggesting this feature. +% +%%********************************************************************** + + +\ProvidesPackage{IEEEtrantools}[2007/01/11 V1.2 by Michael Shell] +\typeout{-- See the "IEEEtrantools_doc.txt" manual for usage information.} +\typeout{-- http://www.michaelshell.org/tex/ieeetran/tools/} +\NeedsTeXFormat{LaTeX2e} + + +% If IEEEtran.cls is detected, error. +{\@ifundefined{IEEEtransversionmajor}{\relax}{% +\PackageError{IEEEtrantools}{IEEEtrantools is not for use with the\MessageBreak + IEEEtran class}% + {Do not load IEEEtrantools - you don't need it.}% +}} + + +% define new needed flags to indicate document options +% and set a few "failsafe" defaults +\newif\if@IEEETOOLSretainorgcmds +\global\@IEEETOOLSretainorgcmdsfalse + + + +% IEEEtran class scratch pad registers +% dimen +\newdimen\@IEEEtrantmpdimenA +\newdimen\@IEEEtrantmpdimenB +% count +\newcount\@IEEEtrantmpcountA +\newcount\@IEEEtrantmpcountB +% token list +\newtoks\@IEEEtrantmptoksA + + +% declare the options +\DeclareOption{retainorgcmds}{\@IEEETOOLSretainorgcmdstrue} + +% get and process any supplied options +\ProcessOptions + + + +% store the nominal value of jot +\newdimen\IEEEnormaljot +\IEEEnormaljot\jot\relax + + +% Itemize, Enumerate and Description (IED) List Controls +% *************************** +% +% +% IEEE seems to use at least two different values by +% which ITEMIZED list labels are indented to the right +% For The Journal of Lightwave Technology (JLT) and The Journal +% on Selected Areas in Communications (JSAC), they tend to use +% an indention equal to \parindent. For Transactions on Communications +% they tend to indent ITEMIZED lists a little more--- 1.3\parindent. +% We'll provide both values here for you so that you can choose +% which one you like in your document using a command such as: +% setlength{\IEEEilabelindent}{\IEEEilabelindentB} +\newdimen\IEEEilabelindentA +\IEEEilabelindentA \parindent + +\newdimen\IEEEilabelindentB +\IEEEilabelindentB 1.3\parindent +% However, we'll default to using \parindent +% which makes more sense to me +\newdimen\IEEEilabelindent +\IEEEilabelindent \IEEEilabelindentA + + +% This controls the default amount the enumerated list labels +% are indented to the right. +% Normally, this is the same as the paragraph indention +\newdimen\IEEEelabelindent +\IEEEelabelindent \parindent + +% This controls the default amount the description list labels +% are indented to the right. +% Normally, this is the same as the paragraph indention +\newdimen\IEEEdlabelindent +\IEEEdlabelindent \parindent + +% This is the value actually used within the IED lists. +% The IED environments automatically set its value to +% one of the three values above, so global changes do +% not have any effect +\newdimen\IEEElabelindent +\IEEElabelindent \parindent + +% The actual amount labels will be indented is +% \IEEElabelindent multiplied by the factor below +% corresponding to the level of nesting depth +% This provides a means by which the user can +% alter the effective \IEEElabelindent for deeper +% levels +% There may not be such a thing as correct "standard IEEE" +% values. What IEEE actually does may depend on the specific +% circumstances. +% The first list level almost always has full indention. +% The second levels I've seen have only 75% of the normal indentation +% Three level or greater nestings are very rare. I am guessing +% that they don't use any indentation. +\def\IEEElabelindentfactori{1.0} % almost always one +\def\IEEElabelindentfactorii{0.75} % 0.0 or 1.0 may be used in some cases +\def\IEEElabelindentfactoriii{0.0} % 0.75? 0.5? 0.0? +\def\IEEElabelindentfactoriv{0.0} +\def\IEEElabelindentfactorv{0.0} +\def\IEEElabelindentfactorvi{0.0} + +% value actually used within IED lists, it is auto +% set to one of the 6 values above +% global changes here have no effect +\def\IEEElabelindentfactor{1.0} + +% This controls the default spacing between the end of the IED +% list labels and the list text, when normal text is used for +% the labels. +\newdimen\IEEEiednormlabelsep +\IEEEiednormlabelsep 0.6em + +% This controls the default spacing between the end of the IED +% list labels and the list text, when math symbols are used for +% the labels (nomenclature lists). IEEE usually increases the +% spacing in these cases +\newdimen\IEEEiedmathlabelsep +\IEEEiedmathlabelsep 1.2em + +% This controls the extra vertical separation put above and +% below each IED list. IEEE usually puts a little extra spacing +% around each list. However, this spacing is barely noticeable. +\newskip\IEEEiedtopsep +\IEEEiedtopsep 2pt plus 1pt minus 1pt + + +% This command is executed within each IED list environment +% at the beginning of the list. You can use this to set the +% parameters for some/all your IED list(s) without disturbing +% global parameters that affect things other than lists. +% i.e., renewcommand{\IEEEiedlistdecl}{\setlength{\labelsep}{5em}} +% will alter the \labelsep for the next list(s) until +% \IEEEiedlistdecl is redefined. +\def\IEEEiedlistdecl{\relax} + +% This command provides an easy way to set \leftmargin based +% on the \labelwidth, \labelsep and the argument \IEEElabelindent +% Usage: \IEEEcalcleftmargin{width-to-indent-the-label} +% output is in the \leftmargin variable, i.e., effectively: +% \leftmargin = argument + \labelwidth + \labelsep +% Note controlled spacing here, shield end of lines with % +\def\IEEEcalcleftmargin#1{\setlength{\leftmargin}{#1}% +\addtolength{\leftmargin}{\labelwidth}% +\addtolength{\leftmargin}{\labelsep}} + +% This command provides an easy way to set \labelwidth to the +% width of the given text. It is the same as +% \settowidth{\labelwidth}{label-text} +% and useful as a shorter alternative. +% Typically used to set \labelwidth to be the width +% of the longest label in the list +\def\IEEEsetlabelwidth#1{\settowidth{\labelwidth}{#1}} + +% When this command is executed, IED lists will use the +% IEEEiedmathlabelsep label separation rather than the normal +% spacing. To have an effect, this command must be executed via +% the \IEEEiedlistdecl or within the option of the IED list +% environments. +\def\IEEEusemathlabelsep{\setlength{\labelsep}{\IEEEiedmathlabelsep}} + +% A flag which controls whether the IED lists automatically +% calculate \leftmargin from \IEEElabelindent, \labelwidth and \labelsep +% Useful if you want to specify your own \leftmargin +% This flag must be set (\IEEEnocalcleftmargintrue or \IEEEnocalcleftmarginfalse) +% via the \IEEEiedlistdecl or within the option of the IED list +% environments to have an effect. +\newif\ifIEEEnocalcleftmargin +\IEEEnocalcleftmarginfalse + +% A flag which controls whether \IEEElabelindent is multiplied by +% the \IEEElabelindentfactor for each list level. +% This flag must be set via the \IEEEiedlistdecl or within the option +% of the IED list environments to have an effect. +\newif\ifIEEEnolabelindentfactor +\IEEEnolabelindentfactorfalse + + +% internal variable to indicate type of IED label +% justification +% 0 - left; 1 - center; 2 - right +\def\@IEEEiedjustify{0} + + + +% commands to allow the user to control IED +% label justifications. Use these commands within +% the IED environment option or in the \IEEEiedlistdecl +% Note that changing the normal list justifications +% is nonstandard and IEEE may not like it if you do so! +% I include these commands as they may be helpful to +% those who are using these enhanced list controls for +% other non-IEEE related LaTeX work. +% itemize and enumerate automatically default to right +% justification, description defaults to left. +\def\IEEEiedlabeljustifyl{\def\@IEEEiedjustify{0}}%left +\def\IEEEiedlabeljustifyc{\def\@IEEEiedjustify{1}}%center +\def\IEEEiedlabeljustifyr{\def\@IEEEiedjustify{2}}%right + + + + +% commands to save to and restore from the list parameter copies +% this allows us to set all the list parameters within +% the list_decl and prevent \list (and its \@list) +% from overriding any of our parameters +% V1.6 use \edefs instead of dimen's to conserve dimen registers +% Note controlled spacing here, shield end of lines with % +\def\@IEEEsavelistparams{\edef\@IEEEiedtopsep{\the\topsep}% +\edef\@IEEEiedlabelwidth{\the\labelwidth}% +\edef\@IEEEiedlabelsep{\the\labelsep}% +\edef\@IEEEiedleftmargin{\the\leftmargin}% +\edef\@IEEEiedpartopsep{\the\partopsep}% +\edef\@IEEEiedparsep{\the\parsep}% +\edef\@IEEEieditemsep{\the\itemsep}% +\edef\@IEEEiedrightmargin{\the\rightmargin}% +\edef\@IEEEiedlistparindent{\the\listparindent}% +\edef\@IEEEieditemindent{\the\itemindent}} + +% Note controlled spacing here +\def\@IEEErestorelistparams{\topsep\@IEEEiedtopsep\relax% +\labelwidth\@IEEEiedlabelwidth\relax% +\labelsep\@IEEEiedlabelsep\relax% +\leftmargin\@IEEEiedleftmargin\relax% +\partopsep\@IEEEiedpartopsep\relax% +\parsep\@IEEEiedparsep\relax% +\itemsep\@IEEEieditemsep\relax% +\rightmargin\@IEEEiedrightmargin\relax% +\listparindent\@IEEEiedlistparindent\relax% +\itemindent\@IEEEieditemindent\relax} + + +% v1.6b provide original LaTeX IED list environments +% note that latex.ltx defines \itemize and \enumerate, but not \description +% which must be created by the base classes +% save original LaTeX itemize and enumerate +\let\LaTeXitemize\itemize +\let\endLaTeXitemize\enditemize +\let\LaTeXenumerate\enumerate +\let\endLaTeXenumerate\endenumerate +% base class should define \description +\let\LaTeXdescription\description +\let\endLaTeXdescription\enddescription + + +% override LaTeX's default IED lists, unless the user requested they be retained +\if@IEEETOOLSretainorgcmds\relax\else +\def\itemize{\@IEEEitemize} +\def\enditemize{\@endIEEEitemize} +\def\enumerate{\@IEEEenumerate} +\def\endenumerate{\@endIEEEenumerate} +\def\description{\@IEEEdescription} +\def\enddescription{\@endIEEEdescription} +\fi + + +% provide the user with the IEEE IED commands +\def\IEEEitemize{\@IEEEitemize} +\def\endIEEEitemize{\@endIEEEitemize} +\def\IEEEenumerate{\@IEEEenumerate} +\def\endIEEEenumerate{\@endIEEEenumerate} +\def\IEEEdescription{\@IEEEdescription} +\def\endIEEEdescription{\@endIEEEdescription} + + +% V1.6 we want to keep the IEEEtran IED list definitions as our own internal +% commands so they are protected against redefinition +\def\@IEEEitemize{\@ifnextchar[{\@@IEEEitemize}{\@@IEEEitemize[\relax]}} +\def\@IEEEenumerate{\@ifnextchar[{\@@IEEEenumerate}{\@@IEEEenumerate[\relax]}} +\def\@IEEEdescription{\@ifnextchar[{\@@IEEEdescription}{\@@IEEEdescription[\relax]}} +\def\@endIEEEitemize{\endlist} +\def\@endIEEEenumerate{\endlist} +\def\@endIEEEdescription{\endlist} + + +% DO NOT ALLOW BLANK LINES TO BE IN THESE IED ENVIRONMENTS +% AS THIS WILL FORCE NEW PARAGRAPHS AFTER THE IED LISTS +% IEEEtran itemized list MDS 1/2001 +% Note controlled spacing here, shield end of lines with % +\def\@@IEEEitemize[#1]{% + \ifnum\@itemdepth>3\relax\@toodeep\else% + \ifnum\@listdepth>5\relax\@toodeep\else% + \advance\@itemdepth\@ne% + \edef\@itemitem{labelitem\romannumeral\the\@itemdepth}% + % get the labelindentfactor for this level + \advance\@listdepth\@ne% we need to know what the level WILL be + \edef\IEEElabelindentfactor{\csname IEEElabelindentfactor\romannumeral\the\@listdepth\endcsname}% + \advance\@listdepth-\@ne% undo our increment + \def\@IEEEiedjustify{2}% right justified labels are default + % set other defaults + \IEEEnocalcleftmarginfalse% + \IEEEnolabelindentfactorfalse% + \topsep\IEEEiedtopsep% + \IEEElabelindent\IEEEilabelindent% + \labelsep\IEEEiednormlabelsep% + \partopsep 0ex% + \parsep 0ex% + \itemsep 0ex% + \rightmargin 0em% + \listparindent 0em% + \itemindent 0em% + % calculate the label width + % the user can override this later if + % they specified a \labelwidth + \settowidth{\labelwidth}{\csname labelitem\romannumeral\the\@itemdepth\endcsname}% + \@IEEEsavelistparams% save our list parameters + \list{\csname\@itemitem\endcsname}{% + \@IEEErestorelistparams% override any list{} changes + % to our globals + \let\makelabel\@IEEEiedmakelabel% v1.6b setup \makelabel + \IEEEiedlistdecl% let user alter parameters + #1\relax% + % If the user has requested not to use the + % labelindent factor, don't revise \labelindent + \ifIEEEnolabelindentfactor\relax% + \else\IEEElabelindent=\IEEElabelindentfactor\labelindent% + \fi% + % Unless the user has requested otherwise, + % calculate our left margin based + % on \IEEElabelindent, \labelwidth and + % \labelsep + \ifIEEEnocalcleftmargin\relax% + \else\IEEEcalcleftmargin{\IEEElabelindent}% + \fi}\fi\fi}% + + +% DO NOT ALLOW BLANK LINES TO BE IN THESE IED ENVIRONMENTS +% AS THIS WILL FORCE NEW PARAGRAPHS AFTER THE IED LISTS +% IEEEtran enumerate list MDS 1/2001 +% Note controlled spacing here, shield end of lines with % +\def\@@IEEEenumerate[#1]{% + \ifnum\@enumdepth>3\relax\@toodeep\else% + \ifnum\@listdepth>5\relax\@toodeep\else% + \advance\@enumdepth\@ne% + \edef\@enumctr{enum\romannumeral\the\@enumdepth}% + % get the labelindentfactor for this level + \advance\@listdepth\@ne% we need to know what the level WILL be + \edef\IEEElabelindentfactor{\csname IEEElabelindentfactor\romannumeral\the\@listdepth\endcsname}% + \advance\@listdepth-\@ne% undo our increment + \def\@IEEEiedjustify{2}% right justified labels are default + % set other defaults + \IEEEnocalcleftmarginfalse% + \IEEEnolabelindentfactorfalse% + \topsep\IEEEiedtopsep% + \IEEElabelindent\IEEEelabelindent% + \labelsep\IEEEiednormlabelsep% + \partopsep 0ex% + \parsep 0ex% + \itemsep 0ex% + \rightmargin 0em% + \listparindent 0em% + \itemindent 0em% + % calculate the label width + % We'll set it to the width suitable for all labels using + % normalfont 1) to 9) + % The user can override this later + \settowidth{\labelwidth}{9)}% + \@IEEEsavelistparams% save our list parameters + \list{\csname label\@enumctr\endcsname}{\usecounter{\@enumctr}% + \@IEEErestorelistparams% override any list{} changes + % to our globals + \let\makelabel\@IEEEiedmakelabel% v1.6b setup \makelabel + \IEEEiedlistdecl% let user alter parameters + #1\relax% + % If the user has requested not to use the + % IEEElabelindent factor, don't revise \IEEElabelindent + \ifIEEEnolabelindentfactor\relax% + \else\IEEElabelindent=\IEEElabelindentfactor\IEEElabelindent% + \fi% + % Unless the user has requested otherwise, + % calculate our left margin based + % on \IEEElabelindent, \labelwidth and + % \labelsep + \ifIEEEnocalcleftmargin\relax% + \else\IEEEcalcleftmargin{\IEEElabelindent}% + \fi}\fi\fi}% + + +% DO NOT ALLOW BLANK LINES TO BE IN THESE IED ENVIRONMENTS +% AS THIS WILL FORCE NEW PARAGRAPHS AFTER THE IED LISTS +% IEEEtran description list MDS 1/2001 +% Note controlled spacing here, shield end of lines with % +\def\@@IEEEdescription[#1]{% + \ifnum\@listdepth>5\relax\@toodeep\else% + % get the labelindentfactor for this level + \advance\@listdepth\@ne% we need to know what the level WILL be + \edef\IEEElabelindentfactor{\csname IEEElabelindentfactor\romannumeral\the\@listdepth\endcsname}% + \advance\@listdepth-\@ne% undo our increment + \def\@IEEEiedjustify{0}% left justified labels are default + % set other defaults + \IEEEnocalcleftmarginfalse% + \IEEEnolabelindentfactorfalse% + \topsep\IEEEiedtopsep% + \IEEElabelindent\IEEEdlabelindent% + % assume normal labelsep + \labelsep\IEEEiednormlabelsep% + \partopsep 0ex% + \parsep 0ex% + \itemsep 0ex% + \rightmargin 0em% + \listparindent 0em% + \itemindent 0em% + % Bogus label width in case the user forgets + % to set it. + % TIP: If you want to see what a variable's width is you + % can use the TeX command \showthe\width-variable to + % display it on the screen during compilation + % (This might be helpful to know when you need to find out + % which label is the widest) + \settowidth{\labelwidth}{Hello}% + \@IEEEsavelistparams% save our list parameters + \list{}{\@IEEErestorelistparams% override any list{} changes + % to our globals + \let\makelabel\@IEEEiedmakelabel% v1.6b setup \makelabel + \IEEEiedlistdecl% let user alter parameters + #1\relax% + % If the user has requested not to use the + % labelindent factor, don't revise \IEEElabelindent + \ifIEEEnolabelindentfactor\relax% + \else\IEEElabelindent=\IEEElabelindentfactor\IEEElabelindent% + \fi% + % Unless the user has requested otherwise, + % calculate our left margin based + % on \IEEElabelindent, \labelwidth and + % \labelsep + \ifIEEEnocalcleftmargin\relax% + \else\IEEEcalcleftmargin{\IEEElabelindent}\relax% + \fi}\fi} + +% v1.6b we use one makelabel that does justification as needed. +\def\@IEEEiedmakelabel#1{\relax\if\@IEEEiedjustify 0\relax +\makebox[\labelwidth][l]{\normalfont #1}\else +\if\@IEEEiedjustify 1\relax +\makebox[\labelwidth][c]{\normalfont #1}\else +\makebox[\labelwidth][r]{\normalfont #1}\fi\fi} + + + + + + + + + +% used only by IEEEtran's IEEEeqnarray as other packages may +% have their own, different, implementations +\newcounter{IEEEsubequation}[equation] + +% e.g., "1a" (used only by IEEEtran's IEEEeqnarray) +\def\theIEEEsubequation{\theequation\alph{IEEEsubequation}} +% just like LaTeX2e's \@eqnnum +\def\theequationdis{{\normalfont \normalcolor (\theequation)}}% (1) +% IEEEsubequation used only by IEEEtran's IEEEeqnarray +\def\theIEEEsubequationdis{{\normalfont \normalcolor (\theIEEEsubequation)}}% (1a) + + + + +%% +%% START OF IEEEeqnarry DEFINITIONS +%% +%% Inspired by the concepts, examples, and previous works of LaTeX +%% coders and developers such as Donald Arseneau, Fred Bartlett, +%% David Carlisle, Tony Liu, Frank Mittelbach, Piet van Oostrum, +%% Roland Winkler and Mark Wooding. +%% I don't make the claim that my work here is even near their calibre. ;) + + +% hook to allow easy changeover to IEEEtran.cls/tools.sty error reporting +\def\@IEEEclspkgerror{\PackageError{IEEEtran}} + +\newif\if@IEEEeqnarraystarform% flag to indicate if the environment was called as the star form +\@IEEEeqnarraystarformfalse + +\newif\if@advanceIEEEeqncolcnt% tracks if the environment should advance the col counter +% allows a way to make an \IEEEeqnarraybox that can be used within an \IEEEeqnarray +% used by IEEEeqnarraymulticol so that it can work properly in both +\@advanceIEEEeqncolcnttrue + +\newcount\@IEEEeqnnumcols % tracks how many IEEEeqnarray cols are defined +\newcount\@IEEEeqncolcnt % tracks how many IEEEeqnarray cols the user actually used + + +% The default math style used by the columns +\def\IEEEeqnarraymathstyle{\displaystyle} +% The default text style used by the columns +% default to using the current font +\def\IEEEeqnarraytextstyle{\relax} + +% like the iedlistdecl but for \IEEEeqnarray +\def\IEEEeqnarraydecl{\relax} +\def\IEEEeqnarrayboxdecl{\relax} + +% \yesnumber is the opposite of \nonumber +% a novel concept with the same def as the equationarray package +% However, we give IEEE versions too since some LaTeX packages such as +% the MDWtools mathenv.sty redefine \nonumber to something else. +\providecommand{\yesnumber}{\global\@eqnswtrue} +\def\IEEEyesnumber{\global\@eqnswtrue} +\def\IEEEnonumber{\global\@eqnswfalse} + + +\def\IEEEyessubnumber{\global\@IEEEissubequationtrue\global\@eqnswtrue% +\if@IEEEeqnarrayISinner% only do something inside an IEEEeqnarray +\if@IEEElastlinewassubequation\addtocounter{equation}{-1}\else\setcounter{IEEEsubequation}{1}\fi% +\def\@currentlabel{\p@IEEEsubequation\theIEEEsubequation}\fi} + +% flag to indicate that an equation is a sub equation +\newif\if@IEEEissubequation% +\@IEEEissubequationfalse + +% allows users to "push away" equations that get too close to the equation numbers +\def\IEEEeqnarraynumspace{\hphantom{\if@IEEEissubequation\theIEEEsubequationdis\else\theequationdis\fi}} + +% provides a way to span multiple columns within IEEEeqnarray environments +% will consider \if@advanceIEEEeqncolcnt before globally advancing the +% column counter - so as to work within \IEEEeqnarraybox +% usage: \IEEEeqnarraymulticol{number cols. to span}{col type}{cell text} +\long\def\IEEEeqnarraymulticol#1#2#3{\multispan{#1}% +% check if column is defined +\relax\expandafter\ifx\csname @IEEEeqnarraycolDEF#2\endcsname\@IEEEeqnarraycolisdefined% +\csname @IEEEeqnarraycolPRE#2\endcsname#3\relax\relax\relax\relax\relax% +\relax\relax\relax\relax\relax\csname @IEEEeqnarraycolPOST#2\endcsname% +\else% if not, error and use default type +\@IEEEclspkgerror{Invalid column type "#2" in \string\IEEEeqnarraymulticol.\MessageBreak +Using a default centering column instead}% +{You must define IEEEeqnarray column types before use.}% +\csname @IEEEeqnarraycolPRE@IEEEdefault\endcsname#3\relax\relax\relax\relax\relax% +\relax\relax\relax\relax\relax\csname @IEEEeqnarraycolPOST@IEEEdefault\endcsname% +\fi% +% advance column counter only if the IEEEeqnarray environment wants it +\if@advanceIEEEeqncolcnt\global\advance\@IEEEeqncolcnt by #1\relax\fi} + +% like \omit, but maintains track of the column counter for \IEEEeqnarray +\def\IEEEeqnarrayomit{\omit\if@advanceIEEEeqncolcnt\global\advance\@IEEEeqncolcnt by 1\relax\fi} + + +% provides a way to define a letter referenced column type +% usage: \IEEEeqnarraydefcol{col. type letter/name}{pre insertion text}{post insertion text} +\def\IEEEeqnarraydefcol#1#2#3{\expandafter\def\csname @IEEEeqnarraycolPRE#1\endcsname{#2}% +\expandafter\def\csname @IEEEeqnarraycolPOST#1\endcsname{#3}% +\expandafter\def\csname @IEEEeqnarraycolDEF#1\endcsname{1}} + + +% provides a way to define a numerically referenced inter-column glue types +% usage: \IEEEeqnarraydefcolsep{col. glue number}{glue definition} +\def\IEEEeqnarraydefcolsep#1#2{\expandafter\def\csname @IEEEeqnarraycolSEP\romannumeral #1\endcsname{#2}% +\expandafter\def\csname @IEEEeqnarraycolSEPDEF\romannumeral #1\endcsname{1}} + + +\def\@IEEEeqnarraycolisdefined{1}% just a macro for 1, used for checking undefined column types + + +% expands and appends the given argument to the \@IEEEtrantmptoksA token list +% used to build up the \halign preamble +\def\@IEEEappendtoksA#1{\edef\@@IEEEappendtoksA{\@IEEEtrantmptoksA={\the\@IEEEtrantmptoksA #1}}% +\@@IEEEappendtoksA} + +% also appends to \@IEEEtrantmptoksA, but does not expand the argument +% uses \toks8 as a scratchpad register +\def\@IEEEappendNOEXPANDtoksA#1{\toks8={#1}% +\edef\@@IEEEappendNOEXPANDtoksA{\@IEEEtrantmptoksA={\the\@IEEEtrantmptoksA\the\toks8}}% +\@@IEEEappendNOEXPANDtoksA} + +% define some common column types for the user +% math +\IEEEeqnarraydefcol{l}{$\IEEEeqnarraymathstyle}{$\hfil} +\IEEEeqnarraydefcol{c}{\hfil$\IEEEeqnarraymathstyle}{$\hfil} +\IEEEeqnarraydefcol{r}{\hfil$\IEEEeqnarraymathstyle}{$} +\IEEEeqnarraydefcol{L}{$\IEEEeqnarraymathstyle{}}{{}$\hfil} +\IEEEeqnarraydefcol{C}{\hfil$\IEEEeqnarraymathstyle{}}{{}$\hfil} +\IEEEeqnarraydefcol{R}{\hfil$\IEEEeqnarraymathstyle{}}{{}$} +% text +\IEEEeqnarraydefcol{s}{\IEEEeqnarraytextstyle}{\hfil} +\IEEEeqnarraydefcol{t}{\hfil\IEEEeqnarraytextstyle}{\hfil} +\IEEEeqnarraydefcol{u}{\hfil\IEEEeqnarraytextstyle}{} + +% vertical rules +\IEEEeqnarraydefcol{v}{}{\vrule width\arrayrulewidth} +\IEEEeqnarraydefcol{vv}{\vrule width\arrayrulewidth\hfil}{\hfil\vrule width\arrayrulewidth} +\IEEEeqnarraydefcol{V}{}{\vrule width\arrayrulewidth\hskip\doublerulesep\vrule width\arrayrulewidth} +\IEEEeqnarraydefcol{VV}{\vrule width\arrayrulewidth\hskip\doublerulesep\vrule width\arrayrulewidth\hfil}% +{\hfil\vrule width\arrayrulewidth\hskip\doublerulesep\vrule width\arrayrulewidth} + +% horizontal rules +\IEEEeqnarraydefcol{h}{}{\leaders\hrule height\arrayrulewidth\hfil} +\IEEEeqnarraydefcol{H}{}{\leaders\vbox{\hrule width\arrayrulewidth\vskip\doublerulesep\hrule width\arrayrulewidth}\hfil} + +% plain +\IEEEeqnarraydefcol{x}{}{} +\IEEEeqnarraydefcol{X}{$}{$} + +% the default column type to use in the event a column type is not defined +\IEEEeqnarraydefcol{@IEEEdefault}{\hfil$\IEEEeqnarraymathstyle}{$\hfil} + + +% a zero tabskip (used for "-" col types) +\def\@IEEEeqnarraycolSEPzero{0pt plus 0pt minus 0pt} +% a centering tabskip (used for "+" col types) +\def\@IEEEeqnarraycolSEPcenter{1000pt plus 0pt minus 1000pt} + +% top level default tabskip glues for the start, end, and inter-column +% may be reset within environments not always at the top level, e.g., \IEEEeqnarraybox +\edef\@IEEEeqnarraycolSEPdefaultstart{\@IEEEeqnarraycolSEPcenter}% default start glue +\edef\@IEEEeqnarraycolSEPdefaultend{\@IEEEeqnarraycolSEPcenter}% default end glue +\edef\@IEEEeqnarraycolSEPdefaultmid{\@IEEEeqnarraycolSEPzero}% default inter-column glue + + + +% creates a vertical rule that extends from the bottom to the top a a cell +% Provided in case other packages redefine \vline some other way. +% usage: \IEEEeqnarrayvrule[rule thickness] +% If no argument is provided, \arrayrulewidth will be used for the rule thickness. +\newcommand\IEEEeqnarrayvrule[1][\arrayrulewidth]{\vrule\@width#1\relax} + +% creates a blank separator row +% usage: \IEEEeqnarrayseprow[separation length][font size commands] +% default is \IEEEeqnarrayseprow[0.25\normalbaselineskip][\relax] +% blank arguments inherit the default values +% uses \skip5 as a scratch register - calls \@IEEEeqnarraystrutsize which uses more scratch registers +\def\IEEEeqnarrayseprow{\relax\@ifnextchar[{\@IEEEeqnarrayseprow}{\@IEEEeqnarrayseprow[0.25\normalbaselineskip]}} +\def\@IEEEeqnarrayseprow[#1]{\relax\@ifnextchar[{\@@IEEEeqnarrayseprow[#1]}{\@@IEEEeqnarrayseprow[#1][\relax]}} +\def\@@IEEEeqnarrayseprow[#1][#2]{\def\@IEEEeqnarrayseprowARGONE{#1}% +\ifx\@IEEEeqnarrayseprowARGONE\@empty% +% get the skip value, based on the font commands +% use skip5 because \IEEEeqnarraystrutsize uses \skip0, \skip2, \skip3 +% assign within a bogus box to confine the font changes +{\setbox0=\hbox{#2\relax\global\skip5=0.25\normalbaselineskip}}% +\else% +{\setbox0=\hbox{#2\relax\global\skip5=#1}}% +\fi% +\@IEEEeqnarrayhoptolastcolumn\IEEEeqnarraystrutsize{\skip5}{0pt}[\relax]\relax} + +% creates a blank separator row, but omits all the column templates +% usage: \IEEEeqnarrayseprowcut[separation length][font size commands] +% default is \IEEEeqnarrayseprowcut[0.25\normalbaselineskip][\relax] +% blank arguments inherit the default values +% uses \skip5 as a scratch register - calls \@IEEEeqnarraystrutsize which uses more scratch registers +\def\IEEEeqnarrayseprowcut{\multispan{\@IEEEeqnnumcols}\relax% span all the cols +% advance column counter only if the IEEEeqnarray environment wants it +\if@advanceIEEEeqncolcnt\global\advance\@IEEEeqncolcnt by \@IEEEeqnnumcols\relax\fi% +\@ifnextchar[{\@IEEEeqnarrayseprowcut}{\@IEEEeqnarrayseprowcut[0.25\normalbaselineskip]}} +\def\@IEEEeqnarrayseprowcut[#1]{\relax\@ifnextchar[{\@@IEEEeqnarrayseprowcut[#1]}{\@@IEEEeqnarrayseprowcut[#1][\relax]}} +\def\@@IEEEeqnarrayseprowcut[#1][#2]{\def\@IEEEeqnarrayseprowARGONE{#1}% +\ifx\@IEEEeqnarrayseprowARGONE\@empty% +% get the skip value, based on the font commands +% use skip5 because \IEEEeqnarraystrutsize uses \skip0, \skip2, \skip3 +% assign within a bogus box to confine the font changes +{\setbox0=\hbox{#2\relax\global\skip5=0.25\normalbaselineskip}}% +\else% +{\setbox0=\hbox{#2\relax\global\skip5=#1}}% +\fi% +\IEEEeqnarraystrutsize{\skip5}{0pt}[\relax]\relax} + + + +% draws a single rule across all the columns optional +% argument determines the rule width, \arrayrulewidth is the default +% updates column counter as needed and turns off struts +% usage: \IEEEeqnarrayrulerow[rule line thickness] +\def\IEEEeqnarrayrulerow{\multispan{\@IEEEeqnnumcols}\relax% span all the cols +% advance column counter only if the IEEEeqnarray environment wants it +\if@advanceIEEEeqncolcnt\global\advance\@IEEEeqncolcnt by \@IEEEeqnnumcols\relax\fi% +\@ifnextchar[{\@IEEEeqnarrayrulerow}{\@IEEEeqnarrayrulerow[\arrayrulewidth]}} +\def\@IEEEeqnarrayrulerow[#1]{\leaders\hrule height#1\hfil\relax% put in our rule +% turn off any struts +\IEEEeqnarraystrutsize{0pt}{0pt}[\relax]\relax} + + +% draws a double rule by using a single rule row, a separator row, and then +% another single rule row +% first optional argument determines the rule thicknesses, \arrayrulewidth is the default +% second optional argument determines the rule spacing, \doublerulesep is the default +% usage: \IEEEeqnarraydblrulerow[rule line thickness][rule spacing] +\def\IEEEeqnarraydblrulerow{\multispan{\@IEEEeqnnumcols}\relax% span all the cols +% advance column counter only if the IEEEeqnarray environment wants it +\if@advanceIEEEeqncolcnt\global\advance\@IEEEeqncolcnt by \@IEEEeqnnumcols\relax\fi% +\@ifnextchar[{\@IEEEeqnarraydblrulerow}{\@IEEEeqnarraydblrulerow[\arrayrulewidth]}} +\def\@IEEEeqnarraydblrulerow[#1]{\relax\@ifnextchar[{\@@IEEEeqnarraydblrulerow[#1]}% +{\@@IEEEeqnarraydblrulerow[#1][\doublerulesep]}} +\def\@@IEEEeqnarraydblrulerow[#1][#2]{\def\@IEEEeqnarraydblrulerowARG{#1}% +% we allow the user to say \IEEEeqnarraydblrulerow[][] +\ifx\@IEEEeqnarraydblrulerowARG\@empty% +\@IEEEeqnarrayrulerow[\arrayrulewidth]% +\else% +\@IEEEeqnarrayrulerow[#1]\relax% +\fi% +\def\@IEEEeqnarraydblrulerowARG{#2}% +\ifx\@IEEEeqnarraydblrulerowARG\@empty% +\\\IEEEeqnarrayseprow[\doublerulesep][\relax]% +\else% +\\\IEEEeqnarrayseprow[#2][\relax]% +\fi% +\\\multispan{\@IEEEeqnnumcols}% +% advance column counter only if the IEEEeqnarray environment wants it +\if@advanceIEEEeqncolcnt\global\advance\@IEEEeqncolcnt by \@IEEEeqnnumcols\relax\fi% +\def\@IEEEeqnarraydblrulerowARG{#1}% +\ifx\@IEEEeqnarraydblrulerowARG\@empty% +\@IEEEeqnarrayrulerow[\arrayrulewidth]% +\else% +\@IEEEeqnarrayrulerow[#1]% +\fi% +} + +% draws a double rule by using a single rule row, a separator (cutting) row, and then +% another single rule row +% first optional argument determines the rule thicknesses, \arrayrulewidth is the default +% second optional argument determines the rule spacing, \doublerulesep is the default +% usage: \IEEEeqnarraydblrulerow[rule line thickness][rule spacing] +\def\IEEEeqnarraydblrulerowcut{\multispan{\@IEEEeqnnumcols}\relax% span all the cols +% advance column counter only if the IEEEeqnarray environment wants it +\if@advanceIEEEeqncolcnt\global\advance\@IEEEeqncolcnt by \@IEEEeqnnumcols\relax\fi% +\@ifnextchar[{\@IEEEeqnarraydblrulerowcut}{\@IEEEeqnarraydblrulerowcut[\arrayrulewidth]}} +\def\@IEEEeqnarraydblrulerowcut[#1]{\relax\@ifnextchar[{\@@IEEEeqnarraydblrulerowcut[#1]}% +{\@@IEEEeqnarraydblrulerowcut[#1][\doublerulesep]}} +\def\@@IEEEeqnarraydblrulerowcut[#1][#2]{\def\@IEEEeqnarraydblrulerowARG{#1}% +% we allow the user to say \IEEEeqnarraydblrulerow[][] +\ifx\@IEEEeqnarraydblrulerowARG\@empty% +\@IEEEeqnarrayrulerow[\arrayrulewidth]% +\else% +\@IEEEeqnarrayrulerow[#1]% +\fi% +\def\@IEEEeqnarraydblrulerowARG{#2}% +\ifx\@IEEEeqnarraydblrulerowARG\@empty% +\\\IEEEeqnarrayseprowcut[\doublerulesep][\relax]% +\else% +\\\IEEEeqnarrayseprowcut[#2][\relax]% +\fi% +\\\multispan{\@IEEEeqnnumcols}% +% advance column counter only if the IEEEeqnarray environment wants it +\if@advanceIEEEeqncolcnt\global\advance\@IEEEeqncolcnt by \@IEEEeqnnumcols\relax\fi% +\def\@IEEEeqnarraydblrulerowARG{#1}% +\ifx\@IEEEeqnarraydblrulerowARG\@empty% +\@IEEEeqnarrayrulerow[\arrayrulewidth]% +\else% +\@IEEEeqnarrayrulerow[#1]% +\fi% +} + + + +% inserts a full row's worth of &'s +% relies on \@IEEEeqnnumcols to provide the correct number of columns +% uses \@IEEEtrantmptoksA, \count0 as scratch registers +\def\@IEEEeqnarrayhoptolastcolumn{\@IEEEtrantmptoksA={}\count0=1\relax% +\loop% add cols if the user did not use them all +\ifnum\count0<\@IEEEeqnnumcols\relax% +\@IEEEappendtoksA{&}% +\advance\count0 by 1\relax% update the col count +\repeat% +\the\@IEEEtrantmptoksA%execute the &'s +} + + + +\newif\if@IEEEeqnarrayISinner % flag to indicate if we are within the lines +\@IEEEeqnarrayISinnerfalse % of an IEEEeqnarray - after the IEEEeqnarraydecl + +\edef\@IEEEeqnarrayTHEstrutheight{0pt} % height and depth of IEEEeqnarray struts +\edef\@IEEEeqnarrayTHEstrutdepth{0pt} + +\edef\@IEEEeqnarrayTHEmasterstrutheight{0pt} % default height and depth of +\edef\@IEEEeqnarrayTHEmasterstrutdepth{0pt} % struts within an IEEEeqnarray + +\edef\@IEEEeqnarrayTHEmasterstrutHSAVE{0pt} % saved master strut height +\edef\@IEEEeqnarrayTHEmasterstrutDSAVE{0pt} % and depth + +\newif\if@IEEEeqnarrayusemasterstrut % flag to indicate that the master strut value +\@IEEEeqnarrayusemasterstruttrue % is to be used + + + +% saves the strut height and depth of the master strut +\def\@IEEEeqnarraymasterstrutsave{\relax% +\expandafter\skip0=\@IEEEeqnarrayTHEmasterstrutheight\relax% +\expandafter\skip2=\@IEEEeqnarrayTHEmasterstrutdepth\relax% +% remove stretchability +\dimen0\skip0\relax% +\dimen2\skip2\relax% +% save values +\edef\@IEEEeqnarrayTHEmasterstrutHSAVE{\the\dimen0}% +\edef\@IEEEeqnarrayTHEmasterstrutDSAVE{\the\dimen2}} + +% restores the strut height and depth of the master strut +\def\@IEEEeqnarraymasterstrutrestore{\relax% +\expandafter\skip0=\@IEEEeqnarrayTHEmasterstrutHSAVE\relax% +\expandafter\skip2=\@IEEEeqnarrayTHEmasterstrutDSAVE\relax% +% remove stretchability +\dimen0\skip0\relax% +\dimen2\skip2\relax% +% restore values +\edef\@IEEEeqnarrayTHEmasterstrutheight{\the\dimen0}% +\edef\@IEEEeqnarrayTHEmasterstrutdepth{\the\dimen2}} + + +% globally restores the strut height and depth to the +% master values and sets the master strut flag to true +\def\@IEEEeqnarraystrutreset{\relax% +\expandafter\skip0=\@IEEEeqnarrayTHEmasterstrutheight\relax% +\expandafter\skip2=\@IEEEeqnarrayTHEmasterstrutdepth\relax% +% remove stretchability +\dimen0\skip0\relax% +\dimen2\skip2\relax% +% restore values +\xdef\@IEEEeqnarrayTHEstrutheight{\the\dimen0}% +\xdef\@IEEEeqnarrayTHEstrutdepth{\the\dimen2}% +\global\@IEEEeqnarrayusemasterstruttrue} + + +% if the master strut is not to be used, make the current +% values of \@IEEEeqnarrayTHEstrutheight, \@IEEEeqnarrayTHEstrutdepth +% and the use master strut flag, global +% this allows user strut commands issued in the last column to be carried +% into the isolation/strut column +\def\@IEEEeqnarrayglobalizestrutstatus{\relax% +\if@IEEEeqnarrayusemasterstrut\else% +\xdef\@IEEEeqnarrayTHEstrutheight{\@IEEEeqnarrayTHEstrutheight}% +\xdef\@IEEEeqnarrayTHEstrutdepth{\@IEEEeqnarrayTHEstrutdepth}% +\global\@IEEEeqnarrayusemasterstrutfalse% +\fi} +% usage: \IEEEeqnarraystrutsize{height}{depth}[font size commands] +% If called outside the lines of an IEEEeqnarray, sets the height +% and depth of both the master and local struts. If called inside +% an IEEEeqnarray line, sets the height and depth of the local strut +% only and sets the flag to indicate the use of the local strut +% values. If the height or depth is left blank, 0.7\normalbaselineskip +% and 0.3\normalbaselineskip will be used, respectively. +% The optional argument can be used to evaluate the lengths under +% a different font size and styles. If none is specified, the current +% font is used. +% uses scratch registers \skip0, \skip2, \skip3, \dimen0, \dimen2 +\def\IEEEeqnarraystrutsize#1#2{\relax\@ifnextchar[{\@IEEEeqnarraystrutsize{#1}{#2}}{\@IEEEeqnarraystrutsize{#1}{#2}[\relax]}} +\def\@IEEEeqnarraystrutsize#1#2[#3]{\def\@IEEEeqnarraystrutsizeARG{#1}% +\ifx\@IEEEeqnarraystrutsizeARG\@empty% +{\setbox0=\hbox{#3\relax\global\skip3=0.7\normalbaselineskip}}% +\skip0=\skip3\relax% +\else% arg one present +{\setbox0=\hbox{#3\relax\global\skip3=#1\relax}}% +\skip0=\skip3\relax% +\fi% if null arg +\def\@IEEEeqnarraystrutsizeARG{#2}% +\ifx\@IEEEeqnarraystrutsizeARG\@empty% +{\setbox0=\hbox{#3\relax\global\skip3=0.3\normalbaselineskip}}% +\skip2=\skip3\relax% +\else% arg two present +{\setbox0=\hbox{#3\relax\global\skip3=#2\relax}}% +\skip2=\skip3\relax% +\fi% if null arg +% remove stretchability, just to be safe +\dimen0\skip0\relax% +\dimen2\skip2\relax% +% dimen0 = height, dimen2 = depth +\if@IEEEeqnarrayISinner% inner does not touch master strut size +\edef\@IEEEeqnarrayTHEstrutheight{\the\dimen0}% +\edef\@IEEEeqnarrayTHEstrutdepth{\the\dimen2}% +\@IEEEeqnarrayusemasterstrutfalse% do not use master +\else% outer, have to set master strut too +\edef\@IEEEeqnarrayTHEmasterstrutheight{\the\dimen0}% +\edef\@IEEEeqnarrayTHEmasterstrutdepth{\the\dimen2}% +\edef\@IEEEeqnarrayTHEstrutheight{\the\dimen0}% +\edef\@IEEEeqnarrayTHEstrutdepth{\the\dimen2}% +\@IEEEeqnarrayusemasterstruttrue% use master strut +\fi} + + +% usage: \IEEEeqnarraystrutsizeadd{added height}{added depth}[font size commands] +% If called outside the lines of an IEEEeqnarray, adds the given height +% and depth to both the master and local struts. +% If called inside an IEEEeqnarray line, adds the given height and depth +% to the local strut only and sets the flag to indicate the use +% of the local strut values. +% In both cases, if a height or depth is left blank, 0pt is used instead. +% The optional argument can be used to evaluate the lengths under +% a different font size and styles. If none is specified, the current +% font is used. +% uses scratch registers \skip0, \skip2, \skip3, \dimen0, \dimen2 +\def\IEEEeqnarraystrutsizeadd#1#2{\relax\@ifnextchar[{\@IEEEeqnarraystrutsizeadd{#1}{#2}}{\@IEEEeqnarraystrutsizeadd{#1}{#2}[\relax]}} +\def\@IEEEeqnarraystrutsizeadd#1#2[#3]{\def\@IEEEeqnarraystrutsizearg{#1}% +\ifx\@IEEEeqnarraystrutsizearg\@empty% +\skip0=0pt\relax% +\else% arg one present +{\setbox0=\hbox{#3\relax\global\skip3=#1}}% +\skip0=\skip3\relax% +\fi% if null arg +\def\@IEEEeqnarraystrutsizearg{#2}% +\ifx\@IEEEeqnarraystrutsizearg\@empty% +\skip2=0pt\relax% +\else% arg two present +{\setbox0=\hbox{#3\relax\global\skip3=#2}}% +\skip2=\skip3\relax% +\fi% if null arg +% remove stretchability, just to be safe +\dimen0\skip0\relax% +\dimen2\skip2\relax% +% dimen0 = height, dimen2 = depth +\if@IEEEeqnarrayISinner% inner does not touch master strut size +% get local strut size +\expandafter\skip0=\@IEEEeqnarrayTHEstrutheight\relax% +\expandafter\skip2=\@IEEEeqnarrayTHEstrutdepth\relax% +% add it to the user supplied values +\advance\dimen0 by \skip0\relax% +\advance\dimen2 by \skip2\relax% +% update the local strut size +\edef\@IEEEeqnarrayTHEstrutheight{\the\dimen0}% +\edef\@IEEEeqnarrayTHEstrutdepth{\the\dimen2}% +\@IEEEeqnarrayusemasterstrutfalse% do not use master +\else% outer, have to set master strut too +% get master strut size +\expandafter\skip0=\@IEEEeqnarrayTHEmasterstrutheight\relax% +\expandafter\skip2=\@IEEEeqnarrayTHEmasterstrutdepth\relax% +% add it to the user supplied values +\advance\dimen0 by \skip0\relax% +\advance\dimen2 by \skip2\relax% +% update the local and master strut sizes +\edef\@IEEEeqnarrayTHEmasterstrutheight{\the\dimen0}% +\edef\@IEEEeqnarrayTHEmasterstrutdepth{\the\dimen2}% +\edef\@IEEEeqnarrayTHEstrutheight{\the\dimen0}% +\edef\@IEEEeqnarrayTHEstrutdepth{\the\dimen2}% +\@IEEEeqnarrayusemasterstruttrue% use master strut +\fi} + + +% allow user a way to see the struts +\newif\ifIEEEvisiblestruts +\IEEEvisiblestrutsfalse + +% inserts an invisible strut using the master or local strut values +% uses scratch registers \skip0, \skip2, \dimen0, \dimen2 +\def\@IEEEeqnarrayinsertstrut{\relax% +\if@IEEEeqnarrayusemasterstrut +% get master strut size +\expandafter\skip0=\@IEEEeqnarrayTHEmasterstrutheight\relax% +\expandafter\skip2=\@IEEEeqnarrayTHEmasterstrutdepth\relax% +\else% +% get local strut size +\expandafter\skip0=\@IEEEeqnarrayTHEstrutheight\relax% +\expandafter\skip2=\@IEEEeqnarrayTHEstrutdepth\relax% +\fi% +% remove stretchability, probably not needed +\dimen0\skip0\relax% +\dimen2\skip2\relax% +% dimen0 = height, dimen2 = depth +% allow user to see struts if desired +\ifIEEEvisiblestruts% +\vrule width0.2pt height\dimen0 depth\dimen2\relax% +\else% +\vrule width0pt height\dimen0 depth\dimen2\relax\fi} + + +% creates an invisible strut, useable even outside \IEEEeqnarray +% if \IEEEvisiblestrutstrue, the strut will be visible and 0.2pt wide. +% usage: \IEEEstrut[height][depth][font size commands] +% default is \IEEEstrut[0.7\normalbaselineskip][0.3\normalbaselineskip][\relax] +% blank arguments inherit the default values +% uses \dimen0, \dimen2, \skip0, \skip2 +\def\IEEEstrut{\relax\@ifnextchar[{\@IEEEstrut}{\@IEEEstrut[0.7\normalbaselineskip]}} +\def\@IEEEstrut[#1]{\relax\@ifnextchar[{\@@IEEEstrut[#1]}{\@@IEEEstrut[#1][0.3\normalbaselineskip]}} +\def\@@IEEEstrut[#1][#2]{\relax\@ifnextchar[{\@@@IEEEstrut[#1][#2]}{\@@@IEEEstrut[#1][#2][\relax]}} +\def\@@@IEEEstrut[#1][#2][#3]{\mbox{#3\relax% +\def\@IEEEstrutARG{#1}% +\ifx\@IEEEstrutARG\@empty% +\skip0=0.7\normalbaselineskip\relax% +\else% +\skip0=#1\relax% +\fi% +\def\@IEEEstrutARG{#2}% +\ifx\@IEEEstrutARG\@empty% +\skip2=0.3\normalbaselineskip\relax% +\else% +\skip2=#2\relax% +\fi% +% remove stretchability, probably not needed +\dimen0\skip0\relax% +\dimen2\skip2\relax% +\ifIEEEvisiblestruts% +\vrule width0.2pt height\dimen0 depth\dimen2\relax% +\else% +\vrule width0.0pt height\dimen0 depth\dimen2\relax\fi}} + + +% enables strut mode by setting a default strut size and then zeroing the +% \baselineskip, \lineskip, \lineskiplimit and \jot +\def\IEEEeqnarraystrutmode{\IEEEeqnarraystrutsize{0.7\normalbaselineskip}{0.3\normalbaselineskip}[\relax]% +\baselineskip=0pt\lineskip=0pt\lineskiplimit=0pt\jot=0pt} + + + +\def\IEEEeqnarray{\@IEEEeqnarraystarformfalse\@IEEEeqnarray} +\def\endIEEEeqnarray{\end@IEEEeqnarray} + +\@namedef{IEEEeqnarray*}{\@IEEEeqnarraystarformtrue\@IEEEeqnarray} +\@namedef{endIEEEeqnarray*}{\end@IEEEeqnarray} + + +% \IEEEeqnarray is an enhanced \eqnarray. +% The star form defaults to not putting equation numbers at the end of each row. +% usage: \IEEEeqnarray[decl]{cols} +\def\@IEEEeqnarray{\relax\@ifnextchar[{\@@IEEEeqnarray}{\@@IEEEeqnarray[\relax]}} +\def\@@IEEEeqnarray[#1]#2{% + % default to showing the equation number or not based on whether or not + % the star form was involked + \if@IEEEeqnarraystarform\global\@eqnswfalse + \else% not the star form + \global\@eqnswtrue + \fi% if star form + \@IEEEissubequationfalse% default to no subequations + \@IEEElastlinewassubequationfalse% assume last line is not a sub equation + \@IEEEeqnarrayISinnerfalse% not yet within the lines of the halign + \@IEEEeqnarraystrutsize{0pt}{0pt}[\relax]% turn off struts by default + \@IEEEeqnarrayusemasterstruttrue% use master strut till user asks otherwise + \IEEEvisiblestrutsfalse% diagnostic mode defaults to off + % no extra space unless the user specifically requests it + \lineskip=0pt\relax + \lineskiplimit=0pt\relax + \baselineskip=\normalbaselineskip\relax% + \jot=\IEEEnormaljot\relax% + \mathsurround\z@\relax% no extra spacing around math + \@advanceIEEEeqncolcnttrue% advance the col counter for each col the user uses, + % used in \IEEEeqnarraymulticol and in the preamble build + \stepcounter{equation}% advance equation counter before first line + \setcounter{IEEEsubequation}{0}% no subequation yet + \def\@currentlabel{\p@equation\theequation}% redefine the ref label + \IEEEeqnarraydecl\relax% allow a way for the user to make global overrides + #1\relax% allow user to override defaults + \let\\\@IEEEeqnarraycr% replace newline with one that can put in eqn. numbers + \global\@IEEEeqncolcnt\z@% col. count = 0 for first line + \@IEEEbuildpreamble #2\end\relax% build the preamble and put it into \@IEEEtrantmptoksA + % put in the column for the equation number + \ifnum\@IEEEeqnnumcols>0\relax\@IEEEappendtoksA{&}\fi% col separator for those after the first + \toks0={##}% + % advance the \@IEEEeqncolcnt for the isolation col, this helps with error checking + \@IEEEappendtoksA{\global\advance\@IEEEeqncolcnt by 1\relax}% + % add the isolation column + \@IEEEappendtoksA{\tabskip\z@skip\bgroup\the\toks0\egroup}% + % advance the \@IEEEeqncolcnt for the equation number col, this helps with error checking + \@IEEEappendtoksA{&\global\advance\@IEEEeqncolcnt by 1\relax}% + % add the equation number col to the preamble + \@IEEEappendtoksA{\tabskip\z@skip\hb@xt@\z@\bgroup\hss\the\toks0\egroup}% + % note \@IEEEeqnnumcols does not count the equation col or isolation col + % set the starting tabskip glue as determined by the preamble build + \tabskip=\@IEEEBPstartglue\relax + % begin the display alignment + \@IEEEeqnarrayISinnertrue% commands are now within the lines + $$\everycr{}\halign to\displaywidth\bgroup + % "exspand" the preamble + \span\the\@IEEEtrantmptoksA\cr} + +% enter isolation/strut column (or the next column if the user did not use +% every column), record the strut status, complete the columns, do the strut if needed, +% restore counters to correct values and exit +\def\end@IEEEeqnarray{\@IEEEeqnarrayglobalizestrutstatus&\@@IEEEeqnarraycr\egroup% +\if@IEEElastlinewassubequation\global\advance\c@IEEEsubequation\m@ne\fi% +\global\advance\c@equation\m@ne% +$$\@ignoretrue} + +% need a way to remember if last line is a subequation +\newif\if@IEEElastlinewassubequation% +\@IEEElastlinewassubequationfalse + +% IEEEeqnarray uses a modifed \\ instead of the plain \cr to +% end rows. This allows for things like \\*[vskip amount] +% This "cr" macros are modified versions those for LaTeX2e's eqnarray +% the {\ifnum0=`} braces must be kept away from the last column to avoid +% altering spacing of its math, so we use & to advance to the next column +% as there is an isolation/strut column after the user's columns +\def\@IEEEeqnarraycr{\@IEEEeqnarrayglobalizestrutstatus&% save strut status and advance to next column + {\ifnum0=`}\fi + \@ifstar{% + \global\@eqpen\@M\@IEEEeqnarrayYCR + }{% + \global\@eqpen\interdisplaylinepenalty \@IEEEeqnarrayYCR + }% +} + +\def\@IEEEeqnarrayYCR{\@testopt\@IEEEeqnarrayXCR\z@skip} + +\def\@IEEEeqnarrayXCR[#1]{% + \ifnum0=`{\fi}% + \@@IEEEeqnarraycr + \noalign{\penalty\@eqpen\vskip\jot\vskip #1\relax}}% + +\def\@@IEEEeqnarraycr{\@IEEEtrantmptoksA={}% clear token register + \advance\@IEEEeqncolcnt by -1\relax% adjust col count because of the isolation column + \ifnum\@IEEEeqncolcnt>\@IEEEeqnnumcols\relax + \@IEEEclspkgerror{Too many columns within the IEEEeqnarray\MessageBreak + environment}% + {Use fewer \string &'s or put more columns in the IEEEeqnarry column\MessageBreak + specifications.}\relax% + \else + \loop% add cols if the user did not use them all + \ifnum\@IEEEeqncolcnt<\@IEEEeqnnumcols\relax + \@IEEEappendtoksA{&}% + \advance\@IEEEeqncolcnt by 1\relax% update the col count + \repeat + % this number of &'s will take us the the isolation column + \fi + % execute the &'s + \the\@IEEEtrantmptoksA% + % handle the strut/isolation column + \@IEEEeqnarrayinsertstrut% do the strut if needed + \@IEEEeqnarraystrutreset% reset the strut system for next line or IEEEeqnarray + &% and enter the equation number column + % is this line needs an equation number, display it and advance the + % (sub)equation counters, record what type this line was + \if@eqnsw% + \if@IEEEissubequation\theIEEEsubequationdis\addtocounter{equation}{1}\stepcounter{IEEEsubequation}% + \global\@IEEElastlinewassubequationtrue% + \else% display a standard equation number, initialize the IEEEsubequation counter + \theequationdis\stepcounter{equation}\setcounter{IEEEsubequation}{0}% + \global\@IEEElastlinewassubequationfalse\fi% + \fi% + % reset the eqnsw flag to indicate default preference of the display of equation numbers + \if@IEEEeqnarraystarform\global\@eqnswfalse\else\global\@eqnswtrue\fi + \global\@IEEEissubequationfalse% reset the subequation flag + % reset the number of columns the user actually used + \global\@IEEEeqncolcnt\z@\relax + % the real end of the line + \cr} + + + + + +% \IEEEeqnarraybox is like \IEEEeqnarray except the box form puts everything +% inside a vtop, vbox, or vcenter box depending on the letter in the second +% optional argument (t,b,c). Vbox is the default. Unlike \IEEEeqnarray, +% equation numbers are not displayed and \IEEEeqnarraybox can be nested. +% \IEEEeqnarrayboxm is for math mode (like \array) and does not put the vbox +% within an hbox. +% \IEEEeqnarrayboxt is for text mode (like \tabular) and puts the vbox within +% a \hbox{$ $} construct. +% \IEEEeqnarraybox will auto detect whether to use \IEEEeqnarrayboxm or +% \IEEEeqnarrayboxt depending on the math mode. +% The third optional argument specifies the width this box is to be set to - +% natural width is the default. +% The * forms do not add \jot line spacing +% usage: \IEEEeqnarraybox[decl][pos][width]{cols} +\def\IEEEeqnarrayboxm{\@IEEEeqnarraystarformfalse\@IEEEeqnarrayboxHBOXSWfalse\@IEEEeqnarraybox} +\def\endIEEEeqnarrayboxm{\end@IEEEeqnarraybox} +\@namedef{IEEEeqnarrayboxm*}{\@IEEEeqnarraystarformtrue\@IEEEeqnarrayboxHBOXSWfalse\@IEEEeqnarraybox} +\@namedef{endIEEEeqnarrayboxm*}{\end@IEEEeqnarraybox} + +\def\IEEEeqnarrayboxt{\@IEEEeqnarraystarformfalse\@IEEEeqnarrayboxHBOXSWtrue\@IEEEeqnarraybox} +\def\endIEEEeqnarrayboxt{\end@IEEEeqnarraybox} +\@namedef{IEEEeqnarrayboxt*}{\@IEEEeqnarraystarformtrue\@IEEEeqnarrayboxHBOXSWtrue\@IEEEeqnarraybox} +\@namedef{endIEEEeqnarrayboxt*}{\end@IEEEeqnarraybox} + +\def\IEEEeqnarraybox{\@IEEEeqnarraystarformfalse\ifmmode\@IEEEeqnarrayboxHBOXSWfalse\else\@IEEEeqnarrayboxHBOXSWtrue\fi% +\@IEEEeqnarraybox} +\def\endIEEEeqnarraybox{\end@IEEEeqnarraybox} + +\@namedef{IEEEeqnarraybox*}{\@IEEEeqnarraystarformtrue\ifmmode\@IEEEeqnarrayboxHBOXSWfalse\else\@IEEEeqnarrayboxHBOXSWtrue\fi% +\@IEEEeqnarraybox} +\@namedef{endIEEEeqnarraybox*}{\end@IEEEeqnarraybox} + +% flag to indicate if the \IEEEeqnarraybox needs to put things into an hbox{$ $} +% for \vcenter in non-math mode +\newif\if@IEEEeqnarrayboxHBOXSW% +\@IEEEeqnarrayboxHBOXSWfalse + +\def\@IEEEeqnarraybox{\relax\@ifnextchar[{\@@IEEEeqnarraybox}{\@@IEEEeqnarraybox[\relax]}} +\def\@@IEEEeqnarraybox[#1]{\relax\@ifnextchar[{\@@@IEEEeqnarraybox[#1]}{\@@@IEEEeqnarraybox[#1][b]}} +\def\@@@IEEEeqnarraybox[#1][#2]{\relax\@ifnextchar[{\@@@@IEEEeqnarraybox[#1][#2]}{\@@@@IEEEeqnarraybox[#1][#2][\relax]}} + +% #1 = decl; #2 = t,b,c; #3 = width, #4 = col specs +\def\@@@@IEEEeqnarraybox[#1][#2][#3]#4{\@IEEEeqnarrayISinnerfalse % not yet within the lines of the halign + \@IEEEeqnarraymasterstrutsave% save current master strut values + \@IEEEeqnarraystrutsize{0pt}{0pt}[\relax]% turn off struts by default + \@IEEEeqnarrayusemasterstruttrue% use master strut till user asks otherwise + \IEEEvisiblestrutsfalse% diagnostic mode defaults to off + % no extra space unless the user specifically requests it + \lineskip=0pt\relax% + \lineskiplimit=0pt\relax% + \baselineskip=\normalbaselineskip\relax% + \jot=\IEEEnormaljot\relax% + \mathsurround\z@\relax% no extra spacing around math + % the default end glues are zero for an \IEEEeqnarraybox + \edef\@IEEEeqnarraycolSEPdefaultstart{\@IEEEeqnarraycolSEPzero}% default start glue + \edef\@IEEEeqnarraycolSEPdefaultend{\@IEEEeqnarraycolSEPzero}% default end glue + \edef\@IEEEeqnarraycolSEPdefaultmid{\@IEEEeqnarraycolSEPzero}% default inter-column glue + \@advanceIEEEeqncolcntfalse% do not advance the col counter for each col the user uses, + % used in \IEEEeqnarraymulticol and in the preamble build + \IEEEeqnarrayboxdecl\relax% allow a way for the user to make global overrides + #1\relax% allow user to override defaults + \let\\\@IEEEeqnarrayboxcr% replace newline with one that allows optional spacing + \@IEEEbuildpreamble #4\end\relax% build the preamble and put it into \@IEEEtrantmptoksA + % add an isolation column to the preamble to stop \\'s {} from getting into the last col + \ifnum\@IEEEeqnnumcols>0\relax\@IEEEappendtoksA{&}\fi% col separator for those after the first + \toks0={##}% + % add the isolation column to the preamble + \@IEEEappendtoksA{\tabskip\z@skip\bgroup\the\toks0\egroup}% + % set the starting tabskip glue as determined by the preamble build + \tabskip=\@IEEEBPstartglue\relax + % begin the alignment + \everycr{}% + % use only the very first token to determine the positioning + % this stops some problems when the user uses more than one letter, + % but is probably not worth the effort + % \noindent is used as a delimiter + \def\@IEEEgrabfirstoken##1##2\noindent{\let\@IEEEgrabbedfirstoken=##1}% + \@IEEEgrabfirstoken#2\relax\relax\noindent + % \@IEEEgrabbedfirstoken has the first token, the rest are discarded + % if we need to put things into and hbox and go into math mode, do so now + \if@IEEEeqnarrayboxHBOXSW \leavevmode \hbox \bgroup $\fi% + % use the appropriate vbox type + \if\@IEEEgrabbedfirstoken t\relax\vtop\else\if\@IEEEgrabbedfirstoken c\relax% + \vcenter\else\vbox\fi\fi\bgroup% + \@IEEEeqnarrayISinnertrue% commands are now within the lines + \ifx#3\relax\halign\else\halign to #3\relax\fi% + \bgroup + % "exspand" the preamble + \span\the\@IEEEtrantmptoksA\cr} + +% carry strut status and enter the isolation/strut column, +% exit from math mode if needed, and exit +\def\end@IEEEeqnarraybox{\@IEEEeqnarrayglobalizestrutstatus% carry strut status +&% enter isolation/strut column +\@IEEEeqnarrayinsertstrut% do strut if needed +\@IEEEeqnarraymasterstrutrestore% restore the previous master strut values +% reset the strut system for next IEEEeqnarray +% (sets local strut values back to previous master strut values) +\@IEEEeqnarraystrutreset% +% ensure last line, exit from halign, close vbox +\crcr\egroup\egroup% +% exit from math mode and close hbox if needed +\if@IEEEeqnarrayboxHBOXSW $\egroup\fi} + + + +% IEEEeqnarraybox uses a modifed \\ instead of the plain \cr to +% end rows. This allows for things like \\[vskip amount] +% This "cr" macros are modified versions those for LaTeX2e's eqnarray +% For IEEEeqnarraybox, \\* is the same as \\ +% the {\ifnum0=`} braces must be kept away from the last column to avoid +% altering spacing of its math, so we use & to advance to the isolation/strut column +% carry strut status into isolation/strut column +\def\@IEEEeqnarrayboxcr{\@IEEEeqnarrayglobalizestrutstatus% carry strut status +&% enter isolation/strut column +\@IEEEeqnarrayinsertstrut% do strut if needed +% reset the strut system for next line or IEEEeqnarray +\@IEEEeqnarraystrutreset% +{\ifnum0=`}\fi% +\@ifstar{\@IEEEeqnarrayboxYCR}{\@IEEEeqnarrayboxYCR}} + +% test and setup the optional argument to \\[] +\def\@IEEEeqnarrayboxYCR{\@testopt\@IEEEeqnarrayboxXCR\z@skip} + +% IEEEeqnarraybox does not automatically increase line spacing by \jot +\def\@IEEEeqnarrayboxXCR[#1]{\ifnum0=`{\fi}% +\cr\noalign{\if@IEEEeqnarraystarform\else\vskip\jot\fi\vskip#1\relax}} + + + +% starts the halign preamble build +\def\@IEEEbuildpreamble{\@IEEEtrantmptoksA={}% clear token register +\let\@IEEEBPcurtype=u%current column type is not yet known +\let\@IEEEBPprevtype=s%the previous column type was the start +\let\@IEEEBPnexttype=u%next column type is not yet known +% ensure these are valid +\def\@IEEEBPcurglue={0pt plus 0pt minus 0pt}% +\def\@IEEEBPcurcolname{@IEEEdefault}% name of current column definition +% currently acquired numerically referenced glue +% use a name that is easier to remember +\let\@IEEEBPcurnum=\@IEEEtrantmpcountA% +\@IEEEBPcurnum=0% +% tracks number of columns in the preamble +\@IEEEeqnnumcols=0% +% record the default end glues +\edef\@IEEEBPstartglue{\@IEEEeqnarraycolSEPdefaultstart}% +\edef\@IEEEBPendglue{\@IEEEeqnarraycolSEPdefaultend}% +% now parse the user's column specifications +\@@IEEEbuildpreamble} + + +% parses and builds the halign preamble +\def\@@IEEEbuildpreamble#1#2{\let\@@nextIEEEbuildpreamble=\@@IEEEbuildpreamble% +% use only the very first token to check the end +% \noindent is used as a delimiter as \end can be present here +\def\@IEEEgrabfirstoken##1##2\noindent{\let\@IEEEgrabbedfirstoken=##1}% +\@IEEEgrabfirstoken#1\relax\relax\noindent +\ifx\@IEEEgrabbedfirstoken\end\let\@@nextIEEEbuildpreamble=\@@IEEEfinishpreamble\else% +% identify current and next token type +\@IEEEgetcoltype{#1}{\@IEEEBPcurtype}{1}% current, error on invalid +\@IEEEgetcoltype{#2}{\@IEEEBPnexttype}{0}% next, no error on invalid next +% if curtype is a glue, get the glue def +\if\@IEEEBPcurtype g\@IEEEgetcurglue{#1}{\@IEEEBPcurglue}\fi% +% if curtype is a column, get the column def and set the current column name +\if\@IEEEBPcurtype c\@IEEEgetcurcol{#1}\fi% +% if curtype is a numeral, acquire the user defined glue +\if\@IEEEBPcurtype n\@IEEEprocessNcol{#1}\fi% +% process the acquired glue +\if\@IEEEBPcurtype g\@IEEEprocessGcol\fi% +% process the acquired col +\if\@IEEEBPcurtype c\@IEEEprocessCcol\fi% +% ready prevtype for next col spec. +\let\@IEEEBPprevtype=\@IEEEBPcurtype% +% be sure and put back the future token(s) as a group +\fi\@@nextIEEEbuildpreamble{#2}} + + +% executed just after preamble build is completed +% warn about zero cols, and if prevtype type = u, put in end tabskip glue +\def\@@IEEEfinishpreamble#1{\ifnum\@IEEEeqnnumcols<1\relax +\@IEEEclspkgerror{No column specifiers declared for IEEEeqnarray}% +{At least one column type must be declared for each IEEEeqnarray.}% +\fi%num cols less than 1 +%if last type undefined, set default end tabskip glue +\if\@IEEEBPprevtype u\@IEEEappendtoksA{\tabskip=\@IEEEBPendglue}\fi} + + +% Identify and return the column specifier's type code +\def\@IEEEgetcoltype#1#2#3{% +% use only the very first token to determine the type +% \noindent is used as a delimiter as \end can be present here +\def\@IEEEgrabfirstoken##1##2\noindent{\let\@IEEEgrabbedfirstoken=##1}% +\@IEEEgrabfirstoken#1\relax\relax\noindent +% \@IEEEgrabfirstoken has the first token, the rest are discarded +% n = number +% g = glue (any other char in catagory 12) +% c = letter +% e = \end +% u = undefined +% third argument: 0 = no error message, 1 = error on invalid char +\let#2=u\relax% assume invalid until know otherwise +\ifx\@IEEEgrabbedfirstoken\end\let#2=e\else +\ifcat\@IEEEgrabbedfirstoken\relax\else% screen out control sequences +\if0\@IEEEgrabbedfirstoken\let#2=n\else +\if1\@IEEEgrabbedfirstoken\let#2=n\else +\if2\@IEEEgrabbedfirstoken\let#2=n\else +\if3\@IEEEgrabbedfirstoken\let#2=n\else +\if4\@IEEEgrabbedfirstoken\let#2=n\else +\if5\@IEEEgrabbedfirstoken\let#2=n\else +\if6\@IEEEgrabbedfirstoken\let#2=n\else +\if7\@IEEEgrabbedfirstoken\let#2=n\else +\if8\@IEEEgrabbedfirstoken\let#2=n\else +\if9\@IEEEgrabbedfirstoken\let#2=n\else +\ifcat,\@IEEEgrabbedfirstoken\let#2=g\relax +\else\ifcat a\@IEEEgrabbedfirstoken\let#2=c\relax\fi\fi\fi\fi\fi\fi\fi\fi\fi\fi\fi\fi\fi\fi +\if#2u\relax +\if0\noexpand#3\relax\else\@IEEEclspkgerror{Invalid character in column specifications}% +{Only letters, numerals and certain other symbols are allowed \MessageBreak +as IEEEeqnarray column specifiers.}\fi\fi} + + +% identify the current letter referenced column +% if invalid, use a default column +\def\@IEEEgetcurcol#1{\expandafter\ifx\csname @IEEEeqnarraycolDEF#1\endcsname\@IEEEeqnarraycolisdefined% +\def\@IEEEBPcurcolname{#1}\else% invalid column name +\@IEEEclspkgerror{Invalid column type "#1" in column specifications.\MessageBreak +Using a default centering column instead}% +{You must define IEEEeqnarray column types before use.}% +\def\@IEEEBPcurcolname{@IEEEdefault}\fi} + + +% identify and return the predefined (punctuation) glue value +\def\@IEEEgetcurglue#1#2{% +% ! = \! (neg small) -0.16667em (-3/18 em) +% , = \, (small) 0.16667em ( 3/18 em) +% : = \: (med) 0.22222em ( 4/18 em) +% ; = \; (large) 0.27778em ( 5/18 em) +% ' = \quad 1em +% " = \qquad 2em +% . = 0.5\arraycolsep +% / = \arraycolsep +% ? = 2\arraycolsep +% * = 1fil +% + = \@IEEEeqnarraycolSEPcenter +% - = \@IEEEeqnarraycolSEPzero +% Note that all em values are referenced to the math font (textfont2) fontdimen6 +% value for 1em. +% +% use only the very first token to determine the type +% this prevents errant tokens from getting in the main text +% \noindent is used as a delimiter here +\def\@IEEEgrabfirstoken##1##2\noindent{\let\@IEEEgrabbedfirstoken=##1}% +\@IEEEgrabfirstoken#1\relax\relax\noindent +% get the math font 1em value +% LaTeX2e's NFSS2 does not preload the fonts, but \IEEEeqnarray needs +% to gain access to the math (\textfont2) font's spacing parameters. +% So we create a bogus box here that uses the math font to ensure +% that \textfont2 is loaded and ready. If this is not done, +% the \textfont2 stuff here may not work. +% Thanks to Bernd Raichle for his 1997 post on this topic. +{\setbox0=\hbox{$\displaystyle\relax$}}% +% fontdimen6 has the width of 1em (a quad). +\@IEEEtrantmpdimenA=\fontdimen6\textfont2\relax% +% identify the glue value based on the first token +% we discard anything after the first +\if!\@IEEEgrabbedfirstoken\@IEEEtrantmpdimenA=-0.16667\@IEEEtrantmpdimenA\edef#2{\the\@IEEEtrantmpdimenA}\else +\if,\@IEEEgrabbedfirstoken\@IEEEtrantmpdimenA=0.16667\@IEEEtrantmpdimenA\edef#2{\the\@IEEEtrantmpdimenA}\else +\if:\@IEEEgrabbedfirstoken\@IEEEtrantmpdimenA=0.22222\@IEEEtrantmpdimenA\edef#2{\the\@IEEEtrantmpdimenA}\else +\if;\@IEEEgrabbedfirstoken\@IEEEtrantmpdimenA=0.27778\@IEEEtrantmpdimenA\edef#2{\the\@IEEEtrantmpdimenA}\else +\if'\@IEEEgrabbedfirstoken\@IEEEtrantmpdimenA=1\@IEEEtrantmpdimenA\edef#2{\the\@IEEEtrantmpdimenA}\else +\if"\@IEEEgrabbedfirstoken\@IEEEtrantmpdimenA=2\@IEEEtrantmpdimenA\edef#2{\the\@IEEEtrantmpdimenA}\else +\if.\@IEEEgrabbedfirstoken\@IEEEtrantmpdimenA=0.5\arraycolsep\edef#2{\the\@IEEEtrantmpdimenA}\else +\if/\@IEEEgrabbedfirstoken\edef#2{\the\arraycolsep}\else +\if?\@IEEEgrabbedfirstoken\@IEEEtrantmpdimenA=2\arraycolsep\edef#2{\the\@IEEEtrantmpdimenA}\else +\if *\@IEEEgrabbedfirstoken\edef#2{0pt plus 1fil minus 0pt}\else +\if+\@IEEEgrabbedfirstoken\edef#2{\@IEEEeqnarraycolSEPcenter}\else +\if-\@IEEEgrabbedfirstoken\edef#2{\@IEEEeqnarraycolSEPzero}\else +\edef#2{\@IEEEeqnarraycolSEPzero}% +\@IEEEclspkgerror{Invalid predefined inter-column glue type "#1" in\MessageBreak +column specifications. Using a default value of\MessageBreak +0pt instead}% +{Only !,:;'"./?*+ and - are valid predefined glue types in the\MessageBreak +IEEEeqnarray column specifications.}\fi\fi\fi\fi\fi\fi\fi\fi\fi\fi\fi\fi} + + + +% process a numerical digit from the column specification +% and look up the corresponding user defined glue value +% can transform current type from n to g or a as the user defined glue is acquired +\def\@IEEEprocessNcol#1{\if\@IEEEBPprevtype g% +\@IEEEclspkgerror{Back-to-back inter-column glue specifiers in column\MessageBreak +specifications. Ignoring consecutive glue specifiers\MessageBreak +after the first}% +{You cannot have two or more glue types next to each other\MessageBreak +in the IEEEeqnarray column specifications.}% +\let\@IEEEBPcurtype=a% abort this glue, future digits will be discarded +\@IEEEBPcurnum=0\relax% +\else% if we previously aborted a glue +\if\@IEEEBPprevtype a\@IEEEBPcurnum=0\let\@IEEEBPcurtype=a%maintain digit abortion +\else%acquire this number +% save the previous type before the numerical digits started +\if\@IEEEBPprevtype n\else\let\@IEEEBPprevsavedtype=\@IEEEBPprevtype\fi% +\multiply\@IEEEBPcurnum by 10\relax% +\advance\@IEEEBPcurnum by #1\relax% add in number, \relax is needed to stop TeX's number scan +\if\@IEEEBPnexttype n\else%close acquisition +\expandafter\ifx\csname @IEEEeqnarraycolSEPDEF\expandafter\romannumeral\number\@IEEEBPcurnum\endcsname\@IEEEeqnarraycolisdefined% +\edef\@IEEEBPcurglue{\csname @IEEEeqnarraycolSEP\expandafter\romannumeral\number\@IEEEBPcurnum\endcsname}% +\else%user glue not defined +\@IEEEclspkgerror{Invalid user defined inter-column glue type "\number\@IEEEBPcurnum" in\MessageBreak +column specifications. Using a default value of\MessageBreak +0pt instead}% +{You must define all IEEEeqnarray numerical inter-column glue types via\MessageBreak +\string\IEEEeqnarraydefcolsep \space before they are used in column specifications.}% +\edef\@IEEEBPcurglue{\@IEEEeqnarraycolSEPzero}% +\fi% glue defined or not +\let\@IEEEBPcurtype=g% change the type to reflect the acquired glue +\let\@IEEEBPprevtype=\@IEEEBPprevsavedtype% restore the prev type before this number glue +\@IEEEBPcurnum=0\relax%ready for next acquisition +\fi%close acquisition, get glue +\fi%discard or acquire number +\fi%prevtype glue or not +} + + +% process an acquired glue +% add any acquired column/glue pair to the preamble +\def\@IEEEprocessGcol{\if\@IEEEBPprevtype a\let\@IEEEBPcurtype=a%maintain previous glue abortions +\else +% if this is the start glue, save it, but do nothing else +% as this is not used in the preamble, but before +\if\@IEEEBPprevtype s\edef\@IEEEBPstartglue{\@IEEEBPcurglue}% +\else%not the start glue +\if\@IEEEBPprevtype g%ignore if back to back glues +\@IEEEclspkgerror{Back-to-back inter-column glue specifiers in column\MessageBreak +specifications. Ignoring consecutive glue specifiers\MessageBreak +after the first}% +{You cannot have two or more glue types next to each other\MessageBreak +in the IEEEeqnarray column specifications.}% +\let\@IEEEBPcurtype=a% abort this glue +\else% not a back to back glue +\if\@IEEEBPprevtype c\relax% if the previoustype was a col, add column/glue pair to preamble +\ifnum\@IEEEeqnnumcols>0\relax\@IEEEappendtoksA{&}\fi +\toks0={##}% +% make preamble advance col counter if this environment needs this +\if@advanceIEEEeqncolcnt\@IEEEappendtoksA{\global\advance\@IEEEeqncolcnt by 1\relax}\fi +% insert the column defintion into the preamble, being careful not to expand +% the column definition +\@IEEEappendtoksA{\tabskip=\@IEEEBPcurglue}% +\@IEEEappendNOEXPANDtoksA{\begingroup\csname @IEEEeqnarraycolPRE}% +\@IEEEappendtoksA{\@IEEEBPcurcolname}% +\@IEEEappendNOEXPANDtoksA{\endcsname}% +\@IEEEappendtoksA{\the\toks0}% +\@IEEEappendNOEXPANDtoksA{\relax\relax\relax\relax\relax% +\relax\relax\relax\relax\relax\csname @IEEEeqnarraycolPOST}% +\@IEEEappendtoksA{\@IEEEBPcurcolname}% +\@IEEEappendNOEXPANDtoksA{\endcsname\relax\relax\relax\relax\relax% +\relax\relax\relax\relax\relax\endgroup}% +\advance\@IEEEeqnnumcols by 1\relax%one more column in the preamble +\else% error: non-start glue with no pending column +\@IEEEclspkgerror{Inter-column glue specifier without a prior column\MessageBreak +type in the column specifications. Ignoring this glue\MessageBreak +specifier}% +{Except for the first and last positions, glue can be placed only\MessageBreak +between column types.}% +\let\@IEEEBPcurtype=a% abort this glue +\fi% previous was a column +\fi% back-to-back glues +\fi% is start column glue +\fi% prev type not a +} + + +% process an acquired letter referenced column and, if necessary, add it to the preamble +\def\@IEEEprocessCcol{\if\@IEEEBPnexttype g\else +\if\@IEEEBPnexttype n\else +% we have a column followed by something other than a glue (or numeral glue) +% so we must add this column to the preamble now +\ifnum\@IEEEeqnnumcols>0\relax\@IEEEappendtoksA{&}\fi%col separator for those after the first +\if\@IEEEBPnexttype e\@IEEEappendtoksA{\tabskip=\@IEEEBPendglue\relax}\else%put in end glue +\@IEEEappendtoksA{\tabskip=\@IEEEeqnarraycolSEPdefaultmid\relax}\fi% or default mid glue +\toks0={##}% +% make preamble advance col counter if this environment needs this +\if@advanceIEEEeqncolcnt\@IEEEappendtoksA{\global\advance\@IEEEeqncolcnt by 1\relax}\fi +% insert the column definition into the preamble, being careful not to expand +% the column definition +\@IEEEappendNOEXPANDtoksA{\begingroup\csname @IEEEeqnarraycolPRE}% +\@IEEEappendtoksA{\@IEEEBPcurcolname}% +\@IEEEappendNOEXPANDtoksA{\endcsname}% +\@IEEEappendtoksA{\the\toks0}% +\@IEEEappendNOEXPANDtoksA{\relax\relax\relax\relax\relax% +\relax\relax\relax\relax\relax\csname @IEEEeqnarraycolPOST}% +\@IEEEappendtoksA{\@IEEEBPcurcolname}% +\@IEEEappendNOEXPANDtoksA{\endcsname\relax\relax\relax\relax\relax% +\relax\relax\relax\relax\relax\endgroup}% +\advance\@IEEEeqnnumcols by 1\relax%one more column in the preamble +\fi%next type not numeral +\fi%next type not glue +} + + +%% +%% END OF IEEEeqnarry DEFINITIONS +%% + + + + +% \IEEEPARstart +% Definition for the big two line drop cap letter at the beginning of the +% first paragraph of journal papers. The first argument is the first letter +% of the first word, the second argument is the remaining letters of the +% first word which will be rendered in upper case. +% In V1.6 this has been completely rewritten to: +% +% 1. no longer have problems when the user begins an environment +% within the paragraph that uses \IEEEPARstart. +% 2. auto-detect and use the current font family +% 3. revise handling of the space at the end of the first word so that +% interword glue will now work as normal. +% 4. produce correctly aligned edges for the (two) indented lines. +% +% We generalize things via control macros - playing with these is fun too. +% +% For IEEEtrantools, we do not use a "@" in the names as these are user +% alterable controls. +% +% V1.7 added more control macros to make it easy for IEEEtrantools.sty users +% to change the font style. +% +% the number of lines that are indented to clear it +% may need to increase if using decenders +\providecommand{\IEEEPARstartDROPLINES}{2} +% minimum number of lines left on a page to allow a \@IEEEPARstart +% Does not take into consideration rubber shrink, so it tends to +% be overly cautious +\providecommand{\IEEEPARstartMINPAGELINES}{2} +% V1.7 the height of the drop cap is adjusted to match the height of this text +% in the current font (when \IEEEPARstart is called). +\providecommand{\IEEEPARstartHEIGHTTEXT}{T} +% the depth the letter is lowered below the baseline +% the height (and size) of the letter is determined by the sum +% of this value and the height of the \IEEEPARstartHEIGHTTEXT in the current +% font. It is a good idea to set this value in terms of the baselineskip +% so that it can respond to changes therein. +\providecommand{\IEEEPARstartDROPDEPTH}{1.1\baselineskip} +% V1.7 the font the drop cap will be rendered in, +% can take zero or one argument. +\providecommand{\IEEEPARstartFONTSTYLE}{\bfseries} +% V1.7 any additional, non-font related commands needed to modify +% the drop cap letter, can take zero or one argument. +\providecommand{\IEEEPARstartCAPSTYLE}{\MakeUppercase} +% V1.7 the font that will be used to render the rest of the word, +% can take zero or one argument. +\providecommand{\IEEEPARstartWORDFONTSTYLE}{\relax} +% V1.7 any additional, non-font related commands needed to modify +% the rest of the word, can take zero or one argument. +\providecommand{\IEEEPARstartWORDCAPSTYLE}{\MakeUppercase} +% This is the horizontal separation distance from the drop letter to the main text. +% Lengths that depend on the font (e.g., ex, em, etc.) will be referenced +% to the font that is active when \IEEEPARstart is called. +\providecommand{\IEEEPARstartSEP}{0.15em} +% V1.7 horizontal offset applied to the left of the drop cap. +\providecommand{\IEEEPARstartHOFFSET}{0em} +% V1.7 Italic correction command applied at the end of the drop cap. +\providecommand{\IEEEPARstartITLCORRECT}{\/} + +% width of the letter output, set globally. Can be used in \IEEEPARstartSEP +% or \IEEEPARstartHOFFSET, but not the height lengths. +\newdimen\IEEEPARstartletwidth +\IEEEPARstartletwidth 0pt\relax + +% definition of \IEEEPARstart +% THIS IS A CONTROLLED SPACING AREA, DO NOT ALLOW SPACES WITHIN THESE LINES +% +% The token \@IEEEPARstartfont will be globally defined after the first use +% of \IEEEPARstart and will be a font command which creates the big letter +% The first argument is the first letter of the first word and the second +% argument is the rest of the first word(s). +\def\IEEEPARstart#1#2{\par{% +% if this page does not have enough space, break it and lets start +% on a new one +\@IEEEtranneedspace{\IEEEPARstartMINPAGELINES\baselineskip}{\relax}% +% V1.7 move this up here in case user uses \textbf for \IEEEPARstartFONTSTYLE +% which uses command \leavevmode which causes an unwanted \indent to be issued +\noindent +% calculate the desired height of the big letter +% it extends from the top of \IEEEPARstartHEIGHTTEXT in the current font +% down to \IEEEPARstartDROPDEPTH below the current baseline +\settoheight{\@IEEEtrantmpdimenA}{\IEEEPARstartHEIGHTTEXT}% +\addtolength{\@IEEEtrantmpdimenA}{\IEEEPARstartDROPDEPTH}% +% extract the name of the current font in bold +% and place it in \@IEEEPARstartFONTNAME +\def\@IEEEPARstartGETFIRSTWORD##1 ##2\relax{##1}% +{\IEEEPARstartFONTSTYLE{\selectfont\edef\@IEEEPARstartFONTNAMESPACE{\fontname\font\space}% +\xdef\@IEEEPARstartFONTNAME{\expandafter\@IEEEPARstartGETFIRSTWORD\@IEEEPARstartFONTNAMESPACE\relax}}}% +% define a font based on this name with a point size equal to the desired +% height of the drop letter +\font\@IEEEPARstartsubfont\@IEEEPARstartFONTNAME\space at \@IEEEtrantmpdimenA\relax% +% save this value as a counter (integer) value (sp points) +\@IEEEtrantmpcountA=\@IEEEtrantmpdimenA% +% now get the height of the actual letter produced by this font size +\settoheight{\@IEEEtrantmpdimenB}{\@IEEEPARstartsubfont\IEEEPARstartCAPSTYLE{#1}}% +% If something bogus happens like the first argument is empty or the +% current font is strange, do not allow a zero height. +\ifdim\@IEEEtrantmpdimenB=0pt\relax% +\typeout{** WARNING: IEEEPARstart drop letter has zero height! (line \the\inputlineno)}% +\typeout{ Forcing the drop letter font size to 10pt.}% +\@IEEEtrantmpdimenB=10pt% +\fi% +% and store it as a counter +\@IEEEtrantmpcountB=\@IEEEtrantmpdimenB% +% Since a font size doesn't exactly correspond to the height of the capital +% letters in that font, the actual height of the letter, \@IEEEtrantmpcountB, +% will be less than that desired, \@IEEEtrantmpcountA +% we need to raise the font size, \@IEEEtrantmpdimenA +% by \@IEEEtrantmpcountA / \@IEEEtrantmpcountB +% But, TeX doesn't have floating point division, so we have to use integer +% division. Hence the use of the counters. +% We need to reduce the denominator so that the loss of the remainder will +% have minimal affect on the accuracy of the result +\divide\@IEEEtrantmpcountB by 200% +\divide\@IEEEtrantmpcountA by \@IEEEtrantmpcountB% +% Then reequalize things when we use TeX's ability to multiply by +% floating point values +\@IEEEtrantmpdimenB=0.005\@IEEEtrantmpdimenA% +\multiply\@IEEEtrantmpdimenB by \@IEEEtrantmpcountA% +% \@IEEEPARstartfont is globaly set to the calculated font of the big letter +% We need to carry this out of the local calculation area to to create the +% big letter. +\global\font\@IEEEPARstartfont\@IEEEPARstartFONTNAME\space at \@IEEEtrantmpdimenB% +% Now set \@IEEEtrantmpdimenA to the width of the big letter +% We need to carry this out of the local calculation area to set the +% hanging indent +\settowidth{\global\@IEEEtrantmpdimenA}{\@IEEEPARstartfont +\IEEEPARstartCAPSTYLE{#1\IEEEPARstartITLCORRECT}}}% +% end of the isolated calculation environment +\global\IEEEPARstartletwidth\@IEEEtrantmpdimenA\relax% +% add in the extra clearance we want +\advance\@IEEEtrantmpdimenA by \IEEEPARstartSEP\relax% +% add in the optional offset +\advance\@IEEEtrantmpdimenA by \IEEEPARstartHOFFSET\relax% +% V1.7 don't allow negative offsets to produce negative hanging indents +\@IEEEtrantmpdimenB\@IEEEtrantmpdimenA +\ifnum\@IEEEtrantmpdimenB < 0 \@IEEEtrantmpdimenB 0pt\fi +% \@IEEEtrantmpdimenA has the width of the big letter plus the +% separation space and \@IEEEPARstartfont is the font we need to use +% Now, we make the letter and issue the hanging indent command +% The letter is placed in a box of zero width and height so that other +% text won't be displaced by it. +\hangindent\@IEEEtrantmpdimenB\hangafter=-\IEEEPARstartDROPLINES% +\makebox[0pt][l]{\hspace{-\@IEEEtrantmpdimenA}% +\raisebox{-\IEEEPARstartDROPDEPTH}[0pt][0pt]{\hspace{\IEEEPARstartHOFFSET}% +\@IEEEPARstartfont\IEEEPARstartCAPSTYLE{#1\IEEEPARstartITLCORRECT}% +\hspace{\IEEEPARstartSEP}}}% +{\IEEEPARstartWORDFONTSTYLE{\IEEEPARstartWORDCAPSTYLE{\selectfont#2}}}} + + + + + + +% determines if the space remaining on a given page is equal to or greater +% than the specified space of argument one +% if not, execute argument two (only if the remaining space is greater than zero) +% and issue a \newpage +% +% example: \@IEEEtranneedspace{2in}{\vfill} +% +% Does not take into consideration rubber shrinkage, so it tends to +% be overly cautious +% Based on an example posted by Donald Arseneau +% Note this macro uses \@IEEEtrantmpdimenB internally for calculations, +% so DO NOT PASS \@IEEEtrantmpdimenB to this routine +% if you need a dimen register, import with \@IEEEtrantmpdimenA instead +\def\@IEEEtranneedspace#1#2{\penalty-100\begingroup%shield temp variable +\@IEEEtrantmpdimenB\pagegoal\advance\@IEEEtrantmpdimenB-\pagetotal% space left +\ifdim #1>\@IEEEtrantmpdimenB\relax% not enough space left +\ifdim\@IEEEtrantmpdimenB>\z@\relax #2\fi% +\newpage% +\fi\endgroup} + + + + + + +% Provide support for the control entries of IEEEtran.bst V1.00 and later. +% V1.7 optional argument allows for a different aux file to be specified in +% order to handle multiple bibliographies. For example, with multibib.sty: +% \newcites{sec}{Secondary Literature} +% \bstctlcite[@auxoutsec]{BSTcontrolhak} +% V1.7 I see no need for \providecommand here. +\def\bstctlcite{\@ifnextchar[{\@bstctlcite}{\@bstctlcite[@auxout]}} +\def\@bstctlcite[#1]#2{\@bsphack + \@for\@citeb:=#2\do{% + \edef\@citeb{\expandafter\@firstofone\@citeb}% + \if@filesw\immediate\write\csname #1\endcsname{\string\citation{\@citeb}}\fi}% + \@esphack} + + + +% need a backslash character for typeout output +{\catcode`\|=0 \catcode`\\=12 +|xdef|@IEEEbackslash{\}} + + +% hook to allow easy disabling of all legacy warnings +\def\@IEEElegacywarn#1#2{\typeout{** ATTENTION: \@IEEEbackslash #1 is depreciated (line \the\inputlineno). +Use \@IEEEbackslash #2 instead.}} + +% provide for legacy commands +\def\PARstart{\@IEEElegacywarn{PARstart}{IEEEPARstart}\IEEEPARstart} + +% provide for legacy IED commands/lengths when possible +\let\labelindent\IEEElabelindent +\def\calcleftmargin{\@IEEElegacywarn{calcleftmargin}{IEEEcalcleftmargin}\IEEEcalcleftmargin} +\def\setlabelwidth{\@IEEElegacywarn{setlabelwidth}{IEEEsetlabelwidth}\IEEEsetlabelwidth} +\def\usemathlabelsep{\@IEEElegacywarn{usemathlabelsep}{IEEEusemathlabelsep}\IEEEusemathlabelsep} +\def\iedlabeljustifyc{\@IEEElegacywarn{iedlabeljustifyc}{IEEEiedlabeljustifyc}\IEEEiedlabeljustifyc} +\def\iedlabeljustifyl{\@IEEElegacywarn{iedlabeljustifyl}{IEEEiedlabeljustifyl}\IEEEiedlabeljustifyl} +\def\iedlabeljustifyr{\@IEEElegacywarn{iedlabeljustifyr}{IEEEiedlabeljustifyr}\IEEEiedlabeljustifyr} + + +\endinput +%%%%%%%%%%%%%%%%%%%%%%%%%% End of IEEEtrantools.sty %%%%%%%%%%%%%%%%%%%%%% +% That's all folks! + diff --git a/submodules/AppCenter-sdk/Vendor/PLCrashReporter/Documentation/Crash Log Format/PLCrashReportFormat.tex b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/Documentation/Crash Log Format/PLCrashReportFormat.tex new file mode 100644 index 0000000000..ebc4957e91 --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/Documentation/Crash Log Format/PLCrashReportFormat.tex @@ -0,0 +1,49 @@ +\documentclass[11pt]{article} +\usepackage{geometry} % See geometry.pdf to learn the layout options. There are lots. +\geometry{letterpaper} % ... or a4paper or a5paper or ... +%\geometry{landscape} % Activate for for rotated page geometry +%\usepackage[parfill]{parskip} % Activate to begin paragraphs with an empty line rather than an indent +\usepackage{graphicx} +\usepackage{amssymb} +\usepackage{epstopdf} +\usepackage[retainorgcmds]{IEEEtrantools} % So we can use IEEE's handy LaTeX tools + +\usepackage{color} +\definecolor{SubtleURL}{cmyk}{1,0,0,1} + +\usepackage[pdftitle={Landon Fuller}, + pdfauthor={Plausible Labs Cooperative, Inc.}, + pdfsubject={Plausible Crash Log Format}, + pdfkeywords={}, + colorlinks=true, + linkcolor=SubtleURL, + citecolor=SubtleURL]{hyperref} +\DeclareGraphicsRule{.tif}{png}{.png}{`convert #1 `dirname #1`/`basename #1 .tif`.png} + +\title{Crash Log Format (DRAFT)} +\author{Plausible Labs Cooperative, Inc.} +%\date{} % Activate to display a given date or no date + +% Terms +\newcommand{\term}{\emph} + +% Spacing +\parskip .5em +\parindent 0em + +\begin{document} +\maketitle +\tableofcontents + +\section{Introduction} + +\section{Notational Conventions} + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119\cite{RFC2119} + +\begin{thebibliography}{99} +\bibitem{RFC2119} Bradner, S., ``Key words for use in RFCs to Indicate Requirement Levels'', BCP 14, RFC 2119, March 1997. +\end{thebibliography} + + +\end{document} \ No newline at end of file diff --git a/submodules/AppCenter-sdk/Vendor/PLCrashReporter/Doxyfile b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/Doxyfile new file mode 100644 index 0000000000..ab34681dcd --- /dev/null +++ b/submodules/AppCenter-sdk/Vendor/PLCrashReporter/Doxyfile @@ -0,0 +1,2549 @@ +# Doxyfile 1.8.18 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. +# +# All text after a single hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the configuration +# file that follow. The default is UTF-8 which is also the encoding used for all +# text before the first occurrence of this tag. Doxygen uses libiconv (or the +# iconv built into libc) for the transcoding. See +# https://www.gnu.org/software/libiconv/ for the list of possible encodings. +# The default value is: UTF-8. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by +# double-quotes, unless you are using Doxywizard) that should identify the +# project for which the documentation is generated. This name is used in the +# title of most generated pages and in a few other places. +# The default value is: My Project. + +PROJECT_NAME = "Plausible CrashReporter" + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. This +# could be handy for archiving the generated documentation or if some version +# control system is used. + +PROJECT_NUMBER = + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer a +# quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = + +# With the PROJECT_LOGO tag one can specify a logo or an icon that is included +# in the documentation. The maximum height of the logo should not exceed 55 +# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy +# the logo to the output directory. + +PROJECT_LOGO = + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path +# into which the generated documentation will be written. If a relative path is +# entered, it will be relative to the location where doxygen was started. If +# left blank the current directory will be used. + +OUTPUT_DIRECTORY = + +# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- +# directories (in 2 levels) under the output directory of each output format and +# will distribute the generated files over these directories. Enabling this +# option can be useful when feeding doxygen a huge amount of source files, where +# putting all generated files in the same directory would otherwise causes +# performance problems for the file system. +# The default value is: NO. + +CREATE_SUBDIRS = NO + +# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII +# characters to appear in the names of generated files. If set to NO, non-ASCII +# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode +# U+3044. +# The default value is: NO. + +ALLOW_UNICODE_NAMES = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, +# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), +# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, +# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), +# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, +# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, +# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, +# Ukrainian and Vietnamese. +# The default value is: English. + +OUTPUT_LANGUAGE = English + +# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all generated output in the proper direction. +# Possible values are: None, LTR, RTL and Context. +# The default value is: None. + +OUTPUT_TEXT_DIRECTION = None + +# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member +# descriptions after the members that are listed in the file and class +# documentation (similar to Javadoc). Set to NO to disable this. +# The default value is: YES. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief +# description of a member or function before the detailed description +# +# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. +# The default value is: YES. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator that is +# used to form the text in various listings. Each string in this list, if found +# as the leading text of the brief description, will be stripped from the text +# and the result, after processing the whole list, is used as the annotated +# text. Otherwise, the brief description is used as-is. If left blank, the +# following values are used ($name is automatically replaced with the name of +# the entity):The $name class, The $name widget, The $name file, is, provides, +# specifies, contains, represents, a, an and the. + +ABBREVIATE_BRIEF = + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# doxygen will generate a detailed section even if there is only a brief +# description. +# The default value is: NO. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. +# The default value is: NO. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path +# before files name in the file list and in the header files. If set to NO the +# shortest path that makes the file name unique will be used +# The default value is: YES. + +FULL_PATH_NAMES = YES + +# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. +# Stripping is only done if one of the specified strings matches the left-hand +# part of the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the path to +# strip. +# +# Note that you can specify absolute paths here, but also relative paths, which +# will be relative from the directory where doxygen is started. +# This tag requires that the tag FULL_PATH_NAMES is set to YES. + +STRIP_FROM_PATH = + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the +# path mentioned in the documentation of a class, which tells the reader which +# header file to include in order to use a class. If left blank only the name of +# the header file containing the class definition is used. Otherwise one should +# specify the list of include paths that are normally passed to the compiler +# using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but +# less readable) file names. This can be useful is your file systems doesn't +# support long names like on DOS, Mac, or CD-ROM. +# The default value is: NO. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the +# first line (until the first dot) of a Javadoc-style comment as the brief +# description. If set to NO, the Javadoc-style will behave just like regular Qt- +# style comments (thus requiring an explicit @brief command for a brief +# description.) +# The default value is: NO. + +JAVADOC_AUTOBRIEF = YES + +# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line +# such as +# /*************** +# as being the beginning of a Javadoc-style comment "banner". If set to NO, the +# Javadoc-style will behave just like regular comments and it will not be +# interpreted by doxygen. +# The default value is: NO. + +JAVADOC_BANNER = NO + +# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first +# line (until the first dot) of a Qt-style comment as the brief description. If +# set to NO, the Qt-style will behave just like regular Qt-style comments (thus +# requiring an explicit \brief command for a brief description.) +# The default value is: NO. + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a +# multi-line C++ special comment block (i.e. a block of //! or /// comments) as +# a brief description. This used to be the default behavior. The new default is +# to treat a multi-line C++ comment block as a detailed description. Set this +# tag to YES if you prefer the old behavior instead. +# +# Note that setting this tag to YES also means that rational rose comments are +# not recognized any more. +# The default value is: NO. + +MULTILINE_CPP_IS_BRIEF = NO + +# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the +# documentation from any documented member that it re-implements. +# The default value is: YES. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new +# page for each member. If set to NO, the documentation of a member will be part +# of the file/class/namespace that contains it. +# The default value is: NO. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen +# uses this value to replace tabs by spaces in code fragments. +# Minimum value: 1, maximum value: 16, default value: 4. + +TAB_SIZE = 8 + +# This tag can be used to specify a number of aliases that act as commands in +# the documentation. An alias has the form: +# name=value +# For example adding +# "sideeffect=@par Side Effects:\n" +# will allow you to put the command \sideeffect (or @sideeffect) in the +# documentation, which will result in a user-defined paragraph with heading +# "Side Effects:". You can put \n's in the value part of an alias to insert +# newlines (in the resulting output). You can put ^^ in the value part of an +# alias to insert a newline as if a physical newline was in the original file. +# When you need a literal { or } or , in the value part of an alias you have to +# escape them by means of a backslash (\), this can lead to conflicts with the +# commands \{ and \} for these it is advised to use the version @{ and @} or use +# a double escape (\\{ and \\}) + +ALIASES = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources +# only. Doxygen will then generate output that is more tailored for C. For +# instance, some of the names that are used will be different. The list of all +# members will be omitted, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or +# Python sources only. Doxygen will then generate output that is more tailored +# for that language. For instance, namespaces will be presented as packages, +# qualified scopes will look different, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources. Doxygen will then generate output that is tailored for Fortran. +# The default value is: NO. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for VHDL. +# The default value is: NO. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice +# sources only. Doxygen will then generate output that is more tailored for that +# language. For instance, namespaces will be presented as modules, types will be +# separated into more groups, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_SLICE = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given +# extension. Doxygen has a built-in mapping, but you can override or extend it +# using this tag. The format is ext=language, where ext is a file extension, and +# language is one of the parsers supported by doxygen: IDL, Java, JavaScript, +# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice, VHDL, +# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: +# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser +# tries to guess whether the code is fixed or free formatted code, this is the +# default for Fortran type files). For instance to make doxygen treat .inc files +# as Fortran files (default is PHP), and .f files as C (default is Fortran), +# use: inc=Fortran f=C. +# +# Note: For files without extension you can use no_extension as a placeholder. +# +# Note that for custom extensions you also need to set FILE_PATTERNS otherwise +# the files are not read by doxygen. + +EXTENSION_MAPPING = + +# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments +# according to the Markdown format, which allows for more readable +# documentation. See https://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by doxygen, so you can +# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in +# case of backward compatibilities issues. +# The default value is: YES. + +MARKDOWN_SUPPORT = YES + +# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up +# to that level are automatically included in the table of contents, even if +# they do not have an id attribute. +# Note: This feature currently applies only to Markdown headings. +# Minimum value: 0, maximum value: 99, default value: 5. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +TOC_INCLUDE_HEADINGS = 5 + +# When enabled doxygen tries to link words that correspond to documented +# classes, or namespaces to their corresponding documentation. Such a link can +# be prevented in individual cases by putting a % sign in front of the word or +# globally by setting AUTOLINK_SUPPORT to NO. +# The default value is: YES. + +AUTOLINK_SUPPORT = YES + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should set this +# tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); +# versus func(std::string) {}). This also make the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. +# The default value is: NO. + +BUILTIN_STL_SUPPORT = NO + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. +# The default value is: NO. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: +# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen +# will parse them like normal C++ but will assume all classes use public instead +# of private inheritance when no explicit protection keyword is present. +# The default value is: NO. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate +# getter and setter methods for a property. Setting this option to YES will make +# doxygen to replace the get and set methods by a property in the documentation. +# This will only work if the methods are indeed getting or setting a simple +# type. If this is not the case, or you want to show the methods anyway, you +# should set this option to NO. +# The default value is: YES. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. +# The default value is: NO. + +DISTRIBUTE_GROUP_DOC = NO + +# If one adds a struct or class to a group and this option is enabled, then also +# any nested class or struct is added to the same group. By default this option +# is disabled and one has to add nested compounds explicitly via \ingroup. +# The default value is: NO. + +GROUP_NESTED_COMPOUNDS = NO + +# Set the SUBGROUPING tag to YES to allow class member groups of the same type +# (for instance a group of public functions) to be put as a subgroup of that +# type (e.g. under the Public Functions section). Set it to NO to prevent +# subgrouping. Alternatively, this can be done per class using the +# \nosubgrouping command. +# The default value is: YES. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions +# are shown inside the group in which they are included (e.g. using \ingroup) +# instead of on a separate page (for HTML and Man pages) or section (for LaTeX +# and RTF). +# +# Note that this feature does not work in combination with +# SEPARATE_MEMBER_PAGES. +# The default value is: NO. + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions +# with only public data fields or simple typedef fields will be shown inline in +# the documentation of the scope in which they are defined (i.e. file, +# namespace, or group documentation), provided this scope is documented. If set +# to NO, structs, classes, and unions are shown on a separate page (for HTML and +# Man pages) or section (for LaTeX and RTF). +# The default value is: NO. + +INLINE_SIMPLE_STRUCTS = NO + +# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or +# enum is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically be +# useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. +# The default value is: NO. + +TYPEDEF_HIDES_STRUCT = NO + +# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This +# cache is used to resolve symbols given their name and scope. Since this can be +# an expensive process and often the same symbol appears multiple times in the +# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small +# doxygen will become slower. If the cache is too large, memory is wasted. The +# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range +# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 +# symbols. At the end of a run doxygen will report the cache usage and suggest +# the optimal cache size from a speed point of view. +# Minimum value: 0, maximum value: 9, default value: 0. + +LOOKUP_CACHE_SIZE = 0 + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in +# documentation are documented, even if no documentation was available. Private +# class members and static file members will be hidden unless the +# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. +# Note: This will also disable the warnings about undocumented members that are +# normally produced when WARNINGS is set to YES. +# The default value is: NO. + +EXTRACT_ALL = NO + +# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will +# be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIVATE = NO + +# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual +# methods of a class will be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIV_VIRTUAL = NO + +# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal +# scope will be included in the documentation. +# The default value is: NO. + +EXTRACT_PACKAGE = NO + +# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be +# included in the documentation. +# The default value is: NO. + +EXTRACT_STATIC = NO + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined +# locally in source files will be included in the documentation. If set to NO, +# only classes defined in header files are included. Does not have any effect +# for Java sources. +# The default value is: YES. + +EXTRACT_LOCAL_CLASSES = YES + +# This flag is only useful for Objective-C code. If set to YES, local methods, +# which are defined in the implementation section but not in the interface are +# included in the documentation. If set to NO, only methods in the interface are +# included. +# The default value is: NO. + +EXTRACT_LOCAL_METHODS = NO + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base name of +# the file that contains the anonymous namespace. By default anonymous namespace +# are hidden. +# The default value is: NO. + +EXTRACT_ANON_NSPACES = NO + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all +# undocumented members inside documented classes or files. If set to NO these +# members will be included in the various overviews, but no documentation +# section is generated. This option has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_MEMBERS = YES + +# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. If set +# to NO, these classes will be included in the various overviews. This option +# has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_CLASSES = YES + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend +# declarations. If set to NO, these declarations will be included in the +# documentation. +# The default value is: NO. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any +# documentation blocks found inside the body of a function. If set to NO, these +# blocks will be appended to the function's detailed documentation block. +# The default value is: NO. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation that is typed after a +# \internal command is included. If the tag is set to NO then the documentation +# will be excluded. Set it to YES to include the internal documentation. +# The default value is: NO. + +INTERNAL_DOCS = NO + +# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file +# names in lower-case letters. If set to YES, upper-case letters are also +# allowed. This is useful if you have classes or files whose names only differ +# in case and if your file system supports case sensitive file names. Windows +# (including Cygwin) ands Mac users are advised to set this option to NO. +# The default value is: system dependent. + +CASE_SENSE_NAMES = NO + +# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with +# their full class and namespace scopes in the documentation. If set to YES, the +# scope will be hidden. +# The default value is: NO. + +HIDE_SCOPE_NAMES = NO + +# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will +# append additional text to a page's title, such as Class Reference. If set to +# YES the compound reference will be hidden. +# The default value is: NO. + +HIDE_COMPOUND_REFERENCE= NO + +# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of +# the files that are included by a file in the documentation of that file. +# The default value is: YES. + +SHOW_INCLUDE_FILES = YES + +# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each +# grouped member an include statement to the documentation, telling the reader +# which file to include in order to use the member. +# The default value is: NO. + +SHOW_GROUPED_MEMB_INC = NO + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include +# files with double quotes in the documentation rather than with sharp brackets. +# The default value is: NO. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the +# documentation for inline members. +# The default value is: YES. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the +# (detailed) documentation of file and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. +# The default value is: YES. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief +# descriptions of file, namespace and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. Note that +# this will also influence the order of the classes in the class list. +# The default value is: NO. + +SORT_BRIEF_DOCS = NO + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the +# (brief and detailed) documentation of class members so that constructors and +# destructors are listed first. If set to NO the constructors will appear in the +# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. +# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief +# member documentation. +# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting +# detailed member documentation. +# The default value is: NO. + +SORT_MEMBERS_CTORS_1ST = NO + +# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy +# of group names into alphabetical order. If set to NO the group names will +# appear in their defined order. +# The default value is: NO. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by +# fully-qualified names, including namespaces. If set to NO, the class list will +# be sorted only by class name, not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the alphabetical +# list. +# The default value is: NO. + +SORT_BY_SCOPE_NAME = NO + +# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper +# type resolution of all parameters of a function it will reject a match between +# the prototype and the implementation of a member function even if there is +# only one candidate or it is obvious which candidate to choose by doing a +# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still +# accept a match between prototype and implementation in such cases. +# The default value is: NO. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo +# list. This list is created by putting \todo commands in the documentation. +# The default value is: YES. + +GENERATE_TODOLIST = NO + +# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test +# list. This list is created by putting \test commands in the documentation. +# The default value is: YES. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug +# list. This list is created by putting \bug commands in the documentation. +# The default value is: YES. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) +# the deprecated list. This list is created by putting \deprecated commands in +# the documentation. +# The default value is: YES. + +GENERATE_DEPRECATEDLIST= YES + +# The ENABLED_SECTIONS tag can be used to enable conditional documentation +# sections, marked by \if ... \endif and \cond +# ... \endcond blocks. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the +# initial value of a variable or macro / define can have for it to appear in the +# documentation. If the initializer consists of more lines than specified here +# it will be hidden. Use a value of 0 to hide initializers completely. The +# appearance of the value of individual variables and macros / defines can be +# controlled using \showinitializer or \hideinitializer command in the +# documentation regardless of this setting. +# Minimum value: 0, maximum value: 10000, default value: 30. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at +# the bottom of the documentation of classes and structs. If set to YES, the +# list will mention the files that were used to generate the documentation. +# The default value is: YES. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This +# will remove the Files entry from the Quick Index and from the Folder Tree View +# (if specified). +# The default value is: YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces +# page. This will remove the Namespaces entry from the Quick Index and from the +# Folder Tree View (if specified). +# The default value is: YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command command input-file, where command is the value of the +# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided +# by doxygen. Whatever the program writes to standard output is used as the file +# version. For an example see the documentation. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents doxygen's defaults, run doxygen with the -l option. You can +# optionally specify a file name after the option, if omitted DoxygenLayout.xml +# will be used as the name of the layout file. +# +# Note that if you run doxygen from a directory containing a file called +# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE +# tag is left empty. + +LAYOUT_FILE = + +# The CITE_BIB_FILES tag can be used to specify one or more bib files containing +# the reference definitions. This must be a list of .bib files. The .bib +# extension is automatically appended if omitted. This requires the bibtex tool +# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info. +# For LaTeX the style of the bibliography can be controlled using +# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the +# search path. See also \cite for info how to create references. + +CITE_BIB_FILES = + +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated to +# standard output by doxygen. If QUIET is set to YES this implies that the +# messages are off. +# The default value is: NO. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES +# this implies that the warnings are on. +# +# Tip: Turn warnings on while writing the documentation. +# The default value is: YES. + +WARNINGS = YES + +# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate +# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: YES. + +WARN_IF_UNDOCUMENTED = YES + +# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as not documenting some parameters +# in a documented function, or documenting parameters that don't exist or using +# markup commands wrongly. +# The default value is: YES. + +WARN_IF_DOC_ERROR = YES + +# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that +# are documented, but have no documentation for their parameters or return +# value. If set to NO, doxygen will only warn about wrong or incomplete +# parameter documentation, but not about the absence of documentation. If +# EXTRACT_ALL is set to YES then this flag will automatically be disabled. +# The default value is: NO. + +WARN_NO_PARAMDOC = NO + +# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when +# a warning is encountered. +# The default value is: NO. + +WARN_AS_ERROR = NO + +# The WARN_FORMAT tag determines the format of the warning messages that doxygen +# can produce. The string should contain the $file, $line, and $text tags, which +# will be replaced by the file and line number from which the warning originated +# and the warning text. Optionally the format may contain $version, which will +# be replaced by the version of the file (if it could be obtained via +# FILE_VERSION_FILTER) +# The default value is: $file:$line: $text. + +WARN_FORMAT = "$file:$line: $text" + +# The WARN_LOGFILE tag can be used to specify a file to which warning and error +# messages should be written. If left blank the output is written to standard +# error (stderr). + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag is used to specify the files and/or directories that contain +# documented source files. You may enter file names like myfile.cpp or +# directories like /usr/src/myproject. Separate the files or directories with +# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING +# Note: If this tag is empty the current directory is searched. + +INPUT = Source + +# This tag can be used to specify the character encoding of the source files +# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses +# libiconv (or the iconv built into libc) for the transcoding. See the libiconv +# documentation (see: https://www.gnu.org/software/libiconv/) for the list of +# possible encodings. +# The default value is: UTF-8. + +INPUT_ENCODING = UTF-8 + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and +# *.h) to filter out the source-files in the directories. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# read by doxygen. +# +# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, +# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, +# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, +# *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C comment), +# *.doc (to be provided as doxygen C comment), *.txt (to be provided as doxygen +# C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, +# *.vhdl, *.ucf, *.qsf and *.ice. + +FILE_PATTERNS = *.c \ + *.cc \ + *.cxx \ + *.cpp \ + *.c++ \ + *.java \ + *.ii \ + *.ixx \ + *.ipp \ + *.i++ \ + *.inl \ + *.idl \ + *.ddl \ + *.odl \ + *.h \ + *.hh \ + *.hxx \ + *.hpp \ + *.h++ \ + *.cs \ + *.d \ + *.php \ + *.php4 \ + *.php5 \ + *.phtml \ + *.inc \ + *.m \ + *.markdown \ + *.md \ + *.mm \ + *.dox \ + *.doc \ + *.txt \ + *.py \ + *.pyw \ + *.f90 \ + *.f95 \ + *.f03 \ + *.f08 \ + *.f18 \ + *.f \ + *.for \ + *.vhd \ + *.vhdl \ + *.ucf \ + *.qsf \ + *.ice + +# The RECURSIVE tag can be used to specify whether or not subdirectories should +# be searched for input files as well. +# The default value is: NO. + +RECURSIVE = YES + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# +# Note that relative paths are relative to the directory from which doxygen is +# run. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. +# The default value is: NO. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories for example use the pattern */test/* + +EXCLUDE_PATTERNS = */.svn/* \ + *Tests.m \ + *Tests.mm \ + *tests.mm \ + *Mock* + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# AClass::ANamespace, ANamespace::*Test +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories use the pattern */test/* + +EXCLUDE_SYMBOLS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or directories +# that contain example code fragments that are included (see the \include +# command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank all +# files are included. + +EXAMPLE_PATTERNS = + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude commands +# irrespective of the value of the RECURSIVE tag. +# The default value is: NO. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or directories +# that contain images that are to be included in the documentation (see the +# \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command: +# +# +# +# where is the value of the INPUT_FILTER tag, and is the +# name of an input file. Doxygen will then use the output that the filter +# program writes to standard output. If FILTER_PATTERNS is specified, this tag +# will be ignored. +# +# Note that the filter must not add or remove lines; it is applied before the +# code is scanned, but not when the output code is generated. If lines are added +# or removed, the anchors will not be placed correctly. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. The filters are a list of the form: pattern=filter +# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how +# filters are used. If the FILTER_PATTERNS tag is empty or if none of the +# patterns match the file name, INPUT_FILTER is applied. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will also be used to filter the input files that are used for +# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). +# The default value is: NO. + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and +# it is also possible to disable source filtering for a specific pattern using +# *.ext= (so without naming a filter). +# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. + +FILTER_SOURCE_PATTERNS = + +# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want to reuse the introduction page also for the doxygen output. + +USE_MDFILE_AS_MAINPAGE = + +#--------------------------------------------------------------------------- +# Configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will be +# generated. Documented entities will be cross-referenced with these sources. +# +# Note: To get rid of all source code in the generated output, make sure that +# also VERBATIM_HEADERS is set to NO. +# The default value is: NO. + +SOURCE_BROWSER = NO + +# Setting the INLINE_SOURCES tag to YES will include the body of functions, +# classes and enums directly into the documentation. +# The default value is: NO. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any +# special comment blocks from generated source code fragments. Normal C, C++ and +# Fortran comments will always remain visible. +# The default value is: YES. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES then for each documented +# entity all documented functions referencing it will be listed. +# The default value is: NO. + +REFERENCED_BY_RELATION = NO + +# If the REFERENCES_RELATION tag is set to YES then for each documented function +# all documented entities called/used by that function will be listed. +# The default value is: NO. + +REFERENCES_RELATION = NO + +# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set +# to YES then the hyperlinks from functions in REFERENCES_RELATION and +# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will +# link to the documentation. +# The default value is: YES. + +REFERENCES_LINK_SOURCE = YES + +# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the +# source code will show a tooltip with additional information such as prototype, +# brief description and links to the definition and documentation. Since this +# will make the HTML file larger and loading of large files a bit slower, you +# can opt to disable this feature. +# The default value is: YES. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +SOURCE_TOOLTIPS = YES + +# If the USE_HTAGS tag is set to YES then the references to source code will +# point to the HTML generated by the htags(1) tool instead of doxygen built-in +# source browser. The htags tool is part of GNU's global source tagging system +# (see https://www.gnu.org/software/global/global.html). You will need version +# 4.8.6 or higher. +# +# To use it do the following: +# - Install the latest version of global +# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file +# - Make sure the INPUT points to the root of the source tree +# - Run doxygen as normal +# +# Doxygen will invoke htags (and that will in turn invoke gtags), so these +# tools must be available from the command line (i.e. in the search path). +# +# The result: instead of the source browser generated by doxygen, the links to +# source code will now point to the output of htags. +# The default value is: NO. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a +# verbatim copy of the header file for each class for which an include is +# specified. Set to NO to disable this. +# See also: Section \class. +# The default value is: YES. + +VERBATIM_HEADERS = NO + +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all +# compounds will be generated. Enable this if the project contains a lot of +# classes, structs, unions or interfaces. +# The default value is: YES. + +ALPHABETICAL_INDEX = NO + +# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in +# which the alphabetical index list will be split. +# Minimum value: 1, maximum value: 20, default value: 5. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +COLS_IN_ALPHA_INDEX = 5 + +# In case all classes in a project start with a common prefix, all classes will +# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag +# can be used to specify a prefix (or a list of prefixes) that should be ignored +# while generating the index headers. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output +# The default value is: YES. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_OUTPUT = Documentation/API + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each +# generated HTML page (for example: .htm, .php, .asp). +# The default value is: .html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a user-defined HTML header file for +# each generated HTML page. If the tag is left blank doxygen will generate a +# standard header. +# +# To get valid HTML the header file that includes any scripts and style sheets +# that doxygen needs, which is dependent on the configuration options used (e.g. +# the setting GENERATE_TREEVIEW). It is highly recommended to start with a +# default header using +# doxygen -w html new_header.html new_footer.html new_stylesheet.css +# YourConfigFile +# and then modify the file new_header.html. See also section "Doxygen usage" +# for information on how to generate the default header that doxygen normally +# uses. +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of doxygen. For a description +# of the possible markers and block names see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each +# generated HTML page. If the tag is left blank doxygen will generate a standard +# footer. See HTML_HEADER for more information on how to generate a default +# footer and what special commands can be used inside the footer. See also +# section "Doxygen usage" for information on how to generate the default footer +# that doxygen normally uses. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style +# sheet that is used by each HTML page. It can be used to fine-tune the look of +# the HTML output. If left blank doxygen will generate a default style sheet. +# See also section "Doxygen usage" for information on how to generate the style +# sheet that doxygen normally uses. +# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as +# it is more robust and this tag (HTML_STYLESHEET) will in the future become +# obsolete. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_STYLESHEET = + +# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined +# cascading style sheets that are included after the standard style sheets +# created by doxygen. Using this option one can overrule certain style aspects. +# This is preferred over using HTML_STYLESHEET since it does not replace the +# standard style sheet and is therefore more robust against future updates. +# Doxygen will copy the style sheet files to the output directory. +# Note: The order of the extra style sheet files is of importance (e.g. the last +# style sheet in the list overrules the setting of the previous ones in the +# list). For an example see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_STYLESHEET = + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that the +# files will be copied as-is; there are no commands or markers available. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_FILES = + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen +# will adjust the colors in the style sheet and background images according to +# this color. Hue is specified as an angle on a colorwheel, see +# https://en.wikipedia.org/wiki/Hue for more information. For instance the value +# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 +# purple, and 360 is red again. +# Minimum value: 0, maximum value: 359, default value: 220. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors +# in the HTML output. For a value of 0 the output will use grayscales only. A +# value of 255 will produce the most vivid colors. +# Minimum value: 0, maximum value: 255, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_SAT = 100 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the +# luminance component of the colors in the HTML output. Values below 100 +# gradually make the output lighter, whereas values above 100 make the output +# darker. The value divided by 100 is the actual gamma applied, so 80 represents +# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not +# change the gamma. +# Minimum value: 40, maximum value: 240, default value: 80. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML +# page will contain the date and time when the page was generated. Setting this +# to YES can help to show when doxygen was last run and thus if the +# documentation is up to date. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_TIMESTAMP = YES + +# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML +# documentation will contain a main index with vertical navigation menus that +# are dynamically created via JavaScript. If disabled, the navigation index will +# consists of multiple levels of tabs that are statically embedded in every HTML +# page. Disable this option to support browsers that do not have JavaScript, +# like the Qt help browser. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_MENUS = YES + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_SECTIONS = NO + +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries +# shown in the various tree structured indices initially; the user can expand +# and collapse entries dynamically later on. Doxygen will expand the tree to +# such a level that at most the specified number of entries are visible (unless +# a fully collapsed tree already exceeds this amount). So setting the number of +# entries 1 will produce a full collapsed tree by default. 0 is a special value +# representing an infinite number of entries and will result in a full expanded +# tree by default. +# Minimum value: 0, maximum value: 9999, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_INDEX_NUM_ENTRIES = 100 + +# If the GENERATE_DOCSET tag is set to YES, additional index files will be +# generated that can be used as input for Apple's Xcode 3 integrated development +# environment (see: https://developer.apple.com/xcode/), introduced with OSX +# 10.5 (Leopard). To create a documentation set, doxygen will generate a +# Makefile in the HTML output directory. Running make will produce the docset in +# that directory and running make install will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at +# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy +# genXcode/_index.html for more information. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_DOCSET = YES + +# This tag determines the name of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# The default value is: Doxygen generated docs. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDNAME = "Plausible Labs" + +# This tag specifies a string that should uniquely identify the documentation +# set bundle. This should be a reverse domain-name style string, e.g. +# com.mycompany.MyDocSet. Doxygen will append .docset to the name. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_BUNDLE_ID = com.plausiblelabs.PLCrashReporter + +# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify +# the documentation publisher. This should be a reverse domain-name style +# string, e.g. com.mycompany.MyDocSet.documentation. +# The default value is: org.doxygen.Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_ID = org.doxygen.Publisher + +# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. +# The default value is: Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_NAME = Publisher + +# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three +# additional HTML index files: index.hhp, index.hhc, and index.hhk. The +# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop +# (see: https://www.microsoft.com/en-us/download/details.aspx?id=21138) on +# Windows. +# +# The HTML Help Workshop contains a compiler that can convert all HTML output +# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML +# files are now used as the Windows 98 help format, and will replace the old +# Windows help format (.hlp) on all Windows platforms in the future. Compressed +# HTML files also contain an index, a table of contents, and you can search for +# words in the documentation. The HTML workshop also contains a viewer for +# compressed HTML files. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_HTMLHELP = NO + +# The CHM_FILE tag can be used to specify the file name of the resulting .chm +# file. You can add a path in front of the file if the result should not be +# written to the html output directory. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_FILE = + +# The HHC_LOCATION tag can be used to specify the location (absolute path +# including file name) of the HTML help compiler (hhc.exe). If non-empty, +# doxygen will try to run the HTML help compiler on the generated index.hhp. +# The file has to be specified with full path. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +HHC_LOCATION = + +# The GENERATE_CHI flag controls if a separate .chi index file is generated +# (YES) or that it should be included in the master .chm file (NO). +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +GENERATE_CHI = NO + +# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc) +# and project file content. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_INDEX_ENCODING = + +# The BINARY_TOC flag controls whether a binary table of contents is generated +# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it +# enables the Previous and Next buttons. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members to +# the table of contents of the HTML help documentation and to the tree view. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +TOC_EXPAND = NO + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that +# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help +# (.qch) of the generated HTML documentation. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify +# the file name of the resulting .qch file. The path specified is relative to +# the HTML output folder. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help +# Project output. For more information please see Qt Help Project / Namespace +# (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace). +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_NAMESPACE = org.doxygen.Project + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt +# Help Project output. For more information please see Qt Help Project / Virtual +# Folders (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual- +# folders). +# The default value is: doc. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_VIRTUAL_FOLDER = doc + +# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom +# filter to add. For more information please see Qt Help Project / Custom +# Filters (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see Qt Help Project / Custom +# Filters (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom- +# filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's filter section matches. Qt Help Project / Filter Attributes (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_SECT_FILTER_ATTRS = + +# The QHG_LOCATION tag can be used to specify the location of Qt's +# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the +# generated .qhp file. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be +# generated, together with the HTML files, they form an Eclipse help plugin. To +# install this plugin and make it available under the help contents menu in +# Eclipse, the contents of the directory containing the HTML and XML files needs +# to be copied into the plugins directory of eclipse. The name of the directory +# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. +# After copying Eclipse needs to be restarted before the help appears. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the Eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have this +# name. Each documentation set should have its own identifier. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. + +ECLIPSE_DOC_ID = org.doxygen.Project + +# If you want full control over the layout of the generated HTML pages it might +# be necessary to disable the index and replace it with your own. The +# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top +# of each HTML page. A value of NO enables the index and the value YES disables +# it. Since the tabs in the index contain the same information as the navigation +# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. If the tag +# value is set to YES, a side panel will be generated containing a tree-like +# index structure (just like the one that is generated for HTML Help). For this +# to work a browser that supports JavaScript, DHTML, CSS and frames is required +# (i.e. any modern browser). Windows users are probably better off using the +# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can +# further fine-tune the look of the index. As an example, the default style +# sheet generated by doxygen has an example that shows how to put an image at +# the root of the tree instead of the PROJECT_NAME. Since the tree basically has +# the same information as the tab index, you could consider setting +# DISABLE_INDEX to YES when enabling this option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_TREEVIEW = NO + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that +# doxygen will group on one line in the generated HTML documentation. +# +# Note that a value of 0 will completely suppress the enum values from appearing +# in the overview section. +# Minimum value: 0, maximum value: 20, default value: 4. +# This tag requires that the tag GENERATE_HTML is set to YES. + +ENUM_VALUES_PER_LINE = 4 + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used +# to set the initial width (in pixels) of the frame in which the tree is shown. +# Minimum value: 0, maximum value: 1500, default value: 250. +# This tag requires that the tag GENERATE_HTML is set to YES. + +TREEVIEW_WIDTH = 250 + +# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to +# external symbols imported via tag files in a separate window. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +EXT_LINKS_IN_WINDOW = NO + +# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg +# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see +# https://inkscape.org) to generate formulas as SVG images instead of PNGs for +# the HTML output. These images will generally look nicer at scaled resolutions. +# Possible values are: png The default and svg Looks nicer but requires the +# pdf2svg tool. +# The default value is: png. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FORMULA_FORMAT = png + +# Use this tag to change the font size of LaTeX formulas included as images in +# the HTML documentation. When you change the font size after a successful +# doxygen run you need to manually remove any form_*.png images from the HTML +# output directory to force them to be regenerated. +# Minimum value: 8, maximum value: 50, default value: 10. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_FONTSIZE = 10 + +# Use the FORMULA_TRANSPARENT tag to determine whether or not the images +# generated for formulas are transparent PNGs. Transparent PNGs are not +# supported properly for IE 6.0, but are supported on all modern browsers. +# +# Note that when changing this option you need to delete any form_*.png files in +# the HTML output directory before the changes have effect. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_TRANSPARENT = YES + +# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands +# to create new LaTeX commands to be used in formulas as building blocks. See +# the section "Including formulas" for details. + +FORMULA_MACROFILE = + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see +# https://www.mathjax.org) which uses client side JavaScript for the rendering +# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX +# installed or if you want to formulas look prettier in the HTML output. When +# enabled you may also need to install MathJax separately and configure the path +# to it using the MATHJAX_RELPATH option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +USE_MATHJAX = NO + +# When MathJax is enabled you can set the default output format to be used for +# the MathJax output. See the MathJax site (see: +# http://docs.mathjax.org/en/latest/output.html) for more details. +# Possible values are: HTML-CSS (which is slower, but has the best +# compatibility), NativeMML (i.e. MathML) and SVG. +# The default value is: HTML-CSS. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_FORMAT = HTML-CSS + +# When MathJax is enabled you need to specify the location relative to the HTML +# output directory using the MATHJAX_RELPATH option. The destination directory +# should contain the MathJax.js script. For instance, if the mathjax directory +# is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax +# Content Delivery Network so you can quickly see the result without installing +# MathJax. However, it is strongly recommended to install a local copy of +# MathJax from https://www.mathjax.org before deployment. +# The default value is: https://cdn.jsdelivr.net/npm/mathjax@2. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest + +# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax +# extension names that should be enabled during MathJax rendering. For example +# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_EXTENSIONS = + +# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces +# of code that will be used on startup of the MathJax code. See the MathJax site +# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an +# example see the documentation. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_CODEFILE = + +# When the SEARCHENGINE tag is enabled doxygen will generate a search box for +# the HTML output. The underlying search engine uses javascript and DHTML and +# should work on any modern browser. Note that when using HTML help +# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) +# there is already a search function so this one should typically be disabled. +# For large projects the javascript based search engine can be slow, then +# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to +# search using the keyboard; to jump to the search box use + S +# (what the is depends on the OS and browser, but it is typically +# , /